From e74812542e2370ccc4b54b042d4c626875915f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 1 Jul 2023 11:34:13 +0200 Subject: [PATCH 0001/1248] Fix flag for MSVS --- CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index fb193b230..8b0031e66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -250,6 +250,7 @@ if(MINGW OR MSVC) # Suppress warnings add_definitions(-D_CRT_SECURE_NO_WARNINGS) add_definitions(-D_SCL_SECURE_NO_WARNINGS) + add_definitions(-D_SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING) # Fix gtest and gmock build set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj") From 57bb011468c2487a93641a4f6bbc9a11126866c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 20 Jul 2023 20:34:00 +0200 Subject: [PATCH 0002/1248] - Do not allow 0 or 1 teams - Do not allow player to set only 1 team in GUI --- client/lobby/RandomMapTab.cpp | 37 +++++++++++++++++++++++++++++++++-- client/lobby/RandomMapTab.h | 2 ++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index d2f55f820..1d63c04d2 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -190,6 +190,7 @@ void RandomMapTab::updateMapInfoByHost() { player.team = team; occupiedTeams.insert(team); + break; //First assigned team is ok } } } @@ -206,8 +207,14 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { playerCountAllowed.insert(i); compCountAllowed.insert(i); - playerTeamsAllowed.insert(i); - compTeamsAllowed.insert(i); + if (i >= 2) + { + playerTeamsAllowed.insert(i); + } + if (i >= 1) + { + compTeamsAllowed.insert(i); + } } auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) @@ -514,6 +521,25 @@ void TemplatesDropBox::setTemplate(const CRmgTemplate * tmpl) GH.windows().popWindows(1); } +void TeamAlignmentsWidget::checkTeamCount() +{ + //Do not allow to select one team only + std::set teams; + for (int plId = 0; plId < players.size(); ++plId) + { + teams.insert(TeamID(players[plId]->getSelected())); + } + if (teams.size() < 2) + { + //Do not let player close the window + buttonOk->block(true); + } + else + { + buttonOk->block(false); + } +} + TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { @@ -580,6 +606,10 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): { button->addOverlay(nullptr); } + button->addCallback([this](bool) + { + checkTeamCount(); + }); } })); @@ -598,4 +628,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): else players.back()->setSelected(team.getNum()); } + + buttonOk = widget("buttonOK"); + buttonCancel = widget("buttonCancel"); } diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index 356085ab5..aed0efa0e 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -94,6 +94,8 @@ public: TeamAlignmentsWidget(RandomMapTab & randomMapTab); private: + void checkTeamCount(); + std::shared_ptr background; std::shared_ptr labels; std::shared_ptr buttonOk, buttonCancel; From 6522cec9699cdbec88c47eedddab691cf1e4f580 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 24 Jul 2023 08:44:37 +0200 Subject: [PATCH 0003/1248] Just the notes --- client/lobby/RandomMapTab.cpp | 4 ++++ lib/rmg/CMapGenOptions.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 1d63c04d2..e3c1239b2 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -160,6 +160,10 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->mapHeader->howManyTeams = playersToGen; + //FIXME: Assign all human-controlled colors in first place + //TODO: Where are human / CPU players toggled in player configuration? + //TODO: Get human player count + std::set occupiedTeams; for(int i = 0; i < playersToGen; ++i) { diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 3201943eb..04f3816af 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -171,6 +171,7 @@ void CMapGenOptions::resetPlayersMap() else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) { + //FIXME: Allow humans to choose any color, even from the end of teh list playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); From f78e766a2db8fecf38410ec7544854592fec2a9c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 19:14:47 +0300 Subject: [PATCH 0004/1248] Bump version to 1.4 --- android/vcmi-app/build.gradle | 4 ++-- cmake_modules/VersionDefinition.cmake | 2 +- debian/changelog | 6 ++++++ launcher/eu.vcmi.VCMI.metainfo.xml | 1 + 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/android/vcmi-app/build.gradle b/android/vcmi-app/build.gradle index 6f21bbe37..ae88837df 100644 --- a/android/vcmi-app/build.gradle +++ b/android/vcmi-app/build.gradle @@ -10,8 +10,8 @@ android { applicationId "is.xyz.vcmi" minSdk 19 targetSdk 31 - versionCode 1201 - versionName "1.2.1" + versionCode 1400 + versionName "1.4.0" setProperty("archivesBaseName", "vcmi") } diff --git a/cmake_modules/VersionDefinition.cmake b/cmake_modules/VersionDefinition.cmake index 5b8409584..f9fdb6992 100644 --- a/cmake_modules/VersionDefinition.cmake +++ b/cmake_modules/VersionDefinition.cmake @@ -1,5 +1,5 @@ set(VCMI_VERSION_MAJOR 1) -set(VCMI_VERSION_MINOR 3) +set(VCMI_VERSION_MINOR 4) set(VCMI_VERSION_PATCH 0) add_definitions( -DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR} diff --git a/debian/changelog b/debian/changelog index f9f61f980..efb2ce135 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +vcmi (1.4.0) jammy; urgency=medium + + * New upstream release + + -- Ivan Savenko Fri, 22 Dec 2023 16:00:00 +0200 + vcmi (1.3.0) jammy; urgency=medium * New upstream release diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 233758bff..855bd4d0e 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -51,6 +51,7 @@ StrategyGame + From 21a39f0b015df46b07ae12d488a74816d9ff5d1e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 19:42:19 +0300 Subject: [PATCH 0005/1248] Removed boost::iostreams in favor of std::stream / boost::filesystem --- cmake_modules/VCMI_lib.cmake | 2 - launcher/jsonutils.cpp | 3 +- lib/CConfigHandler.cpp | 3 +- lib/CModHandler.cpp | 3 +- lib/filesystem/CFileInputStream.h | 3 +- lib/filesystem/CFilesystemLoader.cpp | 6 +- lib/filesystem/CZipLoader.cpp | 13 +- lib/filesystem/FileStream.cpp | 175 --------------------- lib/filesystem/FileStream.h | 67 -------- lib/filesystem/MinizipExtensions.cpp | 52 +++++- lib/logging/CLogger.h | 1 - lib/serializer/BinaryDeserializer.cpp | 3 +- lib/serializer/BinaryDeserializer.h | 3 +- lib/serializer/BinarySerializer.cpp | 3 +- lib/serializer/BinarySerializer.h | 4 +- lib/serializer/CLoadIntegrityValidator.cpp | 1 - mapeditor/jsonutils.cpp | 3 +- 17 files changed, 72 insertions(+), 273 deletions(-) delete mode 100644 lib/filesystem/FileStream.cpp delete mode 100644 lib/filesystem/FileStream.h diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 70c41549a..9d006488d 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -60,7 +60,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/CZipLoader.cpp ${MAIN_LIB_DIR}/filesystem/CZipSaver.cpp ${MAIN_LIB_DIR}/filesystem/FileInfo.cpp - ${MAIN_LIB_DIR}/filesystem/FileStream.cpp ${MAIN_LIB_DIR}/filesystem/Filesystem.cpp ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.cpp ${MAIN_LIB_DIR}/filesystem/ResourceID.cpp @@ -384,7 +383,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/CZipLoader.h ${MAIN_LIB_DIR}/filesystem/CZipSaver.h ${MAIN_LIB_DIR}/filesystem/FileInfo.h - ${MAIN_LIB_DIR}/filesystem/FileStream.h ${MAIN_LIB_DIR}/filesystem/Filesystem.h ${MAIN_LIB_DIR}/filesystem/ISimpleResourceLoader.h ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.h diff --git a/launcher/jsonutils.cpp b/launcher/jsonutils.cpp index 6906da1dc..5d54e12bb 100644 --- a/launcher/jsonutils.cpp +++ b/launcher/jsonutils.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "jsonutils.h" -#include "../lib/filesystem/FileStream.h" static QVariantMap JsonToMap(const JsonMap & json) { @@ -114,7 +113,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + boost::filesystem::fstream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 52fd9d3e9..dc93ecf64 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -11,7 +11,6 @@ #include "CConfigHandler.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileStream.h" #include "../lib/GameConstants.h" #include "../lib/VCMIDirs.h" @@ -77,7 +76,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath savedConf.Struct().erase("session"); JsonUtils::minimize(savedConf, "vcmi:settings"); - FileStream file(*CResourceHandler::get()->getResourceName(ResourceID("config/settings.json")), std::ofstream::out | std::ofstream::trunc); + boost::filesystem::fstream file(*CResourceHandler::get()->getResourceName(ResourceID("config/settings.json")), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 1a78d708f..47c5de7dc 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "CModHandler.h" #include "rmg/CRmgTemplateStorage.h" -#include "filesystem/FileStream.h" #include "filesystem/AdapterLoaders.h" #include "filesystem/CFilesystemLoader.h" #include "filesystem/Filesystem.h" @@ -1158,7 +1157,7 @@ void CModHandler::afterLoad(bool onlyEssential) if(!onlyEssential) { - FileStream file(*CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json")), std::ofstream::out | std::ofstream::trunc); + boost::filesystem::fstream file(*CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json")), std::ofstream::out | std::ofstream::trunc); file << modSettings.toJson(); } diff --git a/lib/filesystem/CFileInputStream.h b/lib/filesystem/CFileInputStream.h index 4dd499188..9ff789ecb 100644 --- a/lib/filesystem/CFileInputStream.h +++ b/lib/filesystem/CFileInputStream.h @@ -10,7 +10,6 @@ #pragma once #include "CInputStream.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -75,7 +74,7 @@ private: si64 dataSize; /** Native c++ input file stream object. */ - FileStream fileStream; + boost::filesystem::fstream fileStream; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 1290406a6..1d3603a1a 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -11,7 +11,6 @@ #include "CFilesystemLoader.h" #include "CFileInputStream.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -88,7 +87,10 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) if (!update) { - if (!FileStream::createFile(baseDirectory / filename)) + // create file, if not exists + boost::filesystem::fstream file(baseDirectory / filename); + + if (!file.is_open()) return false; } fileList[resID] = filename; diff --git a/lib/filesystem/CZipLoader.cpp b/lib/filesystem/CZipLoader.cpp index 3bbbdec3b..690530c73 100644 --- a/lib/filesystem/CZipLoader.cpp +++ b/lib/filesystem/CZipLoader.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "CZipLoader.h" -#include "FileStream.h" #include "../ScopeGuard.h" @@ -155,7 +154,10 @@ std::vector ZipArchive::listFiles(const boost::filesystem::path & f { std::vector ret; - unzFile file = unzOpen2_64(filename.c_str(), FileStream::GetMinizipFilefunc()); + CDefaultIOApi zipAPI; + auto zipStructure = zipAPI.getApiStructure(); + + unzFile file = unzOpen2_64(filename.c_str(), &zipStructure); if (unzGoToFirstFile(file) == UNZ_OK) { @@ -188,7 +190,10 @@ bool ZipArchive::extract(const boost::filesystem::path & from, const boost::file bool ZipArchive::extract(const boost::filesystem::path & from, const boost::filesystem::path & where, const std::vector & what) { - unzFile archive = unzOpen2_64(from.c_str(), FileStream::GetMinizipFilefunc()); + CDefaultIOApi zipAPI; + auto zipStructure = zipAPI.getApiStructure(); + + unzFile archive = unzOpen2_64(from.c_str(), &zipStructure); auto onExit = vstd::makeScopeGuard([&]() { @@ -209,7 +214,7 @@ bool ZipArchive::extract(const boost::filesystem::path & from, const boost::file if (boost::algorithm::ends_with(file, "/")) continue; - FileStream destFile(fullName, std::ios::out | std::ios::binary); + boost::filesystem::fstream destFile(fullName, std::ios::out | std::ios::binary); if (!destFile.good()) return false; diff --git a/lib/filesystem/FileStream.cpp b/lib/filesystem/FileStream.cpp deleted file mode 100644 index 06f89fca5..000000000 --- a/lib/filesystem/FileStream.cpp +++ /dev/null @@ -1,175 +0,0 @@ -/* - * FileStream.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 "FileStream.h" - -#ifdef USE_SYSTEM_MINIZIP -#include -#include -#else -#include "../minizip/unzip.h" -#include "../minizip/ioapi.h" -#endif - -#include - -#define GETFILE static_cast(filePtr) - -#ifdef VCMI_WINDOWS - #ifndef _CRT_SECURE_NO_WARNINGS - #define _CRT_SECURE_NO_WARNINGS - #endif - #include - #define CHAR_LITERAL(s) L##s - using CharType = wchar_t; -#else - #define CHAR_LITERAL(s) s - using CharType = char; -#endif - -namespace -{ -inline FILE* do_open(const CharType* name, const CharType* mode) -{ - #ifdef VCMI_WINDOWS - return _wfopen(name, mode); - #else - return std::fopen(name, mode); - #endif -} - -voidpf ZCALLBACK MinizipOpenFunc(voidpf opaque, const void* filename, int mode) -{ - const CharType* mode_fopen = [mode]() -> const CharType* - { - if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) - return CHAR_LITERAL("rb"); - else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) - return CHAR_LITERAL("r+b"); - else if (mode & ZLIB_FILEFUNC_MODE_CREATE) - return CHAR_LITERAL("wb"); - return nullptr; - }(); - - if (filename != nullptr && mode_fopen != nullptr) - return do_open(static_cast(filename), mode_fopen); - else - return nullptr; -} -} // namespace - -template struct boost::iostreams::stream; - -VCMI_LIB_NAMESPACE_BEGIN - -zlib_filefunc64_def* FileStream::GetMinizipFilefunc() -{ - static zlib_filefunc64_def MinizipFilefunc; - static bool initialized = false; - if (!initialized) - { - fill_fopen64_filefunc(&MinizipFilefunc); - MinizipFilefunc.zopen64_file = &MinizipOpenFunc; - initialized = true; - } - return &MinizipFilefunc; -} - -/*static*/ -bool FileStream::createFile(const boost::filesystem::path& filename) -{ - FILE* f = do_open(filename.c_str(), CHAR_LITERAL("wb")); - bool result = (f != nullptr); - if(result) - fclose(f); - return result; -} - -FileBuf::FileBuf(const boost::filesystem::path& filename, std::ios_base::openmode mode) -{ - auto openmode = [mode]() -> std::basic_string - { - using namespace std; - switch (mode & (~ios_base::ate & ~ios_base::binary)) - { - case (ios_base::in): - return CHAR_LITERAL("r"); - case (ios_base::out): - case (ios_base::out | ios_base::trunc): - return CHAR_LITERAL("w"); - case (ios_base::app): - case (ios_base::out | ios_base::app): - return CHAR_LITERAL("a"); - case (ios_base::out | ios_base::in): - return CHAR_LITERAL("r+"); - case (ios_base::out | ios_base::in | ios_base::trunc): - return CHAR_LITERAL("w+"); - case (ios_base::out | ios_base::in | ios_base::app): - case (ios_base::in | ios_base::app): - return CHAR_LITERAL("a+"); - default: - throw std::ios_base::failure("invalid open mode"); - } - }(); - - if (mode & std::ios_base::binary) - openmode += CHAR_LITERAL('b'); - - filePtr = do_open(filename.c_str(), openmode.c_str()); - - if (filePtr == nullptr) - throw std::ios_base::failure("could not open file"); - - if (mode & std::ios_base::ate) { - if (std::fseek(GETFILE, 0, SEEK_END)) { - fclose(GETFILE); - throw std::ios_base::failure("could not open file"); - } - } -} - -void FileBuf::close() -{ - std::fclose(GETFILE); -} - -std::streamsize FileBuf::read(char* s, std::streamsize n) -{ - return static_cast(std::fread(s, 1, n, GETFILE)); -} - -std::streamsize FileBuf::write(const char* s, std::streamsize n) -{ - return static_cast(std::fwrite(s, 1, n, GETFILE)); -} - -std::streamoff FileBuf::seek(std::streamoff off, std::ios_base::seekdir way) -{ - const auto src = [way]() -> int - { - switch(way) - { - case std::ios_base::beg: - return SEEK_SET; - case std::ios_base::cur: - return SEEK_CUR; - case std::ios_base::end: - return SEEK_END; - default: - throw std::ios_base::failure("bad seek direction"); - } - }(); - if(std::fseek(GETFILE, static_cast(off), src)) - throw std::ios_base::failure("bad seek offset"); - - return static_cast(std::ftell(GETFILE)); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/FileStream.h b/lib/filesystem/FileStream.h deleted file mode 100644 index db57473e0..000000000 --- a/lib/filesystem/FileStream.h +++ /dev/null @@ -1,67 +0,0 @@ -/* - * FileStream.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 -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE FileBuf -{ -public: - using char_type = char; - using category = struct category_ : - boost::iostreams::seekable_device_tag, - boost::iostreams::closable_tag - {}; - - FileBuf(const boost::filesystem::path& filename, std::ios_base::openmode mode); - - std::streamsize read(char* s, std::streamsize n); - std::streamsize write(const char* s, std::streamsize n); - std::streamoff seek(std::streamoff off, std::ios_base::seekdir way); - - void close(); -private: - void* filePtr; -}; - -VCMI_LIB_NAMESPACE_END - -struct zlib_filefunc64_def_s; -using zlib_filefunc64_def = zlib_filefunc64_def_s; - -#ifdef VCMI_DLL -#ifdef _MSC_VER -#pragma warning (push) -#pragma warning (disable : 4910) -#endif -extern template struct DLL_LINKAGE boost::iostreams::stream; -#ifdef _MSC_VER -#pragma warning (pop) -#endif -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE FileStream : public boost::iostreams::stream -{ -public: - FileStream() = default; - explicit FileStream(const boost::filesystem::path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out) - : boost::iostreams::stream(p, mode) {} - - static bool createFile(const boost::filesystem::path& filename); - - static zlib_filefunc64_def* GetMinizipFilefunc(); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/MinizipExtensions.cpp b/lib/filesystem/MinizipExtensions.cpp index e58fb3a9d..9c7a2a407 100644 --- a/lib/filesystem/MinizipExtensions.cpp +++ b/lib/filesystem/MinizipExtensions.cpp @@ -11,7 +11,6 @@ #include "MinizipExtensions.h" #include "CMemoryBuffer.h" -#include "FileStream.h" VCMI_LIB_NAMESPACE_BEGIN @@ -86,10 +85,59 @@ inline int streamProxyClose(voidpf opaque, voidpf stream) } ///CDefaultIOApi +#define GETFILE static_cast(filePtr) + +#ifdef VCMI_WINDOWS + #ifndef _CRT_SECURE_NO_WARNINGS + #define _CRT_SECURE_NO_WARNINGS + #endif + #include + #define CHAR_LITERAL(s) L##s + using CharType = wchar_t; +#else + #define CHAR_LITERAL(s) s + using CharType = char; +#endif + +static inline FILE* do_open(const CharType* name, const CharType* mode) +{ + #ifdef VCMI_WINDOWS + return _wfopen(name, mode); + #else + return std::fopen(name, mode); + #endif +} + +static voidpf ZCALLBACK MinizipOpenFunc(voidpf opaque, const void* filename, int mode) +{ + const CharType* mode_fopen = [mode]() -> const CharType* + { + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) + return CHAR_LITERAL("rb"); + else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + return CHAR_LITERAL("r+b"); + else if (mode & ZLIB_FILEFUNC_MODE_CREATE) + return CHAR_LITERAL("wb"); + return nullptr; + }(); + + if (filename != nullptr && mode_fopen != nullptr) + return do_open(static_cast(filename), mode_fopen); + else + return nullptr; +} zlib_filefunc64_def CDefaultIOApi::getApiStructure() { - return * FileStream::GetMinizipFilefunc(); + static zlib_filefunc64_def MinizipFilefunc; + static bool initialized = false; + if (!initialized) + { + fill_fopen64_filefunc(&MinizipFilefunc); + MinizipFilefunc.zopen64_file = &MinizipOpenFunc; + initialized = true; + } + return MinizipFilefunc; } ///CProxyIOApi diff --git a/lib/logging/CLogger.h b/lib/logging/CLogger.h index 03a36a73c..56463cebf 100644 --- a/lib/logging/CLogger.h +++ b/lib/logging/CLogger.h @@ -10,7 +10,6 @@ #pragma once #include "../CConsoleHandler.h" -#include "../filesystem/FileStream.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 7fe1bfd5a..fcfddda00 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "BinaryDeserializer.h" -#include "../filesystem/FileStream.h" #include "../registerTypes/RegisterTypes.h" @@ -41,7 +40,7 @@ void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalV try { fName = fname.string(); - sfile = std::make_unique(fname, std::ios::in | std::ios::binary); + sfile = std::make_unique(fname, std::ios::in | std::ios::binary); sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway if(!(*sfile)) diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 6b55409af..a0fd4f9ef 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -19,7 +19,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CStackInstance; -class FileStream; class DLL_LINKAGE CLoaderBase { @@ -581,7 +580,7 @@ public: BinaryDeserializer serializer; std::string fName; - std::unique_ptr sfile; + std::unique_ptr sfile; CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! virtual ~CLoadFile(); diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 0bd67c038..93c4c9874 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "BinarySerializer.h" -#include "../filesystem/FileStream.h" #include "../registerTypes/RegisterTypes.h" @@ -38,7 +37,7 @@ void CSaveFile::openNextFile(const boost::filesystem::path &fname) fName = fname; try { - sfile = std::make_unique(fname, std::ios::out | std::ios::binary); + sfile = std::make_unique(fname, std::ios::out | std::ios::binary); sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway if(!(*sfile)) diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 14694ea9a..68c16b968 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -14,8 +14,6 @@ VCMI_LIB_NAMESPACE_BEGIN -class FileStream; - class DLL_LINKAGE CSaverBase { protected: @@ -392,7 +390,7 @@ public: BinarySerializer serializer; boost::filesystem::path fName; - std::unique_ptr sfile; + std::unique_ptr sfile; CSaveFile(const boost::filesystem::path &fname); //throws! ~CSaveFile(); diff --git a/lib/serializer/CLoadIntegrityValidator.cpp b/lib/serializer/CLoadIntegrityValidator.cpp index c0a2c0e11..81b3be830 100644 --- a/lib/serializer/CLoadIntegrityValidator.cpp +++ b/lib/serializer/CLoadIntegrityValidator.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "CLoadIntegrityValidator.h" -#include "../filesystem/FileStream.h" #include "../registerTypes/RegisterTypes.h" diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp index ccf7bd629..e3026c885 100644 --- a/mapeditor/jsonutils.cpp +++ b/mapeditor/jsonutils.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" #include "jsonutils.h" -#include "../lib/filesystem/FileStream.h" static QVariantMap JsonToMap(const JsonMap & json) { @@ -120,7 +119,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - FileStream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + boost::filesystem::fstream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } From 4d08a131d31b6f2f463da87542e1059f4541f32e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 28 Jul 2023 15:51:14 +0300 Subject: [PATCH 0006/1248] Reorganization of boost filesystem usage - Removed (most of) boost filesystem namespace usings - Replaced boost::filesystem::fstream with std::fstream and different constructor that should be available on any plaftorm --- client/CMT.cpp | 3 +- client/ClientCommandManager.cpp | 8 ++--- client/mainmenu/CMainMenu.cpp | 2 -- launcher/jsonutils.cpp | 2 +- lib/CConfigHandler.cpp | 2 +- lib/CModHandler.cpp | 2 +- lib/filesystem/CArchiveLoader.cpp | 12 ++++---- lib/filesystem/CArchiveLoader.h | 8 ++--- lib/filesystem/CFileInputStream.cpp | 2 +- lib/filesystem/CFileInputStream.h | 2 +- lib/filesystem/CFilesystemLoader.cpp | 30 +++++++++---------- lib/filesystem/CZipLoader.cpp | 2 +- lib/logging/CLogger.cpp | 2 +- lib/logging/CLogger.h | 2 +- lib/mapping/CMapService.cpp | 2 +- lib/serializer/BinaryDeserializer.cpp | 2 +- lib/serializer/BinaryDeserializer.h | 2 +- lib/serializer/BinarySerializer.cpp | 2 +- lib/serializer/BinarySerializer.h | 2 +- mapeditor/jsonutils.cpp | 2 +- .../resourceExtractor/ResourceConverter.cpp | 20 ++++++------- .../resourceExtractor/ResourceConverter.h | 8 ++--- test/map/CMapFormatTest.cpp | 2 +- 23 files changed, 56 insertions(+), 65 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 3f4fe83b4..c5b5600a1 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -50,7 +50,6 @@ namespace po = boost::program_options; namespace po_style = boost::program_options::command_line_style; -namespace bfs = boost::filesystem; extern boost::thread_specific_ptr inGuiThread; @@ -196,7 +195,7 @@ int main(int argc, char * argv[]) console->start(); #endif - const bfs::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; + const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; logConfig = new CBasicLogConfigurator(logPath, console); logConfig->configureDefault(); logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index c2c2fa461..438e4f2f6 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -247,7 +247,7 @@ void ClientCommandManager::handleGetConfigCommand() boost::algorithm::replace_all(name, ":", "_"); const boost::filesystem::path filePath = contentOutPath / (name + ".json"); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); file << object.toJson(); } } @@ -273,7 +273,7 @@ void ClientCommandManager::handleGetScriptsCommand() const scripting::ScriptImpl * script = kv.second.get(); boost::filesystem::path filePath = outPath / (name + ".lua"); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); file << script->getSource(); } printCommandMessage("\rExtracting done :)\n"); @@ -300,7 +300,7 @@ void ClientCommandManager::handleGetTextCommand() boost::filesystem::create_directories(filePath.parent_path()); - boost::filesystem::ofstream file(filePath); + std::ofstream file(filePath.c_str()); auto text = CResourceHandler::get()->load(filename)->readAll(); file.write((char*)text.first.get(), text.second); @@ -331,7 +331,7 @@ void ClientCommandManager::handleExtractCommand(std::istringstream& singleWordBu auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll(); boost::filesystem::create_directories(outPath.parent_path()); - boost::filesystem::ofstream outFile(outPath, boost::filesystem::ofstream::binary); + std::ofstream outFile(outPath.c_str(), std::ofstream::binary); outFile.write((char*)data.first.get(), data.second); } else diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 6b72fb1e7..cd56a8208 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -61,8 +61,6 @@ #include #endif -namespace fs = boost::filesystem; - std::shared_ptr CMM; ISelectionScreenInfo * SEL; diff --git a/launcher/jsonutils.cpp b/launcher/jsonutils.cpp index 5d54e12bb..895eee540 100644 --- a/launcher/jsonutils.cpp +++ b/launcher/jsonutils.cpp @@ -113,7 +113,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - boost::filesystem::fstream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index dc93ecf64..091135723 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -76,7 +76,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath savedConf.Struct().erase("session"); JsonUtils::minimize(savedConf, "vcmi:settings"); - boost::filesystem::fstream file(*CResourceHandler::get()->getResourceName(ResourceID("config/settings.json")), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(ResourceID("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/CModHandler.cpp b/lib/CModHandler.cpp index 47c5de7dc..097eb1e79 100644 --- a/lib/CModHandler.cpp +++ b/lib/CModHandler.cpp @@ -1157,7 +1157,7 @@ void CModHandler::afterLoad(bool onlyEssential) if(!onlyEssential) { - boost::filesystem::fstream file(*CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json")), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << modSettings.toJson(); } diff --git a/lib/filesystem/CArchiveLoader.cpp b/lib/filesystem/CArchiveLoader.cpp index bca104a45..b50f15f4d 100644 --- a/lib/filesystem/CArchiveLoader.cpp +++ b/lib/filesystem/CArchiveLoader.cpp @@ -24,7 +24,7 @@ ArchiveEntry::ArchiveEntry() } -CArchiveLoader::CArchiveLoader(std::string _mountPoint, bfs::path _archive, bool _extractArchives) : +CArchiveLoader::CArchiveLoader(std::string _mountPoint, boost::filesystem::path _archive, bool _extractArchives) : archive(std::move(_archive)), mountPoint(std::move(_mountPoint)), extractArchives(_extractArchives) @@ -217,7 +217,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput fileStream.seek(entry.offset); fileStream.read(data.data(), entry.fullSize); - bfs::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); + boost::filesystem::path extractedFilePath = createExtractedFilePath(outputSubFolder, entry.name); // writeToOutputFile std::ofstream out(extractedFilePath.string(), std::ofstream::binary); @@ -235,12 +235,12 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const extractToFolder(outputSubFolder, *inputStream, entry); } -bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName) { - bfs::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; - bfs::path extractedFilePath = extractionFolderPath / entryName; + boost::filesystem::path extractionFolderPath = VCMIDirs::get().userExtractedPath() / outputSubFolder; + boost::filesystem::path extractedFilePath = extractionFolderPath / entryName; - bfs::create_directories(extractionFolderPath); + boost::filesystem::create_directories(extractionFolderPath); return extractedFilePath; } diff --git a/lib/filesystem/CArchiveLoader.h b/lib/filesystem/CArchiveLoader.h index e263e55c5..d1ed59394 100644 --- a/lib/filesystem/CArchiveLoader.h +++ b/lib/filesystem/CArchiveLoader.h @@ -12,8 +12,6 @@ #include "ISimpleResourceLoader.h" #include "ResourceID.h" -namespace bfs = boost::filesystem; - VCMI_LIB_NAMESPACE_BEGIN class CFileInputStream; @@ -58,7 +56,7 @@ public: * * @throws std::runtime_error if the archive wasn't found or if the archive isn't supported */ - CArchiveLoader(std::string mountPoint, bfs::path archive, bool extractArchives = false); + CArchiveLoader(std::string mountPoint, boost::filesystem::path archive, bool extractArchives = false); /// Interface implementation /// @see ISimpleResourceLoader @@ -95,7 +93,7 @@ private: void initSNDArchive(const std::string &mountPoint, CFileInputStream & fileStream); /** The file path to the archive which is scanned and indexed. */ - bfs::path archive; + boost::filesystem::path archive; std::string mountPoint; @@ -107,6 +105,6 @@ private: }; /** Constructs the file path for the extracted file. Creates the subfolder hierarchy aswell **/ -bfs::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); +boost::filesystem::path createExtractedFilePath(const std::string & outputSubFolder, const std::string & entryName); VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CFileInputStream.cpp b/lib/filesystem/CFileInputStream.cpp index eb14e3eda..8f10d0d36 100644 --- a/lib/filesystem/CFileInputStream.cpp +++ b/lib/filesystem/CFileInputStream.cpp @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN CFileInputStream::CFileInputStream(const boost::filesystem::path & file, si64 start, si64 size) : dataStart{start}, dataSize{size}, - fileStream{file, std::ios::in | std::ios::binary} + fileStream{file.c_str(), std::ios::in | std::ios::binary} { if (fileStream.fail()) throw std::runtime_error("File " + file.string() + " isn't available."); diff --git a/lib/filesystem/CFileInputStream.h b/lib/filesystem/CFileInputStream.h index 9ff789ecb..077ec4cef 100644 --- a/lib/filesystem/CFileInputStream.h +++ b/lib/filesystem/CFileInputStream.h @@ -74,7 +74,7 @@ private: si64 dataSize; /** Native c++ input file stream object. */ - boost::filesystem::fstream fileStream; + std::fstream fileStream; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 1d3603a1a..74c3b3da5 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -14,9 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -namespace bfs = boost::filesystem; - -CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, bfs::path baseDirectory, size_t depth, bool initial): +CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, boost::filesystem::path baseDirectory, size_t depth, bool initial): baseDirectory(std::move(baseDirectory)), mountPoint(std::move(_mountPoint)), fileList(listFiles(mountPoint, depth, initial)), @@ -28,7 +26,7 @@ CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, bfs::path baseDire std::unique_ptr CFilesystemLoader::load(const ResourceID & resourceName) const { assert(fileList.count(resourceName)); - bfs::path file = baseDirectory / fileList.at(resourceName); + boost::filesystem::path file = baseDirectory / fileList.at(resourceName); logGlobal->trace("loading %s", file.string()); return std::make_unique(file); } @@ -88,7 +86,7 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) if (!update) { // create file, if not exists - boost::filesystem::fstream file(baseDirectory / filename); + std::fstream file((baseDirectory / filename).c_str()); if (!file.is_open()) return false; @@ -97,7 +95,7 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) return true; } -std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const +std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const { static const EResType::Type initArray[] = { EResType::DIRECTORY, @@ -108,16 +106,16 @@ std::unordered_map CFilesystemLoader::listFiles(const std EResType::ARCHIVE_ZIP }; static const std::set initialTypes(initArray, initArray + std::size(initArray)); - assert(bfs::is_directory(baseDirectory)); - std::unordered_map fileList; + assert(boost::filesystem::is_directory(baseDirectory)); + std::unordered_map fileList; - std::vector path; //vector holding relative path to our file + std::vector path; //vector holding relative path to our file - bfs::recursive_directory_iterator enddir; + boost::filesystem::recursive_directory_iterator enddir; #if BOOST_VERSION >= 107200 // 1.72 - bfs::recursive_directory_iterator it(baseDirectory, bfs::directory_options::follow_directory_symlink); + boost::filesystem::recursive_directory_iterator it(baseDirectory, boost::filesystem::directory_options::follow_directory_symlink); #else - bfs::recursive_directory_iterator it(baseDirectory, bfs::symlink_option::recurse); + boost::filesystem::recursive_directory_iterator it(baseDirectory, boost::filesystem::symlink_option::recurse); #endif for(; it != enddir; ++it) @@ -129,7 +127,7 @@ std::unordered_map CFilesystemLoader::listFiles(const std const auto currentDepth = it.level(); #endif - if (bfs::is_directory(it->status())) + if (boost::filesystem::is_directory(it->status())) { path.resize(currentDepth + 1); path.back() = it->path().filename(); @@ -148,7 +146,7 @@ std::unordered_map CFilesystemLoader::listFiles(const std if (!initial || vstd::contains(initialTypes, type)) { //reconstruct relative filename (not possible via boost AFAIK) - bfs::path filename; + boost::filesystem::path filename; const size_t iterations = std::min(static_cast(currentDepth), path.size()); if (iterations) { @@ -161,13 +159,13 @@ std::unordered_map CFilesystemLoader::listFiles(const std filename = it->path().filename(); std::string resName; - if (bfs::path::preferred_separator != '/') + if (boost::filesystem::path::preferred_separator != '/') { // resource names are using UNIX slashes (/) resName.reserve(resName.size() + filename.native().size()); resName = mountPoint; for (const char c : filename.string()) - if (c != bfs::path::preferred_separator) + if (c != boost::filesystem::path::preferred_separator) resName.push_back(c); else resName.push_back('/'); diff --git a/lib/filesystem/CZipLoader.cpp b/lib/filesystem/CZipLoader.cpp index 690530c73..ced952a1c 100644 --- a/lib/filesystem/CZipLoader.cpp +++ b/lib/filesystem/CZipLoader.cpp @@ -214,7 +214,7 @@ bool ZipArchive::extract(const boost::filesystem::path & from, const boost::file if (boost::algorithm::ends_with(file, "/")) continue; - boost::filesystem::fstream destFile(fullName, std::ios::out | std::ios::binary); + std::fstream destFile(fullName.c_str(), std::ios::out | std::ios::binary); if (!destFile.good()) return false; diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index 9e23d372e..ad51153bd 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -425,7 +425,7 @@ const CColorMapping & CLogConsoleTarget::getColorMapping() const { return colorM void CLogConsoleTarget::setColorMapping(const CColorMapping & colorMapping) { this->colorMapping = colorMapping; } CLogFileTarget::CLogFileTarget(const boost::filesystem::path & filePath, bool append): - file(filePath, append ? std::ios_base::app : std::ios_base::out) + file(filePath.c_str(), append ? std::ios_base::app : std::ios_base::out) { // formatter.setPattern("%d %l %n [%t] - %m"); formatter.setPattern("%l %n [%t] - %m"); diff --git a/lib/logging/CLogger.h b/lib/logging/CLogger.h index 56463cebf..72a0b929e 100644 --- a/lib/logging/CLogger.h +++ b/lib/logging/CLogger.h @@ -219,7 +219,7 @@ public: void write(const LogRecord & record) override; private: - boost::filesystem::fstream file; + std::fstream file; CLogFormatter formatter; mutable std::mutex mx; }; diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index b3a99597e..04b2f4869 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -80,7 +80,7 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: } { boost::filesystem::remove(fullPath); - boost::filesystem::ofstream tmp(fullPath, boost::filesystem::ofstream::binary); + std::ofstream tmp(fullPath.c_str(), std::ofstream::binary); tmp.write(reinterpret_cast(serializeBuffer.getBuffer().data()), serializeBuffer.getSize()); tmp.flush(); diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index fcfddda00..95400caf9 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -40,7 +40,7 @@ void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalV try { fName = fname.string(); - sfile = std::make_unique(fname, std::ios::in | std::ios::binary); + sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway if(!(*sfile)) diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index a0fd4f9ef..1264eee9b 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -580,7 +580,7 @@ public: BinaryDeserializer serializer; std::string fName; - std::unique_ptr sfile; + std::unique_ptr sfile; CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! virtual ~CLoadFile(); diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 93c4c9874..46c99f6de 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -37,7 +37,7 @@ void CSaveFile::openNextFile(const boost::filesystem::path &fname) fName = fname; try { - sfile = std::make_unique(fname, std::ios::out | std::ios::binary); + sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway if(!(*sfile)) diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 68c16b968..8d8f7a297 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -390,7 +390,7 @@ public: BinarySerializer serializer; boost::filesystem::path fName; - std::unique_ptr sfile; + std::unique_ptr sfile; CSaveFile(const boost::filesystem::path &fname); //throws! ~CSaveFile(); diff --git a/mapeditor/jsonutils.cpp b/mapeditor/jsonutils.cpp index e3026c885..a10129c14 100644 --- a/mapeditor/jsonutils.cpp +++ b/mapeditor/jsonutils.cpp @@ -119,7 +119,7 @@ JsonNode toJson(QVariant object) void JsonToFile(QString filename, QVariant object) { - boost::filesystem::fstream file(qstringToPath(filename), std::ios::out | std::ios_base::binary); + std::fstream file(qstringToPath(filename).c_str(), std::ios::out | std::ios_base::binary); file << toJson(object).toJson(); } diff --git a/mapeditor/resourceExtractor/ResourceConverter.cpp b/mapeditor/resourceExtractor/ResourceConverter.cpp index 6cdecc345..fc08b51a8 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.cpp +++ b/mapeditor/resourceExtractor/ResourceConverter.cpp @@ -24,8 +24,8 @@ void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversionOptions) { - bfs::path spritesPath = VCMIDirs::get().userExtractedPath() / "SPRITES"; - bfs::path imagesPath = VCMIDirs::get().userExtractedPath() / "IMAGES"; + boost::filesystem::path spritesPath = VCMIDirs::get().userExtractedPath() / "SPRITES"; + boost::filesystem::path imagesPath = VCMIDirs::get().userExtractedPath() / "IMAGES"; std::vector defFiles = { "TwCrPort.def", "CPRSMALL.def", "FlagPort.def", "ITPA.def", "ITPt.def", "Un32.def", "Un44.def" }; if(conversionOptions.splitDefs) @@ -35,16 +35,16 @@ void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversi doConvertPcxToPng(imagesPath, conversionOptions.deleteOriginals); } -void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::doConvertPcxToPng(const boost::filesystem::path & sourceFolder, bool deleteOriginals) { logGlobal->info("Converting .pcx to .png from folder: %s ...\n", sourceFolder); - for(const auto & directoryEntry : bfs::directory_iterator(sourceFolder)) + for(const auto & directoryEntry : boost::filesystem::directory_iterator(sourceFolder)) { const auto filename = directoryEntry.path().filename(); try { - if(!bfs::is_regular_file(directoryEntry)) + if(!boost::filesystem::is_regular_file(directoryEntry)) continue; std::string fileStem = directoryEntry.path().stem().string(); @@ -53,11 +53,11 @@ void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool d if(boost::algorithm::to_lower_copy(filename.extension().string()) == ".pcx") { auto img = BitmapHandler::loadBitmap(filenameLowerCase); - bfs::path pngFilePath = sourceFolder / (fileStem + ".png"); + boost::filesystem::path pngFilePath = sourceFolder / (fileStem + ".png"); img.save(pathToQString(pngFilePath), "PNG"); if(deleteOriginals) - bfs::remove(directoryEntry.path()); + boost::filesystem::remove(directoryEntry.path()); } } catch(const std::exception& ex) @@ -67,7 +67,7 @@ void ResourceConverter::doConvertPcxToPng(const bfs::path & sourceFolder, bool d } } -void ResourceConverter::splitDefFile(const std::string & fileName, const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::splitDefFile(const std::string & fileName, const boost::filesystem::path & sourceFolder, bool deleteOriginals) { if(CResourceHandler::get()->existsResource(ResourceID("SPRITES/" + fileName))) { @@ -76,13 +76,13 @@ void ResourceConverter::splitDefFile(const std::string & fileName, const bfs::pa anim->exportBitmaps(pathToQString(sourceFolder)); if(deleteOriginals) - bfs::remove(sourceFolder / fileName); + boost::filesystem::remove(sourceFolder / fileName); } else logGlobal->error("Def File Split error! " + fileName); } -void ResourceConverter::splitDefFiles(const std::vector & defFileNames, const bfs::path & sourceFolder, bool deleteOriginals) +void ResourceConverter::splitDefFiles(const std::vector & defFileNames, const boost::filesystem::path & sourceFolder, bool deleteOriginals) { logGlobal->info("Splitting Def Files from folder: %s ...\n", sourceFolder); diff --git a/mapeditor/resourceExtractor/ResourceConverter.h b/mapeditor/resourceExtractor/ResourceConverter.h index cb1035562..faef5abdd 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.h +++ b/mapeditor/resourceExtractor/ResourceConverter.h @@ -9,8 +9,6 @@ */ #pragma once -namespace bfs = boost::filesystem; - // Struct for holding all Convertor Options struct ConversionOptions { @@ -45,14 +43,14 @@ public: private: // Converts all .pcx from extractedFolder/Images into .png - static void doConvertPcxToPng(const bfs::path & sourceFolder, bool deleteOriginals); + static void doConvertPcxToPng(const boost::filesystem::path & sourceFolder, bool deleteOriginals); // splits a .def file into individual images and converts the output to PNG format - static void splitDefFile(const std::string & fileName, const bfs::path & sourceFolder, bool deleteOriginals); + static void splitDefFile(const std::string & fileName, const boost::filesystem::path & sourceFolder, bool deleteOriginals); /// /// Splits the given .def files into individual images. /// For each .def file, the resulting images will be output in the same folder, in a subfolder (named just like the .def file) /// - static void splitDefFiles(const std::vector & defFileNames, const bfs::path & sourceFolder, bool deleteOriginals); + static void splitDefFiles(const std::vector & defFileNames, const boost::filesystem::path & sourceFolder, bool deleteOriginals); }; diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index e1d88f7a4..c1acc6b35 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -31,7 +31,7 @@ static void saveTestMap(CMemoryBuffer & serializeBuffer, const std::string & fil { auto path = VCMIDirs::get().userDataPath() / filename; boost::filesystem::remove(path); - boost::filesystem::ofstream tmp(path, boost::filesystem::ofstream::binary); + std::ofstream tmp(path.c_str(), std::ofstream::binary); tmp.write((const char *)serializeBuffer.getBuffer().data(), serializeBuffer.getSize()); tmp.flush(); From 593b82d1784c7ff310728e839dfbac4dafa216cb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 27 Jul 2023 22:50:50 +0300 Subject: [PATCH 0007/1248] Removed all references to boost::interprocess library --- client/CMT.cpp | 6 -- client/CServerHandler.cpp | 41 +-------- cmake_modules/VCMI_lib.cmake | 1 - lib/Interprocess.h | 84 ------------------- lib/StartInfo.h | 2 - lib/rmg/modificators/ConnectionsPlacer.cpp | 1 - .../resourceExtractor/ResourceConverter.cpp | 4 +- server/CVCMIServer.cpp | 24 +----- server/CVCMIServer.h | 1 - 9 files changed, 6 insertions(+), 158 deletions(-) delete mode 100644 lib/Interprocess.h diff --git a/client/CMT.cpp b/client/CMT.cpp index c5b5600a1..99235e185 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -117,8 +117,6 @@ int main(int argc, char * argv[]) opts.add_options() ("help,h", "display help and exit") ("version,v", "display version information and exit") - ("disable-shm", "force disable shared memory usage") - ("enable-shm-uuid", "use UUID for shared memory identifier") ("testmap", po::value(), "") ("testsave", po::value(), "") ("spectate,s", "enable spectator interface for AI-only games") @@ -241,10 +239,6 @@ int main(int argc, char * argv[]) // Server settings setSettingBool("session/donotstartserver", "donotstartserver"); - // Shared memory options - setSettingBool("session/disable-shm", "disable-shm"); - setSettingBool("session/enable-shm-uuid", "enable-shm-uuid"); - // Init special testing settings setSettingInteger("session/serverport", "serverport", 0); setSettingInteger("general/saveFrequency", "savefrequency", 1); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 225c09c9e..815633f18 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -28,8 +28,6 @@ #elif defined(VCMI_IOS) #include "ios/utils.h" #include -#else -#include "../lib/Interprocess.h" #endif #ifdef SINGLE_PROCESS_APP @@ -154,29 +152,6 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std:: myNames = *names; else myNames.push_back(settings["general"]["playerName"].String()); - -#if !defined(VCMI_ANDROID) && !defined(SINGLE_PROCESS_APP) - shm.reset(); - - if(!settings["session"]["disable-shm"].Bool()) - { - std::string sharedMemoryName = "vcmi_memory"; - if(settings["session"]["enable-shm-uuid"].Bool()) - { - //used or automated testing when multiple clients start simultaneously - sharedMemoryName += "_" + uuid; - } - try - { - shm = std::make_shared(sharedMemoryName, true); - } - catch(...) - { - shm.reset(); - logNetwork->error("Cannot open interprocess memory. Continue without it..."); - } - } -#endif } void CServerHandler::startLocalServerAndConnect() @@ -256,19 +231,13 @@ void CServerHandler::startLocalServerAndConnect() } logNetwork->info("waiting for server finished..."); androidTestServerReadyFlag = false; -#else - if(shm) - shm->sr->waitTillReady(); #endif logNetwork->trace("Waiting for server: %d ms", th->getDiff()); th->update(); //put breakpoint here to attach to server before it does something stupid -#if !defined(VCMI_MOBILE) - const ui16 port = shm ? shm->sr->port : 0; -#else const ui16 port = 0; -#endif + justConnectToServer(localhostAddress, port); logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); @@ -925,13 +894,7 @@ void CServerHandler::threadRunServer() comm += " --lobby-port=" + std::to_string(settings["session"]["port"].Integer()); comm += " --lobby-uuid=" + settings["session"]["hostUuid"].String(); } - - if(shm) - { - comm += " --enable-shm"; - if(settings["session"]["enable-shm-uuid"].Bool()) - comm += " --enable-shm-uuid"; - } + comm += " > \"" + logName + '\"'; logGlobal->info("Server command line: %s", comm); diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 9d006488d..153be71d0 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -585,7 +585,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/IGameEventsReceiver.h ${MAIN_LIB_DIR}/IHandlerBase.h ${MAIN_LIB_DIR}/int3.h - ${MAIN_LIB_DIR}/Interprocess.h ${MAIN_LIB_DIR}/JsonDetail.h ${MAIN_LIB_DIR}/JsonNode.h ${MAIN_LIB_DIR}/JsonRandom.h diff --git a/lib/Interprocess.h b/lib/Interprocess.h deleted file mode 100644 index cafe9666f..000000000 --- a/lib/Interprocess.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Interprocess.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 -#include -#include -#include -#include - -VCMI_LIB_NAMESPACE_BEGIN - -struct ServerReady -{ - bool ready; - uint16_t port; //ui16? - boost::interprocess::interprocess_mutex mutex; - boost::interprocess::interprocess_condition cond; - - ServerReady() - { - ready = false; - port = 0; - } - - void waitTillReady() - { - boost::interprocess::scoped_lock slock(mutex); - while(!ready) - { - cond.wait(slock); - } - } - - void setToReadyAndNotify(const uint16_t Port) - { - { - boost::unique_lock lock(mutex); - ready = true; - port = Port; - } - cond.notify_all(); - } -}; - -struct SharedMemory -{ - std::string name; - boost::interprocess::shared_memory_object smo; - boost::interprocess::mapped_region * mr; - ServerReady * sr; - - SharedMemory(const std::string & Name, bool initialize = false) - : name(Name) - { - if(initialize) - { - //if the application has previously crashed, the memory may not have been removed. to avoid problems - try to destroy it - boost::interprocess::shared_memory_object::remove(name.c_str()); - } - smo = boost::interprocess::shared_memory_object(boost::interprocess::open_or_create, name.c_str(), boost::interprocess::read_write); - smo.truncate(sizeof(ServerReady)); - mr = new boost::interprocess::mapped_region(smo,boost::interprocess::read_write); - if(initialize) - sr = new(mr->get_address())ServerReady(); - else - sr = reinterpret_cast(mr->get_address()); - }; - - ~SharedMemory() - { - delete mr; - boost::interprocess::shared_memory_object::remove(name.c_str()); - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 31b4dfe15..779f9c9e0 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -19,7 +19,6 @@ class CampaignState; class CMapInfo; struct PlayerInfo; class PlayerColor; -struct SharedMemory; /// Struct which describes the name, the color, the starting bonus of a player struct DLL_LINKAGE PlayerSettings @@ -157,7 +156,6 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState { boost::mutex stateMutex; std::string uuid; - std::shared_ptr shm; LobbyInfo() {} diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 5b923acb7..5526d2731 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -25,7 +25,6 @@ #include "WaterAdopter.h" #include "WaterProxy.h" #include "TownPlacer.h" -#include VCMI_LIB_NAMESPACE_BEGIN diff --git a/mapeditor/resourceExtractor/ResourceConverter.cpp b/mapeditor/resourceExtractor/ResourceConverter.cpp index fc08b51a8..f413c40dc 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.cpp +++ b/mapeditor/resourceExtractor/ResourceConverter.cpp @@ -19,8 +19,8 @@ #include "BitmapHandler.h" #include "Animation.h" -#include "boost/filesystem/path.hpp" -#include "boost/locale.hpp" +#include +#include void ResourceConverter::convertExtractedResourceFiles(ConversionOptions conversionOptions) { diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 8a9ad301e..028af34a8 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -33,8 +33,6 @@ #include #include #include "lib/CAndroidVMHelper.h" -#elif !defined(VCMI_IOS) -#include "../lib/Interprocess.h" #endif #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" @@ -142,9 +140,9 @@ CVCMIServer::CVCMIServer(boost::program_options::variables_map & opts) catch(...) { logNetwork->info("Port %d is busy, trying to use random port instead", port); - if(cmdLineOptions.count("run-by-client") && !cmdLineOptions.count("enable-shm")) + if(cmdLineOptions.count("run-by-client")) { - logNetwork->error("Cant pass port number to client without shared memory!", port); + logNetwork->error("Port must be specified when run-by-client is used!!"); exit(0); } acceptor = std::make_shared(*io, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), 0)); @@ -166,17 +164,6 @@ void CVCMIServer::run() if(!restartGameplay) { this->announceLobbyThread = std::make_unique(&CVCMIServer::threadAnnounceLobby, this); -#if !defined(VCMI_MOBILE) - if(cmdLineOptions.count("enable-shm")) - { - std::string sharedMemoryName = "vcmi_memory"; - if(cmdLineOptions.count("enable-shm-uuid") && cmdLineOptions.count("uuid")) - { - sharedMemoryName += "_" + cmdLineOptions["uuid"].as(); - } - shm = std::make_shared(sharedMemoryName); - } -#endif startAsyncAccept(); if(!remoteConnectionsThread && cmdLineOptions.count("lobby")) @@ -189,11 +176,6 @@ void CVCMIServer::run() CAndroidVMHelper vmHelper; vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "onServerReady"); #endif -#elif !defined(VCMI_IOS) - if(shm) - { - shm->sr->setToReadyAndNotify(port); - } #endif } @@ -993,8 +975,6 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o ("version,v", "display version information and exit") ("run-by-client", "indicate that server launched by client on same machine") ("uuid", po::value(), "") - ("enable-shm-uuid", "use UUID for shared memory identifier") - ("enable-shm", "enable usage of shared memory") ("port", po::value(), "port at which server will listen to connections from client") ("lobby", po::value(), "address to remote lobby") ("lobby-port", po::value(), "port at which server connect to remote lobby") diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 879298a8c..285bcd440 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -23,7 +23,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CMapInfo; struct CPackForLobby; -struct SharedMemory; struct StartInfo; struct LobbyInfo; From 7f72f7a82cb813ca3db1b9d65351f54771e1cba7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 28 Jul 2023 14:17:06 +0300 Subject: [PATCH 0008/1248] Cleanup server connection code a bit --- client/CMT.cpp | 2 +- client/CServerHandler.cpp | 10 ++++------ client/CServerHandler.h | 2 +- lib/serializer/Connection.cpp | 6 +++--- server/CVCMIServer.cpp | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 99235e185..4dce2fecb 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -151,7 +151,7 @@ int main(int argc, char * argv[]) { po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); } - catch(std::exception &e) + catch(boost::program_options::error &e) { std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 815633f18..112af7f6c 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -169,7 +169,7 @@ void CServerHandler::startLocalServerAndConnect() CInfoWindow::showInfoDialog(errorMsg, {}); return; } - catch(...) + catch(std::runtime_error & error) { //no connection means that port is not busy and we can start local server } @@ -236,9 +236,7 @@ void CServerHandler::startLocalServerAndConnect() th->update(); //put breakpoint here to attach to server before it does something stupid - const ui16 port = 0; - - justConnectToServer(localhostAddress, port); + justConnectToServer(localhostAddress, 0); logNetwork->trace("\tConnecting to the server: %d ms", th->getDiff()); } @@ -256,9 +254,9 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po port ? port : getHostPort(), NAME, uuid); } - catch(...) + catch(std::runtime_error & error) { - logNetwork->error("\nCannot establish connection! Retrying within 1 second"); + logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); boost::this_thread::sleep(boost::posix_time::seconds(1)); } } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 199aa04a8..e8b20117d 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -115,7 +115,7 @@ public: void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); void startLocalServerAndConnect(); - void justConnectToServer(const std::string &addr = "", const ui16 port = 0); + void justConnectToServer(const std::string & addr, const ui16 port); void applyPacksOnLobbyScreen(); void stopServerConnection(); diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index ac007fa0a..3029aaf91 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -82,7 +82,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, if(error) { logNetwork->error("Problem with resolving: \n%s", error.message()); - throw std::runtime_error("Can't establish connection: Problem with resolving"); + throw std::runtime_error("Problem with resolving"); } pom = endpoint_iterator; if(pom != end) @@ -90,7 +90,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, else { logNetwork->error("Critical problem: No endpoints found!"); - throw std::runtime_error("Can't establish connection: No endpoints found!"); + throw std::runtime_error("No endpoints found!"); } while(pom != end) { @@ -109,7 +109,7 @@ CConnection::CConnection(const std::string & host, ui16 port, std::string Name, } else { - throw std::runtime_error("Can't establish connection: Failed to connect!"); + throw std::runtime_error("Failed to connect!"); } endpoint_iterator++; } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 028af34a8..8ab329289 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -987,7 +987,7 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o { po::store(po::parse_command_line(argc, argv, opts), options); } - catch(po::error & e) + catch(boost::program_options::error & e) { std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; } From 8ea7e9118964f4189e338b6026245f85ab1faa82 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 17:59:18 +0300 Subject: [PATCH 0009/1248] moved mod-related files to new directory --- cmake_modules/VCMI_lib.cmake | 10 ++++++---- lib/{ => modding}/CModHandler.cpp | 0 lib/{ => modding}/CModHandler.h | 0 lib/{ => modding}/CModVersion.cpp | 0 lib/{ => modding}/CModVersion.h | 0 5 files changed, 6 insertions(+), 4 deletions(-) rename lib/{ => modding}/CModHandler.cpp (100%) rename lib/{ => modding}/CModHandler.h (100%) rename lib/{ => modding}/CModVersion.cpp (100%) rename lib/{ => modding}/CModVersion.h (100%) diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 153be71d0..e285915ab 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -115,6 +115,9 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.cpp ${MAIN_LIB_DIR}/mapping/ObstacleProxy.cpp + ${MAIN_LIB_DIR}/modding/CModHandler.cpp + ${MAIN_LIB_DIR}/modding/CModVersion.cpp + ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp ${MAIN_LIB_DIR}/pathfinder/CPathfinder.cpp ${MAIN_LIB_DIR}/pathfinder/NodeStorage.cpp @@ -231,8 +234,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CGameInterface.cpp ${MAIN_LIB_DIR}/CGeneralTextHandler.cpp ${MAIN_LIB_DIR}/CHeroHandler.cpp - ${MAIN_LIB_DIR}/CModHandler.cpp - ${MAIN_LIB_DIR}/CModVersion.cpp ${MAIN_LIB_DIR}/CPlayerState.cpp ${MAIN_LIB_DIR}/CRandomGenerator.cpp ${MAIN_LIB_DIR}/CScriptingModule.cpp @@ -450,6 +451,9 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.h ${MAIN_LIB_DIR}/mapping/ObstacleProxy.h + ${MAIN_LIB_DIR}/modding/CModHandler.h + ${MAIN_LIB_DIR}/modding/CModVersion.h + ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h ${MAIN_LIB_DIR}/pathfinder/CPathfinder.h @@ -563,8 +567,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CGameInterface.h ${MAIN_LIB_DIR}/CGeneralTextHandler.h ${MAIN_LIB_DIR}/CHeroHandler.h - ${MAIN_LIB_DIR}/CModHandler.h - ${MAIN_LIB_DIR}/CModVersion.h ${MAIN_LIB_DIR}/CondSh.h ${MAIN_LIB_DIR}/ConstTransitivePtr.h ${MAIN_LIB_DIR}/Color.h diff --git a/lib/CModHandler.cpp b/lib/modding/CModHandler.cpp similarity index 100% rename from lib/CModHandler.cpp rename to lib/modding/CModHandler.cpp diff --git a/lib/CModHandler.h b/lib/modding/CModHandler.h similarity index 100% rename from lib/CModHandler.h rename to lib/modding/CModHandler.h diff --git a/lib/CModVersion.cpp b/lib/modding/CModVersion.cpp similarity index 100% rename from lib/CModVersion.cpp rename to lib/modding/CModVersion.cpp diff --git a/lib/CModVersion.h b/lib/modding/CModVersion.h similarity index 100% rename from lib/CModVersion.h rename to lib/modding/CModVersion.h From 62fddca21ebbc8753126a113be0c677d6f010ac7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 30 Jul 2023 20:12:25 +0300 Subject: [PATCH 0010/1248] Split massive CModHandler class/file into multiple parts: - IdentifierStorage is now a separate handler in VLC - Renamed ModHandler::Incompatibility exception to ModIncompatibility - Extracted ModScope namespace from ModHandler - Extracted ModUtilities namespace from ModHandler - Split CModHandler.cpp on per-class basis - Replaced some direct members with unique_ptr to reduce header includes --- client/ClientCommandManager.cpp | 9 +- client/adventureMap/CList.cpp | 1 - client/lobby/CLobbyScreen.cpp | 4 +- client/lobby/RandomMapTab.cpp | 1 - client/lobby/SelectionTab.cpp | 1 - client/render/Graphics.cpp | 5 +- client/renderSDL/CBitmapFont.cpp | 2 +- client/widgets/MiscWidgets.cpp | 1 - client/windows/CCastleInterface.cpp | 1 - client/windows/CCreatureWindow.cpp | 1 - client/windows/CKingdomInterface.cpp | 1 - client/windows/GUIClasses.cpp | 1 - cmake_modules/VCMI_lib.cmake | 10 + launcher/modManager/cmodmanager.cpp | 4 +- lib/CArtHandler.cpp | 7 +- lib/CCreatureHandler.cpp | 10 +- lib/CCreatureSet.cpp | 4 +- lib/CGameInfoCallback.cpp | 1 - lib/CGeneralTextHandler.cpp | 2 +- lib/CHeroHandler.cpp | 22 +- lib/CSkillHandler.cpp | 8 +- lib/CTownHandler.cpp | 41 +- lib/GameConstants.cpp | 15 +- lib/IGameCallback.cpp | 5 +- lib/IHandlerBase.cpp | 8 +- lib/JsonDetail.cpp | 7 +- lib/JsonNode.cpp | 16 +- lib/JsonRandom.cpp | 21 +- lib/NetPacksLib.cpp | 1 - lib/ObstacleHandler.cpp | 5 +- lib/RiverHandler.cpp | 2 +- lib/RoadHandler.cpp | 2 +- lib/ScriptHandler.cpp | 1 - lib/StartInfo.cpp | 6 +- lib/TerrainHandler.cpp | 11 +- lib/VCMI_Lib.cpp | 10 +- lib/VCMI_Lib.h | 2 + lib/battle/CBattleInfoCallback.cpp | 1 - lib/bonuses/Bonus.cpp | 8 +- lib/bonuses/Limiters.cpp | 1 - lib/campaign/CampaignHandler.cpp | 16 +- lib/filesystem/Filesystem.cpp | 4 +- lib/gameState/CGameState.cpp | 3 +- .../AObjectTypeHandler.cpp | 4 +- .../CObjectClassesHandler.cpp | 12 +- .../CommonConstructors.cpp | 11 +- .../DwellingInstanceConstructor.cpp | 4 +- .../ShipyardInstanceConstructor.cpp | 4 +- lib/mapObjects/CGHeroInstance.cpp | 4 +- lib/mapObjects/CGTownInstance.cpp | 4 +- lib/mapObjects/CQuest.cpp | 9 +- lib/mapObjects/MiscObjects.cpp | 8 +- lib/mapObjects/ObjectTemplate.cpp | 4 +- lib/mapping/CMapHeader.h | 2 +- lib/mapping/CMapInfo.cpp | 1 - lib/mapping/CMapService.cpp | 6 +- lib/mapping/MapFormatH3M.cpp | 1 - lib/mapping/MapFormatJson.cpp | 15 +- lib/mapping/MapIdentifiersH3M.cpp | 10 +- lib/modding/CModHandler.cpp | 824 ++---------------- lib/modding/CModHandler.h | 313 +------ lib/modding/CModInfo.cpp | 141 +++ lib/modding/CModInfo.h | 80 ++ lib/modding/CModVersion.h | 6 + lib/modding/ContentTypeHandler.cpp | 249 ++++++ lib/modding/ContentTypeHandler.h | 77 ++ lib/modding/IdentifierStorage.cpp | 339 +++++++ lib/modding/IdentifierStorage.h | 110 +++ lib/modding/ModIncompatibility.h | 42 + lib/modding/ModScope.h | 53 ++ lib/modding/ModUtility.cpp | 77 ++ lib/modding/ModUtility.h | 25 + lib/registerTypes/RegisterTypes.h | 1 - lib/registerTypes/TypesClientPacks1.cpp | 1 - lib/registerTypes/TypesClientPacks2.cpp | 1 - lib/registerTypes/TypesLobbyPacks.cpp | 1 - lib/registerTypes/TypesMapObjects1.cpp | 1 - lib/registerTypes/TypesMapObjects2.cpp | 1 - lib/registerTypes/TypesMapObjects3.cpp | 1 - lib/registerTypes/TypesServerPacks.cpp | 1 - lib/rewardable/Info.cpp | 6 +- lib/rmg/CRmgTemplate.cpp | 4 +- lib/rmg/CRmgTemplateStorage.cpp | 1 - lib/rmg/modificators/RoadPlacer.cpp | 5 +- lib/serializer/JsonSerializeFormat.h | 4 +- lib/spells/CSpellHandler.cpp | 9 +- lib/spells/TargetCondition.cpp | 9 +- mapeditor/graphics.cpp | 1 - mapeditor/inspector/townbulidingswidget.cpp | 1 - mapeditor/mainwindow.cpp | 8 +- mapeditor/mapcontroller.cpp | 3 +- mapeditor/mapcontroller.h | 2 +- mapeditor/maphandler.cpp | 1 - mapeditor/mapsettings.cpp | 3 +- mapeditor/validator.cpp | 3 +- scripting/lua/LuaScriptingContext.cpp | 6 +- server/CGameHandler.cpp | 6 +- server/CVCMIServer.cpp | 1 - server/PlayerMessageProcessor.cpp | 5 +- test/scripting/ScriptFixture.cpp | 4 +- test/spells/effects/TimedTest.cpp | 4 +- 101 files changed, 1532 insertions(+), 1279 deletions(-) create mode 100644 lib/modding/CModInfo.cpp create mode 100644 lib/modding/CModInfo.h create mode 100644 lib/modding/ContentTypeHandler.cpp create mode 100644 lib/modding/ContentTypeHandler.h create mode 100644 lib/modding/IdentifierStorage.cpp create mode 100644 lib/modding/IdentifierStorage.h create mode 100644 lib/modding/ModIncompatibility.h create mode 100644 lib/modding/ModScope.h create mode 100644 lib/modding/ModUtility.cpp create mode 100644 lib/modding/ModUtility.h diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 438e4f2f6..9de0ba3d9 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -31,8 +31,10 @@ #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/filesystem/Filesystem.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/ContentTypeHandler.h" +#include "../lib/modding/ModUtility.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" #include "../lib/VCMIDirs.h" #include "CMT.h" @@ -229,7 +231,8 @@ void ClientCommandManager::handleGetConfigCommand() for(auto contentName : contentNames) { - auto& content = (*VLC->modh->content)[contentName]; + auto const & handler = *VLC->modh->content; + auto const & content = handler[contentName]; auto contentOutPath = outPath / contentName; boost::filesystem::create_directories(contentOutPath); @@ -242,7 +245,7 @@ void ClientCommandManager::handleGetConfigCommand() { const JsonNode& object = nameAndObject.second; - std::string name = CModHandler::makeFullIdentifier(object.meta, contentName, nameAndObject.first); + std::string name = ModUtility::makeFullIdentifier(object.meta, contentName, nameAndObject.first); boost::algorithm::replace_all(name, ":", "_"); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index e53968d9f..7844bced8 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -25,7 +25,6 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index d38e209f7..777eaf9a5 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -24,11 +24,11 @@ #include "../../CCallback.h" #include "../CGameInfo.h" -#include "../../lib/CModHandler.h" #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/modding/ModIncompatibility.h" #include "../../lib/rmg/CMapGenOptions.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) @@ -131,7 +131,7 @@ void CLobbyScreen::startScenario(bool allowOnlyAI) CSH->sendStartGame(allowOnlyAI); buttonStart->block(true); } - catch(CModHandler::Incompatibility & e) + catch(ModIncompatibility & e) { logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index dfb0157cb..9f8ca42ac 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -31,7 +31,6 @@ #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/rmg/CMapGenOptions.h" -#include "../../lib/CModHandler.h" #include "../../lib/rmg/CRmgTemplateStorage.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/RoadHandler.h" diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 84b430f32..a7e698e33 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -33,7 +33,6 @@ #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/campaign/CampaignState.h" diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 440653255..942ee5ca5 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -27,7 +27,8 @@ #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" -#include "../lib/CModHandler.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/ModScope.h" #include "CGameInfo.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" @@ -100,7 +101,7 @@ void Graphics::loadPaletteAndColors() void Graphics::initializeBattleGraphics() { auto allConfigs = VLC->modh->getActiveMods(); - allConfigs.insert(allConfigs.begin(), CModHandler::scopeBuiltin()); + allConfigs.insert(allConfigs.begin(), ModScope::scopeBuiltin()); for(auto & mod : allConfigs) { if(!CResourceHandler::get(mod)->existsResource(ResourceID("config/battles_graphics.json"))) diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 0810b649b..c13cd7161 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -14,11 +14,11 @@ #include "../CGameInfo.h" #include "../render/Colors.h" -#include "../../lib/CModHandler.h" #include "../../lib/Languages.h" #include "../../lib/Rect.h" #include "../../lib/TextOperations.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/vcmi_endian.h" #include "../../lib/VCMI_Lib.h" diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0fdc7f18e..bf1de9e63 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -27,7 +27,6 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CGHeroInstance.h" diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index e3f743aec..c24ba5c1e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -40,7 +40,6 @@ #include "../../lib/CBuildingHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CTownHandler.h" diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 1dc950fec..0511bb649 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -29,7 +29,6 @@ #include "../../lib/CStack.h" #include "../../lib/CBonusTypeHandler.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 300092027..d92c6936f 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -32,7 +32,6 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/CSkillHandler.h" #include "../../lib/CTownHandler.h" diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index e921cdf88..e7888cd3e 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -63,7 +63,6 @@ #include "../lib/CCreatureHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" #include "../lib/GameSettings.h" #include "../lib/CondSh.h" #include "../lib/CSkillHandler.h" diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index e285915ab..7152a5327 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -116,7 +116,11 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/ObstacleProxy.cpp ${MAIN_LIB_DIR}/modding/CModHandler.cpp + ${MAIN_LIB_DIR}/modding/CModInfo.cpp ${MAIN_LIB_DIR}/modding/CModVersion.cpp + ${MAIN_LIB_DIR}/modding/ContentTypeHandler.cpp + ${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp + ${MAIN_LIB_DIR}/modding/ModUtility.cpp ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp ${MAIN_LIB_DIR}/pathfinder/CPathfinder.cpp @@ -452,7 +456,13 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/ObstacleProxy.h ${MAIN_LIB_DIR}/modding/CModHandler.h + ${MAIN_LIB_DIR}/modding/CModInfo.h ${MAIN_LIB_DIR}/modding/CModVersion.h + ${MAIN_LIB_DIR}/modding/ContentTypeHandler.h + ${MAIN_LIB_DIR}/modding/IdentifierStorage.h + ${MAIN_LIB_DIR}/modding/ModIncompatibility.h + ${MAIN_LIB_DIR}/modding/ModScope.h + ${MAIN_LIB_DIR}/modding/ModUtility.h ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index d76452ada..27824b826 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -13,7 +13,9 @@ #include "../../lib/VCMIDirs.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CZipLoader.h" -#include "../../lib/CModHandler.h" +#include "../../lib/modding/CModHandler.h" +#include "../../lib/modding/CModInfo.h" +#include "../../lib/modding/IdentifierStorage.h" #include "../jsonutils.h" #include "../launcherdirs.h" diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index d39030400..abbaf32eb 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -13,7 +13,6 @@ #include "ArtifactUtils.h" #include "CGeneralTextHandler.h" -#include "CModHandler.h" #include "GameSettings.h" #include "mapObjects/MapObjects.h" #include "StringConstants.h" @@ -450,7 +449,7 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode const JsonNode & warMachine = node["warMachine"]; if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) { - VLC->modh->identifiers.requestIdentifier("creature", warMachine, [=](si32 id) + VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id) { art->warMachine = CreatureID(id); @@ -459,7 +458,7 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode }); } - VLC->modh->identifiers.requestIdentifier(scope, "object", "artifact", [=](si32 index) + VLC->identifiers()->requestIdentifier(scope, "object", "artifact", [=](si32 index) { JsonNode conf; conf.setMeta(scope); @@ -599,7 +598,7 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) { for(const auto & component : node["components"].Vector()) { - VLC->modh->identifiers.requestIdentifier("artifact", component, [=](si32 id) + VLC->identifiers()->requestIdentifier("artifact", component, [=](si32 id) { // when this code is called both combinational art as well as component are loaded // so it is safe to access any of them diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index f2195f49f..1e3802408 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -15,7 +15,6 @@ #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" #include "CTownHandler.h" -#include "CModHandler.h" #include "GameSettings.h" #include "StringConstants.h" #include "bonuses/Limiters.h" @@ -24,6 +23,7 @@ #include "serializer/JsonUpdater.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -405,7 +405,7 @@ CCreatureHandler::CCreatureHandler() const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const { - std::optional index = VLC->modh->identifiers.getIdentifier(scope, "creature", identifier); + std::optional index = VLC->identifiers()->getIdentifier(scope, "creature", identifier); if(!index) throw std::runtime_error("Creature not found "+identifier); @@ -627,7 +627,7 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json JsonNode advMapFile = node["graphics"]["map"]; JsonNode advMapMask = node["graphics"]["mapMask"]; - VLC->modh->identifiers.requestIdentifier(scope, "object", "monster", [=](si32 index) + VLC->identifiers()->requestIdentifier(scope, "object", "monster", [=](si32 index) { JsonNode conf; conf.setMeta(scope); @@ -920,14 +920,14 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c } } - VLC->modh->identifiers.requestIdentifier("faction", config["faction"], [=](si32 faction) + VLC->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction) { creature->faction = FactionID(faction); }); for(const JsonNode &value : config["upgrades"].Vector()) { - VLC->modh->identifiers.requestIdentifier("creature", value, [=](si32 identifier) + VLC->identifiers()->requestIdentifier("creature", value, [=](si32 identifier) { creature->upgrades.insert(CreatureID(identifier)); }); diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 084fb1e40..42f4185f2 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -14,9 +14,9 @@ #include "CConfigHandler.h" #include "CCreatureHandler.h" #include "VCMI_Lib.h" -#include "CModHandler.h" #include "GameSettings.h" #include "mapObjects/CGHeroInstance.h" +#include "modding/ModScope.h" #include "IGameCallback.h" #include "CGeneralTextHandler.h" #include "spells/CSpellHandler.h" @@ -1046,7 +1046,7 @@ void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) std::string typeName; handler.serializeString("type", typeName); if(!typeName.empty()) - setType(VLC->creh->getCreature(CModHandler::scopeMap(), typeName)); + setType(VLC->creh->getCreature(ModScope::scopeMap(), typeName)); } } diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 2eddb7372..2a44004c1 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -18,7 +18,6 @@ #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo #include "NetPacks.h" // for InfoWindow -#include "CModHandler.h" #include "GameSettings.h" #include "TerrainHandler.h" #include "spells/CSpellHandler.h" diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index d949f1841..d1ec14b91 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -12,9 +12,9 @@ #include "filesystem/Filesystem.h" #include "CConfigHandler.h" -#include "CModHandler.h" #include "GameSettings.h" #include "mapObjects/CQuest.h" +#include "modding/CModHandler.h" #include "VCMI_Lib.h" #include "Languages.h" #include "TextOperations.h" diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index bb688b39d..77cb0067c 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -18,7 +18,6 @@ #include "battle/BattleHex.h" #include "CCreatureHandler.h" #include "GameSettings.h" -#include "CModHandler.h" #include "CTownHandler.h" #include "CSkillHandler.h" #include "BattleFieldHandler.h" @@ -26,6 +25,7 @@ #include "bonuses/Updaters.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -269,7 +269,7 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js for(auto skillPair : node["secondarySkills"].Struct()) { int probability = static_cast(skillPair.second.Integer()); - VLC->modh->identifiers.requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) + VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) { if(heroClass->secSkillProbability.size() <= skillID) heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later @@ -277,7 +277,7 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js }); } - VLC->modh->identifiers.requestIdentifier ("creature", node["commander"], + VLC->identifiers()->requestIdentifier ("creature", node["commander"], [=](si32 commanderID) { heroClass->commander = VLC->creh->objects[commanderID]; @@ -288,20 +288,20 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js { int value = static_cast(tavern.second.Float()); - VLC->modh->identifiers.requestIdentifier(tavern.second.meta, "faction", tavern.first, + VLC->identifiers()->requestIdentifier(tavern.second.meta, "faction", tavern.first, [=](si32 factionID) { heroClass->selectionProbability[FactionID(factionID)] = value; }); } - VLC->modh->identifiers.requestIdentifier("faction", node["faction"], + VLC->identifiers()->requestIdentifier("faction", node["faction"], [=](si32 factionID) { heroClass->faction = factionID; }); - VLC->modh->identifiers.requestIdentifier(scope, "object", "hero", [=](si32 index) + VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) { JsonNode classConf = node["mapObject"]; classConf["heroClass"].String() = identifier; @@ -444,7 +444,7 @@ CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & n loadHeroSkills(hero, node); loadHeroSpecialty(hero, node); - VLC->modh->identifiers.requestIdentifier("heroClass", node["class"], + VLC->identifiers()->requestIdentifier("heroClass", node["class"], [=](si32 classID) { hero->heroClass = classes[HeroClassID(classID)]; @@ -468,7 +468,7 @@ void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); - VLC->modh->identifiers.requestIdentifier("creature", source["creature"], [=](si32 creature) + VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) { hero->initialArmy[i].creature = CreatureID(creature); }); @@ -485,7 +485,7 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const size_t currentIndex = hero->secSkillsInit.size(); hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); - VLC->modh->identifiers.requestIdentifier("skill", set["skill"], [=](si32 id) + VLC->identifiers()->requestIdentifier("skill", set["skill"], [=](si32 id) { hero->secSkillsInit[currentIndex].first = SecondarySkill(id); }); @@ -501,7 +501,7 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const for(const JsonNode & spell : node["spellbook"].Vector()) { - VLC->modh->identifiers.requestIdentifier("spell", spell, + VLC->identifiers()->requestIdentifier("spell", spell, [=](si32 spellID) { hero->spells.insert(SpellID(spellID)); @@ -624,7 +624,7 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) std::function specialtyLoader = [creatureNode, hero, prepSpec] { - VLC->modh->identifiers.requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) + VLC->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) { for (const auto & bonus : createCreatureSpecialty(CreatureID(creature))) hero->specialty.push_back(prepSpec(bonus)); diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 95b69a2ff..96a017208 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -16,10 +16,12 @@ #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModUtility.h" +#include "modding/ModScope.h" #include "JsonNode.h" -#include "CModHandler.h" #include "StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -262,7 +264,7 @@ std::vector CSkillHandler::getDefaultAllowed() const si32 CSkillHandler::decodeSkill(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "skill", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "skill", identifier); if(rawId) return rawId.value(); else @@ -276,7 +278,7 @@ std::string CSkillHandler::encodeSkill(const si32 index) std::string CSkillHandler::encodeSkillWithType(const si32 index) { - return CModHandler::makeFullIdentifier("", "skill", encodeSkill(index)); + return ModUtility::makeFullIdentifier("", "skill", encodeSkill(index)); } VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 78c849e92..6d11f717f 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -15,7 +15,6 @@ #include "JsonNode.h" #include "StringConstants.h" #include "CCreatureHandler.h" -#include "CModHandler.h" #include "CHeroHandler.h" #include "CArtHandler.h" #include "GameSettings.h" @@ -27,6 +26,8 @@ #include "ResourceSet.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -689,7 +690,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons stringID % ret->town->faction->getNameTranslated())); } - VLC->modh->identifiers.requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) + VLC->identifiers()->requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) { ret->upgrade = BuildingID(identifier); }); @@ -721,21 +722,21 @@ void CTownHandler::loadStructure(CTown &town, const std::string & stringID, cons ret->building = nullptr; ret->buildable = nullptr; - VLC->modh->identifiers.tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable { ret->building = town.buildings[BuildingID(identifier)]; }); if (source["builds"].isNull()) { - VLC->modh->identifiers.tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable { ret->building = town.buildings[BuildingID(identifier)]; }); } else { - VLC->modh->identifiers.requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable + VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable { ret->buildable = town.buildings[BuildingID(identifier)]; }); @@ -786,7 +787,7 @@ void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const auto & dst = dstBox[k]; const auto & src = srcBox[k]; - VLC->modh->identifiers.requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) + VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) { dst = BuildingID(identifier); }); @@ -812,7 +813,7 @@ void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const town.clientInfo.towerIconSmall = source["towerIconSmall"].String(); town.clientInfo.towerIconLarge = source["towerIconLarge"].String(); - VLC->modh->identifiers.requestIdentifier("creature", source["shooter"], [&town](si32 creature) + VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature) { auto crId = CreatureID(creature); if((*VLC->creh)[crId]->animation.missleFrameAngles.empty()) @@ -915,14 +916,14 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code { - VLC->modh->identifiers.requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) + VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) { town->moatAbility = SpellID(ability); }); } else { - VLC->modh->identifiers.requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) + VLC->identifiers()->requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) { town->moatAbility = SpellID(ability); }); @@ -949,7 +950,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) for (size_t j=0; jmodh->identifiers.requestIdentifier("creature", level[j], [=](si32 creature) + VLC->identifiers()->requestIdentifier("creature", level[j], [=](si32 creature) { town->creatures[i][j] = CreatureID(creature); }); @@ -962,7 +963,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) { int chance = static_cast(node.second.Float()); - VLC->modh->identifiers.requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) + VLC->identifiers()->requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) { VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; }); @@ -972,7 +973,7 @@ void CTownHandler::loadTown(CTown * town, const JsonNode & source) { int chance = static_cast(node.second.Float()); - VLC->modh->identifiers.requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) + VLC->identifiers()->requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) { VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; }); @@ -1032,7 +1033,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode faction->boatType = EBoatId::NONE; if (!source["boat"].isNull()) { - VLC->modh->identifiers.requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) + VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) { faction->boatType = BoatId(boatTypeID); }); @@ -1054,7 +1055,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode faction->nativeTerrain = ETerrainId::NONE; if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none") { - VLC->modh->identifiers.requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ + VLC->identifiers()->requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ faction->nativeTerrain = TerrainId(index); auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain); @@ -1093,7 +1094,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod info.icons[1][0] = 8 + object->index * 4 + 2; info.icons[1][1] = 8 + object->index * 4 + 3; - VLC->modh->identifiers.requestIdentifier(scope, "object", "town", [=](si32 index) + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { // register town once objects are loaded JsonNode config = data["town"]["mapObject"]; @@ -1136,7 +1137,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod info.icons[1][0] = object->index * 2 + 0; info.icons[1][1] = object->index * 2 + 1; - VLC->modh->identifiers.requestIdentifier(scope, "object", "town", [=](si32 index) + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { // register town once objects are loaded JsonNode config = data["town"]["mapObject"]; @@ -1154,7 +1155,7 @@ void CTownHandler::loadRandomFaction() static const ResourceID randomFactionPath("config/factions/random.json"); JsonNode randomFactionJson(randomFactionPath); - randomFactionJson.setMeta(CModHandler::scopeBuiltin(), true); + randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); } @@ -1183,7 +1184,7 @@ void CTownHandler::initializeRequirements() logMod->warn("Entry contains: "); logMod->warn(node.toJson()); } - return BuildingID(VLC->modh->identifiers.getIdentifier(requirement.town->getBuildingScope(), node.Vector()[0]).value()); + return BuildingID(VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node.Vector()[0]).value()); }); } requirementsToLoad.clear(); @@ -1198,7 +1199,7 @@ void CTownHandler::initializeOverridden() for(const auto & b : jsonNode.Vector()) { - auto bid = BuildingID(VLC->modh->identifiers.getIdentifier(scope, b).value()); + auto bid = BuildingID(VLC->identifiers()->getIdentifier(scope, b).value()); bidHelper.building->overrideBids.insert(bid); } } @@ -1213,7 +1214,7 @@ void CTownHandler::initializeWarMachines() CTown * t = p.first; JsonNode creatureKey = p.second; - auto ret = VLC->modh->identifiers.getIdentifier("creature", creatureKey, false); + auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false); if(ret) { diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 5a1ffe19a..5fcd99513 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -25,6 +25,8 @@ #include #include +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" #include "VCMI_Lib.h" #include "CArtHandler.h"//todo: remove #include "CCreatureHandler.h"//todo: remove @@ -32,7 +34,6 @@ #include "CSkillHandler.h"//todo: remove #include "StringConstants.h" #include "CGeneralTextHandler.h" -#include "CModHandler.h"//todo: remove #include "TerrainHandler.h" //TODO: remove #include "BattleFieldHandler.h" #include "ObstacleHandler.h" @@ -65,7 +66,7 @@ namespace GameConstants si32 HeroTypeID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); if(rawId) return rawId.value(); else @@ -89,7 +90,7 @@ const Artifact * ArtifactID::toArtifact(const ArtifactService * service) const si32 ArtifactID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "artifact", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); if(rawId) return rawId.value(); else @@ -113,7 +114,7 @@ const Creature * CreatureID::toCreature(const CreatureService * creatures) const si32 CreatureID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); if(rawId) return rawId.value(); else @@ -142,7 +143,7 @@ const spells::Spell * SpellID::toSpell(const spells::Service * service) const si32 SpellID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "spell", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); if(rawId) return rawId.value(); else @@ -205,7 +206,7 @@ const FactionID FactionID::NEUTRAL = FactionID(9); si32 FactionID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), entityType(), identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) return rawId.value(); else @@ -225,7 +226,7 @@ std::string FactionID::entityType() si32 TerrainID::decode(const std::string & identifier) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), entityType(), identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) return rawId.value(); else diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 8c5fc9e42..76c3c10fd 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -15,7 +15,6 @@ #include "CSkillHandler.h"// for CSkill #include "NetPacks.h" #include "CBonusTypeHandler.h" -#include "CModHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" #include "bonuses/CBonusSystemNode.h" @@ -38,6 +37,10 @@ #include "gameState/CGameStateCampaign.h" #include "gameState/TavernHeroesPool.h" #include "mapping/CMap.h" +#include "modding/CModHandler.h" +#include "modding/CModInfo.h" +#include "modding/IdentifierStorage.h" +#include "modding/CModVersion.h" #include "CPlayerState.h" #include "GameSettings.h" #include "ScriptHandler.h" diff --git a/lib/IHandlerBase.cpp b/lib/IHandlerBase.cpp index 81475050b..ec51814a0 100644 --- a/lib/IHandlerBase.cpp +++ b/lib/IHandlerBase.cpp @@ -10,18 +10,20 @@ #include "StdInc.h" #include "IHandlerBase.h" -#include "CModHandler.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" +#include "modding/CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN std::string IHandlerBase::getScopeBuiltin() { - return CModHandler::scopeBuiltin(); + return ModScope::scopeBuiltin(); } void IHandlerBase::registerObject(const std::string & scope, const std::string & type_name, const std::string & name, si32 index) { - return VLC->modh->identifiers.registerObject(scope, type_name, name, index); + return VLC->modh->getIdentifiers().registerObject(scope, type_name, name, index); } VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index f6212256f..c056f434b 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -13,9 +13,10 @@ #include "VCMI_Lib.h" #include "TextOperations.h" -#include "CModHandler.h" #include "filesystem/Filesystem.h" +#include "modding/ModScope.h" +#include "modding/CModHandler.h" #include "ScopeGuard.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1007,7 +1008,7 @@ namespace bool testFilePresence(const std::string & scope, const ResourceID & resource) { std::set allowedScopes; - if(scope != CModHandler::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies + if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies { //NOTE: recursive dependencies are not allowed at the moment - update code if this changes bool found = true; @@ -1016,7 +1017,7 @@ namespace if(!found) return false; - allowedScopes.insert(CModHandler::scopeBuiltin()); // all mods can use H3 files + allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files } allowedScopes.insert(scope); // mods can use their own files diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 41edb62ab..8a5df4e53 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -19,8 +19,8 @@ #include "bonuses/Propagators.h" #include "bonuses/Updaters.h" #include "filesystem/Filesystem.h" +#include "modding/IdentifierStorage.h" #include "VCMI_Lib.h" //for identifier resolution -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "JsonDetail.h" #include "StringConstants.h" @@ -565,7 +565,7 @@ void JsonUtils::resolveIdentifier(si32 & var, const JsonNode & node, const std:: var = static_cast(value.Float()); break; case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(value, [&](si32 identifier) + VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) { var = identifier; }); @@ -590,7 +590,7 @@ void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) var = static_cast(value.Float()); break; case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(value, [&](si32 identifier) + VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) { var = identifier; }); @@ -610,7 +610,7 @@ void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) var[i] = static_cast(vec[i].Float()); break; case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(vec[i], [&var,i](si32 identifier) + VLC->identifiers()->requestIdentifier(vec[i], [&var,i](si32 identifier) { var[i] = identifier; }); @@ -638,7 +638,7 @@ void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var) var = static_cast(node.Float()); break; case JsonNode::JsonType::DATA_STRING: - VLC->modh->identifiers.requestIdentifier(node, [&](si32 identifier) + VLC->identifiers()->requestIdentifier(node, [&](si32 identifier) { var = identifier; }); @@ -699,7 +699,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) if(limiterType == "CREATURE_TYPE_LIMITER") { std::shared_ptr creatureLimiter = std::make_shared(); - VLC->modh->identifiers.requestIdentifier("creature", parameters[0], [=](si32 creature) + VLC->identifiers()->requestIdentifier("creature", parameters[0], [=](si32 creature) { creatureLimiter->setCreature(CreatureID(creature)); }); @@ -771,7 +771,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat { std::shared_ptr factionLimiter = std::make_shared(); - VLC->modh->identifiers.requestIdentifier("faction", parameters[0], [=](si32 faction) + VLC->identifiers()->requestIdentifier("faction", parameters[0], [=](si32 faction) { factionLimiter->faction = FactionID(faction); }); @@ -793,7 +793,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) std::shared_ptr terrainLimiter = std::make_shared(); if(!parameters.empty()) { - VLC->modh->identifiers.requestIdentifier("terrain", parameters[0], [=](si32 terrain) + VLC->identifiers()->requestIdentifier("terrain", parameters[0], [=](si32 terrain) { //TODO: support limiters //terrainLimiter->terrainType = terrain; diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 222d8aacf..04bc56096 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -17,7 +17,6 @@ #include "CRandomGenerator.h" #include "StringConstants.h" #include "VCMI_Lib.h" -#include "CModHandler.h" #include "CArtHandler.h" #include "CCreatureHandler.h" #include "CCreatureSet.h" @@ -25,6 +24,8 @@ #include "CSkillHandler.h" #include "IGameCallback.h" #include "mapObjects/IObjectInterface.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -105,7 +106,7 @@ namespace JsonRandom std::string resourceName = loadKey(value, rng, defaultResources); si32 resourceAmount = loadValue(value, rng, 0); - si32 resourceID(VLC->modh->identifiers.getIdentifier(value.meta, "resource", resourceName).value()); + si32 resourceID(VLC->identifiers()->getIdentifier(value.meta, "resource", resourceName).value()); TResources ret; ret[resourceID] = resourceAmount; @@ -146,7 +147,7 @@ namespace JsonRandom { for(const auto & pair : value.Struct()) { - SecondarySkill id(VLC->modh->identifiers.getIdentifier(pair.second.meta, "skill", pair.first).value()); + SecondarySkill id(VLC->identifiers()->getIdentifier(pair.second.meta, "skill", pair.first).value()); ret[id] = loadValue(pair.second, rng); } } @@ -157,7 +158,7 @@ namespace JsonRandom { IObjectInterface::cb->isAllowed(2, skill->getIndex()); auto scopeAndName = vstd::splitStringToPair(skill->getJsonKey(), ':'); - if(scopeAndName.first == CModHandler::scopeBuiltin() || scopeAndName.first == value.meta) + if(scopeAndName.first == ModScope::scopeBuiltin() || scopeAndName.first == value.meta) defaultSkills.insert(scopeAndName.second); else defaultSkills.insert(skill->getJsonKey()); @@ -167,7 +168,7 @@ namespace JsonRandom { auto key = loadKey(element, rng, defaultSkills); defaultSkills.erase(key); //avoid dupicates - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "skill", key)) + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "skill", key)) { SecondarySkill id(identifier.value()); ret[id] = loadValue(element, rng); @@ -180,7 +181,7 @@ namespace JsonRandom ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng) { if (value.getType() == JsonNode::JsonType::DATA_STRING) - return ArtifactID(VLC->modh->identifiers.getIdentifier("artifact", value).value()); + return ArtifactID(VLC->identifiers()->getIdentifier("artifact", value).value()); std::set allowedClasses; std::set allowedPositions; @@ -241,7 +242,7 @@ namespace JsonRandom SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells) { if (value.getType() == JsonNode::JsonType::DATA_STRING) - return SpellID(VLC->modh->identifiers.getIdentifier("spell", value).value()); + return SpellID(VLC->identifiers()->getIdentifier("spell", value).value()); if (!value["level"].isNull()) { @@ -255,7 +256,7 @@ namespace JsonRandom if (!value["school"].isNull()) { - int32_t schoolID = VLC->modh->identifiers.getIdentifier("spellSchool", value["school"]).value(); + int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value(); vstd::erase_if(spells, [=](const SpellID & spell) { @@ -284,7 +285,7 @@ namespace JsonRandom CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) { CStackBasicDescriptor stack; - stack.type = VLC->creh->objects[VLC->modh->identifiers.getIdentifier("creature", value["type"]).value()]; + stack.type = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", value["type"]).value()]; stack.count = loadValue(value, rng); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { @@ -320,7 +321,7 @@ namespace JsonRandom info.minAmount = static_cast(node["min"].Float()); info.maxAmount = static_cast(node["max"].Float()); } - const CCreature * crea = VLC->creh->objects[VLC->modh->identifiers.getIdentifier("creature", node["type"]).value()]; + const CCreature * crea = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", node["type"]).value()]; info.allowedCreatures.push_back(crea); if (node["upgradeChance"].Float() > 0) { diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index b1810d343..548bf3a1f 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -14,7 +14,6 @@ #include "CGeneralTextHandler.h" #include "CArtHandler.h" #include "CHeroHandler.h" -#include "CModHandler.h" #include "VCMI_Lib.h" #include "mapping/CMap.h" #include "spells/CSpellHandler.h" diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index b2f522456..fde260dd6 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -10,7 +10,8 @@ #include "StdInc.h" #include "ObstacleHandler.h" #include "BattleFieldHandler.h" -#include "CModHandler.h" +#include "modding/IdentifierStorage.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -94,7 +95,7 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js info->height = json["height"].Integer(); for(const auto & t : json["allowedTerrains"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", t, [info](int32_t identifier){ + VLC->identifiers()->requestIdentifier("terrain", t, [info](int32_t identifier){ info->allowedTerrains.emplace_back(identifier); }); } diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index ba7344dd0..1f4022391 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -10,9 +10,9 @@ #include "StdInc.h" #include "RiverHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index afaa1e282..ba8620f39 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -10,9 +10,9 @@ #include "StdInc.h" #include "RoadHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index 0fa1416af..c9528b109 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -17,7 +17,6 @@ #include "CGameInterface.h" #include "CScriptingModule.h" -#include "CModHandler.h" #include "VCMIDirs.h" #include "serializer/JsonDeserializer.h" diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index c722c80d5..a2026c330 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -11,13 +11,13 @@ #include "StartInfo.h" #include "CGeneralTextHandler.h" -#include "CModHandler.h" #include "VCMI_Lib.h" #include "rmg/CMapGenOptions.h" #include "mapping/CMapInfo.h" #include "campaign/CampaignState.h" #include "mapping/CMapHeader.h" #include "mapping/CMapService.h" +#include "modding/ModIncompatibility.h" VCMI_LIB_NAMESPACE_BEGIN @@ -74,12 +74,12 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const throw std::domain_error("ExceptionMapMissing"); auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); - CModHandler::Incompatibility::ModList modList; + ModIncompatibility::ModList modList; for(const auto & m : missingMods) modList.push_back({m.first, m.second.toString()}); if(!modList.empty()) - throw CModHandler::Incompatibility(std::move(modList)); + throw ModIncompatibility(std::move(modList)); //there must be at least one human player before game can be started std::map::const_iterator i; diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index fbdcc48df..c48436dfa 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -10,9 +10,10 @@ #include "StdInc.h" #include "TerrainHandler.h" -#include "CModHandler.h" #include "CGeneralTextHandler.h" #include "GameSettings.h" +#include "JsonNode.h" +#include "modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -67,7 +68,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const info->river = River::NO_RIVER; if(!json["river"].isNull()) { - VLC->modh->identifiers.requestIdentifier("river", json["river"], [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("river", json["river"], [info](int32_t identifier) { info->river = RiverId(identifier); }); @@ -87,7 +88,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const for(const auto & t : json["battleFields"].Vector()) { - VLC->modh->identifiers.requestIdentifier("battlefield", t, [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("battlefield", t, [info](int32_t identifier) { info->battleFields.emplace_back(identifier); }); @@ -95,7 +96,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const for(const auto & t : json["prohibitTransitions"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", t, [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("terrain", t, [info](int32_t identifier) { info->prohibitTransitions.emplace_back(identifier); }); @@ -105,7 +106,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const if(!json["rockTerrain"].isNull()) { - VLC->modh->identifiers.requestIdentifier("terrain", json["rockTerrain"], [info](int32_t identifier) + VLC->identifiers()->requestIdentifier("terrain", json["rockTerrain"], [info](int32_t identifier) { info->rockTerrain = TerrainId(identifier); }); diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 7199916a5..141b3acda 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -25,7 +25,10 @@ #include "spells/effects/Registry.h" #include "CSkillHandler.h" #include "CGeneralTextHandler.h" -#include "CModHandler.h" +#include "modding/CModHandler.h" +#include "modding/CModInfo.h" +#include "modding/IdentifierStorage.h" +#include "modding/CModVersion.h" #include "IGameEventsReceiver.h" #include "CStopWatch.h" #include "VCMIDirs.h" @@ -106,6 +109,11 @@ const IBonusTypeHandler * LibClasses::getBth() const return bth; } +const CIdentifierStorage * LibClasses::identifiers() const +{ + return &modh->getIdentifiers(); +} + const spells::effects::Registry * LibClasses::spellEffects() const { return spells::effects::GlobalRegistry::get(); diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index 5b95e4fb8..b6a7e0c4c 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -38,6 +38,7 @@ class CRmgTemplateStorage; class IHandlerBase; class IGameSettings; class GameSettings; +class CIdentifierStorage; #if SCRIPTING_ENABLED namespace scripting @@ -80,6 +81,7 @@ public: spells::effects::Registry * spellEffects() override; const IBonusTypeHandler * getBth() const; //deprecated + const CIdentifierStorage * identifiers() const; CArtHandler * arth; CHeroHandler * heroh; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index f207c7170..d0990a481 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -24,7 +24,6 @@ #include "../spells/CSpellHandler.h" #include "../mapObjects/CGTownInstance.h" #include "../BattleFieldHandler.h" -#include "../CModHandler.h" #include "../Rect.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 8bdd67cc3..6a24f983e 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -24,10 +24,10 @@ #include "../CGeneralTextHandler.h" #include "../CSkillHandler.h" #include "../CArtHandler.h" -#include "../CModHandler.h" #include "../TerrainHandler.h" #include "../StringConstants.h" #include "../battle/BattleInfo.h" +#include "../modding/ModUtility.h" VCMI_LIB_NAMESPACE_BEGIN @@ -146,10 +146,10 @@ static JsonNode subtypeToJson(BonusType type, int subtype) case BonusType::SPECIAL_PECULIAR_ENCHANT: case BonusType::SPECIAL_ADD_VALUE_ENCHANT: case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "spell", SpellID::encode(subtype))); + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "spell", SpellID::encode(subtype))); case BonusType::IMPROVED_NECROMANCY: case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); case BonusType::GENERATE_RESOURCE: return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]); default: @@ -162,7 +162,7 @@ static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) switch(type) { case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(CModHandler::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); default: return addInfo.toJsonNode(); } diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index eace34379..64186b908 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -21,7 +21,6 @@ #include "../CSkillHandler.h" #include "../CStack.h" #include "../CArtHandler.h" -#include "../CModHandler.h" #include "../TerrainHandler.h" #include "../StringConstants.h" #include "../battle/BattleInfo.h" diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index f4452ab7d..e239dec12 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -16,14 +16,16 @@ #include "../filesystem/CCompressedStream.h" #include "../filesystem/CMemoryStream.h" #include "../filesystem/CBinaryReader.h" +#include "../modding/IdentifierStorage.h" #include "../VCMI_Lib.h" #include "../CGeneralTextHandler.h" #include "../TextOperations.h" -#include "../CModHandler.h" #include "../Languages.h" #include "../StringConstants.h" #include "../mapping/CMapHeader.h" #include "../mapping/CMapService.h" +#include "../modding/CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -249,14 +251,14 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) for(auto & k : reader["keepCreatures"].Vector()) { - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "creature", k.String())) + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "creature", k.String())) ret.monstersKeptByHero.insert(CreatureID(identifier.value())); else logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); } for(auto & k : reader["keepArtifacts"].Vector()) { - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "artifact", k.String())) + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "artifact", k.String())) ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); else logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); @@ -292,7 +294,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) if(int heroId = heroSpecialMap[bjson["hero"].String()]) bonus.info1 = heroId; else - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) bonus.info1 = identifier.value(); else logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); @@ -305,14 +307,14 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) case CampaignBonusType::MONSTER: case CampaignBonusType::SECONDARY_SKILL: case CampaignBonusType::ARTIFACT: - if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), bjson["what"].String(), bjson["type"].String())) + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), bjson["what"].String(), bjson["type"].String())) bonus.info2 = identifier.value(); else logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); break; case CampaignBonusType::SPELL_SCROLL: - if(auto Identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "spell", bjson["type"].String())) + if(auto Identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "spell", bjson["type"].String())) bonus.info2 = Identifier.value(); else logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); @@ -355,7 +357,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) if(int heroId = heroSpecialMap[bjson["hero"].String()]) bonus.info2 = heroId; else - if (auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String())) + if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) bonus.info2 = identifier.value(); else logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index aa586efc5..cf9bc4f2e 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -20,7 +20,7 @@ #include "../GameConstants.h" #include "../VCMIDirs.h" #include "../CStopWatch.h" -#include "../CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -214,7 +214,7 @@ void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); - addFilesystem("data", CModHandler::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); + addFilesystem("data", ModScope::scopeBuiltin(), createFileSystem("", fsConfig["filesystem"], extractArchives)); } void CResourceHandler::addFilesystem(const std::string & parent, const std::string & identifier, ISimpleResourceLoader * loader) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 780ed3cb2..44f877daf 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -38,6 +38,7 @@ #include "../mapping/CMap.h" #include "../mapping/CMapEditManager.h" #include "../mapping/CMapService.h" +#include "../modding/IdentifierStorage.h" #include "../pathfinder/CPathfinder.h" #include "../pathfinder/PathfinderOptions.h" #include "../registerTypes/RegisterTypes.h" @@ -1244,7 +1245,7 @@ BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & r } if(map->isCoastalTile(tile)) //coastal tile is always ground - return BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield.sand_shore")); + return BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.sand_shore")); return BattleField(*RandomGeneratorUtil::nextItem(t.terType->battleFields, rand)); } diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 53a19c83b..3b3e859ad 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -13,7 +13,7 @@ #include "IObjectInfo.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGObjectInstance.h" #include "../mapObjects/ObjectTemplate.h" @@ -100,7 +100,7 @@ void AObjectTypeHandler::init(const JsonNode & input) if(!input["battleground"].isNull()) { - VLC->modh->identifiers.requestIdentifier("battlefield", input["battleground"], [this](int32_t identifier) + VLC->identifiers()->requestIdentifier("battlefield", input["battleground"], [this](int32_t identifier) { battlefield = BattleField(identifier); }); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 22265c0cb..96a769eba 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -16,7 +16,6 @@ #include "../GameConstants.h" #include "../StringConstants.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../GameSettings.h" #include "../JsonNode.h" #include "../CSoundBase.h" @@ -36,7 +35,8 @@ #include "../mapObjects/MiscObjects.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" - +#include "../modding/IdentifierStorage.h" +#include "../modding/CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -277,7 +277,7 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons { auto * object = loadFromJson(scope, data, name, objects.size()); objects.push_back(object); - VLC->modh->identifiers.registerObject(scope, "object", name, object->id); + VLC->modh->getIdentifiers().registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -285,7 +285,7 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons auto * object = loadFromJson(scope, data, name, index); assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before objects[static_cast(index)] = object; - VLC->modh->identifiers.registerObject(scope, "object", name, object->id); + VLC->modh->getIdentifiers().registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID) @@ -325,11 +325,11 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) TObjectTypeHandler CObjectClassesHandler::getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const { - std::optional id = VLC->modh->identifiers.getIdentifier(scope, "object", type); + std::optional id = VLC->identifiers()->getIdentifier(scope, "object", type); if(id) { auto * object = objects[id.value()]; - std::optional subID = VLC->modh->identifiers.getIdentifier(scope, object->getJsonKey(), subtype); + std::optional subID = VLC->identifiers()->getIdentifier(scope, object->getJsonKey(), subtype); if (subID) return object->objects[subID.value()]; diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index d786d7f04..390e1f5d9 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -12,7 +12,6 @@ #include "../CGeneralTextHandler.h" #include "../CHeroHandler.h" -#include "../CModHandler.h" #include "../CTownHandler.h" #include "../IGameCallback.h" #include "../JsonRandom.h" @@ -26,6 +25,8 @@ #include "../mapObjects/MiscObjects.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/IdentifierStorage.h" + #include "../mapping/CMapDefines.h" VCMI_LIB_NAMESPACE_BEGIN @@ -57,7 +58,7 @@ std::string ResourceInstanceConstructor::getNameTextID() const void CTownInstanceConstructor::initTypeData(const JsonNode & input) { - VLC->modh->identifiers.requestIdentifier("faction", input["faction"], [&](si32 index) + VLC->identifiers()->requestIdentifier("faction", input["faction"], [&](si32 index) { faction = (*VLC->townh)[index]; }); @@ -76,7 +77,7 @@ void CTownInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [this](const JsonNode & node) { - return BuildingID(VLC->modh->identifiers.getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value()); + return BuildingID(VLC->identifiers()->getIdentifier("building." + faction->getJsonKey(), node.Vector()[0]).value()); }); } } @@ -118,7 +119,7 @@ std::string CTownInstanceConstructor::getNameTextID() const void CHeroInstanceConstructor::initTypeData(const JsonNode & input) { - VLC->modh->identifiers.requestIdentifier( + VLC->identifiers()->requestIdentifier( "heroClass", input["heroClass"], [&](si32 index) { heroClass = VLC->heroh->classes[index]; }); @@ -132,7 +133,7 @@ void CHeroInstanceConstructor::afterLoadFinalization() { filters[entry.first] = LogicalExpression(entry.second, [](const JsonNode & node) { - return HeroTypeID(VLC->modh->identifiers.getIdentifier("hero", node.Vector()[0]).value()); + return HeroTypeID(VLC->identifiers()->getIdentifier("hero", node.Vector()[0]).value()); }); } } diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index 14a28944d..ccf3a7895 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -12,10 +12,10 @@ #include "../CCreatureHandler.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../JsonRandom.h" #include "../VCMI_Lib.h" #include "../mapObjects/CGDwelling.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +43,7 @@ void DwellingInstanceConstructor::initTypeData(const JsonNode & input) for(auto currentCreature = 0; currentCreature < creaturesNumber; currentCreature++) { - VLC->modh->identifiers.requestIdentifier("creature", creaturesOnLevel[currentCreature], [=] (si32 index) + VLC->identifiers()->requestIdentifier("creature", creaturesOnLevel[currentCreature], [=] (si32 index) { availableCreatures[currentLevel][currentCreature] = VLC->creh->objects[index]; }); diff --git a/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp b/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp index 20052b57f..cbbb57541 100644 --- a/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/ShipyardInstanceConstructor.cpp @@ -11,7 +11,7 @@ #include "ShipyardInstanceConstructor.h" #include "../mapObjects/MiscObjects.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,7 +22,7 @@ void ShipyardInstanceConstructor::initTypeData(const JsonNode & config) void ShipyardInstanceConstructor::initializeObject(CGShipyard * shipyard) const { - shipyard->createdBoat = BoatId(*VLC->modh->identifiers.getIdentifier("core:boat", parameters["boat"])); + shipyard->createdBoat = BoatId(*VLC->identifiers()->getIdentifier("core:boat", parameters["boat"])); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 36ac4e7ea..8b1d11df6 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -21,7 +21,6 @@ #include "../TerrainHandler.h" #include "../RoadHandler.h" #include "../GameSettings.h" -#include "../CModHandler.h" #include "../CSoundBase.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" @@ -35,6 +34,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../modding/ModScope.h" #include "../StringConstants.h" #include "../battle/Unit.h" @@ -1487,7 +1487,7 @@ void CGHeroInstance::setHeroTypeName(const std::string & identifier) { if(ID == Obj::HERO || ID == Obj::PRISON) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); if(rawId) subID = rawId.value(); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 4f088b414..041dab771 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -17,7 +17,6 @@ #include "../NetPacks.h" #include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../mapping/CMap.h" @@ -25,6 +24,7 @@ #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../modding/ModScope.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1097,7 +1097,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { auto decodeBuilding = [this](const std::string & identifier) -> si32 { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), getTown()->getBuildingScope(), identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), getTown()->getBuildingScope(), identifier); if(rawId) return rawId.value(); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 2018ca8dc..b935e3edc 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -22,11 +22,12 @@ #include "../IGameCallback.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../serializer/JsonSerializeFormat.h" -#include "../CModHandler.h" #include "../GameConstants.h" #include "../StringConstants.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" +#include "../modding/ModScope.h" +#include "../modding/ModUtility.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1003,7 +1004,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) } if(rewardType != NOTHING) { - fullIdentifier = CModHandler::makeFullIdentifier(scope, metaTypeName, identifier); + fullIdentifier = ModUtility::makeFullIdentifier(scope, metaTypeName, identifier); handler.serializeInt(fullIdentifier, amount); } } @@ -1023,7 +1024,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) fullIdentifier = iter->first; } - CModHandler::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); + ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); auto it = REWARD_RMAP.find(metaTypeName); @@ -1065,7 +1066,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(doRequest) { - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), fullIdentifier, false); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); if(rawId) { diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c5e4fdbda..f5a46e999 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -15,7 +15,6 @@ #include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" -#include "../CModHandler.h" #include "../CSkillHandler.h" #include "../spells/CSpellHandler.h" #include "../IGameCallback.h" @@ -25,6 +24,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1165,7 +1165,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) bonusType = RANDOM; if(!json["rewardPrimSkill"].String().empty()) { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); + auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); if(raw) { bonusType = PRIM_SKILL; @@ -1174,7 +1174,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) } else if(!json["rewardSkill"].String().empty()) { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "skill", json["rewardSkill"].String()); + auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "skill", json["rewardSkill"].String()); if(raw) { bonusType = SECONDARY_SKILL; @@ -1183,7 +1183,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) } else if(!json["rewardSpell"].String().empty()) { - auto raw = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", json["rewardSpell"].String()); + auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", json["rewardSpell"].String()); if(raw) { bonusType = SPELL; diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 83ac7236c..d65008061 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -16,11 +16,11 @@ #include "../GameConstants.h" #include "../StringConstants.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../JsonNode.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/CRewardableConstructor.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -273,7 +273,7 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) { for(const auto & entry : node["allowedTerrains"].Vector()) { - VLC->modh->identifiers.requestIdentifier("terrain", entry, [this](int32_t identifier){ + VLC->identifiers()->requestIdentifier("terrain", entry, [this](int32_t identifier){ allowedTerrains.insert(TerrainId(identifier)); }); } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8814f4933..e5cf5d5fa 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,7 +10,7 @@ #pragma once -#include "../CModVersion.h" +#include "../modding/CModVersion.h" #include "../LogicalExpression.h" #include "../int3.h" #include "../MetaString.h" diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index d2acd4e8e..734a80265 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -25,7 +25,6 @@ #include "../CCreatureHandler.h" #include "../GameSettings.h" #include "../CHeroHandler.h" -#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 04b2f4869..e8a50e8d7 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -15,7 +15,9 @@ #include "../filesystem/CCompressedStream.h" #include "../filesystem/CMemoryStream.h" #include "../filesystem/CMemoryBuffer.h" -#include "../CModHandler.h" +#include "../modding/CModHandler.h" +#include "../modding/ModScope.h" +#include "../modding/CModInfo.h" #include "../Languages.h" #include "../VCMI_Lib.h" @@ -158,7 +160,7 @@ static JsonNode loadPatches(std::string path) for (auto & entry : node.Struct()) JsonUtils::validate(entry.second, "vcmi:mapHeader", "patch for " + entry.first); - node.setMeta(CModHandler::scopeMap()); + node.setMeta(ModScope::scopeMap()); return node; } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 1df7ff008..ab9563afd 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -21,7 +21,6 @@ #include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../CStopWatch.h" -#include "../CModHandler.h" #include "../GameSettings.h" #include "../RiverHandler.h" #include "../RoadHandler.h" diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 3d59ae8b6..0b2eed219 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -17,7 +17,6 @@ #include "CMap.h" #include "MapFormat.h" #include "../ArtifactUtils.h" -#include "../CModHandler.h" #include "../CHeroHandler.h" #include "../CTownHandler.h" #include "../VCMI_Lib.h" @@ -29,6 +28,8 @@ #include "../mapObjects/ObjectTemplate.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/CGTownInstance.h" +#include "../modding/ModScope.h" +#include "../modding/ModUtility.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" #include "../StringConstants.h" @@ -191,7 +192,7 @@ namespace TriggeredEventsDetail break; } - return CModHandler::makeFullIdentifier("", metaclassName, identifier); + return ModUtility::makeFullIdentifier("", metaclassName, identifier); } static EventCondition JsonToCondition(const JsonNode & node) @@ -219,11 +220,11 @@ namespace TriggeredEventsDetail std::string metaTypeName; std::string scope; std::string identifier; - CModHandler::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); + ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); event.metaType = decodeMetaclass(metaTypeName); - auto type = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), fullIdentifier, false); + auto type = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), fullIdentifier, false); if(type) event.objectType = type.value(); @@ -242,7 +243,7 @@ namespace TriggeredEventsDetail //old format if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) { - auto identifier = VLC->modh->identifiers.getIdentifier(data["type"]); + auto identifier = VLC->identifiers()->getIdentifier(data["type"]); if(identifier) event.objectType = identifier.value(); else @@ -1157,7 +1158,7 @@ void CMapLoaderJson::MapObjectLoader::construct() return; } - auto handler = VLC->objtypeh->getHandlerFor( CModHandler::scopeMap(), typeName, subtypeName); + auto handler = VLC->objtypeh->getHandlerFor( ModScope::scopeMap(), typeName, subtypeName); auto * appearance = new ObjectTemplate; @@ -1194,7 +1195,7 @@ void CMapLoaderJson::MapObjectLoader::configure() if(art->ID == Obj::SPELL_SCROLL) { auto spellIdentifier = configuration["options"]["spell"].String(); - auto rawId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeBuiltin(), "spell", spellIdentifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", spellIdentifier); if(rawId) spellID = rawId.value(); else diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 2168848fa..0c327939b 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -13,13 +13,13 @@ #include "../JsonNode.h" #include "../VCMI_Lib.h" -#include "../CModHandler.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -29,7 +29,7 @@ void MapIdentifiersH3M::loadMapping(std::map & resul for (auto entry : mapping.Struct()) { IdentifierID sourceID (entry.second.Integer()); - IdentifierID targetID (*VLC->modh->identifiers.getIdentifier(entry.second.meta, identifierName, entry.first)); + IdentifierID targetID (*VLC->identifiers()->getIdentifier(entry.second.meta, identifierName, entry.first)); result[sourceID] = targetID; } @@ -39,13 +39,13 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) { for (auto entryFaction : mapping["buildings"].Struct()) { - FactionID factionID (*VLC->modh->identifiers.getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); + FactionID factionID (*VLC->identifiers()->getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); auto buildingMap = entryFaction.second; for (auto entryBuilding : buildingMap.Struct()) { BuildingID sourceID (entryBuilding.second.Integer()); - BuildingID targetID (*VLC->modh->identifiers.getIdentifier(entryBuilding.second.meta, "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); + BuildingID targetID (*VLC->identifiers()->getIdentifier(entryBuilding.second.meta, "building." + VLC->factions()->getById(factionID)->getJsonKey(), entryBuilding.first)); mappingFactionBuilding[factionID][sourceID] = targetID; } @@ -90,7 +90,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) for (auto entry : mapping["portraits"].Struct()) { int32_t sourceID = entry.second.Integer(); - int32_t targetID = *VLC->modh->identifiers.getIdentifier(entry.second.meta, "hero", entry.first); + int32_t targetID = *VLC->identifiers()->getIdentifier(entry.second.meta, "hero", entry.first); int32_t iconID = VLC->heroTypes()->getByIndex(targetID)->getIconIndex(); mappingHeroPortrait[sourceID] = iconID; diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 097eb1e79..6857a4381 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -9,560 +9,25 @@ */ #include "StdInc.h" #include "CModHandler.h" -#include "rmg/CRmgTemplateStorage.h" -#include "filesystem/AdapterLoaders.h" -#include "filesystem/CFilesystemLoader.h" -#include "filesystem/Filesystem.h" -#include "CCreatureHandler.h" -#include "CArtHandler.h" -#include "CTownHandler.h" -#include "CHeroHandler.h" -#include "StringConstants.h" -#include "CStopWatch.h" -#include "IHandlerBase.h" -#include "spells/CSpellHandler.h" -#include "CSkillHandler.h" -#include "CGeneralTextHandler.h" -#include "Languages.h" -#include "ScriptHandler.h" -#include "RoadHandler.h" -#include "GameSettings.h" -#include "RiverHandler.h" -#include "TerrainHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "CModInfo.h" +#include "ModScope.h" +#include "ContentTypeHandler.h" +#include "IdentifierStorage.h" +#include "ModIncompatibility.h" -#include +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CStopWatch.h" +#include "../GameSettings.h" +#include "../Languages.h" +#include "../ScriptHandler.h" +#include "../StringConstants.h" +#include "../filesystem/Filesystem.h" +#include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN -CIdentifierStorage::CIdentifierStorage(): - state(LOADING) -{ -} - -void CIdentifierStorage::checkIdentifier(std::string & ID) -{ - if (boost::algorithm::ends_with(ID, ".")) - logMod->warn("BIG WARNING: identifier %s seems to be broken!", ID); - else - { - size_t pos = 0; - do - { - if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase - { - logMod->warn("Warning: identifier %s is not in camelCase!", ID); - ID[pos] = std::tolower(ID[pos]);// Try to fix the ID - } - pos = ID.find('.', pos); - } - while(pos++ != std::string::npos); - } -} - -void CIdentifierStorage::requestIdentifier(ObjectCallback callback) -{ - checkIdentifier(callback.type); - checkIdentifier(callback.name); - - assert(!callback.localScope.empty()); - - if (state != FINISHED) // enqueue request if loading is still in progress - scheduledRequests.push_back(callback); - else // execute immediately for "late" requests - resolveIdentifier(callback); -} - -CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional) -{ - assert(!scope.empty()); - - auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); - auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); - - if (scope == scopeAndFullName.first) - logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); - - ObjectCallback result; - result.localScope = scope; - result.remoteScope = scopeAndFullName.first; - result.type = typeAndName.first; - result.name = typeAndName.second; - result.callback = callback; - result.optional = optional; - return result; -} - -CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional) -{ - assert(!scope.empty()); - - auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); - auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); - - if(!typeAndName.first.empty()) - { - if (typeAndName.first != type) - logMod->error("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); - else - logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope); - } - - if (scope == scopeAndFullName.first) - logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); - - ObjectCallback result; - result.localScope = scope; - result.remoteScope = scopeAndFullName.first; - result.type = type; - result.name = typeAndName.second; - result.callback = callback; - result.optional = optional; - return result; -} - -void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameWithType(scope, fullName, callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, false)); -} - -void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameWithType(name.meta, name.String(), callback, false)); -} - -void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true)); -} - -void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) -{ - requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, true)); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name , type ,scope); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s of type %s from mod %s", name.String(), type, name.meta); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", name.String(), name.meta); - - return std::optional(); -} - -std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) -{ - auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); - - if (idList.size() == 1) - return idList.front().id; - if (!silent) - logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); - - return std::optional(); -} - -void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) -{ - ObjectData data; - data.scope = scope; - data.id = identifier; - - std::string fullID = type + '.' + name; - checkIdentifier(fullID); - - std::pair mapping = std::make_pair(fullID, data); - if(!vstd::containsMapping(registeredObjects, mapping)) - { - logMod->trace("registered %s as %s:%s", fullID, scope, identifier); - registeredObjects.insert(mapping); - } -} - -std::vector CIdentifierStorage::getPossibleIdentifiers(const ObjectCallback & request) -{ - std::set allowedScopes; - bool isValidScope = true; - - // called have not specified destination mod explicitly - if (request.remoteScope.empty()) - { - // special scope that should have access to all in-game objects - if (request.localScope == CModHandler::scopeGame()) - { - for(const auto & modName : VLC->modh->getActiveMods()) - allowedScopes.insert(modName); - } - - // normally ID's from all required mods, own mod and virtual built-in mod are allowed - else if(request.localScope != CModHandler::scopeBuiltin() && !request.localScope.empty()) - { - allowedScopes = VLC->modh->getModDependencies(request.localScope, isValidScope); - - if(!isValidScope) - return std::vector(); - - allowedScopes.insert(request.localScope); - } - - // all mods can access built-in mod - allowedScopes.insert(CModHandler::scopeBuiltin()); - } - else - { - //if destination mod was specified explicitly, restrict lookup to this mod - if(request.remoteScope == CModHandler::scopeBuiltin() ) - { - //built-in mod is an implicit dependency for all mods, allow access into it - allowedScopes.insert(request.remoteScope); - } - else if ( request.localScope == CModHandler::scopeGame() ) - { - // allow access, this is special scope that should have access to all in-game objects - allowedScopes.insert(request.remoteScope); - } - else if(request.remoteScope == request.localScope ) - { - // allow self-access - allowedScopes.insert(request.remoteScope); - } - else - { - // allow access only if mod is in our dependencies - auto myDeps = VLC->modh->getModDependencies(request.localScope, isValidScope); - - if(!isValidScope) - return std::vector(); - - if(myDeps.count(request.remoteScope)) - allowedScopes.insert(request.remoteScope); - } - } - - std::string fullID = request.type + '.' + request.name; - - auto entries = registeredObjects.equal_range(fullID); - if (entries.first != entries.second) - { - std::vector locatedIDs; - - for (auto it = entries.first; it != entries.second; it++) - { - if (vstd::contains(allowedScopes, it->second.scope)) - { - locatedIDs.push_back(it->second); - } - } - return locatedIDs; - } - return std::vector(); -} - -bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) -{ - auto identifiers = getPossibleIdentifiers(request); - if (identifiers.size() == 1) // normally resolved ID - { - request.callback(identifiers.front().id); - return true; - } - - if (request.optional && identifiers.empty()) // failed to resolve optinal ID - { - return true; - } - - // error found. Try to generate some debug info - if(identifiers.empty()) - logMod->error("Unknown identifier!"); - else - logMod->error("Ambiguous identifier request!"); - - logMod->error("Request for %s.%s from mod %s", request.type, request.name, request.localScope); - - for(const auto & id : identifiers) - { - logMod->error("\tID is available in mod %s", id.scope); - } - return false; -} - -void CIdentifierStorage::finalize() -{ - state = FINALIZING; - bool errorsFound = false; - - while ( !scheduledRequests.empty() ) - { - // Use local copy since new requests may appear during resolving, invalidating any iterators - auto request = scheduledRequests.back(); - scheduledRequests.pop_back(); - - if (!resolveIdentifier(request)) - errorsFound = true; - } - - if (errorsFound) - { - for(const auto & object : registeredObjects) - { - logMod->trace("%s : %s -> %d", object.second.scope, object.first, object.second.id); - } - logMod->error("All known identifiers were dumped into log file"); - } - assert(errorsFound == false); - state = FINISHED; -} - -ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): - handler(handler), - objectName(objectName), - originalData(handler->loadLegacyData()) -{ - for(auto & node : originalData) - { - node.setMeta(CModHandler::scopeBuiltin()); - } -} - -bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector & fileList, bool validate) -{ - bool result = false; - JsonNode data = JsonUtils::assembleFromFiles(fileList, result); - data.setMeta(modName); - - ModInfo & modInfo = modData[modName]; - - for(auto entry : data.Struct()) - { - size_t colon = entry.first.find(':'); - - if (colon == std::string::npos) - { - // normal object, local to this mod - modInfo.modData[entry.first].swap(entry.second); - } - else - { - std::string remoteName = entry.first.substr(0, colon); - std::string objectName = entry.first.substr(colon + 1); - - // patching this mod? Send warning and continue - this situation can be handled normally - if (remoteName == modName) - logMod->warn("Redundant namespace definition for %s", objectName); - - logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); - JsonNode & remoteConf = modData[remoteName].patches[objectName]; - - JsonUtils::merge(remoteConf, entry.second); - } - } - return result; -} - -bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) -{ - ModInfo & modInfo = modData[modName]; - bool result = true; - - auto performValidate = [&,this](JsonNode & data, const std::string & name){ - handler->beforeValidate(data); - if (validate) - result &= JsonUtils::validate(data, "vcmi:" + objectName, name); - }; - - // apply patches - if (!modInfo.patches.isNull()) - JsonUtils::merge(modInfo.modData, modInfo.patches); - - for(auto & entry : modInfo.modData.Struct()) - { - const std::string & name = entry.first; - JsonNode & data = entry.second; - - if (data.meta != modName) - logMod->warn("Mod %s is attempting to inject object %s into mod %s! This may not be supported in future versions!", data.meta, name, modName); - - if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) - { - if (modName != "core") - logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName); - - // try to add H3 object data - size_t index = static_cast(data["index"].Float()); - - if(originalData.size() > index) - { - logMod->trace("found original data in loadMod(%s) at index %d", name, index); - JsonUtils::merge(originalData[index], data); - std::swap(originalData[index], data); - originalData[index].clear(); // do not use same data twice (same ID) - } - else - { - logMod->trace("no original data in loadMod(%s) at index %d", name, index); - } - performValidate(data, name); - handler->loadObject(modName, name, data, index); - } - else - { - // normal new object - logMod->trace("no index in loadMod(%s)", name); - performValidate(data,name); - handler->loadObject(modName, name, data); - } - } - return result; -} - -void ContentTypeHandler::loadCustom() -{ - handler->loadCustom(); -} - -void ContentTypeHandler::afterLoadFinalization() -{ - handler->afterLoadFinalization(); -} - -void CContentHandler::init() -{ - handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(&VLC->heroh->classes, "heroClass"))); - handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth, "artifact"))); - handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh, "creature"))); - handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh, "faction"))); - handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh, "object"))); - handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh, "hero"))); - handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell"))); - handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill"))); - handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh, "template"))); -#if SCRIPTING_ENABLED - handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script"))); -#endif - handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield"))); - handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler, "terrain"))); - handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler, "river"))); - handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler, "road"))); - handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle"))); - //TODO: any other types of moddables? -} - -bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) -{ - bool result = true; - for(auto & handler : handlers) - { - result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo >(), validate); - } - return result; -} - -bool CContentHandler::loadMod(const std::string & modName, bool validate) -{ - bool result = true; - for(auto & handler : handlers) - { - result &= handler.second.loadMod(modName, validate); - } - return result; -} - -void CContentHandler::loadCustom() -{ - for(auto & handler : handlers) - { - handler.second.loadCustom(); - } -} - -void CContentHandler::afterLoadFinalization() -{ - for(auto & handler : handlers) - { - handler.second.afterLoadFinalization(); - } -} - -void CContentHandler::preloadData(CModInfo & mod) -{ - bool validate = (mod.validation != CModInfo::PASSED); - - // print message in format [<8-symbols checksum>] - logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); - - if (validate && mod.identifier != CModHandler::scopeBuiltin()) - { - if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier)) - mod.validation = CModInfo::FAILED; - } - if (!preloadModData(mod.identifier, mod.config, validate)) - mod.validation = CModInfo::FAILED; -} - -void CContentHandler::load(CModInfo & mod) -{ - bool validate = (mod.validation != CModInfo::PASSED); - - if (!loadMod(mod.identifier, validate)) - mod.validation = CModInfo::FAILED; - - if (validate) - { - if (mod.validation != CModInfo::FAILED) - logMod->info("\t\t[DONE] %s", mod.name); - else - logMod->error("\t\t[FAIL] %s", mod.name); - } - else - logMod->info("\t\t[SKIP] %s", mod.name); -} - -const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const -{ - return handlers.at(name); -} - static JsonNode loadModSettings(const std::string & path) { if (CResourceHandler::get("local")->existsResource(ResourceID(path))) @@ -574,149 +39,31 @@ static JsonNode loadModSettings(const std::string & path) return JsonNode(); } -JsonNode addMeta(JsonNode config, const std::string & meta) -{ - config.setMeta(meta); - return config; -} - -CModInfo::CModInfo(): - checksum(0), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING) -{ - -} - -CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): - identifier(identifier), - name(config["name"].String()), - description(config["description"].String()), - dependencies(config["depends"].convertTo>()), - conflicts(config["conflicts"].convertTo>()), - checksum(0), - explicitlyEnabled(false), - implicitlyEnabled(true), - validation(PENDING), - config(addMeta(config, identifier)) -{ - version = CModVersion::fromString(config["version"].String()); - if(!config["compatibility"].isNull()) - { - vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); - vcmiCompatibleMax = CModVersion::fromString(config["compatibility"]["max"].String()); - } - - if (!config["language"].isNull()) - baseLanguage = config["language"].String(); - else - baseLanguage = "english"; - - loadLocalData(local); -} - -JsonNode CModInfo::saveLocalData() const -{ - std::ostringstream stream; - stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << checksum; - - JsonNode conf; - conf["active"].Bool() = explicitlyEnabled; - conf["validated"].Bool() = validation != FAILED; - conf["checksum"].String() = stream.str(); - return conf; -} - -std::string CModInfo::getModDir(const std::string & name) -{ - return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/"); -} - -std::string CModInfo::getModFile(const std::string & name) -{ - return getModDir(name) + "/mod.json"; -} - -void CModInfo::updateChecksum(ui32 newChecksum) -{ - // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != checksum) - { - checksum = newChecksum; - validation = PENDING; - } -} - -void CModInfo::loadLocalData(const JsonNode & data) -{ - bool validated = false; - implicitlyEnabled = true; - explicitlyEnabled = !config["keepDisabled"].Bool(); - checksum = 0; - if (data.getType() == JsonNode::JsonType::DATA_BOOL) - { - explicitlyEnabled = data.Bool(); - } - if (data.getType() == JsonNode::JsonType::DATA_STRUCT) - { - explicitlyEnabled = data["active"].Bool(); - validated = data["validated"].Bool(); - checksum = strtol(data["checksum"].String().c_str(), nullptr, 16); - } - - //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion())); - - if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); - - if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment - { - if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) - { - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", name); - implicitlyEnabled = false; - } - } - - if (isEnabled()) - validation = validated ? PASSED : PENDING; - else - validation = validated ? PASSED : FAILED; -} - -bool CModInfo::isEnabled() const -{ - return implicitlyEnabled && explicitlyEnabled; -} - -void CModInfo::setEnabled(bool on) -{ - explicitlyEnabled = on; -} - -CModHandler::CModHandler() : content(std::make_shared()) +CModHandler::CModHandler() + : content(std::make_shared()) + , identifiers(std::make_unique()) + , coreMod(std::make_unique()) { //TODO: moddable spell schools for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) - identifiers.registerObject(CModHandler::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); + identifiers->registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); - identifiers.registerObject(CModHandler::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); + identifiers->registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) { - identifiers.registerObject(CModHandler::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); + identifiers->registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); } for(int i=0; iregisterObject(ModScope::scopeBuiltin(), "primSkill", PrimarySkill::names[i], i); + identifiers->registerObject(ModScope::scopeBuiltin(), "primarySkill", PrimarySkill::names[i], i); } } +CModHandler::~CModHandler() = default; + // currentList is passed by value to get current list of depending mods bool CModHandler::hasCircularDependency(const TModID & modID, std::set currentList) const { @@ -834,35 +181,7 @@ std::vector CModHandler::getModList(const std::string & path) const return foundMods; } -bool CModHandler::isScopeReserved(const TModID & scope) -{ - //following scopes are reserved - either in use by mod system or by filesystem - static const std::array reservedScopes = { - "core", "map", "game", "root", "saves", "config", "local", "initial", "mapEditor" - }; - return std::find(reservedScopes.begin(), reservedScopes.end(), scope) != reservedScopes.end(); -} - -const TModID & CModHandler::scopeBuiltin() -{ - static const TModID scope = "core"; - return scope; -} - -const TModID & CModHandler::scopeGame() -{ - static const TModID scope = "game"; - return scope; -} - -const TModID & CModHandler::scopeMap() -{ - //TODO: implement accessing map dependencies for both H3 and VCMI maps - // for now, allow access to any identifiers - static const TModID scope = "game"; - return scope; -} void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods) { @@ -875,7 +194,7 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co boost::to_lower(modName); std::string modFullName = parent.empty() ? modName : parent + '.' + modName; - if ( isScopeReserved(modFullName)) + if ( ModScope::isScopeReserved(modFullName)) { logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); return; @@ -909,8 +228,8 @@ void CModHandler::loadMods(bool onlyEssential) loadMods("", "", modConfig["activeMods"], true); } - coreMod = CModInfo(CModHandler::scopeBuiltin(), modConfig[CModHandler::scopeBuiltin()], JsonNode(ResourceID("config/gameConfig.json"))); - coreMod.name = "Original game files"; + coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(ResourceID("config/gameConfig.json"))); + coreMod->name = "Original game files"; } std::vector CModHandler::getAllMods() @@ -962,7 +281,7 @@ static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoa // second - add mod.json into checksum because filesystem does not contains this file // FIXME: remove workaround for core mod - if (modName != CModHandler::scopeBuiltin()) + if (modName != ModScope::scopeBuiltin()) { ResourceID modConfFile(CModInfo::getModFile(modName), EResType::TEXT); ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); @@ -990,7 +309,7 @@ void CModHandler::loadModFilesystems() activeMods = validateAndSortDependencies(activeMods); - coreMod.updateChecksum(calculateModChecksum(CModHandler::scopeBuiltin(), CResourceHandler::get(CModHandler::scopeBuiltin()))); + coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); for(std::string & modName : activeMods) { @@ -1038,7 +357,7 @@ std::set CModHandler::getModDependencies(const TModID & modId, bool & is void CModHandler::initializeConfig() { - VLC->settingsHandler->load(coreMod.config["settings"]); + VLC->settingsHandler->load(coreMod->config["settings"]); for(const TModID & modName : activeMods) { @@ -1048,6 +367,13 @@ void CModHandler::initializeConfig() } } +CModVersion CModHandler::getModVersion(TModID modName) const +{ + if (allMods.count(modName)) + return allMods.at(modName).version; + return {}; +} + bool CModHandler::validateTranslations(TModID modName) const { bool result = true; @@ -1112,12 +438,12 @@ void CModHandler::load() // first - load virtual builtin mod that contains all data // TODO? move all data into real mods? RoE, AB, SoD, WoG - content->preloadData(coreMod); + content->preloadData(*coreMod); for(const TModID & modName : activeMods) content->preloadData(allMods[modName]); logMod->info("\tParsing mod data: %d ms", timer.getDiff()); - content->load(coreMod); + content->load(*coreMod); for(const TModID & modName : activeMods) content->load(allMods[modName]); @@ -1136,7 +462,7 @@ void CModHandler::load() logMod->info("\tLoading mod data: %d ms", timer.getDiff()); VLC->creh->loadCrExpMod(); - identifiers.finalize(); + identifiers->finalize(); logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); content->afterLoadFinalization(); @@ -1153,7 +479,7 @@ void CModHandler::afterLoad(bool onlyEssential) modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); } - modSettings[CModHandler::scopeBuiltin()] = coreMod.saveLocalData(); + modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); if(!onlyEssential) { @@ -1163,64 +489,28 @@ void CModHandler::afterLoad(bool onlyEssential) } -std::string CModHandler::normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier) +void CModHandler::trySetActiveMods(const std::map & modList) { - auto p = vstd::splitStringToPair(identifier, ':'); + ModIncompatibility::ModList missingMods; - if(p.first.empty()) - p.first = scope; + for(const auto & mod : modList) + { + auto m = mod.first; + auto mver = mod.second; - if(p.first == remoteScope) - p.first.clear(); + if(allMods.count(m) && (allMods[m].version.isNull() || mver.isNull() || allMods[m].version.compatible(mver))) + allMods[m].setEnabled(true); + else + missingMods.emplace_back(m, mver.toString()); + } - return p.first.empty() ? p.second : p.first + ":" + p.second; + if(!missingMods.empty()) + throw ModIncompatibility(std::move(missingMods)); } -void CModHandler::parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier) +CIdentifierStorage & CModHandler::getIdentifiers() { - auto p = vstd::splitStringToPair(fullIdentifier, ':'); - - scope = p.first; - - auto p2 = vstd::splitStringToPair(p.second, '.'); - - if(!p2.first.empty()) - { - type = p2.first; - identifier = p2.second; - } - else - { - type = p.second; - identifier.clear(); - } -} - -std::string CModHandler::makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier) -{ - if(type.empty()) - logGlobal->error("Full identifier (%s %s) requires type name", scope, identifier); - - std::string actualScope = scope; - std::string actualName = identifier; - - //ignore scope if identifier is scoped - auto scopeAndName = vstd::splitStringToPair(identifier, ':'); - - if(!scopeAndName.first.empty()) - { - actualScope = scopeAndName.first; - actualName = scopeAndName.second; - } - - if(actualScope.empty()) - { - return actualName.empty() ? type : type + "." + actualName; - } - else - { - return actualName.empty() ? actualScope+ ":" + type : actualScope + ":" + type + "." + actualName; - } + return *identifiers; } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 30325f246..d782bfa18 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -9,15 +9,8 @@ */ #pragma once -#include "JsonNode.h" #include "CModVersion.h" -#ifdef __UCLIBC__ -#undef major -#undef minor -#undef patch -#endif - VCMI_LIB_NAMESPACE_BEGIN class CModHandler; @@ -25,225 +18,17 @@ class CModIndentifier; class CModInfo; class JsonNode; class IHandlerBase; - -/// class that stores all object identifiers strings and maps them to numeric ID's -/// if possible, objects ID's should be in format ., camelCase e.g. "creature.grandElf" -class DLL_LINKAGE CIdentifierStorage -{ - enum ELoadingState - { - LOADING, - FINALIZING, - FINISHED - }; - - struct ObjectCallback // entry created on ID request - { - std::string localScope; /// scope from which this ID was requested - std::string remoteScope; /// scope in which this object must be found - std::string type; /// type, e.g. creature, faction, hero, etc - std::string name; /// string ID - std::function callback; - bool optional; - - /// Builds callback from identifier in form "targetMod:type.name" - static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional); - - /// Builds callback from identifier in form "targetMod:name" - static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional); - - private: - ObjectCallback() = default; - }; - - struct ObjectData // entry created on ID registration - { - si32 id; - std::string scope; /// scope in which this ID located - - bool operator==(const ObjectData & other) const - { - return id == other.id && scope == other.scope; - } - - template void serialize(Handler &h, const int version) - { - h & id; - h & scope; - } - }; - - std::multimap registeredObjects; - std::vector scheduledRequests; - - ELoadingState state; - - /// Check if identifier can be valid (camelCase, point as separator) - static void checkIdentifier(std::string & ID); - - void requestIdentifier(ObjectCallback callback); - bool resolveIdentifier(const ObjectCallback & callback); - std::vector getPossibleIdentifiers(const ObjectCallback & callback); -public: - CIdentifierStorage(); - virtual ~CIdentifierStorage() = default; - /// request identifier for specific object name. - /// Function callback will be called during ID resolution phase of loading - void requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback); - ///fullName = [remoteScope:]type.name - void requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback); - void requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback); - void requestIdentifier(const JsonNode & name, const std::function & callback); - - /// try to request ID. If ID with such name won't be loaded, callback function will not be called - void tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback); - void tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback); - - /// get identifier immediately. If identifier is not know and not silent call will result in error message - std::optional getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent = false); - std::optional getIdentifier(const std::string & type, const JsonNode & name, bool silent = false); - std::optional getIdentifier(const JsonNode & name, bool silent = false); - std::optional getIdentifier(const std::string & scope, const std::string & fullName, bool silent = false); - - /// registers new object - void registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier); - - /// called at the very end of loading to check for any missing ID's - void finalize(); - - template void serialize(Handler &h, const int version) - { - h & registeredObjects; - h & state; - } -}; - -/// internal type to handle loading of one data type (e.g. artifacts, creatures) -class DLL_LINKAGE ContentTypeHandler -{ -public: - struct ModInfo - { - /// mod data from this mod and for this mod - JsonNode modData; - /// mod data for this mod from other mods (patches) - JsonNode patches; - }; - /// handler to which all data will be loaded - IHandlerBase * handler; - std::string objectName; - - /// contains all loaded H3 data - std::vector originalData; - std::map modData; - - ContentTypeHandler(IHandlerBase * handler, const std::string & objectName); - - /// local version of methods in ContentHandler - /// returns true if loading was successful - bool preloadModData(const std::string & modName, const std::vector & fileList, bool validate); - bool loadMod(const std::string & modName, bool validate); - void loadCustom(); - void afterLoadFinalization(); -}; - -/// class used to load all game data into handlers. Used only during loading -class DLL_LINKAGE CContentHandler -{ - /// preloads all data from fileList as data from modName. - bool preloadModData(const std::string & modName, JsonNode modConfig, bool validate); - - /// actually loads data in mod - bool loadMod(const std::string & modName, bool validate); - - std::map handlers; - -public: - void init(); - - /// preloads all data from fileList as data from modName. - void preloadData(CModInfo & mod); - - /// actually loads data in mod - void load(CModInfo & mod); - - void loadCustom(); - - /// all data was loaded, time for final validation / integration - void afterLoadFinalization(); - - const ContentTypeHandler & operator[] (const std::string & name) const; -}; +class CIdentifierStorage; +class CContentHandler; +class ResourceID; using TModID = std::string; -class DLL_LINKAGE CModInfo -{ -public: - enum EValidationStatus - { - PENDING, - FAILED, - PASSED - }; - - /// identifier, identical to name of folder with mod - std::string identifier; - - /// human-readable strings - std::string name; - std::string description; - - /// version of the mod - CModVersion version; - - /// Base language of mod, all mod strings are assumed to be in this language - std::string baseLanguage; - - /// vcmi versions compatible with the mod - - CModVersion vcmiCompatibleMin, vcmiCompatibleMax; - - /// list of mods that should be loaded before this one - std::set dependencies; - - /// list of mods that can't be used in the same time as this one - std::set conflicts; - - /// CRC-32 checksum of the mod - ui32 checksum; - - EValidationStatus validation; - - JsonNode config; - - CModInfo(); - CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config); - - JsonNode saveLocalData() const; - void updateChecksum(ui32 newChecksum); - - bool isEnabled() const; - void setEnabled(bool on); - - static std::string getModDir(const std::string & name); - static std::string getModFile(const std::string & name); - -private: - /// true if mod is enabled by user, e.g. in Launcher UI - bool explicitlyEnabled; - - /// true if mod can be loaded - compatible and has no missing deps - bool implicitlyEnabled; - - void loadLocalData(const JsonNode & data); -}; - -class DLL_LINKAGE CModHandler +class DLL_LINKAGE CModHandler : boost::noncopyable { std::map allMods; std::vector activeMods;//active mods, in order in which they were loaded - CModInfo coreMod; + std::unique_ptr coreMod; bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; @@ -263,51 +48,18 @@ class DLL_LINKAGE CModHandler void loadTranslation(const TModID & modName); bool validateTranslations(TModID modName) const; + + CModVersion getModVersion(TModID modName) const; + + /// Attempt to set active mods according to provided list of mods from save, throws on failure + void trySetActiveMods(const std::map & modList); + + std::unique_ptr identifiers; + public: + std::shared_ptr content; //(!)Do not serialize FIXME: make private - /// returns true if scope is reserved for internal use and can not be used by mods - static bool isScopeReserved(const TModID & scope); - - /// reserved scope name for referencing built-in (e.g. H3) objects - static const TModID & scopeBuiltin(); - - /// reserved scope name for accessing objects from any loaded mod - static const TModID & scopeGame(); - - /// reserved scope name for accessing object for map loading - static const TModID & scopeMap(); - - class DLL_LINKAGE Incompatibility: public std::exception - { - public: - using StringPair = std::pair; - using ModList = std::list; - - Incompatibility(ModList && _missingMods): - missingMods(std::move(_missingMods)) - { - std::ostringstream _ss; - for(const auto & m : missingMods) - _ss << m.first << ' ' << m.second << std::endl; - message = _ss.str(); - } - - const char * what() const noexcept override - { - return message.c_str(); - } - - private: - //list of mods required to load the game - // first: mod name - // second: mod version - const ModList missingMods; - std::string message; - }; - - CIdentifierStorage identifiers; - - std::shared_ptr content; //(!)Do not serialize + CIdentifierStorage & getIdentifiers(); /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); @@ -332,13 +84,7 @@ public: void afterLoad(bool onlyEssential); CModHandler(); - virtual ~CModHandler() = default; - - static std::string normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier); - - static void parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier); - - static std::string makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier); + virtual ~CModHandler(); template void serialize(Handler &h, const int version) { @@ -346,30 +92,23 @@ public: { h & activeMods; for(const auto & m : activeMods) - h & allMods[m].version; + { + CModVersion version = getModVersion(m); + h & version; + } } else { loadMods(); std::vector newActiveMods; + std::map modVersions; h & newActiveMods; - - Incompatibility::ModList missingMods; - for(const auto & m : newActiveMods) - { - CModVersion mver; - h & mver; - - if(allMods.count(m) && (allMods[m].version.isNull() || mver.isNull() || allMods[m].version.compatible(mver))) - allMods[m].setEnabled(true); - else - missingMods.emplace_back(m, mver.toString()); - } - - if(!missingMods.empty()) - throw Incompatibility(std::move(missingMods)); - + for(const auto & m : newActiveMods) + h & modVersions[m]; + + trySetActiveMods(modVersions); + std::swap(activeMods, newActiveMods); } diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp new file mode 100644 index 000000000..f8d75fb48 --- /dev/null +++ b/lib/modding/CModInfo.cpp @@ -0,0 +1,141 @@ +/* + * CModInfo.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 "CModInfo.h" + +#include "../CGeneralTextHandler.h" +#include "../VCMI_Lib.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static JsonNode addMeta(JsonNode config, const std::string & meta) +{ + config.setMeta(meta); + return config; +} + +CModInfo::CModInfo(): + checksum(0), + explicitlyEnabled(false), + implicitlyEnabled(true), + validation(PENDING) +{ + +} + +CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): + identifier(identifier), + name(config["name"].String()), + description(config["description"].String()), + dependencies(config["depends"].convertTo>()), + conflicts(config["conflicts"].convertTo>()), + checksum(0), + explicitlyEnabled(false), + implicitlyEnabled(true), + validation(PENDING), + config(addMeta(config, identifier)) +{ + version = CModVersion::fromString(config["version"].String()); + if(!config["compatibility"].isNull()) + { + vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); + vcmiCompatibleMax = CModVersion::fromString(config["compatibility"]["max"].String()); + } + + if (!config["language"].isNull()) + baseLanguage = config["language"].String(); + else + baseLanguage = "english"; + + loadLocalData(local); +} + +JsonNode CModInfo::saveLocalData() const +{ + std::ostringstream stream; + stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << checksum; + + JsonNode conf; + conf["active"].Bool() = explicitlyEnabled; + conf["validated"].Bool() = validation != FAILED; + conf["checksum"].String() = stream.str(); + return conf; +} + +std::string CModInfo::getModDir(const std::string & name) +{ + return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/"); +} + +std::string CModInfo::getModFile(const std::string & name) +{ + return getModDir(name) + "/mod.json"; +} + +void CModInfo::updateChecksum(ui32 newChecksum) +{ + // comment-out next line to force validation of all mods ignoring checksum + if (newChecksum != checksum) + { + checksum = newChecksum; + validation = PENDING; + } +} + +void CModInfo::loadLocalData(const JsonNode & data) +{ + bool validated = false; + implicitlyEnabled = true; + explicitlyEnabled = !config["keepDisabled"].Bool(); + checksum = 0; + if (data.getType() == JsonNode::JsonType::DATA_BOOL) + { + explicitlyEnabled = data.Bool(); + } + if (data.getType() == JsonNode::JsonType::DATA_STRUCT) + { + explicitlyEnabled = data["active"].Bool(); + validated = data["validated"].Bool(); + checksum = strtol(data["checksum"].String().c_str(), nullptr, 16); + } + + //check compatibility + implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin)); + implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion())); + + if(!implicitlyEnabled) + logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); + + if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment + { + if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) + { + logGlobal->warn("Translation mod %s was not loaded: language mismatch!", name); + implicitlyEnabled = false; + } + } + + if (isEnabled()) + validation = validated ? PASSED : PENDING; + else + validation = validated ? PASSED : FAILED; +} + +bool CModInfo::isEnabled() const +{ + return implicitlyEnabled && explicitlyEnabled; +} + +void CModInfo::setEnabled(bool on) +{ + explicitlyEnabled = on; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h new file mode 100644 index 000000000..f78db9336 --- /dev/null +++ b/lib/modding/CModInfo.h @@ -0,0 +1,80 @@ +/* + * CModInfo.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 "../JsonNode.h" +#include "CModVersion.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TModID = std::string; + +class DLL_LINKAGE CModInfo +{ +public: + enum EValidationStatus + { + PENDING, + FAILED, + PASSED + }; + + /// identifier, identical to name of folder with mod + std::string identifier; + + /// human-readable strings + std::string name; + std::string description; + + /// version of the mod + CModVersion version; + + /// Base language of mod, all mod strings are assumed to be in this language + std::string baseLanguage; + + /// vcmi versions compatible with the mod + CModVersion vcmiCompatibleMin, vcmiCompatibleMax; + + /// list of mods that should be loaded before this one + std::set dependencies; + + /// list of mods that can't be used in the same time as this one + std::set conflicts; + + /// CRC-32 checksum of the mod + ui32 checksum; + + EValidationStatus validation; + + JsonNode config; + + CModInfo(); + CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config); + + JsonNode saveLocalData() const; + void updateChecksum(ui32 newChecksum); + + bool isEnabled() const; + void setEnabled(bool on); + + static std::string getModDir(const std::string & name); + static std::string getModFile(const std::string & name); + +private: + /// true if mod is enabled by user, e.g. in Launcher UI + bool explicitlyEnabled; + + /// true if mod can be loaded - compatible and has no missing deps + bool implicitlyEnabled; + + void loadLocalData(const JsonNode & data); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 877615f27..6e43fb2b6 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -10,6 +10,12 @@ #pragma once +#ifdef __UCLIBC__ +#undef major +#undef minor +#undef patch +#endif + VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE CModVersion diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp new file mode 100644 index 000000000..ed2667e7e --- /dev/null +++ b/lib/modding/ContentTypeHandler.cpp @@ -0,0 +1,249 @@ +/* + * ContentTypeHandler.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 "ContentTypeHandler.h" + +#include "CModHandler.h" +#include "CModInfo.h" +#include "ModScope.h" + +#include "../BattleFieldHandler.h" +#include "../CArtHandler.h" +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CHeroHandler.h" +#include "../CSkillHandler.h" +#include "../CStopWatch.h" +#include "../CTownHandler.h" +#include "../GameSettings.h" +#include "../IHandlerBase.h" +#include "../Languages.h" +#include "../ObstacleHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../ScriptHandler.h" +#include "../StringConstants.h" +#include "../TerrainHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../rmg/CRmgTemplateStorage.h" +#include "../spells/CSpellHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): + handler(handler), + objectName(objectName), + originalData(handler->loadLegacyData()) +{ + for(auto & node : originalData) + { + node.setMeta(ModScope::scopeBuiltin()); + } +} + +bool ContentTypeHandler::preloadModData(const std::string & modName, const std::vector & fileList, bool validate) +{ + bool result = false; + JsonNode data = JsonUtils::assembleFromFiles(fileList, result); + data.setMeta(modName); + + ModInfo & modInfo = modData[modName]; + + for(auto entry : data.Struct()) + { + size_t colon = entry.first.find(':'); + + if (colon == std::string::npos) + { + // normal object, local to this mod + modInfo.modData[entry.first].swap(entry.second); + } + else + { + std::string remoteName = entry.first.substr(0, colon); + std::string objectName = entry.first.substr(colon + 1); + + // patching this mod? Send warning and continue - this situation can be handled normally + if (remoteName == modName) + logMod->warn("Redundant namespace definition for %s", objectName); + + logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); + JsonNode & remoteConf = modData[remoteName].patches[objectName]; + + JsonUtils::merge(remoteConf, entry.second); + } + } + return result; +} + +bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) +{ + ModInfo & modInfo = modData[modName]; + bool result = true; + + auto performValidate = [&,this](JsonNode & data, const std::string & name){ + handler->beforeValidate(data); + if (validate) + result &= JsonUtils::validate(data, "vcmi:" + objectName, name); + }; + + // apply patches + if (!modInfo.patches.isNull()) + JsonUtils::merge(modInfo.modData, modInfo.patches); + + for(auto & entry : modInfo.modData.Struct()) + { + const std::string & name = entry.first; + JsonNode & data = entry.second; + + if (data.meta != modName) + logMod->warn("Mod %s is attempting to inject object %s into mod %s! This may not be supported in future versions!", data.meta, name, modName); + + if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) + { + if (modName != "core") + logMod->warn("Mod %s is attempting to load original data! This should be reserved for built-in mod.", modName); + + // try to add H3 object data + size_t index = static_cast(data["index"].Float()); + + if(originalData.size() > index) + { + logMod->trace("found original data in loadMod(%s) at index %d", name, index); + JsonUtils::merge(originalData[index], data); + std::swap(originalData[index], data); + originalData[index].clear(); // do not use same data twice (same ID) + } + else + { + logMod->trace("no original data in loadMod(%s) at index %d", name, index); + } + performValidate(data, name); + handler->loadObject(modName, name, data, index); + } + else + { + // normal new object + logMod->trace("no index in loadMod(%s)", name); + performValidate(data,name); + handler->loadObject(modName, name, data); + } + } + return result; +} + +void ContentTypeHandler::loadCustom() +{ + handler->loadCustom(); +} + +void ContentTypeHandler::afterLoadFinalization() +{ + handler->afterLoadFinalization(); +} + +void CContentHandler::init() +{ + handlers.insert(std::make_pair("heroClasses", ContentTypeHandler(&VLC->heroh->classes, "heroClass"))); + handlers.insert(std::make_pair("artifacts", ContentTypeHandler(VLC->arth, "artifact"))); + handlers.insert(std::make_pair("creatures", ContentTypeHandler(VLC->creh, "creature"))); + handlers.insert(std::make_pair("factions", ContentTypeHandler(VLC->townh, "faction"))); + handlers.insert(std::make_pair("objects", ContentTypeHandler(VLC->objtypeh, "object"))); + handlers.insert(std::make_pair("heroes", ContentTypeHandler(VLC->heroh, "hero"))); + handlers.insert(std::make_pair("spells", ContentTypeHandler(VLC->spellh, "spell"))); + handlers.insert(std::make_pair("skills", ContentTypeHandler(VLC->skillh, "skill"))); + handlers.insert(std::make_pair("templates", ContentTypeHandler(VLC->tplh, "template"))); +#if SCRIPTING_ENABLED + handlers.insert(std::make_pair("scripts", ContentTypeHandler(VLC->scriptHandler, "script"))); +#endif + handlers.insert(std::make_pair("battlefields", ContentTypeHandler(VLC->battlefieldsHandler, "battlefield"))); + handlers.insert(std::make_pair("terrains", ContentTypeHandler(VLC->terrainTypeHandler, "terrain"))); + handlers.insert(std::make_pair("rivers", ContentTypeHandler(VLC->riverTypeHandler, "river"))); + handlers.insert(std::make_pair("roads", ContentTypeHandler(VLC->roadTypeHandler, "road"))); + handlers.insert(std::make_pair("obstacles", ContentTypeHandler(VLC->obstacleHandler, "obstacle"))); + //TODO: any other types of moddables? +} + +bool CContentHandler::preloadModData(const std::string & modName, JsonNode modConfig, bool validate) +{ + bool result = true; + for(auto & handler : handlers) + { + result &= handler.second.preloadModData(modName, modConfig[handler.first].convertTo >(), validate); + } + return result; +} + +bool CContentHandler::loadMod(const std::string & modName, bool validate) +{ + bool result = true; + for(auto & handler : handlers) + { + result &= handler.second.loadMod(modName, validate); + } + return result; +} + +void CContentHandler::loadCustom() +{ + for(auto & handler : handlers) + { + handler.second.loadCustom(); + } +} + +void CContentHandler::afterLoadFinalization() +{ + for(auto & handler : handlers) + { + handler.second.afterLoadFinalization(); + } +} + +void CContentHandler::preloadData(CModInfo & mod) +{ + bool validate = (mod.validation != CModInfo::PASSED); + + // print message in format [<8-symbols checksum>] + logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); + + if (validate && mod.identifier != ModScope::scopeBuiltin()) + { + if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier)) + mod.validation = CModInfo::FAILED; + } + if (!preloadModData(mod.identifier, mod.config, validate)) + mod.validation = CModInfo::FAILED; +} + +void CContentHandler::load(CModInfo & mod) +{ + bool validate = (mod.validation != CModInfo::PASSED); + + if (!loadMod(mod.identifier, validate)) + mod.validation = CModInfo::FAILED; + + if (validate) + { + if (mod.validation != CModInfo::FAILED) + logMod->info("\t\t[DONE] %s", mod.name); + else + logMod->error("\t\t[FAIL] %s", mod.name); + } + else + logMod->info("\t\t[SKIP] %s", mod.name); +} + +const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const +{ + return handlers.at(name); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h new file mode 100644 index 000000000..e1224013e --- /dev/null +++ b/lib/modding/ContentTypeHandler.h @@ -0,0 +1,77 @@ +/* + * ContentTypeHandler.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 "../JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IHandlerBase; +class CModInfo; + +/// internal type to handle loading of one data type (e.g. artifacts, creatures) +class DLL_LINKAGE ContentTypeHandler +{ +public: + struct ModInfo + { + /// mod data from this mod and for this mod + JsonNode modData; + /// mod data for this mod from other mods (patches) + JsonNode patches; + }; + /// handler to which all data will be loaded + IHandlerBase * handler; + std::string objectName; + + /// contains all loaded H3 data + std::vector originalData; + std::map modData; + + ContentTypeHandler(IHandlerBase * handler, const std::string & objectName); + + /// local version of methods in ContentHandler + /// returns true if loading was successful + bool preloadModData(const std::string & modName, const std::vector & fileList, bool validate); + bool loadMod(const std::string & modName, bool validate); + void loadCustom(); + void afterLoadFinalization(); +}; + +/// class used to load all game data into handlers. Used only during loading +class DLL_LINKAGE CContentHandler +{ + /// preloads all data from fileList as data from modName. + bool preloadModData(const std::string & modName, JsonNode modConfig, bool validate); + + /// actually loads data in mod + bool loadMod(const std::string & modName, bool validate); + + std::map handlers; + +public: + void init(); + + /// preloads all data from fileList as data from modName. + void preloadData(CModInfo & mod); + + /// actually loads data in mod + void load(CModInfo & mod); + + void loadCustom(); + + /// all data was loaded, time for final validation / integration + void afterLoadFinalization(); + + const ContentTypeHandler & operator[] (const std::string & name) const; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp new file mode 100644 index 000000000..61a8854d7 --- /dev/null +++ b/lib/modding/IdentifierStorage.cpp @@ -0,0 +1,339 @@ +/* + * IdentifierStorage.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 "IdentifierStorage.h" + +#include "CModHandler.h" +#include "ModScope.h" + +#include "../JsonNode.h" +#include "../VCMI_Lib.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +CIdentifierStorage::CIdentifierStorage(): + state(LOADING) +{ +} + +void CIdentifierStorage::checkIdentifier(std::string & ID) +{ + if (boost::algorithm::ends_with(ID, ".")) + logMod->warn("BIG WARNING: identifier %s seems to be broken!", ID); + else + { + size_t pos = 0; + do + { + if (std::tolower(ID[pos]) != ID[pos] ) //Not in camelCase + { + logMod->warn("Warning: identifier %s is not in camelCase!", ID); + ID[pos] = std::tolower(ID[pos]);// Try to fix the ID + } + pos = ID.find('.', pos); + } + while(pos++ != std::string::npos); + } +} + +void CIdentifierStorage::requestIdentifier(ObjectCallback callback) const +{ + checkIdentifier(callback.type); + checkIdentifier(callback.name); + + assert(!callback.localScope.empty()); + + if (state != FINISHED) // enqueue request if loading is still in progress + scheduledRequests.push_back(callback); + else // execute immediately for "late" requests + resolveIdentifier(callback); +} + +CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional) +{ + assert(!scope.empty()); + + auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); + auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); + + if (scope == scopeAndFullName.first) + logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); + + ObjectCallback result; + result.localScope = scope; + result.remoteScope = scopeAndFullName.first; + result.type = typeAndName.first; + result.name = typeAndName.second; + result.callback = callback; + result.optional = optional; + return result; +} + +CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional) +{ + assert(!scope.empty()); + + auto scopeAndFullName = vstd::splitStringToPair(fullName, ':'); + auto typeAndName = vstd::splitStringToPair(scopeAndFullName.second, '.'); + + if(!typeAndName.first.empty()) + { + if (typeAndName.first != type) + logMod->error("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); + else + logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope); + } + + if (scope == scopeAndFullName.first) + logMod->debug("Target scope for identifier '%s' is redundant! Identifier already defined in mod '%s'", fullName, scope); + + ObjectCallback result; + result.localScope = scope; + result.remoteScope = scopeAndFullName.first; + result.type = type; + result.name = typeAndName.second; + result.callback = callback; + result.optional = optional; + return result; +} + +void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameWithType(scope, fullName, callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, false)); +} + +void CIdentifierStorage::requestIdentifier(const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameWithType(name.meta, name.String(), callback, false)); +} + +void CIdentifierStorage::tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(scope, type, name, callback, true)); +} + +void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const +{ + requestIdentifier(ObjectCallback::fromNameAndType(name.meta, type, name.String(), callback, true)); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const +{ + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s of type %s from mod %s", name , type ,scope); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) const +{ + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s of type %s from mod %s", name.String(), type, name.meta); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) const +{ + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s from mod %s", name.String(), name.meta); + + return std::optional(); +} + +std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) const +{ + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); + + if (idList.size() == 1) + return idList.front().id; + if (!silent) + logMod->error("Failed to resolve identifier %s from mod %s", fullName, scope); + + return std::optional(); +} + +void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) +{ + ObjectData data; + data.scope = scope; + data.id = identifier; + + std::string fullID = type + '.' + name; + checkIdentifier(fullID); + + std::pair mapping = std::make_pair(fullID, data); + if(!vstd::containsMapping(registeredObjects, mapping)) + { + logMod->trace("registered %s as %s:%s", fullID, scope, identifier); + registeredObjects.insert(mapping); + } +} + +std::vector CIdentifierStorage::getPossibleIdentifiers(const ObjectCallback & request) const +{ + std::set allowedScopes; + bool isValidScope = true; + + // called have not specified destination mod explicitly + if (request.remoteScope.empty()) + { + // special scope that should have access to all in-game objects + if (request.localScope == ModScope::scopeGame()) + { + for(const auto & modName : VLC->modh->getActiveMods()) + allowedScopes.insert(modName); + } + + // normally ID's from all required mods, own mod and virtual built-in mod are allowed + else if(request.localScope != ModScope::scopeBuiltin() && !request.localScope.empty()) + { + allowedScopes = VLC->modh->getModDependencies(request.localScope, isValidScope); + + if(!isValidScope) + return std::vector(); + + allowedScopes.insert(request.localScope); + } + + // all mods can access built-in mod + allowedScopes.insert(ModScope::scopeBuiltin()); + } + else + { + //if destination mod was specified explicitly, restrict lookup to this mod + if(request.remoteScope == ModScope::scopeBuiltin() ) + { + //built-in mod is an implicit dependency for all mods, allow access into it + allowedScopes.insert(request.remoteScope); + } + else if ( request.localScope == ModScope::scopeGame() ) + { + // allow access, this is special scope that should have access to all in-game objects + allowedScopes.insert(request.remoteScope); + } + else if(request.remoteScope == request.localScope ) + { + // allow self-access + allowedScopes.insert(request.remoteScope); + } + else + { + // allow access only if mod is in our dependencies + auto myDeps = VLC->modh->getModDependencies(request.localScope, isValidScope); + + if(!isValidScope) + return std::vector(); + + if(myDeps.count(request.remoteScope)) + allowedScopes.insert(request.remoteScope); + } + } + + std::string fullID = request.type + '.' + request.name; + + auto entries = registeredObjects.equal_range(fullID); + if (entries.first != entries.second) + { + std::vector locatedIDs; + + for (auto it = entries.first; it != entries.second; it++) + { + if (vstd::contains(allowedScopes, it->second.scope)) + { + locatedIDs.push_back(it->second); + } + } + return locatedIDs; + } + return std::vector(); +} + +bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const +{ + auto identifiers = getPossibleIdentifiers(request); + if (identifiers.size() == 1) // normally resolved ID + { + request.callback(identifiers.front().id); + return true; + } + + if (request.optional && identifiers.empty()) // failed to resolve optinal ID + { + return true; + } + + // error found. Try to generate some debug info + if(identifiers.empty()) + logMod->error("Unknown identifier!"); + else + logMod->error("Ambiguous identifier request!"); + + logMod->error("Request for %s.%s from mod %s", request.type, request.name, request.localScope); + + for(const auto & id : identifiers) + { + logMod->error("\tID is available in mod %s", id.scope); + } + return false; +} + +void CIdentifierStorage::finalize() +{ + state = FINALIZING; + bool errorsFound = false; + + while ( !scheduledRequests.empty() ) + { + // Use local copy since new requests may appear during resolving, invalidating any iterators + auto request = scheduledRequests.back(); + scheduledRequests.pop_back(); + + if (!resolveIdentifier(request)) + errorsFound = true; + } + + if (errorsFound) + { + for(const auto & object : registeredObjects) + { + logMod->trace("%s : %s -> %d", object.second.scope, object.first, object.second.id); + } + logMod->error("All known identifiers were dumped into log file"); + } + assert(errorsFound == false); + state = FINISHED; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h new file mode 100644 index 000000000..87bc0c3fa --- /dev/null +++ b/lib/modding/IdentifierStorage.h @@ -0,0 +1,110 @@ +/* + * IdentifierStorage.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; + +/// class that stores all object identifiers strings and maps them to numeric ID's +/// if possible, objects ID's should be in format ., camelCase e.g. "creature.grandElf" +class DLL_LINKAGE CIdentifierStorage +{ + enum ELoadingState + { + LOADING, + FINALIZING, + FINISHED + }; + + struct ObjectCallback // entry created on ID request + { + std::string localScope; /// scope from which this ID was requested + std::string remoteScope; /// scope in which this object must be found + std::string type; /// type, e.g. creature, faction, hero, etc + std::string name; /// string ID + std::function callback; + bool optional; + + /// Builds callback from identifier in form "targetMod:type.name" + static ObjectCallback fromNameWithType(const std::string & scope, const std::string & fullName, const std::function & callback, bool optional); + + /// Builds callback from identifier in form "targetMod:name" + static ObjectCallback fromNameAndType(const std::string & scope, const std::string & type, const std::string & fullName, const std::function & callback, bool optional); + + private: + ObjectCallback() = default; + }; + + struct ObjectData // entry created on ID registration + { + si32 id; + std::string scope; /// scope in which this ID located + + bool operator==(const ObjectData & other) const + { + return id == other.id && scope == other.scope; + } + + template void serialize(Handler &h, const int version) + { + h & id; + h & scope; + } + }; + + std::multimap registeredObjects; + mutable std::vector scheduledRequests; + + ELoadingState state; + + /// Check if identifier can be valid (camelCase, point as separator) + static void checkIdentifier(std::string & ID); + + void requestIdentifier(ObjectCallback callback) const; + bool resolveIdentifier(const ObjectCallback & callback) const; + std::vector getPossibleIdentifiers(const ObjectCallback & callback) const; + +public: + CIdentifierStorage(); + virtual ~CIdentifierStorage() = default; + + /// request identifier for specific object name. + /// Function callback will be called during ID resolution phase of loading + void requestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const; + ///fullName = [remoteScope:]type.name + void requestIdentifier(const std::string & scope, const std::string & fullName, const std::function & callback) const; + void requestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const; + void requestIdentifier(const JsonNode & name, const std::function & callback) const; + + /// try to request ID. If ID with such name won't be loaded, callback function will not be called + void tryRequestIdentifier(const std::string & scope, const std::string & type, const std::string & name, const std::function & callback) const; + void tryRequestIdentifier(const std::string & type, const JsonNode & name, const std::function & callback) const; + + /// get identifier immediately. If identifier is not know and not silent call will result in error message + std::optional getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent = false) const; + std::optional getIdentifier(const std::string & type, const JsonNode & name, bool silent = false) const; + std::optional getIdentifier(const JsonNode & name, bool silent = false) const; + std::optional getIdentifier(const std::string & scope, const std::string & fullName, bool silent = false) const; + + /// registers new object + void registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier); + + /// called at the very end of loading to check for any missing ID's + void finalize(); + + template void serialize(Handler &h, const int version) + { + h & registeredObjects; + h & state; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h new file mode 100644 index 000000000..ee7e2e2c4 --- /dev/null +++ b/lib/modding/ModIncompatibility.h @@ -0,0 +1,42 @@ +/* + * ModIncompatibility.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE ModIncompatibility: public std::exception +{ +public: + using StringPair = std::pair; + using ModList = std::list; + + ModIncompatibility(ModList && _missingMods): + missingMods(std::move(_missingMods)) + { + std::ostringstream _ss; + for(const auto & m : missingMods) + _ss << m.first << ' ' << m.second << std::endl; + message = _ss.str(); + } + + const char * what() const noexcept override + { + return message.c_str(); + } + +private: + //list of mods required to load the game + // first: mod name + // second: mod version + const ModList missingMods; + std::string message; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModScope.h b/lib/modding/ModScope.h new file mode 100644 index 000000000..d6a65448d --- /dev/null +++ b/lib/modding/ModScope.h @@ -0,0 +1,53 @@ +/* + * ModScope.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 + +VCMI_LIB_NAMESPACE_BEGIN + +namespace ModScope +{ + +/// returns true if scope is reserved for internal use and can not be used by mods +inline bool isScopeReserved(const std::string & scope) +{ + //following scopes are reserved - either in use by mod system or by filesystem + static const std::array reservedScopes = { + "core", "map", "game", "root", "saves", "config", "local", "initial", "mapEditor" + }; + + return std::find(reservedScopes.begin(), reservedScopes.end(), scope) != reservedScopes.end(); +} + +/// reserved scope name for referencing built-in (e.g. H3) objects +inline const std::string & scopeBuiltin() +{ + static const std::string scope = "core"; + return scope; +} + +/// reserved scope name for accessing objects from any loaded mod +inline const std::string & scopeGame() +{ + static const std::string scope = "game"; + return scope; +} + +/// reserved scope name for accessing object for map loading +inline const std::string & scopeMap() +{ + //TODO: implement accessing map dependencies for both H3 and VCMI maps + // for now, allow access to any identifiers + static const std::string scope = "game"; + return scope; +} + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModUtility.cpp b/lib/modding/ModUtility.cpp new file mode 100644 index 000000000..afa7ef35e --- /dev/null +++ b/lib/modding/ModUtility.cpp @@ -0,0 +1,77 @@ +/* + * CModHandler.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 "ModUtility.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +std::string ModUtility::normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier) +{ + auto p = vstd::splitStringToPair(identifier, ':'); + + if(p.first.empty()) + p.first = scope; + + if(p.first == remoteScope) + p.first.clear(); + + return p.first.empty() ? p.second : p.first + ":" + p.second; +} + +void ModUtility::parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier) +{ + auto p = vstd::splitStringToPair(fullIdentifier, ':'); + + scope = p.first; + + auto p2 = vstd::splitStringToPair(p.second, '.'); + + if(!p2.first.empty()) + { + type = p2.first; + identifier = p2.second; + } + else + { + type = p.second; + identifier.clear(); + } +} + +std::string ModUtility::makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier) +{ + if(type.empty()) + logGlobal->error("Full identifier (%s %s) requires type name", scope, identifier); + + std::string actualScope = scope; + std::string actualName = identifier; + + //ignore scope if identifier is scoped + auto scopeAndName = vstd::splitStringToPair(identifier, ':'); + + if(!scopeAndName.first.empty()) + { + actualScope = scopeAndName.first; + actualName = scopeAndName.second; + } + + if(actualScope.empty()) + { + return actualName.empty() ? type : type + "." + actualName; + } + else + { + return actualName.empty() ? actualScope+ ":" + type : actualScope + ":" + type + "." + actualName; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ModUtility.h b/lib/modding/ModUtility.h new file mode 100644 index 000000000..edf6ae456 --- /dev/null +++ b/lib/modding/ModUtility.h @@ -0,0 +1,25 @@ +/* + * ModUtility.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 + +VCMI_LIB_NAMESPACE_BEGIN + +// NOTE: all methods in this namespace should be considered internal to modding system and should not be used outside of this module +namespace ModUtility +{ + DLL_LINKAGE std::string normalizeIdentifier(const std::string & scope, const std::string & remoteScope, const std::string & identifier); + + DLL_LINKAGE void parseIdentifier(const std::string & fullIdentifier, std::string & scope, std::string & type, std::string & identifier); + + DLL_LINKAGE std::string makeFullIdentifier(const std::string & scope, const std::string & type, const std::string & identifier); + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 985fb927d..08db1d259 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -17,7 +17,6 @@ #include "../CPlayerState.h" #include "../CHeroHandler.h" #include "../CTownHandler.h" -#include "../CModHandler.h" //needed? #include "../mapObjectConstructors/CRewardableConstructor.h" #include "../mapObjectConstructors/CommonConstructors.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp index 794489ec0..614b9d5d6 100644 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ b/lib/registerTypes/TypesClientPacks1.cpp @@ -11,7 +11,6 @@ #include "RegisterTypes.h" #include "../StartInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp index 29fb941fc..482047ad0 100644 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ b/lib/registerTypes/TypesClientPacks2.cpp @@ -13,7 +13,6 @@ #include "../StartInfo.h" #include "../CStack.h" #include "../battle/BattleInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp index 8ac2734a7..56a26e4ff 100644 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ b/lib/registerTypes/TypesLobbyPacks.cpp @@ -16,7 +16,6 @@ #include "../gameState/CGameStateCampaign.h" #include "../gameState/TavernHeroesPool.h" #include "../mapping/CMap.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp index e0c7ab158..dbb424350 100644 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ b/lib/registerTypes/TypesMapObjects1.cpp @@ -11,7 +11,6 @@ #include "RegisterTypes.h" #include "../StartInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp index f712e52fe..b68767f1d 100644 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ b/lib/registerTypes/TypesMapObjects2.cpp @@ -13,7 +13,6 @@ #include "../StartInfo.h" #include "../CStack.h" #include "../battle/BattleInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesMapObjects3.cpp b/lib/registerTypes/TypesMapObjects3.cpp index 56b3dce18..79a471c4f 100644 --- a/lib/registerTypes/TypesMapObjects3.cpp +++ b/lib/registerTypes/TypesMapObjects3.cpp @@ -11,7 +11,6 @@ #include "RegisterTypes.h" #include "../StartInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp index e5d2938a1..e46124d7d 100644 --- a/lib/registerTypes/TypesServerPacks.cpp +++ b/lib/registerTypes/TypesServerPacks.cpp @@ -11,7 +11,6 @@ #include "RegisterTypes.h" #include "../StartInfo.h" -#include "../CModHandler.h" #include "../mapObjects/CObjectHandler.h" #include "../CCreatureHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index e68e44ba3..c82fca352 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -16,10 +16,10 @@ #include "Reward.h" #include "../CGeneralTextHandler.h" -#include "../CModHandler.h" #include "../IGameCallback.h" #include "../JsonRandom.h" #include "../mapObjects/IObjectInterface.h" +#include "../modding/IdentifierStorage.h" VCMI_LIB_NAMESPACE_BEGIN @@ -165,8 +165,8 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand for ( auto node : source["changeCreatures"].Struct() ) { - CreatureID from(VLC->modh->identifiers.getIdentifier(node.second.meta, "creature", node.first).value()); - CreatureID dest(VLC->modh->identifiers.getIdentifier(node.second.meta, "creature", node.second.String()).value()); + CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); + CreatureID dest(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.second.String()).value()); reward.extraComponents.emplace_back(Component::EComponentType::CREATURE, dest.getNum(), 0, 0); diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index ccd0881c6..261477a9d 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -16,9 +16,9 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" -#include "../CModHandler.h" #include "../TerrainHandler.h" #include "../serializer/JsonSerializeFormat.h" +#include "../modding/ModScope.h" #include "../StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -71,7 +71,7 @@ class TerrainEncoder public: static si32 decode(const std::string & identifier) { - return *VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "terrain", identifier); + return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "terrain", identifier); } static std::string encode(const si32 index) diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index 9947782bf..ae5f01c00 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -14,7 +14,6 @@ #include "CRmgTemplate.h" #include "../serializer/JsonDeserializer.h" -#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 498c4fab0..624ff26fe 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -16,8 +16,9 @@ #include "../Functions.h" #include "../CMapGenerator.h" #include "../threadpool/MapProxy.h" -#include "../../CModHandler.h" #include "../../mapping/CMapEditManager.h" +#include "../../modding/IdentifierStorage.h" +#include "../../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -133,7 +134,7 @@ void RoadPlacer::drawRoads(bool secondary) auto tiles = roads.getTilesVector(); std::string roadName = (secondary ? generator.getConfig().secondaryRoadType : generator.getConfig().defaultRoadType); - RoadId roadType(*VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "road", roadName)); + RoadId roadType(*VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "road", roadName)); //If our road type is not enabled, choose highest below it for (int8_t bestRoad = roadType.getNum(); bestRoad > RoadId(Road::NO_ROAD).getNum(); bestRoad--) diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index fd1401833..a2be34204 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -10,7 +10,7 @@ #pragma once #include "../JsonNode.h" -#include "../CModHandler.h" +#include "../modding/IdentifierStorage.h" #include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -369,7 +369,7 @@ public: for(const auto & id : node.Vector()) { - VLC->modh->identifiers.requestIdentifier(U::entityType(), id, [&value](int32_t identifier) + VLC->identifiers()->requestIdentifier(U::entityType(), id, [&value](int32_t identifier) { value.emplace(identifier); }); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 5d470f79b..2087478c9 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -20,7 +20,6 @@ #include "../CGeneralTextHandler.h" #include "../filesystem/Filesystem.h" -#include "../CModHandler.h" #include "../StringConstants.h" #include "../battle/BattleInfo.h" @@ -29,6 +28,8 @@ #include "../mapObjects/CGHeroInstance.h" //todo: remove #include "../serializer/CSerializer.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModUtility.h" #include "ISpellMechanics.h" @@ -458,7 +459,7 @@ JsonNode CSpell::convertTargetCondition(const BTVector & immunity, const BTVecto auto iter = bonusNameRMap.find(bonusType); if(iter != bonusNameRMap.end()) { - auto fullId = CModHandler::makeFullIdentifier("", "bonus", iter->second); + auto fullId = ModUtility::makeFullIdentifier("", "bonus", iter->second); res[targetName][fullId].String() = value; } else @@ -713,7 +714,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { const int chance = static_cast(node.second.Integer()); - VLC->modh->identifiers.requestIdentifier(node.second.meta, "faction", node.first, [=](si32 factionID) + VLC->identifiers()->requestIdentifier(node.second.meta, "faction", node.first, [=](si32 factionID) { spell->probabilities[FactionID(factionID)] = chance; }); @@ -736,7 +737,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { if(counteredSpell.second.Bool()) { - VLC->modh->identifiers.requestIdentifier(counteredSpell.second.meta, counteredSpell.first, [=](si32 id) + VLC->identifiers()->requestIdentifier(counteredSpell.second.meta, counteredSpell.first, [=](si32 id) { spell->counteredSpells.emplace_back(id); }); diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index cc0cbf0d5..ab495ba60 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -18,9 +18,10 @@ #include "../bonuses/BonusParams.h" #include "../bonuses/BonusList.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModUtility.h" #include "../serializer/JsonSerializeFormat.h" #include "../VCMI_Lib.h" -#include "../CModHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -371,7 +372,7 @@ public: } else if(type == "creature") { - auto rawId = VLC->modh->identifiers.getIdentifier(scope, type, identifier, true); + auto rawId = VLC->identifiers()->getIdentifier(scope, type, identifier, true); if(rawId) return std::make_shared(CreatureID(rawId.value())); @@ -380,7 +381,7 @@ public: } else if(type == "spell") { - auto rawId = VLC->modh->identifiers.getIdentifier(scope, type, identifier, true); + auto rawId = VLC->identifiers()->getIdentifier(scope, type, identifier, true); if(rawId) return std::make_shared(SpellID(rawId.value())); @@ -539,7 +540,7 @@ void TargetCondition::loadConditions(const JsonNode & source, bool exclusive, bo std::string type; std::string identifier; - CModHandler::parseIdentifier(keyValue.first, scope, type, identifier); + ModUtility::parseIdentifier(keyValue.first, scope, type, identifier); item = itemFactory->createConfigurable(keyValue.second.meta, type, identifier); } diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index 722e4328e..d781b680f 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -24,7 +24,6 @@ #include "../lib/filesystem/CBinaryReader.h" #include "Animation.h" #include "../lib/CThreadHelper.h" -#include "../lib/CModHandler.h" #include "../lib/VCMI_Lib.h" #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp index 35ef4f8c4..23c75beb9 100644 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "townbulidingswidget.h" #include "ui_townbulidingswidget.h" -#include "../lib/CModHandler.h" #include "../lib/CGeneralTextHandler.h" std::string defaultBuildingIdConversion(BuildingID bId) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 156bcdeb4..bb99703e6 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -21,7 +21,6 @@ #include "../lib/VCMI_Lib.h" #include "../lib/logging/CBasicLogConfigurator.h" #include "../lib/CConfigHandler.h" -#include "../lib/CModHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/GameConstants.h" #include "../lib/mapObjectConstructors/AObjectTypeHandler.h" @@ -31,6 +30,7 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/MapFormat.h" +#include "../lib/modding/ModIncompatibility.h" #include "../lib/RoadHandler.h" #include "../lib/RiverHandler.h" #include "../lib/TerrainHandler.h" @@ -336,17 +336,17 @@ bool MainWindow::openMap(const QString & filenameSelect) if(auto header = mapService.loadMapHeader(resId)) { auto missingMods = CMapService::verifyMapHeaderMods(*header); - CModHandler::Incompatibility::ModList modList; + ModIncompatibility::ModList modList; for(const auto & m : missingMods) modList.push_back({m.first, m.second.toString()}); if(!modList.empty()) - throw CModHandler::Incompatibility(std::move(modList)); + throw ModIncompatibility(std::move(modList)); controller.setMap(mapService.loadMap(resId)); } } - catch(const CModHandler::Incompatibility & e) + catch(const ModIncompatibility & e) { QMessageBox::warning(this, "Mods requiered", e.what()); return false; diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 9cef5a540..70e3067c9 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -19,11 +19,12 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapEditManager.h" #include "../lib/mapping/ObstacleProxy.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/CModInfo.h" #include "../lib/TerrainHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" #include "../lib/serializer/CMemorySerializer.h" #include "mapview.h" #include "scenelayer.h" diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 1c602c0a0..ad2cecad0 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,7 +13,7 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/CModVersion.h" +#include "../lib/modding/CModVersion.h" VCMI_LIB_NAMESPACE_BEGIN using ModCompatibilityInfo = std::map; diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 17a43e0d5..9d09cea0c 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -20,7 +20,6 @@ #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/CHeroHandler.h" #include "../lib/CTownHandler.h" -#include "../lib/CModHandler.h" #include "../lib/GameConstants.h" #include "../lib/JsonDetail.h" diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp index 42041f755..ca42a447e 100644 --- a/mapeditor/mapsettings.cpp +++ b/mapeditor/mapsettings.cpp @@ -17,10 +17,11 @@ #include "../lib/CArtHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CGeneralTextHandler.h" -#include "../lib/CModHandler.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/CGCreature.h" #include "../lib/mapping/CMapService.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/CModInfo.h" #include "../lib/StringConstants.h" #include "inspector/townbulidingswidget.h" //to convert BuildingID to string diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index d1ded12b0..4b70ddc84 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -14,8 +14,9 @@ #include "ui_validator.h" #include "../lib/mapping/CMap.h" #include "../lib/mapObjects/MapObjects.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/CModInfo.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" Validator::Validator(const CMap * map, QWidget *parent) : QDialog(parent), diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 38bed3222..4b3508610 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -24,7 +24,7 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/battle/IBattleInfoCallback.h" #include "../../lib/CGameInfoCallback.h" -#include "../../lib/CModHandler.h" +#include "../../lib/modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -511,14 +511,14 @@ int LuaContext::loadModule() registar->pushMetatable(L); } - else if(scope == CModHandler::scopeBuiltin()) + else if(scope == ModScope::scopeBuiltin()) { // boost::algorithm::replace_all(modulePath, boost::is_any_of("\\/ "), ""); boost::algorithm::replace_all(modulePath, ".", "/"); - auto *loader = CResourceHandler::get(CModHandler::scopeBuiltin()); + auto *loader = CResourceHandler::get(ModScope::scopeBuiltin()); modulePath = "scripts/lib/" + modulePath; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9df39108a..bf31b4b69 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -22,7 +22,6 @@ #include "../lib/int3.h" #include "../lib/ArtifactUtils.h" #include "../lib/StartInfo.h" -#include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CBuildingHandler.h" #include "../lib/CHeroHandler.h" @@ -46,6 +45,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" +#include "../lib/modding/ModIncompatibility.h" #include "../lib/rmg/CMapGenOptions.h" #include "../lib/VCMIDirs.h" #include "../lib/ScopeGuard.h" @@ -2113,7 +2113,7 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField(*VLC->modh->identifiers.getIdentifier("core", "battlefield", "ship_to_ship")); + terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield", "ship_to_ship")); //send info about battles BattleStart bs; @@ -2936,7 +2936,7 @@ bool CGameHandler::load(const std::string & filename) } logGlobal->info("Game has been successfully loaded!"); } - catch(const CModHandler::Incompatibility & e) + catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 8ab329289..0fa101ad0 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -14,7 +14,6 @@ #include "../lib/campaign/CampaignState.h" #include "../lib/CThreadHelper.h" #include "../lib/serializer/Connection.h" -#include "../lib/CModHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" diff --git a/server/PlayerMessageProcessor.cpp b/server/PlayerMessageProcessor.cpp index 268802a9a..2736f68c9 100644 --- a/server/PlayerMessageProcessor.cpp +++ b/server/PlayerMessageProcessor.cpp @@ -16,13 +16,14 @@ #include "../lib/serializer/Connection.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/CModHandler.h" #include "../lib/CPlayerState.h" #include "../lib/GameConstants.h" #include "../lib/NetPacks.h" #include "../lib/StartInfo.h" #include "../lib/gameState/CGameState.h" #include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/modding/IdentifierStorage.h" +#include "../lib/modding/ModScope.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) @@ -179,7 +180,7 @@ void PlayerMessageProcessor::cheatGiveArmy(PlayerColor player, const CGHeroInsta { } - std::optional creatureId = VLC->modh->identifiers.getIdentifier(CModHandler::scopeGame(), "creature", creatureIdentifier, false); + std::optional creatureId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", creatureIdentifier, false); if(creatureId.has_value()) { diff --git a/test/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index 224d746c0..137214fc4 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -8,7 +8,7 @@ * */ #include "StdInc.h" -#include "lib/CModHandler.h" +#include "lib/modding/ModScope.h" #include "ScriptFixture.h" @@ -30,7 +30,7 @@ void ScriptFixture::loadScriptFromFile(const std::string & path) void ScriptFixture::loadScript(const JsonNode & scriptConfig) { - subject = VLC->scriptHandler->loadFromJson(&loggerMock, CModHandler::scopeBuiltin(), scriptConfig, "test"); + subject = VLC->scriptHandler->loadFromJson(&loggerMock, ModScope::scopeBuiltin(), scriptConfig, "test"); GTEST_ASSERT_NE(subject, nullptr); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index 7eb41bf72..b5bddc269 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -12,7 +12,7 @@ #include "EffectFixture.h" #include -#include "lib/CModHandler.h" +#include "lib/modding/ModScope.h" namespace test { @@ -80,7 +80,7 @@ TEST_P(TimedApplyTest, ChangesBonuses) options["cumulative"].Bool() = cumulative; options["bonus"]["test1"] = testBonus1.toJsonNode(); options["bonus"]["test2"] = testBonus2.toJsonNode(); - options.setMeta(CModHandler::scopeBuiltin()); + options.setMeta(ModScope::scopeBuiltin()); setupEffect(options); const uint32_t unitId = 42; From 43795c39a5770bff18ef2f62ea32aead683a856d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 31 Jul 2023 19:50:55 +0300 Subject: [PATCH 0011/1248] Replaced all usage of SDL_Color outside of render with ColorRGBA --- client/CMT.cpp | 1 + client/CMakeLists.txt | 1 + client/NetPacksLobbyClient.cpp | 2 +- client/adventureMap/AdventureMapWidget.h | 1 + client/adventureMap/CList.cpp | 1 + client/adventureMap/CMinimap.cpp | 13 +++--- client/adventureMap/CResDataBar.cpp | 1 + client/battle/BattleAnimationClasses.cpp | 4 +- client/battle/BattleAnimationClasses.h | 1 - client/battle/BattleEffectsController.cpp | 1 + client/battle/BattleFieldController.h | 1 + client/battle/BattleInterfaceClasses.cpp | 2 + client/battle/BattleInterfaceClasses.h | 2 + client/battle/CreatureAnimation.cpp | 30 ++++++------- client/battle/CreatureAnimation.h | 15 +++---- client/gui/CGuiHandler.cpp | 3 ++ client/gui/CIntObject.h | 6 ++- client/gui/InterfaceObjectConfigurable.cpp | 2 +- client/gui/InterfaceObjectConfigurable.h | 3 +- client/gui/WindowHandler.cpp | 1 - client/lobby/CBonusSelection.cpp | 1 + client/lobby/CLobbyScreen.cpp | 7 +-- client/lobby/CSelectionBase.cpp | 2 + client/lobby/OptionsTab.cpp | 2 + client/lobby/RandomMapTab.cpp | 8 ++-- client/mapView/MapView.h | 1 + client/render/Canvas.cpp | 16 ++++--- client/render/Canvas.h | 9 ++-- client/render/ColorFilter.cpp | 5 +-- client/render/ColorFilter.h | 5 +-- client/render/Colors.cpp | 26 +++++------ client/render/Colors.h | 28 ++++++------ client/render/EFont.h | 15 +++++++ client/render/Graphics.cpp | 51 +++++++++------------- client/render/Graphics.h | 21 ++++----- client/render/IFont.cpp | 12 ++--- client/render/IFont.h | 16 +++---- client/render/IImage.h | 4 +- client/renderSDL/CBitmapFont.cpp | 4 +- client/renderSDL/CBitmapFont.h | 4 +- client/renderSDL/CBitmapHanFont.cpp | 4 +- client/renderSDL/CBitmapHanFont.h | 4 +- client/renderSDL/CTrueTypeFont.cpp | 6 +-- client/renderSDL/CTrueTypeFont.h | 2 +- client/renderSDL/CursorHardware.cpp | 2 +- client/renderSDL/CursorSoftware.cpp | 2 +- client/renderSDL/SDLImage.cpp | 4 +- client/renderSDL/SDL_Extensions.cpp | 38 +++++----------- client/renderSDL/SDL_Extensions.h | 6 +-- client/widgets/Buttons.cpp | 19 ++------ client/widgets/Buttons.h | 17 ++------ client/widgets/CComponent.cpp | 2 + client/widgets/CComponent.h | 2 + client/widgets/Images.h | 1 - client/widgets/MiscWidgets.h | 4 ++ client/widgets/Slider.cpp | 1 + client/widgets/TextControls.cpp | 16 ++++--- client/widgets/TextControls.h | 22 +++++----- client/windows/CKingdomInterface.h | 4 ++ client/windows/CMessage.cpp | 2 + client/windows/CMessage.h | 2 +- client/windows/CPuzzleWindow.cpp | 2 +- client/windows/CQuestLog.h | 2 +- client/windows/CSpellWindow.cpp | 2 +- client/windows/CSpellWindow.h | 1 + client/windows/GUIClasses.h | 1 + client/windows/InfoWindows.h | 3 ++ client/windows/QuickRecruitmentWindow.h | 4 ++ 68 files changed, 251 insertions(+), 252 deletions(-) create mode 100644 client/render/EFont.h diff --git a/client/CMT.cpp b/client/CMT.cpp index 4dce2fecb..b938c95c4 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -25,6 +25,7 @@ #include "ClientCommandManager.h" #include "windows/CMessage.h" #include "render/IScreenHandler.h" +#include "render/Graphics.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/CGeneralTextHandler.h" diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index e67d803e1..2121e54f2 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -226,6 +226,7 @@ set(client_HEADERS render/Canvas.h render/ColorFilter.h render/Colors.h + render/EFont.h render/Graphics.h render/ICursor.h render/IFont.h diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 3a153006f..37ecb9788 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -68,7 +68,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & lobby->card->chat->addNewMessage(pack.playerName + ": " + pack.message); lobby->card->setChat(true); if(lobby->buttonChat) - lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + lobby->buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); } } diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index b955208aa..bf02f7c46 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -11,6 +11,7 @@ #include "../gui/InterfaceObjectConfigurable.h" +class CAnimation; class CHeroList; class CTownList; class CMinimap; diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index e53968d9f..fdb904bfa 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -22,6 +22,7 @@ #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../render/Canvas.h" +#include "../render/Colors.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 9c9b9134f..9dc5da7a7 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -20,8 +20,9 @@ #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" #include "../render/Colors.h" -#include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../renderSDL/SDL_Extensions.h" #include "../windows/InfoWindows.h" #include "../../CCallback.h" @@ -30,25 +31,23 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapping/CMapDefines.h" -#include - ColorRGBA CMinimapInstance::getTileColor(const int3 & pos) const { const TerrainTile * tile = LOCPLINT->cb->getTile(pos, false); // if tile is not visible it will be black on minimap if(!tile) - return CSDL_Ext::fromSDL(Colors::BLACK); + return Colors::BLACK; // if object at tile is owned - it will be colored as its owner for (const CGObjectInstance *obj : tile->blockingObjects) { PlayerColor player = obj->getOwner(); if(player == PlayerColor::NEUTRAL) - return CSDL_Ext::fromSDL(*graphics->neutralColor); + return graphics->neutralColor; if (player < PlayerColor::PLAYER_LIMIT) - return CSDL_Ext::fromSDL(graphics->playerColors[player.getNum()]); + return graphics->playerColors[player.getNum()]; } if (tile->blocked && (!tile->visitable)) @@ -185,7 +184,7 @@ void CMinimap::showAll(Canvas & to) }; Canvas clippedTarget(to, pos); - clippedTarget.drawBorderDashed(radar, CSDL_Ext::fromSDL(Colors::PURPLE)); + clippedTarget.drawBorderDashed(radar, Colors::PURPLE); } } diff --git a/client/adventureMap/CResDataBar.cpp b/client/adventureMap/CResDataBar.cpp index 6f8a340ff..afea62a38 100644 --- a/client/adventureMap/CResDataBar.cpp +++ b/client/adventureMap/CResDataBar.cpp @@ -14,6 +14,7 @@ #include "../CPlayerInterface.h" #include "../render/Canvas.h" #include "../render/Colors.h" +#include "../render/EFont.h" #include "../gui/CGuiHandler.h" #include "../gui/TextAlignment.h" #include "../widgets/Images.h" diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index f0db62fd3..3a89b0661 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -384,8 +384,8 @@ void MovementAnimation::tick(uint32_t msPassed) progress += float(msPassed) / 1000 * progressPerSecond; //moving instructions - myAnim->pos.x = static_cast(begX + distanceX * progress ); - myAnim->pos.y = static_cast(begY + distanceY * progress ); + myAnim->pos.x = begX + distanceX * progress; + myAnim->pos.y = begY + distanceY * progress; BattleAnimation::tick(msPassed); diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index 48e9fa48b..f47f8ce9c 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -21,7 +21,6 @@ class Point; VCMI_LIB_NAMESPACE_END -struct SDL_Color; class ColorFilter; class BattleHero; class CAnimation; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index 1477234c5..df394b7d8 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -23,6 +23,7 @@ #include "../CPlayerInterface.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" +#include "../render/Graphics.h" #include "../../CCallback.h" #include "../../lib/battle/BattleAction.h" diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index 00c0ca793..27342bc4c 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -19,6 +19,7 @@ class Rect; VCMI_LIB_NAMESPACE_END class BattleHero; +class CAnimation; class Canvas; class IImage; class BattleInterface; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index a6289d07f..d8f37d0aa 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -29,6 +29,8 @@ #include "../gui/WindowHandler.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IFont.h" +#include "../render/Graphics.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index aa5668bc5..c77f84782 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; struct BattleResult; +struct InfoAboutHero; class CStack; namespace battle @@ -28,6 +29,7 @@ class Unit; VCMI_LIB_NAMESPACE_END +class CAnimation; class Canvas; class BattleInterface; class CPicture; diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index c534f02b2..64397ec04 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -15,28 +15,27 @@ #include "../render/Canvas.h" #include "../render/ColorFilter.h" -#include "../renderSDL/SDL_Extensions.h" -static const SDL_Color creatureBlueBorder = { 0, 255, 255, 255 }; -static const SDL_Color creatureGoldBorder = { 255, 255, 0, 255 }; -static const SDL_Color creatureNoBorder = { 0, 0, 0, 0 }; +static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; +static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 }; +static const ColorRGBA creatureNoBorder = { 0, 0, 0, 0 }; -static SDL_Color genShadow(ui8 alpha) +static ColorRGBA genShadow(ui8 alpha) { - return CSDL_Ext::makeColor(0, 0, 0, alpha); + return ColorRGBA(0, 0, 0, alpha); } -SDL_Color AnimationControls::getBlueBorder() +ColorRGBA AnimationControls::getBlueBorder() { return creatureBlueBorder; } -SDL_Color AnimationControls::getGoldBorder() +ColorRGBA AnimationControls::getGoldBorder() { return creatureGoldBorder; } -SDL_Color AnimationControls::getNoBorder() +ColorRGBA AnimationControls::getNoBorder() { return creatureNoBorder; } @@ -194,7 +193,6 @@ CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController animationEnd(-1), elapsedTime(0), type(ECreatureAnimType::HOLDING), - border(CSDL_Ext::makeColor(0, 0, 0, 0)), speedController(controller), once(false) { @@ -288,7 +286,7 @@ bool CreatureAnimation::incrementFrame(float timePassed) return false; } -void CreatureAnimation::setBorderColor(SDL_Color palette) +void CreatureAnimation::setBorderColor(ColorRGBA palette) { border = palette; } @@ -321,9 +319,9 @@ inline int getBorderStrength(float time) return static_cast(borderStrength * 155 + 100); // scale to 0-255 } -static SDL_Color genBorderColor(ui8 alpha, const SDL_Color & base) +static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base) { - return CSDL_Ext::makeColor(base.r, base.g, base.b, ui8(base.a * alpha / 256)); + return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256)); } static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) @@ -331,9 +329,9 @@ static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; } -static SDL_Color addColors(const SDL_Color & base, const SDL_Color & over) +static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) { - return CSDL_Ext::makeColor( + return ColorRGBA( mixChannels(over.r, base.r, over.a, base.a), mixChannels(over.g, base.g, over.a, base.a), mixChannels(over.b, base.b, over.a, base.a), @@ -355,7 +353,7 @@ void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) { - SDL_Color shadowTest = shifter.shiftColor(genShadow(128)); + ColorRGBA shadowTest = shifter.shiftColor(genShadow(128)); shadowAlpha = shadowTest.a; size_t frame = static_cast(floor(currentFrame)); diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 2e7ac4a7f..d98c12085 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -10,12 +10,11 @@ #pragma once #include "../../lib/FunctionList.h" +#include "../../lib/Color.h" #include "../widgets/Images.h" #include "../render/CAnimation.h" #include "../render/IImage.h" -#include - class CIntObject; class CreatureAnimation; class Canvas; @@ -23,10 +22,10 @@ class Canvas; /// Namespace for some common controls of animations namespace AnimationControls { - /// get SDL_Color for creature selection borders - SDL_Color getBlueBorder(); - SDL_Color getGoldBorder(); - SDL_Color getNoBorder(); + /// get color for creature selection borders + ColorRGBA getBlueBorder(); + ColorRGBA getGoldBorder(); + ColorRGBA getNoBorder(); /// returns animation speed factor according to game settings, /// slow speed is considered to be "base speed" and will return 1.0 @@ -100,7 +99,7 @@ private: uint8_t shadowAlpha; /// border color, disabled if alpha = 0 - SDL_Color border; + ColorRGBA border; TSpeedController speedController; @@ -136,7 +135,7 @@ public: /// should be called every frame, return true when animation was reset to beginning bool incrementFrame(float timePassed); - void setBorderColor(SDL_Color palette); + void setBorderColor(ColorRGBA palette); /// Gets the current frame ID within current group. float getCurrentFrame() const; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index fdccf1517..7976f973c 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -21,6 +21,9 @@ #include "../CGameInfo.h" #include "../render/Colors.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../render/EFont.h" #include "../renderSDL/ScreenHandler.h" #include "../CMT.h" #include "../CPlayerInterface.h" diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 4b4dbfd87..5d8a3f53a 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -9,10 +9,12 @@ */ #pragma once -#include "../render/Graphics.h" -#include "../../lib/Rect.h" #include "EventsReceiver.h" +#include "../../lib/Rect.h" +#include "../../lib/Color.h" +#include "../../lib/GameConstants.h" + class CGuiHandler; class CPicture; class Canvas; diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index aba2998bc..751089445 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -185,7 +185,7 @@ ETextAlignment InterfaceObjectConfigurable::readTextAlignment(const JsonNode & c return ETextAlignment::CENTER; } -SDL_Color InterfaceObjectConfigurable::readColor(const JsonNode & config) const +ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const { logGlobal->debug("Reading color"); if(!config.isNull()) diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 790f1709c..07d0a57c1 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -12,6 +12,7 @@ #include "CIntObject.h" #include "TextAlignment.h" +#include "../render/EFont.h" #include "../../lib/JsonNode.h" @@ -73,7 +74,7 @@ protected: Point readPosition(const JsonNode &) const; Rect readRect(const JsonNode &) const; ETextAlignment readTextAlignment(const JsonNode &) const; - SDL_Color readColor(const JsonNode &) const; + ColorRGBA readColor(const JsonNode &) const; EFonts readFont(const JsonNode &) const; std::string readText(const JsonNode &) const; std::pair readHintText(const JsonNode &) const; diff --git a/client/gui/WindowHandler.cpp b/client/gui/WindowHandler.cpp index 690372589..522155d19 100644 --- a/client/gui/WindowHandler.cpp +++ b/client/gui/WindowHandler.cpp @@ -105,7 +105,6 @@ void WindowHandler::totalRedraw() void WindowHandler::totalRedrawImpl() { logGlobal->debug("totalRedraw requested!"); - CSDL_Ext::fillSurface(screen2, Colors::BLACK); Canvas target = Canvas::createFromSurface(screen2); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 3544b828d..2bd79577b 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -32,6 +32,7 @@ #include "../windows/InfoWindows.h" #include "../render/IImage.h" #include "../render/CAnimation.h" +#include "../render/Graphics.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index d38e209f7..d521f9b52 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -20,6 +20,7 @@ #include "../gui/Shortcut.h" #include "../widgets/Buttons.h" #include "../windows/InfoWindows.h" +#include "../render/Colors.h" #include "../../CCallback.h" @@ -53,7 +54,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) }; buttonChat = std::make_shared(Point(619, 80), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); switch(screenType) { @@ -182,9 +183,9 @@ void CLobbyScreen::toggleChat() { card->toggleChat(); if(card->showChat) - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[531], FONT_SMALL, Colors::WHITE); else - buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL); + buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); } void CLobbyScreen::updateAfterStateChange() diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index cc1d49ba9..de543817e 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -37,6 +37,8 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 2337be6c3..e7d5243ab 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -16,6 +16,8 @@ #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index dfb0157cb..7bdcae494 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -304,9 +304,9 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); + w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } for(auto r : VLC->roadTypeHandler->objects) { @@ -324,9 +324,9 @@ void RandomMapTab::setTemplate(const CRmgTemplate * tmpl) if(auto w = widget("templateButton")) { if(tmpl) - w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL); + w->addTextOverlay(tmpl->getName(), EFonts::FONT_SMALL, Colors::WHITE); else - w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL); + w->addTextOverlay(readText(variables["randomTemplate"]), EFonts::FONT_SMALL, Colors::WHITE); } updateMapInfoByHost(); } diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index a3434dc1a..f6719da84 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -13,6 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct ObjectPosInfo; +class CGObjectInstance; VCMI_LIB_NAMESPACE_END class Canvas; diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index dded81c9e..bf779cc18 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -14,8 +14,10 @@ #include "Colors.h" #include "IImage.h" #include "Graphics.h" +#include "IFont.h" #include +#include Canvas::Canvas(SDL_Surface * surface): surface(surface), @@ -48,7 +50,7 @@ Canvas::Canvas(const Point & size): renderArea(Point(0,0), size), surface(CSDL_Ext::newSurface(size.x, size.y)) { - CSDL_Ext::fillSurface(surface, Colors::TRANSPARENCY ); + CSDL_Ext::fillSurface(surface, CSDL_Ext::toSDL(Colors::TRANSPARENCY) ); SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); } @@ -127,11 +129,11 @@ void Canvas::drawLineDashed(const Point & from, const Point & dest, const ColorR CSDL_Ext::drawLineDashed(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(color)); } -void Canvas::drawBorder(const Rect & target, const SDL_Color & color, int width) +void Canvas::drawBorder(const Rect & target, const ColorRGBA & color, int width) { Rect realTarget = target + renderArea.topLeft(); - CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, color, width); + CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width); } void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) @@ -144,7 +146,7 @@ void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) CSDL_Ext::drawLineDashed(surface, realTarget.topRight(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); } -void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text ) +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ) { switch (alignment) { @@ -154,7 +156,7 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col } } -void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector & text ) +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ) { switch (alignment) { @@ -164,11 +166,11 @@ void Canvas::drawText(const Point & position, const EFonts & font, const SDL_Col } } -void Canvas::drawColor(const Rect & target, const SDL_Color & color) +void Canvas::drawColor(const Rect & target, const ColorRGBA & color) { Rect realTarget = target + renderArea.topLeft(); - CSDL_Ext::fillRect(surface, realTarget, color); + CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); } SDL_Surface * Canvas::getInternalSurface() diff --git a/client/render/Canvas.h b/client/render/Canvas.h index b093d3123..c624507d1 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -13,7 +13,6 @@ #include "../../lib/Rect.h" #include "../../lib/Color.h" -struct SDL_Color; struct SDL_Surface; class IImage; enum EFonts : int; @@ -83,19 +82,19 @@ public: void drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color); /// renders rectangular, solid-color border in specified location - void drawBorder(const Rect & target, const SDL_Color & color, int width = 1); + void drawBorder(const Rect & target, const ColorRGBA & color, int width = 1); /// renders rectangular, dashed border in specified location void drawBorderDashed(const Rect & target, const ColorRGBA & color); /// renders single line of text with specified parameters - void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::string & text ); + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ); /// renders multiple lines of text with specified parameters - void drawText(const Point & position, const EFonts & font, const SDL_Color & colorDest, ETextAlignment alignment, const std::vector & text ); + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); /// fills selected area with solid color, ignoring any transparency - void drawColor(const Rect & target, const SDL_Color & color); + void drawColor(const Rect & target, const ColorRGBA & color); /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index 68a2989d5..327813f64 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -10,11 +10,10 @@ #include "StdInc.h" #include "ColorFilter.h" -#include - #include "../../lib/JsonNode.h" +#include "../../lib/Color.h" -SDL_Color ColorFilter::shiftColor(const SDL_Color & in) const +ColorRGBA ColorFilter::shiftColor(const ColorRGBA & in) const { int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; diff --git a/client/render/ColorFilter.h b/client/render/ColorFilter.h index bfca82695..989fab7bf 100644 --- a/client/render/ColorFilter.h +++ b/client/render/ColorFilter.h @@ -10,10 +10,9 @@ #pragma once -struct SDL_Color; - VCMI_LIB_NAMESPACE_BEGIN class JsonNode; +class ColorRGBA; VCMI_LIB_NAMESPACE_END /// Base class for applying palette transformation on images @@ -32,7 +31,7 @@ class ColorFilter r(r), g(g), b(b), a(a) {} public: - SDL_Color shiftColor(const SDL_Color & in) const; + ColorRGBA shiftColor(const ColorRGBA & in) const; bool operator == (const ColorFilter & other) const; bool operator != (const ColorFilter & other) const; diff --git a/client/render/Colors.cpp b/client/render/Colors.cpp index 57b7f2c5e..c7f5c1f4b 100644 --- a/client/render/Colors.cpp +++ b/client/render/Colors.cpp @@ -11,17 +11,15 @@ #include "StdInc.h" #include "Colors.h" -#include - -const SDL_Color Colors::YELLOW = { 229, 215, 123, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::WHITE = { 255, 243, 222, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::METALLIC_GOLD = { 173, 142, 66, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::GREEN = { 0, 255, 0, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::CYAN = { 0, 255, 255, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::ORANGE = { 232, 184, 32, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::BRIGHT_YELLOW = { 242, 226, 110, SDL_ALPHA_OPAQUE }; -const SDL_Color Colors::DEFAULT_KEY_COLOR = {0, 255, 255, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::RED = {255, 0, 0, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::PURPLE = {255, 75, 125, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::BLACK = {0, 0, 0, SDL_ALPHA_OPAQUE}; -const SDL_Color Colors::TRANSPARENCY = {0, 0, 0, SDL_ALPHA_TRANSPARENT}; +const ColorRGBA Colors::YELLOW = { 229, 215, 123, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::WHITE = { 255, 243, 222, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::METALLIC_GOLD = { 173, 142, 66, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::GREEN = { 0, 255, 0, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::CYAN = { 0, 255, 255, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::ORANGE = { 232, 184, 32, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::BRIGHT_YELLOW = { 242, 226, 110, ColorRGBA::ALPHA_OPAQUE }; +const ColorRGBA Colors::DEFAULT_KEY_COLOR = {0, 255, 255, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::RED = {255, 0, 0, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::PURPLE = {255, 75, 125, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::BLACK = {0, 0, 0, ColorRGBA::ALPHA_OPAQUE}; +const ColorRGBA Colors::TRANSPARENCY = {0, 0, 0, ColorRGBA::ALPHA_TRANSPARENT}; diff --git a/client/render/Colors.h b/client/render/Colors.h index 6f80f7352..5db94e4ed 100644 --- a/client/render/Colors.h +++ b/client/render/Colors.h @@ -9,44 +9,44 @@ */ #pragma once -struct SDL_Color; +#include "../../lib/Color.h" /** - * The colors class defines color constants of type SDL_Color. + * The colors class defines color constants of type ColorRGBA. */ class Colors { public: /** the h3 yellow color, typically used for headlines */ - static const SDL_Color YELLOW; + static const ColorRGBA YELLOW; /** the standard h3 white color */ - static const SDL_Color WHITE; + static const ColorRGBA WHITE; /** the metallic gold color used mostly as a border around buttons */ - static const SDL_Color METALLIC_GOLD; + static const ColorRGBA METALLIC_GOLD; /** green color used for in-game console */ - static const SDL_Color GREEN; + static const ColorRGBA GREEN; /** the h3 orange color, used for blocked buttons */ - static const SDL_Color ORANGE; + static const ColorRGBA ORANGE; /** the h3 bright yellow color, used for selection border */ - static const SDL_Color BRIGHT_YELLOW; + static const ColorRGBA BRIGHT_YELLOW; /** default key color for all 8 & 24 bit graphics */ - static const SDL_Color DEFAULT_KEY_COLOR; + static const ColorRGBA DEFAULT_KEY_COLOR; /// Selected creature card - static const SDL_Color RED; + static const ColorRGBA RED; /// Minimap border - static const SDL_Color PURPLE; + static const ColorRGBA PURPLE; - static const SDL_Color CYAN; + static const ColorRGBA CYAN; - static const SDL_Color BLACK; + static const ColorRGBA BLACK; - static const SDL_Color TRANSPARENCY; + static const ColorRGBA TRANSPARENCY; }; diff --git a/client/render/EFont.h b/client/render/EFont.h new file mode 100644 index 000000000..7f5dfee41 --- /dev/null +++ b/client/render/EFont.h @@ -0,0 +1,15 @@ +/* + * EFonts.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 + +enum EFonts : int +{ + FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD +}; diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 440653255..5d2cf92a3 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -46,23 +46,21 @@ void Graphics::loadPaletteAndColors() auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); std::string pals((char*)textFile.first.get(), textFile.second); - playerColorPalette = new SDL_Color[256]; - neutralColor = new SDL_Color; - playerColors = new SDL_Color[PlayerColor::PLAYER_LIMIT_I]; int startPoint = 24; //beginning byte; used to read - for(int i=0; i<256; ++i) + for(int i=0; i<8; ++i) { - SDL_Color col; - col.r = pals[startPoint++]; - col.g = pals[startPoint++]; - col.b = pals[startPoint++]; - col.a = SDL_ALPHA_OPAQUE; - startPoint++; - playerColorPalette[i] = col; + for(int j=0; j<32; ++j) + { + ColorRGBA col; + col.r = pals[startPoint++]; + col.g = pals[startPoint++]; + col.b = pals[startPoint++]; + col.a = SDL_ALPHA_OPAQUE; + startPoint++; + playerColorPalette[i][j] = col; + } } - neutralColorPalette = new SDL_Color[32]; - auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); CBinaryReader reader(stream.get()); @@ -75,7 +73,7 @@ void Graphics::loadPaletteAndColors() neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; } //colors initialization - SDL_Color colors[] = { + ColorRGBA colors[] = { {0xff,0, 0, SDL_ALPHA_OPAQUE}, {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, @@ -91,10 +89,10 @@ void Graphics::loadPaletteAndColors() playerColors[i] = colors[i]; } //gray - neutralColor->r = 0x84; - neutralColor->g = 0x84; - neutralColor->b = 0x84; - neutralColor->a = SDL_ALPHA_OPAQUE; + neutralColor.r = 0x84; + neutralColor.g = 0x84; + neutralColor.b = 0x84; + neutralColor.a = SDL_ALPHA_OPAQUE; } void Graphics::initializeBattleGraphics() @@ -148,26 +146,20 @@ Graphics::Graphics() //(!) do not load any CAnimation here } -Graphics::~Graphics() -{ - delete[] playerColors; - delete neutralColor; - delete[] playerColorPalette; - delete[] neutralColorPalette; -} - void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) { if(sur->format->palette) { - SDL_Color * palette = nullptr; + SDL_Color palette[32]; if(player < PlayerColor::PLAYER_LIMIT) { - palette = playerColorPalette + 32*player.getNum(); + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); } else if(player == PlayerColor::NEUTRAL) { - palette = neutralColorPalette; + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(neutralColorPalette[i]); } else { @@ -176,7 +168,6 @@ void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) } //FIXME: not all player colored images have player palette at last 32 indexes //NOTE: following code is much more correct but still not perfect (bugged with status bar) - CSDL_Ext::setColors(sur, palette, 224, 32); diff --git a/client/render/Graphics.h b/client/render/Graphics.h index 50ee21be2..1cdcd8ddf 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -9,8 +9,8 @@ */ #pragma once -#include "IFont.h" #include "../lib/GameConstants.h" +#include "../lib/Color.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,13 +27,8 @@ class JsonNode; VCMI_LIB_NAMESPACE_END struct SDL_Surface; -struct SDL_Color; class CAnimation; - -enum EFonts : int -{ - FONT_BIG, FONT_CALLI, FONT_CREDITS, FONT_HIGH_SCORE, FONT_MEDIUM, FONT_SMALL, FONT_TIMES, FONT_TINY, FONT_VERD -}; +class IFont; /// Handles fonts, hero images, town images, various graphics class Graphics @@ -52,11 +47,14 @@ public: static const int FONTS_NUMBER = 9; std::array< std::shared_ptr, FONTS_NUMBER> fonts; + using PlayerPalette = std::array; + //various graphics - SDL_Color * playerColors; //array [8] - SDL_Color * neutralColor; - SDL_Color * playerColorPalette; //palette to make interface colors good - array of size [256] - SDL_Color * neutralColorPalette; + std::array playerColors; + std::array playerColorPalette; //palette to make interface colors good - array of size [256] + + PlayerPalette neutralColorPalette; + ColorRGBA neutralColor; std::map imageLists; @@ -67,7 +65,6 @@ public: //functions Graphics(); - ~Graphics(); void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player }; diff --git a/client/render/IFont.cpp b/client/render/IFont.cpp index 2151b542a..5cc60928b 100644 --- a/client/render/IFont.cpp +++ b/client/render/IFont.cpp @@ -25,24 +25,24 @@ size_t IFont::getStringWidth(const std::string & data) const return width; } -void IFont::renderTextLeft(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLeft(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { renderText(surface, data, color, pos); } -void IFont::renderTextRight(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextRight(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { Point size((int)getStringWidth(data), (int)getLineHeight()); renderText(surface, data, color, pos - size); } -void IFont::renderTextCenter(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextCenter(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { Point size((int)getStringWidth(data), (int)getLineHeight()); renderText(surface, data, color, pos - size / 2); } -void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; @@ -53,7 +53,7 @@ void IFont::renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; currPos.y -= (int)data.size() * (int)getLineHeight(); @@ -65,7 +65,7 @@ void IFont::renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const +void IFont::renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const { Point currPos = pos; currPos.y -= (int)data.size() * (int)getLineHeight() / 2; diff --git a/client/render/IFont.h b/client/render/IFont.h index 4a1b92863..80575de93 100644 --- a/client/render/IFont.h +++ b/client/render/IFont.h @@ -11,16 +11,16 @@ VCMI_LIB_NAMESPACE_BEGIN class Point; +class ColorRGBA; VCMI_LIB_NAMESPACE_END struct SDL_Surface; -struct SDL_Color; class IFont { protected: /// Internal function to render font, see renderTextLeft - virtual void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const = 0; + virtual void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const = 0; public: virtual ~IFont() @@ -40,17 +40,17 @@ public: * @param pos - position of rendered font */ /// pos = topleft corner of the text - void renderTextLeft(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextLeft(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = center of the text - void renderTextRight(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextRight(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = bottomright corner of the text - void renderTextCenter(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const; + void renderTextCenter(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const; /// pos = topleft corner of the text - void renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesLeft(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; /// pos = center of the text - void renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesRight(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; /// pos = bottomright corner of the text - void renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const SDL_Color & color, const Point & pos) const; + void renderTextLinesCenter(SDL_Surface * surface, const std::vector & data, const ColorRGBA & color, const Point & pos) const; }; diff --git a/client/render/IImage.h b/client/render/IImage.h index c7fde1706..bc9b37727 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -14,11 +14,11 @@ VCMI_LIB_NAMESPACE_BEGIN class PlayerColor; class Rect; class Point; +class ColorRGBA; VCMI_LIB_NAMESPACE_END struct SDL_Surface; -struct SDL_Color; class ColorFilter; /// Defines which blit method will be selected when image is used for rendering @@ -40,7 +40,7 @@ enum class EImageBlitMode : uint8_t class IImage { public: - using SpecialPalette = std::vector; + using SpecialPalette = std::vector; static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011; //draws image on surface "where" at position diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 0810b649b..10972e298 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -112,7 +112,7 @@ bool CBitmapFont::canRepresentString(const std::string & data) const return true; } -void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) const +void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const { Rect clipRect; CSDL_Ext::getClipRect(surface, clipRect); @@ -162,7 +162,7 @@ void CBitmapFont::renderCharacter(SDL_Surface * surface, const BitmapChar & char posX += character.rightOffset; } -void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void CBitmapFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { if (data.empty()) return; diff --git a/client/renderSDL/CBitmapFont.h b/client/renderSDL/CBitmapFont.h index 083841160..b1d5bc5b1 100644 --- a/client/renderSDL/CBitmapFont.h +++ b/client/renderSDL/CBitmapFont.h @@ -33,8 +33,8 @@ class CBitmapFont : public IFont void loadModFont(const std::string & modName, const ResourceID & resource); - void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const SDL_Color & color, int &posX, int &posY) const; - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const; + void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; public: explicit CBitmapFont(const std::string & filename); diff --git a/client/renderSDL/CBitmapHanFont.cpp b/client/renderSDL/CBitmapHanFont.cpp index 031a7c301..d97b2ca2b 100644 --- a/client/renderSDL/CBitmapHanFont.cpp +++ b/client/renderSDL/CBitmapHanFont.cpp @@ -35,7 +35,7 @@ size_t CBitmapHanFont::getCharacterIndex(ui8 first, ui8 second) const return (first - 0x81) * (12*16 - 2) + (second - 0x40); } -void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const +void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, const ColorRGBA & color, int &posX, int &posY) const { //TODO: somewhat duplicated with CBitmapFont::renderCharacter(); Rect clipRect; @@ -76,7 +76,7 @@ void CBitmapHanFont::renderCharacter(SDL_Surface * surface, int characterIndex, posX += (int)size + 1; } -void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { int posX = pos.x; int posY = pos.y; diff --git a/client/renderSDL/CBitmapHanFont.h b/client/renderSDL/CBitmapHanFont.h index 32b7e081f..707c360b2 100644 --- a/client/renderSDL/CBitmapHanFont.h +++ b/client/renderSDL/CBitmapHanFont.h @@ -30,8 +30,8 @@ class CBitmapHanFont : public IFont size_t getCharacterDataOffset(size_t index) const; size_t getCharacterIndex(ui8 first, ui8 second) const; - void renderCharacter(SDL_Surface * surface, int characterIndex, const SDL_Color & color, int &posX, int &posY) const; - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + void renderCharacter(SDL_Surface * surface, int characterIndex, const ColorRGBA & color, int &posX, int &posY) const; + void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; public: CBitmapHanFont(const JsonNode & config); diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index d13d0f5bc..47246d708 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -98,7 +98,7 @@ size_t CTrueTypeFont::getStringWidth(const std::string & data) const return width; } -void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const +void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const { if (fallbackFont && fallbackFont->canRepresentString(data)) { @@ -113,9 +113,9 @@ void CTrueTypeFont::renderText(SDL_Surface * surface, const std::string & data, { SDL_Surface * rendered; if (blended) - rendered = TTF_RenderUTF8_Blended(font.get(), data.c_str(), color); + rendered = TTF_RenderUTF8_Blended(font.get(), data.c_str(), CSDL_Ext::toSDL(color)); else - rendered = TTF_RenderUTF8_Solid(font.get(), data.c_str(), color); + rendered = TTF_RenderUTF8_Solid(font.get(), data.c_str(), CSDL_Ext::toSDL(color)); assert(rendered); diff --git a/client/renderSDL/CTrueTypeFont.h b/client/renderSDL/CTrueTypeFont.h index bd3fc4a53..804217a0b 100644 --- a/client/renderSDL/CTrueTypeFont.h +++ b/client/renderSDL/CTrueTypeFont.h @@ -32,7 +32,7 @@ class CTrueTypeFont : public IFont TTF_Font * loadFont(const JsonNode & config); int getFontStyle(const JsonNode & config); - void renderText(SDL_Surface * surface, const std::string & data, const SDL_Color & color, const Point & pos) const override; + void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; public: CTrueTypeFont(const JsonNode & fontConfig); ~CTrueTypeFont(); diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index 1161ee176..4a726a857 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -52,7 +52,7 @@ void CursorHardware::setImage(std::shared_ptr image, const Point & pivot { auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y); - CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY); + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); image->draw(cursorSurface); diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index 5b90709cf..d7e184c75 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -56,7 +56,7 @@ void CursorSoftware::updateTexture() if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) createTexture(cursorImage->dimensions()); - CSDL_Ext::fillSurface(cursorSurface, Colors::TRANSPARENCY); + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); cursorImage->draw(cursorSurface); SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch); diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index cc2a0d682..c04a1aa81 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -327,7 +327,7 @@ void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipM if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) continue; - palette->colors[i] = shifter.shiftColor(originalPalette->colors[i]); + palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); } } @@ -358,7 +358,7 @@ void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, for (size_t i = 0; i < last; ++i) { if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - surf->format->palette->colors[i] = specialPalette[i]; + surf->format->palette->colors[i] = CSDL_Ext::toSDL(specialPalette[i]); } } } diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index b362e137f..d9e6f1953 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -16,6 +16,8 @@ #include "../render/Colors.h" #include "../CMT.h" +#include "../../lib/GameConstants.h" + #include Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) @@ -506,16 +508,18 @@ void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &co drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); } -void CSDL_Ext::setPlayerColor(SDL_Surface * sur, PlayerColor player) +void CSDL_Ext::setPlayerColor(SDL_Surface * sur, const PlayerColor & player) { if(player==PlayerColor::UNFLAGGABLE) return; if(sur->format->BitsPerPixel==8) { - SDL_Color *color = (player == PlayerColor::NEUTRAL + ColorRGBA color = (player == PlayerColor::NEUTRAL ? graphics->neutralColor - : &graphics->playerColors[player.getNum()]); - CSDL_Ext::setColors(sur, color, 5, 1); + : graphics->playerColors[player.getNum()]); + + SDL_Color colorSDL = toSDL(color); + CSDL_Ext::setColors(sur, &colorSDL, 5, 1); } else logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); @@ -584,22 +588,6 @@ bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) return pixelTransparent || pixelCyan; } -void CSDL_Ext::VflipSurf(SDL_Surface * surf) -{ - char buf[4]; //buffer - int bpp = surf->format->BytesPerPixel; - for (int y=0; yh; ++y) - { - char * base = (char*)surf->pixels + y * surf->pitch; - for (int x=0; xw/2; ++x) - { - memcpy(buf, base + x * bpp, bpp); - memcpy(base + x * bpp, base + (surf->w - x - 1) * bpp, bpp); - memcpy(base + (surf->w - x - 1) * bpp, buf, bpp); - } - } -} - void CSDL_Ext::putPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) { uint8_t *p = getPxPtr(ekran, x, y); @@ -808,12 +796,6 @@ void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color SDL_FillRect(dst, &newRect, sdlColor); } -SDL_Color CSDL_Ext::makeColor(ui8 r, ui8 g, ui8 b, ui8 a) -{ - SDL_Color ret = {r, g, b, a}; - return ret; -} - STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) { return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); @@ -827,12 +809,12 @@ void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) { - setColorKey(surface, Colors::DEFAULT_KEY_COLOR); + setColorKey(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); } void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) { - uint32_t key = mapColor(surface, Colors::DEFAULT_KEY_COLOR); + uint32_t key = mapColor(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); auto & color = surface->format->palette->colors[key]; // set color key only if exactly such color was found diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index b5a6882cc..6239c82ed 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -9,7 +9,6 @@ */ #pragma once -#include "../../lib/GameConstants.h" #include "../../lib/Rect.h" #include "../../lib/Color.h" @@ -22,6 +21,7 @@ struct SDL_Color; VCMI_LIB_NAMESPACE_BEGIN +class PlayerColor; class Rect; class Point; @@ -79,21 +79,19 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface int blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface uint32_t colorTouint32_t(const SDL_Color * color); //little endian only - SDL_Color makeColor(ui8 r, ui8 g, ui8 b, ui8 a); void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2); void drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color); void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color & color, int depth = 1); void drawBorder(SDL_Surface * sur, const Rect & r, const SDL_Color & color, int depth = 1); - void setPlayerColor(SDL_Surface * sur, PlayerColor player); //sets correct color of flags; -1 for neutral + void setPlayerColor(SDL_Surface * sur, const PlayerColor & player); //sets correct color of flags; -1 for neutral SDL_Surface * newSurface(int w, int h, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given SDL_Surface * newSurface(int w, int h); //creates new surface, with flags/format same as in screen surface SDL_Surface * copySurface(SDL_Surface * mod); //returns copy of given surface template SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value - void VflipSurf(SDL_Surface * surf); //fluipis given surface by vertical axis //scale surface to required size. //nearest neighbour algorithm diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 1bfdf5908..38f4487ff 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -59,21 +59,9 @@ void CButton::update() redraw(); } -void CButton::setBorderColor(std::optional borderColor) +void CButton::setBorderColor(std::optional newBorderColor) { - setBorderColor(borderColor, borderColor, borderColor, borderColor); -} - -void CButton::setBorderColor(std::optional normalBorderColor, - std::optional pressedBorderColor, - std::optional blockedBorderColor, - std::optional highlightedBorderColor) -{ - stateToBorderColor[NORMAL] = normalBorderColor; - stateToBorderColor[PRESSED] = pressedBorderColor; - stateToBorderColor[BLOCKED] = blockedBorderColor; - stateToBorderColor[HIGHLIGHTED] = highlightedBorderColor; - update(); + borderColor = newBorderColor; } void CButton::addCallback(std::function callback) @@ -81,7 +69,7 @@ void CButton::addCallback(std::function callback) this->callback += callback; } -void CButton::addTextOverlay(const std::string & Text, EFonts font, SDL_Color color) +void CButton::addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); addOverlay(std::make_shared(pos.w/2, pos.h/2, font, ETextAlignment::CENTER, color, Text)); @@ -298,7 +286,6 @@ void CButton::showAll(Canvas & to) { CIntObject::showAll(to); - auto borderColor = stateToBorderColor[getState()]; if (borderColor) to.drawBorder(Rect::createAround(pos, 1), *borderColor); } diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 042d158d8..32c4ab3b9 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -10,11 +10,9 @@ #pragma once #include "../gui/CIntObject.h" -#include "../render/Colors.h" +#include "../render/EFont.h" #include "../../lib/FunctionList.h" -#include - VCMI_LIB_NAMESPACE_BEGIN class Rect; VCMI_LIB_NAMESPACE_END @@ -45,7 +43,7 @@ private: std::array stateToIndex; // mapping of button state to index of frame in animation std::array hoverTexts; //texts for statusbar, if empty - first entry will be used - std::array, 4> stateToBorderColor; // mapping of button state to border color + std::optional borderColor; // mapping of button state to border color std::string helpBox; //for right-click help std::shared_ptr image; //image for this button @@ -64,22 +62,15 @@ public: hoverable,//if true, button will be highlighted when hovered (e.g. main menu) soundDisabled; - // sets border color for each button state; - // if it's set, the button will have 1-px border around it with this color - void setBorderColor(std::optional normalBorderColor, - std::optional pressedBorderColor, - std::optional blockedBorderColor, - std::optional highlightedBorderColor); - // sets the same border color for all button states. - void setBorderColor(std::optional borderColor); + void setBorderColor(std::optional borderColor); /// adds one more callback to on-click actions void addCallback(std::function callback); /// adds overlay on top of button image. Only one overlay can be active at once void addOverlay(std::shared_ptr newOverlay); - void addTextOverlay(const std::string & Text, EFonts font, SDL_Color color = Colors::WHITE); + void addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color); void addImage(std::string filename); void addHoverText(ButtonState state, std::string text); diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 1dbba2257..c4d4dee7d 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -21,6 +21,8 @@ #include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../render/Canvas.h" +#include "../render/IFont.h" +#include "../render/Graphics.h" #include "../windows/CMessage.h" #include "../windows/InfoWindows.h" #include "../widgets/TextControls.h" diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 5e32b6fcd..7799d7b8f 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -10,6 +10,8 @@ #pragma once #include "../gui/CIntObject.h" +#include "../render/EFont.h" + VCMI_LIB_NAMESPACE_BEGIN diff --git a/client/widgets/Images.h b/client/widgets/Images.h index d11dbab45..bfc06f4e3 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -16,7 +16,6 @@ VCMI_LIB_NAMESPACE_BEGIN class Rect; VCMI_LIB_NAMESPACE_END -struct SDL_Color; class CAnimImage; class CLabel; class CAnimation; diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 56d31b643..303f7ff0c 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -16,7 +16,11 @@ VCMI_LIB_NAMESPACE_BEGIN class CGGarrison; struct InfoAboutArmy; +struct InfoAboutHero; +struct InfoAboutTown; class CArmedInstance; +class CGTownInstance; +class CGHeroInstance; class AFactionMember; VCMI_LIB_NAMESPACE_END diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 6246e9df3..b65e2f31e 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -17,6 +17,7 @@ #include "../gui/Shortcut.h" #include "../gui/CGuiHandler.h" #include "../render/Canvas.h" +#include "../render/Colors.h" void CSlider::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) { diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 62d7ddd5d..5401f0f64 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -20,6 +20,8 @@ #include "../adventureMap/CInGameConsole.h" #include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../../lib/TextOperations.h" @@ -44,7 +46,7 @@ void CLabel::showAll(Canvas & to) } -CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const SDL_Color & Color, const std::string & Text) +CLabel::CLabel(int x, int y, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : CTextContainer(Align, Font, Color), text(Text) { setRedrawParent(true); @@ -87,7 +89,7 @@ void CLabel::setText(const std::string & Txt) } } -void CLabel::setColor(const SDL_Color & Color) +void CLabel::setColor(const ColorRGBA & Color) { color = Color; if(autoRedraw) @@ -104,7 +106,7 @@ size_t CLabel::getWidth() return graphics->fonts[font]->getStringWidth(visibleText());; } -CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const SDL_Color & Color, const std::string & Text) : +CMultiLineLabel::CMultiLineLabel(Rect position, EFonts Font, ETextAlignment Align, const ColorRGBA & Color, const std::string & Text) : CLabel(position.x, position.y, Font, Align, Color, Text), visibleSize(0, 0, position.w, position.h) { @@ -193,7 +195,7 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) } while(begin++ != std::string::npos); } -CTextContainer::CTextContainer(ETextAlignment alignment, EFonts font, SDL_Color color) : +CTextContainer::CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color) : alignment(alignment), font(font), color(color) @@ -275,7 +277,7 @@ Rect CMultiLineLabel::getTextLocation() return Rect(); } -CLabelGroup::CLabelGroup(EFonts Font, ETextAlignment Align, const SDL_Color & Color) +CLabelGroup::CLabelGroup(EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : font(Font), align(Align), color(Color) { defActions = 255 - DISPOSE; @@ -292,7 +294,7 @@ size_t CLabelGroup::currentSize() const return labels.size(); } -CTextBox::CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font, ETextAlignment Align, const SDL_Color & Color) : +CTextBox::CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : sliderStyle(SliderStyle), slider(nullptr) { @@ -403,7 +405,7 @@ void CGStatusBar::clear() write({}); } -CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const SDL_Color & Color) +CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, ETextAlignment Align, const ColorRGBA & Color) : CLabel(background_->pos.x, background_->pos.y, Font, Align, Color, "") , enteringText(false) { diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index bf7cabcc3..bc67e8abf 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -12,11 +12,9 @@ #include "../gui/CIntObject.h" #include "../gui/TextAlignment.h" #include "../render/Colors.h" -#include "../render/Graphics.h" +#include "../render/EFont.h" #include "../../lib/FunctionList.h" -#include - class IImage; class CSlider; @@ -30,12 +28,12 @@ protected: /// do actual blitting of line. Text "what" will be placed at "where" and aligned according to alignment void blitLine(Canvas & to, Rect where, std::string what); - CTextContainer(ETextAlignment alignment, EFonts font, SDL_Color color); + CTextContainer(ETextAlignment alignment, EFonts font, ColorRGBA color); public: ETextAlignment alignment; EFonts font; - SDL_Color color; // default font color. Can be overridden by placing "{}" into the string + ColorRGBA color; // default font color. Can be overridden by placing "{}" into the string }; /// Label which shows text @@ -54,11 +52,11 @@ public: std::string getText(); virtual void setAutoRedraw(bool option); virtual void setText(const std::string & Txt); - virtual void setColor(const SDL_Color & Color); + virtual void setColor(const ColorRGBA & Color); size_t getWidth(); CLabel(int x = 0, int y = 0, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, - const SDL_Color & Color = Colors::WHITE, const std::string & Text = ""); + const ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); void showAll(Canvas & to) override; //shows statusbar (with current text) }; @@ -68,9 +66,9 @@ class CLabelGroup : public CIntObject std::vector> labels; EFonts font; ETextAlignment align; - SDL_Color color; + ColorRGBA color; public: - CLabelGroup(EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE); + CLabelGroup(EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE); void add(int x = 0, int y = 0, const std::string & text = ""); size_t currentSize() const; }; @@ -91,7 +89,7 @@ public: // total size of text, x = longest line of text, y = total height of lines Point textSize; - CMultiLineLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE, const std::string & Text = ""); + CMultiLineLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE, const std::string & Text = ""); void setText(const std::string & Txt) override; void showAll(Canvas & to) override; @@ -111,7 +109,7 @@ public: std::shared_ptr label; std::shared_ptr slider; - CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color & Color = Colors::WHITE); + CTextBox(std::string Text, const Rect & rect, int SliderStyle, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA & Color = Colors::WHITE); void resize(Point newSize); void setText(const std::string & Txt); @@ -125,7 +123,7 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const SDL_Color & Color = Colors::WHITE); + CGStatusBar(std::shared_ptr background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const ColorRGBA & Color = Colors::WHITE); CGStatusBar(int x, int y, std::string name, int maxw = -1); //make CLabel API private diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 6a6b51301..57d853932 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -12,6 +12,10 @@ #include "../widgets/CWindowWithArtifacts.h" #include "CWindowObject.h" +VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + class CButton; class CAnimImage; class CToggleGroup; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index bf897be96..c2eaf4251 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -24,6 +24,8 @@ #include "../render/CAnimation.h" #include "../render/IImage.h" #include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../renderSDL/SDL_Extensions.h" #include diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 84c6fa29d..22fae44b5 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../render/Graphics.h" +#include "../render/EFont.h" #include "../../lib/GameConstants.h" struct SDL_Surface; diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 363b85b01..4f3f6f942 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -30,7 +30,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) : CWindowObject(PLAYER_COLORED | BORDERED, "PUZZLE"), grailPos(GrailPos), - currentAlpha(SDL_ALPHA_OPAQUE) + currentAlpha(ColorRGBA::ALPHA_OPAQUE) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); diff --git a/client/windows/CQuestLog.h b/client/windows/CQuestLog.h index b8e9c597d..68f880e68 100644 --- a/client/windows/CQuestLog.h +++ b/client/windows/CQuestLog.h @@ -43,7 +43,7 @@ class CQuestLabel : public LRClickableAreaWText, public CMultiLineLabel public: std::function callback; - CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color &Color = Colors::WHITE, const std::string &Text = "") + CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA &Color = Colors::WHITE, const std::string &Text = "") : CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){}; void clickPressed(const Point & cursorPosition) override; void showAll(Canvas & to) override; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 25265aaea..0d5f248be 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -590,7 +590,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); } - SDL_Color firstLineColor, secondLineColor; + ColorRGBA firstLineColor, secondLineColor; if(spellCost > owner->myHero->mana) //hero cannot cast this spell { firstLineColor = Colors::WHITE; diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index abd7f95a8..9d041bdfc 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -19,6 +19,7 @@ class CSpell; VCMI_LIB_NAMESPACE_END class IImage; +class CAnimation; class CAnimImage; class CPicture; class CLabel; diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 157377aeb..16edc5808 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -18,6 +18,7 @@ VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; class CGDwelling; class IMarket; diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index caf9bfa03..e7911884a 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -15,6 +15,9 @@ VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; +class CGTownInstance; +class CGHeroInstance; class CGGarrison; class Rect; diff --git a/client/windows/QuickRecruitmentWindow.h b/client/windows/QuickRecruitmentWindow.h index cdb36e819..c8aba9fed 100644 --- a/client/windows/QuickRecruitmentWindow.h +++ b/client/windows/QuickRecruitmentWindow.h @@ -11,6 +11,10 @@ #include "../windows/CWindowObject.h" +VCMI_LIB_NAMESPACE_BEGIN +class CGTownInstance; +VCMI_LIB_NAMESPACE_END + class CButton; class CreatureCostBox; class CreaturePurchaseCard; From 69abfda9811a65b0087976215b75633ef3eecf8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 1 Aug 2023 08:23:09 +0200 Subject: [PATCH 0012/1248] stash --- lib/rmg/CMapGenOptions.cpp | 115 +++++++++++++++++++++++++++++++++++-- lib/rmg/CMapGenOptions.h | 1 + 2 files changed, 110 insertions(+), 6 deletions(-) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 04f3816af..374af7f8d 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -25,7 +25,7 @@ CMapGenOptions::CMapGenOptions() playerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr) { - resetPlayersMap(); + initPlayersMap(); setRoadEnabled(RoadId(Road::DIRT_ROAD), true); setRoadEnabled(RoadId(Road::GRAVEL_ROAD), true); setRoadEnabled(RoadId(Road::COBBLESTONE_ROAD), true); @@ -135,16 +135,15 @@ void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value monsterStrength = value; } -void CMapGenOptions::resetPlayersMap() +void CMapGenOptions::initPlayersMap() { - std::map rememberTownTypes; std::map rememberTeam; for(const auto & p : players) { auto town = p.second.getStartingTown(); - if (town != RANDOM_SIZE) + if (town != CPlayerSettings::RANDOM_TOWN) rememberTownTypes[p.first] = FactionID(town); rememberTeam[p.first] = p.second.getTeam(); } @@ -157,7 +156,6 @@ void CMapGenOptions::resetPlayersMap() if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); - //FIXME: what happens with human players here? for(int color = 0; color < totalPlayersLimit; ++color) { CPlayerSettings player; @@ -171,7 +169,112 @@ void CMapGenOptions::resetPlayersMap() else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) { - //FIXME: Allow humans to choose any color, even from the end of teh list + playerType = EPlayerType::COMP_ONLY; + } + player.setPlayerType(playerType); + player.setTeam(rememberTeam[pc]); + players[pc] = player; + + if (vstd::contains(rememberTownTypes, pc)) + players[pc].setStartingTown(rememberTownTypes[pc]); + } +} + +void CMapGenOptions::resetPlayersMap() +{ + //But do not update info about already made selections + std::map rememberTownTypes; + std::map rememberTeam; + + for(const auto & p : players) + { + auto town = p.second.getStartingTown(); + if (town != CPlayerSettings::RANDOM_TOWN) + rememberTownTypes[p.first] = FactionID(town); + rememberTeam[p.first] = p.second.getTeam(); + } + + //Remove players who have undefined properties + boost::remove_if(players, [](std::pair & player) + { + return player.second.getPlayerType() != EPlayerType::AI && player.second.getStartingTown() == CPlayerSettings::RANDOM_TOWN; + }); + + int realPlayersCnt = getPlayerCount(); + if (realPlayersCnt != RANDOM_SIZE) + { + //Trim the number of AI players, then CPU-only players, finally human players + auto eraseLastPlayer = [this](EPlayerType playerType) -> bool + { + for (auto it = players.rbegin(); it != players.rend(); ++it) + { + if (it->second.getPlayerType() == playerType) + { + players.erase(it->first); + return true; + } + } + return false; + }; + + while (players.size() < realPlayersCnt) + { + if (eraseLastPlayer(EPlayerType::AI)) + continue; + if (eraseLastPlayer(EPlayerType::COMP_ONLY)) + continue; + if (eraseLastPlayer(EPlayerType::HUMAN)) + continue; + } + } + + int realCompOnlyPlayersCnt = (compOnlyPlayerCount == RANDOM_SIZE) ? (PlayerColor::PLAYER_LIMIT_I - realPlayersCnt) : compOnlyPlayerCount; + int totalPlayersLimit = realPlayersCnt + realCompOnlyPlayersCnt; + if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) + totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); + + //First colors from the list are assigned to human players, then to CPU players + //FIXME: Assign human players colors first + + //TODO: Where is player type is set in void CVCMIServer::updateAndPropagateLobbyState() + //in ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby + //CPackForLobby + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; ++color) + { + availableColors.push_back(PlayerColor(color)); + } + + auto removeUsedColors = [this, &availableColors](EPlayerType playerType) + { + for (auto& player : players) + { + if (player.second.getPlayerType() == playerType) + { + vstd::erase(availableColors, player.second.getColor()); + } + } + }; + removeUsedColors(EPlayerType::HUMAN); + removeUsedColors(EPlayerType::COMP_ONLY); + //removeUsedColors(EPlayerType::AI); + + //TODO: Assign the remaining colors to random players (AI players) + + for(int color = 0; color < totalPlayersLimit; ++color) + { + CPlayerSettings player; + auto pc = PlayerColor(color); + player.setColor(pc); + auto playerType = EPlayerType::AI; + if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) + { + playerType = EPlayerType::HUMAN; + } + else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) + || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) + { + //FIXME: Allow humans to choose any color, even from the end of the list playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index aa5683ede..65b089722 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -143,6 +143,7 @@ public: static const si8 RANDOM_SIZE = -1; private: + void initPlayersMap(); void resetPlayersMap(); int countHumanPlayers() const; int countCompOnlyPlayers() const; From ad8695ac91a71193c7cd423cdc6d735c0803daf8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 4 Aug 2023 15:19:09 +0300 Subject: [PATCH 0013/1248] Fix saving games - regression from filesystem changes --- lib/filesystem/CFilesystemLoader.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 74c3b3da5..f7266f5f2 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -86,7 +86,7 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) if (!update) { // create file, if not exists - std::fstream file((baseDirectory / filename).c_str()); + std::ofstream file((baseDirectory / filename).c_str(), std::ofstream::binary); if (!file.is_open()) return false; From a9e895c08f19f12a912ad6176c7f486e57aa0ce9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 6 Aug 2023 21:39:39 +0200 Subject: [PATCH 0014/1248] basic subdirectory support --- lib/filesystem/CFilesystemLoader.cpp | 4 ++++ lib/filesystem/Filesystem.cpp | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index f7266f5f2..d56f792be 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -85,6 +85,10 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) if (!update) { + // create folders if not exists + boost::filesystem::path p((baseDirectory / filename).c_str()); + boost::filesystem::create_directories(p.parent_path()); + // create file, if not exists std::ofstream file((baseDirectory / filename).c_str(), std::ofstream::binary); diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index cf9bc4f2e..da8bba83e 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -147,7 +147,7 @@ ISimpleResourceLoader * CResourceHandler::createInitial() for (auto & path : VCMIDirs::get().dataPaths()) { if (boost::filesystem::is_directory(path)) // some of system-provided paths may not exist - initialLoader->addLoader(new CFilesystemLoader("", path, 0, true), false); + initialLoader->addLoader(new CFilesystemLoader("", path, 1, true), false); } initialLoader->addLoader(new CFilesystemLoader("", VCMIDirs::get().userDataPath(), 0, true), false); From e5627b4bb7d144a1e0e5e7a97272b0dc1e9b5676 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 7 Aug 2023 00:10:06 +0200 Subject: [PATCH 0015/1248] tests --- client/lobby/SelectionTab.cpp | 69 +++++++++++++++++++++++++++++++---- client/lobby/SelectionTab.h | 5 ++- 2 files changed, 66 insertions(+), 8 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a7e698e33..3a9374c86 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -148,7 +148,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) int sizes[] = {36, 72, 108, 144, 0}; const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; for(int i = 0; i < 5; i++) - buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); + buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true, ""))); int xpos[] = {23, 55, 88, 121, 306, 339}; const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; @@ -214,6 +214,7 @@ void SelectionTab::toggleMode() { allItems.clear(); curItems.clear(); + curFolders.clear(); if(slider) slider->block(true); } @@ -328,7 +329,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(py >= curItems.size()) return; - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); + std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); @@ -337,9 +338,13 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) // A new size filter (Small, Medium, ...) has been selected. Populate // selMaps with the relevant data. -void SelectionTab::filter(int size, bool selectFirst) +void SelectionTab::filter(int size, bool selectFirst, std::string path) { + path = "AA"; + + curItems.clear(); + curFolders.clear(); if(tabType == ESelectionScreen::campaignList) { @@ -351,7 +356,37 @@ void SelectionTab::filter(int size, bool selectFirst) for(auto elem : allItems) { if(elem->mapHeader && (!size || elem->mapHeader->width == size)) + { + std::string folder = boost::filesystem::path(elem->fileURI).parent_path().string(); + std::vector filetree; + + // delete first element (e.g. 'MAPS') + boost::split(filetree, folder, boost::is_any_of("/")); + filetree.erase(filetree.begin()); + folder = boost::algorithm::join(filetree, "/"); + + // remove current dir + if(boost::algorithm::starts_with(folder, path)) + { + folder = folder.substr(path.size()); + if(boost::algorithm::starts_with(folder, "/")) + folder = folder.substr(1); + + if (std::find(curFolders.begin(), curFolders.end(), "..") == curFolders.end()) { + curFolders.push_back(".."); + } + } + + if(folder != "") + { + boost::split(filetree, folder, boost::is_any_of("/")); + if (std::find(curFolders.begin(), curFolders.end(), filetree[0]) == curFolders.end()) { + curFolders.push_back(filetree[0]); + } + } + curItems.push_back(elem); + } } } @@ -406,7 +441,7 @@ void SelectionTab::sort() void SelectionTab::select(int position) { - if(!curItems.size()) + if(!(curFolders.size() + curItems.size())) return; // New selection. py is the index in curItems. @@ -456,9 +491,14 @@ void SelectionTab::updateListItems() int elemIdx = slider->getValue(); for(auto item : listItems) { - if(elemIdx < curItems.size()) + if(elemIdx < curFolders.size()) { - item->updateItem(curItems[elemIdx], elemIdx == selectionPos); + item->updateItem(curFolders[elemIdx], elemIdx == selectionPos); + elemIdx++; + } + else if(elemIdx - curFolders.size() < curItems.size()) + { + item->updateItem(curItems[elemIdx - curFolders.size()], elemIdx - curFolders.size() == selectionPos); elemIdx++; } else @@ -517,7 +557,7 @@ void SelectionTab::selectFileName(std::string fname) std::shared_ptr SelectionTab::getSelectedMapInfo() const { - return curItems.empty() ? nullptr : curItems[selectionPos]; + return curItems.empty() && selectionPos <= curFolders.size() ? nullptr : curItems[selectionPos - curFolders.size()]; } void SelectionTab::rememberCurrentSelection() @@ -682,6 +722,21 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); } +void SelectionTab::ListItem::updateItem(std::string folderName, bool selected) +{ + labelAmountOfPlayers->disable(); + labelMapSizeLetter->disable(); + iconFormat->disable(); + iconVictoryCondition->disable(); + iconLossCondition->disable(); + labelNumberOfCampaignMaps->disable(); + labelName->enable(); + labelName->setText(folderName); + auto color = selected ? Colors::YELLOW : Colors::WHITE; + labelName->setColor(color); + return; +} + void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) { if(!info) diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 74bc41233..2ee167af0 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -41,6 +41,7 @@ class SelectionTab : public CIntObject std::shared_ptr labelName; ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); + void updateItem(std::string folderName, bool selected = false); void updateItem(std::shared_ptr info = {}, bool selected = false); }; std::vector> listItems; @@ -53,6 +54,8 @@ class SelectionTab : public CIntObject public: std::vector> allItems; std::vector> curItems; + std::vector curFolders; + std::string curFolder; size_t selectionPos; std::function)> callOnSelect; @@ -71,7 +74,7 @@ public: void showPopupWindow(const Point & cursorPosition) override; bool receiveEvent(const Point & position, int eventType) const override; - void filter(int size, bool selectFirst = false); //0 - all + void filter(int size, bool selectFirst = false, std::string path = "MAPS"); //0 - all void sortBy(int criteria); void sort(); void select(int position); //position: <0 - positions> position on the screen From 5e1f1294e5b3b4212d76a12929284b429d42df28 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 8 Aug 2023 00:38:13 +0200 Subject: [PATCH 0016/1248] rework --- client/lobby/SelectionTab.cpp | 147 ++++++++++++++++++++-------------- client/lobby/SelectionTab.h | 28 ++++--- 2 files changed, 106 insertions(+), 69 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 3a9374c86..2d14ccd29 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -41,8 +41,11 @@ #include "../../lib/mapping/MapFormat.h" #include "../../lib/serializer/Connection.h" -bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) +bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { + if(aaa->isFolder || bbb->isFolder) + return (aaa->isFolder > bbb->isFolder); + auto a = aaa->mapHeader.get(); auto b = bbb->mapHeader.get(); if(a && b) //if we are sorting scenarios @@ -130,7 +133,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20} + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder("") { OBJ_CONSTRUCTION; @@ -148,7 +151,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) int sizes[] = {36, 72, 108, 144, 0}; const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; for(int i = 0; i < 5; i++) - buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true, ""))); + buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); int xpos[] = {23, 55, 88, 121, 306, 339}; const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; @@ -214,7 +217,6 @@ void SelectionTab::toggleMode() { allItems.clear(); curItems.clear(); - curFolders.clear(); if(slider) slider->block(true); } @@ -336,15 +338,44 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) CRClickPopup::createAndPush(text); } +std::tuple SelectionTab::checkSubfolder(std::string path) +{ + std::string folderName = ""; + bool parentExists = false; + + std::string folder = boost::filesystem::path(path).parent_path().string(); + std::vector filetree; + + // delete first element (e.g. 'MAPS') + boost::split(filetree, folder, boost::is_any_of("/")); + filetree.erase(filetree.begin()); + folder = boost::algorithm::join(filetree, "/"); + + if(boost::algorithm::starts_with(folder, curFolder) && curFolder != "") + { + folder = folder.substr(curFolder.size()); + if(boost::algorithm::starts_with(folder, "/")) + folder = folder.substr(1); + + parentExists = true; + } + + if(folder != "") + { + boost::split(filetree, folder, boost::is_any_of("/")); + folderName = filetree[0]; + } + + return {folderName, parentExists}; +} + // A new size filter (Small, Medium, ...) has been selected. Populate // selMaps with the relevant data. -void SelectionTab::filter(int size, bool selectFirst, std::string path) +void SelectionTab::filter(int size, bool selectFirst) { - path = "AA"; - + std::string path = ""; curItems.clear(); - curFolders.clear(); if(tabType == ESelectionScreen::campaignList) { @@ -357,32 +388,23 @@ void SelectionTab::filter(int size, bool selectFirst, std::string path) { if(elem->mapHeader && (!size || elem->mapHeader->width == size)) { - std::string folder = boost::filesystem::path(elem->fileURI).parent_path().string(); - std::vector filetree; + auto [folderName, parentExists] = checkSubfolder(elem->fileURI); - // delete first element (e.g. 'MAPS') - boost::split(filetree, folder, boost::is_any_of("/")); - filetree.erase(filetree.begin()); - folder = boost::algorithm::join(filetree, "/"); - - // remove current dir - if(boost::algorithm::starts_with(folder, path)) + if(parentExists) { - folder = folder.substr(path.size()); - if(boost::algorithm::starts_with(folder, "/")) - folder = folder.substr(1); - - if (std::find(curFolders.begin(), curFolders.end(), "..") == curFolders.end()) { - curFolders.push_back(".."); - } + auto folder = std::make_shared(); + folder->isFolder = true; + folder->folderName = ".."; + if (std::find(curItems.begin(), curItems.end(), folder) == curItems.end()) { + curItems.push_back(folder); + } } - if(folder != "") - { - boost::split(filetree, folder, boost::is_any_of("/")); - if (std::find(curFolders.begin(), curFolders.end(), filetree[0]) == curFolders.end()) { - curFolders.push_back(filetree[0]); - } + auto folder = std::make_shared(); + folder->isFolder = true; + folder->folderName = folderName; + if (std::find(curItems.begin(), curItems.end(), folder) == curItems.end() && folderName != "") { + curItems.push_back(folder); } curItems.push_back(elem); @@ -441,7 +463,7 @@ void SelectionTab::sort() void SelectionTab::select(int position) { - if(!(curFolders.size() + curItems.size())) + if(!curItems.size()) return; // New selection. py is the index in curItems. @@ -458,6 +480,11 @@ void SelectionTab::select(int position) rememberCurrentSelection(); + if(curItems[py]->isFolder) { + return; + //TODO + } + if(inputName && inputName->isActive()) { auto filename = *CResourceHandler::get("local")->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); @@ -491,14 +518,9 @@ void SelectionTab::updateListItems() int elemIdx = slider->getValue(); for(auto item : listItems) { - if(elemIdx < curFolders.size()) + if(elemIdx < curItems.size()) { - item->updateItem(curFolders[elemIdx], elemIdx == selectionPos); - elemIdx++; - } - else if(elemIdx - curFolders.size() < curItems.size()) - { - item->updateItem(curItems[elemIdx - curFolders.size()], elemIdx - curFolders.size() == selectionPos); + item->updateItem(curItems[elemIdx], elemIdx == selectionPos); elemIdx++; } else @@ -555,13 +577,16 @@ void SelectionTab::selectFileName(std::string fname) selectAbs(0); } -std::shared_ptr SelectionTab::getSelectedMapInfo() const +std::shared_ptr SelectionTab::getSelectedMapInfo() const { - return curItems.empty() && selectionPos <= curFolders.size() ? nullptr : curItems[selectionPos - curFolders.size()]; + return curItems.empty() ? nullptr : curItems[selectionPos]; } void SelectionTab::rememberCurrentSelection() { + if(getSelectedMapInfo()->isFolder) + return; + // TODO: this can be more elegant if(tabType == ESelectionScreen::newGame) { @@ -624,11 +649,13 @@ void SelectionTab::parseMaps(const std::unordered_set & files) { try { - auto mapInfo = std::make_shared(); + auto mapInfo = std::make_shared(); mapInfo->mapInit(file.getName()); if (isMapSupported(*mapInfo)) + { allItems.push_back(mapInfo); + } } catch(std::exception & e) { @@ -643,7 +670,7 @@ void SelectionTab::parseSaves(const std::unordered_set & files) { try { - auto mapInfo = std::make_shared(); + auto mapInfo = std::make_shared(); mapInfo->saveInit(file); // Filter out other game modes @@ -679,12 +706,13 @@ void SelectionTab::parseCampaigns(const std::unordered_set & files) allItems.reserve(files.size()); for(auto & file : files) { - auto info = std::make_shared(); + auto info = std::make_shared(); //allItems[i].date = std::asctime(std::localtime(&files[i].date)); info->fileURI = file.getName(); info->campaignInit(); - if(info->campaign) + if(info->campaign) { allItems.push_back(info); + } } } @@ -722,22 +750,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); } -void SelectionTab::ListItem::updateItem(std::string folderName, bool selected) -{ - labelAmountOfPlayers->disable(); - labelMapSizeLetter->disable(); - iconFormat->disable(); - iconVictoryCondition->disable(); - iconLossCondition->disable(); - labelNumberOfCampaignMaps->disable(); - labelName->enable(); - labelName->setText(folderName); - auto color = selected ? Colors::YELLOW : Colors::WHITE; - labelName->setColor(color); - return; -} - -void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) +void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool selected) { if(!info) { @@ -752,6 +765,20 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool sel } auto color = selected ? Colors::YELLOW : Colors::WHITE; + if(info->isFolder) + { + labelAmountOfPlayers->disable(); + labelMapSizeLetter->disable(); + iconFormat->disable(); + iconVictoryCondition->disable(); + iconLossCondition->disable(); + labelNumberOfCampaignMaps->disable(); + labelName->enable(); + labelName->setText(info->folderName); + labelName->setColor(color); + return; + } + if(info->campaign) { labelAmountOfPlayers->disable(); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 2ee167af0..f4118508f 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -10,6 +10,7 @@ #pragma once #include "CSelectionBase.h" +#include "../../lib/mapping/CMapInfo.h" class CSlider; class CLabel; @@ -19,12 +20,21 @@ enum ESortBy _playerAm, _size, _format, _name, _viccon, _loscon, _numOfMaps, _fileName }; //_numOfMaps is for campaigns +class ElementInfo : public CMapInfo +{ +public: + ElementInfo() : CMapInfo() { } + ~ElementInfo() { } + std::string folderName = ""; + bool isFolder = false; +}; + /// Class which handles map sorting by different criteria class mapSorter { public: ESortBy sortBy; - bool operator()(const std::shared_ptr aaa, const std::shared_ptr bbb); + bool operator()(const std::shared_ptr aaa, const std::shared_ptr bbb); mapSorter(ESortBy es) : sortBy(es){}; }; @@ -41,8 +51,7 @@ class SelectionTab : public CIntObject std::shared_ptr labelName; ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); - void updateItem(std::string folderName, bool selected = false); - void updateItem(std::shared_ptr info = {}, bool selected = false); + void updateItem(std::shared_ptr info = {}, bool selected = false); }; std::vector> listItems; @@ -52,12 +61,11 @@ class SelectionTab : public CIntObject std::shared_ptr iconsLossCondition; public: - std::vector> allItems; - std::vector> curItems; - std::vector curFolders; + std::vector> allItems; + std::vector> curItems; std::string curFolder; size_t selectionPos; - std::function)> callOnSelect; + std::function)> callOnSelect; ESortBy sortingBy; ESortBy generalSortingBy; @@ -74,7 +82,7 @@ public: void showPopupWindow(const Point & cursorPosition) override; bool receiveEvent(const Point & position, int eventType) const override; - void filter(int size, bool selectFirst = false, std::string path = "MAPS"); //0 - all + void filter(int size, bool selectFirst = false); //0 - all void sortBy(int criteria); void sort(); void select(int position); //position: <0 - positions> position on the screen @@ -84,7 +92,7 @@ public: int getLine() const; int getLine(const Point & position) const; void selectFileName(std::string fname); - std::shared_ptr getSelectedMapInfo() const; + std::shared_ptr getSelectedMapInfo() const; void rememberCurrentSelection(); void restoreLastSelection(); @@ -98,6 +106,8 @@ private: ESelectionScreen tabType; Rect inputNameRect; + std::tuple checkSubfolder(std::string path); + bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); void parseSaves(const std::unordered_set & files); From 8b8006c0e0e795d8f846be1542d18e190b055223 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:04:06 +0200 Subject: [PATCH 0017/1248] bring it to work... --- client/lobby/SelectionTab.cpp | 80 ++++++++++++++++++++++++----------- client/lobby/SelectionTab.h | 2 +- 2 files changed, 56 insertions(+), 26 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 2d14ccd29..af6f62b6a 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -44,7 +44,12 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) { if(aaa->isFolder || bbb->isFolder) - return (aaa->isFolder > bbb->isFolder); + { + if(aaa->isFolder != bbb->isFolder) + return (aaa->isFolder > bbb->isFolder); + else + return boost::ilexicographical_compare(aaa->folderName, bbb->folderName); + } auto a = aaa->mapHeader.get(); auto b = bbb->mapHeader.get(); @@ -316,6 +321,15 @@ void SelectionTab::keyPressed(EShortcut key) void SelectionTab::clickDouble(const Point & cursorPosition) { + int position = getLine(); + int py = position + slider->getValue(); + + if(py >= curItems.size()) + return; + + if(curItems[py]->isFolder) + return; + if(getLine() != -1) //double clicked scenarios list { (static_cast(parent))->buttonStart->clickPressed(cursorPosition); @@ -338,35 +352,38 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) CRClickPopup::createAndPush(text); } -std::tuple SelectionTab::checkSubfolder(std::string path) +std::tuple SelectionTab::checkSubfolder(std::string path) { std::string folderName = ""; - bool parentExists = false; + bool parentExists = (curFolder != ""); + bool fileInFolder = false; - std::string folder = boost::filesystem::path(path).parent_path().string(); std::vector filetree; - // delete first element (e.g. 'MAPS') - boost::split(filetree, folder, boost::is_any_of("/")); + boost::split(filetree, path, boost::is_any_of("/")); filetree.erase(filetree.begin()); - folder = boost::algorithm::join(filetree, "/"); + std::string pathWithoutPrefix = boost::algorithm::join(filetree, "/"); - if(boost::algorithm::starts_with(folder, curFolder) && curFolder != "") + std::string folder = boost::filesystem::path(pathWithoutPrefix).parent_path().string(); + + if(boost::algorithm::starts_with(folder, curFolder)) { folder = folder.substr(curFolder.size()); if(boost::algorithm::starts_with(folder, "/")) folder = folder.substr(1); - - parentExists = true; + + if(folder != "") + { + boost::split(filetree, folder, boost::is_any_of("/")); + folderName = filetree[0]; + } } - if(folder != "") - { - boost::split(filetree, folder, boost::is_any_of("/")); - folderName = filetree[0]; - } + if(boost::algorithm::starts_with(pathWithoutPrefix, curFolder)) + if(boost::count(pathWithoutPrefix.substr(curFolder.size()), '/') == 0) + fileInFolder = true; - return {folderName, parentExists}; + return {folderName, parentExists, fileInFolder}; } // A new size filter (Small, Medium, ...) has been selected. Populate @@ -388,26 +405,27 @@ void SelectionTab::filter(int size, bool selectFirst) { if(elem->mapHeader && (!size || elem->mapHeader->width == size)) { - auto [folderName, parentExists] = checkSubfolder(elem->fileURI); + auto [folderName, parentExists, fileInFolder] = checkSubfolder(elem->fileURI); if(parentExists) { auto folder = std::make_shared(); folder->isFolder = true; folder->folderName = ".."; - if (std::find(curItems.begin(), curItems.end(), folder) == curItems.end()) { + if (boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }) == curItems.end()) { curItems.push_back(folder); } } - auto folder = std::make_shared(); + std::shared_ptr folder = std::make_shared(); folder->isFolder = true; folder->folderName = folderName; - if (std::find(curItems.begin(), curItems.end(), folder) == curItems.end() && folderName != "") { + if (boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }) == curItems.end() && folderName != "") { curItems.push_back(folder); } - curItems.push_back(elem); + if(fileInFolder) + curItems.push_back(elem); } } } @@ -421,7 +439,7 @@ void SelectionTab::filter(int size, bool selectFirst) { slider->scrollTo(0); callOnSelect(curItems[0]); - selectAbs(0); + selectAbs(-1); } } else @@ -445,7 +463,7 @@ void SelectionTab::sortBy(int criteria) } sort(); - selectAbs(0); + selectAbs(-1); } void SelectionTab::sort() @@ -481,8 +499,18 @@ void SelectionTab::select(int position) rememberCurrentSelection(); if(curItems[py]->isFolder) { + if(curItems[py]->folderName == "..") + { + std::vector filetree; + boost::split(filetree, curFolder, boost::is_any_of("/")); + filetree.erase(filetree.end()); + filetree.erase(filetree.end()); + curFolder = filetree.size() > 0 ? boost::algorithm::join(filetree, "/") + "/" : ""; + } + else + curFolder += curItems[py]->folderName + "/"; + filter(0); return; - //TODO } if(inputName && inputName->isActive()) @@ -499,6 +527,8 @@ void SelectionTab::select(int position) void SelectionTab::selectAbs(int position) { + if(position == -1) + position = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); select(position - slider->getValue()); } @@ -574,7 +604,7 @@ void SelectionTab::selectFileName(std::string fname) } } - selectAbs(0); + selectAbs(-1); } std::shared_ptr SelectionTab::getSelectedMapInfo() const diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index f4118508f..fe5eef787 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -106,7 +106,7 @@ private: ESelectionScreen tabType; Rect inputNameRect; - std::tuple checkSubfolder(std::string path); + std::tuple checkSubfolder(std::string path); bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); From 855b94b3163bc574d270b8b965b7094a03cd9765 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:30:28 +0200 Subject: [PATCH 0018/1248] fix --- client/lobby/SelectionTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index af6f62b6a..198678822 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -345,7 +345,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(py >= curItems.size()) return; - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); + std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); From bbe04c747d36155e748714e1a72e42928ec9e115 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 8 Aug 2023 23:46:30 +0200 Subject: [PATCH 0019/1248] Folder Icon, Save, Folder on top... --- Mods/vcmi/Sprites/ScSelC.json | 3 +- Mods/vcmi/Sprites/mapFormatIcons/folder.png | Bin 0 -> 1191 bytes client/lobby/CSavingScreen.cpp | 2 +- client/lobby/SelectionTab.cpp | 90 ++++++++++---------- client/lobby/SelectionTab.h | 2 +- 5 files changed, 49 insertions(+), 48 deletions(-) create mode 100644 Mods/vcmi/Sprites/mapFormatIcons/folder.png diff --git a/Mods/vcmi/Sprites/ScSelC.json b/Mods/vcmi/Sprites/ScSelC.json index 7d8279cc3..499e5740e 100644 --- a/Mods/vcmi/Sprites/ScSelC.json +++ b/Mods/vcmi/Sprites/ScSelC.json @@ -2,6 +2,7 @@ "basepath" : "mapFormatIcons/", "images" : [ - { "frame" : 3, "file" : "vcmi1.png"} + { "frame" : 3, "file" : "vcmi1.png"}, + { "frame" : 99, "file" : "folder.png"} ] } \ No newline at end of file diff --git a/Mods/vcmi/Sprites/mapFormatIcons/folder.png b/Mods/vcmi/Sprites/mapFormatIcons/folder.png new file mode 100644 index 0000000000000000000000000000000000000000..44547ef38e4c9120a5d417848a05d666de28651d GIT binary patch literal 1191 zcmV;Y1X%ltP)EX>4Tx04R}tkv&MmKp2MKwn~dsMC~BrkfAzR@DFj+DionYs1;guFuC*(nlvOS zE{=k0!NH%!s)LKOt`4q(Aov5~>f)s6A|-y86k5c1$8itueecWNcYwcMW~$jS2B?~4 zq!Mu_Vdh$kxtDMM~H<&8_R9XiiS!&MI2RBjq?2& zmle)ioYiubHSft^7|v-c%Uq{5gaj6`1Q7ycR8c}17Gkt(q?kz2dECQ4==eo)$>b`5 zkz)ZBsE`~#_#gc4t(l*kaFfDup!3DHKZb$eF3_mi_V=-EH%@@SGjOG~{FOQ|^GSNG zrA3c`o^9abx~0i`z~v4w@T5zI^3A z0wV>=UiWx+sJ(Ch)-?O~18bskq%TurW&i*H24YJ`L;(K){{a7>y{D4^00PTNL_t(Y z$K{s2Yh+~CJTpH(&+P0h?RFax!8wPjvc0{1 z=UOfBK3E8&;3@etfN_WzYlg!CBBMe&=guHPsN{>USLQl{+4$j4UwZi*Z8!s-7y~gL z;Nv$q=RiQRQQz$RM2=pkkE(I!T@#`_;|2KY&9@(aWVDJ4rOk+jdP*WTgw z?f3r{Rwfy(EQW&b?rtnx6#Pnn7$a&0of`{OWuLPArIEz4>;|pYD`ZYirjen=7!iha zI=4W;WqCuDD@kj_Wyezigb)!aXm@Uc7|u-&5aW<6n;|BTLyo4BVM-AwXyd<&nmlIb)mGQX81`tI3g~DEOH4h< zPKaTMsz=phdi5U4+>m0}hZKLEW(J5MfO$eZq8#>4v!2|X8&LV95Y+mO0nAL37PzcK z^v^JhjW?Z9Q`b*@mh*dKfKeMxcB z+?{IIYf6;mN&C^U95w~$6h-lE|MC3~+8v+&R{haDdxx1?eE86R|LG?W4!B^2Jnukn z0gJ%GMB;zmQ3wOzXP|0wArT-u4KCLJuv3U|QTG1{`x5|ez>U-So4x=5002ovPDHLk FV1l)pH-Z2F literal 0 HcmV?d00001 diff --git a/client/lobby/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp index 562643a92..1126fb444 100644 --- a/client/lobby/CSavingScreen.cpp +++ b/client/lobby/CSavingScreen.cpp @@ -70,7 +70,7 @@ void CSavingScreen::saveGame() if(!(tabSel && tabSel->inputName && tabSel->inputName->getText().size())) return; - std::string path = "Saves/" + tabSel->inputName->getText(); + std::string path = "Saves/" + tabSel->curFolder + tabSel->inputName->getText(); auto overWrite = [this, path]() -> void { diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 198678822..cc0aceb4b 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -345,14 +345,17 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(py >= curItems.size()) return; - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); - if(curItems[py]->date != "") - text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); + if(!curItems[py]->isFolder) + { + std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); + if(curItems[py]->date != "") + text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - CRClickPopup::createAndPush(text); + CRClickPopup::createAndPush(text); + } } -std::tuple SelectionTab::checkSubfolder(std::string path) +std::tuple SelectionTab::checkSubfolder(std::string path) { std::string folderName = ""; bool parentExists = (curFolder != ""); @@ -364,13 +367,11 @@ std::tuple SelectionTab::checkSubfolder(std::string pat filetree.erase(filetree.begin()); std::string pathWithoutPrefix = boost::algorithm::join(filetree, "/"); - std::string folder = boost::filesystem::path(pathWithoutPrefix).parent_path().string(); + std::string baseFolder = boost::filesystem::path(pathWithoutPrefix).parent_path().string(); - if(boost::algorithm::starts_with(folder, curFolder)) + if(boost::algorithm::starts_with(baseFolder, curFolder)) { - folder = folder.substr(curFolder.size()); - if(boost::algorithm::starts_with(folder, "/")) - folder = folder.substr(1); + std::string folder = baseFolder.substr(curFolder.size()); if(folder != "") { @@ -383,50 +384,40 @@ std::tuple SelectionTab::checkSubfolder(std::string pat if(boost::count(pathWithoutPrefix.substr(curFolder.size()), '/') == 0) fileInFolder = true; - return {folderName, parentExists, fileInFolder}; + return {folderName, baseFolder, parentExists, fileInFolder}; } // A new size filter (Small, Medium, ...) has been selected. Populate // selMaps with the relevant data. void SelectionTab::filter(int size, bool selectFirst) { - std::string path = ""; - curItems.clear(); - if(tabType == ESelectionScreen::campaignList) + for(auto elem : allItems) { - for(auto elem : allItems) - curItems.push_back(elem); - } - else - { - for(auto elem : allItems) + if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { - if(elem->mapHeader && (!size || elem->mapHeader->width == size)) + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->fileURI); + + if(parentExists) { - auto [folderName, parentExists, fileInFolder] = checkSubfolder(elem->fileURI); - - if(parentExists) - { - auto folder = std::make_shared(); - folder->isFolder = true; - folder->folderName = ".."; - if (boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }) == curItems.end()) { - curItems.push_back(folder); - } - } - - std::shared_ptr folder = std::make_shared(); + auto folder = std::make_shared(); folder->isFolder = true; - folder->folderName = folderName; - if (boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }) == curItems.end() && folderName != "") { + folder->folderName = ".."; + if (boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }) == curItems.end()) { curItems.push_back(folder); - } - - if(fileInFolder) - curItems.push_back(elem); + } } + + std::shared_ptr folder = std::make_shared(); + folder->isFolder = true; + folder->folderName = folderName; + if (boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }) == curItems.end() && folderName != "") { + curItems.push_back(folder); + } + + if(fileInFolder) + curItems.push_back(elem); } } @@ -472,8 +463,9 @@ void SelectionTab::sort() std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); + int firstMap = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); if(!sortModeAscending) - std::reverse(curItems.begin(), curItems.end()); + std::reverse(std::next(curItems.begin(), firstMap), curItems.end()); updateListItems(); redraw(); @@ -496,8 +488,6 @@ void SelectionTab::select(int position) else if(position >= listItems.size()) slider->scrollBy(position - (int)listItems.size() + 1); - rememberCurrentSelection(); - if(curItems[py]->isFolder) { if(curItems[py]->folderName == "..") { @@ -510,9 +500,13 @@ void SelectionTab::select(int position) else curFolder += curItems[py]->folderName + "/"; filter(0); + slider->scrollTo(0); + return; } + rememberCurrentSelection(); + if(inputName && inputName->isActive()) { auto filename = *CResourceHandler::get("local")->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); @@ -594,6 +588,10 @@ int SelectionTab::getLine(const Point & clickPos) const void SelectionTab::selectFileName(std::string fname) { boost::to_upper(fname); + + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(fname); + curFolder = baseFolder != "" ? baseFolder + "/" : ""; + for(int i = (int)curItems.size() - 1; i >= 0; i--) { if(curItems[i]->fileURI == fname) @@ -604,12 +602,13 @@ void SelectionTab::selectFileName(std::string fname) } } + filter(0); selectAbs(-1); } std::shared_ptr SelectionTab::getSelectedMapInfo() const { - return curItems.empty() ? nullptr : curItems[selectionPos]; + return curItems.empty() || curItems[selectionPos]->isFolder ? nullptr : curItems[selectionPos]; } void SelectionTab::rememberCurrentSelection() @@ -799,7 +798,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); - iconFormat->disable(); + iconFormat->enable(); + iconFormat->setFrame(99); iconVictoryCondition->disable(); iconLossCondition->disable(); labelNumberOfCampaignMaps->disable(); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index fe5eef787..a2c45f125 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -106,7 +106,7 @@ private: ESelectionScreen tabType; Rect inputNameRect; - std::tuple checkSubfolder(std::string path); + std::tuple checkSubfolder(std::string path); bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); From 8693dab9ec687b758d111d2ecf7b148e0cbc517f Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:13:02 +0200 Subject: [PATCH 0020/1248] fix filter --- client/lobby/SelectionTab.cpp | 24 +++++++++++++++++------- client/lobby/SelectionTab.h | 1 + 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index cc0aceb4b..fbf56723f 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -138,7 +138,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder("") + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), curFilterSize(0) { OBJ_CONSTRUCTION; @@ -391,6 +391,10 @@ std::tuple SelectionTab::checkSubfolder(st // selMaps with the relevant data. void SelectionTab::filter(int size, bool selectFirst) { + if(size == -1) + size = curFilterSize; + curFilterSize = size; + curItems.clear(); for(auto elem : allItems) @@ -428,13 +432,19 @@ void SelectionTab::filter(int size, bool selectFirst) sort(); if(selectFirst) { - slider->scrollTo(0); - callOnSelect(curItems[0]); - selectAbs(-1); + int firstPos = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + if(firstPos < curItems.size()) + { + slider->scrollTo(firstPos); + callOnSelect(curItems[firstPos]); + selectAbs(firstPos); + } } } else { + updateListItems(); + redraw(); slider->block(true); if(callOnSelect) callOnSelect(nullptr); @@ -499,9 +509,9 @@ void SelectionTab::select(int position) } else curFolder += curItems[py]->folderName + "/"; - filter(0); + filter(-1); slider->scrollTo(0); - + return; } @@ -602,7 +612,7 @@ void SelectionTab::selectFileName(std::string fname) } } - filter(0); + filter(-1); selectAbs(-1); } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index a2c45f125..569152ebe 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -70,6 +70,7 @@ public: ESortBy sortingBy; ESortBy generalSortingBy; bool sortModeAscending; + int curFilterSize = 0; std::shared_ptr inputName; From 6013549ef804eb8b30626442a2eeb57f9c4211bf Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 9 Aug 2023 00:41:13 +0200 Subject: [PATCH 0021/1248] no old selection --- client/lobby/SelectionTab.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index fbf56723f..53a6fe52f 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -512,6 +512,12 @@ void SelectionTab::select(int position) filter(-1); slider->scrollTo(0); + int firstPos = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + if(firstPos < curItems.size()) + { + selectAbs(firstPos); + } + return; } From 1def98a862122708302dd451a7d5cc0daf790b8d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 9 Aug 2023 03:54:09 +0400 Subject: [PATCH 0022/1248] Tutorial implemented --- Mods/vcmi/config/vcmi/english.json | 1 + client/lobby/SelectionTab.cpp | 10 ++++++--- client/mainmenu/CMainMenu.cpp | 33 ++++++++++++++++++++++++++++-- client/mainmenu/CMainMenu.h | 1 + lib/filesystem/ResourceID.cpp | 1 + 5 files changed, 41 insertions(+), 5 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 26eecc0ba..1da513536 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -38,6 +38,7 @@ "vcmi.radialWheel.splitUnit" : "Split creature to another slot", "vcmi.mainMenu.tutorialNotImplemented" : "Sorry, tutorial is not implemented yet\n", + "vcmi.mainMenu.tutorialNotExist" : "Tutorial map is not found\n", "vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n", "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index a7e698e33..2f7e11b10 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -222,9 +222,13 @@ void SelectionTab::toggleMode() switch(tabType) { case ESelectionScreen::newGame: - inputName->disable(); - parseMaps(getFiles("Maps/", EResType::MAP)); - break; + { + inputName->disable(); + auto files = getFiles("Maps/", EResType::MAP); + files.erase(ResourceID("Maps/Tutorial.tut", EResType::MAP)); + parseMaps(files); + break; + } case ESelectionScreen::loadGame: inputName->disable(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index cd56a8208..3fdc3339c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -47,6 +47,7 @@ #include "../../lib/serializer/CTypeList.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" +#include "../../lib/mapping/CMapInfo.h" #include "../../lib/VCMIDirs.h" #include "../../lib/CStopWatch.h" #include "../../lib/NetPacksLobby.h" @@ -183,7 +184,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.tutorialNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::startTutorial); } break; } @@ -198,7 +199,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.tutorialNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::startTutorial); } } break; @@ -371,6 +372,34 @@ void CMainMenu::openCampaignScreen(std::string name) logGlobal->error("Unknown campaign set: %s", name); } +void CMainMenu::startTutorial() +{ + ResourceID tutorialMap("Maps/Tutorial.tut", EResType::MAP); + if(!CResourceHandler::get()->existsResource(tutorialMap)) + { + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.tutorialNotExist"), std::vector>(), PlayerColor(1)); + return; + } + + auto mapInfo = std::make_shared(); + mapInfo->mapInit(tutorialMap.getName()); + + CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + + GH.dispatchMainThread([mapInfo](){ + while(!CSH->c || !CSH->c->handler) + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + + while(!CSH->mi || mapInfo->fileURI != CSH->mi->fileURI) + { + CSH->setMapInfo(mapInfo); + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + + CSH->sendStartGame(); + }); +} + std::shared_ptr CMainMenu::create() { if(!CMM) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 3d65cf9f5..b0f07d633 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -148,6 +148,7 @@ public: static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); static void openCampaignLobby(const std::string & campaignFileName); static void openCampaignLobby(std::shared_ptr campaign); + static void startTutorial(); void openCampaignScreen(std::string name); static std::shared_ptr create(); diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourceID.cpp index fb63fc428..3519d6ace 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourceID.cpp @@ -129,6 +129,7 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) {".MSG", EResType::MASK}, {".H3C", EResType::CAMPAIGN}, {".H3M", EResType::MAP}, + {".TUT", EResType::MAP}, {".FNT", EResType::BMP_FONT}, {".TTF", EResType::TTF_FONT}, {".BMP", EResType::IMAGE}, From 04c0124759c705266fafc23225eece34a8511171 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 9 Aug 2023 14:59:26 +0400 Subject: [PATCH 0023/1248] Remove unnecessary strings --- Mods/vcmi/config/vcmi/english.json | 2 -- Mods/vcmi/config/vcmi/french.json | 1 - Mods/vcmi/config/vcmi/german.json | 1 - Mods/vcmi/config/vcmi/polish.json | 1 - Mods/vcmi/config/vcmi/ukrainian.json | 1 - client/mainmenu/CMainMenu.cpp | 2 +- 6 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 1da513536..8ef48c372 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -37,8 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Move creatures to another army", "vcmi.radialWheel.splitUnit" : "Split creature to another slot", - "vcmi.mainMenu.tutorialNotImplemented" : "Sorry, tutorial is not implemented yet\n", - "vcmi.mainMenu.tutorialNotExist" : "Tutorial map is not found\n", "vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n", "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 1597ab88d..5d3e0a532 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -30,7 +30,6 @@ "vcmi.capitalColors.6" : "Turquoise", "vcmi.capitalColors.7" : "Rose", - "vcmi.mainMenu.tutorialNotImplemented" : "Désolé, le didacticiel n'est pas encore implémenté\n", "vcmi.mainMenu.highscoresNotImplemented" : "Désolé, le menu des meilleurs scores n'est pas encore implémenté\n", "vcmi.mainMenu.serverConnecting" : "Connexion...", "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 3a1694cc7..9c9e2815a 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", - "vcmi.mainMenu.tutorialNotImplemented" : "Das Tutorial ist aktuell noch nicht implementiert\n", "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index c6a5f4231..4ebc37479 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", - "vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n", "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index e4d03e423..dcb8ddc61 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", - "vcmi.mainMenu.tutorialNotImplemented" : "Вибачте, навчання ще не реалізовано\n", "vcmi.mainMenu.highscoresNotImplemented" : "Вибачте, таблицю рекордів ще не реалізовано\n", "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 3fdc3339c..634c4013a 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -377,7 +377,7 @@ void CMainMenu::startTutorial() ResourceID tutorialMap("Maps/Tutorial.tut", EResType::MAP); if(!CResourceHandler::get()->existsResource(tutorialMap)) { - CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.tutorialNotExist"), std::vector>(), PlayerColor(1)); + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("core.genrltxt.742"), std::vector>(), PlayerColor(1)); return; } From f4869cbfb020f73f915523ff67c5bc1f8bd48a39 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 9 Aug 2023 15:29:48 +0400 Subject: [PATCH 0024/1248] Tutorial load game mode --- client/CServerHandler.cpp | 2 +- client/lobby/SelectionTab.cpp | 7 ++++++- client/mainmenu/CMainMenu.cpp | 2 +- client/mainmenu/CMainMenu.h | 2 +- 4 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 112af7f6c..fe48a0b14 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -682,7 +682,7 @@ int CServerHandler::howManyPlayerInterfaces() ui8 CServerHandler::getLoadMode() { - if(state == EClientState::GAMEPLAY) + if(loadMode != ELoadMode::TUTORIAL && state == EClientState::GAMEPLAY) { if(si->campState) return ELoadMode::CAMPAIGN; diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 2f7e11b10..c7bb977ad 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -613,16 +613,21 @@ void SelectionTab::parseSaves(const std::unordered_set & files) // Filter out other game modes bool isCampaign = mapInfo->scenarioOptionsOfSave->mode == StartInfo::CAMPAIGN; bool isMultiplayer = mapInfo->amountOfHumanPlayersInSave > 1; + bool isTutorial = boost::to_upper_copy(mapInfo->scenarioOptionsOfSave->mapname) == "MAPS/TUTORIAL"; switch(CSH->getLoadMode()) { case ELoadMode::SINGLE: - if(isMultiplayer || isCampaign) + if(isMultiplayer || isCampaign || isTutorial) mapInfo->mapHeader.reset(); break; case ELoadMode::CAMPAIGN: if(!isCampaign) mapInfo->mapHeader.reset(); break; + case ELoadMode::TUTORIAL: + if(!isTutorial) + mapInfo->mapHeader.reset(); + break; default: if(!isMultiplayer) mapInfo->mapHeader.reset(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 634c4013a..d089eb738 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -199,7 +199,7 @@ static std::function genCommand(CMenuScreen * menu, std::vector Date: Wed, 9 Aug 2023 20:27:24 +0200 Subject: [PATCH 0025/1248] erase -> pop_back --- client/lobby/SelectionTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 53a6fe52f..5e37b5225 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -503,8 +503,8 @@ void SelectionTab::select(int position) { std::vector filetree; boost::split(filetree, curFolder, boost::is_any_of("/")); - filetree.erase(filetree.end()); - filetree.erase(filetree.end()); + filetree.pop_back(); + filetree.pop_back(); curFolder = filetree.size() > 0 ? boost::algorithm::join(filetree, "/") + "/" : ""; } else From ef7008a753bb1df2c4072c6705c7bdbea578dcf3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 10 Aug 2023 01:27:13 +0400 Subject: [PATCH 0026/1248] Unblock UI while tutorial loading --- client/mainmenu/CMainMenu.cpp | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index d089eb738..e7501326b 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -386,18 +386,29 @@ void CMainMenu::startTutorial() CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); - GH.dispatchMainThread([mapInfo](){ - while(!CSH->c || !CSH->c->handler) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + std::thread waitForConnectionThread([mapInfo](){ + boost::this_thread::sleep(boost::posix_time::milliseconds(100)); //delay this thread - while(!CSH->mi || mapInfo->fileURI != CSH->mi->fileURI) + //connecting to server + while(CSH->state != EClientState::LOBBY) { - CSH->setMapInfo(mapInfo); + if(CSH->state == EClientState::CONNECTION_CANCELLED || CSH->state == EClientState::NONE) + return; boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } - CSH->sendStartGame(); + //start game from main thread + GH.dispatchMainThread([mapInfo]() + { + while(!CSH->si || mapInfo->fileURI != CSH->si->mapname) + { + CSH->setMapInfo(mapInfo); + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + CSH->sendStartGame(); + }); }); + waitForConnectionThread.detach(); } std::shared_ptr CMainMenu::create() From f24c636d175b335e9cc0fd43e3c4195aef721c36 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 10 Aug 2023 22:15:32 +0200 Subject: [PATCH 0027/1248] code review suggestions --- client/lobby/SelectionTab.cpp | 53 ++++++++++++++++++++--------------- client/lobby/SelectionTab.h | 4 +-- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 5e37b5225..93130e3f0 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -138,7 +138,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), curFilterSize(0) + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0) { OBJ_CONSTRUCTION; @@ -322,12 +322,12 @@ void SelectionTab::keyPressed(EShortcut key) void SelectionTab::clickDouble(const Point & cursorPosition) { int position = getLine(); - int py = position + slider->getValue(); + int itemIndex = position + slider->getValue(); - if(py >= curItems.size()) + if(itemIndex >= curItems.size()) return; - if(curItems[py]->isFolder) + if(curItems[itemIndex]->isFolder) return; if(getLine() != -1) //double clicked scenarios list @@ -355,11 +355,18 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) } } -std::tuple SelectionTab::checkSubfolder(std::string path) +auto SelectionTab::checkSubfolder(std::string path) { - std::string folderName = ""; - bool parentExists = (curFolder != ""); - bool fileInFolder = false; + struct Ret + { + std::string folderName; + std::string baseFolder; + bool parentExists; + bool fileInFolder; + } ret; + + ret.parentExists = (curFolder != ""); + ret.fileInFolder = false; std::vector filetree; // delete first element (e.g. 'MAPS') @@ -367,24 +374,25 @@ std::tuple SelectionTab::checkSubfolder(st filetree.erase(filetree.begin()); std::string pathWithoutPrefix = boost::algorithm::join(filetree, "/"); - std::string baseFolder = boost::filesystem::path(pathWithoutPrefix).parent_path().string(); + filetree.pop_back(); + ret.baseFolder = boost::algorithm::join(filetree, "/"); - if(boost::algorithm::starts_with(baseFolder, curFolder)) + if(boost::algorithm::starts_with(ret.baseFolder, curFolder)) { - std::string folder = baseFolder.substr(curFolder.size()); + std::string folder = ret.baseFolder.substr(curFolder.size()); if(folder != "") { boost::split(filetree, folder, boost::is_any_of("/")); - folderName = filetree[0]; + ret.folderName = filetree[0]; } } if(boost::algorithm::starts_with(pathWithoutPrefix, curFolder)) if(boost::count(pathWithoutPrefix.substr(curFolder.size()), '/') == 0) - fileInFolder = true; + ret.fileInFolder = true; - return {folderName, baseFolder, parentExists, fileInFolder}; + return ret; } // A new size filter (Small, Medium, ...) has been selected. Populate @@ -392,8 +400,8 @@ std::tuple SelectionTab::checkSubfolder(st void SelectionTab::filter(int size, bool selectFirst) { if(size == -1) - size = curFilterSize; - curFilterSize = size; + size = currentMapSizeFilter; + currentMapSizeFilter = size; curItems.clear(); @@ -408,15 +416,17 @@ void SelectionTab::filter(int size, bool selectFirst) auto folder = std::make_shared(); folder->isFolder = true; folder->folderName = ".."; - if (boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }) == curItems.end()) { + auto itemIt = boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }); + if (itemIt == curItems.end()) { curItems.push_back(folder); - } + } } std::shared_ptr folder = std::make_shared(); folder->isFolder = true; folder->folderName = folderName; - if (boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }) == curItems.end() && folderName != "") { + auto itemIt = boost::range::find_if(curItems, [folder](std::shared_ptr e) { return e->folderName == folder->folderName; }); + if (itemIt == curItems.end() && folderName != "") { curItems.push_back(folder); } @@ -698,9 +708,7 @@ void SelectionTab::parseMaps(const std::unordered_set & files) mapInfo->mapInit(file.getName()); if (isMapSupported(*mapInfo)) - { allItems.push_back(mapInfo); - } } catch(std::exception & e) { @@ -755,9 +763,8 @@ void SelectionTab::parseCampaigns(const std::unordered_set & files) //allItems[i].date = std::asctime(std::localtime(&files[i].date)); info->fileURI = file.getName(); info->campaignInit(); - if(info->campaign) { + if(info->campaign) allItems.push_back(info); - } } } diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 569152ebe..02f4eaa90 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -70,7 +70,7 @@ public: ESortBy sortingBy; ESortBy generalSortingBy; bool sortModeAscending; - int curFilterSize = 0; + int currentMapSizeFilter = 0; std::shared_ptr inputName; @@ -107,7 +107,7 @@ private: ESelectionScreen tabType; Rect inputNameRect; - std::tuple checkSubfolder(std::string path); + auto checkSubfolder(std::string path); bool isMapSupported(const CMapInfo & info); void parseMaps(const std::unordered_set & files); From 48747eea9d8ed90b100d1f978c93dd62a64ac093 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 10 Aug 2023 23:37:19 +0200 Subject: [PATCH 0028/1248] bigger line for folder --- .../folder.png => Data/lobby/iconFolder.png} | Bin Mods/vcmi/Data/lobby/selectionEmptyLine.png | Bin 0 -> 4588 bytes Mods/vcmi/Sprites/ScSelC.json | 3 +-- client/lobby/SelectionTab.cpp | 13 +++++++++++-- client/lobby/SelectionTab.h | 2 ++ 5 files changed, 14 insertions(+), 4 deletions(-) rename Mods/vcmi/{Sprites/mapFormatIcons/folder.png => Data/lobby/iconFolder.png} (100%) create mode 100644 Mods/vcmi/Data/lobby/selectionEmptyLine.png diff --git a/Mods/vcmi/Sprites/mapFormatIcons/folder.png b/Mods/vcmi/Data/lobby/iconFolder.png similarity index 100% rename from Mods/vcmi/Sprites/mapFormatIcons/folder.png rename to Mods/vcmi/Data/lobby/iconFolder.png diff --git a/Mods/vcmi/Data/lobby/selectionEmptyLine.png b/Mods/vcmi/Data/lobby/selectionEmptyLine.png new file mode 100644 index 0000000000000000000000000000000000000000..93328649212fe0dff24e8915a7781c45a3c6fc51 GIT binary patch literal 4588 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 literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Sprites/ScSelC.json b/Mods/vcmi/Sprites/ScSelC.json index 499e5740e..7d8279cc3 100644 --- a/Mods/vcmi/Sprites/ScSelC.json +++ b/Mods/vcmi/Sprites/ScSelC.json @@ -2,7 +2,6 @@ "basepath" : "mapFormatIcons/", "images" : [ - { "frame" : 3, "file" : "vcmi1.png"}, - { "frame" : 99, "file" : "folder.png"} + { "frame" : 3, "file" : "vcmi1.png"} ] } \ No newline at end of file diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 93130e3f0..e4c79a0af 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -788,6 +788,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico : CIntObject(LCLICK, position) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pictureEmptyLine = std::make_shared("lobby/selectionEmptyLine.png", -8, -14); labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -797,6 +798,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico labelMapSizeLetter = std::make_shared(41, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelMapSizeLetter->setAutoRedraw(false); // FIXME: This -12 should not be needed, but for some reason CAnimImage displaced otherwise + iconFolder = std::make_shared("lobby/iconFolder.png", -8, -12); iconFormat = std::make_shared(iconsFormats, 0, 0, 59, -12); iconVictoryCondition = std::make_shared(iconsVictory, 0, 0, 277, -12); iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); @@ -808,6 +810,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->disable(); iconVictoryCondition->disable(); iconLossCondition->disable(); @@ -821,8 +825,9 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); - iconFormat->enable(); - iconFormat->setFrame(99); + iconFolder->enable(); + pictureEmptyLine->enable(); + iconFormat->disable(); iconVictoryCondition->disable(); iconLossCondition->disable(); labelNumberOfCampaignMaps->disable(); @@ -836,6 +841,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool { labelAmountOfPlayers->disable(); labelMapSizeLetter->disable(); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->disable(); iconVictoryCondition->disable(); iconLossCondition->disable(); @@ -856,6 +863,8 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr info, bool labelMapSizeLetter->enable(); labelMapSizeLetter->setText(info->getMapSizeName()); labelMapSizeLetter->setColor(color); + iconFolder->disable(); + pictureEmptyLine->disable(); iconFormat->enable(); iconFormat->setFrame(info->getMapSizeFormatIconId()); iconVictoryCondition->enable(); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 02f4eaa90..5e8e35e92 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -45,9 +45,11 @@ class SelectionTab : public CIntObject std::shared_ptr labelAmountOfPlayers; std::shared_ptr labelNumberOfCampaignMaps; std::shared_ptr labelMapSizeLetter; + std::shared_ptr iconFolder; std::shared_ptr iconFormat; std::shared_ptr iconVictoryCondition; std::shared_ptr iconLossCondition; + std::shared_ptr pictureEmptyLine; std::shared_ptr labelName; ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss); From c260ce581496a5bcbc8a1515fac0cd56970f63c8 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 10 Aug 2023 23:52:16 +0200 Subject: [PATCH 0029/1248] current folder in first line --- client/lobby/SelectionTab.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index e4c79a0af..7758999bd 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -415,8 +415,8 @@ void SelectionTab::filter(int size, bool selectFirst) { auto folder = std::make_shared(); folder->isFolder = true; - folder->folderName = ".."; - auto itemIt = boost::range::find_if(curItems, [](std::shared_ptr e) { return e->folderName == ".."; }); + folder->folderName = ".. (" + curFolder + ")"; + auto itemIt = boost::range::find_if(curItems, [](std::shared_ptr e) { return boost::starts_with(e->folderName, ".."); }); if (itemIt == curItems.end()) { curItems.push_back(folder); } @@ -509,7 +509,7 @@ void SelectionTab::select(int position) slider->scrollBy(position - (int)listItems.size() + 1); if(curItems[py]->isFolder) { - if(curItems[py]->folderName == "..") + if(boost::starts_with(curItems[py]->folderName, "..")) { std::vector filetree; boost::split(filetree, curFolder, boost::is_any_of("/")); From 092a0d72a14375eb7083c88ad6406105f69738b4 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Aug 2023 00:19:24 +0200 Subject: [PATCH 0030/1248] whitespace format --- client/lobby/SelectionTab.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 7758999bd..3acd96b53 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -357,13 +357,13 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) auto SelectionTab::checkSubfolder(std::string path) { - struct Ret - { - std::string folderName; - std::string baseFolder; - bool parentExists; - bool fileInFolder; - } ret; + struct Ret + { + std::string folderName; + std::string baseFolder; + bool parentExists; + bool fileInFolder; + } ret; ret.parentExists = (curFolder != ""); ret.fileInFolder = false; From 2b093b88505c683fb8780abf50f4f86dc2d90d13 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Aug 2023 01:46:22 +0200 Subject: [PATCH 0031/1248] case handling --- client/lobby/SelectionTab.cpp | 18 +++++++++--------- lib/filesystem/ResourceID.cpp | 9 +++++---- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 3acd96b53..7fe1318bc 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -357,13 +357,13 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) auto SelectionTab::checkSubfolder(std::string path) { - struct Ret - { - std::string folderName; - std::string baseFolder; - bool parentExists; - bool fileInFolder; - } ret; + struct Ret + { + std::string folderName; + std::string baseFolder; + bool parentExists; + bool fileInFolder; + } ret; ret.parentExists = (curFolder != ""); ret.fileInFolder = false; @@ -773,12 +773,12 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re boost::to_upper(dirURI); CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) { - return boost::algorithm::starts_with(mount, dirURI); + return boost::algorithm::starts_with(boost::algorithm::to_upper_copy(mount), dirURI); }); std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) { - return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); + return ident.getType() == resType && boost::algorithm::starts_with(boost::algorithm::to_upper_copy(ident.getName()), dirURI); }); return ret; diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourceID.cpp index fb63fc428..49fe24bba 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourceID.cpp @@ -45,7 +45,7 @@ static inline EResType::Type readType(const std::string& name) return EResTypeHelper::getTypeFromExtension(FileInfo::GetExtension(name).to_string()); } -static inline std::string readName(std::string name) +static inline std::string readName(std::string name, EResType::Type type) { const auto dotPos = name.find_last_of('.'); @@ -61,7 +61,8 @@ static inline std::string readName(std::string name) name.resize(dotPos); } - toUpper(name); + if(type != EResType::MAP && type != EResType::CAMPAIGN && type != EResType::SAVEGAME) + toUpper(name); return name; } @@ -75,12 +76,12 @@ ResourceID::ResourceID() ResourceID::ResourceID(std::string name_): type{readType(name_)}, - name{readName(std::move(name_))} + name{readName(std::move(name_), readType(name_))} {} ResourceID::ResourceID(std::string name_, EResType::Type type_): type{type_}, - name{readName(std::move(name_))} + name{readName(std::move(name_), type_)} {} #if 0 std::string ResourceID::getName() const From 2d9bb348257744c157ea56081aec9045ed8e58fb Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Aug 2023 01:49:10 +0200 Subject: [PATCH 0032/1248] format --- client/lobby/SelectionTab.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 7fe1318bc..436cd3d7d 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -357,13 +357,13 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) auto SelectionTab::checkSubfolder(std::string path) { - struct Ret - { - std::string folderName; - std::string baseFolder; - bool parentExists; - bool fileInFolder; - } ret; + struct Ret + { + std::string folderName; + std::string baseFolder; + bool parentExists; + bool fileInFolder; + } ret; ret.parentExists = (curFolder != ""); ret.fileInFolder = false; @@ -392,7 +392,7 @@ auto SelectionTab::checkSubfolder(std::string path) if(boost::count(pathWithoutPrefix.substr(curFolder.size()), '/') == 0) ret.fileInFolder = true; - return ret; + return ret; } // A new size filter (Small, Medium, ...) has been selected. Populate From 70d04ad95749a4e2bb7987ca117f387c58c7b1ba Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 11 Aug 2023 20:04:14 +0400 Subject: [PATCH 0033/1248] Replace thread approach with callback based --- client/CServerHandler.cpp | 7 +++++++ client/CServerHandler.h | 5 +++++ client/NetPacksLobbyClient.cpp | 9 ++++++++- client/mainmenu/CMainMenu.cpp | 26 +------------------------- 4 files changed, 21 insertions(+), 26 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index fe48a0b14..ea4e24d19 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -140,6 +140,7 @@ void CServerHandler::resetStateForLobby(const StartInfo::EMode mode, const std:: { hostClientId = -1; state = EClientState::NONE; + mapToStart = nullptr; th = std::make_unique(); packsForLobbyScreen.clear(); c.reset(); @@ -396,6 +397,7 @@ void CServerHandler::sendClientDisconnecting() return; state = EClientState::DISCONNECTING; + mapToStart = nullptr; LobbyClientDisconnected lcd; lcd.clientId = c->connectionID; logNetwork->info("Connection has been requested to be closed."); @@ -553,6 +555,11 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const c->disableStackSendingByID(); } +void CServerHandler::startMapAfterConnection(std::shared_ptr to) +{ + mapToStart = to; +} + void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState) { if(CMM) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index e8b20117d..7c1cfca46 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -74,10 +74,14 @@ public: /// structure to handle running server and connecting to it class CServerHandler : public IServerAPI, public LobbyInfo { + friend class ApplyOnLobbyHandlerNetPackVisitor; + std::shared_ptr> applier; std::shared_ptr mx; std::list packsForLobbyScreen; //protected by mx + + std::shared_ptr mapToStart; std::vector myNames; @@ -148,6 +152,7 @@ public: void sendRestartGame() const override; void sendStartGame(bool allowOnlyAI = false) const override; + void startMapAfterConnection(std::shared_ptr to); void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void endGameplay(bool closeConnection = true, bool restart = false); void startCampaignScenario(std::shared_ptr cs = {}); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 37ecb9788..8329e5e31 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -38,7 +38,9 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon if(pack.uuid == handler.c->uuid) { handler.c->connectionID = pack.clientId; - if(!settings["session"]["headless"].Bool()) + if(handler.mapToStart) + handler.setMapInfo(handler.mapToStart); + else if(!settings["session"]["headless"].Bool()) GH.windows().createAndPushWindow(static_cast(handler.screenType)); handler.state = EClientState::LOBBY; } @@ -136,6 +138,11 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & { pack.hostChanged = pack.state.hostClientId != handler.hostClientId; static_cast(handler) = pack.state; + if(handler.mapToStart && handler.mi) + { + handler.startMapAfterConnection(nullptr); + handler.sendStartGame(); + } } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index e7501326b..c55ba614c 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -383,32 +383,8 @@ void CMainMenu::startTutorial() auto mapInfo = std::make_shared(); mapInfo->mapInit(tutorialMap.getName()); - + CSH->startMapAfterConnection(mapInfo); CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); - - std::thread waitForConnectionThread([mapInfo](){ - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); //delay this thread - - //connecting to server - while(CSH->state != EClientState::LOBBY) - { - if(CSH->state == EClientState::CONNECTION_CANCELLED || CSH->state == EClientState::NONE) - return; - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - - //start game from main thread - GH.dispatchMainThread([mapInfo]() - { - while(!CSH->si || mapInfo->fileURI != CSH->si->mapname) - { - CSH->setMapInfo(mapInfo); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - CSH->sendStartGame(); - }); - }); - waitForConnectionThread.detach(); } std::shared_ptr CMainMenu::create() From 6ca5518ff13e9af584c023998d2c566203d87f3b Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Aug 2023 20:09:41 +0200 Subject: [PATCH 0034/1248] Buxfix --- client/lobby/SelectionTab.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 436cd3d7d..ec26af22f 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -374,8 +374,13 @@ auto SelectionTab::checkSubfolder(std::string path) filetree.erase(filetree.begin()); std::string pathWithoutPrefix = boost::algorithm::join(filetree, "/"); - filetree.pop_back(); - ret.baseFolder = boost::algorithm::join(filetree, "/"); + if(!filetree.empty()) + { + filetree.pop_back(); + ret.baseFolder = boost::algorithm::join(filetree, "/"); + } + else + ret.baseFolder = ""; if(boost::algorithm::starts_with(ret.baseFolder, curFolder)) { @@ -613,8 +618,6 @@ int SelectionTab::getLine(const Point & clickPos) const void SelectionTab::selectFileName(std::string fname) { - boost::to_upper(fname); - auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(fname); curFolder = baseFolder != "" ? baseFolder + "/" : ""; From f0b60cf166510436419a978c53c6b82c07a247e5 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 11 Aug 2023 23:56:20 +0200 Subject: [PATCH 0035/1248] suggestions; use internally uppercase; fix; fullname --- client/lobby/SelectionTab.cpp | 21 +++++++++++++++------ lib/filesystem/ResourceID.cpp | 10 ++++++---- lib/filesystem/ResourceID.h | 4 ++++ lib/mapping/CMapInfo.cpp | 20 +++++++++++++++++--- lib/mapping/CMapInfo.h | 2 ++ 5 files changed, 44 insertions(+), 13 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index ec26af22f..bb0f6adc6 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -242,6 +242,7 @@ void SelectionTab::toggleMode() case ESelectionScreen::saveGame: parseSaves(getFiles("Saves/", EResType::SAVEGAME)); inputName->enable(); + inputName->activate(); restoreLastSelection(); break; @@ -347,7 +348,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(!curItems[py]->isFolder) { - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fileURI); + std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); @@ -414,7 +415,7 @@ void SelectionTab::filter(int size, bool selectFirst) { if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { - auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->fileURI); + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->originalFileURI); if(parentExists) { @@ -618,8 +619,16 @@ int SelectionTab::getLine(const Point & clickPos) const void SelectionTab::selectFileName(std::string fname) { - auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(fname); - curFolder = baseFolder != "" ? baseFolder + "/" : ""; + boost::to_upper(fname); + + for(int i = (int)allItems.size() - 1; i >= 0; i--) + { + if(allItems[i]->fileURI == fname) + { + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(allItems[i]->originalFileURI); + curFolder = baseFolder != "" ? baseFolder + "/" : ""; + } + } for(int i = (int)curItems.size() - 1; i >= 0; i--) { @@ -776,12 +785,12 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re boost::to_upper(dirURI); CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) { - return boost::algorithm::starts_with(boost::algorithm::to_upper_copy(mount), dirURI); + return boost::algorithm::starts_with(mount, dirURI); }); std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) { - return ident.getType() == resType && boost::algorithm::starts_with(boost::algorithm::to_upper_copy(ident.getName()), dirURI); + return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); }); return ret; diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourceID.cpp index 49fe24bba..401d89ef1 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourceID.cpp @@ -45,7 +45,7 @@ static inline EResType::Type readType(const std::string& name) return EResTypeHelper::getTypeFromExtension(FileInfo::GetExtension(name).to_string()); } -static inline std::string readName(std::string name, EResType::Type type) +static inline std::string readName(std::string name, bool uppercase) { const auto dotPos = name.find_last_of('.'); @@ -61,7 +61,7 @@ static inline std::string readName(std::string name, EResType::Type type) name.resize(dotPos); } - if(type != EResType::MAP && type != EResType::CAMPAIGN && type != EResType::SAVEGAME) + if(uppercase) toUpper(name); return name; @@ -76,12 +76,14 @@ ResourceID::ResourceID() ResourceID::ResourceID(std::string name_): type{readType(name_)}, - name{readName(std::move(name_), readType(name_))} + name{readName(name_, true)}, + originalName{readName(std::move(name_), false)} {} ResourceID::ResourceID(std::string name_, EResType::Type type_): type{type_}, - name{readName(std::move(name_), type_)} + name{readName(name_, true)}, + originalName{readName(std::move(name_), false)} {} #if 0 std::string ResourceID::getName() const diff --git a/lib/filesystem/ResourceID.h b/lib/filesystem/ResourceID.h index de29dec48..0e597bd74 100644 --- a/lib/filesystem/ResourceID.h +++ b/lib/filesystem/ResourceID.h @@ -99,6 +99,7 @@ public: } std::string getName() const {return name;} + std::string getOriginalName() const {return originalName;} EResType::Type getType() const {return type;} //void setName(std::string name); //void setType(EResType::Type type); @@ -112,6 +113,9 @@ private: /** Specifies the resource name. No extension so .pcx and .png can override each other, always in upper case. **/ std::string name; + + /** name in original case **/ + std::string originalName; }; /** diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 734a80265..ee1e74a0f 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -43,7 +43,10 @@ void CMapInfo::mapInit(const std::string & fname) { fileURI = fname; CMapService mapService; - mapHeader = mapService.loadMapHeader(ResourceID(fname, EResType::MAP)); + ResourceID resource = ResourceID(fname, EResType::MAP); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); + mapHeader = mapService.loadMapHeader(resource); countPlayers(); } @@ -55,9 +58,17 @@ void CMapInfo::saveInit(const ResourceID & file) mapHeader = std::make_unique(); lf >> *(mapHeader) >> scenarioOptionsOfSave; fileURI = file.getName(); + originalFileURI = file.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); countPlayers(); std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - date = std::asctime(std::localtime(&time)); + + std::tm tm = *std::localtime(&time); + std::stringstream s; + s.imbue(std::locale("")); + s << std::put_time(&tm, "%x %X"); + date = s.str(); + // We absolutely not need this data for lobby and server will read it from save // FIXME: actually we don't want them in CMapHeader! mapHeader->triggeredEvents.clear(); @@ -65,6 +76,9 @@ void CMapInfo::saveInit(const ResourceID & file) void CMapInfo::campaignInit() { + ResourceID resource = ResourceID(fileURI, EResType::CAMPAIGN); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); campaign = CampaignHandler::getHeader(fileURI); } @@ -105,7 +119,7 @@ std::string CMapInfo::getNameForList() const { // TODO: this could be handled differently std::vector path; - boost::split(path, fileURI, boost::is_any_of("\\/")); + boost::split(path, originalFileURI, boost::is_any_of("\\/")); return path[path.size()-1]; } else diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 5724988bd..219969c4a 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -28,6 +28,8 @@ public: std::unique_ptr campaign; //may be nullptr if scenario StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) std::string fileURI; + std::string originalFileURI; + std::string fullFileURI; std::string date; int amountOfPlayersOnMap; int amountOfHumanControllablePlayers; From 4d507f3d8a43867e99012f6b5caf7f8519ad8a1a Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 00:00:35 +0200 Subject: [PATCH 0036/1248] format fix --- lib/mapping/CMapInfo.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index ee1e74a0f..cc70ab059 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -63,11 +63,11 @@ void CMapInfo::saveInit(const ResourceID & file) countPlayers(); std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - std::tm tm = *std::localtime(&time); + std::tm tm = *std::localtime(&time); std::stringstream s; s.imbue(std::locale("")); - s << std::put_time(&tm, "%x %X"); - date = s.str(); + s << std::put_time(&tm, "%x %X"); + date = s.str(); // We absolutely not need this data for lobby and server will read it from save // FIXME: actually we don't want them in CMapHeader! From 52f00ec308c7a179bce5f134ddf4dbcea4d38827 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 01:13:03 +0200 Subject: [PATCH 0037/1248] extra function: getFormattedDateTime --- cmake_modules/VCMI_lib.cmake | 2 ++ include/vstd/DateUtils.h | 12 ++++++++++++ lib/VCMI_lib.vcxproj | 1 + lib/VCMI_lib.vcxproj.filters | 3 +++ lib/mapping/CMapInfo.cpp | 9 +++------ lib/vstd/DateUtils.cpp | 20 ++++++++++++++++++++ 6 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 include/vstd/DateUtils.h create mode 100644 lib/vstd/DateUtils.cpp diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 7152a5327..ba34a1391 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -220,6 +220,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/spells/effects/RemoveObstacle.cpp ${MAIN_LIB_DIR}/spells/effects/Sacrifice.cpp + ${MAIN_LIB_DIR}/vstd/DateUtils.cpp ${MAIN_LIB_DIR}/vstd/StringUtils.cpp ${MAIN_LIB_DIR}/ArtifactUtils.cpp @@ -283,6 +284,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/../include/vstd/ContainerUtils.h ${MAIN_LIB_DIR}/../include/vstd/RNG.h + ${MAIN_LIB_DIR}/../include/vstd/DateUtils.h ${MAIN_LIB_DIR}/../include/vstd/StringUtils.h ${MAIN_LIB_DIR}/../include/vcmi/events/AdventureEvents.h diff --git a/include/vstd/DateUtils.h b/include/vstd/DateUtils.h new file mode 100644 index 000000000..9f3a9cc43 --- /dev/null +++ b/include/vstd/DateUtils.h @@ -0,0 +1,12 @@ +#pragma once + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt); + +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_lib.vcxproj b/lib/VCMI_lib.vcxproj index 2598a5bc6..ea6777a05 100644 --- a/lib/VCMI_lib.vcxproj +++ b/lib/VCMI_lib.vcxproj @@ -320,6 +320,7 @@ + diff --git a/lib/VCMI_lib.vcxproj.filters b/lib/VCMI_lib.vcxproj.filters index a8541fe9d..3679e6a8c 100644 --- a/lib/VCMI_lib.vcxproj.filters +++ b/lib/VCMI_lib.vcxproj.filters @@ -393,6 +393,9 @@ registerTypes + + vstd + vstd diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index cc70ab059..64796bb6f 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "CMapInfo.h" +#include + #include "../filesystem/ResourceID.h" #include "../StartInfo.h" #include "../GameConstants.h" @@ -62,12 +64,7 @@ void CMapInfo::saveInit(const ResourceID & file) fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); countPlayers(); std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - - std::tm tm = *std::localtime(&time); - std::stringstream s; - s.imbue(std::locale("")); - s << std::put_time(&tm, "%x %X"); - date = s.str(); + date = vstd::getFormattedDateTime(time); // We absolutely not need this data for lobby and server will read it from save // FIXME: actually we don't want them in CMapHeader! diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp new file mode 100644 index 000000000..41f2d59c7 --- /dev/null +++ b/lib/vstd/DateUtils.cpp @@ -0,0 +1,20 @@ +#include "StdInc.h" +#include + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + + DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) + { + std::tm tm = *std::localtime(&dt); + std::stringstream s; + s.imbue(std::locale("")); + s << std::put_time(&tm, "%x %X"); + return s.str(); + } + +} + +VCMI_LIB_NAMESPACE_END From d0522b0fee43f9bd75f819d8e459f88c48383b5c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 02:41:48 +0200 Subject: [PATCH 0038/1248] initial window --- client/lobby/OptionsTab.cpp | 143 ++++++++++++++++++++++++++++++++++-- client/lobby/OptionsTab.h | 30 +++++++- 2 files changed, 167 insertions(+), 6 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index e7d5243ab..395857e65 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -15,6 +15,7 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../render/Graphics.h" #include "../render/IFont.h" @@ -414,9 +415,136 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) - : Scrollable(SHOW_POPUP, position, Orientation::HORIZONTAL) +OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo playerInfo) + : CWindowObject(BORDERED) +{ + addUsedEvents(LCLICK | SHOW_POPUP); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(0, 0, 400, 400); + + backgroundTexture = std::make_shared("DIBOXBCK", pos); + updateShadow(); + + genContentCastles(settings, playerInfo); + genContentHeroes(settings, playerInfo); + + center(); +} + +void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, PlayerInfo playerInfo) +{ + PlayerSettings set = PlayerSettings(); + set.castle = set.RANDOM; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 0, 0)); + + int i = 0; + for(auto & elem : playerInfo.allowedFactions) + { + int x = i%3; + int y = i/3+1; + + PlayerSettings set = PlayerSettings(); + set.castle = elem; + + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 48, y * 32)); + factions.push_back(elem); + + i++; + } +} + +void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo) +{ + +} + +void OptionsTab::SelectionWindow::apply() +{ + if(GH.windows().isTopWindow(this)) + { + close(); + } +} + +FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosition) +{ + Point loc = getElement(cursorPosition); + + FactionID faction; + faction = PlayerSettings().NONE; + if (loc.x == 0 && loc.y == 0) + faction = PlayerSettings().RANDOM; + else if(loc.y > 0 && loc.x < 3) + { + int index = loc.x + (loc.y - 1) * 3; + if (index < factions.size()) + faction = factions[loc.x + (loc.y - 1) * 3]; + } + + return faction; +} + +SHeroName OptionsTab::SelectionWindow::getElementHero(const Point & cursorPosition) +{ + return SHeroName(); //TODO +} + +int OptionsTab::SelectionWindow::getElementBonus(const Point & cursorPosition) +{ + return PlayerSettings::Ebonus::NONE; //TODO +} + +Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) +{ + int x = (cursorPosition.x - pos.x) / 48; + int y = (cursorPosition.y - pos.y) / 32; + + return Point(x, y); +} + +void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { + FactionID faction = getElementCastle(cursorPosition); + + PlayerSettings set = PlayerSettings(); + set.castle = faction; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + + // cases when we do not need to display proceed + if(set.castle == -2 && helper.type == TOWN) + return; + if(set.hero == -2 && helper.type == HERO) + return; + + if(faction != PlayerSettings().NONE) + apply(); +} + +void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) +{ + FactionID faction = getElementCastle(cursorPosition); + + PlayerSettings set = PlayerSettings(); + set.castle = faction; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + + // cases when we do not need to display a message + if(set.castle == -2 && helper.type == TOWN) + return; + if(set.hero == -2 && helper.type == HERO) + return; + + GH.windows().createAndPushWindow(helper); +} + +OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, PlayerInfo & playerInfo, SelType type) + : Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL) , CPlayerSettingsHelper(settings, type) + , playerInfo(playerInfo) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -445,6 +573,11 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) GH.windows().createAndPushWindow(*this); } +void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(settings, playerInfo); +} + void OptionsTab::SelectedBox::scrollBy(int distance) { // FIXME: currently options tab is completely recreacted from scratch whenever we receive any information from server @@ -538,9 +671,9 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con else flag = nullptr; - town = std::make_shared(Point(119, 2), *s, TOWN); - hero = std::make_shared(Point(195, 2), *s, HERO); - bonus = std::make_shared(Point(271, 2), *s, BONUS); + town = std::make_shared(Point(119, 2), *s, *pi, TOWN); + hero = std::make_shared(Point(195, 2), *s, *pi, HERO); + bonus = std::make_shared(Point(271, 2), *s, *pi, BONUS); } void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index b27e1dd50..a4f4d4f28 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -17,6 +17,7 @@ struct PlayerInfo; VCMI_LIB_NAMESPACE_END #include "../widgets/Scrollable.h" +#include "../../lib/mapping/CMapHeader.h" class CSlider; class CLabel; @@ -94,14 +95,41 @@ public: CPlayerOptionTooltipBox(CPlayerSettingsHelper & helper); }; + class SelectionWindow : public CWindowObject + { + std::shared_ptr backgroundTexture; + std::vector> components; + + std::vector factions; + std::vector heroes; + + void genContentCastles(PlayerSettings settings, PlayerInfo playerInfo); + void genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo); + + void apply(); + FactionID getElementCastle(const Point & cursorPosition); + SHeroName getElementHero(const Point & cursorPosition); + int getElementBonus(const Point & cursorPosition); + Point getElement(const Point & cursorPosition); + + void clickReleased(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + + public: + SelectionWindow(PlayerSettings settings, PlayerInfo playerInfo); + }; + /// Image with current town/hero/bonus struct SelectedBox : public Scrollable, public CPlayerSettingsHelper { + const PlayerInfo & playerInfo; + std::shared_ptr image; std::shared_ptr subtitle; - SelectedBox(Point position, PlayerSettings & settings, SelType type); + SelectedBox(Point position, PlayerSettings & settings, PlayerInfo & playerInfo, SelType type); void showPopupWindow(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; void scrollBy(int distance) override; void update(); From b4f6c7abab6c1e5da194281fe37a3708aba5839c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 13:19:58 +0200 Subject: [PATCH 0039/1248] big images; basic heroes support --- client/lobby/OptionsTab.cpp | 54 ++++++++++++++++++++++++++----------- client/lobby/OptionsTab.h | 8 +++--- 2 files changed, 43 insertions(+), 19 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 395857e65..ff57477d9 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -80,7 +80,7 @@ void OptionsTab::recreate() } } -size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() +size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) { enum EBonusSelection //frames of bonuses file { @@ -104,7 +104,7 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() case PlayerSettings::RANDOM: return TOWN_RANDOM; default: - return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + 2; + return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + (big ? 0 : 2); } case HERO: switch(settings.hero) @@ -161,14 +161,14 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex() return 0; } -std::string OptionsTab::CPlayerSettingsHelper::getImageName() +std::string OptionsTab::CPlayerSettingsHelper::getImageName(bool big) { switch(type) { case OptionsTab::TOWN: - return "ITPA"; + return big ? "ITPt": "ITPA"; case OptionsTab::HERO: - return "PortraitsSmall"; + return big ? "PortraitsLarge": "PortraitsSmall"; case OptionsTab::BONUS: return "SCNRSTAR"; } @@ -422,7 +422,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(0, 0, 400, 400); + pos = Rect(0, 0, 700, 700); backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); @@ -438,20 +438,20 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla PlayerSettings set = PlayerSettings(); set.castle = set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 0, 0)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (58 - 48) / 2, (64 - 32) / 2)); int i = 0; for(auto & elem : playerInfo.allowedFactions) { - int x = i%3; - int y = i/3+1; + int x = i % ELEMENTS_PER_LINE; + int y = i / ELEMENTS_PER_LINE + 1; PlayerSettings set = PlayerSettings(); set.castle = elem; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 48, y * 32)); + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); factions.push_back(elem); i++; @@ -460,7 +460,29 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo) { + std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; + std::set allowedHeroes; + for(int i = 0; i < allowedHeroesFlag.size(); i++) + if(allowedHeroesFlag[i]) + allowedHeroes.insert(HeroTypeID(i)); + + int i = 0; + for(auto & elem : allowedHeroes) + { + int x = (i % ELEMENTS_PER_LINE) + (ELEMENTS_PER_LINE + 1); + int y = i / ELEMENTS_PER_LINE + 1; + + PlayerSettings set = PlayerSettings(); + set.hero = elem; + + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); + heroes.push_back(elem); + + i++; + } } void OptionsTab::SelectionWindow::apply() @@ -477,13 +499,13 @@ FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosi FactionID faction; faction = PlayerSettings().NONE; - if (loc.x == 0 && loc.y == 0) + if (loc.x == ELEMENTS_PER_LINE / 2 && loc.y == 0) faction = PlayerSettings().RANDOM; - else if(loc.y > 0 && loc.x < 3) + else if(loc.y > 0 && loc.x < ELEMENTS_PER_LINE) { - int index = loc.x + (loc.y - 1) * 3; + int index = loc.x + (loc.y - 1) * ELEMENTS_PER_LINE; if (index < factions.size()) - faction = factions[loc.x + (loc.y - 1) * 3]; + faction = factions[loc.x + (loc.y - 1) * ELEMENTS_PER_LINE]; } return faction; @@ -501,8 +523,8 @@ int OptionsTab::SelectionWindow::getElementBonus(const Point & cursorPosition) Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) { - int x = (cursorPosition.x - pos.x) / 48; - int y = (cursorPosition.y - pos.y) / 32; + int x = (cursorPosition.x - pos.x) / 58; + int y = (cursorPosition.y - pos.y) / 64; return Point(x, y); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index a4f4d4f28..0e8172791 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -61,8 +61,8 @@ public: {} /// visible image settings - size_t getImageIndex(); - std::string getImageName(); + size_t getImageIndex(bool big = false); + std::string getImageName(bool big = false); std::string getName(); /// name visible in options dialog std::string getTitle(); /// title in popup box @@ -97,11 +97,13 @@ public: class SelectionWindow : public CWindowObject { + const int ELEMENTS_PER_LINE = 5; + std::shared_ptr backgroundTexture; std::vector> components; std::vector factions; - std::vector heroes; + std::vector heroes; void genContentCastles(PlayerSettings settings, PlayerInfo playerInfo); void genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo); From 0c4f4daf70d0861d07c501c5df2275aad6efb7c6 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:05:26 +0200 Subject: [PATCH 0040/1248] base rendering --- client/lobby/OptionsTab.cpp | 143 +++++++++++++++++++++++++++--------- client/lobby/OptionsTab.h | 7 +- 2 files changed, 112 insertions(+), 38 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index ff57477d9..fe8155395 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -422,13 +422,14 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(0, 0, 700, 700); + pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 58, 700); backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); genContentCastles(settings, playerInfo); genContentHeroes(settings, playerInfo); + genContentBonus(settings, playerInfo); center(); } @@ -438,13 +439,13 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla PlayerSettings set = PlayerSettings(); set.castle = set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (58 - 48) / 2, (64 - 32) / 2)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + 34, 32 / 2 + 64)); int i = 0; for(auto & elem : playerInfo.allowedFactions) { - int x = i % ELEMENTS_PER_LINE; - int y = i / ELEMENTS_PER_LINE + 1; + int x = i % ELEMENTS_PER_LINE + 1; + int y = i / ELEMENTS_PER_LINE + 2; PlayerSettings set = PlayerSettings(); set.castle = elem; @@ -460,6 +461,11 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo) { + PlayerSettings set = PlayerSettings(); + set.castle = set.RANDOM; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (ELEMENTS_PER_LINE + 1) * 58 + 34, 32 / 2 + 64)); + std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; std::set allowedHeroes; @@ -470,16 +476,40 @@ void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, Play int i = 0; for(auto & elem : allowedHeroes) { - int x = (i % ELEMENTS_PER_LINE) + (ELEMENTS_PER_LINE + 1); - int y = i / ELEMENTS_PER_LINE + 1; + CHero * type = VLC->heroh->objects[elem]; - PlayerSettings set = PlayerSettings(); - set.hero = elem; + if(type->heroClass->faction == settings.castle) + { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + int x = (i % ELEMENTS_PER_LINE) + ELEMENTS_PER_LINE + 2; + int y = i / ELEMENTS_PER_LINE + 2; - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); - heroes.push_back(elem); + PlayerSettings set = PlayerSettings(); + set.hero = elem; + + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); + heroes.push_back(elem); + + i++; + } + } +} + +void OptionsTab::SelectionWindow::genContentBonus(PlayerSettings settings, PlayerInfo playerInfo) +{ + PlayerSettings set = PlayerSettings(); + + int i = 0; + for(auto elem : {set.RANDOM, set.ARTIFACT, set.GOLD, set.RESOURCE}) + { + int x = ELEMENTS_PER_LINE * 2 + 3; + int y = i + 1; + + set.bonus = elem; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 58, y * 64 + 32 / 2)); i++; } @@ -495,11 +525,11 @@ void OptionsTab::SelectionWindow::apply() FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosition) { - Point loc = getElement(cursorPosition); + Point loc = getElement(cursorPosition, 0); FactionID faction; faction = PlayerSettings().NONE; - if (loc.x == ELEMENTS_PER_LINE / 2 && loc.y == 0) + if ((loc.x == ELEMENTS_PER_LINE / 2 || loc.x == ELEMENTS_PER_LINE / 2 - 1) && loc.y == 0) faction = PlayerSettings().RANDOM; else if(loc.y > 0 && loc.x < ELEMENTS_PER_LINE) { @@ -511,56 +541,99 @@ FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosi return faction; } -SHeroName OptionsTab::SelectionWindow::getElementHero(const Point & cursorPosition) +HeroTypeID OptionsTab::SelectionWindow::getElementHero(const Point & cursorPosition) { - return SHeroName(); //TODO + Point loc = getElement(cursorPosition, 1); + + HeroTypeID hero; + hero = PlayerSettings().NONE; + + if(loc.x < 0) + return hero; + + if ((loc.x == ELEMENTS_PER_LINE / 2 || loc.x == ELEMENTS_PER_LINE / 2 - 1) && loc.y == 0) + hero = PlayerSettings().RANDOM; + else if(loc.y > 0 && loc.x < ELEMENTS_PER_LINE) + { + int index = loc.x + (loc.y - 1) * ELEMENTS_PER_LINE; + if (index < heroes.size()) + hero = heroes[loc.x + (loc.y - 1) * ELEMENTS_PER_LINE]; + } + + return hero; } int OptionsTab::SelectionWindow::getElementBonus(const Point & cursorPosition) { - return PlayerSettings::Ebonus::NONE; //TODO + Point loc = getElement(cursorPosition, 2); + + if(loc.x != 0 || loc.y < 0 || loc.y > 3) + return -2; + + return loc.y - 1; } -Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) +Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition, int area) { - int x = (cursorPosition.x - pos.x) / 58; + int x = (cursorPosition.x - pos.x - area * (ELEMENTS_PER_LINE + 1) * 58) / 58; int y = (cursorPosition.y - pos.y) / 64; - return Point(x, y); + return Point(x - 1, y - 1); } void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { FactionID faction = getElementCastle(cursorPosition); + HeroTypeID hero = getElementHero(cursorPosition); + int bonus = getElementBonus(cursorPosition); PlayerSettings set = PlayerSettings(); set.castle = faction; - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + set.hero = hero; + set.bonus = static_cast(bonus); - // cases when we do not need to display proceed - if(set.castle == -2 && helper.type == TOWN) - return; - if(set.hero == -2 && helper.type == HERO) - return; - - if(faction != PlayerSettings().NONE) + if(set.castle != -2) + { + //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); apply(); + } + else if(set.hero != -2) + { + //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + apply(); + } + else if(set.bonus != -2) + { + //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + apply(); + } } void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) { FactionID faction = getElementCastle(cursorPosition); + HeroTypeID hero = getElementHero(cursorPosition); + int bonus = getElementBonus(cursorPosition); PlayerSettings set = PlayerSettings(); set.castle = faction; - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + set.hero = hero; + set.bonus = static_cast(bonus); - // cases when we do not need to display a message - if(set.castle == -2 && helper.type == TOWN) - return; - if(set.hero == -2 && helper.type == HERO) - return; - - GH.windows().createAndPushWindow(helper); + if(set.castle != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + GH.windows().createAndPushWindow(helper); + } + else if(set.hero != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + GH.windows().createAndPushWindow(helper); + } + else if(set.bonus != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + GH.windows().createAndPushWindow(helper); + } } OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, PlayerInfo & playerInfo, SelType type) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 0e8172791..7a62753c4 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -97,7 +97,7 @@ public: class SelectionWindow : public CWindowObject { - const int ELEMENTS_PER_LINE = 5; + const int ELEMENTS_PER_LINE = 4; std::shared_ptr backgroundTexture; std::vector> components; @@ -107,12 +107,13 @@ public: void genContentCastles(PlayerSettings settings, PlayerInfo playerInfo); void genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo); + void genContentBonus(PlayerSettings settings, PlayerInfo playerInfo); void apply(); FactionID getElementCastle(const Point & cursorPosition); - SHeroName getElementHero(const Point & cursorPosition); + HeroTypeID getElementHero(const Point & cursorPosition); int getElementBonus(const Point & cursorPosition); - Point getElement(const Point & cursorPosition); + Point getElement(const Point & cursorPosition, int area); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; From bc4d331888870b24b9f4645f20219f574756be97 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 15:19:40 +0200 Subject: [PATCH 0041/1248] Title --- client/lobby/OptionsTab.cpp | 12 ++++++++++++ client/lobby/OptionsTab.h | 1 + 2 files changed, 13 insertions(+) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index fe8155395..34531af0d 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -427,6 +427,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); + genContentTitle(); genContentCastles(settings, playerInfo); genContentHeroes(settings, playerInfo); genContentBonus(settings, playerInfo); @@ -434,8 +435,17 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo center(); } +void OptionsTab::SelectionWindow::genContentTitle() +{ + components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Town")); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Hero")); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 58 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Bonus")); +} + void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, PlayerInfo playerInfo) { + factions.clear(); + PlayerSettings set = PlayerSettings(); set.castle = set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); @@ -461,6 +471,8 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo) { + heroes.clear(); + PlayerSettings set = PlayerSettings(); set.castle = set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 7a62753c4..05d4ec224 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -105,6 +105,7 @@ public: std::vector factions; std::vector heroes; + void genContentTitle(); void genContentCastles(PlayerSettings settings, PlayerInfo playerInfo); void genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo); void genContentBonus(PlayerSettings settings, PlayerInfo playerInfo); From 876dc28e48bd9ee7b3f049ee7ff2344978dc4926 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 16:49:42 +0200 Subject: [PATCH 0042/1248] rework --- client/lobby/OptionsTab.cpp | 66 +++++++++++++++++++++++-------------- client/lobby/OptionsTab.h | 15 +++++---- 2 files changed, 50 insertions(+), 31 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 34531af0d..ee2fbe59c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -415,26 +415,46 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(PlayerSettings settings, PlayerInfo playerInfo) +OptionsTab::SelectionWindow::SelectionWindow(int color) : CWindowObject(BORDERED) { addUsedEvents(LCLICK | SHOW_POPUP); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + color = color; + pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 58, 700); backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); genContentTitle(); - genContentCastles(settings, playerInfo); - genContentHeroes(settings, playerInfo); - genContentBonus(settings, playerInfo); + genContentCastles(); + genContentHeroes(); + genContentBonus(); center(); } +void OptionsTab::SelectionWindow::apply() +{ + if(GH.windows().isTopWindow(this)) + { + close(); + } +} + +void OptionsTab::SelectionWindow::redraw() +{ + components.clear(); + + genContentTitle(); + genContentCastles(); + genContentHeroes(); + genContentBonus(); +} + void OptionsTab::SelectionWindow::genContentTitle() { components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Town")); @@ -442,7 +462,7 @@ void OptionsTab::SelectionWindow::genContentTitle() components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 58 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Bonus")); } -void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, PlayerInfo playerInfo) +void OptionsTab::SelectionWindow::genContentCastles() { factions.clear(); @@ -451,8 +471,10 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + 34, 32 / 2 + 64)); + + int i = 0; - for(auto & elem : playerInfo.allowedFactions) + for(auto & elem : SEL->getPlayerInfo(0).allowedFactions) { int x = i % ELEMENTS_PER_LINE + 1; int y = i / ELEMENTS_PER_LINE + 2; @@ -469,7 +491,7 @@ void OptionsTab::SelectionWindow::genContentCastles(PlayerSettings settings, Pla } } -void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo) +void OptionsTab::SelectionWindow::genContentHeroes() { heroes.clear(); @@ -490,7 +512,7 @@ void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, Play { CHero * type = VLC->heroh->objects[elem]; - if(type->heroClass->faction == settings.castle) + if(type->heroClass->faction == SEL->getStartInfo()->playerInfos.find(PlayerColor(color))->second.castle) { int x = (i % ELEMENTS_PER_LINE) + ELEMENTS_PER_LINE + 2; @@ -509,7 +531,7 @@ void OptionsTab::SelectionWindow::genContentHeroes(PlayerSettings settings, Play } } -void OptionsTab::SelectionWindow::genContentBonus(PlayerSettings settings, PlayerInfo playerInfo) +void OptionsTab::SelectionWindow::genContentBonus() { PlayerSettings set = PlayerSettings(); @@ -527,14 +549,6 @@ void OptionsTab::SelectionWindow::genContentBonus(PlayerSettings settings, Playe } } -void OptionsTab::SelectionWindow::apply() -{ - if(GH.windows().isTopWindow(this)) - { - close(); - } -} - FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosition) { Point loc = getElement(cursorPosition, 0); @@ -605,8 +619,13 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(set.castle != -2) { + //SEL->getStartInfo()->playerInfos.find(PlayerColor(color))->second.castle = faction; + + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, PlayerColor(color)); + + redraw(); //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - apply(); + } else if(set.hero != -2) { @@ -648,10 +667,9 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } } -OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, PlayerInfo & playerInfo, SelType type) +OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) : Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL) , CPlayerSettingsHelper(settings, type) - , playerInfo(playerInfo) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -682,7 +700,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { - GH.windows().createAndPushWindow(settings, playerInfo); + GH.windows().createAndPushWindow(settings.color.getNum()); } void OptionsTab::SelectedBox::scrollBy(int distance) @@ -778,9 +796,9 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con else flag = nullptr; - town = std::make_shared(Point(119, 2), *s, *pi, TOWN); - hero = std::make_shared(Point(195, 2), *s, *pi, HERO); - bonus = std::make_shared(Point(271, 2), *s, *pi, BONUS); + town = std::make_shared(Point(119, 2), *s, TOWN); + hero = std::make_shared(Point(195, 2), *s, HERO); + bonus = std::make_shared(Point(271, 2), *s, BONUS); } void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 05d4ec224..080cd3cf5 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -99,6 +99,8 @@ public: { const int ELEMENTS_PER_LINE = 4; + ui8 color; + std::shared_ptr backgroundTexture; std::vector> components; @@ -106,11 +108,12 @@ public: std::vector heroes; void genContentTitle(); - void genContentCastles(PlayerSettings settings, PlayerInfo playerInfo); - void genContentHeroes(PlayerSettings settings, PlayerInfo playerInfo); - void genContentBonus(PlayerSettings settings, PlayerInfo playerInfo); + void genContentCastles(); + void genContentHeroes(); + void genContentBonus(); void apply(); + void redraw(); FactionID getElementCastle(const Point & cursorPosition); HeroTypeID getElementHero(const Point & cursorPosition); int getElementBonus(const Point & cursorPosition); @@ -120,18 +123,16 @@ public: void showPopupWindow(const Point & cursorPosition) override; public: - SelectionWindow(PlayerSettings settings, PlayerInfo playerInfo); + SelectionWindow(int color); }; /// Image with current town/hero/bonus struct SelectedBox : public Scrollable, public CPlayerSettingsHelper { - const PlayerInfo & playerInfo; - std::shared_ptr image; std::shared_ptr subtitle; - SelectedBox(Point position, PlayerSettings & settings, PlayerInfo & playerInfo, SelType type); + SelectedBox(Point position, PlayerSettings & settings, SelType type); void showPopupWindow(const Point & cursorPosition) override; void clickReleased(const Point & cursorPosition) override; void scrollBy(int distance) override; From c40c747ccff19c93833296482a34f2088f4dd0e6 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 17:46:35 +0200 Subject: [PATCH 0043/1248] saving states --- client/lobby/OptionsTab.cpp | 38 ++++++++++++++++++------------------- client/lobby/OptionsTab.h | 13 +++++++++++-- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index ee2fbe59c..628c6d7aa 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -415,24 +415,29 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(int color) +OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) : CWindowObject(BORDERED) { addUsedEvents(LCLICK | SHOW_POPUP); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - color = color; + color = _color; pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 58, 700); backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); - genContentTitle(); - genContentCastles(); - genContentHeroes(); - genContentBonus(); + initialFraction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; + initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; + initialBonus = SEL->getStartInfo()->playerInfos.find(color)->second.bonus; + selectedFraction = initialFraction; + selectedHero = selectedHero; + selectedBonus = selectedBonus; + allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; + + redraw(); center(); } @@ -442,6 +447,8 @@ void OptionsTab::SelectionWindow::apply() if(GH.windows().isTopWindow(this)) { close(); + + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, color); } } @@ -471,10 +478,8 @@ void OptionsTab::SelectionWindow::genContentCastles() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + 34, 32 / 2 + 64)); - - int i = 0; - for(auto & elem : SEL->getPlayerInfo(0).allowedFactions) + for(auto & elem : allowedFactions) { int x = i % ELEMENTS_PER_LINE + 1; int y = i / ELEMENTS_PER_LINE + 2; @@ -512,7 +517,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() { CHero * type = VLC->heroh->objects[elem]; - if(type->heroClass->faction == SEL->getStartInfo()->playerInfos.find(PlayerColor(color))->second.castle) + if(type->heroClass->faction == selectedFraction) { int x = (i % ELEMENTS_PER_LINE) + ELEMENTS_PER_LINE + 2; @@ -619,22 +624,17 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(set.castle != -2) { - //SEL->getStartInfo()->playerInfos.find(PlayerColor(color))->second.castle = faction; - - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, PlayerColor(color)); - + selectedFraction = set.castle; redraw(); - //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - } else if(set.hero != -2) { - //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + selectedHero = set.hero; apply(); } else if(set.bonus != -2) { - //CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + selectedBonus = set.bonus; apply(); } } @@ -700,7 +700,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { - GH.windows().createAndPushWindow(settings.color.getNum()); + GH.windows().createAndPushWindow(settings.color); } void OptionsTab::SelectedBox::scrollBy(int distance) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 080cd3cf5..826e8c11d 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -99,7 +99,7 @@ public: { const int ELEMENTS_PER_LINE = 4; - ui8 color; + PlayerColor color; std::shared_ptr backgroundTexture; std::vector> components; @@ -107,6 +107,15 @@ public: std::vector factions; std::vector heroes; + FactionID initialFraction; + HeroTypeID initialHero; + int initialBonus; + FactionID selectedFraction; + HeroTypeID selectedHero; + int selectedBonus; + + std::set allowedFactions; + void genContentTitle(); void genContentCastles(); void genContentHeroes(); @@ -123,7 +132,7 @@ public: void showPopupWindow(const Point & cursorPosition) override; public: - SelectionWindow(int color); + SelectionWindow(PlayerColor _color); }; /// Image with current town/hero/bonus From 481cd89dc8392b3246989f5bd66f900fc43a4da9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 18:20:44 +0200 Subject: [PATCH 0044/1248] drawing --- client/lobby/OptionsTab.cpp | 25 ++++++++++++++++++++++++- client/lobby/OptionsTab.h | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 628c6d7aa..1211ac9e1 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -448,14 +448,37 @@ void OptionsTab::SelectionWindow::apply() { close(); - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, color); + setSelection(); } } +void OptionsTab::SelectionWindow::setSelection() +{ + int selectedFractionPos = -1; + for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN, deltatown > 0 ? 1 : -1, color); +} + void OptionsTab::SelectionWindow::redraw() { + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + components.clear(); + GH.windows().totalRedraw(); + genContentTitle(); genContentCastles(); genContentHeroes(); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 826e8c11d..014f8d04b 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -123,6 +123,7 @@ public: void apply(); void redraw(); + void setSelection(); FactionID getElementCastle(const Point & cursorPosition); HeroTypeID getElementHero(const Point & cursorPosition); int getElementBonus(const Point & cursorPosition); From 8bb7fd31cddb64b5f28d32bf41d398526db1b3db Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:33:25 +0200 Subject: [PATCH 0045/1248] basically running... --- client/lobby/OptionsTab.cpp | 48 ++++++++++++++++++++++++++++++------- client/lobby/OptionsTab.h | 2 ++ 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 1211ac9e1..4a9e1f0db 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -436,6 +436,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) selectedHero = selectedHero; selectedBonus = selectedBonus; allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; + allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; redraw(); @@ -454,6 +455,7 @@ void OptionsTab::SelectionWindow::apply() void OptionsTab::SelectionWindow::setSelection() { + // fraction int selectedFractionPos = -1; for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN, deltatown > 0 ? 1 : -1, color); + if(deltaFraction != 0) + for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN, deltaFraction > 0 ? 1 : -1, color); + + // hero + int selectedHeroPos = -1; + for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::HERO, deltaHero > 0 ? 1 : -1, color); + + // bonus + //int deltaBonus = selectedBonus - initialBonus; + + //if(deltaBonus != 0) + // for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); } void OptionsTab::SelectionWindow::redraw() @@ -487,9 +514,9 @@ void OptionsTab::SelectionWindow::redraw() void OptionsTab::SelectionWindow::genContentTitle() { - components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Town")); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Hero")); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 58 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Bonus")); + components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 58 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); } void OptionsTab::SelectionWindow::genContentCastles() @@ -528,7 +555,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (ELEMENTS_PER_LINE + 1) * 58 + 34, 32 / 2 + 64)); - std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; + std::vector allowedHeroesFlag = allowedHeroes; std::set allowedHeroes; for(int i = 0; i < allowedHeroesFlag.size(); i++) @@ -648,7 +675,10 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(set.castle != -2) { selectedFraction = set.castle; - redraw(); + if(set.castle == -1) + apply(); + else + redraw(); } else if(set.hero != -2) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 014f8d04b..0b198b841 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -115,6 +115,8 @@ public: int selectedBonus; std::set allowedFactions; + std::vector allowedHeroes; + std::vector allowedBonus; void genContentTitle(); void genContentCastles(); From 92159e57efe5e308c087032eac9458cd52e86779 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 19:51:18 +0200 Subject: [PATCH 0046/1248] Update SelectionTab.cpp fix possible -1 --- client/lobby/SelectionTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index bb0f6adc6..0b3af5316 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -328,7 +328,7 @@ void SelectionTab::clickDouble(const Point & cursorPosition) if(itemIndex >= curItems.size()) return; - if(curItems[itemIndex]->isFolder) + if(itemIndex >= 0 && curItems[itemIndex]->isFolder) return; if(getLine() != -1) //double clicked scenarios list From 3d08ecf0249c1c61459e31cbfdf45a86305b9599 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:02:36 +0200 Subject: [PATCH 0047/1248] rename --- client/lobby/OptionsTab.cpp | 4 ++-- client/lobby/OptionsTab.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 4a9e1f0db..89bcbb84b 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -438,7 +438,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; - redraw(); + recreate(); center(); } @@ -678,7 +678,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(set.castle == -1) apply(); else - redraw(); + recreate(); } else if(set.hero != -2) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 0b198b841..f4ab96b95 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -124,7 +124,7 @@ public: void genContentBonus(); void apply(); - void redraw(); + void recreate(); void setSelection(); FactionID getElementCastle(const Point & cursorPosition); HeroTypeID getElementHero(const Point & cursorPosition); From 18b963f3769bc4fe2509b010e1771373a986aa59 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 20:31:00 +0200 Subject: [PATCH 0048/1248] fix --- client/lobby/OptionsTab.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 89bcbb84b..8a2c0220a 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -433,8 +433,8 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; initialBonus = SEL->getStartInfo()->playerInfos.find(color)->second.bonus; selectedFraction = initialFraction; - selectedHero = selectedHero; - selectedBonus = selectedBonus; + selectedHero = initialHero; + selectedBonus = initialBonus; allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; @@ -491,14 +491,14 @@ void OptionsTab::SelectionWindow::setSelection() CSH->setPlayerOption(LobbyChangePlayerOption::HERO, deltaHero > 0 ? 1 : -1, color); // bonus - //int deltaBonus = selectedBonus - initialBonus; + int deltaBonus = selectedBonus - initialBonus; - //if(deltaBonus != 0) - // for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); + if(deltaBonus != 0) + for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); } -void OptionsTab::SelectionWindow::redraw() +void OptionsTab::SelectionWindow::recreate() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; From b73f9d6e30c09a9aafc8acd2e685443bd4e2f826 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 21:50:40 +0200 Subject: [PATCH 0049/1248] calculate size --- client/lobby/OptionsTab.cpp | 44 ++++++++++++++++++++++++++----------- client/lobby/OptionsTab.h | 3 ++- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 8a2c0220a..4e149c048 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -424,11 +424,6 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) color = _color; - pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 58, 700); - - backgroundTexture = std::make_shared("DIBOXBCK", pos); - updateShadow(); - initialFraction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; initialBonus = SEL->getStartInfo()->playerInfos.find(color)->second.bonus; @@ -436,13 +431,43 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) selectedHero = initialHero; selectedBonus = initialBonus; allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; - allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; + std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; + for(int i = 0; i < allowedHeroesFlag.size(); i++) + if(allowedHeroesFlag[i]) + allowedHeroes.insert(HeroTypeID(i)); + + pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 58, calcHeight()); + + backgroundTexture = std::make_shared("DIBOXBCK", pos); + updateShadow(); recreate(); center(); } +int OptionsTab::SelectionWindow::calcHeight() +{ + // size count + int max = 0; + for(auto & elemf : allowedHeroes) + { + int count = 0; + for(auto & elemh : allowedHeroes) + { + CHero * type = VLC->heroh->objects[elemh]; + if(type->heroClass->faction == elemf) + count++; + } + max = std::max(max, count); + } + max = std::max(max, (int)allowedFactions.size()); + + int y = max / ELEMENTS_PER_LINE + 3; + + return y * 64; +} + void OptionsTab::SelectionWindow::apply() { if(GH.windows().isTopWindow(this)) @@ -555,13 +580,6 @@ void OptionsTab::SelectionWindow::genContentHeroes() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (ELEMENTS_PER_LINE + 1) * 58 + 34, 32 / 2 + 64)); - std::vector allowedHeroesFlag = allowedHeroes; - - std::set allowedHeroes; - for(int i = 0; i < allowedHeroesFlag.size(); i++) - if(allowedHeroesFlag[i]) - allowedHeroes.insert(HeroTypeID(i)); - int i = 0; for(auto & elem : allowedHeroes) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index f4ab96b95..9c746b597 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -115,7 +115,7 @@ public: int selectedBonus; std::set allowedFactions; - std::vector allowedHeroes; + std::set allowedHeroes; std::vector allowedBonus; void genContentTitle(); @@ -123,6 +123,7 @@ public: void genContentHeroes(); void genContentBonus(); + int calcHeight(); void apply(); void recreate(); void setSelection(); From f71f4a874b1bb1f56592ee18f049078da710a536 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:59:45 +0200 Subject: [PATCH 0050/1248] Grid and Activaed --- Mods/vcmi/Data/lobby/townBorderBig.png | Bin 0 -> 592 bytes .../Data/lobby/townBorderBigActivated.png | Bin 0 -> 4467 bytes .../Data/lobby/townBorderSmallActivated.png | Bin 0 -> 580 bytes client/lobby/OptionsTab.cpp | 55 +++++++++++++----- client/lobby/OptionsTab.h | 5 +- 5 files changed, 44 insertions(+), 16 deletions(-) create mode 100644 Mods/vcmi/Data/lobby/townBorderBig.png create mode 100644 Mods/vcmi/Data/lobby/townBorderBigActivated.png create mode 100644 Mods/vcmi/Data/lobby/townBorderSmallActivated.png diff --git a/Mods/vcmi/Data/lobby/townBorderBig.png b/Mods/vcmi/Data/lobby/townBorderBig.png new file mode 100644 index 0000000000000000000000000000000000000000..847dab8ed3b7a3d9fcc47356112d457c26f1dc96 GIT binary patch literal 592 zcmeAS@N?(olHy`uVBq!ia0vp^RzU2)!3HF~nNs>07#LeKot*}b)Z^&E;l9Lo%q4TI+k9~H4Ve%-dFW^earo%hA{eQ~P!MhDad&Kup`&|%)8Zmv7` zvt8kosVc2M=2RS6z%6876kGPQxANI()Zu5t^FJ5K3 z&pIOsOR<1fu9F{|{~vsxclOYa8M8(F81CwC`)kO;Qr?($ciaB{+})K-Eb|(**S=kE z*09q5((7E^GL|p7%;MK`eeW^rl{frTk2O+p=eqDkoa?}E$=YDPjyp`Y*?IYEYv-$k978hhy}i9rkime5$#Bl!@@?rJy>-m8rjt{GgP2R= kPtRIF7TE9Wl{b}r`WlwX>60~FfL1Yhy85}Sb4q9e0FY7s&j0`b literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/lobby/townBorderBigActivated.png b/Mods/vcmi/Data/lobby/townBorderBigActivated.png new file mode 100644 index 0000000000000000000000000000000000000000..0cfdd8a62a18a1f6580f8f2a57f6ef27bc9f8cac GIT binary patch literal 4467 zcmeHKX;c$e6n>M*IuId{sL+srEI|}Bfy#0M5g|aNr7TvVJ?w&FtH>U!5D3PysEA8Z zL@6qYMG9_cK~Ds+5v!$c1yKuV1r#G76hT6!GiqDw507pC^app&ee<3B?z!)M-@JJ< zFW2ACn~oSG006qg$1@PSyI?+_O2%4W!qNf&^awG*l30nv22imW08=oh0WBZ`2t&97 z=q2vO;VX=H5Fi-9SS%BB7{hojUYGnDX1&6A9z=LqX9I=_ukc!YILxRoF0>8p1o{iT zBwpA+_~2NB-Uc3kN+wgtBr1hMq0y*_E|adSqoZqNV5rA5G3IhijM;2c3p<|ajM?UF zwzcET*>m|04h~$Nv#XOpXlL&rz>7dM8ckP6cRHOuU0}vG6Z~UC&jAJ%n^7+e%>V=j z1T!GC32?D_lAss%k_t`~CX&b$Dh<)W5>D#lO2GsoOd=79SZ_Mk4~Ptsff?VOY#0zp znUTO0WEGZC%@-VPU6$OH*DE%BnUkBhb=&qGUlr}#RlINif%1cgDh_{h z?D&b3wWm&hSJ!yH>B9G_i_Mp=UB7Yj=hn7c?Opfo|N5Z2r}yEb!KXvRzl}T_9ea-R zg0P4BhrML>1}_H2i$Em8L<-IeBBbHO8AOs9pKRbBK#5E+oFT}fG8Yt<9c`eQ+XoJ? zq7ttnQ!O02EC+FFFPOa>G5Nn@_9wBwc(nsv7{VSN%m5#6lTg4@ zj)x|vFH}C&S{KA|dTQR9$PKL4DD(6t^WG5f|EEhzn{=8ge4sd8skQ8g9_JOIz*ci* z{F&?+f0MGxbKqnoC(hQ*BPc)BJ5tPCOd?59pc0K`qTseld%ZfMnmgKqg1OvrEZ&}S zQ?36*Q`eA~0bzrenE_((uFmR10W9ivMZqc*j0c{PpOtkk)2Ni{4iv0dV&^z& zFqRQ>eYEf~R#3|GSyG_EmgP>pi8C^_%Lp=vg3Yoa2nDAKCJu!L&$+Ma8Dj)>E!z?x zX7#fh#7uwaT|G=WV6Tnt80kmB$ypa?wQ9+-%WJiFQ7}@Tcwd$^r>Hwrl*_?NE|DiI zCiR%qV{&~=zAvxsFO&Pn-}l-{J^q6p30jwdIU1|d2o&TNv}v2WWk2ZIXG|#7y3z4{ zp$e9NNtxa9Cv>YLEnE~Rj8G87)%swcs!O6SFWC(h3bu+Sj-8d~^*&e2x-|^WkFEu8 zPjssfA5#)1^WGHjf2Yf1p-yM`=mIVFdt}J1O|Sj|1z&N(uIyH}JO9{!tsb%AP0CIx1*3FpO^!k$UfDvqC$wVZf94n4C@ zytB0xNy5&Zdp_Si=a@39BieHki0W5JEm{^6es)|=e;ChrA4oeWFcJD^V1WKtZ~n>xD@w)Y~izo cU0E5&>8cVWzq&oR7(X6M#D1RNE{Kr-4)z{!#{d8T literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/lobby/townBorderSmallActivated.png b/Mods/vcmi/Data/lobby/townBorderSmallActivated.png new file mode 100644 index 0000000000000000000000000000000000000000..5aa146d1cf5b78f583d519008f53aae666787a89 GIT binary patch literal 580 zcmeAS@N?(olHy`uVBq!ia0vp^20*O9!3HEluKbo^U|?*`baoE#baqxKD9TUE%t>Wn z(3n^|(bnUzgUr$R=%ub&d~X#VO;~6ZDIh8syM@bjg+Nwon5>W9-zGI)&#vyp526Y_ zByW86;L)1Z&8rnS>Kb0`(_rxJx3$gUWs@;?@#N)J=->&DV^2qktFCXpy=Snn;Md!=(pOftJCDIy3HTvzIc`C zKI@DmEX4v^xlVp;{(ta&-q}MxX3Q4xW4Noo?XMvVOL=43-EI5(b9YxVvCM1KUi)^v zS;I>IORsZv%UHhTGK*i&^}WZeSKjbXJ=RFYo$JCEajpZuC2NEEI_@ypX6NOvt({}X zAo)tGlvzfG>9FgmxbGF^D>iShKfmSIZ^rK$MR#Vg9M}Sk7q%pCcNd1s3`ZF9Yxe$G z3>4uk@Q5sCV9-+rVaAH3_GLi9U{4pvkch)?Zy53(FyL`LxPxo1fyVMA&gBLg3)0Ts zd+Gh%EZ^?r("DIBOXBCK", pos); updateShadow(); @@ -446,7 +447,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) center(); } -int OptionsTab::SelectionWindow::calcHeight() +int OptionsTab::SelectionWindow::calcLines() { // size count int max = 0; @@ -463,9 +464,9 @@ int OptionsTab::SelectionWindow::calcHeight() } max = std::max(max, (int)allowedFactions.size()); - int y = max / ELEMENTS_PER_LINE + 3; + int y = max / ELEMENTS_PER_LINE; - return y * 64; + return y; } void OptionsTab::SelectionWindow::apply() @@ -535,13 +536,26 @@ void OptionsTab::SelectionWindow::recreate() genContentCastles(); genContentHeroes(); genContentBonus(); + genContentGrid(); } void OptionsTab::SelectionWindow::genContentTitle() { - components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 58, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 58 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); + components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 57 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); +} + +void OptionsTab::SelectionWindow::genContentGrid() +{ + for(int y = 0; y("lobby/townBorderBig", (x + 1) * 57, (y + 2) * 63)); + components.push_back(std::make_shared("lobby/townBorderBig", (x + 2 + ELEMENTS_PER_LINE) * 57, (y + 2) * 63)); + } + } } void OptionsTab::SelectionWindow::genContentCastles() @@ -551,7 +565,9 @@ void OptionsTab::SelectionWindow::genContentCastles() PlayerSettings set = PlayerSettings(); set.castle = set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + 34, 32 / 2 + 64)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); + if(selectedFraction == set.RANDOM) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); int i = 0; for(auto & elem : allowedFactions) @@ -564,7 +580,8 @@ void OptionsTab::SelectionWindow::genContentCastles() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); + components.push_back(std::make_shared(selectedFraction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); factions.push_back(elem); i++; @@ -576,9 +593,14 @@ void OptionsTab::SelectionWindow::genContentHeroes() heroes.clear(); PlayerSettings set = PlayerSettings(); - set.castle = set.RANDOM; + set.castle = (initialHero == -2) ? set.NONE : set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 58 + (ELEMENTS_PER_LINE + 1) * 58 + 34, 32 / 2 + 64)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + (ELEMENTS_PER_LINE + 1) * 57 + 34, 32 / 2 + 63)); + if(selectedHero == set.RANDOM || initialHero == set.NONE) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", (ELEMENTS_PER_LINE / 2) * 57 + (ELEMENTS_PER_LINE + 1) * 57 + 34, 32 / 2 + 63)); + + if(initialHero == set.NONE) + return; int i = 0; for(auto & elem : allowedHeroes) @@ -596,7 +618,8 @@ void OptionsTab::SelectionWindow::genContentHeroes() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 58, y * 64)); + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); + components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); heroes.push_back(elem); i++; @@ -616,7 +639,9 @@ void OptionsTab::SelectionWindow::genContentBonus() set.bonus = elem; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 58, y * 64 + 32 / 2)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57, y * 63 + 32 / 2)); + if(selectedBonus == elem) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57, y * 63 + 32 / 2)); i++; } @@ -674,8 +699,8 @@ int OptionsTab::SelectionWindow::getElementBonus(const Point & cursorPosition) Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition, int area) { - int x = (cursorPosition.x - pos.x - area * (ELEMENTS_PER_LINE + 1) * 58) / 58; - int y = (cursorPosition.y - pos.y) / 64; + int x = (cursorPosition.x - pos.x - area * (ELEMENTS_PER_LINE + 1) * 57) / 57; + int y = (cursorPosition.y - pos.y) / 63; return Point(x - 1, y - 1); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 9c746b597..77a92db2f 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -118,12 +118,15 @@ public: std::set allowedHeroes; std::vector allowedBonus; + int amountLines; + void genContentTitle(); + void genContentGrid(); void genContentCastles(); void genContentHeroes(); void genContentBonus(); - int calcHeight(); + int calcLines(); void apply(); void recreate(); void setSelection(); From daa8bc547fd9ddbd04ae8c49d6433377defbfcb6 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 12 Aug 2023 23:28:53 +0200 Subject: [PATCH 0051/1248] icon fix --- client/lobby/OptionsTab.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index b38c2c5c6..08a6aaf73 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -563,10 +563,10 @@ void OptionsTab::SelectionWindow::genContentCastles() factions.clear(); PlayerSettings set = PlayerSettings(); - set.castle = set.RANDOM; + set.castle = (initialFraction == set.NONE) ? set.NONE : set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); - if(selectedFraction == set.RANDOM) + if(selectedFraction == set.RANDOM || initialFraction == set.NONE) components.push_back(std::make_shared("lobby/townBorderSmallActivated", (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); int i = 0; @@ -593,7 +593,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() heroes.clear(); PlayerSettings set = PlayerSettings(); - set.castle = (initialHero == -2) ? set.NONE : set.RANDOM; + set.hero = (initialHero == set.NONE) ? set.NONE : set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + (ELEMENTS_PER_LINE + 1) * 57 + 34, 32 / 2 + 63)); if(selectedHero == set.RANDOM || initialHero == set.NONE) From 4b1224ec8ccf05d2d228a19e3dcc9c6e1c6e4e44 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 13 Aug 2023 14:06:35 +0400 Subject: [PATCH 0052/1248] Implement turn timer feature --- client/CMakeLists.txt | 2 + client/ClientNetPackVisitors.h | 1 + client/NetPacksClient.cpp | 5 ++ client/adventureMap/AdventureMapInterface.cpp | 5 ++ client/adventureMap/AdventureMapInterface.h | 2 + client/adventureMap/AdventureMapShortcuts.cpp | 2 +- client/adventureMap/TurnTimerWidget.cpp | 73 +++++++++++++++++++ client/adventureMap/TurnTimerWidget.h | 39 ++++++++++ lib/CGameInfoCallback.cpp | 16 ++++ lib/CGameInfoCallback.h | 1 + lib/CPlayerState.h | 2 + lib/NetPackVisitor.h | 1 + lib/NetPacks.h | 14 ++++ lib/NetPacksLib.cpp | 6 ++ lib/registerTypes/RegisterTypes.h | 1 + server/CGameHandler.cpp | 31 +++++++- 16 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 client/adventureMap/TurnTimerWidget.cpp create mode 100644 client/adventureMap/TurnTimerWidget.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2121e54f2..b18ccb36e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -12,6 +12,7 @@ set(client_SRCS adventureMap/CMinimap.cpp adventureMap/CResDataBar.cpp adventureMap/MapAudioPlayer.cpp + adventureMap/TurnTimerWidget.cpp battle/BattleActionsController.cpp battle/BattleAnimationClasses.cpp @@ -157,6 +158,7 @@ set(client_HEADERS adventureMap/CMinimap.h adventureMap/CResDataBar.h adventureMap/MapAudioPlayer.h + adventureMap/TurnTimerWidget.h battle/BattleActionsController.h battle/BattleAnimationClasses.h diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index c997f0896..a740e5731 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -94,6 +94,7 @@ public: void visitSystemMessage(SystemMessage & pack) override; void visitPlayerBlocked(PlayerBlocked & pack) override; void visitYourTurn(YourTurn & pack) override; + void visitTurnTimeUpdate(TurnTimeUpdate & pack) override; void visitPlayerMessageClient(PlayerMessageClient & pack) override; void visitAdvmapSpellCast(AdvmapSpellCast & pack) override; void visitShowWorldViewEx(ShowWorldViewEx & pack) override; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index c2f89316f..792d541c9 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -864,6 +864,11 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn); } +void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) +{ + logNetwork->debug("Server sets %d turn time for %s", pack.turnTime, pack.player.getStr()); +} + void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) { logNetwork->debug("pack.player %s sends a message: %s", pack.player.getStr(), pack.text); diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 45ea69a63..b61f5d386 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -17,6 +17,7 @@ #include "CList.h" #include "CInfoBar.h" #include "MapAudioPlayer.h" +#include "TurnTimerWidget.h" #include "AdventureMapWidget.h" #include "AdventureMapShortcuts.h" @@ -35,6 +36,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/StartInfo.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -61,6 +63,9 @@ AdventureMapInterface::AdventureMapInterface(): shortcuts->setState(EAdventureState::MAKING_TURN); widget->getMapView()->onViewMapActivated(); + if(LOCPLINT->cb->getStartInfo()->turnTime > 0) + watches = std::make_shared(); + addUsedEvents(KEYBOARD | TIME); } diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 6f69b2cdd..55546b6a3 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -39,6 +39,7 @@ class CTownList; class CInfoBar; class CMinimap; class MapAudioPlayer; +class TurnTimerWidget; enum class EAdventureState; struct MapDrawingInfo; @@ -64,6 +65,7 @@ private: std::shared_ptr mapAudio; std::shared_ptr widget; std::shared_ptr shortcuts; + std::shared_ptr watches; private: void setState(EAdventureState state); diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 3fb44f0dc..19ee7b455 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -314,7 +314,7 @@ void AdventureMapShortcuts::visitObject() const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero(); if(h) - LOCPLINT->cb->moveHero(h,h->pos); + LOCPLINT->cb->moveHero(h, h->pos); } void AdventureMapShortcuts::openObject() diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp new file mode 100644 index 000000000..8c694af0e --- /dev/null +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -0,0 +1,73 @@ +/* + * TurnTimerWidget.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 "TurnTimerWidget.h" + +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/EFont.h" +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../../CCallback.h" +#include "../../lib/CPlayerState.h" + +#include + +TurnTimerWidget::TurnTimerWidget(): + CIntObject(TIME), + turnTime(0), lastTurnTime(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + watches = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL", 1, 0, 4, 6); + label = std::make_shared(24, 2, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, std::to_string(turnTime)); + + recActions &= ~DEACTIVATE; +} + +void TurnTimerWidget::showAll(Canvas & to) +{ + to.drawColor(Rect(4, 4, 68, 24), ColorRGBA(10, 10, 10, 255)); + + CIntObject::showAll(to); +} + +void TurnTimerWidget::show(Canvas & to) +{ + showAll(to); +} + +void TurnTimerWidget::setTime(int time) +{ + turnTime = time / 1000; + std::ostringstream oss; + oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; + label->setText(oss.str()); +} + +void TurnTimerWidget::tick(uint32_t msPassed) +{ + if(LOCPLINT && LOCPLINT->cb && !LOCPLINT->battleInt) + { + auto player = LOCPLINT->cb->getCurrentPlayer(); + auto time = LOCPLINT->cb->getPlayerTurnTime(player); + cachedTurnTime -= msPassed; + if(time / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time; + cachedTurnTime = time; + } + else setTime(cachedTurnTime); + } +} diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h new file mode 100644 index 000000000..1ef505425 --- /dev/null +++ b/client/adventureMap/TurnTimerWidget.h @@ -0,0 +1,39 @@ +/* + * TurnTimerWidget.hpp, 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 "../gui/CIntObject.h" + +class CAnimImage; +class CLabel; + +class TurnTimerWidget : public CIntObject +{ +private: + + int turnTime; + int lastTurnTime; + int cachedTurnTime; + + std::shared_ptr watches; + std::shared_ptr label; + +public: + + //void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; + + void setTime(int time); + + TurnTimerWidget(); +}; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 2a44004c1..8f52a0e32 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -99,6 +99,22 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve } } +int CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const +{ + if(!color.isValidPlayer()) + { + return 0; + } + + auto player = gs->players.find(color); + if(player != gs->players.end()) + { + return player->second.turnTime; + } + + return 0; +} + const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const { if(gs->map->questIdentifierToId.empty()) diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 8bca8a99d..e7c7db6bf 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -153,6 +153,7 @@ public: virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player) virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; + virtual int getPlayerTurnTime(PlayerColor color) const; //map virtual bool isVisible(int3 pos, const std::optional & Player) const; diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index cfb99617c..8c8dbf843 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -39,6 +39,7 @@ public: bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory EPlayerStatus::EStatus status; std::optional daysWithoutCastle; + int turnTime = 0; PlayerState(); PlayerState(PlayerState && other) noexcept; @@ -71,6 +72,7 @@ public: h & team; h & resources; h & status; + h & turnTime; h & heroes; h & towns; h & dwellings; diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index da1c6e2e6..5bc548d56 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -27,6 +27,7 @@ public: virtual void visitPlayerBlocked(PlayerBlocked & pack) {} virtual void visitPlayerCheated(PlayerCheated & pack) {} virtual void visitYourTurn(YourTurn & pack) {} + virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} virtual void visitSetResources(SetResources & pack) {} virtual void visitSetPrimSkill(SetPrimSkill & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 735737da5..b0270fb8a 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -147,6 +147,20 @@ struct DLL_LINKAGE PlayerCheated : public CPackForClient } }; +struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + int turnTime = 0; + + template void serialize(Handler & h, const int version) + { + h & player; + h & turnTime; + } +}; + struct DLL_LINKAGE YourTurn : public CPackForClient { void applyGs(CGameState * gs) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5dc6e5d56..3b4acd477 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2513,6 +2513,12 @@ void YourTurn::applyGs(CGameState * gs) const playerState.daysWithoutCastle = daysWithoutCastle; } +void TurnTimeUpdate::applyGs(CGameState *gs) const +{ + auto & playerState = gs->players[player]; + playerState.turnTime = turnTime; +} + Component::Component(const CStackBasicDescriptor & stack) : id(EComponentType::CREATURE) , subtype(stack.type->getId()) diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 08db1d259..fd15d6f1f 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -232,6 +232,7 @@ void registerTypesClientPacks1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 748ef73ee..8df34e14a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2067,6 +2067,14 @@ void CGameHandler::run(bool resume) //Change local daysWithoutCastle counter for local interface message //TODO: needed? yt.daysWithoutCastle = playerState->daysWithoutCastle; applyAndSend(&yt); + + if(gs->getStartInfo()->turnTime > 0) //turn timer check + { + TurnTimeUpdate ttu; + ttu.player = player; + ttu.turnTime = gs->getStartInfo()->turnTime * 60 * 1000; //ms + applyAndSend(&ttu); + } } }; @@ -2075,10 +2083,31 @@ void CGameHandler::run(bool resume) if(playerColor != PlayerColor::CANNOT_DETERMINE) { //wait till turn is done + const int waitTime = 100; //ms + int turnTimePropagateFrequency = 5000; //do not send updates too frequently boost::unique_lock lock(states.mx); while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) { - static time_duration p = milliseconds(100); + if(gs->getStartInfo()->turnTime > 0 && !gs->curB) //turn timer check + { + if(gs->players[playerColor].turnTime > 0) + { + gs->players[playerColor].turnTime -= waitTime; + + if(gs->players[playerColor].status == EPlayerStatus::INGAME //do not send message if player is not active already + && gs->players[playerColor].turnTime % turnTimePropagateFrequency == 0) + { + TurnTimeUpdate ttu; + ttu.player = playerColor; + ttu.turnTime = gs->players[playerColor].turnTime; + applyAndSend(&ttu); + } + } + else if(!queries.topQuery(playerColor)) //wait for replies to avoid pending queries + states.players.at(playerColor).makingTurn = false; //force end turn + } + + static time_duration p = milliseconds(waitTime); states.cv.timed_wait(lock, p); } } From c14448ee32abbfe44b96354367b5b7d3eb0908b6 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Aug 2023 15:09:48 +0200 Subject: [PATCH 0053/1248] change design --- Mods/vcmi/Data/DlgBluBk.png | Bin 0 -> 74379 bytes client/lobby/OptionsTab.cpp | 80 +++++++++++++++++++++++------------- client/lobby/OptionsTab.h | 10 ++--- 3 files changed, 55 insertions(+), 35 deletions(-) create mode 100644 Mods/vcmi/Data/DlgBluBk.png diff --git a/Mods/vcmi/Data/DlgBluBk.png b/Mods/vcmi/Data/DlgBluBk.png new file mode 100644 index 0000000000000000000000000000000000000000..acbc2499790bbefaef06203e115ee31ea46af484 GIT binary patch literal 74379 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%0D2;8ZPJyUSFGi2)4B2gJr&O8YsjwD)+F&Dz> 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, Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) +OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType type) : CWindowObject(BORDERED) { addUsedEvents(LCLICK | SHOW_POPUP); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - color = _color; initialFraction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; @@ -436,22 +434,30 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color) if(allowedHeroesFlag[i]) allowedHeroes.insert(HeroTypeID(i)); - amountLines = calcLines(); - pos = Rect(0, 0, (ELEMENTS_PER_LINE * 2 + 5) * 57, (amountLines + 3) * 63); + recreate(type); +} - backgroundTexture = std::make_shared("DIBOXBCK", pos); - updateShadow(); +int OptionsTab::SelectionWindow::calcLines(FactionID faction) +{ + if(faction < 0) + return std::ceil((double)allowedFactions.size() / ELEMENTS_PER_LINE); - recreate(); + int count = 0; + for(auto & elemh : allowedHeroes) + { + CHero * type = VLC->heroh->objects[elemh]; + if(type->heroClass->faction == faction) + count++; + } - center(); + return std::ceil(std::max((double)count, (double)allowedFactions.size()) / (double)ELEMENTS_PER_LINE); } int OptionsTab::SelectionWindow::calcLines() { // size count int max = 0; - for(auto & elemf : allowedHeroes) + for(auto & elemf : allowedFactions) { int count = 0; for(auto & elemh : allowedHeroes) @@ -464,7 +470,7 @@ int OptionsTab::SelectionWindow::calcLines() } max = std::max(max, (int)allowedFactions.size()); - int y = max / ELEMENTS_PER_LINE; + int y = std::ceil((double)max / (double)ELEMENTS_PER_LINE); return y; } @@ -524,36 +530,46 @@ void OptionsTab::SelectionWindow::setSelection() CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); } -void OptionsTab::SelectionWindow::recreate() +void OptionsTab::SelectionWindow::recreate(SelType type) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + int amountLinesMax = calcLines(); + int amountLines = calcLines((type > SelType::TOWN) ? selectedFraction : static_cast(-1)); + + int xMax = (ELEMENTS_PER_LINE * 2 + 5) * 57; + int x = (type == SelType::TOWN) ? (ELEMENTS_PER_LINE + 2) * 57 : xMax; + int yMax = (amountLinesMax + 2) * 63 + 20; + int y = (amountLines + 2) * 63 + 20; + + pos = Rect(0, 0, x, y); + + backgroundTexture = std::make_shared("DlgBluBk", pos); + updateShadow(); + components.clear(); GH.windows().totalRedraw(); - genContentTitle(); genContentCastles(); - genContentHeroes(); - genContentBonus(); - genContentGrid(); + if(type > SelType::TOWN) { + genContentHeroes(); + genContentBonus(); + } + genContentGrid(type == SelType::TOWN, amountLines); + + center(Point((GH.screenDimensions().x / 2) - ((xMax - x) / 2), (GH.screenDimensions().y / 2) - ((yMax - y) / 2))); } -void OptionsTab::SelectionWindow::genContentTitle() +void OptionsTab::SelectionWindow::genContentGrid(bool small, int lines) { - components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 57 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); -} - -void OptionsTab::SelectionWindow::genContentGrid() -{ - for(int y = 0; y("lobby/townBorderBig", (x + 1) * 57, (y + 2) * 63)); - components.push_back(std::make_shared("lobby/townBorderBig", (x + 2 + ELEMENTS_PER_LINE) * 57, (y + 2) * 63)); + if(!small) + components.push_back(std::make_shared("lobby/townBorderBig", (x + 2 + ELEMENTS_PER_LINE) * 57, (y + 2) * 63)); } } } @@ -562,6 +578,8 @@ void OptionsTab::SelectionWindow::genContentCastles() { factions.clear(); + components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); + PlayerSettings set = PlayerSettings(); set.castle = (initialFraction == set.NONE) ? set.NONE : set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); @@ -592,6 +610,8 @@ void OptionsTab::SelectionWindow::genContentHeroes() { heroes.clear(); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); + PlayerSettings set = PlayerSettings(); set.hero = (initialHero == set.NONE) ? set.NONE : set.RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); @@ -631,6 +651,8 @@ void OptionsTab::SelectionWindow::genContentBonus() { PlayerSettings set = PlayerSettings(); + components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 57 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); + int i = 0; for(auto elem : {set.RANDOM, set.ARTIFACT, set.GOLD, set.RESOURCE}) { @@ -721,7 +743,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(set.castle == -1) apply(); else - recreate(); + recreate(SelType::HERO); } else if(set.hero != -2) { @@ -731,7 +753,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { else if(set.bonus != -2) { selectedBonus = set.bonus; - apply(); + recreate(SelType::BONUS); } } @@ -796,7 +818,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { - GH.windows().createAndPushWindow(settings.color); + GH.windows().createAndPushWindow(settings.color, type); } void OptionsTab::SelectedBox::scrollBy(int distance) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 77a92db2f..00196b233 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -118,17 +118,15 @@ public: std::set allowedHeroes; std::vector allowedBonus; - int amountLines; - - void genContentTitle(); - void genContentGrid(); + void genContentGrid(bool small, int lines); void genContentCastles(); void genContentHeroes(); void genContentBonus(); + int calcLines(FactionID faction); int calcLines(); void apply(); - void recreate(); + void recreate(SelType type); void setSelection(); FactionID getElementCastle(const Point & cursorPosition); HeroTypeID getElementHero(const Point & cursorPosition); @@ -139,7 +137,7 @@ public: void showPopupWindow(const Point & cursorPosition) override; public: - SelectionWindow(PlayerColor _color); + SelectionWindow(PlayerColor _color, SelType type); }; /// Image with current town/hero/bonus From e66a41e90bcc48f20453661732d03d7476c4bf1b Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Aug 2023 19:23:27 +0200 Subject: [PATCH 0054/1248] simplify --- client/lobby/OptionsTab.cpp | 283 +++++++++++++++--------------------- client/lobby/OptionsTab.h | 15 +- 2 files changed, 125 insertions(+), 173 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 3ebcc9870..1960c7207 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -27,6 +27,7 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../eventsSDL/InputHandler.h" #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" @@ -415,12 +416,13 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow() textBonusDescription = std::make_shared(getDescription(), Rect(10, 100, pos.w - 20, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } -OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType type) +OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) : CWindowObject(BORDERED) { addUsedEvents(LCLICK | SHOW_POPUP); color = _color; + type = _type; initialFraction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; @@ -434,7 +436,14 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType type) if(allowedHeroesFlag[i]) allowedHeroes.insert(HeroTypeID(i)); - recreate(type); + allowedBonus.push_back(-1); // random + if(initialHero >= -1) + allowedBonus.push_back(0); // artifact + allowedBonus.push_back(1); // gold + if(initialFraction >= 0) + allowedBonus.push_back(2); // resource + + recreate(); } int OptionsTab::SelectionWindow::calcLines(FactionID faction) @@ -453,32 +462,12 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) return std::ceil(std::max((double)count, (double)allowedFactions.size()) / (double)ELEMENTS_PER_LINE); } -int OptionsTab::SelectionWindow::calcLines() -{ - // size count - int max = 0; - for(auto & elemf : allowedFactions) - { - int count = 0; - for(auto & elemh : allowedHeroes) - { - CHero * type = VLC->heroh->objects[elemh]; - if(type->heroClass->faction == elemf) - count++; - } - max = std::max(max, count); - } - max = std::max(max, (int)allowedFactions.size()); - - int y = std::ceil((double)max / (double)ELEMENTS_PER_LINE); - - return y; -} - void OptionsTab::SelectionWindow::apply() { if(GH.windows().isTopWindow(this)) { + GH.input().hapticFeedback(); + close(); setSelection(); @@ -503,6 +492,14 @@ void OptionsTab::SelectionWindow::setSelection() if(deltaFraction != 0) for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN, deltaFraction > 0 ? 1 : -1, color); + else + { + if(type == SelType::TOWN) + { + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, color); + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, -1, color); + } + } // hero int selectedHeroPos = -1; @@ -525,22 +522,29 @@ void OptionsTab::SelectionWindow::setSelection() // bonus int deltaBonus = selectedBonus - initialBonus; + if(initialHero < -1 && ((selectedBonus > 0 && initialBonus < 0) || (selectedBonus < 0 && initialBonus > 0))) // no artifact + { + if(deltaBonus > 0) + deltaBonus--; + else if(deltaBonus < 0) + deltaBonus++; + } + if(deltaBonus != 0) for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); } -void OptionsTab::SelectionWindow::recreate(SelType type) +void OptionsTab::SelectionWindow::recreate() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - int amountLinesMax = calcLines(); int amountLines = calcLines((type > SelType::TOWN) ? selectedFraction : static_cast(-1)); + if(type == SelType::BONUS) + amountLines = 1; - int xMax = (ELEMENTS_PER_LINE * 2 + 5) * 57; - int x = (type == SelType::TOWN) ? (ELEMENTS_PER_LINE + 2) * 57 : xMax; - int yMax = (amountLinesMax + 2) * 63 + 20; - int y = (amountLines + 2) * 63 + 20; + int x = (ELEMENTS_PER_LINE) * 57; + int y = (amountLines) * 63; pos = Rect(0, 0, x, y); @@ -551,47 +555,36 @@ void OptionsTab::SelectionWindow::recreate(SelType type) GH.windows().totalRedraw(); - genContentCastles(); - if(type > SelType::TOWN) { + if(type == SelType::TOWN) + genContentCastles(); + if(type == SelType::HERO) genContentHeroes(); + if(type == SelType::BONUS) genContentBonus(); - } - genContentGrid(type == SelType::TOWN, amountLines); + genContentGrid(amountLines); - center(Point((GH.screenDimensions().x / 2) - ((xMax - x) / 2), (GH.screenDimensions().y / 2) - ((yMax - y) / 2))); + center(); } -void OptionsTab::SelectionWindow::genContentGrid(bool small, int lines) +void OptionsTab::SelectionWindow::genContentGrid(int lines) { for(int y = 0; y("lobby/townBorderBig", (x + 1) * 57, (y + 2) * 63)); - if(!small) - components.push_back(std::make_shared("lobby/townBorderBig", (x + 2 + ELEMENTS_PER_LINE) * 57, (y + 2) * 63)); + components.push_back(std::make_shared("lobby/townBorderBig", x * 57, y * 63)); } } } void OptionsTab::SelectionWindow::genContentCastles() { - factions.clear(); - - components.push_back(std::make_shared((ELEMENTS_PER_LINE - 1) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.518"))); - - PlayerSettings set = PlayerSettings(); - set.castle = (initialFraction == set.NONE) ? set.NONE : set.RANDOM; - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); - if(selectedFraction == set.RANDOM || initialFraction == set.NONE) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", (ELEMENTS_PER_LINE / 2) * 57 + 34, 32 / 2 + 63)); - int i = 0; + for(auto & elem : allowedFactions) { - int x = i % ELEMENTS_PER_LINE + 1; - int y = i / ELEMENTS_PER_LINE + 2; + int x = i % ELEMENTS_PER_LINE; + int y = i / ELEMENTS_PER_LINE; PlayerSettings set = PlayerSettings(); set.castle = elem; @@ -608,21 +601,8 @@ void OptionsTab::SelectionWindow::genContentCastles() void OptionsTab::SelectionWindow::genContentHeroes() { - heroes.clear(); - - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2) * 57, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.519"))); - - PlayerSettings set = PlayerSettings(); - set.hero = (initialHero == set.NONE) ? set.NONE : set.RANDOM; - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, (ELEMENTS_PER_LINE / 2) * 57 + (ELEMENTS_PER_LINE + 1) * 57 + 34, 32 / 2 + 63)); - if(selectedHero == set.RANDOM || initialHero == set.NONE) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", (ELEMENTS_PER_LINE / 2) * 57 + (ELEMENTS_PER_LINE + 1) * 57 + 34, 32 / 2 + 63)); - - if(initialHero == set.NONE) - return; - int i = 0; + for(auto & elem : allowedHeroes) { CHero * type = VLC->heroh->objects[elem]; @@ -630,8 +610,8 @@ void OptionsTab::SelectionWindow::genContentHeroes() if(type->heroClass->faction == selectedFraction) { - int x = (i % ELEMENTS_PER_LINE) + ELEMENTS_PER_LINE + 2; - int y = i / ELEMENTS_PER_LINE + 2; + int x = i % ELEMENTS_PER_LINE; + int y = i / ELEMENTS_PER_LINE; PlayerSettings set = PlayerSettings(); set.hero = elem; @@ -651,137 +631,99 @@ void OptionsTab::SelectionWindow::genContentBonus() { PlayerSettings set = PlayerSettings(); - components.push_back(std::make_shared((ELEMENTS_PER_LINE * 2 + 3) * 57 + 29, 40, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("core.genrltxt.520"))); - int i = 0; - for(auto elem : {set.RANDOM, set.ARTIFACT, set.GOLD, set.RESOURCE}) + for(auto elem : allowedBonus) { - int x = ELEMENTS_PER_LINE * 2 + 3; - int y = i + 1; + int x = i; + int y = 0; - set.bonus = elem; + set.bonus = static_cast(elem); CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57, y * 63 + 32 / 2)); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57 + 6, y * 63 + 32 / 2)); if(selectedBonus == elem) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57, y * 63 + 32 / 2)); + if(elem == set.RESOURCE && selectedFraction >= 0) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); i++; } } -FactionID OptionsTab::SelectionWindow::getElementCastle(const Point & cursorPosition) +int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) { - Point loc = getElement(cursorPosition, 0); - - FactionID faction; - faction = PlayerSettings().NONE; - if ((loc.x == ELEMENTS_PER_LINE / 2 || loc.x == ELEMENTS_PER_LINE / 2 - 1) && loc.y == 0) - faction = PlayerSettings().RANDOM; - else if(loc.y > 0 && loc.x < ELEMENTS_PER_LINE) - { - int index = loc.x + (loc.y - 1) * ELEMENTS_PER_LINE; - if (index < factions.size()) - faction = factions[loc.x + (loc.y - 1) * ELEMENTS_PER_LINE]; - } - - return faction; -} - -HeroTypeID OptionsTab::SelectionWindow::getElementHero(const Point & cursorPosition) -{ - Point loc = getElement(cursorPosition, 1); - - HeroTypeID hero; - hero = PlayerSettings().NONE; - - if(loc.x < 0) - return hero; - - if ((loc.x == ELEMENTS_PER_LINE / 2 || loc.x == ELEMENTS_PER_LINE / 2 - 1) && loc.y == 0) - hero = PlayerSettings().RANDOM; - else if(loc.y > 0 && loc.x < ELEMENTS_PER_LINE) - { - int index = loc.x + (loc.y - 1) * ELEMENTS_PER_LINE; - if (index < heroes.size()) - hero = heroes[loc.x + (loc.y - 1) * ELEMENTS_PER_LINE]; - } - - return hero; -} - -int OptionsTab::SelectionWindow::getElementBonus(const Point & cursorPosition) -{ - Point loc = getElement(cursorPosition, 2); - - if(loc.x != 0 || loc.y < 0 || loc.y > 3) - return -2; - - return loc.y - 1; -} - -Point OptionsTab::SelectionWindow::getElement(const Point & cursorPosition, int area) -{ - int x = (cursorPosition.x - pos.x - area * (ELEMENTS_PER_LINE + 1) * 57) / 57; + int x = (cursorPosition.x - pos.x) / 57; int y = (cursorPosition.y - pos.y) / 63; - return Point(x - 1, y - 1); + return x + y * ELEMENTS_PER_LINE; } void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { - FactionID faction = getElementCastle(cursorPosition); - HeroTypeID hero = getElementHero(cursorPosition); - int bonus = getElementBonus(cursorPosition); + int elem = getElement(cursorPosition); PlayerSettings set = PlayerSettings(); - set.castle = faction; - set.hero = hero; - set.bonus = static_cast(bonus); - - if(set.castle != -2) + if(type == SelType::TOWN) { - selectedFraction = set.castle; - if(set.castle == -1) - apply(); - else - recreate(SelType::HERO); + if(elem >= factions.size()) + return; + set.castle = factions[elem]; + if(set.castle != -2) + selectedFraction = set.castle; } - else if(set.hero != -2) + if(type == SelType::HERO) { - selectedHero = set.hero; - apply(); + if(elem >= heroes.size()) + return; + set.hero = heroes[elem]; + if(set.hero != -2) + selectedHero = set.hero; } - else if(set.bonus != -2) + if(type == SelType::BONUS) { - selectedBonus = set.bonus; - recreate(SelType::BONUS); + if(elem >= allowedBonus.size()) + return; + set.bonus = static_cast(allowedBonus[elem]); + if(set.bonus != -2) + selectedBonus = set.bonus; } + apply(); } void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) { - FactionID faction = getElementCastle(cursorPosition); - HeroTypeID hero = getElementHero(cursorPosition); - int bonus = getElementBonus(cursorPosition); + int elem = getElement(cursorPosition); PlayerSettings set = PlayerSettings(); - set.castle = faction; - set.hero = hero; - set.bonus = static_cast(bonus); - - if(set.castle != -2) + if(type == SelType::TOWN) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - GH.windows().createAndPushWindow(helper); + if(elem >= factions.size()) + return; + set.castle = factions[elem]; + if(set.castle != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + GH.windows().createAndPushWindow(helper); + } } - else if(set.hero != -2) + if(type == SelType::HERO) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - GH.windows().createAndPushWindow(helper); + if(elem >= heroes.size()) + return; + set.hero = heroes[elem]; + if(set.hero != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + GH.windows().createAndPushWindow(helper); + } } - else if(set.bonus != -2) + if(type == SelType::BONUS) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - GH.windows().createAndPushWindow(helper); + if(elem >= 4) + return; + set.bonus = static_cast(elem-1); + if(set.bonus != -2) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + GH.windows().createAndPushWindow(helper); + } } } @@ -818,6 +760,19 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { + PlayerInfo pi = SEL->getPlayerInfo(settings.color.getNum()); + const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(settings.color); + + if(type == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) + return; + + if(type == SelType::HERO && ((pi.defaultHero() != -1 || settings.castle < 0) || foreignPlayer)) + return; + + if(type == SelType::BONUS && foreignPlayer) + return; + + GH.input().hapticFeedback(); GH.windows().createAndPushWindow(settings.color, type); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 00196b233..380f39885 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -100,6 +100,7 @@ public: const int ELEMENTS_PER_LINE = 4; PlayerColor color; + SelType type; std::shared_ptr backgroundTexture; std::vector> components; @@ -116,28 +117,24 @@ public: std::set allowedFactions; std::set allowedHeroes; - std::vector allowedBonus; + std::vector allowedBonus; - void genContentGrid(bool small, int lines); + void genContentGrid(int lines); void genContentCastles(); void genContentHeroes(); void genContentBonus(); int calcLines(FactionID faction); - int calcLines(); void apply(); - void recreate(SelType type); + void recreate(); void setSelection(); - FactionID getElementCastle(const Point & cursorPosition); - HeroTypeID getElementHero(const Point & cursorPosition); - int getElementBonus(const Point & cursorPosition); - Point getElement(const Point & cursorPosition, int area); + int getElement(const Point & cursorPosition); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; public: - SelectionWindow(PlayerColor _color, SelType type); + SelectionWindow(PlayerColor _color, SelType _type); }; /// Image with current town/hero/bonus From 9a63161866bd17033d3381b71f628636d903b6a5 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Aug 2023 20:21:09 +0200 Subject: [PATCH 0055/1248] added random; added names --- client/lobby/OptionsTab.cpp | 60 ++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 1960c7207..ab977294b 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -448,8 +448,10 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) int OptionsTab::SelectionWindow::calcLines(FactionID faction) { + double additionalItems = 1; // random + if(faction < 0) - return std::ceil((double)allowedFactions.size() / ELEMENTS_PER_LINE); + return std::ceil(((double)allowedFactions.size() + additionalItems) / ELEMENTS_PER_LINE); int count = 0; for(auto & elemh : allowedHeroes) @@ -459,7 +461,7 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) count++; } - return std::ceil(std::max((double)count, (double)allowedFactions.size()) / (double)ELEMENTS_PER_LINE); + return std::ceil(std::max((double)count + additionalItems, (double)allowedFactions.size() + additionalItems) / (double)ELEMENTS_PER_LINE); } void OptionsTab::SelectionWindow::apply() @@ -579,7 +581,16 @@ void OptionsTab::SelectionWindow::genContentGrid(int lines) void OptionsTab::SelectionWindow::genContentCastles() { - int i = 0; + int i = 1; + + // random + PlayerSettings set = PlayerSettings(); + set.castle = -1; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); + components.push_back(std::make_shared(29, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + if(selectedFraction == -1) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedFactions) { @@ -593,6 +604,7 @@ void OptionsTab::SelectionWindow::genContentCastles() components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); components.push_back(std::make_shared(selectedFraction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); + components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); factions.push_back(elem); i++; @@ -601,7 +613,16 @@ void OptionsTab::SelectionWindow::genContentCastles() void OptionsTab::SelectionWindow::genContentHeroes() { - int i = 0; + int i = 1; + + // random + PlayerSettings set = PlayerSettings(); + set.hero = -1; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); + components.push_back(std::make_shared(29, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + if(selectedHero == -1) + components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedHeroes) { @@ -620,6 +641,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); + components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); heroes.push_back(elem); i++; @@ -640,6 +662,7 @@ void OptionsTab::SelectionWindow::genContentBonus() set.bonus = static_cast(elem); CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57 + 6, y * 63 + 32 / 2)); + components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); if(selectedBonus == elem) if(elem == set.RESOURCE && selectedFraction >= 0) components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); @@ -662,17 +685,34 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { PlayerSettings set = PlayerSettings(); if(type == SelType::TOWN) { - if(elem >= factions.size()) - return; - set.castle = factions[elem]; + if(elem > 0) + { + elem--; + if(elem >= factions.size()) + return; + set.castle = factions[elem]; + } + else + { + set.castle = -1; + } + if(set.castle != -2) selectedFraction = set.castle; } if(type == SelType::HERO) { - if(elem >= heroes.size()) - return; - set.hero = heroes[elem]; + if(elem > 0) + { + elem--; + if(elem >= heroes.size()) + return; + set.hero = heroes[elem]; + } + else + { + set.hero = -1; + } if(set.hero != -2) selectedHero = set.hero; } From 093e226bffbc721631a82145ecdb246d1b001f27 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 13 Aug 2023 23:50:40 +0400 Subject: [PATCH 0056/1248] Fix compilation --- client/adventureMap/TurnTimerWidget.cpp | 2 +- client/adventureMap/TurnTimerWidget.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 8c694af0e..3a6decee4 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -7,7 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ - +#include "StdInc.h" #include "TurnTimerWidget.h" #include "../CPlayerInterface.h" diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index 1ef505425..346dbaa1e 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -1,5 +1,5 @@ /* - * TurnTimerWidget.hpp, part of VCMI engine + * TurnTimerWidget.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * From ae6308856438fd0147f423ad88d3e776453b3f4d Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 13 Aug 2023 22:58:49 +0200 Subject: [PATCH 0057/1248] outline; squared --- client/lobby/OptionsTab.cpp | 73 ++++++++++++++++++++++++++----------- client/lobby/OptionsTab.h | 3 +- 2 files changed, 53 insertions(+), 23 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index ab977294b..1bad820ef 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -451,7 +451,7 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) double additionalItems = 1; // random if(faction < 0) - return std::ceil(((double)allowedFactions.size() + additionalItems) / ELEMENTS_PER_LINE); + return std::ceil(((double)allowedFactions.size() + additionalItems) / elementsPerLine); int count = 0; for(auto & elemh : allowedHeroes) @@ -461,7 +461,7 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) count++; } - return std::ceil(std::max((double)count + additionalItems, (double)allowedFactions.size() + additionalItems) / (double)ELEMENTS_PER_LINE); + return std::ceil(std::max((double)count + additionalItems, (double)allowedFactions.size() + additionalItems) / (double)elementsPerLine); } void OptionsTab::SelectionWindow::apply() @@ -540,12 +540,33 @@ void OptionsTab::SelectionWindow::setSelection() void OptionsTab::SelectionWindow::recreate() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - int amountLines = calcLines((type > SelType::TOWN) ? selectedFraction : static_cast(-1)); - if(type == SelType::BONUS) - amountLines = 1; - int x = (ELEMENTS_PER_LINE) * 57; + int amountLines = 1; + if(type == SelType::BONUS) + elementsPerLine = allowedBonus.size(); + else + { + // try to make squarish + if(type == SelType::TOWN) + elementsPerLine = floor(sqrt(allowedFactions.size())); + if(type == SelType::HERO) + { + int count = 0; + for(auto & elem : allowedHeroes) + { + CHero * type = VLC->heroh->objects[elem]; + if(type->heroClass->faction == selectedFraction) + { + count++; + } + } + elementsPerLine = floor(sqrt(count)); + } + + amountLines = calcLines((type > SelType::TOWN) ? selectedFraction : static_cast(-1)); + } + + int x = (elementsPerLine) * 57; int y = (amountLines) * 63; pos = Rect(0, 0, x, y); @@ -553,10 +574,6 @@ void OptionsTab::SelectionWindow::recreate() backgroundTexture = std::make_shared("DlgBluBk", pos); updateShadow(); - components.clear(); - - GH.windows().totalRedraw(); - if(type == SelType::TOWN) genContentCastles(); if(type == SelType::HERO) @@ -568,11 +585,20 @@ void OptionsTab::SelectionWindow::recreate() center(); } +void OptionsTab::SelectionWindow::drawOutlinedText(int x, int y, ColorRGBA color, std::string text) +{ + components.push_back(std::make_shared(x-1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x+1, y, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y-1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y+1, FONT_TINY, ETextAlignment::CENTER, Colors::BLACK, text)); + components.push_back(std::make_shared(x, y, FONT_TINY, ETextAlignment::CENTER, color, text)); +} + void OptionsTab::SelectionWindow::genContentGrid(int lines) { for(int y = 0; y("lobby/townBorderBig", x * 57, y * 63)); } @@ -588,14 +614,14 @@ void OptionsTab::SelectionWindow::genContentCastles() set.castle = -1; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - components.push_back(std::make_shared(29, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + drawOutlinedText(29, 56, (selectedFraction == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedFraction == -1) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedFactions) { - int x = i % ELEMENTS_PER_LINE; - int y = i / ELEMENTS_PER_LINE; + int x = i % elementsPerLine; + int y = i / elementsPerLine; PlayerSettings set = PlayerSettings(); set.castle = elem; @@ -604,7 +630,7 @@ void OptionsTab::SelectionWindow::genContentCastles() components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); components.push_back(std::make_shared(selectedFraction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); - components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedFraction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); factions.push_back(elem); i++; @@ -620,7 +646,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() set.hero = -1; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - components.push_back(std::make_shared(29, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + drawOutlinedText(29, 56, (selectedHero == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedHero == -1) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); @@ -631,8 +657,8 @@ void OptionsTab::SelectionWindow::genContentHeroes() if(type->heroClass->faction == selectedFraction) { - int x = i % ELEMENTS_PER_LINE; - int y = i / ELEMENTS_PER_LINE; + int x = i % elementsPerLine; + int y = i / elementsPerLine; PlayerSettings set = PlayerSettings(); set.hero = elem; @@ -641,7 +667,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); - components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); heroes.push_back(elem); i++; @@ -662,10 +688,13 @@ void OptionsTab::SelectionWindow::genContentBonus() set.bonus = static_cast(elem); CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57 + 6, y * 63 + 32 / 2)); - components.push_back(std::make_shared(x * 57 + 29, y * 63 + 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, helper.getName())); + drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::WHITE , helper.getName()); if(selectedBonus == elem) if(elem == set.RESOURCE && selectedFraction >= 0) + { components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); + drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::YELLOW , helper.getName()); + } i++; } @@ -676,7 +705,7 @@ int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) int x = (cursorPosition.x - pos.x) / 57; int y = (cursorPosition.y - pos.y) / 63; - return x + y * ELEMENTS_PER_LINE; + return x + y * elementsPerLine; } void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 380f39885..3a54826a3 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -97,7 +97,7 @@ public: class SelectionWindow : public CWindowObject { - const int ELEMENTS_PER_LINE = 4; + int elementsPerLine; PlayerColor color; SelType type; @@ -124,6 +124,7 @@ public: void genContentHeroes(); void genContentBonus(); + void drawOutlinedText(int x, int y, ColorRGBA color, std::string text); int calcLines(FactionID faction); void apply(); void recreate(); From f01973a4f07e883f4ad65c61846bd8a3f06a5421 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 01:13:37 +0400 Subject: [PATCH 0058/1248] Configurable turn timer --- client/adventureMap/TurnTimerWidget.cpp | 51 ++++++++++++++-------- client/adventureMap/TurnTimerWidget.h | 24 +++++++--- client/gui/InterfaceObjectConfigurable.cpp | 35 ++++++++++----- config/widgets/turnTimer.json | 27 ++++++++++++ 4 files changed, 102 insertions(+), 35 deletions(-) create mode 100644 config/widgets/turnTimer.json diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 3a6decee4..cc7dae67d 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -11,8 +11,7 @@ #include "TurnTimerWidget.h" #include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/Colors.h" + #include "../render/EFont.h" #include "../gui/CGuiHandler.h" #include "../gui/TextAlignment.h" @@ -20,28 +19,41 @@ #include "../widgets/TextControls.h" #include "../../CCallback.h" #include "../../lib/CPlayerState.h" +#include "../../lib/filesystem/ResourceID.h" -#include - -TurnTimerWidget::TurnTimerWidget(): - CIntObject(TIME), - turnTime(0), lastTurnTime(0) +TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c): + CIntObject(), rect(r), color(c) { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - watches = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL", 1, 0, 4, 6); - label = std::make_shared(24, 2, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, std::to_string(turnTime)); - - recActions &= ~DEACTIVATE; } -void TurnTimerWidget::showAll(Canvas & to) +void TurnTimerWidget::DrawRect::showAll(Canvas & to) { - to.drawColor(Rect(4, 4, 68, 24), ColorRGBA(10, 10, 10, 255)); + to.drawColor(rect, color); CIntObject::showAll(to); } +TurnTimerWidget::TurnTimerWidget(): + InterfaceObjectConfigurable(TIME), + turnTime(0), lastTurnTime(0) +{ + REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect); + + recActions &= ~DEACTIVATE; + + const JsonNode config(ResourceID("config/widgets/turnTimer.json")); + + build(config); +} + +std::shared_ptr TurnTimerWidget::buildDrawRect(const JsonNode & config) const +{ + logGlobal->debug("Building widget TurnTimerWidget::DrawRect"); + auto rect = readRect(config["rect"]); + auto color = readColor(config["color"]); + return std::make_shared(rect, color); +} + void TurnTimerWidget::show(Canvas & to) { showAll(to); @@ -50,9 +62,12 @@ void TurnTimerWidget::show(Canvas & to) void TurnTimerWidget::setTime(int time) { turnTime = time / 1000; - std::ostringstream oss; - oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; - label->setText(oss.str()); + if(auto w = widget("timer")) + { + std::ostringstream oss; + oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; + w->setText(oss.str()); + } } void TurnTimerWidget::tick(uint32_t msPassed) diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index 346dbaa1e..63432e0a0 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -11,26 +11,40 @@ #pragma once #include "../gui/CIntObject.h" +#include "../gui/InterfaceObjectConfigurable.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" class CAnimImage; class CLabel; -class TurnTimerWidget : public CIntObject +class TurnTimerWidget : public InterfaceObjectConfigurable { private: + + class DrawRect : public CIntObject + { + const Rect rect; + const ColorRGBA color; + + public: + DrawRect(const Rect &, const ColorRGBA &); + void showAll(Canvas & to) override; + }; int turnTime; int lastTurnTime; int cachedTurnTime; - std::shared_ptr watches; - std::shared_ptr label; - + //std::shared_ptr watches; + //std::shared_ptr label; + + std::shared_ptr buildDrawRect(const JsonNode & config) const; + public: //void tick(uint32_t msPassed) override; void show(Canvas & to) override; - void showAll(Canvas & to) override; void tick(uint32_t msPassed) override; void setTime(int time); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 751089445..b24729528 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -190,18 +190,29 @@ ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const logGlobal->debug("Reading color"); if(!config.isNull()) { - if(config.String() == "yellow") - return Colors::YELLOW; - if(config.String() == "white") - return Colors::WHITE; - if(config.String() == "gold") - return Colors::METALLIC_GOLD; - if(config.String() == "green") - return Colors::GREEN; - if(config.String() == "orange") - return Colors::ORANGE; - if(config.String() == "bright-yellow") - return Colors::BRIGHT_YELLOW; + if(config.isString()) + { + if(config.String() == "yellow") + return Colors::YELLOW; + if(config.String() == "white") + return Colors::WHITE; + if(config.String() == "gold") + return Colors::METALLIC_GOLD; + if(config.String() == "green") + return Colors::GREEN; + if(config.String() == "orange") + return Colors::ORANGE; + if(config.String() == "bright-yellow") + return Colors::BRIGHT_YELLOW; + } + if(config.isVector()) + { + const auto & asVector = config.Vector(); + if(asVector.size() == 4) + return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer(), asVector[3].Integer()); + if(asVector.size() == 3) + return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer()); + } } logGlobal->debug("Uknown color attribute"); return Colors::DEFAULT_KEY_COLOR; diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json new file mode 100644 index 000000000..704646f83 --- /dev/null +++ b/config/widgets/turnTimer.json @@ -0,0 +1,27 @@ +{ + "items": + [ + { //backgound color + "type": "drawRect", + "rect": {"x": 4, "y": 4, "w": 68, "h": 24}, + "color": [10, 10, 10, 255] + }, + + { //clocks icon + "type": "image", + "image": "VCMI/BATTLEQUEUE/STATESSMALL", + "frame": 1, + "position": {"x": 4, "y": 6} + }, + + { //timer field label + "name": "timer", + "type": "label", + "font": "big", + "alignment": "left", + "color": "yellow", + "text": "", + "position": {"x": 24, "y": 2} + }, + ] +} From 1d76f456ad5d8660073dfbfb24e1bae596a8d8cb Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:08:48 +0200 Subject: [PATCH 0059/1248] change servercode to make it more robust --- client/CServerHandler.cpp | 6 ++-- client/CServerHandler.h | 4 +-- lib/NetPacksLobby.h | 6 ++-- server/CVCMIServer.cpp | 56 ++++++++++++++++++++++++++++++++++ server/CVCMIServer.h | 3 ++ server/NetPacksLobbyServer.cpp | 15 +++++++-- 6 files changed, 80 insertions(+), 10 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 112af7f6c..e73ad9863 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -86,6 +86,8 @@ template class CApplyOnLobby : public CBaseForLobbyApply public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { + boost::unique_lock un(*CPlayerInterface::pim); + T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); @@ -454,11 +456,11 @@ void CServerHandler::setPlayer(PlayerColor color) const sendLobbyPack(lsp); } -void CServerHandler::setPlayerOption(ui8 what, si8 dir, PlayerColor player) const +void CServerHandler::setPlayerOption(ui8 what, si16 value, PlayerColor player) const { LobbyChangePlayerOption lcpo; lcpo.what = what; - lcpo.direction = dir; + lcpo.value = value; lcpo.color = player; sendLobbyPack(lcpo); } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index e8b20117d..394599c60 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -62,7 +62,7 @@ public: virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; - virtual void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const = 0; + virtual void setPlayerOption(ui8 what, si16 value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnLength(int npos) const = 0; virtual void sendMessage(const std::string & txt) const = 0; @@ -140,7 +140,7 @@ public: void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; - void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const override; + void setPlayerOption(ui8 what, si16 value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnLength(int npos) const override; void sendMessage(const std::string & txt) const override; diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index 47cad0e7f..6fce191c2 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -211,9 +211,9 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer { - enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS}; + enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS, TOWN_ID, HERO_ID, BONUS_ID}; ui8 what = UNKNOWN; - si8 direction = 0; //-1 or +1 + si16 value = 0; //-1 or +1 PlayerColor color = PlayerColor::CANNOT_DETERMINE; virtual void visitTyped(ICPackVisitor & visitor) override; @@ -221,7 +221,7 @@ struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer template void serialize(Handler &h, const int version) { h & what; - h & direction; + h & value; h & color; } }; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 0fa101ad0..16e9ae9d3 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -837,6 +837,28 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) s.bonus = PlayerSettings::RANDOM; } +void CVCMIServer::optionSetCastle(PlayerColor player, int id) +{ + PlayerSettings & s = si->playerInfos[player]; + FactionID & cur = s.castle; + auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; + + if(cur == PlayerSettings::NONE) //no change + return; + + if(allowed.find(static_cast(id)) == allowed.end() && id != PlayerSettings::RANDOM) // valid id + return; + + cur = static_cast(id); + + if(s.hero >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + { + s.hero = PlayerSettings::RANDOM; + } + if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) + s.bonus = PlayerSettings::RANDOM; +} + void CVCMIServer::setCampaignMap(CampaignScenarioID mapId) { campaignMap = mapId; @@ -884,6 +906,20 @@ void CVCMIServer::optionNextHero(PlayerColor player, int dir) } } +void CVCMIServer::optionSetHero(PlayerColor player, int id) +{ + PlayerSettings & s = si->playerInfos[player]; + if(s.castle < 0 || s.hero == PlayerSettings::NONE) + return; + + if(id == PlayerSettings::RANDOM) + { + s.hero = PlayerSettings::RANDOM; + } + if(canUseThisHero(player, id)) + s.hero = static_cast(id); +} + int CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) { if(dir > 0) @@ -930,6 +966,26 @@ void CVCMIServer::optionNextBonus(PlayerColor player, int dir) } } +void CVCMIServer::optionSetBonus(PlayerColor player, int id) +{ + PlayerSettings & s = si->playerInfos[player]; + + if(s.hero == PlayerSettings::NONE && + !getPlayerInfo(player.getNum()).heroesNames.size() && + id == PlayerSettings::ARTIFACT) //no hero - can't be artifact + return; + + if(id > PlayerSettings::RESOURCE) + return; + if(id < PlayerSettings::RANDOM) + return; + + if(s.castle == PlayerSettings::RANDOM && id == PlayerSettings::RESOURCE) //random castle - can't be resource + return; + + s.bonus = static_cast(static_cast(id)); +} + bool CVCMIServer::canUseThisHero(PlayerColor player, int ID) { return VLC->heroh->size() > ID diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 285bcd440..5ecd942d5 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -104,11 +104,14 @@ public: // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 + void optionSetHero(PlayerColor player, int id); int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); bool canUseThisHero(PlayerColor player, int ID); std::vector getUsedHeroes(); void optionNextBonus(PlayerColor player, int dir); //dir == -1 or +1 + void optionSetBonus(PlayerColor player, int id); void optionNextCastle(PlayerColor player, int dir); //dir == -1 or + + void optionSetCastle(PlayerColor player, int id); // Campaigns void setCampaignMap(CampaignScenarioID mapId); diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index c9ead03b5..eaca04f8c 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -360,14 +360,23 @@ void ApplyOnServerNetPackVisitor::visitLobbyChangePlayerOption(LobbyChangePlayer { switch(pack.what) { + case LobbyChangePlayerOption::TOWN_ID: + srv.optionSetCastle(pack.color, pack.value); + break; case LobbyChangePlayerOption::TOWN: - srv.optionNextCastle(pack.color, pack.direction); + srv.optionNextCastle(pack.color, pack.value); + break; + case LobbyChangePlayerOption::HERO_ID: + srv.optionSetHero(pack.color, pack.value); break; case LobbyChangePlayerOption::HERO: - srv.optionNextHero(pack.color, pack.direction); + srv.optionNextHero(pack.color, pack.value); + break; + case LobbyChangePlayerOption::BONUS_ID: + srv.optionSetBonus(pack.color, pack.value); break; case LobbyChangePlayerOption::BONUS: - srv.optionNextBonus(pack.color, pack.direction); + srv.optionNextBonus(pack.color, pack.value); break; } From bb5ada3bb38e9619b7fe8cdf95c8ce10f9b3e5d7 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:10:06 +0200 Subject: [PATCH 0060/1248] not from this PR --- client/CServerHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index e73ad9863..cfbb48d6f 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -86,8 +86,6 @@ template class CApplyOnLobby : public CBaseForLobbyApply public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { - boost::unique_lock un(*CPlayerInterface::pim); - T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); From 3c9c302fd251727e95c4473c81744321d0768293 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 02:16:25 +0400 Subject: [PATCH 0061/1248] TurnTimerInfo --- client/CServerHandler.cpp | 2 +- client/NetPacksClient.cpp | 2 +- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/adventureMap/TurnTimerWidget.cpp | 6 ++-- client/lobby/OptionsTab.cpp | 2 +- cmake_modules/VCMI_lib.cmake | 2 ++ lib/CGameInfoCallback.cpp | 8 ++--- lib/CGameInfoCallback.h | 3 +- lib/CPlayerState.h | 5 +-- lib/NetPacks.h | 5 +-- lib/NetPacksLib.cpp | 2 +- lib/NetPacksLobby.h | 4 +-- lib/StartInfo.h | 7 ++-- lib/TurnTimerInfo.cpp | 21 +++++++++++ lib/TurnTimerInfo.h | 35 +++++++++++++++++++ server/CGameHandler.cpp | 14 ++++---- server/NetPacksLobbyServer.cpp | 4 +-- 17 files changed, 93 insertions(+), 31 deletions(-) create mode 100644 lib/TurnTimerInfo.cpp create mode 100644 lib/TurnTimerInfo.h diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index e154484f1..950ba77fe 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -474,7 +474,7 @@ void CServerHandler::setTurnLength(int npos) const { vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1); LobbySetTurnTime lstt; - lstt.turnTime = GameConstants::POSSIBLE_TURNTIME[npos]; + lstt.turnTimerInfo.turnTimer = GameConstants::POSSIBLE_TURNTIME[npos] * 60 * 1000; sendLobbyPack(lstt); } diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 792d541c9..c1ab20ccb 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -866,7 +866,7 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) { - logNetwork->debug("Server sets %d turn time for %s", pack.turnTime, pack.player.getStr()); + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.getStr()); } void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index b61f5d386..a2aba5b16 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -63,7 +63,7 @@ AdventureMapInterface::AdventureMapInterface(): shortcuts->setState(EAdventureState::MAKING_TURN); widget->getMapView()->onViewMapActivated(); - if(LOCPLINT->cb->getStartInfo()->turnTime > 0) + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled()) watches = std::make_shared(); addUsedEvents(KEYBOARD | TIME); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index cc7dae67d..9827a8eb4 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -77,11 +77,11 @@ void TurnTimerWidget::tick(uint32_t msPassed) auto player = LOCPLINT->cb->getCurrentPlayer(); auto time = LOCPLINT->cb->getPlayerTurnTime(player); cachedTurnTime -= msPassed; - if(time / 1000 != lastTurnTime / 1000) + if(time.turnTimer / 1000 != lastTurnTime / 1000) { //do not update timer on this tick - lastTurnTime = time; - cachedTurnTime = time; + lastTurnTime = time.turnTimer; + cachedTurnTime = time.turnTimer; } else setTime(cachedTurnTime); } diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index e7d5243ab..28238366b 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -74,7 +74,7 @@ void OptionsTab::recreate() if(sliderTurnDuration) { - sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime)); + sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTimerInfo.turnTimer / (60 * 1000))); labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]); } } diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 7152a5327..c64987e3c 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -264,6 +264,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/ScriptHandler.cpp ${MAIN_LIB_DIR}/TerrainHandler.cpp ${MAIN_LIB_DIR}/TextOperations.cpp + ${MAIN_LIB_DIR}/TurnTimerInfo.cpp ${MAIN_LIB_DIR}/VCMIDirs.cpp ${MAIN_LIB_DIR}/VCMI_Lib.cpp ) @@ -621,6 +622,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/StringConstants.h ${MAIN_LIB_DIR}/TerrainHandler.h ${MAIN_LIB_DIR}/TextOperations.h + ${MAIN_LIB_DIR}/TurnTimerInfo.h ${MAIN_LIB_DIR}/UnlockGuard.h ${MAIN_LIB_DIR}/VCMIDirs.h ${MAIN_LIB_DIR}/vcmi_endian.h diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 8f52a0e32..4784d1e48 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -99,20 +99,20 @@ const PlayerState * CGameInfoCallback::getPlayerState(PlayerColor color, bool ve } } -int CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const +TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const { if(!color.isValidPlayer()) { - return 0; + return TurnTimerInfo{}; } auto player = gs->players.find(color); if(player != gs->players.end()) { - return player->second.turnTime; + return player->second.turnTimer; } - return 0; + return TurnTimerInfo{}; } const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index e7c7db6bf..9808a318d 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -37,6 +37,7 @@ struct TeamState; struct QuestInfo; class CGameState; class PathfinderConfig; +struct TurnTimerInfo; class CArmedInstance; class CGObjectInstance; @@ -153,7 +154,7 @@ public: virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player) virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; - virtual int getPlayerTurnTime(PlayerColor color) const; + virtual TurnTimerInfo getPlayerTurnTime(PlayerColor color) const; //map virtual bool isVisible(int3 pos, const std::optional & Player) const; diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 8c8dbf843..fa1ab5a1e 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -15,6 +15,7 @@ #include "bonuses/Bonus.h" #include "bonuses/CBonusSystemNode.h" #include "ResourceSet.h" +#include "TurnTimerInfo.h" VCMI_LIB_NAMESPACE_BEGIN @@ -39,7 +40,7 @@ public: bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory EPlayerStatus::EStatus status; std::optional daysWithoutCastle; - int turnTime = 0; + TurnTimerInfo turnTimer; PlayerState(); PlayerState(PlayerState && other) noexcept; @@ -72,7 +73,7 @@ public: h & team; h & resources; h & status; - h & turnTime; + h & turnTimer; h & heroes; h & towns; h & dwellings; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index b0270fb8a..e01485a85 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -14,6 +14,7 @@ #include "ConstTransitivePtr.h" #include "MetaString.h" #include "ResourceSet.h" +#include "TurnTimerInfo.h" #include "int3.h" #include "battle/BattleAction.h" @@ -152,12 +153,12 @@ struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient void applyGs(CGameState * gs) const; PlayerColor player; - int turnTime = 0; + TurnTimerInfo turnTimer; template void serialize(Handler & h, const int version) { h & player; - h & turnTime; + h & turnTimer; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 3b4acd477..4e6e65731 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2516,7 +2516,7 @@ void YourTurn::applyGs(CGameState * gs) const void TurnTimeUpdate::applyGs(CGameState *gs) const { auto & playerState = gs->players[player]; - playerState.turnTime = turnTime; + playerState.turnTimer = turnTimer; } Component::Component(const CStackBasicDescriptor & stack) diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index 47cad0e7f..019da09d0 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -240,13 +240,13 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer { - ui8 turnTime = 0; + TurnTimerInfo turnTimerInfo; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { - h & turnTime; + h & turnTimerInfo; } }; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 779f9c9e0..921d2ea9f 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -10,6 +10,7 @@ #pragma once #include "GameConstants.h" +#include "TurnTimerInfo.h" #include "campaign/CampaignConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -80,7 +81,7 @@ struct DLL_LINKAGE StartInfo ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant - ui8 turnTime; //in minutes, 0=unlimited + TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } std::shared_ptr mapGenOptions; @@ -103,14 +104,14 @@ struct DLL_LINKAGE StartInfo h & seedToBeUsed; h & seedPostInit; h & mapfileChecksum; - h & turnTime; + h & turnTimerInfo; h & mapname; h & mapGenOptions; h & campState; } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), turnTime(0) + mapfileChecksum(0) { } diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp new file mode 100644 index 000000000..5f694ac4b --- /dev/null +++ b/lib/TurnTimerInfo.cpp @@ -0,0 +1,21 @@ +/* + * TurnTimerInfo.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 "TurnTimerInfo.h" + +bool TurnTimerInfo::isEnabled() const +{ + return turnTimer > 0; +} + +bool TurnTimerInfo::isBattleEnabled() const +{ + return battleTimer > 0; +} diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h new file mode 100644 index 000000000..f29530726 --- /dev/null +++ b/lib/TurnTimerInfo.h @@ -0,0 +1,35 @@ +/* + * TurnTimerInfo.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 + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE TurnTimerInfo +{ + int turnTimer = 0; //in ms, counts down when player is making his turn on adventure map + int baseTimer = 0; //in ms, counts down only when turn timer runs out + int battleTimer = 0; //in ms, counts down during battles when creature timer runs out + int creatureTimer = 0; //in ms, counts down when player is choosing action in battle + + bool isEnabled() const; + bool isBattleEnabled() const; + + template + void serialize(Handler &h, const int version) + { + h & turnTimer; + h & baseTimer; + h & battleTimer; + h & creatureTimer; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8df34e14a..4e06419bf 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2068,11 +2068,11 @@ void CGameHandler::run(bool resume) yt.daysWithoutCastle = playerState->daysWithoutCastle; applyAndSend(&yt); - if(gs->getStartInfo()->turnTime > 0) //turn timer check + if(gs->getStartInfo()->turnTimerInfo.turnTimer > 0) //turn timer check { TurnTimeUpdate ttu; ttu.player = player; - ttu.turnTime = gs->getStartInfo()->turnTime * 60 * 1000; //ms + ttu.turnTimer = gs->getStartInfo()->turnTimerInfo; applyAndSend(&ttu); } } @@ -2088,18 +2088,18 @@ void CGameHandler::run(bool resume) boost::unique_lock lock(states.mx); while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) { - if(gs->getStartInfo()->turnTime > 0 && !gs->curB) //turn timer check + if(gs->getStartInfo()->turnTimerInfo.isEnabled() && !gs->curB) //turn timer check { - if(gs->players[playerColor].turnTime > 0) + if(gs->players[playerColor].turnTimer.turnTimer > 0) { - gs->players[playerColor].turnTime -= waitTime; + gs->players[playerColor].turnTimer.turnTimer -= waitTime; if(gs->players[playerColor].status == EPlayerStatus::INGAME //do not send message if player is not active already - && gs->players[playerColor].turnTime % turnTimePropagateFrequency == 0) + && gs->players[playerColor].turnTimer.turnTimer % turnTimePropagateFrequency == 0) { TurnTimeUpdate ttu; ttu.player = playerColor; - ttu.turnTime = gs->players[playerColor].turnTime; + ttu.turnTimer = gs->players[playerColor].turnTimer; applyAndSend(&ttu); } } diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index c9ead03b5..5d7cbea35 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -214,7 +214,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack) srv.si->mapname = pack.ourCampaign->getFilename(); srv.si->mode = StartInfo::CAMPAIGN; srv.si->campState = pack.ourCampaign; - srv.si->turnTime = 0; + srv.si->turnTimerInfo = TurnTimerInfo{}; bool isCurrentMapConquerable = pack.ourCampaign->currentScenario() && pack.ourCampaign->isAvailable(*pack.ourCampaign->currentScenario()); @@ -382,7 +382,7 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack) void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack) { - srv.si->turnTime = pack.turnTime; + srv.si->turnTimerInfo = pack.turnTimerInfo; result = true; } From 7bfbfeb48fcf9e11948bd2112c617eb816dc92bc Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:16:53 +0200 Subject: [PATCH 0062/1248] missing --- client/lobby/OptionsTab.cpp | 89 ++++++++++++------------------------- 1 file changed, 28 insertions(+), 61 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 1bad820ef..63508e312 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -478,63 +478,14 @@ void OptionsTab::SelectionWindow::apply() void OptionsTab::SelectionWindow::setSelection() { - // fraction - int selectedFractionPos = -1; - for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN_ID, selectedFraction, color); - int initialFractionPos = -1; - for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::HERO_ID, selectedHero, color); - int deltaFraction = selectedFractionPos - initialFractionPos; - - if(deltaFraction != 0) - for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::TOWN, deltaFraction > 0 ? 1 : -1, color); - else - { - if(type == SelType::TOWN) - { - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, 1, color); - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, -1, color); - } - } - - // hero - int selectedHeroPos = -1; - for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::HERO, deltaHero > 0 ? 1 : -1, color); - - // bonus - int deltaBonus = selectedBonus - initialBonus; - - if(initialHero < -1 && ((selectedBonus > 0 && initialBonus < 0) || (selectedBonus < 0 && initialBonus > 0))) // no artifact - { - if(deltaBonus > 0) - deltaBonus--; - else if(deltaBonus < 0) - deltaBonus++; - } - - if(deltaBonus != 0) - for(int i = 0; isetPlayerOption(LobbyChangePlayerOption::BONUS, deltaBonus > 0 ? 1 : -1, color); + if(selectedBonus != initialBonus) + CSH->setPlayerOption(LobbyChangePlayerOption::BONUS_ID, selectedBonus, color); } void OptionsTab::SelectionWindow::recreate() @@ -763,9 +714,17 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) PlayerSettings set = PlayerSettings(); if(type == SelType::TOWN) { - if(elem >= factions.size()) - return; - set.castle = factions[elem]; + if(elem > 0) + { + elem--; + if(elem >= factions.size()) + return; + set.castle = factions[elem]; + } + else + { + set.castle = -1; + } if(set.castle != -2) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); @@ -774,9 +733,17 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } if(type == SelType::HERO) { - if(elem >= heroes.size()) - return; - set.hero = heroes[elem]; + if(elem > 0) + { + elem--; + if(elem >= heroes.size()) + return; + set.hero = heroes[elem]; + } + else + { + set.hero = -1; + } if(set.hero != -2) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); From 079e8ba1baf5c0ed9ef0254d0623370a9b720831 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 00:40:12 +0200 Subject: [PATCH 0063/1248] naming --- client/lobby/OptionsTab.cpp | 39 ++++++++++++++++++------------------- client/lobby/OptionsTab.h | 6 +++--- 2 files changed, 22 insertions(+), 23 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 63508e312..4f752f4e2 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -424,10 +424,10 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) color = _color; type = _type; - initialFraction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; + initialFaction = SEL->getStartInfo()->playerInfos.find(color)->second.castle; initialHero = SEL->getStartInfo()->playerInfos.find(color)->second.hero; initialBonus = SEL->getStartInfo()->playerInfos.find(color)->second.bonus; - selectedFraction = initialFraction; + selectedFaction = initialFaction; selectedHero = initialHero; selectedBonus = initialBonus; allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; @@ -440,7 +440,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) if(initialHero >= -1) allowedBonus.push_back(0); // artifact allowedBonus.push_back(1); // gold - if(initialFraction >= 0) + if(initialFaction >= 0) allowedBonus.push_back(2); // resource recreate(); @@ -478,8 +478,8 @@ void OptionsTab::SelectionWindow::apply() void OptionsTab::SelectionWindow::setSelection() { - if(selectedFraction != initialFraction) - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN_ID, selectedFraction, color); + if(selectedFaction != initialFaction) + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN_ID, selectedFaction, color); if(selectedHero != initialHero) CSH->setPlayerOption(LobbyChangePlayerOption::HERO_ID, selectedHero, color); @@ -506,7 +506,7 @@ void OptionsTab::SelectionWindow::recreate() for(auto & elem : allowedHeroes) { CHero * type = VLC->heroh->objects[elem]; - if(type->heroClass->faction == selectedFraction) + if(type->heroClass->faction == selectedFaction) { count++; } @@ -514,7 +514,7 @@ void OptionsTab::SelectionWindow::recreate() elementsPerLine = floor(sqrt(count)); } - amountLines = calcLines((type > SelType::TOWN) ? selectedFraction : static_cast(-1)); + amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : static_cast(-1)); } int x = (elementsPerLine) * 57; @@ -526,7 +526,7 @@ void OptionsTab::SelectionWindow::recreate() updateShadow(); if(type == SelType::TOWN) - genContentCastles(); + genContentFactions(); if(type == SelType::HERO) genContentHeroes(); if(type == SelType::BONUS) @@ -556,7 +556,7 @@ void OptionsTab::SelectionWindow::genContentGrid(int lines) } } -void OptionsTab::SelectionWindow::genContentCastles() +void OptionsTab::SelectionWindow::genContentFactions() { int i = 1; @@ -565,8 +565,8 @@ void OptionsTab::SelectionWindow::genContentCastles() set.castle = -1; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - drawOutlinedText(29, 56, (selectedFraction == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedFraction == -1) + drawOutlinedText(29, 56, (selectedFaction == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedFaction == -1) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedFactions) @@ -580,8 +580,8 @@ void OptionsTab::SelectionWindow::genContentCastles() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); - components.push_back(std::make_shared(selectedFraction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedFraction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + components.push_back(std::make_shared(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); + drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedFaction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); factions.push_back(elem); i++; @@ -605,7 +605,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() { CHero * type = VLC->heroh->objects[elem]; - if(type->heroClass->faction == selectedFraction) + if(type->heroClass->faction == selectedFaction) { int x = i % elementsPerLine; @@ -641,11 +641,10 @@ void OptionsTab::SelectionWindow::genContentBonus() components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57 + 6, y * 63 + 32 / 2)); drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::WHITE , helper.getName()); if(selectedBonus == elem) - if(elem == set.RESOURCE && selectedFraction >= 0) - { - components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::YELLOW , helper.getName()); - } + { + components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); + drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::YELLOW , helper.getName()); + } i++; } @@ -678,7 +677,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { } if(set.castle != -2) - selectedFraction = set.castle; + selectedFaction = set.castle; } if(type == SelType::HERO) { diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 3a54826a3..4abf46ae5 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -108,10 +108,10 @@ public: std::vector factions; std::vector heroes; - FactionID initialFraction; + FactionID initialFaction; HeroTypeID initialHero; int initialBonus; - FactionID selectedFraction; + FactionID selectedFaction; HeroTypeID selectedHero; int selectedBonus; @@ -120,7 +120,7 @@ public: std::vector allowedBonus; void genContentGrid(int lines); - void genContentCastles(); + void genContentFactions(); void genContentHeroes(); void genContentBonus(); From e414af221bbfee0b02a5c4e3956a6d5b6a0516d4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 03:20:27 +0400 Subject: [PATCH 0064/1248] Move turn timer logic from GameHandler class --- server/CGameHandler.cpp | 36 ++++------------ server/CGameHandler.h | 3 ++ server/CMakeLists.txt | 2 + server/TurnTimerHandler.cpp | 86 +++++++++++++++++++++++++++++++++++++ server/TurnTimerHandler.h | 33 ++++++++++++++ 5 files changed, 132 insertions(+), 28 deletions(-) create mode 100644 server/TurnTimerHandler.cpp create mode 100644 server/TurnTimerHandler.h diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4e06419bf..9d7648731 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1570,6 +1570,7 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") , complainInvalidSlot("Invalid slot accessed!") + , turnTimerHandler(*this) { QID = 1; IObjectInterface::cb = this; @@ -2019,7 +2020,11 @@ void CGameHandler::run(bool resume) events::GameResumed::defaultExecute(serverEventBus.get()); auto playerTurnOrder = generatePlayerTurnOrder(); - + + if(!resume) + for(auto & playerColor : playerTurnOrder) + turnTimerHandler.onGameplayStart(gs->players[playerColor]); + while(lobby->state == EServerState::GAMEPLAY) { if(!resume) @@ -2068,13 +2073,7 @@ void CGameHandler::run(bool resume) yt.daysWithoutCastle = playerState->daysWithoutCastle; applyAndSend(&yt); - if(gs->getStartInfo()->turnTimerInfo.turnTimer > 0) //turn timer check - { - TurnTimeUpdate ttu; - ttu.player = player; - ttu.turnTimer = gs->getStartInfo()->turnTimerInfo; - applyAndSend(&ttu); - } + turnTimerHandler.onPlayerGetTurn(gs->players[player]); } }; @@ -2084,29 +2083,10 @@ void CGameHandler::run(bool resume) { //wait till turn is done const int waitTime = 100; //ms - int turnTimePropagateFrequency = 5000; //do not send updates too frequently boost::unique_lock lock(states.mx); while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) { - if(gs->getStartInfo()->turnTimerInfo.isEnabled() && !gs->curB) //turn timer check - { - if(gs->players[playerColor].turnTimer.turnTimer > 0) - { - gs->players[playerColor].turnTimer.turnTimer -= waitTime; - - if(gs->players[playerColor].status == EPlayerStatus::INGAME //do not send message if player is not active already - && gs->players[playerColor].turnTimer.turnTimer % turnTimePropagateFrequency == 0) - { - TurnTimeUpdate ttu; - ttu.player = playerColor; - ttu.turnTimer = gs->players[playerColor].turnTimer; - applyAndSend(&ttu); - } - } - else if(!queries.topQuery(playerColor)) //wait for replies to avoid pending queries - states.players.at(playerColor).makingTurn = false; //force end turn - } - + turnTimerHandler.onPlayerMakingTurn(gs->players[playerColor], waitTime); static time_duration p = milliseconds(waitTime); states.cv.timed_wait(lock, p); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index a3f906408..9fbb53abb 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -17,6 +17,7 @@ #include "../lib/battle/BattleAction.h" #include "../lib/ScriptHandler.h" #include "CQuery.h" +#include "TurnTimerHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -100,6 +101,8 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En CVCMIServer * lobby; std::shared_ptr> applier; std::unique_ptr battleThread; + + TurnTimerHandler turnTimerHandler; public: std::unique_ptr heroPool; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4239d07ba..d6842c44a 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -9,6 +9,7 @@ set(server_SRCS CVCMIServer.cpp NetPacksServer.cpp NetPacksLobbyServer.cpp + TurnTimerHandler.cpp ) set(server_HEADERS @@ -22,6 +23,7 @@ set(server_HEADERS CVCMIServer.h LobbyNetPackVisitors.h ServerNetPackVisitors.h + TurnTimerHandler.h ) assign_source_group(${server_SRCS} ${server_HEADERS}) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp new file mode 100644 index 000000000..f74496433 --- /dev/null +++ b/server/TurnTimerHandler.cpp @@ -0,0 +1,86 @@ +/* + * TurnTimerHandler.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 "TurnTimerHandler.h" +#include "CGameHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/StartInfo.h" + +TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): + gameHandler(gh) +{ + +} + +void TurnTimerHandler::onGameplayStart(PlayerState & state) +{ + if(const auto * si = gameHandler.getStartInfo()) + { + if(si->turnTimerInfo.isEnabled()) + { + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = si->turnTimerInfo; + ttu.turnTimer.turnTimer = 0; + gameHandler.applyAndSend(&ttu); + } + } +} + +void TurnTimerHandler::onPlayerGetTurn(PlayerState & state) +{ + if(const auto * si = gameHandler.getStartInfo()) + { + if(si->turnTimerInfo.isEnabled()) + { + state.turnTimer.baseTimer += state.turnTimer.turnTimer; + state.turnTimer.turnTimer = si->turnTimerInfo.turnTimer; + + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + gameHandler.applyAndSend(&ttu); + } + } +} + +void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) +{ + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs) + return; + + if(si->turnTimerInfo.isEnabled() && !gs->curB) + { + if(state.turnTimer.turnTimer > 0) + { + state.turnTimer.turnTimer -= waitTime; + + if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already + && state.turnTimer.turnTimer % turnTimePropagateFrequency == 0) + { + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + gameHandler.applyAndSend(&ttu); + } + } + else if(state.turnTimer.baseTimer > 0) + { + state.turnTimer.turnTimer = state.turnTimer.baseTimer; + state.turnTimer.baseTimer = 0; + onPlayerMakingTurn(state, waitTime); + } + else if(!gameHandler.queries.topQuery(state.color)) //wait for replies to avoid pending queries + gameHandler.states.players.at(state.color).makingTurn = false; //force end turn + } +} diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h new file mode 100644 index 000000000..03bd8d30b --- /dev/null +++ b/server/TurnTimerHandler.h @@ -0,0 +1,33 @@ +/* + * TurnTimerHandler.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; +struct PlayerState; + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class TurnTimerHandler +{ + CGameHandler & gameHandler; + const int turnTimePropagateFrequency = 5000; + +public: + TurnTimerHandler(CGameHandler &); + + void onGameplayStart(PlayerState & state); + void onPlayerGetTurn(PlayerState & state); + void onPlayerMakingTurn(PlayerState & state, int waitTime); +}; From 0c38187916f06980581fe81c777478d9a1da4fe9 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 04:06:26 +0400 Subject: [PATCH 0065/1248] Fix freezes while turn ends at hero move --- client/CPlayerInterface.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 69fde8c7b..2e4f140ae 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -170,6 +170,9 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; + + makingTurn = false; + stillMoveHero.setn(STOP_MOVE); if(GH.windows().findWindows().empty()) { From 380ee41fbaffde9e993d25961626334e9f688c43 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 04:33:41 +0400 Subject: [PATCH 0066/1248] Fix finishing turn while flying over object --- server/CGameHandler.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9d7648731..95a07ae25 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2323,7 +2323,17 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - queries.popIfTop(moveQuery); + if(!transit) + { + for(auto topQuery = queries.topQuery(h->tempOwner); true; topQuery = queries.topQuery(h->tempOwner)) + { + moveQuery = std::dynamic_pointer_cast(topQuery); + if(moveQuery) + queries.popIfTop(moveQuery); + else + break; + } + } logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; }; From a8e5b32b6ae9ad7fd16e2da3acc55f19b715d38c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 04:55:45 +0400 Subject: [PATCH 0067/1248] Fix compiling --- server/CGameHandler.cpp | 4 ++++ server/CGameHandler.h | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 95a07ae25..f7ff8b5ff 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1563,6 +1563,10 @@ int CGameHandler::moveStack(int stack, BattleHex dest) return ret; } +CGameHandler::CGameHandler() + : turnTimerHandler(*this) +{} + CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 9fbb53abb..3dce1f1d7 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -157,7 +157,7 @@ public: void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); void setBattleResult(BattleResult::EResult resultType, int victoriusSide); - CGameHandler() = default; + CGameHandler(); CGameHandler(CVCMIServer * lobby); ~CGameHandler(); From 2ce50915e176a8cf6bec241f45a8677c2f562c00 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 05:03:50 +0400 Subject: [PATCH 0068/1248] Fix ios compilation --- lib/TurnTimerInfo.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 5f694ac4b..0b8a4a454 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "TurnTimerInfo.h" +VCMI_LIB_NAMESPACE_BEGIN + bool TurnTimerInfo::isEnabled() const { return turnTimer > 0; @@ -19,3 +21,5 @@ bool TurnTimerInfo::isBattleEnabled() const { return battleTimer > 0; } + +VCMI_LIB_NAMESPACE_END From 23aaa72dfe281be2a0a68e659ac64cb04008614f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 05:21:57 +0400 Subject: [PATCH 0069/1248] No error on hero move when timer is expired --- server/CGameHandler.cpp | 3 +++ server/NetPacksServer.cpp | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index f7ff8b5ff..c6a5d7f58 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2220,6 +2220,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo // not turn of that hero or player can't simply teleport hero (at least not with this function) if (!h || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer))) { + if(h && getStartInfo()->turnTimerInfo.isEnabled() && gs->players[h->getOwner()].turnTimer.turnTimer == 0) + return true; //timer expired, no error + logGlobal->error("Illegal call to move hero!"); return false; } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 7660bc073..8c2d5a474 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -64,7 +64,6 @@ void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, gh.getPlayerAt(pack.c)); } From e426924930c761b5f10e4fd8e777b55f8db329e2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 15:37:21 +0400 Subject: [PATCH 0070/1248] Close window at time expiring --- client/CPlayerInterface.cpp | 5 +++++ client/gui/CIntObject.h | 2 +- client/windows/InfoWindows.h | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2e4f140ae..416af822b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -181,6 +181,11 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) GH.windows().pushWindow(adventureInt); } + //close window from another player + if(auto w = GH.windows().topWindow()) + if(w->ID == -1 && player != playerID) + w->close(); + // remove all dialogs that do not expect query answer while (!GH.windows().topWindow() && !GH.windows().topWindow()) GH.windows().popWindows(1); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 5d8a3f53a..2807aa413 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -142,7 +142,7 @@ class WindowBase : public CIntObject public: WindowBase(int used_ = 0, Point pos_ = Point()); protected: - void close(); + virtual void close(); }; class IGarrisonHolder diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index e7911884a..501c47459 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -55,7 +55,7 @@ public: std::vector> buttons; TCompsInfo components; - virtual void close(); + void close() override; void show(Canvas & to) override; void showAll(Canvas & to) override; @@ -78,7 +78,7 @@ public: class CRClickPopup : public WindowBase { public: - virtual void close(); + void close() override; bool isPopupWindow() const override; static std::shared_ptr createInfoWin(Point position, const CGObjectInstance * specific); From 5207ba7e00f7fb1fbc02990226a32bcfc4dbe72e Mon Sep 17 00:00:00 2001 From: krs Date: Mon, 14 Aug 2023 15:06:48 +0300 Subject: [PATCH 0071/1248] Removed WoG artifacts from core. Replaced with unused to avoid crashes. --- config/artifacts.json | 162 ++---------------------------------------- 1 file changed, 7 insertions(+), 155 deletions(-) diff --git a/config/artifacts.json b/config/artifacts.json index 98c71050e..714405b47 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -2286,171 +2286,23 @@ "eversmokingRingOfSulfur" ] }, - "magicWand": + // Misiokles: not having these 3 artifacts could lead to crashes. RMG will try to place them in some randoms maps (and crash the game) + "unusedArtifact1": { - "bonuses" : [ - { - "type" : "CASTS", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.implosion", - "type" : "SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.fireball", - "type" : "SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "RANDOM_SPELLCASTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 2, - "subtype" : "spell.lightningBolt", - "type" : "ENCHANTER", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 1, - "type" : "REBIRTH", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MANA_DRAIN", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "HEALER", - "val" : 25, - "valueType" : "BASE_NUMBER" - } - ], "index" : 141, + "class" : "SPECIAL", "type" : ["CREATURE"] }, - "goldTowerArrow": + "unusedArtifact2": { - "bonuses" : [ - { - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ADDITIONAL_ATTACK", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 22, - "type" : "SPELL_LIKE_ATTACK", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "CATAPULT", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ACID_BREATH", - "val" : 20, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 0, - "type" : "SHOTS", - "val" : 200, - "valueType" : "PERCENT_TO_BASE" - }, - { - "addInfo" : 1, - "subtype" : "spell.age", - "type" : "SPELL_BEFORE_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.berserk", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.poison", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "addInfo" : 1, - "subtype" : "spell.disruptingRay", - "type" : "SPELL_AFTER_ATTACK", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], "index" : 142, + "class" : "SPECIAL", "type" : ["CREATURE"] }, - "monstersPower": + "unusedArtifact3": { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 100, - "valueType" : "PERCENT_TO_BASE" - }, - { - "subtype" : 2, - "type" : "CREATURE_DAMAGE", - "val" : 100, - "valueType" : "PERCENT_TO_ALL" - }, - { - "type" : "HP_REGENERATION", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "NO_RETALIATION", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "RETURN_AFTER_STRIKE", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "ATTACKS_ALL_ADJACENT", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 100, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "SPELL_DAMAGE_REDUCTION", - "val" : 100, - "valueType" : "BASE_NUMBER" - } - ], "index" : 143, + "class" : "SPECIAL", "type" : ["CREATURE"] } } From 0c76ae89f8151c248df4f2d4b3f31b18401a3240 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 16:33:41 +0400 Subject: [PATCH 0072/1248] Revert "Auxiliary commit to revert individual files from e426924930c761b5f10e4fd8e777b55f8db329e2" This reverts commit c1d6b2836fca30f99d49012c5ae989cdc5ad0e4f. --- client/gui/CIntObject.h | 2 +- client/windows/InfoWindows.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 2807aa413..5d8a3f53a 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -142,7 +142,7 @@ class WindowBase : public CIntObject public: WindowBase(int used_ = 0, Point pos_ = Point()); protected: - virtual void close(); + void close(); }; class IGarrisonHolder diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index 501c47459..e7911884a 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -55,7 +55,7 @@ public: std::vector> buttons; TCompsInfo components; - void close() override; + virtual void close(); void show(Canvas & to) override; void showAll(Canvas & to) override; @@ -78,7 +78,7 @@ public: class CRClickPopup : public WindowBase { public: - void close() override; + virtual void close(); bool isPopupWindow() const override; static std::shared_ptr createInfoWin(Point position, const CGObjectInstance * specific); From d26fdaefe4626f9bd51d91d6c5ce216c8e02e63d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 14 Aug 2023 23:31:44 +0400 Subject: [PATCH 0073/1248] Battle timer --- client/adventureMap/TurnTimerWidget.cpp | 30 +++++++-- lib/TurnTimerInfo.cpp | 2 +- server/CGameHandler.cpp | 10 +++ server/TurnTimerHandler.cpp | 86 +++++++++++++++++++++++-- server/TurnTimerHandler.h | 6 ++ 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 9827a8eb4..5d47a4e45 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -72,17 +72,35 @@ void TurnTimerWidget::setTime(int time) void TurnTimerWidget::tick(uint32_t msPassed) { - if(LOCPLINT && LOCPLINT->cb && !LOCPLINT->battleInt) + if(LOCPLINT && LOCPLINT->cb) { auto player = LOCPLINT->cb->getCurrentPlayer(); auto time = LOCPLINT->cb->getPlayerTurnTime(player); cachedTurnTime -= msPassed; - if(time.turnTimer / 1000 != lastTurnTime / 1000) + if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero + + if(LOCPLINT->battleInt) { - //do not update timer on this tick - lastTurnTime = time.turnTimer; - cachedTurnTime = time.turnTimer; + if(time.isBattleEnabled()) + { + if(time.creatureTimer / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time.creatureTimer; + cachedTurnTime = time.creatureTimer; + } + else setTime(cachedTurnTime); + } + } + else + { + if(time.turnTimer / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time.turnTimer; + cachedTurnTime = time.turnTimer; + } + else setTime(cachedTurnTime); } - else setTime(cachedTurnTime); } } diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 0b8a4a454..6d10f67ee 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -19,7 +19,7 @@ bool TurnTimerInfo::isEnabled() const bool TurnTimerInfo::isBattleEnabled() const { - return battleTimer > 0; + return creatureTimer > 0; } VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index c6a5d7f58..bff535653 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2091,6 +2091,11 @@ void CGameHandler::run(bool resume) while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) { turnTimerHandler.onPlayerMakingTurn(gs->players[playerColor], waitTime); + if(gs->curB) + { + turnTimerHandler.onBattleLoop(gs->players[gs->curB->getSidePlayer(BattleSide::ATTACKER)], waitTime); + turnTimerHandler.onBattleLoop(gs->players[gs->curB->getSidePlayer(BattleSide::DEFENDER)], waitTime); + } static time_duration p = milliseconds(waitTime); states.cv.timed_wait(lock, p); } @@ -6085,6 +6090,9 @@ void CGameHandler::runBattle() setBattle(gs->curB); assert(gs->curB); //TODO: pre-tactic stuff, call scripts etc. + + turnTimerHandler.onBattleStart(gs->players[gs->curB->getSidePlayer(BattleSide::ATTACKER)]); + turnTimerHandler.onBattleStart(gs->players[gs->curB->getSidePlayer(BattleSide::DEFENDER)]); //Moat should be initialized here, because only here we can use spellcasting if (gs->curB->town && gs->curB->town->fortLevel() >= CGTownInstance::CITADEL) @@ -6247,6 +6255,8 @@ void CGameHandler::runBattle() const CStack * next = nullptr; while((lobby->state != EServerState::SHUTDOWN) && (next = getNextStack())) { + turnTimerHandler.onBattleNextStack(gs->players[next->getOwner()]); + BattleUnitsChanged removeGhosts; for(auto stack : curB.stacks) { diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index f74496433..4e7b918b8 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -10,8 +10,10 @@ #include "StdInc.h" #include "TurnTimerHandler.h" #include "CGameHandler.h" -#include "../lib/CPlayerState.h" +#include "../lib/battle/BattleInfo.h" #include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" +#include "../lib/CStack.h" #include "../lib/StartInfo.h" TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): @@ -30,7 +32,7 @@ void TurnTimerHandler::onGameplayStart(PlayerState & state) ttu.player = state.color; ttu.turnTimer = si->turnTimerInfo; ttu.turnTimer.turnTimer = 0; - gameHandler.applyAndSend(&ttu); + gameHandler.sendAndApply(&ttu); } } } @@ -47,7 +49,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerState & state) TurnTimeUpdate ttu; ttu.player = state.color; ttu.turnTimer = state.turnTimer; - gameHandler.applyAndSend(&ttu); + gameHandler.sendAndApply(&ttu); } } } @@ -64,14 +66,15 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) if(state.turnTimer.turnTimer > 0) { state.turnTimer.turnTimer -= waitTime; + int frequency = (state.turnTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && state.turnTimer.turnTimer % turnTimePropagateFrequency == 0) + && state.turnTimer.turnTimer % frequency == 0) { TurnTimeUpdate ttu; ttu.player = state.color; ttu.turnTimer = state.turnTimer; - gameHandler.applyAndSend(&ttu); + gameHandler.sendAndApply(&ttu); } } else if(state.turnTimer.baseTimer > 0) @@ -84,3 +87,76 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) gameHandler.states.players.at(state.color).makingTurn = false; //force end turn } } + +void TurnTimerHandler::onBattleStart(PlayerState & state) +{ + if(const auto * si = gameHandler.getStartInfo()) + { + if(si->turnTimerInfo.isBattleEnabled()) + { + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + ttu.turnTimer.battleTimer = si->turnTimerInfo.battleTimer; + ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; + gameHandler.sendAndApply(&ttu); + } + } +} + +void TurnTimerHandler::onBattleNextStack(PlayerState & state) +{ + if(const auto * si = gameHandler.getStartInfo()) + { + if(si->turnTimerInfo.isBattleEnabled()) + { + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + if(state.turnTimer.battleTimer < si->turnTimerInfo.battleTimer) + ttu.turnTimer.battleTimer = ttu.turnTimer.creatureTimer; + ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; + gameHandler.sendAndApply(&ttu); + } + } +} + +void TurnTimerHandler::onBattleLoop(PlayerState & state, int waitTime) +{ + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs || !gs->curB) + return; + + if(si->turnTimerInfo.isBattleEnabled()) + { + if(state.turnTimer.creatureTimer > 0) + { + state.turnTimer.creatureTimer -= waitTime; + int frequency = (state.turnTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); + + if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already + && state.turnTimer.creatureTimer % frequency == 0) + { + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + gameHandler.sendAndApply(&ttu); + } + } + else if(state.turnTimer.battleTimer > 0) + { + state.turnTimer.creatureTimer = state.turnTimer.battleTimer; + state.turnTimer.battleTimer = 0; + onBattleLoop(state, waitTime); + } + else if(auto * stack = const_cast(gs->curB.get())->getStack(gs->curB->getActiveStackID())) + { + BattleAction doNothing; + doNothing.actionType = EActionType::DEFEND; + doNothing.side = stack->unitSide(); + doNothing.stackNumber = stack->unitId(); + gameHandler.makeAutomaticAction(stack, doNothing); + } + } +} diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 03bd8d30b..f42d50de1 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -12,6 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN +class CStack; class PlayerColor; struct PlayerState; @@ -23,6 +24,8 @@ class TurnTimerHandler { CGameHandler & gameHandler; const int turnTimePropagateFrequency = 5000; + const int turnTimePropagateFrequencyCrit = 1000; + const int turnTimePropagateThreshold = 3000; public: TurnTimerHandler(CGameHandler &); @@ -30,4 +33,7 @@ public: void onGameplayStart(PlayerState & state); void onPlayerGetTurn(PlayerState & state); void onPlayerMakingTurn(PlayerState & state, int waitTime); + void onBattleStart(PlayerState & state); + void onBattleNextStack(PlayerState & state); + void onBattleLoop(PlayerState & state, int waitTime); }; From b2c8cab9f1835893cbef2a025f9175bf8fe91ddb Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 22:22:43 +0200 Subject: [PATCH 0074/1248] use playerColored --- Mods/vcmi/Data/DlgBluBk.png | Bin 74379 -> 0 bytes client/lobby/OptionsTab.cpp | 4 +++- client/lobby/OptionsTab.h | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) delete mode 100644 Mods/vcmi/Data/DlgBluBk.png diff --git a/Mods/vcmi/Data/DlgBluBk.png b/Mods/vcmi/Data/DlgBluBk.png deleted file mode 100644 index acbc2499790bbefaef06203e115ee31ea46af484..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 74379 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%0D2;8ZPJyUSFGi2)4B2gJr&O8YsjwD)+F&Dz> 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"DlgBluBk", pos); + backgroundTexture = std::make_shared("DiBoxBck", pos); + backgroundTexture->playerColored(PlayerColor(1)); updateShadow(); if(type == SelType::TOWN) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 4abf46ae5..e4a249099 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -17,6 +17,7 @@ struct PlayerInfo; VCMI_LIB_NAMESPACE_END #include "../widgets/Scrollable.h" +#include "../widgets/Images.h" #include "../../lib/mapping/CMapHeader.h" class CSlider; @@ -102,7 +103,7 @@ public: PlayerColor color; SelType type; - std::shared_ptr backgroundTexture; + std::shared_ptr backgroundTexture; std::vector> components; std::vector factions; From 0452dbd80d4c2636393ae370c418e77d7b3f82ea Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 14 Aug 2023 23:08:20 +0200 Subject: [PATCH 0075/1248] remove copy of cropped asset --- Mods/vcmi/Data/lobby/selectionEmptyLine.png | Bin 4588 -> 0 bytes client/lobby/SelectionTab.cpp | 3 ++- 2 files changed, 2 insertions(+), 1 deletion(-) delete mode 100644 Mods/vcmi/Data/lobby/selectionEmptyLine.png diff --git a/Mods/vcmi/Data/lobby/selectionEmptyLine.png b/Mods/vcmi/Data/lobby/selectionEmptyLine.png deleted file mode 100644 index 93328649212fe0dff24e8915a7781c45a3c6fc51..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4588 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 diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 0b3af5316..e0fd75c59 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -27,6 +27,7 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" +#include "../render/IImage.h" #include "../../CCallback.h" @@ -800,7 +801,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico : CIntObject(LCLICK, position) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pictureEmptyLine = std::make_shared("lobby/selectionEmptyLine.png", -8, -14); + pictureEmptyLine = std::make_shared(IImage::createFromFile("camcust"), Rect(25, 121, 349, 26), -8, -14); labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); From 71bcc611f5e6de15c2383b2be177932719c44ef1 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 15 Aug 2023 20:19:13 +0200 Subject: [PATCH 0076/1248] review --- client/lobby/SelectionTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index e0fd75c59..6b2715cad 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -490,9 +490,9 @@ void SelectionTab::sort() std::stable_sort(curItems.begin(), curItems.end(), mapSorter(generalSortingBy)); std::stable_sort(curItems.begin(), curItems.end(), mapSorter(sortingBy)); - int firstMap = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); + int firstMapIndex = boost::range::find_if(curItems, [](std::shared_ptr e) { return !e->isFolder; }) - curItems.begin(); if(!sortModeAscending) - std::reverse(std::next(curItems.begin(), firstMap), curItems.end()); + std::reverse(std::next(curItems.begin(), firstMapIndex), curItems.end()); updateListItems(); redraw(); From 647c7c15a3107df5b7a124bfb12d90709dc8a1a0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 17 Aug 2023 01:10:03 +0400 Subject: [PATCH 0077/1248] Add sound notification for timer --- Mods/vcmi/Sounds/we5.wav | Bin 0 -> 217908 bytes Mods/vcmi/mod.json | 4 +++ client/adventureMap/TurnTimerWidget.cpp | 41 ++++++++++++++---------- client/adventureMap/TurnTimerWidget.h | 4 +-- config/widgets/turnTimer.json | 8 ++++- 5 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 Mods/vcmi/Sounds/we5.wav diff --git a/Mods/vcmi/Sounds/we5.wav b/Mods/vcmi/Sounds/we5.wav new file mode 100644 index 0000000000000000000000000000000000000000..47901221e184fd2074be4aad377096b2d0aaa57b GIT binary patch literal 217908 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 literal 0 HcmV?d00001 diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index 0a0702f0b..8f7618196 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -190,6 +190,10 @@ "MAPS/": [ {"type" : "dir", "path" : "/Maps"} + ], + "SOUNDS/": + [ + {"type" : "dir", "path" : "/Sounds"} ] } } diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 5d47a4e45..476ffe0c8 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "TurnTimerWidget.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../render/EFont.h" @@ -44,6 +46,11 @@ TurnTimerWidget::TurnTimerWidget(): const JsonNode config(ResourceID("config/widgets/turnTimer.json")); build(config); + + std::transform(variables["notificationTime"].Vector().begin(), + variables["notificationTime"].Vector().end(), + std::inserter(notifications, notifications.begin()), + [](const JsonNode & node){ return node.Integer(); }); } std::shared_ptr TurnTimerWidget::buildDrawRect(const JsonNode & config) const @@ -61,7 +68,10 @@ void TurnTimerWidget::show(Canvas & to) void TurnTimerWidget::setTime(int time) { - turnTime = time / 1000; + int newTime = time / 1000; + if((newTime != turnTime) && notifications.count(newTime)) + CCS->soundh->playSound(variables["notificationSound"].String()); + turnTime = newTime; if(auto w = widget("timer")) { std::ostringstream oss; @@ -79,28 +89,25 @@ void TurnTimerWidget::tick(uint32_t msPassed) cachedTurnTime -= msPassed; if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero + auto timeCheckAndUpdate = [&](int time) + { + if(time / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time; + cachedTurnTime = time; + } + else setTime(cachedTurnTime); + }; + if(LOCPLINT->battleInt) { if(time.isBattleEnabled()) - { - if(time.creatureTimer / 1000 != lastTurnTime / 1000) - { - //do not update timer on this tick - lastTurnTime = time.creatureTimer; - cachedTurnTime = time.creatureTimer; - } - else setTime(cachedTurnTime); - } + timeCheckAndUpdate(time.creatureTimer); } else { - if(time.turnTimer / 1000 != lastTurnTime / 1000) - { - //do not update timer on this tick - lastTurnTime = time.turnTimer; - cachedTurnTime = time.turnTimer; - } - else setTime(cachedTurnTime); + timeCheckAndUpdate(time.turnTimer); } } } diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index 63432e0a0..592143e79 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -36,14 +36,12 @@ private: int lastTurnTime; int cachedTurnTime; - //std::shared_ptr watches; - //std::shared_ptr label; + std::set notifications; std::shared_ptr buildDrawRect(const JsonNode & config) const; public: - //void tick(uint32_t msPassed) override; void show(Canvas & to) override; void tick(uint32_t msPassed) override; diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json index 704646f83..fce385ab2 100644 --- a/config/widgets/turnTimer.json +++ b/config/widgets/turnTimer.json @@ -23,5 +23,11 @@ "text": "", "position": {"x": 24, "y": 2} }, - ] + ], + + "variables": + { + "notificationTime": [0, 1, 2, 3, 4, 5, 20], + "notificationSound": "WE5" + } } From edc11fd4510e463cfaac0497d6865525760ebe14 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 09:56:50 +0200 Subject: [PATCH 0078/1248] Map preview initial draft --- client/lobby/SelectionTab.cpp | 68 ++++++++++++++++++++++++++++++++++- client/lobby/SelectionTab.h | 12 ++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 592f70048..35c0667d6 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -18,6 +18,7 @@ #include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -27,6 +28,7 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" +#include "../render/Canvas.h" #include "../render/IImage.h" #include "../../CCallback.h" @@ -37,9 +39,12 @@ #include "../../lib/GameSettings.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/campaign/CampaignState.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapping/CMapService.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" +#include "../../lib/TerrainHandler.h" #include "../../lib/serializer/Connection.h" bool mapSorter::operator()(const std::shared_ptr aaa, const std::shared_ptr bbb) @@ -353,7 +358,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - CRClickPopup::createAndPush(text); + GH.windows().createAndPushWindow(text, ResourceID(curItems[py]->fileURI), tabType == ESelectionScreen::newGame); } } @@ -797,6 +802,67 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re return ret; } +SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceID resource, bool renderImage) + : CWindowObject(BORDERED | RCLICK_POPUP) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(0, 0, 250, 2000); + + label = std::make_shared(text, Rect(20, 20, 250-40, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + if(!label->slider) + label->resize(label->label->textSize); + + pos.h = 20 + label->label->textSize.y + 20; + if(renderImage) + pos.h += 200 + 20; + backgroundTexture = std::make_shared("DIBOXBCK", pos); + updateShadow(); + + // TODO: hacky redraw + label = std::make_shared(text, Rect(20, 20, 250-40, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + if(!label->slider) + label->resize(label->label->textSize); + + if(renderImage) + { + std::shared_ptr img = redrawMinimap(ResourceID(resource.getName(), EResType::MAP)); + image = std::make_shared(img, Point(25, label->label->textSize.y + 40)); + } + + center(GH.getCursorPosition()); //center on mouse +#ifdef VCMI_MOBILE + moveBy({0, -pos.h / 2}); +#endif + fitToScreen(10); +} + +std::shared_ptr SelectionTab::CMapInfoTooltipBox::redrawMinimap(ResourceID resource) +{ + CMapService mapService; + std::unique_ptr map = mapService.loadMap(resource); + Canvas canvas = Canvas(Point(map->width, map->height)); + + for (int y = 0; y < map->height; ++y) + for (int x = 0; x < map->width; ++x) + { + TerrainTile & tile = map->getTile(int3(x, y, 0)); + + ColorRGBA color = tile.terType->minimapUnblocked; + if (tile.blocked && (!tile.visitable)) + color = tile.terType->minimapBlocked; + + canvas.drawPoint(Point(x, y), color); + } + + Canvas canvasScaled = Canvas(Point(200, 200)); + canvasScaled.drawScaled(canvas, Point(0, 0), Point(200, 200)); + + std::shared_ptr img = IImage::createFromSurface(canvasScaled.getInternalSurface()); + + return img; +} + SelectionTab::ListItem::ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss) : CIntObject(LCLICK, position) { diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 5e8e35e92..da44cfe2f 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -11,6 +11,7 @@ #include "CSelectionBase.h" #include "../../lib/mapping/CMapInfo.h" +#include "../widgets/Images.h" class CSlider; class CLabel; @@ -62,6 +63,16 @@ class SelectionTab : public CIntObject std::shared_ptr iconsVictoryCondition; std::shared_ptr iconsLossCondition; + class CMapInfoTooltipBox : public CWindowObject + { + std::shared_ptr backgroundTexture; + std::shared_ptr label; + std::shared_ptr image; + + std::shared_ptr redrawMinimap(ResourceID resource); + public: + CMapInfoTooltipBox(std::string text, ResourceID resource, bool renderImage); + }; public: std::vector> allItems; std::vector> curItems; @@ -100,7 +111,6 @@ public: void restoreLastSelection(); private: - std::shared_ptr background; std::shared_ptr slider; std::vector> buttonsSortBy; From e1e16018d1f6a52f79bd033da51cdd31098d5ac3 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:29:31 +0200 Subject: [PATCH 0079/1248] code review first batch --- client/CServerHandler.cpp | 2 +- client/lobby/OptionsTab.cpp | 42 ++++++++++++++++++------------------- client/lobby/OptionsTab.h | 4 ++-- lib/NetPacksLobby.h | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index cfbb48d6f..5eaa021ea 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -454,7 +454,7 @@ void CServerHandler::setPlayer(PlayerColor color) const sendLobbyPack(lsp); } -void CServerHandler::setPlayerOption(ui8 what, si16 value, PlayerColor player) const +void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const { LobbyChangePlayerOption lcpo; lcpo.what = what; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index c511ffebb..33ba964c0 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -515,7 +515,7 @@ void OptionsTab::SelectionWindow::recreate() elementsPerLine = floor(sqrt(count)); } - amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : static_cast(-1)); + amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : static_cast(PlayerSettings::RANDOM)); } int x = (elementsPerLine) * 57; @@ -549,9 +549,9 @@ void OptionsTab::SelectionWindow::drawOutlinedText(int x, int y, ColorRGBA color void OptionsTab::SelectionWindow::genContentGrid(int lines) { - for(int y = 0; y("lobby/townBorderBig", x * 57, y * 63)); } @@ -564,11 +564,11 @@ void OptionsTab::SelectionWindow::genContentFactions() // random PlayerSettings set = PlayerSettings(); - set.castle = -1; + set.castle = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - drawOutlinedText(29, 56, (selectedFaction == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedFaction == -1) + drawOutlinedText(29, 56, (selectedFaction == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedFaction == PlayerSettings::RANDOM) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedFactions) @@ -596,11 +596,11 @@ void OptionsTab::SelectionWindow::genContentHeroes() // random PlayerSettings set = PlayerSettings(); - set.hero = -1; + set.hero = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - drawOutlinedText(29, 56, (selectedHero == -1) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedHero == -1) + drawOutlinedText(29, 56, (selectedHero == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedHero == PlayerSettings::RANDOM) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); for(auto & elem : allowedHeroes) @@ -675,10 +675,10 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { } else { - set.castle = -1; + set.castle = PlayerSettings::RANDOM; } - if(set.castle != -2) + if(set.castle != PlayerSettings::NONE) selectedFaction = set.castle; } if(type == SelType::HERO) @@ -692,9 +692,9 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { } else { - set.hero = -1; + set.hero = PlayerSettings::RANDOM; } - if(set.hero != -2) + if(set.hero != PlayerSettings::NONE) selectedHero = set.hero; } if(type == SelType::BONUS) @@ -702,7 +702,7 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { if(elem >= allowedBonus.size()) return; set.bonus = static_cast(allowedBonus[elem]); - if(set.bonus != -2) + if(set.bonus != PlayerSettings::NONE) selectedBonus = set.bonus; } apply(); @@ -724,9 +724,9 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } else { - set.castle = -1; + set.castle = PlayerSettings::RANDOM; } - if(set.castle != -2) + if(set.castle != PlayerSettings::NONE) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); GH.windows().createAndPushWindow(helper); @@ -743,9 +743,9 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } else { - set.hero = -1; + set.hero = PlayerSettings::RANDOM; } - if(set.hero != -2) + if(set.hero != PlayerSettings::NONE) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); GH.windows().createAndPushWindow(helper); @@ -756,7 +756,7 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) if(elem >= 4) return; set.bonus = static_cast(elem-1); - if(set.bonus != -2) + if(set.bonus != PlayerSettings::NONE) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); GH.windows().createAndPushWindow(helper); @@ -787,9 +787,9 @@ void OptionsTab::SelectedBox::update() void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) { // cases when we do not need to display a message - if(settings.castle == -2 && CPlayerSettingsHelper::type == TOWN) + if(settings.castle == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) return; - if(settings.hero == -2 && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + if(settings.hero == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; GH.windows().createAndPushWindow(*this); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index e4a249099..355a8e4f2 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -17,8 +17,6 @@ struct PlayerInfo; VCMI_LIB_NAMESPACE_END #include "../widgets/Scrollable.h" -#include "../widgets/Images.h" -#include "../../lib/mapping/CMapHeader.h" class CSlider; class CLabel; @@ -29,6 +27,8 @@ class CComponentBox; class CTextBox; class CButton; +class FilledTexturePlayerColored; + /// The options tab which is shown at the map selection phase. class OptionsTab : public CIntObject { diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index 6fce191c2..09f459326 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -213,7 +213,7 @@ struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer { enum EWhat : ui8 {UNKNOWN, TOWN, HERO, BONUS, TOWN_ID, HERO_ID, BONUS_ID}; ui8 what = UNKNOWN; - si16 value = 0; //-1 or +1 + int32_t value = 0; PlayerColor color = PlayerColor::CANNOT_DETERMINE; virtual void visitTyped(ICPackVisitor & visitor) override; From e61dc2ec2315e2bde273977262ea841c05c5b1de Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:54:53 +0200 Subject: [PATCH 0080/1248] constants --- client/CServerHandler.h | 2 +- client/lobby/OptionsTab.cpp | 42 ++++++++++++++++++------------------- client/lobby/OptionsTab.h | 7 +++++++ 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 394599c60..d6ca437c8 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -62,7 +62,7 @@ public: virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; - virtual void setPlayerOption(ui8 what, si16 value, PlayerColor player) const = 0; + virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnLength(int npos) const = 0; virtual void sendMessage(const std::string & txt) const = 0; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 33ba964c0..8857cbb11 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -518,8 +518,8 @@ void OptionsTab::SelectionWindow::recreate() amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : static_cast(PlayerSettings::RANDOM)); } - int x = (elementsPerLine) * 57; - int y = (amountLines) * 63; + int x = (elementsPerLine) * (ICON_BIG_WIDTH-1); + int y = (amountLines) * (ICON_BIG_HEIGHT-1); pos = Rect(0, 0, x, y); @@ -553,7 +553,7 @@ void OptionsTab::SelectionWindow::genContentGrid(int lines) { for(int x = 0; x < elementsPerLine; x++) { - components.push_back(std::make_shared("lobby/townBorderBig", x * 57, y * 63)); + components.push_back(std::make_shared("lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); } } } @@ -566,10 +566,10 @@ void OptionsTab::SelectionWindow::genContentFactions() PlayerSettings set = PlayerSettings(); set.castle = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - drawOutlinedText(29, 56, (selectedFaction == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedFaction == PlayerSettings::RANDOM) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); + components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedFactions) { @@ -581,9 +581,9 @@ void OptionsTab::SelectionWindow::genContentFactions() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); - components.push_back(std::make_shared(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedFaction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + components.push_back(std::make_shared(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedFaction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); factions.push_back(elem); i++; @@ -598,10 +598,10 @@ void OptionsTab::SelectionWindow::genContentHeroes() PlayerSettings set = PlayerSettings(); set.hero = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, 32 / 2)); - drawOutlinedText(29, 56, (selectedHero == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedHero == PlayerSettings::RANDOM) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, 32 / 2)); + components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedHeroes) { @@ -618,9 +618,9 @@ void OptionsTab::SelectionWindow::genContentHeroes() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * 57, y * 63)); - components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * 57, y * 63)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); heroes.push_back(elem); i++; @@ -640,12 +640,12 @@ void OptionsTab::SelectionWindow::genContentBonus() set.bonus = static_cast(elem); CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * 57 + 6, y * 63 + 32 / 2)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::WHITE , helper.getName()); + components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::WHITE , helper.getName()); if(selectedBonus == elem) { - components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * 57 + 6, y * 63 + 32 / 2)); - drawOutlinedText(x * 57 + 29, y * 63 + 56, Colors::YELLOW , helper.getName()); + components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); + drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::YELLOW , helper.getName()); } i++; @@ -654,8 +654,8 @@ void OptionsTab::SelectionWindow::genContentBonus() int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) { - int x = (cursorPosition.x - pos.x) / 57; - int y = (cursorPosition.y - pos.y) / 63; + int x = (cursorPosition.x - pos.x) / (ICON_BIG_WIDTH-1); + int y = (cursorPosition.y - pos.y) / (ICON_BIG_HEIGHT-1); return x + y * elementsPerLine; } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 355a8e4f2..32c10bd1a 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -98,6 +98,13 @@ public: class SelectionWindow : public CWindowObject { + const int ICON_SMALL_WIDTH = 48; + const int ICON_SMALL_HEIGHT = 32; + const int ICON_BIG_WIDTH = 58; + const int ICON_BIG_HEIGHT = 64; + const int TEXT_POS_X = 29; + const int TEXT_POS_Y = 56; + int elementsPerLine; PlayerColor color; From 077ec65bf6a8fee33b42dee48e5df52efc5b3499 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:15:42 +0200 Subject: [PATCH 0081/1248] simplify --- client/CServerHandler.h | 2 +- client/lobby/OptionsTab.cpp | 90 ++++++++++++++----------------------- client/lobby/OptionsTab.h | 1 + 3 files changed, 35 insertions(+), 58 deletions(-) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index d6ca437c8..6b22d1da8 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -140,7 +140,7 @@ public: void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; - void setPlayerOption(ui8 what, si16 value, PlayerColor player) const override; + void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnLength(int npos) const override; void sendMessage(const std::string & txt) const override; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 8857cbb11..7d16085f4 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -660,58 +660,8 @@ int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) return x + y * elementsPerLine; } -void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { - int elem = getElement(cursorPosition); - - PlayerSettings set = PlayerSettings(); - if(type == SelType::TOWN) - { - if(elem > 0) - { - elem--; - if(elem >= factions.size()) - return; - set.castle = factions[elem]; - } - else - { - set.castle = PlayerSettings::RANDOM; - } - - if(set.castle != PlayerSettings::NONE) - selectedFaction = set.castle; - } - if(type == SelType::HERO) - { - if(elem > 0) - { - elem--; - if(elem >= heroes.size()) - return; - set.hero = heroes[elem]; - } - else - { - set.hero = PlayerSettings::RANDOM; - } - if(set.hero != PlayerSettings::NONE) - selectedHero = set.hero; - } - if(type == SelType::BONUS) - { - if(elem >= allowedBonus.size()) - return; - set.bonus = static_cast(allowedBonus[elem]); - if(set.bonus != PlayerSettings::NONE) - selectedBonus = set.bonus; - } - apply(); -} - -void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) +void OptionsTab::SelectionWindow::setElement(int elem, bool apply) { - int elem = getElement(cursorPosition); - PlayerSettings set = PlayerSettings(); if(type == SelType::TOWN) { @@ -728,8 +678,11 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } if(set.castle != PlayerSettings::NONE) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); - GH.windows().createAndPushWindow(helper); + if(!apply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); + GH.windows().createAndPushWindow(helper); + } } } if(type == SelType::HERO) @@ -747,8 +700,11 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) } if(set.hero != PlayerSettings::NONE) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - GH.windows().createAndPushWindow(helper); + if(!apply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); + GH.windows().createAndPushWindow(helper); + } } } if(type == SelType::BONUS) @@ -758,10 +714,30 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) set.bonus = static_cast(elem-1); if(set.bonus != PlayerSettings::NONE) { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - GH.windows().createAndPushWindow(helper); + if(!apply) + { + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + GH.windows().createAndPushWindow(helper); + } } } + + if(apply) + apply(); +} + +void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) +{ + int elem = getElement(cursorPosition); + + setElement(elem, true); +} + +void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) +{ + int elem = getElement(cursorPosition); + + setElement(elem, false); } OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 32c10bd1a..7efa656e7 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -138,6 +138,7 @@ public: void recreate(); void setSelection(); int getElement(const Point & cursorPosition); + void setElement(int element); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; From ac64b9f2b5e797d647960cd8172a41dea71298bc Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 18:52:00 +0200 Subject: [PATCH 0082/1248] fix --- client/lobby/OptionsTab.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 7efa656e7..fab3a5859 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -138,7 +138,7 @@ public: void recreate(); void setSelection(); int getElement(const Point & cursorPosition); - void setElement(int element); + void setElement(int element, bool apply); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; From af347242b4f196cef9fd67d823c6ac17a69c6f37 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 20:34:31 +0200 Subject: [PATCH 0083/1248] fix compile error --- client/lobby/OptionsTab.cpp | 10 +++++----- client/lobby/OptionsTab.h | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 7d16085f4..3892eae44 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -660,7 +660,7 @@ int OptionsTab::SelectionWindow::getElement(const Point & cursorPosition) return x + y * elementsPerLine; } -void OptionsTab::SelectionWindow::setElement(int elem, bool apply) +void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { PlayerSettings set = PlayerSettings(); if(type == SelType::TOWN) @@ -678,7 +678,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool apply) } if(set.castle != PlayerSettings::NONE) { - if(!apply) + if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); GH.windows().createAndPushWindow(helper); @@ -700,7 +700,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool apply) } if(set.hero != PlayerSettings::NONE) { - if(!apply) + if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); GH.windows().createAndPushWindow(helper); @@ -714,7 +714,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool apply) set.bonus = static_cast(elem-1); if(set.bonus != PlayerSettings::NONE) { - if(!apply) + if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); GH.windows().createAndPushWindow(helper); @@ -722,7 +722,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool apply) } } - if(apply) + if(doApply) apply(); } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index fab3a5859..fc8f515b7 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -98,7 +98,7 @@ public: class SelectionWindow : public CWindowObject { - const int ICON_SMALL_WIDTH = 48; + //const int ICON_SMALL_WIDTH = 48; const int ICON_SMALL_HEIGHT = 32; const int ICON_BIG_WIDTH = 58; const int ICON_BIG_HEIGHT = 64; @@ -138,7 +138,7 @@ public: void recreate(); void setSelection(); int getElement(const Point & cursorPosition); - void setElement(int element, bool apply); + void setElement(int element, bool doApply); void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; From e44f713f01bb6481e65c9dfc9bedce97ec9b4e65 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:09:20 +0200 Subject: [PATCH 0084/1248] side by side map; player colors --- client/lobby/SelectionTab.cpp | 79 ++++++++++++++++++++++++++--------- client/lobby/SelectionTab.h | 18 ++++++-- 2 files changed, 74 insertions(+), 23 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 35c0667d6..aec42454e 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -30,6 +30,7 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/Graphics.h" #include "../../CCallback.h" @@ -358,7 +359,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - GH.windows().createAndPushWindow(text, ResourceID(curItems[py]->fileURI), tabType == ESelectionScreen::newGame); + GH.windows().createAndPushWindow(text, ResourceID(curItems[py]->fileURI), tabType); } } @@ -802,32 +803,42 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re return ret; } -SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceID resource, bool renderImage) +SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType) : CWindowObject(BORDERED | RCLICK_POPUP) { + drawPlayerElements = tabType == ESelectionScreen::newGame; + renderImage = tabType == ESelectionScreen::newGame; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(0, 0, 250, 2000); + std::vector> images; + if(renderImage) + images = redrawMinimap(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); - label = std::make_shared(text, Rect(20, 20, 250-40, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + pos = Rect(0, 0, 2*BORDER + IMAGE_SIZE, 2000); + if(renderImage && images.size() > 1) + pos.w += IMAGE_SIZE + BORDER; + + label = std::make_shared(text, Rect(BORDER, BORDER, pos.w-2*BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!label->slider) label->resize(label->label->textSize); - pos.h = 20 + label->label->textSize.y + 20; + pos.h = BORDER + label->label->textSize.y + BORDER; if(renderImage) - pos.h += 200 + 20; + pos.h += IMAGE_SIZE + BORDER; backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); // TODO: hacky redraw - label = std::make_shared(text, Rect(20, 20, 250-40, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + label = std::make_shared(text, Rect(BORDER, BORDER, pos.w-2*BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!label->slider) label->resize(label->label->textSize); if(renderImage) { - std::shared_ptr img = redrawMinimap(ResourceID(resource.getName(), EResType::MAP)); - image = std::make_shared(img, Point(25, label->label->textSize.y + 40)); + image1 = std::make_shared(images[0], Point(BORDER, label->label->textSize.y + 2*BORDER)); + if(images.size()>1) + image2 = std::make_shared(images[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2*BORDER)); } center(GH.getCursorPosition()); //center on mouse @@ -837,30 +848,60 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI fitToScreen(10); } -std::shared_ptr SelectionTab::CMapInfoTooltipBox::redrawMinimap(ResourceID resource) +Canvas SelectionTab::CMapInfoTooltipBox::createMinimap(std::unique_ptr & map, int layer) { - CMapService mapService; - std::unique_ptr map = mapService.loadMap(resource); Canvas canvas = Canvas(Point(map->width, map->height)); for (int y = 0; y < map->height; ++y) for (int x = 0; x < map->width; ++x) { - TerrainTile & tile = map->getTile(int3(x, y, 0)); + TerrainTile & tile = map->getTile(int3(x, y, layer)); ColorRGBA color = tile.terType->minimapUnblocked; if (tile.blocked && (!tile.visitable)) color = tile.terType->minimapBlocked; + if(drawPlayerElements) + // if object at tile is owned - it will be colored as its owner + for (const CGObjectInstance *obj : tile.blockingObjects) + { + PlayerColor player = obj->getOwner(); + if(player == PlayerColor::NEUTRAL) + { + color = graphics->neutralColor; + break; + } + if (player < PlayerColor::PLAYER_LIMIT) + { + color = graphics->playerColors[player.getNum()]; + break; + } + } + canvas.drawPoint(Point(x, y), color); } - - Canvas canvasScaled = Canvas(Point(200, 200)); - canvasScaled.drawScaled(canvas, Point(0, 0), Point(200, 200)); - - std::shared_ptr img = IImage::createFromSurface(canvasScaled.getInternalSurface()); - return img; + return canvas; +} + +std::vector> SelectionTab::CMapInfoTooltipBox::redrawMinimap(ResourceID resource, int size) +{ + std::vector> ret = std::vector>(); + + CMapService mapService; + std::unique_ptr map = mapService.loadMap(resource); + + for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) + { + Canvas canvas = createMinimap(map, i); + Canvas canvasScaled = Canvas(Point(size, size)); + canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size)); + std::shared_ptr img = IImage::createFromSurface(canvasScaled.getInternalSurface()); + + ret.push_back(img); + } + + return ret; } SelectionTab::ListItem::ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss) diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index da44cfe2f..ec1aa4b30 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -11,10 +11,12 @@ #include "CSelectionBase.h" #include "../../lib/mapping/CMapInfo.h" -#include "../widgets/Images.h" class CSlider; class CLabel; +class CMap; +class CPicture; +class IImage; enum ESortBy { @@ -65,13 +67,21 @@ class SelectionTab : public CIntObject class CMapInfoTooltipBox : public CWindowObject { + const int IMAGE_SIZE = 169; + const int BORDER = 30; + + bool drawPlayerElements; + bool renderImage; + std::shared_ptr backgroundTexture; std::shared_ptr label; - std::shared_ptr image; + std::shared_ptr image1; + std::shared_ptr image2; - std::shared_ptr redrawMinimap(ResourceID resource); + Canvas createMinimap(std::unique_ptr & map, int layer); + std::vector> redrawMinimap(ResourceID resource, int size=144); public: - CMapInfoTooltipBox(std::string text, ResourceID resource, bool renderImage); + CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType); }; public: std::vector> allItems; From f9fa27b1800c5ef498ddaace7918e8ad095eaf11 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 17 Aug 2023 23:40:19 +0200 Subject: [PATCH 0085/1248] fix --- client/lobby/SelectionTab.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index ec1aa4b30..2894a7ad3 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -10,11 +10,11 @@ #pragma once #include "CSelectionBase.h" +#include "../../lib/mapping/CMap.h" #include "../../lib/mapping/CMapInfo.h" class CSlider; class CLabel; -class CMap; class CPicture; class IImage; From 8635c241666597c444b17ede1c6a0b64913ee296 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 18 Aug 2023 00:15:47 +0200 Subject: [PATCH 0086/1248] fix --- client/lobby/OptionsTab.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 3892eae44..a1c553466 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -683,6 +683,8 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); GH.windows().createAndPushWindow(helper); } + else + selectedFaction = set.castle; } } if(type == SelType::HERO) @@ -705,6 +707,8 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); GH.windows().createAndPushWindow(helper); } + else + selectedHero = set.hero; } } if(type == SelType::BONUS) @@ -719,6 +723,8 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); GH.windows().createAndPushWindow(helper); } + else + selectedBonus = set.bonus; } } From e25c070f595a1c11fbc34babe4f124abbdd11b05 Mon Sep 17 00:00:00 2001 From: DjWarmonger Date: Fri, 18 Aug 2023 12:22:01 +0200 Subject: [PATCH 0087/1248] Update README.md - Add counter for 1.3.1 - Remove abandoned Coverity status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 18ca646d1..5c71fb47d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) # VCMI Project From 4c503f8df372669373b6b1524f7e34dce53217fb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 18 Aug 2023 15:13:38 +0300 Subject: [PATCH 0088/1248] Fix merge --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 05ff3fd27..b3ef9ce8c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) From 2fff74deea4f202c95b04147e8cabe15bf7a91c5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 19 Aug 2023 18:59:31 +0400 Subject: [PATCH 0089/1248] Remove uninitialized variable --- client/adventureMap/TurnTimerWidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 476ffe0c8..28a2a314d 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -37,7 +37,7 @@ void TurnTimerWidget::DrawRect::showAll(Canvas & to) TurnTimerWidget::TurnTimerWidget(): InterfaceObjectConfigurable(TIME), - turnTime(0), lastTurnTime(0) + turnTime(0), lastTurnTime(0), cachedTurnTime(0) { REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect); From d4ab3087cb25bed8aca2cba6f58a31538d98e29c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 19 Aug 2023 19:00:08 +0400 Subject: [PATCH 0090/1248] Disable timer for AI players --- server/TurnTimerHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 4e7b918b8..fa477094a 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -61,7 +61,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) if(!si || !gs) return; - if(si->turnTimerInfo.isEnabled() && !gs->curB) + if(state.human && si->turnTimerInfo.isEnabled() && !gs->curB) { if(state.turnTimer.turnTimer > 0) { @@ -128,7 +128,7 @@ void TurnTimerHandler::onBattleLoop(PlayerState & state, int waitTime) if(!si || !gs || !gs->curB) return; - if(si->turnTimerInfo.isBattleEnabled()) + if(state.human && si->turnTimerInfo.isBattleEnabled()) { if(state.turnTimer.creatureTimer > 0) { From 314a17cdd9b062aa22167a52fe967b30220ed60d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 19 Aug 2023 19:45:25 +0400 Subject: [PATCH 0091/1248] Paint timer color into player color --- client/adventureMap/TurnTimerWidget.cpp | 8 ++++++++ config/widgets/turnTimer.json | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 28a2a314d..de980863f 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -15,6 +15,7 @@ #include "../CPlayerInterface.h" #include "../render/EFont.h" +#include "../render/Graphics.h" #include "../gui/CGuiHandler.h" #include "../gui/TextAlignment.h" #include "../widgets/Images.h" @@ -77,6 +78,13 @@ void TurnTimerWidget::setTime(int time) std::ostringstream oss; oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60; w->setText(oss.str()); + + if(graphics && LOCPLINT && LOCPLINT->cb + && variables["textColorFromPlayerColor"].Bool() + && LOCPLINT->cb->getCurrentPlayer().isValidPlayer()) + { + w->setColor(graphics->playerColors[LOCPLINT->cb->getCurrentPlayer()]); + } } } diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json index fce385ab2..c2c8b01cf 100644 --- a/config/widgets/turnTimer.json +++ b/config/widgets/turnTimer.json @@ -28,6 +28,7 @@ "variables": { "notificationTime": [0, 1, 2, 3, 4, 5, 20], - "notificationSound": "WE5" + "notificationSound": "WE5", + "textColorFromPlayerColor": true } } From a09cb23a85172c29e1f7a55a848a9f74c87314f6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 20 Aug 2023 04:06:52 +0400 Subject: [PATCH 0092/1248] Better handling other players for turn timer --- client/adventureMap/TurnTimerWidget.cpp | 22 +++++++++++++++------- server/TurnTimerHandler.cpp | 7 ++----- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index de980863f..842aa168b 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -70,7 +70,9 @@ void TurnTimerWidget::show(Canvas & to) void TurnTimerWidget::setTime(int time) { int newTime = time / 1000; - if((newTime != turnTime) && notifications.count(newTime)) + if((LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID) + && (newTime != turnTime) + && notifications.count(newTime)) CCS->soundh->playSound(variables["notificationSound"].String()); turnTime = newTime; if(auto w = widget("timer")) @@ -108,14 +110,20 @@ void TurnTimerWidget::tick(uint32_t msPassed) else setTime(cachedTurnTime); }; - if(LOCPLINT->battleInt) + auto * playerInfo = LOCPLINT->cb->getPlayer(player); + if(playerInfo && playerInfo->isHuman()) { - if(time.isBattleEnabled()) - timeCheckAndUpdate(time.creatureTimer); + if(LOCPLINT->battleInt) + { + if(time.isBattleEnabled()) + timeCheckAndUpdate(time.creatureTimer); + } + else + { + timeCheckAndUpdate(time.turnTimer); + } } else - { - timeCheckAndUpdate(time.turnTimer); - } + timeCheckAndUpdate(0); } } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index fa477094a..72cd002f2 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -28,11 +28,8 @@ void TurnTimerHandler::onGameplayStart(PlayerState & state) { if(si->turnTimerInfo.isEnabled()) { - TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = si->turnTimerInfo; - ttu.turnTimer.turnTimer = 0; - gameHandler.sendAndApply(&ttu); + state.turnTimer = si->turnTimerInfo; + state.turnTimer.turnTimer = 0; } } } From 52ed8241002691cd4b48b5bd5644b35bd5415ce9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 13:05:25 +0200 Subject: [PATCH 0093/1248] add sound; possibility to click outside to close --- client/lobby/OptionsTab.cpp | 18 +++++++++++++++++- client/lobby/OptionsTab.h | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index a1c553466..8a4fa6596 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -14,6 +14,7 @@ #include "../CGameInfo.h" #include "../CServerHandler.h" +#include "../CMusicHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" @@ -470,6 +471,7 @@ void OptionsTab::SelectionWindow::apply() if(GH.windows().isTopWindow(this)) { GH.input().hapticFeedback(); + CCS->soundh->playSound(soundBase::button); close(); @@ -732,8 +734,19 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) apply(); } +bool OptionsTab::SelectionWindow::receiveEvent(const Point & position, int eventType) const +{ + return true; // capture click also outside of window +} + void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) { + if(!pos.isInside(cursorPosition)) + { + close(); + return; + } + int elem = getElement(cursorPosition); setElement(elem, true); @@ -741,6 +754,9 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition) void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) { + if(!pos.isInside(cursorPosition)) + return; + int elem = getElement(cursorPosition); setElement(elem, false); @@ -939,4 +955,4 @@ void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() buttonBonusLeft->enable(); buttonBonusRight->enable(); } -} +} \ No newline at end of file diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index fc8f515b7..9af09bb6c 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -140,6 +140,7 @@ public: int getElement(const Point & cursorPosition); void setElement(int element, bool doApply); + bool receiveEvent(const Point & position, int eventType) const override; void clickReleased(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; From 1d94536299fc3f050227b138cff71854a399e6d1 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 13:16:39 +0200 Subject: [PATCH 0094/1248] code review --- client/lobby/SelectionTab.cpp | 18 +++++++++--------- client/lobby/SelectionTab.h | 8 +++++--- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index aec42454e..e0cc108a0 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -811,12 +811,12 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - std::vector> images; + std::vector> mapLayerImages; if(renderImage) - images = redrawMinimap(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); + mapLayerImages = createMinimaps(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); pos = Rect(0, 0, 2*BORDER + IMAGE_SIZE, 2000); - if(renderImage && images.size() > 1) + if(renderImage && mapLayerImages.size() > 1) pos.w += IMAGE_SIZE + BORDER; label = std::make_shared(text, Rect(BORDER, BORDER, pos.w-2*BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); @@ -836,9 +836,9 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI if(renderImage) { - image1 = std::make_shared(images[0], Point(BORDER, label->label->textSize.y + 2*BORDER)); - if(images.size()>1) - image2 = std::make_shared(images[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2*BORDER)); + image1 = std::make_shared(mapLayerImages[0], Point(BORDER, label->label->textSize.y + 2*BORDER)); + if(mapLayerImages.size()>1) + image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2*BORDER)); } center(GH.getCursorPosition()); //center on mouse @@ -848,7 +848,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI fitToScreen(10); } -Canvas SelectionTab::CMapInfoTooltipBox::createMinimap(std::unique_ptr & map, int layer) +Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptr & map, int layer) { Canvas canvas = Canvas(Point(map->width, map->height)); @@ -884,7 +884,7 @@ Canvas SelectionTab::CMapInfoTooltipBox::createMinimap(std::unique_ptr & m return canvas; } -std::vector> SelectionTab::CMapInfoTooltipBox::redrawMinimap(ResourceID resource, int size) +std::vector> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourceID resource, int size) { std::vector> ret = std::vector>(); @@ -893,7 +893,7 @@ std::vector> SelectionTab::CMapInfoTooltipBox::redrawMin for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) { - Canvas canvas = createMinimap(map, i); + Canvas canvas = createMinimapForLayer(map, i); Canvas canvasScaled = Canvas(Point(size, size)); canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size)); std::shared_ptr img = IImage::createFromSurface(canvasScaled.getInternalSurface()); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 2894a7ad3..50426b78d 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -10,7 +10,9 @@ #pragma once #include "CSelectionBase.h" -#include "../../lib/mapping/CMap.h" +VCMI_LIB_NAMESPACE_BEGIN +class CMap; +VCMI_LIB_NAMESPACE_END #include "../../lib/mapping/CMapInfo.h" class CSlider; @@ -78,8 +80,8 @@ class SelectionTab : public CIntObject std::shared_ptr image1; std::shared_ptr image2; - Canvas createMinimap(std::unique_ptr & map, int layer); - std::vector> redrawMinimap(ResourceID resource, int size=144); + Canvas createMinimapForLayer(std::unique_ptr & map, int layer); + std::vector> createMinimaps(ResourceID resource, int size); public: CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType); }; From e3a4c65100c6e64f9f62220d84016a29a972bacd Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 14:14:47 +0200 Subject: [PATCH 0095/1248] code review --- client/lobby/SelectionTab.cpp | 22 +++++++++++----------- config/schemas/settings.json | 14 +++++++++++++- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index e0cc108a0..f2522258e 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -807,7 +807,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI : CWindowObject(BORDERED | RCLICK_POPUP) { drawPlayerElements = tabType == ESelectionScreen::newGame; - renderImage = tabType == ESelectionScreen::newGame; + renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -815,13 +815,16 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI if(renderImage) mapLayerImages = createMinimaps(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); - pos = Rect(0, 0, 2*BORDER + IMAGE_SIZE, 2000); + pos = Rect(0, 0, 2 * BORDER + IMAGE_SIZE, 2000); if(renderImage && mapLayerImages.size() > 1) pos.w += IMAGE_SIZE + BORDER; - label = std::make_shared(text, Rect(BORDER, BORDER, pos.w-2*BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - if(!label->slider) - label->resize(label->label->textSize); + auto drawLabel = [&]() { + label = std::make_shared(text, Rect(BORDER, BORDER, pos.w - 2 * BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + if(!label->slider) + label->resize(label->label->textSize); + }; + drawLabel(); pos.h = BORDER + label->label->textSize.y + BORDER; if(renderImage) @@ -829,16 +832,13 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI backgroundTexture = std::make_shared("DIBOXBCK", pos); updateShadow(); - // TODO: hacky redraw - label = std::make_shared(text, Rect(BORDER, BORDER, pos.w-2*BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - if(!label->slider) - label->resize(label->label->textSize); + drawLabel(); if(renderImage) { - image1 = std::make_shared(mapLayerImages[0], Point(BORDER, label->label->textSize.y + 2*BORDER)); + image1 = std::make_shared(mapLayerImages[0], Point(BORDER, label->label->textSize.y + 2 * BORDER)); if(mapLayerImages.size()>1) - image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2*BORDER)); + image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2 * BORDER)); } center(GH.getCursorPosition()); //center on mouse diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 20699af25..eb2ca4d0d 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -3,7 +3,7 @@ { "type" : "object", "$schema" : "http://json-schema.org/draft-04/schema", - "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "gameTweaks" ], + "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ], "definitions" : { "logLevelEnum" : { "type" : "string", @@ -526,6 +526,18 @@ } } }, + "lobby" : { + "type" : "object", + "additionalProperties" : false, + "default" : {}, + "required" : [ "mapPreview" ], + "properties" : { + "mapPreview" : { + "type" : "boolean", + "default" : true + } + } + }, "gameTweaks" : { "type" : "object", "default" : {}, From 952ce3061ba40b6ad8d6695ba96140db162a7ab7 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 14:28:10 +0200 Subject: [PATCH 0096/1248] avoid crash if any error in map --- client/lobby/SelectionTab.cpp | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index f2522258e..195fbdf3b 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -815,6 +815,9 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI if(renderImage) mapLayerImages = createMinimaps(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); + if(mapLayerImages.size() == 0) + renderImage = false; + pos = Rect(0, 0, 2 * BORDER + IMAGE_SIZE, 2000); if(renderImage && mapLayerImages.size() > 1) pos.w += IMAGE_SIZE + BORDER; @@ -889,7 +892,15 @@ std::vector> SelectionTab::CMapInfoTooltipBox::createMin std::vector> ret = std::vector>(); CMapService mapService; - std::unique_ptr map = mapService.loadMap(resource); + std::unique_ptr map; + try + { + map = mapService.loadMap(resource); + } + catch (...) + { + return ret; + } for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) { From 989a0859ef2f391aa0d1705800ae2519caf0c76f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 20 Aug 2023 17:58:40 +0400 Subject: [PATCH 0097/1248] Fix tutorial loading --- client/mainmenu/CMainMenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index c55ba614c..aabff9076 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -383,8 +383,8 @@ void CMainMenu::startTutorial() auto mapInfo = std::make_shared(); mapInfo->mapInit(tutorialMap.getName()); - CSH->startMapAfterConnection(mapInfo); CMainMenu::openLobby(ESelectionScreen::newGame, true, nullptr, ELoadMode::NONE); + CSH->startMapAfterConnection(mapInfo); } std::shared_ptr CMainMenu::create() From 5c5576313b9ed8e3d812c2d50268a8d700aabaa8 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 16:58:13 +0200 Subject: [PATCH 0098/1248] handle long texts better; big window also for only one layer --- client/lobby/SelectionTab.cpp | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 195fbdf3b..874b56aac 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -818,9 +818,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI if(mapLayerImages.size() == 0) renderImage = false; - pos = Rect(0, 0, 2 * BORDER + IMAGE_SIZE, 2000); - if(renderImage && mapLayerImages.size() > 1) - pos.w += IMAGE_SIZE + BORDER; + pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000); auto drawLabel = [&]() { label = std::make_shared(text, Rect(BORDER, BORDER, pos.w - 2 * BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); @@ -829,7 +827,8 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI }; drawLabel(); - pos.h = BORDER + label->label->textSize.y + BORDER; + int textHeight = std::min(350, label->label->textSize.y); + pos.h = BORDER + textHeight + BORDER; if(renderImage) pos.h += IMAGE_SIZE + BORDER; backgroundTexture = std::make_shared("DIBOXBCK", pos); @@ -839,9 +838,13 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI if(renderImage) { - image1 = std::make_shared(mapLayerImages[0], Point(BORDER, label->label->textSize.y + 2 * BORDER)); - if(mapLayerImages.size()>1) - image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, label->label->textSize.y + 2 * BORDER)); + if(mapLayerImages.size() == 1) + image1 = std::make_shared(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER)); + else + { + image1 = std::make_shared(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER)); + image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER)); + } } center(GH.getCursorPosition()); //center on mouse From 080630b39fa31f25a5ea391648440037bcc61c71 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 22:22:46 +0200 Subject: [PATCH 0099/1248] try to fix --- client/lobby/SelectionTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 874b56aac..6ee831eb7 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -821,7 +821,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000); auto drawLabel = [&]() { - label = std::make_shared(text, Rect(BORDER, BORDER, pos.w - 2 * BORDER, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + label = std::make_shared(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!label->slider) label->resize(label->label->textSize); }; From 05a88bd8acc5e7c48319bf499a767b2047ef1f88 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 20 Aug 2023 23:34:09 +0200 Subject: [PATCH 0100/1248] another try... --- client/lobby/SelectionTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 6ee831eb7..fe5e8c0bc 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -823,7 +823,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI auto drawLabel = [&]() { label = std::make_shared(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!label->slider) - label->resize(label->label->textSize); + label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y)); }; drawLabel(); From e8e6c02a4a37a1c92c53519d3f17089f0d5a0c5c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 00:00:37 +0300 Subject: [PATCH 0101/1248] Moved all battle-related functionality of server into a new class --- server/BattleProcessor.cpp | 2754 +++++++++++++++++++++++++++++++++++ server/BattleProcessor.h | 129 ++ server/CGameHandler.cpp | 2768 +----------------------------------- server/CGameHandler.h | 100 +- server/CMakeLists.txt | 2 + server/CQuery.cpp | 5 +- server/CQuery.h | 4 +- server/NetPacksServer.cpp | 9 +- 8 files changed, 2945 insertions(+), 2826 deletions(-) create mode 100644 server/BattleProcessor.cpp create mode 100644 server/BattleProcessor.h diff --git a/server/BattleProcessor.cpp b/server/BattleProcessor.cpp new file mode 100644 index 000000000..44b178c6c --- /dev/null +++ b/server/BattleProcessor.cpp @@ -0,0 +1,2754 @@ +/* + * BattleProcessor.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 "BattleProcessor.h" + +#include "CGameHandler.h" +#include "CQuery.h" +#include "CVCMIServer.h" +#include "HeroPoolProcessor.h" + +#include "../lib/ArtifactUtils.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CStack.h" +#include "../lib/CondSh.h" +#include "../lib/GameSettings.h" +#include "../lib/ScopeGuard.h" +#include "../lib/TerrainHandler.h" +#include "../lib/UnlockGuard.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/battle/CUnitState.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapping/CMap.h" +#include "../lib/modding/IdentifierStorage.h" +#include "../lib/serializer/Cast.h" +#include "../lib/spells/AbilityCaster.h" +#include "../lib/spells/BonusCaster.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/spells/ISpellMechanics.h" +#include "../lib/spells/ObstacleCasterProxy.h" +#include "../lib/spells/Problem.h" + +#define COMPLAIN_RET_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return;}} while(0) +#define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return false;}} while(0) +#define COMPLAIN_RET(txt) {gameHandler->complain(txt); return false;} +#define COMPLAIN_RETF(txt, FORMAT) {gameHandler->complain(boost::str(boost::format(txt) % FORMAT)); return false;} + +CondSh battleMadeAction(false); +CondSh battleResult(nullptr); +boost::recursive_mutex battleActionMutex; +static EndAction end_action; + +static void giveExp(BattleResult &r) +{ + if (r.winner > 1) + { + // draw + return; + } + r.exp[0] = 0; + r.exp[1] = 0; + for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) + { + r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; + } +} + +static void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +{ + int x = targetPosition.getX(); + int y = targetPosition.getY(); + + const bool targetIsAttacker = side == BattleSide::ATTACKER; + + if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + + //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's + if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) + { + if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } + else + { //add back-side guardians for two-hex target, side guardians for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); + + if (!targetIsTwoHex && x > 2) //back guard for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); + else if (targetIsTwoHex)//front-side guardians for two-hex target + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + if (x > 3) //back guard for two-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + } + } + + } + + else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) + { + if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + else + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); + + if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else if (targetIsTwoHex) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + if (x < GameConstants::BFIELD_WIDTH - 4) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + } + } + } + + else if (!targetIsAttacker && y % 2 == 0) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + + else if (targetIsAttacker && y % 2 == 1) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } +} + + +CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): + army(battleSide.armyObject) +{ + heroWithDeadCommander = ObjectInstanceID(); + + PlayerColor color = battleSide.color; + + for(CStack * st : bat->stacks) + { + if(st->summoned) //don't take into account temporary summoned stacks + continue; + if(st->unitOwner() != color) //remove only our stacks + continue; + + logGlobal->debug("Calculating casualties for %s", st->nodeName()); + + st->health.takeResurrected(); + + if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) + { + logGlobal->debug("Ignored arrow towers stack."); + } + else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) + { + auto warMachine = st->unitType()->warMachine; + + if(warMachine == ArtifactID::NONE) + { + logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); + } + //catapult artifact remain even if "creature" killed in siege + else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) + { + logGlobal->debug("War machine has been destroyed"); + auto hero = dynamic_ptr_cast (army); + if (hero) + removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + else + logGlobal->error("War machine in army without hero"); + } + } + else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) + { + if(st->alive() && st->getCount() > 0) + { + logGlobal->debug("Permanently summoned %d units.", st->getCount()); + const CreatureID summonedType = st->creatureId(); + summoned[summonedType] += st->getCount(); + } + } + else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + if (nullptr == st->base) + { + logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); + } + else + { + auto c = dynamic_cast (st->base); + if(c) + { + auto h = dynamic_cast (army); + if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) + { + logGlobal->debug("Commander is dead."); + heroWithDeadCommander = army->id; //TODO: unify commander handling + } + } + else + logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); + } + } + else if(st->base && !army->slotEmpty(st->unitSlot())) + { + logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); + if(st->getCount() == 0 || !st->alive()) + { + logGlobal->debug("Stack has been destroyed."); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); + } + else if(st->getCount() < army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + else if(st->getCount() > army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + } + else + { + logGlobal->warn("Unable to process stack: %s", st->nodeName()); + } + } +} + +void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) +{ + for (TStackAndItsNewCount &ncount : newStackCounts) + { + if (ncount.second > 0) + gh->changeStackCount(ncount.first, ncount.second, true); + else + gh->eraseStack(ncount.first, true); + } + for (auto summoned_iter : summoned) + { + SlotID slot = army->getSlotFor(summoned_iter.first); + if (slot.validSlot()) + { + StackLocation location(army, slot); + gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); + } + else + { + //even if it will be possible to summon anything permanently it should be checked for free slot + //necromancy is handled separately + gh->complain("No free slot to put summoned creature"); + } + } + for (auto al : removedWarMachines) + { + gh->removeArtifact(al); + } + if (heroWithDeadCommander != ObjectInstanceID()) + { + SetCommanderProperty scp; + scp.heroid = heroWithDeadCommander; + scp.which = SetCommanderProperty::ALIVE; + scp.amount = 0; + gh->sendAndApply(&scp); + } +} + +BattleProcessor::BattleProcessor(CGameHandler * gameHandler): + visitObjectAfterVictory(false), + gameHandler(gameHandler) +{ +} + +BattleProcessor::BattleProcessor(): + visitObjectAfterVictory(false), + gameHandler(nullptr) +{ +} + +BattleProcessor::~BattleProcessor() +{ + if (battleThread) + { + //Setting battleMadeAction is needed because battleThread waits for the action to continue the main loop + battleMadeAction.setn(true); + battleThread->join(); + } +} + +FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int remainingBattleQueriesCount) +{ + assert(Query->result); + assert(Query->bi); + auto &result = *Query->result; + auto &info = *Query->bi; + + winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; + loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; + victor = info.sides[result.winner].color; + loser = info.sides[!result.winner].color; + winnerSide = result.winner; + this->remainingBattleQueriesCount = remainingBattleQueriesCount; +} + +FinishingBattleHelper::FinishingBattleHelper() +{ + winnerHero = loserHero = nullptr; + winnerSide = 0; + remainingBattleQueriesCount = 0; +} + +void BattleProcessor::engageIntoBattle(PlayerColor player) +{ + //notify interfaces + PlayerBlocked pb; + pb.player = player; + pb.reason = PlayerBlocked::UPCOMING_BATTLE; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + gameHandler->sendAndApply(&pb); +} + +void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, + const CGTownInstance *town) //use hero=nullptr for no hero +{ + if(gameHandler->gameState()->curB) + gameHandler->gameState()->curB.dellNull(); + + engageIntoBattle(army1->tempOwner); + engageIntoBattle(army2->tempOwner); + + static const CArmedInstance *armies[2]; + armies[0] = army1; + armies[1] = army2; + static const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + + + setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(gameHandler->gameState()->curB->sides[0].color)); + + //existing battle query for retying auto-combat + if(lastBattleQuery) + { + for(int i : {0, 1}) + { + if(heroes[i]) + { + SetMana restoreInitialMana; + restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; + restoreInitialMana.hid = heroes[i]->id; + gameHandler->sendAndApply(&restoreInitialMana); + } + } + + lastBattleQuery->bi = gameHandler->gameState()->curB; + lastBattleQuery->result = std::nullopt; + lastBattleQuery->belligerents[0] = gameHandler->gameState()->curB->sides[0].armyObject; + lastBattleQuery->belligerents[1] = gameHandler->gameState()->curB->sides[1].armyObject; + } + + auto nextBattleQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + for(int i : {0, 1}) + { + if(heroes[i]) + { + nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; + } + } + gameHandler->queries.addQuery(nextBattleQuery); + + this->battleThread = std::make_unique(boost::thread(&BattleProcessor::runBattle, this)); +} + +void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) +{ + startBattlePrimary(army1, army2, tile, + army1->ID == Obj::HERO ? static_cast(army1) : nullptr, + army2->ID == Obj::HERO ? static_cast(army2) : nullptr, + creatureBank); +} + +void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) +{ + startBattleI(army1, army2, army2->visitablePos(), creatureBank); +} + +void BattleProcessor::runBattle() +{ + boost::unique_lock lock(battleActionMutex); + + gameHandler->setBattle(gameHandler->gameState()->curB); + assert(gameHandler->gameState()->curB); + //TODO: pre-tactic stuff, call scripts etc. + + //Moat should be initialized here, because only here we can use spellcasting + if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) + { + const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER); + const auto * actualCaster = h ? static_cast(h) : nullptr; + auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); + auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell()); + auto target = spells::Target(); + cast.cast(gameHandler->spellEnv, target); + } + + //tactic round + { + while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && gameHandler->gameState()->curB->tacticDistance && !battleResult.get()) + { + auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + } + } + + //initial stacks appearance triggers, e.g. built-in bonus spells + auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing + + for (CStack * stack : initialStacks) + { + if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) + { + std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); + auto accessibility = gameHandler->getAccesibility(); + CreatureID creatureData = CreatureID(summonInfo->subtype); + std::vector targetHexes; + const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard + const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); + + /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. + For one-hex targets there are four guardians - front, back and one per side (up + down). + Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front + Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ + if (!guardianIsBig) + targetHexes = stack->getSurroundingHexes(); + else + summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + + for(auto hex : targetHexes) + { + if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex + { + battle::UnitInfo info; + info.id = gameHandler->gameState()->curB->battleNextUnitId(); + info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); + info.type = creatureData; + info.side = stack->unitSide(); + info.position = hex; + info.summoned = true; + + BattleUnitsChanged pack; + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + gameHandler->sendAndApply(&pack); + } + } + } + + stackEnchantedTrigger(stack); + } + + //spells opening battle + for (int i = 0; i < 2; ++i) + { + auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); + if (h) + { + TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + + for (auto b : *bl) + { + spells::BonusCaster caster(h, b); + + const CSpell * spell = SpellID(b->subtype).toSpell(); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + parameters.setSpellLevel(3); + parameters.setEffectDuration(b->val); + parameters.massive = true; + parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); + } + } + } + // it is possible that due to opening spells one side was eliminated -> check for end of battle + checkBattleStateChanges(); + + bool firstRound = true;//FIXME: why first round is -1? + + //main loop + while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && !battleResult.get()) //till the end of the battle ;] + { + BattleNextRound bnr; + bnr.round = gameHandler->gameState()->curB->round + 1; + logGlobal->debug("Round %d", bnr.round); + gameHandler->sendAndApply(&bnr); + + auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it + for (auto &obstPtr : obstacles) + { + if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) + if (sco->turnsRemaining == 0) + removeObstacle(*obstPtr); + } + + const BattleInfo & curB = *gameHandler->gameState()->curB; + + for(auto stack : curB.stacks) + { + if(stack->alive() && !firstRound) + stackEnchantedTrigger(stack); + } + + //stack loop + + auto getNextStack = [this]() -> const CStack * + { + if(battleResult.get()) + return nullptr; + + std::vector q; + gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" + + if(!q.empty()) + { + if(!q.front().empty()) + { + auto next = q.front().front(); + const auto stack = dynamic_cast(next); + + // regeneration takes place before everything else but only during first turn attempt in each round + // also works under blind and similar effects + if(stack && stack->alive() && !stack->waiting) + { + BattleTriggerEffect bte; + bte.stackID = stack->unitId(); + bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); + + const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); + if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) + bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); + + if(bte.val) // anything to heal + gameHandler->sendAndApply(&bte); + } + + if(next->willMove()) + return stack; + } + } + + return nullptr; + }; + + const CStack * next = nullptr; + while((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && (next = getNextStack())) + { + BattleUnitsChanged removeGhosts; + for(auto stack : curB.stacks) + { + if(stack->ghostPending) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); + } + + if(!removeGhosts.changedStacks.empty()) + gameHandler->sendAndApply(&removeGhosts); + + // check for bad morale => freeze + int nextStackMorale = next->moraleVal(); + if(!next->hadMorale && !next->waited() && nextStackMorale < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + //unit loses its turn - empty freeze action + BattleAction ba; + ba.actionType = EActionType::BAD_MORALE; + ba.side = next->unitSide(); + ba.stackNumber = next->unitId(); + + makeAutomaticAction(next, ba); + continue; + } + } + + if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk + { + logGlobal->trace("Handle Berserk effect"); + std::pair attackInfo = curB.getNearestStack(next); + if (attackInfo.first != nullptr) + { + BattleAction attack; + attack.actionType = EActionType::WALK_AND_ATTACK; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + attack.aimToHex(attackInfo.second); + attack.aimToUnit(attackInfo.first); + + makeAutomaticAction(next, attack); + logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); + } + else + { + makeStackDoNothing(next); + logGlobal->trace("No target found"); + } + continue; + } + + const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next); + const int stackCreatureId = next->unitType()->getId(); + + if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) + { + BattleAction attack; + attack.actionType = EActionType::SHOOT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + //TODO: select target by priority + + const battle::Unit * target = nullptr; + + for(auto & elem : gameHandler->gameState()->curB->stacks) + { + if(elem->unitType()->getId() != CreatureID::CATAPULT + && elem->unitOwner() != next->unitOwner() + && elem->isValidTarget() + && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition())) + { + target = elem; + break; + } + } + + if(target == nullptr) + { + makeStackDoNothing(next); + } + else + { + attack.aimToUnit(target); + makeAutomaticAction(next, attack); + } + continue; + } + + if (next->unitType()->getId() == CreatureID::CATAPULT) + { + const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); + + if (attackableBattleHexes.empty()) + { + makeStackDoNothing(next); + continue; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) + { + BattleAction attack; + attack.actionType = EActionType::CATAPULT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + makeAutomaticAction(next, attack); + continue; + } + } + + if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) + { + TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s) + { + return s->unitOwner() == next->unitOwner() && s->canBeHealed(); + }); + + if (!possibleStacks.size()) + { + makeStackDoNothing(next); + continue; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) + { + RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); + const CStack * toBeHealed = possibleStacks.front(); + + BattleAction heal; + heal.actionType = EActionType::STACK_HEAL; + heal.aimToUnit(toBeHealed); + heal.side = next->unitSide(); + heal.stackNumber = next->unitId(); + + makeAutomaticAction(next, heal); + continue; + } + } + + int numberOfAsks = 1; + bool breakOuter = false; + do + {//ask interface and wait for answer + if (!battleResult.get()) + { + stackTurnTrigger(next); //various effects + + if(next->fear) + { + makeStackDoNothing(next); //end immediately if stack was affected by fear + } + else + { + logGlobal->trace("Activating %s", next->nodeName()); + auto nextId = next->unitId(); + BattleSetActiveStack sas; + sas.stack = nextId; + gameHandler->sendAndApply(&sas); + + auto actionWasMade = [&]() -> bool + { + if (battleMadeAction.data)//active stack has made its action + return true; + if (battleResult.get())// battle is finished + return true; + if (next == nullptr)//active stack was been removed + return true; + return !next->alive();//active stack is dead + }; + + boost::unique_lock lock(battleMadeAction.mx); + battleMadeAction.data = false; + while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && !actionWasMade()) + { + { + boost::unique_lock actionLock(battleActionMutex); + battleMadeAction.cond.wait(lock); + } + if (gameHandler->battleGetStackByID(nextId, false) != next) + next = nullptr; //it may be removed, while we wait + } + } + } + + if (battleResult.get()) //don't touch it, battle could be finished while waiting got action + { + breakOuter = true; + break; + } + //we're after action, all results applied + checkBattleStateChanges(); //check if this action ended the battle + + if(next != nullptr) + { + //check for good morale + nextStackMorale = next->moraleVal(); + if( !battleResult.get() + && !next->hadMorale + && !next->defending + && !next->waited() + && !next->fear + && next->alive() + && nextStackMorale > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + BattleTriggerEffect bte; + bte.stackID = next->unitId(); + bte.effect = vstd::to_underlying(BonusType::MORALE); + bte.val = 1; + bte.additionalInfo = 0; + gameHandler->sendAndApply(&bte); //play animation + + ++numberOfAsks; //move this stack once more + } + } + } + --numberOfAsks; + } while (numberOfAsks > 0); + + if (breakOuter) + { + break; + } + + } + firstRound = false; + } + + if (gameHandler->gameLobby()->state != EServerState::SHUTDOWN) + endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); +} + +bool BattleProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) +{ + boost::unique_lock lock(battleActionMutex); + + BattleSetActiveStack bsa; + bsa.stack = stack->unitId(); + bsa.askPlayerInterface = false; + gameHandler->sendAndApply(&bsa); + + bool ret = makeBattleAction(ba); + checkBattleStateChanges(); + return ret; +} + + +void BattleProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +{ + if(attacker->hasBonusOfType(attackMode)) + { + std::set spellsToCast; + TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); + for(const auto & sf : *spells) + { + spellsToCast.insert(SpellID(sf->subtype)); + } + for(SpellID spellID : spellsToCast) + { + bool castMe = false; + if(!defender->alive()) + { + logGlobal->debug("attackCasting: all attacked creatures have been killed"); + return; + } + int32_t spellLevel = 0; + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); + for(const auto & sf : *spellsByType) + { + int meleeRanged; + if(sf->additionalInfo.size() < 2) + { + // legacy format + vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); + meleeRanged = sf->additionalInfo[0] / 1000; + } + else + { + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + } + if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) + castMe = true; + } + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); + vstd::amin(chance, 100); + + const CSpell * spell = SpellID(spellID).toSpell(); + spells::AbilityCaster caster(attacker, spellLevel); + + spells::Target target; + target.emplace_back(defender); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + + auto m = spell->battleMechanics(¶meters); + + spells::detail::ProblemImpl ignored; + + if(!m->canBeCastAt(target, ignored)) + continue; + + //check if spell should be cast (probability handling) + if(gameHandler->getRandomGenerator().nextInt(99) >= chance) + continue; + + //casting + if(castMe) + { + parameters.cast(gameHandler->spellEnv, target); + } + } + } +} + +void BattleProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? +} + +void BattleProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + if(!attacker->alive() || !defender->alive()) // can be already dead + return; + + attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); + + if(!defender->alive()) + { + //don't try death stare or acid breath on dead stack (crash!) + return; + } + + if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) + { + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; + vstd::amin(chanceToKill, 1); //cap at 100% + + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + + int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(staredCreatures, maxToKill); + + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); + if(staredCreatures) + { + //TODO: death stare was not originally available for multiple-hex attacks, but... + const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(staredCreatures); + parameters.cast(gameHandler->spellEnv, target); + } + } + + if(!defender->alive()) + return; + + int64_t acidDamage = 0; + TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); + for(const auto & b : *acidBreath) + { + if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99)) + acidDamage += b->val; + } + + if(acidDamage > 0) + { + const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + + parameters.setEffectValue(acidDamage * attacker->getCount()); + parameters.cast(gameHandler->spellEnv, target); + } + + + if(!defender->alive()) + return; + + if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability + { + double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; + vstd::amin(chanceToTrigger, 1); //cap at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; + + if(defender->unitType()->getId() == bonusAdditionalInfo || + (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) + return; + + battle::UnitInfo resurrectInfo; + resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId(); + resurrectInfo.summoned = false; + resurrectInfo.position = defender->getPosition(); + resurrectInfo.side = defender->unitSide(); + + if(bonusAdditionalInfo != CAddInfo::NONE) + resurrectInfo.type = CreatureID(bonusAdditionalInfo); + else + resurrectInfo.type = attacker->creatureId(); + + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) + resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) + resurrectInfo.count = defender->getCount(); + else + return; //wrong subtype + + BattleUnitsChanged addUnits; + addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); + resurrectInfo.save(addUnits.changedStacks.back().data); + + BattleUnitsChanged removeUnits; + removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&removeUnits); + gameHandler->sendAndApply(&addUnits); + } + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) + { + double chanceToTrigger = 0; + int amountToDie = 0; + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; + amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); + } + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; + } + + vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + BattleStackAttacked bsa; + bsa.attackerID = -1; + bsa.stackAttacked = defender->unitId(); + bsa.damageAmount = amountToDie * defender->getMaxHealth(); + bsa.flags = BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID::SLAYER; + defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured si; + si.stacks.push_back(bsa); + + gameHandler->sendAndApply(&si); + sendGenericKilledLog(defender, bsa.killedAmount, false); + } +} + +void BattleProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) +{ + if(first && !counter) + handleAttackBeforeCasting(ranged, attacker, defender); + + FireShieldInfo fireShield; + BattleAttack bat; + BattleLogMessage blm; + bat.stackAttacking = attacker->unitId(); + bat.tile = targetHex; + + std::shared_ptr attackerState = attacker->acquireState(); + + if(ranged) + bat.flags |= BattleAttack::SHOT; + if(counter) + bat.flags |= BattleAttack::COUNTER; + + const int attackerLuck = attacker->luckVal(); + + if(attackerLuck > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::LUCKY; + } + + if(attackerLuck < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::UNLUCKY; + } + + if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) + { + bat.flags |= BattleAttack::DEATH_BLOW; + } + + const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner()); + if(owner) + { + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); + if (chance > gameHandler->getRandomGenerator().nextInt(99)) + bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; + } + + int64_t drainedLife = 0; + + // only primary target + if(defender->alive()) + drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); + + //multiple-hex normal attack + std::set attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + + std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + if(bonus && ranged) //TODO: make it work in melee? + { + //this is need for displaying hit animation + bat.flags |= BattleAttack::SPELL_LIKE; + bat.spellID = SpellID(bonus->subtype); + + //TODO: should spell override creature`s projectile? + + auto spell = bat.spellID.toSpell(); + + battle::Target target; + target.emplace_back(defender, targetHex); + + spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + event.setSpellLevel(bonus->val); + + auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); + + //TODO: get exact attacked hex for defender + + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + { + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + } + + //now add effect info for all attacked stacks + for (BattleStackAttacked & bsa : bat.bsa) + { + if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield + { + //this is need for displaying affect animation + bsa.flags |= BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID(bonus->subtype); + } + } + } + + attackerState->afterAttack(ranged, counter); + + { + UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); + attackerState->save(info.data); + bat.attackerChanges.changedStacks.push_back(info); + } + + if (drainedLife > 0) + bat.flags |= BattleAttack::LIFE_DRAIN; + + gameHandler->sendAndApply(&bat); + + { + const bool multipleTargets = bat.bsa.size() > 1; + + int64_t totalDamage = 0; + int32_t totalKills = 0; + + for(const BattleStackAttacked & bsa : bat.bsa) + { + totalDamage += bsa.damageAmount; + totalKills += bsa.killedAmount; + } + + { + MetaString text; + attacker->addText(text, EMetaText::GENERAL_TXT, 376); + attacker->addNameReplacement(text); + text.replaceNumber(totalDamage); + blm.lines.push_back(text); + } + + addGenericKilledLog(blm, defender, totalKills, multipleTargets); + } + + // drain life effect (as well as log entry) must be applied after the attack + if(drainedLife > 0) + { + MetaString text; + attackerState->addText(text, EMetaText::GENERAL_TXT, 361); + attackerState->addNameReplacement(text, false); + text.replaceNumber(drainedLife); + defender->addNameReplacement(text, true); + blm.lines.push_back(std::move(text)); + } + + if(!fireShield.empty()) + { + //todo: this should be "virtual" spell instead, we only need fire spell school bonus here + const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); + int64_t totalDamage = 0; + + for(const auto & item : fireShield) + { + const CStack * actor = item.first; + int64_t rawDamage = item.second; + + const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner()); + + if(actorOwner) + { + rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); + } + else + { + rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); + } + + totalDamage+=rawDamage; + //FIXME: add custom effect on actor + } + + if (totalDamage > 0) + { + BattleStackAttacked bsa; + + bsa.flags |= BattleStackAttacked::FIRE_SHIELD; + bsa.stackAttacked = attacker->unitId(); //invert + bsa.attackerID = defender->unitId(); + bsa.damageAmount = totalDamage; + attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured pack; + pack.stacks.push_back(bsa); + gameHandler->sendAndApply(&pack); + + // TODO: this is already implemented in Damage::describeEffect() + { + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 376); + text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); + text.replaceNumber(totalDamage); + blm.lines.push_back(std::move(text)); + } + addGenericKilledLog(blm, attacker, bsa.killedAmount, false); + } + } + + gameHandler->sendAndApply(&blm); + + handleAfterAttackCasting(ranged, attacker, defender); +} + +int64_t BattleProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +{ + BattleStackAttacked bsa; + if(secondary) + bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities + + bsa.attackerID = attackerState->unitId(); + bsa.stackAttacked = def->unitId(); + { + BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); + + bai.deathBlow = bat.deathBlow(); + bai.doubleDamage = bat.ballistaDoubleDmg(); + bai.luckyStrike = bat.lucky(); + bai.unluckyStrike = bat.unlucky(); + + auto range = gameHandler->gameState()->curB->calculateDmgRange(bai); + bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties + } + + int64_t drainedLife = 0; + + //life drain handling + if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) + { + int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; + attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + drainedLife += toHeal; + } + + //soul steal handling + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) + { + //we can have two bonuses - one with subtype 0 and another with subtype 1 + //try to use permanent first, use only one of two + for(si32 subtype = 1; subtype >= 0; subtype--) + { + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) + { + int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); + attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); + drainedLife += toHeal; + break; + } + } + } + bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated + + //fire shield handling + if(!bat.shot() && + !def->isClone() && + def->hasBonusOfType(BonusType::FIRE_SHIELD) && + !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && + CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) + ) + { + //TODO: use damage with bonus but without penalties + auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; + fireShield.push_back(std::make_pair(def, fireShieldDamage)); + } + + return drainedLife; +} + +void BattleProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + BattleLogMessage blm; + addGenericKilledLog(blm, defender, killed, multiple); + gameHandler->sendAndApply(&blm); + } +} + +void BattleProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + const int32_t txtIndex = (killed > 1) ? 379 : 378; + std::string formatString = VLC->generaltexth->allTexts[txtIndex]; + + // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); + boost::algorithm::trim(formatString); + + boost::format txt(formatString); + if(killed > 1) + { + txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish + } + else //killed == 1 + { + txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes + } + MetaString line; + line.appendRawString(txt.str()); + blm.lines.push_back(std::move(line)); + } +} + +void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) +{ + LOG_TRACE(logGlobal); + + //Fill BattleResult structure with exp info + giveExp(*battleResult.data); + + if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped + { + if(heroAttacker) + battleResult.data->exp[1] += 500; + if(heroDefender) + battleResult.data->exp[0] += 500; + } + + if(heroAttacker) + battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill + if(heroDefender) + battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); + + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(gameHandler->gameState()->curB->sides[0].color)); + if (!battleQuery) + { + logGlobal->error("Cannot find battle query!"); + gameHandler->complain("Player " + boost::lexical_cast(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!"); + return; + } + + battleQuery->result = std::make_optional(*battleResult.data); + + //Check how many battle gameHandler->queries were created (number of players blocked by battle) + const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries.allQueries(), battleQuery) : 0; + finishingBattle = std::make_unique(battleQuery, queriedPlayers); + + // in battles against neutrals, 1st player can ask to replay battle manually + if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer()) + { + auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + battleResult.data->queryID = battleDialogQuery->queryID; + gameHandler->queries.addQuery(battleDialogQuery); + } + else + battleResult.data->queryID = -1; + + //set same battle result for all gameHandler->queries + for(auto q : gameHandler->queries.allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery) + otherBattleQuery->result = battleQuery->result; + } + + gameHandler->sendAndApply(battleResult.data); //after this point casualties objects are destroyed + + if (battleResult.data->queryID == -1) + endBattleConfirm(gameHandler->gameState()->curB); +} + +void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) +{ + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(battleInfo->sides.at(0).color)); + if(!battleQuery) + { + logGlobal->trace("No battle query, battle end was confirmed by another player"); + return; + } + + const BattleResult::EResult result = battleResult.get()->result; + + CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle + ChangeSpells cs; //for Eagle Eye + + if(!finishingBattle->isDraw() && finishingBattle->winnerHero) + { + if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) + { + double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); + for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) + { + auto spell = spellId.toSpell(VLC->spells()); + if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) + cs.spells.insert(spell->getId()); + } + } + } + std::vector arts; //display them in window + + if(result == BattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) + { + auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) + { + const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); + if(slot != ArtifactPosition::PRE_FIRST) + { + arts.push_back(art); + ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); + if(ArtifactUtils::isSlotBackpack(slot)) + ma->askAssemble = false; + gameHandler->sendAndApply(ma); + } + }; + + if (finishingBattle->loserHero) + { + //TODO: wrap it into a function, somehow (std::variant -_-) + auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig() && + art->artType->getId() != ArtifactID::SPELLBOOK) + // don't move war machines or locked arts (spellbook) + { + sendMoveArtifact(art, &ma); + } + } + for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) + { + //we assume that no big artifacts can be found + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, + ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning + const CArtifactInstance * art = ma.src.getArt(); + if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won + { + sendMoveArtifact(art, &ma); + } + } + if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? + { + artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) + { + auto artifactsWorn = armySlot.second->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(armySlot.second, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + + if (arts.size()) //display loot + { + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + + iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact + + for (auto art : arts) //TODO; separate function to display loot for various ojects? + { + iw.components.emplace_back( + Component::EComponentType::ARTIFACT, art->artType->getId(), + art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); + if (iw.components.size() >= 14) + { + gameHandler->sendAndApply(&iw); + iw.components.clear(); + } + } + if (iw.components.size()) + { + gameHandler->sendAndApply(&iw); + } + } + //Eagle Eye secondary skill handling + if (!cs.spells.empty()) + { + cs.learn = 1; + cs.hid = finishingBattle->winnerHero->id; + + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s + iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); + + std::ostringstream names; + for (int i = 0; i < cs.spells.size(); i++) + { + names << "%s"; + if (i < cs.spells.size() - 2) + names << ", "; + else if (i < cs.spells.size() - 1) + names << "%s"; + } + names << "."; + + iw.text.replaceRawString(names.str()); + + auto it = cs.spells.begin(); + for (int i = 0; i < cs.spells.size(); i++, it++) + { + iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); + if (i == cs.spells.size() - 2) //we just added pre-last name + iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " + iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); + } + gameHandler->sendAndApply(&iw); + gameHandler->sendAndApply(&cs); + } + cab1.updateArmy(gameHandler); + cab2.updateArmy(gameHandler); //take casualties after battle is deleted + + if(finishingBattle->loserHero) //remove beaten hero + { + RemoveObject ro(finishingBattle->loserHero->id); + gameHandler->sendAndApply(&ro); + } + if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + } + + if(battleResult.data->winner == BattleSide::DEFENDER + && finishingBattle->winnerHero + && finishingBattle->winnerHero->visitedTown + && !finishingBattle->winnerHero->inTownGarrison + && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) + { + gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place + } + //give exp + if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); + + BattleResultAccepted raccepted; + raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); + raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); + raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); + raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); + raccepted.heroResult[0].exp = battleResult.data->exp[0]; + raccepted.heroResult[1].exp = battleResult.data->exp[1]; + raccepted.winnerSide = finishingBattle->winnerSide; + gameHandler->sendAndApply(&raccepted); + + gameHandler->queries.popIfTop(battleQuery); + //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query +} + +void BattleProcessor::battleAfterLevelUp(const BattleResult &result) +{ + LOG_TRACE(logGlobal); + + if(!finishingBattle) + return; + + finishingBattle->remainingBattleQueriesCount--; + logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); + + if (finishingBattle->remainingBattleQueriesCount > 0) + //Battle results will be handled when all battle gameHandler->queries are closed + return; + + //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible + // but the battle consequences are applied after final player is unblocked. Hard to abuse... + // Still, it looks like a hole. + + // Necromancy if applicable. + const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); + // Give raised units to winner and show dialog, if any were raised, + // units will be given after casualties are taken + const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); + + if (necroSlot != SlotID()) + { + finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); + gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + } + + BattleResultsApplied resultsApplied; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + gameHandler->sendAndApply(&resultsApplied); + + gameHandler->setBattle(nullptr); + + if (visitObjectAfterVictory && result.winner==0 && !finishingBattle->winnerHero->stacks.empty()) + { + logGlobal->trace("post-victory visit"); + gameHandler->visitObjectOnTile(*gameHandler->getTile(finishingBattle->winnerHero->visitablePos()), finishingBattle->winnerHero); + } + visitObjectAfterVictory = false; + + //handle victory/loss of engaged players + std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; + gameHandler->checkVictoryLossConditions(playerColors); + + if (result.result == BattleResult::SURRENDER) + gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + + if (result.result == BattleResult::ESCAPE) + gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + + if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() + && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + + if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) + gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); + } + + finishingBattle.reset(); +} + +int BattleProcessor::moveStack(int stack, BattleHex dest) +{ + int ret = 0; + + const CStack *curStack = gameHandler->battleGetStackByID(stack), + *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); + + assert(curStack); + assert(dest < GameConstants::BFIELD_SIZE); + + if (gameHandler->gameState()->curB->tacticDistance) + { + assert(gameHandler->gameState()->curB->isInTacticRange(dest)); + } + + auto start = curStack->getPosition(); + if (start == dest) + return 0; + + //initing necessary tables + auto accessibility = gameHandler->getAccesibility(curStack); + std::set passed; + //Ignore obstacles on starting position + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + + //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) + if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) + { + BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); + + if(accessibility.accessible(shifted, curStack)) + dest = shifted; + } + + if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) + { + gameHandler->complain("Given destination is not accessible!"); + return 0; + } + + bool canUseGate = false; + auto dbState = gameHandler->gameState()->curB->si.gateState; + if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + dbState != EGateState::DESTROYED && + dbState != EGateState::BLOCKED) + { + canUseGate = true; + } + + std::pair< std::vector, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack); + + ret = path.second; + + int creSpeed = curStack->speed(0, true); + + if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) + creSpeed = GameConstants::BFIELD_SIZE; + + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_OUTER) + return true; + if (hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (isGateDrawbridgeHex(hex)) + return true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) + return true; + } + + return false; + }; + + if (curStack->hasBonusOfType(BonusType::FLYING)) + { + if (path.second <= creSpeed && path.first.size() > 0) + { + if (canUseGate && dbState != EGateState::OPENED && + occupyGateDrawbridgeHex(dest)) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + + //inform clients about move + BattleStackMoved sm; + sm.stack = curStack->unitId(); + std::vector tiles; + tiles.push_back(path.first[0]); + sm.tilesToMove = tiles; + sm.distance = path.second; + sm.teleporting = false; + gameHandler->sendAndApply(&sm); + } + } + else //for non-flying creatures + { + std::vector tiles; + const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); + int v = (int)path.first.size()-1; + path.first.push_back(start); + + // check if gate need to be open or closed at some point + BattleHex openGateAtHex, gateMayCloseAtHex; + if (canUseGate) + { + for (int i = (int)path.first.size()-1; i >= 0; i--) + { + auto needOpenGates = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) + return true; + else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto hex = path.first[i]; + if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) + { + if (needOpenGates(hex)) + openGateAtHex = path.first[i+1]; + + //TODO we need find batter way to handle double-wide stacks + //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && needOpenGates(otherHex)) + openGateAtHex = path.first[i+2]; + } + + //gate may be opened and then closed during stack movement, but not other way around + if (openGateAtHex.isValid()) + dbState = EGateState::OPENED; + } + + if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) + { + if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + if (hasWideMoat) + { + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && + path.first[i-1] != ESiegeHex::GATE_INNER && + path.first[i-1] != ESiegeHex::GATE_BRIDGE) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + } + } + + bool stackIsMoving = true; + + while(stackIsMoving) + { + if (verror("Movement terminated abnormally"); + break; + } + + bool gateStateChanging = false; + //special handling for opening gate on from starting hex + if (openGateAtHex.isValid() && openGateAtHex == start) + gateStateChanging = true; + else + { + for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) + { + BattleHex hex = path.first[v]; + tiles.push_back(hex); + + if ((openGateAtHex.isValid() && openGateAtHex == hex) || + (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) + { + gateStateChanging = true; + } + + //if we walked onto something, finalize this portion of stack movement check into obstacle + if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty()) + obstacleHit = true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + //two hex creature hit obstacle by backside + auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false); + if(otherHex.isValid() && !obstacle2.empty()) + obstacleHit = true; + } + if(!obstacleHit) + passed.insert(hex); + } + } + + if (!tiles.empty()) + { + //commit movement + BattleStackMoved sm; + sm.stack = curStack->unitId(); + sm.distance = path.second; + sm.teleporting = false; + sm.tilesToMove = tiles; + gameHandler->sendAndApply(&sm); + tiles.clear(); + } + + //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end + if (curStack->getPosition() != dest) + { + if(stackIsMoving && start != curStack->getPosition()) + { + stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + } + if (gateStateChanging) + { + if (curStack->getPosition() == openGateAtHex) + { + openGateAtHex = BattleHex(); + //only open gate if stack is still alive + if (curStack->alive()) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + } + else if (curStack->getPosition() == gateMayCloseAtHex) + { + gateMayCloseAtHex = BattleHex(); + updateGateState(); + } + } + } + else + //movement finished normally: we reached destination + stackIsMoving = false; + } + } + //handle last hex separately for deviation + if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) + { + if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) + || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) + passed.clear(); //Just empty passed, obstacles will handled automatically + } + //handling obstacle on the final field (separate, because it affects both flying and walking stacks) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + + return ret; +} + +void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) +{ + battleResult.set(nullptr); + + const auto & t = *gameHandler->getTile(tile); + TerrainId terrain = t.terType->getId(); + if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground + terrain = ETerrainId::SAND; + + BattleField terType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator()); + if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) + terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); + + + //send info about battles + BattleStart bs; + bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + + engageIntoBattle(bs.info->sides[0].color); + engageIntoBattle(bs.info->sides[1].color); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(bs.info->sides[0].color)); + bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); + + gameHandler->sendAndApply(&bs); +} + +void BattleProcessor::checkBattleStateChanges() +{ + //check if drawbridge state need to be changes + if (gameHandler->battleGetSiegeLevel() > 0) + updateGateState(); + + //check if battle ended + if (auto result = gameHandler->battleIsFinished()) + { + setBattleResult(BattleResult::NORMAL, *result); + } +} + + +bool BattleProcessor::makeBattleAction(BattleAction &ba) +{ + bool ok = true; + + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack + + const bool isAboutActiveStack = stack && (ba.stackNumber == gameHandler->gameState()->curB->getActiveStackID()); + + logGlobal->trace("Making action: %s", ba.toString()); + + switch(ba.actionType) + { + case EActionType::WALK: //walk + case EActionType::DEFEND: //defend + case EActionType::WAIT: //wait + case EActionType::WALK_AND_ATTACK: //walk or attack + case EActionType::SHOOT: //shoot + case EActionType::CATAPULT: //catapult + case EActionType::STACK_HEAL: //healing with First Aid Tent + case EActionType::MONSTER_SPELL: + + if (!stack) + { + gameHandler->complain("No such stack!"); + return false; + } + if (!stack->alive()) + { + gameHandler->complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if (gameHandler->battleTacticDist()) + { + if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) + { + gameHandler->complain("This is not a stack of side that has tactics!"); + return false; + } + } + else if (!isAboutActiveStack) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } + } + + auto wrapAction = [this](BattleAction &ba) + { + StartAction startAction(ba); + gameHandler->sendAndApply(&startAction); + + return vstd::makeScopeGuard([&]() + { + gameHandler->sendAndApply(&end_action); + }); + }; + + switch(ba.actionType) + { + case EActionType::END_TACTIC_PHASE: //wait + case EActionType::BAD_MORALE: + case EActionType::NO_ACTION: + { + auto wrapper = wrapAction(ba); + break; + } + case EActionType::WALK: + { + auto wrapper = wrapAction(ba); + if(target.size() < 1) + { + gameHandler->complain("Destination required for move action."); + ok = false; + break; + } + int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move + if (!walkedTiles) + gameHandler->complain("Stack failed movement!"); + break; + } + case EActionType::DEFEND: + { + //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) + SetStackEffect sse; + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), + -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(defenseBonusToAdd)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + std::vector buffer; + if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) + { + difference = 1; + buffer.push_back(alternativeWeakCreatureBonus); + } + else + { + buffer.push_back(defenseBonusToAdd); + } + + buffer.push_back(bonus2); + + sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); + gameHandler->sendAndApply(&sse); + + BattleLogMessage message; + + MetaString text; + stack->addText(text, EMetaText::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.replaceNumber(difference); + + message.lines.push_back(text); + + gameHandler->sendAndApply(&message); + //don't break - we share code with next case + } + [[fallthrough]]; + case EActionType::WAIT: + { + auto wrapper = wrapAction(ba); + break; + } + case EActionType::RETREAT: //retreat/flee + { + if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) + gameHandler->complain("Cannot retreat!"); + else + setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses + break; + } + case EActionType::SURRENDER: + { + PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; + int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); + if (cost < 0) + gameHandler->complain("Cannot surrender!"); + else if (gameHandler->getResource(player, EGameResID::GOLD) < cost) + gameHandler->complain("Not enough gold to surrender!"); + else + { + gameHandler->giveResource(player, EGameResID::GOLD, -cost); + setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses + } + break; + } + case EActionType::WALK_AND_ATTACK: //walk or attack + { + auto wrapper = wrapAction(ba); + + if(!stack) + { + gameHandler->complain("No attacker"); + ok = false; + break; + } + + if(target.size() < 2) + { + gameHandler->complain("Two destinations required for attack action."); + ok = false; + break; + } + + BattleHex attackPos = target.at(0).hexValue; + BattleHex destinationTile = target.at(1).hexValue; + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); + + if(!destinationStack) + { + gameHandler->complain("Invalid target to attack"); + ok = false; + break; + } + + BattleHex startingPos = stack->getPosition(); + int distance = moveStack(ba.stackNumber, attackPos); + + logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); + + if(stack->getPosition() != attackPos + && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) + ) + { + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine + break; + } + + if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + gameHandler->complain("Unit can not attack itself"); + ok = false; + break; + } + + if(!CStack::isMeleeAttackPossible(stack, destinationStack)) + { + gameHandler->complain("Attack cannot be performed!"); + ok = false; + break; + } + + //attack + int totalAttacks = stack->totalAttacks.getMeleeValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + + const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + const bool retaliation = destinationStack->ableToRetaliate(); + for (int i = 0; i < totalAttacks; ++i) + { + //first strike + if(i == 0 && firstStrike && retaliation) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) + { + makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + } + + //counterattack + //we check retaliation twice, so if it unblocked during attack it will work only on next attack + if(stack->alive() + && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) + && (i == 0 && !firstStrike) + && retaliation && destinationStack->ableToRetaliate()) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + } + + //return + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) + && target.size() == 3 + && startingPos != stack->getPosition() + && startingPos == target.at(2).hexValue + && stack->alive()) + { + moveStack(ba.stackNumber, startingPos); + //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) + } + break; + } + case EActionType::SHOOT: + { + if(target.size() < 1) + { + gameHandler->complain("Destination required for shot action."); + ok = false; + break; + } + + auto destination = target.at(0).hexValue; + + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); + + if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) + { + gameHandler->complain("Cannot shoot!"); + break; + } + if (!destinationStack) + { + gameHandler->complain("No target to shoot!"); + break; + } + + auto wrapper = wrapAction(ba); + + makeAttack(stack, destinationStack, 0, destination, true, true, false); + + //ranged counterattack + if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) + && destinationStack->ableToRetaliate() + && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) + && stack->alive()) //attacker may have died (fire shield) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); + } + //allow more than one additional attack + + int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + + for(int i = 1; i < totalRangedAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots.canUse() + ) + { + makeAttack(stack, destinationStack, 0, destination, false, true, false); + } + } + break; + } + case EActionType::CATAPULT: + { + auto wrapper = wrapAction(ba); + const CStack * shooter = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + if(!catapultAbility || catapultAbility->subtype < 0) + { + gameHandler->complain("We do not know how to shoot :P"); + } + else + { + const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); + parameters.setSpellLevel(shotLevel); + parameters.cast(gameHandler->spellEnv, target); + } + //finish by scope guard + break; + } + case EActionType::STACK_HEAL: //healing with First Aid Tent + { + auto wrapper = wrapAction(ba); + const CStack * healer = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + + if(target.size() < 1) + { + gameHandler->complain("Destination required for heal action."); + ok = false; + break; + } + + const battle::Unit * destStack = nullptr; + std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + + if(target.at(0).unitValue) + destStack = target.at(0).unitValue; + else + destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); + + if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) + { + gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); + } + else + { + const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + auto dest = battle::Destination(destStack, target.at(0).hexValue); + parameters.setSpellLevel(0); + parameters.cast(gameHandler->spellEnv, {dest}); + } + break; + } + case EActionType::MONSTER_SPELL: + { + auto wrapper = wrapAction(ba); + + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + SpellID spellID = SpellID(ba.actionSubtype); + + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); + + //TODO special bonus for genies ability + if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + + if (spellID < 0) + gameHandler->complain("That stack can't cast spells!"); + else + { + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); + } + break; + } + } + if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND + || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); + if(ba.stackNumber == gameHandler->gameState()->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished + battleMadeAction.setn(true); + return ok; +} + +bool BattleProcessor::makeCustomAction(BattleAction & ba) +{ + switch(ba.actionType) + { + case EActionType::HERO_SPELL: + { + COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); + + const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + COMPLAIN_RET_FALSE_IF((!h), "Wrong caster!"); + + const CSpell * s = SpellID(ba.actionSubtype).toSpell(); + if (!s) + { + logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); + return false; + } + + spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); + + spells::detail::ProblemImpl problem; + + auto m = s->battleMechanics(¶meters); + + if(!m->canBeCast(problem))//todo: should we check aimed cast? + { + logGlobal->warn("Spell cannot be cast!"); + std::vector texts; + problem.getAll(texts); + for(auto s : texts) + logGlobal->warn(s); + return false; + } + + StartAction start_action(ba); + gameHandler->sendAndApply(&start_action); //start spell casting + + parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); + + gameHandler->sendAndApply(&end_action); + if (!gameHandler->gameState()->curB->battleGetStackByID(gameHandler->gameState()->curB->activeStack)) + { + battleMadeAction.setn(true); + } + checkBattleStateChanges(); + if (battleResult.get()) + { + battleMadeAction.setn(true); + //battle will be ended by startBattle function + //endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->heroes[0], gameHandler->gameState()->curB->heroes[1]); + } + + return true; + + } + } + return false; +} + +void BattleProcessor::stackEnchantedTrigger(const CStack * st) +{ + auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); + for(auto b : bl) + { + const CSpell * sp = SpellID(b->subtype).toSpell(); + if(!sp) + continue; + + const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); + const int32_t level = ((val > 3) ? (val - 3) : val); + + spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp); + //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle + battleCast.setEffectDuration(50); + battleCast.setSpellLevel(level); + spells::Target target; + + if(val > 3) + { + for(auto s : gameHandler->gameState()->curB->battleGetAllStacks()) + if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + target.emplace_back(s); + } + else + { + target.emplace_back(st); + } + battleCast.applyEffects(gameHandler->spellEnv, target, false, true); + } +} + +void BattleProcessor::stackTurnTrigger(const CStack *st) +{ + BattleTriggerEffect bte; + bte.stackID = st->unitId(); + bte.effect = -1; + bte.val = 0; + bte.additionalInfo = 0; + if (st->alive()) + { + //unbind + if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) + { + bool unbind = true; + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); + auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st); + + for (auto b : bl) + { + if(b->additionalInfo != CAddInfo::NONE) + { + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent + if(stack) + { + if(vstd::contains(adjacent, stack)) //binding stack is still present + unbind = false; + } + } + else + { + unbind = false; + } + } + if (unbind) + { + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::UNBIND; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + + if (st->hasBonusOfType(BonusType::POISON)) + { + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) //TODO: what if not?... + { + bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); + if (bte.val < b->val) //(negative) poison effect increases - update it + { + bte.effect = vstd::to_underlying(BonusType::POISON); + gameHandler->sendAndApply(&bte); + } + } + } + if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) + { + const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st)); + const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent); + if(opponentHero) + { + ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); + vstd::amin(manaDrained, opponentHero->mana); + if(manaDrained) + { + bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); + bte.val = manaDrained; + bte.additionalInfo = opponentHero->id.getNum(); //for sanity + gameHandler->sendAndApply(&bte); + } + } + } + if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) + { + bool fearsomeCreature = false; + for (CStack * stack : gameHandler->gameState()->curB->stacks) + { + if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) + { + fearsomeCreature = true; + break; + } + } + if (fearsomeCreature) + { + if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% + { + bte.effect = vstd::to_underlying(BonusType::FEAR); + gameHandler->sendAndApply(&bte); + } + } + } + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + int side = gameHandler->gameState()->curB->whatSide(st->unitOwner()); + if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0) + { + bool cast = false; + while(!bl.empty() && !cast) + { + auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); + auto spellID = SpellID(bonus->subtype); + const CSpell * spell = SpellID(spellID).toSpell(); + bl.remove_if([&bonus](const Bonus * b) + { + return b == bonus.get(); + }); + spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell); + parameters.setSpellLevel(bonus->val); + parameters.massive = true; + parameters.smart = true; + //todo: recheck effect level + if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) + { + cast = true; + + int cooldown = bonus->additionalInfo[0]; + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; + ssp.absolute = false; + ssp.val = cooldown; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + } + } +} + +void BattleProcessor::makeStackDoNothing(const CStack * next) +{ + BattleAction doNothing; + doNothing.actionType = EActionType::NO_ACTION; + doNothing.side = next->unitSide(); + doNothing.stackNumber = next->unitId(); + + makeAutomaticAction(next, doNothing); +} + + +void BattleProcessor::setBattleResult(BattleResult::EResult resultType, int victoriusSide) +{ + boost::unique_lock guard(battleResult.mx); + if (battleResult.data) + { + gameHandler->complain((boost::format("The battle result has been already set (to %d, asked to %d)") + % battleResult.data->result % resultType).str()); + return; + } + auto br = new BattleResult(); + br->result = resultType; + br->winner = victoriusSide; //surrendering side loses + gameHandler->gameState()->curB->calculateCasualties(br->casualties); + battleResult.data = br; +} + +void BattleProcessor::removeObstacle(const CObstacleInstance & obstacle) +{ + BattleObstaclesChanged obsRem; + obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&obsRem); +} + +void BattleProcessor::updateGateState() +{ + // GATE_BRIDGE - leftmost tile, located over moat + // GATE_OUTER - central tile, mostly covered by gate image + // GATE_INNER - rightmost tile, inside the walls + + // GATE_OUTER or GATE_INNER: + // - if defender moves unit on these tiles, bridge will open + // - if there is a creature (dead or alive) on these tiles, bridge will always remain open + // - blocked to attacker if bridge is closed + + // GATE_BRIDGE + // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) + // - if Force Field is cast here, bridge can't open (but can close, in any town) + // - deals moat damage to attacker if bridge is closed (fortress only) + + bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); + bool hasStackAtGateInner = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; + bool hasStackAtGateOuter = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; + bool hasStackAtGateBridge = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + BattleUpdateGateState db; + db.state = gameHandler->gameState()->curB->si.gateState; + if (gameHandler->gameState()->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) + { + db.state = EGateState::DESTROYED; + } + else if (db.state == EGateState::OPENED) + { + bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; + bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; + + if (gateCanClose) + db.state = EGateState::CLOSED; + else + db.state = EGateState::OPENED; + } + else // CLOSED or BLOCKED + { + bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; + + if (gateBlocked) + db.state = EGateState::BLOCKED; + else + db.state = EGateState::CLOSED; + } + + if (db.state != gameHandler->gameState()->curB->si.gateState) + gameHandler->sendAndApply(&db); +} + diff --git a/server/BattleProcessor.h b/server/BattleProcessor.h new file mode 100644 index 000000000..f0e653601 --- /dev/null +++ b/server/BattleProcessor.h @@ -0,0 +1,129 @@ +/* + * BattleProcessor.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/GameConstants.h" +#include "../lib/NetPacks.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +struct SideInBattle; + +namespace battle { +class CUnitState; +} + +VCMI_LIB_NAMESPACE_END + +class CBattleQuery; + +struct CasualtiesAfterBattle +{ + using TStackAndItsNewCount = std::pair; + using TSummoned = std::map; + enum {ERASE = -1}; + const CArmedInstance * army; + std::vector newStackCounts; + std::vector removedWarMachines; + TSummoned summoned; + ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations + + CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); + void updateArmy(CGameHandler *gh); +}; + +struct FinishingBattleHelper +{ + FinishingBattleHelper(); + FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} + + const CGHeroInstance *winnerHero, *loserHero; + PlayerColor victor, loser; + ui8 winnerSide; + + int remainingBattleQueriesCount; + + template void serialize(Handler &h, const int version) + { + h & winnerHero; + h & loserHero; + h & victor; + h & loser; + h & winnerSide; + h & remainingBattleQueriesCount; + } +}; + +using FireShieldInfo = std::vector>; + +class BattleProcessor : boost::noncopyable +{ + ////used only in endBattle - don't touch elsewhere + bool visitObjectAfterVictory; + + std::unique_ptr battleThread; + std::unique_ptr finishingBattle; + + void removeObstacle(const CObstacleInstance &obstacle); + void makeStackDoNothing(const CStack * next); + void updateGateState(); + bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + void stackEnchantedTrigger(const CStack * stack); + void stackTurnTrigger(const CStack *stack); + void engageIntoBattle( PlayerColor player ); + void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + + int moveStack(int stack, BattleHex dest); //returned value - travelled distance + void runBattle(); + + void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + + // damage, drain life & fire shield; returns amount of drained life + int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + + void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); + void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + + void checkBattleStateChanges(); + void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); + void setBattleResult(BattleResult::EResult resultType, int victoriusSide); + +public: + CGameHandler * gameHandler; + + BattleProcessor(CGameHandler * gameHandler); + BattleProcessor(); + + ~BattleProcessor(); + + void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); //use hero=nullptr for no hero + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); //if any of armies is hero, hero will be used + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + + void battleAfterLevelUp(const BattleResult &result); + + bool makeBattleAction(BattleAction &ba); + bool makeCustomAction(BattleAction &ba); + + void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle + void endBattleConfirm(const BattleInfo * battleInfo); + + template void serialize(Handler &h, const int version) + { + + } + +}; + diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 68a907cd9..4c69714d9 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -10,59 +10,56 @@ #include "StdInc.h" #include "CGameHandler.h" +#include "BattleProcessor.h" +#include "CVCMIServer.h" #include "HeroPoolProcessor.h" +#include "PlayerMessageProcessor.h" #include "ServerNetPackVisitors.h" #include "ServerSpellCastEnvironment.h" -#include "CVCMIServer.h" -#include "PlayerMessageProcessor.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileInfo.h" -#include "../lib/int3.h" #include "../lib/ArtifactUtils.h" -#include "../lib/StartInfo.h" #include "../lib/CArtHandler.h" #include "../lib/CBuildingHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/pathfinder/CPathfinder.h" -#include "../lib/pathfinder/PathfinderOptions.h" -#include "../lib/pathfinder/TurnInfo.h" -#include "../lib/spells/AbilityCaster.h" -#include "../lib/spells/BonusCaster.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/spells/ObstacleCasterProxy.h" -#include "../lib/spells/Problem.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CTownHandler.h" #include "../lib/CCreatureHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CStack.h" +#include "../lib/CCreatureSet.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CSoundBase.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CTownHandler.h" +#include "../lib/GameConstants.h" #include "../lib/UnlockGuard.h" #include "../lib/GameSettings.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/CondSh.h" +#include "../lib/ScriptHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/TerrainHandler.h" +#include "../lib/VCMIDirs.h" #include "../lib/VCMI_Lib.h" +#include "../lib/int3.h" + +#include "../lib/filesystem/FileInfo.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/gameState/CGameState.h" + #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" #include "../lib/modding/ModIncompatibility.h" -#include "../lib/rmg/CMapGenOptions.h" -#include "../lib/VCMIDirs.h" -#include "../lib/ScopeGuard.h" -#include "../lib/CSoundBase.h" -#include "../lib/TerrainHandler.h" -#include "../lib/CCreatureSet.h" -#include "../lib/CThreadHelper.h" -#include "../lib/GameConstants.h" +#include "../lib/pathfinder/CPathfinder.h" +#include "../lib/pathfinder/PathfinderOptions.h" +#include "../lib/pathfinder/TurnInfo.h" + #include "../lib/registerTypes/RegisterTypes.h" + +#include "../lib/rmg/CMapGenOptions.h" + #include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/Connection.h" #include "../lib/serializer/Cast.h" +#include "../lib/serializer/Connection.h" #include "../lib/serializer/JsonSerializer.h" -#include "../lib/ScriptHandler.h" + +#include "../lib/spells/CSpellHandler.h" + #include "vstd/CLoggerBase.h" -#include #include #include #include @@ -76,8 +73,6 @@ #define COMPLAIN_RET(txt) {complain(txt); return false;} #define COMPLAIN_RETF(txt, FORMAT) {complain(boost::str(boost::format(txt) % FORMAT)); return false;} -CondSh battleMadeAction(false); -CondSh battleResult(nullptr); template class CApplyOnGH; class CBaseForGHApply @@ -129,95 +124,6 @@ static inline double distance(int3 a, int3 b) { return std::sqrt((double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); } -static void giveExp(BattleResult &r) -{ - if (r.winner > 1) - { - // draw - return; - } - r.exp[0] = 0; - r.exp[1] = 0; - for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) - { - r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; - } -} - -static void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard -{ - int x = targetPosition.getX(); - int y = targetPosition.getY(); - - const bool targetIsAttacker = side == BattleSide::ATTACKER; - - if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - - //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's - if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) - { - if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } - else - { //add back-side guardians for two-hex target, side guardians for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); - - if (!targetIsTwoHex && x > 2) //back guard for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); - else if (targetIsTwoHex)//front-side guardians for two-hex target - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - if (x > 3) //back guard for two-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - } - } - - } - - else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) - { - if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - else - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); - - if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else if (targetIsTwoHex) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - if (x < GameConstants::BFIELD_WIDTH - 4) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - } - } - } - - else if (!targetIsAttacker && y % 2 == 0) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - - else if (targetIsAttacker && y % 2 == 1) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } -} PlayerStatus PlayerStatuses::operator[](PlayerColor player) { @@ -574,644 +480,6 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh giveSpells(hero->visitedTown, hero); } -void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) -{ - LOG_TRACE(logGlobal); - - //Fill BattleResult structure with exp info - giveExp(*battleResult.data); - - if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped - { - if(heroAttacker) - battleResult.data->exp[1] += 500; - if(heroDefender) - battleResult.data->exp[0] += 500; - } - - if(heroAttacker) - battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill - if(heroDefender) - battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - if (!battleQuery) - { - logGlobal->error("Cannot find battle query!"); - complain("Player " + boost::lexical_cast(gs->curB->sides[0].color) + " has no battle query at the top!"); - return; - } - - battleQuery->result = std::make_optional(*battleResult.data); - - //Check how many battle queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0; - finishingBattle = std::make_unique(battleQuery, queriedPlayers); - - // in battles against neutrals, 1st player can ask to replay battle manually - if (!gs->curB->sides[1].color.isValidPlayer()) - { - auto battleDialogQuery = std::make_shared(this, gs->curB); - battleResult.data->queryID = battleDialogQuery->queryID; - queries.addQuery(battleDialogQuery); - } - else - battleResult.data->queryID = -1; - - //set same battle result for all queries - for(auto q : queries.allQueries()) - { - auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery) - otherBattleQuery->result = battleQuery->result; - } - - sendAndApply(battleResult.data); //after this point casualties objects are destroyed - - if (battleResult.data->queryID == -1) - endBattleConfirm(gs->curB); -} - -void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo) -{ - auto battleQuery = std::dynamic_pointer_cast(queries.topQuery(battleInfo->sides.at(0).color)); - if(!battleQuery) - { - logGlobal->trace("No battle query, battle end was confirmed by another player"); - return; - } - - const BattleResult::EResult result = battleResult.get()->result; - - CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle - ChangeSpells cs; //for Eagle Eye - - if(!finishingBattle->isDraw() && finishingBattle->winnerHero) - { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) - { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) - { - auto spell = spellId.toSpell(VLC->spells()); - if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && getRandomGenerator().nextInt(99) < eagleEyeChance) - cs.spells.insert(spell->getId()); - } - } - } - std::vector arts; //display them in window - - if(result == BattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) - { - auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) - { - const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); - if(ArtifactUtils::isSlotBackpack(slot)) - ma->askAssemble = false; - sendAndApply(ma); - } - }; - - if (finishingBattle->loserHero) - { - //TODO: wrap it into a function, somehow (std::variant -_-) - auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig() && - art->artType->getId() != ArtifactID::SPELLBOOK) - // don't move war machines or locked arts (spellbook) - { - sendMoveArtifact(art, &ma); - } - } - for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - { - //we assume that no big artifacts can be found - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); - if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won - { - sendMoveArtifact(art, &ma); - } - } - if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? - { - artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - - if (arts.size()) //display loot - { - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - - iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact - - for (auto art : arts) //TODO; separate function to display loot for various ojects? - { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); - if (iw.components.size() >= 14) - { - sendAndApply(&iw); - iw.components.clear(); - } - } - if (iw.components.size()) - { - sendAndApply(&iw); - } - } - //Eagle Eye secondary skill handling - if (!cs.spells.empty()) - { - cs.learn = 1; - cs.hid = finishingBattle->winnerHero->id; - - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); - - std::ostringstream names; - for (int i = 0; i < cs.spells.size(); i++) - { - names << "%s"; - if (i < cs.spells.size() - 2) - names << ", "; - else if (i < cs.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.replaceRawString(names.str()); - - auto it = cs.spells.begin(); - for (int i = 0; i < cs.spells.size(); i++, it++) - { - iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); - if (i == cs.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); - } - sendAndApply(&iw); - sendAndApply(&cs); - } - cab1.updateArmy(this); - cab2.updateArmy(this); //take casualties after battle is deleted - - if(finishingBattle->loserHero) //remove beaten hero - { - RemoveObject ro(finishingBattle->loserHero->id); - sendAndApply(&ro); - } - if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - } - - if(battleResult.data->winner == BattleSide::DEFENDER - && finishingBattle->winnerHero - && finishingBattle->winnerHero->visitedTown - && !finishingBattle->winnerHero->inTownGarrison - && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) - { - swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place - } - //give exp - if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - - BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); - raccepted.heroResult[0].exp = battleResult.data->exp[0]; - raccepted.heroResult[1].exp = battleResult.data->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; - sendAndApply(&raccepted); - - queries.popIfTop(battleQuery); - //--> continuation (battleAfterLevelUp) occurs after level-up queries are handled or on removing query -} - -void CGameHandler::battleAfterLevelUp(const BattleResult &result) -{ - LOG_TRACE(logGlobal); - - if(!finishingBattle) - return; - - finishingBattle->remainingBattleQueriesCount--; - logGlobal->trace("Decremented queries count to %d", finishingBattle->remainingBattleQueriesCount); - - if (finishingBattle->remainingBattleQueriesCount > 0) - //Battle results will be handled when all battle queries are closed - return; - - //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible - // but the battle consequences are applied after final player is unblocked. Hard to abuse... - // Still, it looks like a hole. - - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); - // Give raised units to winner and show dialog, if any were raised, - // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); - - if (necroSlot != SlotID()) - { - finishingBattle->winnerHero->showNecromancyDialog(raisedStack, getRandomGenerator()); - addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); - } - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - sendAndApply(&resultsApplied); - - setBattle(nullptr); - - if (visitObjectAfterVictory && result.winner==0 && !finishingBattle->winnerHero->stacks.empty()) - { - logGlobal->trace("post-victory visit"); - visitObjectOnTile(*getTile(finishingBattle->winnerHero->visitablePos()), finishingBattle->winnerHero); - } - visitObjectAfterVictory = false; - - //handle victory/loss of engaged players - std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; - checkVictoryLossConditions(playerColors); - - if (result.result == BattleResult::SURRENDER) - heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); - - if (result.result == BattleResult::ESCAPE) - heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); - - if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() - && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) - { - RemoveObject ro(finishingBattle->winnerHero->id); - sendAndApply(&ro); - - if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) - heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); - } - - finishingBattle.reset(); -} - -void CGameHandler::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) -{ - if(first && !counter) - handleAttackBeforeCasting(ranged, attacker, defender); - - FireShieldInfo fireShield; - BattleAttack bat; - BattleLogMessage blm; - bat.stackAttacking = attacker->unitId(); - bat.tile = targetHex; - - std::shared_ptr attackerState = attacker->acquireState(); - - if(ranged) - bat.flags |= BattleAttack::SHOT; - if(counter) - bat.flags |= BattleAttack::COUNTER; - - const int attackerLuck = attacker->luckVal(); - - if(attackerLuck > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::LUCKY; - } - - if(attackerLuck < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::UNLUCKY; - } - - if (getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) - { - bat.flags |= BattleAttack::DEATH_BLOW; - } - - const auto * owner = gs->curB->getHero(attacker->unitOwner()); - if(owner) - { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); - if (chance > getRandomGenerator().nextInt(99)) - bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; - } - - int64_t drainedLife = 0; - - // only primary target - if(defender->alive()) - drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); - - //multiple-hex normal attack - std::set attackedCreatures = gs->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - - std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? - { - //this is need for displaying hit animation - bat.flags |= BattleAttack::SPELL_LIKE; - bat.spellID = SpellID(bonus->subtype); - - //TODO: should spell override creature`s projectile? - - auto spell = bat.spellID.toSpell(); - - battle::Target target; - target.emplace_back(defender, targetHex); - - spells::BattleCast event(gs->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); - event.setSpellLevel(bonus->val); - - auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); - - //TODO: get exact attacked hex for defender - - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - { - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - } - - //now add effect info for all attacked stacks - for (BattleStackAttacked & bsa : bat.bsa) - { - if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield - { - //this is need for displaying affect animation - bsa.flags |= BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID(bonus->subtype); - } - } - } - - attackerState->afterAttack(ranged, counter); - - { - UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); - attackerState->save(info.data); - bat.attackerChanges.changedStacks.push_back(info); - } - - if (drainedLife > 0) - bat.flags |= BattleAttack::LIFE_DRAIN; - - sendAndApply(&bat); - - { - const bool multipleTargets = bat.bsa.size() > 1; - - int64_t totalDamage = 0; - int32_t totalKills = 0; - - for(const BattleStackAttacked & bsa : bat.bsa) - { - totalDamage += bsa.damageAmount; - totalKills += bsa.killedAmount; - } - - { - MetaString text; - attacker->addText(text, EMetaText::GENERAL_TXT, 376); - attacker->addNameReplacement(text); - text.replaceNumber(totalDamage); - blm.lines.push_back(text); - } - - addGenericKilledLog(blm, defender, totalKills, multipleTargets); - } - - // drain life effect (as well as log entry) must be applied after the attack - if(drainedLife > 0) - { - MetaString text; - attackerState->addText(text, EMetaText::GENERAL_TXT, 361); - attackerState->addNameReplacement(text, false); - text.replaceNumber(drainedLife); - defender->addNameReplacement(text, true); - blm.lines.push_back(std::move(text)); - } - - if(!fireShield.empty()) - { - //todo: this should be "virtual" spell instead, we only need fire spell school bonus here - const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); - int64_t totalDamage = 0; - - for(const auto & item : fireShield) - { - const CStack * actor = item.first; - int64_t rawDamage = item.second; - - const CGHeroInstance * actorOwner = gs->curB->getHero(actor->unitOwner()); - - if(actorOwner) - { - rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); - } - else - { - rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); - } - - totalDamage+=rawDamage; - //FIXME: add custom effect on actor - } - - if (totalDamage > 0) - { - BattleStackAttacked bsa; - - bsa.flags |= BattleStackAttacked::FIRE_SHIELD; - bsa.stackAttacked = attacker->unitId(); //invert - bsa.attackerID = defender->unitId(); - bsa.damageAmount = totalDamage; - attacker->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured pack; - pack.stacks.push_back(bsa); - sendAndApply(&pack); - - // TODO: this is already implemented in Damage::describeEffect() - { - MetaString text; - text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); - text.replaceNumber(totalDamage); - blm.lines.push_back(std::move(text)); - } - addGenericKilledLog(blm, attacker, bsa.killedAmount, false); - } - } - - sendAndApply(&blm); - - handleAfterAttackCasting(ranged, attacker, defender); -} - -int64_t CGameHandler::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) -{ - BattleStackAttacked bsa; - if(secondary) - bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities - - bsa.attackerID = attackerState->unitId(); - bsa.stackAttacked = def->unitId(); - { - BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); - - bai.deathBlow = bat.deathBlow(); - bai.doubleDamage = bat.ballistaDoubleDmg(); - bai.luckyStrike = bat.lucky(); - bai.unluckyStrike = bat.unlucky(); - - auto range = gs->curB->calculateDmgRange(bai); - bsa.damageAmount = gs->curB->getActualDamage(range.damage, attackerState->getCount(), getRandomGenerator()); - CStack::prepareAttacked(bsa, getRandomGenerator(), bai.defender->acquireState()); //calculate casualties - } - - int64_t drainedLife = 0; - - //life drain handling - if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) - { - int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; - attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - drainedLife += toHeal; - } - - //soul steal handling - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) - { - //we can have two bonuses - one with subtype 0 and another with subtype 1 - //try to use permanent first, use only one of two - for(si32 subtype = 1; subtype >= 0; subtype--) - { - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) - { - int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); - drainedLife += toHeal; - break; - } - } - } - bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated - - //fire shield handling - if(!bat.shot() && - !def->isClone() && - def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && - CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) - ) - { - //TODO: use damage with bonus but without penalties - auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; - fireShield.push_back(std::make_pair(def, fireShieldDamage)); - } - - return drainedLife; -} - -void CGameHandler::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - BattleLogMessage blm; - addGenericKilledLog(blm, defender, killed, multiple); - sendAndApply(&blm); - } -} - -void CGameHandler::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - const int32_t txtIndex = (killed > 1) ? 379 : 378; - std::string formatString = VLC->generaltexth->allTexts[txtIndex]; - - // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); - boost::algorithm::trim(formatString); - - boost::format txt(formatString); - if(killed > 1) - { - txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish - } - else //killed == 1 - { - txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes - } - MetaString line; - line.appendRawString(txt.str()); - blm.lines.push_back(std::move(line)); - } -} - void CGameHandler::handleClientDisconnection(std::shared_ptr c) { if(lobby->state == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) @@ -1271,300 +539,11 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) vstd::clear_pointer(pack); } -int CGameHandler::moveStack(int stack, BattleHex dest) -{ - int ret = 0; - - const CStack *curStack = gs->curB->battleGetStackByID(stack), - *stackAtEnd = gs->curB->battleGetStackByPos(dest); - - assert(curStack); - assert(dest < GameConstants::BFIELD_SIZE); - - if (gs->curB->tacticDistance) - { - assert(gs->curB->isInTacticRange(dest)); - } - - auto start = curStack->getPosition(); - if (start == dest) - return 0; - - //initing necessary tables - auto accessibility = getAccesibility(curStack); - std::set passed; - //Ignore obstacles on starting position - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - - //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) - if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) - { - BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); - - if(accessibility.accessible(shifted, curStack)) - dest = shifted; - } - - if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) - { - complain("Given destination is not accessible!"); - return 0; - } - - bool canUseGate = false; - auto dbState = gs->curB->si.gateState; - if(battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && - dbState != EGateState::DESTROYED && - dbState != EGateState::BLOCKED) - { - canUseGate = true; - } - - std::pair< std::vector, int > path = gs->curB->getPath(start, dest, curStack); - - ret = path.second; - - int creSpeed = curStack->speed(0, true); - - if (gs->curB->tacticDistance > 0 && creSpeed > 0) - creSpeed = GameConstants::BFIELD_SIZE; - - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_OUTER) - return true; - if (hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (isGateDrawbridgeHex(hex)) - return true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) - return true; - } - - return false; - }; - - if (curStack->hasBonusOfType(BonusType::FLYING)) - { - if (path.second <= creSpeed && path.first.size() > 0) - { - if (canUseGate && dbState != EGateState::OPENED && - occupyGateDrawbridgeHex(dest)) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - - //inform clients about move - BattleStackMoved sm; - sm.stack = curStack->unitId(); - std::vector tiles; - tiles.push_back(path.first[0]); - sm.tilesToMove = tiles; - sm.distance = path.second; - sm.teleporting = false; - sendAndApply(&sm); - } - } - else //for non-flying creatures - { - std::vector tiles; - const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); - int v = (int)path.first.size()-1; - path.first.push_back(start); - - // check if gate need to be open or closed at some point - BattleHex openGateAtHex, gateMayCloseAtHex; - if (canUseGate) - { - for (int i = (int)path.first.size()-1; i >= 0; i--) - { - auto needOpenGates = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) - return true; - else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto hex = path.first[i]; - if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) - { - if (needOpenGates(hex)) - openGateAtHex = path.first[i+1]; - - //TODO we need find batter way to handle double-wide stacks - //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && needOpenGates(otherHex)) - openGateAtHex = path.first[i+2]; - } - - //gate may be opened and then closed during stack movement, but not other way around - if (openGateAtHex.isValid()) - dbState = EGateState::OPENED; - } - - if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) - { - if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - if (hasWideMoat) - { - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && - path.first[i-1] != ESiegeHex::GATE_INNER && - path.first[i-1] != ESiegeHex::GATE_BRIDGE) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - } - } - - bool stackIsMoving = true; - - while(stackIsMoving) - { - if (verror("Movement terminated abnormally"); - break; - } - - bool gateStateChanging = false; - //special handling for opening gate on from starting hex - if (openGateAtHex.isValid() && openGateAtHex == start) - gateStateChanging = true; - else - { - for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) - { - BattleHex hex = path.first[v]; - tiles.push_back(hex); - - if ((openGateAtHex.isValid() && openGateAtHex == hex) || - (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) - { - gateStateChanging = true; - } - - //if we walked onto something, finalize this portion of stack movement check into obstacle - if(!battleGetAllObstaclesOnPos(hex, false).empty()) - obstacleHit = true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - //two hex creature hit obstacle by backside - auto obstacle2 = battleGetAllObstaclesOnPos(otherHex, false); - if(otherHex.isValid() && !obstacle2.empty()) - obstacleHit = true; - } - if(!obstacleHit) - passed.insert(hex); - } - } - - if (!tiles.empty()) - { - //commit movement - BattleStackMoved sm; - sm.stack = curStack->unitId(); - sm.distance = path.second; - sm.teleporting = false; - sm.tilesToMove = tiles; - sendAndApply(&sm); - tiles.clear(); - } - - //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end - if (curStack->getPosition() != dest) - { - if(stackIsMoving && start != curStack->getPosition()) - { - stackIsMoving = handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - } - if (gateStateChanging) - { - if (curStack->getPosition() == openGateAtHex) - { - openGateAtHex = BattleHex(); - //only open gate if stack is still alive - if (curStack->alive()) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - sendAndApply(&db); - } - } - else if (curStack->getPosition() == gateMayCloseAtHex) - { - gateMayCloseAtHex = BattleHex(); - updateGateState(); - } - } - } - else - //movement finished normally: we reached destination - stackIsMoving = false; - } - } - //handle last hex separately for deviation - if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) - { - if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) - || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) - passed.clear(); //Just empty passed, obstacles will handled automatically - } - //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - handleObstacleTriggersForUnit(*spellEnv, *curStack, passed); - - return ret; -} CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) + , battles(std::make_unique(this)) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") @@ -1574,19 +553,12 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) IObjectInterface::cb = this; applier = std::make_shared>(); registerTypesServerPacks(*applier); - visitObjectAfterVictory = false; spellEnv = new ServerSpellCastEnvironment(this); } CGameHandler::~CGameHandler() { - if (battleThread) - { - //Setting battleMadeAction is needed because battleThread waits for the action to continue the main loop - battleMadeAction.setn(true); - battleThread->join(); - } delete spellEnv; delete gs; } @@ -2112,45 +1084,6 @@ std::list CGameHandler::generatePlayerTurnOrder() const return playerTurnOrder; } -void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) -{ - battleResult.set(nullptr); - - const auto & t = *getTile(tile); - TerrainId terrain = t.terType->getId(); - if (gs->map->isCoastalTile(tile)) //coastal tile is always ground - terrain = ETerrainId::SAND; - - BattleField terType = gs->battleGetBattlefieldType(tile, getRandomGenerator()); - if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) - terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); - - //send info about battles - BattleStart bs; - bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); - - engageIntoBattle(bs.info->sides[0].color); - engageIntoBattle(bs.info->sides[1].color); - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(bs.info->sides[0].color)); - bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); - - sendAndApply(&bs); -} - -void CGameHandler::checkBattleStateChanges() -{ - //check if drawbridge state need to be changes - if (battleGetSiegeLevel() > 0) - updateGateState(); - - //check if battle ended - if (auto result = battleIsFinished()) - { - setBattleResult(BattleResult::NORMAL, *result); - } -} - void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) { if (!h->hasSpellbook()) @@ -2587,69 +1520,6 @@ void CGameHandler::removeArtifact(const ArtifactLocation &al) ea.al = al; sendAndApply(&ea); } -void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) //use hero=nullptr for no hero -{ - if(gs->curB) - gs->curB.dellNull(); - - static const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - static const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; - - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - - auto lastBattleQuery = std::dynamic_pointer_cast(queries.topQuery(gs->curB->sides[0].color)); - - //existing battle query for retying auto-combat - if(lastBattleQuery) - { - for(int i : {0, 1}) - { - if(heroes[i]) - { - SetMana restoreInitialMana; - restoreInitialMana.val = lastBattleQuery->initialHeroMana[i]; - restoreInitialMana.hid = heroes[i]->id; - sendAndApply(&restoreInitialMana); - } - } - - lastBattleQuery->bi = gs->curB; - lastBattleQuery->result = std::nullopt; - lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject; - lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject; - } - - auto nextBattleQuery = std::make_shared(this, gs->curB); - for(int i : {0, 1}) - { - if(heroes[i]) - { - nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; - } - } - queries.addQuery(nextBattleQuery); - - this->battleThread = std::make_unique(boost::thread(&CGameHandler::runBattle, this)); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) -{ - startBattlePrimary(army1, army2, tile, - army1->ID == Obj::HERO ? static_cast(army1) : nullptr, - army2->ID == Obj::HERO ? static_cast(army2) : nullptr, - creatureBank); -} - -void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank) -{ - startBattleI(army1, army2, army2->visitablePos(), creatureBank); -} void CGameHandler::changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) { @@ -4336,708 +3206,6 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor return true; } -static EndAction end_action; - -void CGameHandler::updateGateState() -{ - // GATE_BRIDGE - leftmost tile, located over moat - // GATE_OUTER - central tile, mostly covered by gate image - // GATE_INNER - rightmost tile, inside the walls - - // GATE_OUTER or GATE_INNER: - // - if defender moves unit on these tiles, bridge will open - // - if there is a creature (dead or alive) on these tiles, bridge will always remain open - // - blocked to attacker if bridge is closed - - // GATE_BRIDGE - // - if there is a unit or corpse here, bridge can't open (and can't close in fortress) - // - if Force Field is cast here, bridge can't open (but can close, in any town) - // - deals moat damage to attacker if bridge is closed (fortress only) - - bool hasForceFieldOnBridge = !battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); - bool hasStackAtGateInner = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; - bool hasStackAtGateOuter = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; - bool hasStackAtGateBridge = gs->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; - bool hasWideMoat = vstd::contains_if(battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - BattleUpdateGateState db; - db.state = gs->curB->si.gateState; - if (gs->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) - { - db.state = EGateState::DESTROYED; - } - else if (db.state == EGateState::OPENED) - { - bool hasStackOnLongBridge = hasStackAtGateBridge && hasWideMoat; - bool gateCanClose = !hasStackAtGateInner && !hasStackAtGateOuter && !hasStackOnLongBridge; - - if (gateCanClose) - db.state = EGateState::CLOSED; - else - db.state = EGateState::OPENED; - } - else // CLOSED or BLOCKED - { - bool gateBlocked = hasForceFieldOnBridge || hasStackAtGateBridge; - - if (gateBlocked) - db.state = EGateState::BLOCKED; - else - db.state = EGateState::CLOSED; - } - - if (db.state != gs->curB->si.gateState) - sendAndApply(&db); -} - -bool CGameHandler::makeBattleAction(BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - bool ok = true; - - battle::Target target = ba.getTarget(gs->curB); - - const CStack * stack = battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack - - const bool isAboutActiveStack = stack && (ba.stackNumber == gs->curB->getActiveStackID()); - - logGlobal->trace("Making action: %s", ba.toString()); - - switch(ba.actionType) - { - case EActionType::WALK: //walk - case EActionType::DEFEND: //defend - case EActionType::WAIT: //wait - case EActionType::WALK_AND_ATTACK: //walk or attack - case EActionType::SHOOT: //shoot - case EActionType::CATAPULT: //catapult - case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::MONSTER_SPELL: - - if (!stack) - { - complain("No such stack!"); - return false; - } - if (!stack->alive()) - { - complain("This stack is dead: " + stack->nodeName()); - return false; - } - - if (battleTacticDist()) - { - if (stack && stack->unitSide() != battleGetTacticsSide()) - { - complain("This is not a stack of side that has tactics!"); - return false; - } - } - else if (!isAboutActiveStack) - { - complain("Action has to be about active stack!"); - return false; - } - } - - auto wrapAction = [this](BattleAction &ba) - { - StartAction startAction(ba); - sendAndApply(&startAction); - - return vstd::makeScopeGuard([&]() - { - sendAndApply(&end_action); - }); - }; - - switch(ba.actionType) - { - case EActionType::END_TACTIC_PHASE: //wait - case EActionType::BAD_MORALE: - case EActionType::NO_ACTION: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::WALK: - { - auto wrapper = wrapAction(ba); - if(target.size() < 1) - { - complain("Destination required for move action."); - ok = false; - break; - } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move - if (!walkedTiles) - complain("Stack failed movement!"); - break; - } - case EActionType::DEFEND: - { - //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) - SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); - int oldDefenceValue = defence.totalValue(); - - defence.push_back(std::make_shared(defenseBonusToAdd)); - defence.push_back(std::make_shared(bonus2)); - - int difference = defence.totalValue() - oldDefenceValue; - std::vector buffer; - if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) - { - difference = 1; - buffer.push_back(alternativeWeakCreatureBonus); - } - else - { - buffer.push_back(defenseBonusToAdd); - } - - buffer.push_back(bonus2); - - sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - sendAndApply(&sse); - - BattleLogMessage message; - - MetaString text; - stack->addText(text, EMetaText::GENERAL_TXT, 120); - stack->addNameReplacement(text); - text.replaceNumber(difference); - - message.lines.push_back(text); - - sendAndApply(&message); - //don't break - we share code with next case - } - [[fallthrough]]; - case EActionType::WAIT: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::RETREAT: //retreat/flee - { - if (!gs->curB->battleCanFlee(gs->curB->sides.at(ba.side).color)) - complain("Cannot retreat!"); - else - setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case EActionType::SURRENDER: - { - PlayerColor player = gs->curB->sides.at(ba.side).color; - int cost = gs->curB->battleGetSurrenderCost(player); - if (cost < 0) - complain("Cannot surrender!"); - else if (getResource(player, EGameResID::GOLD) < cost) - complain("Not enough gold to surrender!"); - else - { - giveResource(player, EGameResID::GOLD, -cost); - setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case EActionType::WALK_AND_ATTACK: //walk or attack - { - auto wrapper = wrapAction(ba); - - if(!stack) - { - complain("No attacker"); - ok = false; - break; - } - - if(target.size() < 2) - { - complain("Two destinations required for attack action."); - ok = false; - break; - } - - BattleHex attackPos = target.at(0).hexValue; - BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gs->curB->battleGetStackByPos(destinationTile, true); - - if(!destinationStack) - { - complain("Invalid target to attack"); - ok = false; - break; - } - - BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); - - logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - - if(stack->getPosition() != attackPos - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) - ) - { - // we were not able to reach destination tile, nor occupy specified hex - // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine - break; - } - - if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - complain("Unit can not attack itself"); - ok = false; - break; - } - - if(!CStack::isMeleeAttackPossible(stack, destinationStack)) - { - complain("Attack cannot be performed!"); - ok = false; - break; - } - - //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); - const bool retaliation = destinationStack->ableToRetaliate(); - for (int i = 0; i < totalAttacks; ++i) - { - //first strike - if(i == 0 && firstStrike && retaliation) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - - //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification - if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) - { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - } - - //counterattack - //we check retaliation twice, so if it unblocked during attack it will work only on next attack - if(stack->alive() - && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) - && (i == 0 && !firstStrike) - && retaliation && destinationStack->ableToRetaliate()) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - } - - //return - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) - && target.size() == 3 - && startingPos != stack->getPosition() - && startingPos == target.at(2).hexValue - && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) - } - break; - } - case EActionType::SHOOT: - { - if(target.size() < 1) - { - complain("Destination required for shot action."); - ok = false; - break; - } - - auto destination = target.at(0).hexValue; - - const CStack * destinationStack = gs->curB->battleGetStackByPos(destination); - - if (!gs->curB->battleCanShoot(stack, destination)) - { - complain("Cannot shoot!"); - break; - } - if (!destinationStack) - { - complain("No target to shoot!"); - break; - } - - auto wrapper = wrapAction(ba); - - makeAttack(stack, destinationStack, 0, destination, true, true, false); - - //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) - && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) - && destinationStack->ableToRetaliate() - && gs->curB->battleCanShoot(destinationStack, stack->getPosition()) - && stack->alive()) //attacker may have died (fire shield) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); - } - //allow more than one additional attack - - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gs->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - for(int i = 1; i < totalRangedAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) - { - makeAttack(stack, destinationStack, 0, destination, false, true, false); - } - } - break; - } - case EActionType::CATAPULT: - { - auto wrapper = wrapAction(ba); - const CStack * shooter = gs->curB->battleGetStackByID(ba.stackNumber); - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) - { - complain("We do not know how to shoot :P"); - } - else - { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult - auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); - parameters.setSpellLevel(shotLevel); - parameters.cast(spellEnv, target); - } - //finish by scope guard - break; - } - case EActionType::STACK_HEAL: //healing with First Aid Tent - { - auto wrapper = wrapAction(ba); - const CStack * healer = gs->curB->battleGetStackByID(ba.stackNumber); - - if(target.size() < 1) - { - complain("Destination required for heal action."); - ok = false; - break; - } - - const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); - - if(target.at(0).unitValue) - destStack = target.at(0).unitValue; - else - destStack = gs->curB->battleGetUnitByPos(target.at(0).hexValue); - - if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) - { - complain("There is either no healer, no destination, or healer cannot heal :P"); - } - else - { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gs->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent - auto dest = battle::Destination(destStack, target.at(0).hexValue); - parameters.setSpellLevel(0); - parameters.cast(spellEnv, {dest}); - } - break; - } - case EActionType::MONSTER_SPELL: - { - auto wrapper = wrapAction(ba); - - const CStack * stack = gs->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.actionSubtype); - - std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if (randSpellcaster && battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = battleGetRandomStackSpell(getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID < 0) - complain("That stack can't cast spells!"); - else - { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gs->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(spellEnv, target); - } - break; - } - } - if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND - || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) - handleObstacleTriggersForUnit(*spellEnv, *stack); - if(ba.stackNumber == gs->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished - battleMadeAction.setn(true); - return ok; -} - -bool CGameHandler::makeCustomAction(BattleAction & ba) -{ - boost::unique_lock lock(battleActionMutex); - - switch(ba.actionType) - { - case EActionType::HERO_SPELL: - { - COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); - - const CGHeroInstance *h = gs->curB->battleGetFightingHero(ba.side); - COMPLAIN_RET_FALSE_IF((!h), "Wrong caster!"); - - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); - if (!s) - { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); - return false; - } - - spells::BattleCast parameters(gs->curB, h, spells::Mode::HERO, s); - - spells::detail::ProblemImpl problem; - - auto m = s->battleMechanics(¶meters); - - if(!m->canBeCast(problem))//todo: should we check aimed cast? - { - logGlobal->warn("Spell cannot be cast!"); - std::vector texts; - problem.getAll(texts); - for(auto s : texts) - logGlobal->warn(s); - return false; - } - - StartAction start_action(ba); - sendAndApply(&start_action); //start spell casting - - parameters.cast(spellEnv, ba.getTarget(gs->curB)); - - sendAndApply(&end_action); - if (!gs->curB->battleGetStackByID(gs->curB->activeStack)) - { - battleMadeAction.setn(true); - } - checkBattleStateChanges(); - if (battleResult.get()) - { - battleMadeAction.setn(true); - //battle will be ended by startBattle function - //endBattle(gs->curB->tile, gs->curB->heroes[0], gs->curB->heroes[1]); - } - - return true; - - } - } - return false; -} - -void CGameHandler::stackEnchantedTrigger(const CStack * st) -{ - auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); - for(auto b : bl) - { - const CSpell * sp = SpellID(b->subtype).toSpell(); - if(!sp) - continue; - - const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); - const int32_t level = ((val > 3) ? (val - 3) : val); - - spells::BattleCast battleCast(gs->curB, st, spells::Mode::PASSIVE, sp); - //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle - battleCast.setEffectDuration(50); - battleCast.setSpellLevel(level); - spells::Target target; - - if(val > 3) - { - for(auto s : gs->curB->battleGetAllStacks()) - if(battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied - target.emplace_back(s); - } - else - { - target.emplace_back(st); - } - battleCast.applyEffects(spellEnv, target, false, true); - } -} - -void CGameHandler::stackTurnTrigger(const CStack *st) -{ - BattleTriggerEffect bte; - bte.stackID = st->unitId(); - bte.effect = -1; - bte.val = 0; - bte.additionalInfo = 0; - if (st->alive()) - { - //unbind - if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) - { - bool unbind = true; - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); - auto adjacent = gs->curB->battleAdjacentUnits(st); - - for (auto b : bl) - { - if(b->additionalInfo != CAddInfo::NONE) - { - const CStack * stack = gs->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent - if(stack) - { - if(vstd::contains(adjacent, stack)) //binding stack is still present - unbind = false; - } - } - else - { - unbind = false; - } - } - if (unbind) - { - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::UNBIND; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - - if (st->hasBonusOfType(BonusType::POISON)) - { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) //TODO: what if not?... - { - bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); - if (bte.val < b->val) //(negative) poison effect increases - update it - { - bte.effect = vstd::to_underlying(BonusType::POISON); - sendAndApply(&bte); - } - } - } - if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) - { - const PlayerColor opponent = gs->curB->otherPlayer(gs->curB->battleGetOwner(st)); - const CGHeroInstance * opponentHero = gs->curB->getHero(opponent); - if(opponentHero) - { - ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); - vstd::amin(manaDrained, opponentHero->mana); - if(manaDrained) - { - bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); - bte.val = manaDrained; - bte.additionalInfo = opponentHero->id.getNum(); //for sanity - sendAndApply(&bte); - } - } - } - if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) - { - bool fearsomeCreature = false; - for (CStack * stack : gs->curB->stacks) - { - if (battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) - { - fearsomeCreature = true; - break; - } - } - if (fearsomeCreature) - { - if (getRandomGenerator().nextInt(99) < 10) //fixed 10% - { - bte.effect = vstd::to_underlying(BonusType::FEAR); - sendAndApply(&bte); - } - } - } - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = gs->curB->whatSide(st->unitOwner()); - if(st->canCast() && gs->curB->battleGetEnchanterCounter(side) == 0) - { - bool cast = false; - while(!bl.empty() && !cast) - { - auto bonus = *RandomGeneratorUtil::nextItem(bl, getRandomGenerator()); - auto spellID = SpellID(bonus->subtype); - const CSpell * spell = SpellID(spellID).toSpell(); - bl.remove_if([&bonus](const Bonus * b) - { - return b == bonus.get(); - }); - spells::BattleCast parameters(gs->curB, st, spells::Mode::ENCHANTER, spell); - parameters.setSpellLevel(bonus->val); - parameters.massive = true; - parameters.smart = true; - //todo: recheck effect level - if(parameters.castIfPossible(spellEnv, spells::Target(1, spells::Destination()))) - { - cast = true; - - int cooldown = bonus->additionalInfo[0]; - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; - ssp.absolute = false; - ssp.val = cooldown; - ssp.stackID = st->unitId(); - sendAndApply(&ssp); - } - } - } - } -} - void CGameHandler::handleTimeEvents() { gs->map->events.sort(evntCmp); @@ -5365,16 +3533,6 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) return true; } -void CGameHandler::engageIntoBattle(PlayerColor player) -{ - //notify interfaces - PlayerBlocked pb; - pb.player = player; - pb.reason = PlayerBlocked::UPCOMING_BATTLE; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - sendAndApply(&pb); -} - void CGameHandler::checkVictoryLossConditions(const std::set & playerColors) { for (auto playerColor : playerColors) @@ -5537,234 +3695,6 @@ bool CGameHandler::dig(const CGHeroInstance *h) return true; } -void CGameHandler::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) -{ - if(attacker->hasBonusOfType(attackMode)) - { - std::set spellsToCast; - TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - spellsToCast.insert(SpellID(sf->subtype)); - } - for(SpellID spellID : spellsToCast) - { - bool castMe = false; - if(!defender->alive()) - { - logGlobal->debug("attackCasting: all attacked creatures have been killed"); - return; - } - int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); - for(const auto & sf : *spellsByType) - { - int meleeRanged; - if(sf->additionalInfo.size() < 2) - { - // legacy format - vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); - meleeRanged = sf->additionalInfo[0] / 1000; - } - else - { - vstd::amax(spellLevel, sf->additionalInfo[0]); - meleeRanged = sf->additionalInfo[1]; - } - if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) - castMe = true; - } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); - vstd::amin(chance, 100); - - const CSpell * spell = SpellID(spellID).toSpell(); - spells::AbilityCaster caster(attacker, spellLevel); - - spells::Target target; - target.emplace_back(defender); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - - auto m = spell->battleMechanics(¶meters); - - spells::detail::ProblemImpl ignored; - - if(!m->canBeCastAt(target, ignored)) - continue; - - //check if spell should be cast (probability handling) - if(getRandomGenerator().nextInt(99) >= chance) - continue; - - //casting - if(castMe) - { - parameters.cast(spellEnv, target); - } - } - } -} - -void CGameHandler::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? -} - -void CGameHandler::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - if(!attacker->alive() || !defender->alive()) // can be already dead - return; - - attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); - - if(!defender->alive()) - { - //don't try death stare or acid breath on dead stack (crash!) - return; - } - - if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - - int staredCreatures = distribution(getRandomGenerator().getStdGenerator()); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); - if(staredCreatures) - { - //TODO: death stare was not originally available for multiple-hex attacks, but... - const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - parameters.setEffectValue(staredCreatures); - parameters.cast(spellEnv, target); - } - } - - if(!defender->alive()) - return; - - int64_t acidDamage = 0; - TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); - for(const auto & b : *acidBreath) - { - if(b->additionalInfo[0] > getRandomGenerator().nextInt(99)) - acidDamage += b->val; - } - - if(acidDamage > 0) - { - const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - - parameters.setEffectValue(acidDamage * attacker->getCount()); - parameters.cast(spellEnv, target); - } - - - if(!defender->alive()) - return; - - if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability - { - double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; - vstd::amin(chanceToTrigger, 1); //cap at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; - - if(defender->unitType()->getId() == bonusAdditionalInfo || - (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) - return; - - battle::UnitInfo resurrectInfo; - resurrectInfo.id = gs->curB->battleNextUnitId(); - resurrectInfo.summoned = false; - resurrectInfo.position = defender->getPosition(); - resurrectInfo.side = defender->unitSide(); - - if(bonusAdditionalInfo != CAddInfo::NONE) - resurrectInfo.type = CreatureID(bonusAdditionalInfo); - else - resurrectInfo.type = attacker->creatureId(); - - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) - resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) - resurrectInfo.count = defender->getCount(); - else - return; //wrong subtype - - BattleUnitsChanged addUnits; - addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); - resurrectInfo.save(addUnits.changedStacks.back().data); - - BattleUnitsChanged removeUnits; - removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); - sendAndApply(&removeUnits); - sendAndApply(&addUnits); - } - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) - { - double chanceToTrigger = 0; - int amountToDie = 0; - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; - amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); - } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; - } - - vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% - - if(getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - BattleStackAttacked bsa; - bsa.attackerID = -1; - bsa.stackAttacked = defender->unitId(); - bsa.damageAmount = amountToDie * defender->getMaxHealth(); - bsa.flags = BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID::SLAYER; - defender->prepareAttacked(bsa, getRandomGenerator()); - - StacksInjured si; - si.stacks.push_back(bsa); - - sendAndApply(&si); - sendGenericKilledLog(defender, bsa.killedAmount, false); - } -} - void CGameHandler::visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h) { if (!t.visitableObjects.empty()) @@ -5863,16 +3793,6 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h return true; } -void CGameHandler::makeStackDoNothing(const CStack * next) -{ - BattleAction doNothing; - doNothing.actionType = EActionType::NO_ACTION; - doNothing.side = next->unitSide(); - doNothing.stackNumber = next->unitId(); - - makeAutomaticAction(next, doNothing); -} - bool CGameHandler::insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) { if (sl.army->hasStackAtSlot(sl.slot)) @@ -6051,430 +3971,6 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s } } -void CGameHandler::runBattle() -{ - boost::unique_lock lock(battleActionMutex); - - setBattle(gs->curB); - assert(gs->curB); - //TODO: pre-tactic stuff, call scripts etc. - - //Moat should be initialized here, because only here we can use spellcasting - if (gs->curB->town && gs->curB->town->fortLevel() >= CGTownInstance::CITADEL) - { - const auto * h = gs->curB->battleGetFightingHero(BattleSide::DEFENDER); - const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(gs->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(gs->curB, &moatCaster, spells::Mode::PASSIVE, gs->curB->town->town->moatAbility.toSpell()); - auto target = spells::Target(); - cast.cast(spellEnv, target); - } - - //tactic round - { - while ((lobby->state != EServerState::SHUTDOWN) && gs->curB->tacticDistance && !battleResult.get()) - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - } - - //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = gs->curB->stacks; //use temporary variable to outclude summoned stacks added to gs->curB->stacks from processing - - for (CStack * stack : initialStacks) - { - if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) - { - std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); - std::vector targetHexes; - const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard - const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); - - /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. - For one-hex targets there are four guardians - front, back and one per side (up + down). - Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front - Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ - if (!guardianIsBig) - targetHexes = stack->getSurroundingHexes(); - else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); - - for(auto hex : targetHexes) - { - if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex - { - battle::UnitInfo info; - info.id = gs->curB->battleNextUnitId(); - info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); - info.type = creatureData; - info.side = stack->unitSide(); - info.position = hex; - info.summoned = true; - - BattleUnitsChanged pack; - pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(pack.changedStacks.back().data); - sendAndApply(&pack); - } - } - } - - stackEnchantedTrigger(stack); - } - - //spells opening battle - for (int i = 0; i < 2; ++i) - { - auto h = gs->curB->battleGetFightingHero(i); - if (h) - { - TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); - - for (auto b : *bl) - { - spells::BonusCaster caster(h, b); - - const CSpell * spell = SpellID(b->subtype).toSpell(); - - spells::BattleCast parameters(gs->curB, &caster, spells::Mode::PASSIVE, spell); - parameters.setSpellLevel(3); - parameters.setEffectDuration(b->val); - parameters.massive = true; - parameters.castIfPossible(spellEnv, spells::Target()); - } - } - } - // it is possible that due to opening spells one side was eliminated -> check for end of battle - checkBattleStateChanges(); - - bool firstRound = true;//FIXME: why first round is -1? - - //main loop - while ((lobby->state != EServerState::SHUTDOWN) && !battleResult.get()) //till the end of the battle ;] - { - BattleNextRound bnr; - bnr.round = gs->curB->round + 1; - logGlobal->debug("Round %d", bnr.round); - sendAndApply(&bnr); - - auto obstacles = gs->curB->obstacles; //we copy container, because we're going to modify it - for (auto &obstPtr : obstacles) - { - if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) - if (sco->turnsRemaining == 0) - removeObstacle(*obstPtr); - } - - const BattleInfo & curB = *gs->curB; - - for(auto stack : curB.stacks) - { - if(stack->alive() && !firstRound) - stackEnchantedTrigger(stack); - } - - //stack loop - - auto getNextStack = [this]() -> const CStack * - { - if(battleResult.get()) - return nullptr; - - std::vector q; - gs->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" - - if(!q.empty()) - { - if(!q.front().empty()) - { - auto next = q.front().front(); - const auto stack = dynamic_cast(next); - - // regeneration takes place before everything else but only during first turn attempt in each round - // also works under blind and similar effects - if(stack && stack->alive() && !stack->waiting) - { - BattleTriggerEffect bte; - bte.stackID = stack->unitId(); - bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); - - const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); - if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) - bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); - - if(bte.val) // anything to heal - sendAndApply(&bte); - } - - if(next->willMove()) - return stack; - } - } - - return nullptr; - }; - - const CStack * next = nullptr; - while((lobby->state != EServerState::SHUTDOWN) && (next = getNextStack())) - { - BattleUnitsChanged removeGhosts; - for(auto stack : curB.stacks) - { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } - - if(!removeGhosts.changedStacks.empty()) - sendAndApply(&removeGhosts); - - // check for bad morale => freeze - int nextStackMorale = next->moraleVal(); - if(!next->hadMorale && !next->waited() && nextStackMorale < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - //unit loses its turn - empty freeze action - BattleAction ba; - ba.actionType = EActionType::BAD_MORALE; - ba.side = next->unitSide(); - ba.stackNumber = next->unitId(); - - makeAutomaticAction(next, ba); - continue; - } - } - - if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk - { - logGlobal->trace("Handle Berserk effect"); - std::pair attackInfo = curB.getNearestStack(next); - if (attackInfo.first != nullptr) - { - BattleAction attack; - attack.actionType = EActionType::WALK_AND_ATTACK; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - attack.aimToHex(attackInfo.second); - attack.aimToUnit(attackInfo.first); - - makeAutomaticAction(next, attack); - logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); - } - else - { - makeStackDoNothing(next); - logGlobal->trace("No target found"); - } - continue; - } - - const CGHeroInstance * curOwner = battleGetOwnerHero(next); - const int stackCreatureId = next->unitType()->getId(); - - if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) - { - BattleAction attack; - attack.actionType = EActionType::SHOOT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - //TODO: select target by priority - - const battle::Unit * target = nullptr; - - for(auto & elem : gs->curB->stacks) - { - if(elem->unitType()->getId() != CreatureID::CATAPULT - && elem->unitOwner() != next->unitOwner() - && elem->isValidTarget() - && gs->curB->battleCanShoot(next, elem->getPosition())) - { - target = elem; - break; - } - } - - if(target == nullptr) - { - makeStackDoNothing(next); - } - else - { - attack.aimToUnit(target); - makeAutomaticAction(next, attack); - } - continue; - } - - if (next->unitType()->getId() == CreatureID::CATAPULT) - { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); - - if (attackableBattleHexes.empty()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) - { - BattleAction attack; - attack.actionType = EActionType::CATAPULT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - makeAutomaticAction(next, attack); - continue; - } - } - - if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) - { - TStacks possibleStacks = battleGetStacksIf([=](const CStack * s) - { - return s->unitOwner() == next->unitOwner() && s->canBeHealed(); - }); - - if (!possibleStacks.size()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) - { - RandomGeneratorUtil::randomShuffle(possibleStacks, getRandomGenerator()); - const CStack * toBeHealed = possibleStacks.front(); - - BattleAction heal; - heal.actionType = EActionType::STACK_HEAL; - heal.aimToUnit(toBeHealed); - heal.side = next->unitSide(); - heal.stackNumber = next->unitId(); - - makeAutomaticAction(next, heal); - continue; - } - } - - int numberOfAsks = 1; - bool breakOuter = false; - do - {//ask interface and wait for answer - if (!battleResult.get()) - { - stackTurnTrigger(next); //various effects - - if(next->fear) - { - makeStackDoNothing(next); //end immediately if stack was affected by fear - } - else - { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - sendAndApply(&sas); - - auto actionWasMade = [&]() -> bool - { - if (battleMadeAction.data)//active stack has made its action - return true; - if (battleResult.get())// battle is finished - return true; - if (next == nullptr)//active stack was been removed - return true; - return !next->alive();//active stack is dead - }; - - boost::unique_lock lock(battleMadeAction.mx); - battleMadeAction.data = false; - while ((lobby->state != EServerState::SHUTDOWN) && !actionWasMade()) - { - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - battleMadeAction.cond.wait(lock); - } - if (battleGetStackByID(nextId, false) != next) - next = nullptr; //it may be removed, while we wait - } - } - } - - if (battleResult.get()) //don't touch it, battle could be finished while waiting got action - { - breakOuter = true; - break; - } - //we're after action, all results applied - checkBattleStateChanges(); //check if this action ended the battle - - if(next != nullptr) - { - //check for good morale - nextStackMorale = next->moraleVal(); - if( !battleResult.get() - && !next->hadMorale - && !next->defending - && !next->waited() - && !next->fear - && next->alive() - && nextStackMorale > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); - - if(diceSize.size() > 0 && getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - BattleTriggerEffect bte; - bte.stackID = next->unitId(); - bte.effect = vstd::to_underlying(BonusType::MORALE); - bte.val = 1; - bte.additionalInfo = 0; - sendAndApply(&bte); //play animation - - ++numberOfAsks; //move this stack once more - } - } - } - --numberOfAsks; - } while (numberOfAsks > 0); - - if (breakOuter) - { - break; - } - - } - firstRound = false; - } - - if (lobby->state != EServerState::SHUTDOWN) - endBattle(gs->curB->tile, gs->curB->battleGetFightingHero(0), gs->curB->battleGetFightingHero(1)); -} - -bool CGameHandler::makeAutomaticAction(const CStack *stack, BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - BattleSetActiveStack bsa; - bsa.stack = stack->unitId(); - bsa.askPlayerInterface = false; - sendAndApply(&bsa); - - bool ret = makeBattleAction(ba); - checkBattleStateChanges(); - return ret; -} - bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) { assert(a->artType); @@ -6541,22 +4037,6 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact return false; } -void CGameHandler::setBattleResult(BattleResult::EResult resultType, int victoriusSide) -{ - boost::unique_lock guard(battleResult.mx); - if (battleResult.data) - { - complain((boost::format("The battle result has been already set (to %d, asked to %d)") - % battleResult.data->result % resultType).str()); - return; - } - auto br = new BattleResult(); - br->result = resultType; - br->winner = victoriusSide; //surrendering side loses - gs->curB->calculateCasualties(br->casualties); - battleResult.data = br; -} - void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { std::vector::iterator tile; @@ -6584,13 +4064,6 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) } } -void CGameHandler::removeObstacle(const CObstacleInstance & obstacle) -{ - BattleObstaclesChanged obsRem; - obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); - sendAndApply(&obsRem); -} - void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; @@ -6706,167 +4179,6 @@ void CGameHandler::showInfoDialog(const std::string & msg, PlayerColor player) showInfoDialog(&iw); } -CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): - army(battleSide.armyObject) -{ - heroWithDeadCommander = ObjectInstanceID(); - - PlayerColor color = battleSide.color; - - for(CStack * st : bat->stacks) - { - if(st->summoned) //don't take into account temporary summoned stacks - continue; - if(st->unitOwner() != color) //remove only our stacks - continue; - - logGlobal->debug("Calculating casualties for %s", st->nodeName()); - - st->health.takeResurrected(); - - if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) - { - logGlobal->debug("Ignored arrow towers stack."); - } - else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) - { - auto warMachine = st->unitType()->warMachine; - - if(warMachine == ArtifactID::NONE) - { - logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); - } - //catapult artifact remain even if "creature" killed in siege - else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) - { - logGlobal->debug("War machine has been destroyed"); - auto hero = dynamic_ptr_cast (army); - if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); - else - logGlobal->error("War machine in army without hero"); - } - } - else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) - { - if(st->alive() && st->getCount() > 0) - { - logGlobal->debug("Permanently summoned %d units.", st->getCount()); - const CreatureID summonedType = st->creatureId(); - summoned[summonedType] += st->getCount(); - } - } - else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - if (nullptr == st->base) - { - logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); - } - else - { - auto c = dynamic_cast (st->base); - if(c) - { - auto h = dynamic_cast (army); - if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) - { - logGlobal->debug("Commander is dead."); - heroWithDeadCommander = army->id; //TODO: unify commander handling - } - } - else - logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); - } - } - else if(st->base && !army->slotEmpty(st->unitSlot())) - { - logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); - if(st->getCount() == 0 || !st->alive()) - { - logGlobal->debug("Stack has been destroyed."); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); - } - else if(st->getCount() < army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - else if(st->getCount() > army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - } - else - { - logGlobal->warn("Unable to process stack: %s", st->nodeName()); - } - } -} - -void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) -{ - for (TStackAndItsNewCount &ncount : newStackCounts) - { - if (ncount.second > 0) - gh->changeStackCount(ncount.first, ncount.second, true); - else - gh->eraseStack(ncount.first, true); - } - for (auto summoned_iter : summoned) - { - SlotID slot = army->getSlotFor(summoned_iter.first); - if (slot.validSlot()) - { - StackLocation location(army, slot); - gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); - } - else - { - //even if it will be possible to summon anything permanently it should be checked for free slot - //necromancy is handled separately - gh->complain("No free slot to put summoned creature"); - } - } - for (auto al : removedWarMachines) - { - gh->removeArtifact(al); - } - if (heroWithDeadCommander != ObjectInstanceID()) - { - SetCommanderProperty scp; - scp.heroid = heroWithDeadCommander; - scp.which = SetCommanderProperty::ALIVE; - scp.amount = 0; - gh->sendAndApply(&scp); - } -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount) -{ - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; - winnerSide = result.winner; - remainingBattleQueriesCount = RemainingBattleQueriesCount; -} - -CGameHandler::FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; - winnerSide = 0; - remainingBattleQueriesCount = 0; -} - CRandomGenerator & CGameHandler::getRandomGenerator() { return CRandomGenerator::getDefault(); @@ -6898,5 +4210,21 @@ void CGameHandler::deserializationFix() //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization // restore any places that requires such pointer manually heroPool->gameHandler = this; + battles->gameHandler = this; playerMessages->gameHandler = this; } + +void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) +{ + battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank ) +{ + battles->startBattleI(army1, army2, tile, creatureBank); +} + +void CGameHandler::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank ) +{ + battles->startBattleI(army1, army2, creatureBank); +} diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 49a6dfcd6..7bb8a3ced 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -11,28 +11,15 @@ #include -#include "../lib/FunctionList.h" #include "../lib/IGameCallback.h" #include "../lib/battle/CBattleInfoCallback.h" -#include "../lib/battle/BattleAction.h" #include "../lib/ScriptHandler.h" #include "CQuery.h" VCMI_LIB_NAMESPACE_BEGIN -class CGameState; -struct StartInfo; -struct BattleResult; struct SideInBattle; -struct BattleAttack; -struct BattleStackAttacked; -struct CPack; -struct Query; -struct SetResources; -struct NewStructures; -class CGHeroInstance; class IMarket; - class SpellCastEnvironment; #if SCRIPTING_ENABLED @@ -42,16 +29,15 @@ namespace scripting } #endif - template class CApplier; VCMI_LIB_NAMESPACE_END class HeroPoolProcessor; -class CGameHandler; class CVCMIServer; class CBaseForGHApply; class PlayerMessageProcessor; +class BattleProcessor; struct PlayerStatus { @@ -80,33 +66,17 @@ public: } }; -struct CasualtiesAfterBattle -{ - using TStackAndItsNewCount = std::pair; - using TSummoned = std::map; - enum {ERASE = -1}; - const CArmedInstance * army; - std::vector newStackCounts; - std::vector removedWarMachines; - TSummoned summoned; - ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - - CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); - void updateArmy(CGameHandler *gh); -}; - class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment { CVCMIServer * lobby; std::shared_ptr> applier; - std::unique_ptr battleThread; public: - boost::recursive_mutex battleActionMutex; + using CCallbackBase::setBattle; std::unique_ptr heroPool; + std::unique_ptr battles; - using FireShieldInfo = std::vector>; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; @@ -135,26 +105,6 @@ public: bool isBlockedByQueries(const CPack *pack, PlayerColor player); bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); - int moveStack(int stack, BattleHex dest); //returned value - travelled distance - void runBattle(); - - ////used only in endBattle - don't touch elsewhere - bool visitObjectAfterVictory; - // - void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle - void endBattleConfirm(const BattleInfo * battleInfo); - - void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - - // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); - - void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); - void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); - - void checkBattleStateChanges(); - void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - void setBattleResult(BattleResult::EResult resultType, int victoriusSide); CGameHandler() = default; CGameHandler(CVCMIServer * lobby); @@ -242,14 +192,6 @@ public: PlayerColor getPlayerAt(std::shared_ptr c) const; bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; - void updateGateState(); - bool makeBattleAction(BattleAction &ba); - bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) - bool makeCustomAction(BattleAction &ba); - void stackEnchantedTrigger(const CStack * stack); - void stackTurnTrigger(const CStack *stack); - - void removeObstacle(const CObstacleInstance &obstacle); bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, ui8 formation ); @@ -284,7 +226,6 @@ public: bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ); void objectVisitEnded(const CObjectVisitQuery &query); - void engageIntoBattle( PlayerColor player ); bool dig(const CGHeroInstance *h); void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); @@ -292,7 +233,7 @@ public: { h & QID; h & states; - h & finishingBattle; + h & battles; h & heroPool; h & getRandomGenerator(); h & playerMessages; @@ -324,39 +265,8 @@ public: void throwAndComplain(CPackForServer * pack, std::string txt); bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - struct FinishingBattleHelper - { - FinishingBattleHelper(); - FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); - - inline bool isDraw() const {return winnerSide == 2;} - - const CGHeroInstance *winnerHero, *loserHero; - PlayerColor victor, loser; - ui8 winnerSide; - - int remainingBattleQueriesCount; - - template void serialize(Handler &h, const int version) - { - h & winnerHero; - h & loserHero; - h & victor; - h & loser; - h & winnerSide; - h & remainingBattleQueriesCount; - } - }; - - std::unique_ptr finishingBattle; - - void battleAfterLevelUp(const BattleResult &result); - void run(bool resume); void newTurn(); - void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); void spawnWanderingMonsters(CreatureID creatureID); @@ -384,8 +294,6 @@ private: void reinitScripting(); void deserializationFix(); - - void makeStackDoNothing(const CStack * next); void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; const std::string complainNoCreatures; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 4239d07ba..ab28bebb5 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,6 +1,7 @@ set(server_SRCS StdInc.cpp + BattleProcessor.cpp CGameHandler.cpp HeroPoolProcessor.cpp PlayerMessageProcessor.cpp @@ -14,6 +15,7 @@ set(server_SRCS set(server_HEADERS StdInc.h + BattleProcessor.h CGameHandler.h HeroPoolProcessor.h PlayerMessageProcessor.h diff --git a/server/CQuery.cpp b/server/CQuery.cpp index 1d0a7d7b3..bada30f93 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CQuery.h" #include "CGameHandler.h" +#include "BattleProcessor.h" #include "../lib/battle/BattleInfo.h" #include "../lib/mapObjects/MiscObjects.h" #include "../lib/serializer/Cast.h" @@ -326,7 +327,7 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { if(result) - gh->battleAfterLevelUp(*result); + gh->battles->battleAfterLevelUp(*result); } void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const @@ -418,7 +419,7 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) } else { - gh->endBattleConfirm(bi); + gh->battles->endBattleConfirm(bi); } } diff --git a/server/CQuery.h b/server/CQuery.h index 089e1c256..1a6de086d 100644 --- a/server/CQuery.h +++ b/server/CQuery.h @@ -8,13 +8,13 @@ * */ #pragma once + #include "../lib/GameConstants.h" -#include "../lib/int3.h" #include "../lib/NetPacks.h" -#include "JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; class CGObjectInstance; class CGHeroInstance; class CArmedInstance; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 3027242c0..a08b85c96 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -11,6 +11,7 @@ #include "ServerNetPackVisitors.h" #include "CGameHandler.h" +#include "BattleProcessor.h" #include "HeroPoolProcessor.h" #include "PlayerMessageProcessor.h" @@ -280,8 +281,6 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { - boost::unique_lock lock(gh.battleActionMutex); - const BattleInfo * b = gs.curB; if(!b) gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); @@ -304,13 +303,11 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); } - result = gh.makeBattleAction(pack.ba); + result = gh.battles->makeBattleAction(pack.ba); } void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) { - boost::unique_lock lock(gh.battleActionMutex); - const BattleInfo * b = gs.curB; if(!b) gh.throwNotAllowedAction(&pack); @@ -325,7 +322,7 @@ void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) if(pack.ba.actionType != EActionType::HERO_SPELL) gh.throwNotAllowedAction(&pack); - result = gh.makeCustomAction(pack.ba); + result = gh.battles->makeCustomAction(pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) From c217d7717a1e65acdd1c306e95551356000d2544 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 00:10:01 +0300 Subject: [PATCH 0102/1248] server queries is now stored as unique_ptr --- server/BattleProcessor.cpp | 14 +++++----- server/CGameHandler.cpp | 39 ++++++++++++++------------- server/CGameHandler.h | 16 +++++++++-- server/CQuery.cpp | 2 +- server/NetPacksServer.cpp | 3 ++- server/ServerSpellCastEnvironment.cpp | 5 ++-- 6 files changed, 47 insertions(+), 32 deletions(-) diff --git a/server/BattleProcessor.cpp b/server/BattleProcessor.cpp index 44b178c6c..288d73f18 100644 --- a/server/BattleProcessor.cpp +++ b/server/BattleProcessor.cpp @@ -381,7 +381,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; } } - gameHandler->queries.addQuery(nextBattleQuery); + gameHandler->queries->addQuery(nextBattleQuery); this->battleThread = std::make_unique(boost::thread(&BattleProcessor::runBattle, this)); } @@ -1384,7 +1384,7 @@ void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, if(heroDefender) battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(gameHandler->gameState()->curB->sides[0].color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); @@ -1395,7 +1395,7 @@ void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, battleQuery->result = std::make_optional(*battleResult.data); //Check how many battle gameHandler->queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries.allQueries(), battleQuery) : 0; + const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; finishingBattle = std::make_unique(battleQuery, queriedPlayers); // in battles against neutrals, 1st player can ask to replay battle manually @@ -1403,13 +1403,13 @@ void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, { auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); battleResult.data->queryID = battleDialogQuery->queryID; - gameHandler->queries.addQuery(battleDialogQuery); + gameHandler->queries->addQuery(battleDialogQuery); } else battleResult.data->queryID = -1; //set same battle result for all gameHandler->queries - for(auto q : gameHandler->queries.allQueries()) + for(auto q : gameHandler->queries->allQueries()) { auto otherBattleQuery = std::dynamic_pointer_cast(q); if(otherBattleQuery) @@ -1424,7 +1424,7 @@ void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) { - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(battleInfo->sides.at(0).color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battleInfo->sides.at(0).color)); if(!battleQuery) { logGlobal->trace("No battle query, battle end was confirmed by another player"); @@ -1619,7 +1619,7 @@ void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) raccepted.winnerSide = finishingBattle->winnerSide; gameHandler->sendAndApply(&raccepted); - gameHandler->queries.popIfTop(battleQuery); + gameHandler->queries->popIfTop(battleQuery); //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4c69714d9..4a15c1844 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -16,6 +16,7 @@ #include "PlayerMessageProcessor.h" #include "ServerNetPackVisitors.h" #include "ServerSpellCastEnvironment.h" +#include "CQuery.h" #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" @@ -255,7 +256,7 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) { auto levelUpQuery = std::make_shared(this, hlu, hero); hlu.queryID = levelUpQuery->queryID; - queries.addQuery(levelUpQuery); + queries->addQuery(levelUpQuery); sendAndApply(&hlu); //level up will be called on query reply } @@ -396,7 +397,7 @@ void CGameHandler::levelUpCommander(const CCommanderInstance * c) { auto commanderLevelUp = std::make_shared(this, clu, hero); clu.queryID = commanderLevelUp->queryID; - queries.addQuery(commanderLevelUp); + queries->addQuery(commanderLevelUp); sendAndApply(&clu); } } @@ -539,11 +540,11 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) vstd::clear_pointer(pack); } - CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) , battles(std::make_unique(this)) + , queries(std::make_unique()) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") @@ -1219,7 +1220,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo LOG_TRACE_PARAMS(logGlobal, "Hero %s starts movement from %s to %s", h->getNameTranslated() % tmh.start.toString() % tmh.end.toString()); auto moveQuery = std::make_shared(this, tmh, h); - queries.addQuery(moveQuery); + queries->addQuery(moveQuery); if (leavingTile == LEAVING_TILE) leaveTile(); @@ -1246,7 +1247,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - queries.popIfTop(moveQuery); + queries->popIfTop(moveQuery); logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; }; @@ -1408,7 +1409,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne void CGameHandler::showBlockingDialog(BlockingDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -1416,7 +1417,7 @@ void CGameHandler::showBlockingDialog(BlockingDialog *iw) void CGameHandler::showTeleportDialog(TeleportDialog *iw) { auto dialogQuery = std::make_shared(this, *iw); - queries.addQuery(dialogQuery); + queries->addQuery(dialogQuery); iw->queryID = dialogQuery->queryID; sendToAllClients(iw); } @@ -1685,7 +1686,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) sendAndApply(&hex); useScholarSkill(hero1,hero2); - queries.addQuery(exchange); + queries->addQuery(exchange); } } @@ -3186,13 +3187,13 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); logGlobal->trace(answer.toJson()); - auto topQuery = queries.topQuery(player); + auto topQuery = queries->topQuery(player); COMPLAIN_RET_FALSE_IF(!topQuery, "This player doesn't have any queries!"); if(topQuery->queryID != qid) { - auto currentQuery = queries.getQuery(qid); + auto currentQuery = queries->getQuery(qid); if(currentQuery != nullptr && currentQuery->endsByPlayerAnswer()) currentQuery->setReply(answer); @@ -3202,7 +3203,7 @@ bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor COMPLAIN_RET_FALSE_IF(!topQuery->endsByPlayerAnswer(), "This query cannot be ended by player's answer!"); topQuery->setReply(answer); - queries.popQuery(topQuery); + queries->popQuery(topQuery); return true; } @@ -3365,7 +3366,7 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h assert(upperArmy); auto garrisonQuery = std::make_shared(this, upperArmy, lowerArmy); - queries.addQuery(garrisonQuery); + queries->addQuery(garrisonQuery); GarrisonDialog gd; gd.hid = hid; @@ -3419,10 +3420,10 @@ bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) } //Ongoing garrison exchange - usually picking from top garison (from o1 to o2), but who knows - auto dialog = std::dynamic_pointer_cast(queries.topQuery(o1->tempOwner)); + auto dialog = std::dynamic_pointer_cast(queries->topQuery(o1->tempOwner)); if (!dialog) { - dialog = std::dynamic_pointer_cast(queries.topQuery(o2->tempOwner)); + dialog = std::dynamic_pointer_cast(queries->topQuery(o2->tempOwner)); } if (dialog) { @@ -3463,7 +3464,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta } } visitQuery = std::make_shared(this, visitedObject, h, visitedObject->visitablePos()); - queries.addQuery(visitQuery); //TODO real visit pos + queries->addQuery(visitQuery); //TODO real visit pos HeroVisit hv; hv.objId = obj->id; @@ -3478,7 +3479,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta ObjectVisitStarted::defaultExecute(serverEventBus.get(), startVisit, h->tempOwner, h->id, obj->id); if(visitQuery) - queries.popIfTop(visitQuery); //visit ends here if no queries were created + queries->popIfTop(visitQuery); //visit ends here if no queries were created } void CGameHandler::objectVisitEnded(const CObjectVisitQuery & query) @@ -4084,7 +4085,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) if (!strcmp(typeid(*pack).name(), typeid(PlayerMessage).name())) return false; - auto query = queries.topQuery(player); + auto query = queries->topQuery(player); if (query && query->blocksPack(pack)) { complain(boost::str(boost::format( @@ -4101,7 +4102,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) void CGameHandler::removeAfterVisit(const CGObjectInstance *object) { //If the object is being visited, there must be a matching query - for (const auto &query : queries.allQueries()) + for (const auto &query : queries->allQueries()) { if (auto someVistQuery = std::dynamic_pointer_cast(query)) { @@ -4150,7 +4151,7 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { - if (auto topQuery = queries.topQuery(hero->getOwner())) + if (auto topQuery = queries->topQuery(hero->getOwner())) if (auto visit = std::dynamic_pointer_cast(topQuery)) return !(visit->visitedObject == obj && visit->visitingHero == hero); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 7bb8a3ced..fd5c44677 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -14,13 +14,22 @@ #include "../lib/IGameCallback.h" #include "../lib/battle/CBattleInfoCallback.h" #include "../lib/ScriptHandler.h" -#include "CQuery.h" VCMI_LIB_NAMESPACE_BEGIN struct SideInBattle; class IMarket; class SpellCastEnvironment; +class CConnection; +class CCommanderInstance; +class EVictoryLossCheckResult; + +struct CPack; +struct CPackForServer; +struct NewTurn; +struct CGarrisonOperationPack; +struct SetResources; +struct NewStructures; #if SCRIPTING_ENABLED namespace scripting @@ -38,6 +47,8 @@ class CVCMIServer; class CBaseForGHApply; class PlayerMessageProcessor; class BattleProcessor; +class Queries; +class CObjectVisitQuery; struct PlayerStatus { @@ -76,6 +87,7 @@ public: std::unique_ptr heroPool; std::unique_ptr battles; + std::unique_ptr queries; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; @@ -90,7 +102,7 @@ public: //queries stuff boost::recursive_mutex gsm; ui32 QID; - Queries queries; + SpellCastEnvironment * spellEnv; diff --git a/server/CQuery.cpp b/server/CQuery.cpp index bada30f93..55f93f46f 100644 --- a/server/CQuery.cpp +++ b/server/CQuery.cpp @@ -143,7 +143,7 @@ bool CQuery::blockAllButReply(const CPack * pack) const } CGhQuery::CGhQuery(CGameHandler * owner): - CQuery(&owner->queries), gh(owner) + CQuery(owner->queries.get()), gh(owner) { } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index a08b85c96..785829851 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -12,6 +12,7 @@ #include "CGameHandler.h" #include "BattleProcessor.h" +#include "CQuery.h" #include "HeroPoolProcessor.h" #include "PlayerMessageProcessor.h" @@ -48,7 +49,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) } gh.throwOnWrongPlayer(&pack, pack.player); - if(gh.queries.topQuery(pack.player)) + if(gh.queries->topQuery(pack.player)) gh.throwAndComplain(&pack, "Cannot end turn before resolving queries!"); gh.states.setFlag(gs.currentPlayer, &PlayerStatus::makingTurn, false); diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 7837ecba0..70882ecad 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "../lib/gameState/CGameState.h" +#include "CQuery.h" #include "CGameHandler.h" #include "ServerSpellCastEnvironment.h" @@ -90,8 +91,8 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function callback) { - auto query = std::make_shared(&gh->queries, color, callback); + auto query = std::make_shared(gh->queries.get(), color, callback); request->queryID = query->queryID; - gh->queries.addQuery(query); + gh->queries->addQuery(query); gh->sendAndApply(request); } From 629ca3f13ec9f3b5ce6e1de2bfbff4b0117effc8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 00:20:35 +0300 Subject: [PATCH 0103/1248] Created directory structure for server files --- server/CGameHandler.cpp | 8 +-- server/CMakeLists.txt | 22 ++++++--- server/CVCMIServer.cpp | 2 +- server/NetPacksServer.cpp | 8 +-- server/ServerSpellCastEnvironment.cpp | 8 +-- server/{ => battles}/BattleProcessor.cpp | 49 +++++++++---------- server/{ => battles}/BattleProcessor.h | 4 +- server/{ => processors}/HeroPoolProcessor.cpp | 20 ++++---- server/{ => processors}/HeroPoolProcessor.h | 0 .../PlayerMessageProcessor.cpp | 23 ++++----- .../{ => processors}/PlayerMessageProcessor.h | 2 +- server/{ => queries}/CQuery.cpp | 12 +++-- server/{ => queries}/CQuery.h | 4 +- 13 files changed, 86 insertions(+), 76 deletions(-) rename server/{ => battles}/BattleProcessor.cpp (99%) rename server/{ => battles}/BattleProcessor.h (98%) rename server/{ => processors}/HeroPoolProcessor.cpp (96%) rename server/{ => processors}/HeroPoolProcessor.h (100%) rename server/{ => processors}/PlayerMessageProcessor.cpp (97%) rename server/{ => processors}/PlayerMessageProcessor.h (98%) rename server/{ => queries}/CQuery.cpp (94%) rename server/{ => queries}/CQuery.h (96%) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4a15c1844..36d5ab062 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -10,13 +10,13 @@ #include "StdInc.h" #include "CGameHandler.h" -#include "BattleProcessor.h" #include "CVCMIServer.h" -#include "HeroPoolProcessor.h" -#include "PlayerMessageProcessor.h" #include "ServerNetPackVisitors.h" #include "ServerSpellCastEnvironment.h" -#include "CQuery.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "queries/CQuery.h" #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index ab28bebb5..26050e6f6 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,12 +1,15 @@ set(server_SRCS StdInc.cpp - BattleProcessor.cpp + battles/BattleProcessor.cpp + + queries/CQuery.cpp + + processors/HeroPoolProcessor.cpp + processors/PlayerMessageProcessor.cpp + CGameHandler.cpp - HeroPoolProcessor.cpp - PlayerMessageProcessor.cpp ServerSpellCastEnvironment.cpp - CQuery.cpp CVCMIServer.cpp NetPacksServer.cpp NetPacksLobbyServer.cpp @@ -15,12 +18,15 @@ set(server_SRCS set(server_HEADERS StdInc.h - BattleProcessor.h + battles/BattleProcessor.h + + queries/CQuery.h + + processors/HeroPoolProcessor.h + processors/PlayerMessageProcessor.h + CGameHandler.h - HeroPoolProcessor.h - PlayerMessageProcessor.h ServerSpellCastEnvironment.h - CQuery.h CVCMIServer.h LobbyNetPackVisitors.h ServerNetPackVisitors.h diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index a7a75f039..83a711fe9 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -36,7 +36,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/VCMIDirs.h" #include "CGameHandler.h" -#include "PlayerMessageProcessor.h" +#include "processors/PlayerMessageProcessor.h" #include "../lib/mapping/CMapInfo.h" #include "../lib/GameConstants.h" #include "../lib/logging/CBasicLogConfigurator.h" diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 785829851..b46d82a5b 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -11,10 +11,10 @@ #include "ServerNetPackVisitors.h" #include "CGameHandler.h" -#include "BattleProcessor.h" -#include "CQuery.h" -#include "HeroPoolProcessor.h" -#include "PlayerMessageProcessor.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "queries/CQuery.h" #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 70882ecad..f61007795 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -8,11 +8,13 @@ * */ #include "StdInc.h" -#include "../lib/gameState/CGameState.h" -#include "CQuery.h" -#include "CGameHandler.h" #include "ServerSpellCastEnvironment.h" +#include "CGameHandler.h" +#include "queries/CQuery.h" + +#include "../lib/gameState/CGameState.h" + ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) : gh(gh) diff --git a/server/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp similarity index 99% rename from server/BattleProcessor.cpp rename to server/battles/BattleProcessor.cpp index 288d73f18..f09a2f663 100644 --- a/server/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -10,32 +10,31 @@ #include "StdInc.h" #include "BattleProcessor.h" -#include "CGameHandler.h" -#include "CQuery.h" -#include "CVCMIServer.h" -#include "HeroPoolProcessor.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/CQuery.h" -#include "../lib/ArtifactUtils.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CStack.h" -#include "../lib/CondSh.h" -#include "../lib/GameSettings.h" -#include "../lib/ScopeGuard.h" -#include "../lib/TerrainHandler.h" -#include "../lib/UnlockGuard.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/battle/CUnitState.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/mapping/CMap.h" -#include "../lib/modding/IdentifierStorage.h" -#include "../lib/serializer/Cast.h" -#include "../lib/spells/AbilityCaster.h" -#include "../lib/spells/BonusCaster.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/spells/ObstacleCasterProxy.h" -#include "../lib/spells/Problem.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CModHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/GameSettings.h" +#include "../../lib/ScopeGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CUnitState.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/serializer/Cast.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/spells/Problem.h" #define COMPLAIN_RET_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return;}} while(0) #define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return false;}} while(0) diff --git a/server/BattleProcessor.h b/server/battles/BattleProcessor.h similarity index 98% rename from server/BattleProcessor.h rename to server/battles/BattleProcessor.h index f0e653601..8a2ff8950 100644 --- a/server/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -9,8 +9,8 @@ */ #pragma once -#include "../lib/GameConstants.h" -#include "../lib/NetPacks.h" +#include "../../lib/GameConstants.h" +#include "../../lib/NetPacks.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/server/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp similarity index 96% rename from server/HeroPoolProcessor.cpp rename to server/processors/HeroPoolProcessor.cpp index f32a88be0..180234388 100644 --- a/server/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -10,17 +10,17 @@ #include "StdInc.h" #include "HeroPoolProcessor.h" -#include "CGameHandler.h" +#include "../CGameHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameSettings.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/gameState/TavernHeroesPool.h" -#include "../lib/gameState/TavernSlot.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameSettings.h" +#include "../../lib/NetPacks.h" +#include "../../lib/StartInfo.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/gameState/TavernHeroesPool.h" +#include "../../lib/gameState/TavernSlot.h" HeroPoolProcessor::HeroPoolProcessor() : gameHandler(nullptr) diff --git a/server/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h similarity index 100% rename from server/HeroPoolProcessor.h rename to server/processors/HeroPoolProcessor.h diff --git a/server/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp similarity index 97% rename from server/PlayerMessageProcessor.cpp rename to server/processors/PlayerMessageProcessor.cpp index 2736f68c9..c9c0f3fd3 100644 --- a/server/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -10,18 +10,19 @@ #include "StdInc.h" #include "PlayerMessageProcessor.h" -#include "CGameHandler.h" -#include "CVCMIServer.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" -#include "../lib/serializer/Connection.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameConstants.h" -#include "../lib/NetPacks.h" -#include "../lib/StartInfo.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/mapObjects/CGTownInstance.h" +#include "../../lib/serializer/Connection.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CModHandler.h" +#include "../../lib/CPlayerState.h" +#include "../../lib/GameConstants.h" +#include "../../lib/NetPacks.h" +#include "../../lib/StartInfo.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" #include "../lib/modding/IdentifierStorage.h" #include "../lib/modding/ModScope.h" diff --git a/server/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h similarity index 98% rename from server/PlayerMessageProcessor.h rename to server/processors/PlayerMessageProcessor.h index 2351140ca..4c0f725ab 100644 --- a/server/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/GameConstants.h" +#include "../../lib/GameConstants.h" VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; diff --git a/server/CQuery.cpp b/server/queries/CQuery.cpp similarity index 94% rename from server/CQuery.cpp rename to server/queries/CQuery.cpp index 55f93f46f..3e3c326aa 100644 --- a/server/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -9,11 +9,13 @@ */ #include "StdInc.h" #include "CQuery.h" -#include "CGameHandler.h" -#include "BattleProcessor.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/mapObjects/MiscObjects.h" -#include "../lib/serializer/Cast.h" + +#include "../CGameHandler.h" +#include "../battles/BattleProcessor.h" + +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/serializer/Cast.h" boost::mutex Queries::mx; diff --git a/server/CQuery.h b/server/queries/CQuery.h similarity index 96% rename from server/CQuery.h rename to server/queries/CQuery.h index 1a6de086d..54497e460 100644 --- a/server/CQuery.h +++ b/server/queries/CQuery.h @@ -9,8 +9,8 @@ */ #pragma once -#include "../lib/GameConstants.h" -#include "../lib/NetPacks.h" +#include "../../lib/GameConstants.h" +#include "../../lib/NetPacks.h" VCMI_LIB_NAMESPACE_BEGIN From 323772fc2ebd08466583b370e3d23a66850b700a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 24 Jul 2023 00:46:29 +0300 Subject: [PATCH 0104/1248] Split CQuery file into multiple files --- server/CGameHandler.cpp | 5 +- server/CGameHandler.h | 4 +- server/CMakeLists.txt | 6 + server/NetPacksServer.cpp | 2 +- server/ServerSpellCastEnvironment.cpp | 2 + server/battles/BattleProcessor.cpp | 11 +- server/processors/PlayerMessageProcessor.cpp | 2 +- server/queries/BattleQueries.cpp | 75 ++++ server/queries/BattleQueries.h | 40 ++ server/queries/CQuery.cpp | 398 +------------------ server/queries/CQuery.h | 155 +------- server/queries/MapQueries.cpp | 227 +++++++++++ server/queries/MapQueries.h | 103 +++++ server/queries/QueriesProcessor.cpp | 129 ++++++ server/queries/QueriesProcessor.h | 40 ++ 15 files changed, 647 insertions(+), 552 deletions(-) create mode 100644 server/queries/BattleQueries.cpp create mode 100644 server/queries/BattleQueries.h create mode 100644 server/queries/MapQueries.cpp create mode 100644 server/queries/MapQueries.h create mode 100644 server/queries/QueriesProcessor.cpp create mode 100644 server/queries/QueriesProcessor.h diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 36d5ab062..a32983423 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -16,7 +16,8 @@ #include "battles/BattleProcessor.h" #include "processors/HeroPoolProcessor.h" #include "processors/PlayerMessageProcessor.h" -#include "queries/CQuery.h" +#include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" #include "../lib/ArtifactUtils.h" #include "../lib/CArtHandler.h" @@ -544,7 +545,7 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) , battles(std::make_unique(this)) - , queries(std::make_unique()) + , queries(std::make_unique()) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") , complainNotEnoughCreatures("Cannot split that stack, not enough creatures!") diff --git a/server/CGameHandler.h b/server/CGameHandler.h index fd5c44677..1f2cc3637 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -47,7 +47,7 @@ class CVCMIServer; class CBaseForGHApply; class PlayerMessageProcessor; class BattleProcessor; -class Queries; +class QueriesProcessor; class CObjectVisitQuery; struct PlayerStatus @@ -87,7 +87,7 @@ public: std::unique_ptr heroPool; std::unique_ptr battles; - std::unique_ptr queries; + std::unique_ptr queries; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 26050e6f6..ed7881e32 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -3,7 +3,10 @@ set(server_SRCS battles/BattleProcessor.cpp + queries/BattleQueries.cpp queries/CQuery.cpp + queries/MapQueries.cpp + queries/QueriesProcessor.cpp processors/HeroPoolProcessor.cpp processors/PlayerMessageProcessor.cpp @@ -20,7 +23,10 @@ set(server_HEADERS battles/BattleProcessor.h + queries/BattleQueries.h queries/CQuery.h + queries/MapQueries.h + queries/QueriesProcessor.h processors/HeroPoolProcessor.h processors/PlayerMessageProcessor.h diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index b46d82a5b..3a5db6dcb 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -14,7 +14,7 @@ #include "battles/BattleProcessor.h" #include "processors/HeroPoolProcessor.h" #include "processors/PlayerMessageProcessor.h" -#include "queries/CQuery.h" +#include "queries/QueriesProcessor.h" #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index f61007795..4f0fe8cba 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -11,9 +11,11 @@ #include "ServerSpellCastEnvironment.h" #include "CGameHandler.h" +#include "queries/QueriesProcessor.h" #include "queries/CQuery.h" #include "../lib/gameState/CGameState.h" +#include "../lib/NetPacks.h" ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index f09a2f663..0b858213d 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -13,21 +13,23 @@ #include "../CGameHandler.h" #include "../CVCMIServer.h" #include "../processors/HeroPoolProcessor.h" -#include "../queries/CQuery.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CModHandler.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" #include "../../lib/GameSettings.h" #include "../../lib/ScopeGuard.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" #include "../../lib/battle/BattleInfo.h" #include "../../lib/battle/CUnitState.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapping/CMap.h" +#include "../../lib/modding/IdentifierStorage.h" #include "../../lib/serializer/Cast.h" #include "../../lib/spells/AbilityCaster.h" #include "../../lib/spells/BonusCaster.h" @@ -350,7 +352,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(gameHandler->gameState()->curB->sides[0].color)); + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); //existing battle query for retying auto-combat if(lastBattleQuery) @@ -2001,7 +2003,7 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co engageIntoBattle(bs.info->sides[0].color); engageIntoBattle(bs.info->sides[1].color); - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries.topQuery(bs.info->sides[0].color)); + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(bs.info->sides[0].color)); bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); gameHandler->sendAndApply(&bs); @@ -2020,7 +2022,6 @@ void BattleProcessor::checkBattleStateChanges() } } - bool BattleProcessor::makeBattleAction(BattleAction &ba) { bool ok = true; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index c9c0f3fd3..d077233c8 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -16,7 +16,7 @@ #include "../../lib/serializer/Connection.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/CModHandler.h" +#include "../../lib/modding/IdentifierStorage.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameConstants.h" #include "../../lib/NetPacks.h" diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp new file mode 100644 index 000000000..46d9ec7cb --- /dev/null +++ b/server/queries/BattleQueries.cpp @@ -0,0 +1,75 @@ +/* + * BattleQueries.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 "BattleQueries.h" +#include "MapQueries.h" + +#include "../CGameHandler.h" +#include "../battles/BattleProcessor.h" + +#include "../../lib/battle/BattleInfo.h" + +void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + if(result) + objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): + CGhQuery(owner) +{ + belligerents[0] = Bi->sides[0].armyObject; + belligerents[1] = Bi->sides[1].armyObject; + + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} + +CBattleQuery::CBattleQuery(CGameHandler * owner): + CGhQuery(owner), bi(nullptr) +{ + belligerents[0] = belligerents[1] = nullptr; +} + +bool CBattleQuery::blocksPack(const CPack * pack) const +{ + const char * name = typeid(*pack).name(); + return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name()); +} + +void CBattleQuery::onRemoval(PlayerColor color) +{ + if(result) + gh->battles->battleAfterLevelUp(*result); +} + +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): + CDialogQuery(owner) +{ + bi = Bi; + + for(auto & side : bi->sides) + addPlayer(side.color); +} + +void CBattleDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + if(*answer == 1) + { + gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); + } + else + { + gh->battles->endBattleConfirm(bi); + } +} diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h new file mode 100644 index 000000000..03fce57a7 --- /dev/null +++ b/server/queries/BattleQueries.h @@ -0,0 +1,40 @@ +/* + * BattleQueries.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 "CQuery.h" + +#include "../../lib/NetPacks.h" + +class CBattleQuery : public CGhQuery +{ +public: + std::array belligerents; + std::array initialHeroMana; + + const BattleInfo *bi; + std::optional result; + + CBattleQuery(CGameHandler * owner); + CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; +}; + +class CBattleDialogQuery : public CDialogQuery +{ +public: + CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); + + const BattleInfo * bi; + + virtual void onRemoval(PlayerColor color) override; +}; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index 3e3c326aa..b8c1e072c 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -10,14 +10,12 @@ #include "StdInc.h" #include "CQuery.h" +#include "QueriesProcessor.h" + #include "../CGameHandler.h" -#include "../battles/BattleProcessor.h" -#include "../../lib/battle/BattleInfo.h" -#include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/serializer/Cast.h" - -boost::mutex Queries::mx; +#include "../../lib/NetPacks.h" template std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") @@ -47,10 +45,10 @@ std::ostream & operator<<(std::ostream & out, QueryPtr query) return out << "[" << query.get() << "] " << query->toString(); } -CQuery::CQuery(Queries * Owner): +CQuery::CQuery(QueriesProcessor * Owner): owner(Owner) { - boost::unique_lock l(Queries::mx); + boost::unique_lock l(QueriesProcessor::mx); static QueryID QID = QueryID(0); @@ -58,7 +56,6 @@ CQuery::CQuery(Queries * Owner): logGlobal->trace("Created a new query with id %d", queryID); } - CQuery::~CQuery() { logGlobal->trace("Destructed the query with id %d", queryID); @@ -150,349 +147,6 @@ CGhQuery::CGhQuery(CGameHandler * owner): } -CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): - CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) -{ - addPlayer(Hero->tempOwner); -} - -bool CObjectVisitQuery::blocksPack(const CPack *pack) const -{ - //During the visit itself ALL actions are blocked. - //(However, the visit may trigger a query above that'll pass some.) - return true; -} - -void CObjectVisitQuery::onRemoval(PlayerColor color) -{ - gh->objectVisitEnded(*this); - - //TODO or should it be destructor? - //Can object visit affect 2 players and what would be desired behavior? - if(removeObjectAfterVisit) - gh->removeObject(visitedObject); -} - -void CObjectVisitQuery::onExposure(QueryPtr topQuery) -{ - //Object may have been removed and deleted. - if(gh->isValidObject(visitedObject)) - topQuery->notifyObjectAboutRemoval(*this); - - owner->popIfTop(*this); -} - -void Queries::popQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); - if(topQuery(player) != query) - { - logGlobal->trace("Cannot remove, not a top!"); - return; - } - - queries[player] -= query; - auto nextQuery = topQuery(player); - - query->onRemoval(player); - - //Exposure on query below happens only if removal didn't trigger any new query - if(nextQuery && nextQuery == topQuery(player)) - nextQuery->onExposure(query); -} - -void Queries::popQuery(const CQuery &query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); - - assert(query.players.size()); - for(auto player : query.players) - { - auto top = topQuery(player); - if(top.get() == &query) - popQuery(top); - else - { - logGlobal->trace("Cannot remove query %s", query.toString()); - logGlobal->trace("Queries found:"); - for(auto q : queries[player]) - { - logGlobal->trace(q->toString()); - } - } - } -} - -void Queries::popQuery(QueryPtr query) -{ - for(auto player : query->players) - popQuery(player, query); -} - -void Queries::addQuery(QueryPtr query) -{ - for(auto player : query->players) - addQuery(player, query); - - for(auto player : query->players) - query->onAdded(player); -} - -void Queries::addQuery(PlayerColor player, QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); - query->onAdding(player); - queries[player].push_back(query); -} - -QueryPtr Queries::topQuery(PlayerColor player) -{ - return vstd::backOrNull(queries[player]); -} - -void Queries::popIfTop(QueryPtr query) -{ - LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); - if(!query) - logGlobal->error("The query is nullptr! Ignoring."); - - popIfTop(*query); -} - -void Queries::popIfTop(const CQuery & query) -{ - for(PlayerColor color : query.players) - if(topQuery(color).get() == &query) - popQuery(color, topQuery(color)); -} - -std::vector> Queries::allQueries() const -{ - std::vector> ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -std::vector Queries::allQueries() -{ - //TODO code duplication with const function :( - std::vector ret; - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - ret.push_back(query); - - return ret; -} - -QueryPtr Queries::getQuery(QueryID queryID) -{ - for(auto & playerQueries : queries) - for(auto & query : playerQueries.second) - if(query->queryID == queryID) - return query; - return nullptr; -} - -void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - if(result) - objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): - CGhQuery(owner) -{ - belligerents[0] = Bi->sides[0].armyObject; - belligerents[1] = Bi->sides[1].armyObject; - - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -CBattleQuery::CBattleQuery(CGameHandler * owner): - CGhQuery(owner), bi(nullptr) -{ - belligerents[0] = belligerents[1] = nullptr; -} - -bool CBattleQuery::blocksPack(const CPack * pack) const -{ - const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name()); -} - -void CBattleQuery::onRemoval(PlayerColor color) -{ - if(result) - gh->battles->battleAfterLevelUp(*result); -} - -void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); -} - -CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): - CDialogQuery(owner) -{ - exchangingArmies[0] = up; - exchangingArmies[1] = down; - - addPlayer(up->tempOwner); - addPlayer(down->tempOwner); -} - -bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const -{ - std::set ourIds; - ourIds.insert(this->exchangingArmies[0]->id); - ourIds.insert(this->exchangingArmies[1]->id); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcOwner); - - if(auto stacks = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); - - if(auto arts = dynamic_ptr_cast(pack)) - { - if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) - if(!vstd::contains(ourIds, *id1)) - return true; - - if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) - if(!vstd::contains(ourIds, *id2)) - return true; - return false; - } - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->id); - - if(auto arts = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); - - if(auto art = dynamic_ptr_cast(pack)) - { - if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) - return !vstd::contains(ourIds, *id); - } - - if(auto dismiss = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, dismiss->heroID); - - if(auto upgrade = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, upgrade->id); - - if(auto formation = dynamic_ptr_cast(pack)) - return !vstd::contains(ourIds, formation->hid); - - return CDialogQuery::blocksPack(pack); -} - -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): - CDialogQuery(owner) -{ - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); -} - -void CBattleDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - if(*answer == 1) - { - gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); - } - else - { - gh->battles->endBattleConfirm(bi); - } -} - -void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - assert(answer); - objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); -} - -CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): - CDialogQuery(owner) -{ - this->bd = bd; - addPlayer(bd.player); -} - -void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - // do not change to dynamic_ptr_cast - SIGSEGV! - auto obj = dynamic_cast(objectVisit.visitedObject); - if(obj) - obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); - else - logGlobal->error("Invalid instance in teleport query"); -} - -CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): - CDialogQuery(owner) -{ - this->td = td; - addPlayer(td.player); -} - -CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - hlu = Hlu; - addPlayer(hero->tempOwner); -} - -void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); - gh->levelUpHero(hero, hlu.skills[*answer]); -} - -void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - -CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): - CDialogQuery(owner), hero(Hero) -{ - clu = Clu; - addPlayer(hero->tempOwner); -} - -void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) -{ - assert(answer); - logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); - gh->levelUpCommander(hero->commander, clu.skills[*answer]); -} - -void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); -} - CDialogQuery::CDialogQuery(CGameHandler * owner): CGhQuery(owner) { @@ -515,47 +169,7 @@ void CDialogQuery::setReply(const JsonNode & reply) answer = reply.Integer(); } -CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): - CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) -{ - players.push_back(hero->tempOwner); -} - -void CHeroMovementQuery::onExposure(QueryPtr topQuery) -{ - assert(players.size() == 1); - - if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard - //TODO what if there were H4-like escape? we should also check pos - { - logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); - //finish movement - visitDestAfterVictory = false; - gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); - } - - owner->popIfTop(*this); -} - -void CHeroMovementQuery::onRemoval(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; - gh->sendAndApply(&pb); -} - -void CHeroMovementQuery::onAdding(PlayerColor color) -{ - PlayerBlocked pb; - pb.player = color; - pb.reason = PlayerBlocked::ONGOING_MOVEMENT; - pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; - gh->sendAndApply(&pb); -} - -CGenericQuery::CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback): +CGenericQuery::CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback): CQuery(Owner), callback(Callback) { addPlayer(color); diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index 54497e460..0d105ca37 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -10,21 +10,17 @@ #pragma once #include "../../lib/GameConstants.h" -#include "../../lib/NetPacks.h" +#include "../../lib/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -class CGObjectInstance; -class CGHeroInstance; -class CArmedInstance; +struct CPack; VCMI_LIB_NAMESPACE_END -class CGameHandler; class CObjectVisitQuery; +class QueriesProcessor; class CQuery; -class Queries; using QueryPtr = std::shared_ptr; @@ -42,7 +38,7 @@ public: std::vector players; //players that are affected (often "blocked") by query QueryID queryID; - CQuery(Queries * Owner); + CQuery(QueriesProcessor * Owner); virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. @@ -60,7 +56,7 @@ public: virtual ~CQuery(); protected: - Queries * owner; + QueriesProcessor * owner; void addPlayer(PlayerColor color); bool blockAllButReply(const CPack * pack) const; }; @@ -76,55 +72,6 @@ protected: CGameHandler * gh; }; -//Created when hero visits object. -//Removed when query above is resolved (or immediately after visit if no queries were created) -class CObjectVisitQuery : public CGhQuery -{ -public: - const CGObjectInstance *visitedObject; - const CGHeroInstance *visitingHero; - int3 tile; //may be different than hero pos -> eg. visit via teleport - bool removeObjectAfterVisit; - - CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); - - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; - virtual void onExposure(QueryPtr topQuery) override; -}; - -class CBattleQuery : public CGhQuery -{ -public: - std::array belligerents; - std::array initialHeroMana; - - const BattleInfo *bi; - std::optional result; - - CBattleQuery(CGameHandler * owner); - CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; - virtual void onRemoval(PlayerColor color) override; -}; - -//Created when hero attempts move and something happens -//(not necessarily position change, could be just an object interaction). -class CHeroMovementQuery : public CGhQuery -{ -public: - TryMoveHero tmh; - bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated - const CGHeroInstance *hero; - - virtual void onExposure(QueryPtr topQuery) override; - - CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); - virtual void onAdding(PlayerColor color) override; - virtual void onRemoval(PlayerColor color) override; -}; - class CDialogQuery : public CGhQuery { public: @@ -136,75 +83,10 @@ protected: std::optional answer; }; -class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs -{ -public: - std::array exchangingArmies; - - CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - virtual bool blocksPack(const CPack *pack) const override; -}; - -class CBattleDialogQuery : public CDialogQuery -{ -public: - CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); - - const BattleInfo * bi; - - virtual void onRemoval(PlayerColor color) override; -}; - -//yes/no and component selection dialogs -class CBlockingDialogQuery : public CDialogQuery -{ -public: - BlockingDialog bd; //copy of pack... debug purposes - - CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CTeleportDialogQuery : public CDialogQuery -{ -public: - TeleportDialog td; //copy of pack... debug purposes - - CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; -}; - -class CHeroLevelUpDialogQuery : public CDialogQuery -{ -public: - CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - HeroLevelUp hlu; - const CGHeroInstance * hero; -}; - -class CCommanderLevelUpDialogQuery : public CDialogQuery -{ -public: - CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); - - virtual void onRemoval(PlayerColor color) override; - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; - - CommanderLevelUp clu; - const CGHeroInstance * hero; -}; - class CGenericQuery : public CQuery { public: - CGenericQuery(Queries * Owner, PlayerColor color, std::function Callback); + CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback); bool blocksPack(const CPack * pack) const override; bool endsByPlayerAnswer() const override; @@ -215,28 +97,3 @@ private: std::function callback; JsonNode reply; }; - -class Queries -{ -private: - void addQuery(PlayerColor player, QueryPtr query); - void popQuery(PlayerColor player, QueryPtr query); - - std::map> queries; //player => stack of queries - -public: - static boost::mutex mx; - - void addQuery(QueryPtr query); - void popQuery(const CQuery &query); - void popQuery(QueryPtr query); - void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) - void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) - - QueryPtr topQuery(PlayerColor player); - - std::vector> allQueries() const; - std::vector allQueries(); - QueryPtr getQuery(QueryID queryID); - //void removeQuery -}; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp new file mode 100644 index 000000000..054f3526c --- /dev/null +++ b/server/queries/MapQueries.cpp @@ -0,0 +1,227 @@ +/* + * MapQueries.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 "MapQueries.h" + +#include "QueriesProcessor.h" +#include "../CGameHandler.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/serializer/Cast.h" + +CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): + CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) +{ + addPlayer(Hero->tempOwner); +} + +bool CObjectVisitQuery::blocksPack(const CPack *pack) const +{ + //During the visit itself ALL actions are blocked. + //(However, the visit may trigger a query above that'll pass some.) + return true; +} + +void CObjectVisitQuery::onRemoval(PlayerColor color) +{ + gh->objectVisitEnded(*this); + + //TODO or should it be destructor? + //Can object visit affect 2 players and what would be desired behavior? + if(removeObjectAfterVisit) + gh->removeObject(visitedObject); +} + +void CObjectVisitQuery::onExposure(QueryPtr topQuery) +{ + //Object may have been removed and deleted. + if(gh->isValidObject(visitedObject)) + topQuery->notifyObjectAboutRemoval(*this); + + owner->popIfTop(*this); +} + +void CGarrisonDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->garrisonDialogClosed(objectVisit.visitingHero); +} + +CGarrisonDialogQuery::CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance * up, const CArmedInstance * down): + CDialogQuery(owner) +{ + exchangingArmies[0] = up; + exchangingArmies[1] = down; + + addPlayer(up->tempOwner); + addPlayer(down->tempOwner); +} + +bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const +{ + std::set ourIds; + ourIds.insert(this->exchangingArmies[0]->id); + ourIds.insert(this->exchangingArmies[1]->id); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->id1) || !vstd::contains(ourIds, stacks->id2); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcOwner); + + if(auto stacks = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, stacks->srcArmy) || !vstd::contains(ourIds, stacks->destArmy); + + if(auto arts = dynamic_ptr_cast(pack)) + { + if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) + if(!vstd::contains(ourIds, *id1)) + return true; + + if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) + if(!vstd::contains(ourIds, *id2)) + return true; + return false; + } + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->id); + + if(auto arts = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, arts->srcHero) || !vstd::contains(ourIds, arts->dstHero); + + if(auto art = dynamic_ptr_cast(pack)) + { + if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) + return !vstd::contains(ourIds, *id); + } + + if(auto dismiss = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, dismiss->heroID); + + if(auto upgrade = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, upgrade->id); + + if(auto formation = dynamic_ptr_cast(pack)) + return !vstd::contains(ourIds, formation->hid); + + return CDialogQuery::blocksPack(pack); +} + +void CBlockingDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + assert(answer); + objectVisit.visitedObject->blockingDialogAnswered(objectVisit.visitingHero, *answer); +} + +CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog & bd): + CDialogQuery(owner) +{ + this->bd = bd; + addPlayer(bd.player); +} + +void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + // do not change to dynamic_ptr_cast - SIGSEGV! + auto obj = dynamic_cast(objectVisit.visitedObject); + if(obj) + obj->teleportDialogAnswered(objectVisit.visitingHero, *answer, td.exits); + else + logGlobal->error("Invalid instance in teleport query"); +} + +CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog & td): + CDialogQuery(owner) +{ + this->td = td; + addPlayer(td.player); +} + +CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + hlu = Hlu; + addPlayer(hero->tempOwner); +} + +void CHeroLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing hero level-up query. %s gains skill %d", hero->getObjectName(), answer.value()); + gh->levelUpHero(hero, hlu.skills[*answer]); +} + +void CHeroLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CCommanderLevelUpDialogQuery::CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp & Clu, const CGHeroInstance * Hero): + CDialogQuery(owner), hero(Hero) +{ + clu = Clu; + addPlayer(hero->tempOwner); +} + +void CCommanderLevelUpDialogQuery::onRemoval(PlayerColor color) +{ + assert(answer); + logGlobal->trace("Completing commander level-up query. Commander of hero %s gains skill %s", hero->getObjectName(), answer.value()); + gh->levelUpCommander(hero->commander, clu.skills[*answer]); +} + +void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + objectVisit.visitedObject->heroLevelUpDone(objectVisit.visitingHero); +} + +CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): + CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) +{ + players.push_back(hero->tempOwner); +} + +void CHeroMovementQuery::onExposure(QueryPtr topQuery) +{ + assert(players.size() == 1); + + if(visitDestAfterVictory && hero->tempOwner == players[0]) //hero still alive, so he won with the guard + //TODO what if there were H4-like escape? we should also check pos + { + logGlobal->trace("Hero %s after victory over guard finishes visit to %s", hero->getNameTranslated(), tmh.end.toString()); + //finish movement + visitDestAfterVictory = false; + gh->visitObjectOnTile(*gh->getTile(hero->convertToVisitablePos(tmh.end)), hero); + } + + owner->popIfTop(*this); +} + +void CHeroMovementQuery::onRemoval(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_ENDED; + gh->sendAndApply(&pb); +} + +void CHeroMovementQuery::onAdding(PlayerColor color) +{ + PlayerBlocked pb; + pb.player = color; + pb.reason = PlayerBlocked::ONGOING_MOVEMENT; + pb.startOrEnd = PlayerBlocked::BLOCKADE_STARTED; + gh->sendAndApply(&pb); +} diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h new file mode 100644 index 000000000..c89be802d --- /dev/null +++ b/server/queries/MapQueries.h @@ -0,0 +1,103 @@ +/* + * MapQueries.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 "CQuery.h" + +#include "../../lib/NetPacks.h" + +//Created when hero visits object. +//Removed when query above is resolved (or immediately after visit if no queries were created) +class CObjectVisitQuery : public CGhQuery +{ +public: + const CGObjectInstance *visitedObject; + const CGHeroInstance *visitingHero; + int3 tile; //may be different than hero pos -> eg. visit via teleport + bool removeObjectAfterVisit; + + CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance *Obj, const CGHeroInstance *Hero, int3 Tile); + + virtual bool blocksPack(const CPack *pack) const override; + virtual void onRemoval(PlayerColor color) override; + virtual void onExposure(QueryPtr topQuery) override; +}; + +//Created when hero attempts move and something happens +//(not necessarily position change, could be just an object interaction). +class CHeroMovementQuery : public CGhQuery +{ +public: + TryMoveHero tmh; + bool visitDestAfterVictory; //if hero moved to guarded tile and it should be visited once guard is defeated + const CGHeroInstance *hero; + + virtual void onExposure(QueryPtr topQuery) override; + + CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory = false); + virtual void onAdding(PlayerColor color) override; + virtual void onRemoval(PlayerColor color) override; +}; + + +class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs +{ +public: + std::array exchangingArmies; + + CGarrisonDialogQuery(CGameHandler * owner, const CArmedInstance *up, const CArmedInstance *down); + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + virtual bool blocksPack(const CPack *pack) const override; +}; + +//yes/no and component selection dialogs +class CBlockingDialogQuery : public CDialogQuery +{ +public: + BlockingDialog bd; //copy of pack... debug purposes + + CBlockingDialogQuery(CGameHandler * owner, const BlockingDialog &bd); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class CTeleportDialogQuery : public CDialogQuery +{ +public: + TeleportDialog td; //copy of pack... debug purposes + + CTeleportDialogQuery(CGameHandler * owner, const TeleportDialog &td); + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; +}; + +class CHeroLevelUpDialogQuery : public CDialogQuery +{ +public: + CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp &Hlu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + HeroLevelUp hlu; + const CGHeroInstance * hero; +}; + +class CCommanderLevelUpDialogQuery : public CDialogQuery +{ +public: + CCommanderLevelUpDialogQuery(CGameHandler * owner, const CommanderLevelUp &Clu, const CGHeroInstance * Hero); + + virtual void onRemoval(PlayerColor color) override; + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; + + CommanderLevelUp clu; + const CGHeroInstance * hero; +}; diff --git a/server/queries/QueriesProcessor.cpp b/server/queries/QueriesProcessor.cpp new file mode 100644 index 000000000..259e33a3e --- /dev/null +++ b/server/queries/QueriesProcessor.cpp @@ -0,0 +1,129 @@ +/* + * CQuery.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 "QueriesProcessor.h" + +#include "CQuery.h" + +boost::mutex QueriesProcessor::mx; + +void QueriesProcessor::popQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%s', query='%s'", player % query); + if(topQuery(player) != query) + { + logGlobal->trace("Cannot remove, not a top!"); + return; + } + + queries[player] -= query; + auto nextQuery = topQuery(player); + + query->onRemoval(player); + + //Exposure on query below happens only if removal didn't trigger any new query + if(nextQuery && nextQuery == topQuery(player)) + nextQuery->onExposure(query); +} + +void QueriesProcessor::popQuery(const CQuery &query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%s'", query); + + assert(query.players.size()); + for(auto player : query.players) + { + auto top = topQuery(player); + if(top.get() == &query) + popQuery(top); + else + { + logGlobal->trace("Cannot remove query %s", query.toString()); + logGlobal->trace("Queries found:"); + for(auto q : queries[player]) + { + logGlobal->trace(q->toString()); + } + } + } +} + +void QueriesProcessor::popQuery(QueryPtr query) +{ + for(auto player : query->players) + popQuery(player, query); +} + +void QueriesProcessor::addQuery(QueryPtr query) +{ + for(auto player : query->players) + addQuery(player, query); + + for(auto player : query->players) + query->onAdded(player); +} + +void QueriesProcessor::addQuery(PlayerColor player, QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "player='%d', query='%s'", player.getNum() % query); + query->onAdding(player); + queries[player].push_back(query); +} + +QueryPtr QueriesProcessor::topQuery(PlayerColor player) +{ + return vstd::backOrNull(queries[player]); +} + +void QueriesProcessor::popIfTop(QueryPtr query) +{ + LOG_TRACE_PARAMS(logGlobal, "query='%d'", query); + if(!query) + logGlobal->error("The query is nullptr! Ignoring."); + + popIfTop(*query); +} + +void QueriesProcessor::popIfTop(const CQuery & query) +{ + for(PlayerColor color : query.players) + if(topQuery(color).get() == &query) + popQuery(color, topQuery(color)); +} + +std::vector> QueriesProcessor::allQueries() const +{ + std::vector> ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +std::vector QueriesProcessor::allQueries() +{ + //TODO code duplication with const function :( + std::vector ret; + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + ret.push_back(query); + + return ret; +} + +QueryPtr QueriesProcessor::getQuery(QueryID queryID) +{ + for(auto & playerQueries : queries) + for(auto & query : playerQueries.second) + if(query->queryID == queryID) + return query; + return nullptr; +} diff --git a/server/queries/QueriesProcessor.h b/server/queries/QueriesProcessor.h new file mode 100644 index 000000000..d0fd6df35 --- /dev/null +++ b/server/queries/QueriesProcessor.h @@ -0,0 +1,40 @@ +/* + * QueriesProcessor.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/GameConstants.h" + +class CQuery; +using QueryPtr = std::shared_ptr; + +class QueriesProcessor +{ +private: + void addQuery(PlayerColor player, QueryPtr query); + void popQuery(PlayerColor player, QueryPtr query); + + std::map> queries; //player => stack of queries + +public: + static boost::mutex mx; + + void addQuery(QueryPtr query); + void popQuery(const CQuery &query); + void popQuery(QueryPtr query); + void popIfTop(const CQuery &query); //removes this query if it is at the top (otherwise, do nothing) + void popIfTop(QueryPtr query); //removes this query if it is at the top (otherwise, do nothing) + + QueryPtr topQuery(PlayerColor player); + + std::vector> allQueries() const; + std::vector allQueries(); + QueryPtr getQuery(QueryID queryID); + //void removeQuery +}; From 5c78060a07aeeb611af7e2231e778d5ffaed1c64 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 13 Aug 2023 23:08:53 +0300 Subject: [PATCH 0105/1248] Moved battle netpack validation to battle processor --- server/NetPacksServer.cpp | 42 +++------------- server/battles/BattleProcessor.cpp | 79 ++++++++++++++++++++++++++++-- server/battles/BattleProcessor.h | 7 ++- 3 files changed, 86 insertions(+), 42 deletions(-) diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 3a5db6dcb..70a6a9cec 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -282,48 +282,18 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { - const BattleInfo * b = gs.curB; - if(!b) - gh.throwAndComplain(&pack, "Can not make action - there is no battle ongoing!"); + if (!gh.hasPlayerAt(pack.player, pack.c)) + gh.throwAndComplain(&pack, "No such pack.player!"); - if(b->tacticDistance) - { - if(pack.ba.actionType != EActionType::WALK && pack.ba.actionType != EActionType::END_TACTIC_PHASE - && pack.ba.actionType != EActionType::RETREAT && pack.ba.actionType != EActionType::SURRENDER) - gh.throwAndComplain(&pack, "Can not make actions while in tactics mode!"); - if(!vstd::contains(gh.connections[b->sides[b->tacticsSide].color], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - else - { - auto active = b->battleActiveUnit(); - if(!active) - gh.throwAndComplain(&pack, "No active unit in battle!"); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwAndComplain(&pack, "Can not make actions in battles you are not part of!"); - } - - result = gh.battles->makeBattleAction(pack.ba); + result = gh.battles->makeBattleAction(pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) { - const BattleInfo * b = gs.curB; - if(!b) - gh.throwNotAllowedAction(&pack); - if(b->tacticDistance) - gh.throwNotAllowedAction(&pack); - auto active = b->battleActiveUnit(); - if(!active) - gh.throwNotAllowedAction(&pack); - auto unitOwner = b->battleGetOwner(active); - if(!vstd::contains(gh.connections[unitOwner], pack.c)) - gh.throwNotAllowedAction(&pack); - if(pack.ba.actionType != EActionType::HERO_SPELL) - gh.throwNotAllowedAction(&pack); + if (!gh.hasPlayerAt(pack.player, pack.c)) + gh.throwAndComplain(&pack, "No such pack.player!"); - result = gh.battles->makeCustomAction(pack.ba); + result = gh.battles->makeCustomAction(pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 0b858213d..b43477dd3 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -349,7 +349,6 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm heroes[0] = hero1; heroes[1] = hero2; - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); @@ -819,7 +818,7 @@ bool BattleProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) bsa.askPlayerInterface = false; gameHandler->sendAndApply(&bsa); - bool ret = makeBattleAction(ba); + bool ret = makeBattleActionImpl(ba); checkBattleStateChanges(); return ret; } @@ -2022,7 +2021,7 @@ void BattleProcessor::checkBattleStateChanges() } } -bool BattleProcessor::makeBattleAction(BattleAction &ba) +bool BattleProcessor::makeBattleActionImpl(BattleAction &ba) { bool ok = true; @@ -2445,7 +2444,7 @@ bool BattleProcessor::makeBattleAction(BattleAction &ba) return ok; } -bool BattleProcessor::makeCustomAction(BattleAction & ba) +bool BattleProcessor::makeCustomActionImpl(BattleAction & ba) { switch(ba.actionType) { @@ -2752,3 +2751,75 @@ void BattleProcessor::updateGateState() gameHandler->sendAndApply(&db); } +bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) +{ + boost::unique_lock lock(battleActionMutex); + + const BattleInfo * b = gameHandler->gameState()->curB; + + if(!b && gameHandler->complain("Can not make action - there is no battle ongoing!")) + return false; + + if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) + return false; + + if(b->tacticDistance) + { + if(ba.actionType != EActionType::WALK && ba.actionType != EActionType::END_TACTIC_PHASE + && ba.actionType != EActionType::RETREAT && ba.actionType != EActionType::SURRENDER) + { + gameHandler->complain("Can not make actions while in tactics mode!"); + return false; + } + + if(player != b->sides[ba.side].color) + { + gameHandler->complain("Can not make actions in battles you are not part of!"); + return false; + } + } + else + { + auto active = b->battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + auto unitOwner = b->battleGetOwner(active); + + if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + return false; + } + + return makeBattleActionImpl(ba); +} + +bool BattleProcessor::makeCustomAction(PlayerColor player, BattleAction &ba) +{ + const BattleInfo * b = gameHandler->gameState()->curB; + + if(!b && gameHandler->complain("Can not make action - there is no battle ongoing!")) + return false; + + if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) + return false; + + if(b->tacticDistance) + { + gameHandler->complain("Can not cast spell during tactics mode!"); + return false; + } + + auto active = b->battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + auto unitOwner = b->battleGetOwner(active); + + if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + return false; + + if(ba.actionType != EActionType::HERO_SPELL && gameHandler->complain("Invalid custom action type!")) + return false; + + return makeCustomActionImpl(ba); +} diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 8a2ff8950..b6ad9e7ff 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -100,6 +100,9 @@ class BattleProcessor : boost::noncopyable void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); void setBattleResult(BattleResult::EResult resultType, int victoriusSide); + bool makeBattleActionImpl(BattleAction &ba); + bool makeCustomActionImpl(BattleAction &ba); + public: CGameHandler * gameHandler; @@ -114,8 +117,8 @@ public: void battleAfterLevelUp(const BattleResult &result); - bool makeBattleAction(BattleAction &ba); - bool makeCustomAction(BattleAction &ba); + bool makeBattleAction(PlayerColor player, BattleAction &ba); + bool makeCustomAction(PlayerColor player, BattleAction &ba); void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); From 44832f3797c725433d66022665fbd31f19968179 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 14 Aug 2023 19:46:42 +0300 Subject: [PATCH 0106/1248] Split BattleProcessor into few more parts --- client/battle/BattleInterfaceClasses.cpp | 12 +- lib/GameConstants.h | 7 + lib/NetPacks.h | 4 +- server/CGameHandler.cpp | 2 +- server/CMakeLists.txt | 6 + server/battles/BattleActionProcessor.cpp | 1355 +++++++++++ server/battles/BattleActionProcessor.h | 58 + server/battles/BattleFlowProcessor.cpp | 687 ++++++ server/battles/BattleFlowProcessor.h | 49 + server/battles/BattleProcessor.cpp | 2591 +--------------------- server/battles/BattleProcessor.h | 107 +- server/battles/BattleResultProcessor.cpp | 554 +++++ server/battles/BattleResultProcessor.h | 78 + 13 files changed, 2882 insertions(+), 2628 deletions(-) create mode 100644 server/battles/BattleActionProcessor.cpp create mode 100644 server/battles/BattleActionProcessor.h create mode 100644 server/battles/BattleFlowProcessor.cpp create mode 100644 server/battles/BattleFlowProcessor.h create mode 100644 server/battles/BattleResultProcessor.cpp create mode 100644 server/battles/BattleResultProcessor.h diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 23226b84b..94ea31b38 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -567,12 +567,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface int text = 304; switch(br.result) { - case BattleResult::NORMAL: + case EBattleResult::NORMAL: break; - case BattleResult::ESCAPE: + case EBattleResult::ESCAPE: text = 303; break; - case BattleResult::SURRENDER: + case EBattleResult::SURRENDER: text = 302; break; default: @@ -601,14 +601,14 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface std::string videoName = "LBSTART.BIK"; switch(br.result) { - case BattleResult::NORMAL: + case EBattleResult::NORMAL: break; - case BattleResult::ESCAPE: + case EBattleResult::ESCAPE: musicName = "Music/Retreat Battle"; videoName = "RTSTART.BIK"; text = 310; break; - case BattleResult::SURRENDER: + case EBattleResult::SURRENDER: musicName = "Music/Surrender Battle"; videoName = "SURRENDER.BIK"; text = 309; diff --git a/lib/GameConstants.h b/lib/GameConstants.h index ab2aaed89..9097f9aa1 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1379,6 +1379,13 @@ enum class EHealPower : ui8 PERMANENT }; +enum class EBattleResult : ui8 +{ + NORMAL = 0, + ESCAPE = 1, + SURRENDER = 2 +}; + // Typedef declarations using TExpType = si64; using TQuantity = si32; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 735737da5..e1f3cad28 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1524,11 +1524,9 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient struct DLL_LINKAGE BattleResult : public Query { - enum EResult { NORMAL = 0, ESCAPE = 1, SURRENDER = 2 }; - void applyFirstCl(CClient * cl); - EResult result = NORMAL; + EBattleResult result = EBattleResult::NORMAL; ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] std::map casualties[2]; //first => casualties of attackers - map crid => number TExpType exp[2] = {0, 0}; //exp for attacker and defender diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a32983423..4e47b8a3c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4212,7 +4212,7 @@ void CGameHandler::deserializationFix() //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization // restore any places that requires such pointer manually heroPool->gameHandler = this; - battles->gameHandler = this; + battles->setGameHandler(this); playerMessages->gameHandler = this; } diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index ed7881e32..49c40d60e 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -1,7 +1,10 @@ set(server_SRCS StdInc.cpp + battles/BattleActionProcessor.cpp + battles/BattleFlowProcessor.cpp battles/BattleProcessor.cpp + battles/BattleResultProcessor.cpp queries/BattleQueries.cpp queries/CQuery.cpp @@ -21,7 +24,10 @@ set(server_SRCS set(server_HEADERS StdInc.h + battles/BattleActionProcessor.h + battles/BattleFlowProcessor.h battles/BattleProcessor.h + battles/BattleResultProcessor.h queries/BattleQueries.h queries/CQuery.h diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp new file mode 100644 index 000000000..7ba68cb27 --- /dev/null +++ b/server/battles/BattleActionProcessor.cpp @@ -0,0 +1,1355 @@ +/* + * BattleActionProcessor.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 "BattleActionProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" +#include "../CVCMIServer.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/GameSettings.h" +#include "../../lib/ScopeGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CUnitState.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/serializer/Cast.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/spells/Problem.h" + +BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +bool BattleActionProcessor::makeBattleAction(BattleAction &ba) +{ + bool ok = true; + + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack + + const bool isAboutActiveStack = stack && (ba.stackNumber == gameHandler->gameState()->curB->getActiveStackID()); + + logGlobal->trace("Making action: %s", ba.toString()); + + switch(ba.actionType) + { + case EActionType::WALK: //walk + case EActionType::DEFEND: //defend + case EActionType::WAIT: //wait + case EActionType::WALK_AND_ATTACK: //walk or attack + case EActionType::SHOOT: //shoot + case EActionType::CATAPULT: //catapult + case EActionType::STACK_HEAL: //healing with First Aid Tent + case EActionType::MONSTER_SPELL: + + if (!stack) + { + gameHandler->complain("No such stack!"); + return false; + } + if (!stack->alive()) + { + gameHandler->complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if (gameHandler->battleTacticDist()) + { + if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) + { + gameHandler->complain("This is not a stack of side that has tactics!"); + return false; + } + } + else if (!isAboutActiveStack) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } + } + + static EndAction end_action; + auto wrapAction = [this](BattleAction &ba) + { + StartAction startAction(ba); + gameHandler->sendAndApply(&startAction); + + return vstd::makeScopeGuard([&]() + { + gameHandler->sendAndApply(&end_action); + }); + }; + + switch(ba.actionType) + { + case EActionType::END_TACTIC_PHASE: //wait + case EActionType::BAD_MORALE: + case EActionType::NO_ACTION: + { + auto wrapper = wrapAction(ba); + break; + } + case EActionType::WALK: + { + auto wrapper = wrapAction(ba); + if(target.size() < 1) + { + gameHandler->complain("Destination required for move action."); + ok = false; + break; + } + int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move + if (!walkedTiles) + gameHandler->complain("Stack failed movement!"); + break; + } + case EActionType::DEFEND: + { + //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) + SetStackEffect sse; + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), + -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(defenseBonusToAdd)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + std::vector buffer; + if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) + { + difference = 1; + buffer.push_back(alternativeWeakCreatureBonus); + } + else + { + buffer.push_back(defenseBonusToAdd); + } + + buffer.push_back(bonus2); + + sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); + gameHandler->sendAndApply(&sse); + + BattleLogMessage message; + + MetaString text; + stack->addText(text, EMetaText::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.replaceNumber(difference); + + message.lines.push_back(text); + + gameHandler->sendAndApply(&message); + //don't break - we share code with next case + } + [[fallthrough]]; + case EActionType::WAIT: + { + auto wrapper = wrapAction(ba); + break; + } + case EActionType::RETREAT: //retreat/flee + { + if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) + gameHandler->complain("Cannot retreat!"); + else + owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); //surrendering side loses + break; + } + case EActionType::SURRENDER: + { + PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; + int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); + if (cost < 0) + gameHandler->complain("Cannot surrender!"); + else if (gameHandler->getResource(player, EGameResID::GOLD) < cost) + gameHandler->complain("Not enough gold to surrender!"); + else + { + gameHandler->giveResource(player, EGameResID::GOLD, -cost); + owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); //surrendering side loses + } + break; + } + case EActionType::WALK_AND_ATTACK: //walk or attack + { + auto wrapper = wrapAction(ba); + + if(!stack) + { + gameHandler->complain("No attacker"); + ok = false; + break; + } + + if(target.size() < 2) + { + gameHandler->complain("Two destinations required for attack action."); + ok = false; + break; + } + + BattleHex attackPos = target.at(0).hexValue; + BattleHex destinationTile = target.at(1).hexValue; + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); + + if(!destinationStack) + { + gameHandler->complain("Invalid target to attack"); + ok = false; + break; + } + + BattleHex startingPos = stack->getPosition(); + int distance = moveStack(ba.stackNumber, attackPos); + + logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); + + if(stack->getPosition() != attackPos + && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) + ) + { + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine + break; + } + + if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + gameHandler->complain("Unit can not attack itself"); + ok = false; + break; + } + + if(!CStack::isMeleeAttackPossible(stack, destinationStack)) + { + gameHandler->complain("Attack cannot be performed!"); + ok = false; + break; + } + + //attack + int totalAttacks = stack->totalAttacks.getMeleeValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + + const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + const bool retaliation = destinationStack->ableToRetaliate(); + for (int i = 0; i < totalAttacks; ++i) + { + //first strike + if(i == 0 && firstStrike && retaliation) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) + { + makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + } + + //counterattack + //we check retaliation twice, so if it unblocked during attack it will work only on next attack + if(stack->alive() + && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) + && (i == 0 && !firstStrike) + && retaliation && destinationStack->ableToRetaliate()) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + } + } + + //return + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) + && target.size() == 3 + && startingPos != stack->getPosition() + && startingPos == target.at(2).hexValue + && stack->alive()) + { + moveStack(ba.stackNumber, startingPos); + //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) + } + break; + } + case EActionType::SHOOT: + { + if(target.size() < 1) + { + gameHandler->complain("Destination required for shot action."); + ok = false; + break; + } + + auto destination = target.at(0).hexValue; + + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); + + if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) + { + gameHandler->complain("Cannot shoot!"); + break; + } + if (!destinationStack) + { + gameHandler->complain("No target to shoot!"); + break; + } + + auto wrapper = wrapAction(ba); + + makeAttack(stack, destinationStack, 0, destination, true, true, false); + + //ranged counterattack + if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) + && destinationStack->ableToRetaliate() + && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) + && stack->alive()) //attacker may have died (fire shield) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); + } + //allow more than one additional attack + + int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + + for(int i = 1; i < totalRangedAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots.canUse() + ) + { + makeAttack(stack, destinationStack, 0, destination, false, true, false); + } + } + break; + } + case EActionType::CATAPULT: + { + auto wrapper = wrapAction(ba); + const CStack * shooter = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + if(!catapultAbility || catapultAbility->subtype < 0) + { + gameHandler->complain("We do not know how to shoot :P"); + } + else + { + const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); + parameters.setSpellLevel(shotLevel); + parameters.cast(gameHandler->spellEnv, target); + } + //finish by scope guard + break; + } + case EActionType::STACK_HEAL: //healing with First Aid Tent + { + auto wrapper = wrapAction(ba); + const CStack * healer = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + + if(target.size() < 1) + { + gameHandler->complain("Destination required for heal action."); + ok = false; + break; + } + + const battle::Unit * destStack = nullptr; + std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + + if(target.at(0).unitValue) + destStack = target.at(0).unitValue; + else + destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); + + if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) + { + gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); + } + else + { + const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + auto dest = battle::Destination(destStack, target.at(0).hexValue); + parameters.setSpellLevel(0); + parameters.cast(gameHandler->spellEnv, {dest}); + } + break; + } + case EActionType::MONSTER_SPELL: + { + auto wrapper = wrapAction(ba); + + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + SpellID spellID = SpellID(ba.actionSubtype); + + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); + + //TODO special bonus for genies ability + if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + + if (spellID < 0) + gameHandler->complain("That stack can't cast spells!"); + else + { + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); + } + break; + } + } + + if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); + + return ok; +} + +bool BattleActionProcessor::makeCustomAction(BattleAction & ba) +{ + switch(ba.actionType) + { + case EActionType::HERO_SPELL: + { + const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if (!h) + { + logGlobal->error("Wrong caster!"); + return false; + } + + const CSpell * s = SpellID(ba.actionSubtype).toSpell(); + if (!s) + { + logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); + return false; + } + + spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); + + spells::detail::ProblemImpl problem; + + auto m = s->battleMechanics(¶meters); + + if(!m->canBeCast(problem))//todo: should we check aimed cast? + { + logGlobal->warn("Spell cannot be cast!"); + std::vector texts; + problem.getAll(texts); + for(auto s : texts) + logGlobal->warn(s); + return false; + } + + StartAction start_action(ba); + gameHandler->sendAndApply(&start_action); //start spell casting + + parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); + + EndAction end_action; + gameHandler->sendAndApply(&end_action); + return true; + } + } + return false; +} + +int BattleActionProcessor::moveStack(int stack, BattleHex dest) +{ + int ret = 0; + + const CStack *curStack = gameHandler->battleGetStackByID(stack), + *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); + + assert(curStack); + assert(dest < GameConstants::BFIELD_SIZE); + + if (gameHandler->gameState()->curB->tacticDistance) + { + assert(gameHandler->gameState()->curB->isInTacticRange(dest)); + } + + auto start = curStack->getPosition(); + if (start == dest) + return 0; + + //initing necessary tables + auto accessibility = gameHandler->getAccesibility(curStack); + std::set passed; + //Ignore obstacles on starting position + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + + //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) + if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) + { + BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); + + if(accessibility.accessible(shifted, curStack)) + dest = shifted; + } + + if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) + { + gameHandler->complain("Given destination is not accessible!"); + return 0; + } + + bool canUseGate = false; + auto dbState = gameHandler->gameState()->curB->si.gateState; + if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + dbState != EGateState::DESTROYED && + dbState != EGateState::BLOCKED) + { + canUseGate = true; + } + + std::pair< std::vector, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack); + + ret = path.second; + + int creSpeed = curStack->speed(0, true); + + if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) + creSpeed = GameConstants::BFIELD_SIZE; + + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + { + return obst->obstacleType == CObstacleInstance::MOAT; + }); + + auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_OUTER) + return true; + if (hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool + { + if (isGateDrawbridgeHex(hex)) + return true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) + return true; + } + + return false; + }; + + if (curStack->hasBonusOfType(BonusType::FLYING)) + { + if (path.second <= creSpeed && path.first.size() > 0) + { + if (canUseGate && dbState != EGateState::OPENED && + occupyGateDrawbridgeHex(dest)) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + + //inform clients about move + BattleStackMoved sm; + sm.stack = curStack->unitId(); + std::vector tiles; + tiles.push_back(path.first[0]); + sm.tilesToMove = tiles; + sm.distance = path.second; + sm.teleporting = false; + gameHandler->sendAndApply(&sm); + } + } + else //for non-flying creatures + { + std::vector tiles; + const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); + int v = (int)path.first.size()-1; + path.first.push_back(start); + + // check if gate need to be open or closed at some point + BattleHex openGateAtHex, gateMayCloseAtHex; + if (canUseGate) + { + for (int i = (int)path.first.size()-1; i >= 0; i--) + { + auto needOpenGates = [&](BattleHex hex) -> bool + { + if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + return true; + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) + return true; + else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) + return true; + + return false; + }; + + auto hex = path.first[i]; + if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) + { + if (needOpenGates(hex)) + openGateAtHex = path.first[i+1]; + + //TODO we need find batter way to handle double-wide stacks + //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + if (otherHex.isValid() && needOpenGates(otherHex)) + openGateAtHex = path.first[i+2]; + } + + //gate may be opened and then closed during stack movement, but not other way around + if (openGateAtHex.isValid()) + dbState = EGateState::OPENED; + } + + if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) + { + if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + if (hasWideMoat) + { + if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + { + gateMayCloseAtHex = path.first[i-1]; + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && + path.first[i-1] != ESiegeHex::GATE_INNER && + path.first[i-1] != ESiegeHex::GATE_BRIDGE) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) + { + gateMayCloseAtHex = path.first[i-1]; + } + } + } + } + + bool stackIsMoving = true; + + while(stackIsMoving) + { + if (verror("Movement terminated abnormally"); + break; + } + + bool gateStateChanging = false; + //special handling for opening gate on from starting hex + if (openGateAtHex.isValid() && openGateAtHex == start) + gateStateChanging = true; + else + { + for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) + { + BattleHex hex = path.first[v]; + tiles.push_back(hex); + + if ((openGateAtHex.isValid() && openGateAtHex == hex) || + (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) + { + gateStateChanging = true; + } + + //if we walked onto something, finalize this portion of stack movement check into obstacle + if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty()) + obstacleHit = true; + + if (curStack->doubleWide()) + { + BattleHex otherHex = curStack->occupiedHex(hex); + //two hex creature hit obstacle by backside + auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false); + if(otherHex.isValid() && !obstacle2.empty()) + obstacleHit = true; + } + if(!obstacleHit) + passed.insert(hex); + } + } + + if (!tiles.empty()) + { + //commit movement + BattleStackMoved sm; + sm.stack = curStack->unitId(); + sm.distance = path.second; + sm.teleporting = false; + sm.tilesToMove = tiles; + gameHandler->sendAndApply(&sm); + tiles.clear(); + } + + //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end + if (curStack->getPosition() != dest) + { + if(stackIsMoving && start != curStack->getPosition()) + { + stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + passed.insert(curStack->getPosition()); + if(curStack->doubleWide()) + passed.insert(curStack->occupiedHex()); + } + if (gateStateChanging) + { + if (curStack->getPosition() == openGateAtHex) + { + openGateAtHex = BattleHex(); + //only open gate if stack is still alive + if (curStack->alive()) + { + BattleUpdateGateState db; + db.state = EGateState::OPENED; + gameHandler->sendAndApply(&db); + } + } + else if (curStack->getPosition() == gateMayCloseAtHex) + { + gateMayCloseAtHex = BattleHex(); + owner->updateGateState(); + } + } + } + else + //movement finished normally: we reached destination + stackIsMoving = false; + } + } + //handle last hex separately for deviation + if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) + { + if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) + || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) + passed.clear(); //Just empty passed, obstacles will handled automatically + } + //handling obstacle on the final field (separate, because it affects both flying and walking stacks) + gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + + return ret; +} + +void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) +{ + if(first && !counter) + handleAttackBeforeCasting(ranged, attacker, defender); + + FireShieldInfo fireShield; + BattleAttack bat; + BattleLogMessage blm; + bat.stackAttacking = attacker->unitId(); + bat.tile = targetHex; + + std::shared_ptr attackerState = attacker->acquireState(); + + if(ranged) + bat.flags |= BattleAttack::SHOT; + if(counter) + bat.flags |= BattleAttack::COUNTER; + + const int attackerLuck = attacker->luckVal(); + + if(attackerLuck > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::LUCKY; + } + + if(attackerLuck < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); + size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + bat.flags |= BattleAttack::UNLUCKY; + } + + if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) + { + bat.flags |= BattleAttack::DEATH_BLOW; + } + + const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner()); + if(owner) + { + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); + if (chance > gameHandler->getRandomGenerator().nextInt(99)) + bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; + } + + int64_t drainedLife = 0; + + // only primary target + if(defender->alive()) + drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); + + //multiple-hex normal attack + std::set attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + + std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); + if(bonus && ranged) //TODO: make it work in melee? + { + //this is need for displaying hit animation + bat.flags |= BattleAttack::SPELL_LIKE; + bat.spellID = SpellID(bonus->subtype); + + //TODO: should spell override creature`s projectile? + + auto spell = bat.spellID.toSpell(); + + battle::Target target; + target.emplace_back(defender, targetHex); + + spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + event.setSpellLevel(bonus->val); + + auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); + + //TODO: get exact attacked hex for defender + + for(const CStack * stack : attackedCreatures) + { + if(stack != defender && stack->alive()) //do not hit same stack twice + { + drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + } + } + + //now add effect info for all attacked stacks + for (BattleStackAttacked & bsa : bat.bsa) + { + if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield + { + //this is need for displaying affect animation + bsa.flags |= BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID(bonus->subtype); + } + } + } + + attackerState->afterAttack(ranged, counter); + + { + UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); + attackerState->save(info.data); + bat.attackerChanges.changedStacks.push_back(info); + } + + if (drainedLife > 0) + bat.flags |= BattleAttack::LIFE_DRAIN; + + gameHandler->sendAndApply(&bat); + + { + const bool multipleTargets = bat.bsa.size() > 1; + + int64_t totalDamage = 0; + int32_t totalKills = 0; + + for(const BattleStackAttacked & bsa : bat.bsa) + { + totalDamage += bsa.damageAmount; + totalKills += bsa.killedAmount; + } + + { + MetaString text; + attacker->addText(text, EMetaText::GENERAL_TXT, 376); + attacker->addNameReplacement(text); + text.replaceNumber(totalDamage); + blm.lines.push_back(text); + } + + addGenericKilledLog(blm, defender, totalKills, multipleTargets); + } + + // drain life effect (as well as log entry) must be applied after the attack + if(drainedLife > 0) + { + MetaString text; + attackerState->addText(text, EMetaText::GENERAL_TXT, 361); + attackerState->addNameReplacement(text, false); + text.replaceNumber(drainedLife); + defender->addNameReplacement(text, true); + blm.lines.push_back(std::move(text)); + } + + if(!fireShield.empty()) + { + //todo: this should be "virtual" spell instead, we only need fire spell school bonus here + const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); + int64_t totalDamage = 0; + + for(const auto & item : fireShield) + { + const CStack * actor = item.first; + int64_t rawDamage = item.second; + + const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner()); + + if(actorOwner) + { + rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); + } + else + { + rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); + } + + totalDamage+=rawDamage; + //FIXME: add custom effect on actor + } + + if (totalDamage > 0) + { + BattleStackAttacked bsa; + + bsa.flags |= BattleStackAttacked::FIRE_SHIELD; + bsa.stackAttacked = attacker->unitId(); //invert + bsa.attackerID = defender->unitId(); + bsa.damageAmount = totalDamage; + attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured pack; + pack.stacks.push_back(bsa); + gameHandler->sendAndApply(&pack); + + // TODO: this is already implemented in Damage::describeEffect() + { + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 376); + text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); + text.replaceNumber(totalDamage); + blm.lines.push_back(std::move(text)); + } + addGenericKilledLog(blm, attacker, bsa.killedAmount, false); + } + } + + gameHandler->sendAndApply(&blm); + + handleAfterAttackCasting(ranged, attacker, defender); +} + +void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +{ + if(attacker->hasBonusOfType(attackMode)) + { + std::set spellsToCast; + TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); + for(const auto & sf : *spells) + { + spellsToCast.insert(SpellID(sf->subtype)); + } + for(SpellID spellID : spellsToCast) + { + bool castMe = false; + if(!defender->alive()) + { + logGlobal->debug("attackCasting: all attacked creatures have been killed"); + return; + } + int32_t spellLevel = 0; + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); + for(const auto & sf : *spellsByType) + { + int meleeRanged; + if(sf->additionalInfo.size() < 2) + { + // legacy format + vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); + meleeRanged = sf->additionalInfo[0] / 1000; + } + else + { + vstd::amax(spellLevel, sf->additionalInfo[0]); + meleeRanged = sf->additionalInfo[1]; + } + if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) + castMe = true; + } + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); + vstd::amin(chance, 100); + + const CSpell * spell = SpellID(spellID).toSpell(); + spells::AbilityCaster caster(attacker, spellLevel); + + spells::Target target; + target.emplace_back(defender); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + + auto m = spell->battleMechanics(¶meters); + + spells::detail::ProblemImpl ignored; + + if(!m->canBeCastAt(target, ignored)) + continue; + + //check if spell should be cast (probability handling) + if(gameHandler->getRandomGenerator().nextInt(99) >= chance) + continue; + + //casting + if(castMe) + { + parameters.cast(gameHandler->spellEnv, target); + } + } + } +} + +void BattleActionProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? +} + +void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) +{ + if(!attacker->alive() || !defender->alive()) // can be already dead + return; + + attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); + + if(!defender->alive()) + { + //don't try death stare or acid breath on dead stack (crash!) + return; + } + + if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) + { + // mechanics of Death Stare as in H3: + // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution + //original formula x = min(x, (gorgons_count + 9)/10); + + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; + vstd::amin(chanceToKill, 1); //cap at 100% + + std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); + + int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); + + double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 + int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count + vstd::amin(staredCreatures, maxToKill); + + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); + if(staredCreatures) + { + //TODO: death stare was not originally available for multiple-hex attacks, but... + const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + parameters.setEffectValue(staredCreatures); + parameters.cast(gameHandler->spellEnv, target); + } + } + + if(!defender->alive()) + return; + + int64_t acidDamage = 0; + TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); + for(const auto & b : *acidBreath) + { + if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99)) + acidDamage += b->val; + } + + if(acidDamage > 0) + { + const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); + + spells::AbilityCaster caster(attacker, 0); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::Target target; + target.emplace_back(defender); + + parameters.setEffectValue(acidDamage * attacker->getCount()); + parameters.cast(gameHandler->spellEnv, target); + } + + + if(!defender->alive()) + return; + + if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability + { + double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; + vstd::amin(chanceToTrigger, 1); //cap at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; + + if(defender->unitType()->getId() == bonusAdditionalInfo || + (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) + return; + + battle::UnitInfo resurrectInfo; + resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId(); + resurrectInfo.summoned = false; + resurrectInfo.position = defender->getPosition(); + resurrectInfo.side = defender->unitSide(); + + if(bonusAdditionalInfo != CAddInfo::NONE) + resurrectInfo.type = CreatureID(bonusAdditionalInfo); + else + resurrectInfo.type = attacker->creatureId(); + + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) + resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) + resurrectInfo.count = defender->getCount(); + else + return; //wrong subtype + + BattleUnitsChanged addUnits; + addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); + resurrectInfo.save(addUnits.changedStacks.back().data); + + BattleUnitsChanged removeUnits; + removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&removeUnits); + gameHandler->sendAndApply(&addUnits); + } + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) + { + double chanceToTrigger = 0; + int amountToDie = 0; + + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; + amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); + } + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count + { + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; + } + + vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% + + if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) + return; + + BattleStackAttacked bsa; + bsa.attackerID = -1; + bsa.stackAttacked = defender->unitId(); + bsa.damageAmount = amountToDie * defender->getMaxHealth(); + bsa.flags = BattleStackAttacked::SPELL_EFFECT; + bsa.spellID = SpellID::SLAYER; + defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); + + StacksInjured si; + si.stacks.push_back(bsa); + + gameHandler->sendAndApply(&si); + sendGenericKilledLog(defender, bsa.killedAmount, false); + } +} + +int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +{ + BattleStackAttacked bsa; + if(secondary) + bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities + + bsa.attackerID = attackerState->unitId(); + bsa.stackAttacked = def->unitId(); + { + BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); + + bai.deathBlow = bat.deathBlow(); + bai.doubleDamage = bat.ballistaDoubleDmg(); + bai.luckyStrike = bat.lucky(); + bai.unluckyStrike = bat.unlucky(); + + auto range = gameHandler->gameState()->curB->calculateDmgRange(bai); + bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties + } + + int64_t drainedLife = 0; + + //life drain handling + if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) + { + int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; + attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + drainedLife += toHeal; + } + + //soul steal handling + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) + { + //we can have two bonuses - one with subtype 0 and another with subtype 1 + //try to use permanent first, use only one of two + for(si32 subtype = 1; subtype >= 0; subtype--) + { + if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) + { + int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); + attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); + drainedLife += toHeal; + break; + } + } + } + bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated + + //fire shield handling + if(!bat.shot() && + !def->isClone() && + def->hasBonusOfType(BonusType::FIRE_SHIELD) && + !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && + CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) + ) + { + //TODO: use damage with bonus but without penalties + auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; + fireShield.push_back(std::make_pair(def, fireShieldDamage)); + } + + return drainedLife; +} + +void BattleActionProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + BattleLogMessage blm; + addGenericKilledLog(blm, defender, killed, multiple); + gameHandler->sendAndApply(&blm); + } +} + +void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) +{ + if(killed > 0) + { + const int32_t txtIndex = (killed > 1) ? 379 : 378; + std::string formatString = VLC->generaltexth->allTexts[txtIndex]; + + // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); + formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); + boost::algorithm::trim(formatString); + + boost::format txt(formatString); + if(killed > 1) + { + txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish + } + else //killed == 1 + { + txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes + } + MetaString line; + line.appendRawString(txt.str()); + blm.lines.push_back(std::move(line)); + } +} diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h new file mode 100644 index 000000000..1f07a3c4b --- /dev/null +++ b/server/battles/BattleActionProcessor.h @@ -0,0 +1,58 @@ +/* + * BattleActionProcessor.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 + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleLogMessage; +struct BattleAttack; +class BattleProcessor; +class BattleAction; +struct BattleHex; +class CStack; +enum class BonusType; + +namespace battle { +class Unit; +class CUnitState; +} + +VCMI_LIB_NAMESPACE_END + +class CGameHandler; + +class BattleActionProcessor : boost::noncopyable +{ + using FireShieldInfo = std::vector>; + + BattleProcessor * owner; + CGameHandler * gameHandler; + + int moveStack(int stack, BattleHex dest); //returned value - travelled distance + void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + + void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + + // damage, drain life & fire shield; returns amount of drained life + int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + + void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); + void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + +public: + BattleActionProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + bool makeBattleAction(BattleAction &ba); + bool makeCustomAction(BattleAction &ba); +}; + diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp new file mode 100644 index 000000000..416210201 --- /dev/null +++ b/server/battles/BattleFlowProcessor.cpp @@ -0,0 +1,687 @@ +/* + * BattleFlowProcessor.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 "BattleFlowProcessor.h" + +#include "BattleProcessor.h" + +#include "../CGameHandler.h" +#include "../CVCMIServer.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/GameSettings.h" +#include "../../lib/ScopeGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CUnitState.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/serializer/Cast.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/spells/Problem.h" + +BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +void BattleFlowProcessor::summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +{ + int x = targetPosition.getX(); + int y = targetPosition.getY(); + + const bool targetIsAttacker = side == BattleSide::ATTACKER; + + if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + + //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's + if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) + { + if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } + else + { //add back-side guardians for two-hex target, side guardians for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); + + if (!targetIsTwoHex && x > 2) //back guard for one-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); + else if (targetIsTwoHex)//front-side guardians for two-hex target + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + if (x > 3) //back guard for two-hex + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); + } + } + + } + + else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) + { + if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + else + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); + + if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); + else if (targetIsTwoHex) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + if (x < GameConstants::BFIELD_WIDTH - 4) + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); + } + } + } + + else if (!targetIsAttacker && y % 2 == 0) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); + } + + else if (targetIsAttacker && y % 2 == 1) + { + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); + BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); + } +} + +void BattleFlowProcessor::onBattleStarted() +{ + gameHandler->setBattle(gameHandler->gameState()->curB); + assert(gameHandler->gameState()->curB); + //TODO: pre-tactic stuff, call scripts etc. + + //Moat should be initialized here, because only here we can use spellcasting + if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) + { + const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER); + const auto * actualCaster = h ? static_cast(h) : nullptr; + auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); + auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell()); + auto target = spells::Target(); + cast.cast(gameHandler->spellEnv, target); + } + + if (gameHandler->gameState()->curB->tacticDistance == 0) + onTacticsEnded(); +} + +void BattleFlowProcessor::onTacticsEnded() +{ + //initial stacks appearance triggers, e.g. built-in bonus spells + auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing + + for (CStack * stack : initialStacks) + { + if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) + { + std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); + auto accessibility = gameHandler->getAccesibility(); + CreatureID creatureData = CreatureID(summonInfo->subtype); + std::vector targetHexes; + const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard + const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); + + /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. + For one-hex targets there are four guardians - front, back and one per side (up + down). + Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front + Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ + if (!guardianIsBig) + targetHexes = stack->getSurroundingHexes(); + else + summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + + for(auto hex : targetHexes) + { + if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex + { + battle::UnitInfo info; + info.id = gameHandler->gameState()->curB->battleNextUnitId(); + info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); + info.type = creatureData; + info.side = stack->unitSide(); + info.position = hex; + info.summoned = true; + + BattleUnitsChanged pack; + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + gameHandler->sendAndApply(&pack); + } + } + } + + stackEnchantedTrigger(stack); + } + + //spells opening battle + for (int i = 0; i < 2; ++i) + { + auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); + if (h) + { + TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + + for (auto b : *bl) + { + spells::BonusCaster caster(h, b); + + const CSpell * spell = SpellID(b->subtype).toSpell(); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + parameters.setSpellLevel(3); + parameters.setEffectDuration(b->val); + parameters.massive = true; + parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); + } + } + } + // it is possible that due to opening spells one side was eliminated -> check for end of battle + owner->checkBattleStateChanges(); + + startNextRound(true); +} + +void BattleFlowProcessor::startNextRound(bool isFirstRound) +{ + BattleNextRound bnr; + bnr.round = gameHandler->gameState()->curB->round + 1; + logGlobal->debug("Round %d", bnr.round); + gameHandler->sendAndApply(&bnr); + + auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it + for (auto &obstPtr : obstacles) + { + if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) + if (sco->turnsRemaining == 0) + removeObstacle(*obstPtr); + } + + const BattleInfo & curB = *gameHandler->gameState()->curB; + + for(auto stack : curB.stacks) + { + if(stack->alive() && !isFirstRound) + stackEnchantedTrigger(stack); + } + + activateNextStack(); +} + +const CStack * BattleFlowProcessor::getNextStack() +{ + std::vector q; + gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" + + if(q.empty()) + return nullptr; + + if(q.front().empty()) + return nullptr; + + auto next = q.front().front(); + const auto stack = dynamic_cast(next); + + // regeneration takes place before everything else but only during first turn attempt in each round + // also works under blind and similar effects + if(stack && stack->alive() && !stack->waiting) + { + BattleTriggerEffect bte; + bte.stackID = stack->unitId(); + bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); + + const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); + if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) + bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); + + if(bte.val) // anything to heal + gameHandler->sendAndApply(&bte); + } + + if(!next->willMove()) + return nullptr; + + return stack; +} + +void BattleFlowProcessor::activateNextStack() +{ + //TODO: activate next round if next == nullptr + const auto & curB = *gameHandler->gameState()->curB; + + const CStack * next = getNextStack(); + + if (!next) + return; + + BattleUnitsChanged removeGhosts; + + for(auto stack : curB.stacks) + { + if(stack->ghostPending) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); + } + + if(!removeGhosts.changedStacks.empty()) + gameHandler->sendAndApply(&removeGhosts); +} + +bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) +{ + const auto & curB = *gameHandler->gameState()->curB; + + // check for bad morale => freeze + int nextStackMorale = next->moraleVal(); + if(!next->hadMorale && !next->waited() && nextStackMorale < 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + //unit loses its turn - empty freeze action + BattleAction ba; + ba.actionType = EActionType::BAD_MORALE; + ba.side = next->unitSide(); + ba.stackNumber = next->unitId(); + + makeAutomaticAction(next, ba); + return true; + } + } + + if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk + { + logGlobal->trace("Handle Berserk effect"); + std::pair attackInfo = curB.getNearestStack(next); + if (attackInfo.first != nullptr) + { + BattleAction attack; + attack.actionType = EActionType::WALK_AND_ATTACK; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + attack.aimToHex(attackInfo.second); + attack.aimToUnit(attackInfo.first); + + makeAutomaticAction(next, attack); + logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); + } + else + { + makeStackDoNothing(next); + logGlobal->trace("No target found"); + } + return true; + } + + const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next); + const int stackCreatureId = next->unitType()->getId(); + + if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) + { + BattleAction attack; + attack.actionType = EActionType::SHOOT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + //TODO: select target by priority + + const battle::Unit * target = nullptr; + + for(auto & elem : gameHandler->gameState()->curB->stacks) + { + if(elem->unitType()->getId() != CreatureID::CATAPULT + && elem->unitOwner() != next->unitOwner() + && elem->isValidTarget() + && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition())) + { + target = elem; + break; + } + } + + if(target == nullptr) + { + makeStackDoNothing(next); + } + else + { + attack.aimToUnit(target); + makeAutomaticAction(next, attack); + } + return true; + } + + if (next->unitType()->getId() == CreatureID::CATAPULT) + { + const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); + + if (attackableBattleHexes.empty()) + { + makeStackDoNothing(next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) + { + BattleAction attack; + attack.actionType = EActionType::CATAPULT; + attack.side = next->unitSide(); + attack.stackNumber = next->unitId(); + + makeAutomaticAction(next, attack); + return true; + } + } + + if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) + { + TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s) + { + return s->unitOwner() == next->unitOwner() && s->canBeHealed(); + }); + + if (!possibleStacks.size()) + { + makeStackDoNothing(next); + return true; + } + + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) + { + RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); + const CStack * toBeHealed = possibleStacks.front(); + + BattleAction heal; + heal.actionType = EActionType::STACK_HEAL; + heal.aimToUnit(toBeHealed); + heal.side = next->unitSide(); + heal.stackNumber = next->unitId(); + + makeAutomaticAction(next, heal); + return true; + } + } + + stackTurnTrigger(next); //various effects + + if(next->fear) + { + makeStackDoNothing(next); //end immediately if stack was affected by fear + return true; + } + else + { + logGlobal->trace("Activating %s", next->nodeName()); + auto nextId = next->unitId(); + BattleSetActiveStack sas; + sas.stack = nextId; + gameHandler->sendAndApply(&sas); + return false; + } +} + +void BattleFlowProcessor::onActionMade(const CStack *next) +{ + //we're after action, all results applied + owner->checkBattleStateChanges(); //check if this action ended the battle + + if(next == nullptr) + return; + + //check for good morale + auto nextStackMorale = next->moraleVal(); + if( !next->hadMorale + && !next->defending + && !next->waited() + && !next->fear + && next->alive() + && nextStackMorale > 0) + { + auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); + size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); + + if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) + { + BattleTriggerEffect bte; + bte.stackID = next->unitId(); + bte.effect = vstd::to_underlying(BonusType::MORALE); + bte.val = 1; + bte.additionalInfo = 0; + gameHandler->sendAndApply(&bte); //play animation + } + } + + if (gameHandler->gameLobby()->state != EServerState::SHUTDOWN) + owner->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); +} + +void BattleFlowProcessor::makeStackDoNothing(const CStack * next) +{ + BattleAction doNothing; + doNothing.actionType = EActionType::NO_ACTION; + doNothing.side = next->unitSide(); + doNothing.stackNumber = next->unitId(); + + makeAutomaticAction(next, doNothing); +} + +bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) +{ + BattleSetActiveStack bsa; + bsa.stack = stack->unitId(); + bsa.askPlayerInterface = false; + gameHandler->sendAndApply(&bsa); + + bool ret = owner->makeBattleAction(ba); + owner->checkBattleStateChanges(); + return ret; +} + +void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) +{ + auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); + for(auto b : bl) + { + const CSpell * sp = SpellID(b->subtype).toSpell(); + if(!sp) + continue; + + const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); + const int32_t level = ((val > 3) ? (val - 3) : val); + + spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp); + //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle + battleCast.setEffectDuration(50); + battleCast.setSpellLevel(level); + spells::Target target; + + if(val > 3) + { + for(auto s : gameHandler->gameState()->curB->battleGetAllStacks()) + if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + target.emplace_back(s); + } + else + { + target.emplace_back(st); + } + battleCast.applyEffects(gameHandler->spellEnv, target, false, true); + } +} + +void BattleFlowProcessor::removeObstacle(const CObstacleInstance & obstacle) +{ + BattleObstaclesChanged obsRem; + obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); + gameHandler->sendAndApply(&obsRem); +} + +void BattleFlowProcessor::stackTurnTrigger(const CStack *st) +{ + BattleTriggerEffect bte; + bte.stackID = st->unitId(); + bte.effect = -1; + bte.val = 0; + bte.additionalInfo = 0; + if (st->alive()) + { + //unbind + if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) + { + bool unbind = true; + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); + auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st); + + for (auto b : bl) + { + if(b->additionalInfo != CAddInfo::NONE) + { + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent + if(stack) + { + if(vstd::contains(adjacent, stack)) //binding stack is still present + unbind = false; + } + } + else + { + unbind = false; + } + } + if (unbind) + { + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::UNBIND; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + + if (st->hasBonusOfType(BonusType::POISON)) + { + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) //TODO: what if not?... + { + bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); + if (bte.val < b->val) //(negative) poison effect increases - update it + { + bte.effect = vstd::to_underlying(BonusType::POISON); + gameHandler->sendAndApply(&bte); + } + } + } + if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) + { + const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st)); + const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent); + if(opponentHero) + { + ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); + vstd::amin(manaDrained, opponentHero->mana); + if(manaDrained) + { + bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); + bte.val = manaDrained; + bte.additionalInfo = opponentHero->id.getNum(); //for sanity + gameHandler->sendAndApply(&bte); + } + } + } + if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) + { + bool fearsomeCreature = false; + for (CStack * stack : gameHandler->gameState()->curB->stacks) + { + if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) + { + fearsomeCreature = true; + break; + } + } + if (fearsomeCreature) + { + if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% + { + bte.effect = vstd::to_underlying(BonusType::FEAR); + gameHandler->sendAndApply(&bte); + } + } + } + BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); + int side = gameHandler->gameState()->curB->whatSide(st->unitOwner()); + if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0) + { + bool cast = false; + while(!bl.empty() && !cast) + { + auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); + auto spellID = SpellID(bonus->subtype); + const CSpell * spell = SpellID(spellID).toSpell(); + bl.remove_if([&bonus](const Bonus * b) + { + return b == bonus.get(); + }); + spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell); + parameters.setSpellLevel(bonus->val); + parameters.massive = true; + parameters.smart = true; + //todo: recheck effect level + if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) + { + cast = true; + + int cooldown = bonus->additionalInfo[0]; + BattleSetStackProperty ssp; + ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; + ssp.absolute = false; + ssp.val = cooldown; + ssp.stackID = st->unitId(); + gameHandler->sendAndApply(&ssp); + } + } + } + } +} diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h new file mode 100644 index 000000000..aca2e9772 --- /dev/null +++ b/server/battles/BattleFlowProcessor.h @@ -0,0 +1,49 @@ +/* + * BattleFlowProcessor.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 + +VCMI_LIB_NAMESPACE_BEGIN +class CStack; +struct BattleHex; +class BattleAction; +struct CObstacleInstance; +VCMI_LIB_NAMESPACE_END + +class CGameHandler; +class BattleProcessor; + +class BattleFlowProcessor : boost::noncopyable +{ + BattleProcessor * owner; + CGameHandler * gameHandler; + + const CStack * getNextStack(); + + bool tryMakeAutomaticAction(const CStack * stack); + + void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void activateNextStack(); + void startNextRound(bool isFirstRound); + + void stackEnchantedTrigger(const CStack * stack); + void removeObstacle(const CObstacleInstance &obstacle); + void stackTurnTrigger(const CStack *stack); + + void makeStackDoNothing(const CStack * next); + bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + +public: + BattleFlowProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + void onBattleStarted(); + void onTacticsEnded(); + void onActionMade(const CStack *stack); +}; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index b43477dd3..dcc9513db 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -10,6 +10,10 @@ #include "StdInc.h" #include "BattleProcessor.h" +#include "BattleActionProcessor.h" +#include "BattleFlowProcessor.h" +#include "BattleResultProcessor.h" + #include "../CGameHandler.h" #include "../CVCMIServer.h" #include "../processors/HeroPoolProcessor.h" @@ -43,284 +47,21 @@ #define COMPLAIN_RET(txt) {gameHandler->complain(txt); return false;} #define COMPLAIN_RETF(txt, FORMAT) {gameHandler->complain(boost::str(boost::format(txt) % FORMAT)); return false;} -CondSh battleMadeAction(false); -CondSh battleResult(nullptr); -boost::recursive_mutex battleActionMutex; -static EndAction end_action; - -static void giveExp(BattleResult &r) -{ - if (r.winner > 1) - { - // draw - return; - } - r.exp[0] = 0; - r.exp[1] = 0; - for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) - { - r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; - } -} - -static void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard -{ - int x = targetPosition.getX(); - int y = targetPosition.getY(); - - const bool targetIsAttacker = side == BattleSide::ATTACKER; - - if (targetIsAttacker) //handle front guardians, TODO: should we handle situation when units start battle near opposite side of the battlefield? Cannot happen in normal H3... - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - - //guardian spawn locations for four default position cases for attacker and defender, non-default starting location for att and def is handled in first two if's - if (targetIsAttacker && ((y % 2 == 0) || (x > 1))) - { - if (targetIsTwoHex && (y % 2 == 1) && (x == 2)) //handle exceptional case - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } - else - { //add back-side guardians for two-hex target, side guardians for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_LEFT : BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_LEFT : BattleHex::EDir::BOTTOM_RIGHT, false), output); - - if (!targetIsTwoHex && x > 2) //back guard for one-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false), output); - else if (targetIsTwoHex)//front-side guardians for two-hex target - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - if (x > 3) //back guard for two-hex - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::LEFT, false), output); - } - } - - } - - else if (!targetIsAttacker && ((y % 2 == 1) || (x < GameConstants::BFIELD_WIDTH - 2))) - { - if (targetIsTwoHex && (y % 2 == 0) && (x == GameConstants::BFIELD_WIDTH - 3)) //handle exceptional case... equivalent for above for defender side - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - else - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::TOP_RIGHT : BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(targetIsTwoHex ? BattleHex::EDir::BOTTOM_RIGHT : BattleHex::EDir::BOTTOM_LEFT, false), output); - - if (!targetIsTwoHex && x < GameConstants::BFIELD_WIDTH - 3) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false), output); - else if (targetIsTwoHex) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - if (x < GameConstants::BFIELD_WIDTH - 4) - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::RIGHT, false), output); - } - } - } - - else if (!targetIsAttacker && y % 2 == 0) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::TOP_LEFT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::LEFT, false).cloneInDirection(BattleHex::EDir::BOTTOM_LEFT, false), output); - } - - else if (targetIsAttacker && y % 2 == 1) - { - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::TOP_RIGHT, false), output); - BattleHex::checkAndPush(targetPosition.cloneInDirection(BattleHex::EDir::RIGHT, false).cloneInDirection(BattleHex::EDir::BOTTOM_RIGHT, false), output); - } -} - - -CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): - army(battleSide.armyObject) -{ - heroWithDeadCommander = ObjectInstanceID(); - - PlayerColor color = battleSide.color; - - for(CStack * st : bat->stacks) - { - if(st->summoned) //don't take into account temporary summoned stacks - continue; - if(st->unitOwner() != color) //remove only our stacks - continue; - - logGlobal->debug("Calculating casualties for %s", st->nodeName()); - - st->health.takeResurrected(); - - if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) - { - logGlobal->debug("Ignored arrow towers stack."); - } - else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) - { - auto warMachine = st->unitType()->warMachine; - - if(warMachine == ArtifactID::NONE) - { - logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); - } - //catapult artifact remain even if "creature" killed in siege - else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) - { - logGlobal->debug("War machine has been destroyed"); - auto hero = dynamic_ptr_cast (army); - if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); - else - logGlobal->error("War machine in army without hero"); - } - } - else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) - { - if(st->alive() && st->getCount() > 0) - { - logGlobal->debug("Permanently summoned %d units.", st->getCount()); - const CreatureID summonedType = st->creatureId(); - summoned[summonedType] += st->getCount(); - } - } - else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - if (nullptr == st->base) - { - logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); - } - else - { - auto c = dynamic_cast (st->base); - if(c) - { - auto h = dynamic_cast (army); - if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) - { - logGlobal->debug("Commander is dead."); - heroWithDeadCommander = army->id; //TODO: unify commander handling - } - } - else - logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); - } - } - else if(st->base && !army->slotEmpty(st->unitSlot())) - { - logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); - if(st->getCount() == 0 || !st->alive()) - { - logGlobal->debug("Stack has been destroyed."); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); - } - else if(st->getCount() < army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - else if(st->getCount() > army->getStackCount(st->unitSlot())) - { - logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); - StackLocation sl(army, st->unitSlot()); - newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); - } - } - else - { - logGlobal->warn("Unable to process stack: %s", st->nodeName()); - } - } -} - -void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) -{ - for (TStackAndItsNewCount &ncount : newStackCounts) - { - if (ncount.second > 0) - gh->changeStackCount(ncount.first, ncount.second, true); - else - gh->eraseStack(ncount.first, true); - } - for (auto summoned_iter : summoned) - { - SlotID slot = army->getSlotFor(summoned_iter.first); - if (slot.validSlot()) - { - StackLocation location(army, slot); - gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); - } - else - { - //even if it will be possible to summon anything permanently it should be checked for free slot - //necromancy is handled separately - gh->complain("No free slot to put summoned creature"); - } - } - for (auto al : removedWarMachines) - { - gh->removeArtifact(al); - } - if (heroWithDeadCommander != ObjectInstanceID()) - { - SetCommanderProperty scp; - scp.heroid = heroWithDeadCommander; - scp.which = SetCommanderProperty::ALIVE; - scp.amount = 0; - gh->sendAndApply(&scp); - } -} - -BattleProcessor::BattleProcessor(CGameHandler * gameHandler): - visitObjectAfterVictory(false), - gameHandler(gameHandler) +BattleProcessor::BattleProcessor(CGameHandler * gameHandler) + : gameHandler(gameHandler) + , flowProcessor(std::make_unique(this)) + , actionsProcessor(std::make_unique(this)) + , resultProcessor(std::make_unique(this)) { + setGameHandler(gameHandler); } BattleProcessor::BattleProcessor(): - visitObjectAfterVictory(false), - gameHandler(nullptr) + BattleProcessor(nullptr) { } -BattleProcessor::~BattleProcessor() -{ - if (battleThread) - { - //Setting battleMadeAction is needed because battleThread waits for the action to continue the main loop - battleMadeAction.setn(true); - battleThread->join(); - } -} - -FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int remainingBattleQueriesCount) -{ - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; - winnerSide = result.winner; - this->remainingBattleQueriesCount = remainingBattleQueriesCount; -} - -FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; - winnerSide = 0; - remainingBattleQueriesCount = 0; -} +BattleProcessor::~BattleProcessor() = default; void BattleProcessor::engageIntoBattle(PlayerColor player) { @@ -383,7 +124,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm } gameHandler->queries->addQuery(nextBattleQuery); - this->battleThread = std::make_unique(boost::thread(&BattleProcessor::runBattle, this)); + flowProcessor->onBattleStarted(); } void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) @@ -399,1592 +140,8 @@ void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInst startBattleI(army1, army2, army2->visitablePos(), creatureBank); } -void BattleProcessor::runBattle() -{ - boost::unique_lock lock(battleActionMutex); - - gameHandler->setBattle(gameHandler->gameState()->curB); - assert(gameHandler->gameState()->curB); - //TODO: pre-tactic stuff, call scripts etc. - - //Moat should be initialized here, because only here we can use spellcasting - if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) - { - const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER); - const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell()); - auto target = spells::Target(); - cast.cast(gameHandler->spellEnv, target); - } - - //tactic round - { - while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && gameHandler->gameState()->curB->tacticDistance && !battleResult.get()) - { - auto unlockGuard = vstd::makeUnlockGuard(battleActionMutex); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); - } - } - - //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing - - for (CStack * stack : initialStacks) - { - if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) - { - std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = gameHandler->getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); - std::vector targetHexes; - const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard - const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); - - /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. - For one-hex targets there are four guardians - front, back and one per side (up + down). - Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front - Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ - if (!guardianIsBig) - targetHexes = stack->getSurroundingHexes(); - else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); - - for(auto hex : targetHexes) - { - if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex - { - battle::UnitInfo info; - info.id = gameHandler->gameState()->curB->battleNextUnitId(); - info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); - info.type = creatureData; - info.side = stack->unitSide(); - info.position = hex; - info.summoned = true; - - BattleUnitsChanged pack; - pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(pack.changedStacks.back().data); - gameHandler->sendAndApply(&pack); - } - } - } - - stackEnchantedTrigger(stack); - } - - //spells opening battle - for (int i = 0; i < 2; ++i) - { - auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); - if (h) - { - TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); - - for (auto b : *bl) - { - spells::BonusCaster caster(h, b); - - const CSpell * spell = SpellID(b->subtype).toSpell(); - - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); - parameters.setSpellLevel(3); - parameters.setEffectDuration(b->val); - parameters.massive = true; - parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); - } - } - } - // it is possible that due to opening spells one side was eliminated -> check for end of battle - checkBattleStateChanges(); - - bool firstRound = true;//FIXME: why first round is -1? - - //main loop - while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && !battleResult.get()) //till the end of the battle ;] - { - BattleNextRound bnr; - bnr.round = gameHandler->gameState()->curB->round + 1; - logGlobal->debug("Round %d", bnr.round); - gameHandler->sendAndApply(&bnr); - - auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it - for (auto &obstPtr : obstacles) - { - if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) - if (sco->turnsRemaining == 0) - removeObstacle(*obstPtr); - } - - const BattleInfo & curB = *gameHandler->gameState()->curB; - - for(auto stack : curB.stacks) - { - if(stack->alive() && !firstRound) - stackEnchantedTrigger(stack); - } - - //stack loop - - auto getNextStack = [this]() -> const CStack * - { - if(battleResult.get()) - return nullptr; - - std::vector q; - gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" - - if(!q.empty()) - { - if(!q.front().empty()) - { - auto next = q.front().front(); - const auto stack = dynamic_cast(next); - - // regeneration takes place before everything else but only during first turn attempt in each round - // also works under blind and similar effects - if(stack && stack->alive() && !stack->waiting) - { - BattleTriggerEffect bte; - bte.stackID = stack->unitId(); - bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); - - const int32_t lostHealth = stack->getMaxHealth() - stack->getFirstHPleft(); - if(stack->hasBonusOfType(BonusType::HP_REGENERATION)) - bte.val = std::min(lostHealth, stack->valOfBonuses(BonusType::HP_REGENERATION)); - - if(bte.val) // anything to heal - gameHandler->sendAndApply(&bte); - } - - if(next->willMove()) - return stack; - } - } - - return nullptr; - }; - - const CStack * next = nullptr; - while((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && (next = getNextStack())) - { - BattleUnitsChanged removeGhosts; - for(auto stack : curB.stacks) - { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } - - if(!removeGhosts.changedStacks.empty()) - gameHandler->sendAndApply(&removeGhosts); - - // check for bad morale => freeze - int nextStackMorale = next->moraleVal(); - if(!next->hadMorale && !next->waited() && nextStackMorale < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); - - if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - //unit loses its turn - empty freeze action - BattleAction ba; - ba.actionType = EActionType::BAD_MORALE; - ba.side = next->unitSide(); - ba.stackNumber = next->unitId(); - - makeAutomaticAction(next, ba); - continue; - } - } - - if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk - { - logGlobal->trace("Handle Berserk effect"); - std::pair attackInfo = curB.getNearestStack(next); - if (attackInfo.first != nullptr) - { - BattleAction attack; - attack.actionType = EActionType::WALK_AND_ATTACK; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - attack.aimToHex(attackInfo.second); - attack.aimToUnit(attackInfo.first); - - makeAutomaticAction(next, attack); - logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); - } - else - { - makeStackDoNothing(next); - logGlobal->trace("No target found"); - } - continue; - } - - const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next); - const int stackCreatureId = next->unitType()->getId(); - - if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) - { - BattleAction attack; - attack.actionType = EActionType::SHOOT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - //TODO: select target by priority - - const battle::Unit * target = nullptr; - - for(auto & elem : gameHandler->gameState()->curB->stacks) - { - if(elem->unitType()->getId() != CreatureID::CATAPULT - && elem->unitOwner() != next->unitOwner() - && elem->isValidTarget() - && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition())) - { - target = elem; - break; - } - } - - if(target == nullptr) - { - makeStackDoNothing(next); - } - else - { - attack.aimToUnit(target); - makeAutomaticAction(next, attack); - } - continue; - } - - if (next->unitType()->getId() == CreatureID::CATAPULT) - { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); - - if (attackableBattleHexes.empty()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) - { - BattleAction attack; - attack.actionType = EActionType::CATAPULT; - attack.side = next->unitSide(); - attack.stackNumber = next->unitId(); - - makeAutomaticAction(next, attack); - continue; - } - } - - if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) - { - TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s) - { - return s->unitOwner() == next->unitOwner() && s->canBeHealed(); - }); - - if (!possibleStacks.size()) - { - makeStackDoNothing(next); - continue; - } - - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) - { - RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); - const CStack * toBeHealed = possibleStacks.front(); - - BattleAction heal; - heal.actionType = EActionType::STACK_HEAL; - heal.aimToUnit(toBeHealed); - heal.side = next->unitSide(); - heal.stackNumber = next->unitId(); - - makeAutomaticAction(next, heal); - continue; - } - } - - int numberOfAsks = 1; - bool breakOuter = false; - do - {//ask interface and wait for answer - if (!battleResult.get()) - { - stackTurnTrigger(next); //various effects - - if(next->fear) - { - makeStackDoNothing(next); //end immediately if stack was affected by fear - } - else - { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - gameHandler->sendAndApply(&sas); - - auto actionWasMade = [&]() -> bool - { - if (battleMadeAction.data)//active stack has made its action - return true; - if (battleResult.get())// battle is finished - return true; - if (next == nullptr)//active stack was been removed - return true; - return !next->alive();//active stack is dead - }; - - boost::unique_lock lock(battleMadeAction.mx); - battleMadeAction.data = false; - while ((gameHandler->gameLobby()->state != EServerState::SHUTDOWN) && !actionWasMade()) - { - { - boost::unique_lock actionLock(battleActionMutex); - battleMadeAction.cond.wait(lock); - } - if (gameHandler->battleGetStackByID(nextId, false) != next) - next = nullptr; //it may be removed, while we wait - } - } - } - - if (battleResult.get()) //don't touch it, battle could be finished while waiting got action - { - breakOuter = true; - break; - } - //we're after action, all results applied - checkBattleStateChanges(); //check if this action ended the battle - - if(next != nullptr) - { - //check for good morale - nextStackMorale = next->moraleVal(); - if( !battleResult.get() - && !next->hadMorale - && !next->defending - && !next->waited() - && !next->fear - && next->alive() - && nextStackMorale > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); - - if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - { - BattleTriggerEffect bte; - bte.stackID = next->unitId(); - bte.effect = vstd::to_underlying(BonusType::MORALE); - bte.val = 1; - bte.additionalInfo = 0; - gameHandler->sendAndApply(&bte); //play animation - - ++numberOfAsks; //move this stack once more - } - } - } - --numberOfAsks; - } while (numberOfAsks > 0); - - if (breakOuter) - { - break; - } - - } - firstRound = false; - } - - if (gameHandler->gameLobby()->state != EServerState::SHUTDOWN) - endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); -} - -bool BattleProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) -{ - boost::unique_lock lock(battleActionMutex); - - BattleSetActiveStack bsa; - bsa.stack = stack->unitId(); - bsa.askPlayerInterface = false; - gameHandler->sendAndApply(&bsa); - - bool ret = makeBattleActionImpl(ba); - checkBattleStateChanges(); - return ret; -} - - -void BattleProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) -{ - if(attacker->hasBonusOfType(attackMode)) - { - std::set spellsToCast; - TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); - for(const auto & sf : *spells) - { - spellsToCast.insert(SpellID(sf->subtype)); - } - for(SpellID spellID : spellsToCast) - { - bool castMe = false; - if(!defender->alive()) - { - logGlobal->debug("attackCasting: all attacked creatures have been killed"); - return; - } - int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); - for(const auto & sf : *spellsByType) - { - int meleeRanged; - if(sf->additionalInfo.size() < 2) - { - // legacy format - vstd::amax(spellLevel, sf->additionalInfo[0] % 1000); - meleeRanged = sf->additionalInfo[0] / 1000; - } - else - { - vstd::amax(spellLevel, sf->additionalInfo[0]); - meleeRanged = sf->additionalInfo[1]; - } - if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) - castMe = true; - } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); - vstd::amin(chance, 100); - - const CSpell * spell = SpellID(spellID).toSpell(); - spells::AbilityCaster caster(attacker, spellLevel); - - spells::Target target; - target.emplace_back(defender); - - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); - - auto m = spell->battleMechanics(¶meters); - - spells::detail::ProblemImpl ignored; - - if(!m->canBeCastAt(target, ignored)) - continue; - - //check if spell should be cast (probability handling) - if(gameHandler->getRandomGenerator().nextInt(99) >= chance) - continue; - - //casting - if(castMe) - { - parameters.cast(gameHandler->spellEnv, target); - } - } - } -} - -void BattleProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? -} - -void BattleProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) -{ - if(!attacker->alive() || !defender->alive()) // can be already dead - return; - - attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); - - if(!defender->alive()) - { - //don't try death stare or acid breath on dead stack (crash!) - return; - } - - if(attacker->hasBonusOfType(BonusType::DEATH_STARE)) - { - // mechanics of Death Stare as in H3: - // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution - //original formula x = min(x, (gorgons_count + 9)/10); - - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; - vstd::amin(chanceToKill, 1); //cap at 100% - - std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); - - int staredCreatures = distribution(gameHandler->getRandomGenerator().getStdGenerator()); - - double cap = 1 / std::max(chanceToKill, (double)(0.01));//don't divide by 0 - int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count - vstd::amin(staredCreatures, maxToKill); - - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); - if(staredCreatures) - { - //TODO: death stare was not originally available for multiple-hex attacks, but... - const CSpell * spell = SpellID(SpellID::DEATH_STARE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - parameters.setEffectValue(staredCreatures); - parameters.cast(gameHandler->spellEnv, target); - } - } - - if(!defender->alive()) - return; - - int64_t acidDamage = 0; - TConstBonusListPtr acidBreath = attacker->getBonuses(Selector::type()(BonusType::ACID_BREATH)); - for(const auto & b : *acidBreath) - { - if(b->additionalInfo[0] > gameHandler->getRandomGenerator().nextInt(99)) - acidDamage += b->val; - } - - if(acidDamage > 0) - { - const CSpell * spell = SpellID(SpellID::ACID_BREATH_DAMAGE).toSpell(); - - spells::AbilityCaster caster(attacker, 0); - - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); - spells::Target target; - target.emplace_back(defender); - - parameters.setEffectValue(acidDamage * attacker->getCount()); - parameters.cast(gameHandler->spellEnv, target); - } - - - if(!defender->alive()) - return; - - if(attacker->hasBonusOfType(BonusType::TRANSMUTATION) && defender->isLiving()) //transmutation mechanics, similar to WoG werewolf ability - { - double chanceToTrigger = attacker->valOfBonuses(BonusType::TRANSMUTATION) / 100.0f; - vstd::amin(chanceToTrigger, 1); //cap at 100% - - if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; - - if(defender->unitType()->getId() == bonusAdditionalInfo || - (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) - return; - - battle::UnitInfo resurrectInfo; - resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId(); - resurrectInfo.summoned = false; - resurrectInfo.position = defender->getPosition(); - resurrectInfo.side = defender->unitSide(); - - if(bonusAdditionalInfo != CAddInfo::NONE) - resurrectInfo.type = CreatureID(bonusAdditionalInfo); - else - resurrectInfo.type = attacker->creatureId(); - - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) - resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) - resurrectInfo.count = defender->getCount(); - else - return; //wrong subtype - - BattleUnitsChanged addUnits; - addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); - resurrectInfo.save(addUnits.changedStacks.back().data); - - BattleUnitsChanged removeUnits; - removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); - gameHandler->sendAndApply(&removeUnits); - gameHandler->sendAndApply(&addUnits); - } - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) - { - double chanceToTrigger = 0; - int amountToDie = 0; - - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; - amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); - } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count - { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; - } - - vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% - - if(gameHandler->getRandomGenerator().getDoubleRange(0, 1)() > chanceToTrigger) - return; - - BattleStackAttacked bsa; - bsa.attackerID = -1; - bsa.stackAttacked = defender->unitId(); - bsa.damageAmount = amountToDie * defender->getMaxHealth(); - bsa.flags = BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID::SLAYER; - defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); - - StacksInjured si; - si.stacks.push_back(bsa); - - gameHandler->sendAndApply(&si); - sendGenericKilledLog(defender, bsa.killedAmount, false); - } -} - -void BattleProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) -{ - if(first && !counter) - handleAttackBeforeCasting(ranged, attacker, defender); - - FireShieldInfo fireShield; - BattleAttack bat; - BattleLogMessage blm; - bat.stackAttacking = attacker->unitId(); - bat.tile = targetHex; - - std::shared_ptr attackerState = attacker->acquireState(); - - if(ranged) - bat.flags |= BattleAttack::SHOT; - if(counter) - bat.flags |= BattleAttack::COUNTER; - - const int attackerLuck = attacker->luckVal(); - - if(attackerLuck > 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); - - if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::LUCKY; - } - - if(attackerLuck < 0) - { - auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); - - if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) - bat.flags |= BattleAttack::UNLUCKY; - } - - if (gameHandler->getRandomGenerator().nextInt(99) < attacker->valOfBonuses(BonusType::DOUBLE_DAMAGE_CHANCE)) - { - bat.flags |= BattleAttack::DEATH_BLOW; - } - - const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner()); - if(owner) - { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); - if (chance > gameHandler->getRandomGenerator().nextInt(99)) - bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; - } - - int64_t drainedLife = 0; - - // only primary target - if(defender->alive()) - drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); - - //multiple-hex normal attack - std::set attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - - std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); - if(bonus && ranged) //TODO: make it work in melee? - { - //this is need for displaying hit animation - bat.flags |= BattleAttack::SPELL_LIKE; - bat.spellID = SpellID(bonus->subtype); - - //TODO: should spell override creature`s projectile? - - auto spell = bat.spellID.toSpell(); - - battle::Target target; - target.emplace_back(defender, targetHex); - - spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); - event.setSpellLevel(bonus->val); - - auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); - - //TODO: get exact attacked hex for defender - - for(const CStack * stack : attackedCreatures) - { - if(stack != defender && stack->alive()) //do not hit same stack twice - { - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); - } - } - - //now add effect info for all attacked stacks - for (BattleStackAttacked & bsa : bat.bsa) - { - if (bsa.attackerID == attacker->unitId()) //this is our attack and not f.e. fire shield - { - //this is need for displaying affect animation - bsa.flags |= BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID(bonus->subtype); - } - } - } - - attackerState->afterAttack(ranged, counter); - - { - UnitChanges info(attackerState->unitId(), UnitChanges::EOperation::RESET_STATE); - attackerState->save(info.data); - bat.attackerChanges.changedStacks.push_back(info); - } - - if (drainedLife > 0) - bat.flags |= BattleAttack::LIFE_DRAIN; - - gameHandler->sendAndApply(&bat); - - { - const bool multipleTargets = bat.bsa.size() > 1; - - int64_t totalDamage = 0; - int32_t totalKills = 0; - - for(const BattleStackAttacked & bsa : bat.bsa) - { - totalDamage += bsa.damageAmount; - totalKills += bsa.killedAmount; - } - - { - MetaString text; - attacker->addText(text, EMetaText::GENERAL_TXT, 376); - attacker->addNameReplacement(text); - text.replaceNumber(totalDamage); - blm.lines.push_back(text); - } - - addGenericKilledLog(blm, defender, totalKills, multipleTargets); - } - - // drain life effect (as well as log entry) must be applied after the attack - if(drainedLife > 0) - { - MetaString text; - attackerState->addText(text, EMetaText::GENERAL_TXT, 361); - attackerState->addNameReplacement(text, false); - text.replaceNumber(drainedLife); - defender->addNameReplacement(text, true); - blm.lines.push_back(std::move(text)); - } - - if(!fireShield.empty()) - { - //todo: this should be "virtual" spell instead, we only need fire spell school bonus here - const CSpell * fireShieldSpell = SpellID(SpellID::FIRE_SHIELD).toSpell(); - int64_t totalDamage = 0; - - for(const auto & item : fireShield) - { - const CStack * actor = item.first; - int64_t rawDamage = item.second; - - const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner()); - - if(actorOwner) - { - rawDamage = fireShieldSpell->adjustRawDamage(actorOwner, attacker, rawDamage); - } - else - { - rawDamage = fireShieldSpell->adjustRawDamage(actor, attacker, rawDamage); - } - - totalDamage+=rawDamage; - //FIXME: add custom effect on actor - } - - if (totalDamage > 0) - { - BattleStackAttacked bsa; - - bsa.flags |= BattleStackAttacked::FIRE_SHIELD; - bsa.stackAttacked = attacker->unitId(); //invert - bsa.attackerID = defender->unitId(); - bsa.damageAmount = totalDamage; - attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); - - StacksInjured pack; - pack.stacks.push_back(bsa); - gameHandler->sendAndApply(&pack); - - // TODO: this is already implemented in Damage::describeEffect() - { - MetaString text; - text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); - text.replaceNumber(totalDamage); - blm.lines.push_back(std::move(text)); - } - addGenericKilledLog(blm, attacker, bsa.killedAmount, false); - } - } - - gameHandler->sendAndApply(&blm); - - handleAfterAttackCasting(ranged, attacker, defender); -} - -int64_t BattleProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) -{ - BattleStackAttacked bsa; - if(secondary) - bsa.flags |= BattleStackAttacked::SECONDARY; //all other targets do not suffer from spells & spell-like abilities - - bsa.attackerID = attackerState->unitId(); - bsa.stackAttacked = def->unitId(); - { - BattleAttackInfo bai(attackerState.get(), def, distance, bat.shot()); - - bai.deathBlow = bat.deathBlow(); - bai.doubleDamage = bat.ballistaDoubleDmg(); - bai.luckyStrike = bat.lucky(); - bai.unluckyStrike = bat.unlucky(); - - auto range = gameHandler->gameState()->curB->calculateDmgRange(bai); - bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); - CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties - } - - int64_t drainedLife = 0; - - //life drain handling - if(attackerState->hasBonusOfType(BonusType::LIFE_DRAIN) && def->isLiving()) - { - int64_t toHeal = bsa.damageAmount * attackerState->valOfBonuses(BonusType::LIFE_DRAIN) / 100; - attackerState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - drainedLife += toHeal; - } - - //soul steal handling - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL) && def->isLiving()) - { - //we can have two bonuses - one with subtype 0 and another with subtype 1 - //try to use permanent first, use only one of two - for(si32 subtype = 1; subtype >= 0; subtype--) - { - if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) - { - int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); - drainedLife += toHeal; - break; - } - } - } - bat.bsa.push_back(bsa); //add this stack to the list of victims after drain life has been calculated - - //fire shield handling - if(!bat.shot() && - !def->isClone() && - def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && - CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) - ) - { - //TODO: use damage with bonus but without penalties - auto fireShieldDamage = (std::min(def->getAvailableHealth(), bsa.damageAmount) * def->valOfBonuses(BonusType::FIRE_SHIELD)) / 100; - fireShield.push_back(std::make_pair(def, fireShieldDamage)); - } - - return drainedLife; -} - -void BattleProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - BattleLogMessage blm; - addGenericKilledLog(blm, defender, killed, multiple); - gameHandler->sendAndApply(&blm); - } -} - -void BattleProcessor::addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple) -{ - if(killed > 0) - { - const int32_t txtIndex = (killed > 1) ? 379 : 378; - std::string formatString = VLC->generaltexth->allTexts[txtIndex]; - - // these default h3 texts have unnecessary new lines, so get rid of them before displaying (and trim just in case, trimming newlines does not works for some reason) - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\n'), formatString.end()); - formatString.erase(std::remove(formatString.begin(), formatString.end(), '\r'), formatString.end()); - boost::algorithm::trim(formatString); - - boost::format txt(formatString); - if(killed > 1) - { - txt % killed % (multiple ? VLC->generaltexth->allTexts[43] : defender->unitType()->getNamePluralTranslated()); // creatures perish - } - else //killed == 1 - { - txt % (multiple ? VLC->generaltexth->allTexts[42] : defender->unitType()->getNameSingularTranslated()); // creature perishes - } - MetaString line; - line.appendRawString(txt.str()); - blm.lines.push_back(std::move(line)); - } -} - -void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) -{ - LOG_TRACE(logGlobal); - - //Fill BattleResult structure with exp info - giveExp(*battleResult.data); - - if (battleResult.get()->result == BattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped - { - if(heroAttacker) - battleResult.data->exp[1] += 500; - if(heroDefender) - battleResult.data->exp[0] += 500; - } - - if(heroAttacker) - battleResult.data->exp[0] = heroAttacker->calculateXp(battleResult.data->exp[0]);//scholar skill - if(heroDefender) - battleResult.data->exp[1] = heroDefender->calculateXp(battleResult.data->exp[1]); - - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); - if (!battleQuery) - { - logGlobal->error("Cannot find battle query!"); - gameHandler->complain("Player " + boost::lexical_cast(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!"); - return; - } - - battleQuery->result = std::make_optional(*battleResult.data); - - //Check how many battle gameHandler->queries were created (number of players blocked by battle) - const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; - finishingBattle = std::make_unique(battleQuery, queriedPlayers); - - // in battles against neutrals, 1st player can ask to replay battle manually - if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer()) - { - auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); - battleResult.data->queryID = battleDialogQuery->queryID; - gameHandler->queries->addQuery(battleDialogQuery); - } - else - battleResult.data->queryID = -1; - - //set same battle result for all gameHandler->queries - for(auto q : gameHandler->queries->allQueries()) - { - auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery) - otherBattleQuery->result = battleQuery->result; - } - - gameHandler->sendAndApply(battleResult.data); //after this point casualties objects are destroyed - - if (battleResult.data->queryID == -1) - endBattleConfirm(gameHandler->gameState()->curB); -} - -void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) -{ - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battleInfo->sides.at(0).color)); - if(!battleQuery) - { - logGlobal->trace("No battle query, battle end was confirmed by another player"); - return; - } - - const BattleResult::EResult result = battleResult.get()->result; - - CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle - ChangeSpells cs; //for Eagle Eye - - if(!finishingBattle->isDraw() && finishingBattle->winnerHero) - { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) - { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battleInfo->sides.at(!battleResult.data->winner).usedSpellsHistory) - { - auto spell = spellId.toSpell(VLC->spells()); - if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) - cs.spells.insert(spell->getId()); - } - } - } - std::vector arts; //display them in window - - if(result == BattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) - { - auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) - { - const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); - if(ArtifactUtils::isSlotBackpack(slot)) - ma->askAssemble = false; - gameHandler->sendAndApply(ma); - } - }; - - if (finishingBattle->loserHero) - { - //TODO: wrap it into a function, somehow (std::variant -_-) - auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig() && - art->artType->getId() != ArtifactID::SPELLBOOK) - // don't move war machines or locked arts (spellbook) - { - sendMoveArtifact(art, &ma); - } - } - for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - { - //we assume that no big artifacts can be found - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); - if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won - { - sendMoveArtifact(art, &ma); - } - } - if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? - { - artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - for (auto armySlot : battleInfo->sides.at(!battleResult.data->winner).armyObject->stacks) - { - auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) - { - MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); - if (art && !art->artType->isBig()) - { - sendMoveArtifact(art, &ma); - } - } - } - } - - if (arts.size()) //display loot - { - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - - iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact - - for (auto art : arts) //TODO; separate function to display loot for various ojects? - { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); - if (iw.components.size() >= 14) - { - gameHandler->sendAndApply(&iw); - iw.components.clear(); - } - } - if (iw.components.size()) - { - gameHandler->sendAndApply(&iw); - } - } - //Eagle Eye secondary skill handling - if (!cs.spells.empty()) - { - cs.learn = 1; - cs.hid = finishingBattle->winnerHero->id; - - InfoWindow iw; - iw.player = finishingBattle->winnerHero->tempOwner; - iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s - iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); - - std::ostringstream names; - for (int i = 0; i < cs.spells.size(); i++) - { - names << "%s"; - if (i < cs.spells.size() - 2) - names << ", "; - else if (i < cs.spells.size() - 1) - names << "%s"; - } - names << "."; - - iw.text.replaceRawString(names.str()); - - auto it = cs.spells.begin(); - for (int i = 0; i < cs.spells.size(); i++, it++) - { - iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); - if (i == cs.spells.size() - 2) //we just added pre-last name - iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); - } - gameHandler->sendAndApply(&iw); - gameHandler->sendAndApply(&cs); - } - cab1.updateArmy(gameHandler); - cab2.updateArmy(gameHandler); //take casualties after battle is deleted - - if(finishingBattle->loserHero) //remove beaten hero - { - RemoveObject ro(finishingBattle->loserHero->id); - gameHandler->sendAndApply(&ro); - } - if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed - { - RemoveObject ro(finishingBattle->winnerHero->id); - gameHandler->sendAndApply(&ro); - } - - if(battleResult.data->winner == BattleSide::DEFENDER - && finishingBattle->winnerHero - && finishingBattle->winnerHero->visitedTown - && !finishingBattle->winnerHero->inTownGarrison - && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) - { - gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place - } - //give exp - if(!finishingBattle->isDraw() && battleResult.data->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) - gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult.data->exp[finishingBattle->winnerSide]); - - BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); - raccepted.heroResult[0].exp = battleResult.data->exp[0]; - raccepted.heroResult[1].exp = battleResult.data->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; - gameHandler->sendAndApply(&raccepted); - - gameHandler->queries->popIfTop(battleQuery); - //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query -} - -void BattleProcessor::battleAfterLevelUp(const BattleResult &result) -{ - LOG_TRACE(logGlobal); - - if(!finishingBattle) - return; - - finishingBattle->remainingBattleQueriesCount--; - logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); - - if (finishingBattle->remainingBattleQueriesCount > 0) - //Battle results will be handled when all battle gameHandler->queries are closed - return; - - //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible - // but the battle consequences are applied after final player is unblocked. Hard to abuse... - // Still, it looks like a hole. - - // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult.data) : CStackBasicDescriptor(); - // Give raised units to winner and show dialog, if any were raised, - // units will be given after casualties are taken - const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); - - if (necroSlot != SlotID()) - { - finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); - gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); - } - - BattleResultsApplied resultsApplied; - resultsApplied.player1 = finishingBattle->victor; - resultsApplied.player2 = finishingBattle->loser; - gameHandler->sendAndApply(&resultsApplied); - - gameHandler->setBattle(nullptr); - - if (visitObjectAfterVictory && result.winner==0 && !finishingBattle->winnerHero->stacks.empty()) - { - logGlobal->trace("post-victory visit"); - gameHandler->visitObjectOnTile(*gameHandler->getTile(finishingBattle->winnerHero->visitablePos()), finishingBattle->winnerHero); - } - visitObjectAfterVictory = false; - - //handle victory/loss of engaged players - std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; - gameHandler->checkVictoryLossConditions(playerColors); - - if (result.result == BattleResult::SURRENDER) - gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); - - if (result.result == BattleResult::ESCAPE) - gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); - - if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() - && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) - { - RemoveObject ro(finishingBattle->winnerHero->id); - gameHandler->sendAndApply(&ro); - - if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) - gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); - } - - finishingBattle.reset(); -} - -int BattleProcessor::moveStack(int stack, BattleHex dest) -{ - int ret = 0; - - const CStack *curStack = gameHandler->battleGetStackByID(stack), - *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); - - assert(curStack); - assert(dest < GameConstants::BFIELD_SIZE); - - if (gameHandler->gameState()->curB->tacticDistance) - { - assert(gameHandler->gameState()->curB->isInTacticRange(dest)); - } - - auto start = curStack->getPosition(); - if (start == dest) - return 0; - - //initing necessary tables - auto accessibility = gameHandler->getAccesibility(curStack); - std::set passed; - //Ignore obstacles on starting position - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - - //shifting destination (if we have double wide stack and we can occupy dest but not be exactly there) - if(!stackAtEnd && curStack->doubleWide() && !accessibility.accessible(dest, curStack)) - { - BattleHex shifted = dest.cloneInDirection(curStack->destShiftDir(), false); - - if(accessibility.accessible(shifted, curStack)) - dest = shifted; - } - - if((stackAtEnd && stackAtEnd!=curStack && stackAtEnd->alive()) || !accessibility.accessible(dest, curStack)) - { - gameHandler->complain("Given destination is not accessible!"); - return 0; - } - - bool canUseGate = false; - auto dbState = gameHandler->gameState()->curB->si.gateState; - if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && - dbState != EGateState::DESTROYED && - dbState != EGateState::BLOCKED) - { - canUseGate = true; - } - - std::pair< std::vector, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack); - - ret = path.second; - - int creSpeed = curStack->speed(0, true); - - if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) - creSpeed = GameConstants::BFIELD_SIZE; - - bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) - { - return obst->obstacleType == CObstacleInstance::MOAT; - }); - - auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_OUTER) - return true; - if (hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto occupyGateDrawbridgeHex = [&](BattleHex hex) -> bool - { - if (isGateDrawbridgeHex(hex)) - return true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && isGateDrawbridgeHex(otherHex)) - return true; - } - - return false; - }; - - if (curStack->hasBonusOfType(BonusType::FLYING)) - { - if (path.second <= creSpeed && path.first.size() > 0) - { - if (canUseGate && dbState != EGateState::OPENED && - occupyGateDrawbridgeHex(dest)) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - gameHandler->sendAndApply(&db); - } - - //inform clients about move - BattleStackMoved sm; - sm.stack = curStack->unitId(); - std::vector tiles; - tiles.push_back(path.first[0]); - sm.tilesToMove = tiles; - sm.distance = path.second; - sm.teleporting = false; - gameHandler->sendAndApply(&sm); - } - } - else //for non-flying creatures - { - std::vector tiles; - const int tilesToMove = std::max((int)(path.first.size() - creSpeed), 0); - int v = (int)path.first.size()-1; - path.first.push_back(start); - - // check if gate need to be open or closed at some point - BattleHex openGateAtHex, gateMayCloseAtHex; - if (canUseGate) - { - for (int i = (int)path.first.size()-1; i >= 0; i--) - { - auto needOpenGates = [&](BattleHex hex) -> bool - { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) - return true; - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) - return true; - else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) - return true; - - return false; - }; - - auto hex = path.first[i]; - if (!openGateAtHex.isValid() && dbState != EGateState::OPENED) - { - if (needOpenGates(hex)) - openGateAtHex = path.first[i+1]; - - //TODO we need find batter way to handle double-wide stacks - //currently if only second occupied stack part is standing on gate / bridge hex then stack will start to wait for bridge to lower before it's needed. Though this is just a visual bug. - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - if (otherHex.isValid() && needOpenGates(otherHex)) - openGateAtHex = path.first[i+2]; - } - - //gate may be opened and then closed during stack movement, but not other way around - if (openGateAtHex.isValid()) - dbState = EGateState::OPENED; - } - - if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) - { - if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - if (hasWideMoat) - { - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) - { - gateMayCloseAtHex = path.first[i-1]; - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && - path.first[i-1] != ESiegeHex::GATE_INNER && - path.first[i-1] != ESiegeHex::GATE_BRIDGE) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) - { - gateMayCloseAtHex = path.first[i-1]; - } - } - } - } - - bool stackIsMoving = true; - - while(stackIsMoving) - { - if (verror("Movement terminated abnormally"); - break; - } - - bool gateStateChanging = false; - //special handling for opening gate on from starting hex - if (openGateAtHex.isValid() && openGateAtHex == start) - gateStateChanging = true; - else - { - for (bool obstacleHit = false; (!obstacleHit) && (!gateStateChanging) && (v >= tilesToMove); --v) - { - BattleHex hex = path.first[v]; - tiles.push_back(hex); - - if ((openGateAtHex.isValid() && openGateAtHex == hex) || - (gateMayCloseAtHex.isValid() && gateMayCloseAtHex == hex)) - { - gateStateChanging = true; - } - - //if we walked onto something, finalize this portion of stack movement check into obstacle - if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty()) - obstacleHit = true; - - if (curStack->doubleWide()) - { - BattleHex otherHex = curStack->occupiedHex(hex); - //two hex creature hit obstacle by backside - auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false); - if(otherHex.isValid() && !obstacle2.empty()) - obstacleHit = true; - } - if(!obstacleHit) - passed.insert(hex); - } - } - - if (!tiles.empty()) - { - //commit movement - BattleStackMoved sm; - sm.stack = curStack->unitId(); - sm.distance = path.second; - sm.teleporting = false; - sm.tilesToMove = tiles; - gameHandler->sendAndApply(&sm); - tiles.clear(); - } - - //we don't handle obstacle at the destination tile -> it's handled separately in the if at the end - if (curStack->getPosition() != dest) - { - if(stackIsMoving && start != curStack->getPosition()) - { - stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); - passed.insert(curStack->getPosition()); - if(curStack->doubleWide()) - passed.insert(curStack->occupiedHex()); - } - if (gateStateChanging) - { - if (curStack->getPosition() == openGateAtHex) - { - openGateAtHex = BattleHex(); - //only open gate if stack is still alive - if (curStack->alive()) - { - BattleUpdateGateState db; - db.state = EGateState::OPENED; - gameHandler->sendAndApply(&db); - } - } - else if (curStack->getPosition() == gateMayCloseAtHex) - { - gateMayCloseAtHex = BattleHex(); - updateGateState(); - } - } - } - else - //movement finished normally: we reached destination - stackIsMoving = false; - } - } - //handle last hex separately for deviation - if (VLC->settings()->getBoolean(EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES)) - { - if (dest == battle::Unit::occupiedHex(start, curStack->doubleWide(), curStack->unitSide()) - || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) - passed.clear(); //Just empty passed, obstacles will handled automatically - } - //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); - - return ret; -} - void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) { - battleResult.set(nullptr); - const auto & t = *gameHandler->getTile(tile); TerrainId terrain = t.terType->getId(); if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground @@ -2017,685 +174,10 @@ void BattleProcessor::checkBattleStateChanges() //check if battle ended if (auto result = gameHandler->battleIsFinished()) { - setBattleResult(BattleResult::NORMAL, *result); + setBattleResult(EBattleResult::NORMAL, *result); } } -bool BattleProcessor::makeBattleActionImpl(BattleAction &ba) -{ - bool ok = true; - - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); - - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack - - const bool isAboutActiveStack = stack && (ba.stackNumber == gameHandler->gameState()->curB->getActiveStackID()); - - logGlobal->trace("Making action: %s", ba.toString()); - - switch(ba.actionType) - { - case EActionType::WALK: //walk - case EActionType::DEFEND: //defend - case EActionType::WAIT: //wait - case EActionType::WALK_AND_ATTACK: //walk or attack - case EActionType::SHOOT: //shoot - case EActionType::CATAPULT: //catapult - case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::MONSTER_SPELL: - - if (!stack) - { - gameHandler->complain("No such stack!"); - return false; - } - if (!stack->alive()) - { - gameHandler->complain("This stack is dead: " + stack->nodeName()); - return false; - } - - if (gameHandler->battleTacticDist()) - { - if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) - { - gameHandler->complain("This is not a stack of side that has tactics!"); - return false; - } - } - else if (!isAboutActiveStack) - { - gameHandler->complain("Action has to be about active stack!"); - return false; - } - } - - auto wrapAction = [this](BattleAction &ba) - { - StartAction startAction(ba); - gameHandler->sendAndApply(&startAction); - - return vstd::makeScopeGuard([&]() - { - gameHandler->sendAndApply(&end_action); - }); - }; - - switch(ba.actionType) - { - case EActionType::END_TACTIC_PHASE: //wait - case EActionType::BAD_MORALE: - case EActionType::NO_ACTION: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::WALK: - { - auto wrapper = wrapAction(ba); - if(target.size() < 1) - { - gameHandler->complain("Destination required for move action."); - ok = false; - break; - } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move - if (!walkedTiles) - gameHandler->complain("Stack failed movement!"); - break; - } - case EActionType::DEFEND: - { - //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) - SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); - int oldDefenceValue = defence.totalValue(); - - defence.push_back(std::make_shared(defenseBonusToAdd)); - defence.push_back(std::make_shared(bonus2)); - - int difference = defence.totalValue() - oldDefenceValue; - std::vector buffer; - if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) - { - difference = 1; - buffer.push_back(alternativeWeakCreatureBonus); - } - else - { - buffer.push_back(defenseBonusToAdd); - } - - buffer.push_back(bonus2); - - sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - gameHandler->sendAndApply(&sse); - - BattleLogMessage message; - - MetaString text; - stack->addText(text, EMetaText::GENERAL_TXT, 120); - stack->addNameReplacement(text); - text.replaceNumber(difference); - - message.lines.push_back(text); - - gameHandler->sendAndApply(&message); - //don't break - we share code with next case - } - [[fallthrough]]; - case EActionType::WAIT: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::RETREAT: //retreat/flee - { - if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) - gameHandler->complain("Cannot retreat!"); - else - setBattleResult(BattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case EActionType::SURRENDER: - { - PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; - int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); - if (cost < 0) - gameHandler->complain("Cannot surrender!"); - else if (gameHandler->getResource(player, EGameResID::GOLD) < cost) - gameHandler->complain("Not enough gold to surrender!"); - else - { - gameHandler->giveResource(player, EGameResID::GOLD, -cost); - setBattleResult(BattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case EActionType::WALK_AND_ATTACK: //walk or attack - { - auto wrapper = wrapAction(ba); - - if(!stack) - { - gameHandler->complain("No attacker"); - ok = false; - break; - } - - if(target.size() < 2) - { - gameHandler->complain("Two destinations required for attack action."); - ok = false; - break; - } - - BattleHex attackPos = target.at(0).hexValue; - BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); - - if(!destinationStack) - { - gameHandler->complain("Invalid target to attack"); - ok = false; - break; - } - - BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); - - logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - - if(stack->getPosition() != attackPos - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) - ) - { - // we were not able to reach destination tile, nor occupy specified hex - // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine - break; - } - - if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - gameHandler->complain("Unit can not attack itself"); - ok = false; - break; - } - - if(!CStack::isMeleeAttackPossible(stack, destinationStack)) - { - gameHandler->complain("Attack cannot be performed!"); - ok = false; - break; - } - - //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); - const bool retaliation = destinationStack->ableToRetaliate(); - for (int i = 0; i < totalAttacks; ++i) - { - //first strike - if(i == 0 && firstStrike && retaliation) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - - //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification - if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) - { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - } - - //counterattack - //we check retaliation twice, so if it unblocked during attack it will work only on next attack - if(stack->alive() - && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) - && (i == 0 && !firstStrike) - && retaliation && destinationStack->ableToRetaliate()) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - } - - //return - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) - && target.size() == 3 - && startingPos != stack->getPosition() - && startingPos == target.at(2).hexValue - && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) - } - break; - } - case EActionType::SHOOT: - { - if(target.size() < 1) - { - gameHandler->complain("Destination required for shot action."); - ok = false; - break; - } - - auto destination = target.at(0).hexValue; - - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); - - if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) - { - gameHandler->complain("Cannot shoot!"); - break; - } - if (!destinationStack) - { - gameHandler->complain("No target to shoot!"); - break; - } - - auto wrapper = wrapAction(ba); - - makeAttack(stack, destinationStack, 0, destination, true, true, false); - - //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) - && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) - && destinationStack->ableToRetaliate() - && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) - && stack->alive()) //attacker may have died (fire shield) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); - } - //allow more than one additional attack - - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - for(int i = 1; i < totalRangedAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) - { - makeAttack(stack, destinationStack, 0, destination, false, true, false); - } - } - break; - } - case EActionType::CATAPULT: - { - auto wrapper = wrapAction(ba); - const CStack * shooter = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) - { - gameHandler->complain("We do not know how to shoot :P"); - } - else - { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult - auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); - parameters.setSpellLevel(shotLevel); - parameters.cast(gameHandler->spellEnv, target); - } - //finish by scope guard - break; - } - case EActionType::STACK_HEAL: //healing with First Aid Tent - { - auto wrapper = wrapAction(ba); - const CStack * healer = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - - if(target.size() < 1) - { - gameHandler->complain("Destination required for heal action."); - ok = false; - break; - } - - const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); - - if(target.at(0).unitValue) - destStack = target.at(0).unitValue; - else - destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); - - if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) - { - gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); - } - else - { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent - auto dest = battle::Destination(destStack, target.at(0).hexValue); - parameters.setSpellLevel(0); - parameters.cast(gameHandler->spellEnv, {dest}); - } - break; - } - case EActionType::MONSTER_SPELL: - { - auto wrapper = wrapAction(ba); - - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.actionSubtype); - - std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID < 0) - gameHandler->complain("That stack can't cast spells!"); - else - { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(gameHandler->spellEnv, target); - } - break; - } - } - if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND - || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) - gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); - if(ba.stackNumber == gameHandler->gameState()->curB->activeStack || battleResult.get()) //active stack has moved or battle has finished - battleMadeAction.setn(true); - return ok; -} - -bool BattleProcessor::makeCustomActionImpl(BattleAction & ba) -{ - switch(ba.actionType) - { - case EActionType::HERO_SPELL: - { - COMPLAIN_RET_FALSE_IF(ba.side > 1, "Side must be 0 or 1!"); - - const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - COMPLAIN_RET_FALSE_IF((!h), "Wrong caster!"); - - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); - if (!s) - { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); - return false; - } - - spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); - - spells::detail::ProblemImpl problem; - - auto m = s->battleMechanics(¶meters); - - if(!m->canBeCast(problem))//todo: should we check aimed cast? - { - logGlobal->warn("Spell cannot be cast!"); - std::vector texts; - problem.getAll(texts); - for(auto s : texts) - logGlobal->warn(s); - return false; - } - - StartAction start_action(ba); - gameHandler->sendAndApply(&start_action); //start spell casting - - parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); - - gameHandler->sendAndApply(&end_action); - if (!gameHandler->gameState()->curB->battleGetStackByID(gameHandler->gameState()->curB->activeStack)) - { - battleMadeAction.setn(true); - } - checkBattleStateChanges(); - if (battleResult.get()) - { - battleMadeAction.setn(true); - //battle will be ended by startBattle function - //endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->heroes[0], gameHandler->gameState()->curB->heroes[1]); - } - - return true; - - } - } - return false; -} - -void BattleProcessor::stackEnchantedTrigger(const CStack * st) -{ - auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); - for(auto b : bl) - { - const CSpell * sp = SpellID(b->subtype).toSpell(); - if(!sp) - continue; - - const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); - const int32_t level = ((val > 3) ? (val - 3) : val); - - spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp); - //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle - battleCast.setEffectDuration(50); - battleCast.setSpellLevel(level); - spells::Target target; - - if(val > 3) - { - for(auto s : gameHandler->gameState()->curB->battleGetAllStacks()) - if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied - target.emplace_back(s); - } - else - { - target.emplace_back(st); - } - battleCast.applyEffects(gameHandler->spellEnv, target, false, true); - } -} - -void BattleProcessor::stackTurnTrigger(const CStack *st) -{ - BattleTriggerEffect bte; - bte.stackID = st->unitId(); - bte.effect = -1; - bte.val = 0; - bte.additionalInfo = 0; - if (st->alive()) - { - //unbind - if (st->hasBonus(Selector::type()(BonusType::BIND_EFFECT))) - { - bool unbind = true; - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); - auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st); - - for (auto b : bl) - { - if(b->additionalInfo != CAddInfo::NONE) - { - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent - if(stack) - { - if(vstd::contains(adjacent, stack)) //binding stack is still present - unbind = false; - } - } - else - { - unbind = false; - } - } - if (unbind) - { - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::UNBIND; - ssp.stackID = st->unitId(); - gameHandler->sendAndApply(&ssp); - } - } - - if (st->hasBonusOfType(BonusType::POISON)) - { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) //TODO: what if not?... - { - bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); - if (bte.val < b->val) //(negative) poison effect increases - update it - { - bte.effect = vstd::to_underlying(BonusType::POISON); - gameHandler->sendAndApply(&bte); - } - } - } - if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) - { - const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st)); - const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent); - if(opponentHero) - { - ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); - vstd::amin(manaDrained, opponentHero->mana); - if(manaDrained) - { - bte.effect = vstd::to_underlying(BonusType::MANA_DRAIN); - bte.val = manaDrained; - bte.additionalInfo = opponentHero->id.getNum(); //for sanity - gameHandler->sendAndApply(&bte); - } - } - } - if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) - { - bool fearsomeCreature = false; - for (CStack * stack : gameHandler->gameState()->curB->stacks) - { - if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) - { - fearsomeCreature = true; - break; - } - } - if (fearsomeCreature) - { - if (gameHandler->getRandomGenerator().nextInt(99) < 10) //fixed 10% - { - bte.effect = vstd::to_underlying(BonusType::FEAR); - gameHandler->sendAndApply(&bte); - } - } - } - BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = gameHandler->gameState()->curB->whatSide(st->unitOwner()); - if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0) - { - bool cast = false; - while(!bl.empty() && !cast) - { - auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); - auto spellID = SpellID(bonus->subtype); - const CSpell * spell = SpellID(spellID).toSpell(); - bl.remove_if([&bonus](const Bonus * b) - { - return b == bonus.get(); - }); - spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell); - parameters.setSpellLevel(bonus->val); - parameters.massive = true; - parameters.smart = true; - //todo: recheck effect level - if(parameters.castIfPossible(gameHandler->spellEnv, spells::Target(1, spells::Destination()))) - { - cast = true; - - int cooldown = bonus->additionalInfo[0]; - BattleSetStackProperty ssp; - ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; - ssp.absolute = false; - ssp.val = cooldown; - ssp.stackID = st->unitId(); - gameHandler->sendAndApply(&ssp); - } - } - } - } -} - -void BattleProcessor::makeStackDoNothing(const CStack * next) -{ - BattleAction doNothing; - doNothing.actionType = EActionType::NO_ACTION; - doNothing.side = next->unitSide(); - doNothing.stackNumber = next->unitId(); - - makeAutomaticAction(next, doNothing); -} - - -void BattleProcessor::setBattleResult(BattleResult::EResult resultType, int victoriusSide) -{ - boost::unique_lock guard(battleResult.mx); - if (battleResult.data) - { - gameHandler->complain((boost::format("The battle result has been already set (to %d, asked to %d)") - % battleResult.data->result % resultType).str()); - return; - } - auto br = new BattleResult(); - br->result = resultType; - br->winner = victoriusSide; //surrendering side loses - gameHandler->gameState()->curB->calculateCasualties(br->casualties); - battleResult.data = br; -} - -void BattleProcessor::removeObstacle(const CObstacleInstance & obstacle) -{ - BattleObstaclesChanged obsRem; - obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); - gameHandler->sendAndApply(&obsRem); -} - void BattleProcessor::updateGateState() { // GATE_BRIDGE - leftmost tile, located over moat @@ -2753,8 +235,6 @@ void BattleProcessor::updateGateState() bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) { - boost::unique_lock lock(battleActionMutex); - const BattleInfo * b = gameHandler->gameState()->curB; if(!b && gameHandler->complain("Can not make action - there is no battle ongoing!")) @@ -2790,7 +270,7 @@ bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) return false; } - return makeBattleActionImpl(ba); + return actionsProcessor->makeBattleAction(ba); } bool BattleProcessor::makeCustomAction(PlayerColor player, BattleAction &ba) @@ -2821,5 +301,44 @@ bool BattleProcessor::makeCustomAction(PlayerColor player, BattleAction &ba) if(ba.actionType != EActionType::HERO_SPELL && gameHandler->complain("Invalid custom action type!")) return false; - return makeCustomActionImpl(ba); + return actionsProcessor->makeCustomAction(ba); +} + +void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +{ + resultProcessor->setBattleResult(resultType, victoriusSide); +} + +bool BattleProcessor::makeBattleAction(BattleAction &ba) +{ + return actionsProcessor->makeBattleAction(ba); +} + +bool BattleProcessor::makeCustomAction(BattleAction &ba) +{ + return actionsProcessor->makeCustomAction(ba); +} + +void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2) +{ + resultProcessor->endBattle(tile, hero1, hero2); +} + +void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) +{ + resultProcessor->endBattleConfirm(battleInfo); +} + +void BattleProcessor::battleAfterLevelUp(const BattleResult &result) +{ + resultProcessor->battleAfterLevelUp(result); +} + +void BattleProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; + + actionsProcessor->setGameHandler(newGameHandler); + flowProcessor->setGameHandler(newGameHandler); + resultProcessor->setGameHandler(newGameHandler); } diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index b6ad9e7ff..bdc5bc53b 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -10,118 +10,61 @@ #pragma once #include "../../lib/GameConstants.h" -#include "../../lib/NetPacks.h" VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -struct SideInBattle; - -namespace battle { -class CUnitState; -} - +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +class BattleAction; +class int3; +class BattleInfo; +struct BattleResult; VCMI_LIB_NAMESPACE_END +class CGameHandler; class CBattleQuery; - -struct CasualtiesAfterBattle -{ - using TStackAndItsNewCount = std::pair; - using TSummoned = std::map; - enum {ERASE = -1}; - const CArmedInstance * army; - std::vector newStackCounts; - std::vector removedWarMachines; - TSummoned summoned; - ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - - CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); - void updateArmy(CGameHandler *gh); -}; - -struct FinishingBattleHelper -{ - FinishingBattleHelper(); - FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); - - inline bool isDraw() const {return winnerSide == 2;} - - const CGHeroInstance *winnerHero, *loserHero; - PlayerColor victor, loser; - ui8 winnerSide; - - int remainingBattleQueriesCount; - - template void serialize(Handler &h, const int version) - { - h & winnerHero; - h & loserHero; - h & victor; - h & loser; - h & winnerSide; - h & remainingBattleQueriesCount; - } -}; - -using FireShieldInfo = std::vector>; +class BattleActionProcessor; +class BattleFlowProcessor; +class BattleResultProcessor; class BattleProcessor : boost::noncopyable { - ////used only in endBattle - don't touch elsewhere - bool visitObjectAfterVictory; + friend class BattleActionProcessor; + friend class BattleFlowProcessor; + friend class BattleResultProcessor; - std::unique_ptr battleThread; - std::unique_ptr finishingBattle; + CGameHandler * gameHandler; + std::unique_ptr actionsProcessor; + std::unique_ptr flowProcessor; + std::unique_ptr resultProcessor; - void removeObstacle(const CObstacleInstance &obstacle); - void makeStackDoNothing(const CStack * next); void updateGateState(); - bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) - void stackEnchantedTrigger(const CStack * stack); - void stackTurnTrigger(const CStack *stack); void engageIntoBattle( PlayerColor player ); - void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); - - int moveStack(int stack, BattleHex dest); //returned value - travelled distance - void runBattle(); - - void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - - // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); - - void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); - void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); void checkBattleStateChanges(); void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - void setBattleResult(BattleResult::EResult resultType, int victoriusSide); - bool makeBattleActionImpl(BattleAction &ba); - bool makeCustomActionImpl(BattleAction &ba); + bool makeBattleAction(BattleAction &ba); + bool makeCustomAction(BattleAction &ba); + void setBattleResult(EBattleResult resultType, int victoriusSide); public: - CGameHandler * gameHandler; - - BattleProcessor(CGameHandler * gameHandler); + explicit BattleProcessor(CGameHandler * gameHandler); BattleProcessor(); - ~BattleProcessor(); + void setGameHandler(CGameHandler * gameHandler); + void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); //use hero=nullptr for no hero void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); //if any of armies is hero, hero will be used void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle - void battleAfterLevelUp(const BattleResult &result); - bool makeBattleAction(PlayerColor player, BattleAction &ba); bool makeCustomAction(PlayerColor player, BattleAction &ba); void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); + void battleAfterLevelUp(const BattleResult &result); template void serialize(Handler &h, const int version) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp new file mode 100644 index 000000000..67df5a9b7 --- /dev/null +++ b/server/battles/BattleResultProcessor.cpp @@ -0,0 +1,554 @@ +/* + * BattleResultProcessor.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 "BattleResultProcessor.h" + +#include "../CGameHandler.h" +#include "../CVCMIServer.h" +#include "../processors/HeroPoolProcessor.h" +#include "../queries/QueriesProcessor.h" +#include "../queries/BattleQueries.h" + +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/GameSettings.h" +#include "../../lib/ScopeGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CUnitState.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/serializer/Cast.h" +#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/spells/BonusCaster.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/ObstacleCasterProxy.h" +#include "../../lib/spells/Problem.h" + +BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner) + : owner(owner) + , gameHandler(nullptr) +{ +} + +void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler) +{ + gameHandler = newGameHandler; +} + +CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): + army(battleSide.armyObject) +{ + heroWithDeadCommander = ObjectInstanceID(); + + PlayerColor color = battleSide.color; + + for(CStack * st : bat->stacks) + { + if(st->summoned) //don't take into account temporary summoned stacks + continue; + if(st->unitOwner() != color) //remove only our stacks + continue; + + logGlobal->debug("Calculating casualties for %s", st->nodeName()); + + st->health.takeResurrected(); + + if(st->unitSlot() == SlotID::ARROW_TOWERS_SLOT) + { + logGlobal->debug("Ignored arrow towers stack."); + } + else if(st->unitSlot() == SlotID::WAR_MACHINES_SLOT) + { + auto warMachine = st->unitType()->warMachine; + + if(warMachine == ArtifactID::NONE) + { + logGlobal->error("Invalid creature in war machine virtual slot. Stack: %s", st->nodeName()); + } + //catapult artifact remain even if "creature" killed in siege + else if(warMachine != ArtifactID::CATAPULT && st->getCount() <= 0) + { + logGlobal->debug("War machine has been destroyed"); + auto hero = dynamic_ptr_cast (army); + if (hero) + removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + else + logGlobal->error("War machine in army without hero"); + } + } + else if(st->unitSlot() == SlotID::SUMMONED_SLOT_PLACEHOLDER) + { + if(st->alive() && st->getCount() > 0) + { + logGlobal->debug("Permanently summoned %d units.", st->getCount()); + const CreatureID summonedType = st->creatureId(); + summoned[summonedType] += st->getCount(); + } + } + else if(st->unitSlot() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + if (nullptr == st->base) + { + logGlobal->error("Stack with no base in commander slot. Stack: %s", st->nodeName()); + } + else + { + auto c = dynamic_cast (st->base); + if(c) + { + auto h = dynamic_cast (army); + if(h && h->commander == c && (st->getCount() == 0 || !st->alive())) + { + logGlobal->debug("Commander is dead."); + heroWithDeadCommander = army->id; //TODO: unify commander handling + } + } + else + logGlobal->error("Stack with invalid instance in commander slot. Stack: %s", st->nodeName()); + } + } + else if(st->base && !army->slotEmpty(st->unitSlot())) + { + logGlobal->debug("Count: %d; base count: %d", st->getCount(), army->getStackCount(st->unitSlot())); + if(st->getCount() == 0 || !st->alive()) + { + logGlobal->debug("Stack has been destroyed."); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, 0)); + } + else if(st->getCount() < army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack lost %d units.", army->getStackCount(st->unitSlot()) - st->getCount()); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + else if(st->getCount() > army->getStackCount(st->unitSlot())) + { + logGlobal->debug("Stack gained %d units.", st->getCount() - army->getStackCount(st->unitSlot())); + StackLocation sl(army, st->unitSlot()); + newStackCounts.push_back(TStackAndItsNewCount(sl, st->getCount())); + } + } + else + { + logGlobal->warn("Unable to process stack: %s", st->nodeName()); + } + } +} + +void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) +{ + for (TStackAndItsNewCount &ncount : newStackCounts) + { + if (ncount.second > 0) + gh->changeStackCount(ncount.first, ncount.second, true); + else + gh->eraseStack(ncount.first, true); + } + for (auto summoned_iter : summoned) + { + SlotID slot = army->getSlotFor(summoned_iter.first); + if (slot.validSlot()) + { + StackLocation location(army, slot); + gh->addToSlot(location, summoned_iter.first.toCreature(), summoned_iter.second); + } + else + { + //even if it will be possible to summon anything permanently it should be checked for free slot + //necromancy is handled separately + gh->complain("No free slot to put summoned creature"); + } + } + for (auto al : removedWarMachines) + { + gh->removeArtifact(al); + } + if (heroWithDeadCommander != ObjectInstanceID()) + { + SetCommanderProperty scp; + scp.heroid = heroWithDeadCommander; + scp.which = SetCommanderProperty::ALIVE; + scp.amount = 0; + gh->sendAndApply(&scp); + } +} + +FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int remainingBattleQueriesCount) +{ + assert(Query->result); + assert(Query->bi); + auto &result = *Query->result; + auto &info = *Query->bi; + + winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; + loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; + victor = info.sides[result.winner].color; + loser = info.sides[!result.winner].color; + winnerSide = result.winner; + this->remainingBattleQueriesCount = remainingBattleQueriesCount; +} + +FinishingBattleHelper::FinishingBattleHelper() +{ + winnerHero = loserHero = nullptr; + winnerSide = 0; + remainingBattleQueriesCount = 0; +} + +void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) +{ + auto const & giveExp = [](BattleResult &r) + { + if (r.winner > 1) + { + // draw + return; + } + r.exp[0] = 0; + r.exp[1] = 0; + for (auto i = r.casualties[!r.winner].begin(); i!=r.casualties[!r.winner].end(); i++) + { + r.exp[r.winner] += VLC->creh->objects.at(i->first)->valOfBonuses(BonusType::STACK_HEALTH) * i->second; + } + }; + + LOG_TRACE(logGlobal); + + //Fill BattleResult structure with exp info + giveExp(*battleResult); + + if (battleResult->result == EBattleResult::NORMAL) // give 500 exp for defeating hero, unless he escaped + { + if(heroAttacker) + battleResult->exp[1] += 500; + if(heroDefender) + battleResult->exp[0] += 500; + } + + if(heroAttacker) + battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill + if(heroDefender) + battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); + + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); + if (!battleQuery) + { + logGlobal->error("Cannot find battle query!"); + gameHandler->complain("Player " + boost::lexical_cast(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!"); + return; + } + + battleQuery->result = std::make_optional(*battleResult); + + //Check how many battle gameHandler->queries were created (number of players blocked by battle) + const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; + finishingBattle = std::make_unique(battleQuery, queriedPlayers); + + // in battles against neutrals, 1st player can ask to replay battle manually + if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer()) + { + auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + battleResult->queryID = battleDialogQuery->queryID; + gameHandler->queries->addQuery(battleDialogQuery); + } + else + battleResult->queryID = -1; + + //set same battle result for all gameHandler->queries + for(auto q : gameHandler->queries->allQueries()) + { + auto otherBattleQuery = std::dynamic_pointer_cast(q); + if(otherBattleQuery) + otherBattleQuery->result = battleQuery->result; + } + + gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed + + if (battleResult->queryID == -1) + endBattleConfirm(gameHandler->gameState()->curB); +} + +void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) +{ + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battleInfo->sides.at(0).color)); + if(!battleQuery) + { + logGlobal->trace("No battle query, battle end was confirmed by another player"); + return; + } + + const EBattleResult result = battleResult.get()->result; + + CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle + ChangeSpells cs; //for Eagle Eye + + if(!finishingBattle->isDraw() && finishingBattle->winnerHero) + { + if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) + { + double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); + for(auto & spellId : battleInfo->sides.at(!battleResult->winner).usedSpellsHistory) + { + auto spell = spellId.toSpell(VLC->spells()); + if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) + cs.spells.insert(spell->getId()); + } + } + } + std::vector arts; //display them in window + + if(result == EBattleResult::NORMAL && !finishingBattle->isDraw() && finishingBattle->winnerHero) + { + auto sendMoveArtifact = [&](const CArtifactInstance *art, MoveArtifact *ma) + { + const auto slot = ArtifactUtils::getArtAnyPosition(finishingBattle->winnerHero, art->getTypeId()); + if(slot != ArtifactPosition::PRE_FIRST) + { + arts.push_back(art); + ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); + if(ArtifactUtils::isSlotBackpack(slot)) + ma->askAssemble = false; + gameHandler->sendAndApply(ma); + } + }; + + if (finishingBattle->loserHero) + { + //TODO: wrap it into a function, somehow (std::variant -_-) + auto artifactsWorn = finishingBattle->loserHero->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig() && + art->artType->getId() != ArtifactID::SPELLBOOK) + // don't move war machines or locked arts (spellbook) + { + sendMoveArtifact(art, &ma); + } + } + for(int slotNumber = finishingBattle->loserHero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) + { + //we assume that no big artifacts can be found + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero, + ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning + const CArtifactInstance * art = ma.src.getArt(); + if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won + { + sendMoveArtifact(art, &ma); + } + } + if (finishingBattle->loserHero->commander) //TODO: what if commanders belong to no hero? + { + artifactsWorn = finishingBattle->loserHero->commander->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + for (auto armySlot : battleInfo->sides.at(!battleResult->winner).armyObject->stacks) + { + auto artifactsWorn = armySlot.second->artifactsWorn; + for (auto artSlot : artifactsWorn) + { + MoveArtifact ma; + ma.src = ArtifactLocation(armySlot.second, artSlot.first); + const CArtifactInstance * art = ma.src.getArt(); + if (art && !art->artType->isBig()) + { + sendMoveArtifact(art, &ma); + } + } + } + } + + if (arts.size()) //display loot + { + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + + iw.text.appendLocalString (EMetaText::GENERAL_TXT, 30); //You have captured enemy artifact + + for (auto art : arts) //TODO; separate function to display loot for various ojects? + { + iw.components.emplace_back( + Component::EComponentType::ARTIFACT, art->artType->getId(), + art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); + if (iw.components.size() >= 14) + { + gameHandler->sendAndApply(&iw); + iw.components.clear(); + } + } + if (iw.components.size()) + { + gameHandler->sendAndApply(&iw); + } + } + //Eagle Eye secondary skill handling + if (!cs.spells.empty()) + { + cs.learn = 1; + cs.hid = finishingBattle->winnerHero->id; + + InfoWindow iw; + iw.player = finishingBattle->winnerHero->tempOwner; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 221); //Through eagle-eyed observation, %s is able to learn %s + iw.text.replaceRawString(finishingBattle->winnerHero->getNameTranslated()); + + std::ostringstream names; + for (int i = 0; i < cs.spells.size(); i++) + { + names << "%s"; + if (i < cs.spells.size() - 2) + names << ", "; + else if (i < cs.spells.size() - 1) + names << "%s"; + } + names << "."; + + iw.text.replaceRawString(names.str()); + + auto it = cs.spells.begin(); + for (int i = 0; i < cs.spells.size(); i++, it++) + { + iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); + if (i == cs.spells.size() - 2) //we just added pre-last name + iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " + iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); + } + gameHandler->sendAndApply(&iw); + gameHandler->sendAndApply(&cs); + } + cab1.updateArmy(gameHandler); + cab2.updateArmy(gameHandler); //take casualties after battle is deleted + + if(finishingBattle->loserHero) //remove beaten hero + { + RemoveObject ro(finishingBattle->loserHero->id); + gameHandler->sendAndApply(&ro); + } + if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + } + + if(battleResult->winner == BattleSide::DEFENDER + && finishingBattle->winnerHero + && finishingBattle->winnerHero->visitedTown + && !finishingBattle->winnerHero->inTownGarrison + && finishingBattle->winnerHero->visitedTown->garrisonHero == finishingBattle->winnerHero) + { + gameHandler->swapGarrisonOnSiege(finishingBattle->winnerHero->visitedTown->id); //return defending visitor from garrison to its rightful place + } + //give exp + if(!finishingBattle->isDraw() && battleResult->exp[finishingBattle->winnerSide] && finishingBattle->winnerHero) + gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); + + BattleResultAccepted raccepted; + raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); + raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); + raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); + raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); + raccepted.heroResult[0].exp = battleResult->exp[0]; + raccepted.heroResult[1].exp = battleResult->exp[1]; + raccepted.winnerSide = finishingBattle->winnerSide; + gameHandler->sendAndApply(&raccepted); + + gameHandler->queries->popIfTop(battleQuery); + //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query +} + +void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) +{ + LOG_TRACE(logGlobal); + + if(!finishingBattle) + return; + + finishingBattle->remainingBattleQueriesCount--; + logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); + + if (finishingBattle->remainingBattleQueriesCount > 0) + //Battle results will be handled when all battle gameHandler->queries are closed + return; + + //TODO consider if we really want it to work like above. ATM each player as unblocked as soon as possible + // but the battle consequences are applied after final player is unblocked. Hard to abuse... + // Still, it looks like a hole. + + // Necromancy if applicable. + const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult) : CStackBasicDescriptor(); + // Give raised units to winner and show dialog, if any were raised, + // units will be given after casualties are taken + const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); + + if (necroSlot != SlotID()) + { + finishingBattle->winnerHero->showNecromancyDialog(raisedStack, gameHandler->getRandomGenerator()); + gameHandler->addToSlot(StackLocation(finishingBattle->winnerHero, necroSlot), raisedStack.type, raisedStack.count); + } + + BattleResultsApplied resultsApplied; + resultsApplied.player1 = finishingBattle->victor; + resultsApplied.player2 = finishingBattle->loser; + gameHandler->sendAndApply(&resultsApplied); + + gameHandler->setBattle(nullptr); + + //handle victory/loss of engaged players + std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; + gameHandler->checkVictoryLossConditions(playerColors); + + if (result.result == EBattleResult::SURRENDER) + gameHandler->heroPool->onHeroSurrendered(finishingBattle->loser, finishingBattle->loserHero); + + if (result.result == EBattleResult::ESCAPE) + gameHandler->heroPool->onHeroEscaped(finishingBattle->loser, finishingBattle->loserHero); + + if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() + && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) + { + RemoveObject ro(finishingBattle->winnerHero->id); + gameHandler->sendAndApply(&ro); + + if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) + gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); + } + + finishingBattle.reset(); +} + +void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +{ + battleResult = std::make_unique(); + battleResult->result = resultType; + battleResult->winner = victoriusSide; //surrendering side loses + gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); + +} diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h new file mode 100644 index 000000000..e883c874a --- /dev/null +++ b/server/battles/BattleResultProcessor.h @@ -0,0 +1,78 @@ +/* + * BattleProcessor.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/GameConstants.h" +#include "../../lib/NetPacks.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct SideInBattle; +VCMI_LIB_NAMESPACE_END + +class CBattleQuery; +class BattleProcessor; +class CGameHandler; + +struct CasualtiesAfterBattle +{ + using TStackAndItsNewCount = std::pair; + using TSummoned = std::map; + enum {ERASE = -1}; + const CArmedInstance * army; + std::vector newStackCounts; + std::vector removedWarMachines; + TSummoned summoned; + ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations + + CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); + void updateArmy(CGameHandler *gh); +}; + +struct FinishingBattleHelper +{ + FinishingBattleHelper(); + FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); + + inline bool isDraw() const {return winnerSide == 2;} + + const CGHeroInstance *winnerHero, *loserHero; + PlayerColor victor, loser; + ui8 winnerSide; + + int remainingBattleQueriesCount; + + template void serialize(Handler &h, const int version) + { + h & winnerHero; + h & loserHero; + h & victor; + h & loser; + h & winnerSide; + h & remainingBattleQueriesCount; + } +}; + +class BattleResultProcessor : boost::noncopyable +{ + BattleProcessor * owner; + CGameHandler * gameHandler; + + std::unique_ptr battleResult; + std::unique_ptr finishingBattle; + +public: + BattleResultProcessor(BattleProcessor * owner); + void setGameHandler(CGameHandler * newGameHandler); + + void setBattleResult(EBattleResult resultType, int victoriusSide); + void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle + void endBattleConfirm(const BattleInfo * battleInfo); + void battleAfterLevelUp(const BattleResult &result); +}; From 6297140bf5cbb26d3730591dca51b3a90caf49c7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 00:51:50 +0300 Subject: [PATCH 0107/1248] Start of stabilization - battles now start correctly --- AI/BattleAI/BattleAI.cpp | 2 +- CCallback.cpp | 2 +- client/battle/BattleStacksController.cpp | 2 +- lib/GameConstants.cpp | 1 - lib/GameConstants.h | 17 +- lib/NetPackVisitor.h | 1 - lib/NetPacks.h | 18 - lib/NetPacksLib.cpp | 5 - lib/battle/BattleAction.cpp | 2 +- lib/registerTypes/RegisterTypes.h | 1 - server/NetPacksServer.cpp | 8 - server/ServerNetPackVisitors.h | 3 +- server/battles/BattleActionProcessor.cpp | 934 ++++++++++++----------- server/battles/BattleActionProcessor.h | 27 +- server/battles/BattleFlowProcessor.cpp | 91 ++- server/battles/BattleFlowProcessor.h | 5 +- server/battles/BattleProcessor.cpp | 50 +- server/battles/BattleProcessor.h | 5 +- server/battles/BattleResultProcessor.cpp | 5 +- server/battles/BattleResultProcessor.h | 4 +- server/queries/BattleQueries.cpp | 2 +- 21 files changed, 601 insertions(+), 584 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 0f7068402..28e16edd5 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -292,7 +292,7 @@ void CBattleAI::activeStack( const CStack * stack ) //spellcast may finish battle or kill active stack //send special preudo-action BattleAction cancel; - cancel.actionType = EActionType::CANCEL; + cancel.actionType = EActionType::NO_ACTION; cb->battleMakeUnitAction(cancel); return; } diff --git a/CCallback.cpp b/CCallback.cpp index 8331a8ec2..93f7fb670 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -206,7 +206,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) void CBattleCallback::battleMakeSpellAction(const BattleAction & action) { assert(action.actionType == EActionType::HERO_SPELL); - MakeCustomAction mca(action); + MakeAction mca(action); sendRequest(&mca); } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 4abc8b6ed..12f8dc368 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -401,7 +401,7 @@ void BattleStacksController::stackRemoved(uint32_t stackID) { BattleAction action; action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - action.actionType = EActionType::CANCEL; + action.actionType = EActionType::NO_ACTION; action.stackNumber = getActiveStack()->unitId(); LOCPLINT->cb->battleMakeUnitAction(action); diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 5fcd99513..733596428 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -248,7 +248,6 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType) static const std::map actionTypeToString = { {EActionType::END_TACTIC_PHASE, "End tactic phase"}, - {EActionType::INVALID, "Invalid"}, {EActionType::NO_ACTION, "No action"}, {EActionType::HERO_SPELL, "Hero spell"}, {EActionType::WALK, "Walk"}, diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 9097f9aa1..052ddeae4 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1000,18 +1000,19 @@ namespace Date enum class EActionType : int32_t { - CANCEL = -3, - END_TACTIC_PHASE = -2, - INVALID = -1, - NO_ACTION = 0, - HERO_SPELL, - WALK, - DEFEND, + NO_ACTION, + + END_TACTIC_PHASE, RETREAT, SURRENDER, + + HERO_SPELL, + + WALK, + WAIT, + DEFEND, WALK_AND_ATTACK, SHOOT, - WAIT, CATAPULT, MONSTER_SPELL, BAD_MORALE, diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index da1c6e2e6..571695f3f 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -134,7 +134,6 @@ public: virtual void visitBuildBoat(BuildBoat & pack) {} virtual void visitQueryReply(QueryReply & pack) {} virtual void visitMakeAction(MakeAction & pack) {} - virtual void visitMakeCustomAction(MakeCustomAction & pack) {} virtual void visitDigWithHero(DigWithHero & pack) {} virtual void visitCastAdvSpell(CastAdvSpell & pack) {} virtual void visitSaveGame(SaveGame & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index e1f3cad28..90921b1c4 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -2513,24 +2513,6 @@ struct DLL_LINKAGE MakeAction : public CPackForServer } }; -struct DLL_LINKAGE MakeCustomAction : public CPackForServer -{ - MakeCustomAction() = default; - MakeCustomAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - } -}; - struct DLL_LINKAGE DigWithHero : public CPackForServer { ObjectInstanceID id; //digging hero id diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5dc6e5d56..f611b1854 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -638,11 +638,6 @@ void MakeAction::visitTyped(ICPackVisitor & visitor) visitor.visitMakeAction(*this); } -void MakeCustomAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMakeCustomAction(*this); -} - void DigWithHero::visitTyped(ICPackVisitor & visitor) { visitor.visitDigWithHero(*this); diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index c0d1f8d9c..1bfdd37ba 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -20,7 +20,7 @@ static const int32_t INVALID_UNIT_ID = -1000; BattleAction::BattleAction(): side(-1), stackNumber(-1), - actionType(EActionType::INVALID), + actionType(EActionType::NO_ACTION), actionSubtype(-1) { } diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 08db1d259..9f5fe19e9 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -352,7 +352,6 @@ void registerTypesServerPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 70a6a9cec..8218997cd 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -288,14 +288,6 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) result = gh.battles->makeBattleAction(pack.player, pack.ba); } -void ApplyGhNetPackVisitor::visitMakeCustomAction(MakeCustomAction & pack) -{ - if (!gh.hasPlayerAt(pack.player, pack.c)) - gh.throwAndComplain(&pack, "No such pack.player!"); - - result = gh.battles->makeCustomAction(pack.player, pack.ba); -} - void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) { gh.throwOnWrongOwner(&pack, pack.id); diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index a9ed4595b..821046418 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -55,8 +55,7 @@ public: virtual void visitBuildBoat(BuildBoat & pack) override; virtual void visitQueryReply(QueryReply & pack) override; virtual void visitMakeAction(MakeAction & pack) override; - virtual void visitMakeCustomAction(MakeCustomAction & pack) override; virtual void visitDigWithHero(DigWithHero & pack) override; virtual void visitCastAdvSpell(CastAdvSpell & pack) override; virtual void visitPlayerMessage(PlayerMessage & pack) override; -}; \ No newline at end of file +}; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 7ba68cb27..d890b466b 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -51,484 +51,530 @@ void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -bool BattleActionProcessor::makeBattleAction(BattleAction &ba) +bool BattleActionProcessor::doEmptyAction(const BattleAction & ba) { - bool ok = true; + return true; +} +bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doWaitAction(const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doBadMoraleAction(const BattleAction & ba) +{ + return true; +} + +bool BattleActionProcessor::doRetreatAction(const BattleAction & ba) +{ + if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) + { + gameHandler->complain("Cannot retreat!"); + return false; + } + + owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); + return true; +} + +bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba) +{ + PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; + int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); + if (cost < 0) + { + gameHandler->complain("Cannot surrender!"); + return false; + } + + if (gameHandler->getResource(player, EGameResID::GOLD) < cost) + { + gameHandler->complain("Not enough gold to surrender!"); + return false; + } + + gameHandler->giveResource(player, EGameResID::GOLD, -cost); + owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); + return true; +} + +bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) +{ + const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if (!h) + { + logGlobal->error("Wrong caster!"); + return false; + } + + const CSpell * s = SpellID(ba.actionSubtype).toSpell(); + if (!s) + { + logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); + return false; + } + + spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); + + spells::detail::ProblemImpl problem; + + auto m = s->battleMechanics(¶meters); + + if(!m->canBeCast(problem))//todo: should we check aimed cast? + { + logGlobal->warn("Spell cannot be cast!"); + std::vector texts; + problem.getAll(texts); + for(auto s : texts) + logGlobal->warn(s); + return false; + } + + StartAction start_action(ba); + gameHandler->sendAndApply(&start_action); //start spell casting + + parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); + + EndAction end_action; + gameHandler->sendAndApply(&end_action); + return true; +} + +bool BattleActionProcessor::doWalkAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(gameHandler->gameState()->curB); - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); //may be nullptr if action is not about stack + if (!canStackAct(stack)) + return false; - const bool isAboutActiveStack = stack && (ba.stackNumber == gameHandler->gameState()->curB->getActiveStackID()); - - logGlobal->trace("Making action: %s", ba.toString()); - - switch(ba.actionType) + if(target.size() < 1) { - case EActionType::WALK: //walk - case EActionType::DEFEND: //defend - case EActionType::WAIT: //wait - case EActionType::WALK_AND_ATTACK: //walk or attack - case EActionType::SHOOT: //shoot - case EActionType::CATAPULT: //catapult - case EActionType::STACK_HEAL: //healing with First Aid Tent - case EActionType::MONSTER_SPELL: + gameHandler->complain("Destination required for move action."); + return false; + } - if (!stack) + int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move + if (!walkedTiles) + { + gameHandler->complain("Stack failed movement!"); + return false; + } + return true; +} + +bool BattleActionProcessor::doDefendAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + + if (!canStackAct(stack)) + return false; + + //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) + SetStackEffect sse; + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + int oldDefenceValue = defence.totalValue(); + + defence.push_back(std::make_shared(defenseBonusToAdd)); + defence.push_back(std::make_shared(bonus2)); + + int difference = defence.totalValue() - oldDefenceValue; + std::vector buffer; + if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) + { + difference = 1; + buffer.push_back(alternativeWeakCreatureBonus); + } + else + { + buffer.push_back(defenseBonusToAdd); + } + + buffer.push_back(bonus2); + + sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); + gameHandler->sendAndApply(&sse); + + BattleLogMessage message; + + MetaString text; + stack->addText(text, EMetaText::GENERAL_TXT, 120); + stack->addNameReplacement(text); + text.replaceNumber(difference); + + message.lines.push_back(text); + + gameHandler->sendAndApply(&message); + return true; +} + +bool BattleActionProcessor::doAttackAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 2) + { + gameHandler->complain("Two destinations required for attack action."); + return false; + } + + BattleHex attackPos = target.at(0).hexValue; + BattleHex destinationTile = target.at(1).hexValue; + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); + + if(!destinationStack) + { + gameHandler->complain("Invalid target to attack"); + return false; + } + + BattleHex startingPos = stack->getPosition(); + int distance = moveStack(ba.stackNumber, attackPos); + + logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); + + if(stack->getPosition() != attackPos && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) ) + { + // we were not able to reach destination tile, nor occupy specified hex + // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine + return true; + } + + if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check + { + destinationStack = nullptr; + } + + if(!destinationStack) + { + gameHandler->complain("Unit can not attack itself"); + return false; + } + + if(!CStack::isMeleeAttackPossible(stack, destinationStack)) + { + gameHandler->complain("Attack cannot be performed!"); + return false; + } + + //attack + int totalAttacks = stack->totalAttacks.getMeleeValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); + const bool retaliation = destinationStack->ableToRetaliate(); + for (int i = 0; i < totalAttacks; ++i) + { + //first strike + if(i == 0 && firstStrike && retaliation) { - gameHandler->complain("No such stack!"); - return false; - } - if (!stack->alive()) - { - gameHandler->complain("This stack is dead: " + stack->nodeName()); - return false; + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); } - if (gameHandler->battleTacticDist()) + //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification + if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) { - if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) - { - gameHandler->complain("This is not a stack of side that has tactics!"); - return false; - } + makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack } - else if (!isAboutActiveStack) + + //counterattack + //we check retaliation twice, so if it unblocked during attack it will work only on next attack + if(stack->alive() + && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) + && (i == 0 && !firstStrike) + && retaliation && destinationStack->ableToRetaliate()) { - gameHandler->complain("Action has to be about active stack!"); - return false; + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); } } - static EndAction end_action; - auto wrapAction = [this](BattleAction &ba) + //return + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) + && target.size() == 3 + && startingPos != stack->getPosition() + && startingPos == target.at(2).hexValue + && stack->alive()) { - StartAction startAction(ba); - gameHandler->sendAndApply(&startAction); + moveStack(ba.stackNumber, startingPos); + //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) + } + return true; +} - return vstd::makeScopeGuard([&]() +bool BattleActionProcessor::doShootAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for shot action."); + return false; + } + + auto destination = target.at(0).hexValue; + + const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); + + if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) + { + gameHandler->complain("Cannot shoot!"); + return false; + } + + if (!destinationStack) + { + gameHandler->complain("No target to shoot!"); + return false; + } + + makeAttack(stack, destinationStack, 0, destination, true, true, false); + + //ranged counterattack + if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) + && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) + && destinationStack->ableToRetaliate() + && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) + && stack->alive()) //attacker may have died (fire shield) + { + makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); + } + //allow more than one additional attack + + int totalRangedAttacks = stack->totalAttacks.getRangedValue(); + + //TODO: move to CUnitState + const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + if(attackingHero) + { + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + } + + for(int i = 1; i < totalRangedAttacks; ++i) + { + if( + stack->alive() + && destinationStack->alive() + && stack->shots.canUse() + ) { - gameHandler->sendAndApply(&end_action); - }); - }; + makeAttack(stack, destinationStack, 0, destination, false, true, false); + } + } + return true; +} + +bool BattleActionProcessor::doCatapultAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); + if(!catapultAbility || catapultAbility->subtype < 0) + { + gameHandler->complain("We do not know how to shoot :P"); + } + else + { + const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); + parameters.setSpellLevel(shotLevel); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + SpellID spellID = SpellID(ba.actionSubtype); + + if (!canStackAct(stack)) + return false; + + std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); + + //TODO special bonus for genies ability + if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + + if (spellID < 0) + gameHandler->complain("That stack can't cast spells!"); + else + { + const CSpell * spell = SpellID(spellID).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); + int32_t spellLvl = 0; + if(spellcaster) + vstd::amax(spellLvl, spellcaster->val); + if(randSpellcaster) + vstd::amax(spellLvl, randSpellcaster->val); + parameters.setSpellLevel(spellLvl); + parameters.cast(gameHandler->spellEnv, target); + } + return true; +} + +bool BattleActionProcessor::doHealAction(const BattleAction & ba) +{ + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + + if (!canStackAct(stack)) + return false; + + if(target.size() < 1) + { + gameHandler->complain("Destination required for heal action."); + return false; + } + + const battle::Unit * destStack = nullptr; + std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); + + if(target.at(0).unitValue) + destStack = target.at(0).unitValue; + else + destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); + + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) + { + gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); + } + else + { + const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); + spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + auto dest = battle::Destination(destStack, target.at(0).hexValue); + parameters.setSpellLevel(0); + parameters.cast(gameHandler->spellEnv, {dest}); + } + return true; +} + +bool BattleActionProcessor::canStackAct(const CStack * stack) +{ + const bool isAboutActiveStack = stack->unitId() == gameHandler->gameState()->curB->getActiveStackID(); + + if (!stack) + { + gameHandler->complain("No such stack!"); + return false; + } + if (!stack->alive()) + { + gameHandler->complain("This stack is dead: " + stack->nodeName()); + return false; + } + + if (gameHandler->battleTacticDist()) + { + if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) + { + gameHandler->complain("This is not a stack of side that has tactics!"); + return false; + } + } + else if (!isAboutActiveStack) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } + return true; +} + +bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) +{ switch(ba.actionType) { - case EActionType::END_TACTIC_PHASE: //wait - case EActionType::BAD_MORALE: - case EActionType::NO_ACTION: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::WALK: - { - auto wrapper = wrapAction(ba); - if(target.size() < 1) - { - gameHandler->complain("Destination required for move action."); - ok = false; - break; - } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move - if (!walkedTiles) - gameHandler->complain("Stack failed movement!"); - break; - } - case EActionType::DEFEND: - { - //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) - SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), - -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); - int oldDefenceValue = defence.totalValue(); - - defence.push_back(std::make_shared(defenseBonusToAdd)); - defence.push_back(std::make_shared(bonus2)); - - int difference = defence.totalValue() - oldDefenceValue; - std::vector buffer; - if(difference == 0) //give replacement bonus for creatures not reaching 5 defense points (20% of def becomes 0) - { - difference = 1; - buffer.push_back(alternativeWeakCreatureBonus); - } - else - { - buffer.push_back(defenseBonusToAdd); - } - - buffer.push_back(bonus2); - - sse.toUpdate.push_back(std::make_pair(ba.stackNumber, buffer)); - gameHandler->sendAndApply(&sse); - - BattleLogMessage message; - - MetaString text; - stack->addText(text, EMetaText::GENERAL_TXT, 120); - stack->addNameReplacement(text); - text.replaceNumber(difference); - - message.lines.push_back(text); - - gameHandler->sendAndApply(&message); - //don't break - we share code with next case - } - [[fallthrough]]; - case EActionType::WAIT: - { - auto wrapper = wrapAction(ba); - break; - } - case EActionType::RETREAT: //retreat/flee - { - if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) - gameHandler->complain("Cannot retreat!"); - else - owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); //surrendering side loses - break; - } - case EActionType::SURRENDER: - { - PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; - int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); - if (cost < 0) - gameHandler->complain("Cannot surrender!"); - else if (gameHandler->getResource(player, EGameResID::GOLD) < cost) - gameHandler->complain("Not enough gold to surrender!"); - else - { - gameHandler->giveResource(player, EGameResID::GOLD, -cost); - owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); //surrendering side loses - } - break; - } - case EActionType::WALK_AND_ATTACK: //walk or attack - { - auto wrapper = wrapAction(ba); - - if(!stack) - { - gameHandler->complain("No attacker"); - ok = false; - break; - } - - if(target.size() < 2) - { - gameHandler->complain("Two destinations required for attack action."); - ok = false; - break; - } - - BattleHex attackPos = target.at(0).hexValue; - BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); - - if(!destinationStack) - { - gameHandler->complain("Invalid target to attack"); - ok = false; - break; - } - - BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); - - logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); - - if(stack->getPosition() != attackPos - && !(stack->doubleWide() && (stack->getPosition() == attackPos.cloneInDirection(stack->destShiftDir(), false))) - ) - { - // we were not able to reach destination tile, nor occupy specified hex - // abort attack attempt, but treat this case as legal - we may have stepped onto a quicksands/mine - break; - } - - if(destinationStack && stack->unitId() == destinationStack->unitId()) //we should just move, it will be handled by following check - { - destinationStack = nullptr; - } - - if(!destinationStack) - { - gameHandler->complain("Unit can not attack itself"); - ok = false; - break; - } - - if(!CStack::isMeleeAttackPossible(stack, destinationStack)) - { - gameHandler->complain("Attack cannot be performed!"); - ok = false; - break; - } - - //attack - int totalAttacks = stack->totalAttacks.getMeleeValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); - const bool retaliation = destinationStack->ableToRetaliate(); - for (int i = 0; i < totalAttacks; ++i) - { - //first strike - if(i == 0 && firstStrike && retaliation) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - - //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification - if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) - { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack - } - - //counterattack - //we check retaliation twice, so if it unblocked during attack it will work only on next attack - if(stack->alive() - && !stack->hasBonusOfType(BonusType::BLOCKS_RETALIATION) - && (i == 0 && !firstStrike) - && retaliation && destinationStack->ableToRetaliate()) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); - } - } - - //return - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE) - && target.size() == 3 - && startingPos != stack->getPosition() - && startingPos == target.at(2).hexValue - && stack->alive()) - { - moveStack(ba.stackNumber, startingPos); - //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) - } - break; - } - case EActionType::SHOOT: - { - if(target.size() < 1) - { - gameHandler->complain("Destination required for shot action."); - ok = false; - break; - } - - auto destination = target.at(0).hexValue; - - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); - - if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) - { - gameHandler->complain("Cannot shoot!"); - break; - } - if (!destinationStack) - { - gameHandler->complain("No target to shoot!"); - break; - } - - auto wrapper = wrapAction(ba); - - makeAttack(stack, destinationStack, 0, destination, true, true, false); - - //ranged counterattack - if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) - && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) - && destinationStack->ableToRetaliate() - && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) - && stack->alive()) //attacker may have died (fire shield) - { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); - } - //allow more than one additional attack - - int totalRangedAttacks = stack->totalAttacks.getRangedValue(); - - //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - if(attackingHero) - { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); - } - - - for(int i = 1; i < totalRangedAttacks; ++i) - { - if( - stack->alive() - && destinationStack->alive() - && stack->shots.canUse() - ) - { - makeAttack(stack, destinationStack, 0, destination, false, true, false); - } - } - break; - } - case EActionType::CATAPULT: - { - auto wrapper = wrapAction(ba); - const CStack * shooter = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) - { - gameHandler->complain("We do not know how to shoot :P"); - } - else - { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, shooter, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult - auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); - parameters.setSpellLevel(shotLevel); - parameters.cast(gameHandler->spellEnv, target); - } - //finish by scope guard - break; - } - case EActionType::STACK_HEAL: //healing with First Aid Tent - { - auto wrapper = wrapAction(ba); - const CStack * healer = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - - if(target.size() < 1) - { - gameHandler->complain("Destination required for heal action."); - ok = false; - break; - } - - const battle::Unit * destStack = nullptr; - std::shared_ptr healerAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::HEALER)); - - if(target.at(0).unitValue) - destStack = target.at(0).unitValue; - else - destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); - - if(healer == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) - { - gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); - } - else - { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, healer, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent - auto dest = battle::Destination(destStack, target.at(0).hexValue); - parameters.setSpellLevel(0); - parameters.cast(gameHandler->spellEnv, {dest}); - } - break; - } + case EActionType::NO_ACTION: + return doEmptyAction(ba); + case EActionType::END_TACTIC_PHASE: + return doEndTacticsAction(ba); + case EActionType::RETREAT: + return doRetreatAction(ba); + case EActionType::SURRENDER: + return doSurrenderAction(ba); + case EActionType::HERO_SPELL: + return doHeroSpellAction(ba); + case EActionType::WALK: + return doWalkAction(ba); + case EActionType::WAIT: + return doWaitAction(ba); + case EActionType::DEFEND: + return doDefendAction(ba); + case EActionType::WALK_AND_ATTACK: + return doAttackAction(ba); + case EActionType::SHOOT: + return doShootAction(ba); + case EActionType::CATAPULT: + return doCatapultAction(ba); case EActionType::MONSTER_SPELL: - { - auto wrapper = wrapAction(ba); - - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - SpellID spellID = SpellID(ba.actionSubtype); - - std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); - - //TODO special bonus for genies ability - if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) - spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - - if (spellID < 0) - gameHandler->complain("That stack can't cast spells!"); - else - { - const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); - int32_t spellLvl = 0; - if(spellcaster) - vstd::amax(spellLvl, spellcaster->val); - if(randSpellcaster) - vstd::amax(spellLvl, randSpellcaster->val); - parameters.setSpellLevel(spellLvl); - parameters.cast(gameHandler->spellEnv, target); - } - break; - } + return doUnitSpellAction(ba); + case EActionType::BAD_MORALE: + return doBadMoraleAction(ba); + case EActionType::STACK_HEAL: + return doHealAction(ba); } + gameHandler->complain("Unrecognized action type received!!"); + return false; +} + +bool BattleActionProcessor::makeBattleAction(const BattleAction &ba) +{ + logGlobal->trace("Making action: %s", ba.toString()); + const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + + StartAction startAction(ba); + gameHandler->sendAndApply(&startAction); + + bool result = dispatchBattleAction(ba); + + EndAction endAction; + gameHandler->sendAndApply(&endAction); if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); - return ok; -} - -bool BattleActionProcessor::makeCustomAction(BattleAction & ba) -{ - switch(ba.actionType) - { - case EActionType::HERO_SPELL: - { - const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); - if (!h) - { - logGlobal->error("Wrong caster!"); - return false; - } - - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); - if (!s) - { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); - return false; - } - - spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); - - spells::detail::ProblemImpl problem; - - auto m = s->battleMechanics(¶meters); - - if(!m->canBeCast(problem))//todo: should we check aimed cast? - { - logGlobal->warn("Spell cannot be cast!"); - std::vector texts; - problem.getAll(texts); - for(auto s : texts) - logGlobal->warn(s); - return false; - } - - StartAction start_action(ba); - gameHandler->sendAndApply(&start_action); //start spell casting - - parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); - - EndAction end_action; - gameHandler->sendAndApply(&end_action); - return true; - } - } - return false; + return result; } int BattleActionProcessor::moveStack(int stack, BattleHex dest) { int ret = 0; - const CStack *curStack = gameHandler->battleGetStackByID(stack), - *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); + const CStack *curStack = gameHandler->battleGetStackByID(stack); + const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); assert(curStack); assert(dest < GameConstants::BFIELD_SIZE); diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 1f07a3c4b..f56c5b542 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -13,7 +13,6 @@ VCMI_LIB_NAMESPACE_BEGIN struct BattleLogMessage; struct BattleAttack; -class BattleProcessor; class BattleAction; struct BattleHex; class CStack; @@ -27,6 +26,7 @@ class CUnitState; VCMI_LIB_NAMESPACE_END class CGameHandler; +class BattleProcessor; class BattleActionProcessor : boost::noncopyable { @@ -48,11 +48,30 @@ class BattleActionProcessor : boost::noncopyable void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); + bool canStackAct(const CStack * stack); + + bool doEmptyAction(const BattleAction & ba); + bool doEndTacticsAction(const BattleAction & ba); + bool doRetreatAction(const BattleAction & ba); + bool doSurrenderAction(const BattleAction & ba); + bool doHeroSpellAction(const BattleAction & ba); + bool doWalkAction(const BattleAction & ba); + bool doWaitAction(const BattleAction & ba); + bool doDefendAction(const BattleAction & ba); + bool doAttackAction(const BattleAction & ba); + bool doShootAction(const BattleAction & ba); + bool doCatapultAction(const BattleAction & ba); + bool doUnitSpellAction(const BattleAction & ba); + bool doBadMoraleAction(const BattleAction & ba); + bool doHealAction(const BattleAction & ba); + + bool dispatchBattleAction(const BattleAction & ba); + public: - BattleActionProcessor(BattleProcessor * owner); + explicit BattleActionProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool makeBattleAction(BattleAction &ba); - bool makeCustomAction(BattleAction &ba); + bool makeBattleAction(const BattleAction &ba); + }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 416210201..96c56a143 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -290,21 +290,35 @@ void BattleFlowProcessor::activateNextStack() //TODO: activate next round if next == nullptr const auto & curB = *gameHandler->gameState()->curB; - const CStack * next = getNextStack(); - - if (!next) - return; - - BattleUnitsChanged removeGhosts; - - for(auto stack : curB.stacks) + // Find next stack that requires manual control + for (;;) { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } + const CStack * next = getNextStack(); - if(!removeGhosts.changedStacks.empty()) - gameHandler->sendAndApply(&removeGhosts); + if (!next) + return; + + BattleUnitsChanged removeGhosts; + + for(auto stack : curB.stacks) + { + if(stack->ghostPending) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); + } + + if(!removeGhosts.changedStacks.empty()) + gameHandler->sendAndApply(&removeGhosts); + + if (!tryMakeAutomaticAction(next)) + { + logGlobal->trace("Activating %s", next->nodeName()); + auto nextId = next->unitId(); + BattleSetActiveStack sas; + sas.stack = nextId; + gameHandler->sendAndApply(&sas); + break; + } + } } bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) @@ -423,7 +437,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) return s->unitOwner() == next->unitOwner() && s->canBeHealed(); }); - if (!possibleStacks.size()) + if (possibleStacks.empty()) { makeStackDoNothing(next); return true; @@ -452,25 +466,11 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) makeStackDoNothing(next); //end immediately if stack was affected by fear return true; } - else - { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - gameHandler->sendAndApply(&sas); - return false; - } + return false; } -void BattleFlowProcessor::onActionMade(const CStack *next) +bool BattleFlowProcessor::rollGoodMorale(const CStack * next) { - //we're after action, all results applied - owner->checkBattleStateChanges(); //check if this action ended the battle - - if(next == nullptr) - return; - //check for good morale auto nextStackMorale = next->moraleVal(); if( !next->hadMorale @@ -491,11 +491,38 @@ void BattleFlowProcessor::onActionMade(const CStack *next) bte.val = 1; bte.additionalInfo = 0; gameHandler->sendAndApply(&bte); //play animation + return true; } } + return false; +} - if (gameHandler->gameLobby()->state != EServerState::SHUTDOWN) - owner->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); +void BattleFlowProcessor::onActionMade(const BattleAction &ba) +{ + const CStack * next = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + + //we're after action, all results applied + owner->checkBattleStateChanges(); //check if this action ended the battle + + if(next == nullptr) + return; + + bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER|| ba.actionType ==EActionType::RETREAT; + + if (heroAction && next->alive()) + { + // this is action made by hero AND unit is alive (e.g. not killed by casted spell) + // keep current active stack for next action + return; + } + + if (rollGoodMorale(next)) + { + // Good morale - same stack makes 2nd turn + return; + } + + activateNextStack(); } void BattleFlowProcessor::makeStackDoNothing(const CStack * next) diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index aca2e9772..4e5969367 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -26,6 +26,7 @@ class BattleFlowProcessor : boost::noncopyable const CStack * getNextStack(); + bool rollGoodMorale(const CStack * stack); bool tryMakeAutomaticAction(const CStack * stack); void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); @@ -40,10 +41,10 @@ class BattleFlowProcessor : boost::noncopyable bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) public: - BattleFlowProcessor(BattleProcessor * owner); + explicit BattleFlowProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); void onBattleStarted(); void onTacticsEnded(); - void onActionMade(const CStack *stack); + void onActionMade(const BattleAction &ba); }; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index dcc9513db..b1b854aa6 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -270,58 +270,20 @@ bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) return false; } - return actionsProcessor->makeBattleAction(ba); -} - -bool BattleProcessor::makeCustomAction(PlayerColor player, BattleAction &ba) -{ - const BattleInfo * b = gameHandler->gameState()->curB; - - if(!b && gameHandler->complain("Can not make action - there is no battle ongoing!")) - return false; - - if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) - return false; - - if(b->tacticDistance) - { - gameHandler->complain("Can not cast spell during tactics mode!"); - return false; - } - - auto active = b->battleActiveUnit(); - if(!active && gameHandler->complain("No active unit in battle!")) - return false; - - auto unitOwner = b->battleGetOwner(active); - - if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) - return false; - - if(ba.actionType != EActionType::HERO_SPELL && gameHandler->complain("Invalid custom action type!")) - return false; - - return actionsProcessor->makeCustomAction(ba); + return makeBattleAction(ba); } void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) { resultProcessor->setBattleResult(resultType, victoriusSide); + resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); } -bool BattleProcessor::makeBattleAction(BattleAction &ba) +bool BattleProcessor::makeBattleAction(const BattleAction &ba) { - return actionsProcessor->makeBattleAction(ba); -} - -bool BattleProcessor::makeCustomAction(BattleAction &ba) -{ - return actionsProcessor->makeCustomAction(ba); -} - -void BattleProcessor::endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2) -{ - resultProcessor->endBattle(tile, hero1, hero2); + bool result = actionsProcessor->makeBattleAction(ba); + flowProcessor->onActionMade(ba); + return result; } void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index bdc5bc53b..2a87dc651 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -44,8 +44,7 @@ class BattleProcessor : boost::noncopyable void checkBattleStateChanges(); void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - bool makeBattleAction(BattleAction &ba); - bool makeCustomAction(BattleAction &ba); + bool makeBattleAction(const BattleAction &ba); void setBattleResult(EBattleResult resultType, int victoriusSide); public: @@ -60,9 +59,7 @@ public: void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle bool makeBattleAction(PlayerColor player, BattleAction &ba); - bool makeCustomAction(PlayerColor player, BattleAction &ba); - void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); void battleAfterLevelUp(const BattleResult &result); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 67df5a9b7..6f1fe3451 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -39,8 +39,8 @@ #include "../../lib/spells/Problem.h" BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner) - : owner(owner) - , gameHandler(nullptr) +// : owner(owner) + : gameHandler(nullptr) { } @@ -550,5 +550,4 @@ void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victor battleResult->result = resultType; battleResult->winner = victoriusSide; //surrendering side loses gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); - } diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index e883c874a..6c14c8122 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -61,14 +61,14 @@ struct FinishingBattleHelper class BattleResultProcessor : boost::noncopyable { - BattleProcessor * owner; +// BattleProcessor * owner; CGameHandler * gameHandler; std::unique_ptr battleResult; std::unique_ptr finishingBattle; public: - BattleResultProcessor(BattleProcessor * owner); + explicit BattleResultProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); void setBattleResult(EBattleResult resultType, int victoriusSide); diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 46d9ec7cb..e3deeef80 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -43,7 +43,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner): bool CBattleQuery::blocksPack(const CPack * pack) const { const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) && strcmp(name, typeid(MakeCustomAction).name()); + return strcmp(name, typeid(MakeAction).name()) != 0; } void CBattleQuery::onRemoval(PlayerColor color) From a1d3181a98ec9f5a8da7e0663258a3da97657ace Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 14:56:24 +0300 Subject: [PATCH 0108/1248] Unified spellcasting handling with other actions --- AI/BattleAI/BattleAI.cpp | 15 +- AI/BattleAI/BattleAI.h | 2 +- server/battles/BattleActionProcessor.cpp | 5 - server/battles/BattleActionProcessor.h | 8 +- server/battles/BattleFlowProcessor.cpp | 178 +++++++++++++---------- server/battles/BattleFlowProcessor.h | 11 +- server/battles/BattleProcessor.h | 22 ++- server/battles/BattleResultProcessor.h | 8 +- 8 files changed, 143 insertions(+), 106 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 28e16edd5..146ee8087 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -283,7 +283,8 @@ void CBattleAI::activeStack( const CStack * stack ) return; } - attemptCastingSpell(); + if (attemptCastingSpell()) + return; logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); @@ -476,14 +477,14 @@ BattleAction CBattleAI::useCatapult(const CStack * stack) return attack; } -void CBattleAI::attemptCastingSpell() +bool CBattleAI::attemptCastingSpell() { auto hero = cb->battleGetMyHero(); if(!hero) - return; + return false; if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK) - return; + return false; LOGL("Casting spells sounds like fun. Let's see..."); //Get all spells we can cast @@ -522,7 +523,7 @@ void CBattleAI::attemptCastingSpell() } LOGFL("Found %d spell-target combinations.", possibleCasts.size()); if(possibleCasts.empty()) - return; + return false; using ValueMap = PossibleSpellcast::ValueMap; @@ -657,7 +658,7 @@ void CBattleAI::attemptCastingSpell() if(battleIsFinishedOpt) { print("No need to cast a spell. Battle will finish soon."); - return; + return false; } } } @@ -786,10 +787,12 @@ void CBattleAI::attemptCastingSpell() spellcast.stackNumber = (!side) ? -1 : -2; cb->battleMakeSpellAction(spellcast); movesSkippedByDefense = 0; + return true; } else { LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); + return false; } } diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 37338d299..38551c13b 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -68,7 +68,7 @@ public: ~CBattleAI(); void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void attemptCastingSpell(); + bool attemptCastingSpell(); void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index d890b466b..5fdcc5bf7 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -136,13 +136,8 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) return false; } - StartAction start_action(ba); - gameHandler->sendAndApply(&start_action); //start spell casting - parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); - EndAction end_action; - gameHandler->sendAndApply(&end_action); return true; } diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index f56c5b542..7dc7a15c3 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -18,7 +18,8 @@ struct BattleHex; class CStack; enum class BonusType; -namespace battle { +namespace battle +{ class Unit; class CUnitState; } @@ -28,6 +29,7 @@ VCMI_LIB_NAMESPACE_END class CGameHandler; class BattleProcessor; +/// Processes incoming battle action queries and applies requested action(s) class BattleActionProcessor : boost::noncopyable { using FireShieldInfo = std::vector>; @@ -71,7 +73,5 @@ public: explicit BattleActionProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool makeBattleAction(const BattleAction &ba); - + bool makeBattleAction(const BattleAction & ba); }; - diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 96c56a143..be38cf39d 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -147,6 +147,72 @@ void BattleFlowProcessor::onBattleStarted() onTacticsEnded(); } +void BattleFlowProcessor::trySummonGuardians(const CStack * stack) +{ + if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) + return; + + std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); + auto accessibility = gameHandler->getAccesibility(); + CreatureID creatureData = CreatureID(summonInfo->subtype); + std::vector targetHexes; + const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard + const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); + + /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. + For one-hex targets there are four guardians - front, back and one per side (up + down). + Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front + Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ + if (!guardianIsBig) + targetHexes = stack->getSurroundingHexes(); + else + summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + + for(auto hex : targetHexes) + { + if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex + { + battle::UnitInfo info; + info.id = gameHandler->gameState()->curB->battleNextUnitId(); + info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); + info.type = creatureData; + info.side = stack->unitSide(); + info.position = hex; + info.summoned = true; + + BattleUnitsChanged pack; + pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); + info.save(pack.changedStacks.back().data); + gameHandler->sendAndApply(&pack); + } + } +} + +void BattleFlowProcessor::castOpeningSpells() +{ + for (int i = 0; i < 2; ++i) + { + auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); + if (!h) + continue; + + TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + + for (auto b : *bl) + { + spells::BonusCaster caster(h, b); + + const CSpell * spell = SpellID(b->subtype).toSpell(); + + spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + parameters.setSpellLevel(3); + parameters.setEffectDuration(b->val); + parameters.massive = true; + parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); + } + } +} + void BattleFlowProcessor::onTacticsEnded() { //initial stacks appearance triggers, e.g. built-in bonus spells @@ -154,73 +220,17 @@ void BattleFlowProcessor::onTacticsEnded() for (CStack * stack : initialStacks) { - if (stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) - { - std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = gameHandler->getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); - std::vector targetHexes; - const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard - const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); - - /*Chosen idea for two hex units was to cover all possible surrounding hexes of target unit with as small number of stacks as possible. - For one-hex targets there are four guardians - front, back and one per side (up + down). - Two-hex targets are wider and the difference is there are two guardians per side to cover 3 hexes + extra hex in the front - Additionally, there are special cases for starting positions etc., where guardians would be outside of battlefield if spawned normally*/ - if (!guardianIsBig) - targetHexes = stack->getSurroundingHexes(); - else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); - - for(auto hex : targetHexes) - { - if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex - { - battle::UnitInfo info; - info.id = gameHandler->gameState()->curB->battleNextUnitId(); - info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); - info.type = creatureData; - info.side = stack->unitSide(); - info.position = hex; - info.summoned = true; - - BattleUnitsChanged pack; - pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); - info.save(pack.changedStacks.back().data); - gameHandler->sendAndApply(&pack); - } - } - } - + trySummonGuardians(stack); stackEnchantedTrigger(stack); } - //spells opening battle - for (int i = 0; i < 2; ++i) - { - auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); - if (h) - { - TConstBonusListPtr bl = h->getBonuses(Selector::type()(BonusType::OPENING_BATTLE_SPELL)); + castOpeningSpells(); - for (auto b : *bl) - { - spells::BonusCaster caster(h, b); - - const CSpell * spell = SpellID(b->subtype).toSpell(); - - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); - parameters.setSpellLevel(3); - parameters.setEffectDuration(b->val); - parameters.massive = true; - parameters.castIfPossible(gameHandler->spellEnv, spells::Target()); - } - } - } // it is possible that due to opening spells one side was eliminated -> check for end of battle owner->checkBattleStateChanges(); startNextRound(true); + activateNextStack(); } void BattleFlowProcessor::startNextRound(bool isFirstRound) @@ -245,8 +255,6 @@ void BattleFlowProcessor::startNextRound(bool isFirstRound) if(stack->alive() && !isFirstRound) stackEnchantedTrigger(stack); } - - activateNextStack(); } const CStack * BattleFlowProcessor::getNextStack() @@ -296,7 +304,13 @@ void BattleFlowProcessor::activateNextStack() const CStack * next = getNextStack(); if (!next) - return; + { + // No stacks to move - start next round + startNextRound(false); + next = getNextStack(); + if (!next) + throw std::runtime_error("Failed to find valid stack to act!"); + } BattleUnitsChanged removeGhosts; @@ -499,27 +513,41 @@ bool BattleFlowProcessor::rollGoodMorale(const CStack * next) void BattleFlowProcessor::onActionMade(const BattleAction &ba) { - const CStack * next = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + const auto & battle = gameHandler->gameState()->curB; + + const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber); + const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID()); + assert(activeStack != nullptr); //we're after action, all results applied owner->checkBattleStateChanges(); //check if this action ended the battle - if(next == nullptr) - return; + bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType ==EActionType::END_TACTIC_PHASE; - bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER|| ba.actionType ==EActionType::RETREAT; - - if (heroAction && next->alive()) + if (heroAction) { - // this is action made by hero AND unit is alive (e.g. not killed by casted spell) - // keep current active stack for next action - return; + if (activeStack->alive()) + { + // this is action made by hero AND unit is alive (e.g. not killed by casted spell) + // keep current active stack for next action + BattleSetActiveStack sas; + sas.stack = activeStack->unitId(); + gameHandler->sendAndApply(&sas); + return; + } } - - if (rollGoodMorale(next)) + else { - // Good morale - same stack makes 2nd turn - return; + assert(actedStack != nullptr); + + if (rollGoodMorale(actedStack)) + { + // Good morale - same stack makes 2nd turn + BattleSetActiveStack sas; + sas.stack = actedStack->unitId(); + gameHandler->sendAndApply(&sas); + return; + } } activateNextStack(); diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index 4e5969367..9c9b8d38c 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_END class CGameHandler; class BattleProcessor; +/// Controls flow of battles - battle startup actions and switching to next stack or next round after actions class BattleFlowProcessor : boost::noncopyable { BattleProcessor * owner; @@ -30,15 +31,17 @@ class BattleFlowProcessor : boost::noncopyable bool tryMakeAutomaticAction(const CStack * stack); void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void trySummonGuardians(const CStack * stack); + void castOpeningSpells(); void activateNextStack(); void startNextRound(bool isFirstRound); void stackEnchantedTrigger(const CStack * stack); - void removeObstacle(const CObstacleInstance &obstacle); - void stackTurnTrigger(const CStack *stack); + void removeObstacle(const CObstacleInstance & obstacle); + void stackTurnTrigger(const CStack * stack); void makeStackDoNothing(const CStack * next); - bool makeAutomaticAction(const CStack *stack, BattleAction &ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + bool makeAutomaticAction(const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) public: explicit BattleFlowProcessor(BattleProcessor * owner); @@ -46,5 +49,5 @@ public: void onBattleStarted(); void onTacticsEnded(); - void onActionMade(const BattleAction &ba); + void onActionMade(const BattleAction & ba); }; diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 2a87dc651..038e10bca 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -27,6 +27,7 @@ class BattleActionProcessor; class BattleFlowProcessor; class BattleResultProcessor; +/// Main class for battle handling. Contains all public interface for battles that is accessible from outside, e.g. for CGameHandler class BattleProcessor : boost::noncopyable { friend class BattleActionProcessor; @@ -39,14 +40,15 @@ class BattleProcessor : boost::noncopyable std::unique_ptr resultProcessor; void updateGateState(); - void engageIntoBattle( PlayerColor player ); + void engageIntoBattle(PlayerColor player); void checkBattleStateChanges(); void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - bool makeBattleAction(const BattleAction &ba); + bool makeBattleAction(const BattleAction & ba); void setBattleResult(EBattleResult resultType, int victoriusSide); + public: explicit BattleProcessor(CGameHandler * gameHandler); BattleProcessor(); @@ -54,14 +56,20 @@ public: void setGameHandler(CGameHandler * gameHandler); - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + /// Starts battle with specified parameters + void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); + /// Starts battle between two armies (which can also be heroes) at specified tile + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); + /// Starts battle between two armies (which can also be heroes) at position of 2nd object + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); - bool makeBattleAction(PlayerColor player, BattleAction &ba); + /// Processing of incoming battle action netpack + bool makeBattleAction(PlayerColor player, BattleAction & ba); + /// Applies results of a battle once player agrees to them void endBattleConfirm(const BattleInfo * battleInfo); - void battleAfterLevelUp(const BattleResult &result); + /// Applies results of a battle after potential levelup + void battleAfterLevelUp(const BattleResult & result); template void serialize(Handler &h, const int version) { diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index 6c14c8122..ec4b13ab6 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -24,7 +24,7 @@ struct CasualtiesAfterBattle { using TStackAndItsNewCount = std::pair; using TSummoned = std::map; - enum {ERASE = -1}; + // enum {ERASE = -1}; const CArmedInstance * army; std::vector newStackCounts; std::vector removedWarMachines; @@ -32,7 +32,7 @@ struct CasualtiesAfterBattle ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); - void updateArmy(CGameHandler *gh); + void updateArmy(CGameHandler * gh); }; struct FinishingBattleHelper @@ -61,7 +61,7 @@ struct FinishingBattleHelper class BattleResultProcessor : boost::noncopyable { -// BattleProcessor * owner; + // BattleProcessor * owner; CGameHandler * gameHandler; std::unique_ptr battleResult; @@ -74,5 +74,5 @@ public: void setBattleResult(EBattleResult resultType, int victoriusSide); void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); - void battleAfterLevelUp(const BattleResult &result); + void battleAfterLevelUp(const BattleResult & result); }; From a1092e0f3f63849ec8eb2fd983a3a077bfc94087 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 15:18:16 +0300 Subject: [PATCH 0109/1248] Fix battle ending --- server/battles/BattleFlowProcessor.cpp | 13 ++++++++++--- server/battles/BattleProcessor.cpp | 5 ++++- server/battles/BattleProcessor.h | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index be38cf39d..73adaa533 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -227,7 +227,8 @@ void BattleFlowProcessor::onTacticsEnded() castOpeningSpells(); // it is possible that due to opening spells one side was eliminated -> check for end of battle - owner->checkBattleStateChanges(); + if (owner->checkBattleStateChanges()) + return; startNextRound(true); activateNextStack(); @@ -301,6 +302,10 @@ void BattleFlowProcessor::activateNextStack() // Find next stack that requires manual control for (;;) { + // battle has ended + if (owner->checkBattleStateChanges()) + return; + const CStack * next = getNextStack(); if (!next) @@ -520,7 +525,10 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) assert(activeStack != nullptr); //we're after action, all results applied - owner->checkBattleStateChanges(); //check if this action ended the battle + + // check whether action has ended the battle + if(owner->checkBattleStateChanges()) + return; bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType ==EActionType::END_TACTIC_PHASE; @@ -571,7 +579,6 @@ bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction gameHandler->sendAndApply(&bsa); bool ret = owner->makeBattleAction(ba); - owner->checkBattleStateChanges(); return ret; } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index b1b854aa6..9b9a13770 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -165,7 +165,7 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co gameHandler->sendAndApply(&bs); } -void BattleProcessor::checkBattleStateChanges() +bool BattleProcessor::checkBattleStateChanges() { //check if drawbridge state need to be changes if (gameHandler->battleGetSiegeLevel() > 0) @@ -175,7 +175,10 @@ void BattleProcessor::checkBattleStateChanges() if (auto result = gameHandler->battleIsFinished()) { setBattleResult(EBattleResult::NORMAL, *result); + return true; } + + return false; } void BattleProcessor::updateGateState() diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 038e10bca..07d437b6e 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -42,7 +42,7 @@ class BattleProcessor : boost::noncopyable void updateGateState(); void engageIntoBattle(PlayerColor player); - void checkBattleStateChanges(); + bool checkBattleStateChanges(); void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); bool makeBattleAction(const BattleAction & ba); From c516b5a64e463c8c45ec26bb68e12559cebeb1b4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 16:17:19 +0300 Subject: [PATCH 0110/1248] Fixes for several discovered edge cases --- lib/NetPacksLib.cpp | 12 +++++------- server/NetPacksServer.cpp | 2 +- server/battles/BattleFlowProcessor.cpp | 19 ++++++++++++------- server/battles/BattleProcessor.cpp | 20 ++++++++++++++------ server/battles/BattleProcessor.h | 4 ++-- 5 files changed, 34 insertions(+), 23 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index f611b1854..e1044bdf6 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2284,14 +2284,12 @@ void StartAction::applyGs(CGameState *gs) return; } - if(ba.actionType != EActionType::HERO_SPELL) //don't check for stack if it's custom action by hero - { - assert(st); - } - else - { + [[maybe_unused]] bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType == EActionType::END_TACTIC_PHASE; + + assert(st || heroAction); // stack must exists for all non-hero actions + + if(ba.actionType == EActionType::HERO_SPELL) gs->curB->sides[ba.side].usedSpellsHistory.emplace_back(ba.actionSubtype); - } switch(ba.actionType) { diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 8218997cd..72499d783 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -285,7 +285,7 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) if (!gh.hasPlayerAt(pack.player, pack.c)) gh.throwAndComplain(&pack, "No such pack.player!"); - result = gh.battles->makeBattleAction(pack.player, pack.ba); + result = gh.battles->makePlayerBattleAction(pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 73adaa533..399bd9012 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -520,9 +520,8 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) { const auto & battle = gameHandler->gameState()->curB; - const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber); - const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID()); - assert(activeStack != nullptr); + const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber, false); + const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID(), false); //we're after action, all results applied @@ -530,11 +529,17 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) if(owner->checkBattleStateChanges()) return; - bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType ==EActionType::END_TACTIC_PHASE; + bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT; + bool tacticsAction = ba.actionType == EActionType::END_TACTIC_PHASE; - if (heroAction) + if (activeStack == nullptr && !tacticsAction) { - if (activeStack->alive()) + throw std::runtime_error("Unexpected action - no active stack!"); + } + + if (heroAction || tacticsAction) + { + if (!tacticsAction && activeStack->alive()) { // this is action made by hero AND unit is alive (e.g. not killed by casted spell) // keep current active stack for next action @@ -578,7 +583,7 @@ bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction bsa.askPlayerInterface = false; gameHandler->sendAndApply(&bsa); - bool ret = owner->makeBattleAction(ba); + bool ret = owner->makeAutomaticBattleAction(ba); return ret; } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 9b9a13770..9378b8037 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -236,7 +236,7 @@ void BattleProcessor::updateGateState() gameHandler->sendAndApply(&db); } -bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) +bool BattleProcessor::makePlayerBattleAction(PlayerColor player, BattleAction &ba) { const BattleInfo * b = gameHandler->gameState()->curB; @@ -263,6 +263,14 @@ bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) } else { + bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType == EActionType::END_TACTIC_PHASE; + + if (ba.stackNumber != b->getActiveStackID() && heroAction == false) + { + gameHandler->complain("Can not make actions - stack is not active!"); + return false; + } + auto active = b->battleActiveUnit(); if(!active && gameHandler->complain("No active unit in battle!")) return false; @@ -273,7 +281,9 @@ bool BattleProcessor::makeBattleAction(PlayerColor player, BattleAction &ba) return false; } - return makeBattleAction(ba); + bool result = actionsProcessor->makeBattleAction(ba); + flowProcessor->onActionMade(ba); + return result; } void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) @@ -282,11 +292,9 @@ void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSid resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); } -bool BattleProcessor::makeBattleAction(const BattleAction &ba) +bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba) { - bool result = actionsProcessor->makeBattleAction(ba); - flowProcessor->onActionMade(ba); - return result; + return actionsProcessor->makeBattleAction(ba); } void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 07d437b6e..c30e2e6da 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -45,7 +45,7 @@ class BattleProcessor : boost::noncopyable bool checkBattleStateChanges(); void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - bool makeBattleAction(const BattleAction & ba); + bool makeAutomaticBattleAction(const BattleAction & ba); void setBattleResult(EBattleResult resultType, int victoriusSide); @@ -64,7 +64,7 @@ public: void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); /// Processing of incoming battle action netpack - bool makeBattleAction(PlayerColor player, BattleAction & ba); + bool makePlayerBattleAction(PlayerColor player, BattleAction & ba); /// Applies results of a battle once player agrees to them void endBattleConfirm(const BattleInfo * battleInfo); From 013417fb7e7ac2bc0711dd2b50bd66e3ff8ffc97 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 19:18:14 +0300 Subject: [PATCH 0111/1248] Code cleanup --- AI/BattleAI/BattleAI.cpp | 3 +- AI/EmptyAI/CEmptyAI.cpp | 6 ++ AI/EmptyAI/CEmptyAI.h | 1 + AI/StupidAI/StupidAI.cpp | 1 + AI/VCAI/VCAI.cpp | 5 +- AI/VCAI/VCAI.h | 1 + client/CPlayerInterface.cpp | 15 ++-- client/CPlayerInterface.h | 2 +- client/Client.h | 4 +- client/NetPacksClient.cpp | 6 +- client/battle/BattleActionsController.cpp | 4 +- client/battle/BattleEffectsController.cpp | 6 +- client/battle/BattleEffectsController.h | 2 +- client/battle/BattleInterface.cpp | 22 +++--- client/battle/BattleInterface.h | 6 +- client/battle/BattleStacksController.cpp | 5 +- client/battle/BattleStacksController.h | 4 +- lib/CGameInterface.h | 7 +- lib/NetPacksLib.cpp | 2 +- lib/battle/BattleAction.cpp | 47 +++++++++++-- lib/battle/BattleAction.h | 7 +- server/battles/BattleActionProcessor.cpp | 84 +++++++++++++++++++---- server/battles/BattleActionProcessor.h | 6 +- server/battles/BattleFlowProcessor.cpp | 67 +++++++++--------- server/battles/BattleFlowProcessor.h | 2 + server/battles/BattleProcessor.cpp | 50 +------------- server/battles/BattleProcessor.h | 2 +- 27 files changed, 215 insertions(+), 152 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 146ee8087..1bee0e9c7 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -18,6 +18,7 @@ #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/CStack.h" // TODO: remove @@ -781,7 +782,7 @@ bool CBattleAI::attemptCastingSpell() LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; spellcast.actionType = EActionType::HERO_SPELL; - spellcast.actionSubtype = castToPerform.spell->id; + spellcast.spell = castToPerform.spell->getId(); spellcast.setTarget(castToPerform.dest); spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 60c13382c..f3b4dd9c5 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -12,6 +12,7 @@ #include "../../lib/CRandomGenerator.h" #include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" void CEmptyAI::saveGame(BinarySerializer & h, const int version) { @@ -73,3 +74,8 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, { cb->selectionMade(0, askID); } + +std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 2598f5dbe..a71806e62 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -32,6 +32,7 @@ public: void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; }; #define NAME "EmptyAI 0.1" diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 85a9fe571..1ccc37f78 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -13,6 +13,7 @@ #include "../../lib/CStack.h" #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" +#include "../../lib/battle/BattleAction.h" static std::shared_ptr cbc; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index f20ceda1f..8fd3ac923 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -2890,4 +2890,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) return true; } - +std::optional VCAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 7d57ad681..e1fd11c0f 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -203,6 +203,7 @@ public: void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; void battleEnd(const BattleResult * br, QueryID queryID) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; void makeTurn(); void mainLoop(); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 69fde8c7b..3a407a796 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -128,7 +128,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): destinationTeleportPos = int3(-1); GH.defActionsDef = 0; LOCPLINT = this; - curAction = nullptr; playerID=Player; human=true; battleInt = nullptr; @@ -769,8 +768,7 @@ void CPlayerInterface::actionStarted(const BattleAction &action) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - curAction = new BattleAction(action); - battleInt->startAction(curAction); + battleInt->startAction(action); } void CPlayerInterface::actionFinished(const BattleAction &action) @@ -778,9 +776,7 @@ void CPlayerInterface::actionFinished(const BattleAction &action) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->endAction(curAction); - delete curAction; - curAction = nullptr; + battleInt->endAction(action); } void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack @@ -935,8 +931,6 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba) EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - assert(curAction); - StackAttackInfo info; info.attacker = cb->battleGetStackByID(ba->stackAttacking); info.defender = nullptr; @@ -2110,3 +2104,8 @@ void CPlayerInterface::showWorldViewEx(const std::vector& objectP EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->openWorldView(objectPositions, showTerrain ); } + +std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index d4372ec5f..c85fce3be 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -66,7 +66,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable int autosaveCount; std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) - const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr) ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation int3 destinationTeleportPos; @@ -173,6 +172,7 @@ protected: // Call-ins from server, should not be called directly, but only via void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleGateStateChanged(const EGateState state) override; void yourTacticPhase(int distance) override; + std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; public: // public interface for use by client via LOCPLINT access diff --git a/client/Client.h b/client/Client.h index 64a3f5f04..013db8e28 100644 --- a/client/Client.h +++ b/client/Client.h @@ -13,7 +13,6 @@ #include #include "../lib/IGameCallback.h" -#include "../lib/battle/BattleAction.h" #include "../lib/battle/CBattleInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,6 +24,7 @@ class CBattleGameInterface; class CGameInterface; class BinaryDeserializer; class BinarySerializer; +class BattleAction; template class CApplier; @@ -118,7 +118,7 @@ public: std::map>> additionalBattleInts; - std::optional curbaction; + std::unique_ptr currentBattleAction; CClient(); ~CClient(); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index c2f89316f..764c954d0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -784,7 +784,7 @@ void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) { - cl.curbaction = std::make_optional(pack.ba); + cl.currentBattleAction = std::make_unique(pack.ba); callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba); } @@ -830,8 +830,8 @@ void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.curbaction); - cl.curbaction.reset(); + callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.currentBattleAction); + cl.currentBattleAction.reset(); } void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 1ccdbf10f..7f45e126e 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -286,7 +286,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) { heroSpellToCast = std::make_shared(); heroSpellToCast->actionType = EActionType::HERO_SPELL; - heroSpellToCast->actionSubtype = spellID; //spell number + heroSpellToCast->spell = spellID; heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; @@ -314,7 +314,7 @@ void BattleActionsController::castThisSpell(SpellID spellID) const CSpell * BattleActionsController::getHeroSpellToCast( ) const { if (heroSpellToCast) - return SpellID(heroSpellToCast->actionSubtype).toSpell(); + return heroSpellToCast->spell.toSpell(); return nullptr; } diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index df394b7d8..bdaba92af 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -94,13 +94,13 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt owner.waitForAnimations(); } -void BattleEffectsController::startAction(const BattleAction* action) +void BattleEffectsController::startAction(const BattleAction & action) { owner.checkForAnimations(); - const CStack *stack = owner.curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = owner.curInt->cb->battleGetStackByID(action.stackNumber); - switch(action->actionType) + switch(action.actionType) { case EActionType::WAIT: owner.appendBattleLog(stack->formatGeneralMessage(136)); diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 574b85212..3ea1452b4 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -60,7 +60,7 @@ public: BattleEffectsController(BattleInterface & owner); - void startAction(const BattleAction* action); + void startAction(const BattleAction & action); //displays custom effect on the battlefield void displayEffect(EBattleEffect effect, const BattleHex & destTile); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 6420cdc77..c21af7bf2 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -234,7 +234,7 @@ void BattleInterface::newRound(int number) console->addText(CGI->generaltexth->allTexts[412]); } -void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 additional) +void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) { const CStack * actor = nullptr; if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) @@ -253,7 +253,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, si32 addit ba.side = side.value(); ba.actionType = action; ba.aimToHex(tile); - ba.actionSubtype = additional; + ba.spell = spell; sendCommand(ba, actor); } @@ -567,12 +567,12 @@ bool BattleInterface::makingTurn() const return stacksController->getActiveStack() != nullptr; } -void BattleInterface::endAction(const BattleAction* action) +void BattleInterface::endAction(const BattleAction &action) { // it is possible that tactics mode ended while opening music is still playing waitForAnimations(); - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast activateStack(); @@ -585,7 +585,7 @@ void BattleInterface::endAction(const BattleAction* action) tacticNextStack(stack); //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed - if(action->actionType == EActionType::HERO_SPELL) + if(action.actionType == EActionType::HERO_SPELL) fieldController->redrawBackgroundWithHexes(); } @@ -594,15 +594,15 @@ void BattleInterface::appendBattleLog(const std::string & newEntry) console->addText(newEntry); } -void BattleInterface::startAction(const BattleAction* action) +void BattleInterface::startAction(const BattleAction & action) { - if(action->actionType == EActionType::END_TACTIC_PHASE) + if(action.actionType == EActionType::END_TACTIC_PHASE) { windowObject->tacticPhaseEnded(); return; } - const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber); + const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); if (stack) { @@ -610,17 +610,17 @@ void BattleInterface::startAction(const BattleAction* action) } else { - assert(action->actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number + assert(action.actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number } stacksController->startAction(action); - if(action->actionType == EActionType::HERO_SPELL) //when hero casts spell + if(action.actionType == EActionType::HERO_SPELL) //when hero casts spell return; if (!stack) { - logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action->stackNumber); + logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action.stackNumber); return; } diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index fabd68984..4124c5f47 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -156,7 +156,7 @@ public: void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all void requestAutofightingAIToTakeAction(); - void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1); + void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE); void sendCommand(BattleAction command, const CStack * actor = nullptr); const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell @@ -188,7 +188,7 @@ public: void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); //call-ins - void startAction(const BattleAction* action); + void startAction(const BattleAction & action); void stackReset(const CStack * stack); void stackAdded(const CStack * stack); //new stack appeared on battlefield void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled @@ -211,7 +211,7 @@ public: void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - void endAction(const BattleAction* action); + void endAction(const BattleAction & action); void obstaclePlaced(const std::vector> oi); void obstacleRemoved(const std::vector & obstacles); diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 12f8dc368..321adc2bd 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -31,6 +31,7 @@ #include "../../CCallback.h" #include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" @@ -663,7 +664,7 @@ bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex return false; } -void BattleStacksController::endAction(const BattleAction* action) +void BattleStacksController::endAction(const BattleAction & action) { owner.checkForAnimations(); @@ -688,7 +689,7 @@ void BattleStacksController::endAction(const BattleAction* action) removeExpiredColorFilters(); } -void BattleStacksController::startAction(const BattleAction* action) +void BattleStacksController::startAction(const BattleAction & action) { removeExpiredColorFilters(); } diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 0be41d287..30352a3db 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -115,8 +115,8 @@ public: void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest - void startAction(const BattleAction* action); - void endAction(const BattleAction* action); + void startAction(const BattleAction & action); + void endAction(const BattleAction & action); void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 3ee27c21a..089dc2e5f 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -9,7 +9,6 @@ */ #pragma once -#include "battle/BattleAction.h" #include "IGameEventsReceiver.h" #include "spells/ViewSpellInt.h" @@ -36,6 +35,7 @@ class CCreatureSet; class CArmedInstance; class IShipyard; class IMarket; +class BattleAction; struct BattleResult; struct BattleAttack; struct BattleStackAttacked; @@ -107,10 +107,7 @@ public: virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) - { - return std::nullopt; - } + virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0; virtual void saveGame(BinarySerializer & h, const int version) = 0; virtual void loadGame(BinaryDeserializer & h, const int version) = 0; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index e1044bdf6..402be6fa1 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2289,7 +2289,7 @@ void StartAction::applyGs(CGameState *gs) assert(st || heroAction); // stack must exists for all non-hero actions if(ba.actionType == EActionType::HERO_SPELL) - gs->curB->sides[ba.side].usedSpellsHistory.emplace_back(ba.actionSubtype); + gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell); switch(ba.actionType) { diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index 1bfdd37ba..5a92ce08e 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -20,8 +20,7 @@ static const int32_t INVALID_UNIT_ID = -1000; BattleAction::BattleAction(): side(-1), stackNumber(-1), - actionType(EActionType::NO_ACTION), - actionSubtype(-1) + actionType(EActionType::NO_ACTION) { } @@ -80,7 +79,7 @@ BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, con { BattleAction ba; ba.actionType = EActionType::MONSTER_SPELL; - ba.actionSubtype = spellID; + ba.spell = spellID; ba.setTarget(target); ba.side = stack->unitSide(); ba.stackNumber = stack->unitId(); @@ -144,7 +143,7 @@ std::string BattleAction::toString() const } boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); - fmt % static_cast(side) % stackNumber % actionTypeStream.str() % actionSubtype % targetStream.str(); + fmt % static_cast(side) % stackNumber % actionTypeStream.str() % spell.getNum() % targetStream.str(); return fmt.str(); } @@ -183,7 +182,7 @@ battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const void BattleAction::setTarget(const battle::Target & target_) { - target.clear(); + target.clear(); for(const auto & destination : target_) { if(destination.unitValue == nullptr) @@ -193,6 +192,44 @@ void BattleAction::setTarget(const battle::Target & target_) } } +bool BattleAction::isUnitAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::WAIT, + EActionType::DEFEND, + EActionType::WALK_AND_ATTACK, + EActionType::SHOOT, + EActionType::CATAPULT, + EActionType::MONSTER_SPELL, + EActionType::BAD_MORALE, + EActionType::STACK_HEAL + }; + + return vstd::contains(actions, actionType); +} + +bool BattleAction::isSpellAction() const +{ + static const std::array actions = { + EActionType::HERO_SPELL, + EActionType::MONSTER_SPELL + }; + + return vstd::contains(actions, actionType); +} + +bool BattleAction::isTacticsAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::END_TACTIC_PHASE, + EActionType::RETREAT, + EActionType::SURRENDER + }; + + return vstd::contains(actions, actionType); +} std::ostream & operator<<(std::ostream & os, const BattleAction & ba) { diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index db65ac37e..12eba04e4 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -28,7 +28,7 @@ public: ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, EActionType actionType; //use ActionType enum for values - si32 actionSubtype; + SpellID spell; BattleAction(); @@ -43,6 +43,9 @@ public: static BattleAction makeRetreat(ui8 side); static BattleAction makeSurrender(ui8 side); + bool isTacticsAction() const; + bool isUnitAction() const; + bool isSpellAction() const; std::string toString() const; void aimToHex(const BattleHex & destination); @@ -56,7 +59,7 @@ public: h & side; h & stackNumber; h & actionType; - h & actionSubtype; + h & spell; h & target; } private: diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 5fdcc5bf7..e9903ddf3 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -58,16 +58,21 @@ bool BattleActionProcessor::doEmptyAction(const BattleAction & ba) bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba) { + if (gameHandler->gameState()->curB->tacticDistance == 0) + { + gameHandler->complain("Cannot end tactics mode - no tactics!"); + return false; + } return true; } bool BattleActionProcessor::doWaitAction(const BattleAction & ba) { - return true; -} + const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + + if (!canStackAct(stack)) + return false; -bool BattleActionProcessor::doBadMoraleAction(const BattleAction & ba) -{ return true; } @@ -113,10 +118,10 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) return false; } - const CSpell * s = SpellID(ba.actionSubtype).toSpell(); + const CSpell * s = ba.spell.toSpell(); if (!s) { - logGlobal->error("Wrong spell id (%d)!", ba.actionSubtype); + logGlobal->error("Wrong spell id (%d)!", ba.spell.getNum()); return false; } @@ -411,7 +416,7 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) { const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(gameHandler->gameState()->curB); - SpellID spellID = SpellID(ba.actionSubtype); + SpellID spellID = ba.spell; if (!canStackAct(stack)) return false; @@ -479,8 +484,6 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba) bool BattleActionProcessor::canStackAct(const CStack * stack) { - const bool isAboutActiveStack = stack->unitId() == gameHandler->gameState()->curB->getActiveStackID(); - if (!stack) { gameHandler->complain("No such stack!"); @@ -500,10 +503,13 @@ bool BattleActionProcessor::canStackAct(const CStack * stack) return false; } } - else if (!isAboutActiveStack) + else { - gameHandler->complain("Action has to be about active stack!"); - return false; + if (stack->unitId() != gameHandler->gameState()->curB->getActiveStackID()) + { + gameHandler->complain("Action has to be about active stack!"); + return false; + } } return true; } @@ -536,8 +542,6 @@ bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) return doCatapultAction(ba); case EActionType::MONSTER_SPELL: return doUnitSpellAction(ba); - case EActionType::BAD_MORALE: - return doBadMoraleAction(ba); case EActionType::STACK_HEAL: return doHealAction(ba); } @@ -545,7 +549,7 @@ bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) return false; } -bool BattleActionProcessor::makeBattleAction(const BattleAction &ba) +bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) { logGlobal->trace("Making action: %s", ba.toString()); const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); @@ -1394,3 +1398,53 @@ void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CS blm.lines.push_back(std::move(line)); } } + +bool BattleActionProcessor::makeAutomaticBattleAction(const BattleAction & ba) +{ + return makeBattleActionImpl(ba); +} + +bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) +{ + const BattleInfo * battle = gameHandler->gameState()->curB; + + if(!battle && gameHandler->complain("Can not make action - there is no battle ongoing!")) + return false; + + if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) + return false; + + if(battle->tacticDistance != 0) + { + if(!ba.isTacticsAction()) + { + gameHandler->complain("Can not make actions while in tactics mode!"); + return false; + } + + if(player != battle->sides[ba.side].color) + { + gameHandler->complain("Can not make actions in battles you are not part of!"); + return false; + } + } + else + { + if (ba.isUnitAction() && ba.stackNumber != battle->getActiveStackID()) + { + gameHandler->complain("Can not make actions - stack is not active!"); + return false; + } + + auto active = battle->battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + auto unitOwner = battle->battleGetOwner(active); + + if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) + return false; + } + + return makeBattleActionImpl(ba); +} diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 7dc7a15c3..10a08165b 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -16,6 +16,7 @@ struct BattleAttack; class BattleAction; struct BattleHex; class CStack; +class PlayerColor; enum class BonusType; namespace battle @@ -64,14 +65,15 @@ class BattleActionProcessor : boost::noncopyable bool doShootAction(const BattleAction & ba); bool doCatapultAction(const BattleAction & ba); bool doUnitSpellAction(const BattleAction & ba); - bool doBadMoraleAction(const BattleAction & ba); bool doHealAction(const BattleAction & ba); bool dispatchBattleAction(const BattleAction & ba); + bool makeBattleActionImpl(const BattleAction & ba); public: explicit BattleActionProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool makeBattleAction(const BattleAction & ba); + bool makeAutomaticBattleAction(const BattleAction & ba); + bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 399bd9012..53bcac325 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -126,12 +126,8 @@ void BattleFlowProcessor::summonGuardiansHelper(std::vector & output, } } -void BattleFlowProcessor::onBattleStarted() +void BattleFlowProcessor::tryPlaceMoats() { - gameHandler->setBattle(gameHandler->gameState()->curB); - assert(gameHandler->gameState()->curB); - //TODO: pre-tactic stuff, call scripts etc. - //Moat should be initialized here, because only here we can use spellcasting if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) { @@ -142,6 +138,14 @@ void BattleFlowProcessor::onBattleStarted() auto target = spells::Target(); cast.cast(gameHandler->spellEnv, target); } +} + +void BattleFlowProcessor::onBattleStarted() +{ + gameHandler->setBattle(gameHandler->gameState()->curB); + assert(gameHandler->gameState()->curB); + + tryPlaceMoats(); if (gameHandler->gameState()->curB->tacticDistance == 0) onTacticsEnded(); @@ -330,11 +334,7 @@ void BattleFlowProcessor::activateNextStack() if (!tryMakeAutomaticAction(next)) { - logGlobal->trace("Activating %s", next->nodeName()); - auto nextId = next->unitId(); - BattleSetActiveStack sas; - sas.stack = nextId; - gameHandler->sendAndApply(&sas); + setActiveStack(next); break; } } @@ -529,36 +529,25 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) if(owner->checkBattleStateChanges()) return; - bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT; - bool tacticsAction = ba.actionType == EActionType::END_TACTIC_PHASE; - - if (activeStack == nullptr && !tacticsAction) - { - throw std::runtime_error("Unexpected action - no active stack!"); - } - - if (heroAction || tacticsAction) - { - if (!tacticsAction && activeStack->alive()) - { - // this is action made by hero AND unit is alive (e.g. not killed by casted spell) - // keep current active stack for next action - BattleSetActiveStack sas; - sas.stack = activeStack->unitId(); - gameHandler->sendAndApply(&sas); - return; - } - } - else + if (ba.isUnitAction()) { + assert(activeStack != nullptr); assert(actedStack != nullptr); if (rollGoodMorale(actedStack)) { // Good morale - same stack makes 2nd turn - BattleSetActiveStack sas; - sas.stack = actedStack->unitId(); - gameHandler->sendAndApply(&sas); + setActiveStack(actedStack); + return; + } + } + else + { + if (activeStack && activeStack->alive()) + { + // this is action made by hero AND unit is alive (e.g. not killed by casted spell) + // keep current active stack for next action + setActiveStack(actedStack); return; } } @@ -752,3 +741,13 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) } } } + +void BattleFlowProcessor::setActiveStack(const CStack * stack) +{ + assert(stack); + + logGlobal->trace("Activating %s", stack->nodeName()); + BattleSetActiveStack sas; + sas.stack = stack->unitId(); + gameHandler->sendAndApply(&sas); +} diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index 9c9b8d38c..20d7a9a23 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -32,6 +32,7 @@ class BattleFlowProcessor : boost::noncopyable void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); void trySummonGuardians(const CStack * stack); + void tryPlaceMoats(); void castOpeningSpells(); void activateNextStack(); void startNextRound(bool isFirstRound); @@ -39,6 +40,7 @@ class BattleFlowProcessor : boost::noncopyable void stackEnchantedTrigger(const CStack * stack); void removeObstacle(const CObstacleInstance & obstacle); void stackTurnTrigger(const CStack * stack); + void setActiveStack(const CStack * stack); void makeStackDoNothing(const CStack * next); bool makeAutomaticAction(const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 9378b8037..8fc28000d 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -151,7 +151,6 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co if (heroes[0] && heroes[0]->boat && heroes[1] && heroes[1]->boat) terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship")); - //send info about battles BattleStart bs; bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); @@ -236,52 +235,9 @@ void BattleProcessor::updateGateState() gameHandler->sendAndApply(&db); } -bool BattleProcessor::makePlayerBattleAction(PlayerColor player, BattleAction &ba) +bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) { - const BattleInfo * b = gameHandler->gameState()->curB; - - if(!b && gameHandler->complain("Can not make action - there is no battle ongoing!")) - return false; - - if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) - return false; - - if(b->tacticDistance) - { - if(ba.actionType != EActionType::WALK && ba.actionType != EActionType::END_TACTIC_PHASE - && ba.actionType != EActionType::RETREAT && ba.actionType != EActionType::SURRENDER) - { - gameHandler->complain("Can not make actions while in tactics mode!"); - return false; - } - - if(player != b->sides[ba.side].color) - { - gameHandler->complain("Can not make actions in battles you are not part of!"); - return false; - } - } - else - { - bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType == EActionType::END_TACTIC_PHASE; - - if (ba.stackNumber != b->getActiveStackID() && heroAction == false) - { - gameHandler->complain("Can not make actions - stack is not active!"); - return false; - } - - auto active = b->battleActiveUnit(); - if(!active && gameHandler->complain("No active unit in battle!")) - return false; - - auto unitOwner = b->battleGetOwner(active); - - if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) - return false; - } - - bool result = actionsProcessor->makeBattleAction(ba); + bool result = actionsProcessor->makePlayerBattleAction(player, ba); flowProcessor->onActionMade(ba); return result; } @@ -294,7 +250,7 @@ void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSid bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba) { - return actionsProcessor->makeBattleAction(ba); + return actionsProcessor->makeAutomaticBattleAction(ba); } void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index c30e2e6da..07df198dd 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -64,7 +64,7 @@ public: void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); /// Processing of incoming battle action netpack - bool makePlayerBattleAction(PlayerColor player, BattleAction & ba); + bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); /// Applies results of a battle once player agrees to them void endBattleConfirm(const BattleInfo * battleInfo); From 67eaef3520000539e9feea0b47074bb1a1877948 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 20:20:12 +0300 Subject: [PATCH 0112/1248] Fixed few more discovered regressions --- lib/NetPacksLib.cpp | 53 +++++++++++--------- server/battles/BattleActionProcessor.cpp | 5 -- server/battles/BattleFlowProcessor.cpp | 2 +- server/processors/PlayerMessageProcessor.cpp | 22 ++++++-- server/processors/PlayerMessageProcessor.h | 2 +- 5 files changed, 47 insertions(+), 37 deletions(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 402be6fa1..eb6aa28e4 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2284,32 +2284,35 @@ void StartAction::applyGs(CGameState *gs) return; } - [[maybe_unused]] bool heroAction = ba.actionType == EActionType::HERO_SPELL || ba.actionType ==EActionType::SURRENDER || ba.actionType ==EActionType::RETREAT || ba.actionType == EActionType::END_TACTIC_PHASE; - - assert(st || heroAction); // stack must exists for all non-hero actions - - if(ba.actionType == EActionType::HERO_SPELL) - gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell); - - switch(ba.actionType) + if (ba.isUnitAction()) { - case EActionType::DEFEND: - st->waiting = false; - st->defending = true; - st->defendingAnim = true; - break; - case EActionType::WAIT: - st->defendingAnim = false; - st->waiting = true; - st->waitedThisTurn = true; - break; - case EActionType::HERO_SPELL: //no change in current stack state - break; - default: //any active stack action - attack, catapult, heal, spell... - st->waiting = false; - st->defendingAnim = false; - st->movedThisRound = true; - break; + assert(st); // stack must exists for all non-hero actions + + switch(ba.actionType) + { + case EActionType::DEFEND: + st->waiting = false; + st->defending = true; + st->defendingAnim = true; + break; + case EActionType::WAIT: + st->defendingAnim = false; + st->waiting = true; + st->waitedThisTurn = true; + break; + case EActionType::HERO_SPELL: //no change in current stack state + break; + default: //any active stack action - attack, catapult, heal, spell... + st->waiting = false; + st->defendingAnim = false; + st->movedThisRound = true; + break; + } + } + else + { + if(ba.actionType == EActionType::HERO_SPELL) + gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell); } } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index e9903ddf3..bddabd8b7 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -58,11 +58,6 @@ bool BattleActionProcessor::doEmptyAction(const BattleAction & ba) bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba) { - if (gameHandler->gameState()->curB->tacticDistance == 0) - { - gameHandler->complain("Cannot end tactics mode - no tactics!"); - return false; - } return true; } diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 53bcac325..3cbe14402 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -547,7 +547,7 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) { // this is action made by hero AND unit is alive (e.g. not killed by casted spell) // keep current active stack for next action - setActiveStack(actedStack); + setActiveStack(activeStack); return; } } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index d077233c8..75e4217fc 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -213,15 +213,27 @@ void PlayerMessageProcessor::cheatGiveMachines(PlayerColor player, const CGHeroI gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::FIRST_AID_TENT], ArtifactPosition::MACH3); } -void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero) +void PlayerMessageProcessor::cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words) { if (!hero) return; - for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods + if (!words.empty()) { - if(VLC->arth->objects[g]->canBePutAt(hero)) - gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + for (auto const & word : words) + { + auto artID = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", word, false); + if(artID && VLC->arth->objects[*artID]) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[*artID], ArtifactPosition::FIRST_AVAILABLE); + } + } + else + { + for(int g = 7; g < VLC->arth->objects.size(); ++g) //including artifacts from mods + { + if(VLC->arth->objects[g]->canBePutAt(hero)) + gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[g], ArtifactPosition::FIRST_AVAILABLE); + } } } @@ -434,7 +446,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla const auto & doCheatGiveArmyCustom = [&]() { cheatGiveArmy(player, hero, words); }; const auto & doCheatGiveArmyFixed = [&](std::vector customWords) { cheatGiveArmy(player, hero, customWords); }; const auto & doCheatGiveMachines = [&]() { cheatGiveMachines(player, hero); }; - const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero); }; + const auto & doCheatGiveArtifacts = [&]() { cheatGiveArtifacts(player, hero, words); }; const auto & doCheatLevelup = [&]() { cheatLevelup(player, hero, words); }; const auto & doCheatExperience = [&]() { cheatExperience(player, hero, words); }; const auto & doCheatMovement = [&]() { cheatMovement(player, hero, words); }; diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index 4c0f725ab..d8f9e9878 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -31,7 +31,7 @@ class PlayerMessageProcessor void cheatBuildTown(PlayerColor player, const CGTownInstance * town); void cheatGiveArmy(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatGiveMachines(PlayerColor player, const CGHeroInstance * hero); - void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero); + void cheatGiveArtifacts(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatLevelup(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatExperience(PlayerColor player, const CGHeroInstance * hero, std::vector words); void cheatMovement(PlayerColor player, const CGHeroInstance * hero, std::vector words); From 2be9664d26604d623f2930ae5e77ce955cdf2186 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 21:28:17 +0300 Subject: [PATCH 0113/1248] Remove no longer used code --- AI/BattleAI/BattleAI.cpp | 10 ---------- server/battles/BattleProcessor.cpp | 5 ----- 2 files changed, 15 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 1bee0e9c7..94df8714d 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -289,16 +289,6 @@ void CBattleAI::activeStack( const CStack * stack ) logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); - if(cb->battleIsFinished() || !stack->alive()) - { - //spellcast may finish battle or kill active stack - //send special preudo-action - BattleAction cancel; - cancel.actionType = EActionType::NO_ACTION; - cb->battleMakeUnitAction(cancel); - return; - } - if(auto action = considerFleeingOrSurrendering()) { cb->battleMakeUnitAction(*action); diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 8fc28000d..0c04bff7c 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -42,11 +42,6 @@ #include "../../lib/spells/ObstacleCasterProxy.h" #include "../../lib/spells/Problem.h" -#define COMPLAIN_RET_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return;}} while(0) -#define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){gameHandler->complain(txt); return false;}} while(0) -#define COMPLAIN_RET(txt) {gameHandler->complain(txt); return false;} -#define COMPLAIN_RETF(txt, FORMAT) {gameHandler->complain(boost::str(boost::format(txt) % FORMAT)); return false;} - BattleProcessor::BattleProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) , flowProcessor(std::make_unique(this)) From 276c00b2841a21207e3c62b046f95652f9c9df66 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 17 Aug 2023 22:15:44 +0300 Subject: [PATCH 0114/1248] Removed excessive includes --- server/battles/BattleActionProcessor.cpp | 18 ++---------------- server/battles/BattleFlowProcessor.cpp | 18 +----------------- server/battles/BattleProcessor.cpp | 18 ------------------ server/battles/BattleResultProcessor.cpp | 14 -------------- 4 files changed, 3 insertions(+), 65 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index bddabd8b7..e32d082f3 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -13,31 +13,17 @@ #include "BattleProcessor.h" #include "../CGameHandler.h" -#include "../CVCMIServer.h" -#include "../processors/HeroPoolProcessor.h" -#include "../queries/QueriesProcessor.h" -#include "../queries/BattleQueries.h" -#include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" -#include "../../lib/CondSh.h" #include "../../lib/GameSettings.h" -#include "../../lib/ScopeGuard.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" #include "../../lib/battle/BattleInfo.h" -#include "../../lib/battle/CUnitState.h" +#include "../../lib/battle/BattleAction.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/modding/IdentifierStorage.h" -#include "../../lib/serializer/Cast.h" +#include "../../lib/NetPacks.h" #include "../../lib/spells/AbilityCaster.h" -#include "../../lib/spells/BonusCaster.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/ObstacleCasterProxy.h" #include "../../lib/spells/Problem.h" BattleActionProcessor::BattleActionProcessor(BattleProcessor * owner) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 3cbe14402..b671a1f13 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -13,32 +13,16 @@ #include "BattleProcessor.h" #include "../CGameHandler.h" -#include "../CVCMIServer.h" -#include "../processors/HeroPoolProcessor.h" -#include "../queries/QueriesProcessor.h" -#include "../queries/BattleQueries.h" -#include "../../lib/ArtifactUtils.h" -#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" -#include "../../lib/CondSh.h" #include "../../lib/GameSettings.h" -#include "../../lib/ScopeGuard.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" #include "../../lib/battle/BattleInfo.h" -#include "../../lib/battle/CUnitState.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/modding/IdentifierStorage.h" -#include "../../lib/serializer/Cast.h" -#include "../../lib/spells/AbilityCaster.h" +#include "../../lib/NetPacks.h" #include "../../lib/spells/BonusCaster.h" -#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ObstacleCasterProxy.h" -#include "../../lib/spells/Problem.h" BattleFlowProcessor::BattleFlowProcessor(BattleProcessor * owner) : owner(owner) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 0c04bff7c..62a0be3a1 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -15,32 +15,14 @@ #include "BattleResultProcessor.h" #include "../CGameHandler.h" -#include "../CVCMIServer.h" -#include "../processors/HeroPoolProcessor.h" #include "../queries/QueriesProcessor.h" #include "../queries/BattleQueries.h" -#include "../../lib/ArtifactUtils.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CStack.h" -#include "../../lib/CondSh.h" -#include "../../lib/GameSettings.h" -#include "../../lib/ScopeGuard.h" #include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" #include "../../lib/battle/BattleInfo.h" -#include "../../lib/battle/CUnitState.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapping/CMap.h" #include "../../lib/modding/IdentifierStorage.h" -#include "../../lib/serializer/Cast.h" -#include "../../lib/spells/AbilityCaster.h" -#include "../../lib/spells/BonusCaster.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/ObstacleCasterProxy.h" -#include "../../lib/spells/Problem.h" BattleProcessor::BattleProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6f1fe3451..912de4e65 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -11,32 +11,18 @@ #include "BattleResultProcessor.h" #include "../CGameHandler.h" -#include "../CVCMIServer.h" #include "../processors/HeroPoolProcessor.h" #include "../queries/QueriesProcessor.h" #include "../queries/BattleQueries.h" #include "../../lib/ArtifactUtils.h" -#include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" -#include "../../lib/CondSh.h" #include "../../lib/GameSettings.h" -#include "../../lib/ScopeGuard.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" #include "../../lib/battle/BattleInfo.h" -#include "../../lib/battle/CUnitState.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/modding/IdentifierStorage.h" #include "../../lib/serializer/Cast.h" -#include "../../lib/spells/AbilityCaster.h" -#include "../../lib/spells/BonusCaster.h" #include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/ObstacleCasterProxy.h" -#include "../../lib/spells/Problem.h" BattleResultProcessor::BattleResultProcessor(BattleProcessor * owner) // : owner(owner) From 8154e848253a194c97aff8331d44491de8359fc1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 13:42:27 +0300 Subject: [PATCH 0115/1248] Fix tactics usage --- server/battles/BattleFlowProcessor.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index b671a1f13..acbd881d0 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -513,6 +513,10 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) if(owner->checkBattleStateChanges()) return; + // tactics - next stack will be selected by player + if(battle->tacticDistance != 0) + return; + if (ba.isUnitAction()) { assert(activeStack != nullptr); From 1c552ba9d9629c545564bee1ab80f91243ae3a18 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 17:38:35 +0300 Subject: [PATCH 0116/1248] Remove unused code --- client/battle/BattleStacksController.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 321adc2bd..0a2145057 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -399,15 +399,7 @@ void BattleStacksController::addNewAnim(BattleAnimation *anim) void BattleStacksController::stackRemoved(uint32_t stackID) { if (getActiveStack() && getActiveStack()->unitId() == stackID) - { - BattleAction action; - action.side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - action.actionType = EActionType::NO_ACTION; - action.stackNumber = getActiveStack()->unitId(); - - LOCPLINT->cb->battleMakeUnitAction(action); setActiveStack(nullptr); - } } void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) From 5f7f0ec2ea57cf093c2179ce6c68b90ca028508f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 17:38:54 +0300 Subject: [PATCH 0117/1248] Declare enum classes as int8_t --- lib/GameConstants.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 052ddeae4..23214e1a1 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -998,7 +998,7 @@ namespace Date }; } -enum class EActionType : int32_t +enum class EActionType : int8_t { NO_ACTION, @@ -1380,7 +1380,7 @@ enum class EHealPower : ui8 PERMANENT }; -enum class EBattleResult : ui8 +enum class EBattleResult : int8_t { NORMAL = 0, ESCAPE = 1, From 584dd2094364a47703d5eedb64c191a552b205f1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 21 Aug 2023 05:06:58 +0400 Subject: [PATCH 0118/1248] Progress on server side for rmg --- client/LobbyClientNetPackVisitors.h | 2 +- client/NetPacksLobbyClient.cpp | 16 +++++++++------- client/mainmenu/CMainMenu.cpp | 5 ++--- client/mainmenu/CMainMenu.h | 9 ++++----- lib/NetPackVisitor.h | 1 + lib/NetPacksLib.cpp | 5 +++++ lib/NetPacksLobby.h | 12 ++++++++++++ lib/gameState/CGameState.cpp | 8 +++++--- lib/gameState/CGameState.h | 5 +++-- lib/registerTypes/RegisterTypes.h | 1 + server/CGameHandler.cpp | 4 ++-- server/CGameHandler.h | 3 ++- server/CVCMIServer.cpp | 26 ++++++++++++++++++++++++-- server/NetPacksLobbyServer.cpp | 4 ++++ 14 files changed, 75 insertions(+), 26 deletions(-) diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 7ea105b02..e8a6c7455 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -52,7 +52,7 @@ public: virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; - virtual void visitLobbyStartGame(LobbyStartGame & pack) override; + virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override; }; diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 8329e5e31..633033ad5 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -59,6 +59,9 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClient void ApplyOnLobbyScreenNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack) { + if(auto w = GH.windows().topWindow()) + GH.windows().popWindow(w); + if(GH.windows().count() > 0) GH.windows().popWindows(1); } @@ -122,16 +125,15 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pac handler.si = pack.initializedStartInfo; handler.si->mode = modeBackup; } - if(settings["session"]["headless"].Bool()) - handler.startGameplay(pack.initializedGameState); + handler.startGameplay(pack.initializedGameState); } -void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) +void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress & pack) { - if(pack.clientId != -1 && pack.clientId != handler.c->connectionID) - return; - - GH.windows().createAndPushWindow(std::bind(&CServerHandler::startGameplay, &handler, pack.initializedGameState)); + if(auto w = GH.windows().topWindow()) + w->set(pack.progress); + else + GH.windows().createAndPushWindow(); } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index aabff9076..af7978a4b 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -582,15 +582,14 @@ void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) }); } -CLoadingScreen::CLoadingScreen(std::function loader) - : CWindowObject(BORDERED, getBackground()), loadingThread(loader) +CLoadingScreen::CLoadingScreen() + : CWindowObject(BORDERED, getBackground()) { CCS->musich->stopMusic(5000); } CLoadingScreen::~CLoadingScreen() { - loadingThread.join(); } void CLoadingScreen::showAll(Canvas & to) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index ffc17b67f..65086f379 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -11,6 +11,7 @@ #include "../windows/CWindowObject.h" #include "../../lib/JsonNode.h" +#include "../../lib/LoadProgress.h" VCMI_LIB_NAMESPACE_BEGIN @@ -178,14 +179,12 @@ public: CSimpleJoinScreen(bool host = true); }; -class CLoadingScreen : public CWindowObject +class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progress { - boost::thread loadingThread; - std::string getBackground(); -public: - CLoadingScreen(std::function loader); +public: + CLoadingScreen(); ~CLoadingScreen(); void showAll(Canvas & to) override; diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index da1c6e2e6..0d418421a 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -145,6 +145,7 @@ public: virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) {} virtual void visitLobbyChatMessage(LobbyChatMessage & pack) {} virtual void visitLobbyGuiAction(LobbyGuiAction & pack) {} + virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) {} virtual void visitLobbyEndGame(LobbyEndGame & pack) {} virtual void visitLobbyStartGame(LobbyStartGame & pack) {} virtual void visitLobbyChangeHost(LobbyChangeHost & pack) {} diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5dc6e5d56..f5877fb82 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -693,6 +693,11 @@ void LobbyGuiAction::visitTyped(ICPackVisitor & visitor) visitor.visitLobbyGuiAction(*this); } +void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyLoadProgress(*this); +} + void LobbyEndGame::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbyEndGame(*this); diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index 09f459326..1b8767217 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -99,6 +99,18 @@ struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate } }; +struct DLL_LINKAGE LobbyLoadProgress : public CLobbyPackToPropagate +{ + unsigned char progress; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & progress; + } +}; + struct DLL_LINKAGE LobbyEndGame : public CLobbyPackToPropagate { bool closeConnection = false, restart = false; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 1fd3a696c..e338edd37 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -404,7 +404,7 @@ void CGameState::preInit(Services * services) this->services = services; } -void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap) +void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap, Load::Progress * progressTracking) { preInitAuto(); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); @@ -416,7 +416,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, bool allow switch(scenarioOps->mode) { case StartInfo::NEW_GAME: - initNewGame(mapService, allowSavingRandomMap); + initNewGame(mapService, allowSavingRandomMap, progressTracking); break; case StartInfo::CAMPAIGN: initCampaign(); @@ -535,7 +535,7 @@ void CGameState::preInitAuto() } } -void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap) +void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress * progressTracking) { if(scenarioOps->createRandomMap()) { @@ -544,8 +544,10 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan // Gen map CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, scenarioOps->seedToBeUsed); + progressTracking = &mapGenerator; std::unique_ptr randomMap = mapGenerator.generate(); + progressTracking = nullptr; if(allowSavingRandomMap) { diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 0281aa18e..4e6b16e25 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -11,6 +11,7 @@ #include "bonuses/CBonusSystemNode.h" #include "IGameCallback.h" +#include "LoadProgress.h" namespace boost { @@ -89,7 +90,7 @@ public: void preInit(Services * services); - void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false); + void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false, Load::Progress * progressTracking = nullptr); void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) @@ -166,7 +167,7 @@ public: private: // ----- initialization ----- void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap); + void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress * progressTracking = nullptr); void checkMapChecksum(); void initGlobalBonuses(); void initGrailPosition(); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 08db1d259..77be1f3ea 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -379,6 +379,7 @@ void registerTypesLobbyPacks(Serializer &s) s.template registerType(); // Only host client send s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 68a907cd9..afe4803e6 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1599,7 +1599,7 @@ void CGameHandler::reinitScripting() #endif } -void CGameHandler::init(StartInfo *si) +void CGameHandler::init(StartInfo *si, Load::Progress * progressTracking) { if (si->seedToBeUsed == 0) { @@ -1609,7 +1609,7 @@ void CGameHandler::init(StartInfo *si) gs = new CGameState(); gs->preInit(VLC); logGlobal->info("Gamestate created!"); - gs->init(&mapService, si); + gs->init(&mapService, si, false, progressTracking); logGlobal->info("Gamestate initialized!"); // reset seed, so that clients can't predict any following random values diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 49a6dfcd6..ad89866b0 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -15,6 +15,7 @@ #include "../lib/IGameCallback.h" #include "../lib/battle/CBattleInfoCallback.h" #include "../lib/battle/BattleAction.h" +#include "../lib/LoadProgress.h" #include "../lib/ScriptHandler.h" #include "CQuery.h" @@ -236,7 +237,7 @@ public: void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero ////////////////////////////////////////////////////////////////////////// - void init(StartInfo *si); + void init(StartInfo *si, Load::Progress * progressTracking = nullptr); void handleClientDisconnection(std::shared_ptr c); void handleReceivedPack(CPackForServer * pack); PlayerColor getPlayerAt(std::shared_ptr c) const; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index a7a75f039..7125fabc7 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -279,6 +279,24 @@ void CVCMIServer::prepareToRestart() bool CVCMIServer::prepareToStartGame() { + Load::Progress * progressTracking = nullptr; + bool progressTrackingFinished = false; + std::thread progressTrackingThread([this, &progressTracking, &progressTrackingFinished](){ + while(!progressTrackingFinished) + { + if(progressTracking) + { + boost::unique_lock queueLock(mx); + std::unique_ptr loadProgress(new LobbyLoadProgress); + loadProgress->progress = progressTracking->get(); + addToAnnounceQueue(std::move(loadProgress)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + progressTrackingFinished = false; + }); + progressTrackingThread.detach(); + gh = std::make_shared(this); switch(si->mode) { @@ -286,12 +304,12 @@ bool CVCMIServer::prepareToStartGame() logNetwork->info("Preparing to start new campaign"); si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); - gh->init(si.get()); + gh->init(si.get(), progressTracking); break; case StartInfo::NEW_GAME: logNetwork->info("Preparing to start new game"); - gh->init(si.get()); + gh->init(si.get(), progressTracking); break; case StartInfo::LOAD_GAME: @@ -305,6 +323,10 @@ bool CVCMIServer::prepareToStartGame() break; } + progressTrackingFinished = true; + while(progressTrackingFinished) + continue; + state = EServerState::GAMEPLAY_STARTING; return true; } diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index eaca04f8c..70e485d93 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -289,6 +289,10 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) result = false; return; } + // Announce loading + std::unique_ptr loadProgress(new LobbyLoadProgress); + srv.addToAnnounceQueue(std::move(loadProgress)); + // Server will prepare gamestate and we announce StartInfo to clients if(!srv.prepareToStartGame()) { From 45f13c7964c194f36f449ae45ddbc93d631e629b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 21 Aug 2023 18:56:01 +0400 Subject: [PATCH 0119/1248] Progress update on client side --- client/Client.cpp | 5 ++++- client/NetPacksLobbyClient.cpp | 3 +++ client/mainmenu/CMainMenu.cpp | 29 +++++++++++++++++++++++++++-- client/mainmenu/CMainMenu.h | 4 ++++ config/mainmenu.json | 6 +++++- lib/gameState/CGameState.cpp | 4 ++-- lib/gameState/CGameState.h | 4 ++-- server/CGameHandler.cpp | 4 ++-- server/CGameHandler.h | 2 +- server/CVCMIServer.cpp | 2 +- 10 files changed, 51 insertions(+), 12 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index eec33cb18..ac20e1a1b 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -181,7 +181,10 @@ void CClient::newGame(CGameState * initializedGameState) gs->preInit(VLC); logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); if(!initializedGameState) - gs->init(&mapService, CSH->si.get(), settings["general"]["saveRandomMaps"].Bool()); + { + Load::Progress * progressTrackingPointer = nullptr; + gs->init(&mapService, CSH->si.get(), progressTrackingPointer, settings["general"]["saveRandomMaps"].Bool()); + } logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); initMapHandler(); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 633033ad5..b99b2fbdb 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -131,7 +131,10 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pac void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress & pack) { if(auto w = GH.windows().topWindow()) + { w->set(pack.progress); + w->redraw(); + } else GH.windows().createAndPushWindow(); } diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index af7978a4b..34e9b2ba5 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -585,7 +585,22 @@ void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) CLoadingScreen::CLoadingScreen() : CWindowObject(BORDERED, getBackground()) { + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + CCS->musich->stopMusic(5000); + + const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; + + const int posx = conf["x"].Integer(), posy = conf["y"].Integer(); + const int blockSize = conf["size"].Integer(); + const int blocksAmount = conf["amount"].Integer(); + + for(int i = 0; i < blocksAmount; ++i) + { + progressBlocks.push_back(std::make_shared(conf["name"].String(), i, 0, posx + i * blockSize, posy)); + progressBlocks.back()->deactivate(); + progressBlocks.back()->visible = false; + } } CLoadingScreen::~CLoadingScreen() @@ -597,13 +612,23 @@ void CLoadingScreen::showAll(Canvas & to) //FIXME: filling screen with transparency? BLACK intended? //Rect rect(0, 0, to->w, to->h); //CSDL_Ext::fillRect(to, rect, Colors::TRANSPARENCY); - + if(!progressBlocks.empty()) + { + int status = float(get()) / (2.55f * progressBlocks.size()); + + for(int i = 0; i < status; ++i) + { + progressBlocks.at(i)->activate(); + progressBlocks.at(i)->visible = true; + } + } + CWindowObject::showAll(to); } std::string CLoadingScreen::getBackground() { - const auto & conf = CMainMenuConfig::get().getConfig()["loading"].Vector(); + const auto & conf = CMainMenuConfig::get().getConfig()["loading"]["background"].Vector(); if(conf.empty()) { diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 65086f379..544873a6a 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -9,6 +9,7 @@ */ #pragma once +#include "../widgets/Images.h" #include "../windows/CWindowObject.h" #include "../../lib/JsonNode.h" #include "../../lib/LoadProgress.h" @@ -26,6 +27,7 @@ class CTabbedInt; class CAnimation; class CButton; class CFilledTexture; +class CLabel; // TODO: Find new location for these enums @@ -181,6 +183,8 @@ public: class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progress { + std::vector> progressBlocks; + std::string getBackground(); public: diff --git a/config/mainmenu.json b/config/mainmenu.json index 9cd4a6923..e9e357ecf 100644 --- a/config/mainmenu.json +++ b/config/mainmenu.json @@ -2,7 +2,11 @@ //images used in game selection screen "game-select" : ["gamselb0", "gamselb1"], - "loading" : ["loadbar"], + "loading" : + { + "background" : ["loadbar"], + "x": 395, "y": 548, "size": 18, "amount": 20, "name": "loadprog" + }, //Main menu window, consists of several sub-menus aka items "window": diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index e338edd37..3cea0fa07 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -404,7 +404,7 @@ void CGameState::preInit(Services * services) this->services = services; } -void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap, Load::Progress * progressTracking) +void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Progress *& progressTracking, bool allowSavingRandomMap) { preInitAuto(); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); @@ -535,7 +535,7 @@ void CGameState::preInitAuto() } } -void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress * progressTracking) +void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress *& progressTracking) { if(scenarioOps->createRandomMap()) { diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 4e6b16e25..546e774ac 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -90,7 +90,7 @@ public: void preInit(Services * services); - void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false, Load::Progress * progressTracking = nullptr); + void init(const IMapService * mapService, StartInfo * si, Load::Progress *&, bool allowSavingRandomMap = false); void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) @@ -167,7 +167,7 @@ public: private: // ----- initialization ----- void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress * progressTracking = nullptr); + void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress *& progressTracking); void checkMapChecksum(); void initGlobalBonuses(); void initGrailPosition(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index afe4803e6..ebc5f9552 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1599,7 +1599,7 @@ void CGameHandler::reinitScripting() #endif } -void CGameHandler::init(StartInfo *si, Load::Progress * progressTracking) +void CGameHandler::init(StartInfo *si, Load::Progress *& progressTracking) { if (si->seedToBeUsed == 0) { @@ -1609,7 +1609,7 @@ void CGameHandler::init(StartInfo *si, Load::Progress * progressTracking) gs = new CGameState(); gs->preInit(VLC); logGlobal->info("Gamestate created!"); - gs->init(&mapService, si, false, progressTracking); + gs->init(&mapService, si, progressTracking); logGlobal->info("Gamestate initialized!"); // reset seed, so that clients can't predict any following random values diff --git a/server/CGameHandler.h b/server/CGameHandler.h index ad89866b0..eaac514e3 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -237,7 +237,7 @@ public: void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero ////////////////////////////////////////////////////////////////////////// - void init(StartInfo *si, Load::Progress * progressTracking = nullptr); + void init(StartInfo *si, Load::Progress *& progressTracking); void handleClientDisconnection(std::shared_ptr c); void handleReceivedPack(CPackForServer * pack); PlayerColor getPlayerAt(std::shared_ptr c) const; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 7125fabc7..41e5b065e 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -291,7 +291,7 @@ bool CVCMIServer::prepareToStartGame() loadProgress->progress = progressTracking->get(); addToAnnounceQueue(std::move(loadProgress)); } - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); } progressTrackingFinished = false; }); From a60d503078c94f225dca689d5a233116d1c567db Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 21 Aug 2023 19:09:56 +0400 Subject: [PATCH 0120/1248] Fix progress bar --- client/LobbyClientNetPackVisitors.h | 1 + client/NetPacksLobbyClient.cpp | 11 +++++++++++ client/mainmenu/CMainMenu.cpp | 2 +- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index e8a6c7455..276250455 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -52,6 +52,7 @@ public: virtual void visitLobbyClientDisconnected(LobbyClientDisconnected & pack) override; virtual void visitLobbyChatMessage(LobbyChatMessage & pack) override; virtual void visitLobbyGuiAction(LobbyGuiAction & pack) override; + virtual void visitLobbyStartGame(LobbyStartGame & pack) override; virtual void visitLobbyLoadProgress(LobbyLoadProgress & pack) override; virtual void visitLobbyUpdateState(LobbyUpdateState & pack) override; virtual void visitLobbyShowMessage(LobbyShowMessage & pack) override; diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index b99b2fbdb..6dd2ef315 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -128,6 +128,17 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pac handler.startGameplay(pack.initializedGameState); } +void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) +{ + if(auto w = GH.windows().topWindow()) + { + w->finish(); + w->redraw(); + } + else + GH.windows().createAndPushWindow(); +} + void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress & pack) { if(auto w = GH.windows().topWindow()) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 34e9b2ba5..c1a4719f2 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -614,7 +614,7 @@ void CLoadingScreen::showAll(Canvas & to) //CSDL_Ext::fillRect(to, rect, Colors::TRANSPARENCY); if(!progressBlocks.empty()) { - int status = float(get()) / (2.55f * progressBlocks.size()); + int status = float(get()) / 255.f * progressBlocks.size(); for(int i = 0; i < status; ++i) { From 0dab3b83a01f8cb83e4c9f5ed43193f31f6d5e34 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 21 Aug 2023 20:45:00 +0200 Subject: [PATCH 0121/1248] date alignment --- client/lobby/CSelectionBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index de543817e..d38212f6a 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -121,7 +121,7 @@ InfoCard::InfoCard() pos.x += 393; pos.y += 6; - labelSaveDate = std::make_shared(158, 19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + labelSaveDate = std::make_shared(310, 38, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE); mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); From b1bfc2239ffc3b4214a8bbfdb5248efd98a708cf Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 00:32:26 +0400 Subject: [PATCH 0122/1248] Backward compatibility with mods --- client/CServerHandler.cpp | 2 +- client/mainmenu/CMainMenu.cpp | 42 ++++++++++++++++++++------------ mapeditor/icons/lock-closed.png | Bin 0 -> 649 bytes mapeditor/icons/lock-open.png | Bin 0 -> 643 bytes 4 files changed, 27 insertions(+), 17 deletions(-) create mode 100644 mapeditor/icons/lock-closed.png create mode 100644 mapeditor/icons/lock-open.png diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 829007394..7b53013a5 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -551,7 +551,7 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const catch (const std::exception & e) { showServerError( std::string("Unable to start map! Reason: ") + e.what()); - return; + throw e; } LobbyStartGame lsg; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index c1a4719f2..969d3f869 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -590,16 +590,18 @@ CLoadingScreen::CLoadingScreen() CCS->musich->stopMusic(5000); const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; - - const int posx = conf["x"].Integer(), posy = conf["y"].Integer(); - const int blockSize = conf["size"].Integer(); - const int blocksAmount = conf["amount"].Integer(); - - for(int i = 0; i < blocksAmount; ++i) + if(conf.isStruct()) { - progressBlocks.push_back(std::make_shared(conf["name"].String(), i, 0, posx + i * blockSize, posy)); - progressBlocks.back()->deactivate(); - progressBlocks.back()->visible = false; + const int posx = conf["x"].Integer(), posy = conf["y"].Integer(); + const int blockSize = conf["size"].Integer(); + const int blocksAmount = conf["amount"].Integer(); + + for(int i = 0; i < blocksAmount; ++i) + { + progressBlocks.push_back(std::make_shared(conf["name"].String(), i, 0, posx + i * blockSize, posy)); + progressBlocks.back()->deactivate(); + progressBlocks.back()->visible = false; + } } } @@ -628,14 +630,22 @@ void CLoadingScreen::showAll(Canvas & to) std::string CLoadingScreen::getBackground() { - const auto & conf = CMainMenuConfig::get().getConfig()["loading"]["background"].Vector(); + std::string fname = "loadbar"; + const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; - if(conf.empty()) + if(conf.isStruct()) { - return "loadbar"; - } - else - { - return RandomGeneratorUtil::nextItem(conf, CRandomGenerator::getDefault())->String(); + if(conf["background"].isVector()) + return RandomGeneratorUtil::nextItem(conf["background"].Vector(), CRandomGenerator::getDefault())->String(); + + if(conf["background"].isString()) + return conf["background"].String(); + + return fname; } + + if(conf.isVector() and !conf.Vector().empty()) + return RandomGeneratorUtil::nextItem(conf.Vector(), CRandomGenerator::getDefault())->String(); + + return fname; } diff --git a/mapeditor/icons/lock-closed.png b/mapeditor/icons/lock-closed.png new file mode 100644 index 0000000000000000000000000000000000000000..9d7e19c1741c352c91b0ebddb7e1748263203d45 GIT binary patch literal 649 zcmV;40(Sk0P)W<=%8c$EE?Klcph;R?Q_5P>1~|k4EH{t?>X=9uXDbUnf|A3X)`DyMlprGNX+A5 zW-j#{EHjH8L%4{gI&r5j;R1$wirGW*0o<&UUB_YUuKfsZ)ZeZlHUr>Po#0%9=6tO= z*;fG7S$U5qU37t`^_dv#C&1x)X2%M&$7}7ulGp=L$$0MJ!-nKLPgOZJ`$4sA@#t07mhp2Y|2G-cNw7_|XHvH*D)CK=hED>JS!<>Dmh- zVifnL15z*9q`U{}CVkQ5vNn7v? zcRPOH!Se=ybyuL(vph3bBVyX$N5{j=Tx}ZCSAZH~1@{Z2t+!xB)r2-`gZR}3;1wn# zVk1{|00;15T~R|FpHc1IRg7atk3L?HP0X}70kA3#~sT!fkW6?vg{qU jh{u`vvN3))fX%kwKT*vpKr@=@00000NkvXXu0mjflvgH% literal 0 HcmV?d00001 diff --git a/mapeditor/icons/lock-open.png b/mapeditor/icons/lock-open.png new file mode 100644 index 0000000000000000000000000000000000000000..d382de4c484d5379f24d57b08f86026701de2d71 GIT binary patch literal 643 zcmV-}0(||6P)3?ENmrW5dgmLUc;3H-- zbH25)%q(8mic9!`{A(@X8U|b0Y~j2Qw`ylMaSXe$3r8@GWz4n_(MG^z?fhZ`=Tt4? z>V^tX-IY&xTELygBKB;gfMfOPPZn?w)ZB9=w)+b2>UbU6`vsT!CEnp^iQ!TKqxg`S zUkmv&7%m}}_JO^sQl!I>r>Q9D}gqS3)cM?{fU3e4k4X1;E; d-%G$|;}2P`)GX9d%Xt6*002ovPDHLkV1jX?B;No4 literal 0 HcmV?d00001 From 4cf28fe00e7b6ac743f02b463197d2c1e9266ca1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 00:40:03 +0400 Subject: [PATCH 0123/1248] Fix linux clang compiling --- server/CVCMIServer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 41e5b065e..3dfcc8de2 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -291,7 +291,7 @@ bool CVCMIServer::prepareToStartGame() loadProgress->progress = progressTracking->get(); addToAnnounceQueue(std::move(loadProgress)); } - std::this_thread::sleep_for(std::chrono::milliseconds(200)); + boost::this_thread::sleep(boost::posix_time::milliseconds(200)); } progressTrackingFinished = false; }); From 7b4b01a2800d38bde7061b35eb3641ae6a18893b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 01:49:50 +0400 Subject: [PATCH 0124/1248] Adopt turn timer to battle refactoring --- server/CGameHandler.cpp | 10 ++- server/CGameHandler.h | 4 +- server/TurnTimerHandler.cpp | 88 ++++++++++++++++---------- server/TurnTimerHandler.h | 4 +- server/battles/BattleFlowProcessor.cpp | 2 + 5 files changed, 66 insertions(+), 42 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3c3961150..f0ec0cc1a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1066,10 +1066,8 @@ void CGameHandler::run(bool resume) { turnTimerHandler.onPlayerMakingTurn(gs->players[playerColor], waitTime); if(gs->curB) - { - turnTimerHandler.onBattleLoop(gs->players[gs->curB->getSidePlayer(BattleSide::ATTACKER)], waitTime); - turnTimerHandler.onBattleLoop(gs->players[gs->curB->getSidePlayer(BattleSide::DEFENDER)], waitTime); - } + turnTimerHandler.onBattleLoop(waitTime); + static time_duration p = milliseconds(waitTime); states.cv.timed_wait(lock, p); } @@ -1272,11 +1270,11 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if(!transit) { - for(auto topQuery = queries.topQuery(h->tempOwner); true; topQuery = queries.topQuery(h->tempOwner)) + for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner)) { moveQuery = std::dynamic_pointer_cast(topQuery); if(moveQuery) - queries.popIfTop(moveQuery); + queries->popIfTop(moveQuery); else break; } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 32ef3ba45..446970f59 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -83,8 +83,6 @@ class CGameHandler : public IGameCallback, public CBattleInfoCallback, public En CVCMIServer * lobby; std::shared_ptr> applier; - TurnTimerHandler turnTimerHandler; - public: using CCallbackBase::setBattle; @@ -108,6 +106,8 @@ public: SpellCastEnvironment * spellEnv; + + TurnTimerHandler turnTimerHandler; const Services * services() const override; const BattleCb * battle() const override; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 72cd002f2..d9b580d51 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -10,11 +10,14 @@ #include "StdInc.h" #include "TurnTimerHandler.h" #include "CGameHandler.h" +#include "battles/BattleProcessor.h" +#include "queries/QueriesProcessor.h" #include "../lib/battle/BattleInfo.h" #include "../lib/gameState/CGameState.h" #include "../lib/CPlayerState.h" #include "../lib/CStack.h" #include "../lib/StartInfo.h" +#include "../lib/NetPacks.h" TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): gameHandler(gh) @@ -80,7 +83,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) state.turnTimer.baseTimer = 0; onPlayerMakingTurn(state, waitTime); } - else if(!gameHandler.queries.topQuery(state.color)) //wait for replies to avoid pending queries + else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries gameHandler.states.players.at(state.color).makingTurn = false; //force end turn } } @@ -101,59 +104,80 @@ void TurnTimerHandler::onBattleStart(PlayerState & state) } } -void TurnTimerHandler::onBattleNextStack(PlayerState & state) +void TurnTimerHandler::onBattleNextStack(const CStack & stack) { - if(const auto * si = gameHandler.getStartInfo()) + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs) + return; + + const auto & state = gs->players.at(stack.getOwner()); + + if(si->turnTimerInfo.isBattleEnabled()) { - if(si->turnTimerInfo.isBattleEnabled()) - { - TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = state.turnTimer; - if(state.turnTimer.battleTimer < si->turnTimerInfo.battleTimer) - ttu.turnTimer.battleTimer = ttu.turnTimer.creatureTimer; - ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; - gameHandler.sendAndApply(&ttu); - } + TurnTimeUpdate ttu; + ttu.player = state.color; + ttu.turnTimer = state.turnTimer; + if(state.turnTimer.battleTimer < si->turnTimerInfo.battleTimer) + ttu.turnTimer.battleTimer = ttu.turnTimer.creatureTimer; + ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; + gameHandler.sendAndApply(&ttu); } } -void TurnTimerHandler::onBattleLoop(PlayerState & state, int waitTime) +void TurnTimerHandler::onBattleLoop(int waitTime) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->curB) return; - if(state.human && si->turnTimerInfo.isBattleEnabled()) + const auto * stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); + if(!stack) + return; + + auto & state = gs->players.at(gs->curB->getSidePlayer(stack->unitSide())); + + auto turnTimerUpdateApplier = [&](const TurnTimerInfo & tTimer) { - if(state.turnTimer.creatureTimer > 0) + TurnTimerInfo turnTimerUpdate = tTimer; + if(tTimer.creatureTimer > 0) { - state.turnTimer.creatureTimer -= waitTime; - int frequency = (state.turnTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); + turnTimerUpdate.creatureTimer -= waitTime; + int frequency = (turnTimerUpdate.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && state.turnTimer.creatureTimer % frequency == 0) + && turnTimerUpdate.creatureTimer % frequency == 0) { TurnTimeUpdate ttu; ttu.player = state.color; - ttu.turnTimer = state.turnTimer; + ttu.turnTimer = turnTimerUpdate; gameHandler.sendAndApply(&ttu); } + return true; } - else if(state.turnTimer.battleTimer > 0) + return false; + }; + + if(state.human && si->turnTimerInfo.isBattleEnabled()) + { + TurnTimerInfo turnTimer = state.turnTimer; + if(!turnTimerUpdateApplier(turnTimer)) { - state.turnTimer.creatureTimer = state.turnTimer.battleTimer; - state.turnTimer.battleTimer = 0; - onBattleLoop(state, waitTime); - } - else if(auto * stack = const_cast(gs->curB.get())->getStack(gs->curB->getActiveStackID())) - { - BattleAction doNothing; - doNothing.actionType = EActionType::DEFEND; - doNothing.side = stack->unitSide(); - doNothing.stackNumber = stack->unitId(); - gameHandler.makeAutomaticAction(stack, doNothing); + if(turnTimer.battleTimer > 0) + { + turnTimer.creatureTimer = turnTimer.battleTimer; + turnTimer.battleTimer = 0; + turnTimerUpdateApplier(turnTimer); + } + else + { + BattleAction doNothing; + doNothing.actionType = EActionType::DEFEND; + doNothing.side = stack->unitSide(); + doNothing.stackNumber = stack->unitId(); + gameHandler.battles->makePlayerBattleAction(state.color, doNothing); + } } } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index f42d50de1..acc545acc 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -34,6 +34,6 @@ public: void onPlayerGetTurn(PlayerState & state); void onPlayerMakingTurn(PlayerState & state, int waitTime); void onBattleStart(PlayerState & state); - void onBattleNextStack(PlayerState & state); - void onBattleLoop(PlayerState & state, int waitTime); + void onBattleNextStack(const CStack & stack); + void onBattleLoop(int waitTime); }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index acbd881d0..72d7b612e 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -315,6 +315,8 @@ void BattleFlowProcessor::activateNextStack() if(!removeGhosts.changedStacks.empty()) gameHandler->sendAndApply(&removeGhosts); + + gameHandler->turnTimerHandler.onBattleNextStack(*next); if (!tryMakeAutomaticAction(next)) { From dd3e9f102b9124706124ce712d230ac71b532c20 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 02:08:42 +0400 Subject: [PATCH 0125/1248] Fix game crash at AI turn --- server/TurnTimerHandler.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index d9b580d51..2fbda0227 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -108,7 +108,10 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs) + if(!si || !gs || !gs->curB) + return; + + if(!stack.getOwner().isValidPlayer()) return; const auto & state = gs->players.at(stack.getOwner()); @@ -133,7 +136,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) return; const auto * stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); - if(!stack) + if(!stack || !stack->getOwner().isValidPlayer()) return; auto & state = gs->players.at(gs->curB->getSidePlayer(stack->unitSide())); From fb21620c60f57610d7c94cfbd1d6686a83f7c0de Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 14:14:50 +0400 Subject: [PATCH 0126/1248] Fix battle timer reset after refactoring --- server/TurnTimerHandler.cpp | 15 ++++++++++++--- server/TurnTimerHandler.h | 2 +- server/battles/BattleFlowProcessor.cpp | 2 ++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 2fbda0227..f61b4e4d8 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -88,12 +88,21 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) } } -void TurnTimerHandler::onBattleStart(PlayerState & state) +void TurnTimerHandler::onBattleStart() { - if(const auto * si = gameHandler.getStartInfo()) + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + return; + + auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + + for(auto i : {attacker, defender}) { - if(si->turnTimerInfo.isBattleEnabled()) + if(i.isValidPlayer()) { + const auto & state = gs->players.at(i); TurnTimeUpdate ttu; ttu.player = state.color; ttu.turnTimer = state.turnTimer; diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index acc545acc..602ecfde0 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -33,7 +33,7 @@ public: void onGameplayStart(PlayerState & state); void onPlayerGetTurn(PlayerState & state); void onPlayerMakingTurn(PlayerState & state, int waitTime); - void onBattleStart(PlayerState & state); + void onBattleStart(); void onBattleNextStack(const CStack & stack); void onBattleLoop(int waitTime); }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 72d7b612e..0f0837a47 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -130,6 +130,8 @@ void BattleFlowProcessor::onBattleStarted() assert(gameHandler->gameState()->curB); tryPlaceMoats(); + + gameHandler->turnTimerHandler.onBattleStart(); if (gameHandler->gameState()->curB->tacticDistance == 0) onTacticsEnded(); From d9a2a7bfd0a80fbb385bb339e8e84393101f0ec6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 16:00:14 +0400 Subject: [PATCH 0127/1248] Fix minor points --- client/CServerHandler.cpp | 2 +- client/mainmenu/CMainMenu.cpp | 9 +++------ client/mainmenu/CMainMenu.h | 4 ++-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 7b53013a5..829007394 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -551,7 +551,7 @@ void CServerHandler::sendStartGame(bool allowOnlyAI) const catch (const std::exception & e) { showServerError( std::string("Unable to start map! Reason: ") + e.what()); - throw e; + return; } LobbyStartGame lsg; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 969d3f869..17fe0e6ef 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -587,6 +587,8 @@ CLoadingScreen::CLoadingScreen() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + addUsedEvents(TIME); + CCS->musich->stopMusic(5000); const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; @@ -609,11 +611,8 @@ CLoadingScreen::~CLoadingScreen() { } -void CLoadingScreen::showAll(Canvas & to) +void CLoadingScreen::tick(uint32_t msPassed) { - //FIXME: filling screen with transparency? BLACK intended? - //Rect rect(0, 0, to->w, to->h); - //CSDL_Ext::fillRect(to, rect, Colors::TRANSPARENCY); if(!progressBlocks.empty()) { int status = float(get()) / 255.f * progressBlocks.size(); @@ -624,8 +623,6 @@ void CLoadingScreen::showAll(Canvas & to) progressBlocks.at(i)->visible = true; } } - - CWindowObject::showAll(to); } std::string CLoadingScreen::getBackground() diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 544873a6a..7e632fe6f 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -9,7 +9,6 @@ */ #pragma once -#include "../widgets/Images.h" #include "../windows/CWindowObject.h" #include "../../lib/JsonNode.h" #include "../../lib/LoadProgress.h" @@ -24,6 +23,7 @@ class CTextInput; class CGStatusBar; class CTextBox; class CTabbedInt; +class CAnimImage; class CAnimation; class CButton; class CFilledTexture; @@ -191,7 +191,7 @@ public: CLoadingScreen(); ~CLoadingScreen(); - void showAll(Canvas & to) override; + void tick(uint32_t msPassed) override; }; extern std::shared_ptr CMM; From dfaf778d1606ce022ab93aeb05f9dc4aa89f0235 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 20:10:20 +0400 Subject: [PATCH 0128/1248] Redesign loading solution --- client/Client.cpp | 4 +-- client/NetPacksLobbyClient.cpp | 2 ++ lib/LoadProgress.cpp | 51 ++++++++++++++++++++++++++++++++++ lib/LoadProgress.h | 25 ++++++++++++++++- lib/gameState/CGameState.cpp | 8 +++--- lib/gameState/CGameState.h | 4 +-- server/CGameHandler.cpp | 2 +- server/CGameHandler.h | 2 +- server/CVCMIServer.cpp | 35 ++++++++++++----------- server/NetPacksLobbyServer.cpp | 4 +-- 10 files changed, 105 insertions(+), 32 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index ac20e1a1b..c650d3f4e 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -182,8 +182,8 @@ void CClient::newGame(CGameState * initializedGameState) logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); if(!initializedGameState) { - Load::Progress * progressTrackingPointer = nullptr; - gs->init(&mapService, CSH->si.get(), progressTrackingPointer, settings["general"]["saveRandomMaps"].Bool()); + Load::ProgressAccumulator progressTracking; + gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); } logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 6dd2ef315..ab6f7e2c0 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -133,6 +133,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack if(auto w = GH.windows().topWindow()) { w->finish(); + w->tick(0); w->redraw(); } else @@ -144,6 +145,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress if(auto w = GH.windows().topWindow()) { w->set(pack.progress); + w->tick(0); w->redraw(); } else diff --git a/lib/LoadProgress.cpp b/lib/LoadProgress.cpp index aa24a4c2e..70eadbdbb 100644 --- a/lib/LoadProgress.cpp +++ b/lib/LoadProgress.cpp @@ -18,6 +18,11 @@ Progress::Progress(): _progress(std::numeric_limits::min()) setupSteps(100); } +Progress::Progress(int steps): _progress(std::numeric_limits::min()) +{ + setupSteps(steps); +} + Type Progress::get() const { if(_step >= _maxSteps) @@ -82,3 +87,49 @@ void Progress::step(int count) _step += count; } } + +void ProgressAccumulator::include(const Progress & p) +{ + boost::unique_lock guard(_mx); + _progress.emplace_back(p); +} + +void ProgressAccumulator::exclude(const Progress & p) +{ + boost::unique_lock guard(_mx); + for(auto i = _progress.begin(); i != _progress.end(); ++i) + { + if(&i->get() == &p) + { + _accumulated += static_cast(p.get()) * p._maxSteps; + _steps += p._maxSteps; + _progress.erase(i); + return; + } + } +} + +bool ProgressAccumulator::finished() const +{ + boost::unique_lock guard(_mx); + for(auto i : _progress) + if(!i.get().finished()) + return false; + return true; +} + +Type ProgressAccumulator::get() const +{ + boost::unique_lock guard(_mx); + auto sum = _accumulated; + auto totalSteps = _steps; + for(auto p : _progress) + { + sum += static_cast(p.get().get()) * p.get()._maxSteps; + totalSteps += p.get()._maxSteps; + } + + if(totalSteps) + sum /= totalSteps; + return static_cast(sum); +} diff --git a/lib/LoadProgress.h b/lib/LoadProgress.h index 2911dab2a..1c3c21b0d 100644 --- a/lib/LoadProgress.h +++ b/lib/LoadProgress.h @@ -18,6 +18,8 @@ namespace Load using Type = unsigned char; +class ProgressAccumulator; + /* * Purpose of that class is to track progress of computations * Derive from this class if you want to translate user or system @@ -29,8 +31,9 @@ class DLL_LINKAGE Progress public: //Sets current state to 0. - //Amount of steps to finish progress will be equal to 100 + //Amount of steps to finish progress will be equal to 100 for default constructor Progress(); + Progress(int steps); virtual ~Progress() = default; //Returns current state of the progress @@ -67,5 +70,25 @@ public: private: std::atomic _progress, _target; std::atomic _step, _maxSteps; + + friend class ProgressAccumulator; }; + +class DLL_LINKAGE ProgressAccumulator +{ +public: + ProgressAccumulator() = default; + + void include(const Progress &); + void exclude(const Progress &); + + bool finished() const; + Type get() const; + +private: + mutable boost::mutex _mx; + long long _accumulated = 0, _steps = 0; + std::vector> _progress; +}; + } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 3cea0fa07..898243378 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -404,7 +404,7 @@ void CGameState::preInit(Services * services) this->services = services; } -void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Progress *& progressTracking, bool allowSavingRandomMap) +void CGameState::init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator & progressTracking, bool allowSavingRandomMap) { preInitAuto(); logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed); @@ -535,7 +535,7 @@ void CGameState::preInitAuto() } } -void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress *& progressTracking) +void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking) { if(scenarioOps->createRandomMap()) { @@ -544,10 +544,10 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan // Gen map CMapGenerator mapGenerator(*scenarioOps->mapGenOptions, scenarioOps->seedToBeUsed); - progressTracking = &mapGenerator; + progressTracking.include(mapGenerator); std::unique_ptr randomMap = mapGenerator.generate(); - progressTracking = nullptr; + progressTracking.exclude(mapGenerator); if(allowSavingRandomMap) { diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 546e774ac..ca906f6ff 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -90,7 +90,7 @@ public: void preInit(Services * services); - void init(const IMapService * mapService, StartInfo * si, Load::Progress *&, bool allowSavingRandomMap = false); + void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = false); void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) @@ -167,7 +167,7 @@ public: private: // ----- initialization ----- void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::Progress *& progressTracking); + void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); void checkMapChecksum(); void initGlobalBonuses(); void initGrailPosition(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 6f238b92c..9a4b35bdd 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -573,7 +573,7 @@ void CGameHandler::reinitScripting() #endif } -void CGameHandler::init(StartInfo *si, Load::Progress *& progressTracking) +void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTracking) { if (si->seedToBeUsed == 0) { diff --git a/server/CGameHandler.h b/server/CGameHandler.h index aacd6a5b8..f235cd430 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -199,7 +199,7 @@ public: void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero ////////////////////////////////////////////////////////////////////////// - void init(StartInfo *si, Load::Progress *& progressTracking); + void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); void handleClientDisconnection(std::shared_ptr c); void handleReceivedPack(CPackForServer * pack); PlayerColor getPlayerAt(std::shared_ptr c) const; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 0c1d255a4..d54d25050 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -279,23 +279,20 @@ void CVCMIServer::prepareToRestart() bool CVCMIServer::prepareToStartGame() { - Load::Progress * progressTracking = nullptr; - bool progressTrackingFinished = false; - std::thread progressTrackingThread([this, &progressTracking, &progressTrackingFinished](){ - while(!progressTrackingFinished) + Load::ProgressAccumulator progressTracking; + Load::Progress current(1); + progressTracking.include(current); + + auto progressTrackingThread = boost::thread([this, &progressTracking]() + { + while(!progressTracking.finished()) { - if(progressTracking) - { - boost::unique_lock queueLock(mx); - std::unique_ptr loadProgress(new LobbyLoadProgress); - loadProgress->progress = progressTracking->get(); - addToAnnounceQueue(std::move(loadProgress)); - } - boost::this_thread::sleep(boost::posix_time::milliseconds(200)); + std::unique_ptr loadProgress(new LobbyLoadProgress); + loadProgress->progress = progressTracking.get(); + addToAnnounceQueue(std::move(loadProgress)); + boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } - progressTrackingFinished = false; }); - progressTrackingThread.detach(); gh = std::make_shared(this); switch(si->mode) @@ -315,7 +312,11 @@ bool CVCMIServer::prepareToStartGame() case StartInfo::LOAD_GAME: logNetwork->info("Preparing to start loaded game"); if(!gh->load(si->mapname)) + { + current.finish(); + progressTrackingThread.join(); return false; + } break; default: logNetwork->error("Wrong mode in StartInfo!"); @@ -323,11 +324,9 @@ bool CVCMIServer::prepareToStartGame() break; } - progressTrackingFinished = true; - while(progressTrackingFinished) - continue; + current.finish(); + progressTrackingThread.join(); - state = EServerState::GAMEPLAY_STARTING; return true; } diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 70e485d93..ec7449e99 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -289,9 +289,6 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) result = false; return; } - // Announce loading - std::unique_ptr loadProgress(new LobbyLoadProgress); - srv.addToAnnounceQueue(std::move(loadProgress)); // Server will prepare gamestate and we announce StartInfo to clients if(!srv.prepareToStartGame()) @@ -303,6 +300,7 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) pack.initializedStartInfo = std::make_shared(*srv.gh->getStartInfo(true)); pack.initializedGameState = srv.gh->gameState(); + srv.state = EServerState::GAMEPLAY_STARTING; result = true; } From 15847c89b6ce0f0106388ac99c5fc38fbde90797 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 22 Aug 2023 21:02:32 +0400 Subject: [PATCH 0129/1248] Fix tests --- test/game/CGameStateTest.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index ae9bd2af0..32f0cd176 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -178,7 +178,8 @@ public: } - gameState->init(&mapService, &si, false); + Load::ProgressAccumulator progressTracker; + gameState->init(&mapService, &si, progressTracker, false); ASSERT_NE(map, nullptr); ASSERT_EQ(map->heroesOnMap.size(), 2); From 68a1b883eb50c4698b0d0f433d10c76747bf2463 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:43:44 +0200 Subject: [PATCH 0130/1248] autosave folders --- Global.h | 4 ++++ client/CPlayerInterface.cpp | 8 +++++--- lib/StartInfo.h | 4 +++- lib/serializer/CSerializer.h | 4 ++-- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Global.h b/Global.h index 3c308f54c..e0c22dfae 100644 --- a/Global.h +++ b/Global.h @@ -121,6 +121,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include #include #include #include @@ -166,6 +167,9 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include +#include +#include #ifndef M_PI # define M_PI 3.14159265358979323846 diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 3a407a796..d1dd2fb8b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -193,6 +193,8 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) void CPlayerInterface::performAutosave() { + std::string id = cb->getStartInfo()->uuid.substr(0, 8); + int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); if(frequency > 0 && cb->getDate() % frequency == 0) { @@ -204,7 +206,7 @@ void CPlayerInterface::performAutosave() prefix = settings["general"]["savePrefix"].String(); if(prefix.empty()) { - prefix = cb->getMapHeader()->name.substr(0, 5) + "_"; + prefix = cb->getMapHeader()->name.substr(0, 18) + "_" + id + "/"; } } @@ -213,7 +215,7 @@ void CPlayerInterface::performAutosave() int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); if(autosaveCountLimit > 0) { - cb->save("Saves/" + prefix + "Autosave_" + std::to_string(autosaveCount)); + cb->save("Saves/Autosave/" + prefix + "Autosave_" + std::to_string(autosaveCount)); autosaveCount %= autosaveCountLimit; } else @@ -222,7 +224,7 @@ void CPlayerInterface::performAutosave() + std::to_string(cb->getDate(Date::WEEK)) + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - cb->save("Saves/" + prefix + "Autosave_" + stringifiedDate); + cb->save("Saves/Autosave/" + prefix + "Autosave_" + stringifiedDate); } } } diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 779f9c9e0..6a672a2da 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -80,6 +80,7 @@ struct DLL_LINKAGE StartInfo ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant + std::string uuid; ui8 turnTime; //in minutes, 0=unlimited std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } @@ -103,6 +104,7 @@ struct DLL_LINKAGE StartInfo h & seedToBeUsed; h & seedPostInit; h & mapfileChecksum; + h & uuid; h & turnTime; h & mapname; h & mapGenOptions; @@ -110,7 +112,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), turnTime(0) + mapfileChecksum(0), turnTime(0), uuid(boost::uuids::to_string(boost::uuids::random_generator()())) { } diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 926d73b6c..086eb0d04 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 825; -const ui32 MINIMAL_SERIALIZATION_VERSION = 824; +const ui32 SERIALIZATION_VERSION = 826; +const ui32 MINIMAL_SERIALIZATION_VERSION = 826; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From 65884b15d0895fa5af3c000a2fa6c8d62ba0f5cb Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 22 Aug 2023 20:51:28 +0200 Subject: [PATCH 0131/1248] Update settings.json best setting as default (subfolder with map-name) --- config/schemas/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index eb2ca4d0d..351e7493b 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -117,7 +117,7 @@ }, "useSavePrefix" : { "type": "boolean", - "default": false + "default": true }, "savePrefix" : { "type": "string", From 937935ce8cfb563472b0fb836bbc4face7ab1ccd Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 23 Aug 2023 01:20:29 +0400 Subject: [PATCH 0132/1248] Fix compiling (how does it work on mac?) --- client/mainmenu/CMainMenu.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 17fe0e6ef..ea2bdb93f 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -641,7 +641,7 @@ std::string CLoadingScreen::getBackground() return fname; } - if(conf.isVector() and !conf.Vector().empty()) + if(conf.isVector() && !conf.Vector().empty()) return RandomGeneratorUtil::nextItem(conf.Vector(), CRandomGenerator::getDefault())->String(); return fname; From 51ba22a63152fa9fba2756481b6db6c32fdbd13c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 00:35:44 +0200 Subject: [PATCH 0133/1248] code review --- client/CPlayerInterface.cpp | 2 +- client/CServerHandler.cpp | 3 --- lib/StartInfo.h | 4 ++-- server/CVCMIServer.cpp | 5 ----- 4 files changed, 3 insertions(+), 11 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index d1dd2fb8b..22d98ad52 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -193,7 +193,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) void CPlayerInterface::performAutosave() { - std::string id = cb->getStartInfo()->uuid.substr(0, 8); + std::string id = cb->getStartInfo()->gameUuid.substr(0, 8); int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); if(frequency > 0 && cb->getDate() % frequency == 0) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 829007394..2ecf2af1c 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -49,9 +49,6 @@ #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" -#include -#include -#include #include #include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 6a672a2da..6f2fc9656 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -80,7 +80,7 @@ struct DLL_LINKAGE StartInfo ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant - std::string uuid; + std::string gameUuid; ui8 turnTime; //in minutes, 0=unlimited std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } @@ -104,7 +104,7 @@ struct DLL_LINKAGE StartInfo h & seedToBeUsed; h & seedPostInit; h & mapfileChecksum; - h & uuid; + h & gameUuid; h & turnTime; h & mapname; h & mapGenOptions; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 83a711fe9..93bedbfc6 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -50,11 +50,6 @@ // for applier #include "../lib/registerTypes/RegisterTypes.h" -// UUID generation -#include -#include -#include - #include "../lib/gameState/CGameState.h" template class CApplyOnServer; From a0450d136338c96d839e2e298d4d95bf28b25f86 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 00:39:09 +0200 Subject: [PATCH 0134/1248] format... --- lib/StartInfo.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 56d0f588f..3b1fe58d2 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -81,7 +81,7 @@ struct DLL_LINKAGE StartInfo ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant - std::string gameUuid; + std::string gameUuid; TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } @@ -105,7 +105,7 @@ struct DLL_LINKAGE StartInfo h & seedToBeUsed; h & seedPostInit; h & mapfileChecksum; - h & gameUuid; + h & gameUuid; h & turnTimerInfo; h & mapname; h & mapGenOptions; From 538acfe3c093e3c3995baaaf480578ef43d8f33f Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 01:08:57 +0200 Subject: [PATCH 0135/1248] Update StartInfo.h fix --- lib/StartInfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 3b1fe58d2..fec4fb7af 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -113,7 +113,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), uuid(boost::uuids::to_string(boost::uuids::random_generator()())) + mapfileChecksum(0), gameUuid(boost::uuids::to_string(boost::uuids::random_generator()())) { } From d2bbe0f35a88ac82b7efd624aa69e19c4d58b77c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 01:47:55 +0200 Subject: [PATCH 0136/1248] starttime in foldername --- client/CPlayerInterface.cpp | 10 +++++----- include/vstd/DateUtils.h | 1 + lib/StartInfo.h | 6 +++++- lib/vstd/DateUtils.cpp | 9 +++++++++ 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index b6498d6b4..fcf684593 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -169,7 +169,7 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; - + makingTurn = false; stillMoveHero.setn(STOP_MOVE); @@ -180,7 +180,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) GH.windows().pushWindow(adventureInt); } - //close window from another player +//close window from another player if(auto w = GH.windows().topWindow()) if(w->ID == -1 && player != playerID) w->close(); @@ -201,7 +201,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) void CPlayerInterface::performAutosave() { - std::string id = cb->getStartInfo()->gameUuid.substr(0, 8); + std::string id = cb->getStartInfo()->gameUuid.substr(0, 4); int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); if(frequency > 0 && cb->getDate() % frequency == 0) @@ -214,7 +214,7 @@ void CPlayerInterface::performAutosave() prefix = settings["general"]["savePrefix"].String(); if(prefix.empty()) { - prefix = cb->getMapHeader()->name.substr(0, 18) + "_" + id + "/"; + prefix = cb->getMapHeader()->name.substr(0, 15) + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; } } @@ -232,7 +232,7 @@ void CPlayerInterface::performAutosave() + std::to_string(cb->getDate(Date::WEEK)) + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - cb->save("Saves/Autosave/" + prefix + "Autosave_" + stringifiedDate); + cb->save("Saves/Autosave/" + prefix + "Autosave_" + id + "_" + stringifiedDate); } } } diff --git a/include/vstd/DateUtils.h b/include/vstd/DateUtils.h index 9f3a9cc43..d080e96b1 100644 --- a/include/vstd/DateUtils.h +++ b/include/vstd/DateUtils.h @@ -6,6 +6,7 @@ namespace vstd { DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt); + DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt); } diff --git a/lib/StartInfo.h b/lib/StartInfo.h index fec4fb7af..afcaade81 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -9,6 +9,8 @@ */ #pragma once +#include "vstd/DateUtils.h" + #include "GameConstants.h" #include "TurnTimerInfo.h" #include "campaign/CampaignConstants.h" @@ -82,6 +84,7 @@ struct DLL_LINKAGE StartInfo ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant std::string gameUuid; + std::string startTimeIso8601; TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } @@ -106,6 +109,7 @@ struct DLL_LINKAGE StartInfo h & seedPostInit; h & mapfileChecksum; h & gameUuid; + h & startTimeIso8601; h & turnTimerInfo; h & mapname; h & mapGenOptions; @@ -113,7 +117,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), gameUuid(boost::uuids::to_string(boost::uuids::random_generator()())) + mapfileChecksum(0), gameUuid(boost::uuids::to_string(boost::uuids::random_generator()())), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))) { } diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index 41f2d59c7..f494f3e66 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -15,6 +15,15 @@ namespace vstd return s.str(); } + DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt) + { + std::tm tm = *std::localtime(&dt); + std::stringstream s; + s.imbue(std::locale("")); + s << std::put_time(&tm, "%Y%m%dT%H%M%S"); + return s.str(); + } + } VCMI_LIB_NAMESPACE_END From cda4ae84aa224c237aad568a12bd7b754ecadc52 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 23 Aug 2023 15:45:00 +0400 Subject: [PATCH 0137/1248] Don't send loading updates too frequently --- server/CVCMIServer.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index d54d25050..4028ecc2d 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -282,14 +282,19 @@ bool CVCMIServer::prepareToStartGame() Load::ProgressAccumulator progressTracking; Load::Progress current(1); progressTracking.include(current); + Load::Type currentProgress = std::numeric_limits::max(); - auto progressTrackingThread = boost::thread([this, &progressTracking]() + auto progressTrackingThread = boost::thread([this, &progressTracking, ¤tProgress]() { while(!progressTracking.finished()) { - std::unique_ptr loadProgress(new LobbyLoadProgress); - loadProgress->progress = progressTracking.get(); - addToAnnounceQueue(std::move(loadProgress)); + if(progressTracking.get() != currentProgress) + { + currentProgress = progressTracking.get(); + std::unique_ptr loadProgress(new LobbyLoadProgress); + loadProgress->progress = currentProgress; + addToAnnounceQueue(std::move(loadProgress)); + } boost::this_thread::sleep(boost::posix_time::milliseconds(50)); } }); From 142889e3a58bce2bcb404029d92e14be347f7d83 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 23:09:17 +0300 Subject: [PATCH 0138/1248] Give all threads created by client human-readable name for debug --- client/CMT.cpp | 12 +++++++++--- client/CPlayerInterface.cpp | 3 +++ client/CServerHandler.cpp | 4 ++-- client/adventureMap/CInGameConsole.cpp | 2 ++ client/battle/BattleInterface.cpp | 2 ++ client/mainmenu/CMainMenu.cpp | 2 +- 6 files changed, 19 insertions(+), 6 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 1a06bb913..fcf9a0e80 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -27,11 +27,12 @@ #include "render/IScreenHandler.h" #include "render/Graphics.h" -#include "../lib/filesystem/Filesystem.h" +#include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" +#include "../lib/CThreadHelper.h" #include "../lib/VCMIDirs.h" #include "../lib/VCMI_Lib.h" -#include "../lib/CConfigHandler.h" +#include "../lib/filesystem/Filesystem.h" #include "../lib/logging/CBasicLogConfigurator.h" @@ -297,7 +298,11 @@ int main(int argc, char * argv[]) #ifndef VCMI_NO_THREADED_LOAD //we can properly play intro only in the main thread, so we have to move loading to the separate thread - boost::thread loading(init); + boost::thread loading([]() + { + setThreadName("initialize"); + init(); + }); #else init(); #endif @@ -429,6 +434,7 @@ void playIntro() static void mainLoop() { + setThreadName("MainGUI"); inGuiThread.reset(new bool(true)); while(1) //main SDL events loop diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 1e3e25275..d98f72fad 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -76,6 +76,7 @@ #include "../lib/UnlockGuard.h" #include "../lib/RoadHandler.h" #include "../lib/TerrainHandler.h" +#include "../lib/CThreadHelper.h" #include "CServerHandler.h" // FIXME: only needed for CGameState::mutex #include "../lib/gameState/CGameState.h" @@ -1933,6 +1934,8 @@ void CPlayerInterface::setMovementStatus(bool value) void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) { + setThreadName("doMoveHero"); + int i = 1; auto getObj = [&](int3 coord, bool ignoreHero) { diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ea6c3fbae..2a992b9f0 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -817,7 +817,7 @@ public: void CServerHandler::threadHandleConnection() { - setThreadName("CServerHandler::threadHandleConnection"); + setThreadName("threadHandleConnection"); c->enterLobbyConnectionMode(); try @@ -898,7 +898,7 @@ void CServerHandler::visitForClient(CPackForClient & clientPack) void CServerHandler::threadRunServer() { #if !defined(VCMI_MOBILE) - setThreadName("CServerHandler::threadRunServer"); + setThreadName("threadRunServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() + " --port=" + std::to_string(getHostPort()) diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 3236a5bc2..8ab4aa479 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -27,6 +27,7 @@ #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" +#include "../../lib/CThreadHelper.h" #include "../../lib/TextOperations.h" #include "../../lib/mapObjects/CArmedInstance.h" @@ -255,6 +256,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText) //some commands like gosolo don't work when executed from GUI thread auto threadFunction = [=]() { + setThreadName("processCommand"); ClientCommandManager commandController; commandController.processCommand(txt.substr(1), true); }; diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index c21af7bf2..dc1ff5161 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -43,6 +43,7 @@ #include "../../lib/NetPacks.h" #include "../../lib/UnlockGuard.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/CThreadHelper.h" BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, @@ -731,6 +732,7 @@ void BattleInterface::requestAutofightingAIToTakeAction() // HOWEVER this thread won't atttempt to lock game state, potentially leading to races boost::thread aiThread([this, activeStack]() { + setThreadName("autofightingAI"); curInt->autofightingAI->activeStack(activeStack); }); aiThread.detach(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index ea2bdb93f..68cf793bf 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -567,7 +567,7 @@ void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) { - setThreadName("CSimpleJoinScreen::connectThread"); + setThreadName("connectThread"); if(!addr.length()) CSH->startLocalServerAndConnect(); else From 87957e74c1af6019d241d76fe7fa47f529f693f2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 31 Jul 2023 17:00:37 +0300 Subject: [PATCH 0139/1248] Replaced boost::thread_specific_ptr with thread_local --- AI/Nullkiller/AIGateway.cpp | 18 ++++++++++-------- AI/Nullkiller/AIUtility.cpp | 4 ---- AI/Nullkiller/AIUtility.h | 6 ++++-- AI/Nullkiller/Behaviors/BuildingBehavior.cpp | 3 --- AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp | 3 --- .../Behaviors/CaptureObjectsBehavior.cpp | 3 --- AI/Nullkiller/Behaviors/ClusterBehavior.cpp | 3 --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 3 --- AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp | 3 --- .../Behaviors/RecruitHeroBehavior.cpp | 3 --- AI/Nullkiller/Behaviors/StartupBehavior.cpp | 3 --- AI/Nullkiller/Engine/DeepDecomposer.cpp | 3 --- AI/Nullkiller/Engine/FuzzyEngines.cpp | 2 -- AI/Nullkiller/Engine/Nullkiller.cpp | 5 +---- AI/Nullkiller/Goals/AbstractGoal.cpp | 3 --- AI/Nullkiller/Goals/AdventureSpellCast.cpp | 3 --- AI/Nullkiller/Goals/BuildBoat.cpp | 3 --- AI/Nullkiller/Goals/BuildThis.cpp | 4 ---- AI/Nullkiller/Goals/BuyArmy.cpp | 3 --- AI/Nullkiller/Goals/CaptureObject.cpp | 2 -- AI/Nullkiller/Goals/CompleteQuest.cpp | 3 --- AI/Nullkiller/Goals/Composition.cpp | 3 --- AI/Nullkiller/Goals/DigAtTile.cpp | 3 --- AI/Nullkiller/Goals/DismissHero.cpp | 3 --- AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp | 3 --- AI/Nullkiller/Goals/ExecuteHeroChain.cpp | 3 --- AI/Nullkiller/Goals/RecruitHero.cpp | 3 --- AI/Nullkiller/Goals/SaveResources.cpp | 3 --- AI/Nullkiller/Markers/ArmyUpgrade.cpp | 3 --- AI/Nullkiller/Markers/HeroExchange.cpp | 3 --- AI/Nullkiller/Markers/UnlockCluster.cpp | 3 --- .../Pathfinding/Actions/BattleAction.cpp | 3 --- .../Pathfinding/Actions/BoatActions.cpp | 7 ++----- .../Pathfinding/Actions/BuyArmyAction.cpp | 3 --- .../Pathfinding/Actions/QuestAction.cpp | 3 --- .../Pathfinding/Actions/TownPortalAction.cpp | 5 +---- AI/VCAI/AIUtility.cpp | 4 ---- AI/VCAI/AIUtility.h | 6 ++++-- AI/VCAI/FuzzyEngines.cpp | 3 --- AI/VCAI/FuzzyHelper.cpp | 5 +---- AI/VCAI/FuzzyHelper.h | 2 ++ AI/VCAI/Goals/AbstractGoal.cpp | 4 ---- AI/VCAI/Goals/AdventureSpellCast.cpp | 4 ---- AI/VCAI/Goals/Build.cpp | 5 ----- AI/VCAI/Goals/BuildBoat.cpp | 4 ---- AI/VCAI/Goals/BuildThis.cpp | 5 ----- AI/VCAI/Goals/BuyArmy.cpp | 5 ----- AI/VCAI/Goals/ClearWayTo.cpp | 5 ----- AI/VCAI/Goals/CollectRes.cpp | 5 ----- AI/VCAI/Goals/CompleteQuest.cpp | 4 ---- AI/VCAI/Goals/Conquer.cpp | 5 ----- AI/VCAI/Goals/DigAtTile.cpp | 5 ----- AI/VCAI/Goals/Explore.cpp | 8 ++------ AI/VCAI/Goals/FindObj.cpp | 4 ---- AI/VCAI/Goals/GatherArmy.cpp | 5 ----- AI/VCAI/Goals/GatherTroops.cpp | 5 ----- AI/VCAI/Goals/GetArtOfType.cpp | 7 +------ AI/VCAI/Goals/RecruitHero.cpp | 5 ----- AI/VCAI/Goals/VisitHero.cpp | 4 ---- AI/VCAI/Goals/VisitObj.cpp | 5 ----- AI/VCAI/Goals/VisitTile.cpp | 5 ----- AI/VCAI/Goals/Win.cpp | 5 ----- AI/VCAI/VCAI.cpp | 18 ++++++++++-------- client/mainmenu/CMainMenu.cpp | 4 +++- test/CMakeLists.txt | 2 -- test/vcai/ResourceManagerTest.h | 13 ------------- test/vcai/ResurceManagerTest.cpp | 1 - 67 files changed, 41 insertions(+), 255 deletions(-) delete mode 100644 test/vcai/ResourceManagerTest.h diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 8be0aa310..c0b87dc4f 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -34,26 +34,26 @@ const float RETREAT_THRESHOLD = 0.3f; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; //one thread may be turn of AI and another will be handling a side effect for AI2 -boost::thread_specific_ptr cb; -boost::thread_specific_ptr ai; +thread_local CCallback * cb = nullptr; +thread_local AIGateway * ai = nullptr; //helper RAII to manage global ai/cb ptrs struct SetGlobalState { SetGlobalState(AIGateway * AI) { - assert(!ai.get()); - assert(!cb.get()); + assert(!ai); + assert(!cb); - ai.reset(AI); - cb.reset(AI->myCb.get()); + ai = AI; + cb = AI->myCb.get(); } ~SetGlobalState() { //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully //TODO: to ensure that, make rm unique_ptr - ai.release(); - cb.release(); + ai = nullptr; + cb = nullptr; } }; @@ -1472,6 +1472,8 @@ void AIGateway::requestActionASAP(std::function whatToDo) boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); + + newThread.detach(); } void AIGateway::lostHero(HeroPtr h) diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index e6962673e..a866f3686 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -25,10 +25,6 @@ namespace NKAI { -extern boost::thread_specific_ptr ai; - -//extern static const int3 dirs[8]; - const CGObjectInstance * ObjectIdRef::operator->() const { return cb->getObj(id, false); diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index 60fef0f24..e8e4636ef 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -57,6 +57,7 @@ using dwellingContent = std::pair>; namespace NKAI { struct creInfo; +class AIGateway; class Nullkiller; const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; @@ -67,7 +68,8 @@ const int ALLOWED_ROAMING_HEROES = 8; extern const float SAFE_ATTACK_CONSTANT; extern const int GOLD_RESERVE; -extern boost::thread_specific_ptr cb; +extern thread_local CCallback * cb; +extern thread_local AIGateway * ai; enum HeroRole { @@ -201,7 +203,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev template void foreach_neighbour(const int3 & pos, const Func & foo) { - CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer + CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; diff --git a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp index 3220ff891..d21b92965 100644 --- a/AI/Nullkiller/Behaviors/BuildingBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuildingBehavior.cpp @@ -20,9 +20,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string BuildingBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp index 7b2a57396..b5260ac3a 100644 --- a/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string BuyArmyBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp index 155f45af6..9ce74a72f 100644 --- a/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp +++ b/AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp @@ -19,9 +19,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; template diff --git a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp index ff0679564..cc376acb8 100644 --- a/AI/Nullkiller/Behaviors/ClusterBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ClusterBehavior.cpp @@ -19,9 +19,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string ClusterBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index d9bdf3904..ded687924 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -25,9 +25,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - const float TREAT_IGNORE_RATIO = 2; using namespace Goals; diff --git a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp index c73b374c0..935e782f5 100644 --- a/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp +++ b/AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp @@ -23,9 +23,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string GatherArmyBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp index 8f9b80f6d..885cc7af2 100644 --- a/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp +++ b/AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string RecruitHeroBehavior::toString() const diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 820beb75f..84abb41fe 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -21,9 +21,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string StartupBehavior::toString() const diff --git a/AI/Nullkiller/Engine/DeepDecomposer.cpp b/AI/Nullkiller/Engine/DeepDecomposer.cpp index 8e649634f..88fd3ed5b 100644 --- a/AI/Nullkiller/Engine/DeepDecomposer.cpp +++ b/AI/Nullkiller/Engine/DeepDecomposer.cpp @@ -24,9 +24,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; void DeepDecomposer::reset() diff --git a/AI/Nullkiller/Engine/FuzzyEngines.cpp b/AI/Nullkiller/Engine/FuzzyEngines.cpp index 2afe4f5ef..c20b39143 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.cpp +++ b/AI/Nullkiller/Engine/FuzzyEngines.cpp @@ -20,8 +20,6 @@ namespace NKAI #define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us -extern boost::thread_specific_ptr ai; - engineBase::engineBase() { rules = new fl::RuleBlock(); diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index d6d7f41dc..8c75b8006 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -24,9 +24,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; #if NKAI_TRACE_LEVEL >= 1 @@ -341,7 +338,7 @@ void Nullkiller::executeTask(Goals::TTask task) try { - task->accept(ai.get()); + task->accept(ai); logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start)); } catch(goalFulfilledException &) diff --git a/AI/Nullkiller/Goals/AbstractGoal.cpp b/AI/Nullkiller/Goals/AbstractGoal.cpp index fd27579c5..1cc45a12a 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.cpp +++ b/AI/Nullkiller/Goals/AbstractGoal.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) diff --git a/AI/Nullkiller/Goals/AdventureSpellCast.cpp b/AI/Nullkiller/Goals/AdventureSpellCast.cpp index 8a8a3cf96..7e62e7d42 100644 --- a/AI/Nullkiller/Goals/AdventureSpellCast.cpp +++ b/AI/Nullkiller/Goals/AdventureSpellCast.cpp @@ -14,9 +14,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const diff --git a/AI/Nullkiller/Goals/BuildBoat.cpp b/AI/Nullkiller/Goals/BuildBoat.cpp index e69b90d3e..86b274f5f 100644 --- a/AI/Nullkiller/Goals/BuildBoat.cpp +++ b/AI/Nullkiller/Goals/BuildBoat.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool BuildBoat::operator==(const BuildBoat & other) const diff --git a/AI/Nullkiller/Goals/BuildThis.cpp b/AI/Nullkiller/Goals/BuildThis.cpp index d61caae44..2f371346e 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -17,12 +17,8 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; - BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid) : ElementarGoal(Goals::BUILD_STRUCTURE) { diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index f2e4aca05..3bc3c0faf 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool BuyArmy::operator==(const BuyArmy & other) const diff --git a/AI/Nullkiller/Goals/CaptureObject.cpp b/AI/Nullkiller/Goals/CaptureObject.cpp index 0dd71dc93..35a5d4417 100644 --- a/AI/Nullkiller/Goals/CaptureObject.cpp +++ b/AI/Nullkiller/Goals/CaptureObject.cpp @@ -18,8 +18,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; - using namespace Goals; bool CaptureObject::operator==(const CaptureObject & other) const diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 6e39c3d47..e4d3fda47 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool isKeyMaster(const QuestInfo & q) diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index 30d3791f9..ca77dc5e8 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool Composition::operator==(const Composition & other) const diff --git a/AI/Nullkiller/Goals/DigAtTile.cpp b/AI/Nullkiller/Goals/DigAtTile.cpp index a573bfbd0..6dce16b59 100644 --- a/AI/Nullkiller/Goals/DigAtTile.cpp +++ b/AI/Nullkiller/Goals/DigAtTile.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const diff --git a/AI/Nullkiller/Goals/DismissHero.cpp b/AI/Nullkiller/Goals/DismissHero.cpp index ce26e4f10..543b16e29 100644 --- a/AI/Nullkiller/Goals/DismissHero.cpp +++ b/AI/Nullkiller/Goals/DismissHero.cpp @@ -14,9 +14,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool DismissHero::operator==(const DismissHero & other) const diff --git a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp index 80e8af201..12ff31847 100644 --- a/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp +++ b/AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ExchangeSwapTownHeroes::ExchangeSwapTownHeroes( diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index d367f96b4..438c88b07 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * obj) diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index e787c7529..fe4aaeb6f 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; std::string RecruitHero::toString() const diff --git a/AI/Nullkiller/Goals/SaveResources.cpp b/AI/Nullkiller/Goals/SaveResources.cpp index 2cf03fc4c..6499ea457 100644 --- a/AI/Nullkiller/Goals/SaveResources.cpp +++ b/AI/Nullkiller/Goals/SaveResources.cpp @@ -15,9 +15,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool SaveResources::operator==(const SaveResources & other) const diff --git a/AI/Nullkiller/Markers/ArmyUpgrade.cpp b/AI/Nullkiller/Markers/ArmyUpgrade.cpp index 0f6d41090..ff61a6454 100644 --- a/AI/Nullkiller/Markers/ArmyUpgrade.cpp +++ b/AI/Nullkiller/Markers/ArmyUpgrade.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) diff --git a/AI/Nullkiller/Markers/HeroExchange.cpp b/AI/Nullkiller/Markers/HeroExchange.cpp index 499122327..cef89c1db 100644 --- a/AI/Nullkiller/Markers/HeroExchange.cpp +++ b/AI/Nullkiller/Markers/HeroExchange.cpp @@ -17,9 +17,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool HeroExchange::operator==(const HeroExchange & other) const diff --git a/AI/Nullkiller/Markers/UnlockCluster.cpp b/AI/Nullkiller/Markers/UnlockCluster.cpp index bbf7c99c7..c52ee8345 100644 --- a/AI/Nullkiller/Markers/UnlockCluster.cpp +++ b/AI/Nullkiller/Markers/UnlockCluster.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - using namespace Goals; bool UnlockCluster::operator==(const UnlockCluster & other) const diff --git a/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp b/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp index b85bff624..33248df50 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BattleAction::execute(const CGHeroInstance * hero) const diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index 9fcf052ce..bbd1e5297 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp @@ -20,14 +20,11 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BuildBoatAction::execute(const CGHeroInstance * hero) const { - return Goals::BuildBoat(shipyard).accept(ai.get()); + return Goals::BuildBoat(shipyard).accept(ai); } Goals::TSubgoal BuildBoatAction::decompose(const CGHeroInstance * hero) const @@ -80,7 +77,7 @@ namespace AIPathfinding void SummonBoatAction::execute(const CGHeroInstance * hero) const { - Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai.get()); + Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai); } const ChainActor * SummonBoatAction::getActor(const ChainActor * sourceActor) const diff --git a/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp b/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp index 683f42246..a676ad5c1 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { void BuyArmyAction::execute(const CGHeroInstance * hero) const diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index a552ff2af..dae8c7bb2 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -16,9 +16,6 @@ namespace NKAI { -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - namespace AIPathfinding { bool QuestAction::canAct(const AIPathNode * node) const diff --git a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp index 2304e39dd..e32a13231 100644 --- a/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp @@ -18,9 +18,6 @@ namespace NKAI using namespace AIPathfinding; -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; - void TownPortalAction::execute(const CGHeroInstance * hero) const { auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL); @@ -28,7 +25,7 @@ void TownPortalAction::execute(const CGHeroInstance * hero) const goal.town = target; goal.tile = target->visitablePos(); - goal.accept(ai.get()); + goal.accept(ai); } std::string TownPortalAction::toString() const diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index e73ddf191..ff70a870d 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -21,12 +21,8 @@ #include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapping/CMapDefines.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; extern FuzzyHelper * fh; -//extern static const int3 dirs[8]; - const CGObjectInstance * ObjectIdRef::operator->() const { return cb->getObj(id, false); diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index ab1dc6521..fb1346382 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -18,6 +18,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" +class VCAI; class CCallback; struct creInfo; @@ -33,7 +34,8 @@ const int ALLOWED_ROAMING_HEROES = 8; extern const double SAFE_ATTACK_CONSTANT; extern const int GOLD_RESERVE; -extern boost::thread_specific_ptr cb; +extern thread_local CCallback * cb; +extern thread_local VCAI * ai; //provisional class for AI to store a reference to an owned hero object //checks if it's valid on access, should be used in place of const CGHeroInstance* @@ -192,7 +194,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev template void foreach_neighbour(const int3 & pos, const Func & foo) { - CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer + CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer for(const int3 & dir : int3::getDirs()) { const int3 n = pos + dir; diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 39003ee2f..91ba40139 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -18,9 +18,6 @@ #define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - engineBase::engineBase() { rules = new fl::RuleBlock(); diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index e6973b597..78595c49c 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -23,9 +23,6 @@ FuzzyHelper * fh; -extern boost::thread_specific_ptr ai; -extern boost::thread_specific_ptr cb; - Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) { if(vec.empty()) @@ -216,7 +213,7 @@ void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pa ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor) { - return evaluateDanger(tile, visitor, ai.get()); + return evaluateDanger(tile, visitor, ai); } ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai) diff --git a/AI/VCAI/FuzzyHelper.h b/AI/VCAI/FuzzyHelper.h index 6973df463..7542e7f70 100644 --- a/AI/VCAI/FuzzyHelper.h +++ b/AI/VCAI/FuzzyHelper.h @@ -51,3 +51,5 @@ public: ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai); ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor); }; + +extern FuzzyHelper * fh; diff --git a/AI/VCAI/Goals/AbstractGoal.cpp b/AI/VCAI/Goals/AbstractGoal.cpp index 87a04f089..45b055d25 100644 --- a/AI/VCAI/Goals/AbstractGoal.cpp +++ b/AI/VCAI/Goals/AbstractGoal.cpp @@ -16,10 +16,6 @@ #include "../BuildingManager.h" #include "../../../lib/StringConstants.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; TSubgoal Goals::sptr(const AbstractGoal & tmp) diff --git a/AI/VCAI/Goals/AdventureSpellCast.cpp b/AI/VCAI/Goals/AdventureSpellCast.cpp index c67d81d66..7c3f7500f 100644 --- a/AI/VCAI/Goals/AdventureSpellCast.cpp +++ b/AI/VCAI/Goals/AdventureSpellCast.cpp @@ -14,10 +14,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CGTownInstance.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const diff --git a/AI/VCAI/Goals/Build.cpp b/AI/VCAI/Goals/Build.cpp index 3bd78b9ef..182c8a4f4 100644 --- a/AI/VCAI/Goals/Build.cpp +++ b/AI/VCAI/Goals/Build.cpp @@ -19,11 +19,6 @@ #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; TGoalVec Build::getAllPossibleSubgoals() diff --git a/AI/VCAI/Goals/BuildBoat.cpp b/AI/VCAI/Goals/BuildBoat.cpp index 76e76791f..9f7f452ca 100644 --- a/AI/VCAI/Goals/BuildBoat.cpp +++ b/AI/VCAI/Goals/BuildBoat.cpp @@ -13,10 +13,6 @@ #include "../FuzzyHelper.h" #include "../AIhelper.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool BuildBoat::operator==(const BuildBoat & other) const diff --git a/AI/VCAI/Goals/BuildThis.cpp b/AI/VCAI/Goals/BuildThis.cpp index 9a62539a7..703f66784 100644 --- a/AI/VCAI/Goals/BuildThis.cpp +++ b/AI/VCAI/Goals/BuildThis.cpp @@ -18,11 +18,6 @@ #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool BuildThis::operator==(const BuildThis & other) const diff --git a/AI/VCAI/Goals/BuyArmy.cpp b/AI/VCAI/Goals/BuyArmy.cpp index f442c3dc5..7c3facb29 100644 --- a/AI/VCAI/Goals/BuyArmy.cpp +++ b/AI/VCAI/Goals/BuyArmy.cpp @@ -13,11 +13,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CGTownInstance.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool BuyArmy::operator==(const BuyArmy & other) const diff --git a/AI/VCAI/Goals/ClearWayTo.cpp b/AI/VCAI/Goals/ClearWayTo.cpp index e20c26ad5..ddea93732 100644 --- a/AI/VCAI/Goals/ClearWayTo.cpp +++ b/AI/VCAI/Goals/ClearWayTo.cpp @@ -16,11 +16,6 @@ #include "../FuzzyHelper.h" #include "../AIhelper.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool ClearWayTo::operator==(const ClearWayTo & other) const diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index 35d66413d..8494952e2 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -18,11 +18,6 @@ #include "../../../lib/mapObjects/CGMarket.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool CollectRes::operator==(const CollectRes & other) const diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 164aacb3c..f724e308b 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -14,10 +14,6 @@ #include "../AIhelper.h" #include "../../../lib/mapObjects/CQuest.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool CompleteQuest::operator==(const CompleteQuest & other) const diff --git a/AI/VCAI/Goals/Conquer.cpp b/AI/VCAI/Goals/Conquer.cpp index 3f9f4b16c..f18b629ed 100644 --- a/AI/VCAI/Goals/Conquer.cpp +++ b/AI/VCAI/Goals/Conquer.cpp @@ -17,11 +17,6 @@ #include "../BuildingManager.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool Conquer::operator==(const Conquer & other) const diff --git a/AI/VCAI/Goals/DigAtTile.cpp b/AI/VCAI/Goals/DigAtTile.cpp index 230d62a5e..2ff0343e9 100644 --- a/AI/VCAI/Goals/DigAtTile.cpp +++ b/AI/VCAI/Goals/DigAtTile.cpp @@ -13,11 +13,6 @@ #include "../VCAI.h" #include "../AIUtility.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool DigAtTile::operator==(const DigAtTile & other) const diff --git a/AI/VCAI/Goals/Explore.cpp b/AI/VCAI/Goals/Explore.cpp index 777b1a8ae..13571423f 100644 --- a/AI/VCAI/Goals/Explore.cpp +++ b/AI/VCAI/Goals/Explore.cpp @@ -18,10 +18,6 @@ #include "../../../lib/StringConstants.h" #include "../../../lib/CPlayerState.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; namespace Goals @@ -41,8 +37,8 @@ namespace Goals ExplorationHelper(HeroPtr h, bool gatherArmy) { - cbp = cb.get(); - aip = ai.get(); + cbp = cb; + aip = ai; hero = h; ts = cbp->getPlayerTeam(ai->playerID); sightRadius = hero->getSightRadius(); diff --git a/AI/VCAI/Goals/FindObj.cpp b/AI/VCAI/Goals/FindObj.cpp index 4588ca4e5..e14c3320c 100644 --- a/AI/VCAI/Goals/FindObj.cpp +++ b/AI/VCAI/Goals/FindObj.cpp @@ -14,10 +14,6 @@ #include "../VCAI.h" #include "../AIUtility.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool FindObj::operator==(const FindObj & other) const diff --git a/AI/VCAI/Goals/GatherArmy.cpp b/AI/VCAI/Goals/GatherArmy.cpp index c36e282e5..2e7ef718f 100644 --- a/AI/VCAI/Goals/GatherArmy.cpp +++ b/AI/VCAI/Goals/GatherArmy.cpp @@ -18,11 +18,6 @@ #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool GatherArmy::operator==(const GatherArmy & other) const diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index a93960237..7af7098e2 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -18,11 +18,6 @@ #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool GatherTroops::operator==(const GatherTroops & other) const diff --git a/AI/VCAI/Goals/GetArtOfType.cpp b/AI/VCAI/Goals/GetArtOfType.cpp index 5195796ec..2f0ef2227 100644 --- a/AI/VCAI/Goals/GetArtOfType.cpp +++ b/AI/VCAI/Goals/GetArtOfType.cpp @@ -13,11 +13,6 @@ #include "../VCAI.h" #include "../AIUtility.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool GetArtOfType::operator==(const GetArtOfType & other) const @@ -28,4 +23,4 @@ bool GetArtOfType::operator==(const GetArtOfType & other) const TSubgoal GetArtOfType::whatToDoToAchieve() { return sptr(FindObj(Obj::ARTIFACT, aid)); -} \ No newline at end of file +} diff --git a/AI/VCAI/Goals/RecruitHero.cpp b/AI/VCAI/Goals/RecruitHero.cpp index 73f24762c..99c5dd0f2 100644 --- a/AI/VCAI/Goals/RecruitHero.cpp +++ b/AI/VCAI/Goals/RecruitHero.cpp @@ -17,11 +17,6 @@ #include "../BuildingManager.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; TSubgoal RecruitHero::whatToDoToAchieve() diff --git a/AI/VCAI/Goals/VisitHero.cpp b/AI/VCAI/Goals/VisitHero.cpp index efb4840d3..5e57743fd 100644 --- a/AI/VCAI/Goals/VisitHero.cpp +++ b/AI/VCAI/Goals/VisitHero.cpp @@ -18,10 +18,6 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool VisitHero::operator==(const VisitHero & other) const diff --git a/AI/VCAI/Goals/VisitObj.cpp b/AI/VCAI/Goals/VisitObj.cpp index 5aab8cf06..be52de94d 100644 --- a/AI/VCAI/Goals/VisitObj.cpp +++ b/AI/VCAI/Goals/VisitObj.cpp @@ -17,11 +17,6 @@ #include "../BuildingManager.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool VisitObj::operator==(const VisitObj & other) const diff --git a/AI/VCAI/Goals/VisitTile.cpp b/AI/VCAI/Goals/VisitTile.cpp index eaec6efbb..369ba7380 100644 --- a/AI/VCAI/Goals/VisitTile.cpp +++ b/AI/VCAI/Goals/VisitTile.cpp @@ -17,11 +17,6 @@ #include "../BuildingManager.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; bool VisitTile::operator==(const VisitTile & other) const diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index 4b4958ed7..f2a3b3dce 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -19,11 +19,6 @@ #include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/StringConstants.h" - -extern boost::thread_specific_ptr cb; -extern boost::thread_specific_ptr ai; -extern FuzzyHelper * fh; - using namespace Goals; TSubgoal Win::whatToDoToAchieve() diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 8fd3ac923..7661dfba5 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -38,8 +38,8 @@ extern FuzzyHelper * fh; const double SAFE_ATTACK_CONSTANT = 1.5; //one thread may be turn of AI and another will be handling a side effect for AI2 -boost::thread_specific_ptr cb; -boost::thread_specific_ptr ai; +thread_local CCallback * cb = nullptr; +thread_local VCAI * ai = nullptr; //std::map > HeroView::infosCount; @@ -48,18 +48,18 @@ struct SetGlobalState { SetGlobalState(VCAI * AI) { - assert(!ai.get()); - assert(!cb.get()); + assert(!ai); + assert(!cb); - ai.reset(AI); - cb.reset(AI->myCb.get()); + ai = AI; + cb = AI->myCb.get(); } ~SetGlobalState() { //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully //TODO: to ensure that, make rm unique_ptr - ai.release(); - cb.release(); + ai = nullptr; + cb = nullptr; } }; @@ -2497,6 +2497,8 @@ void VCAI::requestActionASAP(std::function whatToDo) boost::shared_lock gsLock(CGameState::mutex); whatToDo(); }); + + newThread.detach(); } void VCAI::lostHero(HeroPtr h) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 68cf793bf..407dc66eb 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -562,7 +562,9 @@ void CSimpleJoinScreen::startConnectThread(const std::string & addr, ui16 port) // https://github.com/libsdl-org/SDL/blob/main/docs/README-android.md#threads-and-the-java-vm CVCMIServer::reuseClientJNIEnv(SDL_AndroidGetJNIEnv()); #endif - boost::thread(&CSimpleJoinScreen::connectThread, this, addr, port); + boost::thread connector(&CSimpleJoinScreen::connectThread, this, addr, port); + + connector.detach(); } void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 847978cb3..b0e26de4f 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -92,8 +92,6 @@ set(test_HEADERS spells/targetConditions/TargetConditionItemFixture.h - vcai/ResourceManagerTest.h - mock/BattleFake.h mock/mock_BonusBearer.h mock/mock_IGameCallback.h diff --git a/test/vcai/ResourceManagerTest.h b/test/vcai/ResourceManagerTest.h deleted file mode 100644 index 4b3992cca..000000000 --- a/test/vcai/ResourceManagerTest.h +++ /dev/null @@ -1,13 +0,0 @@ -/* -* ResourceManagerTest.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 - -extern boost::thread_specific_ptr cb; \ No newline at end of file diff --git a/test/vcai/ResurceManagerTest.cpp b/test/vcai/ResurceManagerTest.cpp index df6fd90be..c7a4cb840 100644 --- a/test/vcai/ResurceManagerTest.cpp +++ b/test/vcai/ResurceManagerTest.cpp @@ -12,7 +12,6 @@ #include "gtest/gtest.h" #include "../AI/VCAI/VCAI.h" -#include "ResourceManagerTest.h" #include "../AI/VCAI/Goals/Goals.h" #include "mock_VCAI_CGoal.h" #include "mock_VCAI.h" From 44d16b32fea2e424ef03ffe5673d099eaebccd7b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 23:45:41 +0300 Subject: [PATCH 0140/1248] Use API identical to std classes where possible --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/StupidAI/StupidAI.cpp | 2 +- AI/VCAI/VCAI.cpp | 2 +- Global.h | 9 ++++++--- client/CMT.cpp | 7 ++----- client/CPlayerInterface.cpp | 4 ++-- client/CServerHandler.cpp | 18 +++++++++--------- client/CVideoHandler.cpp | 2 +- client/battle/BattleWindow.cpp | 2 +- client/gui/CGuiHandler.cpp | 8 +++++--- client/mapView/mapHandler.cpp | 2 +- client/windows/CSpellWindow.cpp | 2 +- lib/CConsoleHandler.cpp | 2 +- lib/CRandomGenerator.cpp | 9 ++------- lib/CRandomGenerator.h | 1 - lib/CThreadHelper.cpp | 8 +++++--- lib/CThreadHelper.h | 8 ++++---- lib/logging/CLogger.cpp | 2 +- lib/mapObjects/CGCreature.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/rmg/CMapGenerator.cpp | 2 +- lib/rmg/CRmgTemplate.cpp | 4 ++-- lib/rmg/RmgMap.cpp | 4 ++-- lib/rmg/RmgObject.cpp | 12 ++++++------ lib/rmg/modificators/Modificator.cpp | 2 +- lib/rmg/modificators/RiverPlacer.cpp | 2 +- lib/rmg/threadpool/ThreadPool.h | 4 ++-- server/CGameHandler.cpp | 10 ++-------- server/CVCMIServer.cpp | 10 +++++----- server/StdInc.h | 7 ------- test/JsonComparer.cpp | 2 +- test/map/MapComparer.cpp | 6 +++--- 32 files changed, 72 insertions(+), 87 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index c0b87dc4f..575bbbc0e 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1607,7 +1607,7 @@ void AIStatus::waitTillFree() { boost::unique_lock lock(mx); while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) - cv.timed_wait(lock, boost::posix_time::milliseconds(10)); + cv.wait_for(lock, boost::chrono::milliseconds(10)); } bool AIStatus::haveTurn() diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 1ccc37f78..50db79eb9 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -108,7 +108,7 @@ void CStupidAI::yourTacticPhase(int distance) void CStupidAI::activeStack( const CStack * stack ) { - //boost::this_thread::sleep(boost::posix_time::seconds(2)); + //boost::this_thread::sleep_for(boost::chrono::seconds(2)); print("activeStack called for " + stack->nodeName()); ReachabilityInfo dists = cb->getReachability(stack); std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 7661dfba5..997248ef6 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -2677,7 +2677,7 @@ void AIStatus::waitTillFree() { boost::unique_lock lock(mx); while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) - cv.timed_wait(lock, boost::posix_time::milliseconds(100)); + cv.wait_for(lock, boost::chrono::milliseconds(100)); } bool AIStatus::haveTurn() diff --git a/Global.h b/Global.h index 3c308f54c..e62842a6d 100644 --- a/Global.h +++ b/Global.h @@ -149,8 +149,8 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include -#include -#include +#include +#include #include #include #include @@ -165,7 +165,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include -#include +#include +#include +#include +#include #ifndef M_PI # define M_PI 3.14159265358979323846 diff --git a/client/CMT.cpp b/client/CMT.cpp index fcf9a0e80..aebf3a916 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -54,8 +54,6 @@ namespace po = boost::program_options; namespace po_style = boost::program_options::command_line_style; -extern boost::thread_specific_ptr inGuiThread; - static po::variables_map vm; #ifndef VCMI_IOS @@ -416,7 +414,7 @@ int main(int argc, char * argv[]) else { while(true) - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } return 0; @@ -435,7 +433,6 @@ void playIntro() static void mainLoop() { setThreadName("MainGUI"); - inGuiThread.reset(new bool(true)); while(1) //main SDL events loop { @@ -476,7 +473,7 @@ static void quitApplication() vstd::clear_pointer(console);// should be removed after everything else since used by logging - boost::this_thread::sleep(boost::posix_time::milliseconds(750));//??? + boost::this_thread::sleep_for(boost::chrono::milliseconds(750));//??? if(!settings["session"]["headless"].Bool()) GH.screenHandler().close(); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index d98f72fad..73095a506 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1448,7 +1448,7 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) { auto unlockPim = vstd::makeUnlockGuard(*pim); IgnoreEvents ignore(*this); - boost::this_thread::sleep(boost::posix_time::milliseconds(focusTime)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); } } CCS->curh->show(); @@ -1875,7 +1875,7 @@ void CPlayerInterface::waitForAllDialogs(bool unlockPim) while(!dialogs.empty()) { auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); - boost::this_thread::sleep(boost::posix_time::milliseconds(5)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); } waitWhileDialog(unlockPim); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 2a992b9f0..86bce71b4 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -231,7 +231,7 @@ void CServerHandler::startLocalServerAndConnect() while(!androidTestServerReadyFlag.load()) { logNetwork->info("still waiting..."); - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } logNetwork->info("waiting for server finished..."); androidTestServerReadyFlag = false; @@ -261,7 +261,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po catch(std::runtime_error & error) { logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); - boost::this_thread::sleep(boost::posix_time::seconds(1)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } } @@ -307,7 +307,7 @@ void CServerHandler::stopServerConnection() { if(c->handler) { - while(!c->handler->timed_join(boost::posix_time::milliseconds(50))) + while(!c->handler->timed_join(boost::chrono::milliseconds(50))) applyPacksOnLobbyScreen(); c->handler->join(); } @@ -761,20 +761,20 @@ void CServerHandler::debugStartTest(std::string filename, bool save) else startLocalServerAndConnect(); - boost::this_thread::sleep(boost::posix_time::milliseconds(100)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); while(!settings["session"]["headless"].Bool() && !GH.windows().topWindow()) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); while(!mi || mapInfo->fileURI != CSH->mi->fileURI) { setMapInfo(mapInfo); - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } // "Click" on color to remove us from it setPlayer(myFirstColor()); while(myFirstColor() != PlayerColor::CANNOT_DETERMINE) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); while(true) { @@ -787,7 +787,7 @@ void CServerHandler::debugStartTest(std::string filename, bool save) { } - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } } @@ -826,7 +826,7 @@ void CServerHandler::threadHandleConnection() while(c->connected) { while(state == EClientState::STARTING) - boost::this_thread::sleep(boost::posix_time::milliseconds(10)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); CPack * pack = c->retrievePack(); if(state == EClientState::DISCONNECTING) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 51ea64e0f..f6a88cb54 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -461,7 +461,7 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); - boost::this_thread::sleep(boost::posix_time::millisec(timeToSleepMillisec)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); } return true; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 36c430272..3f1b402f4 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -407,7 +407,7 @@ void BattleWindow::bFleef() auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! //printing message - owner.curInt->showInfoDialog(boost::to_string(txt), comps); + owner.curInt->showInfoDialog(boost::str(txt), comps); } } diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 9df9de849..62614392d 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -36,7 +36,7 @@ CGuiHandler GH; -boost::thread_specific_ptr inGuiThread; +static thread_local bool inGuiThread = false; SObjectConstruction::SObjectConstruction(CIntObject *obj) :myObj(obj) @@ -69,6 +69,8 @@ SSetCaptureState::~SSetCaptureState() void CGuiHandler::init() { + inGuiThread = true; + inputHandlerInstance = std::make_unique(); eventDispatcherInstance = std::make_unique(); windowHandlerInstance = std::make_unique(); @@ -117,7 +119,7 @@ void CGuiHandler::renderFrame() bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate - boost::this_thread::sleep(boost::posix_time::milliseconds(1)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); if(acquiredTheLockOnPim) { @@ -207,7 +209,7 @@ void CGuiHandler::drawFPSCounter() bool CGuiHandler::amIGuiThread() { - return inGuiThread.get() && *inGuiThread; + return inGuiThread; } void CGuiHandler::dispatchMainThread(const std::function & functor) diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index fda8cb883..2bb0a3aae 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -38,7 +38,7 @@ void CMapHandler::waitForOngoingAnimations() while(CGI->mh->hasOngoingAnimations()) { auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); - boost::this_thread::sleep(boost::posix_time::milliseconds(1)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); } } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 0d5f248be..1ed8e0153 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -562,7 +562,7 @@ void CSpellWindow::SpellArea::hover(bool on) if(mySpell) { if(on) - owner->statusBar->write(boost::to_string(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->level])); + owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->level])); else owner->statusBar->clear(); } diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 595de23b3..c6f8f46dd 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -233,7 +233,7 @@ int CConsoleHandler::run() const (*cb)(buffer, false); } else - boost::this_thread::sleep(boost::posix_time::millisec(100)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); boost::this_thread::interruption_point(); #else diff --git a/lib/CRandomGenerator.cpp b/lib/CRandomGenerator.cpp index 4f896444b..657356411 100644 --- a/lib/CRandomGenerator.cpp +++ b/lib/CRandomGenerator.cpp @@ -13,8 +13,6 @@ VCMI_LIB_NAMESPACE_BEGIN -boost::thread_specific_ptr CRandomGenerator::defaultRand; - CRandomGenerator::CRandomGenerator() { resetSeed(); @@ -84,11 +82,8 @@ double CRandomGenerator::nextDouble() CRandomGenerator & CRandomGenerator::getDefault() { - if(!defaultRand.get()) - { - defaultRand.reset(new CRandomGenerator()); - } - return *defaultRand; + static thread_local CRandomGenerator defaultRand; + return defaultRand; } TGenerator & CRandomGenerator::getStdGenerator() diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 8e27b46fa..bfd75f9a7 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -80,7 +80,6 @@ public: private: TGenerator rand; - static boost::thread_specific_ptr defaultRand; public: template diff --git a/lib/CThreadHelper.cpp b/lib/CThreadHelper.cpp index e98a42274..eb5307c54 100644 --- a/lib/CThreadHelper.cpp +++ b/lib/CThreadHelper.cpp @@ -29,10 +29,12 @@ CThreadHelper::CThreadHelper(std::vector> * Tasks, int Thr } void CThreadHelper::run() { - boost::thread_group grupa; + std::vector group; for(int i=0;i group; for(size_t i=0; i payload = context.at(i); - - grupa.create_thread(std::bind(&ThreadPool::processTasks, this, payload)); + group.emplace_back(std::bind(&ThreadPool::processTasks, this, payload)); } - grupa.join_all(); + for (auto & thread : group) + thread.join(); //thread group deletes threads, do not free manually } diff --git a/lib/logging/CLogger.cpp b/lib/logging/CLogger.cpp index ad51153bd..2f9ccdf6f 100644 --- a/lib/logging/CLogger.cpp +++ b/lib/logging/CLogger.cpp @@ -288,7 +288,7 @@ std::string CLogFormatter::format(const LogRecord & record) const boost::algorithm::replace_first(message, "%m", record.message); boost::algorithm::replace_first(message, "%c", boost::posix_time::to_simple_string(record.timeStamp)); - //return boost::to_string (boost::format("%d %d %d[%d] - %d") % dateStream.str() % level % record.domain.getName() % record.threadId % record.message); + //return boost::str (boost::format("%d %d %d[%d] - %d") % dateStream.str() % level % record.domain.getName() % record.threadId % record.message); return message; } diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 27dfa1f58..1c93fd448 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -69,7 +69,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const ms.appendLocalString(EMetaText::GENERAL_TXT,243); break; default: //decision = cost in gold - ms.appendRawString(boost::to_string(boost::format(VLC->generaltexth->allTexts[244]) % decision)); + ms.appendRawString(boost::str(boost::format(VLC->generaltexth->allTexts[244]) % decision)); break; } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 9c65708a6..218dd4575 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1463,7 +1463,7 @@ bool CGHeroInstance::hasVisions(const CGObjectInstance * target, const int subty const int distance = static_cast(target->pos.dist2d(visitablePos())); - //logGlobal->debug(boost::to_string(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange)); + //logGlobal->debug(boost::str(boost::format("Visions: dist %d, mult %d, range %d") % distance % visionsMultiplier % visionsRange)); return (distance < visionsRange) && (target->pos.z == pos.z); } diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 30101ef2a..8ce615a14 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -421,7 +421,7 @@ int CMapGenerator::getNextMonlithIndex() while (true) { if (monolithIndex >= VLC->objtypeh->knownSubObjects(Obj::MONOLITH_TWO_WAY).size()) - throw rmgException(boost::to_string(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); + throw rmgException(boost::str(boost::format("There is no Monolith Two Way with index %d available!") % monolithIndex)); else { //Skip modded Monoliths which can't beplaced on every terrain diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 261477a9d..8c4580230 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -156,7 +156,7 @@ TRmgTemplateZoneId ZoneOptions::getId() const void ZoneOptions::setId(TRmgTemplateZoneId value) { if(value <= 0) - throw std::runtime_error(boost::to_string(boost::format("Zone %d id should be greater than 0.") % id)); + throw std::runtime_error(boost::str(boost::format("Zone %d id should be greater than 0.") % id)); id = value; } @@ -650,7 +650,7 @@ std::string CRmgTemplate::CPlayerCountRange::toString() const } else { - ret += boost::to_string(boost::format("%d-%d") % p.first % p.second); + ret += boost::str(boost::format("%d-%d") % p.first % p.second); } } diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index 0e6320291..ecb866cc8 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -219,7 +219,7 @@ const CMapGenOptions& RmgMap::getMapGenOptions() const void RmgMap::assertOnMap(const int3& tile) const { if (!mapInstance->isInTheMap(tile)) - throw rmgException(boost::to_string(boost::format("Tile %s is outside the map") % tile.toString())); + throw rmgException(boost::str(boost::format("Tile %s is outside the map") % tile.toString())); } RmgMap::Zones & RmgMap::getZones() @@ -354,7 +354,7 @@ bool RmgMap::isAllowedSpell(const SpellID & sid) const void RmgMap::dump(bool zoneId) const { static int id = 0; - std::ofstream out(boost::to_string(boost::format("zone_%d.txt") % id++)); + std::ofstream out(boost::str(boost::format("zone_%d.txt") % id++)); int levels = mapInstance->levels(); int width = mapInstance->width; int height = mapInstance->height; diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 540463948..1991b81a6 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -115,7 +115,7 @@ void Object::Instance::setAnyTemplate() { auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(); if(templates.empty()) - throw rmgException(boost::to_string(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); + throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); dObject.appearance = templates.front(); dAccessibleAreaCache.clear(); @@ -128,7 +128,7 @@ void Object::Instance::setTemplate(TerrainId terrain) if (templates.empty()) { auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); - throw rmgException(boost::to_string(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); } dObject.appearance = templates.front(); dAccessibleAreaCache.clear(); @@ -328,7 +328,7 @@ void rmg::Object::setGuardedIfMonster(const Instance& object) void Object::Instance::finalize(RmgMap & map) { if(!map.isOnMap(getPosition(true))) - throw rmgException(boost::to_string(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); + throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); //If no specific template was defined for this object, select any matching if (!dObject.appearance) @@ -337,7 +337,7 @@ void Object::Instance::finalize(RmgMap & map) auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrainType->getId()); if (templates.empty()) { - throw rmgException(boost::to_string(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); } else { @@ -346,12 +346,12 @@ void Object::Instance::finalize(RmgMap & map) } if (dObject.isVisitable() && !map.isOnMap(dObject.visitablePos())) - throw rmgException(boost::to_string(boost::format("Visitable tile %s of object %d at %s is outside the map") % dObject.visitablePos().toString() % dObject.id % dObject.pos.toString())); + throw rmgException(boost::str(boost::format("Visitable tile %s of object %d at %s is outside the map") % dObject.visitablePos().toString() % dObject.id % dObject.pos.toString())); for(const auto & tile : dObject.getBlockedPos()) { if(!map.isOnMap(tile)) - throw rmgException(boost::to_string(boost::format("Tile %s of object %d at %s is outside the map") % tile.toString() % dObject.id % dObject.pos.toString())); + throw rmgException(boost::str(boost::format("Tile %s of object %d at %s is outside the map") % tile.toString() % dObject.id % dObject.pos.toString())); } for(const auto & tile : getBlockedArea().getTilesVector()) diff --git a/lib/rmg/modificators/Modificator.cpp b/lib/rmg/modificators/Modificator.cpp index bfd03c664..9f2bfa004 100644 --- a/lib/rmg/modificators/Modificator.cpp +++ b/lib/rmg/modificators/Modificator.cpp @@ -119,7 +119,7 @@ void Modificator::postfunction(Modificator * modificator) void Modificator::dump() { - std::ofstream out(boost::to_string(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName())); + std::ofstream out(boost::str(boost::format("seed_%d_modzone_%d_%s.txt") % generator.getRandomSeed() % zone.getId() % getName())); int levels = map.levels(); int width = map.width(); int height = map.height(); diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 5ce043c42..3df3f1d03 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -387,7 +387,7 @@ void RiverPlacer::connectRiver(const int3 & tile) if(tmplates.size() > 3) { if(tmplates.size() % 4 != 0) - throw rmgException(boost::to_string(boost::format("River templates for (%d,%d) at terrain %s, river %s are incorrect") % + throw rmgException(boost::str(boost::format("River templates for (%d,%d) at terrain %s, river %s are incorrect") % RIVER_DELTA_ID % RIVER_DELTA_SUBTYPE % zone.getTerrainType() % river->shortIdentifier)); std::string targetTemplateName = river->deltaName + std::to_string(deltaOrientations[pos]) + ".def"; diff --git a/lib/rmg/threadpool/ThreadPool.h b/lib/rmg/threadpool/ThreadPool.h index 1de524c50..3fbb81612 100644 --- a/lib/rmg/threadpool/ThreadPool.h +++ b/lib/rmg/threadpool/ThreadPool.h @@ -169,7 +169,7 @@ inline void ThreadPool::cancel() auto ThreadPool::async(std::function&& f) const -> boost::future { - using TaskT = boost::packaged_task; + using TaskT = boost::packaged_task; { Lock lock(mx); @@ -189,4 +189,4 @@ auto ThreadPool::async(std::function&& f) const -> boost::future return fut; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 152ef5480..f36dd422e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -66,10 +66,6 @@ #include #include -#ifndef _MSC_VER -#include -#endif - #define COMPLAIN_RET_IF(cond, txt) do {if (cond){complain(txt); return;}} while(0) #define COMPLAIN_RET_FALSE_IF(cond, txt) do {if (cond){complain(txt); return false;}} while(0) #define COMPLAIN_RET(txt) {complain(txt); return false;} @@ -973,7 +969,6 @@ void CGameHandler::run(bool resume) { LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); - using namespace boost::posix_time; for (auto cc : lobby->connections) { auto players = lobby->getAllClientPlayers(cc->connectionID); @@ -1068,8 +1063,7 @@ void CGameHandler::run(bool resume) if(gs->curB) turnTimerHandler.onBattleLoop(waitTime); - static time_duration p = milliseconds(waitTime); - states.cv.timed_wait(lock, p); + states.cv.wait_for(lock, boost::chrono::milliseconds(waitTime)); } } } @@ -2575,7 +2569,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI const CArmedInstance * obj = static_cast(getObjInstance(objid)); if (!obj->hasStackAtSlot(pos)) { - COMPLAIN_RET("Cannot upgrade, no stack at slot " + boost::to_string(pos)); + COMPLAIN_RET("Cannot upgrade, no stack at slot " + std::to_string(pos)); } UpgradeInfo ui; fillUpgradeInfo(obj, pos, ui); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 4028ecc2d..9ff621551 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -179,7 +179,7 @@ void CVCMIServer::run() } while(state == EServerState::LOBBY || state == EServerState::GAMEPLAY_STARTING) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); logNetwork->info("Thread handling connections ended"); @@ -188,14 +188,14 @@ void CVCMIServer::run() gh->run(si->mode == StartInfo::LOAD_GAME); } while(state == EServerState::GAMEPLAY_ENDED) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } void CVCMIServer::establishRemoteConnections() { //wait for host connection while(connections.empty()) - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); uuid = cmdLineOptions["lobby-uuid"].as(); int numOfConnections = cmdLineOptions["connections"].as(); @@ -246,7 +246,7 @@ void CVCMIServer::threadAnnounceLobby() } } - boost::this_thread::sleep(boost::posix_time::milliseconds(50)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); } } @@ -265,7 +265,7 @@ void CVCMIServer::prepareToRestart() campaignBonus = si->campState->getBonusID(campaignMap).value_or(-1); } // FIXME: dirry hack to make sure old CGameHandler::run is finished - boost::this_thread::sleep(boost::posix_time::milliseconds(1000)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } for(auto c : connections) diff --git a/server/StdInc.h b/server/StdInc.h index fbf8bbce5..d26d2298c 100644 --- a/server/StdInc.h +++ b/server/StdInc.h @@ -11,11 +11,4 @@ #include "../Global.h" -#include -#include //no i/o just types -#include -#include -#include -#include - VCMI_LIB_USING_NAMESPACE diff --git a/test/JsonComparer.cpp b/test/JsonComparer.cpp index 850e7425a..99c3c0c99 100644 --- a/test/JsonComparer.cpp +++ b/test/JsonComparer.cpp @@ -116,7 +116,7 @@ void JsonComparer::checkEqualJson(const JsonVector & actual, const JsonVector & for(size_t idx = 0; idx < sz; idx ++) { - auto guard = pushName(boost::to_string(idx)); + auto guard = pushName(std::to_string(idx)); checkEqualJson(actual.at(idx), expected.at(idx)); } diff --git a/test/map/MapComparer.cpp b/test/map/MapComparer.cpp index 317e89fe5..cfc908ead 100644 --- a/test/map/MapComparer.cpp +++ b/test/map/MapComparer.cpp @@ -56,7 +56,7 @@ void checkEqual(const std::set & actual, const std::set & expe for(auto elem : expected) { if(!vstd::contains(actual, elem)) - FAIL() << "Required element not found "+boost::to_string(elem); + FAIL() << "Required element not found "+std::to_string(elem); } } @@ -202,8 +202,8 @@ void MapComparer::compareObject(const CGObjectInstance * actual, const CGObjectI EXPECT_EQ(actual->instanceName, expected->instanceName); EXPECT_EQ(typeid(actual).name(), typeid(expected).name());//todo: remove and use just comparison - std::string actualFullID = boost::to_string(boost::format("%s(%d)|%s(%d) %d") % actual->typeName % actual->ID % actual->subTypeName % actual->subID % actual->tempOwner); - std::string expectedFullID = boost::to_string(boost::format("%s(%d)|%s(%d) %d") % expected->typeName % expected->ID % expected->subTypeName % expected->subID % expected->tempOwner); + std::string actualFullID = boost::str(boost::format("%s(%d)|%s(%d) %d") % actual->typeName % actual->ID % actual->subTypeName % actual->subID % actual->tempOwner); + std::string expectedFullID = boost::str(boost::format("%s(%d)|%s(%d) %d") % expected->typeName % expected->ID % expected->subTypeName % expected->subID % expected->tempOwner); EXPECT_EQ(actualFullID, expectedFullID); From dfe8ca3d6135b5e103393be5d53323c22d84a9cf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 00:53:51 +0300 Subject: [PATCH 0141/1248] Use more lightweight scoped_lock --- client/CMusicHandler.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 521d47eb1..681a02c36 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -238,7 +238,7 @@ void CSoundHandler::setChannelVolume(int channel, ui32 percent) void CSoundHandler::setCallback(int channel, std::function function) { - boost::unique_lock lockGuard(mutexCallbacks); + boost::mutex::scoped_lock lockGuard(mutexCallbacks); auto iter = callbacks.find(channel); @@ -251,7 +251,7 @@ void CSoundHandler::setCallback(int channel, std::function function) void CSoundHandler::soundFinishedCallback(int channel) { - boost::unique_lock lockGuard(mutexCallbacks); + boost::mutex::scoped_lock lockGuard(mutexCallbacks); if (callbacks.count(channel) == 0) return; @@ -272,14 +272,14 @@ void CSoundHandler::soundFinishedCallback(int channel) void CSoundHandler::initCallback(int channel) { - boost::unique_lock lockGuard(mutexCallbacks); + boost::mutex::scoped_lock lockGuard(mutexCallbacks); assert(callbacks.count(channel) == 0); callbacks[channel] = {}; } void CSoundHandler::initCallback(int channel, const std::function & function) { - boost::unique_lock lockGuard(mutexCallbacks); + boost::mutex::scoped_lock lockGuard(mutexCallbacks); assert(callbacks.count(channel) == 0); callbacks[channel].push_back(function); } From 0613756abc6d4369446504475b4589ce7d493cd0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 00:54:40 +0300 Subject: [PATCH 0142/1248] Remove unused code --- client/CPlayerInterface.cpp | 2 -- client/CServerHandler.cpp | 1 - client/eventsSDL/InputSourceKeyboard.cpp | 8 -------- client/gui/CGuiHandler.cpp | 20 ++------------------ client/gui/CGuiHandler.h | 2 -- client/mainmenu/CMainMenu.cpp | 1 - 6 files changed, 2 insertions(+), 32 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 73095a506..89a4a21a2 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -136,7 +136,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): makingTurn = false; showingDialog = new CondSh(false); cingconsole = new CInGameConsole(); - GH.terminate_cond->set(false); firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; @@ -1592,7 +1591,6 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul { if(adventureInt) { - GH.terminate_cond->setn(true); GH.windows().popWindows(GH.windows().count()); adventureInt.reset(); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 86bce71b4..37bac9d74 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -630,7 +630,6 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) { if(CMM) { - GH.terminate_cond->setn(false); GH.curInt = CMM.get(); CMM->enable(); } diff --git a/client/eventsSDL/InputSourceKeyboard.cpp b/client/eventsSDL/InputSourceKeyboard.cpp index 618372aba..01b7c990e 100644 --- a/client/eventsSDL/InputSourceKeyboard.cpp +++ b/client/eventsSDL/InputSourceKeyboard.cpp @@ -59,14 +59,6 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key) Settings s = settings.write["session"]; switch(key.keysym.sym) { - case SDLK_F5: - if(settings["session"]["spectate-locked-pim"].Bool()) - CPlayerInterface::pim->unlock(); - else - CPlayerInterface::pim->lock(); - s["spectate-locked-pim"].Bool() = !settings["session"]["spectate-locked-pim"].Bool(); - break; - case SDLK_F6: s["spectate-ignore-hero"].Bool() = !settings["session"]["spectate-ignore-hero"].Bool(); break; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 62614392d..c6bb020fd 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -111,20 +111,8 @@ void CGuiHandler::stopTextInput() void CGuiHandler::renderFrame() { - // Updating GUI requires locking pim mutex (that protects screen and GUI state). - // During game: - // When ending the game, the pim mutex might be hold by other thread, - // that will notify us about the ending game by setting terminate_cond flag. - //in PreGame terminate_cond stay false - - bool acquiredTheLockOnPim = false; //for tracking whether pim mutex locking succeeded - while(!terminate_cond->get() && !(acquiredTheLockOnPim = CPlayerInterface::pim->try_lock())) //try acquiring long until it succeeds or we are told to terminate - boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); - - if(acquiredTheLockOnPim) { - // If we are here, pim mutex has been successfully locked - let's store it in a safe RAII lock. - boost::unique_lock un(*CPlayerInterface::pim, boost::adopt_lock); + boost::recursive_mutex::scoped_lock un(*CPlayerInterface::pim); if(nullptr != curInt) curInt->update(); @@ -152,14 +140,10 @@ CGuiHandler::CGuiHandler() , captureChildren(false) , curInt(nullptr) , fakeStatusBar(std::make_shared()) - , terminate_cond (new CondSh(false)) { } -CGuiHandler::~CGuiHandler() -{ - delete terminate_cond; -} +CGuiHandler::~CGuiHandler() = default; ShortcutHandler & CGuiHandler::shortcuts() { diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index b88aaadb0..e11124bcf 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -99,8 +99,6 @@ public: /// Calls provided functor in main thread on next execution frame void dispatchMainThread(const std::function & functor); - - CondSh * terminate_cond; // confirm termination }; extern CGuiHandler GH; //global gui handler diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 407dc66eb..9580d91c8 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -392,7 +392,6 @@ std::shared_ptr CMainMenu::create() if(!CMM) CMM = std::shared_ptr(new CMainMenu()); - GH.terminate_cond->setn(false); return CMM; } From a44c08a847f3cfafa0156f31f43740b95b90af8a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 00:55:11 +0300 Subject: [PATCH 0143/1248] Slightly better thread names --- client/CServerHandler.cpp | 6 ++++-- lib/CConsoleHandler.cpp | 2 +- server/CVCMIServer.cpp | 5 ++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 37bac9d74..8a4051fed 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -574,6 +574,8 @@ void CServerHandler::startMapAfterConnection(std::shared_ptr to) void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState) { + setThreadName("startGameplay"); + if(CMM) CMM->disable(); client = new CClient(); @@ -816,7 +818,7 @@ public: void CServerHandler::threadHandleConnection() { - setThreadName("threadHandleConnection"); + setThreadName("handleConnection"); c->enterLobbyConnectionMode(); try @@ -897,7 +899,7 @@ void CServerHandler::visitForClient(CPackForClient & clientPack) void CServerHandler::threadRunServer() { #if !defined(VCMI_MOBILE) - setThreadName("threadRunServer"); + setThreadName("runServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() + " --port=" + std::to_string(getHostPort()) diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index c6f8f46dd..930e6b2ae 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -214,7 +214,7 @@ void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) int CConsoleHandler::run() const { - setThreadName("CConsoleHandler::run"); + setThreadName("consoleHandler"); //disabling sync to make in_avail() work (othervice always returns 0) { TLockGuard _(smx); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 9ff621551..1ebc31679 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -193,6 +193,8 @@ void CVCMIServer::run() void CVCMIServer::establishRemoteConnections() { + setThreadName("establishConnection"); + //wait for host connection while(connections.empty()) boost::this_thread::sleep_for(boost::chrono::milliseconds(50)); @@ -229,6 +231,7 @@ void CVCMIServer::connectToRemote(const std::string & addr, int port) void CVCMIServer::threadAnnounceLobby() { + setThreadName("announceLobby"); while(state != EServerState::SHUTDOWN) { { @@ -419,7 +422,7 @@ public: void CVCMIServer::threadHandleClient(std::shared_ptr c) { - setThreadName("CVCMIServer::handleConnection"); + setThreadName("handleClient"); c->enterLobbyConnectionMode(); while(c->connected) From 3eb19e6ed7421494f64bc13e0a9382d2b653c13e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 21 Aug 2023 01:35:05 +0300 Subject: [PATCH 0144/1248] Fix possible unprotected access to battleint --- client/battle/BattleInterface.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index dc1ff5161..93842e95b 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -105,11 +105,9 @@ void BattleInterface::playIntroSoundAndUnlockInterface() { auto onIntroPlayed = [this]() { + boost::unique_lock un(*CPlayerInterface::pim); if(LOCPLINT->battleInt) - { - boost::unique_lock un(*CPlayerInterface::pim); onIntroSoundPlayed(); - } }; int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); From c9bc3fd4e188cc343c70d56434f988eec9e6d5a1 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 19 Aug 2023 16:58:37 +0300 Subject: [PATCH 0145/1248] CSpellHandler: use getBonusBearer Use getBonusBearer method and cachingStr method for optimization --- lib/spells/CSpellHandler.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 2087478c9..c53af381f 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -383,7 +383,7 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni //affected creature-specific part if(nullptr != affectedCreature) { - const auto * bearer = affectedCreature; + const auto * bearer = affectedCreature->getBonusBearer(); //applying protections - when spell has more then one elements, only one protection should be applied (I think) forEachSchool([&](const spells::SchoolInfo & cnf, bool & stop) { @@ -396,11 +396,12 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni }); CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)); + auto cachingStr = "type_SPELL_DAMAGE_REDUCTION_s_ANY"; //general spell dmg reduction, works only on magical effects - if(bearer->hasBonus(selector) && isMagical()) + if(bearer->hasBonus(selector, cachingStr) && isMagical()) { - ret *= 100 - bearer->valOfBonuses(selector); + ret *= 100 - bearer->valOfBonuses(selector, cachingStr); ret /= 100; } From 8724181a0f9d4fecffecaa38634bdee95625ecd9 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 19 Aug 2023 20:19:59 +0300 Subject: [PATCH 0146/1248] vcmi: spell resistance rework Now instead of XXX_IMMUNITY bonuses we have 2 bonuses with spellSchool subtype: SPELL_SCHOOL_IMMUNITY and NEGATIVE_EFFECT_IMMUNITY. All previous bonuses of subtype 0 is covered by SPELL_SCHOOL_IMMUNITY, and all previous bonuses of subtype 1 is covered by NEGATIVE_EFFECT_IMMUNITY. Unit tests are updated accordingly. --- client/windows/CSpellWindow.cpp | 4 +- include/vcmi/spells/Spell.h | 3 +- lib/CBonusTypeHandler.cpp | 75 ++++++++----------- lib/CCreatureHandler.cpp | 24 +++--- lib/bonuses/BonusEnum.h | 6 +- lib/mapObjects/CGHeroInstance.cpp | 14 ++-- lib/spells/CSpellHandler.cpp | 14 ++-- lib/spells/CSpellHandler.h | 3 +- lib/spells/ISpellMechanics.cpp | 12 --- lib/spells/ISpellMechanics.h | 4 - lib/spells/TargetCondition.cpp | 18 +++-- lib/spells/effects/Damage.cpp | 4 +- server/battles/BattleActionProcessor.cpp | 4 +- test/mock/mock_spells_Mechanics.h | 2 - .../targetConditions/BonusConditionTest.cpp | 2 +- .../ElementalConditionTest.cpp | 28 ++++--- .../TargetConditionItemFixture.h | 3 + 17 files changed, 99 insertions(+), 121 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 1ed8e0153..9184b58ab 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -122,9 +122,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m ++sitesPerOurTab[4]; - spell->forEachSchool([&sitesPerOurTab](const spells::SchoolInfo & school, bool & stop) + spell->forEachSchool([&sitesPerOurTab](const ESpellSchool & school, bool & stop) { - ++sitesPerOurTab[(ui8)school.id]; + ++sitesPerOurTab[(ui8)school]; }); } if(sitesPerTabAdv[4] % 12 == 0) diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index ba16ad105..4b3eb0d27 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -19,13 +19,12 @@ enum class ESpellSchool: int8_t; namespace spells { -struct SchoolInfo; class Caster; class DLL_LINKAGE Spell: public EntityT { public: - using SchoolCallback = std::function; + using SchoolCallback = std::function; ///calculate spell damage on stack taking caster`s secondary skills into account virtual int64_t calculateDamage(const Caster * caster) const = 0; diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 4d5479dd2..1810cdc9b 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -99,60 +99,47 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo fileName = sp->getIconImmune(); break; } - case BonusType::FIRE_IMMUNITY: + case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school + { switch(bonus->subtype) { - case 0: - fileName = "E_SPFIRE.bmp"; - break;//all - case 1: - fileName = "E_SPFIRE1.bmp"; - break;//not positive - case 2: - fileName = "E_FIRE.bmp"; - break;//direct damage - } - break; - case BonusType::WATER_IMMUNITY: - switch(bonus->subtype) - { - case 0: - fileName = "E_SPWATER.bmp"; - break;//all - case 1: - fileName = "E_SPWATER1.bmp"; - break;//not positive - case 2: - fileName = "E_SPCOLD.bmp"; - break;//direct damage - } - break; - case BonusType::AIR_IMMUNITY: - switch(bonus->subtype) - { - case 0: + case SpellSchool(ESpellSchool::AIR): fileName = "E_SPAIR.bmp"; - break;//all - case 1: - fileName = "E_SPAIR1.bmp"; - break;//not positive - case 2: - fileName = "E_LIGHT.bmp"; - break;//direct damage + break; + case SpellSchool(ESpellSchool::FIRE): + fileName = "E_SPFIRE.bmp"; + break; + case SpellSchool(ESpellSchool::WATER): + fileName = "E_SPWATER.bmp"; + break; + case SpellSchool(ESpellSchool::EARTH): + fileName = "E_SPEATH.bmp"; + break; } break; - case BonusType::EARTH_IMMUNITY: + } + // fileName = "E_FIRE.bmp"; //fire damage + // fileName = "E_COLD.bmp"; //cold damage + // fileName = "E_LIGHT.bmp"; //lightning damage + case BonusType::NEGATIVE_EFFECTS_IMMUNITY: + { switch(bonus->subtype) { - case 0: - fileName = "E_SPEATH.bmp"; - break;//all - case 1: - case 2://no specific icon for direct damage immunity + case SpellSchool(ESpellSchool::AIR): + fileName = "E_SPAIR1.bmp"; + break; + case SpellSchool(ESpellSchool::FIRE): + fileName = "E_SPFIRE1.bmp"; + break; + case SpellSchool(ESpellSchool::WATER): + fileName = "E_SPWATER1.bmp"; + break; + case SpellSchool(ESpellSchool::EARTH): fileName = "E_SPEATH1.bmp"; - break;//not positive + break; } break; + } case BonusType::LEVEL_SPELL_IMMUNITY: { if(vstd::iswithin(bonus->val, 1, 5)) diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 1e3802408..9640eb199 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -1181,8 +1181,8 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? break; case 'F': - b.type = BonusType::FIRE_IMMUNITY; - b.subtype = 1; //not positive + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::FIRE); break; case 'O': b.type = BonusType::SPELL_DAMAGE_REDUCTION; @@ -1190,12 +1190,12 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.val = 100; //Full damage immunity break; case 'f': - b.type = BonusType::FIRE_IMMUNITY; - b.subtype = 0; //all + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::FIRE); break; case 'C': - b.type = BonusType::WATER_IMMUNITY; - b.subtype = 1; //not positive + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::WATER); break; case 'W': b.type = BonusType::SPELL_DAMAGE_REDUCTION; @@ -1203,8 +1203,8 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.val = 100; //Full damage immunity break; case 'w': - b.type = BonusType::WATER_IMMUNITY; - b.subtype = 0; //all + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::WATER); break; case 'E': b.type = BonusType::SPELL_DAMAGE_REDUCTION; @@ -1212,8 +1212,8 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.val = 100; //Full damage immunity break; case 'e': - b.type = BonusType::EARTH_IMMUNITY; - b.subtype = 0; //all + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::EARTH); break; case 'A': b.type = BonusType::SPELL_DAMAGE_REDUCTION; @@ -1221,8 +1221,8 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.val = 100; //Full damage immunity break; case 'a': - b.type = BonusType::AIR_IMMUNITY; - b.subtype = 0; //all + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::AIR); break; case 'D': b.type = BonusType::SPELL_DAMAGE_REDUCTION; diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 8d43a48ac..7f1489e79 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -74,10 +74,6 @@ class JsonNode; BONUS_NAME(SPELL_LIKE_ATTACK) /*subtype - spell, value - spell level; range is taken from spell, but damage from creature; eg. magog*/ \ BONUS_NAME(THREE_HEADED_ATTACK) /*eg. cerberus*/ \ BONUS_NAME(GENERAL_DAMAGE_PREMY) \ - BONUS_NAME(FIRE_IMMUNITY) /*subtype 0 - all, 1 - all except positive*/ \ - BONUS_NAME(WATER_IMMUNITY) \ - BONUS_NAME(EARTH_IMMUNITY) \ - BONUS_NAME(AIR_IMMUNITY) \ BONUS_NAME(MIND_IMMUNITY) \ BONUS_NAME(FIRE_SHIELD) \ BONUS_NAME(UNDEAD) \ @@ -175,6 +171,8 @@ class JsonNode; BONUS_NAME(BONUS_DAMAGE_PERCENTAGE) /*If hero can grant conditional damage to creature, value is percentage, subtype is creatureID*/\ BONUS_NAME(BONUS_DAMAGE_CHANCE) /*If hero can grant additional damage to creature, value is chance, subtype is creatureID*/\ BONUS_NAME(MAX_LEARNABLE_SPELL_LEVEL) /*This can work as wisdom before. val = max learnable spell level*/\ + BONUS_NAME(SPELL_SCHOOL_IMMUNITY) /*This bonus will work as spell school immunity for all spells, subtype - spell school: 0 - air, 1 - fire, 2 - water, 3 - earth. Any is not handled for reducing overlap from LEVEL_SPELL_IMMUNITY*/\ + BONUS_NAME(NEGATIVE_EFFECTS_IMMUNITY) /*This bonus will work as spell school immunity for negative effects from spells of school, subtype - spell school: -1 - any, 0 - air, 1 - fire, 2 - water, 3 - earth*/\ /* end of list */ diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 218dd4575..81ec80015 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -620,14 +620,14 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t { int32_t skill = -1; //skill level - spell->forEachSchool([&, this](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([&, this](const ESpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, cnf.id); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; if(outSelectedSchool) - *outSelectedSchool = static_cast(cnf.id); + *outSelectedSchool = SpellSchool(cnf); } }); @@ -650,9 +650,9 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, int maxSchoolBonus = 0; - spell->forEachSchool([&maxSchoolBonus, this](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([&maxSchoolBonus, this](const ESpellSchool & cnf, bool & stop) { - vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, cnf.id)); + vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, SpellSchool(cnf))); }); base = static_cast(base * (100 + maxSchoolBonus) / 100.0); @@ -739,9 +739,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const bool schoolBonus = false; - spell->forEachSchool([this, &schoolBonus](const spells::SchoolInfo & cnf, bool & stop) + spell->forEachSchool([this, &schoolBonus](const ESpellSchool & cnf, bool & stop) { - if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, cnf.id)) + if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, SpellSchool(cnf))) { schoolBonus = stop = true; } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index c53af381f..085f95df9 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -43,22 +43,18 @@ const spells::SchoolInfo SCHOOL[4] = { { ESpellSchool::AIR, - BonusType::AIR_IMMUNITY, "air" }, { ESpellSchool::FIRE, - BonusType::FIRE_IMMUNITY, "fire" }, { ESpellSchool::WATER, - BonusType::WATER_IMMUNITY, "water" }, { ESpellSchool::EARTH, - BonusType::EARTH_IMMUNITY, "earth" } }; @@ -153,7 +149,7 @@ spells::AimType CSpell::getTargetType() const return targetType; } -void CSpell::forEachSchool(const std::function& cb) const +void CSpell::forEachSchool(const std::function& cb) const { bool stop = false; for(auto iter : SpellConfig::SCHOOL_ORDER) @@ -161,7 +157,7 @@ void CSpell::forEachSchool(const std::functiongetBonusBearer(); //applying protections - when spell has more then one elements, only one protection should be applied (I think) - forEachSchool([&](const spells::SchoolInfo & cnf, bool & stop) + forEachSchool([&](const ESpellSchool & cnf, bool & stop) { - if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id)) + if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf))) { - ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id); + ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf)); ret /= 100; stop = true; //only bonus from one school is used } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 769a3ebb1..ff0ba6998 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -44,7 +44,6 @@ class IBattleCast; struct SchoolInfo { SpellSchool id; //backlink - BonusType immunityBonus; std::string jsonName; }; @@ -216,7 +215,7 @@ public: * * Set stop to true to abort looping */ - void forEachSchool(const std::function & cb) const override; + void forEachSchool(const std::function & cb) const override; spells::AimType getTargetType() const; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index f8709b882..003b3dc6a 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -620,18 +620,6 @@ int64_t BaseMechanics::calculateRawEffectValue(int32_t basePowerMultiplier, int3 return owner->calculateRawEffectValue(getEffectLevel(), basePowerMultiplier, levelPowerMultiplier); } -std::vector BaseMechanics::getElementalImmunity() const -{ - std::vector ret; - - owner->forEachSchool([&](const SchoolInfo & cnf, bool & stop) - { - ret.push_back(cnf.immunityBonus); - }); - - return ret; -} - bool BaseMechanics::ownerMatches(const battle::Unit * unit) const { return ownerMatches(unit, owner->getPositiveness()); diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index e540bf20d..efbbc53b8 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -235,8 +235,6 @@ public: virtual int64_t applySpecificSpellBonus(int64_t value) const = 0; virtual int64_t calculateRawEffectValue(int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const = 0; - virtual std::vector getElementalImmunity() const = 0; - //Battle facade virtual bool ownerMatches(const battle::Unit * unit) const = 0; virtual bool ownerMatches(const battle::Unit * unit, const boost::logic::tribool positivness) const = 0; @@ -296,8 +294,6 @@ public: int64_t applySpecificSpellBonus(int64_t value) const override; int64_t calculateRawEffectValue(int32_t basePowerMultiplier, int32_t levelPowerMultiplier) const override; - std::vector getElementalImmunity() const override; - bool ownerMatches(const battle::Unit * unit) const override; bool ownerMatches(const battle::Unit * unit, const boost::logic::tribool positivness) const override; diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index ab495ba60..05d9cb39a 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -23,6 +23,8 @@ #include "../serializer/JsonSerializeFormat.h" #include "../VCMI_Lib.h" +#include + VCMI_LIB_NAMESPACE_BEGIN @@ -173,25 +175,25 @@ protected: bool check(const Mechanics * m, const battle::Unit * target) const override { bool elementalImmune = false; + auto bearer = target->getBonusBearer(); - auto filter = m->getElementalImmunity(); - - for(auto element : filter) + m->getSpell()->forEachSchool([&](const ESpellSchool & cnf, bool & stop) { - if(target->hasBonusOfType(element, 0)) //always resist if immune to all spells altogether + if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, SpellSchool(cnf))) { elementalImmune = true; - break; + stop = true; //only bonus from one school is used } else if(!m->isPositiveSpell()) //negative or indifferent { - if(target->hasBonusOfType(element, 1)) + if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, SpellSchool(cnf))) { elementalImmune = true; - break; + stop = true; //only bonus from one school is used } } - } + }); + return elementalImmune; } }; diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 93295f93e..fb068a860 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -87,9 +87,9 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)) >= 100); //General spell damage immunity //elemental immunity for damage - m->getSpell()->forEachSchool([&](const SchoolInfo & cnf, bool & stop) + m->getSpell()->forEachSchool([&](const ESpellSchool & cnf, bool & stop) { - isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf.id) >= 100); //100% reduction is immunity + isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf)) >= 100); //100% reduction is immunity }); return !isImmune; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index e32d082f3..e280673e6 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1331,7 +1331,9 @@ int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::share if(!bat.shot() && !def->isClone() && def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::FIRE_IMMUNITY) && + !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, SpellSchool(ESpellSchool::FIRE)) && + !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, SpellSchool(ESpellSchool::FIRE)) && + attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::FIRE)) < 100 && CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) ) { diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index e3efe9950..10b4b2a57 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -64,8 +64,6 @@ public: MOCK_CONST_METHOD1(applySpecificSpellBonus,int64_t(int64_t)); MOCK_CONST_METHOD2(calculateRawEffectValue, int64_t(int32_t, int32_t)); - MOCK_CONST_METHOD0(getElementalImmunity, std::vector()); - MOCK_CONST_METHOD1(ownerMatches, bool(const battle::Unit *)); MOCK_CONST_METHOD2(ownerMatches, bool(const battle::Unit *, const boost::logic::tribool)); diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 402e38e1d..91fa9b411 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -49,7 +49,7 @@ TEST_F(BonusConditionTest, ReceptiveIfMatchesType) TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::FIRE_IMMUNITY, BonusSource::OTHER, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, SpellSchool(ESpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 0b7fd5262..f57b92d34 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -20,18 +20,20 @@ class ElementalConditionTest : public TargetConditionItemTest, public WithParamI { public: bool isPositive; + void setDefaultExpectations() { EXPECT_CALL(unitMock, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); - std::vector immunityList = + EXPECT_CALL(mechanicsMock, getSpell()).Times(AtLeast(1)).WillRepeatedly(Return(&spellMock)); + EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { - BonusType::AIR_IMMUNITY, - BonusType::FIRE_IMMUNITY, - }; + bool stop = false; + cb(ESpellSchool::AIR, stop); + cb(ESpellSchool::FIRE, stop); + }); - EXPECT_CALL(mechanicsMock, getElementalImmunity()).Times(AtLeast(1)).WillRepeatedly(Return(immunityList)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); } @@ -54,15 +56,23 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } +TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) +{ + setDefaultExpectations(); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::WATER))); + + EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); +} + TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -70,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 0)); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::AIR_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/TargetConditionItemFixture.h b/test/spells/targetConditions/TargetConditionItemFixture.h index b67714eda..060731fb5 100644 --- a/test/spells/targetConditions/TargetConditionItemFixture.h +++ b/test/spells/targetConditions/TargetConditionItemFixture.h @@ -17,6 +17,7 @@ #include "mock/mock_spells_Mechanics.h" +#include "mock/mock_spells_Spell.h" #include "mock/mock_BonusBearer.h" #include "mock/mock_battle_Unit.h" @@ -30,6 +31,8 @@ public: ::testing::StrictMock mechanicsMock; ::testing::StrictMock unitMock; + ::testing::StrictMock spellMock; + BonusBearerMock unitBonuses; protected: void SetUp() override From 4d67a7e3d1c8df9c49f8e1d6ea1913b1cd80c416 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 19 Aug 2023 20:27:55 +0300 Subject: [PATCH 0147/1248] BonusParams: add XXX_IMMUNITY to deprecated This bonuses does not exist now. --- lib/bonuses/BonusParams.cpp | 71 ++++++++++++++++++++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index 64c5a2eeb..fe2daa6c6 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" +#include "BonusEnum.h" #include "BonusParams.h" #include "BonusSelector.h" @@ -42,7 +43,11 @@ const std::set deprecatedBonusSet = { "FIRE_SPELLS", "AIR_SPELLS", "WATER_SPELLS", - "EARTH_SPELLS" + "EARTH_SPELLS", + "FIRE_IMMUNITY", + "AIR_IMMUNITY", + "WATER_IMMUNITY", + "EARTH_IMMUNITY" }; BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSubtypeStr, int deprecatedSubtype): @@ -261,6 +266,70 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu type = BonusType::SPELLS_OF_SCHOOL; subtype = SpellSchool(ESpellSchool::EARTH); } + else if (deprecatedTypeStr == "AIR_IMMUNITY") + { + subtype = SpellSchool(ESpellSchool::AIR); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "FIRE_IMMUNITY") + { + subtype = SpellSchool(ESpellSchool::FIRE); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "WATER_IMMUNITY") + { + subtype = SpellSchool(ESpellSchool::WATER); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } + else if (deprecatedTypeStr == "EARTH_IMMUNITY") + { + subtype = SpellSchool(ESpellSchool::EARTH); + switch(deprecatedSubtype) + { + case 0: + type = BonusType::SPELL_SCHOOL_IMMUNITY; + break; + case 1: + type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + break; + default: + type = BonusType::SPELL_DAMAGE_REDUCTION; + val = 100; + } + } else isConverted = false; } From 344593e8915c3230b663e065569fc65f93d0235d Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 19 Aug 2023 21:13:57 +0300 Subject: [PATCH 0148/1248] vcmi: made some CSpell properties private There are getters for it. --- client/battle/BattleStacksController.cpp | 11 ++++++++++- client/windows/CSpellWindow.cpp | 10 +++++----- lib/gameState/CGameState.cpp | 4 ++-- lib/rmg/modificators/TreasurePlacer.cpp | 4 ++-- lib/spells/CSpellHandler.h | 13 ++++++------- lib/spells/ISpellMechanics.cpp | 2 +- server/CGameHandler.cpp | 4 ++-- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 0a2145057..f83e97782 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -271,7 +271,16 @@ std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * int effectsPositivness = 0; for(const auto & spellID : activeSpells) - effectsPositivness += CGI->spellh->objects.at(spellID)->positiveness; + { + auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness(); + if(!boost::logic::indeterminate(positiveness)) + { + if(positiveness) + effectsPositivness++; + else + effectsPositivness--; + } + } if (effectsPositivness > 0) return amountPositive; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 9184b58ab..7e9e7b8b1 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -74,9 +74,9 @@ class SpellbookSpellSorter public: bool operator()(const CSpell * A, const CSpell * B) { - if(A->level < B->level) + if(A->getLevel() < B->getLevel()) return true; - if(A->level > B->level) + if(A->getLevel() > B->getLevel()) return false; @@ -562,7 +562,7 @@ void CSpellWindow::SpellArea::hover(bool on) if(mySpell) { if(on) - owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->level])); + owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->getLevel()])); else owner->statusBar->clear(); } @@ -609,12 +609,12 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) if(schoolLevel > 0) { boost::format fmt("%s/%s"); - fmt % CGI->generaltexth->allTexts[171 + mySpell->level]; + fmt % CGI->generaltexth->allTexts[171 + mySpell->getLevel()]; fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 level->setText(fmt.str()); } else - level->setText(CGI->generaltexth->allTexts[171 + mySpell->level]); + level->setText(CGI->generaltexth->allTexts[171 + mySpell->getLevel()]); cost->color = secondLineColor; boost::format costfmt("%s: %d"); diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 898243378..54342be77 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1082,7 +1082,7 @@ void CGameState::initTowns() for(ui32 z=0; zobligatorySpells.size();z++) { const auto * s = vti->obligatorySpells[z].toSpell(); - vti->spells[s->level-1].push_back(s->id); + vti->spells[s->getLevel()-1].push_back(s->id); vti->possibleSpells -= s->id; } while(!vti->possibleSpells.empty()) @@ -1110,7 +1110,7 @@ void CGameState::initTowns() sel=0; const auto * s = vti->possibleSpells[sel].toSpell(); - vti->spells[s->level-1].push_back(s->id); + vti->spells[s->getLevel()-1].push_back(s->id); vti->possibleSpells -= s->id; } vti->possibleSpells.clear(); diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 4e84dc882..3ba86e19f 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -213,7 +213,7 @@ void TreasurePlacer::addAllPossibleObjects() for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - if(map.isAllowedSpell(spell->id) && spell->level == i + 1) + if(map.isAllowedSpell(spell->id) && spell->getLevel() == i + 1) { out.push_back(spell->id); } @@ -328,7 +328,7 @@ void TreasurePlacer::addAllPossibleObjects() std::vector spells; for(auto spell : VLC->spellh->objects) { - if(map.isAllowedSpell(spell->id) && spell->level == i) + if(map.isAllowedSpell(spell->id) && spell->getLevel() == i) spells.push_back(spell); } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index ff0ba6998..1fd48d602 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -187,17 +187,10 @@ public: using BTVector = std::vector; - si32 level; std::map school; - - si32 power; //spell's power - std::map probabilities; //% chance to gain for castles - bool combat; //is this spell combat (true) or adventure (false) - bool creatureAbility; //if true, only creatures can use this spell - si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative bool onlyOnWaterMap; //Spell will be banned on maps without water std::vector counteredSpells; //spells that are removed when effect of this spell is placed on creature (for bless-curse, haste-slow, and similar pairs) @@ -364,6 +357,12 @@ private: std::vector levels; + si32 level; + si32 power; //spell's power + bool combat; //is this spell combat (true) or adventure (false) + bool creatureAbility; //if true, only creatures can use this spell + si8 positiveness; //1 if spell is positive for influenced stacks, 0 if it is indifferent, -1 if it's negative + std::unique_ptr mechanics;//(!) do not serialize std::unique_ptr adventureMechanics;//(!) do not serialize }; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 003b3dc6a..b7621c14f 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -738,7 +738,7 @@ std::unique_ptr IAdventureSpellMechanics::createMechan case SpellID::VIEW_AIR: return std::make_unique(s); default: - return s->combat ? std::unique_ptr() : std::make_unique(s); + return s->isCombat() ? std::unique_ptr() : std::make_unique(s); } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index f36dd422e..194fd0e6e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1620,7 +1620,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t cs1.learn = true; cs1.hid = toHero;//giving spells to first hero for (auto it : h1->getSpellsInSpellbook()) - if (h2Lvl >= it.toSpell()->level && !h2->spellbookContainsSpell(it))//hero can learn it and don't have it yet + if (h2Lvl >= it.toSpell()->getLevel() && !h2->spellbookContainsSpell(it))//hero can learn it and don't have it yet cs1.spells.insert(it);//spell to learn ChangeSpells cs2; @@ -1628,7 +1628,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t cs2.hid = fromHero; for (auto it : h2->getSpellsInSpellbook()) - if (h1Lvl >= it.toSpell()->level && !h1->spellbookContainsSpell(it)) + if (h1Lvl >= it.toSpell()->getLevel() && !h1->spellbookContainsSpell(it)) cs2.spells.insert(it); if (!cs1.spells.empty() || !cs2.spells.empty())//create a message From d20711bcd68e823385504a6c8da0fb14942276fe Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 21 Aug 2023 21:42:16 +0300 Subject: [PATCH 0149/1248] immunities: polishing --- config/bonuses.json | 32 ----------------------------- config/creatures/conflux.json | 38 ++++++++++++++++++++++------------- config/creatures/inferno.json | 8 ++++---- lib/CBonusTypeHandler.cpp | 6 +++--- 4 files changed, 31 insertions(+), 53 deletions(-) diff --git a/config/bonuses.json b/config/bonuses.json index 66b22babd..69017e7d9 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -19,14 +19,6 @@ } }, - "AIR_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPAIR1" - } - }, - "ATTACKS_ALL_ADJACENT": { "graphics": @@ -137,14 +129,6 @@ "hidden": true }, - "EARTH_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPEATH1" - } - }, - "ENCHANTER": { "graphics": @@ -169,14 +153,6 @@ } }, - "FIRE_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPFIRE1" - } - }, - "FIRE_SHIELD": { "graphics": @@ -585,14 +561,6 @@ "hidden": true }, - "WATER_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPWATER1" - } - }, - "WIDE_BREATH": { "graphics": diff --git a/config/creatures/conflux.json b/config/creatures/conflux.json index 48156e276..397d9e154 100755 --- a/config/creatures/conflux.json +++ b/config/creatures/conflux.json @@ -154,8 +154,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "frostRingVulnerablity" : { @@ -246,10 +246,15 @@ "subtype" : "spell.armageddon", "val" : 100 }, - "immuneToWater" : + "immuneToIceBolt" : { - "type" : "WATER_IMMUNITY", - "subtype" : 2 //immune to damage spells only + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" }, "oppositeFire" : { @@ -440,10 +445,15 @@ "subtype" : "spell.armageddon", "val" : 100 }, - "immuneToWater" : + "immuneToIceBolt" : { - "type" : "WATER_IMMUNITY", - "subtype" : 2 //immune to damage spells only + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.iceBolt" + }, + "immuneToFrostRing" : + { + "type" : "SPELL_IMMUNITY", + "subtype" : "spell.frostRing" }, "oppositeFire" : { @@ -661,8 +671,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "frostRingVulnerablity" : { @@ -732,8 +742,8 @@ { "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 //this IS important + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" } }, "graphics" : @@ -763,8 +773,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 //this IS important + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "rebirth" : { diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 7f39cd53e..91a928c64 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -272,8 +272,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" } }, "upgrades": ["efreetSultan"], @@ -315,8 +315,8 @@ }, "immuneToFire" : { - "type" : "FIRE_IMMUNITY", - "subtype" : 0 + "type" : "SPELL_SCHOOL_IMMUNITY", + "subtype" : "spellSchool.fire" }, "fireShield" : { diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 1810cdc9b..90a4aa7e7 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -118,9 +118,9 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo } break; } - // fileName = "E_FIRE.bmp"; //fire damage - // fileName = "E_COLD.bmp"; //cold damage - // fileName = "E_LIGHT.bmp"; //lightning damage + // fileName = "E_FIRE.bmp"; //fire damage + // fileName = "E_COLD.bmp"; //cold damage + // fileName = "E_LIGHT.bmp"; //lightning damage case BonusType::NEGATIVE_EFFECTS_IMMUNITY: { switch(bonus->subtype) From 75c39c656260077b9ffff1c794e9e90f4a68b3b0 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 21 Aug 2023 21:51:06 +0300 Subject: [PATCH 0150/1248] vcmi: handle icons for SPELL_DAMAGE_REDUCTION For all schools and for ANY subtype --- lib/CBonusTypeHandler.cpp | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 90a4aa7e7..2e4038eaf 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -99,6 +99,28 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo fileName = sp->getIconImmune(); break; } + case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools + { + switch(bonus->subtype) + { + case SpellSchool(ESpellSchool::ANY): + fileName = "E_GOLEM.bmp"; + break; + case SpellSchool(ESpellSchool::AIR): + fileName = "E_LIGHT.bmp"; + break; + case SpellSchool(ESpellSchool::FIRE): + fileName = "E_FIRE.bmp"; + break; + case SpellSchool(ESpellSchool::WATER): + fileName = "E_COLD.bmp"; + break; + case SpellSchool(ESpellSchool::EARTH): + fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage + break; + } + break; + } case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school { switch(bonus->subtype) @@ -118,9 +140,6 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo } break; } - // fileName = "E_FIRE.bmp"; //fire damage - // fileName = "E_COLD.bmp"; //cold damage - // fileName = "E_LIGHT.bmp"; //lightning damage case BonusType::NEGATIVE_EFFECTS_IMMUNITY: { switch(bonus->subtype) From d746a96d55cb36ea5463c14fb3d3829ee92743a4 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 21 Aug 2023 22:10:32 +0300 Subject: [PATCH 0151/1248] vcmi: use SpellSchool identifier instead of enum Use identifier instead of enum inside callbacks. It is better and more expandable solution. --- client/windows/CSpellWindow.cpp | 4 ++-- include/vcmi/spells/Spell.h | 4 ++-- lib/JsonRandom.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 14 +++++++------- lib/spells/CSpellHandler.cpp | 12 ++++++------ lib/spells/CSpellHandler.h | 4 ++-- lib/spells/TargetCondition.cpp | 6 +++--- lib/spells/effects/Damage.cpp | 4 ++-- test/mock/mock_spells_Spell.h | 2 +- .../targetConditions/ElementalConditionTest.cpp | 4 ++-- 10 files changed, 28 insertions(+), 28 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 7e9e7b8b1..288a696cb 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -122,9 +122,9 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m ++sitesPerOurTab[4]; - spell->forEachSchool([&sitesPerOurTab](const ESpellSchool & school, bool & stop) + spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) { - ++sitesPerOurTab[(ui8)school]; + ++sitesPerOurTab[school]; }); } if(sitesPerTabAdv[4] % 12 == 0) diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 4b3eb0d27..bc9a80217 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -24,7 +24,7 @@ class Caster; class DLL_LINKAGE Spell: public EntityT { public: - using SchoolCallback = std::function; + using SchoolCallback = std::function &, bool &)>; ///calculate spell damage on stack taking caster`s secondary skills into account virtual int64_t calculateDamage(const Caster * caster) const = 0; @@ -43,7 +43,7 @@ public: virtual bool isSpecial() const = 0; virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind) - virtual bool hasSchool(ESpellSchool school) const = 0; + virtual bool hasSchool(Identifier school) const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual const std::string & getCastSound() const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 04bc56096..a883bd904 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -260,7 +260,7 @@ namespace JsonRandom vstd::erase_if(spells, [=](const SpellID & spell) { - return !VLC->spellh->getById(spell)->hasSchool(ESpellSchool(schoolID)); + return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID)); }); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 81ec80015..f2f49db13 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -620,14 +620,14 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t { int32_t skill = -1; //skill level - spell->forEachSchool([&, this](const ESpellSchool & cnf, bool & stop) + spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, cnf); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; if(outSelectedSchool) - *outSelectedSchool = SpellSchool(cnf); + *outSelectedSchool = cnf; } }); @@ -650,9 +650,9 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, int maxSchoolBonus = 0; - spell->forEachSchool([&maxSchoolBonus, this](const ESpellSchool & cnf, bool & stop) + spell->forEachSchool([&maxSchoolBonus, this](const SpellSchool & cnf, bool & stop) { - vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, SpellSchool(cnf))); + vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, cnf)); }); base = static_cast(base * (100 + maxSchoolBonus) / 100.0); @@ -739,9 +739,9 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const bool schoolBonus = false; - spell->forEachSchool([this, &schoolBonus](const ESpellSchool & cnf, bool & stop) + spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop) { - if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, SpellSchool(cnf))) + if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, cnf)) { schoolBonus = stop = true; } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 085f95df9..22c6ef28e 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -124,7 +124,7 @@ int64_t CSpell::calculateDamage(const spells::Caster * caster) const return caster->getSpellBonus(this, rawDamage, nullptr); } -bool CSpell::hasSchool(ESpellSchool which) const +bool CSpell::hasSchool(SpellSchool which) const { return school.count(which) && school.at(which); } @@ -149,7 +149,7 @@ spells::AimType CSpell::getTargetType() const return targetType; } -void CSpell::forEachSchool(const std::function& cb) const +void CSpell::forEachSchool(const std::function& cb) const { bool stop = false; for(auto iter : SpellConfig::SCHOOL_ORDER) @@ -157,7 +157,7 @@ void CSpell::forEachSchool(const std::functiongetBonusBearer(); //applying protections - when spell has more then one elements, only one protection should be applied (I think) - forEachSchool([&](const ESpellSchool & cnf, bool & stop) + forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf))) + if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, cnf)) { - ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf)); + ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf); ret /= 100; stop = true; //only bonus from one school is used } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 1fd48d602..c4a419c27 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -201,14 +201,14 @@ public: int64_t calculateDamage(const spells::Caster * caster) const override; - bool hasSchool(ESpellSchool school) const override; + bool hasSchool(SpellSchool school) const override; /** * Calls cb for each school this spell belongs to * * Set stop to true to abort looping */ - void forEachSchool(const std::function & cb) const override; + void forEachSchool(const std::function & cb) const override; spells::AimType getTargetType() const; diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index 05d9cb39a..75cb838d6 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -177,16 +177,16 @@ protected: bool elementalImmune = false; auto bearer = target->getBonusBearer(); - m->getSpell()->forEachSchool([&](const ESpellSchool & cnf, bool & stop) + m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, SpellSchool(cnf))) + if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, cnf)) { elementalImmune = true; stop = true; //only bonus from one school is used } else if(!m->isPositiveSpell()) //negative or indifferent { - if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, SpellSchool(cnf))) + if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, cnf)) { elementalImmune = true; stop = true; //only bonus from one school is used diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index fb068a860..ca36a3db9 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -87,9 +87,9 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)) >= 100); //General spell damage immunity //elemental immunity for damage - m->getSpell()->forEachSchool([&](const ESpellSchool & cnf, bool & stop) + m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(cnf)) >= 100); //100% reduction is immunity + isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf) >= 100); //100% reduction is immunity }); return !isImmune; diff --git a/test/mock/mock_spells_Spell.h b/test/mock/mock_spells_Spell.h index f507e3836..3e8aa442e 100644 --- a/test/mock/mock_spells_Spell.h +++ b/test/mock/mock_spells_Spell.h @@ -45,7 +45,7 @@ public: MOCK_CONST_METHOD0(isOffensive, bool()); MOCK_CONST_METHOD0(isSpecial, bool()); MOCK_CONST_METHOD0(isMagical, bool()); - MOCK_CONST_METHOD1(hasSchool, bool(ESpellSchool)); + MOCK_CONST_METHOD1(hasSchool, bool(SpellSchool)); MOCK_CONST_METHOD1(forEachSchool, void(const SchoolCallback &)); MOCK_CONST_METHOD0(getCastSound, const std::string &()); MOCK_CONST_METHOD1(registerIcons, void(const IconRegistar &)); diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index f57b92d34..e513b9ce7 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -30,8 +30,8 @@ public: EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { bool stop = false; - cb(ESpellSchool::AIR, stop); - cb(ESpellSchool::FIRE, stop); + cb(SpellSchool(ESpellSchool::AIR), stop); + cb(SpellSchool(ESpellSchool::FIRE), stop); }); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); From e6710a4eb0d4344dbf15bf73a94254f2fe0e115e Mon Sep 17 00:00:00 2001 From: Konstantin Date: Mon, 21 Aug 2023 22:16:45 +0300 Subject: [PATCH 0152/1248] vcmi: bump serialization version --- lib/serializer/CSerializer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 926d73b6c..086eb0d04 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 825; -const ui32 MINIMAL_SERIALIZATION_VERSION = 824; +const ui32 SERIALIZATION_VERSION = 826; +const ui32 MINIMAL_SERIALIZATION_VERSION = 826; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From b8ab64f448eb435af901ffc653c376666e5806d7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 23 Aug 2023 19:29:50 +0400 Subject: [PATCH 0153/1248] Make options tab configurable --- client/gui/InterfaceObjectConfigurable.cpp | 22 +++- client/gui/InterfaceObjectConfigurable.h | 2 + client/lobby/OptionsTab.cpp | 37 ++++--- client/lobby/OptionsTab.h | 36 +++---- client/lobby/RandomMapTab.cpp | 2 +- config/widgets/optionsTab.json | 112 +++++++++++++++++++++ 6 files changed, 168 insertions(+), 43 deletions(-) create mode 100644 config/widgets/optionsTab.json diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index b24729528..37cb233f3 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -17,6 +17,8 @@ #include "../gui/CGuiHandler.h" #include "../gui/ShortcutHandler.h" #include "../gui/Shortcut.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" #include "../widgets/CComponent.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" @@ -43,6 +45,7 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("texture", &InterfaceObjectConfigurable::buildTexture); REGISTER_BUILDER("animation", &InterfaceObjectConfigurable::buildAnimation); REGISTER_BUILDER("label", &InterfaceObjectConfigurable::buildLabel); + REGISTER_BUILDER("multiLineLabel", &InterfaceObjectConfigurable::buildMultiLineLabel); REGISTER_BUILDER("toggleGroup", &InterfaceObjectConfigurable::buildToggleGroup); REGISTER_BUILDER("toggleButton", &InterfaceObjectConfigurable::buildToggleButton); REGISTER_BUILDER("button", &InterfaceObjectConfigurable::buildButton); @@ -301,6 +304,20 @@ std::shared_ptr InterfaceObjectConfigurable::buildLabel(const JsonNode & return std::make_shared(position.x, position.y, font, alignment, color, text); } +std::shared_ptr InterfaceObjectConfigurable::buildMultiLineLabel(const JsonNode & config) const +{ + logGlobal->debug("Building widget CMultiLineLabel"); + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + auto text = readText(config["text"]); + Rect rect = readRect(config["rect"]); + if(!config["adoptHeight"].isNull() && config["adoptHeight"].Bool()) + rect.h = graphics->fonts[font]->getLineHeight() * 2; + return std::make_shared(rect, font, alignment, color, text); +} + + std::shared_ptr InterfaceObjectConfigurable::buildToggleGroup(const JsonNode & config) const { logGlobal->debug("Building widget CToggleGroup"); @@ -466,11 +483,14 @@ std::shared_ptr InterfaceObjectConfigurable::buildSlider(const JsonNode const auto & result = std::make_shared(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); - if (!config["scrollBounds"].isNull()) + if(!config["scrollBounds"].isNull()) { Rect bounds = readRect(config["scrollBounds"]); result->setScrollBounds(bounds); } + + if(!config["panningStep"].isNull()) + result->setPanningStep(config["panningStep"].Integer()); return result; } diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 07d0a57c1..86a3381c0 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -18,6 +18,7 @@ class CPicture; class CLabel; +class CMultiLineLabel; class CToggleGroup; class CToggleButton; class CButton; @@ -88,6 +89,7 @@ protected: //basic widgets std::shared_ptr buildPicture(const JsonNode &) const; std::shared_ptr buildLabel(const JsonNode &) const; + std::shared_ptr buildMultiLineLabel(const JsonNode &) const; std::shared_ptr buildToggleGroup(const JsonNode &) const; std::shared_ptr buildToggleButton(const JsonNode &) const; std::shared_ptr buildButton(const JsonNode &) const; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index f76e1c12e..87968c2dd 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -31,6 +31,7 @@ #include "../windows/InfoWindows.h" #include "../eventsSDL/InputHandler.h" +#include "../../lib/filesystem/Filesystem.h" #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" @@ -42,23 +43,20 @@ OptionsTab::OptionsTab() : humanPlayers(0) { recActions = 0; - OBJ_CONSTRUCTION; - background = std::make_shared("ADVOPTBK", 0, 6); - pos = background->pos; - labelTitle = std::make_shared(222, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[515]); - labelSubTitle = std::make_shared(Rect(60, 44, 320, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[516]); - - labelPlayerNameAndHandicap = std::make_shared(Rect(58, 86, 100, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[517]); - labelStartingTown = std::make_shared(Rect(163, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[518]); - labelStartingHero = std::make_shared(Rect(239, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[519]); - labelStartingBonus = std::make_shared(Rect(315, 86, 70, (int)graphics->fonts[EFonts::FONT_SMALL]->getLineHeight()*2), EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[520]); + + addCallback("setTurnLength", std::bind(&IServerAPI::setTurnLength, CSH, _1)); + + const JsonNode config(ResourceID("config/widgets/optionsTab.json")); + build(config); + if(SEL->screenType == ESelectionScreen::newGame || SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::scenarioInfo) { - sliderTurnDuration = std::make_shared(Point(55, 551), 194, std::bind(&IServerAPI::setTurnLength, CSH, _1), 1, (int)GameConstants::POSSIBLE_TURNTIME.size(), (int)GameConstants::POSSIBLE_TURNTIME.size(), Orientation::HORIZONTAL, CSlider::BLUE); - sliderTurnDuration->setScrollBounds(Rect(-3, -25, 337, 43)); - sliderTurnDuration->setPanningStep(20); - labelPlayerTurnDuration = std::make_shared(222, 538, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[521]); - labelTurnDurationValue = std::make_shared(319, 559, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + if(auto w = widget("sliderTurnDuration")) + w->deactivate(); + if(auto w = widget("labelPlayerTurnDuration")) + w->deactivate(); + if(auto w = widget("labelTurnDurationValue")) + w->deactivate(); } } @@ -76,10 +74,11 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } - if(sliderTurnDuration) + if(auto turnSlider = widget("sliderTurnDuration")) { - sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTimerInfo.turnTimer / (60 * 1000))); - labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]); + turnSlider->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTimerInfo.turnTimer / (60 * 1000))); + if(auto w = widget("labelTurnDurationValue")) + w->setText(CGI->generaltexth->turnDurations[turnSlider->getValue()]); } } @@ -851,7 +850,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con } pos.x += 54; - pos.y += 122 + serial * 50; + pos.y += 128 + serial * 50; assert(CSH->mi && CSH->mi->mapHeader); const PlayerInfo & p = SEL->getPlayerInfo(s->color.getNum()); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 9af09bb6c..f9a5a6778 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -10,15 +10,14 @@ #pragma once #include "../windows/CWindowObject.h" +#include "../widgets/Scrollable.h" +#include "../gui/InterfaceObjectConfigurable.h" VCMI_LIB_NAMESPACE_BEGIN struct PlayerSettings; struct PlayerInfo; VCMI_LIB_NAMESPACE_END -#include "../widgets/Scrollable.h" - -class CSlider; class CLabel; class CMultiLineLabel; class CFilledTexture; @@ -30,21 +29,19 @@ class CButton; class FilledTexturePlayerColored; /// The options tab which is shown at the map selection phase. -class OptionsTab : public CIntObject +class OptionsTab : public InterfaceObjectConfigurable { - std::shared_ptr background; - std::shared_ptr labelTitle; - std::shared_ptr labelSubTitle; - std::shared_ptr labelPlayerNameAndHandicap; - std::shared_ptr labelStartingTown; - std::shared_ptr labelStartingHero; - std::shared_ptr labelStartingBonus; - - std::shared_ptr labelPlayerTurnDuration; - std::shared_ptr labelTurnDurationValue; + struct PlayerOptionsEntry; + ui8 humanPlayers; - + std::map> entries; + public: + + OptionsTab(); + void recreate(); + void onSetPlayerClicked(const PlayerSettings & ps) const; + enum SelType { TOWN, @@ -52,6 +49,8 @@ public: BONUS }; +private: + struct CPlayerSettingsHelper { const PlayerSettings & settings; @@ -187,11 +186,4 @@ public: private: const OptionsTab & parentTab; }; - - std::shared_ptr sliderTurnDuration; - std::map> entries; - - OptionsTab(); - void recreate(); - void onSetPlayerClicked(const PlayerSettings & ps) const; }; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 588dafef4..88f015e9b 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -41,7 +41,6 @@ RandomMapTab::RandomMapTab(): recActions = 0; mapGenOptions = std::make_shared(); - const JsonNode config(ResourceID("config/widgets/randomMapTab.json")); addCallback("toggleMapSize", [&](int btnId) { auto mapSizeVal = getPossibleMapSizes(); @@ -123,6 +122,7 @@ RandomMapTab::RandomMapTab(): }); } + const JsonNode config(ResourceID("config/widgets/randomMapTab.json")); build(config); updateMapInfoByHost(); diff --git a/config/widgets/optionsTab.json b/config/widgets/optionsTab.json new file mode 100644 index 000000000..8e9072d78 --- /dev/null +++ b/config/widgets/optionsTab.json @@ -0,0 +1,112 @@ +{ + "items": + [ + { + "name": "background", + "type": "picture", + "image": "ADVOPTBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.515", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "core.genrltxt.516", + "rect": {"x": 60, "y": 50, "w": 320, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelPlayerNameAndHandicap", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.517", + "rect": {"x": 58, "y": 92, "w": 100, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingTown", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.518", + "rect": {"x": 163, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingHero", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.519", + "rect": {"x": 239, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + { + "name": "labelStartingBonus", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.520", + "rect": {"x": 315, "y": 92, "w": 70, "h": 0}, + "adoptHeight": true + }, + + // timer + { + "name": "labelPlayerTurnDuration", + "type": "label", + "font": "small", + "alignment": "center", + "color": "yellow", + "text": "core.genrltxt.521", + "position": {"x": 222, "y": 544} + }, + + { + "name": "labelTurnDurationValue", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 319, "y": 565} + }, + + { + "name": "sliderTurnDuration", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 55, "y": 557}, + "size": 194, + "callback": "setTurnLength", + "itemsVisible": 1, + "itemsTotal": 11, + "selected": 11, + "style": "blue", + "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, + "panningStep": 20 + }, + ] +} From 81242d3500d3271816c9bf324fc5dbfbb252a9ab Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 22 Aug 2023 20:57:58 +0300 Subject: [PATCH 0154/1248] Fixed ending of battles due to retreat/surrender --- client/battle/BattleInterface.cpp | 23 ++++------------------- lib/battle/BattleAction.cpp | 10 ++++++++-- lib/battle/BattleAction.h | 1 + server/battles/BattleActionProcessor.cpp | 15 +++++++++++---- server/battles/BattleProcessor.cpp | 3 +++ server/battles/BattleResultProcessor.cpp | 6 ++++++ server/battles/BattleResultProcessor.h | 2 ++ 7 files changed, 35 insertions(+), 25 deletions(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 93842e95b..5bc4133ca 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -325,7 +325,7 @@ void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) curInt->cb->selectionMade(selection, queryID); }; GH.windows().pushWindow(wnd); - + curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 CPlayerInterface::battleInt = nullptr; } @@ -601,28 +601,13 @@ void BattleInterface::startAction(const BattleAction & action) return; } - const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); - - if (stack) - { - windowObject->updateQueue(); - } - else - { - assert(action.actionType == EActionType::HERO_SPELL); //only cast spell is valid action without acting stack number - } - stacksController->startAction(action); - if(action.actionType == EActionType::HERO_SPELL) //when hero casts spell + if (!action.isUnitAction()) return; - if (!stack) - { - logGlobal->error("Something wrong with stackNumber in actionStarted. Stack number: %d", action.stackNumber); - return; - } - + assert(curInt->cb->battleGetStackByID(action.stackNumber)); + windowObject->updateQueue(); effectsController->startAction(action); } diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index 5a92ce08e..f8f63a333 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -205,7 +205,6 @@ bool BattleAction::isUnitAction() const EActionType::BAD_MORALE, EActionType::STACK_HEAL }; - return vstd::contains(actions, actionType); } @@ -215,7 +214,15 @@ bool BattleAction::isSpellAction() const EActionType::HERO_SPELL, EActionType::MONSTER_SPELL }; + return vstd::contains(actions, actionType); +} +bool BattleAction::isBattleEndAction() const +{ + static const std::array actions = { + EActionType::RETREAT, + EActionType::SURRENDER + }; return vstd::contains(actions, actionType); } @@ -227,7 +234,6 @@ bool BattleAction::isTacticsAction() const EActionType::RETREAT, EActionType::SURRENDER }; - return vstd::contains(actions, actionType); } diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index 12eba04e4..593b6290f 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -46,6 +46,7 @@ public: bool isTacticsAction() const; bool isUnitAction() const; bool isSpellAction() const; + bool isBattleEndAction() const; std::string toString() const; void aimToHex(const BattleHex & destination); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index e32d082f3..18da73a5b 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -535,13 +535,20 @@ bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) logGlobal->trace("Making action: %s", ba.toString()); const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - StartAction startAction(ba); - gameHandler->sendAndApply(&startAction); + // for these events client does not expects StartAction/EndAction wrapper + if (!ba.isBattleEndAction()) + { + StartAction startAction(ba); + gameHandler->sendAndApply(&startAction); + } bool result = dispatchBattleAction(ba); - EndAction endAction; - gameHandler->sendAndApply(&endAction); + if (!ba.isBattleEndAction()) + { + EndAction endAction; + gameHandler->sendAndApply(&endAction); + } if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 62a0be3a1..5d7be25ec 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -147,6 +147,9 @@ bool BattleProcessor::checkBattleStateChanges() if (gameHandler->battleGetSiegeLevel() > 0) updateGateState(); + if (resultProcessor->battleIsEnding()) + return true; + //check if battle ended if (auto result = gameHandler->battleIsFinished()) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 912de4e65..6e23d3103 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -528,6 +528,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) } finishingBattle.reset(); + battleResult.reset(); } void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) @@ -537,3 +538,8 @@ void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victor battleResult->winner = victoriusSide; //surrendering side loses gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); } + +bool BattleResultProcessor::battleIsEnding() const +{ + return battleResult != nullptr; +} diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index ec4b13ab6..e758cadfe 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -71,6 +71,8 @@ public: explicit BattleResultProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); + bool battleIsEnding() const; + void setBattleResult(EBattleResult resultType, int victoriusSide); void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); From a5501abd01d1e8af0d8869be9993cf88a27faa67 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 22 Aug 2023 22:15:32 +0300 Subject: [PATCH 0155/1248] Fix skipping battle intro via click --- client/CPlayerInterface.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 89a4a21a2..6819f1943 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -654,6 +654,11 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) { + // when battle starts, game will send battleStart pack *before* movement confirmation + // and since network thread wait for battle intro to play, movement confirmation will only happen after intro + // leading to several bugs, such as blocked input during intro + stillMoveHero.setn(STOP_MOVE); + //Don't wait for dialogs when we are non-active hot-seat player if (LOCPLINT == this) waitForAllDialogs(); From 4ab2e617d5aadee6cf7f63ae679c1b7cfd768b86 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 23 Aug 2023 19:34:33 +0300 Subject: [PATCH 0156/1248] Fix infinite loop on Fear trigger --- lib/battle/BattleAction.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index f8f63a333..488cdf3ac 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -194,7 +194,8 @@ void BattleAction::setTarget(const battle::Target & target_) bool BattleAction::isUnitAction() const { - static const std::array actions = { + static const std::array actions = { + EActionType::NO_ACTION, EActionType::WALK, EActionType::WAIT, EActionType::DEFEND, From 4784ae94b5491f4008c29d2b633b220a0760180c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 20:03:40 +0200 Subject: [PATCH 0157/1248] revert uuid --- Global.h | 4 ---- client/CServerHandler.cpp | 3 +++ lib/StartInfo.h | 4 +--- server/CVCMIServer.cpp | 5 +++++ 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Global.h b/Global.h index e0c22dfae..3c308f54c 100644 --- a/Global.h +++ b/Global.h @@ -121,7 +121,6 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include -#include #include #include #include @@ -167,9 +166,6 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include -#include -#include -#include #ifndef M_PI # define M_PI 3.14159265358979323846 diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index da92e340b..ea6c3fbae 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -49,6 +49,9 @@ #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" +#include +#include +#include #include #include "../lib/serializer/Cast.h" #include "LobbyClientNetPackVisitors.h" diff --git a/lib/StartInfo.h b/lib/StartInfo.h index afcaade81..7c5f46e93 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -83,7 +83,6 @@ struct DLL_LINKAGE StartInfo ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant - std::string gameUuid; std::string startTimeIso8601; TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame @@ -108,7 +107,6 @@ struct DLL_LINKAGE StartInfo h & seedToBeUsed; h & seedPostInit; h & mapfileChecksum; - h & gameUuid; h & startTimeIso8601; h & turnTimerInfo; h & mapname; @@ -117,7 +115,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), gameUuid(boost::uuids::to_string(boost::uuids::random_generator()())), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))) + mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))) { } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 93bedbfc6..83a711fe9 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -50,6 +50,11 @@ // for applier #include "../lib/registerTypes/RegisterTypes.h" +// UUID generation +#include +#include +#include + #include "../lib/gameState/CGameState.h" template class CApplyOnServer; From 1586b9fbd16d851097a950110ba65e1c78e914e9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 21:37:44 +0200 Subject: [PATCH 0158/1248] possibility to restart --- client/CPlayerInterface.cpp | 6 ++---- server/CVCMIServer.cpp | 2 ++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index fcf684593..3c2069874 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -180,7 +180,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) GH.windows().pushWindow(adventureInt); } -//close window from another player + // close window from another player if(auto w = GH.windows().topWindow()) if(w->ID == -1 && player != playerID) w->close(); @@ -201,8 +201,6 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) void CPlayerInterface::performAutosave() { - std::string id = cb->getStartInfo()->gameUuid.substr(0, 4); - int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); if(frequency > 0 && cb->getDate() % frequency == 0) { @@ -232,7 +230,7 @@ void CPlayerInterface::performAutosave() + std::to_string(cb->getDate(Date::WEEK)) + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - cb->save("Saves/Autosave/" + prefix + "Autosave_" + id + "_" + stringifiedDate); + cb->save("Saves/Autosave/" + prefix + "Autosave_" + stringifiedDate); } } } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 83a711fe9..9a7b9abc9 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -284,6 +284,7 @@ bool CVCMIServer::prepareToStartGame() { case StartInfo::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); gh->init(si.get()); @@ -291,6 +292,7 @@ bool CVCMIServer::prepareToStartGame() case StartInfo::NEW_GAME: logNetwork->info("Preparing to start new game"); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); gh->init(si.get()); break; From 26bddbac446442852cc3a260841d28736285ec01 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 23 Aug 2023 22:46:15 +0300 Subject: [PATCH 0159/1248] Fix possible uncaught exception --- lib/vstd/DateUtils.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index 41f2d59c7..167ebac02 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -10,7 +10,14 @@ namespace vstd { std::tm tm = *std::localtime(&dt); std::stringstream s; - s.imbue(std::locale("")); + try + { + s.imbue(std::locale("")); + } + catch(const std::runtime_error & e) + { + // locale not be available - keep default / global + } s << std::put_time(&tm, "%x %X"); return s.str(); } From a84c5fa371a2638e4214f27714deb609ab70bd47 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 23 Aug 2023 22:46:42 +0300 Subject: [PATCH 0160/1248] Fix battle replaying --- server/battles/BattleProcessor.cpp | 1 + server/battles/BattleResultProcessor.cpp | 6 ++++++ server/battles/BattleResultProcessor.h | 1 + 3 files changed, 8 insertions(+) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 5d7be25ec..50f2a31b2 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -67,6 +67,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm heroes[0] = hero1; heroes[1] = hero2; + resultProcessor->setupBattle(); setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6e23d3103..e6616fa38 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -539,6 +539,12 @@ void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victor gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); } +void BattleResultProcessor::setupBattle() +{ + finishingBattle.reset(); + battleResult.reset(); +} + bool BattleResultProcessor::battleIsEnding() const { return battleResult != nullptr; diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index e758cadfe..9c85638ae 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -73,6 +73,7 @@ public: bool battleIsEnding() const; + void setupBattle(); void setBattleResult(EBattleResult resultType, int victoriusSide); void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle void endBattleConfirm(const BattleInfo * battleInfo); From 485af4b4b54b7f8d47bb5774736e1a0c938e2103 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 22:23:05 +0200 Subject: [PATCH 0161/1248] avoid forbidden chars in path --- client/CPlayerInterface.cpp | 6 +++++- lib/vstd/DateUtils.cpp | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ea72d001c..f365d1cea 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -212,7 +212,11 @@ void CPlayerInterface::performAutosave() prefix = settings["general"]["savePrefix"].String(); if(prefix.empty()) { - prefix = cb->getMapHeader()->name.substr(0, 15) + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; + std::string name = cb->getMapHeader()->name.substr(0, 15); + std::string forbiddenChars("\\/:?\"<>| "); + std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' ); + + prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; } } diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index f494f3e66..8965ae72f 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -19,7 +19,6 @@ namespace vstd { std::tm tm = *std::localtime(&dt); std::stringstream s; - s.imbue(std::locale("")); s << std::put_time(&tm, "%Y%m%dT%H%M%S"); return s.str(); } From 9f68a0bf4908428163847c941be176f0ba2ecbf9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:25:48 +0200 Subject: [PATCH 0162/1248] avoid destroing utf-8; no extra Autosave text --- Global.h | 1 + client/CPlayerInterface.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Global.h b/Global.h index e62842a6d..675a3eff4 100644 --- a/Global.h +++ b/Global.h @@ -108,6 +108,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include #include #include #include diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index f365d1cea..0cba0949f 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -212,7 +212,11 @@ void CPlayerInterface::performAutosave() prefix = settings["general"]["savePrefix"].String(); if(prefix.empty()) { - std::string name = cb->getMapHeader()->name.substr(0, 15); + std::string name = cb->getMapHeader()->name; + std::wstring_convert, char32_t> conv; + int txtlen = conv.from_bytes(name).size(); + + TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); std::string forbiddenChars("\\/:?\"<>| "); std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' ); @@ -225,7 +229,7 @@ void CPlayerInterface::performAutosave() int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); if(autosaveCountLimit > 0) { - cb->save("Saves/Autosave/" + prefix + "Autosave_" + std::to_string(autosaveCount)); + cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount)); autosaveCount %= autosaveCountLimit; } else @@ -234,7 +238,7 @@ void CPlayerInterface::performAutosave() + std::to_string(cb->getDate(Date::WEEK)) + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - cb->save("Saves/Autosave/" + prefix + "Autosave_" + stringifiedDate); + cb->save("Saves/Autosave/" + prefix + stringifiedDate); } } } From e6f1677d3a35494ab1ff92a3948a84d3377fce1f Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 23 Aug 2023 23:34:33 +0200 Subject: [PATCH 0163/1248] missing header... --- client/CPlayerInterface.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 0cba0949f..e467907e5 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -68,6 +68,7 @@ #include "../lib/VCMIDirs.h" #include "../lib/CStopWatch.h" #include "../lib/StartInfo.h" +#include "../lib/TextOperations.h" #include "../lib/CPlayerState.h" #include "../lib/GameConstants.h" #include "gui/CGuiHandler.h" From 6efccb2503b3e7a01a2aa937512bdbcdd2168cb5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 24 Aug 2023 15:01:43 +0400 Subject: [PATCH 0164/1248] Fix removing loading screen --- client/NetPacksLobbyClient.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index ab6f7e2c0..d78207b09 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -136,8 +136,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack w->tick(0); w->redraw(); } - else - GH.windows().createAndPushWindow(); } void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress & pack) From 596b98e1c13b32d1fb35731a31e981dd1a31e310 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 24 Aug 2023 16:42:47 +0400 Subject: [PATCH 0165/1248] Fix AI freeze regression --- server/CGameHandler.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 152ef5480..c1f6093c7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1268,16 +1268,14 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - if(!transit) + for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner)) { - for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner)) - { - moveQuery = std::dynamic_pointer_cast(topQuery); - if(moveQuery) - queries->popIfTop(moveQuery); - else - break; - } + moveQuery = std::dynamic_pointer_cast(topQuery); + if(moveQuery + && (!transit || result == TryMoveHero::FAILED || moveQuery->tmh.stopMovement())) + queries->popIfTop(moveQuery); + else + break; } logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; From 37e22927208b1075aa3a2285e8d5f1f3286e8cff Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 24 Aug 2023 18:34:00 +0200 Subject: [PATCH 0166/1248] count unicode chars to extra function --- client/CPlayerInterface.cpp | 3 +-- lib/TextOperations.cpp | 6 ++++++ lib/TextOperations.h | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index e467907e5..c69bd1f75 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -214,8 +214,7 @@ void CPlayerInterface::performAutosave() if(prefix.empty()) { std::string name = cb->getMapHeader()->name; - std::wstring_convert, char32_t> conv; - int txtlen = conv.from_bytes(name).size(); + int txtlen = TextOperations::getUnicodeCharactersCount(name); TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); std::string forbiddenChars("\\/:?\"<>| "); diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index f97851140..0643776ba 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -193,6 +193,12 @@ void TextOperations::trimRightUnicode(std::string & text, const size_t amount) } } +size_t getUnicodeCharactersCount(const std::string & text) +{ + std::wstring_convert, char32_t> conv; + return conv.from_bytes(text).size(); +} + std::string TextOperations::escapeString(std::string input) { boost::replace_all(input, "\\", "\\\\"); diff --git a/lib/TextOperations.h b/lib/TextOperations.h index 9173260a1..0ca1ac69b 100644 --- a/lib/TextOperations.h +++ b/lib/TextOperations.h @@ -46,6 +46,9 @@ namespace TextOperations ///delete specified amount of UTF-8 characters from right DLL_LINKAGE void trimRightUnicode(std::string & text, size_t amount = 1); + /// give back amount of unicode characters + size_t DLL_LINKAGE getUnicodeCharactersCount(const std::string & text); + /// converts number into string using metric system prefixes, e.g. 'k' or 'M' to keep resulting strings within specified size /// Note that resulting string may have more symbols than digits: minus sign and prefix symbol template From e317c23a594fdb9fe8a01aa07ac91f1bbcb78bf9 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 24 Aug 2023 20:31:06 +0200 Subject: [PATCH 0167/1248] fix --- lib/TextOperations.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index 0643776ba..d5c966adb 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -193,7 +193,7 @@ void TextOperations::trimRightUnicode(std::string & text, const size_t amount) } } -size_t getUnicodeCharactersCount(const std::string & text) +size_t TextOperations::getUnicodeCharactersCount(const std::string & text) { std::wstring_convert, char32_t> conv; return conv.from_bytes(text).size(); From c4bc6840eaef46407b4690c769f13074fb14ae7f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 22 Aug 2023 18:45:13 +0300 Subject: [PATCH 0168/1248] Moved management of turn order into a new class --- include/vcmi/events/PlayerGotTurn.h | 2 +- lib/events/PlayerGotTurn.cpp | 4 +- server/CGameHandler.cpp | 204 ++++--------------- server/CGameHandler.h | 50 +---- server/CMakeLists.txt | 2 + server/CVCMIServer.cpp | 14 +- server/CVCMIServer.h | 5 +- server/NetPacksLobbyServer.cpp | 16 +- server/NetPacksServer.cpp | 23 +-- server/ServerNetPackVisitors.h | 5 +- server/TurnTimerHandler.cpp | 5 +- server/battles/BattleFlowProcessor.cpp | 1 - server/processors/HeroPoolProcessor.cpp | 28 +-- server/processors/HeroPoolProcessor.h | 1 - server/processors/PlayerMessageProcessor.cpp | 2 +- server/processors/TurnOrderProcessor.cpp | 183 +++++++++++++++++ server/processors/TurnOrderProcessor.h | 67 ++++++ 17 files changed, 341 insertions(+), 271 deletions(-) create mode 100644 server/processors/TurnOrderProcessor.cpp create mode 100644 server/processors/TurnOrderProcessor.h diff --git a/include/vcmi/events/PlayerGotTurn.h b/include/vcmi/events/PlayerGotTurn.h index a75380cca..162300701 100644 --- a/include/vcmi/events/PlayerGotTurn.h +++ b/include/vcmi/events/PlayerGotTurn.h @@ -29,7 +29,7 @@ public: using ExecHandler = Sub::ExecHandler; static Sub * getRegistry(); - static void defaultExecute(const EventBus * bus, const ExecHandler & execHandler, PlayerColor & player); + static void defaultExecute(const EventBus * bus, PlayerColor & player); virtual PlayerColor getPlayer() const = 0; virtual void setPlayer(const PlayerColor & value) = 0; diff --git a/lib/events/PlayerGotTurn.cpp b/lib/events/PlayerGotTurn.cpp index 123853d31..e4a259e99 100644 --- a/lib/events/PlayerGotTurn.cpp +++ b/lib/events/PlayerGotTurn.cpp @@ -24,11 +24,11 @@ SubscriptionRegistry * PlayerGotTurn::getRegistry() return Instance.get(); } -void PlayerGotTurn::defaultExecute(const EventBus * bus, const ExecHandler & execHandler, PlayerColor & player) +void PlayerGotTurn::defaultExecute(const EventBus * bus, PlayerColor & player) { CPlayerGotTurn event; event.setPlayer(player); - bus->executeEvent(event, execHandler); + bus->executeEvent(event); player = event.getPlayer(); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index ee726c596..3253d1cf4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -16,6 +16,7 @@ #include "battles/BattleProcessor.h" #include "processors/HeroPoolProcessor.h" #include "processors/PlayerMessageProcessor.h" +#include "processors/TurnOrderProcessor.h" #include "queries/QueriesProcessor.h" #include "queries/MapQueries.h" @@ -92,7 +93,7 @@ public: T *ptr = static_cast(pack); try { - ApplyGhNetPackVisitor applier(*gh, *gs); + ApplyGhNetPackVisitor applier(*gh); ptr->visit(applier); @@ -123,51 +124,6 @@ static inline double distance(int3 a, int3 b) return std::sqrt((double)(a.x-b.x)*(a.x-b.x) + (a.y-b.y)*(a.y-b.y)); } -PlayerStatus PlayerStatuses::operator[](PlayerColor player) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - return players.at(player); - } - else - { - throw std::runtime_error("No such player!"); - } -} -void PlayerStatuses::addPlayer(PlayerColor player) -{ - boost::unique_lock l(mx); - players[player]; -} - -bool PlayerStatuses::checkFlag(PlayerColor player, bool PlayerStatus::*flag) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - return players[player].*flag; - } - else - { - throw std::runtime_error("No such player!"); - } -} - -void PlayerStatuses::setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val) -{ - boost::unique_lock l(mx); - if (players.find(player) != players.end()) - { - players[player].*flag = val; - } - else - { - throw std::runtime_error("No such player!"); - } - cv.notify_all(); -} - template void callWith(std::vector args, std::function fun, ui32 which) { @@ -480,7 +436,7 @@ void CGameHandler::changeSecSkill(const CGHeroInstance * hero, SecondarySkill wh void CGameHandler::handleClientDisconnection(std::shared_ptr c) { - if(lobby->state == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) + if(lobby->getState() == EServerState::SHUTDOWN || !gs || !gs->scenarioOps) return; for(auto & playerConnections : connections) @@ -546,6 +502,7 @@ CGameHandler::CGameHandler(CVCMIServer * lobby) : lobby(lobby) , heroPool(std::make_unique(this)) , battles(std::make_unique(this)) + , turnOrder(std::make_unique(this)) , queries(std::make_unique()) , playerMessages(std::make_unique(this)) , complainNoCreatures("No creatures to split") @@ -592,9 +549,7 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack getRandomGenerator().resetSeed(); for (auto & elem : gs->players) - { - states.addPlayer(elem.first); - } + turnOrder->addPlayer(elem.first); reinitScripting(); } @@ -646,7 +601,18 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa } } -void CGameHandler::newTurn() +void CGameHandler::onPlayerTurnStarted(PlayerColor which) +{ + events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which); + turnTimerHandler.onPlayerGetTurn(gs->players[which]); +} + +void CGameHandler::onPlayerTurnEnded(PlayerColor which) +{ + +} + +void CGameHandler::onNewTurn() { logGlobal->trace("Turn %d", gs->day+1); NewTurn n; @@ -965,6 +931,7 @@ void CGameHandler::newTurn() synchronizeArtifactHandlerLists(); //new day events may have changed them. TODO better of managing that } + void CGameHandler::run(bool resume) { LOG_TRACE_PARAMS(logGlobal, "resume=%d", resume); @@ -989,114 +956,31 @@ void CGameHandler::run(bool resume) services()->scripts()->run(serverScripts); #endif - if(resume) + if (!resume) + { + onNewTurn(); + events::TurnStarted::defaultExecute(serverEventBus.get()); + for(auto & player : gs->players) + turnTimerHandler.onGameplayStart(player.second); + } + else events::GameResumed::defaultExecute(serverEventBus.get()); - auto playerTurnOrder = generatePlayerTurnOrder(); - - if(!resume) - for(auto & playerColor : playerTurnOrder) - turnTimerHandler.onGameplayStart(gs->players[playerColor]); - - while(lobby->state == EServerState::GAMEPLAY) + turnOrder->onGameStarted(); + + //wait till game is done + while(lobby->getState() == EServerState::GAMEPLAY) { - if(!resume) - { - newTurn(); - events::TurnStarted::defaultExecute(serverEventBus.get()); - } + const int waitTime = 100; //ms - std::list::iterator it; - if (resume) - { - it = std::find(playerTurnOrder.begin(), playerTurnOrder.end(), gs->currentPlayer); - } - else - { - it = playerTurnOrder.begin(); - } + turnTimerHandler.onPlayerMakingTurn(gs->players[gs->getCurrentPlayer()], waitTime); + if(gs->curB) + turnTimerHandler.onBattleLoop(waitTime); - resume = false; - for (; (it != playerTurnOrder.end()) && (lobby->state == EServerState::GAMEPLAY) ; it++) - { - auto playerColor = *it; - - auto onGetTurn = [&](events::PlayerGotTurn & event) - { - //if player runs out of time, he shouldn't get the turn (especially AI) - //pre-trigger may change anything, should check before each player - //TODO: is it enough to check only one player? - checkVictoryLossConditionsForAll(); - - auto player = event.getPlayer(); - - const PlayerState * playerState = &gs->players[player]; - - if(playerState->status != EPlayerStatus::INGAME) - { - event.setPlayer(PlayerColor::CANNOT_DETERMINE); - } - else - { - states.setFlag(player, &PlayerStatus::makingTurn, true); - - YourTurn yt; - yt.player = player; - //Change local daysWithoutCastle counter for local interface message //TODO: needed? - yt.daysWithoutCastle = playerState->daysWithoutCastle; - applyAndSend(&yt); - - turnTimerHandler.onPlayerGetTurn(gs->players[player]); - } - }; - - events::PlayerGotTurn::defaultExecute(serverEventBus.get(), onGetTurn, playerColor); - - if(playerColor != PlayerColor::CANNOT_DETERMINE) - { - //wait till turn is done - const int waitTime = 100; //ms - boost::unique_lock lock(states.mx); - while(states.players.at(playerColor).makingTurn && lobby->state == EServerState::GAMEPLAY) - { - turnTimerHandler.onPlayerMakingTurn(gs->players[playerColor], waitTime); - if(gs->curB) - turnTimerHandler.onBattleLoop(waitTime); - - states.cv.wait_for(lock, boost::chrono::milliseconds(waitTime)); - } - } - } - //additional check that game is not finished - bool activePlayer = false; - for (auto player : playerTurnOrder) - { - if (gs->players[player].status == EPlayerStatus::INGAME) - activePlayer = true; - } - if(!activePlayer) - lobby->state = EServerState::GAMEPLAY_ENDED; + boost::this_thread::sleep_for(boost::chrono::milliseconds(waitTime)); } } -std::list CGameHandler::generatePlayerTurnOrder() const -{ - // Generate player turn order - std::list playerTurnOrder; - - for (const auto & player : gs->players) // add human players first - { - if (player.second.human) - playerTurnOrder.push_back(player.first); - } - for (const auto & player : gs->players) // then add non-human players - { - if (!player.second.human) - playerTurnOrder.push_back(player.first); - } - return playerTurnOrder; -} - void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) { if (!h->hasSpellbook()) @@ -3617,7 +3501,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) if(p->human) { - lobby->state = EServerState::GAMEPLAY_ENDED; + lobby->setState(EServerState::GAMEPLAY_ENDED); } } else @@ -3662,12 +3546,11 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) } auto playerInfo = getPlayerState(gs->currentPlayer, false); + // If we are called before the actual game start, there might be no current player + // If player making turn has lost his turn must be over as well if (playerInfo && playerInfo->status != EPlayerStatus::INGAME) - { - // If player making turn has lost his turn must be over as well - states.setFlag(gs->currentPlayer, &PlayerStatus::makingTurn, false); - } + turnOrder->onPlayerEndsTurn(gs->currentPlayer); } } @@ -4229,15 +4112,6 @@ void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_ sendAndApply(&no); } -void CGameHandler::deserializationFix() -{ - //FIXME: pointer to GameHandler itself can't be deserialized at the moment since GameHandler is top-level entity in serialization - // restore any places that requires such pointer manually - heroPool->gameHandler = this; - battles->setGameHandler(this); - playerMessages->gameHandler = this; -} - void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) { battles->startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 8d5f354ab..555257537 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -49,36 +49,10 @@ class CVCMIServer; class CBaseForGHApply; class PlayerMessageProcessor; class BattleProcessor; +class TurnOrderProcessor; class QueriesProcessor; class CObjectVisitQuery; -struct PlayerStatus -{ - bool makingTurn; - - PlayerStatus():makingTurn(false){}; - template void serialize(Handler &h, const int version) - { - h & makingTurn; - } -}; -class PlayerStatuses -{ -public: - std::map players; - boost::mutex mx; - boost::condition_variable cv; //notifies when any changes are made - - void addPlayer(PlayerColor player); - PlayerStatus operator[](PlayerColor player); - bool checkFlag(PlayerColor player, bool PlayerStatus::*flag); - void setFlag(PlayerColor player, bool PlayerStatus::*flag, bool val); - template void serialize(Handler &h, const int version) - { - h & players; - } -}; - class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment { CVCMIServer * lobby; @@ -90,6 +64,7 @@ public: std::unique_ptr heroPool; std::unique_ptr battles; std::unique_ptr queries; + std::unique_ptr turnOrder; //use enums as parameters, because doMove(sth, true, false, true) is not readable enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; @@ -99,13 +74,11 @@ public: std::unique_ptr playerMessages; std::map>> connections; //player color -> connection to client with interface of that player - PlayerStatuses states; //player color -> player state //queries stuff boost::recursive_mutex gsm; ui32 QID; - SpellCastEnvironment * spellEnv; TurnTimerHandler turnTimerHandler; @@ -237,6 +210,10 @@ public: void save(const std::string &fname); bool load(const std::string &fname); + void onPlayerTurnStarted(PlayerColor which); + void onPlayerTurnEnded(PlayerColor which); + void onNewTurn(); + void handleTimeEvents(); void handleTownEvents(CGTownInstance *town, NewTurn &n); bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true @@ -248,14 +225,11 @@ public: template void serialize(Handler &h, const int version) { h & QID; - h & states; - h & battles; - h & heroPool; h & getRandomGenerator(); - h & playerMessages; - - if (!h.saving) - deserializationFix(); + h & *battles; + h & *heroPool; + h & *playerMessages; + h & *turnOrder; #if SCRIPTING_ENABLED JsonNode scriptsState; @@ -282,7 +256,6 @@ public: bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); void run(bool resume); - void newTurn(); bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); void spawnWanderingMonsters(CreatureID creatureID); @@ -298,8 +271,6 @@ public: scripting::Pool * getContextPool() const override; #endif - std::list generatePlayerTurnOrder() const; - friend class CVCMIServer; private: std::unique_ptr serverEventBus; @@ -308,7 +279,6 @@ private: #endif void reinitScripting(); - void deserializationFix(); void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; diff --git a/server/CMakeLists.txt b/server/CMakeLists.txt index 7bd113de6..0263fef96 100644 --- a/server/CMakeLists.txt +++ b/server/CMakeLists.txt @@ -13,6 +13,7 @@ set(server_SRCS processors/HeroPoolProcessor.cpp processors/PlayerMessageProcessor.cpp + processors/TurnOrderProcessor.cpp CGameHandler.cpp ServerSpellCastEnvironment.cpp @@ -37,6 +38,7 @@ set(server_HEADERS processors/HeroPoolProcessor.h processors/PlayerMessageProcessor.h + processors/TurnOrderProcessor.h CGameHandler.h ServerSpellCastEnvironment.h diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index c00a0b801..d72f78d0c 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -158,6 +158,16 @@ CVCMIServer::~CVCMIServer() announceLobbyThread->join(); } +void CVCMIServer::setState(EServerState value) +{ + state.store(value); +} + +EServerState CVCMIServer::getState() const +{ + return state.load(); +} + void CVCMIServer::run() { if(!restartGameplay) @@ -1159,7 +1169,7 @@ int main(int argc, const char * argv[]) try { - while(server.state != EServerState::SHUTDOWN) + while(server.getState() != EServerState::SHUTDOWN) { server.run(); } @@ -1168,7 +1178,7 @@ int main(int argc, const char * argv[]) catch(boost::system::system_error & e) //for boost errors just log, not crash - probably client shut down connection { logNetwork->error(e.what()); - server.state = EServerState::SHUTDOWN; + server.setState(EServerState::SHUTDOWN); } } catch(boost::system::system_error & e) diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 5ecd942d5..beb3be62b 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -56,10 +56,10 @@ class CVCMIServer : public LobbyInfo boost::recursive_mutex mx; std::shared_ptr> applier; std::unique_ptr announceLobbyThread, remoteConnectionsThread; + std::atomic state; public: std::shared_ptr gh; - std::atomic state; ui16 port; boost::program_options::variables_map cmdLineOptions; @@ -101,6 +101,9 @@ public: void updateAndPropagateLobbyState(); + void setState(EServerState value); + EServerState getState() const; + // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 54c31236a..2f2695c11 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -53,11 +53,11 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyClientConnected(LobbyClie } } - if(srv.state == EServerState::LOBBY) - { + if(srv.getState() == EServerState::LOBBY) + { result = true; return; - } + } //disconnect immediately and ignore this client srv.connections.erase(pack.c); @@ -115,7 +115,7 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientConnected(LobbyCl // Until UUID set we only pass LobbyClientConnected to this client pack.c->uuid = pack.uuid; srv.updateAndPropagateLobbyState(); - if(srv.state == EServerState::GAMEPLAY) + if(srv.getState() == EServerState::GAMEPLAY) { //immediately start game std::unique_ptr startGameForReconnectedPlayer(new LobbyStartGame); @@ -173,13 +173,13 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb if(pack.shutdownServer) { logNetwork->info("Client requested shutdown, server will close itself..."); - srv.state = EServerState::SHUTDOWN; + srv.setState(EServerState::SHUTDOWN); return; } else if(srv.connections.empty()) { logNetwork->error("Last connection lost, server will close itself..."); - srv.state = EServerState::SHUTDOWN; + srv.setState(EServerState::SHUTDOWN); } else if(pack.c == srv.hostClient) { @@ -198,7 +198,7 @@ void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMess void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack) { - if(srv.state != EServerState::LOBBY) + if(srv.getState() != EServerState::LOBBY) { result = false; return; @@ -300,7 +300,7 @@ void ApplyOnServerNetPackVisitor::visitLobbyStartGame(LobbyStartGame & pack) pack.initializedStartInfo = std::make_shared(*srv.gh->getStartInfo(true)); pack.initializedGameState = srv.gh->gameState(); - srv.state = EServerState::GAMEPLAY_STARTING; + srv.setState(EServerState::GAMEPLAY_STARTING); result = true; } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index b0b61674c..b49392edf 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -14,6 +14,7 @@ #include "battles/BattleProcessor.h" #include "processors/HeroPoolProcessor.h" #include "processors/PlayerMessageProcessor.h" +#include "processors/TurnOrderProcessor.h" #include "queries/QueriesProcessor.h" #include "../lib/IGameCallback.h" @@ -36,24 +37,10 @@ void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) { - PlayerColor currentPlayer = gs.currentPlayer; - if(pack.player != currentPlayer) - { - if(gh.getPlayerStatus(pack.player) == EPlayerStatus::INGAME) - gh.throwAndComplain(&pack, "pack.player attempted to end turn for another pack.player!"); + if (!gh.hasPlayerAt(pack.player, pack.c)) + gh.throwAndComplain(&pack, "No such pack.player!"); - logGlobal->debug("pack.player attempted to end turn after game over. Ignoring this request."); - - result = true; - return; - } - - gh.throwOnWrongPlayer(&pack, pack.player); - if(gh.queries->topQuery(pack.player)) - gh.throwAndComplain(&pack, "Cannot end turn before resolving queries!"); - - gh.states.setFlag(gs.currentPlayer, &PlayerStatus::makingTurn, false); - result = true; + result = gh.turnOrder->onPlayerEndsTurn(pack.player); } void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) @@ -274,8 +261,6 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) if(pack.qid == QueryID(-1)) gh.throwAndComplain(&pack, "Cannot answer the query with pack.id -1!"); - assert(vstd::contains(gh.states.players, pack.player)); - result = gh.queryReply(pack.qid, pack.reply, pack.player); } diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index 821046418..e338cf00d 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -16,11 +16,10 @@ class ApplyGhNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) private: bool result; CGameHandler & gh; - CGameState & gs; public: - ApplyGhNetPackVisitor(CGameHandler & gh, CGameState & gs) - :gh(gh), gs(gs), result(false) + ApplyGhNetPackVisitor(CGameHandler & gh) + :gh(gh), result(false) { } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index f61b4e4d8..d2e568273 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -12,6 +12,7 @@ #include "CGameHandler.h" #include "battles/BattleProcessor.h" #include "queries/QueriesProcessor.h" +#include "processors/TurnOrderProcessor.h" #include "../lib/battle/BattleInfo.h" #include "../lib/gameState/CGameState.h" #include "../lib/CPlayerState.h" @@ -83,8 +84,8 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) state.turnTimer.baseTimer = 0; onPlayerMakingTurn(state, waitTime); } - else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries - gameHandler.states.players.at(state.color).makingTurn = false; //force end turn + else if(!gameHandler.queries->topQuery(state.color)) + gameHandler.turnOrder->onPlayerEndsTurn(state.color); } } diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 0f0837a47..8d24f15a0 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -286,7 +286,6 @@ const CStack * BattleFlowProcessor::getNextStack() void BattleFlowProcessor::activateNextStack() { - //TODO: activate next round if next == nullptr const auto & curB = *gameHandler->gameState()->curB; // Find next stack that requires manual control diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 180234388..47c592876 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "HeroPoolProcessor.h" +#include "TurnOrderProcessor.h" #include "../CGameHandler.h" #include "../../lib/CHeroHandler.h" @@ -32,29 +33,6 @@ HeroPoolProcessor::HeroPoolProcessor(CGameHandler * gameHandler) { } -bool HeroPoolProcessor::playerEndedTurn(const PlayerColor & player) -{ - // our player is acting right now and have not ended turn - if (player == gameHandler->gameState()->currentPlayer) - return false; - - auto turnOrder = gameHandler->generatePlayerTurnOrder(); - - for (auto const & entry : turnOrder) - { - // our player is yet to start turn - if (entry == gameHandler->gameState()->currentPlayer) - return false; - - // our player have finished turn - if (entry == player) - return true; - } - - assert(false); - return false; -} - TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID) { const auto & heroesPool = gameHandler->gameState()->heroesPool; @@ -90,7 +68,7 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (playerEndedTurn(color)) + if (gameHandler->turnOrder->playerAwaitsNewDay(color)) sah.roleID = TavernSlotRole::SURRENDERED_TODAY; else sah.roleID = TavernSlotRole::SURRENDERED; @@ -104,7 +82,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (playerEndedTurn(color)) + if (gameHandler->turnOrder->playerAwaitsNewDay(color)) sah.roleID = TavernSlotRole::RETREATED_TODAY; else sah.roleID = TavernSlotRole::RETREATED; diff --git a/server/processors/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h index dc69a7cf3..3cf48ca5f 100644 --- a/server/processors/HeroPoolProcessor.h +++ b/server/processors/HeroPoolProcessor.h @@ -43,7 +43,6 @@ class HeroPoolProcessor : boost::noncopyable TavernHeroSlot selectSlotForRole(const PlayerColor & player, TavernSlotRole roleID); - bool playerEndedTurn(const PlayerColor & player); public: CGameHandler * gameHandler; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 75e4217fc..26c94c835 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -71,7 +71,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st if(words[1] == "exit" || words[1] == "quit" || words[1] == "end") { broadcastSystemMessage("game was terminated"); - gameHandler->gameLobby()->state = EServerState::SHUTDOWN; + gameHandler->gameLobby()->setState(EServerState::SHUTDOWN); return true; } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp new file mode 100644 index 000000000..bd8b0a95e --- /dev/null +++ b/server/processors/TurnOrderProcessor.cpp @@ -0,0 +1,183 @@ +/* + * TurnOrderProcessor.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 "TurnOrderProcessor.h" + +#include "../queries/QueriesProcessor.h" +#include "../CGameHandler.h" +#include "../CVCMIServer.h" + +#include "../../lib/CPlayerState.h" +#include "../../lib/NetPacks.h" + +TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): + gameHandler(owner) +{ + +} + +bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor waiting) const +{ + return false; +} + +bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) const +{ + const auto * leftInfo = gameHandler->getPlayerState(left, false); + const auto * rightInfo = gameHandler->getPlayerState(right, false); + + assert(left != right); + assert(leftInfo && rightInfo); + + if (!leftInfo) + return false; + if (!rightInfo) + return true; + + if (leftInfo->isHuman() && !rightInfo->isHuman()) + return true; + + if (!leftInfo->isHuman() && rightInfo->isHuman()) + return false; + + return left < right; +} + +bool TurnOrderProcessor::canStartTurn(PlayerColor which) const +{ + for (auto player : awaitingPlayers) + { + if (mustActBefore(player, which)) + return false; + } + + for (auto player : actingPlayers) + { + if (!canActSimultaneously(player, which)) + return false; + } + + return true; +} + +void TurnOrderProcessor::doStartNewDay() +{ + assert(awaitingPlayers.empty()); + assert(actingPlayers.empty()); + + bool activePlayer = false; + for (auto player : actedPlayers) + { + if (gameHandler->getPlayerState(player)->status == EPlayerStatus::INGAME) + activePlayer = true; + } + + if(!activePlayer) + gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED); + + std::swap(actedPlayers, awaitingPlayers); +} + +void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) +{ + //if player runs out of time, he shouldn't get the turn (especially AI) + //pre-trigger may change anything, should check before each player + //TODO: is it enough to check only one player? + gameHandler->checkVictoryLossConditionsForAll(); + + assert(gameHandler->getPlayerState(which)); + assert(gameHandler->getPlayerState(which)->status == EPlayerStatus::INGAME); + + gameHandler->onPlayerTurnStarted(which); + + YourTurn yt; + yt.player = which; + //Change local daysWithoutCastle counter for local interface message //TODO: needed? + yt.daysWithoutCastle = gameHandler->getPlayerState(which)->daysWithoutCastle; + gameHandler->sendAndApply(&yt); +} + +void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) +{ + assert(playerMakingTurn(which)); + + actingPlayers.erase(which); + actedPlayers.insert(which); + + if (!awaitingPlayers.empty()) + tryStartTurnsForPlayers(); + + if (actingPlayers.empty()) + doStartNewDay(); +} + +void TurnOrderProcessor::addPlayer(PlayerColor which) +{ + awaitingPlayers.insert(which); +} + +bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) +{ + if (!playerMakingTurn(which)) + { + gameHandler->complain("Can not end turn for player that is not acting!"); + return false; + } + + if(gameHandler->getPlayerStatus(which) != EPlayerStatus::INGAME) + { + gameHandler->complain("Can not end turn for player that is not in game!"); + return false; + } + + if(gameHandler->queries->topQuery(which) != nullptr) + { + gameHandler->complain("Cannot end turn before resolving queries!"); + return false; + } + + doEndPlayerTurn(which); + return true; +} + +void TurnOrderProcessor::onGameStarted() +{ + tryStartTurnsForPlayers(); + + // this may be game load - send notification to players that they can act + auto actingPlayersCopy = actingPlayers; + for (auto player : actingPlayersCopy) + doStartPlayerTurn(player); +} + +void TurnOrderProcessor::tryStartTurnsForPlayers() +{ + auto awaitingPlayersCopy = awaitingPlayers; + for (auto player : awaitingPlayersCopy) + { + if (canStartTurn(player)) + doStartPlayerTurn(player); + } +} + +bool TurnOrderProcessor::playerAwaitsTurn(PlayerColor which) const +{ + return vstd::contains(awaitingPlayers, which); +} + +bool TurnOrderProcessor::playerMakingTurn(PlayerColor which) const +{ + return vstd::contains(actingPlayers, which); +} + +bool TurnOrderProcessor::playerAwaitsNewDay(PlayerColor which) const +{ + return vstd::contains(actedPlayers, which); +} diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h new file mode 100644 index 000000000..69a95ec71 --- /dev/null +++ b/server/processors/TurnOrderProcessor.h @@ -0,0 +1,67 @@ +/* + * TurnOrderProcessor.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/GameConstants.h" + +class CGameHandler; + +class TurnOrderProcessor : boost::noncopyable +{ + CGameHandler * gameHandler; + + std::set awaitingPlayers; + std::set actingPlayers; + std::set actedPlayers; + + /// Returns true if waiting player can act alongside with currently acting player + bool canActSimultaneously(PlayerColor active, PlayerColor waiting) const; + + /// Returns true if left player must act before right player + bool mustActBefore(PlayerColor left, PlayerColor right) const; + + /// Returns true if player is ready to start turn + bool canStartTurn(PlayerColor which) const; + + /// Starts turn for all players that can start turn + void tryStartTurnsForPlayers(); + + void doStartNewDay(); + void doStartPlayerTurn(PlayerColor which); + void doEndPlayerTurn(PlayerColor which); + +public: + TurnOrderProcessor(CGameHandler * owner); + + /// Add new player to handle (e.g. on game start) + void addPlayer(PlayerColor which); + + /// NetPack call-in + bool onPlayerEndsTurn(PlayerColor which); + + /// Start game (or resume from save) and send YourTurn pack to player(s) + void onGameStarted(); + + /// Returns true if player turn has not started today + bool playerAwaitsTurn(PlayerColor which) const; + + /// Returns true if player is currently making his turn + bool playerMakingTurn(PlayerColor which) const; + + /// Returns true if player has finished his turn and is waiting for new day + bool playerAwaitsNewDay(PlayerColor which) const; + + template void serialize(Handler &h, const int version) + { + h & awaitingPlayers; + h & actingPlayers; + h & actedPlayers; + } +}; From d83aa828f63597b0afd07c925056edc927108822 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 23 Aug 2023 18:46:30 +0300 Subject: [PATCH 0169/1248] Fix turn ending --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 3 +- include/vcmi/events/PlayerGotTurn.h | 2 +- lib/events/PlayerGotTurn.cpp | 3 +- server/CGameHandler.cpp | 5 ++-- server/NetPacksServer.cpp | 2 +- server/TurnTimerHandler.cpp | 2 +- server/processors/TurnOrderProcessor.cpp | 37 ++++++++++++++++-------- server/processors/TurnOrderProcessor.h | 11 +++++-- 8 files changed, 43 insertions(+), 22 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 19f48ed65..11bdeecd5 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -225,7 +225,8 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, if(weakest->count == 1) { - assert(resultingArmy.size() > 1); + if (resultingArmy.size() == 1) + logAi->warn("Unexpected resulting army size!"); resultingArmy.erase(weakest); } diff --git a/include/vcmi/events/PlayerGotTurn.h b/include/vcmi/events/PlayerGotTurn.h index 162300701..278e03c88 100644 --- a/include/vcmi/events/PlayerGotTurn.h +++ b/include/vcmi/events/PlayerGotTurn.h @@ -29,7 +29,7 @@ public: using ExecHandler = Sub::ExecHandler; static Sub * getRegistry(); - static void defaultExecute(const EventBus * bus, PlayerColor & player); + static void defaultExecute(const EventBus * bus, const PlayerColor & player); virtual PlayerColor getPlayer() const = 0; virtual void setPlayer(const PlayerColor & value) = 0; diff --git a/lib/events/PlayerGotTurn.cpp b/lib/events/PlayerGotTurn.cpp index e4a259e99..ccbffecc5 100644 --- a/lib/events/PlayerGotTurn.cpp +++ b/lib/events/PlayerGotTurn.cpp @@ -24,12 +24,11 @@ SubscriptionRegistry * PlayerGotTurn::getRegistry() return Instance.get(); } -void PlayerGotTurn::defaultExecute(const EventBus * bus, PlayerColor & player) +void PlayerGotTurn::defaultExecute(const EventBus * bus, const PlayerColor & player) { CPlayerGotTurn event; event.setPlayer(player); bus->executeEvent(event); - player = event.getPlayer(); } CPlayerGotTurn::CPlayerGotTurn() = default; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3253d1cf4..4f2a51f48 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -609,7 +609,8 @@ void CGameHandler::onPlayerTurnStarted(PlayerColor which) void CGameHandler::onPlayerTurnEnded(PlayerColor which) { - + // 7 days without castle + checkVictoryLossConditionsForPlayer(which); } void CGameHandler::onNewTurn() @@ -3550,7 +3551,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) // If we are called before the actual game start, there might be no current player // If player making turn has lost his turn must be over as well if (playerInfo && playerInfo->status != EPlayerStatus::INGAME) - turnOrder->onPlayerEndsTurn(gs->currentPlayer); + turnOrder->onPlayerEndsTurn(gs->currentPlayer, PlayerTurnEndReason::GAME_END); } } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index b49392edf..69f11d885 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -40,7 +40,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) if (!gh.hasPlayerAt(pack.player, pack.c)) gh.throwAndComplain(&pack, "No such pack.player!"); - result = gh.turnOrder->onPlayerEndsTurn(pack.player); + result = gh.turnOrder->onPlayerEndsTurn(pack.player, PlayerTurnEndReason::CLIENT_REQUEST); } void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index d2e568273..021ee59e1 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -85,7 +85,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) onPlayerMakingTurn(state, waitTime); } else if(!gameHandler.queries->topQuery(state.color)) - gameHandler.turnOrder->onPlayerEndsTurn(state.color); + gameHandler.turnOrder->onPlayerEndsTurn(state.color, PlayerTurnEndReason::TURN_TIMEOUT); } } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index bd8b0a95e..5cbaea310 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -54,7 +54,7 @@ bool TurnOrderProcessor::canStartTurn(PlayerColor which) const { for (auto player : awaitingPlayers) { - if (mustActBefore(player, which)) + if (player != which && mustActBefore(player, which)) return false; } @@ -83,18 +83,19 @@ void TurnOrderProcessor::doStartNewDay() gameHandler->gameLobby()->setState(EServerState::GAMEPLAY_ENDED); std::swap(actedPlayers, awaitingPlayers); + + gameHandler->onNewTurn(); + tryStartTurnsForPlayers(); } void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) { - //if player runs out of time, he shouldn't get the turn (especially AI) - //pre-trigger may change anything, should check before each player - //TODO: is it enough to check only one player? - gameHandler->checkVictoryLossConditionsForAll(); - assert(gameHandler->getPlayerState(which)); assert(gameHandler->getPlayerState(which)->status == EPlayerStatus::INGAME); + //Note: on game load, "actingPlayer" might already contain list of players + actingPlayers.insert(which); + awaitingPlayers.erase(which); gameHandler->onPlayerTurnStarted(which); YourTurn yt; @@ -102,20 +103,28 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) //Change local daysWithoutCastle counter for local interface message //TODO: needed? yt.daysWithoutCastle = gameHandler->getPlayerState(which)->daysWithoutCastle; gameHandler->sendAndApply(&yt); + + assert(actingPlayers.size() == 1); // No simturns yet :( + assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); } -void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) +void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which, PlayerTurnEndReason reason) { assert(playerMakingTurn(which)); actingPlayers.erase(which); - actedPlayers.insert(which); + if (reason != PlayerTurnEndReason::GAME_END) + actedPlayers.insert(which); if (!awaitingPlayers.empty()) tryStartTurnsForPlayers(); if (actingPlayers.empty()) doStartNewDay(); + + assert(!actingPlayers.empty()); + assert(actingPlayers.size() == 1); // No simturns yet :( + assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); } void TurnOrderProcessor::addPlayer(PlayerColor which) @@ -123,7 +132,7 @@ void TurnOrderProcessor::addPlayer(PlayerColor which) awaitingPlayers.insert(which); } -bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) +bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which, PlayerTurnEndReason reason) { if (!playerMakingTurn(which)) { @@ -143,18 +152,22 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) return false; } - doEndPlayerTurn(which); + if (reason != PlayerTurnEndReason::GAME_END) + gameHandler->onPlayerTurnEnded(which); + + doEndPlayerTurn(which, reason); + return true; } void TurnOrderProcessor::onGameStarted() { - tryStartTurnsForPlayers(); - // this may be game load - send notification to players that they can act auto actingPlayersCopy = actingPlayers; for (auto player : actingPlayersCopy) doStartPlayerTurn(player); + + tryStartTurnsForPlayers(); } void TurnOrderProcessor::tryStartTurnsForPlayers() diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index 69a95ec71..fd524d575 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -13,6 +13,13 @@ class CGameHandler; +enum class PlayerTurnEndReason +{ + CLIENT_REQUEST, // client requested end of turn (e.g. press End Turn button) + TURN_TIMEOUT, // Player's turn timer has run out + GAME_END // Player have won or lost the game +}; + class TurnOrderProcessor : boost::noncopyable { CGameHandler * gameHandler; @@ -35,7 +42,7 @@ class TurnOrderProcessor : boost::noncopyable void doStartNewDay(); void doStartPlayerTurn(PlayerColor which); - void doEndPlayerTurn(PlayerColor which); + void doEndPlayerTurn(PlayerColor which, PlayerTurnEndReason reason); public: TurnOrderProcessor(CGameHandler * owner); @@ -44,7 +51,7 @@ public: void addPlayer(PlayerColor which); /// NetPack call-in - bool onPlayerEndsTurn(PlayerColor which); + bool onPlayerEndsTurn(PlayerColor which, PlayerTurnEndReason reason); /// Start game (or resume from save) and send YourTurn pack to player(s) void onGameStarted(); From e40dc76304849d9e20990bf954eac8b48fbc04ad Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 13:35:06 +0300 Subject: [PATCH 0170/1248] Fix giving commands in battles in hotseat --- client/battle/BattleInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 5bc4133ca..deb640985 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -265,7 +265,7 @@ void BattleInterface::sendCommand(BattleAction command, const CStack * actor) { logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); stacksController->setActiveStack(nullptr); - LOCPLINT->cb->battleMakeUnitAction(command); + curInt->cb->battleMakeUnitAction(command); } else { From 97ef69c9ab8bc877a89883758ffacc82683ed9b6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 13:35:37 +0300 Subject: [PATCH 0171/1248] Remove incorrect message on another player defeat --- client/CPlayerInterface.cpp | 9 --------- 1 file changed, 9 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index c69bd1f75..a5a9a9c31 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1625,15 +1625,6 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul if (GH.curInt == this) GH.curInt = nullptr; } - else - { - if (victoryLossCheckResult.loss() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME) //enemy has lost - { - MetaString message = victoryLossCheckResult.messageToSelf; - message.appendLocalString(EMetaText::COLOR, player.getNum()); - showInfoDialog(message.toString(), std::vector>(1, std::make_shared(CComponent::flag, player.getNum(), 0))); - } - } } void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) From f451c1593282d5effd744c61f6c4187331111e0e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 13:36:01 +0300 Subject: [PATCH 0172/1248] Fix handling of retreating of AI's --- server/battles/BattleProcessor.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 50f2a31b2..b7e7a8e1f 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -219,7 +219,8 @@ void BattleProcessor::updateGateState() bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) { bool result = actionsProcessor->makePlayerBattleAction(player, ba); - flowProcessor->onActionMade(ba); + if (!resultProcessor->battleIsEnding()) + flowProcessor->onActionMade(ba); return result; } From a19cdb57bae870a68aa6fe1ad5f7a634311eb5c5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 13:36:35 +0300 Subject: [PATCH 0173/1248] Fix handling of turn order in case of player defeat --- server/CGameHandler.cpp | 9 ++------ server/NetPacksServer.cpp | 2 +- server/TurnTimerHandler.cpp | 2 +- server/processors/TurnOrderProcessor.cpp | 29 ++++++++++++++++++------ server/processors/TurnOrderProcessor.h | 13 ++++------- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4f2a51f48..4ae780090 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3480,6 +3480,8 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) peg.victoryLossCheckResult = victoryLossCheckResult; sendAndApply(&peg); + turnOrder->onPlayerEndsGame(player); + if (victoryLossCheckResult.victory()) { //one player won -> all enemies lost @@ -3545,13 +3547,6 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) } checkVictoryLossConditions(playerColors); } - - auto playerInfo = getPlayerState(gs->currentPlayer, false); - - // If we are called before the actual game start, there might be no current player - // If player making turn has lost his turn must be over as well - if (playerInfo && playerInfo->status != EPlayerStatus::INGAME) - turnOrder->onPlayerEndsTurn(gs->currentPlayer, PlayerTurnEndReason::GAME_END); } } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 69f11d885..b49392edf 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -40,7 +40,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) if (!gh.hasPlayerAt(pack.player, pack.c)) gh.throwAndComplain(&pack, "No such pack.player!"); - result = gh.turnOrder->onPlayerEndsTurn(pack.player, PlayerTurnEndReason::CLIENT_REQUEST); + result = gh.turnOrder->onPlayerEndsTurn(pack.player); } void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 021ee59e1..d2e568273 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -85,7 +85,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) onPlayerMakingTurn(state, waitTime); } else if(!gameHandler.queries->topQuery(state.color)) - gameHandler.turnOrder->onPlayerEndsTurn(state.color, PlayerTurnEndReason::TURN_TIMEOUT); + gameHandler.turnOrder->onPlayerEndsTurn(state.color); } } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 5cbaea310..165d529c6 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -108,13 +108,12 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); } -void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which, PlayerTurnEndReason reason) +void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) { assert(playerMakingTurn(which)); actingPlayers.erase(which); - if (reason != PlayerTurnEndReason::GAME_END) - actedPlayers.insert(which); + actedPlayers.insert(which); if (!awaitingPlayers.empty()) tryStartTurnsForPlayers(); @@ -132,7 +131,24 @@ void TurnOrderProcessor::addPlayer(PlayerColor which) awaitingPlayers.insert(which); } -bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which, PlayerTurnEndReason reason) +void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) +{ + awaitingPlayers.erase(which); + actingPlayers.erase(which); + actedPlayers.erase(which); + + if (!awaitingPlayers.empty()) + tryStartTurnsForPlayers(); + + if (actingPlayers.empty()) + doStartNewDay(); + + assert(!actingPlayers.empty()); + assert(actingPlayers.size() == 1); // No simturns yet :( + assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); +} + +bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) { if (!playerMakingTurn(which)) { @@ -152,10 +168,9 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which, PlayerTurnEndReason return false; } - if (reason != PlayerTurnEndReason::GAME_END) - gameHandler->onPlayerTurnEnded(which); + gameHandler->onPlayerTurnEnded(which); - doEndPlayerTurn(which, reason); + doEndPlayerTurn(which); return true; } diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index fd524d575..ea6e7676b 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -13,13 +13,6 @@ class CGameHandler; -enum class PlayerTurnEndReason -{ - CLIENT_REQUEST, // client requested end of turn (e.g. press End Turn button) - TURN_TIMEOUT, // Player's turn timer has run out - GAME_END // Player have won or lost the game -}; - class TurnOrderProcessor : boost::noncopyable { CGameHandler * gameHandler; @@ -42,7 +35,7 @@ class TurnOrderProcessor : boost::noncopyable void doStartNewDay(); void doStartPlayerTurn(PlayerColor which); - void doEndPlayerTurn(PlayerColor which, PlayerTurnEndReason reason); + void doEndPlayerTurn(PlayerColor which); public: TurnOrderProcessor(CGameHandler * owner); @@ -51,7 +44,9 @@ public: void addPlayer(PlayerColor which); /// NetPack call-in - bool onPlayerEndsTurn(PlayerColor which, PlayerTurnEndReason reason); + bool onPlayerEndsTurn(PlayerColor which); + + void onPlayerEndsGame(PlayerColor which); /// Start game (or resume from save) and send YourTurn pack to player(s) void onGameStarted(); From b6d8c7d4a55236307dedcf043c78fc26c2b5fddb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 18:48:15 +0300 Subject: [PATCH 0174/1248] Fix retreating in player-with-player battles --- server/battles/BattleProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index b7e7a8e1f..445fa5007 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -219,7 +219,7 @@ void BattleProcessor::updateGateState() bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) { bool result = actionsProcessor->makePlayerBattleAction(player, ba); - if (!resultProcessor->battleIsEnding()) + if (!resultProcessor->battleIsEnding() && gameHandler->gameState()->curB != nullptr) flowProcessor->onActionMade(ba); return result; } From c171a3d6be03c30262570c2e3127dfd47c1cd627 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 18:48:47 +0300 Subject: [PATCH 0175/1248] Fix resource bar UI in hotseat --- client/adventureMap/AdventureMapWidget.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index b8f50dcd4..65e1230be 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -372,11 +372,15 @@ void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColo auto container = dynamic_cast(entry); auto icon = dynamic_cast(entry); auto button = dynamic_cast(entry); + auto resDataBar = dynamic_cast(entry); auto texture = dynamic_cast(entry); if(button) button->setPlayerColor(player); + if(resDataBar) + resDataBar->colorize(player); + if(icon) icon->setPlayer(player); From ee8adbe85f01b8494d902ddd3eb9c1d2d89d8079 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 18:53:58 +0300 Subject: [PATCH 0176/1248] Update tavern on end of 7th turn of player in question Allows removal of "retreat after 7th day" workaround and as result - more straightforward code --- lib/gameState/TavernHeroesPool.cpp | 9 ------- lib/gameState/TavernSlot.h | 6 +---- server/CGameHandler.cpp | 11 +++++--- server/processors/HeroPoolProcessor.cpp | 34 +++++-------------------- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index f5e5a6138..70f441f98 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -124,15 +124,6 @@ void TavernHeroesPool::onNewDay() hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); hero.second->mana = hero.second->manaLimit(); } - - for (auto & slot : currentTavern) - { - if (slot.role == TavernSlotRole::RETREATED_TODAY) - slot.role = TavernSlotRole::RETREATED; - - if (slot.role == TavernSlotRole::SURRENDERED_TODAY) - slot.role = TavernSlotRole::SURRENDERED; - } } void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) diff --git a/lib/gameState/TavernSlot.h b/lib/gameState/TavernSlot.h index 192fd047d..698168cff 100644 --- a/lib/gameState/TavernSlot.h +++ b/lib/gameState/TavernSlot.h @@ -24,12 +24,8 @@ enum class TavernSlotRole : int8_t SINGLE_UNIT, // hero was added after buying hero from this slot, and only has 1 creature in army FULL_ARMY, // hero was added to tavern on new week and still has full army - RETREATED, // hero was owned by player before, but have retreated from battle and only has 1 creature in army - RETREATED_TODAY, - - SURRENDERED, // hero was owned by player before, but have surrendered in battle and kept some troops - SURRENDERED_TODAY, + SURRENDERED // hero was owned by player before, but have surrendered in battle and kept some troops }; VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4ae780090..b8ca99c58 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -611,6 +611,11 @@ void CGameHandler::onPlayerTurnEnded(PlayerColor which) { // 7 days without castle checkVictoryLossConditionsForPlayer(which); + + bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day + + if (newWeek) //new heroes in tavern + heroPool->onNewWeek(which); } void CGameHandler::onNewTurn() @@ -702,13 +707,13 @@ void CGameHandler::onNewTurn() { if (elem.first == PlayerColor::NEUTRAL) continue; - else if (elem.first >= PlayerColor::PLAYER_LIMIT) - assert(0); //illegal player number! + + assert(elem.first.isValidPlayer());//illegal player number! std::pair playerGold(elem.first, elem.second.resources[EGameResID::GOLD]); hadGold.insert(playerGold); - if (newWeek) //new heroes in tavern + if (firstTurn) heroPool->onNewWeek(elem.first); n.res[elem.first] = elem.second.resources; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 47c592876..1881f8083 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -68,10 +68,7 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (gameHandler->turnOrder->playerAwaitsNewDay(color)) - sah.roleID = TavernSlotRole::SURRENDERED_TODAY; - else - sah.roleID = TavernSlotRole::SURRENDERED; + sah.roleID = TavernSlotRole::SURRENDERED; sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; @@ -82,10 +79,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroInstance * hero) { SetAvailableHero sah; - if (gameHandler->turnOrder->playerAwaitsNewDay(color)) - sah.roleID = TavernSlotRole::RETREATED_TODAY; - else - sah.roleID = TavernSlotRole::RETREATED; + sah.roleID = TavernSlotRole::RETREATED; sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; @@ -139,26 +133,10 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe void HeroPoolProcessor::onNewWeek(const PlayerColor & color) { - const auto & heroesPool = gameHandler->gameState()->heroesPool; - const auto & heroes = heroesPool->getHeroesFor(color); - - const auto nativeSlotRole = heroes.size() < 1 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[0]->type->getId()); - const auto randomSlotRole = heroes.size() < 2 ? TavernSlotRole::NONE : heroesPool->getSlotRole(heroes[1]->type->getId()); - - bool resetNativeSlot = nativeSlotRole != TavernSlotRole::RETREATED_TODAY && nativeSlotRole != TavernSlotRole::SURRENDERED_TODAY; - bool resetRandomSlot = randomSlotRole != TavernSlotRole::RETREATED_TODAY && randomSlotRole != TavernSlotRole::SURRENDERED_TODAY; - - if (resetNativeSlot) - clearHeroFromSlot(color, TavernHeroSlot::NATIVE); - - if (resetRandomSlot) - clearHeroFromSlot(color, TavernHeroSlot::RANDOM); - - if (resetNativeSlot) - selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true); - - if (resetRandomSlot) - selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); + clearHeroFromSlot(color, TavernHeroSlot::NATIVE); + clearHeroFromSlot(color, TavernHeroSlot::RANDOM); + selectNewHeroForSlot(color, TavernHeroSlot::NATIVE, true, true); + selectNewHeroForSlot(color, TavernHeroSlot::RANDOM, false, true); } bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTypeID & heroToRecruit, const PlayerColor & player) From b057230d854692f8703be38c8bc5470c823bfe6b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 19:10:39 +0300 Subject: [PATCH 0177/1248] Fix quest log button block status --- client/adventureMap/AdventureMapShortcuts.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 19ee7b455..89415023e 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -397,7 +397,7 @@ void AdventureMapShortcuts::moveHeroDirectional(const Point & direction) bool AdventureMapShortcuts::optionCanViewQuests() { - return optionInMapView() && CGI->mh->getMap()->quests.empty(); + return optionInMapView() && !CGI->mh->getMap()->quests.empty(); } bool AdventureMapShortcuts::optionCanToggleLevel() From 66f555f1f7ebde7ab90f172731258342b1bc3653 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 19:10:53 +0300 Subject: [PATCH 0178/1248] Fix error messages on bad morale --- server/battles/BattleActionProcessor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 18da73a5b..878da7417 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -499,6 +499,7 @@ bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) { switch(ba.actionType) { + case EActionType::BAD_MORALE: case EActionType::NO_ACTION: return doEmptyAction(ba); case EActionType::END_TACTIC_PHASE: From f9410145d6f18c731f1f21a9cb471fc4ac4a8732 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 21:35:01 +0300 Subject: [PATCH 0179/1248] Fix handling of "7 days without town" loss condition --- client/ClientCommandManager.cpp | 1 - lib/NetPackVisitor.h | 1 + lib/NetPacks.h | 16 +++++++++- lib/NetPacksLib.cpp | 40 +++++++++++------------- lib/registerTypes/RegisterTypes.h | 1 + server/CGameHandler.cpp | 23 +++++++++++++- server/TurnTimerHandler.cpp | 2 +- server/processors/TurnOrderProcessor.cpp | 8 +++-- server/processors/TurnOrderProcessor.h | 17 +++++----- 9 files changed, 70 insertions(+), 39 deletions(-) diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 9de0ba3d9..c9cce134e 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -471,7 +471,6 @@ void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier) { YourTurn yt; yt.player = colorIdentifier; - yt.daysWithoutCastle = CSH->client->getPlayerState(colorIdentifier)->daysWithoutCastle; ApplyClientNetPackVisitor visitor(*CSH->client, *CSH->client->gameState()); yt.visit(visitor); diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index f0c82b290..bf740e46b 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -27,6 +27,7 @@ public: virtual void visitPlayerBlocked(PlayerBlocked & pack) {} virtual void visitPlayerCheated(PlayerCheated & pack) {} virtual void visitYourTurn(YourTurn & pack) {} + virtual void visitYourTurn(DaysWithoutTown & pack) {} virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} virtual void visitSetResources(SetResources & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 8fd625266..afcbe16c4 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -167,7 +167,21 @@ struct DLL_LINKAGE YourTurn : public CPackForClient void applyGs(CGameState * gs) const; PlayerColor player; - std::optional daysWithoutCastle; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + } +}; + +struct DLL_LINKAGE DaysWithoutTown : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + std::optional daysWithoutCastle; virtual void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 2f1cd86a5..e296c2273 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -108,6 +108,11 @@ void YourTurn::visitTyped(ICPackVisitor & visitor) visitor.visitYourTurn(*this); } +void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitYourTurn(*this); +} + void EntitiesChanged::visitTyped(ICPackVisitor & visitor) { visitor.visitEntitiesChanged(*this); @@ -2025,26 +2030,6 @@ void NewTurn::applyGs(CGameState *gs) if(gs->getDate(Date::DAY_OF_WEEK) == 1) gs->updateRumor(); - - //count days without town for all players, regardless of their turn order - for (auto &p : gs->players) - { - PlayerState & playerState = p.second; - if (playerState.status == EPlayerStatus::INGAME) - { - if (playerState.towns.empty()) - { - if (playerState.daysWithoutCastle) - ++(*playerState.daysWithoutCastle); - else - playerState.daysWithoutCastle = std::make_optional(0); - } - else - { - playerState.daysWithoutCastle = std::nullopt; - } - } - } } void SetObjectProperty::applyGs(CGameState * gs) const @@ -2063,8 +2048,16 @@ void SetObjectProperty::applyGs(CGameState * gs) const { auto * t = dynamic_cast(obj); assert(t); - if(t->tempOwner < PlayerColor::PLAYER_LIMIT) - gs->getPlayerState(t->tempOwner)->towns -= t; + + PlayerColor oldOwner = t->tempOwner; + if(oldOwner.isValidPlayer()) + { + auto * state = gs->getPlayerState(oldOwner); + state->towns -= t; + + if(state->towns.empty()) + *state->daysWithoutCastle = 0; + } if(val < PlayerColor::PLAYER_LIMIT_I) { PlayerState * p = gs->getPlayerState(PlayerColor(val)); @@ -2509,7 +2502,10 @@ void PlayerCheated::applyGs(CGameState * gs) const void YourTurn::applyGs(CGameState * gs) const { gs->currentPlayer = player; +} +void DaysWithoutTown::applyGs(CGameState * gs) const +{ auto & playerState = gs->players[player]; playerState.daysWithoutCastle = daysWithoutCastle; } diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 88d43967c..8c6feefd2 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -232,6 +232,7 @@ void registerTypesClientPacks1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b8ca99c58..9b51db83e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -609,7 +609,28 @@ void CGameHandler::onPlayerTurnStarted(PlayerColor which) void CGameHandler::onPlayerTurnEnded(PlayerColor which) { - // 7 days without castle + const auto * playerState = gs->getPlayerState(which); + assert(playerState->status == EPlayerStatus::INGAME); + + if (playerState->towns.empty()) + { + DaysWithoutTown pack; + pack.player = which; + pack.daysWithoutCastle = playerState->daysWithoutCastle.value_or(0) + 1; + sendAndApply(&pack); + } + else + { + if (playerState->daysWithoutCastle.has_value()) + { + DaysWithoutTown pack; + pack.player = which; + pack.daysWithoutCastle = std::nullopt; + sendAndApply(&pack); + } + } + + // check for 7 days without castle checkVictoryLossConditionsForPlayer(which); bool newWeek = getDate(Date::DAY_OF_WEEK) == 7; // end of 7th day diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index d2e568273..a3b0c81b5 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -84,7 +84,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) state.turnTimer.baseTimer = 0; onPlayerMakingTurn(state, waitTime); } - else if(!gameHandler.queries->topQuery(state.color)) + else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries gameHandler.turnOrder->onPlayerEndsTurn(state.color); } } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 165d529c6..9790e59d8 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -100,8 +100,6 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) YourTurn yt; yt.player = which; - //Change local daysWithoutCastle counter for local interface message //TODO: needed? - yt.daysWithoutCastle = gameHandler->getPlayerState(which)->daysWithoutCastle; gameHandler->sendAndApply(&yt); assert(actingPlayers.size() == 1); // No simturns yet :( @@ -111,6 +109,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) { assert(playerMakingTurn(which)); + assert(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME); actingPlayers.erase(which); actedPlayers.insert(which); @@ -170,7 +169,10 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) gameHandler->onPlayerTurnEnded(which); - doEndPlayerTurn(which); + // it is possible that player have lost - e.g. spent 7 days without town + // in this case - don't call doEndPlayerTurn - turn transfer was already handled by onPlayerEndsGame + if(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME) + doEndPlayerTurn(which); return true; } diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index ea6e7676b..c4b158704 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -37,6 +37,10 @@ class TurnOrderProcessor : boost::noncopyable void doStartPlayerTurn(PlayerColor which); void doEndPlayerTurn(PlayerColor which); + bool playerAwaitsTurn(PlayerColor which) const; + bool playerMakingTurn(PlayerColor which) const; + bool playerAwaitsNewDay(PlayerColor which) const; + public: TurnOrderProcessor(CGameHandler * owner); @@ -46,21 +50,14 @@ public: /// NetPack call-in bool onPlayerEndsTurn(PlayerColor which); + /// Ends player turn and removes this player from turn order void onPlayerEndsGame(PlayerColor which); /// Start game (or resume from save) and send YourTurn pack to player(s) void onGameStarted(); - /// Returns true if player turn has not started today - bool playerAwaitsTurn(PlayerColor which) const; - - /// Returns true if player is currently making his turn - bool playerMakingTurn(PlayerColor which) const; - - /// Returns true if player has finished his turn and is waiting for new day - bool playerAwaitsNewDay(PlayerColor which) const; - - template void serialize(Handler &h, const int version) + template + void serialize(Handler & h, const int version) { h & awaitingPlayers; h & actingPlayers; From 97ba7df15219db84fc6581bc541f77ee4a7795f0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 22:02:24 +0300 Subject: [PATCH 0180/1248] Fix handling of map turn/day limit --- server/CGameHandler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 9b51db83e..81650e755 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -947,6 +947,9 @@ void CGameHandler::onNewTurn() } } + if (!firstTurn) + checkVictoryLossConditionsForAll(); // check for map turn limit + logGlobal->trace("Info about turn %d has been sent!", n.day); handleTimeEvents(); //call objects From 940bdcee3ed9abdb08a2b436dcdae2abd00e0e4f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 24 Aug 2023 23:34:28 +0300 Subject: [PATCH 0181/1248] Fixes according to review --- lib/NetPackVisitor.h | 2 +- lib/NetPacksLib.cpp | 2 +- server/processors/TurnOrderProcessor.cpp | 10 +++++----- server/processors/TurnOrderProcessor.h | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index bf740e46b..44f25dc1c 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -27,7 +27,7 @@ public: virtual void visitPlayerBlocked(PlayerBlocked & pack) {} virtual void visitPlayerCheated(PlayerCheated & pack) {} virtual void visitYourTurn(YourTurn & pack) {} - virtual void visitYourTurn(DaysWithoutTown & pack) {} + virtual void visitDaysWithoutTown(DaysWithoutTown & pack) {} virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} virtual void visitSetResources(SetResources & pack) {} diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index e296c2273..7a1a90463 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -110,7 +110,7 @@ void YourTurn::visitTyped(ICPackVisitor & visitor) void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) { - visitor.visitYourTurn(*this); + visitor.visitDaysWithoutTown(*this); } void EntitiesChanged::visitTyped(ICPackVisitor & visitor) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 9790e59d8..df57a0167 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -108,7 +108,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) { - assert(playerMakingTurn(which)); + assert(isPlayerMakingTurn(which)); assert(gameHandler->getPlayerStatus(which) == EPlayerStatus::INGAME); actingPlayers.erase(which); @@ -149,7 +149,7 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) { - if (!playerMakingTurn(which)) + if (!isPlayerMakingTurn(which)) { gameHandler->complain("Can not end turn for player that is not acting!"); return false; @@ -197,17 +197,17 @@ void TurnOrderProcessor::tryStartTurnsForPlayers() } } -bool TurnOrderProcessor::playerAwaitsTurn(PlayerColor which) const +bool TurnOrderProcessor::isPlayerAwaitsTurn(PlayerColor which) const { return vstd::contains(awaitingPlayers, which); } -bool TurnOrderProcessor::playerMakingTurn(PlayerColor which) const +bool TurnOrderProcessor::isPlayerMakingTurn(PlayerColor which) const { return vstd::contains(actingPlayers, which); } -bool TurnOrderProcessor::playerAwaitsNewDay(PlayerColor which) const +bool TurnOrderProcessor::isPlayerAwaitsNewDay(PlayerColor which) const { return vstd::contains(actedPlayers, which); } diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index c4b158704..af91bc547 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -37,9 +37,9 @@ class TurnOrderProcessor : boost::noncopyable void doStartPlayerTurn(PlayerColor which); void doEndPlayerTurn(PlayerColor which); - bool playerAwaitsTurn(PlayerColor which) const; - bool playerMakingTurn(PlayerColor which) const; - bool playerAwaitsNewDay(PlayerColor which) const; + bool isPlayerAwaitsTurn(PlayerColor which) const; + bool isPlayerMakingTurn(PlayerColor which) const; + bool isPlayerAwaitsNewDay(PlayerColor which) const; public: TurnOrderProcessor(CGameHandler * owner); From ec8d31bbfc4391c4d9d77b5b5b89431e70cc7385 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 18 Aug 2023 13:38:19 +0300 Subject: [PATCH 0182/1248] First step at unifying game identifiers code --- AI/VCAI/Goals/CollectRes.cpp | 4 +- client/adventureMap/CResDataBar.h | 5 - client/lobby/OptionsTab.cpp | 16 +- include/vcmi/Creature.h | 4 +- include/vcmi/Entity.h | 7 +- include/vcmi/Faction.h | 4 +- include/vcmi/FactionMember.h | 4 +- include/vcmi/spells/Spell.h | 6 +- lib/CCreatureHandler.cpp | 2 +- lib/CGameInfoCallback.cpp | 2 +- lib/CHeroHandler.cpp | 2 +- lib/CTownHandler.cpp | 4 +- lib/CTownHandler.h | 4 +- lib/GameConstants.cpp | 27 +- lib/GameConstants.h | 393 ++++++++++++++----------- lib/ResourceSet.cpp | 6 +- lib/ResourceSet.h | 10 - lib/StringConstants.h | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 2 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/mapping/MapReaderH3M.cpp | 2 +- lib/rmg/CMapGenOptions.cpp | 2 +- lib/rmg/CMapGenOptions.h | 3 - lib/rmg/CMapGenerator.cpp | 2 +- lib/rmg/CRmgTemplate.cpp | 12 +- lib/rmg/CZonePlacer.cpp | 4 +- lib/rmg/modificators/TownPlacer.cpp | 2 +- lib/spells/AdventureSpellMechanics.cpp | 2 +- lib/spells/CSpellHandler.cpp | 2 +- server/CVCMIServer.cpp | 10 +- test/entity/CCreatureTest.cpp | 2 +- test/mock/mock_Creature.h | 2 +- 34 files changed, 294 insertions(+), 261 deletions(-) diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index 8494952e2..d0832c3e3 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -170,10 +170,10 @@ TSubgoal CollectRes::whatToDoToTrade() int howManyCanWeBuy = 0; for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) { - if (GameResID(i) == resID) + if (i.getNum() == resID) continue; int toGive = -1, toReceive = -1; - m->getOffer(GameResID(i), resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); + m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); assert(toGive > 0 && toReceive > 0); howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive); } diff --git a/client/adventureMap/CResDataBar.h b/client/adventureMap/CResDataBar.h index d4696cd26..c9bc01286 100644 --- a/client/adventureMap/CResDataBar.h +++ b/client/adventureMap/CResDataBar.h @@ -11,11 +11,6 @@ #include "../gui/CIntObject.h" -VCMI_LIB_NAMESPACE_BEGIN -enum class EGameResID : int8_t; -using GameResID = Identifier; -VCMI_LIB_NAMESPACE_END - /// Resources bar which shows information about how many gold, crystals,... you have /// Current date is displayed too class CResDataBar : public CIntObject diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 87968c2dd..67dee51f9 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -94,7 +94,7 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall }; - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); switch(type) { @@ -191,7 +191,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; default: { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); return (*CGI->townh)[factionIndex]->getNameTranslated(); } } @@ -233,7 +233,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() switch(type) { case OptionsTab::TOWN: - return (settings.castle < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; + return (settings.castle.getNum() < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; case OptionsTab::HERO: return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; case OptionsTab::BONUS: @@ -255,7 +255,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() } std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); auto heroIndex = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; switch(type) @@ -299,7 +299,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() std::string OptionsTab::CPlayerSettingsHelper::getDescription() { - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); switch(type) { @@ -386,7 +386,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = settings.castle >= CGI->townh->size() ? 0 : settings.castle; + auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; @@ -784,7 +784,7 @@ void OptionsTab::SelectedBox::update() void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) { // cases when we do not need to display a message - if(settings.castle == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) + if(settings.castle.getNum() == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) return; if(settings.hero == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; @@ -932,7 +932,7 @@ void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() buttonTownRight->enable(); } - if((pi->defaultHero() != -1 || s->castle < 0) //fixed hero + if((pi->defaultHero() != -1 || s->castle.getNum() < 0) //fixed hero || foreignPlayer) //or not our player { buttonHeroLeft->disable(); diff --git a/include/vcmi/Creature.h b/include/vcmi/Creature.h index 736d16043..645d144ab 100644 --- a/include/vcmi/Creature.h +++ b/include/vcmi/Creature.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CreatureID; class ResourceSet; -enum class EGameResID : int8_t; +class GameResID; /// Base class for creatures and battle stacks class DLL_LINKAGE ACreature: public AFactionMember @@ -63,7 +63,7 @@ public: virtual int32_t getBaseSpeed() const = 0; virtual int32_t getBaseShots() const = 0; - virtual int32_t getRecruitCost(Identifier resIndex) const = 0; + virtual int32_t getRecruitCost(GameResID resIndex) const = 0; virtual ResourceSet getFullRecruitCost() const = 0; virtual bool hasUpgrades() const = 0; diff --git a/include/vcmi/Entity.h b/include/vcmi/Entity.h index f91130d7b..34ccc2757 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -14,8 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class IBonusBearer; class FactionID; -enum class ETerrainId; -template class Identifier; +class TerrainId; class DLL_LINKAGE IConstBonusProvider { @@ -26,9 +25,9 @@ public: class DLL_LINKAGE INativeTerrainProvider { public: - virtual Identifier getNativeTerrain() const = 0; + virtual TerrainId getNativeTerrain() const = 0; virtual FactionID getFaction() const = 0; - virtual bool isNativeTerrain(Identifier terrain) const; + virtual bool isNativeTerrain(TerrainId terrain) const; }; class DLL_LINKAGE Entity diff --git a/include/vcmi/Faction.h b/include/vcmi/Faction.h index 8d98ae075..2bc959567 100644 --- a/include/vcmi/Faction.h +++ b/include/vcmi/Faction.h @@ -16,14 +16,14 @@ VCMI_LIB_NAMESPACE_BEGIN class FactionID; enum class EAlignment : uint8_t; -enum class EBoatId : int32_t; +class BoatId; class DLL_LINKAGE Faction : public EntityT, public INativeTerrainProvider { public: virtual bool hasTown() const = 0; virtual EAlignment getAlignment() const = 0; - virtual EBoatId getBoatType() const = 0; + virtual BoatId getBoatType() const = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index 722ac7117..508d1092f 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -27,7 +27,7 @@ public: /** Returns native terrain considering some terrain bonuses. */ - virtual Identifier getNativeTerrain() const; + virtual TerrainId getNativeTerrain() const; /** Returns magic resistance considering some bonuses. */ @@ -71,4 +71,4 @@ public: int luckValAndBonusList(std::shared_ptr & bonusList) const; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index bc9a80217..38a9a247b 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class SpellID; -enum class ESpellSchool: int8_t; +class SpellSchool; namespace spells { @@ -24,7 +24,7 @@ class Caster; class DLL_LINKAGE Spell: public EntityT { public: - using SchoolCallback = std::function &, bool &)>; + using SchoolCallback = std::function; ///calculate spell damage on stack taking caster`s secondary skills into account virtual int64_t calculateDamage(const Caster * caster) const = 0; @@ -43,7 +43,7 @@ public: virtual bool isSpecial() const = 0; virtual bool isMagical() const = 0; //Should this spell considered as magical effect or as ability (like dendroid's bind) - virtual bool hasSchool(Identifier school) const = 0; + virtual bool hasSchool(SpellSchool school) const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; virtual const std::string & getCastSound() const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 9640eb199..78de10cdf 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -161,7 +161,7 @@ int32_t CCreature::getBaseShots() const int32_t CCreature::getRecruitCost(GameResID resIndex) const { - if(resIndex >= 0 && resIndex < cost.size()) + if(resIndex.getNum() >= 0 && resIndex.getNum() < cost.size()) return cost[resIndex]; else return 0; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 4784d1e48..e430d2249 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -42,7 +42,7 @@ int CGameInfoCallback::getResource(PlayerColor Player, GameResID which) const { const PlayerState *p = getPlayerState(Player); ERROR_RET_VAL_IF(!p, "No player info!", -1); - ERROR_RET_VAL_IF(p->resources.size() <= which || which < 0, "No such resource!", -1); + ERROR_RET_VAL_IF(p->resources.size() <= which.getNum() || which.getNum() < 0, "No such resource!", -1); return p->resources[which]; } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 78adc3b0e..5bda9995d 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -345,7 +345,7 @@ std::vector CHeroClassHandler::loadLegacyData() for(const auto & name : NSecondarySkill::names) entry["secondarySkills"][name].Float() = parser.readNumber(); - for(const auto & name : ETownType::names) + for(const auto & name : NFaction::names) entry["tavern"][name].Float() = parser.readNumber(); parser.endLine(); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index abff75760..85471ec32 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -184,7 +184,7 @@ EAlignment CFaction::getAlignment() const return alignment; } -EBoatId CFaction::getBoatType() const +BoatId CFaction::getBoatType() const { return boatType.toEnum(); } @@ -1034,7 +1034,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode faction->creatureBg120 = source["creatureBackground"]["120px"].String(); faction->creatureBg130 = source["creatureBackground"]["130px"].String(); - faction->boatType = EBoatId::CASTLE; //Do not crash + faction->boatType = BoatId::CASTLE; //Do not crash if (!source["boat"].isNull()) { VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 66aec6196..0b4b40a08 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -207,7 +207,7 @@ public: /// Boat that will be used by town shipyard (if any) /// and for placing heroes directly on boat (in map editor, water prisons & taverns) - BoatId boatType = BoatId(EBoatId::CASTLE); + BoatId boatType = BoatId::CASTLE; CTown * town = nullptr; //NOTE: can be null @@ -232,7 +232,7 @@ public: bool hasTown() const override; TerrainId getNativeTerrain() const override; EAlignment getAlignment() const override; - EBoatId getBoatType() const override; + BoatId getBoatType() const override; void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index 733596428..f059f9b19 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -191,20 +191,7 @@ std::string PlayerColor::getStrCap(bool L10n) const return ret; } -const FactionID FactionID::NONE = FactionID(-2); -const FactionID FactionID::DEFAULT = FactionID(-1); -const FactionID FactionID::CASTLE = FactionID(0); -const FactionID FactionID::RAMPART = FactionID(1); -const FactionID FactionID::TOWER = FactionID(2); -const FactionID FactionID::INFERNO = FactionID(3); -const FactionID FactionID::NECROPOLIS = FactionID(4); -const FactionID FactionID::DUNGEON = FactionID(5); -const FactionID FactionID::STRONGHOLD = FactionID(6); -const FactionID FactionID::FORTRESS = FactionID(7); -const FactionID FactionID::CONFLUX = FactionID(8); -const FactionID FactionID::NEUTRAL = FactionID(9); - -si32 FactionID::decode(const std::string & identifier) +si32 FactionIDBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) @@ -213,32 +200,32 @@ si32 FactionID::decode(const std::string & identifier) return FactionID::DEFAULT; } -std::string FactionID::encode(const si32 index) +std::string FactionIDBase::encode(const si32 index) { return VLC->factions()->getByIndex(index)->getJsonKey(); } -std::string FactionID::entityType() +std::string FactionIDBase::entityType() { return "faction"; } -si32 TerrainID::decode(const std::string & identifier) +si32 TerrainIdBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) return rawId.value(); else - return static_cast(ETerrainId::NONE); + return static_cast(TerrainId::NONE); } -std::string TerrainID::encode(const si32 index) +std::string TerrainIdBase::encode(const si32 index) { return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); } -std::string TerrainID::entityType() +std::string TerrainIdBase::entityType() { return "terrain"; } diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 23214e1a1..ee023ad61 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -33,6 +33,7 @@ class CSkill; class CGameInfoCallback; class CNonConstInfoCallback; + struct IdTag {}; @@ -203,74 +204,6 @@ public: } }; -template < typename T> -class Identifier : public IdTag -{ -public: - using EnumType = T; - using NumericType = typename std::underlying_type::type; - -private: - NumericType num; - -public: - constexpr NumericType getNum() const - { - return num; - } - - constexpr EnumType toEnum() const - { - return static_cast(num); - } - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr explicit Identifier(NumericType _num = -1) - { - num = _num; - } - - /* implicit */constexpr Identifier(EnumType _num): - num(static_cast(_num)) - { - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr bool operator == (const Identifier & b) const { return num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return num != b.num; } - constexpr bool operator < (const Identifier & b) const { return num < b.num; } - constexpr bool operator > (const Identifier & b) const { return num > b.num; } - - constexpr Identifier & operator++() - { - ++num; - return *this; - } - - constexpr Identifier operator++(int) - { - Identifier ret(*this); - ++num; - return ret; - } - - constexpr operator NumericType() const - { - return num; - } -}; - - template std::ostream & operator << (std::ostream & os, BaseForID id); @@ -282,13 +215,85 @@ std::ostream & operator << (std::ostream & os, BaseForID id) return os << static_cast(id.getNum()); } -template -std::ostream & operator << (std::ostream & os, Identifier id) +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class EntityBase { - //We use common type with short to force char and unsigned char to be promoted and formatted as numbers. - typedef typename std::common_type::NumericType>::type Number; - return os << static_cast(id.getNum()); -} +public: + int32_t num; +}; + +template +class EntityIdentifier : public T +{ + using EnumType = typename T::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr int32_t getNum() const + { + return T::num; + } + + constexpr EnumType toEnum() const + { + return static_cast(T::num); + } + + template void serialize(Handler &h, const int version) + { + h & T::num; + } + + constexpr EntityIdentifier(const EnumType & enumValue) + { + T::num = static_cast(enumValue); + } + + constexpr EntityIdentifier(int32_t _num = -1) + { + T::num = _num; + } + + constexpr void advance(int change) + { + T::num += change; + } + + constexpr bool operator == (const EnumType & b) const { return T::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return T::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return T::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return T::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return T::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return T::num > static_cast(b); } + + constexpr bool operator == (const EntityIdentifier & b) const { return T::num == b.num; } + constexpr bool operator <= (const EntityIdentifier & b) const { return T::num <= b.num; } + constexpr bool operator >= (const EntityIdentifier & b) const { return T::num >= b.num; } + constexpr bool operator != (const EntityIdentifier & b) const { return T::num != b.num; } + constexpr bool operator < (const EntityIdentifier & b) const { return T::num < b.num; } + constexpr bool operator > (const EntityIdentifier & b) const { return T::num > b.num; } + + constexpr EntityIdentifier & operator++() + { + ++T::num; + return *this; + } + + constexpr EntityIdentifier operator++(int) + { + EntityIdentifier ret(*this); + ++T::num; + return ret; + } + + constexpr operator int32_t () const + { + return T::num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// class ArtifactInstanceID : public BaseForID { @@ -398,11 +403,6 @@ class TeleportChannelID : public BaseForID friend class CNonConstInfoCallback; }; -// #ifndef INSTANTIATE_BASE_FOR_ID_HERE -// extern template std::ostream & operator << (std::ostream & os, BaseForID id); -// extern template std::ostream & operator << (std::ostream & os, BaseForID id); -// #endif - // Enum declarations namespace PrimarySkill { @@ -410,10 +410,10 @@ namespace PrimarySkill EXPERIENCE = 4}; //for some reason changePrimSkill uses it } -class SecondarySkill +class SecondarySkillBase : public EntityBase { public: - enum ESecondarySkill + enum Type : int32_t { WRONG = -2, DEFAULT = -1, @@ -422,61 +422,51 @@ public: SCHOLAR, TACTICS, ARTILLERY, LEARNING, OFFENCE, ARMORER, INTELLIGENCE, SORCERY, RESISTANCE, FIRST_AID, SKILL_SIZE }; - static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); - - SecondarySkill(ESecondarySkill _num = WRONG) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(SecondarySkill, ESecondarySkill) - - ESecondarySkill num; }; -ID_LIKE_OPERATORS(SecondarySkill, SecondarySkill::ESecondarySkill) +class SecondarySkill : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; enum class EAlignment : uint8_t { GOOD, EVIL, NEUTRAL }; -namespace ETownType//deprecated +class FactionIDBase : public EntityBase { - enum ETownType +public: + enum Type : int32_t { + NONE = -2, + DEFAULT = -1, + RANDOM = -1, ANY = -1, - CASTLE, RAMPART, TOWER, INFERNO, NECROPOLIS, DUNGEON, STRONGHOLD, FORTRESS, CONFLUX, NEUTRAL + CASTLE, + RAMPART, + TOWER, + INFERNO, + NECROPOLIS, + DUNGEON, + STRONGHOLD, + FORTRESS, + CONFLUX, + NEUTRAL }; -} - -class FactionID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(FactionID, si32) - - DLL_LINKAGE static const FactionID NONE; - DLL_LINKAGE static const FactionID DEFAULT; - DLL_LINKAGE static const FactionID CASTLE; - DLL_LINKAGE static const FactionID RAMPART; - DLL_LINKAGE static const FactionID TOWER; - DLL_LINKAGE static const FactionID INFERNO; - DLL_LINKAGE static const FactionID NECROPOLIS; - DLL_LINKAGE static const FactionID DUNGEON; - DLL_LINKAGE static const FactionID STRONGHOLD; - DLL_LINKAGE static const FactionID FORTRESS; - DLL_LINKAGE static const FactionID CONFLUX; - DLL_LINKAGE static const FactionID NEUTRAL; static si32 decode(const std::string& identifier); static std::string encode(const si32 index); static std::string entityType(); }; -class TerrainID +class FactionID : public EntityIdentifier { - //Dummy class used only for serialization public: - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - static std::string entityType(); + using EntityIdentifier::EntityIdentifier; }; +using ETownType = FactionID; + class BuildingID { public: @@ -953,27 +943,50 @@ public: ID_LIKE_OPERATORS(Obj, Obj::EObj) -enum class Road : int8_t +class RoadIsBase : public EntityBase { - NO_ROAD = 0, - FIRST_REGULAR_ROAD = 1, - DIRT_ROAD = 1, - GRAVEL_ROAD = 2, - COBBLESTONE_ROAD = 3, - ORIGINAL_ROAD_COUNT //+1 +public: + enum Type : int32_t + { + NO_ROAD = 0, + FIRST_REGULAR_ROAD = 1, + DIRT_ROAD = 1, + GRAVEL_ROAD = 2, + COBBLESTONE_ROAD = 3, + ORIGINAL_ROAD_COUNT //+1 + }; }; -enum class River : int8_t +class RoadId : public EntityIdentifier { - NO_RIVER = 0, - FIRST_REGULAR_RIVER = 1, - WATER_RIVER = 1, - ICY_RIVER = 2, - MUD_RIVER = 3, - LAVA_RIVER = 4, - ORIGINAL_RIVER_COUNT //+1 +public: + using EntityIdentifier::EntityIdentifier; }; +class RiverIdBase : public EntityBase +{ +public: + enum Type : int32_t + { + NO_RIVER = 0, + FIRST_REGULAR_RIVER = 1, + WATER_RIVER = 1, + ICY_RIVER = 2, + MUD_RIVER = 3, + LAVA_RIVER = 4, + ORIGINAL_RIVER_COUNT //+1 + }; +}; + +class RiverId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; + +using River = RiverId; +using Road = RoadId; + namespace SecSkillLevel { enum SecSkillLevel @@ -1296,37 +1309,58 @@ class BattleField : public BaseForID DLL_LINKAGE const BattleFieldInfo * getInfo() const; }; -enum class EBoatId : int32_t +class BoatIdBase : public EntityBase { - NONE = -1, - NECROPOLIS = 0, - CASTLE, - FORTRESS +public: + enum Type : int32_t + { + NONE = -1, + NECROPOLIS = 0, + CASTLE, + FORTRESS + }; }; -using BoatId = Identifier; - -enum class ETerrainId { - NATIVE_TERRAIN = -4, - ANY_TERRAIN = -3, - NONE = -1, - FIRST_REGULAR_TERRAIN = 0, - DIRT = 0, - SAND, - GRASS, - SNOW, - SWAMP, - ROUGH, - SUBTERRANEAN, - LAVA, - WATER, - ROCK, - ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK +class BoatId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; }; -using TerrainId = Identifier; -using RoadId = Identifier; -using RiverId = Identifier; +class TerrainIdBase : public EntityBase +{ +public: + enum Type : int32_t + { + NATIVE_TERRAIN = -4, + ANY_TERRAIN = -3, + NONE = -1, + FIRST_REGULAR_TERRAIN = 0, + DIRT = 0, + SAND, + GRASS, + SNOW, + SWAMP, + ROUGH, + SUBTERRANEAN, + LAVA, + WATER, + ROCK, + ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK + }; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class TerrainId : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; + +using ETerrainId = TerrainId; class ObstacleInfo; class Obstacle : public BaseForID @@ -1336,16 +1370,26 @@ class Obstacle : public BaseForID DLL_LINKAGE const ObstacleInfo * getInfo() const; }; -enum class ESpellSchool: int8_t +class SpellSchoolBase : public EntityBase { - ANY = -1, - AIR = 0, - FIRE = 1, - WATER = 2, - EARTH = 3, +public: + enum Type : int32_t + { + ANY = -1, + AIR = 0, + FIRE = 1, + WATER = 2, + EARTH = 3, + }; }; -using SpellSchool = Identifier; +class SpellSchool : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; + +using ESpellSchool = SpellSchool; enum class EMetaclass: ui8 { @@ -1387,6 +1431,27 @@ enum class EBattleResult : int8_t SURRENDER = 2 }; +class GameResIDBase : public EntityBase +{ +public: + enum Type : int32_t + { + WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, + COUNT, + + WOOD_AND_ORE = 127, // special case for town bonus resource + INVALID = -1 + }; +}; + +class GameResID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; + +using EGameResID = GameResID; + // Typedef declarations using TExpType = si64; using TQuantity = si32; diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index 50f2c48b6..56fc50442 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -119,7 +119,7 @@ std::string ResourceSet::toString() const bool ResourceSet::nziterator::valid() const { - return cur.resType < GameConstants::RESOURCE_QUANTITY && cur.resVal; + return cur.resType < GameResID::COUNT && cur.resVal; } ResourceSet::nziterator ResourceSet::nziterator::operator++() @@ -150,9 +150,9 @@ void ResourceSet::nziterator::advance() do { ++cur.resType; - } while(cur.resType < GameConstants::RESOURCE_QUANTITY && !(cur.resVal=rs[cur.resType])); + } while(cur.resType < GameResID::COUNT && !(cur.resVal=rs[cur.resType])); - if(cur.resType >= GameConstants::RESOURCE_QUANTITY) + if(cur.resType >= GameResID::COUNT) cur.resVal = -1; } diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 6d879f715..1b4d5908d 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -22,16 +22,6 @@ class JsonSerializeFormat; class ResourceSet; -enum class EGameResID : int8_t -{ - WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - - WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 -}; - -using GameResID = Identifier; - //class to be representing a vector of resource class ResourceSet { diff --git a/lib/StringConstants.h b/lib/StringConstants.h index ff0facbd8..7a8013330 100644 --- a/lib/StringConstants.h +++ b/lib/StringConstants.h @@ -68,7 +68,7 @@ namespace EBuildingType }; } -namespace ETownType +namespace NFaction { const std::string names [GameConstants::F_NUMBER] = { diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 54342be77..1fd9dfa82 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -697,7 +697,7 @@ void CGameState::initRandomFactionsForPlayers() logGlobal->debug("\tPicking random factions for players"); for(auto & elem : scenarioOps->playerInfos) { - if(elem.second.castle==-1) + if(elem.second.castle==FactionID::RANDOM) { auto randomID = getRandomGenerator().nextInt((int)map->players[elem.first.getNum()].allowedFactions.size() - 1); auto iter = map->players[elem.first.getNum()].allowedFactions.begin(); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index d54f2f657..0472e809a 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -516,7 +516,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const std::vector nativeCrits; //slots for(const auto & elem : Slots()) { - if (elem.second->type->getFaction() == subID) //native + if (elem.second->type->getFaction() == getFaction()) //native { nativeCrits.push_back(elem.first); //collect matching slots } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index f5a46e999..b0d0434e6 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -914,7 +914,7 @@ void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler) if(handler.saving) { - for(si32 i = 0; i < skillCount; ++i) + for(SecondarySkill i(0); i < SecondarySkill(skillCount); ++i) if(vstd::contains(allowedAbilities, i)) temp[i] = true; } diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 0b2eed219..e1dd3ff3c 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -396,7 +396,7 @@ void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std if(handler.saving) { for(auto faction : VLC->townh->objects) - if(faction->town && vstd::contains(value, faction->getIndex())) + if(faction->town && vstd::contains(value, faction->getId())) temp[static_cast(faction->getIndex())] = true; } diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index feba9e023..7827f25c4 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -161,7 +161,7 @@ RiverId MapReaderH3M::readRiver() SecondarySkill MapReaderH3M::readSkill() { SecondarySkill result(readUInt8()); - assert(result < features.skillsCount); + assert(result.getNum() < features.skillsCount); return remapIdentifier(result);; } diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 3201943eb..1a0265327 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -506,7 +506,7 @@ const CRmgTemplate * CMapGenOptions::getPossibleTemplate(CRandomGenerator & rand return *RandomGeneratorUtil::nextItem(templates, rand); } -CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(RANDOM_TOWN), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) +CMapGenOptions::CPlayerSettings::CPlayerSettings() : color(0), startingTown(FactionID::RANDOM), playerType(EPlayerType::AI), team(TeamID::NO_TEAM) { } diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index aa5683ede..2e25fd671 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -53,9 +53,6 @@ public: TeamID getTeam() const; void setTeam(const TeamID & value); - /// Constant for a random town selection. - static const si32 RANDOM_TOWN = -1; - private: PlayerColor color; si32 startingTown; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 8ce615a14..69527411f 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -174,7 +174,7 @@ std::string CMapGenerator::getMapDescription() const { ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " is human"; } - if(pSettings.getStartingTown() != CMapGenOptions::CPlayerSettings::RANDOM_TOWN) + if(pSettings.getStartingTown() != FactionID::RANDOM) { ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " town choice is " << (*VLC->townh)[pSettings.getStartingTown()]->getNameTranslated(); diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 8c4580230..b8032bc01 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -374,15 +374,15 @@ void ZoneOptions::serializeJson(JsonSerializeFormat & handler) if(terrainTypeLikeZone == NO_ZONE) { - handler.serializeIdArray("terrainTypes", terrainTypes, std::set()); - handler.serializeIdArray("bannedTerrains", bannedTerrains, std::set()); + handler.serializeIdArray("terrainTypes", terrainTypes); + handler.serializeIdArray("bannedTerrains", bannedTerrains); } handler.serializeBool("townsAreSameType", townsAreSameType, false); - handler.serializeIdArray("allowedMonsters", monsterTypes, std::set()); - handler.serializeIdArray("bannedMonsters", bannedMonsters, std::set()); - handler.serializeIdArray("allowedTowns", townTypes, std::set()); - handler.serializeIdArray("bannedTowns", bannedTownTypes, std::set()); + handler.serializeIdArray("allowedMonsters", monsterTypes); + handler.serializeIdArray("bannedMonsters", bannedMonsters); + handler.serializeIdArray("allowedTowns", townTypes); + handler.serializeIdArray("bannedTowns", bannedTownTypes); { //TODO: add support for std::map to serializeEnum diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index abfb73421..07c56688f 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -442,13 +442,13 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const { auto player = PlayerColor(*owner - 1); auto playerSettings = map.getMapGenOptions().getPlayersSettings(); - si32 faction = CMapGenOptions::CPlayerSettings::RANDOM_TOWN; + si32 faction = FactionID::RANDOM; if (vstd::contains(playerSettings, player)) faction = playerSettings[player].getStartingTown(); else logGlobal->error("Can't find info for player %d (starting zone)", player.getNum()); - if (faction == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) //TODO: check this after a town has already been randomized + if (faction == FactionID::RANDOM) //TODO: check this after a town has already been randomized zonesToPlace.push_back(zone); else { diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index ecb12d7b6..1f046588e 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -60,7 +60,7 @@ void TownPlacer::placeTowns(ObjectManager & manager) player = PlayerColor(player_id); zone.setTownType(map.getMapGenOptions().getPlayersSettings().find(player)->second.getStartingTown()); - if(zone.getTownType() == CMapGenOptions::CPlayerSettings::RANDOM_TOWN) + if(zone.getTownType() == FactionID::RANDOM) zone.setTownType(getRandomTownType(true)); } else //no player - randomize town diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 63126b36f..7802c4fa2 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -215,7 +215,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment { NewObject no; no.ID = Obj::BOAT; - no.subID = BoatId(EBoatId::NECROPOLIS); + no.subID = BoatId::NECROPOLIS; no.targetPos = summonPos; env->apply(&no); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 22c6ef28e..acfc219b3 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -608,7 +608,7 @@ std::vector CSpellHandler::loadLegacyData() auto & chances = lineNode["gainChance"].Struct(); - for(const auto & name : ETownType::names) + for(const auto & name : NFaction::names) chances[name].Integer() = static_cast(parser.readNumber()); auto AIVals = parser.readNumArray(GameConstants::SPELL_SCHOOL_LEVELS); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index d72f78d0c..10affe144 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -840,10 +840,10 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom; - if(cur == PlayerSettings::NONE) //no change + if(cur.getNum() == PlayerSettings::NONE) //no change return; - if(cur == PlayerSettings::RANDOM) //first/last available + if(cur.getNum() == PlayerSettings::RANDOM) //first/last available { if(dir > 0) cur = *allowed.begin(); //id of first town @@ -880,7 +880,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { s.hero = PlayerSettings::RANDOM; } - if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) + if(cur.getNum() < 0 && s.bonus == PlayerSettings::RESOURCE) s.bonus = PlayerSettings::RANDOM; } @@ -935,7 +935,7 @@ void CVCMIServer::setCampaignBonus(int bonusId) void CVCMIServer::optionNextHero(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle < 0 || s.hero == PlayerSettings::NONE) + if(s.castle.getNum() < 0 || s.hero == PlayerSettings::NONE) return; if(s.hero == PlayerSettings::RANDOM) // first/last available @@ -1004,7 +1004,7 @@ void CVCMIServer::optionNextBonus(PlayerColor player, int dir) if(ret < PlayerSettings::RANDOM) ret = PlayerSettings::RESOURCE; - if(s.castle == PlayerSettings::RANDOM && ret == PlayerSettings::RESOURCE) //random castle - can't be resource + if(s.castle.getNum() == PlayerSettings::RANDOM && ret == PlayerSettings::RESOURCE) //random castle - can't be resource { if(dir < 0) ret = PlayerSettings::GOLD; diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index 11ddce729..78fc13bc8 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -99,7 +99,7 @@ TEST_F(CCreatureTest, JsonUpdate) EXPECT_EQ(subject->getFightValue(), 2420); EXPECT_EQ(subject->getLevel(), 6); - EXPECT_EQ(subject->getFaction(), 55); + EXPECT_EQ(subject->getFaction().getNum(), 55); EXPECT_TRUE(subject->isDoubleWide()); } diff --git a/test/mock/mock_Creature.h b/test/mock/mock_Creature.h index 2ce93f407..7df167aec 100644 --- a/test/mock/mock_Creature.h +++ b/test/mock/mock_Creature.h @@ -57,7 +57,7 @@ public: MOCK_CONST_METHOD1(getCost, int32_t(int32_t)); MOCK_CONST_METHOD0(isDoubleWide, bool()); - MOCK_CONST_METHOD1(getRecruitCost, int32_t(Identifier)); + MOCK_CONST_METHOD1(getRecruitCost, int32_t(GameResID)); MOCK_CONST_METHOD0(getFullRecruitCost, ResourceSet()); MOCK_CONST_METHOD0(hasUpgrades, bool()); }; From 17d3d663ee46c9d63664e7f5ba747eb45aee6eb5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 19 Aug 2023 20:48:28 +0300 Subject: [PATCH 0183/1248] Converted creature ID and spell ID to new form --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/AIUtility.cpp | 2 +- AI/Nullkiller/AIUtility.h | 2 +- AI/Nullkiller/Analyzers/ArmyManager.cpp | 4 +- AI/Nullkiller/Goals/BuyArmy.cpp | 2 +- AI/VCAI/AIUtility.cpp | 2 +- AI/VCAI/AIUtility.h | 2 +- AI/VCAI/ArmyManager.cpp | 4 +- AI/VCAI/Goals/FindObj.cpp | 6 +- AI/VCAI/Goals/GatherTroops.cpp | 2 +- AI/VCAI/VCAI.cpp | 6 +- client/widgets/CGarrisonInt.cpp | 2 +- lib/ArtifactUtils.cpp | 8 +- lib/ArtifactUtils.h | 2 +- lib/CCreatureSet.cpp | 2 +- lib/GameConstants.cpp | 34 +++--- lib/GameConstants.h | 142 +++++++++-------------- lib/gameState/CGameState.cpp | 6 +- lib/gameState/CGameStateCampaign.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/ObjectTemplate.cpp | 2 +- lib/mapping/CMap.cpp | 6 +- lib/mapping/CMap.h | 2 +- lib/mapping/MapFormatH3M.cpp | 6 +- lib/mapping/MapFormatJson.cpp | 6 +- lib/mapping/MapReaderH3M.cpp | 22 ++-- lib/rmg/RmgMap.cpp | 4 +- mapeditor/inspector/armywidget.cpp | 2 +- mapeditor/inspector/inspector.cpp | 2 +- server/CGameHandler.cpp | 2 +- server/CVCMIServer.cpp | 10 +- server/battles/BattleActionProcessor.cpp | 6 +- server/battles/BattleResultProcessor.cpp | 2 +- 33 files changed, 135 insertions(+), 171 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 575bbbc0e..697d11eaa 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -757,7 +757,7 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj) { UpgradeInfo ui; myCb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID >= 0 && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count)) + if(ui.oldID != CreatureID::NONE && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count)) { myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]); upgraded = true; diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index a866f3686..04184bcda 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -274,7 +274,7 @@ creInfo infoFromDC(const dwellingContent & dc) creInfo ci; ci.count = dc.first; ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed - if (ci.creID != -1) + if (ci.creID != CreatureID::NONE) { ci.cre = VLC->creatures()->getById(ci.creID); ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index e8e4636ef..b3d50e3e3 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -151,7 +151,7 @@ struct ObjectIdRef } }; -template +template bool objWithID(const CGObjectInstance * obj) { return obj->ID == id; diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 11bdeecd5..bac669ae7 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -256,7 +256,7 @@ std::shared_ptr ArmyManager::getArmyAvailableToBuyAsCCreatureSet( { auto ci = infoFromDC(dwelling->creatures[i]); - if(!ci.count || ci.creID == -1) + if(!ci.count || ci.creID == CreatureID::NONE) continue; vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford @@ -316,7 +316,7 @@ std::vector ArmyManager::getArmyAvailableToBuy( { auto ci = infoFromDC(dwelling->creatures[i]); - if(ci.creID == -1) continue; + if(ci.creID == CreatureID::NONE) continue; if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) { diff --git a/AI/Nullkiller/Goals/BuyArmy.cpp b/AI/Nullkiller/Goals/BuyArmy.cpp index 3bc3c0faf..55a8ec16e 100644 --- a/AI/Nullkiller/Goals/BuyArmy.cpp +++ b/AI/Nullkiller/Goals/BuyArmy.cpp @@ -51,7 +51,7 @@ void BuyArmy::accept(AIGateway * ai) auto res = cb->getResourceAmount(); auto & ci = armyToBuy[i]; - if(objid != -1 && ci.creID != objid) + if(objid != CreatureID::NONE && ci.creID.getNum() != objid) continue; vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index ff70a870d..694803f19 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -224,7 +224,7 @@ creInfo infoFromDC(const dwellingContent & dc) creInfo ci; ci.count = dc.first; ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed - if (ci.creID != -1) + if (ci.creID != CreatureID::NONE) { ci.cre = VLC->creatures()->getById(ci.creID); ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index fb1346382..64cf46d28 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -142,7 +142,7 @@ class ObjsVector : public std::vector { }; -template +template bool objWithID(const CGObjectInstance * obj) { return obj->ID == id; diff --git a/AI/VCAI/ArmyManager.cpp b/AI/VCAI/ArmyManager.cpp index c42c580d2..d639c125e 100644 --- a/AI/VCAI/ArmyManager.cpp +++ b/AI/VCAI/ArmyManager.cpp @@ -118,12 +118,12 @@ ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDw { creInfo ci = infoFromDC(dc); - if(!ci.count || ci.creID == -1) + if(!ci.count || ci.creID == CreatureID::NONE) continue; vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford - if(ci.count && ci.creID != -1) //valid creature at this level + if(ci.count && ci.creID != CreatureID::NONE) //valid creature at this level { //can be merged with another stack? SlotID dst = h->getSlotFor(ci.creID); diff --git a/AI/VCAI/Goals/FindObj.cpp b/AI/VCAI/Goals/FindObj.cpp index e14c3320c..189a5c44b 100644 --- a/AI/VCAI/Goals/FindObj.cpp +++ b/AI/VCAI/Goals/FindObj.cpp @@ -28,7 +28,7 @@ TSubgoal FindObj::whatToDoToAchieve() { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == objid && obj->subID == resID) + if(obj->ID.getNum() == objid && obj->subID == resID) { o = obj; break; //TODO: consider multiple objects and choose best @@ -39,7 +39,7 @@ TSubgoal FindObj::whatToDoToAchieve() { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == objid) + if(obj->ID.getNum() == objid) { o = obj; break; //TODO: consider multiple objects and choose best @@ -59,7 +59,7 @@ bool FindObj::fulfillsMe(TSubgoal goal) if (!hero || hero == goal->hero) for (auto obj : cb->getVisitableObjs(goal->tile)) //check if any object on that tile matches criteria if (obj->visitablePos() == goal->tile) //object could be removed - if (obj->ID == objid && obj->subID == resID) //same type and subtype + if (obj->ID.getNum() == objid && obj->subID == resID) //same type and subtype return true; } return false; diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 7af7098e2..471a997be 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -134,7 +134,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() { for(auto type : creature.second) { - if(type == objid && ai->ah->freeResources().canAfford(VLC->creatures()->getById(type)->getFullRecruitCost())) + if(type.getNum() == objid && ai->ah->freeResources().canAfford(VLC->creatures()->getById(type)->getFullRecruitCost())) vstd::concatenate(solutions, ai->ah->howToVisitObj(obj)); } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 997248ef6..6aa6a187a 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -764,7 +764,7 @@ void makePossibleUpgrades(const CArmedInstance * obj) { UpgradeInfo ui; cb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID >= 0 && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) + if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) { cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); } @@ -2184,8 +2184,8 @@ void VCAI::tryRealize(Goals::BuyArmy & g) auto ci = infoFromDC(t->creatures[i]); if(!ci.count - || ci.creID == -1 - || (g.objid != -1 && ci.creID != g.objid) + || ci.creID == CreatureID::NONE + || (g.objid != -1 && ci.creID.getNum() != g.objid) || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) continue; diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index df41863a2..18bb0388f 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -164,7 +164,7 @@ bool CGarrisonSlot::viewInfo() UpgradeInfo pom; LOCPLINT->cb->fillUpgradeInfo(getObj(), ID, pom); - bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID>=0; //upgrade is possible + bool canUpgrade = getObj()->tempOwner == LOCPLINT->playerID && pom.oldID != CreatureID::NONE; //upgrade is possible std::function upgr = nullptr; auto dism = getDismiss(); if(canUpgrade) upgr = [=] (CreatureID newID) { LOCPLINT->cb->upgradeCreature(getObj(), ID, newID); }; diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index b56f2c7be..a3532bee3 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -190,18 +190,18 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const A return ArtifactUtils::createNewArtifactInstance(VLC->arth->objects[aid]); } -DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, int spellID) +DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID) { CArtifactInstance * art = nullptr; - if(aid >= 0) + if(aid.getNum() >= 0) { - if(spellID < 0) + if(spellID == SpellID::NONE) { art = ArtifactUtils::createNewArtifactInstance(aid); } else { - art = ArtifactUtils::createScroll(SpellID(spellID)); + art = ArtifactUtils::createScroll(spellID); } } else diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index c7b893dee..39532f38e 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -40,7 +40,7 @@ namespace ArtifactUtils DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & sid); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(CArtifact * art); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid); - DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, int spellID = -1); + DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID = SpellID::NONE); DLL_LINKAGE void insertScrrollSpellName(std::string & description, const SpellID & sid); } diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index ef9065aa2..6da4ad768 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -752,7 +752,7 @@ void CStackInstance::giveStackExp(TExpType exp) void CStackInstance::setType(const CreatureID & creID) { - if(creID >= 0 && creID < VLC->creh->objects.size()) + if(creID.getNum() >= 0 && creID.getNum() < VLC->creh->objects.size()) setType(VLC->creh->objects[creID]); else setType((const CCreature*)nullptr); diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index f059f9b19..f9095bc46 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -78,17 +78,17 @@ std::string HeroTypeID::encode(const si32 index) return VLC->heroTypes()->getByIndex(index)->getJsonKey(); } -const CArtifact * ArtifactID::toArtifact() const +const CArtifact * ArtifactIDBase::toArtifact() const { return dynamic_cast(toArtifact(VLC->artifacts())); } -const Artifact * ArtifactID::toArtifact(const ArtifactService * service) const +const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const { - return service->getById(*this); + return service->getByIndex(num); } -si32 ArtifactID::decode(const std::string & identifier) +si32 ArtifactIDBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); if(rawId) @@ -97,22 +97,22 @@ si32 ArtifactID::decode(const std::string & identifier) return -1; } -std::string ArtifactID::encode(const si32 index) +std::string ArtifactIDBase::encode(const si32 index) { return VLC->artifacts()->getByIndex(index)->getJsonKey(); } -const CCreature * CreatureID::toCreature() const +const CCreature * CreatureIDBase::toCreature() const { - return VLC->creh->objects.at(*this); + return VLC->creh->objects.at(num); } -const Creature * CreatureID::toCreature(const CreatureService * creatures) const +const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const { - return creatures->getById(*this); + return creatures->getByIndex(num); } -si32 CreatureID::decode(const std::string & identifier) +si32 CreatureIDBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); if(rawId) @@ -121,27 +121,27 @@ si32 CreatureID::decode(const std::string & identifier) return -1; } -std::string CreatureID::encode(const si32 index) +std::string CreatureIDBase::encode(const si32 index) { return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); } -const CSpell * SpellID::toSpell() const +const CSpell * SpellIDBase::toSpell() const { if(num < 0 || num >= VLC->spellh->objects.size()) { logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); return nullptr; } - return VLC->spellh->objects[*this]; + return VLC->spellh->objects[num]; } -const spells::Spell * SpellID::toSpell(const spells::Service * service) const +const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const { - return service->getById(*this); + return service->getByIndex(num); } -si32 SpellID::decode(const std::string & identifier) +si32 SpellIDBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); if(rawId) @@ -150,7 +150,7 @@ si32 SpellID::decode(const std::string & identifier) return -1; } -std::string SpellID::encode(const si32 index) +std::string SpellIDBase::encode(const si32 index) { return VLC->spells()->getByIndex(index)->getJsonKey(); } diff --git a/lib/GameConstants.h b/lib/GameConstants.h index ee023ad61..e22ffb2f5 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -221,6 +221,14 @@ class EntityBase { public: int32_t num; + + struct hash + { + size_t operator()(const EntityBase & id) const + { + return std::hash()(id.num); + } + }; }; template @@ -790,10 +798,10 @@ enum class ETeleportChannelType MIXED }; -class Obj +class ObjBase : public EntityBase { public: - enum EObj + enum Type { NO_OBJ = -1, ALTAR_OF_SACRIFICE [[deprecated]] = 2, @@ -933,17 +941,15 @@ public: MAGIC_PLAINS2 = 230, ROCKLANDS = 231, }; - Obj(EObj _num = NO_OBJ) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(Obj, EObj) - - EObj num; }; -ID_LIKE_OPERATORS(Obj, Obj::EObj) +class Obj : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; -class RoadIsBase : public EntityBase +class RoadIdBase : public EntityBase { public: enum Type : int32_t @@ -957,10 +963,10 @@ public: }; }; -class RoadId : public EntityIdentifier +class RoadId : public EntityIdentifier { public: - using EntityIdentifier::EntityIdentifier; + using EntityIdentifier::EntityIdentifier; }; class RiverIdBase : public EntityBase @@ -1134,10 +1140,10 @@ namespace GameConstants const auto BACKPACK_START = ArtifactPosition::AFTER_LAST; } -class ArtifactID +class ArtifactIDBase : public EntityBase { public: - enum EArtifactID + enum Type { NONE = -1, SPELLBOOK = 0, @@ -1147,81 +1153,45 @@ public: BALLISTA = 4, AMMO_CART = 5, FIRST_AID_TENT = 6, - //CENTAUR_AXE = 7, - //BLACKSHARD_OF_THE_DEAD_KNIGHT = 8, VIAL_OF_DRAGON_BLOOD = 127, ARMAGEDDONS_BLADE = 128, TITANS_THUNDER = 135, - //CORNUCOPIA = 140, - //FIXME: the following is only true if WoG is enabled. Otherwise other mod artifacts will take these slots. ART_SELECTION = 144, ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 - AXE_OF_SMASHING = 146, - MITHRIL_MAIL = 147, - SWORD_OF_SHARPNESS = 148, - HELM_OF_IMMORTALITY = 149, - PENDANT_OF_SORCERY = 150, - BOOTS_OF_HASTE = 151, - BOW_OF_SEEKING = 152, - DRAGON_EYE_RING = 153 - //HARDENED_SHIELD = 154, - //SLAVAS_RING_OF_POWER = 155 }; - ArtifactID(EArtifactID _num = NONE) : num(_num) - {} - DLL_LINKAGE const CArtifact * toArtifact() const; DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; ///json serialization helpers static si32 decode(const std::string & identifier); static std::string encode(const si32 index); - - ID_LIKE_CLASS_COMMON(ArtifactID, EArtifactID) - - EArtifactID num; - - struct hash - { - size_t operator()(const ArtifactID & aid) const - { - return std::hash()(aid.num); - } - }; }; -ID_LIKE_OPERATORS(ArtifactID, ArtifactID::EArtifactID) - -class CreatureID +class ArtifactID : public EntityIdentifier { public: - enum ECreatureID + using EntityIdentifier::EntityIdentifier; +}; + +class CreatureIDBase : public EntityBase +{ +public: + enum Type { NONE = -1, - ARCHER = 2, - CAVALIER = 10, - CHAMPION = 11, - STONE_GOLEM = 32, - IRON_GOLEM = 33, - IMP = 42, - SKELETON = 56, - WALKING_DEAD = 58, - WIGHTS = 60, - LICHES = 64, - BONE_DRAGON = 68, - TROGLODYTES = 70, - MEDUSA = 76, - HYDRA = 110, - CHAOS_HYDRA = 111, - AIR_ELEMENTAL = 112, - EARTH_ELEMENTAL = 113, - FIRE_ELEMENTAL = 114, - WATER_ELEMENTAL = 115, - GOLD_GOLEM = 116, - DIAMOND_GOLEM = 117, - PSYCHIC_ELEMENTAL = 120, - MAGIC_ELEMENTAL = 121, + ARCHER = 2, // for debug / fallback + IMP = 42, // for Deity of Fire + SKELETON = 56, // for Skeleton Transformer + BONE_DRAGON = 68, // for Skeleton Transformer + TROGLODYTES = 70, // for Abandoned Mine + MEDUSA = 76, // for Siege UI workaround + HYDRA = 110, // for Skeleton Transformer + CHAOS_HYDRA = 111, // for Skeleton Transformer + AIR_ELEMENTAL = 112, // for tests + FIRE_ELEMENTAL = 114, // for tests + PSYCHIC_ELEMENTAL = 120, // for hardcoded ability + MAGIC_ELEMENTAL = 121, // for hardcoded ability CATAPULT = 145, BALLISTA = 146, FIRST_AID_TENT = 147, @@ -1229,27 +1199,24 @@ public: ARROW_TOWERS = 149 }; - CreatureID(ECreatureID _num = NONE) : num(_num) - {} - DLL_LINKAGE const CCreature * toCreature() const; DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; - ID_LIKE_CLASS_COMMON(CreatureID, ECreatureID) - - ECreatureID num; - ///json serialization helpers static si32 decode(const std::string & identifier); static std::string encode(const si32 index); }; -ID_LIKE_OPERATORS(CreatureID, CreatureID::ECreatureID) - -class SpellID +class CreatureID : public EntityIdentifier { public: - enum ESpellID + using EntityIdentifier::EntityIdentifier; +}; + +class SpellIDBase : public EntityBase +{ +public: + enum Type { SPELLBOOK_PRESET = -3, PRESET = -2, @@ -1278,22 +1245,19 @@ public: FIRST_NON_SPELL = 70, AFTER_LAST = 82 }; - SpellID(ESpellID _num = NONE) : num(_num) - {} - DLL_LINKAGE const CSpell * toSpell() const; //deprecated DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; - ID_LIKE_CLASS_COMMON(SpellID, ESpellID) - - ESpellID num; - ///json serialization helpers static si32 decode(const std::string & identifier); static std::string encode(const si32 index); }; -ID_LIKE_OPERATORS(SpellID, SpellID::ESpellID) +class SpellID : public EntityIdentifier +{ +public: + using EntityIdentifier::EntityIdentifier; +}; class BattleFieldInfo; class BattleField : public BaseForID diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 1fd9dfa82..9b5e78cd6 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1575,7 +1575,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio && (ai = dynamic_cast(object.get()))) //contains army { for(const auto & elem : ai->Slots()) //iterate through army - if(elem.second->type->getId() == condition.objectType) //it's searched creature + if(elem.second->type->getIndex() == condition.objectType) //it's searched creature total += elem.second->count; } } @@ -1615,7 +1615,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { for(const auto & elem : map->objects) // mode B - destroy all objects of this type { - if(elem && elem->ID == condition.objectType) + if(elem && elem->ID.getNum() == condition.objectType) return false; } return true; @@ -1636,7 +1636,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio for(const auto & elem : map->objects) // mode B - flag all objects of this type { //check not flagged objs - if ( elem && elem->ID == condition.objectType && team.count(elem->tempOwner) == 0 ) + if ( elem && elem->ID.getNum() == condition.objectType && team.count(elem->tempOwner) == 0 ) return false; } return true; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 0d8729a94..c45beb135 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -155,7 +155,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector & j) -> bool { - CreatureID::ECreatureID crid = j.second->getCreatureID().toEnum(); + CreatureID crid = j.second->getCreatureID(); return !travelOptions.monstersKeptByHero.count(crid); }; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index f2f49db13..309b65468 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -824,7 +824,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b vstd::amin(necromancySkill, 1.0); //it's impossible to raise more creatures than all... const std::map &casualties = battleResult.casualties[!battleResult.winner]; // figure out what to raise - pick strongest creature meeting requirements - auto creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode + CreatureID creatureTypeRaised = CreatureID::NONE; //now we always have IMPROVED_NECROMANCY, no need for hardcode int requiredCasualtyLevel = 1; TConstBonusListPtr improvedNecromancy = getBonuses(Selector::type()(BonusType::IMPROVED_NECROMANCY)); if(!improvedNecromancy->empty()) diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index d65008061..ca1059d9c 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -24,7 +24,7 @@ VCMI_LIB_NAMESPACE_BEGIN -static bool isOnVisitableFromTopList(int identifier, int type) +static bool isOnVisitableFromTopList(Obj identifier, int type) { if(type == 2 || type == 3 || type == 4 || type == 5) //creature, hero, artifact, resource return true; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index b496eda98..c73c1e578 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -355,7 +355,7 @@ int3 CMap::guardingCreaturePosition (int3 pos) const return int3(-1, -1, -1); } -const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj::EObj type) +const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type) { for (CGObjectInstance * object : getTile(pos).visitableObjects) { @@ -418,7 +418,7 @@ void CMap::checkForObjectives() case EventCondition::CONTROL: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); if (cond.object) { @@ -433,7 +433,7 @@ void CMap::checkForObjectives() case EventCondition::DESTROY: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); if (cond.object) { diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 0aa6afb06..957263133 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -116,7 +116,7 @@ public: void banWaterContent(); /// Gets object of specified type on requested position - const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj::EObj type); + const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); CGHeroInstance * getHero(int heroId); /// Sets the victory/loss condition objectives ?? diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index ed28771ff..7bc802a06 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1159,8 +1159,8 @@ CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) { - auto artID = ArtifactID::NONE; //random, set later - int spellID = -1; + ArtifactID artID = ArtifactID::NONE; //random, set later + SpellID spellID = SpellID::NONE; auto * object = new CGArtifact(); readMessageAndGuards(object->message, object, mapPosition); @@ -1176,7 +1176,7 @@ CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::sh artID = ArtifactID(objectTemplate->subid); } - object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID); + object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum()); return object; } diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index e1dd3ff3c..6908a7511 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1189,8 +1189,8 @@ void CMapLoaderJson::MapObjectLoader::configure() if(auto * art = dynamic_cast(instance)) { - auto artID = ArtifactID::NONE; - int spellID = -1; + ArtifactID artID = ArtifactID::NONE; + SpellID spellID = SpellID::NONE; if(art->ID == Obj::SPELL_SCROLL) { @@ -1208,7 +1208,7 @@ void CMapLoaderJson::MapObjectLoader::configure() artID = ArtifactID(art->subID); } - art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID); + art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); } if(auto * hero = dynamic_cast(instance)) diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 7827f25c4..5757316e1 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -65,10 +65,10 @@ ArtifactID MapReaderH3M::readArtifact() else result = ArtifactID(reader->readUInt8()); - if(result == features.artifactIdentifierInvalid) + if(result.getNum() == features.artifactIdentifierInvalid) return ArtifactID::NONE; - if (result < features.artifactsCount) + if (result.getNum() < features.artifactsCount) return remapIdentifier(result); logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); @@ -82,7 +82,7 @@ ArtifactID MapReaderH3M::readArtifact32() if(result == ArtifactID::NONE) return ArtifactID::NONE; - if (result < features.artifactsCount) + if (result.getNum() < features.artifactsCount) return remapIdentifier(result); logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); @@ -120,17 +120,17 @@ CreatureID MapReaderH3M::readCreature() else result = CreatureID(reader->readUInt8()); - if(result == features.creatureIdentifierInvalid) + if(result.getNum() == features.creatureIdentifierInvalid) return CreatureID::NONE; - if(result < features.creaturesCount) + if(result.getNum() < features.creaturesCount) return remapIdentifier(result);; // this may be random creature in army/town, to be randomized later CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); assert(randomIndex < CreatureID::NONE); - if (randomIndex > -16) + if (randomIndex.getNum() > -16) return randomIndex; logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); @@ -168,21 +168,21 @@ SecondarySkill MapReaderH3M::readSkill() SpellID MapReaderH3M::readSpell() { SpellID result(readUInt8()); - if(result == features.spellIdentifierInvalid) + if(result.getNum() == features.spellIdentifierInvalid) return SpellID::NONE; - if(result == features.spellIdentifierInvalid - 1) + if(result.getNum() == features.spellIdentifierInvalid - 1) return SpellID::PRESET; - assert(result < features.spellsCount); + assert(result.getNum() < features.spellsCount); return remapIdentifier(result);; } SpellID MapReaderH3M::readSpell32() { SpellID result(readInt32()); - if(result == features.spellIdentifierInvalid) + if(result.getNum() == features.spellIdentifierInvalid) return SpellID::NONE; - assert(result < features.spellsCount); + assert(result.getNum() < features.spellsCount); return result; } diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index ecb866cc8..c1a908999 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -342,8 +342,8 @@ ui32 RmgMap::getTotalZoneCount() const bool RmgMap::isAllowedSpell(const SpellID & sid) const { - assert(sid >= 0); - if (sid < mapInstance->allowedSpells.size()) + assert(sid.getNum() >= 0); + if (sid.getNum() < mapInstance->allowedSpells.size()) { return mapInstance->allowedSpells[sid]; } diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 426b5a04b..3878fea73 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -80,7 +80,7 @@ bool ArmyWidget::commitChanges() for(int i = 0; i < TOTAL_SLOTS; ++i) { CreatureID creId(uiSlots[i]->itemData(uiSlots[i]->currentIndex()).toInt()); - if(creId == -1) + if(creId == CreatureID::NONE) { if(army.hasStackAtSlot(SlotID(i))) army.eraseStack(SlotID(i)); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 69800e269..eb61e66b8 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -284,7 +284,7 @@ void Inspector::updateProperties(CGArtifact * o) if(instance) { SpellID spellId = instance->getScrollSpellID(); - if(spellId != -1) + if(spellId != SpellID::NONE) { auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4106c2ea6..d9d17f78c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2491,7 +2491,7 @@ bool CGameHandler::upgradeCreature(ObjectInstanceID objid, SlotID pos, CreatureI int newIDpos= vstd::find_pos(ui.newID, upgID);//get position of new id in UpgradeInfo //check if upgrade is possible - if ((ui.oldID<0 || newIDpos == -1) && complain("That upgrade is not possible!")) + if ((ui.oldID == CreatureID::NONE || newIDpos == -1) && complain("That upgrade is not possible!")) { return false; } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 10affe144..ad2738c0b 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -890,7 +890,7 @@ void CVCMIServer::optionSetCastle(PlayerColor player, int id) FactionID & cur = s.castle; auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; - if(cur == PlayerSettings::NONE) //no change + if(cur.getNum() == PlayerSettings::NONE) //no change return; if(allowed.find(static_cast(id)) == allowed.end() && id != PlayerSettings::RANDOM) // valid id @@ -898,11 +898,11 @@ void CVCMIServer::optionSetCastle(PlayerColor player, int id) cur = static_cast(id); - if(s.hero >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + if(s.hero.getNum() >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { s.hero = PlayerSettings::RANDOM; } - if(cur < 0 && s.bonus == PlayerSettings::RESOURCE) + if(cur.getNum() < 0 && s.bonus == PlayerSettings::RESOURCE) s.bonus = PlayerSettings::RANDOM; } @@ -956,7 +956,7 @@ void CVCMIServer::optionNextHero(PlayerColor player, int dir) void CVCMIServer::optionSetHero(PlayerColor player, int id) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle < 0 || s.hero == PlayerSettings::NONE) + if(s.castle.getNum() < 0 || s.hero == PlayerSettings::NONE) return; if(id == PlayerSettings::RANDOM) @@ -1027,7 +1027,7 @@ void CVCMIServer::optionSetBonus(PlayerColor player, int id) if(id < PlayerSettings::RANDOM) return; - if(s.castle == PlayerSettings::RANDOM && id == PlayerSettings::RESOURCE) //random castle - can't be resource + if(s.castle.getNum() == PlayerSettings::RANDOM && id == PlayerSettings::RESOURCE) //random castle - can't be resource return; s.bonus = static_cast(static_cast(id)); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index f6e770ec1..e438c1ea3 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -406,10 +406,10 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); //TODO special bonus for genies ability - if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) < 0) + if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); - if (spellID < 0) + if (spellID == SpellID::NONE) gameHandler->complain("That stack can't cast spells!"); else { @@ -1216,7 +1216,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * int bonusAdditionalInfo = attacker->getBonus(Selector::type()(BonusType::TRANSMUTATION))->additionalInfo[0]; - if(defender->unitType()->getId() == bonusAdditionalInfo || + if(defender->unitType()->getIndex() == bonusAdditionalInfo || (bonusAdditionalInfo == CAddInfo::NONE && defender->unitType()->getId() == attacker->unitType()->getId())) return; diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index e6616fa38..59860edf5 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -383,7 +383,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) { iw.components.emplace_back( Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : 0, 0); + art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : SpellID(0), 0); if (iw.components.size() >= 14) { gameHandler->sendAndApply(&iw); From 62cd8b12d4fdcebb63ecc2bdc1645444d06c4c4e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 19 Aug 2023 21:43:50 +0300 Subject: [PATCH 0184/1248] Converted several namespace enums to enum class --- AI/EmptyAI/CEmptyAI.cpp | 2 +- AI/EmptyAI/CEmptyAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 6 +- AI/Nullkiller/AIGateway.h | 4 +- AI/Nullkiller/AIUtility.cpp | 2 +- AI/Nullkiller/AIUtility.h | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 8 +-- AI/VCAI/VCAI.cpp | 6 +- AI/VCAI/VCAI.h | 4 +- CCallback.cpp | 4 +- CCallback.h | 8 +-- client/CPlayerInterface.cpp | 6 +- client/CPlayerInterface.h | 4 +- client/Client.h | 2 +- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/windows/CHeroWindow.cpp | 2 +- client/windows/CKingdomInterface.cpp | 2 +- client/windows/CTradeWindow.cpp | 12 ++-- client/windows/CTradeWindow.h | 14 ++-- client/windows/GUIClasses.cpp | 8 +-- client/windows/GUIClasses.h | 2 +- include/vcmi/FactionMember.h | 8 +-- lib/BasicTypes.cpp | 10 +-- lib/CCreatureHandler.cpp | 16 ++--- lib/CGameInfoCallback.cpp | 4 +- lib/CGameInfoCallback.h | 8 +-- lib/CGameInterface.h | 2 +- lib/CHeroHandler.cpp | 14 ++-- lib/CHeroHandler.h | 2 +- lib/CPlayerState.h | 2 +- lib/CTownHandler.cpp | 6 +- lib/GameConstants.h | 71 +++++++++---------- lib/IGameCallback.h | 2 +- lib/IGameEventsReceiver.h | 2 +- lib/JsonRandom.cpp | 6 +- lib/NetPacks.h | 6 +- lib/StringConstants.h | 2 +- lib/battle/BattleInfo.cpp | 4 +- lib/battle/CUnitState.cpp | 4 +- lib/battle/DamageCalculator.cpp | 4 +- lib/bonuses/Bonus.cpp | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameState.h | 2 +- lib/gameState/InfoAboutArmy.cpp | 2 +- .../CommonConstructors.h | 2 +- lib/mapObjects/CGDwelling.cpp | 6 +- lib/mapObjects/CGHeroInstance.cpp | 24 +++---- lib/mapObjects/CGHeroInstance.h | 6 +- lib/mapObjects/CGMarket.cpp | 10 +-- lib/mapObjects/CGMarket.h | 12 ++-- lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CGTownBuilding.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 6 +- lib/mapObjects/CGTownInstance.h | 4 +- lib/mapObjects/CQuest.cpp | 8 +-- lib/mapObjects/IMarket.cpp | 18 ++--- lib/mapObjects/IMarket.h | 10 +-- lib/mapObjects/MiscObjects.cpp | 16 ++--- lib/mapping/MapFormatH3M.cpp | 6 +- lib/modding/CModHandler.cpp | 4 +- lib/pathfinder/CGPathNode.h | 4 +- lib/rewardable/Interface.cpp | 2 +- lib/rewardable/Limiter.cpp | 2 +- mapeditor/inspector/questwidget.cpp | 4 +- mapeditor/inspector/rewardswidget.cpp | 2 +- server/CGameHandler.cpp | 10 +-- server/CGameHandler.h | 2 +- server/battles/BattleActionProcessor.cpp | 8 +-- test/battle/CUnitStateTest.cpp | 4 +- test/mock/mock_IGameCallback.h | 2 +- test/spells/effects/TimedTest.cpp | 4 +- 71 files changed, 227 insertions(+), 238 deletions(-) diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index f3b4dd9c5..2074535a3 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -45,7 +45,7 @@ void CEmptyAI::yourTacticPhase(int distance) cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } -void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) +void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) { cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index a71806e62..8a48a797c 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -26,7 +26,7 @@ public: void yourTurn() override; void yourTacticPhase(int distance) override; void activeStack(const CStack * stack) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 697d11eaa..955269029 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -314,9 +314,9 @@ void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID her }); } -void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) +void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) { - LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val); + LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); NET_EVENT_HANDLER; } @@ -552,7 +552,7 @@ void AIGateway::yourTurn() makingTurn = std::make_unique(&AIGateway::makeTurn, this); } -void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) +void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 3a9c23b31..ec7f61aac 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -113,7 +113,7 @@ public: void initGameInterface(std::shared_ptr env, std::shared_ptr CB) override; void yourTurn() override; - void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done @@ -144,7 +144,7 @@ public: void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; diff --git a/AI/Nullkiller/AIUtility.cpp b/AI/Nullkiller/AIUtility.cpp index 04184bcda..501d882c8 100644 --- a/AI/Nullkiller/AIUtility.cpp +++ b/AI/Nullkiller/AIUtility.cpp @@ -241,7 +241,7 @@ bool isObjectPassable(const CGObjectInstance * obj) } // Pathfinder internal helper -bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations) +bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations) { if((obj->ID == Obj::GARRISON || obj->ID == Obj::GARRISON2) && objectRelations != PlayerRelations::ENEMIES) diff --git a/AI/Nullkiller/AIUtility.h b/AI/Nullkiller/AIUtility.h index b3d50e3e3..646616f1a 100644 --- a/AI/Nullkiller/AIUtility.h +++ b/AI/Nullkiller/AIUtility.h @@ -226,7 +226,7 @@ void foreach_neighbour(CCallback * cbp, const int3 & pos, const Func & foo) // a bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater); bool isObjectPassable(const CGObjectInstance * obj); bool isObjectPassable(const Nullkiller * ai, const CGObjectInstance * obj); -bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations); +bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations); bool isBlockVisitObj(const int3 & pos); bool isWeeklyRevisitable(const CGObjectInstance * obj); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 6e3e5f754..7b0b40086 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -244,10 +244,10 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) 10 * art->valOfBonuses(BonusType::MOVEMENT, 1) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 700 * art->valOfBonuses(BonusType::MORALE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::KNOWLEDGE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::SPELL_POWER)) + 500 * art->valOfBonuses(BonusType::LUCK); auto classValue = 0; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 6aa6a187a..d2da3e13a 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -351,9 +351,9 @@ void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, Q }); } -void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) +void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) { - LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val); + LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); NET_EVENT_HANDLER; } @@ -618,7 +618,7 @@ void VCAI::yourTurn() makingTurn = std::make_unique(&VCAI::makeTurn, this); } -void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) +void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index e1fd11c0f..34c53f258 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -146,7 +146,7 @@ public: void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void yourTurn() override; - void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done @@ -177,7 +177,7 @@ public: void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; diff --git a/CCallback.cpp b/CCallback.cpp index 93f7fb670..227e2f0a9 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -242,12 +242,12 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) sendRequest(&pack); } -void CCallback::trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) { trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); } -void CCallback::trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) { TradeOnMarketplace pack; pack.marketId = dynamic_cast(market)->id; diff --git a/CCallback.h b/CCallback.h index 637b9b6c9..062ef8185 100644 --- a/CCallback.h +++ b/CCallback.h @@ -75,8 +75,8 @@ public: virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made virtual void swapGarrisonHero(const CGTownInstance *town)=0; - virtual void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce - virtual void trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; + virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce + virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; virtual int selectionMade(int selection, QueryID queryID) =0; virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0; @@ -171,8 +171,8 @@ public: void endTurn() override; void swapGarrisonHero(const CGTownInstance *town) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; - void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; + void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; + void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; void setFormation(const CGHeroInstance * hero, bool tight) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; void save(const std::string &fname) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index a5a9a9c31..5d62dbdf0 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -466,10 +466,10 @@ void CPlayerInterface::openTownWindow(const CGTownInstance * town) GH.windows().pushWindow(newCastleInt); } -void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) +void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) { EVENT_HANDLER_CALLED_BY_CLIENT; - if (which == 4) + if (which == PrimarySkill::EXPERIENCE) { for (auto ctw : GH.windows().findWindows()) ctw->setExpToLevel(); @@ -510,7 +510,7 @@ void CPlayerInterface::receivedResource() GH.windows().totalRedraw(); } -void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector& skills, QueryID queryID) +void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector& skills, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index c85fce3be..687fee93c 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -109,11 +109,11 @@ protected: // Call-ins from server, should not be called directly, but only via void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; void heroCreated(const CGHeroInstance* hero) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void heroInGarrisonChange(const CGTownInstance *town) override; void heroMoved(const TryMoveHero & details, bool verbose = true) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; diff --git a/client/Client.h b/client/Client.h index 013db8e28..8eb0a275d 100644 --- a/client/Client.h +++ b/client/Client.h @@ -165,7 +165,7 @@ public: bool removeObject(const CGObjectInstance * obj) override {return false;}; void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs = false) override {}; + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; void showBlockingDialog(BlockingDialog * iw) override {}; diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 8715275a4..18b683e43 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -483,7 +483,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) } //check if we can select this object bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; - canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner); + canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES; bool isHero = false; if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town) diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 771d4a521..9e4f8f2fa 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -222,7 +222,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) //primary skills support for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); + primSkillAreas[g]->bonusValue = curHero->getPrimSkillLevel(static_cast(g)); primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); } diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index d92c6936f..c145f3d4f 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -315,7 +315,7 @@ si64 InfoBoxHeroData::getValue() switch(type) { case HERO_PRIMARY_SKILL: - return hero->getPrimSkillLevel(static_cast(index)); + return hero->getPrimSkillLevel(static_cast(index)); case HERO_MANA: return hero->mana; case HERO_EXPERIENCE: diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 7787ec4de..4afe99a6c 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -317,7 +317,7 @@ void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) setID(-1); } -CTradeWindow::CTradeWindow(std::string bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode::EMarketMode Mode): +CTradeWindow::CTradeWindow(std::string bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode Mode): CWindowObject(PLAYER_COLORED, bgName), market(Market), hero(Hero), @@ -585,7 +585,7 @@ void CTradeWindow::getEmptySlots(std::set> & toR toRemove.insert(item); } -void CTradeWindow::setMode(EMarketMode::EMarketMode Mode) +void CTradeWindow::setMode(EMarketMode Mode) { const IMarket *m = market; const CGHeroInstance *h = hero; @@ -616,7 +616,7 @@ void CTradeWindow::artifactSelected(CHeroArtPlace *slot) selectionChanged(true); } -std::string CMarketplaceWindow::getBackgroundForMode(EMarketMode::EMarketMode mode) +std::string CMarketplaceWindow::getBackgroundForMode(EMarketMode mode) { switch(mode) { @@ -635,7 +635,7 @@ std::string CMarketplaceWindow::getBackgroundForMode(EMarketMode::EMarketMode mo return ""; } -CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode) +CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) : CTradeWindow(getBackgroundForMode(Mode), Market, Hero, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -870,7 +870,7 @@ void CMarketplaceWindow::selectionChanged(bool side) redraw(); } -bool CMarketplaceWindow::printButtonFor(EMarketMode::EMarketMode M) const +bool CMarketplaceWindow::printButtonFor(EMarketMode M) const { return market->allowsTrade(M) && M != mode && (hero || ( M != EMarketMode::CREATURE_RESOURCE && M != EMarketMode::RESOURCE_ARTIFACT && M != EMarketMode::ARTIFACT_RESOURCE )); } @@ -1075,7 +1075,7 @@ void CMarketplaceWindow::updateTraderText() traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]); } -CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode) +CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) : CTradeWindow((Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 396b6e435..9b154f013 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -75,7 +75,7 @@ public: std::shared_ptr hRight; EType itemsType[2]; - EMarketMode::EMarketMode mode; + EMarketMode mode; std::shared_ptr ok; std::shared_ptr max; std::shared_ptr deal; @@ -83,7 +83,7 @@ public: std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; - CTradeWindow(std::string bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode); //c + CTradeWindow(std::string bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); //c void showAll(Canvas & to) override; @@ -95,7 +95,7 @@ public: void removeItems(const std::set> & toRemove); void removeItem(std::shared_ptr item); void getEmptySlots(std::set> & toRemove); - void setMode(EMarketMode::EMarketMode Mode); //mode setter + void setMode(EMarketMode Mode); //mode setter void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot @@ -118,9 +118,9 @@ class CMarketplaceWindow : public CTradeWindow std::shared_ptr titleLabel; std::shared_ptr arts; - bool printButtonFor(EMarketMode::EMarketMode M) const; + bool printButtonFor(EMarketMode M) const; - std::string getBackgroundForMode(EMarketMode::EMarketMode mode); + std::string getBackgroundForMode(EMarketMode mode); public: int r1, r2; //suggested amounts of traded resources bool madeTransaction; //if player made at least one transaction @@ -130,7 +130,7 @@ public: void sliderMoved(int to); void makeDeal(); void selectionChanged(bool side) override; //true == left - CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero = nullptr, EMarketMode::EMarketMode Mode = EMarketMode::RESOURCE_RESOURCE); + CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero = nullptr, EMarketMode Mode = EMarketMode::RESOURCE_RESOURCE); ~CMarketplaceWindow(); Point selectionOffset(bool Left) const override; @@ -157,7 +157,7 @@ public: std::shared_ptr expOnAltar; std::shared_ptr arts; - CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode::EMarketMode Mode); + CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); ~CAltarWindow(); void getExpValues(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index e7888cd3e..6e291bc7c 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -400,7 +400,7 @@ void CSplitWindow::sliderMoved(int to) setAmount(rightMin + to, false); } -CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector & skills, std::function callback) +CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, std::function callback) : CWindowObject(PLAYER_COLORED, "LVLUPBKG"), cb(callback) { @@ -434,9 +434,9 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill::PrimarySki levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); - skillIcon = std::make_shared("PSKIL42", pskill, 0, 174, 190); + skillIcon = std::make_shared("PSKIL42", static_cast(pskill), 0, 174, 190); - skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[pskill] + " +1"); + skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[static_cast(pskill)] + " +1"); } @@ -1029,7 +1029,7 @@ void CExchangeWindow::updateWidgets() for(int m=0; mgetPrimSkillLevel(static_cast(m)); + auto value = heroInst[leftRight]->getPrimSkillLevel(static_cast(m)); primSkillValues[leftRight][m]->setText(std::to_string(value)); } diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 16edc5808..2e6f2e6df 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -145,7 +145,7 @@ class CLevelWindow : public CWindowObject void selectionChanged(unsigned to); public: - CLevelWindow(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, std::function callback); + CLevelWindow(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, std::function callback); ~CLevelWindow(); }; diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index 508d1092f..77c95f78e 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -15,11 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BonusList; - -namespace PrimarySkill -{ - enum PrimarySkill : int8_t; -} +enum class PrimarySkill : int32_t; class DLL_LINKAGE AFactionMember: public IConstBonusProvider, public INativeTerrainProvider { @@ -51,7 +47,7 @@ public: /** Returns primskill of creature or hero. */ - int getPrimSkillLevel(PrimarySkill::PrimarySkill id) const; + int getPrimSkillLevel(PrimarySkill id) const; /** Returns morale of creature or hero. Taking absolute bonuses into account. For now, uses range from EGameSettings diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index 37579a53f..f2edcdf9a 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -54,7 +54,7 @@ int AFactionMember::getAttack(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_ATTACK"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -63,7 +63,7 @@ int AFactionMember::getDefense(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_DEFENSE"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -82,12 +82,12 @@ int AFactionMember::getMaxDamage(bool ranged) const return getBonusBearer()->valOfBonuses(selector, cachingStr); } -int AFactionMember::getPrimSkillLevel(PrimarySkill::PrimarySkill id) const +int AFactionMember::getPrimSkillLevel(PrimarySkill id) const { static const CSelector selectorAllSkills = Selector::type()(BonusType::PRIMARY_SKILL); static const std::string keyAllSkills = "type_PRIMARY_SKILL"; auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills); - auto ret = allSkills->valOfBonuses(Selector::subtype()(id)); + auto ret = allSkills->valOfBonuses(Selector::subtype()(static_cast(id))); auto minSkillValue = (id == PrimarySkill::SPELL_POWER || id == PrimarySkill::KNOWLEDGE) ? 1 : 0; return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves } @@ -183,4 +183,4 @@ bool ACreature::isLiving() const //TODO: theoreticaly there exists "LIVING" bonu } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 78de10cdf..205aa1ccf 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -113,13 +113,13 @@ FactionID CCreature::getFaction() const int32_t CCreature::getBaseAttack() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDefense() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } @@ -345,10 +345,10 @@ void CCreature::updateFrom(const JsonNode & data) addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); if(!configNode["attack"].isNull()) - addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); + addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); if(!configNode["defense"].isNull()) - addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); + addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); if(!configNode["damage"]["min"].isNull()) addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); @@ -603,8 +603,8 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); - cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK); - cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE); + cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); + cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); @@ -1030,11 +1030,11 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'A': b.type = BonusType::PRIMARY_SKILL; - b.subtype = PrimarySkill::ATTACK; + b.subtype = static_cast(PrimarySkill::ATTACK); break; case 'D': b.type = BonusType::PRIMARY_SKILL; - b.subtype = PrimarySkill::DEFENSE; + b.subtype = static_cast(PrimarySkill::DEFENSE); break; case 'M': //Max damage b.type = BonusType::CREATURE_DAMAGE; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index e430d2249..7a89fcf4c 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -631,7 +631,7 @@ bool CGameInfoCallback::hasAccess(std::optional playerId) const return !player || player->isSpectator() || gs->getPlayerRelations(*playerId, *player) != PlayerRelations::ENEMIES; } -EPlayerStatus::EStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const +EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const { const PlayerState *ps = gs->getPlayerState(player, verbose); ERROR_VERBOSE_OR_NOT_RET_VAL_IF(!ps, verbose, "No such player!", EPlayerStatus::WRONG); @@ -670,7 +670,7 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav return text; } -PlayerRelations::PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const +PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const { return gs->getPlayerRelations(color1, color2); } diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 9808a318d..caebb235a 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -60,9 +60,9 @@ public: virtual const Player * getPlayer(PlayerColor color) const = 0; // virtual int getResource(PlayerColor Player, EGameResID which) const = 0; // bool isVisible(int3 pos) const; -// PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; +// PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; // void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object -// EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player +// EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player // PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns virtual PlayerColor getLocalPlayer() const = 0; //player that is currently owning given client (if not a client, then returns current player) // const PlayerSettings * getPlayerSettings(PlayerColor color) const; @@ -148,9 +148,9 @@ public: const Player * getPlayer(PlayerColor color) const override; virtual const PlayerState * getPlayerState(PlayerColor color, bool verbose = true) const; virtual int getResource(PlayerColor Player, GameResID which) const; - virtual PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; + virtual PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; virtual void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object - virtual EPlayerStatus::EStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player + virtual EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player) virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 089dc2e5f..8d9e1b284 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -91,7 +91,7 @@ public: virtual void yourTurn(){}; //called AFTER playerStartsTurn(player) //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; + virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID)=0; // Show a dialog, player must take decision. If selection then he has to choose between one of given components, diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 5bda9995d..c60023ad4 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -207,9 +207,9 @@ CHeroClass::CHeroClass(): { } -void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill::PrimarySkill pSkill) const +void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const { - const auto & skillName = PrimarySkill::names[pSkill]; + const auto & skillName = NPrimarySkill::names[static_cast(pSkill)]; auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); //minimal value is 0 for attack and defense and 1 for spell power and knowledge auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; @@ -333,13 +333,13 @@ std::vector CHeroClassHandler::loadLegacyData() parser.readNumber(); // unused aggression - for(const auto & name : PrimarySkill::names) + for(const auto & name : NPrimarySkill::names) entry["primarySkills"][name].Float() = parser.readNumber(); - for(const auto & name : PrimarySkill::names) + for(const auto & name : NPrimarySkill::names) entry["lowLevelChance"][name].Float() = parser.readNumber(); - for(const auto & name : PrimarySkill::names) + for(const auto & name : NPrimarySkill::names) entry["highLevelChance"][name].Float() = parser.readNumber(); for(const auto & name : NSecondarySkill::names) @@ -547,7 +547,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = PrimarySkill::ATTACK; + bonus->subtype = static_cast(PrimarySkill::ATTACK); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); @@ -557,7 +557,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = PrimarySkill::DEFENSE; + bonus->subtype = static_cast(PrimarySkill::DEFENSE); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 3f11968fb..2e9c5df84 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -212,7 +212,7 @@ public: class DLL_LINKAGE CHeroClassHandler : public CHandlerBase { - void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill::PrimarySkill pSkill) const; + void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const; public: std::vector loadLegacyData() override; diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index fa1ab5a1e..1b304eebc 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -38,7 +38,7 @@ public: std::vector quests; //store info about all received quests bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory - EPlayerStatus::EStatus status; + EPlayerStatus status; std::optional daysWithoutCastle; TurnTimerInfo turnTimer; diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 85471ec32..04a928ffb 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -527,13 +527,13 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const b = createBonus(building, BonusType::LUCK, +2); break; case BuildingSubID::SPELL_POWER_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, PrimarySkill::SPELL_POWER); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::SPELL_POWER)); break; case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, PrimarySkill::ATTACK); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::ATTACK)); break; case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, PrimarySkill::DEFENSE); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::DEFENSE)); break; case BuildingSubID::LIGHTHOUSE: b = createBonus(building, BonusType::MOVEMENT, +500, playerPropagator, 0); diff --git a/lib/GameConstants.h b/lib/GameConstants.h index e22ffb2f5..d9667dd56 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -412,11 +412,15 @@ class TeleportChannelID : public BaseForID }; // Enum declarations -namespace PrimarySkill +enum class PrimarySkill : int32_t { - enum PrimarySkill : int8_t { NONE = -1, ATTACK, DEFENSE, SPELL_POWER, KNOWLEDGE, - EXPERIENCE = 4}; //for some reason changePrimSkill uses it -} + NONE = -1, + ATTACK, + DEFENSE, + SPELL_POWER, + KNOWLEDGE, + EXPERIENCE = 4 //for some reason changePrimSkill uses it +}; class SecondarySkillBase : public EntityBase { @@ -599,15 +603,12 @@ namespace BuildingSubID }; } -namespace EMarketMode +enum class EMarketMode : int32_t { - enum EMarketMode - { - RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, - ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, - MARTKET_AFTER_LAST_PLACEHOLDER - }; -} + RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, + ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, + MARTKET_AFTER_LAST_PLACEHOLDER +}; namespace MappedKeys { @@ -686,7 +687,7 @@ namespace MappedKeys { "treasury", BuildingSubID::TREASURY } }; - static const std::map MARKET_NAMES_TO_TYPES = + static const std::map MARKET_NAMES_TO_TYPES = { { "resource-resource", EMarketMode::RESOURCE_RESOURCE }, { "resource-player", EMarketMode::RESOURCE_PLAYER }, @@ -1040,29 +1041,16 @@ enum class EActionType : int8_t DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EActionType actionType); -class DLL_LINKAGE EDiggingStatus +enum class EDiggingStatus : int32_t { -public: - enum EEDiggingStatus - { - UNKNOWN = -1, - CAN_DIG = 0, - LACK_OF_MOVEMENT, - WRONG_TERRAIN, - TILE_OCCUPIED, - BACKPACK_IS_FULL - }; - - EDiggingStatus(EEDiggingStatus _num = UNKNOWN) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(EDiggingStatus, EEDiggingStatus) - - EEDiggingStatus num; + UNKNOWN = -1, + CAN_DIG = 0, + LACK_OF_MOVEMENT, + WRONG_TERRAIN, + TILE_OCCUPIED, + BACKPACK_IS_FULL }; -ID_LIKE_OPERATORS(EDiggingStatus, EDiggingStatus::EEDiggingStatus) - class DLL_LINKAGE EPathfindingLayer { public: @@ -1083,15 +1071,20 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EPathfindingLayer ID_LIKE_OPERATORS(EPathfindingLayer, EPathfindingLayer::EEPathfindingLayer) -namespace EPlayerStatus +enum class EPlayerStatus { - enum EStatus {WRONG = -1, INGAME, LOSER, WINNER}; -} + WRONG = -1, + INGAME, + LOSER, + WINNER +}; -namespace PlayerRelations +enum class PlayerRelations { - enum PlayerRelations {ENEMIES, ALLIES, SAME_PLAYER}; -} + ENEMIES, + ALLIES, + SAME_PLAYER +}; class ArtifactPosition { diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 747d71d72..30bed8824 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -92,7 +92,7 @@ public: virtual bool removeObject(const CGObjectInstance * obj)=0; virtual void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; - virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs=false)=0; + virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; virtual void showBlockingDialog(BlockingDialog *iw) =0; virtual void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) =0; //cb will be called when player closes garrison window diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 8d85df35e..a19c9f11e 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -98,7 +98,7 @@ public: virtual void heroCreated(const CGHeroInstance*){}; virtual void heroInGarrisonChange(const CGTownInstance *town){}; virtual void heroMoved(const TryMoveHero & details, bool verbose = true){}; - virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val){}; + virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val){}; virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val){}; virtual void heroManaPointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after spell casts virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index a883bd904..e5f14a03d 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -119,7 +119,7 @@ namespace JsonRandom std::vector ret; if(value.isStruct()) { - for(const auto & name : PrimarySkill::names) + for(const auto & name : NPrimarySkill::names) { ret.push_back(loadValue(value[name], rng)); } @@ -127,12 +127,12 @@ namespace JsonRandom if(value.isVector()) { ret.resize(GameConstants::PRIMARY_SKILLS, 0); - std::set defaultStats(std::begin(PrimarySkill::names), std::end(PrimarySkill::names)); + std::set defaultStats(std::begin(NPrimarySkill::names), std::end(NPrimarySkill::names)); for(const auto & element : value.Vector()) { auto key = loadKey(element, rng, defaultStats); defaultStats.erase(key); - int id = vstd::find_pos(PrimarySkill::names, key); + int id = vstd::find_pos(NPrimarySkill::names, key); if(id != -1) ret[id] += loadValue(element, rng); } diff --git a/lib/NetPacks.h b/lib/NetPacks.h index afcbe16c4..a958e9606 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -232,7 +232,7 @@ struct DLL_LINKAGE SetPrimSkill : public CPackForClient ui8 abs = 0; //0 - changes by value; 1 - sets to value ObjectInstanceID id; - PrimarySkill::PrimarySkill which = PrimarySkill::ATTACK; + PrimarySkill which = PrimarySkill::ATTACK; si64 val = 0; template void serialize(Handler & h, const int version) @@ -1318,7 +1318,7 @@ struct DLL_LINKAGE HeroLevelUp : public Query PlayerColor player; ObjectInstanceID heroId; - PrimarySkill::PrimarySkill primskill = PrimarySkill::ATTACK; + PrimarySkill primskill = PrimarySkill::ATTACK; std::vector skills; void applyGs(CGameState * gs) const; @@ -2425,7 +2425,7 @@ struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer ObjectInstanceID marketId; ObjectInstanceID heroId; - EMarketMode::EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; + EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] std::vector val; //units of sold resource diff --git a/lib/StringConstants.h b/lib/StringConstants.h index 7a8013330..e6190f756 100644 --- a/lib/StringConstants.h +++ b/lib/StringConstants.h @@ -29,7 +29,7 @@ namespace GameConstants const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; } -namespace PrimarySkill +namespace NPrimarySkill { const std::string names [GameConstants::PRIMARY_SKILLS] = { "attack", "defence", "spellpower", "knowledge" }; } diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index b2d5cd3a8..eb6cd3627 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -475,8 +475,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const static auto nativeTerrain = std::make_shared(); curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, PrimarySkill::ATTACK)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, PrimarySkill::DEFENSE)->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index f317d429d..098b4e746 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -342,8 +342,8 @@ CUnitState::CUnitState(): totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1)), 0), maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2)), 0), - attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK), 0), - defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE), 0), + attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)), 0), + defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, SpellID::CLONE))), cloneID(-1) diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 5ef8c7bed..b9c7cedb7 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -50,9 +50,9 @@ DamageRange DamageCalculator::getBaseDamageSingle() const if(info.attacker->hasBonus(selectorSiedgeWeapon, cachingStrSiedgeWeapon) && info.attacker->creatureIndex() != CreatureID::ARROW_TOWERS) { - auto retrieveHeroPrimSkill = [&](int skill) -> int + auto retrieveHeroPrimSkill = [&](PrimarySkill skill) -> int { - std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, skill))); + std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(skill)))); return b ? b->val : 0; }; diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 6a24f983e..cd493b17b 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -139,7 +139,7 @@ static JsonNode subtypeToJson(BonusType type, int subtype) switch(type) { case BonusType::PRIMARY_SKILL: - return JsonUtils::stringNode("primSkill." + PrimarySkill::names[subtype]); + return JsonUtils::stringNode("primSkill." + NPrimarySkill::names[subtype]); case BonusType::SPECIAL_SPELL_LEV: case BonusType::SPECIFIC_SPELL_DAMAGE: case BonusType::SPELL: diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 9b5e78cd6..2e6e15c2e 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1297,7 +1297,7 @@ UpgradeInfo CGameState::fillUpgradeInfo(const CStackInstance &stack) const return ret; } -PlayerRelations::PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const +PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const { if ( color1 == color2 ) return PlayerRelations::SAME_PLAYER; diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index ca906f6ff..d151e91d3 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -113,7 +113,7 @@ public: BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; - PlayerRelations::PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; + PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists void calculatePaths(const std::shared_ptr & config) override; diff --git a/lib/gameState/InfoAboutArmy.cpp b/lib/gameState/InfoAboutArmy.cpp index 21531787e..12d6fd81c 100644 --- a/lib/gameState/InfoAboutArmy.cpp +++ b/lib/gameState/InfoAboutArmy.cpp @@ -125,7 +125,7 @@ void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLe for (int i = 0; i < GameConstants::PRIMARY_SKILLS ; i++) { - details->primskills[i] = h->getPrimSkillLevel(static_cast(i)); + details->primskills[i] = h->getPrimSkillLevel(static_cast(i)); } if (infoLevel == EInfoLevel::INBATTLE) details->manaLimit = h->manaLimit(); diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 53e73d8d8..58ea22ca7 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -141,7 +141,7 @@ class MarketInstanceConstructor : public CDefaultObjectTypeHandler protected: void initTypeData(const JsonNode & config) override; - std::set marketModes; + std::set marketModes; JsonNode predefinedOffer; int marketEfficiency; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 3253e3e36..8f4b2d982 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -173,12 +173,12 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const return; } - PlayerRelations::PlayerRelations relations = cb->gameState()->getPlayerRelations( h->tempOwner, tempOwner ); + PlayerRelations relations = cb->gameState()->getPlayerRelations( h->tempOwner, tempOwner ); if ( relations == PlayerRelations::ALLIES ) return;//do not allow recruiting or capturing - if( !relations && stacksCount() > 0) //object is guarded, owned by enemy + if(relations == PlayerRelations::ENEMIES && stacksCount() > 0) //object is guarded, owned by enemy { BlockingDialog bd(true,false); bd.player = h->tempOwner; @@ -194,7 +194,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const } // TODO this shouldn't be hardcoded - if(!relations && ID != Obj::WAR_MACHINE_FACTORY && ID != Obj::REFUGEE_CAMP) + if(relations == PlayerRelations::ENEMIES && ID != Obj::WAR_MACHINE_FACTORY && ID != Obj::REFUGEE_CAMP) { cb->setOwner(this, h->tempOwner); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 309b65468..a52d5940e 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -315,7 +315,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { for(int g=0; g(g), type->heroClass->primarySkillInitial[g]); + pushPrimSkill(static_cast(g), type->heroClass->primarySkillInitial[g]); } } if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::DEFAULT, -1)) //set secondary skills to default @@ -456,7 +456,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (ID == Obj::HERO) { - if( cb->gameState()->getPlayerRelations(tempOwner, h->tempOwner)) //our or ally hero + if( cb->gameState()->getPlayerRelations(tempOwner, h->tempOwner) != PlayerRelations::ENEMIES) { //exchange cb->heroExchange(h->id, id); @@ -997,11 +997,11 @@ int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const return sp->getCost(getSpellSchoolLevel(sp)); } -void CGHeroInstance::pushPrimSkill( PrimarySkill::PrimarySkill which, int val ) +void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) { - assert(!hasBonus(Selector::typeSubtype(BonusType::PRIMARY_SKILL, which) + assert(!hasBonus(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(which)) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)))); - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), which)); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), static_cast(which))); } EAlignment CGHeroInstance::getAlignment() const @@ -1314,7 +1314,7 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() return skills; } -PrimarySkill::PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const +PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & rand) const { assert(gainsLevel()); int randomValue = rand.nextInt(99); @@ -1338,7 +1338,7 @@ PrimarySkill::PrimarySkill CGHeroInstance::nextPrimarySkill(CRandomGenerator & r randomValue = 100 / GameConstants::PRIMARY_SKILLS; } logGlobal->trace("The hero gets the primary skill %d with a probability of %d %%.", primarySkill, randomValue); - return static_cast(primarySkill); + return static_cast(primarySkill); } std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerator & rand) const @@ -1372,12 +1372,12 @@ std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerato return chosenSecondarySkill; } -void CGHeroInstance::setPrimarySkill(PrimarySkill::PrimarySkill primarySkill, si64 value, ui8 abs) +void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 abs) { if(primarySkill < PrimarySkill::EXPERIENCE) { auto skill = getBonusLocalFirst(Selector::type()(BonusType::PRIMARY_SKILL) - .And(Selector::subtype()(primarySkill)) + .And(Selector::subtype()(static_cast(primarySkill))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); assert(skill); @@ -1578,7 +1578,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, i).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); - handler.serializeInt(PrimarySkill::names[i], value, 0); + handler.serializeInt(NPrimarySkill::names[i], value, 0); } } } @@ -1591,8 +1591,8 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) { int value = 0; - handler.serializeInt(PrimarySkill::names[i], value, 0); - pushPrimSkill(static_cast(i), value); + handler.serializeInt(NPrimarySkill::names[i], value, 0); + pushPrimSkill(static_cast(i), value); } } } diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a2ca01d81..65be37cef 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -181,7 +181,7 @@ public: bool gainsLevel() const; /// Returns the next primary skill on level up. Can only be called if hero can gain a level up. - PrimarySkill::PrimarySkill nextPrimarySkill(CRandomGenerator & rand) const; + PrimarySkill nextPrimarySkill(CRandomGenerator & rand) const; /// Returns the next secondary skill randomly on level up. Can only be called if hero can gain a level up. std::optional nextSecondarySkill(CRandomGenerator & rand) const; @@ -195,7 +195,7 @@ public: bool canLearnSkill() const; bool canLearnSkill(const SecondarySkill & which) const; - void setPrimarySkill(PrimarySkill::PrimarySkill primarySkill, si64 value, ui8 abs); + void setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 abs); void setSecSkillLevel(const SecondarySkill & which, int val, bool abs); // abs == 0 - changes by value; 1 - sets to value void levelUp(const std::vector & skills); @@ -233,7 +233,7 @@ public: void removeArtifact(ArtifactPosition pos) override; void initExp(CRandomGenerator & rand); void initArmy(CRandomGenerator & rand, IArmyDescriptor *dst = nullptr); - void pushPrimSkill(PrimarySkill::PrimarySkill which, int val); + void pushPrimSkill(PrimarySkill which, int val); ui8 maxlevelsToMagicSchool() const; ui8 maxlevelsToWisdom() const; void recreateSecondarySkillsBonuses(); diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index 2d182cfc8..747c925f4 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -38,17 +38,17 @@ int CGMarket::getMarketEfficiency() const return marketEfficiency; } -bool CGMarket::allowsTrade(EMarketMode::EMarketMode mode) const +bool CGMarket::allowsTrade(EMarketMode mode) const { return marketModes.count(mode); } -int CGMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const +int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const { return -1; } -std::vector CGMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGMarket::availableItemsIds(EMarketMode mode) const { if(allowsTrade(mode)) return IMarket::availableItemsIds(mode); @@ -59,7 +59,7 @@ CGMarket::CGMarket() { } -std::vector CGBlackMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const { switch(mode) { @@ -110,7 +110,7 @@ void CGUniversity::initObj(CRandomGenerator & rand) } } -std::vector CGUniversity::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGUniversity::availableItemsIds(EMarketMode mode) const { switch (mode) { diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index d9e98f975..aff11ce56 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -18,7 +18,7 @@ class DLL_LINKAGE CGMarket : public CGObjectInstance, public IMarket { public: - std::set marketModes; + std::set marketModes; int marketEfficiency; //window variables @@ -32,9 +32,9 @@ public: ///IMarket int getMarketEfficiency() const override; - bool allowsTrade(EMarketMode::EMarketMode mode) const override; - int availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + bool allowsTrade(EMarketMode mode) const override; + int availableUnits(EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited + std::vector availableItemsIds(EMarketMode mode) const override; template void serialize(Handler &h, const int version) { @@ -52,7 +52,7 @@ public: std::vector artifacts; //available artifacts void newTurn(CRandomGenerator & rand) const override; //reset artifacts for black market every month - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; template void serialize(Handler &h, const int version) { @@ -66,7 +66,7 @@ class DLL_LINKAGE CGUniversity : public CGMarket public: std::vector skills; //available skills - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; void initObj(CRandomGenerator & rand) override;//set skills for trade void onHeroVisit(const CGHeroInstance * h) const override; //open window diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 40de75105..f7bc248f2 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -103,7 +103,7 @@ void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const //give prim skills for(int i=0; ichangePrimSkill(h,static_cast(i),primskills[i],false); + cb->changePrimSkill(h,static_cast(i),primskills[i],false); assert(!cb->isVisitCoveredByAnotherQuery(this, h)); @@ -385,7 +385,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { auto s = handler.enterStruct("primarySkills"); for(int idx = 0; idx < primskills.size(); idx ++) - handler.serializeInt(PrimarySkill::names[idx], primskills[idx], 0); + handler.serializeInt(NPrimarySkill::names[idx], primskills[idx], 0); } } diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 556bc91cb..098b519d5 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -199,7 +199,7 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const { si64 val = 0; InfoWindow iw; - PrimarySkill::PrimarySkill what = PrimarySkill::NONE; + PrimarySkill what = PrimarySkill::NONE; switch(bType) { diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 0472e809a..3ad5f2948 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -270,7 +270,7 @@ void CGTownInstance::blockingDialogAnswered(const CGHeroInstance *hero, ui32 ans void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const { - if(!cb->gameState()->getPlayerRelations( getOwner(), h->getOwner() ))//if this is enemy + if(cb->gameState()->getPlayerRelations( getOwner(), h->getOwner() ) == PlayerRelations::ENEMIES) { if(armedGarrison() || visitingHero) { @@ -701,7 +701,7 @@ int CGTownInstance::getMarketEfficiency() const return marketCount; } -bool CGTownInstance::allowsTrade(EMarketMode::EMarketMode mode) const +bool CGTownInstance::allowsTrade(EMarketMode mode) const { switch(mode) { @@ -727,7 +727,7 @@ bool CGTownInstance::allowsTrade(EMarketMode::EMarketMode mode) const } } -std::vector CGTownInstance::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const { if(mode == EMarketMode::RESOURCE_ARTIFACT) { diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 1e914df89..6f8298143 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -138,8 +138,8 @@ public: EGeneratorState shipyardStatus() const override; const IObjectInterface * getObject() const override; int getMarketEfficiency() const override; //=market count - bool allowsTrade(EMarketMode::EMarketMode mode) const override; - std::vector availableItemsIds(EMarketMode::EMarketMode mode) const override; + bool allowsTrade(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; void setType(si32 ID, si32 subID) override; void updateAppearance(); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index b935e3edc..90eb106b2 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -131,7 +131,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const case MISSION_PRIMARY_STAT: for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) { - if(h->getPrimSkillLevel(static_cast(i)) < static_cast(m2stats[i])) + if(h->getPrimSkillLevel(static_cast(i)) < static_cast(m2stats[i])) return false; } return true; @@ -503,7 +503,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi m2stats.resize(GameConstants::PRIMARY_SKILLS); for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(PrimarySkill::names[i], m2stats[i], 0); + handler.serializeInt(NPrimarySkill::names[i], m2stats[i], 0); } break; case MISSION_KILL_HERO: @@ -865,7 +865,7 @@ void CGSeerHut::completeQuest (const CGHeroInstance * h) const //reward cb->giveResource(h->getOwner(), static_cast(rID), rVal); break; case PRIMARY_SKILL: - cb->changePrimSkill(h, static_cast(rID), rVal, false); + cb->changePrimSkill(h, static_cast(rID), rVal, false); break; case SECONDARY_SKILL: cb->changeSecSkill(h, SecondarySkill(rID), rVal, false); @@ -982,7 +982,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) identifier = GameConstants::RESOURCE_NAMES[rID]; break; case PRIMARY_SKILL: - identifier = PrimarySkill::names[rID]; + identifier = NPrimarySkill::names[rID]; break; case SECONDARY_SKILL: identifier = CSkillHandler::encodeSkill(rID); diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index a6c8528a9..bf38c954f 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -20,7 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN -bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMarketMode mode) const +bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const { switch(mode) { @@ -122,12 +122,12 @@ bool IMarket::getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMar return true; } -bool IMarket::allowsTrade(EMarketMode::EMarketMode mode) const +bool IMarket::allowsTrade(EMarketMode mode) const { return false; } -int IMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const +int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const { switch(mode) { @@ -140,7 +140,7 @@ int IMarket::availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) } } -std::vector IMarket::availableItemsIds(EMarketMode::EMarketMode mode) const +std::vector IMarket::availableItemsIds(EMarketMode mode) const { std::vector ret; switch(mode) @@ -166,12 +166,12 @@ IMarket::IMarket() { } -std::vector IMarket::availableModes() const +std::vector IMarket::availableModes() const { - std::vector ret; - for (int i = 0; i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; i++) - if(allowsTrade(static_cast(i))) - ret.push_back(static_cast(i)); + std::vector ret; + for (EMarketMode i = static_cast(0); i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; vstd::next(i, 1)) + if(allowsTrade(i)) + ret.push_back(i); return ret; } diff --git a/lib/mapObjects/IMarket.h b/lib/mapObjects/IMarket.h index 7e9d79359..f279ffe6d 100644 --- a/lib/mapObjects/IMarket.h +++ b/lib/mapObjects/IMarket.h @@ -22,12 +22,12 @@ public: virtual ~IMarket() {} virtual int getMarketEfficiency() const = 0; - virtual bool allowsTrade(EMarketMode::EMarketMode mode) const; - virtual int availableUnits(EMarketMode::EMarketMode mode, int marketItemSerial) const; //-1 if unlimited - virtual std::vector availableItemsIds(EMarketMode::EMarketMode mode) const; + virtual bool allowsTrade(EMarketMode mode) const; + virtual int availableUnits(EMarketMode mode, int marketItemSerial) const; //-1 if unlimited + virtual std::vector availableItemsIds(EMarketMode mode) const; - bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode::EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units - std::vector availableModes() const; + bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units + std::vector availableModes() const; static const IMarket *castFrom(const CGObjectInstance *obj, bool verbose = true); }; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index b0d0434e6..0cc036acd 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -68,14 +68,14 @@ bool CTeamVisited::wasVisited(const TeamID & team) const //CGMine void CGMine::onHeroVisit( const CGHeroInstance * h ) const { - int relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); + auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); - if(relations == 2) //we're visiting our mine + if(relations == PlayerRelations::SAME_PLAYER) //we're visiting our mine { cb->showGarrisonDialog(id,h->id,true); return; } - else if (relations == 1)//ally + else if (relations == PlayerRelations::ALLIES)//ally return; if(stacksCount()) //Mine is guarded @@ -1088,7 +1088,7 @@ void CGScholar::onHeroVisit( const CGHeroInstance * h ) const switch (type) { case PRIM_SKILL: - cb->changePrimSkill(h,static_cast(bid),+1); + cb->changePrimSkill(h,static_cast(bid),+1); iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0); break; case SECONDARY_SKILL: @@ -1143,7 +1143,7 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) switch(bonusType) { case PRIM_SKILL: - value = PrimarySkill::names[bonusID]; + value = NPrimarySkill::names[bonusID]; handler.serializeString("rewardPrimSkill", value); break; case SECONDARY_SKILL: @@ -1195,15 +1195,15 @@ void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) void CGGarrison::onHeroVisit (const CGHeroInstance *h) const { - int ally = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); - if (!ally && stacksCount() > 0) { + auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); + if (relations == PlayerRelations::ENEMIES && stacksCount() > 0) { //TODO: Find a way to apply magic garrison effects in battle. cb->startBattleI(h, this); return; } //New owner. - if (!ally) + if (relations == PlayerRelations::ENEMIES) cb->setOwner(this, h->tempOwner); cb->showGarrisonDialog(id, h->id, removableUnits); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 7bc802a06..2c03c42c6 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -861,7 +861,7 @@ void CMapLoaderH3M::readPredefinedHeroes() { for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++) { - hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); + hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); } } map->predefinedHeroes.emplace_back(hero); @@ -1029,7 +1029,7 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi object->primskills.resize(GameConstants::PRIMARY_SKILLS); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) - object->primskills[x] = static_cast(reader->readUInt8()); + object->primskills[x] = reader->readUInt8(); int gabn = reader->readUInt8(); //number of gained abilities for(int oo = 0; oo < gabn; ++oo) @@ -1768,7 +1768,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx) { - object->pushPrimSkill(static_cast(xx), reader->readUInt8()); + object->pushPrimSkill(static_cast(xx), reader->readUInt8()); } } } diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 010a9a4b5..bed4f0805 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -57,8 +57,8 @@ CModHandler::CModHandler() for(int i=0; iregisterObject(ModScope::scopeBuiltin(), "primSkill", PrimarySkill::names[i], i); - identifiers->registerObject(ModScope::scopeBuiltin(), "primarySkill", PrimarySkill::names[i], i); + identifiers->registerObject(ModScope::scopeBuiltin(), "primSkill", NPrimarySkill::names[i], i); + identifiers->registerObject(ModScope::scopeBuiltin(), "primarySkill", NPrimarySkill::names[i], i); } } diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index 77effdfcb..db570be15 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -190,8 +190,8 @@ struct DLL_LINKAGE PathNodeInfo const TerrainTile * tile; int3 coord; bool guarded; - PlayerRelations::PlayerRelations objectRelations; - PlayerRelations::PlayerRelations heroRelations; + PlayerRelations objectRelations; + PlayerRelations heroRelations; bool isInitialPosition; PathNodeInfo(); diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index f34aa8055..d8ac6d99c 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -57,7 +57,7 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R } for(int i=0; i< info.reward.primary.size(); i++) - cb->changePrimSkill(hero, static_cast(i), info.reward.primary[i], false); + cb->changePrimSkill(hero, static_cast(i), info.reward.primary[i], false); si64 expToGive = 0; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 2f5e25049..bf3195edb 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -74,7 +74,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(size_t i=0; i hero->getPrimSkillLevel(static_cast(i))) + if(primary[i] > hero->getPrimSkillLevel(static_cast(i))) return false; } diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index bd1329f46..f9e2ddcb1 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -46,7 +46,7 @@ void QuestWidget::obtainData() case CQuest::Emission::MISSION_PRIMARY_STAT: activeId = true; activeAmount = true; - for(auto s : PrimarySkill::names) + for(auto s : NPrimarySkill::names) ui->targetId->addItem(QString::fromStdString(s)); for(int i = 0; i < seerhut.quest->m2stats.size(); ++i) { @@ -123,7 +123,7 @@ QString QuestWidget::commitChanges() seerhut.quest->m13489val = ui->targetAmount->text().toInt(); return QString("Reach lvl ").append(ui->targetAmount->text()); case CQuest::Emission::MISSION_PRIMARY_STAT: - seerhut.quest->m2stats.resize(sizeof(PrimarySkill::names), 0); + seerhut.quest->m2stats.resize(sizeof(NPrimarySkill::names), 0); seerhut.quest->m2stats[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); //TODO: support multiple stats return ui->targetId->currentText().append(ui->targetAmount->text()); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index ea2b94e9e..f131ae6a9 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -67,7 +67,7 @@ QList RewardsWidget::getListForType(RewardType typeId) break; case RewardType::PRIMARY_SKILL: - for(auto s : PrimarySkill::names) + for(auto s : NPrimarySkill::names) result.append(QString::fromStdString(s)); break; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index d9d17f78c..7ed103c5e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -248,11 +248,11 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) { case ECommander::ATTACK: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::ATTACK; + scp.accumulatedBonus.subtype = static_cast(PrimarySkill::ATTACK); break; case ECommander::DEFENSE: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = PrimarySkill::DEFENSE; + scp.accumulatedBonus.subtype = static_cast(PrimarySkill::DEFENSE); break; case ECommander::HEALTH: scp.accumulatedBonus.type = BonusType::STACK_HEALTH; @@ -368,7 +368,7 @@ void CGameHandler::expGiven(const CGHeroInstance *hero) // levelUpHero(hero); } -void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs) +void CGameHandler::changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs) { if (which == PrimarySkill::EXPERIENCE) // Check if scenario limit reached { @@ -1612,7 +1612,7 @@ void CGameHandler::heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) { auto h1 = getHero(hero1), h2 = getHero(hero2); - if (getPlayerRelations(h1->getOwner(), h2->getOwner())) + if (getPlayerRelations(h1->getOwner(), h2->getOwner()) != PlayerRelations::ENEMIES) { auto exchange = std::make_shared(this, h1, h2); ExchangeDialog hex; @@ -3590,7 +3590,7 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC bool CGameHandler::dig(const CGHeroInstance *h) { if (h->diggingStatus() != EDiggingStatus::CAN_DIG) //checks for terrain and movement - COMPLAIN_RETF("Hero cannot dig (error code %d)!", h->diggingStatus()); + COMPLAIN_RETF("Hero cannot dig (error code %d)!", static_cast(h->diggingStatus())); createObject(h->visitablePos(), Obj::HOLE, 0 ); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 555257537..64b29bfd2 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -106,7 +106,7 @@ public: bool removeObject(const CGObjectInstance * obj) override; void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs=false) override; + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; void showBlockingDialog(BlockingDialog *iw) override; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index e438c1ea3..42581078f 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -159,11 +159,11 @@ bool BattleActionProcessor::doDefendAction(const BattleAction & ba) //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) SetStackEffect sse; - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, PrimarySkill::DEFENSE, BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, PrimarySkill::DEFENSE, BonusValueType::ADDITIVE_VALUE); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)); + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE))); int oldDefenceValue = defence.totalValue(); defence.push_back(std::make_shared(defenseBonusToAdd)); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 79b5ce357..52b20ad34 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -53,8 +53,8 @@ public: { bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, 0)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, PrimarySkill::ATTACK)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, PrimarySkill::DEFENSE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, static_cast(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, static_cast(PrimarySkill::DEFENSE))); bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, 0)); diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index f6ed9e856..6ec37e5c1 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -43,7 +43,7 @@ public: bool removeObject(const CGObjectInstance * obj) override {return false;} void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) override {}; void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill::PrimarySkill which, si64 val, bool abs=false) override {} + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} void showBlockingDialog(BlockingDialog *iw) override {} void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index b5bddc269..d9205f4c5 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -71,9 +71,9 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, PrimarySkill::KNOWLEDGE); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, static_cast(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, PrimarySkill::KNOWLEDGE); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, static_cast(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); From 0240ee886d9a501296c382d0ca4ca5a8f19e136a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 19 Aug 2023 22:35:44 +0300 Subject: [PATCH 0185/1248] Converted (almost) all namespace enum's to enum classes --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/VCAI/BuildingManager.cpp | 4 +- AI/VCAI/VCAI.cpp | 2 +- CCallback.cpp | 2 +- client/battle/BattleWindow.cpp | 4 +- client/lobby/OptionsTab.cpp | 14 ++-- client/windows/CCastleInterface.cpp | 16 ++-- client/windows/CCastleInterface.h | 6 +- client/windows/GUIClasses.cpp | 2 +- include/vcmi/FactionMember.h | 2 +- lib/CGameInfoCallback.cpp | 4 +- lib/CGameInfoCallback.h | 12 +-- lib/GameConstants.h | 92 ++++++++--------------- lib/battle/BattleHex.h | 8 ++ lib/battle/CBattleInfoCallback.cpp | 20 ++--- lib/battle/CBattleInfoCallback.h | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameState.h | 2 +- lib/gameState/SThievesGuildInfo.h | 2 +- lib/mapping/CMapHeader.h | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- lib/rmg/RmgMap.cpp | 2 +- lib/rmg/RmgMap.h | 2 +- lib/rmg/RmgObject.cpp | 2 +- lib/rmg/TileInfo.cpp | 4 +- lib/rmg/TileInfo.h | 6 +- lib/spells/ISpellMechanics.cpp | 2 +- lib/spells/ISpellMechanics.h | 4 +- server/battles/BattleActionProcessor.cpp | 26 +++---- server/battles/BattleProcessor.cpp | 10 +-- test/mock/mock_IGameInfoCallback.h | 2 +- test/mock/mock_spells_Mechanics.h | 2 +- 34 files changed, 124 insertions(+), 144 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index d796134cd..05ea71ba9 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -357,7 +357,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE && cb->battleGetGateState() == EGateState::BLOCKED - && ap.attack.defender->coversPos(ESiegeHex::GATE_BRIDGE)) + && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) { return EvaluationResult::INEFFECTIVE_SCORE; } diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 955269029..85acbe46c 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -773,7 +773,7 @@ void AIGateway::makeTurn() { MAKING_TURN; - auto day = cb->getDate(Date::EDateType::DAY); + auto day = cb->getDate(Date::DAY); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); boost::shared_lock gsLock(CGameState::mutex); diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 359ccc1ca..54ce129ab 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -160,7 +160,7 @@ void BuildAnalyzer::update() updateDailyIncome(); - if(ai->cb->getDate(Date::EDateType::DAY) == 1) + if(ai->cb->getDate(Date::DAY) == 1) { goldPreasure = 1; } diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index 083abe9c4..5aeba8c6b 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -38,7 +38,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID for (BuildingID buildID : toBuild) { - EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); + EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::HAVE_CAPITAL || canBuild == EBuildingState::FORBIDDEN || canBuild == EBuildingState::NO_WATER) return false; //we won't be able to build this } @@ -52,7 +52,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID { const CBuilding * b = t->town->buildings.at(buildID); - EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID); + EBuildingState canBuild = cb->canBuildStructure(t, buildID); if (canBuild == EBuildingState::ALLOWED) { PotentialBuilding pb; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index d2da3e13a..68a121f41 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -776,7 +776,7 @@ void VCAI::makeTurn() { MAKING_TURN; - auto day = cb->getDate(Date::EDateType::DAY); + auto day = cb->getDate(Date::DAY); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); boost::shared_lock gsLock(CGameState::mutex); diff --git a/CCallback.cpp b/CCallback.cpp index 227e2f0a9..1b1b034b9 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -195,7 +195,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) if(town->tempOwner!=player) return false; - if(!canBuildStructure(town, buildingID)) + if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED) return false; BuildStructure pack(town->id,buildingID); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 3f1b402f4..538b60b6c 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -523,7 +523,7 @@ void BattleWindow::bSpellf() CCS->curh->set(Cursor::Map::POINTER); - ESpellCastProblem::ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); + ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); if(spellCastProblem == ESpellCastProblem::OK) { @@ -629,7 +629,7 @@ void BattleWindow::blockUI(bool on) if(hero) { - ESpellCastProblem::ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO); + ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO); //if magic is blocked, we leave button active, so the message can be displayed after button click canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 67dee51f9..1cb24adcb 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -441,7 +441,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) if(initialHero >= -1) allowedBonus.push_back(0); // artifact allowedBonus.push_back(1); // gold - if(initialFaction >= 0) + if(initialFaction.getNum() >= 0) allowedBonus.push_back(2); // resource recreate(); @@ -451,7 +451,7 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) { double additionalItems = 1; // random - if(faction < 0) + if(faction.getNum() < 0) return std::ceil(((double)allowedFactions.size() + additionalItems) / elementsPerLine); int count = 0; @@ -568,8 +568,8 @@ void OptionsTab::SelectionWindow::genContentFactions() set.castle = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); - drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedFaction == PlayerSettings::RANDOM) + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedFaction.getNum() == PlayerSettings::RANDOM) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedFactions) @@ -677,7 +677,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { set.castle = PlayerSettings::RANDOM; } - if(set.castle != PlayerSettings::NONE) + if(set.castle.getNum() != PlayerSettings::NONE) { if(!doApply) { @@ -800,7 +800,7 @@ void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) if(type == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) return; - if(type == SelType::HERO && ((pi.defaultHero() != -1 || settings.castle < 0) || foreignPlayer)) + if(type == SelType::HERO && ((pi.defaultHero() != -1 || settings.castle.getNum() < 0) || foreignPlayer)) return; if(type == SelType::BONUS && foreignPlayer) @@ -954,4 +954,4 @@ void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() buttonBonusLeft->enable(); buttonBonusRight->enable(); } -} \ No newline at end of file +} diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 041fdf0d4..d7bd05405 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1351,9 +1351,9 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * }; icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 2, 2); - header = std::make_shared("TPTHBAR", panelIndex[state], 0, 1, 73); - if(iconIndex[state] >=0) - mark = std::make_shared("TPTHCHK", iconIndex[state], 0, 136, 56); + header = std::make_shared("TPTHBAR", panelIndex[static_cast(state)], 0, 1, 73); + if(iconIndex[static_cast(state)] >=0) + mark = std::make_shared("TPTHCHK", iconIndex[static_cast(state)], 0, 136, 56); name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); //todo: add support for all possible states @@ -1371,7 +1371,7 @@ void CHallInterface::CBuildingBox::hover(bool on) else if(state==EBuildingState::CANT_BUILD_TODAY) toPrint = CGI->generaltexth->allTexts[223]; else - toPrint = CGI->generaltexth->hcommands[state]; + toPrint = CGI->generaltexth->hcommands[static_cast(state)]; boost::algorithm::replace_first(toPrint,"%s",building->getNameTranslated()); GH.statusbar()->write(toPrint); } @@ -1439,7 +1439,7 @@ CHallInterface::CHallInterface(const CGTownInstance * Town): } } -CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, int state, bool rightClick): +CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, EBuildingState state, bool rightClick): CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), "TPUBUILD"), town(Town), building(Building) @@ -1482,7 +1482,7 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin buy = std::make_shared(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); buy->setBorderColor(Colors::METALLIC_GOLD); - buy->block(state!=7 || LOCPLINT->playerID != town->tempOwner); + buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner); cancel = std::make_shared(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); cancel->setBorderColor(Colors::METALLIC_GOLD); @@ -1495,11 +1495,11 @@ void CBuildWindow::buyFunc() GH.windows().popWindows(2); //we - build window and hall screen } -std::string CBuildWindow::getTextForState(int state) +std::string CBuildWindow::getTextForState(EBuildingState state) { std::string ret; if(state < EBuildingState::ALLOWED) - ret = CGI->generaltexth->hcommands[state]; + ret = CGI->generaltexth->hcommands[static_cast(state)]; switch (state) { case EBuildingState::ALREADY_PRESENT: diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index d3bbc29ae..8aa56bbf1 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -265,7 +265,7 @@ class CHallInterface : public CStatusbarWindow const CGTownInstance * town; const CBuilding * building; - ui32 state;//Buildings::EBuildStructure enum + EBuildingState state; std::shared_ptr header; std::shared_ptr icon; @@ -303,10 +303,10 @@ class CBuildWindow: public CStatusbarWindow std::shared_ptr buy; std::shared_ptr cancel; - std::string getTextForState(int state); + std::string getTextForState(EBuildingState state); void buyFunc(); public: - CBuildWindow(const CGTownInstance *Town, const CBuilding * building, int State, bool rightClick); + CBuildWindow(const CGTownInstance *Town, const CBuilding * building, EBuildingState State, bool rightClick); }; //Small class to display diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 6e291bc7c..cecd521ef 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1711,7 +1711,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): } else if(it.second != EAiTactic::RANDOM) { - text = CGI->generaltexth->arraytxt[168 + it.second]; + text = CGI->generaltexth->arraytxt[168 + static_cast(it.second)]; } personalities.push_back(std::make_shared(283 + 66*counter, 459, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text)); diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index 77c95f78e..d60509f95 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BonusList; -enum class PrimarySkill : int32_t; +enum class PrimarySkill : int8_t; class DLL_LINKAGE AFactionMember: public IConstBonusProvider, public INativeTerrainProvider { diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 7a89fcf4c..de685aa14 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -405,7 +405,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero return true; } -int CGameInfoCallback::getDate(Date::EDateType mode) const +int CGameInfoCallback::getDate(Date mode) const { //boost::shared_lock lock(*gs->mx); return gs->getDate(mode); @@ -551,7 +551,7 @@ std::shared_ptr> CGameInfoCallback::ge return std::shared_ptr>(ptr); } -EBuildingState::EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID ) +EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, BuildingID ID ) { ERROR_RET_VAL_IF(!canGetFullInfo(t), "Town is not owned!", EBuildingState::TOWN_NOT_OWNED); diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index caebb235a..54ab697fb 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -52,7 +52,7 @@ public: //TODO: all other public methods of CGameInfoCallback // //various - virtual int getDate(Date::EDateType mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month // const StartInfo * getStartInfo(bool beforeRandomization = false)const; virtual bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill @@ -107,13 +107,13 @@ public: // const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) // std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited // std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; -// EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements +// EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements // virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; //from gs // const TeamState *getTeam(TeamID teamID) const; // const TeamState *getPlayerTeam(PlayerColor color) const; -// EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements +// EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements //teleport // std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; @@ -140,7 +140,7 @@ protected: public: //various - int getDate(Date::EDateType mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill @@ -205,13 +205,13 @@ public: //virtual const CGTownInstance * getTownInfo(int val, bool mode)const; //mode = 0 -> val = player town serial; mode = 1 -> val = object id (serial) virtual std::vector getAvailableHeroes(const CGObjectInstance * townOrTavern) const; //heroes that can be recruited virtual std::string getTavernRumor(const CGObjectInstance * townOrTavern) const; - virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + virtual EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID);//// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements virtual bool getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject = nullptr) const; //from gs virtual const TeamState *getTeam(TeamID teamID) const; virtual const TeamState *getPlayerTeam(PlayerColor color) const; - //virtual EBuildingState::EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements + //virtual EBuildingState canBuildStructure(const CGTownInstance *t, BuildingID ID) const;// 0 - no more than one capitol, 1 - lack of water, 2 - forbidden, 3 - Add another level to Mage Guild, 4 - already built, 5 - cannot build, 6 - cannot afford, 7 - build, 8 - lack of requirements //teleport virtual std::vector getVisibleTeleportObjects(std::vector ids, PlayerColor player) const; diff --git a/lib/GameConstants.h b/lib/GameConstants.h index d9667dd56..5c8fc936f 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -412,7 +412,7 @@ class TeleportChannelID : public BaseForID }; // Enum declarations -enum class PrimarySkill : int32_t +enum class PrimarySkill : int8_t { NONE = -1, ATTACK, @@ -603,7 +603,7 @@ namespace BuildingSubID }; } -enum class EMarketMode : int32_t +enum class EMarketMode : int8_t { RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, @@ -701,9 +701,7 @@ namespace MappedKeys }; } -namespace EAiTactic -{ -enum EAiTactic +enum class EAiTactic : int8_t { NONE = -1, RANDOM, @@ -711,29 +709,22 @@ enum EAiTactic BUILDER, EXPLORER }; -} -namespace EBuildingState +enum class EBuildingState : int8_t { - enum EBuildingState - { - HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, - NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED - }; -} + HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, + NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED +}; -namespace ESpellCastProblem +enum class ESpellCastProblem : int8_t { - enum ESpellCastProblem - { - OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, - HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, - SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, - NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, - MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all - INVALID - }; -} + OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, + HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, + SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, + NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, + MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all + INVALID +}; namespace ECommander { @@ -757,7 +748,7 @@ enum class EWallState : int8_t REINFORCED, // walls in towns with castle }; -enum class EGateState : uint8_t +enum class EGateState : int8_t { NONE, CLOSED, @@ -766,32 +757,16 @@ enum class EGateState : uint8_t DESTROYED }; -namespace ESiegeHex -{ - enum ESiegeHex : si16 - { - DESTRUCTIBLE_WALL_1 = 29, - DESTRUCTIBLE_WALL_2 = 78, - DESTRUCTIBLE_WALL_3 = 130, - DESTRUCTIBLE_WALL_4 = 182, - GATE_BRIDGE = 94, - GATE_OUTER = 95, - GATE_INNER = 96 - }; -} -namespace ETileType +enum class ETileType : int8_t { - enum ETileType - { - FREE, - POSSIBLE, - BLOCKED, - USED - }; -} + FREE, + POSSIBLE, + BLOCKED, + USED +}; -enum class ETeleportChannelType +enum class ETeleportChannelType : int8_t { IMPASSABLE, BIDIRECTIONAL, @@ -1006,17 +981,14 @@ namespace SecSkillLevel }; } -namespace Date +enum class Date : int8_t { - enum EDateType - { - DAY = 0, - DAY_OF_WEEK = 1, - WEEK = 2, - MONTH = 3, - DAY_OF_MONTH - }; -} + DAY = 0, + DAY_OF_WEEK = 1, + WEEK = 2, + MONTH = 3, + DAY_OF_MONTH +}; enum class EActionType : int8_t { @@ -1041,7 +1013,7 @@ enum class EActionType : int8_t DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EActionType actionType); -enum class EDiggingStatus : int32_t +enum class EDiggingStatus : int8_t { UNKNOWN = -1, CAN_DIG = 0, @@ -1348,7 +1320,7 @@ public: using ESpellSchool = SpellSchool; -enum class EMetaclass: ui8 +enum class EMetaclass: int8_t { INVALID = 0, ARTIFACT, diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 060962780..731252284 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -47,6 +47,14 @@ struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class f static constexpr si16 HEX_BEFORE_ALL = std::numeric_limits::min(); static constexpr si16 HEX_AFTER_ALL = std::numeric_limits::max(); + static constexpr si16 DESTRUCTIBLE_WALL_1 = 29; + static constexpr si16 DESTRUCTIBLE_WALL_2 = 78; + static constexpr si16 DESTRUCTIBLE_WALL_3 = 130; + static constexpr si16 DESTRUCTIBLE_WALL_4 = 182; + static constexpr si16 GATE_BRIDGE = 94; + static constexpr si16 GATE_OUTER = 95; + static constexpr si16 GATE_INNER = 96; + si16 hex; static constexpr si16 INVALID = -1; enum EDir diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 23c20abe3..505c69297 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -93,7 +93,7 @@ static BattleHex WallPartToHex(EWallPart part) using namespace SiegeStuffThatShouldBeMovedToHandlers; -ESpellCastProblem::ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const +ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const { RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); if(caster == nullptr) @@ -187,7 +187,7 @@ bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, auto obstacles = battleGetAllObstaclesOnPos(hex, false); - if(hex != ESiegeHex::GATE_BRIDGE || (battleIsGatePassable())) + if(hex != BattleHex::GATE_BRIDGE || (battleIsGatePassable())) for(const auto & obst : obstacles) if(obst->obstacleType == CObstacleInstance::MOAT) pathHasMoat |= true; @@ -821,7 +821,7 @@ std::vector> CBattleInfoCallback::getAl affectedObstacles.push_back(i); } for(auto hex : unit->getHexes()) - if(hex == ESiegeHex::GATE_BRIDGE && battleIsGatePassable()) + if(hex == BattleHex::GATE_BRIDGE && battleIsGatePassable()) for(int i=0; iobstacleType == CObstacleInstance::MOAT) affectedObstacles.erase(affectedObstacles.begin()+i); @@ -926,7 +926,7 @@ AccessibilityInfo CBattleInfoCallback::getAccesibility() const accessability = EAccessibility::UNAVAILABLE; break; } - ret[ESiegeHex::GATE_OUTER] = ret[ESiegeHex::GATE_INNER] = accessability; + ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessability; } //tiles occupied by standing stacks @@ -955,10 +955,10 @@ AccessibilityInfo CBattleInfoCallback::getAccesibility() const static const std::pair lockedIfNotDestroyed[] = { //which part of wall, which hex is blocked if this part of wall is not destroyed - std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_4)), - std::make_pair(EWallPart::BELOW_GATE, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_3)), - std::make_pair(EWallPart::OVER_GATE, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_2)), - std::make_pair(EWallPart::UPPER_WALL, BattleHex(ESiegeHex::DESTRUCTIBLE_WALL_1)) + std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_4)), + std::make_pair(EWallPart::BELOW_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_3)), + std::make_pair(EWallPart::OVER_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_2)), + std::make_pair(EWallPart::UPPER_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_1)) }; for(const auto & elem : lockedIfNotDestroyed) @@ -1055,7 +1055,7 @@ bool CBattleInfoCallback::isInObstacle( if(vstd::contains(obstacles, occupiedHex)) { - if(occupiedHex == ESiegeHex::GATE_BRIDGE) + if(occupiedHex == BattleHex::GATE_BRIDGE) { if(battleGetGateState() != EGateState::DESTROYED && params.side == BattleSide::ATTACKER) return true; @@ -1080,7 +1080,7 @@ std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePe for(const auto & hex : oi->getStoppingTile()) { - if(hex == ESiegeHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) + if(hex == BattleHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) { if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) continue; // this tile is disabled by drawbridge on top of it diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index 00decc929..ae76c04d2 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -118,7 +118,7 @@ public: si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell - ESpellCastProblem::ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell + ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 2e6e15c2e..05d9a6bb0 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -356,7 +356,7 @@ void CGameState::randomizeObject(CGObjectInstance *cur) } } -int CGameState::getDate(Date::EDateType mode) const +int CGameState::getDate(Date mode) const { int temp; switch (mode) diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index d151e91d3..40c3af61e 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -133,7 +133,7 @@ public: bool isVisible(int3 pos, const std::optional & player) const override; bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; - int getDate(Date::EDateType mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month // ----- getters, setters ----- diff --git a/lib/gameState/SThievesGuildInfo.h b/lib/gameState/SThievesGuildInfo.h index f3423d05f..b712d543e 100644 --- a/lib/gameState/SThievesGuildInfo.h +++ b/lib/gameState/SThievesGuildInfo.h @@ -22,7 +22,7 @@ struct DLL_LINKAGE SThievesGuildInfo std::map colorToBestHero; //maps player's color to his best heros' - std::map personality; // color to personality // ai tactic + std::map personality; // color to personality // ai tactic std::map bestCreature; // color to ID // id or -1 if not known // template void serialize(Handler &h, const int version) diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index e5cf5d5fa..8b04c1f2e 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -52,7 +52,7 @@ struct DLL_LINKAGE PlayerInfo bool canHumanPlay; bool canComputerPlay; - EAiTactic::EAiTactic aiTactic; /// The default value is EAiTactic::RANDOM. + EAiTactic aiTactic; /// The default value is EAiTactic::RANDOM. std::set allowedFactions; bool isFactionRandom; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 2c03c42c6..57fb49a9e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -215,7 +215,7 @@ void CMapLoaderH3M::readPlayerInfo() continue; } - playerInfo.aiTactic = static_cast(reader->readUInt8()); + playerInfo.aiTactic = static_cast(reader->readUInt8()); if(features.levelSOD) reader->skipUnused(1); //TODO: check meaning? diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index c1a908999..85c0c8590 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -269,7 +269,7 @@ bool RmgMap::isRoad(const int3& tile) const return tiles[tile.x][tile.y][tile.z].isRoad(); } -void RmgMap::setOccupied(const int3 &tile, ETileType::ETileType state) +void RmgMap::setOccupied(const int3 &tile, ETileType state) { assertOnMap(tile); diff --git a/lib/rmg/RmgMap.h b/lib/rmg/RmgMap.h index 8ceac0657..11213d031 100644 --- a/lib/rmg/RmgMap.h +++ b/lib/rmg/RmgMap.h @@ -54,7 +54,7 @@ public: int height() const; PlayerInfo & getPlayer(int playerId); - void setOccupied(const int3 &tile, ETileType::ETileType state); + void setOccupied(const int3 &tile, ETileType state); void setRoad(const int3 &tile, RoadId roadType); TileInfo getTileInfo(const int3 & tile) const; diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 1991b81a6..31e287e06 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -356,7 +356,7 @@ void Object::Instance::finalize(RmgMap & map) for(const auto & tile : getBlockedArea().getTilesVector()) { - map.setOccupied(tile, ETileType::ETileType::USED); + map.setOccupied(tile, ETileType::USED); } map.getMapProxy()->insertObject(&dObject); diff --git a/lib/rmg/TileInfo.cpp b/lib/rmg/TileInfo.cpp index 405a430b5..e1701a3b7 100644 --- a/lib/rmg/TileInfo.cpp +++ b/lib/rmg/TileInfo.cpp @@ -54,12 +54,12 @@ bool TileInfo::isUsed() const { return occupied == ETileType::USED; } -void TileInfo::setOccupied(ETileType::ETileType value) +void TileInfo::setOccupied(ETileType value) { occupied = value; } -ETileType::ETileType TileInfo::getTileType() const +ETileType TileInfo::getTileType() const { return occupied; } diff --git a/lib/rmg/TileInfo.h b/lib/rmg/TileInfo.h index 61e874a74..6df6fcc60 100644 --- a/lib/rmg/TileInfo.h +++ b/lib/rmg/TileInfo.h @@ -28,15 +28,15 @@ public: bool isFree() const; bool isUsed() const; bool isRoad() const; - void setOccupied(ETileType::ETileType value); + void setOccupied(ETileType value); TerrainId getTerrainType() const; - ETileType::ETileType getTileType() const; + ETileType getTileType() const; void setTerrainType(TerrainId value); void setRoadType(RoadId type); private: float nearestObjectDistance; - ETileType::ETileType occupied; + ETileType occupied; TerrainId terrain; RoadId roadType; }; diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index b7621c14f..a7d22fcb0 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -468,7 +468,7 @@ bool BaseMechanics::adaptGenericProblem(Problem & target) const return false; } -bool BaseMechanics::adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const +bool BaseMechanics::adaptProblem(ESpellCastProblem source, Problem & target) const { if(source == ESpellCastProblem::OK) return true; diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index efbbc53b8..7d6e395ca 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -180,7 +180,7 @@ class DLL_LINKAGE Mechanics public: virtual ~Mechanics(); - virtual bool adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const = 0; + virtual bool adaptProblem(ESpellCastProblem source, Problem & target) const = 0; virtual bool adaptGenericProblem(Problem & target) const = 0; virtual std::vector rangeInHexes(BattleHex centralHex) const = 0; @@ -262,7 +262,7 @@ class DLL_LINKAGE BaseMechanics : public Mechanics public: virtual ~BaseMechanics(); - bool adaptProblem(ESpellCastProblem::ESpellCastProblem source, Problem & target) const override; + bool adaptProblem(ESpellCastProblem source, Problem & target) const override; bool adaptGenericProblem(Problem & target) const override; int32_t getSpellIndex() const override; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 42581078f..c08d4ce38 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -617,18 +617,18 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) creSpeed = GameConstants::BFIELD_SIZE; - bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) { return obst->obstacleType == CObstacleInstance::MOAT; }); auto isGateDrawbridgeHex = [&](BattleHex hex) -> bool { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + if (hasWideMoat && hex == BattleHex::GATE_BRIDGE) return true; - if (hex == ESiegeHex::GATE_OUTER) + if (hex == BattleHex::GATE_OUTER) return true; - if (hex == ESiegeHex::GATE_INNER) + if (hex == BattleHex::GATE_INNER) return true; return false; @@ -687,11 +687,11 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) { auto needOpenGates = [&](BattleHex hex) -> bool { - if (hasWideMoat && hex == ESiegeHex::GATE_BRIDGE) + if (hasWideMoat && hex == BattleHex::GATE_BRIDGE) return true; - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == ESiegeHex::GATE_OUTER) + if (hex == BattleHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] == BattleHex::GATE_OUTER) return true; - else if (hex == ESiegeHex::GATE_OUTER || hex == ESiegeHex::GATE_INNER) + else if (hex == BattleHex::GATE_OUTER || hex == BattleHex::GATE_INNER) return true; return false; @@ -719,24 +719,24 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) if (!gateMayCloseAtHex.isValid() && dbState != EGateState::CLOSED) { - if (hex == ESiegeHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + if (hex == BattleHex::GATE_INNER && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_OUTER) { gateMayCloseAtHex = path.first[i-1]; } if (hasWideMoat) { - if (hex == ESiegeHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_OUTER) + if (hex == BattleHex::GATE_BRIDGE && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_OUTER) { gateMayCloseAtHex = path.first[i-1]; } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && - path.first[i-1] != ESiegeHex::GATE_INNER && - path.first[i-1] != ESiegeHex::GATE_BRIDGE) + else if (hex == BattleHex::GATE_OUTER && i-1 >= 0 && + path.first[i-1] != BattleHex::GATE_INNER && + path.first[i-1] != BattleHex::GATE_BRIDGE) { gateMayCloseAtHex = path.first[i-1]; } } - else if (hex == ESiegeHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != ESiegeHex::GATE_INNER) + else if (hex == BattleHex::GATE_OUTER && i-1 >= 0 && path.first[i-1] != BattleHex::GATE_INNER) { gateMayCloseAtHex = path.first[i-1]; } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 445fa5007..0ed52c6ef 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -177,11 +177,11 @@ void BattleProcessor::updateGateState() // - if Force Field is cast here, bridge can't open (but can close, in any town) // - deals moat damage to attacker if bridge is closed (fortress only) - bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), true).empty(); - bool hasStackAtGateInner = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_INNER), false) != nullptr; - bool hasStackAtGateOuter = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_OUTER), false) != nullptr; - bool hasStackAtGateBridge = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(ESiegeHex::GATE_BRIDGE), false) != nullptr; - bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(ESiegeHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty(); + bool hasStackAtGateInner = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr; + bool hasStackAtGateOuter = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr; + bool hasStackAtGateBridge = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr; + bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) { return obst->obstacleType == CObstacleInstance::MOAT; }); diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index fb754691d..53c2b1178 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -16,7 +16,7 @@ class IGameInfoCallbackMock : public IGameInfoCallback { public: //various - MOCK_CONST_METHOD1(getDate, int(Date::EDateType)); + MOCK_CONST_METHOD1(getDate, int(Date)); MOCK_CONST_METHOD2(isAllowed, bool(int32_t, int32_t)); //player diff --git a/test/mock/mock_spells_Mechanics.h b/test/mock/mock_spells_Mechanics.h index 10b4b2a57..985e093f3 100644 --- a/test/mock/mock_spells_Mechanics.h +++ b/test/mock/mock_spells_Mechanics.h @@ -18,7 +18,7 @@ namespace spells class MechanicsMock : public Mechanics { public: - MOCK_CONST_METHOD2(adaptProblem, bool(ESpellCastProblem::ESpellCastProblem, Problem &)); + MOCK_CONST_METHOD2(adaptProblem, bool(ESpellCastProblem, Problem &)); MOCK_CONST_METHOD1(adaptGenericProblem, bool(Problem &)); MOCK_CONST_METHOD1(rangeInHexes, std::vector(BattleHex)); From e54287ea5dfe96e922b949d5696cbc876936c219 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 00:22:31 +0300 Subject: [PATCH 0186/1248] Converted remaining identifier to new system --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Goals/AbstractGoal.cpp | 2 +- AI/Nullkiller/Goals/BuildThis.cpp | 2 +- AI/Nullkiller/Goals/Composition.cpp | 2 +- AI/Nullkiller/Goals/RecruitHero.cpp | 2 +- AI/VCAI/BuildingManager.cpp | 2 +- AI/VCAI/Goals/AbstractGoal.cpp | 2 +- AI/VCAI/Goals/Build.cpp | 2 +- AI/VCAI/Goals/BuildThis.cpp | 2 +- AI/VCAI/Goals/CollectRes.cpp | 2 +- AI/VCAI/Goals/Conquer.cpp | 2 +- AI/VCAI/Goals/Explore.cpp | 2 +- AI/VCAI/Goals/GatherArmy.cpp | 2 +- AI/VCAI/Goals/GatherTroops.cpp | 2 +- AI/VCAI/Goals/RecruitHero.cpp | 2 +- AI/VCAI/Goals/VisitObj.cpp | 2 +- AI/VCAI/Goals/VisitTile.cpp | 2 +- AI/VCAI/Goals/Win.cpp | 2 +- AI/VCAI/VCAI.cpp | 2 +- client/CMusicHandler.cpp | 2 +- client/CPlayerInterface.cpp | 2 +- client/ClientCommandManager.cpp | 2 +- client/adventureMap/AdventureMapWidget.cpp | 2 +- client/lobby/OptionsTab.cpp | 24 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 6 +- client/windows/CCastleInterface.cpp | 10 +- client/windows/CCastleInterface.h | 4 +- cmake_modules/VCMI_lib.cmake | 6 +- include/vcmi/Faction.h | 2 +- lib/ArtifactUtils.cpp | 16 +- lib/ArtifactUtils.h | 4 +- lib/CArtHandler.cpp | 29 +- lib/CArtifactInstance.h | 1 + lib/CBuildingHandler.cpp | 8 +- lib/CCreatureHandler.cpp | 2 +- lib/CHeroHandler.cpp | 2 +- lib/CPlayerState.h | 1 + lib/CSkillHandler.cpp | 2 +- lib/CTownHandler.cpp | 8 +- lib/CTownHandler.h | 2 +- lib/GameConstants.cpp | 5 +- lib/GameConstants.h | 1379 +---------------- lib/JsonNode.cpp | 2 +- lib/JsonRandom.cpp | 6 +- lib/NetPacksLib.cpp | 8 +- lib/ResourceSet.cpp | 2 +- lib/StringConstants.h | 125 -- lib/battle/BattleAction.cpp | 5 +- lib/bonuses/Bonus.cpp | 2 +- lib/bonuses/Limiters.cpp | 2 +- lib/campaign/CampaignHandler.cpp | 2 +- lib/constants/EntityIdentifiers.h | 896 +++++++++++ lib/constants/Enumerations.h | 251 +++ lib/constants/NumericConstants.h | 59 + lib/constants/StringConstants.h | 217 +++ lib/gameState/CGameState.cpp | 6 +- lib/gameState/CGameState.h | 1 + lib/gameState/CGameStateCampaign.cpp | 2 +- .../CObjectClassesHandler.cpp | 2 +- .../CommonConstructors.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 6 +- lib/mapObjects/CGTownInstance.h | 4 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/MiscObjects.cpp | 4 +- lib/mapObjects/MiscObjects.h | 2 +- lib/mapObjects/ObjectTemplate.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 6 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/modding/CModHandler.cpp | 2 +- lib/modding/ContentTypeHandler.cpp | 2 +- lib/pathfinder/PathfinderUtil.h | 2 +- lib/rmg/CMapGenerator.cpp | 2 +- lib/rmg/CRmgTemplate.cpp | 2 +- lib/serializer/CSerializer.h | 2 +- lib/spells/CSpellHandler.cpp | 2 +- mapeditor/inspector/questwidget.cpp | 2 +- mapeditor/inspector/rewardswidget.cpp | 2 +- mapeditor/inspector/townbulidingswidget.cpp | 2 +- mapeditor/mapsettings.cpp | 2 +- mapeditor/playerparams.cpp | 2 +- server/CGameHandler.cpp | 2 +- server/CVCMIServer.cpp | 22 +- server/CVCMIServer.h | 2 +- server/battles/BattleResultProcessor.cpp | 4 +- server/queries/CQuery.h | 1 + test/game/CGameStateTest.cpp | 2 +- 91 files changed, 1590 insertions(+), 1654 deletions(-) delete mode 100644 lib/StringConstants.h create mode 100644 lib/constants/EntityIdentifiers.h create mode 100644 lib/constants/Enumerations.h create mode 100644 lib/constants/NumericConstants.h create mode 100644 lib/constants/StringConstants.h diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 85acbe46c..bdb25982d 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1097,7 +1097,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - if (queryID != -1) + if (queryID != QueryID::NONE) { status.addQuery(queryID, "Combat result dialog"); const int confirmAction = 0; diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 54ce129ab..ae506f4af 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -24,7 +24,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo) for(auto &pair : townInfo->buildings) { - if(pair.second->upgrade != -1) + if(pair.second->upgrade != BuildingID::NONE) { parentMap[pair.second->upgrade] = pair.first; } diff --git a/AI/Nullkiller/Goals/AbstractGoal.cpp b/AI/Nullkiller/Goals/AbstractGoal.cpp index 1cc45a12a..98f6a4c49 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.cpp +++ b/AI/Nullkiller/Goals/AbstractGoal.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "AbstractGoal.h" #include "../AIGateway.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI { diff --git a/AI/Nullkiller/Goals/BuildThis.cpp b/AI/Nullkiller/Goals/BuildThis.cpp index 2f371346e..c73cb57e3 100644 --- a/AI/Nullkiller/Goals/BuildThis.cpp +++ b/AI/Nullkiller/Goals/BuildThis.cpp @@ -11,7 +11,7 @@ #include "BuildThis.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI diff --git a/AI/Nullkiller/Goals/Composition.cpp b/AI/Nullkiller/Goals/Composition.cpp index ca77dc5e8..aff59aa7b 100644 --- a/AI/Nullkiller/Goals/Composition.cpp +++ b/AI/Nullkiller/Goals/Composition.cpp @@ -11,7 +11,7 @@ #include "Composition.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI diff --git a/AI/Nullkiller/Goals/RecruitHero.cpp b/AI/Nullkiller/Goals/RecruitHero.cpp index fe4aaeb6f..c6a6c4d4e 100644 --- a/AI/Nullkiller/Goals/RecruitHero.cpp +++ b/AI/Nullkiller/Goals/RecruitHero.cpp @@ -11,7 +11,7 @@ #include "Goals.h" #include "../AIGateway.h" #include "../AIUtility.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" namespace NKAI diff --git a/AI/VCAI/BuildingManager.cpp b/AI/VCAI/BuildingManager.cpp index 5aeba8c6b..f5529777f 100644 --- a/AI/VCAI/BuildingManager.cpp +++ b/AI/VCAI/BuildingManager.cpp @@ -222,7 +222,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t) std::vector extraBuildings; for (auto buildingInfo : t->town->buildings) { - if (buildingInfo.first > 43) + if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST) extraBuildings.push_back(buildingInfo.first); } return tryBuildAnyStructure(t, extraBuildings); diff --git a/AI/VCAI/Goals/AbstractGoal.cpp b/AI/VCAI/Goals/AbstractGoal.cpp index 45b055d25..3ed71f08d 100644 --- a/AI/VCAI/Goals/AbstractGoal.cpp +++ b/AI/VCAI/Goals/AbstractGoal.cpp @@ -14,7 +14,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/Build.cpp b/AI/VCAI/Goals/Build.cpp index 182c8a4f4..4a57d0a5c 100644 --- a/AI/VCAI/Goals/Build.cpp +++ b/AI/VCAI/Goals/Build.cpp @@ -17,7 +17,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/BuildThis.cpp b/AI/VCAI/Goals/BuildThis.cpp index 703f66784..065db0b01 100644 --- a/AI/VCAI/Goals/BuildThis.cpp +++ b/AI/VCAI/Goals/BuildThis.cpp @@ -16,7 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index d0832c3e3..f5ec1d873 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -16,7 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGMarket.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/Conquer.cpp b/AI/VCAI/Goals/Conquer.cpp index f18b629ed..bf0d368f1 100644 --- a/AI/VCAI/Goals/Conquer.cpp +++ b/AI/VCAI/Goals/Conquer.cpp @@ -15,7 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/Explore.cpp b/AI/VCAI/Goals/Explore.cpp index 13571423f..4c25c74c0 100644 --- a/AI/VCAI/Goals/Explore.cpp +++ b/AI/VCAI/Goals/Explore.cpp @@ -15,7 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" #include "../../../lib/CPlayerState.h" using namespace Goals; diff --git a/AI/VCAI/Goals/GatherArmy.cpp b/AI/VCAI/Goals/GatherArmy.cpp index 2e7ef718f..e6e0557f0 100644 --- a/AI/VCAI/Goals/GatherArmy.cpp +++ b/AI/VCAI/Goals/GatherArmy.cpp @@ -16,7 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 471a997be..4af9e7434 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -16,7 +16,7 @@ #include "../ResourceManager.h" #include "../BuildingManager.h" #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/RecruitHero.cpp b/AI/VCAI/Goals/RecruitHero.cpp index 99c5dd0f2..971260202 100644 --- a/AI/VCAI/Goals/RecruitHero.cpp +++ b/AI/VCAI/Goals/RecruitHero.cpp @@ -15,7 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/VisitObj.cpp b/AI/VCAI/Goals/VisitObj.cpp index be52de94d..a31b37af0 100644 --- a/AI/VCAI/Goals/VisitObj.cpp +++ b/AI/VCAI/Goals/VisitObj.cpp @@ -15,7 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/VisitTile.cpp b/AI/VCAI/Goals/VisitTile.cpp index 369ba7380..4e036fbd4 100644 --- a/AI/VCAI/Goals/VisitTile.cpp +++ b/AI/VCAI/Goals/VisitTile.cpp @@ -15,7 +15,7 @@ #include "../FuzzyHelper.h" #include "../ResourceManager.h" #include "../BuildingManager.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index f2a3b3dce..2ad9abdf8 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -17,7 +17,7 @@ #include "../BuildingManager.h" #include "../../../lib/mapping/CMapHeader.h" //for victory conditions #include "../../../lib/mapObjects/CGTownInstance.h" -#include "../../../lib/StringConstants.h" +#include "../../../lib/constants/StringConstants.h" using namespace Goals; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 68a121f41..e59341a6f 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1594,7 +1594,7 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID) logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); battlename.clear(); - if (queryID != -1) + if (queryID != QueryID::NONE) { status.addQuery(queryID, "Combat result dialog"); const int confirmAction = 0; diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 681a02c36..1ffd16eb9 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -19,7 +19,7 @@ #include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/CRandomGenerator.h" #include "../lib/VCMIDirs.h" #include "../lib/TerrainHandler.h" diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 5d62dbdf0..a49ae7013 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -854,7 +854,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) if(!battleInt) { - bool allowManualReplay = queryID != -1; + bool allowManualReplay = queryID != QueryID::NONE; auto wnd = std::make_shared(*br, *this, allowManualReplay); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index c9cce134e..19ac8dcc9 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -22,7 +22,7 @@ #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" #include "../lib/CPlayerState.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/campaign/CampaignHandler.h" #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 65e1230be..7ee69da42 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -29,7 +29,7 @@ #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" -#include "../../lib/StringConstants.h" +#include "../../lib/constants/StringConstants.h" #include "../../lib/filesystem/ResourceID.h" AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 1cb24adcb..bbbd25ca5 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -117,9 +117,9 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) return HERO_RANDOM; default: { - if(settings.heroPortrait >= 0) + if(settings.heroPortrait != HeroTypeID::NONE) return settings.heroPortrait; - auto index = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + auto index = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); return (*CGI->heroh)[index]->imageIndex; } } @@ -208,7 +208,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() { if(!settings.heroName.empty()) return settings.heroName; - auto index = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + auto index = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); return (*CGI->heroh)[index]->getNameTranslated(); } } @@ -235,7 +235,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() case OptionsTab::TOWN: return (settings.castle.getNum() < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; case OptionsTab::HERO: - return (settings.hero < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; + return (settings.hero.getNum() < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; case OptionsTab::BONUS: { switch(settings.bonus) @@ -256,7 +256,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); - auto heroIndex = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + auto heroIndex = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); switch(type) { @@ -264,7 +264,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() return getName(); case HERO: { - if(settings.hero >= 0) + if(settings.hero.getNum() >= 0) return getName() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated(); return getName(); } @@ -403,7 +403,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = settings.hero >= CGI->heroh->size() ? 0 : settings.hero; + auto heroIndex = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); imageSpeciality = std::make_shared("UN44", (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); @@ -438,7 +438,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) allowedHeroes.insert(HeroTypeID(i)); allowedBonus.push_back(-1); // random - if(initialHero >= -1) + if(initialHero.getNum() >= -1) allowedBonus.push_back(0); // artifact allowedBonus.push_back(1); // gold if(initialFaction.getNum() >= 0) @@ -600,8 +600,8 @@ void OptionsTab::SelectionWindow::genContentHeroes() set.hero = PlayerSettings::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); - drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedHero == PlayerSettings::RANDOM) + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedHero.getNum() == PlayerSettings::RANDOM) components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedHeroes) @@ -701,7 +701,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { set.hero = PlayerSettings::RANDOM; } - if(set.hero != PlayerSettings::NONE) + if(set.hero.getNum() != PlayerSettings::NONE) { if(!doApply) { @@ -786,7 +786,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) // cases when we do not need to display a message if(settings.castle.getNum() == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) return; - if(settings.hero == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + if(settings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; GH.windows().createAndPushWindow(*this); diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 18a704a19..feb3c974a 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -77,7 +77,7 @@ void CArtifactsOfHeroBackpack::scrollBackpack(int offset) if(backpackListBox) backpackListBox->resize(getActiveSlotLinesNum()); backpackPos += offset; - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + backpackPos); + auto slot = ArtifactPosition::BACKPACK_START + backpackPos; for(auto artPlace : backpack) { setSlotData(artPlace, slot, *curHero); diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 799135837..f24d836ae 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -63,7 +63,7 @@ void CArtifactsOfHeroBase::init( // CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION_CAPTURING is removed OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); pos += position; - for(int g = 0; g < GameConstants::BACKPACK_START; g++) + for(int g = 0; g < ArtifactPosition::BACKPACK_START; g++) { artWorn[ArtifactPosition(g)] = std::make_shared(slotPos[g]); } @@ -143,7 +143,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe }; slotInc inc_ring = [artsInBackpack](ArtifactPosition & slot) -> ArtifactPosition { - return ArtifactPosition(GameConstants::BACKPACK_START + (slot - GameConstants::BACKPACK_START + 1) % artsInBackpack); + return ArtifactPosition::BACKPACK_START + (slot - ArtifactPosition::BACKPACK_START + 1) % artsInBackpack; }; slotInc inc; if(scrollingPossible) @@ -158,7 +158,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe if(artsInBackpack) backpackPos %= artsInBackpack; - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + backpackPos); + auto slot = ArtifactPosition(ArtifactPosition::BACKPACK_START + backpackPos); for(auto artPlace : backpack) { setSlotData(artPlace, slot, artSet); diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index d7bd05405..9364ad703 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -651,7 +651,7 @@ const CGHeroInstance * CCastleBuildings::getHero() return town->garrisonHero; } -void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades) +void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { logGlobal->trace("You've clicked on %d", (int)building.toEnum()); const CBuilding *b = town->town->buildings.find(building)->second; @@ -876,7 +876,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); } -void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades) +void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); @@ -1613,7 +1613,9 @@ CFortScreen::CFortScreen(const CGTownInstance * town): BuildingID buildingID; if(fortSize == GameConstants::CREATURES_PER_TOWN) { - if(vstd::contains(town->builtBuildings, BuildingID::DWELL_UP_FIRST+i)) + BuildingID dwelling = BuildingID::DWELL_UP_FIRST+i; + + if(vstd::contains(town->builtBuildings, dwelling)) buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); else buildingID = BuildingID(BuildingID::DWELL_FIRST+i); @@ -1718,7 +1720,7 @@ const CCreature * CFortScreen::RecruitArea::getMyCreature() const CBuilding * CFortScreen::RecruitArea::getMyBuilding() { - BuildingID myID = BuildingID(BuildingID::DWELL_FIRST).advance(level); + BuildingID myID = BuildingID(BuildingID::DWELL_FIRST + level); if (level == GameConstants::CREATURES_PER_TOWN) return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 8aa56bbf1..3e05f3079 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -152,7 +152,7 @@ class CCastleBuildings : public CIntObject void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages void enterCastleGate(); - void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID::EBuildingID upgrades);//Rampart's fountains + void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains void enterMagesGuild(); void enterTownHall(); @@ -169,7 +169,7 @@ public: void enterDwelling(int level); void enterToTheQuickRecruitmentWindow(); - void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID::EBuildingID upgrades = BuildingID::NONE); + void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); void addBuilding(BuildingID building); void removeBuilding(BuildingID building);//FIXME: not tested!!! }; diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 0a2a40f39..04c94015b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -369,6 +369,11 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignScenarioPrologEpilog.h ${MAIN_LIB_DIR}/campaign/CampaignState.h + ${MAIN_LIB_DIR}/constants/EntityIdentifiers.h + ${MAIN_LIB_DIR}/constants/Enumerations.h + ${MAIN_LIB_DIR}/constants/NumericConstants.h + ${MAIN_LIB_DIR}/constants/StringConstants.h + ${MAIN_LIB_DIR}/events/ApplyDamage.h ${MAIN_LIB_DIR}/events/GameResumed.h ${MAIN_LIB_DIR}/events/ObjectVisitEnded.h @@ -621,7 +626,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/ScriptHandler.h ${MAIN_LIB_DIR}/ScopeGuard.h ${MAIN_LIB_DIR}/StartInfo.h - ${MAIN_LIB_DIR}/StringConstants.h ${MAIN_LIB_DIR}/TerrainHandler.h ${MAIN_LIB_DIR}/TextOperations.h ${MAIN_LIB_DIR}/TurnTimerInfo.h diff --git a/include/vcmi/Faction.h b/include/vcmi/Faction.h index 2bc959567..6a1afe58f 100644 --- a/include/vcmi/Faction.h +++ b/include/vcmi/Faction.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class FactionID; -enum class EAlignment : uint8_t; +enum class EAlignment : int8_t; class BoatId; class DLL_LINKAGE Faction : public EntityT, public INativeTerrainProvider diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index a3532bee3..eeb94c2c6 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -33,16 +33,16 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid) { const auto * art = aid.toArtifact(); - if(art->canBePutAt(target, GameConstants::BACKPACK_START)) + if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) { - return GameConstants::BACKPACK_START; + return ArtifactPosition::BACKPACK_START; } return ArtifactPosition::PRE_FIRST; } -DLL_LINKAGE const std::vector & ArtifactUtils::unmovableSlots() +DLL_LINKAGE const std::vector & ArtifactUtils::unmovableSlots() { - static const std::vector positions = + static const std::vector positions = { ArtifactPosition::SPELLBOOK, ArtifactPosition::MACH4 @@ -51,9 +51,9 @@ DLL_LINKAGE const std::vector & ArtifactUti return positions; } -DLL_LINKAGE const std::vector & ArtifactUtils::constituentWornSlots() +DLL_LINKAGE const std::vector & ArtifactUtils::constituentWornSlots() { - static const std::vector positions = + static const std::vector positions = { ArtifactPosition::HEAD, ArtifactPosition::SHOULDERS, @@ -98,12 +98,12 @@ DLL_LINKAGE bool ArtifactUtils::checkSpellbookIsNeeded(const CGHeroInstance * he DLL_LINKAGE bool ArtifactUtils::isSlotBackpack(const ArtifactPosition & slot) { - return slot >= GameConstants::BACKPACK_START; + return slot >= ArtifactPosition::BACKPACK_START; } DLL_LINKAGE bool ArtifactUtils::isSlotEquipment(const ArtifactPosition & slot) { - return slot < GameConstants::BACKPACK_START && slot >= 0; + return slot < ArtifactPosition::BACKPACK_START && slot >= ArtifactPosition(0); } DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots) diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index 39532f38e..2e70999b6 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -29,8 +29,8 @@ namespace ArtifactUtils DLL_LINKAGE ArtifactPosition getArtAnyPosition(const CArtifactSet * target, const ArtifactID & aid); DLL_LINKAGE ArtifactPosition getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid); // TODO: Make this constexpr when the toolset is upgraded - DLL_LINKAGE const std::vector & unmovableSlots(); - DLL_LINKAGE const std::vector & constituentWornSlots(); + DLL_LINKAGE const std::vector & unmovableSlots(); + DLL_LINKAGE const std::vector & constituentWornSlots(); DLL_LINKAGE bool isArtRemovable(const std::pair & slot); DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, const ArtifactID & artID, const ArtifactPosition & slot); DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index abbaf32eb..9b8664301 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -15,7 +15,7 @@ #include "CGeneralTextHandler.h" #include "GameSettings.h" #include "mapObjects/MapObjects.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" @@ -239,11 +239,11 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b if(artCanBePutAt(artSet, slot, assumeDestRemoved)) return true; } - return artCanBePutAt(artSet, GameConstants::BACKPACK_START, assumeDestRemoved); + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); } else if(ArtifactUtils::isSlotBackpack(slot)) { - return artCanBePutAt(artSet, GameConstants::BACKPACK_START, assumeDestRemoved); + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); } else { @@ -483,17 +483,16 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode return art; } -ArtifactPosition::ArtifactPosition(std::string slotName): - num(ArtifactPosition::PRE_FIRST) +int32_t ArtifactPositionBase::decode(const std::string & slotName) { #define ART_POS(x) { #x, ArtifactPosition::x }, static const std::map artifactPositionMap = { ART_POS_LIST }; #undef ART_POS auto it = artifactPositionMap.find (slotName); if (it != artifactPositionMap.end()) - num = it->second; + return it->second; else - logMod->warn("Warning! Artifact slot %s not recognized!", slotName); + return PRE_FIRST; } void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const @@ -518,7 +517,7 @@ void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const } else { - auto slot = ArtifactPosition(slotID); + auto slot = ArtifactPosition::decode(slotID); if (slot != ArtifactPosition::PRE_FIRST) art->possibleSlots[ArtBearer::HERO].push_back(slot); } @@ -848,7 +847,7 @@ std::vector CArtifactSet::getBackpackArtPositions(const Artifa { std::vector result; - si32 backpackPosition = GameConstants::BACKPACK_START; + si32 backpackPosition = ArtifactPosition::BACKPACK_START; for(const auto & artInfo : artifactsInBackpack) { const auto * art = artInfo.getArt(); @@ -867,7 +866,7 @@ ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance *art) const for(int i = 0; i < artifactsInBackpack.size(); i++) if(artifactsInBackpack[i].artifact == art) - return ArtifactPosition(GameConstants::BACKPACK_START + i); + return ArtifactPosition::BACKPACK_START + i; return ArtifactPosition::PRE_FIRST; } @@ -893,7 +892,7 @@ const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * if(getArt(slot) == artInst) return slot; - auto backpackSlot = GameConstants::BACKPACK_START; + ArtifactPosition backpackSlot = ArtifactPosition::BACKPACK_START; for(auto & slotInfo : artifactsInBackpack) { if(slotInfo.getArt() == artInst) @@ -1014,7 +1013,7 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const return &artifactsWorn.at(pos); if(pos >= ArtifactPosition::AFTER_LAST ) { - int backpackPos = (int)pos - GameConstants::BACKPACK_START; + int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START; if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) return nullptr; else @@ -1049,7 +1048,7 @@ void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, CArtifactInstanc } else { - auto position = artifactsInBackpack.begin() + slot - GameConstants::BACKPACK_START; + auto position = artifactsInBackpack.begin() + slot - ArtifactPosition::BACKPACK_START; slotInfo = &(*artifactsInBackpack.emplace(position, ArtSlotInfo())); } slotInfo->artifact = art; @@ -1065,7 +1064,7 @@ void CArtifactSet::eraseArtSlot(const ArtifactPosition & slot) } else if(ArtifactUtils::isSlotBackpack(slot)) { - auto backpackSlot = ArtifactPosition(slot - GameConstants::BACKPACK_START); + auto backpackSlot = ArtifactPosition(slot - ArtifactPosition::BACKPACK_START); assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); @@ -1136,7 +1135,7 @@ void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) for(const ArtifactID & artifactID : backpackTemp) { auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); - auto slot = ArtifactPosition(GameConstants::BACKPACK_START + (si32)artifactsInBackpack.size()); + auto slot = ArtifactPosition::BACKPACK_START + (si32)artifactsInBackpack.size(); if(artifact->artType->canBePutAt(this, slot)) putArtifact(slot, artifact); } diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 079db214f..66e405328 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -11,6 +11,7 @@ #include "bonuses/CBonusSystemNode.h" #include "GameConstants.h" +#include "ConstTransitivePtr.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/CBuildingHandler.cpp b/lib/CBuildingHandler.cpp index d5539ebac..a4673e5ae 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/CBuildingHandler.cpp @@ -53,7 +53,9 @@ BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set 1) { - if(vstd::contains(builtBuildings, 37 + hordeLvlsPerTType[townType][1])) //if upgraded dwelling is built + BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]); + + if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built return BuildingID::HORDE_2_UPGR; else //upgraded dwelling not presents return BuildingID::HORDE_2; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 205aa1ccf..8a49bb8b6 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -16,7 +16,7 @@ #include "VCMI_Lib.h" #include "CTownHandler.h" #include "GameSettings.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" #include "serializer/JsonDeserializer.h" diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index c60023ad4..4b9923b36 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -14,7 +14,7 @@ #include "filesystem/Filesystem.h" #include "VCMI_Lib.h" #include "JsonNode.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "battle/BattleHex.h" #include "CCreatureHandler.h" #include "GameSettings.h" diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 1b304eebc..ac8c1d4c6 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -16,6 +16,7 @@ #include "bonuses/CBonusSystemNode.h" #include "ResourceSet.h" #include "TurnTimerInfo.h" +#include "ConstTransitivePtr.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 96a017208..dcd5d6725 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -22,7 +22,7 @@ #include "JsonNode.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 04a928ffb..ac01e787a 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -13,7 +13,7 @@ #include "VCMI_Lib.h" #include "CGeneralTextHandler.h" #include "JsonNode.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "CCreatureHandler.h" #include "CHeroHandler.h" #include "CArtHandler.h" @@ -82,7 +82,7 @@ std::string CBuilding::getDescriptionTextID() const BuildingID CBuilding::getBase() const { const CBuilding * build = this; - while (build->upgrade >= 0) + while (build->upgrade != BuildingID::NONE) { build = build->town->buildings.at(build->upgrade); } @@ -94,7 +94,7 @@ si32 CBuilding::getDistance(const BuildingID & buildID) const { const CBuilding * build = town->buildings.at(buildID); int distance = 0; - while (build->upgrade >= 0 && build != this) + while (build->upgrade != BuildingID::NONE && build != this) { build = build->town->buildings.at(build->upgrade); distance++; @@ -265,7 +265,7 @@ const CBuilding * CTown::getSpecialBuilding(BuildingSubID::EBuildingSubID subID) return nullptr; } -BuildingID::EBuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const +BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const { const auto * building = getSpecialBuilding(subID); return building == nullptr ? BuildingID::NONE : building->bid.num; diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 0b4b40a08..0c15ef396 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -266,7 +266,7 @@ public: const CBuilding * getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const; std::string getGreeting(BuildingSubID::EBuildingSubID subID) const; void setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const; //may affect only mutable field - BuildingID::EBuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; + BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; std::string getRandomNameTranslated(size_t index) const; std::string getRandomNameTextID(size_t index) const; diff --git a/lib/GameConstants.cpp b/lib/GameConstants.cpp index f9095bc46..1b528ec7e 100644 --- a/lib/GameConstants.cpp +++ b/lib/GameConstants.cpp @@ -32,7 +32,7 @@ #include "CCreatureHandler.h"//todo: remove #include "spells/CSpellHandler.h" //todo: remove #include "CSkillHandler.h"//todo: remove -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "CGeneralTextHandler.h" #include "TerrainHandler.h" //TODO: remove #include "BattleFieldHandler.h" @@ -40,6 +40,7 @@ VCMI_LIB_NAMESPACE_BEGIN +const QueryID QueryID::NONE = QueryID(-1); const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1); const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1); @@ -257,7 +258,7 @@ std::ostream & operator<<(std::ostream & os, const EActionType actionType) std::ostream & operator<<(std::ostream & os, const EPathfindingLayer & pathfindingLayer) { - static const std::map pathfinderLayerToString + static const std::map pathfinderLayerToString { #define DEFINE_ELEMENT(element) {EPathfindingLayer::element, #element} DEFINE_ELEMENT(WRONG), diff --git a/lib/GameConstants.h b/lib/GameConstants.h index 5c8fc936f..cb88895ac 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -9,1387 +9,14 @@ */ #pragma once -#include "ConstTransitivePtr.h" +#include "constants/NumericConstants.h" +#include "constants/Enumerations.h" +#include "constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN -class Artifact; -class ArtifactService; -class Creature; -class CreatureService; - -namespace spells -{ - class Spell; - class Service; -} - -class CArtifact; -class CArtifactInstance; -class CCreature; -class CHero; -class CSpell; -class CSkill; -class CGameInfoCallback; -class CNonConstInfoCallback; - - -struct IdTag -{}; - -namespace GameConstants -{ - DLL_LINKAGE extern const std::string VCMI_VERSION; - - constexpr int PUZZLE_MAP_PIECES = 48; - - constexpr int MAX_HEROES_PER_PLAYER = 8; - constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; - - constexpr int ALL_PLAYERS = 255; //bitfield - - constexpr int CREATURES_PER_TOWN = 7; //without upgrades - constexpr int SPELL_LEVELS = 5; - constexpr int SPELL_SCHOOL_LEVELS = 4; - constexpr int DEFAULT_SCHOOLS = 4; - constexpr int CRE_LEVELS = 10; // number of creature experience levels - - constexpr int HERO_GOLD_COST = 2500; - constexpr int SPELLBOOK_GOLD_COST = 500; - constexpr int SKILL_GOLD_COST = 2000; - constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty - constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit - constexpr int ARMY_SIZE = 7; - constexpr int SKILL_PER_HERO = 8; - constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order - - constexpr int SKILL_QUANTITY=28; - constexpr int PRIMARY_SKILLS=4; - constexpr int RESOURCE_QUANTITY=8; - constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type - - // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase - constexpr int F_NUMBER = 9; - constexpr int ARTIFACTS_QUANTITY=171; - constexpr int HEROES_QUANTITY=156; - constexpr int SPELLS_QUANTITY=70; - constexpr int CREATURES_COUNT = 197; - - constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement - - constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits - - constexpr std::array POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; -} - -#define ID_LIKE_CLASS_COMMON(CLASS_NAME, ENUM_NAME) \ -constexpr CLASS_NAME(const CLASS_NAME & other) = default; \ -constexpr CLASS_NAME & operator=(const CLASS_NAME & other) = default; \ -explicit constexpr CLASS_NAME(si32 id) \ - : num(static_cast(id)) \ -{} \ -constexpr operator ENUM_NAME() const \ -{ \ - return num; \ -} \ -constexpr si32 getNum() const \ -{ \ - return static_cast(num); \ -} \ -constexpr ENUM_NAME toEnum() const \ -{ \ - return num; \ -} \ -template void serialize(Handler &h, const int version) \ -{ \ - h & num; \ -} \ -constexpr CLASS_NAME & advance(int i) \ -{ \ - num = static_cast(static_cast(num) + i); \ - return *this; \ -} - - -// Operators are performance-critical and to be inlined they must be in header -#define ID_LIKE_OPERATORS_INTERNAL(A, B, AN, BN) \ -STRONG_INLINE constexpr bool operator==(const A & a, const B & b) \ -{ \ - return AN == BN ; \ -} \ -STRONG_INLINE constexpr bool operator!=(const A & a, const B & b) \ -{ \ - return AN != BN ; \ -} \ -STRONG_INLINE constexpr bool operator<(const A & a, const B & b) \ -{ \ - return AN < BN ; \ -} \ -STRONG_INLINE constexpr bool operator<=(const A & a, const B & b) \ -{ \ - return AN <= BN ; \ -} \ -STRONG_INLINE constexpr bool operator>(const A & a, const B & b) \ -{ \ - return AN > BN ; \ -} \ -STRONG_INLINE constexpr bool operator>=(const A & a, const B & b) \ -{ \ - return AN >= BN ; \ -} - -#define ID_LIKE_OPERATORS(CLASS_NAME, ENUM_NAME) \ - ID_LIKE_OPERATORS_INTERNAL(CLASS_NAME, CLASS_NAME, a.num, b.num) \ - ID_LIKE_OPERATORS_INTERNAL(CLASS_NAME, ENUM_NAME, a.num, b) \ - ID_LIKE_OPERATORS_INTERNAL(ENUM_NAME, CLASS_NAME, a, b.num) - - -#define INSTID_LIKE_CLASS_COMMON(CLASS_NAME, NUMERIC_NAME) \ -public: \ -constexpr CLASS_NAME(const CLASS_NAME & other): \ - BaseForID(other) \ -{ \ -} \ -constexpr CLASS_NAME & operator=(const CLASS_NAME & other) = default; \ -constexpr CLASS_NAME & operator=(NUMERIC_NAME other) { num = other; return *this; }; \ -explicit constexpr CLASS_NAME(si32 id = -1) \ - : BaseForID(id) \ -{} - -template < typename Derived, typename NumericType> -class BaseForID : public IdTag -{ -protected: - NumericType num; - -public: - constexpr NumericType getNum() const - { - return num; - } - - //to make it more similar to IDLIKE - constexpr NumericType toEnum() const - { - return num; - } - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr explicit BaseForID(NumericType _num = -1) : - num(_num) - { - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr bool operator == (const BaseForID & b) const { return num == b.num; } - constexpr bool operator <= (const BaseForID & b) const { return num <= b.num; } - constexpr bool operator >= (const BaseForID & b) const { return num >= b.num; } - constexpr bool operator != (const BaseForID & b) const { return num != b.num; } - constexpr bool operator < (const BaseForID & b) const { return num < b.num; } - constexpr bool operator > (const BaseForID & b) const { return num > b.num; } - - constexpr BaseForID & operator++() { ++num; return *this; } - - constexpr operator NumericType() const - { - return num; - } -}; - -template -std::ostream & operator << (std::ostream & os, BaseForID id); - -template -std::ostream & operator << (std::ostream & os, BaseForID id) -{ - //We use common type with short to force char and unsigned char to be promoted and formatted as numbers. - typedef typename std::common_type::type Number; - return os << static_cast(id.getNum()); -} - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class EntityBase -{ -public: - int32_t num; - - struct hash - { - size_t operator()(const EntityBase & id) const - { - return std::hash()(id.num); - } - }; -}; - -template -class EntityIdentifier : public T -{ - using EnumType = typename T::Type; - - static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); -public: - constexpr int32_t getNum() const - { - return T::num; - } - - constexpr EnumType toEnum() const - { - return static_cast(T::num); - } - - template void serialize(Handler &h, const int version) - { - h & T::num; - } - - constexpr EntityIdentifier(const EnumType & enumValue) - { - T::num = static_cast(enumValue); - } - - constexpr EntityIdentifier(int32_t _num = -1) - { - T::num = _num; - } - - constexpr void advance(int change) - { - T::num += change; - } - - constexpr bool operator == (const EnumType & b) const { return T::num == static_cast(b); } - constexpr bool operator <= (const EnumType & b) const { return T::num <= static_cast(b); } - constexpr bool operator >= (const EnumType & b) const { return T::num >= static_cast(b); } - constexpr bool operator != (const EnumType & b) const { return T::num != static_cast(b); } - constexpr bool operator < (const EnumType & b) const { return T::num < static_cast(b); } - constexpr bool operator > (const EnumType & b) const { return T::num > static_cast(b); } - - constexpr bool operator == (const EntityIdentifier & b) const { return T::num == b.num; } - constexpr bool operator <= (const EntityIdentifier & b) const { return T::num <= b.num; } - constexpr bool operator >= (const EntityIdentifier & b) const { return T::num >= b.num; } - constexpr bool operator != (const EntityIdentifier & b) const { return T::num != b.num; } - constexpr bool operator < (const EntityIdentifier & b) const { return T::num < b.num; } - constexpr bool operator > (const EntityIdentifier & b) const { return T::num > b.num; } - - constexpr EntityIdentifier & operator++() - { - ++T::num; - return *this; - } - - constexpr EntityIdentifier operator++(int) - { - EntityIdentifier ret(*this); - ++T::num; - return ret; - } - - constexpr operator int32_t () const - { - return T::num; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class ArtifactInstanceID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(ArtifactInstanceID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class QueryID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(QueryID, si32) - - QueryID & operator++() - { - ++num; - return *this; - } -}; - -class ObjectInstanceID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(ObjectInstanceID, si32) - - DLL_LINKAGE static const ObjectInstanceID NONE; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class HeroClassID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(HeroClassID, si32) -}; - -class HeroTypeID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(HeroTypeID, si32) - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - - DLL_LINKAGE static const HeroTypeID NONE; -}; - -class SlotID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(SlotID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; - - DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; - DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; - } -}; - -class PlayerColor : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(PlayerColor, ui8) - - enum EPlayerColor - { - PLAYER_LIMIT_I = 8, - }; - - using Mask = uint8_t; - - DLL_LINKAGE static const PlayerColor SPECTATOR; //252 - DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 - DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) - DLL_LINKAGE static const PlayerColor NEUTRAL; //255 - DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map - - DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - DLL_LINKAGE bool isSpectator() const; - - DLL_LINKAGE std::string getStr(bool L10n = false) const; - DLL_LINKAGE std::string getStrCap(bool L10n = false) const; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class TeamID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(TeamID, ui8) - - DLL_LINKAGE static const TeamID NO_TEAM; - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -class TeleportChannelID : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(TeleportChannelID, si32) - - friend class CGameInfoCallback; - friend class CNonConstInfoCallback; -}; - -// Enum declarations -enum class PrimarySkill : int8_t -{ - NONE = -1, - ATTACK, - DEFENSE, - SPELL_POWER, - KNOWLEDGE, - EXPERIENCE = 4 //for some reason changePrimSkill uses it -}; - -class SecondarySkillBase : public EntityBase -{ -public: - enum Type : int32_t - { - WRONG = -2, - DEFAULT = -1, - PATHFINDING = 0, ARCHERY, LOGISTICS, SCOUTING, DIPLOMACY, NAVIGATION, LEADERSHIP, WISDOM, MYSTICISM, - LUCK, BALLISTICS, EAGLE_EYE, NECROMANCY, ESTATES, FIRE_MAGIC, AIR_MAGIC, WATER_MAGIC, EARTH_MAGIC, - SCHOLAR, TACTICS, ARTILLERY, LEARNING, OFFENCE, ARMORER, INTELLIGENCE, SORCERY, RESISTANCE, - FIRST_AID, SKILL_SIZE - }; - static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); -}; - -class SecondarySkill : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -enum class EAlignment : uint8_t { GOOD, EVIL, NEUTRAL }; - -class FactionIDBase : public EntityBase -{ -public: - enum Type : int32_t - { - NONE = -2, - DEFAULT = -1, - RANDOM = -1, - ANY = -1, - CASTLE, - RAMPART, - TOWER, - INFERNO, - NECROPOLIS, - DUNGEON, - STRONGHOLD, - FORTRESS, - CONFLUX, - NEUTRAL - }; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); - static std::string entityType(); -}; - -class FactionID : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -using ETownType = FactionID; - -class BuildingID -{ -public: - //Quite useful as long as most of building mechanics hardcoded - // NOTE: all building with completely configurable mechanics will be removed from list - enum EBuildingID - { - DEFAULT = -50, - HORDE_PLACEHOLDER7 = -36, - HORDE_PLACEHOLDER6 = -35, - HORDE_PLACEHOLDER5 = -34, - HORDE_PLACEHOLDER4 = -33, - HORDE_PLACEHOLDER3 = -32, - HORDE_PLACEHOLDER2 = -31, - HORDE_PLACEHOLDER1 = -30, - NONE = -1, - FIRST_REGULAR_ID = 0, - MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, - TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, - VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, - RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, - SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, - HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, - DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, - DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, - DWELL_LVL_6_UP, DWELL_UP_LAST=43, - - DWELL_LVL_1 = DWELL_FIRST, - DWELL_LVL_7 = DWELL_LAST, - DWELL_LVL_1_UP = DWELL_UP_FIRST, - DWELL_LVL_7_UP = DWELL_UP_LAST, - - //Special buildings for towns. - LIGHTHOUSE = SPECIAL_1, - STABLES = SPECIAL_2, //Castle - BROTHERHOOD = SPECIAL_3, - - MYSTIC_POND = SPECIAL_1, - FOUNTAIN_OF_FORTUNE = SPECIAL_2, //Rampart - TREASURY = SPECIAL_3, - - ARTIFACT_MERCHANT = SPECIAL_1, - LOOKOUT_TOWER = SPECIAL_2, //Tower - LIBRARY = SPECIAL_3, - WALL_OF_KNOWLEDGE = SPECIAL_4, - - STORMCLOUDS = SPECIAL_2, - CASTLE_GATE = SPECIAL_3, //Inferno - ORDER_OF_FIRE = SPECIAL_4, - - COVER_OF_DARKNESS = SPECIAL_1, - NECROMANCY_AMPLIFIER = SPECIAL_2, //Necropolis - SKELETON_TRANSFORMER = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MANA_VORTEX = SPECIAL_2, - PORTAL_OF_SUMMON = SPECIAL_3, //Dungeon - BATTLE_ACADEMY = SPECIAL_4, - - ESCAPE_TUNNEL = SPECIAL_1, - FREELANCERS_GUILD = SPECIAL_2, //Stronghold - BALLISTA_YARD = SPECIAL_3, - HALL_OF_VALHALLA = SPECIAL_4, - - CAGE_OF_WARLORDS = SPECIAL_1, - GLYPHS_OF_FEAR = SPECIAL_2, // Fortress - BLOOD_OBELISK = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MAGIC_UNIVERSITY = SPECIAL_2, // Conflux - }; - - BuildingID(EBuildingID _num = NONE) : num(_num) - {} - - STRONG_INLINE - bool IsSpecialOrGrail() const - { - return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; - } - - ID_LIKE_CLASS_COMMON(BuildingID, EBuildingID) - - EBuildingID num; -}; - -ID_LIKE_OPERATORS(BuildingID, BuildingID::EBuildingID) - -namespace BuildingSubID -{ - enum EBuildingSubID - { - DEFAULT = -50, - NONE = -1, - STABLES, - BROTHERHOOD_OF_SWORD, - CASTLE_GATE, - CREATURE_TRANSFORMER, - MYSTIC_POND, - FOUNTAIN_OF_FORTUNE, - ARTIFACT_MERCHANT, - LOOKOUT_TOWER, - LIBRARY, - MANA_VORTEX, - PORTAL_OF_SUMMONING, - ESCAPE_TUNNEL, - FREELANCERS_GUILD, - BALLISTA_YARD, - ATTACK_VISITING_BONUS, - MAGIC_UNIVERSITY, - SPELL_POWER_GARRISON_BONUS, - ATTACK_GARRISON_BONUS, - DEFENSE_GARRISON_BONUS, - DEFENSE_VISITING_BONUS, - SPELL_POWER_VISITING_BONUS, - KNOWLEDGE_VISITING_BONUS, - EXPERIENCE_VISITING_BONUS, - LIGHTHOUSE, - TREASURY, - CUSTOM_VISITING_BONUS, - CUSTOM_VISITING_REWARD - }; -} - -enum class EMarketMode : int8_t -{ - RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, - ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, - MARTKET_AFTER_LAST_PLACEHOLDER -}; - -namespace MappedKeys -{ - - static const std::map BUILDING_NAMES_TO_TYPES = - { - { "special1", BuildingID::SPECIAL_1 }, - { "special2", BuildingID::SPECIAL_2 }, - { "special3", BuildingID::SPECIAL_3 }, - { "special4", BuildingID::SPECIAL_4 }, - { "grail", BuildingID::GRAIL }, - { "mageGuild1", BuildingID::MAGES_GUILD_1 }, - { "mageGuild2", BuildingID::MAGES_GUILD_2 }, - { "mageGuild3", BuildingID::MAGES_GUILD_3 }, - { "mageGuild4", BuildingID::MAGES_GUILD_4 }, - { "mageGuild5", BuildingID::MAGES_GUILD_5 }, - { "tavern", BuildingID::TAVERN }, - { "shipyard", BuildingID::SHIPYARD }, - { "fort", BuildingID::FORT }, - { "citadel", BuildingID::CITADEL }, - { "castle", BuildingID::CASTLE }, - { "villageHall", BuildingID::VILLAGE_HALL }, - { "townHall", BuildingID::TOWN_HALL }, - { "cityHall", BuildingID::CITY_HALL }, - { "capitol", BuildingID::CAPITOL }, - { "marketplace", BuildingID::MARKETPLACE }, - { "resourceSilo", BuildingID::RESOURCE_SILO }, - { "blacksmith", BuildingID::BLACKSMITH }, - { "horde1", BuildingID::HORDE_1 }, - { "horde1Upgr", BuildingID::HORDE_1_UPGR }, - { "horde2", BuildingID::HORDE_2 }, - { "horde2Upgr", BuildingID::HORDE_2_UPGR }, - { "ship", BuildingID::SHIP }, - { "dwellingLvl1", BuildingID::DWELL_LVL_1 }, - { "dwellingLvl2", BuildingID::DWELL_LVL_2 }, - { "dwellingLvl3", BuildingID::DWELL_LVL_3 }, - { "dwellingLvl4", BuildingID::DWELL_LVL_4 }, - { "dwellingLvl5", BuildingID::DWELL_LVL_5 }, - { "dwellingLvl6", BuildingID::DWELL_LVL_6 }, - { "dwellingLvl7", BuildingID::DWELL_LVL_7 }, - { "dwellingUpLvl1", BuildingID::DWELL_LVL_1_UP }, - { "dwellingUpLvl2", BuildingID::DWELL_LVL_2_UP }, - { "dwellingUpLvl3", BuildingID::DWELL_LVL_3_UP }, - { "dwellingUpLvl4", BuildingID::DWELL_LVL_4_UP }, - { "dwellingUpLvl5", BuildingID::DWELL_LVL_5_UP }, - { "dwellingUpLvl6", BuildingID::DWELL_LVL_6_UP }, - { "dwellingUpLvl7", BuildingID::DWELL_LVL_7_UP }, - }; - - static const std::map SPECIAL_BUILDINGS = - { - { "mysticPond", BuildingSubID::MYSTIC_POND }, - { "artifactMerchant", BuildingSubID::ARTIFACT_MERCHANT }, - { "freelancersGuild", BuildingSubID::FREELANCERS_GUILD }, - { "magicUniversity", BuildingSubID::MAGIC_UNIVERSITY }, - { "castleGate", BuildingSubID::CASTLE_GATE }, - { "creatureTransformer", BuildingSubID::CREATURE_TRANSFORMER },//only skeleton transformer yet - { "portalOfSummoning", BuildingSubID::PORTAL_OF_SUMMONING }, - { "ballistaYard", BuildingSubID::BALLISTA_YARD }, - { "stables", BuildingSubID::STABLES }, - { "manaVortex", BuildingSubID::MANA_VORTEX }, - { "lookoutTower", BuildingSubID::LOOKOUT_TOWER }, - { "library", BuildingSubID::LIBRARY }, - { "brotherhoodOfSword", BuildingSubID::BROTHERHOOD_OF_SWORD },//morale garrison bonus - { "fountainOfFortune", BuildingSubID::FOUNTAIN_OF_FORTUNE },//luck garrison bonus - { "spellPowerGarrisonBonus", BuildingSubID::SPELL_POWER_GARRISON_BONUS },//such as 'stormclouds', but this name is not ok for good towns - { "attackGarrisonBonus", BuildingSubID::ATTACK_GARRISON_BONUS }, - { "defenseGarrisonBonus", BuildingSubID::DEFENSE_GARRISON_BONUS }, - { "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL }, - { "attackVisitingBonus", BuildingSubID::ATTACK_VISITING_BONUS }, - { "defenceVisitingBonus", BuildingSubID::DEFENSE_VISITING_BONUS }, - { "spellPowerVisitingBonus", BuildingSubID::SPELL_POWER_VISITING_BONUS }, - { "knowledgeVisitingBonus", BuildingSubID::KNOWLEDGE_VISITING_BONUS }, - { "experienceVisitingBonus", BuildingSubID::EXPERIENCE_VISITING_BONUS }, - { "lighthouse", BuildingSubID::LIGHTHOUSE }, - { "treasury", BuildingSubID::TREASURY } - }; - - static const std::map MARKET_NAMES_TO_TYPES = - { - { "resource-resource", EMarketMode::RESOURCE_RESOURCE }, - { "resource-player", EMarketMode::RESOURCE_PLAYER }, - { "creature-resource", EMarketMode::CREATURE_RESOURCE }, - { "resource-artifact", EMarketMode::RESOURCE_ARTIFACT }, - { "artifact-resource", EMarketMode::ARTIFACT_RESOURCE }, - { "artifact-experience", EMarketMode::ARTIFACT_EXP }, - { "creature-experience", EMarketMode::CREATURE_EXP }, - { "creature-undead", EMarketMode::CREATURE_UNDEAD }, - { "resource-skill", EMarketMode::RESOURCE_SKILL }, - }; -} - -enum class EAiTactic : int8_t -{ - NONE = -1, - RANDOM, - WARRIOR, - BUILDER, - EXPLORER -}; - -enum class EBuildingState : int8_t -{ - HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, - NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED -}; - -enum class ESpellCastProblem : int8_t -{ - OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, - HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, - SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, - NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, - MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all - INVALID -}; - -namespace ECommander -{ - enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; - const int MAX_SKILL_LEVEL = 5; -} - -enum class EWallPart : int8_t -{ - INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, - KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, - PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ -}; - -enum class EWallState : int8_t -{ - NONE = -1, //no wall - DESTROYED, - DAMAGED, - INTACT, - REINFORCED, // walls in towns with castle -}; - -enum class EGateState : int8_t -{ - NONE, - CLOSED, - BLOCKED, // gate is blocked in closed state, e.g. by creature - OPENED, - DESTROYED -}; - - -enum class ETileType : int8_t -{ - FREE, - POSSIBLE, - BLOCKED, - USED -}; - -enum class ETeleportChannelType : int8_t -{ - IMPASSABLE, - BIDIRECTIONAL, - UNIDIRECTIONAL, - MIXED -}; - -class ObjBase : public EntityBase -{ -public: - enum Type - { - NO_OBJ = -1, - ALTAR_OF_SACRIFICE [[deprecated]] = 2, - ANCHOR_POINT = 3, - ARENA = 4, - ARTIFACT = 5, - PANDORAS_BOX = 6, - BLACK_MARKET [[deprecated]] = 7, - BOAT = 8, - BORDERGUARD = 9, - KEYMASTER = 10, - BUOY = 11, - CAMPFIRE = 12, - CARTOGRAPHER = 13, - SWAN_POND = 14, - COVER_OF_DARKNESS = 15, - CREATURE_BANK = 16, - CREATURE_GENERATOR1 = 17, - CREATURE_GENERATOR2 = 18, - CREATURE_GENERATOR3 = 19, - CREATURE_GENERATOR4 = 20, - CURSED_GROUND1 = 21, - CORPSE = 22, - MARLETTO_TOWER = 23, - DERELICT_SHIP = 24, - DRAGON_UTOPIA = 25, - EVENT = 26, - EYE_OF_MAGI = 27, - FAERIE_RING = 28, - FLOTSAM = 29, - FOUNTAIN_OF_FORTUNE = 30, - FOUNTAIN_OF_YOUTH = 31, - GARDEN_OF_REVELATION = 32, - GARRISON = 33, - HERO = 34, - HILL_FORT = 35, - GRAIL = 36, - HUT_OF_MAGI = 37, - IDOL_OF_FORTUNE = 38, - LEAN_TO = 39, - LIBRARY_OF_ENLIGHTENMENT = 41, - LIGHTHOUSE = 42, - MONOLITH_ONE_WAY_ENTRANCE = 43, - MONOLITH_ONE_WAY_EXIT = 44, - MONOLITH_TWO_WAY = 45, - MAGIC_PLAINS1 = 46, - SCHOOL_OF_MAGIC = 47, - MAGIC_SPRING = 48, - MAGIC_WELL = 49, - MARKET_OF_TIME = 50, - MERCENARY_CAMP = 51, - MERMAID = 52, - MINE = 53, - MONSTER = 54, - MYSTICAL_GARDEN = 55, - OASIS = 56, - OBELISK = 57, - REDWOOD_OBSERVATORY = 58, - OCEAN_BOTTLE = 59, - PILLAR_OF_FIRE = 60, - STAR_AXIS = 61, - PRISON = 62, - PYRAMID = 63,//subtype 0 - WOG_OBJECT = 63,//subtype > 0 - RALLY_FLAG = 64, - RANDOM_ART = 65, - RANDOM_TREASURE_ART = 66, - RANDOM_MINOR_ART = 67, - RANDOM_MAJOR_ART = 68, - RANDOM_RELIC_ART = 69, - RANDOM_HERO = 70, - RANDOM_MONSTER = 71, - RANDOM_MONSTER_L1 = 72, - RANDOM_MONSTER_L2 = 73, - RANDOM_MONSTER_L3 = 74, - RANDOM_MONSTER_L4 = 75, - RANDOM_RESOURCE = 76, - RANDOM_TOWN = 77, - REFUGEE_CAMP = 78, - RESOURCE = 79, - SANCTUARY = 80, - SCHOLAR = 81, - SEA_CHEST = 82, - SEER_HUT = 83, - CRYPT = 84, - SHIPWRECK = 85, - SHIPWRECK_SURVIVOR = 86, - SHIPYARD = 87, - SHRINE_OF_MAGIC_INCANTATION = 88, - SHRINE_OF_MAGIC_GESTURE = 89, - SHRINE_OF_MAGIC_THOUGHT = 90, - SIGN = 91, - SIRENS = 92, - SPELL_SCROLL = 93, - STABLES = 94, - TAVERN = 95, - TEMPLE = 96, - DEN_OF_THIEVES = 97, - TOWN = 98, - TRADING_POST [[deprecated]] = 99, - LEARNING_STONE = 100, - TREASURE_CHEST = 101, - TREE_OF_KNOWLEDGE = 102, - SUBTERRANEAN_GATE = 103, - UNIVERSITY [[deprecated]] = 104, - WAGON = 105, - WAR_MACHINE_FACTORY = 106, - SCHOOL_OF_WAR = 107, - WARRIORS_TOMB = 108, - WATER_WHEEL = 109, - WATERING_HOLE = 110, - WHIRLPOOL = 111, - WINDMILL = 112, - WITCH_HUT = 113, - HOLE = 124, - RANDOM_MONSTER_L5 = 162, - RANDOM_MONSTER_L6 = 163, - RANDOM_MONSTER_L7 = 164, - BORDER_GATE = 212, - FREELANCERS_GUILD [[deprecated]] = 213, - HERO_PLACEHOLDER = 214, - QUEST_GUARD = 215, - RANDOM_DWELLING = 216, - RANDOM_DWELLING_LVL = 217, //subtype = creature level - RANDOM_DWELLING_FACTION = 218, //subtype = faction - GARRISON2 = 219, - ABANDONED_MINE = 220, - TRADING_POST_SNOW [[deprecated]] = 221, - CLOVER_FIELD = 222, - CURSED_GROUND2 = 223, - EVIL_FOG = 224, - FAVORABLE_WINDS = 225, - FIERY_FIELDS = 226, - HOLY_GROUNDS = 227, - LUCID_POOLS = 228, - MAGIC_CLOUDS = 229, - MAGIC_PLAINS2 = 230, - ROCKLANDS = 231, - }; -}; - -class Obj : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class RoadIdBase : public EntityBase -{ -public: - enum Type : int32_t - { - NO_ROAD = 0, - FIRST_REGULAR_ROAD = 1, - DIRT_ROAD = 1, - GRAVEL_ROAD = 2, - COBBLESTONE_ROAD = 3, - ORIGINAL_ROAD_COUNT //+1 - }; -}; - -class RoadId : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class RiverIdBase : public EntityBase -{ -public: - enum Type : int32_t - { - NO_RIVER = 0, - FIRST_REGULAR_RIVER = 1, - WATER_RIVER = 1, - ICY_RIVER = 2, - MUD_RIVER = 3, - LAVA_RIVER = 4, - ORIGINAL_RIVER_COUNT //+1 - }; -}; - -class RiverId : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -using River = RiverId; -using Road = RoadId; - -namespace SecSkillLevel -{ - enum SecSkillLevel - { - NONE, - BASIC, - ADVANCED, - EXPERT, - LEVELS_SIZE - }; -} - -enum class Date : int8_t -{ - DAY = 0, - DAY_OF_WEEK = 1, - WEEK = 2, - MONTH = 3, - DAY_OF_MONTH -}; - -enum class EActionType : int8_t -{ - NO_ACTION, - - END_TACTIC_PHASE, - RETREAT, - SURRENDER, - - HERO_SPELL, - - WALK, - WAIT, - DEFEND, - WALK_AND_ATTACK, - SHOOT, - CATAPULT, - MONSTER_SPELL, - BAD_MORALE, - STACK_HEAL, -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EActionType actionType); - -enum class EDiggingStatus : int8_t -{ - UNKNOWN = -1, - CAN_DIG = 0, - LACK_OF_MOVEMENT, - WRONG_TERRAIN, - TILE_OCCUPIED, - BACKPACK_IS_FULL -}; - -class DLL_LINKAGE EPathfindingLayer -{ -public: - enum EEPathfindingLayer : ui8 - { - LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO - }; - - EPathfindingLayer(EEPathfindingLayer _num = WRONG) : num(_num) - {} - - ID_LIKE_CLASS_COMMON(EPathfindingLayer, EEPathfindingLayer) - - EEPathfindingLayer num; -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EPathfindingLayer & pathfindingLayer); - -ID_LIKE_OPERATORS(EPathfindingLayer, EPathfindingLayer::EEPathfindingLayer) - -enum class EPlayerStatus -{ - WRONG = -1, - INGAME, - LOSER, - WINNER -}; - -enum class PlayerRelations -{ - ENEMIES, - ALLIES, - SAME_PLAYER -}; - -class ArtifactPosition -{ -public: - enum EArtifactPosition - { - TRANSITION_POS = -3, - FIRST_AVAILABLE = -2, - PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack - HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 - RIGHT_RING, LEFT_RING, FEET, //8 - MISC1, MISC2, MISC3, MISC4, //12 - MACH1, MACH2, MACH3, MACH4, //16 - SPELLBOOK, MISC5, //18 - AFTER_LAST, - //cres - CREATURE_SLOT = 0, - COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST - }; - - static_assert (AFTER_LAST == 19, "incorrect number of artifact slots"); - - ArtifactPosition(EArtifactPosition _num = PRE_FIRST) : num(_num) - {} - - ArtifactPosition(std::string slotName); - - ID_LIKE_CLASS_COMMON(ArtifactPosition, EArtifactPosition) - - EArtifactPosition num; - - STRONG_INLINE EArtifactPosition operator+(const int arg) - { - return EArtifactPosition(static_cast(num) + arg); - } - STRONG_INLINE EArtifactPosition operator+(const EArtifactPosition & arg) - { - return EArtifactPosition(static_cast(num) + static_cast(arg)); - } -}; - -ID_LIKE_OPERATORS(ArtifactPosition, ArtifactPosition::EArtifactPosition) - -namespace GameConstants -{ - const auto BACKPACK_START = ArtifactPosition::AFTER_LAST; -} - -class ArtifactIDBase : public EntityBase -{ -public: - enum Type - { - NONE = -1, - SPELLBOOK = 0, - SPELL_SCROLL = 1, - GRAIL = 2, - CATAPULT = 3, - BALLISTA = 4, - AMMO_CART = 5, - FIRST_AID_TENT = 6, - VIAL_OF_DRAGON_BLOOD = 127, - ARMAGEDDONS_BLADE = 128, - TITANS_THUNDER = 135, - ART_SELECTION = 144, - ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 - }; - - DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); -}; - -class ArtifactID : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class CreatureIDBase : public EntityBase -{ -public: - enum Type - { - NONE = -1, - ARCHER = 2, // for debug / fallback - IMP = 42, // for Deity of Fire - SKELETON = 56, // for Skeleton Transformer - BONE_DRAGON = 68, // for Skeleton Transformer - TROGLODYTES = 70, // for Abandoned Mine - MEDUSA = 76, // for Siege UI workaround - HYDRA = 110, // for Skeleton Transformer - CHAOS_HYDRA = 111, // for Skeleton Transformer - AIR_ELEMENTAL = 112, // for tests - FIRE_ELEMENTAL = 114, // for tests - PSYCHIC_ELEMENTAL = 120, // for hardcoded ability - MAGIC_ELEMENTAL = 121, // for hardcoded ability - CATAPULT = 145, - BALLISTA = 146, - FIRST_AID_TENT = 147, - AMMO_CART = 148, - ARROW_TOWERS = 149 - }; - - DLL_LINKAGE const CCreature * toCreature() const; - DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); -}; - -class CreatureID : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class SpellIDBase : public EntityBase -{ -public: - enum Type - { - SPELLBOOK_PRESET = -3, - PRESET = -2, - NONE = -1, - SUMMON_BOAT=0, SCUTTLE_BOAT=1, VISIONS=2, VIEW_EARTH=3, DISGUISE=4, VIEW_AIR=5, - FLY=6, WATER_WALK=7, DIMENSION_DOOR=8, TOWN_PORTAL=9, - - QUICKSAND=10, LAND_MINE=11, FORCE_FIELD=12, FIRE_WALL=13, EARTHQUAKE=14, - MAGIC_ARROW=15, ICE_BOLT=16, LIGHTNING_BOLT=17, IMPLOSION=18, - CHAIN_LIGHTNING=19, FROST_RING=20, FIREBALL=21, INFERNO=22, - METEOR_SHOWER=23, DEATH_RIPPLE=24, DESTROY_UNDEAD=25, ARMAGEDDON=26, - SHIELD=27, AIR_SHIELD=28, FIRE_SHIELD=29, PROTECTION_FROM_AIR=30, - PROTECTION_FROM_FIRE=31, PROTECTION_FROM_WATER=32, - PROTECTION_FROM_EARTH=33, ANTI_MAGIC=34, DISPEL=35, MAGIC_MIRROR=36, - CURE=37, RESURRECTION=38, ANIMATE_DEAD=39, SACRIFICE=40, BLESS=41, - CURSE=42, BLOODLUST=43, PRECISION=44, WEAKNESS=45, STONE_SKIN=46, - DISRUPTING_RAY=47, PRAYER=48, MIRTH=49, SORROW=50, FORTUNE=51, - MISFORTUNE=52, HASTE=53, SLOW=54, SLAYER=55, FRENZY=56, - TITANS_LIGHTNING_BOLT=57, COUNTERSTRIKE=58, BERSERK=59, HYPNOTIZE=60, - FORGETFULNESS=61, BLIND=62, TELEPORT=63, REMOVE_OBSTACLE=64, CLONE=65, - SUMMON_FIRE_ELEMENTAL=66, SUMMON_EARTH_ELEMENTAL=67, SUMMON_WATER_ELEMENTAL=68, SUMMON_AIR_ELEMENTAL=69, - - STONE_GAZE=70, POISON=71, BIND=72, DISEASE=73, PARALYZE=74, AGE=75, DEATH_CLOUD=76, THUNDERBOLT=77, - DISPEL_HELPFUL_SPELLS=78, DEATH_STARE=79, ACID_BREATH_DEFENSE=80, ACID_BREATH_DAMAGE=81, - - FIRST_NON_SPELL = 70, AFTER_LAST = 82 - }; - - DLL_LINKAGE const CSpell * toSpell() const; //deprecated - DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); -}; - -class SpellID : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class BattleFieldInfo; -class BattleField : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(BattleField, si32) - - DLL_LINKAGE static const BattleField NONE; - - DLL_LINKAGE friend bool operator==(const BattleField & l, const BattleField & r); - DLL_LINKAGE friend bool operator!=(const BattleField & l, const BattleField & r); - DLL_LINKAGE friend bool operator<(const BattleField & l, const BattleField & r); - - DLL_LINKAGE const BattleFieldInfo * getInfo() const; -}; - -class BoatIdBase : public EntityBase -{ -public: - enum Type : int32_t - { - NONE = -1, - NECROPOLIS = 0, - CASTLE, - FORTRESS - }; -}; - -class BoatId : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -class TerrainIdBase : public EntityBase -{ -public: - enum Type : int32_t - { - NATIVE_TERRAIN = -4, - ANY_TERRAIN = -3, - NONE = -1, - FIRST_REGULAR_TERRAIN = 0, - DIRT = 0, - SAND, - GRASS, - SNOW, - SWAMP, - ROUGH, - SUBTERRANEAN, - LAVA, - WATER, - ROCK, - ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK - }; - - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - static std::string entityType(); -}; - -class TerrainId : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -using ETerrainId = TerrainId; - -class ObstacleInfo; -class Obstacle : public BaseForID -{ - INSTID_LIKE_CLASS_COMMON(Obstacle, si32) - - DLL_LINKAGE const ObstacleInfo * getInfo() const; -}; - -class SpellSchoolBase : public EntityBase -{ -public: - enum Type : int32_t - { - ANY = -1, - AIR = 0, - FIRE = 1, - WATER = 2, - EARTH = 3, - }; -}; - -class SpellSchool : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -using ESpellSchool = SpellSchool; - -enum class EMetaclass: int8_t -{ - INVALID = 0, - ARTIFACT, - CREATURE, - FACTION, - EXPERIENCE, - HERO, - HEROCLASS, - LUCK, - MANA, - MORALE, - MOVEMENT, - OBJECT, - PRIMARY_SKILL, - SECONDARY_SKILL, - SPELL, - RESOURCE -}; - -enum class EHealLevel: ui8 -{ - HEAL, - RESURRECT, - OVERHEAL -}; - -enum class EHealPower : ui8 -{ - ONE_BATTLE, - PERMANENT -}; - -enum class EBattleResult : int8_t -{ - NORMAL = 0, - ESCAPE = 1, - SURRENDER = 2 -}; - -class GameResIDBase : public EntityBase -{ -public: - enum Type : int32_t - { - WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - COUNT, - - WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 - }; -}; - -class GameResID : public EntityIdentifier -{ -public: - using EntityIdentifier::EntityIdentifier; -}; - -using EGameResID = GameResID; - -// Typedef declarations using TExpType = si64; using TQuantity = si32; - using TRmgTemplateZoneId = int; -#undef ID_LIKE_CLASS_COMMON -#undef ID_LIKE_OPERATORS -#undef ID_LIKE_OPERATORS_INTERNAL -#undef INSTID_LIKE_CLASS_COMMON - VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index f6d4a80df..842b3fdf2 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -23,7 +23,7 @@ #include "VCMI_Lib.h" //for identifier resolution #include "CGeneralTextHandler.h" #include "JsonDetail.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "battle/BattleHex.h" namespace diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index e5f14a03d..270c47b80 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -15,7 +15,7 @@ #include "JsonNode.h" #include "CRandomGenerator.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "VCMI_Lib.h" #include "CArtHandler.h" #include "CCreatureHandler.h" @@ -195,10 +195,10 @@ namespace JsonRandom allowedClasses.insert(CArtHandler::stringToClass(entry.String())); if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING) - allowedPositions.insert(ArtifactPosition(value["class"].String())); + allowedPositions.insert(ArtifactPosition::decode(value["class"].String())); else for(const auto & entry : value["slot"].Vector()) - allowedPositions.insert(ArtifactPosition(entry.String())); + allowedPositions.insert(ArtifactPosition::decode(entry.String())); if (!value["minValue"].isNull()) minValue = static_cast(value["minValue"].Float()); if (!value["maxValue"].isNull()) maxValue = static_cast(value["maxValue"].Float()); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 7a1a90463..33706619d 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1382,7 +1382,7 @@ void HeroRecruited::applyGs(CGameState * gs) const CGTownInstance *t = gs->getTown(tid); PlayerState *p = gs->getPlayerState(player); - if (boatId >= 0) + if (boatId != ObjectInstanceID::NONE) { CGObjectInstance *obj = gs->getObjInstance(boatId); auto * boat = dynamic_cast(obj); @@ -1418,7 +1418,7 @@ void GiveHero::applyGs(CGameState * gs) const { CGHeroInstance *h = gs->getHero(id); - if (boatId >= 0) + if (boatId != ObjectInstanceID::NONE) { CGObjectInstance *obj = gs->getObjInstance(boatId); auto * boat = dynamic_cast(obj); @@ -1880,7 +1880,7 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) break; } - if(srcPos >= GameConstants::BACKPACK_START) + if(srcPos >= ArtifactPosition::BACKPACK_START) { numBackpackArtifactsMoved++; } @@ -1925,7 +1925,7 @@ void AssembledArtifact::applyGs(CGameState *gs) { ArtifactPosition pos = combineEquipped ? artSet->getArtPos(constituent->getId(), true, false) : artSet->getArtBackpackPos(constituent->getId()); - assert(pos >= 0); + assert(pos != ArtifactPosition::PRE_FIRST); CArtifactInstance * constituentInstance = artSet->getArt(pos); //move constituent from hero to be part of new, combined artifact diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index 56fc50442..e89855dcf 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -11,7 +11,7 @@ #include "StdInc.h" #include "GameConstants.h" #include "ResourceSet.h" -#include "StringConstants.h" +#include "constants/StringConstants.h" #include "JsonNode.h" #include "serializer/JsonSerializeFormat.h" #include "mapObjects/CObjectHandler.h" diff --git a/lib/StringConstants.h b/lib/StringConstants.h deleted file mode 100644 index e6190f756..000000000 --- a/lib/StringConstants.h +++ /dev/null @@ -1,125 +0,0 @@ -/* - * StringConstants.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 "GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// -/// String ID which are pointless to move to config file - these types are mostly hardcoded -/// -namespace GameConstants -{ - const std::string RESOURCE_NAMES [RESOURCE_QUANTITY] = { - "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" - }; - - const std::string PLAYER_COLOR_NAMES [PlayerColor::PLAYER_LIMIT_I] = { - "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" - }; - - const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; -} - -namespace NPrimarySkill -{ - const std::string names [GameConstants::PRIMARY_SKILLS] = { "attack", "defence", "spellpower", "knowledge" }; -} - -namespace NSecondarySkill -{ - const std::string names [GameConstants::SKILL_QUANTITY] = - { - "pathfinding", "archery", "logistics", "scouting", "diplomacy", // 5 - "navigation", "leadership", "wisdom", "mysticism", "luck", // 10 - "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", // 15 - "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", // 20 - "artillery", "learning", "offence", "armorer", "intelligence", // 25 - "sorcery", "resistance", "firstAid" - }; - - const std::vector levels = - { - "none", "basic", "advanced", "expert" - }; -} - -namespace EBuildingType -{ - const std::string names [44] = - { - "mageGuild1", "mageGuild2", "mageGuild3", "mageGuild4", "mageGuild5", // 5 - "tavern", "shipyard", "fort", "citadel", "castle", // 10 - "villageHall", "townHall", "cityHall", "capitol", "marketplace", // 15 - "resourceSilo", "blacksmith", "special1", "horde1", "horde1Upgr", // 20 - "ship", "special2", "special3", "special4", "horde2", // 25 - "horde2Upgr", "grail", "extraTownHall", "extraCityHall", "extraCapitol", // 30 - "dwellingLvl1", "dwellingLvl2", "dwellingLvl3", "dwellingLvl4", "dwellingLvl5", // 35 - "dwellingLvl6", "dwellingLvl7", "dwellingUpLvl1", "dwellingUpLvl2", "dwellingUpLvl3", // 40 - "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7" - }; -} - -namespace NFaction -{ - const std::string names [GameConstants::F_NUMBER] = - { - "castle", "rampart", "tower", - "inferno", "necropolis", "dungeon", - "stronghold", "fortress", "conflux" - }; -} - -namespace NArtifactPosition -{ - const std::string namesHero [19] = - { - "head", "shoulders", "neck", "rightHand", "leftHand", "torso", //5 - "rightRing", "leftRing", "feet", //8 - "misc1", "misc2", "misc3", "misc4", //12 - "mach1", "mach2", "mach3", "mach4", //16 - "spellbook", "misc5" //18 - }; - - const std::string namesCreature[1] = - { - "creature1" - }; - - const std::string namesCommander[6] = - { - "commander1", "commander2", "commander3", "commander4", "commander5", "commander6", - }; - - - const std::string backpack = "backpack"; -} - -namespace NMetaclass -{ - const std::string names [16] = - { - "", - "artifact", "creature", "faction", "experience", "hero", - "heroClass", "luck", "mana", "morale", "movement", - "object", "primarySkill", "secondarySkill", "spell", "resource" - }; -} - -namespace NPathfindingLayer -{ - const std::string names[EPathfindingLayer::NUM_LAYERS] = - { - "land", "sail", "water", "air" - }; -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index 488cdf3ac..0e82ca884 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -122,9 +122,6 @@ BattleAction BattleAction::makeRetreat(ui8 side) std::string BattleAction::toString() const { - std::stringstream actionTypeStream; - actionTypeStream << actionType; - std::stringstream targetStream; for(const DestinationInfo & info : target) @@ -143,7 +140,7 @@ std::string BattleAction::toString() const } boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); - fmt % static_cast(side) % stackNumber % actionTypeStream.str() % spell.getNum() % targetStream.str(); + fmt % static_cast(side) % stackNumber % static_cast(actionType) % spell.getNum() % targetStream.str(); return fmt.str(); } diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index cd493b17b..ff81c18eb 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -25,7 +25,7 @@ #include "../CSkillHandler.h" #include "../CArtHandler.h" #include "../TerrainHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" #include "../modding/ModUtility.h" diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index 64186b908..7916edb42 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -22,7 +22,7 @@ #include "../CStack.h" #include "../CArtHandler.h" #include "../TerrainHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index e239dec12..63630b6e2 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -21,7 +21,7 @@ #include "../CGeneralTextHandler.h" #include "../TextOperations.h" #include "../Languages.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../mapping/CMapHeader.h" #include "../mapping/CMapService.h" #include "../modding/CModHandler.h" diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h new file mode 100644 index 000000000..759934307 --- /dev/null +++ b/lib/constants/EntityIdentifiers.h @@ -0,0 +1,896 @@ +/* + * GameConstants.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 "NumericConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Artifact; +class ArtifactService; +class Creature; +class CreatureService; + +namespace spells +{ + class Spell; + class Service; +} + +class CArtifact; +class CArtifactInstance; +class CCreature; +class CHero; +class CSpell; +class CSkill; +class CGameInfoCallback; +class CNonConstInfoCallback; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class IdentifierBase +{ +protected: + constexpr IdentifierBase(int32_t value = -1 ): + num(value) + {} +public: + int32_t num; + + constexpr int32_t getNum() const + { + return num; + } + + struct hash + { + size_t operator()(const IdentifierBase & id) const + { + return std::hash()(id.num); + } + }; + + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) + { + return os << dt.num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons +template +class Identifier : public IdentifierBase +{ + using T = IdentifierBase; +public: + constexpr Identifier(int32_t _num = -1) + :IdentifierBase(_num) + {} + + template void serialize(Handler &h, const int version) + { + h & T::num; + } + + constexpr bool operator == (const Identifier & b) const { return T::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return T::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return T::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return T::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return T::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return T::num > b.num; } + + constexpr FinalClass & operator++() + { + ++T::num; + return static_cast(*this); + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(num); + ++T::num; + return ret; + } + + constexpr operator int32_t () const + { + return T::num; + } + + constexpr void advance(int change) + { + T::num += change; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class IdentifierWithEnum : public T +{ + using EnumType = typename T::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr int32_t getNum() const + { + return T::num; + } + + constexpr EnumType toEnum() const + { + return static_cast(T::num); + } + + template void serialize(Handler &h, const int version) + { + h & T::num; + } + + constexpr IdentifierWithEnum(const EnumType & enumValue) + { + T::num = static_cast(enumValue); + } + + constexpr IdentifierWithEnum(int32_t _num = -1) + { + T::num = _num; + } + + constexpr void advance(int change) + { + T::num += change; + } + + constexpr bool operator == (const EnumType & b) const { return T::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return T::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return T::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return T::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return T::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return T::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return T::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return T::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return T::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return T::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return T::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return T::num > b.num; } + + constexpr IdentifierWithEnum & operator++() + { + ++T::num; + return *this; + } + + constexpr IdentifierWithEnum operator++(int) + { + IdentifierWithEnum ret(*this); + ++T::num; + return ret; + } + + constexpr operator int32_t () const + { + return T::num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class ArtifactInstanceID : public Identifier +{ +public: + using Identifier::Identifier; +}; + +class QueryID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const QueryID NONE; +}; + +class ObjectInstanceID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const ObjectInstanceID NONE; +}; + +class HeroClassID : public Identifier +{ +public: + using Identifier::Identifier; +}; + +class HeroTypeID : public Identifier +{ +public: + using Identifier::Identifier; + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + + DLL_LINKAGE static const HeroTypeID NONE; +}; + +class SlotID : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; + DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; + } +}; + +class PlayerColor : public Identifier +{ +public: + using Identifier::Identifier; + + enum EPlayerColor + { + PLAYER_LIMIT_I = 8, + }; + + using Mask = uint8_t; + + DLL_LINKAGE static const PlayerColor SPECTATOR; //252 + DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 + DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) + DLL_LINKAGE static const PlayerColor NEUTRAL; //255 + DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map + + DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + DLL_LINKAGE bool isSpectator() const; + + DLL_LINKAGE std::string getStr(bool L10n = false) const; + DLL_LINKAGE std::string getStrCap(bool L10n = false) const; +}; + +class TeamID : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const TeamID NO_TEAM; +}; + +class TeleportChannelID : public Identifier +{ +public: + using Identifier::Identifier; +}; + +class SecondarySkillBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + WRONG = -2, + DEFAULT = -1, + PATHFINDING = 0, ARCHERY, LOGISTICS, SCOUTING, DIPLOMACY, NAVIGATION, LEADERSHIP, WISDOM, MYSTICISM, + LUCK, BALLISTICS, EAGLE_EYE, NECROMANCY, ESTATES, FIRE_MAGIC, AIR_MAGIC, WATER_MAGIC, EARTH_MAGIC, + SCHOLAR, TACTICS, ARTILLERY, LEARNING, OFFENCE, ARMORER, INTELLIGENCE, SORCERY, RESISTANCE, + FIRST_AID, SKILL_SIZE + }; + static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); +}; + +class SecondarySkill : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class FactionIDBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NONE = -2, + DEFAULT = -1, + RANDOM = -1, + ANY = -1, + CASTLE, + RAMPART, + TOWER, + INFERNO, + NECROPOLIS, + DUNGEON, + STRONGHOLD, + FORTRESS, + CONFLUX, + NEUTRAL + }; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class FactionID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +using ETownType = FactionID; + +class BuildingIDBase : public IdentifierBase +{ +public: + //Quite useful as long as most of building mechanics hardcoded + // NOTE: all building with completely configurable mechanics will be removed from list + enum Type + { + DEFAULT = -50, + HORDE_PLACEHOLDER7 = -36, + HORDE_PLACEHOLDER6 = -35, + HORDE_PLACEHOLDER5 = -34, + HORDE_PLACEHOLDER4 = -33, + HORDE_PLACEHOLDER3 = -32, + HORDE_PLACEHOLDER2 = -31, + HORDE_PLACEHOLDER1 = -30, + NONE = -1, + FIRST_REGULAR_ID = 0, + MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, + TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, + VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, + RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, + SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, + HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, + DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, + DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, + DWELL_LVL_6_UP, DWELL_UP_LAST=43, + + DWELL_LVL_1 = DWELL_FIRST, + DWELL_LVL_7 = DWELL_LAST, + DWELL_LVL_1_UP = DWELL_UP_FIRST, + DWELL_LVL_7_UP = DWELL_UP_LAST, + + DWELL_UP2_FIRST = DWELL_LVL_7_UP + 1, + + //Special buildings for towns. + LIGHTHOUSE = SPECIAL_1, + STABLES = SPECIAL_2, //Castle + BROTHERHOOD = SPECIAL_3, + + MYSTIC_POND = SPECIAL_1, + FOUNTAIN_OF_FORTUNE = SPECIAL_2, //Rampart + TREASURY = SPECIAL_3, + + ARTIFACT_MERCHANT = SPECIAL_1, + LOOKOUT_TOWER = SPECIAL_2, //Tower + LIBRARY = SPECIAL_3, + WALL_OF_KNOWLEDGE = SPECIAL_4, + + STORMCLOUDS = SPECIAL_2, + CASTLE_GATE = SPECIAL_3, //Inferno + ORDER_OF_FIRE = SPECIAL_4, + + COVER_OF_DARKNESS = SPECIAL_1, + NECROMANCY_AMPLIFIER = SPECIAL_2, //Necropolis + SKELETON_TRANSFORMER = SPECIAL_3, + + //ARTIFACT_MERCHANT - same ID as in tower + MANA_VORTEX = SPECIAL_2, + PORTAL_OF_SUMMON = SPECIAL_3, //Dungeon + BATTLE_ACADEMY = SPECIAL_4, + + ESCAPE_TUNNEL = SPECIAL_1, + FREELANCERS_GUILD = SPECIAL_2, //Stronghold + BALLISTA_YARD = SPECIAL_3, + HALL_OF_VALHALLA = SPECIAL_4, + + CAGE_OF_WARLORDS = SPECIAL_1, + GLYPHS_OF_FEAR = SPECIAL_2, // Fortress + BLOOD_OBELISK = SPECIAL_3, + + //ARTIFACT_MERCHANT - same ID as in tower + MAGIC_UNIVERSITY = SPECIAL_2, // Conflux + }; + + bool IsSpecialOrGrail() const + { + return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; + } +}; + +class BuildingID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ObjBase : public IdentifierBase +{ +public: + enum Type + { + NO_OBJ = -1, + ALTAR_OF_SACRIFICE [[deprecated]] = 2, + ANCHOR_POINT = 3, + ARENA = 4, + ARTIFACT = 5, + PANDORAS_BOX = 6, + BLACK_MARKET [[deprecated]] = 7, + BOAT = 8, + BORDERGUARD = 9, + KEYMASTER = 10, + BUOY = 11, + CAMPFIRE = 12, + CARTOGRAPHER = 13, + SWAN_POND = 14, + COVER_OF_DARKNESS = 15, + CREATURE_BANK = 16, + CREATURE_GENERATOR1 = 17, + CREATURE_GENERATOR2 = 18, + CREATURE_GENERATOR3 = 19, + CREATURE_GENERATOR4 = 20, + CURSED_GROUND1 = 21, + CORPSE = 22, + MARLETTO_TOWER = 23, + DERELICT_SHIP = 24, + DRAGON_UTOPIA = 25, + EVENT = 26, + EYE_OF_MAGI = 27, + FAERIE_RING = 28, + FLOTSAM = 29, + FOUNTAIN_OF_FORTUNE = 30, + FOUNTAIN_OF_YOUTH = 31, + GARDEN_OF_REVELATION = 32, + GARRISON = 33, + HERO = 34, + HILL_FORT = 35, + GRAIL = 36, + HUT_OF_MAGI = 37, + IDOL_OF_FORTUNE = 38, + LEAN_TO = 39, + LIBRARY_OF_ENLIGHTENMENT = 41, + LIGHTHOUSE = 42, + MONOLITH_ONE_WAY_ENTRANCE = 43, + MONOLITH_ONE_WAY_EXIT = 44, + MONOLITH_TWO_WAY = 45, + MAGIC_PLAINS1 = 46, + SCHOOL_OF_MAGIC = 47, + MAGIC_SPRING = 48, + MAGIC_WELL = 49, + MARKET_OF_TIME = 50, + MERCENARY_CAMP = 51, + MERMAID = 52, + MINE = 53, + MONSTER = 54, + MYSTICAL_GARDEN = 55, + OASIS = 56, + OBELISK = 57, + REDWOOD_OBSERVATORY = 58, + OCEAN_BOTTLE = 59, + PILLAR_OF_FIRE = 60, + STAR_AXIS = 61, + PRISON = 62, + PYRAMID = 63,//subtype 0 + WOG_OBJECT = 63,//subtype > 0 + RALLY_FLAG = 64, + RANDOM_ART = 65, + RANDOM_TREASURE_ART = 66, + RANDOM_MINOR_ART = 67, + RANDOM_MAJOR_ART = 68, + RANDOM_RELIC_ART = 69, + RANDOM_HERO = 70, + RANDOM_MONSTER = 71, + RANDOM_MONSTER_L1 = 72, + RANDOM_MONSTER_L2 = 73, + RANDOM_MONSTER_L3 = 74, + RANDOM_MONSTER_L4 = 75, + RANDOM_RESOURCE = 76, + RANDOM_TOWN = 77, + REFUGEE_CAMP = 78, + RESOURCE = 79, + SANCTUARY = 80, + SCHOLAR = 81, + SEA_CHEST = 82, + SEER_HUT = 83, + CRYPT = 84, + SHIPWRECK = 85, + SHIPWRECK_SURVIVOR = 86, + SHIPYARD = 87, + SHRINE_OF_MAGIC_INCANTATION = 88, + SHRINE_OF_MAGIC_GESTURE = 89, + SHRINE_OF_MAGIC_THOUGHT = 90, + SIGN = 91, + SIRENS = 92, + SPELL_SCROLL = 93, + STABLES = 94, + TAVERN = 95, + TEMPLE = 96, + DEN_OF_THIEVES = 97, + TOWN = 98, + TRADING_POST [[deprecated]] = 99, + LEARNING_STONE = 100, + TREASURE_CHEST = 101, + TREE_OF_KNOWLEDGE = 102, + SUBTERRANEAN_GATE = 103, + UNIVERSITY [[deprecated]] = 104, + WAGON = 105, + WAR_MACHINE_FACTORY = 106, + SCHOOL_OF_WAR = 107, + WARRIORS_TOMB = 108, + WATER_WHEEL = 109, + WATERING_HOLE = 110, + WHIRLPOOL = 111, + WINDMILL = 112, + WITCH_HUT = 113, + HOLE = 124, + RANDOM_MONSTER_L5 = 162, + RANDOM_MONSTER_L6 = 163, + RANDOM_MONSTER_L7 = 164, + BORDER_GATE = 212, + FREELANCERS_GUILD [[deprecated]] = 213, + HERO_PLACEHOLDER = 214, + QUEST_GUARD = 215, + RANDOM_DWELLING = 216, + RANDOM_DWELLING_LVL = 217, //subtype = creature level + RANDOM_DWELLING_FACTION = 218, //subtype = faction + GARRISON2 = 219, + ABANDONED_MINE = 220, + TRADING_POST_SNOW [[deprecated]] = 221, + CLOVER_FIELD = 222, + CURSED_GROUND2 = 223, + EVIL_FOG = 224, + FAVORABLE_WINDS = 225, + FIERY_FIELDS = 226, + HOLY_GROUNDS = 227, + LUCID_POOLS = 228, + MAGIC_CLOUDS = 229, + MAGIC_PLAINS2 = 230, + ROCKLANDS = 231, + }; +}; + +class Obj : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class RoadIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NO_ROAD = 0, + FIRST_REGULAR_ROAD = 1, + DIRT_ROAD = 1, + GRAVEL_ROAD = 2, + COBBLESTONE_ROAD = 3, + ORIGINAL_ROAD_COUNT //+1 + }; +}; + +class RoadId : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class RiverIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NO_RIVER = 0, + FIRST_REGULAR_RIVER = 1, + WATER_RIVER = 1, + ICY_RIVER = 2, + MUD_RIVER = 3, + LAVA_RIVER = 4, + ORIGINAL_RIVER_COUNT //+1 + }; +}; + +class RiverId : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +using River = RiverId; +using Road = RoadId; + +class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO + }; +}; + +class EPathfindingLayer : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ArtifactPositionBase : public IdentifierBase +{ +public: + enum Type + { + TRANSITION_POS = -3, + FIRST_AVAILABLE = -2, + PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack + HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 + RIGHT_RING, LEFT_RING, FEET, //8 + MISC1, MISC2, MISC3, MISC4, //12 + MACH1, MACH2, MACH3, MACH4, //16 + SPELLBOOK, MISC5, //18 + AFTER_LAST, + //cres + CREATURE_SLOT = 0, + COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST, + + BACKPACK_START = 19 + }; + + static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class ArtifactPosition : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ArtifactIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + SPELLBOOK = 0, + SPELL_SCROLL = 1, + GRAIL = 2, + CATAPULT = 3, + BALLISTA = 4, + AMMO_CART = 5, + FIRST_AID_TENT = 6, + VIAL_OF_DRAGON_BLOOD = 127, + ARMAGEDDONS_BLADE = 128, + TITANS_THUNDER = 135, + ART_SELECTION = 144, + ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 + }; + + DLL_LINKAGE const CArtifact * toArtifact() const; + DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class ArtifactID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class CreatureIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + ARCHER = 2, // for debug / fallback + IMP = 42, // for Deity of Fire + SKELETON = 56, // for Skeleton Transformer + BONE_DRAGON = 68, // for Skeleton Transformer + TROGLODYTES = 70, // for Abandoned Mine + MEDUSA = 76, // for Siege UI workaround + HYDRA = 110, // for Skeleton Transformer + CHAOS_HYDRA = 111, // for Skeleton Transformer + AIR_ELEMENTAL = 112, // for tests + FIRE_ELEMENTAL = 114, // for tests + PSYCHIC_ELEMENTAL = 120, // for hardcoded ability + MAGIC_ELEMENTAL = 121, // for hardcoded ability + CATAPULT = 145, + BALLISTA = 146, + FIRST_AID_TENT = 147, + AMMO_CART = 148, + ARROW_TOWERS = 149 + }; + + DLL_LINKAGE const CCreature * toCreature() const; + DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class CreatureID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class SpellIDBase : public IdentifierBase +{ +public: + enum Type + { + SPELLBOOK_PRESET = -3, + PRESET = -2, + NONE = -1, + SUMMON_BOAT=0, SCUTTLE_BOAT=1, VISIONS=2, VIEW_EARTH=3, DISGUISE=4, VIEW_AIR=5, + FLY=6, WATER_WALK=7, DIMENSION_DOOR=8, TOWN_PORTAL=9, + + QUICKSAND=10, LAND_MINE=11, FORCE_FIELD=12, FIRE_WALL=13, EARTHQUAKE=14, + MAGIC_ARROW=15, ICE_BOLT=16, LIGHTNING_BOLT=17, IMPLOSION=18, + CHAIN_LIGHTNING=19, FROST_RING=20, FIREBALL=21, INFERNO=22, + METEOR_SHOWER=23, DEATH_RIPPLE=24, DESTROY_UNDEAD=25, ARMAGEDDON=26, + SHIELD=27, AIR_SHIELD=28, FIRE_SHIELD=29, PROTECTION_FROM_AIR=30, + PROTECTION_FROM_FIRE=31, PROTECTION_FROM_WATER=32, + PROTECTION_FROM_EARTH=33, ANTI_MAGIC=34, DISPEL=35, MAGIC_MIRROR=36, + CURE=37, RESURRECTION=38, ANIMATE_DEAD=39, SACRIFICE=40, BLESS=41, + CURSE=42, BLOODLUST=43, PRECISION=44, WEAKNESS=45, STONE_SKIN=46, + DISRUPTING_RAY=47, PRAYER=48, MIRTH=49, SORROW=50, FORTUNE=51, + MISFORTUNE=52, HASTE=53, SLOW=54, SLAYER=55, FRENZY=56, + TITANS_LIGHTNING_BOLT=57, COUNTERSTRIKE=58, BERSERK=59, HYPNOTIZE=60, + FORGETFULNESS=61, BLIND=62, TELEPORT=63, REMOVE_OBSTACLE=64, CLONE=65, + SUMMON_FIRE_ELEMENTAL=66, SUMMON_EARTH_ELEMENTAL=67, SUMMON_WATER_ELEMENTAL=68, SUMMON_AIR_ELEMENTAL=69, + + STONE_GAZE=70, POISON=71, BIND=72, DISEASE=73, PARALYZE=74, AGE=75, DEATH_CLOUD=76, THUNDERBOLT=77, + DISPEL_HELPFUL_SPELLS=78, DEATH_STARE=79, ACID_BREATH_DEFENSE=80, ACID_BREATH_DAMAGE=81, + + FIRST_NON_SPELL = 70, AFTER_LAST = 82 + }; + + DLL_LINKAGE const CSpell * toSpell() const; //deprecated + DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); +}; + +class SpellID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class BattleFieldInfo; +class BattleField : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const BattleField NONE; + DLL_LINKAGE const BattleFieldInfo * getInfo() const; +}; + +class BoatIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NONE = -1, + NECROPOLIS = 0, + CASTLE, + FORTRESS + }; +}; + +class BoatId : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class TerrainIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NATIVE_TERRAIN = -4, + ANY_TERRAIN = -3, + NONE = -1, + FIRST_REGULAR_TERRAIN = 0, + DIRT = 0, + SAND, + GRASS, + SNOW, + SWAMP, + ROUGH, + SUBTERRANEAN, + LAVA, + WATER, + ROCK, + ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK + }; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class TerrainId : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +using ETerrainId = TerrainId; + +class ObstacleInfo; +class Obstacle : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE const ObstacleInfo * getInfo() const; +}; + +class SpellSchoolBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + ANY = -1, + AIR = 0, + FIRE = 1, + WATER = 2, + EARTH = 3, + }; +}; + +class SpellSchool : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +using ESpellSchool = SpellSchool; + +class GameResIDBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, + COUNT, + + WOOD_AND_ORE = 127, // special case for town bonus resource + INVALID = -1 + }; +}; + +class GameResID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +using EGameResID = GameResID; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h new file mode 100644 index 000000000..778bd14ef --- /dev/null +++ b/lib/constants/Enumerations.h @@ -0,0 +1,251 @@ +/* + * GameConstants.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 + +enum class PrimarySkill : int8_t +{ + NONE = -1, + ATTACK, + DEFENSE, + SPELL_POWER, + KNOWLEDGE, + EXPERIENCE = 4 //for some reason changePrimSkill uses it +}; + +enum class EAlignment : int8_t +{ + GOOD, + EVIL, + NEUTRAL +}; + +namespace BuildingSubID +{ + enum EBuildingSubID + { + DEFAULT = -50, + NONE = -1, + STABLES, + BROTHERHOOD_OF_SWORD, + CASTLE_GATE, + CREATURE_TRANSFORMER, + MYSTIC_POND, + FOUNTAIN_OF_FORTUNE, + ARTIFACT_MERCHANT, + LOOKOUT_TOWER, + LIBRARY, + MANA_VORTEX, + PORTAL_OF_SUMMONING, + ESCAPE_TUNNEL, + FREELANCERS_GUILD, + BALLISTA_YARD, + ATTACK_VISITING_BONUS, + MAGIC_UNIVERSITY, + SPELL_POWER_GARRISON_BONUS, + ATTACK_GARRISON_BONUS, + DEFENSE_GARRISON_BONUS, + DEFENSE_VISITING_BONUS, + SPELL_POWER_VISITING_BONUS, + KNOWLEDGE_VISITING_BONUS, + EXPERIENCE_VISITING_BONUS, + LIGHTHOUSE, + TREASURY, + CUSTOM_VISITING_BONUS, + CUSTOM_VISITING_REWARD + }; +} + +enum class EMarketMode : int8_t +{ + RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, + ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, + MARTKET_AFTER_LAST_PLACEHOLDER +}; + +enum class EAiTactic : int8_t +{ + NONE = -1, + RANDOM, + WARRIOR, + BUILDER, + EXPLORER +}; + +enum class EBuildingState : int8_t +{ + HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, + NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED +}; + +enum class ESpellCastProblem : int8_t +{ + OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, + HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, + SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, + NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, + MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all + INVALID +}; + +namespace ECommander +{ + enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; + const int MAX_SKILL_LEVEL = 5; +} + +enum class EWallPart : int8_t +{ + INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, + KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, + PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ +}; + +enum class EWallState : int8_t +{ + NONE = -1, //no wall + DESTROYED, + DAMAGED, + INTACT, + REINFORCED, // walls in towns with castle +}; + +enum class EGateState : int8_t +{ + NONE, + CLOSED, + BLOCKED, // gate is blocked in closed state, e.g. by creature + OPENED, + DESTROYED +}; + +enum class ETileType : int8_t +{ + FREE, + POSSIBLE, + BLOCKED, + USED +}; + +enum class ETeleportChannelType : int8_t +{ + IMPASSABLE, + BIDIRECTIONAL, + UNIDIRECTIONAL, + MIXED +}; + +namespace SecSkillLevel +{ + enum SecSkillLevel + { + NONE, + BASIC, + ADVANCED, + EXPERT, + LEVELS_SIZE + }; +} + +enum class Date : int8_t +{ + DAY = 0, + DAY_OF_WEEK = 1, + WEEK = 2, + MONTH = 3, + DAY_OF_MONTH +}; + +enum class EActionType : int8_t +{ + NO_ACTION, + + END_TACTIC_PHASE, + RETREAT, + SURRENDER, + + HERO_SPELL, + + WALK, + WAIT, + DEFEND, + WALK_AND_ATTACK, + SHOOT, + CATAPULT, + MONSTER_SPELL, + BAD_MORALE, + STACK_HEAL, +}; + +enum class EDiggingStatus : int8_t +{ + UNKNOWN = -1, + CAN_DIG = 0, + LACK_OF_MOVEMENT, + WRONG_TERRAIN, + TILE_OCCUPIED, + BACKPACK_IS_FULL +}; + +enum class EPlayerStatus : int8_t +{ + WRONG = -1, + INGAME, + LOSER, + WINNER +}; + +enum class PlayerRelations : int8_t +{ + ENEMIES, + ALLIES, + SAME_PLAYER +}; + +enum class EMetaclass : int8_t +{ + INVALID = 0, + ARTIFACT, + CREATURE, + FACTION, + EXPERIENCE, + HERO, + HEROCLASS, + LUCK, + MANA, + MORALE, + MOVEMENT, + OBJECT, + PRIMARY_SKILL, + SECONDARY_SKILL, + SPELL, + RESOURCE +}; + +enum class EHealLevel: int8_t +{ + HEAL, + RESURRECT, + OVERHEAL +}; + +enum class EHealPower : int8_t +{ + ONE_BATTLE, + PERMANENT +}; + +enum class EBattleResult : int8_t +{ + NORMAL = 0, + ESCAPE = 1, + SURRENDER = 2, +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h new file mode 100644 index 000000000..7973ac21d --- /dev/null +++ b/lib/constants/NumericConstants.h @@ -0,0 +1,59 @@ +/* + * GameConstants.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 + +VCMI_LIB_NAMESPACE_BEGIN + +namespace GameConstants +{ + DLL_LINKAGE extern const std::string VCMI_VERSION; + + constexpr int PUZZLE_MAP_PIECES = 48; + + constexpr int MAX_HEROES_PER_PLAYER = 8; + constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; + + constexpr int ALL_PLAYERS = 255; //bitfield + + constexpr int CREATURES_PER_TOWN = 7; //without upgrades + constexpr int SPELL_LEVELS = 5; + constexpr int SPELL_SCHOOL_LEVELS = 4; + constexpr int DEFAULT_SCHOOLS = 4; + constexpr int CRE_LEVELS = 10; // number of creature experience levels + + constexpr int HERO_GOLD_COST = 2500; + constexpr int SPELLBOOK_GOLD_COST = 500; + constexpr int SKILL_GOLD_COST = 2000; + constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty + constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit + constexpr int ARMY_SIZE = 7; + constexpr int SKILL_PER_HERO = 8; + constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order + + constexpr int SKILL_QUANTITY=28; + constexpr int PRIMARY_SKILLS=4; + constexpr int RESOURCE_QUANTITY=8; + constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type + + // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase + constexpr int F_NUMBER = 9; + constexpr int ARTIFACTS_QUANTITY=171; + constexpr int HEROES_QUANTITY=156; + constexpr int SPELLS_QUANTITY=70; + constexpr int CREATURES_COUNT = 197; + + constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement + + constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits + + constexpr std::array POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h new file mode 100644 index 000000000..f2edac8cf --- /dev/null +++ b/lib/constants/StringConstants.h @@ -0,0 +1,217 @@ +/* + * constants/StringConstants.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 "GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// +/// String ID which are pointless to move to config file - these types are mostly hardcoded +/// +namespace GameConstants +{ + const std::string RESOURCE_NAMES [RESOURCE_QUANTITY] = { + "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" + }; + + const std::string PLAYER_COLOR_NAMES [PlayerColor::PLAYER_LIMIT_I] = { + "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" + }; + + const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; +} + +namespace NPrimarySkill +{ + const std::string names [GameConstants::PRIMARY_SKILLS] = { "attack", "defence", "spellpower", "knowledge" }; +} + +namespace NSecondarySkill +{ + const std::string names [GameConstants::SKILL_QUANTITY] = + { + "pathfinding", "archery", "logistics", "scouting", "diplomacy", // 5 + "navigation", "leadership", "wisdom", "mysticism", "luck", // 10 + "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", // 15 + "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", // 20 + "artillery", "learning", "offence", "armorer", "intelligence", // 25 + "sorcery", "resistance", "firstAid" + }; + + const std::vector levels = + { + "none", "basic", "advanced", "expert" + }; +} + +namespace EBuildingType +{ + const std::string names [44] = + { + "mageGuild1", "mageGuild2", "mageGuild3", "mageGuild4", "mageGuild5", // 5 + "tavern", "shipyard", "fort", "citadel", "castle", // 10 + "villageHall", "townHall", "cityHall", "capitol", "marketplace", // 15 + "resourceSilo", "blacksmith", "special1", "horde1", "horde1Upgr", // 20 + "ship", "special2", "special3", "special4", "horde2", // 25 + "horde2Upgr", "grail", "extraTownHall", "extraCityHall", "extraCapitol", // 30 + "dwellingLvl1", "dwellingLvl2", "dwellingLvl3", "dwellingLvl4", "dwellingLvl5", // 35 + "dwellingLvl6", "dwellingLvl7", "dwellingUpLvl1", "dwellingUpLvl2", "dwellingUpLvl3", // 40 + "dwellingUpLvl4", "dwellingUpLvl5", "dwellingUpLvl6", "dwellingUpLvl7" + }; +} + +namespace NFaction +{ + const std::string names [GameConstants::F_NUMBER] = + { + "castle", "rampart", "tower", + "inferno", "necropolis", "dungeon", + "stronghold", "fortress", "conflux" + }; +} + +namespace NArtifactPosition +{ + const std::string namesHero [19] = + { + "head", "shoulders", "neck", "rightHand", "leftHand", "torso", //5 + "rightRing", "leftRing", "feet", //8 + "misc1", "misc2", "misc3", "misc4", //12 + "mach1", "mach2", "mach3", "mach4", //16 + "spellbook", "misc5" //18 + }; + + const std::string namesCreature[1] = + { + "creature1" + }; + + const std::string namesCommander[6] = + { + "commander1", "commander2", "commander3", "commander4", "commander5", "commander6", + }; + + + const std::string backpack = "backpack"; +} + +namespace NMetaclass +{ + const std::string names [16] = + { + "", + "artifact", "creature", "faction", "experience", "hero", + "heroClass", "luck", "mana", "morale", "movement", + "object", "primarySkill", "secondarySkill", "spell", "resource" + }; +} + +namespace NPathfindingLayer +{ + const std::string names[EPathfindingLayer::NUM_LAYERS] = + { + "land", "sail", "water", "air" + }; +} + +namespace MappedKeys +{ + + static const std::map BUILDING_NAMES_TO_TYPES = + { + { "special1", BuildingID::SPECIAL_1 }, + { "special2", BuildingID::SPECIAL_2 }, + { "special3", BuildingID::SPECIAL_3 }, + { "special4", BuildingID::SPECIAL_4 }, + { "grail", BuildingID::GRAIL }, + { "mageGuild1", BuildingID::MAGES_GUILD_1 }, + { "mageGuild2", BuildingID::MAGES_GUILD_2 }, + { "mageGuild3", BuildingID::MAGES_GUILD_3 }, + { "mageGuild4", BuildingID::MAGES_GUILD_4 }, + { "mageGuild5", BuildingID::MAGES_GUILD_5 }, + { "tavern", BuildingID::TAVERN }, + { "shipyard", BuildingID::SHIPYARD }, + { "fort", BuildingID::FORT }, + { "citadel", BuildingID::CITADEL }, + { "castle", BuildingID::CASTLE }, + { "villageHall", BuildingID::VILLAGE_HALL }, + { "townHall", BuildingID::TOWN_HALL }, + { "cityHall", BuildingID::CITY_HALL }, + { "capitol", BuildingID::CAPITOL }, + { "marketplace", BuildingID::MARKETPLACE }, + { "resourceSilo", BuildingID::RESOURCE_SILO }, + { "blacksmith", BuildingID::BLACKSMITH }, + { "horde1", BuildingID::HORDE_1 }, + { "horde1Upgr", BuildingID::HORDE_1_UPGR }, + { "horde2", BuildingID::HORDE_2 }, + { "horde2Upgr", BuildingID::HORDE_2_UPGR }, + { "ship", BuildingID::SHIP }, + { "dwellingLvl1", BuildingID::DWELL_LVL_1 }, + { "dwellingLvl2", BuildingID::DWELL_LVL_2 }, + { "dwellingLvl3", BuildingID::DWELL_LVL_3 }, + { "dwellingLvl4", BuildingID::DWELL_LVL_4 }, + { "dwellingLvl5", BuildingID::DWELL_LVL_5 }, + { "dwellingLvl6", BuildingID::DWELL_LVL_6 }, + { "dwellingLvl7", BuildingID::DWELL_LVL_7 }, + { "dwellingUpLvl1", BuildingID::DWELL_LVL_1_UP }, + { "dwellingUpLvl2", BuildingID::DWELL_LVL_2_UP }, + { "dwellingUpLvl3", BuildingID::DWELL_LVL_3_UP }, + { "dwellingUpLvl4", BuildingID::DWELL_LVL_4_UP }, + { "dwellingUpLvl5", BuildingID::DWELL_LVL_5_UP }, + { "dwellingUpLvl6", BuildingID::DWELL_LVL_6_UP }, + { "dwellingUpLvl7", BuildingID::DWELL_LVL_7_UP }, + }; + + static const std::map SPECIAL_BUILDINGS = + { + { "mysticPond", BuildingSubID::MYSTIC_POND }, + { "artifactMerchant", BuildingSubID::ARTIFACT_MERCHANT }, + { "freelancersGuild", BuildingSubID::FREELANCERS_GUILD }, + { "magicUniversity", BuildingSubID::MAGIC_UNIVERSITY }, + { "castleGate", BuildingSubID::CASTLE_GATE }, + { "creatureTransformer", BuildingSubID::CREATURE_TRANSFORMER },//only skeleton transformer yet + { "portalOfSummoning", BuildingSubID::PORTAL_OF_SUMMONING }, + { "ballistaYard", BuildingSubID::BALLISTA_YARD }, + { "stables", BuildingSubID::STABLES }, + { "manaVortex", BuildingSubID::MANA_VORTEX }, + { "lookoutTower", BuildingSubID::LOOKOUT_TOWER }, + { "library", BuildingSubID::LIBRARY }, + { "brotherhoodOfSword", BuildingSubID::BROTHERHOOD_OF_SWORD },//morale garrison bonus + { "fountainOfFortune", BuildingSubID::FOUNTAIN_OF_FORTUNE },//luck garrison bonus + { "spellPowerGarrisonBonus", BuildingSubID::SPELL_POWER_GARRISON_BONUS },//such as 'stormclouds', but this name is not ok for good towns + { "attackGarrisonBonus", BuildingSubID::ATTACK_GARRISON_BONUS }, + { "defenseGarrisonBonus", BuildingSubID::DEFENSE_GARRISON_BONUS }, + { "escapeTunnel", BuildingSubID::ESCAPE_TUNNEL }, + { "attackVisitingBonus", BuildingSubID::ATTACK_VISITING_BONUS }, + { "defenceVisitingBonus", BuildingSubID::DEFENSE_VISITING_BONUS }, + { "spellPowerVisitingBonus", BuildingSubID::SPELL_POWER_VISITING_BONUS }, + { "knowledgeVisitingBonus", BuildingSubID::KNOWLEDGE_VISITING_BONUS }, + { "experienceVisitingBonus", BuildingSubID::EXPERIENCE_VISITING_BONUS }, + { "lighthouse", BuildingSubID::LIGHTHOUSE }, + { "treasury", BuildingSubID::TREASURY } + }; + + static const std::map MARKET_NAMES_TO_TYPES = + { + { "resource-resource", EMarketMode::RESOURCE_RESOURCE }, + { "resource-player", EMarketMode::RESOURCE_PLAYER }, + { "creature-resource", EMarketMode::CREATURE_RESOURCE }, + { "resource-artifact", EMarketMode::RESOURCE_ARTIFACT }, + { "artifact-resource", EMarketMode::ARTIFACT_RESOURCE }, + { "artifact-experience", EMarketMode::ARTIFACT_EXP }, + { "creature-experience", EMarketMode::CREATURE_EXP }, + { "creature-undead", EMarketMode::CREATURE_UNDEAD }, + { "resource-skill", EMarketMode::RESOURCE_SKILL }, + }; +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 05d9a6bb0..df831c513 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -107,7 +107,7 @@ static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & p HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); - if(ps.hero >= 0 && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero + if(ps.hero >= HeroTypeID(0) && !isUsedHero(HeroTypeID(ps.hero))) //we haven't used selected hero { return HeroTypeID(ps.hero); } @@ -777,7 +777,7 @@ void CGameState::placeStartingHeroes() continue; int heroTypeId = pickNextHeroType(playerColor); - if(playerSettingPair.second.hero == -1) + if(playerSettingPair.second.hero == HeroTypeID::NONE) playerSettingPair.second.hero = heroTypeId; placeStartingHero(playerColor, HeroTypeID(heroTypeId), playerInfo.posOfMainTown); @@ -2063,7 +2063,7 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow for(const auto & playerSettingPair : scenarioOps->playerInfos) //remove uninitialized yet heroes picked for start by other players { - if(playerSettingPair.second.hero != PlayerSettings::RANDOM) + if(playerSettingPair.second.hero.getNum() != PlayerSettings::RANDOM) ret -= HeroTypeID(playerSettingPair.second.hero); } diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 40c3af61e..dbb56c45d 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -12,6 +12,7 @@ #include "bonuses/CBonusSystemNode.h" #include "IGameCallback.h" #include "LoadProgress.h" +#include "ConstTransitivePtr.h" namespace boost { diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index c45beb135..fbcd70011 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -146,7 +146,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--) - checkAndRemoveArtifact(ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); + checkAndRemoveArtifact(ArtifactPosition::BACKPACK_START + slotNumber); } } diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index fd2e33b9b..ffa63e29b 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -14,7 +14,7 @@ #include "../filesystem/CBinaryReader.h" #include "../VCMI_Lib.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" #include "../GameSettings.h" #include "../JsonNode.h" diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 390e1f5d9..14aec1665 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -15,7 +15,7 @@ #include "../CTownHandler.h" #include "../IGameCallback.h" #include "../JsonRandom.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../VCMI_Lib.h" diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index a52d5940e..d7dd0a855 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -35,7 +35,7 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../modding/ModScope.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/Unit.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 5fbae6725..4b081c4e5 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -18,7 +18,7 @@ #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../NetPacks.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index f7bc248f2..2e64f438b 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -20,7 +20,7 @@ #include "../CSkillHandler.h" #include "../StartInfo.h" #include "../IGameCallback.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 3ad5f2948..cf2373dfb 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -364,7 +364,7 @@ void CGTownInstance::initOverriddenBids() } } -bool CGTownInstance::isBonusingBuildingAdded(BuildingID::EBuildingID bid) const +bool CGTownInstance::isBonusingBuildingAdded(BuildingID bid) const { auto present = std::find_if(bonusingBuildings.begin(), bonusingBuildings.end(), [&](CGTownBuilding* building) { @@ -428,7 +428,7 @@ DamageRange CGTownInstance::getKeepDamageRange() const }; } -void CGTownInstance::deleteTownBonus(BuildingID::EBuildingID bid) +void CGTownInstance::deleteTownBonus(BuildingID bid) { size_t i = 0; CGTownBuilding * freeIt = nullptr; @@ -467,7 +467,7 @@ void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structu for (int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++) { - BuildingID buildID = BuildingID(BuildingID::DWELL_FIRST).advance(level); + BuildingID buildID = BuildingID(BuildingID::DWELL_FIRST + level); int upgradeNum = 0; for (; town->buildings.count(buildID); upgradeNum++, buildID.advance(GameConstants::CREATURES_PER_TOWN)) diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 6f8298143..6f47cf525 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -175,7 +175,7 @@ public: void removeCapitols(const PlayerColor & owner) const; void clearArmy() const; void addHeroToStructureVisitors(const CGHeroInstance *h, si64 structureInstanceID) const; //hero must be visiting or garrisoned in town - void deleteTownBonus(BuildingID::EBuildingID bid); + void deleteTownBonus(BuildingID bid); /// Returns damage range for secondary towers of this town DamageRange getTowerDamageRange() const; @@ -220,7 +220,7 @@ private: void onTownCaptured(const PlayerColor & winner) const; int getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const; bool townEnvisagesBuilding(BuildingSubID::EBuildingSubID bid) const; - bool isBonusingBuildingAdded(BuildingID::EBuildingID bid) const; + bool isBonusingBuildingAdded(BuildingID bid) const; void initOverriddenBids(); void addTownBonuses(CRandomGenerator & rand); }; diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 90eb106b2..9e73793ca 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -23,7 +23,7 @@ #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../serializer/JsonSerializeFormat.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" #include "../modding/ModScope.h" @@ -814,7 +814,7 @@ void CGSeerHut::finishQuest(const CGHeroInstance * h, ui32 accept) const for(const auto & ci : parts) { if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, GameConstants::BACKPACK_START); + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); } } } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 0cc036acd..2779765d9 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -11,7 +11,7 @@ #include "StdInc.h" #include "MiscObjects.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" @@ -1291,7 +1291,7 @@ CGBoat::CGBoat() { hero = nullptr; direction = 4; - layer = EPathfindingLayer::EEPathfindingLayer::SAIL; + layer = EPathfindingLayer::SAIL; } bool CGBoat::isCoastVisitable() const diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index d681fe605..f159f418d 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -351,7 +351,7 @@ public: const CGHeroInstance *hero; //hero on board bool onboardAssaultAllowed; //if true, hero can attack units from transport bool onboardVisitAllowed; //if true, hero can visit objects from transport - EPathfindingLayer::EEPathfindingLayer layer; + EPathfindingLayer layer; //animation filenames. If empty - animations won't be used std::string actualAnimation; //for OH3 boats those have actual animations diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index ca1059d9c..675f98486 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -14,7 +14,7 @@ #include "../filesystem/CBinaryReader.h" #include "../VCMI_Lib.h" #include "../GameConstants.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../CGeneralTextHandler.h" #include "../JsonNode.h" #include "../TerrainHandler.h" diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 57fb49a9e..cd8d341a6 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -898,7 +898,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) int amount = reader->readUInt16(); for(int i = 0; i < amount; ++i) { - loadArtifactToSlot(hero, GameConstants::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); + loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); } } @@ -917,7 +917,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) return false; } - if(art->isBig() && slot >= GameConstants::BACKPACK_START) + if(art->isBig() && slot >= ArtifactPosition::BACKPACK_START) { logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName); return false; @@ -1652,7 +1652,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(auto & elem : map->disposedHeroes) { - if(elem.heroId == object->subID) + if(elem.heroId.getNum() == object->subID) { object->nameCustom = elem.name; object->portrait = elem.portrait; diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 6908a7511..e8bd5fbe1 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -32,7 +32,7 @@ #include "../modding/ModUtility.h" #include "../spells/CSpellHandler.h" #include "../CSkillHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index bed4f0805..a26ab4248 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -22,7 +22,7 @@ #include "../GameSettings.h" #include "../Languages.h" #include "../ScriptHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" #include "../spells/CSpellHandler.h" diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index ed2667e7e..afcfd0ff9 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -29,7 +29,7 @@ #include "../RiverHandler.h" #include "../RoadHandler.h" #include "../ScriptHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../rmg/CRmgTemplateStorage.h" diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index 0121c8448..49efd7833 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -22,7 +22,7 @@ namespace PathfinderUtil using FoW = std::shared_ptr>; using ELayer = EPathfindingLayer; - template + template EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, FoW fow, const PlayerColor player, const CGameState * gs) { if(!(*fow)[pos.z][pos.x][pos.y]) diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 69527411f..d526d17c5 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -19,7 +19,7 @@ #include "../mapping/CMapEditManager.h" #include "../CTownHandler.h" #include "../CHeroHandler.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" #include "CZonePlacer.h" #include "TileInfo.h" diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index b8032bc01..71e00fec7 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -19,7 +19,7 @@ #include "../TerrainHandler.h" #include "../serializer/JsonSerializeFormat.h" #include "../modding/ModScope.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 086eb0d04..f09baeffb 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -60,7 +60,7 @@ class DLL_LINKAGE CSerializer } template - static NT idToNumber(const BaseForID &t) + static NT idToNumber(const IdentifierBase &t) { return t.getNum(); } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index acfc219b3..f048d36c5 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -20,7 +20,7 @@ #include "../CGeneralTextHandler.h" #include "../filesystem/Filesystem.h" -#include "../StringConstants.h" +#include "../constants/StringConstants.h" #include "../battle/BattleInfo.h" #include "../battle/CBattleInfoCallback.h" diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index f9e2ddcb1..a21392983 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -15,7 +15,7 @@ #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/CHeroHandler.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" QuestWidget::QuestWidget(const CMap & _map, CGSeerHut & _sh, QWidget *parent) : diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index f131ae6a9..bcc974112 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -15,7 +15,7 @@ #include "../lib/spells/CSpellHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" RewardsWidget::RewardsWidget(const CMap & m, CGPandoraBox & p, QWidget *parent) : diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp index 23c75beb9..29ee4e500 100644 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -130,7 +130,7 @@ QStandardItem * TownBulidingsWidget::addBuilding(const CTown & ctown, int bId, s for(int i = 0; i < model.rowCount(pindex); ++i) { QModelIndex index = model.index(i, 0, pindex); - if(building->upgrade == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) + if(building->upgrade.getNum() == model.itemFromIndex(index)->data(Qt::UserRole).toInt()) { parent = model.itemFromIndex(index); break; diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp index ca42a447e..66239ff3c 100644 --- a/mapeditor/mapsettings.cpp +++ b/mapeditor/mapsettings.cpp @@ -22,7 +22,7 @@ #include "../lib/mapping/CMapService.h" #include "../lib/modding/CModHandler.h" #include "../lib/modding/CModInfo.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "inspector/townbulidingswidget.h" //to convert BuildingID to string //parses date for lose condition (1m 1w 1d) diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index ebf29c9b2..9734f8f68 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -12,7 +12,7 @@ #include "playerparams.h" #include "ui_playerparams.h" #include "../lib/CTownHandler.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7ed103c5e..74f426a4d 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2676,7 +2676,7 @@ bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocat { if(!ArtifactUtils::isBackpackFreeSlots(dst.getHolderArtSet())) COMPLAIN_RET("Backpack is full!"); - vstd::amin(dst.slot, GameConstants::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size()); + vstd::amin(dst.slot, ArtifactPosition::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size()); } if(!(src.slot == ArtifactPosition::TRANSITION_POS && dst.slot == ArtifactPosition::TRANSITION_POS)) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index ad2738c0b..5b3eecdef 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -727,7 +727,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; pset.heroName = pinfo.mainCustomHeroName; @@ -876,7 +876,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) } } - if(s.hero >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + if(s.hero.getNum() >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { s.hero = PlayerSettings::RANDOM; } @@ -935,10 +935,10 @@ void CVCMIServer::setCampaignBonus(int bonusId) void CVCMIServer::optionNextHero(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle.getNum() < 0 || s.hero == PlayerSettings::NONE) + if(s.castle.getNum() < 0 || s.hero.getNum() == PlayerSettings::NONE) return; - if(s.hero == PlayerSettings::RANDOM) // first/last available + if(s.hero.getNum() == PlayerSettings::RANDOM) // first/last available { int max = static_cast(VLC->heroh->size()), min = 0; @@ -956,7 +956,7 @@ void CVCMIServer::optionNextHero(PlayerColor player, int dir) void CVCMIServer::optionSetHero(PlayerColor player, int id) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle.getNum() < 0 || s.hero == PlayerSettings::NONE) + if(s.castle.getNum() < 0 || s.hero.getNum() == PlayerSettings::NONE) return; if(id == PlayerSettings::RANDOM) @@ -967,19 +967,19 @@ void CVCMIServer::optionSetHero(PlayerColor player, int id) s.hero = static_cast(id); } -int CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) +HeroTypeID CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) { if(dir > 0) { for(int i = min + incl; i <= max - incl; i++) if(canUseThisHero(player, i)) - return i; + return HeroTypeID(i); } else { for(int i = max - incl; i >= min + incl; i--) if(canUseThisHero(player, i)) - return i; + return HeroTypeID(i); } return -1; } @@ -989,7 +989,7 @@ void CVCMIServer::optionNextBonus(PlayerColor player, int dir) PlayerSettings & s = si->playerInfos[player]; PlayerSettings::Ebonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); - if(s.hero == PlayerSettings::NONE && + if(s.hero.getNum() == PlayerSettings::NONE && !getPlayerInfo(player.getNum()).heroesNames.size() && ret == PlayerSettings::ARTIFACT) //no hero - can't be artifact { @@ -1017,7 +1017,7 @@ void CVCMIServer::optionSetBonus(PlayerColor player, int id) { PlayerSettings & s = si->playerInfos[player]; - if(s.hero == PlayerSettings::NONE && + if(s.hero.getNum() == PlayerSettings::NONE && !getPlayerInfo(player.getNum()).heroesNames.size() && id == PlayerSettings::ARTIFACT) //no hero - can't be artifact return; @@ -1051,7 +1051,7 @@ std::vector CVCMIServer::getUsedHeroes() if(hero.heroId >= 0) //in VCMI map format heroId = -1 means random hero heroIds.push_back(hero.heroId); - if(p.second.hero != PlayerSettings::RANDOM) + if(p.second.hero.getNum() != PlayerSettings::RANDOM) heroIds.push_back(p.second.hero); } return heroIds; diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index beb3be62b..450fe3732 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -108,7 +108,7 @@ public: void setPlayer(PlayerColor clickedColor); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 void optionSetHero(PlayerColor player, int id); - int nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); + HeroTypeID nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); bool canUseThisHero(PlayerColor player, int ID); std::vector getUsedHeroes(); void optionNextBonus(PlayerColor player, int dir); //dir == -1 or +1 diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 59860edf5..6bf6e6fcb 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -265,7 +265,7 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed - if (battleResult->queryID == -1) + if (battleResult->queryID == QueryID::NONE) endBattleConfirm(gameHandler->gameState()->curB); } @@ -334,7 +334,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) //we assume that no big artifacts can be found MoveArtifact ma; ma.src = ArtifactLocation(finishingBattle->loserHero, - ArtifactPosition(GameConstants::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning + ArtifactPosition(ArtifactPosition::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning const CArtifactInstance * art = ma.src.getArt(); if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won { diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index 0d105ca37..9eddbccdc 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -21,6 +21,7 @@ VCMI_LIB_NAMESPACE_END class CObjectVisitQuery; class QueriesProcessor; class CQuery; +class CGameHandler; using QueryPtr = std::shared_ptr; diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 32f0cd176..4b41d2e3e 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -167,7 +167,7 @@ public: pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; pset.heroName = pinfo.mainCustomHeroName; From c2d5f7f22ff732c1c6a447e0831f1418a20183f0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 01:26:10 +0300 Subject: [PATCH 0187/1248] Formatting & cleanup --- lib/constants/EntityIdentifiers.h | 155 +++++++++++++++++++----------- 1 file changed, 100 insertions(+), 55 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 759934307..921ac83f9 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -366,44 +366,11 @@ public: DWELL_UP2_FIRST = DWELL_LVL_7_UP + 1, - //Special buildings for towns. - LIGHTHOUSE = SPECIAL_1, - STABLES = SPECIAL_2, //Castle - BROTHERHOOD = SPECIAL_3, - - MYSTIC_POND = SPECIAL_1, - FOUNTAIN_OF_FORTUNE = SPECIAL_2, //Rampart - TREASURY = SPECIAL_3, - - ARTIFACT_MERCHANT = SPECIAL_1, - LOOKOUT_TOWER = SPECIAL_2, //Tower - LIBRARY = SPECIAL_3, - WALL_OF_KNOWLEDGE = SPECIAL_4, - - STORMCLOUDS = SPECIAL_2, +// //Special buildings for towns. CASTLE_GATE = SPECIAL_3, //Inferno - ORDER_OF_FIRE = SPECIAL_4, - - COVER_OF_DARKNESS = SPECIAL_1, - NECROMANCY_AMPLIFIER = SPECIAL_2, //Necropolis - SKELETON_TRANSFORMER = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MANA_VORTEX = SPECIAL_2, - PORTAL_OF_SUMMON = SPECIAL_3, //Dungeon - BATTLE_ACADEMY = SPECIAL_4, - - ESCAPE_TUNNEL = SPECIAL_1, FREELANCERS_GUILD = SPECIAL_2, //Stronghold - BALLISTA_YARD = SPECIAL_3, - HALL_OF_VALHALLA = SPECIAL_4, + ARTIFACT_MERCHANT = SPECIAL_1, - CAGE_OF_WARLORDS = SPECIAL_1, - GLYPHS_OF_FEAR = SPECIAL_2, // Fortress - BLOOD_OBELISK = SPECIAL_3, - - //ARTIFACT_MERCHANT - same ID as in tower - MAGIC_UNIVERSITY = SPECIAL_2, // Conflux }; bool IsSpecialOrGrail() const @@ -739,31 +706,102 @@ class SpellIDBase : public IdentifierBase public: enum Type { + // Special ID's SPELLBOOK_PRESET = -3, PRESET = -2, NONE = -1, - SUMMON_BOAT=0, SCUTTLE_BOAT=1, VISIONS=2, VIEW_EARTH=3, DISGUISE=4, VIEW_AIR=5, - FLY=6, WATER_WALK=7, DIMENSION_DOOR=8, TOWN_PORTAL=9, - QUICKSAND=10, LAND_MINE=11, FORCE_FIELD=12, FIRE_WALL=13, EARTHQUAKE=14, - MAGIC_ARROW=15, ICE_BOLT=16, LIGHTNING_BOLT=17, IMPLOSION=18, - CHAIN_LIGHTNING=19, FROST_RING=20, FIREBALL=21, INFERNO=22, - METEOR_SHOWER=23, DEATH_RIPPLE=24, DESTROY_UNDEAD=25, ARMAGEDDON=26, - SHIELD=27, AIR_SHIELD=28, FIRE_SHIELD=29, PROTECTION_FROM_AIR=30, - PROTECTION_FROM_FIRE=31, PROTECTION_FROM_WATER=32, - PROTECTION_FROM_EARTH=33, ANTI_MAGIC=34, DISPEL=35, MAGIC_MIRROR=36, - CURE=37, RESURRECTION=38, ANIMATE_DEAD=39, SACRIFICE=40, BLESS=41, - CURSE=42, BLOODLUST=43, PRECISION=44, WEAKNESS=45, STONE_SKIN=46, - DISRUPTING_RAY=47, PRAYER=48, MIRTH=49, SORROW=50, FORTUNE=51, - MISFORTUNE=52, HASTE=53, SLOW=54, SLAYER=55, FRENZY=56, - TITANS_LIGHTNING_BOLT=57, COUNTERSTRIKE=58, BERSERK=59, HYPNOTIZE=60, - FORGETFULNESS=61, BLIND=62, TELEPORT=63, REMOVE_OBSTACLE=64, CLONE=65, - SUMMON_FIRE_ELEMENTAL=66, SUMMON_EARTH_ELEMENTAL=67, SUMMON_WATER_ELEMENTAL=68, SUMMON_AIR_ELEMENTAL=69, + // Adventure map spells + SUMMON_BOAT = 0, + SCUTTLE_BOAT = 1, + VISIONS = 2, + VIEW_EARTH = 3, + DISGUISE = 4, + VIEW_AIR = 5, + FLY = 6, + WATER_WALK = 7, + DIMENSION_DOOR = 8, + TOWN_PORTAL = 9, - STONE_GAZE=70, POISON=71, BIND=72, DISEASE=73, PARALYZE=74, AGE=75, DEATH_CLOUD=76, THUNDERBOLT=77, - DISPEL_HELPFUL_SPELLS=78, DEATH_STARE=79, ACID_BREATH_DEFENSE=80, ACID_BREATH_DAMAGE=81, + // Combar spells + QUICKSAND = 10, + LAND_MINE = 11, + FORCE_FIELD = 12, + FIRE_WALL = 13, + EARTHQUAKE = 14, + MAGIC_ARROW = 15, + ICE_BOLT = 16, + LIGHTNING_BOLT = 17, + IMPLOSION = 18, + CHAIN_LIGHTNING = 19, + FROST_RING = 20, + FIREBALL = 21, + INFERNO = 22, + METEOR_SHOWER = 23, + DEATH_RIPPLE = 24, + DESTROY_UNDEAD = 25, + ARMAGEDDON = 26, + SHIELD = 27, + AIR_SHIELD = 28, + FIRE_SHIELD = 29, + PROTECTION_FROM_AIR = 30, + PROTECTION_FROM_FIRE = 31, + PROTECTION_FROM_WATER = 32, + PROTECTION_FROM_EARTH = 33, + ANTI_MAGIC = 34, + DISPEL = 35, + MAGIC_MIRROR = 36, + CURE = 37, + RESURRECTION = 38, + ANIMATE_DEAD = 39, + SACRIFICE = 40, + BLESS = 41, + CURSE = 42, + BLOODLUST = 43, + PRECISION = 44, + WEAKNESS = 45, + STONE_SKIN = 46, + DISRUPTING_RAY = 47, + PRAYER = 48, + MIRTH = 49, + SORROW = 50, + FORTUNE = 51, + MISFORTUNE = 52, + HASTE = 53, + SLOW = 54, + SLAYER = 55, + FRENZY = 56, + TITANS_LIGHTNING_BOLT = 57, + COUNTERSTRIKE = 58, + BERSERK = 59, + HYPNOTIZE = 60, + FORGETFULNESS = 61, + BLIND = 62, + TELEPORT = 63, + REMOVE_OBSTACLE = 64, + CLONE = 65, + SUMMON_FIRE_ELEMENTAL = 66, + SUMMON_EARTH_ELEMENTAL = 67, + SUMMON_WATER_ELEMENTAL = 68, + SUMMON_AIR_ELEMENTAL = 69, - FIRST_NON_SPELL = 70, AFTER_LAST = 82 + // Creature abilities + STONE_GAZE = 70, + POISON = 71, + BIND = 72, + DISEASE = 73, + PARALYZE = 74, + AGE = 75, + DEATH_CLOUD = 76, + THUNDERBOLT = 77, + DISPEL_HELPFUL_SPELLS = 78, + DEATH_STARE = 79, + ACID_BREATH_DEFENSE = 80, + ACID_BREATH_DAMAGE = 81, + + // Special ID's + FIRST_NON_SPELL = 70, + AFTER_LAST = 82 }; DLL_LINKAGE const CSpell * toSpell() const; //deprecated @@ -877,7 +915,14 @@ class GameResIDBase : public IdentifierBase public: enum Type : int32_t { - WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, + WOOD = 0, + MERCURY, + ORE, + SULFUR, + CRYSTAL, + GEMS, + GOLD, + MITHRIL, COUNT, WOOD_AND_ORE = 127, // special case for town bonus resource From e2718db7912bbafae7c808cdbe2894edcc852616 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 18:16:00 +0300 Subject: [PATCH 0188/1248] Converted several enumerations into constants --- client/battle/BattleSiegeController.cpp | 12 +- cmake_modules/VCMI_lib.cmake | 3 +- lib/CCreatureSet.h | 2 +- lib/CTownHandler.cpp | 2 +- .../EntityIdentifiers.cpp} | 109 +++--- lib/constants/EntityIdentifiers.h | 328 ++++++++---------- lib/constants/Enumerations.h | 2 +- lib/constants/NumericConstants.h | 2 +- lib/mapping/MapFeaturesH3M.cpp | 2 + lib/mapping/MapFeaturesH3M.h | 2 + lib/mapping/MapReaderH3M.cpp | 4 +- 11 files changed, 211 insertions(+), 257 deletions(-) rename lib/{GameConstants.cpp => constants/EntityIdentifiers.cpp} (71%) diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 63d5bd9e5..63c8cab10 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -65,16 +65,12 @@ std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisua { case EWallVisual::BACKGROUND_WALL: { - switch(town->town->faction->getIndex()) - { - case ETownType::RAMPART: - case ETownType::NECROPOLIS: - case ETownType::DUNGEON: - case ETownType::STRONGHOLD: + auto faction = town->town->faction->getIndex(); + + if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) return prefix + "TPW1.BMP"; - default: + else return prefix + "TPWL.BMP"; - } } case EWallVisual::KEEP: return prefix + "MAN" + addit + ".BMP"; diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 04c94015b..ddbd7815b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -42,6 +42,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignHandler.cpp ${MAIN_LIB_DIR}/campaign/CampaignState.cpp + ${MAIN_LIB_DIR}/constants/EntityIdentifiers.cpp + ${MAIN_LIB_DIR}/events/ApplyDamage.cpp ${MAIN_LIB_DIR}/events/GameResumed.cpp ${MAIN_LIB_DIR}/events/ObjectVisitEnded.cpp @@ -246,7 +248,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/CStack.cpp ${MAIN_LIB_DIR}/CThreadHelper.cpp ${MAIN_LIB_DIR}/CTownHandler.cpp - ${MAIN_LIB_DIR}/GameConstants.cpp ${MAIN_LIB_DIR}/GameSettings.cpp ${MAIN_LIB_DIR}/IGameCallback.cpp ${MAIN_LIB_DIR}/IHandlerBase.cpp diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 4ef0e7ea7..c103a6af7 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -55,7 +55,7 @@ public: CreatureID idNumber; h & idNumber; if(idNumber != CreatureID::NONE) - setType(dynamic_cast(VLC->creatures()->getByIndex(idNumber))); + setType(dynamic_cast(VLC->creatures()->getById(idNumber))); else type = nullptr; } diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index ac01e787a..cb19a3983 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -186,7 +186,7 @@ EAlignment CFaction::getAlignment() const BoatId CFaction::getBoatType() const { - return boatType.toEnum(); + return boatType; } TerrainId CFaction::getNativeTerrain() const diff --git a/lib/GameConstants.cpp b/lib/constants/EntityIdentifiers.cpp similarity index 71% rename from lib/GameConstants.cpp rename to lib/constants/EntityIdentifiers.cpp index 1b528ec7e..37815f8ab 100644 --- a/lib/GameConstants.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -1,5 +1,5 @@ /* - * GameConstants.cpp, part of VCMI engine + * EntityIdentifiers.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,8 +8,6 @@ * */ -#define INSTANTIATE_BASE_FOR_ID_HERE - #include "StdInc.h" #ifndef VCMI_NO_EXTRA_VERSION @@ -56,6 +54,44 @@ const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255); const PlayerColor PlayerColor::PLAYER_LIMIT = PlayerColor(PLAYER_LIMIT_I); const TeamID TeamID::NO_TEAM = TeamID(255); +const SpellSchool SpellSchool::ANY = -1; +const SpellSchool SpellSchool::AIR = 0; +const SpellSchool SpellSchool::FIRE = 1; +const SpellSchool SpellSchool::WATER = 2; +const SpellSchool SpellSchool::EARTH = 3; + +const FactionID FactionID::NONE = -2; +const FactionID FactionID::DEFAULT = -1; +const FactionID FactionID::RANDOM = -1; +const FactionID FactionID::ANY = -1; +const FactionID FactionID::CASTLE = 0; +const FactionID FactionID::RAMPART = 1; +const FactionID FactionID::TOWER = 2; +const FactionID FactionID::INFERNO = 3; +const FactionID FactionID::NECROPOLIS = 4; +const FactionID FactionID::DUNGEON = 5; +const FactionID FactionID::STRONGHOLD = 6; +const FactionID FactionID::FORTRESS = 7; +const FactionID FactionID::CONFLUX = 8; +const FactionID FactionID::NEUTRAL = 9; + +const BoatId BoatId::NONE = -1; +const BoatId BoatId::NECROPOLIS = 0; +const BoatId BoatId::CASTLE = 1; +const BoatId BoatId::FORTRESS = 2; + +const RiverId RiverId::NO_RIVER = 0; +const RiverId RiverId::WATER_RIVER = 1; +const RiverId RiverId::ICY_RIVER = 2; +const RiverId RiverId::MUD_RIVER = 3; +const RiverId RiverId::LAVA_RIVER = 4; + +const RoadId RoadId::NO_ROAD = 0; +const RoadId RoadId::DIRT_ROAD = 1; +const RoadId RoadId::GRAVEL_ROAD = 2; +const RoadId RoadId::COBBLESTONE_ROAD = 3; + + namespace GameConstants { #ifdef VCMI_NO_EXTRA_VERSION @@ -192,7 +228,7 @@ std::string PlayerColor::getStrCap(bool L10n) const return ret; } -si32 FactionIDBase::decode(const std::string & identifier) +si32 FactionID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) @@ -201,17 +237,16 @@ si32 FactionIDBase::decode(const std::string & identifier) return FactionID::DEFAULT; } -std::string FactionIDBase::encode(const si32 index) +std::string FactionID::encode(const si32 index) { return VLC->factions()->getByIndex(index)->getJsonKey(); } -std::string FactionIDBase::entityType() +std::string FactionID::entityType() { return "faction"; } - si32 TerrainIdBase::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); @@ -231,68 +266,8 @@ std::string TerrainIdBase::entityType() return "terrain"; } -std::ostream & operator<<(std::ostream & os, const EActionType actionType) -{ - static const std::map actionTypeToString = - { - {EActionType::END_TACTIC_PHASE, "End tactic phase"}, - {EActionType::NO_ACTION, "No action"}, - {EActionType::HERO_SPELL, "Hero spell"}, - {EActionType::WALK, "Walk"}, - {EActionType::DEFEND, "Defend"}, - {EActionType::RETREAT, "Retreat"}, - {EActionType::SURRENDER, "Surrender"}, - {EActionType::WALK_AND_ATTACK, "Walk and attack"}, - {EActionType::SHOOT, "Shoot"}, - {EActionType::WAIT, "Wait"}, - {EActionType::CATAPULT, "Catapult"}, - {EActionType::MONSTER_SPELL, "Monster spell"}, - {EActionType::BAD_MORALE, "Bad morale"}, - {EActionType::STACK_HEAL, "Stack heal"}, - }; - - auto it = actionTypeToString.find(actionType); - if (it == actionTypeToString.end()) return os << ""; - else return os << it->second; -} - -std::ostream & operator<<(std::ostream & os, const EPathfindingLayer & pathfindingLayer) -{ - static const std::map pathfinderLayerToString - { - #define DEFINE_ELEMENT(element) {EPathfindingLayer::element, #element} - DEFINE_ELEMENT(WRONG), - DEFINE_ELEMENT(AUTO), - DEFINE_ELEMENT(LAND), - DEFINE_ELEMENT(SAIL), - DEFINE_ELEMENT(WATER), - DEFINE_ELEMENT(AIR), - DEFINE_ELEMENT(NUM_LAYERS) - #undef DEFINE_ELEMENT - }; - - auto it = pathfinderLayerToString.find(pathfindingLayer.num); - if (it == pathfinderLayerToString.end()) return os << ""; - else return os << it->second; -} - const BattleField BattleField::NONE; -bool operator==(const BattleField & l, const BattleField & r) -{ - return l.num == r.num; -} - -bool operator!=(const BattleField & l, const BattleField & r) -{ - return l.num != r.num; -} - -bool operator<(const BattleField & l, const BattleField & r) -{ - return l.num < r.num; -} - const BattleFieldInfo * BattleField::getInfo() const { return VLC->battlefields()->getById(*this); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 921ac83f9..dba54dc5c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -1,5 +1,5 @@ /* - * GameConstants.h, part of VCMI engine + * EntityIdentifiers.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -41,6 +41,8 @@ protected: constexpr IdentifierBase(int32_t value = -1 ): num(value) {} + + ~IdentifierBase() = default; public: int32_t num; @@ -57,6 +59,16 @@ public: } }; + constexpr void advance(int change) + { + num += change; + } + + constexpr operator int32_t () const + { + return num; + } + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) { return os << dt.num; @@ -69,7 +81,7 @@ public: template class Identifier : public IdentifierBase { - using T = IdentifierBase; + using BaseClass = IdentifierBase; public: constexpr Identifier(int32_t _num = -1) :IdentifierBase(_num) @@ -77,110 +89,90 @@ public: template void serialize(Handler &h, const int version) { - h & T::num; + h & BaseClass::num; } - constexpr bool operator == (const Identifier & b) const { return T::num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return T::num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return T::num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return T::num != b.num; } - constexpr bool operator < (const Identifier & b) const { return T::num < b.num; } - constexpr bool operator > (const Identifier & b) const { return T::num > b.num; } + constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } constexpr FinalClass & operator++() { - ++T::num; + ++BaseClass::num; return static_cast(*this); } constexpr FinalClass operator++(int) { FinalClass ret(num); - ++T::num; + ++BaseClass::num; return ret; } - - constexpr operator int32_t () const - { - return T::num; - } - - constexpr void advance(int change) - { - T::num += change; - } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -template -class IdentifierWithEnum : public T +template +class IdentifierWithEnum : public BaseClass { - using EnumType = typename T::Type; + using EnumType = typename BaseClass::Type; static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); public: constexpr int32_t getNum() const { - return T::num; + return BaseClass::num; } constexpr EnumType toEnum() const { - return static_cast(T::num); + return static_cast(BaseClass::num); } template void serialize(Handler &h, const int version) { - h & T::num; + h & BaseClass::num; } constexpr IdentifierWithEnum(const EnumType & enumValue) { - T::num = static_cast(enumValue); + BaseClass::num = static_cast(enumValue); } constexpr IdentifierWithEnum(int32_t _num = -1) { - T::num = _num; + BaseClass::num = _num; } - constexpr void advance(int change) + constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() { - T::num += change; + ++BaseClass::num; + return static_cast(*this); } - constexpr bool operator == (const EnumType & b) const { return T::num == static_cast(b); } - constexpr bool operator <= (const EnumType & b) const { return T::num <= static_cast(b); } - constexpr bool operator >= (const EnumType & b) const { return T::num >= static_cast(b); } - constexpr bool operator != (const EnumType & b) const { return T::num != static_cast(b); } - constexpr bool operator < (const EnumType & b) const { return T::num < static_cast(b); } - constexpr bool operator > (const EnumType & b) const { return T::num > static_cast(b); } - - constexpr bool operator == (const IdentifierWithEnum & b) const { return T::num == b.num; } - constexpr bool operator <= (const IdentifierWithEnum & b) const { return T::num <= b.num; } - constexpr bool operator >= (const IdentifierWithEnum & b) const { return T::num >= b.num; } - constexpr bool operator != (const IdentifierWithEnum & b) const { return T::num != b.num; } - constexpr bool operator < (const IdentifierWithEnum & b) const { return T::num < b.num; } - constexpr bool operator > (const IdentifierWithEnum & b) const { return T::num > b.num; } - - constexpr IdentifierWithEnum & operator++() + constexpr FinalClass operator++(int) { - ++T::num; - return *this; - } - - constexpr IdentifierWithEnum operator++(int) - { - IdentifierWithEnum ret(*this); - ++T::num; + FinalClass ret(BaseClass::num); + ++BaseClass::num; return ret; } - - constexpr operator int32_t () const - { - return T::num; - } }; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -284,54 +276,70 @@ public: { WRONG = -2, DEFAULT = -1, - PATHFINDING = 0, ARCHERY, LOGISTICS, SCOUTING, DIPLOMACY, NAVIGATION, LEADERSHIP, WISDOM, MYSTICISM, - LUCK, BALLISTICS, EAGLE_EYE, NECROMANCY, ESTATES, FIRE_MAGIC, AIR_MAGIC, WATER_MAGIC, EARTH_MAGIC, - SCHOLAR, TACTICS, ARTILLERY, LEARNING, OFFENCE, ARMORER, INTELLIGENCE, SORCERY, RESISTANCE, - FIRST_AID, SKILL_SIZE + PATHFINDING = 0, + ARCHERY, + LOGISTICS, + SCOUTING, + DIPLOMACY, + NAVIGATION, + LEADERSHIP, + WISDOM, + MYSTICISM, + LUCK, + BALLISTICS, + EAGLE_EYE, + NECROMANCY, + ESTATES, + FIRE_MAGIC, + AIR_MAGIC, + WATER_MAGIC, + EARTH_MAGIC, + SCHOLAR, + TACTICS, + ARTILLERY, + LEARNING, + OFFENCE, + ARMORER, + INTELLIGENCE, + SORCERY, + RESISTANCE, + FIRST_AID, + SKILL_SIZE }; static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); }; -class SecondarySkill : public IdentifierWithEnum +class SecondarySkill : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; -class FactionIDBase : public IdentifierBase +class DLL_LINKAGE FactionID : public Identifier { public: - enum Type : int32_t - { - NONE = -2, - DEFAULT = -1, - RANDOM = -1, - ANY = -1, - CASTLE, - RAMPART, - TOWER, - INFERNO, - NECROPOLIS, - DUNGEON, - STRONGHOLD, - FORTRESS, - CONFLUX, - NEUTRAL - }; + using Identifier::Identifier; + + static const FactionID NONE; + static const FactionID DEFAULT; + static const FactionID RANDOM; + static const FactionID ANY; + static const FactionID CASTLE; + static const FactionID RAMPART; + static const FactionID TOWER; + static const FactionID INFERNO; + static const FactionID NECROPOLIS; + static const FactionID DUNGEON; + static const FactionID STRONGHOLD; + static const FactionID FORTRESS; + static const FactionID CONFLUX; + static const FactionID NEUTRAL; static si32 decode(const std::string& identifier); static std::string encode(const si32 index); static std::string entityType(); }; -class FactionID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -using ETownType = FactionID; - class BuildingIDBase : public IdentifierBase { public: @@ -379,10 +387,10 @@ public: } }; -class BuildingID : public IdentifierWithEnum +class BuildingID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class ObjBase : public IdentifierBase @@ -530,56 +538,35 @@ public: }; }; -class Obj : public IdentifierWithEnum +class Obj : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; -class RoadIdBase : public IdentifierBase +class DLL_LINKAGE RoadId : public Identifier { public: - enum Type : int32_t - { - NO_ROAD = 0, - FIRST_REGULAR_ROAD = 1, - DIRT_ROAD = 1, - GRAVEL_ROAD = 2, - COBBLESTONE_ROAD = 3, - ORIGINAL_ROAD_COUNT //+1 - }; + using Identifier::Identifier; + + static const RoadId NO_ROAD; + static const RoadId DIRT_ROAD; + static const RoadId GRAVEL_ROAD; + static const RoadId COBBLESTONE_ROAD; }; -class RoadId : public IdentifierWithEnum +class DLL_LINKAGE RiverId : public Identifier { public: - using IdentifierWithEnum::IdentifierWithEnum; -}; + using Identifier::Identifier; -class RiverIdBase : public IdentifierBase -{ -public: - enum Type : int32_t - { - NO_RIVER = 0, - FIRST_REGULAR_RIVER = 1, - WATER_RIVER = 1, - ICY_RIVER = 2, - MUD_RIVER = 3, - LAVA_RIVER = 4, - ORIGINAL_RIVER_COUNT //+1 - }; + static const RiverId NO_RIVER; + static const RiverId WATER_RIVER; + static const RiverId ICY_RIVER; + static const RiverId MUD_RIVER; + static const RiverId LAVA_RIVER; }; -class RiverId : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -using River = RiverId; -using Road = RoadId; - class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase { public: @@ -589,10 +576,10 @@ public: }; }; -class EPathfindingLayer : public IdentifierWithEnum +class EPathfindingLayer : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class ArtifactPositionBase : public IdentifierBase @@ -622,10 +609,10 @@ public: static std::string encode(const si32 index); }; -class ArtifactPosition : public IdentifierWithEnum +class ArtifactPosition : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class ArtifactIDBase : public IdentifierBase @@ -656,10 +643,10 @@ public: static std::string encode(const si32 index); }; -class ArtifactID : public IdentifierWithEnum +class ArtifactID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class CreatureIDBase : public IdentifierBase @@ -695,10 +682,10 @@ public: static std::string encode(const si32 index); }; -class CreatureID : public IdentifierWithEnum +class CreatureID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class SpellIDBase : public IdentifierBase @@ -812,10 +799,10 @@ public: static std::string encode(const si32 index); }; -class SpellID : public IdentifierWithEnum +class SpellID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; class BattleFieldInfo; @@ -828,22 +815,15 @@ public: DLL_LINKAGE const BattleFieldInfo * getInfo() const; }; -class BoatIdBase : public IdentifierBase +class DLL_LINKAGE BoatId : public Identifier { public: - enum Type : int32_t - { - NONE = -1, - NECROPOLIS = 0, - CASTLE, - FORTRESS - }; -}; + using Identifier::Identifier; -class BoatId : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; + static const BoatId NONE; + static const BoatId NECROPOLIS; + static const BoatId CASTLE; + static const BoatId FORTRESS; }; class TerrainIdBase : public IdentifierBase @@ -873,14 +853,12 @@ public: static std::string entityType(); }; -class TerrainId : public IdentifierWithEnum +class TerrainId : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; -using ETerrainId = TerrainId; - class ObstacleInfo; class Obstacle : public Identifier { @@ -889,27 +867,18 @@ public: DLL_LINKAGE const ObstacleInfo * getInfo() const; }; -class SpellSchoolBase : public IdentifierBase +class DLL_LINKAGE SpellSchool : public Identifier { public: - enum Type : int32_t - { - ANY = -1, - AIR = 0, - FIRE = 1, - WATER = 2, - EARTH = 3, - }; -}; + using Identifier::Identifier; -class SpellSchool : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; + static const SpellSchool ANY; + static const SpellSchool AIR; + static const SpellSchool FIRE; + static const SpellSchool WATER; + static const SpellSchool EARTH; }; -using ESpellSchool = SpellSchool; - class GameResIDBase : public IdentifierBase { public: @@ -930,12 +899,21 @@ public: }; }; -class GameResID : public IdentifierWithEnum +class GameResID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; }; +// Deprecated +// TODO: remove +using ESpellSchool = SpellSchool; +using ETownType = FactionID; using EGameResID = GameResID; +using River = RiverId; +using Road = RoadId; +using ETerrainId = TerrainId; + + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 778bd14ef..452cfbcd1 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -1,5 +1,5 @@ /* - * GameConstants.h, part of VCMI engine + * Enumerations.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index 7973ac21d..e291f4c3b 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -1,5 +1,5 @@ /* - * GameConstants.h, part of VCMI engine + * NumericConstants.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index b5d3a4b7b..4be3af4d4 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -58,6 +58,8 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() result.terrainsCount = 10; result.artifactSlotsCount = 18; result.buildingsCount = 41; + result.roadsCount = 3; + result.riversCount = 4; result.heroIdentifierInvalid = 0xff; result.artifactIdentifierInvalid = 0xff; diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index 4757d4223..75e1515d5 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/lib/mapping/MapFeaturesH3M.h @@ -47,6 +47,8 @@ public: int spellsCount; int skillsCount; int terrainsCount; + int roadsCount; + int riversCount; int artifactSlotsCount; int buildingsCount; diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 5757316e1..5c8a13d97 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -147,14 +147,14 @@ TerrainId MapReaderH3M::readTerrain() RoadId MapReaderH3M::readRoad() { RoadId result(readInt8()); - assert(result < Road::ORIGINAL_ROAD_COUNT); + assert(result.getNum() < features.roadsCount); return result; } RiverId MapReaderH3M::readRiver() { RiverId result(readInt8()); - assert(result < River::ORIGINAL_RIVER_COUNT); + assert(result.getNum() < features.riversCount); return result; } From b61c9a9e35d05ab7865d88c6e81a7dc0a5200138 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 20 Aug 2023 20:09:32 +0300 Subject: [PATCH 0189/1248] Fix build --- client/CPlayerInterface.cpp | 2 +- lib/CBonusTypeHandler.cpp | 61 +++++++++++++------------------ lib/constants/EntityIdentifiers.h | 2 - lib/constants/Enumerations.h | 2 + lib/constants/StringConstants.h | 1 - scripting/lua/LuaStack.h | 6 +-- 6 files changed, 32 insertions(+), 42 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index a49ae7013..f5c56009d 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -183,7 +183,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) // close window from another player if(auto w = GH.windows().topWindow()) - if(w->ID == -1 && player != playerID) + if(w->ID == QueryID::NONE && player != playerID) w->close(); // remove all dialogs that do not expect query answer diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 2e4038eaf..60f3211e3 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -101,62 +101,53 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo } case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools { - switch(bonus->subtype) - { - case SpellSchool(ESpellSchool::ANY): + if (bonus->subtype == SpellSchool::ANY.getNum()) fileName = "E_GOLEM.bmp"; - break; - case SpellSchool(ESpellSchool::AIR): + + if (bonus->subtype == SpellSchool::AIR.getNum()) fileName = "E_LIGHT.bmp"; - break; - case SpellSchool(ESpellSchool::FIRE): + + if (bonus->subtype == SpellSchool::FIRE.getNum()) fileName = "E_FIRE.bmp"; - break; - case SpellSchool(ESpellSchool::WATER): + + if (bonus->subtype == SpellSchool::WATER.getNum()) fileName = "E_COLD.bmp"; - break; - case SpellSchool(ESpellSchool::EARTH): + + if (bonus->subtype == SpellSchool::EARTH.getNum()) fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage - break; - } + break; } case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school { - switch(bonus->subtype) - { - case SpellSchool(ESpellSchool::AIR): + if (bonus->subtype == SpellSchool::AIR.getNum()) fileName = "E_SPAIR.bmp"; - break; - case SpellSchool(ESpellSchool::FIRE): + + if (bonus->subtype == SpellSchool::FIRE.getNum()) fileName = "E_SPFIRE.bmp"; - break; - case SpellSchool(ESpellSchool::WATER): + + if (bonus->subtype == SpellSchool::WATER.getNum()) fileName = "E_SPWATER.bmp"; - break; - case SpellSchool(ESpellSchool::EARTH): + + if (bonus->subtype == SpellSchool::EARTH.getNum()) fileName = "E_SPEATH.bmp"; - break; - } + break; } case BonusType::NEGATIVE_EFFECTS_IMMUNITY: { - switch(bonus->subtype) - { - case SpellSchool(ESpellSchool::AIR): + if (bonus->subtype == SpellSchool::AIR.getNum()) fileName = "E_SPAIR1.bmp"; - break; - case SpellSchool(ESpellSchool::FIRE): + + if (bonus->subtype == SpellSchool::FIRE.getNum()) fileName = "E_SPFIRE1.bmp"; - break; - case SpellSchool(ESpellSchool::WATER): + + if (bonus->subtype == SpellSchool::WATER.getNum()) fileName = "E_SPWATER1.bmp"; - break; - case SpellSchool(ESpellSchool::EARTH): + + if (bonus->subtype == SpellSchool::EARTH.getNum()) fileName = "E_SPEATH1.bmp"; - break; - } + break; } case BonusType::LEVEL_SPELL_IMMUNITY: diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index dba54dc5c..a96ed5017 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -914,6 +914,4 @@ using River = RiverId; using Road = RoadId; using ETerrainId = TerrainId; - - VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 452cfbcd1..1c2cd7b06 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -9,6 +9,8 @@ */ #pragma once +VCMI_LIB_NAMESPACE_BEGIN + enum class PrimarySkill : int8_t { NONE = -1, diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h index f2edac8cf..cf8d12a6a 100644 --- a/lib/constants/StringConstants.h +++ b/lib/constants/StringConstants.h @@ -213,5 +213,4 @@ namespace MappedKeys }; } - VCMI_LIB_NAMESPACE_END diff --git a/scripting/lua/LuaStack.h b/scripting/lua/LuaStack.h index 880536e6b..7963c1d77 100644 --- a/scripting/lua/LuaStack.h +++ b/scripting/lua/LuaStack.h @@ -26,13 +26,13 @@ namespace detail template struct IsRegularClass { - static constexpr auto value = std::is_class::value && !std::is_base_of::value; + static constexpr auto value = std::is_class::value && !std::is_base_of::value; }; template struct IsIdClass { - static constexpr auto value = std::is_class::value && std::is_base_of::value; + static constexpr auto value = std::is_class::value && std::is_base_of::value; }; } @@ -78,7 +78,7 @@ public: template::value, int>::type = 0> void push(const T & value) { - pushInteger(static_cast(value.toEnum())); + pushInteger(static_cast(value.getNum())); } template::value, int>::type = 0> From edd029c79c5310122a229c823c101c02a3ddc89a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 01:08:48 +0300 Subject: [PATCH 0190/1248] Replace "currentPlayer" from gamestate with "activePlayers" - Allows multiple active players at once, e.g. simturns - Cleared up validation of netpacks by server, e.g. always check for pack sender --- client/CPlayerInterface.cpp | 2 +- client/Client.cpp | 7 -- client/Client.h | 1 - client/NetPacksClient.cpp | 22 +++++-- client/adventureMap/AdventureMapInterface.cpp | 5 +- client/adventureMap/TurnTimerWidget.cpp | 24 +++++-- client/adventureMap/TurnTimerWidget.h | 4 +- lib/CGameInfoCallback.cpp | 14 ++-- lib/CGameInfoCallback.h | 6 +- lib/NetPacksLib.cpp | 3 +- lib/gameState/CGameState.h | 6 +- scripting/lua/api/GameCb.cpp | 1 - server/CGameHandler.cpp | 54 +++++---------- server/CGameHandler.h | 13 ++-- server/NetPacksServer.cpp | 65 ++++++++++--------- server/processors/TurnOrderProcessor.cpp | 6 +- 16 files changed, 114 insertions(+), 119 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index f5c56009d..721857fa2 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1469,7 +1469,7 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) void CPlayerInterface::objectRemoved(const CGObjectInstance * obj) { EVENT_HANDLER_CALLED_BY_CLIENT; - if(LOCPLINT->cb->getCurrentPlayer() == playerID && obj->getRemovalSound()) + if(LOCPLINT->cb->isPlayerMakingTurn(playerID) && obj->getRemovalSound()) { waitWhileDialog(); CCS->soundh->playSound(obj->getRemovalSound().value()); diff --git a/client/Client.cpp b/client/Client.cpp index c650d3f4e..b78d1a62b 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -693,13 +693,6 @@ std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h } } -PlayerColor CClient::getLocalPlayer() const -{ - if(LOCPLINT) - return LOCPLINT->playerID; - return getCurrentPlayer(); -} - #if SCRIPTING_ENABLED scripting::Pool * CClient::getGlobalContextPool() const { diff --git a/client/Client.h b/client/Client.h index 8eb0a275d..55dc5e708 100644 --- a/client/Client.h +++ b/client/Client.h @@ -156,7 +156,6 @@ public: void invalidatePaths(); std::shared_ptr getPathsInfo(const CGHeroInstance * h); - virtual PlayerColor getLocalPlayer() const override; friend class CCallback; //handling players actions friend class CBattleCallback; //handling players actions diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index ac94b9150..e27f38edb 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -301,9 +301,15 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) } }; + ArtifactLocation srcLoc(pack.srcArtHolder, pack.artsPack0.front().srcPos); + ArtifactLocation dstLoc(pack.dstArtHolder, pack.artsPack0.front().dstPos); + // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. - callInterfaceIfPresent(cl, cl.getCurrentPlayer(), &IGameEventsReceiver::bulkArtMovementStart, - pack.artsPack0.size() + pack.artsPack1.size()); + callInterfaceIfPresent(cl, srcLoc.owningPlayer(), &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + + if (srcLoc.owningPlayer() != dstLoc.owningPlayer()) + callInterfaceIfPresent(cl, dstLoc.owningPlayer(), &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + applyMove(pack.artsPack0); if(pack.swap) applyMove(pack.artsPack1); @@ -386,9 +392,15 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface auto initInterfaces = [this]() { cl.initPlayerInterfaces(); - auto currentPlayer = cl.gameState()->currentPlayer; - callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, currentPlayer); - callOnlyThatInterface(cl, currentPlayer, &CGameInterface::yourTurn); + + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (cl.gameState()->isPlayerMakingTurn(player)) + { + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); + callOnlyThatInterface(cl, player, &CGameInterface::yourTurn); + } + } }; for(auto player : pack.players) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 18b683e43..175bf25e4 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -327,7 +327,7 @@ void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuma mapAudio->onEnemyTurnStarted(); widget->getMinimap()->setAIRadar(!isHuman); - widget->getInfoBar()->startEnemyTurn(LOCPLINT->cb->getCurrentPlayer()); + widget->getInfoBar()->startEnemyTurn(playerID); setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); } @@ -363,8 +363,7 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) onCurrentPlayerChanged(playerID); setState(EAdventureState::MAKING_TURN); - if(LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID - || settings["session"]["spectate"].Bool()) + if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool()) { widget->getMinimap()->setAIRadar(false); widget->getInfoBar()->showSelection(); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 842aa168b..76bd90129 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -67,14 +67,18 @@ void TurnTimerWidget::show(Canvas & to) showAll(to); } -void TurnTimerWidget::setTime(int time) +void TurnTimerWidget::setTime(PlayerColor player, int time) { int newTime = time / 1000; - if((LOCPLINT->cb->getCurrentPlayer() == LOCPLINT->playerID) + if((LOCPLINT->cb->isPlayerMakingTurn(LOCPLINT->playerID)) && (newTime != turnTime) && notifications.count(newTime)) + { CCS->soundh->playSound(variables["notificationSound"].String()); + } + turnTime = newTime; + if(auto w = widget("timer")) { std::ostringstream oss; @@ -83,18 +87,23 @@ void TurnTimerWidget::setTime(int time) if(graphics && LOCPLINT && LOCPLINT->cb && variables["textColorFromPlayerColor"].Bool() - && LOCPLINT->cb->getCurrentPlayer().isValidPlayer()) + && player.isValidPlayer()) { - w->setColor(graphics->playerColors[LOCPLINT->cb->getCurrentPlayer()]); + w->setColor(graphics->playerColors[player]); } } } void TurnTimerWidget::tick(uint32_t msPassed) { - if(LOCPLINT && LOCPLINT->cb) + if(!LOCPLINT || !LOCPLINT->cb) + return; + + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) { - auto player = LOCPLINT->cb->getCurrentPlayer(); + if (!LOCPLINT->cb->isPlayerMakingTurn(player)) + continue; + auto time = LOCPLINT->cb->getPlayerTurnTime(player); cachedTurnTime -= msPassed; if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero @@ -107,7 +116,8 @@ void TurnTimerWidget::tick(uint32_t msPassed) lastTurnTime = time; cachedTurnTime = time; } - else setTime(cachedTurnTime); + else + setTime(player, cachedTurnTime); }; auto * playerInfo = LOCPLINT->cb->getPlayer(player); diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index 592143e79..e5d5614fc 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -39,13 +39,13 @@ private: std::set notifications; std::shared_ptr buildDrawRect(const JsonNode & config) const; - + public: void show(Canvas & to) override; void tick(uint32_t msPassed) override; - void setTime(int time); + void setTime(PlayerColor player, int time); TurnTimerWidget(); }; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index de685aa14..efb208fd2 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -320,8 +320,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero dest.initFromHero(h, infoLevel); //DISGUISED bonus implementation - - if(getPlayerRelations(getLocalPlayer(), hero->tempOwner) == PlayerRelations::ENEMIES) + if(getPlayerRelations(*player, hero->tempOwner) == PlayerRelations::ENEMIES) { //todo: bonus cashing int disguiseLevel = h->valOfBonuses(Selector::typeSubtype(BonusType::DISGUISED, 0)); @@ -705,9 +704,9 @@ bool CGameInfoCallback::isOwnedOrVisited(const CGObjectInstance *obj) const return visitor->ID == Obj::HERO && canGetFullInfo(visitor); //owned or allied hero is a visitor } -PlayerColor CGameInfoCallback::getCurrentPlayer() const +bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const { - return gs->currentPlayer; + return gs->actingPlayers.count(player); } CGameInfoCallback::CGameInfoCallback(CGameState * GS, std::optional Player): @@ -932,11 +931,6 @@ const CGHeroInstance * CGameInfoCallback::getHeroWithSubid( int subid ) const return gs->map->allHeroes.at(subid).get(); } -PlayerColor CGameInfoCallback::getLocalPlayer() const -{ - return getCurrentPlayer(); -} - bool CGameInfoCallback::isInTheMap(const int3 &pos) const { return gs->map->isInTheMap(pos); @@ -944,7 +938,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const { - gs->getTilesInRange(tiles, pos, radious, getLocalPlayer(), -1, distanceFormula); + gs->getTilesInRange(tiles, pos, radious, *player, -1, distanceFormula); } void CGameInfoCallback::calculatePaths(const std::shared_ptr & config) diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 54ab697fb..775439b5d 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -63,8 +63,6 @@ public: // PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; // void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object // EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player -// PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns - virtual PlayerColor getLocalPlayer() const = 0; //player that is currently owning given client (if not a client, then returns current player) // const PlayerSettings * getPlayerSettings(PlayerColor color) const; @@ -99,7 +97,6 @@ public: // const TerrainTile * getTile(int3 tile, bool verbose = true) const; // std::shared_ptr> getAllVisibleTiles() const; // bool isInTheMap(const int3 &pos) const; -// void getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula = int3::DIST_2D) const; //town // const CGTownInstance* getTown(ObjectInstanceID objid) const; @@ -151,8 +148,7 @@ public: virtual PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const; virtual void getThievesGuildInfo(SThievesGuildInfo & thi, const CGObjectInstance * obj); //get thieves' guild info obtainable while visiting given object virtual EPlayerStatus getPlayerStatus(PlayerColor player, bool verbose = true) const; //-1 if no such player - virtual PlayerColor getCurrentPlayer() const; //player that currently makes move // TODO synchronous turns - PlayerColor getLocalPlayer() const override; //player that is currently owning given client (if not a client, then returns current player) + virtual bool isPlayerMakingTurn(PlayerColor player) const; //player that currently makes move // TODO synchronous turns virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; virtual TurnTimerInfo getPlayerTurnTime(PlayerColor color) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 33706619d..a43ee2890 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2501,7 +2501,8 @@ void PlayerCheated::applyGs(CGameState * gs) const void YourTurn::applyGs(CGameState * gs) const { - gs->currentPlayer = player; + gs->actingPlayers.clear(); + gs->actingPlayers.insert(player); } void DaysWithoutTown::applyGs(CGameState * gs) const diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index dbb56c45d..aa274b0ec 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -86,6 +86,9 @@ public: //we have here all heroes available on this map that are not hired std::unique_ptr heroesPool; + /// list of players currently making turn. Usually - just one, except for simturns + std::set actingPlayers; + CGameState(); virtual ~CGameState(); @@ -95,7 +98,6 @@ public: void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) - PlayerColor currentPlayer; //ID of player currently having turn ConstTransitivePtr curB; //current battle ui32 day; //total number of days in game ConstTransitivePtr map; @@ -151,7 +153,7 @@ public: { h & scenarioOps; h & initialOpts; - h & currentPlayer; + h & actingPlayers; h & day; h & map; h & players; diff --git a/scripting/lua/api/GameCb.cpp b/scripting/lua/api/GameCb.cpp index 5bf19b3df..7227f403b 100644 --- a/scripting/lua/api/GameCb.cpp +++ b/scripting/lua/api/GameCb.cpp @@ -30,7 +30,6 @@ const std::vector GameCbProxy::REGISTER_CUSTOM = { {"getDate", LuaMethodWrapper::invoke, false}, {"isAllowed", LuaMethodWrapper::invoke, false}, - {"getCurrentPlayer", LuaMethodWrapper::invoke, false}, {"getPlayer", LuaMethodWrapper::invoke, false}, {"getHero", LuaMethodWrapper::invoke, false}, diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 74f426a4d..ad459f051 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1003,7 +1003,10 @@ void CGameHandler::run(bool resume) { const int waitTime = 100; //ms - turnTimerHandler.onPlayerMakingTurn(gs->players[gs->getCurrentPlayer()], waitTime); + for(auto & player : gs->players) + if (gs->isPlayerMakingTurn(player.first)) + turnTimerHandler.onPlayerMakingTurn(player.second, waitTime); + if(gs->curB) turnTimerHandler.onBattleLoop(waitTime); @@ -1064,7 +1067,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { const CGHeroInstance *h = getHero(hid); // not turn of that hero or player can't simply teleport hero (at least not with this function) - if (!h || (asker != PlayerColor::NEUTRAL && (teleporting || h->getOwner() != gs->currentPlayer))) + if(!h || (asker != PlayerColor::NEUTRAL && teleporting)) { if(h && getStartInfo()->turnTimerInfo.isEnabled() && gs->players[h->getOwner()].turnTimer.turnTimer == 0) return true; //timer expired, no error @@ -1278,7 +1281,7 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui const CGHeroInstance *h = getHero(hid); const CGTownInstance *t = getTown(dstid); - if (!h || !t || h->getOwner() != gs->currentPlayer) + if (!h || !t) COMPLAIN_RET("Invalid call to teleportHero!"); const CGTownInstance *from = h->visitedTown; @@ -1646,12 +1649,6 @@ void CGameHandler::sendAndApply(CPackForClient * pack) logNetwork->trace("\tApplied on gs: %s", typeid(*pack).name()); } -void CGameHandler::applyAndSend(CPackForClient * pack) -{ - gs->apply(pack); - sendToAllClients(pack); -} - void CGameHandler::sendAndApply(CGarrisonOperationPack * pack) { sendAndApply(static_cast(pack)); @@ -1672,7 +1669,7 @@ void CGameHandler::sendAndApply(NewStructures * pack) bool CGameHandler::isPlayerOwns(CPackForServer * pack, ObjectInstanceID id) { - return getPlayerAt(pack->c) == getOwner(id); + return pack->player == getOwner(id) && hasPlayerAt(getOwner(id), pack->c); } void CGameHandler::throwNotAllowedAction(CPackForServer * pack) @@ -1687,14 +1684,14 @@ void CGameHandler::throwNotAllowedAction(CPackForServer * pack) void CGameHandler::wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer) { std::ostringstream oss; - oss << "You were identified as player " << getPlayerAt(pack->c) << " while expecting " << expectedplayer; + oss << "You were identified as player " << pack->player << " while expecting " << expectedplayer; logNetwork->error(oss.str()); if(pack->c) playerMessages->sendSystemMessage(pack->c, oss.str()); } -void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id) +void CGameHandler::throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id) { if(!isPlayerOwns(pack, id)) { @@ -1703,9 +1700,14 @@ void CGameHandler::throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id) } } -void CGameHandler::throwOnWrongPlayer(CPackForServer * pack, PlayerColor player) +void CGameHandler::throwIfWrongPlayer(CPackForServer * pack) { - if(!hasPlayerAt(player, pack->c) && player != getPlayerAt(pack->c)) + throwIfWrongPlayer(pack, pack->player); +} + +void CGameHandler::throwIfWrongPlayer(CPackForServer * pack, PlayerColor player) +{ + if(!hasPlayerAt(player, pack->c) || pack->player != player) { wrongPlayerMessage(pack, player); throwNotAllowedAction(pack); @@ -2173,30 +2175,6 @@ bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr return connections.at(player).count(c); } -PlayerColor CGameHandler::getPlayerAt(std::shared_ptr c) const -{ - std::set all; - for (auto i=connections.cbegin(); i!=connections.cend(); i++) - if(vstd::contains(i->second, c)) - all.insert(i->first); - - switch(all.size()) - { - case 0: - return PlayerColor::NEUTRAL; - case 1: - return *all.begin(); - default: - { - //if we have more than one player at this connection, try to pick active one - if (vstd::contains(all, gs->currentPlayer)) - return gs->currentPlayer; - else - return PlayerColor::CANNOT_DETERMINE; //cannot say which player is it - } - } -} - bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos) { const CArmedInstance * s1 = static_cast(getObjInstance(id)); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 64b29bfd2..3e9ad7b1e 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -178,7 +178,6 @@ public: void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); void handleClientDisconnection(std::shared_ptr c); void handleReceivedPack(CPackForServer * pack); - PlayerColor getPlayerAt(std::shared_ptr c) const; bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); @@ -243,16 +242,22 @@ public: void sendToAllClients(CPackForClient * pack); void sendAndApply(CPackForClient * pack) override; - void applyAndSend(CPackForClient * pack); void sendAndApply(CGarrisonOperationPack * pack); void sendAndApply(SetResources * pack); void sendAndApply(NewStructures * pack); void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); + /// Unconditionally throws with "Action not allowed" message void throwNotAllowedAction(CPackForServer * pack); - void throwOnWrongOwner(CPackForServer * pack, ObjectInstanceID id); - void throwOnWrongPlayer(CPackForServer * pack, PlayerColor player); + /// Throws if player stated in pack is not making turn right now + void throwIfPlayerNotActive(CPackForServer * pack); + /// Throws if object is not owned by pack sender + void throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id); + /// Throws if player is not present on connection of this pack + void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player); + void throwIfWrongPlayer(CPackForServer * pack); void throwAndComplain(CPackForServer * pack, std::string txt); + bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); void run(bool resume); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index b49392edf..5d2622f1e 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -37,76 +37,80 @@ void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) { - if (!gh.hasPlayerAt(pack.player, pack.c)) - gh.throwAndComplain(&pack, "No such pack.player!"); - + gh.throwIfWrongPlayer(&pack); result = gh.turnOrder->onPlayerEndsTurn(pack.player); } void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); + gh.throwIfWrongOwner(&pack, pack.hid); result = gh.removeObject(gh.getObj(pack.hid)); } void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) { - result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, gh.getPlayerAt(pack.c)); + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, pack.player); } void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); + gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.teleportHero(pack.hid, pack.dest, pack.source, gh.getPlayerAt(pack.c)); + result = gh.teleportHero(pack.hid, pack.dest, pack.source, pack.player); } void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) { - //checks for owning in the gh func - result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, gh.getPlayerAt(pack.c)); + gh.throwIfWrongPlayer(&pack); + result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, pack.player); } void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) { + gh.throwIfWrongPlayer(&pack); result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); } void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack) { + gh.throwIfWrongPlayer(&pack); result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount); } void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack) { + gh.throwIfWrongPlayer(&pack); result = gh.bulkMergeStacks(pack.src, pack.srcOwner); } void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack) { + gh.throwIfWrongPlayer(&pack); result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner); } void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack) { - gh.throwOnWrongOwner(&pack, pack.id); + gh.throwIfWrongOwner(&pack, pack.id); result = gh.disbandCreature(pack.id, pack.pos); } void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) { - gh.throwOnWrongOwner(&pack, pack.tid); + gh.throwIfWrongOwner(&pack, pack.tid); result = gh.buildStructure(pack.tid, pack.bid); } void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) { + gh.throwIfWrongOwner(&pack, pack.tid); result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level); } void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) { - gh.throwOnWrongOwner(&pack, pack.id); + gh.throwIfWrongOwner(&pack, pack.id); result = gh.upgradeCreature(pack.id, pack.pos, pack.cid); } @@ -120,37 +124,38 @@ void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) { - gh.throwOnWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally + gh.throwIfWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally result = gh.moveArtifact(pack.src, pack.dst); } void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) { - const CGHeroInstance * pSrcHero = gh.getHero(pack.srcHero); - gh.throwOnWrongPlayer(&pack, pSrcHero->getOwner()); + gh.throwIfWrongOwner(&pack, pack.srcHero); result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap); } void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) { - gh.throwOnWrongOwner(&pack, pack.heroID); + gh.throwIfWrongOwner(&pack, pack.heroID); result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo); } void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) { - gh.throwOnWrongPlayer(&pack, pack.al.owningPlayer()); + gh.throwIfWrongPlayer(&pack, pack.al.owningPlayer()); result = gh.eraseArtifactByClient(pack.al); } void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); + gh.throwIfWrongOwner(&pack, pack.hid); result = gh.buyArtifact(pack.hid, pack.aid); } void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { + gh.throwIfWrongPlayer(&pack); + const CGObjectInstance * market = gh.getObj(pack.marketId); if(!market) gh.throwAndComplain(&pack, "Invalid market object"); @@ -177,7 +182,7 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) gh.throwAndComplain(&pack, "This hero can't use this marketplace!"); if(!allyTownSkillTrade) - gh.throwOnWrongPlayer(&pack, player); + gh.throwIfWrongPlayer(&pack, player); result = true; @@ -231,28 +236,31 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); + gh.throwIfWrongOwner(&pack, pack.hid); result = gh.setFormation(pack.hid, pack.formation); } void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) { - if (!gh.hasPlayerAt(pack.player, pack.c)) - gh.throwAndComplain(&pack, "No such pack.player!"); + gh.throwIfWrongPlayer(&pack); result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); } void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) { - if(gh.getPlayerRelations(gh.getOwner(pack.objid), gh.getPlayerAt(pack.c)) == PlayerRelations::ENEMIES) + gh.throwIfWrongPlayer(&pack); + + if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES) gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard"); - result = gh.buildBoat(pack.objid, gh.getPlayerAt(pack.c)); + result = gh.buildBoat(pack.objid, pack.player); } void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) { + gh.throwIfWrongPlayer(&pack); + auto playerToConnection = gh.connections.find(pack.player); if(playerToConnection == gh.connections.end()) gh.throwAndComplain(&pack, "No such pack.player!"); @@ -266,21 +274,20 @@ void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { - if (!gh.hasPlayerAt(pack.player, pack.c)) - gh.throwAndComplain(&pack, "No such pack.player!"); + gh.throwIfWrongPlayer(&pack); result = gh.battles->makePlayerBattleAction(pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) { - gh.throwOnWrongOwner(&pack, pack.id); + gh.throwIfWrongOwner(&pack, pack.id); result = gh.dig(gh.getHero(pack.id)); } void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) { - gh.throwOnWrongOwner(&pack, pack.hid); + gh.throwIfWrongOwner(&pack, pack.hid); const CSpell * s = pack.sid.toSpell(); if(!s) @@ -299,7 +306,7 @@ void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) { if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions - gh.throwOnWrongPlayer(&pack, pack.player); + gh.throwIfWrongPlayer(&pack, pack.player); gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); result = true; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index df57a0167..ee38aa5c3 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -103,7 +103,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) gameHandler->sendAndApply(&yt); assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); + assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); } void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) @@ -122,7 +122,7 @@ void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) assert(!actingPlayers.empty()); assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); + assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); } void TurnOrderProcessor::addPlayer(PlayerColor which) @@ -144,7 +144,7 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) assert(!actingPlayers.empty()); assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->getCurrentPlayer() == *actingPlayers.begin()); + assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); } bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) From 274bf739b8b8079dca2ed6eb78867697af314cf3 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 8 Aug 2023 18:37:42 +0300 Subject: [PATCH 0191/1248] BattleAI: damage cache and switch to different model of spells evaluation --- AI/BattleAI/AttackPossibility.cpp | 127 ++++++++++--- AI/BattleAI/AttackPossibility.h | 31 +++- AI/BattleAI/BattleAI.cpp | 247 +++++++++++++++----------- AI/BattleAI/BattleAI.h | 9 - AI/BattleAI/BattleExchangeVariant.cpp | 135 +++++++------- AI/BattleAI/BattleExchangeVariant.h | 32 +++- AI/BattleAI/PotentialTargets.cpp | 21 ++- AI/BattleAI/PotentialTargets.h | 5 +- AI/BattleAI/StackWithBonuses.cpp | 13 +- AI/BattleAI/StackWithBonuses.h | 1 + AI/Nullkiller/Engine/Nullkiller.cpp | 2 +- 11 files changed, 407 insertions(+), 216 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 63cb7f209..4a4de7e46 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -18,6 +18,81 @@ uint64_t averageDmg(const DamageRange & range) return (range.min + range.max) / 2; } +void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + auto damage = averageDmg(hb->battleEstimateDamage(attacker, defender, 0).damage); + + damageCache[attacker->unitId()][defender->unitId()] = damage / attacker->getCount(); +} + + +void DamageCache::buildDamageCache(std::shared_ptr hb, int side) +{ + auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool + { + return true; + }); + + std::vector ourUnits, enemyUnits; + + for(auto stack : stacks) + { + if(stack->unitSide() == side) + ourUnits.push_back(stack); + else + enemyUnits.push_back(stack); + } + + for(auto ourUnit : ourUnits) + { + if(!ourUnit->alive()) + continue; + + for(auto enemyUnit : enemyUnits) + { + if(enemyUnit->alive()) + { + cacheDamage(ourUnit, enemyUnit, hb); + cacheDamage(enemyUnit, ourUnit, hb); + } + } + } +} + +int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); + + if(damage == 0) + { + cacheDamage(attacker, defender, hb); + + damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount(); + } + + return static_cast(damage); +} + +int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb) +{ + if(parent) + { + auto attackerDamageMap = parent->damageCache.find(attacker->unitId()); + + if(attackerDamageMap != parent->damageCache.end()) + { + auto targetDamage = attackerDamageMap->second.find(defender->unitId()); + + if(targetDamage != attackerDamageMap->second.end()) + { + return static_cast(targetDamage->second * attacker->getCount()); + } + } + } + + return getDamage(attacker, defender, hb); +} + AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack) : from(from), dest(dest), attack(attack) { @@ -42,7 +117,8 @@ int64_t AttackPossibility::calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, - const CBattleInfoCallback & cb) + DamageCache & damageCache, + std::shared_ptr state) { const float HEALTH_BOUNTY = 0.5; const float KILL_BOUNTY = 1.0 - HEALTH_BOUNTY; @@ -52,11 +128,11 @@ int64_t AttackPossibility::calculateDamageReduce( // FIXME: provide distance info for Jousting bonus auto attackerUnitForMeasurement = attacker; - if(attackerUnitForMeasurement->isTurret()) + if(!attackerUnitForMeasurement || attackerUnitForMeasurement->isTurret()) { - auto ourUnits = cb.battleGetUnitsIf([&](const battle::Unit * u) -> bool + auto ourUnits = state->battleGetUnitsIf([&](const battle::Unit * u) -> bool { - return u->unitSide() == attacker->unitSide() && !u->isTurret(); + return u->unitSide() != defender->unitSide() && !u->isTurret(); }); if(ourUnits.empty()) @@ -65,15 +141,18 @@ int64_t AttackPossibility::calculateDamageReduce( attackerUnitForMeasurement = ourUnits.front(); } - auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0); + auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0); - auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage); - auto damagePerEnemy = enemyDamage / (double)defender->getCount(); + auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth())); } -int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state) +int64_t AttackPossibility::evaluateBlockedShootersDmg( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state) { int64_t res = 0; @@ -84,10 +163,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a auto hexes = attacker->getSurroundingHexes(hex); for(BattleHex tile : hexes) { - auto st = state.battleGetUnitByPos(tile, true); - if(!st || !state.battleMatchOwner(st, attacker)) + auto st = state->battleGetUnitByPos(tile, true); + if(!st || !state->battleMatchOwner(st, attacker)) continue; - if(!state.battleCanShoot(st)) + if(!state->battleCanShoot(st)) continue; // FIXME: provide distance info for Jousting bonus @@ -97,8 +176,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a BattleAttackInfo meleeAttackInfo(st, attacker, 0, false); meleeAttackInfo.defenderPos = hex; - auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo); - auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo); + auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); + auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; res += gain; @@ -107,13 +186,17 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a return res; } -AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state) +AttackPossibility AttackPossibility::evaluate( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state) { auto attacker = attackInfo.attacker; auto defender = attackInfo.defender; const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); - const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker)); + const auto attackerSide = state->playerToSide(state->battleGetOwner(attacker)); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo); @@ -141,9 +224,9 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf std::vector units; if (attackInfo.shooting) - units = state.getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); + units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); else - units = state.getAttackedBattleUnits(attacker, defHex, false, hex); + units = state->getAttackedBattleUnits(attacker, defHex, false, hex); // ensure the defender is also affected bool addDefender = true; @@ -172,7 +255,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; DamageEstimation retaliation; - auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation); + auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth()); vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth()); @@ -181,7 +264,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth()); damageDealt = averageDmg(attackDmg.damage); - defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state); + defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state); ap.attackerState->afterAttack(attackInfo.shooting, false); //FIXME: use ranged retaliation @@ -191,11 +274,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) { damageReceived = averageDmg(retaliation.damage); - attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state); + attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state); defenderState->afterAttack(attackInfo.shooting, true); } - bool isEnemy = state.battleMatchOwner(attacker, u); + bool isEnemy = state->battleMatchOwner(attacker, u); // this includes enemy units as well as attacker units under enemy's mind control if(isEnemy) @@ -225,7 +308,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf } // check how much damage we gain from blocking enemy shooters on this hex - bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, state); + bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, damageCache, state); #if BATTLE_TRACE_LEVEL>=1 logAi->trace("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld", diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index 7a9cf766f..f96f59a6d 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -15,6 +15,22 @@ #define BATTLE_TRACE_LEVEL 0 +class DamageCache +{ +private: + std::unordered_map> damageCache; + DamageCache * parent; + +public: + DamageCache() : parent(nullptr) {} + DamageCache(DamageCache * parent) : parent(parent) {} + + void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr hb); + void buildDamageCache(std::shared_ptr hb, int side); +}; + /// /// Evaluate attack value of one particular attack taking into account various effects like /// retaliation, 2-hex breath, collateral damage, shooters blocked damage @@ -40,14 +56,23 @@ public: int64_t damageDiff() const; int64_t attackValue() const; - static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state); + static AttackPossibility evaluate( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state); static int64_t calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, - const CBattleInfoCallback & cb); + DamageCache & damageCache, + std::shared_ptr cb); private: - static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state); + static int64_t evaluateBlockedShootersDmg( + const BattleAttackInfo & attackInfo, + BattleHex hex, + DamageCache & damageCache, + std::shared_ptr state); }; diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 94df8714d..8da28e145 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -44,7 +44,55 @@ SpellTypes spellType(const CSpell * spell) return SpellTypes::OTHER; } -std::vector CBattleAI::getBrokenWallMoatHexes() const +class BattleEvaluator +{ + std::unique_ptr targets; + std::shared_ptr hb; + BattleExchangeEvaluator scoreEvaluator; + std::shared_ptr cb; + std::shared_ptr env; + bool activeActionMade = false; + std::optional cachedAttack; + PlayerColor playerID; + int side; + int64_t cachedScore; + DamageCache damageCache; + +public: + BattleAction selectStackAction(const CStack * stack); + void attemptCastingSpell(const CStack * stack); + std::optional findBestCreatureSpell(const CStack * stack); + BattleAction goTowardsNearest(const CStack * stack, std::vector hexes); + std::vector getBrokenWallMoatHexes() const; + void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only + void print(const std::string & text) const; + + BattleEvaluator(std::shared_ptr env, std::shared_ptr cb, const battle::Unit * activeStack, PlayerColor playerID, int side) + :scoreEvaluator(cb, env), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb) + { + hb = std::make_shared(env.get(), cb); + damageCache.buildDamageCache(hb, side); + + targets = std::make_unique(activeStack, damageCache, hb); + cachedScore = EvaluationResult::INEFFECTIVE_SCORE; + } + + BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + std::shared_ptr hb, + DamageCache & damageCache, + const battle::Unit * activeStack, + PlayerColor playerID, + int side) + :scoreEvaluator(cb, env), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache) + { + targets = std::make_unique(activeStack, damageCache, hb); + cachedScore = EvaluationResult::INEFFECTIVE_SCORE; + } +}; + +std::vector BattleEvaluator::getBrokenWallMoatHexes() const { std::vector result; @@ -110,7 +158,7 @@ BattleAction CBattleAI::useHealingTent(const CStack *stack) return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack } -std::optional CBattleAI::findBestCreatureSpell(const CStack *stack) +std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) { //TODO: faerie dragon type spell should be selected by server SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); @@ -141,40 +189,37 @@ std::optional CBattleAI::findBestCreatureSpell(const CStack * return std::nullopt; } -BattleAction CBattleAI::selectStackAction(const CStack * stack) +BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { //evaluate casting spell for spellcasting stack std::optional bestSpellcast = findBestCreatureSpell(stack); - HypotheticBattle hb(env.get(), cb); + auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); + auto score = EvaluationResult::INEFFECTIVE_SCORE; - PotentialTargets targets(stack, hb); - BattleExchangeEvaluator scoreEvaluator(cb, env); - auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb); - - int64_t score = EvaluationResult::INEFFECTIVE_SCORE; - - - if(targets.possibleAttacks.empty() && bestSpellcast.has_value()) + if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) { - movesSkippedByDefense = 0; + activeActionMade = true; return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); } - if(!targets.possibleAttacks.empty()) + if(!targets->possibleAttacks.empty()) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Evaluating attack for %s", stack->getDescription()); #endif - auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb); + auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); auto & bestAttack = evaluationResult.bestAttack; + cachedAttack = bestAttack; + cachedScore = evaluationResult.score; + //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) { // return because spellcast value is damage dealt and score is dps reduce - movesSkippedByDefense = 0; + activeActionMade = true; return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); } @@ -194,7 +239,7 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack) bestAttack.attackerDamageReduce, bestAttack.attackValue() ); - if (moveTarget.score <= score) + if (moveTarget.scorePerTurn <= score) { if(evaluationResult.wait) { @@ -202,12 +247,12 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack) } else if(bestAttack.attack.shooting) { - movesSkippedByDefense = 0; + activeActionMade = true; return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); } else { - movesSkippedByDefense = 0; + activeActionMade = true; return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); } } @@ -215,9 +260,11 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack) } //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. - if(moveTarget.score > score) + if(moveTarget.scorePerTurn > score) { score = moveTarget.score; + cachedAttack = moveTarget.cachedAttack; + cachedScore = score; if(stack->waited()) { @@ -238,7 +285,7 @@ BattleAction CBattleAI::selectStackAction(const CStack * stack) if(brokenWallMoat.size()) { - movesSkippedByDefense = 0; + activeActionMade = true; if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); @@ -284,7 +331,11 @@ void CBattleAI::activeStack( const CStack * stack ) return; } - if (attemptCastingSpell()) + BattleEvaluator evaluator(env, cb, stack, playerID, side); + + result = evaluator.selectStackAction(stack); + + if(evaluator.attemptCastingSpell(stack)) return; logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); @@ -294,8 +345,6 @@ void CBattleAI::activeStack( const CStack * stack ) cb->battleMakeUnitAction(*action); return; } - - result = selectStackAction(stack); } catch(boost::thread_interrupted &) { @@ -320,7 +369,7 @@ void CBattleAI::activeStack( const CStack * stack ) cb->battleMakeUnitAction(result); } -BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector hexes) const +BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) { auto reachability = cb->getReachability(stack); auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); @@ -357,9 +406,6 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vectorhasBonusOfType(BonusType::FLYING)) @@ -371,7 +417,7 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vectorbattleGetAllObstacles(); for (const auto & obst: obstacles) { @@ -396,7 +442,7 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector>>>>>> ea22737e9 (BattleAI: damage cache and switch to different model of spells evaluation) { auto hero = cb->battleGetMyHero(); if(!hero) @@ -488,7 +538,7 @@ bool CBattleAI::attemptCastingSpell() vstd::erase_if(possibleSpells, [](const CSpell *s) { - return spellType(s) != SpellTypes::BATTLE; + return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; }); LOGFL("I know how %d of them works.", possibleSpells.size()); @@ -499,7 +549,7 @@ bool CBattleAI::attemptCastingSpell() { spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); - if(!spell->isDamage() && spell->getTargetType() == spells::AimType::LOCATION) + if(spell->getTargetType() == spells::AimType::LOCATION) continue; const bool FAST = true; @@ -518,7 +568,7 @@ bool CBattleAI::attemptCastingSpell() using ValueMap = PossibleSpellcast::ValueMap; - auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, HypotheticBattle & state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool + auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, std::shared_ptr state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool { bool firstRound = true; bool enemyHadTurn = false; @@ -529,7 +579,7 @@ bool CBattleAI::attemptCastingSpell() for(auto & round : queue) { if(!firstRound) - state.nextRound(0);//todo: set actual value? + state->nextRound(0);//todo: set actual value? for(auto unit : round) { if(!vstd::contains(values, unit->unitId())) @@ -538,11 +588,11 @@ bool CBattleAI::attemptCastingSpell() if(!unit->alive()) continue; - if(state.battleGetOwner(unit) != playerID) + if(state->battleGetOwner(unit) != playerID) { enemyHadTurn = true; - if(!firstRound || state.battleCastSpells(unit->unitSide()) == 0) + if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0) { //enemy could counter our spell at this point //anyway, we do not know what enemy will do @@ -556,15 +606,15 @@ bool CBattleAI::attemptCastingSpell() ourTurnSpan++; } - state.nextTurn(unit->unitId()); + state->nextTurn(unit->unitId()); - PotentialTargets pt(unit, state); + PotentialTargets pt(unit, damageCache, state); if(!pt.possibleAttacks.empty()) { AttackPossibility ap = pt.bestAction(); - auto swb = state.getForUpdate(unit->unitId()); + auto swb = state->getForUpdate(unit->unitId()); *swb = *ap.attackerState; if(ap.defenderDamageReduce > 0) @@ -574,7 +624,7 @@ bool CBattleAI::attemptCastingSpell() for(auto affected : ap.affectedUnits) { - swb = state.getForUpdate(affected->unitId()); + swb = state->getForUpdate(affected->unitId()); *swb = *affected; if(ap.defenderDamageReduce > 0) @@ -587,7 +637,7 @@ bool CBattleAI::attemptCastingSpell() auto bav = pt.bestActionValue(); //best action is from effective owner`s point if view, we need to convert to our point if view - if(state.battleGetOwner(unit) != playerID) + if(state->battleGetOwner(unit) != playerID) bav = -bav; values[unit->unitId()] += bav; } @@ -638,13 +688,13 @@ bool CBattleAI::attemptCastingSpell() { bool enemyHadTurn = false; - HypotheticBattle state(env.get(), cb); + auto state = std::make_shared(env.get(), cb); evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); if(!enemyHadTurn) { - auto battleIsFinishedOpt = state.battleIsFinished(); + auto battleIsFinishedOpt = state->battleIsFinished(); if(battleIsFinishedOpt) { @@ -661,74 +711,60 @@ bool CBattleAI::attemptCastingSpell() auto evaluateSpellcast = [&] (PossibleSpellcast * ps, std::shared_ptr) { - HypotheticBattle state(env.get(), cb); + auto state = std::make_shared(env.get(), cb); - spells::BattleCast cast(&state, hero, spells::Mode::HERO, ps->spell); - cast.castEval(state.getServerCallback(), ps->dest); - ValueMap newHealthOfStack; - ValueMap newValueOfStack; + spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps->spell); + cast.castEval(state->getServerCallback(), ps->dest); - size_t ourUnits = 0; + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool{ return true; }); - std::set unitIds; - - state.battleGetUnitsIf([&](const battle::Unit * u)->bool - { - if(!u->isGhost() && !u->isTurret()) - unitIds.insert(u->unitId()); - - return false; - }); - - for(auto unitId : unitIds) - { - auto localUnit = state.battleGetUnitByID(unitId); - - newHealthOfStack[unitId] = localUnit->getAvailableHealth(); - newValueOfStack[unitId] = 0; - - if(state.battleGetOwner(localUnit) == playerID && localUnit->alive() && localUnit->willMove()) - ourUnits++; - } - - size_t minTurnSpan = ourUnits/3; //todo: tweak this - - std::vector newTurnOrder; - - state.battleGetTurnOrder(newTurnOrder, amount, 2); - - const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, state, minTurnSpan, nullptr); - - if(turnSpanOK || castNow) - { - int64_t totalGain = 0; - - for(auto unitId : unitIds) + auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { - auto localUnit = state.battleGetUnitByID(unitId); + auto original = cb->battleGetUnitByID(u->unitId()); + return !original || u->speed() != original->speed(); + }); - auto newValue = getValOr(newValueOfStack, unitId, 0); - auto oldValue = getValOr(valueOfStack, unitId, 0); + DamageCache innerCache(&damageCache); + innerCache.buildDamageCache(state, side); - auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; + if(needFullEval || !cachedAttack) + { + PotentialTargets innerTargets(activeStack, damageCache, state); + BattleExchangeEvaluator innerEvaluator(state, env); - if(localUnit->unitOwner() != playerID) - healthDiff = -healthDiff; + if(!innerTargets.possibleAttacks.empty()) + { + innerEvaluator.updateReachabilityMap(state); - if(healthDiff < 0) - { - ps->value = -1; - return; //do not damage own units at all - } + auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); - totalGain += (newValue - oldValue + healthDiff); + ps->value = newStackAction.score; + } + else + { + ps->value = 0; } - - ps->value = totalGain; } else { - ps->value = -1; + ps->value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); + } + + for(auto unit : allUnits) + { + auto newHealth = unit->getAvailableHealth(); + auto oldHealth = healthOfStack[unit->unitId()]; + + if(oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->battleGetUnitByID(unit->unitId()); + auto dpsReduce = AttackPossibility::calculateDamageReduce(nullptr, originalDefender ? originalDefender : unit, damage, innerCache, state); + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + + ps->value += ourUnit * goodEffect * dpsReduce; + } } }; @@ -767,7 +803,7 @@ bool CBattleAI::attemptCastingSpell() }; auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); - if(castToPerform.value > 0) + if(castToPerform.value > cachedScore) { LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); BattleAction spellcast; @@ -777,8 +813,12 @@ bool CBattleAI::attemptCastingSpell() spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; cb->battleMakeSpellAction(spellcast); +<<<<<<< HEAD movesSkippedByDefense = 0; return true; +======= + activeActionMade = true; +>>>>>>> ea22737e9 (BattleAI: damage cache and switch to different model of spells evaluation) } else { @@ -788,7 +828,7 @@ bool CBattleAI::attemptCastingSpell() } //Below method works only for offensive spells -void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) +void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) { using ValueMap = PossibleSpellcast::ValueMap; @@ -849,6 +889,11 @@ void CBattleAI::print(const std::string &text) const logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); } +void BattleEvaluator::print(const std::string & text) const +{ + logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); +} + std::optional CBattleAI::considerFleeingOrSurrendering() { BattleStateInfoForRetreat bs; diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 38551c13b..73bff11c8 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -68,9 +68,6 @@ public: ~CBattleAI(); void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - bool attemptCastingSpell(); - - void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only void activeStack(const CStack * stack) override; //called when it's turn of that stack void yourTacticPhase(int distance) override; @@ -80,8 +77,6 @@ public: void print(const std::string &text) const; BattleAction useCatapult(const CStack *stack); BattleAction useHealingTent(const CStack *stack); - BattleAction selectStackAction(const CStack * stack); - std::optional findBestCreatureSpell(const CStack *stack); void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero @@ -98,8 +93,4 @@ public: //void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack - -private: - BattleAction goTowardsNearest(const CStack * stack, std::vector hexes) const; - std::vector getBrokenWallMoatHexes() const; }; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 05ea71ba9..a53bb646b 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -18,9 +18,11 @@ AttackerValue::AttackerValue() } MoveTarget::MoveTarget() - : positions() + : positions(), cachedAttack() { score = EvaluationResult::INEFFECTIVE_SCORE; + scorePerTurn = EvaluationResult::INEFFECTIVE_SCORE; + turnsToRich = 1; } int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state) @@ -61,14 +63,14 @@ int64_t BattleExchangeVariant::trackAttack( std::shared_ptr defender, bool shooting, bool isOurAttack, - const CBattleInfoCallback & cb, + DamageCache & damageCache, + std::shared_ptr hb, bool evaluateOnly) { const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - DamageEstimation retaliation; // FIXME: provide distance info for Jousting bonus BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); @@ -77,9 +79,8 @@ int64_t BattleExchangeVariant::trackAttack( bai.attackerPos.setXY(8, 5); } - auto attack = cb.battleEstimateDamage(bai, &retaliation); - int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2; - int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb); + int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb); + int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb); int64_t attackerDamageReduce = 0; if(!evaluateOnly) @@ -108,36 +109,33 @@ int64_t BattleExchangeVariant::trackAttack( if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) { - if(retaliation.damage.max != 0) - { - auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2; - attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb); + auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb); + attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb); - if(!evaluateOnly) - { + if(!evaluateOnly) + { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, retaliation, dps: %lld, %lld", - defender->getDescription(), - attacker->getDescription(), - retaliationDamage, - attackerDamageReduce); + logAi->trace( + "%s -> %s, retaliation, dps: %lld, %lld", + defender->getDescription(), + attacker->getDescription(), + retaliationDamage, + attackerDamageReduce); #endif - if(isOurAttack) - { - dpsScore -= attackerDamageReduce; - attackerValue[attacker->unitId()].isRetalitated = true; - } - else - { - dpsScore += attackerDamageReduce; - attackerValue[defender->unitId()].value += attackerDamageReduce; - } - - attacker->damage(retaliationDamage); - defender->afterAttack(false, true); + if(isOurAttack) + { + dpsScore -= attackerDamageReduce; + attackerValue[attacker->unitId()].isRetalitated = true; } + else + { + dpsScore += attackerDamageReduce; + attackerValue[defender->unitId()].value += attackerDamageReduce; + } + + attacker->damage(retaliationDamage); + defender->afterAttack(false, true); } } @@ -153,7 +151,11 @@ int64_t BattleExchangeVariant::trackAttack( return score; } -EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb) +EvaluationResult BattleExchangeEvaluator::findBestTarget( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) { EvaluationResult result(targets.bestAction()); @@ -161,7 +163,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * ac for(auto & ap : targets.possibleAttacks) { - int64_t score = calculateExchange(ap, targets, hb); + int64_t score = calculateExchange(ap, targets, damageCache, hb); if(score > result.score) { @@ -176,14 +178,14 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * ac logAi->trace("Evaluating waited attack for %s", activeStack->getDescription()); #endif - hb.getForUpdate(activeStack->unitId())->waiting = true; - hb.getForUpdate(activeStack->unitId())->waitedThisTurn = true; + hb->getForUpdate(activeStack->unitId())->waiting = true; + hb->getForUpdate(activeStack->unitId())->waitedThisTurn = true; updateReachabilityMap(hb); for(auto & ap : targets.possibleAttacks) { - int64_t score = calculateExchange(ap, targets, hb); + int64_t score = calculateExchange(ap, targets, damageCache, hb); if(score > result.score) { @@ -197,7 +199,11 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * ac return result; } -MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb) +MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) { MoveTarget result; BattleExchangeVariant ev; @@ -237,16 +243,20 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni { // FIXME: provide distance info for Jousting bonus auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); - auto attack = AttackPossibility::evaluate(bai, hex, hb); + auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure - auto score = calculateExchange(attack, targets, hb) / turnsToRich; + auto score = calculateExchange(attack, targets, damageCache, hb); + auto scorePerTurn = score / turnsToRich; - if(result.score < score) + if(result.scorePerTurn < scorePerTurn) { + result.scorePerTurn = scorePerTurn; result.score = score; result.positions = closestStack->getAttackableHexes(activeStack); + result.cachedAttack = attack; + result.turnsToRich = turnsToRich; } } } @@ -287,7 +297,7 @@ std::vector BattleExchangeEvaluator::getAdjacentUnits(cons std::vector BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, PotentialTargets & targets, - HypotheticBattle & hb) + std::shared_ptr hb) { auto hexes = ap.attack.defender->getHexes(); @@ -308,7 +318,7 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( { for(auto adjacentUnit : getAdjacentUnits(unit)) { - auto unitWithBonuses = hb.battleGetUnitByID(adjacentUnit->unitId()); + auto unitWithBonuses = hb->battleGetUnitByID(adjacentUnit->unitId()); if(vstd::contains(targets.unreachableEnemies, adjacentUnit) && !vstd::contains(allReachableUnits, unitWithBonuses)) @@ -349,7 +359,8 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( int64_t BattleExchangeEvaluator::calculateExchange( const AttackPossibility & ap, PotentialTargets & targets, - HypotheticBattle & hb) + DamageCache & damageCache, + std::shared_ptr hb) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from); @@ -374,7 +385,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( return 0; } - HypotheticBattle exchangeBattle(env.get(), cb); + auto exchangeBattle = std::make_shared(env.get(), hb); BattleExchangeVariant v; auto melleeAttackers = ourStacks; @@ -389,10 +400,9 @@ int64_t BattleExchangeEvaluator::calculateExchange( if(unit->isTurret()) continue; - bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true); + bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true); auto & attackerQueue = isOur ? ourStacks : enemyStacks; - if(!vstd::contains(attackerQueue, unit)) { attackerQueue.push_back(unit); @@ -403,11 +413,11 @@ int64_t BattleExchangeEvaluator::calculateExchange( for(auto activeUnit : exchangeUnits) { - bool isOur = cb->battleMatchOwner(ap.attack.attacker, activeUnit, true); + bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; - auto attacker = exchangeBattle.getForUpdate(activeUnit->unitId()); + auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); if(!attacker->alive()) { @@ -420,17 +430,18 @@ int64_t BattleExchangeEvaluator::calculateExchange( auto targetUnit = ap.attack.defender; - if(!isOur || !exchangeBattle.getForUpdate(targetUnit->unitId())->alive()) + if(!isOur || !exchangeBattle->getForUpdate(targetUnit->unitId())->alive()) { auto estimateAttack = [&](const battle::Unit * u) -> int64_t { - auto stackWithBonuses = exchangeBattle.getForUpdate(u->unitId()); + auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto score = v.trackAttack( attacker, stackWithBonuses, - exchangeBattle.battleCanShoot(stackWithBonuses.get()), + exchangeBattle->battleCanShoot(stackWithBonuses.get()), isOur, - *cb, + damageCache, + hb, true); #if BATTLE_TRACE_LEVEL>=1 @@ -446,7 +457,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( } else { - auto reachable = exchangeBattle.battleGetUnitsIf([&](const battle::Unit * u) -> bool + auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool { if(!u->alive() || u->unitSide() == attacker->unitSide()) return false; @@ -472,19 +483,19 @@ int64_t BattleExchangeEvaluator::calculateExchange( } } - auto defender = exchangeBattle.getForUpdate(targetUnit->unitId()); - auto shooting = cb->battleCanShoot(attacker.get()); + auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); + auto shooting = exchangeBattle->battleCanShoot(attacker.get()); const int totalAttacks = attacker->getTotalAttacks(shooting); - if(canUseAp && activeUnit == ap.attack.attacker && targetUnit == ap.attack.defender) + if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() && targetUnit->unitId() == ap.attack.defender->unitId()) { - v.trackAttack(ap, exchangeBattle); + v.trackAttack(ap, *exchangeBattle); } else { for(int i = 0; i < totalAttacks; i++) { - v.trackAttack(attacker, defender, shooting, isOur, exchangeBattle); + v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle); if(!attacker->alive() || !defender->alive()) break; @@ -495,12 +506,12 @@ int64_t BattleExchangeEvaluator::calculateExchange( vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle.getForUpdate(u->unitId())->alive(); + return !exchangeBattle->getForUpdate(u->unitId())->alive(); }); vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle.getForUpdate(u->unitId())->alive(); + return !exchangeBattle->getForUpdate(u->unitId())->alive(); }); } @@ -581,13 +592,13 @@ void BattleExchangeVariant::adjustPositions( } } -void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb) +void BattleExchangeEvaluator::updateReachabilityMap( std::shared_ptr hb) { const int TURN_DEPTH = 2; turnOrder.clear(); - hb.battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); + hb->battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); reachabilityMap.clear(); for(int turn = 0; turn < turnOrder.size(); turn++) diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 2cfba35d0..3d95dd912 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -26,7 +26,10 @@ struct AttackerValue struct MoveTarget { int64_t score; + int64_t scorePerTurn; std::vector positions; + std::optional cachedAttack; + uint8_t turnsToRich; MoveTarget(); }; @@ -65,7 +68,8 @@ public: std::shared_ptr defender, bool shooting, bool isOurAttack, - const CBattleInfoCallback & cb, + DamageCache & damageCache, + std::shared_ptr hb, bool evaluateOnly = false); int64_t getScore() const { return dpsScore; } @@ -91,11 +95,27 @@ private: public: BattleExchangeEvaluator(std::shared_ptr cb, std::shared_ptr env): cb(cb), env(env) {} - EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb); - int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); - void updateReachabilityMap(HypotheticBattle & hb); - std::vector getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); + EvaluationResult findBestTarget( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + int64_t calculateExchange( + const AttackPossibility & ap, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + void updateReachabilityMap(std::shared_ptr hb); + std::vector getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, std::shared_ptr hb); bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); - MoveTarget findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb); + + MoveTarget findMoveTowardsUnreachable( + const battle::Unit * activeStack, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + std::vector getAdjacentUnits(const battle::Unit * unit); }; \ No newline at end of file diff --git a/AI/BattleAI/PotentialTargets.cpp b/AI/BattleAI/PotentialTargets.cpp index 268350b5d..a341921e6 100644 --- a/AI/BattleAI/PotentialTargets.cpp +++ b/AI/BattleAI/PotentialTargets.cpp @@ -11,11 +11,14 @@ #include "PotentialTargets.h" #include "../../lib/CStack.h"//todo: remove -PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state) +PotentialTargets::PotentialTargets( + const battle::Unit * attacker, + DamageCache & damageCache, + std::shared_ptr state) { - auto attackerInfo = state.battleGetUnitByID(attacker->unitId()); - auto reachability = state.getReachability(attackerInfo); - auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, false); + auto attackerInfo = state->battleGetUnitByID(attacker->unitId()); + auto reachability = state->getReachability(attackerInfo); + auto avHexes = state->battleGetAvailableHexes(reachability, attackerInfo, false); //FIXME: this should part of battleGetAvailableHexes bool forceTarget = false; @@ -25,7 +28,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet if(attackerInfo->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) { forceTarget = true; - auto nearest = state.getNearestStack(attackerInfo); + auto nearest = state->getNearestStack(attackerInfo); if(nearest.first != nullptr) { @@ -34,14 +37,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet } } - auto aliveUnits = state.battleGetUnitsIf([=](const battle::Unit * unit) + auto aliveUnits = state->battleGetUnitsIf([=](const battle::Unit * unit) { return unit->isValidTarget() && unit->unitId() != attackerInfo->unitId(); }); for(auto defender : aliveUnits) { - if(!forceTarget && !state.battleMatchOwner(attackerInfo, defender)) + if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender)) continue; auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility @@ -49,7 +52,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet int distance = hex.isValid() ? reachability.distances[hex] : 0; auto bai = BattleAttackInfo(attackerInfo, defender, distance, shooting); - return AttackPossibility::evaluate(bai, hex, state); + return AttackPossibility::evaluate(bai, hex, damageCache, state); }; if(forceTarget) @@ -59,7 +62,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet else unreachableEnemies.push_back(defender); } - else if(state.battleCanShoot(attackerInfo, defender->getPosition())) + else if(state->battleCanShoot(attackerInfo, defender->getPosition())) { possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID)); } diff --git a/AI/BattleAI/PotentialTargets.h b/AI/BattleAI/PotentialTargets.h index fbb855339..e7b622026 100644 --- a/AI/BattleAI/PotentialTargets.h +++ b/AI/BattleAI/PotentialTargets.h @@ -17,7 +17,10 @@ public: std::vector unreachableEnemies; PotentialTargets(){}; - PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state); + PotentialTargets( + const battle::Unit * attacker, + DamageCache & damageCache, + std::shared_ptr hb); const AttackPossibility & bestAction() const; int64_t bestActionValue() const; diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 5370c3aac..3311e8f90 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -124,7 +124,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c for(const Bonus & bonus : bonusesToUpdate) { - if(selector(&bonus) && (!limit || !limit(&bonus))) + if(selector(&bonus) && (!limit || limit(&bonus))) { if(ret->getFirst(Selector::source(BonusSource::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype)))) { @@ -150,12 +150,18 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c int64_t StackWithBonuses::getTreeVersion() const { - return owner->getTreeVersion(); + auto result = owner->getTreeVersion(); + + if(bonusesToAdd.empty() && bonusesToUpdate.empty() && bonusesToRemove.empty()) + return result; + else + return result + treeVersionLocal; } void StackWithBonuses::addUnitBonus(const std::vector & bonus) { vstd::concatenate(bonusesToAdd, bonus); + treeVersionLocal++; } void StackWithBonuses::updateUnitBonus(const std::vector & bonus) @@ -163,6 +169,7 @@ void StackWithBonuses::updateUnitBonus(const std::vector & bonus) //TODO: optimize, actualize to last value vstd::concatenate(bonusesToUpdate, bonus); + treeVersionLocal++; } void StackWithBonuses::removeUnitBonus(const std::vector & bonus) @@ -197,6 +204,8 @@ void StackWithBonuses::removeUnitBonus(const CSelector & selector) vstd::erase_if(bonusesToAdd, [&](const Bonus & b){return selector(&b);}); vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);}); + + treeVersionLocal++; } std::string StackWithBonuses::getDescription() const diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 87743ba6b..024cb4d76 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -47,6 +47,7 @@ public: std::vector bonusesToAdd; std::vector bonusesToUpdate; std::set> bonusesToRemove; + int treeVersionLocal; StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack); diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 8c75b8006..b69073f33 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -138,8 +138,8 @@ void Nullkiller::updateAiState(int pass, bool fast) { memory->removeInvisibleObjects(cb.get()); - dangerHitMap->calculateTileOwners(); dangerHitMap->updateHitMap(); + dangerHitMap->calculateTileOwners(); boost::this_thread::interruption_point(); From 03395a3d8abeffdc67f2990601067034f5bd4ca2 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 13 Aug 2023 13:56:04 +0300 Subject: [PATCH 0192/1248] TBB for battle AI spellcast an fixes --- AI/BattleAI/AttackPossibility.cpp | 7 +- AI/BattleAI/BattleAI.cpp | 147 +++++++++++++----------------- AI/BattleAI/CMakeLists.txt | 6 +- AI/BattleAI/StackWithBonuses.cpp | 19 +++- AI/BattleAI/StackWithBonuses.h | 2 + 5 files changed, 92 insertions(+), 89 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 4a4de7e46..496f73414 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -132,7 +132,12 @@ int64_t AttackPossibility::calculateDamageReduce( { auto ourUnits = state->battleGetUnitsIf([&](const battle::Unit * u) -> bool { - return u->unitSide() != defender->unitSide() && !u->isTurret(); + return u->unitSide() != defender->unitSide() + && !u->isTurret() + && u->creatureId() != CreatureID::CATAPULT + && u->creatureId() != CreatureID::BALLISTA + && u->creatureId() != CreatureID::FIRST_AID_TENT + && u->getCount(); }); if(ourUnits.empty()) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 8da28e145..d5e59b6d7 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -13,6 +13,7 @@ #include "StackWithBonuses.h" #include "EnemyInfo.h" +#include "tbb/parallel_for.h" #include "../../lib/CStopWatch.h" #include "../../lib/CThreadHelper.h" #include "../../lib/mapObjects/CGTownInstance.h" @@ -704,96 +705,70 @@ void BattleEvaluator::attemptCastingSpell(const CStack * activeStack) } } - struct ScriptsCache - { - //todo: re-implement scripts context cache - }; - - auto evaluateSpellcast = [&] (PossibleSpellcast * ps, std::shared_ptr) - { - auto state = std::make_shared(env.get(), cb); - - spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps->spell); - cast.castEval(state->getServerCallback(), ps->dest); - - auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool{ return true; }); - - auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool - { - auto original = cb->battleGetUnitByID(u->unitId()); - return !original || u->speed() != original->speed(); - }); - - DamageCache innerCache(&damageCache); - innerCache.buildDamageCache(state, side); - - if(needFullEval || !cachedAttack) - { - PotentialTargets innerTargets(activeStack, damageCache, state); - BattleExchangeEvaluator innerEvaluator(state, env); - - if(!innerTargets.possibleAttacks.empty()) - { - innerEvaluator.updateReachabilityMap(state); - - auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); - - ps->value = newStackAction.score; - } - else - { - ps->value = 0; - } - } - else - { - ps->value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); - } - - for(auto unit : allUnits) - { - auto newHealth = unit->getAvailableHealth(); - auto oldHealth = healthOfStack[unit->unitId()]; - - if(oldHealth != newHealth) - { - auto damage = std::abs(oldHealth - newHealth); - auto originalDefender = cb->battleGetUnitByID(unit->unitId()); - auto dpsReduce = AttackPossibility::calculateDamageReduce(nullptr, originalDefender ? originalDefender : unit, damage, innerCache, state); - auto ourUnit = unit->unitSide() == side ? 1 : -1; - auto goodEffect = newHealth > oldHealth ? 1 : -1; - - ps->value += ourUnit * goodEffect * dpsReduce; - } - } - }; - - using EvalRunner = ThreadPool; - - EvalRunner::Tasks tasks; - - for(PossibleSpellcast & psc : possibleCasts) - tasks.push_back(std::bind(evaluateSpellcast, &psc, _1)); - - uint32_t threadCount = boost::thread::hardware_concurrency(); - - if(threadCount == 0) - { - logGlobal->warn("No information of CPU cores available"); - threadCount = 1; - } - CStopWatch timer; - std::vector> scriptsPool; + tbb::parallel_for(tbb::blocked_range(0, possibleCasts.size()), [&](const tbb::blocked_range & r) + { + for(auto i = r.begin(); i != r.end(); i++) + { + auto & ps = possibleCasts[i]; + auto state = std::make_shared(env.get(), cb); - for(uint32_t idx = 0; idx < threadCount; idx++) - { - scriptsPool.emplace_back(); - } + spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); + cast.castEval(state->getServerCallback(), ps.dest); - EvalRunner runner(&tasks, scriptsPool); - runner.run(); + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); + + auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool + { + auto original = cb->battleGetUnitByID(u->unitId()); + return !original || u->speed() != original->speed(); + }); + + DamageCache innerCache(&damageCache); + innerCache.buildDamageCache(state, side); + + if(needFullEval || !cachedAttack) + { + PotentialTargets innerTargets(activeStack, damageCache, state); + BattleExchangeEvaluator innerEvaluator(state, env); + + if(!innerTargets.possibleAttacks.empty()) + { + innerEvaluator.updateReachabilityMap(state); + + auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); + + ps.value = newStackAction.score; + } + else + { + ps.value = 0; + } + } + else + { + ps.value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); + } + + for(auto unit : allUnits) + { + auto newHealth = unit->getAvailableHealth(); + auto oldHealth = healthOfStack[unit->unitId()]; + + if(oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->battleGetUnitByID(unit->unitId()); + auto dpsReduce = AttackPossibility::calculateDamageReduce(nullptr, originalDefender ? originalDefender : unit, damage, innerCache, state); + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + + ps.value += ourUnit * goodEffect * dpsReduce; + } + } + } + }); LOGFL("Evaluation took %d ms", timer.getDiff()); diff --git a/AI/BattleAI/CMakeLists.txt b/AI/BattleAI/CMakeLists.txt index 7feed93e2..1850e24f1 100644 --- a/AI/BattleAI/CMakeLists.txt +++ b/AI/BattleAI/CMakeLists.txt @@ -37,7 +37,11 @@ else() endif() target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) -target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET}) +target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb) vcmi_set_output_dir(BattleAI "AI") enable_pch(BattleAI) + +if(APPLE_IOS AND NOT USING_CONAN) + install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+ +endif() diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 3311e8f90..c77e2b4fc 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -52,6 +52,23 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: battle::CUnitState::operator=(*Stack); } +StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack) + : battle::CUnitState(), + origBearer(Stack->getBonusBearer()), + owner(Owner), + type(Stack->unitType()), + baseAmount(Stack->unitBaseAmount()), + id(Stack->unitId()), + side(Stack->unitSide()), + player(Stack->unitOwner()), + slot(Stack->unitSlot()) +{ + localInit(Owner); + + auto state = Stack->acquireState(); + battle::CUnitState::operator=(*state); +} + StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info) : battle::CUnitState(), origBearer(nullptr), @@ -265,7 +282,7 @@ std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) if(iter == stackStates.end()) { - const CStack * s = subject->battleGetStackByID(id, false); + const battle::Unit * s = subject->battleGetUnitByID(id); auto ret = std::make_shared(this, s); stackStates[id] = ret; diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index 024cb4d76..c7692434e 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -51,6 +51,8 @@ public: StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack); + StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack); + StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info); virtual ~StackWithBonuses(); From dc88f14e0b9110a12137c137b8805c30056738f8 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 19 Aug 2023 10:22:34 +0300 Subject: [PATCH 0193/1248] BattleAI: positive/negative effect multiplier --- AI/BattleAI/AttackPossibility.cpp | 27 +- AI/BattleAI/AttackPossibility.h | 1 + AI/BattleAI/BattleAI.cpp | 697 +------------------------- AI/BattleAI/BattleAI.h | 2 + AI/BattleAI/BattleEvaluator.cpp | 679 +++++++++++++++++++++++++ AI/BattleAI/BattleEvaluator.h | 80 +++ AI/BattleAI/BattleExchangeVariant.cpp | 50 +- AI/BattleAI/BattleExchangeVariant.h | 16 +- AI/BattleAI/CMakeLists.txt | 2 + 9 files changed, 852 insertions(+), 702 deletions(-) create mode 100644 AI/BattleAI/BattleEvaluator.cpp create mode 100644 AI/BattleAI/BattleEvaluator.h diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 496f73414..ce1803c34 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -103,6 +103,12 @@ int64_t AttackPossibility::damageDiff() const return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg; } +int64_t AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const +{ + return positiveEffectMultiplier * (defenderDamageReduce + shootersBlockedDmg) + - negativeEffectMultiplier * (attackerDamageReduce + collateralDamageReduce); +} + int64_t AttackPossibility::attackValue() const { return damageDiff(); @@ -121,9 +127,6 @@ int64_t AttackPossibility::calculateDamageReduce( std::shared_ptr state) { const float HEALTH_BOUNTY = 0.5; - const float KILL_BOUNTY = 1.0 - HEALTH_BOUNTY; - - vstd::amin(damageDealt, defender->getAvailableHealth()); // FIXME: provide distance info for Jousting bonus auto attackerUnitForMeasurement = attacker; @@ -146,11 +149,21 @@ int64_t AttackPossibility::calculateDamageReduce( attackerUnitForMeasurement = ourUnits.front(); } - auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); - auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0); - auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); + auto maxHealth = defender->getMaxHealth(); + auto availableHealth = defender->getFirstHPleft() + ((defender->getCount() - 1) * maxHealth); - return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth())); + vstd::amin(damageDealt, availableHealth); + + auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); + auto enemiesKilled = damageDealt / maxHealth + (damageDealt % maxHealth >= defender->getFirstHPleft() ? 1 : 0); + auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); + + // lets use cached maxHealth here instead of getAvailableHealth + auto firstUnitHpLeft = (availableHealth - damageDealt) % maxHealth; + auto firstUnitHealthRatio = firstUnitHpLeft == 0 ? 1 : static_cast(firstUnitHpLeft) / maxHealth; + auto firstUnitKillValue = (1 - firstUnitHealthRatio) * (1 - firstUnitHealthRatio); + + return (int64_t)(damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY)); } int64_t AttackPossibility::evaluateBlockedShootersDmg( diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index f96f59a6d..cd28839f4 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -55,6 +55,7 @@ public: int64_t damageDiff() const; int64_t attackValue() const; + int64_t damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const; static AttackPossibility evaluate( const BattleAttackInfo & attackInfo, diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index d5e59b6d7..60e2729f6 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" #include "BattleAI.h" +#include "BattleEvaluator.h" #include "BattleExchangeVariant.h" #include "StackWithBonuses.h" @@ -29,90 +30,6 @@ #define LOGL(text) print(text) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) -enum class SpellTypes -{ - ADVENTURE, BATTLE, OTHER -}; - -SpellTypes spellType(const CSpell * spell) -{ - if(!spell->isCombat() || spell->isCreatureAbility()) - return SpellTypes::OTHER; - - if(spell->isOffensive() || spell->hasEffects() || spell->hasBattleEffects()) - return SpellTypes::BATTLE; - - return SpellTypes::OTHER; -} - -class BattleEvaluator -{ - std::unique_ptr targets; - std::shared_ptr hb; - BattleExchangeEvaluator scoreEvaluator; - std::shared_ptr cb; - std::shared_ptr env; - bool activeActionMade = false; - std::optional cachedAttack; - PlayerColor playerID; - int side; - int64_t cachedScore; - DamageCache damageCache; - -public: - BattleAction selectStackAction(const CStack * stack); - void attemptCastingSpell(const CStack * stack); - std::optional findBestCreatureSpell(const CStack * stack); - BattleAction goTowardsNearest(const CStack * stack, std::vector hexes); - std::vector getBrokenWallMoatHexes() const; - void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only - void print(const std::string & text) const; - - BattleEvaluator(std::shared_ptr env, std::shared_ptr cb, const battle::Unit * activeStack, PlayerColor playerID, int side) - :scoreEvaluator(cb, env), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb) - { - hb = std::make_shared(env.get(), cb); - damageCache.buildDamageCache(hb, side); - - targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; - } - - BattleEvaluator( - std::shared_ptr env, - std::shared_ptr cb, - std::shared_ptr hb, - DamageCache & damageCache, - const battle::Unit * activeStack, - PlayerColor playerID, - int side) - :scoreEvaluator(cb, env), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache) - { - targets = std::make_unique(activeStack, damageCache, hb); - cachedScore = EvaluationResult::INEFFECTIVE_SCORE; - } -}; - -std::vector BattleEvaluator::getBrokenWallMoatHexes() const -{ - std::vector result; - - for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL }) - { - auto state = cb->battleGetWallState(wallPart); - - if(state != EWallState::DESTROYED) - continue; - - auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart); - auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); - - result.push_back(moatHex); - } - - return result; -} - CBattleAI::CBattleAI() : side(-1), wasWaitingForRealize(false), @@ -159,161 +76,22 @@ BattleAction CBattleAI::useHealingTent(const CStack *stack) return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack } -std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) -{ - //TODO: faerie dragon type spell should be selected by server - SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); - if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) - { - const CSpell * spell = creatureSpellToCast.toSpell(); - - if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) - { - std::vector possibleCasts; - spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); - for(auto & target : temp.findPotentialTargets()) - { - PossibleSpellcast ps; - ps.dest = target; - ps.spell = spell; - evaluateCreatureSpellcast(stack, ps); - possibleCasts.push_back(ps); - } - - std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); - if(!possibleCasts.empty() && possibleCasts.front().value > 0) - { - return possibleCasts.front(); - } - } - } - return std::nullopt; -} - -BattleAction BattleEvaluator::selectStackAction(const CStack * stack) -{ - //evaluate casting spell for spellcasting stack - std::optional bestSpellcast = findBestCreatureSpell(stack); - - auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); - auto score = EvaluationResult::INEFFECTIVE_SCORE; - - if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) - { - activeActionMade = true; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - if(!targets->possibleAttacks.empty()) - { -#if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Evaluating attack for %s", stack->getDescription()); -#endif - - auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); - auto & bestAttack = evaluationResult.bestAttack; - - cachedAttack = bestAttack; - cachedScore = evaluationResult.score; - - //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. - if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) - { - // return because spellcast value is damage dealt and score is dps reduce - activeActionMade = true; - return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); - } - - if(evaluationResult.score > score) - { - score = evaluationResult.score; - - logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", - bestAttack.attackerState->unitType()->getJsonKey(), - bestAttack.affectedUnits[0]->unitType()->getJsonKey(), - (int)bestAttack.affectedUnits[0]->getCount(), - (int)bestAttack.from, - (int)bestAttack.attack.attacker->getPosition().hex, - bestAttack.attack.chargeDistance, - bestAttack.attack.attacker->speed(0, true), - bestAttack.defenderDamageReduce, - bestAttack.attackerDamageReduce, bestAttack.attackValue() - ); - - if (moveTarget.scorePerTurn <= score) - { - if(evaluationResult.wait) - { - return BattleAction::makeWait(stack); - } - else if(bestAttack.attack.shooting) - { - activeActionMade = true; - return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); - } - else - { - activeActionMade = true; - return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); - } - } - } - } - - //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. - if(moveTarget.scorePerTurn > score) - { - score = moveTarget.score; - cachedAttack = moveTarget.cachedAttack; - cachedScore = score; - - if(stack->waited()) - { - return goTowardsNearest(stack, moveTarget.positions); - } - else - { - return BattleAction::makeWait(stack); - } - } - - if(score <= EvaluationResult::INEFFECTIVE_SCORE - && !stack->hasBonusOfType(BonusType::FLYING) - && stack->unitSide() == BattleSide::ATTACKER - && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) - { - auto brokenWallMoat = getBrokenWallMoatHexes(); - - if(brokenWallMoat.size()) - { - activeActionMade = true; - - if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) - return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); - else - return goTowardsNearest(stack, brokenWallMoat); - } - } - - return BattleAction::makeDefend(stack); -} - void CBattleAI::yourTacticPhase(int distance) { cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } -uint64_t timeElapsed(std::chrono::time_point start) -{ - auto end = std::chrono::high_resolution_clock::now(); - - return std::chrono::duration_cast(end - start).count(); -} - void CBattleAI::activeStack( const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); + auto timeElapsed = [](std::chrono::time_point start) -> uint64_t + { + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); + }; + BattleAction result = BattleAction::makeDefend(stack); setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) @@ -332,12 +110,19 @@ void CBattleAI::activeStack( const CStack * stack ) return; } - BattleEvaluator evaluator(env, cb, stack, playerID, side); + BattleEvaluator evaluator(env, cb, stack, playerID, side, strengthRatio); result = evaluator.selectStackAction(stack); - if(evaluator.attemptCastingSpell(stack)) - return; + if(!skipCastUntilNextBattle && evaluator.canCastSpell()) + { + auto spelCasted = evaluator.attemptCastingSpell(stack); + + if(spelCasted) + return; + + skipCastUntilNextBattle = true; + } logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); @@ -370,103 +155,6 @@ void CBattleAI::activeStack( const CStack * stack ) cb->battleMakeUnitAction(result); } -BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) -{ - auto reachability = cb->getReachability(stack); - auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); - - if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked - { - return BattleAction::makeDefend(stack); - } - - std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool - { - return reachability.distances[h1] < reachability.distances[h2]; - }); - - for(auto hex : hexes) - { - if(vstd::contains(avHexes, hex)) - { - return BattleAction::makeMove(stack, hex); - } - - if(stack->coversPos(hex)) - { - logAi->warn("Warning: already standing on neighbouring tile!"); - //We shouldn't even be here... - return BattleAction::makeDefend(stack); - } - } - - BattleHex bestNeighbor = hexes.front(); - - if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) - { - return BattleAction::makeDefend(stack); - } - - scoreEvaluator.updateReachabilityMap(hb); - - if(stack->hasBonusOfType(BonusType::FLYING)) - { - std::set obstacleHexes; - - auto insertAffected = [](const CObstacleInstance & spellObst, std::set obstacleHexes) { - auto affectedHexes = spellObst.getAffectedTiles(); - obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); - }; - - const auto & obstacles = hb->battleGetAllObstacles(); - - for (const auto & obst: obstacles) { - - if(obst->triggersEffects()) - { - auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); - auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); - - if(triggerIsNegative) - insertAffected(*obst, obstacleHexes); - } - } - // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. - // We just check all available hexes and pick the one closest to the target. - auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int - { - const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc) - const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat - - auto distance = BattleHex::getDistance(bestNeighbor, hex); - - if(vstd::contains(obstacleHexes, hex)) - distance += NEGATIVE_OBSTACLE_PENALTY; - - return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; - }); - - return BattleAction::makeMove(stack, *nearestAvailableHex); - } - else - { - BattleHex currentDest = bestNeighbor; - while(1) - { - if(!currentDest.isValid()) - { - return BattleAction::makeDefend(stack); - } - - if(vstd::contains(avHexes, currentDest) - && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) - return BattleAction::makeMove(stack, currentDest); - - currentDest = reachability.predecessors[currentDest]; - } - } -} - BattleAction CBattleAI::useCatapult(const CStack * stack) { BattleAction attack; @@ -515,348 +203,16 @@ BattleAction CBattleAI::useCatapult(const CStack * stack) return attack; } -<<<<<<< HEAD -bool CBattleAI::attemptCastingSpell() -======= -void BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ->>>>>>> ea22737e9 (BattleAI: damage cache and switch to different model of spells evaluation) -{ - auto hero = cb->battleGetMyHero(); - if(!hero) - return false; - - if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK) - return false; - - LOGL("Casting spells sounds like fun. Let's see..."); - //Get all spells we can cast - std::vector possibleSpells; - vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool - { - return s->canBeCast(cb.get(), spells::Mode::HERO, hero); - }); - LOGFL("I can cast %d spells.", possibleSpells.size()); - - vstd::erase_if(possibleSpells, [](const CSpell *s) - { - return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; - }); - - LOGFL("I know how %d of them works.", possibleSpells.size()); - - //Get possible spell-target pairs - std::vector possibleCasts; - for(auto spell : possibleSpells) - { - spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); - - if(spell->getTargetType() == spells::AimType::LOCATION) - continue; - - const bool FAST = true; - - for(auto & target : temp.findPotentialTargets(FAST)) - { - PossibleSpellcast ps; - ps.dest = target; - ps.spell = spell; - possibleCasts.push_back(ps); - } - } - LOGFL("Found %d spell-target combinations.", possibleCasts.size()); - if(possibleCasts.empty()) - return false; - - using ValueMap = PossibleSpellcast::ValueMap; - - auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, std::shared_ptr state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool - { - bool firstRound = true; - bool enemyHadTurn = false; - size_t ourTurnSpan = 0; - - bool stop = false; - - for(auto & round : queue) - { - if(!firstRound) - state->nextRound(0);//todo: set actual value? - for(auto unit : round) - { - if(!vstd::contains(values, unit->unitId())) - values[unit->unitId()] = 0; - - if(!unit->alive()) - continue; - - if(state->battleGetOwner(unit) != playerID) - { - enemyHadTurn = true; - - if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0) - { - //enemy could counter our spell at this point - //anyway, we do not know what enemy will do - //just stop evaluation - stop = true; - break; - } - } - else if(!enemyHadTurn) - { - ourTurnSpan++; - } - - state->nextTurn(unit->unitId()); - - PotentialTargets pt(unit, damageCache, state); - - if(!pt.possibleAttacks.empty()) - { - AttackPossibility ap = pt.bestAction(); - - auto swb = state->getForUpdate(unit->unitId()); - *swb = *ap.attackerState; - - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilAttack); - if(ap.attackerDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - - for(auto affected : ap.affectedUnits) - { - swb = state->getForUpdate(affected->unitId()); - *swb = *affected; - - if(ap.defenderDamageReduce > 0) - swb->removeUnitBonus(Bonus::UntilBeingAttacked); - if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId()) - swb->removeUnitBonus(Bonus::UntilAttack); - } - } - - auto bav = pt.bestActionValue(); - - //best action is from effective owner`s point if view, we need to convert to our point if view - if(state->battleGetOwner(unit) != playerID) - bav = -bav; - values[unit->unitId()] += bav; - } - - firstRound = false; - - if(stop) - break; - } - - if(enemyHadTurnOut) - *enemyHadTurnOut = enemyHadTurn; - - return ourTurnSpan >= minTurnSpan; - }; - - ValueMap valueOfStack; - ValueMap healthOfStack; - - TStacks all = cb->battleGetAllStacks(false); - - size_t ourRemainingTurns = 0; - - for(auto unit : all) - { - healthOfStack[unit->unitId()] = unit->getAvailableHealth(); - valueOfStack[unit->unitId()] = 0; - - if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) - ourRemainingTurns++; - } - - LOGFL("I have %d turns left in this round", ourRemainingTurns); - - const bool castNow = ourRemainingTurns <= 1; - - if(castNow) - print("I should try to cast a spell now"); - else - print("I could wait better moment to cast a spell"); - - auto amount = all.size(); - - std::vector turnOrder; - - cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once - - { - bool enemyHadTurn = false; - - auto state = std::make_shared(env.get(), cb); - - evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); - - if(!enemyHadTurn) - { - auto battleIsFinishedOpt = state->battleIsFinished(); - - if(battleIsFinishedOpt) - { - print("No need to cast a spell. Battle will finish soon."); - return false; - } - } - } - - CStopWatch timer; - - tbb::parallel_for(tbb::blocked_range(0, possibleCasts.size()), [&](const tbb::blocked_range & r) - { - for(auto i = r.begin(); i != r.end(); i++) - { - auto & ps = possibleCasts[i]; - auto state = std::make_shared(env.get(), cb); - - spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); - cast.castEval(state->getServerCallback(), ps.dest); - - auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); - - auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool - { - auto original = cb->battleGetUnitByID(u->unitId()); - return !original || u->speed() != original->speed(); - }); - - DamageCache innerCache(&damageCache); - innerCache.buildDamageCache(state, side); - - if(needFullEval || !cachedAttack) - { - PotentialTargets innerTargets(activeStack, damageCache, state); - BattleExchangeEvaluator innerEvaluator(state, env); - - if(!innerTargets.possibleAttacks.empty()) - { - innerEvaluator.updateReachabilityMap(state); - - auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); - - ps.value = newStackAction.score; - } - else - { - ps.value = 0; - } - } - else - { - ps.value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); - } - - for(auto unit : allUnits) - { - auto newHealth = unit->getAvailableHealth(); - auto oldHealth = healthOfStack[unit->unitId()]; - - if(oldHealth != newHealth) - { - auto damage = std::abs(oldHealth - newHealth); - auto originalDefender = cb->battleGetUnitByID(unit->unitId()); - auto dpsReduce = AttackPossibility::calculateDamageReduce(nullptr, originalDefender ? originalDefender : unit, damage, innerCache, state); - auto ourUnit = unit->unitSide() == side ? 1 : -1; - auto goodEffect = newHealth > oldHealth ? 1 : -1; - - ps.value += ourUnit * goodEffect * dpsReduce; - } - } - } - }); - - LOGFL("Evaluation took %d ms", timer.getDiff()); - - auto pscValue = [](const PossibleSpellcast &ps) -> int64_t - { - return ps.value; - }; - auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); - - if(castToPerform.value > cachedScore) - { - LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); - BattleAction spellcast; - spellcast.actionType = EActionType::HERO_SPELL; - spellcast.spell = castToPerform.spell->getId(); - spellcast.setTarget(castToPerform.dest); - spellcast.side = side; - spellcast.stackNumber = (!side) ? -1 : -2; - cb->battleMakeSpellAction(spellcast); -<<<<<<< HEAD - movesSkippedByDefense = 0; - return true; -======= - activeActionMade = true; ->>>>>>> ea22737e9 (BattleAI: damage cache and switch to different model of spells evaluation) - } - else - { - LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); - return false; - } -} - -//Below method works only for offensive spells -void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) -{ - using ValueMap = PossibleSpellcast::ValueMap; - - RNGStub rngStub; - HypotheticBattle state(env.get(), cb); - TStacks all = cb->battleGetAllStacks(false); - - ValueMap healthOfStack; - ValueMap newHealthOfStack; - - for(auto unit : all) - { - healthOfStack[unit->unitId()] = unit->getAvailableHealth(); - } - - spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell); - cast.castEval(state.getServerCallback(), ps.dest); - - for(auto unit : all) - { - auto unitId = unit->unitId(); - auto localUnit = state.battleGetUnitByID(unitId); - newHealthOfStack[unitId] = localUnit->getAvailableHealth(); - } - - int64_t totalGain = 0; - - for(auto unit : all) - { - auto unitId = unit->unitId(); - auto localUnit = state.battleGetUnitByID(unitId); - - auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; - - if(localUnit->unitOwner() != getCbc()->getPlayerID()) - healthDiff = -healthDiff; - - if(healthDiff < 0) - { - ps.value = -1; - return; //do not damage own units at all - } - - totalGain += healthDiff; - } - - ps.value = totalGain; -} - void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) { LOG_TRACE(logAi); side = Side; + strengthRatio = static_cast(army1->getArmyStrength()) / static_cast(army2->getArmyStrength()); + + if(side == 1) + strengthRatio = 1 / strengthRatio; + + skipCastUntilNextBattle = false; } void CBattleAI::print(const std::string &text) const @@ -864,11 +220,6 @@ void CBattleAI::print(const std::string &text) const logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); } -void BattleEvaluator::print(const std::string & text) const -{ - logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); -} - std::optional CBattleAI::considerFleeingOrSurrendering() { BattleStateInfoForRetreat bs; diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 73bff11c8..9c60a32b4 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -62,6 +62,8 @@ class CBattleAI : public CBattleGameInterface bool wasWaitingForRealize; bool wasUnlockingGs; int movesSkippedByDefense; + float strengthRatio; + bool skipCastUntilNextBattle; public: CBattleAI(); diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp new file mode 100644 index 000000000..d5d101f1e --- /dev/null +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -0,0 +1,679 @@ +/* + * BattleAI.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 "BattleEvaluator.h" +#include "BattleExchangeVariant.h" + +#include "StackWithBonuses.h" +#include "EnemyInfo.h" +#include "tbb/parallel_for.h" +#include "../../lib/CStopWatch.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleStateInfoForRetreat.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/battle/BattleAction.h" + +// TODO: remove +// Eventually only IBattleInfoCallback and battle::Unit should be used, +// CUnitState should be private and CStack should be removed completely +#include "../../lib/CStack.h" + +#define LOGL(text) print(text) +#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) + +enum class SpellTypes +{ + ADVENTURE, BATTLE, OTHER +}; + +SpellTypes spellType(const CSpell * spell) +{ + if(!spell->isCombat() || spell->isCreatureAbility()) + return SpellTypes::OTHER; + + if(spell->isOffensive() || spell->hasEffects() || spell->hasBattleEffects()) + return SpellTypes::BATTLE; + + return SpellTypes::OTHER; +} + +std::vector BattleEvaluator::getBrokenWallMoatHexes() const +{ + std::vector result; + + for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL }) + { + auto state = cb->battleGetWallState(wallPart); + + if(state != EWallState::DESTROYED) + continue; + + auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart); + auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); + + result.push_back(moatHex); + } + + return result; +} + +std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) +{ + //TODO: faerie dragon type spell should be selected by server + SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); + if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) + { + const CSpell * spell = creatureSpellToCast.toSpell(); + + if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) + { + std::vector possibleCasts; + spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); + for(auto & target : temp.findPotentialTargets()) + { + PossibleSpellcast ps; + ps.dest = target; + ps.spell = spell; + evaluateCreatureSpellcast(stack, ps); + possibleCasts.push_back(ps); + } + + std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; }); + if(!possibleCasts.empty() && possibleCasts.front().value > 0) + { + return possibleCasts.front(); + } + } + } + return std::nullopt; +} + +BattleAction BattleEvaluator::selectStackAction(const CStack * stack) +{ + //evaluate casting spell for spellcasting stack + std::optional bestSpellcast = findBestCreatureSpell(stack); + + auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); + auto score = EvaluationResult::INEFFECTIVE_SCORE; + + if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) + { + activeActionMade = true; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(!targets->possibleAttacks.empty()) + { +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Evaluating attack for %s", stack->getDescription()); +#endif + + auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); + auto & bestAttack = evaluationResult.bestAttack; + + cachedAttack = bestAttack; + cachedScore = evaluationResult.score; + + //TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc. + if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff()) + { + // return because spellcast value is damage dealt and score is dps reduce + activeActionMade = true; + return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id); + } + + if(evaluationResult.score > score) + { + score = evaluationResult.score; + + logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", + bestAttack.attackerState->unitType()->getJsonKey(), + bestAttack.affectedUnits[0]->unitType()->getJsonKey(), + (int)bestAttack.affectedUnits[0]->getCount(), + (int)bestAttack.from, + (int)bestAttack.attack.attacker->getPosition().hex, + bestAttack.attack.chargeDistance, + bestAttack.attack.attacker->speed(0, true), + bestAttack.defenderDamageReduce, + bestAttack.attackerDamageReduce, bestAttack.attackValue() + ); + + if (moveTarget.scorePerTurn <= score) + { + if(evaluationResult.wait) + { + return BattleAction::makeWait(stack); + } + else if(bestAttack.attack.shooting) + { + activeActionMade = true; + return BattleAction::makeShotAttack(stack, bestAttack.attack.defender); + } + else + { + if(bestAttack.collateralDamageReduce + && bestAttack.collateralDamageReduce >= bestAttack.defenderDamageReduce / 2 + && score < 0) + { + return BattleAction::makeDefend(stack); + } + else + { + activeActionMade = true; + return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from); + } + } + } + } + } + + //ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code. + if(moveTarget.scorePerTurn > score) + { + score = moveTarget.score; + cachedAttack = moveTarget.cachedAttack; + cachedScore = score; + + if(stack->waited()) + { + return goTowardsNearest(stack, moveTarget.positions); + } + else + { + return BattleAction::makeWait(stack); + } + } + + if(score <= EvaluationResult::INEFFECTIVE_SCORE + && !stack->hasBonusOfType(BonusType::FLYING) + && stack->unitSide() == BattleSide::ATTACKER + && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + { + auto brokenWallMoat = getBrokenWallMoatHexes(); + + if(brokenWallMoat.size()) + { + activeActionMade = true; + + if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) + return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); + else + return goTowardsNearest(stack, brokenWallMoat); + } + } + + return BattleAction::makeDefend(stack); +} + +uint64_t timeElapsed(std::chrono::time_point start) +{ + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); +} + +BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) +{ + auto reachability = cb->getReachability(stack); + auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); + + if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked + { + return BattleAction::makeDefend(stack); + } + + std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool + { + return reachability.distances[h1] < reachability.distances[h2]; + }); + + for(auto hex : hexes) + { + if(vstd::contains(avHexes, hex)) + { + return BattleAction::makeMove(stack, hex); + } + + if(stack->coversPos(hex)) + { + logAi->warn("Warning: already standing on neighbouring tile!"); + //We shouldn't even be here... + return BattleAction::makeDefend(stack); + } + } + + BattleHex bestNeighbor = hexes.front(); + + if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) + { + return BattleAction::makeDefend(stack); + } + + scoreEvaluator.updateReachabilityMap(hb); + + if(stack->hasBonusOfType(BonusType::FLYING)) + { + std::set obstacleHexes; + + auto insertAffected = [](const CObstacleInstance & spellObst, std::set obstacleHexes) { + auto affectedHexes = spellObst.getAffectedTiles(); + obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); + }; + + const auto & obstacles = hb->battleGetAllObstacles(); + + for (const auto & obst: obstacles) { + + if(obst->triggersEffects()) + { + auto triggerAbility = VLC->spells()->getById(obst->getTrigger()); + auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage(); + + if(triggerIsNegative) + insertAffected(*obst, obstacleHexes); + } + } + // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. + // We just check all available hexes and pick the one closest to the target. + auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int + { + const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc) + const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat + + auto distance = BattleHex::getDistance(bestNeighbor, hex); + + if(vstd::contains(obstacleHexes, hex)) + distance += NEGATIVE_OBSTACLE_PENALTY; + + return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance; + }); + + return BattleAction::makeMove(stack, *nearestAvailableHex); + } + else + { + BattleHex currentDest = bestNeighbor; + while(1) + { + if(!currentDest.isValid()) + { + return BattleAction::makeDefend(stack); + } + + if(vstd::contains(avHexes, currentDest) + && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) + return BattleAction::makeMove(stack, currentDest); + + currentDest = reachability.predecessors[currentDest]; + } + } +} + +bool BattleEvaluator::canCastSpell() +{ + auto hero = cb->battleGetMyHero(); + if(!hero) + return false; + + return cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK; +} + +bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) +{ + auto hero = cb->battleGetMyHero(); + if(!hero) + return false; + + LOGL("Casting spells sounds like fun. Let's see..."); + //Get all spells we can cast + std::vector possibleSpells; + vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool + { + return s->canBeCast(cb.get(), spells::Mode::HERO, hero); + }); + LOGFL("I can cast %d spells.", possibleSpells.size()); + + vstd::erase_if(possibleSpells, [](const CSpell *s) + { + return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION; + }); + + LOGFL("I know how %d of them works.", possibleSpells.size()); + + //Get possible spell-target pairs + std::vector possibleCasts; + for(auto spell : possibleSpells) + { + spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); + + if(spell->getTargetType() == spells::AimType::LOCATION) + continue; + + const bool FAST = true; + + for(auto & target : temp.findPotentialTargets(FAST)) + { + PossibleSpellcast ps; + ps.dest = target; + ps.spell = spell; + possibleCasts.push_back(ps); + } + } + LOGFL("Found %d spell-target combinations.", possibleCasts.size()); + if(possibleCasts.empty()) + return false; + + using ValueMap = PossibleSpellcast::ValueMap; + + auto evaluateQueue = [&](ValueMap & values, const std::vector & queue, std::shared_ptr state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool + { + bool firstRound = true; + bool enemyHadTurn = false; + size_t ourTurnSpan = 0; + + bool stop = false; + + for(auto & round : queue) + { + if(!firstRound) + state->nextRound(0);//todo: set actual value? + for(auto unit : round) + { + if(!vstd::contains(values, unit->unitId())) + values[unit->unitId()] = 0; + + if(!unit->alive()) + continue; + + if(state->battleGetOwner(unit) != playerID) + { + enemyHadTurn = true; + + if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0) + { + //enemy could counter our spell at this point + //anyway, we do not know what enemy will do + //just stop evaluation + stop = true; + break; + } + } + else if(!enemyHadTurn) + { + ourTurnSpan++; + } + + state->nextTurn(unit->unitId()); + + PotentialTargets pt(unit, damageCache, state); + + if(!pt.possibleAttacks.empty()) + { + AttackPossibility ap = pt.bestAction(); + + auto swb = state->getForUpdate(unit->unitId()); + *swb = *ap.attackerState; + + if(ap.defenderDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilAttack); + if(ap.attackerDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilBeingAttacked); + + for(auto affected : ap.affectedUnits) + { + swb = state->getForUpdate(affected->unitId()); + *swb = *affected; + + if(ap.defenderDamageReduce > 0) + swb->removeUnitBonus(Bonus::UntilBeingAttacked); + if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId()) + swb->removeUnitBonus(Bonus::UntilAttack); + } + } + + auto bav = pt.bestActionValue(); + + //best action is from effective owner`s point if view, we need to convert to our point if view + if(state->battleGetOwner(unit) != playerID) + bav = -bav; + values[unit->unitId()] += bav; + } + + firstRound = false; + + if(stop) + break; + } + + if(enemyHadTurnOut) + *enemyHadTurnOut = enemyHadTurn; + + return ourTurnSpan >= minTurnSpan; + }; + + ValueMap valueOfStack; + ValueMap healthOfStack; + + TStacks all = cb->battleGetAllStacks(false); + + size_t ourRemainingTurns = 0; + + for(auto unit : all) + { + healthOfStack[unit->unitId()] = unit->getAvailableHealth(); + valueOfStack[unit->unitId()] = 0; + + if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) + ourRemainingTurns++; + } + + LOGFL("I have %d turns left in this round", ourRemainingTurns); + + const bool castNow = ourRemainingTurns <= 1; + + if(castNow) + print("I should try to cast a spell now"); + else + print("I could wait better moment to cast a spell"); + + auto amount = all.size(); + + std::vector turnOrder; + + cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once + + { + bool enemyHadTurn = false; + + auto state = std::make_shared(env.get(), cb); + + evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); + + if(!enemyHadTurn) + { + auto battleIsFinishedOpt = state->battleIsFinished(); + + if(battleIsFinishedOpt) + { + print("No need to cast a spell. Battle will finish soon."); + return false; + } + } + } + + CStopWatch timer; + + tbb::parallel_for(tbb::blocked_range(0, possibleCasts.size()), [&](const tbb::blocked_range & r) + { + for(auto i = r.begin(); i != r.end(); i++) + { + auto & ps = possibleCasts[i]; + auto state = std::make_shared(env.get(), cb); + + spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); + cast.castEval(state->getServerCallback(), ps.dest); + + auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); + + auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool + { + auto original = cb->battleGetUnitByID(u->unitId()); + return !original || u->speed() != original->speed(); + }); + + DamageCache innerCache(&damageCache); + innerCache.buildDamageCache(state, side); + + if(needFullEval || !cachedAttack) + { + PotentialTargets innerTargets(activeStack, damageCache, state); + BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio); + + if(!innerTargets.possibleAttacks.empty()) + { + innerEvaluator.updateReachabilityMap(state); + + auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); + + ps.value = newStackAction.score; + } + else + { + ps.value = 0; + } + } + else + { + ps.value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); + } + + for(auto unit : allUnits) + { + auto newHealth = unit->getAvailableHealth(); + auto oldHealth = healthOfStack[unit->unitId()]; + + if(oldHealth != newHealth) + { + auto damage = std::abs(oldHealth - newHealth); + auto originalDefender = cb->battleGetUnitByID(unit->unitId()); + + auto dpsReduce = AttackPossibility::calculateDamageReduce( + nullptr, + originalDefender && originalDefender->alive() ? originalDefender : unit, + damage, + innerCache, + state); + + auto ourUnit = unit->unitSide() == side ? 1 : -1; + auto goodEffect = newHealth > oldHealth ? 1 : -1; + + if(ourUnit * goodEffect == 1) + { + if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot())) + continue; + + ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); + } + else + ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + } + } + } + }); + + LOGFL("Evaluation took %d ms", timer.getDiff()); + + auto pscValue = [](const PossibleSpellcast &ps) -> int64_t + { + return ps.value; + }; + auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue); + + if(castToPerform.value > cachedScore) + { + LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value); + BattleAction spellcast; + spellcast.actionType = EActionType::HERO_SPELL; + spellcast.spell = castToPerform.spell->id; + spellcast.setTarget(castToPerform.dest); + spellcast.side = side; + spellcast.stackNumber = (!side) ? -1 : -2; + cb->battleMakeSpellAction(spellcast); + activeActionMade = true; + + return true; + } + + LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value); + + return false; +} + +//Below method works only for offensive spells +void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps) +{ + using ValueMap = PossibleSpellcast::ValueMap; + + RNGStub rngStub; + HypotheticBattle state(env.get(), cb); + TStacks all = cb->battleGetAllStacks(false); + + ValueMap healthOfStack; + ValueMap newHealthOfStack; + + for(auto unit : all) + { + healthOfStack[unit->unitId()] = unit->getAvailableHealth(); + } + + spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell); + cast.castEval(state.getServerCallback(), ps.dest); + + for(auto unit : all) + { + auto unitId = unit->unitId(); + auto localUnit = state.battleGetUnitByID(unitId); + newHealthOfStack[unitId] = localUnit->getAvailableHealth(); + } + + int64_t totalGain = 0; + + for(auto unit : all) + { + auto unitId = unit->unitId(); + auto localUnit = state.battleGetUnitByID(unitId); + + auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; + + if(localUnit->unitOwner() != getCbc()->getPlayerID()) + healthDiff = -healthDiff; + + if(healthDiff < 0) + { + ps.value = -1; + return; //do not damage own units at all + } + + totalGain += healthDiff; + } + + ps.value = totalGain; +} + +void BattleEvaluator::print(const std::string & text) const +{ + logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); +} + + + diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h new file mode 100644 index 000000000..b3f61091c --- /dev/null +++ b/AI/BattleAI/BattleEvaluator.h @@ -0,0 +1,80 @@ +/* + * BattleEvaluator.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/AI_Base.h" +#include "../../lib/battle/ReachabilityInfo.h" +#include "PossibleSpellcast.h" +#include "PotentialTargets.h" +#include "BattleExchangeVariant.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class EnemyInfo; + +class BattleEvaluator +{ + std::unique_ptr targets; + std::shared_ptr hb; + BattleExchangeEvaluator scoreEvaluator; + std::shared_ptr cb; + std::shared_ptr env; + bool activeActionMade = false; + std::optional cachedAttack; + PlayerColor playerID; + int side; + int64_t cachedScore; + DamageCache damageCache; + float strengthRatio; + +public: + BattleAction selectStackAction(const CStack * stack); + bool attemptCastingSpell(const CStack * stack); + bool canCastSpell(); + std::optional findBestCreatureSpell(const CStack * stack); + BattleAction goTowardsNearest(const CStack * stack, std::vector hexes); + std::vector getBrokenWallMoatHexes() const; + void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only + void print(const std::string & text) const; + + BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + const battle::Unit * activeStack, + PlayerColor playerID, + int side, + float strengthRatio) + :scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio) + { + hb = std::make_shared(env.get(), cb); + damageCache.buildDamageCache(hb, side); + + targets = std::make_unique(activeStack, damageCache, hb); + cachedScore = EvaluationResult::INEFFECTIVE_SCORE; + } + + BattleEvaluator( + std::shared_ptr env, + std::shared_ptr cb, + std::shared_ptr hb, + DamageCache & damageCache, + const battle::Unit * activeStack, + PlayerColor playerID, + int side, + float strengthRatio) + :scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio) + { + targets = std::make_unique(activeStack, damageCache, hb); + cachedScore = EvaluationResult::INEFFECTIVE_SCORE; + } +}; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index a53bb646b..b392af13a 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -41,7 +41,7 @@ int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, Hypothe unitToUpdate->movedThisRound = affectedUnit->movedThisRound; } - auto attackValue = ap.attackValue(); + auto attackValue = ap.damageDiff(positiveEffectMultiplier, negativeEffectMultiplier); dpsScore += attackValue; @@ -97,11 +97,11 @@ int64_t BattleExchangeVariant::trackAttack( if(isOurAttack) { - dpsScore += defenderDamageReduce; + dpsScore += defenderDamageReduce * positiveEffectMultiplier; attackerValue[attacker->unitId()].value += defenderDamageReduce; } else - dpsScore -= defenderDamageReduce; + dpsScore -= defenderDamageReduce * negativeEffectMultiplier; defender->damage(attackDamage); attacker->afterAttack(shooting, false); @@ -125,12 +125,12 @@ int64_t BattleExchangeVariant::trackAttack( if(isOurAttack) { - dpsScore -= attackerDamageReduce; + dpsScore -= attackerDamageReduce * negativeEffectMultiplier; attackerValue[attacker->unitId()].isRetalitated = true; } else { - dpsScore += attackerDamageReduce; + dpsScore += attackerDamageReduce * positiveEffectMultiplier; attackerValue[defender->unitId()].value += attackerDamageReduce; } @@ -206,7 +206,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( std::shared_ptr hb) { MoveTarget result; - BattleExchangeVariant ev; + BattleExchangeVariant ev(getPositiveEffectMultiplier(), getNegativeEffectMultiplier()); if(targets.unreachableEnemies.empty()) return result; @@ -353,6 +353,11 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( } } + vstd::erase_if(exchangeUnits, [&](const battle::Unit * u) -> bool + { + return !hb->battleGetUnitByID(u->unitId())->alive(); + }); + return exchangeUnits; } @@ -376,7 +381,8 @@ int64_t BattleExchangeEvaluator::calculateExchange( std::vector ourStacks; std::vector enemyStacks; - enemyStacks.push_back(ap.attack.defender); + if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) + enemyStacks.push_back(ap.attack.defender); std::vector exchangeUnits = getExchangeUnits(ap, targets, hb); @@ -386,14 +392,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( } auto exchangeBattle = std::make_shared(env.get(), hb); - BattleExchangeVariant v; - auto melleeAttackers = ourStacks; - - vstd::removeDuplicates(melleeAttackers); - vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool - { - return !cb->battleCanShoot(u); - }); + BattleExchangeVariant v(getPositiveEffectMultiplier(), getNegativeEffectMultiplier()); for(auto unit : exchangeUnits) { @@ -403,12 +402,20 @@ int64_t BattleExchangeEvaluator::calculateExchange( bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true); auto & attackerQueue = isOur ? ourStacks : enemyStacks; - if(!vstd::contains(attackerQueue, unit)) + if(exchangeBattle->getForUpdate(unit->unitId())->alive() && !vstd::contains(attackerQueue, unit)) { attackerQueue.push_back(unit); } } + auto melleeAttackers = ourStacks; + + vstd::removeDuplicates(melleeAttackers); + vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool + { + return !cb->battleCanShoot(u); + }); + bool canUseAp = true; for(auto activeUnit : exchangeUnits) @@ -430,7 +437,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( auto targetUnit = ap.attack.defender; - if(!isOur || !exchangeBattle->getForUpdate(targetUnit->unitId())->alive()) + if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) { auto estimateAttack = [&](const battle::Unit * u) -> int64_t { @@ -459,7 +466,10 @@ int64_t BattleExchangeEvaluator::calculateExchange( { auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool { - if(!u->alive() || u->unitSide() == attacker->unitSide()) + if(u->unitSide() == attacker->unitSide()) + return false; + + if(!exchangeBattle->getForUpdate(u->unitId())->alive()) return false; return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool @@ -506,12 +516,12 @@ int64_t BattleExchangeEvaluator::calculateExchange( vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle->getForUpdate(u->unitId())->alive(); + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); }); vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool { - return !exchangeBattle->getForUpdate(u->unitId())->alive(); + return !exchangeBattle->battleGetUnitByID(u->unitId())->alive(); }); } diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 3d95dd912..9837924a6 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -59,7 +59,8 @@ struct EvaluationResult class BattleExchangeVariant { public: - BattleExchangeVariant(): dpsScore(0) {} + BattleExchangeVariant(float positiveEffectMultiplier, float negativeEffectMultiplier) + : dpsScore(0), positiveEffectMultiplier(positiveEffectMultiplier), negativeEffectMultiplier(negativeEffectMultiplier) {} int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state); @@ -80,6 +81,8 @@ public: std::map & reachabilityMap); private: + float positiveEffectMultiplier; + float negativeEffectMultiplier; int64_t dpsScore; std::map attackerValue; }; @@ -91,9 +94,15 @@ private: std::shared_ptr env; std::map> reachabilityMap; std::vector turnOrder; + float negativeEffectMultiplier; public: - BattleExchangeEvaluator(std::shared_ptr cb, std::shared_ptr env): cb(cb), env(env) {} + BattleExchangeEvaluator( + std::shared_ptr cb, + std::shared_ptr env, + float strengthRatio): cb(cb), env(env) { + negativeEffectMultiplier = strengthRatio; + } EvaluationResult findBestTarget( const battle::Unit * activeStack, @@ -118,4 +127,7 @@ public: std::shared_ptr hb); std::vector getAdjacentUnits(const battle::Unit * unit); + + float getPositiveEffectMultiplier() { return 1; } + float getNegativeEffectMultiplier() { return negativeEffectMultiplier; } }; \ No newline at end of file diff --git a/AI/BattleAI/CMakeLists.txt b/AI/BattleAI/CMakeLists.txt index 1850e24f1..335c92f5c 100644 --- a/AI/BattleAI/CMakeLists.txt +++ b/AI/BattleAI/CMakeLists.txt @@ -1,6 +1,7 @@ set(battleAI_SRCS AttackPossibility.cpp BattleAI.cpp + BattleEvaluator.cpp common.cpp EnemyInfo.cpp PossibleSpellcast.cpp @@ -15,6 +16,7 @@ set(battleAI_HEADERS AttackPossibility.h BattleAI.h + BattleEvaluator.h common.h EnemyInfo.h PotentialTargets.h From 5f13a0bbda6b17d9f721d3965476eb2a97fabfe1 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 26 Aug 2023 13:06:41 +0300 Subject: [PATCH 0194/1248] BattleAI: spellcast fixes and floating point score --- AI/BattleAI/AttackPossibility.cpp | 15 +- AI/BattleAI/AttackPossibility.h | 14 +- AI/BattleAI/BattleAI.cpp | 33 +++- AI/BattleAI/BattleAI.h | 1 - AI/BattleAI/BattleEvaluator.cpp | 43 ++++- AI/BattleAI/BattleEvaluator.h | 2 +- AI/BattleAI/BattleExchangeVariant.cpp | 218 +++++++++++++++++--------- AI/BattleAI/BattleExchangeVariant.h | 23 +-- AI/BattleAI/PossibleSpellcast.h | 2 +- AI/BattleAI/StackWithBonuses.cpp | 9 +- 10 files changed, 241 insertions(+), 119 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index ce1803c34..91cc7f62f 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -22,7 +22,7 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit { auto damage = averageDmg(hb->battleEstimateDamage(attacker, defender, 0).damage); - damageCache[attacker->unitId()][defender->unitId()] = damage / attacker->getCount(); + damageCache[attacker->unitId()][defender->unitId()] = static_cast(damage) / attacker->getCount(); } @@ -98,18 +98,18 @@ AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const Battl { } -int64_t AttackPossibility::damageDiff() const +float AttackPossibility::damageDiff() const { return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg; } -int64_t AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const +float AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const { return positiveEffectMultiplier * (defenderDamageReduce + shootersBlockedDmg) - negativeEffectMultiplier * (attackerDamageReduce + collateralDamageReduce); } -int64_t AttackPossibility::attackValue() const +float AttackPossibility::attackValue() const { return damageDiff(); } @@ -119,7 +119,7 @@ int64_t AttackPossibility::attackValue() const /// Half bounty for kill, half for making damage equal to enemy health /// Bounty - the killed creature average damage calculated against attacker /// -int64_t AttackPossibility::calculateDamageReduce( +float AttackPossibility::calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, @@ -163,7 +163,7 @@ int64_t AttackPossibility::calculateDamageReduce( auto firstUnitHealthRatio = firstUnitHpLeft == 0 ? 1 : static_cast(firstUnitHpLeft) / maxHealth; auto firstUnitKillValue = (1 - firstUnitHealthRatio) * (1 - firstUnitHealthRatio); - return (int64_t)(damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY)); + return damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY); } int64_t AttackPossibility::evaluateBlockedShootersDmg( @@ -270,7 +270,8 @@ AttackPossibility AttackPossibility::evaluate( for(int i = 0; i < totalAttacks; i++) { - int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; + int64_t damageDealt, damageReceived; + float defenderDamageReduce, attackerDamageReduce; DamageEstimation retaliation; auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); diff --git a/AI/BattleAI/AttackPossibility.h b/AI/BattleAI/AttackPossibility.h index cd28839f4..2181d883a 100644 --- a/AI/BattleAI/AttackPossibility.h +++ b/AI/BattleAI/AttackPossibility.h @@ -46,16 +46,16 @@ public: std::vector> affectedUnits; - int64_t defenderDamageReduce = 0; - int64_t attackerDamageReduce = 0; //usually by counter-attack - int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) + float defenderDamageReduce = 0; + float attackerDamageReduce = 0; //usually by counter-attack + float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) int64_t shootersBlockedDmg = 0; AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); - int64_t damageDiff() const; - int64_t attackValue() const; - int64_t damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const; + float damageDiff() const; + float attackValue() const; + float damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const; static AttackPossibility evaluate( const BattleAttackInfo & attackInfo, @@ -63,7 +63,7 @@ public: DamageCache & damageCache, std::shared_ptr state); - static int64_t calculateDamageReduce( + static float calculateDamageReduce( const battle::Unit * attacker, const battle::Unit * defender, uint64_t damageDealt, diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 60e2729f6..3ff581c61 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -81,7 +81,28 @@ void CBattleAI::yourTacticPhase(int distance) cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); } -void CBattleAI::activeStack( const CStack * stack ) +float getStrengthRatio(std::shared_ptr cb, int side) +{ + auto stacks = cb->battleGetAllStacks(); + auto our = 0, enemy = 0; + + for(auto stack : stacks) + { + auto creature = stack->creatureId().toCreature(); + + if(!creature) + continue; + + if(stack->unitSide() == side) + our += stack->getCount() * creature->getAIValue(); + else + enemy += stack->getCount() * creature->getAIValue(); + } + + return enemy == 0 ? 1.0f : static_cast(our) / enemy; +} + +void CBattleAI::activeStack(const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); @@ -110,7 +131,11 @@ void CBattleAI::activeStack( const CStack * stack ) return; } - BattleEvaluator evaluator(env, cb, stack, playerID, side, strengthRatio); +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Build evaluator and targets"); +#endif + + BattleEvaluator evaluator(env, cb, stack, playerID, side, getStrengthRatio(cb, side)); result = evaluator.selectStackAction(stack); @@ -207,10 +232,6 @@ void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2 { LOG_TRACE(logAi); side = Side; - strengthRatio = static_cast(army1->getArmyStrength()) / static_cast(army2->getArmyStrength()); - - if(side == 1) - strengthRatio = 1 / strengthRatio; skipCastUntilNextBattle = false; } diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index 9c60a32b4..95841684e 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface bool wasWaitingForRealize; bool wasUnlockingGs; int movesSkippedByDefense; - float strengthRatio; bool skipCastUntilNextBattle; public: diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index d5d101f1e..64c505591 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -100,11 +100,14 @@ std::optional BattleEvaluator::findBestCreatureSpell(const CS BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Select stack action"); +#endif //evaluate casting spell for spellcasting stack std::optional bestSpellcast = findBestCreatureSpell(stack); auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb); - auto score = EvaluationResult::INEFFECTIVE_SCORE; + float score = EvaluationResult::INEFFECTIVE_SCORE; if(targets->possibleAttacks.empty() && bestSpellcast.has_value()) { @@ -136,7 +139,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) { score = evaluationResult.score; - logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld", + logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%2f -%2f = %2f", bestAttack.attackerState->unitType()->getJsonKey(), bestAttack.affectedUnits[0]->unitType()->getJsonKey(), (int)bestAttack.affectedUnits[0]->getCount(), @@ -145,7 +148,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) bestAttack.attack.chargeDistance, bestAttack.attack.attacker->speed(0, true), bestAttack.defenderDamageReduce, - bestAttack.attackerDamageReduce, bestAttack.attackValue() + bestAttack.attackerDamageReduce, + bestAttack.attackValue() ); if (moveTarget.scorePerTurn <= score) @@ -513,11 +517,20 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) CStopWatch timer; +#if BATTLE_TRACE_LEVEL >= 1 + tbb::blocked_range r(0, possibleCasts.size()); +#else tbb::parallel_for(tbb::blocked_range(0, possibleCasts.size()), [&](const tbb::blocked_range & r) { +#endif for(auto i = r.begin(); i != r.end(); i++) { auto & ps = possibleCasts[i]; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); +#endif + auto state = std::make_shared(env.get(), cb); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); @@ -531,12 +544,17 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) return !original || u->speed() != original->speed(); }); - DamageCache innerCache(&damageCache); + DamageCache safeCopy = damageCache; + DamageCache innerCache(&safeCopy); innerCache.buildDamageCache(state, side); if(needFullEval || !cachedAttack) { - PotentialTargets innerTargets(activeStack, damageCache, state); +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Full evaluation is started due to stack speed affected."); +#endif + + PotentialTargets innerTargets(activeStack, innerCache, state); BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio); if(!innerTargets.possibleAttacks.empty()) @@ -586,14 +604,27 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) } else ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "Spell affects %s (%d), dps: %2f", + unit->creatureId().toCreature()->getNameSingularTranslated(), + unit->getCount(), + dpsReduce); +#endif } } +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("Total score: %2f", ps.value); +#endif } +#if BATTLE_TRACE_LEVEL == 0 }); +#endif LOGFL("Evaluation took %d ms", timer.getDiff()); - auto pscValue = [](const PossibleSpellcast &ps) -> int64_t + auto pscValue = [](const PossibleSpellcast &ps) -> float { return ps.value; }; diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index b3f61091c..cb183f46b 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -33,7 +33,7 @@ class BattleEvaluator std::optional cachedAttack; PlayerColor playerID; int side; - int64_t cachedScore; + float cachedScore; DamageCache damageCache; float strengthRatio; diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index b392af13a..8ae233e41 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -25,40 +25,107 @@ MoveTarget::MoveTarget() turnsToRich = 1; } -int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state) +float BattleExchangeVariant::trackAttack( + const AttackPossibility & ap, + std::shared_ptr hb, + DamageCache & damageCache) { + auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); + + const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; + static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); + const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); + + float attackValue = 0; auto affectedUnits = ap.affectedUnits; affectedUnits.push_back(ap.attackerState); for(auto affectedUnit : affectedUnits) { - auto unitToUpdate = state.getForUpdate(affectedUnit->unitId()); + auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); - unitToUpdate->health = affectedUnit->health; - unitToUpdate->shots = affectedUnit->shots; - unitToUpdate->counterAttacks = affectedUnit->counterAttacks; - unitToUpdate->movedThisRound = affectedUnit->movedThisRound; - } + if(unitToUpdate->unitSide() == attacker->unitSide()) + { + if(unitToUpdate->unitId() == attacker->unitId()) + { + auto defender = hb->getForUpdate(ap.attack.defender->unitId()); - auto attackValue = ap.damageDiff(positiveEffectMultiplier, negativeEffectMultiplier); + if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate()) + continue; - dpsScore += attackValue; + auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb); + auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb); + + attackValue -= attackerDamageReduce; + dpsScore -= attackerDamageReduce * negativeEffectMultiplier; + attackerValue[unitToUpdate->unitId()].isRetalitated = true; + + unitToUpdate->damage(retaliationDamage); + defender->afterAttack(false, true); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, ap attack, %s, dps: %lld, score: %lld", - ap.attack.attacker->getDescription(), - ap.attack.defender->getDescription(), - ap.attack.shooting ? "shot" : "mellee", - ap.damageDealt, - attackValue); + logAi->trace( + "%s -> %s, ap retalitation, %s, dps: %2f, score: %2f", + defender->getDescription(), + unitToUpdate->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + retaliationDamage, + attackerDamageReduce); #endif + } + else + { + auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); + auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb); + + attackValue -= collateralDamageReduce; + dpsScore -= collateralDamageReduce * negativeEffectMultiplier; + + unitToUpdate->damage(collateralDamage); + +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace( + "%s -> %s, ap collateral, %s, dps: %2f, score: %2f", + attacker->getDescription(), + unitToUpdate->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + collateralDamage, + collateralDamageReduce); +#endif + } + } + else + { + int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); + float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb); + + attackValue += defenderDamageReduce; + dpsScore += defenderDamageReduce * positiveEffectMultiplier; + attackerValue[attacker->unitId()].value += defenderDamageReduce; + + unitToUpdate->damage(attackDamage); + +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace( + "%s -> %s, ap attack, %s, dps: %2f, score: %2f", + attacker->getDescription(), + unitToUpdate->getDescription(), + ap.attack.shooting ? "shot" : "mellee", + attackDamage, + defenderDamageReduce); +#endif + } + } + + attackValue += ap.shootersBlockedDmg; + dpsScore += ap.shootersBlockedDmg * positiveEffectMultiplier; + attacker->afterAttack(ap.attack.shooting, false); return attackValue; } -int64_t BattleExchangeVariant::trackAttack( +float BattleExchangeVariant::trackAttack( std::shared_ptr attacker, std::shared_ptr defender, bool shooting, @@ -71,23 +138,15 @@ int64_t BattleExchangeVariant::trackAttack( static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); - // FIXME: provide distance info for Jousting bonus - BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); - - if(shooting) - { - bai.attackerPos.setXY(8, 5); - } - int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb); - int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb); - int64_t attackerDamageReduce = 0; + float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb); + float attackerDamageReduce = 0; if(!evaluateOnly) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace( - "%s -> %s, normal attack, %s, dps: %lld, %lld", + "%s -> %s, normal attack, %s, dps: %lld, %2f", attacker->getDescription(), defender->getDescription(), shooting ? "shot" : "mellee", @@ -107,36 +166,33 @@ int64_t BattleExchangeVariant::trackAttack( attacker->afterAttack(shooting, false); } - if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) + if(!evaluateOnly && defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting) { auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb); attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb); - if(!evaluateOnly) - { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace( - "%s -> %s, retaliation, dps: %lld, %lld", - defender->getDescription(), - attacker->getDescription(), - retaliationDamage, - attackerDamageReduce); + logAi->trace( + "%s -> %s, retaliation, dps: %lld, %2f", + defender->getDescription(), + attacker->getDescription(), + retaliationDamage, + attackerDamageReduce); #endif - if(isOurAttack) - { - dpsScore -= attackerDamageReduce * negativeEffectMultiplier; - attackerValue[attacker->unitId()].isRetalitated = true; - } - else - { - dpsScore += attackerDamageReduce * positiveEffectMultiplier; - attackerValue[defender->unitId()].value += attackerDamageReduce; - } - - attacker->damage(retaliationDamage); - defender->afterAttack(false, true); + if(isOurAttack) + { + dpsScore -= attackerDamageReduce * negativeEffectMultiplier; + attackerValue[attacker->unitId()].isRetalitated = true; } + else + { + dpsScore += attackerDamageReduce * positiveEffectMultiplier; + attackerValue[defender->unitId()].value += attackerDamageReduce; + } + + attacker->damage(retaliationDamage); + defender->afterAttack(false, true); } auto score = defenderDamageReduce - attackerDamageReduce; @@ -144,7 +200,7 @@ int64_t BattleExchangeVariant::trackAttack( #if BATTLE_TRACE_LEVEL>=1 if(!score) { - logAi->trace("Attack has zero score d:%lld a:%lld", defenderDamageReduce, attackerDamageReduce); + logAi->trace("Attack has zero score d:%2f a:%2f", defenderDamageReduce, attackerDamageReduce); } #endif @@ -159,33 +215,22 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( { EvaluationResult result(targets.bestAction()); - updateReachabilityMap(hb); - - for(auto & ap : targets.possibleAttacks) - { - int64_t score = calculateExchange(ap, targets, damageCache, hb); - - if(score > result.score) - { - result.score = score; - result.bestAttack = ap; - } - } - if(!activeStack->waited()) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Evaluating waited attack for %s", activeStack->getDescription()); #endif - hb->getForUpdate(activeStack->unitId())->waiting = true; - hb->getForUpdate(activeStack->unitId())->waitedThisTurn = true; + auto hbWaited = std::make_shared(env.get(), hb); - updateReachabilityMap(hb); + hbWaited->getForUpdate(activeStack->unitId())->waiting = true; + hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true; + + updateReachabilityMap(hbWaited); for(auto & ap : targets.possibleAttacks) { - int64_t score = calculateExchange(ap, targets, damageCache, hb); + float score = calculateExchange(ap, targets, damageCache, hbWaited); if(score > result.score) { @@ -196,6 +241,24 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( } } +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Evaluating normal attack for %s", activeStack->getDescription()); +#endif + + updateReachabilityMap(hb); + + for(auto & ap : targets.possibleAttacks) + { + float score = calculateExchange(ap, targets, damageCache, hb); + + if(score >= result.score) + { + result.score = score; + result.bestAttack = ap; + result.wait = false; + } + } + return result; } @@ -361,14 +424,14 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( return exchangeUnits; } -int64_t BattleExchangeEvaluator::calculateExchange( +float BattleExchangeEvaluator::calculateExchange( const AttackPossibility & ap, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb) { #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from); + logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); #endif if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE @@ -439,7 +502,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive()) { - auto estimateAttack = [&](const battle::Unit * u) -> int64_t + auto estimateAttack = [&](const battle::Unit * u) -> float { auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId()); auto score = v.trackAttack( @@ -452,7 +515,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( true); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Best target selector %s->%s score = %lld", attacker->getDescription(), u->getDescription(), score); + logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), u->getDescription(), score); #endif return score; @@ -497,9 +560,10 @@ int64_t BattleExchangeEvaluator::calculateExchange( auto shooting = exchangeBattle->battleCanShoot(attacker.get()); const int totalAttacks = attacker->getTotalAttacks(shooting); - if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() && targetUnit->unitId() == ap.attack.defender->unitId()) + if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() + && targetUnit->unitId() == ap.attack.defender->unitId()) { - v.trackAttack(ap, *exchangeBattle); + v.trackAttack(ap, exchangeBattle, damageCache); } else { @@ -530,7 +594,7 @@ int64_t BattleExchangeEvaluator::calculateExchange( v.adjustPositions(melleeAttackers, ap, reachabilityMap); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Exchange score: %lld", v.getScore()); + logAi->trace("Exchange score: %2f", v.getScore()); #endif return v.getScore(); @@ -560,7 +624,7 @@ void BattleExchangeVariant::adjustPositions( vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); } - int64_t notRealizedDamage = 0; + float notRealizedDamage = 0; for(auto unit : attackers) { @@ -576,7 +640,7 @@ void BattleExchangeVariant::adjustPositions( continue; } - auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> int64_t + auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float { auto score = vstd::contains(reachabilityMap[h], unit) ? reachabilityMap[h].size() diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 9837924a6..19da2721a 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -16,7 +16,7 @@ struct AttackerValue { - int64_t value; + float value; bool isRetalitated; BattleHex position; @@ -25,8 +25,8 @@ struct AttackerValue struct MoveTarget { - int64_t score; - int64_t scorePerTurn; + float score; + float scorePerTurn; std::vector positions; std::optional cachedAttack; uint8_t turnsToRich; @@ -36,12 +36,12 @@ struct MoveTarget struct EvaluationResult { - static const int64_t INEFFECTIVE_SCORE = -1000000; + static const int64_t INEFFECTIVE_SCORE = -10000; AttackPossibility bestAttack; MoveTarget bestMove; bool wait; - int64_t score; + float score; bool defend; EvaluationResult(const AttackPossibility & ap) @@ -62,9 +62,12 @@ public: BattleExchangeVariant(float positiveEffectMultiplier, float negativeEffectMultiplier) : dpsScore(0), positiveEffectMultiplier(positiveEffectMultiplier), negativeEffectMultiplier(negativeEffectMultiplier) {} - int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state); + float trackAttack( + const AttackPossibility & ap, + std::shared_ptr hb, + DamageCache & damageCache); - int64_t trackAttack( + float trackAttack( std::shared_ptr attacker, std::shared_ptr defender, bool shooting, @@ -73,7 +76,7 @@ public: std::shared_ptr hb, bool evaluateOnly = false); - int64_t getScore() const { return dpsScore; } + float getScore() const { return dpsScore; } void adjustPositions( std::vector attackers, @@ -83,7 +86,7 @@ public: private: float positiveEffectMultiplier; float negativeEffectMultiplier; - int64_t dpsScore; + float dpsScore; std::map attackerValue; }; @@ -110,7 +113,7 @@ public: DamageCache & damageCache, std::shared_ptr hb); - int64_t calculateExchange( + float calculateExchange( const AttackPossibility & ap, PotentialTargets & targets, DamageCache & damageCache, diff --git a/AI/BattleAI/PossibleSpellcast.h b/AI/BattleAI/PossibleSpellcast.h index a4e0021c7..4581d381d 100644 --- a/AI/BattleAI/PossibleSpellcast.h +++ b/AI/BattleAI/PossibleSpellcast.h @@ -27,7 +27,7 @@ public: const CSpell * spell; spells::Target dest; - int64_t value; + float value; PossibleSpellcast(); virtual ~PossibleSpellcast(); diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index c77e2b4fc..6c5f57e31 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -45,7 +45,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: id(Stack->unitId()), side(Stack->unitSide()), player(Stack->unitOwner()), - slot(Stack->unitSlot()) + slot(Stack->unitSlot()), + treeVersionLocal(0) { localInit(Owner); @@ -61,7 +62,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: id(Stack->unitId()), side(Stack->unitSide()), player(Stack->unitOwner()), - slot(Stack->unitSlot()) + slot(Stack->unitSlot()), + treeVersionLocal(0) { localInit(Owner); @@ -76,7 +78,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle: baseAmount(info.count), id(info.id), side(info.side), - slot(SlotID::SUMMONED_SLOT_PLACEHOLDER) + slot(SlotID::SUMMONED_SLOT_PLACEHOLDER), + treeVersionLocal(0) { type = info.type.toCreature(); origBearer = type; From 65c9ed0e09674b5a58097bbb4f94b12432cdc6c4 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 15:29:53 +0200 Subject: [PATCH 0195/1248] Flatpak - Use new screenshots --- launcher/eu.vcmi.VCMI.metainfo.xml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 249adc947..b966cd20b 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -22,16 +22,34 @@ - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_1.png + https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_2.png + https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_3.png + https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440 - https://media.moddb.com/images/engines/1/1/766/moddb_screenshot_4.png + https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440 + + + https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440 + + + https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440 + + + https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440 + + + https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440 + + + https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440 + + + https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440 https://vcmi.eu/ From bd0f9bb280d8f55b6fc5614a03ec3aa9c0d5423c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 19:53:36 +0200 Subject: [PATCH 0196/1248] preparation --- client/CMusicHandler.cpp | 8 +++++--- client/eventsSDL/InputHandler.cpp | 6 ++++++ client/eventsSDL/InputHandler.h | 3 +++ client/mapView/MapView.cpp | 15 +++++++++++++++ client/mapView/MapView.h | 5 +++++ client/mapView/MapViewActions.h | 3 ++- 6 files changed, 36 insertions(+), 4 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 1ffd16eb9..1d1638f21 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -14,12 +14,13 @@ #include "CMusicHandler.h" #include "CGameInfo.h" #include "renderSDL/SDLRWwrapper.h" +#include "eventsSDL/InputHandler.h" #include "gui/CGuiHandler.h" #include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/constants/StringConstants.h" +#include "../lib/StringConstants.h" #include "../lib/CRandomGenerator.h" #include "../lib/VCMIDirs.h" #include "../lib/TerrainHandler.h" @@ -629,7 +630,8 @@ bool MusicEntry::play() } } - startTime = SDL_GetTicks(); + startTime = GH.input().getTicks(); + playing = true; return true; } @@ -640,7 +642,7 @@ bool MusicEntry::stop(int fade_ms) { playing = false; loop = 0; - uint32_t endTime = SDL_GetTicks(); + uint32_t endTime = GH.input().getTicks(); assert(startTime != uint32_t(-1)); float playDuration = (endTime - startTime + startPosition) / 1000.f; owner->trackPositions[currentName] = playDuration; diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index c65fefef0..2670a09c0 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -28,6 +28,7 @@ #include "../../lib/CConfigHandler.h" #include +#include InputHandler::InputHandler() : mouseHandler(std::make_unique()) @@ -253,6 +254,11 @@ void InputHandler::hapticFeedback() fingerHandler->hapticFeedback(); } +uint64_t InputHandler::getTicks() +{ + return SDL_GetTicks64(); +} + bool InputHandler::hasTouchInputDevice() const { return fingerHandler->hasTouchInputDevice(); diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index ac245f247..607b5dd81 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -68,6 +68,9 @@ public: /// do a haptic feedback void hapticFeedback(); + /// Get the number of milliseconds since SDL library initialization + uint64_t getTicks(); + /// returns true if system has active touchscreen bool hasTouchInputDevice() const; diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 76f981337..27d2ca925 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -25,6 +25,7 @@ #include "../render/Canvas.h" #include "../render/IImage.h" #include "../renderSDL/SDL_Extensions.h" +#include "../eventsSDL/InputHandler.h" #include "../../CCallback.h" @@ -82,6 +83,13 @@ void BasicMapView::showAll(Canvas & to) render(to, true); } +void MapView::tick(uint32_t msPassed) +{ + postSwipe(msPassed); + + controller->tick(msPassed); +} + void MapView::show(Canvas & to) { actions->setContext(controller->getContext()); @@ -115,9 +123,16 @@ void MapView::onMapScrolled(const Point & distance) void MapView::onMapSwiped(const Point & viewPosition) { + swipeHistory[GH.input().getTicks()] = viewPosition; + controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); } +void MapView::postSwipe(uint32_t msPassed) { + if(!actions->dragActive) + swipeHistory.clear(); +} + void MapView::onCenteredTile(const int3 & tile) { controller->setViewCenter(tile); diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index f6719da84..3bec70ca7 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -49,7 +49,12 @@ class MapView : public BasicMapView { std::shared_ptr actions; + std::map swipeHistory; + + void postSwipe(uint32_t msPassed); + public: + void tick(uint32_t msPassed) override; void show(Canvas & to) override; MapView(const Point & offset, const Point & dimensions); diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index de6158f43..c78e8de40 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -24,7 +24,6 @@ class MapViewActions : public CIntObject Point dragDistance; double pinchZoomFactor; - bool dragActive; void handleHover(const Point & cursorPosition); @@ -44,4 +43,6 @@ public: void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override; void wheelScrolled(int distance) override; + + bool dragActive; }; From fe6d96f4a0878868ee81d0938fe15f83fe0ac4ec Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 20:19:25 +0200 Subject: [PATCH 0197/1248] angle calc --- client/mapView/MapView.cpp | 8 +++++++- client/mapView/MapView.h | 2 ++ lib/Point.h | 5 +++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 27d2ca925..56a57e74f 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -129,8 +129,14 @@ void MapView::onMapSwiped(const Point & viewPosition) } void MapView::postSwipe(uint32_t msPassed) { - if(!actions->dragActive) + if(!actions->dragActive && swipeHistory.size() > 0) + { + Point diff = swipeHistory.end()->second - swipeHistory.begin()->second; + double angle = diff.angle(); + + std::cout << angle; swipeHistory.clear(); + } } void MapView::onCenteredTile(const int3 & tile) diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index 3bec70ca7..23d9550b2 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -50,6 +50,8 @@ class MapView : public BasicMapView std::shared_ptr actions; std::map swipeHistory; + double postSwipeAngle; + double postSwipeSpeed; void postSwipe(uint32_t msPassed); diff --git a/lib/Point.h b/lib/Point.h index 0d8827562..229875b4f 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -116,6 +116,11 @@ public: return std::sqrt(lengthSquared()); } + double angle() const + { + return std::atan2(y, x) * 180.0 / M_PI; + } + template void serialize(Handler &h, const int version) { From 2286e0c7b1d27d5422921c5fcc7aeeba31479f99 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 21:13:33 +0200 Subject: [PATCH 0198/1248] implement speed and move --- client/mapView/MapView.cpp | 22 +++++++++++++++++++--- lib/Point.h | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 56a57e74f..fbdc50cf8 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -131,12 +131,28 @@ void MapView::onMapSwiped(const Point & viewPosition) void MapView::postSwipe(uint32_t msPassed) { if(!actions->dragActive && swipeHistory.size() > 0) { - Point diff = swipeHistory.end()->second - swipeHistory.begin()->second; - double angle = diff.angle(); + Point diff = Point(0, 0); + for (auto & x : swipeHistory) + diff += x.second; - std::cout << angle; + uint64_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; + + postSwipeAngle = diff.angle(); + postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + + std::cout << postSwipeAngle << " " << postSwipeSpeed << " " << diff.x << "x" << diff.y << " " << timediff << "\n"; swipeHistory.clear(); } + if(postSwipeSpeed > 0.1) { + double len = postSwipeSpeed * static_cast(msPassed); + Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); + + std::cout << len << " " << delta.x << "x" << delta.y << "\n"; + + controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); + + postSwipeSpeed /= 1.1; + } } void MapView::onCenteredTile(const int3 & tile) diff --git a/lib/Point.h b/lib/Point.h index 229875b4f..5289b8c58 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -118,7 +118,7 @@ public: double angle() const { - return std::atan2(y, x) * 180.0 / M_PI; + return std::atan2(y, x); // rad } template From e57e9e8e9fcc8666db7bdb8d5f339b08e9d03c61 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 21:29:24 +0200 Subject: [PATCH 0199/1248] ready draft --- client/mapView/MapView.cpp | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index fbdc50cf8..79462e810 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -129,26 +129,25 @@ void MapView::onMapSwiped(const Point & viewPosition) } void MapView::postSwipe(uint32_t msPassed) { - if(!actions->dragActive && swipeHistory.size() > 0) + if(!actions->dragActive) { - Point diff = Point(0, 0); - for (auto & x : swipeHistory) - diff += x.second; + if(swipeHistory.size() > 1) + { + Point diff = Point(0, 0); + for (auto & x : swipeHistory) + diff += x.second; - uint64_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; + uint64_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; - postSwipeAngle = diff.angle(); - postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond - - std::cout << postSwipeAngle << " " << postSwipeSpeed << " " << diff.x << "x" << diff.y << " " << timediff << "\n"; + postSwipeAngle = diff.angle(); + postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + } swipeHistory.clear(); } if(postSwipeSpeed > 0.1) { double len = postSwipeSpeed * static_cast(msPassed); Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); - std::cout << len << " " << delta.x << "x" << delta.y << "\n"; - controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); postSwipeSpeed /= 1.1; From cde22b3755f131810067d7ca460f432c6c3e7371 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 21:37:29 +0200 Subject: [PATCH 0200/1248] fix --- client/CMusicHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 1d1638f21..6c527c2e7 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -20,7 +20,7 @@ #include "../lib/JsonNode.h" #include "../lib/GameConstants.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/StringConstants.h" +#include "../lib/constants/StringConstants.h" #include "../lib/CRandomGenerator.h" #include "../lib/VCMIDirs.h" #include "../lib/TerrainHandler.h" From 72c4c23e2e6c7116f8678d2e9fe93770e2915303 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 22:25:12 +0200 Subject: [PATCH 0201/1248] swapped --- launcher/eu.vcmi.VCMI.metainfo.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index b966cd20b..49fd864a3 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -22,10 +22,10 @@ - https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 + https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 - https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440 + https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440 https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440 From a354a7f696c4a69ab8c74634913c0bda50a8e77a Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 22:30:02 +0200 Subject: [PATCH 0202/1248] compatible with older sdl --- client/eventsSDL/InputHandler.cpp | 4 ++-- client/eventsSDL/InputHandler.h | 2 +- client/mapView/MapView.cpp | 2 +- client/mapView/MapView.h | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 2670a09c0..7afaf3665 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -254,9 +254,9 @@ void InputHandler::hapticFeedback() fingerHandler->hapticFeedback(); } -uint64_t InputHandler::getTicks() +uint32_t InputHandler::getTicks() { - return SDL_GetTicks64(); + return SDL_GetTicks(); } bool InputHandler::hasTouchInputDevice() const diff --git a/client/eventsSDL/InputHandler.h b/client/eventsSDL/InputHandler.h index 607b5dd81..0584de677 100644 --- a/client/eventsSDL/InputHandler.h +++ b/client/eventsSDL/InputHandler.h @@ -69,7 +69,7 @@ public: void hapticFeedback(); /// Get the number of milliseconds since SDL library initialization - uint64_t getTicks(); + uint32_t getTicks(); /// returns true if system has active touchscreen bool hasTouchInputDevice() const; diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 79462e810..291641e19 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -137,7 +137,7 @@ void MapView::postSwipe(uint32_t msPassed) { for (auto & x : swipeHistory) diff += x.second; - uint64_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; + uint32_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; postSwipeAngle = diff.angle(); postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index 23d9550b2..7c1dd9397 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -49,7 +49,7 @@ class MapView : public BasicMapView { std::shared_ptr actions; - std::map swipeHistory; + std::map swipeHistory; double postSwipeAngle; double postSwipeSpeed; From 6b71820197eb290c3e6852e3a755e83809b81d6f Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 26 Aug 2023 23:25:46 +0200 Subject: [PATCH 0203/1248] avoid some edge cases --- client/mapView/MapView.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 291641e19..5120f4ae2 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -134,10 +134,17 @@ void MapView::postSwipe(uint32_t msPassed) { if(swipeHistory.size() > 1) { Point diff = Point(0, 0); - for (auto & x : swipeHistory) - diff += x.second; + std::pair firstAccepted; + uint32_t now = GH.input().getTicks(); + for (auto & x : swipeHistory) { + if(now - x.first < 150) { // only the last 150 ms are catched + if(firstAccepted.first == 0) + firstAccepted = x; + diff += x.second; + } + } - uint32_t timediff = swipeHistory.rbegin()->first - swipeHistory.begin()->first; + uint32_t timediff = swipeHistory.rbegin()->first - firstAccepted.first; postSwipeAngle = diff.angle(); postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond From 4eecca2d9a44ad65e8d3c3ae55fcf3d7cf85a43e Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:22:29 +0200 Subject: [PATCH 0204/1248] code review --- client/mapView/MapView.cpp | 7 ++++--- client/mapView/MapView.h | 6 +++--- client/mapView/MapViewActions.cpp | 2 ++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 5120f4ae2..4477363a6 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -87,7 +87,7 @@ void MapView::tick(uint32_t msPassed) { postSwipe(msPassed); - controller->tick(msPassed); + BasicMapView::tick(msPassed); } void MapView::show(Canvas & to) @@ -123,7 +123,7 @@ void MapView::onMapScrolled(const Point & distance) void MapView::onMapSwiped(const Point & viewPosition) { - swipeHistory[GH.input().getTicks()] = viewPosition; + swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); } @@ -150,7 +150,8 @@ void MapView::postSwipe(uint32_t msPassed) { postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond } swipeHistory.clear(); - } + } else + postSwipeSpeed = 0.0; if(postSwipeSpeed > 0.1) { double len = postSwipeSpeed * static_cast(msPassed); Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index 7c1dd9397..2b377761e 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -49,9 +49,9 @@ class MapView : public BasicMapView { std::shared_ptr actions; - std::map swipeHistory; - double postSwipeAngle; - double postSwipeSpeed; + std::vector> swipeHistory; + double postSwipeAngle = 0.0; + double postSwipeSpeed = 0.0; void postSwipe(uint32_t msPassed); diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index b1cd6c240..6f71182f1 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -121,6 +121,8 @@ void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdat void MapViewActions::gesture(bool on, const Point & initialPosition, const Point & finalPosition) { + dragActive = on; + pinchZoomFactor = 1.0; } From bbd69fd4305ec8fa35cab5c3616374c3c64ee72a Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:33:52 +0200 Subject: [PATCH 0205/1248] added setting --- client/mapView/MapView.cpp | 6 ++++-- config/schemas/settings.json | 6 +++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 4477363a6..9371f7470 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -85,7 +85,8 @@ void BasicMapView::showAll(Canvas & to) void MapView::tick(uint32_t msPassed) { - postSwipe(msPassed); + if(settings["adventure"]["smoothDragging"].Bool()) + postSwipe(msPassed); BasicMapView::tick(msPassed); } @@ -123,7 +124,8 @@ void MapView::onMapScrolled(const Point & distance) void MapView::onMapSwiped(const Point & viewPosition) { - swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); + if(settings["adventure"]["smoothDragging"].Bool()) + swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); } diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 351e7493b..dd3877cfe 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -221,7 +221,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -265,6 +265,10 @@ "leftButtonDrag" : { "type" : "boolean", "default" : false + }, + "smoothDragging" : { + "type" : "boolean", + "default" : true } } }, From ce20d913e02eeb266350b645e18a43c9f7fd4d0b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 27 Aug 2023 01:35:38 +0300 Subject: [PATCH 0206/1248] Fix checking PlayerColor's for validness --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/Engine/FuzzyHelper.cpp | 2 +- AI/VCAI/FuzzyHelper.cpp | 2 +- AI/VCAI/VCAI.cpp | 2 +- client/Client.cpp | 2 +- client/NetPacksClient.cpp | 2 +- client/adventureMap/CMinimap.cpp | 2 +- client/lobby/SelectionTab.cpp | 2 +- client/render/Graphics.cpp | 2 +- client/renderSDL/SDLImage.cpp | 2 +- lib/IGameCallback.cpp | 4 ++-- lib/NetPacksLib.cpp | 6 +++--- lib/StartInfo.cpp | 2 +- lib/battle/BattleInfo.cpp | 3 +-- lib/constants/EntityIdentifiers.cpp | 14 +++++++------- lib/gameState/CGameState.cpp | 6 +++--- lib/mapObjects/CArmedInstance.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 4 ++-- lib/mapping/MapReaderH3M.cpp | 23 +++++++++++++++-------- mapeditor/graphics.cpp | 2 +- mapeditor/maphandler.cpp | 2 +- server/CGameHandler.cpp | 8 ++++---- server/NetPacksServer.cpp | 4 ++-- server/processors/HeroPoolProcessor.cpp | 2 +- 25 files changed, 55 insertions(+), 49 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index bdb25982d..a32bee870 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1081,7 +1081,7 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; - assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); + assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index 2757cb35a..e550c47ba 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -111,7 +111,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) { auto cb = ai->cb.get(); - if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat + if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat return 0; switch(obj->ID) diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index 78595c49c..c125e2315 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -282,7 +282,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) { auto cb = ai->myCb; - if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat + if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat return 0; switch(obj->ID) diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index e59341a6f..a66152e58 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1578,7 +1578,7 @@ void VCAI::completeGoal(Goals::TSubgoal goal) void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; - assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); + assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); diff --git a/client/Client.cpp b/client/Client.cpp index c650d3f4e..8094d1491 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -570,7 +570,7 @@ void CClient::battleStarted(const BattleInfo * info) for(auto & battleCb : battleCallbacks) { if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || battleCb.first >= PlayerColor::PLAYER_LIMIT) + || !battleCb.first.isValidPlayer()) { battleCb.second->setBattle(info); } diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index ac94b9150..b36d8c81a 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -570,7 +570,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) //inform all players that see this object for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i) { - if(i->first >= PlayerColor::PLAYER_LIMIT) + if(!i->first.isValidPlayer()) continue; if(gs.isVisible(t, i->first) || diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 9dc5da7a7..7165b9e6e 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -46,7 +46,7 @@ ColorRGBA CMinimapInstance::getTileColor(const int3 & pos) const if(player == PlayerColor::NEUTRAL) return graphics->neutralColor; - if (player < PlayerColor::PLAYER_LIMIT) + if (player.isValidPlayer()) return graphics->playerColors[player.getNum()]; } diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 5b69ff3dc..7399c79dd 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -886,7 +886,7 @@ Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptrneutralColor; break; } - if (player < PlayerColor::PLAYER_LIMIT) + if (player.isValidPlayer()) { color = graphics->playerColors[player.getNum()]; break; diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index 6b40e7edc..ea9b55122 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -139,7 +139,7 @@ void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) if(sur->format->palette) { SDL_Color palette[32]; - if(player < PlayerColor::PLAYER_LIMIT) + if(player.isValidPlayer()) { for(int i=0; i<32; ++i) palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index c04a1aa81..1778bbad6 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -247,7 +247,7 @@ void SDLImage::setBlitMode(EImageBlitMode mode) void SDLImage::setFlagColor(PlayerColor player) { - if(player < PlayerColor::PLAYER_LIMIT || player==PlayerColor::NEUTRAL) + if(player.isValidPlayer() || player==PlayerColor::NEUTRAL) CSDL_Ext::setPlayerColor(surf, player); } diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 76c3c10fd..4efa55cab 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -82,7 +82,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, int mode, int3::EDistanceFormula distanceFormula) const { - if(!!player && *player >= PlayerColor::PLAYER_LIMIT) + if(!!player && !player->isValidPlayer()) { logGlobal->error("Illegal call to getTilesInRange!"); return; @@ -114,7 +114,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const { - if(!!Player && *Player >= PlayerColor::PLAYER_LIMIT) + if(!!Player && !Player->isValidPlayer()) { logGlobal->error("Illegal call to getAllTiles !"); return; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 33706619d..b4668a621 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -770,7 +770,7 @@ void LobbyShowMessage::visitTyped(ICPackVisitor & visitor) void SetResources::applyGs(CGameState * gs) const { - assert(player < PlayerColor::PLAYER_LIMIT); + assert(player.isValidPlayer()); if(abs) gs->getPlayerState(player)->resources = res; else @@ -2018,7 +2018,7 @@ void NewTurn::applyGs(CGameState *gs) for(const auto & re : res) { - assert(re.first < PlayerColor::PLAYER_LIMIT); + assert(re.first.isValidPlayer()); gs->getPlayerState(re.first)->resources = re.second; } @@ -2058,7 +2058,7 @@ void SetObjectProperty::applyGs(CGameState * gs) const if(state->towns.empty()) *state->daysWithoutCastle = 0; } - if(val < PlayerColor::PLAYER_LIMIT_I) + if(PlayerColor(val).isValidPlayer()) { PlayerState * p = gs->getPlayerState(PlayerColor(val)); p->towns.emplace_back(t); diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index cc68d451a..5625807ff 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -203,7 +203,7 @@ PlayerInfo & LobbyInfo::getPlayerInfo(int color) TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color) { - if(color < PlayerColor::PLAYER_LIMIT) + if(color.isValidPlayer()) return getPlayerInfo(color.getNum()).team; else return TeamID::NO_TEAM; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index eb6cd3627..d5a714dcb 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -60,8 +60,7 @@ void BattleInfo::calculateCasualties(std::map * casualties) const CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) { PlayerColor owner = sides[side].color; - assert((owner >= PlayerColor::PLAYER_LIMIT) || - (base.armyObj && base.armyObj->tempOwner == owner)); + assert(!owner.isValidPlayer() || (base.armyObj && base.armyObj->tempOwner == owner)); auto * ret = new CStack(&base, owner, id, side, slot); ret->initialPosition = getAvaliableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found? diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 37815f8ab..1f7a6b9d6 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -47,12 +47,12 @@ const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER = SlotID(-3); const SlotID SlotID::WAR_MACHINES_SLOT = SlotID(-4); const SlotID SlotID::ARROW_TOWERS_SLOT = SlotID(-5); -const PlayerColor PlayerColor::SPECTATOR = PlayerColor(252); -const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(253); -const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(254); -const PlayerColor PlayerColor::NEUTRAL = PlayerColor(255); +const PlayerColor PlayerColor::SPECTATOR = PlayerColor(-4); +const PlayerColor PlayerColor::CANNOT_DETERMINE = PlayerColor(-3); +const PlayerColor PlayerColor::UNFLAGGABLE = PlayerColor(-2); +const PlayerColor PlayerColor::NEUTRAL = PlayerColor(-1); const PlayerColor PlayerColor::PLAYER_LIMIT = PlayerColor(PLAYER_LIMIT_I); -const TeamID TeamID::NO_TEAM = TeamID(255); +const TeamID TeamID::NO_TEAM = TeamID(-1); const SpellSchool SpellSchool::ANY = -1; const SpellSchool SpellSchool::AIR = 0; @@ -194,12 +194,12 @@ std::string SpellIDBase::encode(const si32 index) bool PlayerColor::isValidPlayer() const { - return num < PLAYER_LIMIT_I; + return num >= 0 && num < PLAYER_LIMIT_I; } bool PlayerColor::isSpectator() const { - return num == 252; + return num == SPECTATOR.num; } std::string PlayerColor::getStr(bool L10n) const diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index df831c513..c81342752 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -184,9 +184,9 @@ std::pair CGameState::pickObject (CGObjectInstance *obj) { PlayerColor align = (dynamic_cast(obj))->alignmentToPlayer; si32 f; // can be negative (for random) - if(align >= PlayerColor::PLAYER_LIMIT) //same as owner / random + if(!align.isValidPlayer()) //same as owner / random { - if(obj->tempOwner >= PlayerColor::PLAYER_LIMIT) + if(!obj->tempOwner.isValidPlayer()) f = -1; //random else f = scenarioOps->getIthPlayersSettings(obj->tempOwner).castle; @@ -1699,7 +1699,7 @@ PlayerColor CGameState::checkForStandardWin() const TeamID winnerTeam = TeamID::NO_TEAM; for(const auto & elem : players) { - if(elem.second.status == EPlayerStatus::INGAME && elem.first < PlayerColor::PLAYER_LIMIT) + if(elem.second.status == EPlayerStatus::INGAME && elem.first.isValidPlayer()) { if(supposedWinner == PlayerColor::NEUTRAL) { diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index fc54bb44a..37f7fd950 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -143,7 +143,7 @@ void CArmedInstance::armyChanged() CBonusSystemNode & CArmedInstance::whereShouldBeAttached(CGameState * gs) { - if(tempOwner < PlayerColor::PLAYER_LIMIT) + if(tempOwner.isValidPlayer()) if(auto * where = gs->getPlayerState(tempOwner)) return *where; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index cf2373dfb..9363a29cb 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -489,7 +489,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const //give resources if there's a Mystic Pond if (hasBuilt(BuildingSubID::MYSTIC_POND) && cb->getDate(Date::DAY) != 1 - && (tempOwner < PlayerColor::PLAYER_LIMIT) + && (tempOwner.isValidPlayer()) ) { int resID = rand.nextInt(2, 5); //bonus to random rare resource diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 2779765d9..24c32a4e4 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1568,7 +1568,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const h->showInfoDialog(69); giveBonusTo(h->tempOwner); - if(oldOwner < PlayerColor::PLAYER_LIMIT) //remove bonus from old owner + if(oldOwner.isValidPlayer()) //remove bonus from old owner { RemoveBonus rb(GiveBonus::ETarget::PLAYER); rb.whoID = oldOwner.getNum(); @@ -1581,7 +1581,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const void CGLighthouse::initObj(CRandomGenerator & rand) { - if(tempOwner < PlayerColor::PLAYER_LIMIT) + if(tempOwner.isValidPlayer()) { // FIXME: This is dirty hack giveBonusTo(tempOwner, true); diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 5c8a13d97..45b418737 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -147,14 +147,14 @@ TerrainId MapReaderH3M::readTerrain() RoadId MapReaderH3M::readRoad() { RoadId result(readInt8()); - assert(result.getNum() < features.roadsCount); + assert(result.getNum() <= features.roadsCount); return result; } RiverId MapReaderH3M::readRiver() { RiverId result(readInt8()); - assert(result.getNum() < features.riversCount); + assert(result.getNum() <= features.riversCount); return result; } @@ -188,17 +188,24 @@ SpellID MapReaderH3M::readSpell32() PlayerColor MapReaderH3M::readPlayer() { - PlayerColor result(readUInt8()); - assert(result < PlayerColor::PLAYER_LIMIT || result == PlayerColor::NEUTRAL); - return result; + uint8_t value = readUInt8(); + + if (value == 255) + return PlayerColor::NEUTRAL; + + assert(value < PlayerColor::PLAYER_LIMIT_I); + return PlayerColor(value); } PlayerColor MapReaderH3M::readPlayer32() { - PlayerColor result(readInt32()); + uint32_t value = readUInt32(); - assert(result < PlayerColor::PLAYER_LIMIT || result == PlayerColor::NEUTRAL); - return result; + if (value == 255) + return PlayerColor::NEUTRAL; + + assert(value < PlayerColor::PLAYER_LIMIT_I); + return PlayerColor(value); } void MapReaderH3M::readBitmaskBuildings(std::set & dest, std::optional faction) diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index d781b680f..ce797921c 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -227,7 +227,7 @@ void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player) if(sur->format() == QImage::Format_Indexed8) { auto palette = sur->colorTable(); - if(player < PlayerColor::PLAYER_LIMIT) + if(player.isValidPlayer()) { for(int i = 0; i < 32; ++i) palette[224 + i] = playerColorPalette[player.getNum() * 32 + i]; diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 9d09cea0c..f5f3790ed 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -419,7 +419,7 @@ QRgb MapHandler::getTileColor(int x, int y, int z) if(player == PlayerColor::NEUTRAL) return graphics->neutralColor; else - if (player < PlayerColor::PLAYER_LIMIT) + if (player.isValidPlayer()) return graphics->playerColors[player.getNum()]; } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 74f426a4d..7c4745bd0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -804,7 +804,7 @@ void CGameHandler::onNewTurn() setPortalDwelling(t, true, (n.specialWeek == NewTurn::PLAGUE ? true : false)); //set creatures for Portal of Summoning if (!firstTurn) - if (t->hasBuilt(BuildingSubID::TREASURY) && player < PlayerColor::PLAYER_LIMIT) + if (t->hasBuilt(BuildingSubID::TREASURY) && player.isValidPlayer()) n.res[player][EGameResID::GOLD] += hadGold.at(player)/10; //give 10% of starting gold if (!vstd::contains(n.cres, t->id)) @@ -845,7 +845,7 @@ void CGameHandler::onNewTurn() } } } - if (!firstTurn && player < PlayerColor::PLAYER_LIMIT)//not the first day and town not neutral + if (!firstTurn && player.isValidPlayer())//not the first day and town not neutral { n.res[player] = n.res[player] + t->dailyIncome(); } @@ -1312,13 +1312,13 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne const CGTownInstance * town = dynamic_cast(obj); if (town) //town captured { - if (owner < PlayerColor::PLAYER_LIMIT) //new owner is real player + if (owner.isValidPlayer()) //new owner is real player { if (town->hasBuilt(BuildingSubID::PORTAL_OF_SUMMONING)) setPortalDwelling(town, true, false); } - if (oldOwner < PlayerColor::PLAYER_LIMIT) //old owner is real player + if (oldOwner.isValidPlayer()) //old owner is real player { if (getPlayerState(oldOwner)->towns.empty() && getPlayerState(oldOwner)->status != EPlayerStatus::LOSER) //previous player lost last last town { diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index b49392edf..654f225ed 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -164,10 +164,10 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) PlayerColor player = market->tempOwner; - if(player >= PlayerColor::PLAYER_LIMIT) + if(!player.isValidPlayer()) player = gh.getTile(market->visitablePos())->visitableObjects.back()->tempOwner; - if(player >= PlayerColor::PLAYER_LIMIT) + if(!player.isValidPlayer()) gh.throwAndComplain(&pack, "No player can use this market!"); bool allyTownSkillTrade = (pack.mode == EMarketMode::RESOURCE_SKILL && gh.getPlayerRelations(player, hero->tempOwner) == PlayerRelations::ALLIES); diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 1881f8083..b9c9ae227 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -270,7 +270,7 @@ std::vector HeroPoolProcessor::findAvailableHeroesFor(const Pl const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerColor & player) { - if(player >= PlayerColor::PLAYER_LIMIT) + if(!player.isValidPlayer()) { logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr()); return nullptr; From 4137a66f2ae23b875b8bd5d834aac7900bc2c2fc Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 00:53:09 +0200 Subject: [PATCH 0207/1248] fix division by zero edge case --- client/mapView/MapView.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 9371f7470..582ac725d 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -148,8 +148,11 @@ void MapView::postSwipe(uint32_t msPassed) { uint32_t timediff = swipeHistory.rbegin()->first - firstAccepted.first; - postSwipeAngle = diff.angle(); - postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + if(diff.length() > 0 && timediff > 0) + { + postSwipeAngle = diff.angle(); + postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + } } swipeHistory.clear(); } else @@ -205,4 +208,4 @@ PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, con controller->activatePuzzleMapContext(tileToCenter); controller->setViewCenter(tileToCenter); -} +} \ No newline at end of file From d1d0d6d62e81a2dccc3787a496cfc8a7f301a5a8 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 02:35:10 +0200 Subject: [PATCH 0208/1248] fps independent --- client/mapView/MapView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 582ac725d..12b4b83fa 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -163,7 +163,7 @@ void MapView::postSwipe(uint32_t msPassed) { controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); - postSwipeSpeed /= 1.1; + postSwipeSpeed /= 1 + msPassed * 0.006; } } @@ -208,4 +208,4 @@ PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, con controller->activatePuzzleMapContext(tileToCenter); controller->setViewCenter(tileToCenter); -} \ No newline at end of file +} From b77bccdc24be8f77f924e6124b35be3407167e37 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 12:09:26 +0200 Subject: [PATCH 0209/1248] code review --- client/mapView/MapView.cpp | 8 ++++---- client/mapView/MapView.h | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 12b4b83fa..41edef10b 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -139,14 +139,14 @@ void MapView::postSwipe(uint32_t msPassed) { std::pair firstAccepted; uint32_t now = GH.input().getTicks(); for (auto & x : swipeHistory) { - if(now - x.first < 150) { // only the last 150 ms are catched + if(now - x.first < postSwipeCatchIntervalMs) { // only the last x ms are catched if(firstAccepted.first == 0) firstAccepted = x; diff += x.second; } } - uint32_t timediff = swipeHistory.rbegin()->first - firstAccepted.first; + uint32_t timediff = swipeHistory.back().first - firstAccepted.first; if(diff.length() > 0 && timediff > 0) { @@ -157,13 +157,13 @@ void MapView::postSwipe(uint32_t msPassed) { swipeHistory.clear(); } else postSwipeSpeed = 0.0; - if(postSwipeSpeed > 0.1) { + if(postSwipeSpeed > postSwipeMinimalSpeed) { double len = postSwipeSpeed * static_cast(msPassed); Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); - postSwipeSpeed /= 1 + msPassed * 0.006; + postSwipeSpeed /= 1 + msPassed * postSwipeSlowdownSpeed; } } diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index 2b377761e..d5c7abaaf 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -52,6 +52,10 @@ class MapView : public BasicMapView std::vector> swipeHistory; double postSwipeAngle = 0.0; double postSwipeSpeed = 0.0; + + const int postSwipeCatchIntervalMs = 150; + const double postSwipeSlowdownSpeed = 0.006; + const double postSwipeMinimalSpeed = 0.1; void postSwipe(uint32_t msPassed); From f16b93c391688899bf1bb6c6296eb5b3a0b981ff Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 14:00:40 +0200 Subject: [PATCH 0210/1248] dead area for zoom --- client/mapView/MapViewController.cpp | 13 ++++++++++--- client/mapView/MapViewController.h | 6 +++++- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 09f6589d6..46668da5b 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -90,13 +90,13 @@ void MapViewController::modifyTileSize(int stepsChange) // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale // try to determine current zooming level and change it by requested number of steps - double currentZoomFactor = model->getSingleTileSize().x / 32.0; + double currentZoomFactor = targetTileSize.x / static_cast(defaultTileSize); double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; double newZoomFactor = std::pow(1.01, newZoomSteps); - Point currentZoom = model->getSingleTileSize(); - Point desiredZoom = Point(32,32) * newZoomFactor; + Point currentZoom = targetTileSize; + Point desiredZoom = Point(defaultTileSize,defaultTileSize) * newZoomFactor; if (desiredZoom == currentZoom && stepsChange < 0) desiredZoom -= Point(1,1); @@ -112,7 +112,14 @@ void MapViewController::modifyTileSize(int stepsChange) }; if (actualZoom != currentZoom) + { + targetTileSize = actualZoom; + if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea) + actualZoom.x = defaultTileSize; + if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) + actualZoom.y = defaultTileSize; setTileSize(actualZoom); + } } MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index bc67e90de..32db5b7c4 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -10,9 +10,9 @@ #pragma once #include "IMapRendererObserver.h" +#include "../../lib/Point.h" VCMI_LIB_NAMESPACE_BEGIN -class Point; struct ObjectPosInfo; VCMI_LIB_NAMESPACE_END @@ -50,6 +50,10 @@ class MapViewController : public IMapObjectObserver std::shared_ptr puzzleMapContext; private: + const int defaultTileSize = 32; + const int zoomTileDeadArea = 4; + Point targetTileSize = Point(32, 32); + bool isEventInstant(const CGObjectInstance * obj); bool isEventVisible(const CGObjectInstance * obj); bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); From 2fef0e8d40b1e40ca0ed1342fc50fa4c2d9f4adf Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 26 Aug 2023 16:26:05 +0300 Subject: [PATCH 0211/1248] Fix market of time, crashes game --- config/objects/generic.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/config/objects/generic.json b/config/objects/generic.json index 602f314cc..49c6e01ca 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -870,7 +870,15 @@ }, "marketOfTime" : { // Unused/not implemented H3 object present on some RoE maps "index" :50, - "handler": "generic" + "handler": "generic", + "types" : { + "object" : { + "index" : 0, + "aiValue" : 0, + "rmg" : { + } + } + } }, "tavern" : { "index" :95, From 4b807e01f1e4d490f315374468a26356d873de40 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 27 Aug 2023 20:44:03 +0300 Subject: [PATCH 0212/1248] BattleAI: fix freeze --- AI/BattleAI/AttackPossibility.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 91cc7f62f..9a34420ea 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -30,7 +30,7 @@ void DamageCache::buildDamageCache(std::shared_ptr hb, int sid { auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool { - return true; + return u->isValidTarget(); }); std::vector ourUnits, enemyUnits; From 3e502955b32bac05af4a28d62ac7815fa55775a3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 22:34:02 +0400 Subject: [PATCH 0213/1248] Support FilledTexturePlayerColored in configurable interface --- client/gui/InterfaceObjectConfigurable.cpp | 20 +++++++++++++++++++- client/gui/InterfaceObjectConfigurable.h | 1 + 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 37cb233f3..d8ffc8957 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -28,6 +28,7 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../../lib//constants/StringConstants.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/filesystem/ResourceID.h" @@ -219,8 +220,19 @@ ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const } logGlobal->debug("Uknown color attribute"); return Colors::DEFAULT_KEY_COLOR; - + } + +PlayerColor InterfaceObjectConfigurable::readPlayerColor(const JsonNode & config) const +{ + logGlobal->debug("Reading PlayerColor"); + if(!config.isNull() && config.isString()) + return PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, config.String())); + + logGlobal->debug("Unknown PlayerColor attribute"); + return PlayerColor::CANNOT_DETERMINE; +} + EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const { logGlobal->debug("Reading font"); @@ -510,6 +522,12 @@ std::shared_ptr InterfaceObjectConfigurable::buildTexture(const logGlobal->debug("Building widget CFilledTexture"); auto image = config["image"].String(); auto rect = readRect(config["rect"]); + auto playerColor = readPlayerColor(config["color"]); + if(playerColor.isValidPlayer()) + { + auto result = std::make_shared(image, rect); + result->playerColored(playerColor); + } return std::make_shared(image, rect); } diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 86a3381c0..b8a155b26 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -80,6 +80,7 @@ protected: std::string readText(const JsonNode &) const; std::pair readHintText(const JsonNode &) const; EShortcut readHotkey(const JsonNode &) const; + PlayerColor readPlayerColor(const JsonNode &) const; void loadToggleButtonCallback(std::shared_ptr button, const JsonNode & config) const; void loadButtonCallback(std::shared_ptr button, const JsonNode & config) const; From 3960e5aa324385b16792778a2693e03aae20003d Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:55:37 +0200 Subject: [PATCH 0214/1248] Update client/mapView/MapView.cpp Co-authored-by: Ivan Savenko --- client/mapView/MapView.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 41edef10b..bdcaf33b1 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -130,7 +130,8 @@ void MapView::onMapSwiped(const Point & viewPosition) controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); } -void MapView::postSwipe(uint32_t msPassed) { +void MapView::postSwipe(uint32_t msPassed) +{ if(!actions->dragActive) { if(swipeHistory.size() > 1) From da3014bd545d5877bbf4b8793a1ae2ed16e7100c Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 21:40:56 +0200 Subject: [PATCH 0215/1248] Haptic feedback and increased dead zone --- client/mapView/MapViewController.cpp | 9 +++++++++ client/mapView/MapViewController.h | 3 ++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 46668da5b..5d865ec69 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -20,6 +20,7 @@ #include "../adventureMap/AdventureMapInterface.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" @@ -118,6 +119,14 @@ void MapViewController::modifyTileSize(int stepsChange) actualZoom.x = defaultTileSize; if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) actualZoom.y = defaultTileSize; + + bool isInDeadZone = targetTileSize != actualZoom; + + if(!wasInDeadZone && isInDeadZone) + GH.input().hapticFeedback(); + + wasInDeadZone = isInDeadZone; + setTileSize(actualZoom); } } diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index 32db5b7c4..3a23b917e 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -51,8 +51,9 @@ class MapViewController : public IMapObjectObserver private: const int defaultTileSize = 32; - const int zoomTileDeadArea = 4; + const int zoomTileDeadArea = 5; Point targetTileSize = Point(32, 32); + bool wasInDeadZone = true; bool isEventInstant(const CGObjectInstance * obj); bool isEventVisible(const CGObjectInstance * obj); From 3ce9d022d5d5d292740c9e5776b23fbdbc04ed3e Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 22:00:20 +0200 Subject: [PATCH 0216/1248] try to remove glitch --- client/mapView/MapView.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index bdcaf33b1..021ce5ea6 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -156,8 +156,7 @@ void MapView::postSwipe(uint32_t msPassed) } } swipeHistory.clear(); - } else - postSwipeSpeed = 0.0; + } if(postSwipeSpeed > postSwipeMinimalSpeed) { double len = postSwipeSpeed * static_cast(msPassed); Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); From 1c89c71031040aacc2fc063f6b8d2e0e946346cb Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 22:55:05 +0200 Subject: [PATCH 0217/1248] Update MapViewController.cpp --- client/mapView/MapViewController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 5d865ec69..736fb424f 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -120,7 +120,7 @@ void MapViewController::modifyTileSize(int stepsChange) if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) actualZoom.y = defaultTileSize; - bool isInDeadZone = targetTileSize != actualZoom; + bool isInDeadZone = targetTileSize != actualZoom || actualZoom == defaultTileSize; if(!wasInDeadZone && isInDeadZone) GH.input().hapticFeedback(); From b61890355f13a493c0e8f1478ede093c907bf2a2 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 27 Aug 2023 23:17:47 +0200 Subject: [PATCH 0218/1248] Update MapViewController.cpp --- client/mapView/MapViewController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 736fb424f..d9be1d728 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -120,7 +120,7 @@ void MapViewController::modifyTileSize(int stepsChange) if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) actualZoom.y = defaultTileSize; - bool isInDeadZone = targetTileSize != actualZoom || actualZoom == defaultTileSize; + bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize); if(!wasInDeadZone && isInDeadZone) GH.input().hapticFeedback(); From 9f51f421284b45b514efa062fdae5f691bd89242 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 28 Aug 2023 00:49:32 +0200 Subject: [PATCH 0219/1248] use fps in calculation --- client/mapView/MapView.cpp | 3 +++ client/mapView/MapView.h | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 021ce5ea6..7444bd74c 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -103,6 +103,9 @@ MapView::MapView(const Point & offset, const Point & dimensions) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; actions = std::make_shared(*this, model); actions->setContext(controller->getContext()); + + // catch min 10 frames + postSwipeCatchIntervalMs = static_cast(10.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float())); } void MapView::onMapLevelSwitched() diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index d5c7abaaf..c95839638 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -52,8 +52,8 @@ class MapView : public BasicMapView std::vector> swipeHistory; double postSwipeAngle = 0.0; double postSwipeSpeed = 0.0; - - const int postSwipeCatchIntervalMs = 150; + + int postSwipeCatchIntervalMs; const double postSwipeSlowdownSpeed = 0.006; const double postSwipeMinimalSpeed = 0.1; From 220145bd9b77bab835a52efe974af8a40035ee65 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 28 Aug 2023 00:59:08 +0200 Subject: [PATCH 0220/1248] reintroduce setting to zero while holding --- client/mapView/MapView.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 7444bd74c..215bce8dc 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -104,7 +104,7 @@ MapView::MapView(const Point & offset, const Point & dimensions) actions = std::make_shared(*this, model); actions->setContext(controller->getContext()); - // catch min 10 frames +// catch min 10 frames postSwipeCatchIntervalMs = static_cast(10.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float())); } @@ -159,7 +159,8 @@ void MapView::postSwipe(uint32_t msPassed) } } swipeHistory.clear(); - } + } else + postSwipeSpeed = 0.0; if(postSwipeSpeed > postSwipeMinimalSpeed) { double len = postSwipeSpeed * static_cast(msPassed); Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); From 3c1892a7d2c3d2baf3d94459ffc9f2fdea5596fe Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:03:50 +0200 Subject: [PATCH 0221/1248] adjust timing --- client/mapView/MapView.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index 215bce8dc..cc7e0dbee 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -104,8 +104,8 @@ MapView::MapView(const Point & offset, const Point & dimensions) actions = std::make_shared(*this, model); actions->setContext(controller->getContext()); -// catch min 10 frames - postSwipeCatchIntervalMs = static_cast(10.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float())); + // catch min 6 frames + postSwipeCatchIntervalMs = std::max(100, static_cast(6.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float()))); } void MapView::onMapLevelSwitched() From 6e7b13fcbeca9eca8457edb6bbc37dffe606f3a7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 16:21:46 +0400 Subject: [PATCH 0222/1248] Add compatibility with minor version --- lib/modding/CModVersion.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 6e43fb2b6..7d7ff03de 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -31,7 +31,7 @@ struct DLL_LINKAGE CModVersion static CModVersion fromString(std::string from); std::string toString() const; - bool compatible(const CModVersion & other, bool checkMinor = false, bool checkPatch = false) const; + bool compatible(const CModVersion & other, bool checkMinor = true, bool checkPatch = false) const; bool isNull() const; template void serialize(Handler &h, const int version) From 39fbdd300d461fcddb4dfdd6852e324fe66402b6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 14:07:22 +0400 Subject: [PATCH 0223/1248] Refactor combo box --- client/widgets/ComboBox.cpp | 8 ++++++++ client/widgets/ComboBox.h | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 client/widgets/ComboBox.cpp create mode 100644 client/widgets/ComboBox.h diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp new file mode 100644 index 000000000..2041c6868 --- /dev/null +++ b/client/widgets/ComboBox.cpp @@ -0,0 +1,8 @@ +// +// ComboBox.cpp +// vcmiclient +// +// Created by nordsoft on 24.08.2023. +// + +#include "ComboBox.hpp" diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h new file mode 100644 index 000000000..2b447b995 --- /dev/null +++ b/client/widgets/ComboBox.h @@ -0,0 +1,13 @@ +// +// ComboBox.hpp +// vcmiclient +// +// Created by nordsoft on 24.08.2023. +// + +#ifndef ComboBox_hpp +#define ComboBox_hpp + +#include + +#endif /* ComboBox_hpp */ From c064b805c28923f1dfc0783bf58a6e3fa0c802d0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 14:07:38 +0400 Subject: [PATCH 0224/1248] Refactor combo box --- client/gui/InterfaceObjectConfigurable.cpp | 28 +++ client/gui/InterfaceObjectConfigurable.h | 2 + client/lobby/RandomMapTab.cpp | 192 ++++----------------- client/lobby/RandomMapTab.h | 37 ---- client/widgets/Buttons.h | 2 +- client/widgets/ComboBox.cpp | 182 ++++++++++++++++++- client/widgets/ComboBox.h | 75 ++++++-- 7 files changed, 301 insertions(+), 217 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 37cb233f3..2fff4284c 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -20,6 +20,7 @@ #include "../render/Graphics.h" #include "../render/IFont.h" #include "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" @@ -52,6 +53,7 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("labelGroup", &InterfaceObjectConfigurable::buildLabelGroup); REGISTER_BUILDER("slider", &InterfaceObjectConfigurable::buildSlider); REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout); + REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox); } void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f) @@ -513,6 +515,32 @@ std::shared_ptr InterfaceObjectConfigurable::buildTexture(const return std::make_shared(image, rect); } +std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonNode & config) +{ + logGlobal->debug("Building widget ComboBox"); + auto position = readPosition(config["position"]); + auto image = config["image"].String(); + auto help = readHintText(config["help"]); + auto result = std::make_shared(position, image, help, config["dropDown"]); + if(!config["items"].isNull()) + { + for(const auto & item : config["items"].Vector()) + { + result->addOverlay(buildWidget(item)); + } + } + if(!config["imageOrder"].isNull()) + { + auto imgOrder = config["imageOrder"].Vector(); + assert(imgOrder.size() >= 4); + result->setImageOrder(imgOrder[0].Integer(), imgOrder[1].Integer(), imgOrder[2].Integer(), imgOrder[3].Integer()); + } + + loadButtonBorderColor(result, config["borderColor"]); + loadButtonHotkey(result, config["hotkey"]); + return result; +} + /// Small helper class that provides ownership for shared_ptr's of child elements class InterfaceLayoutWidget : public CIntObject { diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 86a3381c0..784220198 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -27,6 +27,7 @@ class CSlider; class CAnimImage; class CShowableAnim; class CFilledTexture; +class ComboBox; #define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1)) @@ -99,6 +100,7 @@ protected: std::shared_ptr buildAnimation(const JsonNode &) const; std::shared_ptr buildTexture(const JsonNode &) const; std::shared_ptr buildLayout(const JsonNode &); + std::shared_ptr buildComboBox(const JsonNode &); //composite widgets std::shared_ptr buildWidget(JsonNode config) const; diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 88f015e9b..fad8a28eb 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -18,6 +18,7 @@ #include "../gui/MouseButton.h" #include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" #include "../widgets/MiscWidgets.h" #include "../widgets/ObjectLists.h" @@ -102,11 +103,6 @@ RandomMapTab::RandomMapTab(): }); //new callbacks available only from mod - addCallback("templateSelection", [&](int) - { - GH.windows().createAndPushWindow(*this, int3{mapGenOptions->getWidth(), mapGenOptions->getHeight(), 1 + mapGenOptions->getHasTwoLevels()}); - }); - addCallback("teamAlignments", [&](int) { GH.windows().createAndPushWindow(*this); @@ -125,6 +121,35 @@ RandomMapTab::RandomMapTab(): const JsonNode config(ResourceID("config/widgets/randomMapTab.json")); build(config); + //set combo box callbacks + if(auto w = widget("templateList")) + { + w->onConstructItems = [](std::vector & curItems){ + auto templates = VLC->tplh->getTemplates(); + + boost::range::sort(templates, [](const CRmgTemplate * a, const CRmgTemplate * b){ + return a->getName() < b->getName(); + }); + + curItems.push_back(nullptr); //default template + + for(auto & t : templates) + curItems.push_back(t); + }; + + w->onSetItem = [&](const void * item){ + this->setTemplate(reinterpret_cast(item)); + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + return reinterpret_cast(item)->getName(); + if(idx == 0) + return readText(variables["randomTemplate"]); + return std::string(""); + }; + } + updateMapInfoByHost(); } @@ -360,163 +385,6 @@ std::vector RandomMapTab::getPossibleMapSizes() return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_GIANT}; } -TemplatesDropBox::ListItem::ListItem(const JsonNode & config, TemplatesDropBox & _dropBox, Point position) - : InterfaceObjectConfigurable(LCLICK | HOVER, position), - dropBox(_dropBox) -{ - OBJ_CONSTRUCTION; - - build(config); - - if(auto w = widget("hoverImage")) - { - pos.w = w->pos.w; - pos.h = w->pos.h; - } - setRedrawParent(true); -} - -void TemplatesDropBox::ListItem::updateItem(int idx, const CRmgTemplate * _item) -{ - if(auto w = widget("labelName")) - { - item = _item; - if(item) - { - w->setText(item->getName()); - } - else - { - if(idx) - w->setText(""); - else - w->setText(readText(dropBox.variables["randomTemplate"])); - } - } -} - -void TemplatesDropBox::ListItem::hover(bool on) -{ - auto h = widget("hoverImage"); - auto w = widget("labelName"); - if(h && w) - { - if(w->getText().empty()) - h->visible = false; - else - h->visible = on; - } - redraw(); -} - -void TemplatesDropBox::ListItem::clickPressed(const Point & cursorPosition) -{ - if(isHovered()) - dropBox.setTemplate(item); -} - -void TemplatesDropBox::ListItem::clickReleased(const Point & cursorPosition) -{ - dropBox.clickPressed(cursorPosition); - dropBox.clickReleased(cursorPosition); -} - -TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size): - InterfaceObjectConfigurable(LCLICK | HOVER), - randomMapTab(randomMapTab) -{ - REGISTER_BUILDER("templateListItem", &TemplatesDropBox::buildListItem); - - curItems = VLC->tplh->getTemplates(); - - boost::range::sort(curItems, [](const CRmgTemplate * a, const CRmgTemplate * b){ - return a->getName() < b->getName(); - }); - - curItems.insert(curItems.begin(), nullptr); //default template - - const JsonNode config(ResourceID("config/widgets/randomMapTemplateWidget.json")); - - addCallback("sliderMove", std::bind(&TemplatesDropBox::sliderMove, this, std::placeholders::_1)); - - OBJ_CONSTRUCTION; - pos = randomMapTab.pos; - - build(config); - - if(auto w = widget("slider")) - { - w->setAmount(curItems.size()); - } - - //FIXME: this should be done by InterfaceObjectConfigurable, but might have side-effects - pos = children.front()->pos; - for (auto const & child : children) - pos = pos.include(child->pos); - - updateListItems(); -} - -std::shared_ptr TemplatesDropBox::buildListItem(const JsonNode & config) -{ - auto position = readPosition(config["position"]); - listItems.push_back(std::make_shared(config, *this, position)); - return listItems.back(); -} - -void TemplatesDropBox::sliderMove(int slidPos) -{ - auto w = widget("slider"); - if(!w) - return; // ignore spurious call when slider is being created - updateListItems(); - redraw(); -} - -bool TemplatesDropBox::receiveEvent(const Point & position, int eventType) const -{ - if (eventType == LCLICK) - return true; // we want drop box to close when clicking outside drop box borders - - return CIntObject::receiveEvent(position, eventType); -} - -void TemplatesDropBox::clickPressed(const Point & cursorPosition) -{ - if (!pos.isInside(cursorPosition)) - { - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); - } -} - -void TemplatesDropBox::updateListItems() -{ - if(auto w = widget("slider")) - { - int elemIdx = w->getValue(); - for(auto item : listItems) - { - if(elemIdx < curItems.size()) - { - item->updateItem(elemIdx, curItems[elemIdx]); - elemIdx++; - } - else - { - item->updateItem(elemIdx); - } - } - } -} - -void TemplatesDropBox::setTemplate(const CRmgTemplate * tmpl) -{ - randomMapTab.setTemplate(tmpl); - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); -} - TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index 356085ab5..e23657548 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -51,43 +51,6 @@ private: std::set playerCountAllowed, playerTeamsAllowed, compCountAllowed, compTeamsAllowed; }; -class TemplatesDropBox : public InterfaceObjectConfigurable -{ - struct ListItem : public InterfaceObjectConfigurable - { - TemplatesDropBox & dropBox; - const CRmgTemplate * item = nullptr; - - ListItem(const JsonNode &, TemplatesDropBox &, Point position); - void updateItem(int index, const CRmgTemplate * item = nullptr); - - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void clickReleased(const Point & cursorPosition) override; - }; - - friend struct ListItem; - -public: - TemplatesDropBox(RandomMapTab & randomMapTab, int3 size); - - bool receiveEvent(const Point & position, int eventType) const override; - void clickPressed(const Point & cursorPosition) override; - void setTemplate(const CRmgTemplate *); - -private: - std::shared_ptr buildListItem(const JsonNode & config); - - void sliderMove(int slidPos); - void updateListItems(); - - RandomMapTab & randomMapTab; - std::vector> listItems; - - std::vector curItems; - -}; - class TeamAlignmentsWidget: public InterfaceObjectConfigurable { public: diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 32c4ab3b9..ec387451f 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -35,7 +35,7 @@ public: BLOCKED=2, HIGHLIGHTED=3 }; -private: +protected: std::vector imageNames;//store list of images that can be used by this button size_t currentImage; diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 2041c6868..3e85c0cb6 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -1,8 +1,176 @@ -// -// ComboBox.cpp -// vcmiclient -// -// Created by nordsoft on 24.08.2023. -// +/* + * ComboBox.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 "ComboBox.h" -#include "ComboBox.hpp" +#include "Slider.h" +#include "Images.h" +#include "TextControls.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" + +ComboBox::DropDown::Item::Item(const JsonNode & config, ComboBox::DropDown & _dropDown, Point position) + : InterfaceObjectConfigurable(LCLICK | HOVER, position), + dropDown(_dropDown) +{ + build(config); + + if(auto w = widget("hoverImage")) + { + pos.w = w->pos.w; + pos.h = w->pos.h; + } + setRedrawParent(true); +} + +void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) +{ + if(auto w = widget("labelName")) + { + item = _item; + w->setText(dropDown.comboBox.getItemText(idx, item)); + } +} + +void ComboBox::DropDown::Item::hover(bool on) +{ + auto h = widget("hoverImage"); + auto w = widget("labelName"); + if(h && w) + { + if(w->getText().empty()) + h->visible = false; + else + h->visible = on; + } + redraw(); +} + +void ComboBox::DropDown::Item::clickPressed(const Point & cursorPosition) +{ + if(isHovered()) + dropDown.setItem(item); +} + +void ComboBox::DropDown::Item::clickReleased(const Point & cursorPosition) +{ + dropDown.clickPressed(cursorPosition); + dropDown.clickReleased(cursorPosition); +} + +ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): + InterfaceObjectConfigurable(LCLICK | HOVER), + comboBox(_comboBox) +{ + REGISTER_BUILDER("item", &ComboBox::DropDown::buildItem); + + comboBox.onConstructItems(curItems); + + addCallback("sliderMove", std::bind(&ComboBox::DropDown::sliderMove, this, std::placeholders::_1)); + + pos = comboBox.pos; + + build(config); + + if(auto w = widget("slider")) + { + w->setAmount(curItems.size()); + } + + //FIXME: this should be done by InterfaceObjectConfigurable, but might have side-effects + pos = children.front()->pos; + for (auto const & child : children) + pos = pos.include(child->pos); + + updateListItems(); +} + +std::shared_ptr ComboBox::DropDown::buildItem(const JsonNode & config) +{ + auto position = readPosition(config["position"]); + items.push_back(std::make_shared(config, *this, position)); + return items.back(); +} + +void ComboBox::DropDown::sliderMove(int slidPos) +{ + auto w = widget("slider"); + if(!w) + return; // ignore spurious call when slider is being created + updateListItems(); + redraw(); +} + +bool ComboBox::DropDown::receiveEvent(const Point & position, int eventType) const +{ + if (eventType == LCLICK) + return true; // we want drop box to close when clicking outside drop box borders + + return CIntObject::receiveEvent(position, eventType); +} + +void ComboBox::DropDown::clickPressed(const Point & cursorPosition) +{ + if (!pos.isInside(cursorPosition)) + { + assert(GH.windows().isTopWindow(this)); + GH.windows().popWindows(1); + } +} + +void ComboBox::DropDown::updateListItems() +{ + if(auto w = widget("slider")) + { + int elemIdx = w->getValue(); + for(auto item : items) + { + if(elemIdx < curItems.size()) + { + item->updateItem(elemIdx, curItems[elemIdx]); + elemIdx++; + } + else + { + item->updateItem(elemIdx); + } + } + } +} + +void ComboBox::DropDown::setItem(const void * item) +{ + comboBox.setItem(item); + + assert(GH.windows().isTopWindow(this)); + GH.windows().popWindows(1); +} + +void ComboBox::DropDown::constructItems() +{ + comboBox.onConstructItems(curItems); +} + +ComboBox::ComboBox(Point position, const std::string & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): + CButton(position, defName, help, 0, key, playerColoredButton) +{ + addCallback([&, dropDownDescriptor]() + { + GH.windows().createAndPushWindow(dropDownDescriptor, *this); + }); +} + +void ComboBox::setItem(const void * item) +{ + if(auto w = std::dynamic_pointer_cast(overlay)) + addTextOverlay(getItemText(0, item), w->font, w->color); + + onSetItem(item); +} diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index 2b447b995..01b3bc4e3 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -1,13 +1,68 @@ -// -// ComboBox.hpp -// vcmiclient -// -// Created by nordsoft on 24.08.2023. -// +/* + * ComboBox.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 -#ifndef ComboBox_hpp -#define ComboBox_hpp +#include "../gui/InterfaceObjectConfigurable.h" +#include "Buttons.h" -#include +class ComboBox : public CButton +{ + class DropDown : public InterfaceObjectConfigurable + { + struct Item : public InterfaceObjectConfigurable + { + DropDown & dropDown; + const void * item = nullptr; + + Item(const JsonNode &, ComboBox::DropDown &, Point position); + void updateItem(int index, const void * item = nullptr); + + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; + }; + + friend struct Item; + + public: + DropDown(const JsonNode &, ComboBox &); + + void constructItems(); + bool receiveEvent(const Point & position, int eventType) const override; + void clickPressed(const Point & cursorPosition) override; + void setItem(const void *); + + private: + std::shared_ptr buildItem(const JsonNode & config); + + void sliderMove(int slidPos); + void updateListItems(); + + ComboBox & comboBox; + std::vector> items; + std::vector curItems; + }; + + friend class DropDown; + + void setItem(const void *); -#endif /* ComboBox_hpp */ +public: + ComboBox(Point position, const std::string & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key = {}, bool playerColoredButton = false); + + //define this callback to fill input vector with data for the combo box + std::function &)> onConstructItems; + + //callback is called when item is selected and its value can be used + std::function onSetItem; + + //return text value from item data + std::function getItemText; +}; From c7ded69edc368f646f6367115e7dd47eb432bf15 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 14:52:23 +0400 Subject: [PATCH 0225/1248] Minor changes --- client/CMakeLists.txt | 2 ++ client/widgets/ComboBox.cpp | 16 +++++++--------- client/widgets/ComboBox.h | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index b18ccb36e..3fd04a848 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -95,6 +95,7 @@ set(client_SRCS widgets/CComponent.cpp widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp + widgets/ComboBox.cpp widgets/Images.cpp widgets/MiscWidgets.cpp widgets/ObjectLists.cpp @@ -253,6 +254,7 @@ set(client_HEADERS widgets/CComponent.h widgets/CGarrisonInt.h widgets/CreatureCostBox.h + widgets/ComboBox.h widgets/Images.h widgets/MiscWidgets.h widgets/ObjectLists.h diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 3e85c0cb6..349307b5d 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -35,7 +35,8 @@ void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) if(auto w = widget("labelName")) { item = _item; - w->setText(dropDown.comboBox.getItemText(idx, item)); + if(dropDown.comboBox.getItemText) + w->setText(dropDown.comboBox.getItemText(idx, item)); } } @@ -71,7 +72,8 @@ ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): { REGISTER_BUILDER("item", &ComboBox::DropDown::buildItem); - comboBox.onConstructItems(curItems); + if(comboBox.onConstructItems) + comboBox.onConstructItems(curItems); addCallback("sliderMove", std::bind(&ComboBox::DropDown::sliderMove, this, std::placeholders::_1)); @@ -153,11 +155,6 @@ void ComboBox::DropDown::setItem(const void * item) GH.windows().popWindows(1); } -void ComboBox::DropDown::constructItems() -{ - comboBox.onConstructItems(curItems); -} - ComboBox::ComboBox(Point position, const std::string & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton) { @@ -169,8 +166,9 @@ ComboBox::ComboBox(Point position, const std::string & defName, const std::pair< void ComboBox::setItem(const void * item) { - if(auto w = std::dynamic_pointer_cast(overlay)) + if(auto w = std::dynamic_pointer_cast(overlay); getItemText) addTextOverlay(getItemText(0, item), w->font, w->color); - onSetItem(item); + if(onSetItem) + onSetItem(item); } diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index 01b3bc4e3..960e717b2 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -34,7 +34,6 @@ class ComboBox : public CButton public: DropDown(const JsonNode &, ComboBox &); - void constructItems(); bool receiveEvent(const Point & position, int eventType) const override; void clickPressed(const Point & cursorPosition) override; void setItem(const void *); From d758727c235c9c0df1f8ae025d93b37b4cabae72 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 20:20:26 +0400 Subject: [PATCH 0226/1248] Basic timer ui is almost complete --- client/CServerHandler.cpp | 6 +- client/CServerHandler.h | 5 +- client/gui/InterfaceObjectConfigurable.cpp | 43 +++++-- client/gui/InterfaceObjectConfigurable.h | 6 +- client/lobby/OptionsTab.cpp | 132 +++++++++++++++++++-- client/widgets/ComboBox.cpp | 7 ++ client/widgets/ComboBox.h | 2 + config/widgets/optionsTab.json | 23 +++- 8 files changed, 196 insertions(+), 28 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 8a4051fed..ddf40406d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -39,6 +39,7 @@ #include "../lib/CThreadHelper.h" #include "../lib/NetPackVisitor.h" #include "../lib/StartInfo.h" +#include "../lib/TurnTimerInfo.h" #include "../lib/VCMIDirs.h" #include "../lib/campaign/CampaignState.h" #include "../lib/mapping/CMapInfo.h" @@ -475,11 +476,10 @@ void CServerHandler::setDifficulty(int to) const sendLobbyPack(lsd); } -void CServerHandler::setTurnLength(int npos) const +void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const { - vstd::amin(npos, GameConstants::POSSIBLE_TURNTIME.size() - 1); LobbySetTurnTime lstt; - lstt.turnTimerInfo.turnTimer = GameConstants::POSSIBLE_TURNTIME[npos] * 60 * 1000; + lstt.turnTimerInfo = info; sendLobbyPack(lstt); } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 777cbc29e..edecd3a64 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CConnection; class PlayerColor; struct StartInfo; +struct TurnTimerInfo; class CMapInfo; class CGameState; @@ -64,7 +65,7 @@ public: virtual void setPlayer(PlayerColor color) const = 0; virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; - virtual void setTurnLength(int npos) const = 0; + virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; virtual void sendMessage(const std::string & txt) const = 0; virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? virtual void sendStartGame(bool allowOnlyAI = false) const = 0; @@ -146,7 +147,7 @@ public: void setPlayer(PlayerColor color) const override; void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; - void setTurnLength(int npos) const override; + void setTurnTimerInfo(const TurnTimerInfo &) const override; void sendMessage(const std::string & txt) const override; void sendGuiAction(ui8 action) const override; void sendRestartGame() const override; diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 2fff4284c..60bdacbda 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -54,6 +54,7 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("slider", &InterfaceObjectConfigurable::buildSlider); REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout); REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox); + REGISTER_BUILDER("textInput", &InterfaceObjectConfigurable::buildTextInput); } void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f) @@ -63,9 +64,15 @@ void InterfaceObjectConfigurable::registerBuilder(const std::string & type, Buil void InterfaceObjectConfigurable::addCallback(const std::string & callbackName, std::function callback) { - callbacks[callbackName] = callback; + callbacks_int[callbackName] = callback; } +void InterfaceObjectConfigurable::addCallback(const std::string & callbackName, std::function callback) +{ + callbacks_string[callbackName] = callback; +} + + void InterfaceObjectConfigurable::deleteWidget(const std::string & name) { auto iter = widgets.find(name); @@ -340,7 +347,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleGroup(cons if(!config["selected"].isNull()) group->setSelected(config["selected"].Integer()); if(!config["callback"].isNull()) - group->addCallback(callbacks.at(config["callback"].String())); + group->addCallback(callbacks_int.at(config["callback"].String())); return group; } @@ -413,8 +420,8 @@ void InterfaceObjectConfigurable::loadToggleButtonCallback(std::shared_ptr 0) - button->addCallback(callbacks.at(callbackName)); + if (callbacks_int.count(callbackName) > 0) + button->addCallback(callbacks_int.at(callbackName)); else logGlobal->error("Invalid callback '%s' in widget", callbackName ); } @@ -426,8 +433,8 @@ void InterfaceObjectConfigurable::loadButtonCallback(std::shared_ptr bu std::string callbackName = config.String(); - if (callbacks.count(callbackName) > 0) - button->addCallback(std::bind(callbacks.at(callbackName), 0)); + if (callbacks_int.count(callbackName) > 0) + button->addCallback(std::bind(callbacks_int.at(callbackName), 0)); else logGlobal->error("Invalid callback '%s' in widget", callbackName ); } @@ -483,7 +490,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildSlider(const JsonNode auto value = config["selected"].Integer(); bool horizontal = config["orientation"].String() == "horizontal"; const auto & result = - std::make_shared(position, length, callbacks.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); + std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); if(!config["scrollBounds"].isNull()) { @@ -541,6 +548,26 @@ std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonN return result; } +std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const JsonNode & config) const +{ + logGlobal->debug("Building widget CTextInput"); + auto rect = readRect(config["rect"]); + auto offset = readPosition(config["backgroundOffset"]); + auto bgName = config["background"].String(); + auto result = std::make_shared(rect, offset, bgName, 0); + if(!config["alignment"].isNull()) + result->alignment = readTextAlignment(config["alignment"]); + if(!config["font"].isNull()) + result->font = readFont(config["font"]); + if(!config["color"].isNull()) + result->setColor(readColor(config["color"])); + if(!config["text"].isNull()) + result->setText(readText(config["text"])); + if(!config["callback"].isNull()) + result->cb += callbacks_string.at(config["callback"].String()); + return result; +} + /// Small helper class that provides ownership for shared_ptr's of child elements class InterfaceLayoutWidget : public CIntObject { @@ -625,7 +652,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const if(!config["alpha"].isNull()) anim->setAlpha(config["alpha"].Integer()); if(!config["callback"].isNull()) - anim->callback = std::bind(callbacks.at(config["callback"].String()), 0); + anim->callback = std::bind(callbacks_int.at(config["callback"].String()), 0); if(!config["frames"].isNull()) { auto b = config["frames"]["start"].Integer(); diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 784220198..b2558655c 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -28,6 +28,7 @@ class CAnimImage; class CShowableAnim; class CFilledTexture; class ComboBox; +class CTextInput; #define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1)) @@ -59,6 +60,7 @@ protected: void addWidget(const std::string & name, std::shared_ptr widget); void addCallback(const std::string & callbackName, std::function callback); + void addCallback(const std::string & callbackName, std::function callback); JsonNode variables; template @@ -101,6 +103,7 @@ protected: std::shared_ptr buildTexture(const JsonNode &) const; std::shared_ptr buildLayout(const JsonNode &); std::shared_ptr buildComboBox(const JsonNode &); + std::shared_ptr buildTextInput(const JsonNode &) const; //composite widgets std::shared_ptr buildWidget(JsonNode config) const; @@ -116,7 +119,8 @@ private: int unnamedObjectId = 0; std::map builders; std::map> widgets; - std::map> callbacks; + std::map> callbacks_int; + std::map> callbacks_string; std::map conditionals; std::map shortcuts; }; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index bbbd25ca5..922c37df2 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -21,6 +21,7 @@ #include "../render/Graphics.h" #include "../render/IFont.h" #include "../widgets/CComponent.h" +#include "../widgets/ComboBox.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/MiscWidgets.h" @@ -44,19 +45,118 @@ OptionsTab::OptionsTab() : humanPlayers(0) { recActions = 0; - addCallback("setTurnLength", std::bind(&IServerAPI::setTurnLength, CSH, _1)); + //addCallback("timerFieldChangedBase", <#std::function callback#>) + + addCallback("setTimerPreset", [&](int index){ + if(!variables["timerPresets"].isNull()) + { + auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); + TurnTimerInfo tinfo; + tinfo.baseTimer = tpreset.at(0).Integer() * 1000; + tinfo.turnTimer = tpreset.at(1).Integer() * 1000; + tinfo.battleTimer = tpreset.at(2).Integer() * 1000; + tinfo.creatureTimer = tpreset.at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } + }); + + auto parseTimerString = [](const std::string & str){ + std::stringstream sstrm; + int a, b; + sstrm << str; + sstrm >> a; + char c = sstrm.get(); + if(c == ':') + { + sstrm >> b; + return a * 60 + b; + } + return -1; + }; + + addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo; + tinfo.turnTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo; + tinfo.battleTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo; + tinfo.creatureTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); const JsonNode config(ResourceID("config/widgets/optionsTab.json")); build(config); - if(SEL->screenType == ESelectionScreen::newGame || SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::scenarioInfo) + //set timers combo box callbacks + if(auto w = widget("timerModeSwitch")) { - if(auto w = widget("sliderTurnDuration")) - w->deactivate(); - if(auto w = widget("labelPlayerTurnDuration")) - w->deactivate(); - if(auto w = widget("labelTurnDurationValue")) - w->deactivate(); + w->onConstructItems = [&](std::vector & curItems){ + if(variables["timers"].isNull()) + return; + + for(auto & p : variables["timers"].Vector()) + { + curItems.push_back(&p); + } + }; + + w->onSetItem = [&](const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + { + for(auto wname : (*tObj)["hideWidgets"].Vector()) + if(auto w = widget(wname.String())) + { + w->setEnabled(false); + } + for(auto wname : (*tObj)["showWidgets"].Vector()) + if(auto w = widget(wname.String())) + { + w->setEnabled(true); + } + } + redraw(); + } + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + return readText((*tObj)["text"]); + } + return std::string(""); + }; + + w->setItem(0); } } @@ -76,9 +176,19 @@ void OptionsTab::recreate() if(auto turnSlider = widget("sliderTurnDuration")) { - turnSlider->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTimerInfo.turnTimer / (60 * 1000))); - if(auto w = widget("labelTurnDurationValue")) - w->setText(CGI->generaltexth->turnDurations[turnSlider->getValue()]); + if(!variables["timerPresets"].isNull()) + { + for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) + { + auto & tpreset = variables["timerPresets"].Vector()[idx]; + if(tpreset.Vector().at(1).Integer() == SEL->getStartInfo()->turnTimerInfo.turnTimer / 1000) + { + turnSlider->scrollTo(idx); + if(auto w = widget("labelTurnDurationValue")) + w->setText(CGI->generaltexth->turnDurations[idx]); + } + } + } } } diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 349307b5d..29ac42d0c 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -172,3 +172,10 @@ void ComboBox::setItem(const void * item) if(onSetItem) onSetItem(item); } + +void ComboBox::setItem(int id) +{ + std::vector tempItems; + onConstructItems(tempItems); + setItem(tempItems.at(id)); +} diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index 960e717b2..ced39987d 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -64,4 +64,6 @@ public: //return text value from item data std::function getItemText; + + void setItem(int id); }; diff --git a/config/widgets/optionsTab.json b/config/widgets/optionsTab.json index 8e9072d78..ca3c936f2 100644 --- a/config/widgets/optionsTab.json +++ b/config/widgets/optionsTab.json @@ -75,7 +75,6 @@ // timer { - "name": "labelPlayerTurnDuration", "type": "label", "font": "small", "alignment": "center", @@ -100,7 +99,7 @@ "orientation": "horizontal", "position": {"x": 55, "y": 557}, "size": 194, - "callback": "setTurnLength", + "callback": "setTimerPreset", "itemsVisible": 1, "itemsTotal": 11, "selected": 11, @@ -108,5 +107,23 @@ "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, "panningStep": 20 }, - ] + ], + + "variables": + { + "timerPresets" : + [ + [0, 60, 0, 0], + [0, 120, 0, 0], + [0, 240, 0, 0], + [0, 360, 0, 0], + [0, 480, 0, 0], + [0, 600, 0, 0], + [0, 900, 0, 0], + [0, 1200, 0, 0], + [0, 1500, 0, 0], + [0, 1800, 0, 0], + [0, 0, 0, 0], + ] + } } From 86e0ea15fb3f35177656b718d7f3fa54014d8e24 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 20:38:30 +0400 Subject: [PATCH 0227/1248] Fix turn timer settings --- client/lobby/OptionsTab.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 922c37df2..296a7cc34 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -78,7 +78,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo; + TurnTimerInfo tinfo = CSH->si->turnTimerInfo; tinfo.baseTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -87,7 +87,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo; + TurnTimerInfo tinfo = CSH->si->turnTimerInfo; tinfo.turnTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -96,7 +96,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo; + TurnTimerInfo tinfo = CSH->si->turnTimerInfo; tinfo.battleTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -105,7 +105,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo; + TurnTimerInfo tinfo = CSH->si->turnTimerInfo; tinfo.creatureTimer = time; CSH->setTurnTimerInfo(tinfo); } From 1e527a6942f190e900360782951ebdbdeb8d8544 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 20:49:27 +0400 Subject: [PATCH 0228/1248] Minor changes --- client/battle/BattleWindow.cpp | 2 +- client/lobby/OptionsTab.cpp | 2 +- config/widgets/{battleWindow2.json => battleWindow.json} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename config/widgets/{battleWindow2.json => battleWindow.json} (100%) diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 538b60b6c..c4469540a 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - const JsonNode config(ResourceID("config/widgets/BattleWindow2.json")); + const JsonNode config(ResourceID("config/widgets/BattleWindow.json")); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 296a7cc34..55b54256f 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -176,7 +176,7 @@ void OptionsTab::recreate() if(auto turnSlider = widget("sliderTurnDuration")) { - if(!variables["timerPresets"].isNull()) + if(turnSlider->isActive() && !variables["timerPresets"].isNull()) { for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) { diff --git a/config/widgets/battleWindow2.json b/config/widgets/battleWindow.json similarity index 100% rename from config/widgets/battleWindow2.json rename to config/widgets/battleWindow.json From 6380a02e10752d6c0fb74eb0d2a799a184685d88 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 21:04:40 +0400 Subject: [PATCH 0229/1248] Upgrade launcher default repo --- config/schemas/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index dd3877cfe..139b13cb2 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -478,7 +478,7 @@ }, "defaultRepositoryURL" : { "type" : "string", - "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.3.json", + "default" : "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.4.json", }, "extraRepositoryEnabled" : { "type" : "boolean", From 3962b7126210674d18bf18f1f0fc728c306b191a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 25 Aug 2023 21:14:45 +0400 Subject: [PATCH 0230/1248] Remove unused variable --- lib/constants/NumericConstants.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index e291f4c3b..fdc2a67bd 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -52,8 +52,6 @@ namespace GameConstants constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits - - constexpr std::array POSSIBLE_TURNTIME = {1, 2, 4, 6, 8, 10, 15, 20, 25, 30, 0}; } VCMI_LIB_NAMESPACE_END From fdaf05514aa74e1527ee1190d1b65ad88cc0fe95 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 03:14:01 +0400 Subject: [PATCH 0231/1248] Correct network data exchange and time parsing --- client/lobby/OptionsTab.cpp | 102 ++++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 23 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 55b54256f..0280189ba 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -44,9 +44,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) { recActions = 0; - - //addCallback("timerFieldChangedBase", <#std::function callback#>) - + addCallback("setTimerPreset", [&](int index){ if(!variables["timerPresets"].isNull()) { @@ -61,24 +59,43 @@ OptionsTab::OptionsTab() : humanPlayers(0) }); auto parseTimerString = [](const std::string & str){ - std::stringstream sstrm; - int a, b; - sstrm << str; - sstrm >> a; - char c = sstrm.get(); - if(c == ':') + auto sc = str.find(":"); + if(sc == std::string::npos) + return std::stoi(str); + + auto l = str.substr(0, sc); + auto r = str.substr(sc + 1, std::string::npos); + if(r.length() == 3) //symbol added { - sstrm >> b; - return a * 60 + b; + l.push_back(r.front()); + r.erase(r.begin()); } - return -1; + else if(r.length() == 1) //symbol removed + { + r.insert(r.begin(), l.back()); + l.pop_back(); + } + + int sec = std::stoi(r); + if(sec >= 60) + { + if(l.empty()) //9:00 -> 0:09 + return sec / 10; + + l.push_back(r.front()); //0:090 -> 9:00 + r.erase(r.begin()); + } + else if(l.empty()) + return sec; + + return std::stoi(l) * 60 + std::stoi(r); }; addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo = CSH->si->turnTimerInfo; + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.baseTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -87,7 +104,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo = CSH->si->turnTimerInfo; + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.turnTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -96,7 +113,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo = CSH->si->turnTimerInfo; + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.battleTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -105,7 +122,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) int time = parseTimerString(str) * 1000; if(time >= 0) { - TurnTimerInfo tinfo = CSH->si->turnTimerInfo; + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.creatureTimer = time; CSH->setTurnTimerInfo(tinfo); } @@ -133,15 +150,24 @@ OptionsTab::OptionsTab() : humanPlayers(0) if(auto * tObj = reinterpret_cast(item)) { for(auto wname : (*tObj)["hideWidgets"].Vector()) + { if(auto w = widget(wname.String())) - { w->setEnabled(false); - } + } for(auto wname : (*tObj)["showWidgets"].Vector()) + { if(auto w = widget(wname.String())) - { w->setEnabled(true); - } + } + if((*tObj)["default"].isVector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; + tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; + tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; + tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } } redraw(); } @@ -173,15 +199,18 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } - + + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; + + //classic timer if(auto turnSlider = widget("sliderTurnDuration")) { - if(turnSlider->isActive() && !variables["timerPresets"].isNull()) + if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer) { for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) { auto & tpreset = variables["timerPresets"].Vector()[idx]; - if(tpreset.Vector().at(1).Integer() == SEL->getStartInfo()->turnTimerInfo.turnTimer / 1000) + if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) { turnSlider->scrollTo(idx); if(auto w = widget("labelTurnDurationValue")) @@ -190,6 +219,33 @@ void OptionsTab::recreate() } } } + + //chess timer + auto timeToString = [](int time) -> std::string + { + std::stringstream ss; + ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; + return ss.str(); + }; + + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(turnTimerRemote.baseTimer), false); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(turnTimerRemote.turnTimer), false); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(turnTimerRemote.battleTimer), false); + if(auto ww = widget("chessFieldCreature")) + ww->setText(timeToString(turnTimerRemote.creatureTimer), false); + + if(auto w = widget("timerModeSwitch")) + { + if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer) + { + if(auto turnSlider = widget("sliderTurnDuration")) + if(turnSlider->isActive()) + w->setItem(1); + } + } } size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) From 0bc2302f1fa875d1d070a9e48608156286e0c7f5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 05:39:29 +0400 Subject: [PATCH 0232/1248] Fix battle timer logic --- server/CGameHandler.cpp | 4 +- server/TurnTimerHandler.cpp | 98 ++++++++++++++++++------------------- server/TurnTimerHandler.h | 9 ++-- 3 files changed, 55 insertions(+), 56 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index adebe608a..789e64712 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -604,7 +604,7 @@ void CGameHandler::setPortalDwelling(const CGTownInstance * town, bool forced=fa void CGameHandler::onPlayerTurnStarted(PlayerColor which) { events::PlayerGotTurn::defaultExecute(serverEventBus.get(), which); - turnTimerHandler.onPlayerGetTurn(gs->players[which]); + turnTimerHandler.onPlayerGetTurn(which); } void CGameHandler::onPlayerTurnEnded(PlayerColor which) @@ -991,7 +991,7 @@ void CGameHandler::run(bool resume) onNewTurn(); events::TurnStarted::defaultExecute(serverEventBus.get()); for(auto & player : gs->players) - turnTimerHandler.onGameplayStart(player.second); + turnTimerHandler.onGameplayStart(player.first); } else events::GameResumed::defaultExecute(serverEventBus.get()); diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index a3b0c81b5..fe042268b 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -26,63 +26,65 @@ TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): } -void TurnTimerHandler::onGameplayStart(PlayerState & state) +void TurnTimerHandler::onGameplayStart(PlayerColor player) { if(const auto * si = gameHandler.getStartInfo()) { if(si->turnTimerInfo.isEnabled()) { - state.turnTimer = si->turnTimerInfo; - state.turnTimer.turnTimer = 0; + timers[player] = si->turnTimerInfo; + timers[player].turnTimer = 0; } } } -void TurnTimerHandler::onPlayerGetTurn(PlayerState & state) +void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(const auto * si = gameHandler.getStartInfo()) { if(si->turnTimerInfo.isEnabled()) { - state.turnTimer.baseTimer += state.turnTimer.turnTimer; - state.turnTimer.turnTimer = si->turnTimerInfo.turnTimer; + timers[player].baseTimer += timers[player].turnTimer; + timers[player].turnTimer = si->turnTimerInfo.turnTimer; TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = state.turnTimer; + ttu.player = player; + ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); } } } -void TurnTimerHandler::onPlayerMakingTurn(PlayerState & state, int waitTime) +void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs) return; + auto & state = gs->players.at(player); + if(state.human && si->turnTimerInfo.isEnabled() && !gs->curB) { - if(state.turnTimer.turnTimer > 0) + if(timers[player].turnTimer > 0) { - state.turnTimer.turnTimer -= waitTime; - int frequency = (state.turnTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); + timers[player].turnTimer -= waitTime; + int frequency = (timers[player].turnTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && state.turnTimer.turnTimer % frequency == 0) + && timers[player].turnTimer % frequency == 0) { TurnTimeUpdate ttu; ttu.player = state.color; - ttu.turnTimer = state.turnTimer; + ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); } } - else if(state.turnTimer.baseTimer > 0) + else if(timers[player].baseTimer > 0) { - state.turnTimer.turnTimer = state.turnTimer.baseTimer; - state.turnTimer.baseTimer = 0; - onPlayerMakingTurn(state, waitTime); + timers[player].turnTimer = timers[player].baseTimer; + timers[player].baseTimer = 0; + onPlayerMakingTurn(player, 0); } else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries gameHandler.turnOrder->onPlayerEndsTurn(state.color); @@ -103,12 +105,12 @@ void TurnTimerHandler::onBattleStart() { if(i.isValidPlayer()) { - const auto & state = gs->players.at(i); + timers[i].battleTimer = si->turnTimerInfo.battleTimer; + timers[i].creatureTimer = si->turnTimerInfo.creatureTimer; + TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = state.turnTimer; - ttu.turnTimer.battleTimer = si->turnTimerInfo.battleTimer; - ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; + ttu.player = i; + ttu.turnTimer = timers[i]; gameHandler.sendAndApply(&ttu); } } @@ -118,24 +120,22 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB) + if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; - if(!stack.getOwner().isValidPlayer()) + auto player = stack.getOwner(); + + if(!player.isValidPlayer()) return; - - const auto & state = gs->players.at(stack.getOwner()); - - if(si->turnTimerInfo.isBattleEnabled()) - { - TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = state.turnTimer; - if(state.turnTimer.battleTimer < si->turnTimerInfo.battleTimer) - ttu.turnTimer.battleTimer = ttu.turnTimer.creatureTimer; - ttu.turnTimer.creatureTimer = si->turnTimerInfo.creatureTimer; - gameHandler.sendAndApply(&ttu); - } + + if(timers[player].battleTimer < si->turnTimerInfo.battleTimer) + timers[player].battleTimer = timers[player].creatureTimer; + timers[player].creatureTimer = si->turnTimerInfo.creatureTimer; + + TurnTimeUpdate ttu; + ttu.player = player; + ttu.turnTimer = timers[player]; + gameHandler.sendAndApply(&ttu); } void TurnTimerHandler::onBattleLoop(int waitTime) @@ -151,20 +151,19 @@ void TurnTimerHandler::onBattleLoop(int waitTime) auto & state = gs->players.at(gs->curB->getSidePlayer(stack->unitSide())); - auto turnTimerUpdateApplier = [&](const TurnTimerInfo & tTimer) + auto turnTimerUpdateApplier = [&](TurnTimerInfo & tTimer, int waitTime) { - TurnTimerInfo turnTimerUpdate = tTimer; if(tTimer.creatureTimer > 0) { - turnTimerUpdate.creatureTimer -= waitTime; - int frequency = (turnTimerUpdate.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); + tTimer.creatureTimer -= waitTime; + int frequency = (tTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && turnTimerUpdate.creatureTimer % frequency == 0) + && tTimer.creatureTimer % frequency == 0) { TurnTimeUpdate ttu; ttu.player = state.color; - ttu.turnTimer = turnTimerUpdate; + ttu.turnTimer = tTimer; gameHandler.sendAndApply(&ttu); } return true; @@ -174,14 +173,13 @@ void TurnTimerHandler::onBattleLoop(int waitTime) if(state.human && si->turnTimerInfo.isBattleEnabled()) { - TurnTimerInfo turnTimer = state.turnTimer; - if(!turnTimerUpdateApplier(turnTimer)) + if(!turnTimerUpdateApplier(timers[state.color], waitTime)) { - if(turnTimer.battleTimer > 0) + if(timers[state.color].battleTimer > 0) { - turnTimer.creatureTimer = turnTimer.battleTimer; - turnTimer.battleTimer = 0; - turnTimerUpdateApplier(turnTimer); + timers[state.color].creatureTimer = timers[state.color].battleTimer; + timers[state.color].battleTimer = 0; + turnTimerUpdateApplier(timers[state.color], 0); } else { diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 602ecfde0..d805905a4 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CStack; class PlayerColor; -struct PlayerState; +struct TurnTimerInfo; VCMI_LIB_NAMESPACE_END @@ -26,13 +26,14 @@ class TurnTimerHandler const int turnTimePropagateFrequency = 5000; const int turnTimePropagateFrequencyCrit = 1000; const int turnTimePropagateThreshold = 3000; + std::map timers; public: TurnTimerHandler(CGameHandler &); - void onGameplayStart(PlayerState & state); - void onPlayerGetTurn(PlayerState & state); - void onPlayerMakingTurn(PlayerState & state, int waitTime); + void onGameplayStart(PlayerColor player); + void onPlayerGetTurn(PlayerColor player); + void onPlayerMakingTurn(PlayerColor player, int waitTime); void onBattleStart(); void onBattleNextStack(const CStack & stack); void onBattleLoop(int waitTime); From 0bb352565a6c319f96ac2a0ae2242530fa97c957 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 05:45:45 +0400 Subject: [PATCH 0233/1248] Fix creature timer update --- server/TurnTimerHandler.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index fe042268b..d3aca09d9 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -156,7 +156,9 @@ void TurnTimerHandler::onBattleLoop(int waitTime) if(tTimer.creatureTimer > 0) { tTimer.creatureTimer -= waitTime; - int frequency = (tTimer.creatureTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); + int frequency = (tTimer.creatureTimer > turnTimePropagateThreshold + && si->turnTimerInfo.creatureTimer - tTimer.creatureTimer > turnTimePropagateThreshold) + ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already && tTimer.creatureTimer % frequency == 0) From e4e9bcfb9b594b76c4ffbedf6cda6188f0f42262 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 05:58:22 +0400 Subject: [PATCH 0234/1248] Show time for other player during battle --- client/adventureMap/TurnTimerWidget.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 76bd90129..283a3c83c 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -13,6 +13,8 @@ #include "../CGameInfo.h" #include "../CMusicHandler.h" #include "../CPlayerInterface.h" +#include "../battle/BattleInterface.h" +#include "../battle/BattleStacksController.h" #include "../render/EFont.h" #include "../render/Graphics.h" @@ -21,6 +23,7 @@ #include "../widgets/Images.h" #include "../widgets/TextControls.h" #include "../../CCallback.h" +#include "../../lib/CStack.h" #include "../../lib/CPlayerState.h" #include "../../lib/filesystem/ResourceID.h" @@ -70,7 +73,7 @@ void TurnTimerWidget::show(Canvas & to) void TurnTimerWidget::setTime(PlayerColor player, int time) { int newTime = time / 1000; - if((LOCPLINT->cb->isPlayerMakingTurn(LOCPLINT->playerID)) + if(player == LOCPLINT->playerID) && (newTime != turnTime) && notifications.count(newTime)) { @@ -99,9 +102,15 @@ void TurnTimerWidget::tick(uint32_t msPassed) if(!LOCPLINT || !LOCPLINT->cb) return; - for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + for (PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) { - if (!LOCPLINT->cb->isPlayerMakingTurn(player)) + auto player = p; + if(LOCPLINT->battleInt) + { + if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) + player = stack->getOwner(); + } + else if (!LOCPLINT->cb->isPlayerMakingTurn(player)) continue; auto time = LOCPLINT->cb->getPlayerTurnTime(player); From 614a285fb8a1eb680cbd8b992b683570f88eede1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 06:15:28 +0400 Subject: [PATCH 0235/1248] Fix hot seat timer visualization --- client/adventureMap/TurnTimerWidget.cpp | 14 ++++++++++---- client/adventureMap/TurnTimerWidget.h | 7 +++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 283a3c83c..68428ad03 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -41,7 +41,7 @@ void TurnTimerWidget::DrawRect::showAll(Canvas & to) TurnTimerWidget::TurnTimerWidget(): InterfaceObjectConfigurable(TIME), - turnTime(0), lastTurnTime(0), cachedTurnTime(0) + turnTime(0), lastTurnTime(0), cachedTurnTime(0), lastPlayer(PlayerColor::CANNOT_DETERMINE) { REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect); @@ -73,8 +73,8 @@ void TurnTimerWidget::show(Canvas & to) void TurnTimerWidget::setTime(PlayerColor player, int time) { int newTime = time / 1000; - if(player == LOCPLINT->playerID) - && (newTime != turnTime) + if(player == LOCPLINT->playerID + && newTime != turnTime && notifications.count(newTime)) { CCS->soundh->playSound(variables["notificationSound"].String()); @@ -117,6 +117,12 @@ void TurnTimerWidget::tick(uint32_t msPassed) cachedTurnTime -= msPassed; if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero + if(lastPlayer != player) + { + lastPlayer = player; + lastTurnTime = 0; + } + auto timeCheckAndUpdate = [&](int time) { if(time / 1000 != lastTurnTime / 1000) @@ -130,7 +136,7 @@ void TurnTimerWidget::tick(uint32_t msPassed) }; auto * playerInfo = LOCPLINT->cb->getPlayer(player); - if(playerInfo && playerInfo->isHuman()) + if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) { if(LOCPLINT->battleInt) { diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index e5d5614fc..ccc801eb4 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -18,6 +18,12 @@ class CAnimImage; class CLabel; +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; + +VCMI_LIB_NAMESPACE_END + class TurnTimerWidget : public InterfaceObjectConfigurable { private: @@ -35,6 +41,7 @@ private: int turnTime; int lastTurnTime; int cachedTurnTime; + PlayerColor lastPlayer; std::set notifications; From de8bd483980821cd2ef2cc04bdd5ea22eff5501f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 26 Aug 2023 14:01:19 +0400 Subject: [PATCH 0236/1248] Added right click pop-up for text inputs --- client/gui/InterfaceObjectConfigurable.cpp | 2 ++ client/lobby/OptionsTab.cpp | 4 +++- client/widgets/TextControls.cpp | 12 ++++++++++-- client/widgets/TextControls.h | 3 +++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 60bdacbda..f70ca35a5 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -565,6 +565,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const Js result->setText(readText(config["text"])); if(!config["callback"].isNull()) result->cb += callbacks_string.at(config["callback"].String()); + if(!config["help"].isNull()) + result->helpBox = readText(config["help"]); return result; } diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 0280189ba..4f726904c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -61,7 +61,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) auto parseTimerString = [](const std::string & str){ auto sc = str.find(":"); if(sc == std::string::npos) - return std::stoi(str); + return str.empty() ? 0 : std::stoi(str); auto l = str.substr(0, sc); auto r = str.substr(sc + 1, std::string::npos); @@ -75,6 +75,8 @@ OptionsTab::OptionsTab() : humanPlayers(0) r.insert(r.begin(), l.back()); l.pop_back(); } + else if(r.empty()) + r = "0"; int sec = std::stoi(r); if(sec >= 60) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 5401f0f64..591a8d4dd 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -17,6 +17,7 @@ #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../windows/CMessage.h" +#include "../windows/InfoWindows.h" #include "../adventureMap/CInGameConsole.h" #include "../renderSDL/SDL_Extensions.h" #include "../render/Canvas.h" @@ -495,7 +496,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList(bgName, bgOffset.x, bgOffset.y); - addUsedEvents(LCLICK | KEYBOARD | TEXTINPUT); + addUsedEvents(LCLICK | SHOW_POPUP | KEYBOARD | TEXTINPUT); #if !defined(VCMI_MOBILE) giveFocus(); @@ -604,6 +605,13 @@ void CTextInput::keyPressed(EShortcut key) } } +void CTextInput::showPopupWindow(const Point & cursorPosition) +{ + if(helpBox.size()) //there is no point to show window with nothing inside... + CRClickPopup::createAndPush(helpBox); +} + + void CTextInput::setText(const std::string & nText) { setText(nText, false); diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index bc67e8abf..14b8f6b13 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -213,6 +213,8 @@ protected: std::string visibleText() override; public: + std::string helpBox; //for right-click help + CFunctionList cb; CFunctionList filters; void setText(const std::string & nText) override; @@ -224,6 +226,7 @@ public: void clickPressed(const Point & cursorPosition) override; void keyPressed(EShortcut key) override; + void showPopupWindow(const Point & cursorPosition) override; //bool captureThisKey(EShortcut key) override; From 86ab97c64f187da96b4bbd84b4edaa74da553f4b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 00:30:05 +0400 Subject: [PATCH 0237/1248] Fix stop on flight for timer --- server/CGameHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 789e64712..578bb4901 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1183,7 +1183,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { moveQuery = std::dynamic_pointer_cast(topQuery); if(moveQuery - && (!transit || result == TryMoveHero::FAILED || moveQuery->tmh.stopMovement())) + && (!transit || result != TryMoveHero::SUCCESS)) queries->popIfTop(moveQuery); else break; From 899cf5ee99dafec6d671f5deb255435d6cd7b3db Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 00:57:40 +0400 Subject: [PATCH 0238/1248] Update mods repo link for android --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java index 4f3cc0582..2bbd73ae3 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/ActivityMods.java @@ -50,7 +50,7 @@ import eu.vcmi.vcmi.util.ServerResponse; public class ActivityMods extends ActivityWithToolbar { private static final boolean ENABLE_REPO_DOWNLOADING = true; - private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.3.json"; + private static final String REPO_URL = "https://raw.githubusercontent.com/vcmi/vcmi-mods-repository/develop/vcmi-1.4.json"; private VCMIModsRepo mRepo; private RecyclerView mRecycler; From 44791b6665abaa742720eb84062501983015e5de Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 21:14:06 +0400 Subject: [PATCH 0239/1248] Revert "Auxiliary commit to revert individual files from c7390a921e3871102292f589978e9e9010eface9" This reverts commit e749954761facf4a760c6b4ef361f2ebce7b1f01. --- client/battle/BattleWindow.cpp | 2 +- config/widgets/{battleWindow.json => battleWindow2.json} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename config/widgets/{battleWindow.json => battleWindow2.json} (100%) diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index c4469540a..538b60b6c 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - const JsonNode config(ResourceID("config/widgets/BattleWindow.json")); + const JsonNode config(ResourceID("config/widgets/BattleWindow2.json")); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); diff --git a/config/widgets/battleWindow.json b/config/widgets/battleWindow2.json similarity index 100% rename from config/widgets/battleWindow.json rename to config/widgets/battleWindow2.json From 49a76ac14c02f89d63b71e18a8fcff6d47cf38a5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 21:39:45 +0400 Subject: [PATCH 0240/1248] Code review tweaks --- client/lobby/OptionsTab.cpp | 9 ++++++++- client/widgets/TextControls.cpp | 7 ++++++- client/widgets/TextControls.h | 6 +++--- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 4f726904c..59f77054f 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -58,7 +58,14 @@ OptionsTab::OptionsTab() : humanPlayers(0) } }); - auto parseTimerString = [](const std::string & str){ + //helper function to parse string containing time to integer reflecting time in seconds + //assumed that input string can be modified by user, function shall support user's intention + // normal: 2:00, 12:30 + // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, + // adding symbol (>60 seconds): 12:095 -> 129:05 + // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 + auto parseTimerString = [](const std::string & str) -> int + { auto sc = str.find(":"); if(sc == std::string::npos) return str.empty() ? 0 : std::stoi(str); diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 591a8d4dd..be496a84e 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -607,7 +607,7 @@ void CTextInput::keyPressed(EShortcut key) void CTextInput::showPopupWindow(const Point & cursorPosition) { - if(helpBox.size()) //there is no point to show window with nothing inside... + if(!helpBox.empty()) //there is no point to show window with nothing inside... CRClickPopup::createAndPush(helpBox); } @@ -624,6 +624,11 @@ void CTextInput::setText(const std::string & nText, bool callCb) cb(text); } +void CTextInput::setHelpText(const std::string & text) +{ + helpBox = text; +} + void CTextInput::textInputed(const std::string & enteredText) { if(!focus) diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 14b8f6b13..916df747a 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -209,16 +209,16 @@ public: class CTextInput : public CLabel, public CFocusable { std::string newText; + std::string helpBox; //for right-click help + protected: std::string visibleText() override; - -public: - std::string helpBox; //for right-click help CFunctionList cb; CFunctionList filters; void setText(const std::string & nText) override; void setText(const std::string & nText, bool callCb); + void setHelpText(const std::string &); CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB); CTextInput(const Rect & Pos, const Point & bgOffset, const std::string & bgName, const CFunctionList & CB); From 439254f92195d411d776d44da707c1f795b577f4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 22:06:38 +0400 Subject: [PATCH 0241/1248] Fix mistake --- client/widgets/TextControls.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 916df747a..ec7fe6bfa 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -213,6 +213,8 @@ class CTextInput : public CLabel, public CFocusable protected: std::string visibleText() override; + +public: CFunctionList cb; CFunctionList filters; From 78397d6d1a1843fa08fe9523738521df43055281 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 27 Aug 2023 23:28:10 +0400 Subject: [PATCH 0242/1248] Fix compiling --- client/gui/InterfaceObjectConfigurable.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index f70ca35a5..bab69dd1d 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -566,7 +566,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const Js if(!config["callback"].isNull()) result->cb += callbacks_string.at(config["callback"].String()); if(!config["help"].isNull()) - result->helpBox = readText(config["help"]); + result->setHelpText(readText(config["help"])); return result; } From 084122bc80f8c252c92ec934fe7bd43518042f4c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 17:26:16 +0400 Subject: [PATCH 0243/1248] Fix compiling --- server/CGameHandler.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 578bb4901..4e8d1b62e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1003,9 +1003,9 @@ void CGameHandler::run(bool resume) { const int waitTime = 100; //ms - for(auto & player : gs->players) - if (gs->isPlayerMakingTurn(player.first)) - turnTimerHandler.onPlayerMakingTurn(player.second, waitTime); + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + if(gs->isPlayerMakingTurn(player)) + turnTimerHandler.onPlayerMakingTurn(player, waitTime); if(gs->curB) turnTimerHandler.onBattleLoop(waitTime); From 0a6ea63b09098d7d343fffe7d28dd3cfdfe9960f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 17:41:00 +0400 Subject: [PATCH 0244/1248] Revert "Add compatibility with minor version" This reverts commit 6e7b13fcbeca9eca8457edb6bbc37dffe606f3a7. --- lib/modding/CModVersion.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 7d7ff03de..6e43fb2b6 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -31,7 +31,7 @@ struct DLL_LINKAGE CModVersion static CModVersion fromString(std::string from); std::string toString() const; - bool compatible(const CModVersion & other, bool checkMinor = true, bool checkPatch = false) const; + bool compatible(const CModVersion & other, bool checkMinor = false, bool checkPatch = false) const; bool isNull() const; template void serialize(Handler &h, const int version) From 925bde68ff894ea2b2b71b6ce1a20c6f8d05957e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 17:43:33 +0400 Subject: [PATCH 0245/1248] Fix compatibility check with game version --- lib/modding/CModInfo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 7b6d66f25..9d1905e54 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -108,8 +108,8 @@ void CModInfo::loadLocalData(const JsonNode & data) } //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion())); + implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true)); + implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true)); if(!implicitlyEnabled) logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); From 93f14c984d028fbd3e2f84849ab678d5e0570322 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 17:49:13 +0400 Subject: [PATCH 0246/1248] Fix compatibility check with patch version --- lib/modding/CModInfo.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 9d1905e54..4baab342a 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -108,8 +108,8 @@ void CModInfo::loadLocalData(const JsonNode & data) } //check compatibility - implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true)); - implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true)); + implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true)); + implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); if(!implicitlyEnabled) logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); From 98f7ed9dfd570ba97b6c1384e52ed181c3e782d8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 04:42:05 +0400 Subject: [PATCH 0247/1248] Implement player start turn query --- AI/EmptyAI/CEmptyAI.cpp | 3 ++- AI/EmptyAI/CEmptyAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 6 ++++-- AI/Nullkiller/AIGateway.h | 2 +- AI/VCAI/VCAI.cpp | 6 ++++-- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 9 +++++--- client/CPlayerInterface.h | 4 ++-- client/ClientCommandManager.cpp | 1 + client/NetPacksClient.cpp | 4 ++-- lib/CGameInterface.h | 2 +- lib/NetPacks.h | 3 ++- server/processors/TurnOrderProcessor.cpp | 5 +++++ server/queries/MapQueries.cpp | 26 ++++++++++++++++++++++++ server/queries/MapQueries.h | 15 ++++++++++++++ 15 files changed, 73 insertions(+), 17 deletions(-) diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 2074535a3..4c2829308 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -30,8 +30,9 @@ void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_p playerID = *cb->getMyColor(); } -void CEmptyAI::yourTurn() +void CEmptyAI::yourTurn(QueryID queryID) { + cb->selectionMade(0, queryID); cb->endTurn(); } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 8a48a797c..297795839 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -23,7 +23,7 @@ public: virtual void loadGame(BinaryDeserializer & h, const int version) override; void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn() override; + void yourTurn(QueryID queryID) override; void yourTacticPhase(int distance) override; void activeStack(const CStack * stack) override; void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index a32bee870..9ae840d4b 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -544,10 +544,12 @@ void AIGateway::initGameInterface(std::shared_ptr env, std::shared_ retrieveVisitableObjs(); } -void AIGateway::yourTurn() +void AIGateway::yourTurn(QueryID queryID) { - LOG_TRACE(logAi); + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); NET_EVENT_HANDLER; + status.addQuery(queryID, "YourTurn"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); status.startedTurn(); makingTurn = std::make_unique(&AIGateway::makeTurn, this); } diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index ec7f61aac..c4064f164 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -111,7 +111,7 @@ public: std::string getBattleAIName() const override; void initGameInterface(std::shared_ptr env, std::shared_ptr CB) override; - void yourTurn() override; + void yourTurn(QueryID queryID) override; void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index a66152e58..231c77cd8 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -610,10 +610,12 @@ void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr(&VCAI::makeTurn, this); } diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 34c53f258..ea341b4af 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -144,7 +144,7 @@ public: std::string getBattleAIName() const override; void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn() override; + void yourTurn(QueryID queryID) override; void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 721857fa2..2fb9882ea 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -243,7 +243,7 @@ void CPlayerInterface::performAutosave() } } -void CPlayerInterface::yourTurn() +void CPlayerInterface::yourTurn(QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; { @@ -273,10 +273,10 @@ void CPlayerInterface::yourTurn() adventureInt->onPlayerTurnStarted(playerID); } } - acceptTurn(); + acceptTurn(queryID); } -void CPlayerInterface::acceptTurn() +void CPlayerInterface::acceptTurn(QueryID queryID) { if (settings["session"]["autoSkip"].Bool()) { @@ -322,6 +322,9 @@ void CPlayerInterface::acceptTurn() else logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); } + + JsonNode reply(JsonNode::JsonType::DATA_NULL); + cb->sendQueryReply(reply, queryID); } void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 687fee93c..c5048637f 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -133,7 +133,7 @@ protected: // Call-ins from server, should not be called directly, but only via void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles void newObject(const CGObjectInstance * obj) override; void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) - void yourTurn() override; + void yourTurn(QueryID queryID) override; void availableCreaturesChanged(const CGDwelling *town) override; void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it void playerBonusChanged(const Bonus &bonus, bool gain) override; @@ -228,7 +228,7 @@ private: void heroKilled(const CGHeroInstance* hero); void garrisonsChanged(std::vector objs); void requestReturningToMainMenu(bool won); - void acceptTurn(); //used during hot seat after your turn message is close + void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close void initializeHeroTownList(); int getLastIndex(std::string namePrefix); void doMoveHero(const CGHeroInstance *h, CGPath path); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 19ac8dcc9..5a4c2f8cf 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -471,6 +471,7 @@ void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier) { YourTurn yt; yt.player = colorIdentifier; + yt.queryID = -1; ApplyClientNetPackVisitor visitor(*CSH->client, *CSH->client->gameState()); yt.visit(visitor); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 93da77d4b..6408c2566 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -398,7 +398,7 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface if (cl.gameState()->isPlayerMakingTurn(player)) { callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); - callOnlyThatInterface(cl, player, &CGameInterface::yourTurn); + callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, -1); } } }; @@ -873,7 +873,7 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) logNetwork->debug("Server gives turn to %s", pack.player.getStr()); callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); - callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn); + callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); } void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 8d9e1b284..41fa7bbf3 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -88,7 +88,7 @@ class DLL_LINKAGE CGameInterface : public CBattleGameInterface, public IGameEven public: virtual ~CGameInterface() = default; virtual void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB){}; - virtual void yourTurn(){}; //called AFTER playerStartsTurn(player) + virtual void yourTurn(QueryID askID){}; //called AFTER playerStartsTurn(player) //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index a958e9606..5c28c69af 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -162,7 +162,7 @@ struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient } }; -struct DLL_LINKAGE YourTurn : public CPackForClient +struct DLL_LINKAGE YourTurn : public Query { void applyGs(CGameState * gs) const; @@ -172,6 +172,7 @@ struct DLL_LINKAGE YourTurn : public CPackForClient template void serialize(Handler & h, const int version) { + h & queryID; h & player; } }; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index ee38aa5c3..164fa488f 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -11,6 +11,7 @@ #include "TurnOrderProcessor.h" #include "../queries/QueriesProcessor.h" +#include "../queries/MapQueries.h" #include "../CGameHandler.h" #include "../CVCMIServer.h" @@ -98,8 +99,12 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) awaitingPlayers.erase(which); gameHandler->onPlayerTurnStarted(which); + auto turnQuery = std::make_shared(gameHandler, which); + gameHandler->queries->addQuery(turnQuery); + YourTurn yt; yt.player = which; + yt.queryID = turnQuery->queryID; gameHandler->sendAndApply(&yt); assert(actingPlayers.size() == 1); // No simturns yet :( diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 054f3526c..a237d9118 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -15,6 +15,32 @@ #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/serializer/Cast.h" +PlayerStartsTurnQuery::PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player): + CGhQuery(owner) +{ + addPlayer(player); +} + +bool PlayerStartsTurnQuery::blocksPack(const CPack *pack) const +{ + return blockAllButReply(pack); +} + +void PlayerStartsTurnQuery::onAdding(PlayerColor color) +{ + //gh->turnTimerHandler.setTimerEnabled(color, false); +} + +void PlayerStartsTurnQuery::onRemoval(PlayerColor color) +{ + //gh->turnTimerHandler.setTimerEnabled(color, true); +} + +bool PlayerStartsTurnQuery::endsByPlayerAnswer() const +{ + return true; +} + CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) { diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index c89be802d..dc6890f75 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -13,6 +13,21 @@ #include "../../lib/NetPacks.h" +class TurnTimerHandler; + +//Created when player starts turn +//Removed when player accepts a turn +class PlayerStartsTurnQuery : public CGhQuery +{ +public: + PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player); + + bool blocksPack(const CPack *pack) const override; + void onAdding(PlayerColor color) override; + void onRemoval(PlayerColor color) override; + bool endsByPlayerAnswer() const override; +}; + //Created when hero visits object. //Removed when query above is resolved (or immediately after visit if no queries were created) class CObjectVisitQuery : public CGhQuery From 71cbb0e2e7a4cec8f0e8414f288838e299610fc2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 13:21:49 +0400 Subject: [PATCH 0248/1248] Change query reply method --- client/CPlayerInterface.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2fb9882ea..ce4f4a419 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -323,8 +323,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID) logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); } - JsonNode reply(JsonNode::JsonType::DATA_NULL); - cb->sendQueryReply(reply, queryID); + cb->selectionMade(0, queryID); } void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) From 6226ddf4a72c66c915017edabd4ef85164fadcad Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 01:10:04 +0400 Subject: [PATCH 0249/1248] Fix potential concurrenccy issues --- server/TurnTimerHandler.cpp | 9 +++++++++ server/TurnTimerHandler.h | 1 + 2 files changed, 10 insertions(+) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index d3aca09d9..ea621bef2 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -32,6 +32,7 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) { if(si->turnTimerInfo.isEnabled()) { + std::lock_guard guard(mx); timers[player] = si->turnTimerInfo; timers[player].turnTimer = 0; } @@ -44,6 +45,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(si->turnTimerInfo.isEnabled()) { + std::lock_guard guard(mx); timers[player].baseTimer += timers[player].turnTimer; timers[player].turnTimer = si->turnTimerInfo.turnTimer; @@ -66,6 +68,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) if(state.human && si->turnTimerInfo.isEnabled() && !gs->curB) { + std::lock_guard guard(mx); if(timers[player].turnTimer > 0) { timers[player].turnTimer -= waitTime; @@ -98,6 +101,8 @@ void TurnTimerHandler::onBattleStart() if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; + std::lock_guard guard(mx); + auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); @@ -123,6 +128,8 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; + std::lock_guard guard(mx); + auto player = stack.getOwner(); if(!player.isValidPlayer()) @@ -145,6 +152,8 @@ void TurnTimerHandler::onBattleLoop(int waitTime) if(!si || !gs || !gs->curB) return; + std::lock_guard guard(mx); + const auto * stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); if(!stack || !stack->getOwner().isValidPlayer()) return; diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index d805905a4..649cf206c 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -27,6 +27,7 @@ class TurnTimerHandler const int turnTimePropagateFrequencyCrit = 1000; const int turnTimePropagateThreshold = 3000; std::map timers; + std::recursive_mutex mx; public: TurnTimerHandler(CGameHandler &); From 4ee47b01aeca8efa0568dddef29f0e850d16c598 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 01:59:47 +0400 Subject: [PATCH 0250/1248] Make possible to play with different timers independently --- client/adventureMap/AdventureMapInterface.cpp | 2 +- lib/TurnTimerInfo.cpp | 4 ++-- server/TurnTimerHandler.cpp | 9 +++------ 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 175bf25e4..50f20c376 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -63,7 +63,7 @@ AdventureMapInterface::AdventureMapInterface(): shortcuts->setState(EAdventureState::MAKING_TURN); widget->getMapView()->onViewMapActivated(); - if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled()) + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled()) watches = std::make_shared(); addUsedEvents(KEYBOARD | TIME); diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 6d10f67ee..0594fad65 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -14,12 +14,12 @@ VCMI_LIB_NAMESPACE_BEGIN bool TurnTimerInfo::isEnabled() const { - return turnTimer > 0; + return turnTimer > 0 || baseTimer > 0; } bool TurnTimerInfo::isBattleEnabled() const { - return creatureTimer > 0; + return creatureTimer > 0 || battleTimer > 0; } VCMI_LIB_NAMESPACE_END diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index ea621bef2..de931ec51 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -30,12 +30,9 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) { if(const auto * si = gameHandler.getStartInfo()) { - if(si->turnTimerInfo.isEnabled()) - { - std::lock_guard guard(mx); - timers[player] = si->turnTimerInfo; - timers[player].turnTimer = 0; - } + std::lock_guard guard(mx); + timers[player] = si->turnTimerInfo; + timers[player].turnTimer = 0; } } From 7dc1717ec699a092c7370b85f656871a6aa1b233 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 02:00:02 +0400 Subject: [PATCH 0251/1248] Handle timer for tactic phase --- server/TurnTimerHandler.cpp | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index de931ec51..b5d86827b 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -151,11 +151,21 @@ void TurnTimerHandler::onBattleLoop(int waitTime) std::lock_guard guard(mx); - const auto * stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); - if(!stack || !stack->getOwner().isValidPlayer()) - return; + ui8 side = 0; + const CStack * stack = nullptr; + bool isTactisPhase = gs->curB.get()->battleTacticDist() > 0; - auto & state = gs->players.at(gs->curB->getSidePlayer(stack->unitSide())); + if(isTactisPhase) + side = gs->curB.get()->battleGetTacticsSide(); + else + { + stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); + if(!stack || !stack->getOwner().isValidPlayer()) + return; + side = stack->unitSide(); + } + + auto & state = gs->players.at(gs->curB->getSidePlayer(side)); auto turnTimerUpdateApplier = [&](TurnTimerInfo & tTimer, int waitTime) { @@ -192,9 +202,14 @@ void TurnTimerHandler::onBattleLoop(int waitTime) else { BattleAction doNothing; - doNothing.actionType = EActionType::DEFEND; - doNothing.side = stack->unitSide(); - doNothing.stackNumber = stack->unitId(); + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; + else + { + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); + } gameHandler.battles->makePlayerBattleAction(state.color, doNothing); } } From 2c61d1b23f628d89e2c6a44a4c878e21f11749c2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 03:14:58 +0400 Subject: [PATCH 0252/1248] Use precision clocks for timer --- server/CGameHandler.cpp | 11 ++++++++--- server/TurnTimerHandler.cpp | 6 +++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 4e8d1b62e..b7371bd4f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -999,18 +999,23 @@ void CGameHandler::run(bool resume) turnOrder->onGameStarted(); //wait till game is done + auto clockLast = std::chrono::high_resolution_clock::now(); while(lobby->getState() == EServerState::GAMEPLAY) { + const auto clockDuration = std::chrono::high_resolution_clock::now() - clockLast; + const int timePassed = std::chrono::duration_cast(clockDuration).count(); + clockLast += clockDuration; + const int waitTime = 100; //ms for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) if(gs->isPlayerMakingTurn(player)) - turnTimerHandler.onPlayerMakingTurn(player, waitTime); + turnTimerHandler.onPlayerMakingTurn(player, timePassed); if(gs->curB) - turnTimerHandler.onBattleLoop(waitTime); + turnTimerHandler.onBattleLoop(timePassed); - boost::this_thread::sleep_for(boost::chrono::milliseconds(waitTime)); + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); } } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index b5d86827b..6db8dbb17 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -72,7 +72,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) int frequency = (timers[player].turnTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && timers[player].turnTimer % frequency == 0) + && timers[player].turnTimer / 100 * 100 % frequency == 0) { TurnTimeUpdate ttu; ttu.player = state.color; @@ -132,7 +132,7 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) if(!player.isValidPlayer()) return; - if(timers[player].battleTimer < si->turnTimerInfo.battleTimer) + if(timers[player].battleTimer == 0) timers[player].battleTimer = timers[player].creatureTimer; timers[player].creatureTimer = si->turnTimerInfo.creatureTimer; @@ -177,7 +177,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && tTimer.creatureTimer % frequency == 0) + && (tTimer.creatureTimer / 100 * 100 % frequency) == 0) { TurnTimeUpdate ttu; ttu.player = state.color; From 15f57a08177e435d01f1855b6a0020e288ab61c7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 04:40:35 +0400 Subject: [PATCH 0253/1248] Timer enabler --- server/TurnTimerHandler.cpp | 51 +++++++++++++++++++++---------------- server/TurnTimerHandler.h | 2 ++ 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 6db8dbb17..f1f51eac6 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -33,9 +33,17 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) std::lock_guard guard(mx); timers[player] = si->turnTimerInfo; timers[player].turnTimer = 0; + timerEnabled[player] = true; } } +void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) +{ + std::lock_guard guard(mx); + assert(player.isValidPlayer()); + timerEnabled[player] = enabled; +} + void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(const auto * si = gameHandler.getStartInfo()) @@ -58,14 +66,15 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs) + if(!si || !gs || gs->curB || !si->turnTimerInfo.isEnabled()) return; + std::lock_guard guard(mx); + auto & state = gs->players.at(player); - if(state.human && si->turnTimerInfo.isEnabled() && !gs->curB) + if(state.human && timerEnabled[player]) { - std::lock_guard guard(mx); if(timers[player].turnTimer > 0) { timers[player].turnTimer -= waitTime; @@ -146,7 +155,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) { const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB) + if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; std::lock_guard guard(mx); @@ -189,29 +198,27 @@ void TurnTimerHandler::onBattleLoop(int waitTime) return false; }; - if(state.human && si->turnTimerInfo.isBattleEnabled()) + if(state.human && timerEnabled[state.color] + && !turnTimerUpdateApplier(timers[state.color], waitTime)) { - if(!turnTimerUpdateApplier(timers[state.color], waitTime)) + if(timers[state.color].battleTimer > 0) { - if(timers[state.color].battleTimer > 0) - { - timers[state.color].creatureTimer = timers[state.color].battleTimer; - timers[state.color].battleTimer = 0; - turnTimerUpdateApplier(timers[state.color], 0); - } + timers[state.color].creatureTimer = timers[state.color].battleTimer; + timers[state.color].battleTimer = 0; + turnTimerUpdateApplier(timers[state.color], 0); + } + else + { + BattleAction doNothing; + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; else { - BattleAction doNothing; - doNothing.side = side; - if(isTactisPhase) - doNothing.actionType = EActionType::END_TACTIC_PHASE; - else - { - doNothing.actionType = EActionType::DEFEND; - doNothing.stackNumber = stack->unitId(); - } - gameHandler.battles->makePlayerBattleAction(state.color, doNothing); + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); } + gameHandler.battles->makePlayerBattleAction(state.color, doNothing); } } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 649cf206c..c5b201435 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -27,6 +27,7 @@ class TurnTimerHandler const int turnTimePropagateFrequencyCrit = 1000; const int turnTimePropagateThreshold = 3000; std::map timers; + std::map timerEnabled; std::recursive_mutex mx; public: @@ -38,4 +39,5 @@ public: void onBattleStart(); void onBattleNextStack(const CStack & stack); void onBattleLoop(int waitTime); + void setTimerEnabled(PlayerColor player, bool enabled); }; From 68b11dbe3845ad37cebfcf96d044e88e880cff89 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 04:45:35 +0400 Subject: [PATCH 0254/1248] Fix no-base turn timer --- server/TurnTimerHandler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index f1f51eac6..1f592ee67 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -51,7 +51,8 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) if(si->turnTimerInfo.isEnabled()) { std::lock_guard guard(mx); - timers[player].baseTimer += timers[player].turnTimer; + if(si->turnTimerInfo.baseTimer > 0) + timers[player].baseTimer += timers[player].turnTimer; timers[player].turnTimer = si->turnTimerInfo.turnTimer; TurnTimeUpdate ttu; From 21af69e0b7ada6271bf770fa2857e62c1cc9365a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 04:52:02 +0400 Subject: [PATCH 0255/1248] Fix timer synchornization --- server/TurnTimerHandler.cpp | 12 ++++++++++-- server/TurnTimerHandler.h | 1 + 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 1f592ee67..495aca009 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -34,6 +34,7 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) timers[player] = si->turnTimerInfo; timers[player].turnTimer = 0; timerEnabled[player] = true; + timerLastUpdate[player] = std::numeric_limits::max(); } } @@ -59,6 +60,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) ttu.player = player; ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); + timerLastUpdate[player] = 0; } } } @@ -79,15 +81,17 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) if(timers[player].turnTimer > 0) { timers[player].turnTimer -= waitTime; + timerLastUpdate[player] += waitTime; int frequency = (timers[player].turnTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && timers[player].turnTimer / 100 * 100 % frequency == 0) + && timerLastUpdate[player] >= frequency) { TurnTimeUpdate ttu; ttu.player = state.color; ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); + timerLastUpdate[player] = 0; } } else if(timers[player].baseTimer > 0) @@ -124,6 +128,7 @@ void TurnTimerHandler::onBattleStart() ttu.player = i; ttu.turnTimer = timers[i]; gameHandler.sendAndApply(&ttu); + timerLastUpdate[i] = 0; } } } @@ -150,6 +155,7 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) ttu.player = player; ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); + timerLastUpdate[player] = 0; } void TurnTimerHandler::onBattleLoop(int waitTime) @@ -182,17 +188,19 @@ void TurnTimerHandler::onBattleLoop(int waitTime) if(tTimer.creatureTimer > 0) { tTimer.creatureTimer -= waitTime; + timerLastUpdate[state.color] += waitTime; int frequency = (tTimer.creatureTimer > turnTimePropagateThreshold && si->turnTimerInfo.creatureTimer - tTimer.creatureTimer > turnTimePropagateThreshold) ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && (tTimer.creatureTimer / 100 * 100 % frequency) == 0) + && timerLastUpdate[state.color] >= frequency) { TurnTimeUpdate ttu; ttu.player = state.color; ttu.turnTimer = tTimer; gameHandler.sendAndApply(&ttu); + timerLastUpdate[state.color] = 0; } return true; } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index c5b201435..367898325 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -28,6 +28,7 @@ class TurnTimerHandler const int turnTimePropagateThreshold = 3000; std::map timers; std::map timerEnabled; + std::map timerLastUpdate; std::recursive_mutex mx; public: From b5417c667c42fee1099f3cd5053b267ce2f3edb8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 05:06:43 +0400 Subject: [PATCH 0256/1248] Hold timer client counter --- client/CPlayerInterface.cpp | 10 +++++++++- client/CPlayerInterface.h | 3 +++ client/adventureMap/TurnTimerWidget.cpp | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ce4f4a419..fa035e9e2 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -140,7 +140,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; - + timerEnabled = true; duringMovement = false; ignoreEvents = false; numOfMovedArts = 0; @@ -272,6 +272,8 @@ void CPlayerInterface::yourTurn(QueryID queryID) makingTurn = true; adventureInt->onPlayerTurnStarted(playerID); } + + timerEnabled = false; } acceptTurn(queryID); } @@ -324,6 +326,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID) } cb->selectionMade(0, queryID); + timerEnabled = true; } void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) @@ -2124,3 +2127,8 @@ std::optional CPlayerInterface::makeSurrenderRetreatDecision(const { return std::nullopt; } + +bool CPlayerInterface::isTimerEnabled() const +{ + return timerEnabled; +} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index c5048637f..157158a71 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -57,6 +57,7 @@ namespace boost /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public IUpdateable { + bool timerEnabled; bool duringMovement; bool ignoreEvents; size_t numOfMovedArts; @@ -206,6 +207,8 @@ public: // public interface for use by client via LOCPLINT access ///returns true if all events are processed internally bool capturedAllEvents(); + + bool isTimerEnabled() const; CPlayerInterface(PlayerColor Player); ~CPlayerInterface(); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 68428ad03..a66587a87 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -114,7 +114,8 @@ void TurnTimerWidget::tick(uint32_t msPassed) continue; auto time = LOCPLINT->cb->getPlayerTurnTime(player); - cachedTurnTime -= msPassed; + if(LOCPLINT->isTimerEnabled()) + cachedTurnTime -= msPassed; if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero if(lastPlayer != player) From 432ed18579f59128175c5e2e67446570dc266c8a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 15:09:15 +0400 Subject: [PATCH 0257/1248] Fix overlap of timer and fps widgets --- client/gui/CGuiHandler.cpp | 4 ++-- config/widgets/turnTimer.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index c6bb020fd..504f404ad 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -184,11 +184,11 @@ Point CGuiHandler::screenDimensions() const void CGuiHandler::drawFPSCounter() { - static SDL_Rect overlay = { 0, 0, 64, 32}; + static SDL_Rect overlay = { 0, 0, 24, 24}; uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); SDL_FillRect(screen, &overlay, black); std::string fps = std::to_string(framerate().getFramerate()); - graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(10, 10)); + graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2)); } bool CGuiHandler::amIGuiThread() diff --git a/config/widgets/turnTimer.json b/config/widgets/turnTimer.json index c2c8b01cf..5965ed2f0 100644 --- a/config/widgets/turnTimer.json +++ b/config/widgets/turnTimer.json @@ -3,7 +3,7 @@ [ { //backgound color "type": "drawRect", - "rect": {"x": 4, "y": 4, "w": 68, "h": 24}, + "rect": {"x": 4, "y": 4, "w": 72, "h": 24}, "color": [10, 10, 10, 255] }, @@ -21,7 +21,7 @@ "alignment": "left", "color": "yellow", "text": "", - "position": {"x": 24, "y": 2} + "position": {"x": 26, "y": 2} }, ], From 883c68b1512ff79aac7d03bf185dc019ec6def04 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 28 Aug 2023 19:57:42 +0400 Subject: [PATCH 0258/1248] Remove unused variable --- server/CGameHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b7371bd4f..b97ef40e3 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1006,8 +1006,6 @@ void CGameHandler::run(bool resume) const int timePassed = std::chrono::duration_cast(clockDuration).count(); clockLast += clockDuration; - const int waitTime = 100; //ms - for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) if(gs->isPlayerMakingTurn(player)) turnTimerHandler.onPlayerMakingTurn(player, timePassed); From 44ece25042574f220e19f10e800195eae504171f Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 29 Aug 2023 00:14:49 +0200 Subject: [PATCH 0259/1248] disable unavailable heroes --- .../vcmi/Data/lobby/townBorderBigGrayedOut.png | Bin 0 -> 604 bytes client/lobby/OptionsTab.cpp | 17 ++++++++++++++++- client/lobby/OptionsTab.h | 1 + 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png diff --git a/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png b/Mods/vcmi/Data/lobby/townBorderBigGrayedOut.png new file mode 100644 index 0000000000000000000000000000000000000000..c7b67233898d8e23eaf0c7e9b1d43e29a134947c GIT binary patch literal 604 zcmeAS@N?(olHy`uVBq!ia0vp^RzU2)!3HF~nNs>07#LeJot*WXk<8hxTC-yhQiXqppuC~|EG&Y9jPd|(_j8wuKYbi{pM34o7EXt%t}oO@>HB2 zFZwG^(1k_0BiSQqnVou9W<}rn>K{L<#2-iAd;U*`+3R3RgTI2G!QBlV<{jx~x^q9< z70#Hd+WKQo#ia$@LiR& zA@Y@0EwhXald$TkxbGF^8+LE6KfmSoZ^mmrMd#Ak7B2!u3tN)6y9>jA5L~c#`DCC7 zXMsm#F$061G6*wPEVVBK3g&paIEG~0dwbhaw84Of^TScX~>>eRvxS7dfEn)~N|=igetStartInfo()->playerInfos) + { + if(player.first != color && (int)player.second.hero > PlayerSettings::RANDOM) + unusableHeroes.push_back(player.second.hero); + } + allowedBonus.push_back(-1); // random if(initialHero.getNum() >= -1) allowedBonus.push_back(0); // artifact @@ -795,8 +801,13 @@ void OptionsTab::SelectionWindow::genContentHeroes() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); - components.push_back(std::make_shared(selectedHero == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + std::string image = "lobby/townBorderBig"; + if(selectedHero == elem) + image = "lobby/townBorderBigActivated"; + if(std::find(unusableHeroes.begin(), unusableHeroes.end(), elem) != unusableHeroes.end()) + image = "lobby/townBorderBigGrayedOut"; + components.push_back(std::make_shared(image, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); heroes.push_back(elem); i++; @@ -876,6 +887,10 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { set.hero = PlayerSettings::RANDOM; } + + if(doApply && std::find(unusableHeroes.begin(), unusableHeroes.end(), heroes[elem]) != unusableHeroes.end()) + return; + if(set.hero.getNum() != PlayerSettings::NONE) { if(!doApply) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index f9a5a6778..2d5cc43d6 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -114,6 +114,7 @@ private: std::vector factions; std::vector heroes; + std::vector unusableHeroes; FactionID initialFaction; HeroTypeID initialHero; From 196cd41e35b60bc88cabbaf46b968a0cfe0509c1 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 29 Aug 2023 02:04:32 +0200 Subject: [PATCH 0260/1248] fix multiplayer --- client/lobby/OptionsTab.cpp | 12 ++++++++++++ client/lobby/OptionsTab.h | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index eb5353db8..9c08b23fe 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -200,6 +200,11 @@ void OptionsTab::recreate() entries.clear(); humanPlayers = 0; + for (auto selectionWindow : GH.windows().findWindows()) + { + selectionWindow->reopen(); + } + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; for(auto & pInfo : SEL->getStartInfo()->playerInfos) { @@ -671,6 +676,13 @@ void OptionsTab::SelectionWindow::setSelection() CSH->setPlayerOption(LobbyChangePlayerOption::BONUS_ID, selectedBonus, color); } +void OptionsTab::SelectionWindow::reopen() +{ + std::shared_ptr window = std::shared_ptr(new SelectionWindow(color, type)); + close(); + GH.windows().pushWindow(window); +} + void OptionsTab::SelectionWindow::recreate() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 2d5cc43d6..10c4c64c7 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -115,7 +115,7 @@ private: std::vector factions; std::vector heroes; std::vector unusableHeroes; - + FactionID initialFaction; HeroTypeID initialHero; int initialBonus; @@ -145,6 +145,8 @@ private: void showPopupWindow(const Point & cursorPosition) override; public: + void reopen(); + SelectionWindow(PlayerColor _color, SelType _type); }; From c8243313b8030407037a960febf39b03e7917048 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 29 Aug 2023 14:53:15 +0400 Subject: [PATCH 0261/1248] Translations for turn timers --- Mods/vcmi/config/vcmi/english.json | 9 +++++++++ client/gui/InterfaceObjectConfigurable.cpp | 6 ++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 8ef48c372..cb7c2489f 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -191,6 +191,15 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", + "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Base timer}\n\nCounts down when {turn timer} runs out, never resets. If timer expires, player ends turn.", + "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nCounts down when player is making turn on adventure map, resets every turn. If {base timer} is enabled, unspent time will be added to it.", + "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when {stack timer} runs out, resets every battle. if timer expires, currently acting stack will defend.", + "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Creature timer}\n\nCounts down when player is choosing action in battle, resets after each stack turn.", + "vcmi.optionsTab.widgets.labelTimer" : "Timer", + "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", + "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", + + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!", diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index bab69dd1d..65da83227 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -157,6 +157,8 @@ std::string InterfaceObjectConfigurable::readText(const JsonNode & config) const return ""; std::string s = config.String(); + if(s.empty()) + return s; logGlobal->debug("Reading text from translations by key: %s", s); return CGI->generaltexth->translate(s); } @@ -561,8 +563,8 @@ std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const Js result->font = readFont(config["font"]); if(!config["color"].isNull()) result->setColor(readColor(config["color"])); - if(!config["text"].isNull()) - result->setText(readText(config["text"])); + if(!config["text"].isNull() && config["text"].isString()) + result->setText(config["text"].String()); //for input field raw string is taken if(!config["callback"].isNull()) result->cb += callbacks_string.at(config["callback"].String()); if(!config["help"].isNull()) From 03496e673866ace0c20913195256c5fecc98fce2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 29 Aug 2023 15:48:42 +0400 Subject: [PATCH 0262/1248] Code review fixes --- server/CGameHandler.cpp | 13 +++---------- server/TurnTimerHandler.cpp | 30 ++++++++++++++++++++---------- server/TurnTimerHandler.h | 6 ++++-- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b97ef40e3..cbe6505b5 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -999,20 +999,13 @@ void CGameHandler::run(bool resume) turnOrder->onGameStarted(); //wait till game is done - auto clockLast = std::chrono::high_resolution_clock::now(); + auto clockLast = std::chrono::steady_clock::now(); while(lobby->getState() == EServerState::GAMEPLAY) { - const auto clockDuration = std::chrono::high_resolution_clock::now() - clockLast; + const auto clockDuration = std::chrono::steady_clock::now() - clockLast; const int timePassed = std::chrono::duration_cast(clockDuration).count(); clockLast += clockDuration; - - for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) - if(gs->isPlayerMakingTurn(player)) - turnTimerHandler.onPlayerMakingTurn(player, timePassed); - - if(gs->curB) - turnTimerHandler.onBattleLoop(timePassed); - + turnTimerHandler.update(timePassed); boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); } } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 495aca009..6020de0be 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -28,9 +28,9 @@ TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): void TurnTimerHandler::onGameplayStart(PlayerColor player) { + std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { - std::lock_guard guard(mx); timers[player] = si->turnTimerInfo; timers[player].turnTimer = 0; timerEnabled[player] = true; @@ -47,11 +47,11 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { + std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { if(si->turnTimerInfo.isEnabled()) { - std::lock_guard guard(mx); if(si->turnTimerInfo.baseTimer > 0) timers[player].baseTimer += timers[player].turnTimer; timers[player].turnTimer = si->turnTimerInfo.turnTimer; @@ -65,15 +65,28 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) } } +void TurnTimerHandler::update(int waitTime) +{ + std::lock_guard guard(mx); + if(const auto * gs = gameHandler.gameState()) + { + for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + if(gs->isPlayerMakingTurn(player)) + onPlayerMakingTurn(player, waitTime); + + if(gs->curB) + onBattleLoop(waitTime); + } +} + void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { + std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || gs->curB || !si->turnTimerInfo.isEnabled()) return; - std::lock_guard guard(mx); - auto & state = gs->players.at(player); if(state.human && timerEnabled[player]) @@ -107,12 +120,11 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) void TurnTimerHandler::onBattleStart() { + std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; - - std::lock_guard guard(mx); auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); @@ -135,13 +147,12 @@ void TurnTimerHandler::onBattleStart() void TurnTimerHandler::onBattleNextStack(const CStack & stack) { + std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; - std::lock_guard guard(mx); - auto player = stack.getOwner(); if(!player.isValidPlayer()) @@ -160,13 +171,12 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) void TurnTimerHandler::onBattleLoop(int waitTime) { + std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; - std::lock_guard guard(mx); - ui8 side = 0; const CStack * stack = nullptr; bool isTactisPhase = gs->curB.get()->battleTacticDist() > 0; diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 367898325..572831da3 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -31,14 +31,16 @@ class TurnTimerHandler std::map timerLastUpdate; std::recursive_mutex mx; + void onPlayerMakingTurn(PlayerColor player, int waitTime); + void onBattleLoop(int waitTime); + public: TurnTimerHandler(CGameHandler &); void onGameplayStart(PlayerColor player); void onPlayerGetTurn(PlayerColor player); - void onPlayerMakingTurn(PlayerColor player, int waitTime); void onBattleStart(); void onBattleNextStack(const CStack & stack); - void onBattleLoop(int waitTime); + void update(int waitTime); void setTimerEnabled(PlayerColor player, bool enabled); }; From b6000841f10732740e53cf1346363eea17902e29 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 29 Aug 2023 21:08:18 +0200 Subject: [PATCH 0263/1248] code review --- client/lobby/OptionsTab.cpp | 6 +++--- client/lobby/OptionsTab.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 9c08b23fe..f9de0b3f3 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -620,7 +620,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) for(auto & player : SEL->getStartInfo()->playerInfos) { if(player.first != color && (int)player.second.hero > PlayerSettings::RANDOM) - unusableHeroes.push_back(player.second.hero); + unusableHeroes.insert(player.second.hero); } allowedBonus.push_back(-1); // random @@ -817,7 +817,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() std::string image = "lobby/townBorderBig"; if(selectedHero == elem) image = "lobby/townBorderBigActivated"; - if(std::find(unusableHeroes.begin(), unusableHeroes.end(), elem) != unusableHeroes.end()) + if(unusableHeroes.count(elem)) image = "lobby/townBorderBigGrayedOut"; components.push_back(std::make_shared(image, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); heroes.push_back(elem); @@ -900,7 +900,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) set.hero = PlayerSettings::RANDOM; } - if(doApply && std::find(unusableHeroes.begin(), unusableHeroes.end(), heroes[elem]) != unusableHeroes.end()) + if(doApply && unusableHeroes.count(heroes[elem])) return; if(set.hero.getNum() != PlayerSettings::NONE) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 10c4c64c7..b3d9f7e9a 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -114,8 +114,8 @@ private: std::vector factions; std::vector heroes; - std::vector unusableHeroes; - + std::set unusableHeroes; + FactionID initialFaction; HeroTypeID initialHero; int initialBonus; From 40c7ddcaf4973a368733945824cbf2bef83457fd Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:22:22 +0200 Subject: [PATCH 0264/1248] dim adventuremap --- client/adventureMap/AdventureMapInterface.cpp | 13 +++++++++++++ client/adventureMap/AdventureMapInterface.h | 3 +++ client/render/Canvas.h | 2 +- client/renderSDL/SDL_Extensions.cpp | 5 ++++- 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 175bf25e4..8d816261c 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -24,6 +24,9 @@ #include "../mapView/mapHandler.h" #include "../mapView/MapView.h" #include "../windows/InfoWindows.h" +#include "../windows/CCastleInterface.h" +#include "../windows/CHeroWindow.h" +#include "../windows/CKingdomInterface.h" #include "../CGameInfo.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" @@ -153,15 +156,25 @@ void AdventureMapInterface::deactivate() void AdventureMapInterface::showAll(Canvas & to) { CIntObject::showAll(to); + dim(to); LOCPLINT->cingconsole->show(to); } void AdventureMapInterface::show(Canvas & to) { CIntObject::show(to); + dim(to); LOCPLINT->cingconsole->show(to); } +void AdventureMapInterface::dim(Canvas & to) +{ + if(!GH.windows().findWindows().empty() || + !GH.windows().findWindows().empty() || + !GH.windows().findWindows().empty()) + to.drawColor(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y), ColorRGBA(0, 0, 0, 128)); +} + void AdventureMapInterface::tick(uint32_t msPassed) { handleMapScrollingUpdate(msPassed); diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 55546b6a3..39fa43281 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -89,6 +89,9 @@ private: /// casts current spell at specified location void performSpellcasting(const int3 & castTarget); + /// dim interface if some windows opened + void dim(Canvas & to); + protected: /// CIntObject interface implementation diff --git a/client/render/Canvas.h b/client/render/Canvas.h index c624507d1..773be3cae 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -93,7 +93,7 @@ public: /// renders multiple lines of text with specified parameters void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); - /// fills selected area with solid color, ignoring any transparency + /// fills selected area with solid color void drawColor(const Rect & target, const ColorRGBA & color); /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 6702153a3..7b5e8b4c0 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -804,7 +804,10 @@ void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - SDL_FillRect(dst, &newRect, sdlColor); + SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); + SDL_FillRect(tmp, NULL, sdlColor); + SDL_BlitSurface(tmp, NULL, dst, &newRect); + SDL_FreeSurface(tmp); } STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) From 1d6469ab36586c1ae3c075a302622418e5170111 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:43:37 +0200 Subject: [PATCH 0265/1248] battlewindow --- client/adventureMap/AdventureMapInterface.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 8d816261c..e91f04793 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -27,6 +27,7 @@ #include "../windows/CCastleInterface.h" #include "../windows/CHeroWindow.h" #include "../windows/CKingdomInterface.h" +#include "../battle/BattleWindow.h" #include "../CGameInfo.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" @@ -171,7 +172,8 @@ void AdventureMapInterface::dim(Canvas & to) { if(!GH.windows().findWindows().empty() || !GH.windows().findWindows().empty() || - !GH.windows().findWindows().empty()) + !GH.windows().findWindows().empty() || + !GH.windows().findWindows().empty()) to.drawColor(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y), ColorRGBA(0, 0, 0, 128)); } From b4c20734f37ba743d015afa5fdb5fdc78d58c9af Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Aug 2023 02:17:29 +0400 Subject: [PATCH 0266/1248] Support timer logic for pve and pvp --- client/adventureMap/TurnTimerWidget.cpp | 3 +- server/TurnTimerHandler.cpp | 250 ++++++++++++++--------- server/TurnTimerHandler.h | 20 +- server/battles/BattleResultProcessor.cpp | 4 +- 4 files changed, 177 insertions(+), 100 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index a66587a87..88b1ccd08 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -110,7 +110,8 @@ void TurnTimerWidget::tick(uint32_t msPassed) if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) player = stack->getOwner(); } - else if (!LOCPLINT->cb->isPlayerMakingTurn(player)) + + if(p != player || !LOCPLINT->cb->isPlayerMakingTurn(player)) continue; auto time = LOCPLINT->cb->getPlayerTurnTime(player); diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 6020de0be..936b2926b 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -31,10 +31,11 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { - timers[player] = si->turnTimerInfo; - timers[player].turnTimer = 0; - timerEnabled[player] = true; - timerLastUpdate[player] = std::numeric_limits::max(); + timerInfo[player].timer = si->turnTimerInfo; + timerInfo[player].timer.turnTimer = 0; + timerInfo[player].isEnabled = true; + timerInfo[player].isBattle = false; + timerInfo[player].lastUpdate = std::numeric_limits::max(); } } @@ -42,7 +43,17 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) { std::lock_guard guard(mx); assert(player.isValidPlayer()); - timerEnabled[player] = enabled; + timerInfo[player].isEnabled = enabled; +} + +void TurnTimerHandler::sendTimerUpdate(PlayerColor player) +{ + auto & info = timerInfo[player]; + TurnTimeUpdate ttu; + ttu.player = player; + ttu.turnTimer = info.timer; + gameHandler.sendAndApply(&ttu); + info.lastUpdate = 0; } void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) @@ -52,15 +63,12 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(si->turnTimerInfo.isEnabled()) { + auto & info = timerInfo[player]; if(si->turnTimerInfo.baseTimer > 0) - timers[player].baseTimer += timers[player].turnTimer; - timers[player].turnTimer = si->turnTimerInfo.turnTimer; + info.timer.baseTimer += info.timer.turnTimer; + info.timer.turnTimer = si->turnTimerInfo.turnTimer; - TurnTimeUpdate ttu; - ttu.player = player; - ttu.turnTimer = timers[player]; - gameHandler.sendAndApply(&ttu); - timerLastUpdate[player] = 0; + sendTimerUpdate(player); } } } @@ -79,45 +87,66 @@ void TurnTimerHandler::update(int waitTime) } } +bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor player, int waitTime) +{ + if(timer > 0) + { + auto & info = timerInfo[player]; + timer -= waitTime; + info.lastUpdate += waitTime; + int frequency = (timer > turnTimePropagateThreshold + && initialTimer - timer > turnTimePropagateThreshold) + ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; + + if(info.lastUpdate >= frequency) + sendTimerUpdate(player); + + return true; + } + return false; +} + void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) { std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || gs->curB || !si->turnTimerInfo.isEnabled()) + if(!si || !gs || !si->turnTimerInfo.isEnabled()) return; - auto & state = gs->players.at(player); - - if(state.human && timerEnabled[player]) + auto & info = timerInfo[player]; + const auto * state = gameHandler.getPlayerState(player); + if(state && state->human && info.isEnabled && !info.isBattle && state->status == EPlayerStatus::INGAME) { - if(timers[player].turnTimer > 0) + if(!timerCountDown(info.timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) { - timers[player].turnTimer -= waitTime; - timerLastUpdate[player] += waitTime; - int frequency = (timers[player].turnTimer > turnTimePropagateThreshold ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit); - - if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && timerLastUpdate[player] >= frequency) + if(info.timer.baseTimer > 0) { - TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = timers[player]; - gameHandler.sendAndApply(&ttu); - timerLastUpdate[player] = 0; + info.timer.turnTimer = info.timer.baseTimer; + info.timer.baseTimer = 0; + onPlayerMakingTurn(player, 0); } + else if(!gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries + gameHandler.turnOrder->onPlayerEndsTurn(state->color); } - else if(timers[player].baseTimer > 0) - { - timers[player].turnTimer = timers[player].baseTimer; - timers[player].baseTimer = 0; - onPlayerMakingTurn(player, 0); - } - else if(!gameHandler.queries->topQuery(state.color)) //wait for replies to avoid pending queries - gameHandler.turnOrder->onPlayerEndsTurn(state.color); } } +bool TurnTimerHandler::isPvpBattle() const +{ + const auto * gs = gameHandler.gameState(); + auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + if(attacker.isValidPlayer() && defender.isValidPlayer()) + { + const auto * attackerState = gameHandler.getPlayerState(attacker); + const auto * defenderState = gameHandler.getPlayerState(defender); + if(attackerState && defenderState && attackerState->human && defenderState->human) + return true; + } + return false; +} + void TurnTimerHandler::onBattleStart() { std::lock_guard guard(mx); @@ -129,18 +158,47 @@ void TurnTimerHandler::onBattleStart() auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + bool pvpBattle = isPvpBattle(); + for(auto i : {attacker, defender}) { if(i.isValidPlayer()) { - timers[i].battleTimer = si->turnTimerInfo.battleTimer; - timers[i].creatureTimer = si->turnTimerInfo.creatureTimer; + auto & info = timerInfo[i]; + info.isBattle = true; + info.timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); + info.timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); - TurnTimeUpdate ttu; - ttu.player = i; - ttu.turnTimer = timers[i]; - gameHandler.sendAndApply(&ttu); - timerLastUpdate[i] = 0; + sendTimerUpdate(i); + } + } +} + +void TurnTimerHandler::onBattleEnd() +{ + std::lock_guard guard(mx); + const auto * gs = gameHandler.gameState(); + const auto * si = gameHandler.getStartInfo(); + if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + return; + + auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + + bool pvpBattle = isPvpBattle(); + + for(auto i : {attacker, defender}) + { + if(i.isValidPlayer() && !pvpBattle) + { + auto & info = timerInfo[i]; + info.isBattle = false; + if(si->turnTimerInfo.baseTimer && info.timer.baseTimer == 0) + info.timer.baseTimer = info.timer.creatureTimer; + else if(si->turnTimerInfo.turnTimer && info.timer.turnTimer == 0) + info.timer.turnTimer = info.timer.creatureTimer; + + sendTimerUpdate(i); } } } @@ -153,20 +211,17 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) return; - auto player = stack.getOwner(); - - if(!player.isValidPlayer()) - return; + if(isPvpBattle()) + { + auto player = stack.getOwner(); - if(timers[player].battleTimer == 0) - timers[player].battleTimer = timers[player].creatureTimer; - timers[player].creatureTimer = si->turnTimerInfo.creatureTimer; + auto & info = timerInfo[player]; + if(info.timer.battleTimer == 0) + info.timer.battleTimer = info.timer.creatureTimer; + info.timer.creatureTimer = si->turnTimerInfo.creatureTimer; - TurnTimeUpdate ttu; - ttu.player = player; - ttu.turnTimer = timers[player]; - gameHandler.sendAndApply(&ttu); - timerLastUpdate[player] = 0; + sendTimerUpdate(player); + } } void TurnTimerHandler::onBattleLoop(int waitTime) @@ -179,65 +234,72 @@ void TurnTimerHandler::onBattleLoop(int waitTime) ui8 side = 0; const CStack * stack = nullptr; - bool isTactisPhase = gs->curB.get()->battleTacticDist() > 0; + bool isTactisPhase = gs->curB->battleTacticDist() > 0; if(isTactisPhase) - side = gs->curB.get()->battleGetTacticsSide(); + side = gs->curB->battleGetTacticsSide(); else { - stack = gs->curB.get()->battleGetStackByID(gs->curB->getActiveStackID()); + stack = gs->curB->battleGetStackByID(gs->curB->getActiveStackID()); if(!stack || !stack->getOwner().isValidPlayer()) return; side = stack->unitSide(); } - auto & state = gs->players.at(gs->curB->getSidePlayer(side)); + auto player = gs->curB->getSidePlayer(side); + if(!player.isValidPlayer()) + return; - auto turnTimerUpdateApplier = [&](TurnTimerInfo & tTimer, int waitTime) + const auto * state = gameHandler.getPlayerState(player); + if(!state || state->status != EPlayerStatus::INGAME || !state->human) + return; + + auto & info = timerInfo[player]; + if(info.isEnabled && info.isBattle && !timerCountDown(info.timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) { - if(tTimer.creatureTimer > 0) + if(isPvpBattle()) { - tTimer.creatureTimer -= waitTime; - timerLastUpdate[state.color] += waitTime; - int frequency = (tTimer.creatureTimer > turnTimePropagateThreshold - && si->turnTimerInfo.creatureTimer - tTimer.creatureTimer > turnTimePropagateThreshold) - ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; - - if(state.status == EPlayerStatus::INGAME //do not send message if player is not active already - && timerLastUpdate[state.color] >= frequency) + if(info.timer.battleTimer > 0) { - TurnTimeUpdate ttu; - ttu.player = state.color; - ttu.turnTimer = tTimer; - gameHandler.sendAndApply(&ttu); - timerLastUpdate[state.color] = 0; + info.timer.creatureTimer = info.timer.battleTimer; + timerCountDown(info.timer.creatureTimer, info.timer.battleTimer, player, 0); + info.timer.battleTimer = 0; + } + else + { + BattleAction doNothing; + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; + else + { + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); + } + gameHandler.battles->makePlayerBattleAction(player, doNothing); } - return true; - } - return false; - }; - - if(state.human && timerEnabled[state.color] - && !turnTimerUpdateApplier(timers[state.color], waitTime)) - { - if(timers[state.color].battleTimer > 0) - { - timers[state.color].creatureTimer = timers[state.color].battleTimer; - timers[state.color].battleTimer = 0; - turnTimerUpdateApplier(timers[state.color], 0); } else { - BattleAction doNothing; - doNothing.side = side; - if(isTactisPhase) - doNothing.actionType = EActionType::END_TACTIC_PHASE; + if(info.timer.turnTimer > 0) + { + info.timer.creatureTimer = info.timer.turnTimer; + timerCountDown(info.timer.creatureTimer, info.timer.turnTimer, player, 0); + info.timer.turnTimer = 0; + } + else if(info.timer.baseTimer > 0) + { + info.timer.creatureTimer = info.timer.baseTimer; + timerCountDown(info.timer.creatureTimer, info.timer.baseTimer, player, 0); + info.timer.baseTimer = 0; + } else { - doNothing.actionType = EActionType::DEFEND; - doNothing.stackNumber = stack->unitId(); + BattleAction retreat; + retreat.side = side; + retreat.actionType = EActionType::RETREAT; //harsh punishment + gameHandler.battles->makePlayerBattleAction(player, retreat); } - gameHandler.battles->makePlayerBattleAction(state.color, doNothing); } } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 572831da3..cdbcb6ca3 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -10,11 +10,12 @@ #pragma once +#include "../lib/TurnTimerInfo.h" + VCMI_LIB_NAMESPACE_BEGIN class CStack; class PlayerColor; -struct TurnTimerInfo; VCMI_LIB_NAMESPACE_END @@ -22,18 +23,28 @@ class CGameHandler; class TurnTimerHandler { + struct PlayerTimerInfo + { + TurnTimerInfo timer; + bool isEnabled = true; + bool isBattle = false; + int lastUpdate = 0; + }; + CGameHandler & gameHandler; const int turnTimePropagateFrequency = 5000; const int turnTimePropagateFrequencyCrit = 1000; const int turnTimePropagateThreshold = 3000; - std::map timers; - std::map timerEnabled; - std::map timerLastUpdate; + std::map timerInfo; std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); void onBattleLoop(int waitTime); + bool timerCountDown(int & timerToApply, int initialTimer, PlayerColor player, int waitTime); + bool isPvpBattle() const; + void sendTimerUpdate(PlayerColor player); + public: TurnTimerHandler(CGameHandler &); @@ -41,6 +52,7 @@ public: void onPlayerGetTurn(PlayerColor player); void onBattleStart(); void onBattleNextStack(const CStack & stack); + void onBattleEnd(); void update(int waitTime); void setTimerEnabled(PlayerColor player, bool enabled); }; diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6bf6e6fcb..f983b9797 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -264,6 +264,8 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta } gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed + + gameHandler->turnTimerHandler.onBattleEnd(); if (battleResult->queryID == QueryID::NONE) endBattleConfirm(gameHandler->gameState()->curB); @@ -463,7 +465,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); raccepted.heroResult[0].exp = battleResult->exp[0]; raccepted.heroResult[1].exp = battleResult->exp[1]; - raccepted.winnerSide = finishingBattle->winnerSide; + raccepted.winnerSide = finishingBattle->winnerSide; gameHandler->sendAndApply(&raccepted); gameHandler->queries->popIfTop(battleQuery); From f30f00faa078cf43219f4b066cd468dc0ab51a60 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Aug 2023 02:18:44 +0400 Subject: [PATCH 0267/1248] Timer pausing while waiting for player accept turn --- server/queries/MapQueries.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index a237d9118..900c36cdd 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -28,12 +28,12 @@ bool PlayerStartsTurnQuery::blocksPack(const CPack *pack) const void PlayerStartsTurnQuery::onAdding(PlayerColor color) { - //gh->turnTimerHandler.setTimerEnabled(color, false); + gh->turnTimerHandler.setTimerEnabled(color, false); } void PlayerStartsTurnQuery::onRemoval(PlayerColor color) { - //gh->turnTimerHandler.setTimerEnabled(color, true); + gh->turnTimerHandler.setTimerEnabled(color, true); } bool PlayerStartsTurnQuery::endsByPlayerAnswer() const From 9a42abe2a7f8d46cb2346629f552be042432bdc8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Aug 2023 03:11:46 +0400 Subject: [PATCH 0268/1248] Extended timer info to exhange between client and server --- client/CPlayerInterface.cpp | 9 --- client/CPlayerInterface.h | 3 - client/adventureMap/TurnTimerWidget.cpp | 24 +++---- lib/TurnTimerInfo.h | 5 ++ server/TurnTimerHandler.cpp | 96 ++++++++++++------------- server/TurnTimerHandler.h | 13 +--- 6 files changed, 67 insertions(+), 83 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index fa035e9e2..79cef66f6 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -140,7 +140,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; - timerEnabled = true; duringMovement = false; ignoreEvents = false; numOfMovedArts = 0; @@ -272,8 +271,6 @@ void CPlayerInterface::yourTurn(QueryID queryID) makingTurn = true; adventureInt->onPlayerTurnStarted(playerID); } - - timerEnabled = false; } acceptTurn(queryID); } @@ -326,7 +323,6 @@ void CPlayerInterface::acceptTurn(QueryID queryID) } cb->selectionMade(0, queryID); - timerEnabled = true; } void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) @@ -2127,8 +2123,3 @@ std::optional CPlayerInterface::makeSurrenderRetreatDecision(const { return std::nullopt; } - -bool CPlayerInterface::isTimerEnabled() const -{ - return timerEnabled; -} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 157158a71..c5048637f 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -57,7 +57,6 @@ namespace boost /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public IUpdateable { - bool timerEnabled; bool duringMovement; bool ignoreEvents; size_t numOfMovedArts; @@ -207,8 +206,6 @@ public: // public interface for use by client via LOCPLINT access ///returns true if all events are processed internally bool capturedAllEvents(); - - bool isTimerEnabled() const; CPlayerInterface(PlayerColor Player); ~CPlayerInterface(); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 88b1ccd08..bd9606d62 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -102,22 +102,27 @@ void TurnTimerWidget::tick(uint32_t msPassed) if(!LOCPLINT || !LOCPLINT->cb) return; - for (PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) { auto player = p; if(LOCPLINT->battleInt) { if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) player = stack->getOwner(); + else + continue; + if(p != player) + continue; } - - if(p != player || !LOCPLINT->cb->isPlayerMakingTurn(player)) + else if(!LOCPLINT->cb->isPlayerMakingTurn(player)) continue; auto time = LOCPLINT->cb->getPlayerTurnTime(player); - if(LOCPLINT->isTimerEnabled()) + if(time.isActive) cachedTurnTime -= msPassed; - if(cachedTurnTime < 0) cachedTurnTime = 0; //do not go below zero + + if(cachedTurnTime < 0) + cachedTurnTime = 0; //do not go below zero if(lastPlayer != player) { @@ -140,15 +145,10 @@ void TurnTimerWidget::tick(uint32_t msPassed) auto * playerInfo = LOCPLINT->cb->getPlayer(player); if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) { - if(LOCPLINT->battleInt) - { - if(time.isBattleEnabled()) - timeCheckAndUpdate(time.creatureTimer); - } + if(time.isBattle) + timeCheckAndUpdate(time.creatureTimer); else - { timeCheckAndUpdate(time.turnTimer); - } } else timeCheckAndUpdate(0); diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index f29530726..843e0c0de 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -19,6 +19,9 @@ struct DLL_LINKAGE TurnTimerInfo int battleTimer = 0; //in ms, counts down during battles when creature timer runs out int creatureTimer = 0; //in ms, counts down when player is choosing action in battle + bool isActive = true; //should be paused if set to false + bool isBattle = false; //indicator for current timer mode + bool isEnabled() const; bool isBattleEnabled() const; @@ -29,6 +32,8 @@ struct DLL_LINKAGE TurnTimerInfo h & baseTimer; h & battleTimer; h & creatureTimer; + h & isActive; + h & isBattle; } }; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 936b2926b..7473b2f8c 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -31,11 +31,11 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) std::lock_guard guard(mx); if(const auto * si = gameHandler.getStartInfo()) { - timerInfo[player].timer = si->turnTimerInfo; - timerInfo[player].timer.turnTimer = 0; - timerInfo[player].isEnabled = true; - timerInfo[player].isBattle = false; - timerInfo[player].lastUpdate = std::numeric_limits::max(); + timers[player] = si->turnTimerInfo; + timers[player].turnTimer = 0; + timers[player].isActive = true; + timers[player].isBattle = false; + lastUpdate[player] = std::numeric_limits::max(); } } @@ -43,17 +43,16 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) { std::lock_guard guard(mx); assert(player.isValidPlayer()); - timerInfo[player].isEnabled = enabled; + timers[player].isActive = enabled; } void TurnTimerHandler::sendTimerUpdate(PlayerColor player) { - auto & info = timerInfo[player]; TurnTimeUpdate ttu; ttu.player = player; - ttu.turnTimer = info.timer; + ttu.turnTimer = timers[player]; gameHandler.sendAndApply(&ttu); - info.lastUpdate = 0; + lastUpdate[player] = 0; } void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) @@ -63,10 +62,10 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(si->turnTimerInfo.isEnabled()) { - auto & info = timerInfo[player]; + auto & timer = timers[player]; if(si->turnTimerInfo.baseTimer > 0) - info.timer.baseTimer += info.timer.turnTimer; - info.timer.turnTimer = si->turnTimerInfo.turnTimer; + timer.baseTimer += timer.turnTimer; + timer.turnTimer = si->turnTimerInfo.turnTimer; sendTimerUpdate(player); } @@ -91,14 +90,13 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor { if(timer > 0) { - auto & info = timerInfo[player]; timer -= waitTime; - info.lastUpdate += waitTime; + lastUpdate[player] += waitTime; int frequency = (timer > turnTimePropagateThreshold && initialTimer - timer > turnTimePropagateThreshold) ? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit; - if(info.lastUpdate >= frequency) + if(lastUpdate[player] >= frequency) sendTimerUpdate(player); return true; @@ -114,16 +112,16 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) if(!si || !gs || !si->turnTimerInfo.isEnabled()) return; - auto & info = timerInfo[player]; + auto & timer = timers[player]; const auto * state = gameHandler.getPlayerState(player); - if(state && state->human && info.isEnabled && !info.isBattle && state->status == EPlayerStatus::INGAME) + if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME) { - if(!timerCountDown(info.timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + if(!timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) { - if(info.timer.baseTimer > 0) + if(timer.baseTimer > 0) { - info.timer.turnTimer = info.timer.baseTimer; - info.timer.baseTimer = 0; + timer.turnTimer = timer.baseTimer; + timer.baseTimer = 0; onPlayerMakingTurn(player, 0); } else if(!gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries @@ -164,10 +162,10 @@ void TurnTimerHandler::onBattleStart() { if(i.isValidPlayer()) { - auto & info = timerInfo[i]; - info.isBattle = true; - info.timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); - info.timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); + auto & timer = timers[i]; + timer.isBattle = true; + timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); + timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); sendTimerUpdate(i); } @@ -191,12 +189,12 @@ void TurnTimerHandler::onBattleEnd() { if(i.isValidPlayer() && !pvpBattle) { - auto & info = timerInfo[i]; - info.isBattle = false; - if(si->turnTimerInfo.baseTimer && info.timer.baseTimer == 0) - info.timer.baseTimer = info.timer.creatureTimer; - else if(si->turnTimerInfo.turnTimer && info.timer.turnTimer == 0) - info.timer.turnTimer = info.timer.creatureTimer; + auto & timer = timers[i]; + timer.isBattle = false; + if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) + timer.baseTimer = timer.creatureTimer; + else if(si->turnTimerInfo.turnTimer && timer.turnTimer == 0) + timer.turnTimer = timer.creatureTimer; sendTimerUpdate(i); } @@ -215,10 +213,10 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) { auto player = stack.getOwner(); - auto & info = timerInfo[player]; - if(info.timer.battleTimer == 0) - info.timer.battleTimer = info.timer.creatureTimer; - info.timer.creatureTimer = si->turnTimerInfo.creatureTimer; + auto & timer = timers[player]; + if(timer.battleTimer == 0) + timer.battleTimer = timer.creatureTimer; + timer.creatureTimer = si->turnTimerInfo.creatureTimer; sendTimerUpdate(player); } @@ -254,16 +252,16 @@ void TurnTimerHandler::onBattleLoop(int waitTime) if(!state || state->status != EPlayerStatus::INGAME || !state->human) return; - auto & info = timerInfo[player]; - if(info.isEnabled && info.isBattle && !timerCountDown(info.timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) + auto & timer = timers[player]; + if(timer.isActive && timer.isBattle && !timerCountDown(timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) { if(isPvpBattle()) { - if(info.timer.battleTimer > 0) + if(timer.battleTimer > 0) { - info.timer.creatureTimer = info.timer.battleTimer; - timerCountDown(info.timer.creatureTimer, info.timer.battleTimer, player, 0); - info.timer.battleTimer = 0; + timer.creatureTimer = timer.battleTimer; + timerCountDown(timer.creatureTimer, timer.battleTimer, player, 0); + timer.battleTimer = 0; } else { @@ -281,17 +279,17 @@ void TurnTimerHandler::onBattleLoop(int waitTime) } else { - if(info.timer.turnTimer > 0) + if(timer.turnTimer > 0) { - info.timer.creatureTimer = info.timer.turnTimer; - timerCountDown(info.timer.creatureTimer, info.timer.turnTimer, player, 0); - info.timer.turnTimer = 0; + timer.creatureTimer = timer.turnTimer; + timerCountDown(timer.creatureTimer, timer.turnTimer, player, 0); + timer.turnTimer = 0; } - else if(info.timer.baseTimer > 0) + else if(timer.baseTimer > 0) { - info.timer.creatureTimer = info.timer.baseTimer; - timerCountDown(info.timer.creatureTimer, info.timer.baseTimer, player, 0); - info.timer.baseTimer = 0; + timer.creatureTimer = timer.baseTimer; + timerCountDown(timer.creatureTimer, timer.baseTimer, player, 0); + timer.baseTimer = 0; } else { diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index cdbcb6ca3..5039dc07b 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -22,20 +22,13 @@ VCMI_LIB_NAMESPACE_END class CGameHandler; class TurnTimerHandler -{ - struct PlayerTimerInfo - { - TurnTimerInfo timer; - bool isEnabled = true; - bool isBattle = false; - int lastUpdate = 0; - }; - +{ CGameHandler & gameHandler; const int turnTimePropagateFrequency = 5000; const int turnTimePropagateFrequencyCrit = 1000; const int turnTimePropagateThreshold = 3000; - std::map timerInfo; + std::map timers; + std::map lastUpdate; std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); From cc3ca9a41e4af9a8ab78e4056932c5500f5898b0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Aug 2023 03:33:59 +0400 Subject: [PATCH 0269/1248] Timers fixes --- client/adventureMap/TurnTimerWidget.cpp | 99 ++++++++++++------------ client/adventureMap/TurnTimerWidget.h | 2 + lib/TurnTimerInfo.h | 2 +- server/TurnTimerHandler.cpp | 14 ++-- server/battles/BattleResultProcessor.cpp | 3 +- 5 files changed, 64 insertions(+), 56 deletions(-) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index bd9606d62..b47d111da 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -97,60 +97,63 @@ void TurnTimerWidget::setTime(PlayerColor player, int time) } } +void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed) +{ + const auto & time = LOCPLINT->cb->getPlayerTurnTime(player); + if(time.isActive) + cachedTurnTime -= msPassed; + + if(cachedTurnTime < 0) + cachedTurnTime = 0; //do not go below zero + + if(lastPlayer != player) + { + lastPlayer = player; + lastTurnTime = 0; + } + + auto timeCheckAndUpdate = [&](int time) + { + if(time / 1000 != lastTurnTime / 1000) + { + //do not update timer on this tick + lastTurnTime = time; + cachedTurnTime = time; + } + else + setTime(player, cachedTurnTime); + }; + + auto * playerInfo = LOCPLINT->cb->getPlayer(player); + if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) + { + if(time.isBattle) + timeCheckAndUpdate(time.creatureTimer); + else + timeCheckAndUpdate(time.turnTimer); + } + else + timeCheckAndUpdate(0); +} + void TurnTimerWidget::tick(uint32_t msPassed) { if(!LOCPLINT || !LOCPLINT->cb) return; - for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + if(LOCPLINT->battleInt) { - auto player = p; - if(LOCPLINT->battleInt) - { - if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) - player = stack->getOwner(); - else - continue; - if(p != player) - continue; - } - else if(!LOCPLINT->cb->isPlayerMakingTurn(player)) - continue; - - auto time = LOCPLINT->cb->getPlayerTurnTime(player); - if(time.isActive) - cachedTurnTime -= msPassed; - - if(cachedTurnTime < 0) - cachedTurnTime = 0; //do not go below zero - - if(lastPlayer != player) - { - lastPlayer = player; - lastTurnTime = 0; - } - - auto timeCheckAndUpdate = [&](int time) - { - if(time / 1000 != lastTurnTime / 1000) - { - //do not update timer on this tick - lastTurnTime = time; - cachedTurnTime = time; - } - else - setTime(player, cachedTurnTime); - }; - - auto * playerInfo = LOCPLINT->cb->getPlayer(player); - if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) - { - if(time.isBattle) - timeCheckAndUpdate(time.creatureTimer); - else - timeCheckAndUpdate(time.turnTimer); - } + if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack()) + updateTimer(stack->getOwner(), msPassed); else - timeCheckAndUpdate(0); + updateTimer(PlayerColor::NEUTRAL, msPassed); + } + else + { + for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + { + if(LOCPLINT->cb->isPlayerMakingTurn(p)) + updateTimer(p, msPassed); + } } } diff --git a/client/adventureMap/TurnTimerWidget.h b/client/adventureMap/TurnTimerWidget.h index ccc801eb4..f8b4b97fc 100644 --- a/client/adventureMap/TurnTimerWidget.h +++ b/client/adventureMap/TurnTimerWidget.h @@ -46,6 +46,8 @@ private: std::set notifications; std::shared_ptr buildDrawRect(const JsonNode & config) const; + + void updateTimer(PlayerColor player, uint32_t msPassed); public: diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index 843e0c0de..c708345b4 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -19,7 +19,7 @@ struct DLL_LINKAGE TurnTimerInfo int battleTimer = 0; //in ms, counts down during battles when creature timer runs out int creatureTimer = 0; //in ms, counts down when player is choosing action in battle - bool isActive = true; //should be paused if set to false + bool isActive = false; //is being counting down bool isBattle = false; //indicator for current timer mode bool isEnabled() const; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 7473b2f8c..9dae655af 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -187,14 +187,18 @@ void TurnTimerHandler::onBattleEnd() for(auto i : {attacker, defender}) { - if(i.isValidPlayer() && !pvpBattle) + if(i.isValidPlayer()) { auto & timer = timers[i]; timer.isBattle = false; - if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) - timer.baseTimer = timer.creatureTimer; - else if(si->turnTimerInfo.turnTimer && timer.turnTimer == 0) - timer.turnTimer = timer.creatureTimer; + + if(!pvpBattle) + { + if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) + timer.baseTimer = timer.creatureTimer; + else if(si->turnTimerInfo.turnTimer && timer.turnTimer == 0) + timer.turnTimer = timer.creatureTimer; + } sendTimerUpdate(i); } diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index f983b9797..bf0827949 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -263,9 +263,8 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta otherBattleQuery->result = battleQuery->result; } - gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed - gameHandler->turnTimerHandler.onBattleEnd(); + gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed if (battleResult->queryID == QueryID::NONE) endBattleConfirm(gameHandler->gameState()->curB); From 00216168bf2aacb00591d83ad1ee522b9a0c156e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 30 Aug 2023 03:44:09 +0400 Subject: [PATCH 0270/1248] Timer works as designed --- server/CGameHandler.cpp | 7 +++---- server/TurnTimerHandler.cpp | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index cbe6505b5..2ca17f61d 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1178,11 +1178,10 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner)) { moveQuery = std::dynamic_pointer_cast(topQuery); - if(moveQuery - && (!transit || result != TryMoveHero::SUCCESS)) - queries->popIfTop(moveQuery); - else + if(!moveQuery || (transit && result == TryMoveHero::SUCCESS)) break; + + queries->popIfTop(moveQuery); } logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 9dae655af..f7e7a2688 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -44,6 +44,7 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) std::lock_guard guard(mx); assert(player.isValidPlayer()); timers[player].isActive = enabled; + sendTimerUpdate(player); } void TurnTimerHandler::sendTimerUpdate(PlayerColor player) From e650f0100b645bf4378289854482eae55005d009 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:41:28 +0200 Subject: [PATCH 0271/1248] setting + all non popups --- client/adventureMap/AdventureMapInterface.cpp | 14 +++++++++----- config/schemas/settings.json | 8 ++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index e91f04793..bad80e24c 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -170,11 +170,15 @@ void AdventureMapInterface::show(Canvas & to) void AdventureMapInterface::dim(Canvas & to) { - if(!GH.windows().findWindows().empty() || - !GH.windows().findWindows().empty() || - !GH.windows().findWindows().empty() || - !GH.windows().findWindows().empty()) - to.drawColor(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y), ColorRGBA(0, 0, 0, 128)); + for (auto window : GH.windows().findWindows()) + { + std::shared_ptr casted = std::dynamic_pointer_cast(window); + if (!casted && !window->isPopupWindow()) + { + to.drawColor(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y), ColorRGBA(0, 0, 0, std::clamp(settings["adventure"]["backgroundDimLevel"].Integer(), 0, 255))); + return; + } + } } void AdventureMapInterface::tick(uint32_t msPassed) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 57a3ef16b..dd702c0c2 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -226,7 +226,7 @@ "type" : "object", "additionalProperties" : false, "default" : {}, - "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging" ], + "required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel" ], "properties" : { "heroMoveTime" : { "type" : "number", @@ -274,7 +274,11 @@ "smoothDragging" : { "type" : "boolean", "default" : true - } + }, + "backgroundDimLevel" : { + "type" : "number", + "default" : 128 + }, } }, "battle" : { From 31e055880997bb0aed16ca0e44ec638603f4c3d8 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:43:59 +0200 Subject: [PATCH 0272/1248] remove unneeded headers --- client/adventureMap/AdventureMapInterface.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index bad80e24c..c98911b14 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -24,10 +24,6 @@ #include "../mapView/mapHandler.h" #include "../mapView/MapView.h" #include "../windows/InfoWindows.h" -#include "../windows/CCastleInterface.h" -#include "../windows/CHeroWindow.h" -#include "../windows/CKingdomInterface.h" -#include "../battle/BattleWindow.h" #include "../CGameInfo.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" From 3e169ef2f58693ab6518e8c4394edc16d2f64e59 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 01:07:39 +0300 Subject: [PATCH 0273/1248] Fix visiting of markets --- lib/mapObjects/IMarket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index bf38c954f..4bbccc872 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -169,7 +169,7 @@ IMarket::IMarket() std::vector IMarket::availableModes() const { std::vector ret; - for (EMarketMode i = static_cast(0); i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; vstd::next(i, 1)) + for (EMarketMode i = static_cast(0); i < EMarketMode::MARTKET_AFTER_LAST_PLACEHOLDER; i = vstd::next(i, 1)) if(allowsTrade(i)) ret.push_back(i); From 0f4f9b323343866bd6c82413619ac948feb41ad9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 01:32:32 +0300 Subject: [PATCH 0274/1248] Fix handling of opening spells when tactics is present --- server/battles/BattleFlowProcessor.cpp | 6 ++++++ server/battles/BattleProcessor.h | 1 + 2 files changed, 7 insertions(+) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 8d24f15a0..dc1977d19 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -507,6 +507,12 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) { const auto & battle = gameHandler->gameState()->curB; + if (ba.actionType == EActionType::END_TACTIC_PHASE) + { + onTacticsEnded(); + return; + } + const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber, false); const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID(), false); diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 07df198dd..40cce6aee 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -39,6 +39,7 @@ class BattleProcessor : boost::noncopyable std::unique_ptr flowProcessor; std::unique_ptr resultProcessor; + void onTacticsEnded(); void updateGateState(); void engageIntoBattle(PlayerColor player); From 2e411eca193e0dd505b40844304ae48b27d80a19 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 01:38:16 +0300 Subject: [PATCH 0275/1248] Do not allow stopping movement while using water walk over water --- client/CPlayerInterface.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 8353e7a55..ca45b0a93 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -2025,13 +2025,13 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) auto canStop = [&](CGPathNode * node) -> bool { - if (node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL) - return true; + if (node->layer != EPathfindingLayer::LAND && node->layer != EPathfindingLayer::SAIL) + return false; - if (node->accessible == EPathAccessibility::ACCESSIBLE) - return true; + if (node->accessible != EPathAccessibility::ACCESSIBLE) + return false; - return false; + return true; }; for (i=(int)path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) From d7f06986512d53388458999b360d813b4b718ce7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 31 Aug 2023 20:46:48 +0400 Subject: [PATCH 0276/1248] Change text hints --- Mods/vcmi/config/vcmi/english.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index cb7c2489f..4fb4c6722 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -191,10 +191,10 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Base timer}\n\nCounts down when {turn timer} runs out, never resets. If timer expires, player ends turn.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nCounts down when player is making turn on adventure map, resets every turn. If {base timer} is enabled, unspent time will be added to it.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when {stack timer} runs out, resets every battle. if timer expires, currently acting stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Creature timer}\n\nCounts down when player is choosing action in battle, resets after each stack turn.", + "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", + "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", + "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", + "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", "vcmi.optionsTab.widgets.labelTimer" : "Timer", "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", From 3593911ddead0e11aede6f2c113d8cf6386fc295 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:00:02 +0200 Subject: [PATCH 0277/1248] improve readability --- client/adventureMap/AdventureMapInterface.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index c98911b14..96c1864c6 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -171,7 +171,9 @@ void AdventureMapInterface::dim(Canvas & to) std::shared_ptr casted = std::dynamic_pointer_cast(window); if (!casted && !window->isPopupWindow()) { - to.drawColor(Rect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y), ColorRGBA(0, 0, 0, std::clamp(settings["adventure"]["backgroundDimLevel"].Integer(), 0, 255))); + Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); + ColorRGBA colorToFill(0, 0, 0, std::clamp(settings["adventure"]["backgroundDimLevel"].Integer(), 0, 255)); + to.drawColor(targetRect, colorToFill); return; } } From 1721fe1ad6e09f85c46d898c1751797ee529642b Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:03:54 +0200 Subject: [PATCH 0278/1248] optimize if disabled --- client/adventureMap/AdventureMapInterface.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 96c1864c6..68eba1766 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -171,9 +171,11 @@ void AdventureMapInterface::dim(Canvas & to) std::shared_ptr casted = std::dynamic_pointer_cast(window); if (!casted && !window->isPopupWindow()) { + int backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); - ColorRGBA colorToFill(0, 0, 0, std::clamp(settings["adventure"]["backgroundDimLevel"].Integer(), 0, 255)); - to.drawColor(targetRect, colorToFill); + ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); + if(backgroundDimLevel > 0) + to.drawColor(targetRect, colorToFill); return; } } From 31956549e68bee82219b9a196b59c42605417ad6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 03:29:50 +0400 Subject: [PATCH 0279/1248] Double click behavior --- launcher/modManager/cmodlistview_moc.cpp | 31 ++++++++++++++++++++++++ launcher/modManager/cmodlistview_moc.h | 2 ++ launcher/modManager/cmodlistview_moc.ui | 3 +++ 3 files changed, 36 insertions(+) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 7ecf53ab3..cfab9c50a 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -888,3 +888,34 @@ QString CModListView::getTranslationModName(const QString & language) return QString(); } + +void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) +{ + if(!index.isValid()) + return; + + auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString()); + + if(mod.isAvailable() && !mod.getName().contains('.')) + { + on_installButton_clicked(); + return; + } + + if(mod.isUpdateable()) + { + on_updateButton_clicked(); + } + + if(mod.isDisabled()) + { + on_enableButton_clicked(); + return; + } + + if(mod.isEnabled()) + { + on_disableButton_clicked(); + } +} + diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index e6ede36c1..c645d503f 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -126,6 +126,8 @@ private slots: void on_screenshotsList_clicked(const QModelIndex & index); + void on_allModsView_doubleClicked(const QModelIndex &index); + private: Ui::CModListView * ui; }; diff --git a/launcher/modManager/cmodlistview_moc.ui b/launcher/modManager/cmodlistview_moc.ui index 72961f923..d5bfa0b34 100644 --- a/launcher/modManager/cmodlistview_moc.ui +++ b/launcher/modManager/cmodlistview_moc.ui @@ -119,6 +119,9 @@ Qt::Horizontal + + 12 + QAbstractItemView::SingleSelection From ea7ab9d5ed756a3aa3ae5ac36f01dbe109b7dc89 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 04:12:41 +0400 Subject: [PATCH 0280/1248] Migrate launcher to vcmi versions --- launcher/modManager/cmodlist.cpp | 88 +++++++----------------- launcher/modManager/cmodlist.h | 3 - launcher/modManager/cmodlistview_moc.cpp | 3 +- lib/modding/CModVersion.cpp | 11 +++ lib/modding/CModVersion.h | 2 + 5 files changed, 38 insertions(+), 69 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index f04c485e2..8733adc42 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -14,54 +14,7 @@ #include "../../lib/JsonNode.h" #include "../../lib/filesystem/CFileInputStream.h" #include "../../lib/GameConstants.h" - -namespace -{ -bool isCompatible(const QString & verMin, const QString & verMax) -{ - QVersionNumber vcmiVersion(VCMI_VERSION_MAJOR, - VCMI_VERSION_MINOR, - VCMI_VERSION_PATCH); - - auto versionMin = QVersionNumber::fromString(verMin); - auto versionMax = QVersionNumber::fromString(verMax); - - auto buildVersion = [](QVersionNumber & ver) - { - const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch - - if(ver.segmentCount() < maxSections) - { - auto segments = ver.segments(); - for(int i = segments.size(); i < maxSections; ++i) - segments.append(0); - ver = QVersionNumber(segments); - } - }; - - if(!versionMin.isNull()) - { - buildVersion(versionMin); - if(vcmiVersion < versionMin) - return false; - } - - if(!versionMax.isNull()) - { - buildVersion(versionMax); - if(vcmiVersion > versionMax) - return false; - } - return true; -} -} - -bool CModEntry::compareVersions(QString lesser, QString greater) -{ - auto versionLesser = QVersionNumber::fromString(lesser); - auto versionGreater = QVersionNumber::fromString(greater); - return versionLesser < versionGreater; -} +#include "../../lib/modding/CModVersion.h" QString CModEntry::sizeToString(double size) { @@ -110,18 +63,24 @@ bool CModEntry::isUpdateable() const if(!isInstalled()) return false; - QString installedVer = localData["installedVersion"].toString(); - QString availableVer = repository["latestVersion"].toString(); + auto installedVer = localData["installedVersion"].toString().toStdString(); + auto availableVer = repository["latestVersion"].toString().toStdString(); - if(compareVersions(installedVer, availableVer)) - return true; - return false; + return (CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer)); +} + +bool isCompatible(const QVariantMap & compatibility) +{ + auto compatibleMin = CModVersion::fromString(compatibility["min"].toString().toStdString()); + auto compatibleMax = CModVersion::fromString(compatibility["max"].toString().toStdString()); + + return (compatibleMin.isNull() || CModVersion::GameVersion().compatible(compatibleMin, true, true)) + && (compatibleMax.isNull() || compatibleMax.compatible(CModVersion::GameVersion(), true, true)); } bool CModEntry::isCompatible() const { - auto compatibility = localData["compatibility"].toMap(); - return ::isCompatible(compatibility["min"].toString(), compatibility["max"].toString()); + return ::isCompatible(localData["compatibility"].toMap()); } bool CModEntry::isEssential() const @@ -186,10 +145,10 @@ QVariant CModEntry::getValueImpl(QString value, bool localized) const if(repository.contains(value) && localData.contains(value)) { // value is present in both repo and locally installed. Select one from latest version - QString installedVer = localData["installedVersion"].toString(); - QString availableVer = repository["latestVersion"].toString(); + auto installedVer = localData["installedVersion"].toString().toStdString(); + auto availableVer = repository["latestVersion"].toString().toStdString(); - useRepositoryData = compareVersions(installedVer, availableVer); + useRepositoryData = CModVersion::fromString(installedVer) < CModVersion::fromString(availableVer); } auto & storage = useRepositoryData ? repository : localData; @@ -310,10 +269,8 @@ CModEntry CModList::getMod(QString modname) const if(settings.value("active").toBool()) { - auto compatibility = local.value("compatibility").toMap(); - if(compatibility["min"].isValid() || compatibility["max"].isValid()) - if(!isCompatible(compatibility["min"].toString(), compatibility["max"].toString())) - settings["active"] = false; + if(!::isCompatible(local.value("compatibility").toMap())) + settings["active"] = false; } for(auto entry : repositories) @@ -322,10 +279,11 @@ CModEntry CModList::getMod(QString modname) const if(repoVal.isValid()) { auto repoValMap = repoVal.toMap(); - auto compatibility = repoValMap["compatibility"].toMap(); - if(isCompatible(compatibility["min"].toString(), compatibility["max"].toString())) + if(::isCompatible(repoValMap["compatibility"].toMap())) { - if(repo.empty() || CModEntry::compareVersions(repo["version"].toString(), repoValMap["version"].toString())) + if(repo.empty() + || CModVersion::fromString(repo["version"].toString().toStdString()) + < CModVersion::fromString(repoValMap["version"].toString().toStdString())) { //take valid download link and screenshots before assignment auto download = repo.value("download"); diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index b8a9276ae..4143ed4b8 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -74,9 +74,6 @@ public: QVariant getValue(QString value) const; QVariant getBaseValue(QString value) const; - // returns true if less < greater comparing versions section by section - static bool compareVersions(QString lesser, QString greater); - static QString sizeToString(double size); }; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 7ecf53ab3..2d3d218d6 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -25,6 +25,7 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/Languages.h" +#include "../../lib/modding/CModVersion.h" void CModListView::setupModModel() { @@ -191,7 +192,7 @@ QString CModListView::genChangelogText(CModEntry & mod) std::sort(versions.begin(), versions.end(), [](QString lesser, QString greater) { - return CModEntry::compareVersions(lesser, greater); + return CModVersion::fromString(lesser.toStdString()) < CModVersion::fromString(greater.toStdString()); }); std::reverse(versions.begin(), versions.end()); diff --git a/lib/modding/CModVersion.cpp b/lib/modding/CModVersion.cpp index 0624dc1e7..4f80274bd 100644 --- a/lib/modding/CModVersion.cpp +++ b/lib/modding/CModVersion.cpp @@ -60,4 +60,15 @@ bool CModVersion::isNull() const return major == 0 && minor == 0 && patch == 0; } +bool operator < (const CModVersion & lesser, const CModVersion & greater) +{ + if(lesser.major == greater.major) + { + if(lesser.minor == greater.minor) + return lesser.patch < greater.patch; + return lesser.minor < greater.minor; + } + return lesser.major < greater.major; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 6e43fb2b6..73c2e724b 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -42,4 +42,6 @@ struct DLL_LINKAGE CModVersion } }; +DLL_LINKAGE bool operator < (const CModVersion & lesser, const CModVersion & greater); + VCMI_LIB_NAMESPACE_END From e9ba1d73d1aeddea7ae6b076dd9da0dfe94e8e62 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 04:36:53 +0400 Subject: [PATCH 0281/1248] Support partial versions --- lib/modding/CModVersion.cpp | 29 ++++++++++++++++++++++++----- lib/modding/CModVersion.h | 8 +++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/lib/modding/CModVersion.cpp b/lib/modding/CModVersion.cpp index 4f80274bd..084010602 100644 --- a/lib/modding/CModVersion.cpp +++ b/lib/modding/CModVersion.cpp @@ -20,9 +20,9 @@ CModVersion CModVersion::GameVersion() CModVersion CModVersion::fromString(std::string from) { - int major = 0; - int minor = 0; - int patch = 0; + int major = Any; + int minor = Any; + int patch = Any; try { auto pointPos = from.find('.'); @@ -45,11 +45,29 @@ CModVersion CModVersion::fromString(std::string from) std::string CModVersion::toString() const { - return std::to_string(major) + '.' + std::to_string(minor) + '.' + std::to_string(patch); + std::string res; + if(major != Any) + { + res += std::to_string(major); + if(minor != Any) + { + res += '.' + std::to_string(minor); + if(patch != Any) + res += '.' + std::to_string(patch); + } + } + return res; } bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool checkPatch) const { + if(minor == Any || other.minor == Any) + checkMinor = false; + if(patch == Any || other.patch == Any) + checkPatch = false; + + assert(checkMinor || !checkPatch); + return (major == other.major && (!checkMinor || minor >= other.minor) && (!checkPatch || minor > other.minor || (minor == other.minor && patch >= other.patch))); @@ -57,11 +75,12 @@ bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool ch bool CModVersion::isNull() const { - return major == 0 && minor == 0 && patch == 0; + return major == Any; } bool operator < (const CModVersion & lesser, const CModVersion & greater) { + //specific is "greater" than non-specific, that's why do not check for Any value if(lesser.major == greater.major) { if(lesser.minor == greater.minor) diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 73c2e724b..15221dab3 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -20,9 +20,11 @@ VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE CModVersion { - int major = 0; - int minor = 0; - int patch = 0; + static const int Any = -1; + + int major = Any; + int minor = Any; + int patch = Any; CModVersion() = default; CModVersion(int mj, int mi, int p): major(mj), minor(mi), patch(p) {} From 15037b782f862c0eac692b9c64f836d1bbb4b593 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 04:39:32 +0400 Subject: [PATCH 0282/1248] Fix assert --- lib/modding/CModVersion.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modding/CModVersion.cpp b/lib/modding/CModVersion.cpp index 084010602..b44d51c6d 100644 --- a/lib/modding/CModVersion.cpp +++ b/lib/modding/CModVersion.cpp @@ -66,7 +66,7 @@ bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool ch if(patch == Any || other.patch == Any) checkPatch = false; - assert(checkMinor || !checkPatch); + assert(!checkPatch || (checkPatch && checkMinor)); return (major == other.major && (!checkMinor || minor >= other.minor) && From 8040cafb3d9687c3945cd5a8c26d5cedb4b3941e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 15:17:46 +0400 Subject: [PATCH 0283/1248] Code review tweaks --- lib/modding/CModVersion.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/modding/CModVersion.cpp b/lib/modding/CModVersion.cpp index b44d51c6d..3ed3640a8 100644 --- a/lib/modding/CModVersion.cpp +++ b/lib/modding/CModVersion.cpp @@ -61,16 +61,14 @@ std::string CModVersion::toString() const bool CModVersion::compatible(const CModVersion & other, bool checkMinor, bool checkPatch) const { - if(minor == Any || other.minor == Any) - checkMinor = false; - if(patch == Any || other.patch == Any) - checkPatch = false; + bool doCheckMinor = checkMinor && minor != Any && other.minor != Any; + bool doCheckPatch = checkPatch && patch != Any && other.patch != Any; - assert(!checkPatch || (checkPatch && checkMinor)); + assert(!doCheckPatch || (doCheckPatch && doCheckMinor)); return (major == other.major && - (!checkMinor || minor >= other.minor) && - (!checkPatch || minor > other.minor || (minor == other.minor && patch >= other.patch))); + (!doCheckMinor || minor >= other.minor) && + (!doCheckPatch || minor > other.minor || (minor == other.minor && patch >= other.patch))); } bool CModVersion::isNull() const From 5875dc6b8f031cd3b1f7fff07ef9863730967604 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 17:40:52 +0400 Subject: [PATCH 0284/1248] Smarter double-click --- launcher/modManager/cmodlistview_moc.cpp | 31 ++++++++++++++++++------ 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index cfab9c50a..236627ef7 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -893,29 +893,46 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) { if(!index.isValid()) return; - - auto mod = modModel->getMod(index.data(ModRoles::ModNameRole).toString()); - - if(mod.isAvailable() && !mod.getName().contains('.')) + + auto modName = index.data(ModRoles::ModNameRole).toString(); + auto mod = modModel->getMod(modName); + + bool hasInvalidDeps = !findInvalidDependencies(modName).empty(); + bool hasBlockingMods = !findBlockingMods(modName).empty(); + bool hasDependentMods = !findDependentMods(modName, true).empty(); + + if(!hasInvalidDeps && mod.isAvailable() && !mod.getName().contains('.')) { on_installButton_clicked(); return; } - if(mod.isUpdateable()) + if(!hasInvalidDeps && !hasDependentMods && mod.isUpdateable() && index.column() == ModFields::STATUS_UPDATE) { on_updateButton_clicked(); + return; + } + + if(index.column() == ModFields::NAME) + { + if(ui->allModsView->isExpanded(index)) + ui->allModsView->collapse(index); + else + ui->allModsView->expand(index); + + return; } - if(mod.isDisabled()) + if(!hasBlockingMods && !hasInvalidDeps && mod.isDisabled()) { on_enableButton_clicked(); return; } - if(mod.isEnabled()) + if(!hasDependentMods && !mod.isEssential() && mod.isEnabled()) { on_disableButton_clicked(); + return; } } From 3ee91ca9dc303bdcdf0014d1c1d2db4172002826 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 1 Sep 2023 18:43:26 +0400 Subject: [PATCH 0285/1248] Support local and download mod size --- launcher/modManager/cdownloadmanager_moc.cpp | 11 +++++----- launcher/modManager/cdownloadmanager_moc.h | 4 ++-- launcher/modManager/cmodlistview_moc.cpp | 17 +++++++++------- launcher/modManager/cmodlistview_moc.h | 2 +- launcher/modManager/cmodmanager.cpp | 21 ++++++++++++++------ 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index a190bf757..20d06881b 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -18,13 +18,13 @@ CDownloadManager::CDownloadManager() SLOT(downloadFinished(QNetworkReply *))); } -void CDownloadManager::downloadFile(const QUrl & url, const QString & file) +void CDownloadManager::downloadFile(const QUrl & url, const QString & file, qint64 bytesTotal) { QNetworkRequest request(url); FileEntry entry; entry.file.reset(new QFile(CLauncherDirs::get().downloadsPath() + '/' + file)); entry.bytesReceived = 0; - entry.totalSize = 0; + entry.totalSize = bytesTotal; entry.filename = file; if(entry.file->open(QIODevice::WriteOnly | QIODevice::Truncate)) @@ -79,7 +79,7 @@ void CDownloadManager::downloadFinished(QNetworkReply * reply) break; } } - downloadFile(qurl, filename); + downloadFile(qurl, filename, file.totalSize); return; } @@ -131,7 +131,8 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte entry.file->write(entry.reply->readAll()); entry.bytesReceived = bytesReceived; - entry.totalSize = bytesTotal; + if(bytesTotal) + entry.totalSize = bytesTotal; quint64 total = 0; for(auto & entry : currentDownloads) @@ -144,7 +145,7 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte emit downloadProgress(received, total); } -bool CDownloadManager::downloadInProgress(const QUrl & url) +bool CDownloadManager::downloadInProgress(const QUrl & url) const { for(auto & entry : currentDownloads) { diff --git a/launcher/modManager/cdownloadmanager_moc.h b/launcher/modManager/cdownloadmanager_moc.h index 6d33ba8b3..8c0e8d664 100644 --- a/launcher/modManager/cdownloadmanager_moc.h +++ b/launcher/modManager/cdownloadmanager_moc.h @@ -48,10 +48,10 @@ public: // returns true if download with such URL is in progress/queued // FIXME: not sure what's right place for "mod download in progress" check - bool downloadInProgress(const QUrl & url); + bool downloadInProgress(const QUrl & url) const; // returns network reply so caller can connect to required signals - void downloadFile(const QUrl & url, const QString & file); + void downloadFile(const QUrl & url, const QString & file, qint64 bytesTotal = 0); public slots: void downloadFinished(QNetworkReply * reply); diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 7ecf53ab3..8dd7549ce 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -243,8 +243,11 @@ QString CModListView::genModInfoText(CModEntry & mod) result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); - if(mod.getValue("size").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble()), lineTemplate.arg(tr("Download size"))); + if(mod.getValue("localSize").isValid()) + result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSize").toDouble()), lineTemplate.arg(tr("Size"))); + if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("size").isValid()) + result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble() * 1024.0), lineTemplate.arg(tr("Download size"))); + result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); if(mod.getValue("licenseURL").isValid()) @@ -535,7 +538,7 @@ void CModListView::on_updateButton_clicked() auto mod = modModel->getMod(name); // update required mod, install missing (can be new dependency) if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); } } @@ -565,11 +568,11 @@ void CModListView::on_installButton_clicked() { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); } } -void CModListView::downloadFile(QString file, QString url, QString description) +void CModListView::downloadFile(QString file, QString url, QString description, qint64 size) { if(!dlManager) { @@ -591,7 +594,7 @@ void CModListView::downloadFile(QString file, QString url, QString description) ui->progressBar->setFormat(progressBarFormat); } - dlManager->downloadFile(QUrl(url), file); + dlManager->downloadFile(QUrl(url), file, size); } void CModListView::downloadProgress(qint64 current, qint64 max) @@ -855,7 +858,7 @@ void CModListView::doInstallMod(const QString & modName) { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods"); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); } } diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index e6ede36c1..4cfd179a7 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -51,7 +51,7 @@ class CModListView : public QWidget // find mods that depend on this one QStringList findDependentMods(QString mod, bool excludeDisabled); - void downloadFile(QString file, QString url, QString description); + void downloadFile(QString file, QString url, QString description, qint64 size = 0); void installMods(QStringList archives); void installFiles(QStringList mods); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 27824b826..b85c363d2 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -87,15 +87,24 @@ void CModManager::loadMods() ResourceID resID(CModInfo::getModFile(modname)); if(CResourceHandler::get()->existsResource(resID)) { - boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); - auto mod = JsonUtils::JsonFromFile(pathToQString(name)); - if(!name.is_absolute()) + //calculate mod size + qint64 total = 0; + ResourceID resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); + if(CResourceHandler::get()->existsResource(resDir)) { - auto json = JsonUtils::toJson(mod); - json["storedLocaly"].Bool() = true; - mod = JsonUtils::toVariant(json); + for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next()) + total += iter.fileInfo().size(); + total /= 1024; //to Kb } + boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); + auto mod = JsonUtils::JsonFromFile(pathToQString(name)); + auto json = JsonUtils::toJson(mod); + json["localSize"].Integer() = total; + if(!name.is_absolute()) + json["storedLocaly"].Bool() = true; + + mod = JsonUtils::toVariant(json); localMods.insert(QString::fromUtf8(modname.c_str()).toLower(), mod); } } From d2f0a8057332e84326988c9e3b59a25d596aab25 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 1 Sep 2023 21:36:32 +0200 Subject: [PATCH 0286/1248] Update VCMI icon (#2672) * Update VCMI icon Fixes #1993 Sources will be added to https://github.com/vcmi/vcmi-assets * Use entire canvas height for shield, add remaining icons * Use a slighty more saturated brown * Align VCMI letters to pixel grid for 32px icons * Align VCMI letters to pixel grid for 48px icon * Align VCMI letters to pixel grid for 64px icon * Add 96px icon to .ico * Update Android and iOS icons * Install 22px icon on Linux * Update macOS .icns file * Add more icons * Update iOS icons * Add more sizes to .ico files * Don't use .png inside .ico * Only include resolutions up to 256px in .ico files * Center map editor icon --- Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png | Bin 2053 -> 1706 bytes .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 6879 -> 7365 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 4089 -> 3886 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 9423 -> 11063 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 15928 -> 19986 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 22297 -> 30231 bytes client/CMakeLists.txt | 1 + client/icons/vcmiclient.1024x1024.png | Bin 244834 -> 405747 bytes client/icons/vcmiclient.128x128.png | Bin 13793 -> 16246 bytes client/icons/vcmiclient.16x16.png | Bin 907 -> 703 bytes client/icons/vcmiclient.2048x2048.png | Bin 677279 -> 1123397 bytes client/icons/vcmiclient.22x22.png | Bin 0 -> 1069 bytes client/icons/vcmiclient.256x256.png | Bin 31477 -> 46588 bytes client/icons/vcmiclient.32x32.png | Bin 2381 -> 2255 bytes client/icons/vcmiclient.48x48.png | Bin 4089 -> 3886 bytes client/icons/vcmiclient.512x512.png | Bin 86025 -> 138300 bytes client/icons/vcmiclient.64x64.png | Bin 5919 -> 5579 bytes client/icons/vcmiclient.svg | 460 ++++++------------ client/icons/vcmiclient.xpm | 238 --------- .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 1335 -> 893 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 3599 -> 2547 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 6294 -> 4722 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 2310 -> 1969 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 6022 -> 5544 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 10581 -> 9696 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 3599 -> 3170 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 9270 -> 8437 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 17311 -> 15254 bytes .../AppIcon.appiconset/Icon-App-60x60@1x.png | Bin 0 -> 5738 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 17311 -> 15254 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 32216 -> 28160 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 8740 -> 8050 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 24617 -> 22668 bytes .../Icon-App-83.5x83.5@2x.png | Bin 28669 -> 24879 bytes client/ios/vcmi_logo.png | Bin 23326 -> 32594 bytes client/vcmi.ico | Bin 1059029 -> 410222 bytes launcher/VCMI_launcher.ico | Bin 32038 -> 410222 bytes launcher/icons/menu-game.png | Bin 47461 -> 46588 bytes mapeditor/icons/menu-game.png | Bin 5482 -> 5579 bytes osx/vcmi.icns | Bin 598147 -> 613853 bytes 40 files changed, 163 insertions(+), 536 deletions(-) create mode 100644 client/icons/vcmiclient.22x22.png delete mode 100644 client/icons/vcmiclient.xpm create mode 100644 client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png diff --git a/Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png b/Mods/vcmi/Sprites/mapFormatIcons/vcmi1.png index b575cf133b307fa2c9d4bae8f608aca4c3379f67..a2d7b11263147b03a26ce56635838bffc2cc979a 100644 GIT binary patch delta 1646 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 diff --git a/android/vcmi-app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/vcmi-app/src/main/res/mipmap-hdpi/ic_launcher.png index 0112f05838dd89acc8ff1c41d820e6ea45a84c8b..4adf6a8e40e8aba8f98d4634038bc6e5dff7f579 100644 GIT binary patch delta 7351 zcmWlebzD?k5QgcF6_H%JLs*bT8l*!6q&t@GjtfXgcbA}aNi5w6D}ASlQW8s^_cffQX1lh7izOGe{4klVtT@h8`M3FND(g_;p)m^5pu$Q z!AGlTPMwtFb~tWWg%^+8eGAv`}Xa^P2%?zDbwzSxzv{}Shy+j z3R9~m*TA+g(0g+7jnvR_jjO12E#Tk}Z^&8PLV>F&l@!)VN`vpo+5?Mf=D+XWtGz27 z-j2V5MxQ2JVMyOy9vOZ1JNsqU_2(PcIez58w&6i^1O$AId4d{xdHCZ+Z+-m}a@~`py{{?x z?Ozv*j7uNv^9M zi2IzGnD&z}ZN^f0#{>xD1DWg!URMZ`#ow@`2Pc$??ibJLd;^7rG`62j5m-d;w9(q4@{-NKv#OBP3M=1XHu zX_Q6R!xZ=vplg}-col_RmnTc=b*0W41s&bv&E?TDFC{H)S#NLA)YR*sbww52P#Xgr z23Wj7L+Xe%MmAIFKYWhuJfl{Z``IyJU_C#G+!`siOiU{K-k4`$Q)JwBw9!RdW%c@U z*;V*ty|*YclZ>bLpGVNT-9nwo;aWF!DQT9m2kz0)QG9&7R;`{gGfBgUO_PB}zDU?X z2Zl_j^YUmoz2BmHetpoP6z~3pw$A^3r9d0r<@GKWZ zUDvw&8nsn|*30#(U)Snc5yD+uhR{o2mjvgf3{G@aY9`Mm=oR-1DtK?zbTh%ihD3qv&xzR-1Awmsh& z)7QJ^TyEL>{d=P%j_219x!G3F-SNlD%9v%>-hxry-u!aY8^y1LX_P;d5B|lmH($QN z7bx5vlCMjbbxg1^;0BCZ;z6B!+vYFawOAG^vwuYkd&n*uxmYmMtkn}|T9dFMidDLp zES76-ZJk+NO_&$c^Q|2md3m#y7N3|1Dyl-S8hn*DVhx_l&PM;cm#2N`P*IPFINqM@ z46e7+3k%3p7FF1DE;Z7OB+)bK9rDDVy!vv*h-j7PTMWR&`J^E-m(t#b36^OCe!&l? z%$OM5rKKhPE+a&TiSsTk0 zbkuM6V2P0rmk7K(U9?Ja|2@fYdw17kcDw`cKlAkw2OAFiZG`sgDHGvl0JjP&|vaP+5Pth21| z^HHQLILgEosxsM%QEWEin4b+61C~NF4StyTJo;nj6H=h$nErJ2oD>CyhSR@yUTfrz zic0i6A2ZVL4iE&@9;eQlqGX=C1MN&wBoRxb5{l-OSkF1$MV>cq?VluTgPEJ1o&7<)xq!al^CobWQx=oXPsBN4onJ|wmZ1{4U>cYp7fs}TG4~csDy?1xCXELZ zCatN7a#BJBYi_SSg?>~dJ%;*6xsq2*&C*Phj5vacx3ZL2EjB}E>5y<8P6fu4|38gS z+U}D~j@r(cY;oV|DJ<~v&;ky}K^f)Wg(F7{J;l--Fzk;3i1)zMPr#5N1CLzB{*yL_ zHUmQ5kGF;1Fz*E zi*v{O)d3t+w)0h+QV566Jp0OoC4j;wDOq?GOow!bySgT^Jv-C*BKkzQ;NO)}lZFt2 zz0C`IAN)A7WR3(yU9p**BMtLEk99?7(JAFo1lFn?%%z&Z&EoGh0j;2SlYYHtl zlsnm>O$J?33#017G1n~kVs2+-L^%;ED*NWmQyPGV22}0D$h64!KmpYVLj}Y7 zuV=cAuebLQZmkY|hK#O5#1j(jFP^e+0(2;TYLxVQ2xVnIv49{h-pG`3;Z&u0qOvaM zGtP`y6sS4vB!R#VEeSiiKZCrwV{NS(tLM*uH|KOyhDFBT(hGO*{oLG6i+g%{q*}QT z_5tVR7H_{ov9T8CCOo(Z8oGVi=b84FYxt3-%5U zculb=M0=*6%8pNpr(lG|d$@nNeX161D|W(23EI@vl4UT$f#6@FoE|=}jTg2v-kLI` zjbuyv>`j-h3leO~?)>>YZAeTj>ait-Vg-aYhttD_)jjvd@)oALVbu8Nn?Oj|&!1X|gU9J(g*+)YO2cS}md8mLZ`VK8Z4b)Hwz|>9MGD14C_~YNJJOljvYWWS!c*-X z87cvR#%ITM$W2gTBtCgb^D7+uEQ|x*13PA=#AVS@83|5$nZ_ELCTG<*W?*XN zyE^sVLF@P2vt{%*P-tO)BllA{pStJv9|h;&>P%}{JULG{c+~y7naU1Z%FyQKd9uFw zBqz&N8!k(BGrt>Q0^%eiX*3wG{`^?h!`9$Mh3*-+g&1V#e~iIyo61x1WU9(yQJeFaytiXL)kM?8g_3)`w8yosk@ID50x?JWfEPl;NC@{`~;>RgVKh%Qh4Q@Efg15oc`q2FH zyj2dC3f6UqbnVVO>eN{)FwY=ukF*`b#J{dDx{$lkqQId2an8NVcT~ju&ys4lDN*$h3UpHcT)#O4YlezRx=JZeILtmh z%&e$Fo{O?m1LwT?hUWRo^@E^1*ZFKy=gqfwQsTOTVLN5aNx&T~(9vj5=A=Ko7(@C0 zAHxz1LbSC@tRWg2W4ZXDc}`a0LQcX-{7%+Yq9HA*GEU#7!mHBSh^19yVsG6IBNqy> z=xK02ZXXU21DogZ@jKVQ%O7HQX?y8LNh|2zqw4UhQG6`ag9nHS4Qh1{hPuy`sW&4K z76xQ>2aUcbrmVnl)UG5i&i({$jPUdacqF1wFzYe!kt{tm=;*Pl|Dp99j!DgH+9%Ok zgF^c4YL~~S09!L*hnidh<1cAw1cbKl-t10sx)KBMTE>#}7!8ecosjew<)BU{$-a>0`@T$Xvw)h3dKK)-`?$?CIwD|D zY+SCK{4ug{yJ`lW4Eg1UFub2~T@y=HFQ1{VTmAekMo;e>j8Sho>1OlVuMk~v`1j$V z^-%0H9Dw1M`D;gs-KF1=Bi1CNjlZbQz-B5tCnv`7#p^NnUM%a%{Y2`hutuMvszT{6 z52t34=zoa$3iemiRFr-VnS}9~16Xn&tPD{gvZPdk?52}grUmF)l{#XEoEZ-S@;e%G2&jrP@IU_!C;qPe1IN*0}I!V|mvS{L+$hOmH(i_-AmXhJ!Acsx_7F72}>)F1TW`R-Zc*@6DUO0nvq5Tes7| zAn>i&HgCrZZxD9IqQs$vms+O4nL=NnHf_z^zAbex_%xElHF%rr12aG4p4ehl|MOl~ zUFv%i4wTIAzWewbo5>28lHZCB1_%c!^5*Di{(Qhw1+&=zMb)u35)vCwEWDN5Ve*Y$ z?UXU0N>g7&Sy2(Y)8}ZQ#TjfZ2Vj6Dp~GLV4`(Y>p9o(rC~%78XAzL8e?oghVX=x7 zP%;wA8(v_(SpR!_u2CoOGE4sySkLRuiXmy7hZASIm+!&&4#x5)a=7BGytc4g0 zzGvy?pWtpQ>WzLf_){L8+Ma$TU#g~FXCkbrtSixZSYA9U!lskL=tD2+NOp=7WSgff zWIvxwV`}nDK`pk~f8{DpvFXlFUyhquoAyxSScgEYFz zYJg_3#)0yeN>0DC_)b-pjfo}%`eLY6**{v=mKISpH~R}w3P-)tKnIOh+e zJLZXNHc+MX#D}!Hq!VbNo|TL_I4!~m!s!JXq`#OF^qqGVzp=PJVnS0wuY)jNmm|%# zi)eui)wdU?Zvh`8XiCUsk0OQCYc$&f@zvE;_rdX&BbYNh-Z>q>z+h^W;La6@XWe<# zl~P4L>_nqHLa%|zDCq@dd&o=u-T|-OmzSg_7-n9D#SZ-{)@RZf6zwIX$5eY3H}uF; z+V!Em-M@O?p*@wbth89W^3Vgi8+miS3-q6NwbgHTxGb_IsdMN5opbDLv)sOD!gY{K zr?oe-mUQ2s{N4g24x&saiI%y$53uV`%*}UD-t?ID))iZPlEHbRrl$7A#WY_{tej&^ z-191iWag5vH_Dk2L|DrC3Bzk<+mRYT*z!IJ~01wUwL!<0go9x#dS7 zJeBwH!V|s`<=wsVxegnN=Qy&2(_2|iqQap=|HIon776$yZeafuFCoN*q_Ht{_C1~!i7bW%lvF^z7>^-> zI30954_iM6qowQ&$Uq+!B&jKsw|(uN6?_-1BrKR_mQyhH(d!*s49WQI#8BgCLjylP z%}A^skhgY>*`y#;YxyD`Wq*I)@@NU+E>qXz#$Kw#u3)bI)1P+R!(XGJPV&MKUHbOw zM^tbl`Ame3qNtd`)(LH}#4Ci(*%Mie(8Gg+z?#m^JI^_X49f;3umx^vN?Thq4S8r{ zlUDVi`~JGB?d;y7a7JGQ|MAgp9IUW&tGFG2A@=_>H=R`v_$77f+<}0G>eVxb# zcuhwKg#B4ZtH2|O*BB>o0UXxz18V1Q**9ok`ne%>-f zppp3c(`pk+2}MGho{fnLnYlu!aQItf;;g391>i0!Z{}EOXlMu~+6bPNV789f&T#90 z4s{ZWbB<2vugKZ+r%g*s`(7kfM33VJjUMr+Xzw`zZJ6e)!w?-RW?!cEFt446=4}sAmp=+L z%hvoOUH<343hE6HL<`nv#C_;Xtxwjv&scGL3#*rN&&@?Z<*jI(4`?NMG$G6$G93dSPp%S zBBbu2KGKEK(Xkax%z)-t!I9TySpD^7KVi_zJ%z>jk4q>zi+mU8F?32V_pB($2AY43 zEVm?BP%O5nZ$V0En7k8`gwu{!+SBUlc&7IR-Wt1Og4h78zuCgBv~gt5Z}U!qPXZB2 z54Xv^Hi`VXoA#(;X;bs_ml+oa3m^*PFSAcoVm=Kn0wa~)vbL>8`r((VLm2t-G)nq3 zhBgU%q)j$+jR8wtfS~mTvUASdXCYXJC#IQWNw*AP3x~7!gjicgfM_Lkgt>6n{fA9f zf_-i84RBJhDUMcAU+?O4e|teJUoZS`6gC6CiFcDd<9XJ{(oEouUrBG14p(!1Ox?Bl$c=z@6=f3TwPBddk*S4 z$7>jX78EqNy}iHrU=$(tFcN9ikC?A9Zchas;)LC}?X)iidUX84lFHHkNtdJ#6Lw8@ z?c>(55X5)f$cM@s{bT&V7}`wKO5J<8)LX}mDx4h3*848Ug}#sw!iVVwzLf2rGr5SO||-hNpy-y|7r6FD)t~2x${(pU5v)H;5l=^B0}LZ zpvwJ^%Ph_(9Quj#|7nVwTQXRnE>G4cPEXwqrvd^3yfif@$feG6Ch~t2cBTy*h<h4h3!nxZUUpd6o2MbV{sWeAPD+ft2?@P~L zJqW;+F*H8r_!laIya|~I(A4NRg!lK$fSLd)kui^AgTD1@hj)_ekH+mvU$NFEpX?!l zu?aK(?Tq@vA5BPmS68+SmDfC32Aw|7XKw6G{df3sQw7BcDajTD(z~e=bELDiE(TWE zm%7*65W-akitZDC{|bov9)AK6s&=J3sQetYsGJ;15aTl>zEqkiCALRaugnbx_MVR18~7K!j_B_=JRZ%9{{;Dx}) z-`uZA@o^a{<{Z@2)zAKn@*>5NAijw);24S|H?0sC7nka_8O@w(;;U~8Fg>iQuDiYL z@IH88tXfk53Twe@@VW5%*G8#plsiStb``;us~SOVu5>JpXR@L8hVD-2O<~0c9a|ov zAnH@E)M3mU1Nn(MIzCyd6;Nqw&0SC2LnWNj7=;-LacKnnPkSPlhm-ZvMmo~Z3 zm2(2sr6A~wzF~P>be6$c-d{x(Dx#8-rzHNulG#nZ?3qAbtFV|jVG z)yK;V`hDBu>`Ca}o_+JYQ#-Kz&e-n)TQpOXU$2V)D6=AfC(7bW%IWXl>`Y8d1cby* zAVyRv)rMLbWDdnVWoz3?Rc$c??k{#Rpd!fV0N3{I%r58UR%`y8GY5o|JQz2=Es^o)4pdnu; HYaad|BBoBT delta 6861 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 DKce3x diff --git a/android/vcmi-app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/vcmi-app/src/main/res/mipmap-xhdpi/ic_launcher.png index 308b6da2800d274f2541db9b95f080adfb953296..46c1851ea59be90a680b878eced195cc6fc3dc3e 100644 GIT binary patch literal 11063 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 z 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 literal 15928 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 Rf4TJyP#R|z)@{Xd8qu`>Vw diff --git a/android/vcmi-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/vcmi-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index ecbf76a7aaea2295f97d1635495dc4238bf77227..5a63d09d632e51403dca821ada0762d81267dadb 100644 GIT binary patch literal 30231 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 JTPAH9@_*URe$4;? literal 22297 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 JwaLu~{|EOd_JIHZ diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 3fd04a848..9e851487d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -462,6 +462,7 @@ endif() if(NOT WIN32 AND NOT APPLE AND NOT ANDROID) #FIXME: move to client makefile? install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.16x16.png" DESTINATION share/icons/hicolor/16x16/apps RENAME vcmiclient.png) + install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.22x22.png" DESTINATION share/icons/hicolor/22x22/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.32x32.png" DESTINATION share/icons/hicolor/32x32/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.48x48.png" DESTINATION share/icons/hicolor/48x48/apps RENAME vcmiclient.png) install(FILES "${CMAKE_SOURCE_DIR}/client/icons/vcmiclient.64x64.png" DESTINATION share/icons/hicolor/64x64/apps RENAME vcmiclient.png) diff --git a/client/icons/vcmiclient.1024x1024.png b/client/icons/vcmiclient.1024x1024.png index 3d5d93ad8578e2d470e47a3913f8c01ac96ec804..d21f276ef3dbd8b564e882e7339607c3c96acbec 100644 GIT binary patch literal 405747 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

8h+cTY7W2eW9i*+_qLr(Qq;p4G( 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"m(YG_#5ylASWk`C% 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

TdZ*!FE8ecIx(+Uyk{gLq-|p= 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

G56Xsj#nBTE zNyG^Um~@{0>wHM7NOm~{`oJMHMmw6vn#sh^Ethpc!C|;q0GazDiIPQ8@KNIv^XD?s ztA?#@ARMHsF=wXwyP9T5TzkCOrKx5}q*2gH=4rTP*f7)7gF6N8JZ(JS0aTpW;op;W z$D=%Fy@jvTD{ffr%~S<(8dx?2#oisAj2Kc z%)-jajzOP6_}%^kaR;)uZ8sgW>eS0|V6uf;*aSsW3oBaMY1eBTAOFoe3(sYo;V&O* zZ$EI+uY3Je*-zE2zB_eC+_Kqkz092#sG;cDX%MB1kh~aiA+*s&Qtvo-6pjD*k)Wko z=Ta=9PvhZQ(I9zIqsy6#?FB$KyhmUp6>nn3Q!J94%`lrg6$Gx9+)aVuC)PMp%z%;R z?CAJ}r48S`bRJ~EA!)hdQUCM|Ww^Rm+^Q+T)~c97NcVy3HCL!Kxffh2MT2VU(fQ5C zYk?uzVPV^E8V*L;7@et_4e7#8g&#-WFWok5^%cqdxRbAzKbxseax$t$9ITGk9VNGA(LS zTx;YiX!kxVey6ZReZazd4t>j4JaU176#$nS4L#|VSX3_J>$K`FDf>~{vi0J^mwdx9 zBQy3rocHlk#Rri-{xDBo)(hIWUu!8Ckun@s&s-m4tC))2!G}^CV9<|xzgxk<2-dgQKqFSK z6tKXCs}#}~#Lm|jdmb~@{>KODD#rzeWp~e}7$Lod+hE#BL_9>f7NK(>##z-%yqF%t zI`ITa`Jf89>gbot(HttmYi1nu*8GJb5B%VOWq=OQ`=3Jfk!EW$AtKD02AAt+gB{_< z7VOzyxTzC3u-fI9&=l{d5+-O5Hz(y4h^!vQ09FWZ(;I>7~c6z+^lfb>ayqrN@@Z*GJW$^4eK1xCe%$ElIT7M`ZQS<%7 zvftD*+YaN*-+(+hJS5fYBBo8Qzq}G95go2KSo9W;aI)m?Ckd$bRFG4QsqU+5oHtG7 zxmgoGKSre+HHR7vrz2^3bsp=XEV3Dh?{xg74i8rmtf&VMe9|WXHyw#FBCpxgbqH~o zy#~CcATGYm-r{W0Anun7ygJy4)W47Lq^d{I@!E|;z)+Nyy@ zT&vFu-WgfFMi+d+R!v0tYGdzKV);x_@LVuyeS}Nh5Lov?71d*t$z47BJFxYiiTEjt z6(F0|OyM71mJKDJe7qalBehvR52911n&Qpb>ygZ{{)qN9uBmJA zteBXHQU{Pr$vC&9eLf}*XLX|MJK4w1PxqZAAIXSH079peMaFjGN?PP6%?ZtCDMWB+M>W>vnDDdCIL zU)}`#VOux_8_GH@sfGzg|FmZuqa<_#tOYLo}7UmAS> zRY#jM&{J3=DMyi+g!f?4KhR&@rpi~MCG{bQWQB=IQq^PmxV}x7gvdae^Bs~r@Uc4I;^esf(jyMI8#i}1qGFcUB!Va@2&1JA0=8I zN?i?j0Jji7-Oe<0RyPk(m>-o{yf`-kP1jJk8aB!Z3>{m{4X3?l=?D%u>M1$8UG`P# z^IT|uRTV1z=!THHXzW<+iq_Y$o4VW+d76Ezh%3cjZB4@Y(0l2pQmbe0jf3lJ9uTMe z7)Ol8(?KTAQ#F7&w2%VIK+X2ydeZ%KEq}kmg3fdrf9{VxrHDREzR!xBV6M^-Y+(*$ zsgkv%^*qVxLHIWL7wiCP2a4ks#Me+M0tV0`54>Ee9!=S~NlkwlU+ zow81gJLKPDYdr8M9mi8JR$CGiBe^S|=oU$g-&&wY@f5xmOq~_?Ag#>TWQKeCE)}Vu zK<7+zx$}KzeLS&It3G&baKl7g$+0@B0*2ND@z$usn6<-tg}zbBR@>%AKQgT@NO0nL z)f*=?|3es!bz1j#ph}-^hR$K)I9rrn*Pmx7b(eZT*Y~oo2p=O3$%f*L*B)uii)R?K zUEnwE@2~ViOYih31eF>37C@M{2xZH22Tk!@i;E^geGVS{Zc({V@bbSa0zaY+$0L>K zGaqqh`)a5H*$UYIj09w5O~t1t>rV`3VF_a^*PknPqR0`d2vO(4z$hgF5mDN>iw-B? zj~OtoSJ+9K5A=NI?hov+R-qxiGx1UdXvJU;lMBvyp)5=0>(^FWaX%lVR99<*-(#*0xO-EgSv>d2qO>r2)}?j zKTkeYW4oS7cLD>)1lqn?vqmb7hOti?v_qTkA@o_Lew!bB>ofe;4I3}nhyq5z=LDcj zb)wEOF6)I9muW+sb@1n%brp7(R|A3j`v=JZ$jqkx{{F=GcJmE!q@nwv36yKJ$-PWI zvx>=oas*4k=Ei4d_jd~5Z@a*M7n4+#c|Lcs{1AA0nK>fKr-lyrn|U_-w}EtqvHh=l zMBSx2Qucd6Mp~`-c8g5=8wKlE*r|imdTPH2mY>e5tm{{HlhUHJ7lmK-E4a<7_E(p- zX45miX>y7Y0z4|y)Rx~})lMa%Ip#AgSdaSVQJ#B z=zr_Kz3Lt?OEr=zM={wcAr9NZHz&gXqW}y31#gImW1S?y(p!T^71q_Cs?_x}f=7(& zXj8W~Khr)xktj^%J0XX$Y;`iS=1Y3=qAqseGc`D`UqUi}mYGPE94^~|zC8*KgT8k} z(AOQi4{)3QQt*2J{(ZOW`0FIasc3mIHB>flQ?M7jK z*vU-1WMVO&fluR4qwR8;+F=u<@j~UFVn<>WDytoQhLEf3R1(I>hUYmm@D8_XNez;H zS}6_*q?VzA4qrln3(jYB3Sem@c4%s>Ah0+9&wfDsUDE9!C$%5^?_Nhs6*Y!*BwcTQe}gV|;Uq$s z>tua2su38x&6G|MHoa@_ZFV*{HzBGjMn=OGHU3?U{%Y@D9)!rsswAvyl&m@!so(J96k>{kS3$ zdrn5ht;Apm=pKm?`F++9&Xq=Bg$L&oKqn#fDHe%%yAgEv+N1Ah8zG%@=;Wh6NgT2v zoxe6#uR4sZJ%1DQG^NoM{S;PyqfM`i#)xoo9EPMEzgtz3?NHPhgte{u4nHLzx3|Zh zV1E4yX>dPQ;jf$#E|rKB@PZ`{}t57AaYRsyBVQ(YF6QoekXz|sWn||xuOr4 zxE-gvlvdV6c`mk!8@B_hC0|4_>IUQnQsi?4k7izbNJt8H zCN}nG!oYn#{U`;|ImQtnd>MCQ`YFG!UnAle+QS!x*@V^8R|>=n_U(`>DCbRQqpRuP4rBJsGGH@+5ZtixA&b_P(%bS*ZLu`H^3@_k>sA#NO znJkI-ce9z?1G9Ww>K8_LCSwA4pr(XEFjaJZYxbF1Tg?j%Gi#JD?uV_*IK7*t-`^Ln zqd%gJk0p>>hM4hhF{k;=(JMw#kEi(a4Z5dNy3^-{JkQ6etF&f$E^rqWsL!sQw4Rn3A~85thu zU@Fl}dmHIta)c0zTX1iO_dUa1s}J3ih$zxnUzx7KA+434z{G2SULk$uH=z{!k{f|m zy7m41QXRmpq!f$OdC)>Hn-=+5nH)tbs-pFa@b2u6T`Ulg4l81`h?)aef}-jy?(K1j z2hzrEd*7qK(!I)W?L^bpNO(7kXIR4It{f;OHtOusJgwajyB)+d%Z1j z5gM?cMG;o&6+nAojf1(|_OQci!rZl4e%dT3KA&O()O^-zZ=3S6fF&(OW(QKw^!&i@p$y7X+BIKjqcNX<>+y+R6o;(g{4`xUv~Cvk`W zBlq}C{zvYSb0rbJ<=}@juG8+e5=@D@b|Oaglvl#MF6w#C@C_;0_(30~4!S2^Y;QD3 zB%GRAbJ`7w5gZAcYR6)(%*vA|USzH#qx`8*`&0T1frn_q_)$ ztsN`>!&6-Bt;&Q)S)hZfpFEI4*2N1Y-C;{FNPJbz#le<->(=0#v>l#w)2Zc9uU1dq z$4#pWpl5`pa;{NL*`~VdCo*W)qx1g6>*|M(s6M7j%*^?-hWF?ZZ#}yVRtB8=rOa5L z3MLW<=ef%(wdne~y1HKK;N+)AN~wO>sut$3E#LBYRa+cf=Euo&E^Q>-9Ud9Owd-XY z(M759azK|VHv**pI3?^18OeMRb`w-95n-|o=5Ff7B3<7Pl_Hnyunn8z>SbdCg4&y` zbP+%+kGx0QiOIT-l@THorS#@a`BQ(Zo7(--NW~{z-du!NQwILj5Cll?%*BY-W;Zx39$Yg_1t?Xc~QRf8?BwEFhf=3t4*82gUg%ZTQ*0zT{%*YTAu%8OeH z!}&jW#Sp;M(M-ml6L^(#c_t@=hOF%{3XjidXRf8%r>TVk6UMyHus;2}o;nGGk=t8@lS^vA*?Ueuu~`m?IFRo=Gfu`n76UVX z;MvTm^uR*2bgH|a*Cu-ui`HmaxtYjLYkKv4XkesL^E8kc^wF_vCNR%XFmZ*CnRtoh>M~$l6tv#F_nSd zSuCP-b!er>@rAw0FNc-(mmLl|E`@o5vfs&!dV*720m*3M@1|=p=u#)x*viZya6B}M zBjv!jzXbfmHvkX@Vc)^E^fYW6u5x3^~_5!73YLuEztmRbr#>_)}&rO3F4CZn97_ z^4_3Y37*{xU0dw$vxuNZb0u;YyK>Q(`}Lidyz3cwn#!|all-F2%zryt6|sy1_X=Ix zXEU33g)F)?99EAYTQl`=AaYu`ox#QAJM5RdiaCwO7~1@{dyv zyH?s~abNf18GLu|QOuiqLsBnQT;*z8i|!SO?sn zEd7d_4!Wzp*2VS80a?mpO?t;Ky%2Cutqscyh1)fF^v1@;wxCrJ1JuQpPrA5@3NwA< z=`UN7{9onkP*kil{m`<I0*tSH!9($H}!@-ZsOIFpp;}i9= z+EX)OEuQYMKB?+YtyW8lJzM}@wvQbq>=(&&9!y9=;{7m>MffK|gxjX+ps6IM+`ubz zranQxE)}yzi{f9Pw-zYC00{(i$buMxJs*Y#Uw4*JYPfEVv$L~<1&mJ|z2XB5M$Z-1yQLqoc<+>w^R(_10rT2a7FOIb>pEr{ za8dza%;^0K7M-=#^YbvKH{u1?m;O!$-|(b9Jn5X;;7megR>|P(c8>xCpz|Bi(xG7I z+TgCw+q^8}9bN-d1XsI14xOEyS=w4t`C0(YBI_z5Y%|Rs#ss8bqhjM8uPJL??I@l& zxJ$+XDQ%q?lBh6S;SI5mr)dxXcNM6 zEnB^7Zh0X`Z{bi#ao)YgzeMr3quL!P(jK@#(tl*6T5d$t7s%QF7fOKDGI{>X#xFV? z0ridFGyM#hkm>2#TvV7%(cG|4RwhFWay?&L?|Qg#r`Q{gwzrek^6(XyIpp>nJ(=gJ zPD%uYUsZU`V-9`-B<0TV9YyrH+80HvVaKR;83{~luceQey15lQZ9c|z?jI2K=o=W$ zw-kao(i~thF1u-!fIPqO$(CZb?7(S!!qHu!7r5y-oQ?UdV9In0!W~Pu_7SUtjoN z;Y6*bDHyd?Otq5IazyjRTUFC_>Cik{8d&2t86*X0u{Uv1(ohmjm?iq=ZF^ksz;saS zpUn=38%(As5-nF!+BCz3!U8kAqigrN_C-BgIhLfM3K^8bFisCC3xD z0Z@_{!t|680N^?KZ@vXVDXnQlAqkABa^@&0~qCHQUk|naHIbEBe-guX_P3g^a?PI93C`E9}{}rr)Ksy{RMGM zQ3Rm)kgOv7vIAyj$|NDKIA8)BuAzFJG6?vl6lE z{{jBMID)ZWf!2^ue>3ipS--W+J(^oca)NvS!189C5goPmM9{;Rz%?J@W1pciUVCxq zG*S*)9!nK=0L-S*H7#xpT9)LdO~a zk>V_vtjTl)i<)CJUVQzIczj4^?}wzeH~kx6)$5Rgq@P`Pw-L)L&9sN3GdRHSUQd@i zl)=avV?7n1z{zy=v=;n?ItHqmo0}hwLk;s}!Y|;dn0;XQotpR83&AMJfC+84oQ41@vIY)T+KHRu$m8o;2Kl19CkbYR&qPFaZqb3Q!CF4cTUo0jD<)HV(hh!|P4xe@Qt zXX+IySZ-oV`zltk+4e?mA=k>h=Y`sb$rKA)yvLW6%Ri2zAQ#jiHdX1K1#t$AU$A8A zW2Tq4;jHO0ran7!^|kOT!1MHeZhzrKjqH&)Re$9pcC`CglzO=AfrreN2^<%tkv(VV zZ>cPOc88y3NMt|mOckECvvF;olbMa>toOq()d~Rn7>|$AtV_o`S-ipy4MrVG4Fg(a zr7Jbi?#LE#ymZIryzlbDt06TVI&i-Ni!}0@}E4oFr$vfp$7FH zJ)QA>hC_pfG1Y9^zi{7KWQanD;SuVGRXr&ST}(Fw7RP#(d^6%#@Xxr9B@p&OERX+Q z8pGequ=oGVT_b#?KY>W?=nMRp;uxeA?x)ih15xx~NNtU2xe5I$<41(hF%PW|e;tO^)T0}P#^kN!Lvg-go;Xf?@SlREQ zJm*33dXfJiE$se|6v5DtR3SS<0Z!8O;?{_EX<}^Xfb3bM1s|@aJ!^x@X zOfP%g7*cg~hyq^P!H~37H=Y?EQ6&?yil4W)iz!*5qs!SAaFPjglk$vP=rp6N{+If+ zsk$LqbTxEIL-?t2yqw6BAr)n$B3DQQ?16=ETYCK^=h$3E16yJha3L2f^eY+`7j$in z-wEbcdhj6{ew+XEzD(!D_tgyoqF!!;3 z{nKUO+d=U+4f2YIfk5@QJEME_sK(tGPPw+8@n^l|LV`sX0Wue9MjGmNbHP+QUS__D zL;_1jLb@nDKzH3Q*idbz?@tTy7B|s*C+pFLH|C>_-Fp#X%vKG~U zt;YOad}yq1Ljg!ho}p8_>2a{Ji$AxN?ERUgw#B~yL~3hkaQX=abVApBRc5(o$(M}Q zai!yB5!@|m9#cG-$e1(Jg*^OO7P;*J!H=dRxr5rb=SaDGv=>ot^7PH+H$9vz95tZJG4m;v2 zMs6QoLd~mm5fW8gTd1dR0ivhXZyQ*(v)o$tg$y*NOG%-}VC&euBG^kue!#$&yHIc` zQWyuuu)~c%RA#*(!w}ZOSiJ-6)xG*#_IYO}G?~5V{eAIosobI2TEoZu0ir;|h=_VM zZPAy}C&<$e*erbWqUhVhp+z0$Lu%cyyS5tsSi>z%+1<<8jtQB+aHFC$E5@sl7OFt3 zT#0_u1n~;;NC`$!*xJ(O?$r&J339!5R-))oe|8kY7#Z#{Eljozd^O_^}K+ zF5TE&3nnqn&t&$0?ToU6vvZvXEPPKRN~aBEvL%reiI}EHuP9eEFt0t!acHQ?^O)f+ zA1dW>zH{A5@3$hKu5&_kLp*)j&eyz*3S1$CR8~^u{=|QifxS{-5Q=H6D@Q&muAu z-mVRLu(@**7&LlbZU6rH-R?jod#zETRbI@+UW<0&0TAffMY32zrMzMXZ}$v~zyeEN zuQe>4g`Mqm<-&NDO(8{pwnp7+;@f46 zpaZa?`~3Z&#dg6oNb?vliw zrz}^1_HMzAd)h`pz^%%4o>%9yjhL$)(R8oC^`L+`b>{9l#fhxc5X92`(4LQ#0E`a` zoc%dea*8_1B`b+FUfY4co=r?FugDe9A9NvyA>^chJ)$W5iJ6jS1FtTCendQ zBCV@+yW@fof(2MWS5v%&&Dx6JhljhvOXGJo)@jUy_5p!kIED#VR=k9lo3luN=%BI1jC9hW?+`$`@6QRfo5DOjXFh zWcswmMpfxSK84F|_F5Hq+q`^Q*zVc6`h|iu6SW6S3!*+j!TqSJlQ=?CJU~Q{L-tbc z9~-R)So_ZqZ_=XE?0Kv)+i`usgXDW|Ys{+L($GEd#7*#LZsC)qe)w%L`cl!^#n=u6 z%RRx&5@Ev8dS3aL!-k~0K&3vPprf7vnc{c1)6KQQ ztOkz(iGw?i4{_@ns;di};=Xq02CoTqc5z7O`n(cb7kG;^XT!a)Rz(DQkR<5=@i_l5 zI)=LgY;N(%5jjUVyB{Ch6pZEFBX*y`e^QStGo?X*bZ%>u^xnzG+YJZ2GXe}+Ka9~^{>8SDH>N7Y}GFai^8@jxfICbJ@m-P>cRt*L(y zym*WM*99gKi!|~!!5L7-^|YXe+59r-Q)AB)NOCupAB_K$O(zOVzguv1% zU4q1tOS5#d@4fns=Xu`WKi+vA#~Gc08P@B*&+|Oa?fO6a2RyM%BNiks(H_B-F( zCbsBZDh9CPz^0wO1ikkCqd*cE*t&-OBSCKcKbzL}^>B7jNodiHy4XDMaf2rebY*c=0!CsKa?zTSReuBX^h*lyw+3Ei~~StxK{A*>{$p+D`PM&3UTvN<&@dkBgiu6})xpFiAvUD7KU@~$`P3UwMj4SEn5;Pt@9 z*qmKa{mz8%h%KsW?Hh&I&r&*ZC}ItK`C+1;Uqu2KPMqdofhdz}FC5cd|Xz7E;W;^_(IR|<8`2*BcMQTPPWTpmmgPfHZPUq2j-m(d)lxm29IOhc?r z7vh}shD!^B$nKU9Nd^T_(*D>qe@90m0GsCak>&3*-yA7u)(@Cu&8e=j#tryaI^tRq zN`lC(7nP)`+mnbnH_$I-i!loITVA4Mn3&r?0@W|+EU&*e`QPI#B?Mk3CZ(n+89Z7k zzf_W%mM1aGAx?AGXNYX4N0%A~B2w0f$Mi&fR<8GNitsO+z6AOU(a7z06RwyHm(o64 zVv^!jvp0|rWlZJm_9xs7GNG>k^J{Tw2r1M96f1+H^rL!<79+sL*4=#42=1US|Bx!V z|4Htb3wXz6Q2B7!<)KAW0ZVs*`{8K>pb32yby_cEAn57NKZB9Tfy#)4ZvN6-cH=Rq0zVTZvvEVac$IE71I7(b{g#QsW~8&07A z=JsgqVDj*d3@tziab0nd^8nhB+x8Y}!KX#5w$}n|u{klRa^*?XQ4fXKx=lNH*h*|} zzQBcK1UcWai{si>#?U4C?Wc_q-pX1$m;LkCj7T7!f4W|fj@_q;B98Um57{R3JBela0pC)W&NiJs0fAH7wdOrt zn1$UNx)=6D=jpi|Oo6;OnILoykTPtYBM_A_7x;(nH;0kmuK_FD%Kad*Z3xJ3swXop z4%u}5q55V&tRV2m$&oc+d>y$suXi;}@eU7&j&*j%{7qaDQW%~Cty>&a`H1K&c)Xe% z{aO)P<-N!q4@qeBvLr9()%#co6_IuHC#$qzZEeJe*#5qP<0bZ}oRiJKhaPo32h!Fd zc~mU|N?73>RQpg;jVGHhZnWULHl3`;sdTy}O_JG&LDA&l6g2kvdt4jlW@1nY>vspI zpwto@r=V47l5YGS)jRYuC46>Mc2O(sv$R2lE#uwjZyRlUJrVF@X9+$Sc$Ox-Xyc3GL|(>y87w&!Hha1%l2a@=!&RZRX+HmcZbGiQ6zx4ycQ@;gs~ zoAr*=y6aB~S;NMp+M0Z-kVjL~;epq?UI5jRd^qs=-1(=oVJ}x?;iWRL^Jw-PaT`gc z6X&s58(Pa?KauzVte=g%hC@5Rncs!f>t4fgABA$!4=*+rNEnRyI>gM~)tfNI1f#C~ zZ7-HDxeV0RBV}ervzlgvDho4|^LBt3d15SkoI4Tka=1J8*;=RX>T2)1SZ6bFaa>0@ zUAx%u@I#{?gehCJrsth*+c?nz5yvqI;5RFqw%UPW62i;eo`JkGmDy)ZWK#OLGaV<+ zsoY@}e9+=p?!R?5xUiu|>ohkrF5(_JA@kh-=t$ST%&35Ub;M}G;lt9RGY8Av0hzo8 z7nY#e5BAnmP)?6D`WO3pRhCgO<-3BTG843*J66o~Y@cBu+OucK*~B~OQOyDHq5RAm z68qVru&AMpAK`9J<7bDJ-)fM?ZJ(@h zQH%jQOk-H(d+k~=ORMZ8G+ZNiw5B4rPzW~x5QC;PTWBC`B(ns!3kNBk12|AY_(3b@ z{Eo7@K?H$hy1gcS%Il5M_G+$+M7Mv^x`e&@YtScePi9A9_789MXH=TjSea_(ghqJ-x zY@6#6&X10!u+JA4q#LW99Zv!-vwMk)4VnrCj7dbaw8WA)_Tmlid!GRS zu=BMcwm)@_j0kz|_Wcq)TxLQUxzEo$~0OrX1KOT4Bi{MgT5pap# z(H|$c{d;#7$*p*+rZB`_U5EsjhjV?GT*FrL|ITXGx>7__c?d6X$b}_PiKeXs8CtY8 zlIrBl5D<6%Z&JAE&0CyWim+~k)xq)-%KPc%Vh3SNYM6A9lV{FuY59w(f=ejKGSFt@ ze0LCaeir+H`{)9-&+;NPvAN+~@2y8eu9Uf>!)YOU<0knew}x&=;+PJy@=5Q5j3-NT zNr|Dxn~eBymxl$ZOn>WYIHy%rf+4#aAjw7{fCPpUzCVm}btoqR&>qkc+gRS#zv9k` zfQ$)z#Nz+Tm>R4;*S`<0#h-&KWg8MOZv3|wU@Y3~;Yj=4bTz74oy81ckS^!OlUln0 z!-^YM1f&CsKB2c>D$F}4p>2|1yKIA)%aa-v3XkWAEG=Euktu4Pb!eVO5zs%~{uW6NKoHgh8)i^^v1#trK8F}=0KvuIeI z)lEraKrn(@A#yuQ~-oGz*baY(d&P9|f zqw_ByKLwznbzA^6_j@Dy0hqGY9{b~f=1`QsiXM(%T5I-vIs5R0bz0<8bi*Ij^7fGb z15zw;YjF1B|E5_wyOh2%FUs$}1z!xF{ER#J3ZNpjVElK-kqRP?6vfGGJJyZ;!d{Y%pkLMLkRMN2xhxq5`@)ZNu= z3-&F_T>swVCt7ra{x+%64&f8h{$o-zKbVgKEFO_d9_ttVf&NR9_aIK#l3_qNd~dr( zB$P?y8^$L(|6Rc4z_X>C?@!+B5ZMzM6H6z~VUhLN(y;PUM(=pCFQtZZ;nus}pOO*| zKM(m@jp?h>F?;}Vl8o2^7wqY&(AR_Se~QehhHe4JcRiH)HlemD;ekz6O$+hqTOj3W zQ1^vvg|sg58X0?zULgtB4=*;ah6DQzM!+$W<4ZVcm)NY)xR>$$dfMrsx zYYsdxzs!9UaGHV{Aq`ZgVDdgnHt%7TMQztgC&>cb=SFfS=^5}N_HcTcs zG$^m%Z+_e#XlE1qeeXWmrYY#|=fp&EpXsvFDY|^xn=J+G_1YD*=nO@H|bny^N%grK}~ypgr^R z-9aN`<5_e$${CPS+b{nnAZJufvSeb2g%%JeT6`UpC3msne;IrrH~6iRZwnn!`yd`C zdh3QKYHY6gS8SUA5|@y|7jat4rjOxDU3cfJBn!XNEE}G)_UzRFmS?cQ!X>PEw4r-N zsV9o%w>i=;qdDj`tX+TUZn_I@X_^C&w;}+dYvcHPE);G9JVd|agc+h3)#n+u?FbDI zj=+Qev?}|WL4o4$=T8ffqRJ4hr59_PCvb`I%{b*BN4uE0`m_OHE}$T1Gw6iTSO|005%kR2l%E5+|(a%>=*cLjrXAZ(GytsgC|a&7o8H zE~N{XULNUz*AU(a)Mkf>+2v^rsH*$h)3pMr8CUBfQ%6j{hDppO)OFXT>MPeVg?ZGV zM}uX1W{?v@xK*Nkt77)=+}~Tj!5-#<3>ORUO9?Y~H-G#O1m9|uP;6Py8v-v;m%0)4 z0yNa&Utm)_?TdTGbn3m&))*uJ56IN1xu&U(Tb--r-&BWhnaciOFZ=(J6xD-#zk2?> z^fGn?EmsdkJDI@KYLixeQ2i>vFbLd(5c(q&Ksc)YM?%eyFAahmH@>5;5Ha!HUsfAY z8ZBP#L~s47OTz+((OP20m|L+<9ip8jcCdO5^y~;y4w>`^wSRn+tpTJlg`Gze^y!@i z92Wo`K+z#n#`o3h*%OSP5A z-ESZKrpH+yetbOhiH=Y2QNa=I%@jr%yb>xz-j#|62(EQC9NxXU>Yisq1AQ| zfX8iIdR?i&P#W@w{UY->hZnaa6*U&2K}pvz)LC`|5cQYJ2V=?J5+IrkH{X*v5K8DPv|K3XTR5B^$U#=iA>~^9=g;GkCrS4JX{afegsZ> zLB_JcY<+UnD{P>uexs*c9Gh@~{C8Hi@46o1|B$+6pmp~Ayr38FA-=moxnJeiC>#B? zot^RTrh!){w3;EdvzNP1&!zX7M(m_Y!z^i%f8$T#$lTTdITHFiR+^m`4NRYDna_a) z1i;mS>;Mis4d9nDry84>C{DcS_qMZwtfvNGTYFE<-8CH#aS;+N!x&l&>w zfZpY&uqsR?yT9wT-T=#Sznr)B_%8;Tkh#uzkyO=i#U}ZTGhPw1Tsee48msjq4IEu*mCf z1cB4fT>j)(YL^aW&#kvlJEh7OX&>_HTHN@YYVdt?VQ$g&4R3UWLpzPIsDAb0SlpT} zeQ0$=_FlSu8ae81zd=;SG8;BYQ(bY`; zRWHM@%MMB=cD9D_8ov$je&^;^K8AWQDFZeIUVC^s$=c6C$KT2yqViG|B#`L`mBFbf z@TBpdNjLD`U+9lJ?h}_Xj@|hpIXyotd;kt~Qe&PKj61R3dF3H21Gva#s1R>rm;d+` z_d;1ZZ&kR?mN-8L7je+69A>*&bWi#k$dq|T_SDl=q;351-BgG`Z+C_!1IER1!F%>3 z(1meQv&+1|HOx3JGQ+#};mPzsf(Z4CPrr^=Dltl zN&y^!KpYi*9QtPRs+cl*i9=TJxfV@G_hCMKt1wm1Xd^+X^0hNBAf_;}VK_cQYbm^d z>p=9QsgXqr(Cl*kc#MND5)~Z~_Vv*{{FiD3f5CXgzGN@r{hdQy$G|j7?*ujsTWGSt z=INuDDGC6xe5+%O2w)Wzcgg(#M45n(cv3S4B+m%?bFmlwA+nbBX#-l zY=?d+JXjq2?@@sP6n^hMAMP*LEH~!Zft{Y^rTJ&q8!LUkh{kl2Zn?I5odfgJ?m3V& z{YIA&k(Q3ps|ykcsFcB|C?JkJtKHqSoLx9MWSg550d%WFDSyS2idY*oIv^Wt8Ymjm zRIs}?_dY2TOWfBI=E>CWTePL=C^U9 zBhY5uq8;>K2QWe8GB}LWT?+t~Pd>6FY;AQ1%5H-4cQJ=Z>H0RNKP`IYbC#W1ne!A= zyN3Oa^r}xcTds;Y{eJ7xQ_%d;=-Ye4c^ieFRuxCmNmK zSYj0>*68+D-%5r`2aeX0!BIU9rt@=60jTS%tEs6ej*I{fQ~f8k{QDS=bhYB2X|XeD z@rrryWB@l;qE!wlqcnvdilbS|;pvX)uDn6Saps}KS;$r{=|du?U|_K-q!S{}xPhy# zzK1Ryzz+q?i~4>*Zs7hOARdFUkxtehB7xm#XSz?bl3U&q+f$reKv!#LFG>9z&-G4t zJPNMyL;$H<;%nrek#4V9>J5y^0*$`D8cuUfFFcJKYCk28j+*V5j@Y*Faq+i%q^jN_ z^RF}|WasP*CNiR_6hbnvnGK)ub%^rj{7T7?4Zo1s)_yhRjGz;PasPUumG0@aRP|YWDD)9*--4T=U#6Dd}LPM7Fx2fmO;=qw;jE*f&EY4K4LH$hRW;eC(kdtH+JeV`=fSBAz6v;` z`lHX46xp&z$o&7jp%lFeS-NZd-Ze_mlQ#_R%W|)_ZP`zw_i55aSFt(9MykI7w4UQDy{1MVfLj~> z-z`$C_fZP!{~190w_^5J@NKa@sS{vh|FKAYtzB(*H<(^&r*U=lKk1eHiUfYHYgDDE z#&tyX+zn?{b{QTx-{I!d;0`WT)0DZGp!a7Y_!E%5rg1v-fzz>BW-O0%sf}7;IQi`vepXlp$fp)&((C;pq82)e%@O%Xf zfj68r6jdG*Hhk=`*=dmvsv@>AX<~Q;g>0NOWN)CQv--IT8t1g=sa8$ zQBbG-*98$p+>?i)nVK1=RXr*?3wKK(UC>8djzpYP>;OAoi>?T|>@%Cz0CcaB!psJ*rLg(JpLMM)gi4s}4U;zIfG# zy@FZrKp?49Ob?Lmex8irOn!bT*&8EbT&{5JQo*lXcZ>hY=3RsGkyP1I{~y2<^ZK&( zAiHmT;b8uV>xw+64}Z8LnmX5dbq1G=M@+|Ee>>dE^1K9p226mAaa( zh!%w)xv^P{Pf3#_jDhE8$Jf^kJoIOqGQ>&V_%0+BY-qD|Y>GlAyQ`AtONEd{dNSjl zhdVnva}9y0>&%9OQi>YqUB{!l15!q7p1*=)<2F9w0#bzf3bS}N9q_fshalSxzQe`G zvq(0AzCD~0E%O1Mt|9<(@ueTzpr zBDJ=O5`3n^H+tv4#nVQg-7wPPuKk@)`;0@VL`c`K#iCjJ9&nWkp4SWJb%yZjF_#y6 zhLdxSc$(d${@Zg0m`Fwc2A;bE5KaoT<<6Tw*@aGr-ri1ahzCfeEf)MSG5r?H{z9W# z2r(+kIsa>8!q81b$0}&6a63;>2lf~yKERgy-K)mb$raBW)4r>)fO`6cV$|`kT}O8e zezL3djR!uAuCZ4Bu1FJo!axB)!Ksky7mY(yXC+$RNnixcAw=T8q^Hw*F@ngSEV53v z)`cS~aRVwyM44)w^*yr!@nv(bID(|4UZxrUq;H7G(9?RK;xs%ww7p@oHn;Dx?XX5; z-0fsCrm>&-&)o_LwE0}aV6Kj(zs@KYyWH;i#{|k99z6kdvu4hA9}7rX$-3$Pw>+kv z8C(1B>gB)kv|JVHkkoGf#ZJEMPH-anPa#Pgj9hDMOZL$5tf%WpTJ3eYl>&?B63Gt@ z2=g_XO0@_FXu$i5hYx_~zC<_F-p>yO`*0zGIq~`UX)u_y<(wK-$Zh>e)87N&ZSqX` z(tw<>FHcaHBGMShHxn`@0)8*wr?MfEw{7nNLtJb&GvcxJQ7=nZ9GwvZ3b^!jgbxGE z#-eLZNNrUmdg7e(%Rv=B`wgC(0qcnYxruv!n+N?I#VX*5?j8sx0T})O_4Jd(*zmAF z(jq`Mgv3pxKDz zSn5-L6_o=T!@a4FJV`vO$G1!D45L%;tN?LI-;9VHs!Xi!1nNPXF6EY=X?cJ}OGbon zxq))|Fua}rhT@%CIZETK8ORWIFN(P2~XIdA2 z6_T-VBI<7#z}>)(Ire-b^>6)wA`lesy|YLZ^}OHXJ-O@|fzMqigCHPhaby4*Hsv$D zzCvUNu6waWT<0E9zGAqHKQLY$YvBhRwF!VKh7^tDh=K#9gyw7a0*k&IHIEavl6*KI zfAzfr!q8aY9np<}+F};xUHqU#2;bUuZ?o_8=x|~^^7hX|9w(C8-01kQ!jGf{zkPE| z{G)_1DwUjatcIhX zVb+Ei5qs;6R65)1@7_luJ^DUOpHaZ{Dvc32Fka2=vDG-`-!s|1bLOmEa1+MemB2G_ z>BbO{qn}|#73^yKAx&w;*;6H)kmqnTPG&xSduHtdg+0%~&c=T7vhj`2d$j60lbb(< zU)TM0O-CrGV-auQs*XFZ< zqt>O)LMR~`c3xBieh0MLg}1ip#(=znmxsJ}uBF){8Fc(+idM9%Awkd=%NnVGR=ZkZ zoa2Nh#E{CzgjCElrbg$4RL=>oIe8|qSh>DYrGVYce13~vBZl(tJC0Puv>}N0;O~S~ z`_w5hII3|aPb@rYM^fV#;v#;*D4qeb*M;33pkrvPxdE{*Zmd*v*l z0H_VC{H^29moo^vja8b?G4Y$6`4cxC!4{=obdU zdjivq=8wCzf%~t@V1TD^(h92?3O}1P9bI29VxU4OG(Yc6nRZIpr)yT%hfW!;L7*ws zUBQ3yH7VpumC_ROE~4q8ya6@1D7a4uDkBvJaJmobzJ!WicxgAKR>LqK zB;3zzano{bieosz4C?-M1G%%K;~vx-Jg;Zy!q71#xkkAO%J8in+WvMA=?JuwKES}k z##UUm0_1iu=SQ;`#ABp)o|Z{=Wdpr=ns^3qZPvv?*N&oxhr5UeI&X&Iz3QlM} z4&%#yrm7Zw+A#n#L~MZQQxBJR{7x? zd|4LHD3kH8(D0vuS9_Lrv0Uz%K_#f{MiIji)|z1)86WwVI=l%qMdSGl64s8f{l;RTKQ)hbKC4~c`C`r2uDLV> zb264}D8j>-9;hZ3Cu`~}ZSyiW+%vQ(Z4S5Pkd#?>1Kj{UB_BzIRJ4&~V@NQB472%2|OKH@0ZV0rGIjj~9*Mzwi)e_M(lxVJMhqFoS*aAN>gT`B3 z;~e4+sv52s7KN)m__QmQmuep5!Oz7f4N>4=5|QQPE`$4`e(fddtz4X|%DS2n(-fQ# z<}5JBmiGwK`LwX?iR3~&LwI@F)I&;W_6MmHrl?;@pLdXCkl+vN0t`^-+l^mwa4EWB z6y-@^$$3`x0Y=c9=?uRuTxD(ymdCPb`ppI>(4Yru-g$>_LD*5!uYWY(M6Nq`!>-4j z%I%0={lKdpOkuoe!weuUjX7B}o5yq(k&Q6qSwggiFjzy82LUvDE#3#KPsQ+>zi(j; zf!7{nRZCSmU}y{sAQ#&3Lb9_Y+ZNg+wkp~XLC%XU732NBAtjAWAx)Ok4H|)OJe4e0 zdsnjaR%#y6J+@3;z=%mUTg`wXYyv!ZnhSgShNDUrWwr9ijZE{w2g)n;!!6=tSo24pz5)pOoZ! zUGL}7+%QO$QY>>mfOt}b@_sDjE=q{1Dv=W2$27LRdF8J6#W~b>^$I$;g&8yKK^SBHq%4kzCjt@Wm76yC>0ekpZI`Te#;@{sA&gVZX+OFdJ3Br^5zAAw7tUN9bbCUrwm0y7 zey+-BD+qo9q-v~)Cs;omqgcQb52(Asy)Q|A5`(-j@_&WisiRu*enc-ErJ13@j)!rb z#Dbob(2lP2oX4FQkGD~;(O26NHcuWy=T<%|t3?}4t=ed-ypYunm_hV6Js z&#W%z*N7|Ra&~Rrl^Zr<&~0lA1()=%NtTsy9qhT=X(?mTeA(m|Qt*II(876%#C(Dq z%!NyvwmR`zslAYN8jd*gT-={im0U%TxP>0|3U#?OC%laqawbKbV4FD*C{`4#-r<-$ zyX3S;{7lgQ7vA96Hy!&JE(=PCAMhcSALABk<#E2gD4I$%Qu@XQ40I#q_?}dlR!ZD*>%Ci&NiKIumi*%0 zVb6RxH~WzC5rR|A&e@Q>z3BwGpL+2@%6T(iKBl67h) zSlw8*tU#*q8t5}Tx@u@NS52}I1_E+;je5CztNwwvKtd^*Tx-aKv5KU{id)jtDAN%& z+td7bBG;FW#yGCs?`y6A!WhcxwrYy3XQY^8r|a13hxnd97noW!2~50)jcPa# z6}gU7tB3Bt5D}o0hyD4uzla;Cx>?OSEw%w2r=QGQQUwLy7GflH_r471AvTW79Yzjr zGe4Qz*x=--Xv3;fPO zC@;i-+%Mj1@7gX& z#st3lF4%i$!Y(t^p`UtczjvvWz3!p#vi49ub{ucHvenOgHQez{3`ptForcaP?~|P9 zgZC#WLcPa9;|sozgNUolVz8xz9?RY~Ai@ZFko)Vxpm}_Jyi4Y4UBkx-JBRN2gdygh zstjpvxlr?9%QQHS2B_0lZSUbc3n?@`BF@B%{T)wSU<=9?DXt>!5B)^$*qri~B?F%> zE=;_P5)2Y8lVB2(-ZlYANIv_S3#b1gPmavP9P;MfDU6`x$~i*Bi8&&4yVq*_Sb2$Ju0wn30GqRkh5L{= zmS!TpJoWbOmnj-6F_$pzGXn5)@T|F|2}jn?+H$3D!E|%4V%%9njGyre`vbK-p=yLI zq4ZFa>}>r#Ek}xpyIQ1nbr##B6;+9MNHUE-#}=QXy2UW-cF$(g~nRRXtUEHi7iE=DWqqbw zS-JQc)S~JrkyR=~+dO8po@kKI6EH4D62sP1=8`Tzp>0^=Wkgm^z>OjZ_|=mBBasZE z;847$0_8|j+4Ao2Pj;|B@kZVss$pd2WB?W2dyt3EXVOqwcaOuq&&xOd`&f7ndGy}Q z8R(H-j}wpJk$IV9!kdZ)FCg%FdF%!I9v@C+6@4KSY>j=&{(8szwt^uWmMJ^%ynn|S z6>dHBsoHa&sZ)4B9Y>0Jsi=;;Iy&kiot$LMlJ?%O)-1|gl2r~Y4admJJZ6UZg0_(N znmO+YJiG6${`F}3mfoklcf|G@BfB=-EfJjr)9KyvXGV|QloL#IzXl&aeh3H2mR3BF z&e7#%9BgsB_v^$TNk=!g$O`*OnkCp!T${T{RS8vhG^}fA3qi#4m&Z)gU}oKr7W#Sm zE&nv@Ou9$naEK|Y18 z)h8f;H(!r~t%Q+G&g+PY?3!9)4IbY08g15)BNuH>UdJ-bFkL=^CYI9Y)ey2#5!U*j z1iMwXc5+XuESZnOimY<@n-n*Sb;8`vid`-?z8%f%5s>T_Yi{J0q>@Q5CJ&mVFq!tn z{;m>M!5tF7+fQFhw@*;=4&3+U5cg}tO>o|Ugv|>wNUl1rBt9qiYkN-LEZL3|hnRI* zI**^_Wo1jrfsN!NoNhQ?drftyg7e+WOCv*bt-?w{R@zrX%1eon$(@mfNNKgjAEC3a z4M9J3p8X(~Kiq{~-t)cdc8>w%SCF;P$^kxxu^SaswGjXcLlgcYjG)=;2sE z*0S^B_#!qdfkwM z1ypuwj*Bo4Jvgls3V2rGgmEesn6tRz4OSIANvRE(%8sr?ivIROv{Ekl{{P7-kSnd zm9G+XktreVUNU`Pp{=pX^Fqz<*fn+f*L6UFN7hd4TYVba$I+TrrrYy8koTR?jbabX zQZHeReSa5ieBxbj-di7e^rZ3tx&ig?HxB# zpR@4E*BW<_vO1M2;WgIK>3WYK-CR=(iG-i68SV~LWtpIZH;cD4=-xenPmLUBkEHwL zZC0eOa9CQoWA32mnlCm##?BrK!6`na~6 zExEpmb=rV8Nbmcw`QgfQEkkYZcPQZR9=Pf8x*yo6=BJaEhoHiv`D){Sb0H#cOcqQ% zkohh115+TUc~h1iiBQ`#|=L) zh?yccwOv$<>&V2(Lo-+L-rzCE_1A@q>;dg9*50WoHfqYTn+Ky)(JlkuxJBsbSKoEBjQ)(qJXkoW83$i!ETyrxWr8mG3;bM*INrL zSY9CGiG-oi6V_0m26srSD}fJwFy_$^q#v($Ou{sS|l!%NLJJT>)*ssaTj9ktJtt5G5ZAPl!TP=?QQ7wxArFcH}@ zl$BEum9B1!wTrOGXZbYQ4ks_+lS{4|PD&Gzo^URs8hvy)KKTetVg@6 z*|vk>%=+4yTtCXIG(ItUw+*(pH`n0(VgLg^&ZOZNil`_%xV1a<|R41 z`b@V*#H(k6MkDz;fX6Q3L5%?H)wpM^kF<)f^Svg=-(~hTP>!eX6h*6k=hwFzKPLPpNG~~C0^$3u#a14nkq!>sbg}9;vSp#AHDk)4k zPR_xK)RVc30za3oHjSCWCK)>CKnzaaBa{XR)sAhIrHF*smlatM&g7<6vuNtQVg!EC z$&o$9UauuFx!*$;6OUUan*18|QF3GX&%7!3+EvYg0VKRdvEls)>9hu4d(&SERwM7@ zkcZ>YtUD$+Jfl{UcJ?W|G8FWN`uYQ=O{Il2@YnW*<`%ctTVd#XYL8D~C)p^h&K;C} z;EfUapp#vBmjGmiFUU{&?jcVf;RDk~B;@Q00BcLpg0NqF9~qK+mkv31`#GVFx7HP=R}9GFuh8k5Ip4e#(F(n0CTZ2>Hg&5KI_p#XqKnj%pC(qD^OLtXw`1bg)f>RI=!<~||PrBk<#4-;hKZGC(=XUT0wXe;axwLUETHK^G z7(zur6=}MP-dI1Q61M!+oj|e&*izDUe4sIPK+B)Wxy9@R8X!|X7tgQ?%UwTxkifo5 zIrYH%{h!ANRE`kJ(4aW`;+T5c@6@zjl^!}!TJn9} z%mrYGy`EH3zEmaO@3pR(&~8p_>Wv${D|OgDt>$VaG2>)8@qS@!`TlCVTLDgs9UShLP5&K-bQ^Y}6uRm>M(z z2Hgddav^Vr_Adku0f%aa5sWdm`mIp=?5*>UU0M)=^HF_$LxZ13!1Whe^r>pyxQ?-L zhQK_>=Z9mFPzGe^AjY!_T97BW^g;F97Y|ui zhjFuzKq8`p#$}v}H@k+XOjoW?f5qK->=YE1+?VSj{6aF1k1%Z8s>SILSC&>MSKbaR zml&E`iBMn*5xGN?W#?G&-YCzhQdywL>GW+CdJ?#uPzvJA?T(I?TprJNqjT3Ze8TR> zb7kiQg3rM=K7mILqU`*VRaQz?d)DZ}hiE^&5WRsD`3IKbuG2-B+# zk?G72pawjof1}KCIEG_<4*jax38)qWdxzN&`+<+hEqBI;t%|j<(?dR9z7;yqyw7bJ z6L0*vsVGDR&LtI%u>pw&#L9Ywz>`&gU}n zp?xmP9d4YCPoKI~>@crgbi|q8Cugy#JU4P>0c4)_+=+JRutnB?oBanP%gXP28&e)$ zCW-@dn~`BO*y2|bWmaLCu!;Ar*pLtS#_9E7FXK|u-uL?(mqt?455gSGjZe1$(o2&) zxk_{W0mdTH2WHYnj3AloJ(y|0`5% zJmZ`ctZO1PdGE=!&AsQwHJk~GcZgO4SZGv%w*VlWzMlYez>c_kG+Z`}ZT2i*8sWI7 z(_M*-d^De92zeaa$b(bT31k^m9Y1Ln)z%945kO2R8fc+PE8~q+oGFGBR06_G4*H{Z z!}ZgU%RO+P2&KL*lrw|zz&A@slCBAnoYd4etrW?s`%;_jZ^f#sL}7M;1!y;fT)ub3 z>)s!w+I_=pA`FZk{?O+^abQerKGNy5;>4gpw}yRDE|v19#NL$E94%j>?VmHjw|zZP z8jDhH0oU*ORfczoF`g#J5o3tVQr+A5F9c=s2K7$25u7T|s739@>p^%I6# zoS(0a2tB((rQ#pBNP@-P79LB6=~Q(QU$`P>TQZu@rI5OXE;qAWeaQle~Xq>z};cR1WpavX&89cLa#+x%XVOjI0-cnJWYa*Y& zqRRu)FqglUVFd>c`BI%y4*c-r!?;cJp_AwdpSUx3Iwg@&5jZi_z=?Sgt$OZ2*<KT1m0=SA-hUmF=g`zv0{@ zyFNn5AoyZkcwYU~NF2O+5OQEngZXIQoCK|5%CU|$>%}8z6et+NfK?(W2;d9yfDyx6-g-p8Wadhw2pI zffKBbI}1C@%z^48$fS|dp7FT4zA%C0PGh9IHyY~KHirmt*^qie$K zVQ>%b8azmFhoHe-f+xXUgTvq+bg@?hq`vySu~AyVq`i=^xP5=TzNw3#Zd8 zZs2Dkrj!BZ4^q5U>)zcwQRJ|UfUO_@m6{dxETwZev9^=SVN9TlZbK8?Nj;Nw%H}w> zwK{Prb@>0`2L;mT8+nphsO)u{7w$m^RZvN%0u`{RO@y zf}F`r>drp0V9@I4$sYH$mzxuqt+<%Lbps<>0^V~C%6`5dcvJm0{SQjn`0xkvE~|&&51MW6 ze{TAn3kzRj*F=CH_W^J!iwhc#C$pXJPFJNGT=QNTPYPk13`OgtH3U6(w}kehwjDIo zU$8me+KO}_vJ&5g+e5x{=Qr3yOA$TG;@(bQyeDPI?dVB=IaKF##6XSFx^r-LxtXV2 z^YantL>i}+BqZGPK!(1XSI1@N)j%l2RtKU0_ zS3l>kJei&$e)aWOyV9UUfk~`x_OJYHsG6Sk7vAcET_wZ1g+OOH?CZ2)6ZXJ89Q5V; z*Y^;=Q``_)e&@UPu8%cKG2Fqh@N$DxA~g9&^Boa+SayUtxGQ?2v)GMODOFu!Ztj#} zqQ>00y$3TEfAnx=(f3j8`sI=3DO$_zw_8Vhe%qhdp5|s+@HoBSD3LQT%JPA0m`>Qf zvKx;7Svv&5Loqv-rS-qYP=}ig=BhoI-`eYFb}pKS+p)!dowHoM#OW57>)~u9UxFC_ zz}Kj*er_mc(;*qs{2cbB)R4mf?%JFtbJ1YgYoUio^&H83I3sse;i18M0n-1^1>jpb zc|#g~^&d|@&@UAA@d(tp*7v{>z@E~1@m=qsc3{A)&ump*Ab!_uz|%gKi8YBy)LHHK za>QrZxe_EA_nb;;ex5$3x4%k$kcZctC4$-L4(=ppes+hReymGuQ8`_G^AW}x;6{Ia zA@94{mGf1uHtuL270^hhABkSlyMZ+?J1;~ee_JS<4$m%Nj?l2ko}kvCq`2{RIKz$} zOrhtvjXAs)7(5cFZgtr&SKXK_wHme@2P}3!m!J8aGSR(6wET?^YkIJ=S~v+vO4}m4I_q z?wIau7ywmd@zG!#LNrO_1KKNs_87V3Mmm!5M>+!8`6qCDK2Z~$%bV3~#6apTf9hy% zRox+W#_9YRpKGA(flV##mH4oBk(GEpxeas}70J-d{hV$ulBBRb0?XrD0cU1s^X1u- z#TU-RNSE;Ej@u_z@LbJoCWOc<8Q^kBTRbh6~{3)KV9~x7)%VHt^y%1!9HgRM9 zzL5JCaox(TWU$~D42}wCSTplm#UW?_Bd59m*B!!$r`{1|JMZJ;XR;QCK z>aCvQAXjUY&fj|zj$UP&WxXNWcbad6qrQnh(||l%!BO~Kk&w&-Cu1~AEa#L**z`zq zBZUKLI9v65jum@VQ^`W2?fVo@TwQv7Y?WG~K5X%CJX zA0cs}M#YSvj(9mha%prwYNOzoVAV?U>92alxI4JN|5| zi`@2)<8}eXCm}BHdmZlttL*V;K~W@e0|yfh?*R`NC#Q>8*z$Ekk9i7+<{hl&!RCKX zo_)dY2n-!yuY5dmA6I^&j}uJa>@5O{32hG~f@7>E0n`C4-JHoNP_U(x_-#N}<}#={nz( zo8H^oR?&+CXze^NKGDcZ1C) zTTpwOQaxwcRe+FknfeZe-`@@MAhSZX?r9QR^@z$MlPtBCL&t4=)Ge7Vv*FwanEr3c z4zz>zPB7yU<_IQIT4VgS%lCTa-`NZX>WL2AHL$DJE+h%pWF;4tRe$68|FC>k#{e~0 z1G=U6v74i9E%YnDdOI-Ax!cwc%ip?IXgd!?CDDix#3*kqpah_tGuREn@lx9Q(P(9@ zmbJ4X2(Q;T1P1t(zjATA>cLor&e%EIH^(d#bmwuMd%G^k50b%QeT4&94Q>gdLBxG1 zBSt{0b!~3UW12*hbGt@Ms1e{e;WNgJRjM*#Q0zPg<>fPap*A`VE8Cz!;6AgB>cfFS z@HU@XPQxC^iR{zMJ=-D!^3B=|mbQOwe<@*S{vcY_zgEo$^tAFpbnGj-zi|c&=(u#% z($u(~8(4cd51VyqFhCMU#G6dB_MOtLmkTshh5x(no$anKVnHtAmL(IGEqgT_lOIZF z@r^=zuXeu`w1-?7W?3E6eyIBI>Y)`R-mdiNV)&|eqQfhT+s8(X)`+}*mR?C(Mnq+i z8$0x(cOl>WdVQY$r`D@c+uOf``)Z>$Yd1v*?aXP@rH%E%z_g1RdE!;Mkl{hl7Y|o# zvC-=LUl-w?1>Kz+Y>HV0pHal#8nTT$cd$b|j!hW;j&^Z$3t=W)D{}0wdz^zQ&0JYxB6WV~2d&vsss7LF2sSve86HIO!vsvRkN?)A_6;dNM)&h^F=Ijd z)9@ehGh$@?d_!_EBzsOL!o2@H-RxP8PTg85uj=MlY7|A%7g!yR1gx@{;Fk#7KHzQA zljJvFB9c-S9K1K#juI zC?zV)V|M349kGLbh9Rb97%)^_o0g>D+@o<}`^m)%DffPWv9P}mOM{MU6`-z$0lC!G zn)tRih~dx?Q!Du4!tz|^C=r#CxsG^Pw;O|702RFaDjE$NhFj`nV*H3>v zO`nDi#J}qz6l{CTFT_5g+(yMmHm1Q2@6_du1K+LpBSQ4mpBqH0F3jb&^_NF4e&W+2?xK`Yv*!*l`~9f^?>``NY=8E?b}vO4e39Ia;VuKW*^S1VW_oG41p+CpSqd zG)$#F#&aX0qTyOCNEREwg&U3`tpZAI>hc6CuP09#pNdkQGtC(n#>8(-r>;xum&BWn zTVW)&|1khJtvB?h@!>VniFay;I@fIh^~grNv2ktn%~GAQh%3ZNRINR0qn5yUJ_w9` z<-U%QgEr)*j>?(Je*66Usyd4SW9@KZ<}XGo2u65Qp=R778LXqE(ahK0q9i@3suJ6z z@^ynnG4BY_S8gBq#FJp*7FTCAW!v)1F;7iIXz99s&N&yD{Xu{Z1Uu=Lvo|^(o-L2P z-JiW}9X>wlH3yTd>%844{OP)m3v|r<)9z&=2}~R16<$FJvfPY>s}TG^s{kJ-@PYA7 zO*b9In3PxETjAcYi!uB0@a$m^-w$=uI!rCw2wv~J9dzDtSQAxV9(4tC^MMvGt~@HY zg-Q<#Nqgiu=_$*lV2-){w8HLCVBy4OY-N{BC3rV}_z5Cd*&>L1E())Kfg1~V`uonO zM&t&JsCI43Mc^RY1Q#W5x_$f8@!1ylJ8Ja{`qv}5N0aa1Cg&NWhWy;!`ksEB>2af6 z`~++1EDZ7po#t4Z>4c7ecV<7z`-YpMs-h~ALc@RX%FyWV^?lE8FJ~yNZ+FR~m$j6i z++U=>=FYF_9WIPQ4_*aWs}9^9F4hHdtMLi&QzG0~0s19VN4lsT^S}4jWO4sEGpnzU z?7n5MYkG8PGSpr@I-fguPAd|YQ&TBb{WtnSFj|<;{%O4_i=n&p+dhXdi1VP8mr^~+rL zNU^3mMW>*@?}O{VNABA&mDE4DQq=NwPQjRW!yOj=i%(6id3*H_O)Nw^?9D|X=W;Ku z1~JpF!gkJgwh_1_FqG3^QtV|qQtq7|5;qZix7J&yxcDJ?%$=2$Fy&9@(K7$fvkV_x62fzK<;4SY^IQsNsz?c_1d8Ks^B0F`zczcv@G++b- zvD8OQq_fPp7|%fv7k^_FzaMWke8{O* zVP1JYTe&XhfA5tMr9q6`@)W~!5i@v+Q}~`;qyVS#mk)08;V^1$gSl2+Wz}xyf2%;5 zd5owuY`K^+H+`5dwec86J=F^GSX?nusIQb<#Csb<$upoI^NtTIq6aaqX{x}E=0CYi zC2uXl9#seb0?Gx<)Y$Ag#Lb7Nl1kK7x>;&rNr$Y%PSU$j3vivwF>;Z1=|-o&fQp=a zg^`_;J~ypNeL8IJK##U5Dr2qHZ~U+Ia53rJ+OIt3u6C&G)b%O;#8|N84-?!h)|a^G zj|NNzHacfw&8`7gS1xo6-z5Pi2KG!5t8H6NPyGyPp=#T~VSeqw)a>3KL#1)BO{`|HHsm!ZST>YIQj4WU0p#;H+l)QbEs6G%7xTWn4u{|-cUCd8WPuB$sirO%DD}sODkD2v;mJ}a$ov|A{dw;8cmS=+#^{{6ZM0Hf0CuNTXoPY&h};H~0Oe=V`)V966R z-;nWgVi$}H)FKoIPfu&W;+4Dp@_VEDG_JS+KXDXS_?Da;G;p%Z)oeq)drf#in#CT4 zbW_)g*Q$(4x2Ifg_4dlV`izW>KDmscJhHMEo(*wEP#8aSep2ZX&96)@XNOZ98aDj( z;|D&=DnMs^8YgbtzTT`2lWfN3&g}y%LX7x*@-hc# ziPsUW)qJypk*%O>9b|JD1S7TdlbSrwZBT9`X4YdDj{pEWKpy`gwk4oi`H+r%K9tvD zB=K(J>&(TrPxzIv0dDLMfoFQCc2mPI8r2dn{E#$b0YvPy4I|E156aqzNy7{Y&4OwR z-#3L=lxd0FB3f$w^T@MSy8O3sGxGjW)D^o^UN>qatW+-WuiAfjmS(E;B7vK^JJ8p% zAgijW1puEi8#aFBYuwM4LpU+(=400JeN4zqp6MNcJ{FDrpZ=l&|6V$h@{=_#=b~-k z2T}bWgrH>{R*L+ZVD7svgDOe-)+Y}R-=S#+m5b{k4Sl|5j2YLPlp>D`W?QPi>)$j`T!UE1dc5aAYp;Lt@9FjMBJ0FD6xkGcpL}RDTj{bUv8v(yCfS_6b*sJK< z`FegzUU{M69(9T{xVbsW;Q3);mKW8D_FO3=I5i4xAMiTmESI5?q2cSc+(}zF2Pgb_ zu!5juX86&0ST<1cHU9)|1?3ID+Da9|clN=Skcb#rSy|=NEI@_td5i8kf(WUv^dFR3 z5i1U91TWrAVgC&H?j^(1zasZ2M21yM6X>^nonPd?+vIpWCz8KjdN)65TX)>tclRcpV)=Vl4OHEp3>`8Kym4PvffE;H{*`I&wt({iM_KH;5a?^Uz1Wf+Rejg zQep3t&B|DEOOxex0jM9x{5o%mO)#>qMUB<7&39iI!^7dm*7{2+>aH!I77iM!6eR8R z`JQ!Q`cV9YmQ(lJT!re%Z{*K7Hn7Tu&GL-16fSvqQMQZo*hnxtK@l3}BLh=7G3_!% zAYc zv*B3F>l^&Ku-SO5C|cmt2|>gY8xC>#i8Rww(|-*G#XVY!K)oGJSs z)kq%jO&B%y{6c)gisoN-tRK`z{QIAZpS*>pc-m!=wz*K2_9?dPx_l0$X>%Lnoh!4; z@{zqN+7MAr8uSNoCy!6`UvBua#kWydx8ebb%$)hZxSC?N0|adtfibhCp?5m7A2HGS z3IiKFz^V^tNg8TwTA4BU1h{e{k3U$T0M6y{1 z9}WuvWX$Xu3tk}kJUmcvZJZ!+gNMDhW~Im&DUg@{r;?Xh#VejFE!|e8FXwgM8{D}* zCeHk&2JzRebVFHz9FkU%b};kqD54gYDQ`aCM8MqkVP42?_R&+r)XunhJ-yN`j?S*hPP`Lsj;(qX9iI1v6xZ{vH3vkWzF^hJ z1Qj+v_fHl4ZzXo)O+!uIwP*)|A2ihpcNSD84(oA83<oef5m7 zKX#ghNe9f`GIMh;sjp0|6Wc9svUM2EV(pbyo`~p`RaQ5Kykpd^3wJR(A2j9TMZ?6a z167)h%sc0={?5YA)Lpc!tIXqosIpnQvasF&apeHIV#}dh^t75KR{U={PKX!|nw!G( zB~CKLxYziXKSOy5Zti>EgbWf{{=z~O%ta-O5gQiw7PORqo@`kYTgw0nKJvcHLHcq8 zh2d?WznTLk_SNv+@T(Wbw!*gJ)iP)_Im3T$5XD#4R=1N&vXpW>bGMgC9|l~KAq+De z6EPo_nN@ev3Y61yc8q4+=E`xTE{2nO2PH;DBvRbB1UiXxin0giP#Y@|1H<}Isx$Z? zb>e)BPm*v57K0_fX2KAdyXYMtaXJ`N9)CCWd^kOM3rkZBtQugY>L;JdI7EOybDILC zMq<)Yh92C`-VDrj%(ve^xB43DH*COsvb4kEB7r*C=lVpxc zeEB&LE2SPXJD7 zhs#tw2_QycJ`kYduFeA9RMf>$|3g74|B@jee51!OR_vW5ZzpE zk-llU=*&2elYW`#n{A2UWbWXfBp%QpM=R9GjoRDizu*Nj#>TwgbtP6 z#L7ov5g7Qib)Nv&y2jkuP`aUMAYHWEQ$*~QSo8b?RqV#)Gdi1!m-kP;_0e2;D zHfEA<{{GdnUOzJ21p~T`hxS*V+5%eFu6`MK@eNi0CapX@9}s(uzZS6|WqR#spcRhOj5`%};*6@~Z zmjT6}8C>kMn^<8`QJSmI+a)>VZ8*GVL;MUDy7L4h>k>+uQO2R@2glx?CW@+c^iQWv zR@O%UVr>r=Fry@?zTCDtntk!ZS!b}t@OSo#RIvq2U|qMt+Nl5_F-P0oRT=w>tZFaf zmj>J98(B>6-q(-H*dM*z*+fW0j~&-?$gwrt-A>oyJTtr)=U+!aUea0FHtB2ze@k?h zm|`_w(kBErSahJg|LmG?KgkTu*dLj7Tk*1+8&T_!f~@uDR)Lf4{LRqH3Hbe1KxV#X zb>7gdRzF-pKz|WtaW@J5Yr35jXBz*TAlfV-HO2C@S@EOOhL&=|ibQS$D(YH37E=G{sIqYBOiScgxMT_CzfP3J1(ox{Y^{>tYKrkedxbWv(< z_~U?-F5}Rcwb`8ABeTphB)bE-(`}HY(hiF+VhZ?t5+rHoFO^?)RnFJ%Z8SoAql@1# zfON&Eb;oe$V22Jr+{ETJ6|E$xgzq-&kC8W;UG!n& z%L*y@6SRl-P+i1yQaMCkk3|?x^WT~OTiA%a0#xA2nyrE6nTPj6ztBED*CrRxk7IWL6MN z{(tRbj-BDjCm&@FCuURKA_-cX1H|jkq)xtQZ?2%W`RUw=DCGvVbsG^s&icF7jZ%22 zO&)gz4?SXt95{3`K9q*A{A4soyoxSCuahyUkl$VuSZ59YNb zLYmThqa*Rx&%)xf9sWr|d5y-wUe19$M$f*e0GY0hY&)0S2-{Hp+!}~eWv(4UOPo=O zb~SUesKeu?vdJd37XU43X+RLmN3-}26U#aO6XcM^h+Zd`y1F*#?YPqcr5OG}3 z1(eMYlSgcG*(3>gTDuSgk|8pBSC{_9Hmf>eFqsHf<-z{rCXpv_82H#o&b#nydlNJ7 z;r$aJb;9sdhgO!!bxeY?W`1X~{vzp!%N<&R_z&KM`Nduy1*uHmP<8vu;}SP0ZFui3 zDdXMj#9J46cUd&B$Y_w@(pEGG?>%K|T6sM0svCE`>i=bSV%8+iMeDK>1R>nwN5Ans z``Vd7Gi}@MJ%}hA#VfvK^vnH}7R_N}^ zX?pa@CK`-I@skHY*Q-uLi}9O^{4)@{DRto8toADN(8fYEmE9w|6QO;lVvt%b#^0(; zSc3$D1K41X-y_!B`EPp@XhcI$q{G~0=sXgaCm|g=?Po)z)2Qjlq|(0ENq-mQW`31XNHyPg|ubkCS~NGIH; z>K0&h*iocID5vt528RE_mAuUAk(m7)VM%+`!;jHqNcDmyG2G@I&xzLVQtxf{PdwPe z7Tm11t3axrzgm?oiKRZiwl}Cmu3X8*$1YQP(nfWM)Ei-NBaGOmy`B&g^8_jzcIsAvhu14>$)r6j6lINhO;-krit435I!3QQEm;7vO5M8;)Y zv{cSTTu743nCnQRC`{a$luO*}M)e{pMQw9B%MqEntuX#g z0}<@@M?7MlfU7T2&P=k={IdA`uv(Jl%O^<)j|5lu#Kz*E1pJ3Un+Cxj+!iPVc8`CG zN|a6nKxX<%V_bK%8d=u3C99edaesqdO=y9*0|+-=tM6MWsftln2d9h$dkQDwd1Jxq z-d^&K_4`V#f243!k={@EY1A-jJ!$H&LB^Q^Sv$KGuSMtw&hp#`Vk$h|(=q>-qcuw~ zfOfq$S^aqR5+PUgNkZdRAPPmhYhEWuXY!^APB&ckG$qiHpRC97S3s70YVH+vw_PSE zF7Ro84?0_~<4ZYp?m4BCJ^uuuTO3-sibas2JcottvRz*-6T)4`s_dHPz# zAjSlR%U0^|V+fg*UC^azX5&r@V^3%JUX)udOf-MG!`lt?L(%nzXAlG~wuW|g2^+Ay_E z?wqNGIhE_z&8Wc9lf@mP`U(Q2r+0UufTFa(=hR`-_YB)osYU3or-C@tJMihwA3qc{ z*Y`0>l|thpKSX?^>oY!r3F5YG*`)s+H_CBV&Ktp*LF-+S$BCgL(;oD6JY|8I4IC#<&f-D0Juok603t4M8Npc;~ zk0&hd#V6QbAs9#3_whp;#~2b`1<8khq;S&}pc@$ZAMb{-8@H+&ChA?J*f%jf*&?6Y zUw`0JQl}dBYGR1bH`rmA|1}EpkDsjsEvf=>3!I%+eCno83#ebVo-?93UtrQ?ZzQ1% zQ@~;J8pqFtd%5Osb#=w{k+-s>0|LJqe_)~HY3BZyt;B9R_gO304P^L3#Fdg~dzE37 z1mN|%jXjeUtDKowss4z#PZ{`B=(EaGkxRZ~DE7O?<1DO%B99eBQoSJpQ2=~gtzdi} zFL!=2(M*^jMeQY9B7kT{#etGwc*9=88&}_yO|iUqC|aDmMd~O6v4oMWl1k5~bU-qQ z2nE61N`sGNkVGy3_j&KjPZ+sc2DI-TQRTid6hML|Vw{{M@*!l6p`&2=zf1%jB<sb%Tmo9m+b0nwm8;r!$BW0P; zidVle>VD4Eft^z*5qFK3^_o^A3WntPSlG;oE<gVvflj4oLUBPjb`qjd!MY!fpjOpl4Vfooh@x^7AKEY zUT!p}EO}mR8sdK%gj~H7!)NC=RMlvRzp0?~V?+J;8rcp5rCEE~MT@e8LI1c5|57q& zhIDutDd8453(~C`Qz~YA|6lEksNI$7EX(g>?KLp6!|cEQmsne{TKU!T6+uWFvd(yG zf^h-;UeV-lY6u262_?;Wh!m8TmU@O_e16@%q!~ zVSa+-UZnc@ibwC|?hd&-C+meB$?6e|+NAgz1t*xr2G(rdUcK0{t87Hw-xEYzr>6jh zRv1DQPWg(rWBt^8vW=wJhnE=F+z}MG-GZjN5&t;@7iqw9j_bdSrH%pLD02V+T792x z4`J43vUKiXvurL(#-~;q1maFjL2C)7v;FZwVx-him8dP(2X(8oAiL>T!kQHlup9@mR-~2*MfIwM> zOP@3T!S~>37{4W3!V_U;)Z$+dqApXyPuj)X$;G>6x2%t4aLjhA>hWqSR}&cwJh6E{ z__dnOp%EHdp?JhlChcKoTDFg9O0P58T)LKo(FF?T+Hm?#wm#%l=we2n($sWiUMx{z zfJ4~+Ca)qH-zfB6a-LTct*{M*YM;P;39o1Z77w&VNLAOkFaJozK&8~@>*cJm(`@+@ z#kMgZ0~Dw6pxrWPX-yF_H1dZC*;?PRA7m9oqUn8l0oBp#qHd7=9W-E#y-VwX38wJ|Y(cYd-zH%SO(2=0!4T z_qnJ&$Fx2ona90C5ynh%jeJ)z9Cb1r-Gy8tFQj^VkBk51J?F3}Q@(gnC#|?x&oFJ- zM}?7=kMKkS=;pfc&!>0(9!sgzJa6v0d_Il*dMaN-(xHdAcspiHR>pp`(7i3ZmC?saLdlHjBFq3F+! z>Z@Nuxc59YntsmlfnSTGWL=y}vGj)rf{qIPbEuwfaWLa0rhg;79~1#oP1_&+$T=!9Ez6#^XttIQcYEKjRUHT4m#9oz_)8>%Z#}yPe`D_zO4;vcsZlhy?zQMnUfPW2Jt| zBduPK+qb`t2ud{)5|6)^6H4r1U?(_UP~Z$UmSoG`+1p;~s4)lL;C zSBV900VTHyw@@D%4(qmHL6G*#(K&W6LNx?L`y7+hCio#dAt6HSdH&J4sh|vbXq&L8 zlJ|@Qlf8A6)4UD?5Z5>)F~-Xd2{8CRr%GF<((t&}&FY6;HGR(U9r-iG)Jn*^{Ljd< zh00UxaRPV!HB;8XnFyx-{)QVC2sWEg4GEF;YKjc)iNo&>fQdCC<1Hf9>Ob{GjW4t8 zdjPD3Od9dtIGah{$>^S_-D(2?;h!e(3?_7Jx78U?=18dsn{aE_@pZy+uxf`jlV|m` zUsuv;hTlNeXGbULxycZ&ry8DY46@(_BADuyuHXk zPNY5bgt=h19HA*?9%qCk3S$1Ac`3JckZ>nR>zavnZ}|^`YJWgUCPF9MTN5!}T?S9O z-RMc{-?8P2*0-B^V;eA_ux-;z0W2roSiP+xccM1m0#v$mM%CxCksWuyxs~GK!rL_= z()y{0horcw##PVZUMMRf#2%A3oa-(M&)+wb3sDLY@_DFl)J>_$9?hqD`>+gGI(> zMnPs`{#t4e@NdnF#tre>W=)J_xOApl7^)1JjKjaLrT;R_8rV$0mq5MPVlOfI&2Qs@ zmdA;@L#vmHxT}EZ{z7B&&3XE+L-~^$ajRk;Uv>VE&%=ZWi=K)z)-y@tMErPX^)x2K z2Qgi4_4-@Sgm5w&%u`=8f287tNZ;epapd6AEus^$Mltm=yr=rTT%<>5b|}t7t%M@A z!y&$kC=UE}?UYN8G*Cp*$ciiGEasDeBu&E1j}%oIcOJiV(lvi4WM$ik&lo0?p+& zCxHIdx=(ZYiGi8r?fGFp1DSyvX6~S3ox$jQ4sw#Ibe_11B$Q064G6>tP8ko>mT zHMp-&JTU(iwv3006b4I+bj6pb&a)(`$qF>QG1U^+{o?!6U=+T26ndMoz@tRsL~dc3ovM@-zAM+!`~}&O!7tGyLL@3TX$x z{iKsop^UXCj|d8t*W!z`KoK9WkT7FPK0Pwyk^lR56=ZtO+0P$~REbhJEUbQ)r#ojc z{Z!nK8)4k@lD zhs_X^;NIbmW^x8x#m_#>YDrU1;;9E}0uoJGB-(4#PW~gY()L&=H#2Vr80BsqV)&As zNyxaftRT(DU6rJZjp^Ls7kG(o7(I3K`nq5oh2ebldGix$&8Twy#5YNMjDQU8u9xqf z?ia{|O6`~egx?|B&rDm6CL!5Q!3GTou6u}@p5{&Op@b*54$<%Y6sgv+-Y$?}iZ+(n zwfudk(cb#n;L94zRzX$qA#pg=%lWHIXh1jx%gdrdz*V)p-Kp<_%ry3keW23b@9zKf zfjs(j4)e!BWR6Ga{(yKs@$P@f*oN~-JG@jx(&jyR#KBoEhujG*lvB6DlTIB7=PJyV zt}uI(E<_;9#Late5!uY(mM-YoQygE1Qk>@O@;4vcz6T~d|BlmSzl6}>FYS_5tTgh9 z!@)guB7bK@pJ(hgU$TmL!8wJzMx$@8wam8IcHtcgsV6s#t@VK_`y|~TD70PkTzNLT zu7`z2;i_MwByOFlbsVT%0vs8d${)^zihGgFjO-dkhn6xVgwN>I0Y>{c_4T z*j=R+c`9~i{_-f#3o_|?6acdk0CiaO&k5ZJr$zlwXY0Q~7W{6Kv+INwevxg$}e0}hg~7mU(${G#Hvk;^6vlSPUk4%PEF7I3Yr^ksh+$CC?0A{BM6}zc>^Z7r${q zBEppq-O6-c?A0fHq+^b*;QsmK_Gcb-oRavklcRi&?ADN%l4810Yh@+R4iz>wHtu+Z z1vL1FrBbey+|dItgR<890~NnZF5>q$)=?|~r0960;I|pG`fZ<*91gPfb0$(i)d%pW zVpGSFT}L$Qb$$VXu224`9CQVH)%H6IK*IKV*IqNOIiHT={265V@{r+#HpBP6mE(}w z>O5x$y;ub@%^?5jKdbxx-m^5X^G5P^vU7Q&eBURmtRVz97XKk$#fM3vFt`8l^HL)C$)D#6OtUET)q@N}U(=?x%@uV^Xv9)0ZsE>ZPS`}!#^z8r z5+{z(8c3EpIkoaZ z0Ct?Z)(;J9!FJbO>vw^W+71NqbP@-x<#bay!kK#S+s_~QfjD+K#=)DGqX_(uIliHl z^1Suyq;@CtRE}Q6G4#9&xSqTJ`J2CLn{dJeoLUDoOf+JzW|xE)V&)M7_E) zl!LXOQYPHgO|Im+_q?}1K(oVMZO5~W`2o%mR-J!~= zMtb`CUrSNh{!-`_f^e%I6bLtB_aWZ-xo`+yWmqb>AoKol1O zXu2N?%7V*6;u`QebZ3Jc3GtFA}>zL+u_W+&nBhM<9Hx%r#ru%7_VTX94?1s>uoQKnEp94v*) z-kxuLX;LquH3M7Q)gG=n60DITz1J#qy+Rdf2qI}>)TR2Jx2XNf<@XvUw$XCNZeMEd zexGp;3%Gy25_{v~fPxK;Mg5IT^YE&}(j)b;)ll|a=u@2YCg6V&Ex)K&kDQ1+N znJE%Kn_^NZft6%_dDIHzAoVY?&tvXGeW#B~kk*dw&%;oKtB;aCge-jxV<+}-=9@(< zV~83<%V~Ova_P8CK5PR%BrnNZFm4|O5{i$Nc=RCovD$ZV^Cn6O&QG7;Nh^-7zcpl{=v<1MzfsGW6 zmb8&FO?koJ6JHduo9tO0D@rbM{f;qeMsB}@wR<%LyI`8zfWsRQ_Zc|v|4{XpVNrJ9 z`#5~f3=G{J(%mgc4I$ki-Jo=X(hQAAOQ;|N2m%5UB7$@ZqS7Iql2Q@_3^V_^Ki}hb zJjXNdX5P$o?Y-AN*E-i)=Q3*{C@LCeZ>&?U%dvj=Vy^a6_9O9v)Mo?~`};B3f`3UG z`rm>Ns)@^^t*_^<7T8YK7=jPk;VYayf|zXHo=JAN{F0LT5W_ue;a_`3+Ea_;6+86q z;UTZSHrH!5ur+9Nv&dVa{zUi1Z{CP!&=q_(*^swUS1e~=c*N4^5~lc$uzFvyMoUTB zb?AZvGnvw}ryhD2N!@%|Jo^i6%|yTRrBA>ob3l?u;m(uioc0vDf2Yy5ij?y&W9eii zjw8C#376J;fmnaGKJ8y6`-J*!#Y0Ah)`nGh6^-nG)ukTfQO3Z>f#MYIzrm({Ko32* zY3LyEr1ILVc%YS=CBrjD=Jy?1&h79dw50CLO(Ev8)^`~&pd2Zp<4e668PU$3Q@@z1 z#T~ODoA)p8rtKMeM_F9w1~BDIuBdN)Ry(OvIIa+1|RDX#Wcn%u~8hMZ7ib%?6Aew8iJSMfRv!>hksOGV!G-h)_=&$ zF9{3}Fp`VBdBegtUNhO{w34B}y7Ths;Z?=|s|AR(H2Zb$DNu|P8aEhWsXZ~jP5~-? z8(RO^azx!cvS1H{|3q8m>Df&p^xEZ{Vj2Ch2EproSx2kHh=&XMBw32sqCOw4ROR~d zqq95u*s+fA)3XYz>>YIGcI8EZw!!j_q|+OxD*gU8)|u~0tm774JlT&$ey#wKJ*%Y8 z@Gca-PN!$NkUBaHXG2;sjqO_n z!i1Sv6)-H7i_TF^g~)#?q#7-6Y_y6v&!i9Y{#qxtVi(Lz+aV_ca#?=c7q$9o5h=>CONwA?m=#rXlZ`OUtmaz^Z|Kw zfIcv%-Umj7QaK8K;HSPXQc>iXQv-L#Vtb14qUKLY9TZYb5R&!^F)IuY7oXX`Q8?+H z=ch4zC6yO18buJP$_VZxXy6tSqM)sDr7+T|4^M%v}Vv=t4xSVDBRe3FCknaa`V$=vTC zQ-U*XX(M?e_|-wd7%^={&EHiE1hM+^^QCjj zpK9kG3(YjmcPVaHowKm!rZMIC6lKnoKq!z`Z`CXZ1!80XHID3u_r5IgWPs~Vy!yrG z*_@0NQl+dT1HZCJTGpiX;+-s_`&UZj_4^0x~IxSMlrgQ>*fCF zu;hDPu6#sqxbQX~PZO}-gGzVyt@0Q0W#63EkaUADI-B1`2c8A-cEloJ; zjYRGSe1H3hO`iQk$cfC^wXjlc6`jsZReoP%JRb#6ef3Z0MCqQkaSijwQ7XQ^beyb@ z;%5c#K7N{PlgxdEagUImzANfNu{n+aVogudiZxR)!}$Mcx+x-s>EU(9NAlSA-c|dj z)i|`8CZT>f<1c^gF5mN|SSYGC&3#0T`7V2c^sbda){J}3#%4*CoTOnPR$NP}dVAsf>f6<4|q2*-}INuR6Y)Y_jzL4Z!ycgTI|as_{uitW zB**kaL?A6pCj`;$Gpb%evQusS_qrCKR?zp%qnvFryr6>r)q|To6KzogQRNxpe(!yB z5}!19OR7l>{;2mvZyZW&wf*(8=h8X|5ES^zd0iDv^|lkX&j2u#ycDp>H5!8m zHRWiH*qdy;HvnZpof|xRAV2WX#P;@eeWR2gH$3A(jaOj7aCeR7C3BZ_abMQAEHva4 z#izVL(8mb$=gDF*{Udzsvdio4)Qc&x$AaVu*(}bS4Km>T*VeqUDn6zowd@VZ@MR5xCC z`#VXdO%;Mra)@I2(*cIJm@FunTDofyR0C*qv)fwZ~2(T#jPQ1{_B6P7)KvAp;lb<_QIy5 zYUGtxHu`HqTRu%pr;o0?8O?`CWZ%zsrHgsX;SK%Euw`(=G+@L~cGK}M!qD6K^=Xu>@fGKG2s=EaSQ@vwG6X_vGZc=~fI&SE#MmcI4F_YxAI zKVCq@YS^5Zb%T<|wZmkoGSSi>27u+h8_s+;*uPtBp{&rC=SOmhYnc zq?Gat6ZnYuk7r2r5a)IT^|9sAPkxNPtu3+mwiDICa)nq^7JF+h$nJ)SlO^$&O`@JPV7p-jYg(5wIT8rzw0G0Akx z64tL?!?+#WIr~~QNtTKRBCc+iA`bHZwk-M(J(~h!E2C8L8NMpQh^_0O3A+Tuuh_%nlHYqq{ki}X?Y$wsDHt+L+YIH&rhcj zEnOs>5t`qhYIZ2f*EVEu#xUEr$hMGxW!?CCzP@PkKqZuM4C7kl#+Z^EEn${V zul=K=(KlGf%lWgl9g*mp9BcrTCPA|CxJtBokfMt*fgDaP9Jr8JXtdNxIR5SagA!)Tae4?*_mTYjSxg+?E$oD1 zy{zHIRnnqr?|c^hAyQ68%LT$l%0Bw-m%t4O)YYjj`kqTm@8E4gQCn*oGq&e1LTOXW8(rhVybxCaur6Zvq3*VQzHx4`jf5 zRKssA-&*8jO0G(Nt_*AaZDszTN5ME8mh&*w!vmm3#c^=4mw z-4N>gCoQTs{+@}L`efEB{80V=tP|KNHal!ux=8JD6l+0h*fVu-u@WFup zj)6|j#|F-HrB3mvKJt>4;>Sk+KKMq|4(OWf2GNW}cPSlpbMGZ8y;hyL*SEV-8}vGK zR8xInC}moPGAzf#xx})=zQn)OoS^aI}wUD9o%!=w>Q{+yWd`rGe?10BBYVG z-b`XdyLTW|Cj`LSfnJS%dw6F-OgjO0kTnIZ{DFepxx0gH=6-%Ikj(_Yt&kyW`bo)Ls+Q&q$zMu8%6Mhr<^4DWlk#tbh zH_WVOwhJpb<_6NYdR;HNf6r*;_n}CEZ#cSVo5X3^thOz;m=rU|zL+5|!qO`@yz$wPr${lhvi8EDS8w=+6nzY_hrC!> zzczhRtB4CuD;u14z@-^ar}*(T3jCT~mOjCBMx_q>I5#ucK0UwYTYXs>^gGNEIOco= z?PoSe>aPAY4u!CaTuR8@A)?e*QVrKY5|Hm@pM0AK#6{QAAR?7wq}ZYX@WXXAk~HKc za#r6;-tgvr0G|Pyd)|$5_su8-JsM-NcqxKx#~g5@k5`u+(tFhaq}`B~c361#{fkNs zzVo`fB=@8RSNJ@rA3z~uj>Gd@?lCDxt0JGFeLr_yJlG&h^E>{!7!|n1IzI*g-XHzD z?71-NW))2Sn-5MV`WZDJi1^M%(FScgd&pxHK_`z^{KLXLRy!iDZf1WLK2v@2lun-+ z$Al#@F1DoMxTQ7Z(_d^T@$cr_(v1t}!v{aTY4);ez{PA1wz|=z7sOId%U@=1dEbq@JzN|dR zy0mn?t`*F@h`@!!y>mdNJF>>?OY6>gF#tzWXzUXEnjK;AIAP9g&NKJ}cbR|RBcq(; zjWv*SH6U3vfBn;Ve^5x-s)3brWjy~G8(RF7ZFz%CtN%Isu2EeP9cnGB3x zTF}?Asd5(i&_Z*}E}g41iZ1{Z^sHcdNAEnudaE9gUo(L46WFciucdtmxxV?Mk=hxG zH1V?ad~zm($B~9h%jCFzu7Gl8@jf@1yRBi`y}ai+8#u7Nh}VwUx6dq3bE0b_<(nHj zhgzHxj@h_TD@rmPC{AhM*mGVeuNs6zHL>)1vv<3AgUI*jzdZhX6rAFnqRBtL`Zd~@ zlDDMf{ZFUKKlEl+jelpoTsx)aaQ7RZ@A~ZrB80B-_UQq57@gC1a-+OY!KWK_4#_1r z!bK|4Z5jc9{aZvgYHi|Oy@7`C&u`KMBW^kNRMW^PCD`wZpP{B*)e3T9W0{>#J7G7x z1=ZtX)sFbXlONt~%T&k;3AE50Em;|E2`|w{+&5V5AS^ze|w4wVzUF2%S zdspYj3>RG(p;Q4&JF`fCE+d=(<1Gz1r8B~sM+Z{!q6?QqxmQ1)_gB`n-_LcnNjzt` z{diBwX31^ozHVHtxzW@RWKMqCxRLnTo6X_kh2!#i@1H95LBo+Def5?vijcZoOW#Ff ziYp2~efm_8GhX0puMMtv+p+1$w9y?95p8py*+m4q_5Y40%M(+ZnsL)W3+6OI#$T2L z*1EM~Io<{w4o&$SFIJY2;{Q;zfb>LQ@D8-Q$N=}wQpZ>IP`1zw#M_s|sCqJ4e5$>d zVmRO2-o}YLsuyFyX`owT2KSfElYJQGa=EDtgUxrtE=VauN2p zi@%KZfj8A-_F50Gh|THaw2fe+`Z}#M$MmbCeq0(RRBIG*hO3RbRX4>-jLY3IoU4bm zHo}55{ss5XYbT1hyT&T*!~9i^LEUK866-8FVOIHB;*_UA)2RJM2sV6r%@7r9dQ%B~;JIWFfIe zB%zGZKJrSSc>=$O=A|i!r+a`#)5VTwkf;3kqLBiAhjt}#PZ@}imj;}}^Y0f0s*+^U z6AyT9n6bSUL3`^lT!n1B`8iAT*TsPK>FO>j=+aGE<)>1JsPN|}iY@pCtIVfv%+4z{ z@%BabjDZhxhm|mYmPLu&0A!^)OAuE2=663H8A~!mhZE-Lb8Qj*TYlm!>ZA4L_x_$U zcuZ4l+VCy&h6BgNwS>OZuUZJtzYleW6GW;5=iau_^p%LPT}UGyRb)MW);BI?fu7+# z@Man)zmgK5lm-wldv}roa8~hW;M|l8@be|#V3-UMJSNo3@FmY!jV#D@^G}dkfL>;T z9cA>@y$|J3z;QAR`YyW$hOCwqV$093p-aZi-7ly(Cb$+h^G=SpYx%A-=lVXEvgtRN z8tvz=L18LG@#weEw{Y7P$SXj1~yKu^sRz{I0yLirNgm3lt{a z6vkF~{B!*UzDC%ts2^Ujp<%C4nL~F|GM`t7eX3L2s+P^Ql%!ZDAg@;RPs3PrWn!tnSl@@iR7&!6)US zZgVU`DU-6mgcuh=j1=i%9TfO7qVEYb+7z=WJ+La|CQO1$b^6;rw4m=?%~zV6+}W#W z6S2uWLLRV_0CFE3^gieQ(&(#cI^O|SfL7cXtTcOg21-}WyGcWaEJGkwEyZ&kPpRZ z+GET}96NWSfrvAT4oqMYTx9@1#S{Y5(4cQW8^eoI;e-qEq|mF z-cTFh;>_inv#nO)FzkFmraZeX1sK+Og-W5r#2}3##4}E|LMm9K|4;&A@zXXq;T0&P z#3#aHy3P0bo4u5`V<#eYwi));bKr~JjB{P)DWF7_B@hB1nN`L+31XMbj=KBv^ZLJj z0J7P*q~56A+6&f46vTv6Wb4$|X4A2;BM+4Rk>5z~WofyNOl{e^t~{~H@mp@J9PU<~ z0Ll8#i#g%&9mVfnu;TbNh4K6j5$9<@#r}SIGK^O?wnMV`PNb)#gc4Xw(hW}pwrZh! z@!j)5?MMCZ=F_a_3_6MuM|kCwM(VV~pP%4K!c<7s7M61vfvhBW6_l??mK?3B@=XIC zFfphZT>8GsC*uBYNMUlmq9?AXh50F$bvT6IS^Z(%#`Lut@_Q7l#F$@^btye*Fi?R9?}Ch_Boa#C zU6DtYT=w@*x8wqJI^FX`IRv_hs3SO}!CXZVxkq)DFjZvbG@yka^W*2`htP8^WaSLt zv0^Eb-SSHoR%RP^C#bgA;;ij{`}Ga>jm9C1u=?XNkG(mpE}JED-|sYn{uF)5bxU2U=f>DD;s(0( zGsnY5ePE?o{!_P&)2vF(#izz-MeQ@8LWduy0_4u~NlO5=^PJr9p4NK#(LIWELIbNF z5Z*{-1fFsVgep~4ohd<*qJj7RPzuFBs%!XmrKJPDrsmma_BUFZyLRe*>GMMmtv^EX z|HS@~1sv-bvCdC)efDDxYIi;4;P27Tv^IPwQ@oo-Q$A{<5_t&RIf1`vY_#oHE)W?z zU(W^nIXpb{7UPK)kR#j^lRd_Wwf^uXA0CPn$cWeXgp(T)R1flwD5}i={9HH~t?e$A z%MuiMcB*t8Xs_hDnB)~=@3ipEgqi3`2uaPs{k~5Eigz%se}M~+R{qEmiE4sr&Qcw| z)u*w_IQkP9&h1gcpDmG94v}* z^grJ<61$Ybr{<7^^VZD=(lC?p@*k!io97i+^LqB*F|_}>i#>%#yT5?(hY-t;q&aGN z#Ndu-&>BBHQ}%TE_KMDLwKF2>-=Ck0Li*eEz$_Ca%>v{|uX@7I`Gly-Iy>n&Xr`$! z*w{ZoO7}_b>lfN!gPxO_ZX+e98uwOxe$up5pKbIvWz%Cv2AS;SncMrVr~BH>q>dos zfQ4QVx}c1aP0wO?h7joU2gEsgYk-Z*?EZ@q=|{Di`k5i+*CVHi>}=Y)MaL^epX(Xf zv@;VB1J^k@Tprr7VjuKJWXUHzitKS0T4)F#+6B7-M(Xr0$M#7b(FI$Q>JR`VChE}n zpgjaTMjKRo9`O)J0u4o=f8UCdfF-m_%4CLc6hiMzuaejfHX~FLx3#?}E`HzJYcxoG zJg#fA?zTvYl}U2-3AG?6Cs2G&4Zi^c0=&YNY zWr@UF>NK?>u$1g|m!GchEFsZ+FTXR2`4ufKgaJz?9(bAn?~lF*B{$odNLD7F#uJ8m1P0s6R++KIlVJQ z2pC`p;N8SdqRR5}5!ZQS$|(Aw9!sVz3``22-leFLPm=IeYO%BDosxw}xGGX@;Z5+- zuAczj^TNz|tNq!z*~b;ssUatCl?fvShw$OVw7XhVVIlB;4G$gxh9LTAxihw~rsL(B z>s+{gcR$Tb{H@~QM$au$IMPXb1_-o#q03YQioC&|CoR!xkb3x86qRmno}ihhDhUif z3Gi7YOe~{0e+(Gq!Z!RoJ99Cg6#o96suWFfKn;#hd;-|EFls3h(mmFNX^-nX0HZV~ zq{GW|`}b47`wK$MbkdzY+wSk$j;6NlR#cEbJnk;kW}^(lcV^wqUQ=_r1THWC3G!eb zo=;`CfXzN=4!ZCQsGZB0^pWlS97_o^Kvudy%ELC9BK44MithzL-O?UQx#b1;%Es$9 zDrcyRQf%yoJ^G3EBBr7jveaKCfZ&7IwaGpr6HLGuNFffR&=q}qdDnh*ylRJ~#*(so zF&&kN`Nd=Tj3GQIu@3w@jR(!lsBaUE>B6DBwUwcBV5o+tDE>V@w8!<0AayD}$61q? z`9-@p2A2B*_Yv$5D&b*hJ$WV#K8t}Cv%vqW1#sw(wxWRayp)TIJtr~`r=C>d;Y0HA zO%!Raf-M*}1+Fe!D0;=p(+TdO(;0#-S~8UqoJ zZ1PBFPIEM@)pntSM!&QGx~Qv2n(&br0GdJG$&=5l1#pz&v7YHUmIrwob%H!i?!7(k z)X=*GiE?&!1QW(`>#hjUHUdUXPliTR(tC?r{kuCDd&29#o1KR9qW2#g|9EKV!yUSL zH|3Ycs~#WAy^(##<|1YZhDPh2v=87C;YhqIErmfh>nn}T5L_X_x(k~0oUFV_Tc_uP zOoN_O-HiE9K7asLF8?G(csxi$K5YL&v$&XAUby20$y#?>!rsl_*p=>fUDf(Uh4a9- z;nm+t$KQfVlA%Uzq>B$$w)d1FCv@9GblBzu)JZ&aN+db3hR6Sc0^+)Mz8Ti?{SM?B09&^F4yZ>ASKDS zwve)JDyUV;DSD;yJ_o86+E6m!+Y<}%VV8kRLC-_4BWhndGn`MGYtj4R(PIuCsdDfD zcz2&9WPr+cNNh4ljGcoYvvZYXS#O>BHM!lSD(tVuz#P>)M>4mv7tZ`|QDqT(ytQt- zb(a~zM$Hqb?Y)MW7k)8&=310ly- zcs=T)OvHR$;m#k}bYe*fa=wsXAKwm6lWKe*Q^%7bgEWYJR3uQ#Ic-8o8Kh$}C@@Xh zJzSo^l_U$TW8k%o(&2zs&e$i56d9tqA(u$tRM)pq{(btx%e@(bycVBCRbz%_27sDk zsQlfk%;RsCBrtygxI-3$Ls$64*+hSGCe%xl5b#&uJ8`;NiFu3bXw`5Y=L**F1gJU$ zUh6pa$&$-o@P{8}Hv5>H@UQq^eRiJ4oG@WjQhJWkp$etp^K&m-VSg;0-xjqV zO}$59g2|5_ip0A)dw0Hb9Q*}>J36_asG#Q&exQT+xuj#$X;9iU)=4h#go~*szEEXm zf>&Uo$769RWeh)dwi{;zFzU+HB%(A-mg)qWeU71>zW>Ib10{vw%!JsNX$Dbee>z?Y zcS(ZzhwmW~ZDfi>+UM0>LY>((w>oO)tk-vO{*`_CpPkbOp?NM`rS{ zpgz_P4v88HlW!j$Dhr?6-p3;Aw0U3X4s40Dk(bgGCvNo=f5>jEv`;)vpvQ_K{wjtV z5B7?J`fY47=CqTC|DD^rCR{5>yLGRBp|}F`&xDQ>4Q@+FU=t{Yv>H0B2UdlxnVVwe zp9)2SS-rjEsr#?F&oEj?1hY3G;se={_*`d1o?7O7)WZLHTO4tg&-NS#9E$VR{`}ko z#y?@lZlFVKkV7Z!c%0icJ5JRRij4)y%e_L^lYoeyh-sV_{XC z)ovvI>LECSy!&CmXV0lX1yn*fK83Pp@mjCH(kSM9#0=l>(hg_%w(vl90Xr;N`_)}* zj0<2%#Ax0CwWNsntH$SqS3oh;6a>oh1hQfo%J7s9`JdoA?!*%a&>|8y@sTY$lcBva zDO)T!mQ0yDxsU)>rVc9U#SL{7&ft^(f4DhU)uRm}&n(!!YAUA$;F7>Gu(WYrIad7k z<>vyZrONPadcn~)2opzHGHFp;aBE95LaxP*p}T~yMTvX3J?~(I*D_c467A)YHZnJB zmKjN9?*9F;QuF!>(KpvefvN(Fjrgg4)O$J(dmn1I%HA0((dYNP2oHghF+|EuDlu$o zHA^@2uPri(wMaZ4Qvax|f|h>*i~hq%r2$F^&QR>#Rx6s#A!C>(80i>n6t*eU!L%LD zHhqtsYAyCmGm%Qdav)gfB*#K0pQI4S6{?^EE!CLb{nY_L{?2-x@?XFijaliu)kCUr z?K%Oq_i8}Eh{%=Z=vABZ@0X2PO0i)vWP2O#eGN8by)M7K^ z5Tsn#65SZ0JKP_Gj$*kQvyWEeIB_t^2`24&vg7}8`@?&Tz8ncJSk%_9u3pLbs)=#)<~g1CNrddqq7>Z!ZBS&yOc0S%Dv znEv9{j|2o6kRhUX!`C8K&o5$xGnI=QJo8H`v40Tx*TrNY~!rC^UD5ihOH8y+UDQHb>`#l_brZNnfL?|mo8iQ+^GKi|n zFiE?0_!U#Js*z2Z<8l63%xJ=jwWa!Y++%4N8ZRO0gX&_7YH3v(_N7vBcy4X3gDE{^ zMg)=P7$#slutuo8tV;uvVys6=J?bOYDJ|m9*<; zkdY#lp07__iEBYgPWUT{x?WC6y@fl++VG&H~C%?i~^= z=x5+iWm8rc?Yf95G22Jb;}0}I?LUU=e-06(RTqtF#~wYt|BhZM$gDQ*{9HFf-16W@RUDp8a8D*MX;I|r>|HWYaRB1;DUyEWe9 zZs}7$wDg(-I&X7j+^eQ#F?XW@`I;;ib2*ne=ZH?mEU#r^wCmng;6(3Rv1W(D21rgc zaFja>%|NXT;0DYRfU}`Ecl?7{*|KA<>5;ArI&KhvYiDJ8ZPd;f0YNLQ}0$fFCtT`4(dv7J zaQHVJ*cm}<1@-LPf4I-ORf*Y<;2yllC2Xv?Xn`HU){a#O`B^QF4SMZqm5}1~ppxcy zW@2EYe359jWZZIr*jFCe(d{yni5s$PXC@vm6|Tg@wH$oKwi@SDczh(B$NGVU#;i_s ztaoV{gG=>Qj*?Om=j_;+^+U)I5RZVuX8;HpH`jfko_pUM zOG2X&uVh5d)C@wOakw=%by%dWjQNZV;-+x1ZkZA4oh>&deojdO3pzul+shYPo+a4J zBsw!wjW;@xitlWc71d*1ktXWeHphM-8kGi=i>UA^z>*N~U;!DpFd<~AoE zH4>?LQ2j*3CwRPtS!tWyuOdY16XgRV0=wCZPK=WeTwyptb#`%fMm$D;`Hy4jFT z)GLdIVW~zS|F9rw*oDrnjjQq@MuyigCq1!nhz#d`*=n79pEBH0CEklyikKcH1VAX< z=^blBpde-M9(q>YKDH`wno(^wLlfgbriHhc$xlkAT29Q8eZ~1I=1CGjDGqph{C?06 zv%`YIy^Cbq%OrbZz5j7rqd-UUnbr8V!1P+`*mR@&8fYOt}nh*b@l1=$|CJI2XYVT1&) zqAyj60fPdSyRG_4jKte*41XhKH=2t%%I*Vb`u-fiTr#Trs<)b;h!H5!?OyE>i7pNNS5xW}P)OCA+J>Oa73ea4aON9F z0Yjfk#j>-NeD3>qoG+?P525W|Abfc5oHBl7MzvcYXaXDmU!rO>RnAlvnM`hub3;!H zvY^l(G{7}E0E7~(vF}7(9ledZxVdPzx$n2a=7GPa)e1l0TX>Vxdh_p)h0^Q_O-n{Y z0&?bx2`)+T$v!d7r;Qo%1fm87sNcqGN8RZ77n)RJJL6W}h+RJZzGaQRQ~LbE<~lU> z?>cw~L=U(S6zCn}0kb67)uicx*`tr&VP1Y->2P&~)`Qu)e|5*-o)Jesg^%jaz2EYO z1EtWEyF|0M8_~ZHj%~ySePoDn8^p;=LQL;jvmE;kk^@XGkZI2>Pc{po40}e)|2ff; z03RjxKuXJwb#q-`e;IUL)D?i!0W2>fLr+oH2R07Z-$}I5LAveEa^<$l7YTX{U=7jl z{MX8^D`2XsxE7KJgoC}t#i5T&g9t+CB8&&?oeTvA`N3iGHAH;$xLeOkpD+lr9)Bp^ z`21-C45vwQ#}Q)=Tdn>%Y>a*4Wn?*HE2r_G+`?4nBJG8-|_h$Nc#b^5TF zkcUA)U(vVoqlq0s#kHA9AXkDR;$QEMzMW2qIi5`2w1q%|JeFl+Y#(d_D1Wf*u`;pa z2#KIrmp((kT~WFES}V2vp{N3?ni1Z?F8Cc|ml#I~2;Dt{#_6UWcoXAqA+G8}><;ej z>!{Q(>g^_e&A~F|B_wjO*gJ$1c63)@r{n<)#{(aMqUW0+pja{D651~t19?T%ju6^Iyu?(C^wY~6GN67 zVE*IEbF@Et2q{_5Goh;*&Ph{A)`awW~V^RPg zEDhANwK`L zM`^Y&>QanQn7SWmDgHhsRzzX^0Bd^=Sf=y=-Zuh?uO`U>hRxB4ntzz=AIc6i(=j^e zOg$2@!OIzN18qS;&+3g~o?Wp-^qs>OWFf|xm*hw*YvN9`9;d%tz}J5z#;|Fg&L)gL zeq~}ao(51j|1tPBU?sXKW1dQ=Hee-2o>;7v*!V78oSqo(mb2T}fA8BrCd2de&nvgb z2F;8cfeDCo7^yPFJ&H42gEWBL6L5y8u#k^$Z}WpH2;n3+pDRJrEpm2JO(4VorFK zqf@`o70irP8l5NjI*Wyvr2o%OSV^qf)eFbZq6`%O60tQxbDbf;^H}(RYCsjt!xFZA zuzK{@tQp_O@1jQqqwuz1vxc9i(th^)areTzA0HX5yMwU<;abo0eHf4}7vZ3Vma91X z`-#q!{}EcLH3a71T!NSnlm$1e1>q%?k0woRDS@6{a z50scJXb`Xu-`oA6UECFq_`I)rNyzV{mZK}8ryRVz!reA>4? z<^st&okAzJLG+FA_^74*J%pibL$v`D&>7rwg=d!n-jVn9&sA7E19f?SFpwA zh8ZFKuZ?0C+?R_v|9d>mgRAPcjTf%~Jyb~K$1>X5&#hYoK;l97#g1*MEPi;H0d{v{ z$MZk{Tn1hfhb6$b#et#BD>`kbS6w4#Ut z)2!2T{HV=u=H##5IClM#Zc+{LrTr3CrUi*~6JpbKxXKnxcr~yos?>+2B!>cJSfs*Q z;A7pwiTZ|*%L*?4x3}1ET{t3%=HEE#)hYLP`@j*m1m*JUL>Tf@`g8UY zY9=7iDW>&T#OQsgvP|o9(*5_H{Mgne{|XiVe!wx36Mqv(;>`cR(d;9buP*wxrXZXd z|9_?|NMxFVI*tduK@HNUL13X?gn_JPU{(D#cV@V~{nGy#-ae`4blca4Eg@KvvgP|r z1*&h5>RyvZvE~pv{a|c9j<;t<1&g##e(P&*xKqylVz{H$)|whnWt8VHlxQe{Rz1%@ zu2lbTGZiCwtwp<4jj3FPhh_i1Ib9Wq(x6U#y}RwSSHXg(YJuGWte9ii_${+z8UtC# zxg?m{1%hltVM!X_#$5Y1j7ENN)lyj!DP^ zHUOI>#IAoy@&lpt{Q3!#$E^C;8b(LBi!}VSF=rq&22nSk{e2UdP6N#w<5fOUT-W_Q7DVfEme=(?(6F-b#0a3m?W6TjJHu=(l zpXTI?;4Z*Qf4sxIkiw&2aulF1aTd4jh)RD}Z_gEQFEm)AiByk~Aje0tL4U=kxQAk5 zj2M#ZGdi{G#|?p(DOVZ0iK6~?M?a6bsAst>TD?AncVja5arQVFnVQ~GWE;g(qh`9* zkzU<_Md)7^EFO#ILNDwWEO=*R=^sb}=Mz~m*L0+Q!^oQ}C~8Q#2!TQ0TdT(eB}U+L14JTNqQvpvIn9-j55nOdEdx@%Kc+PR~V%l5;QE!3~D0*px#S`FP+?S#6zmR0?yRre>s!TWhGcA~B^~LnuP}rh z{M%9HK`l5;JWEn7-NpMqSb=I%j`jkYEwSWl_2p1LdOU#3l;C|f_@K0)>(O?&HwT7$ zLz$6BETHDYQtu(^<&48KyZ2A2_ht$;MgP`OhLfLH8CObAw>zkB`Aq4421_#vJkSu; zZ~yPKf0F==E-kKxQ~x%OhD>>NGSPnZ9m=gh57EEsaM|iao1VpR2E8Z+mm(F#lBm8> zUypU77pp)r6{1ip9svdL>)`uTrS3uUT-KfQH;SG_(#LehXDdL~NSbHSyQ6jLR8y7z z`zOL=DL_aKNyKaHO}33mR$D%hIok3E$~z>@h@qA_dLPqCj4&U)$a_4fN4govNPrl7bB^y$J;zgb3Y(E?KZP0McnS3e!D;*o+Ggrx>8>Z}`-;aK_6Tc@( z`Kxm5VN(^}EenNP)|2=+U?H4W$O)KFIzNzqR9c0jiBBXBtW`pPJb3sd?u1FN8bljz z8!_%GnV)(Lm$*i7plwJXuRgQ)(_Z}ec8<**NGP)3^@$mg*jb9&h{pK1KPyu->vBN9 ze($)Z@t7gIrT((y%A52JhB^HFzv$vU$nh6lYi{L-D+i{Y*pj*MXF}^d`Px)RCN6p@ zJ+u$Bpkl5@W{|Dwig_S!T-Wr2T_y~rpPyd`saAo#=N|Gs5-<#HdCj;=58-0VFY<3CfOyS9ih*cp+ zzgk6}X1hqk_}wRiy@9CU0*w(b#3=gTJa8Rdm<0BIutTlWfB7$AfRAnHoYz3kR1Ew> z7O?@_b##Yse1OAFvX8Re|6eUYfLHUf0QIMd(W}o`8z7V>1`h1M6sE+4aAI+Nx#9l| zA-~=CiIjP`oj35EvrgQmhz(nv4dXuF`v0i^&~`{*KT0_x(K{KY#ekAI>@N@p?Vi>*-^= zOISfGpybCDO62aE$yfJI6y>!Q9LHt`Dd}0_@uvRzWqW;# z0p7h2Kh?yd&NU{K1U6vk=|*j9Mx=zn%kg9OAk@kH)|cs3;Tf^BRK+9jLd!%JG=*3c zm!HziXkknGu7;Vu!lOm%#mX{kKamMD7IyP0HnfX)LL|hS0~@CYureOxOvQc90t8wC zfqY2tq*fg9n(=saIu&@BjlJx!SqdffI!d>TnCI0}9(5(c%@n4I0Rg8;TiZMhGw1_q zsYljH#;Zr$j6wWj9==Ye1AWF5^`^Dx@OWjhKJD|R4f;S5PP|>&?-eC}D4ZQ{%pw28-jnG-TnuF=&;8g9t#Qd#cCF7DdgGxa9-Nb6jmuQ)p-iR9> zC5xQnF&n$@!W`GVM=KgNTcCB(S^aik3pfA(N< zet&vzSlYM*yF;q7;F#np!?((zZ38q)+jlUD`r2lT?>x7mTX~P81)(1pD`Tpir*jh^ zZhbN=Z~HYxoF8g;Vmy>qw)?a&4|yvLn8ws2h8q$+J7r(g_Ez?6r~=Q2upc{|*a-=^ zT&D_^Ea#CSKs@nusV5~8Lixs$g*E>6n;UJt^*pButkBnNNS;$T0Ec}eKgMWL>%to9 z$!QFt=hJz(UAi{OwoC49@iFrfxi;dX{oI;Y%}#3aC};2j0wF_BiSd$5Pbxh=HRK{l zdVia2+wvU`<(0;8Lxc~w{S#rn{s?I1`39@Bfg}kZFQHaRA90R9Z`5D!nT~SE+m=_; zLNPqh3_R2OWy0Qt$VvPIw7%8yc}%VQQQetq?^m^@h(UDiz}hGJ%u;_l+*7>=z`X54 z2#`D<`aGFEpNT*8VZB&!qpHVsPo2%5V|iDgTLosThO5(-$FwN$^m4sMdS@Fo{lzCbZd7q1eUqkn{ zA~GuHPJX_y@Mgv{tt8s6MgrbSozeyn4vNPqa|?fqt_)bISx zODthTF7zKo*^XvqyVxGm@A=Yi#gyi|>Va>=@B5GzT$>OvwB!Q$`b<4|BqlUr_s7lN zvUJO}6dEn2^;Kt+P9LE-v20z6^> ziHXM?r%m&oC1z**{;07^y;&dx7*j9?@I1-euwatJkgQrc>C#C}OMMF@AiU+w+YVo_ zoFkE@qR5v`0+Id_tEV(K%v&BbHfZj&b#pLdDS;17-&e=RWSrjbt{blz+B~-!WCo5l zcri;V&g0=rFOH01Bbz?bV?)LwyL!KR-2a`|%~41!-e(7O`{X(pd@pQuDXDxRCPwBZ z^Y{l`sJtg&g={}tl7fa^v*1A74Bx@@eUE$9$K=k2k9zc-lV@|SRc?JCz+1)fbs`lQ zMe;|@bd;=x7`GRl^7)e__~-p!wGZ!S$qDLKY4(ccI2vS)P+fWLEmH<&O=z5mF#|c= zz(Y#Fmh8nzV{Nz0(>GU`0mtFdtTTC5D@n`ETYJQOst{oA*6n8MJ4Fm5NRmxSxVCU~ zko#Quvvk2tOu9?{JC`HzyJ2cyDtKxi0FRZ{JH;Ypc1Zaa*K&M4ME0!JCg4V8y$_My z(}DY6dkq}TA`4A1Z$4@K&;!74mokvsNlKtcPhY2OqY-azK_R8cu&F9Yg?xz;s%E|@ z_>aw?NZ0v*r|QJ%h^JZ46^F^G>ycfE;JB_^k99W7suzshvW1 zZkXA;(xuSJoPUMk^E!A~ZOUPGJ%kLQiq=e=w+A(he_Fm(2k?AQ*7xMS@$7KUpj&F` zWw8{L*PHKy-XkLsfYFq`(~SLUPuFLI+hboB_$m~AwSRtN)Z0nV7Q8T^^xwB9j3pa8 z+F$?hI7^P|fExOU4O-gED!!kqldn<+gTq*zROVO#5t8Q^d`YG`(59_Lj^t@!%Kf6R-=xp}cfn7fz4M|o z<>8``k3flK8b2C{b0OUces6bXyjdu1+dvH0dO=#X`}CX+9bbIsZ(lFD0gx(Cbse={ z8f|&uNEuL-ui`?Q18{aO1uq!Hq6jW7{?K{eWm{Hh6eLq^ z=NZZ|9tt`Gl1@#4#Gif8OkIu(y}$d^u`?vJD2Zz*52E>wKGV9ozX)ie(|u|>O7U-~ zCHRKJ-}h;{upe?`dZoP)spCa)w<~X4e}_Ij)7Cn`RZS*spBV_e3uGlLy;t^K#YJRq zr*%&+qQ!!R9Q|pq^?4soAL!ig5h=E)kXI72Ox&q9UE-XXC)%=1hH?6Dv9j}5zV(H) zdnPjL_@Vaw?=+C4d5s+?mWtO(?(Qd^?a_EX{;2qsrzs z?ukhhh> z4w?VPOH9lo6)4e+mHf|%C($VgSWyA4ty-ftxO}MbL zUS@m{C4KV3%r_Z0m@v!IO=Sy*13U5lDa*z`(xg*E9MX2d{g|5g7K2~Pz!1+JTI-mr zpX>qnRpVzkt?wig`hLLNVC+NA)7|5RldaiMF5)ieNjQ7&D4L|A*KAkW{FD6#2e;TM z1TQYfr4WB$w6x^4dr*AGja|I{W{dMS-wL+wHjJ~dPbUW=9upNv!VlQa?P$4fdQ!Fr zs*Q4eLQv9tU?6&(ma&;TNw1m;WImVSQy?8{HQS%GmdqvuW~v=%6acU%#Q8c`>F#Bg zu*A$)>5gA}%Sz^DTjl#wI**3EwxS^BjtA+FH&=8vWT zM8R;xuu;~bn(W~++*zxjRBa$<_o5a+SB+|B&0vDvLdG)P)quK%dm7C>?#YWBDEJn=EVjbksH4zt=@Q)#eJM zyAxxZ@q2qmnCV$7P{o_R9+Z;B6fWD}V>NKD`1Q|){P;cZ+J(S22OLXS69=YCEz)wC zIa*z`dp`|)S1FSMl{THEnF}Oys>nSe0XJU#a&~P^BpZgk{L;$n?1%4$LgisPMqO#r zEIVA(X`t}XmjMCuJ!?^#?+zW5D;=UkYl*fsI}bf*%t-i>I0!Jd2R>9b{Tn=^ll@Z< z5y`q0`MN@r_KQ;bZDy=ibyQeD;Ax78{|*Pg$C;1h)!y5y?q5wjps{{IK&924YQN&Z zv+ij-6Z^6s9cZ!P{PWVR4i0&8xW42Vx3@HY(F9U_7$|K%YG`}5gtKd+D6wp* z3wKXfL7stubWxBd0c0o{E3Djjwmpiyrluy$G=PfLa2WDEj4b2oEI;O}T0zRIJqn!! zYiBunD(5XJC|R?Ps}1gB0feT%AEnp$UFJi9hCVFQJn3@Ah~V9KbBj~z>E_nyO0uItZl53+o8RmCYTqPzbZ$@soE-|H^`m1HyMa_JieVN)qRo(6d z$%doeKUwGx}CT$*f=M9uUgEWzNqoag*~7 z04o;zypj@RNxUVNTG1h`-ZJG$DI+ULPV1M;t6#}TmBU8-pH`jxKP1|$-|vy31Qkd{{l&y5rw9t z^!}cP^>gg?CoAx9M&ePi z;oH;cFf*-_Pzyu|cHDDg=JY4KCt@e~tlcrGTFgHH^Pb}J`ZV~bJKSmxi?xX^{P+`vc?tK)`KF)bMWfPp+R<{`$V)Hfl2{+U0F+%Z#hf9T5B~R8hPi#kg{5 zE$Y1FS^Erh5-iXs-S4Ya7~GS^_}6Q>v6visv}li?A7?&b&br;vQ8;E~+c4{>2sID& zzfb??Ukp9z2?W_ex6xAvL+Ou&qMyFK)dS1myxFJeymmGq?VMCM(J}cw-pg?HMbf z9&mEUlyviq-{au|G3y19v&#kR{Hv1M$=|dA<6EF$Vi0^iID|+LeEU+=;{z z!@^cT_v59Qbvw_EiKfxf$H?b^`O7JCQyi=BnIgevGvL))3a^~`7DtNa(dS5?txAvf zKreRH5ggqcMMc*3G6++ss>(!rq=PXvwqF+l=rOy_FL}MA2Xgj*&;hYd1@4Q6vqNQH zX|BOK4JiYu2@8b1Eg9X43p;UYl$#ZwFKJ{w;sBY(RV8=IHuW_-Aj!@PL|r30Z!sP8 zQ3H}RXu(HFI%Z&?R96xx0!BV61I$M3cXw2g=nq8Szo^bvH+cZ?%WRl0gp!j@d|$oL zxGe?f?wzo|9QiA;dkNx6#$b!Ms|BZ}63KA<61Wx7F`U6KLAK=#DH!hfOHoUqUmW^Y ztTYu~!XKbQP3Lq?y9G*<1y9;l9TzTzQ<()+Jm-Q5BQ5PJeIhvW>!jQ+cb zWGsOq!u18QGc8HfGax2AovtfD>$-ecMH@4qNT1~ROEt}Yi2DMx#`yglWWEfTkr%3V znr;V!ywWIw5bZ_gP7{zRT*&r(I(rfQ4~@7_Wo+2&f78Ci1~#M=4Zx&N{?G-8vp1-A z`lDe5rO}Ok5xi}`@)~(6odw5k%SXRvn@LKm7X`L4Em_f^CfnUtwb}^41_t=`y zVfKl_?`U*%kIxq4Dhz~9+E{eb^`(GUyHJoKqi7a^0ratto$jaElRcuKndu0Ada$bv z2fSalYp>B}TbuQA#OPeE;qpO>|IcaNE>87m!37A`Gk@O)pRt1AStlAiX*Weh|LSAg z@e|)K`fkg6*inho#Yo)9r9*rQkPCuf#mSMIo4=ndr8gJwXo?U!vQN3Zjb(HVoPJ5@ z;V!AT2i~PdzLsE1Hk$Z0v)`v(C963Dy|Pq${R{ZTURihT1SN2d-~GC`J2uvyC-L4h zZ5Xq0CeK{qV|FO$S?CTI!YDri=nVg*d#im{CtuJd zq$>=^K1l~3GZzu#J?{YEYx=mE0eI|I&$HD;Hy*T1#ne^;X){!OGJ|xn44D7UOB{WL zJ=~aT1C0dc^+JG}c=ZNUEW=s?pwsm8IyyydKr%;4{Y^J%T@HnpONbII-~CS5KDwq zZEJhq_uLOWf{7FFJzC{ZtSBjb0eMmOLuzXQlq8=!E1z8lM?n2o{2j z6+N7Fzf6Hd1#s@^EXKnDVuPAKG$9Okd)>(R9KSFIgoc_B1Gw%eh!PE23DkO>ou3Vr z_QU4>HBY6e@WpkY#$V;&jo>@r-4%1ttYXeYfmBF_x5uk6*oxjMfNSP0eM-leQRy`d z>D6(5QR@u;jQPw4XnurlQ{Vge^SdByMWD}nK<(mE#`;;`FD)rpX#@A?OWV^66QO9G zLk}9`1?xLV)?TUkD?gvQ{3&t&ZzH*FbsaVy7A$W2=$Rk*b|f|#Qev+}0{G`c_~V5l zfPs&YHuk)2^9xGlCpYkd1R0AWRX<%CWBrC8U1P=z(9MX;)rL!)S3qEvH)P{S60_lr zO?;#OLc_OrF>ffiy@+jT2)|#79<9+_v|Ae5M^~IuAHWbifY*NZX+T=bU9qFnf$^&JihEzX=CY_up8>w$%HQlfA`31Q-JNUV)V6TRVW~H3S+s5uG#i z*hiTftu-YNsv6DN>=8K6V+|Rsrp{0>8~O{E7IkUF1=|V3+}(xcY}?|tA)PszRxmE@ z?>=t@TMoy_akwmd@?=)!H*mBE2O*(x@-cAI0{RtKe5VOhtDwur5abSB?ay~dsS_2J z#LCd{>9R!#@^bBA!iQ zH~p4wiJe(ij_tc9pX+Bz%c15W^gvQByz>y!u4cyI(_6P9MthJ*GPoTt)}iRaysALX zIi5imU;(h+gDqD>5bY44218OZt@ol|l$LG5&JHO)Y4id6x7-(Q^M0BaFcadEWOAZ6 z3}H+`kgNB^sG0LZ(#e2}e|WB)rm{ZMyB5N70rM4(=C~AdnF6$3jZ`y7eYAe02MAQW z5ej_(R#|PaTe;Jx&;I~e;N=2#fq( zoj=exT%`Z!CEMWFJWZkrT4yuH(V2PGPvpsh{p7#hKbFbEvt9B0P8kaRCyfU#z897! z&oyfeRzQr+Rg*l3HCt}UdgF7#<1+v7(-NN32v)$@$?Lz1ZkWs$wAXQIb`W-%u+0KM zY-kjiVWNQ7Q96_6h>vV?KIGn-Cva155p);#Z#>V2cLtI19S`S0ptCx5%Wk4g0zS6d z%&YVvW@u4eS((gVXB0M=Q8~0^LjfB;mrIX9`otWhwCv#TGy0xWA&k}os=K?5)o6!( zGIkDiNi^QGIkG`Ij}gGU(z$Y8FkP=)#aBGG$t>1LIK^DILJ_9}&V2FJjr+w$k7nLa^rZdQ z4*q!zJO|V1Bxl7OO%iQ>o2{g#?5w>RVo%>8opE;@@COn@AR|mbEC-Q~7Nn0zp;12WYs1p)94 zzK7>G^Xr*h9J2Y}hUyaql7eE^9+aTrs8;_Y0pzCkm8p^0q(EkMhVNuP$qyJF6>uh( zpd`q24~C?>EM2|;oxUhTm;(ztfJ|q3@I|MxBV6|{4$@2)GfL(mKPbpf|JBZ^Ww_f>KxxfUJN<`a zel+jP-P_mIaDK7Bfz}MqM>F7gWc^y>fob2gdAS11EX z$gqGUPH}J~7BZO{07{K#Hzw=6+ygijc4|DX6q?<#f z*9CFU%o_kvQ!{B9Z-(AKq<-IUY#()wDxH~#xLr^Sb~^3K=mfCtW^HLta$ka5j-wSo zDqj{J?MLcfH28QiD~RRihlR~gz04Na;H`3H>@^adDl{?@0%QvTB*F}Mr7NK*yurq} z^!p(q(Mb;eBU1A6+?7hfJL633$b6<}rleLd<0HyLR6EZ&I`}vdfisizn`-sUxD2hQ zN9wI=G^1|G0?2_5J0S79DRpUizRTjfH0hktq=L;cetlMGl}u~26Z(I(0MJxE8i!5P zg$mQ9q3$VTZ?`oQ;-zy6vEyhP9C>pB4iaOz`W8qCQd!3;eSLrenQ)qyKqq&|g#_7Q z^UM61G`g>7XBTMaPNj}<(ujFgAf)JQ8nXpt1ROl}0wN`90+zU^Qn;?t{7dT(R-6Un zu#(aR&DS;zIB1gDkhRJnrl+>?Y`Ac9?$*1d? zS^V3S4%rUHwhr85p1PGjF(>r!Q3iD4X+c-ETfl|n^ zaA@o11nSLSBqH69enKpkRAMM`G%ap^?KMe@94i3_G3K^?`(bAn@5{NcdN*1y5~Zeo zE8R$nP^gd(--;u~v1KagBO_>aA!PFZICf;pG-XE8HjStliE1CXpHn7D(rBt;w~rZB z(b6M*-b<{F9@20B^8J11vAPvtQD_0lwIkCNU<#kw z+AqPRff!unEvlBt?abX@T8D+Jn?wdVcNkjg#TjJm`%&7XV6f@)+C6hsV#fyR0@mJM zbtpQxmGfp3Ou_eLF!q|2d6@HhFYHOGzhJ`A#RAAoZ!#MNRxbwK_KB78dehFm^d9;E zq511yoOEGFJRcTFsrlJ(^oN&J4oz}14v%b2jF%z<0UbM+Gr!!+1;XN+&q&j?ecQ-;u>3Ck@P5{NsPl@i&%M^B?qE<*5 zest$GNx2mYF6fzD>bsUe4XlYZC=@)1!R7}MR3Ft;Sx4*Qa`5a2<)9u6`L4g0fgVcF z$Dii^SoTwHZ=2*k9XxU7xG#h(I6l`7`@jL1UooYoJNA~Qafl=JrAfxrTw)&t9#$0% zxKC<>)ErDTOX&H!-M79cs`v>yF8- zGP2fmm7_sGylzXI6I5#F1rKp$KJA|NNwdFtxXdyKAezMtfF>lc^2;5L-MlylfPmDd z#MDfDe{JCPKR^=S>2EtA*0g+h`r*CGve}v1Y#WQdpPwOtRqxf=(U#6pRML=O@g?fVzd^ z_YIXhL+TOdWY$gM(W)7S+pty}v^eMUFlqGD4}!wNK`fHEHrXMb&44*h%qMdt2Y`us z_luq{r`uDiMMny`U)Q=^EKKoqK9)5KUI06=-le`I=(Z%bD+m(8=-{Te_&JnTMz@;t zVXJWY1@y1=B&Dip1P@j?f4i6Pgc^@39|SrFE(T9OZ-*qN7eH!`yAI?+j}ku~8+K#W zC2s$VUsv%j>t#h!gT^`lKO{xYK@HyBhiMtbdZ(vGQ*)cAKkZtQSlTpbMN=AwfCJy{ zN;dM+rmJ@z*{`flAE|Q!bbyk^Z5VnkQSd)u#D5=wed<5D<4@D?v$C~m(n2eyGYpj* z+7jGyS;6m!&Iv{M;eF4a1G{?2$xIEnno$m66$in51V?FMAXyfnyo=CBnM4I+QZqhr z3+TaG&9Si~5qr?tj2w7pB#hhtD;uCBi_iO~_emU#08|-kJ>f|GM{r2X#t4z}o>{kJ zC+NgAs7p7JQ3sW)T58hZ`Z)%LiKtzcDXu!!^8TI5DVE~uN+G$|koovc|II&zMkS)L zjR6@Dujr4}*@-FV@h4<5z#y8|sUzLzhX%+M3-WJ?DLuhY2Z4p6J9i;@Nf*lBO+7m`qYIexbZ_+r z38uZ-)j?op&)p6na~1nR{S1uE?S@f);g3%J&H70D_htJiz@7=3;=F8V+E}>jsm=NL z;e9bSC`iU?=iYkE4qRzdHX^^pQa39vOcU+@F8A{t|?%j9;9s*SK%rTJhH}2eQABf+r##0T!CrRBC*yAavL2>({3(K@_xGpqnrg z5HDo@I_;13w9#2&{z+0b$%jN9BBJxV1Z44z%h`gE{Z+k#lV}d&BQS}wZsRK$8eC}y zWGL~-0@kK+iX31MWkZfwd1C=dxc-TYo|zNcC|~(shR0;qjDnSZOlNV~nN^*y8uBQ<_``o-4rl_tS+H zJnS=`Yo~w?I!fRs81@V&7V%Y&dn#U(8J_vt;gQ&79Yql%WxGzr){k_gyp;ewwZNZ&~i)7e|M z-j}7bWzzUa@?C!r?H8_T%54bKy~_wsPgu%r50sY0>garIt+E4ngQs16zQ9WZpQ#V^ z=Db&hhP^FJk`$+8_ERoTg2bz1WxBB{RiDi#YvTz@x2!DO7T0aAX5>Mh3qkJx0=azw zrk>X}s+}RQU(j-1#4*lqJfQoUa+{7}P*vL1^Hw+$j@5o~-rAfFtv+FQjl?LmWk|q- zsHg-iA$Qv#z(fkE7i{3&cloC3{n;DI3U3{rzQUH2^-PX~Wv7)wMxp=QDn=nXB&T4u zNOfhOB{+jcRlcl0lY>f}-X%dF3L^71jV(la?3c65(=tA96$;xmVKG_J2{SIcM}ND$TESFHzu z24Ay)mU|?A9V>`J6EluE+(#FNRndc}6Sev<$~!2{AgnZy$w2`CoJv0PUtsgrS10O8 ztV>R2RzV0a$K+}wn4F4XeCgp}EhwV9+O%`S5e%)&efV#LcF-raDuE5}_%3+*N=Kpy zr;r3q@Zx;*y=^tyn|DDjH&)yH0|YDUfHH6}p9+3~O_m0u>QItoOjKmEHI~o1JDn5k zM|bX654%=BvAJ#XqO#aPr2MohxF?o^{6{FoS{Ul=pxO*ykOC{&}xvU{@YzvMXUsZEU>A?AA% za(@k+6$Pf7riA}i2Pr$lWZ38-jt<8gVE>b8D-?4S+P9FesMPjgj#Q^-?g8Q#;L{kO z*KgE2_UPPNbBoI}3PrYD;v@zJ5tubeF_&SiAj0@rBU+paCUTrV8Roo7q!3qcoll$^ zyV9eRzj|oCoOKOy@lQ*ab~~>~9^_m3;-NVADU*q2RcSCA{!EuQMJesu2ymYpt51#Z z*s^WOAVdJtCwljOr6iVSkTT^CaB8P+EzJqfW?l$k3quOQJ^XScx46O%k{9)W3UK!H za-fUB9dGrz+Z>M#H_9@Dn6p{G!LOd;^KSYz8z3|*BwJtjXvy829KE${({u(GB=ki> z1NU@V!P;k?ZDSf2UCtr~SfK+lRz11?PWISHSY4MvKTF1aq5&~pBrAn_3sj&0qM2RW z7UyEFAp`hjXwngp*JwLL*(o~d%&nr;3m|#ct-6Ecx32-yhfl5mt$WQI{IBed{ms@5 zH*_+-Zc$7(nAVEyZe2vV2NFEj^m-@dm5#e)Qq#DmB9Gf5{&4zMgHL4iobUMBw+vQt zIj&f@U)H#ktst(`nbRIIoL^$v2^C&gjo>*MTGD1>qH=C@9RxrF^^N-f>bpQYckvya zyCfUe0URF#-?gyKp%DQi;fMkt#HCsy!t!PkYYlrW*sN&kmvro+U?MHl0#u#Yo4VDN;h}N4Q20@QT)X z=QizeyQj}chF8M&>S*6v)Hk!_=(1iqac&YxVqiRz5)1vm=NaYl?1GoUF-9h#{g4F; z@RdWn-edJCf%{r9c$0T1uRR3nlz12f#na6$wnSsO0&GrYG#NZ@kg3T49`fxRHA}hC zLlyehcLc}Gd2OR@@AJ&Ot+<=}+7+1uJ&2}8gT3*FfEB!yQ#lNp3aS9!->_tC5Dh<> zaWc|%D7s9k%S%+b=KQK+Ow(AzlQu2gDywbv%i*nSr(uafxj^=S>%=JgoUN798i}wz z9q?}N&s`v(S))6EIXo>34!rrqkUfFoCgg;XilCi41lYB>mpiteo0Bd3>^GcUThd^UdO|#q$2lJotI>|86L#lO|s$u{t7V}Kw~Ywv9)(Vei{9A zrSss)EP~3Bzr)ddeOoaKE+m;_64UtSw!)!*XQs{f(&Nvp`LHp6;{9={z9-%$0re$k z8=FeP`m_9&{;{?|oeQ3$4v6~iIspYOcaBX;-0sQUmoWsAEHnompP;B(UOd9{f%S;r z){z99SbDTO<#YP}Q)*<7feE!PyTo5#!lN&oR>wGg3cP@UT;iNYYCPR(T&xBCc z3|2h;VFc@JBD4dt)$-ndR8j@;o_ueXGDg&9C(b^lHp^u`A`3jSvCXN<0ZR}g>H&g| zWm>gFU|%kUbn^>*_VkuK*Y727aMbIf4+4KVhMgwxXB4y(udt@%rN&QNq6pF32W^Q9pN`Ew-W|#M zUbk5=a`~l*Uvrye^JZtIG&Q0&i~lc2Ac_@ostyac2Is++$;zS?RHb?5n}FIBYbxN* zRSfSzNEATEPtxG1HJ6|<%gc*=LM}d~Ke3STtBAg=&DGPo!oWdYNByJ$xo{RX<-7Im z=;!ai7m=aj2D*q3TbGqvgr($zYc{%U;!RHci_<+i`t}YB1Xsf$ZES!1IdsbQ8(v&Zm zTwH>c{x_+nztq$8n_Q5%K9VE!573(yI~nzw4NsYjDtX==r%ydYQ2mszXP%k>Co-y% ztHjAhw;{!fW8d;kUaB`@y>W*b0fH;(+zrI81Z-+L?ZvIYj|(H17OQE>@W5$8!?Z7- z&O#z4c<;}hHwx}T!?1HpE4aChYr?5ZLgkH$R8>)i3 z4L;lt@XCT~Qmfcf*jV%KJO;hL^t6}$0A%*h{KML)v>dS3daVE*g@Jzi_rfuzd?VTg z(`p>sP!U~`Gb=ntef5U0+ z@4Eeu+Qi?Wx}gM~_toas)enWC7qZjlP;8}6>8TnSH{HzVTxx)0X;r;Nr-7E|G)J#b zxl+^fA>U-boKWg@WXEgATU8Y=9AuL2Q6sCI!6Q@z^)d z?Vj}6E%mRbhpZV22 zp}u{cH@hb??!tZVtG|`85!y{4o1Xg& zI-Q4%-?+ZL%RPf(t+mk^dlWblq3?F=oheLn90VAzt9%AlyyrC{GRp{tMn76#2KZo zBZ-~ZIgJt=a`j!9TNpYnl@p`{em8H7CInhsGlBtHf+Qk`h8?Wv*L-P;&??q*7#iN(*z?170J~-hIH@8ogR_&=^*EUga>ygb)q~1-1 zjw7KvFs$@jBOLqnsWFECuYUrmA+Ucs!71kfuNly-5P5V0&43B1_)_C0 zwRQ19C)cnv&t^pe}9U{V&6raQ(s`? zeF>!H>>wlTYicr=x49l&5SQV|6lX-Qu%M6fp453zpEGk2HhmKsZLCE<+$RE#xJ?xa zevBz*CKR2i6dY2>@Gs$(IB?^@!X6CR<{?x>b9_( zWdA4dEeG-%sXR*XUzg3BNL|3NyJ33$nh@8|{a$?@8~MldGfkrrwv zs%F8BMxu3G(@BVt_amUw``;)6_(Gam#fJdhi2C6LjWpo3i@CIn`G-U`HkKdJ@U6nD z;aH2!fFiSh$2W*cOug53g)d?4C3$ZA)}kXMzevugc(n1Mq}tN=$vz=1hIqFzL*qz{ zngWPZA^ZDtB_*vjJ3G8x@6>x_qVcse_qGXwQvU8IVP(k0ezw;%A^kQnN|xriIV zHobdPad_CaEB3)<{F0iSEjjSY1URClE#ksn%)o412Rd2T6kVZ5Yfs3t}Y{yAdIXvbap*5yn6Jvb}DwRs%bRS@3jPb8n zhJ+#YZ*VnSmv1zSL2K@K&!cM6*t*Bd6uIBurDE72X4dnU-|pIs%8EW78ZLR&D7_n| zG%>`*zewy6S2)=W*7GH7&9=O!RXUmp)Lx^+njn%zQ*1s5c&8Uf-%i2nrdt^bP-5>c z6Cylckc5cCm~C@EbZA4}zbTXywrILyurr{04L5rceGif+F9KlA$xEH4mDZ-pDRj~m zwl4vZk<)bLno*;cU#3k)QA{%O_?;oYDqPUC4y~q;W9+Ozw zFJaOBi==5nor_z(U*k3Sw&K4FA@eovL(9*q_ABbk`EE)Kh%rNqmC-)JOi%~%ttsEl zNfQ&3--?-qz)+0rZr^?4#dNa-PBjQH^F~pIm{_8CnX5FDS@0s3X*N+Cl72~7FO(ds z2+%3X4^Ynh`%^dt#RDUlWwH$=qV(AtO=B4PVtiukC9oT9d_J0?O4A~5gZEsy8RS<#d+hLF2;jE;yCZP_H-h)e#Znn8>-3>E3?;)YK+{YXqTsdC zN=aOrUh_0LY|6PJWdyJWkMb8oD8V0i7<`mvCxIEz`7`*AWX-a+KWt_;P#tIh3?j@( zY#x+AM&b~VoPOQtu1Q3eLPwD<6L8s_{Y~-x+f9a7ubw%OBa0w=U{9k?;&}sDE-0tC zDO5q_T;~yApVlp22N3uv)eh&wo!oWUzE&^lxB|Pz`hdLqlvSZPU4d{lhY5S5|1fx3 z7=rC&!?%+S!KJ6K+c5-)=@dcJD&SkB`ae)}jU#{lPIiVE#_d;-6$P!1WO`NB#x?I9 zx6gg|G{3$o`6@Ek`#z$0sj#2p9mp;9{e7wIS!$6LiZ)0A5I=g-Ohjj5VkvCuRU7?k1|L5^0w#036Qrg@*Pv_@XmulbPunL>YAnt)1$o@WSIUswN8FZK3u;h#{`{TTHt1N(=00uZ`$X zAvO8a<$z_5eD;&lFZIqQ6!{K9BL_2^Xv!}I#=T5?^zE#fU;TgQB~r`okF7Dvn5%sz zsrym9vBl{lG~+R-BpyG&@3f2mFhjxnTfs{=M!b|uk2=Gk*mya!0_J?%2{7knbUuDb zts-`pD4c`*^@a4F_e}JzfkE*lfTOBFh(q3s4wPyhaD6B!C|#P-Q>b^p9=z@|G+%)o zZRl5hV6%jwF)0X`LGN#jHfOAtca5NB-W(sAdNVnkte0h6LLzF|yq%Q#tQ#RoTf2H; zQ(;~?##cqCm!~N{S0#&Q9~L7s6Kt!$^5t^o4rUdHis;-Ux_wT#@wXon97_zjo9@Q- za)l#?8CrrPet$xY#w0@aFEa~vl5{zC(b}E@eG)th(UjPgq+bavB^jNM91@b2G9(GZ z0YEkin{XB1A*RH;6XZJINk7r()*(u!<$3*v|*chx+jp% zuz{2-oIduSspfqy+t}yzFP(B;dek5s{jFu@Sky`4?{p;;2#spaGe|A!PnW?>`w8fP_52xS}+cJrLAU^x6RbRY{NZhKx<82_`@#sxy-l z9dV4Bcop>4AJ%H|7!_LqN4T-B`TlcfoIQ&ZblAHgWwWL`RsHj7#pUIOe2n04*7$qN zW31scj)d1n?*uP#9S?|Hz%getLj~Kg6*T5=@NE}aIHwx@eIHN$rNI)ejy@>->>bJd z?^V}#B87<8hPX93-eJ7xL14PQ^oJ8SNvFSk;n*r$$OCVzN4%0F*wof}Y_t;32-`DF zu+_U!h}~PB7JY$1UiGiWpI*jgt{zhBnrk&J4667Y$kvdFQ&&Hxcl6GM z$FB&Nju$iR*2&{RjqR;jRo^N4c7Kigl;r4-J5ChSK>uk?I?s~TQQoP7N?3EXHZVkcWWJF~_0Jt% zPwP4J{%$W5N||A-JIA2+!ZtxTx=%6H?>;t^MPEn}9f( zNk`N5aI2?s zZ^!wI0aqonrrl6(L4YrxF6S@PU24}A+=~RRs{Nrmuy?_u7&{s45J4bU5{?HS#AUXb zT<4N&`@L>Pr3t&a;=V_OLK1FdK7l4chNC~iMk zr+m+JVKMfQb!F#+J$uKBe#ev<+L@EpN8unlV@c=0Y+0nO8~gflR@O6-WghK2XZ3i) zk(%xglnT_+osE*)(TgNE*2CiYuY<)YH@M6|!(DRt0rwgcmMx2XQAdu;{u;V*>0hl% zszkz45_8BWASvNUM+~yiwtCI=!^gd4u`8t5E!eM823;knS1alPAoqA&^Ctal8;eB(>+ExU0xYKNI|ZXCg|6 zfO#z$wI|B-SURmR*UPS?FN(w8*!;bfq)QyrM#}>~j(zsV%q9|k`Sv<> z-$=^paS>XOM-~<*TM3Omr~BaEg^8a6r@NkV7V42+yhOED3(a!;3J587C9Hc+H$*Jv zu3seEp{*2q4=Wj|3shl)5qOcjH>*un(Z($+A;1(^2G){|`-H85U*R zb$!jiz|bv7NH+)~3W&haNC?u>NJ~j5Au*JMq?Afai8Rt(l1ir_jdV9me3$q0y>oEP zU*_z!*IsMyN55QxA9vh;@^X=`L6f%fqhod#fk!}w_n#Se+JTSVXtuG%W^-9tSxG{9 z@67n^-zj1ipW{!iIcLM!&@mOAFZQR~Fc0_vziN9iY^o}d8*r7$Z)a^B@ec=#*umPB zA_EOR#&8!j`=AvH+)oxzKQuqzguru0d1?Uw<`qo7jg&tDgw=Q{)=y483FFGx#Gd6 z#y{M+bnyfao>}Pu>m0)5{)Frpu27p#&!*R0%)^gcX$;j@nLg3jChwPcbZP{iyw!T8 zgHd5>3Mr}EvhuM*-{EHlNEDHC$8n_(^U07m7Y8!`qOe(x#Ka5Yb`spP5(S9Jxg5o zyNFf7Dzlf9(={XnG$>@BQn4tbt{%s~>f<7ths%Hv4-_8v*o$_4OA~t9W3jkQ_4V)H z%G$gUkFv-%B3_zat_IeG8Vt*1T={#fyp{2X7~Jx5 zdM#3wO*w?_V=~Z#k>}9IpxoxF0hUV&d!!88?`HM%thBVQW%=+|J5z$#<)UwdH*x9h zV`pl7(j{sH!oS{4>i1cS{&}ADt=6QM>CeHdxc%aPn@HkX~PWs#yt9sjFExctA3QT3SA6fMt zF5@#_rlv={TDo-bJ39Am3ffae7DW$`u06Oj;6Y4ZkTgDG;foUd`Dy#?DSDRp`tzc3 zT=bP~OB>YX2kpx}F4yCSEv4o@WAhI>$=seA!Q<)CSdi}6FD)xm{Xhm703|Gcxk{h( z9Sg%?r2fzT&EN#%C$L0ZH}fjLv1~^p!rlm|*pdNc1d(-PvMib~-~|zIK&>T?d2eR8 z$5>HT={S9NH*gs?-xj!dMg;c&AM=BtE^kk-N>ByQ6+`udK07 z2@@WH`!#w5^s5t^eimtQ)`6bV*Ft`8>rw;o6O>PN7TjZKr%jjeatrbz5+NbjuC%TX zXTdsLT#VRak00yNSP;iDOTNRuICx70(1p;(Wp%7JyUh-bL>)ZA>nu z9^VRGxYNxcFW4`T3pMT(mQHWY`_B|AT`1=s3^wM*hz>r_#1X-eSZQpDboic(9x+j003 z=XDHD@6gh9*G0!o(qYHw+deiIMlU{rT8;WuEq;9r*i8Y_XY0RJ@O~+ZOw|_s~#lPTZ>pdRujtbX~$W zWUmYu`RKW3ey1)5L-v9OdEDtrMozAT^$86WtqK5U=n+qeUJXE?!OS4C^2^6Te5K8J zaP#352lJ@C6z|mv?p^cN-7b2zX76o*vcU6zr0vL3NI`ANzg6z|tEI$a_gL`rzfWuwyVVVV+54>!SwQBFPzmtu zTkt(R_GS3U-lg2sE#Nm{HWc*|P$p&WL7^V&>+6@$07zQWJ3mlEriKCM3I+4%taf?} z)m9*U;uGV)dnf;ofcKqes77Mqo})mqi^Vb=xD_{U(!&?@_N-R{{MU|7Oo#h#4+M`U z;TmwoXR{Ff-PpMIIju{j1h0J@%ExxV zko}FX>1Wm<4j?9oOvPGlUI~stS&S$EgrESRq#*9QO9wIu@4l5iW5ChT^FF%doxffg zx4u@xR)BX+IS=y*-MDE}{@O)t(9!%NZPW!U{@_-Nz=4TFtV*C~C&z4)oB#L5vLk&% zj3(abAXe6SP6<6%g8MM6mB0J8yie?N%O*acp6*G%it~CynRyqAS)U-g#r>R6o?c>4 z(wn-?Js0Z)S<1i=e8;MWP7Y!r6;XiKPmgN|rae3^z7}9H3GMU0H_c4054_O6 zQ>kxsNe)oNGRee_+T)g#=Ddjt#p!)&Df+qh^Ybn5n^MT~&jR#CjmNVQ^P~%BtxXTA zv0l#$i8g%LJ%k0SQf9y_ZTH?e+1PRxl|pZTxjKP27JZR_BBPXc)%N#3qABsDgf2?X z@4vTD5z;=Q4``S5%dql53|s%M(E!^`c%{+S&dj=cG@mYqi#Q=q5(3TG;t3>q$FWiU zk1aNm@1@ISTN3p=#R(HaGM$`=`&|mVEHthQ0wBrK?7d69Z_*;(qae~u zp_)&40dEORf>I z?jI{3fJBmP2cFh%ZX#zsiJ02AaMQR_IZUOGyEHISZ|Jh0Cf2whONp77a_KxpGKjfK zxB8s?y-)LleFn@!F>*F0&uH5CPp#1X@0tpe<^i;G(Rt@O1)@$cM1&wTPcHl7K<$9p zf`w&cP4fdL!2?Psc(d^rdw**q84xFBW>%Q}Ko<_uH@+>;Eg=X%YZ{<kML!wb>R_{*VZGOL!oVIioDtW=MfB0+4;>@CwHZ z7e?_{fSOk+r;%bPljIxA#pTudHgBmTmQC+M)xs|z2PDv>YvT2RO@=y_oex1uJdb=yt%xy&<O{h*MByPW}10MIMPVdcr z$M+aoUR065oyco*zLI$-L#YqA6WO&68`B-9P(7 zGq*Rh-gBzSc1#7etM+LV5fNTPHDyVfsSv9rke}2kP`^xy7z{ht4GBNM){GvKECH>9 zLLflr9s0ts@<|=S<1IZZ21C~3syrv`O9*_Y3*;q1@KW+lS|Xz@mNL9b77mOthyu{;pH^Oj>d zt^}gQ?pn*7c9Cd6nnw-w@bHi>;rLf|S!rmO86kS@8cl!!lI$>jyUanSO+;w?J_B4f z&C4gDMe{EKT50V3yK+7))76oFn2GNJ$yY-o*=~p5T34Q5ntNd|<2W;8oKxX`psFCv zh)g6`xHE>m?wAqD3f6(`*$m`8_;|{HfOR)&S7!Fl%fLc9uCh>DyudNNRLgBdh>mqY zJ;Vasx$NMr$fA5bH#8`CP)C8Cc&)ysLkK*%u7M#+JL`A>gE*ctrvvnKm!sHd*u?blP?_TsaTJd0p9ZkWqxom-Nj~ie2j=*( z@m)vh`&8UWe}Osbe~hUdGd1A6`HkgsePP)ojm8JBCzr}9{D9KR=Y**D7dCZK7vG-) zr!~(qDT#wal8u2|YYpa@<5aSLJquA^)A1~l0e0QVua4o3l7_}Lm12!)RxsyP0!7djr;ppU0F6b~)W50U}KDZ1=$Y*gRh$pIaN?UI;c zN8J+QSE?{~LQwB^*{LbX!p11=Pii3TE0#xeoUA$vfbHQ-wX}r!<7YTNO4sSlnLe>o zO-t(0Rc}!5KOnI(y88PIKyjJ>%x`9p23Sg<^XIv7GOauMqKThg6FBH#9|l^k*6I}f z`VX(Z*?bR0$nimt^@AU@{Y7}ErvTH>;zfW8FODZH)%5HN#if?@JH>37zQa3vZfxOdeSp;o({m=ZBFZV3d z#YRm6i^$^#ai9pjSz6PlM8bHzaZFvYS3UwlLN`%|D+yFHm2br|rF_y`Q;W?+e~Fcr z=MsTuGR*0WbM#IP&sDQ4Qrn_J*W@W7Mk&^!eQ9c+M za^Huv32*o^wV{2LOkX|n6^N={81n(?cqG(wPX;Mo^GM`6dsjV8FCQ`M_x5blxXrSP zjN*QH*%K3-t}C9zlNgVdYymD#n1qEj&P7&r(z@+zN~tj@JiS40}Tz zdzKE1JnFBvZX&BA(_>N~h_}x3z(8tgw#5#@-uJ@E1Fz4rF6{wwGxdUWHZkL6DsTDI zcFN3Xid#;^-?ZeFMLm{Js{__+f)Um*z>V1IaC0;_8Qdl-32+xc0H;2RYc|zV+cbM3 zqHSULopg#b;eXK&I00P9i6o7eOI-d?l@Fxdxz{33^`R%g6T|#C6Y#(a!iZ!X zUQU4g?{@fxSULu=0@}R3*%d!&dpvB5WjRj4HRD~PCM4cZLqm(ZR$~1VV02mA%bc7`oL3{{2Z2kQCYRfK5u;wGS z`~~Vbu(86EteDzX_RXmVSh9(!h7xIGYH>U~-B01?;LDy&`1{D*I6qkx_5^j6Y7Wt2 zu_Ol(dQ}5~4eYSk9DR+s^4N$wehE}kT1*I^rRoFfYkMC8j;aAyOxEWcGEr-wlc!Dm zZ>cWao(P#I1bpe+&D#FWPxaDp9bu{`9Pf0Op;!!v*X-PUeiB2 z?YvOw>r1OGbkDjzi<4C!2)Pa(_Qt1O+&-%1^$py)H(8S~&^;X{;n>Z{@ zt$I<9W0@r1fflptY6?AZ&triye@sFvf!}L)RpiVqRx&fJC$n^GqolDPLq}%D1I(cp zZJj6y4BgS9?BTt4CU5)(Ykcu$?Y;WP>p`+y%;#E)05(g`3Y*2;UH_HaY*vlD4QWz# zn5#duM}u(}HnC?{h^fZ+XK}&KjQ%C&S7Se|7lp1itb6Dl`&@gTfexzQs6d)_8c(VK zDM19c2|slsHBK6WdGKKvI8ja&r8GbfT`md`4bHyGNb^($$je*$+0V7yJU?xz=)Nfa zg+Yr1DE|VbWX@?I+kDmD2H4wU?w|+KU%8p_!%%~KKQXhXZ7*QB6krB`9tpy4Ul@=j z<9nW;4Omo_tH@9Usbe@h_NCK%|Ae1>vNB~p-!tMLs1A;-Y*wxJbez6K&M}%XJnfh< zAJCZsKxN1b)_6Usb_DCP=JMV3Pa1wjjq@8>kGnh7BOBMrMD7!dUspj*P^<2A%}@cq zVb)a}*F~u3l$}si&j0wMnJGdfseM=>7ur-6`9uJkCjy3AdD+UCPp9RqwLpCDb1nLH zxUPpdo_H39c4z3E~}OR!jyxTpy!(nWkwLsf?|Bgg|bv=1?eR^a$^Gc zFqXI$FJ3ih^YLeWKzGjI{SzroA{OrA43G9i?xz+mqMh5@-~34de2|kx4P_YmoC~yO z-O+#bptv63;i029DZOPRWBiBVugq#G#I{s}pScU3?;jA5BM7a^i96%B`<&e0R);H$ z#~gUQCFLfzV5TrnJDJiB4#aIBxOvlRwm=vp94WpX$=2TD`VlZ+6pPl)Um^U;Z)Lo0 zqGP;IAXYJg!ezzx;DK^w)(L(+Gus!I zP0h+Uj-QE)ga_RhwjWgqZl}_rQ+tOqrHor%EqBVNG%nI32VL5JL9?BC5$r2+a-`M) z)xjH7Efv7#M?#Ac#)j0JL|w`TJ|Mo9c;u+h!>`5UZ~1%FhI693(v)l8;}nwZ*SN*R zP$U+!h`j;3ci$*AcLs`l(pE_^_v#^=W2a{5{X)&vzcCEwcl5#t+Y`QTxUIg|%qb?TbP%F~ z;jCrBD@e5XbUO0rMw;Ya_7)51(bN^Za&QC^EAS58ehz@zEgO+sPqK<1ckMfLm9RHP zZ6ZN8T1wF<1}kvLVQ;|>W36bKnXSSxZoR0Ho%G3vsYNEkW|Z>0ONO7){!a_=`@egT z2nNKP60i*Gtr+^EQq}MVc%fQVMov1RGMvL9V?1ir4tkD&7?AGSYiMd(8i+Z`TEvPi zUQgvj@yT=^q(V;F52ywAXn$h=eEi6Mm7G2tXMtzjVEq6EW*q{V&BU>Z1EGDi$jn>W zkEW9E--N%lrwbXNn|SS92-FMH#k@M6;MH|)b+{0h!V=VMZyziEmAOg&P z(cU3^ccB0q?~Grdaq$w6Yti&~dM;tn7xqBbg)%gYCi%M-^t49Zy`U)`73{FoaPX4w zBoMI{e(&;fH7<4JXXg8lKW}6xr>jb04ysMRyFH!`yFq;sBShXC>1I~(NKKMM=<__e zc62K>YZb}|RALZypD@(w_`dnD1vA(y#xJtye$4>)3o`5>85u>|q>e{G;JWfn$a5J$ zH+CfCg)4T!S3mBWzmrV%GJig9j7U)~RS0l~X}#Z(kW%8ITzd&fdhK^7bcMTtRS>bn zxmQf$e(n%4A%8s0BU9x?1D4f22wsGxv|fj(aGqBfbeY@X%F2=+e@qo&jPojuYS6}5 zT#C3z{bAe}VmfKh+nVKgNo9^*_yDpHG*+3kx+|t(%h^6+>yCh56dcx-1>RlM0>Hax zwf|5`jsZB(VyJ!0b5-8<_$Z|Aj}^x&U#XXiwvhHI99}GR)>sQi$IKg_P4z*wHsnwP zo<)njjKg{#^sn|h-k8tG9Ge}Z$y6y20RaNf)+N6u6YZ2j#04?tPCW-w$KeW+GQbLy z_Fni4ptznEw%YSrsL6Gw?GoO1!<3n=nb#7)hGW z$4Uf-yfCy99h$5+AkI@T(Zcd^<-SIbS`Uc!)?GB-}V z6sJU>G_L6})>Xr`Vg2eF!*9s!YXfPw;B0?X3SqH6Hf$5)sucQ#nhQwOKXjt&RLhb; zH$GlFvH_G8#eMf`ruQ)&ZJ25NPdz#o$3vl+E!R5n?4Bw4seyELJ5T8&93aQ9lL{)d z36`vree<6$NC+Z$2;dd5*zRh;T_4cXo2?ikv>0A!5%=6AzeP*?tikc`hS$=m1U{C*^7+2>z#Gjq%|kj$J0n$z zirfEAW1+DfZ%qczrqpkB6OY{TTfp;i0THlJgtBI4$v;vffjDReCG&$IuOBOJ4)89F zbZPUL#6}JM*&YR zGl+-$ver{d@Ew7}nCN}`?ZbR30 zZ^uePV0~&QkiE3ExcH0YedRnXWSTr8n@u5}L2RH*5UTD`1LzaKYE;q-GvXm`ECHTo zNym6C_;5e++TB&VJfy{v*gBIWLta*p0Yj1ErSh^zh5wfAq@eGO(8SA?d;YR_{fJl? zu|5*dZN4TPcNEvSm$hkB`H8u%^7kd~s>|L{*S_lxI>SaB1F;ug z0oaQ`ERqA&fs!%A@g8tQwx7+?13zb4h^75ljxb$Lp9dfu@HV&U`^&Cb;#gTPytj4L9#g$L%|!j zgB2=}p~z@^{61V5Pw1yMZPH7YP{X@Puvx9`@^CD*^J_UmTWAqni^yY5cB|z&AoU)e z<{cTcmVnhBE4P96N0=tA*!+gJ{lu3xNQJa0&zGFSB)uiZLH|9HJG=mWkL8DK`*J^L z%ZJB~@5PAL9v`-1MC2Y;7wOTxG(0GIRu6hr@x2W6^+`yC~q99l>4kJa!Dm!(&JpfTS7rPa0Qi$*2Qx~YkqBKe_}(3 zknJEx0DP2Zzblm_q-0()Bs|1&^BQ#hnwa@S>h4!jw~|W4QAcoW{(0Au)n@WusmMR{ zGx>VZR&F!Rw9(Ip*DZ}7m>Q89Tg<24Y;O8JjPZsIGvir8vhU?6B0QhSrQ`}`_@?QS z=Qv*}_OJt90OLFU#kUvxr7f7AoeS)G+hr;~&^l%qKzc+;hpQ8KFfZg;>qkzJqIsB; ziL(TRCr@Q3A4|Ci&XOM)TaQOxwM z-wx0LDW}5kml}|wxjqR)^UYT>m?K9ov`UIQs<#U(u=?zV2hF8m_ob^k5z&7~?owRY zui~L2QlCHQknwS+xgHIl9gIZ~GDB#w*y=Cterj(Gsc_u5%P9T&VKlKm;Kj*5^QDDipUTO_%DrPI-AAim;B?|^2!)qgY_zqF z0Zu-DL~>j&Ew{i$kMxc&IU`v_;{s2 zyfC40My9~bc2sQ(U$K*+4hb*zE~`uW5&9qyyZ?Lpb0u_nyQepa1!Y9B^yc&>M8t{F zCLO|RsOlmdd-(Rsw*VHgI$o%0>|s&)xxN3SjpFrEB;W|hhU}`lyw>9ZyRE(pjyP2t zdg_E>-u8<+)S(WKFnFiQ3mJI)1r<@?qOcO2!#efj^**iBoq4h*zrEl7qB0Nz%NFc+ zKlZG|i9*Fe&r9(4xjKM)UJ^VBm<45o1GH#dVs5CYHP=6I`F^4K-y`}l9}LiL3WOVi z5~$K-u|?mVjsw)tL9>NCUtrx++d^1{Ca_P3Gg_YlALJ@G9fw-a6!?8`fM{?B84azO z^WjC`OR#DW3xhSE3`rtPZ#;Tw18_V%u%VO5G_-9&7a|JQ9^NW+5oQ$@cyxVDD;Zhz znrE`Y9oPK{?yA}WV;A(?uKbA%Rz z(rLCK>JI0XbRnRW;*Vzf!q*Ed^|JB`%Ja(k{|Cm=hx2?u4@d+CVK2C)!@%jFr_gx_O47#NC^m57^f9}l$u@??4_t0iR3P@D}F`5^&rL!9& zs#Lk=eyih|@m!rxe{8*g}3=16j^Am`58=QN`)0o8)?+(^wPKfZrZ-x z=Y8>&rS$I2n_mX5i?O$d*PW~1NNC9nI0eh;UgHUmW}H%3K5`i#LV7+wX|mj=FOFlj zUJc8KPsgP{`sjOA6gRGLR-N=q*Mboc%ycsrkc6=XtAd zbOSdc|I{l{(CpriG4FCx?Hw(Auxv=zD68wHwwr~Fw(H`yM|Spk;eY&TQ>@jkxj0bG ziEJpXZ>yZh8>wSZGj;*W-LIA6=p<#Q5q}P{&rFDbDW(vfMv6^uvZ)BeaiO0`YCvZv);Z zH}<&GgPmG$#s9u4OOATy^W#MZZoH)?hATF!^mBd2(Ql38swj8?siprT(NwQ#gC!q* zFb%Du&N1^?J@EB%Ln0F|WKFi^on2j7^@ovPl)ag@SlxSqxkSI{YA7w}FLoT!12CMW zm%`Z81~=FgD)0mE*CJ+eTHc8>=YC$E8ESA=EO_k4D+=gG-vTK>Gkkif}htAQ@C05ubbb3dWJ;n za(l-O`!`MIT3|G+8+`bdJ1sW55H1rIMahdVP6{Aselx8{%Wz>RT*>sIH2f&PKEx#n zKR!G&p|1!kEyo`D9^mf}p_`MUqe zm#oJD&3>Ra_b!-}#cY7t-@LLNIrH~lEEvA<0Jg+mcKNGs8G1Ts)szHh;2rWQU{Bv- zrx*XA$-p9D8`gdO1Tur%tf=Pu`3?}u*reVsEbKXqU%wIB4QKvXS@~i>O-}K&;RJjU2NSY3{qY!7Hv!kgd{uBV+77FQgIosA~A>Z9Xu{B{VYyuaRI3_r1*zDM&;f2Q|MzUw4IG z^1TQj_brmSk`Gt!`s%NN`VMBLRsYLMQ^VRl6UNSMV$c$KF2dd0-#Ag7#VRw~Q}VQSr8SM!A6k7e1)9@{bt)&Uylz68Km2ZL#Zv zSpImXEAG0inhT?Uyi4CrnfFPcKD2KXbE}hC>Gcn~CT2hGz};jb+~She>%7h?_x8(8 zxkoef?Sqz1_Yq*+`U=9vMLFpF{hr=8?Ijjv^qD04uzY$JVP^Q3%ohs|59&B>j0k{N zQd!!*nN}tZ?utX$4aTXz*dpn`X$gX&LCb1hC^5xkUswlu!P)V zAFS(+mou26KeYmqB<+EcfhhBU=F0*8^4*xR^*1DGjmlBL9L4#VibVGMxmt_fIJEfz zQU~|4*MpXv;?2QA49LJ*xVep*z9__JG{2-Q^Sh=9&J~_c6IDMaAg}(TY?82Ziq8-1 zF|_X++>K7w#Wb^DgEV>S$l{2d7JY@066a5OIXBCtuE=S`^rO= z_ziCme9)2~Prnwaulew{TbOoNc7-bjAF6$~YS2dh%(I6T zVxfys{lFoRd^1ZS-{!{#(Vg`f!nNQS`mx++%A8_KylnC~;62Z;=M|I5IjHNiLEI;Y zHJpg`MSo*2zN38uxaIO1%`~rDET3^IU#qgBbU?j$$5B1CPgn>yO912SJF;*L>&lLz zl!lZr<#DipJ+($KO8J1~vyJR}U4EOc#4DymOO*cfcik5L8Q~NI{k%^ybr=J3s*}6D zfvc@-*WxuM*~DqI#%RGm?-thygl{Bml`4Um$5SViYk`)&=xdwa`gaB|NZJJfGL`~} z-}$T^KeONEa)?3ec@5exEXV%mLQi!}AuQh4kYS>p6!)qDUBW1gP$(Eqmk1GC@Md~V z60O88NwS185EgSSgeW`(obkd!>5<$_bM;}fGPf&!1*~i9-=y1Y1iTnPp3rJF6(*zr zZ$90-VC)OgPA241-1p+t{u*?#uK#oAqE{VHJ)(QiiO;`SHm8*E^&M#rSwkGez;Y?R zlZREgi#Sg77{%cFH{e;TjWtipL8JzRSA^hln!~4TuioMf-2D0#F)LMom%riFzGm@m z=EikRbF%;q@M?}*5zv! zyDOyPy7(xFH_u$Guc+pELr^Kik93LQ7!?+&6?`PjuOaoh5khvMHl%Boh_-Ir++8UKx zUG-hogn6#ZTQLI^Bt#UfsB^mcmzv+!njZZukJoNxD5L#(!tp)wE({r)c#~grFJ0HX z*5cvEq^`3vrzoh!>dfZgNHJ}m-Ny+il0irA5T`w+>}trXwds;J@Q8N9`)OGl)HyV3 zy}^KJlMQ9GAbUv=&~45Ke-Vy+TxC~o4&t&><2q0gtEacTJ6OJS%zY5_a-(Y43v;Hz z{Dc-V6wIdObv*2lzTa7pd){KO_zMzkq8^{c&BfKgrz^A}e#?o#y_Ik3yA1D2xjqj0 z4X-0m$B-W_4yB?OgSHE_1s@rRmO?^9G;-j8k12%ZrTf@z3pnNE)fRbjPj|@zi&x_l zRJA%E?iaxyRZsRQDD>Cv4qGDr7G8dfAtB0PR*N|YZE;1eX;Auc*0;ewc~j7RgYcmO zom_15uw`~TnTBGj9{UCgGQ8NI1oXZ~+Ggy*xIDKW1#h&HS7M%c;}I@D=&|2uicp-D(XEECcz#*@53hxhr+%(0Cgcq-B`73Tz;en*?S`~(a`)M&}6AtIXhmu3gx*mVQ$?>`K*gn2vt z0*i3r?&T~x?(CgVz%A^EVoqZ{38&TO70r|pA;B5aicZ^<=K;PGNqZ}#4_5-n^TVky zV+*zBgjK~(nKU=x)4G!8Ql(KHqa$Le*39{d>8pYTHU|zi$72hs+WF_z_-|L8J5Hz^V%0@%64N)P>mMFXxj9Sfu7*&1 zW@`{lo4&ROxu} zBTCqiUGh`?;`c0PWfKqyUkC$~WU1Q=h3s$7w~T(RGix1H=l{sabT!YCdyL+!FF-xS zyg!yquKPx!5~j(tb8WOerCWi!#QOUt@%MqQDqfAob0P$j<`)H?zV{%FFsNcaeQU8+ z?irx|PPgNALg2z1hd9Dxd9e?O#?d61sg(dyy*G=P^E%p?w580j!?z8p&kNaD6fa|l zMcy5T`=He?B}!7$>c6+U1L9sTtnogX^oPf;ZYmeWpFtY~-pV6N?BnM^&{y|f>IAZH zyH!CYN=xGY^E#)$pY7*vY-Wl=iZPPFdJr>?bBIB?*A1=zy z_iCQaM^E9!q&$S$dU|%odgeOnpPHO>#nr1q3xdM zgSRYenp*wtnl@IWK-acZyqA`!B-^=>1o;|Yvfx0FlD*>P>zTu>buK~L%7P>ufhZ-O zbkthE&dfcT19%-LE+{(a#Ds)X6L@hb#He1AJ`I0>Y%$L-v-`j*fagaae4KsiM|d5Z z%|j*f;UIwJ!ij{X3zBlUGwSDWj5P6>rizw&w*_? zN5h1>YPcgmmxkG(>#;fef$_2NR56$vUSHoalYRPXW1DX*7c<7nlIZb=Ecs~D-(Seb z>;xaDo!sX1i_r)`kuMN&Miwb(6x-Wx7yAX+?5PhPivkv6Z)nz(qm~@^ie2*Zr}c4m zh_!lb0xolz|4fAjqQ{|)C6FW&HI3BAJowt7=G%nUg)C#N9}NlGqX2zU5c;M|d-VJn z@aAmq94x=K7W)+t@&h3n&kM)HPu+SE#qukM^Jrt|57X{hey+}l$KhtvgGI7~x^yn~ zNYRd%Lj&!Vg2j04_Y%)5;PB*9wwG@JK2Qzk7HYI%^C72iTj-$cOxJX6+pDNqB8MQ4 z_RBMqBZrq}g^{*V-JmG#m|>;>9&uh|ocZYPN5Wf>$BH+iI$KmMii|Z@+jnDyhZ?hq zxt=m79@fY3L262VSOBF=1N?FU9w~p_htLj&!H@*;FU$JLkQRE%|Fo@ z@dDWZ!l7l(`x3G#d-jil#4x}4;HZ311rkDyWH?LZ5)*vQTMg521j>-q{U(NT?3tUl zKR@C7cuOTg?b$AQRV|x?AX@-Q*N$O8BC7uLHS~$C^G3j&kMvYqu#+%QmNR#iwX5@T zXN=md`{T)Ry~dW7uvA{s!y*yI-Z#d%JizbK{2yCvSG*sJ0%2D<{C3mL*qUzX)ImEo zX&gWXwH(;G=Q<{LS?6Me_h>GFnWr>P%^>{SCg^@6?4-@wbYc;+S9DJv1l8UlxFZu1o>f}A12@P{Gjt=DtzQ z@KfE@zOH8xim->Zi+sKXIHOT+^V!aU=~zAu6{3RhTGYWcYvtsPY1Jo?4x5Ur?cDpe z*(gV=jz2FvxW}P}GjQynj&r&1)%LcjZ2Iw!MY?6XhUkWvL!3lSBKvLuc;KTFGC7X5 z(Y=mw=*PS)Q^SZP9EH&P=032X8kOKX@A0X~gM_I!pI(&-z ztwIb5pYw02|Imvkk+YjhD(w97TtDW(dGHLzfKMSMKcBkRy3Y6e2{tbZ;FW*he8m40 zC?tAN>iwYz0K=Tj`aO`>(b$A4_8y5lYi+}f(p0|@iI;=yae9G1Kg1>X9?8=2cQ6V+ zk*(J#f!jBWB)GRLq-uB=wfmOh0=U|LmhU6s;=ZRm=lgRTVDI7uTGFMy{`*uUGVz;R zpylBk4;zBQJfZ}E9wo3xfx5|X9qX$EFPX(f2~>#pB4^x2V@`yL7JYYj`nMUZ5?!SF za~zH}-p*lnjuZ+7zjAN2%=0a@T>m*OgcOqTmw!O%9%ap((2WHsB;e1iv{YRGpNdgL9bdaSyFD&Y2o z^5i-w0u^cnvJ+#xMzXS5%aKwIHkkAYg53lWY^p7q#zgV!ig=63qL7{geF~0TCSxYG z`}1eANA5w4`qOrQ@Ti^ca73}O(uR}dQ=C4C+#xI=D#hJgy3V*MswaFH^YBYsig@o0 z;>LoiyGA6y*(QEFci@Iroop44sFxR_y`HE|3TlpS!P?h0`DUoErswF}johH(OyV9H zdU+YZz&f_E0Qc}f=j$pYvdmEiSWvp`;*41Pe@vZaS6p4Nt-EmvE+Sd4d&f8*_yO#_datUQvz~d9jhokBK*>N4W$Ht2 zIl7xT2272(RD{BsgPLMw7{N0EWW9~i<4s1d-u>izTQl>o>t}&_?Xbcaz)Jgx`(y+7 z9j?am$vgIa;)QnpE3X!hYRzc5m>9-aTa_XXP-y{UXi&fcK-C0eO?gdPfE+!iM1A9Q zhP=ms!bM*q@khyZ<{`I@VN7yGy-dmz-&l8=K>AB`VKNx?x-lHGy ze*RZ;-hvfaf_Zt(v9%~0!qvC&o5(tqJmq92{x3k*k<91tGF<%vviVNreI@DhdE#Ov z8VlLz9B)!>eUNs9GW&{}u4Raq!UUo44Lh5D4uJHGc7cahu#R{Tw0>%cZUZsl>P~4R zlJ!lCU9ji5T)?O;fjC8eTgYONrbpbODcd?(9&X!{{<;o`*J~uAc5GlW)vJwpE!cHb zU1&CnBQ66Lb5*D*9CI#1;9(B1GeguIC2@;5^6&;i1FE~Z@d={Y8@Ld_6J8>=HsO_1 zMI&v34hwN(^m_?`wT&U=c=TRwD^Gyrd-um&P`eHP5N#jY$fEh&926DVt85oyAM;LJ z>X-^YSzp`DLB7jL``8Zw6c83`mG`O+q6it{s${2P3=Pp>a;D*{mx($~|z)&;kD}f}Cg||Hk z+CdgjRi{T@Uhq;4`b~6uZH+Geh{TMU#+Kj=hLl z`A0(6S^?DYNRYOpyS3=!a3${7-Gk{|X$_5d#4Ct&{QW8WPeDvz)IC!)1)`_qcl6bm zsG%a^eWEZ4>|2ucltDKiOe?5vtG`t;{(HN=ak(I4-Mn+|HckKpKwVy8&;=~`ww4fb ztYh8*tFyI{jGAy5n;$7~g;cc7yu?FAcE~t(lfh7NWkv zE=i}RIbgwebvtXh6|V~8{)Xr9q6dN7;Gr2<8+5zq3gmFB$C0#uSLB?cr3Z1{R8az2 zc2UmPJO4a4VpFbC&ic0o5@)90V1mKDz!55VxPFNylCtF569$e5od}kNh#^~ zl8EcON8aR@OOzBI6lmehy&_v^$siG65Uad{C|Nj?36LdXqn7J$EbekMq{MGD)6cJx z#@M8Hm|anT7ZV@v=2ELt$CpXZBDh3U|NZ{{&>d7Eb4nVr3(-a-yQ%*>rZRogCVRgR zrnAL&1<*YSL_pe@_Qc}E0`&+j|EM5wf!|?N;yz>F;!9xjzTJejiFHU=nqlcd4~zlD z62BH9en5)la%}pe9J(Iz_xIVKvcqPPac4}lIwh*c!LuL1RiddlBH~ACV=e^7uz&Gppl#Zctfog{gu7Ax9dGClrWmKU;S0M zy|LK9t<+s-^!gRAeD!1B>Er&sP&#&jV@oTFcchbn>c48P1R;o>0qgw-5yN836gT7M zZ902eW+s;F=H)2+(#y6eMm*jb)(U8}5Lp zAlXV#zPi;4!Pj1xj$jikh~tEwp>dmOUWFDpQosZQ68%-yyBtpjGAI(pspeb{KGjyr zp&pOdo%&2E;>d!Z8X*0}-uA@Hlh`ebHaP8g-1a178`}6;oB3+7!x?PmSb;U8)<_F9 z--ER$?4Y`bK(6}zxE)s?$<5?pjm@L*fu-Ij=E-2IV+lnm)uAf_v)dMvzZcI8#+WZ~ zTn0nYs|}U;oF2GX{vE~Ua3Ts4mKGL!KoCw5J$cBD*nU0(y-poHAQqqx>->Y<`+8UA zP3hzAqw^*l;>cAYB%E`D`sCFS9h)#3_(xy_FkFK8HCGZr7Qpr$uOaL4T-1_%sx^+b z?d$Z=5MkK84BOB1!z74(f;@{afB>=%=I#njwt1=*hz#N1?m^_Bow zB!m?4>KC6sVLj&$ z1SgxDHmX%o=CY!VebUvq2W6IEm2q(dL+tsKqsBljuClg&L#`G+>$<0C)J%xey5&w= z0od@bWy%$UZP=n!H&h9$T$MokrtA^DH{hVA=GfZnij%3&kEe?bmU{B+34LV&?31iw zqBK|3h{e;oN2zoOX~{Q41(RuvV)+?dC}Vdq>XVl@dofV5Rb8mv0+r$f+AcQO`z>ZZ zWs*f!ac8}%i^3JHq>m~_K3hnXM7E48)q0-ImFB4bc`$jOK?LLrX=#K(!=nz|f${uK zy9VP%joYL$JIK#Gsq@WGJ8CIA64k_c@pt_hN@&+~^*cVcq)7~<5B(M--2ar9e~3y{ z08*k^V=?qniznWjG(fk)7Bdug?XhETezQ+@0>!Bttu8ws%aD-WX_(X*aQx4P;5~c_ z^Nu?m$|>k?T-;!wjN(>n=!Dw)zwHiY_M_d{he6M?t^V-E*|SrXl*0ET4r`xVBB>yK ztIz^~wV0-q5S2&Hyk@t(e?YXdjSfbwD@1rkuJthj;zbxyHb0lpSHFs$L7$;?RusYdi|$FC_%a^s0rg7xU>mF zCW_93z&c9$xfS)ZMvp#OM`|c8l0s>Oa;WL?4eLr>hQD_FYqHa3`+PQ^v*4eW@iaPGU)*7I8_l zmP;HTUN14$18sIm9}_2DWTb>ueb>>8xb?sByDu!K!!zrEa=KXWOv`Oh?*ZfoW~8*} zXp-fB{fV{i|EGQ9B;}G3DXiIzA@u;U+Cc*e!1Dx&MmUEo?g@*p#`=@=C;-N52Lkc0 zMuJjn%3OBi*rOgST&ml4kS33yXQ73Cu6N%d8!=yapAdH1VDht-L`H5qn^p=X7y`d?GS8oMaYo^okLPuEBYlIc2nx#cCF^RI>FL-h9*=c!p}W8 z`Nx1C_}&G**E_>8W)IZ{-q*K?Jd`h}M18$x)Gh}@8Wf^1Y%n3ZY>Dn9!3^JPr*})E zzgx(OAsW-=u{D#c^~bXJ^~B?ul3A!twP8h5L^l4Ej;@!bR800l=Hu;wVel@<0OyIH z$WOy%hR@h~O0H*z!u78#anFTDa*@(>oUkzh$m{?}CWS@qwPjCkubzW$mzgmaul502 zMiC<|K-Xh=SVwlT$44950s6l-3U6&H#%bYMt4&L>C*^T z-khNkJ6tO7rQfh>3jI@(^riCop^A2&5%+pEy)bjUI5PS?4Er}`ls^U+Itw^5`mLwm zoq_fN>kWR~<>g2Yxpu#HM}jP(UQmZ!!HOF30*dB0lLbsZ<8IA#P`djV&-bcXJD!^7 z%*@O+OiWBdB5oWXm{3Q0qJM&DWr(69rSCPqw>r_4K%KmoJCd4fEWTOpqqKK3ywtB? zf(Dp%+i+Z18_%3LReh4j%--zScOV`>)w!_Y^pA0p8HUCGIY=yU!6uXzWx-##nTNls zYwNhQ^%ai<{Z~QPW;+}u&EEB$jFnrIA%Q(6qOGf%pM}~3{lf9vEVrev?!B#k4P~rx(USRL;?YHKW?EQSam<&{eTkj zay&iVSiw!ccOi1 zG`C>W1^M(HcXK>-&tn-Bmp_h2pv=5a>%$ziauKbpw{`AjpOP&~&-K7aH>+tQ@d9_a zOB;sN+u0kPnf{Kfp^QybRRia!{+(BtXzj0S8?wCp@H!M@Z>EG%4k3SU%2i41j2Nlk z*1HBvp=zY|-yHZZ@@}R&5vbaQLOG?99I{}N#kc(CR)E&LC9J;`MSF*O9m4!u%+W}F zEhPW_Y=)UQ69%|SkN}K7d7iFDw6*a*2(|N(V*@uI3m@wDqRTSqW~aO=~Ihj}yhg)GK=amYShjDIq3qqs46v-~1&9tPUQ za_#vw;Iw)eq;)k}zkK+dchc20;g{Db;N3kLZ?@Q6rGKYXtWx5pmj@Y3#UF2tdnWe| z>o1$bI{iv*ffGe!nNqjX|1CKjn99JtVtCm+^y@}R3v+7NC0XaZH_o$41m0I2eLq;C^5M>4w=5}xiAzQ%l;%5aswOrmS zeE{!Pc#Kl75WZkR^+2WsN*wykR5U;_!I>se&B?$W5{Qcre>=O@aflG6j%^pXt5|U|8 zi&{3f4p7+hb)>S5A61Vt&(_z5>VUX0*iK*3YUKbw*pL{e7BJGeUcP8abxBjHbcIgp zKm&#pEUcq;p=CAzL^U-bIQiwgxnY=qtiX6jONLe4NA;zfRXIm_>RTC@MPrkoR)v^Jwk&9EHk}LA{tV%;`aJ+k7gbY$Ggby^I zbT!x$sQwchk6JnonEdSS)1m|2m|POma+2dsG6ll{%Y~P z6_$LZ#(#1;gQj5lB@4bYZP5TVkAW>0a?AZ`A$zy%Na;@giiXIc^@*~eX_Ka2ex97% z*Inv|8iVfl)7=Ox{=v_G-yg_9Y`pjoo66bgSS=_iyjC+O12>NC!zq!_D^uIX`EnEZ zKZF}Ox%c>euhRjDA98EufP@@z7UMug$f|PVbnlYVKo$@$xL~?uNrj)sMT#ZaLZZUt zlBGhG#)GVo#;cR{ZlNt~4nN)f2g^VL zg^Vx(OD|$+%vkL0RtNXEQ`icsjv6q;e~#rELtamW{&4*fZ9v;|sTTHAysU0w1ck#U zy01j4u}zHdYDswkP+hJm(OfO*k&YA5%hxC5XI)cpKf7c{xBw`4yPCFb-9xX5v{q}4 zPNGF~NJAQLVdWFZ5D-W%LAeQX=~iElkai3tmyl=6wJ85aOg@%zc)gFsX{VvZm?>$M zZ@ja)Xc)-4N3EVylKn^JY?YCp5MmJg<9X415a2h&h%$qBr!@sAbmKEP(SV}ZzB=F{ zTO|JgH3g!ZK#*ydyAUg-68bZUvvTkNM?1Ji;e+!^sp^_?$M&S~c-9{-oZN#*Fo!3# z9`B2lXcik}4ZN;;9||jHJi0oz^=N5~1Oo3Fxh8m!gG^4&*=<%@h#>O^eD~iH+ju`m zgEBEeHJpdlm4u`pWwLGmHe3vVQ7Z=>))hS-V#Ps8t%hRznMBLf9)?`y^$9hAR=}o3 zS2*j?>thoM^E-qYuxuj{xS8OSdlT_Gmj?( z)R%pAdMCM#MAt;0Spa~PVbB@2Qg=hhp<05_cQbDEL(x7z@iaXJxw0gh|3sJWz#zSq zldsd?my4R8+$v_lC#Lg}H>IY%Xmt=3-Zb(nH2=AwwtVWMksGvC`{!#T9%Q5K%I(7g zc9UzW#fM0W4q@gUDeR#RKe&DOBwB8K_o34wx2DbaY=w9rKpq-At@q>XX7u-i=0BIr zqoB-DVXO3%-}S1Q=Wg0guc4M3I`5CZVu{j?w1BU>e1c^=RiW<%CtcmEV*+2eiN9(D z8tn0o*3ftAK{PjQ67%0DNYF*efLGOT2SJIDa)1_bQno#tzCxm$>`>revex;DN+3QJ zj_}O-j7@kD8*@OA#_9tWz$pIcZOSO}kkF zT4r6t(-wwy#wNuF{i0cLvt21u2FjzTjB%*WD9ij;yra|*7gGj;LP{`?y8AMW&n!+! zw|^fGjR}fKa_$9*xuY3?%w!Pis1|y@w=N0iSUeP1JH%y>}3i? z!_L;gDUwwGO%y!~C`BoCKWdlarW%GR$;q;Cgje!QOxuyU4`j}e>VPb41KHUV~{rvgz9Bcoc zSVST(x(*T+x~T!&>}u$HVj3jI>{enz_1TbWMdjt&ou^cioWY|CG^0ozv^=HpstGak>jE7Z{=ie#+S6 zQ^cQ1DRu}e0HCuBLEP*j6ZkM|=xiudu?fbmOR{G-<6=K?;M(hp=^Sl;rR`QsGkV0dBrT0?@Kk(Sems`_fUWdda*TNidVM{JW%3W)x_U~Ty4W(;Hbqi_x;H+q zfQ|bKL7bl0C?PL3`Emcp96jN7e2l7;a^rd1tJ&)FbK+p*(JXhq&lN{Wr)$F+SHdmwfK zbB#k6^Hrl8s@?0*ICUc&AowY$^w+(PytxH>F;~yP%_)7pHY@2<+ybEaXI7y^h&IL! zGMff4q@JQ4PQ?1N$NFe78Dw*}v?F9fjT|=aX=6mqu|e{$0ud=d?y<8^a$pQqfxN*! zU0@(DB>*?7LIKg!%8ytj>B@qt%(OT02vkA9C`~mT?l=*Decb8hUGnc(I zeIt_NWk$ghEJr41)NQ(BZ8KUd?Ov50e)p6lG@1dsbXOk%%N)SW$<@ubQWY}}|LMsB z1zmc-1T=o?WB3^T_&tzP>vO%ah(zD5qaTmp@;gCKVO!0j=!PrVh%IZrv6vuSL;HZG zn{xW?Qd+qE4U&-*)Gd8`ZXjKC1aDkA2ZcMuC;>UWJTL!hcKTkN8o5Kt;a5-5!ztfh z$AaEPbD~@k#!7AHeb`5E`nt?$yXR@p7kzSk_*FJx$GxBs}+Y}_? zhK9MWQ25jqTFvdr$QzBCubhf!I}cpq3Qt83v)>18C`L;HPMorDeyLam6W@7J@1Xuk z)~e1&;N+c8>;a$;0`o+5+l25i!aIda*hB9eu*M6+x4@VMpGdW_0_wR_o6@s^&x2U{ zo!E#tk${61@n&gLtcsde;$+Rwr1dkQKc!TxGQCra=;LYN3#5PZgsS*_9$Zc?aS03- zAlxFw@YWo~3V%vn!)79xGZs5M5 z24%dO1Ul5CzW}rSt)5FskUmhoj;2ASx`h7P@Ak}W)a6f@?=)O=&hF0qHhlA;W#BD% zVKf9Ex*c~wIyLUx{zzd3LSo+YuX1-~WCVUQklyxHb!j?t@=6;Bw51PXki&S!Ud|mT zhmLrg_XI=A^#ynx6^}3loa%+XQ8QvKFj_=F+dm0ixo`j93-CgqXAu9#&{v0bms@UM zL2vIoQtLpE?!((C6_I=MFB)Luf*epY()gD9aV5O|>h*CR#4l%Gd1Mw8SPYxR8vxgx z^TMgA6>{*=Pd#nC@txeewQ>%DZV(_mN4tv8scTs<@q2o#~a(Xf>IL)g!$opRJA z3pp?+z3CxX%IqQlxU|W%?zRsr<@>dBdSVYM;j5@}kwh?AS(_CTt&3%;8)ZBu0P5@P z+F_Ni^u=1!(Cg1zgqFdUk;?K3x)wead=p2P$8~RF$XX3~! z8hL_}5pcDOae4_TCM}DOZm^kruN!4kED0qx+758gw?8(n?W8KB6^HE@^co$m$)0l(jX_k%YdoK z$>?YltGght7i`SSVIkyHJ|m*M9nEu!88WpsmpWjyaZ^eA!6~8DoFSFivRBZP5auJl zXqQ}nIL;~a2ATuuFeI$PBJ`LKL!AgIeoX-SF-vO;MehK69e?7ISqln-Jb5m3J1EMueJr?oQnJ_MupE@ zTAPD{3jxM_;f?(DnYkTj_=?#dpXB|W+kyWOPX}Gb4B8$d1HiEn0Gx?{-=7mNTnO*i zoORKnPf!ZH=)QCiVZ2-Y>LNjc-h}e4k-o1I_>sRB~+J+g4_bFY^4DCXJ=C7o(s{0YX>w^o@Zwoxw)tOo~= z2CZl<3}Bd;E?m4o3;Xfr(TWPnB#9&!$;6$RCs!VJ=~#QX>TH*Px$g6TW7ufDdRke@ z2>9jHVgY!_XTCnj#6*XB|ECOBN#x*f_mqLZ7EeynO+YQ!EDCv>Tf8w>Ucvqbli~3C znKy4P_8I5O0k%V^;UCb0z^@}Y*yL;B=V6D2mFJR#?I1U%nHgBZ6=|rT5*p9h_U1Wz zyg4iF3=;t_haY3S)miSSk8RXm&`qh2C(e$5i?`QKy@RQ2u-0e&&y&EQXxL^`T@izC zJOn-0V4xd2z(YZ+Hv-ez$K&7GrpOLq79?@U6QFJ_)jiyOY4Lhw^ntIz$OGb-wup5E+CFLjVSL&FyCDCc{tS`^-H1vVZNZ1oD4 zR^FG0L7q7BVoK#K!)5C_jkx$66p;5|rf9R@lHFuUKMj1J?GWs%xF|p0=U@H!R$C$m zNbU!fiI)GWoc6=HJ9K+^{&71pv(9W-^DnArtG5Zy6at9?|Jiat&{q7%k8jP43Xeg0 zx$84jUDlul0?oGk#RfBeSGdaTt;_6pQe0K@fT2I%@+|*Q5&qly1&98?*Xl1#T2`=N zwep8t=DG@HGbx=9J*^SgZ&=dEUpV*n#E96l*<*59lgWV6|3OcPF=Kvt3*cWAu0PDZ zy0rOCu@U;1fh3vpX|ZW0C_L!u?{Bd6u>1W3^UTKkpWL@!omXBP$A5NYAJQ&DuEImI zhO)YtHe+0xP;$)9QZL*x2K2;7m{#hh-~BgQp3FG})x-yZGufxGhwsnkk1mI%|L%M5 zn~+^Gng62Od@gt5AgO27CTMB-gDP$v)BUe*>(uTj2=0Z=;fn(?U}~e?17PTXB*}0F4j+#F!dZVK_j37n7OU0eNlfoE*QP_cS`+_D zUK_FT!w*0imuZvCKt>tx4(J6JM=S=XTnM~n=yE)6rli49aQKEl-*g&EYTB3<)dURo z!pq>-uKXt75JorntoYd$e#V9P3}uPqt>UamR*<2lyk?Z@x0N!p_t8=p(5@+1|*alPG-l>*#LN$4e#zp*1|$&~({D2t?OaEBTcasPhQwk-a;L~6Ek>6vx-Avo*P z?*xA~$$>o1(a*0bnGLYCX6d&t4`ho2qed!lf_Xi+o9!mI@8N0QbISwZIJg|{Rwfka ziTI(PgK2!aP9}j}flRLGRaK7{ zm~JqQvFfx=V*ZqjsU(d3{YE+l1_p+PfdIZ{J?Xb6VTa_P2ZwHcN)b0C>m6B#j-wn^ z2jLV)fGe2Ieuu5gsH=up3E3u9W~BMtX^%H%)cPgAUT+Ad;|)F%x<_v8GZ1udx-+~| zqm1|=iG28(>A>wviA|fg8eh;kY{Bv@$_YG8oR`yIspXboe+3X>sn#z+^YM27r|Yqx z*DW-(cQALCOWeFMk9~HG4N&&6JN6Y!rn;b{ zRZr5=MRHvR;Za0RbV|;s^OkN;z@*fy{?tqe!7s#6YL8FI&Lo)UrJYu3XWNU^Mnx$&N?3lCAY@j8s>6{cSXNTLc_}L8Q14`LUbM{7M26yMB z1hlucE*y$q^565wnA6M9d$f+F+vN5SO>KSMf)H(PK9Df8pmA0WbdSg2qr!@(5`?>$ zTBF})@Q*ZtiRa@}57rCwLuRb7x!bph;RwFU7FpF7zGdI{hE8rZQRv)E-|`r=rD{nF<9 z>9D*sGs?+gjTUM}40H~?GYxp>K3IBPgAj^t2sa)pLI8N1E&X$A!i<{(S6GjmZ)ltC z@P@okeEYb#br7wEzpej!7k?{dh||60R2ilLp>gY_cFjL=A@t1z(v;0GuHQQB#cOTQ zsk!lg+NYe$nbYgZ`4S}93W4gR>5xm*D*>yLSr#+u)yjz<#B~`SS}TA}y0FXeI!uDc zWbIJn^0dEl-#3SjmDMvi}|3vpiG)igc$4 z&$vb+q@WwGCopC-C6?5q;jBGZzC}h_x=vv&t2lexSvhB7V*Sn30iQ(IvK#9TGoD9qV`}UuJzI4TY*GIkhg5}%j@~{itSh*@W25MIGg_yn-n=pwv~Dj?mZ{9Vxda= zLfg$-ue*Fq$xrL_0kl$x6-$7k9`h;)$d&?gDCIX_WyX4DHHeHVI@Iym8fqG4GBTS> zri;t=l}$`SwDt=*PmxYkY7L9Uh~?>4vOu z%2)r|U}%8Z!i=UF{jYwfX3V;UAr)^7$Fm<~FiZjpc)5~nii%*aJ@dfFEGTmvcAuA{ zEi{^?prgvPWVi%F)}4)LJ~`%ydEcl<3Iqv`=4`2H$7)UJ4s0Y@-f%6~l{DO$wui=e z6Z`5jm#LZZ`}0Mwqc)uj&Q~;m-_1VcZvxP5P4R9yBv};vGGc^YDN}zm%-J>YjX_XL z{<`VKG&^H|NygxJ*mJ&@mGIVfIsI5HnX$#@=UyZczSxsc#U8M115>)Qi>+skM022j zheq_{XgM}{0Srl3c<%gi^ciQVX*kzs%D8{e%?E3X+eWBM)zcF{zFxT7>tgoc;at!| zzCPl4^21^T_TyALmLJO0e#280vkax;Wda)>0|eTkD5^s%v(0Pf9vaKGoIIC`_R##araDsw2tFV;Hy z=1pV&X_WF?2d%}k_~&|x*v9^QkL@INBQ^_ZcNz~QN-6O9KXR;KPOONCGiM5m*)^v` zxI=Ul&=BoDT|Mckg2xZvf7YP#>}^xwSa|I~3okp)UUW}>XRYVQTBig5^{LsP${ZlS zHrPVo5ANK3A!8>&4RB7zM%S!k=GIb7mMJAcULEb+tORtNcpMFI7B-s(X!+-URSnDu z3FL~jmS7;r8+G{MK&mvsHIb>^|A|`6`}}iQ@h{bjpKl?|c{+Hmj{^GjTOY4;Z847g zIC&Pu3dnfL8%2B9>|?FoSKA7WW;_u59iZ}iu%y=p@#kld@WIt*1!Ifrxq}$dCihHS zzO(a@2-F&DA;*z5U-}vQL7(J$l5HCs)=oyZz8+y^B7h9G3l!kGi>MvINnD4#;3dA~ zFdr2nD3EIqMdl>h@sd5SFrN$U?lcdFeXG~y#47S90BX~M8s*~kFgr8oW>FkoBt$g4 zze|vfadTvrJnbn~r?RS=>-E6U8y_CE`zO4YV=v`HQS;Ye4?Cd|&t>`e?@c!>)xDRm zd@AtYndtZLesJ10VV_<~615EojwWS0XClPV=zYa@_@fn@gU+a#wdsDsoq*&${u>ty z)16?_ypfO4uS!F#*2#mPOocgYxCLK9t6jIeaCv-z;r7SVHlfVgQs~s_~*9q>W05=&lvPm zf6kfx>h;f4%4UPcNREaC6=e;BGgPmzaxs8HZ5PItI3MXWygAKlcJH@6t%c)Wir!*c z38(qz#i>mD+>$|eyF0JFvyejCht7vUmy7cQFRe^q+8q7;SR*K~Ce}h+VHJH@Kt)h$~oJgWdvnu9ErMIqVHt`K8D2w|0^P_IZtj`>*3q4IGNC6z5A z!V#jV%XHgxb+C)q-|0NCQAV8em%uNpNi6niHNh8(dZqkYXk+HU`R0jjhRD@>h;%YaxO>yVOhs5l)Gyu<1nf;f3Jr3 z@BNcq0jSK3Oqk0^pa8{%fL4@Dr*I5&kReCWbGyF4t(J&UN{;KsI&jK@H*rXcWfd#|!?a?a_ zcEo~On$T4arra|~bQQsx;(;P~#{Se16J{6RM^B^qC{I4yV{1 zc@+o|oJP1+o3}!)qW^e?v2S!A-0PVd+o-8JxFitp&)#q_D4nc^6{n0mouSPb`JxiJ zGp`JOVD|8*^E=fv?5`8=_jekSzb<73l8}6_4@dc`XYuLX_5BrPjWjg6kgiaoSK&?XE~ivSkRsqEmCgHF_hca#Cz;!Uh(bq z8smB78-!%IiAZT{cUF)8-a_*+j~8jYU$nQ~Yds_AGPM$?pYsMF|Dhn&eWN;^2;cp+aE~)dF3p^+~K{(%EDC!m#7?z8hazLdsW>EX0LQZB&_v4DJN2h z0s+tN7LZ>>SE2{j9XQb8E@-3AWn~Em$%rubI^ayZF?i3zC*z9(l|`l{5r(bwXJS@D zH>B+D2>Ab=ynBJ|H&)gWRLno1;Q$QzzQNVsiuyxV)X_D|epm@&dp36m4v<(W31Y;V zq_>Z^_lH=ftnSN!s?kHx#x*PF9F$^~08nwb>(Zk4Cf+4*lu#uOW(--J#Ud`ZA)*gY zTh<{t;d@;_ogg1sx=pg-r=kO zUkCHhaW7GNLFzHE)4E)AUuwFp5TmhNArNr?8}|3wb{|`5cYTk-8hd{YGmU@M4k<~Dq*DE|^?8MpW63D|fe zC_4aNAQa0UTmNKB*2yE**9xI{Y(y&J!n%XuTsY#H67x z^^LH>c2a4pBqm;MXe03l`H(?(T&L?9lQqHrT*2fj+cJtr-^z}7v;ByChw7pmm{K0~ ze5-|bqyITl5OiM`wLw7gQ*Ljz~oYo_eQ|_b>y{71FH(;VG;?m67{#drp7>M z#(#y15`Jl|D(lqbnWTq;vaQxo^=~KI$&j<mmC&yl+5?@DwoLoxj1)i66_j7k@vf zZ5W8C{u%FUxM}voqx^uo3W<+A@H~XCDqr;)@6&-Ry9n1jsVoVu@mCAPjh?bk)6&{th*85V7Kf`VoOi2@U;pk zu7CE_fkpQ0VIW==0-r%oN&@2tRYQ`>3r$?jV159pHiQ(IHr#GzX%w}^8#u3M@m-cZsQ)) zPCm{b*4x|a+H`C{-TUeV#lA_PhJi0Oce_f+X}P(>6*V2Qw*+R+yQisfazBVU-CE7k zJ2KggV`BUQWbp)?;3&%U0Neb&edvPQRG0JNVjnPzlJh!$&G^XMF~Jz~w8z+9>7{&O zIsOEmv_hi#8>{|bX6W-O&l(6eY@)}>{R=NGic`=M$~fc4TZ#*l>tw2>{yLI}W!VyH zDS#WW`R}}UK^hf6ome+O)(M_e1N+vCy);TSIED+yu`pU?0glCW65vQZIaO7d$Q^%; z&^OAR zPwU83a$~UeHB$lHn>Z8qK(~B>IG=T&b|As#c^bMA8Rua(Kx)M-=lXyA|NY`e;{CEH z0@+}sVVqO6%x?A>Q26OG*?XF`!zWG@{^eh0^9_dJK>hf3%|7Dk?ICF&Kkw<3eak1| zk9O3IaHKBQ+`vWcM?)aOrQ=g5I6sTun4#JH#_ScfiSi^hxf@hCsV5!u2wYi7m|zsf zZzS-D@9{+e3~?Jko{(&d^TH=*?|b`ENhpWxRX&0eMW$(vgHTAqW+|5gA6El$OG8uk zz9i!;Kuxw&==mkb4)sjU$HwIQy49)EHaLKU}nb~U%Z#WW2XL6+rJ#eeTp$#6Z zQX66Sz#R_)3<4e@RAk>d#%KxX$H!25iz<#it)%IRhQv82L%%bPiOETf_jmE>ohPlB z#=--6w$~9Xq_~*1K09dlGMxHMTp9&H5h~xNQb8Lg9WiH=OiMOh(E;i)c7zQlf)$n^ zqmP|v9!$x4JDvqyn95^+3?JlQlrMzOvJBRg%5dvhQN-9!akC%uQ7Ef)ja0Ci9L7cY zYH#&zDg`)Xb$`pJiJiG{r_>&dtbhRNEaBv11(vL=tXR|1%g~6N4S=;Vp#Aj$A{YXa zFM<3VdHogW-KHMLec}CHC*~(jU3GOX@2^k%F-ZK5FEG!90aJ1YHIdRu3Hx?b5hI*z zJm8k-1`;(O)`pe2sZ61ozP_<}D#ZfImmm&NR z82OsJT#Z-wrxFX4Tpe4x5?JH$5?H*sen@1HXes1+d9n2q7_tVzOdR|>>*-cV_ zt8^M%0l@7T5rU58KkYvpx_D4Dt?5yBqRxH|Ro4r3=kibmd6MY}=34{OWgt?Mf3?|? z4$1+fyXzx8)yFM(cCg8}25A;<3BQV5%QR(AiDrEBS-{L*gmWg?^Qx2}W8eTcktNkE z&$n{@hI6f~erps9tpUe?+p0?X9`1(4u%x+ONknb{M!LG?Xuu3hFSVz58zA}QU5iar zEM9K13av*c#8PtBba#hhS#AOF%C_8RqBw9v2aP___{fi6iS=U2+!NEE z@09vehvh&?3mu2Fh0(-3IhpEKxGWKk@z~$j`4oh@Nd?lf$A^SCYJVIAJ*};Ezn^@( z!9oKx_ zjGvhHJmO|8a6^sNq@86qbG#Po+(&vmDpL88l%+8qTAb(yIWza?vt5fr?xOXOtU$w? zBM%4TA9v;f{jkszp4+o_6UvOqRSsikpJElT9N^K68p=%nEHM0?0_6xOm=o~31D z)Z5@U%QM{VKlDz_$9vv0-1!MY#G%ClSMPi-CA;SywCk;6$w!%X%%^B3k_|P_!f`x* z-}hIAk*h3}kAvJ7u(`kn)OTn1wgI0G{Tw(n=5(YK8V5h3;ZVgIN0Wd* zautGJehk#tBVpf%wM>USI-Z)b@T`2Aihu43$mq#lKSprwFE(O7qmp=X76_;HLMBvS zt@{drQsv2`8hi1U<^6o*lyH&C$S3OZRpe!7>)W@X)^$gJ-xClD^-9=!sw3b=kW8ov zP(DcOdJLp41(QGo-HaMiO6N~xfr5KvNIrHfE`Rl9bo)H?+yCJ8K{U1KSZ8nZE zh$!*8i|+ZBv!yq~6yyNH{uH(9TdSFNk802B~x=I2Y_W!*A zkovcclFy&`^qx6&ZW`r8Xy_s;`Ee^|pYA3fWKpF~n-md8Xv z9H3y6YY&>!+Np1l2Hj*D5B6*xD*xQ~HZ)H$KFu;fB>`&PwQMjHpLonXSaXUKC?rfR zC`QV#|0gO0gIP z98z1gg#k}IrIhjci&9hE$XA=+C!t#sPb|;rN@t4k%Noy z_4g7cxRHuHURZzEFr+gY9C(p@8|t$ka9=U1$QE&_N*ENqk>mYHu&sx0zd zsA0s_hDFzq8HFN;$nF4D<}7^?s6DMUCi12t4_Gg6`=aXmo*fDI^$vDt9Ewe(;?X9I z$9ggvPFbYlBDz_i`wRHYoda<2t5}tvkZPPG^4u;XIF0uZ(PJVh0Ggh|@|c8&!TVQ# z%LkzS`vYw*uJge{l^+_3kiJRzmpnrN%iW%$wOU14QQ2gaiqDIY^`BB60Fr`0v<(3$ z6SqA+JPRNs4caq+sog&Q_>Uue_!{<0iKFMD$3CV|qi5q1=b)zu;N}KAXtI zo0u9jnGcnKUi$8*CKCQScIdmCV)MV>^V@pAGs|a|S{FQZ(bcvl;fQ?*D{|bQEGrEf#X8e%)+W&;n+F)?Y&%Wf2xu!o%Q>vxR zlHU^=X=Q$gh-MbRFK&7CwY-OFz^#E{a4hNEi0sZ|I+kbGYel*CAhZI5+F#-8i? z1_}RLXH2c*{O`nbViv<*VkQE+`>h1jlY%ZcMFk)v^z+0oZ|OioH(cmXsN|WVo?mFu z^XLuj+>@+962!9EjRWTFzOZ+zhS_3D&k@+F|4jg!nSUQJBd^pI(=0aCC2#vHA^c%} z6&CIb&isEPBb)ni8h(1U%iG|-XHd5uhSa-AXH-bMuwvZyIVs!d zmp<{r`|az#CsmNJM#4$2-y?-7z`!3_;Y8A!mp*d*^2o~Oz^DMooLY@qvU$hoF|vXI zwb0X#LnMVsi5!o=l1(*_T4#w}O4N@XlvF@p2m0@O9!QJwXsP-O@rG}IioyB-G)MHm zr!&w)i{I@Rf<3#Q&dh$nA=I?w;dQ8SfNF^kkioqEst&OVkPmuH0{%oF!|CVE027xw z+_d7B_sUQCzpljFt08bqJ2NvgtVSCx;A(i#=}CF_3}duEHemHwx%0# zGrL`5Fyr3Q@3{~nGF1VLmmXwtdbGMjgv^g;qYuw%*jLmnDp2lq?_dEJK`)vM@xzxWN_8-BX*rv8;Irp$<%}fNKt7TMneuuK*DP0awBkf0i zeW-AXHOTR;TbUponmNzaL0>~VEmyjG<01G~Ux@70!42J99{KUMFN{#JUL*ebUf3d} zAXiJr4e7GOzgs!qp#*YP8}&xKLQvM?o=|u^Zyu_?j<>{mr$YwW(%i$)kMY1q#@rST zFJjP@JTl%bJdfvXT7ON1G*CNIEDv=SpvlZj!0YcZZw;U{yT@BGLmon{h+3LUlyIX| z=LIA$emx=*&z}G9HBrcWrDqloSK`F%u`P;IJJX+` zOT)rPiL+2En7t$+8|Ogs+n0p)em2XW9mTu77e)gKokPYB9z7^gF#ny~2n*_?$jgh3 z?vKm+GjOcVHPW$7dcF&x7t{bQ%&x-hP(pzYE1lB^vIF;jU*fWSRz|qewN`QN$5jWc zy8}yv59y08zk}Woc)Q)JK_Xx@5A>^k^FB{~@U&ho3*?Trs`$@y<~Fpo*s(a(712Av z;ExUO_yny`rUnG06Hp5gh`C^TuoqR30scwD^(DB8WoskXw7KAH?-55z_o5zWb^3{RQ74kK1_&=<@_dnJD z|3CgXIEQSR8Bvr}R`xz5qpT2Rm(o%8CgWHkLaFR6WS5n!BQvsxgXEx z^Ya>?_viEd<=f@bA8_37kNf>PZ+D_T(9VtTrRoFvKt70ZqtFq&6%j{__Z0{Z~6l&$Hx+Jsl}IH z`S8w4{VqW&vB33UUS!y3rr&`C{G0et=lIDm1Aa7OPk=kBRE9=Yi)u}pyiGw6i)|tW z&vxi_k&7fGHWlVgAfrBGipP-#$l#xxG#JCMTWZLM$a;u_mx&i`pk;gyMnopXh$*|D zvZV091zEL&x>SQSyC4!B8B1Hw=Dgxgi@u&OqgT8%msAsSd2FJ2P_tT5O6-^y-&Q%E zK7_wV+sO4BwtGB9%_zO1161oWIF!}vNrfpU2Aoy`u6Rv;v4uG>TLU1v$iczEDr8Z& z96r+B5Ow1D@FzQ~Mqkc99FJKpii;9~hzDb6-G{OFZ85NbhSD#iw;pQVnLL`@ z@#T`@pDLpju0V~UYen0q=a9UDP@`ffg#N7FMc#4`eGAU{Wb={+%*44aL88|$)hGp4`m-M|H|A!iTiz@KoKF0DhfohrTcvEMI3L{Vf z605~@xW`tOn->F~`QYzl6D$eamc$3S@?yeh?qd8uvL>=%UD<~?)g?*x+VKL)t+v=m zgtW(!w2cjlV%k->?%7e;a`N*%9#QHapJ>Esl*X+4A^%E-z6H(PrI73D@0Xd>Nn>T* zR}G)yZ*-N%QH=>CvMXxjF5KyJK*?Q8FztCSFemZ$$|hSU5mif!2TCJKbrV^wl` zQd~-7;PMM23eG#sfj67#m|6WP$>$if~^UksJ@VtJ-eo;lR$N-G(yFQRj`KKnk zHTOnX`Sb6mIO!8ItyNen3|?t>2YGl~TCT|%;UsBBdTbw#Q>d9+fzUAe8j z-Z36z3j=49AxyD)=({$+O@CfpSm_v*5>4O|LR{z%iT{jw9_1hRC7f3#y!%^1KVK=W zf5IFE1*8Uxm7S;HGw>jBdj4!F7hD@$pZfxAz0cbz*WMMagSaZ5b@g3PGJLaEjZ&_~ zq0kz8N+xNOxzWo(lJ@Xl{m>ibV_U>#E(L}Dr1SFtpO)hnW~HTYzq+?OG4a$_TEsMr zxN(&{$eSnzNq`t6hTq}NZz53Qz}u_dXVxY|RU1y?Em3(ygt*MI@l(|x1%3v@?LyL$ z1(lF~oc=!byF0nC@i4%DSEk^Zy!M{~0j%SWtPT|_Qx?8X8^WrCUQS|J!)=B+&DYFn z932Lq;*{BJHG0jt?;{Y+x=Eg-pQrimy56*rD(MBb^Oj+sqfO zk4nXen{9yRv2#jh<8~qLgo(_sfbQD9%uUQY4|Cl^u-5%h*;~rAGV|uS+#JDMSDy2; zqc?3?E~rGO&VDNys{lv9O-I6%Pdn^KPN3%?zau5N9)Jur#!m!e(lobxa5c7ri8vPLV1iHe~Z; z`l%U@kaW4_odZWw&eFY_2Olet7Ots1NAxiU$zHp65HSS1n?`%<+Vir3&85B|59;Ah za&fg8T-ieX(9PsWRad(r#|&-mnP)L;L<5zl;Kj(G#+f{yL`lJ%!Cx>dn-YbRi(64m zpK-qIUe&f9Qm>V!21(vH{`k0Q=kESIVnVqdvig%pxQDhZyiPI1Pm15zY3$PhVI!ub z>z#GZ2lxG=(J6veNGurC3{Y@nFDS0pjxjAl{`wN+7}q;r$fqBpEjz!bboLs0K()G-JVFLdRVXn>RxaldiqfeO>B28qvP>Ozn~T$Q!>!1a3vlbGnI`@0u+{ge9W(F6BqA#W7|u#Es|@94MQqsI z%7VFV_#N~@WKmM$CDr(7|4QmGuxx!)As~2 zI{MRCbV8cS5jL@@>VGeI)I$?U;YVdZ6dqxxEsiuaJNxk!yBBo4z%~S(XheSKnH&xt zIRUD%h;*aywb|BtW_S8q-1?Fp)TV5+T=>BvX|JPD&b54UsAtTFs-_>x;rs4aUzN>Y zF*`Sj@euC*+VpjmaVhlLoGaQKbNEV^3XB2yGPpQgs${i91EnJ^3pF{a-QfiG`MGG= z-Ipq%=Og|)ofE3Rds>Hm-h~A$pUKju*<&2uPpzK#bkpA958Vw^16M)EZ9r;3+@9a z(ufTdWWXl19tppQinbN`jzV8R*)!G#8vVug*rDPON6a1x+(41Hbh)k{qD*(?YSs{| z5FGHxNi`j}J?fG>0`l&EQa-fIS`hLDc#?g7_b?l9dZGr&2$CDW-rkUUb^ZvB5!;)? z#ZclUu&XbHwbyR!1!Sh?65&*(c&;tvz}{FBh6z}z{jlP};LeB4CN$G2`5vsRb1PNQ zshG%6?g^wzsgd}fy|KrRjsGB#x3PH()4MN-o%`UFLVR@EYrU0K^BN6xbp1NBs%BP1 z2I{sh9vP={Ne0`Ucanx@=~Q+{gtlb1v0m@;H-W93bd57_(bZ;v*{dmhV5!(_tceaX z;`P`-UTT_R3Op#e@%drKt=gxi=o_Xh@Zvk*j}YKhl6#IFZM!wDGwbTAAvf$ku>T}T zi|%$-IOK$l9RuMQn5P7Y7+0?X%WSz)6q+!1xTWqL6FmmzYgMo<%0Dt&c2sEJs2dx?44Ybqfaxm`vHcKr6_i5 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%0A7aS@;TpM7;pK&wKJvW|tm}2V>RX$#A=C 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 diff --git a/client/icons/vcmiclient.128x128.png b/client/icons/vcmiclient.128x128.png index 5a1457b087726f478873652bac3158a44080b00a..9080c94bcddc1668435bfe2f3a1ff1a035e6f515 100644 GIT binary patch literal 16246 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 literal 13793 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 diff --git a/client/icons/vcmiclient.16x16.png b/client/icons/vcmiclient.16x16.png index a4458bd8c7cdee0aeb5d68d280163b58f4a73bfb..73ed1df8bcf5979dc153ec5f64dad9e8bfdd4362 100644 GIT binary patch delta 636 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

7a(82fn!Pvsz^c` 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 z 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 HHe^L+HTe~f+v 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%0ABN2*l2_c8X*RkA_a0*4?6?Vf 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

K17auJ6{h$yBHJ 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 z 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


B1PhfzZ^)WCkg{wp46)U+Mmt6=)eg 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

T* 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

; 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�N)q=U`*oX1{MgOu3 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⩔%J+osMXRX!3NT!|DeEhl-g4**@;*r}W4!>VSImL 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 z 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 b4YnfIQ;9eCXI#87FB;4tn}Y@WFI@kBVVQ}n literal 677279 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%0A*_E@YC`1q`fx)t+|m)&qt}j4L*aTL4WXw%8p604 z#)V2ruL{oXc`HEQ2wcx6_qYV8MzaWgw2f^OxhAYz+{^HXdn`%A_k!Rn_dmOGse!^U z+q|G(;vI%Y@acR+ErPvIk3?gZ0bV!DN~i~{_!VYcXJQv2bD0=UVs-=eK78^~oG=04 z6Ccc^6b&t&Ik(ttqAG|l2pJg}ZJV1zGyD5aM_XHO{Y+9w^72?r2L^hIi;MS%?kt7P zgOT-_qoWYn{DCS{%hhxK$e9Q4oN2d+6w9rh+4GUv{5UC*8T1OY0Q$U|Iij z{_U$rvLye@IyW*^N?@Xp?dI zgB+yk$J0iZo@<&U%l7V+bk9ummTqeSeOiX}Txr(jX4kVwi*E=NG18U(uUvBztlUkxHCi(}CBuvh8Ou$*zWxL#QxB^d+Q2M$_rjWyd7hQX)thPF25M zNNFH(6y$q0{TgDKA96INJB|Tfa_>&z`+ZTSOS->8-j9Ni#WS<-qH4C>-WfN6K6GY& z-f4ehquwt#_?oI(gT&X({R1p~=LpyW{6d0msH$m5b93`#0GeyFqoV^ft!;3$b*NW? zD6E`&Ci2~r68;ywl-d2k<81b>AyGVs6yovQw}M#7mC;D|?52Zpw~p9p^;M{YF}-`=9PeTu<1EHD%iNSEfPFAE~Bl1-39efNEz zi6xV!BVDnCzU`Xpfm%4J`4)Oj6%9FIgHGD`sv=9e>pN@i>S5B>)R;_%t;wV4(19ya zvKjjkS3GOw`ZY$l@?8m@mJ+0@G-WH)k;zWHc6cpBkE|zA8D+rgw6H!E{H75M*~6+T`df58&oBv!h#j4|Ydc zN#Ldf)WuUvXLItGMPOHUC*ui%;^TT!ei=2ik1!Ip%1N2-1b(yO`boMb+fdqqaa9o{ zK{$b{56%QpVH+?!1MjFgJ#x-m_9t*8*#cmcO%yhq(YDmH1fsxyf8%GRUr2zhs)mL% zfTtNisfz`tFu%0)?g$(kI3tOYtzREA0(o!S1-z_PKK*@sJ0ZXzFu?0MraYm-gKSpT zf=6s$0-wkhz3;SyydPY=$C2w(Jh|v|XTqd-a^+3+!W8P zTxm!n$8k8yvv^0a(8O|>*NT34An8bD?Kq@Q_ONkY03+rZUsEtj~PY~_WYA#ntR_4R&=+_>ua4ZAgQmQ0cBL`X$WEi+!{C@JxDro?^;DwmK<{k`CLU=|M>Zp^o}VPD+N`D zrW~Si{mOG%ZmaUO+B<2y>`w~ z6eLUi&1_43`ZT1>jnWPl4Z+yWcc9lH(#qWx9A?S+JR*me2@@D?`H^mviHgLPBg5^A z-yICWU2LZJD77SwsUnd8=$I3&eT$7*1)pPTqbTm{ES+sv=Ppu;S2X87e*IE5Usp;lKXd>qdZLM3q`-@Nn)nV=5L60Ri>KYJa`H5@PzQpB!UWP-MFuf}fIj>ZK{ zxPc!9luo?y9a{+~#=fbs&IP}ESu;~Rd8hjOiV1qqt@;iq3{|G%9FEAW9;$KI2psPj zX#{DF?ncqQ*P?lK2^bqbZxOQb`KdozoGa}(yCYPiNt}8d+_3^dEf=9k@!5Bw6Z=G1 zsFDyf`%a%Y$nGM}LL_mF=%lkq=BgZ~j{yuoE^VC`@)tTiE;_nD_|u4YRu`mY?ZQD9 zO>#(q%h%wjDfUeBzI61s)#->2L=Ziewl2x6WvPcxIxFHkYDZtCg&noCEvEe9pwYiY zYr3<_FUu=(xhdrraMs~E?EU%!Wxw@YLVYTnS=Y zXs!r}Pw;jOEYE*7;Wj+06o36Sc=T=N5sTkU{7h!6rHB3f|mUZWK1Tt09k1m zQZjj+=u3fbl~5gOx&mIK=ZDP5J9l@5#ro3Mg;jh1pp6;2!%zBG-pRlPY|cf_8R)$h zaiCr1YL-4mNci;hR-Li3mls@qx+jAr!-+2oJtW)bpT76a^nE&+h^i_L;gY~N&U|l+ zn)PocSu3{Nk=zbHUv8U-?|4jMc6{IqAzKf=;( z73^HDl=CM3w@KgJ=l2KMPAa2^XoR2OoIDQ!&%9TV)mA_Qz|e~L^aWQaQgZu(^$@>v zhNIdCZ4-N^x@36ei!oDHNEue9J=3k`YC@stoto?00S(;1MROPYXSb@@!4~I$BHP%R zTFZnxXNTOXIojF4mHgf=XkI+@VwmI6^>KKZAt*hbZRJF=?-{=(vO7TueOb6M#*|jv zs)o3D_=E@|Ll~;0*3?5e?)21ab+4j0SQQ_RgTU(aAQVmgh)v|mkuXAJ_8DxyAzF6S zV`FgD*l)t%muwt)%502;`SZK}daul@Dn$0Igk7VZ5)-Rp?Ii}U^`Z2TXYKO=n`+l{ z?99=-hv&&fV+vu+eg$!<_wKsscTyGcD}liVdIt4=(Jxl;Hjh3*@>7^8PL9hU17}Ve zvoq&7cXIwjLA0(glhI*lCw*|BtuQemA^(Ym{i*x(uYWIoL}PUxUS?dT(Lh`+9scM_ z`w?}W>MM)EqH!HXp^yRo)K*t}U~;*Q*@6bPA}LNVqm7?G-<@gCZ1dbdQV0$-RhCZ4 z=(|Ha)a6}gCBf(zcTMW+>QGJEDj9m`FE7B2WVX$X%4Eai>52VDE5UX)dF$SknwrIr zY>tx|(f%Q$-vj;quSsQy2Ul5cG@}8Qj4mz~fu|8YkbQ1j21a1Y+`bT^uq$Az(k)mV z*v>2YefwSZyo5@w8~B-iJKN>5&qZ!EDKGAVI(k0!JG=&){muuSHd`G7Rvg(BaaYfL zKFWG@Uf_-b$9BM}#037KYnvOUovcZ7lN){JQ0|W?qQuVL+p3627t|PItmNsEiHB1o zgD)5u7jI|L>icA_|F~g6dWG6^xVmtt@}JrG!(`!1L6jQ8ISz0eg7u~$?#ZmyryXq? zI28wQ#vEhmxI>qToKG_g9pnq2kX4WUJ+CB3*xzv7@9nTXyG263i*m{#vr8c@}w;zh`UA6YPLF&QQ z{Bl{f;Nl#d^yQGsijGZyVoYJl& zBX>2`B6YAklj7T}TPD#H!pu!+LS8rTVE-M@y=^4v-xbCWTAUw`yF0XK9T0VG*m$Oz zFy>sAZ()oL0Cn-CPYOdU^=VyyR{SK z$L-}M_|5yhCdID%4SAVQhaSclD#;fYXd0yAJ;F~)yC}x>r4XovnbUGNhraHTG9*@1 zOr*s0w$pXy;UrBReliLyRLEHLFtqNAIO?P?J$$SH&_M6N4ZBJ&7Hfy;=V7H^kRA?- zm1aHZD1#*fgh6TU0(ykOF&=J>YEZ=xet(Lxi@jy@5 zIpf=>#P&QK;ZB4zEY#PE;5FOcW4rZ>)p9K)o9iYY4(p-pfeX9B^E!EIp=ji$B#MaZ zP23WRPo&znCan&XP^Z$gF~^PWbKR2R5ji$tmoEp2$iC&-os0-(pg=_pVAm8$+S>BK zXK!QKzW&JZLWz=W+wR#*cjm-^Ge`6W9pCCRr)%Zy4i=^J(0;2A`2vo-S%epADDydpc3UC?lo{Z4)8epZY9&;khMJZsUbw3bZ8*%Gl8F zmh_tv5EF|~4fcD58$sRb{XVyb3mD9)!s?ZXrbG~tM5-60IIkDvu6(JAyKyW~6W8a& z_bDYgI8o&g`o5rOS&@}7>3&9*und*eS)S-LlvAf3;o2$itr2 z>@wjUhwR&!LTl{#*O?CtLAJYFBjV@0+==B^w4TYZcv~9Cc&O6hW=I!!Ddg_LpiRXN zNtGVY^d!;32AupHTK85u_&eRtFwjFho5&k+B@glkazO{{n&-#t-}sh+y84_r0k7EM zyyl~!<5o;T+f@5%Fdwx(EKUc)lSnT_YuDs9HAgT2Q6JMNd~fS|l@8-e(+>-0JJsd0E2}N`Syn!VOXb!%Kxowy!BGSrOe}TDK z09cP)4ejK4%Y~HRj7#~cHKA9Z^0V;dVys-xN12gx$B=|P|90VraS)t*_AT;y7Ek1?we?toNKS1G*S$A%_Ye|?ox6rcxl=GyaoX|r!2 zbfeI{Y{wUI00t&j2-BJclv3d2$7p`S-n(yr9Jg!CUH(nTNp9qG%C8gO2H+I z|BBFQH&b2l<;|MS&L2@~-O5gIeU%yAd1dTkc|<~0ZavBIwU5>g8h_?{+txr#tifKV zh8U5_XTDFz*<)0#&saO;N|2aNZyqXnI=ToIVQ&h1C@HPSA;dj3n*OaJ+V57_ggtEl z;zNZixtz(X*BPXbkvvI0WD8Z&h$~}stdX$$h3Kyag5+Vk!EETKLl&# z%)d5>e~Mb6dkiTkIWnM!sS&bkeNJF(m3u@Z|U1WrNlU;;lk&?5? zY?vhSEVlCSIf9d$!GKr1bz+O?OKh2C2cS4z=Diei5wU^dMm-7m`qvT?x&%F>dJc&);0A?w&eQ7g!lqCbSv=Rla&k7RA| zi2m319dsB#2#CBP6gcl8ln!E2+#mUYvI`G80K(U-A%@V{ z$|V3yMoh6%@3P}K3$K?bO6Y;ZVK@I{`>ITk||8G243nOCV#Yq!!vsa90>436JZVuc7j>V~iv_xfZY(p%Td_ zE98^xD&5a9_P35D*f4@gDCCZjx)`{x9-?;a?86ZC&gsrtz^!CmWFUNbk-HP2WV%zu zNEnnt3I?d%H#Cv05VLkZZ9 zP4Md|LqJZH^95ZlIXF2+BL-vBhJ`|?T zRU$xbpmA!JRMs|4NRZC}Q{>Z~W&wtBNZWX-cCGMDZT8|;h&^CNIL`P(27A&yjHr}S zT?u3w$aTu_?nQ9c(-bxVH9;;OXP}=;`M+L+DVCkGg~5t3wJn`PfPB_k%@t6%Q~ecT4UNeeEG3 z1N_57pDEL|G1uq@oz8h7MUSgfAwp6Or6-HHfE7 za1y_|YMhv!6%`rpBGe^|U^nn{#h7RU66IUOgX{sh;Fs+72=1YSYbau}bX7B@FL;+Mxb0+Q zHUX?k-X5q`yTyr1u&8B*X4Xg{D#b!5I&Ysa!W$)DzKY2`osPVLuHssaID%`ll0fUAultzVE=ReuLz3pKnEwrQx0&gR!~E){b-;6Uve_ zWz7qL&6vhjV@>sN(o25bb}Ow))9dpJ7r<1-jrxW@mV^XT%7@DmwyDz_s{F%{fPEu@m4tNWP+_2 z7$@F!XaZ(%*-3yfp$RL8G=kzLAbb3gDIO4kbh@87?Lz86@@`9NI)J5(w=-5}?cU@Az>hX)@V~OBST>c5z*u4Ot#=IB7>g}hxwlHxl02dd=(~?^KQNS(&U~CFsOwFV zl856z;OOyepP=eH@h_8_cZgXCs)gWzVcyN4Dfyg-!$+EPMrUs_Yb0)|!QeQlZ&Dmj zNA<06DH32dU>aJOtuJGe}rKb;D0C(k$cYR zqEEP*7eK93W-06nO$JOb<#voy3Cac^_>gFV0xEe~P{{Bd#eof&$kJzu7q*+GvaSBNo4l^EDZktxp_$Y*VDFj;U~ z&bk9M`h_TRAgKbOB$qmwOjVE}5bnY7&^A8K9r#^HhR{{VJ41Lo6Fk(xc|RsQ))XFy zgMz#9WPS*8=c-RA&6KnJ?PfwN%kcvT|-~R)ppIWhp(}k*p zSUrIgEeExxTkXF=MaX(w)H}v2Yn?PHrunBmQrAV)kpzD>nsz zE`Gt!`|7KP&`)lXP{KDT!Hx$Vm1(8B!WVijD8BZI5+%T*unZ$qYbM(Ny4<84ZS$YS zb(Z_s(bd$o!W#Pjg}IR=IK>=LyFYDnF-Y^C>J}KRG)` zWsc#ew-uynRv3rbB(CR@g_6>)rkOV30Vg=@Eawpl^7hi<#>UB~^Ml$W2$W-q7r^WH(d0iCbyesU6 zBB~G`XCzc~susx zF}BKpXR)V0kp3u@y=Bk%uCX!ixt5n>+*Qc?R(e5N1a$8>N``qma!|?yK^H-8SUD#c zPCWRai?9)B&{?y*hDTn1GQYem@2BJSKA_~?cw)GkPCIHltxZ2PNb@IPf}GKMXRU6( z??fbw3^KVpqoo=s+b&1M?@&**4K~V=psB~x9wC1j@u4E-4n`Q-TTe5CTOwmZgd$=W zslOd=cujv_)}7MMmHDkhdPvHb6dVAHqV3sE{?@HW>0=uLUZ1egH8{D0X*YoRNbokv z${(*{Zn$|{YUh4!YZOYnyd8GcOpgs zG~#D8w&lDO9*A?pdV+l%^nIbO*nf2Q=M-jJ3G&w`iOfN&eepSv(ZPq@gt{c!_4)Rs zpnL2%JQ^>c21$&MS29Alk#bX-kjS#8hg`>EthRA_A$RsS1%amiP z4iqLfeL${Jh0h9|TzsI*$&iVFtA1cab?oN@sCPtBef=Z)X_d_vE>sm4c_-OnR{=lE z{aM}W5)$zhR(7A?RYj*K`+Wd5jwvSNTH$DnEedv`0#fUAFA=(hdkdpPKf(aZS$j?` z<-|3AFvg;3=K>sUg^~{W^^zKLdC?H5-W6T-u|P(PWP8JlhY2|rFe}!q9Gk++I;R4| zR(;)}F%)nE15V4%auK}8DfyMJG!Eo}W6==bo>Pq42ki5|4^N`fQ>P5WFTw04|3DZd)K2w57Q z#yvP0Wx<;c*Vr>AiC^lv-EAF}+7ex$*z4a|CKwe$g5vg(_!Smv#T*_)Li=v%QjS&_PJF5Cq2cbD%ww_!8+lLu}k@H3Rl9Ce4X2n7ZhR^^VO< z_vk$REsfvmn_zu+cZY72XVcF~6HkU}1{yq^LBOEE$zd|{VpwL&6ZYY#j%ZEU75(=Y z>#cJ=wSpu{-_DXPL^W<62eOh3Xa7gEkF&}+agM3WgWw|{?vGrdXioC4G6Q_my7T*~ zYPl;pZ_+L*=Z3!_D0Nh(28j69-$emDix0i2J_|tUN27$~7||z1N05ARK$?3oHj2+u46CfXpxu32{B zt0p6%8Gr~u!~vsn@zDf4_STTwe^CJBJMSftvI^=md$E`rAMOd}yY4Q|XyOZ+=W(Xz zRhoQJ))wJuJF!%|QC!CBJ}B^M%~bWixtQ*o#P6iZg1Cthh&jPSv~hR>qn+G+40q{^ z`47(8*vwpkTW2@tmepTG)sl#2*c)ofPD=D(`FCnQrQcMzz<_e2G&=_yVOSIY@Hw3` z;8K#k4b0Og+JyS{GCA1mXQn``ZJoSvZ0#I5%gxRGyyw~HtNEEh68&v;JYu|N_w1&J zNTk4>Pzt5Cy6tb-+14EDctM@GnizBb2Y?3E5#&sMrpu)SliJ354vajyOQZgMa>`Nr)hcg!BZ;q`)fTr%;)sE zv4alCAPnHGI)%4zm54F>OHNYYb`=&8zvU}cm9*Ag#Ch!9q zoAS|4=u=qGPopG!*Z1n=PdlaogBGk#)}zD-U{)s7P>1*FX_ww7>S$k*2vPbMdE8T6LFn- z^dY~EZ8>S}_ot6rdqUsmR$*fdV|lpTiXu1#=dGQ{NfyXH{7aeiOitp>`+u_wBPgPy z4Tcd0+Q4S~pV_`9=t{mhXHh8$>1sK{OzU@X{hvJ;Xykv*e%(pWh9F?4;1{);22le*cc+;CDuwtJO+?Ld3JKsR#@n67+{i)`O*2# zf>2-6hKEmv4Iefs<4GhM;Iabjo6 zy(|1o=b_~3_3cR@rJNKK9zScylmEdeyg>n9i$(O(T^}eWyelI73&P`@V@||1o|kHR z1dkzRe95!%Awr#jQXTrJOo&-Ce&tg@NS*=VUYlbDRWo1sH&8yH$C*Vdo{=O{AlwrW zjK{nZ1p^^f^Rytz$@nt}9+Z`Tx5mLov_E)_IRUz5hYYQP1qxrnNm(&my?#zt1?x4+o*%Z%KA z@s}3>GNbEM0UpkP`RXEk0nZZ4hY-B09}T$)02sa-Jz^^ zQs!FuXLqhdBIJ29?S+ckJ7QK{412j(jVX38V=Sh4i!pH1EHIpP1r!oXxhwhMywKlF z+zH%lnFesYMrdt*{)A0jVmNA0P9W?$opH3 zm$Z%FnWd=9iqAT>fvMCptT~D#G>N1Xz!<|Amhyf6U#zQv>h`IJw$jH0(^{gv@ZUFN znFAdZ#_CfzHW6Cliyq-nq2)`>UHSL}V9fG^d}ia*7tqRBP$EV1_`Tm22a(1mo+1Q| zp^|*lkMch)1vk9+xTUD`!o_bts7R7(OkukYq+TV}y?W~R@I*m4_=R_8xsXufWOibe zq4%8&fQbf}tnxJ77=xqtS;zy2flQ<4o5jT$$~s$8o7|owA{Ue`r9BQKDHvSJLxHs* z$X2j3(CGa2^mON&IgM;7rVvg48b!rYqv!V$Wm%uH;5Qrj0zv4XB_>BkiIY8bHXFFu z2ZK{c#g5S?-cbbhHnfo8Kp{OAB$o*eYy&b@;38;ECG5&yiXlpWWMb3=AZvWim$I`7 z7=Fn%C-28ge8}rF$F=QQT<1oKlhNrMp>zC*T;~G}YnFt9tcf4$0^EN%P1PETWi(lc z!aHXEC59LP*(Lwi(Ggj`^6uQZM+M`Oou;y%IEEQk9Fzv zKGBPg?%0C30TMtvr{u_WoG|c!RyFKCDKf$^2aI|O>Bl$dZXhhN6@Q!yrFuEeV#gXy zaO02Oc;jzXY4TkUyJk*_gaUwK%HNNnm{x;ByR>G8B6ih!5RcY#=|pl@((^j&k7NK9 zzBm%cNXlu9qsjTF8W`ozlIX#oDc7cGl|lC=Qqn$$#jJGi6J$0bH2g2RgPpFoJlP!F z<_v5>>Pnsk)BMqz|GZ9yyV}7ptA|NvnMO~Yt}#d8hj3!JHT1s!Iy`!~;Xeq!zbA!G zfG_Ap(an7;eN3VcIoEbC)8Rua)Iz{owku`Jk14RYXv|_L$Wx*;sOP0 zh^Cfz%4q>7RZx*9BXEeVWI14~&?cD`#PfR6`Xnn;p?GVBOuQFjQV}@-S84r+{HTLZ zFx?H^@3!8wZA+Xx>(=j6gPA}9LwL844kgj>sgG;Es0`JkpjUgIq`}?lsZaUQ|B&as z$WI7+P*SRsy}XEgGTWg3l^ z|Bz`Yg8*Mco8Y8!6(5CwQ0$i)d;uogn?PN|Ya4%{9a)ma9NZMR1Zb}kZvwH?aV9lp zBz$rrFSYTIteQ6;B{U~G9LRV=l~Y+mW7eJ7wsRfl_0NOVU@=_aqdyKPSLixy(D}}5 zvaYw-J~sPSAZdE$GASpRVEw(ug+y~=K14kDZsX08zyq(}56S~m0s{qhHC?ImuNdT! z2_8yvOdY(OC{VO+rrCyb#=^*G#8P~EnzfvOQmCH@ng{4!V>Cg(9BC>WCJkI-kgJ;7 zrd6Bny2hGSUv^e8Yq*yok5n?xI)Z(Q?)Lh0poGl=%wJpFO{A1NYHF7)eX=^wY3|>+ zzj)ynWdBP{mP$^KGX5W8GTWkcZ%k}cZe06x^ON^pcI5vB2hNN#G$>Y}dKa#r=NxfI zONO4wqEp466$S)LRKU(78>AZ)-0wDuZEK|Qkmlmi(t>6+Jw^K_q(4Ypz)7j9-tk<|s;bBIaHkv6bHAbR4dlOWpZ`8r-zU8q7=67mXnxH(^$xEkH(^p)Fc8C_nq~(;+y2THV zoiSJ_?n??w#dX)#zv*?w-}oAcHWm}(Ajnf|vYfv9<#W1>u*r)kwQr#w{A@d&8LSDW zSO(A3)ul;YJD0&g#=E7RXL+&0uSJ@iMi|WE6ni#M7j6N+CSGWxXT!h2t6F;EtC?xW zZ|Uwjr5RZhhZnnpD`LBgpqa=geIiWoRR4}BgRZc(kLf4Z$tken&?^Rb5*<0?sYVr^ zu^zXaCq5kQGRucJMBdS^F%?}8EPm8<2Xv!Ec$cv_SWFDWtM20{e1daGFkP#hJr^}Q z@IB#m=O~RuSxlxgfk&a-udWitw1vw%-YBnkB-=6U?;$pmz*qN$*sUk%jZlp(v}F}utZ$+cXeUpQuF*P;ReK>i zLsJEnE-N&PscF0Hv_N19_SpO>jonu8!=BjNu`WG^SVmxQkYJ;9?22cb`IWDyvVhyQs6;BTNxB<8_1_YEf zkZ?E2+%hlg=4~tHU1-+;g|86g4b8oKQ>Z&zNQ|FZI%_x zu(G{``1y>C3M@S_;_N2EZMFE$IJ7&t#Z{boRSK4vqINKO%XoXzUkce(!C8i!-A_wG zEjR$JxbYOnUk~IBeO7};leLtdAB+`l&vaK2@POqi@Pu-7RW71%W3M*#^9ctC;Nf?; zvkQ?%6qmV4()jI=Wh|7kaX_D>142UB33F8oOC>8PI4Y|xefJeqm<9ozPf zNHiR>9qMPyumWhxU$X#jd^+1%ap56u`LHV_fmfq_nrJPU5%lz#wkC*> z{|Zu%txuqV`}uFyr~lEx;mP|06ETPNzfBE%UbqZ=`2H1rxvt0Fr~fVb6kkNGS6hB{ zs(o-NLg)Shh5eQG0|>YX1|*Dd=A61zsLdX4w> zy%>xNgNx#|EU1Xp2Bf)bJ0W@fZ*2enM`d)mRrvqCG8Qo^`I2ptrF{beFmeK<_WxQM zjRd=DRx`|?n)JKEj6XWX5Uxtd_+VV-&+j1A%$TO-*7zLlhr+vHJ+H_Cg7EsiRxStc zg{Y^qCDP^C2$LkrktVY7CM!S1SHJMtR8<__Fb5__$O?w9J*3C@UiXK5C_SbX!(uyu zaWQ*eV8)5I2M-1xpw;MKmk&(!3!q!P&BA~WwTR0D??c50U=Xk+FkFnj(xQX~=G}WJ z`7>eWgerwO{B=X)_pKqUUw!xwz|y-D(=MG|ZJ}e<6CcaDJBB_DlE@ReZf=<&_teJ` z#vnYQAiW#aF(95;u{}vtE)*AiS)z<-I)^|KdADpw&`$nsQ;osaT_*M{P z8k`c?EQQwWr3+NtNRBfl#2b(&R>8 zjxmaP-0!VWDBxoWlD`XFGMJXOh~&b0UmWu!zU2ZZheXS^j{}G>+T^d>4s)W&0n`$i z2%l0iuLjiz?{-CwWB9Oq4!N4GiUTeeSP&y2?_Y-9AmlUO-0=<=yt8fejp1tH z8)sBi4gw_)R7>r3rpT;cPk`A+J^OPW-9khh^)!l5I-58Ljc%_YjHV2R_TuRn0)h|fjx^P!G@h^Una8}pOFwvtq zTcI}fUaE=+b48=NJb z=Y3Mm1*i%g1Mr@v8T=k7tHk#Rpw=A&MROrw_SksRHb`~?5D|qGhkpQ~2TC6O7eIvT zH-K?dyoZr1+hYbe-3oZSIQO3MAA8pEzQx@NA=N- z)wOOkBR|)jTZmPCRrcZLgFzys&Ut3S&w9?#`Wj4)G1>Rx;hi9;?^{pJQvmG(U}UD+2K==^m6oBnCiFO~6>|;G!&kagsOGv6`JvTgXrJ6c9QD>J-;2 zgyM#vcbr0$KIX}k1K;{(2`yK0SJSor5pxBgcsbzrbnk8L?W{Ea-B#wT zZls?AyLBDY_hn~1BIZM~GhO8xoh8lgZ*y2bI^2-6u0RniycRYDZPOz~C#>6#=U zVp|3cyYZ%A_P7+p))i)q^INX1t`K1Smrw%N$PWAzR$eM{07%wjKljV?|88+?p2&;( zH;*IgxG3eUcw04hjGEXIHoZ9An4-(4{^KM1%*imx^lzdvN-vcpz_EG@qQ{bTOvU70 z{UJjhsWt5ehOsg+0|&1Xa0%|{-Wu7s^wxZ4J=ZmQGCRsy7(XsSO3H;z=bddG3-B)> z-=n`BiBLa)IzOZK@VKzk(V%Fb9}y-wv#Ai&qhv0sE;jkxeY2yuSlKL%X>!VWk~Mek zQn7M-@zy(~uce5uI_ETCAMlk(K`BN2bUi!({!_^nfi>v$A%;{ofE8T^CbIOHV>Dv{ z`3<~lnu~aiP)|;($=|fWR<0SpLRtV$Nhcv|NBZ{^$p`$fCyTy-S%a8E9x*0bY)zyH z$X=$S9i#nt1^i@Y>BJOA! z!{viDvCx7WXtgF4NBzIlaSY+hjmS6yLaN;agnO8mKZ+>VhF{+BAI|!K&3x^3WrUZn zMn%-q__c%H131^bXz_QQb0eRx-6R0%#V--OcW`m;`$H-sOMQ0Il19HEceYxv9N3d^ zFA)Da1VJW!I&gE6<-%~P&skGP@hv^cP!D-O7-j~6Q~fmNX&tl&ghEpx|+udSs5~gNt@4Oi2XPB~Pip zO&_3-3YBd7%Rly4cwGc#+duR941wusHxfS)K1Efc`W%`b-}pyCoCbP0qV_xT+7TDm z%+9RI>*;&C&z$(kx(^Xg)$Hx24D|k?9v-Igy%oa0wk*apJR56m4b_Z6H_Og(no>xY za1JH$-d6^wg;R}!2rNt$5R7EKUHH>6yOT3Gd4SSms8T;ZEGNl}3U>fxN+6cf_weRK zJjqD4N94fbny~H1)BVFNHeM5Mgb~%r18dUz zKgO6^cSFb-V-ko6+!78xosx-U>7j>?LMLS-w=U(idR z`?s_A8@*gPj)w#N{GvA5nF9P&Ay% zx*kn%ik;Z`MHgY5ZM9KfE3yNK$Hl{^x7^lz>zeFj*r;f!0=M0&**wlLL^@k`oAc(B zkRQPYN4Xce`+f2_@?nG9IZWYnBKkONsjpfHbRb3Q$gr)KO+o-sOe=2jNHb?(Bw6%6 zF)CsuK45UTMkENP-<6?%@y0xTp!Vhv3t6D0IQ5E-MiWPk&^nFmkhrJVrDk7~)O>p# zuy0vS-MEdE^zzJMUMw%bC#ko=b4iB%Xe;! zX73S|oqN-gV|@lC5EsqH5fdkLckEMa-YYXK!@0^7JHt~WjK;$ywx`pJT?|vhH(@B^ z3-a(Y8lek|bd6qTU|1I3ru6cOYb_1YYq>>V6)S{d?D?w2SfgA_nf$(iJpyI8B&J-s z>*#V5??Z16n2f4V(B58M!X-ALy_{JZj7*7F+$+ZrQRNrLFKw5COsBJS@CY~x?4a%5TwhXJEhs=rje482HA9Xr*NN*p7_3d|9i){W1O+|jB^Be-)F5g z=Uj8Gr%0z@gk3<7pog$Lxa3i08B95LL+L0c_gh5x23_K&{R@gU%TdR#%K4i*JQ#Ul zZkFK4RSEn^T!JzQhcfyr5)RV1EYsUyu`W}`k2TInH0lkTYnx0W{|+y%C%HMQL*w{0 zSOP*uGGmK*L^7rPYhnt_d=*TF^$uxYCBR4i&n>wZWSTAZ1kenMtcLnui;>oZzzEZ# z(){a}tGlxo7o8gKk^^^2K(CF)*{m?Bf?XQRFQzgQ zKh0Jdohcfyf3BL0kAIsjd1J;eIv2mu)C7D*JsYur{`|;U9{E%a%0Uh`Vak?O-AO1! z$F~0U9)vK#=8Hfh+75VltnN&A8WB$_pr+UgDNpQOdg6QHv3wV_Q3qx0=LsZlZHnNB zCzt{XH76_tPmFgsqJxRZ217&mbGH5~l)z(BwR`C1yZ=|z zP(~GTctTcE;f&uXwES%mV3tEwj<4*?Kb5W2DC7$NuPP7w70oUqj}1|ArVbMi7(BlVI%Md-IN6<$ zPu^l@k3!L|kH&0JBN)614la8nuA4ITmlvS6>`@WC!V0PUt9X@>nW0kZzs@lS#+%Pm z2bl2oDAm@&m+zz-h=^rI%5w>yYNi_PM<$|bD~~48)ruc7bc%k6rZjiGm_%;1!h8y= zdA5UihJheb9)Y)j3H4X1Wjm&+unOu;iqMM15-q2_IR?W#jE%IGL7WGg`HPE?3BlTs zpCwab^a+Z5-+1eFcjEb(J8k!HG4sg7Gi_u7sUZSy%$P+9FU$@V#eHghs>tP`I`vcw z3@dtiM}!5#XggNgkR%lu^qI*e-zm9h3{;rX=Q>R3EO~G{v`uNqkaOZpo%k!vAf${C zb~f-Rem|6wM;1$#EWf1ZVm}OP1+?dL-oED2CjMwN%?@CPG%Z&HsUB)-Wgjo1nkUb67<&blVTaQcCPY;Zy zAI-&sF!%PIcNSfYb_`+8JYk12L5eCVL{^47Y)M8%ZM^8xKE{H*B1M^-%5I4YM@1Q% z_mMp1i(Y9%{I1K6lOYK250Pb$hI;d2(uH}H@Gm+oX&nV$jt_K`hbWY;xXTAWdml}? zvpHN#mb_{lL#|0nao$Noz$TFOfY?QGiu5pzc0|gT$#5iSt27c(aN_Hjgv-@_1dE+9 zX(_zT*I8YqEsdB9RZd7wHKY(GU7C9iAG1PwA@z>w(P@_w)x84uBmcI%TFZrPu}cHb zsJ48@&hNqB^kybuLG7kzM2qJ)u>&N+`@jac2M{CqbpfPCtf?DLBy`pcJUD98L&VeX z8~i_Q`8DG~lDLgNa32~ue>b(#uR1D3k~Q8;N=)o2O?)rxv8Qr=|1&iWx*1q1j*XCOTxudG&CtG-lPdmugr}Z2dg45KE~_=%9GE`bP(O81z`v2Qk?8b1~JcY!rVV@gz^O#f?Ps+c*Zzr;Y@k7&%C=Ao>ztA zU_CGv#!6#kNyi>n@LGk%Ju24-YhBdz!r+yX#bl#I&&`N94q26{S_neW$R{KlKr3Zcq7C{0THCiK)a22|M05 zoLulw>1HVF3JTF}`>>orpLw1+p5a%dQ}nFuzewUBJLST<2D5C1B!k2jGBr8fhl}KL z9ynz)S8}2wZ+M?>dYCZm6k^Vd-q;fY79$W(WY3Iz!~^EdOjND7v1iJDX)8nYe_#ca z^;#i)p#h7^bEAnAMOcW!sDIaGMZ%-CDRZ`|@uV4MQ(xXLwz~;Ae987ArnOlXv08G= zIlPKa-v@Vo>cg~k$qo}XEA(OU!*0H#4H1V-gM=l$aS?>Y8X0cB($G)kJIiX|FkAHD z12s%7|5U+P+vk4bZl-H`#=>YhwX47}jZ~d7p>KG(@Pgux*v>Cd{|^BjqftI5@ap_AIvxYdWT% zdX1i!_Cm2ViWwAa$>^dbC+nf0?W*A12!H(d8UuGNwg1^FP z*k!P1geI(zC@?Y7LVQ6o%AsztOAxndX@#f7X9on+Q{?nM35-i@Vf}LsSLa)u%cqc&sCY0%hx8dc^ znyq$+D8|<%ESuwvR~W*Npg{#Z>d<>Y+H)kNSfw%{-*rkxMvS%1lIfkrie! z8BF{^=AA&HjrF}ri0hAT(60UwPXekKC}3tLR5*9t#XjH2hJKI2M`?^n^ewXki$`ck zT-ihPSQ9eAKuOc-{byc5C;`TQ;k{P^?Hb#7fu1fFq^7mk!N<&__p^ zc8@?HDosUv-!1%2LUI!zcGUiCcZ@|MOHAbA-3Ov&3AA6(@@WsX{;6HxgF)>AXh8Y` zH7Ps&1#RvpVn0(Co93JkucL=1U_rJ{)Y!LjrL3|x;jNWohn7JgL%Uk?2nlV|x02i| zspxmxe9spunD>2HrG2ejrLx|dUjZ4!*U9dSdnrr4y5Gclh=XO8ssgPmYh4#R*-=hF zFs-33bF%B)hP~cv3A(*kb{5d4B>O_||{n)O`?>O_&lM;T)0 z#78l=!RqI56aGwuU}Xrbztit}Vudtk{ZyRd{^aX9ONI5YiSb@k6TFMg3<`n<+ca_2 zXpH%2+1jh&ftYW-&K`%%1PW4)<%Ab}VMm|Yz)Qv}lD!71{}CjoB34)t24>(<^xwS+MLv3#T9~=Yp6S%?3nJ+CgxP=0`IJlhO{wQN$3uO+3_SO z)3Ik{5~x1^i~jr$hp3+;A!Sbo(l^vr5*HVJ$EbZ-!edszq@LS;5xEt5h zM?}~1B;Vy1y#LL@P|1a(>3OqmAq1aPxde?iuUBchf4p$@{xIWWL{zVH#WcXgJ8=@= z=ewgx%T@~BzU#}VDYy5uu5HY0R*b&lLw6?c31?O{YwR)HEq!7KNKYu^Il>5e9I!>1 z9kwX(EsE0$e=t$dhlDUtEXCjOV=FJs)B zY~#SL`yU{&xvp3tDdVFcBFKO>!vHM(P;7Jl!ZWE-Yg`1+oVr>wlQuT31sS1D1&?%- zw;@2RBlPW`hXPa+qeQAPJogfMmIrzaHC@~Q3O&p1XL#L4VI{HTv&dj6B_{jyM$=yZ z{!OodK)QXOO20zf5Y5xDI}m?=n&Ly6lPs0#AY^O*Ibfm*oo^PgrJ41&udtz zKG)Q6CiE*x22fkJH%+7aa6AM**$;AT@v@0UfnPRYr0+Uh?#Nqx_HXr}`g}hKNx@Lt z=XW{re<;+h33T^oyu4uLhQ)8%jMTR~T-%yTRmo{L24sZlq(dzKM3lv|a*gePurtSO zxBW&iqBlcGY25jR3&7mZ3{b%Ip*cZMm7GJ@)S^BUAmkX}c0ecwbR$v>ufENUPsOvY z8BI`CINMO@=wgb9m@CJ>uI1jfQhKUw6a)!ynz^`0YbUX9TDD$wSc%D6qp~e!wih5M zaV|gXBSTaWMVFGovoy)-s+PVlP@Zc0sm%V~=Q)$4kEzTN^`mR1&I(IWz7WXY)}OMdXF+pd}O=m=teXF19V1on0M^~-y|mp`pgjh2t-)8 zh!w#J5;uK$x?@Q<9UqHv-+=E)&baYWtW$}DOmdw(ACxlr(3pvm0+gv5%;-s7^!yP& zzQpj|e`-n28H}hzBm)6%OP^cGuy3Oj#VzbD-_uef&n77-Y{UJ1_3d@iAL>RV^&$ zr^wFjt>x*T$6hV^{Je>e5*3Y6HRCx5)rcUPL6tAmprl;=+Hu>RlbxiUa|l#Xads!) z{U8t&Bh^fe#tPV=7F@@Js~MpG`BCOYwz*w;FquD<<0!Miolx~%0nZkBcmT(z7t+3E ziN|B>r<{+UTVsQ*U9F7K8$b}iU#Dt{>;J$6cXvvq-YGL(=C2vJOI-B#St zgi7U&z5Dq3IzSt`Cgg~N(K@aTO($|L=Y10 zXMu;Si@y5+W>h_NA6$5g3L_wX&hn{L#g>EparWyy;cF%g7SJ2;d};NGt=rS5OuvCR zz1L@@;K37x(^qxw8XLYdmfjgwYLRMnU1+NP~ta~vSCS*x;jYV1hfbfd64 zK|}~Znu_x`0nQ5k5D;Ir$PciNxUg_?p+!tcl!_bbOd%Cvm-00;tU5Asa+-8M0yU^d zbg->kS1}j;;bL0&p{B#GN)co3Ox@(B$8$s5udj@JXnc|p%I$YNd=w6_UM@W{k_S z@jR13Em$tq$RYS@ZTA2Rl6Z5?f>euQcCmgeemy!D1iPjZXo48S*q{6*o=BJ^wsyqV zGx>PFyTQ+6+!?OCHYdtB12U^NBd0zPk7V zMa6Zs!5i~4)8Xe{ zj=1a~w&3#6Y~)QSlTYa;IznH-=c(2U4axXhiBM#@>;9mVjt699h#W+S7pQgVvj2+5 zf_+TAiJOVLd|+UASeje>^kpnW0# zW6iR_*izLedgpjT-_}0?h0I>ps^N#3#UM(hl?-JVJOPa^D$Gc|6AEke_6IVEXqtLF z8z|v9UwlQik;f(>*r5ZkI~fKu2*xRwoL`Rrfy=fJFkc8R$E_Fz_(1!GGTkz^R3m?YK-_hR9~mdYzks z^5(lKscU=yBUEamN?z!MrM4Yte3~f<-!G%-b)LMJqNS?CVd5$I573i_*6sW8Ay$*g zE&JzdS$zN!970;i_;97n_5{5T%x(M~c%3Y)hoz?Vb&h~uwW?Vk({!X3X|EYUGHrC{!O@ZP45ogE)# zBtih7b5RrNzZ1B8vBtLghdUCKcCPA~|L5AqZ1b<$)*1TWVTOCZN@aH(>l42g9-eAc zTgE&6yom^)YK;9W(zw9YiIdT&FUyaDl?IK!6}^@*IHnmkGI^{yn5B1+!Ct|X+EC{R zZQYVfV_L6{B#=1bx0YF19lqt;t^g`%uoJkz#m?EQmm0Frk=P03D;?W{!ZO|=6%Fyp zZww3Zdt5g`#V=2(Ug0=&n$#8Nk3nMiSd_|20mdg$b)ZjwoEV z)IZC2d8XD8%&3yTpccku2h|X`bblNtNosm7g1VI_E^Mmcs&?`HCe&ZIFxZWcvYyKKNk>uRC` z#5s<<_H|*^J8bY80T(g2`OE_SKz@GV?F}H} zU03^S#$;wP5=jIJD3h8*wsq$OIwwMLY5$}58YX^n>q0KM5E@ z*l)I^u`P}$El$wx|9c!gR!?O56GzQ@x>B#y5v_wsWLF>|&~7qHHiBl{Je=w?MvR&n z6W8c})UqI_CKEG$zmDZ^u`y0}l_R1*j={p;S>>_)Z1g9Cg`$z5^!-admM2s(k_ zo3ohqdMb(Cr#`h&I&?C_N;6dnmU(2s^Pl7RZ;bYQ>>+M5D-85F8G`FySMZ~Yl% zU(hOfq02B(Ae5ViONzfiH`mo-X1Q4DV(ey6sfB7$lx#T{b_dhn}I9WdSOA{kFpbAJ0-TB`lF@> zFJUr_r-6pV31BVYXp5wO(ebr8rcqN-obQ5GA`#ut(VfN?Pd}H+?P5!22{Mj(GA`!z zTyyOSEoEn$Yg7ntTWm2ZL?moXEWO&cf)IwD`nI!M1;TxYthcw1@I1x;?Gxtrg^`bV zn7_S7wB?q;(*okV#P3k%YdXC~t2R-xp{APGmlh=KMKRXd#o54o@t@bwi~2cLg1fd!K)i zx+RB)y_sc$^-#8`BmbX3$6a0~e6tjRC8*EZzE?`QX6&BpOh+6R?~CE!T1&fNS&8a{ z(R}}&(#Bl6iOOOX7aiqovTUz%D?PD=pTon$;QDX5x~>=f`jjfPK1Gptczz;MD6-RjZ6e4-jDo-F|4nvbZ|Q z`1T6jlYez9GSC5CO3BeViDg)%V_g?*yGOxq?q#K&Rz{mKvXE*>OWW)%DkYXfqVDvb z8wFG2)9(g)Oc{V?#eel1b7k9>8tP)8lF+)+Yr-A-Lo`~W!u|C>j5}zydVb@gj_^(= zY3s*lyaGwcECm!R{T;0nn&NM!oRdfZHgiksL{^wp{w6lZFHZAVpS@k6dObeg=z-5W z(NK*`3lsHEL@AgE9>nvlm+2|@yhEiQ)WPs1JUcbg;M zvIrhpl@uKy&RLB*tq0Q%&Hl}ZUl552#5leLS{pBhtfQor%u0uR-Qn7EnHkdogD_Vo z>RX{aycCn5HTzMbMi-!(3`SAK&U=)mBF^nwa;^D{ugz4WWp_rpCAw+;TrT0HWAA5x|0XFL%Fa~Kbt6LD06$QI^uxquAMKljcI!mmkSk74z0=^^2Lg0jCvzPB8Cb&MQ z;0(R4u#6m0w0IIr^wKV05Kp`?BIl{pdT%p@Xn$**t8V(JeVx^K($3Elst+0(Kl%}E zNAP#N#;I@6Fh3Ea)N*5L#LPA4ssgH6B9CW@xxbiMYiXiQQMh-jn)^=7&971FkAFbT zo}&G3uW~h2ioJ%Sf==wt_ubXO<%?U8sC}1^Hm-@V2q)P$H^O8pwm?M zxuL8$7b|JKa-_q5k#Xr0Q12&gE)E)QgLGdAwt3FHtV;0)&kj%=KokFu5w;Ix{Fwv5 zH$t%S7jV1(N?!S*`?RhAue-(C-|aAv&nruGKKNEGm$2DYs035~#v=Lg_)FrZyR}JM zC2-~YCezW=5l2BjS}g0a-xD2c?;HV+o0nW zvKl6eo)U9W<>s_@3%!cf3ZV2}?#FBX^j_cl?`DXW$Cc$@UI2plt3Bljit)X!BT+QT(0#M`M1V`LLLZ2 z;V&lNy{5nhC z2tpf8PQk-K9HwIJ@wpQvbB`~1LtnxOLjNy@IEFgpcDNPt@#;tIz8S4~LZ5qO3s8Mk zNu=$yqo+zVwu2E>R7sAx$$PB`uEsaXNahWAu_MNt-rpM5NICtoPY@2Uf{~6(6;szO zhE802f2eV9@$*u6P)muWJ-ySaS!gL^_9UgD{=;Hq2P4tH8Yzk3d<}TAC`$mG`(GOI zBpiLm($?wC6B2CkXysVKB)UhQeq2P=rC~dPQS**cHMekY0N!jVNjSysXuhW6_}WQP zJWFR3g*CXs!&!X-_Z?<1Z-Y^05R&{%!4se9g|Enn->?bCxS=9!V`1|g?xVLGP0zFY z^W94If2?=1B6Qw8^|!x;2!P)9fGGY)^NUCwz^}{Hm#VWb(dyoDRjZqf9*{o2m=`GF zm&Tx;7ggh*AO_-i_IE^~1TCXDiIHNR@qym+j>F!gcWDUKlh$>em5^}n8<_WyX0XKU zEV7Ux^H9E{8vPi74Jhw>yqyMoLz$xqI2bhF-SmihHRee_PPcl(X%#dTKUBruxs~%L zKFj3OzG@I^Aq(Me-tN;-u+_ZZyzqV_+ruV}%)jy8>a|pct$<=`O89gsf^Z1}nv0C1 zQoTlDv`uWu#{K2<{>CzX9!5BhG7#37NPDEf#WA!O%gvD?-(QA!xVujzgPTYQv@w0D zQFo}rv;tE`VKWL1DkLGw+p!Q{yfC~1x&rWf@2)ORh7A$IPjerLX7M{lbVCt45` z6_m^*I>9%*p@3TRNYMZEiHaBW!S{O3doz5h>3iaS8SvQ&$Q7LVhOFX-%9xY$P^uKx z-VGzDs;OrVG2#G)E|<8`rj$uI9r}O?9pQ9?*jYKlE8(a1^%nr1%QLfE2e|R|f z4-eL?sM2R1TWI^3+3EpY3SdRqbvNdp-leoaqQ-Wf8|b_Ig)p0gzRW1RN<=tC@J(w0 z$CLX{U!FvXJVKYs z09>%9b32IEODQqv4^8vhX*Z8WueI|iY)O}MCos9+Jf;!Q__s_=O%OR1GVv&t_uA?D zmplr6t{@(U>`yeQZadfl3IoS&cz#5R;GL0AL$D6JZ@-=YF zXZG1laq-b&-{SlYlg17^_wx3f`q$V!)IRh+Tk08wx49!b z{l-$Uo9Mf3-9OZPLHJ@DV`d=lp*>wO2kz|8GzX!yEh}PiO!t&YKNGBf?fBlE3v9$` z@;|zPpLpNdm_0#{hx_Wh<)a;hCK~hGJRA6=nYSCb2dP@UJE;7S4~3Pvvs{ab#F1}o za#})NFi_dM+o~`4+2=KK+CkwWI{dLu5lt1C^( z7ULVnClVw*4#NzRp3fKuLdDqN4{G>NpPHCZF|3((-H5$Y*r#~l?tHaiM*lf^(Hl~H z>%9Z3KJAxOyC&MHpdM7y$3}@Ae##ue^0y|P4-J6rlzIA*p}Ux#Se{}ASsj|UbNbUt z3@LFW;Q&i(>>r=7L~d)oLdqVJI8-+NM)ks8N<&gOXCnGN6XwuTQec->n7sqJPZwTn zO`Ijt@^j)U7M$IJveSVKLrm;?cQa9AmYpD8MO6J)#X#<+4={+@E3xw6yV<5SK!)1Xk2*7qnb(H1pp%=Z8Ai zzH_b(J-VoJb;{iYf5!DfR9m_KK0Kah`XMnFs;TKRDENLtp9!1U3YoL;ML*A(y`+ZS z9mQ;qLqA4M4l4tVn?S!Srbx`v+_N0x8)5Ae*ykUANObngBI%&GQdgWWZWNHCPcR}C z`d{a6MC%6SAp_+qTEsnIG!iHQN}Rqp z4ZP2hkLh431^fBE4*!I&&JQp!FnD+bT4$S(aBy&Tj7+nC2(ck*MKn>$?~hsE3uPln zI`@$AwYc-_Lv`7dn20<9*^>Ow_?wiGmS_A*FTLsJS+P~sPJPqacoqmL;hN8_yt2RE z7i$ih;IP$D>b66pHFw0JxI^GxPPi^ge(maCKK>9Jq=b$AWZ$SEA0sH2`tBYnLzsH2 zquL+T{@}*8B@UXz8 ze`DkDZ%0R5P#5T{Gj`xyKNCVXYWuKB(Cc5#u;h3e#D%QJ70URU;}#bJWN!ouJd7QMR0rY58?99%nF z*Z1<-e$KT|8%lH&DrtcG^V(OQNU*+6{Sr8H6xn|xM7)R|5qUMCJkc*#&QDBh8KvFz za$kV`BMn&C`nE*qz^Io06NI~ex<;FvE!0CtKXM{lU_awqEdg2267aRmF>orfM2L~U zcS?Ja5YAxxQA4$AAo7*xLd`R}tg0CXjJ|np?4O3fA$SI*0#Oe2pV6T9cT4a2vIRU_ z+KG)*Y0o*=#U>B5M#k|Xq8L0V9^-l8i25n)03%Ov0O@00bW9=du~ zV-eoh^NBP}dcB^@rc29vr}#I@a) zieh@zI5MIhNmN``<~KD(IQFLG1LBv7iJNEOE8sfYuxlu8!&{53Jumkb<>~KRBh3ga z6ayeuEr;sT+lOdx9LyFMUlKcnFj^d#yQ=bs<=}=ZxpyVAaQ>>UskvYG=G^%KOm@DWbQ65RkgL?M2a*_>tEbyLOp1K z5@NiE-hL=6ed7@Qt)=k)Y6WYFBu+Xfd5;PLSAG|tXGI@&$yLWKCCgxsQ`JB|f9Zvta@M#vqW-|6)?;kC&-Gzc!4Pt z-f3iEurkfZK24ZzU0Mk5Iq{mqN&I>|VjM*(JQW=%W4kSehPxk|^7C0Lre2mS5c}Dm zviD1@)z{T+G`Q_ADc(e=Vs&~D@E7(6P4NR7YT(_B2|^lNy;})hM0Iu3<|dQ1wRK&{ ztkF-I^~y9tEPpE&oT@Zgy^NbMOA4PD0oKGcZUvK*i_<5Nl-QSJQ_~XJgH6w}{nFDZ zL4EDeV!9%^kGdljc5Ts@V&{y)c#QeisK#}JZ~Jj7SHqwe(AmD)QuaFA;Y0x+lY;F- zCi$?&zHLHA{B$CsS4(YP8n}2WGIR_4sw7|EuL5tulXa?qcV$m|C>FjM5G(AgS#>6S zBurci36i=M$IxSX55*Xl#6@v9acL4c$5N{4kUte6BQ@_k>EAyXMgvB;M+zlBK=ET~ zdyvWFeVM2tlzYRXhI_WbVFPZW&c=js?(u6bHKEhw{%HQ|$}O1G*9;1>`CU}*%F3#0 z7}MIs<~_dib8zy6ms9+DFX7*>Oh6x%th30@M)@)DnawB}fqQKmJWmCLJ?*=Su2{MQ zUNSu=)zXxU?|xwh+;edV=YoGrbFffGFMHs;g$c)|BKmr%>Z?ONJ>5e{@w3Z7u+fe* z2nnJNZi{M_ijxA{xI60_9 zD+YZ4gnOp{_Gg4CilA4j^g6%wYKu9PEHMqQW36OJgDSP`67 zhm~lO(hSHutcdXvsn~`Ww7z>jEb?JK1XfrNCmtChbh1f2pD)+RtscJ=k=#0^#OSu< zzR(c#$bfWQ6?se(OP#O4@I^kG=m4W?{cs?;p{qe?rAbaDnhmey$OE4g+^q;a%64y_ z$lNrmoa7PMm|5(mBq1g1h2ARmz>iZIfG@yJ3RB>)AVamPs+A*J{61;^$22)Q5O_)Al z#t!|q?bt9@f* zYfF8C`3y|fL6pUT1_y%p9=osJ@9rLb$I=y$=>aZ{&+2v7x6G1V#AB+uDZ$ctSXXRc z*_=-NkO|ZjRX`_?1Xqm}faL86oVA^*&RxBVXdvf$8nk>Gk=-(G~;N7aZTt_6q z$hewW6;h$YoFH?4GDH|mxYClSU>d_ZyHE5H%KLve3&rsRx)X4Sb?8=L$b$pzdxkmQ@!>a`FbQ2^B(( zY8)+Rq6U*|Eqs5qVxf>>qgH;IoSyCSK5i*Zc}9mBWe4f%cG6Sn*ZEw(psnAJDPbBKr_;9~0w{Iuhiv#VKKy2&M) zV;Z^QBai5s4qKPemZ>RHuhX7N9zMR-&U_6XuT$j%v2j=@-nJY#dl>B>2cIw_J(Li9 zM)<7SQ@{yNB<#aQAy!zjmd!&0Oh`DSm-pk-!j+(wxd+c2>U4UA9QcfCs*mW>(Y`~G zZs0@eE_HNtu(JkN%ZJS?NFHJo#oa|930L`2StHQV7N2{}$o6ox(9yG)6?yj za56v#Y#m+S7Yu*Ne{(j8{0|Hq2lnH%W?m_0Q+#9>&}T@o=4}2-Ms-SH?U+GKUTuB5 z^whiX8qa}Zs)lI@H!I63qb+!y-@qhXAD6z7X64S*ZWUSCZ#E3>a7TOS+8j)I4lHsI z&TCVcC#?zTxF=OqGy<^28mUc!%bvH~pB^kBT{Ri_%o@B<%Dvx=c;XUk_}8vB@94Bu(hFa@4`($B}2e9Y|pcUQ+axK9PcQS6)A^iT(`#9 zNTutk4p(meM)*_kp0K_Y z%cos{I`N$^sKR;@*}ts=nW{GW3Z~i~m&2r#YtebH*hJx^;_*aO2Sx54ieC{K!o=*^ z{3QqD8x-vY=tcQ3=zH7DWvLvTqx?KHeS7KUee@(zKJBNZ3`axhOe}a0I}eX(oQnH7 z$ijC|y9og*Mk@*n-yJ@8Df`M#40mYY&YehenvN&>GBb&ryvGfZM44{c2pzB1ps}mU-DY#1U^yat!ZLYbT49NIhP0wJeNZO z?QuvEAvk!qXCv5k`t={pLlRtSPfm6z1N@$O9o3UjRBav>wE|TKdG4_(vx;qkpM#1l zt#Q>Vsx>=0P1$s>oN014_*X|e!9mP^23?dv%zM@!KVKffk)TJo1Mtmb`gG0e}qPbz3mN)G$NvrV6m#S!@E_xHu6U9=>iIfG2F|aDIC(MuIR@!$H&G8 z%SBt;hi5B?XGAywHRnN+E<XY%HL>=ca~w;-pc>dTA6YFFpeSAu^rA}|s20%44EWe-C1 zf`ZBlT75ET&neltxw%i4J~oIJP4`_z?KAsCh@e&GVztxrSo6KBB{$-$PyUtUodm~a zgON9dO4!c-AjjJ&BG23S%9tup4@<~oe?>eo$3@CMorAzz8~})TVAtYB)*lAf4yoHP;Qik~39!F$m4dpa6sf^7U;o&BR-Jt->*i z0US)munm`=agd>FTk(EkgpZz}; zOwJoQMBSa2y(txkM9TtVbduZkYKybp{TzK8L~~8dR28b&Fj(l7hNv(M(yA{G#7=qU$-en-j?lK zvs8tU>{rW>q|>yEd2v|9cH>F-G9_>}B0NYqSojTLP96eDwK$%ju9%FyI}nsRSzAt5wA0FBKK~g46uDhv)ugWmQ#2 z^w}rqzQ|j61m)B5a3E?L;?7@^(zK9ewFuIrBhj=V1wVjG8YE-gh#pn3v_d!LuO>IL z=M;5z63n-$=yxv(|2ozCnBD*!3&L9Ak-a|t8@(Fe@~!tEE%XNg39ENuwfsQ&ONa}z zva(4pYX}FYz^_FFz-A*G8(hseCTgFexuJgT4oT<1-%~osrKb&nK$QM&S~vM(M=A0) zNoaYWw_d@zrT5yo8_Exg*=3)^hsuFif?xSpeT*ld z3Vw3Jv-tJe2CLC5vqvkT9RW6_?3!GEUf5_7<}IAI@+p1&(wwwop;=BHDF%E@H;p^> zh8aYm*U#|w zsujJtI*)hz)`X^nA5R?J114*0?@#-Zj=m|-Qv+*_!ZI0}k2^vI>G`^7b8*+bn0fxkQZ%7j+xJ?e8-}&Y;N;Iu&C{Wc> z<$_i9dbizhz$DjZNM$9~{;}})uWe&rZgyGowVBukcFy38FMiZS+2oq2P1ICw+(Mf0 zAT4)xhS{ZrsJrSd?jC+C|DNB{Imux6?f4kMNQVzOcXlbSC4qRGM~Fny0?rQ!Ny?OU zw3Ukc$30-j520YYRszsPZjxON*H$3AWHO_I!PR=kOx91fT>cZ;q*W~!WC}?O=H$|x zk&g~Df%0|IN|J*S@YxMIU2de1a5oa@0WU|SEN=(XGm8?SDC}B*0q*!KkKjpU(7~{` zS=jvGk9FJ z)Adc~l{R%}wR*V8@_oCj-Sl3kv-pBum%itn{sU?6guXaVvEtqt!8zq3zeGRN5(KAccTPE4EsUezjWUGg@nn+sGMyz8ZkC)i6GPj-H?8!a$S)#xo2x|5Z zbt*T5HN;(E{k63%A6fKoMkXdEQV>6!er{mn;xaIThlg^LxYL@!WW(zBs0xSZks}z| z;h!Y_+LS7$G&@8!#D(?qS^H6y7p-dH7yat&$F!xG(iWMRSoc4p6SvMIQhIrbdByFo=U%EP7;B8OWeXHdz z+C`iIm{%wv|76gn4LT5pP7cX8=10AE-ayJWQ3bttTlyMK(S=S=S#}pMo(p{>I6)C0 z!r3XhT5ET`x+JZ4zY05)0<1{{07o9rlLP8FNCd)19X&%K6yrHnQ{3b9sKQ&}{y4r@ zZPT|$(diUJCB@8?f`LwkZ(g*=Rwt`3IGnniR1@UdJSjE&B?Q@t7roj)_A-T(ULBDC zN^h82b*$s)ToGLG$L5slJdd|9xe+G5)HH#E(tgWCJ##eLkSFwIT*2|#+D)OW(>}H& zdBU`vJN6fT^?KaI#;$Gw1H;7MDm84a^8~Zg3Z-}twKQL8u4v@L-i^SKCTdB`kNL(X zo(e-Ndb&Oyda%c~aPp{X&sJe3eBIoflS4Y~gTu+RG}VXk8C9%CBbCPF{zw#(PEZ!t zQJ)!l?5@b02_AVej?;P3${2!J^^)E=wYMz?2+P?oLHA#m8^gc$ z5uJr4)gkL7`rwMru4{bh<)njpIt_CRb)lsloA?5oWy4JqH2n!b+=(vo4r;jARoMA} zOKwrf_>knvPafmyThS#%TYK+GiQKl_4@&@6h)ga_uTH&p+ch*sDIspQjdz4bA(!#h z-%T!SFGPl=gu>`m8e004b?YBy@X>i$2+25o@KfNCw6=k`dfDc{#r&OOLd5k$Br4|L zpY8Cd>5FR&CdbFe!!Iu{<-t9d@xY975Q>!Vl`pJR`JMKhlYMsZCpN7n4V{OTgTjo3 zN$H}%IQiiD>b@xul1t1`_8qdAGfLq*^!6AAj&H)6+Dv~=JH8e8?6dwvg?YjY)dqzw zJibOYvAs?pF=W=7g{>O)_zZ4slU~8?p(1?VCaA@7GysR^h7`FJ-dLj9GZE^<{8lg% za}Z%+&`Uf$4K_96ef;TUM%RX(8wL{AAP}_L^$ym`CAo=+;6|)fYY@BUUCgM zU|Uc~uiU`)@p945HsQ0Y0kc;7SCTO9fm~_Wa~?|@tNETzHh8_)yUHSkoy$W^c%9(0 z+TmBcy*=A3NhJ*nDP`dem;4Ryu0JMg`7heSy@Q3$ue|)rthwcnX8ORbETorvMPB~; z=fnDKmx3_r#*pC+xIZiwXjmc?XMRn;1WxqHM9qb3zq7S>v^Uz3*a=khynOtA%#~=V zm>VKMQLu6gve-9dvs!kP-4;__36Zoxj_}IiMlMIXK!0U}w3Bp#>wS5-eYsWiYbQ|X zLY|F|Gf7dmYw++su`d|RkJal@8G)p+=l_U*T=oi<%dWD#b2zF$&)!gyDT|UxD-^T- zIfRYp%D?GULRJE0S77WicA{5ZXQUu7YcXxAxM0I{1peH^6nAs#P5F`ymf7Y;Z^mqx8S-bl!51MrjkC<<0rP{pl;9;S ztySw*HzSRhbj3R|2DM!nx!V2^7V7=^JH&Q4cWIa3D`;6WO9ro@2izm5j^s6{?&9sI zPd{zu3tGz|sM3zGC|`NwX{O!0{v!LSrXSNTYP7o!UY@anWAt}D-A)mezNE*$FyWvb zo%_}Br|Xjl0|oQ3N6Gr!*22^AJj;N=43Hm~f|q^Fwt@>-;QfX-J`7ydPv=e@H@7~J zCXkTN|BAl`gxmTJ-}2|vo2N^5#N4RwWGJi<&&Mky?|NUYCeKR_xE3XE>ruo6u6zTztAbqvAVm0BMh=_KlB;e(tT2t#Dax)fwIG?3* zv5@`0NPFw3DBJK`c<65F25FG)Rs@j}k&sU5?oLHQx?5Db1StU-Ko}a7?xCeS28NmM z!S{X7`F-o0wZ1>T#Vmy3Kc9Q9>)QL;d*AZCBO@9iV*9CglBMIqK+D2&1_ zuJo?geS1{V&!K>CduGyD{OYrxfu8K0R@%@Wo0QqAG3OZG8OQt*5TlF4Tt{e}L>o*y ze7dkCI)H1S+|PlR+-EN(SK5g@Wn1wFxCUP2JvSN@Ct6HY{*$2mC!@ZsfwyOL6yVp0 ze$B1$FVoa@c6QovSc>Zkr7Vg3qdNc>T;4u_wMSQb7IuxQ4^xC>L zgGVbLam{9uxjUg1Sgs#|&VEm6Z{Q1CC5^AaublEH=f>KMt1f&<9+L;Z#TNVMFuf+z zBn?|JN!B)}*k2E6dzOJzq^GAi-F@yy3szSL^xuVb?7QLi2(hr|`U4><)nZovQriHA zt!*pxeRp!r;Opz#FmrWG#9@B8^){MwW0#939kt8rfo2_$ixED=wH6W|xjTj=O&w4gFdZ)OBs4>w zW>RVO`CVN)r!~h1l6CMIu66)v6`x>4?yjftC!6Q({|jnN69#aq1#fwYW*fvh;@RK; z?v2Pu*T!;b++9n&O-|WW*9rptn~z>EF}bVS9^9A~EgW1rU=%+w?O{XZfrJVf zz&kxSuPQYErCJUEmnL-^}*omx*eDH@=m`aO1@pa+{ zY27ywo4u0mHY}h#!;ES6Rz00jyzWJTZQWrsZJ!Ao_B$i>o94JPyTDflq@O9W2}D}@JHwNTAi9O>gPVJZ>16Y~v)AR7?EomBkZ{`w$H~=If1h@$>EB?y zo{GQWRwTd@@zNgaP@t1Cm7{e&`lW{TvtWC74|F*}tle;nHw6dc)w5(APd5n3RRF04 zZIop!o^~%-CkTjx!2P7u2C(H@ANKEsikp?40=PSay4Byk6QTpB)}?Z)Z%@DW`0@g16LMV(GquMzQ-V#AvKy3$ z@^;1LT5aGmR*Jh4__Hx8m1wN%TO?woYvsll$p`<1gdw5)A?K;xkOvUU{Gi)aR`v-l zfOEP{U&*a&CIt{AvD?ii-n}4{u|E!GOG#HLJ^r!ayU@H?&Ll1R{chtitcE!RE4fBk zkI_+Brr@{Ap!wz|P8cnS|BKHU|D#xOkX?bCiLxaSMg!0cfG8C08gQZRVk7~VS>J13 z#uaLTrPb{TmHf@GM4&iU2pRBR3YCWC7KdJv_%eoIic^nm44yI2`nvFDnZx?p37B{x zTgv{XoAT|mr(UtDN0_P01czrKZ{~QJ%?lok&mRS z->Bss=QkPIDM4`R$D4nm8x#|HD;ZmIRYs=I z&}%F8_`ZXWF`_Np?6hA=hz=Zz;QR{*fEcqeJRV;jnVgYCc7nKo2sAlqNfLzmI4nK6 zi4xTwaj>hHVzxTSq4Rfb%#5Z-(-PPCQhwz7#~RjA+UeM@*i4wez5X;){}@Qc zETagPHCk)bsNHPgOF4*lv`b3%<)-TU1)ky_X==K` zPu*5?IgKCan!VlD8L0gfpTT$4wTwBA&rd{j5*Ug+T6Kjlbbdg>5)l{xj?ge4qv$Ku zDiq9DKUq2o0lXw)Ys(w{t38mhujn9ivG+?w>I79f@Tk*OTTT8M{u=(u$Ip=k7=Rjp zq)T$U9zuR{d}v0RxDDxEfe9wi3ghN6e^(Mk0XyD@ri;*gV3(Gbb_O|(cN#YXCd270 zs=9<`ToF}#&pw^4QtM0SM_CX@peRw}W zCO0+*Eg6K-%oq-!J=cWE;F^}p04yy_6Upqt!dVlMgp36EaG9Z=*UGu_zt4Yt9S#s> zMA-9El#f}0Ch|#$;9Ai`!foGUfI$?++D8`*0;+%$V59w|4Lqhj?2&D0=Dq$c)~>2l zD*uUQ45*5_!8735FTC{TK(_G+;SNicU-^)%WGKPj%9h)!vBX7nd}p|50s-RF(4-Ch z7Wcr%f8%I7g(})_Z*N&zpS5U7vIC%#_2y=Z&sHyVY+@8)^%!JkdO?CnW;5h zyEqYCZVz}nQ)^}!>f8MA&qVu{unK+k%_}_74B?xznf6g0@UlBvCsrlS41!ars0#0 zUgl`TPIJrM@kH+jQsE;@Ng;}T$@B;9 zUxP=5GhVSfc45G4Z7h}d2{1-un(XKAq>d<1IT-NTiUX zF+LwEfb7gy^cBZs&I9T{EhK0x1gMHvay#lf<+CtuMFT6upL0j-I!0TvHBZegKn6}o zOz9a{m-c2P{QaY5Y7>5quKY9zKUyk+v$EN0`=O`LKStp*BH_YM-$!!~>mXoK$Uq4_ zTY=HT=ZStK%EQeN`ax$Wp18D(av^=wmlURjc~uAZ0Kw1aiv3aygxlQg`w7tYzmpf3 zWHKR!(6LrZj?#N}xmL)3RWU9v&VBS348lzy?#3;n`%RCd2!On068H%HMd?lFH%;vKc&gOIdfDk7E^{+*pveLND&nZ020im}ZFd-;fHWG|59p<2VCx zh?37DJl#?S*(0UZF@61yemOLTALP%8p$1~vby@K3TyEKvNE1provrB}>>0V2ORH5f<+5Is_do#I&hhZSN3C!PUO;Vm&e!8PRq>RP>Ic*g zpO7yWxAboUT9Fj5XEm^W0_#+r#{vm7LV%-6!Rf!+9`)Mj0qdD^nTY}VRB^4XT6@z@ zBVF^;df8R}R@kp^7K;A)e@3bRm?@8>Z4wfU1ttblhO|LNO9z~{LDAPcCa)y6-*nv} zKzCgLOa(>4$nGZWcQ3TQ6^?8(9bkTXAt26CUd=+63aF(3z8LtptF;GT;MSUU{9Mq7 z%m$xymyY0l?CjCWrY%oVdK$8Nj(hX8jQhcOeYzO#>p7(MwH;`s`E1xd$m_lQb#!Mm z^1uvXcAXh|TB^Ee246GUcRGR|ADip_I$u`u@394zkVhH?>;UV07y+bK{2)$9-y4*F zC5v@vU{ISc@gWZ{q!xX23U)fr3(N;^m=Om^w}UiEIv@|mK3eEyCQXO_%uKM>YhL2a z8PzVB{}Nz~5J7>byWrJz{{(?9Y#3C5#Y2fP!o%m?nahk(-6Loe%KS-DbY&r&i@m1{ zF3{kUEgAgH6%BhRdC#%j6-_}ve2!)XlQ`q;UZ_+)=o8AKUD&|LQ^Atz>VsY*RG&*= z=MXnNYHCC9Hq;>{l$RUzCI>+2p>+B)p0D3XWdjG4W@culU|DbH14_Oo*6o=%=Ei?} z;Frf?3#e%>3TuZduh{JV4L_kv`?(5Fm~Fx++5$g~p4C%Be-*W zU~mM0B9YqN86fajCjuj6uQ}v>{(=8&)j&tgeE(=v)`D{QRT@AUIS=Uc>|{3l6UFgm zY!$5SO?Pk2F0CAha(AZ5_?y{s>`O4@T8dwxQNizT0;A&KxchnIso4K&{J`h-Rqz2Q zq`XG&pNJqJv2w;KFtuQR8`vE1JjX9XkA2$|=<9oZe9%1AL=4x~#;rAS8Crkm-f5=hM`_z!>@wKfuQlDu(i|Ups2{0h zB^)I$nULQpRDP(YytO;nQbh^uH{B1vVHMwhQqw%1J`~kXGk{heP&&c&cM$Y!u)}Jo zM!!^xLCJ^ZqWSpx^8K?Kc*iMZea0D*6K#XAHEBAIgTT7z58Gz}jrFcZp1$1A3NqO$ zb>Gw5?0!xG%~wE9vzLFSNV#T0Hb56u{h4YbuUY)K#t8tw$QSDF%oS9Av1XotC`FB% zn|z)1X`WHtt4jELJ}&y_KZI0`&ik7&${vZd8m<1qucBXr?czLA3oQJ$xR`T4KwMq1 zp5I2Y%ofTo0Roa!uJX)=;zVs_F-Dv>`F7WvwfP;C{BEbaY(S%rnSt=1++0)Dnle~8 zR%-Zq%SAfTgl&iQ*S4gXJxd#DfASfg0_2-Lc3Ra*fi5nlWNCc{yCS!0*Z&knB3Fr! z)bBne#|krO9XAx-(a8gwtgPEihU?{rzcP45v@uMg86X`o9R$#cOdX*F-N$kgtF)j~ zt3{|}ulufNMSRq&(K8cl0w7OVi)bVG8EW*Jr#JEZ+6~FHw4&IC_B&n(u>vFb$7JDI z9Nx5>Tm=v*b|0av3rKN+Iw17-o9KNsk``dyRA3U$RzGg2?d%glqDiPT^lMomq&*3H zDm{(w_QzIBCL($;lOyZd-}8==doNqmqv;bytaD=cPHpyI^wqfb`oHzm;S%0XAHy@E z7_I!{`#JtMT`#F*yW@bQxlmtAXk=BWPCS7j-?t{8@%g!@mzIpx`6l0dFOv@TQ=GQ~ zhC*dh9KR`_g1$8Kl1)f+v0APkv!AChZo)Jp@6 zz}sNUuyDNz#H%j{_zs?`i(^wCs{`U2CmdYSWD;fFVx?kOx5252MWd<%C52++pS?NK z+JA&PPFmM$E+HcJ29soT&+g56XPp-Rs!%r-%jt$ez^d6zf;Zwe5&<7Ttk#`B6n4D# zQH+F*t8^6ik`5GgbOEaQ?xCJ1dF?XYCqu0F|5RqGNJ#zR{h`Ty$HnZ%*<$&KiZN_^ zHjDhRnTqbTYt*IZM*4O;Q$IzF&nySme+ zu*$xr$?pQ}G!i>owGi9AYg)#8f^GPE_kn?|5s!RFw4n+5XeK&el@cK=n9v+0f6Qj;0Blt`)`-Uf=LlH{Ip2@6y+_fLqP6#y!(fk0|Ej10Wn8yf}4j|)F{F< zd(-Ohp#gl!?1cA~HRaQ2?{`{M2D$#8O>!pAyFEU!B5DXv(S%oc$)v@( z^L)TO^~TI|6xO@SWB08Fw#q7)+j&EJv>*D9gv;8J>NHy_D|&fmJP$nO(`xF1aG=sJ z+foOt0nNR*gpAB08WUzn2Fbl=5)2Fk87VSTF911R8ynz$xzKPNW#a9MQLlLWPp(VI*Xngv)ok49N}J-1`b54cBp7=67Z)k0}O zfegt)&pp4LDykB%q^JaQ9%a z{RQ+1V14N3>$^SGY^wG%*hZvrH!;@K5Nz1LuSNE0TN0jag<$7OLh8W$X5foO(gw7AnJL3 zpX8BdLc5OHCrur3)6=8Hg-R<^F(5^SPF@|O+OQq3*o6fn;J!P(gpS!Iz7&+ptE;c_ zT7aWhwpYsXL!-vQ&M z%QXKo8FYw3jUet~WH5uU`p_AK$2C8uol)I%G&M@6l9u6c2D`{OsmFkfKw?jV4VxI1|bf=@SWDDuQDFK?+~)dfUqH-3VyGkh&n#e4jzO1jfP19ZFmC?_b8M zM)&simVHAv^)+QMEM9OqF;Q3#?|p{K`~v$`fh?zQnV>r-K3?J1UODqJcXRo;Z5771an6%vgKBo z^}Ib6wnViF32p##(}zU+d5ZvCkO`b%A9+C!k6{qW(ycfP!C8)ojeU#GN=sL9D+Vft zbd_Hp=-<(`@>W+DS0ru9($w%WCi6{71p*T`{H7Km3Sa8H5I^ojo)ngCXpN1YY>n)# zStO+|=9YcfFSc20(uT@z2B!YJO(E#Y;h7|G1pekhg^vW0R7p16k{wS&3hh=o+TGw1)ul0+$2I{mPTPY_a@38fSD4A zcI4-2c=t2xmt9oD#Kf+d;;U0oSY_cIM&DiU-e77$N-dcZ#h@jzBpsG6ajPLkVU8^+ z4Y$pRKj+|YoztNJT7~B4wi?@DC`` zZTFA#^-$rA@zol7+{v0XcZ^Z&M!1bR00%F(Fq_52)2)ma7Vh`OfKhj8>8b6HYVhG< z&GnMyy#}#o{U>P?Mi$w6(0t5yAe*16x{JA!Gr%DGaN|lC*x}@rY1tTo^vkoaxbK(9 z4jwpvS>XciADfT{pa*nl7jUF+hCcb4|Ncxo$SCZZv(9K_&5%_q(z z+@Vx8|!9iQ)3JZ^%vz~*E;kv~`Nt`O{UA#%N1p(h%zrctgp#Tk!tJ5L-+(9 zmaV|rr?~yrHOTc;WCim$TMLAp?23}q6V8ASfxqyWv?WaRhcbGuOk)C-euHzx_JuFY zCLX)U-fJexU7S!I=Z347;rP0R>oj2WWbdT|J=2eCJopi%lP7bl!`px3fjEIlpa7Tz zKym}W`gxiEdL{1{LU+fjXeF@uVgxQXhVHE`<9O%w%bT7GMdH@Uge@+ApM1NBzD@U{ z!LLk0`X!^RUwPT;-fN9Gj^ZVe(Y-%It;?DqXY;Tl6YuwK`}~tYoWVkf!D7DcavHF< zV9mwJerxg#XAv}?DU1#ZiVTwjg&q!W=XnmUQ@H8HoZQI*?h1Er|D7pUXJ-pw2)xZF z1SB^NV`m#6HT-`Lhq4&5c@#|l;@KE7HvaYrPP9eL&o)`=Y9(=sUOzw{%fbFzGoi$yU8eYX1pcy-tnYu+2s>z4!`> zjEI#XFCLO%tbHz(k-l4|k8Z(dA)*1x?#iKeK9ESBCJPyCY<)UH%6A=G^160rc?Ldm zWYPN5p)q2_X|7h50xAHmEjTs&$s>}l324;Fmpkm6jzX`ta94i#M~drbma(6n*&|(g zcvwDS3l~Ff3|^+}zcaaJ591sr`(;)i`_%HMKv+~$p=8uK6DzO$FLpcdE5j5AW>Fu5 zcM(lItY?R}Um^v;p;!D!TKE;R2l@Us+Qw&oIT#kPupkZiKH1aX-Z`fR`*P#P1_(99 zglNk|IScuf7IWaPgd!u)%v8zeLC2k%vG&zh>tTb%q0B77YD@o_pH zKZPA|=0=K0N@^h^ULrSENbx960>(4U$}bHM5h^ki=52F6M(6Upw+A69ze?RiakS zjtNl$WQks~bZj?BE}NP{m&Ac}^(Am#f+Jm*e{|&z#ri0{jD zSMu=du%OK)_gS&x5AFmnggnkkJC!Pv<-5bp@j-qO=GBc`ErsayO@k!XeLHfRCk9(8 z179d|Xg{L>sqlLj0lvUp+dm}a#{%^GtWc~ABGHNHm;^xJR5=Q0DP!nEj>k-;n=rU< zXxx$y+2@7#HNwA*f}L}1)BGY6+EDC{&EfXDQ)~IllI=`xtmRlYFz-nPLYodk(*Cat z-Exavk2=B84nM-O0~ApjglFeaODekk$v+tF^7RP1ggxmpjEj!Ol97>t-mF8IT&40c zQBYiF!WtTc)k{CY#Qq+LZdS#;mZ58F%aY4l41CRvmN-sqZLZGa5|Ke%?Gpl?PlPAM z`n`Jv!cM#?+s)-nQ#@0L8r&MaGxHLx@(a9Ts`fA;4VssgQkhU!Z09Vh_iJh)ghofF zK`80&Vv?0g??T6xK*wo`80PbWG!8&vE08HkSFO?ga7 z-Db|ZH1V1l{+N|4Mz<8@%Gbn5S`J?}E?Cd4&wq|j&bJ>Bo356jU8-{UFCqnG+*>l1yDdx>6ir+*1=<#L~b6UB3Rf!EphWeqftI| zX#II`;5@hC}-|}=J_n}8HYzaV|NVQdbW>b8QEM98ixm_{?w;*AU*><@wO0e z@%?cMm|(Z7iqnm?kBF)tWrjlqccW~E$GjuL|M*9dIOdc!<}k&QCd!l(p|qn9z8^OP z75^U+rSwq;7+{jCNbp z(qGsu&1z+oq`*E1hs>e=1o*(OAb!t1lWhKvyXfV#wcD=vS9ho=PQUS9%ZVoo_wcTr zc)cPLEzu}u))#qFzo+_!FXrd>(qEZ=ykU(Lxg#Yo59G(W;?Pz@%T{BHMnj9X5orLK zjyi0nNnCyf>DmCwj09iT+T+7rT-Vx1_pRl}?G~+iDcR-fG>eC>QQbG3#%t&zo`;FU zGeg7oR}Qgh)Ev=mDE;3O>OK;4li2L|qWu}N&20!pc_5H7kN&Ov8&hLwS9dyH$}xRv zhNACaT=czSit;)!6iKvX$fB{p767(}4K?x(Q6vF!&M z7s;D2d%}uABoK}wk2u$nH{haa>2h?xB|>&J+~)esK(X9eAT86t(V!{)I_ZLzKjB5LtnLS|?T_sl%BoDr{XWx6eR zKy1{~e`2^uqJH0rN4ym$yM_cc#1!ZCJ9{I`era7{20Y^8!SKSeGnqt;x@pTsQ>#W} z%T@=v+o4U|p()&OxMLk+>?-B>j z&p|FwNj#}#XgS8}x_hekrVh)^f`|{xrAc`vz7KC@n-voX7fS@ce3Mr-PXOkrDK#gL zd!@6FIZm$nE&Um-gQk9NjZTUKP(<)st-6JZuk313mW@rJX7EyAfqv16^iI;$Sl~B1 zI@ui3H#RXT$tH_4K5{%9k zj{+JaD{J#XXok>mPDNI#!Pot);C*dxF0RjaCZ0fE*)7IHQ2S+`im2e@gO2||Q1_dR zf)M98s&>N6+X%>t#qIGIH7vKR>X}B(4JyVpr3CkCvbn|qIpDV$VAzlmza(!FyRQzU zEhOvn3JHq_Gf)-$#rN)h!Ah$>&Ea#-VVnAsrX9mx%llLU@{S1)viQR0P|Mi0)-VIG zXI;cv7MuW1EOTRI%hNco5XecTqukz>=xnh=BuHc3R}0qoI!dan>8{GqC+i(xVTt7R zN5xD^U*O%jzd3o7dc~{arGCbllGiqrJO3XIWnKLLqoIsxSCLX9P7K1*9@~Wmyq*|Q z_puKQfS@`pR0BiBn@Yr{L(J5ZGf`wf2YVdxOe(9C=g-C{ecmL6D)u`nVN6O}y!O`b zKY=-Psp)Q|y|ue{E@J#)`?>D%@-@?s_>W%VAn}C7Q;i)GJP)0fq;KDnuG6#2@(!Z& zw8y2oe~78G6<4WKDZH|t)Z=xYcZ?CcQzLt6__9hNHwh>rrv?j`18V*{EIpwJeL$2A zYYvSArqdM4+GzPZIEuc$K9w_B$K!hE;%^AX5iv>GAS6P?1uKk7j-@K;si9&nthY!b z^|uxQn^dP1)yic8G}2YjZ5mC~-%{$iCOnh7ht%Gkq4~Y!HF^=I0JF81Td*e?@8=Hp zPvTCdKfi+BdWeRjM0-F}WoUBH&@t`oBwFQ0AbXCJE_r^Mj!GWY)$Uh_qjuYfXINcx zH0z+*>S?QX#lmYBkl?6fvK1VpY0bz1p@`-|6E`(xQn7&caiL1CU39z52rP(JNk0c~ z>XT*dv`Z?Re+QL#X2DRiZurd#zDry4}ym)wan;EjLscee8erk-O?K4tyyv7%7~l=7O_FKJQh9SxR6gm)NHGLP&(20r}}RYCUIrM;k2aQ_Mqy-tl#J@M9Lh1hpF1R8=NFf?rU8M+v&Uc|cj1*cW&m)c_LW4ZjiW=5543GkOth#%3xaG8{zy+GT) zL1`=`5|IXmgpmZ42>@Q;`$$EX|CDI3#ri#pGBH=Rzy;hiL8d;IVdr;U9_QrXtF(nREOVRjs9m{A2)KJz8ae{$lF;eAZ*>+cZXwoD}E0 z-qVuo@QTmNb9ycwWI`sa=p*1z_4F>K@I`!rk7E+u}h~su`OYLw8Tz)H{QQ?A`zygaw0A8+>jSR8=+KC&HBdFjWiEyG#&+ZcbwX0W-=Fa0;7#Ph2xfD0vueSD zBp3q&2vC4lEs2`Takzf{J4CX!W7^UC9?0|Q+|Hn4a(FC0eGpZaX6C0uWAnD``dzv< zWX?NV&)l>)0fKrxSux6XP5T(g&@8@ADeReUlQC1USvOkarS3r(lJgi}rC zy?DCcv~@YOIyCQH{n#>l^wqj)S04K_8s7-J=e#kR5Bs;Mo_THGHG}!O@hy5#sS5Yc zK$D~|-cRX6#X-f@*s?e86le8VoRvM*CQ=**KhS5aZsn{k`I7M$eG0Cr%Nje@4?P(> zsYN7qDvyf6u2*R~VHdR>t+iru!MkJ&>`vuc<6_!|$iz3fbVPC?*Vm%Y>xVCxn~oD*K zs^}S&r{T1MAw>#=px{D}mJ$Y3%><37vM2$ga|blD&gS}v$T<7?njF-IrwxH%>uEYR z0V7aJw;G!Lc9?^ZY9jgZaYcatf#Bc55kd4n`h;oMS5P<8>aD_5mz=lTmrabjiv0b` zv?8vseVV5Ie>#Q!vp=TE|KSU9XElNKhx*^`DI3i6?8Zw9<{7T}< zVJ3MZB`oZy`fwUF>*8xrGhchzwn9_6Nm@CD+xGpC`ufX>pZPp8HZpcxj)#IMg>b8Z ztLD+@$vwbWF%~nrGBbweif3m19D9UlV8LrDsN_dWg=Im}#lVJ-Hi zgJU<6>*a$H@Z&u*~&)4O8^Q3^0fg$>dA#89e5FJ`4#+{mI{iB zVH^hhEb+DJpF%-LWIFm41TYZAf`hofhVh=WEW8G^ng*b^>j~3k8Ux(hI&t@HWI<|& zN~6b%Z9yUqAf_VM`s;1pULQ`0J_e8RFrVc%vBj=*=3K6@GT@nuin2?I(o~4Z3sd3F zq%672gmfH9UN7mxD@A?U@*y7!L=+!)8w__YK*cExR-Z)((kI*OEYL+TX`I^OVBAo1DmdI5o!} z{q`sn9)kyRgFa|?sdY{h|BRYOSMC(Uu0P8BTZJ=B5CnyxdY4jaG>}EqVwI+5^dZ$Y zA^sF2B%l;y5E$^=xF`~k$K*YG&0xa>+%+QqCj&{7^LQIB*M;^J1vA&~Quw|*O zsCXy=UU_ORMXxbSqD*BGWs9{1W5fe@YCc#_0|o`|F;18|~zZ|$Qv%Bz=gd5Jkdt9cJCJLZ~mjUCP%ukD3 z6>`hC#C;6kdG~4hhau-Q%)fx1C|ia|RZb@Lrh{(J5l%bvKVh9JZT|=Ts{N3B-}EByQ6vqA~xC z{brn&4EFA)6KnLKXFx(;t9;+g-GebJIy|>=D9V6j?MLc0Fy)2=-F3=hR~On#^Dve5 z(7VKpK1lxJZQ;tNCQ*}>Clbjh}KP2K2O0b4lh5bVx>0ta1_NX?!jpNyWS^%g4=$L-wD!d{xco+J7 z2=7K$dQP89Q3o_9>NIEn=C|xa{#xRdj$LKDSt$=$Gl=VahIyZA&AF{VFQ|jpyKc_O z5G!a`=4xR9}cs!sy7oo{+!cdo;#>e8G%xsiRjvP*`*lyAB9VtQ=A+ zLm4jG@rcYW!EIrhcV#v{OBD)Pl1BFwIW zUSX6NCa4vI?_zrW}_>}_NH03H7E>rLJuV0 zlE&2Qux*HWwf9jwx7#I1k{u{IPL^1wpOyEHl3eF&{TNxm!R;NX8g}jf@*PSWiUKt@G0E$cWrE>X=2wJa&p*EPA7DUvvS8gR zI`|1g<>&^v32;5eu>>2UR+DruoyZKB%OP-`Sr&=5XH-u~kbSyK!O5uiE}E~s!$U@6U3_^CZO=d6NDwlk@@9hqb5Vmsc8YzbuO6ZDy`l` z*8J`w!#OoiN3smUoL3#;DZ2g2W!$3Wk5a+{;=agl-=7GkQV(J(x90##v_+*Mq;5$b zCjW%LcznaDU#odUS70RmR+6KJEl09{YN!WgDR0ls2rZXw_)Ye*zB-1qJu0ez32;PS z)v`)}>`>90atVG2Nn;OhdFJs^EG3{IPEc)aZ2@&3$FyG*Q2AHWBd8NGo@s|C>O$s7 zvh|+%<=@#`CChYv5gk3vy_DZ#<2G|#$M1D&n0<(`i}HIWoQfm1OVXud&)XeR>C_&E z^k>9-apYsrWVSDEc3cbGI=q+(-;lAHi7W#t-#zIdJkm#>DpHYx4kcieRp4&N`ENrz z+ucSH7{F@DeAnspP#couakH&j)Ng^~HTGKQ$;}V-wB-<=M~-XL>=Pv3W6`9}N=X^N zN(8#^J34>MW{hSXC67c8U*U`!lS6#OX)L~$~NOksk&ezFmv@C84qsonD9RY-J1MT@l~q^2tR7?alo z#H(&RJhlg!``@uHut$5bIObQE&IaVuGXkXX%H@N59C%I4;Sp7t-*3uTzIVTO61;y? zLYL^=>fl83=4Xj7}-;##pvy^*TStWb8p_c?U*1vEM!YrY!R(zJUYx-KN5#EO8_ z#H$qd@4)!CdLAU<1BB5y`!jX z5K^omfkSGqTJd&r)`A*$z9zo=YFCRSsA`X_=_aB!v>+KSJSaj98Ta35~6J_qqP5bs|Y+&G&NQJjqZ69q!Cm(G?W(?`e zdWW^zNCtPH6Qx>Loj05OogbqzWza=7*1v*)7Z^xIdD)sU)srdC*e#|wIaccR?+W1w zP!|3>3-Le?Aqh!RNcdYU{t`;r7Z01I!Jp7QurGGchoQ$e8wS#DW!p^JT44oNa{+#S z*vpsc7fmo{Y@ue)Fg((%(J+VQ{fEc6K7?-L^n%Bmc-dyUSr$i8|=i55gubx3S@)8ER6>69Ojxdt2c2n)yed_XpzR zv*-PAU>CjS+gNf}O2l11ZP_qq-eGFhYHZo@+NxpM9QdJe*}MUG$I!-$k! z_R$AFOPRJMkewpKSV0>KallV%5Ajm!Wk=$Oj2EMQBOLKuT1uYTAKz-NxFwB{jlYxa z`c3xXnFE<7s_82ZFrNfG5A_9|@h5qQ^__4*%ux73Xng4H5pqS2;QYCt?-j;M$SooN zg4owzx+!d@$-Ny?3lE8oI+5Ya-St@de?%XD;k$lrum&0UP^)JDaJ+x&bK(o^_1uAN z0x7ji_rVi?i4cMhC3${|b$@(8#G4^E=R1lIOm`<;vtrS(a#*5NPzDG1CATFR3g3B- zI@;mVU*87Jg~kHV4@;7wsENYt=%@{@^k^Q%XvSc9`JvU*hKn2Rynd1z4;o>=mqctK zFE~8n(9H8IAIgCNhVl5Oj5CR~PZXe%?rCJ1b)^u^a5{h}pc30osAt8}C1NX>j0B_g zchq@EW0_B}XB|FrP@md=@S+g~b%J`I2cHl$&kvrj?Rf4(a*Rx9!~VNR5HP37)R_wu zJ^K3FT!sP!4gs3?&_Q{IL*Ax%*cC+vrN(BKUO>rfrxer+&<-rdy_l-7r)l{B&;0wl z)h~aQ3N~*e4mHxWM+MfM+-1%BJ6FDfn(jP0Vb4P&FTOA3gmyS|u9;bNn5wjAv=2KP ziR|eXUl-V<>3sMqq^mC41+$!&z8<3-$++7F$Rqc6)K?dkxupe`%v(DS5qL>+;h-Eb z<{b)~xx)e%&Daz+9EvP_s*N(vFO^j)^K=vzxyDct*M)EkVi0J2 zXm4rBgcfv6)r8|HfeU})lv)7&GRtm3+{NWg#t?GVcGOA_3Nk=B?tB$PUiT{zm3xeI z=ESU`J;j5F;FV-4_wC$$kvL@v#ga3YlzU-w^Sx#7+Esn~c#*$Us~ru517!crz4d=gWzai?q1T=iOw}~!}~R@f6Z}A zq@&sHF#~m@z`aJUZ+cJgaJNYxsvPBrCTogojeWyt+P6dlsYi~XI;96-0|sCfX)m_XRt zb7!x7a~PCOJFS$?`h!;QiB8qLd@vj3p0WurNlNS1ZA+FNhxylgy0^YdQr9!IPGWmC zEfJ)F^t!2G(4mrynBuAaDh>>c0{*QQ3CH|fTRh`DQKNISd_K33rf1YLWK$)oO793b zozz$47n6Udyc(ML&2nqYfdgPl78k|2^?giYTtM~m*`^P7ZYtg*!$GcKn&dA##Czug z3Whm(`cT`~vH_dvb8OsL47Ye_&UOnenDOqic|?Wy;MRx{o_@7AGu!C)d_hTs4ui@$ zb@HKPp0g0T*xtxt4}@q)FX`dF)oo@mk1k6_@_Om1WU#0Gt>`Sa0~H!bxd_}jq2n9; zh(IYo^>v<8LZab@@jM}zFU-or`v4-;8ksR7s$NpeN_9sr~4 zg3pyVm|uWvOxyBRiz!;n?Ma>)o#xah5I@d_o452ILOPNf!j19C$gZ}4pDhp2p?h2r zZ_m32XK7|1b$CsXQKLDQfUV~-u)zl`FErZZLY>X44}3_H%%yTI1PjHbvGNM76s{S@I+$?m5Nd_SH0i9N5H=}-e1b=pF^wO*z^rlEP>kB z0&p$hPtQIpV$9b(E3Q_We{t63SwY6+V=eK;?2ijbpK|-U*Z1gn^+x%d21AD0zE9hE zcF;EsB{`u@>RI-O_dshHj1BF9BwmrUU8Eg*t{ot)<|RJd^qR*wRyf7DKKp^mxupD; zgxZV@r5s&J9@?-!EkBi{esaD}AyG7k!9)ncK5y;pT;bKP9GMG}<8l9;NyU2?8TrEj zm^pej9Z=EgMgAYc-ZH4_zHJv?bc2L+NvCu-64FQuNQ1P%qPwJ|L%O89yFt3UOB$qG zYX6tlbwAJj?)~ANVaAbR9B0OV&{vh;gojg5xnFL5H5i_?pw;UYqMdgX< zk7C#{!0;abLtn|yEPvUHJAr?-mCSczBdXsU8avaHK%)6z`p= z039aW?YGdEKm`eB8uOEkK0|I4f`I?rD0{H|(6FsVeGJ~aTA<{0UP@jFKkeu4d-eE} z_e1#jeEW5I9q#fmZizFa_uhhj!}57c#qE45ngh0XVMO8CptA0Oyh*ZBLNT6X5nHNa z>hb+JE^q3%J$Gi^*b^LpHnXK`_|eV9HW+FgE$vzee6aO0$fd0f8k4=Mv{x)PyA+j> zkr}RJ&Uclb|B141zIpNi0kNFM!Y`7PDoUqk3ZbqRmF254In%rRW49q^7&{l7FZ--l zZCT|<5D^S@OVo!kmC^p`|K+YhT#fg;CWJBRsvzEAum54L<6-9o*&jE*c+bLuOswM` z9z0szE`y@~Gi?C$d4D8yYmtT|!JxZ9 zNg)du=x~%*Nu3&7iKn3EbXdUqqm^RGeh>gXHpZGc3XxJSdXmDT zsJ(uj|Chcv!vVGua>>gj^+%b^WE}n;ys$oFKknwJef0Xfk%!VRJ4qSb5mep^|=#b3{U|U;_cWjZzfC1 z@gF>JTIMJqZP-hK=O#AH_6Tw{?os{jCcB(vTMHh?YERPRmsu6rr`n+ve1hTsOHN)W z(QSe?7~+1gIA|?V9>asE#??8!Qn4{7$|5qpD`(-w6dV@;BWWQcd{-s_8F-aWLMODg zJM9qbHJIUD>e&DZ_QgVdS>)lx0@?E!PesxW~ZS8rEuzVJ*?jj7tJfji5w4$j>_;3lQ;UQ zTK^r#Pd}xJ)@@@bswUvI>sfaa)XE9?FO{?NI-PT6Xsbjme+5w8MZb?45`hv?I*_m( zRy^$5D6MgV=xgf~O*C(z3+%&NgooxByEm^tlZV1!E~4zS#hw3-1L1 za>0Yw^Y8q~OvifdO7S$9ZO%~`tqgzT7B(bVtCD_0E~Kf9qd&A<($$=H*7UPSfFe6w zMF(OV8#@v+8A}FXzugmG_Wrm((r3Zt6%puJT>Kbw%mEoqc3P0*=EFxK+0vTmnzE#` zo3!wJ`i>eHipcP}XubN+_X?>xGfao*k%fOTv9|9JOe`rQk%)Q_RVAeQs2M=H?e~NA z_Yv;gr)P`{H(!Tiw?$WCz~k8R$JK-*b-sy$D-7?l=I%8TKC0gl5z*i96t-&L77^)j zRFQNLM?AI~o_61Le%Gb5Ta}d9LIhG;J_kN(HEz2DTM%OAy5!RH=C_JGqSS)`ZS5dh zNL%Tld=(s_lPRdJ{o&5Ix6u4)Pq+NY{LS~@!ug_dTx6;TxkwzBU@0mnEE4H41;~9u1pNtEHDEtWwWzB?q|&>1TG>xvk4FRW8B4 zHb$V~--Kdar|U1BU+(d{6{u#gzAlWgpghB1pNmdNj@M(R=F@e=!hyn(8C!wB68LiGoA`7p~Z83#SpI! z&DZ8>nXi|<#CxC1RDC9S|BR2BHKwstYK;gc2iiihYSbPVBq5n2p{!z0DxZCKV=^o3 ziH%LM z-+M-0yQiBG&M$JiIeWX;BU$j8-6>h6{!FIw=}xsdSi2fQ$+^F+Qdsi%>U8M`W#zZt z_Of3>CCs>$UV(f zo$7z7j|fMXIfobp*2ThTryG$&J#T{4JArKsLgi#F8srk3&UL1Ld^2g7uY(~ovgn7 z!*odPEwZ4NOWg^wOx12iZ}ftFXF$&NeIh%NJ%(VFh~9*6GRU0$6LLnEDJ&Oi^Cx`xfm z+E21Cg#?qBa7*n6;yTH6OzhdIa$PAH)qm|c`qck{E+>48Y9-X9hefirGTTjfK)#3g z{+o_1P!obD_q3x_Qba$X#1aT^X!PxdIZoior`$jGFQa7@P}CvOLVa}PP2W+(C@4BU zU5Ac`n@@xHzF%$d1oCa9B2+O4+HfF0nE=hGjj zHXy_3^4=33J`Bxc!^96p0W-X+m-k6>uXWn%y@78~{)FF)ZX}#m(`7~>x$k%QIit%n zF4rM*Yv6s$ZppnP01*!dE1JNs5C6MfV%SqiH!l4*qgE}74#;eK7TqOyTZP>mTA*s1 zamSnF+k(cL8Z$s_dz{y=&b;Y3<6G?(M} zBS6)JTuhn#m0J!1JH#DPd-EDxcN}|Q)G!OATL-2QAIkL&>6ameQHe#RJYx>8t3+ZY#yVM;G$tYanw$vsiJ04$dK5Is|jA)x)O-dU^KW+iz zI6gXaQbm6WLZS4p{P(5{f;O~s!s)9Z@B8BhhUT3fEI@etq>bG*U8C^6hplJ*)e*QVHBHlQ2WUP$ubrh{sj=aW`=Mpb(Hq@#zvBHp=F$#tUtN4S_-P zX)R~XriJR{+b7EvVn-fdd=`k}>L1-c0J=lJ1-eUGSunJS)JC6jfdlJzK3aCw(>4gK z_7Mi4<}kMoJJBklGP={`4*k0MTs!$_N4s7rUUnCkAV?x?47OGJf34DyuY_|h2^sF} z%qlP5HTN6D!Bxkm+K41#-FgfC&H#Y9T2^ysqi?J55g?G&rn{`ALusLHBS{o#3^A$3OLeO@{^Q4^Xka6Wsat> z+YI?qlZ2BSmlp&eeR<~$Y=53dUj*-28us&lcG1( zvMw-oC9rRV3S}WY3ZxlP&0f^X5Rlm1SkSm-#1l;28vSte2ww$t3Tx&u4~XP`-GZgC z8OOeEUHGXa&=})Qy*L3Mxh1y-ccaRYz*jEMe z=Nh>{uTr?u!0-(qK{@M*64`0@we#O10qK%zR>lX@DaFR*#E4RA#tC8DD9!b<0A0O>(e z2c1zCvOBiiqlwwsn8dL=lau7m^H(if@7o9>}p zz93n{z-xf_2D-HBpKfH987ghlJQ+=ltfdt!j!HJ$*Z9)f)TbKkV=2}K!0nKOiT)C1rMAQz~2 z+Wtc1+0yG?(`8xLZQEj%_Owj%rEM|LlpbCyCt%_WMMceGY$W~b*NnMiBR67ed6U~G zIWEIG1OdIZ_PZ>*s!V31r=G&6Q)6b|)-+6Q$JekyzlQN|oV8-z0|_F~Dsqlh9;6dP z0Qv=5$%@BUnO{zclcGpLR8B+;wih}&?ExxOXXI-^aye?Vs`;P0d%f6|@~GlHhFM#X zo}xr_{gH(oYc;P3%f=Y99-jUOsjKSUr&L zIQ>1}YItIRL?vh|2e+OmVnr!aSO|ycP_zp30`S!FN*Z)#ZTP)`IEJ8LU^I@nEOU4L zlwgc2b=Ev?z;WMuJ|#alx?KQdKVbg|b?2F#+Drqh|JEN{U$OX`@{9mDaAbPGQXN$v*EPsRgaE)G!PJvM5|3ztc}`b`v5DP? zN5CxWaxnMU^aYmS@YSvbl45=wqt=(wBIQ&w1gHiNQSen~E(&=i{^lzPfq(3*Rrc!> zi7TKDcmfbOJ~JC%Ule@1mRBQfwSy+8zX3uxC&2A(KlB1;k*41!?OjqS=}z9@q2RGG zlyl3chaIn)vS;UIPrvmSr?H1;_nJitRO`wV#j$_V`YXJSp1H)0 z7s>TQI_Pub85r5li((qy6sNUDgi2RW7kC=W%q7z}lPKa~nR?9frP@V){zQ@{b?Gkr z?jtbxM1FD~AmP{8H`<6F3e9j&a`}sOz{gAR+cvcP=_m7XH7B5=c{zGXdfQL*PG00^ zSnkmF)j~rl<_$zkXbN&d7}LoMW?V)nmX{F`iOPxd=JP6tTNQxl%ubUKcn_2?c_*Te zT57MfKsO~`Xc66hK4B~mH9F52@u?`N3$^;b3;TY|wH?g$7{}-!YDE(ak*2?p%U=VM z?K!*`4WJgdqyN7|1+IOekJ~OVA@)0I0t3Y1y9+XqzZgcsv9?z(oql~SLxj6@We7H{ z?D=`(>yCY%bW%}PY}Np}#j}|ZC8Dbgfa;?}h=gugMF+lycT`4=!@C^0QJjf@O9wcq zWKx#xJK+fB3$}7r5GnUJ4~S7ZFI_Z98N>1o?m|jmy}nDbcEM9UE{ayGm)Te{XjwLF zSap_ewO*aSYas!aF6WkAl*d))d7F;)Z5iqAGxsUJvf~fj@1bwxtEL#gIK5FIy4cFa zmrz&f;ve-8Hb&hAo;a(a-<|FYUFW@7u9dl*efH#*{QMzBSp~7b@`b{-D_rXo~+t|s!89;RCsuDhkA7Epq=oCT}5r-sf3q^kxx98g++=eEYRpG1N$ReBS0|oUeODbgi6}pC1@-J?R_1d|R z6&{(GthBDTANx)e4%JZ_XT&T?kw$3LWQyd|M!7Wp=5^N=8A{eN8u?#~8*FB&?6)ak z3txFEk|{kR1c(8rPdt08^G;hMU(#>KVor%4fz8QjyJ7kG)H(fAoNrc2Ifgky)m;84 zGZB^SrV!G^0_;g~X-FjoMRt$xY$~oG8~pSE?B!xE_%};_gt3ossu6clbHyzY(bUzY zuX?vabjHoqO(swgEs0NfxbLUn=gjAxFM;ieTf;1^b)9c_rN%E4u%W|%ruKSK+3poX z-<`ztlDi2mh4|vzMDHT8#qt4C1`B^H#TFu8c_Iz2mVMW&?$RF!4XwXl{YSV! zE~?in5IZ+EG+L66QD&=1g4phsiUC?^-twbr(|y8E{j9%C)u!V?{{Vo(4@T?8P*^H4 zfPlAHCUyYXsmXLG@X^+yHdG4f@o^KT;i)46zv8Q)h=NM=R$Nwmd*C>-8znJfUxRQ3 z!edL>nC$HE^tS2_m_2Bpmf3*kJq!ri>!jWLQ^owzv8Qk*jRc&OWKCmKKXhxoF;p`M zBb?8kqsH4_&c%m0cLc-4Gl6i7}!UZO4e;W1XYeJc!0c#Z(%>qb(W!y^GieaX( znG)Vo?YIg?-G|t(C^sz9&cvM9>OAt5`#%ekn)=t`;#gn*K@(_Wc``NNsp=GY)}mtc z%<(-*=L29(LShAoFpvQN?UgOF`+RF&a~kt(@M^EaZCNdUpy((+>YF@GyH7>4)TyaM zB>x~c3FRp%r`H>+#@=!8WlC}@_dQ>QT%b4hJ&`aGP4Un!PKp9;5}>Eac;~~GZXldq zYY{Q+b4X?kR(=@Iv2o%rd&0B-{JSHF)~YanhCo|C&UTFPw@8);vGSx80Njm-n$rc! zQkglmF%6y&sP(EL)(iQDPi(pxH& zFVzZ1G{4zjrJ(Xn+{RPtF@`Fy=mOw+l(jY?reD-Q9wm-Uoj^YE2povF^7xdJ@+X!B z-^M=kB~QO5a&T-80a2<4~C)<_)@IPIVxhix!FUL*@CIq&M z`Jo5ly#@9%c5Ok-Uk_CD=1>^nm=&M8hvt&9;GK0+=KR<`u>viIDP;>mHySjt%_EH! zeiom#c3io1d=W@s*McmcKW#Sehg(sM9Cv^d$K9FLlEE2CDa{e#kESaM5DLV28U0nD zF}Bl4f+DMLtL2NxkZpSu@Q@g5&M%x0B6zP(&Y>NTozJi0-PXS?cFZ?>AVJAu%7yaR zrTvOW9wPPG1J%og4khY6&d>0`d^GE{mC)kwgZ7zB1z?sD?AFL!CbU0T0?PXJ2D~uh z@2hE8NW;HHK+>ydzRJFp&Fs$F(>FA4lgbt{{IWb2edPTtRB3lqDZ1;8h-eSv3IU^( z=ay?>5Bp{nC$r2t7xzbp0b?EcbK9|{`ZCVb^GGb+<4Ne`>|?aB5MNud(X_C}UPBJj zx-JchA@`Y85XjjOS>+O=>Svm)Y_6NEzZHDsEl>m4amA#{SV;I!sHLJ4Ol4^&_TMsGuOb|Q-~^H&CD<7IP|Hi_DaBlnR% z!s99eO`HsP6&?%Um^E3^(D%eV;N;5#mMw09QYv%Nr;RSe3WBSC=*Egos0I|R039oV zV9zbbb*Eb4ap#uf3>0U9z0URSdco9H^f|r@9RzQrvp?8;QYLip4$GP;tSa;}1D71L zlQNr(Z@EJ)z`^y&KiVGn_boZfXp+W@cTa2EfL9SyHMBistK}8&^8ubr*00F6-E%mra}+0*fixvV?_e1R%Em zv=Vbe_;liOx{3~pjTQhg0%qPid3RJcG$)dm9mkrVEC_@tsspa-5IZmN#46^sXWqdQ zJEy~Kkz6b9M^2zrn=*yHhHRRqP_i?#wEp}O^~L^6RKd@K3Od5fLSQnp+++b|I}cHJ zAzJ_YwvS!)EYFHg-VGu_ip|%5z%CRG&5gqR07u8>ElbP(qpHY>!sD-x*9Coi)C;}f zF#9J&cVlH-7bou3q8`|7`QcPz0@wtsMy3A)@ zbh+Bgn=0b(n9SLt=F_6N$8L&^K&1rFJsTqn`aku*zQu8Yd9Sk5hYk@`;6L`Gm9tG5 zE6Adl1l9AjUbp$ezP6ZH;^MYQKoQ@E=={Ow0$Js@lIY ztvFG(0@$U%z|*Aof^9u@h`c6^`nBkbpVs@bYK8Cv1fw+SizDkeH6I>wr@qhQWHM8n zs4In@=>5=V$WBa16_8=)1e)7;el~o%zi}pEQ}r)T7A#@lg)0}7)sRUhd{$XVe3|XJ z^aitvqhHjo84mj^ljh0Eq+D$|+S@5iP}zaOmz`Q)9HC&+u2}wPfd=`M;xA((;m-8o zgQRQ(a3Z>FYi_LfHuqe}9t6Kk-v7P%k@t$5NxuRB_V0gK@W1Mwu zYQ}(j>u;}fc$qMl$|#PXZ}gMm@mX?f+m1!FC*Oi6Y(b!{cLLE*^E8=5&y;TnR{R~i zLKHbMAiB)=2}G%MdVB@iOFanlSLr;!hI5@QN;F~@!=0`wDp;`{JZR73D`?X_y;_3f z692;^0sSpYDkaFFzSp_s8Z~4lc3_Fdmf7w)ulgead=t3H^{N-|->{rwWX7E$YKn`+sGQf5qkLM*$rHS_;G6Q4><#uOBrt0m@o#L^Q@(J*& ztUYhSTb=ngB#HG5D#D)m~ao`4aR42 zWFVsi0LIO0q)-SIa>oYEGXst;#LG%GmJtHFP(ez7yigSTp2Kad=3{qpw&z=x(j@5V ze8r&xi7On>@mw$V4J%eFwl4I_z{|-%ywS7602pUhP-{B8fx)sbRKu{ZLLLxT76)&2 z{0+&>l_|u=Lnx8iBr;^RZ5+NRpfmc>-+{A-bf?9Mhm!L{QROv3+qKGF7!hav=nUT( z<22vJ=Y+1>;^x-HTKmKCVr2Eo(TF$YbckO<2GeVJyX+dztMNmu zW8oXJ(RYNbpA?t;tDokn5|?~P{T6=qFQJi-&{e-IZ9n}M@tYfFsC&^mip>UiQV5yF zUw}n#abDkx`d3H6%|hN7^eN%4AH|0X>@S#clU#Gw6o!uuTlSJR>_k%Y4Dh2YG%P!X zXwHgI%^vEaB~6<2dunne=;ec*_rFTxD38H7zj0@iakII8R4b&d$!BGo*EI&cMt*-U zREAS;pro)|>Gqueiin266;0g|;*Bhpnfs^_aa~5%pF`+|%?C0Msz7OAy>~f?sQ3~bRIH*|i?duK2)Iy;RD?97K7mImt^ zgx)*v_mI~8AKz&=ii-n-sV|3{{hK0d1@PyM$6FnyUDDkp+B)k;;{w4cZ>Z}+b=*k% zbVVI|!H$FF40_*P77pPDLP_qtp=Z->i5j$N?D*t}e+-iP1VDI-Fv9xFnehp)CGAGa zE3+E7?VY~)JkBNd8yP@z+w*UsHgOzE zv=Z26c48R|K$1T?3B*_&>Ptg9gUSTWt~=Pf&bGh zdhfr}7}?h!Zbj?_KzEfec3Yd@UzsO#B+{`a z_371so34FX^F|B?JE|z>R-o_PWda@sfBKkd!yo2X2X_}l`C=?4(jwPnefwEi#&8avQdfk#>RCEZ;4OEvj;Iv0SP`u@rjK0LLxTMZ0sjM?#<1D-3n(b(NHaHt-7__wO%s@H(1r<-yd=YR|KAd<0YT)}l{zb8@Al z+;InY)6BEgTS*u`{L1c93WQsH-JBftaeC0!2ck99&)4J{7%mTQ8(#!VHVA{h&$iy_ zQLp>Jua;Qrr24xY2s7=11(_1Fp;xsmvFdexkyK%%E4*HEV|bXFot#o@Z8G~)kN5xI z$n|2!mzJKNpB{p_KNN39B$7?Ao$HmakzU7Vj$wU;`Pz`trEQPS=^5 zZ==)qoPR)>hzPE65LsV~)v1UlTyJ~xgNG>$%`_9ybtCwRr>AzgiH$1)w@V_x~_VAhlF_e$we#Dq#@U}kzV<2#P6auG5 zV>9VS|86!kAAcMC1`r_@%f|zRJ}8egy|D@nP8@0a-PR98R|-DgBp6CfqLkDY$s|`H zR+q6ClYHUv%?O+66SqDiSGWCLiMnhQ^rNoO0BRW?!_nkE^p`VE;Rh(z!sXg#n`2gU z5VJxW5Ad4}@T1*szif4FCz-avs5V_6EZHU={ne%yh+Y#Gc$uOyz{tyQl~ynWgjzgn z^m(PPKhr2QgZDKD+0D8cos++v3~aZ_wu_PZ>=AzLsb;Lc(t8aFacj`?SKDbDf~~!H z3U=3sA=qEN)H)cq;Q?t!_tzBk#7=DG0xh1HTi;vpI??ooOrT`Hi#c_mzxoAaU%vla zy!+S<=ieGfM3`UBWgqiz#)D?dMPSnHh@p=}GCJ{hXG^)l~ zvn`+2q)=VoStfXW)-5Q2Kzc;q=tXNW$R4ynra7^nu&E|IO^}C&8L1qm7uD5jFL73HllBHvlw8_$@y`9!GZ;0P7%em(>|j|J&BuENw`; z;9S^`7|ne}6o6No_!83t#M=NvkE`tye`lKUUY3y!lp|O%CEyL}0-8Z5N=Z2>4vBBS zhv97fD5IBQLTy6Ul6`%J{4mzS+0%ct0DuvSkTn4}cb7RY41WMPo+`y(($ zzWBQ8o!^jwKG~ryGVwo*j~3hWfOgWL<4+%~=iS^&UW-X%c$HIV=KYSI94!jka{|Q1 z`rp?|fGwbwHC1uyXXh)9CjktRv{YqA@O6} z*<~=s%FPQm)}Idpm(z_)_V?U>E?SKqHPTaFN z0m%Lj)C6GhVqVLt03TgXiAEY&qyOh%AfT;ghZQ+&9-%nN8BTxl;u(<$P2|5|8`N!sS{G)#0%)jYVDHAzi)d_($wYmb}IOltjt}n~s75@?CO;f}I=x@;nDJAlg%0MUV7$@s~1Rt?z17?Lt(7 z+%3aByw~F@XeC`c>o|;RNj4XaYwycs?KM_B+rD02V@~`^3C{SmA2-7T zORf!@@Af&oqy+2A^EUvt1OtK-ak0D`vI!y5O7^Yo9~&uI#Q^Ci4lJ|8UC%JNlGaI2 z2!b7}ppZ~mL7OZ3#Wy+Ws-rIzT#e05Ge8Nff>I6=GwIdUCkY~F>o3c7a#k0He8tum zNu@)OT3OrP{0z-^&^1Pa%YDqyXtnd_FODd7u+?oJF^2NKJJ?IZ7^?4xB30Z;1WA8M zxPNCrw$|3EZD!QU4&a-5WOpu|kE@mP88n#;nQ?3qB9`aiJ4mvCXeJQ+I_q4ssPLJC zqBQE2wgKH$y@Fqto|e?R=Q}@`iUTd*rH;g(krEvWkLl3pfZhbilO!16*914NJ4^m8 zkH6naE_6}BfZ9tdX>y0w%RJrs`%S)X3}Dg!*Hj+&r&>k!Y*=siW;$u&Bb1rYXD7sv!+E1O> z>(`+sSj-g-flr90d5_&9I>%#BZNi3bGwrCS*GiTi8vW<=eZHNl+YTzP`9vO8S|f3| z#7Wc@v=WIXYI{}~3D7Y200R*gXcquVZs08Hq6`02?c*8X+UJ_#G@pMr4I#viLsLtBW|THezQQB)~I zPRHXoqL(dV7De(ZK@sJ2@msb7)@f;0IJ`Tt{2>vsW;CnsE-sKitUf&bT`GT`v0+&! z`!ikU>=z!X5F&9pa2>oGCg=hdk&R@(y15iArDI4+nO2hn=l@45S6UAB7^nNuMZaCp z^WkJwJru@E5LcA;k{ZZlIwI^!r8$8r$wP5e=1+k79JK==P=I)V#estIFb)DVKmtpF zO|z*t&YkWYqJsPD^!YCx0Ux9%{g;kFZa2#T5Ic!Rf`D7{p{C%-M^Jku zI~2)BE%F~tBNN>PUwALn(LMb0qRc;z!f3n05|H`z8Uya+ncglgDNmBq$(u`){WRpb zA5Gn;0Yk};pPp>q%biiz;W6TU@GzdSSb)X6(ubYRc%O;RTId0Ljt)Q9Qs_mh!O`Ja zg&d(bN8Ta6M03|gzIyK$L*j$dD|0H6@@u$aV-;I|;SY92rq^cy`O{pNsV9zI#xH2a zgyTXocmYd>i?Y)q*+gHp{^<2q5FZE*juo+T{Ge;oH2qTeY1jTleUc=(RTBs{C~JYD zDsnIm?sd6k=q2f`x4AY)my5z(IMOr4(*B1>0jI2jTsZ=8FB}7`0FNvBiFEag?-og2 zcI!Pcb0ZBE|6N;{evqoR)S|Z}bBm_ryWl)DDI0I!Yx@3X&6v?2ZJG)2nq>QWuK@w3 zc5HF0X0opaUjgVwk89HzbhfkLj4p89d}Du{0Ht~5$^_~qAEQ1rYJa=_RI*dgat_CI|eiB@(a$fG8>}I-LuOfskbi6_6tj^G;i3Bc=xzoC3 zFneNd)1)3PXwi&lfuj2d394F3w?YPbLz8Bx%T~a$4=HobZ{>#yN=yU=miOEo<_{Pl zdkLNa!rpeH!%Jvc*hQ5up!l{90a?3I(FYa^eA^evm1Ga~iA!JmIa) zWza^E>b}D`=~*oalw@j>g#M+AR8aJ+*W0$7DNjSvpy054Wt$z2wCY-I$d1s5`PJWH z=T;(Lzm{)OEz-&-+c$oFIt))yS-S4S8Mg^qHX1g}2hTyJEiK<`%Q`?`@BSK7`RBZR zhUwSgXvV0V(erKd;C=pXKXS5W&TcEeSR|%Ah>UIZGh5EK=TJNv2$E;)MIP8>=m5u) zxw-Ee^!x3^ZG= zMn;-$%&io693Z2FKvx|D9wAuzTE0&mAp09TXbSBVgeX>a?XuUR?TShB8nN>_Yp&_i z!4hqn37c^S>+AQr{&w>+zdB)V=O#Ke{7&S znH|U@zYc;;{@_?4o2tzao~(M$w~&?0_ZrRiDUD)|Tzqc?@#0m}bdN;}+35bj40hh5 zUf#4gU#*S!(Vn@*H2V*vQA>id4kPSaV@c_wnAaopyzbP@t5o-H>?Vq6O`3zXLwwXD zuie9jC~I96p^m}v4thnW>c?rng1Kalj(VrCV6S+yfqzI1n?Hm4kT2mQKem_y&or3f z7~74_a5;>=ykKHHZfmt9i2_EuH_cx@H9kAZC!O<$Dova)oi7UI`!$QCq$DKKALJWc zp18x~s)LU-L~0V3MD%ev=%ER%3iu$gq%SykUeS(d!6G{?&Yp%)d-Zv^MP2Q&vX%em^l>F>ac+I@e4gcY7rT|5ro&0IOOc^WJvCq&ZnjXyIfF zFSz#S@9uXEz)b}hum;EJOLEbMCdTN~`V0pz2f$&Ud}`iF0dEI^?IPY2nUV-L2GM7x zsdjfuD1CMd44PKsDTv&vgbn*GBE6q?R!a}H?r&{YD?U^!gz>>Hh`+{><}30$e|e^6 z%{&F>*HWC{oHJh8^x`g7h}`dpWxM5fP17^DxbrGF1+3#vKm*IK;L`7jdZe<oRc#y?B$HId$jo;2CpeRVDP;?|*W0AV=usGg-)dwj zyhr{m@}kF;9Ef1@L0JFxga@j7T6g2<#+l`P`h=Sv-z(oeu0?2xhDLToU&sEE0K^bc z&?iS&`(EmSnS%NTG<VD9tc#Tw&hDJq~+xOP(gbPY|GghEd0>{MR*R4X3T@5V&!@S5afmUr+b)z z0NmvjH#zbj&U|n!_vjl{d_zMAHQo|aD*4i+IUqpD@=r)k9>@M$V`R$Ir~$R=u(Bu-c7PNG(XIQTvh0#b#)snZ-n-u-#N@z_P^L{M!CNJJO}!)lXDm zeR(vMkxR8K{7^Py$Ok`fZZQjHI5f`)CdT=Mo*YZUzP({6tEu!HugzA#&%O$>Qi0SQ z!fe{bKZ1ILE-qB&5(S_R4$?AJHDs309cu}B73?A`6&E>!b6?|B{#3VB6tcBLt9izq zXttHvOJFiuNW&uH5W5(CS4v+>Dz6jsYa4TEnh}_`Td)AhmY4`U^T6`8Kmlog?mI1g z(=XFth5NK2ru0zqrqlzzgqu)h3kStP6eXqXOxh?c-BoIaU9}+Y%?~3Tkp;6&mZR>sTj^f;zRJCE;n2S>afVTbg^pH1afUH~fx;0zL5OC4uxo)LS2T2iF=t=v zcoDl$f_ehpr{z}$4_9)JV_6U@AlV7#9eDULJDIf^KyE79-vTkzZYiFlM!@?U=XR!R zCPS4#{Bg;sSq(NEUg;Z>w%7uW1(ad@Z9p}WvrYY7Un+a#9mJA~PTf#$W$)|arC;zN z3)pxyy{R+<@E<*nvpvOd0|hmQE|ji{-wkJURFgxodSWiFFw!9}}!8OO6gFWFwxQ9&r!NeS3Zxs25;@ zX`+Kvd_eL&+wo(R4oqUkkw?d_>+Q^K*ElHqY zMcxo)rwC;?er5FMVRo9&TRwJ_yY3$}BjJxGo#^&jyCt!cf@!eTbma5@L}sDxJX+z` z-jEIL##5rI;xBQ=eG${s;#D+R$_S?8eV6``KTiDX8!;O6Hcfj#k~Fl3GpS)6^Yn+q zL^0-4b|N7|^%uiQ%)u@oapO93@V5$y8JW8IC}9^NN)pagB_OVFS8l8k1+CqDAISHb z)6nc0P67M~>~D%l;IBzzDuZ6<)L>*$bvIj1v)g+!p^*XLeUw+GKQ*Z_vBr0_e0}bo z9&|w<&b;T_{brM3Spc?)YF$?!Ewl^Sj*)*u}WMP@1thm%Pze#4+D@V>Gd80b_o zG})FG`#w;i7*r+mhdyTI{Y|L3~d;FdDA}A ztkMgm)E=IwBsEKJx!~sOEPg7c9(bVV7ksNpBs;H{u(NJYC(>-Z=cl|Ni+W_E7wbcdWcF}z6m*XlwD)iTKSAXifHBX@J8H+?>CAOMBRwc zr!mNYsk3>G3fuM|4cN#Rn#RLThupZCo`l&7RF>X(I2Eup15eM9L_)O;T~EH7W*E#fbp z%P~0YX_~46rNP|rUc(!Skx_&Jf#+2s*>M*dbBW-s7P_rD->(IRu{R=$oqWt9ynD5) z&73zVC2eR5QXPW7VSihCmcnU~*!=W|(t&CrPl$P2Ke|7s3AX@+YotDkUtDGHTbf-u+f;1x_-93VcG9oyH zh?I1<(g@NGN=h^5Kk)d(-uvC>UGG`vn`^oJ;@tOj{qnl;o0ij1ca>*Ed0W01NG+a! zuNar|fSyC3mWw;hQ+2IK3Yn&W0Ot93s4kjhwbXITCQh5BBOe z$?+zee5HMsZLck%AAlgZM4sb9_d>{hFu;KsK@#*+fTRk&B^Y@jtQ>c6Z^}YTP~g`a zMpS@HVv2i7gD}b9NA^_5&RntZYlTv27QwMnv0G47me_zXA?kwo8^{G@M$;5sU)+{C z1G2tN=$7;iHY7I6IIQ_%#iW_4fxeiXJDl3#7P(K)iLSp~243N3DrURe1-KsQllhKM z4ig9HOWW8#Nqgr_==V1GKS%hk*sx_6k@Cf!yocT|#ewK8g)5+}Z{)#gpYTn6KIhrj zX20*~br^JV-Hk4PeXb{>VDaq{%h)bg0*nco;zrTZNrLxM=BSU@+h0fUM^jT!s^B($ z-^!<<`UJRW>_^^%M@jKocC0HZN+eg0j!{$evtw^!5thsE*sEwIcM4$7bLZVAlCUO$NX(Um=yD;`Of1Je~i^L}1 ziQwC(oB3w7VzI9}xo|Z0 z)zVKLP1ukq@=TbS@SVq9m(pMjba=y4VUrX3IDIdBa;;Pzf~Tk?Mqn*~WJq1?iooC^ z^PXrlm9NMW2~teojCyppxB=U+E={b8mzfBckx)DlU!}N{*{iH$VXM(vnVd8uueK!H zPMe?^ObNwKNA7)PS}Qs;dI=gPer4>Ls}Jx4UWD-ZX*%^@Lb6Y4&lo#h&XDyh6i_u?{fZwW zF3XYI$4Pt(TQf;IZ&h8P9jZD99SgmHRCnA$V2ht0M%=#7(HJiMmBQ?&3_F^q4Bc}O z{^P;^w&sc4XDWXW%(U>IsivCTMIR3lvN3r0owM_KK|O5fl{B8W$BhabZx67Lgse*u zvVIW{ZDM0CK;f3jADLI!Lz)xPzkO4yB=tSPD}O5Yc}#!(9<18R2%dRB(auFe@|r%T zR`n@PW*wPN+ViTqTFxIVa zdte%9mrYi&s?53?uk0ast7_5C->GhbKsy#@tL;84RFYozi;s73`Q>@Obw?F{Mphnu zi5Ejs`SCXhw56_KA%lgL_e7PuW^U-ii>jAnVd_Hn_8p1w6TVt%(33x2eSgwf3g!=?(i&eCR&4gtdp$V^ zLJCo~2pime5i!cs)F8U>WYFP#w_NiKrIu)EhUW+g#P-d@G?(!qjrfO2{?`$f@KU!p z^u^f2&I}mQs4eNAVa$ zMt_80HZMh6E|~FD+l9d--TVkJ5d?FZ$Ov;r7Zv1q&nIzMqo9&cS!NS3+ZUeo=x$Za zWfk#^66`_m=H|)OY;02wsULl~xRaFm7sLjHLax6D(x>ujB1h7PQ10L zU=`roe|@V?MLF6)xQ|28RNfBJ2tmGmJTLySWfNh|hzrr|Pdld#sdc&(KRTyQBLW?K z$%7!UMLD&rvD=6xVkX1r@q~L>aMM@BL&CM)r9ywLLUxjZi!n5Z2$G@OW}x4seZ(;9Xc{E6>(*`ThWW4&D4c6WFd4`1R9oK436jQj1tFVo^4JSnw z@TYn|YPB}1?c@$R%st1v_|Eqdw-Yx~++X{tTz^}la{Vbmt#J>y0eiB4u(L8DXEufB z2|oCNbWe-0w;ktz32@6SOQqO~_6Z-?FRb9*-Dn z(PO{Irq)#kDr+ma)%L%u|Lp^-()or_?|53xnB^3^i(3TF1b!wD@1loHtlk{JIu$rKqnA zio1)br*JI#Y;Y9=4fYUpd0e+_l6nzJ50SK-hL1YAD%quxIl_uXFDh{Bwq|9|b_*4W z&J@L=&Kq=ayL@y*#ix2lg5N*qHNx|90WG&w=Vaq~1zQ;b2$d?L>uZEBaPvn_6halE z*O+5B+6rom9vP`l<6CaH)!yV+A0C={5c@8}IFVvvG4=<7G{_#0 z%i5AO{5NoGzu&+5HH`!E_!yO6Gs$f3;G)sN7as}+hsaiYdn>7eeFo9dC3$&Y3h-A~ z7b((yX90xc4P?&iu$FIa&Eku?+N!#nU!MsfPe+XC3{1ZhQ-VV}s9l?pkuTqYl;E3Hnz{ z1f()XjQfN<_$KQO=K{+E8{T{=uW7yP$(YorI#ZE=qn!pLFU;F0qMGb^`E6JO_UyYT zPSb^_+H&}oG^*PA1T0mXSN!@FUoOdXiI0GvwmPkKPyR)lOsG{fSUk<_In9J-DsO!? zoo?oiqa!Z}bwjEAXV}<<^L*DQO0(b%MdyEDcW9L$;Tk@2k3%={6-+diy%R6nhiQ4i|;QPn0R`motPdCU0ASD~7hglJ-5?S~)&$ zak!4hKu~KYePR?Hzf-Vy+s3OM8dsZIWQI%G{xWvq905@Orj$V66a_lGqGZ@+UU~zpjo0Hxb z*LgYQa%B<09}iWuSt``&2Q!JgZ^jW=LdgmDd;!o%To+V0yqF{+Nw)hKr!+6LsH?P= z&a&`L#hc+b(b&(TZe(7|$4p>`HSm0_=mK!jcPpIQr;S70{fD4gYVouqu_Z3;D26YF z$~`*AhLGr(O2bCpW}Bo?WhdU`W;8tm_lu=)|5F0-7@9?xF`MulG4I$_?6JSdpjAP~ zWP`(>V8oJLwR8viJI3noF0|NzFx>)T=*5mtESY zyBaj3P8rIVrpr1XGY0A+=uKFU4T~$6P~)3Z4fq}qWBzvZ{HSP-Gwidx0+!5n?sT3X zlmaLj%PBUZ>w5hmwfcsJqte&$hJ&70RqO83LBQ>zy1bfoPTSJ%;^DonS%5cfMYwQe z8oCB+3!2+uL+>Jv-r84R@)co|^i{m`<4EFAmZYbkTZEItm!TpQ+TI#1C%(9s>gGBVL zY@TAuXApbbu{QqM^2>b`3Uy^n++8H((-1av9`V)|ACln^`rQ5M@q=4om!gtWs+`hc1>ca)JghQO4aiLp zf?S8x@;gqkdlLKpkV|53qTdzX%T>!lOi$jdSn4rGNY8Y=jP{iLj14g{zfdh_gk$Wg zC92ng)$ik8oy*HJ_Z1*gMHxLdo|EC*c zJ^0dKRqPR=tvs={I3<;qD157E#v+hmU2TpquNG;DAHd*xTk7HB#}FIm{I|kRj#y`+ z(n5a&BdQL*hp2CREo;~$_ZsoAA%DEE(?F4zNil|WPOWr6)_B20@nlT`@ze1)o=s#i z100c4wO4UjND2N=DXCV!oICmGLoh5xE8u-n_r|Mep}D{s%0gCT8G5DA>hv)(R%z!N zkeo``&nNi8dChbcGJ|cKYa#xzpfvj&f^Lw1YJy)_5ivZ;p3{P&n^h<84;;YL#^D-6 zCswcMd7`Ozi(U@jBg6n%I+QA%h>F%Z_d(L3*^UTti)4OP_S?-*a;_*#!0%V0z4SwD zq{kErAj64T)YQ&9zt#-7WzfD}eEv3HwmkW20ZeU?gzzVFt#6Y+spXgmqF3>EyOY=IDzR2OusAZr3)OmPE2{&fiso^j5Hrw~XG-uqfKdX#4Qu zj`+AY1H-O`rQk<4H8~B7=IG@|E3ZQm@GD7T)SO~2hpCX~Wf+C(kKC3QFLZU3>xeLl zPIbx8jCXiuMA{_|D`Ap$bg&eiU~xaUzlVpVso+wi8S)T1$>iQ}N%U=R1m)F^OHsKY z+E=rOX@V4LOt^sX@SOTle*ksUQjDW&bhk5RLfK>3yGL6jDelgnt#)QW1G7JC)E7Sdm?Ml^a@WlUS2L?2ejwClBsATIRp%X+3D$9 zcDTuzetwgEkkTRSh-0Q98!X?0-JU)yf6jZ}l(8p`7YLSee5*tZ3JPKwV(m6Ux;)$E zR{WeX45gTm^`xlTY&5%(+~lx=cVqFFVzpD@)(()ns{Kwj=WN#B(=xt%o_QE!0Z2+D zASq!5+gv`fRol5y<-d$?5*|G61m}3x()^`cys`a@f`)-cwPJBmRWtofs$Z*&4u8iDx>&5cpgQaRf{xzZrqN_UmpVKzK%QC>#S$_e zs%G9$9F(T;yoSG2t?_c?HK%u;6p-PI6kb+Xnjsg?JFw-65z&*_^m1v|onO_5O&WdT zol1Fno^?~DR{Eep_#84=6I+rrPdaa!WHtCrznCyq3mtwq{RYtiJksfsS+cZ$=LHGH z7xGqQmkR5Qq$jLI(PDD7}w^n)w@7%BkhLnCqvRnuOq35u;SixwyH+ zB2MXu3loa!#Z_}wy3GiLOE=ghOOn%pK!F(uZmL&u`-mtodb~k=?GjZd04{&7nzq2> zl@+c1m4bq}HHcA7Wk!L5d9e#rn)qeOw;SqJ+hA`T2>F}UP)+`H@bg3eWb!+@VwRFs zSRH!>>gL75B3CseN=H62JyN&$F2O8fV;TU=$qUJv<2MYlZXL_pWgsGTmRi^<*?paF zn~jOt;ruu`Ioa}nBa-KQvA=P&6+Iekc0DpM0nV zQFi-YR-onzsr~R%8(ow)H{TX$CYV?!Z{WE#u*DN&Amh&)3*Do&D@ENX!#c*Sk((&! zY>cw|ku~l}W6lCHP9-1jR0=of768Cr}-+eJaH%vp=>~VARTHbiMb3fm6P7(PjE`TO(>zDGIF;#Ig4$p_TritZKBXl26 zbdV5w;Mz>D7yI*5c;w5v{5zFgTY{q6#0T!KzJ$*uq5ak)Ad3)VF!r%bX^yjlvyscw_lz51j6Et@rg-2^H&Tr6> zV3WIW;H$Xdc5IIR`X_A7S&Cgh1`*;lVDNDO&I@g4l{Dc@5-xx}4Z~Aogovf;`EMKV_dATt9IQmIUK>;f6^E^3FEn|>aL^*Z^ zxP`~jTe|+}lam&X9UpZU)I>P3YP5FOzO0zfdq1FYE6kuyHSR2cA4)gLnl8H}c}!Po zOKK+YIy}3}&-CMmG${k2(Nhxet1D%;Qy1My*Idyy=}Uv-JYaI@llevzNbm7wey+Oe z>G9Lr$7nqp4liu~hqvuYt;=_c?{bE7WJgLkf1qrtO;efEjOpvrZ0X6FtG%Yfl}@v5 z-h4(>DNc#%1K;A}6?`x#aAd&V$gUSphDR8&Ewn@R>%_?`GmMl_j#2Y3^;D0723VVn zz@^NlC+&5{W*;B^(9OjoWH1BfM1Z@^tO@Wet}D%n)9{b#xoNuI+GWjO zoW2&ZM!tFXiS{QG+P`-2nV3ZN61M7i=MM&|pPZzf|1n13eTB)sH^~rSfSzw;U?0Bd zsOAR!I&{^p<uu@M6+Buad)SqXam zGV>G$uQ+WwU`g_YzsU3WPv%!$JmOMp9~7lSb&cusjFQ%j86Wc;1<5tKO6HY6Ws}_a zF|^z1^1&C^zoFHfs&jj`VO+u5=5YC--DWxy4|4G19f2B-wilT(gfbJ4|B3jd6=cq8 zBw9Cv?uk>}Nyuj)ZBO{2XZ_PEPUPAoZl=Sc`7<(rZDG`8Hy6+jW`bw($8*~U8Y?;s zon;rrci1_(c#vihYC{Ex+vtn8D?UHw(TAtBc1o!KLNoX+6!XBQ5DvRzV_ zbL8ja#<#%zMrGEZX%C;7OH_>t+L22nvfQ5Ds{89`bvD?%MJ;r;w#SiNr>&RtRV78TP6_;TI_=5ugTJ{bX&=|T5%lITSu?0ITt~U z-Gr!Z+&FFhC#nm>OwA}5X9@Fy*c^pm^u|*$iflbT>#sbF)A29cOcxqDT4(uOljH?z zWN1SNcK!XyBFt8vws1FXYMQo2z)js8fs(vO+~MR(O)7EACXVezu8l z=fnr32p|B2UMPfMWCS-HEzhTKS{=UNsy21#PXVgBTw1G@lfxhCO5VYQkMpPfMAc%eCjRaI(>n(LAuifvOLY59pqq%ineo_C}TM zx6r=o=Hk~vwVQ80JH?|ZFefS_W2n|B@K$G}^>K=#u$bcvR=>LDGzjZ|HMAzwmhu4t zJlClJU(xf9#zu?SnR`-+Joek+I634<;pM;q=6K^ABrZW7Z&6Jt0KSSq&TQ|y~CJN>w3GwYGVwH>`Y!9JJ% zukX^8uDu4s!_ZKi`eNE_Td2x;&I?&imtR!;$bRnavYOV>)kUn_-SPRH4}E1#=HoZg z8b@Oxyp{BK3`6qk)O-l$DDd&Ws`%l}Y+JE9@BdnRXqg!PytOHBX!Bq#(f)=(+p1B~ zhy)qes4cMwEU);yAw8d{8b%7&%P!bmn$;wpsq*xj`uZN%{&8*w8i0(11i05)^Mr|33|U~`yeB(bTt7h zUB7y!v`Ikuz2f3C>!+PY+9Olb=M91hsh#>|O`k9ThauqMbH+jbS`w{u=y|#nyczRp zjIe-l#ylBNv_(#-;!PLtbXr=AJcSN^rVNIasfFO4I;)ZXY*;|5i0rE2)oh$mH%6pZ zBVZjCX|CW-slFWlVCwD1DshcO$he+_*c?TFWuHg9FwHTv2?1pFylw-RC_0CwVPn>o zJ_*I3dctR1X%TSnnPt$48=-u8Huqe?^>E>j8|Hcp=iC{8vF-XjoasL^hxY^*Mcxt7 z0!VYy>CY#O*GF?CH!2~(%ES|UzFnhyHdE>bSrR_Euh$cYUtxCTF)J%ihJUOtZE$$= z`O}`T7!ux0v7pObz~IlNWSOnlxsZs0_f&N(-{Ug#{nX#>xhxHzn&!jpXs z72FqCm7c2Hwio>TbD{>l(t-(8)Dar>5}hrRWchzClbVYuj)=ioAdC;56!nojnHwX^ zC^S!RUwyRV>a+f6MTmkWSu|3Rc5i@2d_>>i@}O^nS*Ak zDB>z87qw{}@Cj>6p2cT3p8F=x;yRMrIBb_+L(5{=DpFfE>42Nx$|b@pX-hZSWqe4b z9q+8PqblBHqCydT54n{V;#Q2`k?Zr3;mTxxVekcXaIWHXBc7N;R%~zyt!(yiE*5E9u zFBkZs@W*?8#HJuIJ2&t-`N`U>u*LQqBjJ5S95}XGGgmV+p`5C2rx5iU$q*1GIz#ft z{}9puOi@1tpY`g~%*@D}rU!+7*#6(me(XM-sPg=bHXI(oau?Ygm+zlYzYzowAi& znd6(G%1xM0(zB}9NHulZ7wO+pPyJkOtbj<0?E-}K;3b0_0>z1162xzIVldSBR74@o@bL>l8jt=s2V*6q7CLil}zCfQ{_e| zU3jl^%f(u|0U}w8@htg?4-gMwYi5#egn^(~XA^Lc~-}A66m=8iE_>T!UZ%UHo1#N9phM`$D6{au0z z8|c^(*cU=(C|YG}AH^Ev`<9`_O5uJqHPzKrdW7w5Uiml8w ztUHS5Kicd!ThTl2K6x@CsoShN`(o8OvwMQGpsvTK_2FmFYZI%xXX~L@#gE`a@`Ryb zR7!O;@5aVF7AfSp_Fi{o6L!{w|Nz$L>LUC7{$H~Wxa1CM@l@K2J0mi_E(|1WV>JqsveNeB3%DQu^dCLs|)vRTqRwSIncasnisIqxdZkQgG|V)Vhj)@Z6=! z6~|W%!gk6po#qQqb>n}G1e^$9y8dBK9LbIcPI_)KhuEA~9T5(_TtaKB|46kcQ{%uP z+ICwk-;0L`>6rOXhNA_-7{jP$0}$rXG)wbX*T>D-0RO%jJKsj-`cyEi&9eP&AC2Uq z*vPR_+PK7Th3(8hw{$A2$SMyD0T0Of)*N`o_xG06_7^gLRt(sZ7#U>^eP+kU(w|KO zjz)T~)x=B{mA_wFZHG5sE%?Tw)=xg+g@b6w(2#{p1j;b#z#~XyZehaZ?r_Wg&=(p3I+M9 z^pnWi?K!Y2vah|3mRco;9Ms!#e+pc+Lp^ftb1rn^$j?8zZnM-@H*d{OFN3WUqr8`B zUmX<%bI!c??)o+yBTbn-?^1MN|I0oHal5w|im@p;JzPZMj7WDe6Yrtclv}k%)W4(B zpP}ik)i7WE6{Iow%v$1rT9c%HGUUQJ7u=BW%dVXq>Ovja!-WX{|F5RG=4@k#4OQFH z|J^Vr`LTOHWct4h3}My%HgjTRT&pOSUL_hVi$dVaS!W%e`C5%r!q}J0GW({|9xLBC zhsbLz36aou@$y=TEao{R{=Tx3XPoKb#aDa&sb>N1UeC`OANR@khvTuBkRC+V?4uJQ=+BTh|s?hv;q?vc5B>+uLaVVm9tNWDl<4?pKY`JKA5}9Y2cv zNrwT2d9LdA-?c_d<|k{@;E*>00a1@XZ0@WKffygpOvsO|UNy)f;m3Um_m{KM-Pd(k zJ?S)?cka#E@}+hSwUGeN>aBgLgs&nyd3s)p1STnE%eo@0?kius0Z%y8&TB7W!rd2e zLN(SUBH+FlV?7ZbWrS@$!1>@qvr_yQQlbP}vBCSp)wqLVaJ~V)rmH>_%&xXzDDXWp zt=PL#gt7C^Y;kSIuImktUkx8I>SW6#-&)0fEjD0F*eHSLf7iX}USBH75QW$kRxD)U zjDKpiP6NfD*vENCm53|sb^t@<$5U6uRr5)&Vm5PoLrce_MR%O?su5k&zZSi4>3Qb^r>4k5M-Oh5P$M zGO32Ad5Mc1=?5G9ixpv^1cFy#e)p{gB41If1N-~h2PmA(7P~F(BAo(SUSjwh&C($+ zs}VwA-r2?L(DT0b-i%{;^5h0kl{V(monpT-HEA66g6M>kBlEZIX#Qk}302Bw)Dmf0 z%jP-%>Sq{}YLa=@bxXq*dJDuOR78z#)c7Mt*IFYJk*HJ*7x)g||2|&`#_|5(9D{Qp z+bRc4Z!zb`PnfkxobPH#<1I+ZBmysU70T zS!qVn$9WaN$^_XNxhI!J4C=CA`GFUQ%kfMZf;x(yohD0gbKd`9Jg}2>CNgqooF&vW zs=_4yvL60gEH51WlR69eYtZNzasDTl3>pw|xW!mjS= zVey=1fYXEB1u;)2FgZSxtETq6gWR}T|AwB=O5dOFBQ9@7wLi%0u^-Xr%%?J-LaCdV znTsk(kQHcO&sz7oO8|US4Y85OY?j0y z=!}i2G&U^T5Sy>B`$AWz=mLwn*;K#sc|s<~D_6#YMtMptfFm(*d}KtpUXbaVW{5>5 za0uw%n)x%k>G&oan@L@Cl&#IBl^b_9j=r^rVJwOET%`mdZC>jFcKT}@DsG>2CMqi{ zYfhF7_dbBJAX?nvBkDIJ3Cv0!A8yf^zcGc(_8dQ>?^T}J_;fy2Dpw6ZCut=+-20W0 zoqs_3-3j8B$@pCcRgm-z{wDNBkt@VWKAzCMCOe5U1w6`ttI0?5$+dp+e33 zSqv`ik%?mF@;B=-*)%x*hImlCBD^XrM7t%|@p>d#&Lw=TGsQsc7PVAQ?q={6T!ViR zgDTTPi$Lb-wW{F#s?HSHwq@d9nF+Y9Hc6uIBgP<)FA8bSQdYRDRzJ`Z|H@iekmpgI z9kKYs4?CQ#ja1CNv%6l`S8`Y z97Y?-_#SOXN7Mz~+R%4utkI>L24SY-##Ow*p+fix{*>gQo=8t|g+jWy>e`wpJ z^Cvr8$n^7d$695IgE65CyoZrtXTlS*@`HuOHcU6vaEvU3a@r|A6H!ioQxjhJ>(Qu!FY*3Wl7 zgM-w$ET^6;v`8Mmu7*~(8tr8tlBaP-TH1kcVh)_!vAHeJK$rVcjrFsEer#A#d2xWcTU$D<}^aeYRC*$YUHK`!{f zE!(02e+&gS)y6Eji>kPt*%$mCyp}x?2^hTc0$X6G7z;m1R{MOnZnZrH@( zMU~R|ta_vx<4YX|4U}CfVt6*!r9V0E>Hjw?8@$$l(nY-#MpzqySgRD0@s}epW8(r4 z5Z`0qwszG8j<=QG*daF4ZKZ(1N#$p*-uz!V4OgZ@rzt~JXwv^~G+=Dd?9hkk{ONpV zxk_5wiF(ubBw@WG=wMh?BSFZT@gGYh&L1X6cMn`~-hq+~npY+ z!Z?3G_M+~GXY=ZD5+MPbm~c&1SbXRoV_?*ZWawyT*JyLUpysO|RZaDGnUjeLPo~>? z)&;n&^5MZCHRpJ88fb7h3pDut1nJ3msf)o&(6;Ar5^t z*ODE+Yez6FK9IqBc};C!#+`J;dq+e|aAYUDVuWsw*1kF}JugdY=?Z2g%;(J{n-|Y$ z@w5Uzd!G|X)0!BqC0yp(nc6;3yCqs5|YG+5fuo$`SrB`de0=z0K_Sp;JFBqvH z`BC%WzcPq;J+I~*$?77$?x0ZpyEFkIvzcE5zvIjU*8+wdP{zEBUH#w-Etd^b9^e~N zAphvptU0Ox4bVwazag>Zv&qu@gpj#dM%8wM@a?ZlSEIelnq_aQ;)Y)66OZbR&4D68 zLRSbk{kJM{LR1mSEthkaqdPS$t6cwOA#B~yk5AUD&6qzaXvoPLCn|La5XR)25@685 z+a_hM*;jniTu#L+;nv7o=-Q3iRA(QAAaDU%s>ELEa zyyo~PB}f$T+TYxC&RscdrSVXkgZMx1^t08N%OFgnf6jAXp5ASsYp5XtxO zg$_?OCD-K7f{fKXwkRpi9-;Wnqo5)mR>o8kq=f!uYZYu>2qyi$=FAPuX~JIcIlohH zIy32ELq7$3tONG1xAaLQTFl5%$0^b)aVo#w!fU%F`Ykoaro8EsOmdv+o0<+z30hBu ziK26pk#r`{nRxS`doo{cbA3m@8F}AuwKfHo^43H%?C-(4*GD=7%mvfGHRL`V=P;WP zK`qBJ4De{8pW7DRiEIQ_32L_11xQfO8k;ep9Pm@%Nr zj~#1j>MMW3)&_Pc7uQxI(U=_x zTeX6qPnM9Y;1_Mxy0|~w#>q>+pMAJ^i{0RzdW%HPkM}XBnd=UL^Fs)m$k0o%qERQ2 zC_$!Iv!Vk&@d9^hHPZeSL$sc=pxevgLn=ob-QTr&e3=tA{V^SuOq@MCwcQ1^S-D?-ZyL`T135LAl}p$CR7`TSO5PeC1oR% zCw2Vzxf2Run!{fAZql%Wt2M7y^WMTK(A2jQOycTUa3p?iPF*P!fMF-MHOrvcS5Cf) zW5O`M)8Y;NP~WooUq7nj*YSP^JwM9y#J*`X_F`A|^?^8p5@xu78pht^K#3*U9VUf? zgWaK@=wpy#P(0cAj^3?lrnHxOBo+7hX+hUv=(fh{wo;B6Ptz%*P>)i1%_;8L#!DaF zr0Mqk(Sfwz$=seo;>)b>nvCoP25nJp-F>WK_$+#4&h8luEPUqevv=c8ZqT z(glc*6W9^ucZ!F_f}IOfbrbK{@*<9`Ai5c=?dF(dQ{JXv91K_u@BdqaGi6Uhye!M2 zZI%+D=M^(UfgqpLTS%Ep(HpgOjB@yTZMHCn4*8$DoGob~O~fAM-Ys_FW+}Syy%z>t zBf#gM22p8CA7Zeh9`}pD=!vMWP7jAP{*VCC(PHBNF-PV}$wB@Eg;@S_tGR?5nb9+q zmJ0h}8UJIwlXQsf!sH~V)Ts(0hzbn={lw&!2Z*s2xCPekFriY5lyyMCs)gX<-;%gb zhZ}_dUp%MMr)UM3mN!{#_VT-nOJCYJm6TvFc4thy_M2(_7X8p0GYfOowVyw1B*s2L zb{xAtP?!pTkwvPbx%Ae1q~vRkFh2eu^qg@^RaMoqt$y?u^zsh9r`vw1d4n1EfXbZX zR^pic9VgUGP%P>9+k@BF^8}oW%a!b^pZap?lyvST+LdyHh$I{n#>c2kA>`qx)xCl1 zn2@Aeoj6}50tc{7L0@Qqi*A(I6Eb{BnLH>|$*iVu^joj_c1MG1EvmUi*MH!PJTFZl zWg+21+9lI(5%xYB{tU(}ngbD8bik;h2QMP-#WNuqcPPyGlG&#^{iRVn8>dVLas9Dg zq`&aK6rLBw`66=0-K9WqB7N{2!kR9f#%n$J%I`q4j_O*T6G_;But97}vx9b2cBMz} zMcsHdfou^~Q-4t0YROc|X~-oRdj21c2VvDI^RIMiAjU9v9mEwKkBIN(G5ynaXs8K* zFmO-EQJm(hKrgYDPNr%Vcn|hxVuISNqe}4~+@^W!wRc(Yhd?5bg8>zTMixQrOR6`* z8-#PNEVN0e2E|)FV`u&Je`34K*^a`NVUm(6Bo(+dOhCzoZG@Y9s=vBpBk@1uK2 zGx3by!N&Le0B!;81Y>16%~(sGoYcMJPw1!7Xouy4&v9E6 z4{F{7wJxdnukR$&c+}rcyzPolb;@43tCqua&!4pt2a;-EUQ>g%O^EYb=Mz zH+qx(J3q(t)#t1lJ~+B}4)Q*mex{4uo7n#mOKv)5Q$V7D6MKG)SKMiaRg1D0N$j&rK3^hi<;v zYIpAD>l$S+rJgKoKAj^#cZ`q|l1(n{Nv$q)F2XQU!Se{|7Jk8?0P=TMYt)(oXCnj> zB{m<;crRu7TEu{?DbmTHeJNINi*wS{t5P2WOg6-YHDS*Nd9%cY^G(3hc4lzHwrQC@#Z=2w=tJoX{^=hlCC_5z{EK=kdpL0jC{I#ge110{xsMMU#0up!HYG_2Fw}6n&Eu9Q)#>j zP|NgSS+n#b@{kUqQLJL&{=u!{k%-=d&gDJRSxh6C=*T((VT(eIdb=G_@WuO<21wg!U~yvd7I zkbE(+Q@9`o9*p`L6YDs!qCc8?mdsdLBDHjw942X3?s(a07o7Urw>i^)9vH~|sbG2* zu(Eot<#gR?p*@$8&s;grUR(O~#(=irQq7IxFEjspm)q-0hz&?cFz!xS|IYLR3LP2m zM>uSyvkD65m@s%@NS@5t_&pmL@3CrqBct$7UNs>3v{XaI?9GZ}GFIbvGS@@H!pbUR zzWa1pn*Z}l`?~06i#scMR0V1uPhL6)`-S;#o?_3iWFf}UVD9R)qZ9#qi)7n|&8hi$ z!FNXXX=61sFN<85Uh#_R-eCxmxfLR9uJa~)Gh{<4U3FvP_$?`SA2~#Mohv<~0T@-z zz^Jrz5sr^81`5@jIIr;i!57}>X)DNC`N~o3wA0Xw39HwV@B_QLD}~BzW>x)gSoPFhEX25rSG4=i z?gZ3@PY3*WE8BAkQ;<2H3>^4B9}6HrVyD|+a{R(VS{q~?7AdZHCSAuyKa@=O?*@8s z#@ZGufN0C$gOmK}mm%NLL8*8l;n8mEkA%t1GLNQ~eLiA(AfVp6S{N*VlztYCHfpb2 zSy2UK1c*?E^T4Ax+WX>rvY&j-OTcvI{d4c1j&$D;PYs0d3|1yG?1+yL*&d~~bEkRv z_N+H?{I`!@UyOvu!Gw}5;aa-}w|~T)Y_@bo9u6mM-a3>C+H=@aObymb#MqM zMg$zEfd_K#+@fL2iY3OSxSw8tv8iXSoY~aS{~|o(13B`q?EqePkl*VlXM2L!W^PH- zcfQ@7D<7IBteF77CR3LirjcCIlp1~zu%r4ejVAoekr{g#m3OgsJ0L}e0_Keq$K^&M{^xo zO0rX=)pPRUD=vj+W1e*UZ)5&=zJFz?U~V8!;Nz=Wuj9@PNN=(n+U&_%A4HcOpZ>|M zcDaXdTkGcECDHTX$l$htij=64B&Jd&>}!!;@FFu2PH#i%OjIO35(|_`b8z$_ED5BV zpz6L)H@U=+$N7#wi~$;OD`rk@yVCxtn{X!C=+{hXH;Sy%-`iVIdmzvTI!CO3C)nOd z8&~yh&PV#BA|8)Ko&4NLj&IA4J=uC1;)bpClbxT=5+K=)F}c#(OxaIGc{%4lJK%w9 z0H^J4DI$Ii(C+@dFnHC@`Y`cG{Q6DfdSdB})uOYK1FSYo>SKfOi7|5=%#8GVA(7`m zT^X1G7)%)&;n1IY6rb`-pJYf|^}!c?pgQ_Lti%{m|62d&+4bYLzVNT@e+^Z;5E~D} z*uV9-iD{!?m5<_M;F}>$`>&Ram_M=o@3z+2_iK+M*s6Kd!kez73?uWxxdfvMLXzTJ7_(Yo5i% zGU2AqVFZOlF`7Lc@z$a+c)TIT(RS|LxaXfwcg0$BawMSa9(|z!anRa7{U^pwd11SA?#E1vTAgTZ^{rpg4Y{PbX3H*A40pVDa=a6xuzeR{ zoHisR`qiHCqZ_A$Fm;?$`q+6>#hfoZvPL;$jko<#{MRj)&8~PHAUUcTEM4s*(8i60xV@N$0O_5$M8&uP8&jfL&w$1~|}@`nNzL$ygvc!lEhI zV&%+in!&Qu(3&9{1j&;{`^Fp|G1o8+er(C1oB5z|edal(D&OY-iGb6}+rK%_6qw+j zFjgg|7JI4`pI%1+%{NuCoA(}R(-^xe7jU1rJ625mnW3fiYY!z~OPjrZPse=KgRp~PKR*y_d zjS=v|AlN0dNRmLmt;chRHD*kxBEmJVk<%j}P(v!RVC-n&$5;QIV+dPqW()80%V6D1 zw&>U$-w`gytpr~+mBsy?ElpGm#Dkoti4%!Crh z&y7w*ygJ9Jst0n@=K2>emhZ1mbvTnZj@w?{Z9eK+8gYyHZ9}z-o`9oZOvRu=ng;&qULnRlVd%W(A_H6hXnFuU~!(^H4-&uf+TH+2iM}*^fKC5Yy4Q6Rj zRJMu%Fzl?d1X82J1ivk5;3*>{x`N+yh9^gVcY5=?%MXe#N{c>%9l5wKFs?LcP5rI; z5}L--5@mQsJHGXFW|Li;{WI8bSVQvnU;~DBwC2WWF+>m+zDWXQ{?VvQjv#RP%&(!J z&B+v=p2ApZouifr>uRC9J9Sb}f`)xYH&8&dAN=Q=W-dW6-#^$!r^?~p(g*Lu1tzA- ziOJ8tPi{EOx`5;YMEd-FA3Favr=%h5!ko6N*N$uaj)fQ?&mXobT-+a>5-QDQQm^s_ zmSvQ!**0S(^NoeC$N8Arltm)!Vkj+KMf)I4QT2OStg68i4%P{1<^1N+N?F&Xi}+1aJcEv`&wFFytBp|NObKVeMU2<@cJ=oxlTy z4HV|Q!k!g`bVC=iX$7HX-Q1XIDF7djJWyyhmJ8VZZpAakS>{uG@^-v0eL%x*yq)$m z!Q65+k*$bbd~x<6EzXqQ?xEb^?)0Zfua^-VRR{rR1RS~TRohy6hv+aJXugXj^NO}p1KY<{MN=E<~ z1nrwjCI{F_fzL3}R;?*A{jhBqfVO_+i% zCgEhtfOC_ra`?@Xa)=E903!#MiB@f%#lbLe6)H|^+j1ZSmhzu2mi?#b+)-lr@#KvY zKi;9As5?I=`_hL29n4s?!7A%x1kQlW@g90!Ul6E*jcPgf(1SsW8RpJ!PBRHCr|X~s zn*MxXwvIiQ*Thrt{>@{y3G-o0onh~2e~X&>rmsNp*iYxZ+@5@RBXFM^L3X)VNnwr< zS*?}@-Lir>?5aWhB`P)#mMv}oCmMMWZ2!EkQNZfKgK=--BO+0wwyuHu z8QXC{@5@RrFwpssJ4Uc+Yx&@;TAO)wA%-c$*|AtM#tP5^D8v6l+F6EGxwX-H(TWm+ zgpwi+QqmxZAOns9UqPPXK|QjSF2Qbt*?WpfOcvo1AN5OPk`hrTe5Twrh|?l`i#8&N)=eC; zC_|%RN|&*kZ`jJ|UoQ`N{YFv*hs-)eFyIF@t+xJr9`X%)p@k4iJ<**`&AVp{zc-ia zL?}#11{nyxoiWI=>8HaMVl91Zq8Tqnn&#KaS;_8b|4!lKFIJRQw8!nS(a@}BW(gS% zSB-)xF&9jUL#q{wK%zEWCvwxRg3MaExPpxeXMj+#815|1 zvXH(kt>nEv_9Y@%5JbDk@D*#(ffoxf8zwAqVGjo)oWiq{o?R??}F~3gfJHcz}86qgQ)d8 z1j)3PB`3w&3w|ytKeLDbatL!U&Vb;{|BgNau0?&-zeu3CeUIQTzhXQ?L%Uko%)bp5 zOz@+pm9j0*96)R#P;p%E>a}>vcZ%vlZ~zUe1K38y&EhX_l&@DS$%oqDOi`<4j! zaRi7M;xITTl2l<5r*qEh3wJ|L<+(JF&n2_7qD{kH?u z)C~R1uh27pFMR4C0lf;ogI9qj@mscDDnS=37phb$x@M4-AM~;WPk*|XRHo8C)1+o_ ze!GZQQy&AXBYDKBa?^jv@1xGEgFn5Jjk6=gh$77KDD7L{xKCB2LT+nss3^|u7R5ek zOfsA4WE3|b1|Z+yP@ZG9a5HK!A35D%E$Ow=vJPg00=t*)F-f>OOUtd ziEX$;>S1Da5dlz$q~KkjW9KfU-9lg*fi|R`-jk4x;&NEc420zS73ijkV^1zge_Z87 zI820TiH_)9X|-e6Y5rH_@oYU0t?b62NB~*HYtkp!5{n-?Te~Z+1FVVIhFPl0H;i&Fo;MGsV%#+Hbl#VQsELG7Q$D1P zLD*nz!bH>fU-&2znx|W2f9;I_4Ql)Y!hw{R&q3hH+Oz}XMXE$ z@IL`*S_u6F_?9q)&>TB&jzgj?WrE_D%s$r5@kXj{Sz{L)W^n!V=XQa5;pnUgz^9}S z4vuJEz>oR-RQcV}p_Zr&7VP8Yb!CZzU6~J+rV9*deE|I`&fi^6-=0ox1h?quT6PyH zfqW_s)9O=diC*)bAyKCHL@6^PJ6T}TMP+5JvKA?a&vvt5!^vy+MhAe`08DuiKLhBq z#Cyk*MQAMFlf&>Y71?h;)T}6zwP61YcT{|Qzi5N1gfTNp21MdtYk?V0PgPmcMTt-A zH(IBX9RdUA@B8Xi%aO)>iYI7_)fxxTx5t5o`;2fY2+Z}+C4CAW0hC1fa+|=%ZAG73 zF=zMakE_qXpAc~=r}%tvzH=ps3W}rKp6nH=Xup5R%6c={k$Fh!u}C95@gn>rsqLT(!6h_?C}7Jt``ef7UM{JA~yK)e9!?g-3_9s zH=j*%Sv4OU|DcH5hs?tNmz-ljji_45_1m8wcXFS9r-{|V^f*VyhY6N_{RK|)Fcdp! z2+cE5E4JkaoZ#gMjD_8|_XR$`1;ZXmiHGX@y68ds+zed6Z+KdEn#iO!_WzJ{d~dkF zwI6zPyc|=P!ceZIb#rt&P-w3p8KvKKauyeLPkj7@!4c@rxJBW3c&JK1LVRV8g)(uS z5a>5qS|C9`DtfXfRsTU8h5vZB0{4+pBoX&pua^4TUnM4$mi(=6+B|(M58WJ>1SsS$ zNuEkfa?kP=w-JEf&a_;vkTX%hH9kJRc+V2NFV+m^UBYAS37)mzJY(grcvgXxF?8?uL|_KtFAp z-nw49T<|a+12MpLr9tjR8qu$w_SfJ^H1vD7>XuaiP!3jCZwf)9WBq6c%xV}hO}?2 ztOI(*AL}T%Yj}`!C}q&&gS65D?y$q?>8>bam#AY!A};KE*^E~EW&w=HD((=-2&bc` zA}Q@tGwQE`PWwJ%h@MN!+BO4*RW0KUJ>=ksct#{MK1O}wcX!!{;H_*(nSUHAhxY2q z>gmDk;VzX>Q(0OA+T}HINHmgzL?hNS(WqhNVN5R==Qiqkr>E(7PHbVYNT3gb$xvEk z5fGSysaU4tuD@S82yP5|Nr-@hVhJ@??a9B>+5!wN_IuFIux*{?zl+6~nB3+>S6+Q( zPgiF(t!$aTh?>kE^+5+4jX^2&J8=@c$`~ZwKhM`Q0=WV6L$SxK9j?;D? ztr7kqwA(NY^BM|EpNopaflb%)&s(O8EpHl2?E2mW962W&93q~L>{A7>isO@#-Kn{x z%+p6;t7r@;q zpf}@w<@8x@9Mh&lY(jOOabgys5n5g!-DSG0(r|?C2DJ7CjR&Vv%mYc_k%+AiNrZQe zm$)`MfjSKNRDU6Wff}flK@I%u(+sZfbdl)ajYPplPDIQZypFY(wx;h7>j%Big?Lx# ziFX4u5X+(u{J-MPgD6nknalveC82|p)6w_yVkfk;(bk*l9=D!b|M(51{(rI-&v?IT zVAsFQ_>1@}L%_gW=rf;__Wa-?Ej94DqOFtE?<5EmkVVca{oykBzr%lfa9XZAs|3(` zdxel1S<@boS?}@TfB4S?`l^@ygm_R&4ZK~Nm;k9X?dK7h*Y|Vx|C}*rn~4{Ix3Kip zuI$uA^$;0u^EDt1O#sAm1MsG6xgN{yEX*APEOT60-@DCezS>m>tej zCw28p<{hWlHMJ$$o6h)|k!GYqD}dYx`9Z?oi%2}qNL=%pxEiAZ{A9H3HXs4TMD$4hOa&u8izpcjR!rIx)3~Mm0R-e`V`h>6(j!}F zSafueDYpj)I*$s25U>wjYE(*xxJf~kMFm&_%h$Ng1#2LpwoVs`9DYZ`qb)z* zw96$hul^qXH{d|4=LERB#{S5iH{r3>AnA;__s0TPq-kpL2+z%6rCC4)!3sb2Th=~BP%z`be|5IF3;;A&OA1`UL7r(e_ZV>OW#?zqVH z8Et>CFcloQvAeq)|Ff!*tdVCg0Z(3Kk#QIu4+%L4aOh0x5%ds+v9YmDZM2eg8{%u3 zV8Omvc7wluVnA{A^Tq5--@XMzAfxOE`bl4EYhPyAiIGd|bC!97F3T~m^e`{HPzhis z%Oy)Ya~?iNz*^~WF0#;=zA)9;Ej z5dkm@9-Ep8Pd)qzTNenR;n^3`1E9~J?~^~{@d$z(ATj)TKVY2Lw4gSW9!F8Z3X6+F zE*xi_dI49mYY|Weq>)>~ucJzXK~&4h@vi$c0?TIlcIR%x0;M!XzNU;8Qx_GxPfH}Ake}Tn?^8WuPdhq8xKKpjcxu_6LcgzA#Nd|NPC1ClcMZUNGLVc4Y)|@ z!r(lAnP=VO+WN}HmTUQ*&1S)7+F6D%9b_0GPEH>auIXz)$hv=U#ln;aSVXw7F2K8T z*RQ|B06PQlK+x=0pNKdv%*bP@4WyxHsw@Ea!I%$X^5w!)b14u)8Oe={chFQ9K++xSAeR(WGz7O0Hb zJvJG$TP;;BT8W2hBgTBnh+(2Q)Z{e|S1YMj%|uQ&+eJl-Q$^Gc-<4beIT~1 zDf%-r$6r&jPleJDg2hiq(LJPBq&P_N_asmi<<-><)2{Hnzx5@8o@V1@rrtTe#a_4O z=j~|3Jz}m{QlE4db86<>gi2>J)yth@m@XO0Xlgm|1w9p;{sMkAAn@=8k#tOX0P*0V z{t#^FHhGUE_#)EK1=94qs$H$B?d;UafUN|M55x;o?62cPWHA|qzeo~@%#%BpgwNQ$ z2}pbLQ-qt0@%CJQ*M2o~b)Yx8kTlNWsP^O>D^$p_mh^@gnQt66sOQoLAiFkfE>I5u z((;7Aui&+Rs!1F3RcZdq=E&Q}gF%3f1~MiwyVt+3K~<0)U;CINr5LOD*DjaV{Hi6h ztndYoX`kq{4q*<}YrV=T%!IP7?|`of1RR;1Rpu*FKsS>+z7S~1VPk}Wo&a*quXd!^ z^w+R3%zek2OUWHTMApLm{Y#ojG8-9H@EX~5{l~83GEwWBiqKCkr(WP4`mvX`60~7} zxFhm0f~hgCq-_SeYBGK8|622 z>1Okd`5X2uB-%1s>4@4gCba80l_~2;RJ3nD?djq7170C)qvy33VVuL+v>7d;ybIC?$e#D zH~pu4gn7LE|9i>CWUncuN}@kxBQK+FnFk=#{GmA|Cz>Yz z`T5pANY4{iFQ0^7{otozQPrY*#9BlL>Ic%4wGdo6k6!{dm+N?&_8 zw*QdaXzOvAVR00nedNZ9AQ=9v!R}~HLh}L#Y4Hho44lmJ?NUkZ>SI@nqTl?m=5AWQ zwUD>NBlu`kdQ^oHBJqB;46G)Ie?VV6A{Evt$2;;$vhVokPlK7a=A{DDYb~w#c3nlo z9mMOOWQ?q(!JGi%jKF+8{2lO4J{QoNjg}^44-vw8BSQi827x*n5KOs5rW-VE2n(0d zt!00%Sd;^N=cW6C1Mh)X=uK7~d46{iy-f>!+xk!z!}j)D!lM|Ys}tevDR@+C zXzq3fun3beqg^6c;VyU(L;{|T>+xaoT*?n;hb+CzQ8)HG)vPt@B;H(!OcDK(zku1Q z0=@YjYeXLe4B}pi>`cyjTqdtT^mwF(V9?sm?we50^y#)A5rKWb7h{^uJNv}n$`IG+ zcan;Z48U^HR1hn=g)+9LFsKAJ#3RqsB4QXW1lE9m*oC+Df#|9^cmRCGba@+-H8t~+ zQO@xJ@KCy@2)smF>Si6>bbx6jZ z$QI><{B64z8&TJ$EL@B9_j82vpKDB^=F|~v_@@Hk7N*Ew`eMlg-Ioxd7umn(OrC=p zz$8VlJ`>9pMzNYsRo{?{qx3e$moDtQwAQ{g5L{P>7H9!C|do2IXQLWo}) z<-)76O-{}2b{$FwKA8>_$B^x|L1(%b^4WNIt^-2vrZ@>a%}+_41XP=ig$bJDDW^Q} z6JgX}PO_vrjTZHmd!!PR@+4Cj=XNU6T68dwPmV~Vy+nG<5M$qKt)OK?7(gky7|Wr( z3*QjK66iex?P`WfVZ#+=hx)gw@~Q;NaX#n-@R(Nf4xt*stLk5V9xhP8Ar}040>}lR zE-FNzd2DOHf1S&`S{kQBM<9@LNJq7G>#tzr69CBy^uBjhYXUs(|I>{*rd0Z!=pPZ0 zZX78u7G%#Ga)Ad$8~=MW{;b1^cdupEs;*VE4T0ADtuF-%5E-mvMBK~!&JoSH-Oh|0 zK6Y;4FOtG&2I|;k3_QKwtM}&4W)Jg#VkV!PUZ9rf zul&OwmbZFNvlUTCw6^Jmg^W^{<7OZ%v4NjP*q*o61rE23WR%j1qT|JuNcPbZX7+>5 z$o4EJE#6t6DSLccuwy#?9pN9p5Q;9F5ii5lx}TAiXhGJ=8%}m#M`$ zd1)61&JmJOz8E85CzzBm*Nk{&B->!DdP#H_8G?;F5P1M?5|&Z*U!hi7NEs43sOs!h zH@1diG?*}Z1{811q|K^=8YqOCAa}wID;p3T7za2kFQ7V2PeG&u0MVufE z7)sC}Fw(XXo@E$+veXa&A%;oLT^3S*qo#cG?Q`Sd^1)b^WJf?Y4A>(m4Db+&#r#`Z z5qT>7t^A8y?=n>WWz?bGLECaM*9!`lWuMivEWaR&@Bk1U+*9r4Kv?keyAW|oTY!#* zmz5I(tba4c&%C__6q)B4Uf@$6k4HNpOy86dfSOsekJ|_K6Ke(?CQ1lE^S^0^6qN{C z<}d9i?Xwm1>=4Mz2P9So@M?tQV)JGv6^yxSxz*F8H`XKDnkaF1va(!Tv|OF<28cU+ zb3Lj61%VmDaeb&^2R8Iyq%Pi%ZaaAbdt~t)N#P%vJhkCCK^ZI)=?vm~+ef^;jvces zk8qUwA~r?3k^bG9A>eJ&OUqUtNKZqRTMicgTEKnoZYJi#j?wVyCpq4s(t5ba8+Kk6 zUW!l12$9`p<03xlC>4x!X`#7p5gY*KVxEUM8`>5UggBvJzX$~?~klSN5jnX zU#J}vz@XIt2M#FqneNjPrMoUIf8Z5jBXpWza=7?M=Yp^{^su92)+7+9x0ikuR`|$6 zVsf4eFL80i;I~60(wt+0&8)Nt#qOGHJhRK3cvz1hw$|Vo0h8px-54&4NVE0&a>?=$ zO*He6kmH7xrg+Q`5TFF147$e$1XEN%D$bjj#)ogA&l_?P`e@w)j2Zw5l5$g9ri2$a;mJ=&!EGj2Kr`goPhCM6MHG?8olMNb$GMF_HXM&RmI` zjSr@QM*+xb0QfJ*r!L+(pfe{!1Pe7vZ$c)KGq$}aNgdjdFzI7YCT)wqfpirh-yV|( z%Q}}(&IA@odc6Rn3f8YuroTIWH|9piAN+^fNYgQ9JP6tT{{+(2tur9<`z!!q+y96a zmUq|iK=|da`YXfR5Q>1(5r{tG#MJK@+x27m<7HK^}Yk zFFLNXtj#=Iz3qY|)F%rU`~l!mincNn=S(okB9V#YY>G^s9L!dXf0adYpP1uRsTmL? z{OiV0m_m#k*BQ5dAnyzv0Upje<%L8fmFw-)s8WXM1t)!T?9lVk?D3Vq?-7tuZcxas zPC>4^5m(6-E>J4tHh~UYtBKs_&Z`MHtNa{EEPGl);gkV>6rJFzZ`(t3hH|)z)d9$1 ze(%)jq3db%j`zpL6Ge$~YpC(-R`r}9AgbT{%IVlHKau{(@n}RP30fD|PovzoV0G3c z5cgTcKbEg{S_KG`nxCibBYj^ROeWQ9WG8BiPESW)4gO48Ug1q46k<2|@wy%BDZzPi z7*;e%kV%4SCic`dlz05(t?wd4oZ2q3KHOaC%@Lo%qfC+^(lS)syOcE1CCdw_I5o&`&)`ajn|B}URwq2PZOkuZIQVGl9yHNhI*O6PK3a^V zBR0CFrw)*ZN}_4^%_DltNZu-*Jk$-T&bhRSu4h>cTRM6?J;q(rgkEyPBDq})Lh5Q% z<~&q?^#ckUUhg@qu(nyuzVL2B$&;s$Ru+L?a>45xlg$)S7dj3s`5qa0g5p+G*OrB$ z*0ph|l(_P4CPd&?k!6Jd8E=OUtD0tq(nx-vuPy!cZ z43Fh0rq(7DieJR~-<VcH?^3!la)n>8hH_mU#%N}B}c;k@27-P2F zc_FG+q^9nk+%k`jtHp7<1|L)G3-e8tb6H_d_LA2-V&0%1s$bWMINUdsF}rtYG$R9Ij??t%~V=kF7`pTZ3+a%EM3~n zK4-Xszj_ydXLo7zKJ8x40hb9lHPWcAb;6Bi)=vZWA_sn)_EitFuJN5$r#hT73nhXA zNfIsPO%ysG>!fxGZ)guxUm1{`rlE25JLgpDYPR{a#O@pE1ie}5<8f2$1~R3Mw^kVe zs4g$W8!g$}MlS+#uF3t!`iYj}>@)_mvm5?}V#ByFa@8-beL3z~c4RggAEZ9XX^igA zEJ%|pJ8yV)*YE?Bg)ID|4Oga0he3tk!8Ke#r+Yu>lqH@W#h!V;Tjeoi$JCg0|{L@H$9CCG*O zmX4g9$BXr=&y_U%#b_mbLP_0)}KuThARp?HWu2-c^NUofkbQ!Bw}MoBCgUE zR@WsSs8obrAU|lkE~?kQD16=NQ@HchXL#O}8~~80+W`3daSC3Mz<%&N zW8m#9P_2^ca=Xp&*2M5j+|)wcDuIl9@I-#KsbNMIm+WY#`#BfP8#MwH)s#J@$g!># z`oeF8Q>9BAq6o?OQKS|6C0!6caw;;d`GHd>Mta2Irk$>BXYn?0@qBozB>`CkVKxiY zeG>HH=>U=9>+&?rw}e|9US~zITx@UPoV)AzRp3UuxjF)u4_G$8)P{=u2%CHW z>Kt*M3ZHj+T&j}LXlLpXoWAV!22NARC3i1QTZ*YArR3SOI=-3j2cd-#P6`QA^luy5 zq74sw;1WTuLi;C{IP3rx#UgUX8Nk0-@(J*!WVQ z-`A__f}I?)>_K^sr1%@zu4Z9Ky^B6#zs8^{B41$YOi;2r^9&WL~h_vpzDcW2p-;9)81P|)1D=Pm(gIMTRjWdzX+$8Gt(iK`Fiu*6m?yIfjbKrxZDg(UvEMT z++k1$2{CXd*aYIha*M;yc#J9N4ZJy&{{3?XRdrUjuf$z%u!5AwuDYJ`?NAw`6U0NtrAX{8qt<45lBl9P3dt1Gl{NUTw)0a+%4r&mH`$FVZ zjMcujW#J2XI0>(G5Zlnn8i3$%+lY$AuiOU#2>G#f5$vRtsIhszH%`obUc&`liln!} z^p<9yeUA*?c12~K+0-1{%gFe%sE3PzZX75v!Qp^^h_!Z$17zDOB_APYmyVT#No)Dp z%cLxQQ{cXaqZ;9FUH|?;)`!9QlVKc!q71ou*&Dlos~3Q%jl?evZiBvq8f~*4;OOy; zJ(y;+Z(tJ5xb_t}KM?vC@G>I%8238u!sI|lR&1y}r$s|E6D1h!pu#*!W7eC=4|iRI z_5fT4Hry~CowgX$*vOj2&VEo8C#)3827T^B#SbYZ9y^#hSY2FPG~;?s)m$rD zEEsUZX|32<31eXEsJ}<0dz2mvlI*TWqQwQSA7n)2@pgEG=`h=NR38S;?=Bo4o@@~p zx=)#&!*crZytHcEl(ug#zh07M466crPZ>Fvcgh^sfdUq3vNx`_`>y$AGPu{%PCV?E zD$N^uLR*zzZrUvHcHUjogWvd}tX{nUazRNk{P%D9>-GV(b-OeJLc?We`;cdmUvSW^ zoRr9qE!YB`<3m248gRUA>U(dj=y5-!J&f@avA0FYJmCOTYozVdHK33Vli!16p8lM6 z;yr)n%NRt&B)J4=Na3jmduC_DXkLI$zClbg(u;YCQcI*vh0+XO9 z5awU60YoNHD*JQTbhk#%GAP;L@B$?p2@f`E>G?Q?ZF{9-K*`1cDA{;(Sn)${s|VTn z%)@{Jil^hi>ohMkNu~?KJ-&n@b3fDx6@2HDiccH9p)751y8!`LNvLCtHI=sbR>>iI zIOs|`-CwSVzg#TP2Nr$>`D0Yo3r5GTVA=m=v>-P~gm=IbOzONvW?Ytb55LN&5Z{Ox zKox2V_jYQB7Zn%(xKL)(cq+Mg)u7xsK#Sa$_nfe?c;ibl+L3;Mt27RiUD)9jbQmXx zb@0Y%sQ<9#6-a*#1br+ro28k>biZc#JMqV?3&0>wT9}1p!J`r%tzttT2#OW;l2g-K zf26v2%SxrII33o78M1^5D)XZ!h+`@IWSVyq;s$wk)M+D%Kv zm(FdhRY21L7Jzc<74{DfY=La}Or zH;4sG2q~yblRDbU&3n&(sD#&tcn@(vgpeEA!ZG@9gN1`9K@zWpiBEMR460UyD(w_Q zH}ybWZxwg*2tTO1>k?=~*4@bht^XxTfGRHJH0uJbiprqvPAYkxF%B`^iFMo{WoZ}& z4F{GLXAF{=2vDl{0xDHpIxAJ=g{XKDwe$KrH*U{W%VuFP-omN0F*pYP-Q#0XDhg}GKC=>|*w|L4d(I!fK90e5 z>9dvAiXn$A+%ePxSWMeAUJE?*PisqyHHdQ?pN9^K!*mYQ($d%g!?CG<4&UZ+7NE5|e!LnHn?aNVJW-nbA zsOfgTg`?f0MFWVm%$fCwuYK3(p|ag1;Lt_@)s~`8z9%{J+%Y~DU%tz^HZfdB3nqQD0H6Y>Yf5t(2D-Ixq_aE43eph}FBWMeL4@~Gsas^~?zxYgD5 zehi{uCtPxKr6!AjBC{7J`gmz54V`=xks~kv!wawh=vNV;NT{fuC?Z(LOzIvhb^k=B zE+Zkww}hH&ly<2JA>wuu^cOu1as7sK*34^5Cqj(2cFuRPGKzuI))3*T{oF;0qku{r zA(FQA=wAH-c;&sTV_R&$Dl4XmQT{}*0h;@yprQ=&35Z6T0%)XJ7E*v)-~V~xzL!xU z`qPJ?#L5YHN(H0=gw`h2%ZMYwyukca6>wi5hJ?{eSqP#j3|yoJ3^Fm{aO|U~Y**)F zX4m6myJA=1tO~!eqzC#5T;ed-@WHElRcpi^3r7DwcP*pepOqC|rzGM?YtlCSdmB9i zVsaxDyU<*Ipu3QW5wI-u^igN^fSv=6o6+Rq?y4vVi*C`)4P9j@7rp{c_iVAT1RT_g ze;n?cGR{WhiZFUG^234D8Qf>e%qK?zcTK_NU1^TVn!LRyNC?*SJl;2lo#^;f#hr7d z7IergFkiVgfRMSKBWb&rTJa;WZQS*=(XYV1A_^750}waj~41< zDsf#L)nM7Y06)!pnYWT^11oKuxi&R~lSg-U#rJ29rSCnwD}CNn?n7S+YYejkn^62- z-TaKwi}^b#{j0t`i7gqz-9Ccgh^I#Wc#)-2slLzNOxh9%62sUF!aY{Lb(8l zEVf^+>uRg1#ku8J%CIfUl?&s?VWF9>t!oFFm^MvJyyeAM(t)_-$mgG0h~>iqs7>iw zGWk)@A>O7Jx}H}GMOnpcUS0R2W#E}%# zOvr*^{ri&;iw-}Q&RxrQHH6Csj3%}m;P&DEo>C9Oe29x$E#1FFGqtU{a*rxKp8=GS zVGHXAa`HQg*urb5&)wvH5XYT1RikUrnr}Em!#i}R<$7&A=@9)yu%Y>v8lI0ckY%Y2 zX<5n#`H9$Ph_KEBch}?G+GiPF{zkx84JAB33w)*@G(!u0mg!Ds*jzbfk=`}+?S+Tp zO@FEEAp$!m3DwA7UI6fTcArq{!yr$qEhs~zyZM^j5S8|{wx&0AuYd> zV~^`y_;oQ#{BBw!S`3o`&#w+T;O|P!_IQygzTH`6XV*HPS7tI8L>DH!*N57#kgPFR z?Fpu9&<<8{vVU@{dL{3IKHcV?tIDIpFT-h@CwF(4T^vIO`t{nOeJZMDts>VUeRd!5 z4dW74Nk{j^ZM%nF&aQ>!C)_Me<}p2Cd-Bgpb(+@eynsUc3HYI z_)ei<{b<6YTD0UybkIvCkR22g4x!c%N}9#7@oOnk7&?)?0jTRg;uGG{>nG#C4Mf#V9=|xP9sxLujzjt;5~OEUm=oM{!F=DBY4SFZ~vo z&9G8&j{zzEbaPZL^@~8CtD7LZRm>)yPW}yO^0L-*4ZVjrux&4yiG2ZMc$7;Y8yvMc zhLs`z4j8%)xJU4d?Yc5KXC%!S$fkmPjGUh6CK9cAYGE8PlL1T@eX!pN5zIZnfFv?( zS!P9k3zH;eS0LR%*M4gLMPlHvaZj=I0sfOrJQoJX;sL?^@Of$SyUuD$gtr0!xd(gV zvgAFB%ho;<3)WFFdT@ey4RsLB>kWc-riWwd3|=CIIXl!1V*Insh`p9qgGqGN4o`ON zyfLe(2b&4mny0cGCAG_=`p}tY78r3{qUKL0`IBa7FWymppL56lGxFPhd>`c%_jB;O zD89g8VJ!a4jEo#XdIcACr2}XTZl?qT?^l8vsM<5(4v{n!>v+-`-@ur z^{4njpgRTxx-&m9)vW8qlSKqGoP^A!S70o0-)QnRD~4oO@Q!DvvyXV3&J=((G)SFo z^$m>M(H>x;*zk}x`A+DH{_|n!1YO(8qHnLnYwK9PBbiv_jUSL`Mve$0dhvh zdZLVF_9&Fgcx&yN7OUZdQzvwXem_0UXpEc}Qjzay>bCm1rWxkXb{5LK;W}WErb-l< z?U-Hka4~kcuL>vQo3!fUR0{cUhhuGyGF;q(+|BAq5dSwqtP*}XK$k#n!L&{CFI^Bc z##r)Dk}dA%O?;|#SsP2uTbWKb^{pC5cfH8MGqKm6iQS-H@SkD`G~@mUF$4i^5NZD{O+Exu-Z%MpTx}YEc}qT}z*G@^DTuQMvE0 z;1Na4bfc3?F&b;3{fF5<Tz#_WsZ&Yl9Et=t;n(+3%h1FYk%#+ z@xV%FUmEn<>SB?xCQB^rx<*0bSv4aCv`dZVVojz`DwK3a0;eKD#h`DGiM!cFTph04 zv6dTNMjKU>CiwkDLkq`LQdJ0|+=MPCEa*cau0IQ)u@1rjjWwoSYiiYOR4Mi+sf)9N zK=-qLG{+m2zP4bqX5DEretI~sH9`n;+D=($SF!$c@__^CBvMe zXN)~0i@Xk>QD03I%@TR~XzR4hWuT65z=UQax$Cs!ietJQ#6z5>8(#on#ABu5oEB2 z;J-SdTpCrVJAFBbk}DGs^Hz2NKqH?@AF6Hh)9f-9@Re!PwBHc2?w(o8Owzknuhoa5=;y1KiA zD@%1!0zyL(GhWPRqQ}dmRei$8i@zSr?WHkCxYlBVmsE@Q1u@$eq1n8K|D5wGmcVWG z0BH4HR_s%{>Pa?dwfKU3Ya#@$yKC`I{GBF{JcjU8()d&r(mljjSJ34mb@6}8n5p1( zW6B7%`k|mBhuA3!AaO)hV3tf?PWt0!3u{WryLU@{5{!0*tiR9oY+-rb>Ag;tYsql2 z?*gROJmM2R@M9G;2xsnlm>!g=QBVR{V3)hxCrx$2)lo$6hNz-^R<=O~$7er8GEs2Q)7{GGh2) zx2_Fd%kiV9iKhn@VEXcIy~FNDwl}Nu$YJVUcW)YtQIv^Py&-)^8q3NCeF;Cw z&+vi-lAHO3%g^o&z04Ixp!Jn!P~Vh{z3PY9J*OZR@Iwg>xQ$xdFh zwhMMO4J^D5rVPaFCeK+~WWg^?JZY^;VfnCnJXRkFXUU*@H3d%%`|?Eha}y~R#6*7R z#ERMf5qEJc29vfenu@ok(snrewLSjy{i=Qeq3&Q#4%YFpF9Lcw4_2$F@3e=am`VCD zKs;o`5^@Z%QW0MyfS|#+1Z;L*cp2T1)zQpBt|zWR>j^}wUqK!w==H0MGD+g*h-WQ! z{svU<$i%UqsJ={qtX%<-wg22!xLst}d+C&hvm9zSEfwpp`F<_OU113`KZS&;zS)YO^Hevg8)V@0^ z<5Tnz1l3kvuG!XO+7k;m9^YBg_AEaQJwtc=C4M8%_34F!XvdJ#>1iYNgc#wXKGa6? zO+|pvj2U(Day`eao5WLe3)ZnHXceOv*Tc7c+8A{1n_1_&nI-fJ>(>kMx_^Jl>g&6Y zu8%8JX*O!K&H)}rEF{P(g z(FLMG$_AxL&5vy~JCEcR&O+%6`Kvaa*6v>M?0sOxk-&e>lL`~xpJ<7#t;6D0LhfVP zXEh4J?G3tdd)MbwZ0jy3tfsn;qu=_T;CU1Ar}J-lwgjhmsY_+U8Avocn8V9@EsILw zk-dXKW0B;eoD<9AOYJsA)J%Q?Y)hn^2=-10DNcTOZg((BBtp()~ zb%S1Hx0HpFK>vdiZnwdiW_&NN7&qMq{w7~BBlu%))Ped=6W~QQZorZ9{zl~Mf$irP zQ?=iJDOlSp;rvGJMd`>2+TLyYD{D#N#Bv9@aZ`xbj~i1(CNoTMGVb%CogKO(I!hw(c<=f@d-TJYqzvLj|K_qKNi~-9nb=xKIfVG zb^gyGF;5?Q_qa`i^t8zg274rM>^~325uVO@r|9s5Rq>wotb%BfHm9cNGb?lOb?+Tk z=Zp|0hYUK*p+ypX+M2x|bN*hxI*T30*dah#iu@phWCW>(_hXd)I3N7+W5DXhnr?)G%l36` z46l#B7N#$_+6}?g&R~+bHf{YPw6U%ZW1FcA>~EYD4lZaOdqTZ@y>&CsliFQ~ZSo#m zsl5WyE@wgqG?#~a=Uw~hBgO1UqB>2D{LypumU&^6r3nXucd9v^uSgO{|FBtU_ zcvz+JM_EYw&4l5VZsu(wqOO32hR}B(y+u2c=Ve8Um&b@>YZ&9Yp%Y*1H_g=?AV0A9 zR-Y_KjgSWz8a~W(3mGmMi)Y|jpxM?ayo5))>H2uqh1;Vax77>#0C&sn8T92(Xj^)3 z-<(f+I6S{uG?6JPO9J2*i0cEFIEvg!mg|e+#II*jG~-S=--hI<(CdB-QY&mi9`*1) zv=PFK9UQOEpI%PxbyBHQ3sFQ-<@o(OdO)7J{wlYh!$ifBc@1qEyQZQ@?{WFCh)s@= zN?;L2)#j8Y#8af>?Cq!C;}dTII_I)^z)Omx>K_X;lLq@c?RH{hrOr*$)1UKBW2n$8 zvnOxijEQJnc=Rf|vIe}r0PbMB9mL&t*qcOcM{KvR_R*SU_gI9|8e=-N`JtW(B-ri5W2Bx<)ak4uRua>%U zI18HA+T(78Zf;sFM%IIvJLTu}g9{@$Xrb+yV769>$x{60!}f$>(l|JCf&lo8-WB^m zCM4UR#<54=tuNn`*K&U6=>wi;8v0;m{vi+EiW^Ud(7y{av$J_;ynN8RZ z#{Qf3@vK4Q>DbNGM_nmr_611%zylw=f3sO1o{&ls7`|m#Zg{7B6Vt{byA*WUaaqfA zT20<+Vhk00^x*JpPdNm&oI;?MlOz<=fyge{@}W(hEmHrc6&nc`?e7H(vJ$#|yP+|A zFC(XP4CtarB?3lL6Sr~nSk~D37w-tNv$r1WBOFj542@L35k#vokV+7q2VrQW0qUO{ zaDIJ}v^(5SNH}fXEY6&dU(3)Tv)uS72iO4jBRnuyr<4elJX--`gK79Q9Lht7iKKx@ zB>89HqBv7V=vTAMlyF9vU7Yt{$`5Ge5$5!oX2vo=BEq~h<5{F5{Lr1g2 zfIY01@aEv7#k<4Q1uoT-U)y1F(yAUn0|}QIaW^*k7+az%RLi$IBsS`B367L9;(rkqHb7i|~gLvR(4R z;LUCeP{%0=JK)U@eYa|Wct8aa4}Q-Ht-Hgd@XtK8vi8GCQrO_`CPP0(xnhIZe9inny5xCzk7Ut>fg8Nv=?2Ac2V&czaS1pJOw682aIjy}2E0P+@$F&P;p0=J>SxjfWYkV(u z;bj++s#)hul3Vsa-F655m5z;b>kRdS0BO8V_lNOUF9wKXjCV$r|MIegxGGv0y#UBM zZ(#3y1y&d5WN`|Bf2CJz>Gpp=?tgv9ACcejj5+h{SjLmX^a%lsUg|Nc5V2UuvCstx z;GGgFz#yvm4xwi1fZroc{^B%14uBGH!noRrYk*w}2#4BN>8L3Ge%3=XLrjSMbT$mm zKx$+%^}3u3+{F3=E6&dQPr|L=eTY?kI(Kn*MO1~_4D^<0n_;1hlUt+)HL%a)M1F{sb zHS*?_On>oOJe#7}UA}*x_r93OJOj+$<<_Vs+8XWDBiEd??U{KG zE1{k$Neze19zT|jpOEoL3_h;a3JApsA_H{W9Dy33%|KwU3zol9m$Fr6Ok<=`H*Xo} z1Ija-R&SGhBD9Dd6vHLON{Ecd#8i~mjqx&Pl3*@Ik!Kh3DPGkM2(W)9NoKDA#{^7R;y>J;PH}2} z^VOaW%;wAEXZj|jh>#l~tt=X@U16X-0+gD*R#OB)Go9>jIDvMBP~n!Shw0}h*K~oc z_kRhQ|69s-rvt^BMsLcB+jh+EwzbDMwM3GLzaMZF-7rF#c@93`J+SsJ)acM>5Ix>9 zTRjge1Hc&;<+ohl=uWwbHB>o+IgYueg&s^&$$xu51AWDT*9NL5!*lu@RrGUi`x#}O z8a`oMiu^y+y=PF=S=;s7fPhMrC`k|rl2Hjw5JW%)rBRWb1wka|90Vl_2nZ5+yY`=bYaCZ_sh(p8KiiRK4d^osTm$pQhRSf9-3n^;=-G>UyxdVcfOzaAfFW zoheCQ1I`ZCD0O^y?^Epkk)HRaWGk-Gz)B_)puV3C{wO-n0iaapo=XqywU{-VKLkwh zyT!tzyS)lt{7}>g{P7O@X&Df@HpD%4uf4ihi$6hL@C4FG{oS)IsczIB9Yni)Frj54 zrGUvCBfPvmY88}?00qZbT4ZuIFE1bs&rjP0iE^fuD@>mEs+!WP6T03esaZH3WC>Vf-<{+6j>-68K>Mr)wWn1%cKu70zZhAdf{8LY|b4 zH_sc!RFDnk?Wd3i=Od53-~m`QNoja+B|l^NT?@3L;bYuw1x!rvCs5}$__&W9s~L9S zO6ay^r@i;_#a%xykC?L*Ecd41lE>ATegHF#&8H>A$uL2N;Gx_byB!uCymJUP(2*nu z&D#kv)y5bOV0DslXav(Pb2~WPb3a^@UR`$|_+e~<^&~DK5AedZwLc!74X9!vQiW_K zk6xE2XMP3N|9dLBR_qS-Aq9;+a(lb)VlReW0S_4cR^!BCEjJ|tezpG?mmTe~J9 zL%;jsYG|eDssj_yEp8w1!9bz;4A@i0JZvw969EK-pn6Gx@Nfd;`ok5fhs&kmSe{7& zk+5wELY_1P?p`JGjpZCbl(Xx2<;o!2!pPP!;3a6A&MmS%6 zL|nz>m#N93y?%+7pl6QQDGV}aR)&L)mm?f#1MMI4>;m>5yRD+m_Dg4 zGV)+4%T90m5%|I9I$>ZdO4jdJ4;53gZWOQgvd)3?)DZTk#Yu=~O|oNwxuP__3H zpM~WDIke3V9vfIA*Y?@;qX2$o`Ff1BL-R$V0FGt9^GO=NyZ|;k&j+aMuJE(!5+^sl z0>LowITJBX1f;;;k5>lnv6+#|zp{N^JtP3{@_rI#`8S(RA|9srhN1Q`oQK(h>J=ra zeG0AhM_@`&M8s~6MxCQ+`p&kME%UFMrAyo+py84Zflkfje$9<29lKl}3w~&f z;UD;ExT0*S2XlMDbjG~wz-{b&JJt5>`#q6Qj%;Xb2It~p@2KQuAJ1TrRL#ngx;|fb z@b;&k_|uCkhr#{xkH0pop7qFRg(hb$HEwP0^O96UreXC6fqMFD-H>lo-HX)~`b(%n zwYId3@2pMTrMaD_3==gK>H<|l5|LW*MT{pIE|ZfC*9Pj5@GHH#=AJ$%vISv=t(LuL z5I?y(-@!>-`KNM9i3kyL*JAzjh|n*pSa@%ZDk|oGo93F_G{sXy3z)|%)64vzL+*^E z9nKX(npff6fw;~l)^`cfi?6?H85v9yZ=OMb?wg30-)40*Jh?t|A{YHvz*TTC}Smn3FY;-j;-7?$BHF-wwF=x9_upAReg=+DC%Q8qQVU6BX{EJ zEkUXI#G7ai%f|X2K671?=R%r#hQd@EdN5xv+*{yI$6l5vU0$#UY$uS|()pnW?7@+O z=3+GUmldv-F4#P3^{@T-$GinaKw0)e*;lp-?@Qr@at9nfas5Ua;(pce@LSRDQ}fj? z>13DUo+{LUy$GaDeK6=J&g#E4+B8NTuR_Da%^e{?csRdLQY0p z4K}Q4Z@j*%kxYM#*(6dTrh&JWD~_k^;IR(37iP7qV0)?jJOg$i478;>9^M!6J?|?T zq!4Sd?JU2#l^|FJ^HvQZ5a6Xppq_eFBMyq*bY9P&d;oKlNEDbn^WV~2f4y-)7LGVZ zHlr4&qKt~#O@PW`C0lk!T8CBCtdkl0c#EA!s8GRK|2#!pLz1P}6qv-(9|!wP3w8}) zO1nVRbELJ3* zc&VL!yc2fgC(*7EjA@D(7uTq6ucEOIP)gcAG^RnR4#<`tDYRkf`ULL)_8{m@Kx{VJ zgiS&i^pMnqCY)E_kXZM3u;bOSLDQk?svM|dbtT{g97i?Qn5)qo7*6#m7PIR`pb_#1 zsC2HPw0QOwYG(PHP}%5XjNV5;v5p67Xh)ibT}O^l5yP^=aBy|+s6PPUbP%VbYF?>B$bT`3U|r#Z#c1)_Nn`<@LKUqryvhO)!2kawd}EaM5 zjD>ba@^EL{r%*@0tR_V2NEwuWw_CH!llw%a>V=!lzqq(UxaQWwS z+RB-}HhCDYz~b^0Pd*q2UuC5uG59_Kk_VlDHb-=-aXb!ejA7#;tzt=-EB#LuK>Rl< zA&z|QL_bqOWcGA0AiHzsIP0N|j$u+vJBt@!pv0ggkFe$0HpS0UKd3q6YT>;14MtRM z>h3}Bu{^STvf`Wv9=sWJf}9tJlrEoT2lgVIDZSfLo^;+U+f==O$sY!996APIP1#V( z7g_euZI!iO>`!BcZcCVcR)A)5kWE2iIor82@RwNTuazw+K`Rtt;)B)Ei$M3pR^a>X z?6(MvnueAc8J@u^KFXYSNy*mis={&rE~^KQ_Zd&`n6sW2K)cuC#BsZqWh6EtGhT65 z5ZEg%&gRc#cqnIph%er0h39p^7A_NZ@0!YWuyA*L%!LMkN+T8efE=>bOH{e0 zf9{&Wm^8kH{}{RZ5$iD{`ghEi)Y1K5#_r&b-Af)G9>el9TnsR^LtK~r-GQZe&O1^j zvyOYWzsRl2kG1o6TpPGI;4f8biqd&wMK|0`@EucK9W}r1ZNti)G_9%exh%>Mg(>_B?K>^lbh4=Ob7mzBwWMyrwrr3E zLCzr}D+f#4B8HNJh*w#UtFmm}&>c^fK6zg_EQO6hQhPnGE)nmfE-n(ZC0M+R$dDfd zM&7nuTbc;R%YIW;Fwe{m%^I)kXak?WvZdD;UE1#`C-fJD;Z2$oM%WfcMZxlx9mL_M z{EH9aAkRPHOBH4FoSH07L5IN8O=k}@#eE9qYexSKK}~T}*zeGpUi33+{&6Hbx+(4* zIl`xrl)ECL1CxRXYKl7zN~9#Sp&g7u88brnEa)77o}+Mi&<_V>S9CwzQv4M(N+y!C ziE^Hly!gcIay_kz|B^t&JB81CWj?kAwW5c^edMLrVZ0bEynNSB6PMHPgX^63l*1_i z^je!#UV@b~DF{FVoP6kaopxKL)_y;m)*%+6<2RbZH|zwpmVTYx4GPZn)~bzNb&qSS zLqf0-;qae2eKaNe-uQXwcexx2y<#z5{sb&W_e?+W3yF(_O|`}&x7VOa@cD_{vG;Fs z2Xvo9`Ao=MsKlCv7QO+}tp*C9x-D>s|ZBSxOs^neq)(L`%rO$ z5#0lw3ERKUwWO)a><;xqWnD|lYjAoIMv{46J}AjGvnsfZ6&P+5hVRfR8^G$SDsOch z<%(bR{tQjMuRLBTY&jR;*rTk^@~{xBvM)YHr*vcs-oQ(59>1nz-UDb7f^O0Xc2!<$ z$hj^&t@?O9#;oN|YUqgInnvd{6?}xG-Ie;@3v52198X10Z{%T@;VE=yuW7rM?FR%&tl%o$fb;jra-RCr9RdDA*e z=1FdAqFMQY#>Mr%)(@p%3R5HcZ7ICr6aDqrQV3Qx1qeIfG;3$EOtiOJ2iR5c8f&Z| zkGWR(74=C-MMa(Q7{PfWa!e`v(n*MwN_gagx|iJd__*79gBjK24n?$HO*t=IXSpMq zE(_jzl1>w0TK!nA3mj+9#A6F*u2?leP`<9;z_w_`rD=PCNtt6q-cwe(1lDVu_jnai zi1=1=`vc40ixB&a^6`IewKL-jLj-q+E>?4wzg2kqWi*-WL=tahH*42I2X!i71d?x; zna&>e)7qJHN9!Qbz%mNfF|6X?&HXWBrzl-+ zJWnaUsL_Tc=*AX^;jwt+lzPR9()~qoyTj<4*IE3?@&O}P|;qzJ#za@DN@4w_B$}bo@y^s$hL{jas96r-z0IDwJdvIWS&7X zq+@Dx#f|}qyAkp&N)uWh_2_n@gS+LSARc5}nbc_b!vg`@AV5wt=dbsHzXH;!07uWY z^Aa#9r=6fV1M!{$x$>Ssn%KH~CvGqX@CZn+c6Ecg1znMJ)>MnwxNc}7hYX|cM_Xm` zNBe87!D*Fu2lErsu($8oh;u3pv@|B?o$tF%A3YBhILmc6W>UU*8e-A{ETZbmJlO-| zn^(BM9KAmPy7e}-$M{{}l6GTVnlTGcd-PO(%I#UzZcad89*+F(qg)9(7;%Up&xRgS zpszZzLwg*(K2a+;5r=YK*^F|MKstLaDggDl+6bCGC#LwzY`q|<*m^{uFU@oa+L>{b8AgS`P7g* zG}7u>Y`e_EeI$qto`SZv6GIQ6>;90$J(MCOc7JRV{Rd9-A89TKKs^Eg)PMKx_Z;Ii zO$(NbNg8mRb0l;OX=bz8&C1PGWdFzDj?#7~AZlv5$8bZdu?1it@v<$E!J)Ng*Q%jZ z`Hx)w4U>@@Sjtq$pjB1H6QHIX8mI{p;5*C2aluT!gR~hhzE13kqx%`@ki6I$CQFe4 z3x`{W!cHHTR@FRuj<$+;5aVK?hs)VZ7f4|y(f!6i&U9S6GGiknwYoKq30=WyQHd;l&>yl{4aqH=-u;C0<-)Fq{bNPwfI(wHM`RP<61UmnxR@ zz0l?(!PLUQigZl3Z|%)batUzVEiEPbTuUL(;^#L4A?A4Z;;uQZ*_```_Ypudf$!?m zpYkP$^OV5D&8+x+@2dRLFD20W3D7S=zYa*?|6kNEYXux0ie_l?Fq)(OO+NWoqvvLx zF5CGzr?F+C2!TSHZ*LU>s|f{E5DoqWceA;je%V+c|Gp?PZgn(@*f{wD#6LSGHUN&M z=^s;mk`icOgrN60+r$pud}Lw*LHiiU?Mn6pgT^-E41#*UHb+k0<3HGIuS*y?jPS!&Ucm?YR%D5-Dolw#p%-fIcGsNNqx1z+aOdBmxWT{;=s5PPA!w5!w&hu9ky7*pt~wZqHBB-j z5#s~U=%X=B5-i-I)=vo2fYB6*@d$p~x-A^*NGJCxB05j!IB%_&d3Dh#Nf-}H4+L?7 z%%jzDEyQqy8D_Oam z;a@v3omF_Z(W*LMMG{plGkTeNh`M2!Jck*ii0TgmwQPD+s+I-bYo7NOph#CW+ciuQ z2yR4ec)+EDG|GfVgJWxghAi!=@kbfF0KH6gT)rp+rfjZ+_QsXbUGIJ>kl!%U!ed|r z2RCln7^C!PyZvxuNxSq;T!eDgJk>=L>&A1pB4;2-f@ZFIi0j5RJEww!MpiE9!z?QC zVgUC{wsexv5&J;`cE(cl$`v(}Xij+yi-EEuP_7Nib=?vgGj(<aF>0jc{jvkLHN8Qnv?nO!l<29O$`bi<{DAK zP6OqOdQ03F_+AYCw){J84x?lcgSy9DJC5JM_rY?-ILP#m7vqmCtOUN)F9VpvZ>cMp z*5oP|ZL2y)&mRAf$UYf8LmCdTuRt?TIs=cm5Aig%p-;P|orp#lw{UC`5<~+b7p%qi zBnNVB9Y=vC{7a6E;DI=q?2hfRb#1Qi9cVUhVc250aT)yb-kTz26gqNE2cS(o_~>9| zXF&XD-C0yCs>BxH|7%RMHougj?$%ITdKV}cbez=;(c{E-rBlmT&pj;b$>T9Ub!%?v zs)=!`>;Cr2wQ?arvlYs7o!;9hOjD+9@Bs(k@pJqcHqq?I3v&By%4!UlkVx2CWTcQ; znB3_;N78pcHJaBIK`=M0BVZ0;9tS8`vtinb0Ga@5bW!+VJOgHOdAYB^D-Q6O!siSn zi%26=y6%F6eM|K_%lkEKaV^!EhQKra3MxPVfCWGQ&jF2AE#U?Sgn)(hRxeNi{GWPE zhg-`Xm2h|9)r6l-F#!6+(dxP))#@csf<}=JKb~!ca83hBRKGGv-ro%g^bHWBp^o zph!{golwm=>Fy zLtgMps#fD8LtEbfQQa=1!b4C^xuWl1W7 z6x+2U#4K1{Y^?4DEcEF3Z}Ii|gZM6}H!bvV_n=N5Wx9;8+L`?^Xkg1Tavs8@CJ(mN z%>o?S;2u!?r6skn@ zTrFYW@MaAP3k!E>2Y-}%H)s-9D}xK3q5SxQ|9I>71)nIuqxCJJUlf>;G8!&04?Hpu zR7V;cWGl{f%qb|pC|v`eYzdJ032XkL7@dwr*|h+a9s1z~Emznc2198-3LC*7sbNAZ zCQDo3D24P-#q0G~?uZ-`DNz_>^xg_!FGfW?judEY;LX_ep1`->!vQi0UZxHjVo+J} z;F+iCM-uOO{KvEcdBd+reHw3;2GZ!C)yuuA*GzFphz+og;gxMMMGL{1dTaSVnEapzoULOaUZlGBHLO2I|oA_u^m$+>a+1fYMwJO8GC1kW<~P)2q@ zyrhe`5HIOi{jge(?Iy7@l@Y2>VSX;wXz88-Kxhlb3<4HV3tAt`EK=_OLkTpI05qq` zZTIPy*R&?r@KF2}{)*AcvweHkK!WfT)J?M>Jf(YbnEy+#ceD}4=AzUdLy|OnqY z^5vhoUS&A>Hoh`skU91_Ml@NpQ@!Eea);Kum(HNlq_WUq&AwYu9pa+Z(bl0sU)gmm z<*?n|t9u6BSy%%kQk;0i*l%5YW96#9G8pD;cQ1&V#t`U^moa8EN@Dw1~M+l(5 zpKI?^k1JRjPWsR~=Sig|~Bq}Ew^r!Hn z-{T(8>UK~_2DPEFm2&_{rG=WRF#E%Y9SCI2D)d7D0u<%pZsg9EEseUeV)-S08Y(ic5%K;lG!vlQ4*e{aM z%`~565~)%yK#bGOKgG_JYrbvKKmdu66;rqsRoh!pdvWQA7aO*@>8{)(C>fYfceHz? z6(&*zKHH01xiG672wmI{Aa--v<81cKjKhfruz?t>7wu2bMm6Pyp<>!mqVV_>NZR&0 zb#6&T-(*C49IqIrM4@i}o}y5*Ej>(Zd@$bKElcn*IM*&~5$=k;4LB=LaPyK-9H#X= zI^^+oc@N5oOlcht_X}g<vZs#25W*bl^V9i=8{l3;QJTWx(|y z)Am7cZgqIEoBHv-^n(4GDDm^w9fuiNU3o%#vU@;}%%tXO7j)PEq% z2%7!_VHWmsv@0-fyD28yDFYCV|5$w^!A4$>RC&xVlN*4E;ZVC-E!uqen`6wUNxYp@ z=3e`s>iNU5E+hIhs?5yqF^|Q+RxW8IptnQbtg!VDM3~>xVGM#1fLkhh2v+1Pbpx%C zS#gNmQA$ESzzjo-q)Xnrb$7YZW-mFN-2K;!n?96_eaLf!OQX2Aktu7(&|nD&r$_q- zTf^Eblrh^ZPYEvMov9vLo)9!JUOv%0x#gfKouyp+UOL8f|I_%m5~q?wsd1L**I7Ms z`l&$i1XfelYkq`d3^wO|J_evyq}n37>lI5<@`CEIa5UF+X7Vc~Wm_klcCc9y{g?6a zBn3i(>jjr2OdF?tg}xPNZBQftcjuQ(?m)<3)`4T9{2$sUS|cGbM9_K}YF2FbD%)2Y89e954BIPukNXWDxctc0&-st#B`8JJDhE zQNgqgyEB?^scT5U%_?F3D>L}r`gq*qt=}UOlvr56SzZGVV-#vaMFqUb!P>m^;~oI* zL`Cd?iEZz|9z+O)7Y-t(eTbNG9l~F+b z=xe&$*v%3>&klhri@wCLLNbXMXP2K=5a=`#2IPfj+3N|Qp#;q7kJh=$5jq|e_0%6Y=B{g&-yPK z$@#j*bJzH0F!?VV3qf|#XEU^ao%G*1VKbTfB--Lf&#U-B?fMG^L8lIgW~OYxu6h#n zq#HY~GW2K(dGbU|)n?C3X1r zY`lAEQ(dcehn7~ETVVcJ04~O(cJQlz1SW@h21o{NN*}`TkYUOC2Fai-WfdRhQsQz+ zXTlHJ?F5Cgple?!C*h?`kE3uWqb_i+Z41L^#5!K2nhHlXS_Q97;Ie|R`h>pF9*@Rjj`WASC_{Y$=R@j zneMCq-8&_#Jx9|`t0m$DIyDAgY3c~0(}8iNy?a(c`CZUH_cZ5Jj+P(z&ngSOEG$-= z+CkED0|B~r=AErS6ZEDr)e5L%-HmC9Ko9L_iE-?{5Zt}4jl~Semg)gwc2dw;_Y}Q? z)j453f;-`ej(o+fKe^rjTtU9K&z5Xj$OVN{0oYkHhaVb$P&8b{UjW`>V83dB_!aeO zh$Inh;_QA?3p5il_y&js=@@{wGXXO6bEgg2r&FBO@ru84omSP~WjqxdVyzpgpPPna z4(=F<2^*Kr$};w^fEW5h@P|&aBK9@S z%Z_j8S)h-2RtSZ%Zt&x-n9T5{Arw2B7scE8rW`kcd zt{`W*r)jWgNI}S0FY@JURg3n@E$$g*DJe0RZJ50OXmln8c$Rwkm||WrO%@>eHUO>A zi5;GLm8WO63x9LN_UM`1|1n3F-@vjLVFCCH(1vA){b9GEcz_c`URazP2Rv$!-A{vb zE`Utxy9kmXvy;Y@h6P}NXH7c}b20BGd(o(Ep+beH8fjzD6b;3eOiM)OIm>q1m?))< zS9)syBdGFz5*5uijy%1Ix6gLb(aj_8&$&xJfq#eFy|zGX{ka{3m)yF6n%5K7Al*m; zysYqoO&zEOR1H1ajX6FP)2ITjM!WC7v6X6c{QoSQnZwL=@oY; zct!6PJ=+?Z@YoBBMk^v3i02-Cs4wa7Uh=BJN`y7-xEP184yS=~0BJ$~ZG%?@nH?aS ziSiNnVy5|D!}T2v%N}|pUKDtaXZ1cgP_1IC|5~nmcgmpy{9+BmXH`Nv#WX-0 zE*3+W=KMq3PgVSoXBnIGLMeE`6SM3q#0amCQb&TkE-dy^Q=?1P^yXk_Z_+Rca&dBb z)>{;2VdQR45RY?Xo-=-7T@B_sL0%nBbdU>cqVgCkqZomP=bhCa?cw6HJi13P;5WPN zx!bU;O0V_KOnW02)p`OG@2)SU^m_XKSx{*}sw{oInf4|a!KyYkw1>;wk1n`)S)0|m zxb(8Tjx{9~XH0>hg`>5h+av}jFjB+T_@z_#QqLZf!OcpAEoBl#EFtiXYS&g4IKbqFU{@jp-%)J?&n#M6tSQ@2>x;M$ znUpUMrw!+IusQK>+@1EpC5w^U!|hj*=aj-_dz$P(R|v@!s*0q-vF z&vQPt~-0P!AXXc)s3j8e%Glk`{7G!d)q5xQ=h#H9#EehELNB%`*qd& z(y!^DjU|WW1+~cJz^P7S80P*?%|v5oP_#+bZK(6BWa`DF@8~Nen47^GgjiPMw&grn zXjS*+T&QMFf$0Cg^b24QB?MV5KS;gMicv6$b34@%jzW{(uexGSA;7YPwZHX#|GZ9+ zIo4CK-HgsSPFZHEP}!F}ihcP!6YQRE=Ub2KLR7lC^R#00A}wIINkt9Cxdv;zyxXD; zxGgr?o{NWI{Xaxbf^?qY;~QS%H=-*$Vjhw%oEZG%sNRYO*+%VC$Z`LKIyeB=(A_ct z{=*gn(1}dVeT-@UnehOZ^KIY|0qWx$@vqe50AB_wBf(wGYl4dbku%b9In?#L&-Ljrz#z zM=JfG1L^;jBo`!DkRmrc75`CaQrTto-i*zgS1_u3KtjRDKko(iAFRp2Iq%O^89x|KGF?6BGvfjftT0y;}az!(HH3b=}f-|e;~ zlkcDwHV3MO{Y8}RaZ_>xMzdeUg=06$6(ma0<+-JX(Y1(>{llfs$g=UdeD{txt+K4sI6n>wqoDqsSi6$>(tx`ZVoe>*7|cZ;nX(EffS*2 zvgJv0LuSF5q8!sMZ;A77j^8uMvdebvTM- z3JN29e?pox5#(ws?U8O|`_bILR|Y5hJ%k_g=Hjtj<@Yzr>esiAisha6eFX&tr>S|i z*CL@Y3L#GjUaCrMpL@V}u_Vjyo1H@86{oC1S<_N9wY#M{+_W?qq-)Xc5{3<4!mEPW z1!{oc@4x%3#mJQa7Ygm&qjg3@Wf!U|xLepkf&&N>XC0<|URgT!9)HI&1p0lk>GCk` zB-mLsO`_XVdCq2FaYFECMM=){$D`GD%@4lhW9tibd44y5xJw4OTV$QP)m;lhebyDKLp9mqzh@!PiAx`014-V z%0)!w+YW~JXf|Otj+~7T7(7b*0lY%^v9ow4dY45XU$(!|vz1nV_ow&`O}yoh1h=a$ z-9E~R1In(c+u04*q2HPG7NNA3L5R_a_UCwtH`lH>*s&fI`+jTf*9LCSS$V$E=&##N zn59I_W@+GJk1qJz#qMeiE_UhGoI=vAGcd_9mR2pT$pSfnLe@WINbs;J0$2NQHW`TD z;aGY{r%Gs~CIomCT&1fyH_a>P2dC+H2Lc;jLiH8ZAW%&i`O(KRWjv-^0XPg|zNi#^ z;hfSi>Ruu)hWQ9}qh*V3OP%=-(7`U(_WG|x$she?I02bZ`@w(kB>&mG1zxv$SEQdd zlX$XjITqCZOR)+y&8Z`Ur^k{7`$VMAzkCPKWh?(LktFFBkA{X_NG3lofu@cF+zIn^ z>;zgT(A43uHeeo4n}Mho_x$?=Mm|2k!paF9!%gNr7MGl-ij=pyN}rMbkl!uF--Di1 z+xrtul#M#e%-{Tp22N#V6QF!v9Iq}kCLP8o{j^QT2#@x6tc65cBgYoVVbL)~`q7Q^ z26HyeCV9^^om^-Ae+TZssV;i9_QOvBp!oX$md@~j_^bpWOHi^Hn2-nIBnO`e(Ug+P z!Z7jr_8Qw5B*s0r`ty{>EV>kIoxd?6f3fJGn!J;utj{dH0_?i=Eey4~dV7BmBBcp4 zb^HG2v}|M}G|u9U5iUt{R59Gp5wcY_aXtg*|E^nEru#HhG_yetKRA}Z(|`d(SD>8Q zSNuvIL37ecLmu}@lL{b(|Cd}=H5~`i0lMsqbaxyGd%$AZ@ehe&BAxH;7l@2>g9V9K z-U4Q|7z6l858FSp_xhtdl`e`MP+mh=y^uyR8`E8)dDHKw=NcO$O_SYjRBkCqw2vCY=7Psi}3)i~=H z%}hdM%A|c~)8JA?u%Q1=BvUU-2>|Ay}(-}EJj-+O|+q)fu%0I#GFwC7=ub9 z!7lnM{MewY&02<~`5I9jZdMxfd`&ppC}XE`WMK;Sr!;L=a7wI+#hLhHzXi(6JEl}}M4mmg^1p&5ez@ zrecCIs&gQ%@Sv2Ue2(qeUIF-xkKA_soZYKI>RIMnwRwy93&|cus5#4cZ|DT}oB1i} z=p)pY^kn`@fTe!j+8S5qFqpaJ+vUz4c90DBrc_i3JnxMz_&|V~JPBeDfB-FET`;bq zcGk$8k1;ig;A$4aM#m@L3@xTnX)?ObZng-jf!N&L-ErI`AIQ5KTfhW0aD)MrMVur# z5&&zefm=*X5jl2w8d+WV+Up)IYo(CC7a|7JMv?77Zmn$C6+-+sRM2A*KSP?XyF!ov zvT2)mKVLYJ5qa&?-d(sk?|Ubsiq;z|2`8~hn`F)jL@=(&iq4pNO!RSHd&T0fx^yXJ zN8@ZJUfb3;DgagfbIWiHn4^X3BrbLHzTiis3Tcq)$sn*bD#!v)So&dbjm}@1&~G?! zbJG9ZlvT;i0f|EE_Fq!|ZW%_cXL<0FJ8+IaisNhe#g&KN-y=X91NE5W1w{1XJHz=! zpx(T+T3(&tNbIm3Y#&88iEms0Tv*dHTFt+_0RBF(Qum{Fx80rLc&5Gk1+HA{J;lZ^ zOc9RW62}Xye8#<5!}*-`)5*Z14Skc(30)rj@bb$;C~_nHc5;8sqT~>_oXS!BItD}G zPTf{`b#rK~azC;5orJ(sWnly)5ZK#I;&MMjS;0Gj7rgQ?;&Vc1O9)n&t2ujgvr?vu zdZ`qSaAskY8JM;}gUaZ1b0;)eU53as#qjHcfnIIQ9mVjw?)(CJSs~{Bfs#BH7T*@QoahbM<73ek&>25(4uz{gmHB)6JWRlk{hg~S&qW6)gIv)# zNk#1_qD`<#Z5hlHor}alf)6fQWJo;is!V{-I&F#IZP7OkQ4mYdV1r^9=j;5f7gT5> z&}PdIP@L-6Z0XaRF`r@dOwen=SVsm7#85*Xzv_9!;Iv=accHJiP(|LrT@a=sgu+zT zMW8N4z>+`i{M^lrY=11-g+p_V8)M;Bm==FlAnooMo_IFqtr<{YC|<jqzQ7jm{i3O5&v?}?^XSagROs1!i3NE9n#HwId{b5l^Pdc;h6FWW-AsxK)_?lNf z&6UWyC!%zuF#Kbe-J;pqXtfHNMN3SKqkv0=1!cir5$-6A0-q2^t9PhsfAjC;n#2VW zWrxPjJ`TwDb8;Er6XJ5t8Xn$gP@mKkJ@M5kZ^PNm?VOmIttiFFfD>mqfMwt=tW8(D z$j#aa?R7dFs2v6G-+XIYvu0W!(gcU`>eC2SS5{E8Xu0R4Y8~}`*f-Q)s`4R~z*Ay+A4{=dcUug3b{e`aN(QNJ0vwKQ zlDO0)kQot!CJ#S=HmuhO6uYYHv;MEnmg{DvEGSOEnGL;A;y)J+;?fOiL}v2~47h%G zL!F4G?A!!lKGtl62B`biO47PLB$sJKKCcEmRwu6AqwD{We9A^CM)`Z+UNUfCN~U-+ zXIT3MU;}5tdnk|9wLEl$1Ij$abY>}?%6CgK1r@58CMGf+DYa4d__#0nZ05P?FPzg|^%WJ2s zlerM*)UuhHlXO6-;)>BGC{;XV{VXbsSpA(kv16QQ{b`^fGjjI>%mQBHL7^&^9E51&q+Ocl?*?FVk<&aKE&jZ(P!cONsn$^?#w6OD8 zcU6nG^>-yVPJg7lJ*h6Uf2CMjYRKa-IQAiTNU*cegW8oe?KAsI z0-?a}`FgWIuk+UG-CLcv8v6SBCheq5X}HGD!6Yvfr|e>z4?G_w?ec2=(Pc2#WttGs z9cD?9__cs3X={IXcrWAIgIio8#Ts9ubzoECUKIqp|D zeU!Bk;i9MYWp1v1bM_~8Z^!O>rOLwddXG;m2Mn(a%eamAOWp??9qFhU`wX|tT;I%v zCfLA;Oq|E__YQTwb8=UB_Gc5ZL_-KZz243xsJ#@pgnA@FW|6_=d+Q8b@xIL4tEvdO zd(UrO5w2!e!4!#P+7Tp=LZ<&=2QrPRucq4|8 zD^EV9#{~;!NO|N~30R|b79ND4F7M5!7wWwmT2FkUsMMR2ocT2psGo2lHMGuw5Z2D8 ze7 z22X>wECPcGG$u&(G{0mFVZcJ{inl>i+3Nnz8i4!}moM1<(8G9ImvG>~SuRG8w_HUe zE{zM5S|nWv%xUlo&Gir`nkreW;cZwi&Re>8`|=p9{I#U)gQF7RPlmhF>8h@W&jz+E z+4sVF-%ZTU&gS9t;w55=!`4woG>qy4rI83C^LFm`$FMaI@z z43CTQQqw?@iVYOb={pNfu;FfzLs9+%%IWLTjW*e~&54;qZ=@`l*IiB6BMD1ZjPXj0 zfhcJ#5F)X-F^hY>;Qhuu4Uv(UrV34qpz3#^{InCOi<`6O>T;&xycT1I{6k~|p1h?6 zFyoImpgbk%{w%>=l+WfTrc7&@=%}BCa+!1-I?2|p{d z$&}ab(X{3(z7H@=bbeh@NH!30 z;VcIc=CZ@pxpaGy^sDC5eNL;T5fuOo#%pLLSL71!(YxLI; zvDWr+Lm$Gi>0XzGeqbe`ynT~0QC?GI_u?W>22smr_-RmPpRRhB5h#-HOAT9+5=_k= zFVFMVMdGN3TbN|>o1K|9VN-%pxZ!pu=-xN2IiQ`$ux6{?tqGF$HJ#%1DN&|x@P6_> zLj82y`2p{hJ7dLgMPh7P6Fk%dHIH0f63h*Kg3g=(EyRuX*p!>z28C%J2i1rFTSy53L#JRL>vI&sgu#VZCOWxg$72P`6-!C4yy*8E!dlwYnp$B~0 z!}R%}fcj8P@#81wg)VZ>V5zOJnr8zKRMS&mNeOt1CcMIeNjig&{rCWI``egb9Mk6IqMXf;Oed%SJ^?Kvt8a74m}TLCOhY+m^f_o6c%*D- z>|hCI09$5%(X9S;9g}8s{T8;jIaLV*2vdf(#|9QHHY<}+w(HWCM5i+$^7@K4L1uDJ zsAKNEs)93WcF-kNf5{#TutODQSd?WnLVqa6ytaM^hAv-Jvdzl6l*Vn>*H|?Z(}VU{ z3_r*%#+3Po&X|m`RtldS?Tk}pyLWX^K8s&os3>+Mve=s`;Mbjft4<6~HuFA7LC)Fr zN}cBo;3_tdJ=5JgLfV0V+F; zlWRw#@Ha~%ni}f^enm!%8lVJ*}7E4Cm-!SL4fhC^nQt zC~&9yg3y&qhkDfNJv;NG;YH#A-};?NLfGi$`tu8TBb$5|@NNv8?}`Y<-fk^B@-jQp zW_=p6hXvD$1rRD58ZclPx?OB2RoCC^m3EgTWP&bg(+OfcU#Cm1+t7n z>KXoFz9W44kVVrjWZ7=;Wp6}1MW3TW}%*D5QA5D7WBB#@>%Hg6MC8mF1tjv2DAU=NJxAh`0lO7CuvL1nqcKJx+q0@hziK?-W$E zX12P%Y;hvN-+=&m_h`5}N#d($(Q+rCSH_NX;Wsh&x}J#<`v(66SF{}sQXJ_QbS*Hb zH30DVs2K2tn2hl(tW-$EJ{9BCTg9()uuzWZd|dZye7_k<;$a~qO~Df1A_`cOK}WOH z^P|bd7}K|5p9nFD`!+LS149HbTC6)m1S3YG6y-y52}@=$@q>LlVHj`28=E0I*r_+t z2jrjUjz_SNl8eC_(C4!_A~)cEV%fdx>P(l0&+at!9*}kk_C^m3sIRi&2~&C|_IPg52ZMb8n|P1uJaQi}bobv^@zh>pwnz#%o%j4f3cHa)xJW*m zw;ndA#fYFuC2>2Wp$2`?&iO2$kLuFZ@O^`W)8%$_`a{d;(4^%X^e!P3FpiT&K4h&r!GMaWG1;|NG?P^*?O`m(Pywx+WX6s~zvv3fb zyqQ5*-b#v?MSb&+?198fF9nw|U8<+*|0PCgi$~C%e0vn;q-Zn&a1B5>kEB#a8jUudhbi9_77# zED!ew5d}K4Iqh0+Yv^S)XfWJ3Xk@+tnOaGJWIH?iT1Z$}Uf`wHS~{4w3s%;W+!LAE z>t($^ihz{>d&aMz5_C_eYYhf`v|kIamJw8!(O~{6VkvWL3#AjC9vA1^+1#ujQ@PHb z<_c=jb`fyO>Rh50+nUT!Yc&8gkg8F_HW0%7{GG6Fn{lIBz&ge9ta?uYb3%4rf$vsF z-UAvOKHlqw_~}9~GTdH@Yr-wmv1k>nZ+byII?-#PtGvq4YFh^ zlsO{Er7(O`4?IPn??buG9ky06lb~;~; zEEhgvcZi~X@O;^Fx0WTO zTrq~r?)ppj0Zr*IrW?X%;rluFLD~Zzm*0WpYMz>KIh#tSv)Uycsl%Ru7#C)MW z|6-Y00ENtpxlAvLO~G)&iNT4_*S+s_Uq8b9S}t&iOZf_E>T6t!{ zbPn*AnjUnKw)9`GYyT?mcFPH1EX4D?XtQn+!MDJ&{L}4p)U0s*`_E*$(AU@}$MWL* zsT5h>ZcsSg@i>++5(UKk2Z);{@7h0eGxp-(?B%`mw<4Rv=f5_Ik4H!Q8K<{{oll?F zSJcEX#fFW3KCk(@5JQ?#=X3r!A>2!6bxhu(_agaB<8PkPS+V=r3Q0RER#*W*zJX9K z+vfiv?k&Tr+}8flNec)l64If7grEYG6hu)J1QevC8>G7%NkI?+g$XDMh;$>}9n#(1 z-Sr>OL|tp`c+Y#zxz2UIZCU%;dr-IX|xIynbk#w1AV=F{Xb|coBFqN-%sCSFG9O5<&*{zwL;L1B1 z$&gP&FPxCZc^K|ttb6T1lWnTY8ss{cVS#CsusiiMs}YN za!1ZggcA}-8`bP2lHH3U?$8i`4l*2w=F}hajaV`Jp~iw2bJt;V?g>HC{rg4{U-^1k z=qllv=pZ1*R%yCz;B#Gn)39g5*0d(*F6JfHkco z&TkR!X(GvJ0O&aJK6JKKczgXUaZ;uvYf_bsV-+-F76m5EKRdD3hSZu zIM{xe9^13!gX-)g~<(uz|XmS8xvs0HiSmd*O z%d#FndPeKH(T!poFlrA zO!b41&CrVkLuU+>K0ZUWjh zUdtJ!5*bcydfY)vA$*eu8+)V~E@y##4os-c5=|{O-9IspH)gH}DUz*k_^R)NZab9x zTpT>Ii}6E@SJ$<``jX%Q@N0mj?3QSfCEDKZFJc6-{4(AoAeIJ+O-N-}UBH!p3pU^D z-m^0x<$(0`jKZU++{f)Z- z{Wjh)>W$rr&J$QLqc>)rSz2k`yhZOE^I<>a)5fPDx6oQ)t_rwletzgaZBAVhH~c%c zvi`M8I#aK$zy`%R5f@YsyLj-3Atzad-#Qme+`#iVC6x`jrgdzx!9N-C;{%hJCI{!hg$9c+Y8bKnsE8D+!wJO3vcGJv>1Y-qX6mGQZ)Mc`?%IS3YpFVF%E708 zjQ;8LI#83fA>ocz>rjiZ0cn9{#xHfUaAvhmq>dL*F?o=K>vL-n4bv4LhKm@q7t6@* z>8B2_0^oS*2}|_M4LBQ>iM!Y(dB=~G{LypWo~E;x#xGW1IgXYX)D6{&0SRHH z>?ER)1IC>u?>-*}=IvS9g+BlV1Y#YSx5j(?I>r~00_rf^OiMLCLb?=oHtjK@lta-XO?Xd1zq zyuVT3vU07=uOTj6twy?m@YpbQuZXnzde>SA6B6-qX~@Uz%!hweomIB2++AdJcmUkY zW%>1&PY6g>iRso5&KKtMZs#r379-!_3P4|iOWI@4*xnAdZj30Sw1ZvUTxrrSqPW+t zwJWrF!}DWdXAy9JD9j7WHPR;9QGVvHlr72Y_qX2XP6u2eFx5Qq!%r_ z1>h!ALDSu13SR=iO-6jpvFy5m7CB1yNE!nnhLx3R4)JZ8HDQi70$2{|giEX$q^KF3WRzmm=bU&eGog z9fDqEgSiRtkxYCH^kNo;iMhBp)cM%J+}x6!&kK|#@3@jYfit2eX<+Vs4EUoVc=B~k zMW!7#X?=|xc^N2|>^@F47E-t?>G&r(q&6q>8hRRH{y@zFXzTA&*ym=lHvHCm4*Is9 z50_Vnivo}6AwhFsPR_LJo~#?&y?LU{<6Hnt@70%=AI}M&m4+E)m5m*r6*;6?Je=Gm zS6({IJ6#)4(H6K?yqL6-yRFm>K-kDU9bQ&%%me64QsNSMRHhwA=X=wbi)i|M}L%fQ}a(8R}`KFqiY`NPk*potPBG)!6 zPyG^f3^%fR9|io3V!)1_Z;$&mp({mj6i8=W4JN)}dbg!;mzSW=`FL9ia6Nt}qYV#t zmd=L3mv&{VXtZZ@_dDJLpIAOFX308*wPfflr|0_1`!>Q)OB^%YbaLwDAJJ1h2)+m1 zxQ}-234L#RpTm*^Rmo?U(tAtAU{@XN92_Ldf@n!4Cm3>T_xdlCjeDNY#tB2n**IMR znvECX`uh5xK@4+ywX!%K+2+lL@9OLMR99Uelv?HVj5x;>;aPV$-f63{CXW1d`v!lN znwp2SoZ*k9jnlzYb%R}uU<&3a0-S6*)*Qk3R2FO?f@H`tGhoYrz|!DuPK2l_A{^gWL>>zi`1F-r^*LO#5rz&6(Q+Iak# zAPZ#rnM+lwV9PSLC;NgX3p-Cfu%p(_5U+t8$9WC%*X#@x3X!rpnwtwPoVf{x=NyB> z=Hw`A`U{4uptAy96SSQ0*&2ocI}4y0x-Q4@$H^}2u?Te60nMd=JYOHRPIWeO4XafO z5%P!U!z)7i&0AJ13XxLLYnjz zXrNq?waY5e`O(25Fp|l3v$sza?-Qqd`T`PD@OjB$?^SK0cxZA<`uK89OdgUG@@@IY z>HR09AW*^2NbxVYaezy^?&!P`R)DHILRRSc!uJ{^BE{d}BnM?T3S^$HhoZdlxiua) z*&VgV5-j;I6ygZ18?^~ZA%69hr2}ECod_J{uSG8o)LPw2>VBsE_q#ohxv->6YQ-i_ z{iz+yfXAoHu6%4u8->!#zm3~bkmS=GZ3VENUtH@g31uGXr-Ho;H0ye_r(Pbsm}C1P zcp1-u{tiCuA|Ina$(`H_x>?1FTuX2RloQVE6VQObVP|JY^HS9QNh;1r%KdrDYFP^O z^14S(7QOxi$kvRmKpqp+g)EER$=9!zr;tMvS!Y8b#k~~L-(;cslBYG%+@mB5U#3BM!IvXM|J&p@eceEz@^ zyT2au;5NAWLiss=CVlI+fMR+7%bNaOU3Zi9>(Uh!&q}nxuJeJ^b!QmGKRG|N0uI0W zmZ-?7x)Y2hCL%`XM4^M!-Q@}L@+&ROZnv3hDOg7zHb7!$yk_3_6`B_f#G6h+PhyZ*wCXU}JF zixUOS>TY&S2!5eUda>j#J6js?DY_+Iwhw?8e3_2c8YdIi5U*x$jjMBV)jJjHy|e2p zppV8g&R2JFwU{^_b`JG*WrK~9(7eT%xA-=f1sgkCpR&e+<>NS$gqE{VUH*pdMjbK0 zK5ip_=R~i`SMWX=7R+Z^B471`qT4s2VuRab@fcAzJHt*fyLn79Oih;??;JP!J71lk zAx7>av8uZxYy3b=~X#QQjGk-Xsd9Z&1fXK}~# zqg=t;wzQeC{$K2JY!(MQ7G{kuDZJXU$&T(egr3|+-iJ%H>=Dt7AeYsnEDX5T6fAbl zO;<)>yOlpo@$)^BK=xA)C88)`+YhoIUM<{6fuPBaE1G|UCQCJ+9Df7hjPEt&&Kbhn zl&sH&9Y0v4?MaO9M6(L@uC=}=OwFofO3a&(#xeK>FBLcz)=>e6gc(#o)Y=eG&CSR0 zF1eKtqfb5d&Q03!Zh$jXGxL9V^jQM?-Z`MCANv_ssFC^d0plv>cv56cYsG+I5(%J+ zqJsCiju7JI3Q*E+Df?hn(+}G>zDA74#VHFZw&oI!m-+7A4PR3$eTlQ)UQ4j9=q|cv zYS{n7lX3tsxdPo`@|H1rtC;3?c6$qZV`qCJgVURxyFqLViA&FaFkMuraGRF3I1QG; zIU&?)D1)f{&1aK0wTkc;KDu@o$;ahQ-Tan!T9?dYX=7s($l*CIM_qVfu}0d&eS7fF z%^JEN-)7PkxMnyt-u@E>Hq*7xyv7T-A2X$j*;ilthPzd`haUnix3BnM(7W?96I`LX zwO0r6aOpaG<04D?yPihyeJ4^deBi-7!*xp6{K9% zm4boZW_qHA@lU4s=W79Z4JWRBVAtxjdTk zbzl||s#m%UH~b?>Mm@K|9t_N1iBO_cyaCq8PFTRM-=X~CYk z_IJIwoe$rzlZ^~~TRQC#3nFwnVEOaZ8o?j(V&8O4v-nGZ z9hkGV62Hw5IXVAg1i5QHtz(>CHpRN)qkSG-Bxv(-@i&qt_~-H z(eqXTt)gje(`Q$_S?!b2r_4y5&L9LVfk?&=Jc4PYh$sU}-PbnuGZE|C*{~=ahg6Zd zcZ?tO^mp95d_f+)5AIuG`I@{K6UBhHuSa30KN23oz3Tbo-u2f3$QZ=3pI`Z0;uXw= zr#{SJ;Hn`qAo_AH8hQfKdO+!FMP2tL%#UV-yYmDfns;RIvBsQf*m1F$d0{p8uW79b z$td{zp^dBZ*5n$%s3x3*$ns>2+|kDaq7dfgp4JtY*y7?=iH-w|1NNTdVagYkyFJ5co(l$E?<<+A?5HweaY`x zkzme!eT5BUn&Nr01~h1OstFe}m%z{e6tIu`zTkr+uEE;Q*b!CD^DD;a>Xw!8(*0PtKgXz}^PoS(4TsjK!bGRHYgzgH@o_Ga-4Xo>9r?sS zE|BhEoVdGqXF<|H6k40W^Gpn$XGe(qe@Xz&*EW;y->lYle8vq-=^Pn;TF3k}V=^9$ zV8Sp!A9@u!f@(>i^~IP2%~t)ti3iMI#Dgdt@=5(G@sPRfqdU}bf}l%+FIMO5g1VDl3Igr&3tltDH`zu z_h1k{+l2=9YMs+99*gmk2+$~=Ha+J*!84K@7iy<^53?pQtk=$Yc`vsL|GsXPU2PIuUcNi6G-YAv% z@gc-@Io-_e;O!!9QkPQE*0;RF$SngBCGm}Z-bK(0?O8ObKNa7o1B7tN*FdQ+%dXX( zgLZs!QKW^ep$3`C6xhqgXzKTP@x-yP1V98IkPAZEE_26$fs2R}d4fJj9=Q9{*NEM$ zWo>yl4xx@X`=XyQv@-vV2KT20@`e`-uSfOsJKl;Llg5HwtriYw&`oe!P$YDtj{Vz^ zCW9Q(Wb;)&DY}*JINlC&Zf5VEd{1gf0K2EGprBANA5Wovlvc5qi!F3vdq0}(_Yk1`&U>Xj; zfZnPf{*y9A26wOM*N6}&>FnUQiQvZKIjO zuV|ZG2V7x@Wzn3>B@Ehg!a0uVgRF@Kghd!iaa%pk#A5^Q#u`=3+ewLh*jeE}Fhb`V zy(rTcCf=K;W`%+j`h+z}%;5vIgyZ|X+#S44I7{nIHzyW!QMiaV6(eOWI?=n9Q>o1l zZ%L=tnon%h2Y|0a5y9p)h_59Temy@cbmslzvz<6;6s#dngZJ*T<|lZvjLpgZ%s+2D z@>Rr;(3pD%hz2biCHJ5o53XK(mLS+Y5mnGPEjHXWy6@j`aPXr-Jw7Zy5cRvp(S{;y zY|#QJF%K6Hr=o$e>4wQm2j+uE5vkq%!&O6*He^R{omtw(o{KN=xAN&kLfe$X;|&r{ zRbT5~{aoHhws%Z^YTXSl4fUo0Uv^J{X}UVq?Q+dVCSFKP{{^9t>RaYu| zNU2Fn@wrH*b$#$fn`ypO24{5I%gC2;MdI_yX7pl*Iy{|Tl9P|^K<^+Ten&?o;X&NV zayJdomE9Xoc;IV+e)ybf`U6nlq09xJ(uxuHX|7`4I-oOA5qhSF#Ei#>Zlq?ju+!NZ zPF=XY`EaSWYV+K-Zmb)7l9WSFDk6~?bUV_VOeJEICQ3*g6s0ufN| zD}2PDI07h`_3*LtJ)ri#+-7>aHhk_n`B@UtSSA9eLdMEun9<;H7zVh*|3_-DX5xh> z>sEIyz%Dm|AL{5AL-^7a2#csDV|>zi{zfWnAqF+J5G;PE`10L||9F!F!>bW5F;W|% zc7~Epbooc=5v_We^NwraC@3`GJ8nD(1B>(8u!x%gP-@LYs%Ln3#H*b>FYPsadC$Q+ z*`~WUhxkdoXY8Uj=?R_#E%agU4NRr+IUWU(w3{4h1;UTE2A^6o(7TDb9v|$cl?q=z zxE09ZEkEuwh)$E~fnANIoq~=4((hR?|g@=Ev%f$H85Hh=!`)N*c|T>2gB;G(49G;%@co z5SpL8zI4NsQ&Fp7Uh|IBzikhejp_hDaRTiJNK`Sb_9X@L{pL#izGqSgB~4mLSos`} zSb{6?_O?YeGglF7=YNpV5>m8YG7ST%E0h>KxHqJXNR^K;vXJVXS9Z2lxk`=9lfKRC z5P$st2|);WLDLBJvz)Z!z^z4r7j^{>f+Y9I8@k-sDhPy60|f={i$;h#-Gp-=+^5`2 zw%Ml1Q`YtxpxQ*&g;erEnXk3`P8JUxJ$j1zq;oU%Oq^V4$4=2rX8Q^X3V>2~dm+rt z#fPH=Ib;cBU3Rv2H*ws!YeGn40Nu=Z?!rzRs6cYKFdz}yrQh8fLifP(KCrd5_Nb2F zeG=s#C=B}wI>NW-b*$u;Fczd#AmLs9&khjO1b}GMGI@(#%3;Og?p113kyof0MhX2FQAk zz7PPb4W}|r#NunjmLxMU3`h(#@v$BCV)xA( zAlLMY6!K@nT;#q#V0Ds|4ga?rK2p|YO>QlHh+>(&e8Ftyc(Q7@0N_T{OWG)RRvqr% z{alawo__O$0Gh4(v#puvcVbXWw%PoJ3F5-3jk^;IoExqofoeKkQe3%itZLGAH^-42 z^J^-qyN?#q-}3PA6vR&94rWx6-d_!mbsU##sa4Jrz5mABinxpQUSz>2#Qw@VJ;x&0JEy~3?eUI|@R zC>+3rmjP={*ZG1Q{X=bI{Rux2=&`Vg5@X?%ZxVU;OmR1OwNCQ*Hi>TIb(+yI;m-jXmHW$`k5CLWjs`Qkw|c&&R~^>Yj^E0Eh~BSf(9l<*r}6z* zQtLBMr8{XFApV;j3AN|oobYR)a%8&XF_QbQt0lw=*kYOd1AF=V)+@<=qR~JS$?W@3 z#+D&vMMCI z!KAt-cgRT$G(yjoz4wM*JefEj(UiMh+yaUni$ca9Z@5Yy9M9JF1f5+oJyG5U655;V zLr!e>0A&s?U6fPDqutfR!`+-*!`TGNq7yfZrVsY}gvVk+#B*ym5~j;(uV!N*=VWEu zC+o4bxk-@NZo@q9^wDJP%k=a}?STE4Z6{pb0x^Ry!Ca4`&B04?pVW^4@(ak{{v~PC z>iuxgOhIosO9{AN0DgeUj4az|;0S13kKrlH7=!bj5$`-fCk|rqqCFB?1SSe>PU<1*6|Bna*KA@2oy*j$~ zlSB>(>?TXJ+C~*-GBK&Nh}k@1Bvjb`kPg>5m0s$$`sh-Mk2`R>w<-QetX_T!QsM&? zNF0luBR>2&jqIrG-v1RKFo z9rW1oeQW-mtP0|Yme0)brims3q@Vu+LHPh;x)|@BK}iB6ezPx>5%jrwPdu3px0rrO zKXeZ~T^50As{&F6>GVOzb<8O*2hrevc4fiLc0{jes^3O<#AHY;wc!d zmdwt2nZ$7HPkd$P5_IU?sO8Ih&vuOThN>UAq=h!rg>0(-;}gM47U3uw1koFsaYhjR4U?DT)V*9 zgVUHL5zY1srG6}4_!^wX^SM1+@VPd|@i$`fJ6A=i;c6e1UrXyqbg67c;-@wUrJu3< z&w{@E(o0}LySko17DX#CO^3(4cc=YmaNUQf&$LmtsmL}> zCN{j%!1vv1FW)Z+R+KSm8tfDA>u-6D^@<{Ip;G_$T_k--0Ess)u{f1H#9AyUa1O%r z*2OK{Bi-4M7ZrNwa|Uh#64l2?efsNNmk!)3IZg`zGhsBi+>QC0oRIb-)z@dHV1n#v zKFf0FZl$6$9_5GkK_jm$0gw7nMON&vN|5zsCU&v40WLMr=c$9dn$vDbB_vH=u(VRZ z;zI&f)++bb0sy2Pre1o;&g(mAy`9$gu4a&)?C#_T5-KcA)8o`*aSaOL%e^Ry6c}1* zc&{1uPeAP-pAOQ}z+>PWrL~foIcyMKhSyAGlFkLaxBC1IKeaMdN_M+Y6JQ; zY^+~LD%RtHV%_;2FrE7g)}Su+V}Nc>R#D z2AMu77^0qm@NslXulSR%GJ3w+&W85@j4|weeq{4%n|QZnY#yOmm7)9Cx8C;NVLfI> zs~euE?%!p=uDU1*860AcH1%6^nP*?Ck661iWqT0u`=|ZQZWur7twH4;$AsuU0qKaS zL0Z;WN&8%T=6|6|7csxd^cpMHeMXNhxIV7xFNy?~K{ZrqAWs9RhN4Mgg!XSCkbCQa zpZl9%=2zqodj5{klrC1Ng&49WbwZ3_plWX%H^S24q4N%3EvM;kS)cO>>94096$|kc zKjv=#L=?zMWL*`K`>h2?pi*?(zmOC~cX2<7H*eRxwFmF;vK=ew+=^Y&LfUbRAPH?D z!(HREOK&k6__16g$1Lto(gDli&QjlI28}BsmZ~x$hV}A6ah0L5#p$yc(Z6xhEgo)85UIDH16Q~9_N|xUgG*0> z?M}qUolLmiqQLgU^xJ2q`P@`0z)fv?4Vg>q*TL{5V?b02T+aM&eo5Zd!CE9>5s4pD z2h=<`K%6B!4LW5MOz6dm0>Ugzjqs&_f^q@)lKL@Y`@#@kkaVKK%Fpac)M9Yw3x`nl#uGhzmD+oL{z?~bWV=rIlKV0Q?>wJKq?q9z>4XSe8*$n$MYtsm4k;+h>};tSOf*#cgtItKV@DoU%ouX5Tuno&`>_ zEreqfIOvMPrOU=XNQ?$DHEaS8@gLt|u6DSUNh<_ft!ak6Y~yHaon7CIKJ|9ye2ei- zJa!!nSbODXrDy9KQ>2DDKP4(aQd2$sg`rQomlJv2#F!aehO+67-a)S}-SwQzl`h7d zK<7Y`XD>3E)vi$FsEmA@3YeClIXiqa1wdzLAF}8f(o2FPrR}d(-%10`4l_j(M?C~L z^L=n{?Rm_R$9~^NwOSk6ElG)lHf*{9jUWc1Xz5%S=B!m9 z*Z2Gktshv3fKmr&q4joRD5visk22POJIc+biuqI{?U$<#iBI37Xs@no8VmSdzre{g z@JEhLv{jX1%3pD1;NE&oKm3hYVr3bApI_7!PaBwPthvkssSaQ&q_U2ceX^|5Sy}F= zghTaD%Lz*>?r`{Hu6{@qkpKCQyHH$`%hbzZ@;~`bSb+c<={rFhl-lt7V2}jR{M>e8 z0eXHUt|V+7av*;e(}+!faRr8RYFY%p&lG_#I^l*Nup=|I1%Kzg zkrb!e0TpThW~Rt44;&N6-jteZz*8X@aIbF5gNbt$7 z7Wy}2?xzI8oVqMks%Z_%Kwuxd$ycxoSA4-`ED{4|H$6>{9-DmqSsXfq720Y zZTQ)!`V+oG4uObh)=MekgD+;}`uHr|E(v@J6uLXN&ULgKvAq-`|E@#HKTw3%6PhukrXpTXV0}Bu( z=bTXk2Y+gR<~H-VhVoBxU?q&?s3xn9{*Gp%C?kyMxdt`%sFG-Czq;Vn!h`L!?Q1ky zX=-vk)2d)(O^(li6kP4fsG0eMfBh!@ojHsGitD<54)yY;)+2#8Lk$PDKM?b+R(q?E zRkWD*!Y6gADIkMAJcyhhxGejq3^0T3sRq>SulfSlYIO(AhWbLTg@7ab_=Z!795hM^ z8eQwkC@9PpCQxn$1{Ma(&6Q$1p8dg#>W{mSxN>E!3vyacmURAR0fZiUUmVcg^|ig+ z;H$t++~Cz%#uLr}JpEb5?k5Z0MP+4aBlA60;6h=x%-NtEossF6MytuX?+1SEj-CEBUsDPYbQ zK07EaLvv?(xau~n1j_Y*3jLW&TFzN$=^I+%XCom@j1Xt9dO`UyzNSD<;bNbGNYBY` z|K6Z0acIHwqvq!3;tWs)k>Jya)y-|+@55V^7c~CB)+{<^kQTCkM&dRk0mbVIvs^u} z1+E#KL9vo4f}h6?)cx zyn)POS#8fKj?_q36lYX$dD#B96yCcK+3z+Wn;)D(fc;w6{NJ<<@%EGa=6var3&}VK8iiVr*X#kabM72HbeEm2WYa2xgI1$GCox=t?qWP z(?>v9E?G_-DjnTNo&Qbv=C^d-jORz(o`X9P1P-W34V~Tw$pi3w4pfovw3FQ3t^2gZ zcp~Y2GvPht1jSoV``Ii{BMl^x;5{{s9!IP6>VG^AB z)B+`@4srnkRivj?yjF62Ho{9L^$#-u8QCF8%ZHrRjLHiyUvc3#*~`#EmR?-zNqicz zPkI-gT>8#($o=7E#RpPP%#IyBI*%dkV%P3|?-YBj)S&COi=;AAdn!2buo~o*y}tEJ zq(~^}&`{0&0aU7a|I;-T6a=CTZ@8G<8E@NTnIWx8_OE*`ujs2_iY?87`zbl3z6cQz z_V3z`+<%=ut5 zkLAAcVRbSqdV9?h)KQ{~|QD8!YBKJ%?j`Psnz zlxDTCBBgW=4l^4v^e}>N;5~&{48++DAf$AuO2H)eH!1tCQ#P$|2NZ6=pWNimZs7#!J-aQJw8dD zj$J6z*^dKaauRaUmI$yXfSDHMEb24gXf?MGNwg^+$Q~l|HIf0HOAAv(8(&s$#3F^N z`&y{=I79{D9(MZlzc2`$kJu(8I`l_Hr?L(hNdzF4oL=1?N@z$?vT@KZ6EBZ^Rlheq zEy~{3KF~U$Ih1c8NCKmlwAVT3cRE(imW){~-2i*KHJAyS@Sq5d!LHD;qjxTQ!aCB( zBTBMC!msMP{j{9S>Ch&FYI=_1*;vEO&d$xETXlW<`&y2mUk4@o3PmNBClppW0YG8Q zFY5{=zKuMDp79?=4rGvB;iPr;CIw7myYsl)y zOa(145O?Q7Ic;ERl})iwnV8jEFd{m-QyB!}4TFQMl&vn+$}LPT#2}0k_9Qg8X&@qR zxaaRg9@bE%x9dLA;%Ayz8%(?wKkr#FJcuW873bbnXC#~mBB@OJ!d}3epBpPm{mHf2 zzQbqTJB^KJ+}uj+C&#do-2kj(zhg_zRZL83)PyN0Waipl>mYBH3AcvZ1mcH;RB{YH z0VRf}V?Ba}oPrmi{kng@#HCXA>r^^0Jd7#@!Xtp4=7Mh z50Fvfhn9dfx-;FcqhN=2BNhZL?3k8GjhH%1ENOsid4$)U{V~C$_{l2}3Ljdg2hMOK zZ(y&$)s3$Hm)zSX}=L^p{ZtDTkqOe3~1a znK%C}#mwT2{0%73Ft2AZ4>C>v;E}w^?R+((h!wS%n@t_?)BvH~!}J^W^IxNdhIA z?`B{8R=;LjdGc|$KJiOyx#=e;O$~&Q1WDf7C+t>&WOit`0@*gW-rDdHGfCcRrB$Hq z<;W+geqB$)IzHGsf^`4_U=vZ-VCpJ}FdD?3Fv%w)*0GUfu)tjEunMtEH7sofSMS=< z8hCMm*Ism*LvEHqpU&+ij{QjBz@vae;c0(lK$*hk&p=FNm50Z*UCQg*My@@9w`~SU zFFTO#SU5Nwl)cZUB!ZFF?;l=Ne_9O#4kElG5ij-e2SBNwcTUi{0w@0EarJI2n4fgJ znyc6eCAfa8u}?5EPCctTK%@L+1&)7kHrwxdk`hB9;oFzL)|-Kyl!K=#!K2fQNL{3e z%3a{UR**JdrNg&;4A$EOzWz$Sv}j&G_Sje@i5JFlM7B3QvKIJqlvY{kC{H;{od+LJwR zzopz;qZ;&sz53y1er(HU!h@%GnD3W^L_jWnBUgt}&Yh z6DXyquxGnBRCbmHTN@j5Ia~w&Y+92$j*aqXpJbU#Et&*3%twvd33~UUibgj8YH z9=%s(qwT1gyP0o*(#F=L#P;g#=%vw1C4PS3!3MU@m6e!>2kGPOcUOB9d4!D7beP~5 zVh3sWx#5F>`bgx76DfgZN@}xATc5%Q_%UU{vNg!B8hxZ%ua{B`G7Yh}m%*hBv3c)$ zkoq_y1~LeF0%RUb=Q&uOa#B*azp}*x4Lc&D*rd)S-Ss=HlTSK=3aN{3YsX&8PJyD-{0lWOZ9iG=PUvaXH^ zSrT%>fANrQ#VZ=f5Mv^u=mZ}qN6p%I?x-aQ%Ub+Xh!?At&n=0O zLwm)7H~hCVRPe`B3$0q7*#P!umJTjryN;2afq_;p61q=|QHDvW#;$>t&A4h=#Fz}ij-`x23?Zi9P+}iuW!JgA=rbOJ{-`SKJ;VaN zE-)mA4tO&pT4wJVeBZ4vEMSxK#D5Z@^?=_$MQ113FasR!;)5u^oL|u@>0(wP*ZhoV z?9k>XhDY%&L!h8LbJICHG;`j90I$pS?Uay6E~HfKDofqvn33(kdgDSx%_ix7olbgN7JvLh%)a;?sk`b zRoQqRKc$432XZMR5`SStXQeE25APsP>%ZRW3vL;AxIu22`KvKsBif`33eEwWzXK^L z)1Q+vK!KFP(zlWZCyD;uK=eyN?_3m>iShR4hXKgC zFlK~xaKP9#2EJRGZTw1WX;Bqb7>DaOJP<)wt%j>5xXoH&X7Yz0xFL8K_N;LsP=OiZ z@2KVFPdp1&?zwz>3qcb(;0A^Q5|I0?%-X?Dc`wGojkX}EseW6~RLa>29$4eD)kRt{ zr4(t^hae$?3J7KZM{5DBf1mjxXU8Fqr!)Q}5s(6WWq@D*b2PJ1GuH(&G4Z@!v5I5K zfBoo+wFFAct`3!5%*2bG#=d$r!0t`qC4A`4X^fj!%c}k!lhWw6W69`E6-txr1GF-GWp+r zz<*Pir$ctYmi|{#BPkQ8J)re&W-|b^-nB55CK$3GNMOn9(Db zUft0Yq%+hejmiEaT8bD}FO5(YkZcV_Y+ZwOt6AIxYKH0Bz(^^{9t{BfkDX!zHDkTo z$rAs(mUwobR#zUcZm7M>{O?dB?tC9XdgzmW)qPvAdJlwR=$q&qKE(aJ=L^Vp{;BEW z=H>&GN5_M2)B+CelO#F*dFv9~qg*=-uW*8^=@7u)02e*k<~MJUg8@ti4}gr3BaEGJ zadN?5a(cD3A8wwS0f89GfP`m*<|zNh95xIc&-wbRu2He~h(=twvZ0Af*`OsD5q_O^B2= zjgCMcmK5p_K7eEwKxOxd5zdx8Ln8C|co3Y^Iy(y} zV(J`2LIzKU(r+iS_;Sx*O3}bZ!u_w1ceg0WC2&yzAr$G1nK^hnYPA%=rIhei_DbeW z6vgsR8j)e8XeP?yT#E+EPhJUKR~eWmI*Y*m%xQNttK+rh_;Z28ZfrkkT->OyX( z$1|%ZN5u_U`Bi7#o*5nENtC8&Aow|xhU*sfA@O*@xp3wKoC@Cg7IqaiOspt5YXn_*=bEA-c`v@!>9O{g@Zw_(h*5vj1Knb;cVhRJ)4vPyD z(yV*GO&3r*;~)U13!L0cJi!bmJ~2LCo0)l4M3>C&woxWt{szV0y?-YRw*Zb{@KM%B zj=|)Wv z`q?T4<=h0hpl4wYT$=Y>B>g*8tavnK`5C9S)kg8qp6ki==(}TqTU)1TD*z%M&Qm_H z2i(jM5W4_w2GS8ziMLE1)4fq>J8tOL(;esuebT!caxENU-9FD;3JggXyIR_yN2e#`?9JA{)j|AMEMv^ldu4|p4{J~Al`QrPgt3WBOaC?PD#5L^EE$NnNe zFN~JnlHPLTZQz3LS??}bN5qb*?~+ukFat?MoZIOP<_qGT!?%lhQZ)e9cl(x>?|ZCK zsP67nLscZ7Oic$P(EVgI$?zBO->vfyDtWv8se~CLl8No64Ohr^W zgX(CqseW1Pw-x{q{6M1dl-zjgL2{k22Qs`(!-U8$4^9;EIwQur|IQZ)RnbhmH(J1E z`yB-F0-LRdsmRYt!}sRZ*&wac^CEqX7i7|lI#+-O2{T_X?h~2ClU~9|V*E{zgUIrw z5LrGDEP}4E&*i03**HV;KPkKsCwoA92=5n6!ioQTZp`Z3xl>Pow>IgAuk5K-JUedY zZEkO5bc}#Xn5y_+VNig2%L!WD`%ak~_&;6MB$LRiTACrUg9@_2ZNE?}b<}k=#^?Eb zcVI{xg)ke$h1Hn6-2>^5!+7JqG^eleVNdTc3>jtlM;2dQKVlQ#JH4i!4UdP+TGB6| zowT05G;m}PqaaB=SJBb!g2GY;6(ZCwGIF=|f=cD&66YBXuKJe=+*hy8?^ihgMh!?9 zN`#l%7vP0bQ-FC)MgeG<*ugF;NX8c-ITH(lNJeTI4W2Q^<(z=fdYve}b^i z39pgMhEgrIHM;geE17}n2Q|P{0bBJL_MB(ou7&v*)?i@NJrv?)0+Qid&3rd>ZF%cR zlbP~AY6r>o7eRuxyxNB)k@-PeMz{V!y-^mXp#&%MeHQt~!YNO!#=NoKuf%CWLRfg0 zJg4?O;TEvpA@$zSXZi#9dxs6i#N09oV5fCm+!G2mQGsIHLKWRxQczYT)o} z#5*-9VtrLnkP?Y%?tSj!slT#!$nlsm1i$-A$#u`20yJgGK%=wfz6?vJIib4+p<7#4 ze0mAC{YY7KJ4f%0nOmug z(^4?!K2cRh`~1wew`*A{<7jBHQv+ikjA?=b&m4Q z4fiZfWcKm!Uo0Nc-{ju?>RyF>Ij87<3;FB{akmbyAcrEH8Jn4Zto>KS=q^PBixHGk zg#?U1U}P=yvjK6vE@cV{y#FZyu->L{_Hb89+@B|lgPq-2)1C+BkuXns^4u(Ss2;?> z-CV;ozYcf+pk%XK8!JDfmk|HF{16KKyTD4W?rE8kp09dFl_2O@T5To;9pPe<+{Y`~ z>iL%*=^7cQDGhhrKLy1HT}je$YoMgrJ)_{5mC?ENHAhG?p8J?Tqzi`)_y>)9p&LKY zF`~e6n-xT@SX$Yh;oC6+bii9#+cN))aD{8XW}b*tDe#Zd`9cT`^kW@%HbHCug&Pwd zwh>tJ0`?Yy7xzr*h5JFe$!H~Cg9uS%4W-jlEWY(UJXTjc_yLg?sRm~~c~a*0It~#aus^_4@VI zvma7fB5?-6+-0NNojV37Dmn9}ITgS>ZL~ByJKMYTY2~g(W!i=mg@KpzGu5dsa*EyR zM@+H5BKv^6-xG>I7o zihtPawtRGvshd#p)8or8@ofuzESu)!Nd88?DcKeJ0O#co5Da!3nbGR?uwWJLA9L9H z+&ss9@~;f2UodSrg5I_8qBMwTWv0&9*LtV8L4g#Bf}WGGagZ1m`?*%*M+>5(k+3l%KSG2xm-3>Ig zwaCc&c0!R*e4?hR&QnjeF-Le7;P3XRD)Ng!mI!;5A%%`BDVU=$p=j(cCzrbYNkhBB zt~l&2EA{IQqX3si630i$nN9%M%cVtvC^x7?8El?Z(GBNToGTVKW_9^!6N|Tky<-E6 z0x*ElozO8XI*cZi+L+v^bdJ_n6ktbcAizG<)%cGOXKnhwX7=6f_hHzsUaN^?*D&Y2 zX$xSVhotDUy{3v)&tO&KNDOc#Sl^&H_G`9MuvWF@AJj)z4gnU?K_Kss=0oSLMjEEh z12=!ijO@}qPiyNLg&s(=VNgpwO-pQjxQ$B!%3 z+Ca)`_&DU&|1}}E^l1wrt~Gb=+KS|{BRnxZ6P21i9zHT98phKQ1JN_1G9ZA)-KDV9 z6kN7zZu@i~5&H>nbQIxhS~~!>@G!ZEtMSlUh!_jBgVwVdk)(}2jJ!E&>_M6Y{E-1~ znu_h#TA~Td;Z_+R%%jzgyX#n2bD^Lhl(ihy)UI}%%@yBh=yP*6HIAkraS`@5bEI==6`zxw7l<{0Pi?R`JjeXX_56$JPh z)vTl`)t|6ixJiuguLTX02eeRxyV9F#oFHqW^RDz3XO-#a#nR$x`L6{+ho-MJ}_vYcacTdOcv%(%+92?r7!u zR3yj@Wy$37>wZ#Jj~dF<8WG-%k2iZW2iQAtMFF9)6v2stT8V7YNYEmEh^Ip#>wS+F-p41=y1>zTx3Rz~wSgnJqz&UnR#>^49ul92- z5^yf6p`6HJ+gDxaN(Z0Ct@RI`j6hdZ!4x$)FfXnmEfbRpByYJ|Oh#{5pOq=-PWC?; zh?{Nu6-xtDSM2)LTQ>Abgf(8-(QAyJib?N}d){|20zBfKy+r~)XwU3K5$oz)aS0@~ zzKt7oWaaq&xN6URi?47D&OJMe4<56kQ_g@iFTFFRrijFpKe4g;+W3-AumF4pol*fW zm$~mI%q^G+Cf0+cTpZ@u1q`A}mR`^<)gS3UK7i&;eki(n*1 zZ(QSbvU6Q7PF-VWD5n2E7X)?MzKk=PHkDlP{cX+uhkx(FP2$eb6W1wa!@=*1Tllkg^q;f5tX=o{+~M|Vxcs!-;gSfES$Xrs+I**CjXVE z{6%%#r?51x5MZWvVCNI-heg88@{&!>N|)`oXY2*mwlK^&H)P51yCSVFp=-dGW)9^9 z$7O9RFS}vkVurGbOiJQ=+a~BtauLcb#wU_5@pl-c*DlTPtQ_(-#jT25pWj%~=0<&a zJ;CGorS_$^OOf{Il-S_a6*0H1wRI?gT4iskM%%fNK3&L{8~Uok-$Z>qpY&W632jZJ z8tx*lcpk9u;K5|%Hp+d&7^jzOhA3AJnO(Z2`>{UeeXimhw1xcd$bAUy?dB@x$LAxB z%%b!Vi%S!1lpwp2KX~W;f6*gyvT!DkJfY&?HHoLoettFjrkm%g`5i;p@9W)hlN6s; z157EBjH6HjrNPYf3D|*%PlJw z_Xug|vN9H@jX0;E)92g}{<|xPiv_5Txv?4uXrBTn3?ic^xc zC9s-Urnxi0vc;I1_Tja7|8-n^O5uUq4Tw)^FP$C5RY6%FvVMNeNY!udPDftT)Q%3Z z+wvY-`%l2x2#;6+# znO^YmR`;AHH!(Y1w-1|58-Wn#_Z07j#Sn1xPTRtwK~0zPA}$p7=oyvQ7$$?gT%ua& zshkbc;=+NT4~Ld=^G*nEW-w%0fgi%`fwu_?c`UBc$n)Tw5Uh`wy*s%V>HHH3L(5(#b4PhAEzuCXW<73=$DW}9fiJ_-skCv3XT4bxpk1}G5U+S>A zbH|tAwCj|a1yL&45x8F^MzKXvtQ=lY|D5xx*l~&G+c@9B!qBFylL|N@=3&-eZq5Pb z@<96mOia$PireM4OMAz^71*R7anTs>eBee3I7}v8RK|jlKdG}^mk&F*jdr=UVX<*> z&CTCEpLg%>V0**wR??z2pPDEA(ZK~Hi*`^ZhFiVgC(ZfiZyC&fHzboKW*7dMaT=;! zB(y@lGIEV8nn;DE{eM(&Ippheh*3HH&FbP{aWexR=I?r**SGQIkKLh2k&RHIRC;y$ z1zFC)i0T$rsU5k7WXHP=WjB$8exf)`nRk?8pp%hp-C>ZaOrOIwHrhk^z`G5U`#wV9 z&%2n^bHqry(sildakd;D!Lb*|E|jE;n+X0dbTGT1)whs@Nq&3U0d*PDYK)kf$}A^t zkG$XnV`7>YjrepsIVkKLcIQ#vqvq3_*RJlQFEP6rP%Wa9Jvs|o&YbY+akhWvB8fAK zsWkJ-SNKKh1Qo;$Gze&niaKL$vab(RIy-n}C2T2Bnat<=tCn-ouC*Vr@cKAcOH_lP z5s_LgaUxW=pzDIRqqQ}>3>7rVDt)F+0mCEWN)7vP3u1gI&hk(Eqyo@d+P2V`v^xpY zNDg@JI~SxM=<6G-fwBdXmxDFPB%H|9Qvc;I^oSs8Jijv9OG!1Dp+H=-waYJ2(&CRDZTsRSD*A-j)XKPLcz>98}TVp!=Q|#QM>Rwd=Eo@f^81UTOJx|J0en zHla&fb@aDM*Pr3a?xdwR#N4*E9U}7FI`Y31c6knJ3N*((&-SJ|b|kpSVsOBbEWc?8 zzo%|Bf1=@EF1jNw3xp8v7BJJfLyS_3X{R2PpoI~0{rWx#9yx6{NN@gCIYM0yt`L|< zD5br39Re@%9>&$Ps%tkW=L#dvTTfedd87xa(Ov+hFN_;xxP;In(pG1XAisnb#Y9vA z3+9#Y`LwxFCSr;ib^uu(ACVz2{;oFizb@5jhG5Gs+X&%V+O^}wH{lu;^hN$7-RRf+ zPPX&ygW`{uG__1WnJQ6#_kyD4B^=M$h!0oam)C%741h#_upaTlQzGr?;fKM z+Yn}ozal}%DfafoM0!ZZd@~l4wubMQr+!~8`gB~U`|zZSJ~h*Ln8KY>&V?`|JUlcE zZb&y4vw#ERC=@eLY=>bnm|Biyhrz1;M*MVuWO)G(Zz6j9n@>|#)wX>iCkf<%yXxQBt@i3@Wj#L_btjM#Bo_j>FtFSBAd)_I+nJZe5gejF9+#5t+|pl3}Z z5VSJ^jrl(5NSz@V`{4O_h(DaQ%^?TO*M_lds*pxA(Qb(Pu1AcpN@4nB&P)_*dW?xS zkSJ{t<+-O&MbxWDTlJ5`=-ofg;a&+=Up99Je#IFzz1*6=BWGq?QWAP)I%g~#HTeEX zJo0K`1%qA7Bp&+k&w7nR)C}(F&rL4%W*=oFsleN^k?}Jn3jD7HF1sL7nMvC-^*~nDJTpn$_cn;pJTbq8!*6}j!kr1t_>JK)pPJMFAC2~ zUcH)WVID2Ou~U*%oWR=wFTgBERsN>P>w5<3Ybt>XWw{GbGmW@sV~gYV7$?!;p_-u@5k^yb-K2mGv&-`EPyqtq@gD8mF>Sni!>CHm5)} zf6AnK^H!R{nL_AhrlPAKOKyi0P%wdZ7n;Q>uP`T1b3^#K(m7$@L`iDCOlfbE^C!@b zrIh1f?pv}9u(l*=2?kh;DsW8OQDn73oZdIp3cI>3p2cK0bW~HcscWXcP~ua5V6NBw z1o(F5Eyt-;hd55X8=AL*Oqx9T{fC}*is?`wi*&=Eyw;NLb&*``Y@z>$Q~g6HDI=6g zhr+bk#Zo!YefL7^@NJ))Wg>pM9l`2n3ORXag1cdHsQ)EoN(V;QMS1K%pdNIgnJWyk8gP!$RU**w<7cq@-+Jlx4q z1=yWA_Xo)MqcX6x&*Sx^H7&_(sP>KJ3VtBtDo@Y&L;5eAbm&11#tH5W< zwmKJ&WR%F6zPL{_i1-`|c^~rCIpkgHDA)5m!=;N-qlVTO+f-SDy^?Ybm*Ia;pV0K# z)cqNiWR)d`BN^gvlY=nwJ@!L=4m6U+2PAsg%<8A4It>C;}A_&fpedGhJ`=SLNs&L@*~%^)t>s*v=)NJ7zxQAa+3Jyw0#|y*C2_g zF5jFv2(n5ds=AO#T$-eMs-E2rPjw*|Dh^A%l>%B_V{2o4N_mD5N+s7IcUNWI#TN3Y zPd>Kodd)ioa+_TF$J6=FqlLxOa=R{|A$k@9yteEoe0-N;9_KY$7JpxqW<~6vZXtneZ^EI?O((iwlbv&QJ+L92O zt`x7`6S@KAINHe=XA`j+fs%lQdA)H{2s~)yNT5;HsfZ4&_R-AoK zc|oX^u35&5{KK3H$4ySb?ld{9T{N=x^kuNP9wk9J#w}TLvH<-8$?OSt5k)|29e$BS z%OBkeVPu{8U25-!Y4VzdWlCb5$P)W{)1ufg7b4|X{H?(ie1=|^V{v8UdhxVkC7(`t z(9iMlb(R%Jp6XCHo@&6XJN=yPFKcbfJe^PBo;rDfzbI;Gr9gBgX8lgmU}a~5YGq_t zCZ(_BtR|v!q>Xuu&h{sXh!AW(-IN z%)ESya)(`N>|jg+#+7cVvx|nwUAKcCUgD4J+1MW(6flH|HvFp8m}#zEX$y;g0dE2F z(pZ&eMxc@BoPt;aY+piP&=<;rBghaiLsh329o(_Y&8-mSKJw#+UC>?Vf+mi=^g|-x zqYlM{vQ`DL5DiE7ld(}KuhM_@u^Q}oJgDB&I`@ixDktNeAz&Pn&KTCTI9 zke#Zjv(;}tot**oUaJY|0gC(d>5u1qa?ILddyv4P{?S*v_d~h7i(*~(fk+4(1|)2_ z)i?61B1;*1cS9aVqyxX+PNjW)oi}rec&cvtA-WR8;&c%pR8Wwe6X}dSq7DmQ<;UB8U z7Sh|B8TE2wvS}l~yZ}pL{B(Qw?WZ*w5i?0c2)a-o>>+wCgt}}R`vMJKy>l!lW7vbG z;9?b!LS`QIm#}FSSQlQO`GJVj`ili%;CT@8?)GAM1K|?8U62^4kMSzNOK+LU(%Ty-neB>Eo6+#@~m$<2+uEk)O9c;$^uO3$oT{ z9GC8jWAf*LW~M6;Y|%^CbJ#g(_9#;0ll4&rf|u}UY}BaUi9L7#7gIRz4Lorim#+8q zTr9mYBtLOEg%zX_7VnQ6y7*_F@dB4$+Mo=;V|sfh!}OVaKm)!uh1zw6=X@5EZYxUcFt9g_Jl__r=le1ZTKdem+^rL-e0 zFUa~UyK&mgS#c39z7%vI&jzh^Jx&%e4(Waol_@ax@H}_R;ku3uK56g50d4I91-zHH z8~R>utlpwZFveZEF8VVIMoP4JP#fx5)n!Thna=}Fj0#gg4VJH z*M~}PBDKEqS69#^f&krKAC>NPrjn_EdX3`^2}^C&m~Xq4IES4EezbB>m!XtmISY}- zDr=FFqPmOpu^|T%>M=`!_vH@N;N!=ZgsWuu1DVLxtSmb4dN zv5OAaGI|%AYlE?1vrT{g2Vrzgt?wQ&IyXUiqwL1lvZ{1M>9UMDNa9E+vo&@PZ&tDC z?nyM}+9~Kz*BMNrSpR^BIucjIm_@j@0l#AZ>flK1d*=2l&ZlOKxiMcdvoeDQKSsWI z$Zbs9O}iM5E4%rN(CoQM&r1HBnfH7Y|9;iTZB>c|U05B=42rXw`(BKPr)inltpyeS z*chu_dha~pUybM&{`076`kke9T?51j{p=2AhnVkXCc{tn>F7}|KN4tk-DJ~+ow-wx zsT`}WdTcn1G^e`Xj!bT3K{VyEMF&JvzC4|f(28N`W3rlSfVDZpxQflKhH%`=#T;X4 zE8%pX?{<#z?XHR^@3jFf_W@RP0gcG^{E4kfb@=#?%*NLw)_WF7{G58orkE#`!jG3} zGDWeMP4UcW(_a6TO(~y@Bw9t}NFI+sri^r44-%>lFPfY9NAn>Y>X)2GMb`!aEEj+F z41nc4a$d7VZUZ=*DT@x;)VBd$6|K1i`j++cCjb?VL<-A@e)pW*KUNAw3AIH+|G2dw zoI(n?Svj|z>F;N}VS{e-_RPUsP&3$o4}THFQirFXe1Mw4MuqL|_D0|5Wo+KF57M3^NG!^{x#@U<)4kl)`Pd1)!Rqa~v&XBVNQvhPB^uI6u^;L< zcQ0460TUZHH(vGu?YTG++AM?#*>q5SnX%lAcxI=-jv|T$90Q?nIFaBkZ-={ND?*zzh@t)Sa7Yt~OWeFSRs{hf#|}3xmk{yD%}H~*P3!ECH6xU~ zD@i=Tyk;15Zm)G2nCKmtFy%lPuZbvgJ#T5X$#vt1%vub_z>=<)8Q=8$1^L?QL$U>K z^x~g??aJNTBDeP>Eft8zKML0B5kvpbScsG**)NDJ%q`htl^oA=1gW|Ku4)Wg)B7@> zFfpNH{SUNv>+bd_2>GJ%-jIGB6yjzYJ*x8yQ8aade!cyL(7o~c12op}2OW1?$+T9h z)k@TicM~XZ&m*~=m^KBVdu!XvaS`HXs8DhE63-i90SciU@~f#URJ z9*~(B`MS?kW~X8SO}@(c?B3I{doXAyrkYEjvirFZ3MEwC zDsQgtaIFnuUwv`p@!)idh8uT+5$kkg$VqwU(BzUdw-2L#XXi#y<@L&Ib~?Hy`<8RA zRY@}3x(;f~Uoa2d$gi0oeQEY8%3txW)7A~feFZy%O#=W=7attx>w7I<@@**P_^=ex z;7N4M;$Gc)T`&N72iw`!?!u;_>0OC)?y|hR{OIUb%yHbHdzEbC0xH9SEw}=XrC$e} zN5qDiH1Kl}t3jQTCpZUP%&wssp$Wm2Y({hw+$|i1@i6M9rh$2D7|DEAu=7H%Z|r$G z5!84aU7wc?5H`}J%cz7L?YkNy|2(BCD84PSbcyy?J-*QSWR$|FXO&aXlrw{L+q4Lu zBpAUwKJr8EEOoKdZ1rBZjR@#jAPg@EDXU2p8!qA+uZt;LILf2KJm0;aGw@0A#(go1 z!VwnLa|jcf;b0|FJ2NzWgTpid1t>Ake8rg{vAssBLizUKJQh8DIDhoUt;df?a4URO zJ0fm;kLTBA?PL1;vfivZNXv_aP)NBC2qkL}$4&}qwZ95y1jMpLjPxuSzLu8OnqdFK za;nS6L^EKnov$10#`BO$38xapV^Y|lqAh6eVz{(6RiYKb^?hQZvkCiBGID`DBlhL&u=EoWHl z8-Gh{D@RI1yTv;bAQGL(t?KJ+M;&a3ev6ypmWthSg_*Lc@g{cX(9dtc+T} z4r30XK*RpQyLB}%=A1Pd**f`<6e+g{16`ncg_*A{9m?@6!nrtSNUI)xwL?nkh$?kT z7CbmyCsiOq0#hmru5uIp0PR8H>G4uDH=0$cRFg@NpQ>O0pkK&O)?;Xew@pqgfVi-# zbB+?LBPf)tZW(59@uXJX=bIRcJ=l{eM;1@Ng6onJgPIzcQBC~9ttL|^Kaw8WI`R^8 zvGI>xo%6&#{by`3*{05`+u3>ddyd*XPKzO1yUHJgkvA}@+f9|6If#M+TdQo8N2ia1 z@p}%3?$K?Qk0JDaV*Kz$X zH4{mF`mITE{7Y_M>3M7>p+BnZg6sIK@5J6S9k<34jwOdztfK<5XArxem5yy#D3aR-Gzw>H*Zp4 z&S5o@p;I@xzL8Bptys0C2h`~CFYNR9nP>B8!N5swve||FmBKp{Jku|(l~gEkQoTJ=+^gw>FZcVbA_7-_F9D|L| zilSco3--LS>vld;60o&7vpSe%X=WMfH=iWOx3}=wI@yG4kfPkOdfSL*phd&tZe{vJ zkrg}p$N}UbDfA==ew;`RYAZW^Vd<~SY`QVGgqNv`5%}G`B;jE4D17Xwq;K%l$*g`>L>BkQ}hq?B0eNI5c+&nF8@kal|HAyQ(j`Q8BMQKyU; zv6MU5Uk80r<#JC6%RO0#LmvBit10~R3VzEwq(+!&20l!^R8Yb+g@K46HQjgr-ThIh zvriv8s?D_%f+%24Vrk~$Zef85VlrVwZp)L$PKySIhAt7z#MrlI!E>W=UBiPByAUb{ z=DFLQ7GasHcW;Z|7Rr6>mN@nNd$-*Ej8{8{&1{-PanDGx(Ob0pdoN6#TWaq;T)VV3 z=Xwj8MFetJC)&}i0st*9c5qppbLXpZbZh-)^y{v5G1B%zU>1-V!o(RVRS|hKSS?P= z%C{tVMZ=r&P`B=DY?tU-J1Sr7qQ7m>n<-e{#Wko;5Kgcl{wu+LmfSY=HnpjSc6F_A zi(7{=!u1kl0s#XBQ6Po(`^bcpHs*NQ1VEV2bBRZ=5y&P>16HS7CIs`trro{z*KZ-R zv`N)zDu6FuLEwwRYZniEP^1P@<97&|l-=1qi$`ooYBgyyC}46;yTum^B@-h9vZ-DT z+9_03-GFoJClqsu_3F>lT--y)xECTL35#V;20o3IwrohapR013ih4w}Nq`tm zCQkxf*l5SkX$Z~G(afH!3{od z9b=C7)K~Zm_0akKr0nL+EhnljScb9ZeScJqe+pKcVDRO$-P<*0j3I%);0WTD%mxs0 zP2O!rq}EuBF$)iQTk4N}mbwQcCpO1#=TSb>{*26lJf(vWX!KeA)bgg?*&%?;K) z>Qg>~!#z;y6tB}@_Z$h7Jh73`@*dJsr2|QOS<+G-AQpNNHekTAe8H!vV2U%N!^C$I z$&-9eHY3=f{U{+o&JebcbPLXCdjmny#d;vQnUlYL|KeEf-z2xL#Iu^RsdoqB>I-`T zAqBaBKi2GPNzd{qd4VKBC%*h3A#hju8sh)4ySTwER%3-{>n`$iL`Eqs^{%kINAKc}WwI?4dYeMF1OjjXI7}>@ygi zC>^or`N+9QSk)cSiy?ZafO<1FYlMQ|PhqD3b`-xN7KtVtRpvayC_t#G&i$W|Ea>(~&6% zS@!T{n8)b2D8vl;*#;`a`;QNyP`OsZIs~9`1vv*vvWCle|6ZuGhrDM^+}pDP=4->1 zEx4fa8b!(Z)aiE%H$$2l9fKr0e&CHWV3@p@e!v=?&NRsoOFP}-&1|N1=n@d!V8BO+ zZo8@$|9dH4p7W(&ufe{w1Vm}YmIhB~HN3q`bfb(&R(?a>!nC391*J@c5g7hU*0;3U zQwU5}Y#2YD9x`ATF<0U_Sk0w|OPmXZ*IkikMM{~Fl?*&NgKRqtkZR2$No9I^c7=_d_$((0@3{O!~YQh|Ss(q6{bffiH1w51) z<1@dV1n@)(qq6fEvIre};Q2UKg>}X5Y9+$?_=9)sXks=|V>8GT16<_d?SnyCYKTS> zcJg$!EHd$`*^I5RE3Q?#w)f|hZJSKB2e2gyKVB0>2Bzrf>MPxuve6V;i_S2TY z%S>(~Q^G_wt;c9TTYT?U1l<;gYf*cmDlHNFBFU^O`*_iKm6>9{R0v!V;ak zTKnK5?$JBtzs98&N|UpFj`M0lnPb0BBHYQ(lxiNl*NTBk*4$W%i7IT}QB)Vz=yfT< zYpa8n&4xl3&n92IEsl5j!xDD1H9oFcV9=%RlDEHtFy(q^1AI6Of2_gswbgw`Mtr9# zXevR1UL*PG))~6I=B>~CY6+#W%LULzU6?yHIuzuMJDF_Om)mgLot)tQS6{rZ-~U;8 zBzUT+Y7pG9H2s1^WjQ6?wiaZ~0?$4ZRcGZqu1foCakdf)6zi{hz~^&_kE`$cOlNm@ zRIF@k_Toc99VkwOQyOt+vB0d_E(Ad13&3o9Wf6as2r$L{{)qzJ(wL*bsX&lz4rsUM zUUaN!iNBPuFFE{(SrWi)qD{e=zUT@=)^T!RF#d0KFGAP9buS=8^BBB)-=G+J&~t`5 zaV=DbM$LQy;k%!th&aZNdkySGM<7w<;qI%{=u}K}w`3whDfJsS3_VB>musvw)T{cU zPuRR~@u`aXoWqQZU@`nagcE!Dtz#O8kn6U)m2Q?rc3D`px&<;u7irc)k38-6kb zeC>8#{AIjR-u1#Cpl0EIE7#_~_@l05{u*@ZpO|)TxSw)P5Odvdadvk0oBb~Npc+1e z{YmXaB~hfkuP_;=f&yK8muX5>Bf96^qiq_L!N}6O05kgT2S(&1%CI$S3EM`1XX~{c zyA?am!sP^u8dg>?MSh{QT4n*d2=2wvEr{;ANtc>rm9N5H77W|Dg38Vs(hMYN_sTbP zA~4)WV-+#|u#am@Y@f=GJ`6fVOhKB}!8Bvb16Hp*xLUiNWdIUu$?jPCyr#@3=kA!p zZCkv$)<{^HUpVS{2J%@oYBjT4$9iVj;WxMuoE?i3vQUf?8a^u42HmhQFO8D)Jdca$ z>@CdSiA@?v7(T*(=NqlHlHllkbDF5dgO-*ID;qfJl=FWyas9N<5Dw_~s#g;q@KE*X?0 z6`sSM`G=t8TVC-UyRG{nO*P9N`^h~7o3N!a`?CNgvbpoEoJ!9RjVDNqx3kjkFpct{ zT9UpWcwN-)>DKj`+}Am8%yzt-n^aGoqP;MBa_n=Af2Vj)eje?`UcGME>om9!2ykwc9bT3Y& z!kXPWsGjw*YB)yOB>kw-NcH8Y$7WK{R|#&OU)3vp)hn`USIRSP9lfKbrFAIauI5z_ zvuE$5jsuLX!-C>J=vYKWN6DFkEp(k`}>`RkmWgM zSn3~JQt7hC8<)J{8(I4YjqSIbRr?PbTYV!2p|L3toQdHM@5l2HWOcE!I;%U^f=UjYnKxJ|9wga6Os;bv|Uk{)ol&CpeM&24kP`qky^Q z6549mm)87pDmX4>qXDv2ZT@cls;i4 zecB~}JDT5;f1w{JvNn=yw&^qX-siihHF%$!kMA4#`1TV|wzgCm&j$2st-N>)ZvTBR zj$JI77GGEinin~4DRbMs4Io-n?i1C%bj%1zb9ZNstNupf@#;@!zZS&Ln*~fWsLS^s z>(!zA)wVGr3Qgk&R6`TQ%Y;7aqurqV>;?W&&CP2$yGtg)OZD9ja~5-%TekBJX{!EW z33y%b+PE>V;gM+{PBKYB;Q7{7m*jK!`@#7FFgd&Lj8Fa?Z`I}8msrb}l90|ollg(wXR>~VkxpLGdu-$B7S+D@+5oMzMdPcH2mg1Ur3+tOn(!+ z>ME0~@sI)XRm#O(!Ma?{KR~u((`6SwB^r7mAS8>M_mx9quv(fh(A8jlAo@WlW3Pgx zCV_|?9C&B)9F``->lFwbYq6y7NC%az8ZirLKzb*JCKp=7kU~A1>u(6(3I2#O-U^w9#7Zu-XD$*ICi!( z;MkG#?070XvoDo8o{L|_L*IX1b!C^SrfwJXZwqs->9I}CRjWB>W^`*jGVTzFx=m*h zsx4nY9cfoCrFUqU`82hnqf5fXj9r^Zm|nm-T|`t6W1fGgAj2z~eSB$>0dlxeR-#-k z8|}hK8|ANI@qWPDb9Vgt)yz{^x6SRFYh}$yYX6cR>HOpjaXqF;YpAC)VK-i=v>L=JV-~ganRymI$DV&ZGE@x%W%;xzQA+^0*He^IY3VbMyg+{Q%4bR>+{k`3LiW zH`mLSorBu*2rEz3s`%NeVa~PD_HpsCl@T;ypUAAM)B)XcC%eee@m%=KJr#-W#4qOU z!wo2a;*POst+P|1cGl9G;4V18@Vr#)j?h;k zN;89}TT3fmXGG7Q?8V7ZkS`Y-92gjQ_N?a46UXfv`dOswY?TNrV1D)33x{p%`IxT# z^ZBGlww@+8O*H#J7Ubz#`}po18XyxO=93;8qEz&lS7VSlH67Hb%1QCyL@)>Zl;@Mx z4ZTX#{4ETgiRx2kuO7h@|3p2H)V3HvBS2HsAQMw*se#IjMudiD4=v4`xK1NRO;hAa z51mFsPX`}gd`NRg^ESAsJ}nV&^A=JG6;=QZZ6}z8o1~HErWK}>UxdNPh5B#jA}Y<_ zAOpxcY$>-A>Go=`LbhUjWh7t<3RYXcU&F8sEN66d&pvy^X{_yKT8nGDf){E7O2qRD z{liY5KZ}d|2YT?N!Y$+^9p>>K2rJ+DazCa!TfJ&p zvIgO4=TXGjgVupY!347qcU^6<9C5tq5>;bGnLVRf@=~h+QXMl1lXz zV=o|r;ld;TU&&CB6 z9YxNok#;lJtc_IF;9Za5%c3IRbE(zM_u==3f5r(Af$i6_7i%ilZjyDf2o_X(5}qpn z@Y#f%+Osmvr`jGSLf1N_iw&(sIGmD;CAF>^BT!SIn~D7EjXE)Q)U-XM+M%7%R8Op# z)!JgTA7&$@b55Az!Tvf8}1?Lw+pe+tHY7FJ<^#YN_tr&u0g0F;sw_SA$nB=WhP@41RM|Dyhx zJ+c=+dCmYhsUHVUTKl_^3b6qg&S8^qn7fl|n$4g>s$3mi+|HPaf?n~7)N+?~>Yo}O z=_1>m_~iAJuJH4UZ!gK8noW8$CGf34=PBXVk4m+Y%8sgF-6JMH8Plhd%CK!VA@)tT zaq30R^CIwL;nnaby-rdz42w=+a+(K+JSRu%d``aYht( zfevp;JVY>OU3^S*e9YD=j3`SN8U@^HEs-3yE=!72%$dBhH+)JBbCsvgO~^eA2*B)L zIs?lW9Of*>McK;9>G%zcT!87Epa;Z?AGK<79sJNrBKQA>fE7haH(6B^@bd-|1&P40iHxBOVojNR$_W!*OwQB{~Et^uC{MY`tiLpDy zWy+v)T(Kkr$JXtxR(H4H)gng8EMA=o4Tfgj3M#!%?I@5Gg?p@2`v%I*i{Qi#R&EC% zm<|miaX;-^p(`6^pY84>6pT!W&N%my?bcd!-=RQz+}fs!QOJ)vHjYL!KIvX1cs=_w zvS|Q}R@m~A3bTZ#N!aWYLWHxE{rsbvmmd=R%Wdn@ZP!aakz-{K9&)sv43x-&(g{+O z2(!=5-PIul=so_LuL{FUhwyH$!!SeDszk~#IZGOGAhW8AJ~wGcabh&zbkP>0A3oWH7|TgkKMwo?G*fE%KQ@yzYxw z8y7F$v=6;nI&e9W)Om)uJ_&neGNurk>Q|)jhVGv!e@r0orqL{7)SL+@sy46u6V+?TwgGr`{|LD%2h*_@># z8hn8aQTTCQxb$!6OlJCqVOHJX_~(@H(|V_WJ!}Q3*BDD)%EouG?WMGh(k(`i;rY+Y za32CQYD9)gq#)|Um7E`YVF1UJx9tF(80O`ld5_)H72b4Czqqk!t+LqJj_Xh;B*TGr z=6!nG!vLpN&I=F68v7|hjfIrW+bBumUM<8|UTqd+V;l`_I8g|iaOE=6%ZRx9Z_ZoH ze%dx|BZUjVV{Jyz@RZ0p|o{emt;O60sC_Y*hUJ6 zRc?1m9d>!N%SR%)>F015b?4l!1l4%@wxUpsX zwEkcF?FZ;eP9OwOp>Mj=Rcwqt@RlFhzmzE4bQT(P30mm4B?34zGrj0Er1^JyNf3iL zVtJ_M_f=It7D*0PmuCb^=g@Sjs#Kka$?wSs{c;G;4xPSO9id_+$>#OL+yB2>Z`1tN zp`w+4$j1$I9*_9PMnx~KYuCS@A}8tjM4fn7in@Td-^Tg(PZzHE8SE;;miD#xlx!c5 zpeq>{ay}`84k9H{z9>K`tddl`d}c&r?r{NNI$F8Ls8?$(edjK`eB-$9TIRrD%Hk^Z zH%e5#Dg0#JQ%u#-5eh>Es5_$5$lkkAd}#zLvs|WQys>~aouC>g9=_###Kf=r#{}gs zv4X%A1Vn@=dG=t>Rl9z%?Q@r+K9%u4pe=d#b%WN^Y%~x8%}*asieJAJ@Nq4kfArb; ze7yOXZp(Rd+yZ9&U7uOq$4mUV#3&ro46amzZV?|@(3vWw;0?&xf^!ukhJQlDQ>9@6 z6f*d)Jd*AZZ^FZH;$+|~IjYFEYtUbzP4>2!CCverEkO;eXB!!&nHqg7o?y*VRs*Yz z#kBS!Rx00yC@`1tRO9om-Kq7sB&-^7L8XR?%E1%ucl=4+_@P_ppUCeCz)x4?iH=}m z5L;^5Pt}JX%Dx&$Bs$Vm?@N~!?7&|czjO29r;rQiXd&BdQf{OTyq^jYI&4hunM;g5 zS_Ho_MeM=slurTNM?3RtLTjtz5|sUP)&H|E7!f43L6B5*_Y!DuJiPenm^OLw_uPYg zv-N;A^y>9#k2yPKVc-EONbLAmdVWNNMrFVLUSETc(qNx6x6eY&FRr55(Tj`8Sl@Qo zre(+CsX6Q)s?jCUgHUAW)5=c#>7N%+lk$DDn^o9XhfxosB6?b-JC{%3Gt1ttx=pLl zny-FJ6KEX4IF|icDSJx)_oryz(DyWb`94$dVA-zw#*s&~YwQ+-l@+|K3l9#=vvPeS zI??~0XdP38+X6U5$cq3I!ao&S`M!yWU-EL~VQ+1O_bB|h=2EP>y+j7(!l%*o$5s}B0i zg&g}1CWvnw2e_p0cQ3hL$g+8Jg0T-Yzpmj0Dbfe*TKrr>V{3RP;$d)7B+uIV#F8^2 z@SW;vJo{nmVzwHNOYA!bd5ESyd`z7NyZG!8Tpae7o6_g`*7}&A)}Wi|6X)%&$ zb>%Y`R-Bzmiz1L?!qyPJ#dyYklG8 zGUvduF>7dgiToDYiViN>Wm#kij$q1gYn^bqjiGlvUUZG`N-Bh!FmEBx@qbgLB=pS9 zYD1n~AtxF<#B`jiE3WEF?>0+8^w?>Nh`)VlY0w)m_xABHs{iB3niJ31;Y*mmo3Oge zX!j*Rxut+uN{#<1vg1U2;x-~2P%lEjn5llDEk)z#@L~%BrvIl+fbvr1*#|2L)dfSL z2YGPE^1K-h1VyV2g?nO%iPP^_r) zb*s?W09C}RI(ncv1tcZh`@geUHpwCwx6ytHKTH(t+4eMGZrIHEq>l$dI?fbqRJIwO zKq4cS8qL7HF16`wF(UiM^$>3(R8X_<;+4K#nmVT9n{TBD;m5s)`5(CrXEyGW11hHv za&q(X5;f-FKx88<2#9P-;6JRmg}^97J|%do8bD;uV9BoGIAVo(Qt=;+Ipq8up@O`I z(%0X4jk=4*ueams@TLsD9QoEwo=_7#(UfriBb+q(&p}E%@A8-yMILq4T|6L9 zsMdaeVBX--L0C<#?x>X4&xcV~7CR>EmDF#+o1)#Br&@exJ&S8uJ4Hv?uA$W+(h%?7 z35o49vECd*51NAHaDkfiknXFUOW(Zi(w;Ay&8ZW!yc*H?rFgpnBH}#-rWW(H&xW3z z`F5S!%*d-R39t?zG5s9)jme=tBILdk`>3dC;=GWM%n)uBaHP2gk}{DShtCTk(Nh5_ zX(~XhwEw-T9vB7An5Qj8Bx0D^)t7+hXsho;-U(pcVo(InoInr9QbwkK6>XWRh}`9j z>AX(9q_56dr3GPK5s$x}O+uQa_E5{j?ucNuxQ9;5LrXSvA zo}3Vvi%3~8jyMs)CXIcd+yd!;Wc>-t8C1HVulC#OVH)E8bZ!UXufokdNL!nqcPq5D zJwe*qprP$tSLz@g;hf-84qU=nbKA+m##wci{h6+U0_se5z}GcLp3A0OoyvV=cg&W- zsOY{HEV@(mW-1{(GxDC)jwAnP3)fH6H!5=~JN8f3Ved>heQXN4i_6m7J(eFeSz55| z6eEyZ<1#PCB;vf`dg0c9j!q$?sYw&#g`Ya`f?%JRYdYnwZ92& zZzvCuAVyA^Iz@K&RNi~Cf(M5V$waUlr$1*bz&w*B`f@mC?qdcQyE$Ed*MU~iM(hW?D0U(?)Ui?JEN z{3VC+nfX^tkrAVYw&xCa5n&m=_z?f*R3VnOH37CN|B#<~=LfSc3ArwuyNp=+n?{Za z;LJ~kA$NGe4f9}cSkat2?{f$<04{VN78{9;Q#YBNO@5xGAN}FG8|a^}j@5JyO7yl# z6FhDu>lfffFt?SyE9w4Ea*-T3*cuGvzT(c?Hez0(&QDk)#c$y-jYi<$NVl3=Eu!63 zcV{hIlG3GB7knb?BLqJ1-VAp*?uqCRyOQ`o*Y3(J>y z8HG7IhQwMIdvZ)>eI|r8+;h5ycGOp*_w%}yTO4y1;-1m`Q=5xpGD4!F1qJgsBVm5r z&HjeFt~{65r?Bf&-)v{{i-A%UuCtGCkYej0cWI{G;dZV!qPgb{Z=OPSqh2>LIZOj7 z1(6d^{6B&&q+-F?w339y3^_=1=wi|Fv z7&Sq>FNkv~HSG!5)dce8L1uE>k9&w~7DU=w$`hJ(vJtobALJvq#3#vm+{-C1NwoTH ztJ!lOA7ytl`#VgrOF`gF2aE~Fl|kjPY@+{3M~bkpe-xGRIIO1vC^hk+F~reGx@xbnnXCvt}^Y87ja$O*gWW2 zD$dU`7jL5OY}Gbc!+hh?(5$)T&xJU7=L%5b*sY(R%BH4T+j!aYpAX!zE3FK&W})%U zT(5 z(`x4@lP;S5dWt{R9tnLgG7S#xZ}g{(ZFZ3-L0MPHmKn=5_++Wimf}z+SK-ZO1OgGX zH>_5D|2_712|&^IUx%$ff;1b03#cWo-!e>R$T|)GTwM>+{bF9M zIFfZD`hn@)Q-A?ac*1HiVgri~+?{D6rZr+9_YK-QYG!F5hZZfxJ2;rK6S@rs>JEQ+ zV1Z_LV@1OwVP{=odkuRf)1AofVKdcY;?r)Q>>l|$5ffFarJFam66bgA zVc*88gz968RR>MHam7<(V`Iwu@dxiqvg3g_WvBlYuR1w1{s zxY(+o(Dnx(+YxX4BaIMT&>A5ui#@50h(692z)KAw zY?`e5QTj%&Z3nPVal&5vSlKG${U?!62Ix`q?O%15At&bJ@-0)vjKiq0tTYfw^sO>3 zL84E3xvXhN0d@JM#tqlp#plfGp5`2f^&!K6>&oER-+RYyJ6bGnn_2dx_#xG)D&MBa zrskr^$e;g@ySI$WI{W*DFF-($5(GuMB?P4#kw!u#1f?74Mq24^kVXONmhO_0M!LJZ z^V!#h&dh)A`#$G6>v{E@H?wA(wS2*5@89?PNgPNc=^+Vy3j2%<-_4xo02xLwx1BQgbw15WB1;QYh;43_}Ncap-Vj)sOw^$L}jD*y^ z8j=MK3x!H3MfVE&uhNB2RP*^BQ^l!N>}Nvz=Gk3X7$7$i1W%hC?-xpIT>Aw86^{KI z70wM*M7a@0|EE>Rgn*Y8&G`6E4}q&PG^~ra<3GX!{$U;ZTMNq<>!It*r4RAZ$Z!n_ZT zmkr6*9nd0NHMV?+4<@|ENNhUX!Z@4WF(7IDZ2foBFW5_q5(3uDcYspo*&jlW=Rr9I`K>I%*icaed zmyS+&F^=^ko#K&{uC^S1x43#TIFD2G6#Tsf7JC;=Kl{Pac=4)APzRQ%@v5eR00JXu zK1c~l*IM56M`G%CG<8tA&8#T01?2!V(CEYUiLob#1K~NV0<~O)8ckM_-JqB z#IWC?)d>D!{4`C*Zi}+iBy2?0U5JX|1;Sp+cgTN4yIgu3%|UY`{)zuFr68jd?bu1OuVO{byJixh3nitl`A~FBywBtsUuD@%I8x2j= zntwFfF?Y{*-Y4wGTN;Z<@o5%cr$+!n>S_R8HN@tOEe*mTVB%;_+*Z8AWqIQOsg}1(I(X0M1xT=@_U3;p+-ia^PmWiBG+)^pI|MQ z5se&c!;fj@T|tBZ@msBry_`U1K`kFa2_BdChu=Z{D;Xk04khpBz+I+-G}g|{Dy?ed z)1{wRIOS2YAUIKu*Wh-Iq4*4}iy{fuMKO94<^vU-iy6ZG(;N0dh6<=9kgwNFXsv(q zrK(mMk1?ors;NFsM~bt;BJxTb%t)WP_ct{3|Mc~CJiZb=^uN}>IBZ!Lc^U$wCC}7? z7E2(io9}-`h1phTKL*%+zU%dQLI>Zkh|Eq`ycg;49n7E3c3swubX)^TewL951wgGo zL8@QTt&*;k_?ixkmkcO^+3i%1s0+x*$+z?JZ1dqD9LBS5aMj~pBQwWm2`7QZQYI<{ zf2(AH5#efMV`J;C9RC$G8xu_4A)K#<-8HxFlR z^U;a5r6oDYTK*CkzC7&{9~Ar%I(PVnbud(*!dwNJ(6QTvRTd$`Da8UfEhXZ=f&|Jm zA!u?s2_RPf9i&Adi%_pb+yBG!BQpZb^JAQ1AE^Kuc#$?rDVWVqt33*i-u@9QR zH}Mgg<}iX@BN!Xva_BJoxQvXrJOxn?p#S}Uty*1m4%BSl3SAsjE!=GRQvr`M zQ_QGqyH7uJLmGEhtf@Mk(TR-q8@_+v3PpBd+%`k6co^RLyV2Q$e8d&Uhi+MCAj%FF zEs*Aw2q8^nSj&Gzq*`2*lS`cR_2~SFZ2_gN?E>vXP9uB4)Lu>?I52mQ%jDX%y=k2f zV(2B8t>a~H%Gqt+P0R@bcNTysCd1LMh;B_!KmG+@qjO^&2u$;ItXNkLdQh{d&zW=;esCj_8IK5x}hjVn&%*T#5Z=*2dZYB zasB%F1w6sf?6S?$6&`)>SZ#V(6Non-G^5##&BRvcOZhjbUS;bd2i5`e$NDwL#56vp zimE4Md&?YZF<66I49*CuX@59|nB}c}s%MK&Ua1NBb(YgCna~4 zcepE6^LtAUTqTXTJBT-m&wQ%`N|+_@-6qCqWZ$n;Q!nmPIj$J8oxIKul^q@)E=%Hf z{eg^FfB#GGQ@7(f91VhtA7waop@Er!M*ol{WE%4d2n1i!iTozc&CiNgR2My*cK(@%0tW^w?paWhX$~WPlZ^EvbN}{F$1}ZIpg5R6m!q=&R2nf1Jutal&7vg`qq0Mi|meS|1 zUTOmAK4*~0UAR*GSYykyO+J5^ius0Xxq<%tCa3w#4wfT8s2Z&d?mL9SX1IbdTHDOZ zbZA@__FZo|PV;)HEkzcOns`g(fk(SLlw6DEi2niircFMB;=TW*ykUCgD^_ipQLfqe z*d@wl#XoEd$Pt;-wut6RoEV;sl?8JSCW!#kFUsTdwYkXn`1sh4iSP^HFep&?5CJ@Q zhr`pRz&Up=F(wimHO*AS;Huiw5o$nKIRt3y99h*ek<$=Z`2h0$?}8?Uh9YsfWxM(p zzUDw65mYZL1}?P5Vw(|h4T*O3W>dj5d3EY_Va??w%V~W+zV=1Bq?^Z9K0zeW$&ZL+fAD(K6kT-06$M&K6tzxb(v{Zc;T@WCI!fS?v6%e`A zL2@(3znfF2V=b?1Vk0zybGl{tcgmp-*E3nY&sFvE|DD0Wh!gR{BIp6$fZ`T6VBjH2 z=e--V*MG9%pv--GsqNoSHP=)V+k-fBw$sz%xZh>&leg4r>l%(D=#J0N=1Q`xX8_zW zD(asrb^zVJcf)~_FVkh%e^o$|3F5xSD`ia-xmxRJf{x!$1(WfSzB99bMPe}jc#VTj zpX5=7v^+;U$V3Y5%+Agp-28lnpfiZ%l%fj2-j%P8MGa5CIkY+L{Lo?~XsZ|m{mxpEra;RyU2KK1(_{NLV zy?{ny%^O03_(!GxFHON(oIFG(u{>_=@*lpR3{P@vhb(xZ1VW#Lw8I9-ux;0CK@vzW z^W%LENwEjPPcmwUUL~msg!Xvf8>}-bUZ`tB=Yl~~yoj1{jCAXx<<-(Wa?{%0&Yv${ zugWtrF)hU7^u@Y~$)Ok)X8Q(%`?io)|}d-X4KzzV0zKvO2l zNK`6xJ)G_A?ADvv;8eje<&J!4j%v)wCG(N*_Q^(WvZ60n$xZNJI}VI5dqD`HkI{Wqkk;afz~R zz_UQv_A`CcX12|(-87JLH8e)F=K&L2efBibsaaGecvZK8kV@%-=W zwlM5-sRBjm&Q68}3ckUQOOc{Nx}&rZcN(|rAF`rn^?%A~*M$35ap92t>LiMk_6z<)^7X)Ci1;okIH3k%=kwax& z?=hCrSUDcHiR6cWXI1?bEQmdQT~mbuCPVU`M80sd+EePb6fkfk|B&4*fshEeTE#nJ{~mHX&1`zHf}5I82Y;9goi=?5_{E zrGunZkT2$Pk+B^hC$l0jA!1TLc?{#ZphILr+5pmN6gCJ2q&MNs$Aoa6gx_b1@r1s3 z_+J-l6Omq0t5OakuKH41+A)`+$FI|ex6MIw#PGf$Tcm~MhfhKWh&U%IP{|7d9M-v@ z2xAQ~G!i*aJn3Si7XZTXR&zf=0gEAqjI`sSkM>(@LPGmxeYPp+XUgg0-??$WPyohI znW8Jh2hFwqc9U!Udcxr|G}fkqzv5G2IfvYuMj*PY)>{Jm*W&JOR+axJs#IdYoP3s} z@=X{Ah{yr4(jB`p)GElZpUw!mz@NWq~E8ROYH{$`_O=g1f;l#sLlbwvbw!PYiOWq)WS6yBGFo}OFlwaADpG~oynfyM zOSj)0*iPkuurZ*YsvoEQ*Asx2$UF2^g0j||+Vh)m)f*gA4;oV?@eqONM`-Uh_-N5$ z*ji!$p%paTA+`ou84+dsqj}(o)FqVs&-4=^XT?)=^TsgE0npe2sFpcN-oL08QqUPuDx*CJ2+U8iB+ICi zuHyI2L0Fjs=I{a+V6A7Y3jTw&_B+j~bh;*sRCc*Mv(evjuEv8XyP|@jaW|c0bpG1E z@=AHt;Y-v)us*`&(C|-}1%h>>z-0alRX*2B>$V$BRbiX6vD4ij;~DAGV?95Sww-VL zGSCdE2ta8TX3sOsfaXx7{A=;ikFsTbBCQ$-GpFdhS3`vr=<+|uAW7Y$a1h=3g%))u zm>mc0V}GRZ6t28$I;~;N6V>LGus(nvgOe18ZEp#jj)_Pi(xr`@VX#MZSTgZo01}^S zxjg_mTAEoXOMzJw3GCHmExQ+I1{IzQ1f|18AT{qI!v7V1^FLp_$DyDK;<@*LhHpEP zzaunJ{x5sZ1CT?bBj8UZ3a1;I%+GvOe3H9R zyMgvW2Zn&qy&g1^haw<9F^a4721H1ni3q#2AE01De0;8c#GFM8xT*w#4_S%P&TrDI zi}xlh{-Tg(egMd8cpZ-d$n-B(_n|}%K#>(+K>hjZA}Xq%gRpIC+J~tlFrYJFKeVbr zMy2SrS#6)AgBW1@Jy)SQQ$Wk;fRW7^^M8NPlum1A=P^kXISu{A*`WxcW#Swr6*wgw z9=UUI^*iQ!;2%46*FDqUF1*V|_!G>^+E9ByD)zAK4m9vjP#@+ka2G;#O$uo zjp#HG)%EIvYuzSvK;P0bsvb0$gMtP+XhFk;A>?gRtSGnmpbbrEN%ve1XtQPmdZ{-S zmA|$82`vhzNB~v_e-_p^$>}f*tucp&q+kRQ#al7tN#0L2Yv96*PDwuw60(qK{gQTX zH;aBIf#jDxO#v*F|1Sd$;8QaF{~Ia)-=s}9=ry)5`!?7x$0+ANy+Hy8D`~hYQZs-5 z^+NPf(n4CHVK#HJEXwrv{QaRxHtx{e{=bmqztR%se$_S6aM^gnvWEYW z0JsL9H@C?q;^M#c=Slrnal>dX<$t2dp%*!HX>Es_tvdo|z=O8f;(s5jVATi?kOj$^ zT}k2v86fi{xeH@=+^o(OtkmiPl2s4-w9ZAMsPfm9EATvohR- zf5b`o85mIzFGvGwkJ!&Dr`1Sao~ujU@w|j(!ef%~dmtT>d}Oh|Y^}y{s+4A`O-HXd z-PnZ%Xu@p3uNO{7@z--h!H^w%QGZ$nNyZQb?TW1Vn*8o+ zfTq2b->&>_BHvh#F4Ys`Jxr(t%d|l@HS*DZFEjCXq>b&6RGjCn0x|8<&#zmnd3;UE zBsK4ir0-~HW8ZZqL+^cLEjv7#Sty3>p~e2bu76JvPflEWWY zGu|?urRl!a`wBF`?sS3H%_mW4}#B!4lwZ(|%SDUGqMKDky0EzyaSGzr~YFUlX^R5xrM&=&RAZzxrg|&(P zNFozr55FO=@dy&j1Rd=jXJ8zrxjLFa12TNt;_5N)-mR#tb!ORe(dAXM@IL;1@*_(q)3+6Bv+OYlqDhD$j`j z$77%7KLkWLIfswK+7cvmIfBH(g!zBCQypQt|Iq>v7KvhV9s{3Ls#yNq+M)(Un$H_m zxb2G&c1EuRSZE1IJ$=itVonfio*xB(z@o5JOZYfKQ_$Z;)G@U=cONRl2C0G@B9Uj&&T*wXIeY+hGT=EZqn zKU--Cw?`lQcRp3`v)$(G?UR>lYfYORyTR2$#Gz0&hd8`I`nc9uZ=(n^G9&Q#BV|8- zrPrCohYwt?Sl?>rRs<47P3i;Rb$B(Jy?2L7h1-k*$$g2Sf z3&_ilKon|!50D6Rp5m`MTINMgd8FOM)^H7RdMdD$djN@O36I{WoCA`5C+k{;CN&)Lt$l$CZyXw>f#VbqvPO*apsY-0VSc$GMBGARuf8rXm~`v#oQTy2%DeR zIUnaIbdY%k)LNp)q=YK%(^QRyrTK@9ig zFaa76yDct8){aX%SFQ@s>Fw~b*Av-i!5+v{#oqaWr5i=U^d%Tqq-130bFUUoZ|GDn z!soBshf4(|ldeOG%=T>7@K#Ls*kV*{4%LE(Kb(k{^=cQ?DENVbs2Z%;(>g;1Uas*1$Ok z+Dzj3(363xihw$Fn<8@}rwgz&(^l9Y*4+t=FfDl%yA3DjRiKt`qcjf9%%!Fl#(KTY z_wkK?^_Uz@VMh&O{wE((zq7vx%a$QUtHpozM6rO!)wA?m;yqjED@q0$sM7+kUMxiM zYh=6_7jGdeRX4$6b4r+Vf`sepqV1ZuKLI_^TVr2U_eg{&(CPgT0;cG zsa&s5JnsD~k4~N?`M!~QWtROK5R0js7-awPaT+vQhQ`)O8_LQ2Q)&N6`se3Ej;HmR zgYOf!PxDOqy-v*3oG5w@r&;Bc7lDas;3~8ZZ)W=Xs_qRmd+YVDA^$uzr!(3PHukD7 z6a*ziNAJ`r&W$cKYSHnrVLZ%K>Q3Ft&k&&)87*kc?9aP;)0k0}vIqCvmYTLe{){U~ zUZ42xGlSH7pmYXSflOKg)VfUcPk%YmO!$FQg2yC2DJdOq?ndR~MW|Tn@bDR|wIp2L z_WE3X{WwGz4;l|`77qlal@fW>^9*mdedNL%tZ)ccf~kC~ahJb?A;ZSPoa#TN_4o4M zYJ-w*WTXx>;}@@?)e->Bn#0zLW=EYtD%Eeqo-^_Mn*1M~2$n6-PK0onzqaMkRcbU6 z-i|*BI&&PxJmZO=- zwhmj2ViJ*hPccu>H30m(3r-7k(N*-Ed^!k{y>~sJES$<8pG4bN9GT4Uy3%jCk zr@)mX_-}HBA-Y>CN>MrdzoA#M?H4&Xy_(oc!-7e-b3mn@tsw#$vOy7HG7Za5RgEkU z84}FczxxiNFKJq73y=i`CUE6xuTslc1K$+?EMR?gCXj6j^<#rJ5@;FCy)T*3AUF@H zO0KRO;J;Fyis7o&Iv=y*N{4w$JiufuR(9qpSQku~TMP{TZbjBlabGgK)L&8m=*gat|# zg3r<(OFx+Ucw0)zVU_D3;Vu+ZYpa7yyCn@hCf@_expcS#$~Q~&ByTjcvHzV1<7aZt z5Y&_LH#^CMVC-%;quyW959g+4+n)FJ%DK4G9y_!*1O*C0U^)u{Mg~A0udOIwJw*|zhaWBs18Y6OnDB!0X zG;R6#t=ntAi~Q@$YiVRib;3fp&&AtSk)5xPyMv?P^G3HTw{u9RAAAFmFG9kw@!+6& z9P4v)ZIf4t6~+ zt`i&vM9O3Z7AerbrvyS=*7NJ8FOJ4Zl2TQr4DJqH^Jf0NtI|^BOpaZ7p~b*R5cyQQ zlnG0rZsU_aWT}H$$|9bYBges3pxQkL8K0+AXfLn(o|v117s>QJf>W_H@Ms->Cz~#H zbQ+uRa@RNM?(Q~!qYxLqe;>qWef|a8RV_)m&Y-{bGLuM^2G0JeMSd^B9Ap7h)M$;j z4B7_4C5ac$dkz)whRf?2-CYCV5vi%f+A(xpC0-t~+$prg0CYXTAMiHI=ym^3DmsEG zHI+Gsp84U_mCoLjV}pXQQpAc^*fO|^*o-m%C+SFG=`+_oQ&l#jq4S^vT>e{@`Rp7TOy%`_x|*qu>|B;7L)K z$?Llw>}JAK(y}WeuROl}4c;mKWJFQaVhKVdm5}gKUp&KC$Vg#&FtLEbNz#yBPAAUw z=pFQk>}avsPrDe;C@W{Y`p5deDDgQ8dur|{A&v-^ z?I^p|UF-{VlJI=BxJ(JtN0*nsVhym(eWfU>H_OTH+MgJSM|ac^^U@8EH7o7qi!?+H4(X;A6r zA=_h5A3CvZeDayb5sq>;7{%S887(LKptCfS+$VM-dL&%RFr%`_`S+em z^kWc|uZoQEeAIP#tAyCw`3z^g6ALta@Mx^A2-3fIUegpoC5f%Id{;E8D9SwiBxTH? z-qO`vq;;*tCKIDHeAW&v`iq_4tC>))Yoq*@v6?Jtd4E%N623Jj^t= zr5Hh)SeIM6Ai#);Pf8}FHN`1A@a-(bd9&Wfwc)mJ)l7PEjGek@icZ=AtAIp0(T08@PoL|iXmc&Nao*q z;GM27lX3=vLNELaOd-kD&0j;)HB-@OWzXLX&4(&3MWp^(?htR@q~?1TO8}wfyTyIH zVD-~uL;9rex%Q=@E^1#78|?R5lz zV_Ioqe#;D$8jF^~Y3H4)C!PAAexx+KX_4tY@lZY9WQFY5cXn&VwQL`78NTU2X0Z)8OV+$E?4-x& z>-2IYW>K~%w23@3{e=>4z;4^KoM5Uvjl!N+aseIoMOdcqY3Z!cJCPVZh!~M5EAa_! zGYfqlb7GG={-P~B%sT2ul|R``kEmc$P}N(kjA$#y>@)e)8NVmE5`KA11>vhKVGew6 ztMB&UH`$6NYHx+!4@r_*MjdX~eTKC1QmmR!_Nw3<``yxJ>9b_XstU5$AnTc{xbP)z;!O|qYr zgq!3&E%?SR=x)c4n{bUSa0$kGW_+?h@pAjUtEF1^yh>UgNs?d?>YVunQr=#CMU!Qbzd}wD*k&lh%j9Jy!(N9oZk;FZNFzSi9-B z$+ywy?6hrUu|`qorq4t%IvE??f6#1H4ij)ORQWx|V`@#L6y7O9F(2^%ZU^6aP+EYR z9Q~2HG@7gcR5kM}*(0|jyjWURlNP&LrGq0BaOm1NECGh1;K_^U4g}LkYvvsP$37$a4$YUB0C9DJPGNE zz@3*bCc=B*t6i_J_SLUWlct~ZUS8gZL^PynTFl5s?TXKdwbQx(s0*h?@0)-;S$+E2 z_7kt|>+Zp9P3AiR&A4X-5T~Gn^ZaR-s@cp2_t$Uk*y@VzW->Bg}v8x7U3rqa2oxo65(5zdb{KZt^J|O{@s8jbrgWO3xxMdd@N#l+W>y@3lQ>|_>aFwo^%%b9* zgWu(h0Gd0q-=fGhW*$bNEA9CW@fR03cunbgJt;XgX4n>Ly&a0{p(xBYLX$aCJ6y@! zMQf==WQ<(M^Nnf#ER?|<6Fv&{83|3TTpa%*MjW9cnCfjKcj{sr(?!&|m>=sF)_CFn z5=Dp#*0+*A%*;`5XJgBnBpH(TU+AS)YRMmrFmKCnZw*Pby&_K7$wckExlWFj;-?WA zUyZ}7LvX~att>g_TUV*fXiiF^)E{(3!HJM);y>>!s{gg9`$+r485_fWE5py= z6-+%B?qjm;Ar@Br)Df~E_UT9FOtuz{-U*-Jf{r=coE>7Qdor`mDfrJv4_>rg{&E73 z=B}6OgI${2bV*Iil&+RZog)_TN#RaoL2yY{%h>HNYN*IQMN7NI5$mW>705H;WJ4VA z+X^K;Io(Xezoh!M_=(0YXyE^B@w>+_@4Q&5Q=E%tfj-NQq21_4W%*;U(NGx2Y;Y)z zdnE7Y8UuAu^%^xz%0n-^(+UimbTE6!Sw7xfuDdWdfvgk=K~DPG20H9r6^RTk==1qbAF^i|VTv%H{rrLm z(D|?Ui!s*g5uaar(PZYVcm!;C@G&bp;<$V#H8{H_eP>bhjaUCWI^^gWD(A z$>?skIkIXMhUskX-Se@)Q|Z@vyu`@*rWQfSVPP{0L)WW9FZgQITbo5p6!1u;U)gX5 zl;Bwk!4*^$rFy@Q?`I>u@_?+Uvb2BalW$!5c1fO;6?};(zw1lm=L%KY4)_wrnZwpx z$}C?O;i;q6bhU8Z_+^Zf=Gtk!qtwEVXotKpq8hNT-pCCa_8w*S(~w2^82MBuO|&n? z-c5oh8E+RqQTMDgl0zm|$@Wmg`_Eqz=!2`^Oh?Qr$U$H1J?dPVbb#IDD`rM7R&ucv z>(Tl!FylRrlvaYQFWcf_)$ycrIv&2u?!^b>r=Zg}3DGBFwG%T!+h*g6p}y$67*cn= z6`T+M68~i%3Z&gvDVKsmz`sXJbd6KQIh>fb3mpcOxl?6D~+b51Cp(*!>77>!F zjtRJYR8t7J9%!;_C?7yfqcJ>C!NgsPyu#euN!k8D>m9xb0QsEX&l^xzIy_RejNXr2 zj~m1J#_B)L`3b$C4S!fC<*swF&FRQfmvjpEMI-?X4Y=;R1(YM^2HM_fT<;}U_M=nzTDV4w5b8N4p4SlYSy|){tP32z_;bqG11db;FM`f8WBt zj@v0+(UFFb{Sgi!-{PfDuulPL(@24s&nCK*R_H8Qfu>4T=Ur^W+x)inkQJ>y z&_8}#1Kc_FHhs9A9nQ^@Co z2L{%keo7VKD^ER7Rt@e5)kVE7e(fq&2P{k8@QZi4BlH@Y6rOv%6RpUWX~#MV?|8DK zi665Ni65z1O}pQrqN=v0fZ}rc9{gRf*HeZD6FOETQThJ@WtZ89c09-G23@sEkqNiuz6iZwk~qJ& zE`;2v0sRR(y*+>)`VVXi^w8t*s!5gbolMAhq;f(_b0M=sy3$NTN!Ds)3tqLF7aA(~ z)ZKHnKTMna(ktDDLHqa1dv$A?YTYV?2}AdJa_qXo*^CUU?6lFE_1xf)uZUY**)~Hv zy@|!*^4u*5I(7b%KHQuo(VC3Fo#Z3<6xZuxl4+oJa)sxQ{|+bSddak(Yc26mrYh?C zEw^qP9Y-v`VVDGTPh;|w0iB1`_L%Oz0_f^1xDb0gsoRj9b=S+`5=AAZKH}%1yF$9D zVd)WHc*QsS41>|me<-__-_d<^BG*=4S;=gcml3d-t)J)xcC=X5)x?){CmnZE?I-fQ z-P;fZbvaHdX?$`<0uic>S`7+*72F{Q1Lf##Fi@5`Jak(yWATx)AHdLOn5qIAn==)? zt&v%)FxgWLJw~oCJ-$*qMO1iL@h!{@IDLo51yA&oDVkd!4nxF&n-&8l*#qVqAUyl|~2eNz@2T*sz^LQZYAuofW{b`W}RuY}is9fjE zt%=`5qgn0Iim+U??#CDN_aM~sYwMRBW;X{l)Q(;XCPEtbjt9GrpV|-_e#s>ux#8uH zAVNoeF4P~`Jd+yh&}s7J2c7vJQ_0K(aU#J!0wUn_=!BZ}Hy#`cYNoenxh<~qr=&PPvqgk?h%Y5Y~Zo7fc}Ip zWyau%)pIrgOI9$l(u>)FulXtf)_9VpT;%fgdn9d9m%#2pNbiK<4#qYIJ5w>Yzpu6~ zQUL@stp%#aFoy~0Rwa@g(hld}X5grQ`+g|BbfGz5+IM{^=~>Bnb;Z)aGD7wB!RU}l z$~2!VMyKUdthS^iMEjg1#FP1?+Gqj@WhC5j_zYVBhn2yD4zcU^y1dH7t_+5Ur^QRB zbJpQoaL_N-7CpMGBF|b^M*$vcE@OVRLi&Zj&VXlEKWO(gRjaGrQnRN+we$tS_A8Nh zQG+{fD}Hrme#G-AB?7<2TURISjh?*MuDoogQWBhowFR8{p+Z0$^mUXt_Ill*sP=5} z)%@|Tz?2!IyEr?e$%PAj^PUMB(1i>cyp?uf7AbvbtJMtN&d!-^-RMSr_a9*#tyx<4 zbxufqoU#H5!j7Fx+mMN9)G09jXaRV{*ey5q8pt`?t8RAiN7Zcy`&{J@XQH2ZQPI+Z znFMUzZ<)OStOQYM>c%aIxNPytt6ys$nKfn*yQMV5Dr1ml^ne-ADRObi4X>D2>>T1` zwO}yFSYZWqx`-w0BS$y%3KE&!mc9g?HK2&c+|sM1dvcg5;P@}@j_pV!!Mn8rD8=@* z&5t|YUFEWfa{6yVr@Ja;51r>8`2G zb%+tj?C@>br^;^SD&Ca?J@7kWV4FTSK!DV9kGLT5uV5K3@9jNn7n)bkRf(dsP0eWz zj!HQ2gl%VJ1DP~cQbeODV{h*<+Z_!(7anl==y92!mv8nE-q}1|q{?5KFQ_d#b(A@R zuLNyT#wl3FQD>y*&f0(=gHXeEsD+KqqC^&`3yQmE3SH5tCz-e%p_gVJqJBt-PL6pi z5?n0s$-W@^78 zWjV~FC(X&+oH5jzARP{3Z(q~M@0+X+j>#c9Q241KycsfqrMh=j3 zeJ^b4C&V>+lpLUgGhP(Y#fA|Z$WX@9%-N1!5XJ5GE3RjkcF5!7b>TOz>!pVS>G#wt zT+YdG@4KANd5SBK2oO(h>cgS2**W3WM_+yh=++=utbu%VzV2$huGaIUbVdL&GjE%# z`RXEYL87tQCMt*7;DX1npAiO!=mGZ zM_Tkx1U%?W>K*8Fa=MQk-{W`SSY!oAai^ip(>lwicP=17PS%t$IH30|e2ZxF43R0{ zEnmVnnR#Qu`%_yOI2R1C(=8s~3e|ntU(`9H$|LHY3tipH@P!#m14nQXLnFa%2fbt4 zvIrr(g;u5uFU8k3u@ya?d6^j`b_++AGiXJ1A1gz6_mK;T#+vuI`}&kSu^1l}I569C z5_B>lFf{SZry4zYS}VUdT?uy{=X?55?&W@KT~PL@_vm z=7Tg6ooj19?b_!9#C{kA62?Xr^esu!t5jU)DhG`5+i)`q$s|`tYrnb#9jmC+eYkKX zs`WyYz38moE`con_EU)*xWq2QzZAXEucS7ObMj@8}!sSgb=NwqX7R1<1#fei@kpy&F zGcfULL{9-EqMP1G!WT`sgx9tc8Y~fJJ5nz#`}GX~uD<^pxbnUMuI_BajkO$g%>|-& z2Ou)Y-c!+pUNUO#2VX`so%@$?>YmM50hBNkeu0)>ahINrijbKr>1{D$J=?~vao($O zx|f?eaYkr4nbN&8H9W04CyU`P zPL|@#P#dqDF97}_Nyg)ng!waRt{76FO;LY5eXT)s9Xxj?*3hZx(=H(Eb(CLSy6|o! zBaiQ1`wGhdhaOn1V4tf1Y@DB}GSB*+L$G8|)Xp+6-V)Vu>VgFeoVYj0C9N%_$|xHS z6216H#z3J?tk)SZC)V*uww!{Sh@z?X193Awx2`T|0k04Dw_j6VqPaPmSG~&aZ3LKj zbu>N}Eo$mV448cUD)hV|SEq);32ROZKF4f>QHrRwo#yjR^}!?0#y_}A4KJ=p!$qSVRt)!H14)06sq@m`|ecXduQ^;+r=~*_;J_<-3j&i zsHE?W87V&IGB>|nykB2^C+KEq$woN9yCnz5FXLoPpTY^`^WVBt~xhXf4WSP z;`bdabw9PnFN#~!bz;Cs*W@khT}y27uzvr3Pnmik&TUT3UGO1`cy4#8>{g0Qs=ib; z?Yexqb2k~iI4POLHX~Ie^;c+UQi~DuQE%ZQjk6{U*fOy8ljx57S>vnJIj|OX?VwvD zAj=M4--BOcW@?&4Wc9Ay_D!ss=R#)#np&+m5p-)l8f1>BI+n+ev6jUDfg?gWzqTre zS=ir#(?e45%*!LO7hxdWnAKice6Aw&fcfB!m^{!bkE zW;aD*I7WVBoul?HdQF1|-=)4SyFU&>!h$;NI-KMw3-M}@A zcWojI-w)rqzx6*5Gv@td(KJ&k+N6){_OIMY5~tYV0q8g(L`w^nWeQZjh#q)C4CIST zlB*pD?Ha>1Hb`%|H@p1PU7Fp!v9sYYuIF!+rDaMtQY8YGklZ9N6TVBFvg_(CyuBkU z8r1yyYFSDZI$^v1%%=;TE-)4hJCcm~{2)m%osz?(oGa zXxLG@j}Q$+?_Olko9A`0k)Aa-&_SHzPh?EaB6xt~1To<_ld|+1+6hTQ)bp0#I64iw z(!<)XXZMQkRMEL*6jlV>D(%2Bid2UT^pYiE6PZL8UJ9EDRx$sV8-w=e#-NsfQ;rAcInPtyRCS}m*9=HAUoc=W(ujMrUm$JuR9IeW2x&iQy59!?@_!mbFG-wvbP z?!M~21BvkGuzA6@pISdge>cYAG1t&nja9O12DC+2B*@|grA)rd`Q{kT)C+8m68vf> zviN4q$S)h2x{vJU9j(W}i#G~=@sxGJiAduI)VeH5+)U?~5nnZcHjYmOuqJduQ21Tu z)*A|b6M$Gmj(i9rWp;4tYno}80~17GGPCeSQ1Y(xRNL(7NG7|bJwM)V@~+s~#8e<5 z1wY4vdviBgQn<(HOvg8bup=3bOf_mnxC7=8?MPG$$av99YEB1Ye3hq#J^Vi zjp>C{gzUB5Sv2dhxwPNUvVJuwglcAwg=?S;8-k+Dk`u3PuIh;Bn0(x13BXfMMST7&o zPYG{5hN$sNpQAyp@mIWuIXKJ&fIC4syPnbanfm9nhhnwD1R>Dd@_V?G`!n20Bz#j* zRrTbCgE0Iix*P_rx00DDPeau=Bx{SCX8L}gNYEa183wq;2*54k!KUj)f<~UL{ZuoZ zn;n1^3@16k5?7S`_eX3nk_d{l(|aVY2>Fp4*|n6mt*A8>IDb&|2I)c(rWaN}@1-%T zc?O#(g92+o79SJ&bd*>qL&=zeU2=-nBAiq%>XVa~%UZ25x3ub*OFt0*EVmHHUp0$j zYExLv1pD1u*E#q(YiPmFePzDc?tO_}qozA^L!VrpGk=sS)kXfdjDh4$Ii1S=LSaHE zIh`Rpx!uJ^xGr}{Yh@CKNPM~!s{wyP&M{oj37)WxQ}$s}@`>upkp`XeleIn6+N-nm zsJMyS$wS-%cS00b_+9X3f?dzTues*gu~=vz_KEqLkMe&Rrisa!Sk6sJzrDQ5;jq>j zNcD!1{5)yR!dB5ZcOSZ9r>Hu9eic{UvT`3;{ejcv*_ysGse!d)7J{01h{<|KD03Y+=)rH zxaU-|fz2xpd(r^>pu88A04XHGr1Wtj;jpYH=HIEP8fk;0;4L&7M8^M3lhkh(6(EYg zHa5mw(IsuA!!2ESFy3*SN}55DwVx8ebc~9r=FwJ#l5%!?C3N=h7Ul0CM780|-AM=7 zY-DLzX2!QQUB%@m7CAp5rExvZ1&i>Er89@jIximq%F%DnDo_;=HE(^cl^ROd$mQ6UGpKO;v`9MLK@1~Fq?0TGNs^ts&mY-5 z6ajagGVnU%_ocDz@hWMHI8NekNNhqCl zZ7WE^N80t1N&A_`i})$Kwvu$F&osoQ=al-I;+?+MFEY+xHc7NOr zQ2jC*hCzNd_ns|w$bmkcDx(LcR1P-e(!I@V_zrgXkr`dftXM|R0T=nJB+~1?)C8}J zPnPumo(B}#gv&%;toM-LP4_NzCoTT&Kv_2BC z5n-m0e)tu7M%u{+f+O$VcoZ+Z$0?~M?vr~La)YUZH;=3T zwR&}vqb0(gHkUF#ml!UfK!~DKD-5fO_g9GhH{tP%S!G-O5V3 z<4am}z-dGZa_*xoy~*@5{#_cJ;@kk8JWbJ(0h7F00to7%;4EmWX`Nktcg6sr*MDOi z0r0yOj=KvSa2JbA*9^heV)JKWE64(LhWPz!h33Rak#*Xu_iUujJ{dy!3@R)?*OF@O z^`o`1VooK052*mX7w}3c3*fsC$X~tFkEG0Rt99?s3IY@_G$FZ& zK$yDPuocC8<8FrRr6m`|hlCFZ)>xmd`jr0ju;=ETTlV!db1I^xPLcV$;E8Fde)0brl7%#$&d1 zxp$UYKen5{NZppdqey3BZg&E(gn&lg%8R*{I}~EHc1u0*=50c!3~mbHZ;qS*&kN)T z#{w70txryRpF>W3RgByphPx)BZK{8e)>U@UUmz5EOH{jFSrtpeM_pNMgt*1>_(egi z6gRHOX3k>gcdGKCtHZoS63)}NMEJ^T34K}O_p|EY>aPz9%%*G3cV_2XF=g}f64!i8 zy49m9xyuut?})_oDr4ch1f!Mxe53~SXQH$$eweyGnXQkD<$gq-+;UUUJ3Yg%H&Yb3 z0nZCxe;{TkYYIaauJ^1SdSdTpV)Dfa#U9bAg6Y+V$|(wVJ_@yBiUy&=u41G4?8Gq4 z4fy6ws&K99~Rj5IzU5l9+9U0tUK6&rGfXf4yOcICBQ-4%g z!=HXOj3^`f*GK*ScXFPXvhdm8H*r%OQ7zZ;v#9#tYwd8UCA zTy;a5-AspGL1RU7iW=*F*f!v;>%2hOfbmP^;pY~I{T~7n-66^jr1Q~sWDiID?9v<_ zk{L1&{KeGKx|r)YU`I%TiOX9y&LxiZRm0a0&JtF11Wv)ZXQ|W99<+n|Xu6c2M$GRF z;cPi+ietaBszEtgWl5GEYdC+a0(s>-YexoOqxpo!4G@i<5GIc6_{9AN{eT)ZS*@&8mxqt63wVCYkr3HD%OxMHQew%4j+A}CA1)iK9 z)SCdKH#=n)w*d^vdNuc|^3n^`fx}WriPL>Ws}p3RiHtIA*21v@G*4B0pvHeBj4)=o zwxp+Z4TFE9Drt=iN&KgU(4D>d%Q$LFc7F39aw{I*1Ph@|*2{qZRZYV3swrAAGhi=I z9a>jAt5d4|unu3l4a0JG7@|T9pC*N-+=djp@((k-lh;G^-?t}^&gGt1mk)^!_R{Y#_4jduQh7z zm5u0##hG^;2wHy8wa<%+-lX$GCx^&1f+xN<|4*f*5_NI@9e?^L81#EaZl@BKWj#qH#G_SU?a;+$S;*)&9>-BbcNa7$~IObM~=#!q2p3+$2Mvf#@+JHFSL@Y5qdsVk8~ ztg$3q=4q5y360C<+uE#I~uYMNQpmDT(>;vp>w0iOnK}YUG{(d08 zw<0WkG)btzoGrvn>Zb}@+4{JVw{eR<$o?qOB8y!xntzyBo~OvffSxmg7v-S)V3WpX zMkLytNX!QlPmvwf+mk&{PEwvfLiqA&`VzmsD}fu626-32c9ztRIj~6xM`h3(lWGl4 zyL;@y&CuzTgL4-g2KHv zr35+zkp7|a=->+A*lXNqm3T?Lkg5o1Z;G}lSCJUWKu5LMgz)2yic{R|0Z0qc9f(Kp z9t7Z|&s|8^Y*0GwMUY&%Q}+drrg>9;`)k$p_tD7kTd(EOgH~!#MiA}Om|BfWxQB8h z>|CqbVGfh*0}_jWDI?PiXs7m;s%nh0wN`c}#Uisrn>b^N|0NbeFHtC5w4gON;)C0C zNJ4!~FHZ{L@v1nrlC$&Xx{HrTMCSV{8j-O!6CZN*UPt?lYxsZ4gfQYN{>qPgCp|t- zBAOcFUDWw|934O59QWnC-egaH3qLpL^E_7;{2v>Symu>^js7xLP{=GIt_lahS-&L{ zeq4Qy+wOKM=J%pqT$9r=AMQtc6eJzFLL{Yr@4@hv3(kyMq^QIlvvbT6Ak+KWlMhGQ zij%Pz0h~Nw3G0zaDCrGNey{Be>x2efY)D$F^vS2~?p_Lwht~?Rc+?2*xAX-bx9vI4 z;?;&Fuul?fF39Ny>>ofiHD4`FeAk<7F(Jsc(u!(zN9KD4u(Us3E?!(ZwN%(85`WdP zEa5d!`HYJNSP~%`n&yCdft2YpFUD>@g%y9?w8&`V1fpJMz5xzv1ix1ZfRXzx#+vRv zx%EG0p0|wJO#Aq;D|>0c{Z&h^U@v3)D(lUM#U0nd{rX_;^s@ZLWW8g0Qc%Ah(B!8% z_Npi4ozEi+K=GT&$HdBw+7Zm`Lu=)9f!oE6r#FGXX6CpKUvBO`RQH)5P~ICrHiyMC?XziA%76z|kd-@4S8cOq-2)_1rnDA3LacwJ(P3JW`3heWa z3)N|aHC)IXFz)NriVGLe#nXE~_bXL@93H<5Sq=!ais!!^+WW)yovcY?`&I^*xLmyz z0!K5;bWulXP%P>F$xK2Q^1V!9+()JVGr+3z`%tT9w#u(&qy*BD$a=7CCvbGS-E z8=CE=go1Z(&^>;zz<3-h9N84>qIuC~bCt2&>`gN-jqMkt1sG~W2SwXm-~}uRgQy=0 zRJ`>iM(%A_DRYW6;xRTG71bf}i{^@t^>9P(h3kGtQ+}X?nYYMa!aBITP%H_B8o8Ihll5V#8(z3m$HjC($Z*6lu z?RV$sc*sU+hg(ZPnNH&$r_QsO2&ZdT?XNX;Lz7lZtds7qntADvZq#(UG0wZd4W?mF z(wtzln&PnD&Hr8xH%rmLTU#S&RW#|QL@)VYoET%!&A}aY=xZf9_C1F%-0Oe*XS@Y` z@dQyBAn{vQnCJIkigzCRFznNiwITxR+|A+@xTqM{g8&xxgD#e1P+|r}Ot6_GYsSu4 z9>wvw`iU`McLq$*4k+v~4rWDs#~r6o zSYtezzNi~hM4`*6c>Pl*C~M0R0HgPC*57y{eiOa*D=nncfE-y>R^Cq>CAC?mex6XL z{#jorwW{$yR+=ag^W4MqX{4^thw~wKjw}Q!(kgE1f2896pZk$T|2Gy4a`t8wy0{dW z1d333>{_r{wSenK7&xJ;8z14GFQgeu<<1bAyv*tzPx`4r1Y4P6^>ZgA1p`hSjK~_J zQ@UeiCA22NHfAlKbFu>qt8w?xQMtR1{;#|R_q+!0u`mqp&*2qV5XPL^3O}A`C9pFP+ zmXUX~MQ)%tHxh=kM?=^yL*7a}z>YrDX_MED+aGS9VL#7CeSu&Q6I4G~qhNp8rFL!D z@60$k?e6X9_<;3vg7uVURfF%@14XhoY)Lz*2GT(_?KNnk;-|4e1^ls~!n!;7zMoM~ zHeQ^Z(UV+t?8v2C&@03^{mKuQF+^Ukt7DNN$5xf#;vuE~i>5r8K$X!VpA#NX~ zV!-VzUtOaNTn`YPB0sRZ0G00-;@4csS`7{w-u|w}jNy%plj;nd@tuhDWA~l{b5V@^ zJZ%d&r(L#_+Z--5?hM{J*QtAf{0CXyIj}$>!Vy|pws=_Q!BW)#=QgHH@OS=CMD>H! zeTjC#xp{M1T20?l9vcI4USf%{Y8zpg)oak7MMtQQ{X)<#E{IUT8sd!WW6^D7edjz? zwZQUAm3C-h>jRdqL|vUqjZy{1tYtTGMR}>51WEIjNvv{3*wZ47P5L>SwP?f0)58lb z{?T33F}{Vm7$YPRBAd0F5RRDAQ0b)Rhbc+VC$ZT=R*6zgDCq&j#^~E-UQjG%C%*yh zJ>t-P@vQv$&SKGI5Wl~^dMQwk*h<})a;ru6zJ&rd7ThswRQ}P~pr&F8azFq#neR5S zqZ5BtSo@RANkyUU@td0kQz~1uCcavxDKJ2iZF6zk>Tv%gHfVhg3KlNq zrmN+^(ob`^YfBEZ;rKT@j8f(dZ!{TI6AgTjs37R3ydo&HsF%&BLSzpI4B4Ab*v-RO zzK>_jYk?)A1IbnL((*voWXMuuLb@0E-3&O3z}>g3n&xP`@N5Qsh@ib%D$ zdYJk4r&+(c_;UO+kz>IU=X+=2S&?8;SJ$7R-AfKEUrH8bny#0oH51 zbvVryP;%@wztUpNXMSU+A~!|6UtDzBlMRbMTn`V(J|QILxZ-{RK#tEyj4NEVYP&C z{`oh5?XK!k#egxsQ?Qw)#6{Vi6Lhb63@Y!&PQ1+@OouKhL}F{B06ZLvIB&K6-o^Qa zTmtvk1OVfGIM)>5myG>&m4@gF*y!e8|IMa=!0u;}jDvBjc>jF?fj1p{o}=Bjn%9@E zI}@__Tld)^2-&4xoB-VV)&olSM|yKahuAHA|>aSv2(7=PRy=?w2Qn}79> z?~^-1bX7YwL1iNQCy7hvGsuBc zU_a1-fXEM$Q}qW_O@&@YgtjTj8B2T026IRN) zX?})9?E4&AeL&@PxY(U(b1(gi_H`F7|2GiV5kq880eA(aJ?Se7JqCoi@JsQs4c<+@ z#7D&&M3fq(4rKP9;r{>Hp|;P;tbUv!u|(ewL?FD_4>e3PlM{bU>@!q%hG(CK;@CEE z?(jJ)`>+Z1FQ5aa_yjx1p1YUqsiGRb^P45}pyea)b++R7#)S2fRg5$`NvqH6Prfyn zX2fdqUlLp$#{x1T0c|C@lycEE>}Gj1P`%Rob$ z-Fxz-TQAZr7T+p8bSwW8G1Q((xjJRcB20v=%~zX`wQ`IOpE*N& z9}B_-xu|$?^GU&f_4HRG&f4Y9iZA?{yy(*+i|eAZkZI_``=LZjIs4LweSOc^ly9Qh zqzY0f=FxQp_>XqLiSIM-=eBTwh`86o*2q5LD6}0hYH(y!@R3F2#>`ji<2fh1xTKaO z$Sag6nnxS2u@dd#0_i@n-QJm|%Yq{^J`-=84ph5xWL#x}?{}OLU-GLMz-FRha9BTt z!D1$j44Hzw6;q0|H~sA={Y`Fufi{azq_u1}wGfdLDX*WIu%Wh5K43g$A#+~BjgLUC zxsp1&5JZKdDHf5|w=si+{!oH`Y+CO&eDkk$$7nKnNVMTC4Xn|ag3Alt|11H^VSxuO zCrZa4?TrueVnv6)chX2#EV&_@EdO9=5~s&C9I#;CL|#R;^jKe@^?)fa@DtVA&TZ8z z=v7YW$A}^=`Z6nKjCT8<){5CU^Lk^unfp3LU6{pF(; zw*=e?*BZ|hzx`A$$4gm~>`{_=z%$ud_GhS3#NQhK#->QK%TS-PlaV{xQl<_+Ygt;a zI~7V(0MMJlk%+BV!?~sorJLKZo~CJ-lZDRB+F8 zSNqva%ECpPS=iY>EzIqhri_D7?5$E`)sRQhj`_FvEL2gqhue|9_Q#8IyL!DwN~K#Y z;QE=@8taDL7uh7~kDX&06!G}+`Sps*%bmfT14XViLOP=c`Z{K3lYn`?r#E~DYT%P6 zT>w%fR3xiiCY4G`b9NthDE=ZdhG28x@qz$HJMoe*p7C5&a^_-W#98fgtlveX1(uM0 zo*vzxQ#&&87-MOomZKuu`?X0h{usgmuGYA`l162PP|VT;wqaGj1T0^4kg~^;Q;<1> z$$@(3(p#|b8B8pCaX;SvNXTx`WH^uTdQsk>+Qd)OSKoFHH3sW&sVBrMs@x;_enz8# z45jt7->sW%TuS}jmx~%8iTxnKWA=RzH@~Diu#!(;H~*&NZ~P8=*$E0XSYE$iq8`Lr zKNUx}IfUFOn<#DDd1#?^4DCX>pn=){Y}AQZ#leDr`uN;nQv?<;izjB)^BdRMfY#T{wpXZsa)2@-f`+1&kzFjbjnKHZ;>b=Dl zeIn&5EulOHGvR%CiY4alUox1K*<&vl87}htUe{<;rVRHFl8+7TAb(L;jdK3>61*!R z?!v!K=G6r{n4~ZVsC<9g^$pbI4CY^Hp{ONGXdYZNM_oMjXA^E*KV9Na2JAxSTHRzTXZhtA9GG)P*TNXa&;ZrAwxY;pC2kd`$Jb8CPo74M5 zlyxm`I24ZHhaUI!rDBViFAXgHEs#K%wgqY3c#c!Bn(Ge+sZW7lDcLmyj1q32B? z&(ldf_#6x|srhH{T1ZPgk~-};#A}k3DB3j5Dk4}g<^YyQr98U)eBALD6Wtoo-nut) zMm$=C$~w~Jpj~dp#8a6ST+0O5!As5*S)}5zTrnyQg_o(UgryI_dR7d zJSq?9_8TTx5UG{zwkHb|ro@$wz;m(k;#_T3n@*<{zMu5W84gZwi2=`^_KtxK@K$-v zwPN$X$9dYK^NjAC}m}3zcBy+ z&|C#t+Cx6as%$JXDtfeV9e<$rF2qBMFU|PegW%_V&S$@ZQCD?`|GB&)pdS^ypr|dZ zH)jd+x`_yiuGGs)AjOkon75*1@PI={77R@H6gJ)*OF!zi?DPKT;; zQEe~yF*Bd3r7XN@vJ%_2jQYqbtIOz769iWQpD<_aJVKaU!0o0VYq|G8up1`&YrWXQ}CHbEO>31{@5CVAn^NurU#Btu8)j?PvwBJJ!rQ>y*3@ zAGhfTxwte9ND@9?pYWdFC2~E!yu3y=f zg;ZLl8Sdrt5JZw27Ej6WS8Bvl)@SK;PSi3j1WQ8j8z}#5Y(4a877Jher_c4#z681CXrp@ z`~$j?w}(>ZDn#7dSF4YY@o<^RtQF~i$eGgpxWF-FWstHFdD9SY_R*$WHk%CG2vD+P z>A5%XTWW9A=;Ha7q+jer>jGd;V`j@PSuuYrxshIVkn<)cHY;_i)#`XORuw$Sgh4D1 zilvFPgj|egguu07y5jK)=xLvJf4c%bP@meo5{On(eDQC%jMg3EZgbN(!WaqndUSoX zsuT>#z}wt#9DB{#Y+P&7x$MaTbVR?QVrGc!e?8dD|#YEr&34s?a6+>us94pVts*K(Zt;Xzo()F=*%@PoyGnKP!mm0a# z&FKt&4EzZ@2Uvrx7g6I44UG)@i;~HlsQn1U2@aL6`FLco=fPr+)Q1LS!n5`nA!|Ec z;OwOXJ)yp%q$;89Ipqboj4RQfLqhVmOQI*)LVdd~;2y}K{(=0VS28rcN+Q3eVO4#0 zHnB~*E4SD#U8sQEO70ImTc|W5(bunPcGkXr+fg)s!>gun2@*R2A=_}gMZEb2z92LHh zt?2#1(liPb1I`J~y^CnKXPIWsj_`bMCxMdrmNNB&2U=y@b&Toe(a?*ZXK~nl5hwPh}$nTt%5DS)xiD&j&9|Zc*nn!{c>R z5)k%3W`F;F-!7-IIUt``ROo6Lx#e|#eR}du`0P7a$y@v_w3U!(7y^Akd zEc%l%gfftwS*vO3;~ZUoo=6UyOQ zx$W6N3G8QZJFM}G7sU2~BZ3rpYaQfxBzn>JlP(}VXodfs8l-By)G&?vR+2|R%f+@T z!K^TaO^pAA0NTBkkj6sw**jB@#HPqgQEQhvx=2L?fZxyv?h=KiS}VEQg7GJUtehn4 z`1i6e(+4ONbNCgA&lC~Xh(32zL4=OD02H~ZC4}MPUugXcgdrvumo`Wa3~FiaHfW=C z7ZeMoCE@#7T>&XUPQNv5m@{?M_En!(_~kxXhkHF>>WSfMdy3nTtT z#Yb7|(sWk3hWSB;vqdk_NO)X{0V4XOT3LC%x_DC#8L(DQ7LwGf5Pb zmk;i$>@dx3#$le~(OeaXQ^v`#I}&$<64Wt9nDmMDfd#|GdG?~>3Fr7I59j0UtZ(bh z^(i%3#gvJKKhzJa8OZ~}XsI%mwV`ZaFyt$$XN9b-n#ir7eIap#JyN9M4sk_uMGyM$ zo{$%9Q~{8hOtS;ta$G|hNsND`wG;BoZkOEESwd2VkUBu7TT+}5-sW@W#iUoGk`^C6 zX#kttmQ{Tu?IEGW$Nb|F@Z)=T*Z%ZvWx^r$b2Aj_+U|VFq@MPCZlas7s~0DnUh`fV(B)epVI3e(3ZO~}@$^*NgTRYCM zEfK`%yR}ch%8JZ;_&cW=`j_U{Pd-&}{~Jc)SZ}^l;Fyv1QFp|jYJx8K&zIeENw9Sh*OjmQU)SBVUUDzPcHVl14?YEAP2!Y>^^trKy zF1=QTPY~VjVg#=?GoC`RhDds?+^V)|8*Aq7MZ9InXFRh`#{@eqea;&?)v`+?+%xR= za;k|~gi1Hb4FuFq=ts9S;F+_f7ug19Vl${aB4&5ub6c7$R$nKFk<`!4=^)qF{SAV` zZC_!MpCs_kiVSV$-ucUaTADfSzma+Jw8F<)i23}%MZa>4Xr%24L+u~irlyCC%^7D#aq4UmBIxf2}p3WbxW?x9is-8f;O;EwGCux!#Dn#?Um00Z1V}ac)wDC${sl z$AYJl5#pp$M`!V_G4NV5&#K-W28lJBX1!?iGbp2n=r#a=4<64T3;<3Ri#2C{H^oaD zv6tz+2E3WV?50S_t2yct%ezG%6!KMqN_8BT-+uTbV&=NDG59**);Ng_tQ^w?{G*^k z+6D1!P)X(*h^L4rP^m|{k8UgpVdyvDq^q0fPOwhYpc%b zTgB>Kg~MNnhSmR~xv@MH&#CP0?Xep#-I}Gs4+1{uaul!x7B4A&Ax4-C=nsw0e;9)a<%Zd#Y1hJ8%ahGt6w3eX zu_1yqJO|SF1#}GDC!c;r>JR`@^9Ra9Oba~@H&)O+KC7Q@aB}LDRe2iwH8ot&yvlMq zM`97Hmj$(Q{CneFa+(#7i(4pbTC^F4Rl3g392z9mBUdSGVV2VxeyQ(YzpOB=dpS2D z-3GyDxx#G?KmO04Hieud3 z3y>t-s)^~vyT(9l7_`!|J$7n$k4Q&vZ4BfiwrooGHusULz+^3HbK_5P%~t3fYlBIt zL&4%}D*Rq@eJgV9VlHpxt3p0QG)?w7+u>KRT3J0Mn-vgDVbuW)TD%y~cZIJ^~#ut@#p#kKnkmgx)5^d(DcscK(gNZHe93sqJa2yIr9pkL%a+Nn7 zr)q5yz0cJ*;T9NIVBPC`@iOHBEM2M2F$Wz3H9Bf~&NDNa$q=4`INA#?^K}2JghOQ? zf^5Rn6u@ON4eVh|PJHbiEp8c|-Vr=$SwfokTsiJRFao)ZFuH8oQXiotcIhjF=CN`| zdc`h$c03p$M%%x#HwbwDH++Qot$Y3VZ?UI2a=_2))wa6y5KX|X^r^U{CLK-=)&&*2 zIMybT`4@`l&sh-&wnW>#eGDrxpPk)NDTFSYo%rvkXZ?=t1a`54$bWxH@$E@?Et~jBHZYUndw6pAKv3PX`4ik!Q9!N$; zgC-}tx6@z*Nw?739mjKb6=;vk+D6^M7kpzE%izrFTv*gjw&Ck;|5Ha`Jlzx%X zy^7E2qIHssmW+qR+_0){{AYf5z3U6;4pj+Pam#^vNOuw<8Gv6VBL-#M$71w0)sKZ*9lJV{NkicDCBH525oC}6wv!NiJasf1`rFV>*= zAXm&N1qzaN={1g}6G(Opm61@R(ijR>#!8>o{c8c8EpdE zA5eAIz)+5o0wqi(9}dq=*`m5IbuB6-`QSemptq=YaN?r3C>h(ATnzd9=Yci>ZCyfp z&_&ExjTPv zavBwJS(7iX8(?*4ct{+_^-go$co=m{xUv zY+e<_LP_y*Pwg4XX!DI4J9<029b?26#e=ED&^yVMAD*ZBynR=ENQ*c2NG|+~V%#tX z0teWi11l}cr)oc8)4YDi5 z?o`M4J~($lF1?Ih-yXjMy^S2lnl?W6s)Glsm&X!iBcNaZH9;yEO8#?8)qupBqCfE~ ztc%*ODcS^%vq0}t9ghR$dUbouF}w`Nwd^(3-CP7|GmmYYklaH2O2eyz5}Wl?8ECfM z9}6Kcr!+6fa`EQI>S<4PB$8_AOw>i?>mHc7#2=+a*=X5vTt-G&cMO+=PblVuV;T3} z%oC2vDy2)B1I0fO6hbNe2NaTmB4_Np;+zp0F}5nYeP+&3?~ImM@Z3bbBoU6_Ejc)~ z?LK9og`S};LFin|?;}d{c!^c!j<1FF0ININva=|(bl$6-KqgaB&|yj)<4ZD{Q~(+MAJ)d;6zg#fV1KJ!BHWEPo;)$3r}IrgQ-XjPh4aYJ?4jIix}V0^J8RuJWp#Z(CYTlR`0#7_Q5?gv8sjnjI+LWAO=Jh@`0Tm z&I47CBr>abPW*DSfJp$are`n3C|RjdDgRRK@r#6(B~le)AvG@9P3XOL~*JiF)4s@-e1np}O5LaEre`CC^YqsXB7XStZ9 zF!Pz|N(IATbnw|UiTDKdl9Fn5qvC-#yW7we5GS*PB8s17K_%kRZkWxK2C{t%;_|;u zWL#_RVMBsjF{&_&*&^x(kK*$;wKq9-xkl|@xaQ>O)i87M!l6%=f(5PW$?DK7tjaRK zBXZ-oS4eVUGGVBr_Nu(c(~_KrnlQ8c8=H>wV@-&v+!HGBXcxoo3hA(R}rG zBcV93JoE>h+%F#W)YFtf)I&YpOBs&fm_n_2^6bRPScq^*p*Gp(2+i(~) zzLGwhuW`a+d}w8MC}G2I%BR_;YZ&<=CNo`uc_^S2(ullP0zT#bH<@EX7DH z)0s3C`%jB{OGsk%;h`M~wb;{(3tBqqSMe`DS}n2;5>DlgS`%*bVPtrz&}?glZNUp- z`Z?DK>Y-`?apTD8PzLIx#`?0t>c%`z-mY;;J@UPL<=}7Z&nT^Q&u*iTzq1!JX!U*% zv=&-;A;wf$zCt}7!%!h|`s>?Z6r3fwVPa#N2`%CEe24|aL+Yb-JVm;Ac*tBbduH#+?8Q*ZUg&EfvSnIx4%9-t& z*_iH-RME!D9O+iPNq`K(#f_StBuJPmjcDo(|#fTa9WG70653FHh zV7xuJJ&=L z0!t1jhH|SZ39PQxD{todBt37J_v_70RvlOg32er-#NC_x`VB9vE|jxk>uL+|^`3HS zA0OaDkpA3mZEZc>fAcNiUr@!WULN|5XhsgqVT!XMP`!g6rLYdK?U;Kz*4?jvLWe-w zlBY`Zev(mr3zS%8lfV({S_u*d$m+L8yR&(yQ8B5ye60Iybccej-(oMv^;h&4umbA~ zdvj#!rNOi(&=C+Psx*&Y$0TYpFmxpzHk`koPbi)Mg4KB!A2twxCwy<7U_I;BFv_aG z`5m~0pTH?R4pz?*(*0V|2bAwTifPE;YBxn9FItoG9gm4gfCh>|C1=p~W%*B_|ERl5 zoD0_>l^0P;!8Iv7Kw4AzEPVE$K=esrrszuaLWV|Y02+bpg}o?uxGc0|1E1m1{N+2E zmIF&}o4k9Z4xb47`<&px>sGc-zQOcuF2Dc>`ox{8qT}Ae5&ypQ#@ZUL>=~GVtHr#N z?|6}MPrPca_=RNbv9W_Fd4 z78Xw+mM+iwF|?HO=K0i#Ug5@VyMtkosb@Z1O(Vq=?69#czF31*{N1rWPj%)?OTt~& zg6E-4*;Q#%rt?Cvd0tn`6j{_ChhQgY*7_nvS|V2gF}9YHXPey5Nxuucb}M8TiDI`L z7bk|)e2{S7tKF-r0P0ra>L+BI)@hi8n9Nl|I7cJE;U)w00CGRciAMV7wX#kRMM}cJ zVxVUU-n{TazTEe`czyKY-}jbPJMH7*iu;UVdNn^@)kGiz`&n*)AAJz0vu@6=`dEco zdq*R^CxN>Jr`rrqPVAz|7k`pKUL1!`?fQlEVcxuOR+97*o~~DIfQ8jdXBxBpq6IWKu@|AEOC(JAB9Fsyc-zlnHZf7~ktMSxYC>a*{8Hpg3V~KNn}sjl0TR5RYObwitj$*? z-+|=SXKQxw498eI0y{CO?cVl>0 z)~28}cwO3lgcHj1i^BISy^D6GaPRJQUW~Z_D~-LpycdYv^kfs^k(zI;yG$zkn5ghe zgaQZ#MYMy@6%&Ovjg!znQygX+cjVUFno8UMIk#G`%2lDIQm@x{CHd3fcf+2FiBl*q1j2hQ1%zvLrwQVozS zVPVf?RSyl-e|M9v&2BiupUw;s-@%^K-ywOH!RneEIVG$ohB)PyR5dsl&Ae((1%N9m z>Stv#H*o>96I+q!vIiM-g&&2q;&?PCC_p-O_edbcKU9G>T|AaLXJx zxON}vLm7-7kmw>>$x38Bf)X9Fk^`B!ia$(hTWx>~kb289HeU-&8NQjms@XIvAhgyW z#5s#FtP=`XL;*g~JGp#rL#&xKe9h`y^0~P`b9PWmVAc|_#nwL2ra*&~*?VE`p85T8 zj5!v<6@h=cSV><(FQiuumZ~Apt2t!A1xAG{gp5sUG2eRr2|9{>SW-EBx%RHsPu&X| z{9PCW5JCHKZk96%OU><$A+b~@g3#rXnN4RW9HKA-N&)svZJ8UlYnJ+|cT(cGx=mlV zUj}_=O6Cs2=?`m}ty3tDVSU$`O>%6gzofDDx=~zsl zEn#E8BB4aE6UW>>ys29C@tGKj6l;EjaknOc!uzj3K7sZt@WXEK2jkLX8Mi3{6Bxt% z%dUw0Ov))W^YM=!;r^~yd>dI;D$u7Be^RJJ=SeIwUn6l`j7Roe{zp$Y`(QlJaHVl# z?p5I}^COi#V_WkkI~q7A;>}X3oBL?J2ZwFpPE9|X#u=v>gVjVjQSEQAb zyK*94hdcK-#vriE$&q};{8|_Vdtx3eYqHnc`=X3ETjIBJ&CfWULivsmBI?xZqBb{* z;$v~!6Y_SmAGF=me;K%A&u)@O!rFlj;TJy2W$Pm;yu7|wu+3FYjJkK64-x!_ulSi9 z-U6QU+zf%j)QpZTFGMsou`gI9+zi`3VBTjtW*%mXKB#=8De&F+=|40yWUE;$3xUG5 zc^K>v|sBsgbF zDy`#{`v}{C_wa+dVCykmC&gaiGpp7jGFV6khTunf$}*{~35JOYD5l8DZIDnOFDlpa zMdMf4tzJMt;tk(l7^2b73@*mgfUJGf2s&u?9q$iv@>N@ujaj4sMn@;Ca>v*`$c^;B z5e~#%?y_@L*L-GE1L6*bS+w6J-4YdqWZvtL^E{P6?dTs{HmAAso-GhJ|GJTfykob0 zfVv4b+VY5$s=mdnb4T?X4KxTzgDzBf&bz3Y)%c&|;x(2&S6LwA5ilz3*;u5cw_vRB zwOeH>g6oF~Z#3AJWM!f-26vqdZxORxVMv$_fd`RRN>({rtWc%^?a&f3YwHC*$(gu%*3rS`~s@D?Qdom#0{-cJJ%&&yjM^?+_LbwVr}; zdKa+1Bv{eWGxhk`Vqqrmj@Yt3KCE9CSfdB7ef`*9>MxfYf)g=$05zHWv6_>Q`fWM; zRS_xa{t>|WtnafigCDd`MI0?0I)EUx30DEcl)-Q*)Om1_!CK|#^fW%RiJ~c_oo|`R z`&(S)It^}V1O8y-m1^+)3xZI`@y5REIfRK2Q~Bg`>Hb?E-pyr0Dzug7WizhErbldc z@r!7D7FQfpZpqKvBt=b`=ZfaxUOSyP-3*!dHFf&WE5N^p6*$ixsf3|ZQJ1G^t@w!Q zTbp@YxsXX$IKw@Ipk#DT6MxsrK&$g0;h(}_{DQIs?r~_?Ikdl4!2d}trd>S zn;6*evh6{^5;^?BcgX@3ABj{S=bORwvp#qOMdTpZ9l?Q_r*?M=`~Ru1l;}%87v0}@ zP8?mt+IlYZ(_7^iYmTsmLZ@E0B|iowzMg-n2?t3lj)i@18w!qVA=;=mOGy?`9`YY7 zRG;J{V_b*@x6diJpB$>=N(Aq&idJIKTUFyMS6V&QJW_c+;25nEm!U9!Ad!+zF5J4; z^7@9WtP@_&ITlE(Pi8CeM&ocN7O93*8>Z014R?yC0HZgB5xKS1A&Iz_a+@4Fq2x5j zk`Bd$<2}VGR&j4aRI*>FX*LJ8GiuKQ+7O&ctHa&#o7K$WS#RCK_Mh}4tuu5admbzLNuUeT-Dy!u8f4E7!JI?3n&|!Lu3H)knlq^SW;)Oz;%@V&_ zyW|4XkYp5%XU_UA&`G=!&zDv&%DuF|JQ?$EZvGCqTVrzUwd{Mc%7^>A3czpgFi01Z zPj$xmYZ)8Cw1SbDf!Ud~p%g{&c`gZ>#zua77Htf}=!?q!${8QTTng;a#9T&8ln z#KYWPP@K+c@=gA6d;7S91S6u)y>Kbb?1gudO62N~6RuBi&6`S2a0s4oEx#Pdg5baW z$}OP-xur?dW>ZuA3G11K_jO-;qjJn(YRM^pb(0+lz%qesNvDAMXktGP(_YRxw|+Fq z6SRrl)x9ZCU@wo>v+{l6xehMCq;yk&6mse#?rdD6H6~nW-X2aDr##&VKYmN7jc3CW zI}H9cB?;VcFn1yNU41~MHE5#@8?>g!9CI3a&gJA%$`f!-SEe^f7|w6@1-Hj9oSu-- zzLLc`v6T^=`zyVd{py%)WA1rx)Q4YUjZ&vFalgFl=caw({m(F6ClaN{*<+;wN|)(= zMs8Q_lRd+bK}s;6eS*!sYIWVqhD+jnB8Ek6B8gLEENu3Imbm%XIHsuiy&MRD`&^g7HCqM#3~v(A1dS}4;&RY3 z^>kIAm(^OT+iB~P05cH)EiOSi$`pMD&*jH>E`#Af-6+1c@vT;c$1*0`Yrny zEk7fRp^&)kgR?@a)skS-XmW{_S-g4<6hX&V`&nc0$g??z(of~r4isu!I%!Y4Fid(a9Sac{b zGF*^9W8YUwNtoKcP|Ar-gq*clfqk|n4{Izhqm20?Q1NHScho(bE2!rBGG%3ZSg?+h|y@k z5_o^O*rn=e-{)>QM!=FMi5q7{ftE}FjyzaIp|xrKA~h`7@JNmbsPU&dH*sV5 zTE=~MO%(Gv6rYdmr}LnW?YaxkbQ%BvfcWLIj^AOjcY=o4>SL%Sh_BDyjgPi#Mr@W?FvnQ05<5koDmr zlMEq-NJ`n$0MN#EWlF~+!U20v14w|GpQP5Ra0#&2s-EozgjQ3xR(bm@%I2E-CbnR; zcd>1i>ej0MQ&nWG!Zp84b0|)5DU~dwQu>*fvJs%10N~wwAR@ec`Lf!!)y(A4`>|3+ z-eb4LqUpE(A9=EnU3FSuyDt?LmHS7fe3id7RQ)t8kg09c0V!T$gaGYC0DZdQbCLIB zXD*`DiMC;eG)IJ4uD|LNK7ROc`2L3<-jdu!N5VC%@eTQF`epl*>9juj2jaTiDA6PT zi)`%^06=)zliDB6^F9SYZI{RXb4TTMO4982c#EV{j9yDwou?YQWc71to&uoLeK`TZ zT9dVOndb4)^{KIYcq?rW=CM&^^XT~SuC>LWT+@X`TRdbvR+=oMO_qh1CkSY|Nvxz_ z0O0=;X>4Nt^y$;*ckkc7{rc;#pM?WVzC5hS&b&%qeo>{fGP-oDOxc`JWCZx~@PHfd z>LqTzocr=lCt6Nh@uZCOMgU=7h!;9hk|~#$m+;}k2Ld1u2?UQ%zX5n>oz`@Fbc$8| z*vVD9YuQ!7lI+VJlp42_&PLc>>@0smns>yL9uuX0MXv;r8P8rl(ctQqQr;EGdz(aX zRR)dSU+5|?xWZrmle>&^k5lf4*t<%EFsSU>A^0WYgUpE5PEBN z4xL<~_#6nn>eb%9HUVeUS8j9m@p@0C=!pB9~)Q>37hZ&tdCBAa*KtSG_tP z>wS>=915PwM0X@2&=g_&PSXE z04zx%>LXi9hcyHuYif|kr)yR8ZN;(e)Qi?N{HS;S9F1ktt%H4?V~fTtpj3WAdI3P+ zt5?4TU@^yfbU$thLLV&nrBt8RfhPAM$%g-^98#6YPXItsRDx4GV8{#UVg>W_bgje_h!m*ft|SV(*VR+mH34- z?l@U`M%AX>45+%YREG6u{kFFCb!vETb4_1h8z&(YyK&@d-OVc&o=l#~SYEUgd&*{4 z=sR@14`H9$AU?(B;!@|f5v)zg+K;)&N>J`;5Ma3=*0gVO0pM2WRw;}o-Qad7_TilQjW}9fFJRq z;bmFRb}5%GY=wS?wgCWvFE|x2fo<F21OTvHeGPaIHUON0 zyt{}!6RdVuHdJsK==;IyANzu_m(F!3nSQ47${s6w(xfkl++WS3c*WWUW5utF>irJX z7ia9{{6KJ33mT_*XuK9QZa0C*CwLlM-4q%MB1@jMsru{xqUQo?P`UkLY3qRtVpnG; zuwW{Xb^&2GwG&A`D_dJ~vkSHk6@lOG^9Mxs0sGScw7vz0%>V!(07*naRA4IzUDS;m zH8=uYzPI6F@5YVPum2<72_n0^u*3fKKd`ZQAo=UFKpi2QUf8t*0Pw(nwWIV$ZS4qg{HYz<*b{bkJ_qI01KsE7T!Ns_Ehu?nH|M#}o$NFK$R4#Ef*KZj zZ2ycC-!${&r6oF&ORpG0ZFB<5VCGGEpP|H|ZGo*@L|bJ}0{~crV*I2#AkDb?$3n&> zRhU+T+kjlhYOB2llnzbV^_$a6#aGRl8e6cC7#f`hU~K|m&4AE%jJ64&*hn4CBfP!M zHUB?*@78TsZljBRT#`z*OLDv2?Y^9sjFF6y^LO%`f0{gJWF+|o>C^3Y`_dc8mc-7R};%~7BYl7z_nGP zX~YhIG{-u34|W1%G~z{cgAp?Xh&6^5(WeH);y`1_^cLe!U7fL4jK}w(`N)i8lIH5P zO6NZ%6$5U`ISV>!T0^X@ld|Smhi<+nCjUO>=WuPWn)lWSZ8k$tiV?S^1F!~y^$hx6=E&oKh*#*)37pqOvE{(N6)Z6A|w-reD@ z*O`_1Oxv^P&&qzVPqT>U+w)}aOcQXi4X|FSIYchU-lf>UXo5dH+r~D>WzyU;iNuW4 z^b8Gb=oAeQD(qMbfAXNHexPHCcD%3*DWm?=FaPNk1(T3%ae`g9krn7b6mHl(ZKFRf zi*Nk5gsbuS#y=2m{pU2LiOZAp*V1#9Kevb5$?Z})kK#^^%sqhH%5BSW`kd)mrC|}b ziod}LcyI%ND_#G;4{p2S`P#T^(^k*-37<`S$UGk_9JQrNKQk_w`S1pT;+cOhleDA1 zGxc%Ys@&XO-rO|(_ZXS#kpJb&moNYNum0+9cC0g2=N+y!1jq-Fhv%0L(v40MoK^mP4JOC(MEnkB> z*0C0v)9i={v*x2#uAIU?6v%ju&FB~ts#mdy%T+WUmJV}o0FAoPlyEa#jqAKK@2+vq zIgPv%=vsI0IzqTR7jQ#_7JTm7aCe2v-sg>ms`dt8MgxW@p);lb*&eaLwLIEa#ryOK zABQ&pxz-!NCM5w;rQTqQ@+nJkg&@z3tYF)5o!d5m^_PxsKcvH<`BpxO6R7cVzB{gH zg-jE5OPo1HqX{?gZ!`=Fy)J7jW3Z9blxNtDw4kZNKHP<)!gxAypA5z!9s^O%~ zKq&T_y=vY{VxDPD#X8c2U^<2~Li#L4wV#CP9u3x}?(w1XYVRwRHv+%5yj$G=TI+I@ z6Jyd_H`r=uHf*on_@_iGgBF!Ne){~UQ#m9Wl6ttI(Y}MZ45QH!BUqB#NWRlLIlmg1 z?1`I|Qs2fW+L`O*Gg{(pD{lZZWxnPqZ}U@|OS-0te5oR*u=l}MqaNlTYyB_(_+MV2 z7_pS8Vb^Ui8LB8}r9Ugrw}hv0lH%F*zs5_7=X$Hp0PtT+zk*TO&$LSV&Cj`SOk7F$ z9LHRSpSMZ;iIFeaU~X@YyQZ!xZH9k}X4OsKJ_X?LeR=?x?YUKaj%x?c*T&6$ufn9xxf)ouKS`S?~i9IkLK6t3#dd2{|84@AaZFc1hcy_-zqswyZcbm=_8zg*9TjDFN`CxQH zm*J;64)a`#UmSisdh`gF)=Vg#(Ij8|_=uA49X=F;t0a?a@|{~Jj(mD;T|vjE!2CZ| zkm2EqJRb=EzHpP4Z}3Tj#Dk~(X2(Jqp27f~|Jiw9t!y0(7OODEbfv5Hx}9zWnIVT0 zy3?)cg%TMl9NK}25*aDXa)9*oL1_#MBZXNO(xP?|R>K!Mg!J@IH=66}1%4xM*W^Rc zaPf{93>?&@{Ds=B?UY{Et~Bn156akc-T*|z+mX`UAcg7ncFsQgurk~@G~_7SQH2M? zjVioQBG1BmHPxDnYchYMk#TQDqeh|Oy&ufmk zjmSpr=yM{qD~uP_H5zhQi+Z@`HNC31CZ#WwBVIC*3a?gJ;sbaCD1_%R>R4#zc@mh? zgpM@(4utpt`v5n+t4)Qiu`zu}CJ^Tk4NK~)NT&p1B45;;K0Sqt@e)Wm6DT5M1jSW- ze>MczQ@NwrP^{VMe8#sup*NI`0BHJmR>&^L;C^=8m&>JbS9Xo6~Hc8nQ(ForP5{~wHLX+3%GBHF5pPoWFA(&wF z#H=he8rRHA;O-6)3tX+}!+8U6tdeqJ+`9f2aF@){^JGSI03YU48<;JD8u5KUwK33? z_9|fDnO;QVzqAx$iDXDjihsUEzenwiQpD-nWi+Tz<>hcY#CMIRqzBnSx;K{{B;aOm z0Od`8eJ|R^v|MW}o6c`##O+8$S*e}{OAAGvimb4#KKX}#_{Y}?u^P}f0KV#Jtat~- z0d5S?=uhz~Lx9Q%0EExp03?MgrM|YK(Jv?u(<2F|y1iem`dkLb<*+_;8!Mb^V8uGC zw&!-wo`K``;zaxn7Vv!;0L&Hg)=RoW`VGI&uP)~%THoLkhT)b-Qnr@ipghO=1~2W=Q{^gDjWS6ul&kY_t~#~@i*_k3 zE#Yeor%GZinAj>k@od^f&1fgyx<~Ko;gTlG;5Sl=iu(&rz1@cTu&xEgN=hY0dTi*Y&}? z#QK5dHClre@1PzS6p{;GV?8`OL^ob>MsDm3GtfQ3+dE)Dn!m5K0b#5o3C(q_7~Vb? z#J;yTfD$)zErJ1GhG6mPMz}?VX&>|V;YmZ9@%m;1gqN}4iUmiQLn#1C}^icHRh&x3<@ju^!ry@1M-%bC3Y_HY08^{ZE} z^e|Zmu~Mx6A(h>hSaNrGK5DN8op>Q8|C6;oB1H_8BS}H!w zlMDyW*75l9W4v1#04V* zHc(cko=_^I2)oi)>DC4;ZkJ~zGMevYe&X+)oZGQbG7E(phx_4|v<4A31gLJ!+X)dJ zLbk4W)f+f7Zvg&f-}x1t%87ET)~@wpF`<4voK)DSitwhE>u>NyWJRz6n@vPjKnRui+f^Xqa>R(EH(d0K-0^5qnu;JsEm4 zE882o{FnN@^O)?Ge=rKrb6<*=1wQSv0ODr>d>uK~7U&%?M`nrKACX5^MXX_hxXAy@ zU;gSPVuxmI9o64c_=0%TpWXNKpHMmw734X-a>t)<`)7rJ1ufz5_9Mk}_`*@j!(mBr z?9Cv@<$9|PuBEi&P4Ie{m4BwL70owz4|os*fObx5HT0A5t8hCJcg^qn;H>hUeLfS; za7~|o>TsXFI5W>yiB!S8t< zKYBE^77g&lk0IZ@I>!4B;a-e!)zmhZM6W4!mFMg_;e&&R*Z>}?oKF!RQhL1h1?ddh zwe(H#AF!b}iTehhI!u-2N%L(-X{#!B8g80#RRxhO31 zt1(+mY2jphPUJ}76weEaW4gw**vJO!`Zb&6?xQjjjZIp2?c7WAMTT&Hvhy*(UW09Z z>nB+T_sw0Q;%lnk>>v13Pj6z?_%hocF5CoLm&~X7oc1Z1`dPGrxf8)CzLDg!_Xy^F zyR8X!8Mxt=K~b2x*_UQVgr9eoV`C}SO21` z&>8now%weC^6zT#3DKaq(?!~Iq)V4#34y=YR3YXGtZB_5IrEM=Wl&U%Sjj+9dPs=% z4|K;xFr~Ex+?vD^|AQIVT=}UDrfW_Gc%V=~Z6#b=ZK5-ka)-{?*%eB)7ZlmSy>${`7fVax906%8bNc21WHK zr}&;!uVel^0;4GpkY0bFhw=rF3ZnATW@(zVF@8~hsIO4^m+{JAs6H<@)IwcK3~InD zGZVJAZAdtPCErW%PyhVOm;Es>BPL$3Uut9pd_#2E-_jo|_a&uqeU))QcKr`TLjVSy zsQj1XbGX`UB>m3fGndF)ucWk=er1t1`(42`<*eue#`@0Ge+rNF)pAP7@%h5X;sM}j zYi`B!DsJ`rK5;p17QTk&v*8S9#F-;_h%&wlxTY`8h@D6Cgz@s2%{GwPyx{nm%l~S# zs?sl$JQcozTFXh1X7J+W%NKw5w|`q4v=ZdQrz6NO@KZG50Q`W8c8M9ER(w5l;G{%H zrm4zP<~yULFWRugXCRLQ49h;w2j2ItQ-6BhqRPVb+5evD3Z5J4@l8zX_Q`ugTX>>whZmK6w2~; zGOS}{;h7RSg-^8u@G+f*dG3aG`T?BA?G3=VOH+!Y5rENS z`ENC$P{5*4ryNG>6H{uMktl*M<<=1OCMxowtc*fET*q} zjv%F5z%d%gF*@;k!XjFPVj}r24syF#*&3p$ne*Oe++JG1Eswrjc*DR`63sqYi|i9R zDy)*7OiC-+W}DWWN|IB!J4W2g=HLT(YE4Cg)_IBzt;TO9?wDuXtFki%AF4iM8seNw z_*-*6i`K|{QOBqAQ17eDKKc66YnL{DDy!{Wy-Bk=E?s^4>dk&JJIZ{c9QgeC^N1si z$+&qjl-Hd1Ro4 zcvhy*ib2NNkfLfngN#;8#1kM!_dM@E{KNnI3VV-KF6|XI0DtOfmlWRsJi@4$*9}GW zCtUtUzmnoxDjvSY1d`LK|XGR||(wp<# z;GXbZ8vs-b`Y5h?Ud2_vM{)0)zWTk-a}_>{pTV{GA0r&K;mo|sj3eXZvCZOej-Snu z9OjLx!r{M5%9G2TF+z^hUoGoD0K5h86g3Ed_UEs@dS#i@Azj4o8~K4!f5=lP^?_O# z;}eV^43$O8h@Z>RAjhXXHrSK8Dmtrynblq8DJn6V$}$h?sQPzR{)(o{<+2~btfWE! zc=Gf++krn6Bb(7b@5d3bVlGG7XLyI>s7P&OQul?2D(6#yhnll8Jlwy|voy&x@cv~# z6x^ip8+;Owl*UyUA)NVh?NXk@K`vosYo(~`VXZ5^0Us%mk3Vm)axt$EPZy$r!+n&> z(6${Yk;@zSN_Xg&v7N~e0)R&7u1w`IYJD(809>x{20)!uc>_>|sEz4h*x~N(_XF$; ztZI?LmFmi{3((tTdw6q$6!;qR15b9r8*l$Z+-Rirvh$j>xz_t3 zt}(4)?q&LPpnWYgRWKi+ICy#+dZk)(UeL_|8o=I9*bo3s*U_F= z4SZVQU!CTQZ^*58j`{z!p%%38-H&JlFiZz$=Ar!@GL)?oa%rgh%Awk`*BKK3UX|P# z#e;onL#Rh2;=)r9W7DE$g_b>d3DHA8E6|*G8f<;#G%rF7XOGy@d^a8(Xz|QE!=q8b z*j7ufJFC-CF%y}779iv1B9>QeLee$f0G3n~3t^Gyz_}{nq&1_*o&3d;a{VJ7tU`F&y7*78@*~xEC;&X)2g`1JF)yw!RZjqHyF* zz3&R#O3CkW4ScJg`C-Gtn?UDeuG8mv=FcndC*0oyM#&QwV=z54o_j+1^Pm6x_V>U4 z60q~}*H~A&SSHCk7D(>vUe#qx*e(&>)6Y=-O z`&8hlJyrTDPiAPTOXJbn7~=`Pr1fh|EkFs!i#{4+{@&cj%_jzM6_FPuobL!~gMxV%$UdG%R@NmES&Pvl~GrUjxZW8kieuz*iTj}s!4|s#X)#6^x zN@ER&y?&`|&C2p#6#8SKL{_>&dZ8p9+Qlug15s|*fp>tJ!NW|6j9oDi!c{w2`Q86> z{Zs?GQb1$13bxIImhgJb;pTMNE!r}=xvz+V@x%c>SGr9O>~=pn3ee@& zmm|>Ujq(gOqA{M27QF!gyJEPxl8N^VLH{H`>-4zG4DU;7tmGzbvg5Xf*+4p9#A;w- zo_i=PnJ+Go@!`Ayv;m>Mb>I4TQV^C5A9uu6FJX!6zU}2H?}ZJJk`iya?$7jTY^!+S zm5CnTSNS2Xyl`AYTS=rN_PSt<9#RK-VTgOSL#9JxxES?CF*8;Y8B@fXuZDUCjXY*f zYe0eS%j?;n_p&26|EUcpD0{ak5euZFz7o2wuX}4Ti>Uabj-Vj-_Ej`=6|>9A+{G*d z5uWjD<>fj*TQj3qE2#7H*KfZyTKN5gjR2lJd4i`;pN{V>wq_}4ZSuriww2Lf&4GpHO_<;(T0>YbHwgDc^?G60xuovSUi70;`< zb|P-|e4n&c+*!I;&o~i(l=eBmQTwX&Yv)!D&&H`u;&Ps|xBh4Itx-8Mdh;g%s&)u~ ze}@|Y2;m3D{QC9l7f-+V;%WG{W)dt1$Onc;paRMX!+{{AwDj6oNF5ea9BP$#DNVl# zY6qFK@{`N{Uz??6dE;3rs)NqR}hbv$|e81Ie@006WnKZ1M*1^^EUt1(#4 z$s=5TUuz4zguY5&kKvARI6qFn={0N%$r;-{1ywC~wL0ffEpK zP4?v0@q=7bD9Z=sDvF!QJ=l82Ad?VnE#R+j><-=mtO6?zM@r+R5*a}B7QpgaJg8E) zt+AcW4!^t|U5U>_fEc#JGa8e9JiH$U?*K`$pS=M{>+bfcodR-2P4l4vb|}lr@m=2) zWj;Qj-n2X$ukK29V#rb6{?FmUa3hDU>o|N?i99iISl#{yg*|<{s$Co6n%suD+UxIg zLC32Bz$VzZ-P`8r;y3VZF^jfKNS8cw>;M2D07*naRKeB!&>Y?6w1$0}*c!8DZ(9u0 z%-P#T?J(ee5K510j&ILSc}q)cFwA>+x;U1#BapKG%tM6a)nEy>SkK9x#~K(ImpSbX zz<)9UsaG6fL9Fi>$GFTV1IiSi*U$_tHHMW0Ee~1d+kve9qvdRomnrJz{oHhA*v6&! zy-&8iJayl%N_qMK$X;WE8v@X`s#}|T1K=B&-p1|?z}SP*(HBjVz9uiBQLH&2W{z^K z6wUVAV#BKN_e`DE6Z1Or^Hs@xu& z1tBT%pCztF(f(M_TmM-1MYicj@+?rC{Gdl}+!E#M0MjK*kTw16+ zpUXX#n1A+p1rfO2Xq#5dGC>2-aIslyJl_Ow-=&np>6j>Q){$W@{^r{acX!?J(Tuam z^Hp!xPoFGR?F`Yp z&YN1#Z|^Re$L6E&f5y{#UNdxZq6&q-^Ul#ofXYgAb0ITt3DgtQYSO-ROe#O;x~`cE`Sw4TQF#PV$jbiNy2xn_ZZ)TnH$niwiX_>L)CWz@5}IJlEe}2fJ2>sG!CnAJsk7HK|T+s z=hr6+4M6sVYnraeGhJ&-iS{Mrr+xxWTT`+HVJ4@ zwsu}D2z&X=r96jc-|uEcif8b2R{VSTq!L+c?9#KTO1K@pY-`6PtaUX?XQeWS{SBX7 zugl(cbN@+7WfASOSeMqVh5(yj|K=(6vAdS;va2?+mnUeU9b4~*p&gpu%hIrpk@aZ9 z{BV=PE5nT(2I{#w4YQ2C3K#8wU~HFl`Py@Z*bOWR?eya+GTb2iW@VA8CBHk^A!;w* zWL(Af%QySeX5+mNxzBUY+UFU~HKjY;QnQicH5T35j%&_eN_oAvUZBkw8$Zu$v{CvH zW?mF{2_oI3?8>wZ&=<$C9!3|mBhIrBy@XWbi~qDCw#YLV(mndTKp$@7E^+f1fG5ck zH$3$R@9#pAmtjf}wRNN01z|4c`oZhcx}{NHp%0g?pMAXda=Rj?sXKmO?>cwvTh$l8 z;lHorW(HKezj(j9a^2=KlOCEv#P@XeG3?!GJ54#1&KI@9!^z*bqI0+dKVKu`>Q8L| z)2S`=zkLs-n%*`?yibMuE{v0A?w@GU`JucuwOeF!PUXzlm1L}#3W~ForW6DO=kJx0 z-uD#h8Sysp))d-LZLr5-FeW3E_(01M6@)Q4m?dotOmh^tWhg9yy0mr$E*RxZ`#H~< zHKn9nfy?{Pa<7tzJHt0wx!S9eoD_@iqfBXc#!7Sska5#=itT1qVeh#pZ)*BrYptOh zswUp7GEf8X>aAZ0GQ&ug@%s9F_UswqX%XzvMdgpkb$zF3p^gevz7lc|lDz?hnPux* ztj(aDD!WdV|6B(1jQ&{wf5nq-2oTE&O8fp_E?0lq5Ww?$%R^f{cA&cqhsS#DjXbZ@ zb9$-nCI90;{^Kk7u+FyE2H?NRblj8V{Fe7?D|QaI0c^&l8=I>)7h%1YqN zb!d3HvlQhUU5j>DJ;jZRp4ZDVvvrMy+ zN>(_nN@(k`yKfpJo%&?z4ekvxJRIPij4#tGmU)jlkKnzQ?oH&r!Dk9VF_#p3&@&Eu zZ9REw9(1FZH7J0Z9J;#pHaA+(j-sJ!M_CQb9`2$!^IhqCcC-T<^?io!fv zs>qs7u6Rjk-XD%|)4QO}qq_b=f0U}o#P!%afqtEYdtA^0e+fl&oztaiaNBnl>ECp3 z7of>GLx9Q~K=>qp?wLV>6e-cgZm$2eHMWnpVV+nELTArsXx!7W#I<7ON^bzgn3g5) zjN_a)0AN^G_4MmHgoIG>IO+A1C}4F-XQU0jR|9STtOMgw*_$}WH)R*|GcoRI&+1{I z89!p$6rcJ^OHkaf+{F{9z2=l)*&m_#zE9=I3B=MScwtOrU1Bl054{2S22)oVBI`B{ zu~NpCc??c0ld1B2f{TuOYOlK1xMr~h@LZp{jpXyhOrd@P#lpNLH#e@)Y-4wr9~5+H zK(kpvB5+SC8U}(PfL~wR-T$+kT73qf0eJrWc}k1;gAo855`_M17#3>=O0K*+apz^D z1(9E9oVb z%g=f8ZYho9knm^ua$0`Qw6qj|6wY*1!1Pzox$UdC8$2-F+W?@QH?OPka$H-aaa{GA zs_$Y0Lf2NN6{O0E>d=!6QypJ7@+HsVgWwkSXKTDs*<@}eFHk+fX&jrltUD2l? zz3eyGH_x9=$^ZcQ^H*QJ+O}=GTrTuM(k>d%%l_fgrvs;kD0EEm;X7AVdJuXvZa5I1 z-~qea{-i($3tTDA;)^=pu-{VRmV|bXuJ%WTX1|}>l@XOUfc^W@ z0!YKwx)3iL&@Qnbbb_RIfZN&gHZ&i@tt*9?R+?K&ZvZ(=IZYJsxnwVyjD-<;rrF%bJaat^s;q54DIaw zAVyr6n|re2lYxVFob`(gyo9|RXjwB49Qj>gy#22ON4%q_$M+q&DOapH569#Ha#mrf zbk9A*g$fm;#h4m*E_AmU?t*}sTtBNnx*?ttihcI;n9j8V4t^6uk(c%P?YEplsJfrZ@=fWC(fX=sgTFT4Tho$!mU6|oj~UJ9sgK&h3U zexW(xyfp5yC9f59V};PVu;!dE_6*DeP!Fl`n{x}LFO_o&OF5&klr*NT9AztW&%#20 zr!;tX09$`{Cd!Jjt@C{CD}nP@9h6oeLuQo)5Wl7~uVd(Cnvb4rzg*#N~bWZ0nIH%Bdu3&?gl0PU=VP*;Mtd7x-69p1W2?BYhUqNny1XevuwQe&oMDSiQ~{r z9^*XTKi#`n19=jN-T-v2MI!-L?0dZPBjqzOzmv$}eKY2C>2f0AqO0@f`j?2yj|Erj zF*fBp6AFTecK{W2-gfuLJY_RQ&xM^wV; z__^T~@Ih?3+a=yCKv257>?08;2|eZH$ay%P4>q|St2_y+U1^1{d8WXU@v7{h&=&Bg zbr4OL$B%LM_HFh0$^`?DzW}~{dzO-W!9(rNK8ACIDrwgKEP+&Mt%zLR%2kIUo)-qT$3~6_q&(M*Q6v2JUl4H z(ygXKsm$r4i$}zCjLP(>64}B^my+wW4DIUS3-v=#EiRPG+HUKTq{wZ}62)VktfYx;fae(Q>quOiqFN2wjwBQ&>65i(1sL)0pq z4^zuUU+4SQ9Y`CHCM$if6W9T2frZMxko2h4tUbet#gC71)d5H~y;RCeh(i*c{(_nlj zHYg+u-V(z-PbqlPA@(7}g4Q{t6r{&IbD?{bwIwDN1Vpads>m|Opjw*rl_r*B8iJ9) zxvq=Kq!&`*HHN}RyH~DCB`dqpxdLq|trsOSdIK<< z1GKO|?9)QbO-9q3uy9WS8ww);rEj0uvH7Wd^uAcl(6EO$EOD(^YuAajA|Kcr01s!c zFJftDO5h`IN5zYxkHOaSHFQOoIC+5aJY(sA%@bCx*&d^YAZGt!Nz8#eRS;7C4AO8rCpFGwD=xrsOJv#P}rKS1Y zC=Ka(0NP(XN)1@I0X-Cc7$+N|I;K;mtF_8<_9pQLxRX5-Bq zyGH_NeX;KFT)Z20vW!kjMK65lo1w233DpVwum0uNufc)=3&-*KPlz}7S;vw$nv9nf z{`#wK{`0f$K1vsV4xswW#sQ7eDmVR^=G@i_XG{3`Ip@n|r2S^N>?O)9sw z>bKc17SA`h621=ufRnpwiQ9_bmeO01zKXl%?@XDQuA{hF+p6}@;#TpW6CAbin)IW- z&GM5nnylCt#Iem~<i~-{6M=m9jO4Gv6${0VHL6rCi;^R^%Rrr%S>Q zvQ0JwAegygpDB@%!rl&-Z>+=vCGwWSV>^KT5RJp$4|WD>9pcK?)@fI70ZfY@7WTXv zZe6BBvvy=}0FC;=mBQ3+lkHTgR(L&NS(%luq&$^g_l3efer@lIL*=W`cpQXDCcieU z{W65JydIRyVw300?SD2((Hr~u);v!LA)^35%n(DEPQ}$R@(BS|FBy18 z3OgwW673n`(hkhI9kVilBUj5a+(cP|c7lKcVS<}R?LdG^-T)GTGCX}5Ao2+`7pPu>xd$qdYcZq70o0_fUyZvbipV8v)Vpal6IDoyO>Yf#Ac z@!v3di;4K7idDn5b`BJl&IjRbK3D*IzX}h^3cGJ*XWf@0?o0Dh!PWxY$}q~Nj3S=k zC4`Ne4Yg#rwGZcmL6H_pbETK=7a~L}-TZH!e{j#Oi4x0p8kHiyxg6&@(--*^qg4E- zJl&zpF{KzG8VH}E$GBSi6j$+!q!Qb3W0@yc!#p-Dm^IgTxoTcxsN-nThYUONDdQUK z^{?8_b0zPAWflHS6OU@ZouoWQVC$bU+6|<*Hi+QTdFf6@Gr5@U3rM^GAh@VEEA2N^ zynD-=Y&)6V5a8>#-}W-r9sox7|0K&(eqDprIGC)+ePVk+G$0s_xVa!>6liGX)<4vh z&Ya!UXwSzYB9t$Q*PVILCO*Eb?6?E?NCn3?YrI^0P_q=d{09FhJXIm zuU_@XB!CX#|K8&orRlTKGb{XoXaoSl*YxUH5Df)1t`+47J%i%%a}H-qlPW%LMq^wq zH>dG$WyFy4=Jw>j8EC)Eguu*sb9?eLm(hy&Sv_+h^acZbNCSW~+iW}WJWI=QZ4s9l zM*hxv<*NPcUsZn<#w4z4Q`L8?J~_huT;Ql(*UUL)I*#|0lawm^5rjn@r!jw<^#YLF6Sv;I;X}1BEt> zY`)j;;iybKRK{|JgEDmtgTrXC4Bf9*%2#U#Mgxb?F4|N?9zcn_7j@CYd+j^XcIyVI zb?-5kASX3|$3PE+3quW|cDpx#J+Iw35((F=*c*UI|4l{)I^b?Ju>Tj7tpXewa=5a5 zyfuFm&PwC6;b72kV8}5uXt41_9N(G2LoI*%c-tE5Xd4{>%N~)81maxWd`O1$U3u8& z&tAEG*S6ONp!sPQe?CmG8w&7T>1FBTK1T^3KF?+A?-|yZE%i$pkF>s}Pp+`STSy$d z0Z76Z#?QQC63*h}jCWLPZUiwVd5wsA>gIAnn#(gIE<^dYvyiD1$2}!0UgHfwa)YM0 zG2wFX9^k{qE|;D7UBQChSIy&e2rqP)p$d84g~eotFoLkvR3KpHqkD{dd4^MPaEMgm4#-`Q{XHEs! zvWY(ro^kUh0mAVFFOQ(ag3+OE=6#xUS}qS*89v80<)xaZ<8zrVU%6KR+aqWX$hT?* zAs8AoEcJ>F3>~KXT#kp!j-iC2$I}#=-Xt%HJi&b_c{M{Ex2AQ->5E`pt*w(|h-fEw zm6OIbihGP5_v1tS{8<*H{n8c)NY-Io#p^gwg?@wFiOG?Qdq=XO$+F7#=|q}GX4~Cr z1Vey!0Ysu9z`l5PLx9)s?qITGn&4~@@a);M_|DM~!pZ}TF42Av4DTw|7csb5n{~Za z>#nc;&2gxghBN9wQ~#e8^;Us+d93`;H~YC>H)h(OH!80#!^27s-|!b}Yvh}`9eJE` z8&hidshQY-T_%WZ^2=ZT@*9fHyRe4e0(gbm^}puZQoL?e`uqq{d7p3mH+t4E|K@n> zf%J#snSZkZ0a7~8=2;n4zWmvPik3`oML(zIwy*v!i1lQs+Q)TN&$+D?>2x+|JjZJV_}$IrquIE(iwz)@RP>6LUk>uY85Ri9_cbv7;) zEm`r;ajS7(&Hap1ZJ-Ui`yAcyD%u4=`|8DuujZn80y=Q22!9`Z6?Aq)x_O_4uPWSM zz_$vY=_v5|WjM-+65|Ttc5Flx#D;@|jF-cF(0c}wl72ZJS>UVa0&uxphOsE?c>MH( z@2(HY$Y!)wbx2N65kq>BH!rj_U1eBXO_UB!aVbubLUD=}cPVbg-L*(?had$C#flfF zXz}72+$BhHcXtUIHs9{E|MT3;oqNulnK`c@=(^l11UqcCj6K~g+lyV0g<;`hW;twM z*LDJ1&~~$mj%ha;?AJHkQTe%Jrh7K9yAq9mW%R1_%DeqMLsP)XSc65f*nZfF6flm? z?HxP3cA|K5IJhZ~|elsh{IBwZwX@$t&PA*?-h2u^}KlY}%Ex776YV8hf7r2In z%`aisMZV#@JZvUeuNE$bop(Q-qW%0U7&TqfzNm;lz-GvN8@~M?nHyg+E-~H3?ix5S zZ%2qZEEFm6!ZAOz1Vo(%dkHBc^qtQ67QPTWLw^JnhoneEtPVC)TR{tW=G||lb)0pp z9g!v*gVf7!J!3URgd!S}!kM%E^RwUH@gl=7%y7U1?V+Lv4pn~GJG}FClozSb`e;Zd zn(x+oy3NdYy_S3jnax_$?-$DNfd#ktlkJ!^BZK zIAwMI?n%vsXO*!gzI>gsA9bZ6=sT?eStYQaA8sfebfpoZis0 zL_Dw+-tAWn@4p}E{cu-8&Hr83!Iz>&ffG@EtrOU5aWW5mvyk)dGQ6DDM2MyL#+UGl zw7@-_d{@?f0_0v2OQKHzT{xl`i=l{lH>yEQCsb2oX;f(^q)EU1Q8CTlY=UC>LY>jj zOthe*`2*dD9v#a>*KEVwFr65MFHGtuX($YYU;X)G<>>xM#5N?S{d6UvU^UGcVZ}b3 zu(saqdu}sd9(7U|?Ic4?=hZ`^!LD8p6uzh!q3EjH7so#48r_oaREKrB>h0yx^V3-1 zBKO;*Kk_Q;MTO&3-RH|@#8un>&hEcr++98)Z1m5m91Z!M8+a6G7Xe@vr{FnJF$^T8 z`NXs{?wX}${>+5vsb#W(#F2F!{CeIAo3CC62O6$Y?oZh*+>+2sCO16^=Aa3!@9ewj z$^!I6Fkf~r*d|a)zlYsqUGMLcIcq!T+{k^>0a24T1F!0&Y?R0Zz$2hAL~y(sKx=aP zoF!-5;O8A4s18_B4J&(ey=Z?lz8(Cs!h4uY(5_}!o-Hn}OMxejDGPnnekpg|@~gcP z-yYx74o^DX`qUjnd?5z_8E&u+@vjG5pRF!80doT``$?2H2o5f;{#nhWiM;2dclhS zrp=*^Rt-p#Jg9CKN9D_E^g63^r5%(ZVbtXAb4rh2NAzQWIREVfP1af#v2GlcONevl zo~a#S=b|>-KX*}Y&nv2F`7KxG!aaih_#elrO_iEk&-^o2&sjtZ#sV9r1b(q7S)@l% zqg+D;eIQ*pmrmlhu@@T3y?{j*y0j4o_4$~X_5oY<1jE&3-#yWv@w-iMzUcy~Z7(*r zo;#j{vUd42y+}xaqnDOoO7_ucVb0xRIvRu zId-aWZPO7H%x$DUu1@BiTDDh{R<1u>f$jX75ey*|5K_DFPgxhJlgfXsJqcg`K|)T7 zqIbg*@R9x{lup%0$pVC(MFpfQ>})w~OR9V0hbg)e2@C>Sq20>_z9m)6Z{E>KIW>8C zUU!bS)cp)U8!rNe3Po60GdtfdXAP`DszfX^w7QE=&2p&h-xw294S+`!^ov=ZSW2*- z!EA7~3E3dgjpFaU_R|AhpVUHxiweRceZTy9)t&6=y+jn~D30OC?z2v9En8oJ1f(w`3ro>?GTD)a@0XHwPk4pB(`2x!rVDtCMSHh{`Hi|y0nb#_T)|a7eXTB`W zw9oPZ@(PIKkPwEk*zf^yP8XbrD@cLr6p9HQ#3d85nN3 ztDPQ591LQ`1<37oLQI`V+^}Y4QwKA1w%y{RRh`c#s= z$;+OX@eBu1)=wQVqGmP2o;K9Em(g0uS!helSg@N(cd0bVJ^S6^-0qZeI8^sWc$mC%%&`(ML&Pm8YOZWiPoFVPjoMV-!bm&*(#kEi zTW)0f(YvV3$(DwUob8E!%0q7u+4R`w<^ccfA0YU;=63<_#uVYka+6%cD#cmDWj$u@9NrJ6$e{&%YEzV>k`7w4Xg_sTtPHP~?t_6~#>)9G-i*tqDNb*t%P*a(!Th zUH<$0;CDKIudp`9>}-1_7s&n!-m=Il4C@pX z4>#8nOI~NV>9r}S?T@}5TSI`YdX-@k@eh4^+ zaxo}uwfq=yJMI~9h5j{sbHu)_+Kq|P->=)3*cBmW1;ByS(I6*1RQ-W7iQg{vy_wMZhN&#^oKJZ`W*K?% zgTJ)s%Bz9rA-g-UVe4Hn;opZ~kn+CuPcPJIRKdPou&sb1Ti*dnr%^QR|b|ss2kvDkZNI;jmeoS;XQXuq)Nlr#ak%9GHPqNFfU@2d>?6*`$l7E$4z;8?z z^xZ{`;_kZ*oCm{BR1MZ-6gtPGT`8x3nRe!cT?>?&> znu{>G&~LgJDIs)cMG$S5aj!r%0VR8jw&>g<%G2uw6-Kh!HYwgw-ZH&JQT%@%QTYXM z;illOphJ9f?`$UB)0krGMN2hcGIdv;%pP|4E`FjKjoG*g(xxG8G)D)ET*6z~A5Xdo zuYG6qWTAy4#fsKzA>{bF^@b^$M|ta52#>tlqAQ&@S9ScsvZvD&g&n&u#m6QC{YxF@ z$7sd=wa`Cti*vEmHw;M)Kg77Yt}JwN3LY5K$tkG=Xfk>Z>Xm!@?-e-Ova!&5SIy7t z^#*Rr-u*jE0Mp&Cyv-04D&9K9JCfEeJN=US>#fB$wu&}hi|(?|W#phP&5O?z(H9*o z1_`(9re#9=J-uHON{Nm+^OV(pOwz`t%XyJ={NGjD9|vMDwGoV0Ue@6#%lgSpdZ}O| zBrNeF8E1t^kGT@j)y@kd5Qel?SkeaI0v#~n@YnzrM+4>vuIe`4Sn4fOUL4N2kC~so zRUxApw2@NtTC@?!_yYG}PXaf$VW>uZtuF9_Eq~bAuZDPlNFD<)Snl{{l-%UCO{L*u z-)RR9>|pBr8d&PnscQV@c5l$1WuS$WSVmMKE1m_;1GE+ML;nS$PX0ItWAcBiB~XQ> z;6hc7!NN~J%~wjBezvaDx#x65zP#M=>a4Yw)65)SV#=;*zZCy98H0m^MqF)1H@Npoc2 z{f(G$h`!6!^q>9x*i3?fi6Ux@@jL+u6t(H81Rieg&Uc@ z5C}ds?h*cc%aRnDE`Xg-L?CCTkes>HpZKVx9L9I#V_?JDx6;$4y@D%2w z=d|!mZ(x#Z*HQvq78w%{|M^I_^O0PH{;v=AI)YC~%T{N!AsGs~Y`FML7lwb3jflc! z9cQrHZ-vM46>}wspZ%va<0<{A?p*z5NNy^Kc}v0`dWn(nBtA%61hLUoHQZbSP!9k$ z#?JINbslUE*LUA%?{2$U&N6P)u+L_4qPesn@H#H)8)lH= ztFjn6RU_fb16vfF)eyvZ6=N2ke4711!Xd@hAu&NN)J5~XO7O48p?qS(05BC6N5f?_ zWw2@gz_y@e9Zu2_diVZa*Vz^|rzNVB+#+g3fPl^>%`F;rv&u!ekj=Dg1(Swvi_u(L z>6W-JM!@wI7B4k&++%(Y$LBTT#Nu7uK(fJAq*;MVdXsVg*e#gd$E+OIT>OBvX;baA zw^2%K8vVlx?Sz+to8TAsq%xpO7$WNK5o|kve?)_4w`})kJLKe0a$tBMsl!g3zoG*r zW@MpVJVw!~n7!2m;%}-Wq^`$`v~_wV_2h#gDGd$Xir1v?kl0 zt!yn1BEBAqlycykwbiN3_u;s^JG9qWubN*;Js(E6(Q6k8z7KLCoT{gnyaRC z77$gEbdB<+`3d_q+lNH*z3O0L!>KJ3MF4R58d%K6nqrg}$mfrv-fws=2O~0m-EjL5 zp`cGPMDiD$umW3&B|>W89{lU(m4m4WhoH?3_1~j;xp!%!fd%#t}6BNa#)kY>W9ghw-UC5M_*dESEmjfs zW7PW~F$8t&pv-3-2=;Y<;*8;?_b-v^X&Vy+P6flM;QnhiBT-WdXiHb%lFm2@^R$~$ zzaDoGRW2Pf4^MsB7-qk&02zx=ndcRDa2g|0#B5gdY-7$NpQBezT=b%lQ865%xoTVV ztHm947K4A{Qtbe_2Te@xVpehkD|rl=W~yKFa8d)h0pw*dW-m_YT-xR)iVh}{-!`GL_Y~KTw17@ zrojq&*Awl^ic}$76Bcs^%EE4aajiKWKA4OfY?V6BdM*ejDJ0DEt?Y{`Xcn89#Cn>rvq{{i*XnVFy$(~qm=}`tZGvh(+^kVyFf`5qAkow-VLdA|7g@_2Gqq!WFT^Vj-5S;QRh ztw%aOAoz@3BISlcR8A~4204b|I6uEbaGWAthyov|Jn|&<4GuwTT6_Uf(FE#1UyYWO zH>)>CRza*=?k2F5w~WlH(LOj1E7qaHqt#B;Iqr+)R{|><0dX^8LjoyR;;aw05r7nY z3EnxPWg{n+tm>mCLW*hZ;PXL%#HL;KjyG!1#Nq5Z+e=Jnr(Y0LJfPfbd`ZY3%vTj! z=R-|MIc3BS`+7#nl}@(ClplKwv8y1}9oQYW=WCp$+^;R{`f@KCypN(1!TNrA9)9O` zG@AR|cR3wm4PNa;Y}~36Dj(vEt8OlL_R)Uw}m>SFXAVmA4=J?7en^kN6i(7-1A4= zBmNkT2GBG^O8`MG7XVGYlg`#5YoDt#t)IMomFsOdGZS4ZJ>YI^oGvT;{pPl@%|pbP z5#0pt2F3;|%H%`Rlyal+GyaN&nHr0TmqFAZdm37sZ7<|q|G$Q8mcrt?N# zZ0e-*@w{c0rlbL`rHw3Nw0rN3A@>L&4hOAOSSd2 zh_$a{TIFGmi#XdYg`uH%_z`KMYSB&bzrFmq4-Czb9nQ-&MJ?7XZ|BQ#UO0ykna3jM z{bpDx@OkgsnX+4sFrjEGqFI* z^TfUPt+p?Lao+xb(H7RkLbY*czPy#6_FMNHJp{IKCsY zUD^0}Qxj$4K;ztA0$arlc!%(r1^~g&B*b?kL!Wz(5=6l zr)|PC=M-DfFw~7faTpgI*KFlb!DaG5@ziVO)&r`ZtzW=_TrW;;P*V>(qnY_n0pN#vVy@g#UdRg#bR_hdamb{ z8_y`~)K{*rp7*!apn|hk+dSzz+ZedD&UBMPyYu`B3>IoIl`T%;qF`?TOWkr4nGwU( z?cCl|7l#a4`WjsUBT}zuHg^N++?;EI&wBtH7qe#= z&#ADoW*(57B8(CWUIWD04bp#v4xY3emGHPwFJ4*(7pQDALGWk(TqNUb``WrHtKken zRzJ%QQmE!o*9n9hecZQS5J$QYXD~qZc{rN=VFmz5m}Ys042BPr(dJ(e!IaI%d`~8s z-moGV$H>AOvr@IifE4dw{jEGNzZBi*g(kK6&|DMtzne@r+6HpuNjnrBm%`Z|RGViL z9lR>bmj`by&Q4$K{o^A{>Yk`4=D$f_c?3kIC#O9P_G~>&$b%_Mud-s>7+&(P0&-nU z6)u=eQ^aBo+euY=lSCqKsoK9~pT^^|NUrx?Q#=E@>CxsnPtf!-)~r*B-S9qg!new3 z0udY$xHM~tkt0s+=T8v~(WXqMztR;C?N0MNbh$mY1eKk%7dpw7DtIk6RlS$5_s*jc zrTAqGtGi=aWuh7~3p)n=*08Z-!DQp7d~^VIB{S=~D=p*%C0#-VDCgvX;SC!!F~)By zHWZf$Q*apzp|NUbID+!C-rx?LH2qH6S=ZSX1BppQwN{J#HZR(sw4E@2x5Hd)z)63e-@6N5 zS#!m@UN`B3JxnjWD)%pKA2@nU7;X(Ou--Fa38G>^39f}Hw5=VYj~Zut@5csEKkGba zAv8Gv4qpPFuin~4tNmbu6Gi5w?)8>Qc?xP`vTO{`b=mL(Fx#c+5xrrDt547vWd+AA5KXdz_;;H+rqow0$mI_`X#Uc(ueK_jA7zdzs zHE&Sumi{Z@*$u+$K6pi75=yNl1*Kr?Dwkx{5Hi90=DdtRN#8B)(cgtJ*qq7Jd(HSN z6M0`g6!@lMIcP)#Ij8g!w~m#p>6TLR(f++JZdaGe`n9@P`Pw#tX~=(|5&!v&$c+E} z!tvrx?aUPJhbBzyag_VL(^cnhD5UEsqUG&(yLI~ujg=sSf5H+F2f^z7dE28D`Zm;u znJwKXmkjLR5i(DOnc4dp_`y2NW#9Ing{jsL=5MRk zhq3@GCJBh59r|%e#YVlGIPzN76AM&nO2zW+>^vdpg@OlQ#*j|-8*q~b0mIf>?kbzOrOR)R{sY5|cE0p;$VT7eRYz*qWV$WU z8v*FCNTgDC?wx*kGKduSUqvsdWDi`*nEEi!&O}S$MXThee=OabpuE6nEf--G=^BMTq;7X@%li5$B~Xuwt)~*RTSq$?P5+PMRgsUrL7HE|^uiob zhQ+6-Tk`}{KYbDhZ=T&&PH-Lt>J7qy?<}VT`6@Qi+YxP@9L&5X6MY=wqeHXOd~?q? z=S}M3h5Uf*AJ9wGNjD#s^XpTPUr8R9n|{s})Egl$m(3}2%061Y6COB!3b)pH%ktd51R2nVz+7QZBw?O{GdP#bQuN}~1Sij2YKnM@pC#L{D#043bMXz1}&k+Hm?Tvi!B&0RhX?iT3)(T*| zwWjV@x*dq$hhs|Il#{Q@)fuZQLhe?L_zdL*|fk^H-5*EVLL^U|k*CIg&mpq6<`efS}Hr zXK0a`2w)O$d$kIB9s~s#TFsx$e|f?9NCUXs^To;aC6=jWnSXsQzc)8{3t6NCl)&vW zVu=S7$X#0;2mXu@{qtlw<-rB8hub?Egat&GPM6-OCHXpMwkWS(n)7v@O)aq!mc{)i0=@VG-olpx>H0qw++zYdo>s;E zSoZw^K6Il3zy4@n=U%dq+d719ssX?#7s$8I1c1|W5)Knp;Sd~>O8f^r^-CKG4y|5^ z8a%0cX?sbjU!;g#xTtB(622Qv1#c~4I*f&~r43UNUsKvN`Bc-1%cTBbw2S%_2jCR% zjD{CG{igdy1#(s17TrAgoo1iM^aEqOv<19~C~k?)Ihxd|Z%`SjgUTS_@XvDM4k^N+ zsGlqh67>*k77oJj4izu3Cjx1${U6E|l`dvv?j^G5&q#yc0uol|sCxJnWnSQ$j6rLp zr1;S3<}O>o&|UDi%yE)WHd-wH!nFjytP?Jr?*HMQEk@z+P=x+?K!rD5vvmrCzS0rwsSSmZyR2P*)xe55G4!|Nno;Pf>;T}&_jPRRg?6Jje;Dt#8oEOm!AHn1E%EDBFii)R z!gh&@rM2SmwQ3(0>fMK$H23+)YTn%5r}ML;P7au8WHW8oXDI1y`GkYXFQ3=sXg}?i zAygj3gXEL;#b@vdMiD$=DRiI&x2TitGyGE&)qhCBAC*$HTa*T!bBl=2J&)h?JWKiR zS2k@Q`>76by_U{`lc)gc`L`xCmIy+x zf0r8!BHTBLNnb^4*~Qp@F%*+g7$77lo_ybf?Zx2>*TvlW-<& z>E%jb9BaHL@rTnY)}G}FLJrls?;vu(WXErWW)Gy%K;zCJe_ID{Tj5!ipl&tr^YqB7 zZ^M=CQi|T(>@CU8%-@q{(y%_8+KQ{lsqeqHoqjTYMlPYdDh`aX{-Frkwbp;g6@OFn zrsb89+^UU7tj<#naDv|@n%z7K;vud1zDkGl&EDGtr6_5s_@RPT^8Q~ZLP(M>c}NN?Y!P^CRMdR?N9O;r0LGkNx>b=+ zcG_~Eg`1e6Zz9XN>zcw94)PIV&k_Ot1$8}tFDr~uX>e`_TW5v&0; zzjD&~_Fm2epc4W3WkV^lAN1$39h4|bujA)ey^+L>PoM69UthmP#Rd4n$%d3>Y zvtdg1d@SIeF1S<4w-@#K?sVB3xYq7_ee;7OM~5R^5-=1QO!(Wv3nCsgKlf(Ddg+J7 z?;_E#Eu@2ZCqrf@MA*c#9f>s?z%*0MMyK?| z`mEV3TBB^*H-026(b0|Vd=X4zFOJ)uakn+s%aDX82FG_m1Tq+6)VK|A=R`H%?}s&r zMjectCZ4e832s(xo>*&?Ga07@vb1ThA;_3vuy$BiZmQg@l~LgPEI%h9RwnI77j`(Y zMV=LYh8C}#<>=a5hb^ektj0I6PW!b|^%yf@nR)#Xqd+v;#gg>KfE91?sTB)|xltoh7PP1=$EUbPwqg$5R zr!v?u)w<&gVW9eHQ0`xi&)*ZdNd&abTpwIoKHtlUvrNc9!)KCLYn2n$IoOO}gI%t31xV{o z^>o7$tP?L&vn1Cszd_ZN>I2TqzY$#z89ye+!_Y;44S6q}tYZrf+z4 zlI@h|N6d5K1YA&$>~|1hWnz%UK5StKyaGYcHlR93PWv*9tcy;@8jRCJ*5A2h%t?+= z0SlhO1eEL=J}34t1-T2AIDMw%oRG)a7b@q3BE34LL+POAvvGkD`FGiw`SE-45S^f>AK=32*Kr{AC7O&1wGE>!m57};}3gi zAC`NiK$3ycJRuH+b#y*MwPM8ST;Y*+4jpm<;EvJ)=-qNPj%xPW`JboxB@lc@EYTQ7#T+PfXO*g$eyL@+TmHa)vJ(G!zl zr>n;IHi1mGlGiyNt6;b9qO9j5e$RNV2eaDmN}K^>qf;wtO5LKpnj4qvfs}^Vyc|K) zRrepN+F=+z*M1U18ZIA*vZD;U120^ybI!@v9G1pYCwXt>W0NjbeiLt8r3tf|D1hZ$ z*1d8kpj;LI!S&p~eUKLQpZOZg`S~f>Q@2S=SIUK{=AHJJbwFcjc>5BQaj)r+p~&J| z2$6yfX&x#n_GR8*>VQRPkqXS?958)+#1Z&(sC10+5fchQFI6u8WYc2EmuD<*-oD*- z?2jCV;$(|+-fam41eh6zA1}X%^;ow~v%ApAeC4mFeyA+rUpPUM|MgG&(s7L{*lNlnD5zyGy-JJX5{|6S&QpQewq`~BCDF~wiu8mromqQpLijJYiB zt9570cWC&E;x&cB%&>q;XbH#lFaaZdLvOuK5pF>97Z1#4+BgDOWaop@^9`f;(dCoD zvQTwPa63Y}Ibth5i5Ic&l;;mHx&ddj)Zj5$_B^z8j1uGa+N#zAbU?$+z z4)o{eeKEq=C*pnfW)t#RB`2jbUGhtH z@57gO5XIM*dEizZKR_FX6#9Dn3Ikmn&9atP>z=mrWr0}ZWxD1?9wFh3oWz>xDLY%21$(TUHfg^h<}XcT_J&M z^*;aTy#LYZ9E|zFHsw)N&02tSw_IzGjeX-ZQSCMEt8!;sTkmS)WQPL=t59T&{p9Z@oCXCfCDb~qF%VV zfy&?9ZHqLYleI$f@%-$9^t=_(Qk>Q793UZn;do}ThRg0RwCYHFS;GuD_4(b*3v5~` zzzX1iQ@68l_^-|P{a0ZSbH2BEUf8DutG;hiBD@G=*{`Ud)t0a>N4tP;b2i?(jpuwus_qeJ!xCAF!+{+835TQ+o=bgcPi~YZo%C@kO&MdauKwr zdrdb`-}cwB?Z`$=+d%T+EZMehY%#=y;cH&|bL~T1%G#?SdH?TM*VmP=@Ou;Zf>;0$ zE7auunPBv$YXv3J<72^oxX;2vhY)%ZXVHGEdUyzT*6tSVNk)qLJnyCtYAX(PV*xr) z%7f$_bJ=BR>5~6^_8q8>b|LrB*kXo4U%0U)0^X-_zPJp%Hz1+~w{f^RzF##xg>|mp z^XaS*4QE#0e%+-QC7kp=M!9>4PvoP|H#wq@i^27*MH$f;5R##A8H4n>KqLa!2!W|? zgZPKi9RXf5;``HNudo@Y$z{ia*SLz%D;0DVK6+GYl8bSlJWqesZnd01GVAu>L2iL9 zgA&3s4JEq=gKCb;?>+h7;R+MqDH!C02PvB!!=>DmC zTVay=I`MBY8eSy`!5m1zj$|4|`v_t@zE!^*Nn!|ZQTgw!Xaa*Q1}MvYUx>SYC}a@5 z!s(sWO-^k{WhD9eEwH_sv7Q-!hrZ{6aHHysKgK{a3d(P!lc+{>#h5nn+Hh-$?b?IS%c z{mt}2>Nvs7>wZF`nwc7Q^-53*)OnSX?iAQdg`kslsLp`I>59a;mib=*1!N_xZl8B{ z)p2y53d#Ubd1l?xJSG4H)b&oryE4a>_uBK#o@=XctyXO?&hsjz@#Rovr_7#U1@%$@1 zjT_XnL@Vd!e>L#;Kg8g49~acSKb$4;^$Jop9XVe9m+0`PzJ#zyhfXh8&$2TqB_jU9 z!NRsQTuD^JI8@Y+<1zmbm9i;-4u|M={0FqJ$QC_Gvp@D)Kok2zO~1KcLymai|LyIm zRev%o?CJVcbQ2yh2a>TB{|X{mwZEH^u2Vf2?TnF{5U<$fm1Vh#(r9ykT_FH=RhGx0 zRIIyYsZqgUu!`2Yu^rdfYiRH-`71ESQN-BsJQ*8deL(F~b^NPr^MB(M`#99}aC<%| z{)i86T%B<94R;a+v2jqDwk?U8o!&jbcZ4|j^*mC$;(uFZo2Lx-5&8TRcVS<`gr<7}G(HE&bOZzJyH6h-gUu^mub5!^`UvRmku>*DS7M~|| z4~v7q9gP321%jm-C@$8DslPpqS%DF2bL&QDIBYDJS*3|fxh>H{K7&k^U2J&oo!@Oq z9nq!#jT1amk2H%SIl}upO3av0AT{==+f#k^Q!#XPtAqiMF~ZMnD$orYK$lO+i`??} z6h0T*;Unl9qa2;c0LNw7S`On4mbYgG&&r;`XFDVVNI6Y&XpbgadS7gy0X8CKo(cR2 z{5tO~KP}hwIlll6_l?n>pWsn}98C1eDta9F+MILro4WO zI=eR_UR$A;46OUm$^PrJv0u4iWKq=@zoIXn9@q~H!MtHL^_J}IUpLG?+TiU;`b{?R zM9Zt%Nptbi_uxFXbAGokSVmSfik`SSzW z&$wMo=Ycw}5G(}1#&=SEf$_CJGBV}dr{Fpx{MPGDh0fD4 z-@mZNN7MNZb)J8A-|`%(2RwoHDtF!J06-i<-AFCdx9{JpK`HKY^w-M3U&)G`4xwd9 zPKwM7x1I%$id$UNnPIM9KBb)=p-+GoR{SdGYd-}VT0Zk@_NQg;O*ikBTk|d!&C!;> zF2A4w%py7IeV+WU_hL`|$!a_8ret(nJw6|U?ICMC%L~o9wy}T=q5agQ+ug87{i||d zaK_ABYqX%hm>su>unJ&7_y+Na_X-qy!bl=c2kSUt!d?g-qya-*TU;h$sq`bqw&rzU z$B`tmfG&J|(V2mSpC|tLZSU4+jDB4~#9yDU)u_c`9(*lTjE{Uf!{w#_3X3yq{(bl- zz^gz`jtuAu={kdEq51wea0xw9KCy@GnmCbJwO=b z6{|$WrU*nYM7PVpMg4^oUaY2pjK)ZFd~NgSo}+eIbDqT(HtQ74fVI7h=9)tOwAbzH z$?{i+H{HG&vxc|r*|a@^{3m9QnVCZQIQ)0ini1GozQ;Uw9)|qjUS!R zYrgpvl{fjY+tGu?A`?v^pO3GLlAnf+L;!#;$?h>a6Hm-b@ohVM8o9UbT6OpGIH-si zYM=ce6+$+tG(S%SaUpYPJWH(a0C#bVN1&HMqzB;aWvN`^S|`Y~31PDE$qrz@b=#L! zRZ3x0Hrz*p``MRX)w5@^5-~1fO1R$Zgf4JN@VsaLW-ZIVs`r@uPXV^uWgiWE*5P)n zQ9>huBistQy0kjOiB;e>uMe7;c=U~jT5-4FTR)BD??+-oEYh~5cz1tW!iS5Fy&ygL z`~d1XT_*zg;v7gtnVW_yC)-`I+s~Ajtt9LbLD@gkrSV2)07?S5!?^_;Om`j9wx<*2t4Z6s>rJV0bs zEvX80(?b8`p1ojP=hu;%F?Uwk$#7MjV6ze<4gf4L%aHsNBc6d17?lz;%C+4-I;e82 z0dBs+nuQo>_gF`io0NSxMhSMlgj!uk<6N%+9EEwOs-H3|saf`$*nT91;UM9}PDveA zmcKUPy@JIsrW;*`y8!;ix8S`c+SVxt&G^+8RG` zK&E)TB^Ivb9QbR9@XLIp80(mFUu~18Q@az?KtMokiQsIa*Q=#~$WR&xaO^GYS=Gz4 zKA0#!BQ!pCE3f%p4h8%{>qQ1Eh-b+<4=VR5Ev|)C1le2t7@vOJ29H%uGmKI^9<887 zTLpJY03bjvYr@vZv~(U; ze|G=)_{dVRsFGTRn`o(YE?)(6=K4fh{{|2jdf@v`rRm!s)OSO?AkF$zE6lfS{K{dF zq8I{?*WeP_?B@bmzET%MxD#Khf(=8R)X!nnyV>Ch;u=y_VTu0!GMhMc&o3i?O#J!wvQV-pauhZ**PoJ6Q4b5`IwWb3%xvD zNGbNe%v)l7`E_QP2CDo>^wMhg!fstEOy_GA-s#Mh2^vufPWU>x3}2JvO;1iUo7g9Q z!m=fU<48h9Q>G~uAZ5x=<;w}zJnnx#pnKSHAtu-ZS9Md{WxPUH@L($w4MA!G{#>Q& zOw5|u;s8ogTytd3Ye3EIJT(%Qs#Ilh|1P|xUjQ`}_fgj= zH~wz}@9hb_K;9p%{5?488n$>_3fAE|(ZRNer=Jy}yahpJIQ|-f5KTpWFAGg=o1y*w zBDuh?qh!87z(}dt=F!=jn$X9O6*EyMzD^i&r#atFt*WuT_je~UjeUHmzH-mae;{nc zZ1F7kbv$34lFynnHI5`D;xSVlBvpX0jKw{(R+Rpaz*sa;?B&JG8hu0I`K!x_o7hsb zWOkhv00otjV(R@-r6Ixr_wWAIovniPhQ{n(QBY&^4Kbh~le^8AE-aY69M&Y34sdxJ zle2rU8n>1WL=q%RjiT%Xy~1pkFb=;}erm*4*Nz-Y;i;5P+uUMSL<$be%jJvSwn16h ze*m4GPdJLPMXyzi@=*a*(BN^NgVSot%z9UjFJVr%@NKv?nZvw8Xet1B)Sj<$T~3DW z3+~$y^fotFO8sceznOI*xCXF$t5dTXRAHtQjQT$`U1d~N@6$b(PU#R3kVYD$QzWFj zy9DV@fvdD2-7O*A-3UkuNOyyDHy60?`Tf^=?+5tA<*YgLJTtRr&z{|VY|8d_F2cXZ znpPa;jSyUQ3@TI5v4hVXES)t<^U^Y2B+eOh&3~_M{HSyM?t@n1zg(8?Ws7XOl${qlm*$<}qYXbq&5&R+W~XOkVTTU|+@V_0QgJ+otxnZ*N)c zUqHHlaq4S!{6;ga_l2{UJ^Wf(#Y^D?1a?0<)L<-ZVt0qNp{Asr<3hrE`mJZXc(_;N z&%CCUG;W%G;LpC3u{XObo!_^Szo33O@qYVq4;zh>3F$?+(7mf;Hf$O-^Lt$-`M^6- zuP_*B(Q3qExi$*z0@s~n_f(HYc6;p?pAZr9oO^hxN~g_KlJ^?3@jSQhW;yq(Tczwa zHVy($#v{LS-n*9+4I9*cix|B@k$kRUrA_kt#l~6&-++Ke451_4_JK zRVF~Ht8P%&-`@8?{&4QF|9= z*578FU_qe;vm|A@$}Xqp&vZN2wkedBqbbu)IqOTSCpedz>|vD54W}ifUY1W)oj$2+ z5a?4++?~@}r^A!Z(~A+O$*Bo-sVUlyF$)@bEr)GYmGwSfnp9#HKJ8o4rK;(oE@8ta zWKK|5_8qXm*3(B#`lP%*$P}s4RCyKCte-ykC%}53JFPheA|b^cy*;}*>$sO3w)Gkd z0a*Vqe{mG*PwVknT8iqc2#Su54*wtP7ab^}zz>@^M*7o0S)T(sy6-~%K($kGjdUh4 zh02H!&p0JlPN`5;Cal0INsnMQ!C1C~5w!3)whw*F$@>SbbIYEN5ntw_?c0s7(kg`N zw7e%|3~OEg3L8Ym#rVd`%D z=^Sfr$L+*VV@&??*&xRPWTC#NC8fND0oyCa=>@MX2M+|bXg>L%*UAk|=D$n&W-{2? za;n-9UoRhOk@>S+C_*`a#=xR*^&>iOrFv28saB3`7V6SNgX4)Uq0T&erxxQ{#jKh`SHNtZFD5FG|*2-2}`85~@6kmCcph zYA1pzTeZ8@xITOOP*6nW0KZ<&Ssg#bTw$SdPgt&SVuNOH#+r9EFth9We=Y#C!?kz= zy6EJZK9Q$oOZnAn|8{}ef*TidAN4NmP?2#K6D5s>woVhhr>cXCC#&}dZI_!3)wJP_ zA@ma41Ne4k7rTW^Zv~ysQDU+2`tlJwyTN3!o~TgMgOrEAqxfn#oZ?^M{y(yfLH_|g zmao2&^UVeO56ZN53RJ_WNx>C*FOh0-&bBk%UnzSQ@EsQYj&*2sy%a3I)qGhJ$GctH zl+m0RIa3Gb7>OizAa;EPk_uG$`=eJVco zQPvgCVcUh_&RJ^qyS1%?CYB9({m(6!CQdGnm|byfP^&N{cRwG0!F?nD*s|CNxr_C@ z)SIA@kswqMBjxgH6*5?;(tFT;7v(I9!;5K;MY-$)8?q2@JS+2GvUDuzm?pYz0y9~X zBkzC`!sv;9`;neaHcY)I$p%HjQS4DQ$$75 zun9Z8Q6c;(u2PW_aaHv0(AlHgHRd`cPOFs zV+}BKm-cH8S^n(dCxz&4L7EsUiKb>&>NQL8I~YrA*#Ksv;w#cb(le}TtKWg{<$?Ll zcwoFg2sA|o3p^*IAMMv451Yd{QM!8(DPHC8F5JG&t3S2jekNRAUN!b2_(qNe`I!)J z78>B{yo|1KQJNp}g4o&ePqr~7g}7~@3g8)tqkq=F=aAs%ujx*qG=@C|niU#J&9F8U z++ilsH{yL^;bK-Io3QI(0Pvd?AM!agJZ%)>-ZmKVW<_|vZrgPYq~r-gt@D+el-(#^ zF>B7+{j>gBXxxVNmr|~)ri0a|>Ng5m;57sbBh`EP^zz`a@oPKTW4-z!4$LKVQlZnU zvWQlOmT;xU>tc!v`S!~-*I@Au?Sh8U-i=t5{5iHdPkeeh&R3uQ~D1q+g^ z^N6!*U?BJwftpnG@@O-)76O&M6*Dw4F$rOtH%H@qecgn$^Jw{leJUmB$7|=e_P(gv zNI@l)elxf8rnk4GRxuc>`X+Dvc?qH?Z=EZ^un$EKHY_$1{*;pMi{BDYYK!ltwl5XH z9ox!QQ~Cp_!94VRlGk1;GHw-X6yw(uG!Rpd;t;@{RPTG~O0RExG-=0a`{+qKmx4 z##;Y;hjkGrK$+xrNUh&^73=H6|H@{au)*}hlB%rmA+NF(@h9wr9Bp3mtO{{;S1XBU z#O}`5{$=2rr)g)wYN;-6j5F6`{t&vyBVCer5qwUVe?`#i+q1+H-OglypHA#Ul~f(9 z6)_m1VE$`Y3IBw@8+f{jOpSqPybG96FHmiwUoh#YRNqC=J%Ti%C_EJ zRv3;mt+9?hp6v94?)$lMPbbe#J#AjeywQfB^5V@(>|ea0DlA0F3&SG6Nk-}^hty@C z=F2ysfH~pcLcgG}Nk@n7^(khoI_x@}wjo2MtLkL}KlA7I6zw`W{xY~;k}Lomy_z{I zIS}XlY7^D95hZjHl1Vv>g1~yqa^(JhQA-vi=kfB=D}qI)6D6rD6@O=&xcLiB1Ol9Q9oUuGM$JyAZk~m z;s8>I{B~tJP$f(FIt~yW$~IpAY?YFtnR$c2NrArB85qcO+-_5*JVfEq?4Ft9o|$-j zc=l;e7c!1~6cB^KZ*{xpktT1=ZQVb5y&8IBKaK(#Qv@#<+PI?LA>aARZDjqSS>9O5 z+4JIBoO@rg^iC^U(H1{3awKpq&{GCcf&SyXa${-hW3VN?M>4p})d6iCn|Lt98U_1b zd+VipLf45%M0j(oRH%))mvOc`217Pjy@{t@V8BZy>&3L|11_;*9nvpqD3y4P^hdOJ z8t5vJEsV?#srZ(2ejCnWn}@znQ}$FXjQg%|XwFz`^hyEKqs)0GZqMt|NK45%8cGLel07vc<9rViF?ff3w z7e8=~5q{KMr92@AZTmOBm1UM|Ok(C%aH*7m`CS6-z0Jf$0VBq7pP9uEU;ZCYrT1d? zowB{08XqT#6)x=VLyq|%alhWn%`Fk#b$Wj{u=l=EKD`2N}R^~dAUFb`5=ZM zQId^$dRfXRo@>$9_(5fR4~?{3suuo^H$0}-U7tyQRD!8>?A+CL2EUof+|>_8l(Y&J z#YT9MRD#0c;^fO$^&UdX8^npk?r15RZg z+TE|db3*Kn{-p>KG)o4gDou*EjNpwd!-PwRSln}4BSN=jj2LIVddO4HIV%(p-W>i4C+2fcRG zPqVYU(?=Y>-x8?*GF+?9OtViR8O8trQxco2_|J&-T(+p~&vPI$sSLEfTFx%_=M= zOpJ@xA~i?zI%dK5C!TnFGkQ2zp>p(Kdp+ujB8E>=@pxJmSD#n`4BjHoseZWUrZt|s zt4dZouw^UR<>dDKONpazLJqu$Y;!IXJ7#Br*XY6^E^>&_1*P^XJgC^JEy%@`{he%W zv_A0l;ZFF#Y&-D&YJVyWhsx1yI4vvH4I3gdBwjkDg0@=d060U=*1Zy+_XKW}#KcE} za^r9gu}SIInpOhD?CIXN75!BQ{WvAWb7_C*CY6;$eK?ffsO^#DMl#I9nCDaRkf$v{ zk>;UW!5H0g+*S~0m|3M|B)tb&6T!=k15fudm|l4HbNgH)Kc_Jk%I?YoqW=odDfo>P zsEW!uv9C_r2uV`rhy^lUyZs%ds^+kobhGB*n&3VPVyU%9?A3O!?R{fpY1q}GsdT^O z?k3)ROo2Vp89!6JbJsYXq-<6%7l- zj!>5uO{+6k%+3FuA<=~lwGAvOBg4hTD=N+@$D-22cPBkl55klko#1gBKE zuz<1G)2D7h);JT|l7wKNXkuf?AIG!nLC-n`G`(}IvXoR)Q`4N%fnyo0V zeWuLH&}w8U6MFwbrN%510NP}YuGg+dlGJb_R(TQBp^>T5IF>0)zlZi)^<M}0eb-yM1R`*^Bwh~9&jDu z*5`2)-o_!K_7LYWB_kdLfjRQ-yXkb*~+X1GP8US5)-VRR0^?5C3g^gcu#@jyBWzJ`6K<&*$P!Z=ZkHK8vVqfO0={wXbBE z_`Rb~YQz1veUg=}j(Ox!wLByK+?ws76|JiS7m3+UCS6P%FIA}P@(L0Fcm~fot)@P| zi8~JaE&_4)f3M?s^qNi`#pgjNtM`C2VF9Nb@7~C0jOP1yMUFY2J_M+R1=udzEXYoO zL`69I1wd^A+?woBok^SkYWSC3=-vW$=#Q04%4>Udg4;?j_NtE>mbSoq@M0szjD8 z(NP>$Zj8dLl5*vHuXvuDGr)DvC0KMIe%ReR**aokwZdCP!{R1+@Q`}yG%@j@pVKgi z1lD`&;C=WntID5$4u13iElBVtTMY$+xt<^!o3uF1TYHiiii^_AupQ>x)?ZWQ|T|t@VbLpn^pn+2SwvRYoQ*I)V^seP&$JXtQN56 z+WC&AQk2DPz1nQEz67DeX1b!O&Haq84K)pF@G-&0q7|#vRgE%7>%_jOvCL`nB-fkw zjJ7NXlZg!6R-rR3`bD)LS-I18tA|GuJGExRWLO?W|H$LHaTK@3#!=o}vuuzVCq|!q zX9w{Da&`HugPF2LCpy4U6-JHEd%DByf**d4r_C$>9FGE~y$TLa zED3T4pw?e`Hv<*Yqsf!}O`yBj)g(B8f%8c+fWLa>oGi))LV&t;=_5qnou+H9 zG=yRxr%p~)+6KE7q6*83tFaFTXwQyXI1JZNi!UoxF@fPA@1&zo$;qM55t zBB(C3KH}n@47p%n1w4^>Ap6?zIkh6e@t)vQazYx*0+= z+RvK2q_+-exYt7SPXR@ivIfY1uaA$t$pMpJ#55-SH0f^}kZ(444ge_a#@P6^s7d$+ z(j;QOhO)J0yR>_vSSlj0W-5zw?JPf%f4oK}I?Uy7q*| z&TqRPVjaYSuLpQay6UTG=X$j+wNlGT5HEJEV28eu(8bEJT(gL&Yoy}j6wi2NYZ6LD z;tY3slhc?sM%VJOx@-P3E)=MRgwTvn8IfJ=jGX6b;|=BC-9uTt09ibPhXS8UB3eC` z8k({tktzM4chj0;#eJW{B=vIsVfPk5Px1&|tW$p-`^miN3~BL?`42;F>#qv9E}O-n zAoZ9GJu@P9WwTOP5PWceG`>y}6VsRs1*vDY4WGEzXs?97SO-fbiyLieH{m~l(<|>? zhjVyu4j}97GJstuV9*0NOJyFu3L3iA(0}9)y1=Tes;U}0$i0u~&Ani=N;P$knt^xM z^$c%4>Z-sr+NC!>&j`NRq2!+UoLm(!^0+qp?_!+?4A^_a=hyO6j0jJjzktAM6Ao_WreyXyw$N@i33PW`(eA*aAs>HI*; zzr&5$7oKL!zFR))v{+y~_A^41^l5nS5V0d86vn%N@rhx;pNw3K*D00EzP>j?vKGYc zXk4gBUsSZd(+%RU=Ol4J%OV;T@QgHhI?%ap&d@-|@@{Qf zxaHMaMU_5Ban5E0XtnkA;9l6=CylH76q6$%1O};2f_j47xX0Z5 z@Dn0uCXsbSVHR5j-k7HocQ?=hZLa>z3Vrmr4J+GFYjIr<^A5V_1x;uw8!IQ|iP6Q_ z<)_|)H&VJb%(pyA&O6m2!yX}I*yWe5Xsx~qd=n0sxiu~>zwts6XmNnlgVMmW>e-?( zE&R|{SBT2DHr447&w z{HF_DV|J8C&EfLS$4*AvOpxW_9l<4Y#gur>JIN`Zezfk9^yYd#(HDU1FYV31GCz2j z;eD4jr3CC6TzxSjhRd@B;KI9izb^Q3zO=$8m}p{W?W!Po-*>v!HNFI3vGQ4m3xyMy z3A+fy4&>tAWuD`zz%4r7eGFhvEniB9i@62~Rw@UO-fXqze$^xQIl~04XGnZRSZA!n zUhp2f4s%Xi$ws&7d9^`|WBH<4j2$LcRW?HY@{9(l$)iwZ8VjN*+sP-tScnDr9Jm@{ zWdJ4K0K+Lc^qCK3*KBcTQWz11!)bASkvD^s5;p)Wy$L2BCLEW=&2_(hQ6DSpr+Lsx zBL8@bf{6G^Jx}l(#o%J^!sl@8%w6nU6IDF^s=3peaw^4z(;p79ZOdutXKBI1v4EqNzS0&wusQS z|@`l$}yN8o7W0Sa^_@8wTlQDJ9z z-~fp)+uUHT>Ef+IiNK+F(X!@dNd`TYk02BSF&lEC@Hj;mVD~WV34wGrOJv1(ytl@g ze=P?Od>+St8h}qkD)1vx=fUsSSg+QeR5k`rvAXt@|$t#jj%X34qW1NJ=^a~q3AnEVmdGuK4mGsn#d1qK+6HcjBCity2{ z#Wk;HcY_hG-BXhYT%Ep6_P4h#5eM#OX-^7$eavc`7a2UekMw_}c#YId?LCPw1_N|gU(ANcHC{PB@nknAb&Ty3bL96Y>Qgztt3 zWEG^QkClYjwZSq}X;yqnJrz`bs|axP{VevyF*tc?<*D|l1K%gP(lIejjyNqBieo|$+Dkao;S3RucG4Gk}l(qi=pF`&IGz<7Jq`)jNmZ|?;q^x z_V))8jSOyaREeuY47GEtu^!XO3ttNps4DI}q<+M&%xNraiQn&X?BzU#fo<397w~^@ z^M@+1=_0IxfnpZ^h8U8b8rl{U7G!Lin zItA6Q-B|Dec-`N?d+{Tu_Hxcmjk$1mgBHAE%Ej2=<|*$OEyZ`~bWx$Tj327E=!gvo zfbT{;?~Bh{O^FFV=D{?l$pTsx)JL#TJ;kVv%0dKE)a;k;8qLq?X3)^{2dU?+)evdBV9%QSf zZe6i?tQAEmY|bd;1zz07+*X43W#Hoy+^H-5FYdY^jmp0yfKlO>V z_$-oi%<(2Y@i9!~D5|vWg({hhVj9QJbr#JD3{zG!_VSu$>J}2EOq&rCA1Kn)}v*R z(l7pOm)8Xk%i_hjV?Be3uWdbkTFNVUNv1Y4FhSAVv(WKLX;I3FZf)Mw zU_rE%bag64eRzcZ3%(VKo|o8)N>~&vJap5R+hg1V|!`b$c;b5r~AM;}uJ(JwaGIw2v+V8%clG?py~+R}GbAj)Y5DWSKM{kzCPM3Bmk=3`sLd;g>?h?) z+9y;WcTyoRUYO!IANDqoTX^n!XeGShfBADWbaiO%H2AbG`HaRM)w;*-hjC}?@Ek}< zncg;6;;6h$NuQl0F=|Y#stj5nHg)m$nQMw^oVYzS%6`l*Bqc)0LVwyf8H8ZQ~=apFaU z=KZ%^HLF9;5@)YhR}F5v3)j~jcmT)uTfO;A#u`k547dDWJ~OR$`fy0h2^cR1pnFl# zrH)ls)G0jw7SA>#vtRKn`%8*kb20_zC%)@Vq=tnj9eg_t)@>|{hZTRAHA=1KWvF!u zQDCwpLtBC$TtGf^0D=}G2fVlltuND1FF(2)r*TUt8oy1kwYoS$F6 zMrdQr|8ioVi)N>=E+BB5#Tem7@&8x}IZ0EOuay?Xy{*UVfZ&K+rV$ zlPO&=$eML&$3I_Kxx6X4&NTNh^Ig1z4f!G)rDdgMo+3qMHM420slgGwThPtpEt|ie ziym7+ml%h7idJQ>DLKHt)6^Lv^q!ZTT)CCFEL{Exd5EwN5kyPFC*r)GSRWcs-U+HsHnl*W7{=>zC@SoH2Gl`sn%V$HTL=! z)X4^IJxD~+@mi(dZH5+1zuJPXhC-LR91TZJU}IjNdM) zoAwbYcj)rG^e*|N1HwvY*&6#SSU{$T-@Dw&Bp2@KPgz=NDYS?JTQIkYzvBP|oNhjHt*ovZcRY?db&fGxx88TXN41~*wA8>4 zw2wo33#r1|{)CKwIQen(YDzfmT_~vc^@fx7S!tQ#s%_H* zcrSHeNs88b$2nc;CDpQjLX??h06h^EZ*8y3=wcGuul7#3MRrbr;pansez;VG`mT~f zcz|+5eB9h_9(u*BGDDt*117l>&|oyJtz6T}&MdcPkYuv0vdBrzd{Ba^v2+4m@|o=? z_L<#x9oO__VLe%4$^hSEG~km|B;v)#;`^HQAy~Yq^t9snfQ)(r}#$>dJ4**`GBI^M;?Ne9@VCtWus5FXRL2RfZ<##}s z-b1^u#dvt%e@&nt@8AdIBa$a)H8Hp!BWZ4~jTc?n^j$oWXK7Ffu0<@@9JPaWxD)a` zA`u#}q1#o)*qTdZI(-eJtE##Bf*)RS<3%ojt!}|-Y9>;g{Sw5c&OocjX+K2MZ`!f? zC`O$`t?AVjrT$X1@1d#HU0^DzrI*zA_8isJlDtLyF#vVDGlFt>*o+pT&4sKk1{qlI zUjJ@@V9$2=dbmY6o~vAxJ$l90)XV$BW)2$8UXb}X3Lu4}u-XeYrF{3eQutBsJYyL? zNF`Q+Vf`4Yq|}O+HN&-}co0bu1v0X~q5gY1{T@aG=O=*UNIiIcLfj1G6`{VJTQt3p zFaoQ;BmAU!y!ae-9t@kKE-r-PsellP6s;>sA#2!|M)~ZLif=LPkZI?kuI6fQ#6idV zhF4!>57mvI=UEFYH9p>c=Lb5n-tA#S@zvhY#U*oC6-^Q=v+|)_x7+%XX!iwLRn5ON zeQU0~>m=0)ViDqh;ww)WVeT_^J-ej9O$=T{`E^6eDa9NPs-}Ju6ZdB(I}h0-Z0ddu zP`@QCAym*`x6?htvW+RHeX>t^wkYu~M4*_Aj2wvm=GKHXufF;o502B2%X;g4AItI- zGqX63h}zDG({c7tc{l?lI$8lF&SFId--~C)V2Rwqx?i94eo(7@m~x397?68JrFn=8 z58x?|i2#!#r!}5S4(3AD9B=-@GJygA#5aL(FB>@9=T(9J>p+`L@i(W-EHz1wfw-H2 z)Q_rt@Th{dcnEeR$W+u;ogC&Y)z zu}j+S&M3FxXz(I@NM7eQele^>eLB0=I8*+?V~Kn(rbjEaBNG`q{&Ldf&jd3R{x`6${KntQPgP`4EX%A(zVD_R?&1l4Uc4^ime|HY+d{XWhA zE>Z&QSDUG7KZw88gX_96;#i>leRvpUnSor*LekNW@MC_Jm42Z*BNg}t0VcL$Zz3pn zfBAE+520$SlAQfrp(IgxfZaJo>@~enp3@+@g591vfkfp?a+6%H1^9+Z--R$Wb(!A3%kqnH<;@YAar+XyL@TU|YU z*)G8IKy{J4tFxq{@n`7iXGIbPvtC)$Fx)(I$FeQHBw1OCzzOh|v`6DCR_%XnfCbU4 zci0GC*4Y(bAf%?yy|y6ZwMmbIbu#gxnz^o^1H&L$1-)(OkjpqIuM47b=-$jkosSWMTpmQ$ZfeBdxyAn&Vnm z*Ym~k(%Rk&{plgWC31-&T7s;M9sZQ(;O_40+PkRAC!5w?_LSVQk@BHsMmAi1`d#Z! z0ju!k+Ljn{5H-3qB1ltvYJ*&X1xZZEtHb1n6+(ne!u!#PWGVMeNkK;GP=W@n%V`4m zVjc8ZsA#(#5=--}u#y(cK@iaiFYlSb%^i#c!NrFhb^06d9@}hMi#;48m!O}ME+s`^ zbzPl)fcPGmqd*wT;UNAsM|{qg<}R}Le= zOoQxEy&f1~DB5P&Y?Io9S>nbu(XKVoN$VbREVVj}c&l{w6!ZdqcY^!UKexJhnH=H1 zvD1W&ziL^Xb;026Wlp&7CkODdlGW#i95HdhkRYFVpsv%g0!eB->q$(+1Jo{2B>o7h zU~{x&y~)b@%p<{cKT1kXbK|iRjXZV1i2y(?Os}=Hwr_%W$KoABU>H*l1v+}?Mdv6I zQcI1K~u&7bYs04E;gL-7i`8o0&W(_W7_ z_hH*xeHrZtPJ=?k>v%D%g-QYPjt6`Cye_f{7wKx!7XJ&@ZITm6!+%ioN{bPYI5zt zD%d~Ht9J&WQ(+bPzTdbsYFt>Hb3ZJN1aef?)1bu;1b<6wjXO&rL9AUfPnicqU#4w- z8a1(&FF)4dH2HPPa{l6jG-G)_Eh`C}IP-p%{kV%Zk!9--e>1h>gNU}cfr)QKQ1u*z z>7(fcNLQzQP_i7g%5=n7|7La#mzdSWrwge?1x_EIAla|7w=R=m;xHPx=w0>_`O)1p zu+9v0dZK!>qX1;%{iM04G>9oZ(Wkw!etFljA4XG^wmRwax?WCagolg&4~`g!X{s{m z@HIeAB!Ma|A!L4`?If4A=Q(S}y;#ru)n^pG)fNl?SbG@7t+U0C3(vn}D|j**PQ`mN z%1kg3rX(XQMi&9|vvo;R1@1#4n3Oq^gnq69qtjdJGML@-D!D|3@UE>4Amw=7#Vf=# zDcFMhw&+dvp-_q2R_#}DoT!xAhro4gYBh-?7AHVaig9?iNN0Js|LtaY2^so__!Z)A zGGuq-PJtY~Hf6#BM*JzW7RO_9Va$BS7r?-;)cI3-iY28{n~{-(pN%>Z{s-6)hT2x% zwlkF6Qak*Fi2~Xi#tek!A?%a<`n|lcHWnFx7r%}X41hnh=cpXyw;i(~p1#SdHL%{x z7RK?=41ackibx7F?{(-n^84)lKg}Qh8IZiV{4a^dYxv?R|GU9jp25*@D(zK?pS~GF zH>K)#K>KW40S9qoC&LB=IVw zxDumuEoU-^XskU{%a=ptU>u)0UT2dSeOF39wZvDSedA_D-D4=Q{cqmXp-+9amV;`kX=(h>G2_!J&O>h5Anvu0P7^XXF}jqo9|66+ubsbd;Ah#Q#8uq&KUQXK}O7*!0HjYA_0V zG#K8Jg9#Uo9PaO~rWAeMG6da-@`dm;TdQ4ia4PQYaP33agUUbFeC<4#)`jnkuW|nB zXWk6=p;MjP6e8GUsZ@zKwaq?QedK2M%>Z0%n-^W)1i0gfC5dZ|Jk&JCHgbY9-iW5d z8&b0)UxZZjQ`ODE5^#5AtkCD(nWNG54Qe0eLPw+3OeIFb!$;3m$h^1dUgzbqV~)~1 zi|3jI&`f=V$+oI=CGS1!%4S|04jxZZ&#}U71@N~f(;Uqe3TI_ygR|=-yPW@oxgH6r zvVUqYO`F{Hv6{H~S5$9XiY=e1(5l+McH`{Nr;!Il>!y*@(NmM-sHm-s*%tnt9CtjR z78S5=MydHxU<>(0On5t$i&p+bHWdR8p1+EZ%il=7ozu$7%KDMOsi6e)R%(Z4w(dW; zrlt-)xiR!W@P`dM*xP2i3XKbY$>pczp zB+pWr-0$exJ*2knZm%LT<0NP{e-R_pd@fxhcwHot#@@hDc%m_s!Dukj2;dXHhf-&S z+o;{sXZAtgWfd))KqK(kq)J{wa2pQ0t@ktWDuZu&HEOcd6-givXk$w5_j1j_Wn3PCK>b`DCo1-Ho3u4V;+r~Norrh z4vn)}LYh#utFoahu@YC~L%4g&4WOZU{trvQ>3Y04 zp93|UCJAw4pB;#BX6maa>_P!!?WYrLQfg!3u-2QE=WefF!4~WdF+DWboYtmTxXQan zr%_>gn7=@=ozyecfgVWsxx=ES^Rz^w{*%CuKuuF8-+A*%OBW-e9fFp|ei5%T=<39j z!Njv(eh~kBg&z{RAz}2SQZV`3K;p!jW1r9uGxxc@I8(>iKf{j9NBXanKQ{RiI?+Fw zUOc>s_zuWl<}`QFPE&uQ$Xh^rNAE(e!79cA8H*6e?NRT^=fx}$o-UEnM@+Rk4HEh; z9>1R&uGu!JIjgNN#u(&z6(W+!53PAEkq_5F`2vb8*zH(>8?_D`@@@}!y5Gl*Ao`L6 z(D#TK_JQ4^5K2@|@YqRMyp?}UPKQ8WKQ|n~mX^v);Q#nd?3#XZ59zVQ&L~ea@Gn_z z&Eu}K64Z$*A3?fb+9beX-pSU<($6c?mbXlB;)_6qZ1w=j+O?3oTW&M5!9dXQdL^sG zNSff1H{^aI)<}Q1LE6S=-(xt&sQHlV$1#@ktk)7Qc&hsh7HGN4j|u`+|D5{t@?^FX zxbcfWB1e2MJWt@U$yHAg<9L0M@N@RS!wv=8oK9l8>N%x?(AGfrrAQ){cy1l$ zM_Jr-&N*jZiEdH|Sg%2KkJk)kPjCcIwxegg9VfSMrfs_As)paZl7Bl-OwGP+?*teP zZzB-B)?7`=jMIbzShWa6h;F|)~cHISpy1$cAgI99K%eLYp z6VCXhu2hUx9SME>{r#_(z@06J>0HiFCX7^t_`9!7|6#AsJN4EW1b(Ktet_w?l8?pR z^ucCpLxY84thD9scn0BZ|1h_7!r`1E0^@AkaLbePO@V_91faXKMqx$Wug5k?grJ6# z$KX2vJ%GG{FZP$Z#20VAL&w@+E11$}g6{mkyIjYpfnwu#>0a zG2R&0>Ujx4s%4AOTkD#131oEw_%^^F^sTojocu^O4V9z_sR}LcW|4a+5W!RvSa$)~ z+Qiqhff!^`a4NwY_Qm6mK&4wwZX8!PzX7+2k&)5NkeKMFJ(h`rz_>XSL>hvov+htJ z%t7FcX}+0XKTBO6qVS|DP`3+nVn^{4Fw{YU3d_~ z+aYu_jfJF;>418JFDgZ~KTSF#7frtm)^`{pn==R{c@AAqXLr3hcj|e-7nJ-HHsp({ zs!h-!3Daz`R*ZQ$AL51b*QmR&8n(+$J!Ao*pKmvsV|K8q9oLQ8Ay=V5LW1(cL!~h_ z(eeWwFD=v|6oBnHZwRAh`gMT$;Uiw{>7e!R1fa zewW>Ncp1{OOwboj$NUKirn)b8RU6-Tk->jw_#&I{JQxdeDrdT=^~fkk;H9h8HnxNT zE?pi-MY}}nE;Wf&=fX4nlsauR=%qY^9k}~1aQ#*8O+I})ClTfsMYAL^pGbDIyxLKy zd{eKbkE!y~dFw{kI=ws999zFogqVfKY=$knUDqh(z za_+R`fVCD#48`kiHmrc^@FSyzKV8@0eN+P<5B!`Ul7a|v&97foyBT9IxjyNMlRU72 zswoa{bFKDoZf^dKRCPX#{>ALJF#wtt-V}-43oH>+N198ZQIN|~m4lmNUH(9S{}fhq zQyE^sr~0!~p0IUb`rX@v_PME%r`0k83&lq95vP~s_$>j5wgKJJKZdpwfW z&I<5q}}SdegYC!k23H6=m5 z)KOaoU@mM${z`huzS9mOrZlg95^I+|S#S8(4zKesAtfHB1ZnaCEtt-QUePtQ(0n+3H-$VLcWjgm`HjT+&x^aV_oe!9qYzqOR+Lkkv+ zKC`F%@xA*JlFbi3_xcMcm@raMt)fXpTQARs_mcSl)Wb_)bxrg_HcDr1PRGRmddA6; zn@i!1WX^KmkMz6CC4@Wq>`3sh&x)7fAOCgMV#Q47k@hZ9et9&ZJ>!qNh3t0;av(qc zk_zy2oH#dbm^EHP246;v>`J`R9|MaSvE}A#smzu0qGX0`Key4f4}mlXp!kpfQAF`e zgv2XbsH$+c;maGz(#6f;-KSs8!LhFD-Hg{%1a%P0(Jw# zwo8=i$yu;aPn}`dKK;$7M`OEcA-Ryc%>N+D`D3q?Ty~z05{0prToEg`5wsB{g=z&1 z@dW)$_dkn?h)aR=ur6Aj_fRJ-Z-Il3)|M7!D+HH15+F}mhC%XhJ5RsjZF+mqYrxrZ zddAm!(0bOa+tuk8^_t#q>_PaCd5m|fAwg3Hm`+1HY<7~5RXb@z>2}o%^Zy-ZZ$&Av zhS}CG)39=pbOQSH+U+s_5}%}qRS{13yfm$&>2l;y#lkqSg)bIpYP-1Bh%)v0T6vKD zV7J>-*%3)r_bSZ;CA;KT`b~9=f)+ecq>`)SFB9#U+`C9srroWM3{?0qg!$ou>(XZIKT~}^ocO9C@&JJ^YTh<&}+zA@xe9=W#yvRq=sH0!Tx6_Kx!4& z)S1Ti&t!%Ma_s@WYb?(j1BCGf@@~9NmhMZ0Z2t9kUr=rT7w*wHK?wNl0D76_xtA6-pQ-~)zu%5n|*IjLL&qUH3!OlY5poQlxnHXP)lsj1Q#eGkp)535E;I= zy}em(+>NF+f^FkylW`=#V>(J53RGoADe%_zM|J3yLpUsuOv2Kn$m<=Zla)Sw4igFT zrlh}8@Bj@7W$XiWNhe15P10giT}=usCkG4ZGm_gg4OkAQ5|rj#6(u*D-fD*;&fMkB zIv7+gHKtw;qH-kX?DyroQCn?9$IUZ|!X=x?*$(>o-BW=oz>J*|MP9e)|A>0)u&BB( zZ1@ZB%sAe|~LAdPeiNO!1o%plSr-Hn1YNK1}Lr!+`+OM}GByocxazTbEL;o`am z&Y5-gUU9E`-FxP2McIyD^XMWRU$~OMFRVLt2GJZSRV=9$(eEcq zKZpU32+ltbRDE2w3Cy48`OttCRIq+NH=7akVm>?w6!#(Sw+E-l1Tm=0wZLTW{&IuD z0rOFq(r#e&=&c)%ln2i%3U`-flCfp{Q&y$|GsyrWP{{~-b~&8ME9wVfs3G;9)T1e6 zfFdNuBcZLMsog&-15Seo4#6S2cRdFUMkcoF0f^weu&#zE8!xvTKPbj^o9&|m5b9oG zn9X;&zDR{CfJoXX&tkNnrOo0CSzi=-VQk~!`ytgRD#+{2L$!`Yr&W1575-Ga)r4MN zd2$;hOK3;s;X>c{=7wDI$D7f>kmR(as?#CkizDELKM@1~yQm|ijO@QB)?czo6sz~E z*XaH#BFI5U+wE7vWP6}J2d9;`#ewISH-4$Uz}EdPm1U%q1}_x_8X$I#|Cs@Hi+>JUtd=@x@qoYY z$;p)%89FarthV@$U3Tj{MBxf3Pd~n7l?)XHjN_u)v^Z!f-!+KBDwDpKOPtfp^wV6Q zMIa=k^8H*NgOLO0w<&OZ4S-+N&B+O2qoH6u!C0?xtCvi{JG%i)O1i{7AZG$KVx}(h2S{q@_3A)6$=M2p(AZj|*^ilAwMuw8eE7VX)h# z-jH&5{)L4<)7esPiB@UKlQ{Xkvwmzu^k1@+k@bIj44}zU46bsNQY?={}T`G zd|TOJ!iqu6-v1Vj37yH!*0+HOcjLuTiT>O-cg7aWB#07jIJY%-VdWpxH`%Ty>(bNL zDlO*@f3`LUeF%#gMM5EqHTs@kLb5-T13(%h7bhb;y?Bqdg0bnqUGK0g*cn4DJr)F| zw&%78@@QUNkcZajMvq=$RqN_SQx>Vs$bZ<7r1msvVEO&&nczf1edFoxwGV&%f+iYNl8oGP`ls{ql>#8N7z35C--u5_f#M$)&Z8bJ1ym{ znXkl~{mn8N*U9e+5_^``;6ZUinl-NHLK*_^YAKDE(uEfa%bqb^w?vftO@iSLyw$IQ zfNwvVXKcy;evT2>wOFoXu`Xbx#Z?SU9C?;)v&%VD#N>>_fS{W%<9pj7M4FfdxN+Dm zYCo_l#a45v&y$?pUNtO;+|r#(=@ov*F<&=lcPGRde#^nWZ;@q`)sL0xt7qt@QAF8` zIrZ$B#sibGPY<#Gvw0KV3dH;fZq=<`J%7u`e0Fg{v#TL=MUt1Y@Ei1qX3x_{ijm;F z${u*G9m2>2C1^O~wZ>UjxkmYM+z;ntO4sxDH6LJOS@m2R4XcsirojS38#-nx4`(T)#5`f6QX;=qBG!?IRh=2%L-j z_bAu^KH&NAoc$^YeF@u{h)5V3GpMqsAA3ha9Kcins&E~dWYDpmf^(IUf%hDh`{R#E z8kKsKmq@$IQ1YR^8W6HLlz@!=scW;G23|B_5E1D+DFSpZ%z@L`3I%jRG1|b-x+MSI zD;1mp7@lscaD^vvkhAqWwa6!^_YlD%{#cmow!HF4J|8)h@;L%({&YMdXoY+*jcFw= z5CA8PR}@!Db+#=1<^NWl?oSiJqCTrwP!u+hZ^$JXXj#p7%G%aGesAsZ&BJ=c*sC%gNTKMn8%u}rDx?<&`+9dm`t(sY_@GgVM`A4BU-eZWasl2j6%wGd82G7&U zPnut(+*9f3&|cU4w*nNYDptV0zxT2K{J}*0#}3W(oDNHedElR~YW96$qD5Vpggs$h zP+j@-oxKOsn5gpR#C3&Y=N@#$JFPWnTXNV#*8X|=hTuib>cX(HZPFCvrsCcT)n-CV zJpv-CR?SFD`J$x4>Wnv&>g} z6BPBF=Zlq)t2OTfYiXL~-{`WQjxH^Oo99h0GK=9n;z_1&xpf8!sdD2#qw!Bmf7EKfE z#J@0Bw8Tc<>n|M^yfCdZ=Y1vTb%M3hh^d(EL_;xMkPL5HOXw{>q8}>8{~A`)Fz&1X z{4pL>6)ENuLZc$rF<2_1`bRKS+H~* zW>fi0GZfVOWF5NijNPv(csHZp(?JJl2d7|Ii4VGYeTH^FZB1YKW#d~$uRXZM3IQyl zCM+oE3K%~vE#)~FA)f{(mc{J?*7Q%S6005yO2lu!&Z`Ozs|k4gxh<#fOkqylS9dzX zIj({~|IZ_xou*sObV=W@^&aB9RFe011b_wXV=y~`7krQsn+-6(dXY$7Xuq^5_%R|} zl4LAa?tI>mdS+(k2zm_){eqgPYj}ojX8HnUQ+GA*ib1J045|I)2ujG|TXRs5-8+{z zO2X`RGGvzf8a(|s<$~SR2i<^MbZQD(|M1Q7j`uH?`JAKw@>+ipj>p<3NMW&TY@!Ux0> zepo=RaCy$ImOSI^HB9j{hcS7!TyK9%${G9W*`UmtK&~6-dom@?)hsKN;HDL=`u0b}yC8we#DJvOqew7KfGh`085%G)*BS~*F~ zoM~iVIw|`S7IDCH!F(=q+%@Y+55_o9c5emDIdZT^4j+6L$CBH;VNCth;qLzSMI|_X zwZ8`~XdZ`dVZy7BylDB90It>LD-=5XQ5!KlYwBpTrB5bjgDs5h^og^GTv~E(EqF2& zc=3EvFio36SPO4k;N)kG@R&rId*=F3+EhN$t@vcLr0~dHQ2qCAMYnd%!eFB!$N>B3 zT;-%qwpPKiUtN?s#CbWaAmS8g;_N=8N-tkSdi}x=J+1B~<>dLEqDWsw)w>So3yQww&(RAgAqMqoPp( zXkGegwNJo6Hb>H!&uLB%4**x5H`4r?cayO0yacg_s?_*<^T+3L>0U4CCi1xor|_uB z;vULGwDGfey)d#q=&*%l{S(8&%@fvdaRDCuhwU$z($3HUf{p}}Z#&mP1;KULiOKJ} z-@|yTszSn_X);wlk2!<>Kz^-HG`h?!NGMm&)GP@`N_45jXlf9c*}qw+GV2hnXejjD)leT`w27AGWmG1xl6BPQi{ zb#R=M8nNH?g!)$+eTd7NNOqvo&}OstU)zG4vzD^6#!~T)3^jK10SpkmPj|qSZsd2v zGRaHhaRs2;;B}|mQ8UBMlXN10S7&3qwm8UI#fLMfOr4e&w zJ;jPm^}*RFW9A*wA5K9BT7_kO1?qEl?JKwhw(mb;@Lb6x{K$GNDv(=p_b>+7LVxv! z;(oP)iJlaQ0L;J+39E5ekzHk5f0)<9JOeSdl#m-%i8n3WP*-dZe&Vr~va&0@vu_i- z=N3DdFphIHX@?qD;x9n9S-;~Km;{sAPC&!q8Jw;vpe9UQ)ftVnHk+)!GOIy#Yw**p zsyK1Immtt$md&U7pYvE@`Yjj8jO?dIdu{zaqhGoQqy--m!2Pg^zAR~_Fs@R7G7un_ zRG0Gmd6urP13D!?Mlm||RGp33N*ns@85Usvt&g{I9D9TEuklGaS+eJpgDw-OiaAV4`%J4_4PX-gLc7HKrYTEp9UJqG^8q;i~@bDfsv zJD*#yXP2Jn>EO)&vRT6fTXO$dh7u0`$RQMuAKjZ7l>dsNVOoc|g2VWHFV$>JUBIjT zX0J@@Y@^L{e>9_uy_7UmSK)}8vu1CWiVS3ZuY}{!&9{K8JrblxwmvP`Tpfvhq2K=B z|9*tO5a?i*d9af2DNObqq^GHf3BQZPqctPIFPq<)_B!TrQgf9hzA|c0xceFnlQ+Fm zsnKEi*N zP6iW$a&M4cmQ|C0Qkhijvc-lr69*!QOSOyF$O*7I-oT0F)& zIGRWQ8-kk{;g|S5x&{n~B9Jz>FL4kgN-((F!!dQ96mtoDW z^?zQxz_vDX+JDTjZI3uxxMFkIc&_<3Rp?!$k~1DdnY>4uP8bMT8VuoQ!k z^t$Lj0${-jhQfipcP$l)YPM`!1z`#QBxo}+j}z>kS+{u(V8$76Os#63d`eN&yKa0I zmIpC(=8^Ye%O5Rih$`NhDcA&Q6{VmwF`OD-RlVO2ok-&;^R9d==?CSh0cy)91rhOA z1>AMv;dX}j!&QpNCL$gd=ou4HE#mj`e~TZuLI*1me4BHRtB=78Si^)03Xq7kRV$%C z8T>n1flLKna_6MPZF=rEvEdwu<6nfLc!J+1iym}Wp!)D&_)U}r4tkZF#wrzK(qf9s z4~`|x9{RPPBS_0kr&iq6HSIRPyLL%-b4fMdudL6T(f-N~g~RPt_iYHk|I#hgPxg+q zQ1&KCQk2}%xIlR%ijrrY02ZQDi#Tb{(01m3eS5+7^w2>8i@RV-SD;QZ#?hE{e(y&; z-GguzK#XqB0ZS9_5KcuDK~qAf<^U^pd0*6;qPT1E@9Fb$8nLpPPbH(Kz<^@J4=RGM zt&ia)I`R&yADQ}V`QZ^J=*?$BXy}IaKFMPpdcqVN_W2!HbE<>h&%5 z(DK+l2k|IZ6Q7=)6;NQ)5MhtOcoxi4#$@PD#;=zZ-wU}i#M)G?o zAd`=`Yv?2~<;g6!92QJc3eQSeL## z>!k`v(KpK`2*(;p9%ELVpsE!x{ydQ-Bg{^6owoxcPeYsBx#^wWv` zzkd5T4!&OZ|C-wX{45CHR+zHAn+Q&N+uwiPDv5ld+w*(r?jFZPuG6Fbg^g^o6Wd=Q zby|V6#MkyVc(A1CZ?Lfk=9WXCck6 z;#~svTWj<}O5*p~OsopE-?p8Lp1PEPhXCv$`)FYf>5Ml<2Aq78+V;Uk1^8!Eg-67< zE{~71zW=h-@B#X66e=pcj3ZetnjJ@(hV3{mqR#gJvABw+k&KljTo2%sIxY z3vFIhaJm>5CsknJ&hgHpB_*K$!n7VD@+RyXbI7n3vm#Y)_jzG1!KG990W#bTt;KFs z&~erc6pSoZ01Co>bYHvENByn5n%K2ZAxV}2X10=j%kzJ>SI>AtMk&3wVAvRR?7%-K z03DW%enTZjD*Kz4M_o#pxh5yLZUfm@ND6YAC(|*Btn8_Nd+qgrwPGvM27A>|pgjrQ<4$4QqbVO7jeq+wSHk zXes|n{vUi~f)yj)Sb6!v6$o}4F}j@7D?>56LSmvQ1x$VLUYGgIMU&h#7`j3l%C|sC zZu0c6JNzK#gvXeYI8C1P;QfgaQ&mU7>b(1#!kmnX_oujQ+AAe-7b*i>ry4bp9qKB5 z3+T4Yu*rdqAW^_x*GOwgPBWWwN>50uHQ`p<6$pZf(!7=f3AV3$KP|!XOlCr~+KRL) z84u68RHpBn92_bTuY?7BqAX$$Dc(6f>nVF@wgr5r!y&^2%b@XG`O#HDHaa3`%enOCWoogVe z@%4mmS>@i{|9iyvRfTaPT=Ale_D7D7;O?tBH+A)4cfkHsf!IqJ`l#qnRqm%YO(1?A z3r_POZ3<~Jsu%*!lf3A{k^VpsPeoj)fW)if{uCgwCBpjpBIuD2BJqQBNVu7n2wGl} zew&v9FdiBq3VW`=#AKvA4^n?}s^8LDa$`h`Q(s%vVmdhI7t$#7iObamC`JH4P|TWv78X%S$iF8kvS?8q@y zBD+eeTagjI*!O1|@ z3Rey~EC6W40r49_8U8g=f%1gy=TEU(()}&evPw*m-WO2S1HAskBpE(OcL7?m4Jzjc z=Iy%g=r}993C)uw1+o_KJpVjhNwo`NVER`dKiPShF(p(wUfx=MJg%JJ3iI04xE|m3XM;kdt-TarO@ za021}%?%n-j3%$+5_^Gk}OvBY{K^8 zoN^~~!rw_@rpjM1#2nPq+y2yvTXwi|#6i^V^I4xf&js?a+=u{j&oUI(+l@v7x32cf zbL-ZBen!jGe={%<9`zmXZZ(Fb^BwVwgF^$8_Z#-#5yO;rT(zMP15A z4T6pQE<0p7%Uyj>a{m^n_4+LqSBZl3QY{&)!5@ad+(e?0OxCkIMj}yDSdIVW3mY$Y z4$)a2J(T7p+;P6&Ou$3np8d0<@fZ6=J0bC|q#)WX+LQgSolC#rgOL#Ulensm`$KhI zj7DTHTg(a+?g(j%@zJ_o!m{5iEI$TtZT_nbfR{-F-qm8n#!#r_C#=u^fEQl zOGrG_xdZuc5+lwc5nuc7iU#NEihf*=TBlg*mEGu;rN}qnbFjLrlo?Om+li2HX9fYe z!tiE+gTFn*9SP(UUf+sE6nINDWKm;97x(v3NkI{GHdsL zc2ntog2m7)@a%~K+8D=gDh%+1o9g!P<$)Rkb-Z^~k;6GfH4~BF3=^Kx>m(|`zr$W& z0U*91QQwxwU0LUXunq})EbYx4becUX0x3+A{lxEX`lbx5G(Ob(rhKesZ+2*A%D+oN zQ=ZMyPu6_=xQFr~@EJBly?qtHSdcizx^R$&ce1<%iEIQ0mH=gY&nUqG1*M2^^{LxD zCt`-FNa_h$`)92a#p_>*Khu!`p@#%l}6BnKo zmD*iz78jy+oqBYBV2o54nvrZ~?8*4aC^#e0%(}qo!lJYNIFA{rYd}jJ2ab%$Zp?wC zugjQ_!9A#!rT>U!f&=A!6F>CPCaQCTx0_>SqYWT?{HyTugc7$95jsbqXAQCRz;1~X zHiJEwPpUzUmN^u3trJT=UmX1iB%d(GX?jM?&K2geWc8%%bbpIe#Xk0XDrYFh8bO|3 zq24anFMyO?>QIU_6}1uRE+|&4fboZhf&TXW|4at*7(nml_WJ9FNcz71<|_q2a#;J5 zqe`tp4-ioUfkTPv`tR=Rs2P-$-_fa`O3kKMMe;Yg?a8)ASN)c%c^9ebiUv$sSt{{U zfH9LgIxfzM-#R)jjc?o4yfY0F3c-26FC)-LqM#bH`5@t^`O}TRm8IH8`dGoyy^($4 zLw$&OKih+4r2k)udO>C;=JnLrzM+4W^=qOXw=KB}WdXDe8m&8KBI4nwge~Fld=wV} zh;ni2mvtP^C_HT$fVM0}%B$w+P!TwNt`eeYGL#a>b?7I*B7ASvEPS zx34+L+lpZIxz%7wD1iJ!dB>X4K9=_PA)Q?ASl`|c^!ykXR|E-{)v{-a8~D@VENAD# z3Ie1IfRdby&PVtIpg&Cv>+#7|3wfUIM+W*E{um!7Nwbt6yUXFb^pg={^xJasTZVG% z$xmn`e>7F#y+w6XuLm<2G=70R%`WQ+6lieuNE~gS+b3+PpU!>0QrLd;9j~JL=7%sm zvi^tL2bL+?n~Na;9)7X<>Ci>&^(m>Yn+ zp5bJwy*kL9g8EPrI1mqr#f#HOTl&H@&CQQYdL%9j2|ioV-1<7bDzh#$YN*NF${99V zS1@7K&uIUF`iboZE|{E|!gYkQSfrtCT9{u>FD>{@xqrzt z)*}SW&BXH?gkQusa1Mo`NvreSxbHz0;JE&AAe6ITiyR`#H1p4p!Py{x?aFiwz5B z<+%(kauGA3H&-3~z~DE`lO?}{uqWti4QE>OPU1Hc(+Ne8oSXG}=^hB!?F9D{kY1v-^iOI)lavSW#KDDJ+gN$ixHBUt?5e^fCoixMykKODhR$tc3CKAH`b7|;_T>+oUMDCm$I=EQMvK0*~b zkyKCLj(oavT9#2Am|u(Z0I1T;g$|Ck$t;$yC>_pNKVxFJqBcD>*3tQ)6zK)5(j+

SiNr*;67OFD(Q(I4Ud1bSLm96ID?W1KS2!c0Y#mO7GJmkLd%S#(~-29YZq z`L4uCJmotbPT@6w`*!$=M9i=NxNDf()=45Kj^_odbu%kIs!P(t+fy=IZ}6vQhA!Ghm|>KvaglfbI~QY?fs|U)4w`tNu(LV zhh%md8$0lp+0^CQ0IAVh)zTVO{xvzD3+5&hyV))JEvQL2euJ7_A4OX5$?EfcSC z-Y2h;c`((ICM#{(&jk{XYQOrTDK5I!o}pUep(5ikDtNdwaiyoTQ(F|0)fAH%K<|W4 zKa@*$vpgG;8PN2DX#*QQs416EUN(aS9wGe5&HV)>QuM{LMsI}xbC9_-5k6_pA!DyI zH2`P(V7C7ASrBay1f;}h;hqUIF4I8H&G(C)za+>7a3j^2PG2#dV#^1V`AUMSoK;c? z(2AT~o^OqScp(n4(Otp^)2P4!piHE6BmW^lml|2!Vfs|-pwFPJD5hp=_?G21nGyb( z7>FiVegwGT?HFVIV%0RI+_do_q>MP4@m!3MV_wX-f*Mfr%Vv}es?Qg;SJvgoeW%j% zt*66>Bm~IxjUvlscVdZK5X;Z^Ld4uAR+0txJSab{kg-YO4EJYBE*>>|qgo8JR3%+a zU6B2RPsHN;pT*l7iQgdLKLwm&61Yol<`nLU0~>N#TVsG+mmEe~stis#MSxl26D@oE zl^1{hHU3KqC59<-(Mac5MdzAuzC{(drL9@v@Hj?doPZ{o?R)yf1bX9!Z~gIS*TFsk zQ{WwGG(?W%N-dXiy4H34^(#Icw`+PKob!1%*QTWvqu)@7I3*Pj{ZQhRD%y`ud3E`r zd&n`0ss3m{XJne6+*&1GDk+|yGq$&pAn)ok~>g@qb~9>vc9KFs2|l< ztvZ7FQq%x0HW zrL-Z}bajg}bjcK`G;oW`m(3c<3uL*AwV4XFOf({qY;`EY*fLPw$wz2CdNT0qUoR0# z=F@2VZUQ@0h$U`gwZkbFA5ySFuNz28z(7Ov^*w)>??*-mJ=?uM;05DO(5eTeXSG>c zy_x=91J1{Gr=)uO!gv_>Sw*-%GgHUxY^whW65(~GAvJuurIx{zM|7Qih4d0R@7SP| ztFVVCe7e0h36;7W9&AW>+mp9v{pS5t#ek^bekh=}4`hb_G6(H#BhiU5s_Y8-I^ z8QS|>ymuv=%YvuNcM|MS6#eImKnGPUg!m}!5;E{jXET-z_a^(VAQt`zmVBv+_K)Ze z-M3~YfAr6hvBKM>`b3EJ+ps|?OA1ISpBqs{n_)H~J+2!Cv#A%)B5K4~nD@;vNez^u z(R(Y(H`lWf-{#*5%ut6-kW66kY2sIVEL-CAPMmP5Q!)-nYSiThE7r#kRAcwFAgRd-=TG@8 zI=BW>7;i0&#?Q5>;|=sWPitt?yU$VlGe=xBr1%5X1i|_bW@t^wd5Za|PgOAK9^E_P z%wsE@CFJVhS@=~4#h{kq!`tHIC#s8^vGNnA7i-teie1CEVO%2T}AMpK}QMpJB(R_ETe&F# zQN&1ge(gvx!=i_i@^+(P7&jF+727!bUT}K7pwuKwY&t3k(tC0q^~ClYhCvjn zpNvuAotuwgDz0KQojE?5Y~Wnue>I>xA3#N8`mWc)j;mZN32Z=5Q{^oboj&+cTH9bk ziV6;b(?|5>3ht_NUO>UcIWq<@%sb*9+H%u4(AeDAsiB9Rcs0d#jr}=`#t$H5KgiY| zHnA;7)f6Q3N!Xq$_CbV{9nK~5xJM`8OxgseBfe$8l z6Xdb^Is-s2mqL$^ZgPTLJJm=bVtOu*XMlR+Vi_Rh>C6eq!KJ({nB!hTuE{O12)rO| zevR-wc}j{b0s83VlHqNYzc_qTF#|O^Y|n;lDvK31&gNnVU&B^%XsH5P6uzaPMIIXS~=N zO~k;qSeg2;k2}8O&Q>ZOHJenGtV=v5g*>ROLfz%wzITVa*#`~MJPve557vK;k7@e_ zbEuY|7TeqD!y!J&I~H0OoOOA%;Gn+}MZUC|x*-TL^9KE;LjLbR^FUL=zI99ens{o& z_#8%jb4X@9%{)`}?FkThH{K%k_h|%jWP^7tv%HbAwyk{L!YJqTjo}g|wmz64k!37h zv8wy0AqvehvJ?CL@OHkYhc&DB*ue!IUCf1Ui-QMy^#qtQ?vel|o>(k~1X?sP_V<-D zQQp7UZKe8T-@K(kH$=lxEY}@=6|ORGW$h4h`i(`gM$fei60 z%kL};QN~O-UG5pGD_3jn#K;O;DBEe~`R^-i<{&X7X#;=nJ=hO2o+0qQJj;i_^0xEI zrn>Kps-TWkE0KqTHv{K2E-fbN4~V8aNaE?`Hk`jKs>^NEj!NGC-S+WJEVYqs=OznO z^ZmD@fjQB8_?sAL${pQ|j}6Gb`X<*e=>NmPJ#%`guH~8iS?5ijfNZS@4ORTBc4=_S zI$`-EeHax4Py87rx7#?A9?=DIoBQE1JjPf#7{vq55lV z{R|9er-T>XiV%4Wm>qr`6PjGhr)QJKC+DNl7*ui)&}iNdz}i@xN5-FUx##Dv~r}j=ho+r{UUcD;QCc{99-EbAZ#J$W(ajR*HSHG6Be~Ew&b;a{j zHR5uZ@qMY6(hI!wpKhYmSl;ncTgF~Gsd7*=vr}ocWcDw>1V!o+*T-VAOP%CDdVdk* zx=uQrSV@3}+Vo6Q_k4h|DZy%Bx_p2Tz%EcSv_^paQKh3inYG)_tYPsS_qpMB*bWX;58^`=5lP2(f9&#PqS zk{B(^aAZ)o8~P*$``wt70zq7y4fBI8h0mCXdv>Nch^&ED+tjyvQa8^5$h?Tv-Xgyc zQ&K=%^-{SbEP!6qrl1rl&|zxr|JI_apciFk0a~O8%Z%$cn-Pi>H&W&3;yB%De(~#i zO3rW1PES1!1K%{bNWD(-su=PWf>o~%z;L|SL04xkM*MBe3##<)n(IQ@|6S$atyG0$ z#@ZhpiV&z;K_E3jo}ELESt|;gk?UqgVb$S4l;{JCSQK$pOWqUglRqJ`c~riV0d`%i zy2SJZ<~bDDAD?`>=4F@2N_lS4-jC~8>)J;`ARUlvd)t4}J7RL4SyrOmX_|r+l^K1a zgHY5tOsg{xeW9wLmrtKED<3AjOg7x3lQpNmlln$=Mu>-W@3TdKi@A9l;*1Yfs0kxR z1h3G?V9VY-y6qvZ(vEkZ$E+f)6m4QN(YVjVjqfvYOG`|j7D@f-Mum1G3#cQ#Fqnd3 z`8Q1HfvOto;GWzXMb1gJ@ECqS{+y5cm1kzyQxs#z`0zJ_(P?>4JZEG}->MzpA+a=4 zIKNc)=kpda%JU$FHVoSyOp?SI2+yKVI#seL;FZR2`&ub59zVfB_y_??1yD-~Bo(6+ z4QASfz;gTk_I?i^gZ5gFC@32e!az1!AP{jOk$d;<)Uj_7XL24QHl}1fUC5h0WEHN? zS~=eF=>51V-+a9el ziSVXpjTjnO z-KjokZwBrT+~4(R;sjIx{!m{^B_D>K8>LceG$ORl6!8-WPTpbBFzPhTb&KnfQqaNu zh3T{~{Emf*o_u!>d0)mHZTXnCH_~bXokx16!nkmA`pAXR?Ngjy({*U+GcC?@BNpdm zVM}R84PveXmG(bRTUv~k?`b0V>J+}lR`GTT%Vl0WRZuyMpki-=zAoW-JC4_tj%0b! zjIao-JRa~gQASIXH;GiN*EeZ4Kr8RhbbL?!CO588fx)^@>!4!7V0lsYkU&Ia)D}>y z8pr_;o6n_S|JXnJtZIU#_W77$SJ}n&t7wM|3Tag-K|fl+@Y9EFp@?fPOPFDn-J`Ec zyG++@IpxIy>&06##Qo3B%2sSy--vLM)q7C-zSn-!ml5gx7YDsJ2|*SFNv60mm9?%XI2zU3=JYCO&#U;@H@6v&k#<1Wy>b!*UtsF$n8~ z_Q2JGYl{cBJdUmhdUHe~|H`3r^fyQ`9q4(4p08$Ek@Omj6fG&vR;ow^Skqx_mnyL3y zNC6}KDd(Og+Sj+AyU%`Jn?ocM%+n~$aTVk^6W_0~O~{jJN2@Y{q&S82#qsSz*ryRn z;a|*SRPcT};O)yA-wC$qCp~0**;QN`SXhxornI%C?^Xt^5jlIp>rj zKQ3x(k56<-KFw|zh18Bm_A;G*YjRL5+hjOQjV3vr|zYOQSG!r|)`W zx>n4S&_XQkEDa0p1pR{HERRIhAP=9#fu{+b49351%rSOA_qw8okBlxc#Qr9}U4Ow6 z&TXNmm6KJp1MrK(-tcbW8~EMMJ6n@KW+1&uY?I-cB)@&QmLQBvbuEOkinsC?-*=H4 z&5e)6wEcE_MYn2X2raLlSt2mUTSlVk;5I(a{Hu6+Ke25*D-BFh2*UtBIt4K@n1v=e zB26-3iKDPAUg5uvA;?B6!>SqR6r#>aExS3dqprh8Q+(+v)9NL!Z1fP}Bl+;lT$(D$ z4I(2^Tngu32Hq8aG}Hzl8Y#;rfxFliK)inuI)Z=me+%Xew`Vcf+ZZmGAn+TjL9`sa zzL^~pt&*>mBQt7*R&SOnDE6&Iul(T!t0U2KoZ$N+i2fJ-><`;^ zbR+bK%ct8-CHk*SF-r&@-~O%5^&8U( z{N^XfHE+6;P`JwG{%=@LmG^7SLfT__tv;PU#;K;6yp;%``vtlp`)3H9mk9_SYL9 zOM>v{d?0_)h&<{YCvbZd^Hr)pQ2)=S)*UYangcN)dqdlC_dkb7aX5mHb02_Rw|ySO zrA8q8Sz4=Q&1(O+NHfDy_|e7rvyVJ%oJurkIGbWU_PGT%lcbhkh`Cae;!Ykt&w`_M1<<8+3`)BgiIe|II+m9{nUz%?{SQGg9s^QFy_;@(&1WHB zw5kc8pE;cx4;rs^ydmstU(}!`Hs>4&Ip>wz5VcvPQzR$820L5}jm!RLh!zb=ALAZP z;|S!~f&U?4Uj4h5{1%xxF)~N%>b?Iin>^Y+{_U8iLYs`iN;q|Ot?>1k8F(~{q6fW( z@00HHYPvRrya3@{wdvNYqd<@s@5jf_18Zc;KwUIR_ zPA8Z0`h+u4m8NTRZNHbDd!D?roph|9@N;pb@2Vt0_}6whgjTpi4aP&hYZrbwqA_dA z_Nwoi+T#}CoOvf+>p1^nu37}PRpVZ%S@>Tg>XS%`zn%OGeI86iZw=*|4QuH2eOCpY zuT(vcwRV2Sb<8_IoDhW~QA5{i0UsGy5d$GDRjN7I8TFa?r=w+}n`K`_lwnTfW}H4L z>SbGcY9Q-kB+`1x3K_t|1g$BCc5j0+E{FGKa-3MhwondF*%!`-T0iBZ0VA%!&i9!q z*>q#j1q*NDw%vSHd%MiCTk6#Icw}QkxFQBiLE;j;s~dGCSySW=V7-IT`vB8<`xaxR zF@_1+@K-eMqJ3@V*ErRBa>gq>6&dbVEH z#t%jLQUvSlKBOPZc?N|Z@Yi#s`rLf;d}Rqx;$xngZNI4FSV`?{H*Q6|M4XjssE0yC zwBX^Ohw-j(Yx^m@>Wk)0cOM;0%2#+2dOph<84%_+Y}TY5ItXNEtHb~DZcO2$4U|CI zXYIW1uE5jL{BOUZ&U2AMfv;`e?rAX#I?X|_PXQha4|csSUw%`TGL??MzVxlSzE>S0 zLd7_@?dr)Ht8h;t*jOGR1c1#8*pU@blE{@$`?B55w4zuQZAiM?4p@7YBne=YGbZ2Z zF3CSNH}Uwq`K68y!vv%id`0z%Jp@LhdXXD16AEQ;$=z#0$}(qd zEE|K%XDr#hmPhO=2o*yx_-cO8<2q^%0*}?k`S7La z4bniVeZ@+ILib0%9NB*D>6R!dBKtZN-;%ckRj-LIdwopXb=%}}YAb3xzcpX+1q&aM zEds*39!R!bkkj%jeL8>++uou70_U(xqOPX2zS}NpoAu9q)<< zTROzc1H+RNUO_mr1-0-B(&+K)RWtCG5Iub64^-orAXXO8;xA0d|InpDIJh(2pGY9O zpEe>Ru9Gb!?n0ld{DKLPNI$uo31l~n29TegxX$x3F@fW(XLlE(%Xi29?79Arr0Blr zzb;&58$}ZPvvgV}l&u?Sh&=1|SbD_e@Cv;+wPNy3MWL4GJP&XPx~!FC?I!>;r-3q~ZYDr3|0# z#I_GHbu5Jd_mZjybO@E@f+gnVazA2mr^Z?Iujj1l@S56Ogs%`M4&w74S7x*KC*R3F zdG<$tq3P|Fd*$X4b;z+1B^GZu?7N3v`-IXmEx^?zpK+L;S7{8KUIKm^{K7#orF(Zo z?-viPbq(`OlM_H9H$ge^ff|1`G{A)X|8w=g%J&C3sF9~pUtNdZE%qB_YS*@@O<2qY zMj6F-JhhAguPm@pISQ*^v|73=DsjwfLaC z%ddU`>Hd`Ktuipt=f8i)s!puUxfVc%+CLI~VxWG?S{7_&`WiJK?5;E8{;nT~;nq|z z;LQ_c*fK|{Egf=+2wo!k2UqvUrYoLsv(-j2{sT_g_VDn`T0})RbpJZ{&w5srwE~Nz zK8WTL-BjJj#R@PkerUSQk0GCz7cjoEDt0;ToYGMzUx|Eflyg(qw@mwB1)(m%#B_>8 z+`2-Kt^$u{gpW`M!DC1n{#KJeHU|G>#Zxt7+otuv1C+5#-|KF+@${BWyJR6bRm%3wsMRM`t`lP7ynh(kXhsm<2z#sUFm04|wJUBft=k2oZ zjqzKkNM?JyKg*D*jZ9tRwY3mnTp11U_5T4@9JQB~{}Jw+`6I8dT&_DOtROkdkZEO! zB3)#-=252R`~|-F^!McmtZL@JWhf1BWc27zQ3&WjUAWCR3`x}y!>fapSF-wS&2koT zVZH0UMWFTm#|5y?kPEy`wQWSpxuTxfMs!4C5fPxv{@z`Y|36%PcRZDU`1gGrAtNKQ zDJm-?J3F(;s%#;Wy|d$-Z!#lE$Ow^=oxP8($jUgjgJkc`8P7-G@9+0Kujl;H>s6<7 z?)!Z1Yre1dbzQ4(eG$feK8(NRf4mbh5|HJh?pO5-rSzNX(Wy78Q>P6||8Os!co7l8 zm_Gc|W-09_%kwqHd>9Xx% z&r1&C;Wzf;ihQ_~-`-@S`RZ%eoS8YiXxq|2h!vahsk!nG?dkke$u$H^yoW~v{ejj^ z%cv6k@j->lAHC_&3qc|zE@y2!$a7~k?`sij3tz$|h{;+|`&-l~egH}cV3W9T4^4z( zVCBZIGY0-9@;*7!)!6wy#lD%~)Ad-lnMNG%Dw)}?@husiI~jz|-Qe3u*84gX z{ZS-Vk`dM>dLK322s?#b%Q)NP1;y7@G)Ph|!e?rt6FFJ^{jBHZ#9O%V3_AtJt`AG% zKj?|E{$K5W{Wd7NZSJe{uuucfwZ^j<%+s5UwIarZ;+Z^@9tIyAYh`@-=0*+|{N_@r zOK(G2p}>}!;q`;p@xXSF)XJQk3o(f}(oIpm#`f9jhSfR5#Qx{~=Z+niADo}5@93ZV zIRxEz-rRZa1ktn+==H(DoweHmz$ipidr*Tj_-I!6>Umr*Hh!)u!6KftTe$DnE2ez@GVyXb7`IE*M`%E46^BsYiheQ2Z* z3qfAvj{Y%9RcOo^8Z0=tCaq(ExOH%m3ZA0zTW+apb{fQ92F<08a?;n_xzEBcJq1v> zMx8O!@cU&G9xjd8mHe_)$lbRR4h@9{8TFOc_-hWAWNyqB4>R^vuFTj{BW*#yZOHqp(L8+}z0#$A`g!n+n*= zW0H|~D7E!w2`8Mto^+f%gine;O|Je{Oosv~rlXv?!Y)=D)X3-%kZ z5&leGNYApSf~`@g&#qgllLCACa;(ogqlN2|zJ8P&PZ;SLh>alwP#I-bo3uLzCdXY- zMR?)?k@!nofMJbC1Kyz5baIB_K>K5Dw&Df>{BJAXmXq+3en(;vBXxMc^{M^UH}$xr zh`}on72f&mN7^r`&mq6Gh1DMN<@55giV-y>_q(+=%mx4@rf346hcGvMA9e_N4Bv%2e6PlL;x0WW|C8=v>flJ-DF z8OUqpbcXKb1oXI{--NwV03}TMH!?i=cMI@ zzlE}>G75Z3jPc9{@iQ&iG@vv5Jul(Me{(hAtXl>4_Q!px^HY&i-2y$L%dq zKDANAt|=`@*YB3A#1)hb-UMWATx`$Oq*)4+FsCt|pPw{*PjI z_LIcETklD|#L3l}n{=<{mzF+iY{4Vum@rTC!Ymjk`}%vh^4!$nxz$+C{cnAFdFmIw`;^n)qWObj2Xr%V6UxfsNTxqC zCWKoK%gcu=KYyV^5Bo=+0k(63e#)b3NEeUIep3989(U8yNak<2tSSo4Nc% zsxuykC$J*g>rf_@l{w;+B&9FCC1eMnN-EgM0AyOn8WuNk@YCG_vLFc=v}w9k%`8N2 z9h|<_a*SXQgkV$LWZKW1R#im6G{dH(L-WAN`eEz$4rbrj(}My3Z7%m_!>J zlwmTU{+{4n>C8eUP6g`{>`qweIL5w{was5=j-V~3#?(XR@Qv)daYk=``ApF{Y ztAzbTn;1%uyGEq+XcT?<+!gCz6XQ=*TG6XVFpc@^Vciu= zO9(~cj?erCh?+!H=Q9jY?*a(|Qy@Sxr{xg%uR7GUGrf`8whG{B4 zqyrx71{E3{hBBa31aE{l{?|ZUBzuU?g40Jm$;k?))8&_ z!+8At*#o_BIB@Jv-rK4+cB3Gc+!F78$)+(?k)rqbt5#d^@WLh7N}^mq#6ac@_=(8v z=CdQWA{MtCWqX}p{YUBxvHd*?cd#}*?&pD((|pep@HVmzlZLQmF|<5jxk_1n7QP&@q0?)A3c zk}9VK51CWPoMS|>*CGqw?RfXSiX^82K$Xt>1W5O!QCfb@8#P=vfj1B znn9+>n54e!0ceO29(!i{rDJG-x>!NxYw_gpYlB9#dVjS?=o}mw=Mix&yU78WGH(~KHY+9{XTkyj4q_BaL5?wL0+)v{;?EFFi+9A4!|E|R`O9!eGuPn; zj!clJj~1QX31)0o`+P6*OcbR(%FLEeUj0)+(AgV+q>XGqVKK1v3{^=&!kYTGG;du< zBlY7uL*R0&UVhFHQh}15E2b4}8%PmVOX~i@Pkh+dPX!VEr-I~-`Y`q1Z z4f*Ug=EWh3Wqxbr-L;V5!87;O^u`MO35gK1bNK@khN_*U$z1=068h%CefDVozL1L@ zJvvXlvmU35bD!7^LPy@8>A@~>r>5N}XEEAYK*WR%g;`La)!}G5Es_W~rg8Ff!4V`S z`aTRRGllW!vHL7U&Z~G?b$Cc?!!4oz^&UX8?B^1{LaO8gmJ-4tf)ZyJWFv+n2OhWC z|1}(X%(FBa*|29!MD!f^(*elGw0y9w>mD&&?Abi9$PkXTX-PVq=TaImdg_O?$DHaT z0K`uH5*6mVdQo8or~YrLA^bWY%qJDZUEsB~ zCoQm&e|K-@pKocGPtSC|2~dB)iz$I=1trF!ly=EV#m!9>p6iI`(yl-RUP8u{X5W7H z+T;IK&&9b+p7!9T(08_O1SHZTYLiE>7pbhIh@a#*TZ$v6Z+-?UkRo{Y8~hxPbdY73T3JS( zS+Nqzoy}hb(BmTiKM)i7iObph#Q!>-&VP0xG;(!Gjzsa`$Er=S6}GQYFC+4lRUMwJ zfC67`P+Z=<2}<`sp2&*tKz$KXM7rt_*L9{Q85St|+~Qe;Lqd9XddS=T7BQ1a560q# zB`ozUJ1Xp7$_J)Q$t zgOB9vVcRKl^od}9tzh1AOwfVolHhxfWq;rNP@Ig@@^$h}epJmy>mGi?@`TaX)Zg;G zwlk$PX$k2e|5-%P%>`8(Xkae9j0g$*k4j!d&N%enV}|;hH}R&wEpZ{oYfmRb37chi z@nf7*MfjOxK^_ENS7GMCdVFl3D@^YC_@W%hcSdh;CSb3wZO+6UmxBq^5Zy(l(idoT zEi5W*3#IAgJmHKM$Xwja-REnM=1G=zTP>d$p-f%7$7{+&^bVI?k$+!C21tYmelLUO zIA2$^cUu>Kd=;_RK2~U4#>7!OO!20Jk>%nMqA95XaV#uzywtPXK#c1x?sfeeKLW2D z-MibMLH)z>RG_9AT>g-rzW2;tK@cSsaZp6_h{(0F+c-Sd339Sw0hRkbM2UEjdmMhbhJr>%aA8hTHoZYDqGBaN?Bq{rZ;-m z7H3pgP{s^_rP2xXI&*ILVH5lxOZhs6!&${sH#K}X!9}Ky*(HiG#_AV4B}{#t3m=cd zl|4V{);IIDN#E-Wi^l|n2(&B^Z^>B!0fF4<0`7>A13P0e9CI>b5cJG<3SbZSYe2*u zA-HO2GT*#N&1PeS-4vlS&S6jE!u;}VFfVK5_8oo24L$5Ch=#ppB06%Z%z{O`V8mU! z!8{*uCau;gN3^?8!%b>^%z1&Q+w*&sS{(omqrfVRi=7 z)a0K#r=}fh`8PdHD2^5m3}OjfTuK5g$Qa@+=`$1(xUp)mf$r0#ns6||5ULiEGx?il z%$ZO@=G$)fI$YW74d25d3vRfjm+5z_yO`AvmoA4MZ;pm@~B0A4iSz<6mQK0Qc(Hj+t%|EDP_O@m9KcUvgHK*%X9#= zq(SQP)7jG+eZ-5<0p^qp6X~IoMvJR>3KdmIy}y}BAgZRGv^(>9#lsNh$~gqkXD*px zE(tGXANm99p>Ot39kAMrs_;7iOX!BniQLXqaTHJ!_X8GM!!M$qum26fFYJy}LNr(~SJz{Sayz%tzIgQ0 zuPbY*QFWhbawASB7C0g_oHoluw)_X?s%oAqVHLTsZu@M*^<^3{)N3(*;>aFt@(Sm{Iz=%iW6g%r zaB(?V(?@G`1f%YlT9!JeFP2>8)J=bexNP5k5c0K&FBBo%k2nzg$Fhypa-A7AluEG` zpkL$-D=kY2jK<3DqWBq zVGWf}(aH`t21Lm0Lyuz}Sn4-M{54`A_1Diu?aR{1dtqyH_brJbDeLcn1=GAdTr1|6 zsFcE;QD?da^4Xu5hopq@1eIuLHobc?BPKLDZYj*oOt$7tl{$-OC4uDxJ#-+W*~MvgXU`-G z#6YDd>N1&-?Bt5)0wA?WM|jOetJyPCDXjtD0TdF(@dLt)x1a>@;C?0(JaZS9;5s?8 ze&f*$!IxQ(vR1l`33t;f~aby^Gh^B?7PYq@k=?gJzAk15KuE7?eYbK)-|3 zq>K{beyXmX>z*oSI|@mqZKS!bjVprhWmDhySJ7!HA=hhI5Oz>%a7@?vnnL^2U(Lj< zzg%Tt@UD9bU1vdlFB(%4XK{~R+|r%wy2-p(?(DyaQpIl4;;Ssegl}GE2`zlku#B_h zGZG`V41B>~CyDjz`ARbT2u(iHA<14E%sUAZL&*X13>{JLmPv#?T`Lgnzdj2fWm+SH$+ew+aNXQR^WakKK zBrtZ|&jyc%Akzh9-uBbNJ>Z;2-a!=#`H~Duf?Q4Z(c?mX3QU#LNPP+kH5(H+>{WT8QWXVMUQ+fp3jsM$LC8ise9L{|X50(-^2<9J8}UL&_+CV@Fd4_D_P zEJE}qi>-lGPA+!Z1Z1Xiy#ynhaxPkv{l0pTVl+u${E4>YqOAd`tdC%Eg>XfU5bk(G ziN_ob36^tg20Y?VSdYm50Ad^eqx^~eOSuxMlE2CMJ=VEEfk{Hn9Ln_x zM^Gfk-Q89BVF@?<8}~LE%r&qB_SX8Up|0)TLGzo*S_IQq0t_U!)}~qQB{IRw?f2Ng z-$5!#r!E5d{is``oka*|%z|U7mNg_Bx{<@HH)U9tk;y+zg8}}%w}q>S`zHa(ZB!3M zcP}m4>eg393aTJS4jNhzn}QOU!lZ${gH2vb^>bvB4-E!T<})Hh_O9mF8N8!Aa@PZ{ zhslwCD5gg9!zZ?ch_>1a)Kwn<2Gw0!wCR-`ov?qMV6y@9bQJFVvik;6^~*9UNQi;Z zD*I=o(roi%1RjJPlm@e?xQ7CBuX*(zZ{-TGG{u$DHMsj%x0IDIq$7l%A^LV48?5#E z2S3aA9t};d@&v>X<#j*_5fCtij_LS7pc7z6H}^$IH~Qk@4x`-u@-k`;)k(6(3u;qM z`5~tu8W6(VgCmn72=Xlmr^%9o?`($&46klf%T_i10dhE*K`UH?*|FTdp^bV>&)XjE z92v#GLZyC41+x}MzAmL(xnv?$X*$~Zm2D>zZXVvChm-u~Kvu%Y-c)3H_lWcEC)bLc z&Rvv2N7TtjE9P;_sQnb$ro2Y$n#v1Xl+i=s5L7RtOrJfM8hiNHS;!gvYq4&?ky>p8 zi(*2dOWA=Th}@k+d~<49ss|%}TQRlQ zRXKI!fgtPA06jY33qRV#hYu{zlaa^P*`3p)^>{VbR6=cnWbL2F&2Zy6w@*-jV6y6L z?w-lb>pQWqZ~0?2hD1+{$bJD$NuI{G#Jc%&(J~+Vf#)QVDSEQCXeR7J9tT6J|lyZUaY1zkz}E zYK8AuNRW;CQwVi)FyQMfN@d9(84wijn{e}Ma@R34WYTZEYJWPGLaSs_WZH*>?{0#V zEhgBd@0RrOCT;SrS4*h`UaP;~c#LnHxt+&_pnxMEFz<4BwJUu#5gUNkPVD;;ANo$+ zdYRG3A&R@%ScSi7ql(w2)JwBSf-nQ#-v(43>PqPj^8Ni&D2iIcfqJtLflwsIu!`IyWr1Mq&eP zA6*0gj48_fJ=>IH?)uJmpux|qL^s7KcE~n{vNkG^nt->5Vj>`@LLx@5r!K&p|0~p7<0ofRIm=`t8!Jh<=j@dtK8Qn|a z2#FJu>y#&?XXoM&p5cYClm@d-t5g1!Y6}u5^t{udq}d!I7LCx&MSz^^g_wszl>+Ic z$9$7NQ<%3n(tIH=3VFLw3V_TlW?ycRfYQ<(%OiY$V=o2<6-ROlS4xB+L0pmUw(cb0 z*_6HNJA7p`fYIc-K@4P_%vEMf+@B^1EpS>~Wb~mAkB$?Xp#p7u9XXs5tE=`v_Q0#N zCLGo@U~FL4L3CSY^8AHbCrUkaNa-ybu};fsc&ZDVG__5f22uYfbg@=@jvNcJ&;8fv z$h%Flea>hS^QuzB)n`q{a-d};B(6`ZTMV_bZ)xH-QsuGfI&!EyOQ7`F_FG2 zax&Uz!K{O9zsq1>zat+iighh;T}XZ@{|(_(mrZ;AwX2u5&=g|wbE!wwteUQ`L+y9L zkXKVdFv{Na+k+A-0Jf9qy&*^tV~)Q67yhHDM$<#WIXu4!7S)8rPDPL|Kvb{(Cd&owO%oWjjB@kFoOn;mev@%zh7Z!H^Hbaq$quVAZiG^h| z95}h9GEp@->-ivhzC&v25?oE{pI`>Gi(B~c&K$JW)1hEEgfS>#S%0Q**QRl`Yy#-h zQp+|>mM!gKHA9s+H2RCrbir&MA+e2Mpei?>xp>-b3-U_Yw@;1)H}TGefxkP4>R4AS z+e0r)7nY`UMMYJoOAVH2Z&DIL)Gg^%&at^8^ZJb@s;tYM`Rs}8vY)+^WAH@|#3kR% z6Q{c1e&U|+Huyx-dZt*7@J|>N5}yo{mMZGmDWMAF>XpH#ou0OQ3(IdgC&2;kh$>qZ z^80!;BT6y~j@{}i+lUUgd+dWIe-5}#D^u=a;-NtBn=dyW6jik2L_L)PxBeW)`y0>PVT5hNGy(jcVm!C?6myG(TWpE#wo~`JpizsfP-->b2BUYaCPx^w zC2ro|*Q{XAdq|4o?6^FME1WJrdcm^Al|=}!kUl&NwE;SwB=Q1;b#jDQKU~!>%tL9a zKKLTDl2eq0ZN-K`n#KkD^WnV(4&4lj`6_i(u!k}f9W62i`+e^VU!Z*~OlgT6-I|4I z34!No%0lZn9-}IIbTZ%eeAUdG#;YBhCO6<3E1ek9(?0b6h}$e<$wqe&=1E#%t`%RHBVveKZT{K@;dEKsJ98sFlQhC1gt3-^{bIBe$SDG-+KgH z4flH*r?8}WHaE-NhNcBd4;SNx%=z(2H%r0xv4$6l(**F2OpY(1YaYFDEAOB=rzg&L zwSgrxDDIJmqB>k}neJd%s*7r67S$ah304{^E#_QHafx&o|312dM?J(M+CKi4|A<-$ zt!Jo36sM&GG82T_bPtK{cdPE?3Tp7?~U*9k%GHa(S!mDV{DanRpLUR1t+@Img@)Po_F2C7=zBk;O z_w#z)xA5^b0(3)(iA*q`8D5i4V0i7MN5C1t?9`#9$GTXwX7ZL9`Ki1*aO|mvb9}R`9 zw%iIM;(7IAoa{6sD?es3UbJa<()~Y6_u5Mkj`Y0eT)>e8tn_!hYW4`S#1YMAd6d7YhJI*KS~0)e=9sbR&`Y4CXy}>#68Oj+&JeAASa)Vo`x*p#=?iCW+<$$(SI!&YN}C zVbA%wxEI2q$yY6T0bWnuegxtRa^sGMFHmlB;fNfe{nNg)&0wV}{__>2=V1ZclhgS}fBqCc3QtpH4DWrf~-a-Q^uA1Q-r2E8A&>q{TB`~J_f@JT?;X?F$ z&#kbxB5);U3vJn$LJj2hhuvGNXFiFdljVQm=E(`jDmLXN=*ZOjy#MlOhF^;w{DNvl zfdygtaqA<5_WW&=cMyu^#Tt6(%a5z=w?Ew&LNwgcIp`jDG5@mX&EPSszC|Q*J=W9W zb-)jfS}GQpxCINXRk_(WN6Hc#vi=o+$7S1;N1G->RVrKIXl32}g!Q1>cUOB4^eGR}?tpwbrhnz|b z)WN;nxxhkhQ0#k>&)AG?!~JV?Y=6&l62Z!|YuoR)-X?e}*l{3waX5@I9N@gB-^jN7vNhzq&zEl$ z7vk*^2ug#kk%XZDZ(;YdXW~x6tqR#k63ns?+FQZ|2kwRwU4un4+&|zwfh=RxheVRO zVa3UMHcPUDW2Hc5ekH@~t<@bgXxPr0<7gv;Fw-KH6S^Z3?BYbD0)wJNONA(gQc6tR zsOZ(&%d6zF!J_UTgDyyS_Frv?9Lj=?X1e@)4D8?-f@Y0d|8@wl^ol^zqUs|y#3goQ zdbxzU71q>yo+Hkn?Mm7B4!+n&kn}`g<01sT5>UV6 zr2?^1K{hHtH!s!4$DX;Sc0nqrK&;0wS#XnJYI5qd+jaV>lj(A~o8+qd^*x_OuUN0M zu%)+kS=ADu-JN`v6MeH_A-~bRAktWtyzpn=9$JWhZmUHzK4U|c8Dil%3Tp|q_pli) zIIa9tyPH%wn}TQ^-Yj9{kGzT0tTQvuK`SWJh|hi;TNSwQj7R|9Hs9mtm`32Nca`q6 z6Pw-^mQUG90|>{H;|%=j^IiZO*MPzw(}zF}X!PQ-OTe*NH-&?3y0()Vj2HbF^7EF< zFw1~{s`LOFtveOpduQQ_vXaWDU!t}u{icpCFUjBRd~_V~X(4a`(#2Ma9^XcxW+P7C zRH|U?75{xL1+F2Z?hON)tV&3ZtOB1S@Rn~L{(5gyd#qjN>dq$9eK0*=>@UBdhO<$d%H!g@&TG(<=XbZ z!4WMYUE5ES=Ll*Oy!1is`kMrKcpAJ1GfmsfvQ615J)Ji1#k_YUJmb3@(-L`;20_0_ z7??O52d-dutyY-LshZ2~f{79wILe4XX6RjUaiFc)1xZQ88)n~7i6eA4c>LBlP;nmG zbBN*L2fAf+hW@{(Vj8lrM%l2JL=zjuHCLJ{X_dyqBQ=`_8PX|-HJA&v<+28p{`jND zd@BYkN6aknm1ZjSFVRiuSCqtXw6d>nKNgw8XA)YOs#|bhMM$^&vn^$_t2Qe_@B}vZ z0^ptLTpvU3k)t!?MG2eywrg%RlNZl{o%K!UrNhsSgJ_&2;)e3aSF^1u-#dSuS_Fle z!lo73dBjAX;0_IX3Vw(73Ma>VZoi+}3Nt{JU#lG^^uujo7Uxa#+TZ9mZdsak8eDMG zPz&TB*niww$~CcPKKBmo-{*}GoQG}Pg#28j>oRTgVo2T~mj8N#J@*cJs6q-YT1by~ zvUyptld~oKbSw(zn5g#VI?(;zC(Q=}+$;R0(vzzJ`I`3^ZX41byc!NTc6|D+CX(dx zYN@}(e4t9~MCsxm@3bVV0k0`k*cHqRSyh<5OOpn%9G5A!NYujEP1k$R1`re86={Q^ z!%)2okkoxlvgqH;#EuCfKDcLWX5*Igwtv^DopAT<7H12gFR$wn@JqftcTT#C#X@%Y z_?60C%QpN!7Tkn#J23;!OSwbGtxruah^((1q$Ev=6K!e9InFX<-k*$Rbt_*^-1jA_ zQc6ftg+~Paq#O^MX7XDyB9FzVE z?}QE%*eK>EeOHugN}Emo&El7%g`MF*sq~MP`(M@Jyq8!jVt(|;1X`0Q3fG#B3Z!m~ zjbGPT2}vtd_{>ADWTckBwIJHW?RhjlCFb{{RfJ*sf85tv>n0X4TvS9^dPt(xhXgiA zM;7FFbuAzNH@~Ajd<~WG?4Wm?Y<%wj9wtV7EkM5{d@ZF)Bj%ZXzTt5}x(`q+q&W=; z8JXRugxnrWK~`$o^9vN*$j@a#QEYmDltR?ax1`=7jRB3=dU3E6wq|*^n(#)!t2~4p#5OqockYo zh--obCK#R~$dg`DZwr->6J~N5dy!@C3dWEeH8?kudYlCHHKCmPjOT0Xn0hmLiI`i+ zluvm+mY-gBJ(4gg?Q~XK&MSY&q0JEZ=BWaE>@y3)>C?IxBQLR#oKqUZ%0LHtWYREmP)UD2%cc_s3%uGSgJSt&IF*`jzPB;4gB!L3`fg~1jr~8*d z@_P_lZ)o|w(e}AUrfVXOyj(2o6wgfEMY1fB^f7A|snwGBw8rI-psD?pD{gqu|52lh zIJ8yb9Nkc)TKZu%xJEJP_C{R3`w$t%UM)}^j40Jm!)}f}*|cy>oK<&FQffK%>S+Id zD~xS1F>r?2W{v(@PQ8WtI*+;wTFhglHR1mQo!-EZUcFx(*L$*}-#ZLC+SnG(l-3uz zIb|#axYURflXaIxd52ayy4uR ziBd)UR>=8Zh?xbDgvvs*XcGGpdWWhOA@h<`DLxhwbOyC&vT|-?xBbNY#LV> z;Z&@0*bV6e9uNZbg8grhz+rU`$8CL;t2Ey|S3|tVJeX=dL|A0eUUXrnLeHt#i3gyf z)_?Gy4a*O%lsY}cF#b`*BE~s+i{6g#2VO0H+ngXSS>gM#$WvptvJJc(RlM@e@j;YY!9C~Y4EY=3wEiIs=!h!9AV~Xuz#dj z!}rQ`a0VQ< zw_j!`9O-@DY!ulS+6in)x=ZI;+e7z5{En)#UcM3g{& z3SkQP13{Iy|M(t_u+^;ZSuT40*#PrU?>|ioF$l_;A z`C$LjET@K7l@mKIxP~z)MP-e>frA?oSmBf);uU_CXYACi<;s1(@n_OR;SP)PM&e_L zXdTBFdN}3DzAQ0|(&|U)VcjY}GA@k6AzlxmAmO6)aiQeMGs^|BE!==Z0@mp1r>XN# zK6qD+Jlb}OfeMZ?9F(523jDO>+R{x3=n{8u=G^J>yp3i28apwHYQ@LcyuJ}T#$PQ7 z5;>+VIpW8N?m>iD@eXSn!s+tXb|+`IZ!ineIdzCef3YoS>)Sm=!9gfDvYp9xS5%xp z|4cywe(P&}jQShu-0*|Q(Z?La{g0jKB8FJ0-xtZ9kG}7FZ^S?j*+P@~cEGry-kBGS zpR%u^cdZ?!G925n2P8QM*oA4+Vw}#yZIXw=e!Y(-$dpVMa8W5lfgO;og&;z!ywF7i zBhb86@Ou56)X4Pp)U3M7ZV9w(8=-Z5>FZ*JFDQ63&bJ+J{tPfO2^0WFQO(wf`xg$M zCV>Q-6@TBohT>`1dsCcn(S!nTnTww`>bQhkZ(O2REZ1c%?w@e$1QSrCbS}Pqa`4U+q%!)us|k+tXnj z>7TDOJv~-lo)ReLFG7<+BjVAJgF`^BKu@b=cDq!vNJKRPSQJeBa-QKO>L{ za*o7TU{gk2c8Mfz*M~8jt_Se#n1w)mU3a!8=-6IT2n@lR?%yYMvpGtRPb2 z37LZm3$(T4r*J4unr1g@Ek@p?^8|ShD;%u$HuK>3lC*tB!$ZSdi( z;&O!|9NR=*@wJnx%8843sqUc>sh7FwxFqJJ$?n55RH?V=7Og&G#_?=7SVW2O*$SiD zx{)Sv4tWP5j!axaJhLp^H#|br3AMZNVRd8n62H&Lo&%_&lJ-&+0&VJRsv(y7g%ml< z+fHyuDSZ}qb&fOTQj>6a{au4IZ*+XTm?5^UIK)|n!*CBQ$3&don(#GGG|v-mKN`I* zuyN{+9k`MCyh}R%`bXz3>QndL%1)Lm?>B;Ws3Q}4NBt;shq9A5c?rjPCUtvXgY42D z8ifjEE!+>MMMeeaZU?C#-HYp9mzL;7ZS`Qey_3D;ui=Jh9p?wCFFMjRE)r| zVJJOJj1qEeK{3<^b`-zXB)6?GeF!DUks~EY-prvZzx~-p@$9%UlMY40y2zm3Pi9?` zuL>qu@Re7%p)Fz-m^DZ!p8$@)GL)F`lS%2zZ>{1yfdR#f>jr8;egv>6yGzOv!QNds zPWwwSfae5fq*V^_$&B)@**~-!`W;$v@(%JIGglEt1z@Y|Rg7qjJ}_pAiqH5tUf+E& zX`i{#X8!QL$-1a0ep zFsW`R25+H6@5T#^ztS$^whEW1EjxP=eDPJcp0s_1iBpXfRQ@O;T$A#d4sH%TczTI+ z+2egONiJx5PQLX|*G`pfr%_g!hr+vu`V~(K?}lYBb(I3Zc@*B1Z>7R)mL#J$513^q zPe5T$b!UTIGFr08mbk>sGW-fsJ6>}-@1E9DAr_Xzrl=a;ZO>7~c)%XW`Hw~@!C?IM zHX{DWl~`WxuCD^69h+P4&389cxP=HpW(`czirPvie^}3l`TlEB{wHP$RD*KQA5|=P zrl#CR-U)K<84*sv{^~|08j<5AEu%y$A1zc?-s`@6xjaYwLM}606`@z=Iwo3cpF_nA zLo%kIsX5Qji_H0xwHON2BktrgpMPfymiCRjBCG?q^NGJ#l`?|;SRBlk+Zm7@D30e! zm%)f@79AL=Dpxh=2vd>Vm(TkufUZw5?i$kw`KD~l%=j>xr0Z$1FtWeDqSS!#08Fj% zg>CUK)wPtJezu7p<+PvqpdyY`5Z%V50Q#L8AuTDcu$)Q-N`3S>X}x5qcJK97nH4AZ zk1T_AV?|4r1n;1JM#l*Pfv_Imzm0Zhuj=s z<`W1f7UYg)vTHj|kQ=JAh{E6h{pyE646I8=%h5ZlnW*V~xiVq%9_gD%TyNo*b6d7* zs@Dmp&8bp5vl^T~bQSUMrJ4y|gXPbf+!SbzF)9NYhF-gI?xwJRGyC0c_tnHEKjUG56?lH~bGT|FOdM|!pC1K#IQ zEWaYTY^u6|WB}rlI^jKprrR-_#sa>HDf_SQ`NMT;-e$FbQn%i=2HbgwQaURB>=HrH zSXu9WDe*jghSM5!fxtNsRrRYiEk|N0A=IP4CH~)l3`8G`faH;wY55xAuq}{Knrh?4 zpy@FafT?Q3n~ocvBpxF8Mw3t@5lTNL58MRK5lR(|j_tUaDVae&pPqVow5o+5JiQBM z_Q&(&z54DZcUra#S-&{Z+>YL~Kf(rNx$;PNG^t>mqmp#M|{+2uaI`~3OYh&&m&-0&yA2ea~X zTqu$gt*Ra&mWa>V9Am||Zr6cH*}4anQw*nO%WD6WK3}!TaU{cBGzCG3zE+>d?RfUs zbgy4@tAQ&^420Myto)oJdIHwpw?TcP82yrcnLaY?!|ba2l2$l=)qi$!hVlOiQ>$}e zO55bDgOweHn%__dW{C`A9A>LEK`(AAjrj<)C8@&!PGxK1KuojZ{Y&bc(y?!3c?{7X zO_NtKTmY}Sz-o;!*pqqyd-w&$iwJs&yu5}AJk)8#ujE&ARQmQV*q&L}sib5Mo-Cg- z?8hgAAGF>5zc|11>q)mio)>T}Y%D4ub-T0k(OBPymfVZA#77(|tg2a7A2)2~Xr_TM zo-gKx8@s$KM(o5fFZTP2Xpx&J8A_7$ln*w+Ak%(xopt%9TPss$Wl&){olv;fE&i8A z-9~i9SFmowyL2!@PKQh%2}>95HX)pr?67hCRj&j0&q`&LD?D~ zWJ&_F4FI4q6_M)ryi3F3o5I1UBQU8iYDo|Tv422KKMO^FeBeg^m9y;ER^|=X6)*fI zvk(!|%O>ztp5jpY+*!D~Ek_I!h#)-RW>Kn1*M`yTB!-F$B5(3;g@UOQz5@}L*JeV= zh^KE)Z&ZlF$3B#8`R3DoQuPBe4ROl-wZ-PZf z(7$s5_W6wO?s<=JlhVtIjW6Uev(_;O9+6{2g=h~R*^NC!)hzo`!LSdxxqWtwFgY#O z+9jJ`5dx1?=k|m?_NQY|21Y-Na|wbT&sN{dWoMr7I;XQvnXeb+U&C^ae`Z_&9lz2q zDX0(CHn&zhls=-S;&M#KlX=-I;o5%S*PZqzh07kTaMu2VqxGv)u_(8 zuspz2_IX(Bo~#~z_I$r5A85WYKV4Rs7Ivz-1qM4tkm%)nk(F^IezRaenenM~N^@=I zd#tUa<-HHJP*O8)dBjnY{9wrAP8&L={`HAuakfCjgC#R)EBXct;$)iQ$tI25_cKHf zbJ{CO9u?z)%07K1S5C<< zpk@Uh1#IOqg!RiHfs+XbC^0neHj-lE)$+YBcZ0V#K46bpsXx>5v&akHYeNF-fyFk= zOy3DINlUDdLL3}2?+R2rYP^*CE-G#;GRVsKw!OUhJH@=~1EB4!v@4gD=wTPh=}Sk! zm`7Bqej+V?ln4v{!w2aPC)r~74Q=Z{qaa|;L`KoYynsCiDmFhx(~XZ>CECYr61<_2F;WG*~bNL00xcl92n; ztNI32jr(R?f!dQZ0t7m4BuCdt7cX=$V~%yj#m9I%U=uqtD;r!?OnihY1*pI$J5;>4 z50XN{puwwiY4&7emak~6DleAd52j=X4mY(gDuYokWHcNeoCvO+b;e_r&I`GWU8t0z zk+IShwObD&FHLy{G@Ve1W&hJsHzQQQMfyAPW8}cAC$`=thGJjxI*i}-xG*08yx)Q% zK?;Mt=DPdmyJle)%y~_enF#PGJA>B~n25~RZ4b_=L#5vwCO4E-pfTPugT`9Hs5QO% z-_O=u0x4!ub|^F*A>)`k;p^5$A9H7_*K;9m4#cAQ6r4|of@VZ5%lp9U-^%*ThiW&- zk9o58Z^PEj`@~2eH*SNBC|kN6|P6?e|S)M1}6QvHFjTnu-xxw-u6CW z>7sB)o%vBLh5F8`uD<({^6!6!HS+YDEjlMw>wPiR77=ERDsTUsBRR30Kl9@xq@l?0 ztM0f2@#vGEw*_dKD@}C!@@0haCo_78iW?L9*%7g80}ND3st+x-#gRz|yvcVE|IjMw zT388iDaB0|*yNk{RciRL8+~`F{vxl|*O+1}6G~{n21Wr&Mc>brF|$!9vmC&?!~Y@8 zw011ekQ^?ss-}$$^VUOI;2`M%bg$h_3p+!%CVUer>9+k&@SKXm<Q5Bc4@|3LC^1}!K1IgdV!)LM$S%Te=tsgZ7d?Fv%}#lye-$tyUO zxW|U1iCCD>QzLh$hPw#PcOebY`29{xAvxwrK4Q&VrY1eU!5v671@9PGrHh{W$eZv;gi*dX*VR01L z4N}y9IB*{e5O2LFx=I&73E=-Q^_F2#e&6@-J(PpADB(y;2}*LAYv9dr&e>=0wbx!}Z+qK$R03Db47GiI zz;XFJ?)l=6k{Xsac1q6<_eqku5JLfDbsQt-&t`AX_QNMwZO)R4*S`g1nw**5 z^VH3Ep%XVOF9ju$yi?#)M|;6Oj_{Q3HhWJCMLB^DzS>`2KRzRW+E1?kpb`!MstBBs zW4ik{9uigre0o|yQ*ZdGrq+RTUqbx5H~&^YN4?x}f{$uGXXwe*J*DF+o#oVdI0;xj z+PweCeEoM+&5k@+vrA3$IbXZRtqWGE1skpd<-!CNO<+7W=&nL_9@Jf5D{lhjGwAC} zwtqvZ^MvGeBR+lOB*m+O4#M=A-%1aM?i#l7*ae9@iFVz&Z2i?c z)AquApHQf${IQ(J%7F>nOoD`gPjtEc=hd&@%EL+0*?G0!JuSEkzd?F~)B5VGIE~VZ zh`2|D-)%$f6~Kl~0p6cfc(+1o&YJ5A^*Z(?QtY-5u$S0mm1A}v9KrhVS2_o%U+9nM2ekM>yT8Um#zBmSdDpKNaE4B6hfzB?1mVW@D4 zGKVi|Kl<>ag?(^ZOE>%e@}`UK|3>A^g9J|voyLeb!F?NYBmYC~iM&3f2=teH-O(zT zAbi`mjeEs*q2l`?adgMb{+7`J00gkRgg+P+khxGXM*3!>c=wZ(A>y+4CN%Lm`=HZ2 zo9%`rtTZflsME8upYKU047nYs=stfc(3ANmN&aTn^{mXr=UgAN-aA`XqqcCHgSJPv z$@bHhsk%zN5%vpKus+Uh+jGoFXw2Ogk$^2p68S5>Fej?;dc0fJ}ZhD8oJbE09ev z3UWLiJg;!WTgl{o5k4I6!DSpheSnT&3{#Q z=Lno<^}4PT_vjS#I}T6{Mf8n$Pfe|uSv)jXP3Q<^3k+(5^2!(Mzjc*7|3FkxSO^)I zv_~iWM5c9y&XPc*6caz$8b{kvtgNi)#Kpz=pirnOgU53byT`L~Y$_&fpg1jnTvFoP zw)FOKT$78fM;8&0vB^aO;XS=lGFu=5v@zVhK>oOoy)y)py!9gF;^l6j8;hYOPZ9*m z_Dxbg2bIvnG7jwv!T%vjF9vQW5d2*rSfmfib9((6E#WxbL=Gut4R{RRy0I-neehBG zSs48cW_yD*_>@GUX6~J10GGss7B4St?5){rr99Y5vd+V^yldnN9I#`q32&PzcYplvb$**MKk6a{H$XiT%ME_|eX+HJLJ`jPRAL@L#|(yJAVzjg zC|-vg()v49Yj=fszuL|rd*V9pC6Ih$LxdbctVy6Bu%F4n&csY3%=X8@{5Q8PpCN*= ze~2PUF>^I{2D2ct9q{V3k4nHp1%x9I-)&)Vv&HD9U5?#g>LSDsFVX&9?)*>6e8lsEZQ5_m*61+t0YJ z@c3wywlD_S@%Tc23^=cU`m{=d!cJEl?VJF}GjRDCj*zEs*eG^GOznpA9ejzcL_$8h z(%riVnf;OG_*Qnj=010bAI7bwqypPm=!g^FJs&nr+Px^9cs)}<>9EngnB-XN9zsF^ zT*%NANtf(H(df)i`mIF3UahuxGpkX;jTS#t_VVLDNcLuwAm9-4JDNVudjFIb`1NB9p!0v@k^0=yWHqb{ zC@sq}IxlGp(>>x!c6A@2!4(uI`ycyK%a<|j%r%PH$~g_yuz@zgDuXVl!<~1xvY;-E zro7qwHt%hS0=f+z*3;Eoz?oqPKBOV{q`2dq(I-ZL|4-dv?cvBRHJ4!iTiYw*R&U@| zdn^wYbxi7#y7&2kC!1;3ril2!IikB^5a9LBED%>vIfrFSLW>eK9d9#39FgCU{5_}7 zoPw;+n7jnCn)M#*>oeT?pwxLW|BP&lE9_L&yUYB?!^s>0E1raNd#Q-4ifsX`2C3FR z5)U(f&)4%@TxHJ59c?K5+%jSssNG0v8%$$VTm<1izk*t>NgO?`&PHJ0=pGMeL8}t< zE$g}sl#&?fp>S8?#2P7XGXH>#?l~KLctoe zAMRN~z9}hA4F*8jZo{xhYK#HfQr&f|LJBpZ5IpuM0(|V%*r-tLgOys-*w;lH0#4;r z=wf>GPLl_gIL526xfml@l<|&$xmGf;Ktk)`wQ+-C#mf7p@2wwDixxZ~6IKNZvHapY z&n0091<|SBOcv26A#uAB}Y11Z-Rd+C9KP(fJi@*{3N10gh!w zymL#y&hPFaZjMJ9**GfSGEq(rL^oiRd4XI+dX|jkPMO@mpfSHGZcb5&u8jOboZZvhT>QdjrN6%J)HvnD}HvJeBG z)qqU*%Dmeb8*52W+;Fw~&KF`1gdkyCE6{dCdT@7c+T&Jzd>_(dhTI@KabviraMQaU zJ9*81JJ8I}2fl9@d^|ucNGmjK3YgX0A7E*0ci|$3ya%s?z6^>o-PQeLC=luW+Y^dk z)<3Lr{H@dIduriRNcv{xXg>M3L{oXZRzvLw^0A0yf)7^jWi4M*&i0y~wyiHsy(q*C z3>q9};Yl4|jK04PpkbAM!WS4XXBx^o-ygepPNGCP@jBIB5>_v)rW7Jb(3bQ5g`HZ* zm89OVlew#Yc=K{KC|gob&0yfhHUXPoBg9Hz`p&TWoY8_qo1PvPQS?e* z`x^ZOxg*ILOOb9Z*L-cS#qUp}c4bSfzdWi`OT!|wG+A7}6t2L{9_4j%=Z1WzL)W2pfB8KWWn#&SqvU_L z!3;KcW{b^p!Z^*)n(TK=%!nr8(x{7G@lOG>H78DH5Fp=@0DY%C6e6aDh|m9s@2o_9 zCqCN%Z^~ZL%!Ru;)H?RzB>j#;A__>M0Xq0u7aC-Y++@%^g8_gaKQK+$Gq<`cNQ3cx zz#31}(70*oO10qhGGO{-$HTv7a9r`N;q7@Jsr;F>ig;rqO<1=;c1PdeSt&kOiy*bi zIJJ0U+9F7aQby%c&mhi6NEH~;CEqJU)jLewZ*!^&FSrLUC5WLQw011#1J73o`~2~r z?uppVW$kH-*kiTd@^kQ#-rlu$UigT-aA!NmZPTK#cM=R|V}{%th5ZQw!7MD+~5~G!@at9NYiNy(})mI@o`CT9Go8S z7n#+UaeAbTGLRDq1zuB%@I)i5Jasi~Kt0XqL}QIFVRFruPeGs^WY_opPBP)KgR#pB zptCaPTC=yb2)Mfhqr? zxpImzn~o<1SOslTOdEH0)AA#eoY#})mZ3}?B)<@{KDA5y)_=tI%6{I};McYZd{wwx zmtzk4buh#au-9FjZ(!&J4E6J>Sp297^4fq|%p8KOhX^pOeIaK-^u5_nSoWvslR~YE za7eoN%$*T)`}w3Qq-reW#7U&2l12ZNj6hZ}=+xv8b$!R5l+^-&3Jow&&ntLhOQ<3M z>IlO7Quph@Lcm;q0;N|P6QCJET;Up;dr(}_HGh>Yud~e*GGDG7IhD+`lSk@^BIW(r z%RrXgqJ1@L+qgtn35a5n_a2X7PHQy@0-GX&jZIDWYF~#^(mWl#qS%amtvwZB2jog| z|LlNjzB;ZhDK8(-w*KNt(Etb62QIRTau81||MKe)cw~vBmn&eQJ*oqD@ze$D4}x%Px#*MH+$*!It56Np z8D}t-S_q&nf>cPi#{cF<3y@(x9;ulV1_=prUF4;8Es&{vh&;68K>Q&BWOEHQY5MOd zL?q#ctt*|riRrHe>buALNKPk3zZgWg3!ObRkUWkmuBS97)aBd|CFmj}p1*6{-2jIx z{kH% zfj>gYw<)qKT@YG#p!jO)#2Vo4Tf|r^0_v@ezxbjQ&y#IqM@K3h9Ub(E0Ugk<{`_cX z+Bd0twjkd_&2FGix8BtncVHD)t5yX0taL&sOeZ9l5YsbEE73`!=?Wtcad^%+8)up% zT89jtuiSe;Pk0cjGaYR9cbbU0Oh^oFZ0vH_N710|cx>r#Bc)yT zCV@52Y>k;Fl0d}u`apoRxA(YGZ{m2R7Rfs~ob>1H>}&{$KOb>r7%3jU3Rm~rq~z-b zUaQdpnshC#%g9!!CSTk^Frge-XVEWm;Jik^90VXYv=Wt|Y~3^uv5~_p41SoetB%+o z$3PQ50XFoux*RCErHlyQT_4J26W4!pbWqhw&^5cw{HM-)EaK|+I0N?M4*DJp=o=%s zdSgBT-r?X)vDm3OU+d+DFi6vAfvoKLkLH<8K0ve`&jlO|u4YeB2)r;xsx4bydGqb& z6?=y*nnHhdF3b1s#gNb@m%SS6^iR?{8&c8<+3g5eQh?nx;(~rDtNk9Xe`d+ISb)NA zOmud3@;&{#{K7D)X579)qb*9elLLsgBS5fObV1WsVmgkh3oA`k#Q-Y~;Eqt~ncAS& z6<73&O~Olr{T2@W1Aja>11ABV%(`M12rL??sn@wJ-uH#?=>jZSH7v0Uvf)%zd*?&B z4t{kf)-&?>X)7h;~GbX7|mZ~%xuhv<}G{e+Gf!c#R&rPZxRIiO14icdSIl=^$ zag#*H~@nOr|xoP=Pt{&t@U6(( zjx~AY73g*-gR9VbkhbTFGQPP1^Z&bw^+?=4eHNbEPqg_ZIZz*_2Gq}o0dG@N;asP2 zCj85KD+5;e#B_M?#j#bfP8Bf|P^I9D`EzrL<&mcl3)sU-g6QEOf}VP%`BZSAj%AEx zpMjWSzP}`6C7b zkQ3njz(Dnm1CzO$#z4^E$#&lQ8P}qWYLhC;lORF-^>wPKsf(B>cU+jixP_oD8$6zs zo%|{Ny4-<~r6(-rP5c?@7|;;>Y797(cwj=rpm;*xO)U3AR0xJhu85^u3fQ(Mf(>-1 zn3|r1@G(L{GpyI&2)`_y?z1U^5ODy-kS>KGkq-l^MOx4Ep}aW9E7h9+L%RQq?Y5V( zJ!*e(ZSbTwRkjL1eWjJZY4VciM(D?2men6d((Bf;V)>pWnHLKUh`zx~w7HGL;&~JiY zu)&47r^tbJ?Q2C$ zsuvf%DHO9eJ9{DqYFYdj@|*2|skJw<+(Fu2PlkL-1|AnN-=xRCT5Z(s+Z{R^mcLTJ z{b#FArow)N474Cfx$}Y7!O=)&KfZgbW?AYXc*_QKt01huKG*@m`EP@SwFX#oY$85tZhC|osd_F@L1 zy!X3?q3N3FA_(iDIDZw_JIOs8bHctakHvxK3o2s(aMT=mBQfer>Wl3Reg3-lZFG^h zJjast-;|@S$EFCK-FopXO%-G8$_vloo3BlvNT*I})@UOEh(v}VUL8k(R}DGReu)D}w^H{wCyI2SN3 z7J=FI(fT}rVnYI**QX9B`fcrNam3tB*c?(svpPdZZmg8KAL>Q8U38Xod824GGTBRn z6BfXLNGtzrNf_ySnp;TLPQsd{Na6K-knJaP7BDJXfsG^VMQ&K8Or&9a zoL|2x?>cL49+H^xb{*DGA94V*iZxO2!Sb&lB?91p5n^`nS!(0fnWK;+7rJb0GBx^# z-7)uPei-Mf0`&Z8iNr&c<=?@ta5HZX#d~}gFJ~qu8voRIy1Ba0HTwuW!v}6crnih; zP5_;q>8`?5TK{}GSiN2FFRI&P0J>q$Wy=nT(TmZQFN=w%V8u+*2TW&N&$YOIbh-x` z;4h?Jm*zKBLfdW>Ls{TI&MRsOK>1l= zfMD7$$DxGm)EBO)Qs8M+g!k1kkwE0zjx+qQn>2ti^VI;foEAcq#7`U(uUEulF1||a z?LH5elWVDR-I?_e>gcYdyT$>sgf-D?B*1Skf!34Uv-Ttz7@{zWVGeogo3!=w-CH-h zNyc4?cU1}v#-{{YbTKA2l!$`IXCqtHW5B}+tzrm6q07a5F$mL7xYgQ809JR*~Yf~>ijA(2oFgL+5lC!BA5>dmVk^y+iZ zjo(e}_0y!vxC=87q1j;QRm^maslFVB=Ic>A-&W*RgQ52g(?d!p+!Xo(FvK(c&F62D z3!e%)@aYQNJqas$j7!M3Bouu4y=7Su{UODO=Lh!{@bjaPUJ}7W^cZk^NNi$yi0eR; zVSDBS9Gw8ABPtlxp<)7)3zF12%Jo&%|EC3Tq2KnD2j2Pm)}HQn8UeGtrt->KqW0i% zjY0wa8FHhck-YWWbwPQ6T48_647+wARYlrk3U0$_8k~t#HB|u6Wii!;!#RsCq2L4X zkWZakrrT3hfv-G0E5Ldh;bbI5x?`hfL`{`aeE0EeftW zgE7vu=seXnaZQ@z6|08fhR()AvSHsZ&+np2#{j`Q+vQOd>Hah9&2lAuk^^)%O4D2! zN&mAoJt_?!+}YH@O0U54HxuxF_nOCTJ-=v3(Dfgf$s)!0rUj%1>t*)#U(_K3+IqD> z_EFNz2F2Ufs*|!`$GW=_zxW9P2qrZb?dHmail+P+si~2@#l$qXl&j+l;U_jsoVeHtGf*DAPxD0{pS(2eB1aa4E#qv&3&r7lQ52>pYL$OOQ=(Ypc`M|@>QVq z#IU8KWMyQUa&-zSlO|36*7<;OWVXZ(h+mj6`#A;_>3-(mv%zyQLT+vi_Y5BTB%He6 z!#^txoYs(1dwz6UxnCCy#*RErOTu{S33iJ(iPy5q91J=5%1}TQG8tMX&{rP)zeMo= z@_)`%US~r(1+5w5AJVcfD+Qe<3*C3;_0e?ha zcZ1mv&RBIWjXx$J6r|lQ^SaHemX?-rGu|gtjN;oX5(0`7z8F9Lz%X%uC^q>S|WG=+j(|eHAcSO1DX8keX4>zEr8AH7ks>XAH>lx$Z{`9V*Vb=$WKf0t@_daExN=F#NtMG-uW z77Rh$Q*y6>C3mY+_|S0IPK$Ir#CLJo{{` zyfL&-$fl(BC{dQKKF3j(4pBk|v~qL19bf!TI7c2k@2tw*GsFdFwvmD^A9y;6v;x2W z%~4OXL9kV@tpyFYO4b%O$|t`QF5Ec=WR<{zH+MhK%qETj1FFTUe*g$eqI1mPu01PWOf3)0}M0G##f9N!dhL3C_| zHS7#;03$?`HVn9Zhlz{=^0Og&$^oSKNW!)Q=budPs3H*(AWWPH5G5`FP*VA6<~xLj>?I1(U?FV3 zkmq7eiGarx5xTa)9e^J7x_9m=N%aFH(<*j^2{&gd{lQdvZgZ1=n9O<66k<_+Ta%R0DNQ!6KH%CFKgGvBRtRk$zE$~>wRhI5FsEc zzW!&UmVOP~H-b=SqMfAA+DWSjLhhKuaq+1oYX>@ZW4WEgs8xvrK^G@68~n0D=o^b| z4~bFJ^4?JwMB%R&cua!}`#p+so=%M}H@}qPN*%LEQ#=l-gei5O5Cil5J%f=Hh@;AyBk~#dFZWW zMyPu|gyE<$8Q81u-|$xk(rWAOui!>* zyt9^bdpgEE$!+$y4?rY-SgD*sV-l`^lCS+##PXK;vk=fAsHt*>Y!gHq%8cj%tmD%l z?C$>qF|6^K>UAkh_2LICL^h|STLQ}kqR!V?&q#_Oes@f31dAZ)&s*C_1g#l2zKw-% z-B;U=``{PD)Du&9(0s@ca2wf85_#v4a?rHXqx$At#Jri9xWG$$arA%^x6F96a*|+& zlpmgp3R3hg4=Y#T=5tpBElj}QNCf&ya3O732hBRjm;wC8`&12`J|=lk3qS~1Us;Z@ z@dYpSwx#fUbUu5&#rrm6EbHB_}7(4kbxEMO-ax?{?WKM7bq0 z0E-?!@4@Q*_OJ(Lgqq&dAgb{To$Js_Fo})u2WL{~nRB*R4SX@X_5L@VL#y_Yo5EQd z2;n9(VP=E~Yyam|`a2r?8RI@Omc7OV75S&|9o}CW^TIQsug3bLw>P zk~aUCg~-n#AtAY7B+zga#EDE0x|Jmw(#QuHT*FenHaj~SLINF3S=`#guo~A6UzR&k7A$vqxBt<Nvp-Ln{>%AWKgNJrz6^}{CV1g5I919% zc365$+W|}BN<|Q}&T?t7AMe0lZQhQ7-b!z<2=dMuhwP@YTW0{-L3i+*@XrsCiif4zgjtpkyz`eimmNCA9f zlyS?keSc|LD=wF0oDe+gWT%t1(6HLZ7&**mPJ<5GTYr%VQ7wLda$icWrP#9f>ykml zZHmJ3@a={9v)-c@0XKQ7^FVO}d``(8C}aU?geKr7q>a+}4puJh#?>M_pv6v$hEI8a zdC!chro41_U(nn0SB>$_?-qosDjgvS$rP*x$$ZX&vyV0ClKLtEmBg)aG6nAg@vteQWlb8I9+#d7jKeQCG7o{0J*upmkE$# zECnKlA9T=qqA74#eN2I9j&8uNW6R90?x>%Fby5QVEWOKT9ST)@M`n(C87}mr&}oqT z{U^*@sI9bCB(Np?Q>BKWKw>fNu% ztAJ46VWA+L^LC@~Mx3imhqOYHQBEcU_&Yr#u#H;D{^qKBu6aB#G~q2U z7HSSHMO6q2CCZ{B?Twi;XY%agNxBI0!R?1P|1E~7ek6Y4yEV~4VwA-C?gC6nrZwQ6 zVJQ>Kk|*YcPS2Mm%e=ETGu9InEUV%NsRf&%g6;g&Jy+%~1dAqxPRC+S+qG4S7={aL zen*UumE+&;f^jD0$R4$gdC_1$MS{9mU_s`v7Z^5bEDD~drRvDHz5D_}Q_Cxhkk7}d zbn^Ejd$b}b&{o{-1nEW4hsMYjKX)*MicoFt>?}uZgu71NgJd z2qU?Itrmq%=s7Pi;GoIzlqb2ZBV10=Ri2qxt%WE(hRT5jBSxj z_S*Mt`b4r z@TY(DK~rQrA9(;yM1}zYM@cqB@i-_%Ui|%^Nt2$NKlH-YV%}uWsCI$J1w1FD1nJZl zqx6qDK}e~sQ>dk`?s6sU7!yOkvi2;Bj$W0Jw#Qwo=gmP7ZbZ^@U#7}%c?_rZh-b<| zyTFK~;v`3~K0lf2H5P0p;$qjoQgQZdULbn_V3 zm{FNpeq45peyoduFee{cf_p!{@O{0M!xN?f=8>t0q5DFnsb$M4aYX+)hRBp=4X?>1 z@k??$&S(58{%R9a^D&Vr{iH>-Ch~TQj7A^WC6Pno}WS~YJu#k*|&2aiYxtZIBTfK;FDotsi@GUskp`gkHdJbVh=$2y%br7wXUm)KC7N_J!&)$W;ZI zA0?10zS<6iRq+=3+1)*hKQg0ZrJTI~#U}O+EDLhbS3VI=FssauCbl4?&Qu)QFehBi z0@oF$qwfE~H9cucnpLq0tC?XIwLb|yiobpZA;zw%_7P= zwF0YKMJp*@Fv@K#T+z>z91QeR78-nb)tCz7hlEl(rsT3ihw|OCUnE{QmvEl7y7~LLd(Saniln;QZxS9I%?x7VZC54 zX0D$IP*+zkVo*Lnpyr$~r;urW8^ZP_e~J42P{4Si>k|c9Npg%gIdym4;I50AHYW@r zIc(^0wkvwo^R5?&M%<|$O$2bWhJk*j2r_C7eg1I$IZsN5{6)+s3Pcn%&s!YbBv^=; zn@NU0`)E0HB$U04%%9!Y<23dTW=uCD`d+Fh0{wgEZUfQl=<|Q*y;(Qn#iTH8eH_yJ zs7O!+zldYJcnn%C^uKc~@%?lT20#b1)E6_NWV!(Wv=ClR(m%7ha49)D**2&KAESkK zIQwq=cC>WFQ#wq^HZ4^(j|0~pbeTKSRp;eTfjFyd>qA83+P48GmnHavPK~Q~8fv{M zc?8oilUA4zINKgZfJR2;t5?X3#cBT)r@ z!AdHA9&E&d4Nq#)#3RiJPZtL@F7wD6-t1Wu*vHkJ81vjq1uqymKxAM~qc|G!>G-2i zx&@TFKt1d*?kGeNU>jFbK( zg5DmMiWaTCl|=ty>I$h83>{jksE?6@p^x+q_0l{B5pz#@N+S;9SK&vRR;Q6;yjh!p zLNxG?gK1Z?m+8k}bC{ccz^bYJke>oC?-I!1MW5ZI5IO1%l#z2~adT`2 z9~}Nxx8Z%@A5;7O8E2ep$@O)<2c|2XNvRs{a)vSoK}*dMy~_F~P)&KyANk@0{0&5! zVpk?#R}WI;(q7`a$QujUp}3ho&p*heHGGW+i%+E-sznTF}q=dS*wHkU(YI> z-B=vTuAq*L!S!;`GfH2Qc9_-ke!797GDoU=_RG7STZvt}dDPf(99H0Cknf|&=Vk5ALg zuE*Cm^p;fltf#tdIILUbh*@66`JbtKD}xO!{`PaBZ#z8Z0(Q6@rA%a{wvL)5)4ZokOV2Oxjq7wJNsv74!z z_U%Fk1uXz6lD{%4qZ;H8WeTVvBas0ha8J47Vz-6Jq?}AMMyGDt zj7*JvkO{&M^8A)AK}Dk_;WR}QP~R|L0aSI0k5*83)0SQJx$>r_<_Yul+h?q-;DhV5 z8U zt*Po6EyI{{bnW;<%^$y`+6)uZpl2ox!UUf?lCMHj{|M3k(LMWn_N~#kbv{S0^>|s1 z%g+ubQ}8o97WCRl<;)+og-Jvmi0;GTb?4ypUZm;iZ|(J8S9YIQJ9;)l*`&ct40MCz z^52fq%%G7OICE8a@6Ej(6+NJuy4P=x_1nASqS|;@{NeQi!Xuqph7+k@t8BY`QkjpzJDBO~FZE8mH4MmLv{g}jkMl>TV`Yv#z~|F@QoYDUc^ z>CJecwgG?f{3VC7k}JP+P%%(L$QO?4JmWKNp}#XS4#VC$`}js1Q;TCve5Uvx_9#m$ zoJjyw`%zsCeOJ@Gaqf4qb@V$Zc^s|h<^TwbV<|;|NEhWge0!p9v)n8wFrYh8pk8gn z=#t!)ujC!g9esF6Wj}N6^JXZNy6!A_jz)ZMwDyx^v?(R$+ znTh>DUoYlXwfs+WLcw94q_sy(j?LjdWdW=kS=u+3(u*N(877;7Fsb``s$%6sO_eGZ zgOr2ULdx>P?CEP{3b;zLl~CL7WQ$e14*kv77dk#B)LLz~7mmk?1Rwl6V2cbQ?_9f% zRs66jDCP1xU(5aj2rfrp^;BQMyFs?_F6h)j?w{#-2g*ql>-QP&QBp^Dn90VPbu_Z~ zHuW0T)Kwwx!^Tfbx@7dKDA?-SHwjbX1_g_Aq-Gi^>xCU7D>!`Skj&= zzeG^53tTxfy0yP_S|o00C6Xo>vF34k)M?TB)GTTo=-yzzS_8+b0>fX-6CyRs^ivYY@d0H zZIm;jH)QIj$p<#Xy#+9uYoy71tUIi60_%`y9`Y$UpFS8cf7NuTyu#`3o5vP@L!P&w zdJsy7O$7t(F(s4ttg`fChhATew)<&*lcpzA0PfXEJe!0u{H!^q@}?3mADFORd(q8I zvQ+bWnslg!oM$Wb8$AE-A7kMjbv44uwZFHPXH$d*GTARQBx4LQ?_T|=*vQ^}N*nRO zJ}D+=O=?VOEIT`dR}tMr#Ja9_4O_$HIK&&XcJ>dmh5Q;gfw9+x?)?e_o}G>iZp?yD z4EQtgBW;KHb8fF_MF4lt@&E8-*BiU-LF~-{sr+2z(@Ma1I-*s`YlB^fqc&lS z*ZY|64#pahVcxJiVOT>>`rf!q0Et~Bk!9cX2?9usf>`Jh^P;J@GdVLR)a61xy@)>j z)CFlYy$Z5u2f?`L@J~-z+C+mV9CFH))IR6veW<(_Vg97Ae7>tHg7|42&$K`Xk>D~V zNb`Xn3Ko(UwLg=?QRG8siY{enVsVi6LnthZ$|%rviXniN~Hm)muGr8Nu`c` zP@TwuZT{|c_gtarFFAyH-sJ1P+xu_)GL(qFznwSe!o&Yb`nV2}GIaG*mHrcf+4oV{ zYCdI`rXr$K;ehRmWS=gM#A^gMR#X0;7GOZC$4mOA^v$%Yw;kg$l`!^uvcY`zl*EP% zT!=ZNzEx)?6(bdZrL#}hzTzIpcf2mw7UZ0zksi0c7tex(-9gAeBkC;UZYGExm zR?o$2?g*6H_g44lRW;8LBq^@NA2*YpO|($*PC>gLVuNYw6H{#nl$T$q0tfOeVB7|o z>SIA`v%2K%tAQ9>LVB5e?qV$e zD`dJVN|#iyaD^WBGLvaC%&{!U%kn8!Cq_^r`hKRI-JIB_fMjQ9yB(c7Ka*H}_=UN; zH6=O_9MVX=BBkv}r0m=}lteS!5}vo*0upkWI*nD077edw<0;awZ5%!Xla;seSs>mZ z8~Q+l*Dz$zv6Qzf@b8TQIlAS-qxKTEDiv!x&E*$);uLN)Nnx7r{b43yE`xn+Fhq@- z>a`1rIqGqeEC)-izaz{HaW|5VR5{S!rSEJa2Z_=PgG1QiLwNRTnV0__Tlhtq|H^kF ztE70H=Nf$pb|DyK9M#`EtDBs)~T7mKi2D>QKCf(J- z2cFi}OmIh_S!!uEZjGyo`@N`m9>p0uBr6~EF_#Qu5ag!CFon!peAGR#DMKebotOM% z84zcMAq;bN+E#TvQ=Xa1PjWm^7IW!|v_~`@n-`-n1$e zb5izpHLmD5rQASDP#;tFtH+6~*ETm}&z_0M%OIBS^n#$Xjb!${nP#|7#Jqm9DFk;w zh2jD4eik|{67S`J%eGx$;g)YB_($<+5%tS5mPQzk#;u*&>H(qr1V#4tJj%=M<^kr! zY{+0-j=fuiZ==E`)i*!SZb0>PhOol>b!ZJ%`tl6bJc7SwbQ&y11@gUN0_$x5x$zf+lAXk+|?IZZ(9 zTbB+*N^Yznfb@?rg&W)UN~iip(~{N&car>R-f=lM_f#f7kzO;*6=x{@4%j4_Yz2=O zai!j&Z@H{T@i@1rhbKQ2;{ORZO9Tt-kVs^T5lRpvcvDJs_{_2#T_ni{!xg{@S@^(KClz<4wiw zch2f>Z*)5Qo>X}6Q(M+uSQW?!T8nNKJ${JbL|l_!4>erl^Hmd@)?Dm%)0(U^28$m2 z`)>3O`+rR#*NY)$F3_h_Wz0ckVL8h;+iiy2((<(iX7aP8W;ocA5hUzfj75u6q`WD4Qm$e=sx{)A&Mwg`Q55Wm}cwmjj8tntB~KXdts z8VYfT%uXqjJEqgf4`2OP(fM<25hL+NiU#xZFFqhWtvaVnB><3ZfZv2P*_u2AfSotj z$bt2K$m$01LH$$|1mQq#LCE?|l50wTb!b;G*2~5>g9|;Xe`nKCT^txf!-(2)ehJXs zSh>w{&`j{?MVfY)mSOkdnaKdYm>*~a@vsQ{*u!9X{H+6r5={d1N~H6z^{p-0T@-Fc ziO+ET9GvabNsL>VnTfb^tq6g;z{=t3;M01p651_QCmUzamRv_{tlz$h_=wY16(oE9 z&__f8ojg-smDfseVJw41=?2B%;9UZY1mo%LRM$a9?J__8fJA`hQ;{Fk#ICl_Yw3;^ zs#UQ9>l4ud{-C<7Y5&BfVI;lu_uY#aqb^o>LU9oHa)^X2?2h`{m0-`lS+qO01 z6#=Ks>Ni)5)CgU~f#UZL(H2|)hc-KH_#WZCBme%>CFTW{iz*3j!zfu>ad>a+&Z4-(M9Cf5e2XHK* zj&%V8joRS&+efIheRviZ;n&AT_~E;^n3?Cn7M4>%sDwfU1H=MTUWj@f8X_Uk%86Ah zIi4hnjB&V~G=@)Mg`%cML)pJi>rh@GK<)3R_5OyCFAPzh7?V(oZ*8`6s(-F1h$BmYkX{$gb; z0*-T|ydCFDx6=m_6)vTVbN@?Lt%F-oy_-b2qgO6D{e7R;^8&%A&y-tPYb7ND*#M5O zb)hsFIN#9%rnb3}8gYurIuwiJOf{p)bI&=G#qD+u{c87wJe?QW9R!NDcjJBvLG0en zw2zYpo*eF4L*$Zs6+?k1pVC~k8D75ARC+y(aJU(ymF3HdtW;?_s%EMD$U6osLh&Dt zjCh8k4X2Cq7)V0Hcuq)~QAI!6BHZ?0S*&k$$WO{}L8ciwFY(Hlex7Z*P(M8Q;*Mg( z{D}Eu67{iwYgor~U9^B4{=)yx{<^o-W*U&6?(+oZ{J*Lbh}s#SnAF5nA}yiHCZX=n zzQxJ=td)NDX6+gL;G?{jK3zN|G1}7nM;)Jkg-~lqGYO#S{V-)s1%kKc?O~ zDyr^%A3j4P4bm-2NrNC=($WIbUDDkQN(%@|3j&e~BGO0=-AD;Yx0JxpG0e>G==1%& z>-~?#0v6}Y-uJ%teZ@V)8`{*GIEU5h#P5mNkpU$#UVcH=?vd-$3c8*ZGbWEIdEcgd zxbHwBs5-{Dla3hL31>_oo09XFGpvDZ66dwXJ7YVOzrx=x1{rBljH@pLAj|gE*h}$F z_45967eoTNZ9sGJ(brl6*NS^;{;!@s6MN>bADI8CZ>5;;pM27pSz~id8I4$Ua=UZ& zq9K3jy|@_q&_{$?aZ0PNT}2!kZzD%h^WVN=MW?z(9>%9ZMwtCF3| z!PJlH{O-a_&1&dA#Q)1hhUpDYj^+~oWbv~k#b@21NYp94EjW#%nzKKl+xM&!QrZLq zl|$CP=wH`-SFk6dz{3WRW#myj>n<676RAGF6-o&4Uj`KDddmVHY>huKcX-C=5`40x z#q{VupqTg96l~r!q;f_fB+p^I=WG zsR1L727Zl^eqHmzCtrV1vMiEBxf0JTsgc+vi*xmMDN22fGNa!=d~D3OxC~N??^6z< zR?MFi>ZA7O#%GNwA(lNIan*?sXQLB|3_Ym%_Ti_huPLoUO$BbYh9IlkFCipJ9aJ@*&7zlHiQpiW&dNk+}qCe=JwxpQ@c zVVa)JhN!xJ4t&4wO!%*pwwbDJ5zm(yj)55ok?R}WWe=ccwG#sYrw9%hoHO3G79%Uc!(X|M%m8vqCtgV ztup(e5jlz@X|pR_2)uo$ivleIpx4YF9D9xTowN$EHpQ**^GMw$O&h^|IcngldZw5| z8W_%&Gmr&9F<<7*qgr*8|GkHo`ECjg)J!W_UiV?SGkp=RCe!g#^EALO3=~5$HXf#j zFVycte=;*H6pccr6Dy%oFN}brt-XuqGA0Um#*!icSpJ=Hx!?)9C~ph)9r~{Dk$VL- zxX;fuY+$Y5@XlMwikJylgs~Q(SEP=zw<&K>N48tGki!q*B7*=a;?>&+r$98#KmB+b<6EeK(uC=bo9RcOpFRf}iZT^odsnth^YbiD*; zIAbnRBO1q{rs8A>e8D2k`tU+9s092E*)KdK^R+7oAsIAPNf3#OhzBRy@t^AxKE6Tc z!;o$?Z}o5d=!O(;p!*HIrHex>QGZBBKz6TFh~V9H?U^|!OC<(L1C6YzIFz7i#Cwif z+%!GZej>!nSfUd4yq)B{M-{zU4HT|$^!GQ_!;*&+2tH1nv#BbHMl7h5WgV>aP4`~k z<`A&8c&`sE7>s|>n%S%Vw(mM0{yDUx`}k-gsY!{(OVXY(6W^t&SB6@GWp2UGM4HO)r^iX=%EhR})u-qlFsA_gth z+MX45Z1H!mgKNBo>MGk5|HEqYp~WBKN_Q8H1HRX%!DleCA{9gn zy$lu0V#rijhCLLqu(Aw#e5iK;t-<3x9B{H(LLr%0>maZ-yrjF+2R*gMT15 z)4{%S4tSsJA>&9+JBVj3t(LZSYF3k0dGe)}Z1c50J;A*wp7g`#goQUzf!Rb>QqLRm z__`9P$qL4Z22-N!N)=IBe5TrW*K1RC!~MOtjMe0K$|5OeDIIc}WGjE?w0mYeD?<3N z$sK9xxRoZ8)6_eVPKpk02YFvbDg0f$q%{Oj>0VaZ6q3q5a424y$U519-flCa3iA6u ztDNJ8lTJzWy>GZB1AkEO|4=pM#7Zu8DUqTaNY*0Hg-VOh#TG|8BkNVp=PVH78 ziy?4+v6WC8#8Z+iJ7$(BpjYt${ae-_#tyCSBD5b1`Wq6|+vsEl;Tthx(?yFqn~cG4 zTY?wvJ%S|CLw^2IXkj8=er@DNTm+wbe)P7kpwTZQuwaKkN4Q_>Yq)O}m~@ePr2NLA zyGguNSpDBaynMp(ELIyT7d?XqK2Baw8ND{aa%@%A z)BkaQ_x*nPvfsP*ecO#&l;)?U52m4(86N_w2cGY%CyK$@TgrRHD_4rPf6L>+k!&#pgnU z^GbhG>>@riuVjFd^(>r}42Zd8wEb`}sMOlO{%iG$IUfE_0BFBOOW{ls%#L1tA4xD$ z@W%lgUHV|73-PhGlJWf#iTauL+P9c;Uxrc;v)PV3d+#9%=*^EB(J61J!96Y$MVCCnwT;PwWj+&I`uwfiUlMd ze(%IxJ16|k+f&F8QBnM0n#7Sed0VqaTva$hf9rQpnH4Dwe$I0CXp=#+NMKl9a8sKR zZsQ~4N4F!DV4M(Rzj`mfBjyGh9N{nY)^fJfGx@8DN{=1PC&?cTpL3kqf$l~V-Nqe~ z>xS4>UNNHWLH_=~#^3D5jq!g}NXBKPdin|_zPu$J2DdSgTkcp-kwMMJQfw^OoKXLZ z+18HvrTp=!3?0UyY+%H!$8s^XM6>jfmnYg1j@t71;=7%t7&_W@5EZbrP_l-c{E%~# zLtxJ@N1o3$k-mTtnf{3&aSfUCm=Se~eC#*z)t)b@P8-PGkET1_G!2PleJWngPE?dkxH0wwio1cM)|aQp0!~= z#oOM|B>9O;_s4`=;+~WG@(inrq7f|KlD2pXp4?aC^co|FirlY5mxR;uVQHe4Je+|f zchVmsxy~?mL_4RSy9(%D9oKp-&PW-&<5eWKUpLqMSI%t6p8TC>eU=c_Z!@He2)+*! ztXUBv8K8NdOpk}8IA{m$1Kre?p~oh^RcEQQm{)@)*alSL63Ia1 zpd_BMjS7GL8L0RjXc7+~L=PHCZ0#04wkOK-?Qn1*y78bJ)qt0Qz~bEi06fF~x&}Be zVrh`iBmS<0ihLBG*c~c247o}rVXI>YEQN-cCrIxs@Awr1FWovmLo$xHoTvnE-d3U* z$m|MBcJP^4k2L9c3I%?K zS8Ta(*Mi|EP6}yNr{!on)<6s9N(ON8kK;=(&e+w%g&XOvmg-GRmS!>vRz~61by_P` zLeB5|0G97#zYSUp`+z0crA(deRBc&C4Ky>=ZRE5xS#1#P-NNnT15psIsr?^XyRUc$ zj3&Y;_}w35m}t%9c~3b9FE_5U=Z1t?CUIDs&&Ey~z$KAs5XGYklNOwxcQ9`!tuc(z z#s5qVs2+(ucV0Jt+#0=XMs?8porV5D=4r@me{iPVeYk-8*QWLj=SG5IZSM4q4~#B- zEef!A^D-9`jvtug!tuYylSj&1AAG^CIM`S6+xDZ(6BzW@7k{EW++W&1^pEINT?pJ< zZ=pikg2`93m|rPUdDyGi*!W~a;V{I~kwqN4dIdHh`})#i^l}gRGrt`?)5)_S5p~!w zs>b|6=`>Qp!ta5}a0x*IIj%P7y#b>=4*QRm*2S<#Bs96h5{dw6EuK^;9B>!K{Q5N} zNVkLz)Qw-FR6y8rYk*37@u=>i@2(2mkXronj6=0jMU8>Tcgsb%9{jrBCRu<1YBJ0R z%dz6?^V>LYaiqU@XQ%mL_j)9LM|X>Fc#UCU#Tuc*g_Jn9Z~AM;XuI>+c({9(fv0d( z*Yb0(^m_3#(TXqYq2yH=Wg~PKO!Uv7-#tQel_-EYncqx5K-;T!j+pz4YWPi@tMIKW zMa>6zO9GHRP!%0eJ3>=(lvco8Hob%9*@DFlg5C(45Aq!$p@@S4jtv!I;@+)I38s{d{#1YF}|Z?QX`B0b`64Usm9tULQbaHen`oQ_IlSS|ze z#@29Vq;r9a4AtMm@GwuNXRfxagy`-I1F;RMr|&qa_oba)^O{#Cm2Dx(d(S1x)6O-oodsI7_)X%E$ubR!>z~Yat=qI|}hN89!eqp(2Jm0yD@QZ%W%I{{{ zo(@J53W9J?v(sbXpA}_d_xvN)HNG+M?)t^DR18q-=7vXF2p#~<-#lq@he!@r`iG6G zVy2^;cH}*<;7qtAC~U{E{5K3d2?m=Sb9WNoHd4GGt7+q$9wzTJyIPqeGx79UJ})D; zrsI77*PiNWPv$Ow5$6ij9}^J6Wu)6ji=-s6KjjEYG4I+)=q@z zeKYbauuXi6To%Ad>;(GWO*ObbS7}ox3cQ0ZaCI6D5lk>@1@KW+8EZHh*OjQ zTVYa?+gUhrLEg=!nj4#K>YD+sPc-R`U+W*Nv3NTp`0m=&=BU$uA1ryH6@RI)?ux)2 zJNpuvrLT`_U^-<#4`rb@yMMqFxnFYHeBbdv5`dM{kc2!EBiO&mhwh`S9=w(#|eHHiqCQ1ajuk;~KPu=rg zaF}H+ia+kEY{6{`yfow~Zl3PL_+3~44oC9Vt5lxrkxBI<$GJbi_Lx`?-tFaKkM;JZ zkwIFfHMpb2x(7O^CDU4lSga(JvS0y2WEkiHngo{FE5X>nS$|inahL?VOym_=x%@_mv zbqW6QzOIeo|IFwM1)a#0xc%7y3;~JPE1x*_5ksxT`E9HP^nUwA)p8ac_t*1DAxE;I zarw|2E^Uxu2|Dt+f{O*Bx*K0?6x6JMCPS|_B~fT)YOTEag9Ga~-bbJBIOv*dVd93S?u!pqoVQiLaPN~0g8v_pfZKS! z=j3U+zJ+*7m%4%fKvhLMNwAtrhtoIwmquzxwCWbdzjjJQJ{QB|c z1WM^3?vw1FyZ4KxyARsvz>fTWME-XWCAUd{=%DxX_W2OBf>BTv$J&Ib@3;@~WRqlA zr!S%d%#7g&Ew~k8CGG@#kXwDe(s4g={;xx-3t)8>`tNFdSI>;(=-ee2_ragbE!#q< zGgFs$onVI$mYiA2U!5d1D3S9Cyn?ej)Y5*vAt<1kqNsr|NL|tVlSqF2_V$~eLAdW* zCHpFqgp^T5PSbYkWfZ|#vjGVw!Cs%c~sC79PG(q$=cSgEbD0MZv1ANlesNU4en zZL2ZW=K%6`YsMeZY@l-Rnb6oBnTJm`F})25b*pDS&}NHwI95l>7g?S^TQvF|eWL#Q zKx2T)%Z)65?_XNbfR=UqE$GXTPrU39D7SRP`jqrXObBO(W~cq^Tn4?KKrQ3Of_HKA zW=0#3DG0=V%wtl(Z%&U=8rh%yXVHK8ufL%wnCZk{1KbzfdkbWzR1I~vKJHq1ZZS$) zpagY(riv}SIn(CX`a&AkatQ=j_PMa{?C=rc- zMK-dmEz0(^{`xD~G$I@f>C*NW7#-g{SHuj3F~Fv-9T?J+wDV~XO@9|I6yJ0-92-7q zMrUwINJJ6*v(2pCo?@K+WhdThYS0{GjunPZ>C5pKxk?@sb5visW=y9)A^ug2KQuD~ z;Ti?hM{Qc$9@Bz)w+KO7K<`w}Rplx4W{_j7^b2LgQPNipQG_FZmF+)k|KFRUE&P9_ zLJixc7#+m}4CcqhgG9#@XtuYdj0BKA3%8)hcxPXj$GW=K9UFMO9z8y0=6y`g=TFRp zDs=}`sr-u~zGwY3p?cy7%(36tE+Mp5sV->?Jw28vNUJBX#+eb3TrBk%O{gSi47?$j z>UlmrWRp;E#9UAB(4`2WhzCeV!Il?EUG{e5Lzx!|s&)Dn6k30ITU5T3z#c3yRgU7B zhK=AAHH4O`6SrqsGN%P!re2P2tc3}&(qTgS90iLc$Tj5%=p zh`}jHkGjFNTEnZEB!KtQEz>y&c0cn*AzfQ$ z-&}`$4{;hvZ?}Co-EF)VZuvXJZy<-_OZrF5thh^_} zt#-^Ju~J=I742cJhjE@i+ROx^WVw)9D+Z@>)ix$d<$obcOQq_ESux(+NfI-7L zTqIe(HIbHxGO~~3pQKrpOj;pfj~KySoyf`UNlF7-1Lwy0J(aOfWHUW_YkU%|lEL7i zZZ*Op$LRZ_yUr%U*-Mul73^JTo9YSfg2^*Uz^HJPbqV^-w)lf5anl{QPYw2#CL0rj zxVOw`MAiOb-;Yyts2>S>79G-h=*~l2e)g|tUffTPtQQ2VO?vIv5Q=77)qs8?BztXj z?+y&Tbd)#jpkDE+6G-9lFLvLZJ^r+k2TF1A{}Wbs(e3i1cogM?U>VqQGyHLvhSDxk z(0O~u46rF@F(4Ish9`S_Rg`@rI}<=0O9GADMqh!3s+9(YiYIesUPxi{rAS~97s zr9iw1O?Kgau+hEMd%k%SYIV>G@u^j7z?iRID49mCPcUDYjQTsvrJ#zM_?2!(up-u~ zf$%^EIe(RVb&TF~m$oy!Y;unqg91-6WL>Dgf*$hLN6OO5)nUk)?P?*dr1{4ApN22t zKXQim@7W}`J^$ifyDq#_1BokiGIL5bOlDIU0H6seZZRM&@B*X-i2jC`f}FsOBpIaIR@~`=XEwB{Ibhk>L-J^Jr%ft)!kWprIn20uQWVpFlwQW1tV=E|GF=I z3&?6D-b*bePM{~YDuf|v%2)I@vc8F59a}SgKlt32LWlW91=lVn0<~@@nzMF>3Io#+ zc)a*S$S8&mm(Vm-`Ka-8d7hAVH%UY1RJ#y`nM4&Kby>1G~44s0$>(n;8*OOUNZ zzx~a|9`Wf6*UOR?g0X}z(vbbAXm{h(6?l;QtdnDACmLC(nrzLuR_kMt=y&~OM_mLX z$~Y{j4j->5^1^Fpd1ng;Q`H9J+sw-pUKUBTnQ%;^9HsraNK@O`*z)?;RQ^|U^vr=| z6FLWKYL!d{$S&A1id^Y{h*dnqC2{6N{eJgG4qjma|I@`% z!2*#ROX9q2Q1tOyP)^o;wGj;et>vl!R&gJFFzceX_kMJ~Ho^uXF8}`!7u5r63*-a# zm-BdN!Q&Mnf}OW0V`}hOKyStiV{lB;M3Ks0j@zg}&sMM-^REl`PJ>uB@ka@pBz0O2dUgUwvheJO?J5DpbN7Plud?ku{|$d_7NLSfkeo1NNecz2@+&kP7Qg7Zlc49R~^~3 zERGFG49e02L^;!5RDT~c(W2}@rH@0lIso|MV&ZubzR2O`=e1H3M;WR}~$)n=G$d}TJ+qL%$qKb?cT{kX)Q^UYF%Z1}+o zLxy_=$zik``V?{^s3fr_#!u_ZOdLJ7Iyb;4J>r zdqMLYB(w=UG80Gy#`P)ydL5UqKZfQ1AfT9VB_+bF_MUvtL_SGZdEt`j6wa)Z#8J%k zTbd#0ioE7!&wdQNB3(sjF0^hSS)j5f*t}F2#|g6XLZv@V8I9R-gxW^m*3Z}<-;mgb zAC!ge8r5AKP=GmLf3I^&r(y$mtiuoR4*GV`oZ}T)k0)hCCi-0z<@79hDjev>i-Ifr zF+j^ABPPhVM_nf~Nq2!4pJ=9;&#$V(p%dO`JFvS%fLEJW(&^48Z@+^K{5h?4wN6Yy z0^md!rS_%#jy1;4KE?+D>HFs-ZZSR?M;=*cr%#1D1ZtMcgf}(xmX~Og$>-i*~B?ZXJuj`HPRcX>=Ios+chg=F61{*``hMT~|CKSf%mz++sgOpae6#A`z*3)1`tB11>;ZsLsA zI+~qf#V6`VIuXF?zhggvN2&>Qi;f>#5U&~WzV1X1+H6P!WuE`KW*vprs0FFtUl@7j zF&_MWfv>q`asRN_9mMhyO_hT~b17qHz7V5!M$>jIqkH_uLcH!TxNc&dd4HG+ev}Aa zgQc9TQZZMOQ1Bb?YQ=5r>*5@Hzy}wxjO<$<6|5HU{ z*((FMtB1#7e^%OJ69ODJas+=eCy8)t>F@n^`|j4@>emnxbD7YppR!mB?_?o;a|+5V z6K`Xa*&{?!<z zPXY-BIj0T~#bmS4gCGO#Rx$^I?+ih)s#Vb^&+EV70U z%u^%qS$pJ?;Q>FaMRT%aA~=`#pP@2#nD?UUP}~V}gQTSVJfOT8c(PsHex=Iygj;Yq zWW+&OD)fp}7Ja5q&98!H+wmrbGpR5mALb!9OrJ{ETZVLW+-K{!2yJs6d+$m;dPwbc z&D;|vEn9kWxdY+=Fbk}XV)DGg6;MqAc7+mk_q>kve_nL`@eHrcYstdB+!qSd#y4*J z1q;n{6M^?RbKcz*-K@*p=CA)e=?X+%$@UaeMV!@-($|hMsT7M-@!p=+g85ZZn^@38 zfeA{ieHaos=wUF)myN$bc%pm7&$Z=!AC^ZfC}b-aH<692Fapv<;5{((L@4TwFPb?z zYreBgWgx8-?3IE#-EY!?=Z#*juKoCPIwsP~FSxEEBbbRGNEZh@ovL0@d#aaU`HzUZ zR9+~e#Q=nVZzRifrRGE5P(Ri?%bWw=$f6pT0Ri&}m@XI<)yq4IBR7EOEfN^ON9zjc z$t4s*RZpv)0ufBNs|Yd_s}F#~CKnjlNm$@PvB##H|FSh$|FKeh4qy2E+EReOf(+0j zf@?eRUD;BA=j^og(`M|RMK7^(M71l>aK+z7?EO}e8E;Msd_=ofWYQAz&^l zg^g9HO3X^miLRC0wO9xN{EGdh)je%JRI(I%A@@mze~BOq{Vwy+&$E1H+3+BL|BKGO z@^xy!kMH6N{x3@jLg$!{wV)f_c@l^ZpI3B!-48bOB{Na;hHUnV<4N9<#%N9L7Jp1O{Fhb5ba;<-BSa!T?b$ zoI1n0_?(|t%pxKoINMoHhXSD?h(I77w?Uc5@@2I%$<`HN4#EsFw=`AZD0z305nGIo z&7B}_ixztY(Kl;-$^owa3UGlJFjovbO zzg<*25ALd#R9gs$kCUNd47OeRbvF0p+Kk%qlEUykdeaSN`HpmrJT6Dab$HEfwjrAc zbVYu}J#TWu>G@-Kgh5V_R4`6_c8jmu7d%QW&DCbL@)brx2;Q_nd& zlftJMtCzfU40Qvtfupf5VjRjh9QKAEK<}lXZAT**khuqDSWp)ZPV%1^0eJ~o`zY@* z9_iIJP<#Q<0rUME4wiED{y9a1T4XgPA|b8L*2~<&gb<;=_+aldDEmzXO_fcnMaE7M z6@o%u_N;#hgN4tmSa7unEGb~vd8RiKRk(At0mWQAB2X|3iu%$UI-&p2?R{GZ^?ijJ z_xZ(#XxR{tNj9~2OF<|l>4rpJTc1mK+{loLD(QV3%|bo~COizOr;%Y<1fBaM3^(&x zII!N(cc9a$9OtF81La^>gsY*Uk~>-YTiS&FJX%`B-rAuMu0C1V$yt2)`<9&StOYdn zxPZZ1j>7fk`0`ez&=bo17<9n##halJBguJ5mMk2R#`3mk4BA!lD6ALvYhr@a8k}xb#EC@#3;LVFbi+}4+Ac? z#8*9k=P8)>?CSqHUUs~sta1|~7;61}g46hUMxeDr^t0rRLq0|;tW4u`zXUhZJ`T#Z z3EM^TE1>#Ch6M_`5wean@Lk7z?2`65yUymfRI=Mitr3)zENKTfQJ|6Fv)*Vkq&QWL z6M6kc8ILQYK(#72vZDN%Gs$K;lDh|5m{pbe)fwWA1JODtYHXdadLseM8P+pM2j5jm zW^{R@cqM6YjTuY4u@gTPrsDNyLq@bM`eF``o%K~impNx**gFQs#kO%1G245riyyVi zHTKHwP5C{!rMajxZ}Klkxa|*RWAC!0q2}ON#}=c&mrEunotkVrab8vApG?? zF)zFW^XeQFh$SOTEcqL!8_vr%+g)P^$P@;iT4O~B;WPGFIB9z`Qm{s)#p=P046uz1 zKBe7jEnzVl3f@e+@cWPwEVn3G&Al|RCcqD{( zhr_x6SiKsevycw?86Y6|qX^ochy4LtKQ*DOAO4+uJ>3yB+|9W$Mifvnnv-%}PZA<~zWe6eRXP{rBk_ zr4R-`HK!0O;$YA5XsZpEWcl87>~l+Snyl2%QQ`1@)<3m73;qKm;dagN+!Qr=%!ry_ z!&N6l9dpjy&?yo-hWU(mrlf zs9uy4z!tPXDn~_M3z@T|7t&AD+S)a&A^0jK13a8(?_E|l zoFAVym@#-|Ikor`9v$uK!K*J`NONBc=iQD57@5BGh(2WoM`Zs(Z|6h5cB6;2CB)4F z2s%XnR2ZX;&F==M`s>26>%W1oXHX6^!uuCNFRVWo_G)52p7dSdBHJUZIE&kQc{z<; zl9E3pFy&%E?kPb;K@Y|2YvRa=``Uksj(;+SHTlwFMR{BN^x6B##UK?(dIX04o6HS@RueUN9yDNVrhHm+6X7$-0w&Z!g~ z6j>Q^y0}jy+N=x>WpBHy?lyZ*_eA_x0*Ovd$Rz&L0gC21(q) zM#jPnO#>>JQ$Xk`46;SFWfiG3Wp6%ih(*)m-<{`CxOlL&;UtX_jhBDLpNMp zeyRBn-A+G(yrdZFlX@oFQEQLHHgRZ=DV9xVf5zA6B34 z!8}VqmQLhJZ>SsIYFdrV+m`}#cvF~BA~ZM<+ouF6=kNf&YSAAts97OZ<|{Sv>MC$z zR_NtYz;1uCd-COHNXK*a%VQ!4RTo}FN~c}dZE{UiEYxT0wQ6*iOQ!(VC*k*k9U{-d z;)GU-Dt*opJlp+9nloHODw8y3%|r6Pdt1vna7r*h-#NPa*2^+XJ@Mh6l@eOmjk;6m z_2oXDoZIitZ3a*-C~ar-9;2KVkBj*c=PCY)TQh=dX6Rux0qUX{Nzt^4CJ^) z!mo5929giG-r5*B^sPMvev!8vau=yIooQBR2Kr{Nx6>uKkgkcdQh?3TTIYW%(t9J5Jaq+hu+dhfNEra15dt_9V|AthYyy~r?#O^pdiw)GNXq~CU zpa~nvl?-sV8~E0?l!m!lL~#7+_E`9#rv~n7qv!;Kz|+moFxxO0BFI2{uP6pE9(5g% zR2IlbCzr;IpEOhQ%Eal=HVvZWv)@>^(Uu&2KdX@3TlsUfyW_S5CBlR%T+V8ElVmSN z8^tgy;W?O*Fb(Niy(iTkTFQ`){?{S5BRy~V{CPh!qbq7ea2`*}-E<1W?jBZR&B7Fh zZxC8z zu@&osQ)SLUSjcB?pT5;aQ8*eM|5=dS18fpghn)bq<5AP+>eBq98y~+H`BU`BB zB5282Tw{m2!jl?dBHj@bGc&JwHxlU|d5Gws!I(Bbu3h~u79DL{itAFwr2{wF{m|xY zI>?Ug+kwv#gxT*n>x`pFUwXf1(~sy635GvUb=Kc|uq~n5y`J+wEkGrZ60S3X`KJYM z^RiUuB*7U%UIOFXct$&1BEyZ0e76!O#L|Cg(H?w$IF$Zy%x{XYK~MNIS84tj#3Ke_L)Nux{YSbNW zDu2y9#ak8rP;+z8d$OCkyp`H*|Lkm*4CC2V&D*&ClarGHKu%c^Z)<`EBiF2bTog#b z1`8UEJC=aq^v2Pvly&D;)WU0I!OvtS1fLm7p7LNaRF~aqnNMaEH|2-Dtl5Nq7) zM+UP=<5J>}chc&}ul{HB7{$>11}?m*hpHR#>~tNWTWH=?Z~P5Ezh!t$ef@OVIBrA;upD0+z=bNYkridt2cP~fE)h`L zuO%7xi{ntI6jb{~rPz4JD9F%YJTsW)+=+hrlw`=Cm96{6G-$FwZie>K(tuZ~J^a^@ z6EGKBPlnBs-T3skc2b8=8Jlyq!OlX-WyUORahVVN$aNT!kMJr)77h+A|~B)YfBsj~(SrBcuX_~5LH<5J>h&zmc%a9`BH zmz-+&;>{sTdq}Y7%xD(vC3E^L_F2%?FdS7hs=Ujuh#GdrwELcM341p0SO`2*SB^c` zp3gdlYcu`kejSk>aJHk{8gRyG(H1&)2g!d4g~Q2OFHy#=!6)Ak1#ixO<|~K*3cv3f z89bNpwev^h&*cU5i!4Z2^~e47$VBl_eK@M*z7=OZm3t-;=ZTzYXul{@U{?KloxvKR zTZIXC_wm^WhlfLB4%a@Xlo0_Yqp^e-nT5z#U+^F&V>~ApyfwN}De*da7mny^iY+uL zZk3l5szqV`ZgUXIBhV<=zdar;)Ha($7D2LQ-kvFVd zFIvWmjtEF9gKB*|Dt2r$+(|eACcWh?{%FK-X-FaKt$`j)DO-_mHLZy`e^II`5k%oS zb=YMI2RQy^lW1WjH631U`3Q42*C%&`FppBfpCsR-Hr6qM@a62FZ+8ENn-UiHhjrYK z1MS6EEJl#}lrJX9rp);)IR4G`|ABP5lM}wnA(jOY-xx2bLX@_eB zobX1>*35k)srouxV~jVzDk#p-q#rvzsDFc99|yuHeDKU0tkK^>Yqg?U?xGFpm>))^ z-240jTQ(*5RHHj0h59ba#M#nddcc#Vc-IMV3b_eTg2Z_os&UGsm)Un!J@S)NiHi7k zGC5a(0(yL^FyGEzw+mU`Q*ESU+Ps*<3fCC8bW&Az~?hPBSr|DDzm^7T<3)w(38x<}ckk(7O1)Cl`GcGsw{uAMVFB>%+v0d=^RTtym_ibUI zg6Twy@q7vcn7}J|BrW%WT1Ft3+Y$9TnQXXQ_V~<5Xp*Jiksn~CjQ2R~MueDAA}T2< zX=3Gp{WuMzHQ+>qxGjK?zRL3OcrH#wM#TG@hzIXKJ)nZW)@;YPlrkoZ-Gt!LuGK1k zKMr0t8@V=g?HhR#=2=57j$HJuhxk&IP}h{@4ERcBOKQD@vV&mnZ zSv7+<`kSZIAm2Fwi(BPXtjYngyEpNdGKsih{dK_Ca?gtqTF_?>JH9d!`m`_gJrqJR zTlFTWKv8;wav>8=lZHP=z}gafNe1@#WGBioo`PNceL}?oZG+`>03g9Tn*< z)H8FTpPLQ^Rh}g~OKifpH=f8O3Qay=y-A`UvuF*h=IZhJFh&842?;5Ze51rKuu_6) zUK>rVcls!>V-o>G1=`p7wPsU>jvJh)YqM-|DeT(V?E(>-E0jsKJJ(q^1`GbF>AJfeDqCH9PEFe;yiWY9z#R16NjU-9@f+D(wr9j zg#ugpnFt{6;J}5nAAl;d(^14ZL%JHiY=br9=nk6SSZq@2&#*Ba#m>jd$rcMR&vqQJ zI>Kk{8vkKswgojRvvU*1koyR~Kf;t3c@oEpR%$|*m)isr?(=t}E5JYH5qFP85yhd4 zwON(>LhdJ9*?rYL;w}(T>s`dwn6PIH$<0s<1tx?0=0V1o7U3PaKl?q|2G2BbzqQqW z@5W?C<$a*ccl^bbLGZs%3B)(5YIU(mVQsr9;ivk;s;`wpE&>Ly#LbnpF~?uy0KcX% zMC@<>&fVh(%8_Ac z6VeF8!4vdSzh+KH8l-b0UOtwxt4RC3&i zR8@%b`?K|c!rT$?Y)OBbsURCKP(Xwkkuf26MWl>5{P5w`UPYC1?{xJ$W;qTczI$~J z+Dvd;70j#+cl$Rv?Nw_*tabtLBp6{Ju_{Tc!T)x>gM9p~S%s$h1g%}}FHS=rPR`@! zsa$M~`f2YhV+Gum+P}=!JHqsZOm<0hXFd4LCvkvEs+nQUYH+rbtJMxmCc{jsCWwaq zeh_DaZQaI^zaPE1BH)UGf4VT@iWUV zn)==TngG6KhUDKlou2P~xgDLd)Ok57v?~5Lkt;6#{lcQRCVTO-_4D1NhtooY5eHPv z{bM~%Sq%CKcPQ-f;P93sixsd}LNq=ZWpFM2kNdLt2nt|kg)Lk{5pGNg6&}TO;kpPSC?XUn+JQ84>kqt&vjNeo` zL!RVi3Xw9Q{G4PQxz4OjC1YW;B6K=inVFf#dt1Y$Qu7l4u!@`}fu9^-0=LWM>)ZNO z|2GK_#Mx9U7JkRFmL8J4>Z-^0hk~UIOVUuk>wtT$(Dj3=#(LHWA8^zce?HA<^~@~9 zH9>?o^C3qgZ!grf^k%gFGsBSUv`sfyfw4fus-ku6K}+gu{Ki;4oNLM+OAZrIP@U+3 zBa4#QTVbMjpC}Oyc7Q!;!PG;_JZ2hNq5%=c8`(M{J!Kl4=N}PW{jcd?kXjA?bbdHI z+!S=`K<6wMZZ^HOV%ZkgV#F6u*XO%>Wc$YnIGvmx(U1gLDu$~eOB^ZOV%+8Oso4MB zM|K`j-^1v0+j+Sn?nCFWqI6ep@F-nTbD&X*(Nsvh1g7eppG}0jUHkk}#Qzb-zTkiDjdI`8y41@;LATLY+&TP2Xzc!GBFP_Ir zyQW>Jw55pv#=kxgv+PST{yar`2oQ4ii?*dENC@j+(pxs)&yOic!YVfftYwW^X!nDW zSxj7Wi1lzp*sL$t`SX5%d|b*(;B5>Mz_L{tPWr3JNGSpi%;gNH+)wNQdO8fD$4|_l!y?f`o)1-AE%LIfT;E zB}hsSFm%t%d-&da-}U1U7P6LW>6zy|vG;!V@!oQfOBqde&?(#juTDP(^`**<_jKQ* z_%kKCTq)XaRCH#D62*;*6m3?TA&o#@=|x{g3%o;_2SOYcjKcd`Lw2w=kpxDDV_^Z= zG{Y>Y=}7U8^qJL^{O4RMq88PN0=^_E->aV3X-8@Nfbz{qc0Fu~A4*I}usbvKgY@gR zPh_ah3M*Qk?}mZrcc=CAzt-A%bipn3;MVXF5&S`h(ph(5WNN}yj;sW5gd(uGu)`lf z2t$wk++f-p(WG{a=H2=Jdt-cb)nbm|+C~&eCrob(mAVE$_9d0jcy7Lk-a8merhMY* z7B`qm27z(uLg?|gZim0#45&B-RHWS_atKoVBPU$y)Wsd@Z3odwpDESqjH(9J zUm|VNThXv0+SO1{W}y(~=jZ?7i%nrZL1>zC9IWE}OW-GTm$3YiPER#xh( zLhaSjjwTP{!xD+^y+oYs4XOGI6d+ya2qBjBad_37{Z%{4hBw~W{{oQ;1R}LRD=PbJ z?6?L|a4!=kAGO6mm+`QE>&>leD5_e4Q+e-Q<9(+vA{|368@ZU= z#zYm47;Rb0Fdpb(AasS4<1WRW82n%>(%YL^!!W35V`HFrO;r6Kv3Ll5Zv5->vB>wd zO|s?udC~BTO+5btbDZ}AV(Q6xJrKBdfjt*YS5u##DEFGE2r^@lHMphUUw`u(7gJpz zU}1lS7YZG72OGM&M`jpdX=+CXaO>9~OqOrD!0x(k3d^QsFaN|I7O zX^`h|7^|R2lz>i;QzTpwg+1* z=e>?~;P7?RuXCq=Z~h64QLC4YeSuCPlh>x_h&}l*h(sOAL4F6n7_}u6Z@6<{Y-dzt zzfT*_==eVxNFfA9{>n^rCqB4q#637twji07bkkI;S!n3wsxW_W3S96rLQPj08*pV* zjt9rD5rw!;Y0z?th;t|k@&DknH%$Ct042f~sZWz9&C3-|X6jw*hr4+5A63H0?()|> zayrCS7c`_*Son0cX)uyY(BV&=P;3RT208z1z`q+l@UGsHR;5x@;_-u}cj6V_V)1b5 zwVf&|Vz-1(KGTk`ULk?Qj`o1ex9fvW&ld`#=FCko+N|{0q<_?pC=!^%pgS_s6Kj!; z6{W9m!G2C1rrlLtFBm|@iL%W`@)sgIXuATk*zl0vuNo>`Nj*Iz#|rdJtAzdX9NfG1V$b|U6{KqrlKCejeQmCbc0Th?b(^eCx@Gq zv<}yx)*-pksLwFxe{JEpz%ze3S^zaAAWO8R*SQb|dy?hAEW;{46V7~KxIo==`2PRz z)LQlS%#Py*5e51#0l)YBZw2;T85ly^*=4O8t0RlXOTtPC6Sf>o7b~Q9W1q@XZL>3M z!-aToiwrQuRiRu`3gut*(I31sQf}keYG*w3&WTkAC>}aLFTMFaDq9 zv+H_oIw#!~LXBrGwnxYfTN+_x<6o@Qyi6)@B5tOR#AwW)t??BH>>FmRYj=%du&IP6!yY*JDDo;ts*Z0? z-SRT+d8Dab4`9(^XTz>GTRoykEOA@hmh0_su`9(U3f^RMC|W2PDpyCEFg&BuxK0My zB|HbJ6+rA;=?4AzcH(KV-ln(anN7S=*C$NU!AmGQ>B{Ia5)hT2Hi2 z&F!zYwy)4MT6J1rl?z~L|HR(j%_x*eUXvo(dF zFaL6(Ja_@I@h5;}bsGO+CS5COR1$Q5;E4*&6xpx6jrHcsgEkcXm1|JKU{_4o&>e}+R z7$L+hHp1O&C?2H!KC$#EUg7)a?LM`ab9hmraWs+{Zapx!>-fLP>*FK51qU-RG}m~!O3mJ=!EfBTN3`A&Q;)WL9&j;I zfhm(0v;m%v+mhmXw3G-KoSRAMK6t>}Y8&`y=yUPl3bM0Rj?on{Ok~PIje7H{%vri5 z(WG5;^7vpEP664xpfFL2JF)n#)0yra<^)@vI~L8NdZ z*%RYyXE|Gq_eh>ZJQn=l>kkkH)LxTu)N1QL85ys_&I;ax#NB-)^9$HdTWHP+fI{k7 z<5mr{ytv094&!_4CG8CpZGwv1C9(JnOR+?wCa}+$tJF%EQLgU&dGdQCe&}!^hOjtY z8W3D~HzcW)Vd~wp{wMt^;%XgQc=US^mP9LE@g#-f)4Z9U&(VnrlupUZ; zLO!x&XkiZ&OH?~gsJzA>rw1^)&?Bv&fQIBJ%p@y3P!|Ws1&omOd5Yr-q5Zf2Rctk0 zMP|m-wr}q6(;Q&)y^i;ElWzsK-wG!y{b&DQrhbpQ1)cZSl3NqEvVs!X?sQTTB+4w- zWqzLe*2Z`i(%(dD4yhwE&E1|c+cG&XA8agZr^@2zpu=9LI!Wk4)^0*LWd^rN&6-h8%MHm_u%JH4HG7x2k zMHL=88^-b+n+3!6ec}~oLg~`vc3g!Ys;0{YztuE+YGXqxHa2GEdpV(2I;l0klJ+_= zRI|$?a$quH*Zk}c0{I*N-_!;=&q#?kVi>o%?G&0}+GrDuKD$>93SBUK+Eg!LoY7ewCZvB|BMT6(c#*|Bygy zn2*HzR(G~GQpm>cv0MtPC{JIn~rMrr*V7Rhl!~pTqavX)SAJ=8)tyzWo$-gfPvoJn3+^^4unOS6gSb&>r2a6lL{^9|`{J$s2B3GL{ z^Hzmv+v~cUjFw{0s&!5ko;UwBckO&MefQ=1o20S5+jBS>i%_}IlWSM344#^6XgMXk zA4LzIB1T_ho8QGpu**%$`!E$M?T|H9b2_mI-RWkM$GoWBld{<0`&47Q@b-UULXQZe zuEWkxRt%8^2(x^X$w{UMUXhcqH9HFUgL#xL36$SqOc}6|fVfbCEAA%&!3CNe4Xl*9@Tg%RlFnGp#%ug-IBM_Y_h&(wX|AE|4 zCp6FK_Vv3N`&w(<@n0%#zDV@{fNUM>+p<*R!;|hl_xjW5DH1M5_fgONae5S#p9%XS z{B62BUI?>Akm$pGU7;Zejh%*FaPx^L&UI_-S#<*a(=AYdb{4`W-9nz2#KP3}c#W20 zfayOb?k>ay^)NNU=mu4M*0?Rt>>p-8X&>k!W9ZRk0du$#YN8gU@^9Z%c-dplY|&Fn zF|=JLY2!{Zv(!NWn2*hRCmO#RL)9UOzo6g;Q2weJR563?3P{`^-x@%S>d$-ZWu92< zkW$!l^uXx<&4ZvEnK;)^M-D(yG#pee!nd;M6vYWX>wGyM-p5;<^9DD9VU_2>SOJqS zaBKLhGdb$4EoiYs?A`EatYn(p$y%|aCmjGgEB8oNOhZiLYp--e@_c8Y>VU^7DBz^? zNv(Q@50p^qLC5iIcz<<*j@Bq^SL-RyzXqoq%1BC_?yt=9^vmhi@s?++hqV#YhnCCm z%0SVgU2W)3IxE$MyYdn&a|1Jow#2|3>C65#;fh`oGa3 zL*C)%|K{`yA4J~Ot|roFHppiA+@Ktoe=>95f>LJmL%CAK8(t@Z;7v6Barq{;5qGfI z8bYgdgv>wfe+m6awVRJDFdS_Fq{rFcE4u?-zuqnfYJ#_f)@9$anwsB{zg}85kv`k3Te8sY2z2Q+6t8rF%}mvbPG9)Lr35v$ z|L1Er-qUBhjeqkWT0+m6=R|>xW`Y#qp|&Jth}jc6+|p%F#YXEd93ES=d*mNT2{i>un2KLV752 z3J4D+`lz0G+DdYQkc@$R%@j?=Aq@?U));(CH$n)Sz?8<~UU$K=y$QM??^_c8)!q#AkPLS-Qp|d?19~>; z*#UaQyWlQKx|zJUW3CC;Of<)y#99&t>7GnVh-s0l9Wa@8#liX%-tc52sR)RWv$0V8 z&mjcw8UXb5|FB!f4x+#Dd10@=gPIIZ+HZ}yb7dCNF1j;7CaS%yXd9*{>sF4A)bSTAY zmq?cn6Ij!9_Q7a7SidM0s)N>sZmrq&;vW* zyxDpc%%^dJPK08SgkofNHBwDGhM)+ z2C9yr{jurOBC!AmQ%LC+Sefd=zsv#-+6xVb&N~Ya!FpODD_nlPz*G`-7(cdkgfF_= z7y9y^NBSc^-F<{oNhl}nvSr}^X#pfk7q$`jG2id6$l$IYT~qq1FVFIx4;fbzhaWs% zr847FUSsHFiMp^%%j1M(me3D66GI!Ln1pb7onwn#PPlyp1q2&fz zt%TRQc3acN8UC@hC5EycN;#b_MhTyAo+Aa-W;XG0ai3D8y?6S;tW?!rpFc?a6~I{y zB(qi3)!IH-?Y-FJ?FgjbLIi##;uF~e2~=E*dG2DBD%&o~btJ7EGPOKvNIxnP{d9Tj zbLtt%$e^Ca0DH`#5d-Syken4=e<;xto;?f0>|hujEq8i9Ev=DpF1g0i7j9V(HwMVS z&|t;E1tc_K$T`6M3IJd(hCk}QsDv7FVBhS8p!taT7gPy%XqpCE&CU-pa9zqgAMSmX z0V^XN?w&(Qd+5d_;k)zX^QzwZ{pA75Q|zV2KMu@{ZI==Q6$v2W)a9tZX_vIqmD#)@ zLwKht`dgSv{$9?(pWIa0YvX}m`5euEokrS1N@37K)n@UFs9;2$#Z1(^P;y{)(0`WwzYi9+NQ8Ff>>+bozVd%!$dqaB~Fmpjt&|pBb~S-A9ALh0Sly!NLw$VEsporVHavqnn@H+ z(_c+Jr~5Z$bV=U!tR3(E@Y%Q51hPUOQ`Eg>xu5RF&Aw!B&hOM!<`C11%`o|M^g(i1 z1(LU-n8j3rl(F!{)t*LP?E~odpB8@O2P&a~e-_jKwuf?^HmA^p2_V~LMb(wc5KaRG zWIo13sS{+|d*@^E06+mv`gaBX4FRNjH@A}K?Q&TQIUqKQpC-T&_Q;Cw1(kwQ-}&@= zGe1ajAldVy8#W3WN5VY2EH5)_g zut@H(eM&WH+YO;D<&HTHJh$%dkWV~h@K`zj>E{JmVigf6SfTe_8J+A2nB_@PreA&$955bpuR}A=ZO_RIpjMlsECwt4P>x6;x6C9!s7YGN{2^M zF>rYvQLdlLiaYD!*i}`i z_xTQIJg8`kgdKLmKi(pPYKHBn{`uc#`Qn*i>i`?>?PiAGQ1>y6j~NmcmMg?a7wwgN zHI0+f;cNRJdQ0-hr`e24cZ;($ZJEJ9wxAySs;t{=AP7DF6~npJN9b zq|TtzN0EH%F~h6}IybHCke6lYOObe|6hiaV%8F}qsB@=s zJ?bC#_VfpRuYL1t2H5nm)DitlAat@Go5oGgO@(NZ+w11^)w5y+$D#%ka;u_o=j3SS z?#s)N7m3rVq=W;7&Mf<{#*ZX#_1G0Vg|3_#C63cr8NoL^5&ri@&Iqg!Rym-Tj^hOs z>1tKxcHcC2;AZZm4x3Gtec8Fl5}eymX2EVGMxydz@#jt%Hn(vkYq8u}mtT=IP>H+R z866>u*2xD?D&M>@+H?pa=r~B?JQUEY>tMWfgq``p-DBR~OHxXiR z0;ZQ>80%BqV#uS6elb}W4;Ip9L}k?)k*D$BSR|h`;hjOU2SUAt?i`cN_$Zi$roY{l`=1q*$TSv#cYSGxv7mu z-RQIK!)!f2US;S`7^E50fQcZ}sn%zrz73PtXsop0-*X+HP;7`(M8th|d5zvzh=LQ` zz1`Ekk|)OHcdKWk3#f@}TTaMr>Wo&nRmiPWU}};i8n>u=$LyAkjbEQ0INm!aqMvV& z|L|YbQ<;+UYnmD9!fS;DO1vl1@j8ElHd5yK_;{9*znQ4x0U@pIq8XR?j@o zO(G)DxQs>I)426W8pZC_laNMS5D!!X?ikg_gA`g!TsfjTU)Va^1!yb}HsdD;nV#gb z+Ucb1I41Pa`%pp=QLv_ne$lQsk*&Z#4}lKA`C!J&tTgnRQ+aSfA?U>#p-dPFcx8m{;T@_|To#)hjg`gwLbJ%d0r_2E5_tTQW zbVXUb7JspK<-WMFKMh*bVjf5w07Gk9EFTcN8yp)fq)|t1su`7n9qveBPEo` zHI&ffqKI${IBX~-1EMoY`RLc%Y-jowv7IQTE>{C7`YfUsp zf>kT^RqoFrvb5P?fja;u)ν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Ξ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 diff --git a/client/icons/vcmiclient.32x32.png b/client/icons/vcmiclient.32x32.png index 89819f2980d4c1e591d0d1390d3ddebb41c34fd4..cd849931bdd3ec06d14ab8e2319d084454e05710 100644 GIT binary patch delta 2200 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 a@o%AMmhx&4LVN%K002ovPDHLk0$_q1?LZU& delta 2327 zcmV+y3F!9E5zP{iD+>Tf0000;0Wr>7sF5}(f7(ezK~z|UotJrxT~!&tf9IZe@4NTD zb=pqfOetN4RV22ifRutPkxhzd)M#Q^%9>!%U<@%4ku=djFec(sB}xbw5KyC%uo;{w zTMKQ4X_;=*jafR=_hxy^ynFAv+wqV0W+}9QoaD3_su|E zD5VfWBrm)4(udZqTX$D3nYN~8=> zO5rzr;xNQWNam$2GIk1~3HtgE5UZH-WMv$v&FlfCR3wDhQ7RUlhUcXQc5bCQe29Eo z8@YUbrnb^)>{J%RvM`b+(nz8we{`fK(4<64iJ6e(EFC?Y!7vQ;Y0(YdAGT?PA(dL~ zNNZ2ccmSX_F)=Z27)DA-om#bu=XrRpi|2W`u1mhHjVK7v>vI&hlux&nZa`!ZM-g!p z5zUc6DU77aWVw3icmQ#^QaO^#<(3HD1R(HzO2s0s>o!4Lw+WKZW7(-$f8}Abj^{}9 z0crRFsZ<7lBf}%T#{&o_Cnok5+S|_;hI!0co~qSmj8B8)^LeuQJW&)9&ynZ^5^6ej zDow3c_4{`3ehvs`0E7?%h>K%mo0lzJdZUyQX_!R5H%k|WjF(DHkk*p8p4aq7D%Cmx zA>kaa%=8%{v5_X3N>VJ1e~Ec=6%~lu||z1hvU>+4VfnYM6PnxQr2c z_%cbDXnMs7V60RlSqtcL99+j`pxVGl8b~Q=&*%8u8C{HyP0)8>5KYsNQnKQlFA$B4 z^WsY{iA9SRZD?;96j-1^LehPS|8=iY>e@Q)r8Yu8Rx;CzpyTw8cm%iL5NYy~oCo0h@Z8 z0nJD#)?Kb}8}oSefBF5aN~S5yYr-`G*t}ye6H_kVxHx3tJSbMcNSZ`Zqzd_vTzV#a zH}}Av_Z8j$jrhq8vHjW(nL6*x29LdvK$8-LVqq>|+dhqrkBy**9-_U2QrTp%sEFg3 zh96U|h3p&Bc;F7joxhyq)@x(h^D%t~1yim-0JU1ptJHndfAMA`v!;{^xAje)_4Xb} zXn`>Fh#&~)ey@$?9rMsMjq;RF-3`$-&{c?5I>O)na)|v~4fgR;PSiH=G3kkWvmc{H7Am3#rt6GMNmgoOw1t@z~|lll6U$Q8*Z%7+2-+&*AvuTpD8CGj3R2a+E}F?Qum@` z0gfI$8t-~P5pUfqICW8tKfhv;&E`1o!b?C1Ds>;P5unmpyzBzzb>_MMHkXUe)d4{x zh$v6_>^T^)Zy1znFj@0?y%*|U$dnsUDi#~Q?~hcge?E>k>w)RJz%3LCpFeGp*1Iqp z@WxKDySMkTGlUS@Zyx>?PxmOUTs6*jzgC<1@U1;*`VLDpsnH0U9#F(=-EGi6Ac&NP zib5POB8p;$hKG*=L8Ts0cN3yD_t{7o$FcX`VcQ)nN-YedL+@Ls_{bUhpY z@Cg6hf80hw%tpt_DIL9B!Lm&D59>^n+X!8sYH^6_^z4i@n|Cij6N+N7*sGMP{A)|z zcN$VbFm((-sZ`o0gqVJbnE&bNessmk6-&;!@KTmP`vg&-qf|^B$EY}Ffr^Pz7J)Bm zOioZf8j$vVW>**5aydFrUPaw;MhD+|;dY=je>BAU*58p*N*(i3QA&kMsSt1F| zH5XG@xG)|b9^MEzCo*zEfYusSpMLh)e3+E9)lKi!s#R8BehmvcPx|oeTTrHDa{G@qaK)F` z($&@FZQH*6K49vj5}S_%Xnk0D`k7~bIy5xsgki)b*L|CgC0!qa$mH_eboafif4S^( z3WWmQJw1Ovc<^9P>r|bvh)fJDpBzhGB5He=p-l1_*thj+2*g z(G}Oyx#%Qx!{Egip5OMJ>u*>K6hAJl`=kH@$X$Kq6~F)Lb!)$wPG>aRwy`W5%eJwn zYujSW+i&l_>#pyAeSCa;uTm=he^}k0H1C{-xM$x>D6j&py##MAKkTU*UL0B xo{ydRrvjJ*q5(;ocZs0+#&3MwhC5#QAG)C8m#n-o_y7O^07*qoM6N<$0fMIBc4hzo diff --git a/client/icons/vcmiclient.48x48.png b/client/icons/vcmiclient.48x48.png index 4c6c239af6af520fa83c64b56c7c59352c2c074d..1e59f556d15abdc5278297b08344e5f466262510 100644 GIT binary patch delta 3844 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 DKce3x diff --git a/client/icons/vcmiclient.512x512.png b/client/icons/vcmiclient.512x512.png index 8c0b7e4dfb9db531601697f249bf05bd402b838e..0b8bd58deb97493e47fbd260dd9f0b6c930871d9 100644 GIT binary patch literal 138300 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 zRz919

_l4nQCT)Dqu4>$Tcc_-$oldlg;6ZW(!(bX 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 diff --git a/client/icons/vcmiclient.64x64.png b/client/icons/vcmiclient.64x64.png index 89865281ba8e8ed340483567bcc85d983c7cc867..71ae6ca544537f3aabd3cb5465e7386d7d37ac49 100644 GIT binary patch delta 5551 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 delta 5893 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 b)AauUIjT_6r`FN{00000NkvXXu0mjf{bg16 diff --git a/client/icons/vcmiclient.svg b/client/icons/vcmiclient.svg index 0d1f01b5b..474c07a1d 100644 --- a/client/icons/vcmiclient.svg +++ b/client/icons/vcmiclient.svg @@ -2,19 +2,22 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + gradientTransform="translate(1.5699347,0.84779727)" /> diff --git a/client/icons/vcmiclient.xpm b/client/icons/vcmiclient.xpm deleted file mode 100644 index ce82b0f95..000000000 --- a/client/icons/vcmiclient.xpm +++ /dev/null @@ -1,238 +0,0 @@ -/* XPM */ -static char *x[] = { -/* columns rows colors chars-per-pixel */ -"32 32 200 2 ", -" c gray11", -". c #161616", -"X c #2C0E00", -"o c #2D1003", -"O c #25140C", -"+ c #28130A", -"@ c #311305", -"# c #331609", -"$ c #34190C", -"% c #391C0D", -"& c #241610", -"* c #241A16", -"= c #251E1A", -"- c #2C1C14", -"; c #391E11", -": c #2A241D", -"> c #20201F", -", c #3D2112", -"< c #38231B", -"1 c #3C351E", -"2 c #242323", -"3 c #2C2B2B", -"4 c #282625", -"5 c #332E20", -"6 c #362D29", -"7 c #302620", -"8 c #363122", -"9 c #3C3521", -"0 c #3E352C", -"q c #36302D", -"w c #353433", -"e c #3D3B3B", -"r c #3B3431", -"t c #412516", -"y c #462A1B", -"u c #492E1E", -"i c #45271B", -"p c #443A1B", -"a c #4A3F1C", -"s c #4A2F20", -"d c #4D3323", -"f c #423B24", -"g c #48372E", -"h c #483F25", -"j c #503627", -"k c #513728", -"l c #543B2C", -"z c #593D2F", -"x c #483D37", -"c c #473C37", -"v c #573E30", -"b c #583F31", -"n c #5A4C1D", -"m c #63531C", -"M c #6F5C18", -"N c #725D13", -"B c #7D6515", -"V c #4E4426", -"C c #514623", -"Z c #44423F", -"A c #5C4234", -"S c #5A4438", -"D c #625220", -"F c #6C5A24", -"G c #6B5A29", -"H c #715E26", -"J c #674D3F", -"K c #604537", -"L c #7A6525", -"P c #746129", -"I c #444242", -"U c #494645", -"Y c #4C4B4A", -"T c #4A4845", -"R c #554A46", -"E c #5B4D45", -"W c #514742", -"Q c #555352", -"! c #595757", -"~ c #5D5B5A", -"^ c #694E40", -"/ c #6B5549", -"( c #775D4F", -") c #705648", -"_ c #6B5F58", -"` c #755E52", -"' c #62605E", -"] c #7E6557", -"[ c #7A665A", -"{ c #666564", -"} c #696766", -"| c #6D6B6A", -" . c #6A6865", -".. c #7B6B62", -"X. c #726D69", -"o. c #7A7674", -"O. c #7D7B7B", -"+. c #767471", -"@. c #95750A", -"#. c #98770A", -"$. c #846B1B", -"%. c #876D17", -"&. c #8E731E", -"*. c #93761D", -"=. c #96781C", -"-. c #9B7B10", -";. c #A07F0F", -":. c #A07D10", -">. c #836C22", -",. c #866F29", -"<. c #8C7324", -"1. c #997D22", -"2. c #967A28", -"3. c #826A5D", -"4. c #80675A", -"5. c #856D60", -"6. c #897164", -"7. c #8C7569", -"8. c #8F786C", -"9. c #927C6F", -"0. c #827D79", -"q. c #937E72", -"w. c #A7840F", -"e. c #AE8A16", -"r. c #A5831A", -"t. c #B08C1C", -"y. c #B79015", -"u. c #BD961A", -"i. c #A88825", -"p. c #B08E23", -"a. c #BB9624", -"s. c #BB9626", -"d. c #C69D1B", -"f. c #C29B22", -"g. c #C29C2A", -"h. c #CCA220", -"j. c #CDA42A", -"k. c #D9AD26", -"l. c #D2A726", -"z. c #E6B726", -"x. c #86827D", -"c. c #8A847F", -"v. c #978175", -"b. c #94867D", -"n. c #9B867A", -"m. c #9E897D", -"M. c #998377", -"N. c #848383", -"B. c #8A8680", -"V. c #8F8B86", -"C. c #9B8D85", -"Z. c #938E89", -"A. c #918984", -"S. c #96928E", -"D. c #9A9693", -"F. c #9C9996", -"G. c #9D9B99", -"H. c #939292", -"J. c #A28E82", -"K. c #A69287", -"L. c #A5948A", -"P. c #AC988D", -"I. c #A9958A", -"U. c #A69C96", -"Y. c #AD9C93", -"T. c #A29D99", -"R. c #A89F99", -"E. c #B19E93", -"W. c #A5A29F", -"Q. c #B3A196", -"!. c #B8A69C", -"~. c #B9A69C", -"^. c #A4A3A2", -"/. c #A9A6A3", -"(. c #ADABA9", -"). c #ABA8A5", -"_. c #BCABA1", -"`. c #B2AEAC", -"'. c #B5A9A3", -"]. c #B3B2AF", -"[. c #B3B2B2", -"{. c #B9B5B3", -"}. c #BAB9B9", -"|. c #BBB9B6", -" X c #C0AFA5", -".X c #C4B3AA", -"XX c #C8B8AE", -"oX c #C1BFBE", -"OX c #CDBDB4", -"+X c #CCC9C7", -"@X c #CFCCCB", -"#X c #C5C3C1", -"$X c #D3D1CF", -"%X c #D6D3D2", -"&X c #DDDBDA", -"*X c #EDECEB", -"=X c #E4E3E3", -"-X c #F3F3F3", -";X c #F9F9F9", -":X c None", -/* pixels */ -":X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X", -":X:X:XO.V.| Q :X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:XZ Q ' T :X:X", -":X:XI G.(.}.}.}.(.^.H.N.O.| } ~ ! ' { { } X.o.0.B.x.x.x.x.x.:X:X", -":X:X3 N.H.^.[.}.}.}.}.}.}.}.}.[.`.(./.^.T.T.D.S.Z.V.B.x.x.o.:X:X", -":X:X:X| O. .^ ..b.D./.[.{.}.}.}.{.[.`.(.^.F.S.D.W.'.'.c.c.' :X:X", -":X:X:X! { c % s b ^ ( 5.7.q.b.C.C.L.L.I.Y.E.~. X XXXOXZ.V.~ :X:X", -":X:X:XU ~ c X @ t j K ) [ 3.6.7.q.b.n.J.L.P.E.E.~. XXXZ.D.Y :X:X", -":X:X:Xw Q Z X X X % y v ^ ( ] 5.6.8.q.M.C.J.I.E.Q.~.~.F.F.:X:X:X", -":X:X:X4 Y U X X X X @ t d A ) ] 3.6.7.9.q.n.J.K.P.E.W.W.D.:X:X:X", -":X:X:X:XI U @ X X X X X % i z ^ ( 4.3.6.8.q.M.n.m.I.U.(.x.:X:X:X", -":X . w 3 * + X + O & & & - d K g x Z ^ 7...U E R N.N.e :X:X", -":XG j.9 2 ,.s.* + = s.j.j.j.P * i F j.j.0 [ R i.l.i.' Y g.2.:X:X", -":XH z.f 2 2.k.* * p.f.,.,.2.l.G X L z.z.8 x q p.z.s.{ Y k.i.:X:X", -":Xh i.f.>.d.L : = h.1.O X : L V X H k.p.a.4 <.f.a.p.{ e h.1.:X:X", -":X:XF d.<.d.w k = d.&.+ X X X X o F d.B d.2 <.t.<.r.{ w u.*.:X:X", -":X:XF y.$.y.I ' : e.$.- # & = * X D y.V =.e.e.D %.=.{ 2 e.$.:X:X", -":X:X9 %.w.n Q G.: -.B : : 1 w.n X n w.: M w.-.2 B %.Y > -.B :X:X", -":X:X:Xm #. -X-X6 a @.@.@.#.M 5 o n @.: 1 a a = N B w @.N :X:X", -":X:X:X8 a #X;XC.7 p p p p 8 , # 8 p * X X X + 1 p p 9 :X:X", -":X:X:X:X:X:XO.*X@Xk d h u u u y t % # o X X X < Q Y :X:X:X:X:X:X", -":X:X:X:X:X:X:X&X*X[ d j j d s u u t , % @ o X r Y 3 :X:X:X:X:X:X", -":X:X:X:X:X:X:XF.&XW.z k k j j s d u y , , # - c I :X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X$X%X/ z l k k d d d u u t , 6 w 3 :X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:XF.@XT.v l l l k d d d d u S .Y :X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:XI |.#X..A z l l k j j d v F.F.! :X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:XO.{.]./ A z l z l j z {.=X}.:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:XF.].).` A A z l S `.-X;XO.:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:Xe G.).].V.....b.$X=X*X^.:X:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:X:XT D.W.(.{.}.+X%X&X/.:X:X:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:X:X:Xe x.T./.].|.}.O.:X:X:X:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:X:X:X:X:XY | o. .:X:X:X:X:X:X:X:X:X:X:X:X:X:X", -":X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X:X" -}; diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index aee9354ef91e09371b9b8cf4927e13c5cfe89bda..d575b4b1186b831541ec66d62106175179380f5c 100644 GIT binary patch delta 870 zcmV-s1DX7{3jGFg!@4AFJ{ju0Xp9v)Wtd|s`uub(vq3L)@3kCv7e+S}VH6bb-L zO-&I+3Eh8v%`1QJaP;5@9LK?RT{4->b2l`f-JG7D5(ELZZ8JVTP8`PwAufZ-6gP=5 zO_R2h@5u%V^SvobrBd^hOQjM@DWsHyVMv;$D5U_<(0@=UL=vK8ifQNwA*fcX&4Cve z7f30o)oRr1b;2-2N{Oav7@AI!KoTot3R21!U@xQ`$1zEgaD04BE|}F2R0fFip3(n?_=9G07;VII1Z_j=(#*Wal(@zAc`V1O~bM* z(ln(|DA3*A&A`CG$4!9%y}iBlo}M0ld7R)!A%9XT&V8Raj?pyjX(M}{hiRHDFE9Ug zEpUE*zSZ+Q-Wgq>7Sw5kf+Ut$mWAUun5N10_BKKYCMG7{|G)mqA@A$!yVu#-$)DD{ zTtoq=_>2u4Om?%xVHG&J;CHk;iZ&gXe#-lZ-AT3cJ$-`~gg zeP4Xv7jtuS*{ihLf1`Dkt*x#70rdR(@Bw{qzJ*~J)M_t&(F`_ w`1k#y>FH_bTET6;^@W9n&u3<4zPXL>KM)Y%?u5@l2Db{3B!5t4SV?A0O#mtY000O80s#^L0{{R30RRC20EcM+1ONa40RR91 zCIA2c000A^0RRI4000310RRA?0ssU6000310054l0{{d7000310052v0001_M{J$| z00g2*L_t(|oQ;%!Ow@H4$KTKA^ZkDBjypIyJbtFAAcCO7pMNwI|EU%uQ#aAsU%BQ# zZP{GS)yxfStz6~SnydM%Tp2U4Ic;h=)?hMD!4ic70VjCS3CID*9e4Nr`RN~YO3ixy zdY)&`^WJ;U_PhxH5k&M3SHiKw1 ziuDr{+Rp9UrgT%EHgrQXOcTN~Wy`W$08#)r86z+P6j{c|$mmr7gVLIDF) zwzif7Fa`$)`k<@jZNFbNZna#c+`I*%XhkXJrDq7vIb=nF&2EFuW{1OOC%eN=Np>4e zY8H>G##pD%Hvj;vsi_H~1}q$&yshdc-1CyaCBkqZKy59}G|<<>bWLTJX)se)nWm~N zkw{P?9)H7BcuFwY21Swj?@mli3n55KPfr(g5LQ`Pyxs255YZIwKn!sWm>vpa_^%ce zEFD5gX(^PXOn9EN;Be@O#$1qPE*3BH;_d^r833A^nm7T#r7sI=8vmf7!3p>KOI(41 zx^rWq`c*HLEN4hW7*5rVBE{{({=J!K^%p_Me1EK514yQ!g*M=Lc+<@A^3X}SiCqDrWr@JcNBW82lofMknIUT*X}dl zH6I)Zl;r2$osY6{Dl$jWJb9ipC`g09b7;@O49XbTpiNxTi1j z?z%P0`zp7-fsWgnm655yv=qc*G7Qs(JAb_i^mK(#_v5&TCOB)qBWVxE9-at{xsL=R z!59EM@u&=dg~rA!pDkNjuh-*63}%6;mI&Gbq0wU-IV9&F>GpSQ zeDassqok&$LaM0&J9_lUsgB#7#iRq_djZcWS*6Pkr^#&&tFUaNR{qLs@`+>L z{IY4&meck1^_+;zf4$)oqE>xKAomrtKL`vriR+1+R3dmXOmT>SqJ aBlsJd^(S008Df$E00008Z; zw8+OULD=2dnc4X`bIy6s`@X#x=Afp7ENs)0oU57hJiqgQKF^CWhPk>+rP7u?d-e>> zrA>=Dx-MS4xO!}C?C+&g>4V|n;rv|LofE)vx%{2;=g*&9vSbO3Mk5=B;e|&Yd8BVH zY~L_owOW1h=zr0ppZ53ni*{I)QuCrHnj9G!+4O${41!?$*x1;Gg9i`(VcxuXXsuC7 zVObVJ2wc~7j4}V*xpU{=ZY1O#0jt&OttU^O^qzkDX@B3oea?jo7gmO0xH}BP*FOC4 zLw)%0;X}1rZSms8i+SC9sqNgk^Vsn4@b3HX zzklA>eGLx}+nqRsG3LMiFg`y1?>LTcZ8n<#gkeY!1lYEX)|!P27owCRiX!^@`q;mJ zKSBuVhhL!Dzm93qLy{!;zE3urC6mceE|;m->o|^s5F)(?1_nNU?z!jI%}PO~lwBwk zh@yz4OMjQLV#Nvu1_pTd-FJEU<(CmcuxQaDCMG6$_St7yzI-{mckf1E=)UwGw^l!9 zaUqXoS%hJT>$+qz8R9rb2!ZE$ELgCBMT-_OG&I!RiNopi1HkC$C~v?0Hp`bUPmMnL ztVWY4aK6M*=*u@ z9!Zkmc^(TFE@X6c^r}L21e8)TI5^1A&=5zC9ARv1j8mshA*E!^nl%g#4zhLYR`U5g zLI{j82q8$cLMwxD48j0$g~Fxx=rIX_SD?<_kWvX$Bwaf?OK#l9Ywk-U>Jrt zj(@}JufM_fzkfH|w{NGbs|(w<5kjDpLMcU(B##-lrv!5BjrhRn>&%pMS!cz^4yw{8(aVB2=8mJkB1H9!hyjKQ+3%XqCz z2rycsEz;*O1|h(;9k4y5ltfWPyJT-U|6Z7j>eaaxam zSwY9Lv4m*B8H3TZ;%yt0ZiQVKfsC%aY^~N&z|MfVT#nPHPxHwqpWwPKK@d=@)o>gK z-}kXBi)=Ovw8CZE_LXpB&_*M$(0>|?WiSd7rAd^=wGGuUAfL}OfBt+XCnq}scFaK` zL~PqOTeoh-bzPz;B8np5O47FrM{AuHj&HRD6QZT4HikqiB9$PurYQ|UthrdJQms~* zoSeMAA{ieapDY%OG@DJb*(^a2V2q*BXr#R_NfM-#X>I~Y-O59Qz!-r6lz%p84T&;n z1yUJCzbK}8+h{a80(J~>tyUW^l}b!aO_9&%nVy~|iXwy%Y5ui)q-9xYc$G020|FZ% zEQHV))q=$+v;j*C#xH!)Qdn!Gl+{@SN-58@6?9#fzx?huln*QNT|KytkJgaS=CFi~ zZ@Uz-Ilh&h$It)#9+^xgt$*+&Nzyd1ZJT<%j@FvzpMRdg!9h-+K24G&=R0w946*CF zpB0NmUU}seN~IEZ&jNx-(+KNG8RL0AVH_iqq_wPS1A??++xC?euf1rr^R`$lQm@x( zHk%k@ux@sC=~F0 zpJl7oQVa}@xPep(ty=TdvII>T5hgK?B3<#JlTej>vckUe7Y=4$@>%K>B+1*UkW29D8!+;C!-D_FH^)x?7jKKRRPaeU23ADcICKDc4ShF`ar%$*}6 z)N<>YYy?QHh?4}VVm!~o7?Z*sJ$jTViiqR5{lTSHuU>uIwfdda+06Fs+ke&H-~T72 z6jDk?_J918YR?8PRvRd-(W*7#+7siWk3OPStD%&_^E_r|X6ie3>{xi$U3X3YPXGZP zee}_vxvu+3QB6-}i~*n0t3TMF0ASIe#(Hnk4VP|2`AbQ)IJQy1TnE#xO82 zuvUDk)=cPGUsZ{9i@87U%*RBt) z$Io>GwjpO`W==O6jipf(;rl*)eSM6LjnUQBMR#|%>FMbi8W|b+_0$l!Q3dAUQa#s)C*J7Q#~*J^`#%rmCgSE1sCYK(&A2d00000NkvXXu0mjf-kj!0 delta 3598 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 UOHQgZ00000Ne4wvM6N<$g8a1EC;$Ke diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6c58aa09fba28e9a36e747111fc1c5a1d0320681..3071027d3a8307d59c584b3a6c4f336ca5ab4c68 100644 GIT binary patch literal 4722 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≥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 M07*qoM6N<$f|)l{;Q#;t diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 045a0bc8983aeda71803e5a43ca116481448c18c..bb0ee71e7a4ed021a38c5e8c258fae467e431788 100644 GIT binary patch delta 1955 zcmV;U2VD4u60r}EB!3BTNLh0L01aUP01aUQJS1W+0000PbVXQnQ*UN;cVTj60C#tH zE@^ISb7Ns}WiD@WXPfRk8UO$XK1oDDR7i=nmV0c~`CrKhI{df}Y5w579P zt&SBo#45I^6YExBB-V0SLc9=(%NBOYF4?j$GjW-jOWYt!Hh&;vD+|yqE}(!8ZGv)} zlZ%CR)D5}y(5r3F>F@mR@4G)Nh@!5r?0NGhzr4xwe17lq=i+A6M|8+sz+S;_PuC7rjWu*`T*LAbF*q`X?b#@P@$Ci&_Pd6&l90g=vS6y1+v*}u)n__ z!!W3-s=~Hy4jec@B9Y+GUw_ZVF~x}K5eNi`$K!aOM>d;Pz#B@bl%{F1NF=g;$BrF) zKlP@jX;!#Qy#J@qavzrlux}&cDoUZa49G9LI4e zD=QC&Z0DREsFr4+93;rbFOB_a?&xx?i59VG6;NZKLN@Cd!Vy+otYYoOPp zcW`i!?(S}c5JaO&(natqj z*q5QAq@-j{d3mxmHz%TX|Lek*sZ&c9PAf0DBcO$8TDX{0D#h`mhY>=cltd|oZF?-4 zKZl*adzQgiCE<{cJ>1XC=rBrZoN}fhCMQT67KZ8lG$$-tOv_1~?mqKHrIga8l*K>% z&VPy-_chECx8DBP?CR;IHJdkli|?=9OjlPIM~)ofkH3DLi#-EK-zOT;c=Om9UU}m! zEOfs1=mPGXRl#qz9^m5Zdq_J0nwHeD@a{@>zwvL@Z$FxNa!GCVffJXep6)&)0ChRN zO49^$=hQ5oSyP_png+`E=;-L+v6kCWu7Ab%*1f=iJ<{>k}^12~Su zEuSgm<=2i=TwKh`l`H8V%5d<_cSvWlC=|ZyvVHe)lxuO%?N#K)a=^1V)|tX{9e)5k z+ZwO;2hjG`?fZW5PWM^gcWvdG276yU%G|paFmNe_Yi8Nfc96QN$#lK?Cu(o1q^}RA z=9f_Qg?sUAlOr7|GU*J3u>jL63UJLa-t8H|e}7yWzUv_UtDt>-eOW2xInOplX;E%K z_zu$dXm4*PJ!arqCe07j@WQS=bbsvL%s)C$F*!HQxxTZMefBOq#|EYN<@QeIPLHzk zo&>FbJ4esJ7_MXE`+mRYI&jsv4FE_f)0?-wE+^*%)Zw?f#i65TsH>}^w5$x*GH95U zB<%=}^mr8Jg=mtp*pE)x8`EQ>+-%n{p^}bLjbbonyIjf)A$(Ely$d8|xhmt-KUGd7{a~$p%=D`^n zo@1k=q-^R`HoW|IeCd%I^0}p00|bCPKje}&$EOP8uZ$2vl$Mr0T3(X>1IzJb?d&@i z=jG*{E+{Bq$JRekoUfy4I)5mM7SM29$s7U^^4)4A99<8md zoP7Hznvm&>y~A}%shL8EbSjnFdj&e)HsbQHDW&APb?c0HJRa@s?d9Z)&rq44OI|2W zSPzjG$>HX^mryuw3DIbjU@!=PWm#<6w23KGrf~A)Nd^W6g6-|?AAj`9)ioUlDW#ND zN=bm`=H?lOVNg<1!g;HLjBVmd52+-clw|EJx~>z6LN zKsudf&6+jyKPmR7%72v=D^`5Ywr!=9V%gV!&e@?XhGQ{mW-)z}a5#*nX^f4Hv1`{Z zgb-9!Rk3K%qH7-R2}1!~*VP@zAr^}<@2gMq&R_;#N^}uqT%7UJOD_?N#n5$~`uh5Z z8yg$<{ZG)fYuCz(ii*O~(NTiIAPXLQmSd?QOxp!u_uf53B7YGK!yu7JtgWxF|MP!~ zd5tjpB-^-gV~t@Loxxxb-}kxL)j?U!OmcE^&~=?~IQ;zb<;%Z0kysPfy{lHO>dMW{ zz11{LOw;7f2fj=ulff_yB9X{569rxu^eS-jvhb2p5{X3aU$$)7kFF=qgwuPqtzNx4 p7!HS>p`oFItsGmo4qvbTe*i*e6*z! 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 literal 10581 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 diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index e5583f4b795b7be4509784cc5d1b9ef4976b11fd..ae4a6118764d915ebbf6e2263523c3296384b05c 100644 GIT binary patch delta 3165 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 D;inR> delta 3598 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 UOHQgZ00000Ne4wvM6N<$f>a0D8~^|S diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 244512b032ee61a19f785438dec2d04c5bd7aab1..220b233b8c9e7d0ba0711a917316419ca3249912 100644 GIT binary patch literal 8437 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 Sz74PfkdmDGyJ{J;u>S$yq>5Mo literal 9270 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 U^Xr27ze_u6N?MB53YHQ7113o8Gynhq diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index ca7b489be0646c32b05936199b16cbef80997c01..1a407db91c34eea39912858b04a76c647d644912 100644 GIT binary patch literal 15254 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 literal 17311 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 diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..caaf8bc5cab5018f15419db24372c80a9fa55540 GIT binary patch literal 5738 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 Lg{~?_#WwQ)S3f~L literal 0 HcmV?d00001 diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index ca7b489be0646c32b05936199b16cbef80997c01..1a407db91c34eea39912858b04a76c647d644912 100644 GIT binary patch literal 15254 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 literal 17311 zcmYIvbx<7L6K!yZ;I6^lU4l!36J&9BcmHsA4Xz=`;u73FEWWry7GK=u@q2%~S5q^$ zt7fWh&Arv#r%#_7qoyK{fl7k<>C-0+MFkm+kJ0CUH!|YKQ^cHu`SBUlet)pEF)!we==z@|Ju>3wc$4JjREKXgmdUL8{g#TE zwprfxmOmU9omSA@#%nijtCU#c2=EnF0Bf)^sAi4dVQFEK$KbJ}ML$KR`T6+?cLzYl zOy30|Jwal5Fjj~utO0%b-p@VW^N*mc!H}gLPakrlsQ)+6A^FCDI3!+C3hM?}`oHnl zTF05de8#4f7~JR{^*0U*-W|+egnP>l%d^;mGm_ztw|@~`$?g#?O5c4tsjGuo3qjlE zf1Yv<;!#&pcf!R2&=@M8_Ud9@mBOb%MJdAE0%R;itv3BV$LX?~L)=5e`czD+$fkL^ zk5)tvc`x|4C$2#v&d)-f-p*ja|AxovyTs!j-m*$LZjm zItV1`73E@orK_(pm@)L;2u|YOISAq{ejC^O5T}#aNWEesqmCzdGE<3&C7bDg!s@F& zb8;8^R(Lv_x9e4fbd-tN~!gh`0 z2*gXvC|qn}PfVCO+*T$NmU@KjTJ7pvTE$iWyHlayX#~R4^&ayzu%mwKUvO@M7oW0z z3jk18G$hM8`jcz`=z-!$6R#Qx=pAlG0sihrRJ z7csrYU0WF_tEm|Q!fJKhA8av&070;8YiD-`O)KkKKy7_srFP2?Xa4DpZhxEd7PLNZLRmNav~>>h={*rxAuJZ+o$q$#&URAEZB?9-MmqMLW;lb^(z-l zyO&1rGZdu)(~hgH?$2OHMF6!j?s1Fopu^Ddm0d$uV^>F4Be$-R5e1pJ&!4TM+wZ|M zr2C}~f@Fz)zUnuA8|MIZy!;lY_di^|1(tf4g1}-%11^#TrJ+Jpt)`V4d0*tBszp0G zMJ>zu^>wd^sy09Zak&<7#X-3urgn++764pTu(nzqUq3Z4x3cO0JGJRgSv7Uo&VTP* zvEe4cB(fyd<2U0an?0$~ScWxc-PDV3*$(unzdE9|Gf|4uDE~(mRJ7Z}g#a;j0X=$e zxI@7T9vm15@?N^tODCEsI=(_EafGur$^j4m*+4~&kl_8HE@1QSVf|?rK{HMwAA9o6 zc9)W9tvTNO+_h%Q(L_hBIn)-rg!TQKjzE39i1Dj+63yj)7YoOfXVc6z(dkzgK}MN$ z8*DjQ)2OhwuS{%$y{32uWMb+xXWC6W-GtD=I524fueuTm(5eKXP}{zepxCHdTZW+u zP)Xb3hXFw^-H8^R9+xBbO>sZ$!wS&Gqp%;AKWCy!i>&zsu5n!Rq)*3ThGAF)aj?^#2J$xkl>2M}B#OJKb%V>QW`Rz-BOjscMmVO708A(jE-6{DN@QH3&`V zv&65+%jAxtHKy|2{cG3%@Hd67O8WG_Il(P@3X|8g5ZMQqlh^fz;e$6^aLF zX~IIc_YVDxF_)jHq7)kv*RR&8RFqKD1zl;636&?G^ROUV0GTR7pG|}Gc(k=uu!74xocsi z$9=TbsSOf0ppf&6)>bNqmR2!T1*duL!!FmLR!sd*3(D|&Hlmk7CM_CCoe{w&B9?Vo zC~A)Ghe%hWWRGyt_Jz|f3ce5Fik|2>HoqNOs5Ps^lIjq`uv|j>1L9|Dns*tc^gcz^ zZM+?_pUaHp3%bqe27)nu^1iqnp+kgdX$xs~Qv(wdRc)=R^&v~n9;kv}Q13QHu!TGV zY+Z$>-GuY`Vrdu@HJCHfc@y+gleMdBYJsjM?@b;{+O8mV;9_?E#^1lEtY=5WRT-#? zC6zVT3PYC6X??pRrW5zDCF4 z?Muq+xVXk6q1 zrOTI{ERN^<=RBNrcR_V=MI1fR9*@6*ZOF^FNWLha(G^l@DFnf1cRC##m@F(T+$0IV zwW_q3Gq^(STbPHBNwywGmV92$olP6D0ZhkJ^z%*nHdCP4yqsTRJfjQiRwSA{3ps2g z2?X310uReWO}RN|FYufYAS9l|-m-HdDmkKqHvwpV0kLc*dDM--hzi@WBLb76{VIyg8e zSEzqMQ_KH{aI z{4p6wze;=zKX34WxMwUmAUghumjt6(f%pgHF+dQ&!Z6FiZzW)DAa|cl9>~u9(A4_MmWFQ=%xTX#(9O&v&C_LSGjVEzC`4h=dNC<66~{e9ABp+M(&i-E-kL>K=kot`z2H*&7uxzx1kSYnF%;H^r6*5c;xEu3mn&4Z||X7y}_JB=qHQE&EsfVvYa5(8oK$| zzj!xKVkHq0;#+#({*i%o9Zb3CM~S3OQBlpuu0tc4I7en|_$z>g+TMzn6g8gFuuw2QEq26j6B0iTMwGYWBI2k($l!9i)i0cgq*|rRQ2dj5 z-i*hVVIyZwQS=)XxHjvk?Na$9OxgWy6HX$2J3dtVu11_1eW2Q=y3Z%cA5{-#L%M@v~9`|X-6zTVqK4=TY95Iygf zR-08naGjOL$73QT`$~dp|KVxPIc%t!Md_G1J-F#tJ*nE3zP^>c9UV{bGZGtLSASA1 z!!ax3Al>L_Yis{n(}ciPF|sO&3^B<_?n<77muWLVfz+`Hxa^I8X{KGlX24;+FfLP; zSlBeu?^cXgqH6BINEO~qG*GBcKL(>ph+sOx3?n&tnG&@Vc}mrd9Mg}dPuMW#tTY7b zq1eiq`f4&tX}2p`2N-&pm?g-b?|2(y9F zu7%lGAn6`WiHhKIv*`m9jy0Zg;$B{7;q=2y?j(RQ$wfU5*Eb*rl`_mDM6`{C+cn5= zmGj4FMw3LiQ~7;W2@QB7Cf`*|-L4c?d`l%u7ipENi$Z-%WkTF$e^N!e`RHimqG=HY zPJ-%0*tHhaBohoPH$!OZbuUfj5alCcoz&#BD+#ReX|UCeb~2=WVNm#WqQ9Yl(Mm#N zAm8=?2<$kl|J6f*=DxtezkjtZHkrDqDOnV8u4ugTL94DNM;`b!gkjqe758GVr=ClO z*0N0T<6T}#c~^{dHmW8+7oTinP$6W#nWm0Rxo8e4nt1e$C#jBv3%M8{cvx8R5UgV>;?5TXENd z6DF|(*p`0HDfu?E!W>#hL(xz+8bNGrVMG2Y2NEQaku-JRIvER2l9k045Y~U@)}eJg zvi0|yP5$@i2hvfPY!;;{xKiN%_nQ4^zghZ@7#hJN{;4K{=;u~k;;P;kBT=F$$ zfi_{ctn7jk$l@*rYN_e1Ar`|48(9-kF2)K)aOwmxv%K<0%_)8VzIf{yoVy$d5k`*x zo>;*~R$@RI+?l!Q=!PKcad41^!U+0YY%9osg}cwIw5x3MagiMWGN!xezy-VHGLyLZ zp4KF#>?9Qo^U!gV*x!zXzv{sY zoz%$J`>6YniUN$7(lNMszhjbmV8CEUxx2!$z_2jN-UL;0w#y*+gGXaH33@K){ToHC zweXq&%2%S&v_r8l>F^@$P2eMTc5-7{i^e?uJT3${C8mS_q**2>(LO5PElVH*q(llA zQ5sIM1+xQI7RIl1{6@;8RA*v2`PBq9j7xSomMqOW#FG#qK`zX$CD!I5C8z8~6`qbd zlNq~M37+tt{Hq5`Y@6hDB6ECP0C?{bFrtuRW5$HBX*?4@^Et9P20937g z#la>p-#s2q?7oDHfiC)0yi_&u1kJ-3{^lH#F^$F1*sS=fq*yNGDt`be#T>+Abtm#! zko;`Tt}EpfW#8)845b|<9n&&K>oY+qEsf^U5S*^-q0jiySWBNrQdppTzcmgK;XAU~ zk_iza!u+~1)yvX@*rCOM>c&5RmIIH+D+&=1KgtbC6@KwRB33pwG0XTP27vi@m$sh1zNMpOM9bn2#8((Fao+vT z%EWel4~V37vf)ydYR-Ca;1qXsrg_; z_aRw?0U^~KW{(yMtLC_0V{ul?CktevCMQPq4M+~GrS7pbHmi$v2v9AxP+GtK^w>lU z(CKgE>=@i712mLMz8r6~O z{P>L;W#92N0I24=g00IiiGfe^g&;w=M>GzVaadhLBR!!;#}n|k?t5M9krlLlTXS{A zSXw4=4$ywP3R)Lhu?WOKfhq~$Wm@9EPEPA9SGrOHs7|95E(3jDzFqERq~2sL2~)PW zJT>wF$%^m`SJO7?lj`v>*PGLh`J&kULF3meG6+>*bXV?n_bDf`AYN2r5hqmN!%AaN z#wAXS{MQZ%a`~j2pkAkg*FTC$F5U%wxto11!=QJP6B*S5}oeI&U`b zT!BZ5LBVsNX}s<#n*A$HBUv81gj0n;%WQ-#RI(oFjl96rAI_F9IQxMflKa637yw?> zq#x9sr3*#fOI%#kPeb20vKuTS%K6r$7!*x5eA4d(p@BKS=8in#qA2_xT(>V)(vS1l zLMKJYjrH_a>SVZqWjO}~z+0J-FnRlkvRm$=Yn=IuA-Hy`Tu0(&MzF-SjZ%hw_iUL_ zU0NB}DPgyUBw?dEOx$Ws!l^GAC!?Xe_@QHMnH$QRElfZ(jk;f17e6P0>7jrPihOh#rWfCIQ74Y?|Mn*(fw|aPcz*qMS2DpJB%~rrTf=_KU6R z`=Mt4)S*E<4O3U!-fxW~-b*FfytVC_XmPtdcueY0@BR&9Mk6(57YdG8@B&_j67t`4 zVp?zn?0;Xbudn-dMk#|uA?PbST|V%g{U-InK|ycZAN*`#@XcYDj#5Hgju=G^ogxdK zFih|h`y?U4rY4>cID>U9obiXcjI|ido7Ufa$CBgJ%Pk?iFzbTNhr@7^@EKz` zX4ngYE2^>rmdy!p#feQc-B&xS|1qjsbCzu8BrX!i(gOdx*0r96kK5@B2}9E3zz@{> z%!<24`$w1K<+Q;ZZS>Bl3kms$`$>$Aq#Sepwr3~{>OL>RrG?ze&#u}1gZ!3#u8Ix~ zq8g_9h!_=YT!+K1q5|uWa>1@exWrtpG`2qgw~AZlc`VWsSv35stIKR`yy4_$U3(^u ze6&kzw*I>x;>$Z3%~IZ+W`0X5$BO`k9Z8po5RBTH#TGvI!;2L2K6Oamcel~`2#B=H zUp{0fOR0u5nuVeL@a8Ep-4iUt$-$l9Iv=OIr={&(alI)1EQ`}Jh>OjILdV^W zge!m6?1>hGMLv~sC1vhi!6PZRrZBFjl8KLwDAv&;yyzDO=vGNBGxz76MX{}#6-POe zFbnggB}q?!iTqrAr9S@G5F7V)ax~V}v|Is4Ifqs&E&QCy9J0V`Tdu@KRMd7A0A<+d?` zHnX@-QAZfXG==bpi%n@;)sS2AsA9>_Tql>eTF2jZ8I^Yn%l7|D#XhLvu{2fjDQPUR z9XT<9CA-rHy(nV*;qBs*Ly0Lrwi~noChUM&Swgz!yR)?JPWIQR-dC2Tx5>EaGtB31 z%U;{_l6{VMFS_^FcPK~jihuCoAmy8j*SxZTSVv>y>RPKK3rH`rG*rMafV%fF4#IUv zE@%5quqGo z_E*sN#jizH=28!{M0ri3-OGFLuL}&z3mv5PI3skQjTCT30l_TBUFHeH8M0oEE5sQb zC4;n?C4aV;ZNc{IPZwXhZevd#`t*@*I^-2S65L`Dk<{|nC<_jXD+>OlR&Pysbjq6k zU9NFhIC)9ZcRo;F^S0B39P$F2X$bV zYtr~E)k@u+mCianRbq8La1;Pq_M@@g}8%LPY@b#n7COScsocyJao%9kklbrh!}NBLOP~rXsvK^Bq;LVAqX%z`hJY&w_t;UFwFGLsxG~YuNCs!lEZkY*ugli& zznY?B77Nrj%McNdh9CY)MeZ&(Pd`AH*JEWtsT607(4}v%}A%$&|LhSG95{ z4zu5~X7wKBcO5n6lR;VC&cQH2F4@ZZo5)9A7m*O1DZB^tD~Qj@L5}hB_Op%lF`1#) zx;M0~a@FXM^EjVW8M`(YBkCTP9Q|i9ZmN9E?5?8T)`_C63IG~?hCBZsksl_rmY)Y# zk{Ren%)d9M(HCek#Ig$PgVKJXR9D;6#?W)ohcn{JnmS(i5i`bDa8iGvYqb|F{WNR4 z(mx`VDV>RCVIoSabOh+k#A!b1+JaI}#F>?+mgP!mhmYR%Q*1uoZm8^`)70}6jKP0c@7Jm|c0S-&mcD{AtAngxkNTQocnh2H-aIzqyh zrmycH#hqj@kRc)Wh5d$s8D@} zh+tr~FU^LEd>bLJjJ7ho5eMa#zdQiH4R*z}@kQ|0ObrIdwtv#OwdD@uE$W_9AEg|J z4F(doVEvyg)-BbxUMZkky1F(B6M4la>TL5t(eD8uuLx(wXo%4v10$!le2y}p*|`NBR6L&U zOJUyY+La5`bq=+B#}0)XBU&s&(QVG+NZ8q{0YWiBncvYDV}EhtkY5obNuv-|5L+<~ zqDSh5auvua?;X$7)MUlvp(_fjlTDN9LqNqX!`uR^)0tEAf6051)u#(>?a<>tnGcFF z+YW%IIxm98LP`UzDy%96TGQGQ!+wl1N{nQ#C|mfd0B83vcDccA3R-fVLh{|*C5){4 z0)=5*ftlJ_iXO;f^aKvgW=q;Sl?%sO)jz5hm0ZwQ=<`(=IIe(-+6uxG(jZA!0J#w8 zQ~zkv5*eqi-r4Qj$r8>AzkiT+w&~li?$HI2x*X@iUh`3og=&057dDCK>GS0-Un>ai z`#yxD_hu@q=4r9ka`G>YDuvBuzI-dts*1)3s8wieO8oEd}IAV zMELG`!{GJ;9xJ9DO292hd{n!wq0!-|-PFapI^z}g(KWmHD@uPtB2*th3T3iAXTcP&gr3;+x4=Z6di+YFmIL%Ymc1g0&|d4?)Jk03#Yo zE{9O){n?r|jPJjhw8G~F>wccmLMz z>FJhU27c1V6)<|6SO8n^tN)g%(+O{_PiXs!ZR%&c@>J${_+p3wV+F4^(LZ=pP{exp z#VXn(1UA>xp}HU9{AB+YS~MsWI?-MZBW(JYUKQTFZ{JjEv1;J0l<-2o_ui9VRr#_X z>xDKhPJnAE)UJHOJ5x0TJY^(gc)tAefpVRGpWB|gMOmiC4b?^ zk~ULKO4IE9JgE_9VudiO5YZ0ESDxo?V zVBpc9v@DcrMYy-N*a)y#$WHu5hMEO#ON;-4KWShXp-FA*XkDee$s}wk%Z+MhV4;a? zwazlWl>#(}s8-!~a}e<{0|_E7&1y!4lNt(WTXP0V#e7b>bRnEz(|BoyA0)T*zNRxw z{mRSa!_D}__oiA6(?3iHtk8A8xm?;xG6?5Tou{9FlRZp8IH6a2gkHPCKthws>Wk2K zHop5v*t3BJYcQdw@MD^Nm9j~afBiu;{lFVvli*HaPOc)LVX0{w5LFZgG;;rfuf{G|2E z9f*ObLQSREahwTnWFLkLu$Lc;YxKE}YnO{4mImuzN!q2qBoQH@^iT1Bkxb<-E72J( zExsn>A}MZpn&kLRP5<#EBI(n>Iu_MmjYuh|Gw)swAntV;)p3c5;bN1S4O-0%qi0pp zqE>@DH~z$258ej;)>z1)Rjl6~l{Fysv+hRzJRp(|)g02Q-d1Vx@MH1tOSDTV1~8Ql zG}w>_jqh;jn*2ACHbd!`N2#u8%6qK7@9V&fdwR4mQbPbW9~C7P{OL|BoYFORR09-6 z`m3L`Kaw;$S8!lo;Okiow>JO>AxX#2+ZHRnsHvBturBaas}son`IDMM%2TRk(}edh zH3KBoM4`CZh@heLhmIw zSqos$es9fq^~!t9mdGj!TGtvu+PvdGR%dWA3Za-ilbBpTRk0E{qh7?!GLlZ&A|6O% z@6t3=ic(MV)DAR?L;#9$7pex#n)u+oqpBOe5B9e!Zhac)lovmOAl3MZY)9>O_ zcwo;KOOQ)YZfe4l9ZPB)Nwy@u{&sB=ReYkX@B=-K!DuNv`Kq*8YIu{f-;~zGvB(D^ z=?k-;#k%^(lh~k6`;e9G%AWEaXTwOYPGWmW!h@I)8WbMYaju%}v%B*^`2QW=ZC)=Q+cYA4d1aZu(G z5cy_A6wg=8Ey-IQRXE6sWwz-5H9Sr!GlsvifdV^1MS4a|3k}}a(i~3Ro>eJ=F^MPf zui)t)j!b!!vhk;HdFUgot05@Y47{GCz>7BHM`l}CdGwbFq`J}eD58YKOa!bwG@S#f zg7vL^XJ}8ByS*CFh8cc%GTyL2@(a7ao36Rkj&F%2@5IaQ?k*3g;ZvTqm_{~=>n&=% z4GWEI>85t>uQtxV@k&~9PEQV-&E?&eR%GJFCpr!K#MJ<;xtqxp?J1>dYe3Oj>0(X?zp2$iV7ST z5moGfe%^`Nj|#9xUf6nVg+k}=V{P8Q-=nVpqAeZ;iEHS+u={u;8>9s zO9dBeZX5U{rG`o)Sfmev_Ug-2#;&@S?@ycV&sNvP-5z|F{IO)o&>W{#!XoG8>yfza zXG;tXSyrd4?FwI}ST>o;?0&uGi3hxNz1aoVa&zj>yff@4vdLZ4QSpw#eHvbg8Oi(n@`;Mg4K7&D7PR)dkr; zPBwlm$8wz-F8DHv$tl7&xpTnub{+goW95>^E9Co@6u0VMDW>0c&gZ@v!7NFrq!*8o zo71HtdTyr0RL0`_9oqw4#MC!BN3Ag)Wk(ZRI3F};!nyoDTj(=7EAhOWAJcR~5RsaE z@#vPVn1*}@_M&`WpAvY#gE(2S%=nBh>U7-)zbTyMSl!-@m~1ce-xIuT6k-*;J{cPK z;_ru&ioc|IzwQO!-0LUErSSqyfR?;uJFW(9Te;TSH<9Qo{8vA+Oe)y_>T&Fh2)%9~0}oP4yj@kg~U+J5mW z7Dt;bd05kg0hR}Zm=LL8_Md_c&BKmbf|J10-lIIUS%wNhkB6}@ee=+sf4jTWJw6pw zyOUeFx|2ctMB=#cBHPg#SIlBnD3Yk((o1US1?$L! zu2Yvz3A;^?+c60 zv{OME4c5=dmJ%vb%QD*me4Xx;!22zjFupEj68&C9&wZOI|B5qV^m&S`t=>*alX2{&@n&Qk9Qd05HSfEcNHdg; z)~zkXuCX6q(#xls2V`@NbzVg1V-GW?YMQuABhM@g8XPM3=`u3y9HQUZW6guCR@v@V?n~V zqNdV{jI@bGQ4Z4d?cJA1;{$pNAKyOvy<&p#FfRnwFL1kaojMLW?>GFR4b`<>Zr@q} zw@NYL%&$t(eW!Ny_gB?|Py2%ocIC}>nd;hVf|I*Skqvx_Y@WZhMK}&eKcn=A$~c$v z%XKl6WQkmWH(rn-0DT4)NgJnp*lfQY!Ky2T0 zk*!CNJLT)4Jcko6No5QFA2MLV;{AaB=9|j-`-sjsI8jqkn!VALz}VjPQFbZi0BxbV z<#wdRRZSHhn|hiJa=t!T`pkFsB)QsBbS7-?x@!K#(ig^As=IEu;eMb^V-ah+i$=M) z&^RO&NnmqY+H(%tv$J`Eq%?w4MK0R%#H(ZUqLQh7$F*puQ3tB$UQKAg@`I*G_V$8L zZYiidT$bJ+?Tshjq{7|34T}Ne)IvRO230>?g#*LB^I^f^Wl?ta_cN$s=~UcSCu|cE z5RlCT-p%YzK%5=%+gH*dk2Ar+?{2s4(kv|D2sP{H8=Xn(0oPOBfv4t_{!99sSDD1n zZbEFuRCsch&m2AbI9_?3QLjm?!*?A8a=?KUl|qU4InlVud0ItsiSZ(p6tF4KvI=1< z*e^s*S-qOx=fp)AFE(qFgRn8>VsrL8&)ds4EUrVV0h1fD%|xkb8_HFtJYC+# zo#`~uB<})0{6sS2ouqdw2_#5GrS_`LkDZwYhz+=!0$cCQqhO-(^qD5rAIbu-hhq)A z)K9`B$xyP)o+1LmLkQ>K1Zm z0{-z7K5lG1By9bm1uxa);=(`PjTXMa^<1q;M6bkBoRHghQ;#lnM3?DG*nS?R+TJf3 zL044&uZi~rpTCXNbU-F;2oM*i2rcIMdYWjDj>h6^ARpuU^TYrb$MX&jKKTfB@E-y9 zo?63w#;04q;c3v{&}50{b@$+&uC5mp?4L<-?zRJsK3UkQtP?}W*u}_aoD4Ph4*@Q% zixC84;%}Ep5;sOZ`&+?}JQ;8iX;?uQV#5sLuIAJW~|9<5-6xc>2LVFCv8Kf5S2H#KAjbn=C z1ESHs;U!x_(O4SpBe(?5`oi}JoxU-CKWxE{a6epe%?#5NS1aKC5SQ^}FlI`510t>; zWb9kI8r>{BJ#WQLJk1dBkyw48koQ^t>ALkcL514vs#Vh9llO6nfO+Mi*vNTso6%o* zHqYd7m56d)RIt=<@tQ*a^YdWeJ>$&>j;$+j8wei$WVu!~g%)M9+WrOfe%IRw-E>y3 zzVr`9^@>SIysy!?72Y#cIUUCIeq&_lvmK19#9}Ass{m<#^hfT$ zV7EhJ7z#ywhViqeeXu50m$yY=pg(i$u790u4)b7b#JSUFhRQ^R#E!)(HxDLEzBXpL z$E-iZ;q;C@2#VLcBv>^Xd#s>q>7fu8u!l1ZM+T*Vo-fTPE6`wx4C#PvBZ?SwZr%4r z41mZmN$ZbwQ`zog8o6VgKjj~Yvn{=m+J@26wY*&62)<>(Vymm^C}A8ZN${04vOt%Y z#=)-jJz2Avc8sITw%j{+p=KHk|D4REQB`$vceo$4o_6W_!7OToxR=>R@B+iD2L4pi zN-{7B&ys3$Nt6=aZy!4{+xlIF!$Sj|1r5kBqqbfjMkRV4%+d%7u#8mTrL*flT?+*7 z{Cmcv6z|f`&f9TO$V?b(jHj7Cy6-q$ZAoL?|u zxSGb(Dl0I2&ii%r$3u(3_65DAzXA>xPng=a*}Sbgp(s6dooMUGgW?m#>pp{o-qVy3 z0c-C%a^%GY+(*U~*b2D*LNnkpb6?t@ykjzElRkUqsfaV2vD6jubmMS3{b$ydBaL=y zpU|O*bHxXi+tuTWv5g!pg?AknyXI9Iba zud)O-A1)dqWYa8v-XH}=4#Jeq{^yWBd*UVZJ|K45bFrID zEM(R1AOp_MG1ZV??;99J+Ji1N=)VcRxx8kpz|x)Q!WaCqmGS8?g-y5TEw~Z8+)tfZ zZ_KZ}!&~$r$G@IX@?EFIEF8anQ3;{u_;NeI61JVH%t0>da?#Hbe88f*WX}f%gI`=Y zO=h4xM3{%3{>^J_cLtYo9`R%_6138)?73fls045wu@uNW&{_+oRDD~?qjg1Sn-F$m zXiIgB)BdhP0{!~SYNh8{^w*j)NrSajsSy@}mE=bvJ2>e4&)Qggyc3h7brNJ77EJ_M zH*8yXJ9)eBfou@s`)1zMBye*|#{8ly5X&+Gc?&pKxZixTG(W9RH0JfB*LAQJ%A6!h z%J`w`K*wmFAZ)WV>wvTJl^;iybt4{tri3toOSjC}B&}R;IE{8)oLTP>Xn`|zcg7bBU)m)c?tjf%#}CW{=iKvjT;1v=VqXiW zuA)Uj(QUZMH;;lMGvJ+>(kAa1R&w*!Nu_1wHLJNc`P~v>(hb8Y z(hAG8I|jd+`zO^@cxY~m-KiTZk4S>M!)kk+K+mh;>3T!HQ=NPP!hT{S_KHooJBU); zZJ#NxbHl%|Pf1AfMM>XD$90+&vkYu-K@7j0MGG}g< zW=#VrO>4(QbMZ|+KovWE8-cq-#mp2*g(pkVlS{+67P}-n?i;Lg!eyPmrud;RM}?5H zC8KZMM&@2lA4i+&9aniz+MvtSvg+vn+xWGR(R~@XH_J#JlKxNY{?P70US-OM-K<_W z9bKy*Lpd~fcEln^4(5|!po94RK=LBglsnLC<5jnV_lxid`*`#aiVmIKt2e2{;*$lj zYF^B^MgfO|OW3X$#PE_n=v+Fuu+l5HvXXy(e*U|%Dvy}h;NM@wtxhWgvyY|IVz&#L zZ7;Kwud{A7^)C$;v*s?vn$LUQ>#n0>s!;#C=tO#rc8|n{O!Z84Rb0@PKNIgSrX>V; ze$cz(gs3EcUGVkbyVHMe+tvbckYsVaOwN8)E#x- z1%1^@4R6*c*RGzkS+M0L`L3!ud2@s3z4@45LGiMA?{vFK=>qC58H><~2{k3F58nMa zqVzhF@>m)NurDO^J)JX(CtSGzHQqN)RyuDaxU$z`iWGA?&eW=ErSnhBV2plC@QweQ zX1_st9zg{#kIhpSnT9}uk0_fQ*R;F5skt-Cw3zWfJP4>A_xSj@1ARM(2BH1Y0a{P= zx{(mnQG}L`szq)%-LKtmy1&2P0t{mZd~la!P*(^NXKiFkjrKyjvn0!j4Hp>Hwy+u& zkr{0INg4g&_;^AN6LP#B?;qqrrS{63*G-iBIn{NEj$wh#-Gk0)v|3V|9~eD}jg>Xv zn+(XVJ0c6#h!3^kKL)4Q5do)_;O)ME&W%h~(kHiK8+&O!naqUc7JI^Z$SLuCq=8PX z62c+8to<(3b2vT$PZXTUP}`5P#4?QlpX8ayvUwR4bQeY`#X>#Zl)XT>r0)juhYGoD ze|=Yd+gCmR)N?ke_lT;=(KUgbI5~RT`TExOcGnwxW|TCYu7M}J14NnZB_(w*rz_~r zGDMx4;>x&{0R!x&7}Scswhuj3CX4(sD{|oOR+(kCn=U4uSwBa6I5K{a<)8jOe{wH{ zRwg={O=dn=^_yk@B7WZv6?=QwGk*LZaxv&RXW^?&yQtEvkA66*>wUZIJa4~u?z!I{ zl+OWX?)&~j(+mfl%IYGDPjdgt!MHOD#1JFT3_*hwQVB3%i~KtDWVV29mvG>4Zq8?@ zgLap;+M+>UL@0MtzunCpd5!e^`%tmQhW5K`+qdM1K5U1LN<6PjZ*ueT-S$<8-&QtF zulu8H|B&aT`k&*B33)^Ey*FOZGBR2ubF{RQN7Gi4B6k?gpIg}sd>((G%O>gYqUijb z@nc|1QJl96`xnUVyf}nneolomw11D?%kU^>(Xb#YsO2$O%kzCI_q-SeI3M0 zvJ(94{C>9aq5Xe9HsRC_YO!!esY8I@>xPVwINgca2Z8wxw=KLAkDy#TKP}p2<%f>?nisn?+TOq- zMh;iH0ws%u#uFPoJ~#h_6)obW=~Gv?!TnodnKrP#qfx4N%bN3W_T=_xcu}=U)ZF$+ z(D)^{*Y{yop0fM$K=2 zaQl^MIv}(QuE_#Oe;*LO_VNNs6mVRt*#}Jjs00A)coW^1huvd?>ue z-X7a#;Lnz8wTd1Yw!8yMwic$}DW{FGQeyGl4ne|v9WM;LtE=reu0j&ON=ZszA*mDY zqNmuUt@pRGI&OQrtb04IYu3MQ-*GuiqfJR@pQgS_qb)|*#lj@BdTXz+%@(gjjg#-d zB`I@M^PxY^8Q=`hDH{L;bWb7ZbpNd0yg($m1?UdBmEcIlP`N7aYgZ5N&S^k{-zDB| zgM~ko)KHjbpA3i1w0`>t*+S$fZ5@%|%X|^&TkHL%547AMlbzA8~h=RHKtV?yA#!0W5r+O4_PTJ7f!x%NsZPdYY<)!Ee_Y9RflSy zEbu#9je^{ln;=qFzkZGFrQXtn`Vda&=An9iSK8@kP#nnWx!I!ZgcLkW1V0{RZBghT z1uFO3XUKk`;U!4`DwYJ@Lf$sFUZ<`>EA16)DAiR6*K|}m36(ruN+q}-r@-Dg%^e*R zg1i&DmIkxQav^(UKw4n3w*A7I=JzQOSDPhilcuAAL82IKRo0^p_bf>90`#ce$%YjG z?GGz_Y?^+PPs^RY@$25JyGqW-GipLcq@^+UZrXTRmUtP%=;h}TYw?_cj^f~;z!}l_ z&%LusrP7-!BSV*7hZ@MVSjbt3rS=(=`2e)FpK526lJtCjl9Hce3zI1;w=DKp7wA2` zZoLmqlN%eonQz;ItJ#b-F933!|1p+#xu3_nPmA5R1)W3RZZIk{y$T>k_H1MDZa-~7d*jyA38@$KPY9iO2!hI z+fGZ83Dp8|+}K*(pWV4&uh-7aNB0a~TdXL!!e5o?AXL6$!)k?-#qhi6Giq=prN#%k zlf<9GDH2d`H7R|jw}4CbXGYH3bG$$L!3oi>6m;wwWL)iHV;l9{-nx4KC*1-f{l!V> ziKQZ279}`wC?8nA@@ibcfC+5*@ciMm|MuTL|D~s&dG^!5eXn)o!fV$|IvJSjufP7T zXP$ZHkG}ksuYIvU$X05#sw#@ymyJEGF*s{*&a9hMv5oPD&BTT*X7bqjO5xU;=pHo` zh4IyDMLqX}1B-wD=l}KQr=NND4{x~PhI=`KHGE%9_>o}IlW_g@*C)5#cH3Ka?bz{4 z?|%1nzp#0Bc2|~-Vrs40AUDS>8@0Zo_t*yhVJ%!_qM~0QO;WTHo_Tiv!JEJI)h{kD zuly(P{e6+6{7Ar$9D)IExZwtU(@i&B*lM*t`lf5%@bh~wy6`Hkq+$vKf>%mO*~O>S zfX6brOuZFBDZODb1o?~CT1&N3q2C`C_doF9ckjIW-oF`*MqlvWKmDVGT=)@1FqA=f zKL@=3+;h+Q&~Zw6AP7o3pKUGi}sr^)yMOY^UHMR8TnU@;n=^b$WTf+dFn_ z@%Z;2cNgLU0LV$QnU4|b6 z#Fp^mh~P<>e7}<+?S;O79bSjm;dOW&UWeD=bvP^d{{ea!pd diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index aa20b6471aa05309bce4789f3fe9449d148dbe1e..b7d87b2ab8fdbe09c31ee96875cf0ed7777fb2e2 100644 GIT binary patch literal 28160 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 literal 32216 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 z

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 HVEN>KaGA| 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 diff --git a/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/client/ios/Images.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index c08e869640fdbe2b4dfff07d1497af30af2fe75b..a9bdfe432cfe7a97e55f97d79f0370bf900f48d4 100644 GIT binary patch literal 24879 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

iI8w{Q=Js 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 G`~LwMC&t47 literal 1059029 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

N9t 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 _e#D|#T8Sgl+Q&!5dbpLS8P}hsmUOK z3v|}tuF(D4U+itXKYi6)iwqvlm&5?2tv}r+)i4_b^V=?JRc1XU$q&L1Zf+OWHewbIM1HR68yNaVOWMY|A!vu@vkWOC zLi1jREYhg;!a;65O)@*&z_(afxv+T|KVaTH+Ucy7xl}*5uk?F0*yUhKe%odCpOVOI z7a_2`{+!icb~z1blc%g5Dq>FAi-8G)XC_n+cw`DB8ttubm1gzlBDlB#d1SO6O1l;n zv@~R$hU1+X^9Q^+ki^x~>elQCsx)y$kUzUo?7 ze^RI>NomR$H&v0K?1|AQPSF%bT}{aPZX#$_q&l7do5uj+iRLo(oXOddm^BLltV*CXA|1-uQW%hKZKY8<_3{x+*@HItPGCl- z=DYo|1nwsSV{Db!Im2O!9n1Ik@%1|@&Cea;;I-vhNa9UWpMbQHOKX0>1|^0AY@`cwEQMZF ziHP4YWsW0$ywD@c5Jy^+rwzMK7!X{qwK%xdyv@o;ON!b~2vL4clu1b@$cj@^dbxM0 zJ?!KzGjp7D1)nT~$RYXfkBCF5AvAyAf!G#u7Yz2ROAz$YiHR!`cPb11BJ#*yw8$DU z!?r4ApUI%=%Map_NSKj=4^x(Ppg)^A!FnBL*bOcTrmMm=Y z;&&G?$h+)6ajh)oQO1$iYoF0NY+vqD41=baG*c92WMC(zeo3YH_X-n9DP3|=1=Cyf=u z+sIkqY|#Q5A9$b0%Ki+W1P0BEo^KD~wYE3iJDuMg2cH^zxJXULvWWXD zs=_}@Fv{6Ta9z?`02TMYk6hP!nWqmUGQ1^67$THxI}5}1gkzpZ^u65xuM1fJ&aLJ7vpa`Jcv{PIfKoQ^4^Kt3cJc{%fOL< z#Ei@D92PeBt}^U@<&e#h3-q17*I@r&Ovw>t=8@8%x5{9mnKB{}Ey^C<_)E%Dr8wrO zl33>SPCiqxUS5D|0(0ORxxe=#O^+y{bG)Fo>E&~Kr%&3a{039BB;gu?4r`73WDE&PF=>!uv~reeK0b21LhPz^{! zut%0b`0PCwBFu@3&EFAc8VBbCwJe&u#W-~p8nDx!#pXIwNxF`oQ>%CZDZkhC4n}5H zTYm=Yn}KY3;utUdzCoJ?qSM62)H{=Vw-LoMk5Ce-PJ=9~_sj6sh)3%S zpF-19R_m<>_>C}+3}asr)v)cUZ_EAN`6j%--S?|(rN{@hxB;2hIQkr$Z-HCB3n4$% zt5iRvlA-H;TKp6{lsM=Po8RX|_qu|VY3b%Je_*WB7hEe^#2chjvqUe_l`%x|bX z{PJeUKg`^}Y0mz0S=SRjyobTuCROkxkFqsK#l#{>%M1#VTeblyL7g5Ir2tK*_zQpy zA`%v+iR&f+YG@Jkm2rirf8yd=Xgk;smoP1U$I_$VFSF)LMQzite@xtdI-(7E5+`!J zr2Uuq>AAHvl;qtH#Un~u)?9wzzr0s;9NY}4DPiT`aO;AstnGem?myx zB%cSJ8T`5p=((&NG^)i)>@;XBhc%k5`oe^jN6d<<*bK*ZXCu&S)*}se5S%U8x?_Px zJWMj;?&y=$OV7my$pD}w)d%Rvc5>%7RFe4xxI4&T@la~6_vDUd!VcAM0!?t|{Q&~U zF%t7L)u>vJT-_YN(B=#Cs*qu`iYq1j+_}VDSj1bIj=rA%91h)AJRds<1Ib^J~ zm)u-pKG6H?mdFdABDY^e8BSj=+Qu_~^Q_oNrPr*;t+Jc_)II|0hA+v;g`T->}P|=$4T--hVS? zNr1Ay?;n8yB*@VnV^(hVel+GU?%TUks1RCD zkuxkgxq>Ojk-8wcB*NVlzW+1q+3*Qtp>-MLWQE{>DN`tRyNM-$DTCk9JL{$3DT$Lz zR@DA&ostvu{E>5Xm0k!DgNmftR%5;L-+GtXSReQB{EB+Af-@IL77?DVI|N~tJOUo< z$ZnrcUgGYNni#=Ea#_gA_YYQ-1fb;nP%G$T337E&ge3)2?!@Q0|7d>vUk%lH&Es;Q zq`NLI!LK^llNE3I9%gt9LyKct8W~jQw+Phq1gs>#8OUgJfkYfMI`-}+6p@!ig1H>FP?fTcXSgk^QV-(>TN%i_H)iczsc0Rr5e&aZH6;~nN?#gIHG zaq7hg*Q%EGaPRhdcdqck((V(DdGp2BR1COW6PQ8b-~>Z*%>(<$PpRYZl(OI7+LU$Q zeYbQSlrb-f(aC||n=Lpv{&B^2D-6mY-A3_y^9IBH7t6@TZjt+imYqT}kTD50XNEOa zi*zDvV@17|hx3;szxQeurjYeV0zFFDBZHU_2v9)lv9V;u5-wixh-&^I#y+?(cb(=< z8SjE?DtW6q-2yp2Cw>=hcw{Pqn`hN|g`L4wLT=#)=1xJBwU?LdK1l*oCa_*6*vq}L z)_s9$Y1GbDEYu3-2;rW|>ub&^^Q=6W+BZ+wvy!_kgl-Kye*>mG@zv00igskV6E$La z6Hl3Y7(D^ChYK?Hwf-HU>7f^`!;x73HDOkg@Es`Q4zA*39DHJZQOig1wi^h?eMerX zc*XE1pV6%oyJ6`|XKmx;AMGK_^mWpR z7#Ntg9nM&x|E-tC55Vl@4Hk|8JgB&f)B^#eTkirnw+h1@s*SBtu_Xu?1m1k565a(jOHHF@Xi*>cKnsQ>BFi5nVQ z8NvPH?^TALS^$)7+s2Ffbtg@l@IkTfEF2QFW{OoRM8NyRDR*;NQ?j0FSw1LoG5&R; z=%QeuZqP{O(O1Coy&-Y%lM)eqaHq!I946rvYt%G$-|@c+x|4jOl!hwOAFvVYSasEW*j& zqZe69abe6okJTVnV()EOlF;+Mg1Y>#a}?01Z9f9oU4ha9l?P&e z=6A9Y8Rz_c{Q_w(Ijx_x(YUGZu78RH5kO^U+rApi)?XlrKlarE-4BefMULyXePmwy zR{a5qvY8+=f0h-;J^57nOr5XK=qVZ8(Q4>34(NqQsoOZ2dv9eK5%<+lo!2RP?URPwP^lUXXF(oMfacrLdL1e24qOCCAQ&p@#?YsY|NBB5z*3Gb`t zWjM_Ie9qAnou%W2kmtFDPDTigY9#_e?|!y`*ZLFMKjM*;quMdt%v?t$H`wY3n=eWSf;c>P0UqL zQ!K3r&w9BTH1V)7a=-Ons#>4)4G^a=M~~E!)Y`WIOB?b8TgvnlQLRF!;D{1wcd4QO z6tTii_HX8@+?!q`Y{xN;e=INnsyPaHHu>WaU{grmL8f}1UH7MTygJ3QObs)GaLCOS zK1@V=-~0U}k}S>ezb>wGJg#B;t`NnAsk`^SxcCX4CUdKJdaElLf5S~PFk0^qFj^9o z;#2;x9Avh3%m}_&M=Zo zndtrsMA3#V0pXpQ+*A$`kUW$Rq z?U*K72+yQ>Jj-MsNNP^?fF zYb4}uCm}+I?8Rw)x(;8~Gj9%|EG%2HLuNg(za{)Ol*jiLE04fs7f{rj9s^D%c_s#H z+2=pmNEXbTVSz@W(}99-jpg&a7*3dXo0h=|4edoE+aho(i}y{47ga;U)F=rU7K`b# z!2Vh=)C56G5bc0@z%q^`MHc>|6lxTHlQ5nqk>VI!YStR^F@={m1opQVxNoSvodGsN zqbsS2UorY30`O?K>Rs zt^@~|&`k(F1I7JNQc*bJyA687N#YN2xe zV$>B<7O+10(_Q!HYKg)#*gzS@E{&#hB9*#_R>>|Ptex@&>O2xQJ#{lmh*X45_ZIGh2r3g5UZrzUWs|r zm#0ky^*G406MF&X*=^^eYTla?j$@PfAyFE7iYxZ~TruBkFZNJ&1Vn+=r5 zxwbM_N%0LJNULs0i#~~3o_la+PUSWKtAt(u+*V&1|IEX z=<9Op)iz5f{#x9A7I(+c-vyFe5SE%mq^4hh5HLG`< zw4QUJ$BD}WCUMKubXqMnS!C{<{tBTd7l^lkYL>FR1UQav@lxU-SQu&C>WbyeN+I0p z0YTfu2%2&}q13YK6OHCH75F$A3vpX`o1Zm*aDkdy#H2b>ag#c^cN|>}bCvOn<P+<*^8?sccJUBR>#-xAn6le(p}yM+geEl z>Z*V&X%i7+;-;Kx9*Jd*sfxN)N#Ux5GF{x~tsIEZrG@MlUxC?WYfB>?FTHy$_R#x^ z{=cDV6<^*(i|BJF%LKi5P8z+-o2l}WnBmkySJ$=}f689W3{^M{Zr0^f`Z5Nt;y zCGqVcnrwRYr1!sl;-J;FHA@)3yO-oOIVe7c5hR>9p7w#bqmA1ZAjetkuRImYGJ0<| z)&K28+TjvyR~#3LSf<05M#9dvq_uWzMkz?#4Bc`C{bCZ*)3bTqeZ3=dz$(VPS`X=e zk0K2Dq3H&??s=!o?=A8pnZSiuYLc3yxQ{{DBA7rrvprQ>3^d(o^9e zCMN;;EltR)cI%q>my5|%g_7(|0ZpINzo$sTZtFwFxn3|EDzBed1L_x>k@{3#Q^Iyd zJ1y|cVC+qq4(U_!Jp!l_ey^n0dNGp2kqamT;}BgT79VIQr`Dhg$Q2-x?2d2snv`Ti^Ae^Mv>vtNm8{b>;I z^SA1R&k-6^86A`J{>FCz^P|Ku+LNC1WoXV}UlrGbBwS8&ZtJI8%tBhPAfIyNkXRks zr}|9ah#B(F!Z>K8yFOuu9_)s}!*wNefPf3si{{Y3_LEiOgP__ks9F<->?M>mQ3Uwz zGQONtzP+f8!q99h>1|ma&&IZ`v$NVQ9>dyDf8Bqco_*avG_;PNQLk?=linglLnl;= zynL$o{r!pp)5wdA9e0;_c&OOYY$uG&E$J|J3@I&nfLH8jmO&2o_ng&Y6*;?_81*l{ z=)7(F{mHlAbl0{BX>g!2_4R}?nl^0^WmcjB$3zj;7~n-#LDGMNzILGB!darwBKkd} z_K98L5d%(Paa-^D0R~}czlKI!DgKpl|LcGIWV3F0eLb#nb*{lX9TQEizqhRL z&~b$<`m@0+VxD=ss3qJ>B;f3bJ6><)g;%>JpSKLA^fM|-nrC!I7nyK}dXSGl-g78slWd9vH#lWu+7@mxX3K?BmQ z(pP@wu6}Y)-##7L_RK@KLYao%hIE2~*wDYf@Qz#_Up~N*!;X)|{-lNqbuELaG9n?G z7x!X)c#uOH&W0gtOyNqAOt;~SV#|K-V61l&X7|shPhOT}(OruCFRU+bxJAP+M zP;>IBMICuStn&1*H#dL^JE$B^?2oS5*I{Ynm5z=w5)tMCgmQ`q3EPb1B~}@T{?ZN$ z>J(v~GPJt*9al9gs0y#4s?az{<&*3cs0DjSeAr2@i^e`%mR4Cz%1bfL>Mip_PHz*v zIt^sVVUdI{xgFWgYhCEzkACD(9k3Cp!W#LtyjH0iqTU{8s2{oQT^H!Yso8{yNCZ{Jh7!2(&ls9)*dWyjC;`;8Y zYSrJ@1%>1trCjX7lYW`RRV=`f%3vXjkRp!cO*UO_URZA9cP|>L3*)0}+{2g@7toWXgYE zxKz%Z$EIiVYG*LaO}V%P+3`|NW%R#3_~Z+C3k#Z0%G3eZnCxQ7OUzjHXDmzSeij4c zJ;H9CmDQ%Bj(ALP<=upaPc*0V!4*RP_4~GK6Nj4v)JW)Sx#J+m?w`ABp>_WqPgi)U z6V?rZ(LH0?Rke?4@+N9nUC5EN9$@!#{CD%2Dw?aJg1cfKa_?x*SIlcq3!N-)?pR#? zfLql;714@^H5}B`BN_&{njtXCfnuZ2Z8_IxM)&W5kL@!o9RaTwKj|8%@GUUKT@1M8 zai;0K+B49e4%^cLV%o)mqTO5t&d^B?gdUnnm-n1Ve@1>82F}}n#dH%!>^bSYzD_(ZS)iDpRa2X=Nw+%{n?;U@{S@ih2GEnot_do|C(&-m;0GFPGb%9)9Bw zDSVQC^;!|Gml36p4J!q1kWp$8SUmYLiHfXNmPHw0Jt#Xz_d(4|Z9SO#>!q8*j)(T! zzVK(!3Vg}G|8^|Z;wq!El31NGzP&xiBV-EP@Rq{1`sDkI_!Nsvg|w(tUC*?P!pEHy zo80Q1brD+=$nLk}Z+482VxYS#9w9itmRq;$12vZ?Gb_g%Do!L58gHuQK3pbkBgp{5~<)_@SKa*vjACv%OM$xMwG+s(arygf~3x5bh2k zuPy)_K$tJK^q#DM@WAyi72~s52=WCHO3DjXt{m!1>r_?Fpj0^8J1P6x5uMp@&2@28 z$V;{rw&p|v)`<&zlCko3#9+{`j{WQvCRuOM> zLzANprwNj)c^Pw%(${)rbfzu#P)~m{Pr*4it*_>IvkK+}>aR0Mm~;S^YIKkl0SkmZ z3V|mT0d>ABSeB@jt(?t8x71# z^x(C`b!0;ADm>a>Ox;lmNr@F!{fdDe0a+2)fbw5bU(8X5BZJ^8<+t}E3)kORy=!|T zK;c*f*G3~sj!bi}IbTJah*w5Y&Ub*AUnUI|=;(If0*%hUDPXi(`}$5p3+HT{uyOt2 zVRhp@Z8e}Pe+ji>IDu91cwGzLI7yB6P>Ud@Z~9JA>cv(~URDuAo{~>`!i+rjb(Zeg zTEOG>83&qkI!@ZWa6H&SofsgBICzwt#U;ZQu%BzG2@bmff$i5)M`{8RcNP(cb)C11 zvg4RmNMaZnre`cm0iCc|#CuCnd&Ic}oVkYm&Dk6Ww--I(;1`MEt8N3~82Lh!$ix#H zi=&s}eddk66w{GTKrbbt1Wdf>QF2>prb^XaUf7vfpU3oI-H5XYZ?zXtg#zAL4I8i4 zQ28q&xzr}4rOG0ECOfWTiSq9DStdAPf%MVw3E;sF;qIyOFSxRYBEuEZ)vUX-@N7yJ zrxRb6#ySRZ-0bE0HQr0m+_LE$BXXW;{%4-~QIem9@&I2&FMNjnQnW^RR;H$+a%^hk zQ@-JepdSvX{#ZSa@ZBVeZ>bt-S}s(@IV+2WE8W81g2;R24+dZCsLbqBHMNqK^OoE}Sft%W){fz-XzC1y^N7=`#BB#_MKY5k}9k2lfF zH>BT?d4+u(FN3Fg?rmXAFC|zsMUzY~;0x%Bxt^7Z7Fj|E@OZmKuq3-S)+MHv17fc% zgqxajAOQ_bGOhEKYs=#m`+mI7wiuk>Okic9BX$~2b>yHfbON9z( zpyU)v^YNi}SSiYajzeEnwjto!}kltM}-v zG2@p2oa6GD?t&Rb=WNKP>K8a-VRP4wDQSq9f(Nx5xqQFw{f5A_(QB8j2WmRcto_`y z{#xKKTAEp_!2DXJovlWalYMMIUP#k2XhUP@%CJ3x=|W7Z-ZAHD@S>?|z8YAX1p@NF zdqm*Q%^5_)85$V9{TFB61~Z+sC4LM@q`N7qis;r{oz%~GFuG?{^ZD%Jbg}*B^qU25>n9e7 z?j;LX9g8liU1m$@l5xiC-Jszcag_P&md)*$zlM)*5@dQ#k(iM%gbhonP(a`!4$;3w zvsBI+2V>TKfYx>91gTQ29^51&(78v4SDZ9Jkrsn-61DGicnC^=b?AzHr54OIZmsHo z>Gvpo=Uqv(l;92bSp}hWHhpRAx5f|oUnWHvfpFSWaR%3# zzRev7c2PT9Z7HT;!W}Xu_LBM^Spsr`yq8~G z86Wv-ciiqre2$+xDY_26sn?pVvS}_iHf6!DggG~CIXR=&z_K}kO(?~nV~2K3QF*>3 zYny2dtkAmU1YYa#d3_UiZn<36jNiG~6en1x1;+ z-WCWg)}?tQ#mx6PpIA@_yfL{~5is)wetD2}ati@duzB@d)zhj!&x}@ETmv%w^g??1%=ZU4 z9ery9$U&ft63PfrXk+0M%>>^;cwkxEq??)BrVS{JdtcEEl0{yb}mv z5xE+yQ2$tpz6HDvR*Gm_pg(vEj^SGiznwL@fb9x1*I1FG#97~a1?$0Ro?V^zMV(v> z2KVncw)NjkLS#NF3Wg*MDK|dv4F$KNP2T(734D(>=TN~2ccigYdKZOVH!-;I?!K}2+qzL~E-yj(rGns&h=zy6; z41H0+`V{v}M`x_DN)!{P^LZ>I%sFNOG=Si?wv^8q!5Lro2E@IJt6M7i@HMS-(msLr zWfHn~D?QnEgKtL==zfbH@{$6UF((PaQOWXDzPm_!RJ7GnD2>cFFR z(vfx=-ehwolizr9*hiGN=Zipaw7B0A;xjrZmHA#HZq1xo_&_q8`wO-o8k`?O2jxY< zGWfRxm^{OflZqsPR}2;+Rm;!nUON`I?myM>mYJhKE-Ujy)pnoDubhb+#z%jp@&DJo zd!6hQ&fC|8RxOgUm#T~GkD3n-&zg&UGaMLOz$ESWxYrj9a@SE?eJwjq^bh1Y3@Yec zRYfL-4+5Ykz%nfH-zdSZZrNwy*aZTJDc)lxiO^XrSUb$Lbcs+KFS!Ke0jZ#(i7?uOklAMjfDZNLeR z&QAF#_fd{&J0*KMSc+E>)~E<%)QwsFln`E_uc1Tzl>#NhfbDxb?bqhZUd(S1ZC@#hjv7U-XiC{X$e@5vpXamnJ%6 zNUEBCy|Oe^*)vck)D+_oCBbp|HLxh|{HzL%UIsbPR*NT^%65#&64M}12|<}hli?t4 zi`*rO{N8^Ao6=jr2&AclV?RH8oF;tvLbm*mlwgQznM(i*EdoK-f+)a%HTGl*Z^B(1 z2rUQlaSqkeX11Tep9=B}79x0NIZmjB0SQX%VW)KO7H)n~+z}Ev0ywCOaTBiBuqIpm zg1(ufktS_!N&fC_{2Unp(!5q`vGFusbF4b!+hN`{u#1!D8Fx{4P-e(T?ey!~yL^H4 zbqW#c^DqLJF2{BxdA=V-^Ujj3W(3ntF=~Y@JPCf|?B_BFzkLf`Syq8TMMcFqtGhB)Js4CYHSjeBm4ZBqYXO z4=yo~)e#P>+&|B`xY2D1au8taVKO2AOm$0uQVFEwECYH_D~d966JdsF{y7oQTjJ}# zbB3F`4hWJHEp)c(s-JCU$!Of2hMvu>#aWGE%@@0wN7?LXWqEML9N#?}V!BQldY%O% zJ)t(KkJqyh+X^Ujb7;%$El=NrkkrW3Zi18F9#kX%HICs{njj+IVe=J+2`IM`BbB!# zvznl{Jc7rRKT}eEZ8wo4eA6POmhZdrWrh{F%+!_^wPmf@yMoRX?Y4pkYsm{n?pMD} z`p9@_HGIj|&L|dUd|umMWnXU2-aow?rjIVgP>H>w9jG6)zXo+v9^nnW4i=cQLIGR8 z2Vl$hHD=6Zs{Shy?GHcUf6@$DJ+5x)Htfme;u(+`eWE7t?;-02BxO^&t;|z{*lk02 zVID$-#wis3@OlKMfT$UY2oisqFWtld{cd`Xazi2+sZpA_Du3Z3m^$EVt#x7ZR^mtY z5sx+d(9Pnu{8(5cQ=L5H#`>V{%P|{dd!h8tyULg1ekqKjih{U}rYXUER zGm^2NVxV|bHB^NF-^YQl&qWe32|84QL(o;5V$htY#H3Jxw`<%RghemZ`qYIDvVViH z*8Y=h6`Pug0$yGc9n{|fmZg0hgbYa1Ug7pv-R5rj6K|h@IQY~QS1^~>+-GNBkLJ(| zNy!(N=5WH{(K*`QNy1|0^qq1gX_xQ5M=$8N7E!V<(_3-zugT4lZ|1|ZAEYC&QxQ$b z+kQc&B$1f}{sB!L$%m<#eacf8^}9^vQG0h_rbYjD8k=b9ssch>=5BM(t(_NbK+TSY z2uZiF;D+dO`td>89O4=xKbXAJ)DZ9b6wxz(pf5iz0;U)uTP)UQkf@dfVJ9c%6sjPh zE_r~|Q0OvSuf+yl4D(27NmlUQBZ|4V!Bc?HtFjPDhl;22;lJ8@qb4WW_;$G#{eK7f zIsXA{s1(Qkkjw%Tl1?I>zYzqxbZzut z)cOmtRB^-xG?Be*nS7{A`vZQymVOPe6|m}^1=Y$7XuaPs>#Rm+wN$f`&kPM6ZhOdh z_C9l4dKYzRicfQYF3-C*SL6pWNL&>}q?;;Ih?cL+JQ%s#HECdmZ?T zjGw7rRPYsq{uln7H1>wZxjxkGRr0|*&1kRwNkUQB>olVvKS1I8yf=iV4^IChU!IGm zCz>&jCfsqXSIpL*an#ZTFv#cWe3x-wF(O9&TCB^K{1qm&+%ELoO+`lT8aL@=!{_r* zC**wRh>Jp_>l1gM1q2dqOZ`Av+KYvwh#uVj&?tOyBvF|Se1FxooUT7bA`w6ctGb6M zu0@uBU10i22pN8q|F|La_GVM1Af^2*itV^rVukMmoC;S%Cqm!q*SO6IGU=vU(AldD zY;H0C*u~Qu5NyBx{poN#`wT!l&ZFdHz7o=>5B9^%($gd%;_!PWmhi|xVto}lD)HHk zwH6ISFxg;cn;XUHzc|IOUjUusSE-E?@oMx_{sdP1`T=p|kWR+B%-8~5Muw+Ahu{Tg+I9QEGjZF(v<6A3>M z#^2|qo**f1i<26wv=zDb2H@}v{PKvlVWE1uI}IAE*TdT4Ouhe>ebZflEHA$S{)5OL zRUL{qn#NsM0L+O~$Ax2LPmdlp?l`Qyns{f@+o`78BP4yvLxI6Dhn?7ZP1YwJ$m=B- zt4y!uiBOT~A4!ZiFDWG{JkJJ7URE`06M!rAy4|3!!5s97}xO8pSJjdW~{~dU%QDZI!2=ilHt&Y^V z*GN^>FjU&_NyaQ0RzuMYM8Eg3N}{EmF9}*FgOrTj?nYW-xqm5hhZtDy?$Htu!c5*6@}WWbF}6wfgSF;4R_;%iS?WH2aDE~kW=(4j%O^TW zRwFh%To8sC`-$L2HO`Or)cDB7>>0fW>#Vp0g zPQ@8qNpYw!cP3eT1bIP;~4?Sjoc zYBEPI7Dx7@?Vr>e^kX(-JFE4w+r1dXniWS>6W_c2KE^HeLbKkpTdaPPvkB7#`N63i zIA-f>g=da)-!I&LHqTh(wvS}U77vn>^ka;iQZK2neFl+KJt}wqr`T`VR~=tDuFa0& zzGh3dqoqESPHbmAO74qH~`$9|TT&#)DyV6$ZSQi9Llw3aYHZ`wA{V0ZQGRQC)yhzyE#RpUl% zY;lxC8KJ<#{<-%q@SWoIWq2Pvj&q+PNSSAe`Yb;bH3S8g9qm2?s_nq-Pqvk4iZC4KH~lH#I7Sz_2Q!GppKL&zNACha7c&ofCc-!lt8%k|_`_bY!5_vWkMPFBZzcIHvk~_OPboVL6@ZXVgG4O$X zKMIeDTw=({Ot-Y3j1t}zJ^qpY(*HLOq!Ah8&#JSv68 z;c*tlZw%fSz1#N2J*cai{DDoK_;WlscusuwfV=68*cug}LZ?E*qo?mpMc&E9@|EO+3*m0TljT!M%!VeF z{ZsL2cA~as+D^&VtMO>wF%U07_XC+q`oSkAC5ct_Q>-cdkk8{W*Q{8qo%Vv{0=V;3 z@!?zEDlveO>OZ#=H|c?Q<_T3*|9zwpY~QRp)PGoW~4Nhl07)uBkFjiz+q z>nV~kLP%?kCXaowmLkxCvcD}dKY!JjB?H+ZgP=j}VjvVUCMIZK*LQ4?-~JTSAcEkZ z6z3y0NY8+pQF!rn+$em}@Ree0HE~gQWHaJCpfa+pmlq4U2pgDz)|uDLA3?e_zz(}m z%z8K|N4)eg)(V_|j!xOeFBV^fbSCeEhc$WzT#kn?TCw?}XdBizoVdJKx(*$mPo3V0 zXZ8|f5h(1M`pDcTF}ct?93f$wY%6r8;LHoI2rn86D!;f;VA+}3e-fJ-vHMs90l=fC z&&mhVHzFXZ%!D*xbC|Lp@VRo9HiAXeEn)Dk&!-9=h@$VdPlH$khR+8$Cml4 zAq*u{(Ux#KZX7s z=&unXSvf*bSMBSmTgh8dmlH`+1V!{vbJnq|V%sUO{@f=bgN`q1tq)E&smv~?{m6=;3crj_-T`^I&_L!p!tnWt! z*wP}tIAsw>TA5-TuhHP(#v;;vK`Rk`o}wB0Gmjx`jc9~oGUN`nvUWds`6|-0W0_$Y z+&{ybxcA%^j@+k}TqbnzE)KK8I9U2LA7OpoTTCgVbwnt1S)WxMk#1Kg?DD#nsPhL3 zVHmh()BF66(@O}7Foz6>fNXc@CuxVq{wk!2G3=Dr~rB1zLXR^0w)zl z;{O4UKybgmln0WxBWaM})ASci9o+xG)CWv`Ae2wrPyYvJo<+1gklz;GBSUJBW1{E; za6CHzL<$NY9BE#|Uh;R%|1H14k=}|s6|(dRm1XFJ#{It$aNO7X+@hk5KW--gS}!tX zxxYvVc|=?~k^U0^{wJgVI8ol+-Q9iu{Q2{%SFc{}Z|vv|8==}jfC{I6T84k%jM-S= zqvBN2tPM#ArD@y)KHJXtl~39(El43LgARQ0Ihdz^E57cR0u88)7cXAG?*3k{D{KH> zoCAC@a0}3qtGQ=_L~l7V;Pgk_yjYl#?|V4yL}{sUgvF0GdEUEeZ-F`b^*iQH&yiSX zE8Pu%ykeEk>t>zf`^xwKvCdZNEFY=@>X?J<{($@9I@JR>y;4oa7ow?N#_j-8z=PyO z9vl-H&*_;uzlVR)ZJ}Xs3L)vzFyzG-yYVZ&eK+O11Ekj=@?m&+=H3lpbWo*Vmcw{& zs~zjW^E(LX*QUFvAI58(IC`_O$$llT^TpWo?go(dgQ3?r))4fu%Iy9y+T3p9;~CMh z-MFKA?C-|Y-2f1VYl_LM$N2J^j_Vy6i@T`jbk1#j8IHrZ$s?ZxG$58XxJw+Qaa2`8>6)qU;oRy1C%zH;>Y_&A#j7|t!Ki)6=6RNPA|&y)+tyOlyp>iLtGY5QiPl5 zDVbnF#%GQgkg-#lbRN}oBJC1y_z~zQ*yA_|T%F|0X|`LUX33ZPsk4;6aY&pk@uSW0 z>;7b;fM{-mIRXshCe9gY5Y0H}L&$dn0N6W*!{lU2*9(N2}L{dUPmaCi(Jm7lk{D%C!ORPDpy^pMtr9c_EO`t3>lfk?<6Y=C;!>k;Nsg(yZAgix1`CLD)t?b*pv3f>^5&WB$7+lQB zPFhJ9)8~!>H_q2_E|23ggfZxKtZ10eKixfyZAqU$(jb8I^K*Fh>Q(vNLSDqC_o8Vg zt$VB^d+{6VD31lm=8&|rc2oaN=1taO#vz}7={~GsQ*`JgCk+O`Njk!GZyWW?WHE9x z4U&4v=ppkTy{};ABPk$&{PMiYXJsYym%sdVTfp*B5U=PzlB-1j(&c*x<(B?5Pul8V z`X8Y@4j@1or~pZO(zZjA21yS3jfef1Q{(l1n&(fZrEc0ov^WxQYgOXDph^+@gA z0NC>mHS1(__%FfobBG%~(LyO`KiX(6!yGbEC2=LoDkP7Yp&=2pS}P2^(0yL1fh>dW@S}nR^6ZPzWd(o-%Yn*X2BmYngv4a*z^X3 zU^%Q{$(G$9cCbYni50A7!$?R-Ac2r*F@Q#50WBC>{a*KbuVLLBj;tR%-*zDtk!X8u2ct#9Io}h4ez!7x8c1c z@=k96()u4F+~V`0dS&Q$GI!5v$-g`cB;T=2$gW7QvOPl0E!Xze;R#IMU0J7$o&=dj0Uezg$y#YuI%vQ(Z4WM=p zT#oY6B;G}SQI(R+f#}Trd&Oe)d+U6#Mt7+yEbMr$ZGS#XnaQXA6&T8@$8}&SO7cZ@ zNOI9&YyMqhtPdSkd`gpI0ElBKp4V?;4eb;H5sbwa@RAklQ$#?+N$K zc0m~ce)uWl=B6k<3JZR_ru?%nzpS?++4?6Q8)z>|ZA`^;gn=hCdo5oNy={|=QlYA7 zTKdUU`!K3`$$a^@Z9~=OC~}^bdF%;sja1I$eoHEc3Ypd?nlkXeWXjY%di~Z0GLezD z1ko6|5)#!Kj7FxkmnS-V6G&*z(!Az zYeP{cvnOBg_cq7(*?yOZt$G}(r`N{vmoUF4X^`HYXTe(k%QQTX01)!t zvuV??aS2UpjzL};SJ&6qzx>57{=~Cr+f)J&^0OOJ2OHf2)j-bX0sjl47qXxRII}X> zYn^jyF>O{C)7V|QPWq#3+;WPAmt6~GfL@TDT{BRmNC2Kae?A{1Gd}qg^!?SIQhgBI zwZj{YB(`LS6X>*kALy2GHlMp<_w#k3xR22wbqB}u+A7CZ9((HX5F_6hO$VMjIxBfa z#GzWi$i>Xje%(V0-MZ+i7c7%p1VSU~N;X56@-jTyzgrS1?E-YXE4}vU-Xdxuhi*dL`-13xRvKd8qfJ*253;v$#7vvi`SWryd}uUI`{_pG`sX zByo4^3bB3`D-|!j1r)P3f_Vq!I|NiaeoKItJp_sjRvJ+}dr3U!B%W(tpe5*cPFbnd zJ15^U?cTP3=fp}@r(`7hl6wEzwep=xqp<9YR9^!0-T_-(mbghvXCMNXP>*>{6mb__odwW+i{BRySDMIN>2Y%Ydcc5Bk?qfxSQIQ z@Dm`En}(0rs#c}GJq!0uNsO0y+*TA-P2XI-D*<@(z8M5n#Yj2EEAQtQUwk3tI0@;| zrOjnzuh+g5N#)_+iISdh4`RIms3%Jj>&dS3Hm^BD@?WMQ`;4+JK%3%GQ3TNG=xqW@ zDkfe2m5KoEdiju$Dxaj<2vrRjz;l2*9~tQCxv!xnHsn?s5{t@+j-uNr(@UO z_7%L-s~bb^z!QZc<~qw{!&?o=){cCIu;`sw>?%Tq1|+grh?a|l{{G!cT#pSTiQELx zQgiCjg-dlSN6^T04zKaLo?e*bxJW8ElzLc&2D>jnwf+(0YI9x6Bynr=%3mof%d)5D z($|~7*82d_YH7JJK~P-zD@BilPH|%^tgPlOIc~?|-LtvRJ3xITX5opV#-4C*{olgb zgxfvldNvC(0F4Z+^~#kD!!w{dYT%Q?fOw9! zWCe@1IW|7x4Pe5Uw$n6A$WL>#p!WN)C2Qt_niuHucMVBZ7PfHZ>j1yUmOu zk5D|I*h-{AzLY)P(*h*grwHJA15jH6@HbNtNV+8avM2EZ)a?q^gu3UX)<8A(_zBhD zbYRERjHX<(WA?_1d7Gkbkh04g0N3BvqZ2J0f)VigIO0K);)IB+f%1i{-)bPO+d4=& zk#?rm<5#jv_GeUk&w9)r22$;hWZ76=v=yh|EdEqkCPoXYQ-W!G=%z-}@6^saG?!EN zgytza5BH`wfcAm$vs5SjI(28sJv*s8cD*J;@_ZnRBD?EbbyT@#@(Ga7`p9jhTu-zm zel!7pA$OgCBl)pa1%=|N8sMU1MAUc#r;OTk@ZTjJ)&m z+{pSL&`Jbk&dKcr$mB=PAJ9m8GU>`=`|xHHC(psikFK+E_Slvsne$s=TMiGh{NAyx z`|Moco^fjhfV5w|F6}s%cFKFdX^@7M@xohi*daU0<2JurZL{b6ZRO66t$J+5+p*f7 zw#R{Ml5_oH;zrNtY`cI_zO(YlIxhfz!20q0*`%H5yVtMZz@lN`Zq~G;*JE{oa zwF5R@6lr0U)&1s3~Ye)@|$UHO4A-o46w~4|)Arao1;* zx`ShR?2bDyhdNTHP1$>ViDR@Lcna7iU!fOrJ+jq|9JXDpm!JB*kmRbS{C1PbTD*=8 zq-#wc@6>XtdU~X?C@w*b`8-m&E-q^7aY!PkREP_s0MS|d@M4{~XQ|Z;YP*AKnXK>U z$Qu9^GQ#r)u<#-`CFE2F@sxqI=yu=-#8Kp^^(@yWq;?N;A%$dFjVFqsHvk)^y^0)n z_Xe;o7LzKsbV&54zJxc2a=RxPFwsc0)XEfS!a)zMPB{p9()s zYj!NipnfhR)>{+mb`vStc+;SqdGYby&FAfsE&$|I2rR6M0AfFEP(n(g?2b18wh3SO zU8$T!Y4ZdyH=tLz35$3pT3@Qav2I^WD9Q@v=Gb=#P~HGG=ZxIn?0ZoTDc8^*!jjBA zP3~Q;X|9*ZK|qU1qB(%v6ah*nDv7aN`h0FHLhE1o-tax(Urq9(^!Wsc``_{qBpLBF zW}TX7|mq6EL%B;&qh-{V@6{oNHIeN0HA#1u_18WG&O0Sek-DR)_DAap{bb1RdJz4_s$Bk?U?izY9J<)q?d zyY+o^zb!y6+cC-hMBj&0$7AxE);_7uBbp}x?G2!gMU|3(B=+0+)*o>{qpxp8l;QPb zEaRz+6M;5#ZGE%<3z@Gzbb7UNCU5jhLfY*aCzt=tU;ov0Y3pR96ZA0RYcyU&8fh~5 zZ!P*qX%V)q|81D_k)s%L{@{eI)jiCytOv(EbL4FMc*nMU-mgtpmN#vL83}$JwxEVp zBAwAWo4y@I(jZQr0|w=kJT~oecl@+)1`hyf<9O?rkJIFMcJ8PQ4up@Cf7TX9&)uf& z&yKCKPG58&-ntO@{LTa~dQ<#+Giy?yoS z)fcdk7Ep!|wh93xEMoR-3sA3F6Z5}zBU0a7(_8B3L67@z*lXl@xv(kgF}udO5yOXa z@XeXFmmC@6z!&%yiUZ5@=eT+Qe!g&k1U!EM{PccrNp6Mv`Z~)6)R6$quA7uA4dT27 z!>kY*aa#E=sdK*=-0~r$J#jndOnZtzha>J>#X#W6VnnvC0o29nb#c<}1T7Zf zN<4rex!U#NiWDlux)xR?pd4N3_Vknv`M_LZ1FYjOX@Aq?YkGcof2X;H19@lY*)ex%X5B zbpgj*mq=4gNJ+k9V3IW6< zH-M^hfTX?;l@u#(G7?+DqIn9)aFhVJeCva)t8darX^Ytts=na~*zq)%esNot8|Xp3 z0m$3Q^-xmWn56oVrbi`TigFJ!Y8$Ua7`5U7wa;rxR6rxo_*Z-*_&F+GCAeIgL~GBJ zY&$Nq9w(9Nqe;r@#U9jJ>Sd;K+D_;u&q^BKk8X)b^>Z6DeY5beNo|XVdvgwHUsg9{ z6Y}#{X9u1G{A=`D*j%1=y;AK}*|~xuv^Zv0q@pZpKNrKT9qNm!7o-;T?J^(mq{mcp z;hAOIct@AFeZSjQ8wn`NHPGLU5}S~b)6$fgGmtakhaZ0$Tk@CVm!E%z=bt=p<8#sG zQVGC5i8anV&q>6=96Jw#sx#MiElTdh6^YK*W=ZZRq(aWObhV|wR0QB^DX#&ft@)(~ z0j2yzd;>*4C@PcXI=4gqliL}#AfMH%<>h#XBEYQQ@K)ge@?ZX|w|x8b-RX$oZ=fyr z7oB=VcH}JK&%f;E-yW05pF|uD{;YC95^AauU;!Gg@$$S?m)X{`Y;bx+cvK` zs5tO^S@7P-yLo)_Dd>R}07f|4@y+U;MgplnJ-eT2&v`L-8&3Ww`3&p5Bi%i1^prj* zcO4<_xt?x&p7vd{RR-F5@;*n)G4c+4Ebz!y9q#Cuq3d-U9$Dyii-c)RuG(-nAl*qS zOKWi%ZuMy68@mY0B!MN7v-Jkh)q@w&WVp3MT?4HR(#Y0S%Ff;bWLer`A?iZoDobra zK55ceT1~flQrjBdTGdxl&GC9*ydrpt8caYeA}lSi8#&d==7qNvH|}8l56nGR*ztO& z*nwfWYnXU()I6Ico?U5<%!{K!DFC3Vh(WTPazR!-OwG-I5?cxofRqLKE|i*)5;AFP ze}0yw_i{;4qD~>?8d`l)#5>oXQ@GlsLY^x(6yIziU9m>H*Wx}1W=RVdN4yPzM+(;TE4<~ z7eGQ$0Mg!Kw0uvos#R(I0uAn>%$^%;{sOG+s^{wyX{{8rAzM0@%{d;GcIT=MSKKQZ zCHjPxg8FbvMJ?*A_R;!5MWm_JjM4z_kIiKjmj9_;-|x-MeY#G&m9gDvN$T0v6G>2Z zPd@qmkGr}fF5X$p2H5xjY?Blo#xKBVSHA-F!Nr&jjMpocWP2U|AJL` zmvT)h{dBEQ4`8=H7i~|Ib}9ikWUi?NOl2#hRY6MT1|lqM9S@ZLG;vqZm7B}Dus17i zw@SQ=dq8Exq_x`7n#@quy@BUwm<;C^<4kq{Url#bpo(I0JwE{WglrB(|z%o&v zmsxWr`Q9^mFBWwy`EUE6Ozr-v?EBu1g#5sFx!&cM8 zJFh=R{W+U14P>}Jv+W4vUmBbrf!k#?2pkQgM#p5*w&zFrj0d5C4iBjSu;(+Ib{vo5 z?71{KXFH6=36HMX`oqpJ2Inx4+iK5Q`|cg$TpG6Ob>x|oole@2^z8q+dZM7?yOYRkQVTEMbK6ytLa+m30drB~Bwi#H zpzNYC9y<2v!XB8)^X|3TYwH* z(s*xJDKu;-a*Pxj>ivYizaxbQfBfC_Gnk;S1(bcaJWS&4D(_?=>g#VwFOhs+SqY%Do>=v$ z9%1-fU_Aj)dk3m#7pX9!AAQlzrorv^x06jyVx}2L$WCDU%}vlPw%SM{k8vp=HJjx2 z;8xad37|jP|Lw}mfvZ?X~7cpa(s}vFXd~ByiBu=ubNkY`i&6Z0BcoCP(f9p z*&NJ{!MbY)3l8(u77>-?WydC{`@8}0<$GlPvZkSI8Yfjn_RUDO*v_xYPURYw-Imth z<4t+$CG563oMsZ^kFNN2oX&)P0^PK6B+K52v0@#|a=*XmY&q$?y46sM0BHe;v?9Rz z@~nygZ-2aLV@LCVOND?hzW74VIg*}(a326sP3huawU?Tti1ODuw{fLy$94RMs#MlI z+@?yxE9q_>SQGU%g8}((E&FA@O_^yu<ZOjTYweNDKcuL^b`Utp_&h=&myRoni-? zL=a{A7ysg4d@mmnb18@419$`9^*>*i&3XP*@$oZg^4?nfC-~O?Nu0A`m^gb5&aYJ< zuuYYAq~TqcC$d(4R(=_mCh_)f!Q^%i>Sx;_P2@&M_ayVt;X``>n31OTy!SggmnP>v z)E_3{S@}opV%ujcyr<1C9b0vo#e1XEQCo8-@3lD%(%4E9=!t)MZg$^i&%TxMKA;J! z`yBq@d1WU6>Gjps>+Pv2U(ptvwi^Gaythaw7aBVm87>p>?8(b=IP#sfi+4>a)7HSp zYGZZ_2UgC8ZoB(|Ck*NiCNf9Q(_DUcy0q`Qz47;f7-z6-Kvu2C(%e5l&^npISYt+xE^Iz?yd38(wTlKmf^Y0tz8j0P!r=a*KNHSLD#oM~fHaxu|CsCMI1-*F!wWOaRGx%hkBH z=v-1K-ljp4Kw>dCbDI-VSvqSxm+%(QG6zywRm^3fmh&r&lU_P*?~fuuS>7lxSdSax zh1BmH+B?=VKqHQ9Un4TN+{@8z9V+BIC7wN{kW!z`(J0b*8rgy@_f!b(z&FigjX*#4 z6&=?&S09rBy6W+_2J?e^=$B-Tya8MTb-N?I9g`|I)8zqf_qAD$A_F(6EMgF6Cf;wB zuYGrEMuONDh9m4iR(k`WR0t?EZvgzBSC`PK6(grq%T<*C_>xp=QIC`O3<}Q&UW1Ew zHH*@#kI>*ow*G!wR^9o5Qh_H;;ybn_E4dtRj@nKL+Fv$Jr?5!1fn|FV={)7Qg^++u zN(q4b{`E5~Tw~YoMEhoId6LWkM-*KxBY(c<R=KCq+{?OgGLl{Ce3 zLfl`K#u||Sxo%;*UXxi-mA8E=ZCDin9OZ#>x{N7j+XCok&Tds8&&lf;{iX<_p}K`D z8cBBe&;PUk?A`nK?>)+SJKHf^Mluw6#)@1Y^v#vU_lw`3vDyA)2K$75^(;31jc*?D;`IdS*#x7^|yY@G9s zXTP)d*gAJ4{*rK)x_V!JdrXr!PGcF!$nfkw$$S<^e)inVo-Lb~j3caT0bc|7?W~Qy z|Ni?Qe|-P`{U!>FlOBI` z*8i{;x`Ge_8kgmf#%fVk$J+(!RPt6~F+|kyHBnWsDqZq!>%(Td1CS^;WFv@rB~gzS zUL2B{M7iW%&(-ZeQf0cNlEEsEy141O86~WHN~2k})UYC2Uy9Q}u$-n(RWn0od@go@<-cdui)Mt=^T0$zodvn0JoH<{Zw1T;2yk(n=i6-5*OkvAC6FlM#Kn~sw^z2BSNn7&%Wqp*m%hO3) zlO9zl$%jGRc_>AIejYOYW7j;i62L0!-<<9&0b3+w>nFEmYaUkFW_rOb zy#Z{Nn(ra!A=YHb%9gbe(J(@fO4O^U|K-*@*J7b~b15G|zK2xPGV49+H?=aBc5dXE zEE5xKTo2`EK$S%`nL6m(YGl{pcM$$Mk~R|G=t#ur>1v!ak=$M*>I3P3?64e39gLGQ z?Z2z2_G3DeKj;l$m#pkkXZcsnD42UFlOrRKs;E+xm%zhCxpj*q-CKKCA1rn3+_nQ+ zm9CP~p({NE5>lrKfJOEJVhaGIp%TFJPd>qCpMEN;M!Ift%wULo**d*q0Lu4Gv=^7H zg!I$q^c!^u#r>Te3yny4s^!*}`fYr4Q#?-%w+2An01HYHpsY=2()mkI0ktNXR9PU( z0jyiz*yqXih2AsA$WGtUVX|&*WB%1&{ncCON4-8u;`R}m`+rOP!^qx1Zvo)B@8@56 z&MEBAUBi6AU&=xHm=E&G`7lin?GMKWE8Q1N8&FDx2cO>g_yUm92w!06?q9ck{549u|g-q0!Q?dKv`AOHcPx zzvF^;x+D;|NYuru&Pu)tl(o1PPv$GoldBM6bYwX%WXVNIlJO$%B;pkbT?NE<-&#eH zAnq(lj271MqTE%UOqlcPN#kt7vR+BlL$*T#kGmSV&g;KS04%&9np)nq-o+YRkyV9! zXX=@%MJX@TEvu!hZI|LbAPafG+{2OezYUKRH)huV;#qUU_uvgcK8q^cJ*(8!8-QMX zuFqb3ZmsoyF4j${hN;H7a|XjSQME8_54Kv7%enVrpw|0DSS$y!qlmp@0Xu@1TTrxi zpzfX0o<-zBxZ`o7B(K5{?`-ehqm@YZJx*Vv`x1`Ub06_=!Kefcwgk}S-UY!7MGnD*Y>!d$Oz<<<$CyRQivmtiy#|<$0R`KIbX{5DA5@qJTx*om0I|-xu9hD#UX# zYHcxkg48^&eSC*^v9`tlztb`wN!T|+ZdDktM3;884N!4kUdzKoSds*qyj@KZpd!g{k^U~p>%DIP+4oP8 zxTCfmcP7a@=^=xs*wLO0_4fFTePrG`mnvC^;z0e~3QaC=#^P{SjClZ}d|lb>zCl7vo0x zjMU+p%*T@XJSQ-Jt0$ATW}I^)_`7WZ%COUJ3y`hyfVf`NsBJ4C3OrH*(C+?QpXMqg znW2wtZQCAu@E3piFW>4G7mt}v_#0?KU&cwYpNag0p6vIO0pzh%0+4^n&B4!8WMr7>04OoHm{&^0)bu+0{eR^A>^>&U@Bh}bG|Tf3 zqK>iyRkGJ_-dz9U=Rf~-)t(j;`c2qHL#Gj?oRqJVTPv>yDB>EMrwDiX!1+oHi2vc2 zb)pC$Eu?Hc?&a&WLDEmJ5!fYf@_6>_nVXiC$MY8tssM1Sc62AueuL0B%9R<7T4Z2r zgd@+{`SolOCha&UBX7GEz^>=)34*$VLHr%kBFw|1Yo~HsWjGKSqv}9lKYDT%`ukE& zsITjQT$Ss0dhEf=Y5Lo1No4s!XyXwvK4C_rq%GG}+o!%#h9#>eKqjo#Ey;}k*c!35 zM6JgrsVu)h3Eh}O)|<&GcFm{}rAg|uM0+`wsOuQIe!z1f|*hNwkcJ3dUXK^b$vi64xQy=fuLp}G# zDjwq5B=O|%Jte>@J1~KWN+8vX1tB)D_UEw@)hDK2FYhlg36@f~;5qzmig8`5cZ(EH zPE*_v_fJ0eWZyaYPL;f96z{!5>e(w|bpgMKM( zbFi(Ndq-NrG?g6>zSHCq58Z5SGN@`jeDV?*O3PDeBCFa;&WqFr+Cqk38*>XA2@a&R zG$o{3M^b$+b#*E)ST4s)+sdQ!29N~lpY@gi`~r1K04c3rLKf~@i*k=3ytrMcdU8Qa zn=M=+zt^TI!QTAjH`SRhsTab3NLoM2vh%0k_M@BV6!+$-Q?0)qdpM$IjcEhf1aZ$JIJ-zo9^ z+4ZfOWUs%41gkBe`)5G9z;7N>_;RC647mjWztl;OsYL%-)o=MTnA+{uiU7H+2`^ZX zkc$_mx9@+dWvCuo#0P}?lP|vfQmwl-7yU_;xoy$4ii+{NqYz-v$(x#Uu5oSQyL(aF zU0_n3g%y#H+Gn_y{DQP6NErx4q<&ifqHL4^sLE&FNwa4uiVBNG*u5Rz*0ZqtS$RVEkfT4EotsRYXe%w+DYvKIv{UXD zbqxeQdUWLK`lE?BsBgYnobO0v`<+?i)fi`B{omFNeQyCh8QU+iS`t~7O}k!4H|Ei# zg9wX-ZgT1ku1mEz6$@ElWmM{vMwMHCtP6ZpuH}V8VBPa~;q=P_WfE<`ZOpk<#f?(D zT;yxL#G-0jAZ$tl025b9kEN;H8DsrVPCL{lf(+lUH-NfuyC*zU+)b_ApiZ%)dUz-c zl=4|rDn$+(Zx`Cpo;8!2eCNms+stL(Q_BI2`x39*=GmMn z0qh@Nl+tsbwM_V_JC3@jKmeAeWVN2!Rs^Wt0E(n~FH<4B2&?x1e$Oba`&pav+Bpp6glx6+RCm%Tr*oGeAZg@7r3QEHJgwf@5i zoq793UoX+R^Nke;@?th-^{Tts=vBLZaoj&!$<6+H@UFNyB-&0&CAZ`GCw0N5@ZMx= zOYKWmC0hH%VSQ8W^W-&BUfrWbkF}B*dwB$V_104RQ4}XwEZ<~CiU2@Sf-Pyhd;gOn z$=g-}ZA_Ms@a3yl-FmDpBY$i9hrW&-)YzE&LA@-mlnBJqFV`{i_u_%-tyH(#2waqCaxv~uhBq|%Zk{aabMuWNl76%Sf4{Trd>C9B49UFYUJ zs(tsDr}B)izyA7%H*enDI8k!IngDz!&RZ`48NO$L1Wo#nlmR@e|1>xzk^Y{rtdESd zt>bk}GmeU!YrQ-?S$WVVa{iXV@hooS6#$qk2VmozJsySkwB6RZr-Iwm&3j(RiL>Xtj>XC+ zC*4^YlXI<2a}nn~84Ir*-h1*rfNKC>Owz&i_4V7Fb6%EZk%V&{*zM9JN54oY&UKjz z4MnvmXl{&JaGMe9j$A$V6ah*c&BC^!k17J#vY2x&sXv;e%fat~dvP1Z0rmi}dCl_| zds2Kb9QmrJ*|lWiQ!kSS*V++=ocE48oLP9VX1V|V-TH!$7lZX1%uY|AJsQhR)1nYfunal1%nrSRa?1LE0jR`Ba-R_x$^ zSrLQd*8h_rmf9G$T4o7v4kQvMZTt^*C%sy>Y0}58=n;TamKd`-$!TdWP|mB`^P<~H zym4*&gCT+5_bt}?*Urc6_6JtkTi!4GEOvd1B{^ibM2?l-279tSY~>a8o%@)#7rVQE zova+V?=4rFofiEkjpf@c(G+r{G3pajo3@`hkVz?10;p={i24)O=ajbvKw4FLisFE3R5Ni~ zW0OA*v)jhl@3lQEKWc*;+1^WFk2e6I^&j`{4S;g3^n4CtCo0XNpZnf!ji)k>sC{#m z@C^&8m`WhmciwWl_r%5oZfGo_~hE*6Qp)UO4JbK)KnnflkVwr@<|UUto? z9rm|xeE%s85*gp5S}uFkK|iqb=cGQY8|bQ%FbvUH&OgfwHhaD!wC9y7&((ICs=zxv zY{&xj&DVdWR}x-GNQD5Y3~*CwC=~(9L$%ano>eUMSx#k4gYuh%*DAu=_ol*lsQzi; zac^G7)dal#@kWi6>$WV3$6;%K`NfxCsBx@6W?f9U^xsK8#cKSe($QAzQUO9;ujVu) zWOce!o*l{}UG7d+p`QFN*JiZ-hrnr+GUnzfpeYHo_*~+VTfd6pEud`;(2jLKW|RaR z1pr%L8`mlb_f;pqXKQB$-NRr1^d=mmEa^GWpoD$S8g`pz}C@CJ~U zrR`ZYx@LLqw&#b!kwanDnmwJ$(ay(R#0vPhhexui2K3`s1l@@N(%fJ8u1Mp!+w ztou{f{3~&Ub=#FfT4_ai?LB}!1@Omb?O&EEPcT~*c#}L6om^S}rt9SD%QJmC>NkCd zEZ_7!YZBZ6lIS@6#WPNw?zzSo3(_PVbH;vF9B%+C;V)!mAMGgv6h(k)N}RCocpWWqmIdiOc!5U zmGn12G`*aMaGypbLr_G%%)9n8+>g>aY$ejyNK!v%s^ziP{#4dhDeG+jwATP?@%38m z?q8?T(g9l8|8ek-^W<84PW~lqTl^>IJO*J;5y0cwy!+|>VsI^) zV;MgS2X^2?C;)6u?GDH%^XHx0^S4FvFFWFFdcasQV5{sHoWsuJqiudv?rhllXlz}x z)#-S0YW_NrvuV*Fycl`e(Jjt(-o5wQVSx`W`|kn#!;QAW^_w?WN&x_<6KA=IH&X;4 zd+kVmL>S9!>Y|YutSIreXeRfTuEW-(wW*wPt&S?>g6dE+lz zXgquN3^zB;Thp)}&prWue1H40-JzmKzW$rftC5k5xM~tHNROQB<+|I}bI>gs4X)XH z?nwN7UGtc@?MQIzIy!Q0GBFy42iI)oO9y1{Q2#q2hUbBg5Ow4rE%e30ZRd&|OlhJm z)Cgivrqw7nLrU^up=50UWuePg%B)siLiF@%@wqMP$)dEZb$lZk60<&n5Q0jR%Ua3u zlD;Cx6JRa&t5p*(+LvK2)1=L{e`#ZC^{!h2AR&o8qh-CN8`9o8fUt@ufR;iNQSmUq z?p0wFX2lqF;Sa^YO1`tW&V}R|xn4Kcpi}GrLHO3z|DK}n$ojvuV^eV8d)S_xB%Wh+ zzqx(U<_63KRyWPLd2m?QJN1?U{La*SnW?$;=S9=R3&`S0t*gXJ?ObC6WN7 zOwAg#EfydU_ z%v36gCbk6V_Moohy(tT%3v3ZCOFNRF=l)W81|W#=`W=wvsDxT8ysHGDZ9urc=k{%R z$u$5e3-fL8C(vVZKXrY~XpKxcrkuXH4(7aDtWG|-Bqjqo6;AiqR_=2 zi<(z}Rf%BDC)2W)qby41MckSN`2PLPs)PlgQyvpa0I$CI5;^CxwPHQxld+X84YF=r~B0tP7|(ueRl7;AqdP`)YsSF`SC?*O3Q zJ3!kGpygk$6USUo70#sEz%zQ&#J-NVk8$6&{uvU_{J;9EssLbz!FGc98odGlVeAE9 zcI<_1r=84sKrhA1GSJ|>Z3Ztb+g$Se7Vjt1qro};lkGsx@vLoNX=o%{3#Flz^|zI)pCl6afFxNDs} z&TN#f{UB}6mGreEqThC6qjHY!^XOMz=UxAYL7h|{*VotAzx~Z`oEphRJBWokVHM?F zV~#tQlcs^x`J8p0MoyNcUQ1LhbrP8gUlo^wBZ6ApuxZNHavc=qCM3IO-;nV!ror`ejG)Mq1 ziR;>?L+p9-&E+%1^>tObkwrDy2`64i=dAR2P?E}Zd`T@W&WpWK@!3RTivS*GTh9Z+ zP5e^pohrFbQl3y019-td)uv|4@oF1N7+cK~yAWM!nCrPVhaMMe@|9h}H!IYK^afBB zHE!VzAldM6yf3Z)#hk(_JWy*_0Cg?^l$E8NYdP!Ub!pAjZ7Qla06CiHv{#Q1#T!KC zr?chdeXG~@1z4$|EvoYK&l&U=Q+>yDn(fB%ikaR3r1QYh4Ft6{Hm4<@wfYVU;!hPI zLG?W(NhSo`pnX47aYP-+Sfi)bbY@EhruN&tN(s23LeNWspA({{2*895yO`FM zHZ~8uB|usgvocXp0$8o_r4m3U!jf0TfU49*tvJ0$0^QF1J_1e}KS8G|3&q}Jb`;kRntr@(vBe70oJEErM!p`0e^32w z0UTMfscp>ak`1OE@@nL`Q3mpp7@pkG|86MP=&bk8YSp{Gop2k!1A});q?~k%sn;A` zWmuGL6Mg8GQc}9ROIlD`5b5p^kdCE6x*McZy1Q4p5tQ!kmX7_N_uC))bFaPTo;fpf zrau3xTxnvb!!Bh_uduy9hH{lgB_0w0<_i8vE}9yI@o2J4rf~MJ{N?X)HSB@2Z{H}< z4D|bcBA)LM8vb>!_aPioV_3R;^Xo}L(5Ao2w**~;dH&pRYN!fa zE}e+S<+$KdA3SvC=!l~G`DQ8SiBK{+(4YVdr|*kqVpGb#?x34Xwt%l(Qs|fvq(jg; zkp{!6DENhHrfy<=i0%8ha`skTtO!6GCFkd5yJXl@U5v=N^~3DmOtizzU(T|Chs=N! z$D@J`u-Gn2lKbwwx<>v3F=x3M5vCz%T%}U8H#zI)g1P6R>~Zu&wDa=+>f7EYTl|Z6vvmS;-eIJzb(#>XYc_Vm#^2{@tyJ{*I#{j)@_# zc8l>AT8vwx6OPN7F|PjxQh2ZI@KL>w_lIY%WnzAc%pHczO-W}-Wh3l&gv(>Byc1I` ztfk=?#C2M>ocn$_?V)PvsoFZzn=s^rnST=^aPnJ3IJ|!hamPdqP8~P5N+ZopUN+d2 zc~0PGK6@En4Kwoj%arR%c}vvKWaM2@mCEMOHp3_G+I$M_VX4H@)t4h{%e0(Mc5m!e z;J7{&dBd~Q%CtKBqt|%+e+To*&|j3(?_eqbo}23-Jy&Ke+BkeVcKWPC`l^2h|hJ zowm+OQ~PYq6PvL;5Lh$TM^OFqja&m^qWbfnbrI^9pz@r#aS9w!d)~8=eWs1a zw#1rkUt8WYJM7fs1~WC9E%mm*S8+eX8VAc#D(DWT$!A@~t6OC;R^&an4%D{~)73s} zl@yZr)hj`G8a?9$IUc$3U@AWJYD&g7hQb@7CugE84fp@7Tvnz(?heai5)frR6)USB za(v3DV_Q$rQA+varYa#`vch^5;~s2Pe$)2bnns=eR_Qb0V4J>SQ~J8X=zZhJN4P}M z5&}9POf06IQC=VzRoNz|rGXp2u@Y{UVDrW>B5>!ypxejq%i2og(go{18i!$L*+G>L z-|ggPIqhIupi64XGrwK^-PbVhZ=t@KOQ)jgdyMs6EaHmMzYR%aMd4Lu3&n5EQ=Xc2 z;mz<)VIPPY*Z%s_6DkAKG!j=X(|RwO!EkxGNmGZL5ahTk0!Wre;7nSJ7deXaFG*ft z-4J%v4Jp=LHKgx9e94(qpaWhp*y=uyn&R!pK~b6lKbm~(QK56d z%a~rd81!td8+H*&;d{67hmb?;nm+g|U_=P#&f7B0`xet>SkAc+RifUsK1|-Lis`$> z3}ecj8}g2uu7A9qU9)ImS4l7LD#j?%OXa`lj=nlp-=!sH(MK~wpKk%}-V1|~Bp)qm za`&R&w@sC4zjig3)~dqB9Uu!}YdM!L9ckEGW{r*|UcFi$piulyZ>@E&Uq{Coy~fOA z*GEy|PKhIk%)REUgxX+VtI*Wrt^5|k)ayvQ)1Nk;;TkJz)?U-}G2CgvGya-@Qlo^l zS9`dSglY4~@m6id9+`g$u7<7o_=ciw^I{UJ&dOp6{FneAHJT?zfDZ4;YQs#s-l%yp zdhHL1{#aL<={{<*s5eYKVTn-Kn(j+cG1pt9_eOh)sC;CK0^UQKQXj_Bi`z?P_(u&vtoq>n0xOV(in=%cpcL-Zej)haM%c+TKxA-2LiK@pWzkfXMOW#2 z(Btv)gN8Nn(hyYd@l!cKsqY|=U(u(|!I9|(-iC%XxrZ!$W)a{;3gEBGui8p6C8;;l zcp+vw7Ib&y*bx=&q{}#Ux|rB+V9K}|K4UwYIHY}^oG`;~m^s(|po%E+(n+<~#X`40 z3cPPEf6Hp3AFqEu>WD*dT0H0LTq+MYP6~XJWr9C%v#5$dgsQ$~_;GyqGPv0uh9ZRX z?8Y=%zc+btZ^PfBybMv^`j*yfpl0bh;eUP5Pdv^oH#QKASM^DP&eQ z4^yIyg1X@avLiOR)k z%I{1Y3=T#^J6@_`yY^k7u*>;s@DyRTWa7Oy{tXU{wK-Q4KJ6KaPf?)7QU?aKjmEp_6r0>mIqd66|E<1;>2FI-J(Qb{qXQG$M^Yq4pti17y(x&5-A9KlMWAc7`Q)O zX)jkc?6^~7(`e&6FaOw1V3|vRr5jqvyR7 z?a~S4U*s2j6Ftp9Q{lFQNtvv|I#TB#!@bN0TJ?TFgHzTbua?z&=DFzTt{#G7zwxWg{u0H1BsG$mNX#j;woa9F|D^^=fWf7~7 zx(J<0#|#R~+x8cM#rks-NKKFwQ!Gcb2xrQfP+1qA`{&lxF7Kus7xM(Keol7Cg}!)Z z-$9|-j1ca{?aRN^yM@uok$bgd01uY{)gDmig8LATVMEwTtp^=xi_^a%<-AAsr z2@kx92i5%&A*e0B-JZC2Gu6JqhR=tZj9H(O>8LwYXJKvLX75;;O^@2f6aP$p7V!V7 zxs*cLs{eOA?UC^0V~(`nc1~&`>|u%ZqDTO+zK8iecEiNacgHi*4Tp8&E}fn;9%UIJ zT?19$m)!PGdfV`O_YU_x?=bbU&$_&xFDGG7li~`c&Qp0kqje$*OyfgV*DTXZ)EhE; zMk5WD#Idm}Ax?*a6=&Pa7TLlhoyFsB!%yeE*s!_o`;6}UwSrq<-43ayKYYZwH!nVn z)X{`D>?)?+t?r3`W$(M6YUUJze<*RQy?N8Nez3o?`13#a9Pi6vamaB5GQ8Kt%k!fj z+PZk~TCHaUi}%wt?ESaE+}k8;93j1Ze%6oLr*#-DG~Oax7e zTNMdqj{4m1Hwk0)izGlNNfuL;xlAw`E8r1Bfe}z!^+xs#kkCF?9PjtmZD76Vsr!NY zdlgQV=k#C2u0X{zHUyz&okKoFPj+N_m}nWhN{T5Qq(lkOaE1tLMCNx zk>qnbbn2=!oB51xrW4Hkjujh1T)hAMJ)~`eq*|N^YZuYvnhn-Br-q{ zW7P=wD^2x$i$CxB{xQVbskC*+a@&*q(5p=EA=PC8@235z`L5jW>RRI$d;k<>Ad-;d zyxj0vo@K7k_2~($(7q2(NVZvO;=D%Bo9jatQO}!+`NHz{g|n0zll6k1_d@NKZpKHe zRPCEkAh>d&^~%!Re{5?qs{1ao8!E8oeKp^F^!fatILG$_{5r*b?l(>4`~5wv3hs$w z-3zGcNPKEUpuBg3aF{4MkmHQ5B(@Na@k%v?LzvWZV&LLe-bV3*@S-D6|5*v;nTue!t30{}$PYNMc$Xfr3 z%)^b~qBssJ;wQLPSxr0K{uVq5SHg--bCR9KuSC`&8cgkEJTXm>Ul<7A(wcgrwLe41 z(2n?iLp$wSSnU&e1K`=m!!8P1xffJ9SN)a8NVf{#{i!Kha7GUo%nAe1)cICf_NLU6=S)Hw&YhCKl(~V zciyh{pZRPG=DpE2#jH9I;neAiYVn<=(L%pxiz;{eL=>$J3^I zvw_XdQmw~aKe{`~_FM;CVOQ*L0;|sE5AZ{BtueTXDselPi7Gk?&Y|yC$6Vi*^(-tg z^5e8H+B%kbL9{2+yi$rS*GXFF$Q9tA^@>dwG#IdjUgvFN-QVIR0nmP^tvJ`=+s?fQ zQGb@P9Ej)D%}IOa71p|v8QSB|jQ76M6N%ZpbIwjwDiKSjpHfk@u%{OTpwHj8&&)3R zM#O22$%wvx+#5>$5f7WvL;{sc2>R5e0q;+eV;mf4Sb8b3r~kH|pP%{d9{yZr6rW^vys3|F!YTR&_TN zoPJL}NxwXV^gmr&1B&}|?m437>Hqjf@LG8B6x#M=sdG|FKa0feI3^D)HW^;F7k0wE zrt>#T0^Y|nIkXbRKabL$mh8D1QHZzqGoT>f{YD3L1-Mo&pA6^#Wm9upzW^wm@hqHI z-Q^QSALND4CRW;ht;>h!ZHbmmwkRitA5Snqe{Dm)vONXs*Vcofz{_k@h@PmTwnUHN z>mYWs{qSA0Ylp14F+vdG10>cF0*7%vkTlnR)i4XEit7(FP9DN%fk?@EnccIqcpCfk zK}H?xNV~A@0hCSzUQ?ZJa@X(5)|K|#c4=?mwnYLnJ>^ZxaAC(We7k4?(1l;}8pl%w z@yz+%CC_ealLy(vD`t|XGHaIX=A{ai8SZg8;lkp?jF>H!Hsa#hLRv`WV8ih0!$gU+ zjo&J2r}aqwbN%ZSc?WL_o^{{iuxh(o@xAdeP`)(3bgr(}_HukeEGM&@xtgXXNSF|0 zM_)XvrcZU3Mt@e>+D=VEMk`ykU9<5J%Hf926jYs@q7;X;M|tjZ3Sj7ZVzfDj#A+0^ z4-}3Xf(`})q1@0{-l)@Flj9`8oh?(}ol`4;z$a`Gr1{&PFUaebFD#0?wNcIBw67Fu zYnt}0*W5_3#N>lt63Rnxcf4LpIEEhjJ9DB%1_}e&~1bsykZc(GS z${SB-HpOqLpTEt*a?477IV0sS6Zo_)=VzH_{|6w7Wx4U!D~#!DHm4cCW*=Ik+tmrp zAKog-1teoCHlTWJ`_WWV^pOI)b^7E%rI{n&u^5^jdh_*iQMvNW==|kQSR7kA%5kWq8AMpcTB8c-&w#ht6 zaL68tn_gh+)sihr&8=hwHYaW{p2Yfejb=vK(k1FR0)VxR2k3P|>X`u|3a;3m&GhxZ`xD70Uxj%o8|>0m9@+>o7_$!=AHl)}0L_2^=bzfwlq zWdC+INy1&EgX91lk%!;4<#2M|cmsQYsA=q{)Cwg?xHal+$Udw-a;=ug@wlZ|Fl`kdyTux%L8ip2ag&p}BWt>ys!uY=_3)UdSN_tM}iN2lL z75w@v{sj>gksY_5jhi$4kpLQtPYfSd1%76z-!>%!e*+6e?C}W8w)ERh(-aEN5@0c! zAP(Sh>fpSqz^UPRX{u93k~#7&?6j7Fr!dVx0U?=q3KgYxpJ5=yK#K`<0vgjJ71Emp zVs+_+#Bqskͣ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

%M%^e?4!GnRU7hhptDH2$s`ox#>U3ZB zJ~pdSX=_FGZu~qjQTEvoN1ma@>$hCvx1(E=a@Gxa-EY}oGZ?54fno9Ufw%>=h)n4| zHy&tNy($jPnivOXI(dB73EDmd%9G&`o)K&Mg()z<^OVuTp@@2%Z9VXU-0S_VTyDU# z5Fl447X(Fe?I9c3{=jZBpv;08$rMX3N07~<5JLZvNhGMO^*-{XwA_*B2K1*|fFU|> zc+0v3^41ZFpyroObD3_w!HJh3$=JiAPx;8u6R%KZUYsK2k6}@sH5_=C{`5^TQb`Xo zRPPCrbF}#%-KerwTl_*n_GaO6DnomdcG&{ax$cyO57tUqY?{~XpBwgbJUqVPl&1d1 z2XTg52Oe`S|0N^g#FxqJ*F9Tr=fpZB{MTcHlZ9Uz<|#SJhH>vMHqLHz$+{4p~Kq!bdLV0VlE9WE6wY0@ao< z5Vv}BjGE7Wvo_#wsP*5wq(1MM5yPPqi<{A=P~e04EMiG|;Em%#D1GS*`Xi<=rCYi7 zorW+bo+Ju>bezZU)iRFX_xCTOaS6#N-rH_`7zdiy!p=KSNJ*?ve^d;HEGGXV4y!&T zC8Kc(+k?w@&B)rQA5M1s3&0ALo^58I4~&7-vI4;emy!y`uQX#wzOZlVi^$mkDB`9+Vw+VMc0ALXQXu99=*Nhj;|k(dvXRU-Td zIm`q%+C>-*)Qkk$Z1Y@LpK>O+Xi+G6z5+jzxqf0u)?Qgq@v6X{?xRC6WYP}jGn8lD zyTXPl8TA9o$)jY4->OMmNRXI_kU%6zG@KG3*O51#N^WD{+kQ6S*@V+<%s-rKS1cb(X``My%>+W&eh2*DKIX<8{c?H2B{^T{B%7yb&0VF;#U2Hel$k-Q8Ec$@{fjGqd-eio6;zOxC*Y9wx!5uOT8n{w~m z^N+fB-zEj2h(C3mRHOviGZs;&0{^z>VPytAL;H=61lnc#4V^J_!fihfAxD+_u_ZOJ`3@3wnyeuJ)Je@G#{y#yR;MnzSm-Z1s#IEM{ ztqjpWD&XJar*{cH9a0{YY$FOhuQ58Uy-;U<@b(%uQHd3{HTqTaNMO_6m0nM_@9$}_oW7l@ULdWa0&I+`?qdZjU{F(y@|i-E>bqOf`!U=+C1UZ z(Rg4_KrPZOMYAqMoi00=T`NFuSkZ>FfofZ3J9c{s5=&d4KBtOY!HB1dJVW3pTt9fG zpuR&SPdAmIn$dQEs_M1p(`^m`fH~z}cM4ArD@bnkUF+fRx_7n&gv=(R{^2krLNtWt zf$M0w7|&*Ppo!hxebwx`TEKIFT4IMDo6!hEmi{qRv$^>{*om6=YPj~_kHnl64Bxzg z0nVV3RDJi=C$E1p;Vki8Ew5SySv!GbM=ic)Byg|mK16HMpuPhtbvGr3j*IfQv;5G zdi)h#nPgyP9b8#@;n;RNOx1Qj*miHH!GE69HewISEBR(Yl-m3JwSa}nZIlC!I>Lvb zg=>(6-u-eJ)BJRyTjDX^JmjP;uFr^r6ck1Af-%I!<9g9O)TeMi{+a~h@dU;2ZsOxf zkC9(fo0~vQNNl26n(J<3tR%xq(a}<28B$%jOoBsCi>`F4ASqgj`E&sGQi0*(7Q=kE zcO^C-Pt=X@E~%<#pAiNBy9eb11TjM;TJMcXaO9V#+{kVyg;_Pcq|#c$S~m$x&+x8T z^uc_(DCA7UJju1aD)wDpeeo=TJooaGuZgsO{FHKEQfTwNRKolC=&B!1-5Pu}DfyP^EGDk$(;K{8trG;;HkBdZZ#L=_1Tqdvivs@5&3@ccu3=Os)fo z)-LwAyr{&YoZ``m*{zOsucje7BJ35&A%Lh22mzqcy%1u_Hm5GcOI1`vzJz{Mzb(5j zUB0{fZG_lF777A9J%1>0#8n602HFXrStb4E8sMW;-y~)+WW6fV&>EdfR@q#S^jjpP zCpzc!JD0(-TV8syKn_Jlg`kWZKlDovqdifea|rXNW;=uF)uL^~c5pMr11-`MNo1)n zhyu{|KJ7XTNx>(O`yzTFM-s>YL~(MkPQQMoMaHv^%Dq+?krL4D(isr{se@7WA6}-o z`74h*bga-{?*EMrrg!ARu1|K3Rb=@iwhxVSQ9ZN$j3hkXmX&vpYVaEYS9sLQ+wV*h>6aEnj9)l4s`_o& z-|@K#)uLg5P;Z4tkUZV;)cO@UVn_Hhj*HbX)MTiL{ewd$X@D$BjZtovZ60w3azeODCm`(f#GLpRW0tLr z!=dSKdxc}7DOP(65Y&x=1G1F7M?0WCcQ-%9X>%Yc-wx{+1MqnDqCh-ix)rvaO9o(P zODfu8fv8)MbFYFy8PQar0UO$rgk7P~(WishwnZ}$_yh~#4?@2BviG-uVfy6yq zl6iw>C0VDW>f_PUz^f02tb(pOQ!+-5ElW(){K5V|!6T(&Ty{(vb7V~|LhId(z?z@trpL!F;9yHlwbWoN4lcsY! zbSfnh%+<=>)XSW*iNp?HjGwZkU!6Yf&Lqd?LFJoVj+@}VqqYAn9!3z_(;D%ec~d@K z++wWZPe-c6OdqF_?dZWw;y06SwSl7ktq|aqlqnTtqrgH2Fqm4M;WA%RjC5h=2-n-> z&8B(HU5W7&B^1@l$}!zzQq!!$6zd}eN}(J;#n#wasS)%yeX{Ae)mtfn&>ymb3S4Wj zT;=2y@7iAi?G`_qd4CR!Qo0DTfgt4me{l$`5wh_<1o*5J>oyLUHH_>L$)N%+CwF>n z@Of?j-4wrF>okKuxRQGK5`U=L&ty4c8Eg%qCIZ<5tjf)*2b#Gh;I>DYaiXKqsv2JI zg~ChBZYn4^t>+C1Ge_?JD*%Zk6H%WVCvb1)bk4pQx~!O*1d=h+V~=8?!L!8}jZho? zxg0=l0OMZAlFfZAi8U0=qh}yQjzVAYm0J6>AmYxt1f^RY>Wz+tg<6-e!MgxhV(~ah zC3>LJYmMdqfe3c(L{VGO`3gT&c*#%az7}mr2oljZ+^trZK(pLXFK0 zo4`<~FjF*;aTnw}U$A00DO7gcdn{k+&-Tu0mP%5K?vMJ9#7;|jWo?L7EQrh&W&(Z5 ze@7Bd!UIQ0Ki%duXy%J^eLf)hFmGrpLYEm4;>U(FFU<|$V^(C)d2XL=&U5}ySVV>n z5CppvkumC@13Z&`#x~dQ26`XhBecbl_h2JY6xN)9yt=Ammf^F7+Teu+GLTg)U}U>) zLV(u$O>QH9kQr1Ez(s-Ny!*r;_DsvAPfhujiE-x-$?T8jI2Y0nvG1$P1ZrXnO9Tfr z#FI8<{aBV0CaHZ$B-EDOXu`0-h~KTm-I7Rw?UY9Dez!S#^(GDR=lelxg&<+ileUh7Kbj>>(3gZb7aFuGKW?@yKGek6gec=rfVnunM{cEXcB>t z26c2x=gE>K{7GyA(kzQ-;B#V6w|GFp)~8}BB4=_}hC_Fqf~o#yPf9<$Sl_2jwRvhv z-gKFDd&2E~^2HM7ZYe3mCAf?{hwl8I48@ZXxEhT*$8I$9xO~w{XR#` zP6{PWQKjQcKYwb&ESOasJW@kNs9kzN$1OVjrq74O6CZWSfHJmK&JBO-R^m4qB$nN_ zR^MOXizq+9U{wEir0+GVT)#8= zeL+$V`tCxqHgdp3HJaj_Uk@KYe!D^=ZVyBQM}zM#Up!Y4L%YW!)RK)*Mrc=plC+|q zT1(LS)n~YX>c)u=pH??`7^s_~;}%ljD12uUw0#IJ28b+qyr0y*Ba|tGD#b(qMg7s` zO!>vYn>^X0d}v}7wsY2HxvuYD9a`Ol56t^bWE3I5FH*g!AVcGpz9xA6 z7FOq^O?7vBxIlBOdSG#a-+P~pr?cKbjWsA$sVu{IU=J7(R?P}-IwD7o^e9H#|#~DB)#7K)SG1DZevTgFo;vxl=+w92lUuOo@gVfedxS6MH}zH z8W4bjJL~c10VYpAU00e+F9f>i>4i(Z%lclGsK6XE;o~2A@UfJ*F&rj`bC@@|!8Sn( z@qBwdXFH!t9y1E5z|?8QrsO+5fhY!(^7If}1`4(NOwPe4cY0)X=qu9#4#kAW@>whw zqZkl_a(CUMP4v(-)yyuv760;V`lq)(BV(s2^1dmfRRZ6OEcv=B=+Kj} zHa`Of;V;|&0`nJk^wfP+JS(DeR!FU9qF;Vh6Bky_K<@t{ZPxfRy7PnP57z-$nJJ$k zHB$_gF%7UtkMa+&)~a-leUAA%1-Q5KR}N9VOF^C)86pdii~caa!l&=QF3wkW4(2>tl!Az+nWJpu|9% zRL|aj#X*WgE$ocO-Fm|a_K7Hr9j2A!h#s*+vwl_oJ^q`&M*4b0m*n9gR`f({c`yj^ z<#s^aoyKQRPLn4z<<=D(+9BJ%uf@5BgIJV^Z=N{22h9^SsMdA2q!nu?t^2fHU_-Mo>yCPk7 z|Hlo)cn}{0ZkJUI)xTZ#$D23of|Lp&)C@^)P}1Ja+D8{T&*}%}q|1HJVJgl@{x)X^ zGenv%q7F5+J`*XN8BwN3XnW$XRa;atl15bJ+PVu(Va=9pOv5H#+U+Ks<`DI)=Sj8S z9|HdwK9rZCLP4v)rM6G&av%u$eB){45wdg=X6*H*O9GdoC^Yh3>(J(>2T`gZEAQxA z8oQzE%%M8-XqI{i)J!7Vl2b^@?lr@;u%!>d&ktR-`X8uQ{IFJ@0<4h&X!QE6KDP)X zpKTF$zg4nUk8x^>;t=4F{a!@|cwK9QW$c04w^2Zj^2GemRFwyftxqmagQNWAlB3#KZ-S3UE^Vp zpoHKy@I8ehT%=!j?dfWp%m!t)9`)6cry!@DjBj+V`s4qY#T-r$`!p14bTH$tqRnTsAn zON~V88oy*LueI4Agwf06tn4F1n+2j;fZK+G7%0A$%yBk+j*R3zU!zeNy34d{7hQ73qfpCB`y=(Z=?1oc7VM6Wq2-y*A%VGQZUbR(9K~BoYLPBM|uOFJbni zm2qmjL*nq)lqBJpVQC^~qlf0}!OkqoDu|VNhE%#{v!k=$?Nb|N{t){ zctqJ+7*HG~76|Ee=d3_$vC%$01EZJ@Vo4h`$tCoMplldu=bYIz%et}tE#rRMQO=3xjkA)RRpjkf_D}l#M8qlBSze}puvTtEIwwB+SUN}$eox2a$Blb|olm2=_w@{g1V|81}Ihi=6G#03*r z1hKYNt1fY$956xMIu;x2wxSIFvAq8iyB@ew$DTw8tSA}XjZaqS1ZhYygok0n+twzT z#)CzOK%iJW9LvjzG`08JncRr3m6R5zC0kv8P2uGg6|<0rc(t>Vr@AVeN$Yu*H(cuq)ZEqi`@3|G zxsT0gb98E+`NwYiYKPy)^p^OqvEz99D+fx>jpkch?eywk>5NYlLj~*zy+nRe$+RVX zBRAhPcDBZ~xRh)@373=pLEU%vIql67#}^hgO;wuc*<805zESrDZ^^^67$5NLQ4f6G zJZ8bmh1D6kg*JcW-%`E0n_qsCbXVLJ0st2IHYWs@kaH!R(~sgsVeY8~=U8ndBN21C zzxX8dGD_J0j?U^K{*0WEfn(OvRP{Efi|Puan0epsVZagTIErMw^X5R{F&KI3JO}b6 zSL2=tlMDkyvl=c|iIdM+i(fP33=jl(+L6AE{T*SwT&CQ5HlXxABq%XvWxIvEUF%9I z%|jLb%d2%fE6fRTf4eF~ggEm78){G9$3;{ST;51b^3CKB0B#~}s<4*Hyc>nh#;jK; zh_;g!MxIQB^~Gs7SWX5=ZX1g-v>fZjdpz?SuU9%o4ffv=&pj4tzmva0DbTPvaMv;7 z@4`Dv41N4`U$>wG*2?Qa@rvjo@gk+jjw51>nah!5dWP&xLq^4DQDmO~ItH$1nHknI zP15ijvx7g~dW=%483Ik}XcswmiwH@*UPqjrdQPTV>zeMYrmZw)EYqxlf zNMILLwZyIHP_yVT?yv>|cXyvS{7{InQ&jJt*xwkl_qvRauSub!{KA)>uscWi)Fijx zX@na7vu7-zjZyO6#V_I%qr~B=vc8{ZId?y)^$|F_ZtMW<(zXjItawPl1hk?6Pa`76 zy0J_K3&NV^iLWN^ja^sf+pZ4~{}3^0O*t8(Ep((|u+NsIS*ryGT|A45s?4+2mRiJ_ zr)%sqm1us9pv>DqZ%uKt|3vY;7DO<)UQ7$wK_msXKoQvrAKa~LZM2Fm6*NBqVP6;W zTi1ckpgbnnX4vbetx8MjZbGhwa<4wj$etO=DW_Sb?dL^G%sp7&8U3z( z{ml-XAF{4L2pM-YEYyQ2%nDD%bwYqTiMt)YKpo%&UnIFLdGdlXt&k>Eni`vrh-m|# zV$)_)P{K~zngHYKSnQJwR4l|B0Ck24X(u7M#1gF+@ona!Y?xD!sCnRiuN<*A`MM1c z$}xOaku5C?Iz4v3-RfTWQwz*tBiUYlldK?#8CWhnJXw_y^1XBYQN91l)gWc(xDo%O zIJ{v~m`PaU3PXLK6k6V+R)-nJjyzAg250jky&8&cm4>zwc>p~C6 z-t<`{tvy=&{~@I9iqG?T8>0*(X?Eju(pgS-*rt*stDyppLX;Z{AnwakQd zsU*cHR49I~AUSQDawlw#oa#pXO;=@e$60f(5X-r;FrGprUkg-L9A)3fK0n>6WUwt{Cw(R`@Cqz7W?D;6u^;KO z8(#gJ3sO$zd1p=?efTA#*oRCqA_r=TrTz^Q5B)o5SKDR%naA@CwIYI0v-HZNm!d{x z4p|-G`?#V^_gLAsqu+I^`H@efM_S5kW+I;L*OYAwaxYiEi#Z!YKYWt$dV7gMIGzBK zEQ4i?dWnLjfuw}WDraXARlIU~0c8Ql?9?p~D?{Ms(~iaDp+_1kz}WFHF26IDV<;Z= zw08VtgMV~&ciT|Gts6NbB^_zb&cw#>EbI~fZMXX8Su@fWJJz*Sc_>%EM5$TkM`zm2 zn_r5xb9Ba=YnQ_x9lROYydFRBcD=Z-)Eh%?Wob7ZUqOVzly*0Cea zeDIvj`cJddRGs2TrPVWE8O|?Fw=M?+LwR@_IYL@-rGu-|!=oiRz3Gi+IOd+DZ2{|%zdOUWj>!63oA$u9%U zoITQ|vwIQ#>i;)WnDZz=<6~f%w4E^@6sSi2D^NoXbceoUMSh13ZS$TGYOrKhQ#_7Z zp(j;}^c~&7m0RE*Z1)RHw8xT^-3bd~kj!%71GUR|PEVmyTzja4dgM$_37F4^Nx_Ms zP&4-&%D89X5<=l8BWB31e*am_pT2aUV8)s(F7X*<>?7TQCue>ihuZiPZOwgIFTY00 z5@ZquRmH3)dog;qNHD!944>JS!(!1C);ze3T$h3mQ)-0FZp-@F? z=wFUUcwd*fjPLlHIk4lC3)S=d(x7wVsp=x7VO9$@*+23MJvOBRx49&)fkMBzXdbr#C=i? zct)1hb`9}CTM1%N13F;GZO9e9WkiG?2rR&d3kol8stU)D$MYyV!u#Cd7jX z*IY0XZREX>tib>&aC#h!Lv}JEa%?^i$cjDQmVlu*pKUJ#q!6qkPfW<)^iwB7ThR+v zv`B}m(6syfE~nWYgWc;Otd8tIOSHzj%UQdI*zP6*91Mo-hJLyeey}e*hO&u9Q{8;x zv-sI$**dZ93o^vR=kWFK31GAH#1iH}QCFaNH-Ko1o1uNhEyoh8{IbiflUB$q`wh?~ zddF$~B`l$;vO3RP2^iSC>&xfWS4g0$S|cC#x}36|MJPLg+;J?BVhm0nhp$SW>{da>cB&KQ@<(tu;VIe zhF7iRoZ>{QfuSA~=yfnN;0`)GFs6xq^#)a!madcZjgI}v5U2Gr-_}Q+)5)X3~i~j+;8+~!2c>F8?lvqtX>Z6fpbw-}OPxcg_7NP^;bOfq=%lH!l}1vBaGD2!HxJAvvhxUy5{Dvg8f>8K7JiFb!;f|*x6%NKEo zWY=p=87x1zXHI=k*vndi*qy$wuT009xvlt|L^gVFNPW(cWG226_(44TH%8uE zwxGD#_*&lE8U@7rrSG(`GE$OyogqC`IpwekE zb+;*)`0#l{4{l?7KU_J=MgTx*%&Epv%oHsM@*$AK%pZLcoclLF`qhe(g{P##U5@NE zAwv|j53%dVRpJ`Re<;t0>hI#3m$mZ=t z<8xtA{64zNsb1&S3^bpmkI|{8G--3mi-g9Opp9hoVuyp+P4y9Y;7QbNMoYKalXsnf z6Jm}!2-_wVxGFplbNTM7&hd2Yv?A6m7Mr~){Jw?+Vad!^)%_LEM%us+YsjoR_DhL@ zGHlB7%s1=;CA%2c^tUK;F}&9%TlQ)*#YpR)M%xe%B}oQscQkju(UqB*MC%y~<|h@i zR-%MZJX+vKTz~+@?)gTs0C5c9n+CeDkYmcPXAYWCNG2wF>T~pq5-NNNWBW@s_&=yU zn5lb>%FoBY+QWpN#|u@x0|v|zMAMqHaR(k~s_#907C}93qx9LL;8(cybO|}i3Bbrq zY#(eswYj_h^{^~%f}YQ!CC2T5AW;iblQYUWlj44-;Fs#5HhV-TulB^8tD^WL$=kHR z$MX3Kp=REHSj)L&jF{-2My&&e$F)g8nHSpmKgT9k9@RN1Rf3OOfPW@f8-(|58{KOFn${jQn zZGM#%Yir*mFsCOO>loP&;{TE}LY-ivQc{hovP4JcQu2BiSmJ!^J9T9%pdr||6~=xg ze`{P)E3?M{wZ}PG18tLMD$Q;-64ZzYPy{?6Itbr`({+#4h1bojP~QPeDM;@2zi*KC`e#vhj4YzLjvDYM$ zzX(5t1ZN7iaP_}ONK#nXL6~`3G+&j?a_Xcr|Vb*ze=2}di#v*q@0`-}6 zQ0=+)Wl8FlHZvS_K1mAP>&Ez83McA`-KEB`SN>;AX|=OK_9Whl-V2pnWYv{}ZR7W; z;(^g?YqLw%VtyIt>~|;4gkQvIUF`7ZKP;OEzaWhJ#w&%R(J;bC)=I8ow_ECb02J2{ z@O8MyyBGGIl!ZJ$`%stIZT4Y-x}x-W+`dbBzfpj@AGG1`^1#`391ec-6e!h)Y?fly zbC9eFw0sZ-w>unv>#TH9ei1p_U%fK=_rLd--t&uK_mv^z5ng*`)C%f5VQAtf=3<>A zEv1NU>t$JA^=Kpbq*2QiXf~E@ZL6c5CHsErKMsn@9 z(d>-jji0((hN?m5b`VF1_f5N98+7HiBC;6KR*kf!j zY*)p|JEMtUd@p41>Y>GExLcDp?;Mb51o>%~t6= zL~vMfe&yQMEpyHEatLDnr{mU#-49A_3B?(XhjtK9eo(1scb4O@`!ItmXV?2piQWFh z?l(+b5eJ|H>NS9d5?O*B8Q-f-AEi#||? z3OKEHynZ2`*}J@}=8qqWPp7!f8^0Zw7;1_Yt31*tOL&BM6wV;od}W12|A+{5N=f#; z&9XpiH{gpIIP1)z^}vX{P~>v9oY{Az`On<}CMd`xE7bG3u^(7|i zdA#5w78sd-;y#I$7jdjzDE0|kS%DLi4J9r}V@6RIRiN69(?afO{7pKd#kUh6Pvhvf zdcv-kNo<~J*|7`PS9v_G;UO$J29NnIEn3?zI5i*Y&5h0Md~F?M)U!7PVnKG6475<0 zeh#<4n*qHuPojwdFmndaGTv>84D|aYJy0Dc>%$8R!-1Eo`o)c~=hgUg%OE{feKzxX zh2O!9*>tbJD1*b1Y2M>khVL31`X?T>TRo0q%q5pd6qZfmjgPhhUTdrJUg5RC9i%-m zYF;6stB8kd$4gsG2jEAOROv^;A89@&vWybfn+8{XlKXitACtA{xc8xe?zG5Z1nCa| z)aLuNC52T=2t(q3LSWQ>J9{)BOoxh4i!javc~^^xmre=#gnT4T8s%_F0205KH%N5>}EyXWeM@Y)V#bhQ+k5=iC4M~x#AHx8C2N*5l zqV$<+s96@vgzxDu)yL(8ywzhf60gqBPfJGl;*uj8KLqw+70iYXCPSe3TeoN+QGEt{ z|3d}i_UDb4o0l7x%VA7kqKZk&@lfls;ovbu%N6Z5Dk;5<%+ZMkRN9HHQ|!_ISMdBN zBlB)J<^d7#lqMj+a*e93r5L5i5NLHqumiMaAfk10dtTdE5t?!j`txWVh^K87h&zq0 zaHgN}F+H>%6~|Q)H6Sdgb*&KXG8{0WA!u38vFJnQ#x4gG=rCYd^u-P;nFl!0prK-g zlOf>?Sf7oIj=o;L*;@bER29jej6v@2iWGR}|61{bYBu;A+-QlxwAlarBgqDpVLqBe zR1wk6AEiRFH7~$Uwj&|@#u?+RdE8$V0N5kBc z!s{7)kC>%#ck@Z_G8Rus{{H688>}yi$z^io+J5f$t_dp~HGVJ7U!HC3=A*}8S8Ypz z9$(=|uY;V%nifmX7?xC1d8PL~p0s=g_wf&Or0rM5slwW$O^x{>xW3H@@h=BTBPv)+ zcPBKwrvzRD50Cx}Vw$_H_k_=(F4E40$oqj@Wvi!H=RORRd25UG$@!eu{+GX?7yaEo zy<07h2Fz9wk>K;LyGtYdehy`w(`?B`yWLL~2lE(~+dLq;w9YLtK7%fwtIhnEyZnvY zpT>_rt)*8w!j=+J+y7-YmM6)x45QN~R6YPW1IBj<(GCFix*@c~9BHZ+j|Ax^m=fe; z7SNc083V>|eW;#MiyN-|(4d4QUV41?WVwDK^Bz%Q$npy1y6z_ zIF`Aw*F=VUm?*r@h(y zxIzN7wqr1dDAWv z?apSeI`tDOGjdLM>baf?Z}PNDN?W}a#E*MuVx`mz8Q?aDnaMhqPsOo(7VDr?J~H+l z@v00iJOPF*Pn*M*!EVjbeg6O=m4(`lv(~l8B3(fAB%&bX(nS9yQlg^B5ny0O+%o8=PAFXQ7k-DjMzbq!&FM$KI)XELXl3Fwr4fKR*Edo-t8R zes-+`DC+vsy5WAcMqT%RC!bFyouHXH*R1vxRh5%-8Ve`ZO#fEVUat#a!F+}L{Dl`T zlIiryY|jS~|4FUe1g>`%@hfaEkS&+OmCg&n|J4&~K9nHCaD|Tsmdz4BEI`%D7KSNa z2i?9A>xSW^HKVuU7)HZp=i1BNVK}m)P~=V7zG^PNWm!9Pwz@K=^Y=!*d8C7P7^~@Ndn13Lyj_mj z^A{Dpd|s&cuS4qHIgj4?`mTc&t66GlK~_J)TvNxfa^9q%^fMpkC-q+nSyOXdfP|)Z zpP-iBbV2!->1dmB4I-5hYt9x_`Rh$l;R*~#5oFsSu}klDN-iT&P?jKjvdCncMWT^$2rc-UA+_ z_}_y9!-2l3oA!c1$uU{$oXocoADbiXydF&X9josih=#Hnh%VZVEA2J0bY4c$%p z75?$$HLfl6fuk_*D>Xv`j3@-i#L_j9n)i%_Y$JqZbkBB;qE0BU76xB5+Y{RLo2m`M zNDjGgBe?C;(q;J7)#Q5R@p7azij z`O5GKeP~UUh-(yl33Cn6ZTuQ1z;#Fe03KZh!GU4`-nYp8tmxA$aS79~?KyN_Yhm8n zj?w;F(;1|$b-Nh#)Hw$nVn9?U?GQsL)|Ck;K`fCHzc*SZLic~=pC7^-ItvAyUGnGf z-KCx$^-OZ#AW`frj}`;cyRVoKgP$X`x6v<|`I!ucHGg8gnZjYh@j>|muN}CzIFZLl zPtKWAOXQ|&lqvx@_1C?=g&3lQOscy>sVcFKXeAQu)Ap2{hh}oHZ)cE3<>yJ+2f@@? z-O2}I*kVd)+J7#h<>J3KGqFEzQxVpdh>Mp~&K~)i!Ov^y{dXHSuFNT|mcL6O!?Do2 zq&F1|NGFZ&63>zM!M|T9S?MfF@3x|!;_R_KobSZy$zRyKABs63q zCwUti|N1=BN~`gAUx3@(1N}0vj}-102SWK^5{ok0kUA8)|C7UQ0J$JS#|K2uVv=mp`Qgl-Olh~i`44h*ce*)KmnM(G zf}YDR;5?I`{3?V1<>^tUhG>o+nY=#JN>leL)j-gj_jIb3vLHz#BBt#rFFI9o5N9Gf zL}8$NfzlrKoL4o#69(jWH)h=Jd2Y0Z(2H>%LcGvTS7HYYEeO$oS+(b{Z+_%4=HiL(ac;WbNmh+qKR5%doN>Q_ND2%xzA<3A%vez!8!4rjZNE9gvWQaO& zpYKF7F{NrElXG6_sr5q?#l+q0O8QBP<1^n*B>eV2b9h8W9rR}?S9wlf`|P={kX^Q;V7{pec9WxHMA ze_+zu-SP6l`Bhz~_Pq!bcs-W57J!LHyggGMFlPwf%lpQK#F})+ewiw;x55Jg)b#6} zcLy5*7hiZ8kC#(Y`A-dhl{D0dsJ|zInX@b+6#GIc_H#fiX`YdXO{Rzvf?0|AP^9qc zF;OOII0Y38p-FkwTmXXDgLb8F;}-ee&mV;D))?j}exJlFZ4Lo1^T%uqbt__U9rjNo z$gynH8%{ct+iNflo&C-mwf~*xt_YpON!SPx=~Jc|?yHJzh#zq-?TZp}TfQTI%5X}z zWvnz6#(CN47)`oZnie6JcS*RC?&oivC!!wCAgS!&wccK%T{a831HhLPPo!Hu(9Gj_ zr=uzN>)K3!L_aLI>EDXoGlLNT>+*+y{=5;ass5W`8zPICBu!>-8WX&};A#_CSUeB) zQ0l8^n`Tq15>8N$SC9s4=`gHKzg~(CSk-T4eC<3IdFDK9@Lmna+obyQ8H7Blu^77$tPm_mTM)V44g);DSWSIxRoM}E4^}>Ws)`Zh>$vz7i1V{)%;2d1G z-!B=R@PB(~Q}>kg<8xD7NO0z41D~~1*hua7Ax~?!7fmq#&9`*$gqjiC=k3Z%+|WQU ztLndjhD8U~!uFq@D{qPJssxEW>`CNXumSHVPjQdO@j_AH16`nN$k5STQ#C_b*H~L= zs!t(8M87T5<7abyh}QDs$MiM_VCCFrQ{Zzqjsb~=Qd?rUoUQSbi1_g(ZHI$6HeVfL z@7eN}!Qh&b6Wg2NlFJ@HwK;Yef|_?|i>)wX4V+v>IGI?(P5&8#oura}6J<~R=(@0~?NS0}=Az8M>>_@0Ka z3L)OVT0l#-UGr&WJ4!PF$jb|2+?OOsn{7E@I2sBalBKr!uw>*m_`E7A+T8U6sgi3| z?lfmiE3d~&7=7rP66Dn?ntqYaQcP*Wrj|TZ@*&9JIw_ehUnVSRQi7iqiiRed1^nxd zHk42Hr`@|tqrURr`+;~`KAxZj#CoY`d(#s9xOc~Fa6>$O4Z->ZfeNtJj=ig#d~rRk z1)?Fy-O%}3qlp@dxrMTQZ4$lz@4aR(tlK-4h7D6=YPi?cO^-m83K+&pVG)VN8Mx|j#7_aDL_hLQa(5iR^@J%N+;3}q4t4k!x?%HsT;!V9K)jv;NCo;5 zN-pnInMV56$Wor70_u~yPfOsyIKcAPX|aC3VC!(y$4)f`%3*}WXnX8Zgv$T#4z*k0VBY=5_+QP~37Nm<%ja=vMxKX7yE zt2bt!U$WGs2A%5u+qXu0eWG!i+zqN1_*qCjV#IrKDg`6ViA_#Z!umEc5z0Gm7ZGN& z4M2YdMkiH37~QXuiie$)4ipy8&#mxc0Og=|_)Sp|kC~`^5p!*-0M}?XRMLcuSQ6pp z2fXffG60x_G2jaXFK zqe#U{&wvmm!u+lHp%(j0SWj`0r7=Z?otX?x7ntHu44Num%82*^kQeLm!N-{+NVMJ??hnvJQMcYAPDV{ zF09!HTtF)14D17@Jbshz;-tP zOV^N0*x@MDpGz`~`pjj>VI^$SM1a&xI+vrhv8BVgKg0euv)Omh==jT#Dfby^-A}_K zapNJq-^UYchnqS!CJpTI0Ut1ldMJi@HKVjo;+D~w0zsDr~7Q=U=v3p1N zZ6H9c8-e%bp8$?C$m%P%H|&%gj{N;*G1EjWjoR`~?fazor`TLpMI-g@=S=>+H&>Nz zKL=MN*3S?4ZMbOw8$XCD8UeJ1B=*HTbrXY=dn-^iDx=-bR83nhq10KGzQ0cvm^Uz$ z5_L2Y%njG!)lAh>y9YvrhOt@CWg8?D3Db$yID|JPK!NQonI1u7Abs6Wvtsk;7{x@P~+ zc$Nob{0z--_JfYlpR=6s)|Gse{=%Sgf*c#b%NFIXmReAVIcgoM7&sGMP- z%dkb&WN@E`|CGf#tx#%!s*`+Re~r*GTk(Y9o%)W}Oa3%XZajNvF5E1x_=SHLIFcH#NAiOr&sf6`VhF`a z%s{s((iEe9>LP24=vXHhPF>vzuGfOPKJaLcfkZT5ZP;hd<+9$K?+L10>oKC@gp@eA zi>%@jQI!Zc()WS$D&7dRb8?V5=>MM$ONgC0hb}IKEN+bxA9^J3u7(V+6hZfd!|;S; zxom<{1(*S-n%&gUj(~GWe_-^JSgg8}D(Mi-#NCC$-Y;jW;3HllZ=W`!U<-1|{EH_9 zvzfJTJ)e3=B-kv*YP31Dc|e()-UR^TvOsbq;RIU)gmRlBm04XHWThRGQkXNi*dbHq zX)ma{u&hl3IZ7nlRq|hz{Y?PPM zQgR$&`3GGOCNSMtzDxxX&k-WP`fIGbZ)PbX^${NsL2cuacBsGLG}re{fK?$zg$&sBwS`h#%k5nsM?Kt!G6PeF7` zyBvv1omqbDJ&2Y0w3Qua~a>el65*U?5H;B|ok3$MN|*(G zGkwJ}2N@0e$=mY4mIjh;*Z1ZH7!Ii@I(_8@v1-mL+e-YT4%svP;cGn9nbc&e*7xox zimtNCq~^$j?;4qBzNTXHJMDPB;1Hfz`}5+jX8w;9bQ>(xFo3%yGiQ)t*fl)ZFDgY$ z+^B=+R1auL`*F^y=aIz7GWvKv2_vGvcC2VkI_qgWz`4t!HR>q?EX*v4uom`z9MB55 zLqLa^kbT-0I|P`;`+Q9;&}$qHng4de^MOD@GaA_4QQg8r65`4rW7fkOlkJec3s7g8 zMzghThVgcrV@@ZBx!}r;L@*%M`ThvXB6k+LA|S)h1l9m0%Yn=RE$~O29YF4=M;b$c zoN08f22+}m|6&nOFl|m$hfQW&pK~5E%9;{}X|Rkhv+oT=s9WdF5-M=^;i5i?&v8x@ zi=|xBID_QLjR;{?jAHaYSa_rnI`jD+E>YP`hVw8x`Ah5X*sTLBBG@owX%N#ly>quS zmlIN&>B)W;kzOdOiKFGW!>K(MeZVsx-nXIFruX~hwe0eo;|m3raqNA1JETnikzB+T z5LdB`ee24SUuZdJH!}tGQ1y#H#muiar@b^Q=_?R}ItORI>^@%^zg!)Y2R=?yr_rO^ zmDjjNQ;L!247L!W!I7yXlI}7!WKoL6ILr@Fq_7wj0-%>yn+%}kPE0GrXETsO1NWlr z8xPiFE7P#vzF!ZRNV&FP>B(RYD6?i^NXo{CxQEe|J^Ks%X9mZ*>&WaXq;6%6^m0N? z?7YYl1}%ynnOk)AJ(v9^9qXLZw2+2HTU5aA1e*XuR@=0l#ZjGvZ_{D{C2f%>Hb6?+ zP5@uZ))MPST|%Ls*u8T@CfEsR0HxUe-tnuWQk3x(zvIHDVTSp7qFd4@rrDXVv=cvj zn)`aBi|u`a{5VWZHZg{1sbP=3-jHtsv&=7rpRm;9jO`G>=^PMTtv8?P|67(}5^d=* zpW10jRz3pDH1Vj*2v>*dKN+ybW6?F~fdgn<4zPd!4Uw}w|G5c#$^YXYrz;jL;?gn*9`QevABmS=R;1Pg(KBofWb0s;rgB zSSijlHyQfVT%Ho4xSe3G;r9)I9j{13POZ#*!d?Du_MzAp07vbipJvf%mMbpDuXKy@ z#fQWqpXR?St`(kqUgB@Tn9=Ov7Oh}0V`OpoHgRshud>BG3FqqMt{RoVS>np~Du1JQ zOj7j~skUwkd(6=Zl)f8wsH>9w#h^bQ3%^)tN2&;xs!4_Vze^p>Vy&wnPR71&=Xc(X zSVd9>9|qKR=&Q=|S0V3bJ<-{pMOsdQ=IbCSe~qK=c?#u)VmnFkXb0DXt{0DSl4#7N zNIO9%T5=Xu#&21Ea$k&^Px-ztg<3;a+_*GV$3|bW@d3z6DSgrt_l(Lf8JI(o?!7Hz z>MP4mX8gCUqt2wY_bC^xzZ8Ias!{~DJQRHIHVqw@nBJv-J$6KPq^Wq?jRzHn^#Znp z3xK6nckeZ&WFe27!E^=Ewgb(NaN7pigKA2gR}lW#8^7Gg*)P_Jf|lOcY^z>6f|-}U zMTx)D@09rPzsHZ6W*{w;|8u~C!HK766#EB5?eE*In?>GtAYF;5xt>Xb;vFTd4mf{0Sx0a_7xSTWdfq#zm=ihe zloDqEqv`ORi2cnHe&FNKpnfT-445D&2s3HYx+`)u*uv98CH6xB0Y&QjlPohhP?M-z zb5A_(e+C4JL-GAhK^`Ous&fYbklx*uu5~#kZdrW+?>?s2dnQ%=;}{L3opw3${{GL8 zkiQ*04;|W9$O~F#Ju0114Q+n9o~cNRKw}eKon$i6>Z}jx+hzB_839uXJ>LSl$T0k= z;EVhRdr*T%25eycdemEh99Pq-F%ZE;{Ucd<&PE@MIy4Xq>&Nrog+$^PBxxk0OEggVq%yMJL$3{F*Z45xBA^`X*=uYRl|`map)mQU-eCXLh20 z{XpvduW^1SEmHU+3B+SVIS#z|2Fc_v;EIT=O7KiA8%(%h3b2+7Sa3sG zL??(ro`ASg?@ZJSw%{v$4|3l-vB+XpZ~6(w`C66=cRs7g@uINXRUe1t=Z{3_=N^`P zB=38b@B&WzlkD-=f)^1Ok;4 zK2VC1-DIogN!^@3pPjG!yfTf^z#i{>P|@tl^LBA0A+bwr!wp0q(ZDLH|M%5<-3zc7 znW|u)0V0QyJi*U+GVvsGJ0;=(MK$RRL%nw3r6q) z3|%B2Awnv?#C)W5)qSb^TC{5JlCS_{w8JoFx^w*#JZECu85YR|#r#V zQ;fvBbi;d4%Vkw6t`R_UV~+kK`+Ud|jP4Nuc-jb?Ud3s7xr8at#3qwtogeIC3#tta zlF1hsS?aq}SQK)0(*Bzw5W{n{UQ^eLvK}XpPh=kl1bkCoL8)aMg$V%q7TQ$)o(%0x)|?v)$6Kb)A6OW9N>y@xPsDey3S-cD>)O`slk;>q zx&1e(h^lFtxEDi5x1MhV1+={L=CGaLv`?G3lngxF&iFq?(XHJ*P3nxwnOcC(Z&BHV zCrL9BF`~`hcvN66KkGmQ6Uk-Xrh2N!vxx>VPPO7H2v1~>p-`v2AOYwJ1yU?tJ%*`( zt|fXr(T0lQToEu-3r_7I8HSe90^Q3S@EE_s^Xwi=^PNwm3;)ehgQl3uXi+XO>9Egp zd&663*a~dS0s5F`EQrr;GAG@Mk?rum$#IA#*$kecdK{Q*asR)iIZcLuE+OL1u3a%4Z?ZU-1*l>^LW=Yp{{nJleioSLL< zV*yO32hln=Fzg4E1xya`?A3n%l>{`LY^F9j23_L? zn`y!|weuF@4O`wWh?fr2_af2a58C36{^oWr7~dvI{lb#PoL$A?aMwu&1d%ulYd-24 z&{nxStu*(O%MC4veUbFD2xWV-?<$giykEEXSUpmj909h~F)_^d$mPbG1DuO3qS=tP1extwyR1-6QSt7&oNPA}n+E zBv)45KEU-_V%Cywzt#g0ZA5r#z;Dg zUK(Dsh3PdRoIf#w{}rWQz`)?$VXez_G{40u3Gd5CW$6-*NxIjeQK?zy!9d zfMyZb>j{^u{FJG?H!lvW1RtC$9+rUo<_1g2Z{H;?QNX&&7I^%+@DoR@95BQ4S66AH zHAgTjn@iaAG$v7xY~`8RyENW`H6*K{icO1n{xgvU_XZu95OYC_eavb{*fl%X(oC^W z%d!(L*2B8T112+)Bh@cBA%u>^pkv(`6;4JFguH0e`@#p5JvD|5xCfA*CECLR|db<@Vky%1`Ix zYbda?a$mjjFyx_Y|BW?~+nj|xa!mFNZe%~{VqfI=DMy|k1-~FTgY-)E_s|L$l|b#& z+X)QRNBw%g^OtnkVPS!}eKfL^4|Ip0}~@j-$88-ghYYry5uTR8KzfY#}^5e z=kzK+ySF}-@Of59uUfXq^03R=Foo8q-X?3gF>;AXVh|a9M&i!RC{yj4|8AQ0`GHw5 zy;hXJp=Y9VYOV#+6e2GnjKzS6JHM`A2*7E54`-89ok>975XPiosp#F0@u#9JuTNcB zGUO9cxuBn#;^}4C5F23S;4e{MyT#XKK;5HaC3B0 z8)b=gP1PnZX~qj zy{^x3FYu7FU%7(k-+fg;eBFpUO(l?pIiphXOY`drbBP37ZRAZjKp@fPDQ>$uvba@*6mEL)z8vGv5h z0xQ}&6NO*e@`n<-FUo9S(L6|J{zUbE5dMK75BJ8@@B94=5;!Q5kds+19;1&!X6z+> zD5g8!cG)HW^vv3dv~cm~DQhyKy3R;l)vSBCpO*HI$l2;oJfd%mDMN%A57*6Rb`>jQ zo($c5-8?7-m$r}fD@YDIT0Rz+4w{u{o@+3U{|(sP4pue)$BNO%P8%MWEIA(m%XPd(f>-rXJnEnpSpJ3kR=pmCs5`9_3`_T;ZGtR6CcmiTES=RoT+*4qOFm-B7rw-r5t0ay{rBC9e`=TVL1c68;ukHY7+8*vedKU`E@Rcdj;Vn*Uh^B3Gu!rvXem53F{F_ZGTyE(;y^g8UZ(ObZE$nj``?0eS z>xix`vwt(E?P4Km>tpr2Kg(GG<;TdBiy(t{8u51!RbrbKcNe{-KBN@O+0j~ma=TOd z@U)ehD`uU^8A?z-%-^>v0SN7Y|A74Iy0!a?x1(k|&m-+E?rHze_GX_@R_DpsX}D#f z7LjdfZe7i8MNjV)M*V4rahio)V!PtEk2@*8w-XzW6Fby-sLLBqX)kY`jGmD7DCmM_ zO($gG%5Bqhz4@89Qr=N2Fh$*$Q|Hpl{dy?#r5sHkM%|x$w0y|+)P?{LQFXm7*JMkVo_nD&MH-)iR= zmnPVS+@(u!693#8=Z@_i%ZR-{EL;&u8fXNczV-e#aN%}g$fJ0ZT1QYnx)ZD{3lt2M zomO_|dn)|OeG>ZF$(^bGC*x(}GVxLFcSGy{u<2a329Rx}VeAAhpP0#M2nWGLU-bIp z*XiPDo(#|sspRq}KhANCgYxJP+N{)Lqa?PC3CKPBju)=kvclZ7g58?}okGhpQ z1M*sKUbX}`+OBxJZ-16;AvWgh$TyNwo3(yKYY6e#b*V7t~rW%Rg zqSSuG!xb%D)w&&$JG#bwF0BQB;7MOeEn_ZZ-9RK0ye2U6IsDN1aOUgjcmv)Sy>a_~ zNB4p-E7p%2E~KL@?q^h_oF7`hV+Zh0O^}YxxlAu%*h6pj{m*CM`-1!GhFxzX$qh_E z{ZI_g?n_xrsa@6lSWyE8)g;2TVSBl=V*(q`6)Ka|_xcl;t8F}0wtmJo3Xb^LC--Y< zV_%FaQpT2Ze%{*^WS8h2GHr~LX1L{O{GP&y?^3DMUg}{hPqRfmWHjjDQfWfZJ6fszjvYaXH`KgXm{E|;DLIxKigj| z!^eC{hPX@}%vH6ne_)(){as);lI3K_oA}U0w2<2xFqf!%F7de@`JjR2&ORxX6uwz_ zzfJux_=I`hnr*_AzezqX$4-WG*W6%M_x^hZv^ffr9u>S$mkvA+fo%lz-DGtA@ebWYGI1K&EupuJoL#jx- zMpvxcW*{wW>soT(JgUT5P4U!*`nJRK>0U|)z*XxIw>VK?O9@owe}oSEXr z_eI-pGFI(>wG`ZBpm}Co#VJzxmG0BD4j=#Ds!LIv8phIq!BHW{lUeTL`f~bndOYk* zyt)&j7@9znpS|gVH`BGGOy`k8_d;7Trjy4v$ohx`a*As@7}C6fYeEdx8LH20N1 zO_s`@X->wvM(QO7i8!ZP+l2Joed)Q|+_>Fh6T%HiColde&|zr%r>X^B_MIz=1zk)u z_8JQ?4C4mGV2^$}MS@WxAPPXb1%Q${)# z^GK-Z+j83|xS+4XwmVOOW7VMEcNX;AFbBvLKadJ~mRMs~!LgRO`bc8Q4`Y#2i@x_H zXlSx+3EQRz7t^dRx;Z(xz#sC_h%F(rp5h1g#G|!oDB5qo9&ebd?a!G@`};6>ner{d=_ON2Bp z^XdESzhK4*2B#4ArjF!*>by%7g{8lplZqF{*OiA}kYnXo5XQrUr7_ud9|ot$M2WxU7McG>f157ZN)@^Bmwvr*1b(I8_eH+wnNN;2>u+ZB zc#jF0N!e6*Ny?wtIl8qHI)sv%o`854Sk#H#58ExE56$#FJNCKd@Nz1nm#$Z$txd=fr1&GC5jpT9$X|Io`lnfrwo_en4*m#MTVDP4(*EamkB*AjZWIK%ozKt8m`LYVuB zrf)|qZOB%dLr6%3eBr|0V;t^}3@D&f?dFoH2g*eLJo|Fi^Dvi09nldhpzW5H-?+WA z4~$$Bq3tK^Ws17--O!|l8(klZ^9 z9BNSU&a+p~VAS8&Tv?%_O{F@*f*%+Za+60|7xEtv^ zF9^^sMv_+OHS$76MoTr1H-bwYk{&X(a;RclIW!aw8WE1+!)IjpNMF z&71H~s>bBN7_t9*xe8l(b-*$v* zgtd)1c*rXyjLRk4G#A-Scl#bVLvmLLip?(H#?)yY9b0ncldv!LFdt0&dN*}&D+37U zW_F*{#Ji_{e%aT1kqPZr77`vF6ya+RApZphBp-e!WvcHED{JQl8Fu1FniuZk*OiGm z;ppWF1+;z@{df=h)YgXwQr(+2tA7nfaPEJ%a=W&nUl;Y9?R!q}GK`e44-w{F5qQyS zkN5f*O$Nxl*;(Qz6YR($X|*Y~?bhJ-3FJ|*a#NN_t3t|EBAPTGb#`(Zx8&euFMaKhd3u%x^DZ)r- zEISNH%{6~ZNP!C$Ns^dEwqpQd;$0VO9d=gBy-H^C-NLqI zr%QtRecdO`CNge&2K6t0^~SPnjq(>tgVfS@fzNs7X2$23s$<8)2UeC9G_^2`J{iDi z&N4cnEbETu4-#ik6(!nK#tLL}9g{IrsA)u2J55gc1beI?J4_76bpAQ~SnY1iX)aeY z^3h8zb8E}~OLRj;=OE1iwAaR~^xv@ejbxf9@7`}y24eT9e>wNh&E#U~vT2qb0K2mQsAy*ka~`xxYcR@;2#+gNgYe|PqT2}Y~^efAffe~)}La%6R#(h!<& zD*aTFYyoy{~@sKCzrV> znF&tysfUi%|K#|~=XjPkbYZRFThnyGK=Yy$NKcY+C=ytk4oQc zip?14IdM?X^5hV0+uFDXpOUiH<^5`*eQoYy6}&sR)&$-%jgI~(=SloPX*lL#vsm=e z+}gJ7Un~jESWpPbADF{}Yi>}WA%k)-u>9Y+rQik)N0~LOGmcuGoV9@VEYTRH6>)W4 zUTr#u&r5iJc4P1}5C91ri0=u|y!8j(3Ie&-BJ$;*-;G&)gZew1neNaL&?bjoG*JaZ zOqoEX!Dd}p2)xfpvC*MO8+i}{Vh)W2gTleczCtwGG%?8sqSA3}&WUn{xM6K3Ud2g6 z_f}KI-;41PH-gz``iqB4+`zDy*lYb^pCOCecgafSv$GW65Lg1*dt4lKKV zJR!sCsDAT9V6^dbC*K6qnw9ZvK@-U_m5%ww!U+7Hwsge-|?5ruP?6Jfh^LY8g< zt>cRUeyMgzWuXw8a&!=7eA1S8{@&7dLJ{@osA^^!J6%BIcWK_2k(*P-(#Z!_v*YyXZJ`U4LUjs(6)q7oR@asGDp{|ORdo|f z4}-^)>zU2#B47bI#%6AKc;*(%Ca{2;U+H?t%@zk^y976HVS02sU8$ucq23jO7#O-+B&EAskO8C{=4`)nF3$a2@EhKF z_g>Gl)_*BA8|-&qgS$PS;8zpRdGT?SBA!ESDWB1``Szf(iGo$5h=Tcm)d}}OeR-C> z`((8EHLXSzXmj@8*5GXC_I4*ey6#`A^&iy$qV&Mq^hKdG;r1>WM4WNPrw^f%_>i4v zi4}C}7{}<|D8VKM$%7v(>^_A2=3HWk^z}oX6!NZld_%Rzas%sDnU`1s$hw8>UfiCh zScY9P-#uh2Y{cx?dD+0w+0@Y`Q9J#=UdHO#zDrH~cm}_EI6xc89Q5}zDrURW$QE?wG#DltwF-^lZu7nY%tL2lfcu^0V zwCR`CrD{foz0|3GMkpm>xAq?*vsLUqtAT5!*v42?B+=&3sSCXx#Cc|OE&7}A(r%m+ z-^a>;Tp%Q!2=1L4W8{){Uc`Pb^x+`&ZW*!}5|Nh~Vdi1sgP9zBg0Lq# z1DWbQ$yk-uQjEEpG~&z{RA0ZWkF{ta54N4OA%yOwej+t)GHxSnW{uj&a^#sW9|a)t z8G=?u>3ATV!XFVXx3}^jJ$$%5n~*KK^W)iIggC#aH_d<|k58Y%lhXG&Tr&V+3Ca5S z^-;?S#G}j*P&z#1H?U3ML;KHOizEJc`XfFcT9V$~Y))_ojK_`4jc9MI1ygv=+^9{J zP(H|Hr|1hVRc1j`#N2y+)u1Flp6&vXB{&+1Fq1q!gVd6#^srOxcuN zu^DK)WH!@>p38TGj)fs@SlBLc{~25{F<+#zRwgD|9i-S$%8aULN6!e$L_aPF8~7L< z+I)7608{#XYA3UEuu@<$_fQaBKQyZH&abLZKB?fn(Kfi-d&|-)-@1mPI(U=vl~Qn{ zk~Pv2)`qaj1absZN+zY79EPfj^g%wNl-~HNMZZ*#kK|6#e&7ZNs=` zvqsSShS`#i1P0oiZf68&*S#Gpbjaj(b)sq*L~W5XgkzhK`(;d#etQ`xh6|cTHv>2} z$denyh|H5fS;z|Fn>1FH#rO`LJmJhm^L{OQ?%N}=4C;1Vl|R-uv|@_@Vo(~a zuBPj&ufj_YBP2q}DVBfxs<{Z&vzDlpX%r_sanwe0_BdLcr^$4oTCb#`VcWKzcwr0I zufBLdaLP3mXLhm8sJCVUA{)W-5BluGZpDi}rYZ-(9}Rl(WNtyiEO$P|OkMoVCp&WG z_S6@1yCR9s1@B$!LkMeCtet>W3*&?1%Yq+#qyo%b-YeNPi!zzPQ`dE#YTmXuRKHhy zO1AijScR=W{JBv_YYd%1UZN~nlh34lDJwGYjQR6MTd}1?5q6UwN-2K#V)AMQ(`5mE zA^+@&|5w>;nkYk1*=%3pL~$1 z|G^l8#ENvH$=(rzj8L2jc5=){M*>wa=dUpqhJq?Zvs^y&5rY$>j3llW=(3yn;EJ@6 zd0uvD&^UpO5poJQEJBLpRVGB9d2n1Fg2q8LU*KF#neL6op(Kuz)vJp2xqF82dA@PB z$UVuq5L)u6r}Q`nYG9tSVm+FLtV4|GcsFxOP2GKhq?o6 zNzX};G^0@XSYz~W+l9~Rn(Mh5)m(2AQzj17dvO$F44xFP+z5|F+VsK!ckx?OQHl9+99NRsYKUerv3A8n=?nv z{V2~VA(LrZeUZIqGn5XgUt>2&d?F!sU+e) zJE!|?49O>ov%{OHtm~b89OCci9T0RNh8%yJ`W?=B)w{eo!s9evTOCmQuv^I`20cs{ z!))kI*Mn8A(_bJo93%45s*eZQ8DgCB;xCI<(GxOknff8~aUoPunalsWMYSVa+37gLnrT=T&VIT>i5`IEHxjXkkQW z2*2j?K}q1z4CEeHN8bqo4yt+O(9x%c7Xu3B9S-`rkQ5e=ma_hZbGttW4dBhKe)r*P znv%A9ZFn)y9O^O@;g(z>6F=AV?oiihwZ-S6S5sBY!~Kt08Fwh6KKi<*evwle14M}0ZohPpFZ!4U)0~8c|E^^4NB_Ago>E8k*5I~{reoc znU5{dLZyLYPAya<5NvJuv6A|shDKuEx%iFb>^mMwD5m-yDnxa)jFs1xWi^>_@I-Up z|5v3k-BcT$Hu=#yQ+g>!?}!&P%eix=Zf-@2t=K>$iDFQHZOod)0VNuz9tp|jI;(O#?L1cLldd$OJm$wDaz#6n<6#bcCS!IFv#59H;~ww7pz z-~je1svC|Wxvcy?>;ejePJEz%O$fuo3gDQ0zM+K6lI?c;!TEGanoS%4IqQn3xQiVN$ zf?xYltXal$sbW^cr5Comoj^TT#g6*WV$v4roq z0>9AEpNuvDCck(W5V>Tk*}oN>B3^^bYDk2q2jJB5yF6DkTo5cEmVB^0l zFGdw&lVI!GBl8gwIanAT9muN}Ec=~SjO)5e5PZ}RMY_f9VJWP}0G(`fci*lBT|lAF zpWuHae51~KZR32W6JIzapg4n?QMly@8om9xIsO4yoS*KmT$Rx_`}VtP+7N&}Ntznz zFtVF_I#pDzi^sR8PThNa3U}reg!f-U@9!~l)YLiT1Ndn> zF@o0)A*2N~CY3wo0uB)w32!pxb-KS?Rbq}zy&SDCz|y-rSrV`)&GPcpHZRu7X8Yt? z_uEy?pFBJXoefR7#i15ngN-0Vo1^B&m(+Fm-7+YXDarH0mt0CfE1Q#f=Lz!kh+4}& z75NUB*YOM%S-EX7eCH}l>J=T^8~fc(+s1MbUNtm4x6z@#eZ=4$;-MOtMHMtvC<;Yh#QV{-2Oc7>usJt_Q z308S$M({D3EQGwiaQI{Y*%wWu*v;Yo*(pZkpbUE*;tb|4E3OyTjC8ITvbneOv8(B# z+iN^?Q`s(bH6a%rf<3AblcvrB3fB=lK*kYOk z0;fq!B+%mJhi(yWFd}ia8tr}FO6&|fR8{Mnl_$4F##PMrU&4OjpVK4h!UodEwx$cV z?$OwHZt7r)X&HKffS&)Mh-H!-yNFmKh|8d(Eh{RtFJ7&LwZzN zM~|5Eg$l(a3n3q^!H{SJ{yFw}-EdDtKb*t9k*O>~6GIc;%P{3Ishx)}oyddlWXIFg z?-5m?>gIR0-fuXqFK{pvWP~i?B;j3w2XiS8%ERR3J4F=-IH)M{R2irY)(UKlRUrvK zcSGwp?=*ehWhqz(cd$l|%a@}ub`v;(WL^WY-VdU5H&`;Dvm}jh3DS$%4g`<|4(GdX z;o^9;S-(&(w-K449j0=-c!PCc&5+6(c7{$u zg?CcW(l4g8N5`vzs6fF;kq+o?3ZcH2ebUl;f1rHv8)OUjm46~=R(zFV6&Fa@K!DaB zvp6n+HK>3B=}y|{FvBupWx3bDRkAh2;R$Q6z5yt+;%QSW=j9Vg zZx2Psk&?2M@!t#y3?Tx29beNszd0ZPT0A`6#2@c(*Nq9^C|2GR;`8}N8a8NI?e3dq z_fNMK%1{=B={_%a_h!!d1uZGR=!9l85KJ+q9Y6O}h9 zN5gpXgARGv-rbwLqi$@BS0Al%y&FzXm?qr@rU)~X|h_I3j0Tdz-oNwyF^PN%6 z3D*v2B5F1}*h!CS4TI39MePa4v`fuXBne~tR+XkEHg>G8alM@7QG({eXakPQ%ELjkY zON|N6hXX3Uq%a`KCr9b;){-NhvAgIBb-xNcE@Wc1&Cdbb>TJ;|Q1k;v=yLNo+l)SS zU@S>E?`wp~78XIcTbfd(rP}E0uJup1cu2*;>7(S?MpOfOs$QlKf(H<|`{v(0|3x|L zrV{>>+qGKiUT0v$y{&*WX%-3=E~7RjqfQ%9`MQqoXk?c7tf6xFyYAczt;Y|fLcXvV zB!T)k0Y5Yixb1$JfM3+xrZ$iu7U8)Bwk+ZHJieZLo72G|$=_Sjc`rkV7GM|*RDV^V zf;7fjC!Q#};A0Y!=vYcJ=AOm@YWJBtFW*5PVa3wG7BgKl7IaUpcYz85jcPOBT~{iIOV2{EhmCMR@k;}zU0@ii{$A17nDh z+Z>O>#|+Dl^8dXe-YeZSf<_jVvSOM`Sw z6P_fB0sMVrQpfJ8sltc41wS1x+i^vABwG%sZsrnIwS492q_+X1-c@A%5F`)a8N`@F}nN3VKOMoMmlQdWcLv?*OUO*PVM`$n_M zLl1$anC^9J$D2O_K8V{@)2Rmnj44*BGe+w%uYP_)DwDda7slqhyVbxb>pH)EORgYH zzC(t7uf6l{{}myR5>ZG9CROL_aD&t)@kJp~*Efs$pJ!@uCf%(TO9TT8_;{CJHez(} zpH@*H^q6eFb20?@1rYA;&9f1l%SHHu*^?7x=WH62WUs_FY>*Q^zVikuLBTQ&IZ%J{ zo*&}B45pw}kLed*rUSZVo{`#QK@Kmq*#t0!?ySB`pGBM#PZqS5oab-_gQLcS;*uv|jX?<-{W^^u0LQXsoY+4r(K# z2z0+#qwNV0{u~BksiO%XC#)beyhQb0mq0yD8lnR?!?#TzReyn~yF1S4UddFd(jjsK zbjj^zuHotO%>O?h7i~rn^g5x4^M6?JKHWr65cGV+5DmIU6!+d&KEm|9E``Bpvkh@F z?2$o|8~C6kiLG!Dix3}K%zH$ZqEJHUeUFv(UGGY$%S6&l)JF3_2OKlYzPOU!bd;%& zENVX0?~?3ii*)1INXgt!1-=ENlB)nXU+P;(ie1$Z##CcdI6d9FWE_11j~~q1h8(a^ zO0jFU?XT)NRa0+ zW!ApQx@sg&XtJtOkb8}Z!T%egt){U)po?7Z2hj?I%g$8}5v|xj{M(J*DH_{&jaZuW zn?ZQ|LAxG9ZkvX>T2UENJ*>z<9whH#U4aOU!{q(9??#p_v&s?io0B_C2p_%?Owj-q z05hRaN$!#M>tP=Lawb6u-w=CdxQ*{bRDvFOW0|liD--V9?a*rmc*>>GIl-ieH4kS1 zb4WIw%d{)1Nvz#;P*B#U>OOwZ?7oF2IHl4DFF5I?l&sqM@4NIkb&P$t`sEzxPffjx zA%?Xa)nGi%AH0E5&s8pX9RInv<6db-I&wffSI?W>?K=q?YFO$TI}D$jX7m1IPYvNN zhX2yyH=jM=kF6={J7Yc5GzHcDnC*k85Dkf}Xhaym>O)P$Mp^OSTeB@k0 z_*lx5a8=k8+7e0IJ0wGbz?~XpBL0pZgTxWNryTLp*QP^X-dtn)Yq9)@T|>z3^WOwJ zepatfyn8R(1_*SM>0(5vc7g<^#CVI{n& zcb2sgpGPRyvKd3?>1%&j&|INP>Q^zLzq6tppe@XYD0hB4=dWNT8K@;cLrq|s zkxt=H8YVJX*4IK}5is$8GG4w)wIi*}fOgBc2118}@)DdW-XFQsr(TK`yi>uDWjHw< z{`7V`fC3uB5>Fu>Qz#q%a;pgME?DJxuxd+iN{QE-#D=Cigmy-l#-P$tr_9#&qn#kP zHKNlW#By%<^kC=B^Mk-^asS)MpL!Q@UZ6*0p}Ps)2RFoSBBK2EI~-b-Y{k?8WPBc# z=e>)9v&H4tAh}VdJ5VcM&+LM9lt8A`efkSfL${9Js1L_#?c0X)R~!@{urk+uS6#*V z#%9l(jDn*5Uy~Xr=imIt1Sih`H1ZZ-T@#;EV6{hs4ilIZ3FMGlCECpx6;CNYog0IhNm(YS2pUp%uY`#bbH@=N)&16e54A&?usQS&#cvH@x#tC zT4(pw3Q!T*{Wu7}^^P@P2Yx?yPX^q``RrO;M!`;4Y6&Rl#HN9*!yq86ArbL^d%-~M zCTwgOkW?C+Dn$1UK#8Q45qG9#&6F%HazM3CU7Gxe3jFZ8srkvi+0D2G2ZlM!y$~B7 zx4k6PtUkt{muskAZQn08RE%QW$g%aPEzGK{JBLZL^(Lj;dUbQXl@3<+LqhM>^I*p! z)LqgIe~W=pCKX2HCEZ@vK6c-X=o?lE96Z#pU9p&)BeIFV8+7cJi zJDqZENz|Kl$xo;Y-}EZn#SZr71(MSh&)>|HwaBe;QXGvFC8#>>H)M(hK!Fp8&{G;w z`WyBKHEqbt2As$aGySU4Coh?keZqzN102i5G|$0v=yk}4X?+&A+SqHFFz|@X`>=5a z$~=^3St#PEfYQ)N7L#JEw$f3;t&_Zn_V|`Fgi3@DvFs=1AwanD+Xq?%&J0icqX-Jc zRd$)EYW{o_C{jwGClF5bcihy}nqHG@rqc+L%F?;Bzid;;`~|6c^M(&rfE!%RHe%Z5 z{)g56dv5!75GBhJ)t(S36z_pS;2FUh2Qj#_uRC=UOrxoI(PmY|RxIy`R8d&1)IA_O zAUv0Fb6e9 ziak1tRdn9!WK0jM92g z+RoFr=HLDt{r+UIV~PZHn7;wX!+%J?_o)Cb+AUQc*9D^GZa#gA70TE;GgePC3iHr_ z?WM9kmNNhk`4{SJ@w?$v5cJBC@}pc;zHvt=S9 zIcK8)BIq2AhZ_uPZEoD87z`t@`llPy`fF7^>-Gh^V3G$hgepj=1f;!!jQUzVc!u%G3+em&7^9` zrytqt0)1<~{MaM1yF+Vd;DEPbmkwkXDj2Jd{aV2$j2u!!L<8_%(m zstxzwd)|qc_~w8y$)v;-a!UjP!icBo z+F@X5inZLIGw|+oN0|WrZ&Q-J%Ob__-@|n;BO-Mdd~Z0q;A>PV5rF`x-lJDUD_U76 z=@;BY7^e4F5OGGRM|ClGWcTt_y6|E-rK4g8ootnHLj!qLvh5rEg#9{8wqyNbY}Bgr zk?TQh%w3;ZyifH2(UBKDVFffqMY?tFy8$}wMs#k_ihy>UkE~HWo`{NS!8Z<>tIh>GtdH)_{lZf;+QX+AW<5+7hC0?>)B(DvW4V&_dJLHk zwX3~$f4=aB`DBd}Vd(Tih&J|Edb3oXtTJuo7%T+n7`w50(CgvH_4)B<}DWA zE`Ccx<|@|55_snEK(`!A;YvPY-Z9vtM46V&vv)Yk%3m!q;*VRpSmDCd*mztA$j zlvdB~IFLFyDm!PLiP45YP%R1fSQYdZih`4Y5)of+COS~ft+c9mDSEAV`CqDdv1^i- zR3<79IN>|zKa^Z9s4%}Fdi(JDT-?7BYxJF|hLUPt=Qqds~+oO^0~;-o>Gp3{Y|;=^DNQU1Pat<-uEy%mkPPUf_3<9$Nq%}tw@_2tDJ43A$FnA=MgaJG-pT*~g z7A*?UDWzIptxXDDg*ab}zdGcN1|jY>7bvzd4DHh={fr6>aVdVTyTW*Cpya8$* zQ=~2Y*-ZI?N&3bBWB&WqXN~p%(^p3)0`ppC$dy^2TyfH><6~ENQPm|Z@YX@$-#^XN zhE{j$XVLU%e0hJDE)(x#$_f^u#MwJPJ3oHS_zk&mt^8d7qi?i&tai*-<0K^=ma&17 z=JtD*I#jyo_Zs5ep2u{Zwy>}CRr49Kyw^YM4@@||VF>{wbO>>~19I>ebcFm9+?Toj z;hmQnf2_qv@rKTJ2GL08ut1kia-%?V4jP-&ipbY<7@NOSMLj1u?VqTHomo--7VTbV zDzZ+57h3^Y0vmybZSdwmXu;7sJP~g^o~G|s$jlEr!A0^OJtfXiUOH;JqeQ1uGzSM; z*GDYH8=XUG9BuSmnGckKA5J+WlxAZ~r73r8kj)il8)Ud+Fw?ZVk;ZBx(Z)e-ij@0N87tl8wKejQ!ii-O+ywyeD04aWRIjWbol} z`d{y#9(F$zsyh)Ns7oW6XOSYrO!Lml1i>+`(?FdGjT|75IeHHaXUXuZYO&QWiBpVy zmjMHjr)bXa#!2RCol6zrCd%nGg5;HLFnzABPHLKC74I!@{7!x=>_I-(BK(NSYO+Xc zzd6n#EVRCEeV0cj%hGG0C6(ghU^QIF^QoitJt%TX0-r@p!S&8QOu{WpG2H%_RR%$X zHy2Y*X~PdI5XOpCy2;?UO?a14`9}rUc#><3u?|qC#P4fs$M4v=Z5l}3>~kM_$RrGiG+3-rd|-ii8e#4P{rJkm z@dt7~TWwzIv5)&77{L6kkWYuFy@xZ5b%8a=Qpk)VeZ($xLu6&H=43n-vu-|Mu8)|x zd2TvC`9){(CzkLFe~|<0ozNg$pV7xg1YhRI+p7-j&DX(}1C0d1r`Ib^8}JjaZodbh zXrB06%v2`uQXQtyh)r8!Plm_DSN;wlp(UVjUpOFOT8Xtho;U%HgrCv3<5#db@}R9x zfv=%~u=qEQ>`+w_V)OBr0>1{V+r4__Q-P6)iy zZ!3o`PW`+IrT=g?C;UkHl92Hl;_C=L`sUSdDnQbFWRSTW>ZRrOlYQO}bxH@NvI{&P zlT)@Hquwv~eUM+bCHuz%uNI8E?c=`nYd?%IGfJb`G;|%aBO-zdPA(=*Zpgvh_Xsep z*qI*h0aPK^XO@`!Z-NMe3{kF4VN_WKGBV2X1U* zKzLGWSc%oQnR1dBMIhPz)s0olZwVOtZG%dOnjWjd8o!A!{);t#IDPsOSwTk-ZJe}K zmyDUZqoG&jW~9g``S(4?cb(U6v*@ekf(NIc|BDcG-;P23uR^9kX0NUs5bX6qY>rrJdRttxN7qdt zVlI>Zo30x%cA{vdwDgNgomAQk^53nk@Q95Y3N`sEKnTM$zyyQiczqQw__cH^kTes^WXbnO_y#Hn6cKhu#N;-befL4-`NZA35QstW{!0a1((kxe;`*cs1e3?hHLP8T;84t zw#y7@Q4@69WA=nO0h2kg%J~(IeB1cGkbj~9Lf#k$VK7$QZ&IlX!YsE-#bEY2FW!1m zsou)>Y63o5BCMw@(-fVCP{}u$a&FMhz|gHC#F&`=OY>zPcmm3w@QfEcK{$;Dx=l5` zOT~e&z+v#c836l2!$5Ytg2>I zTY->u^^*h}3NAA2h-fr-}1j3g}3)&!1TQ_#H!%2q5x8$%rKgltBcaG@X6L1|2ZZ6{nVdF*%`;~;yfCc%#xV>w5Fn`YJbCAk)*D=QKg@o#Nx)*-nn zug-9jEBg1&vRKJiKg@Q=bi<}jv^X~s#=b^aY^x&`=t$ezlr@N?r7HUzAhm9jSK^Wu zN!E_Vp3l~p4r`=72UkFu0FDCvr!P8kGu883&dhorfz{9&UqqkPD;eBAz zwtbLDcRf${rYfcqZzp^Uq{;oeAheRzr&XuyN2(}#ikvUo*sLhoUNVe%INFc@g>ot{ zl}V>BY^U9eireK4!JAF-U`09kM_HwH*NcgX5SQxK>+~S&yIqPRq6u^31-c^DwE15R zKDwk5snOI2>t^6^5JOi#D_TlJaU#?Wzda<^oq*4=;1$uo#cauQE~}=nW9PhvvCxZu z!a%^>qioryT)HyxvWn|Vnu^ZvsJ>c{my5X&!qQ>=J@?%V4X)*UWh^N6rrj|YdMnEw zEkh?isIj{A`i1SF+5Th8W%7a#DNro0Lb4zOI^Bw(5`Vmqc(@?%zE$kLjnO7_SV5FK zC+Ok`))7tO(`JIvG1UKYKoGwwSB*sl=?e{neiDBY^8ZT?$HY0=AQ<`C&#C(HU9J!n z0Z}YSPv`EvKtU;W8qJetr4Q23SN${01T9n7_<`iJs3|t;8sYCfKdgaF>qogAaH|xB z@?=qwN$sf7n;o4xD_K2x?L-SB%H$LP2ar12O2*b*$jWS@t!{P`?p~A|dNuzuKQN=F z!Gi$K#k-nn&KJnr!n2+{JoJg;e(1Bm0nQFaZMtP9o$^C_k-}UhAG%KJ6=CblA_G4j zq~{3O=CAUk#;C|uHWu5rF-US>a>b%bSsQ}PmUz)Eun>wr?#QDnw9_$6P0Gj)^SZZdz0;6!)-NoF|~O>{?JFSLad*} zGu$WBRBFcGX3@ISqC}0b{82Zy$}HFD?&{v9NiJ*wV_i2&e{COhNp$b+TNkl7>)s8w zPG?5L5v-n;z2IPBa&z2OE#bW3CJ>rD&8g;}Fxa8>y!Wm7?X5W2*lGtJ+WnUzWddHg zI{FP~<9>v;XwNjiZq5<${I7Nak>QKxz}&cxpc*4wNaqcDS*mezqNO2>g4QT{#!A5z z*c;3iQ1I&8f^QZNQ>s1}hU+MfCtWdNy11&5u0345tK`3}dMeuLK~zk)2pVgNL+E}6 zBBQ-r&lXM~HM7_NLy#d+&wVorZo2vRRYOI*G6 zhNb=`!|_s)RLW)MRDrNrv?)ap0j1S4gBbqn>)5mk@=!9A$l(L~4f_kkDg342D9uqq z>EJnq`X9FKQKT!aRf#rhwX8R&H5fl)7~4ZAXMYMOx<-+6Ymm-1;~Mv`mj~cj*~eQs)v@V@vp#oB#A(Bgm5h5MlW{ z#zg(0j>af%BnVcZUmGuu?`>*mHrp*k;*NhTxS7uRS|3Nh9CDD5%ZZ84a9 zzxHn!Xzs6aT?JYehlG6-oFXISIPJp0_}_xy}=!pPkL8Bnb1@W z&~(UC^pWH=+>`OS_J#yc-g^&ji{K4d3*)V`bdve3I59PV-nrV1t1W74750!f0hEbqzN{6PHk=7 zr$DQ1BdjwrL?O8Ke(AG|0|F-BjBgpsKh{3;K+^VJlOeufg0Um{6{0!fYsx(fe34N%V(ez{4$(qhw zdoG(4aMh-1zm-xBoqTKwo4hrdT{bSBEcjqz%Y9X+JAj%BYL6vSSTQPq*%a>WCNJAu zALyXDWQy{=a&DPHLCVk)&#-QsK6)jN2Mq7Sm1AZV)|M>yB_dAFnDjbKz7PNuzyc-Kw-klKq|RQw02z zl;~{zbCH+YQFk7s*h(Bbv#`0aoTKqgprtxH^kXm4kAnU=$f7}f)p&a&WZp%Ql%~(B z#bb@Z`;#a@oj5Ah`w;%s$YFA68Ua7K@da87L^LkMz;<}~O>4_J)z=($C)~Znx-1G% z2hJvAaH*n)_rvwEY|w42lM&o6=X;a%D?m>%4H;Hu9}l@%Rco>=m6$Ry9Og(UTb_ReY(bJMuRctYR0+FwN*KYyWfzlcf;VW?OO zZ{Q-y?U5elCZgS~s58uNi=>jW^f6eL$y;jz5|C&!Px{1Pfkw+IT9i4CK*kNjRI95Z zZ2|yn^vE0;(qBU%(I^V*Up5X9P3p?4kB@|il{QwxW|6{Y4+D4<6pxQN#qoXHM?;Q& zUih|~XM|5P{V9!*;9?t?f!uCTg>Mv`h$GYQ`A#TcDK$|N#!)AAgrVPh*XxqAs-o+fS3K`qg(g-A#&mbbmVNv{?x2ntBI;S(Cy1NP7GKzTKqF z(68#WwNa+Ol~|G_jr3K4JaZZl(G;v+x$^K3q5q&BoF%_~?^R<8mnJNFN*s8o%-l`q zq5oAH6z0#?`?Z#8YU7Pip>4Y%6c=J3+WKDnq;(xobQ0(gC#-~*h_kYsGHP5@ zBLhs*X^`0YysO1D%ya#fN+;ujoTB@3GBsYEsY4I30oLoQeS`NxDVp`l@zU?4vvHX_ zT$8HUFc)U7%D*@w6u|yAS1<0yv`_pNp%58li~)T&G4m&;3cOE|KSaC6_TC-BqY(3T zC*?GLV#%<~HOFnoTsihD zjV!UBjl0|+=U=jg)ux3T)BHZ^M~-wfS{`$;>}xh59K32xdI(bfosjrj2n07&ABX$` zWY?cAcshJ^!~b{Yr?@y`_KG^a`YsMh`T3SCnY*?l??uLT(K%~Vzhgds*viHTzQP|> z@vac~9sB_&&$x*|5W zF7knmBR8R#B|y25J@}dfSxS90bjBj$(3Ld(;6ok#Fe`uMSLwQ;Ki_`4C)cm=eW;_Z44##XQlbx5qW;<0MJ3hhoyrp5 za?^!2@j)ho@~z+4>JPfrahLimWsRV)Frx|Z8{)L&LdKTqDurk+Qo{<=?jHYKRUSc# zJN2V<18wu2%jVB}OBa5tpKcdxEf?3uD*`RT)vq_$F-!8o$8+B>%aNEp+1rYUG2zxZJTj|_O^^Z65i`B$zDLVHR@YO6%yodKVY!rj6V#oO3VR`)`j=PGE5n&QU z`W)&=dh!-wu?Zo6J3j})w>$CXXYS(~iZdD=9cbV^dOgc}`BSUqkCN0|-?|Ipq43TYOXmTW11o!EiM4I@rhqjX(%0G~(6ZuH}=yzq57 zUp3-cD^FXT%hTms&-&Ub42zqNslbtC#@uWa#&on!i&2wi|DE2M55lkV8R&3JT)=06{tR8amBK`asUQ)Z<6RitOw=b8J7y=-`8-VT&BRZaF& zUAg;Rip;>9%I7vu**{deRl2L|;_cD`2!r_6yu zKihPcx3WiK9D{?eIBUahqub~Y0ZCC5Ty#bvWI0^{~@@YUhB5vkt?wRifnp1Ebs zC6sUlDWW6s31UDs4FM2_XQbo5!?q9FVPj*;G(FE^UL2xf)>_7wjp!}@ajjg=>bhwjFE!L)2tf;Mi5C>&0f7a-;z-X}dS}bjN-~M{lbAhO#NKVsg{oEA*H>wrVimtUj+V)GNgp@l9HYHg+&?Q) zRyc4<`^&kt;gClFZ|{@%glywT)APi)bBEglkB%$|`?ot5$VLs8KO26Z!+tcub2Hxr zA~H8Ukc%B|8f4_0+#6lZ#4~FF0$sYgXa7=m0JdsPeOrsqMR8qJ74Ping=~+dc@b$=5?28piSK?B7h$q8wo`rHLCUGKBk@ zYN^`AkEsKT7-%KK3oUPT_?}}97p5_HIFRwF?3ST#&t49G&+mse%oR?=bd^R<1YP*K zy15aIDq0#Q^~e?~frXphx^=voMlc8`D?P2v2R?r!mue*`d`CDG&yqYVQ{=LFU7EMv zesi~P8uBd}+_iyl`sQ9r{OVGI;ju7zewP;3$aO>Rl`ReA%v0F!nX{gv z=B>^1#i4;-V&{SJqp@HaVPAjK^aVuS(AXAOzM*|ch69aDnoOQ12m+hDDf)fza+~#nEG1LG zS6&h5t{)emresbTc~UK(Isryh+oAv2TDHzGxI9mZZHadaYqImV%sAN^wDU$;LRRy5 z-AsI^`BV)KF29c`D7}Uug6EVEUfNbt>#tmTy?59|+(KllGQ}SWBKq9OcVJIYe*sOb zK!=b-mk_$sNxJcYDi)My3F;M$VR?O+nm~eEpt8RjRq`BMmp=@d#)9f5*xU5)2kMS3 zL8J!E`6|&>Z@mCvZwES_LtKw*Fdm$q$IGuSwFnp!#p~{z z%Mj0m*w3`++o#d}{VhT(b_GdiY>9Sw#r-nDK`-ySz`$&Dr8{{+B|^-o5zz0ECh72v zHytEuQ{=Y&K%eXTy_@K{h!E7$DkV=+bnYc03L6|3ZZ>@5qk~O*8E}%_hMU{V7j;Kh zD$~1XIBg;L#zXIZ%Odb2d$I18;858hItNjrYQP^w32NS#x+VNYw%eb6kpgbdl6DTYWw2#iHvh>qnOsl#OSQ;3!_;pr(2rC*jMjQVxZ23 zk9ZGv`+0)}(h-+9Et)3^<#BVuXT<8}pyGtLLh zV(aY&=wm);Yok9wJq)ytKJxV6%|JhQTx-`8PZxJpEU#wftSTOA(fX_SE^GxN6Xt!8 zF`OE9M~RiT-l{WKGd1yzU;M~=X#8P*QXGHmLZv50H6R+Fr7AfGT2JPdj_dr*poP!? ztw{rR6#F9JP|NBaiz1GyQ{=7anNhHaY*f#|OjvdM|M2yeVO6%@_UKxR1}TwFK|&g& zK|l}?q`SM3?p}0*h#=kF-La%Wx*JJpq#OPZ@B7>P?Ek*bIed`o;)5UV=f3A0bIdWv zbQDxm6#KZb$y;{6y<^XR$c?Ep{h+HLpC!^y8L81x0c@^CX%cj0kcb{#naUx4C@#+T z#VIW>)wlL;^X*$sbTUs!xsUZS@Mz#x8e3DcmM*uQVfbtq}uo5yF>d&2hz9m{Y|wh|_{1Zuvl#gVe- zX-4rRN2y;(x#>h+bKcFejULm+&%F3V23H-!>yE76B?n!N5rxC0kj}Nn8G&Y~&_hM1 z8pv)4jdOFgM_Vvn@Fr7DMDxs0KFjw*RlejdZ*T4*rUY3x8H-iZ*RzlOqx3-%-iR(g zGq{&WgU*w79v8Qigh2FjzF)kMf16O2t9m`H9X8lL-1$Bo%gMUf=Xv5a5P?8lIFC6) zyCs@`WCP6hkIcqOz)qNxj75mf9Ru)cMqjPLrNHZ03AX)5cGr3~!Y{SVDcgR-l=^g7 zVF11Y6K`>*qoLoZQn2T`c;q75n?iOH)iiM($X#!~;nqJGw#ae5*RS^xr^TFkZkQmHHx+J3Jf6`sCmENvN z)RR?)I2NU$?bFhrC;YxG`ddhr^G_RJS;i$@F3(o}d&$JMa;!h*2&N1_FdD|c-=a&r zDvQ!i*1KA;b+L7M0zj^Lj3ba{3Cg%gC0>yg25 z`KOPg#(QO;Ks3+nf}TP=VspZ5s8UPKX^%#>%O_^j(Ww_-h$L-1t?()8OqrE%>K>_9 z&y;e-zU1Ss3gCn6|LM_0Sw&s&RQjoU+)c#G5He?zxJ~WT5wO@VMT)?STy4li07*EpfOV1{``i zbVP9?@!Gl?yIk1VsC+om_r}`m9J>1U>FM*xSk{n!c^L=kHE}2%4s`A#FPgM=T-U>W zu5%x|kY7m^me9kj-c z)d^$`p)e_qNg+h%3UyyD=cS}D^I`&UGo<8v_`O!A?k)BlmZ5NzP*oX#k!D8}K!v4% z0gWud+SD%fj1!)P@NR={jkck(WDTmLOVoaD2hwgFZHh)v(}<-h?n{-BlGGfXn(#nN z0r`Y72EdKo3+wKexy3$RWi=`^3Bv-pYf($9E&jt^^VvfV^7^uEbJFU zFYYuVN_y`fYtvHh1FV^Ki$f*f`QG@H_On>L)Lbf;WE6BD$#})q7KlwzKfH7Dm-gup zF#Mw;rj!9e>5jv0*um zoosa>!@59k_GlMGmV?Q(Bp{vXdts_yDDM6ii3Ghj&?CIJCpnBY#C*9BQX@`n5B_3n zyD@K;@pCw(p%FQM3CGq8?doxV^)-BT-a;i2NG&ntIw4%Xr!Qd9Q^c7)`h!}{T!op@ zO_9991pHXkfP5~-$RN3dNSDC-NpEkwhvpF_87Jw>s?ca9YswC6*pxA;Ha-8Xh;w_@ z4)dyB7JC^YE4KREpS1;l5tMK~y7<+d z)h9IiL97~PbFrZ*M?D9L@(_l*faY%_Ezz$n4)NmAKFiR#77^MQV>JwU1*C9gn~zY!?xs`?swV^ZsNo;2fm8r@o6 z)zlQ0m-l$oJ~jDpVLb3SMtdQFTYaypZPadJ+4zvc_?29V#;KX_;}j+x&MPrjg(2C8 ztK-$Dz1t|_j2t0+iJ;rpI|X;VYwcPRKd1=7uubRv%>-d21*Fhf{RvS)o268%t2&h* z$m8UtHPYph2YR{DSMtvnVe0x?g!vK4;B6afh*5ALX6Ct(*snjQmNcW^%C~~-q`qHt zTnp_AWIehhRkQU?dypL=&LH%Aa@kuu2$fpp%M^L<&v_M2DZmx_UU!ju-~L_eS|0cL z>)c_QZ;p&FQ}4IvVe&hBpv;*WI{v`^>yUO@^(c@Wc6erT6seHXnA0%0s|*LYb^H3iBv5~a{YvfaU69e)`+M_~VXzy-c$U3K_Y8I`f_)zQu0 zEYTcEumQbGvApbMbVhflmtL&v&Y-ewMdV8y%9}QJRFTER%+7vgl_)jEua9G1wwpOA zg1lU59lERh-i=N-r%VQk$!TpUzip_}l7({9FDv-HEG3_8_(DCJ*&M_JnEO0Jt+G@)Qe7WC|^lXR2d7fDYB&A;zx!5Fsj;clbkX&F1F zrcKS0UB+F=C^B6D2EyW=7Gk~;!Aw9`U=quaxyLW7ZJt*HzBeJhjb~4f4NsRz3Z`cB zZfjE5%}l}(Wp=cLoPYp>bvcpk6?3wej}}x-*9YP0YR~rX^@$Oie_fP}z|$VD_T4pH zcjleJmp^%>h%lVac4-Fwz-DX*NDZRNK^lFVZ1O;oy}Bv z?3G^{RmX@qrN(AlO&Jt&igcc#M6OZTf&a2y5z#wAJ1x=f8$ zCeJ^~`GDvPb|>8A_Yu`v!8>1ce&R?x)!68a3!u!&@j7;XyHSdbZ{$T zt@9z>Rfc)7`g$$%CaeT3KbtmS35pdu1u_h1%!ibToKWb2MW9R%PF+Xi=Ypn@t60$L z7tg^E8?xkH;%4KHmpFt!0OCC%>F<7XdEc+6&(m@~D4gWoMdNzgzG?%u>E**5HbqaS zi^GW=YaLqUTIn7#7cEV7^nnfwo+qrx7h0Ss#WL2({hVO|K}$qI@k;pppLZtx=}$B$WME$wmx}%ChRk$I+xHN!Y6Z0m;B>aXb|kN>b-pLO8GeZ(erhu zZ(Il3Zz|C0`0Y(x#{K4YH;9#rD;PLJ{srF1q5EM9oV8( zR#*GuRJ8iXjAp2Lx}07Jg>r$UVKVt@lj*bfv^s6L8!XtDEsaS$CS$&v$%Y+yJ+awu zP1@n%_?gBgjR)bgIJNNEQOgFzK_ASe!ru17>mLg{rP8PfR-nM9N~X|6M|iZdJst^) zi;1iZ7wXIz_out-;`W5q^veiz3vv$dAm^F2Df@MDwEZjLeh8{|x6}qig-ywP*>9I4 z=>h)Vv;bli_e=Y!0K(8^an_gbr^)K$gdw~)gL}RvKhiQY$3Vk00i7UoPC~qX`gSNe_WQml+%Of>Zu>H5$ z6vQ#%npEHDJ6;rL1x%;RZXNC05K(XU4Y{dvhG{p8S_-~u&x>hU6P`szm4`}IfY$!Yj+R?uZj&M!n4mn6wD3Kjm%k8zmz&v!4t;f4Lp!Q(w2!68<_?J**$37r zDGhMM(F`;Ve+mq{`9L8tm-`)^!AUWQ~?jCj?p+@wB@0B)*)2B4-jV=rFn~@*#)bUn!@o~oVfdsO(SlimsNc>;Z$k(#iV-1}rd+ zUhmh3R3>1yVQKPI;m?xjr8c$KVxH;^*f%5;kksQGxVTa59c_P3J+eX;V6AXX64>i= zwjSC)wSjBQ>A3<77jG+`Zfym(keVk+*9yB65iO}2#MtaSy@L3+;+!YZC$93O?Tl=! z*JQ7qZ0Br-2v8e(GN=X4hTicwT(fekD{U*Pti{iEglT`hco@vLNaA48u+|T%2sXxE zF_oQr^|jhSY>GJ%G+{jG?)rM;6Rk=fvdr$Z>9KHd`F1&PxmxLOu2QUzR$<;@L#GAx zicFkul%hto`I|Bc{NiU+7!p`spbm2*m)>8=XTwls^{MxccDCt%SgYWvFZP)D)%hM_ zfnU<9i%m17Mt*?ot1mS;5GTO*u(ICBD9Y5@;51}KO)l-;g_A?5{Z7d2f+UI#Led4t za2GY28wWVX_-PY)0v#)63vW|$PCmTcOVw?Iyr5I93GZ4+0H=$?aJjQDHu|grw_I(7wE87YDb^26xI?=49Gu+MJ>hB59fs2>9Gz4*>H<$56q@ zVcu>X5IM(0VN*-_Tc}53A+H|Kjc9=&mh;!HW#JlTOCCdchGWq|%af2(!|m^)O`@Yw zcY1Sr0~=V1AwemfyAGs|fy5w^0Ez%5+D(6hHmo4+KqZ!yvq%4v4gtNyg_EVA$T>&Z zji3DpKdswWaQ6;o$ob3TuE;yF?l67-2BavCifcG`xYTc?<@xqt@2kJCOy?d*Z8zL7 zH=BZ<2Kp|`u{p49&xkWy&P9PXHhpY2#}=rlUznBb(0$NE$Q%$l>1{Z`IY8`RkF)25 znu>ie1;rhR$=;?I4x)UoO1U!6*G zE{xu_UPVA$-^AlPV)$ z`(a0)Uv<0_KR~EA?5!1=Sqho~@;g=7jd7*zuA{OL@Qz*7d%>z-Qq!ocxgeF~rXgKb zbn3c4Cob7Q=0d}(VmcdRo4Zdoc%l4<1CyIFM#3903{-xb#|j(5q_E>26&Mc)?rD!U zW67hsX43mpXz?`|T9VeP1F$giXDQ#=GKozc1r?G-;T2rD)7IleR}1_6zuqX8MW;G8 zRcTxCes+J-IuK$o+(Wiz*-niJ7P`s6v;o<@2$m*a`p(jXWi!B{*tee5%@+A~_MR`96lm}1Hs5JR5^scHYQT!fS8&gGbGzL#&RR#73P53Ihbo6W$$s?arzSD}gObje2+nJ;>aL*>Y z=9+$QwRSK{Aal`7MR`9o6BJ36rmT{cL#k}8j+EVErM6d%?ni$~7_|1yTr!G@vl2Ev zKv`Me{V4^;$HpQZ9^riD$%HOdQIUIuDQGarO=kPZAHj|OcQ(=IMBYzjM}f=F*`=OS znw%t`(lry-Y<%CTLj+_P_#KUoPXWtf43ou0CDadBbQ84D?xYP!ynKDg)1U$(Xf`pRaCAYB*>T9UzLDcF&FVN;&^`^;XG0dy8OW|vPQ^tVWgzG@;UYR zu{I^e8;&2|Rxa7dTy;(7EN#RY{vpU0#7p+Zg`<-F7({}E8HZ^f{}{TxX{cXT;CWK{ zJ&u{D>f}!B%M-;WS|#Pm>_=Ub!UDX#+FpSoGyIyH9L?(n4c{vXDBimZS8^CFNcZ*a z3{gnN(>i{`5jPR;GV0At*Bu|J!YwnwY`1h>?7g5r>K z1ODy6FqQMDQGu~so;YYhe$jkkkc_9WH)0DZRFP8oaj(eXv_MdwB>~{$($SOzH=TyDkQNwH$@?+1rftTzdBKDOIby!yEJ-Fd@f zRBYv#&UZ=+slxk>rv?^|>AjS7;W8uE|RO;cKWRKz3`SL5lT3y_}CeKsP3cv;^) zcEhs@1`qAdZfdnxiaV?3)bu+@#ULassGhh@OmLL-|?;^X>&Cx>%OU0fg(SVE7_#| zdOZS(+jiytW%U@B+ z6Hn(yXz?F^!JZjlbV8x2D&~4+q*VTcOY&NyeqOT{XZ3x#u;GdPsf2nj{3}u2+i|i} z-oo*ALW8@L%d3_4x|){7^x>D{cHd1l42Q~Ea6=Ce#Zq6FEmq9&ED&;H)iVi4!PPLq zz7T_MHqK6)8Y){AM>fopOp)EpEA?7$@Nt)S2&-58Hzo^g;)wdW6mg{HKT%p%WLHo0 zWxL_CM>{F1;GS6SBzE$BJ-3&p+9dZ!a6hfFY%kP&>yBhpM8q^|Q{-a^4`okCOJt}U zQGMNf6u$k*JM^--_`Z!sYuGa*IjcTCEzEcP;sa-|^5K_~#$A%K+~n&s!kr@@Rzd$k z-UU9QL4@|6hKKvpX%I;?Lg&Pc0b`lLiHO@9!%<_bam7&4=7b=qA_YPCdw& z7$}j=FCRK@&Kd&ul5ut5>@cm@gq$%d@D?_=(SAGC}Al|LToe21KURTFAB3sn1y557V-E;a{@i&9pk2nONh zOJ3=rQ`p+4lkwnbQYirB{AZH*Zs7Z3P3zPJ%u5Kz#92GcWfYgO(oFP?QHu4}>8dx4a<&_WRbz5;QKJV$())PIbMId{a| zV5ZWI$<_-6`_T2KC62|V%YtuSYyLdfStY8Z&JH|-N!uI|Wq17pswvZgHIDCeqGtYKJ9@Xe4 zC1Ry7r4>f*?nxauBY&NI8>HznzKABC9>$|si>}p}e7D#kLa`>e25x|FL-~HpEh5wg zIqs$&lc*wPcUfqF0JQc#)YlcE96{&NM)-XlTN{loi$Bfli`Nofk7B-dv1fB;?_;#j z$r0NjP@~uKF|jS0flqT|djo5g4hPoV>6mq=KK{#4kLHRzN485OFKfEds2DDn7?AcW z!_@Tx-^WQUYmor@bFc5yuaR?TaE0zR5r)4npNaGeUR|L8u9SB_qnhf1 z_}3=e3|c;rts*8QB-pLTFa>|x;IoCMIa%2pB0+x*IeoDF5Pt@md#_Noeb)j5t$mVNroBO1h(Lh($h zoY7qz!P5s~dG?Dz*Yyn5ncu}|&f76IgT>$0XVQq$Ya8vX;|`Q{XMX{jZMYY*JbLuu z4EoKzvMNOkNJG6-d&)n_bkQ@bjs5tKZxL>~>7o2L)>*)pOT953(&hB2P&OA@UX1`U zvS)_n775VvCR_E`yRifT)of*LHnZZ%4fF5 zaK?9da>!{BvBcP%Lf4M7`$XsN^NM#Cbr*_(2tX#rSE#xmDc=v8AJ@i&B>1FA@PAcR zk#&6E7$F2Vryj+4e||p|TN{$xE;U;`+!y`LvUDmQkLg&0_6)SZ}4l1(XK&5F)mT))VWhV(E}+c zVGNV<#~dDyqKxJ|?+!A8PTF@C4!orBvvUA98AA#uN+hG-E+K!No8Ba-?k1$TOuoZE znO(>-Mu`e+HBtbp7t=4w*j2*#Jh162{WpHRRL7l!aZn3$im~-DjXWw3GA=aa;ztyysL(mM_lD-)y=*y2=D4w-vR~X7 zec0S3iR-;-{b)yRpYaae4(XdfLHsJo`*Nto5OsYyuuguy(rc4Pe%L3;uHoVH=g)IX zOC-c&3mGb^Q&-mD>N*45-FdE8>I*!+kM~iCb#L2UQ|<|1qGNJbTCgOr%eXCmny*zJ zS=oEZ>xv}bx3Vv$ng(7tx6s&ER_*i^U%UVAeER#B2}FVx2GN51I*erz;ncT}-~A9Y z@Wbqkq=fGVS0SJB5#1u9xMtC3*-g)NqKLpU82@%77txLo<_!u-K&iV+a~8@YBeOzH%*iWOe#!B}<739~9& zQn=;0cxl!bIAqw^b|7VKetYNlh)O*Yg*r5NO=^=<DG83HI3(1k5f?n)@<1=TK3)IpENJNS|s&;i0jI<6JFyxQ#pVp z|@I=S3OW7E_L>aSH(}H?JGWRe# z^S0PH^}Ph_u{X1CR_y8KxuMO-v4)(fv>rbH;;Q;L?XRF9t1BuG+ER3n;t}70jnb_7_W9V?)FM5 z=+~lN%C?`B%`3^l363j=@!sO-YQH^V;S$+0Xa$Q8obza|RYI)P(uX31$WYo|F#aQJ z_%95D$Vo4&gyTNyQz!ZDtD8@L@4WB$daf=`$>6!y)4AsxCr+r(rlo|14?qx=Z#^g! zT{BiYKQeqT%oKT?2JzVjkyQ;VPw}q_*Ix(Ylvp~T7mBDh7DCbo(+R+gt(KP}2bA`m zbIiE$)DhD9f<)TZHI7qm56Sy8xK1YL|7sNwq+@E`IYHOYF%VS9RbN$Gk8)V#1_FeZ zHg9IUl1^Nur|$#q#|sM3HWMww$*V7fvv862n0+$aP#ic-2yCjP|^!$55sVO z04guj=^XYD@ZSCaf75Y&2i5@i)JSr$ya{SR`Gn!T0_MTP;Jki|PjCBe=Dr~;oKB7V zG?z#CdkLTWFvlJz)|F`Pe9R)~2((k~JsW-km{7F%K5+X!>~`E`JT6BlVoLZqBX<_Y zEkY`(8eqV`OD?@4)@bsaj`0EEgudS1=qzpLl;zq@#psx_Z5O0V%?<_$Lc90E9TG*D z)-4&%cX1q8&66i>d1W^+G;yU8jxMQ&tqs<(4NQymfv;s~gmQzq#iq1|GfM1*)s-0k zcEmyyt-rl_;ag={$K$QE4dK+ZAuU9o%^tx2W}yZ3xb%-oJ)#>nYb@LLIGjW&dmW^y zevL{^pLkDJIH2jF4Q=UF^R^kDe&gG4DN*v%1jBfrp5u6HhN__)OY|reKBCGQ!Q3a0 zYwF&Oojkqy$EKE=aJRYi;d3Cuph`$kAmy8pqfH23U0EP9iCR@d^-UH#p*4y8wcC)d z9ki>q;7ctxt6Fpqu|6LRJVmQLxoPv%TWR;c6Er^YIz2unYL#dEN^j(iK6WS*iJ~b` zUO!?-~!<@+w8&U5fcT$JOFBLjCI@@wm6U zrzq@c_==qUu=aVB>8~k6iowV?=Oe~ny*-Iqt_)c7=;7+Fw3cwLQbtV=k#ARFz@LEu z7<&hPHD37*Us>k{u>PLU+O*@`+``cxmMKfnDUNQJzJe+o>w7~Do>JICQ02ykG|BxJ zoqnE)ag~d7`yqTec@q~0r&P=sH(XdF{pt+AqRybVgwOvfiqM7(gTz#PByB zKA))m!eVwSGhedZIS4}!9k$jVv6V9lntthf#orFr)70$T-Ztfr*m$RuZA_f?`I6g^ zeg;qg39sMhedZ3LNkOOpU027F-}Tky3}bUm)-p*^zk}n}p77dPItR^ClOUn^v>(53 z1jDM1<#7^SrK7#W6#v9uZfJ$)aLwS~b&-iQEsT|af8r0GU4PmTdYJr23puu5PIrK# zJ&l~{NXrFksq_V$*LND|T$)yi)9*UbSo9sOlpZKl3;z!8uWqs9~sePyz20m#o8dx$=*iosgdpcP4bO~DeSCfuz#Fi}OB7R{Py zPV_mJ{50C_e^+*u`n$cZNmBQia|4vrbMlTDN#x@#qc!7LJt)g-n5VL(Mavi1gVT^? zo6qkzr&hGwHToJhxRw?vk4KBXqr@|L*vIh6+m;*gNvO;kgfd-KvHMzEM*HuqB zK>?6#+YRacW5!d*J^FR-8Jw32hmf$LO;F(C)H&S%H54gIlB|fPr*Y6hgallNX3&S3 z@o(z|hoZSy!znDs@X*NtDI2INOi9676`wl);+LpK;KEAvGV-!4@0T-Y1fq>jm-EQ$ z2P$5yYc6h#cnn|G3ei2#e?h64XcPV1pey6!uQ3vJatz)Fx@fZoA)i|h&*q0@PsDSE z{C0JMN`I9RH!AsXu4s4KN!`uhS{kgGk$JDk_&ClYGD8EJoDq7pkgqu+y0C-thPw(? zXFYePmXEUzt<8VkU)c_u2sX0!T7zr3`Ws((RihiMFg2kEjV7RMh18C(>2=s|OahXE zJ^O%OlmlRKTVxW&>Z>i+_>SbYneP2&B}5VJ81Th_X1qTTKY-sR zv8PllKNhDK61I=x-G;BSj@BjTF_Y@Gu5*qb#>?Hmo92;xKJb+|SZxH8lF@5Gw5OYhp+#ltJjCBt-p%J} zdn*wt5G?H$yz$CF z<^+Z0@FYYFqWy}y0yqeD4GV!5{L|dWzz(DaghHijDQPxqif^k7IZ(@ujk7z+cdgJ>Dzd`FZdJ~|R zYl1)pja|gQ43e!8hZ5Fvai83ujcRH37IC*D;JD#Z@ox^erhwQ!elLA|r~jf?W53ft zjBckh*Q=_EAQz2Lf^Psm6IsS$8gEy)7KK7PUXCo-?)l)`kJS4kv8XFG8xRP@^{H#J z5mfo?u@O{V;H4oYj7M>h5<{XyLU1Ofy8cBOLv5#xYEpwn&zc>Gp+tRwkKL0cbYO9D z@u6E@!B;W(r+lv;L9Ad=ly@Z)Y!)ann}H4UW>B6Qj)A+(Gu`Bfnr+vX*I_4k<9ei& zq8{c=JMLP(maA)a2S`BBU9;oa4wxd(Tf#g_S_{L?{ zYKT3xVH>KhjkY|PsQP(CGk*t8iw6dfuP?k6c9-N?SXhjVj70G+5Ml+tlN*tQV(*IH zL|BsS#G`Bx4{EnNY?(>~ZevXAfVhd=6G9wnL!1g{mpT$|1T9wNlV>+46n}TNcdhZT z0=fj?f_;}5jQNFyy{gFku@Lhi%eF1{s~fq0jP1f@46xCWAG5=!k-=4%8&bRFlwv{Q z!3h%PO$NLeTrvwc?tl>vBS)hE(mWJj3wJ_V-8)2om=H|@>8U{JAxecoHphE?H6Fx( zYDF&$K9LWjHt?TbY1TMDcP^7aud?2jEl`q%a)AR&P6xWM$R@p{LDtxnUkP6;>z55y z0H*3Jq4_uPs(e+bzk5+jrd`1?pyNAeE7gamXmY}> zy@1-54^+_qo+?uM%DR@}vA=jpyORvWI6gL@{uxi>k{QG&h**C{M3O3R4b463=J~Of zbyAX30SugyI@(iQL5&xlh3ZM~N#~~`^uiHKJ?f8mAxtU;JL`V`Jv(x#eAB~tkjHHg za%qn$909Df^E3#js!;Bfu;^rH_08 zdQ}~I8^`|QGR~eta!So`a00Qb<8Ize515L)`cQykBU^Dp5oPii^Z7@&?;a;C!%$N| z+n2F_7!YVhd!uoK007%}um4b0@?c&TH2Ye}%_^Vy5Xtx5`)*|66Bn?T@otygOqfZC0A@)9ZWk200FfI^@I8LraSR)^neVNhJ|jEcpWXizs)SWS&xpaDM#(WK znM{y`7JcSq80z!gl7y73xt$5V&;-aLQ4M_)O=^^4*x()d!twT-N(heMwDd}VDXz;D zzw-o2gemYK6}U@y0EG8TB7^M&Xp~j9wvy299cKYdPw1_bv~V!jHW0LcX0#vF+7NI}v!_k^$&peP{fs(ONzzq{}`NSQ!8WcLGGw`~J;HkWO?@kQSP=B{!1l z2H17dU>-5^af`+GW*%T}$S=CRv)i=dyYl{Zv^}{FWvg5^{W0Zuw-?Tow zIX?SddN_=pt~~Mz%D{+Rnu!C4pxT`9$uAzW=!v-o#-}5)d^wHxrm|T>>L@FPL)dtbzn z8kJ4X!IO*vyS2l(Ad)8IZv`*ckC;$qYM+yovOFEz@m%5n5^exsgsH=_(nrRfNSHPfoWna>uxDG8H~ z3z*_=A0!uO1o37Bvz8!}%Qw5)K?F z+?@l=k{r>%lJu}B2tVvzeQlNg(NU+a9x1J}p&E?A!9y1i!RD!Z!7WOi^ zrMA`o$Edf_t)K<{1R%N;yJQ4d{T zzeXxTud{UB?tqq&DT~ix>0_F}!|3WaB;+F;;3H*TdHdBPTwmYc5ckpx!T-&TwZYwN z^1`ZU@};JNx_?S^__ixd{|C4N(HQ?;1wwso1buS}hAS%GkD&#+a0Wv6qNIJC(A5v^ zyoKPYr*pZd^M4!pVWjpMD50C-UM&`Yee!Q&#wsS*#KcQrbX9D5;Ht5p{bYIO4p?8^ z7$!XrpNWPgDrTOj#zGP>jx5hn`#)LeA#v!7pc=9(eH`ILYzq2x9u+|g(7ZKad$I9!K_K&-g+CB+%5OJoEBlYG_l++g!C3t_up|oQl9x0sRaQl7@6}y) zt$Iv;u%~^lnQ-?q*6};8x0u@FR1G?m%IFRj8*>;}@r3uJkF3EX>AX}N+@AqUGYb;W zqdZ+4i{^DZk%Xmh0w@A65+$F0)(PfR8_uc>b_?U8jQ%4d&p&&Iuv?x+lJku~DG?Fgnh)7^&Uu6{CLg6Dqs;cQT;m7`bY?auy*B#v-m)HKU|nUklvZzN$FdYMkzw9 zpceY0i7Gq(cdMn88P%HGE6;J(lVo`7DR1$bo>7rmyRP0uL1D1|;fmRp$%v1%{ z+TRSur;uVNr=YdGf3$Kie1x^mK5lK3lm)}@x0*3GRsvB37mJZriFC{xIn7nYaHJPDQ6;HjYT{2Rcb9AJD9prmdfbwMow6hNSd=7gN=N63}!GvR4QSlknvo$Ro_y zy!`Kyd=vA_Y2l~;q11W~D<9=tQ^WTV7EoWQ8yEnc3qlj+j)Zrk)GYq6LER08OD3ku z)77#p2t)JzOUS-)=9f*lY9g2qS4p94LJuc!M0W(!3pB+SWx9OMeJyQudEw^|-NOHc zqQA0hEMiq6_xi;M-tN$d9*)#;W;`ATzoP8@yza}i8BoNMd-okBQ+3eJV&HGV6QHK_ zc@{@ugizsZpBpLMu?=k%G&+>gfUG>1(EHs%F+tbh%ywY8Qx`D&opJ1}b%->9een9k zI^PeAN?4+pI|&h94eqCn{6}y*PMsgWx7mLRyM_L=M8v`k&+T&yQCv=4fioZ+&&*@X zOyY*%RBtAHfplXj?8TjK8L^IN;$tvWx|k@o&#Awdct+LR3j=KQGqSX<6nrDV({m^2 zMCsBiz11|#LQ@W^^q%GXS&Lh&pnrHins3sJR$4ZZ2R{Rv1nGPZg`qOV)ypvY!z`yS zuO=)(gv0b7Ilt`NsQT>+ptF|%U@O`?66lmBetejoRw*qj<7B7&_`pkyWC(x$b5R{? z4d!_)a6MZ3XQbA(x+vTQbl6N}8BH@H zGIq8E=`oP}iZK{mV+ecFzs}-*y?ib~kOZ2#egc;I+~d*21gAuTysW+FwRljO+DAxD zZt6i_%sv_DAxHCU0xsO_Gc&W2HgS!u)!{!z+7QrzXILo?;&ineZ{wy=eJHR4F4+Gn zKmK!2X1o6-1+16wiU^o_iPj@;$q5x7+Dmlxcpvyo`@973v)k&$1jMSv&BAZm5*`^# zz0VYv!Br?xh2R*h=JuV)Qq?c*^B({+uFW~BYxYj86__3;rECzvW>kISp{dVET3;^o zHfRAE`(mIG5(W2XG^69E@_e2y78KpcW3hWO`F_n>e*OO+&(RWOHj=8)ROm<5m+^DlEIkBQQYp5b zNSHQQY#vXz0FW4GyQuKcbKv!~e6`Pw)8QX@=C~3t-VlOM5z@$L85+?&tFr5o3kmu_ zASg)k;|Jw#5jJ>~(j5H!u9;AGWWV?mkaa|nwJQ@^)%OlzFrW1Bh<@- zWeDHswKX_Q3!EfT@CI0%0hBw`^uY~tO^=?rWE>=|lV@OQB&*fTM}i??{~yEz_bcXe zNhZtI50@QHY0z@**OXYDAyqKe-QOQuSeG(e8AxCSTyS4LxTWL))tOC9q6;zxjz76SlKRQfN~O+pCYR?-7O)DIW= zmHO4d46geyy)?j4CyQ!Mm3Gcq7=nqY){XgC(7Lc%KU7^ z?ET{3TJ14tyR@#^iHRHW+q=Y)(c~vV1=pA-SKlkw3_XBLU~nO@_rvjO{Q-t1sB~#T_ zwC=#m8<_wsJr<@mOGU9(Hocfty_u@yU#R*y6LqpeO1uc9&`RM~`$;h^6ab}T><9gz zLF+T?-Zlcix&uql-F#_hhU?w0Qpm+W#jE!b+z@bS{PlNt-nDV;MLT(Wla`c}%!}R1 z`$R(1mwG9~Z>5MsH$V6Kd0riPy9170cck>R$sMb|?Hw(#CV&p%MyP0hH{`XB=+P~Z z_%?(A!*SpR#uB16qC?;8e707=&?{R0N{n38cC=~+vSGJw*t^?e@o`VFF;0~-zKrAZ zZ{u;}niLMj&ZL@7k%n__fW$6m(gpj?F!-ou3Q>yNMA-fYG-&8cfE}8?wR6<(hNfQ( zC2qSz#GxOm%Xa{NbDj17;z-nJx6mSiKDxmBjZX#M>W-1&b(0!=z=Wqf?)tqr^l##} z5yU(K8Z?BQ1=$CcfT0^`*%semmOLx1m{b`sP06JLNXh+sZB2ay8c0dfmvxfl~@~FYfg5E7=S(VqR4zJY|&W1 z7wYMALUjqBO@muM$QM!0vL=(4p!>?7!t7q>Dwq_lB~0YaHi>z2qW~Bk4)eci{~I=< zeV5_gZ*@zrKD9nK$pJz*v0m%p7Sp!F+sM^bZLCBz3iL%R@PH}CPD^n%MtrCqAmM%? z>;JCTC2W-6rxDlMyo{>_1`U4vum{Y)2=#uaBQGIlM&uTK(nHV0=_1oa&n(2I$ezrX zy!n5+sm8Oim!S{_G%H>{S%5G>&*zDv)U-GUqC1TuC8!6>@462?vaeqPV!axCrt2K5}g)B}uM$qRZ}) zq&TO?c#yJJs53ZQh#J4eXF9(-{fA!y<_>@>rvP%i#U{L^a{rdUO!CQDLmCA9Kg>_A zAeX>r)a7=v-sSr^EVMd_m`8mS5uo1CM5*^@DX}1|kZ?9Bh4PRX?`N{c9NkLP*3v zdYelFCTskf4Xn|6{3C)z(&-t8ESs$T~g}Ra``sy#ls^Cc8 zFaCfXi->aH z%VMy#80`)?F2L3JnP#Hwq-@o?P8OXx4M=R!0Cn2DWaLIuTmgz`#*?S{jE8v{sdr|! z0bxTviV~IhNCT!s$ZQa&T#q=5%p+BbYT{QttYaQMqp0T+-Tl@l=-QxzdJtey`EycE zXt~6n9ReT=t$x4UqJBR$xUW$lUcT>SSs#GI!$?~%0dKO5M35(m^Uc@Ay?IgmY&jS-z2{0)&u27iAS5-LuknUmn1hT3> z=8{02Om3`NWltKNC2vjop{_dr4D`j?R&KQ~{QUPI>pi3Obv>w_>%}vcf4mN^YQ_W$ z*yL(Og0N0Mv~EAfzkq)&$~UpCUMXjI~0S=l}0g0mJFtvcxfHblKV9LuZnikI6aHV9>hzJL1S&T-Yl>@Hi9cEj z96b*n+UF2!xu=SX3L_KurXS4Vw}AiIBiD8#6D$A;v%T48qxa8OGkqHO;TzUp_e8JK zRqpXaI-z{5?`yTsQ;YxMmoOmzR)#_HfBTF}trDa#?UCRtxKW%pS91@5q01yenMJR^ zzkiNAJ!U*k9$Mg09n`2K26~rR2yfT#H`q(elZu#5LYI-pG6@>rw|AzU?zY%ee>-Zs z2*aVRY;Nx4;?zLbH?c-%N%!$ijWv78R)hxxFZ`@EK$WvT_7=#HkL#`1u@wtG+*Tgl%KO&SO?K0pt*O96R$kf?aLi{|L^ z(xtHw(->#BChS&3S~#BzubW37`=Y0j?;hA8x&p9+q(8WnWP|{(j`g?NGV7PsDPltq(o9WrBrGn5|S#Q(x3~bHX>+(xIlW|kw!t1i zk?XSODJepUpHo{DK#p>6z+v9o?^s5DQlArX-W1cJ-jw7HwMrIyFnIbzjEN`Qlb zn!vdlx-_g_@pTA~HR54p4ljxL3+_0W3zkU+0rA?rzo{TSR&W4KN^(}@s(%Cf9Zc52 z^AyM4NoHF(!A1Dhddl?XbHu)C=x~;hpH4+rH{f=-Ie0Ll@+frk`FFd8QU*r+Mf5O_ zu-yLQ0(7%aaR4D|c6Ek7Z?UfjMrVIv&{56WQ|diQ!OKlQ-S>*I`Jmxx3bS5auVz1e z;R2}fUS6;A@RJrvr(V;{<)28r4f|lExx2;U;~DayNOET5UV0|oI7_Ps!TpR+uO3~s zCwXqI&3G>@zE1pe(xE$$i*W&B39iQ-(;${an#Szr3%T)}%*E6;5MZ?eIm;{zt|`y8@(spW1DLT{L;8gn6qm5}=^%@U)Z z!MyHJ`x@w;saah_aS(&X7YR`bhu;fYK`9?+P-9WV=~cQ7^l_+pexqvOj|$nsgE;yIoF0iPTzmI2^`yjI=7(>g({?Y0+oBm0u$*DZ*tu zYEA#V^p{zJByTsHnHVwfUl9lU4o>n8agSQk!H}kni8WtM8;w3c6F)Rse~T=O2j|TO zN5dJPcGv^kq{oM_G38Ek0JRlP(;eW7r&v-&I`1ZJ5#w9?77$N3Dhkb>zg3?uj zbQN7cb*A}VF#|u@?OS#%*sq>t&kzVbva#V!%8=n!IO}}hQy*z-_YG^`{4Pt9j8Co3 zFJ)MjeX7b6Np0<*?F(n%1x2Noq^-`&PhS9eKDFco#}}aqYdyaW`Ep^OkkbQkBF%e2 zD!R@BKCr_yv)#jA#ntZ47Vg?r{+uFf15NLD%8ozAI31bhPlS7vR@IUZD9Yu?>mO&4 zBk9OFXrC|U?hOGTz`9Jud#cdEC}wFnMMU*+)%I12{o!Z$;*16i@UTc{y6H zsgSUGR%mARy9a`3h)^&F4P=YQNP%Z*ggG<`2Hlwg&Sfj$B5Fi5ZX4M}3!KfvdxJNt z2P&IoOo83or)cZ_8bWFvTAKwz@vJ;2PS9a9T;B^;X3=BGY z^lFV+Zx5M0%4DLdGa9fyg(5q}s=T+CzeZ(AJo7gp zpwLmipco_~2d;a(#FDpVP%+KG#J;y|s#cEwhT#kbI)q5y3z3dbQp-7=?Muo!OR zu^!%QJH8-sdK$#?s&Hj#HvrAL`{l+NVcm%(p-I^wB(n`;kP!UoYIcKf%j*lHvq7Ch z*aBOyyBrTwYGir>L?RlA{R;W%=6SGaH^CWuhmAR6%Ep{!DraA3ZDN=2jI45uP=vTe znuuaq;^7EX;w_V*AF1s7Y+CqucPHnXTEkhNdbts()>QR~usRQZx$ORm+MK?>V-SwY z$(e}Oz9;0m)FWQqtuCUv*Z_o^^&Xg>6CFz)FTI~rZ<(VGnB@s_8`>V5n{V#rP18}^ zr02V^4&adGG@m0?kT8b2Y8B8`2LXLXU5}I_@1^cnIB!A;c>PPl-{TwgUP6j|2I2mSfGAadcY`ME*?8A)V+w$Aj3~pxDgF#K5Y|tIQMl(uLP^E5_ z^Dt7=ACHN@lvY7bY=0K z8W1erRBJI>+iUh;TX}35-?#F>^_0t=g5O3%BOCR0;IcIZI5fqQG30;yykI$D>PthV zTlKVtQnnhYkBX-&Uf;h*EHC}0k4pJxStr+^*IDR!HT2>5c}0KxyuvmbV~Ih~W>4XL zen~P}jo?6X9rnQlu(w~%jTVIAlQRE2JUBYsDpoe}8t!i08PD>-S$Xvepev-$H;f+9O_oktee?g1lpn!kl_iJ<|k~Cxx1n^uZYlHm*Kc z=KHg@cJ`Ys7T#q^Q<$&r8q3L7S6)ghaJR%b)ez=b{X*c`!SH3&gv{w3zl_?|h1*;8-HQoP&AF#~~kAT=hU5`!_`vcN4 zaK2{WW;RV7VXfO=Efy2^%?zdh;L3Z&UE8-+WR%Z8gy9a*KqFlYW6%AgR^*FE^_ui_ z1YsQI(dP=11m33_uNxrjSl{t*9V+3-leZ{Mz_y(Aj+}$j85MS!=#DeEC2oZeNA7@p zM2(}FqkSJw@43~f+{aoUYP?V-?{kV&o38e8#`BuBm=BR0U2~QXkFaVr32iP7 z;3V_Z?S#R`h1wc%I%%+4V#sOKKm$!%r5%Zbs9l-%q62tKn%2wbibnVMH1C~AHkTIY zy)L7(u&9KyGG;P%#Xh5Ij8cHorHrjI*r>suyZp2v%+h(s=esrw+x*hUbP$CF=pWV= zf)dY-`L=`vrWVg3FW77PM;~gc758Tz3f8d_hq;Ddyf5(0-wTW?j85PK0^H%Wm!?HD z5}Ie)##;{1OVhgc>?m=Pq0uWaaoj^H0F?PWqP!C7FnjrOiA0@fOtZ6=bO$DhNUnss z|6WWyF>z7C;3qn|l57p)A#VOog1m}?&^zfjpK)CI{H3^xX|pa>YLLM{RS)kTv8)(8 zBUO%_??)d&s6u*_7ajk9tll^QcOLpLIKdPD;s|z*qmwVS3;(Zgx8YevII~WWhd|e; z9t*TwGI3gF?l)-igCB&Gm_l7O32eXnhzVnd1G~kNw7w-prMz@5{0M9CC4<0sGE#TRFozNWN zG$pnZ@BG~a9^@+Q-cm)n6Of9BEIwNPmD63OX7vrNjo2!Ys=mX<3H@*zvV#xG+C&^X zi*O~Jr}>0V4LdlR?g=JJI^oHkPZl`05DfeqoN+LkdIF=^<7u=YTIUCWQ!xnLyqXQqs`=&rnlEot@s6J4 zX>uJ&V$k_ij3yrlL;@3(%rzs?NYce#YQDZUo~bq-Fw(CLu=XO-85MRaE^ep@g_JDX z9tkpzu5-mgfp%Dn-kaZN+-3HD$_2K!t%=T*qS&RL3L{nwf4=;9@O-`V!PjY+;UR=6 zXrA1Qqze0jJ#|MK7EXY7r$oNL5`N?Xla$|wX&6?z{rW0%*nPAS^SC9AaE~dchd|cs z6A-2Pmntaz4@f?n;dGXw>j;d-DQK|$2Y<^m!y^#zB5g5|_;78IAV3;(tZ3{+kv~Y@ zEC2t2Fr<>p$^wfRtFP-<1U8H>-OmVWqrcc7=FYWwEd(E!Bb*qoX(}fQtsoGHys9c` z6$0vwbN30ntuRN3qYLw!$-csJ9IpYnzt@IMv|*kj*CGzy)x10nze_7W0RQS{bzI#~ zlm~3~udaId)7QiA<=oB4fAiQu2uoyC-(Ik!A`m8BRCg2!@Nk9!)^MEdDsoxI)mYW3 zQl6xl%^hL)2G$`$+g#Q50KCC=JKfX3;GTiE7{x`8r6l38iK1vczY_CW-FUC*PLMp7 z>m>tiMtk-S!<2|^SsijUscguj3M#tj9r&FjkW;KK-d&i&)dbc9L9(Ao4NpyDR2X;T z!FD>%<6nj$*fBPNyL;%{n-(&A-xE5(c_qjj`tpsRq9fPiZsO<%4aU2PgEI@hMG(Sh zbYxE?*z7g#Pn%a*s7USLltm~3VmZi&?>`WKKd@a>drJ3#$|~owpb;0wx6>SKpv_H* zt;H$D>%(}8IIg{e>c42L0SG$_*TMw=v2TxCzc3*R=JXk_O`Ad<7Xc#zbWB0(dEY`; zksGH_SfB|sMWhfd`s4?DHe1JhHjifZ88N4B{ESXXSDU$7Snw2J4^x{G@{o^ky@Un~ z!FaWEM;@>K_>z+nXNX|GzWTBXgKdb zDK|hTjP(8V0#MeaDYxe3Vno& z_7z1*JY5d=iP_38UD%XHo(pra7ibqYw4esB3WT|jYP|ihX8DP{MRi=jHfgNKuWZCl z+Duf7(zH+Uf_K`grWehM&+21ZbYQZBqu#?k51)soH|FTD>g*RmD84nIMkwuZi?q2c z6#VRFiumyG?0GeTH-@7mT|G&uP}8dZVG;#rf~Ix zdh_du&k{BUkSQkm`i=<{BLQ*-Vr|Yf{*HB;)Px3#=YS(=-p8M+OjeN)Ra9$Er6YNe zZxsU=k=mqm^uV<(NvW}{~wwKL_!~5dedIl#| z?EU^0&PdeJ;Aque&)!nyks_~tU(4X#XhY29nDS8Yp?y*={=lnlz>28w>eds;1(ML z>G>6qo=vi_L`)Ek72OLJa2pxlW#()CErh6_qSBg1(bf8alsef;NSJR=8_!+YU*EtJ zf5qjL5JcTvaX>izyr3DFDE4d};`{I2Bca{!mAC~|y3Wa1usIl-lzpaJ5ZgJ0^axw; zml0fg?~*C`%B^Ia?y*2O5a$_BAHVFKW#woy*A*&gB<>ox8cA=wlc~dDE#w~d9BQ=W zg$Zt`uUN<&%(dMs1Q_4hCZ$(lk{_}9{SU@LgXlaRDlrh5`mRGN5d4X-?X>V%(`2~& zP=ZqS{+EP>I-ZMjG9{ZG%7J&|Ru=y;o{%xOxy){;?>YL7hb{_4rH;OK4mtP(uxzid_eEG*kWFz-(`stUUu$?D2BO-s>r&8#i;Piea>Nd8ilL9EsL!2fY1EaIs z2w&>1+6H_%qe6NtI8N{R{NRoJMRV%HMX&paQrBbD{Y6QWRWP*3yOt~M zKRyWWZ*gp!vE(q)N3P9ujFMTl?Vw0uQdX&br$>c-@TNrHfI8i4{2{upbVk&mAL#ES z948-K4cs0ZJ#3~kFlo;!uLeVK!0I2t$F^Q=`z*g>8I6(@tNK6$nnP?_V;ojtT@EDA zPwdJ7wiUGmYdS2K#X3AA+RYYVQCj_R9jI6zhNhIgs5(^0CJtFS`9sLny_Y+ ztp0)@T!q|TF>&P~6t%JYJItvMD+UeCsWWC~P6iqDt}$*XZmy8Re^keDvQk@+*=*P#S42WDx;mHR8!50ZGRa zRm+vJoM03!zEs0sLfRuYD9kMT!VR!5+E_8*5nx5y0YY!X;L})*xiQS^(cnK!ATjxU!f!4 zL=RVKeI7-2{@h{f-dNSmxo)`Rd@R)b@g8-s1Mg7@kLlK*3Fu)dPn?blmrt)Fy@GR} z2Spv1C}YxWJPojE=&KdxOf~7m)5-{kxd5id7N2A z{$L%}>ByU_-z#UL38M#D3^LM3*Y!nRMqP<$I)On%1;j1~3=9i7nzr$<7eCciyrdi` zl+|AnUawT>N~NP`|K6!M6y$Q7%9ml}W5{p>3i~TD;o$Y}1GCFXQjb14uT0ddN2kR8 z01t(_c0X5z$?1NyW8JvmX9R>-|J&&J1Ib4h{O8BDJ-wm!#mLZyT9l$@O%bMI& zU`hDd;{<>WRyJl9IB-*_;D>*m1EB0N_?O8fT6cm`ShB=GEsn%?8>OkvrT9D>aO}I?zvvUei zfVxv^w>0W3J0Xe>HDyVO%p{u;hd*r;VWE`O$KiATd{@Mmnz1?xLTnTWP^wv+a@0DH zs^#_!xO}PAyq|-krr%zs#JbmnG7(9R63beinH>?%Qk6+g1N;;{x(Yo$`Ob&>g*Tv+ zT3U$T$^LwK0PggA=Dx*1r#jfphPXhw*oOpTrtC6GU?5iQ|p$yvqu^AQeBK!+`a` z<@}WzdG9dv-z)+D1zO|UDE|*}hW_SXRE-`mjeL^B>Gdn1he>h*h4~n!Au>jg>$f`~ ztXBM5dBk!|KZ6Mc%C4xJ&dI~WBQA$yvipD?Jf)gOm7Y3{?KiR*;;Sl3<1ClF$Y~3d z`$Ciu5@Hp?Al!cSIi$m?l$`oKEise#1O`;+i149L$So#dN}9I}30 z<5lZjD*gRK$&(HbvIui4R`73Y+)s^$L$&J<>OtdcFkaASOB^~Qz6Vd4e6CTd2cUnW zTPjznbaYF|SwCid5sI>T04eg2i-d5%{mNCNgi0Mt-=1Yfw2yz+TkdQolJj}1-hP|q zs&L-eeOBN+k}d(lvZ$4MjMkga_oVWHJ0p+wmRjQB(p!$xxhSx-!_KbiZB?gI=rxw+ z2=cl7f{(Ib*W@8s|1(nMJmY;XCeF~KcqMjsGB%j{=!1aX%t+^lz{iFR61Op~QnLU| z=`w9U4@kGO3C-%IXJ%i09C>(+3)v}?Tulr+GnsU-e!Nj=@*ZF!J00jKM!$UsAeWuF zX$x~RmT}UDg>r?fSMT!Fy_}SX!t(g|vt<>?DUm?(GqCd<$4sYptn!S}s=K5)8)wvD zieWJFgd8YFjosAg-hPuv7jO;?kt=G_n8Sm_;87ngWq*14v57eyg=cPMX-J3)wnA

Yi1?yhm5L!u-d7WoPSB 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�x^*O^vat2xuuzJhsB(|pz!N-(NpVvH>% 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 A^Z)<= diff --git a/launcher/VCMI_launcher.ico b/launcher/VCMI_launcher.ico index 40818c4644c43924635690ea58dcdce6786e5466..0c431b80834046e105793a1999046b8dcc0e4fad 100644 GIT binary patch literal 410222 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˻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

iI8w{Q=Js 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 G`~LwMC&t47 literal 32038 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 diff --git a/mapeditor/icons/menu-game.png b/mapeditor/icons/menu-game.png index 5f632e2b71bab48cd3c01d300a8146267c7c193a..71ae6ca544537f3aabd3cb5465e7386d7d37ac49 100644 GIT binary patch literal 5579 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

nesosX8jT5c-WqdczYXUx5f&xQm|xaqtO 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 z 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
    XJ;yKywoPa$5oXD13MK z@N)56-aH(V?1^^VJ4&y#A<$wg>xlW@Q<^`kAB6Gw`I}nK%Tw#_Z~ty?jWNN(IHE&= zzv^tAnkdw4mhyEu_aB(P#Ad;!t6nIdjZ`t?7~`aaybPt)@Ls5_8|j~nNoK%suK2E! zD3D2t<-s4l>V-OTKI8fRfFnZfm`7H}aIr2}tg72Vc8nzmUCXUia zh~6Jnm@hgq@IZ(NxNfcKKc!RSxY1Q+z;AoRyfjrz^$f3-d0E7SCN7tk$B)F~10y?% zM|OFk^>=IpN|^t;21qoT@DAM&?tJn%7kq`@eOdda)nlU6ALKz|7HP7`V&I6HB@86?f#ixW~$=T3sb0iE5c~6 z`0#Wof+JmD3`#iB{RYk3O3_I${Cb~c#EDSk;=fx*B@1Gzm?x7HEwX%9;?(fF=m!o6 zdU9{MF3ZI9q0!+Sm2S)NLvw*LXhi#it$XI(IUAX;ACg+p-Fw9%K!lQ_SeVN#+eRgK2%;sJxFPGL zKzBfZ9fGhQTS02hg)&l~%XgF?36cf00p=zWsI zAatJTjG%b=W$*DSZyEZ26>pTV3`BM_^Y-NQ`3*<_vq0iRP-tQB+jU*;@Lj;f_?1G* zw{g9Tob~KM^xQK6SrTF-f)wtGa3SNdqB1MM{%3h&xe`d`jW`iMCxK?weXtFbAC@+iv zW7c6D-mh1zyoQXpQur?yIu{C3H*BZs`@9c9FoRP^$S2NZS+s*B`Yr(L7TG{7A$7y- z=nChU0UEo+C2s}kjHI7RIhuSCJi$%-agN=yx`fW96ZI>`yMC%yV;8yY_|&xVgE{;V zn8i6TQJhIdpv!S*QCi(FU6Hzude3*DND;4?b~5Hl7bedu%0v`~xr`m3_2&1K%pT)eD_00f|(;q6FL6Nuh0Vn#sv-B)ea>d0gLy^AyReQM^3sp0sQA z)X0t=EGsuHkl}k3A*(P*aGgwnFf$#I%<{f^9ohMs;Wn|(X8ASJ#z~v-3nzVtIF6;7 z|9XOl@=v=ZQx@8h5RsowNdJ!Fj$FE2A;8a&y5GIk+e8tm8Nkn>(~6$ZoD9*J&4N+J zZ+t%;#|&?thV9GWM4h+`5$?Ua$4~x6v#EV_Qw9HKIR_TP^;lq$A6ADAyDg(X#wVjD zf5I;cr@>C+#9ibEHLpN(p0E3+Www4r>cE^&E-W@Qw5P!ni^~UC)dUlO!`E|tvf5ms zPlgIHC!KNvuQu2iS$8Rv@A5u{B5DU!-<(H{4s)r58c(eMTwEy}x$u$1cvpZ(VOyx3 zh^s48Z)o4C=)D>tI`W%$$s~)T^CTG{G(lxt`RzL~jg{$B7m#8O*j$LP>0vZJN;dZlCuTlvpR<3{Sd0JU}zVP0` zbR6^WLil?~OAJYO(Khb`xqv}kiJ7{t1^8{;!QBQ@9|N8(Q`+EzKCB6ka-61o&h>$JU>6Te%OJdftOIFtb=L2 zJ4%KwK~10El7_5Mz8pN;0#N=e>0+G)9;yZKqE2^=sQwcJuhirsi7Qk ze9@Ez&^`%iOXUk(pUFhKGdSMd0&tm59dnj{(zXbURB-603ZO5qXevyzk?3LrPDucp zg>T_oeR0^bImB;-9}^V&h3aDqu&LEMcY&>ZfnzhuMZR3d0e>3X0pfp>Z=*lNK12DL zXVwN2GrV9Jxws!`u6=y*?Rf9U=L_V^4I4&8%mDD|)2HTH74#_%3&3rf{GQ2X{CP~{ za-%{}M-qXf#pmAs!^jkGvJm_Hn_`>L3!2MqFI`T8H5r`D) z>IS zFMjsj#xk9y%d`0$9~&H^@nLV>a%KSFKed?O<47Fk7j4P@Feo!2JX^Ilv*qm{=h-;ua~ zumb7YKlC>+;7Mc#T?K8X0v0_TY!Pm2B?*h{$@O(Q+7b% z?$It)fKuDIv@Qm>ox&Y?Cs<-267~v^rO%@aoSVIU9Qz@qB>;HlKfG@-Y`-7I*5S*- zyOx}=b_m$d|aWOinx5 z+#|Jd0BYbLZKcfx=4aN=E&gin{1St)RBK~25bq87Ez`pQ@MTj3?lTa-7w*pb8H0Wv z5Zc=@)8!Q>`oM16fH4G=>)2C^-pnG&!qI7qa0yygN*Flg=E6J!&^&_=Y}e;A7-!JW z@9laiSeAoRKqHtD5&Y#}{^e~8b|u|XP->;QR5QIs4NlJwSnT)2Q zVq-B^U(A75P{3Zyx@y z?$X>JYdF0bI^aZTC_oCTH7RX(nen1Vb3noxoP!NM@W6Y6wuk%)GyEf+;@_9RudWhQ zfhG9?I8e`Pon59;93LyoQz3l$BUv*K!$$_>ZcftY^rFs3POl2T z>YL&x3%2srcr|3j^g$Jq3JS}}t>@uwK7z*P-m`q!fKhn)a!WieA{zsM>SX$!mtK8Y z#(<&Iq~0MM((XxMBwfuJ<>3uLp8xC#l<%Sl;mMDYW8#;fZ#cgB#_3@I__F96aG$-q z=DVBrjW|fOK3J?v``JJHpgp3@u+@>3F3@|X0pzypz`T&+BuzSy;=y1VeE?~J?&-dj zm50G8uf?B%{gj1IO7fVA!=xyA^ziz4rxOxNM0|vXBAn7+MNQ@`N z0+v3D4*nKgpF=5&h8PC?E?!lv2r^oJUq!WrG%s6SE zP5Q`Syzl~kp+2FVRaviantA@Y%q#=7X1sCnHYDMZ=0d{I76hnOXOBu{VAbuF&$1|sYxiKyB^5BKoZkzn@H5<5rbaTpYoxX$v zC?B+?2-t&K@ap$BBi8%1Be&ro*Cta%rblGM!gNMI3MX;Opk>J5m6(9jl%i5>Vy$Rd zND$s_N1yYg>+pf?TCxoMi-_BuHd?89*V?a&LYcU&OMefr(PRq@djPNXVQ`Vo+N0kS z+u2jS+te@dpHrcH1lvA9;W@7?ONU~m03x(6Y2ap!??QeU9g@Hyp9O_vtw8^?>`#Ff zm0J}}Xj=qTS;q~{uk~+?I56T+rK6RZYK+>B^I?jnmLGt`t!)+yrVCoC2>yTe-XF%2=GhWDdAn!kxBPex z{u(&wSx8uGJOgJXB#;o&x=0wfi-C|pI5;b3XIH%I43gecF%NGPj^qp_x0R+tFrQMWSkR`SyfkT*l>+MDkSELQRQvt zziBc(Qxsn?+fAr$`v@hzi07sGRxagpsBv;joFFW73}Jk6JG;b}Mv0;2wj_`;UUR*x zUKf0UO z_;llrtL&7JEG%^7XyP^L z7pY%55TnjsmxX>U@uPg!Ac5e=m@l&8Z5B=k2fib4#ZFVfEQT1J6WV{_LEoMXN?CByo{wU2*iU; zB@UNk<)0qV5_{)%yw(kv^db2QCcSeV@WseC5PLaYK5hYcTe9cC^PdNGY;oP?iFq6* z?pO#Lu~DV?u5{X?zCy~86PlkU-bPk2>BI3MBP-EmP(|{$u8R0$tXh`KmqaY$br|sI zi`r*q`hdQ~$2iXC-(zGnhO9j@UaTiWsd&W%P+ji%8jyP{yZ_{YOMc>Ge!hot}`ByIYrEy6C?>E zXkIS#%TTu1(|C_Y7zTcr#eJhSuL=gCUv0(@^{Rj}y}}MKclNy3_G<3LrG42(eY{v*tVL&F{J z@y)(TdJkB*&+ShnuvMP%W$7**COrO*Re}kw%Zx9pT3Fgr&u?5Vh&FK{&yJilTd4nx zjrskl_+mVFN20XN-v0VL4tMxV$kr-mJWrT^?d_(V0*K}NYZi^Oxb5`d5IgxE&{rFo z0nloUC_u$}RClz3=V9`T%%v=(AoDRKM@Ubi?FP?&CHV*dye+xqwD+gg=&ety>oPq4 z0s(+19b=LjaUlj5Hrk6Vn&}HD@e1bx{^>xt5#!VPG|`My58}80sh5ydc^n=0 zPj~h!@bJ_6|GB^O2mINe{c-mpHGAc~bGKotzs<3Q(RvqlZE9Z3*$3Fyl6iv(uAx=jeECgJ{Xhks57m)Hl2$ zyQ^o-JJM@z!$D}$e>}f^^|j9LN-X&{2VRM?z50U)W{f}WcsR#@`YB=n9!y^c2tYZz z$yZq0(Cm@{5#&AXeXBIE`8(22v@5R1IoECkXBK1a*b_vxm1dJ|j5%e#GcEB$9%@Cp zYw~3DTv3(UkBQlJb7y`8j-}0Saoi^Tj`m9MV(z)4gy&}TX9Emd+`n&V@#Jr7Tc9}y z2z!&9yf>YQ09})9XmRTbd!IZ()uG^(vo%o7?x2~_(9wNMThd-5vSUUaFa-KU> zEe*jqUZ0+90|eYK)TpEaW0EedT`?kpJ-Jo%XUb|T8ArV(l1^#9{{9Vr`_KLv{_Kx` zjC5}?8vP>@!x#>q<%}7hxgyu%nspR^K}ZN&_8eRxXW|@M6!Smr zPiyIG?2kVc{EoyHpZ>Zwrfq)i$^;xlwyir=g~gZLkF0ptfLJMF54(6YqF<=^lwPxi&~RMDr&g%m96ZCF0toaTEa&ul1ac87{jtC8>4+PI6K z?M*))u9z!Xv=1_N&7EqyjNs-N8R8W{>yG#n@GHkwtKD$HJ^J%3u69*t-!feU0&F<{ z5{%9IKRZ&{7}x1+;~HOhOrC@<4obDAj#FjfLegx6D>t~7 z;tO{1C3YVLzSOyKj4=1&#h6}

    j6={wAISz)pX$d(+~E0|0y2ZhK`&lh3e%Nr2x< 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 $}WX2)$;hOn(073#P?}e-<#Cq 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

    yYKje*q>Z%Xt?(J*_bCI zm+AqhF;m%MAh;5$Xq{6WU%al2AS666EMZJ9siu>z^GlWDlHtqUuHD4?<6H=`2_uF2 zW0A2l0(+2YFy6`m+uWForKj4-GrFeUXX8?567(Xfa9VthRGE>rDr0=L`G}97|5B|j z6{HHbACh-8CW1#cE_RWtQiNl!WJ`6h?chK;J>gW_PXNjMJi&_An6Qd?w}#k;{u`e5 zjgNJU3Plb|A56rN_!Tpv*Um0qkXO&_q2Q)SoRDc;XF9eUI@X#)(`*0r8lAFufh{YR zUCiLQ(zOEOs}%4|ZAdU9QNF**38*(kJij8x%j3o3R9wI)Fm&Cq5E~>{keROkfbu8| z7?Jo9EmQ421V+fYi0^PoElr_l4U83j(rDzr1@FlkCCVHps<|+1cbNa%zl=pgDQCo` z*C>~YevwTeK{W&U)tzVaTtjH}w$ePZ&_J^x6lGF+ERAi^gvXxJ_K|kW_&5bDPZgooEq~u?0*5=C>f)KdV?d2`Usa$rtLb zaxvR*9X5US(||@@pg4j}E@DHVSWgRqM6H#t7aNHcT?+T=_nl1)e67oj`Fdio1yymYmiTn2?DYpH`-cwJ2FspHh7)=hiw|_i)uuglD&OsHT_<<20aC-+3!8fK= zU}OOkAMLqFnlqL5!%s38i}#{L){a6!=94i|pU(&p_xo9yF}Ii)m0OM*?me#mDA$d? zU_H>|?8B^6`K;XV4DIna!CG5Ja8txek8LSmTFxf=I>}!J4)-b)Vk|Fpk)qm%(sN;Rhb< z_%BAqT#lI1CHBtCziFBY;0YHObInnAE-Vx zZ!UISUYy$89gKf4G2~`}&n%?5C$qU`%dWYPNZX%N(e;etG9i|IvoHFh@%(G}Vqj)o zp7EBrl(Z0TaOFsBZPI#82_m4$#ern}^QRyAZK}EdUv*dk-D>c4>5Mv#=GV z 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�_R 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 Ql)yi`^!MpebPk379~pe)hX4Qo literal 598147 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 >VbUcDcvy-ELpFd5qpSB;>O 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ⅈ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

    kf5W#4=EYxEtHHyf|cvP>pnD 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%0DRz`Kn*sp8|{hr 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�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

    (@bTP0$ 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 Date: Sat, 2 Sep 2023 00:18:23 +0400 Subject: [PATCH 0287/1248] Unify internal units --- launcher/modManager/cmodlist.cpp | 7 ++--- launcher/modManager/cmodlistview_moc.cpp | 33 ++++++++++++++---------- launcher/modManager/cmodmanager.cpp | 3 +-- 3 files changed, 22 insertions(+), 21 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index f04c485e2..247c8b18e 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -65,12 +65,9 @@ bool CModEntry::compareVersions(QString lesser, QString greater) QString CModEntry::sizeToString(double size) { - static const QString sizes[] = - { - /*"%1 B", */ "%1 KiB", "%1 MiB", "%1 GiB", "%1 TiB" - }; + static const std::array sizes { "%1 B", "%1 KiB", "%1 MiB", "%1 GiB", "%1 TiB" }; size_t index = 0; - while(size > 1024 && index < 4) + while(size > 1024 && index < sizes.size()) { size /= 1024; index++; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 8dd7549ce..3753134fc 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -26,6 +26,11 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/Languages.h" +inline double mbToBytes(double mb) +{ + return mb * 1024 * 1024; +} + void CModListView::setupModModel() { modModel = new CModListModel(this); @@ -246,7 +251,7 @@ QString CModListView::genModInfoText(CModEntry & mod) if(mod.getValue("localSize").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSize").toDouble()), lineTemplate.arg(tr("Size"))); if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("size").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble() * 1024.0), lineTemplate.arg(tr("Download size"))); + result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("size").toDouble())), lineTemplate.arg(tr("Download size"))); result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); @@ -538,7 +543,7 @@ void CModListView::on_updateButton_clicked() auto mod = modModel->getMod(name); // update required mod, install missing (can be new dependency) if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); } } @@ -568,7 +573,7 @@ void CModListView::on_installButton_clicked() { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); } } @@ -587,8 +592,8 @@ void CModListView::downloadFile(QString file, QString url, QString description, connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); - - QString progressBarFormat = "Downloading %s%. %p% (%v KB out of %m KB) finished"; + + QString progressBarFormat = tr("Downloading %s%. %p% (%v MB out of %m MB) finished"); progressBarFormat.replace("%s%", description); ui->progressBar->setFormat(progressBarFormat); @@ -599,16 +604,16 @@ void CModListView::downloadFile(QString file, QString url, QString description, void CModListView::downloadProgress(qint64 current, qint64 max) { - // display progress, in kilobytes - ui->progressBar->setMaximum(max / 1024); - ui->progressBar->setValue(current / 1024); + // display progress, in megabytes + ui->progressBar->setMaximum(max / (1024 * 1024)); + ui->progressBar->setValue(current / (1024 * 1024)); } void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors) { - QString title = "Download failed"; - QString firstLine = "Unable to download all files.\n\nEncountered errors:\n\n"; - QString lastLine = "\n\nInstall successfully downloaded?"; + QString title = tr("Download failed"); + QString firstLine = tr("Unable to download all files.\n\nEncountered errors:\n\n"); + QString lastLine = tr("\n\nInstall successfully downloaded?"); bool doInstallFiles = false; // if all files were d/loaded there should be no errors. And on failure there must be an error @@ -791,8 +796,8 @@ void CModListView::checkManagerErrors() QString errors = manager->getErrors().join('\n'); if(errors.size() != 0) { - QString title = "Operation failed"; - QString description = "Encountered errors:\n" + errors; + QString title = tr("Operation failed"); + QString description = tr("Encountered errors:\n") + errors; QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok); } } @@ -858,7 +863,7 @@ void CModListView::doInstallMod(const QString & modName) { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mod.getValue("size").toDouble() * 1024 * 1024); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); } } diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index b85c363d2..6e06b4973 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -94,13 +94,12 @@ void CModManager::loadMods() { for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next()) total += iter.fileInfo().size(); - total /= 1024; //to Kb } boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); auto mod = JsonUtils::JsonFromFile(pathToQString(name)); auto json = JsonUtils::toJson(mod); - json["localSize"].Integer() = total; + json["localSize"].Float() = total; if(!name.is_absolute()) json["storedLocaly"].Bool() = true; From 9cd3b537adb163ca9159b1699f5ade8f147c794a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 03:17:32 +0400 Subject: [PATCH 0288/1248] Add turn timer allowed option --- server/TurnTimerHandler.cpp | 11 ++++++++++- server/TurnTimerHandler.h | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index f7e7a2688..355eac96c 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -36,6 +36,7 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player) timers[player].isActive = true; timers[player].isBattle = false; lastUpdate[player] = std::numeric_limits::max(); + endTurnAllowed[player] = true; } } @@ -47,6 +48,13 @@ void TurnTimerHandler::setTimerEnabled(PlayerColor player, bool enabled) sendTimerUpdate(player); } +void TurnTimerHandler::setEndTurnAllowed(PlayerColor player, bool enabled) +{ + std::lock_guard guard(mx); + assert(player.isValidPlayer()); + endTurnAllowed[player] = enabled; +} + void TurnTimerHandler::sendTimerUpdate(PlayerColor player) { TurnTimeUpdate ttu; @@ -63,6 +71,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { if(si->turnTimerInfo.isEnabled()) { + endTurnAllowed[player] = true; auto & timer = timers[player]; if(si->turnTimerInfo.baseTimer > 0) timer.baseTimer += timer.turnTimer; @@ -125,7 +134,7 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) timer.baseTimer = 0; onPlayerMakingTurn(player, 0); } - else if(!gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries + else if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries gameHandler.turnOrder->onPlayerEndsTurn(state->color); } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 5039dc07b..65538d9ca 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -29,6 +29,7 @@ class TurnTimerHandler const int turnTimePropagateThreshold = 3000; std::map timers; std::map lastUpdate; + std::map endTurnAllowed; std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); @@ -48,4 +49,5 @@ public: void onBattleEnd(); void update(int waitTime); void setTimerEnabled(PlayerColor player, bool enabled); + void setEndTurnAllowed(PlayerColor player, bool enabled); }; From 0fc6e2b31667c0eabef53b894940e51e8a073397 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 03:17:51 +0400 Subject: [PATCH 0289/1248] rollback query blocking --- server/CGameHandler.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 2ca17f61d..55a7fe7c2 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1106,11 +1106,14 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER); const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining()); + const bool standAtObstacle = t.blocked && !t.visitable; + const bool standAtWater = !h->boat && t.terType->isWater() && (t.visitableObjects.empty() || !t.visitableObjects.back()->isCoastVisitable()); + //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) - if (((!t.terType->isPassable() || (t.blocked && !t.visitable && !canFly)) + if (((!t.terType->isPassable() || (standAtObstacle && !canFly)) && complain("Cannot move hero, destination tile is blocked!")) - || ((!h->boat && !canWalkOnSea && !canFly && t.terType->isWater() && (t.visitableObjects.size() < 1 || !t.visitableObjects.back()->isCoastVisitable())) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) + || ((standAtWater && !canFly && !canWalkOnSea) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) && complain("Cannot move hero, destination tile is on water!")) || ((h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) && complain("Cannot disembark hero, tile is blocked!")) @@ -1175,14 +1178,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo visitObjectOnTile(t, h); } - for(auto topQuery = queries->topQuery(h->tempOwner); true; topQuery = queries->topQuery(h->tempOwner)) - { - moveQuery = std::dynamic_pointer_cast(topQuery); - if(!moveQuery || (transit && result == TryMoveHero::SUCCESS)) - break; - - queries->popIfTop(moveQuery); - } + queries->popIfTop(moveQuery); logGlobal->trace("Hero %s ends movement", h->getNameTranslated()); return result != TryMoveHero::FAILED; }; From 48fb167feffbaa636e991c4e90a8e63362d1e6dc Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 03:27:46 +0400 Subject: [PATCH 0290/1248] Fix timer end turn while standing on obstacles/water --- server/CGameHandler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 55a7fe7c2..d6f700b6e 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1262,6 +1262,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if(h->boat && !h->boat->onboardAssaultAllowed) lookForGuards = IGNORE_GUARDS; + turnTimerHandler.setEndTurnAllowed(h->getOwner(), !standAtWater && !standAtObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); return true; } From e85eb36bbd654dad53f281e31a73b95af9b2f4cd Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 13:14:24 +0400 Subject: [PATCH 0291/1248] Battle timer fix --- server/TurnTimerHandler.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 355eac96c..4ef71ad35 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -160,7 +160,7 @@ void TurnTimerHandler::onBattleStart() std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + if(!si || !gs || !gs->curB) return; auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); @@ -174,6 +174,7 @@ void TurnTimerHandler::onBattleStart() { auto & timer = timers[i]; timer.isBattle = true; + timer.isActive = si->turnTimerInfo.isBattleEnabled(); timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); @@ -187,7 +188,7 @@ void TurnTimerHandler::onBattleEnd() std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + if(!si || !gs || !gs->curB) return; auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); @@ -201,7 +202,7 @@ void TurnTimerHandler::onBattleEnd() { auto & timer = timers[i]; timer.isBattle = false; - + timer.isActive = true; if(!pvpBattle) { if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) From 1ef407ea9fd7c0481e00ceffd908e43aa3e2bff1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 14:10:50 +0400 Subject: [PATCH 0292/1248] Fixes --- config/schemas/mod.json | 4 ++++ launcher/modManager/cdownloadmanager_moc.cpp | 3 +++ launcher/modManager/cmodlist.cpp | 5 ++++- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 2c1fc8501..e4dd19ab3 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -22,6 +22,10 @@ "type" : "string", "description" : "Author of the mod. Can be nickname, real name or name of team" }, + "size": { + "type" : "number", + "description" : "Approximate size of mod, compressed by zip algorithm, in Mb" + }, "changelog" : { "type" : "object", "description" : "List of changes/new features in each version", diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index 20d06881b..3a9c86471 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -141,6 +141,9 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte quint64 received = 0; for(auto & entry : currentDownloads) received += entry.bytesReceived > 0 ? entry.bytesReceived : 0; + + if(received > total) + total = received; emit downloadProgress(received, total); } diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 247c8b18e..f2700e759 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -324,9 +324,10 @@ CModEntry CModList::getMod(QString modname) const { if(repo.empty() || CModEntry::compareVersions(repo["version"].toString(), repoValMap["version"].toString())) { - //take valid download link and screenshots before assignment + //take valid download link, screenshots and mod size before assignment auto download = repo.value("download"); auto screenshots = repo.value("screenshots"); + auto size = repo.value("size"); repo = repoValMap; if(repo.value("download").isNull()) { @@ -334,6 +335,8 @@ CModEntry CModList::getMod(QString modname) const if(repo.value("screenshots").isNull()) //taking screenshot from the downloadable version repo["screenshots"] = screenshots; } + if(repo.value("size").isNull()) + repo["size"] = size; } } } From 348b6b0f328949cff6af42b0e6f5958f31d8e8a9 Mon Sep 17 00:00:00 2001 From: Nordsoft91 Date: Sat, 2 Sep 2023 14:11:56 +0400 Subject: [PATCH 0293/1248] Update launcher/modManager/cmodlistview_moc.cpp Co-authored-by: Ivan Savenko --- launcher/modManager/cmodlistview_moc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 3753134fc..243199a7c 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -26,7 +26,7 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/Languages.h" -inline double mbToBytes(double mb) +static double mbToBytes(double mb) { return mb * 1024 * 1024; } From 32ad463170de930022834328088fb21a8d1d85e0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 14:15:24 +0400 Subject: [PATCH 0294/1248] Rename --- launcher/modManager/cmodlistview_moc.cpp | 4 ++-- launcher/modManager/cmodmanager.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 243199a7c..dc960f435 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -248,8 +248,8 @@ QString CModListView::genModInfoText(CModEntry & mod) result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg(tr("Installed version"))); result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); - if(mod.getValue("localSize").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSize").toDouble()), lineTemplate.arg(tr("Size"))); + if(mod.getValue("localSizeBytes").isValid()) + result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("size").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("size").toDouble())), lineTemplate.arg(tr("Download size"))); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 6e06b4973..9918da3cb 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -99,7 +99,7 @@ void CModManager::loadMods() boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); auto mod = JsonUtils::JsonFromFile(pathToQString(name)); auto json = JsonUtils::toJson(mod); - json["localSize"].Float() = total; + json["localSizeBytes"].Float() = total; if(!name.is_absolute()) json["storedLocaly"].Bool() = true; From 3126875e6aba46ead533fe2f218dee28fc5c73c3 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sat, 2 Sep 2023 14:36:57 +0300 Subject: [PATCH 0295/1248] arts bulk move crash fixed --- client/NetPacksClient.cpp | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index a87ddc69b..2c692a31c 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -306,14 +306,13 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) } }; - ArtifactLocation srcLoc(pack.srcArtHolder, pack.artsPack0.front().srcPos); - ArtifactLocation dstLoc(pack.dstArtHolder, pack.artsPack0.front().dstPos); + auto srcOwner = std::get>(pack.srcArtHolder)->tempOwner; + auto dstOwner = std::get>(pack.dstArtHolder)->tempOwner; // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. - callInterfaceIfPresent(cl, srcLoc.owningPlayer(), &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); - - if (srcLoc.owningPlayer() != dstLoc.owningPlayer()) - callInterfaceIfPresent(cl, dstLoc.owningPlayer(), &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + if(srcOwner != dstOwner) + callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); applyMove(pack.artsPack0); if(pack.swap) From 05ba56a87375ded63fbf64961405f67ba266dd6a Mon Sep 17 00:00:00 2001 From: Gwart Date: Sat, 2 Sep 2023 16:21:54 +0200 Subject: [PATCH 0296/1248] #2646 add a null check if creature is empty --- client/windows/GUIClasses.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index cecd521ef..74ceb4317 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -748,6 +748,9 @@ void CExchangeController::moveStack( SlotID sourceSlot) { auto creature = source->getCreature(sourceSlot); + if(creature == nullptr) + return; + SlotID targetSlot = target->getSlotFor(creature); if(targetSlot.validSlot()) From 20fd0d8901d607604aa188b8fb6d2ce8da2bb040 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Sep 2023 18:35:32 +0200 Subject: [PATCH 0297/1248] fix cursor issue --- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/render/Canvas.cpp | 4 ++-- client/render/Canvas.h | 4 ++-- client/renderSDL/SDL_Extensions.cpp | 18 ++++++++++++------ client/renderSDL/SDL_Extensions.h | 2 +- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index af5f1a5a1..d77a939b6 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -175,7 +175,7 @@ void AdventureMapInterface::dim(Canvas & to) Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); if(backgroundDimLevel > 0) - to.drawColor(targetRect, colorToFill); + to.drawColor(targetRect, colorToFill, false); return; } } diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index bf779cc18..f087df4f8 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -166,11 +166,11 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG } } -void Canvas::drawColor(const Rect & target, const ColorRGBA & color) +void Canvas::drawColor(const Rect & target, const ColorRGBA & color, const bool replace) { Rect realTarget = target + renderArea.topLeft(); - CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); + CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color), replace); } SDL_Surface * Canvas::getInternalSurface() diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 773be3cae..82ea82019 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -93,8 +93,8 @@ public: /// renders multiple lines of text with specified parameters void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); - /// fills selected area with solid color - void drawColor(const Rect & target, const ColorRGBA & color); + /// fills selected area with solid color, allows replacing or overdrawing + void drawColor(const Rect & target, const ColorRGBA & color, const bool replace = true); /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 7b5e8b4c0..459f3f200 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -799,15 +799,21 @@ void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) fillRect(dst, allSurface, color); } -void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color ) +void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color, const bool replace) { SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); - uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); - SDL_FillRect(tmp, NULL, sdlColor); - SDL_BlitSurface(tmp, NULL, dst, &newRect); - SDL_FreeSurface(tmp); + + // replacing or overdrawing (relevant for transparency) + if(replace) + SDL_FillRect(dst, &newRect, sdlColor); + else + { + SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); + SDL_FillRect(tmp, NULL, sdlColor); + SDL_BlitSurface(tmp, NULL, dst, &newRect); + SDL_FreeSurface(tmp); + } } STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index 6239c82ed..630bd09e0 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -58,7 +58,7 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); void fillSurface(SDL_Surface * dst, const SDL_Color & color); - void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); + void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color, const bool replace = true); void updateRect(SDL_Surface * surface, const Rect & rect); From e0460a0dec788d390fdeaf486a83f725bee66144 Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:04:06 +0200 Subject: [PATCH 0298/1248] up should be first --- client/lobby/SelectionTab.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 7399c79dd..e2514cb8f 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -55,7 +55,11 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh if(aaa->isFolder != bbb->isFolder) return (aaa->isFolder > bbb->isFolder); else + { + if(boost::algorithm::starts_with(aaa->folderName, "..") || boost::algorithm::starts_with(bbb->folderName, "..")) + return boost::algorithm::starts_with(aaa->folderName, ".."); return boost::ilexicographical_compare(aaa->folderName, bbb->folderName); + } } auto a = aaa->mapHeader.get(); From 3d08518daccc240531d7e2679e1704391cb5044e Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:37:56 +0200 Subject: [PATCH 0299/1248] fix difficulty widget --- client/lobby/CSelectionBase.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index d38212f6a..91004a84b 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -218,6 +218,10 @@ void InfoCard::changeSelection() flagbox->recreate(); labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + mapInfo->mapHeader->difficulty]); iconDifficulty->setSelected(SEL->getCurrentDifficulty()); + if(SEL->screenType == ESelectionScreen::loadGame) + for(auto & button : iconDifficulty->buttons) + button.second->setEnabled(button.first == SEL->getCurrentDifficulty()); + const std::array difficultyPercent = {"80%", "100%", "130%", "160%", "200%"}; labelDifficultyPercent->setText(difficultyPercent[SEL->getCurrentDifficulty()]); From 53277306cdf2818584f0e5330770e06e07deb84d Mon Sep 17 00:00:00 2001 From: Michael <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:39:16 +0200 Subject: [PATCH 0300/1248] add also savegame --- client/lobby/CSelectionBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 91004a84b..26a9139f8 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -218,7 +218,7 @@ void InfoCard::changeSelection() flagbox->recreate(); labelDifficulty->setText(CGI->generaltexth->arraytxt[142 + mapInfo->mapHeader->difficulty]); iconDifficulty->setSelected(SEL->getCurrentDifficulty()); - if(SEL->screenType == ESelectionScreen::loadGame) + if(SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::saveGame) for(auto & button : iconDifficulty->buttons) button.second->setEnabled(button.first == SEL->getCurrentDifficulty()); From 75258baefc9cfc8d0b524273d14ce63884a2c5ef Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 2 Sep 2023 22:53:44 +0400 Subject: [PATCH 0301/1248] Chat minor improvements --- launcher/lobby/lobby_moc.cpp | 21 +++++++++++++++--- launcher/lobby/lobby_moc.h | 6 ++++++ launcher/lobby/lobby_moc.ui | 42 ++++++++++++++++++++++-------------- 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index 727cbcab1..d145b5240 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -41,10 +41,12 @@ Lobby::Lobby(QWidget *parent) : QString hostString("%1:%2"); hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); hostString = hostString.arg(settings["launcher"]["lobbyPort"].Integer()); - + namesCompleter.setModel(ui->listUsers->model()); + namesCompleter.setCompletionMode(QCompleter::InlineCompletion); ui->serverEdit->setText(hostString); ui->userEdit->setText(QString::fromStdString(settings["launcher"]["lobbyUsername"].String())); ui->kickButton->setVisible(false); + ui->messageEdit->setCompleter(&namesCompleter); } void Lobby::changeEvent(QEvent *event) @@ -151,6 +153,7 @@ void Lobby::serverCommand(const ServerCommand & command) try case JOINED: case KICKED: protocolAssert(args.size() == 2); + session = ""; joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); if(args[1] == username) @@ -261,7 +264,7 @@ void Lobby::serverCommand(const ServerCommand & command) try ui->listUsers->clear(); for(int i = 0; i < amount; ++i) { - ui->listUsers->addItem(new QListWidgetItem(args[i + 1])); + ui->listUsers->addItem(new QListWidgetItem("@" + args[i + 1])); } break; } @@ -322,6 +325,7 @@ catch(const ProtocolError & e) void Lobby::onDisconnected() { authentificationStatus = AuthStatus::AUTH_NONE; + session = ""; ui->stackedWidget->setCurrentWidget(ui->sessionsPage); ui->connectButton->setChecked(false); ui->serverEdit->setEnabled(true); @@ -342,7 +346,12 @@ void Lobby::chatMessage(QString title, QString body, bool isSystem) curs.movePosition(QTextCursor::End); curs.insertText(title + ": ", fmtTitle); curs.insertText(body + "\n", fmtBody); - ui->chat->ensureCursorVisible(); + + if(ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() > 32) + { + ui->chat->ensureCursorVisible(); + ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); + } } void Lobby::sysMessage(QString body) @@ -564,3 +573,9 @@ void Lobby::on_optLoadGame_toggled(bool checked) } } + +void Lobby::on_chatSwither_clicked() +{ + isGlobalChat = session.isEmpty() ? true : !isGlobalChat; + ui->chatSwither->setText(isGlobalChat ? tr("Global chat") : tr("Room chat")); +} diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h index c372b6204..9ec5b26cc 100644 --- a/launcher/lobby/lobby_moc.h +++ b/launcher/lobby/lobby_moc.h @@ -64,6 +64,8 @@ private slots: void on_optLoadGame_toggled(bool checked); + void on_chatSwither_clicked(); + private: QString serverUrl; int serverPort; @@ -76,6 +78,7 @@ private: QString username; QStringList gameArgs; QMap hostModsMap; + QCompleter namesCompleter; enum AuthStatus { @@ -83,6 +86,9 @@ private: }; AuthStatus authentificationStatus = AUTH_NONE; + + bool isGlobalChat = true; + std::chrono::time_point lastTimePointScrollBar; private: QMap buildModsMap() const; diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui index 8001d3c8a..23574507c 100644 --- a/launcher/lobby/lobby_moc.ui +++ b/launcher/lobby/lobby_moc.ui @@ -71,11 +71,25 @@ 0 - - - Players in lobby + + + 0 - + + + + Users in chat + + + + + + + Global chat + + + + @@ -109,22 +123,18 @@ - + + + + - Lobby chat + + + + type you message - - - - true - - - - - - From d0b35674473dd5839fbcb1f29831738bc50e4235 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 01:46:25 +0400 Subject: [PATCH 0302/1248] Advanced chat --- launcher/lobby/lobby_moc.cpp | 54 +++++++++++++++++++++++++++++++----- launcher/lobby/lobby_moc.ui | 2 +- 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index d145b5240..279def2ad 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -337,17 +337,57 @@ void Lobby::onDisconnected() void Lobby::chatMessage(QString title, QString body, bool isSystem) { - QTextCharFormat fmtBody, fmtTitle; - fmtTitle.setFontWeight(QFont::Bold); - if(isSystem) - fmtBody.setFontWeight(QFont::DemiBold); + const QTextCharFormat regularFormat; + const QString boldHtml = "%1"; + const QString colorHtml = "%2"; + bool meMentioned = false; + bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); QTextCursor curs(ui->chat->document()); curs.movePosition(QTextCursor::End); - curs.insertText(title + ": ", fmtTitle); - curs.insertText(body + "\n", fmtBody); - if(ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() > 32) + QString titleColor = "Olive"; + if(isSystem || title == "System") + titleColor = "ForestGreen"; + if(title == username) + titleColor = "Gold"; + + curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); + + QRegularExpression mentionRe("@[\\w\\d]+"); + auto subBody = body; + int mem = 0; + for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) + { + body.insert(mem + match.capturedEnd(), QChar(-1)); + body.insert(mem + match.capturedStart(), QChar(-1)); + mem += match.capturedEnd() + 2; + subBody = body.right(body.size() - mem); + } + auto pieces = body.split(QChar(-1)); + for(auto & block : pieces) + { + if(block.startsWith("@")) + { + if(block == "@" + username) + { + meMentioned = true; + curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); + } + else + curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); + } + else + { + if(isSystem) + curs.insertHtml(colorHtml.arg("ForestGreen", block)); + else + curs.insertText(block, regularFormat); + } + } + curs.insertText("\n", regularFormat); + + if(meMentioned || isScrollBarBottom) { ui->chat->ensureCursorVisible(); ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui index 23574507c..fb714f078 100644 --- a/launcher/lobby/lobby_moc.ui +++ b/launcher/lobby/lobby_moc.ui @@ -78,7 +78,7 @@ - Users in chat + Users in lobby From ef853ab96d5c288a72317a4bdb4311c402aa9ec3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 03:05:48 +0400 Subject: [PATCH 0303/1248] Move chat logic to separate class --- launcher/CMakeLists.txt | 3 + launcher/lobby/chat_moc.cpp | 141 +++++++++++++++++++++++++++++++++++ launcher/lobby/chat_moc.h | 58 ++++++++++++++ launcher/lobby/chat_moc.ui | 121 ++++++++++++++++++++++++++++++ launcher/lobby/lobby_moc.cpp | 114 ++++++---------------------- launcher/lobby/lobby_moc.h | 11 +-- launcher/lobby/lobby_moc.ui | 106 ++++++++++++-------------- 7 files changed, 396 insertions(+), 158 deletions(-) create mode 100644 launcher/lobby/chat_moc.cpp create mode 100644 launcher/lobby/chat_moc.h create mode 100644 launcher/lobby/chat_moc.ui diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 8a77fc06d..9fa61ab82 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -18,6 +18,7 @@ set(launcher_SRCS lobby/lobby.cpp lobby/lobby_moc.cpp lobby/lobbyroomrequest_moc.cpp + lobby/chat_moc.cpp ) set(launcher_HEADERS @@ -39,6 +40,7 @@ set(launcher_HEADERS lobby/lobby.h lobby/lobby_moc.h lobby/lobbyroomrequest_moc.h + lobby/chat_moc.h main.h ) @@ -52,6 +54,7 @@ set(launcher_FORMS updatedialog_moc.ui lobby/lobby_moc.ui lobby/lobbyroomrequest_moc.ui + lobby/chat_moc.ui ) set(launcher_TS diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp new file mode 100644 index 000000000..1c7c4aff7 --- /dev/null +++ b/launcher/lobby/chat_moc.cpp @@ -0,0 +1,141 @@ +#include "chat_moc.h" +#include "ui_chat_moc.h" + +Chat::Chat(QWidget *parent) : + QWidget(parent), + ui(new Ui::Chat) +{ + ui->setupUi(this); + + namesCompleter.setModel(ui->listUsers->model()); + namesCompleter.setCompletionMode(QCompleter::InlineCompletion); + + ui->messageEdit->setCompleter(&namesCompleter); + + for(auto i : {GLOBAL, ROOM}) + chatDocuments.push_back(new QTextDocument(this)); + + ui->chat->setDocument(chatDocuments[GLOBAL]); +} + +Chat::~Chat() +{ + delete ui; +} + +void Chat::setUsername(const QString & user) +{ + username = user; +} + +void Chat::setSession(const QString & s) +{ + session = s; + if(session.isEmpty()) + setChatId(GLOBAL); + else + setChatId(ROOM); +} + +void Chat::addUser(const QString & user) +{ + ui->listUsers->addItem(new QListWidgetItem("@" + user)); +} + +void Chat::clearUsers() +{ + ui->listUsers->clear(); +} + +void Chat::chatMessage(const QString & title, QString body, bool isSystem) +{ + const QTextCharFormat regularFormat; + const QString boldHtml = "%1"; + const QString colorHtml = "%2"; + bool meMentioned = false; + bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); + + QTextCursor curs(ui->chat->document()); + curs.movePosition(QTextCursor::End); + + QString titleColor = "Olive"; + if(isSystem || title == "System") + titleColor = "ForestGreen"; + if(title == username) + titleColor = "Gold"; + + curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); + + QRegularExpression mentionRe("@[\\w\\d]+"); + auto subBody = body; + int mem = 0; + for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) + { + body.insert(mem + match.capturedEnd(), QChar(-1)); + body.insert(mem + match.capturedStart(), QChar(-1)); + mem += match.capturedEnd() + 2; + subBody = body.right(body.size() - mem); + } + auto pieces = body.split(QChar(-1)); + for(auto & block : pieces) + { + if(block.startsWith("@")) + { + if(block == "@" + username) + { + meMentioned = true; + curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); + } + else + curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); + } + else + { + if(isSystem) + curs.insertHtml(colorHtml.arg("ForestGreen", block)); + else + curs.insertText(block, regularFormat); + } + } + curs.insertText("\n", regularFormat); + + if(meMentioned || isScrollBarBottom) + { + ui->chat->ensureCursorVisible(); + ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); + } +} + +void Chat::sysMessage(QString body) +{ + chatMessage("System", body, true); +} + +void Chat::on_messageEdit_returnPressed() +{ + emit messageSent(ui->messageEdit->text()); + ui->messageEdit->clear(); +} + +void Chat::on_sendButton_clicked() +{ + emit messageSent(ui->messageEdit->text()); + ui->messageEdit->clear(); +} + +void Chat::on_chatSwitch_clicked() +{ + if(chatId == GLOBAL && !session.isEmpty()) + setChatId(ROOM); + else + setChatId(GLOBAL); +} + +void Chat::setChatId(ChatId _chatId) +{ + static const QMap chatNames{{GLOBAL, "Global"}, {ROOM, "Room"}}; + + chatId = _chatId; + ui->chatSwitch->setText(chatNames[chatId] + " chat"); + ui->chat->setDocument(chatDocuments[chatId]); +} diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h new file mode 100644 index 000000000..a596b754a --- /dev/null +++ b/launcher/lobby/chat_moc.h @@ -0,0 +1,58 @@ +#ifndef CHAT_MOC_H +#define CHAT_MOC_H + +#include + +namespace Ui { +class Chat; +} + +class Chat : public QWidget +{ + Q_OBJECT + + enum ChatId + { + GLOBAL = 0, + ROOM + }; + + QCompleter namesCompleter; + QString username, session; + ChatId chatId = GLOBAL; + + QVector chatDocuments; + +private: + void setChatId(ChatId); + +public: + explicit Chat(QWidget *parent = nullptr); + ~Chat(); + + void setUsername(const QString &); + void setSession(const QString &); + + void clearUsers(); + void addUser(const QString & user); + + void chatMessage(const QString & title, QString body, bool isSystem = false); + +signals: + void messageSent(QString); + +public slots: + void sysMessage(QString body); + +private slots: + void on_messageEdit_returnPressed(); + + void on_sendButton_clicked(); + + void on_chatSwitch_clicked(); + +private: + Ui::Chat *ui; +}; + +#endif // CHAT_MOC_H diff --git a/launcher/lobby/chat_moc.ui b/launcher/lobby/chat_moc.ui new file mode 100644 index 000000000..a3208d982 --- /dev/null +++ b/launcher/lobby/chat_moc.ui @@ -0,0 +1,121 @@ + + + Chat + + + + 0 + 0 + 465 + 413 + + + + Form + + + + 2 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + + Users in lobby + + + -1 + + + + + + + Global chat + + + + + + + + + + 0 + 0 + + + + + 16777215 + 96 + + + + 0 + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + QListView::SinglePass + + + + + + + + + + -1 + + + 0 + + + + + + + + type you message + + + + + + + send + + + + + + + + + + diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index 279def2ad..7b3bae59e 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -34,19 +34,18 @@ Lobby::Lobby(QWidget *parent) : { ui->setupUi(this); - connect(&socketLobby, SIGNAL(text(QString)), this, SLOT(sysMessage(QString))); + connect(&socketLobby, SIGNAL(text(QString)), ui->chatWidget, SLOT(sysMessage(QString))); connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString))); connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected())); + connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString))); QString hostString("%1:%2"); hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); hostString = hostString.arg(settings["launcher"]["lobbyPort"].Integer()); - namesCompleter.setModel(ui->listUsers->model()); - namesCompleter.setCompletionMode(QCompleter::InlineCompletion); + ui->serverEdit->setText(hostString); ui->userEdit->setText(QString::fromStdString(settings["launcher"]["lobbyUsername"].String())); ui->kickButton->setVisible(false); - ui->messageEdit->setCompleter(&namesCompleter); } void Lobby::changeEvent(QEvent *event) @@ -112,7 +111,7 @@ void Lobby::serverCommand(const ServerCommand & command) try { case SRVERROR: protocolAssert(args.size()); - chatMessage("System error", args[0], true); + ui->chatWidget->chatMessage("System error", args[0], true); if(authentificationStatus == AuthStatus::AUTH_NONE) authentificationStatus = AuthStatus::AUTH_ERROR; break; @@ -121,7 +120,8 @@ void Lobby::serverCommand(const ServerCommand & command) try protocolAssert(args.size()); hostSession = args[0]; session = args[0]; - sysMessage("new session started"); + ui->chatWidget->setSession(session); + ui->chatWidget->sysMessage("new session started"); break; case SESSIONS: @@ -154,6 +154,7 @@ void Lobby::serverCommand(const ServerCommand & command) try case KICKED: protocolAssert(args.size() == 2); session = ""; + ui->chatWidget->setSession(session); joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); if(args[1] == username) @@ -161,8 +162,9 @@ void Lobby::serverCommand(const ServerCommand & command) try hostModsMap.clear(); ui->buttonReady->setText("Ready"); ui->optNewGame->setChecked(true); - sysMessage(joinStr.arg("you", args[0])); + ui->chatWidget->sysMessage(joinStr.arg("you", args[0])); session = args[0]; + ui->chatWidget->setSession(session); bool isHost = command.command == JOINED && hostSession == session; ui->optNewGame->setEnabled(isHost); ui->optLoadGame->setEnabled(isHost); @@ -170,7 +172,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } else { - sysMessage(joinStr.arg(args[1], args[0])); + ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0])); } break; @@ -247,7 +249,7 @@ void Lobby::serverCommand(const ServerCommand & command) try QString msg; for(int i = 1; i < args.size(); ++i) msg += args[i]; - chatMessage(args[0], msg); + ui->chatWidget->chatMessage(args[0], msg); break; } @@ -261,10 +263,10 @@ void Lobby::serverCommand(const ServerCommand & command) try amount = args[0].toInt(); protocolAssert(amount == (args.size() - 1)); - ui->listUsers->clear(); + ui->chatWidget->clearUsers(); for(int i = 0; i < amount; ++i) { - ui->listUsers->addItem(new QListWidgetItem("@" + args[i + 1])); + ui->chatWidget->addUser(args[i + 1]); } break; } @@ -280,7 +282,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } default: - sysMessage("Unknown server command"); + ui->chatWidget->sysMessage("Unknown server command"); } if(authentificationStatus == AuthStatus::AUTH_ERROR) @@ -295,7 +297,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } catch(const ProtocolError & e) { - chatMessage("System error", e.what(), true); + ui->chatWidget->chatMessage("System error", e.what(), true); } void Lobby::dispatchMessage(QString txt) try @@ -319,13 +321,14 @@ void Lobby::dispatchMessage(QString txt) try } catch(const ProtocolError & e) { - chatMessage("System error", e.what(), true); + ui->chatWidget->chatMessage("System error", e.what(), true); } void Lobby::onDisconnected() { authentificationStatus = AuthStatus::AUTH_NONE; session = ""; + ui->chatWidget->setSession(session); ui->stackedWidget->setCurrentWidget(ui->sessionsPage); ui->connectButton->setChecked(false); ui->serverEdit->setEnabled(true); @@ -335,82 +338,12 @@ void Lobby::onDisconnected() ui->sessionsTable->setRowCount(0); } -void Lobby::chatMessage(QString title, QString body, bool isSystem) -{ - const QTextCharFormat regularFormat; - const QString boldHtml = "%1"; - const QString colorHtml = "%2"; - bool meMentioned = false; - bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); - - QTextCursor curs(ui->chat->document()); - curs.movePosition(QTextCursor::End); - - QString titleColor = "Olive"; - if(isSystem || title == "System") - titleColor = "ForestGreen"; - if(title == username) - titleColor = "Gold"; - - curs.insertHtml(boldHtml.arg(colorHtml.arg(titleColor, title + ": "))); - - QRegularExpression mentionRe("@[\\w\\d]+"); - auto subBody = body; - int mem = 0; - for(auto match = mentionRe.match(subBody); match.hasMatch(); match = mentionRe.match(subBody)) - { - body.insert(mem + match.capturedEnd(), QChar(-1)); - body.insert(mem + match.capturedStart(), QChar(-1)); - mem += match.capturedEnd() + 2; - subBody = body.right(body.size() - mem); - } - auto pieces = body.split(QChar(-1)); - for(auto & block : pieces) - { - if(block.startsWith("@")) - { - if(block == "@" + username) - { - meMentioned = true; - curs.insertHtml(boldHtml.arg(colorHtml.arg("IndianRed", block))); - } - else - curs.insertHtml(colorHtml.arg("DeepSkyBlue", block)); - } - else - { - if(isSystem) - curs.insertHtml(colorHtml.arg("ForestGreen", block)); - else - curs.insertText(block, regularFormat); - } - } - curs.insertText("\n", regularFormat); - - if(meMentioned || isScrollBarBottom) - { - ui->chat->ensureCursorVisible(); - ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); - } -} - -void Lobby::sysMessage(QString body) -{ - chatMessage("System", body, true); -} - void Lobby::protocolAssert(bool expr) { if(!expr) throw ProtocolError("Protocol error"); } -void Lobby::on_messageEdit_returnPressed() -{ - socketLobby.send(ProtocolStrings[MESSAGE].arg(ui->messageEdit->text())); - ui->messageEdit->clear(); -} - void Lobby::on_connectButton_toggled(bool checked) { if(checked) @@ -418,6 +351,7 @@ void Lobby::on_connectButton_toggled(bool checked) ui->connectButton->setText(tr("Disconnect")); authentificationStatus = AuthStatus::AUTH_NONE; username = ui->userEdit->text(); + ui->chatWidget->setUsername(username); const int connectionTimeout = settings["launcher"]["connectionTimeout"].Integer(); auto serverStrings = ui->serverEdit->text().split(":"); @@ -438,9 +372,9 @@ void Lobby::on_connectButton_toggled(bool checked) ui->serverEdit->setEnabled(false); ui->userEdit->setEnabled(false); - sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); + ui->chatWidget->sysMessage("Connecting to " + serverUrl + ":" + QString::number(serverPort)); //show text immediately - ui->chat->repaint(); + ui->chatWidget->repaint(); qApp->processEvents(); socketLobby.connectServer(serverUrl, serverPort, username, connectionTimeout); @@ -450,7 +384,7 @@ void Lobby::on_connectButton_toggled(bool checked) ui->connectButton->setText(tr("Connect")); ui->serverEdit->setEnabled(true); ui->userEdit->setEnabled(true); - ui->listUsers->clear(); + ui->chatWidget->clearUsers(); hostModsMap.clear(); updateMods(); socketLobby.disconnectServer(); @@ -613,9 +547,7 @@ void Lobby::on_optLoadGame_toggled(bool checked) } } - -void Lobby::on_chatSwither_clicked() +void Lobby::onMessageSent(QString message) { - isGlobalChat = session.isEmpty() ? true : !isGlobalChat; - ui->chatSwither->setText(isGlobalChat ? tr("Global chat") : tr("Room chat")); + socketLobby.send(ProtocolStrings[MESSAGE].arg(message)); } diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h index 9ec5b26cc..5d65c3bac 100644 --- a/launcher/lobby/lobby_moc.h +++ b/launcher/lobby/lobby_moc.h @@ -33,12 +33,9 @@ public slots: void updateMods(); private slots: - void on_messageEdit_returnPressed(); - - void chatMessage(QString title, QString body, bool isSystem = false); - void sysMessage(QString body); void dispatchMessage(QString); void serverCommand(const ServerCommand &); + void onMessageSent(QString message); void on_connectButton_toggled(bool checked); @@ -64,8 +61,6 @@ private slots: void on_optLoadGame_toggled(bool checked); - void on_chatSwither_clicked(); - private: QString serverUrl; int serverPort; @@ -78,7 +73,6 @@ private: QString username; QStringList gameArgs; QMap hostModsMap; - QCompleter namesCompleter; enum AuthStatus { @@ -86,9 +80,6 @@ private: }; AuthStatus authentificationStatus = AUTH_NONE; - - bool isGlobalChat = true; - std::chrono::time_point lastTimePointScrollBar; private: QMap buildModsMap() const; diff --git a/launcher/lobby/lobby_moc.ui b/launcher/lobby/lobby_moc.ui index fb714f078..07406b877 100644 --- a/launcher/lobby/lobby_moc.ui +++ b/launcher/lobby/lobby_moc.ui @@ -14,6 +14,9 @@ + + 0 + @@ -62,77 +65,31 @@ + + -1 + 0 + + 0 + + + 10 + 0 - - - 0 - - - - - Users in lobby - - - - - - - Global chat - - - - - - - + - + 0 0 - - - 16777215 - 96 - - - - 0 - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - true - - - QListView::SinglePass - - - - - - - - - - - - - type you message - @@ -150,6 +107,18 @@ + + 0 + + + 0 + + + 0 + + + 0 + @@ -220,6 +189,21 @@ + + 0 + + + 0 + + + 0 + + + 0 + + + -1 + @@ -314,6 +298,14 @@ + + + Chat + QWidget +

    lobby/chat_moc.h
    + 1 + + From b6e2c454c0c3317068aa5479e86a917db9da13a4 Mon Sep 17 00:00:00 2001 From: Evgeniy Meshcheryakov Date: Sun, 3 Sep 2023 08:05:02 +0300 Subject: [PATCH 0304/1248] Show only non-empty teams in teams popup --- client/lobby/CSelectionBase.cpp | 43 ++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index d38212f6a..c96115ce5 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -389,32 +389,37 @@ CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr ico : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, "DIBOXBCK") { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.w = 256; - pos.h = 90 + 50 * SEL->getMapInfo()->mapHeader->howManyTeams; labelTeamAlignment = std::make_shared(128, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[657]); labelGroupTeams = std::make_shared(FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - for(int i = 0; i < SEL->getMapInfo()->mapHeader->howManyTeams; i++) + + std::vector> teams(PlayerColor::PLAYER_LIMIT_I); + for(ui8 j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) { - std::vector flags; - labelGroupTeams->add(128, 65 + 50 * i, boost::str(boost::format(CGI->generaltexth->allTexts[656]) % (i+1))); - - for(int j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) + if(SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) { - if((SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) - && SEL->getPlayerInfo(j).team == TeamID(i)) - { - flags.push_back(j); - } - } - - int curx = 128 - 9 * (int)flags.size(); - for(auto & flag : flags) - { - iconsFlags.push_back(std::make_shared(icons, flag, 0, curx, 75 + 50 * i)); - curx += 18; + teams[SEL->getPlayerInfo(j).team].insert(j); } } + + auto curIdx = 0; + for(const auto & team : teams) + { + if(team.empty()) + continue; + + labelGroupTeams->add(128, 65 + 50 * curIdx, boost::str(boost::format(CGI->generaltexth->allTexts[656]) % (curIdx + 1))); + int curx = 128 - 9 * team.size(); + for(const auto & player : team) + { + iconsFlags.push_back(std::make_shared(icons, player, 0, curx, 75 + 50 * curIdx)); + curx += 18; + } + ++curIdx; + } + pos.w = 256; + pos.h = 90 + 50 * curIdx; + background->scaleTo(Point(pos.w, pos.h)); center(); } From b0105e8a3ac61cda516495d4a810a8c244e1b393 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 16:47:34 +0400 Subject: [PATCH 0305/1248] Channels support --- launcher/lobby/chat_moc.cpp | 50 ++++++++++++++++++++++++++---------- launcher/lobby/chat_moc.h | 4 +++ launcher/lobby/lobby.h | 23 ++++++++++++++--- launcher/lobby/lobby_moc.cpp | 26 ++++++++++++++++--- launcher/lobby/lobby_moc.h | 1 + 5 files changed, 82 insertions(+), 22 deletions(-) diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp index 1c7c4aff7..8ec1b288f 100644 --- a/launcher/lobby/chat_moc.cpp +++ b/launcher/lobby/chat_moc.cpp @@ -15,7 +15,7 @@ Chat::Chat(QWidget *parent) : for(auto i : {GLOBAL, ROOM}) chatDocuments.push_back(new QTextDocument(this)); - ui->chat->setDocument(chatDocuments[GLOBAL]); + setChatId(GLOBAL); } Chat::~Chat() @@ -31,10 +31,15 @@ void Chat::setUsername(const QString & user) void Chat::setSession(const QString & s) { session = s; - if(session.isEmpty()) - setChatId(GLOBAL); - else - setChatId(ROOM); + + on_chatSwitch_clicked(); +} + +void Chat::setChannel(const QString & channel) +{ + static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; + + setChatId(chatNames.value(channel)); } void Chat::addUser(const QString & user) @@ -47,7 +52,7 @@ void Chat::clearUsers() ui->listUsers->clear(); } -void Chat::chatMessage(const QString & title, QString body, bool isSystem) +void Chat::chatMessage(const QString & title, const QString & channel, QString body, bool isSystem) { const QTextCharFormat regularFormat; const QString boldHtml = "%1"; @@ -55,7 +60,12 @@ void Chat::chatMessage(const QString & title, QString body, bool isSystem) bool meMentioned = false; bool isScrollBarBottom = (ui->chat->verticalScrollBar()->maximum() - ui->chat->verticalScrollBar()->value() < 24); - QTextCursor curs(ui->chat->document()); + static const QMap chatNames{{"global", GLOBAL}, {"room", ROOM}}; + QTextDocument * doc = ui->chat->document(); + if(chatNames.contains(channel)) + doc = chatDocuments[chatNames.value(channel)]; + + QTextCursor curs(doc); curs.movePosition(QTextCursor::End); QString titleColor = "Olive"; @@ -99,36 +109,48 @@ void Chat::chatMessage(const QString & title, QString body, bool isSystem) } curs.insertText("\n", regularFormat); - if(meMentioned || isScrollBarBottom) + if(doc == ui->chat->document() && (meMentioned || isScrollBarBottom)) { ui->chat->ensureCursorVisible(); ui->chat->verticalScrollBar()->setValue(ui->chat->verticalScrollBar()->maximum()); } } +void Chat::chatMessage(const QString & title, QString body, bool isSystem) +{ + chatMessage(title, "", body, isSystem); +} + void Chat::sysMessage(QString body) { chatMessage("System", body, true); } +void Chat::sendMessage() +{ + QString msg(ui->messageEdit->text()); + ui->messageEdit->clear(); + emit messageSent(msg); +} + void Chat::on_messageEdit_returnPressed() { - emit messageSent(ui->messageEdit->text()); - ui->messageEdit->clear(); + sendMessage(); } void Chat::on_sendButton_clicked() { - emit messageSent(ui->messageEdit->text()); - ui->messageEdit->clear(); + sendMessage(); } void Chat::on_chatSwitch_clicked() { + static const QMap chatNames{{GLOBAL, "global"}, {ROOM, "room"}}; + if(chatId == GLOBAL && !session.isEmpty()) - setChatId(ROOM); + emit channelSwitch(chatNames[ROOM]); else - setChatId(GLOBAL); + emit channelSwitch(chatNames[GLOBAL]); } void Chat::setChatId(ChatId _chatId) diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h index a596b754a..52068d955 100644 --- a/launcher/lobby/chat_moc.h +++ b/launcher/lobby/chat_moc.h @@ -25,6 +25,7 @@ class Chat : public QWidget private: void setChatId(ChatId); + void sendMessage(); public: explicit Chat(QWidget *parent = nullptr); @@ -32,14 +33,17 @@ public: void setUsername(const QString &); void setSession(const QString &); + void setChannel(const QString &); void clearUsers(); void addUser(const QString & user); + void chatMessage(const QString & title, const QString & channel, QString body, bool isSystem = false); void chatMessage(const QString & title, QString body, bool isSystem = false); signals: void messageSent(QString); + void channelSwitch(QString); public slots: void sysMessage(QString body); diff --git a/launcher/lobby/lobby.h b/launcher/lobby/lobby.h index 2ff03f721..4e77c802a 100644 --- a/launcher/lobby/lobby.h +++ b/launcher/lobby/lobby.h @@ -12,7 +12,7 @@ #include #include -const unsigned int ProtocolVersion = 4; +const unsigned int ProtocolVersion = 5; const std::string ProtocolEncoding = "utf8"; class ProtocolError: public std::runtime_error @@ -24,10 +24,10 @@ public: enum ProtocolConsts { //client consts - GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, + GREETING, USERNAME, MESSAGE, VERSION, CREATE, JOIN, LEAVE, KICK, READY, FORCESTART, HERE, ALIVE, HOSTMODE, SETCHANNEL, //server consts - SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE + SESSIONS, CREATED, JOINED, KICKED, SRVERROR, CHAT, CHATCHANNEL, START, STATUS, HOST, MODS, CLIENTMODS, USERS, HEALTH, GAMEMODE, CHANNEL }; const QMap ProtocolStrings @@ -88,6 +88,10 @@ const QMap ProtocolStrings //host sets game mode (new game or load game) //%1: game mode - 0 for new game, 1 for load game {HOSTMODE, "%1"}, + + //set new chat channel + //%1: channel name + {SETCHANNEL, "%1"}, //=== server commands === //server commands are started from :>>, arguments are enumerated by : symbol @@ -149,9 +153,16 @@ const QMap ProtocolStrings //received chat message //arg[0]: sender username - //arg[1]: message text + //arg[1]: channel + //arg[2]: message text {CHAT, "MSG"}, + //received chat message to specific channel + //arg[0]: sender username + //arg[1]: channel + //arg[2]: message text + {CHATCHANNEL, "MSGCH"}, + //list of users currently in lobby //arg[0]: amount of players, following arguments depend on it //arg[x]: username @@ -164,6 +175,10 @@ const QMap ProtocolStrings //game mode (new game or load game) set by host //arg[0]: game mode {GAMEMODE, "GAMEMODE"}, + + //chat channel changed + //arg[0]: channel name + {CHANNEL, "CHANNEL"}, }; class ServerCommand diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index 7b3bae59e..ad0d5dfa2 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -38,6 +38,7 @@ Lobby::Lobby(QWidget *parent) : connect(&socketLobby, SIGNAL(receive(QString)), this, SLOT(dispatchMessage(QString))); connect(&socketLobby, SIGNAL(disconnect()), this, SLOT(onDisconnected())); connect(ui->chatWidget, SIGNAL(messageSent(QString)), this, SLOT(onMessageSent(QString))); + connect(ui->chatWidget, SIGNAL(channelSwitch(QString)), this, SLOT(onChannelSwitch(QString))); QString hostString("%1:%2"); hostString = hostString.arg(QString::fromStdString(settings["launcher"]["lobbyUrl"].String())); @@ -121,7 +122,6 @@ void Lobby::serverCommand(const ServerCommand & command) try hostSession = args[0]; session = args[0]; ui->chatWidget->setSession(session); - ui->chatWidget->sysMessage("new session started"); break; case SESSIONS: @@ -155,14 +155,11 @@ void Lobby::serverCommand(const ServerCommand & command) try protocolAssert(args.size() == 2); session = ""; ui->chatWidget->setSession(session); - joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); - if(args[1] == username) { hostModsMap.clear(); ui->buttonReady->setText("Ready"); ui->optNewGame->setChecked(true); - ui->chatWidget->sysMessage(joinStr.arg("you", args[0])); session = args[0]; ui->chatWidget->setSession(session); bool isHost = command.command == JOINED && hostSession == session; @@ -172,6 +169,7 @@ void Lobby::serverCommand(const ServerCommand & command) try } else { + joinStr = (command.command == JOINED ? "%1 joined to the session %2" : "%1 left session %2"); ui->chatWidget->sysMessage(joinStr.arg(args[1], args[0])); } break; @@ -253,6 +251,21 @@ void Lobby::serverCommand(const ServerCommand & command) try break; } + case CHATCHANNEL: { + protocolAssert(args.size() > 2); + QString msg; + for(int i = 2; i < args.size(); ++i) + msg += args[i]; + ui->chatWidget->chatMessage(args[0], args[1], msg); + break; + } + + case CHANNEL: { + protocolAssert(args.size() == 1); + ui->chatWidget->setChannel(args[0]); + break; + } + case HEALTH: { socketLobby.send(ProtocolStrings[ALIVE]); break; @@ -551,3 +564,8 @@ void Lobby::onMessageSent(QString message) { socketLobby.send(ProtocolStrings[MESSAGE].arg(message)); } + +void Lobby::onChannelSwitch(QString channel) +{ + socketLobby.send(ProtocolStrings[SETCHANNEL].arg(channel)); +} diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h index 5d65c3bac..e4241d39c 100644 --- a/launcher/lobby/lobby_moc.h +++ b/launcher/lobby/lobby_moc.h @@ -36,6 +36,7 @@ private slots: void dispatchMessage(QString); void serverCommand(const ServerCommand &); void onMessageSent(QString message); + void onChannelSwitch(QString channel); void on_connectButton_toggled(bool checked); From 1f9bec32a117958c49111d56345504a10dee1f62 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 17:55:16 +0400 Subject: [PATCH 0306/1248] Maybe ununsed --- launcher/lobby/chat_moc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp index 8ec1b288f..aad01dd69 100644 --- a/launcher/lobby/chat_moc.cpp +++ b/launcher/lobby/chat_moc.cpp @@ -12,7 +12,7 @@ Chat::Chat(QWidget *parent) : ui->messageEdit->setCompleter(&namesCompleter); - for(auto i : {GLOBAL, ROOM}) + for([[maybe_unused]] auto i : {GLOBAL, ROOM}) chatDocuments.push_back(new QTextDocument(this)); setChatId(GLOBAL); From 96d244a3158e305973992820bfd342725cd6f92a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 18:28:22 +0400 Subject: [PATCH 0307/1248] Fix compiling --- launcher/lobby/chat_moc.cpp | 10 ++++++++++ launcher/lobby/chat_moc.h | 15 +++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/launcher/lobby/chat_moc.cpp b/launcher/lobby/chat_moc.cpp index aad01dd69..a260ba312 100644 --- a/launcher/lobby/chat_moc.cpp +++ b/launcher/lobby/chat_moc.cpp @@ -1,3 +1,13 @@ +/* + * chat_moc.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 "chat_moc.h" #include "ui_chat_moc.h" diff --git a/launcher/lobby/chat_moc.h b/launcher/lobby/chat_moc.h index 52068d955..d5a735f67 100644 --- a/launcher/lobby/chat_moc.h +++ b/launcher/lobby/chat_moc.h @@ -1,7 +1,16 @@ -#ifndef CHAT_MOC_H -#define CHAT_MOC_H +/* + * chat_moc.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 +#include namespace Ui { class Chat; @@ -58,5 +67,3 @@ private slots: private: Ui::Chat *ui; }; - -#endif // CHAT_MOC_H From ecf9b1aa2fba40b5254aa6af6508cde730ee67ad Mon Sep 17 00:00:00 2001 From: Evgeniy Meshcheryakov Date: Mon, 4 Sep 2023 10:08:56 +0300 Subject: [PATCH 0308/1248] Fix NKAI compilation with NKAI_PATHFINDER_TRACE_LEVEL>=2 --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 4ff2e4ef9..7707033eb 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -1208,7 +1208,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); @@ -1232,7 +1232,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); @@ -1258,7 +1258,7 @@ bool AINodeStorage::hasBetterChain( "Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i", source->coord.toString(), candidateNode->coord.toString(), - candidateNode->actor->hero->name, + candidateNode->actor->hero->getNameTranslated(), candidateNode->actor->chainMask, candidateNode->actor->armyValue, node.moveRemains - candidateNode->moveRemains); From 8406e3e4cddfccb99cf69168c892fa39c588a6fd Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 3 Sep 2023 23:41:57 +0400 Subject: [PATCH 0309/1248] Fix abandoned map crash --- lib/mapObjects/MiscObjects.h | 3 ++- mapeditor/inspector/inspector.cpp | 12 ++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index f159f418d..b0efd00cc 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -196,6 +196,8 @@ public: GameResID producedResource; ui32 producedQuantity; std::set abandonedMineResources; + + bool isAbandoned() const; private: void onHeroVisit(const CGHeroInstance * h) const override; @@ -209,7 +211,6 @@ private: std::string getObjectName() const override; std::string getHoverText(PlayerColor player) const override; - bool isAbandoned() const; public: template void serialize(Handler &h, const int version) { diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index eb61e66b8..a4742b054 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -184,8 +184,16 @@ void Initializer::initialize(CGMine * o) if(!o) return; o->tempOwner = defaultPlayer; - o->producedResource = GameResID(o->subID); - o->producedQuantity = o->defaultResProduction(); + if(o->isAbandoned()) + { + for(auto r = GameResID(0); r < GameResID::COUNT; ++r) + o->abandonedMineResources.insert(r); + } + else + { + o->producedResource = GameResID(o->subID); + o->producedQuantity = o->defaultResProduction(); + } } void Initializer::initialize(CGResource * o) From c3663e4e1b53f3189e674001a2c07d2e59d749bb Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 4 Sep 2023 01:38:55 +0400 Subject: [PATCH 0310/1248] Fix neutral objects serializing --- lib/mapObjects/CGObjectInstance.cpp | 3 +++ mapeditor/inspector/inspector.cpp | 3 +++ 2 files changed, 6 insertions(+) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 4b081c4e5..9a7ec6ecf 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -341,6 +341,9 @@ void CGObjectInstance::serializeJsonOptions(JsonSerializeFormat & handler) void CGObjectInstance::serializeJsonOwner(JsonSerializeFormat & handler) { + if(handler.saving && tempOwner == PlayerColor::NEUTRAL) + return; + ui8 temp = tempOwner.getNum(); handler.serializeEnum("owner", temp, PlayerColor::NEUTRAL.getNum(), GameConstants::PLAYER_COLOR_NAMES); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index a4742b054..cbedb899a 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -118,7 +118,10 @@ void Initializer::initialize(CGHeroInstance * o) o->tempOwner = defaultPlayer; if(o->ID == Obj::PRISON) + { + o->subID = 0; o->tempOwner = PlayerColor::NEUTRAL; + } if(o->ID == Obj::HERO) { From b7a438e8f4ae745b0010795bb3f0c7fc7fbdade8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 4 Sep 2023 02:57:37 +0400 Subject: [PATCH 0311/1248] Fix hero initialization form random map --- mapeditor/mapcontroller.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 70e3067c9..0f1703926 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -142,6 +142,7 @@ void MapController::repairMap() { nih->typeName = "prison"; nih->subTypeName = "prison"; + nih->subID = 0; } nih->type = type; From 8f5594613b486bbd18edf7a20f894983e583e0ce Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 4 Sep 2023 02:57:51 +0400 Subject: [PATCH 0312/1248] Fix signed number serialization --- lib/mapObjects/CGObjectInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 9a7ec6ecf..edaf90447 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -344,7 +344,7 @@ void CGObjectInstance::serializeJsonOwner(JsonSerializeFormat & handler) if(handler.saving && tempOwner == PlayerColor::NEUTRAL) return; - ui8 temp = tempOwner.getNum(); + si8 temp = tempOwner.getNum(); handler.serializeEnum("owner", temp, PlayerColor::NEUTRAL.getNum(), GameConstants::PLAYER_COLOR_NAMES); From 50d89dec241434ee14a7a4d3d1deec87a7f963b3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 4 Sep 2023 03:33:01 +0400 Subject: [PATCH 0313/1248] Fix spells issue in map editor --- mapeditor/inspector/inspector.cpp | 10 +++++----- mapeditor/validator.cpp | 5 +++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index cbedb899a..f4fcf0f0b 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -172,7 +172,7 @@ void Initializer::initialize(CGArtifact * o) std::vector out; for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - //if(map->isAllowedSpell(spell->id)) + if(VLC->spellh->getDefaultAllowed().at(spell->id)) { out.push_back(spell->id); } @@ -300,10 +300,10 @@ void Inspector::updateProperties(CGArtifact * o) auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) { - //if(map->isAllowedSpell(spell->id)) - delegate->options << QObject::tr(spell->getJsonKey().c_str()); + if(map->allowedSpells.at(spell->id)) + delegate->options << QObject::tr(spell->getNameTranslated().c_str()); } - addProperty("Spell", VLC->spellh->getById(spellId)->getJsonKey(), delegate, false); + addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); } } } @@ -540,7 +540,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant { for(auto spell : VLC->spellh->objects) { - if(spell->getJsonKey() == value.toString().toStdString()) + if(spell->getNameTranslated() == value.toString().toStdString()) { o->storedArtifact = ArtifactUtils::createScroll(spell->getId()); break; diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 4b70ddc84..2ba230d57 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -16,6 +16,7 @@ #include "../lib/mapObjects/MapObjects.h" #include "../lib/modding/CModHandler.h" #include "../lib/modding/CModInfo.h" +#include "../lib/spells/CSpellHandler.h" #include "../lib/CHeroHandler.h" Validator::Validator(const CMap * map, QWidget *parent) : @@ -141,8 +142,8 @@ std::list Validator::validate(const CMap * map) { if(ins->storedArtifact) { - if(!map->allowedSpells[ins->storedArtifact->getId().getNum()]) - issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); + if(!map->allowedSpells[ins->storedArtifact->getScrollSpellID()]) + issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toSpell(VLC->spells())->getNameTranslated().c_str()), false); } else issues.emplace_back(QString(tr("Spell scroll %1 doesn't have instance assigned and must be removed")).arg(ins->instanceName.c_str()), true); From 823ffa7a079e75836be7a65e6f893ebecc715f89 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 23 Aug 2023 15:07:50 +0300 Subject: [PATCH 0314/1248] Always use ResourcePath for referencing images and animations --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- client/CMT.cpp | 2 +- client/CMusicHandler.cpp | 10 +- client/CPlayerInterface.cpp | 6 +- client/CServerHandler.cpp | 2 +- client/CVideoHandler.cpp | 2 +- client/Client.cpp | 2 +- client/ClientCommandManager.cpp | 12 +- client/adventureMap/AdventureMapWidget.cpp | 32 ++- client/adventureMap/AdventureMapWidget.h | 4 +- client/adventureMap/AdventureOptions.cpp | 12 +- client/adventureMap/CInfoBar.cpp | 38 ++-- client/adventureMap/CInfoBar.h | 3 +- client/adventureMap/CList.cpp | 20 +- client/adventureMap/CMinimap.cpp | 2 +- client/adventureMap/CResDataBar.cpp | 4 +- client/adventureMap/CResDataBar.h | 5 +- client/adventureMap/TurnTimerWidget.cpp | 4 +- client/battle/BattleAnimationClasses.cpp | 16 +- client/battle/BattleAnimationClasses.h | 13 +- client/battle/BattleEffectsController.cpp | 6 +- client/battle/BattleFieldController.cpp | 16 +- client/battle/BattleInterface.cpp | 4 +- client/battle/BattleInterfaceClasses.cpp | 40 ++-- client/battle/BattleObstacleController.cpp | 8 +- client/battle/BattleObstacleController.h | 4 +- client/battle/BattleProjectileController.cpp | 6 +- client/battle/BattleProjectileController.h | 5 +- client/battle/BattleSiegeController.cpp | 46 ++-- client/battle/BattleSiegeController.h | 5 +- client/battle/BattleStacksController.cpp | 8 +- client/battle/BattleWindow.cpp | 18 +- client/battle/CreatureAnimation.cpp | 2 +- client/battle/CreatureAnimation.h | 4 +- client/gui/CursorHandler.cpp | 10 +- client/gui/CursorHandler.h | 3 +- client/gui/InterfaceObjectConfigurable.cpp | 20 +- client/lobby/CBonusSelection.cpp | 21 +- client/lobby/CLobbyScreen.cpp | 16 +- client/lobby/CSavingScreen.cpp | 4 +- client/lobby/CScenarioInfoScreen.cpp | 2 +- client/lobby/CSelectionBase.cpp | 22 +- client/lobby/OptionsTab.cpp | 50 ++--- client/lobby/OptionsTab.h | 2 +- client/lobby/RandomMapTab.cpp | 4 +- client/lobby/SelectionTab.cpp | 46 ++-- client/lobby/SelectionTab.h | 13 +- client/mainmenu/CCampaignScreen.cpp | 6 +- client/mainmenu/CMainMenu.cpp | 48 ++--- client/mainmenu/CMainMenu.h | 2 +- client/mainmenu/CreditsScreen.cpp | 2 +- client/mapView/MapRenderer.cpp | 22 +- client/mapView/MapRenderer.h | 8 +- client/mapView/MapViewCache.cpp | 2 +- client/render/CAnimation.cpp | 38 ++-- client/render/CAnimation.h | 5 +- client/render/CBitmapHandler.cpp | 4 +- client/render/CDefFile.cpp | 10 +- client/render/CDefFile.h | 3 +- client/render/Graphics.cpp | 24 +-- client/render/Graphics.h | 5 +- client/render/IImage.h | 6 +- client/renderSDL/CBitmapFont.cpp | 4 +- client/renderSDL/CBitmapFont.h | 4 +- client/renderSDL/CBitmapHanFont.cpp | 2 +- client/renderSDL/CTrueTypeFont.cpp | 2 +- client/renderSDL/SDLImage.cpp | 6 +- client/widgets/Buttons.cpp | 6 +- client/widgets/Buttons.h | 9 +- client/widgets/CArtifactHolder.cpp | 6 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 4 +- client/widgets/CComponent.cpp | 12 +- client/widgets/CComponent.h | 6 +- client/widgets/CGarrisonInt.cpp | 2 +- client/widgets/CWindowWithArtifacts.cpp | 2 +- client/widgets/ComboBox.cpp | 2 +- client/widgets/ComboBox.h | 2 +- client/widgets/CreatureCostBox.cpp | 2 +- client/widgets/Images.cpp | 16 +- client/widgets/Images.h | 17 +- client/widgets/MiscWidgets.cpp | 48 ++--- client/widgets/RadialMenu.cpp | 8 +- client/widgets/Slider.cpp | 8 +- client/widgets/TextControls.cpp | 4 +- client/widgets/TextControls.h | 5 +- client/windows/CCastleInterface.cpp | 79 ++++--- client/windows/CCastleInterface.h | 4 +- client/windows/CCreatureWindow.cpp | 60 +++--- client/windows/CCreatureWindow.h | 7 +- client/windows/CHeroBackpackWindow.cpp | 4 +- client/windows/CHeroWindow.cpp | 40 ++-- client/windows/CKingdomInterface.cpp | 62 +++--- client/windows/CKingdomInterface.h | 10 +- client/windows/CMessage.cpp | 4 +- client/windows/CPuzzleWindow.cpp | 8 +- client/windows/CQuestLog.cpp | 10 +- client/windows/CQuestLog.h | 2 +- client/windows/CSpellWindow.cpp | 22 +- client/windows/CTradeWindow.cpp | 64 +++--- client/windows/CTradeWindow.h | 6 +- client/windows/CWindowObject.cpp | 12 +- client/windows/CWindowObject.h | 13 +- client/windows/CreaturePurchaseCard.cpp | 8 +- client/windows/GUIClasses.cpp | 166 +++++++------- client/windows/InfoWindows.cpp | 18 +- client/windows/InfoWindows.h | 4 +- client/windows/QuickRecruitmentWindow.cpp | 10 +- .../windows/settings/AdventureOptionsTab.cpp | 4 +- client/windows/settings/BattleOptionsTab.cpp | 4 +- client/windows/settings/GeneralOptionsTab.cpp | 4 +- client/windows/settings/OtherOptionsTab.cpp | 4 +- .../windows/settings/SettingsMainWindow.cpp | 4 +- cmake_modules/VCMI_lib.cmake | 4 +- launcher/firstLaunch/firstlaunch_moc.cpp | 4 +- launcher/mainwindow_moc.cpp | 2 +- launcher/modManager/cmodmanager.cpp | 4 +- lib/BattleFieldHandler.cpp | 2 +- lib/BattleFieldHandler.h | 3 +- lib/CBonusTypeHandler.cpp | 6 +- lib/CBonusTypeHandler.h | 2 +- lib/CConfigHandler.cpp | 4 +- lib/CCreatureHandler.cpp | 6 +- lib/CCreatureHandler.h | 5 +- lib/CCreatureSet.cpp | 2 +- lib/CCreatureSet.h | 2 +- lib/CGeneralTextHandler.cpp | 10 +- lib/CHeroHandler.cpp | 6 +- lib/CHeroHandler.h | 9 +- lib/CTownHandler.cpp | 34 ++- lib/CTownHandler.h | 23 +- lib/IBonusTypeHandler.h | 4 +- lib/JsonDetail.cpp | 4 +- lib/JsonNode.cpp | 16 +- lib/JsonNode.h | 10 +- lib/ObstacleHandler.cpp | 2 +- lib/ObstacleHandler.h | 5 +- lib/RiverHandler.cpp | 2 +- lib/RiverHandler.h | 3 +- lib/RoadHandler.cpp | 2 +- lib/RoadHandler.h | 3 +- lib/ScriptHandler.cpp | 4 +- lib/TerrainHandler.cpp | 2 +- lib/TerrainHandler.h | 3 +- lib/battle/BattleInfo.cpp | 2 +- lib/battle/CObstacleInstance.cpp | 16 +- lib/battle/CObstacleInstance.h | 13 +- lib/campaign/CampaignHandler.cpp | 8 +- lib/campaign/CampaignState.cpp | 18 +- lib/campaign/CampaignState.h | 13 +- lib/filesystem/AdapterLoaders.cpp | 30 +-- lib/filesystem/AdapterLoaders.h | 28 +-- lib/filesystem/CArchiveLoader.cpp | 16 +- lib/filesystem/CArchiveLoader.h | 10 +- lib/filesystem/CFilesystemLoader.cpp | 24 +-- lib/filesystem/CFilesystemLoader.h | 16 +- lib/filesystem/CZipLoader.cpp | 14 +- lib/filesystem/CZipLoader.h | 12 +- lib/filesystem/Filesystem.cpp | 16 +- lib/filesystem/Filesystem.h | 4 +- lib/filesystem/ISimpleResourceLoader.h | 14 +- lib/filesystem/ResourceID.h | 158 -------------- .../{ResourceID.cpp => ResourcePath.cpp} | 106 ++++----- lib/filesystem/ResourcePath.h | 203 ++++++++++++++++++ lib/gameState/CGameState.cpp | 6 +- .../CommonConstructors.cpp | 8 +- .../CommonConstructors.h | 8 +- lib/mapObjects/CObjectHandler.cpp | 4 +- lib/mapObjects/MiscObjects.h | 6 +- lib/mapObjects/ObjectTemplate.cpp | 18 +- lib/mapObjects/ObjectTemplate.h | 5 +- lib/mapping/CMapInfo.cpp | 8 +- lib/mapping/CMapInfo.h | 4 +- lib/mapping/CMapService.cpp | 6 +- lib/mapping/CMapService.h | 12 +- lib/mapping/MapEditUtils.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 6 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/mapping/MapIdentifiersH3M.cpp | 6 +- lib/modding/CModHandler.cpp | 22 +- lib/modding/CModHandler.h | 4 +- lib/modding/CModInfo.cpp | 2 +- lib/rmg/CMapGenerator.cpp | 2 +- lib/rmg/modificators/RiverPlacer.cpp | 2 +- lib/spells/CSpellHandler.cpp | 10 +- lib/spells/CSpellHandler.h | 7 +- lib/spells/effects/Obstacle.cpp | 4 +- lib/spells/effects/Obstacle.h | 4 +- mapeditor/Animation.cpp | 12 +- mapeditor/BitmapHandler.cpp | 6 +- mapeditor/graphics.cpp | 20 +- mapeditor/mainwindow.cpp | 12 +- mapeditor/maphandler.cpp | 6 +- .../resourceExtractor/ResourceConverter.cpp | 2 +- scripting/lua/LuaScriptingContext.cpp | 2 +- server/CGameHandler.cpp | 4 +- test/game/CGameStateTest.cpp | 4 +- test/map/CMapEditManagerTest.cpp | 6 +- test/map/CMapFormatTest.cpp | 22 +- test/mock/mock_MapService.cpp | 12 +- test/mock/mock_MapService.h | 4 +- 201 files changed, 1390 insertions(+), 1362 deletions(-) delete mode 100644 lib/filesystem/ResourceID.h rename lib/filesystem/{ResourceID.cpp => ResourcePath.cpp} (59%) create mode 100644 lib/filesystem/ResourcePath.h diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 7b0b40086..202644db3 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -68,7 +68,7 @@ PriorityEvaluator::~PriorityEvaluator() void PriorityEvaluator::initVisitTile() { - auto file = CResourceHandler::get()->load(ResourceID("config/ai/object-priorities.txt"))->readAll(); + auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll(); std::string str = std::string((char *)file.first.get(), file.second); engine = fl::FllImporter().fromString(str); armyLossPersentageVariable = engine->getInputVariable("armyLoss"); diff --git a/client/CMT.cpp b/client/CMT.cpp index aebf3a916..4c0d97a76 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -251,7 +251,7 @@ int main(int argc, char * argv[]) // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) { - if (!CResourceHandler::get()->existsResource(ResourceID(filename))) + if (!CResourceHandler::get()->existsResource(ResourcePath(filename))) handleFatalError(message, false); }; diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 6c527c2e7..57a3c5aad 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -75,7 +75,7 @@ void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) CSoundHandler::CSoundHandler(): listener(settings.listen["general"]["sound"]), - ambientConfig(JsonNode(ResourceID("config/ambientSounds.json"))) + ambientConfig(JsonNode(ResourcePath("config/ambientSounds.json"))) { listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); @@ -126,7 +126,7 @@ Mix_Chunk *CSoundHandler::GetSoundChunk(std::string &sound, bool cache) if (cache && soundChunks.find(sound) != soundChunks.end()) return soundChunks[sound].first; - auto data = CResourceHandler::get()->load(ResourceID(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll(); + auto data = CResourceHandler::get()->load(ResourcePath(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll(); SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops @@ -357,7 +357,7 @@ CMusicHandler::CMusicHandler(): { listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); - auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourceID & id) -> bool + auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool { if(id.getType() != EResType::SOUND) return false; @@ -369,7 +369,7 @@ CMusicHandler::CMusicHandler(): return true; }); - for(const ResourceID & file : mp3files) + for(const ResourcePath & file : mp3files) { if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) addEntryToSet("battle", file.getName()); @@ -573,7 +573,7 @@ void MusicEntry::load(std::string musicURI) try { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourceID(std::move(musicURI), EResType::SOUND))); + auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourcePath(std::move(musicURI), EResType::SOUND))); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); } catch(std::exception &e) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2240cd638..8a3daf45b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1112,11 +1112,11 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v for (auto & component : components) intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); + std::vector > > pom; + pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0}); if (cancel) { - pom.push_back(std::pair >("ICANCEL.DEF",0)); + pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0}); } int charperline = 35; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ddf40406d..780ea9097 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -748,7 +748,7 @@ void CServerHandler::debugStartTest(std::string filename, bool save) if(save) { resetStateForLobby(StartInfo::LOAD_GAME); - mapInfo->saveInit(ResourceID(filename, EResType::SAVEGAME)); + mapInfo->saveInit(ResourcePath(filename, EResType::SAVEGAME)); screenType = ESelectionScreen::loadGame; } else diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index f6a88cb54..c685ff422 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -85,7 +85,7 @@ bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scal doLoop = loop; frameTime = 0; - ResourceID resource(std::string("Video/") + fname, EResType::VIDEO); + ResourcePath resource(std::string("Video/") + fname, EResType::VIDEO); if (!CResourceHandler::get()->existsResource(resource)) { diff --git a/client/Client.cpp b/client/Client.cpp index b1d89cfa4..c5087bdbc 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -222,7 +222,7 @@ void CClient::loadGame(CGameState * initializedGameState) // try to deserialize client data including sleepingHeroes try { - boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourceID(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); + boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); if(clientSaveName.empty()) throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 5a4c2f8cf..43b98723d 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -182,12 +182,12 @@ void ClientCommandManager::handleNotDialogCommand() void ClientCommandManager::handleConvertTextCommand() { logGlobal->info("Searching for available maps"); - std::unordered_set mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == EResType::MAP; }); - std::unordered_set campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == EResType::CAMPAIGN; }); @@ -292,7 +292,7 @@ void ClientCommandManager::handleGetTextCommand() VCMIDirs::get().userExtractedPath(); auto list = - CResourceHandler::get()->getFilteredFiles([](const ResourceID & ident) + CResourceHandler::get()->getFilteredFiles([](const ResourcePath & ident) { return ident.getType() == EResType::TEXT && boost::algorithm::starts_with(ident.getName(), "DATA/"); }); @@ -317,7 +317,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu { std::string URI; singleWordBuffer >> URI; - std::unique_ptr anim = std::make_unique(URI); + std::unique_ptr anim = std::make_unique(AnimationPath::builtin(URI)); anim->preload(); anim->exportBitmaps(VCMIDirs::get().userExtractedPath()); } @@ -327,11 +327,11 @@ void ClientCommandManager::handleExtractCommand(std::istringstream& singleWordBu std::string URI; singleWordBuffer >> URI; - if(CResourceHandler::get()->existsResource(ResourceID(URI))) + if(CResourceHandler::get()->existsResource(ResourcePath(URI))) { const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / URI; - auto data = CResourceHandler::get()->load(ResourceID(URI))->readAll(); + auto data = CResourceHandler::get()->load(ResourcePath(URI))->readAll(); boost::filesystem::create_directories(outPath.parent_path()); std::ofstream outFile(outPath.c_str(), std::ofstream::binary); diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 7ee69da42..1af8cdb8d 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -30,7 +30,7 @@ #include "../PlayerLocalState.h" #include "../../lib/constants/StringConstants.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) : shortcuts(shortcuts) @@ -56,11 +56,11 @@ AdventureMapWidget::AdventureMapWidget( std::shared_ptr s for (const auto & entry : shortcuts->getShortcuts()) addShortcut(entry.shortcut, entry.callback); - const JsonNode config(ResourceID("config/widgets/adventureMap.json")); + const JsonNode config(ResourcePath("config/widgets/adventureMap.json")); for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) { - ResourceID resourceName(entry.String(), EResType::IMAGE); + ResourcePath resourceName(entry.String(), EResType::IMAGE); playerColorerImages.push_back(resourceName.getName()); } @@ -127,22 +127,22 @@ Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & bounding return Rect(topLeft + boundingBox.topLeft(), dimensions); } -std::shared_ptr AdventureMapWidget::loadImage(const std::string & name) +std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) { - ResourceID resource(name, EResType::IMAGE); + ImagePath resource = ImagePath::fromJson(name); if(images.count(resource.getName()) == 0) - images[resource.getName()] = IImage::createFromFile(resource.getName()); + images[resource.getName()] = IImage::createFromFile(resource); return images[resource.getName()]; } -std::shared_ptr AdventureMapWidget::loadAnimation(const std::string & name) +std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & name) { - ResourceID resource(name, EResType::ANIMATION); + AnimationPath resource = AnimationPath::fromJson(name); if(animations.count(resource.getName()) == 0) - animations[resource.getName()] = std::make_shared(resource.getName()); + animations[resource.getName()] = std::make_shared(resource); return animations[resource.getName()]; } @@ -158,15 +158,14 @@ std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & i { Rect targetArea = readTargetArea(input["area"]); Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); - std::string image = input["image"].String(); - return std::make_shared(loadImage(image), targetArea, sourceArea); + return std::make_shared(loadImage(input["image"]), targetArea, sourceArea); } std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) { auto position = readTargetArea(input["area"]); - auto image = input["image"].String(); + auto image = AnimationPath::fromJson(input["image"]); auto help = readHintText(input["help"]); bool playerColored = input["playerColored"].Bool(); @@ -259,9 +258,8 @@ std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & in Rect area = readTargetArea(input["area"]); size_t index = input["index"].Integer(); size_t perPlayer = input["perPlayer"].Integer(); - std::string image = input["image"].String(); - return std::make_shared(area.topLeft(), loadAnimation(image), index, perPlayer); + return std::make_shared(area.topLeft(), loadAnimation(input["image"]), index, perPlayer); } std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) @@ -298,7 +296,7 @@ std::shared_ptr AdventureMapWidget::buildMinimap(const JsonNode & in std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonNode & input) { Rect area = readTargetArea(input["area"]); - std::string image = input["image"].String(); + auto image = ImagePath::fromJson(input["image"]); auto result = std::make_shared(image, area.topLeft()); @@ -320,7 +318,7 @@ std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonN std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & input) { Rect area = readTargetArea(input["area"]); - std::string image = input["image"].String(); + auto image = ImagePath::fromJson(input["image"]); auto background = std::make_shared(image, area); @@ -330,7 +328,7 @@ std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & std::shared_ptr AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input) { logGlobal->debug("Building widget CFilledTexture"); - auto image = input["image"].String(); + auto image = ImagePath::fromJson(input["image"]); Rect area = readTargetArea(input["area"]); return std::make_shared(image, area); } diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index bf02f7c46..185440397 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -48,8 +48,8 @@ class AdventureMapWidget : public InterfaceObjectConfigurable Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon); Rect readArea(const JsonNode & source, const Rect & boundingBox); - std::shared_ptr loadImage(const std::string & name); - std::shared_ptr loadAnimation(const std::string & name); + std::shared_ptr loadImage(const JsonNode & name); + std::shared_ptr loadAnimation(const JsonNode & name); std::shared_ptr buildInfobox(const JsonNode & input); std::shared_ptr buildMapImage(const JsonNode & input); diff --git a/client/adventureMap/AdventureOptions.cpp b/client/adventureMap/AdventureOptions.cpp index 5f9dc7b67..328baedeb 100644 --- a/client/adventureMap/AdventureOptions.cpp +++ b/client/adventureMap/AdventureOptions.cpp @@ -25,22 +25,22 @@ #include "../../lib/StartInfo.h" AdventureOptions::AdventureOptions() - : CWindowObject(PLAYER_COLORED, "ADVOPTS") + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - viewWorld = std::make_shared(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); + viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); - exit = std::make_shared(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); - scenInfo = std::make_shared(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); + scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); scenInfo->addCallback(AdventureOptions::showScenarioInfo); - puzzle = std::make_shared(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); + puzzle = std::make_shared(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); - dig = std::make_shared(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); + dig = std::make_shared(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero()) dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); else diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 7f5226d46..cbfd5e835 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -51,7 +51,7 @@ CInfoBar::EmptyVisibleInfo::EmptyVisibleInfo() CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATHR"); + background = std::make_shared(ImagePath::builtin("ADSTATHR")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) heroTooltip = std::make_shared(Point(0,0), hero); @@ -62,7 +62,7 @@ CInfoBar::VisibleHeroInfo::VisibleHeroInfo(const CGHeroInstance * hero) CInfoBar::VisibleTownInfo::VisibleTownInfo(const CGTownInstance * town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATCS"); + background = std::make_shared(ImagePath::builtin("ADSTATCS")); if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) townTooltip = std::make_shared(Point(0,0), town); @@ -88,36 +88,36 @@ CInfoBar::VisibleDateInfo::VisibleDateInfo() forceRefresh.push_back(label); } -std::string CInfoBar::VisibleDateInfo::getNewDayName() +AnimationPath CInfoBar::VisibleDateInfo::getNewDayName() { if(LOCPLINT->cb->getDate(Date::DAY) == 1) - return "NEWDAY"; + return AnimationPath::builtin("NEWDAY"); if(LOCPLINT->cb->getDate(Date::DAY_OF_WEEK) != 1) - return "NEWDAY"; + return AnimationPath("NEWDAY"); switch(LOCPLINT->cb->getDate(Date::WEEK)) { case 1: - return "NEWWEEK1"; + return AnimationPath("NEWWEEK1"); case 2: - return "NEWWEEK2"; + return AnimationPath("NEWWEEK2"); case 3: - return "NEWWEEK3"; + return AnimationPath("NEWWEEK3"); case 4: - return "NEWWEEK4"; + return AnimationPath("NEWWEEK4"); default: - return ""; + return AnimationPath(); } } CInfoBar::VisibleEnemyTurnInfo::VisibleEnemyTurnInfo(PlayerColor player) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("ADSTATNX"); - banner = std::make_shared("CREST58", player.getNum(), 0, 20, 51); - sand = std::make_shared(99, 51, "HOURSAND", 0, 100); // H3 uses around 100 ms per frame - glass = std::make_shared(99, 51, "HOURGLAS", CShowableAnim::PLAY_ONCE, 1000); // H3 scales this nicely for AI turn duration, don't have anything like that in vcmi + background = std::make_shared(ImagePath::builtin("ADSTATNX")); + banner = std::make_shared(AnimationPath::builtin("CREST58"), player.getNum(), 0, 20, 51); + sand = std::make_shared(99, 51, AnimationPath::builtin("HOURSAND"), 0, 100); // H3 uses around 100 ms per frame + glass = std::make_shared(99, 51, AnimationPath::builtin("HOURGLAS"), CShowableAnim::PLAY_ONCE, 1000); // H3 scales this nicely for AI turn duration, don't have anything like that in vcmi } CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() @@ -148,14 +148,14 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() } //generate widgets - background = std::make_shared("ADSTATIN"); + background = std::make_shared(ImagePath::builtin("ADSTATIN")); allyLabel = std::make_shared(10, 106, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); enemyLabel = std::make_shared(10, 136, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); int posx = allyLabel->pos.w + allyLabel->pos.x - pos.x + 4; for(PlayerColor & player : allies) { - auto image = std::make_shared("ITGFLAGS", player.getNum(), 0, posx, 102); + auto image = std::make_shared(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, posx, 102); posx += image->pos.w; flags.push_back(image); } @@ -163,14 +163,14 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo() posx = enemyLabel->pos.w + enemyLabel->pos.x - pos.x + 4; for(PlayerColor & player : enemies) { - auto image = std::make_shared("ITGFLAGS", player.getNum(), 0, posx, 132); + auto image = std::make_shared(AnimationPath::builtin("ITGFLAGS"), player.getNum(), 0, posx, 132); posx += image->pos.w; flags.push_back(image); } for(size_t i=0; i("itmtl", i, 0, 6 + 42 * (int)i , 11)); + hallIcons.push_back(std::make_shared(AnimationPath::builtin("itmtl"), i, 0, 6 + 42 * (int)i , 11)); if(halls[i]) hallLabels.push_back(std::make_shared( 26 + 42 * (int)i, 64, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(halls[i]))); } @@ -180,7 +180,7 @@ CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vector("ADSTATOT", 1, 0); + background = std::make_shared(ImagePath::builtin("ADSTATOT"), 1, 0); auto fullRect = Rect(CInfoBar::offset, CInfoBar::offset, data_width - 2 * CInfoBar::offset, data_height - 2 * CInfoBar::offset); auto textRect = fullRect; auto imageRect = fullRect; diff --git a/client/adventureMap/CInfoBar.h b/client/adventureMap/CInfoBar.h index c69fed1f0..8dc8c88cb 100644 --- a/client/adventureMap/CInfoBar.h +++ b/client/adventureMap/CInfoBar.h @@ -11,6 +11,7 @@ #include "../gui/CIntObject.h" #include "CConfigHandler.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -86,7 +87,7 @@ private: std::shared_ptr animation; std::shared_ptr label; - std::string getNewDayName(); + AnimationPath getNewDayName(); public: VisibleDateInfo(); }; diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 4758dac6b..cd84d4348 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -206,9 +206,9 @@ void CList::selectPrev() CHeroList::CEmptyHeroItem::CEmptyHeroItem() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - movement = std::make_shared("IMOBIL", 0, 0, 0, 1); - portrait = std::make_shared("HPSXXX", movement->pos.w + 1, 0); - mana = std::make_shared("IMANA", 0, 0, movement->pos.w + portrait->pos.w + 2, 1 ); + movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); + portrait = std::make_shared(ImagePath::builtin("HPSXXX"), movement->pos.w + 1, 0); + mana = std::make_shared(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1 ); pos.w = mana->pos.w + mana->pos.x - pos.x; pos.h = std::max(std::max(movement->pos.h + 1, mana->pos.h + 1), portrait->pos.h); @@ -219,9 +219,9 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) hero(Hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - movement = std::make_shared("IMOBIL", 0, 0, 0, 1); - portrait = std::make_shared("PortraitsSmall", hero->portrait, 0, movement->pos.w + 1); - mana = std::make_shared("IMANA", 0, 0, movement->pos.w + portrait->pos.w + 2, 1); + movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); + portrait = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->portrait, 0, movement->pos.w + 1); + mana = std::make_shared(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1); pos.w = mana->pos.w + mana->pos.x - pos.x; pos.h = std::max(std::max(movement->pos.h + 1, mana->pos.h + 1), portrait->pos.h); @@ -238,7 +238,7 @@ void CHeroList::CHeroItem::update() std::shared_ptr CHeroList::CHeroItem::genSelection() { - return std::make_shared("HPSYYY", movement->pos.w + 1, 0); + return std::make_shared(ImagePath::builtin("HPSYYY"), movement->pos.w + 1, 0); } void CHeroList::CHeroItem::select(bool on) @@ -319,7 +319,7 @@ std::shared_ptr CTownList::createItem(size_t index) { if (LOCPLINT->localState->getOwnedTowns().size() > index) return std::make_shared(this, LOCPLINT->localState->getOwnedTown(index)); - return std::make_shared("ITPA", 0); + return std::make_shared(AnimationPath::builtin("ITPA"), 0); } CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): @@ -327,14 +327,14 @@ CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): town(Town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - picture = std::make_shared("ITPA", 0); + picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; update(); } std::shared_ptr CTownList::CTownItem::genSelection() { - return std::make_shared("ITPA", 1); + return std::make_shared(AnimationPath::builtin("ITPA"), 1); } void CTownList::CTownItem::update() diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 7165b9e6e..358cd6de4 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -94,7 +94,7 @@ CMinimap::CMinimap(const Rect & position) pos.w = position.w; pos.h = position.h; - aiShield = std::make_shared("AIShield"); + aiShield = std::make_shared(ImagePath::builtin("AIShield")); aiShield->disable(); } diff --git a/client/adventureMap/CResDataBar.cpp b/client/adventureMap/CResDataBar.cpp index afea62a38..ea7914ecf 100644 --- a/client/adventureMap/CResDataBar.cpp +++ b/client/adventureMap/CResDataBar.cpp @@ -24,7 +24,7 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/ResourceSet.h" -CResDataBar::CResDataBar(const std::string & imageName, const Point & position) +CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) { pos.x += position.x; pos.y += position.y; @@ -37,7 +37,7 @@ CResDataBar::CResDataBar(const std::string & imageName, const Point & position) pos.h = background->pos.h; } -CResDataBar::CResDataBar(const std::string & defname, int x, int y, int offx, int offy, int resdist, int datedist): +CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist): CResDataBar(defname, Point(x,y)) { for (int i = 0; i < 7 ; i++) diff --git a/client/adventureMap/CResDataBar.h b/client/adventureMap/CResDataBar.h index c9bc01286..b37abeacf 100644 --- a/client/adventureMap/CResDataBar.h +++ b/client/adventureMap/CResDataBar.h @@ -10,6 +10,7 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/filesystem/ResourcePath.h" /// Resources bar which shows information about how many gold, crystals,... you have /// Current date is displayed too @@ -25,10 +26,10 @@ class CResDataBar : public CIntObject public: /// For dynamically-sized UI windows, e.g. adventure map interface - CResDataBar(const std::string & imageName, const Point & position); + CResDataBar(const ImagePath & imageName, const Point & position); /// For fixed-size UI windows, e.g. CastleInterface - CResDataBar(const std::string &defname, int x, int y, int offx, int offy, int resdist, int datedist); + CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist); void setDatePosition(const Point & position); void setResourcePosition(const GameResID & resource, const Point & position); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index b47d111da..2550ef23d 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -25,7 +25,7 @@ #include "../../CCallback.h" #include "../../lib/CStack.h" #include "../../lib/CPlayerState.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c): CIntObject(), rect(r), color(c) @@ -47,7 +47,7 @@ TurnTimerWidget::TurnTimerWidget(): recActions &= ~DEACTIVATE; - const JsonNode config(ResourceID("config/widgets/turnTimer.json")); + const JsonNode config(ResourcePath("config/widgets/turnTimer.json")); build(config); diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 3a89b0661..355b8d679 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -807,7 +807,7 @@ void CatapultAnimation::tick(uint32_t msPassed) Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); std::string soundFilename = (catapultDamage > 0) ? "WALLHIT" : "WALLMISS"; - std::string effectFilename = (catapultDamage > 0) ? "SGEXPL" : "CSGRCK"; + AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK"); CCS->soundh->playSound( soundFilename ); owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); @@ -879,42 +879,42 @@ uint32_t CastAnimation::getAttackClimaxFrame() const return maxFrames / 2; } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): BattleAnimation(owner), animation(std::make_shared(animationName)), effectFlags(effects), effectFinished(false), reversed(reversed) { - logAnim->debug("CPointEffectAnimation::init: effect %s", animationName); + logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName()); } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects, bool reversed): EffectAnimation(owner, animationName, effects, reversed) { battlehexes = hex; } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed): EffectAnimation(owner, animationName, effects, reversed) { assert(hex.isValid()); battlehexes.push_back(hex); } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos, int effects, bool reversed): EffectAnimation(owner, animationName, effects, reversed) { positions = pos; } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed): EffectAnimation(owner, animationName, effects, reversed) { positions.push_back(pos); } -EffectAnimation::EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects, bool reversed): +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed): EffectAnimation(owner, animationName, effects, reversed) { assert(hex.isValid()); diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index f47f8ce9c..93ff9c3df 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" #include "BattleConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -334,17 +335,17 @@ public: }; /// Create animation with screen-wide effect - EffectAnimation(BattleInterface & owner, std::string animationName, int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false); /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset - EffectAnimation(BattleInterface & owner, std::string animationName, Point pos , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, std::string animationName, std::vector pos , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos , int effects = 0, bool reversed = false); /// Create animation positioned at certain hex(es) - EffectAnimation(BattleInterface & owner, std::string animationName, BattleHex hex , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, std::string animationName, std::vector hex, int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, std::string animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); ~EffectAnimation(); bool init() override; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index bdaba92af..eb3d2e419 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -27,7 +27,7 @@ #include "../../CCallback.h" #include "../../lib/battle/BattleAction.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" #include "../../lib/NetPacks.h" #include "../../lib/CStack.h" #include "../../lib/IGameEventsReceiver.h" @@ -48,7 +48,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, std::string so { size_t effectID = static_cast(effect); - std::string customAnim = graphics->battleACToDef[effectID][0]; + AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]); CCS->soundh->playSound( soundFile ); @@ -132,7 +132,7 @@ void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer void BattleEffectsController::loadColorMuxers() { - const JsonNode config(ResourceID("config/battleEffects.json")); + const JsonNode config(ResourcePath("config/battleEffects.json")); for(auto & muxer : config["colorMuxers"].Struct()) { diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 8eda33038..a53e5d8c2 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -120,20 +120,20 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; //preparing cells and hexes - cellBorder = IImage::createFromFile("CCELLGRD.BMP", EImageBlitMode::COLORKEY); - cellShade = IImage::createFromFile("CCELLSHD.BMP"); - cellUnitMovementHighlight = IImage::createFromFile("UnitMovementHighlight.PNG", EImageBlitMode::COLORKEY); - cellUnitMaxMovementHighlight = IImage::createFromFile("UnitMaxMovementHighlight.PNG", EImageBlitMode::COLORKEY); + cellBorder = IImage::createFromFile(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); + cellShade = IImage::createFromFile(ImagePath::builtin("CCELLSHD.BMP")); + cellUnitMovementHighlight = IImage::createFromFile(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); + cellUnitMaxMovementHighlight = IImage::createFromFile(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - attackCursors = std::make_shared("CRCOMBAT"); + attackCursors = std::make_shared(AnimationPath::builtin("CRCOMBAT")); attackCursors->preload(); initializeHexEdgeMaskToFrameIndex(); - rangedFullDamageLimitImages = std::make_shared("battle/rangeHighlights/rangeHighlightsGreen.json"); + rangedFullDamageLimitImages = std::make_shared(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); rangedFullDamageLimitImages->preload(); - shootingRangeLimitImages = std::make_shared("battle/rangeHighlights/rangeHighlightsRed.json"); + shootingRangeLimitImages = std::make_shared(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); shootingRangeLimitImages->preload(); flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); @@ -150,7 +150,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): } else { - std::string backgroundName = owner.siegeController->getBattleBackgroundName(); + auto backgroundName = owner.siegeController->getBattleBackgroundName(); background = IImage::createFromFile(backgroundName, EImageBlitMode::OPAQUE); } diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index deb640985..8db671a94 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -440,8 +440,8 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) bool side = sc->side; addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_A.DEF" : "SP07_B.DEF", leftHero)); - stacksController->addNewAnim(new EffectAnimation(*this, side ? "SP07_B.DEF" : "SP07_A.DEF", rightHero)); + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); }); } diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 94ea31b38..4557d6cff 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -342,7 +342,7 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her currentFrame(0.f), flagCurrentFrame(0.f) { - std::string animationPath; + AnimationPath animationPath; if(!hero->type->battleImage.empty()) animationPath = hero->type->battleImage; @@ -364,9 +364,9 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her animation->verticalFlip(); if(defender) - flagAnimation = std::make_shared("CMFLAGR"); + flagAnimation = std::make_shared(AnimationPath::builtin("CMFLAGR")); else - flagAnimation = std::make_shared("CMFLAGL"); + flagAnimation = std::make_shared(AnimationPath::builtin("CMFLAGL")); flagAnimation->preload(); flagAnimation->playerColored(hero->tempOwner); @@ -386,7 +386,7 @@ HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * posit if(initializeBackground) { - background = std::make_shared("CHRPOP"); + background = std::make_shared(ImagePath::builtin("CHRPOP")); background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); background->colorize(hero.owner); } @@ -406,7 +406,7 @@ void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) auto currentSpellPoints = hero.details->mana; auto maxSpellPoints = hero.details->manaLimit; - icons.push_back(std::make_shared("PortraitsLarge", hero.portrait, 0, 10, 6)); + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 10, 6)); //primary stats labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); @@ -423,8 +423,8 @@ void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); - icons.push_back(std::make_shared("IMRL22", morale + 3, 0, 47, 131)); - icons.push_back(std::make_shared("ILCK22", luck + 3, 0, 47, 143)); + icons.push_back(std::make_shared(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131)); + icons.push_back(std::make_shared(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143)); //spell points labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); @@ -446,7 +446,7 @@ void HeroInfoBasicPanel::show(Canvas & to) } HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) - : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, "CHRPOP") + : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if (position != nullptr) @@ -462,16 +462,16 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("CPRESULT"); + background = std::make_shared(ImagePath::builtin("CPRESULT")); background->colorize(owner.playerID); pos = center(background->pos); - exit = std::make_shared(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); + exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); exit->setBorderColor(Colors::METALLIC_GOLD); if(allowReplay) { - repeat = std::make_shared(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); + repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); repeat->setBorderColor(Colors::METALLIC_GOLD); labels.push_back(std::make_shared(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel"))); } @@ -507,7 +507,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if(heroInfo.portrait >= 0) //attacking hero { - icons.push_back(std::make_shared("PortraitsLarge", heroInfo.portrait, 0, xs[i], 38)); + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.portrait, 0, xs[i], 38)); sideNames[i] = heroInfo.name; } else @@ -525,7 +525,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if(best != stacks.end()) //should be always but to be safe... { - icons.push_back(std::make_shared("TWCRPORT", (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); + icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); sideNames[i] = (*best)->unitType()->getNamePluralTranslated(); } } @@ -552,7 +552,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if (creature->getId() == CreatureID::ARROW_TOWERS ) continue; // do not show destroyed towers in battle results - icons.push_back(std::make_shared("CPRSMALL", creature->getIconIndex(), 0, xPos, yPos)); + icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos)); std::ostringstream amount; amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); @@ -676,8 +676,8 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) pos.x += parent->pos.w/2 - pos.w/2; pos.y += 10; - icons = std::make_shared("CPRSMALL"); - stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); + icons = std::make_shared(AnimationPath::builtin("CPRSMALL")); + stateIcons = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); } else { @@ -686,10 +686,10 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) pos.x += 0; pos.y -= pos.h; - background = std::make_shared("DIBOXBCK", Rect(0, 0, pos.w, pos.h)); + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); - icons = std::make_shared("TWCRPORT"); - stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESSMALL"); + icons = std::make_shared(AnimationPath::builtin("TWCRPORT")); + stateIcons = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); //TODO: where use big icons? //stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESBIG"); } @@ -750,7 +750,7 @@ StackQueue::StackBox::StackBox(StackQueue * owner): CIntObject(SHOW_POPUP | HOVER), owner(owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"); + background = std::make_shared(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge")); pos.w = background->pos.w; pos.h = background->pos.h; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index d41b09a66..a2acf75ee 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -42,7 +42,7 @@ BattleObstacleController::BattleObstacleController(BattleInterface & owner): void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) { - std::string animationName = oi.getAnimation(); + AnimationPath animationName = oi.getAnimation(); if (animationsCache.count(animationName) == 0) { @@ -50,7 +50,7 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) { // obstacle uses single bitmap image for animations auto animation = std::make_shared(); - animation->setCustom(animationName, 0, 0); + animation->setCustom(animationName.getName(), 0, 0); animationsCache[animationName] = animation; animation->preload(); } @@ -76,7 +76,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector(obstacle["appearAnimation"].String()); + auto animation = std::make_shared(AnimationPath::fromJson(obstacle["appearAnimation"])); animation->preload(); auto first = animation->getImage(0, 0); @@ -87,7 +87,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector if we know how to blit obstacle, let's blit the effect in the same place Point whereTo = getObstaclePosition(first, obstacle); //AFAIK, in H3 there is no sound of obstacle removal - owner.stacksController->addNewAnim(new EffectAnimation(owner, obstacle["appearAnimation"].String(), whereTo, obstacle["position"].Integer(), 0, true)); + owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true)); obstacleAnimations.erase(oi.id); //so when multiple obstacles are removed, they show up one after another diff --git a/client/battle/BattleObstacleController.h b/client/battle/BattleObstacleController.h index 0f33626f9..b05ded9a6 100644 --- a/client/battle/BattleObstacleController.h +++ b/client/battle/BattleObstacleController.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../../lib/filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN struct BattleHex; @@ -35,7 +37,7 @@ class BattleObstacleController float timePassed; /// cached animations of all obstacles in current battle - std::map> animationsCache; + std::map> animationsCache; /// list of all obstacles that are currently being rendered std::map> obstacleAnimations; diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index c2b2715e0..71fac3f52 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -188,7 +188,7 @@ void BattleProjectileController::initStackProjectile(const CStack * stack) projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName); } -std::shared_ptr BattleProjectileController::createProjectileImage(const std::string & path ) +std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) { std::shared_ptr projectile = std::make_shared(path); projectile->preload(); @@ -204,7 +204,7 @@ std::shared_ptr BattleProjectileController::createProjectileImage(co std::shared_ptr BattleProjectileController::getProjectileImage(const CStack * stack) { const CCreature & creature = getShooter(stack); - std::string imageName = creature.animation.projectileImageName; + AnimationPath imageName = creature.animation.projectileImageName; if (!projectilesCache.count(imageName)) initStackProjectile(stack); @@ -361,7 +361,7 @@ void BattleProjectileController::createProjectile(const CStack * shooter, Point void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell) { double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y)); - std::string animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); + AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); assert(!animToDisplay.empty()); diff --git a/client/battle/BattleProjectileController.h b/client/battle/BattleProjectileController.h index a9622c230..b54a5bca1 100644 --- a/client/battle/BattleProjectileController.h +++ b/client/battle/BattleProjectileController.h @@ -11,6 +11,7 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -83,13 +84,13 @@ class BattleProjectileController BattleInterface & owner; /// all projectiles loaded during current battle - std::map> projectilesCache; + std::map> projectilesCache; /// projectiles currently flying on battlefield std::vector> projectiles; std::shared_ptr getProjectileImage(const CStack * stack); - std::shared_ptr createProjectileImage(const std::string & path ); + std::shared_ptr createProjectileImage(const AnimationPath & path ); void initStackProjectile(const CStack * stack); bool stackUsesRayProjectile(const CStack * stack) const; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 63c8cab10..3b88b75f4 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -28,7 +28,7 @@ #include "../../lib/CStack.h" #include "../../lib/mapObjects/CGTownInstance.h" -std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const +ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const { auto getImageIndex = [&]() -> int { @@ -68,44 +68,44 @@ std::string BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisua auto faction = town->town->faction->getIndex(); if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) - return prefix + "TPW1.BMP"; + return ImagePath::builtinTODO(prefix + "TPW1.BMP"); else - return prefix + "TPWL.BMP"; + return ImagePath::builtinTODO(prefix + "TPWL.BMP"); } case EWallVisual::KEEP: - return prefix + "MAN" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP"); case EWallVisual::BOTTOM_TOWER: - return prefix + "TW1" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP"); case EWallVisual::BOTTOM_WALL: - return prefix + "WA1" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP"); case EWallVisual::WALL_BELLOW_GATE: - return prefix + "WA3" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP"); case EWallVisual::WALL_OVER_GATE: - return prefix + "WA4" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP"); case EWallVisual::UPPER_WALL: - return prefix + "WA6" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP"); case EWallVisual::UPPER_TOWER: - return prefix + "TW2" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP"); case EWallVisual::GATE: - return prefix + "DRW" + addit + ".BMP"; + return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP"); case EWallVisual::GATE_ARCH: - return prefix + "ARCH.BMP"; + return ImagePath::builtinTODO(prefix + "ARCH.BMP"); case EWallVisual::BOTTOM_STATIC_WALL: - return prefix + "WA2.BMP"; + return ImagePath::builtinTODO(prefix + "WA2.BMP"); case EWallVisual::UPPER_STATIC_WALL: - return prefix + "WA5.BMP"; + return ImagePath::builtinTODO(prefix + "WA5.BMP"); case EWallVisual::MOAT: - return prefix + "MOAT.BMP"; + return ImagePath::builtinTODO(prefix + "MOAT.BMP"); case EWallVisual::MOAT_BANK: - return prefix + "MLIP.BMP"; + return ImagePath::builtinTODO(prefix + "MLIP.BMP"); case EWallVisual::KEEP_BATTLEMENT: - return prefix + "MANC.BMP"; + return ImagePath::builtinTODO(prefix + "MANC.BMP"); case EWallVisual::BOTTOM_BATTLEMENT: - return prefix + "TW1C.BMP"; + return ImagePath::builtinTODO(prefix + "TW1C.BMP"); case EWallVisual::UPPER_BATTLEMENT: - return prefix + "TW2C.BMP"; + return ImagePath::builtinTODO(prefix + "TW2C.BMP"); default: - return ""; + return ImagePath(); } } @@ -118,10 +118,10 @@ void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVis canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); } -std::string BattleSiegeController::getBattleBackgroundName() const +ImagePath BattleSiegeController::getBattleBackgroundName() const { const std::string & prefix = town->town->clientInfo.siegePrefix; - return prefix + "BACK.BMP"; + return ImagePath::builtinTODO(prefix + "BACK.BMP"); } bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const @@ -341,7 +341,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); CCS->soundh->playSound( "WALLHIT" ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, "SGEXPL.DEF", positions)); + owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions)); } owner.waitForAnimations(); diff --git a/client/battle/BattleSiegeController.h b/client/battle/BattleSiegeController.h index 262b78b76..7acb38f13 100644 --- a/client/battle/BattleSiegeController.h +++ b/client/battle/BattleSiegeController.h @@ -11,6 +11,7 @@ #include "../../lib/GameConstants.h" #include "../../lib/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -76,7 +77,7 @@ class BattleSiegeController std::array, EWallVisual::WALL_LAST + 1> wallPieceImages; /// return URI for image for a wall piece - std::string getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; + ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; /// returns BattleHex to which chosen wall piece is bound BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; @@ -102,7 +103,7 @@ public: /// queries from other battle controllers bool isAttackableByCatapult(BattleHex hex) const; - std::string getBattleBackgroundName() const; + ImagePath getBattleBackgroundName() const; const CCreature *getTurretCreature() const; Point getTurretCreaturePosition( BattleHex position ) const; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index f83e97782..32c401612 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -76,10 +76,10 @@ BattleStacksController::BattleStacksController(BattleInterface & owner): animIDhelper(0) { //preparing graphics for displaying amounts of creatures - amountNormal = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountPositive = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountNegative = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); - amountEffNeutral = IImage::createFromFile("CMNUMWIN.BMP", EImageBlitMode::COLORKEY); + amountNormal = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountPositive = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountNegative = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountEffNeutral = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 79f558932..4e62ea056 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -37,7 +37,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CStack.h" #include "../../lib/CConfigHandler.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" #include "../windows/settings/SettingsMainWindow.h" BattleWindow::BattleWindow(BattleInterface & owner): @@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - const JsonNode config(ResourceID("config/widgets/BattleWindow2.json")); + const JsonNode config(ResourcePath("config/widgets/BattleWindow2.json")); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); @@ -436,23 +436,23 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) if(!w) return; - std::string iconName = variables["actionIconDefault"].String(); + AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]); switch(action.get()) { case PossiblePlayerBattleAction::ATTACK: - iconName = variables["actionIconAttack"].String(); + iconName = AnimationPath::fromJson(variables["actionIconAttack"]); break; case PossiblePlayerBattleAction::SHOOT: - iconName = variables["actionIconShoot"].String(); + iconName = AnimationPath::fromJson(variables["actionIconShoot"]); break; case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - iconName = variables["actionIconSpell"].String(); + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); break; case PossiblePlayerBattleAction::ANY_LOCATION: - iconName = variables["actionIconSpell"].String(); + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); break; //TODO: figure out purpose of this icon @@ -461,11 +461,11 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) //break; case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - iconName = variables["actionIconReturn"].String(); + iconName = AnimationPath::fromJson(variables["actionIconReturn"]); break; case PossiblePlayerBattleAction::WALK_AND_ATTACK: - iconName = variables["actionIconNoReturn"].String(); + iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]); break; } diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index 64397ec04..ee0106f0b 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -185,7 +185,7 @@ void CreatureAnimation::setType(ECreatureAnimType type) speed = speedController(this, type); } -CreatureAnimation::CreatureAnimation(const std::string & name_, TSpeedController controller) +CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller) : name(name_), speed(0.1f), shadowAlpha(128), diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index d98c12085..66ad3f285 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -70,7 +70,7 @@ public: using TSpeedController = std::function; private: - std::string name; + AnimationPath name; /// animation for rendering stack in default orientation - facing right std::shared_ptr forward; @@ -122,7 +122,7 @@ public: /// name - path to .def file, relative to SPRITES/ directory /// controller - function that will return for how long *each* frame /// in specified group of animation should be played, measured in seconds - CreatureAnimation(const std::string & name_, TSpeedController speedController); + CreatureAnimation(const AnimationPath & name_, TSpeedController speedController); /// sets type of animation and resets framecount void setType(ECreatureAnimType type); diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index f51ab4310..dbdf7853e 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -46,10 +46,10 @@ CursorHandler::CursorHandler() cursors = { - std::make_unique("CRADVNTR"), - std::make_unique("CRCOMBAT"), - std::make_unique("CRDEFLT"), - std::make_unique("CRSPELL") + std::make_unique(AnimationPath::builtin("CRADVNTR")), + std::make_unique(AnimationPath::builtin("CRCOMBAT")), + std::make_unique(AnimationPath::builtin("CRDEFLT")), + std::make_unique(AnimationPath::builtin("CRSPELL")) }; for (auto & cursor : cursors) @@ -100,7 +100,7 @@ void CursorHandler::dragAndDropCursor(std::shared_ptr image) cursor->setImage(getCurrentImage(), getPivotOffset()); } -void CursorHandler::dragAndDropCursor (std::string path, size_t index) +void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) { CAnimation anim(path); anim.load(index); diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 531594b3a..b17686b93 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" class ICursor; class IImage; @@ -143,7 +144,7 @@ public: /// @param image Image to replace cursor with or nullptr to use the normal cursor. void dragAndDropCursor(std::shared_ptr image); - void dragAndDropCursor(std::string path, size_t index); + void dragAndDropCursor(const AnimationPath & path, size_t index); /// Changes cursor to specified index void set(Cursor::Default index); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index dc97aba1a..e3d9a8a34 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -31,7 +31,7 @@ #include "../../lib//constants/StringConstants.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" InterfaceObjectConfigurable::InterfaceObjectConfigurable(const JsonNode & config, int used, Point offset): InterfaceObjectConfigurable(used, offset) @@ -110,7 +110,7 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) { if (!config["library"].isNull()) { - const JsonNode library(ResourceID(config["library"].String())); + const JsonNode library(ResourcePath(config["library"].String())); loadCustomBuilders(library); } @@ -305,7 +305,7 @@ EShortcut InterfaceObjectConfigurable::readHotkey(const JsonNode & config) const std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNode & config) const { logGlobal->debug("Building widget CPicture"); - auto image = config["image"].String(); + auto image = ImagePath::fromJson(config["image"]); auto position = readPosition(config["position"]); auto pic = std::make_shared(image, position.x, position.y); if(!config["visible"].isNull()) @@ -369,7 +369,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildToggleButton(co { logGlobal->debug("Building widget CToggleButton"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); auto button = std::make_shared(position, image, help); if(!config["items"].isNull()) @@ -395,7 +395,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildButton(const JsonNode { logGlobal->debug("Building widget CButton"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); auto button = std::make_shared(position, image, help); if(!config["items"].isNull()) @@ -522,7 +522,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildImage(const JsonNo { logGlobal->debug("Building widget CAnimImage"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); int group = config["group"].isNull() ? 0 : config["group"].Integer(); int frame = config["frame"].isNull() ? 0 : config["frame"].Integer(); return std::make_shared(image, frame, group, position.x, position.y); @@ -531,7 +531,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildImage(const JsonNo std::shared_ptr InterfaceObjectConfigurable::buildTexture(const JsonNode & config) const { logGlobal->debug("Building widget CFilledTexture"); - auto image = config["image"].String(); + auto image = ImagePath::fromJson(config["image"]); auto rect = readRect(config["rect"]); auto playerColor = readPlayerColor(config["color"]); if(playerColor.isValidPlayer()) @@ -546,7 +546,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonN { logGlobal->debug("Building widget ComboBox"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); auto result = std::make_shared(position, image, help, config["dropDown"]); if(!config["items"].isNull()) @@ -573,7 +573,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildTextInput(const Js logGlobal->debug("Building widget CTextInput"); auto rect = readRect(config["rect"]); auto offset = readPosition(config["backgroundOffset"]); - auto bgName = config["background"].String(); + auto bgName = ImagePath::fromJson(config["background"]); auto result = std::make_shared(rect, offset, bgName, 0); if(!config["alignment"].isNull()) result->alignment = readTextAlignment(config["alignment"]); @@ -664,7 +664,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const { logGlobal->debug("Building widget CShowableAnim"); auto position = readPosition(config["position"]); - auto image = config["image"].String(); + auto image = AnimationPath::fromJson(config["image"]); ui8 flags = 0; if(!config["repeat"].Bool()) flags |= CShowableAnim::EFlags::PLAY_ONCE; diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 2bd79577b..4fa6a65b2 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -65,18 +65,17 @@ CBonusSelection::CBonusSelection() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - std::string bgName = getCampaign()->getRegions().getBackgroundName(); - setBackground(bgName); + setBackground(getCampaign()->getRegions().getBackgroundName()); - panelBackground = std::make_shared("CAMPBRF.BMP", 456, 6); + panelBackground = std::make_shared(ImagePath::builtin("CAMPBRF.BMP"), 456, 6); - buttonStart = std::make_shared(Point(475, 536), "CBBEGIB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT); - buttonRestart = std::make_shared(Point(475, 536), "CBRESTB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT); - buttonBack = std::make_shared(Point(624, 536), "CBCANCB.DEF", CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL); + buttonStart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT); + buttonRestart = std::make_shared(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT); + buttonBack = std::make_shared(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL); campaignName = std::make_shared(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName()); - iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 735, 26); + iconsMapSizes = std::make_shared(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 735, 26); labelCampaignDescription = std::make_shared(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); campaignDescription = std::make_shared(getCampaign()->getDescription(), Rect(480, 86, 286, 117), 1); @@ -97,13 +96,13 @@ CBonusSelection::CBonusSelection() for(size_t b = 0; b < difficultyIcons.size(); ++b) { - difficultyIcons[b] = std::make_shared("GSPBUT" + std::to_string(b + 3) + ".DEF", 0, 0, 709, 455); + difficultyIcons[b] = std::make_shared(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, 455); } if(getCampaign()->playerSelectedDifficulty()) { - buttonDifficultyLeft = std::make_shared(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); - buttonDifficultyRight = std::make_shared(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); + buttonDifficultyLeft = std::make_shared(Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this)); + buttonDifficultyRight = std::make_shared(Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this)); } for(auto scenarioID : getCampaign()->allScenarios()) @@ -302,7 +301,7 @@ void CBonusSelection::createBonusesIcons() break; } - std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), "", CButton::tooltip(desc, desc)); + std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc, desc)); if(picNumber != -1) picName += ":" + std::to_string(picNumber); diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index b523d50c2..25d845b70 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -43,17 +43,17 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) { tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); - buttonSelect = std::make_shared(Point(411, 80), "GSPBUTT.DEF", CGI->generaltexth->zelp[45], 0, EShortcut::LOBBY_SELECT_SCENARIO); + buttonSelect = std::make_shared(Point(411, 80), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[45], 0, EShortcut::LOBBY_SELECT_SCENARIO); buttonSelect->addCallback([&]() { toggleTab(tabSel); CSH->setMapInfo(tabSel->getSelectedMapInfo()); }); - buttonOptions = std::make_shared(Point(411, 510), "GSPBUTT.DEF", CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); + buttonOptions = std::make_shared(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); }; - buttonChat = std::make_shared(Point(619, 80), "GSPBUT2.DEF", CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); + buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); buttonChat->addTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE); switch(screenType) @@ -63,7 +63,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) tabOpt = std::make_shared(); tabRand = std::make_shared(); tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2); - buttonRMG = std::make_shared(Point(411, 105), "GSPBUTT.DEF", CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); + buttonRMG = std::make_shared(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); buttonRMG->addCallback([&]() { toggleTab(tabRand); @@ -72,24 +72,24 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1)); - buttonStart = std::make_shared(Point(411, 535), "SCNRBEG.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME); initLobby(); break; } case ESelectionScreen::loadGame: { tabOpt = std::make_shared(); - buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; } case ESelectionScreen::campaignList: tabSel->callOnSelect = std::bind(&IServerAPI::setMapInfo, CSH, _1, nullptr); - buttonStart = std::make_shared(Point(411, 535), "SCNRLOD.DEF", CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CButton::tooltip(), std::bind(&CLobbyScreen::startCampaign, this), EShortcut::LOBBY_BEGIN_GAME); break; } - buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [&]() + buttonBack = std::make_shared(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [&]() { CSH->sendClientDisconnecting(); close(); diff --git a/client/lobby/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp index 1126fb444..5a43f2fc3 100644 --- a/client/lobby/CSavingScreen.cpp +++ b/client/lobby/CSavingScreen.cpp @@ -40,7 +40,7 @@ CSavingScreen::CSavingScreen() tabSel->toggleMode(); curTab = tabSel; - buttonStart = std::make_shared(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRSAV.DEF"), CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME); } const CMapInfo * CSavingScreen::getMapInfo() @@ -80,7 +80,7 @@ void CSavingScreen::saveGame() close(); }; - if(CResourceHandler::get("local")->existsResource(ResourceID(path, EResType::SAVEGAME))) + if(CResourceHandler::get("local")->existsResource(ResourcePath(path, EResType::SAVEGAME))) { std::string hlp = CGI->generaltexth->allTexts[493]; //%s exists. Overwrite? boost::algorithm::replace_first(hlp, "%s", tabSel->inputName->getText()); diff --git a/client/lobby/CScenarioInfoScreen.cpp b/client/lobby/CScenarioInfoScreen.cpp index acccf2da4..51a6f242a 100644 --- a/client/lobby/CScenarioInfoScreen.cpp +++ b/client/lobby/CScenarioInfoScreen.cpp @@ -45,7 +45,7 @@ CScenarioInfoScreen::CScenarioInfoScreen() card->changeSelection(); card->iconDifficulty->setSelected(getCurrentDifficulty()); - buttonBack = std::make_shared(Point(584, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonBack = std::make_shared(Point(584, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } CScenarioInfoScreen::~CScenarioInfoScreen() diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 5701a17bc..4c3fbbf4a 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -80,16 +80,16 @@ CSelectionBase::CSelectionBase(ESelectionScreen type) pos.h = 584; if(screenType == ESelectionScreen::campaignList) { - setBackground("CamCust.bmp"); + setBackground(ImagePath::builtin("CamCust.bmp")); } else { const JsonVector & bgNames = CMainMenuConfig::get().getConfig()["game-select"].Vector(); - setBackground(RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault())->String()); + setBackground(ImagePath::fromJson(*RandomGeneratorUtil::nextItem(bgNames, CRandomGenerator::getDefault()))); } pos = background->center(); card = std::make_shared(); - buttonBack = std::make_shared(Point(581, 535), "SCNRBACK.DEF", CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonBack = std::make_shared(Point(581, 535), AnimationPath::builtin("SCNRBACK.DEF"), CGI->generaltexth->zelp[105], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } void CSelectionBase::toggleTab(std::shared_ptr tab) @@ -125,7 +125,7 @@ InfoCard::InfoCard() mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); - playerListBg = std::make_shared("CHATPLUG.bmp", 16, 276); + playerListBg = std::make_shared(ImagePath::builtin("CHATPLUG.bmp"), 16, 276); chat = std::make_shared(Rect(26, 132, 340, 132)); if(SEL->screenType == ESelectionScreen::campaignList) @@ -134,14 +134,14 @@ InfoCard::InfoCard() } else { - background = std::make_shared("GSELPOP1.bmp", 0, 0); + background = std::make_shared(ImagePath::builtin("GSELPOP1.bmp"), 0, 0); parent->addChild(background.get()); auto it = vstd::find(parent->children, this); //our position among parent children parent->children.insert(it, background.get()); //put BG before us parent->children.pop_back(); pos.w = background->pos.w; pos.h = background->pos.h; - iconsMapSizes = std::make_shared("SCNRMPSZ", 4, 0, 318, 22); //let it be custom size (frame 4) by default + iconsMapSizes = std::make_shared(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 318, 22); //let it be custom size (frame 4) by default iconDifficulty = std::make_shared(0); { @@ -149,7 +149,7 @@ InfoCard::InfoCard() for(int i = 0; i < 5; i++) { - auto button = std::make_shared(Point(110 + i * 32, 450), difButns[i], CGI->generaltexth->zelp[24 + i]); + auto button = std::make_shared(Point(110 + i * 32, 450), AnimationPath::builtin(difButns[i]), CGI->generaltexth->zelp[24 + i]); iconDifficulty->addToggle(i, button); if(SEL->screenType != ESelectionScreen::newGame) @@ -165,8 +165,8 @@ InfoCard::InfoCard() labelScenarioDescription = std::make_shared(26, 132, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); labelVictoryCondition = std::make_shared(26, 283, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[497]); labelLossCondition = std::make_shared(26, 339, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[498]); - iconsVictoryCondition = std::make_shared("SCNRVICT", 0, 0, 24, 302); - iconsLossCondition = std::make_shared("SCNRLOSS", 0, 0, 24, 359); + iconsVictoryCondition = std::make_shared(AnimationPath::builtin("SCNRVICT"), 0, 0, 24, 302); + iconsLossCondition = std::make_shared(AnimationPath::builtin("SCNRLOSS"), 0, 0, 24, 359); labelVictoryConditionText = std::make_shared(60, 307, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); labelLossConditionText = std::make_shared(60, 366, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); @@ -356,7 +356,7 @@ CFlagBox::CFlagBox(const Rect & rect) labelAllies = std::make_shared(0, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); labelEnemies = std::make_shared(133, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); - iconsTeamFlags = std::make_shared("ITGFLAGS.DEF"); + iconsTeamFlags = std::make_shared(AnimationPath::builtin("ITGFLAGS.DEF")); iconsTeamFlags->preload(); } @@ -390,7 +390,7 @@ void CFlagBox::showPopupWindow(const Point & cursorPosition) } CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr icons) - : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, "DIBOXBCK") + : CWindowObject(BORDERED | RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("DIBOXBCK")) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index f9de0b3f3..48f675425 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -137,7 +137,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) } }); - const JsonNode config(ResourceID("config/widgets/optionsTab.json")); + const JsonNode config(ResourcePath("config/widgets/optionsTab.json")); build(config); //set timers combo box callbacks @@ -343,18 +343,18 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) return 0; } -std::string OptionsTab::CPlayerSettingsHelper::getImageName(bool big) +AnimationPath OptionsTab::CPlayerSettingsHelper::getImageName(bool big) { switch(type) { case OptionsTab::TOWN: - return big ? "ITPt": "ITPA"; + return AnimationPath::builtin(big ? "ITPt": "ITPA"); case OptionsTab::HERO: - return big ? "PortraitsLarge": "PortraitsSmall"; + return AnimationPath::builtin(big ? "PortraitsLarge": "PortraitsSmall"); case OptionsTab::BONUS: - return "SCNRSTAR"; + return AnimationPath::builtin("SCNRSTAR"); } - return ""; + return {}; } std::string OptionsTab::CPlayerSettingsHelper::getName() @@ -553,7 +553,7 @@ OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelp void OptionsTab::CPlayerOptionTooltipBox::genHeader() { - backgroundTexture = std::make_shared("DIBOXBCK", pos); + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, getTitle()); @@ -585,7 +585,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); auto heroIndex = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); - imageSpeciality = std::make_shared("UN44", (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); } @@ -717,7 +717,7 @@ void OptionsTab::SelectionWindow::recreate() pos = Rect(0, 0, x, y); - backgroundTexture = std::make_shared("DiBoxBck", pos); + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), pos); backgroundTexture->playerColored(PlayerColor(1)); updateShadow(); @@ -747,7 +747,7 @@ void OptionsTab::SelectionWindow::genContentGrid(int lines) { for(int x = 0; x < elementsPerLine; x++) { - components.push_back(std::make_shared("lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderBig"), x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); } } } @@ -763,7 +763,7 @@ void OptionsTab::SelectionWindow::genContentFactions() components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedFaction.getNum() == PlayerSettings::RANDOM) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedFactions) { @@ -776,7 +776,7 @@ void OptionsTab::SelectionWindow::genContentFactions() CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); - components.push_back(std::make_shared(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig", x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); + components.push_back(std::make_shared(ImagePath::builtin(selectedFaction == elem ? "lobby/townBorderBigActivated" : "lobby/townBorderBig"), x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedFaction == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); factions.push_back(elem); @@ -795,7 +795,7 @@ void OptionsTab::SelectionWindow::genContentHeroes() components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); if(selectedHero.getNum() == PlayerSettings::RANDOM) - components.push_back(std::make_shared("lobby/townBorderSmallActivated", 6, (ICON_SMALL_HEIGHT/2))); + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedHeroes) { @@ -814,11 +814,11 @@ void OptionsTab::SelectionWindow::genContentHeroes() components.push_back(std::make_shared(helper.getImageName(true), helper.getImageIndex(true), 0, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, (selectedHero == elem) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - std::string image = "lobby/townBorderBig"; + ImagePath image = ImagePath::builtin("lobby/townBorderBig"); if(selectedHero == elem) - image = "lobby/townBorderBigActivated"; + image = ImagePath::builtin("lobby/townBorderBigActivated"); if(unusableHeroes.count(elem)) - image = "lobby/townBorderBigGrayedOut"; + image = ImagePath::builtin("lobby/townBorderBigGrayedOut"); components.push_back(std::make_shared(image, x * (ICON_BIG_WIDTH-1), y * (ICON_BIG_HEIGHT-1))); heroes.push_back(elem); @@ -843,7 +843,7 @@ void OptionsTab::SelectionWindow::genContentBonus() drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::WHITE , helper.getName()); if(selectedBonus == elem) { - components.push_back(std::make_shared("lobby/townBorderSmallActivated", x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); + components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::YELLOW , helper.getName()); } @@ -1075,18 +1075,18 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp" }}; - background = std::make_shared(bgs[s->color.getNum()], 0, 0); + background = std::make_shared(ImagePath::builtin(bgs[s->color.getNum()]), 0, 0); labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); if(SEL->screenType == ESelectionScreen::newGame) { - buttonTownLeft = std::make_shared(Point(107, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color)); - buttonTownRight = std::make_shared(Point(168, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[133], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, +1, s->color)); - buttonHeroLeft = std::make_shared(Point(183, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[148], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, -1, s->color)); - buttonHeroRight = std::make_shared(Point(244, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[149], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, +1, s->color)); - buttonBonusLeft = std::make_shared(Point(259, 5), "ADOPLFA.DEF", CGI->generaltexth->zelp[164], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, -1, s->color)); - buttonBonusRight = std::make_shared(Point(320, 5), "ADOPRTA.DEF", CGI->generaltexth->zelp[165], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, +1, s->color)); + buttonTownLeft = std::make_shared(Point(107, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[132], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, -1, s->color)); + buttonTownRight = std::make_shared(Point(168, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[133], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::TOWN, +1, s->color)); + buttonHeroLeft = std::make_shared(Point(183, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[148], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, -1, s->color)); + buttonHeroRight = std::make_shared(Point(244, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[149], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::HERO, +1, s->color)); + buttonBonusLeft = std::make_shared(Point(259, 5), AnimationPath::builtin("ADOPLFA.DEF"), CGI->generaltexth->zelp[164], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, -1, s->color)); + buttonBonusRight = std::make_shared(Point(320, 5), AnimationPath::builtin("ADOPRTA.DEF"), CGI->generaltexth->zelp[165], std::bind(&IServerAPI::setPlayerOption, CSH, LobbyChangePlayerOption::BONUS, +1, s->color)); } hideUnavailableButtons(); @@ -1095,7 +1095,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con { flag = std::make_shared( Point(-43, 2), - flags[s->color.getNum()], + AnimationPath::builtin(flags[s->color.getNum()]), CGI->generaltexth->zelp[180], std::bind(&OptionsTab::onSetPlayerClicked, &parentTab, *s) ); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index b3d9f7e9a..042b84bea 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -62,7 +62,7 @@ private: /// visible image settings size_t getImageIndex(bool big = false); - std::string getImageName(bool big = false); + AnimationPath getImageName(bool big = false); std::string getName(); /// name visible in options dialog std::string getTitle(); /// title in popup box diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index fad8a28eb..f116104bc 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -118,7 +118,7 @@ RandomMapTab::RandomMapTab(): }); } - const JsonNode config(ResourceID("config/widgets/randomMapTab.json")); + const JsonNode config(ResourcePath("config/widgets/randomMapTab.json")); build(config); //set combo box callbacks @@ -388,7 +388,7 @@ std::vector RandomMapTab::getPossibleMapSizes() TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { - const JsonNode config(ResourceID("config/widgets/randomMapTeamsWidget.json")); + const JsonNode config(ResourcePath("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; int humanPlayers = randomMapTab.obtainMapGenOptions().getPlayerCount(); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index e2514cb8f..7200fc690 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -158,16 +158,16 @@ SelectionTab::SelectionTab(ESelectionScreen Type) if(tabType != ESelectionScreen::campaignList) { sortingBy = _format; - background = std::make_shared("SCSELBCK.bmp", 0, 6); + background = std::make_shared(ImagePath::builtin("SCSELBCK.bmp"), 0, 6); pos = background->pos; - inputName = std::make_shared(inputNameRect, Point(-32, -25), "GSSTRIP.bmp", 0); + inputName = std::make_shared(inputNameRect, Point(-32, -25), ImagePath::builtin("GSSTRIP.bmp"), 0); inputName->filters += CTextInput::filenameFilter; labelMapSizes = std::make_shared(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]); int sizes[] = {36, 72, 108, 144, 0}; const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"}; for(int i = 0; i < 5; i++) - buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), filterIconNmes[i], CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); + buttonsSortBy.push_back(std::make_shared(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true))); int xpos[] = {23, 55, 88, 121, 306, 339}; const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"}; @@ -177,7 +177,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type) if(criteria == _name) criteria = generalSortingBy; - buttonsSortBy.push_back(std::make_shared(Point(xpos[i], 86), sortIconNames[i], CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria))); + buttonsSortBy.push_back(std::make_shared(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria))); } } @@ -203,17 +203,17 @@ SelectionTab::SelectionTab(ESelectionScreen Type) pos.x += 3; pos.y += 6; - buttonsSortBy.push_back(std::make_shared(Point(23, 86), "CamCusM.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps))); - buttonsSortBy.push_back(std::make_shared(Point(55, 86), "CamCusL.DEF", CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name))); + buttonsSortBy.push_back(std::make_shared(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps))); + buttonsSortBy.push_back(std::make_shared(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name))); break; default: assert(0); break; } - iconsMapFormats = std::make_shared("SCSELC.DEF"); - iconsVictoryCondition = std::make_shared("SCNRVICT.DEF"); - iconsLossCondition = std::make_shared("SCNRLOSS.DEF"); + iconsMapFormats = std::make_shared(AnimationPath::builtin("SCSELC.DEF")); + iconsVictoryCondition = std::make_shared(AnimationPath::builtin("SCNRVICT.DEF")); + iconsLossCondition = std::make_shared(AnimationPath::builtin("SCNRLOSS.DEF")); for(int i = 0; i < positionsToShow; i++) listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); @@ -244,7 +244,7 @@ void SelectionTab::toggleMode() { inputName->disable(); auto files = getFiles("Maps/", EResType::MAP); - files.erase(ResourceID("Maps/Tutorial.tut", EResType::MAP)); + files.erase(ResourcePath("Maps/Tutorial.tut", EResType::MAP)); parseMaps(files); break; } @@ -367,7 +367,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - GH.windows().createAndPushWindow(text, ResourceID(curItems[py]->fileURI), tabType); + GH.windows().createAndPushWindow(text, ResourcePath(curItems[py]->fileURI), tabType); } } @@ -556,7 +556,7 @@ void SelectionTab::select(int position) if(inputName && inputName->isActive()) { - auto filename = *CResourceHandler::get()->getResourceName(ResourceID(curItems[py]->fileURI, EResType::SAVEGAME)); + auto filename = *CResourceHandler::get()->getResourceName(ResourcePath(curItems[py]->fileURI, EResType::SAVEGAME)); inputName->setText(filename.stem().string()); } @@ -723,7 +723,7 @@ bool SelectionTab::isMapSupported(const CMapInfo & info) return false; } -void SelectionTab::parseMaps(const std::unordered_set & files) +void SelectionTab::parseMaps(const std::unordered_set & files) { logGlobal->debug("Parsing %d maps", files.size()); allItems.clear(); @@ -744,7 +744,7 @@ void SelectionTab::parseMaps(const std::unordered_set & files) } } -void SelectionTab::parseSaves(const std::unordered_set & files) +void SelectionTab::parseSaves(const std::unordered_set & files) { for(auto & file : files) { @@ -786,7 +786,7 @@ void SelectionTab::parseSaves(const std::unordered_set & files) } } -void SelectionTab::parseCampaigns(const std::unordered_set & files) +void SelectionTab::parseCampaigns(const std::unordered_set & files) { allItems.reserve(files.size()); for(auto & file : files) @@ -800,7 +800,7 @@ void SelectionTab::parseCampaigns(const std::unordered_set & files) } } -std::unordered_set SelectionTab::getFiles(std::string dirURI, int resType) +std::unordered_set SelectionTab::getFiles(std::string dirURI, EResType resType) { boost::to_upper(dirURI); CResourceHandler::get()->updateFilteredFiles([&](const std::string & mount) @@ -808,7 +808,7 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re return boost::algorithm::starts_with(mount, dirURI); }); - std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourceID & ident) + std::unordered_set ret = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident) { return ident.getType() == resType && boost::algorithm::starts_with(ident.getName(), dirURI); }); @@ -816,7 +816,7 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, int re return ret; } -SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType) +SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType) : CWindowObject(BORDERED | RCLICK_POPUP) { drawPlayerElements = tabType == ESelectionScreen::newGame; @@ -826,7 +826,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI std::vector> mapLayerImages; if(renderImage) - mapLayerImages = createMinimaps(ResourceID(resource.getName(), EResType::MAP), IMAGE_SIZE); + mapLayerImages = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), IMAGE_SIZE); if(mapLayerImages.size() == 0) renderImage = false; @@ -844,7 +844,7 @@ SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourceI pos.h = BORDER + textHeight + BORDER; if(renderImage) pos.h += IMAGE_SIZE + BORDER; - backgroundTexture = std::make_shared("DIBOXBCK", pos); + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); drawLabel(); @@ -903,7 +903,7 @@ Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptr> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourceID resource, int size) +std::vector> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourcePath resource, int size) { std::vector> ret = std::vector>(); @@ -935,7 +935,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico : CIntObject(LCLICK, position) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pictureEmptyLine = std::make_shared(IImage::createFromFile("camcust"), Rect(25, 121, 349, 26), -8, -14); + pictureEmptyLine = std::make_shared(IImage::createFromFile(ImagePath::builtin("camcust")), Rect(25, 121, 349, 26), -8, -14); labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -945,7 +945,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico labelMapSizeLetter = std::make_shared(41, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelMapSizeLetter->setAutoRedraw(false); // FIXME: This -12 should not be needed, but for some reason CAnimImage displaced otherwise - iconFolder = std::make_shared("lobby/iconFolder.png", -8, -12); + iconFolder = std::make_shared(ImagePath::builtin("lobby/iconFolder.png"), -8, -12); iconFormat = std::make_shared(iconsFormats, 0, 0, 59, -12); iconVictoryCondition = std::make_shared(iconsVictory, 0, 0, 277, -12); iconLossCondition = std::make_shared(iconsLoss, 0, 0, 310, -12); diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 50426b78d..e4d67ef83 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CMap; VCMI_LIB_NAMESPACE_END #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/filesystem/ResourcePath.h" class CSlider; class CLabel; @@ -81,9 +82,9 @@ class SelectionTab : public CIntObject std::shared_ptr image2; Canvas createMinimapForLayer(std::unique_ptr & map, int layer); - std::vector> createMinimaps(ResourceID resource, int size); + std::vector> createMinimaps(ResourcePath resource, int size); public: - CMapInfoTooltipBox(std::string text, ResourceID resource, ESelectionScreen tabType); + CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType); }; public: std::vector> allItems; @@ -134,8 +135,8 @@ private: auto checkSubfolder(std::string path); bool isMapSupported(const CMapInfo & info); - void parseMaps(const std::unordered_set & files); - void parseSaves(const std::unordered_set & files); - void parseCampaigns(const std::unordered_set & files); - std::unordered_set getFiles(std::string dirURI, int resType); + void parseMaps(const std::unordered_set & files); + void parseSaves(const std::unordered_set & files); + void parseCampaigns(const std::unordered_set & files); + std::unordered_set getFiles(std::string dirURI, EResType resType); }; diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index d1c9fd328..c8d695aee 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -86,7 +86,7 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt if(!button["help"].isNull() && button["help"].Float() > 0) help = CGI->generaltexth->zelp[(size_t)button["help"].Float()]; - return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), button["name"].String(), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); + return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), AnimationPath::fromJson(button["name"]), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); } CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) @@ -109,14 +109,14 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) if(status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); - graphicsImage = std::make_shared(config["image"].String()); + graphicsImage = std::make_shared(ImagePath::fromJson(config["image"])); hoverLabel = std::make_shared(pos.w / 2, pos.h + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, ""); parent->addChild(hoverLabel.get()); } if(status == CCampaignScreen::COMPLETED) - graphicsCompleted = std::make_shared("CAMPCHK"); + graphicsCompleted = std::make_shared(ImagePath::builtin("CAMPCHK")); } void CCampaignScreen::CCampaignButton::show(Canvas & to) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 9580d91c8..bd6edd96a 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -78,7 +78,7 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared(config["background"].String()); + background = std::make_shared(ImagePath::fromJson(config["background"])); if(config["scalable"].Bool()) background->scaleTo(GH.screenDimensions()); @@ -237,7 +237,7 @@ std::shared_ptr CMenuEntry::createButton(CMenuScreen * parent, const Js EShortcut shortcut = GH.shortcuts().findShortcut(button["shortcut"].String()); - auto result = std::make_shared(Point(posx, posy), button["name"].String(), help, command, shortcut); + auto result = std::make_shared(Point(posx, posy), AnimationPath::fromJson(button["name"]), help, command, shortcut); if (button["center"].Bool()) result->moveBy(Point(-result->pos.w/2, -result->pos.h/2)); @@ -262,7 +262,7 @@ CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) } CMainMenuConfig::CMainMenuConfig() - : campaignSets(JsonNode(ResourceID("config/campaignSets.json"))), config(JsonNode(ResourceID("config/mainmenu.json"))) + : campaignSets(JsonNode(ResourcePath("config/campaignSets.json"))), config(JsonNode(ResourcePath("config/mainmenu.json"))) { } @@ -291,7 +291,7 @@ CMainMenu::CMainMenu() GH.defActionsDef = 63; menu = std::make_shared(CMainMenuConfig::get().getConfig()["window"]); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - backgroundAroundMenu = std::make_shared("DIBOXBCK", pos); + backgroundAroundMenu = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); } CMainMenu::~CMainMenu() @@ -374,7 +374,7 @@ void CMainMenu::openCampaignScreen(std::string name) void CMainMenu::startTutorial() { - ResourceID tutorialMap("Maps/Tutorial.tut", EResType::MAP); + ResourcePath tutorialMap("Maps/Tutorial.tut", EResType::MAP); if(!CResourceHandler::get()->existsResource(tutorialMap)) { CInfoWindow::showInfoDialog(CGI->generaltexth->translate("core.genrltxt.742"), std::vector>(), PlayerColor(1)); @@ -397,7 +397,7 @@ std::shared_ptr CMainMenu::create() std::shared_ptr CMainMenu::createPicture(const JsonNode & config) { - return std::make_shared(config["name"].String(), (int)config["x"].Float(), (int)config["y"].Float()); + return std::make_shared(ImagePath::fromJson(config["name"]), (int)config["x"].Float(), (int)config["y"].Float()); } CMultiMode::CMultiMode(ESelectionScreen ScreenType) @@ -405,20 +405,20 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUPOPUP.bmp"); + background = std::make_shared(ImagePath::builtin("MUPOPUP.bmp")); pos = background->center(); //center, window has size of bg graphic - picture = std::make_shared("MUMAP.bmp", 16, 77); + picture = std::make_shared(ImagePath::builtin("MUMAP.bmp"), 16, 77); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 465, 440, 18), 7, 465)); playerName = std::make_shared(Rect(19, 436, 334, 16), background->getSurface()); playerName->setText(getPlayerName()); playerName->cb += std::bind(&CMultiMode::onNameChange, this, _1); - buttonHotseat = std::make_shared(Point(373, 78), "MUBHOT.DEF", CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); - buttonHost = std::make_shared(Point(373, 78 + 57 * 1), "MUBHOST.DEF", CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); - buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), "MUBJOIN.DEF", CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); - buttonCancel = std::make_shared(Point(373, 424), "MUBCANC.DEF", CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonHotseat = std::make_shared(Point(373, 78), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this)); + buttonHost = std::make_shared(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this)); + buttonJoin = std::make_shared(Point(373, 78 + 57 * 2), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this)); + buttonCancel = std::make_shared(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL); } void CMultiMode::hostTCP() @@ -453,7 +453,7 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S : loadMode(LoadMode), screenType(ScreenType), host(Host) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUHOTSEA.bmp"); + background = std::make_shared(ImagePath::builtin("MUHOTSEA.bmp")); pos = background->center(); //center, window has size of bg graphic std::string text = CGI->generaltexth->allTexts[446]; @@ -466,8 +466,8 @@ CMultiPlayers::CMultiPlayers(const std::string & firstPlayer, ESelectionScreen S inputNames[i]->cb += std::bind(&CMultiPlayers::onChange, this, _1); } - buttonOk = std::make_shared(Point(95, 338), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT); - buttonCancel = std::make_shared(Point(205, 338), "MUBCANC.DEF", CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL); + buttonOk = std::make_shared(Point(95, 338), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CMultiPlayers::enterSelectionScreen, this), EShortcut::GLOBAL_ACCEPT); + buttonCancel = std::make_shared(Point(205, 338), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], [=](){ close();}, EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 381, 348, 18), 7, 381)); inputNames[0]->setText(firstPlayer, true); @@ -498,7 +498,7 @@ void CMultiPlayers::enterSelectionScreen() CSimpleJoinScreen::CSimpleJoinScreen(bool host) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared("MUDIALOG.bmp"); // address background + background = std::make_shared(ImagePath::builtin("MUDIALOG.bmp")); // address background pos = background->center(); //center, window has size of bg graphic (x,y = 396,278 w=232 h=212) textTitle = std::make_shared("", Rect(20, 20, 205, 50), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); @@ -515,14 +515,14 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); - buttonOk = std::make_shared(Point(26, 142), "MUBCHCK.DEF", CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); inputAddress->giveFocus(); } inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); inputPort->setText(std::to_string(CSH->getHostPort()), true); - buttonCancel = std::make_shared(Point(142, 142), "MUBCANC.DEF", CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); } @@ -601,7 +601,7 @@ CLoadingScreen::CLoadingScreen() for(int i = 0; i < blocksAmount; ++i) { - progressBlocks.push_back(std::make_shared(conf["name"].String(), i, 0, posx + i * blockSize, posy)); + progressBlocks.push_back(std::make_shared(AnimationPath::fromJson(conf["name"]), i, 0, posx + i * blockSize, posy)); progressBlocks.back()->deactivate(); progressBlocks.back()->visible = false; } @@ -626,24 +626,24 @@ void CLoadingScreen::tick(uint32_t msPassed) } } -std::string CLoadingScreen::getBackground() +ImagePath CLoadingScreen::getBackground() { - std::string fname = "loadbar"; + ImagePath fname = ImagePath::builtin("loadbar"); const auto & conf = CMainMenuConfig::get().getConfig()["loading"]; if(conf.isStruct()) { if(conf["background"].isVector()) - return RandomGeneratorUtil::nextItem(conf["background"].Vector(), CRandomGenerator::getDefault())->String(); + return ImagePath::fromJson(*RandomGeneratorUtil::nextItem(conf["background"].Vector(), CRandomGenerator::getDefault())); if(conf["background"].isString()) - return conf["background"].String(); + return ImagePath::fromJson(conf["background"]); return fname; } if(conf.isVector() && !conf.Vector().empty()) - return RandomGeneratorUtil::nextItem(conf.Vector(), CRandomGenerator::getDefault())->String(); + return ImagePath::fromJson(*RandomGeneratorUtil::nextItem(conf.Vector(), CRandomGenerator::getDefault())); return fname; } diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 7e632fe6f..327fdb908 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -185,7 +185,7 @@ class CLoadingScreen : virtual public CWindowObject, virtual public Load::Progre { std::vector> progressBlocks; - std::string getBackground(); + ImagePath getBackground(); public: CLoadingScreen(); diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp index dd8aae62c..8848ec407 100644 --- a/client/mainmenu/CreditsScreen.cpp +++ b/client/mainmenu/CreditsScreen.cpp @@ -26,7 +26,7 @@ CreditsScreen::CreditsScreen(Rect rect) pos.h = rect.h; setRedrawParent(true); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/CREDITS.TXT"))->readAll(); + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/CREDITS.TXT"))->readAll(); std::string text((char *)textFile.first.get(), textFile.second); size_t firstQuote = text.find('\"') + 1; text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote); diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 0b073177a..be14e7dcf 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -94,7 +94,7 @@ MapTileStorage::MapTileStorage(size_t capacity) { } -void MapTileStorage::load(size_t index, const std::string & filename, EImageBlitMode blitMode) +void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode) { auto & terrainAnimations = animations[index]; @@ -247,7 +247,7 @@ uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & co MapRendererBorder::MapRendererBorder() { emptyFill = std::make_unique(Point(32,32)); - animation = std::make_unique("EDG"); + animation = std::make_unique(AnimationPath::builtin("EDG")); animation->preload(); } @@ -309,9 +309,9 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & MapRendererFow::MapRendererFow() { - fogOfWarFullHide = std::make_unique("TSHRC"); + fogOfWarFullHide = std::make_unique(AnimationPath::builtin("TSHRC")); fogOfWarFullHide->preload(); - fogOfWarPartialHide = std::make_unique("TSHRE"); + fogOfWarPartialHide = std::make_unique(AnimationPath::builtin("TSHRE")); fogOfWarPartialHide->preload(); for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) @@ -387,7 +387,7 @@ std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectI return getAnimation(info->animationFile, generateMovementGroups); } -std::shared_ptr MapRendererObjects::getAnimation(const std::string & filename, bool generateMovementGroups) +std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups) { auto it = animations.find(filename); @@ -422,7 +422,7 @@ std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectI { assert(dynamic_cast(obj) != nullptr); assert(obj->tempOwner.isValidPlayer()); - return getAnimation(heroFlags[obj->tempOwner.getNum()], true); + return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true); } if(obj->ID == Obj::BOAT) @@ -557,10 +557,10 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & } MapRendererOverlay::MapRendererOverlay() - : imageGrid(IImage::createFromFile("debug/grid", EImageBlitMode::ALPHA)) - , imageBlocked(IImage::createFromFile("debug/blocked", EImageBlitMode::ALPHA)) - , imageVisitable(IImage::createFromFile("debug/visitable", EImageBlitMode::ALPHA)) - , imageSpellRange(IImage::createFromFile("debug/spellRange", EImageBlitMode::ALPHA)) + : imageGrid(IImage::createFromFile(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) + , imageBlocked(IImage::createFromFile(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) + , imageVisitable(IImage::createFromFile(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) + , imageSpellRange(IImage::createFromFile(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) { } @@ -616,7 +616,7 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & } MapRendererPath::MapRendererPath() - : pathNodes(new CAnimation("ADAG")) + : pathNodes(new CAnimation(AnimationPath::builtin("ADAG"))) { pathNodes->preload(); } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index 2ee036f88..6fc1ccce7 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../../lib/filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN class int3; @@ -30,7 +32,7 @@ class MapTileStorage public: explicit MapTileStorage(size_t capacity); - void load(size_t index, const std::string & filename, EImageBlitMode blitMode); + void load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode); std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); }; @@ -69,13 +71,13 @@ public: class MapRendererObjects { - std::unordered_map> animations; + std::map> animations; std::shared_ptr getBaseAnimation(const CGObjectInstance * obj); std::shared_ptr getFlagAnimation(const CGObjectInstance * obj); std::shared_ptr getOverlayAnimation(const CGObjectInstance * obj); - std::shared_ptr getAnimation(const std::string & filename, bool generateMovementGroups); + std::shared_ptr getAnimation(const AnimationPath & filename, bool generateMovementGroups); std::shared_ptr getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index 18ed65208..528ba5b39 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -28,7 +28,7 @@ MapViewCache::MapViewCache(const std::shared_ptr & model) : model(model) , cachedLevel(0) , mapRenderer(new MapRenderer()) - , iconsStorage(new CAnimation("VwSymbol")) + , iconsStorage(new CAnimation(AnimationPath::builtin("VwSymbol"))) , intermediate(new Canvas(Point(32, 32))) , terrain(new Canvas(model->getCacheDimensionsPixels())) , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 40cf03681..6d2a20f7a 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -22,7 +22,7 @@ std::shared_ptr CAnimation::getFromExtraDef(std::string filename) size_t pos = filename.find(':'); if (pos == -1) return nullptr; - CAnimation anim(filename.substr(0, pos)); + CAnimation anim(AnimationPath::builtinTODO(filename.substr(0, pos))); pos++; size_t frame = atoi(filename.c_str()+pos); size_t group = 0; @@ -144,7 +144,7 @@ void CAnimation::exportBitmaps(const boost::filesystem::path& path) const return; } - boost::filesystem::path actualPath = path / "SPRITES" / name; + boost::filesystem::path actualPath = path / "SPRITES" / name.getName(); boost::filesystem::create_directories(actualPath); size_t counter = 0; @@ -179,16 +179,15 @@ void CAnimation::init() source[defEntry.first].resize(defEntry.second); } - ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); + if (vstd::contains(graphics->imageLists, name.getName())) + initFromJson(graphics->imageLists[name.getName()]); - if (vstd::contains(graphics->imageLists, resID.getName())) - initFromJson(graphics->imageLists[resID.getName()]); - - auto configList = CResourceHandler::get()->getResourcesWithName(resID); + auto jsonResource = name.toType(); + auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); for(auto & loader : configList) { - auto stream = loader->load(resID); + auto stream = loader->load(jsonResource); std::unique_ptr textData(new ui8[stream->getSize()]); stream->read(textData.get(), stream->getSize()); @@ -200,28 +199,21 @@ void CAnimation::init() void CAnimation::printError(size_t frame, size_t group, std::string type) const { - logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name, group, frame); + logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name.getOriginalName(), group, frame); } -CAnimation::CAnimation(std::string Name): - name(Name), +CAnimation::CAnimation(const AnimationPath & Name): + name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), preloaded(false), defFile() { - size_t dotPos = name.find_last_of('.'); - if ( dotPos!=-1 ) - name.erase(dotPos); - std::transform(name.begin(), name.end(), name.begin(), toupper); - - ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); - - if(CResourceHandler::get()->existsResource(resource)) + if(CResourceHandler::get()->existsResource(name)) defFile = std::make_shared(name); init(); if(source.empty()) - logAnim->error("Animation %s failed to load", Name); + logAnim->error("Animation %s failed to load", Name.getOriginalName()); } CAnimation::CAnimation(): @@ -238,13 +230,13 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra { if(!source.count(sourceGroup)) { - logAnim->error("Group %d missing in %s", sourceGroup, name); + logAnim->error("Group %d missing in %s", sourceGroup, name.getName()); return; } if(source[sourceGroup].size() <= sourceFrame) { - logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name); + logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name.getName()); return; } @@ -253,7 +245,7 @@ void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFra if(clone.getType() == JsonNode::JsonType::DATA_NULL) { - std::string temp = name+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); + std::string temp = name.getName()+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); clone["file"].String() = temp; } diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index 7e26d13cd..b39bfe401 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/GameConstants.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class JsonNode; @@ -29,7 +30,7 @@ private: std::map > > images; //animation file name - std::string name; + AnimationPath name; bool preloaded; @@ -53,7 +54,7 @@ private: std::shared_ptr getFromExtraDef(std::string filename); public: - CAnimation(std::string Name); + CAnimation(const AnimationPath & Name); CAnimation(); ~CAnimation(); diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index c18612e8f..9fc4e93ac 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -110,14 +110,14 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna logGlobal->warn("Call to loadBitmap with void fname!"); return nullptr; } - if (!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) + if (!CResourceHandler::get()->existsResource(ResourcePath(path + fname, EResType::IMAGE))) { return nullptr; } SDL_Surface * ret=nullptr; - auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll(); + auto readFile = CResourceHandler::get()->load(ResourcePath(path + fname, EResType::IMAGE))->readAll(); if (isPCX(readFile.first.get())) {//H3-style PCX diff --git a/client/render/CDefFile.cpp b/client/render/CDefFile.cpp index a19acc5be..bf4a79b06 100644 --- a/client/render/CDefFile.cpp +++ b/client/render/CDefFile.cpp @@ -24,7 +24,7 @@ class CFileCache static const int cacheSize = 50; //Max number of cached files struct FileData { - ResourceID name; + AnimationPath name; size_t size; std::unique_ptr data; @@ -34,7 +34,7 @@ class CFileCache std::copy(data.get(), data.get() + size, ret.get()); return ret; } - FileData(ResourceID name_, size_t size_, std::unique_ptr data_): + FileData(AnimationPath name_, size_t size_, std::unique_ptr data_): name{std::move(name_)}, size{size_}, data{std::move(data_)} @@ -43,7 +43,7 @@ class CFileCache std::deque cache; public: - std::unique_ptr getCachedFile(ResourceID rid) + std::unique_ptr getCachedFile(AnimationPath rid) { for(auto & file : cache) { @@ -97,7 +97,7 @@ static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs) return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; } -CDefFile::CDefFile(std::string Name): +CDefFile::CDefFile(const AnimationPath & Name): data(nullptr), palette(nullptr) { @@ -124,7 +124,7 @@ CDefFile::CDefFile(std::string Name): {0, 0, 0, 64 } // shadow border below selection ( used in battle def's ) }; - data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); + data = animationCache.getCachedFile(Name); palette = std::unique_ptr(new SDL_Color[256]); int it = 0; diff --git a/client/render/CDefFile.h b/client/render/CDefFile.h index 3aec57f4c..ad6de846e 100644 --- a/client/render/CDefFile.h +++ b/client/render/CDefFile.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/vcmi_endian.h" +#include "../../lib/filesystem/ResourcePath.h" class IImageLoader; struct SDL_Color; @@ -39,7 +40,7 @@ private: std::unique_ptr palette; public: - CDefFile(std::string Name); + CDefFile(const AnimationPath & Name); ~CDefFile(); //load frame as SDL_Surface diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index ea9b55122..c23751484 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -44,7 +44,7 @@ Graphics * graphics = nullptr; void Graphics::loadPaletteAndColors() { - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); std::string pals((char*)textFile.first.get(), textFile.second); int startPoint = 24; //beginning byte; used to read @@ -62,7 +62,7 @@ void Graphics::loadPaletteAndColors() } } - auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); + auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); CBinaryReader reader(stream.get()); for(int i=0; i<32; ++i) @@ -102,10 +102,10 @@ void Graphics::initializeBattleGraphics() allConfigs.insert(allConfigs.begin(), ModScope::scopeBuiltin()); for(auto & mod : allConfigs) { - if(!CResourceHandler::get(mod)->existsResource(ResourceID("config/battles_graphics.json"))) + if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) continue; - const JsonNode config(mod, ResourceID("config/battles_graphics.json")); + const JsonNode config(mod, ResourcePath("config/battles_graphics.json")); //initialization of AC->def name mapping if(!config["ac_mapping"].isNull()) @@ -204,7 +204,7 @@ void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) void Graphics::loadFonts() { - const JsonNode config(ResourceID("config/fonts.json")); + const JsonNode config(ResourcePath("config/fonts.json")); const JsonVector & bmpConf = config["bitmap"].Vector(); const JsonNode & ttfConf = config["trueType"]; @@ -228,7 +228,7 @@ void Graphics::loadFonts() void Graphics::loadErmuToPicture() { //loading ERMU to picture - const JsonNode config(ResourceID("config/ERMU_to_picture.json")); + const JsonNode config(ResourcePath("config/ERMU_to_picture.json")); int etp_idx = 0; for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { int idx = 0; @@ -279,16 +279,14 @@ void Graphics::initializeImageLists() addImageListEntries(CGI->skills()); } -std::shared_ptr Graphics::getAnimation(const std::string & path) +std::shared_ptr Graphics::getAnimation(const AnimationPath & path) { - ResourceID animationPath(path, EResType::ANIMATION); + if (cachedAnimations.count(path) != 0) + return cachedAnimations.at(path); - if (cachedAnimations.count(animationPath.getName()) != 0) - return cachedAnimations.at(animationPath.getName()); - - auto newAnimation = std::make_shared(animationPath.getName()); + auto newAnimation = std::make_shared(path); newAnimation->preload(); - cachedAnimations[animationPath.getName()] = newAnimation; + cachedAnimations[path] = newAnimation; return newAnimation; } diff --git a/client/render/Graphics.h b/client/render/Graphics.h index 281226d9b..dfa0e1123 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -11,6 +11,7 @@ #include "../lib/GameConstants.h" #include "../lib/Color.h" +#include "../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -42,10 +43,10 @@ class Graphics void loadFonts(); void initializeImageLists(); - std::map> cachedAnimations; + std::map> cachedAnimations; public: - std::shared_ptr getAnimation(const std::string & path); + std::shared_ptr getAnimation(const AnimationPath & path); //Fonts static const int FONTS_NUMBER = 9; diff --git a/client/render/IImage.h b/client/render/IImage.h index bc9b37727..e5961082a 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -9,6 +9,8 @@ */ #pragma once +#include "../../lib/filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN class PlayerColor; @@ -84,8 +86,8 @@ public: virtual ~IImage(); /// loads image from specified file. Returns 0-sized images on failure - static std::shared_ptr createFromFile( const std::string & path ); - static std::shared_ptr createFromFile( const std::string & path, EImageBlitMode mode ); + static std::shared_ptr createFromFile( const ImagePath & path ); + static std::shared_ptr createFromFile( const ImagePath & path, EImageBlitMode mode ); /// temporary compatibility method. Creates IImage from existing SDL_Surface /// Surface will be shared, called must still free it with SDL_FreeSurface diff --git a/client/renderSDL/CBitmapFont.cpp b/client/renderSDL/CBitmapFont.cpp index 3271254cd..36a29d174 100644 --- a/client/renderSDL/CBitmapFont.cpp +++ b/client/renderSDL/CBitmapFont.cpp @@ -24,7 +24,7 @@ #include -void CBitmapFont::loadModFont(const std::string & modName, const ResourceID & resource) +void CBitmapFont::loadModFont(const std::string & modName, const ResourcePath & resource) { if (!CResourceHandler::get(modName)->existsResource(resource)) { @@ -72,7 +72,7 @@ void CBitmapFont::loadModFont(const std::string & modName, const ResourceID & re CBitmapFont::CBitmapFont(const std::string & filename): maxHeight(0) { - ResourceID resource("data/" + filename, EResType::BMP_FONT); + ResourcePath resource("data/" + filename, EResType::BMP_FONT); loadModFont("core", resource); diff --git a/client/renderSDL/CBitmapFont.h b/client/renderSDL/CBitmapFont.h index b1d5bc5b1..b5fa2b4f4 100644 --- a/client/renderSDL/CBitmapFont.h +++ b/client/renderSDL/CBitmapFont.h @@ -12,7 +12,7 @@ #include "../render/IFont.h" VCMI_LIB_NAMESPACE_BEGIN -class ResourceID; +class ResourcePath; VCMI_LIB_NAMESPACE_END class CBitmapFont : public IFont @@ -31,7 +31,7 @@ class CBitmapFont : public IFont std::unordered_map chars; uint32_t maxHeight; - void loadModFont(const std::string & modName, const ResourceID & resource); + void loadModFont(const std::string & modName, const ResourcePath & resource); void renderCharacter(SDL_Surface * surface, const BitmapChar & character, const ColorRGBA & color, int &posX, int &posY) const; void renderText(SDL_Surface * surface, const std::string & data, const ColorRGBA & color, const Point & pos) const override; diff --git a/client/renderSDL/CBitmapHanFont.cpp b/client/renderSDL/CBitmapHanFont.cpp index d97b2ca2b..cb527d054 100644 --- a/client/renderSDL/CBitmapHanFont.cpp +++ b/client/renderSDL/CBitmapHanFont.cpp @@ -98,7 +98,7 @@ void CBitmapHanFont::renderText(SDL_Surface * surface, const std::string & data, CBitmapHanFont::CBitmapHanFont(const JsonNode &config): fallback(new CBitmapFont(config["fallback"].String())), - data(CResourceHandler::get()->load(ResourceID("data/" + config["name"].String(), EResType::OTHER))->readAll()), + data(CResourceHandler::get()->load(ResourcePath("data/" + config["name"].String(), EResType::OTHER))->readAll()), size((size_t)config["size"].Float()) { // basic tests to make sure that fonts are OK diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index 47246d708..76bf4ed96 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -24,7 +24,7 @@ std::pair, ui64> CTrueTypeFont::loadData(const JsonNode & config) { std::string filename = "Data/" + config["file"].String(); - return CResourceHandler::get()->load(ResourceID(filename, EResType::TTF_FONT))->readAll(); + return CResourceHandler::get()->load(ResourcePath(filename, EResType::TTF_FONT))->readAll(); } TTF_Font * CTrueTypeFont::loadFont(const JsonNode &config) diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 1778bbad6..0a53abb23 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -24,14 +24,14 @@ class SDLImageLoader; -std::shared_ptr IImage::createFromFile( const std::string & path ) +std::shared_ptr IImage::createFromFile( const ImagePath & path ) { return createFromFile(path, EImageBlitMode::ALPHA); } -std::shared_ptr IImage::createFromFile( const std::string & path, EImageBlitMode mode ) +std::shared_ptr IImage::createFromFile( const ImagePath & path, EImageBlitMode mode ) { - return std::shared_ptr(new SDLImage(path, mode)); + return std::shared_ptr(new SDLImage(path.getName(), mode)); } std::shared_ptr IImage::createFromSurface( SDL_Surface * source ) diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index ae3411330..585029634 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -89,7 +89,7 @@ void CButton::addOverlay(std::shared_ptr newOverlay) update(); } -void CButton::addImage(std::string filename) +void CButton::addImage(const AnimationPath & filename) { imageNames.push_back(filename); } @@ -232,7 +232,7 @@ void CButton::hover (bool on) } } -CButton::CButton(Point position, const std::string &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): +CButton::CButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback, EShortcut key, bool playerColoredButton): CKeyShortcut(key), callback(Callback) { @@ -357,7 +357,7 @@ void CToggleBase::addCallback(std::function function) callback += function; } -CToggleButton::CToggleButton(Point position, const std::string &defName, const std::pair &help, +CToggleButton::CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList callback, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton), CToggleBase(callback) diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index ec387451f..8944d2928 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -12,6 +12,7 @@ #include "../gui/CIntObject.h" #include "../render/EFont.h" #include "../../lib/FunctionList.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class Rect; @@ -36,7 +37,7 @@ public: HIGHLIGHTED=3 }; protected: - std::vector imageNames;//store list of images that can be used by this button + std::vector imageNames;//store list of images that can be used by this button size_t currentImage; ButtonState state;//current state of button from enum @@ -72,7 +73,7 @@ public: void addOverlay(std::shared_ptr newOverlay); void addTextOverlay(const std::string & Text, EFonts font, ColorRGBA color); - void addImage(std::string filename); + void addImage(const AnimationPath & filename); void addHoverText(ButtonState state, std::string text); void setImageOrder(int state1, int state2, int state3, int state4); @@ -84,7 +85,7 @@ public: bool isHighlighted(); /// Constructor - CButton(Point position, const std::string & defName, const std::pair & help, + CButton(Point position, const AnimationPath & defName, const std::pair & help, CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); /// Appearance modifiers @@ -145,7 +146,7 @@ class CToggleButton : public CButton, public CToggleBase void setEnabled(bool enabled) override; public: - CToggleButton(Point position, const std::string &defName, const std::pair &help, + CToggleButton(Point position, const AnimationPath &defName, const std::pair &help, CFunctionList Callback = 0, EShortcut key = {}, bool playerColoredButton = false ); void clickPressed(const Point & cursorPosition) override; diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 5e443d959..d27a2975f 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -89,7 +89,7 @@ void CCommanderArtPlace::createImage() if(ourArt) imageIndex = ourArt->artType->getIconIndex(); - image = std::make_shared("artifact", imageIndex); + image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); if(!ourArt) image->disable(); } @@ -247,11 +247,11 @@ void CHeroArtPlace::createImage() else if(ourArt) imageIndex = ourArt->artType->getIconIndex(); - image = std::make_shared("artifact", imageIndex); + image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); if(!ourArt) image->disable(); - selection = std::make_shared("artifact", ArtifactID::ART_SELECTION); + selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); selection->disable(); } diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index f8c509d05..d0cb80bd3 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -42,7 +42,7 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position) for(int i = 0; i < visibleCapacityMax; i++) { - auto artifactSlotBackground = std::make_shared("heroWindow/artifactSlotEmpty", + auto artifactSlotBackground = std::make_shared( ImagePath::builtin("heroWindow/artifactSlotEmpty"), Point(slotSizeWithMargin * (i % HERO_BACKPACK_WINDOW_SLOT_COLUMNS), slotSizeWithMargin * (i / HERO_BACKPACK_WINDOW_SLOT_COLUMNS))); backpackSlotsBackgrounds.emplace_back(artifactSlotBackground); diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index f24d836ae..3ee38c6d7 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -86,8 +86,8 @@ void CArtifactsOfHeroBase::init( artPlace->leftClickCallback = lClickCallback; artPlace->rightClickCallback = rClickCallback; } - leftBackpackRoll = std::make_shared(Point(379, 364), "hsbtns3.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(-1); }, EShortcut::MOVE_LEFT); - rightBackpackRoll = std::make_shared(Point(632, 364), "hsbtns5.def", CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT); + leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollHandler]() { scrollHandler(-1); }, EShortcut::MOVE_LEFT); + rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT); leftBackpackRoll->block(true); rightBackpackRoll->block(true); } diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index c4d4dee7d..0ad59f1dd 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -102,7 +102,7 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts } } -const std::vector CComponent::getFileName() +std::vector CComponent::getFileName() { static const std::string primSkillsArr [] = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; static const std::string secSkillsArr [] = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; @@ -115,9 +115,9 @@ const std::vector CComponent::getFileName() static const std::string heroArr [] = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; static const std::string flagArr [] = {"CREST58", "CREST58", "CREST58", "CREST58"}; - auto gen = [](const std::string * arr) + auto gen = [](const std::string * arr) -> std::vector { - return std::vector(arr, arr + 4); + return { AnimationPath::builtin(arr[0]), AnimationPath::builtin(arr[1]), AnimationPath::builtin(arr[2]), AnimationPath::builtin(arr[3]) }; }; switch(compType) @@ -131,12 +131,12 @@ const std::vector CComponent::getFileName() case spell: return gen(spellsArr); case morale: return gen(moraleArr); case luck: return gen(luckArr); - case building: return std::vector(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons); + case building: return std::vector(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons); case hero: return gen(heroArr); case flag: return gen(flagArr); } assert(0); - return std::vector(); + return {}; } size_t CComponent::getIndex() @@ -251,7 +251,7 @@ std::string CComponent::getSubtitleInternal() return ""; } -void CComponent::setSurface(std::string defName, int imgPos) +void CComponent::setSurface(const AnimationPath & defName, int imgPos) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); image = std::make_shared(defName, imgPos); diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 7799d7b8f..6db60c438 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -11,7 +11,7 @@ #include "../gui/CIntObject.h" #include "../render/EFont.h" - +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -45,8 +45,8 @@ private: std::vector> lines; size_t getIndex(); - const std::vector getFileName(); - void setSurface(std::string defName, int imgPos); + std::vector getFileName(); + void setSurface(const AnimationPath & defName, int imgPos); std::string getSubtitleInternal(); void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL); diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 18bb0388f..593ace244 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -421,7 +421,7 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa pos.x += x; pos.y += y; - std::string imgName = owner->smallIcons ? "cprsmall" : "TWCRPORT"; + AnimationPath imgName = AnimationPath::builtin(owner->smallIcons ? "cprsmall" : "TWCRPORT"); creatureImage = std::make_shared(graphics->getAnimation(imgName), 0); creatureImage->disable(); diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 3a9c3156a..16b6dd950 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -271,7 +271,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const { if(artSetPtr->isActive()) { - CCS->curh->dragAndDropCursor("artifact", pickedArtInst->artType->getIconIndex()); + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); if(srcLoc.isHolder(hero) || !std::is_same_v>) artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); } diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 29ac42d0c..7b6ee4920 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -155,7 +155,7 @@ void ComboBox::DropDown::setItem(const void * item) GH.windows().popWindows(1); } -ComboBox::ComboBox(Point position, const std::string & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): +ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton) { addCallback([&, dropDownDescriptor]() diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index ced39987d..dafd496bf 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -54,7 +54,7 @@ class ComboBox : public CButton void setItem(const void *); public: - ComboBox(Point position, const std::string & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key = {}, bool playerColoredButton = false); + ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key = {}, bool playerColoredButton = false); //define this callback to fill input vector with data for the combo box std::function &)> onConstructItems; diff --git a/client/widgets/CreatureCostBox.cpp b/client/widgets/CreatureCostBox.cpp index 94c31e29d..546c71864 100644 --- a/client/widgets/CreatureCostBox.cpp +++ b/client/widgets/CreatureCostBox.cpp @@ -38,7 +38,7 @@ void CreatureCostBox::createItems(TResources res) TResources::nziterator iter(res); while(iter.valid()) { - ImagePtr image = std::make_shared("RESOURCE", iter->resType); + ImagePtr image = std::make_shared(AnimationPath::builtin("RESOURCE"), iter->resType); LabelPtr text = std::make_shared(15, 43, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "0"); resources.insert(std::make_pair(iter->resType, std::make_pair(text, image))); diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 9767717b7..e86923bc9 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -42,15 +42,15 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) pos.h = bg->height(); } -CPicture::CPicture( const std::string &bmpname, int x, int y ) +CPicture::CPicture( const ImagePath &bmpname, int x, int y ) : CPicture(bmpname, Point(x,y)) {} -CPicture::CPicture( const std::string &bmpname ) +CPicture::CPicture( const ImagePath & bmpname ) : CPicture(bmpname, Point(0,0)) {} -CPicture::CPicture( const std::string &bmpname, const Point & position ) +CPicture::CPicture( const ImagePath & bmpname, const Point & position ) : bg(IImage::createFromFile(bmpname)) , visible(true) , needRefresh(false) @@ -113,7 +113,7 @@ void CPicture::colorize(PlayerColor player) bg->playerColored(player); } -CFilledTexture::CFilledTexture(std::string imageName, Rect position): +CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position): CIntObject(0, position.topLeft()), texture(IImage::createFromFile(imageName)) { @@ -142,7 +142,7 @@ void CFilledTexture::showAll(Canvas & to) } } -FilledTexturePlayerColored::FilledTexturePlayerColored(std::string imageName, Rect position) +FilledTexturePlayerColored::FilledTexturePlayerColored(const ImagePath & imageName, Rect position) : CFilledTexture(imageName, position) { } @@ -171,7 +171,7 @@ void FilledTexturePlayerColored::playerColored(PlayerColor player) texture->adjustPalette(filters[player.getNum()], 0); } -CAnimImage::CAnimImage(const std::string & name, size_t Frame, size_t Group, int x, int y, ui8 Flags): +CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, int x, int y, ui8 Flags): frame(Frame), group(Group), flags(Flags) @@ -307,7 +307,7 @@ bool CAnimImage::isPlayerColored() const return player.has_value(); } -CShowableAnim::CShowableAnim(int x, int y, std::string name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): +CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): anim(std::make_shared(name)), group(Group), frame(0), @@ -448,7 +448,7 @@ void CShowableAnim::setDuration(int durationMs) frameTimeTotal = durationMs/(last - first); } -CCreatureAnim::CCreatureAnim(int x, int y, std::string name, ui8 flags, ECreatureAnimType type): +CCreatureAnim::CCreatureAnim(int x, int y, const AnimationPath & name, ui8 flags, ECreatureAnimType type): CShowableAnim(x, y, name, flags, 100, size_t(type)) // H3 uses 100 ms per frame, irregardless of battle speed settings { xOffset = 0; diff --git a/client/widgets/Images.h b/client/widgets/Images.h index bfc06f4e3..7716cf8e9 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -11,6 +11,7 @@ #include "../gui/CIntObject.h" #include "../battle/BattleConstants.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class Rect; @@ -49,9 +50,9 @@ public: CPicture(std::shared_ptr image, const Rect &SrcRext, int x = 0, int y = 0); //wrap subrect of given surface /// Loads image from specified file name - CPicture(const std::string & bmpname); - CPicture(const std::string & bmpname, const Point & position); - CPicture(const std::string & bmpname, int x, int y); + CPicture(const ImagePath & bmpname); + CPicture(const ImagePath & bmpname, const Point & position); + CPicture(const ImagePath & bmpname, int x, int y); /// set alpha value for whole surface. Note: may be messed up if surface is shared /// 0=transparent, 255=opaque @@ -71,7 +72,7 @@ protected: Rect imageArea; public: - CFilledTexture(std::string imageName, Rect position); + CFilledTexture(const ImagePath & imageName, Rect position); CFilledTexture(std::shared_ptr image, Rect position, Rect imageArea); void showAll(Canvas & to) override; @@ -80,7 +81,7 @@ public: class FilledTexturePlayerColored : public CFilledTexture { public: - FilledTexturePlayerColored(std::string imageName, Rect position); + FilledTexturePlayerColored(const ImagePath & imageName, Rect position); void playerColored(PlayerColor player); }; @@ -105,7 +106,7 @@ private: public: bool visible; - CAnimImage(const std::string & name, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); + CAnimImage(const AnimationPath & name, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); CAnimImage(std::shared_ptr Anim, size_t Frame, size_t Group=0, int x=0, int y=0, ui8 Flags=0); CAnimImage(std::shared_ptr Anim, size_t Frame, Rect targetPos, size_t Group=0, ui8 Flags=0); ~CAnimImage(); @@ -166,7 +167,7 @@ public: //Set per-surface alpha, 0 = transparent, 255 = opaque void setAlpha(ui32 alphaValue); - CShowableAnim(int x, int y, std::string name, ui8 flags, ui32 frameTime, size_t Group=0, uint8_t alpha = UINT8_MAX); + CShowableAnim(int x, int y, const AnimationPath & name, ui8 flags, ui32 frameTime, size_t Group=0, uint8_t alpha = UINT8_MAX); ~CShowableAnim(); //set animation to group or part of group @@ -213,6 +214,6 @@ public: //clear queue and set animation to this sequence void clearAndSet(ECreatureAnimType type); - CCreatureAnim(int x, int y, std::string name, ui8 flags = 0, ECreatureAnimType = ECreatureAnimType::HOLDING); + CCreatureAnim(int x, int y, const AnimationPath & name, ui8 flags = 0, ECreatureAnimType = ECreatureAnimType::HOLDING); }; diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index e1fdbda5b..7e5ab637f 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -126,7 +126,7 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero) pos.h = 64; if(hero) - portrait = std::make_shared("PortraitsLarge", hero->portrait); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait); } void CHeroArea::clickPressed(const Point & cursorPosition) @@ -201,7 +201,7 @@ CMinorResDataBar::CMinorResDataBar() pos.x = 7; pos.y = 575; - background = std::make_shared("KRESBAR.bmp"); + background = std::make_shared(ImagePath::builtin("KRESBAR.bmp")); background->colorize(LOCPLINT->playerID); pos.w = background->pos.w; @@ -233,7 +233,7 @@ void CArmyTooltip::init(const InfoAboutArmy &army) continue; } - icons.push_back(std::make_shared("CPRSMALL", slot.second.type->getIconIndex(), 0, slotsPos[slot.first.getNum()].x, slotsPos[slot.first.getNum()].y)); + icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), slot.second.type->getIconIndex(), 0, slotsPos[slot.first.getNum()].x, slotsPos[slot.first.getNum()].y)); std::string subtitle; if(army.army.isDetailed) @@ -276,7 +276,7 @@ CArmyTooltip::CArmyTooltip(Point pos, const CArmedInstance * army): void CHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 3, 2); if(hero.details) { @@ -286,8 +286,8 @@ void CHeroTooltip::init(const InfoAboutHero & hero) labels.push_back(std::make_shared(158, 98, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); - morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74); - luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91); + morale = std::make_shared(AnimationPath::builtin("IMRL22"), hero.details->morale + 3, 0, 5, 74); + luck = std::make_shared(AnimationPath::builtin("ILCK22"), hero.details->luck + 3, 0, 5, 91); } } @@ -314,7 +314,7 @@ CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstan void CInteractableHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared("PortraitsLarge", hero.portrait, 0, 3, 2); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 3, 2); title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero.name); if(hero.details) @@ -325,8 +325,8 @@ void CInteractableHeroTooltip::init(const InfoAboutHero & hero) labels.push_back(std::make_shared(158, 98, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(hero.details->mana))); - morale = std::make_shared("IMRL22", hero.details->morale + 3, 0, 5, 74); - luck = std::make_shared("ILCK22", hero.details->luck + 3, 0, 5, 91); + morale = std::make_shared(AnimationPath::builtin("IMRL22"), hero.details->morale + 3, 0, 5, 74); + luck = std::make_shared(AnimationPath::builtin("ILCK22"), hero.details->luck + 3, 0, 5, 91); } } @@ -337,17 +337,17 @@ void CTownTooltip::init(const InfoAboutTown & town) //order of icons in def: fort, citadel, castle, no fort size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; - fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31); + fort = std::make_shared(AnimationPath::builtin("ITMCLS"), fortIndex, 0, 105, 31); assert(town.tType); size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - build = std::make_shared("itpt", iconIndex, 0, 3, 2); + build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); if(town.details) { - hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31); + hall = std::make_shared(AnimationPath::builtin("ITMTLS"), town.details->hallLevel, 0, 67, 31); if(town.details->goldIncome) { @@ -355,18 +355,18 @@ void CTownTooltip::init(const InfoAboutTown & town) std::to_string(town.details->goldIncome)); } if(town.details->garrisonedHero) //garrisoned hero icon - garrisonedHero = std::make_shared("TOWNQKGH", 149, 76); + garrisonedHero = std::make_shared(ImagePath::builtin("TOWNQKGH"), 149, 76); if(town.details->customRes)//silo is built { if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { - res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75); - res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::WOOD), 0, 7, 75); + res2 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), town.tType->primaryRes, 0, 7, 81); } } } @@ -399,18 +399,18 @@ void CInteractableTownTooltip::init(const InfoAboutTown & town) //order of icons in def: fort, citadel, castle, no fort size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; - fort = std::make_shared("ITMCLS", fortIndex, 0, 105, 31); + fort = std::make_shared(AnimationPath::builtin("ITMCLS"), fortIndex, 0, 105, 31); assert(town.tType); size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - build = std::make_shared("itpt", iconIndex, 0, 3, 2); + build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town.name); if(town.details) { - hall = std::make_shared("ITMTLS", town.details->hallLevel, 0, 67, 31); + hall = std::make_shared(AnimationPath::builtin("ITMTLS"), town.details->hallLevel, 0, 67, 31); if(town.details->goldIncome) { @@ -418,18 +418,18 @@ void CInteractableTownTooltip::init(const InfoAboutTown & town) std::to_string(town.details->goldIncome)); } if(town.details->garrisonedHero) //garrisoned hero icon - garrisonedHero = std::make_shared("TOWNQKGH", 149, 76); + garrisonedHero = std::make_shared(ImagePath::builtin("TOWNQKGH"), 149, 76); if(town.details->customRes)//silo is built { if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { - res1 = std::make_shared("SMALRES", GameResID(EGameResID::WOOD), 0, 7, 75); - res2 = std::make_shared("SMALRES", GameResID(EGameResID::ORE), 0, 7, 88); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::WOOD), 0, 7, 75); + res2 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared("SMALRES", town.tType->primaryRes, 0, 7, 81); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), town.tType->primaryRes, 0, 7, 81); } } } @@ -492,7 +492,7 @@ void MoraleLuckBox::set(const AFactionMember * node) else imageName = morale ? "IMRL42" : "ILCK42"; - image = std::make_shared(imageName, bonusValue + 3); + image = std::make_shared(AnimationPath::builtin(imageName), bonusValue + 3); image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon } diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index 2d6aa478b..b2e9d44fe 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -27,10 +27,10 @@ RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - inactiveImage = std::make_shared("radialMenu/itemInactive", Point(0, 0)); - selectedImage = std::make_shared("radialMenu/itemEmpty", Point(0, 0)); + inactiveImage = std::make_shared(ImagePath::builtin("radialMenu/itemInactive"), Point(0, 0)); + selectedImage = std::make_shared(ImagePath::builtin("radialMenu/itemEmpty"), Point(0, 0)); - iconImage = std::make_shared("radialMenu/" + imageName, Point(0, 0)); + iconImage = std::make_shared(ImagePath::builtin("radialMenu/" + imageName), Point(0, 0)); pos = selectedImage->pos; selectedImage->setEnabled(false); @@ -56,7 +56,7 @@ RadialMenu::RadialMenu(const Point & positionToCenter, const std::vectorpos); diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index b65e2f31e..955c01649 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -178,7 +178,7 @@ CSlider::CSlider(Point position, int totalw, std::function Moved, int if(style == BROWN) { - std::string name = getOrientation() == Orientation::HORIZONTAL ? "IGPCRDIV.DEF" : "OVBUTN2.DEF"; + AnimationPath name = AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "IGPCRDIV.DEF" : "OVBUTN2.DEF"); //NOTE: this images do not have "blocked" frames. They should be implemented somehow (e.g. palette transform or something...) left = std::make_shared(Point(), name, CButton::tooltip()); @@ -191,9 +191,9 @@ CSlider::CSlider(Point position, int totalw, std::function Moved, int } else { - left = std::make_shared(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBLF.DEF" : "SCNRBUP.DEF", CButton::tooltip()); - right = std::make_shared(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF", CButton::tooltip()); - slider = std::make_shared(Point(), "SCNRBSL.DEF", CButton::tooltip()); + left = std::make_shared(Point(), AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "SCNRBLF.DEF" : "SCNRBUP.DEF"), CButton::tooltip()); + right = std::make_shared(Point(), AnimationPath::builtin(getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF"), CButton::tooltip()); + slider = std::make_shared(Point(), AnimationPath::builtin("SCNRBSL.DEF"), CButton::tooltip()); } slider->actOnDown = true; slider->soundDisabled = true; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index be496a84e..6d8dc59b9 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -419,7 +419,7 @@ CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, E autoRedraw = false; } -CGStatusBar::CGStatusBar(int x, int y, std::string name, int maxw) +CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) , enteringText(false) { @@ -503,7 +503,7 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) +CTextInput::CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB) :cb(CB), CFocusable(std::make_shared(this)) { pos += Pos.topLeft(); diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index ec7fe6bfa..c1623835b 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -14,6 +14,7 @@ #include "../render/Colors.h" #include "../render/EFont.h" #include "../../lib/FunctionList.h" +#include "../../lib/filesystem/ResourcePath.h" class IImage; class CSlider; @@ -124,7 +125,7 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const ColorRGBA & Color = Colors::WHITE); - CGStatusBar(int x, int y, std::string name, int maxw = -1); + CGStatusBar(int x, int y, const ImagePath & name, int maxw = -1); //make CLabel API private using CLabel::getText; @@ -223,7 +224,7 @@ public: void setHelpText(const std::string &); CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB); - CTextInput(const Rect & Pos, const Point & bgOffset, const std::string & bgName, const CFunctionList & CB); + CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB); CTextInput(const Rect & Pos, std::shared_ptr srf); void clickPressed(const Point & cursorPosition) override; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 9364ad703..74bd835a1 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -263,7 +263,7 @@ bool CBuildingRect::receiveEvent(const Point & position, int eventType) const } CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level) - : CWindowObject(RCLICK_POPUP, "CRTOINFO", Point(centerX, centerY)) + : CWindowObject(RCLICK_POPUP, ImagePath::builtin("CRTOINFO"), Point(centerX, centerY)) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); background->colorize(Town->tempOwner); @@ -282,7 +282,7 @@ CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstanc auto res = static_cast(i); if(creature->getRecruitCost(res)) { - resPicture.push_back(std::make_shared("RESOURCE", i, 0, 0, 0)); + resPicture.push_back(std::make_shared(AnimationPath::builtin("RESOURCE"), i, 0, 0, 0)); resAmount.push_back(std::make_shared(0,0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(creature->getRecruitCost(res)))); } } @@ -310,13 +310,13 @@ CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroS pos.h = 64; upg = updown; - portrait = std::make_shared("PortraitsLarge", 0, 0, 0, 0); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 0, 0); portrait->visible = false; - flag = std::make_shared("CREST58", 0, 0, 0, 0); + flag = std::make_shared(AnimationPath::builtin("CREST58"), 0, 0, 0, 0); flag->visible = false; - selection = std::make_shared("TWCRPORT", 1, 0); + selection = std::make_shared(AnimationPath::builtin("TWCRPORT"), 1, 0); selection->visible = false; set(h); @@ -981,9 +981,8 @@ void CCastleBuildings::enterTownHall() void CCastleBuildings::openMagesGuild() { - std::string mageGuildBackground; - mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; - GH.windows().createAndPushWindow(LOCPLINT->castleInt,mageGuildBackground); + auto mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; + GH.windows().createAndPushWindow(LOCPLINT->castleInt, mageGuildBackground); } void CCastleBuildings::openTownHall() @@ -1009,7 +1008,7 @@ CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, boo ui32 creatureID = town->creatures[level].second.back(); creature = CGI->creh->objects[creatureID]; - picture = std::make_shared("CPRSMALL", creature->getIconIndex(), 0, 8, 0); + picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, 8, 0); std::string value; if(showAvailable) @@ -1109,14 +1108,14 @@ CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townH if(townHall) { buildID = 10 + town->hallLevel(); - picture = std::make_shared("ITMTL.DEF", town->hallLevel()); + picture = std::make_shared(AnimationPath::builtin("ITMTL.DEF"), town->hallLevel()); } else { buildID = 6 + town->fortLevel(); if(buildID == 6) return;//FIXME: suspicious statement, fix or comment - picture = std::make_shared("ITMCL.DEF", town->fortLevel()-1); + picture = std::make_shared(AnimationPath::builtin("ITMCL.DEF"), town->fortLevel()-1); } building = town->town->buildings.at(BuildingID(buildID)); pos = picture->pos; @@ -1154,7 +1153,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst addUsedEvents(KEYBOARD); builds = std::make_shared(town); - panel = std::make_shared("TOWNSCRN", 0, builds->pos.h); + panel = std::make_shared(ImagePath::builtin("TOWNSCRN"), 0, builds->pos.h); panel->colorize(LOCPLINT->playerID); pos.w = panel->pos.w; pos.h = builds->pos.h + panel->pos.h; @@ -1167,12 +1166,12 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); - icon = std::make_shared("ITPT", 0, 0, 15, 387); + icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); - exit = std::make_shared(Point(744, 544), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); exit->setImageOrder(4, 5, 6, 7); - auto split = std::make_shared(Point(744, 382), "TSBTNS", CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() + auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() { garr->splitClick(); heroes->splitClicked(); @@ -1182,11 +1181,11 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst Rect barRect(9, 182, 732, 18); auto statusbarBackground = std::make_shared(panel->getSurface(), barRect, 9, 555); statusbar = CGStatusBar::create(statusbarBackground); - resdatabar = std::make_shared("ARESBAR", 3, 575, 37, 3, 84, 78); + resdatabar = std::make_shared(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78); townlist = std::make_shared(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() ); - townlist->setScrollUpButton( std::make_shared( Point(744, 414), "IAM014", CButton::tooltipLocalized("core.help.306"))); - townlist->setScrollDownButton( std::make_shared( Point(744, 526), "IAM015", CButton::tooltipLocalized("core.help.307"))); + townlist->setScrollUpButton( std::make_shared( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"))); + townlist->setScrollDownButton( std::make_shared( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"))); if(from) townlist->select(from); @@ -1277,7 +1276,7 @@ void CCastleInterface::recreateIcons() hall = std::make_shared(80, 413, town, true); fort = std::make_shared(122, 413, town, false); - fastArmyPurchase = std::make_shared(Point(122, 413), "itmcl.def", CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); fastArmyPurchase->setAnimateLonelyFrame(true); @@ -1351,9 +1350,9 @@ CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * }; icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 2, 2); - header = std::make_shared("TPTHBAR", panelIndex[static_cast(state)], 0, 1, 73); + header = std::make_shared(AnimationPath::builtin("TPTHBAR"), panelIndex[static_cast(state)], 0, 1, 73); if(iconIndex[static_cast(state)] >=0) - mark = std::make_shared("TPTHCHK", iconIndex[static_cast(state)], 0, 136, 56); + mark = std::make_shared(AnimationPath::builtin("TPTHCHK"), iconIndex[static_cast(state)], 0, 136, 56); name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); //todo: add support for all possible states @@ -1405,7 +1404,7 @@ CHallInterface::CHallInterface(const CGTownInstance * Town): statusbar = CGStatusBar::create(statusbarBackground); title = std::make_shared(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->town->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated()); - exit = std::make_shared(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); auto & boxList = town->town->clientInfo.hallSlots; boxes.resize(boxList.size()); @@ -1440,7 +1439,7 @@ CHallInterface::CHallInterface(const CGTownInstance * Town): } CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, EBuildingState state, bool rightClick): - CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), "TPUBUILD"), + CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), ImagePath::builtin("TPUBUILD")), town(Town), building(Building) { @@ -1480,11 +1479,11 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin tooltipNo.appendTextID("core.genrltxt.596"); tooltipNo.replaceTextID(building->getNameTextID()); - buy = std::make_shared(Point(45, 446), "IBUY30", CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); + buy = std::make_shared(Point(45, 446), AnimationPath::builtin("IBUY30"), CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); buy->setBorderColor(Colors::METALLIC_GOLD); buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner); - cancel = std::make_shared(Point(290, 445), "ICANCEL", CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); + cancel = std::make_shared(Point(290, 445), AnimationPath::builtin("ICANCEL"), CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); cancel->setBorderColor(Colors::METALLIC_GOLD); } } @@ -1589,7 +1588,7 @@ CFortScreen::CFortScreen(const CGTownInstance * town): title = std::make_shared(400, 12, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE, fortBuilding->getNameTranslated()); std::string text = boost::str(boost::format(CGI->generaltexth->fcommands[6]) % fortBuilding->getNameTranslated()); - exit = std::make_shared(Point(748, 556), "TPMAGE1", CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); std::vector positions = { @@ -1637,16 +1636,16 @@ CFortScreen::CFortScreen(const CGTownInstance * town): statusbar = CGStatusBar::create(statusbarBackground); } -std::string CFortScreen::getBgName(const CGTownInstance * town) +ImagePath CFortScreen::getBgName(const CGTownInstance * town) { ui32 fortSize = static_cast(town->creatures.size()); if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) fortSize--; if(fortSize == GameConstants::CREATURES_PER_TOWN) - return "TPCASTL7"; + return ImagePath::builtin("TPCASTL7"); else - return "TPCASTL8"; + return ImagePath::builtin("TPCASTL8"); } void CFortScreen::creaturesChangedEventHandler() @@ -1673,7 +1672,7 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * addUsedEvents(SHOW_POPUP); - icons = std::make_shared("TPCAINFO", 261, 3); + icons = std::make_shared(ImagePath::builtin("TPCAINFO"), 261, 3); if(getMyBuilding() != nullptr) { @@ -1766,8 +1765,8 @@ void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) GH.windows().createAndPushWindow(getMyCreature(), true); } -CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem) - : CStatusbarWindow(BORDERED, imagem) +CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename) + : CStatusbarWindow(BORDERED, imagename) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1781,7 +1780,7 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem) auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 7, 556); statusbar = CGStatusBar::create(statusbarBackground); - exit = std::make_shared(Point(748, 556), "TPMAGE1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); static const std::vector > positions = { @@ -1800,7 +1799,7 @@ CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem) if(itown->mageGuildLevel() && owner->town->spells[i].size()>j) spells.push_back(std::make_shared(positions[i][j], CGI->spellh->objects[owner->town->spells[i][j]])); else - emptyScrolls.push_back(std::make_shared("TPMAGES.DEF", 1, 0, positions[i][j].x, positions[i][j].y)); + emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); } } } @@ -1812,7 +1811,7 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) addUsedEvents(LCLICK | SHOW_POPUP | HOVER); pos += position; - image = std::make_shared("SPELLSCR", spell->id); + image = std::make_shared(AnimationPath::builtin("SPELLSCR"), spell->id); pos = image->pos; } @@ -1836,7 +1835,7 @@ void CMageGuildScreen::Scroll::hover(bool on) } CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): - CStatusbarWindow(PLAYER_COLORED, "TPSMITH") + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSMITH")) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1845,7 +1844,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 8, pos.h - 26); statusbar = CGStatusBar::create(statusbarBackground); - animBG = std::make_shared("TPSMITBK", 64, 50); + animBG = std::make_shared(ImagePath::builtin("TPSMITBK"), 64, 50); animBG->needRefresh = true; const CCreature * creature = CGI->creh->objects[creMachineID]; @@ -1869,13 +1868,13 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); - buy = std::make_shared(Point(42, 312), "IBUY30.DEF", CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(224, 312), "ICANCEL.DEF", CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + buy = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30.DEF"), CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); if(possible) buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); else buy->block(true); - costIcon = std::make_shared("RESOURCE", GameResID(EGameResID::GOLD), 0, 148, 244); + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 148, 244); } diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 3e05f3079..353d96542 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -355,7 +355,7 @@ class CFortScreen : public CStatusbarWindow std::shared_ptr resdatabar; std::shared_ptr exit; - std::string getBgName(const CGTownInstance * town); + ImagePath getBgName(const CGTownInstance * town); public: CFortScreen(const CGTownInstance * town); @@ -385,7 +385,7 @@ class CMageGuildScreen : public CStatusbarWindow std::shared_ptr resdatabar; public: - CMageGuildScreen(CCastleInterface * owner,std::string image); + CMageGuildScreen(CCastleInterface * owner, const ImagePath & image); }; /// The blacksmith window where you can buy available in town war machine diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 0511bb649..67c0b8f58 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -118,7 +118,7 @@ void CCommanderSkillIcon::clickPressed(const Point & cursorPosition) callback(); } -static std::string skillToFile(int skill, int level, bool selected) +static ImagePath skillToFile(int skill, int level, bool selected) { // FIXME: is this a correct hadling? // level 0 = skill not present, use image with "no" suffix @@ -156,24 +156,24 @@ static std::string skillToFile(int skill, int level, bool selected) if (selected) sufix += "="; //level-up highlight - return file + sufix + ".bmp"; + return ImagePath::builtin(file + sufix + ".bmp"); } -CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, std::string backgroundPath, int yOffset) +CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset) : parent(parent) { pos.y += yOffset; OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if(!backgroundPath.empty()) { - background = std::make_shared("stackWindow/" + backgroundPath); + background = std::make_shared(backgroundPath); pos.w = background->pos.w; pos.h = background->pos.h; } } CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "spell-effects", yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/spell-effects"), yOffset) { static const Point firstPos(6, 2); // position of 1st spell box static const Point offset(54, 0); // offset of each spell box from previous @@ -205,7 +205,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT,effect))->turnsRemain; boost::replace_first(spellText, "%d", std::to_string(duration)); - spellIcons.push_back(std::make_shared("SpellInt", effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); if(++printed >= 8) // interface limit reached break; @@ -214,7 +214,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int } CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) - : CWindowSection(owner, "bonus-effects", 0) + : CWindowSection(owner, ImagePath::builtin("stackWindow/bonus-effects"), 0) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -240,7 +240,7 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li } CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): - CWindowSection(owner, "", yOffset) + CWindowSection(owner, {}, yOffset) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -262,7 +262,7 @@ CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, } CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "button-panel", yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/button-panel"), yOffset) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -277,7 +277,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) { LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr); }; - dismiss = std::make_shared(Point(5, 5),"IVIEWCR2.DEF", CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); + dismiss = std::make_shared(Point(5, 5),AnimationPath::builtin("IVIEWCR2.DEF"), CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); } if(parent->info->upgradeInfo && !parent->info->commander) @@ -314,9 +314,9 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps); } }; - auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), "stackWindow/upgradeButton", CGI->generaltexth->zelp[446], onClick); + auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); - upgradeBtn->addOverlay(std::make_shared("CPRSMALL", VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); + upgradeBtn->addOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); if(buttonsToCreate == 1) // single upgrade avaialbe upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; @@ -341,17 +341,17 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) }; std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; - parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), "stackWindow/upgradeButton", CButton::tooltipLocalized(tooltipText), onSwitch); - parent->switchButtons[buttonIndex]->addOverlay(std::make_shared("stackWindow/switchModeIcons", buttonIndex)); + parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CButton::tooltipLocalized(tooltipText), onSwitch); + parent->switchButtons[buttonIndex]->addOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); } parent->switchButtons[parent->activeTab]->disable(); } - exit = std::make_shared(Point(382, 5), "hsbtns.def", CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); + exit = std::make_shared(Point(382, 5), AnimationPath::builtin("hsbtns.def"), CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); } CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, "commander-bg", yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/commander-bg"), yOffset) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -360,7 +360,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i return Point(10 + 80 * (index%3), 20 + 80 * (index/3)); }; - auto getSkillImage = [this](int skillIndex) -> std::string + auto getSkillImage = [this](int skillIndex) { bool selected = ((parent->selectedSkill == skillIndex) && parent->info->levelupInfo ); return skillToFile(skillIndex, parent->info->commander->secondarySkills[skillIndex], selected); @@ -413,7 +413,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i if(parent->info->levelupInfo) { - abilitiesBackground = std::make_shared("stackWindow/commander-abilities.png"); + abilitiesBackground = std::make_shared(ImagePath::builtin("stackWindow/commander-abilities.png")); abilitiesBackground->moveBy(Point(0, pos.h)); size_t abilitiesCount = boost::range::count_if(parent->info->levelupInfo->skills, [](ui32 skillID) @@ -447,8 +447,8 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i abilities = std::make_shared(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount); - leftBtn = std::make_shared(Point(10, pos.h + 6), "hsbtns3.def", CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); - rightBtn = std::make_shared(Point(411, pos.h + 6), "hsbtns5.def", CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); + leftBtn = std::make_shared(Point(10, pos.h + 6), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); + rightBtn = std::make_shared(Point(411, pos.h + 6), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); if(abilitiesCount <= 6) { @@ -505,7 +505,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) dmgMultiply += parent->info->owner->getPrimSkillLevel(PrimarySkill::ATTACK); - icons = std::make_shared("stackWindow/icons", 117, 32); + icons = std::make_shared(ImagePath::builtin("stackWindow/icons"), 117, 32); const CStack * battleStack = parent->info->stack; @@ -556,7 +556,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s if(parent->info->commander) { const CCommanderInstance * commander = parent->info->commander; - expRankIcon = std::make_shared("PSKIL42", 4, 0, pos.x, pos.y); + expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); expArea = area; @@ -568,7 +568,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s } else { - expRankIcon = std::make_shared("stackWindow/levels", stack->getExpRank(), 0, pos.x, pos.y); + expRankIcon = std::make_shared(AnimationPath::builtin("stackWindow/levels"), stack->getExpRank(), 0, pos.x, pos.y); expArea = std::make_shared(Rect(pos.x, pos.y, 44, 44)); expArea->text = parent->generateStackExpDescription(); } @@ -586,14 +586,14 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); if(art) { - parent->stackArtifactIcon = std::make_shared("ARTIFACT", art->artType->getIconIndex(), 0, pos.x, pos.y); + parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); parent->stackArtifactHelp->type = art->artType->getId(); if(parent->info->owner) { parent->stackArtifactButton = std::make_shared( - Point(pos.x - 2 , pos.y + 46), "stackWindow/cancelButton", + Point(pos.x - 2 , pos.y + 46), AnimationPath::builtin("stackWindow/cancelButton"), CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() { parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); @@ -604,14 +604,14 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s } -std::string CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) +ImagePath CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) { if(showExp && showArt) - return "info-panel-2"; + return ImagePath::builtin("stackWindow/info-panel-2"); else if(showExp || showArt) - return "info-panel-1"; + return ImagePath::builtin("stackWindow/info-panel-1"); else - return "info-panel-0"; + return ImagePath::builtin("stackWindow/info-panel-0"); } void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value1, int64_t value2) @@ -882,7 +882,7 @@ void CStackWindow::setSelection(si32 newSkill, std::shared_ptrgeneraltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; }; - auto getSkillImage = [this](int skillIndex) -> std::string + auto getSkillImage = [this](int skillIndex) { bool selected = ((selectedSkill == skillIndex) && info->levelupInfo ); return skillToFile(skillIndex, info->commander->secondarySkills[skillIndex], selected); diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index 0ffe83a88..8dcf88066 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -10,6 +10,7 @@ #pragma once #include "../../lib/bonuses/Bonus.h" +#include "../../lib/filesystem/ResourcePath.h" #include "../widgets/MiscWidgets.h" #include "CWindowObject.h" @@ -48,7 +49,7 @@ class CStackWindow : public CWindowObject { std::string name; std::string description; - std::string imagePath; + ImagePath imagePath; }; class CWindowSection : public CIntObject @@ -58,7 +59,7 @@ class CStackWindow : public CWindowObject protected: CStackWindow * parent; public: - CWindowSection(CStackWindow * parent, std::string backgroundPath, int yOffset); + CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset); }; class ActiveSpellsSection : public CWindowSection @@ -138,7 +139,7 @@ class CStackWindow : public CWindowObject void addStatLabel(EStat index, int64_t value1, int64_t value2); void addStatLabel(EStat index, int64_t value); - static std::string getBackgroundName(bool showExp, bool showArt); + static ImagePath getBackgroundName(bool showExp, bool showArt); std::array statNames; std::array statFormats; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index 46bcfda40..797e18816 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -24,7 +24,7 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - stretchedBackground = std::make_shared("DIBOXBCK", Rect(0, 0, 410, 425)); + stretchedBackground = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 410, 425)); pos.w = stretchedBackground->pos.w; pos.h = stretchedBackground->pos.h; center(); @@ -36,7 +36,7 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero) addCloseCallback(std::bind(&CHeroBackpackWindow::close, this)); - quitButton = std::make_shared(Point(173, 385), "IOKAY32.def", CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN); + quitButton = std::make_shared(Point(173, 385), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN); } void CHeroBackpackWindow::showAll(Canvas &to) diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 05ca48d85..9c7d6808a 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -63,48 +63,48 @@ CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInsta OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); pos += pos_; - image = std::make_shared("PortraitsSmall", hero->portrait); + image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->portrait); pos.w = image->pos.w; pos.h = image->pos.h; } CHeroWindow::CHeroWindow(const CGHeroInstance * hero) - : CStatusbarWindow(PLAYER_COLORED, "HeroScr4") + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("HeroScr4")) { auto & heroscrn = CGI->generaltexth->heroscrn; OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); curHero = hero; - banner = std::make_shared("CREST58", LOCPLINT->playerID.getNum(), 0, 606, 8); + banner = std::make_shared(AnimationPath::builtin("CREST58"), LOCPLINT->playerID.getNum(), 0, 606, 8); name = std::make_shared(190, 38, EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); title = std::make_shared(190, 65, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - statusbar = CGStatusBar::create(7, 559, "ADROLLVR.bmp", 660); + statusbar = CGStatusBar::create(7, 559, ImagePath::builtin("ADROLLVR.bmp"), 660); - quitButton = std::make_shared(Point(609, 516), "hsbtns.def", CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); + quitButton = std::make_shared(Point(609, 516), AnimationPath::builtin("hsbtns.def"), CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); if(settings["general"]["enableUiEnhancements"].Bool()) { - questlogButton = std::make_shared(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - backpackButton = std::make_shared(Point(424, 429), "buttons/backpack", CButton::tooltipLocalized("vcmi.heroWindow.Backpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); - dismissButton = std::make_shared(Point(534, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.Backpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); + dismissButton = std::make_shared(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); } else { dismissLabel = std::make_shared(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); questlogLabel = std::make_shared(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - dismissButton = std::make_shared(Point(454, 429), "hsbtns2.def", CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); - questlogButton = std::make_shared(Point(314, 429), "hsbtns4.def", CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + dismissButton = std::make_shared(Point(454, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); } formations = std::make_shared(0); - formations->addToggle(0, std::make_shared(Point(481, 483), "hsbtns6.def", std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); - formations->addToggle(1, std::make_shared(Point(481, 519), "hsbtns7.def", std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); + formations->addToggle(0, std::make_shared(Point(481, 483), AnimationPath::builtin("hsbtns6.def"), std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); + formations->addToggle(1, std::make_shared(Point(481, 519), AnimationPath::builtin("hsbtns7.def"), std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); if(hero->commander) { - commanderButton = std::make_shared(Point(317, 18), "buttons/commander", CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); + commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("buttons/commander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); } //right list of heroes @@ -113,7 +113,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) //areas portraitArea = std::make_shared(Rect(18, 18, 58, 64)); - portraitImage = std::make_shared("PortraitsLarge", 0, 0, 19, 19); + portraitImage = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 19, 19); for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) { @@ -127,7 +127,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) primSkillValues.push_back(value); } - auto primSkills = std::make_shared("PSKIL42"); + auto primSkills = std::make_shared(AnimationPath::builtin("PSKIL42")); primSkills->preload(); primSkillImages.push_back(std::make_shared(primSkills, 0, 0, 32, 111)); primSkillImages.push_back(std::make_shared(primSkills, 1, 0, 102, 111)); @@ -136,7 +136,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) primSkillImages.push_back(std::make_shared(primSkills, 4, 0, 20, 230)); primSkillImages.push_back(std::make_shared(primSkills, 5, 0, 242, 111)); - specImage = std::make_shared("UN44", 0, 0, 18, 180); + specImage = std::make_shared(AnimationPath::builtin("UN44"), 0, 0, 18, 180); specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); specName = std::make_shared(69, 205); @@ -148,7 +148,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) expValue = std::make_shared(68, 252); manaValue = std::make_shared(211, 252); - auto secSkills = std::make_shared("SECSKILL"); + auto secSkills = std::make_shared(AnimationPath::builtin("SECSKILL")); for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); @@ -194,7 +194,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) specImage->setFrame(curHero->type->imageIndex); specName->setText(curHero->type->getSpecialtyNameTranslated()); - tacticsButton = std::make_shared(Point(539, 483), "hsbtns8.def", std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); + tacticsButton = std::make_shared(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); @@ -210,7 +210,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]); garr = std::make_shared(Point(15, 485), 8, Point(), curHero); - auto split = std::make_shared(Point(539, 519), "hsbtns9.def", CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }); + auto split = std::make_shared(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }); garr->addSplitBtn(split); } if(!arts) @@ -224,7 +224,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) listSelection.reset(); if(serial >= 0) - listSelection = std::make_shared("HPSYYY", 612, 33 + serial * 54); + listSelection = std::make_shared(ImagePath::builtin("HPSYYY"), 612, 33 + serial * 54); } //primary skills support diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index cd2c5f40f..95009f659 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -169,7 +169,7 @@ std::string InfoBoxAbstractHeroData::getNameText() return ""; } -std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) +AnimationPath InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) { //TODO: sizes switch(size) @@ -181,11 +181,11 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) case HERO_PRIMARY_SKILL: case HERO_MANA: case HERO_EXPERIENCE: - return "PSKIL32"; + return AnimationPath::builtin("PSKIL32"); case HERO_SPECIAL: - return "UN32"; + return AnimationPath::builtin("UN32"); case HERO_SECONDARY_SKILL: - return "SECSK32"; + return AnimationPath::builtin("SECSK32"); default: assert(0); } @@ -197,11 +197,11 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) case HERO_PRIMARY_SKILL: case HERO_MANA: case HERO_EXPERIENCE: - return "PSKIL42"; + return AnimationPath::builtin("PSKIL42"); case HERO_SPECIAL: - return "UN44"; + return AnimationPath::builtin("UN44"); case HERO_SECONDARY_SKILL: - return "SECSKILL"; + return AnimationPath::builtin("SECSKILL"); default: assert(0); } @@ -209,7 +209,7 @@ std::string InfoBoxAbstractHeroData::getImageName(InfoBox::InfoSize size) default: assert(0); } - return ""; + return {}; } std::string InfoBoxAbstractHeroData::getHoverText() @@ -437,7 +437,7 @@ size_t InfoBoxCustom::getImageIndex() return imageIndex; } -std::string InfoBoxCustom::getImageName(InfoBox::InfoSize size) +AnimationPath InfoBoxCustom::getImageName(InfoBox::InfoSize size) { return imageName; } @@ -457,7 +457,7 @@ void InfoBoxCustom::prepareMessage(std::string & text, std::shared_ptr("KSTATBAR", 10,pos.h - 45)); - resdatabar = std::make_shared("KRESBAR", 7, 111+footerPos, 29, 5, 76, 81); + statusbar = CGStatusBar::create(std::make_shared(ImagePath::builtin("KSTATBAR"), 10,pos.h - 45)); + resdatabar = std::make_shared(ImagePath::builtin("KRESBAR"), 7, 111+footerPos, 29, 5, 76, 81); } void CKingdomInterface::generateObjectsList(const std::vector &ownedObjects) @@ -602,27 +602,27 @@ void CKingdomInterface::generateButtons() ui32 footerPos = OVERVIEW_SIZE * 116; //Main control buttons - btnHeroes = std::make_shared(Point(748, 28+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->overview[11], CGI->generaltexth->overview[6]), + btnHeroes = std::make_shared(Point(748, 28+footerPos), AnimationPath::builtin("OVBUTN1.DEF"), CButton::tooltip(CGI->generaltexth->overview[11], CGI->generaltexth->overview[6]), std::bind(&CKingdomInterface::activateTab, this, 0), EShortcut::KINGDOM_HEROES_TAB); btnHeroes->block(true); - btnTowns = std::make_shared(Point(748, 64+footerPos), "OVBUTN6.DEF", CButton::tooltip(CGI->generaltexth->overview[12], CGI->generaltexth->overview[7]), + btnTowns = std::make_shared(Point(748, 64+footerPos), AnimationPath::builtin("OVBUTN6.DEF"), CButton::tooltip(CGI->generaltexth->overview[12], CGI->generaltexth->overview[7]), std::bind(&CKingdomInterface::activateTab, this, 1), EShortcut::KINGDOM_TOWNS_TAB); - btnExit = std::make_shared(Point(748,99+footerPos), "OVBUTN1.DEF", CButton::tooltip(CGI->generaltexth->allTexts[600]), + btnExit = std::make_shared(Point(748,99+footerPos), AnimationPath::builtin("OVBUTN1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[600]), std::bind(&CKingdomInterface::close, this), EShortcut::GLOBAL_RETURN); btnExit->setImageOrder(3, 4, 5, 6); //Object list control buttons - dwellTop = std::make_shared(Point(733, 4), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPos(0);}); + dwellTop = std::make_shared(Point(733, 4), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPos(0);}); - dwellBottom = std::make_shared(Point(733, footerPos+2), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPos(-1); }); + dwellBottom = std::make_shared(Point(733, footerPos+2), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPos(-1); }); dwellBottom->setImageOrder(2, 3, 4, 5); - dwellUp = std::make_shared(Point(733, 24), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToPrev(); }); + dwellUp = std::make_shared(Point(733, 24), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToPrev(); }); dwellUp->setImageOrder(4, 5, 6, 7); - dwellDown = std::make_shared(Point(733, footerPos-18), "OVBUTN4.DEF", CButton::tooltip(), [&](){ dwellingsList->moveToNext(); }); + dwellDown = std::make_shared(Point(733, footerPos-18), AnimationPath::builtin("OVBUTN4.DEF"), CButton::tooltip(), [&](){ dwellingsList->moveToNext(); }); dwellDown->setImageOrder(6, 7, 8, 9); } @@ -677,7 +677,7 @@ void CKingdomInterface::artifactRemoved(const ArtifactLocation& artLoc) CKingdHeroList::CKingdHeroList(size_t maxSize) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - title = std::make_shared("OVTITLE",16,0); + title = std::make_shared(ImagePath::builtin("OVTITLE"),16,0); title->colorize(LOCPLINT->playerID); heroLabel = std::make_shared(150, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[0]); skillsLabel = std::make_shared(500, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[1]); @@ -711,14 +711,14 @@ std::shared_ptr CKingdHeroList::createHeroItem(size_t index) } else { - return std::make_shared("OVSLOT", (index-2) % picCount ); + return std::make_shared(AnimationPath::builtin("OVSLOT"), (index-2) % picCount ); } } CKingdTownList::CKingdTownList(size_t maxSize) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - title = std::make_shared("OVTITLE", 16, 0); + title = std::make_shared(ImagePath::builtin("OVTITLE"), 16, 0); title->colorize(LOCPLINT->playerID); townLabel = std::make_shared(146, 10,FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[3]); garrHeroLabel = std::make_shared(375, 10, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->overview[4]); @@ -758,14 +758,14 @@ std::shared_ptr CKingdTownList::createTownItem(size_t index) if(index < townsList.size()) return std::make_shared(townsList[index]); else - return std::make_shared("OVSLOT", (index-2) % picCount ); + return std::make_shared(AnimationPath::builtin("OVSLOT"), (index-2) % picCount ); } CTownItem::CTownItem(const CGTownInstance * Town) : town(Town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 6); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 6); name = std::make_shared(74, 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); income = std::make_shared( 190, 60, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(town->dailyIncome()[EGameResID::GOLD])); @@ -777,7 +777,7 @@ CTownItem::CTownItem(const CGTownInstance * Town) size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - picture = std::make_shared("ITPT", iconIndex, 0, 5, 6); + picture = std::make_shared(AnimationPath::builtin("ITPT"), iconIndex, 0, 5, 6); openTown = std::make_shared(Rect(5, 6, 58, 64), town); for(size_t i=0; icreatures.size(); i++) @@ -819,7 +819,7 @@ public: ArtSlotsTab() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 4); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 4); pos = background->pos; for(int i=0; i<9; i++) arts.push_back(std::make_shared(Point(269+i*48, 66))); @@ -837,10 +837,10 @@ public: BackpackTab() { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared("OVSLOT", 5); + background = std::make_shared(AnimationPath::builtin("OVSLOT"), 5); pos = background->pos; - btnLeft = std::make_shared(Point(269, 66), "HSBTNS3", CButton::tooltip(), 0); - btnRight = std::make_shared(Point(675, 66), "HSBTNS5", CButton::tooltip(), 0); + btnLeft = std::make_shared(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0); + btnRight = std::make_shared(Point(675, 66), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), 0); for(int i=0; i<8; i++) arts.push_back(std::make_shared(Point(294+i*48, 66))); } @@ -905,7 +905,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) std::string hover = CGI->generaltexth->overview[13+it]; std::string overlay = CGI->generaltexth->overview[8+it]; - auto button = std::make_shared(Point(364+(int)it*112, 46), "OVBUTN3", CButton::tooltip(hover, overlay), 0); + auto button = std::make_shared(Point(364+(int)it*112, 46), AnimationPath::builtin("OVBUTN3"), CButton::tooltip(hover, overlay), 0); button->addTextOverlay(CGI->generaltexth->allTexts[stringID[it]], FONT_SMALL, Colors::YELLOW); artButtons->addToggle((int)it, button); } @@ -915,7 +915,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) garr = std::make_shared(Point(6, 78), 4, Point(), hero, nullptr, true, true); - portrait = std::make_shared("PortraitsLarge", hero->portrait, 0, 5, 6); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait, 0, 5, 6); heroArea = std::make_shared(5, 6, hero); name = std::make_shared(73, 7, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero->getNameTranslated()); diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 57d853932..0950419d9 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -103,7 +103,7 @@ public: //methods that generate values for displaying virtual std::string getValueText()=0; virtual std::string getNameText()=0; - virtual std::string getImageName(InfoBox::InfoSize size)=0; + virtual AnimationPath getImageName(InfoBox::InfoSize size)=0; virtual std::string getHoverText()=0; virtual size_t getImageIndex()=0; @@ -124,7 +124,7 @@ public: std::string getValueText() override; std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; + AnimationPath getImageName(InfoBox::InfoSize size) override; std::string getHoverText() override; size_t getImageIndex() override; @@ -166,7 +166,7 @@ class InfoBoxCustom : public IInfoBoxData public: std::string valueText; std::string nameText; - std::string imageName; + AnimationPath imageName; std::string hoverText; size_t imageIndex; @@ -174,7 +174,7 @@ public: std::string getValueText() override; std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; + AnimationPath getImageName(InfoBox::InfoSize size) override; std::string getHoverText() override; size_t getImageIndex() override; @@ -194,7 +194,7 @@ public: std::string getValueText() override; std::string getNameText() override; - std::string getImageName(InfoBox::InfoSize size) override; + AnimationPath getImageName(InfoBox::InfoSize size) override; std::string getHoverText() override; size_t getImageIndex() override; }; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index c2eaf4251..35da927e1 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -79,7 +79,7 @@ void CMessage::init() { for(int i=0; i("DIALGBOX"); + dialogBorders[i] = std::make_unique(AnimationPath::builtin("DIALGBOX")); dialogBorders[i]->preload(); for(int j=0; j < dialogBorders[i]->size(0); j++) @@ -92,7 +92,7 @@ void CMessage::init() } } - background = IImage::createFromFile("DIBOXBCK.BMP", EImageBlitMode::OPAQUE); + background = IImage::createFromFile(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); } void CMessage::dispose() diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index 4f3f6f942..ff6a98a01 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -28,7 +28,7 @@ #include "../../lib/StartInfo.h" CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) - : CWindowObject(PLAYER_COLORED | BORDERED, "PUZZLE"), + : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("PUZZLE")), grailPos(GrailPos), currentAlpha(ColorRGBA::ALPHA_OPAQUE) { @@ -36,14 +36,14 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) CCS->soundh->playSound(soundBase::OBELISK); - quitb = std::make_shared(Point(670, 538), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN); + quitb = std::make_shared(Point(670, 538), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CPuzzleWindow::close, this), EShortcut::GLOBAL_RETURN); quitb->setBorderColor(Colors::METALLIC_GOLD); mapView = std::make_shared(Point(8,9), Point(591, 544), grailPos); - logo = std::make_shared("PUZZLOGO", 607, 3); + logo = std::make_shared(ImagePath::builtin("PUZZLOGO"), 607, 3); title = std::make_shared(700, 95, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[463]); - resDataBar = std::make_shared("ARESBAR.bmp", 3, 575, 32, 2, 85, 85); + resDataBar = std::make_shared(ImagePath("ARESBAR.bmp"), 3, 575, 32, 2, 85, 85); int faction = LOCPLINT->cb->getStartInfo()->playerInfos.find(LOCPLINT->playerID)->second.castle; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 560f407ad..de1f9878e 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -49,7 +49,7 @@ void CQuestLabel::showAll(Canvas & to) CMultiLineLabel::showAll (to); } -CQuestIcon::CQuestIcon (const std::string &defname, int index, int x, int y) : +CQuestIcon::CQuestIcon (const AnimationPath &defname, int index, int x, int y) : CAnimImage(defname, index, 0, x, y) { addUsedEvents(LCLICK); @@ -87,7 +87,7 @@ void CQuestMinimap::addQuestMarks (const QuestInfo * q) onMapViewMoved(Rect(), tile.z); - auto pic = std::make_shared("VwSymbol.def", 3, offset.x, offset.y); + auto pic = std::make_shared(AnimationPath::builtin("VwSymbol.def"), 3, offset.x, offset.y); pic->moveBy (Point ( -pic->pos.w/2, -pic->pos.h/2)); pic->callback = std::bind (&CQuestMinimap::iconClicked, this); @@ -117,7 +117,7 @@ void CQuestMinimap::showAll(Canvas & to) } CQuestLog::CQuestLog (const std::vector & Quests) - : CWindowObject(PLAYER_COLORED | BORDERED, "questDialog"), + : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("questDialog")), questIndex(0), currentQuest(nullptr), hideComplete(false), @@ -128,9 +128,9 @@ CQuestLog::CQuestLog (const std::vector & Quests) minimap = std::make_shared(Rect(12, 12, 169, 169)); // TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin description = std::make_shared("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE); - ok = std::make_shared(Point(539, 398), "IOKAY.DEF", CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT); + ok = std::make_shared(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT); // Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button - hideCompleteButton = std::make_shared(Point(10, 396), "sysopchk.def", CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); + hideCompleteButton = std::make_shared(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover")); slider = std::make_shared(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, 0, Orientation::VERTICAL, CSlider::BROWN); slider->setPanningStep(32); diff --git a/client/windows/CQuestLog.h b/client/windows/CQuestLog.h index 68f880e68..3d719d2dd 100644 --- a/client/windows/CQuestLog.h +++ b/client/windows/CQuestLog.h @@ -54,7 +54,7 @@ class CQuestIcon : public CAnimImage public: std::function callback; //TODO: merge with other similar classes? - CQuestIcon(const std::string &defname, int index, int x=0, int y=0); + CQuestIcon(const AnimationPath & defname, int index, int x=0, int y=0); void clickPressed(const Point & cursorPosition) override; void showAll(Canvas & to) override; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 288a696cb..67d88c2ff 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -93,7 +93,7 @@ public: } spellsorter; CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED, "SpelBack"), + CWindowObject(PLAYER_COLORED, ImagePath::builtin("SpelBack")), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), @@ -166,23 +166,23 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m //numbers of spell pages computed - leftCorner = std::make_shared("SpelTrnL.bmp", 97, 77); - rightCorner = std::make_shared("SpelTrnR.bmp", 487, 72); + leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97, 77); + rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487, 72); - spellIcons = std::make_shared("Spells"); + spellIcons = std::make_shared(AnimationPath::builtin("Spells")); - schoolTab = std::make_shared("SpelTab", selectedTab, 0, 524, 88); - schoolPicture = std::make_shared("Schools", 0, 0, 117, 74); + schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524, 88); + schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117, 74); - schoolBorders[0] = std::make_shared("SplevA.def"); - schoolBorders[1] = std::make_shared("SplevF.def"); - schoolBorders[2] = std::make_shared("SplevW.def"); - schoolBorders[3] = std::make_shared("SplevE.def"); + schoolBorders[0] = std::make_shared(AnimationPath::builtin("SplevA.def")); + schoolBorders[1] = std::make_shared(AnimationPath::builtin("SplevF.def")); + schoolBorders[2] = std::make_shared(AnimationPath::builtin("SplevW.def")); + schoolBorders[3] = std::make_shared(AnimationPath::builtin("SplevE.def")); for(auto item : schoolBorders) item->preload(); mana = std::make_shared(435, 426, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); - statusBar = CGStatusBar::create(7, 569, "Spelroll.bmp"); + statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 4afe99a6c..74a24ced9 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -87,22 +87,22 @@ void CTradeWindow::CTradeableItem::setID(int newID) } } -std::string CTradeWindow::CTradeableItem::getFilename() +AnimationPath CTradeWindow::CTradeableItem::getFilename() { switch(type) { case RESOURCE: - return "RESOURCE"; + return AnimationPath::builtin("RESOURCE"); case PLAYER: - return "CREST58"; + return AnimationPath::builtin("CREST58"); case ARTIFACT_TYPE: case ARTIFACT_PLACEHOLDER: case ARTIFACT_INSTANCE: - return "artifact"; + return AnimationPath::builtin("artifact"); case CREATURE: - return "TWCRPORT"; + return AnimationPath::builtin("TWCRPORT"); default: - return ""; + return {}; } } @@ -317,7 +317,7 @@ void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) setID(-1); } -CTradeWindow::CTradeWindow(std::string bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode Mode): +CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode Mode): CWindowObject(PLAYER_COLORED, bgName), market(Market), hero(Hero), @@ -616,23 +616,23 @@ void CTradeWindow::artifactSelected(CHeroArtPlace *slot) selectionChanged(true); } -std::string CMarketplaceWindow::getBackgroundForMode(EMarketMode mode) +ImagePath CMarketplaceWindow::getBackgroundForMode(EMarketMode mode) { switch(mode) { case EMarketMode::RESOURCE_RESOURCE: - return "TPMRKRES.bmp"; + return ImagePath::builtin("TPMRKRES.bmp"); case EMarketMode::RESOURCE_PLAYER: - return "TPMRKPTS.bmp"; + return ImagePath::builtin("TPMRKPTS.bmp"); case EMarketMode::CREATURE_RESOURCE: - return "TPMRKCRS.bmp"; + return ImagePath::builtin("TPMRKCRS.bmp"); case EMarketMode::RESOURCE_ARTIFACT: - return "TPMRKABS.bmp"; + return ImagePath::builtin("TPMRKABS.bmp"); case EMarketMode::ARTIFACT_RESOURCE: - return "TPMRKASS.bmp"; + return ImagePath::builtin("TPMRKASS.bmp"); } assert(0); - return ""; + return {}; } CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) @@ -685,14 +685,14 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta initItems(false); initItems(true); - ok = std::make_shared(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN); - deal = std::make_shared(Point(307, 520), "TPMRKB.DEF", CGI->generaltexth->zelp[595], [&](){ makeDeal(); } ); + ok = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN); + deal = std::make_shared(Point(307, 520), AnimationPath::builtin("TPMRKB.DEF"), CGI->generaltexth->zelp[595], [&](){ makeDeal(); } ); deal->block(true); if(sliderNeeded) { slider = std::make_shared(Point(231, 490), 137, std::bind(&CMarketplaceWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(229, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[596], [&](){ setMax(); }); + max = std::make_shared(Point(229, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[596], [&](){ setMax(); }); max->block(true); } else @@ -740,15 +740,15 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta int specialOffset = mode == EMarketMode::ARTIFACT_RESOURCE ? 35 : 0; //in selling artifacts mode we need to move res-res and art-res buttons down if(printButtonFor(EMarketMode::RESOURCE_PLAYER)) - buttons.push_back(std::make_shared(Point(18, 520),"TPMRKBU1.DEF", CGI->generaltexth->zelp[612], [&](){ setMode(EMarketMode::RESOURCE_PLAYER);})); + buttons.push_back(std::make_shared(Point(18, 520),AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], [&](){ setMode(EMarketMode::RESOURCE_PLAYER);})); if(printButtonFor(EMarketMode::RESOURCE_RESOURCE)) - buttons.push_back(std::make_shared(Point(516, 450 + specialOffset),"TPMRKBU5.DEF", CGI->generaltexth->zelp[605], [&](){ setMode(EMarketMode::RESOURCE_RESOURCE);})); + buttons.push_back(std::make_shared(Point(516, 450 + specialOffset),AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], [&](){ setMode(EMarketMode::RESOURCE_RESOURCE);})); if(printButtonFor(EMarketMode::CREATURE_RESOURCE)) - buttons.push_back(std::make_shared(Point(516, 485),"TPMRKBU4.DEF", CGI->generaltexth->zelp[599], [&](){ setMode(EMarketMode::CREATURE_RESOURCE);})); + buttons.push_back(std::make_shared(Point(516, 485),AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], [&](){ setMode(EMarketMode::CREATURE_RESOURCE);})); if(printButtonFor(EMarketMode::RESOURCE_ARTIFACT)) - buttons.push_back(std::make_shared(Point(18, 450 + specialOffset),"TPMRKBU2.DEF", CGI->generaltexth->zelp[598], [&](){ setMode(EMarketMode::RESOURCE_ARTIFACT);})); + buttons.push_back(std::make_shared(Point(18, 450 + specialOffset),AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], [&](){ setMode(EMarketMode::RESOURCE_ARTIFACT);})); if(printButtonFor(EMarketMode::ARTIFACT_RESOURCE)) - buttons.push_back(std::make_shared(Point(18, 485),"TPMRKBU3.DEF", CGI->generaltexth->zelp[613], [&](){ setMode(EMarketMode::ARTIFACT_RESOURCE);})); + buttons.push_back(std::make_shared(Point(18, 485),AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], [&](){ setMode(EMarketMode::ARTIFACT_RESOURCE);})); updateTraderText(); } @@ -1076,7 +1076,7 @@ void CMarketplaceWindow::updateTraderText() } CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) - : CTradeWindow((Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, Mode) + : CTradeWindow(ImagePath::builtin(Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1093,10 +1093,10 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, new CTextBox(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW); slider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(147, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider)); + max = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider)); sacrificedUnits.resize(GameConstants::ARMY_SIZE, 0); - sacrificeAll = std::make_shared(Point(393, 520), "ALTARMY.DEF", CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this)); + sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this)); initItems(true); mimicCres(); @@ -1108,9 +1108,9 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, //%s's Creatures labels.push_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); - sacrificeAll = std::make_shared(Point(393, 520), "ALTFILL.DEF", CGI->generaltexth->zelp[571], std::bind(&CAltarWindow::SacrificeAll,this)); + sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), CGI->generaltexth->zelp[571], std::bind(&CAltarWindow::SacrificeAll,this)); sacrificeAll->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); - sacrificeBackpack = std::make_shared(Point(147, 520), "ALTEMBK.DEF", CGI->generaltexth->zelp[570], std::bind(&CAltarWindow::SacrificeBackpack,this)); + sacrificeBackpack = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), CGI->generaltexth->zelp[570], std::bind(&CAltarWindow::SacrificeBackpack,this)); sacrificeBackpack->block(hero->artifactsInBackpack.empty()); arts = std::make_shared(Point(-365, -12)); @@ -1119,7 +1119,7 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, initItems(true); initItems(false); - artIcon = std::make_shared("ARTIFACT", 0, 0, 281, 442); + artIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), 0, 0, 281, 442); artIcon->disable(); } @@ -1130,20 +1130,20 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - ok = std::make_shared(Point(516, 520), "IOK6432.DEF", CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN); + ok = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN); - deal = std::make_shared(Point(269, 520), "ALTSACR.DEF", CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this)); + deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this)); if(Mode == EMarketMode::CREATURE_EXP) { - auto changeMode = std::make_shared(Point(516, 421), "ALTART.DEF", CGI->generaltexth->zelp[580], std::bind(&CTradeWindow::setMode,this, EMarketMode::ARTIFACT_EXP)); + auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CTradeWindow::setMode,this, EMarketMode::ARTIFACT_EXP)); if(Hero->getAlignment() == ::EAlignment::EVIL) changeMode->block(true); buttons.push_back(changeMode); } else if(Mode == EMarketMode::ARTIFACT_EXP) { - auto changeMode = std::make_shared(Point(516, 421), "ALTSACC.DEF", CGI->generaltexth->zelp[572], std::bind(&CTradeWindow::setMode,this, EMarketMode::CREATURE_EXP)); + auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CTradeWindow::setMode,this, EMarketMode::CREATURE_EXP)); if(Hero->getAlignment() == ::EAlignment::GOOD) changeMode->block(true); buttons.push_back(changeMode); diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 9b154f013..bdf253b89 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -35,7 +35,7 @@ public: class CTradeableItem : public CIntObject, public std::enable_shared_from_this { std::shared_ptr image; - std::string getFilename(); + AnimationPath getFilename(); int getIndex(); public: const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact @@ -83,7 +83,7 @@ public: std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; - CTradeWindow(std::string bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); //c + CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); //c void showAll(Canvas & to) override; @@ -120,7 +120,7 @@ class CMarketplaceWindow : public CTradeWindow bool printButtonFor(EMarketMode M) const; - std::string getBackgroundForMode(EMarketMode mode); + ImagePath getBackgroundForMode(EMarketMode mode); public: int r1, r2; //suggested amounts of traded resources bool madeTransaction; //if player made at least one transaction diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index e164090f4..b9eafdcb3 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -33,7 +33,7 @@ #include -CWindowObject::CWindowObject(int options_, std::string imageName, Point centerAt): +CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point centerAt): WindowBase(0, Point()), options(options_), background(createBg(imageName, options & PLAYER_COLORED)) @@ -54,7 +54,7 @@ CWindowObject::CWindowObject(int options_, std::string imageName, Point centerAt setShadow(true); } -CWindowObject::CWindowObject(int options_, std::string imageName): +CWindowObject::CWindowObject(int options_, const ImagePath & imageName): WindowBase(0, Point()), options(options_), background(createBg(imageName, options_ & PLAYER_COLORED)) @@ -81,7 +81,7 @@ CWindowObject::~CWindowObject() CCS->curh->show(); } -std::shared_ptr CWindowObject::createBg(std::string imageName, bool playerColored) +std::shared_ptr CWindowObject::createBg(const ImagePath & imageName, bool playerColored) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); @@ -95,7 +95,7 @@ std::shared_ptr CWindowObject::createBg(std::string imageName, bool pl return image; } -void CWindowObject::setBackground(std::string filename) +void CWindowObject::setBackground(const ImagePath & filename) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); @@ -237,10 +237,10 @@ bool CWindowObject::isPopupWindow() const return options & RCLICK_POPUP; } -CStatusbarWindow::CStatusbarWindow(int options, std::string imageName, Point centerAt) : CWindowObject(options, imageName, centerAt) +CStatusbarWindow::CStatusbarWindow(int options, const ImagePath & imageName, Point centerAt) : CWindowObject(options, imageName, centerAt) { } -CStatusbarWindow::CStatusbarWindow(int options, std::string imageName) : CWindowObject(options, imageName) +CStatusbarWindow::CStatusbarWindow(int options, const ImagePath & imageName) : CWindowObject(options, imageName) { } diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index 66e7ca27a..dd49f107b 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -10,12 +10,13 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/filesystem/ResourcePath.h" class CGStatusBar; class CWindowObject : public WindowBase { - std::shared_ptr createBg(std::string imageName, bool playerColored); + std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); std::vector> shadowParts; @@ -30,7 +31,7 @@ protected: bool isPopupWindow() const override; //To display border void updateShadow(); - void setBackground(std::string filename); + void setBackground(const ImagePath & filename); public: enum EOptions { @@ -45,8 +46,8 @@ public: * imageName - name for background image, can be empty * centerAt - position of window center. Default - center of the screen */ - CWindowObject(int options, std::string imageName, Point centerAt); - CWindowObject(int options, std::string imageName = ""); + CWindowObject(int options, const ImagePath & imageName, Point centerAt); + CWindowObject(int options, const ImagePath & imageName = {}); ~CWindowObject(); void showAll(Canvas & to) override; @@ -55,8 +56,8 @@ public: class CStatusbarWindow : public CWindowObject { public: - CStatusbarWindow(int options, std::string imageName, Point centerAt); - CStatusbarWindow(int options, std::string imageName = ""); + CStatusbarWindow(int options, const ImagePath & imageName, Point centerAt); + CStatusbarWindow(int options, const ImagePath & imageName = {}); protected: std::shared_ptr statusbar; }; diff --git a/client/windows/CreaturePurchaseCard.cpp b/client/windows/CreaturePurchaseCard.cpp index 18be16e94..879f0e588 100644 --- a/client/windows/CreaturePurchaseCard.cpp +++ b/client/windows/CreaturePurchaseCard.cpp @@ -35,17 +35,17 @@ void CreaturePurchaseCard::initButtons() void CreaturePurchaseCard::initMaxButton() { - maxButton = std::make_shared(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMax,slider), EShortcut::RECRUITMENT_MAX); + maxButton = std::make_shared(Point(pos.x + 52, pos.y + 180), AnimationPath::builtin("QuickRecruitmentWindow/QuickRecruitmentAllButton.def"), CButton::tooltip(), std::bind(&CSlider::scrollToMax,slider), EShortcut::RECRUITMENT_MAX); } void CreaturePurchaseCard::initMinButton() { - minButton = std::make_shared(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMin,slider), EShortcut::RECRUITMENT_MIN); + minButton = std::make_shared(Point(pos.x, pos.y + 180), AnimationPath::builtin("QuickRecruitmentWindow/QuickRecruitmentNoneButton.def"), CButton::tooltip(), std::bind(&CSlider::scrollToMin,slider), EShortcut::RECRUITMENT_MIN); } void CreaturePurchaseCard::initCreatureSwitcherButton() { - creatureSwitcher = std::make_shared(Point(pos.x + 18, pos.y-37), "iDv6432.def", CButton::tooltip(), [&](){ switchCreatureLevel(); }); + creatureSwitcher = std::make_shared(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); }); } void CreaturePurchaseCard::switchCreatureLevel() @@ -104,7 +104,7 @@ CreaturePurchaseCard::CreaturePurchaseCard(const std::vector & creat void CreaturePurchaseCard::initView() { picture = std::make_shared(pos.x, pos.y, creatureOnTheCard); - background = std::make_shared("QuickRecruitmentWindow/CreaturePurchaseCard.png", pos.x-4, pos.y-50); + background = std::make_shared(ImagePath::builtin("QuickRecruitmentWindow/CreaturePurchaseCard.png"), pos.x-4, pos.y-50); creatureClickArea = std::make_shared(Point(pos.x, pos.y), picture, creatureOnTheCard); initAmountInfo(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 74ceb4317..1e8097c26 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -213,7 +213,7 @@ void CRecruitmentWindow::showAll(Canvas & to) } CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, int y_offset): - CStatusbarWindow(PLAYER_COLORED, "TPRCRT"), + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPRCRT")), onRecruit(Recruit), level(Level), dst(Dst), @@ -228,9 +228,9 @@ CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, c slider = std::make_shared(Point(176, 279), 135, std::bind(&CRecruitmentWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - maxButton = std::make_shared(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); - buyButton = std::make_shared(Point(212, 313), "IBY6432.DEF", CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); - cancelButton = std::make_shared(Point(290, 313), "ICN6432.DEF", CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); + maxButton = std::make_shared(Point(134, 313), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); + buyButton = std::make_shared(Point(212, 313), AnimationPath::builtin("IBY6432.DEF"), CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); + cancelButton = std::make_shared(Point(290, 313), AnimationPath::builtin("ICN6432.DEF"), CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); title = std::make_shared(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); availableValue = std::make_shared(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -318,7 +318,7 @@ void CRecruitmentWindow::sliderMoved(int to) } CSplitWindow::CSplitWindow(const CCreature * creature, std::function callback_, int leftMin_, int rightMin_, int leftAmount_, int rightAmount_) - : CWindowObject(PLAYER_COLORED, "GPUCRDIV"), + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GPUCRDIV")), callback(callback_), leftAmount(leftAmount_), rightAmount(rightAmount_), @@ -331,8 +331,8 @@ CSplitWindow::CSplitWindow(const CCreature * creature, std::function(Point(20, 263), "IOK6432", CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(214, 263), "ICN6432", CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); + ok = std::make_shared(Point(20, 263), AnimationPath::builtin("IOK6432"), CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(214, 263), AnimationPath::builtin("ICN6432"), CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); int sliderPosition = total - leftMin - rightMin; @@ -401,7 +401,7 @@ void CSplitWindow::sliderMoved(int to) } CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, std::function callback) - : CWindowObject(PLAYER_COLORED, "LVLUPBKG"), + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("LVLUPBKG")), cb(callback) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -420,8 +420,8 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); } - portrait = std::make_shared("PortraitsLarge", hero->portrait, 0, 170, 66); - ok = std::make_shared(Point(297, 413), "IOKAY", CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait, 0, 170, 66); + ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); //%s has gained a level. mainTitle = std::make_shared(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated())); @@ -434,7 +434,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); - skillIcon = std::make_shared("PSKIL42", static_cast(pskill), 0, 174, 190); + skillIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), static_cast(pskill), 0, 174, 190); skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[static_cast(pskill)] + " +1"); } @@ -450,7 +450,7 @@ CLevelWindow::~CLevelWindow() } CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) - : CStatusbarWindow(PLAYER_COLORED, "TPTAVERN"), + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), tavernObj(TavernObj) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -479,9 +479,9 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) rumor = std::make_shared(rumorText, Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - cancel = std::make_shared(Point(310, 428), "ICANCEL.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); - recruit = std::make_shared(Point(272, 355), "TPTAV01.DEF", CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); - thiefGuild = std::make_shared(Point(22, 428), "TPTAV02.DEF", CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); + cancel = std::make_shared(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); + recruit = std::make_shared(Point(272, 355), AnimationPath::builtin("TPTAV01.DEF"), CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); + thiefGuild = std::make_shared(Point(22, 428), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold { @@ -600,7 +600,7 @@ CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); - portrait = std::make_shared("portraitsLarge", h->portrait); + portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->portrait); } } @@ -618,7 +618,7 @@ static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADE static bool isQuickExchangeLayoutAvailable() { - return CResourceHandler::get()->existsResource(ResourceID(std::string("SPRITES/") + QUICK_EXCHANGE_BG, EResType::IMAGE)); + return CResourceHandler::get()->existsResource(ResourcePath(std::string("SPRITES/") + QUICK_EXCHANGE_BG, EResType::IMAGE)); } CExchangeController::CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2) @@ -828,7 +828,7 @@ void CExchangeController::moveArtifact( } CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) - : CStatusbarWindow(PLAYER_COLORED | BORDERED, isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2"), + : CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")), controller(this, hero1, hero2), moveStackLeftButtons(), moveStackRightButtons() @@ -850,10 +850,10 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, titles[0] = std::make_shared(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0])); titles[1] = std::make_shared(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1])); - auto PSKIL32 = std::make_shared("PSKIL32"); + auto PSKIL32 = std::make_shared(AnimationPath::builtin("PSKIL32")); PSKIL32->preload(); - auto SECSK32 = std::make_shared("SECSK32"); + auto SECSK32 = std::make_shared(AnimationPath::builtin("SECSK32")); for(int g = 0; g < 4; ++g) { @@ -874,7 +874,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, for(int m=0; m < hero->secSkills.size(); ++m) secSkillIcons[leftRight].push_back(std::make_shared(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88)); - specImages[leftRight] = std::make_shared("UN32", hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); + specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); expImages[leftRight] = std::make_shared(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45); expValues[leftRight] = std::make_shared(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); @@ -883,8 +883,8 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } - portraits[0] = std::make_shared("PortraitsLarge", heroInst[0]->portrait, 0, 257, 13); - portraits[1] = std::make_shared("PortraitsLarge", heroInst[1]->portrait, 0, 485, 13); + portraits[0] = std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInst[0]->portrait, 0, 257, 13); + portraits[1] = std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInst[1]->portrait, 0, 485, 13); artifs[0] = std::make_shared(Point(-334, 150)); artifs[0]->setHero(heroInst[0]); @@ -959,12 +959,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, luck[b] = std::make_shared(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true); } - quit = std::make_shared(Point(732, 567), "IOKAY.DEF", CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); if(queryID.getNum() > 0) quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); }); - questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), "hsbtns4.def", CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); - questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), "hsbtns4.def", CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); + questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); + questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); Rect barRect(5, 578, 725, 18); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), barRect, 5, 578)); @@ -973,31 +973,31 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, garr = std::make_shared(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true); auto splitButtonCallback = [&](){ garr->splitClick(); }; - garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), "TSBTNS.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); if(qeLayout) { - moveAllGarrButtonLeft = std::make_shared(Point(325, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight()); - echangeGarrButton = std::make_shared(Point(377, 118), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy()); - moveAllGarrButtonRight = std::make_shared(Point(425, 118), QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToLeft()); - moveArtifactsButtonLeft = std::make_shared(Point(325, 154), QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToRight()); - echangeArtifactsButton = std::make_shared(Point(377, 154), QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[4]), controller.onSwapArtifacts()); - moveArtifactsButtonRight = std::make_shared(Point(425, 154), QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF", CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToLeft()); + moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight()); + echangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy()); + moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToLeft()); + moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToRight()); + echangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), controller.onSwapArtifacts()); + moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToLeft()); for(int i = 0; i < GameConstants::ARMY_SIZE; i++) { moveStackLeftButtons.push_back( std::make_shared( Point(484 + 35 * i, 154), - QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF", + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveStackToLeft(SlotID(i)))); moveStackRightButtons.push_back( std::make_shared( Point(66 + 35 * i, 154), - QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF", + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveStackToRight(SlotID(i)))); } @@ -1053,11 +1053,11 @@ void CExchangeWindow::updateWidgets() } CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) - : CStatusbarWindow(PLAYER_COLORED, "TPSHIP") + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSHIP")) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - bgWater = std::make_shared("TPSHIPBK", 100, 69); + bgWater = std::make_shared(ImagePath::builtin("TPSHIPBK"), 100, 69); auto handler = CGI->objtypeh->getHandlerFor(Obj::BOAT, boatType); @@ -1067,7 +1067,7 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat if (boatConstructor) { - std::string boatFilename = boatConstructor->getBoatAnimationName(); + AnimationPath boatFilename = boatConstructor->getBoatAnimationName(); Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2); bgShip = std::make_shared(boatFilename, 0, 7, 120, 96, 0); @@ -1081,11 +1081,11 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat goldCost = std::make_shared(118, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, goldValue); woodCost = std::make_shared(212, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, woodValue); - goldPic = std::make_shared("RESOURCE",GameResID(EGameResID::GOLD), 0, 100, 244); - woodPic = std::make_shared("RESOURCE", GameResID(EGameResID::WOOD), 0, 196, 244); + goldPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 100, 244); + woodPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::WOOD), 0, 196, 244); - quit = std::make_shared(Point(224, 312), "ICANCEL", CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); - build = std::make_shared(Point(42, 312), "IBUY30", CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); + build = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30"), CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); build->addCallback(onBuy); for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) @@ -1136,7 +1136,7 @@ CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id pos.x += 45 + (id%3)*83 + id/6*83; pos.y += 109 + (id/3)*98; - icon = std::make_shared("TWCRPORT", parent->army->getCreature(SlotID(id))->getId() + 2); + icon = std::make_shared(AnimationPath::builtin("TWCRPORT"), parent->army->getCreature(SlotID(id))->getId() + 2); count = std::make_shared(28, 76,FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(size)); } @@ -1166,7 +1166,7 @@ void CTransformerWindow::updateGarrisons() } CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero) - : CStatusbarWindow(PLAYER_COLORED, "SKTRNBK"), + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), hero(_hero), market(_market) { @@ -1185,9 +1185,9 @@ CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInst } } - all = std::make_shared(Point(146, 416), "ALTARMY.DEF", CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); - convert = std::make_shared(Point(269, 416), "ALTSACR.DEF", CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(392, 416), "ICANCEL.DEF", CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); + all = std::make_shared(Point(146, 416), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); + convert = std::make_shared(Point(269, 416), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(392, 416), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); titleLeft = std::make_shared(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area @@ -1208,7 +1208,7 @@ CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int topBar = std::make_shared(parent->bars, 0, 0, -28, -22); bottomBar = std::make_shared(parent->bars, 0, 0, -28, 48); - icon = std::make_shared("SECSKILL", _ID * 3 + 3, 0); + icon = std::make_shared(AnimationPath::builtin("SECSKILL"), _ID * 3 + 3, 0); name = std::make_shared(22, -13, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(ID)->getNameTranslated()); level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); @@ -1256,7 +1256,7 @@ void CUniversityWindow::CItem::showAll(Canvas & to) } CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market) - : CStatusbarWindow(PLAYER_COLORED, "UNIVERS1"), + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), hero(_hero), market(_market) { @@ -1285,7 +1285,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket } else { - titlePic = std::make_shared("UNIVBLDG"); + titlePic = std::make_shared(ImagePath::builtin("UNIVBLDG")); } titlePic->center(Point(232 + pos.x, 76 + pos.y)); @@ -1298,7 +1298,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket for(int i=0; i(this, goods[i], 54+i*104, 234)); - cancel = std::make_shared(Point(200, 313), "IOKAY.DEF", CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } @@ -1309,7 +1309,7 @@ void CUniversityWindow::makeDeal(int skill) CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) - : CStatusbarWindow(PLAYER_COLORED, "UNIVERS2.PCX"), + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), owner(owner_) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1322,10 +1322,10 @@ CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bo clerkSpeech = std::make_shared(text, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); name = std::make_shared(230, 37, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - icon = std::make_shared("SECSKILL", SKILL*3+3, 0, 211, 51); + icon = std::make_shared(AnimationPath::builtin("SECSKILL"), SKILL*3+3, 0, 211, 51); level = std::make_shared(230, 107, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[1]); - costIcon = std::make_shared("RESOURCE", GameResID(EGameResID::GOLD), 0, 210, 210); + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 210, 210); cost = std::make_shared(230, 267, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "2000"); std::string hoverText = CGI->generaltexth->allTexts[609]; @@ -1336,10 +1336,10 @@ CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bo boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); boost::replace_first(text, "%d", "2000"); - confirm = std::make_shared(Point(148, 299), "IBY6432.DEF", CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); + confirm = std::make_shared(Point(148, 299), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); confirm->block(!available); - cancel = std::make_shared(Point(252,299), "ICANCEL.DEF", CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + cancel = std::make_shared(Point(252,299), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } @@ -1350,16 +1350,16 @@ void CUnivConfirmWindow::makeDeal(int skill) } CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) - : CWindowObject(PLAYER_COLORED, "GARRISON") + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GARRISON")) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); garr = std::make_shared(Point(92, 127), 4, Point(0,96), up, down, removableUnits); { - auto split = std::make_shared(Point(88, 314), "IDV6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } ); + auto split = std::make_shared(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } ); garr->addSplitBtn(split); } - quit = std::make_shared(Point(399, 314), "IOK6432.DEF", CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); std::string titleText; if(down->tempOwner == up->tempOwner) @@ -1381,8 +1381,8 @@ CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance } title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); - banner = std::make_shared("CREST58", up->getOwner().getNum(), 0, 28, 124); - portrait = std::make_shared("PortraitsLarge", down->portrait, 0, 29, 222); + banner = std::make_shared(AnimationPath::builtin("CREST58"), up->getOwner().getNum(), 0, 28, 124); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->portrait, 0, 29, 222); } void CGarrisonWindow::updateGarrisons() @@ -1391,7 +1391,7 @@ void CGarrisonWindow::updateGarrisons() } CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) - : CStatusbarWindow(PLAYER_COLORED, "APHLFTBK"), + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), fort(object), hero(visitor) { @@ -1403,28 +1403,28 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI for(int i=0; i < resCount; i++) { - totalIcons[i] = std::make_shared("SMALRES", i, 0, 104 + 76 * i, 237); + totalIcons[i] = std::make_shared(AnimationPath::builtin("SMALRES"), i, 0, 104 + 76 * i, 237); totalLabels[i] = std::make_shared(166 + 76 * i, 253, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); } for(int i = 0; i < slotsCount; i++) { - upgrade[i] = std::make_shared(Point(107 + i * 76, 171), "", CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); + upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath(), CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) - upgrade[i]->addImage(image); + upgrade[i]->addImage(AnimationPath::builtin(image)); for(int j : {0,1}) { - slotIcons[i][j] = std::make_shared("SMALRES", 0, 0, 104 + 76 * i, 128 + 20 * j); + slotIcons[i][j] = std::make_shared(AnimationPath::builtin("SMALRES"), 0, 0, 104 + 76 * i, 128 + 20 * j); slotLabels[i][j] = std::make_shared(168 + 76 * i, 144 + 20 * j, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); } } - upgradeAll = std::make_shared(Point(30, 231), "", CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); + upgradeAll = std::make_shared(Point(30, 231), AnimationPath(), CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) - upgradeAll->addImage(image); + upgradeAll->addImage(AnimationPath::builtin(image)); - quit = std::make_shared(Point(294, 275), "IOKAY.DEF", CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); + quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); garr = std::make_shared(Point(108, 60), 18, Point(), hero, nullptr); @@ -1591,7 +1591,7 @@ int CHillFortWindow::getState(SlotID slot) } CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): - CStatusbarWindow(PLAYER_COLORED | BORDERED, "TpRank"), + CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin("TpRank")), owner(_owner) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1599,8 +1599,8 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): SThievesGuildInfo tgi; //info to be displayed LOCPLINT->cb->getThievesGuildInfo(tgi, owner); - exitb = std::make_shared(Point(748, 556), "TPMAGE1", CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); - statusbar = CGStatusBar::create(3, 555, "TStatBar.bmp", 742); + exitb = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); + statusbar = CGStatusBar::create(3, 555, ImagePath::builtin("TStatBar.bmp"), 742); resdatabar = std::make_shared(); resdatabar->moveBy(pos.topLeft(), true); @@ -1626,7 +1626,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); } - auto PRSTRIPS = std::make_shared("PRSTRIPS"); + auto PRSTRIPS = std::make_shared(AnimationPath::builtin("PRSTRIPS")); PRSTRIPS->preload(); for(int g=1; g(283 + 66*g, 24, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[16+g])); - auto itgflags = std::make_shared("itgflags"); + auto itgflags = std::make_shared(AnimationPath::builtin("itgflags")); itgflags->preload(); //printing flags @@ -1672,10 +1672,10 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): int counter = 0; for(auto & iter : tgi.colorToBestHero) { - banners.push_back(std::make_shared(colorToBox[iter.first.getNum()], 253 + 66 * counter, 334)); + banners.push_back(std::make_shared(ImagePath::builtin(colorToBox[iter.first.getNum()]), 253 + 66 * counter, 334)); if(iter.second.portrait >= 0) { - bestHeroes.push_back(std::make_shared("PortraitsSmall", iter.second.portrait, 0, 260 + 66 * counter, 360)); + bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.portrait, 0, 260 + 66 * counter, 360)); //TODO: r-click info: // - r-click on hero // - r-click on primary skill label @@ -1699,7 +1699,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): for(auto & it : tgi.bestCreature) { if(it.second >= 0) - bestCreatures.push_back(std::make_shared("TWCRPORT", it.second+2, 0, 255 + 66 * counter, 479)); + bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); counter++; } @@ -1729,7 +1729,7 @@ CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::st index(_id) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - border = std::make_shared("TPGATES"); + border = std::make_shared(ImagePath::builtin("TPGATES")); pos = border->pos; setRedrawParent(true); @@ -1759,7 +1759,7 @@ void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) } CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, "TPGATE"), + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), selected(initialSelection) { @@ -1775,7 +1775,7 @@ CObjectListWindow::CObjectListWindow(const std::vector & _items, std::share } CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, "TPGATE"), + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), onSelect(Callback), selected(initialSelection) { @@ -1794,7 +1794,7 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); - exit = std::make_shared( Point(228, 402), "ICANCEL.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); + exit = std::make_shared( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); if(titleWidget) { @@ -1807,7 +1807,7 @@ void CObjectListWindow::init(std::shared_ptr titleWidget_, std::stri Point(14, 151), Point(0, 25), 9, items.size(), 0, 1, Rect(262, -32, 256, 256) ); list->setRedrawParent(true); - ok = std::make_shared(Point(15, 402), "IOKAY.DEF", CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); + ok = std::make_shared(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); ok->block(!list->size()); } diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 4b9a541bb..d9d09e27a 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -68,7 +68,7 @@ void CSelWindow::selectionChange(unsigned to) redraw(); } -CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID) +CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); ID = askID; @@ -199,9 +199,9 @@ void CInfoWindow::showInfoDialog(const std::string &text, const TCompsInfo & com void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & components, const CFunctionList &onYes, const CFunctionList &onNo, PlayerColor player) { assert(!LOCPLINT || LOCPLINT->showingDialog->get()); - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); - pom.push_back(std::pair >("ICANCEL.DEF",0)); + std::vector > > pom; + pom.push_back( { AnimationPath::builtin("IOKAY.DEF"), 0 }); + pom.push_back( { AnimationPath::builtin("ICANCEL.DEF"), 0 }); std::shared_ptr temp = std::make_shared(text, player, components, pom); temp->buttons[0]->addCallback( onYes ); @@ -212,8 +212,8 @@ void CInfoWindow::showYesNoDialog(const std::string & text, const TCompsInfo & c std::shared_ptr CInfoWindow::create(const std::string &text, PlayerColor playerID, const TCompsInfo & components) { - std::vector > > pom; - pom.push_back(std::pair >("IOKAY.DEF",0)); + std::vector > > pom; + pom.push_back({AnimationPath::builtin("IOKAY.DEF"), 0}); return std::make_shared(text, playerID, components, pom); } @@ -372,7 +372,7 @@ Point CInfoBoxPopup::toScreen(Point p) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "TOWNQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(town, iah, LOCPLINT->localState->getCurrentTown()); //todo: should this be nearest hero? @@ -382,7 +382,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "HEROQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("HEROQVBK"), toScreen(position)) { InfoAboutHero iah; LOCPLINT->cb->getHeroInfo(hero, iah, LOCPLINT->localState->getCurrentHero());//todo: should this be nearest hero? @@ -392,7 +392,7 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero) } CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr) - : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, "TOWNQVBK", toScreen(position)) + : CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position)) { InfoAboutTown iah; LOCPLINT->cb->getTownInfo(garr, iah); diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index e7911884a..707bc0505 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -48,7 +48,7 @@ public: class CInfoWindow : public CSimpleWindow { public: - using TButtonsInfo = std::vector>>; + using TButtonsInfo = std::vector>>; using TCompsInfo = std::vector>; QueryID ID; //for identification std::shared_ptr text; @@ -128,7 +128,7 @@ class CSelWindow : public CInfoWindow public: void selectionChange(unsigned to); void madeChoice(); //looks for selected component and calls callback - CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); + CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); //notification - this class inherits important destructor from CInfoWindow }; diff --git a/client/windows/QuickRecruitmentWindow.cpp b/client/windows/QuickRecruitmentWindow.cpp index f2c718018..ae976282b 100644 --- a/client/windows/QuickRecruitmentWindow.cpp +++ b/client/windows/QuickRecruitmentWindow.cpp @@ -31,19 +31,19 @@ void QuickRecruitmentWindow::setButtons() void QuickRecruitmentWindow::setCancelButton() { - cancelButton = std::make_shared(Point((pos.w / 2) + 48, 418), "ICN6432.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + cancelButton = std::make_shared(Point((pos.w / 2) + 48, 418), AnimationPath::builtin("ICN6432.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); cancelButton->setImageOrder(0, 1, 2, 3); } void QuickRecruitmentWindow::setBuyButton() { - buyButton = std::make_shared(Point((pos.w / 2) - 32, 418), "IBY6432.DEF", CButton::tooltip(), [&](){ purchaseUnits(); }, EShortcut::GLOBAL_ACCEPT); + buyButton = std::make_shared(Point((pos.w / 2) - 32, 418), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(), [&](){ purchaseUnits(); }, EShortcut::GLOBAL_ACCEPT); buyButton->setImageOrder(0, 1, 2, 3); } void QuickRecruitmentWindow::setMaxButton() { - maxButton = std::make_shared(Point((pos.w/2)-112, 418), "IRCBTNS.DEF", CButton::tooltip(), [&](){ maxAllCards(cards); }, EShortcut::RECRUITMENT_MAX); + maxButton = std::make_shared(Point((pos.w/2)-112, 418), AnimationPath::builtin("IRCBTNS.DEF"), CButton::tooltip(), [&](){ maxAllCards(cards); }, EShortcut::RECRUITMENT_MAX); maxButton->setImageOrder(0, 1, 2, 3); } @@ -74,8 +74,8 @@ void QuickRecruitmentWindow::initWindow(Rect startupPosition) pos.w += 108 * (creaturesAmount - 3); pos.x -= 55 * (creaturesAmount - 3); } - backgroundTexture = std::make_shared("DIBOXBCK.pcx", Rect(0, 0, pos.w, pos.h)); - costBackground = std::make_shared("QuickRecruitmentWindow/costBackground.png", pos.w/2-113, 335); + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK.pcx"), Rect(0, 0, pos.w, pos.h)); + costBackground = std::make_shared(ImagePath::builtin("QuickRecruitmentWindow/costBackground.png"), pos.w/2-113, 335); } void QuickRecruitmentWindow::maxAllCards(std::vector > cards) diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index 499858037..d48b8dcc1 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -11,7 +11,7 @@ #include "AdventureOptionsTab.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" @@ -44,7 +44,7 @@ AdventureOptionsTab::AdventureOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourceID("config/widgets/settings/adventureOptionsTab.json")); + const JsonNode config(ResourcePath("config/widgets/settings/adventureOptionsTab.json")); addCallback("playerHeroSpeedChanged", [this](int value) { auto targetLabel = widget("heroSpeedValueLabel"); diff --git a/client/windows/settings/BattleOptionsTab.cpp b/client/windows/settings/BattleOptionsTab.cpp index 1e657d61c..0b95198bd 100644 --- a/client/windows/settings/BattleOptionsTab.cpp +++ b/client/windows/settings/BattleOptionsTab.cpp @@ -13,7 +13,7 @@ #include "../../battle/BattleInterface.h" #include "../../gui/CGuiHandler.h" #include "../../../lib/CConfigHandler.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../../lib/CGeneralTextHandler.h" #include "../../widgets/Buttons.h" #include "../../widgets/TextControls.h" @@ -23,7 +23,7 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; setRedrawParent(true); - const JsonNode config(ResourceID("config/widgets/settings/battleOptionsTab.json")); + const JsonNode config(ResourcePath("config/widgets/settings/battleOptionsTab.json")); addCallback("viewGridChanged", [this, owner](bool value) { viewGridChangedCallback(value, owner); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index ae747f90e..6ed20cabf 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -26,7 +26,7 @@ #include "../../widgets/TextControls.h" #include "../../../lib/CGeneralTextHandler.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" static void setIntSetting(std::string group, std::string field, int value) { @@ -105,7 +105,7 @@ GeneralOptionsTab::GeneralOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourceID("config/widgets/settings/generalOptionsTab.json")); + const JsonNode config(ResourcePath("config/widgets/settings/generalOptionsTab.json")); addCallback("spellbookAnimationChanged", [](bool value) { setBoolSetting("video", "spellbookAnimation", value); diff --git a/client/windows/settings/OtherOptionsTab.cpp b/client/windows/settings/OtherOptionsTab.cpp index 90e288e07..aa5e5325a 100644 --- a/client/windows/settings/OtherOptionsTab.cpp +++ b/client/windows/settings/OtherOptionsTab.cpp @@ -11,7 +11,7 @@ #include "OtherOptionsTab.h" -#include "../../../lib/filesystem/ResourceID.h" +#include "../../../lib/filesystem/ResourcePath.h" #include "../../gui/CGuiHandler.h" #include "../../widgets/Buttons.h" #include "CConfigHandler.h" @@ -26,7 +26,7 @@ OtherOptionsTab::OtherOptionsTab() : InterfaceObjectConfigurable() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourceID("config/widgets/settings/otherOptionsTab.json")); + const JsonNode config(ResourcePath("config/widgets/settings/otherOptionsTab.json")); addCallback("availableCreaturesAsDwellingLabelChanged", [](bool value) { return setBoolSetting("gameTweaks", "availableCreaturesAsDwellingLabel", value); diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index c05592e7d..2dfa11577 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -21,7 +21,7 @@ #include "CGeneralTextHandler.h" #include "CPlayerInterface.h" #include "CServerHandler.h" -#include "filesystem/ResourceID.h" +#include "filesystem/ResourcePath.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "render/Canvas.h" @@ -35,7 +35,7 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourceID("config/widgets/settings/settingsMainContainer.json")); + const JsonNode config(ResourcePath("config/widgets/settings/settingsMainContainer.json")); addCallback("activateSettingsTab", [this](int tabId) { openTab(tabId); }); addCallback("loadGame", [this](int) { loadGameButtonCallback(); }); addCallback("saveGame", [this](int) { saveGameButtonCallback(); }); diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index ab2fbea2c..785979d74 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -64,7 +64,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/FileInfo.cpp ${MAIN_LIB_DIR}/filesystem/Filesystem.cpp ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.cpp - ${MAIN_LIB_DIR}/filesystem/ResourceID.cpp + ${MAIN_LIB_DIR}/filesystem/ResourcePath.cpp ${MAIN_LIB_DIR}/gameState/CGameState.cpp ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.cpp @@ -401,7 +401,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/filesystem/Filesystem.h ${MAIN_LIB_DIR}/filesystem/ISimpleResourceLoader.h ${MAIN_LIB_DIR}/filesystem/MinizipExtensions.h - ${MAIN_LIB_DIR}/filesystem/ResourceID.h + ${MAIN_LIB_DIR}/filesystem/ResourcePath.h ${MAIN_LIB_DIR}/gameState/CGameState.h ${MAIN_LIB_DIR}/gameState/CGameStateCampaign.h diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index 8f1d43ecd..9748b3f27 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -230,8 +230,8 @@ bool FirstLaunchView::heroesDataDetect() CResourceHandler::load("config/filesystem.json"); // use file from lod archive to check presence of H3 data. Very rough estimate, but will work in majority of cases - bool heroesDataFoundROE = CResourceHandler::get()->existsResource(ResourceID("DATA/GENRLTXT.TXT")); - bool heroesDataFoundSOD = CResourceHandler::get()->existsResource(ResourceID("DATA/TENTCOLR.TXT")); + bool heroesDataFoundROE = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT")); + bool heroesDataFoundSOD = CResourceHandler::get()->existsResource(ResourcePath("DATA/TENTCOLR.TXT")); return heroesDataFoundROE && heroesDataFoundSOD; } diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 99db438c9..6bb00171a 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -110,7 +110,7 @@ MainWindow::MainWindow(QWidget * parent) computeSidePanelSizes(); - bool h3DataFound = CResourceHandler::get()->existsResource(ResourceID("DATA/GENRLTXT.TXT")); + bool h3DataFound = CResourceHandler::get()->existsResource(ResourcePath("DATA/GENRLTXT.TXT")); if (h3DataFound && setupCompleted) ui->tabListWidget->setCurrentIndex(TabRows::MODS); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 27824b826..dc7144a14 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -84,7 +84,7 @@ void CModManager::loadMods() for(auto modname : installedMods) { - ResourceID resID(CModInfo::getModFile(modname)); + ResourcePath resID(CModInfo::getModFile(modname)); if(CResourceHandler::get()->existsResource(resID)) { boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); @@ -295,7 +295,7 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) bool CModManager::doUninstallMod(QString modname) { - ResourceID resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); + ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY); // Get location of the mod, in case-insensitive way QString modDir = pathToQString(*CResourceHandler::get()->getResourceName(resID)); diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index 2f6df594b..9c559ffe7 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -21,7 +21,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto * info = new BattleFieldInfo(BattleField(index), identifier); - info->graphics = json["graphics"].String(); + info->graphics = ImagePath::fromJson(json["graphics"]); info->icon = json["icon"].String(); info->name = json["name"].String(); for(const auto & b : json["bonuses"].Vector()) diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index a3e550485..9bacf79a2 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -15,6 +15,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "battle/BattleHex.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,7 +25,7 @@ public: BattleField battlefield; std::vector> bonuses; bool isSpecial; - std::string graphics; + ImagePath graphics; std::string name; std::string identifier; std::string icon; diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 60f3211e3..8de2163d9 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -85,7 +85,7 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonu return text; } -std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const +ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const { std::string fileName; bool fullPath = false; @@ -191,12 +191,12 @@ std::string CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bo if(!fileName.empty() && !fullPath) fileName = "zvs/Lib1.res/" + fileName; - return fileName; + return ImagePath::builtinTODO(fileName); } void CBonusTypeHandler::load() { - const JsonNode gameConf(ResourceID("config/gameConfig.json")); + const JsonNode gameConf(ResourcePath("config/gameConfig.json")); const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); load(config); } diff --git a/lib/CBonusTypeHandler.h b/lib/CBonusTypeHandler.h index d747b8d98..01d36809f 100644 --- a/lib/CBonusTypeHandler.h +++ b/lib/CBonusTypeHandler.h @@ -51,7 +51,7 @@ public: virtual ~CBonusTypeHandler(); std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; - std::string bonusToGraphics(const std::shared_ptr & bonus) const override; + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const override; template void serialize(Handler & h, const int version) { diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 091135723..6aa7321e8 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -60,7 +60,7 @@ void SettingsStorage::init() JsonUtils::assembleFromFiles(confName).swap(config); // Probably new install. Create config file to save settings to - if (!CResourceHandler::get("local")->existsResource(ResourceID(confName))) + if (!CResourceHandler::get("local")->existsResource(ResourcePath(confName))) CResourceHandler::get("local")->createResource(confName); JsonUtils::maximize(config, "vcmi:settings"); @@ -76,7 +76,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath savedConf.Struct().erase("session"); JsonUtils::minimize(savedConf, "vcmi:settings"); - std::fstream file(CResourceHandler::get()->getResourceName(ResourceID("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 8a49bb8b6..872804635 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -415,7 +415,7 @@ const CCreature * CCreatureHandler::getCreature(const std::string & scope, const void CCreatureHandler::loadCommanders() { - ResourceID configResource("config/commanders.json"); + ResourcePath configResource("config/commanders.json"); std::string modSource = VLC->modh->findResourceOrigin(configResource); JsonNode data(configResource); @@ -884,7 +884,7 @@ void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graph void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const { - creature->animDefName = config["graphics"]["animation"].String(); + creature->animDefName = AnimationPath::fromJson(config["graphics"]["animation"]); //FIXME: MOD COMPATIBILITY if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) @@ -933,7 +933,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c }); } - creature->animation.projectileImageName = config["graphics"]["missile"]["projectile"].String(); + creature->animation.projectileImageName = AnimationPath::fromJson(config["graphics"]["missile"]["projectile"]); for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) { diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 0ef2a26df..f1dee53ca 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -18,6 +18,7 @@ #include "IHandlerBase.h" #include "CRandomGenerator.h" #include "Color.h" +#include "filesystem/ResourcePath.h" #include #include @@ -57,7 +58,7 @@ public: std::set upgrades; // IDs of creatures to which this creature can be upgraded - std::string animDefName; // creature animation used during battles + AnimationPath animDefName; // creature animation used during battles si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now. /// names of files with appropriate icons. Used only during loading @@ -97,7 +98,7 @@ public: std::vector missleFrameAngles; int troopCountLocationOffset, attackClimaxFrame; - std::string projectileImageName; + AnimationPath projectileImageName; std::vector projectileRay; //bool projectileSpin; //if true, appropriate projectile is spinning during flight diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 6da4ad768..ec09548c0 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -777,7 +777,7 @@ std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, b return VLC->getBth()->bonusToString(bonus, this, description); } -std::string CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const +ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const { return VLC->getBth()->bonusToGraphics(bonus); } diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index c103a6af7..7c81c544f 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -96,7 +96,7 @@ public: //overrides CBonusSystemNode std::string bonusToString(const std::shared_ptr& bonus, bool description) const override; // how would bonus description look for this particular type of node - std::string bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others //IConstBonusProvider const IBonusBearer* getBonusBearer() const override; diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index a8c66a578..624a0b08b 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -48,7 +48,7 @@ void CGeneralTextHandler::detectInstallParameters() "ukrainian" } }; - if(!CResourceHandler::get("core")->existsResource(ResourceID("DATA/GENRLTXT.TXT", EResType::TEXT))) + if(!CResourceHandler::get("core")->existsResource(ResourcePath("DATA/GENRLTXT.TXT", EResType::TEXT))) { Settings language = settings.write["session"]["language"]; language->String() = "english"; @@ -64,7 +64,7 @@ void CGeneralTextHandler::detectInstallParameters() // load file that will be used for footprint generation // this is one of the most text-heavy files in game and consists solely from translated texts - auto resource = CResourceHandler::get("core")->load(ResourceID("DATA/GENRLTXT.TXT", EResType::TEXT)); + auto resource = CResourceHandler::get("core")->load(ResourcePath("DATA/GENRLTXT.TXT", EResType::TEXT)); std::array charCount{}; std::array footprint{}; @@ -121,7 +121,7 @@ protected: CLegacyConfigParser::CLegacyConfigParser(std::string URI) { - ResourceID resource(URI, EResType::TEXT); + ResourcePath resource(URI, EResType::TEXT); auto input = CResourceHandler::get()->load(resource); std::string modName = VLC->modh->findResourceOrigin(resource); std::string language = VLC->modh->getModLanguage(modName); @@ -430,7 +430,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; - if (CResourceHandler::get()->existsResource(ResourceID(QE_MOD_COMMANDS, EResType::TEXT))) + if (CResourceHandler::get()->existsResource(ResourcePath(QE_MOD_COMMANDS, EResType::TEXT))) readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); { @@ -575,7 +575,7 @@ CGeneralTextHandler::CGeneralTextHandler(): } if (VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) { - if(CResourceHandler::get()->existsResource(ResourceID("DATA/ZNPC00.TXT", EResType::TEXT))) + if(CResourceHandler::get()->existsResource(ResourcePath("DATA/ZNPC00.TXT", EResType::TEXT))) readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); } } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 4b9923b36..280d09105 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -243,8 +243,8 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js heroClass->id = HeroClassID(index); heroClass->identifier = identifier; heroClass->modScope = scope; - heroClass->imageBattleFemale = node["animation"]["battle"]["female"].String(); - heroClass->imageBattleMale = node["animation"]["battle"]["male"].String(); + heroClass->imageBattleFemale = AnimationPath::fromJson(node["animation"]["battle"]["female"]); + heroClass->imageBattleMale = AnimationPath::fromJson(node["animation"]["battle"]["male"]); //MODS COMPATIBILITY FOR 0.96 heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); heroClass->imageMapMale = node["animation"]["map"]["male"].String(); @@ -438,7 +438,7 @@ CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & n hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); hero->portraitSmall = node["images"]["small"].String(); hero->portraitLarge = node["images"]["large"].String(); - hero->battleImage = node["battleImage"].String(); + hero->battleImage = AnimationPath::fromJson(node["battleImage"]); loadHeroArmy(hero, node); loadHeroSkills(hero, node); diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 2e9c5df84..86f328f8c 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -14,11 +14,12 @@ #include #include -#include "../lib/ConstTransitivePtr.h" +#include "ConstTransitivePtr.h" #include "GameConstants.h" #include "bonuses/Bonus.h" #include "bonuses/BonusList.h" #include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -78,7 +79,7 @@ public: std::string iconSpecLarge; std::string portraitSmall; std::string portraitLarge; - std::string battleImage; + AnimationPath battleImage; CHero(); virtual ~CHero(); @@ -160,8 +161,8 @@ public: std::map selectionProbability; //probability of selection in towns - std::string imageBattleMale; - std::string imageBattleFemale; + AnimationPath imageBattleMale; + AnimationPath imageBattleFemale; std::string imageMapMale; std::string imageMapFemale; diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index cb19a3983..3d4e1388c 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -752,9 +752,9 @@ void CTownHandler::loadStructure(CTown &town, const std::string & stringID, cons ret->pos.z = static_cast(source["z"].Float()); ret->hiddenUpgrade = source["hidden"].Bool(); - ret->defName = source["animation"].String(); - ret->borderName = source["border"].String(); - ret->areaName = source["area"].String(); + ret->defName = AnimationPath::fromJson(source["animation"]); + ret->borderName = ImagePath::fromJson(source["border"]); + ret->areaName = ImagePath::fromJson(source["area"]); town.clientInfo.structures.emplace_back(ret); } @@ -877,22 +877,14 @@ void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const readIcon(source["icons"]["fort"]["normal"], info.iconSmall[1][0], info.iconLarge[1][0]); readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]); - info.hallBackground = source["hallBackground"].String(); + info.hallBackground = ImagePath::fromJson(source["hallBackground"]); info.musicTheme = source["musicTheme"].String(); - info.townBackground = source["townBackground"].String(); - info.guildWindow = source["guildWindow"].String(); - info.buildingsIcons = source["buildingsIcons"].String(); + info.townBackground = ImagePath::fromJson(source["townBackground"]); + info.guildWindow = ImagePath::fromJson(source["guildWindow"]); + info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); - //left for back compatibility - will be removed later - if(!source["guildBackground"].String().empty()) - info.guildBackground = source["guildBackground"].String(); - else - info.guildBackground = "TPMAGE.bmp"; - if(!source["tavernVideo"].String().empty()) - info.tavernVideo = source["tavernVideo"].String(); - else - info.tavernVideo = "TAVERN.BIK"; - //end of legacy assignment + info.guildBackground = ImagePath::fromJson(source["guildBackground"]); + info.tavernVideo = source["tavernVideo"].String(); loadTownHall(town, source["hallSlots"]); loadStructures(town, source["structures"]); @@ -1012,7 +1004,7 @@ void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const std::ostringstream suffix; suffix << std::setfill('0') << std::setw(2) << index; - spi.filename = prefix + suffix.str(); + spi.filename = ImagePath::builtinTODO(prefix + suffix.str()); faction.puzzleMap.push_back(spi); } @@ -1031,8 +1023,8 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); - faction->creatureBg120 = source["creatureBackground"]["120px"].String(); - faction->creatureBg130 = source["creatureBackground"]["130px"].String(); + faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]); + faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]); faction->boatType = BoatId::CASTLE; //Do not crash if (!source["boat"].isNull()) @@ -1156,7 +1148,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod void CTownHandler::loadRandomFaction() { - static const ResourceID randomFactionPath("config/factions/random.json"); + static const ResourcePath randomFactionPath("config/factions/random.json"); JsonNode randomFactionJson(randomFactionPath); randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 0c15ef396..4c8da2082 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -23,6 +23,7 @@ #include "bonuses/BonusList.h" #include "Point.h" #include "rewardable/Info.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -154,7 +155,10 @@ struct DLL_LINKAGE CStructure CBuilding * buildable; // building that will be used to determine built building and visible cost. Usually same as "building" int3 pos; - std::string defName, borderName, areaName, identifier; + AnimationPath defName; + ImagePath borderName; + ImagePath areaName; + std::string identifier; bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) template void serialize(Handler &h, const int version) @@ -175,7 +179,7 @@ struct DLL_LINKAGE SPuzzleInfo ui16 number; //type of puzzle si16 x, y; //position ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) - std::string filename; //file with graphic of this puzzle + ImagePath filename; //file with graphic of this puzzle template void serialize(Handler &h, const int version) { @@ -209,11 +213,10 @@ public: /// and for placing heroes directly on boat (in map editor, water prisons & taverns) BoatId boatType = BoatId::CASTLE; - CTown * town = nullptr; //NOTE: can be null - std::string creatureBg120; - std::string creatureBg130; + ImagePath creatureBg120; + ImagePath creatureBg130; std::vector puzzleMap; @@ -303,11 +306,11 @@ public: std::string iconLarge[2][2]; std::string tavernVideo; std::string musicTheme; - std::string townBackground; - std::string guildBackground; - std::string guildWindow; - std::string buildingsIcons; - std::string hallBackground; + ImagePath townBackground; + ImagePath guildBackground; + ImagePath guildWindow; + AnimationPath buildingsIcons; + ImagePath hallBackground; /// vector[row][column] = list of buildings in this slot std::vector< std::vector< std::vector > > hallSlots; diff --git a/lib/IBonusTypeHandler.h b/lib/IBonusTypeHandler.h index 60914bace..b38c56f3d 100644 --- a/lib/IBonusTypeHandler.h +++ b/lib/IBonusTypeHandler.h @@ -9,6 +9,8 @@ */ #pragma once +#include "filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN class IBonusBearer; @@ -22,7 +24,7 @@ public: virtual ~IBonusTypeHandler() = default; virtual std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const = 0; - virtual std::string bonusToGraphics(const std::shared_ptr & bonus) const = 0; + virtual ImagePath bonusToGraphics(const std::shared_ptr & bonus) const = 0; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index c056f434b..ce1b2a96d 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -1005,7 +1005,7 @@ namespace namespace Formats { - bool testFilePresence(const std::string & scope, const ResourceID & resource) + bool testFilePresence(const std::string & scope, const ResourcePath & resource) { std::set allowedScopes; if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies @@ -1030,7 +1030,7 @@ namespace } #define TEST_FILE(scope, prefix, file, type) \ - if (testFilePresence(scope, ResourceID(prefix + file, type))) \ + if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ return "" std::string testAnimation(const std::string & path, const std::string & scope) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 842b3fdf2..66a1596ac 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -80,7 +80,7 @@ JsonNode::JsonNode(const char *data, size_t datasize): *this = parser.parse(""); } -JsonNode::JsonNode(ResourceID && fileURI): +JsonNode::JsonNode(ResourcePath && fileURI): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -89,7 +89,7 @@ JsonNode::JsonNode(ResourceID && fileURI): *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const ResourceID & fileURI): +JsonNode::JsonNode(const ResourcePath & fileURI): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -98,7 +98,7 @@ JsonNode::JsonNode(const ResourceID & fileURI): *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const std::string & idx, const ResourceID & fileURI): +JsonNode::JsonNode(const std::string & idx, const ResourcePath & fileURI): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); @@ -107,7 +107,7 @@ type(JsonType::DATA_NULL) *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(ResourceID && fileURI, bool &isValidSyntax): +JsonNode::JsonNode(ResourcePath && fileURI, bool &isValidSyntax): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -1255,9 +1255,9 @@ const JsonNode & getSchemaByName(const std::string & name) std::string filename = "config/schemas/" + name; - if (CResourceHandler::get()->existsResource(ResourceID(filename))) + if (CResourceHandler::get()->existsResource(ResourcePath(filename))) { - loadedSchemas[name] = JsonNode(ResourceID(filename)); + loadedSchemas[name] = JsonNode(ResourcePath(filename)); return loadedSchemas[name]; } @@ -1447,7 +1447,7 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo for(const std::string & file : files) { bool isValidFile = false; - JsonNode section(ResourceID(file, EResType::TEXT), isValidFile); + JsonNode section(ResourcePath(file, EResType::TEXT), isValidFile); merge(result, section); isValid |= isValidFile; } @@ -1457,7 +1457,7 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo JsonNode JsonUtils::assembleFromFiles(const std::string & filename) { JsonNode result; - ResourceID resID(filename, EResType::TEXT); + ResourcePath resID(filename, EResType::TEXT); for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) { diff --git a/lib/JsonNode.h b/lib/JsonNode.h index c1d39991e..bdae6ea58 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -18,7 +18,7 @@ using JsonVector = std::vector; struct Bonus; class CSelector; -class ResourceID; +class ResourcePath; class CAddInfo; class ILimiter; @@ -61,10 +61,10 @@ public: //Create tree from Json-formatted input explicit JsonNode(const char * data, size_t datasize); //Create tree from JSON file - explicit JsonNode(ResourceID && fileURI); - explicit JsonNode(const ResourceID & fileURI); - explicit JsonNode(const std::string& idx, const ResourceID & fileURI); - explicit JsonNode(ResourceID && fileURI, bool & isValidSyntax); + explicit JsonNode(ResourcePath && fileURI); + explicit JsonNode(const ResourcePath & fileURI); + explicit JsonNode(const std::string& idx, const ResourcePath & fileURI); + explicit JsonNode(ResourcePath && fileURI, bool & isValidSyntax); //Copy c-tor JsonNode(const JsonNode ©); diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index fde260dd6..fbfc7efe2 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -90,7 +90,7 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js auto * info = new ObstacleInfo(Obstacle(index), identifier); - info->animation = json["animation"].String(); + info->animation = AnimationPath::fromJson(json["animation"]); info->width = json["width"].Integer(); info->height = json["height"].Integer(); for(const auto & t : json["allowedTerrains"].Vector()) diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 09bd52fd2..d5b2ab3f0 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -14,6 +14,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "battle/BattleHex.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -31,7 +32,9 @@ public: Obstacle obstacle; si32 iconIndex; std::string identifier; - std::string appearSound, appearAnimation, animation; + std::string appearSound; + AnimationPath appearAnimation; + AnimationPath animation; std::vector allowedTerrains; std::vector allowedSpecialBfields; diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index 1f4022391..3cc04eb37 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -36,7 +36,7 @@ RiverType * RiverTypeHandler::loadFromJson( info->id = RiverId(index); info->identifier = identifier; info->modScope = scope; - info->tilesFilename = json["tilesFilename"].String(); + info->tilesFilename = AnimationPath::fromJson(json["tilesFilename"]); info->shortIdentifier = json["shortIdentifier"].String(); info->deltaName = json["delta"].String(); diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index 22fad8660..dddd81d41 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -14,6 +14,7 @@ #include #include "GameConstants.h" #include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -49,7 +50,7 @@ public: std::string getNameTextID() const override; std::string getNameTranslated() const override; - std::string tilesFilename; + AnimationPath tilesFilename; std::string shortIdentifier; std::string deltaName; diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index ba8620f39..458d1588d 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -36,7 +36,7 @@ RoadType * RoadTypeHandler::loadFromJson( info->id = RoadId(index); info->identifier = identifier; info->modScope = scope; - info->tilesFilename = json["tilesFilename"].String(); + info->tilesFilename = AnimationPath::fromJson(json["tilesFilename"]); info->shortIdentifier = json["shortIdentifier"].String(); info->movementCost = json["moveCost"].Integer(); diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index 156d7635d..e9e1d9df6 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -14,6 +14,7 @@ #include #include "GameConstants.h" #include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,7 +36,7 @@ public: std::string getNameTextID() const override; std::string getNameTranslated() const override; - std::string tilesFilename; + AnimationPath tilesFilename; std::string shortIdentifier; ui8 movementCost; diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index c9528b109..d672c74dd 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -90,7 +90,7 @@ void ScriptImpl::serializeJson(vstd::CLoggerBase * logger, JsonSerializeFormat & { resolveHost(); - ResourceID sourcePathId("SCRIPTS/" + sourcePath); + ResourcePath sourcePathId("SCRIPTS/" + sourcePath); auto rawData = CResourceHandler::get()->load(sourcePathId)->readAll(); @@ -115,7 +115,7 @@ void ScriptImpl::serializeJsonState(JsonSerializeFormat & handler) void ScriptImpl::resolveHost() { - ResourceID sourcePathId(sourcePath); + ResourcePath sourcePathId(sourcePath); if(sourcePathId.getType() == EResType::ERM) host = owner->erm; diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index c48436dfa..6bd09d3c6 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -28,7 +28,7 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const info->modScope = scope; info->moveCost = static_cast(json["moveCost"].Integer()); info->musicFilename = json["music"].String(); - info->tilesFilename = json["tiles"].String(); + info->tilesFilename = AnimationPath::fromJson(json["tiles"]); info->horseSound = json["horseSound"].String(); info->horseSoundPenalty = json["horseSoundPenalty"].String(); info->transitionRequired = json["transitionRequired"].Bool(); diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 5a10f1afa..fa7623136 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -15,6 +15,7 @@ #include "GameConstants.h" #include "IHandlerBase.h" #include "Color.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -67,7 +68,7 @@ public: ColorRGBA minimapUnblocked; std::string shortIdentifier; std::string musicFilename; - std::string tilesFilename; + AnimationPath tilesFilename; std::string terrainViewPatterns; std::string horseSound; std::string horseSoundPenalty; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index d5a714dcb..ba7f0d5c3 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -348,7 +348,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const std::vector> creBankFormations[2]; std::vector commanderField; std::vector commanderBank; - const JsonNode config(ResourceID("config/battleStartpos.json")); + const JsonNode config(ResourcePath("config/battleStartpos.json")); const JsonVector &positions = config["battle_positions"].Vector(); CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]); diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index c69db1117..e1de4b984 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -60,12 +60,12 @@ bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const return true; } -const std::string & CObstacleInstance::getAnimation() const +const AnimationPath & CObstacleInstance::getAnimation() const { return getInfo().animation; } -const std::string & CObstacleInstance::getAppearAnimation() const +const AnimationPath & CObstacleInstance::getAppearAnimation() const { return getInfo().appearAnimation; } @@ -119,8 +119,8 @@ void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) //We need only a subset of obstacle info for correct render handler.serializeInt("position", pos); handler.serializeString("appearSound", obstacleInfo.appearSound); - handler.serializeString("appearAnimation", obstacleInfo.appearAnimation); - handler.serializeString("animation", obstacleInfo.animation); + handler.serializeStruct("appearAnimation", obstacleInfo.appearAnimation); + handler.serializeStruct("animation", obstacleInfo.animation); handler.serializeInt("animationYOffset", animationYOffset); handler.serializeBool("hidden", hidden); @@ -214,8 +214,8 @@ void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("nativeVisible", nativeVisible); handler.serializeString("appearSound", appearSound); - handler.serializeString("appearAnimation", appearAnimation); - handler.serializeString("animation", animation); + handler.serializeStruct("appearAnimation", appearAnimation); + handler.serializeStruct("animation", animation); handler.serializeInt("animationYOffset", animationYOffset); @@ -239,12 +239,12 @@ void SpellCreatedObstacle::battleTurnPassed() turnsRemaining--; } -const std::string & SpellCreatedObstacle::getAnimation() const +const AnimationPath & SpellCreatedObstacle::getAnimation() const { return animation; } -const std::string & SpellCreatedObstacle::getAppearAnimation() const +const AnimationPath & SpellCreatedObstacle::getAppearAnimation() const { return appearAnimation; } diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index 9cf304d97..0d74d95fd 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -10,6 +10,7 @@ #pragma once #include "BattleHex.h" #include "NetPacksBase.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -51,8 +52,8 @@ struct DLL_LINKAGE CObstacleInstance virtual void battleTurnPassed(){}; //Client helper functions, make it easier to render animations - virtual const std::string & getAnimation() const; - virtual const std::string & getAppearAnimation() const; + virtual const AnimationPath & getAnimation() const; + virtual const AnimationPath & getAppearAnimation() const; virtual const std::string & getAppearSound() const; virtual int getAnimationYOffset(int imageHeight) const; @@ -88,8 +89,8 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance bool nativeVisible; //Should native terrain creatures reveal obstacle std::string appearSound; - std::string appearAnimation; - std::string animation; + AnimationPath appearAnimation; + AnimationPath animation; int animationYOffset; @@ -107,8 +108,8 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance void battleTurnPassed() override; //Client helper functions, make it easier to render animations - const std::string & getAnimation() const override; - const std::string & getAppearAnimation() const override; + const AnimationPath & getAnimation() const override; + const AnimationPath & getAppearAnimation() const override; const std::string & getAppearSound() const override; int getAnimationYOffset(int imageHeight) const override; diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 63630b6e2..133e99a62 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -59,7 +59,7 @@ void CampaignHandler::readCampaign(Campaign * ret, const std::vector & inpu std::unique_ptr CampaignHandler::getHeader( const std::string & name) { - ResourceID resourceID(name, EResType::CAMPAIGN); + ResourcePath resourceID(name, EResType::CAMPAIGN); std::string modName = VLC->modh->findResourceOrigin(resourceID); std::string language = VLC->modh->getModLanguage(modName); std::string encoding = Languages::getLanguageOptions(language).encoding; @@ -75,7 +75,7 @@ std::unique_ptr CampaignHandler::getHeader( const std::string & name) std::shared_ptr CampaignHandler::getCampaign( const std::string & name ) { - ResourceID resourceID(name, EResType::CAMPAIGN); + ResourcePath resourceID(name, EResType::CAMPAIGN); std::string modName = VLC->modh->findResourceOrigin(resourceID); std::string language = VLC->modh->getModLanguage(modName); std::string encoding = Languages::getLanguageOptions(language).encoding; @@ -592,7 +592,7 @@ std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr campDescriptions; if(campDescriptions.empty()) //read once { - const JsonNode config(ResourceID("config/campaign_regions.json")); + const JsonNode config(ResourcePath("config/campaign_regions.json")); for(const JsonNode & campaign : config["campaign_regions"].Vector()) campDescriptions.push_back(CampaignRegions::fromJson(campaign)); } @@ -66,9 +66,9 @@ CampaignRegions CampaignRegions::getLegacy(int campId) return campDescriptions.at(campId); } -std::string CampaignRegions::getBackgroundName() const +ImagePath CampaignRegions::getBackgroundName() const { - return campPrefix + "_BG.BMP"; + return ImagePath::builtin(campPrefix + "_BG.BMP"); } Point CampaignRegions::getPosition(CampaignScenarioID which) const @@ -77,7 +77,7 @@ Point CampaignRegions::getPosition(CampaignScenarioID which) const return Point(region.xpos, region.ypos); } -std::string CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const +ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const { auto const & region = regions[static_cast(which)]; @@ -89,20 +89,20 @@ std::string CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex std::string color = colors[colorSuffixLength - 1][colorIndex]; - return campPrefix + region.infix + "_" + type + color + ".BMP"; + return ImagePath::builtin(campPrefix + region.infix + "_" + type + color + ".BMP"); } -std::string CampaignRegions::getAvailableName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getAvailableName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "En"); } -std::string CampaignRegions::getSelectedName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getSelectedName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "Se"); } -std::string CampaignRegions::getConqueredName(CampaignScenarioID which, int color) const +ImagePath CampaignRegions::getConqueredName(CampaignScenarioID which, int color) const { return getNameFor(which, color, "Co"); } diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 51fb5c034..aa73801e3 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -9,7 +9,8 @@ */ #pragma once -#include "../../lib/GameConstants.h" +#include "../lib/GameConstants.h" +#include "../lib/filesystem/ResourcePath.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" @@ -47,14 +48,14 @@ class DLL_LINKAGE CampaignRegions std::vector regions; - std::string getNameFor(CampaignScenarioID which, int color, std::string type) const; + ImagePath getNameFor(CampaignScenarioID which, int color, std::string type) const; public: - std::string getBackgroundName() const; + ImagePath getBackgroundName() const; Point getPosition(CampaignScenarioID which) const; - std::string getAvailableName(CampaignScenarioID which, int color) const; - std::string getSelectedName(CampaignScenarioID which, int color) const; - std::string getConqueredName(CampaignScenarioID which, int color) const; + ImagePath getAvailableName(CampaignScenarioID which, int color) const; + ImagePath getSelectedName(CampaignScenarioID which, int color) const; + ImagePath getConqueredName(CampaignScenarioID which, int color) const; template void serialize(Handler &h, const int formatVersion) { diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index fddd889fb..c15b1b4a8 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -19,17 +19,17 @@ CMappedFileLoader::CMappedFileLoader(const std::string & mountPoint, const JsonN { for(auto entry : config.Struct()) { - //fileList[ResourceID(mountPoint + entry.first)] = ResourceID(mountPoint + entry.second.String()); - fileList.emplace(ResourceID(mountPoint + entry.first), ResourceID(mountPoint + entry.second.String())); + //fileList[ResourcePath(mountPoint + entry.first)] = ResourcePath(mountPoint + entry.second.String()); + fileList.emplace(ResourcePath(mountPoint + entry.first), ResourcePath(mountPoint + entry.second.String())); } } -std::unique_ptr CMappedFileLoader::load(const ResourceID & resourceName) const +std::unique_ptr CMappedFileLoader::load(const ResourcePath & resourceName) const { return CResourceHandler::get()->load(fileList.at(resourceName)); } -bool CMappedFileLoader::existsResource(const ResourceID & resourceName) const +bool CMappedFileLoader::existsResource(const ResourcePath & resourceName) const { return fileList.count(resourceName) != 0; } @@ -39,14 +39,14 @@ std::string CMappedFileLoader::getMountPoint() const return ""; // does not have any meaning with this type of data source } -std::optional CMappedFileLoader::getResourceName(const ResourceID & resourceName) const +std::optional CMappedFileLoader::getResourceName(const ResourcePath & resourceName) const { return CResourceHandler::get()->getResourceName(fileList.at(resourceName)); } -std::unordered_set CMappedFileLoader::getFilteredFiles(std::function filter) const +std::unordered_set CMappedFileLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : fileList) { @@ -64,7 +64,7 @@ CFilesystemList::~CFilesystemList() { } -std::unique_ptr CFilesystemList::load(const ResourceID & resourceName) const +std::unique_ptr CFilesystemList::load(const ResourcePath & resourceName) const { // load resource from last loader that have it (last overridden version) for(const auto & loader : boost::adaptors::reverse(loaders)) @@ -77,7 +77,7 @@ std::unique_ptr CFilesystemList::load(const ResourceID & resourceN + EResTypeHelper::getEResTypeAsString(resourceName.getType()) + " wasn't found."); } -bool CFilesystemList::existsResource(const ResourceID & resourceName) const +bool CFilesystemList::existsResource(const ResourcePath & resourceName) const { for(const auto & loader : loaders) if (loader->existsResource(resourceName)) @@ -90,14 +90,14 @@ std::string CFilesystemList::getMountPoint() const return ""; } -std::optional CFilesystemList::getResourceName(const ResourceID & resourceName) const +std::optional CFilesystemList::getResourceName(const ResourcePath & resourceName) const { if (existsResource(resourceName)) return getResourcesWithName(resourceName).back()->getResourceName(resourceName); return std::optional(); } -std::set CFilesystemList::getResourceNames(const ResourceID & resourceName) const +std::set CFilesystemList::getResourceNames(const ResourcePath & resourceName) const { std::set paths; for(auto& loader : getResourcesWithName(resourceName)) @@ -117,9 +117,9 @@ void CFilesystemList::updateFilteredFiles(std::functionupdateFilteredFiles(filter); } -std::unordered_set CFilesystemList::getFilteredFiles(std::function filter) const +std::unordered_set CFilesystemList::getFilteredFiles(std::function filter) const { - std::unordered_set ret; + std::unordered_set ret; for(const auto & loader : loaders) for(const auto & entry : loader->getFilteredFiles(filter)) @@ -139,7 +139,7 @@ bool CFilesystemList::createResource(std::string filename, bool update) // Check if resource was created successfully. Possible reasons for this to fail // a) loader failed to create resource (e.g. read-only FS) // b) in update mode, call with filename that does not exists - assert(load(ResourceID(filename))); + assert(load(ResourcePath(filename))); logGlobal->trace("Resource created successfully"); return true; @@ -149,7 +149,7 @@ bool CFilesystemList::createResource(std::string filename, bool update) return false; } -std::vector CFilesystemList::getResourcesWithName(const ResourceID & resourceName) const +std::vector CFilesystemList::getResourcesWithName(const ResourcePath & resourceName) const { std::vector ret; diff --git a/lib/filesystem/AdapterLoaders.h b/lib/filesystem/AdapterLoaders.h index 3e0386ed5..c54c24d50 100644 --- a/lib/filesystem/AdapterLoaders.h +++ b/lib/filesystem/AdapterLoaders.h @@ -10,7 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -38,19 +38,19 @@ public: /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - std::optional getResourceName(const ResourceID & resourceName) const override; + std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; private: /** A list of files in this map - * key = ResourceID for resource loader - * value = ResourceID to which file this request will be redirected + * key = ResourcePath for resource loader + * value = ResourcePath to which file this request will be redirected */ - std::unordered_map fileList; + std::unordered_map fileList; }; class DLL_LINKAGE CFilesystemList : public ISimpleResourceLoader @@ -68,15 +68,15 @@ public: ~CFilesystemList(); /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - std::optional getResourceName(const ResourceID & resourceName) const override; - std::set getResourceNames(const ResourceID & resourceName) const override; + std::optional getResourceName(const ResourcePath & resourceName) const override; + std::set getResourceNames(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; bool createResource(std::string filename, bool update = false) override; - std::vector getResourcesWithName(const ResourceID & resourceName) const override; + std::vector getResourcesWithName(const ResourcePath & resourceName) const override; /** * Adds a resource loader to the loaders list diff --git a/lib/filesystem/CArchiveLoader.cpp b/lib/filesystem/CArchiveLoader.cpp index b50f15f4d..c7b47f59c 100644 --- a/lib/filesystem/CArchiveLoader.cpp +++ b/lib/filesystem/CArchiveLoader.cpp @@ -78,7 +78,7 @@ void CArchiveLoader::initLODArchive(const std::string &mountPoint, CFileInputStr entry.compressedSize = reader.readUInt32(); // Add lod entry to local entries map - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; if(extractArchives) { @@ -123,7 +123,7 @@ void CArchiveLoader::initVIDArchive(const std::string &mountPoint, CFileInputStr entry.compressedSize = 0; offsets.insert(entry.offset); - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; } offsets.insert(static_cast(fileStream.getSize())); @@ -162,14 +162,14 @@ void CArchiveLoader::initSNDArchive(const std::string &mountPoint, CFileInputStr entry.offset = reader.readInt32(); entry.fullSize = reader.readInt32(); entry.compressedSize = 0; - entries[ResourceID(mountPoint + entry.name)] = entry; + entries[ResourcePath(mountPoint + entry.name)] = entry; if(extractArchives) extractToFolder("SOUND", fileStream, entry); } } -std::unique_ptr CArchiveLoader::load(const ResourceID & resourceName) const +std::unique_ptr CArchiveLoader::load(const ResourcePath & resourceName) const { assert(existsResource(resourceName)); @@ -187,7 +187,7 @@ std::unique_ptr CArchiveLoader::load(const ResourceID & resourceNa } } -bool CArchiveLoader::existsResource(const ResourceID & resourceName) const +bool CArchiveLoader::existsResource(const ResourcePath & resourceName) const { return entries.count(resourceName) != 0; } @@ -197,9 +197,9 @@ std::string CArchiveLoader::getMountPoint() const return mountPoint; } -std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const +std::unordered_set CArchiveLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : entries) { @@ -229,7 +229,7 @@ void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, CInput void CArchiveLoader::extractToFolder(const std::string & outputSubFolder, const std::string & mountPoint, ArchiveEntry entry) const { - std::unique_ptr inputStream = load(ResourceID(mountPoint + entry.name)); + std::unique_ptr inputStream = load(ResourcePath(mountPoint + entry.name)); entry.offset = 0; extractToFolder(outputSubFolder, *inputStream, entry); diff --git a/lib/filesystem/CArchiveLoader.h b/lib/filesystem/CArchiveLoader.h index d1ed59394..33b410062 100644 --- a/lib/filesystem/CArchiveLoader.h +++ b/lib/filesystem/CArchiveLoader.h @@ -10,7 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -60,11 +60,11 @@ public: /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; /** Extracts one archive entry to the specified subfolder. Used for Video and Sound */ void extractToFolder(const std::string & outputSubFolder, CInputStream & fileStream, const ArchiveEntry & entry) const; /** Extracts one archive entry to the specified subfolder. Used for Images, Sprites, etc */ @@ -98,7 +98,7 @@ private: std::string mountPoint; /** Holds all entries of the archive file. An entry can be accessed via the entry name. **/ - std::unordered_map entries; + std::unordered_map entries; /** Specifies if Original H3 archives should be extracted to a separate folder **/ bool extractArchives; diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index d56f792be..c90df4dd0 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -23,7 +23,7 @@ CFilesystemLoader::CFilesystemLoader(std::string _mountPoint, boost::filesystem: logGlobal->trace("File system loaded, %d files found", fileList.size()); } -std::unique_ptr CFilesystemLoader::load(const ResourceID & resourceName) const +std::unique_ptr CFilesystemLoader::load(const ResourcePath & resourceName) const { assert(fileList.count(resourceName)); boost::filesystem::path file = baseDirectory / fileList.at(resourceName); @@ -31,7 +31,7 @@ std::unique_ptr CFilesystemLoader::load(const ResourceID & resourc return std::make_unique(file); } -bool CFilesystemLoader::existsResource(const ResourceID & resourceName) const +bool CFilesystemLoader::existsResource(const ResourcePath & resourceName) const { return fileList.count(resourceName); } @@ -41,7 +41,7 @@ std::string CFilesystemLoader::getMountPoint() const return mountPoint; } -std::optional CFilesystemLoader::getResourceName(const ResourceID & resourceName) const +std::optional CFilesystemLoader::getResourceName(const ResourcePath & resourceName) const { assert(existsResource(resourceName)); @@ -56,9 +56,9 @@ void CFilesystemLoader::updateFilteredFiles(std::function CFilesystemLoader::getFilteredFiles(std::function filter) const +std::unordered_set CFilesystemLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for (auto & file : fileList) { @@ -70,7 +70,7 @@ std::unordered_set CFilesystemLoader::getFilteredFiles(std::function bool CFilesystemLoader::createResource(std::string filename, bool update) { - ResourceID resID(filename); + ResourcePath resID(filename); if (fileList.find(resID) != fileList.end()) return true; @@ -99,19 +99,19 @@ bool CFilesystemLoader::createResource(std::string filename, bool update) return true; } -std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const +std::unordered_map CFilesystemLoader::listFiles(const std::string &mountPoint, size_t depth, bool initial) const { - static const EResType::Type initArray[] = { + static const EResType initArray[] = { EResType::DIRECTORY, EResType::TEXT, EResType::ARCHIVE_LOD, EResType::ARCHIVE_VID, EResType::ARCHIVE_SND, EResType::ARCHIVE_ZIP }; - static const std::set initialTypes(initArray, initArray + std::size(initArray)); + static const std::set initialTypes(initArray, initArray + std::size(initArray)); assert(boost::filesystem::is_directory(baseDirectory)); - std::unordered_map fileList; + std::unordered_map fileList; std::vector path; //vector holding relative path to our file @@ -124,7 +124,7 @@ std::unordered_map CFilesystemLoader::listF for(; it != enddir; ++it) { - EResType::Type type; + EResType type; #if BOOST_VERSION >= 107200 const auto currentDepth = it.depth(); #else @@ -177,7 +177,7 @@ std::unordered_map CFilesystemLoader::listF else resName = mountPoint + filename.string(); - fileList[ResourceID(resName, type)] = std::move(filename); + fileList[ResourcePath(resName, type)] = std::move(filename); } } diff --git a/lib/filesystem/CFilesystemLoader.h b/lib/filesystem/CFilesystemLoader.h index 8aeb23581..9d7687865 100644 --- a/lib/filesystem/CFilesystemLoader.h +++ b/lib/filesystem/CFilesystemLoader.h @@ -10,7 +10,7 @@ #pragma once #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -34,13 +34,13 @@ public: /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; bool createResource(std::string filename, bool update = false) override; - std::optional getResourceName(const ResourceID & resourceName) const override; + std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; private: /** The base directory which is scanned and indexed. */ @@ -51,10 +51,10 @@ private: size_t recursiveDepth; /** A list of files in the directory - * key = ResourceID for resource loader + * key = ResourcePath for resource loader * value = name that can be used to access file */ - mutable std::unordered_map fileList; + mutable std::unordered_map fileList; /** * Returns a list of pathnames denoting the files in the directory denoted by this pathname. @@ -65,7 +65,7 @@ private: * @return a list of pathnames denoting the files and directories in the directory denoted by this pathname * The array will be empty if the directory is empty. Ptr is null if the directory doesn't exist or if it isn't a directory. */ - std::unordered_map listFiles(const std::string &mountPoint, size_t depth, bool initial) const; + std::unordered_map listFiles(const std::string &mountPoint, size_t depth, bool initial) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/filesystem/CZipLoader.cpp b/lib/filesystem/CZipLoader.cpp index ced952a1c..bc518f0d8 100644 --- a/lib/filesystem/CZipLoader.cpp +++ b/lib/filesystem/CZipLoader.cpp @@ -61,9 +61,9 @@ CZipLoader::CZipLoader(const std::string & mountPoint, const boost::filesystem:: logGlobal->trace("Zip archive loaded, %d files found", files.size()); } -std::unordered_map CZipLoader::listFiles(const std::string & mountPoint, const boost::filesystem::path & archive) +std::unordered_map CZipLoader::listFiles(const std::string & mountPoint, const boost::filesystem::path & archive) { - std::unordered_map ret; + std::unordered_map ret; unzFile file = unzOpen2_64(archive.c_str(), &zlibApi); @@ -84,7 +84,7 @@ std::unordered_map CZipLoader::listFiles(const std:: unzGetCurrentFileInfo64(file, &info, filename.data(), static_cast(filename.size()), nullptr, 0, nullptr, 0); std::string filenameString(filename.data(), filename.size()); - unzGetFilePos64(file, &ret[ResourceID(mountPoint + filenameString)]); + unzGetFilePos64(file, &ret[ResourcePath(mountPoint + filenameString)]); } while (unzGoToNextFile(file) == UNZ_OK); } @@ -93,12 +93,12 @@ std::unordered_map CZipLoader::listFiles(const std:: return ret; } -std::unique_ptr CZipLoader::load(const ResourceID & resourceName) const +std::unique_ptr CZipLoader::load(const ResourcePath & resourceName) const { return std::unique_ptr(new CZipStream(ioApi, archiveName, files.at(resourceName))); } -bool CZipLoader::existsResource(const ResourceID & resourceName) const +bool CZipLoader::existsResource(const ResourcePath & resourceName) const { return files.count(resourceName) != 0; } @@ -108,9 +108,9 @@ std::string CZipLoader::getMountPoint() const return mountPoint; } -std::unordered_set CZipLoader::getFilteredFiles(std::function filter) const +std::unordered_set CZipLoader::getFilteredFiles(std::function filter) const { - std::unordered_set foundID; + std::unordered_set foundID; for(const auto & file : files) { diff --git a/lib/filesystem/CZipLoader.h b/lib/filesystem/CZipLoader.h index 255624164..139e49188 100644 --- a/lib/filesystem/CZipLoader.h +++ b/lib/filesystem/CZipLoader.h @@ -11,7 +11,7 @@ #include "ISimpleResourceLoader.h" #include "CInputStream.h" -#include "ResourceID.h" +#include "ResourcePath.h" #include "CCompressedStream.h" #include "MinizipExtensions.h" @@ -46,19 +46,19 @@ class DLL_LINKAGE CZipLoader : public ISimpleResourceLoader boost::filesystem::path archiveName; std::string mountPoint; - std::unordered_map files; + std::unordered_map files; - std::unordered_map listFiles(const std::string & mountPoint, const boost::filesystem::path &archive); + std::unordered_map listFiles(const std::string & mountPoint, const boost::filesystem::path &archive); public: CZipLoader(const std::string & mountPoint, const boost::filesystem::path & archive, std::shared_ptr api = std::shared_ptr(new CDefaultIOApi())); /// Interface implementation /// @see ISimpleResourceLoader - std::unique_ptr load(const ResourceID & resourceName) const override; - bool existsResource(const ResourceID & resourceName) const override; + std::unique_ptr load(const ResourcePath & resourceName) const override; + bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; void updateFilteredFiles(std::function filter) const override {} - std::unordered_set getFilteredFiles(std::function filter) const override; + std::unordered_set getFilteredFiles(std::function filter) const override; }; namespace ZipArchive diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index da8bba83e..726e9bb8b 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -84,7 +84,7 @@ void CFilesystemGenerator::loadDirectory(const std::string &mountPoint, const Js if (!config["depth"].isNull()) depth = static_cast(config["depth"].Float()); - ResourceID resID(URI, EResType::DIRECTORY); + ResourcePath resID(URI, EResType::DIRECTORY); for(auto & loader : CResourceHandler::get("initial")->getResourcesWithName(resID)) { @@ -96,16 +96,16 @@ void CFilesystemGenerator::loadDirectory(const std::string &mountPoint, const Js void CFilesystemGenerator::loadZipArchive(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, EResType::ARCHIVE_ZIP)); + auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, EResType::ARCHIVE_ZIP)); if (filename) filesystem->addLoader(new CZipLoader(mountPoint, *filename), false); } -template +template void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, archiveType)); + auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, archiveType)); if (filename) filesystem->addLoader(new CArchiveLoader(mountPoint, *filename, extractArchives), false); } @@ -113,10 +113,10 @@ void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const Json void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourceID(URI, EResType::TEXT)); + auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, EResType::TEXT)); if (filename) { - auto configData = CResourceHandler::get("initial")->load(ResourceID(URI, EResType::TEXT))->readAll(); + auto configData = CResourceHandler::get("initial")->load(ResourcePath(URI, EResType::TEXT))->readAll(); const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); filesystem->addLoader(new CMappedFileLoader(mountPoint, configInitial), false); } @@ -131,7 +131,7 @@ ISimpleResourceLoader * CResourceHandler::createInitial() //recurse only into specific directories auto recurseInDir = [&](const std::string & URI, int depth) { - ResourceID ID(URI, EResType::DIRECTORY); + ResourcePath ID(URI, EResType::DIRECTORY); for(auto & loader : initialLoader->getResourcesWithName(ID)) { @@ -210,7 +210,7 @@ ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives) { - auto fsConfigData = get("initial")->load(ResourceID(fsConfigURI, EResType::TEXT))->readAll(); + auto fsConfigData = get("initial")->load(ResourcePath(fsConfigURI, EResType::TEXT))->readAll(); const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); diff --git a/lib/filesystem/Filesystem.h b/lib/filesystem/Filesystem.h index a6b894ab0..1ead410f1 100644 --- a/lib/filesystem/Filesystem.h +++ b/lib/filesystem/Filesystem.h @@ -11,7 +11,7 @@ #include "CInputStream.h" #include "ISimpleResourceLoader.h" -#include "ResourceID.h" +#include "ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +27,7 @@ class DLL_LINKAGE CFilesystemGenerator CFilesystemList * filesystem; std::string prefix; - template + template void loadArchive(const std::string & mountPoint, const JsonNode & config); void loadDirectory(const std::string & mountPoint, const JsonNode & config); void loadZipArchive(const std::string & mountPoint, const JsonNode & config); diff --git a/lib/filesystem/ISimpleResourceLoader.h b/lib/filesystem/ISimpleResourceLoader.h index 88c6a9fad..33272b1a9 100644 --- a/lib/filesystem/ISimpleResourceLoader.h +++ b/lib/filesystem/ISimpleResourceLoader.h @@ -12,7 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; -class ResourceID; +class ResourcePath; /** * A class which knows the files containing in the archive or system and how to load them. @@ -28,14 +28,14 @@ public: * @param resourceName The unqiue resource name in space of the archive. * @return a input stream object */ - virtual std::unique_ptr load(const ResourceID & resourceName) const = 0; + virtual std::unique_ptr load(const ResourcePath & resourceName) const = 0; /** * Checks if the entry exists. * * @return Returns true if the entry exists, false if not. */ - virtual bool existsResource(const ResourceID & resourceName) const = 0; + virtual bool existsResource(const ResourcePath & resourceName) const = 0; /** * Gets mount point to which this loader was attached @@ -49,7 +49,7 @@ public: * * @return path or empty optional if file can't be accessed independently (e.g. file in archive) */ - virtual std::optional getResourceName(const ResourceID & resourceName) const + virtual std::optional getResourceName(const ResourcePath & resourceName) const { return std::optional(); } @@ -59,7 +59,7 @@ public: * * @return std::set with names. */ - virtual std::set getResourceNames(const ResourceID & resourceName) const + virtual std::set getResourceNames(const ResourcePath & resourceName) const { std::set result; auto rn = getResourceName(resourceName); @@ -83,7 +83,7 @@ public: * @param filter Filter that returns true if specified ID matches filter * @return Returns list of flies */ - virtual std::unordered_set getFilteredFiles(std::function filter) const = 0; + virtual std::unordered_set getFilteredFiles(std::function filter) const = 0; /** * Creates new resource with specified filename. @@ -100,7 +100,7 @@ public: * * @return vector with all loaders */ - virtual std::vector getResourcesWithName(const ResourceID & resourceName) const + virtual std::vector getResourcesWithName(const ResourcePath & resourceName) const { if (existsResource(resourceName)) return std::vector(1, this); diff --git a/lib/filesystem/ResourceID.h b/lib/filesystem/ResourceID.h deleted file mode 100644 index 0e597bd74..000000000 --- a/lib/filesystem/ResourceID.h +++ /dev/null @@ -1,158 +0,0 @@ -/* - * ResourceID.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 - -VCMI_LIB_NAMESPACE_BEGIN - - -/** - * Specifies the resource type. - * - * Supported file extensions: - * - * Text: .txt .json - * Animation: .def - * Mask: .msk .msg - * Campaign: .h3c - * Map: .h3m - * Font: .fnt - * Image: .bmp, .jpg, .pcx, .png, .tga - * Sound: .wav .82m - * Video: .smk, .bik .mjpg .mpg - * Music: .mp3, .ogg - * Archive: .lod, .snd, .vid .pac .zip - * Palette: .pal - * Savegame: .v*gm1 - */ -namespace EResType -{ - enum Type - { - TEXT, - ANIMATION, - MASK, - CAMPAIGN, - MAP, - BMP_FONT, - TTF_FONT, - IMAGE, - VIDEO, - SOUND, - ARCHIVE_VID, - ARCHIVE_ZIP, - ARCHIVE_SND, - ARCHIVE_LOD, - PALETTE, - SAVEGAME, - DIRECTORY, - ERM, - ERT, - ERS, - OTHER, - UNDEFINED, - LUA - }; -} - -/** - * A struct which identifies a resource clearly. - */ -class DLL_LINKAGE ResourceID -{ -public: - /** - * Default c-tor. - */ - //ResourceID(); - - /** - * Ctor. Can be used to create identifier for resource loading using one parameter - * - * @param fullName The resource name including extension. - */ - explicit ResourceID(std::string fullName); - - /** - * Ctor. - * - * @param name The resource name. - * @param type The resource type. A constant from the enumeration EResType. - */ - ResourceID(std::string name, EResType::Type type); - - /** - * Compares this object with a another resource identifier. - * - * @param other The other resource identifier. - * @return Returns true if both are equally, false if not. - */ - inline bool operator==(ResourceID const & other) const - { - return name == other.name && type == other.type; - } - - std::string getName() const {return name;} - std::string getOriginalName() const {return originalName;} - EResType::Type getType() const {return type;} - //void setName(std::string name); - //void setType(EResType::Type type); - -private: - /** - * Specifies the resource type. EResType::OTHER if not initialized. - * Required to prevent conflicts if files with different types (e.g. text and image) have the same name. - */ - EResType::Type type; - - /** Specifies the resource name. No extension so .pcx and .png can override each other, always in upper case. **/ - std::string name; - - /** name in original case **/ - std::string originalName; -}; - -/** - * A helper class which provides a functionality to convert extension strings to EResTypes. - */ -class DLL_LINKAGE EResTypeHelper -{ -public: - /** - * Converts a extension string to a EResType enum object. - * - * @param extension The extension string e.g. .BMP, .PNG - * @return Returns a EResType enum object - */ - static EResType::Type getTypeFromExtension(std::string extension); - - /** - * Gets the EResType as a string representation. - * - * @param type the EResType - * @return the type as a string representation - */ - static std::string getEResTypeAsString(EResType::Type type); -}; - -VCMI_LIB_NAMESPACE_END - - -namespace std -{ -template <> struct hash -{ - size_t operator()(const VCMI_LIB_WRAP_NAMESPACE(ResourceID) & resourceIdent) const - { - std::hash intHasher; - std::hash stringHasher; - return stringHasher(resourceIdent.getName()) ^ intHasher(static_cast(resourceIdent.getType())); - } -}; -} diff --git a/lib/filesystem/ResourceID.cpp b/lib/filesystem/ResourcePath.cpp similarity index 59% rename from lib/filesystem/ResourceID.cpp rename to lib/filesystem/ResourcePath.cpp index 75c516abf..120cb5203 100644 --- a/lib/filesystem/ResourceID.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -1,5 +1,5 @@ /* - * ResourceID.cpp, part of VCMI engine + * ResourcePath.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -8,39 +8,21 @@ * */ #include "StdInc.h" -#include "ResourceID.h" +#include "ResourcePath.h" #include "FileInfo.h" +#include "../JsonNode.h" +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" + VCMI_LIB_NAMESPACE_BEGIN -// trivial to_upper that completely ignores localization and only work with ASCII -// Technically not a problem since -// 1) Right now VCMI does not supports unicode in filenames on Win -// 2) Filesystem case-sensivity is only problem for H3 data which uses ASCII-only symbols -// for me (Ivan) this define gives notable decrease in loading times -// #define ENABLE_TRIVIAL_TOUPPER - -#ifdef ENABLE_TRIVIAL_TOUPPER -static inline void toUpper(char & symbol) -{ - static const int diff = 'a' - 'A'; - if (symbol >= 'a' && symbol <= 'z') - symbol -= diff; -} - -static inline void toUpper(std::string & string) -{ - for (char & symbol : string) - toUpper(symbol); -} -#else static inline void toUpper(std::string & string) { boost::to_upper(string); } -#endif -static inline EResType::Type readType(const std::string& name) +static inline EResType readType(const std::string& name) { return EResTypeHelper::getTypeFromExtension(FileInfo::GetExtension(name).to_string()); } @@ -67,63 +49,49 @@ static inline std::string readName(std::string name, bool uppercase) return name; } -#if 0 -ResourceID::ResourceID() - :type(EResType::OTHER) -{ -} -#endif - -ResourceID::ResourceID(std::string name_): - type{readType(name_)}, - name{readName(name_, true)}, - originalName{readName(std::move(name_), false)} +ResourcePath::ResourcePath(const std::string & name_): + type(readType(name_)), + name(readName(name_, true)), + originalName(readName(name_, false)) {} -ResourceID::ResourceID(std::string name_, EResType::Type type_): - type{type_}, - name{readName(name_, true)}, - originalName{readName(std::move(name_), false)} +ResourcePath::ResourcePath(const std::string & name_, EResType type_): + type(type_), + name(readName(name_, true)), + originalName(readName(name_, false)) {} -#if 0 -std::string ResourceID::getName() const + +ResourcePath::ResourcePath(const JsonNode & name, EResType type): + type(type), + name(readName(name.String(), true)), + originalName(readName(name.String(), false)) { - return name; } -EResType::Type ResourceID::getType() const +void ResourcePath::serializeJson(JsonSerializeFormat & handler) { - return type; -} - -void ResourceID::setName(std::string name) -{ - // setName shouldn't be used if type is UNDEFINED - assert(type != EResType::UNDEFINED); - - this->name = std::move(name); - - size_t dotPos = this->name.find_last_of("/."); - - if(dotPos != std::string::npos && this->name[dotPos] == '.' - && this->type == EResTypeHelper::getTypeFromExtension(this->name.substr(dotPos))) + if (!handler.saving) { - this->name.erase(dotPos); + JsonNode const & node = handler.getCurrent(); + + if (node.isString()) + { + name = readName(node.String(), true); + originalName = readName(node.String(), false); + return; + } } - toUpper(this->name); + handler.serializeInt("type", type); + handler.serializeString("name", name); + handler.serializeString("originalName", originalName); } -void ResourceID::setType(EResType::Type type) -{ - this->type = type; -} -#endif -EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) +EResType EResTypeHelper::getTypeFromExtension(std::string extension) { toUpper(extension); - static const std::map stringToRes = + static const std::map stringToRes = { {".TXT", EResType::TEXT}, {".JSON", EResType::TEXT}, @@ -173,11 +141,11 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension) return iter->second; } -std::string EResTypeHelper::getEResTypeAsString(EResType::Type type) +std::string EResTypeHelper::getEResTypeAsString(EResType type) { #define MAP_ENUM(value) {EResType::value, #value}, - static const std::map stringToRes = + static const std::map stringToRes = { MAP_ENUM(TEXT) MAP_ENUM(ANIMATION) diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h new file mode 100644 index 000000000..6ccfa1d8e --- /dev/null +++ b/lib/filesystem/ResourcePath.h @@ -0,0 +1,203 @@ +/* + * ResourcePath.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class JsonSerializeFormat; + +/** + * Specifies the resource type. + * + * Supported file extensions: + * + * Text: .txt .json + * Animation: .def + * Mask: .msk .msg + * Campaign: .h3c + * Map: .h3m + * Font: .fnt + * Image: .bmp, .jpg, .pcx, .png, .tga + * Sound: .wav .82m + * Video: .smk, .bik .mjpg .mpg + * Music: .mp3, .ogg + * Archive: .lod, .snd, .vid .pac .zip + * Palette: .pal + * Savegame: .v*gm1 + */ +enum class EResType +{ + TEXT, + ANIMATION, + MASK, + CAMPAIGN, + MAP, + BMP_FONT, + TTF_FONT, + IMAGE, + VIDEO, + SOUND, + ARCHIVE_VID, + ARCHIVE_ZIP, + ARCHIVE_SND, + ARCHIVE_LOD, + PALETTE, + SAVEGAME, + DIRECTORY, + ERM, + ERT, + ERS, + LUA, + OTHER, + UNDEFINED, +}; + +/** + * A struct which identifies a resource clearly. + */ +class DLL_LINKAGE ResourcePath +{ +protected: + /// Constructs resource path based on JsonNode and selected type. File extension is ignored + ResourcePath(const JsonNode & name, EResType type); + +public: + /// Constructs resource path based on full name including extension + explicit ResourcePath(const std::string & fullName); + + /// Constructs resource path based on filename and selected type. File extension is ignored + ResourcePath(const std::string & name, EResType type); + + inline bool operator==(const ResourcePath & other) const + { + return name == other.name && type == other.type; + } + + inline bool operator<(const ResourcePath & other) const + { + if (type != other.type) + return type < other.type; + return name < other.name; + } + + bool empty() const {return name.empty();} + std::string getName() const {return name;} + std::string getOriginalName() const {return originalName;} + EResType getType() const {return type;} + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler & h, const int version) + { + h & type; + h & name; + h & originalName; + } + +protected: + /// Specifies the resource type. EResType::OTHER if not initialized. + /// Required to prevent conflicts if files with different types (e.g. text and image) have the same name. + EResType type; + + /// Specifies the resource name. No extension so .pcx and .png can override each other, always in upper case. + std::string name; + + /// name in original case + std::string originalName; +}; + +template +class DLL_LINKAGE ResourcePathTempl : public ResourcePath +{ + template + friend class ResourcePathTempl; + + ResourcePathTempl(const ResourcePath & copy) + :ResourcePath(copy) + { + type = Type; + } + +public: + using ResourcePath::ResourcePath; + + ResourcePathTempl() + :ResourcePath("", Type) + {} + + static ResourcePathTempl builtin(const std::string & filename) + { + return ResourcePathTempl(filename, Type); + } + + static ResourcePathTempl builtinTODO(const std::string & filename) + { + return ResourcePathTempl(filename, Type); + } + + static ResourcePathTempl fromJson(const JsonNode & path) + { + return ResourcePathTempl(path, Type); + } + + template + ResourcePathTempl toType() const + { + ResourcePathTempl result(static_cast(*this)); + return result; + } + + ResourcePathTempl addPrefix(const std::string & prefix) const + { + ResourcePathTempl result; + result.name = prefix + this->getName(); + result.originalName = prefix + this->getOriginalName(); + + return result; + } +}; + +using AnimationPath = ResourcePathTempl; +using ImagePath = ResourcePathTempl; + +namespace EResTypeHelper +{ + /** + * Converts a extension string to a EResType enum object. + * + * @param extension The extension string e.g. .BMP, .PNG + * @return Returns a EResType enum object + */ + EResType getTypeFromExtension(std::string extension); + + /** + * Gets the EResType as a string representation. + * + * @param type the EResType + * @return the type as a string representation + */ + std::string getEResTypeAsString(EResType type); +}; + +VCMI_LIB_NAMESPACE_END + +namespace std +{ +template <> struct hash +{ + size_t operator()(const VCMI_LIB_WRAP_NAMESPACE(ResourcePath) & resourceIdent) const + { + std::hash intHasher; + std::hash stringHasher; + return stringHasher(resourceIdent.getName()) ^ intHasher(static_cast(resourceIdent.getType())); + } +}; +} diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c81342752..18fd60bb3 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -29,7 +29,7 @@ #include "../VCMI_Lib.h" #include "../battle/BattleInfo.h" #include "../campaign/CampaignState.h" -#include "../filesystem/ResourceID.h" +#include "../filesystem/ResourcePath.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/DwellingInstanceConstructor.h" @@ -602,7 +602,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan else { logGlobal->info("Open map file: %s", scenarioOps->mapname); - const ResourceID mapURI(scenarioOps->mapname, EResType::MAP); + const ResourcePath mapURI(scenarioOps->mapname, EResType::MAP); map = mapService->loadMap(mapURI).release(); } } @@ -804,7 +804,7 @@ void CGameState::removeHeroPlaceholders() void CGameState::initStartingResources() { logGlobal->debug("\tSetting up resources"); - const JsonNode config(ResourceID("config/startres.json")); + const JsonNode config(ResourcePath("config/startres.json")); const JsonVector &vector = config["difficulty"].Vector(); const JsonNode &level = vector[scenarioOps->difficulty]; diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 14aec1665..eab678f1d 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -182,10 +182,10 @@ void BoatInstanceConstructor::initTypeData(const JsonNode & input) layer = EPathfindingLayer(pos); onboardAssaultAllowed = input["onboardAssaultAllowed"].Bool(); onboardVisitAllowed = input["onboardVisitAllowed"].Bool(); - actualAnimation = input["actualAnimation"].String(); - overlayAnimation = input["overlayAnimation"].String(); + actualAnimation = AnimationPath::fromJson(input["actualAnimation"]); + overlayAnimation = AnimationPath::fromJson(input["overlayAnimation"]); for(int i = 0; i < flagAnimations.size() && i < input["flagAnimations"].Vector().size(); ++i) - flagAnimations[i] = input["flagAnimations"].Vector()[i].String(); + flagAnimations[i] = AnimationPath::fromJson(input["flagAnimations"].Vector()[i]); bonuses = JsonRandom::loadBonuses(input["bonuses"]); } @@ -201,7 +201,7 @@ void BoatInstanceConstructor::initializeObject(CGBoat * boat) const boat->addNewBonus(std::make_shared(b)); } -std::string BoatInstanceConstructor::getBoatAnimationName() const +AnimationPath BoatInstanceConstructor::getBoatAnimationName() const { return actualAnimation; } diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 58ea22ca7..5c50ded30 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -113,15 +113,15 @@ protected: bool onboardAssaultAllowed; //if true, hero can attack units from transport bool onboardVisitAllowed; //if true, hero can visit objects from transport - std::string actualAnimation; //for OH3 boats those have actual animations - std::string overlayAnimation; //waves animations - std::array flagAnimations; + AnimationPath actualAnimation; //for OH3 boats those have actual animations + AnimationPath overlayAnimation; //waves animations + std::array flagAnimations; public: void initializeObject(CGBoat * object) const override; /// Returns boat preview animation, for use in Shipyards - std::string getBoatAnimationName() const; + AnimationPath getBoatAnimationName() const; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index dccbb6610..614877f31 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -12,14 +12,14 @@ #include "CObjectHandler.h" #include "CGObjectInstance.h" -#include "../filesystem/ResourceID.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN CObjectHandler::CObjectHandler() { logGlobal->trace("\t\tReading resources prices "); - const JsonNode config2(ResourceID("config/resources.json")); + const JsonNode config2(ResourcePath("config/resources.json")); for(const JsonNode &price : config2["resources_prices"].Vector()) { resVals.push_back(static_cast(price.Float())); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index f159f418d..13488e6e2 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -354,9 +354,9 @@ public: EPathfindingLayer layer; //animation filenames. If empty - animations won't be used - std::string actualAnimation; //for OH3 boats those have actual animations - std::string overlayAnimation; //waves animations - std::array flagAnimations; + AnimationPath actualAnimation; //for OH3 boats those have actual animations + AnimationPath overlayAnimation; //waves animations + std::array flagAnimations; CGBoat(); bool isCoastVisitable() const override; diff --git a/lib/mapObjects/ObjectTemplate.cpp b/lib/mapObjects/ObjectTemplate.cpp index 675f98486..3b623c76c 100644 --- a/lib/mapObjects/ObjectTemplate.cpp +++ b/lib/mapObjects/ObjectTemplate.cpp @@ -116,8 +116,6 @@ void ObjectTemplate::afterLoadFixup() usedTiles[0][0] = VISITABLE; visitDir = 0xFF; } - boost::algorithm::replace_all(animationFile, "\\", "/"); - boost::algorithm::replace_all(editorAnimationFile, "\\", "/"); } void ObjectTemplate::readTxt(CLegacyConfigParser & parser) @@ -127,7 +125,7 @@ void ObjectTemplate::readTxt(CLegacyConfigParser & parser) boost::split(strings, data, boost::is_any_of(" ")); assert(strings.size() == 9); - animationFile = strings[0]; + animationFile = AnimationPath::builtin(strings[0]); stringID = strings[0]; std::string & blockStr = strings[1]; //block map, 0 = blocked, 1 = unblocked @@ -182,7 +180,7 @@ void ObjectTemplate::readTxt(CLegacyConfigParser & parser) void ObjectTemplate::readMsk() { - ResourceID resID("SPRITES/" + animationFile, EResType::MASK); + ResourcePath resID(animationFile.getName(), EResType::MASK); if (CResourceHandler::get()->existsResource(resID)) { @@ -197,7 +195,7 @@ void ObjectTemplate::readMsk() void ObjectTemplate::readMap(CBinaryReader & reader) { - animationFile = reader.readBaseString(); + animationFile = AnimationPath::builtin(reader.readBaseString()); setSize(8, 6); ui8 blockMask[6]; @@ -251,8 +249,8 @@ void ObjectTemplate::readMap(CBinaryReader & reader) void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) { - animationFile = node["animation"].String(); - editorAnimationFile = node["editorAnimation"].String(); + animationFile = AnimationPath::fromJson(node["animation"]); + editorAnimationFile = AnimationPath::fromJson(node["editorAnimation"]); const JsonVector & visitDirs = node["visitableFrom"].Vector(); if (!visitDirs.empty()) @@ -325,8 +323,8 @@ void ObjectTemplate::readJson(const JsonNode &node, const bool withTerrain) void ObjectTemplate::writeJson(JsonNode & node, const bool withTerrain) const { - node["animation"].String() = animationFile; - node["editorAnimation"].String() = editorAnimationFile; + node["animation"].String() = animationFile.getOriginalName(); + node["editorAnimation"].String() = editorAnimationFile.getOriginalName(); if(visitDir != 0x0 && isVisitable()) { @@ -577,7 +575,7 @@ void ObjectTemplate::recalculate() calculateTopVisibleOffset(); if (visitable && visitDir == 0) - logMod->warn("Template for %s is visitable but has no visitable directions!", animationFile); + logMod->warn("Template for %s is visitable but has no visitable directions!", animationFile.getOriginalName()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 4560d75ea..3f967d76d 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -11,6 +11,7 @@ #include "../GameConstants.h" #include "../int3.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -47,10 +48,10 @@ public: /// print priority, objects with higher priority will be print first, below everything else si32 printPriority; /// animation file that should be used to display object - std::string animationFile; + AnimationPath animationFile; /// map editor only animation file - std::string editorAnimationFile; + AnimationPath editorAnimationFile; /// string ID, equals to def base name for h3m files (lower case, no extension) or specified in mod data std::string stringID; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 64796bb6f..e36ba25c2 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -12,7 +12,7 @@ #include -#include "../filesystem/ResourceID.h" +#include "../filesystem/ResourcePath.h" #include "../StartInfo.h" #include "../GameConstants.h" #include "CMapService.h" @@ -45,14 +45,14 @@ void CMapInfo::mapInit(const std::string & fname) { fileURI = fname; CMapService mapService; - ResourceID resource = ResourceID(fname, EResType::MAP); + ResourcePath resource = ResourcePath(fname, EResType::MAP); originalFileURI = resource.getOriginalName(); fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); mapHeader = mapService.loadMapHeader(resource); countPlayers(); } -void CMapInfo::saveInit(const ResourceID & file) +void CMapInfo::saveInit(const ResourcePath & file) { CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); lf.checkMagicBytes(SAVEGAME_MAGIC); @@ -73,7 +73,7 @@ void CMapInfo::saveInit(const ResourceID & file) void CMapInfo::campaignInit() { - ResourceID resource = ResourceID(fileURI, EResType::CAMPAIGN); + ResourcePath resource = ResourcePath(fileURI, EResType::CAMPAIGN); originalFileURI = resource.getOriginalName(); fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); campaign = CampaignHandler::getHeader(fileURI); diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 219969c4a..33d3874a1 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -15,7 +15,7 @@ struct StartInfo; class CMapHeader; class Campaign; -class ResourceID; +class ResourcePath; /** * A class which stores the count of human players and all players, the filename, @@ -46,7 +46,7 @@ public: CMapInfo &operator=(const CMapInfo &other) = delete; void mapInit(const std::string & fname); - void saveInit(const ResourceID & file); + void saveInit(const ResourcePath & file); void campaignInit(); void countPlayers(); // TODO: Those must be on client-side diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index e8a50e8d7..c17db6fc2 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -30,7 +30,7 @@ VCMI_LIB_NAMESPACE_BEGIN -std::unique_ptr CMapService::loadMap(const ResourceID & name) const +std::unique_ptr CMapService::loadMap(const ResourcePath & name) const { std::string modName = VLC->modh->findResourceOrigin(name); std::string language = VLC->modh->getModLanguage(modName); @@ -40,7 +40,7 @@ std::unique_ptr CMapService::loadMap(const ResourceID & name) const return getMapLoader(stream, name.getName(), modName, encoding)->loadMap(); } -std::unique_ptr CMapService::loadMapHeader(const ResourceID & name) const +std::unique_ptr CMapService::loadMapHeader(const ResourcePath & name) const { std::string modName = VLC->modh->findResourceOrigin(name); std::string language = VLC->modh->getModLanguage(modName); @@ -108,7 +108,7 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) return modCompatibilityInfo; } -std::unique_ptr CMapService::getStreamFromFS(const ResourceID & name) +std::unique_ptr CMapService::getStreamFromFS(const ResourcePath & name) { return CResourceHandler::get()->load(name); } diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index 27c947a7d..9a8a33c7f 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -12,7 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN -class ResourceID; +class ResourcePath; class CMap; class CMapHeader; @@ -39,7 +39,7 @@ public: * @param name the name of the map * @return a unique ptr to the loaded map class */ - virtual std::unique_ptr loadMap(const ResourceID & name) const = 0; + virtual std::unique_ptr loadMap(const ResourcePath & name) const = 0; /** * Loads the VCMI/H3 map header specified by the name. @@ -47,7 +47,7 @@ public: * @param name the name of the map * @return a unique ptr to the loaded map header class */ - virtual std::unique_ptr loadMapHeader(const ResourceID & name) const = 0; + virtual std::unique_ptr loadMapHeader(const ResourcePath & name) const = 0; /** * Loads the VCMI/H3 map file from a buffer. This method is temporarily @@ -81,8 +81,8 @@ public: CMapService() = default; virtual ~CMapService() = default; - std::unique_ptr loadMap(const ResourceID & name) const override; - std::unique_ptr loadMapHeader(const ResourceID & name) const override; + std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMapHeader(const ResourcePath & name) const override; std::unique_ptr loadMap(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; std::unique_ptr loadMapHeader(const uint8_t * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; void saveMap(const std::unique_ptr & map, boost::filesystem::path fullPath) const override; @@ -101,7 +101,7 @@ private: * @param name the name of the map * @return a unique ptr to the input stream class */ - static std::unique_ptr getStreamFromFS(const ResourceID & name); + static std::unique_ptr getStreamFromFS(const ResourcePath & name); /** * Gets a map input stream from a buffer. diff --git a/lib/mapping/MapEditUtils.cpp b/lib/mapping/MapEditUtils.cpp index e6ff84f86..483abf928 100644 --- a/lib/mapping/MapEditUtils.cpp +++ b/lib/mapping/MapEditUtils.cpp @@ -174,7 +174,7 @@ void TerrainViewPattern::WeightedRule::setNative() CTerrainViewPatternConfig::CTerrainViewPatternConfig() { - const JsonNode config(ResourceID("config/terrainViewPatterns.json")); + const JsonNode config(ResourcePath("config/terrainViewPatterns.json")); static const std::string patternTypes[] = { "terrainView", "terrainType" }; for (int i = 0; i < std::size(patternTypes); ++i) { diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index cd8d341a6..e7040ce5d 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -984,8 +984,8 @@ void CMapLoaderH3M::readObjectTemplates() auto tmpl = reader->readObjectTemplate(); templates.push_back(tmpl); - if (!CResourceHandler::get()->existsResource(ResourceID( "SPRITES/" + tmpl->animationFile, EResType::ANIMATION))) - logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile, tmpl->id, tmpl->subid ); + if (!CResourceHandler::get()->existsResource(tmpl->animationFile)) + logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); } } @@ -1328,7 +1328,7 @@ CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::sha if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); - logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile, mapPosition.toString()); + logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString()); return new CGObjectInstance(); } diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index e8bd5fbe1..3190ea6e8 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -904,7 +904,7 @@ std::unique_ptr CMapLoaderJson::loadMapHeader() JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) { - ResourceID resource(archiveFilename, EResType::TEXT); + ResourcePath resource(archiveFilename, EResType::TEXT); if(!loader.existsResource(resource)) throw std::runtime_error(archiveFilename+" not found"); diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 0c327939b..2a50df036 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -56,7 +56,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); std::string vcmiName = boost::to_lower_copy(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(ResourceID( "SPRITES/" + vcmiName, EResType::ANIMATION))) + if (!CResourceHandler::get()->existsResource(ResourcePath( "SPRITES/" + vcmiName, EResType::ANIMATION))) logMod->warn("Template animation file %s was not found!", vcmiName); mappingObjectTemplate[h3mName] = vcmiName; @@ -108,10 +108,10 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) void MapIdentifiersH3M::remapTemplate(ObjectTemplate & objectTemplate) { - std::string name = boost::to_lower_copy(objectTemplate.animationFile); + std::string name = boost::to_lower_copy(objectTemplate.animationFile.getName()); if (mappingObjectTemplate.count(name)) - objectTemplate.animationFile = mappingObjectTemplate.at(name); + objectTemplate.animationFile = AnimationPath::builtinTODO(mappingObjectTemplate.at(name)); ObjectTypeIdentifier objectType{ objectTemplate.id, objectTemplate.subid}; diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index a26ab4248..a43c91666 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -30,9 +30,9 @@ VCMI_LIB_NAMESPACE_BEGIN static JsonNode loadModSettings(const std::string & path) { - if (CResourceHandler::get("local")->existsResource(ResourceID(path))) + if (CResourceHandler::get("local")->existsResource(ResourcePath(path))) { - return JsonNode(ResourceID(path, EResType::TEXT)); + return JsonNode(ResourcePath(path, EResType::TEXT)); } // Probably new install. Create initial configuration CResourceHandler::get("local")->createResource(path); @@ -157,7 +157,7 @@ std::vector CModHandler::getModList(const std::string & path) const std::string modDir = boost::to_upper_copy(path + "MODS/"); size_t depth = boost::range::count(modDir, '/'); - auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourceID & id) -> bool + auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool { if (id.getType() != EResType::DIRECTORY) return false; @@ -200,9 +200,9 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co return; } - if(CResourceHandler::get("initial")->existsResource(ResourceID(CModInfo::getModFile(modFullName)))) + if(CResourceHandler::get("initial")->existsResource(ResourcePath(CModInfo::getModFile(modFullName)))) { - CModInfo mod(modFullName, modSettings[modName], JsonNode(ResourceID(CModInfo::getModFile(modFullName)))); + CModInfo mod(modFullName, modSettings[modName], JsonNode(ResourcePath(CModInfo::getModFile(modFullName)))); if (!parent.empty()) // this is submod, add parent to dependencies mod.dependencies.insert(parent); @@ -228,7 +228,7 @@ void CModHandler::loadMods(bool onlyEssential) loadMods("", "", modConfig["activeMods"], true); } - coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(ResourceID("config/gameConfig.json"))); + coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(ResourcePath("config/gameConfig.json"))); coreMod->name = "Original game files"; } @@ -283,19 +283,19 @@ static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoa // FIXME: remove workaround for core mod if (modName != ModScope::scopeBuiltin()) { - ResourceID modConfFile(CModInfo::getModFile(modName), EResType::TEXT); + ResourcePath modConfFile(CModInfo::getModFile(modName), EResType::TEXT); ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); } // third - add all detected text files from this mod into checksum - auto files = filesystem->getFilteredFiles([](const ResourceID & resID) + auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) { return resID.getType() == EResType::TEXT && ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); }); - for (const ResourceID & file : files) + for (const ResourcePath & file : files) { ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); @@ -318,7 +318,7 @@ void CModHandler::loadModFilesystems() } } -TModID CModHandler::findResourceOrigin(const ResourceID & name) +TModID CModHandler::findResourceOrigin(const ResourcePath & name) { for(const auto & modID : boost::adaptors::reverse(activeMods)) { @@ -483,7 +483,7 @@ void CModHandler::afterLoad(bool onlyEssential) if(!onlyEssential) { - std::fstream file(CResourceHandler::get()->getResourceName(ResourceID("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << modSettings.toJson(); } diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index eaa84d84a..2fbc33fb6 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -20,7 +20,7 @@ class JsonNode; class IHandlerBase; class CIdentifierStorage; class CContentHandler; -class ResourceID; +class ResourcePath; using TModID = std::string; @@ -67,7 +67,7 @@ public: void loadModFilesystems(); /// returns ID of mod that provides selected file resource - TModID findResourceOrigin(const ResourceID & name); + TModID findResourceOrigin(const ResourcePath & name); std::string getModLanguage(const TModID & modId) const; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 4baab342a..7fb5fd48a 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -152,7 +152,7 @@ bool CModInfo::checkModGameplayAffecting() const "obstacles" }; - ResourceID modFileResource(CModInfo::getModFile(identifier)); + ResourcePath modFileResource(CModInfo::getModFile(identifier)); if(CResourceHandler::get("initial")->existsResource(modFileResource)) { diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index d526d17c5..d18e7b5c4 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -51,7 +51,7 @@ int CMapGenerator::getRandomSeed() const void CMapGenerator::loadConfig() { - static const ResourceID path("config/randomMap.json"); + static const ResourcePath path("config/randomMap.json"); JsonNode randomMapJson(path); config.shipyardGuard = randomMapJson["waterZone"]["shipyard"]["value"].Integer(); diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 3df3f1d03..0738bdc72 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -390,7 +390,7 @@ void RiverPlacer::connectRiver(const int3 & tile) throw rmgException(boost::str(boost::format("River templates for (%d,%d) at terrain %s, river %s are incorrect") % RIVER_DELTA_ID % RIVER_DELTA_SUBTYPE % zone.getTerrainType() % river->shortIdentifier)); - std::string targetTemplateName = river->deltaName + std::to_string(deltaOrientations[pos]) + ".def"; + AnimationPath targetTemplateName = AnimationPath::builtinTODO(river->deltaName + std::to_string(deltaOrientations[pos]) + ".def"); for(auto & templ : tmplates) { if(templ->animationFile == targetTemplateName) diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index f048d36c5..fdb16c975 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -516,9 +516,9 @@ CSpell::AnimationItem::AnimationItem() : } ///CSpell::AnimationInfo -std::string CSpell::AnimationInfo::selectProjectile(const double angle) const +AnimationPath CSpell::AnimationInfo::selectProjectile(const double angle) const { - std::string res; + AnimationPath res; double maximum = 0.0; for(const auto & info : projectile) @@ -861,10 +861,10 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & CSpell::TAnimation newItem; if(item.getType() == JsonNode::JsonType::DATA_STRING) - newItem.resourceName = item.String(); + newItem.resourceName = AnimationPath::fromJson(item); else if(item.getType() == JsonNode::JsonType::DATA_STRUCT) { - newItem.resourceName = item["defName"].String(); + newItem.resourceName = AnimationPath::fromJson(item["defName"]); newItem.effectName = item["effectName"].String(); auto vPosStr = item["verticalPosition"].String(); @@ -889,7 +889,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & for(const JsonNode & item : projectile) { CSpell::ProjectileInfo info; - info.resourceName = item["defName"].String(); + info.resourceName = AnimationPath::fromJson(item["defName"]); info.minimumAngle = item["minimumAngle"].Float(); spell->animationInfo.projectile.push_back(info); diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index c4a419c27..9d8bea267 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -20,6 +20,7 @@ #include "../GameConstants.h" #include "../battle/BattleHex.h" #include "../bonuses/Bonus.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -65,7 +66,7 @@ public: double minimumAngle; ///resource name - std::string resourceName; + AnimationPath resourceName; template void serialize(Handler & h, const int version) { @@ -76,7 +77,7 @@ public: struct AnimationItem { - std::string resourceName; + AnimationPath resourceName; std::string effectName; VerticalPosition verticalPosition; int pause; @@ -119,7 +120,7 @@ public: h & affect; } - std::string selectProjectile(const double angle) const; + AnimationPath selectProjectile(const double angle) const; } animationInfo; public: diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 5a3247694..428e065e3 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -86,8 +86,8 @@ void ObstacleSideOptions::serializeJson(JsonSerializeFormat & handler) serializeRelativeShape(handler, "range", range); handler.serializeString("appearSound", appearSound); - handler.serializeString("appearAnimation", appearAnimation); - handler.serializeString("animation", animation); + handler.serializeStruct("appearAnimation", appearAnimation); + handler.serializeStruct("animation", animation); handler.serializeInt("offsetY", offsetY); } diff --git a/lib/spells/effects/Obstacle.h b/lib/spells/effects/Obstacle.h index 4bf9159b3..a1e9b1f56 100644 --- a/lib/spells/effects/Obstacle.h +++ b/lib/spells/effects/Obstacle.h @@ -31,8 +31,8 @@ public: RelativeShape range; //position of obstacles relative to effect destination std::string appearSound; - std::string appearAnimation; - std::string animation; + AnimationPath appearAnimation; + AnimationPath animation; int offsetY = 0; diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 45d003f24..80795d02a 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -81,7 +81,7 @@ class FileCache static const int cacheSize = 50; //Max number of cached files struct FileData { - ResourceID name; + ResourcePath name; size_t size; std::unique_ptr data; @@ -91,7 +91,7 @@ class FileCache std::copy(data.get(), data.get() + size, ret.get()); return ret; } - FileData(ResourceID name_, size_t size_, std::unique_ptr data_): + FileData(ResourcePath name_, size_t size_, std::unique_ptr data_): name{std::move(name_)}, size{size_}, data{std::move(data_)} @@ -100,7 +100,7 @@ class FileCache std::deque cache; public: - std::unique_ptr getCachedFile(ResourceID rid) + std::unique_ptr getCachedFile(ResourcePath rid) { for(auto & file : cache) { @@ -169,7 +169,7 @@ DefFile::DefFile(std::string Name): qRgba(0, 0, 0, 128), // 50% - shadow body below selection qRgba(0, 0, 0, 64) // 75% - shadow border below selection }; - data = animationCache.getCachedFile(ResourceID(std::string("SPRITES/") + Name, EResType::ANIMATION)); + data = animationCache.getCachedFile(ResourcePath(std::string("SPRITES/") + Name, EResType::ANIMATION)); palette = std::make_unique>(256); int it = 0; @@ -583,7 +583,7 @@ void Animation::init() source[defEntry.first].resize(defEntry.second); } - ResourceID resID(std::string("SPRITES/") + name, EResType::TEXT); + ResourcePath resID(std::string("SPRITES/") + name, EResType::TEXT); //if(vstd::contains(graphics->imageLists, resID.getName())) //initFromJson(graphics->imageLists[resID.getName()]); @@ -656,7 +656,7 @@ Animation::Animation(std::string Name): name.erase(dotPos); std::transform(name.begin(), name.end(), name.begin(), toupper); - ResourceID resource(std::string("SPRITES/") + name, EResType::ANIMATION); + ResourcePath resource(std::string("SPRITES/") + name, EResType::ANIMATION); if(CResourceHandler::get()->existsResource(resource)) defFile = std::make_shared(name); diff --git a/mapeditor/BitmapHandler.cpp b/mapeditor/BitmapHandler.cpp index 79034c88c..67c5d4224 100644 --- a/mapeditor/BitmapHandler.cpp +++ b/mapeditor/BitmapHandler.cpp @@ -92,13 +92,13 @@ namespace BitmapHandler logGlobal->warn("Call to loadBitmap with void fname!"); return QImage(); } - if(!CResourceHandler::get()->existsResource(ResourceID(path + fname, EResType::IMAGE))) + if(!CResourceHandler::get()->existsResource(ResourcePath(path + fname, EResType::IMAGE))) { return QImage(); } - auto fullpath = CResourceHandler::get()->getResourceName(ResourceID(path + fname, EResType::IMAGE)); - auto readFile = CResourceHandler::get()->load(ResourceID(path + fname, EResType::IMAGE))->readAll(); + auto fullpath = CResourceHandler::get()->getResourceName(ResourcePath(path + fname, EResType::IMAGE)); + auto readFile = CResourceHandler::get()->load(ResourcePath(path + fname, EResType::IMAGE))->readAll(); if(isPCX(readFile.first.get())) {//H3-style PCX diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index ce797921c..0466a56ca 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -40,7 +40,7 @@ Graphics * graphics = nullptr; void Graphics::loadPaletteAndColors() { - auto textFile = CResourceHandler::get()->load(ResourceID("DATA/PLAYERS.PAL"))->readAll(); + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); std::string pals((char*)textFile.first.get(), textFile.second); playerColorPalette.resize(256); @@ -59,7 +59,7 @@ void Graphics::loadPaletteAndColors() neutralColorPalette.resize(32); - auto stream = CResourceHandler::get()->load(ResourceID("config/NEUTRAL.PAL")); + auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); CBinaryReader reader(stream.get()); for(int i = 0; i < 32; ++i) @@ -129,8 +129,8 @@ void Graphics::loadHeroAnimations() { for(auto templ : VLC->objtypeh->getHandlerFor(Obj::HERO, elem->getIndex())->getTemplates()) { - if(!heroAnimations.count(templ->animationFile)) - heroAnimations[templ->animationFile] = loadHeroAnimation(templ->animationFile); + if(!heroAnimations.count(templ->animationFile.getName())) + heroAnimations[templ->animationFile.getName()] = loadHeroAnimation(templ->animationFile.getName()); } } @@ -270,7 +270,7 @@ std::shared_ptr Graphics::getHeroAnimation(const std::shared_ptr(); } - std::shared_ptr ret = loadHeroAnimation(info->animationFile); + std::shared_ptr ret = loadHeroAnimation(info->animationFile.getName()); //already loaded if(ret) @@ -279,8 +279,8 @@ std::shared_ptr Graphics::getHeroAnimation(const std::shared_ptr(info->animationFile); - heroAnimations[info->animationFile] = ret; + ret = std::make_shared(info->animationFile.getOriginalName()); + heroAnimations[info->animationFile.getName()] = ret; ret->preload(); return ret; @@ -294,7 +294,7 @@ std::shared_ptr Graphics::getAnimation(const std::shared_ptr(); } - std::shared_ptr ret = mapObjectAnimations[info->animationFile]; + std::shared_ptr ret = mapObjectAnimations[info->animationFile.getName()]; //already loaded if(ret) @@ -303,8 +303,8 @@ std::shared_ptr Graphics::getAnimation(const std::shared_ptr(info->animationFile); - mapObjectAnimations[info->animationFile] = ret; + ret = std::make_shared(info->animationFile.getOriginalName()); + mapObjectAnimations[info->animationFile.getName()] = ret; ret->preload(); return ret; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index bb99703e6..4b481ac72 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -181,7 +181,7 @@ MainWindow::MainWindow(QWidget* parent) : // Some basic data validation to produce better error messages in cases of incorrect install auto testFile = [](std::string filename, std::string message) -> bool { - if (CResourceHandler::get()->existsResource(ResourceID(filename))) + if (CResourceHandler::get()->existsResource(ResourcePath(filename))) return true; logGlobal->error("Error: %s was not found!", message); @@ -317,7 +317,7 @@ bool MainWindow::openMap(const QString & filenameSelect) std::string fname = fi.fileName().toStdString(); std::string fdir = fi.dir().path().toStdString(); - ResourceID resId("MAPEDITOR/" + fname, EResType::MAP); + ResourcePath resId("MAPEDITOR/" + fname, EResType::MAP); //addFilesystem takes care about memory deallocation if case of failure, no memory leak here auto * mapEditorFilesystem = new CFilesystemLoader("MAPEDITOR/", fdir, 0); @@ -512,13 +512,13 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust auto templ = templates[templateId]; //selecting file - const std::string & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile; + const AnimationPath & afile = templ->editorAnimationFile.empty() ? templ->animationFile : templ->editorAnimationFile; //creating picture QPixmap preview(128, 128); preview.fill(QColor(255, 255, 255)); QPainter painter(&preview); - Animation animation(afile); + Animation animation(afile.getOriginalName()); animation.preload(); auto picture = animation.getImage(0); if(picture && picture->width() && picture->height()) @@ -533,8 +533,8 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust QJsonObject data{{"id", QJsonValue(ID)}, {"subid", QJsonValue(secondaryID)}, {"template", QJsonValue(templateId)}, - {"animationEditor", QString::fromStdString(templ->editorAnimationFile)}, - {"animation", QString::fromStdString(templ->animationFile)}, + {"animationEditor", QString::fromStdString(templ->editorAnimationFile.getOriginalName())}, + {"animation", QString::fromStdString(templ->animationFile.getOriginalName())}, {"preview", jsonFromPixmap(preview)}}; //create object to extract name diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index f5f3790ed..891d83901 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -81,15 +81,15 @@ void MapHandler::initTerrainGraphics() std::map riverFiles; for(const auto & terrain : VLC->terrainTypeHandler->objects) { - terrainFiles[terrain->getJsonKey()] = terrain->tilesFilename; + terrainFiles[terrain->getJsonKey()] = terrain->tilesFilename.getName(); } for(const auto & river : VLC->riverTypeHandler->objects) { - riverFiles[river->getJsonKey()] = river->tilesFilename; + riverFiles[river->getJsonKey()] = river->tilesFilename.getName(); } for(const auto & road : VLC->roadTypeHandler->objects) { - roadFiles[road->getJsonKey()] = road->tilesFilename; + roadFiles[road->getJsonKey()] = road->tilesFilename.getName(); } loadFlipped(terrainAnimations, terrainImages, terrainFiles); diff --git a/mapeditor/resourceExtractor/ResourceConverter.cpp b/mapeditor/resourceExtractor/ResourceConverter.cpp index f413c40dc..a6481ed58 100644 --- a/mapeditor/resourceExtractor/ResourceConverter.cpp +++ b/mapeditor/resourceExtractor/ResourceConverter.cpp @@ -69,7 +69,7 @@ void ResourceConverter::doConvertPcxToPng(const boost::filesystem::path & source void ResourceConverter::splitDefFile(const std::string & fileName, const boost::filesystem::path & sourceFolder, bool deleteOriginals) { - if(CResourceHandler::get()->existsResource(ResourceID("SPRITES/" + fileName))) + if(CResourceHandler::get()->existsResource(ResourcePath("SPRITES/" + fileName))) { std::unique_ptr anim = std::make_unique(fileName); anim->preload(); diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 4b3508610..8759ad7b3 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -522,7 +522,7 @@ int LuaContext::loadModule() modulePath = "scripts/lib/" + modulePath; - ResourceID id(modulePath, EResType::LUA); + ResourcePath id(modulePath, EResType::LUA); if(!loader->existsResource(id)) return errorRetVoid("Module not found: "+modulePath); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index d6f700b6e..b60723b28 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1722,7 +1722,7 @@ void CGameHandler::save(const std::string & filename) try { { - CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME))); + CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME))); saveCommonState(save); logGlobal->info("Saving server state"); save << *this; @@ -1745,7 +1745,7 @@ bool CGameHandler::load(const std::string & filename) try { { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourceID(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); loadCommonState(lf); logGlobal->info("Loading server state"); lf >> *this; diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 4b41d2e3e..52786cc63 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -23,7 +23,7 @@ #include "../../lib/battle/BattleInfo.h" #include "../../lib/CStack.h" -#include "../../lib/filesystem/ResourceID.h" +#include "../../lib/filesystem/ResourcePath.h" #include "../../lib/mapping/CMap.h" @@ -146,7 +146,7 @@ public: si.mode = StartInfo::NEW_GAME; si.seedToBeUsed = 42; - std::unique_ptr header = mapService.loadMapHeader(ResourceID(si.mapname)); + std::unique_ptr header = mapService.loadMapHeader(ResourcePath(si.mapname)); ASSERT_NE(header.get(), nullptr); diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index 0b29d06ef..048c2f5bd 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" -#include "../lib/filesystem/ResourceID.h" +#include "../lib/filesystem/ResourcePath.h" #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "../lib/TerrainHandler.h" @@ -111,7 +111,7 @@ TEST(MapManager, DrawTerrain_View) { try { - const ResourceID testMap("test/TerrainViewTest", EResType::MAP); + const ResourcePath testMap("test/TerrainViewTest", EResType::MAP); // Load maps and json config CMapService mapService; const auto originalMap = mapService.loadMap(testMap); @@ -120,7 +120,7 @@ TEST(MapManager, DrawTerrain_View) // Validate edit manager auto editManager = map->getEditManager(); CRandomGenerator gen; - const JsonNode viewNode(ResourceID("test/terrainViewMappings", EResType::TEXT)); + const JsonNode viewNode(ResourcePath("test/terrainViewMappings", EResType::TEXT)); const auto & mappingsNode = viewNode["mappings"].Vector(); for (const auto & node : mappingsNode) { diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index c1acc6b35..030cfd20a 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -90,7 +90,7 @@ TEST(MapFormat, Random) static JsonNode getFromArchive(CZipLoader & archive, const std::string & archiveFilename) { - ResourceID resource(archiveFilename, EResType::TEXT); + ResourcePath resource(archiveFilename, EResType::TEXT); if(!archive.existsResource(resource)) throw std::runtime_error(archiveFilename + " not found"); @@ -153,14 +153,14 @@ TEST(MapFormat, Objects) { static const std::string MAP_DATA_PATH = "test/ObjectPropertyTest/"; - const JsonNode initialHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now + const JsonNode initialHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now - const JsonNode initialObjects(ResourceID(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedObjects(ResourceID(MAP_DATA_PATH + "objects.ex.json")); + const JsonNode initialObjects(ResourcePath(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedObjects(ResourcePath(MAP_DATA_PATH + "objects.ex.json")); - const JsonNode expectedSurface(ResourceID(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourceID(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(ResourcePath(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(ResourcePath(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(initialHeader, initialObjects, expectedSurface, expectedUnderground); @@ -192,11 +192,11 @@ TEST(MapFormat, Terrain) { static const std::string MAP_DATA_PATH = "test/TerrainTest/"; - const JsonNode expectedHeader(ResourceID(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedObjects(ResourceID(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedObjects(ResourcePath(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedSurface(ResourceID(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourceID(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(ResourcePath(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(ResourcePath(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(expectedHeader, expectedObjects, expectedSurface, expectedUnderground); diff --git a/test/mock/mock_MapService.cpp b/test/mock/mock_MapService.cpp index 1b85b9edf..8e0c83591 100644 --- a/test/mock/mock_MapService.cpp +++ b/test/mock/mock_MapService.cpp @@ -23,15 +23,15 @@ MapServiceMock::MapServiceMock(const std::string & path, MapListener * mapListen CZipSaver saver(io, "_"); - const JsonNode header(ResourceID(path+CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode objects(ResourceID(path+CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode surface(ResourceID(path+"surface_terrain.json")); + const JsonNode header(ResourcePath(path+CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode objects(ResourcePath(path+CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode surface(ResourcePath(path+"surface_terrain.json")); addToArchive(saver, header, CMapFormatJson::HEADER_FILE_NAME); addToArchive(saver, objects, CMapFormatJson::OBJECTS_FILE_NAME); addToArchive(saver, surface, "surface_terrain.json"); - ResourceID undergroundPath(path+"underground_terrain.json"); + ResourcePath undergroundPath(path+"underground_terrain.json"); if(CResourceHandler::get()->existsResource(undergroundPath)) { @@ -53,12 +53,12 @@ std::unique_ptr MapServiceMock::loadMap() const return res; } -std::unique_ptr MapServiceMock::loadMap(const ResourceID & name) const +std::unique_ptr MapServiceMock::loadMap(const ResourcePath & name) const { return loadMap(); } -std::unique_ptr MapServiceMock::loadMapHeader(const ResourceID & name) const +std::unique_ptr MapServiceMock::loadMapHeader(const ResourcePath & name) const { initialBuffer.seek(0); CMapLoaderJson initialLoader(&initialBuffer); diff --git a/test/mock/mock_MapService.h b/test/mock/mock_MapService.h index 656b3c771..cf2da28a9 100644 --- a/test/mock/mock_MapService.h +++ b/test/mock/mock_MapService.h @@ -29,8 +29,8 @@ class MapServiceMock : public IMapService public: MapServiceMock(const std::string & path, MapListener * mapListener_); - std::unique_ptr loadMap(const ResourceID & name) const override; - std::unique_ptr loadMapHeader(const ResourceID & name) const override; + std::unique_ptr loadMap(const ResourcePath & name) const override; + std::unique_ptr loadMapHeader(const ResourcePath & name) const override; std::unique_ptr loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; std::unique_ptr loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override; From 6f0108e462cac0fabbbc42b488b936c8c0720eb1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 2 Sep 2023 00:26:14 +0300 Subject: [PATCH 0315/1248] Use ResourcePath for referencing texts and json's --- client/CMusicHandler.cpp | 2 +- client/adventureMap/AdventureMapWidget.cpp | 2 +- client/adventureMap/TurnTimerWidget.cpp | 2 +- client/battle/BattleEffectsController.cpp | 2 +- client/battle/BattleWindow.cpp | 2 +- client/gui/InterfaceObjectConfigurable.cpp | 2 +- client/lobby/OptionsTab.cpp | 2 +- client/lobby/RandomMapTab.cpp | 4 +-- client/mainmenu/CMainMenu.cpp | 3 ++- client/render/Graphics.cpp | 6 ++--- .../windows/settings/AdventureOptionsTab.cpp | 2 +- client/windows/settings/BattleOptionsTab.cpp | 2 +- client/windows/settings/GeneralOptionsTab.cpp | 2 +- client/windows/settings/OtherOptionsTab.cpp | 2 +- .../windows/settings/SettingsMainWindow.cpp | 2 +- launcher/modManager/cmodmanager.cpp | 2 +- lib/CArtHandler.cpp | 4 +-- lib/CBonusTypeHandler.cpp | 2 +- lib/CConfigHandler.cpp | 6 ++--- lib/CCreatureHandler.cpp | 10 +++---- lib/CGeneralTextHandler.cpp | 17 ++++++------ lib/CGeneralTextHandler.h | 4 ++- lib/CHeroHandler.cpp | 8 +++--- lib/CSkillHandler.cpp | 2 +- lib/CTownHandler.cpp | 16 +++++------ lib/JsonNode.cpp | 27 +++++++------------ lib/JsonNode.h | 9 +++---- lib/TerrainHandler.cpp | 2 +- lib/battle/BattleInfo.cpp | 2 +- lib/campaign/CampaignHandler.cpp | 4 +-- lib/campaign/CampaignState.cpp | 2 +- lib/filesystem/AdapterLoaders.cpp | 4 +-- lib/filesystem/AdapterLoaders.h | 2 +- lib/filesystem/CFilesystemLoader.cpp | 4 +-- lib/filesystem/CFilesystemLoader.h | 2 +- lib/filesystem/ISimpleResourceLoader.h | 2 +- lib/filesystem/ResourcePath.h | 6 ++++- lib/gameState/CGameState.cpp | 2 +- .../CObjectClassesHandler.cpp | 8 +++--- lib/mapObjects/CObjectHandler.cpp | 2 +- lib/mapping/CMapService.cpp | 4 +-- lib/mapping/MapEditUtils.cpp | 2 +- lib/modding/CModHandler.cpp | 14 +++++----- lib/modding/CModInfo.cpp | 6 ++--- lib/modding/CModInfo.h | 2 +- lib/rmg/CMapGenerator.cpp | 3 +-- lib/spells/CSpellHandler.cpp | 2 +- server/CGameHandler.cpp | 5 ++-- test/map/CMapEditManagerTest.cpp | 2 +- test/map/CMapFormatTest.cpp | 20 +++++++------- test/mock/mock_MapService.cpp | 8 +++--- 51 files changed, 124 insertions(+), 130 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 57a3c5aad..0b74e73ca 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -75,7 +75,7 @@ void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) CSoundHandler::CSoundHandler(): listener(settings.listen["general"]["sound"]), - ambientConfig(JsonNode(ResourcePath("config/ambientSounds.json"))) + ambientConfig(JsonPath::builtin("config/ambientSounds.json")) { listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 1af8cdb8d..ebff69812 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -56,7 +56,7 @@ AdventureMapWidget::AdventureMapWidget( std::shared_ptr s for (const auto & entry : shortcuts->getShortcuts()) addShortcut(entry.shortcut, entry.callback); - const JsonNode config(ResourcePath("config/widgets/adventureMap.json")); + const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) { diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 2550ef23d..1530713d1 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -47,7 +47,7 @@ TurnTimerWidget::TurnTimerWidget(): recActions &= ~DEACTIVATE; - const JsonNode config(ResourcePath("config/widgets/turnTimer.json")); + const JsonNode config(JsonPath::builtin("config/widgets/turnTimer.json")); build(config); diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index eb3d2e419..178b15196 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -132,7 +132,7 @@ void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer void BattleEffectsController::loadColorMuxers() { - const JsonNode config(ResourcePath("config/battleEffects.json")); + const JsonNode config(JsonPath::builtin("config/battleEffects.json")); for(auto & muxer : config["colorMuxers"].Struct()) { diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 4e62ea056..7595df164 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner): REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - const JsonNode config(ResourcePath("config/widgets/BattleWindow2.json")); + const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index e3d9a8a34..d27d97b45 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -110,7 +110,7 @@ void InterfaceObjectConfigurable::build(const JsonNode &config) { if (!config["library"].isNull()) { - const JsonNode library(ResourcePath(config["library"].String())); + const JsonNode library(JsonPath::fromJson(config["library"])); loadCustomBuilders(library); } diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 48f675425..78c5c141a 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -137,7 +137,7 @@ OptionsTab::OptionsTab() : humanPlayers(0) } }); - const JsonNode config(ResourcePath("config/widgets/optionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/optionsTab.json")); build(config); //set timers combo box callbacks diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index f116104bc..aaaacea86 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -118,7 +118,7 @@ RandomMapTab::RandomMapTab(): }); } - const JsonNode config(ResourcePath("config/widgets/randomMapTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/randomMapTab.json")); build(config); //set combo box callbacks @@ -388,7 +388,7 @@ std::vector RandomMapTab::getPossibleMapSizes() TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { - const JsonNode config(ResourcePath("config/widgets/randomMapTeamsWidget.json")); + const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; int humanPlayers = randomMapTab.obtainMapGenOptions().getPlayerCount(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index bd6edd96a..3f0a121bf 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -262,7 +262,8 @@ CMenuEntry::CMenuEntry(CMenuScreen * parent, const JsonNode & config) } CMainMenuConfig::CMainMenuConfig() - : campaignSets(JsonNode(ResourcePath("config/campaignSets.json"))), config(JsonNode(ResourcePath("config/mainmenu.json"))) + : campaignSets(JsonPath::builtin("config/campaignSets.json")) + , config(JsonPath::builtin("config/mainmenu.json")) { } diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index c23751484..cf5c23ce9 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -105,7 +105,7 @@ void Graphics::initializeBattleGraphics() if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) continue; - const JsonNode config(mod, ResourcePath("config/battles_graphics.json")); + const JsonNode config(mod, JsonPath::builtin("config/battles_graphics.json")); //initialization of AC->def name mapping if(!config["ac_mapping"].isNull()) @@ -204,7 +204,7 @@ void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) void Graphics::loadFonts() { - const JsonNode config(ResourcePath("config/fonts.json")); + const JsonNode config(JsonPath::builtin("config/fonts.json")); const JsonVector & bmpConf = config["bitmap"].Vector(); const JsonNode & ttfConf = config["trueType"]; @@ -228,7 +228,7 @@ void Graphics::loadFonts() void Graphics::loadErmuToPicture() { //loading ERMU to picture - const JsonNode config(ResourcePath("config/ERMU_to_picture.json")); + const JsonNode config(JsonPath::builtin("config/ERMU_to_picture.json")); int etp_idx = 0; for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { int idx = 0; diff --git a/client/windows/settings/AdventureOptionsTab.cpp b/client/windows/settings/AdventureOptionsTab.cpp index d48b8dcc1..90db19c93 100644 --- a/client/windows/settings/AdventureOptionsTab.cpp +++ b/client/windows/settings/AdventureOptionsTab.cpp @@ -44,7 +44,7 @@ AdventureOptionsTab::AdventureOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourcePath("config/widgets/settings/adventureOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/adventureOptionsTab.json")); addCallback("playerHeroSpeedChanged", [this](int value) { auto targetLabel = widget("heroSpeedValueLabel"); diff --git a/client/windows/settings/BattleOptionsTab.cpp b/client/windows/settings/BattleOptionsTab.cpp index 0b95198bd..35abf73cb 100644 --- a/client/windows/settings/BattleOptionsTab.cpp +++ b/client/windows/settings/BattleOptionsTab.cpp @@ -23,7 +23,7 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; setRedrawParent(true); - const JsonNode config(ResourcePath("config/widgets/settings/battleOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/battleOptionsTab.json")); addCallback("viewGridChanged", [this, owner](bool value) { viewGridChangedCallback(value, owner); diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 6ed20cabf..2851873bc 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -105,7 +105,7 @@ GeneralOptionsTab::GeneralOptionsTab() addConditional("desktop", true); #endif - const JsonNode config(ResourcePath("config/widgets/settings/generalOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/generalOptionsTab.json")); addCallback("spellbookAnimationChanged", [](bool value) { setBoolSetting("video", "spellbookAnimation", value); diff --git a/client/windows/settings/OtherOptionsTab.cpp b/client/windows/settings/OtherOptionsTab.cpp index aa5e5325a..c2709b6a6 100644 --- a/client/windows/settings/OtherOptionsTab.cpp +++ b/client/windows/settings/OtherOptionsTab.cpp @@ -26,7 +26,7 @@ OtherOptionsTab::OtherOptionsTab() : InterfaceObjectConfigurable() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourcePath("config/widgets/settings/otherOptionsTab.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/otherOptionsTab.json")); addCallback("availableCreaturesAsDwellingLabelChanged", [](bool value) { return setBoolSetting("gameTweaks", "availableCreaturesAsDwellingLabel", value); diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index 2dfa11577..3ced217de 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -35,7 +35,7 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(ResourcePath("config/widgets/settings/settingsMainContainer.json")); + const JsonNode config(JsonPath::builtin("config/widgets/settings/settingsMainContainer.json")); addCallback("activateSettingsTab", [this](int tabId) { openTab(tabId); }); addCallback("loadGame", [this](int) { loadGameButtonCallback(); }); addCallback("saveGame", [this](int) { saveGameButtonCallback(); }); diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index dc7144a14..82ec45d56 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -84,7 +84,7 @@ void CModManager::loadMods() for(auto modname : installedMods) { - ResourcePath resID(CModInfo::getModFile(modname)); + auto resID = CModInfo::getModFile(modname); if(CResourceHandler::get()->existsResource(resID)) { boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 9b8664301..5eeebb5e4 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -332,8 +332,8 @@ std::vector CArtHandler::loadLegacyData() static std::map classes = {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; - CLegacyConfigParser parser("DATA/ARTRAITS.TXT"); - CLegacyConfigParser events("DATA/ARTEVENT.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/ARTRAITS.TXT")); + CLegacyConfigParser events(TextPath::builtin("DATA/ARTEVENT.TXT")); parser.endLine(); // header parser.endLine(); diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 8de2163d9..f583288a4 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -196,7 +196,7 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu void CBonusTypeHandler::load() { - const JsonNode gameConf(ResourcePath("config/gameConfig.json")); + const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); load(config); } diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 6aa7321e8..fd3a2fe85 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -55,12 +55,12 @@ SettingsStorage::SettingsStorage(): void SettingsStorage::init() { - std::string confName = "config/settings.json"; + JsonPath confName = JsonPath::builtin("config/settings.json"); - JsonUtils::assembleFromFiles(confName).swap(config); + JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); // Probably new install. Create config file to save settings to - if (!CResourceHandler::get("local")->existsResource(ResourcePath(confName))) + if (!CResourceHandler::get("local")->existsResource(confName)) CResourceHandler::get("local")->createResource(confName); JsonUtils::maximize(config, "vcmi:settings"); diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 872804635..10b293f0a 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -415,7 +415,7 @@ const CCreature * CCreatureHandler::getCreature(const std::string & scope, const void CCreatureHandler::loadCommanders() { - ResourcePath configResource("config/commanders.json"); + auto configResource = JsonPath::builtin("config/commanders.json"); std::string modSource = VLC->modh->findResourceOrigin(configResource); JsonNode data(configResource); @@ -507,7 +507,7 @@ std::vector CCreatureHandler::loadLegacyData() std::vector h3Data; h3Data.reserve(dataSize); - CLegacyConfigParser parser("DATA/CRTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/CRTRAITS.TXT")); parser.endLine(); // header @@ -698,7 +698,7 @@ void CCreatureHandler::loadCrExpMod() } } - CLegacyConfigParser expBonParser("DATA/CREXPMOD.TXT"); + CLegacyConfigParser expBonParser(TextPath::builtin("DATA/CREXPMOD.TXT")); expBonParser.endLine(); //header @@ -745,7 +745,7 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) globalEffects.addNewBonus(b); }; - CLegacyConfigParser parser("DATA/CREXPBON.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/CREXPBON.TXT")); Bonus b; //prototype with some default properties b.source = BonusSource::STACK_EXPERIENCE; @@ -804,7 +804,7 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const { - CLegacyConfigParser parser("DATA/CRANIM.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/CRANIM.TXT")); parser.endLine(); // header parser.endLine(); diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 624a0b08b..e11a3e903 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -119,9 +119,8 @@ protected: } }; -CLegacyConfigParser::CLegacyConfigParser(std::string URI) +CLegacyConfigParser::CLegacyConfigParser(const TextPath & resource) { - ResourcePath resource(URI, EResType::TEXT); auto input = CResourceHandler::get()->load(resource); std::string modName = VLC->modh->findResourceOrigin(resource); std::string language = VLC->modh->getModLanguage(modName); @@ -250,7 +249,7 @@ bool CLegacyConfigParser::endLine() void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) { - CLegacyConfigParser parser(sourceName); + CLegacyConfigParser parser(TextPath::builtin(sourceName)); size_t index = 0; do { @@ -434,7 +433,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); { - CLegacyConfigParser parser("DATA/RANDTVRN.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT")); parser.endLine(); size_t index = 0; do @@ -449,7 +448,7 @@ CGeneralTextHandler::CGeneralTextHandler(): while (parser.endLine()); } { - CLegacyConfigParser parser("DATA/GENRLTXT.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/GENRLTXT.TXT")); parser.endLine(); size_t index = 0; do @@ -460,7 +459,7 @@ CGeneralTextHandler::CGeneralTextHandler(): while (parser.endLine()); } { - CLegacyConfigParser parser("DATA/HELP.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/HELP.TXT")); size_t index = 0; do { @@ -473,7 +472,7 @@ CGeneralTextHandler::CGeneralTextHandler(): while (parser.endLine()); } { - CLegacyConfigParser parser("DATA/PLCOLORS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/PLCOLORS.TXT")); size_t index = 0; do { @@ -487,7 +486,7 @@ CGeneralTextHandler::CGeneralTextHandler(): while (parser.endLine()); } { - CLegacyConfigParser parser("DATA/SEERHUT.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/SEERHUT.TXT")); //skip header parser.endLine(); @@ -531,7 +530,7 @@ CGeneralTextHandler::CGeneralTextHandler(): } } { - CLegacyConfigParser parser("DATA/CAMPTEXT.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/CAMPTEXT.TXT")); //skip header parser.endLine(); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 83a7ee7b4..6bbbc98c1 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -9,6 +9,8 @@ */ #pragma once +#include "filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN class CInputStream; @@ -56,7 +58,7 @@ public: /// end current line bool endLine(); - explicit CLegacyConfigParser(std::string URI); + explicit CLegacyConfigParser(const TextPath & URI); }; class CGeneralTextHandler; diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 280d09105..dceb58b47 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -320,7 +320,7 @@ std::vector CHeroClassHandler::loadLegacyData() std::vector h3Data; h3Data.reserve(dataSize); - CLegacyConfigParser parser("DATA/HCTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/HCTRAITS.TXT")); parser.endLine(); // header parser.endLine(); @@ -681,9 +681,9 @@ std::vector CHeroHandler::loadLegacyData() std::vector h3Data; h3Data.reserve(dataSize); - CLegacyConfigParser specParser("DATA/HEROSPEC.TXT"); - CLegacyConfigParser bioParser("DATA/HEROBIOS.TXT"); - CLegacyConfigParser parser("DATA/HOTRAITS.TXT"); + CLegacyConfigParser specParser(TextPath::builtin("DATA/HEROSPEC.TXT")); + CLegacyConfigParser bioParser(TextPath::builtin("DATA/HEROBIOS.TXT")); + CLegacyConfigParser parser(TextPath::builtin("DATA/HOTRAITS.TXT")); parser.endLine(); //ignore header parser.endLine(); diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index dcd5d6725..355a44198 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -146,7 +146,7 @@ void CSkill::serializeJson(JsonSerializeFormat & handler) ///CSkillHandler std::vector CSkillHandler::loadLegacyData() { - CLegacyConfigParser parser("DATA/SSTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/SSTRAITS.TXT")); //skip header parser.endLine(); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 3d4e1388c..8015ceb52 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -330,7 +330,7 @@ std::vector CTownHandler::loadLegacyData() return dest[town]["town"]["buildings"][EBuildingType::names[building]]; }; - CLegacyConfigParser parser("DATA/BUILDING.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/BUILDING.TXT")); parser.endLine(); // header parser.endLine(); @@ -382,7 +382,7 @@ std::vector CTownHandler::loadLegacyData() } } { - CLegacyConfigParser parser("DATA/BLDGNEUT.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/BLDGNEUT.TXT")); for(int building=0; building<15; building++) { @@ -420,7 +420,7 @@ std::vector CTownHandler::loadLegacyData() } } { - CLegacyConfigParser parser("DATA/BLDGSPEC.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/BLDGSPEC.TXT")); for(int town=0; town CTownHandler::loadLegacyData() } } { - CLegacyConfigParser parser("DATA/DWELLING.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/DWELLING.TXT")); for(int town=0; town CTownHandler::loadLegacyData() } } { - CLegacyConfigParser typeParser("DATA/TOWNTYPE.TXT"); - CLegacyConfigParser nameParser("DATA/TOWNNAME.TXT"); + CLegacyConfigParser typeParser(TextPath::builtin("DATA/TOWNTYPE.TXT")); + CLegacyConfigParser nameParser(TextPath::builtin("DATA/TOWNNAME.TXT")); size_t townID=0; do { @@ -1148,9 +1148,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod void CTownHandler::loadRandomFaction() { - static const ResourcePath randomFactionPath("config/factions/random.json"); - - JsonNode randomFactionJson(randomFactionPath); + JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json")); randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); } diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 66a1596ac..a09d193ab 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -80,7 +80,7 @@ JsonNode::JsonNode(const char *data, size_t datasize): *this = parser.parse(""); } -JsonNode::JsonNode(ResourcePath && fileURI): +JsonNode::JsonNode(const JsonPath & fileURI): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -89,16 +89,7 @@ JsonNode::JsonNode(ResourcePath && fileURI): *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const ResourcePath & fileURI): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const std::string & idx, const ResourcePath & fileURI): +JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); @@ -107,7 +98,7 @@ type(JsonType::DATA_NULL) *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(ResourcePath && fileURI, bool &isValidSyntax): +JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax): type(JsonType::DATA_NULL) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -1253,11 +1244,11 @@ const JsonNode & getSchemaByName(const std::string & name) if (vstd::contains(loadedSchemas, name)) return loadedSchemas[name]; - std::string filename = "config/schemas/" + name; + auto filename = JsonPath::builtin("config/schemas/" + name); - if (CResourceHandler::get()->existsResource(ResourcePath(filename))) + if (CResourceHandler::get()->existsResource(filename)) { - loadedSchemas[name] = JsonNode(ResourcePath(filename)); + loadedSchemas[name] = JsonNode(filename); return loadedSchemas[name]; } @@ -1444,10 +1435,10 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo isValid = true; JsonNode result; - for(const std::string & file : files) + for(const auto & file : files) { bool isValidFile = false; - JsonNode section(ResourcePath(file, EResType::TEXT), isValidFile); + JsonNode section(JsonPath::builtinTODO(file), isValidFile); merge(result, section); isValid |= isValidFile; } @@ -1457,7 +1448,7 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo JsonNode JsonUtils::assembleFromFiles(const std::string & filename) { JsonNode result; - ResourcePath resID(filename, EResType::TEXT); + JsonPath resID(filename); for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) { diff --git a/lib/JsonNode.h b/lib/JsonNode.h index bdae6ea58..97833d7ff 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -9,6 +9,7 @@ */ #pragma once #include "GameConstants.h" +#include "filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -18,7 +19,6 @@ using JsonVector = std::vector; struct Bonus; class CSelector; -class ResourcePath; class CAddInfo; class ILimiter; @@ -61,10 +61,9 @@ public: //Create tree from Json-formatted input explicit JsonNode(const char * data, size_t datasize); //Create tree from JSON file - explicit JsonNode(ResourcePath && fileURI); - explicit JsonNode(const ResourcePath & fileURI); - explicit JsonNode(const std::string& idx, const ResourcePath & fileURI); - explicit JsonNode(ResourcePath && fileURI, bool & isValidSyntax); + explicit JsonNode(const JsonPath & fileURI); + explicit JsonNode(const std::string & modName, const JsonPath & fileURI); + explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); //Copy c-tor JsonNode(const JsonNode ©); diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 6bd09d3c6..0c8ce487f 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -127,7 +127,7 @@ std::vector TerrainTypeHandler::loadLegacyData() objects.resize(dataSize); - CLegacyConfigParser terrainParser("DATA/TERRNAME.TXT"); + CLegacyConfigParser terrainParser(TextPath::builtin("DATA/TERRNAME.TXT")); std::vector result; do diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index ba7f0d5c3..d4e306c58 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -348,7 +348,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const std::vector> creBankFormations[2]; std::vector commanderField; std::vector commanderBank; - const JsonNode config(ResourcePath("config/battleStartpos.json")); + const JsonNode config(JsonPath::builtin("config/battleStartpos.json")); const JsonVector &positions = config["battle_positions"].Vector(); CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]); diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 133e99a62..e2bb31f38 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -592,7 +592,7 @@ std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr campDescriptions; if(campDescriptions.empty()) //read once { - const JsonNode config(ResourcePath("config/campaign_regions.json")); + const JsonNode config(JsonPath::builtin("config/campaign_regions.json")); for(const JsonNode & campaign : config["campaign_regions"].Vector()) campDescriptions.push_back(CampaignRegions::fromJson(campaign)); } diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index c15b1b4a8..c95ac4c77 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -128,9 +128,9 @@ std::unordered_set CFilesystemList::getFilteredFiles(std::function return ret; } -bool CFilesystemList::createResource(std::string filename, bool update) +bool CFilesystemList::createResource(const ResourcePath & filename, bool update) { - logGlobal->trace("Creating %s", filename); + logGlobal->trace("Creating %s", filename.getOriginalName()); for (auto & loader : boost::adaptors::reverse(loaders)) { if (writeableLoaders.count(loader.get()) != 0 // writeable, diff --git a/lib/filesystem/AdapterLoaders.h b/lib/filesystem/AdapterLoaders.h index c54c24d50..23bf6d6a6 100644 --- a/lib/filesystem/AdapterLoaders.h +++ b/lib/filesystem/AdapterLoaders.h @@ -75,7 +75,7 @@ public: std::set getResourceNames(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; std::unordered_set getFilteredFiles(std::function filter) const override; - bool createResource(std::string filename, bool update = false) override; + bool createResource(const ResourcePath & filename, bool update = false) override; std::vector getResourcesWithName(const ResourcePath & resourceName) const override; /** diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index c90df4dd0..8298ae15c 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -68,9 +68,9 @@ std::unordered_set CFilesystemLoader::getFilteredFiles(std::functi return foundID; } -bool CFilesystemLoader::createResource(std::string filename, bool update) +bool CFilesystemLoader::createResource(const ResourcePath & resID, bool update) { - ResourcePath resID(filename); + std::string filename = resID.getOriginalName(); if (fileList.find(resID) != fileList.end()) return true; diff --git a/lib/filesystem/CFilesystemLoader.h b/lib/filesystem/CFilesystemLoader.h index 9d7687865..301adc361 100644 --- a/lib/filesystem/CFilesystemLoader.h +++ b/lib/filesystem/CFilesystemLoader.h @@ -37,7 +37,7 @@ public: std::unique_ptr load(const ResourcePath & resourceName) const override; bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - bool createResource(std::string filename, bool update = false) override; + bool createResource(const ResourcePath & filename, bool update = false) override; std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; std::unordered_set getFilteredFiles(std::function filter) const override; diff --git a/lib/filesystem/ISimpleResourceLoader.h b/lib/filesystem/ISimpleResourceLoader.h index 33272b1a9..b9b9c3f94 100644 --- a/lib/filesystem/ISimpleResourceLoader.h +++ b/lib/filesystem/ISimpleResourceLoader.h @@ -90,7 +90,7 @@ public: * * @return true if new file was created, false on error or if file already exists */ - virtual bool createResource(std::string filename, bool update = false) + virtual bool createResource(const ResourcePath & filename, bool update = false) { return false; } diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 6ccfa1d8e..fe2094ba5 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -19,7 +19,8 @@ class JsonSerializeFormat; * * Supported file extensions: * - * Text: .txt .json + * Text: .txt + * Json: .json * Animation: .def * Mask: .msk .msg * Campaign: .h3c @@ -36,6 +37,7 @@ class JsonSerializeFormat; enum class EResType { TEXT, + JSON, ANIMATION, MASK, CAMPAIGN, @@ -167,6 +169,8 @@ public: using AnimationPath = ResourcePathTempl; using ImagePath = ResourcePathTempl; +using TextPath = ResourcePathTempl; +using JsonPath = ResourcePathTempl; namespace EResTypeHelper { diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 18fd60bb3..a998d1c0e 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -804,7 +804,7 @@ void CGameState::removeHeroPlaceholders() void CGameState::initStartingResources() { logGlobal->debug("\tSetting up resources"); - const JsonNode config(ResourcePath("config/startres.json")); + const JsonNode config(JsonPath::builtin("config/startres.json")); const JsonVector &vector = config["difficulty"].Vector(); const JsonNode &level = vector[scenarioOps->difficulty]; diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index ffa63e29b..ead7a1f40 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -112,7 +112,7 @@ std::vector CObjectClassesHandler::loadLegacyData() { size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_OBJECT); - CLegacyConfigParser parser("Data/Objects.txt"); + CLegacyConfigParser parser(TextPath::builtin("Data/Objects.txt")); auto totalNumber = static_cast(parser.readNumber()); // first line contains number of objects to read and nothing else parser.endLine(); @@ -132,7 +132,7 @@ std::vector CObjectClassesHandler::loadLegacyData() std::vector ret(dataSize);// create storage for 256 objects assert(dataSize == 256); - CLegacyConfigParser namesParser("Data/ObjNames.txt"); + CLegacyConfigParser namesParser(TextPath::builtin("Data/ObjNames.txt")); for (size_t i=0; i<256; i++) { ret[i]["name"].String() = namesParser.readString(); @@ -142,7 +142,7 @@ std::vector CObjectClassesHandler::loadLegacyData() JsonNode cregen1; JsonNode cregen4; - CLegacyConfigParser cregen1Parser("data/crgen1"); + CLegacyConfigParser cregen1Parser(TextPath::builtin("data/crgen1")); do { JsonNode subObject; @@ -151,7 +151,7 @@ std::vector CObjectClassesHandler::loadLegacyData() } while(cregen1Parser.endLine()); - CLegacyConfigParser cregen4Parser("data/crgen4"); + CLegacyConfigParser cregen4Parser(TextPath::builtin("data/crgen4")); do { JsonNode subObject; diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index 614877f31..492974869 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN CObjectHandler::CObjectHandler() { logGlobal->trace("\t\tReading resources prices "); - const JsonNode config2(ResourcePath("config/resources.json")); + const JsonNode config2(JsonPath::builtin("config/resources.json")); for(const JsonNode &price : config2["resources_prices"].Vector()) { resVals.push_back(static_cast(price.Float())); diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index c17db6fc2..8b52b0a72 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -154,9 +154,9 @@ std::unique_ptr CMapService::getMapLoader(std::unique_ptrexistsResource(ResourcePath(path))) { - return JsonNode(ResourcePath(path, EResType::TEXT)); + return JsonNode(path); } // Probably new install. Create initial configuration CResourceHandler::get("local")->createResource(path); @@ -200,9 +200,9 @@ void CModHandler::loadOneMod(std::string modName, const std::string & parent, co return; } - if(CResourceHandler::get("initial")->existsResource(ResourcePath(CModInfo::getModFile(modFullName)))) + if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) { - CModInfo mod(modFullName, modSettings[modName], JsonNode(ResourcePath(CModInfo::getModFile(modFullName)))); + CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); if (!parent.empty()) // this is submod, add parent to dependencies mod.dependencies.insert(parent); @@ -224,11 +224,11 @@ void CModHandler::loadMods(bool onlyEssential) } else { - modConfig = loadModSettings("config/modSettings.json"); + modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); loadMods("", "", modConfig["activeMods"], true); } - coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(ResourcePath("config/gameConfig.json"))); + coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); coreMod->name = "Original game files"; } @@ -283,7 +283,7 @@ static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoa // FIXME: remove workaround for core mod if (modName != ModScope::scopeBuiltin()) { - ResourcePath modConfFile(CModInfo::getModFile(modName), EResType::TEXT); + auto modConfFile = CModInfo::getModFile(modName); ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); } diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 7fb5fd48a..1901e8923 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -75,9 +75,9 @@ std::string CModInfo::getModDir(const std::string & name) return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/"); } -std::string CModInfo::getModFile(const std::string & name) +JsonPath CModInfo::getModFile(const std::string & name) { - return getModDir(name) + "/mod.json"; + return JsonPath::builtinTODO(getModDir(name) + "/mod.json"); } void CModInfo::updateChecksum(ui32 newChecksum) @@ -152,7 +152,7 @@ bool CModInfo::checkModGameplayAffecting() const "obstacles" }; - ResourcePath modFileResource(CModInfo::getModFile(identifier)); + JsonPath modFileResource(CModInfo::getModFile(identifier)); if(CResourceHandler::get("initial")->existsResource(modFileResource)) { diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 72e60f01a..6e8a5012d 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -69,7 +69,7 @@ public: void setEnabled(bool on); static std::string getModDir(const std::string & name); - static std::string getModFile(const std::string & name); + static JsonPath getModFile(const std::string & name); /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects bool checkModGameplayAffecting() const; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index d18e7b5c4..30dcd24fe 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -51,8 +51,7 @@ int CMapGenerator::getRandomSeed() const void CMapGenerator::loadConfig() { - static const ResourcePath path("config/randomMap.json"); - JsonNode randomMapJson(path); + JsonNode randomMapJson(JsonPath::builtin("config/randomMap.json")); config.shipyardGuard = randomMapJson["waterZone"]["shipyard"]["value"].Integer(); for(auto & treasure : randomMapJson["waterZone"]["treasure"].Vector()) diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index fdb16c975..d5ac49775 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -561,7 +561,7 @@ std::vector CSpellHandler::loadLegacyData() using namespace SpellConfig; std::vector legacyData; - CLegacyConfigParser parser("DATA/SPTRAITS.TXT"); + CLegacyConfigParser parser(TextPath::builtin("DATA/SPTRAITS.TXT")); auto readSchool = [&](JsonMap & schools, const std::string & name) { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b60723b28..701c27ab7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1717,12 +1717,13 @@ void CGameHandler::save(const std::string & filename) logGlobal->info("Saving to %s", filename); const auto stem = FileInfo::GetPathStem(filename); const auto savefname = stem.to_string() + ".vsgm1"; - CResourceHandler::get("local")->createResource(savefname); + ResourcePath savePath(stem.to_string(), EResType::SAVEGAME); + CResourceHandler::get("local")->createResource(savePath); try { { - CSaveFile save(*CResourceHandler::get("local")->getResourceName(ResourcePath(stem.to_string(), EResType::SAVEGAME))); + CSaveFile save(*CResourceHandler::get("local")->getResourceName(savePath)); saveCommonState(save); logGlobal->info("Saving server state"); save << *this; diff --git a/test/map/CMapEditManagerTest.cpp b/test/map/CMapEditManagerTest.cpp index 048c2f5bd..aefffdbc2 100644 --- a/test/map/CMapEditManagerTest.cpp +++ b/test/map/CMapEditManagerTest.cpp @@ -120,7 +120,7 @@ TEST(MapManager, DrawTerrain_View) // Validate edit manager auto editManager = map->getEditManager(); CRandomGenerator gen; - const JsonNode viewNode(ResourcePath("test/terrainViewMappings", EResType::TEXT)); + const JsonNode viewNode(JsonPath::builtin("test/terrainViewMappings")); const auto & mappingsNode = viewNode["mappings"].Vector(); for (const auto & node : mappingsNode) { diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index 030cfd20a..f1b1a4d77 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -153,14 +153,14 @@ TEST(MapFormat, Objects) { static const std::string MAP_DATA_PATH = "test/ObjectPropertyTest/"; - const JsonNode initialHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now + const JsonNode initialHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME));//same as initial for now - const JsonNode initialObjects(ResourcePath(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedObjects(ResourcePath(MAP_DATA_PATH + "objects.ex.json")); + const JsonNode initialObjects(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedObjects(JsonPath::builtin(MAP_DATA_PATH + "objects.ex.json")); - const JsonNode expectedSurface(ResourcePath(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourcePath(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(JsonPath::builtin(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(JsonPath::builtin(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(initialHeader, initialObjects, expectedSurface, expectedUnderground); @@ -192,11 +192,11 @@ TEST(MapFormat, Terrain) { static const std::string MAP_DATA_PATH = "test/TerrainTest/"; - const JsonNode expectedHeader(ResourcePath(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode expectedObjects(ResourcePath(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode expectedHeader(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode expectedObjects(JsonPath::builtin(MAP_DATA_PATH + CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode expectedSurface(ResourcePath(MAP_DATA_PATH + "surface_terrain.json")); - const JsonNode expectedUnderground(ResourcePath(MAP_DATA_PATH + "underground_terrain.json")); + const JsonNode expectedSurface(JsonPath::builtin(MAP_DATA_PATH + "surface_terrain.json")); + const JsonNode expectedUnderground(JsonPath::builtin(MAP_DATA_PATH + "underground_terrain.json")); std::unique_ptr originalMap = loadOriginal(expectedHeader, expectedObjects, expectedSurface, expectedUnderground); diff --git a/test/mock/mock_MapService.cpp b/test/mock/mock_MapService.cpp index 8e0c83591..d9883c9d6 100644 --- a/test/mock/mock_MapService.cpp +++ b/test/mock/mock_MapService.cpp @@ -23,15 +23,15 @@ MapServiceMock::MapServiceMock(const std::string & path, MapListener * mapListen CZipSaver saver(io, "_"); - const JsonNode header(ResourcePath(path+CMapFormatJson::HEADER_FILE_NAME)); - const JsonNode objects(ResourcePath(path+CMapFormatJson::OBJECTS_FILE_NAME)); - const JsonNode surface(ResourcePath(path+"surface_terrain.json")); + const JsonNode header(JsonPath::builtin(path+CMapFormatJson::HEADER_FILE_NAME)); + const JsonNode objects(JsonPath::builtin(path+CMapFormatJson::OBJECTS_FILE_NAME)); + const JsonNode surface(JsonPath::builtin(path+"surface_terrain.json")); addToArchive(saver, header, CMapFormatJson::HEADER_FILE_NAME); addToArchive(saver, objects, CMapFormatJson::OBJECTS_FILE_NAME); addToArchive(saver, surface, "surface_terrain.json"); - ResourcePath undergroundPath(path+"underground_terrain.json"); + auto undergroundPath = JsonPath::builtin(path+"underground_terrain.json"); if(CResourceHandler::get()->existsResource(undergroundPath)) { From 97b7d44c88cf9fedc0e35819bd86521cdc354bab Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 2 Sep 2023 00:57:25 +0300 Subject: [PATCH 0316/1248] Use ResourcePath for video accessing --- client/CMT.cpp | 6 +++--- client/CVideoHandler.cpp | 19 +++++++++---------- client/CVideoHandler.h | 15 ++++++++------- client/battle/BattleInterfaceClasses.cpp | 8 ++++---- client/mainmenu/CCampaignScreen.cpp | 2 +- client/mainmenu/CCampaignScreen.h | 2 +- client/mainmenu/CMainMenu.cpp | 2 +- client/windows/CSpellWindow.cpp | 4 ++-- client/windows/GUIClasses.cpp | 2 +- lib/CTownHandler.cpp | 2 +- lib/CTownHandler.h | 2 +- lib/campaign/CampaignHandler.cpp | 8 ++++---- lib/campaign/CampaignHandler.h | 3 ++- lib/campaign/CampaignScenarioPrologEpilog.h | 4 +++- lib/filesystem/ResourcePath.h | 6 ++++++ 15 files changed, 47 insertions(+), 38 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 4c0d97a76..ee4ae11cc 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -423,10 +423,10 @@ int main(int argc, char * argv[]) //plays intro, ends when intro is over or button has been pressed (handles events) void playIntro() { - if(CCS->videoh->openAndPlayVideo("3DOLOGO.SMK", 0, 1, true, true)) + if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) { - if (CCS->videoh->openAndPlayVideo("NWCLOGO.SMK", 0, 1, true, true)) - CCS->videoh->openAndPlayVideo("H3INTRO.SMK", 0, 1, true, true); + if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) + CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); } } diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index c685ff422..6bf48fc35 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -70,30 +70,28 @@ CVideoPlayer::CVideoPlayer() , doLoop(false) {} -bool CVideoPlayer::open(std::string fname, bool scale) +bool CVideoPlayer::open(const VideoPath & fname, bool scale) { return open(fname, true, false); } // loop = to loop through the video // useOverlay = directly write to the screen. -bool CVideoPlayer::open(std::string fname, bool loop, bool useOverlay, bool scale) +bool CVideoPlayer::open(const VideoPath & fname, bool loop, bool useOverlay, bool scale) { close(); - this->fname = fname; + this->fname = fname.addPrefix("VIDEO/"); doLoop = loop; frameTime = 0; - ResourcePath resource(std::string("Video/") + fname, EResType::VIDEO); - - if (!CResourceHandler::get()->existsResource(resource)) + if (!CResourceHandler::get()->existsResource(fname)) { - logGlobal->error("Error: video %s was not found", resource.getName()); + logGlobal->error("Error: video %s was not found", fname.getName()); return false; } - data = CResourceHandler::get()->load(resource); + data = CResourceHandler::get()->load(fname); static const int BUFFER_SIZE = 4096; @@ -382,7 +380,8 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo void CVideoPlayer::close() { - fname.clear(); + fname = VideoPath(); + if (sws) { sws_freeContext(sws); @@ -467,7 +466,7 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) return true; } -bool CVideoPlayer::openAndPlayVideo(std::string name, int x, int y, bool stopOnKey, bool scale) +bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale) { open(name, false, true, scale); bool ret = playVideo(x, y, stopOnKey); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 684270685..877147ff5 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -10,6 +10,7 @@ #pragma once #include "../lib/Rect.h" +#include "../lib/filesystem/ResourcePath.h" struct SDL_Surface; struct SDL_Texture; @@ -17,7 +18,7 @@ struct SDL_Texture; class IVideoPlayer { public: - virtual bool open(std::string name, bool scale = false)=0; //true - succes + virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes virtual void close()=0; virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; @@ -30,10 +31,10 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: - std::string fname; //name of current video file (empty if idle) + VideoPath fname; //name of current video file (empty if idle) virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} - virtual bool openAndPlayVideo(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) + virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { return false; } @@ -49,7 +50,7 @@ public: bool nextFrame() override {return false;}; void close() override {}; bool wait() override {return false;}; - bool open(std::string name, bool scale = false) override {return false;}; + bool open(const VideoPath & name, bool scale = false) override {return false;}; }; #ifndef DISABLE_VIDEO @@ -85,14 +86,14 @@ class CVideoPlayer : public IMainVideoPlayer bool doLoop; // loop through video bool playVideo(int x, int y, bool stopOnKey); - bool open(std::string fname, bool loop, bool useOverlay = false, bool scale = false); + bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); public: CVideoPlayer(); ~CVideoPlayer(); bool init(); - bool open(std::string fname, bool scale = false) override; + bool open(const VideoPath & fname, bool scale = false) override; void close() override; bool nextFrame() override; // display next frame @@ -101,7 +102,7 @@ public: void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) - bool openAndPlayVideo(std::string name, int x, int y, bool stopOnKey = false, bool scale = false) override; + bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; //TODO: bool wait() override {return false;}; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 4557d6cff..937d6ca0e 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -581,7 +581,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface } CCS->musich->playMusic("Music/Win Battle", false, true); - CCS->videoh->open("WIN3.BIK"); + CCS->videoh->open(VideoPath::builtin("WIN3.BIK")); std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->battleGetMyHero(); @@ -598,19 +598,19 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface { int text = 311; std::string musicName = "Music/LoseCombat"; - std::string videoName = "LBSTART.BIK"; + VideoPath videoName = VideoPath::builtin("LBSTART.BIK"); switch(br.result) { case EBattleResult::NORMAL: break; case EBattleResult::ESCAPE: musicName = "Music/Retreat Battle"; - videoName = "RTSTART.BIK"; + videoName = VideoPath::builtin("RTSTART.BIK"); text = 310; break; case EBattleResult::SURRENDER: musicName = "Music/Surrender Battle"; - videoName = "SURRENDER.BIK"; + videoName = VideoPath::builtin("SURRENDER.BIK"); text = 309; break; default: diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index c8d695aee..b7922003b 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -99,7 +99,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) pos.h = 116; campFile = config["file"].String(); - video = config["video"].String(); + video = VideoPath::fromJson(config["video"]); status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index 0189980e6..f430b3e9a 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -37,7 +37,7 @@ private: CampaignStatus status; std::string campFile; // the filename/resourcename of the campaign - std::string video; // the resource name of the video + VideoPath video; // the resource name of the video std::string hoverText; void clickReleased(const Point & cursorPosition) override; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 3f0a121bf..6a8d226c1 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -116,7 +116,7 @@ void CMenuScreen::activate() { CCS->musich->playMusic("Music/MainMenu", true, true); if(!config["video"].isNull()) - CCS->videoh->open(config["video"]["name"].String()); + CCS->videoh->open(VideoPath::fromJson(config["video"]["name"])); CIntObject::activate(); } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 67d88c2ff..99db9419c 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -397,13 +397,13 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { if(settings["video"]["spellbookAnimation"].Bool()) - CCS->videoh->openAndPlayVideo("PGTRNLFT.SMK", pos.x+13, pos.y+15); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); } void CSpellWindow::turnPageRight() { if(settings["video"]["spellbookAnimation"].Bool()) - CCS->videoh->openAndPlayVideo("PGTRNRGH.SMK", pos.x+13, pos.y+15); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); } void CSpellWindow::keyPressed(EShortcut key) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 1e8097c26..aae29023e 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -513,7 +513,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) if(LOCPLINT->castleInt) CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); else - CCS->videoh->open("TAVERN.BIK"); + CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); } void CTavernWindow::recruitb() diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 8015ceb52..3268b20f5 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -884,7 +884,7 @@ void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); info.guildBackground = ImagePath::fromJson(source["guildBackground"]); - info.tavernVideo = source["tavernVideo"].String(); + info.tavernVideo = VideoPath::fromJson(source["tavernVideo"]); loadTownHall(town, source["hallSlots"]); loadStructures(town, source["structures"]); diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 4c8da2082..bb24808b9 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -304,7 +304,7 @@ public: int icons[2][2]; std::string iconSmall[2][2]; /// icon names used during loading std::string iconLarge[2][2]; - std::string tavernVideo; + VideoPath tavernVideo; std::string musicTheme; ImagePath townBackground; ImagePath guildBackground; diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index e2bb31f38..fb6dc65da 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -166,7 +166,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) ret.hasPrologEpilog = !identifier.isNull(); if(ret.hasPrologEpilog) { - ret.prologVideo = identifier["video"].String(); + ret.prologVideo = VideoPath::fromJson(identifier["video"]); ret.prologMusic = identifier["music"].String(); ret.prologText = identifier["text"].String(); } @@ -590,13 +590,13 @@ std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr> getFile(std::unique_ptr file, bool headerOnly); - static std::string prologVideoName(ui8 index); + static VideoPath prologVideoName(ui8 index); static std::string prologMusicName(ui8 index); static std::string prologVoiceName(ui8 index); diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 5ab80f680..44dd1c961 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -9,12 +9,14 @@ */ #pragma once +#include "../filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE CampaignScenarioPrologEpilog { bool hasPrologEpilog = false; - std::string prologVideo; // from CmpMovie.txt + VideoPath prologVideo; // from CmpMovie.txt std::string prologMusic; // from CmpMusic.txt std::string prologText; diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index fe2094ba5..be2d30d20 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -83,6 +83,11 @@ public: return name == other.name && type == other.type; } + inline bool operator!=(const ResourcePath & other) const + { + return name != other.name || type != other.type; + } + inline bool operator<(const ResourcePath & other) const { if (type != other.type) @@ -171,6 +176,7 @@ using AnimationPath = ResourcePathTempl; using ImagePath = ResourcePathTempl; using TextPath = ResourcePathTempl; using JsonPath = ResourcePathTempl; +using VideoPath = ResourcePathTempl; namespace EResTypeHelper { From 8dfdfffd8739c641418c9f6e5e51e555a7a54d4a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 13:03:15 +0300 Subject: [PATCH 0317/1248] Use ResourcePath for audio files --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/AIGateway.h | 2 +- AI/VCAI/VCAI.cpp | 2 +- AI/VCAI/VCAI.h | 2 +- client/CMusicHandler.cpp | 62 +++++++++---------- client/CMusicHandler.h | 34 +++++----- client/CPlayerInterface.cpp | 19 +++--- client/CPlayerInterface.h | 2 +- client/adventureMap/CInGameConsole.cpp | 2 +- client/adventureMap/MapAudioPlayer.cpp | 8 +-- client/adventureMap/MapAudioPlayer.h | 3 +- client/adventureMap/TurnTimerWidget.cpp | 2 +- client/battle/BattleAnimationClasses.cpp | 20 +++--- client/battle/BattleAnimationClasses.h | 4 +- client/battle/BattleEffectsController.cpp | 16 ++--- client/battle/BattleEffectsController.h | 3 +- client/battle/BattleInterface.cpp | 4 +- client/battle/BattleInterfaceClasses.cpp | 8 +-- client/battle/BattleSiegeController.cpp | 2 +- client/battle/BattleStacksController.cpp | 12 ++-- client/lobby/CBonusSelection.cpp | 2 +- client/lobby/CSelectionBase.cpp | 2 +- client/mainmenu/CCampaignScreen.cpp | 6 +- client/mainmenu/CMainMenu.cpp | 2 +- client/mainmenu/CPrologEpilogVideo.cpp | 2 +- client/render/CAnimation.cpp | 2 +- include/vcmi/spells/Spell.h | 1 - lib/CCreatureHandler.cpp | 19 +++--- lib/CCreatureHandler.h | 16 ++--- lib/CGeneralTextHandler.cpp | 8 +-- lib/CTownHandler.cpp | 2 +- lib/CTownHandler.h | 2 +- lib/IGameEventsReceiver.h | 2 +- lib/JsonDetail.cpp | 4 +- lib/ObstacleHandler.h | 2 +- lib/TerrainHandler.cpp | 6 +- lib/TerrainHandler.h | 6 +- lib/battle/CObstacleInstance.cpp | 8 +-- lib/battle/CObstacleInstance.h | 6 +- lib/campaign/CampaignHandler.cpp | 8 +-- lib/campaign/CampaignHandler.h | 2 +- lib/campaign/CampaignScenarioPrologEpilog.h | 2 +- lib/campaign/CampaignState.cpp | 2 +- lib/campaign/CampaignState.h | 4 +- lib/filesystem/CFilesystemLoader.cpp | 2 +- lib/filesystem/Filesystem.cpp | 6 +- lib/filesystem/ResourcePath.cpp | 3 +- lib/filesystem/ResourcePath.h | 7 +++ .../AObjectTypeHandler.cpp | 6 +- lib/mapObjectConstructors/SObjectSounds.h | 8 ++- lib/mapObjects/CGObjectInstance.cpp | 6 +- lib/mapObjects/CGObjectInstance.h | 6 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/spells/CSpellHandler.cpp | 4 +- lib/spells/CSpellHandler.h | 4 +- lib/spells/effects/Obstacle.cpp | 2 +- lib/spells/effects/Obstacle.h | 2 +- mapeditor/Animation.cpp | 2 +- test/map/CMapFormatTest.cpp | 2 +- 59 files changed, 195 insertions(+), 192 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 9ae840d4b..7257e8618 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -387,7 +387,7 @@ void AIGateway::heroCreated(const CGHeroInstance * h) NET_EVENT_HANDLER; } -void AIGateway::advmapSpellCast(const CGHeroInstance * caster, int spellID) +void AIGateway::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) { LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); NET_EVENT_HANDLER; diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index c4064f164..dd352013d 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -152,7 +152,7 @@ public: void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; void playerBonusChanged(const Bonus & bonus, bool gain) override; void heroCreated(const CGHeroInstance *) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 231c77cd8..3db1fa7f9 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -475,7 +475,7 @@ void VCAI::heroCreated(const CGHeroInstance * h) NET_EVENT_HANDLER; } -void VCAI::advmapSpellCast(const CGHeroInstance * caster, int spellID) +void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) { LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); NET_EVENT_HANDLER; diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index ea341b4af..912f7bde2 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -185,7 +185,7 @@ public: void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; void playerBonusChanged(const Bonus & bonus, bool gain) override; void heroCreated(const CGHeroInstance *) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 0b74e73ca..65c61e933 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -119,25 +119,25 @@ void CSoundHandler::release() } // Allocate an SDL chunk and cache it. -Mix_Chunk *CSoundHandler::GetSoundChunk(std::string &sound, bool cache) +Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) { try { if (cache && soundChunks.find(sound) != soundChunks.end()) return soundChunks[sound].first; - auto data = CResourceHandler::get()->load(ResourcePath(std::string("SOUNDS/") + sound, EResType::SOUND))->readAll(); + auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops if (cache) - soundChunks.insert(std::pair(sound, std::make_pair (chunk, std::move (data.first)))); + soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))}); return chunk; } catch(std::exception &e) { - logGlobal->warn("Cannot get sound %s chunk: %s", sound, e.what()); + logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); return nullptr; } } @@ -153,7 +153,7 @@ int CSoundHandler::ambientDistToVolume(int distance) const return volume * (int)ambientConfig["volume"].Integer() / 100; } -void CSoundHandler::ambientStopSound(std::string soundId) +void CSoundHandler::ambientStopSound(const AudioPath & soundId) { stopSound(ambientChannels[soundId]); setChannelVolume(ambientChannels[soundId], volume); @@ -163,13 +163,13 @@ void CSoundHandler::ambientStopSound(std::string soundId) int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) { assert(soundID < soundBase::sound_after_last); - auto sound = sounds[soundID]; - logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound); + auto sound = AudioPath::builtin(sounds[soundID]); + logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); return playSound(sound, repeats, true); } -int CSoundHandler::playSound(std::string sound, int repeats, bool cache) +int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) { if (!initialized || sound.empty()) return -1; @@ -182,7 +182,7 @@ int CSoundHandler::playSound(std::string sound, int repeats, bool cache) channel = Mix_PlayChannel(-1, chunk, repeats); if (channel == -1) { - logGlobal->error("Unable to play sound file %s , error %s", sound, Mix_GetError()); + logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); if (!cache) Mix_FreeChunk(chunk); } @@ -290,14 +290,14 @@ int CSoundHandler::ambientGetRange() const return static_cast(ambientConfig["range"].Integer()); } -void CSoundHandler::ambientUpdateChannels(std::map soundsArg) +void CSoundHandler::ambientUpdateChannels(std::map soundsArg) { boost::mutex::scoped_lock guard(mutex); - std::vector stoppedSounds; + std::vector stoppedSounds; for(auto & pair : ambientChannels) { - const std::string & soundId = pair.first; + const auto & soundId = pair.first; const int channel = pair.second; if(!vstd::contains(soundsArg, soundId)) @@ -320,7 +320,7 @@ void CSoundHandler::ambientUpdateChannels(std::map soundsArg) for(auto & pair : soundsArg) { - const std::string & soundId = pair.first; + const auto & soundId = pair.first; const int distance = pair.second; if(!vstd::contains(ambientChannels, soundId)) @@ -372,9 +372,9 @@ CMusicHandler::CMusicHandler(): for(const ResourcePath & file : mp3files) { if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) - addEntryToSet("battle", file.getName()); + addEntryToSet("battle", AudioPath::fromResource(file)); else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) - addEntryToSet("enemy-turn", file.getName()); + addEntryToSet("enemy-turn", AudioPath::fromResource(file)); } } @@ -383,11 +383,11 @@ void CMusicHandler::loadTerrainMusicThemes() { for (const auto & terrain : CGI->terrainTypeHandler->objects) { - addEntryToSet("terrain_" + terrain->getJsonKey(), "Music/" + terrain->musicFilename); + addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); } } -void CMusicHandler::addEntryToSet(const std::string & set, const std::string & musicURI) +void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) { musicsSet[set].push_back(musicURI); } @@ -421,7 +421,7 @@ void CMusicHandler::release() CAudioBase::release(); } -void CMusicHandler::playMusic(const std::string & musicURI, bool loop, bool fromStart) +void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) { boost::mutex::scoped_lock guard(mutex); @@ -451,7 +451,7 @@ void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bo return; // in this mode - play random track from set - queueNext(this, whichSet, "", loop, fromStart); + queueNext(this, whichSet, AudioPath(), loop, fromStart); } void CMusicHandler::queueNext(std::unique_ptr queued) @@ -468,7 +468,7 @@ void CMusicHandler::queueNext(std::unique_ptr queued) } } -void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const std::string & musicURI, bool looped, bool fromStart) +void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) { queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); } @@ -523,7 +523,7 @@ void CMusicHandler::musicFinishedCallback() }); } -MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart): +MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart): owner(owner), music(nullptr), playing(false), @@ -552,16 +552,16 @@ MusicEntry::~MusicEntry() Mix_HaltMusic(); } - logGlobal->trace("Del-ing music file %s", currentName); + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); if (music) Mix_FreeMusic(music); } -void MusicEntry::load(std::string musicURI) +void MusicEntry::load(const AudioPath & musicURI) { if (music) { - logGlobal->trace("Del-ing music file %s", currentName); + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); Mix_FreeMusic(music); music = nullptr; } @@ -569,22 +569,22 @@ void MusicEntry::load(std::string musicURI) currentName = musicURI; music = nullptr; - logGlobal->trace("Loading music file %s", musicURI); + logGlobal->trace("Loading music file %s", musicURI.getOriginalName()); try { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(ResourcePath(std::move(musicURI), EResType::SOUND))); + auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(musicURI)); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); } catch(std::exception &e) { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, musicURI); + logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, musicURI.getOriginalName()); logGlobal->error("Exception: %s", e.what()); } if(!music) { - logGlobal->warn("Warning: Cannot open %s: %s", currentName, Mix_GetError()); + logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); return; } } @@ -601,7 +601,7 @@ bool MusicEntry::play() load(*iter); } - logGlobal->trace("Playing music file %s", currentName); + logGlobal->trace("Playing music file %s", currentName.getOriginalName()); if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) { @@ -646,7 +646,7 @@ bool MusicEntry::stop(int fade_ms) assert(startTime != uint32_t(-1)); float playDuration = (endTime - startTime + startPosition) / 1000.f; owner->trackPositions[currentName] = playDuration; - logGlobal->trace("Stopping music file %s at %f", currentName, playDuration); + logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); Mix_FadeOutMusic(fade_ms); return true; @@ -664,7 +664,7 @@ bool MusicEntry::isSet(std::string set) return !setName.empty() && set == setName; } -bool MusicEntry::isTrack(std::string track) +bool MusicEntry::isTrack(const AudioPath & track) { return setName.empty() && track == currentName; } diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index a0180cd74..56e1e3d7e 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -35,15 +35,14 @@ public: class CSoundHandler: public CAudioBase { private: - //soundBase::soundID getSoundID(const std::string &fileName); //update volume on configuration change SettingsListener listener; void onVolumeChange(const JsonNode &volumeNode); using CachedChunk = std::pair>; - std::map soundChunks; + std::map soundChunks; - Mix_Chunk *GetSoundChunk(std::string &sound, bool cache); + Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); /// have entry for every currently active channel /// vector will be empty if callback was not set @@ -54,12 +53,12 @@ private: boost::mutex mutexCallbacks; int ambientDistToVolume(int distance) const; - void ambientStopSound(std::string soundId); + void ambientStopSound(const AudioPath & soundId); void updateChannelVolume(int channel); const JsonNode ambientConfig; - std::map ambientChannels; + std::map ambientChannels; std::map channelVolumes; void initCallback(int channel, const std::function & function); @@ -76,7 +75,7 @@ public: // Sounds int playSound(soundBase::soundID soundID, int repeats=0); - int playSound(std::string sound, int repeats=0, bool cache=false); + int playSound(const AudioPath & sound, int repeats=0, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); @@ -84,16 +83,13 @@ public: void soundFinishedCallback(int channel); int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); + void ambientUpdateChannels(std::map currentSounds); void ambientStopAllChannels(); // Sets std::vector battleIntroSounds; }; -// Helper //now it looks somewhat useless -#define battle_sound(creature,what_sound) creature->sounds.what_sound - class CMusicHandler; //Class for handling one music file @@ -109,16 +105,16 @@ class MusicEntry uint32_t startPosition; //if not null - set from which music will be randomly selected std::string setName; - std::string currentName; + AudioPath currentName; - void load(std::string musicURI); + void load(const AudioPath & musicURI); public: - MusicEntry(CMusicHandler *owner, std::string setName, std::string musicURI, bool looped, bool fromStart); + MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); ~MusicEntry(); bool isSet(std::string setName); - bool isTrack(std::string trackName); + bool isTrack(const AudioPath & trackName); bool isPlaying(); bool play(); @@ -135,20 +131,20 @@ private: std::unique_ptr current; std::unique_ptr next; - void queueNext(CMusicHandler *owner, const std::string & setName, const std::string & musicURI, bool looped, bool fromStart); + void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); void queueNext(std::unique_ptr queued); void musicFinishedCallback(); /// map -> - std::map> musicsSet; + std::map> musicsSet; /// stored position, in seconds at which music player should resume playing this track - std::map trackPositions; + std::map trackPositions; public: CMusicHandler(); /// add entry with URI musicURI in set. Track will have ID musicID - void addEntryToSet(const std::string & set, const std::string & musicURI); + void addEntryToSet(const std::string & set, const AudioPath & musicURI); void init() override; void loadTerrainMusicThemes(); @@ -156,7 +152,7 @@ public: void setVolume(ui32 percent) override; /// play track by URI, if loop = true music will be looped - void playMusic(const std::string & musicURI, bool loop, bool fromStart); + void playMusic(const AudioPath & musicURI, bool loop, bool fromStart); /// play random track from this set void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart); /// play random track from set (musicSet, entryID) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 8a3daf45b..e3b0c5f59 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1657,7 +1657,7 @@ void CPlayerInterface::viewWorldMap() adventureInt->openWorldView(); } -void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellID) +void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) { EVENT_HANDLER_CALLED_BY_CLIENT; @@ -1667,8 +1667,7 @@ void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, int spellI if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) localState->erasePath(caster); - const spells::Spell * spell = CGI->spells()->getByIndex(spellID); - auto castSoundPath = spell->getCastSound(); + auto castSoundPath = spellID.toSpell()->getCastSound(); if(!castSoundPath.empty()) CCS->soundh->playSound(castSoundPath); } @@ -1992,22 +1991,22 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) elem.coord = h->convertFromVisitablePos(elem.coord); int soundChannel = -1; - std::string soundName; + AudioPath soundName; - auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> std::string + auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> AudioPath { if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) - return ""; + return {}; if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) - return ""; + return {}; if (moveType == EPathNodeAction::BLOCKING_VISIT) - return ""; + return {}; // flying movement sound if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) - return "HORSE10.wav"; + return AudioPath::builtin("HORSE10.wav"); auto prevTile = cb->getTile(h->convertToVisitablePos(posPrev)); auto nextTile = cb->getTile(h->convertToVisitablePos(posNext)); @@ -2073,7 +2072,7 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) { // Start a new sound for the hero movement or let the existing one carry on. - std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); if(newSoundName != soundName) { diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index c5048637f..01cfe11af 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -128,7 +128,7 @@ protected: // Call-ins from server, should not be called directly, but only via void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override; void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override; void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; - void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; //called when a hero casts a spell + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles void newObject(const CGObjectInstance * obj) override; diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 8ab4aa479..8ee0145ae 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -105,7 +105,7 @@ void CInGameConsole::print(const std::string & txt) } GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set - CCS->soundh->playSound("CHAT"); + CCS->soundh->playSound(AudioPath::builtin("CHAT")); } bool CInGameConsole::captureThisKey(EShortcut key) diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 17e8aeecb..6697d7814 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -123,9 +123,9 @@ void MapAudioPlayer::removeObject(const CGObjectInstance * obj) vstd::erase(objects[z][x][y], obj->id); } -std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) +std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) { - std::vector result; + std::vector result; for(auto & objectID : objects[tile.z][tile.x][tile.y]) { @@ -147,8 +147,8 @@ std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) void MapAudioPlayer::updateAmbientSounds() { - std::map currentSounds; - auto updateSounds = [&](const std::string& soundId, int distance) -> void + std::map currentSounds; + auto updateSounds = [&](const AudioPath& soundId, int distance) -> void { if(vstd::contains(currentSounds, soundId)) currentSounds[soundId] = std::min(currentSounds[soundId], distance); diff --git a/client/adventureMap/MapAudioPlayer.h b/client/adventureMap/MapAudioPlayer.h index 31a00e6d3..f6c752887 100644 --- a/client/adventureMap/MapAudioPlayer.h +++ b/client/adventureMap/MapAudioPlayer.h @@ -10,6 +10,7 @@ #pragma once #include "../mapView/IMapRendererObserver.h" +#include "../../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN class ObjectInstanceID; @@ -29,7 +30,7 @@ class MapAudioPlayer : public IMapObjectObserver void addObject(const CGObjectInstance * obj); void removeObject(const CGObjectInstance * obj); - std::vector getAmbientSounds(const int3 & tile); + std::vector getAmbientSounds(const int3 & tile); void updateAmbientSounds(); void updateMusic(); void update(); diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 1530713d1..af0840755 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -77,7 +77,7 @@ void TurnTimerWidget::setTime(PlayerColor player, int time) && newTime != turnTime && notifications.count(newTime)) { - CCS->soundh->playSound(variables["notificationSound"].String()); + CCS->soundh->playSound(AudioPath::fromJson(variables["notificationSound"])); } turnTime = newTime; diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 355b8d679..8cde61997 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -114,7 +114,7 @@ void StackActionAnimation::setGroup( ECreatureAnimType group ) currGroup = group; } -void StackActionAnimation::setSound( std::string sound ) +void StackActionAnimation::setSound( const AudioPath & sound ) { this->sound = sound; } @@ -179,7 +179,7 @@ HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) : StackActionAnimation(owner, stack) { setGroup(ECreatureAnimType::HITTED); - setSound(battle_sound(stack->unitType(), wince)); + setSound(stack->unitType()->sounds.wince); logAnim->debug("Created HittedAnimation for %s", stack->getName()); } @@ -187,14 +187,14 @@ DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack : StackActionAnimation(owner, stack) { setGroup(ECreatureAnimType::DEFENCE); - setSound(battle_sound(stack->unitType(), defend)); + setSound(stack->unitType()->sounds.defend); logAnim->debug("Created DefenceAnimation for %s", stack->getName()); } DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): StackActionAnimation(owner, stack) { - setSound(battle_sound(stack->unitType(), killed)); + setSound(stack->unitType()->sounds.killed); if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) setGroup(ECreatureAnimType::DEATH_RANGED); @@ -315,7 +315,7 @@ MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack : AttackAnimation(owner, attacker, _dest, _attacked) { logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); - setSound(battle_sound(getCreature(), attack)); + setSound(getCreature()->sounds.attack); setGroup(selectGroup(multiAttack)); } @@ -356,7 +356,7 @@ bool MovementAnimation::init() if (moveSoundHander == -1) { - moveSoundHander = CCS->soundh->playSound(battle_sound(stack->unitType(), move), -1); + moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); } Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); @@ -453,7 +453,7 @@ bool MovementEndAnimation::init() logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); - CCS->soundh->playSound(battle_sound(stack->unitType(), endMoving)); + CCS->soundh->playSound(stack->unitType()->sounds.endMoving); if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) { @@ -494,7 +494,7 @@ bool MovementStartAnimation::init() } logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); - CCS->soundh->playSound(battle_sound(stack->unitType(), startMoving)); + CCS->soundh->playSound(stack->unitType()->sounds.startMoving); if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) { @@ -632,7 +632,7 @@ RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CSt : AttackAnimation(owner_, attacker, dest_, defender), projectileEmitted(false) { - setSound(battle_sound(getCreature(), shoot)); + setSound(getCreature()->sounds.shoot); } bool RangedAttackAnimation::init() @@ -806,7 +806,7 @@ void CatapultAnimation::tick(uint32_t msPassed) explosionEmitted = true; Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); - std::string soundFilename = (catapultDamage > 0) ? "WALLHIT" : "WALLMISS"; + auto soundFilename = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS"); AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK"); CCS->soundh->playSound( soundFilename ); diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index 93ff9c3df..11fd827a8 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -69,11 +69,11 @@ class StackActionAnimation : public BattleStackAnimation { ECreatureAnimType nextGroup; ECreatureAnimType currGroup; - std::string sound; + AudioPath sound; public: void setNextGroup( ECreatureAnimType group ); void setGroup( ECreatureAnimType group ); - void setSound( std::string sound ); + void setSound( const AudioPath & sound ); ECreatureAnimType getGroup() const; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index 178b15196..fb64fbfc8 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -41,10 +41,10 @@ BattleEffectsController::BattleEffectsController(BattleInterface & owner): void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) { - displayEffect(effect, "", destTile); + displayEffect(effect, AudioPath(), destTile); } -void BattleEffectsController::displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile) +void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile) { size_t effectID = static_cast(effect); @@ -69,22 +69,22 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt switch(static_cast(bte.effect)) { case BonusType::HP_REGENERATION: - displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition()); + displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition()); break; case BonusType::MANA_DRAIN: - displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition()); + displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition()); break; case BonusType::POISON: - displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition()); + displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition()); break; case BonusType::FEAR: - displayEffect(EBattleEffect::FEAR, "FEAR", stack->getPosition()); + displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition()); break; case BonusType::MORALE: { std::string hlp = CGI->generaltexth->allTexts[33]; boost::algorithm::replace_first(hlp,"%s",(stack->getName())); - displayEffect(EBattleEffect::GOOD_MORALE, "GOODMRLE", stack->getPosition()); + displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition()); owner.appendBattleLog(hlp); break; } @@ -107,7 +107,7 @@ void BattleEffectsController::startAction(const BattleAction & action) break; case EActionType::BAD_MORALE: owner.appendBattleLog(stack->formatGeneralMessage(-34)); - displayEffect(EBattleEffect::BAD_MORALE, "BADMRLE", stack->getPosition()); + displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition()); break; } diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 3ea1452b4..203a121fd 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -11,6 +11,7 @@ #include "../../lib/battle/BattleHex.h" #include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" #include "BattleConstants.h" VCMI_LIB_NAMESPACE_BEGIN @@ -64,7 +65,7 @@ public: //displays custom effect on the battlefield void displayEffect(EBattleEffect effect, const BattleHex & destTile); - void displayEffect(EBattleEffect effect, std::string soundFile, const BattleHex & destTile); + void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile); void battleTriggerEffect(const BattleTriggerEffect & bte); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 8db671a94..6729e4a1a 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -353,7 +353,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) if(!spell) return; - const std::string & castSoundPath = spell->getCastSound(); + const AudioPath & castSoundPath = spell->getCastSound(); if (!castSoundPath.empty()) { @@ -419,7 +419,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) if (!sc->resistedCres.empty()) { addToAnimationStage(EAnimationEvents::HIT, [=](){ - CCS->soundh->playSound("MAGICRES"); + CCS->soundh->playSound(AudioPath::builtin("MAGICRES")); }); } diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 937d6ca0e..971d94d46 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -580,7 +580,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface break; } - CCS->musich->playMusic("Music/Win Battle", false, true); + CCS->musich->playMusic(AudioPath::builtin("Music/Win Battle"), false, true); CCS->videoh->open(VideoPath::builtin("WIN3.BIK")); std::string str = CGI->generaltexth->allTexts[text]; @@ -597,19 +597,19 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface else // we lose { int text = 311; - std::string musicName = "Music/LoseCombat"; + AudioPath musicName = AudioPath::builtin("Music/LoseCombat"); VideoPath videoName = VideoPath::builtin("LBSTART.BIK"); switch(br.result) { case EBattleResult::NORMAL: break; case EBattleResult::ESCAPE: - musicName = "Music/Retreat Battle"; + musicName = AudioPath::builtin("Music/Retreat Battle"); videoName = VideoPath::builtin("RTSTART.BIK"); text = 310; break; case EBattleResult::SURRENDER: - musicName = "Music/Surrender Battle"; + musicName = AudioPath::builtin("Music/Surrender Battle"); videoName = VideoPath::builtin("SURRENDER.BIK"); text = 309; break; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 3b88b75f4..7bb7a908f 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -340,7 +340,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) for (auto attackInfo : ca.attackedParts) positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); - CCS->soundh->playSound( "WALLHIT" ); + CCS->soundh->playSound( AudioPath::builtin("WALLHIT") ); owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions)); } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 32c401612..e9c255187 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -462,7 +462,7 @@ void BattleStacksController::stacksAreAttacked(std::vector at addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); if (attackedInfo.fireShield) - owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, "FIRESHIE", attackedInfo.attacker->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition()); if (attackedInfo.spellEffect != SpellID::NONE) { @@ -481,7 +481,7 @@ void BattleStacksController::stacksAreAttacked(std::vector at if (attackedInfo.rebirth) { owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - owner.effectsController->displayEffect(EBattleEffect::RESURRECT, "RESURECT", attackedInfo.defender->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition()); addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); }); } @@ -594,7 +594,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info ) { owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); - owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, "GOODLUCK", attacker->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition()); }); } @@ -602,7 +602,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info ) { owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); - owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, "BADLUCK", attacker->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition()); }); } @@ -610,7 +610,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info ) { owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); - owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, "DEATHBLO", defender->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition()); }); for(auto elem : info.secondaryDefender) @@ -645,7 +645,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info ) { owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() { - owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, "DRAINLIF", attacker->getPosition()); + owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition()); }); } diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 4fa6a65b2..526370897 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -114,7 +114,7 @@ CBonusSelection::CBonusSelection() } if (!getCampaign()->getMusic().empty()) - CCS->musich->playMusic( "Music/" + getCampaign()->getMusic(), true, false); + CCS->musich->playMusic( getCampaign()->getMusic(), true, false); } void CBonusSelection::createBonusesIcons() diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 4c3fbbf4a..1eb555d65 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -339,7 +339,7 @@ void CChatBox::keyPressed(EShortcut key) void CChatBox::addNewMessage(const std::string & text) { - CCS->soundh->playSound("CHAT"); + CCS->soundh->playSound(AudioPath::builtin("CHAT")); chatHistory->setText(chatHistory->label->getText() + text + "\n"); if(chatHistory->slider) chatHistory->slider->scrollToMax(); diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index b7922003b..45708a64b 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -75,7 +75,7 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config) void CCampaignScreen::activate() { - CCS->musich->playMusic("Music/MainMenu", true, false); + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, false); CWindowObject::activate(); } @@ -129,12 +129,12 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to) // Play the campaign button video when the mouse cursor is placed over the button if(isHovered()) { - if(CCS->videoh->fname != video) + if(CCS->videoh->fname != video.addPrefix("VIDEO/")) CCS->videoh->open(video); CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); // plays sequentially frame by frame, starts at the beginning when the video is over } - else if(CCS->videoh->fname == video) // When you got out of the bounds of the button then close the video + else if(CCS->videoh->fname == video.addPrefix("VIDEO/")) // When you got out of the bounds of the button then close the video { CCS->videoh->close(); redraw(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 6a8d226c1..0327ff4ac 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -114,7 +114,7 @@ void CMenuScreen::show(Canvas & to) void CMenuScreen::activate() { - CCS->musich->playMusic("Music/MainMenu", true, true); + CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true); if(!config["video"].isNull()) CCS->videoh->open(VideoPath::fromJson(config["video"]["name"])); CIntObject::activate(); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index e4db4fc94..b8b7ab49e 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -28,7 +28,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f updateShadow(); CCS->videoh->open(spe.prologVideo); - CCS->musich->playMusic("Music/" + spe.prologMusic, true, true); + CCS->musich->playMusic(spe.prologMusic, true, true); // MPTODO: Custom campaign crashing on this? // voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 6d2a20f7a..947cce37f 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -182,7 +182,7 @@ void CAnimation::init() if (vstd::contains(graphics->imageLists, name.getName())) initFromJson(graphics->imageLists[name.getName()]); - auto jsonResource = name.toType(); + auto jsonResource = name.toType(); auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); for(auto & loader : configList) diff --git a/include/vcmi/spells/Spell.h b/include/vcmi/spells/Spell.h index 38a9a247b..60e7b8023 100644 --- a/include/vcmi/spells/Spell.h +++ b/include/vcmi/spells/Spell.h @@ -45,7 +45,6 @@ public: virtual bool hasSchool(SpellSchool school) const = 0; virtual void forEachSchool(const SchoolCallback & cb) const = 0; - virtual const std::string & getCastSound() const = 0; virtual int32_t getCost(const int32_t skillLevel) const = 0; virtual int32_t getBasePower() const = 0; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 10b293f0a..86074a4f9 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -955,17 +955,14 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c creature->special = config["special"].Bool() || config["disabled"].Bool(); const JsonNode & sounds = config["sound"]; - -#define GET_SOUND_VALUE(value_name) creature->sounds.value_name = sounds[#value_name].String() - GET_SOUND_VALUE(attack); - GET_SOUND_VALUE(defend); - GET_SOUND_VALUE(killed); - GET_SOUND_VALUE(move); - GET_SOUND_VALUE(shoot); - GET_SOUND_VALUE(wince); - GET_SOUND_VALUE(startMoving); - GET_SOUND_VALUE(endMoving); -#undef GET_SOUND_VALUE + creature->sounds.attack = AudioPath::fromJson(sounds["attack"]); + creature->sounds.defend = AudioPath::fromJson(sounds["defend"]); + creature->sounds.killed = AudioPath::fromJson(sounds["killed"]); + creature->sounds.move = AudioPath::fromJson(sounds["move"]); + creature->sounds.shoot = AudioPath::fromJson(sounds["shoot"]); + creature->sounds.wince = AudioPath::fromJson(sounds["wince"]); + creature->sounds.startMoving = AudioPath::fromJson(sounds["startMoving"]); + creature->sounds.endMoving = AudioPath::fromJson(sounds["endMoving"]); } void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index f1dee53ca..b6cc5b11f 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -132,14 +132,14 @@ public: //sound info struct CreatureBattleSounds { - std::string attack; - std::string defend; - std::string killed; // was killed or died - std::string move; - std::string shoot; // range attack - std::string wince; // attacked but did not die - std::string startMoving; - std::string endMoving; + AudioPath attack; + AudioPath defend; + AudioPath killed; // was killed or died + AudioPath move; + AudioPath shoot; // range attack + AudioPath wince; // attacked but did not die + AudioPath startMoving; + AudioPath endMoving; template void serialize(Handler &h, const int version) { diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index e11a3e903..3e54d7cf7 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -48,7 +48,7 @@ void CGeneralTextHandler::detectInstallParameters() "ukrainian" } }; - if(!CResourceHandler::get("core")->existsResource(ResourcePath("DATA/GENRLTXT.TXT", EResType::TEXT))) + if(!CResourceHandler::get("core")->existsResource(TextPath::builtin("DATA/GENRLTXT.TXT"))) { Settings language = settings.write["session"]["language"]; language->String() = "english"; @@ -64,7 +64,7 @@ void CGeneralTextHandler::detectInstallParameters() // load file that will be used for footprint generation // this is one of the most text-heavy files in game and consists solely from translated texts - auto resource = CResourceHandler::get("core")->load(ResourcePath("DATA/GENRLTXT.TXT", EResType::TEXT)); + auto resource = CResourceHandler::get("core")->load(TextPath::builtin("DATA/GENRLTXT.TXT")); std::array charCount{}; std::array footprint{}; @@ -429,7 +429,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; - if (CResourceHandler::get()->existsResource(ResourcePath(QE_MOD_COMMANDS, EResType::TEXT))) + if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); { @@ -574,7 +574,7 @@ CGeneralTextHandler::CGeneralTextHandler(): } if (VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) { - if(CResourceHandler::get()->existsResource(ResourcePath("DATA/ZNPC00.TXT", EResType::TEXT))) + if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT"))) readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); } } diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 3268b20f5..b5d2fcec9 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -878,7 +878,7 @@ void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]); info.hallBackground = ImagePath::fromJson(source["hallBackground"]); - info.musicTheme = source["musicTheme"].String(); + info.musicTheme = AudioPath::fromJson(source["musicTheme"]); info.townBackground = ImagePath::fromJson(source["townBackground"]); info.guildWindow = ImagePath::fromJson(source["guildWindow"]); info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index bb24808b9..01c8c5dbe 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -305,7 +305,7 @@ public: std::string iconSmall[2][2]; /// icon names used during loading std::string iconLarge[2][2]; VideoPath tavernVideo; - std::string musicTheme; + AudioPath musicTheme; ImagePath townBackground; ImagePath guildBackground; ImagePath guildWindow; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index a19c9f11e..ebce9c261 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -116,7 +116,7 @@ public: virtual void showTavernWindow(const CGObjectInstance *townOrTavern){}; virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; virtual void showQuestLog(){}; - virtual void advmapSpellCast(const CGHeroInstance * caster, int spellID){}; //called when a hero casts a spell + virtual void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID){}; //called when a hero casts a spell virtual void tileHidden(const std::unordered_set &pos){}; virtual void tileRevealed(const std::unordered_set &pos){}; virtual void newObject(const CGObjectInstance * obj){}; //eg. ship built in shipyard diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index ce1b2a96d..a395fe4b8 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -1036,13 +1036,13 @@ namespace std::string testAnimation(const std::string & path, const std::string & scope) { TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); - TEST_FILE(scope, "Sprites/", path, EResType::TEXT); + TEST_FILE(scope, "Sprites/", path, EResType::JSON); return "Animation file \"" + path + "\" was not found"; } std::string textFile(const JsonNode & node) { - TEST_FILE(node.meta, "", node.String(), EResType::TEXT); + TEST_FILE(node.meta, "", node.String(), EResType::JSON); return "Text file \"" + node.String() + "\" was not found"; } diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index d5b2ab3f0..958e46e62 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -32,7 +32,7 @@ public: Obstacle obstacle; si32 iconIndex; std::string identifier; - std::string appearSound; + AudioPath appearSound; AnimationPath appearAnimation; AnimationPath animation; std::vector allowedTerrains; diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 0c8ce487f..a6050cb40 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -27,10 +27,10 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const info->identifier = identifier; info->modScope = scope; info->moveCost = static_cast(json["moveCost"].Integer()); - info->musicFilename = json["music"].String(); + info->musicFilename = AudioPath::fromJson(json["music"]); info->tilesFilename = AnimationPath::fromJson(json["tiles"]); - info->horseSound = json["horseSound"].String(); - info->horseSoundPenalty = json["horseSoundPenalty"].String(); + info->horseSound = AudioPath::fromJson(json["horseSound"]); + info->horseSoundPenalty = AudioPath::fromJson(json["horseSoundPenalty"]); info->transitionRequired = json["transitionRequired"].Bool(); info->terrainViewPatterns = json["terrainViewPatterns"].String(); diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index fa7623136..7455a0cbe 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -67,11 +67,11 @@ public: ColorRGBA minimapBlocked; ColorRGBA minimapUnblocked; std::string shortIdentifier; - std::string musicFilename; + AudioPath musicFilename; AnimationPath tilesFilename; std::string terrainViewPatterns; - std::string horseSound; - std::string horseSoundPenalty; + AudioPath horseSound; + AudioPath horseSoundPenalty; std::vector paletteAnimation; diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index e1de4b984..729409727 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -70,7 +70,7 @@ const AnimationPath & CObstacleInstance::getAppearAnimation() const return getInfo().appearAnimation; } -const std::string & CObstacleInstance::getAppearSound() const +const AudioPath & CObstacleInstance::getAppearSound() const { return getInfo().appearSound; } @@ -118,7 +118,7 @@ void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) //We need only a subset of obstacle info for correct render handler.serializeInt("position", pos); - handler.serializeString("appearSound", obstacleInfo.appearSound); + handler.serializeStruct("appearSound", obstacleInfo.appearSound); handler.serializeStruct("appearAnimation", obstacleInfo.appearAnimation); handler.serializeStruct("animation", obstacleInfo.animation); handler.serializeInt("animationYOffset", animationYOffset); @@ -213,7 +213,7 @@ void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) handler.serializeBool("removeOnTrigger", removeOnTrigger); handler.serializeBool("nativeVisible", nativeVisible); - handler.serializeString("appearSound", appearSound); + handler.serializeStruct("appearSound", appearSound); handler.serializeStruct("appearAnimation", appearAnimation); handler.serializeStruct("animation", animation); @@ -249,7 +249,7 @@ const AnimationPath & SpellCreatedObstacle::getAppearAnimation() const return appearAnimation; } -const std::string & SpellCreatedObstacle::getAppearSound() const +const AudioPath & SpellCreatedObstacle::getAppearSound() const { return appearSound; } diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index 0d74d95fd..b3759d8c4 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -54,7 +54,7 @@ struct DLL_LINKAGE CObstacleInstance //Client helper functions, make it easier to render animations virtual const AnimationPath & getAnimation() const; virtual const AnimationPath & getAppearAnimation() const; - virtual const std::string & getAppearSound() const; + virtual const AudioPath & getAppearSound() const; virtual int getAnimationYOffset(int imageHeight) const; @@ -88,7 +88,7 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance bool revealed; bool nativeVisible; //Should native terrain creatures reveal obstacle - std::string appearSound; + AudioPath appearSound; AnimationPath appearAnimation; AnimationPath animation; @@ -110,7 +110,7 @@ struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance //Client helper functions, make it easier to render animations const AnimationPath & getAnimation() const override; const AnimationPath & getAppearAnimation() const override; - const std::string & getAppearSound() const override; + const AudioPath & getAppearSound() const override; int getAnimationYOffset(int imageHeight) const override; void fromInfo(const ObstacleChanges & info); diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index fb6dc65da..ab2cc63c5 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -152,7 +152,7 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.name = reader["name"].String(); ret.description = reader["description"].String(); ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); - ret.music = reader["music"].String(); + ret.music = AudioPath::fromJson(reader["music"]); ret.filename = filename; ret.modName = modName; ret.encoding = encoding; @@ -167,7 +167,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) if(ret.hasPrologEpilog) { ret.prologVideo = VideoPath::fromJson(identifier["video"]); - ret.prologMusic = identifier["music"].String(); + ret.prologMusic = AudioPath::fromJson(identifier["music"]); ret.prologText = identifier["text"].String(); } return ret; @@ -599,10 +599,10 @@ VideoPath CampaignHandler::prologVideoName(ui8 index) return VideoPath(); } -std::string CampaignHandler::prologMusicName(ui8 index) +AudioPath CampaignHandler::prologMusicName(ui8 index) { std::vector music; - return VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index))); + return AudioPath::builtinTODO(VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index)))); } std::string CampaignHandler::prologVoiceName(ui8 index) diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index 5ecf6ca5c..7a6d0fd1f 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -34,7 +34,7 @@ class DLL_LINKAGE CampaignHandler static std::vector> getFile(std::unique_ptr file, bool headerOnly); static VideoPath prologVideoName(ui8 index); - static std::string prologMusicName(ui8 index); + static AudioPath prologMusicName(ui8 index); static std::string prologVoiceName(ui8 index); public: diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 44dd1c961..00b321188 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -17,7 +17,7 @@ struct DLL_LINKAGE CampaignScenarioPrologEpilog { bool hasPrologEpilog = false; VideoPath prologVideo; // from CmpMovie.txt - std::string prologMusic; // from CmpMusic.txt + AudioPath prologMusic; // from CmpMusic.txt std::string prologText; template void serialize(Handler &h, const int formatVersion) diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 53e8fb3ec..f8bfe0271 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -159,7 +159,7 @@ std::string CampaignHeader::getEncoding() const return encoding; } -std::string CampaignHeader::getMusic() const +AudioPath CampaignHeader::getMusic() const { return music; } diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index aa73801e3..247d86fa4 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -76,7 +76,7 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable CampaignRegions campaignRegions; std::string name; std::string description; - std::string music; + AudioPath music; std::string filename; std::string modName; std::string encoding; @@ -95,7 +95,7 @@ public: std::string getFilename() const; std::string getModName() const; std::string getEncoding() const; - std::string getMusic() const; + AudioPath getMusic() const; const CampaignRegions & getRegions() const; diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 8298ae15c..9175d0d2d 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -103,7 +103,7 @@ std::unordered_map CFilesystemLoader::lis { static const EResType initArray[] = { EResType::DIRECTORY, - EResType::TEXT, + EResType::JSON, EResType::ARCHIVE_LOD, EResType::ARCHIVE_VID, EResType::ARCHIVE_SND, diff --git a/lib/filesystem/Filesystem.cpp b/lib/filesystem/Filesystem.cpp index 726e9bb8b..84378f56b 100644 --- a/lib/filesystem/Filesystem.cpp +++ b/lib/filesystem/Filesystem.cpp @@ -113,10 +113,10 @@ void CFilesystemGenerator::loadArchive(const std::string &mountPoint, const Json void CFilesystemGenerator::loadJsonMap(const std::string &mountPoint, const JsonNode & config) { std::string URI = prefix + config["path"].String(); - auto filename = CResourceHandler::get("initial")->getResourceName(ResourcePath(URI, EResType::TEXT)); + auto filename = CResourceHandler::get("initial")->getResourceName(JsonPath::builtin(URI)); if (filename) { - auto configData = CResourceHandler::get("initial")->load(ResourcePath(URI, EResType::TEXT))->readAll(); + auto configData = CResourceHandler::get("initial")->load(JsonPath::builtin(URI))->readAll(); const JsonNode configInitial(reinterpret_cast(configData.first.get()), configData.second); filesystem->addLoader(new CMappedFileLoader(mountPoint, configInitial), false); } @@ -210,7 +210,7 @@ ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier) void CResourceHandler::load(const std::string &fsConfigURI, bool extractArchives) { - auto fsConfigData = get("initial")->load(ResourcePath(fsConfigURI, EResType::TEXT))->readAll(); + auto fsConfigData = get("initial")->load(JsonPath::builtin(fsConfigURI))->readAll(); const JsonNode fsConfig(reinterpret_cast(fsConfigData.first.get()), fsConfigData.second); diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index 120cb5203..cba1fbb43 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -94,7 +94,7 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension) static const std::map stringToRes = { {".TXT", EResType::TEXT}, - {".JSON", EResType::TEXT}, + {".JSON", EResType::JSON}, {".DEF", EResType::ANIMATION}, {".MSK", EResType::MASK}, {".MSG", EResType::MASK}, @@ -148,6 +148,7 @@ std::string EResTypeHelper::getEResTypeAsString(EResType type) static const std::map stringToRes = { MAP_ENUM(TEXT) + MAP_ENUM(JSON) MAP_ENUM(ANIMATION) MAP_ENUM(MASK) MAP_ENUM(CAMPAIGN) diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index be2d30d20..8ed843c40 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -140,6 +140,12 @@ public: :ResourcePath("", Type) {} + static ResourcePathTempl fromResource(const ResourcePath & resource) + { + assert(Type == resource.getType()); + return ResourcePathTempl(resource); + } + static ResourcePathTempl builtin(const std::string & filename) { return ResourcePathTempl(filename, Type); @@ -177,6 +183,7 @@ using ImagePath = ResourcePathTempl; using TextPath = ResourcePathTempl; using JsonPath = ResourcePathTempl; using VideoPath = ResourcePathTempl; +using AudioPath = ResourcePathTempl; namespace EResTypeHelper { diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 3b3e859ad..9766f4123 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -83,13 +83,13 @@ void AObjectTypeHandler::init(const JsonNode & input) } for(const JsonNode & node : input["sounds"]["ambient"].Vector()) - sounds.ambient.push_back(node.String()); + sounds.ambient.push_back(AudioPath::fromJson(node)); for(const JsonNode & node : input["sounds"]["visit"].Vector()) - sounds.visit.push_back(node.String()); + sounds.visit.push_back(AudioPath::fromJson(node)); for(const JsonNode & node : input["sounds"]["removal"].Vector()) - sounds.removal.push_back(node.String()); + sounds.removal.push_back(AudioPath::fromJson(node)); if(input["aiValue"].isNull()) aiValue = std::nullopt; diff --git a/lib/mapObjectConstructors/SObjectSounds.h b/lib/mapObjectConstructors/SObjectSounds.h index ca1b8fef9..8d41817c0 100644 --- a/lib/mapObjectConstructors/SObjectSounds.h +++ b/lib/mapObjectConstructors/SObjectSounds.h @@ -9,13 +9,15 @@ */ #pragma once +#include "../filesystem/ResourcePath.h" + VCMI_LIB_NAMESPACE_BEGIN struct SObjectSounds { - std::vector ambient; - std::vector visit; - std::vector removal; + std::vector ambient; + std::vector visit; + std::vector removal; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 4b081c4e5..92a2c96ce 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -214,7 +214,7 @@ std::string CGObjectInstance::getObjectName() const return VLC->objtypeh->getObjectName(ID, subID); } -std::optional CGObjectInstance::getAmbientSound() const +std::optional CGObjectInstance::getAmbientSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).ambient; if(!sounds.empty()) @@ -223,7 +223,7 @@ std::optional CGObjectInstance::getAmbientSound() const return std::nullopt; } -std::optional CGObjectInstance::getVisitSound() const +std::optional CGObjectInstance::getVisitSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).visit; if(!sounds.empty()) @@ -232,7 +232,7 @@ std::optional CGObjectInstance::getVisitSound() const return std::nullopt; } -std::optional CGObjectInstance::getRemovalSound() const +std::optional CGObjectInstance::getRemovalSound() const { const auto & sounds = VLC->objtypeh->getObjectSounds(ID, subID).removal; if(!sounds.empty()) diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 03e27ebc9..4360e610f 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -82,9 +82,9 @@ public: virtual bool isTile2Terrain() const { return false; } - std::optional getAmbientSound() const; - std::optional getVisitSound() const; - std::optional getRemovalSound() const; + std::optional getAmbientSound() const; + std::optional getVisitSound() const; + std::optional getRemovalSound() const; /** VIRTUAL METHODS **/ diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 3190ea6e8..cab269cab 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -904,7 +904,7 @@ std::unique_ptr CMapLoaderJson::loadMapHeader() JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) { - ResourcePath resource(archiveFilename, EResType::TEXT); + JsonPath resource = JsonPath::builtin(archiveFilename); if(!loader.existsResource(resource)) throw std::runtime_error(archiveFilename+" not found"); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index d5ac49775..7e5478212 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -310,7 +310,7 @@ const std::string & CSpell::getIconScroll() const return iconScroll; } -const std::string & CSpell::getCastSound() const +const AudioPath & CSpell::getCastSound() const { return castSound; } @@ -896,7 +896,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & } const JsonNode & soundsNode = json["sounds"]; - spell->castSound = soundsNode["cast"].String(); + spell->castSound = AudioPath::fromJson(soundsNode["cast"]); //load level attributes const int levelsCount = GameConstants::SPELL_SCHOOL_LEVELS; diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index 9d8bea267..ccf575518 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -262,7 +262,7 @@ public: const std::string & getIconScenarioBonus() const; const std::string & getIconScroll() const; - const std::string & getCastSound() const override; + const AudioPath & getCastSound() const; void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); @@ -354,7 +354,7 @@ private: std::string iconScroll; ///sound related stuff - std::string castSound; + AudioPath castSound; std::vector levels; diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 428e065e3..720bc1fa0 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -85,7 +85,7 @@ void ObstacleSideOptions::serializeJson(JsonSerializeFormat & handler) serializeRelativeShape(handler, "shape", shape); serializeRelativeShape(handler, "range", range); - handler.serializeString("appearSound", appearSound); + handler.serializeStruct("appearSound", appearSound); handler.serializeStruct("appearAnimation", appearAnimation); handler.serializeStruct("animation", animation); diff --git a/lib/spells/effects/Obstacle.h b/lib/spells/effects/Obstacle.h index a1e9b1f56..1dd256e1a 100644 --- a/lib/spells/effects/Obstacle.h +++ b/lib/spells/effects/Obstacle.h @@ -30,7 +30,7 @@ public: RelativeShape shape; //shape of single obstacle relative to obstacle position RelativeShape range; //position of obstacles relative to effect destination - std::string appearSound; + AudioPath appearSound; AnimationPath appearAnimation; AnimationPath animation; diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 80795d02a..412810328 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -583,7 +583,7 @@ void Animation::init() source[defEntry.first].resize(defEntry.second); } - ResourcePath resID(std::string("SPRITES/") + name, EResType::TEXT); + JsonPath resID = JsonPath::builtin("SPRITES/" + name); //if(vstd::contains(graphics->imageLists, resID.getName())) //initFromJson(graphics->imageLists[resID.getName()]); diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index f1b1a4d77..26082c8c9 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -90,7 +90,7 @@ TEST(MapFormat, Random) static JsonNode getFromArchive(CZipLoader & archive, const std::string & archiveFilename) { - ResourcePath resource(archiveFilename, EResType::TEXT); + JsonPath resource = JsonPath::builtin(archiveFilename); if(!archive.existsResource(resource)) throw std::runtime_error(archiveFilename + " not found"); From 0f88b8969bdab54e544be4e12f6b26e8794e1453 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 13:39:42 +0300 Subject: [PATCH 0318/1248] Removed some usages of std string as resource path --- client/adventureMap/AdventureMapWidget.cpp | 17 ++++----- client/adventureMap/AdventureMapWidget.h | 6 +-- client/render/CAnimation.cpp | 2 +- client/render/CBitmapHandler.cpp | 43 ++++++++++++---------- client/render/CBitmapHandler.h | 4 +- client/renderSDL/SDLImage.cpp | 10 ++--- client/renderSDL/SDLImage.h | 2 +- client/windows/GUIClasses.cpp | 2 +- lib/mapping/MapIdentifiersH3M.cpp | 2 +- lib/modding/CModHandler.cpp | 5 +-- mapeditor/Animation.cpp | 4 +- 11 files changed, 49 insertions(+), 48 deletions(-) diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index ebff69812..aea6e80bd 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -59,10 +59,7 @@ AdventureMapWidget::AdventureMapWidget( std::shared_ptr s const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) - { - ResourcePath resourceName(entry.String(), EResType::IMAGE); - playerColorerImages.push_back(resourceName.getName()); - } + playerColorerImages.push_back(ImagePath::fromJson(entry)); build(config); addUsedEvents(KEYBOARD); @@ -131,20 +128,20 @@ std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) { ImagePath resource = ImagePath::fromJson(name); - if(images.count(resource.getName()) == 0) - images[resource.getName()] = IImage::createFromFile(resource); + if(images.count(resource) == 0) + images[resource] = IImage::createFromFile(resource); - return images[resource.getName()]; + return images[resource]; } std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & name) { AnimationPath resource = AnimationPath::fromJson(name); - if(animations.count(resource.getName()) == 0) - animations[resource.getName()] = std::make_shared(resource); + if(animations.count(resource) == 0) + animations[resource] = std::make_shared(resource); - return animations[resource.getName()]; + return animations[resource]; } std::shared_ptr AdventureMapWidget::buildInfobox(const JsonNode & input) diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index 185440397..fca239ffc 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -29,11 +29,11 @@ class AdventureMapWidget : public InterfaceObjectConfigurable std::vector subwidgetSizes; /// list of images on which player-colored palette will be applied - std::vector playerColorerImages; + std::vector playerColorerImages; /// list of named images shared between widgets - std::map> images; - std::map> animations; + std::map> images; + std::map> animations; /// Widgets that require access from adventure map std::shared_ptr heroList; diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 947cce37f..a99a9e306 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -69,7 +69,7 @@ bool CAnimation::loadFrame(size_t frame, size_t group) // still here? image is missing printError(frame, group, "LoadFrame"); - images[group][frame] = std::make_shared("DEFAULT", EImageBlitMode::ALPHA); + images[group][frame] = std::make_shared(ImagePath::builtin("DEFAULT"), EImageBlitMode::ALPHA); } else //load from separate file { diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index 9fc4e93ac..626d3d31f 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -21,7 +21,7 @@ namespace BitmapHandler { SDL_Surface * loadH3PCX(ui8 * data, size_t size); - SDL_Surface * loadBitmapFromDir(std::string path, std::string fname); + SDL_Surface * loadBitmapFromDir(const ImagePath & path); } bool isPCX(const ui8 *header)//check whether file can be PCX according to header @@ -103,28 +103,23 @@ SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) return ret; } -SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fname) +SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) { - if(!fname.size()) - { - logGlobal->warn("Call to loadBitmap with void fname!"); - return nullptr; - } - if (!CResourceHandler::get()->existsResource(ResourcePath(path + fname, EResType::IMAGE))) + if (!CResourceHandler::get()->existsResource(path)) { return nullptr; } SDL_Surface * ret=nullptr; - auto readFile = CResourceHandler::get()->load(ResourcePath(path + fname, EResType::IMAGE))->readAll(); + auto readFile = CResourceHandler::get()->load(path)->readAll(); if (isPCX(readFile.first.get())) {//H3-style PCX ret = loadH3PCX(readFile.first.get(), readFile.second); if (!ret) { - logGlobal->error("Failed to open %s as H3 PCX!", fname); + logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName()); return nullptr; } } @@ -146,7 +141,7 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna } else { - logGlobal->error("Failed to open %s via SDL_Image", fname); + logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName()); logGlobal->error("Reason: %s", IMG_GetError()); return nullptr; } @@ -187,19 +182,29 @@ SDL_Surface * BitmapHandler::loadBitmapFromDir(std::string path, std::string fna { CSDL_Ext::setDefaultColorKey(ret); } - return ret; } -SDL_Surface * BitmapHandler::loadBitmap(std::string fname) +SDL_Surface * BitmapHandler::loadBitmap(const ImagePath & fname) { - SDL_Surface * bitmap = nullptr; - - if (!(bitmap = loadBitmapFromDir("DATA/", fname)) && - !(bitmap = loadBitmapFromDir("SPRITES/", fname))) + if(fname.empty()) { - logGlobal->error("Error: Failed to find file %s", fname); + logGlobal->warn("Call to loadBitmap with void fname!"); + return nullptr; } - return bitmap; + SDL_Surface * bitmap = loadBitmapFromDir(fname); + if (bitmap != nullptr) + return bitmap; + + SDL_Surface * bitmapData = loadBitmapFromDir(fname.addPrefix("DATA/")); + if (bitmapData != nullptr) + return bitmapData; + + SDL_Surface * bitmapSprites = loadBitmapFromDir(fname.addPrefix("SPRITES/")); + if (bitmapSprites != nullptr) + return bitmapSprites; + + logGlobal->error("Error: Failed to find file %s", fname.getOriginalName()); + return nullptr; } diff --git a/client/render/CBitmapHandler.h b/client/render/CBitmapHandler.h index b36156211..3bd8a7e82 100644 --- a/client/render/CBitmapHandler.h +++ b/client/render/CBitmapHandler.h @@ -9,10 +9,12 @@ */ #pragma once +#include "../../lib/filesystem/ResourcePath.h" + struct SDL_Surface; namespace BitmapHandler { //Load file from /DATA or /SPRITES - SDL_Surface * loadBitmap(std::string fname); + SDL_Surface * loadBitmap(const ImagePath & fname); } diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 0a53abb23..dbbe7dd6d 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -31,7 +31,7 @@ std::shared_ptr IImage::createFromFile( const ImagePath & path ) std::shared_ptr IImage::createFromFile( const ImagePath & path, EImageBlitMode mode ) { - return std::shared_ptr(new SDLImage(path.getName(), mode)); + return std::shared_ptr(new SDLImage(path, mode)); } std::shared_ptr IImage::createFromSurface( SDL_Surface * source ) @@ -89,9 +89,7 @@ SDLImage::SDLImage(const JsonNode & conf, EImageBlitMode mode) fullSize(0, 0), originalPalette(nullptr) { - std::string filename = conf["file"].String(); - - surf = BitmapHandler::loadBitmap(filename); + surf = BitmapHandler::loadBitmap(ImagePath::fromJson(conf["file"])); if(surf == nullptr) return; @@ -118,7 +116,7 @@ SDLImage::SDLImage(const JsonNode & conf, EImageBlitMode mode) } } -SDLImage::SDLImage(std::string filename, EImageBlitMode mode) +SDLImage::SDLImage(const ImagePath & filename, EImageBlitMode mode) : surf(nullptr), margins(0, 0), fullSize(0, 0), @@ -128,7 +126,7 @@ SDLImage::SDLImage(std::string filename, EImageBlitMode mode) if(surf == nullptr) { - logGlobal->error("Error: failed to load image %s", filename); + logGlobal->error("Error: failed to load image %s", filename.getOriginalName()); return; } else diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 74a28e003..10aecd70d 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -43,7 +43,7 @@ public: //Load image from def file SDLImage(CDefFile *data, size_t frame, size_t group=0); //Load from bitmap file - SDLImage(std::string filename, EImageBlitMode blitMode); + SDLImage(const ImagePath & filename, EImageBlitMode blitMode); SDLImage(const JsonNode & conf, EImageBlitMode blitMode); //Create using existing surface, extraRef will increase refcount on SDL_Surface diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index aae29023e..a83ec3640 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -618,7 +618,7 @@ static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADE static bool isQuickExchangeLayoutAvailable() { - return CResourceHandler::get()->existsResource(ResourcePath(std::string("SPRITES/") + QUICK_EXCHANGE_BG, EResType::IMAGE)); + return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG)); } CExchangeController::CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2) diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 2a50df036..70789a001 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -56,7 +56,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); std::string vcmiName = boost::to_lower_copy(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(ResourcePath( "SPRITES/" + vcmiName, EResType::ANIMATION))) + if (!CResourceHandler::get()->existsResource(AnimationPath::builtin("SPRITES/" + vcmiName))) logMod->warn("Template animation file %s was not found!", vcmiName); mappingObjectTemplate[h3mName] = vcmiName; diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index ac1a7ad93..78fa70a5c 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -290,9 +290,8 @@ static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoa // third - add all detected text files from this mod into checksum auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) { - return resID.getType() == EResType::TEXT && - ( boost::starts_with(resID.getName(), "DATA") || - boost::starts_with(resID.getName(), "CONFIG")); + return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) && + ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); }); for (const ResourcePath & file : files) diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 412810328..7bced089f 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -169,7 +169,7 @@ DefFile::DefFile(std::string Name): qRgba(0, 0, 0, 128), // 50% - shadow body below selection qRgba(0, 0, 0, 64) // 75% - shadow border below selection }; - data = animationCache.getCachedFile(ResourcePath(std::string("SPRITES/") + Name, EResType::ANIMATION)); + data = animationCache.getCachedFile(AnimationPath::builtin("SPRITES/" + Name)); palette = std::make_unique>(256); int it = 0; @@ -656,7 +656,7 @@ Animation::Animation(std::string Name): name.erase(dotPos); std::transform(name.begin(), name.end(), name.begin(), toupper); - ResourcePath resource(std::string("SPRITES/") + name, EResType::ANIMATION); + auto resource = AnimationPath::builtin("SPRITES/" + name); if(CResourceHandler::get()->existsResource(resource)) defFile = std::make_shared(name); From 9cfcf5ea19585a8007a27bebbb83ef5737165ec7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 14:29:02 +0300 Subject: [PATCH 0319/1248] Fix regressions --- client/CMusicHandler.cpp | 12 ++++++++---- client/CVideoHandler.cpp | 8 ++++++-- client/adventureMap/CInfoBar.cpp | 10 +++++----- client/adventureMap/MapAudioPlayer.cpp | 2 +- client/mainmenu/CCampaignScreen.cpp | 15 +++++---------- client/render/CAnimation.cpp | 4 +--- client/windows/CKingdomInterface.cpp | 6 +++--- client/windows/CKingdomInterface.h | 2 +- client/windows/CPuzzleWindow.cpp | 2 +- lib/CConfigHandler.cpp | 2 +- lib/JsonNode.cpp | 2 +- lib/filesystem/CFilesystemLoader.cpp | 2 +- lib/filesystem/ResourcePath.h | 16 +++++++++++----- 13 files changed, 45 insertions(+), 38 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 65c61e933..273404229 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -566,19 +566,23 @@ void MusicEntry::load(const AudioPath & musicURI) music = nullptr; } - currentName = musicURI; + if (CResourceHandler::get()->existsResource(musicURI)) + currentName = musicURI; + else + currentName = musicURI.addPrefix("MUSIC/"); + music = nullptr; - logGlobal->trace("Loading music file %s", musicURI.getOriginalName()); + logGlobal->trace("Loading music file %s", currentName.getOriginalName()); try { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(musicURI)); + auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); } catch(std::exception &e) { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, musicURI.getOriginalName()); + logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); logGlobal->error("Exception: %s", e.what()); } diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 6bf48fc35..cb2eba631 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -77,14 +77,18 @@ bool CVideoPlayer::open(const VideoPath & fname, bool scale) // loop = to loop through the video // useOverlay = directly write to the screen. -bool CVideoPlayer::open(const VideoPath & fname, bool loop, bool useOverlay, bool scale) +bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale) { close(); - this->fname = fname.addPrefix("VIDEO/"); doLoop = loop; frameTime = 0; + if (CResourceHandler::get()->existsResource(videoToOpen)) + fname = videoToOpen; + else + fname = videoToOpen.addPrefix("VIDEO/"); + if (!CResourceHandler::get()->existsResource(fname)) { logGlobal->error("Error: video %s was not found", fname.getName()); diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index cbfd5e835..ef23a86b9 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -94,18 +94,18 @@ AnimationPath CInfoBar::VisibleDateInfo::getNewDayName() return AnimationPath::builtin("NEWDAY"); if(LOCPLINT->cb->getDate(Date::DAY_OF_WEEK) != 1) - return AnimationPath("NEWDAY"); + return AnimationPath::builtin("NEWDAY"); switch(LOCPLINT->cb->getDate(Date::WEEK)) { case 1: - return AnimationPath("NEWWEEK1"); + return AnimationPath::builtin("NEWWEEK1"); case 2: - return AnimationPath("NEWWEEK2"); + return AnimationPath::builtin("NEWWEEK2"); case 3: - return AnimationPath("NEWWEEK3"); + return AnimationPath::builtin("NEWWEEK3"); case 4: - return AnimationPath("NEWWEEK4"); + return AnimationPath::builtin("NEWWEEK4"); default: return AnimationPath(); } diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 6697d7814..fafbe9c18 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -140,7 +140,7 @@ std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) } if(CGI->mh->getMap()->isCoastalTile(tile)) - result.emplace_back("LOOPOCEA"); + result.emplace_back(AudioPath::builtin("LOOPOCEA")); return result; } diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 45708a64b..8a1282e2d 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -128,17 +128,7 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to) // Play the campaign button video when the mouse cursor is placed over the button if(isHovered()) - { - if(CCS->videoh->fname != video.addPrefix("VIDEO/")) - CCS->videoh->open(video); - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); // plays sequentially frame by frame, starts at the beginning when the video is over - } - else if(CCS->videoh->fname == video.addPrefix("VIDEO/")) // When you got out of the bounds of the button then close the video - { - CCS->videoh->close(); - redraw(); - } } void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) @@ -149,6 +139,11 @@ void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPositio void CCampaignScreen::CCampaignButton::hover(bool on) { + if (on) + CCS->videoh->open(video); + else + CCS->videoh->close(); + if(hoverLabel) { if(on) diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index a99a9e306..1f81c5e34 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -217,9 +217,7 @@ CAnimation::CAnimation(const AnimationPath & Name): } CAnimation::CAnimation(): - name(""), - preloaded(false), - defFile() + preloaded(false) { init(); } diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 95009f659..4bcae8c91 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -417,7 +417,7 @@ si64 InfoBoxCustomHeroData::getValue() return value; } -InfoBoxCustom::InfoBoxCustom(std::string ValueText, std::string NameText, std::string ImageName, size_t ImageIndex, std::string HoverText): +InfoBoxCustom::InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText): IInfoBoxData(CUSTOM), valueText(ValueText), nameText(NameText), @@ -531,7 +531,7 @@ std::shared_ptr CKingdomInterface::createOwnedObject(size_t index) { OwnedObjectInfo & obj = objects[index]; std::string value = std::to_string(obj.count); - auto data = std::make_shared(value, "", "FLAGPORT", obj.imageID, obj.hoverText); + auto data = std::make_shared(value, "", AnimationPath::builtin("FLAGPORT"), obj.imageID, obj.hoverText); return std::make_shared(Point(), InfoBox::POS_CORNER, InfoBox::SIZE_SMALL, data); } return std::shared_ptr(); @@ -587,7 +587,7 @@ void CKingdomInterface::generateMinesList(const std::vector(value, "", "OVMINES", i, CGI->generaltexth->translate("core.minename", i)); + auto data = std::make_shared(value, "", AnimationPath::builtin("OVMINES"), i, CGI->generaltexth->translate("core.minename", i)); minesBox[i] = std::make_shared(Point(20+i*80, 31+footerPos), InfoBox::POS_INSIDE, InfoBox::SIZE_SMALL, data); minesBox[i]->removeUsedEvents(LCLICK|SHOW_POPUP); //fixes #890 - mines boxes ignore clicks } diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 0950419d9..52a5623ed 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -170,7 +170,7 @@ public: std::string hoverText; size_t imageIndex; - InfoBoxCustom(std::string ValueText, std::string NameText, std::string ImageName, size_t ImageIndex, std::string HoverText=""); + InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText=""); std::string getValueText() override; std::string getNameText() override; diff --git a/client/windows/CPuzzleWindow.cpp b/client/windows/CPuzzleWindow.cpp index ff6a98a01..79edef9cb 100644 --- a/client/windows/CPuzzleWindow.cpp +++ b/client/windows/CPuzzleWindow.cpp @@ -43,7 +43,7 @@ CPuzzleWindow::CPuzzleWindow(const int3 & GrailPos, double discoveredRatio) logo = std::make_shared(ImagePath::builtin("PUZZLOGO"), 607, 3); title = std::make_shared(700, 95, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[463]); - resDataBar = std::make_shared(ImagePath("ARESBAR.bmp"), 3, 575, 32, 2, 85, 85); + resDataBar = std::make_shared(ImagePath::builtin("ARESBAR.bmp"), 3, 575, 32, 2, 85, 85); int faction = LOCPLINT->cb->getStartInfo()->playerInfos.find(LOCPLINT->playerID)->second.castle; diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index fd3a2fe85..471a71205 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -76,7 +76,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath savedConf.Struct().erase("session"); JsonUtils::minimize(savedConf, "vcmi:settings"); - std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index a09d193ab..f9608eac2 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1448,7 +1448,7 @@ JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bo JsonNode JsonUtils::assembleFromFiles(const std::string & filename) { JsonNode result; - JsonPath resID(filename); + JsonPath resID = JsonPath::builtinTODO(filename); for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) { diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 9175d0d2d..0a59a7279 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -70,7 +70,7 @@ std::unordered_set CFilesystemLoader::getFilteredFiles(std::functi bool CFilesystemLoader::createResource(const ResourcePath & resID, bool update) { - std::string filename = resID.getOriginalName(); + std::string filename = resID.getOriginalName() + '.' + boost::to_lower_copy(EResTypeHelper::getEResTypeAsString(resID.getType())); if (fileList.find(resID) != fileList.end()) return true; diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 8ed843c40..0057d2338 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -133,9 +133,15 @@ class DLL_LINKAGE ResourcePathTempl : public ResourcePath type = Type; } -public: - using ResourcePath::ResourcePath; + ResourcePathTempl(const std::string & path) + :ResourcePath(path, Type) + {} + ResourcePathTempl(const JsonNode & name) + :ResourcePath(name, Type) + {} + +public: ResourcePathTempl() :ResourcePath("", Type) {} @@ -148,17 +154,17 @@ public: static ResourcePathTempl builtin(const std::string & filename) { - return ResourcePathTempl(filename, Type); + return ResourcePathTempl(filename); } static ResourcePathTempl builtinTODO(const std::string & filename) { - return ResourcePathTempl(filename, Type); + return ResourcePathTempl(filename); } static ResourcePathTempl fromJson(const JsonNode & path) { - return ResourcePathTempl(path, Type); + return ResourcePathTempl(path); } template From 6a260a60cfde0aaedd8a7b027a27644740e11ebe Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 16:16:00 +0300 Subject: [PATCH 0320/1248] Fix resource creation --- lib/CConfigHandler.cpp | 2 +- lib/filesystem/AdapterLoaders.cpp | 4 ++-- lib/filesystem/AdapterLoaders.h | 2 +- lib/filesystem/CFilesystemLoader.cpp | 5 +++-- lib/filesystem/CFilesystemLoader.h | 2 +- lib/filesystem/ISimpleResourceLoader.h | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- lib/mapping/MapIdentifiersH3M.cpp | 2 +- lib/modding/CModHandler.cpp | 2 +- server/CGameHandler.cpp | 2 +- 10 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 471a71205..6e507dbe7 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -61,7 +61,7 @@ void SettingsStorage::init() // Probably new install. Create config file to save settings to if (!CResourceHandler::get("local")->existsResource(confName)) - CResourceHandler::get("local")->createResource(confName); + CResourceHandler::get("local")->createResource("config/settings.json"); JsonUtils::maximize(config, "vcmi:settings"); JsonUtils::validate(config, "vcmi:settings", "settings"); diff --git a/lib/filesystem/AdapterLoaders.cpp b/lib/filesystem/AdapterLoaders.cpp index c95ac4c77..8bdf8abab 100644 --- a/lib/filesystem/AdapterLoaders.cpp +++ b/lib/filesystem/AdapterLoaders.cpp @@ -128,9 +128,9 @@ std::unordered_set CFilesystemList::getFilteredFiles(std::function return ret; } -bool CFilesystemList::createResource(const ResourcePath & filename, bool update) +bool CFilesystemList::createResource(const std::string & filename, bool update) { - logGlobal->trace("Creating %s", filename.getOriginalName()); + logGlobal->trace("Creating %s", filename); for (auto & loader : boost::adaptors::reverse(loaders)) { if (writeableLoaders.count(loader.get()) != 0 // writeable, diff --git a/lib/filesystem/AdapterLoaders.h b/lib/filesystem/AdapterLoaders.h index 23bf6d6a6..6fea768d7 100644 --- a/lib/filesystem/AdapterLoaders.h +++ b/lib/filesystem/AdapterLoaders.h @@ -75,7 +75,7 @@ public: std::set getResourceNames(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; std::unordered_set getFilteredFiles(std::function filter) const override; - bool createResource(const ResourcePath & filename, bool update = false) override; + bool createResource(const std::string & filename, bool update = false) override; std::vector getResourcesWithName(const ResourcePath & resourceName) const override; /** diff --git a/lib/filesystem/CFilesystemLoader.cpp b/lib/filesystem/CFilesystemLoader.cpp index 0a59a7279..09073660c 100644 --- a/lib/filesystem/CFilesystemLoader.cpp +++ b/lib/filesystem/CFilesystemLoader.cpp @@ -68,9 +68,10 @@ std::unordered_set CFilesystemLoader::getFilteredFiles(std::functi return foundID; } -bool CFilesystemLoader::createResource(const ResourcePath & resID, bool update) +bool CFilesystemLoader::createResource(const std::string & requestedFilename, bool update) { - std::string filename = resID.getOriginalName() + '.' + boost::to_lower_copy(EResTypeHelper::getEResTypeAsString(resID.getType())); + std::string filename = requestedFilename; + ResourcePath resID(filename); if (fileList.find(resID) != fileList.end()) return true; diff --git a/lib/filesystem/CFilesystemLoader.h b/lib/filesystem/CFilesystemLoader.h index 301adc361..ec75c62a0 100644 --- a/lib/filesystem/CFilesystemLoader.h +++ b/lib/filesystem/CFilesystemLoader.h @@ -37,7 +37,7 @@ public: std::unique_ptr load(const ResourcePath & resourceName) const override; bool existsResource(const ResourcePath & resourceName) const override; std::string getMountPoint() const override; - bool createResource(const ResourcePath & filename, bool update = false) override; + bool createResource(const std::string & filename, bool update = false) override; std::optional getResourceName(const ResourcePath & resourceName) const override; void updateFilteredFiles(std::function filter) const override; std::unordered_set getFilteredFiles(std::function filter) const override; diff --git a/lib/filesystem/ISimpleResourceLoader.h b/lib/filesystem/ISimpleResourceLoader.h index b9b9c3f94..836a3a505 100644 --- a/lib/filesystem/ISimpleResourceLoader.h +++ b/lib/filesystem/ISimpleResourceLoader.h @@ -90,7 +90,7 @@ public: * * @return true if new file was created, false on error or if file already exists */ - virtual bool createResource(const ResourcePath & filename, bool update = false) + virtual bool createResource(const std::string & filename, bool update = false) { return false; } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index e7040ce5d..0b156c422 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -984,7 +984,7 @@ void CMapLoaderH3M::readObjectTemplates() auto tmpl = reader->readObjectTemplate(); templates.push_back(tmpl); - if (!CResourceHandler::get()->existsResource(tmpl->animationFile)) + if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/"))) logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); } } diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 70789a001..05c35020c 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -56,7 +56,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); std::string vcmiName = boost::to_lower_copy(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(AnimationPath::builtin("SPRITES/" + vcmiName))) + if (!CResourceHandler::get()->existsResource(AnimationPath::builtinTODO("SPRITES/" + vcmiName))) logMod->warn("Template animation file %s was not found!", vcmiName); mappingObjectTemplate[h3mName] = vcmiName; diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 78fa70a5c..6fb6f0ac5 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -35,7 +35,7 @@ static JsonNode loadModSettings(const JsonPath & path) return JsonNode(path); } // Probably new install. Create initial configuration - CResourceHandler::get("local")->createResource(path); + CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json"); return JsonNode(); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 701c27ab7..6525c1b00 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1718,7 +1718,7 @@ void CGameHandler::save(const std::string & filename) const auto stem = FileInfo::GetPathStem(filename); const auto savefname = stem.to_string() + ".vsgm1"; ResourcePath savePath(stem.to_string(), EResType::SAVEGAME); - CResourceHandler::get("local")->createResource(savePath); + CResourceHandler::get("local")->createResource(savefname); try { From 3f921fa771eceba2ac81ca15046f9f3f64c4e2b1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 16:16:09 +0300 Subject: [PATCH 0321/1248] Serialization version bump --- lib/serializer/CSerializer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index f09baeffb..c49fb86fc 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 826; -const ui32 MINIMAL_SERIALIZATION_VERSION = 826; +const ui32 SERIALIZATION_VERSION = 827; +const ui32 MINIMAL_SERIALIZATION_VERSION = 827; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From 1d0e696db6239151b675c9cf9fc8909f849a1520 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 18:01:44 +0300 Subject: [PATCH 0322/1248] Added RenderHandler that acts as factory for images and animations --- client/CMakeLists.txt | 3 ++ client/ClientCommandManager.cpp | 3 +- client/adventureMap/AdventureMapWidget.cpp | 5 ++- client/battle/BattleAnimationClasses.cpp | 3 +- client/battle/BattleFieldController.cpp | 19 +++++----- client/battle/BattleInterfaceClasses.cpp | 17 +++++---- client/battle/BattleObstacleController.cpp | 9 +++-- client/battle/BattleProjectileController.cpp | 3 +- client/battle/BattleSiegeController.cpp | 8 ++-- client/battle/BattleStacksController.cpp | 9 +++-- client/battle/BattleWindow.cpp | 3 +- client/battle/CreatureAnimation.cpp | 6 ++- client/gui/CGuiHandler.cpp | 7 ++++ client/gui/CGuiHandler.h | 4 +- client/gui/CursorHandler.cpp | 15 ++++---- client/gui/CursorHandler.h | 2 +- client/lobby/CBonusSelection.cpp | 3 +- client/lobby/CSelectionBase.cpp | 3 +- client/lobby/SelectionTab.cpp | 11 +++--- client/mapView/MapRenderer.cpp | 24 ++++++------ client/mapView/MapRenderer.h | 12 +++--- client/mapView/MapViewCache.cpp | 5 ++- client/mapView/MapViewCache.h | 2 +- client/render/CAnimation.h | 1 + client/render/Graphics.cpp | 4 +- client/render/IImage.h | 14 ++----- client/render/IRenderHandler.h | 36 ++++++++++++++++++ client/renderSDL/RenderHandler.cpp | 40 ++++++++++++++++++++ client/renderSDL/RenderHandler.h | 25 ++++++++++++ client/renderSDL/SDLImage.cpp | 18 --------- client/widgets/Buttons.cpp | 3 +- client/widgets/Images.cpp | 9 +++-- client/windows/CCastleInterface.cpp | 5 ++- client/windows/CHeroWindow.cpp | 5 ++- client/windows/CMessage.cpp | 7 ++-- client/windows/CSpellWindow.cpp | 11 +++--- client/windows/CWindowObject.cpp | 7 ++-- client/windows/GUIClasses.cpp | 11 +++--- 38 files changed, 246 insertions(+), 126 deletions(-) create mode 100644 client/render/IRenderHandler.h create mode 100644 client/renderSDL/RenderHandler.cpp create mode 100644 client/renderSDL/RenderHandler.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 9e851487d..ae542146d 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -84,6 +84,7 @@ set(client_SRCS renderSDL/CTrueTypeFont.cpp renderSDL/CursorHardware.cpp renderSDL/CursorSoftware.cpp + renderSDL/RenderHandler.cpp renderSDL/SDLImage.cpp renderSDL/SDLImageLoader.cpp renderSDL/SDLRWwrapper.cpp @@ -235,6 +236,7 @@ set(client_HEADERS render/IFont.h render/IImage.h render/IImageLoader.h + render/IRenderHandler.h render/IScreenHandler.h renderSDL/CBitmapFont.h @@ -242,6 +244,7 @@ set(client_HEADERS renderSDL/CTrueTypeFont.h renderSDL/CursorHardware.h renderSDL/CursorSoftware.h + renderSDL/RenderHandler.h renderSDL/SDLImage.h renderSDL/SDLImageLoader.h renderSDL/SDLRWwrapper.h diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 43b98723d..7b7d2d932 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -17,6 +17,7 @@ #include "CServerHandler.h" #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" +#include "render/IRenderHandler.h" #include "../lib/NetPacks.h" #include "ClientNetPackVisitors.h" #include "../lib/CConfigHandler.h" @@ -317,7 +318,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu { std::string URI; singleWordBuffer >> URI; - std::unique_ptr anim = std::make_unique(AnimationPath::builtin(URI)); + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI)); anim->preload(); anim->exportBitmaps(VCMIDirs::get().userExtractedPath()); } diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index aea6e80bd..5654b2b72 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -22,6 +22,7 @@ #include "../mapView/MapView.h" #include "../render/CAnimation.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" @@ -129,7 +130,7 @@ std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) ImagePath resource = ImagePath::fromJson(name); if(images.count(resource) == 0) - images[resource] = IImage::createFromFile(resource); + images[resource] = GH.renderHandler().loadImage(resource); return images[resource]; } @@ -139,7 +140,7 @@ std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & n AnimationPath resource = AnimationPath::fromJson(name); if(animations.count(resource) == 0) - animations[resource] = std::make_shared(resource); + animations[resource] = GH.renderHandler().loadAnimation(resource); return animations[resource]; } diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 8cde61997..f2ad52f34 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -24,6 +24,7 @@ #include "../CPlayerInterface.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" #include "../../lib/CStack.h" @@ -881,7 +882,7 @@ uint32_t CastAnimation::getAttackClimaxFrame() const EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): BattleAnimation(owner), - animation(std::make_shared(animationName)), + animation(GH.renderHandler().loadAnimation(animationName)), effectFlags(effects), effectFinished(false), reversed(reversed) diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index a53e5d8c2..33d5204e0 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -26,6 +26,7 @@ #include "../render/Canvas.h" #include "../render/IImage.h" #include "../renderSDL/SDL_Extensions.h" +#include "../render/IRenderHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" #include "../adventureMap/CInGameConsole.h" @@ -120,20 +121,20 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; //preparing cells and hexes - cellBorder = IImage::createFromFile(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); - cellShade = IImage::createFromFile(ImagePath::builtin("CCELLSHD.BMP")); - cellUnitMovementHighlight = IImage::createFromFile(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - cellUnitMaxMovementHighlight = IImage::createFromFile(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); + cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); + cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP")); + cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); + cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - attackCursors = std::make_shared(AnimationPath::builtin("CRCOMBAT")); + attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")); attackCursors->preload(); initializeHexEdgeMaskToFrameIndex(); - rangedFullDamageLimitImages = std::make_shared(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); + rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); rangedFullDamageLimitImages->preload(); - shootingRangeLimitImages = std::make_shared(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); + shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); shootingRangeLimitImages->preload(); flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); @@ -146,12 +147,12 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): if(bfieldType == BattleField::NONE) logGlobal->error("Invalid battlefield returned for current battle"); else - background = IImage::createFromFile(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); + background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); } else { auto backgroundName = owner.siegeController->getBattleBackgroundName(); - background = IImage::createFromFile(backgroundName, EImageBlitMode::OPAQUE); + background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE); } pos.w = background->width(); diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 971d94d46..55ba20d8e 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -37,6 +37,7 @@ #include "../windows/CMessage.h" #include "../windows/CSpellWindow.h" #include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" #include "../adventureMap/CInGameConsole.h" #include "../../CCallback.h" @@ -352,7 +353,7 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her else animationPath = hero->type->heroClass->imageBattleMale; - animation = std::make_shared(animationPath); + animation = GH.renderHandler().loadAnimation(animationPath); animation->preload(); pos.w = 64; @@ -364,9 +365,9 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her animation->verticalFlip(); if(defender) - flagAnimation = std::make_shared(AnimationPath::builtin("CMFLAGR")); + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR")); else - flagAnimation = std::make_shared(AnimationPath::builtin("CMFLAGL")); + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL")); flagAnimation->preload(); flagAnimation->playerColored(hero->tempOwner); @@ -676,8 +677,8 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) pos.x += parent->pos.w/2 - pos.w/2; pos.y += 10; - icons = std::make_shared(AnimationPath::builtin("CPRSMALL")); - stateIcons = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); + icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); + stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); } else { @@ -688,10 +689,10 @@ StackQueue::StackQueue(bool Embedded, BattleInterface & owner) background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); - icons = std::make_shared(AnimationPath::builtin("TWCRPORT")); - stateIcons = std::make_shared(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); + icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT")); + stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); //TODO: where use big icons? - //stateIcons = std::make_shared("VCMI/BATTLEQUEUE/STATESBIG"); + //stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG"); } stateIcons->preload(); diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index a2acf75ee..54ca1200b 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -22,6 +22,7 @@ #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" #include "../../lib/battle/CObstacleInstance.h" @@ -49,14 +50,14 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { // obstacle uses single bitmap image for animations - auto animation = std::make_shared(); + auto animation = GH.renderHandler().createAnimation(); animation->setCustom(animationName.getName(), 0, 0); animationsCache[animationName] = animation; animation->preload(); } else { - auto animation = std::make_shared(animationName); + auto animation = GH.renderHandler().loadAnimation(animationName); animationsCache[animationName] = animation; animation->preload(); } @@ -76,7 +77,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector(AnimationPath::fromJson(obstacle["appearAnimation"])); + auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"])); animation->preload(); auto first = animation->getImage(0, 0); @@ -104,7 +105,7 @@ void BattleObstacleController::obstaclePlaced(const std::vectorvisibleForSide(side.value(), owner.curInt->cb->battleHasNativeStack(side.value()))) continue; - auto animation = std::make_shared(oi->getAppearAnimation()); + auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation()); animation->preload(); auto first = animation->getImage(0, 0); diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index 71fac3f52..f4b861c27 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -16,6 +16,7 @@ #include "CreatureAnimation.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../gui/CGuiHandler.h" #include "../CGameInfo.h" @@ -190,7 +191,7 @@ void BattleProjectileController::initStackProjectile(const CStack * stack) std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) { - std::shared_ptr projectile = std::make_shared(path); + std::shared_ptr projectile = GH.renderHandler().loadAnimation(path); projectile->preload(); if(projectile->size(1) != 0) diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 7bb7a908f..704340ed2 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -20,8 +20,10 @@ #include "../CMusicHandler.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" #include "../../lib/NetPacks.h" @@ -180,7 +182,7 @@ BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTo if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) continue; - wallPieceImages[g] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); + wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); } } @@ -246,7 +248,7 @@ void BattleSiegeController::gateStateChanged(const EGateState state) wallPieceImages[EWallVisual::GATE] = nullptr; if (stateId != EWallState::NONE) - wallPieceImages[EWallVisual::GATE] = IImage::createFromFile(getWallPieceImageName(EWallVisual::GATE, stateId)); + wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId)); if (playSound) CCS->soundh->playSound(soundBase::DRAWBRG); @@ -355,7 +357,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) auto wallState = EWallState(owner.curInt->cb->battleGetWallState(attackInfo.attackedPart)); - wallPieceImages[wallId] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); + wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); } } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index e9c255187..53618ca08 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -28,6 +28,7 @@ #include "../gui/CGuiHandler.h" #include "../render/Colors.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" #include "../../lib/spells/ISpellMechanics.h" @@ -76,10 +77,10 @@ BattleStacksController::BattleStacksController(BattleInterface & owner): animIDhelper(0) { //preparing graphics for displaying amounts of creatures - amountNormal = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountPositive = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountNegative = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountEffNeutral = IImage::createFromFile(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountNormal = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountPositive = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountNegative = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 7595df164..1347b5c3e 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -29,6 +29,7 @@ #include "../windows/CMessage.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../adventureMap/CInGameConsole.h" #include "../../CCallback.h" @@ -469,7 +470,7 @@ void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) break; } - auto anim = std::make_shared(iconName); + auto anim = GH.renderHandler().loadAnimation(iconName); w->setImage(anim); w->redraw(); } diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index ee0106f0b..b22718129 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -13,8 +13,10 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" +#include "../gui/CGuiHandler.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" +#include "../render/IRenderHandler.h" static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 }; @@ -196,8 +198,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll speedController(controller), once(false) { - forward = std::make_shared(name_); - reverse = std::make_shared(name_); + forward = GH.renderHandler().loadAnimation(name_); + reverse = GH.renderHandler().loadAnimation(name_); //todo: optimize forward->preload(); diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index 504f404ad..ae4f9bdab 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -25,6 +25,7 @@ #include "../render/IFont.h" #include "../render/EFont.h" #include "../renderSDL/ScreenHandler.h" +#include "../renderSDL/RenderHandler.h" #include "../CMT.h" #include "../CPlayerInterface.h" #include "../battle/BattleInterface.h" @@ -75,6 +76,7 @@ void CGuiHandler::init() eventDispatcherInstance = std::make_unique(); windowHandlerInstance = std::make_unique(); screenHandlerInstance = std::make_unique(); + renderHandlerInstance = std::make_unique(); shortcutsHandlerInstance = std::make_unique(); framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); } @@ -206,6 +208,11 @@ IScreenHandler & CGuiHandler::screenHandler() return *screenHandlerInstance; } +IRenderHandler & CGuiHandler::renderHandler() +{ + return *renderHandlerInstance; +} + EventDispatcher & CGuiHandler::events() { return *eventDispatcherInstance; diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index e11124bcf..e6d6aa963 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -22,6 +22,7 @@ class IStatusBar; class CIntObject; class IUpdateable; class IShowActivatable; +class IRenderHandler; class IScreenHandler; class WindowHandler; class EventDispatcher; @@ -41,6 +42,7 @@ private: std::unique_ptr windowHandlerInstance; std::unique_ptr screenHandlerInstance; + std::unique_ptr renderHandlerInstance; std::unique_ptr framerateManagerInstance; std::unique_ptr eventDispatcherInstance; std::unique_ptr inputHandlerInstance; @@ -67,7 +69,7 @@ public: void stopTextInput(); IScreenHandler & screenHandler(); - + IRenderHandler & renderHandler(); WindowHandler & windows(); /// Returns currently active status bar. Guaranteed to be non-null diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index dbdf7853e..d5c7a91cd 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -17,6 +17,7 @@ #include "../renderSDL/CursorHardware.h" #include "../render/CAnimation.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" @@ -46,10 +47,10 @@ CursorHandler::CursorHandler() cursors = { - std::make_unique(AnimationPath::builtin("CRADVNTR")), - std::make_unique(AnimationPath::builtin("CRCOMBAT")), - std::make_unique(AnimationPath::builtin("CRDEFLT")), - std::make_unique(AnimationPath::builtin("CRSPELL")) + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")) }; for (auto & cursor : cursors) @@ -102,9 +103,9 @@ void CursorHandler::dragAndDropCursor(std::shared_ptr image) void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) { - CAnimation anim(path); - anim.load(index); - dragAndDropCursor(anim.getImage(index)); + auto anim = GH.renderHandler().loadAnimation(path); + anim->load(index); + dragAndDropCursor(anim->getImage(index)); } void CursorHandler::cursorMove(const int & x, const int & y) diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index b17686b93..091837eb2 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -114,7 +114,7 @@ class CursorHandler final { std::shared_ptr dndObject; //if set, overrides currentCursor - std::array, 4> cursors; + std::array, 4> cursors; bool showing; diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 526370897..8b7ffc7dd 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -31,6 +31,7 @@ #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/CAnimation.h" #include "../render/Graphics.h" #include "../gui/CGuiHandler.h" @@ -306,7 +307,7 @@ void CBonusSelection::createBonusesIcons() if(picNumber != -1) picName += ":" + std::to_string(picNumber); - auto anim = std::make_shared(); + auto anim = GH.renderHandler().createAnimation(); anim->setCustom(picName, 0); bonusButton->setImage(anim); if(CSH->campaignBonus == i) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 1eb555d65..2c228330f 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -39,6 +39,7 @@ #include "../render/CAnimation.h" #include "../render/Graphics.h" #include "../render/IFont.h" +#include "../render/IRenderHandler.h" #include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" @@ -356,7 +357,7 @@ CFlagBox::CFlagBox(const Rect & rect) labelAllies = std::make_shared(0, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[390] + ":"); labelEnemies = std::make_shared(133, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[391] + ":"); - iconsTeamFlags = std::make_shared(AnimationPath::builtin("ITGFLAGS.DEF")); + iconsTeamFlags = GH.renderHandler().loadAnimation(AnimationPath::builtin("ITGFLAGS.DEF")); iconsTeamFlags->preload(); } diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 7200fc690..e22c936d3 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -30,6 +30,7 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/Graphics.h" #include "../../CCallback.h" @@ -211,9 +212,9 @@ SelectionTab::SelectionTab(ESelectionScreen Type) break; } - iconsMapFormats = std::make_shared(AnimationPath::builtin("SCSELC.DEF")); - iconsVictoryCondition = std::make_shared(AnimationPath::builtin("SCNRVICT.DEF")); - iconsLossCondition = std::make_shared(AnimationPath::builtin("SCNRLOSS.DEF")); + iconsMapFormats = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCSELC.DEF")); + iconsVictoryCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRVICT.DEF")); + iconsLossCondition = GH.renderHandler().loadAnimation(AnimationPath::builtin("SCNRLOSS.DEF")); for(int i = 0; i < positionsToShow; i++) listItems.push_back(std::make_shared(Point(30, 129 + i * 25), iconsMapFormats, iconsVictoryCondition, iconsLossCondition)); @@ -923,7 +924,7 @@ std::vector> SelectionTab::CMapInfoTooltipBox::createMin Canvas canvas = createMinimapForLayer(map, i); Canvas canvasScaled = Canvas(Point(size, size)); canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size)); - std::shared_ptr img = IImage::createFromSurface(canvasScaled.getInternalSurface()); + std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); ret.push_back(img); } @@ -935,7 +936,7 @@ SelectionTab::ListItem::ListItem(Point position, std::shared_ptr ico : CIntObject(LCLICK, position) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pictureEmptyLine = std::make_shared(IImage::createFromFile(ImagePath::builtin("camcust")), Rect(25, 121, 349, 26), -8, -14); + pictureEmptyLine = std::make_shared(GH.renderHandler().loadImage(ImagePath::builtin("camcust")), Rect(25, 121, 349, 26), -8, -14); labelName = std::make_shared(184, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); labelName->setAutoRedraw(false); labelAmountOfPlayers = std::make_shared(8, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index be14e7dcf..3d1cc79be 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -15,9 +15,11 @@ #include "mapHandler.h" #include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" @@ -102,11 +104,11 @@ void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBl { if (!filename.empty()) { - entry = std::make_unique(filename); + entry = GH.renderHandler().loadAnimation(filename); entry->preload(); } else - entry = std::make_unique(); + entry = GH.renderHandler().createAnimation(); for(size_t i = 0; i < entry->size(); ++i) entry->getImage(i)->setBlitMode(blitMode); @@ -247,7 +249,7 @@ uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & co MapRendererBorder::MapRendererBorder() { emptyFill = std::make_unique(Point(32,32)); - animation = std::make_unique(AnimationPath::builtin("EDG")); + animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG")); animation->preload(); } @@ -309,9 +311,9 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & MapRendererFow::MapRendererFow() { - fogOfWarFullHide = std::make_unique(AnimationPath::builtin("TSHRC")); + fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC")); fogOfWarFullHide->preload(); - fogOfWarPartialHide = std::make_unique(AnimationPath::builtin("TSHRE")); + fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE")); fogOfWarPartialHide->preload(); for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) @@ -394,7 +396,7 @@ std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath if(it != animations.end()) return it->second; - auto ret = std::make_shared(filename); + auto ret = GH.renderHandler().loadAnimation(filename); animations[filename] = ret; ret->preload(); @@ -557,10 +559,10 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & } MapRendererOverlay::MapRendererOverlay() - : imageGrid(IImage::createFromFile(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) - , imageBlocked(IImage::createFromFile(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) - , imageVisitable(IImage::createFromFile(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) - , imageSpellRange(IImage::createFromFile(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) + : imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) + , imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) + , imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) + , imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) { } @@ -616,7 +618,7 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & } MapRendererPath::MapRendererPath() - : pathNodes(new CAnimation(AnimationPath::builtin("ADAG"))) + : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"))) { pathNodes->preload(); } diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index 6fc1ccce7..e93d0fb0d 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -23,11 +23,11 @@ class CAnimation; class IImage; class Canvas; class IMapRendererContext; -enum class EImageBlitMode : uint8_t; +enum class EImageBlitMode; class MapTileStorage { - using TerrainAnimation = std::array, 4>; + using TerrainAnimation = std::array, 4>; std::vector animations; public: @@ -91,7 +91,7 @@ public: class MapRendererBorder { - std::unique_ptr animation; + std::shared_ptr animation; std::unique_ptr emptyFill; size_t getIndexForTile(IMapRendererContext & context, const int3 & coordinates); @@ -105,8 +105,8 @@ public: class MapRendererFow { - std::unique_ptr fogOfWarFullHide; - std::unique_ptr fogOfWarPartialHide; + std::shared_ptr fogOfWarFullHide; + std::shared_ptr fogOfWarPartialHide; public: MapRendererFow(); @@ -117,7 +117,7 @@ public: class MapRendererPath { - std::unique_ptr pathNodes; + std::shared_ptr pathNodes; size_t selectImageReachability(bool reachableToday, size_t imageIndex); size_t selectImageCross(bool reachableToday, const int3 & curr); diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index 528ba5b39..cb69c9fdf 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -18,6 +18,9 @@ #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../gui/CGuiHandler.h" #include "../../lib/mapObjects/CObjectHandler.h" #include "../../lib/int3.h" @@ -28,7 +31,7 @@ MapViewCache::MapViewCache(const std::shared_ptr & model) : model(model) , cachedLevel(0) , mapRenderer(new MapRenderer()) - , iconsStorage(new CAnimation(AnimationPath::builtin("VwSymbol"))) + , iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"))) , intermediate(new Canvas(Point(32, 32))) , terrain(new Canvas(model->getCacheDimensionsPixels())) , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) diff --git a/client/mapView/MapViewCache.h b/client/mapView/MapViewCache.h index d2795ccc0..01b09a32e 100644 --- a/client/mapView/MapViewCache.h +++ b/client/mapView/MapViewCache.h @@ -52,7 +52,7 @@ class MapViewCache std::unique_ptr intermediate; std::unique_ptr mapRenderer; - std::unique_ptr iconsStorage; + std::shared_ptr iconsStorage; Canvas getTile(const int3 & coordinates); void updateTile(const std::shared_ptr & context, const int3 & coordinates); diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index b39bfe401..7332051c8 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -18,6 +18,7 @@ VCMI_LIB_NAMESPACE_END class CDefFile; class IImage; +class RenderHandler; /// Class for handling animation class CAnimation diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index cf5c23ce9..8721d6356 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -24,6 +24,8 @@ #include "../renderSDL/CTrueTypeFont.h" #include "../render/CAnimation.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/filesystem/CBinaryReader.h" @@ -284,7 +286,7 @@ std::shared_ptr Graphics::getAnimation(const AnimationPath & path) if (cachedAnimations.count(path) != 0) return cachedAnimations.at(path); - auto newAnimation = std::make_shared(path); + auto newAnimation = GH.renderHandler().loadAnimation(path); newAnimation->preload(); cachedAnimations[path] = newAnimation; diff --git a/client/render/IImage.h b/client/render/IImage.h index e5961082a..4308069c9 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -24,7 +24,7 @@ struct SDL_Surface; class ColorFilter; /// Defines which blit method will be selected when image is used for rendering -enum class EImageBlitMode : uint8_t +enum class EImageBlitMode { /// Image can have no transparency and can be only used as background OPAQUE, @@ -82,15 +82,7 @@ public: virtual void verticalFlip() = 0; virtual void doubleFlip() = 0; - IImage(); - virtual ~IImage(); - - /// loads image from specified file. Returns 0-sized images on failure - static std::shared_ptr createFromFile( const ImagePath & path ); - static std::shared_ptr createFromFile( const ImagePath & path, EImageBlitMode mode ); - - /// temporary compatibility method. Creates IImage from existing SDL_Surface - /// Surface will be shared, called must still free it with SDL_FreeSurface - static std::shared_ptr createFromSurface( SDL_Surface * source ); + IImage() = default; + virtual ~IImage() = default; }; diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h new file mode 100644 index 000000000..5d0fed385 --- /dev/null +++ b/client/render/IRenderHandler.h @@ -0,0 +1,36 @@ +/* + * IRenderHandler.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/filesystem/ResourcePath.h" + +struct SDL_Surface; + +class IImage; +class CAnimation; +enum class EImageBlitMode; + +class IRenderHandler : public boost::noncopyable +{ +public: + /// Loads image using given path + virtual std::shared_ptr loadImage(const ImagePath & path) = 0; + virtual std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) = 0; + + /// temporary compatibility method. Creates IImage from existing SDL_Surface + /// Surface will be shared, caller must still free it with SDL_FreeSurface + virtual std::shared_ptr createImage(SDL_Surface * source) = 0; + + /// Loads animation using given path + virtual std::shared_ptr loadAnimation(const AnimationPath & path) = 0; + + /// Creates empty CAnimation + virtual std::shared_ptr createAnimation() = 0; +}; diff --git a/client/renderSDL/RenderHandler.cpp b/client/renderSDL/RenderHandler.cpp new file mode 100644 index 000000000..87eb99091 --- /dev/null +++ b/client/renderSDL/RenderHandler.cpp @@ -0,0 +1,40 @@ +/* + * RenderHandler.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 "RenderHandler.h" + +#include "../render/CAnimation.h" +#include "SDLImage.h" + + +std::shared_ptr RenderHandler::loadImage(const ImagePath & path) +{ + return loadImage(path, EImageBlitMode::ALPHA); +} + +std::shared_ptr RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode) +{ + return std::make_shared(path, mode); +} + +std::shared_ptr RenderHandler::createImage(SDL_Surface * source) +{ + return std::make_shared(source, EImageBlitMode::ALPHA); +} + +std::shared_ptr RenderHandler::loadAnimation(const AnimationPath & path) +{ + return std::make_shared(path); +} + +std::shared_ptr RenderHandler::createAnimation() +{ + return std::make_shared(); +} diff --git a/client/renderSDL/RenderHandler.h b/client/renderSDL/RenderHandler.h new file mode 100644 index 000000000..76e30ee9c --- /dev/null +++ b/client/renderSDL/RenderHandler.h @@ -0,0 +1,25 @@ +/* + * RenderHandler.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 "../render/IRenderHandler.h" + +class RenderHandler : public IRenderHandler +{ +public: + std::shared_ptr loadImage(const ImagePath & path) override; + std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) override; + + std::shared_ptr createImage(SDL_Surface * source) override; + + std::shared_ptr loadAnimation(const AnimationPath & path) override; + + std::shared_ptr createAnimation() override; +}; diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index dbbe7dd6d..891b2e975 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -24,24 +24,6 @@ class SDLImageLoader; -std::shared_ptr IImage::createFromFile( const ImagePath & path ) -{ - return createFromFile(path, EImageBlitMode::ALPHA); -} - -std::shared_ptr IImage::createFromFile( const ImagePath & path, EImageBlitMode mode ) -{ - return std::shared_ptr(new SDLImage(path, mode)); -} - -std::shared_ptr IImage::createFromSurface( SDL_Surface * source ) -{ - return std::shared_ptr(new SDLImage(source, EImageBlitMode::ALPHA)); -} - -IImage::IImage() = default; -IImage::~IImage() = default; - int IImage::width() const { return dimensions().x; diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 585029634..45d5322eb 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -25,6 +25,7 @@ #include "../windows/InfoWindows.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" +#include "../render/IRenderHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" @@ -268,7 +269,7 @@ void CButton::setIndex(size_t index) if (index == currentImage || index>=imageNames.size()) return; currentImage = index; - auto anim = std::make_shared(imageNames[index]); + auto anim = GH.renderHandler().loadAnimation(imageNames[index]); setImage(anim); } diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index e86923bc9..8e59f81cb 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -15,6 +15,7 @@ #include "../gui/CGuiHandler.h" #include "../renderSDL/SDL_Extensions.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/CAnimation.h" #include "../render/Canvas.h" #include "../render/ColorFilter.h" @@ -51,7 +52,7 @@ CPicture::CPicture( const ImagePath & bmpname ) {} CPicture::CPicture( const ImagePath & bmpname, const Point & position ) - : bg(IImage::createFromFile(bmpname)) + : bg(GH.renderHandler().loadImage(bmpname)) , visible(true) , needRefresh(false) { @@ -115,7 +116,7 @@ void CPicture::colorize(PlayerColor player) CFilledTexture::CFilledTexture(const ImagePath & imageName, Rect position): CIntObject(0, position.topLeft()), - texture(IImage::createFromFile(imageName)) + texture(GH.renderHandler().loadImage(imageName)) { pos.w = position.w; pos.h = position.h; @@ -178,7 +179,7 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i { pos.x += x; pos.y += y; - anim = std::make_shared(name); + anim = GH.renderHandler().loadAnimation(name); init(); } @@ -308,7 +309,7 @@ bool CAnimImage::isPlayerColored() const } CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha): - anim(std::make_shared(name)), + anim(GH.renderHandler().loadAnimation(name)), group(Group), frame(0), first(0), diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 74bd835a1..31ebd1559 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -31,6 +31,7 @@ #include "../widgets/TextControls.h" #include "../render/Canvas.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/ColorFilter.h" #include "../adventureMap/AdventureMapInterface.h" #include "../adventureMap/CList.h" @@ -89,10 +90,10 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town } if(!str->borderName.empty()) - border = IImage::createFromFile(str->borderName, EImageBlitMode::ALPHA); + border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::ALPHA); if(!str->areaName.empty()) - area = IImage::createFromFile(str->areaName, EImageBlitMode::ALPHA); + area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA); } const CBuilding * CBuildingRect::getBuilding() diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 9c7d6808a..e67a7e317 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -28,6 +28,7 @@ #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" #include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" @@ -127,7 +128,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) primSkillValues.push_back(value); } - auto primSkills = std::make_shared(AnimationPath::builtin("PSKIL42")); + auto primSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL42")); primSkills->preload(); primSkillImages.push_back(std::make_shared(primSkills, 0, 0, 32, 111)); primSkillImages.push_back(std::make_shared(primSkills, 1, 0, 102, 111)); @@ -148,7 +149,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) expValue = std::make_shared(68, 252); manaValue = std::make_shared(211, 252); - auto secSkills = std::make_shared(AnimationPath::builtin("SECSKILL")); + auto secSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSKILL")); for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 35da927e1..d1c2bce2c 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -23,6 +23,7 @@ #include "../gui/CGuiHandler.h" #include "../render/CAnimation.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/Canvas.h" #include "../render/Graphics.h" #include "../render/IFont.h" @@ -69,7 +70,7 @@ struct ComponentsToBlit namespace { - std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; + std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; std::shared_ptr background;//todo: should be CFilledTexture @@ -79,7 +80,7 @@ void CMessage::init() { for(int i=0; i(AnimationPath::builtin("DIALGBOX")); + dialogBorders[i] = GH.renderHandler().loadAnimation(AnimationPath::builtin("DIALGBOX")); dialogBorders[i]->preload(); for(int j=0; j < dialogBorders[i]->size(0); j++) @@ -92,7 +93,7 @@ void CMessage::init() } } - background = IImage::createFromFile(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); + background = GH.renderHandler().loadImage(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); } void CMessage::dispose() diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 99db9419c..457f37e16 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -30,6 +30,7 @@ #include "../widgets/TextControls.h" #include "../adventureMap/AdventureMapInterface.h" #include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" #include "../../CCallback.h" @@ -169,15 +170,15 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97, 77); rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487, 72); - spellIcons = std::make_shared(AnimationPath::builtin("Spells")); + spellIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("Spells")); schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524, 88); schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117, 74); - schoolBorders[0] = std::make_shared(AnimationPath::builtin("SplevA.def")); - schoolBorders[1] = std::make_shared(AnimationPath::builtin("SplevF.def")); - schoolBorders[2] = std::make_shared(AnimationPath::builtin("SplevW.def")); - schoolBorders[3] = std::make_shared(AnimationPath::builtin("SplevE.def")); + schoolBorders[0] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevA.def")); + schoolBorders[1] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevF.def")); + schoolBorders[2] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevW.def")); + schoolBorders[3] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevE.def")); for(auto item : schoolBorders) item->preload(); diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index b9eafdcb3..c21c1a219 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -20,6 +20,7 @@ #include "../windows/CMessage.h" #include "../renderSDL/SDL_PixelAccess.h" #include "../render/IImage.h" +#include "../render/IRenderHandler.h" #include "../render/Canvas.h" #include "../CGameInfo.h" @@ -210,9 +211,9 @@ void CWindowObject::setShadow(bool on) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowCorner), Point(shadowPos.x, shadowPos.y))); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowRight ), Point(shadowPos.x, shadowStart.y))); - shadowParts.push_back(std::make_shared( IImage::createFromSurface(shadowBottom), Point(shadowStart.x, shadowPos.y))); + shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowCorner), Point(shadowPos.x, shadowPos.y))); + shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowRight ), Point(shadowPos.x, shadowStart.y))); + shadowParts.push_back(std::make_shared( GH.renderHandler().createImage(shadowBottom), Point(shadowStart.x, shadowPos.y))); } SDL_FreeSurface(shadowCorner); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index a83ec3640..fe1c2a9ed 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -42,6 +42,7 @@ #include "../lobby/CSavingScreen.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" #include "../CMT.h" #include "../../CCallback.h" @@ -850,10 +851,10 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, titles[0] = std::make_shared(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0])); titles[1] = std::make_shared(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1])); - auto PSKIL32 = std::make_shared(AnimationPath::builtin("PSKIL32")); + auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32")); PSKIL32->preload(); - auto SECSK32 = std::make_shared(AnimationPath::builtin("SECSK32")); + auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32")); for(int g = 0; g < 4; ++g) { @@ -1262,7 +1263,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - bars = std::make_shared(); + bars = GH.renderHandler().createAnimation(); bars->setCustom("UNIVRED", 0, 0); bars->setCustom("UNIVGOLD", 1, 0); bars->setCustom("UNIVGREN", 2, 0); @@ -1626,7 +1627,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); } - auto PRSTRIPS = std::make_shared(AnimationPath::builtin("PRSTRIPS")); + auto PRSTRIPS = GH.renderHandler().loadAnimation(AnimationPath::builtin("PRSTRIPS")); PRSTRIPS->preload(); for(int g=1; g(283 + 66*g, 24, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[16+g])); - auto itgflags = std::make_shared(AnimationPath::builtin("itgflags")); + auto itgflags = GH.renderHandler().loadAnimation(AnimationPath::builtin("itgflags")); itgflags->preload(); //printing flags From a30e7ba321e81423fafa2a60beeb4f0824af82f0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 21:40:19 +0300 Subject: [PATCH 0323/1248] Remove bitmasks of PlayerColor's. Add encode/decode methods --- lib/constants/EntityIdentifiers.cpp | 10 ++++ lib/constants/EntityIdentifiers.h | 43 +++++++--------- lib/gameState/TavernHeroesPool.cpp | 4 +- lib/gameState/TavernHeroesPool.h | 4 +- lib/mapObjects/CGPandoraBox.cpp | 18 ++----- lib/mapObjects/CGPandoraBox.h | 2 +- lib/mapping/CMap.cpp | 2 +- lib/mapping/CMap.h | 2 +- lib/mapping/MapFormatH3M.cpp | 4 +- lib/mapping/MapFormatJson.cpp | 22 ++------ lib/mapping/MapReaderH3M.cpp | 11 ++++ lib/mapping/MapReaderH3M.h | 1 + lib/serializer/JsonSerializeFormat.h | 75 ---------------------------- 13 files changed, 56 insertions(+), 142 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 1f7a6b9d6..bc0e92843 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -228,6 +228,16 @@ std::string PlayerColor::getStrCap(bool L10n) const return ret; } +si32 PlayerColor::decode(const std::string & identifier) +{ + return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); +} + +std::string PlayerColor::encode(const si32 index) +{ + return GameConstants::PLAYER_COLOR_NAMES[index]; +} + si32 FactionID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index a96ed5017..1cdb4300f 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -59,6 +59,11 @@ public: } }; + template void serialize(Handler &h, const int version) + { + h & num; + } + constexpr void advance(int change) { num += change; @@ -87,11 +92,6 @@ public: :IdentifierBase(_num) {} - template void serialize(Handler &h, const int version) - { - h & BaseClass::num; - } - constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } @@ -122,21 +122,11 @@ class IdentifierWithEnum : public BaseClass static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); public: - constexpr int32_t getNum() const - { - return BaseClass::num; - } - constexpr EnumType toEnum() const { return static_cast(BaseClass::num); } - template void serialize(Handler &h, const int version) - { - h & BaseClass::num; - } - constexpr IdentifierWithEnum(const EnumType & enumValue) { BaseClass::num = static_cast(enumValue); @@ -230,7 +220,7 @@ public: } }; -class PlayerColor : public Identifier +class DLL_LINKAGE PlayerColor : public Identifier { public: using Identifier::Identifier; @@ -240,19 +230,20 @@ public: PLAYER_LIMIT_I = 8, }; - using Mask = uint8_t; + static const PlayerColor SPECTATOR; //252 + static const PlayerColor CANNOT_DETERMINE; //253 + static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) + static const PlayerColor NEUTRAL; //255 + static const PlayerColor PLAYER_LIMIT; //player limit per map - DLL_LINKAGE static const PlayerColor SPECTATOR; //252 - DLL_LINKAGE static const PlayerColor CANNOT_DETERMINE; //253 - DLL_LINKAGE static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) - DLL_LINKAGE static const PlayerColor NEUTRAL; //255 - DLL_LINKAGE static const PlayerColor PLAYER_LIMIT; //player limit per map + bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + bool isSpectator() const; - DLL_LINKAGE bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - DLL_LINKAGE bool isSpectator() const; + std::string getStr(bool L10n = false) const; + std::string getStrCap(bool L10n = false) const; - DLL_LINKAGE std::string getStr(bool L10n = false) const; - DLL_LINKAGE std::string getStrCap(bool L10n = false) const; + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); }; class TeamID : public Identifier diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 70f441f98..bad731896 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -74,7 +74,7 @@ void TavernHeroesPool::setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, bool TavernHeroesPool::isHeroAvailableFor(HeroTypeID hero, PlayerColor color) const { if (perPlayerAvailability.count(hero)) - return perPlayerAvailability.at(hero) & (1 << color.getNum()); + return perPlayerAvailability.at(hero).count(color) != 0; return true; } @@ -131,7 +131,7 @@ void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) heroesPool[HeroTypeID(hero->subID)] = hero; } -void TavernHeroesPool::setAvailability(HeroTypeID hero, PlayerColor::Mask mask) +void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set mask) { perPlayerAvailability[hero] = mask; } diff --git a/lib/gameState/TavernHeroesPool.h b/lib/gameState/TavernHeroesPool.h index 97c54879c..fb5dc136f 100644 --- a/lib/gameState/TavernHeroesPool.h +++ b/lib/gameState/TavernHeroesPool.h @@ -44,7 +44,7 @@ class DLL_LINKAGE TavernHeroesPool /// list of which players are able to purchase specific hero /// if hero is not present in list, he is available for everyone - std::map perPlayerAvailability; + std::map> perPlayerAvailability; /// list of heroes currently available in taverns std::vector currentTavern; @@ -71,7 +71,7 @@ public: void addHeroToPool(CGHeroInstance * hero); /// Marks hero as available to only specific set of players - void setAvailability(HeroTypeID hero, PlayerColor::Mask mask); + void setAvailability(HeroTypeID hero, std::set mask); /// Makes hero available in tavern of specified player void setHeroForPlayer(PlayerColor player, TavernHeroSlot slot, HeroTypeID hero, CSimpleArmy & army, TavernSlotRole role); diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 2e64f438b..9a4dd89e8 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -443,8 +443,9 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) void CGEvent::onHeroVisit( const CGHeroInstance * h ) const { - if(!(availableFor & (1 << h->tempOwner.getNum()))) + if(availableFor.count(h->tempOwner) == 0) return; + if(cb->getPlayerSettings(h->tempOwner)->isControlledByHuman()) { if(humanActivate) @@ -490,20 +491,7 @@ void CGEvent::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeBool("aIActivable", computerActivate, true, false, false); handler.serializeBool("humanActivable", humanActivate, true, false, true); handler.serializeBool("removeAfterVisit", removeAfterVisit, true, false, false); - - { - auto decodePlayer = [](const std::string & id)->si32 - { - return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, id); - }; - - auto encodePlayer = [](si32 idx)->std::string - { - return GameConstants::PLAYER_COLOR_NAMES[idx]; - }; - - handler.serializeIdArray("availableFor", availableFor, GameConstants::ALL_PLAYERS, decodePlayer, encodePlayer); - } + handler.serializeIdArray("availableFor", availableFor); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index b6c94a7bd..6cef83266 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -72,7 +72,7 @@ class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects { public: bool removeAfterVisit = false; //true if event is removed after occurring - ui8 availableFor = 0; //players whom this event is available for + std::set availableFor; //players whom this event is available for bool computerActivate = false; //true if computer player can activate this event bool humanActivate = false; //true if human player can activate this event diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index c73c1e578..9805821a2 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -35,7 +35,7 @@ void Rumor::serializeJson(JsonSerializeFormat & handler) handler.serializeString("text", text); } -DisposedHero::DisposedHero() : heroId(0), portrait(255), players(0) +DisposedHero::DisposedHero() : heroId(0), portrait(255) { } diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 957263133..4e3402eb5 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -59,7 +59,7 @@ struct DLL_LINKAGE DisposedHero HeroTypeID heroId; HeroTypeID portrait; /// The portrait id of the hero, -1 is default. std::string name; - PlayerColor::Mask players; /// Who can hire this hero (bitfield). + std::set players; /// Who can hire this hero (bitfield). template void serialize(Handler & h, const int version) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index cd8d341a6..2c3862745 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -692,7 +692,7 @@ void CMapLoaderH3M::readDisposedHeroes() map->disposedHeroes[g].heroId = reader->readHero().getNum(); map->disposedHeroes[g].portrait = reader->readHeroPortrait(); map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); - map->disposedHeroes[g].players = reader->readUInt8(); + reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); } } } @@ -995,7 +995,7 @@ CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition) readBoxContent(object, mapPosition); - object->availableFor = reader->readUInt8(); + reader->readBitmaskPlayers(object->availableFor, false); object->computerActivate = reader->readBool(); object->removeAfterVisit = reader->readBool(); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index e8bd5fbe1..917bc623f 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -729,18 +729,16 @@ void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) { HeroTypeID type(HeroTypeID::decode(entry.first)); - ui8 mask = 0; + std::set mask; for(const JsonNode & playerData : entry.second["availableFor"].Vector()) { PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); if(player.isValidPlayer()) - { - mask |= 1 << player.getNum(); - } + mask.insert(player); } - if(mask != 0 && mask != GameConstants::ALL_PLAYERS && type.getNum() >= 0) + if(!mask.empty() && mask.size() != PlayerColor::PLAYER_LIMIT_I && type.getNum() >= 0) { DisposedHero hero; @@ -760,24 +758,14 @@ void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format - for(const DisposedHero & hero : map->disposedHeroes) + for(DisposedHero & hero : map->disposedHeroes) { std::string type = HeroTypeID::encode(hero.heroId); auto definition = definitions->enterStruct(type); JsonNode players(JsonNode::JsonType::DATA_VECTOR); - - for(int playerNum = 0; playerNum < PlayerColor::PLAYER_LIMIT_I; playerNum++) - { - if((1 << playerNum) & hero.players) - { - JsonNode player(JsonNode::JsonType::DATA_STRING); - player.String() = GameConstants::PLAYER_COLOR_NAMES[playerNum]; - players.Vector().push_back(player); - } - } - definition->serializeRaw("availableFor", players, std::nullopt); + definition->serializeIdArray("availableFor", hero.players); } } diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 45b418737..f7328b9de 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -35,6 +35,12 @@ SpellID MapReaderH3M::remapIdentifier(const SpellID & identifier) return identifier; } +template<> +PlayerColor MapReaderH3M::remapIdentifier(const PlayerColor & identifier) +{ + return identifier; +} + template Identifier MapReaderH3M::remapIdentifier(const Identifier & identifier) { @@ -227,6 +233,11 @@ void MapReaderH3M::readBitmaskFactions(std::set & dest, bool invert) readBitmask(dest, features.factionsBytes, features.factionsCount, invert); } +void MapReaderH3M::readBitmaskPlayers(std::set & dest, bool invert) +{ + readBitmask(dest, 1, 8, invert); +} + void MapReaderH3M::readBitmaskResources(std::set & dest, bool invert) { readBitmask(dest, features.resourcesBytes, features.resourcesCount, invert); diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index c8e6bf6ff..ca2029722 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -47,6 +47,7 @@ public: void readBitmaskBuildings(std::set & dest, std::optional faction); void readBitmaskFactions(std::set & dest, bool invert); + void readBitmaskPlayers(std::set & dest, bool invert); void readBitmaskResources(std::set & dest, bool invert); void readBitmaskHeroClassesSized(std::set & dest, bool invert); void readBitmaskHeroes(std::vector & dest, bool invert); diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index a2be34204..1d1ff4848 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -335,81 +335,6 @@ public: } } - ///si32-convertible identifier set <-> Json array of string - ///Type U is only used for code & decode - ///TODO: Auto deduce U based on T? - template - void serializeIdArray(const std::string & fieldName, std::set & value, const std::set & defaultValue) - { - std::vector temp; - - if(saving && value != defaultValue) - { - temp.reserve(value.size()); - - for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } - serializeInternal(fieldName, temp, &U::decode, &U::encode); - } - - if(!saving) - { - JsonNode node; - serializeRaw(fieldName, node, std::nullopt); - if(node.Vector().empty()) - { - value = defaultValue; - } - else - { - value.clear(); - - for(const auto & id : node.Vector()) - { - VLC->identifiers()->requestIdentifier(U::entityType(), id, [&value](int32_t identifier) - { - value.emplace(identifier); - }); - } - } - } - } - - ///bitmask <-> Json array of string - template - void serializeIdArray(const std::string & fieldName, T & value, const T & defaultValue, const TDecoder & decoder, const TEncoder & encoder) - { - static_assert(8 * sizeof(T) >= Size, "Mask size too small"); - - std::vector temp; - temp.reserve(Size); - - if(saving && value != defaultValue) - { - for(si32 i = 0; i < Size; i++) - if(value & (1 << i)) - temp.push_back(i); - serializeInternal(fieldName, temp, decoder, encoder); - } - - if(!saving) - { - serializeInternal(fieldName, temp, decoder, encoder); - - if(temp.empty()) - value = defaultValue; - else - { - value = 0; - for(auto i : temp) - value |= (1 << i); - } - } - } - ///si32-convertible instance identifier <-> Json string template void serializeInstance(const std::string & fieldName, T & value, const T & defaultValue) From 82989e630291fbeedd1bf43bff62d8e41fa9d05c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 21:42:02 +0300 Subject: [PATCH 0324/1248] Use pointers in containers to guaranteed fixed address --- lib/rmg/CRmgTemplateStorage.cpp | 14 +++++++------- lib/rmg/CRmgTemplateStorage.h | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index ae5f01c00..9e6423b9b 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -29,7 +29,7 @@ void CRmgTemplateStorage::afterLoadFinalization() { for (auto& temp : templates) { - temp.second.afterLoad(); + temp.second->afterLoad(); } } @@ -39,10 +39,10 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const { JsonDeserializer handler(nullptr, data); auto fullKey = scope + ":" + name; //actually it's not used - templates[fullKey].setId(fullKey); - templates[fullKey].serializeJson(handler); - templates[fullKey].setName(name); - templates[fullKey].validate(); + templates[fullKey]->setId(fullKey); + templates[fullKey]->serializeJson(handler); + templates[fullKey]->setName(name); + templates[fullKey]->validate(); } catch(const std::exception & e) { @@ -67,7 +67,7 @@ const CRmgTemplate * CRmgTemplateStorage::getTemplate(const std::string & templa auto iter = templates.find(templateName); if(iter==templates.end()) return nullptr; - return &iter->second; + return iter->second.get(); } std::vector CRmgTemplateStorage::getTemplates() const @@ -76,7 +76,7 @@ std::vector CRmgTemplateStorage::getTemplates() const result.reserve(templates.size()); for(const auto & i : templates) { - result.push_back(&i.second); + result.push_back(i.second.get()); } return result; } diff --git a/lib/rmg/CRmgTemplateStorage.h b/lib/rmg/CRmgTemplateStorage.h index 2837fc262..8a129dff1 100644 --- a/lib/rmg/CRmgTemplateStorage.h +++ b/lib/rmg/CRmgTemplateStorage.h @@ -37,7 +37,7 @@ public: std::vector getTemplates() const; private: - std::map templates; + std::map> templates; }; From c8a6cd74cca6cce7332dbdc44ae322da13636bb6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 21:42:20 +0300 Subject: [PATCH 0325/1248] Additional checks for invalid access to IdentifierStorage --- lib/modding/IdentifierStorage.cpp | 24 ++++++++++++++++-------- lib/modding/IdentifierStorage.h | 6 +++--- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 61a8854d7..c9ffc82a0 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -20,11 +20,6 @@ VCMI_LIB_NAMESPACE_BEGIN -CIdentifierStorage::CIdentifierStorage(): - state(LOADING) -{ -} - void CIdentifierStorage::checkIdentifier(std::string & ID) { if (boost::algorithm::ends_with(ID, ".")) @@ -52,7 +47,7 @@ void CIdentifierStorage::requestIdentifier(ObjectCallback callback) const assert(!callback.localScope.empty()); - if (state != FINISHED) // enqueue request if loading is still in progress + if (state != ELoadingState::FINISHED) // enqueue request if loading is still in progress scheduledRequests.push_back(callback); else // execute immediately for "late" requests resolveIdentifier(callback); @@ -138,6 +133,9 @@ void CIdentifierStorage::tryRequestIdentifier(const std::string & type, const Js std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const { + //TODO: RE-ENABLE + //assert(state != ELoadingState::LOADING); + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); if (idList.size() == 1) @@ -150,6 +148,8 @@ std::optional CIdentifierStorage::getIdentifier(const std::string & scope, std::optional CIdentifierStorage::getIdentifier(const std::string & type, const JsonNode & name, bool silent) const { + assert(state != ELoadingState::LOADING); + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(name.meta, type, name.String(), std::function(), silent)); if (idList.size() == 1) @@ -162,6 +162,8 @@ std::optional CIdentifierStorage::getIdentifier(const std::string & type, std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, bool silent) const { + assert(state != ELoadingState::LOADING); + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(name.meta, name.String(), std::function(), silent)); if (idList.size() == 1) @@ -174,6 +176,8 @@ std::optional CIdentifierStorage::getIdentifier(const JsonNode & name, boo std::optional CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & fullName, bool silent) const { + assert(state != ELoadingState::LOADING); + auto idList = getPossibleIdentifiers(ObjectCallback::fromNameWithType(scope, fullName, std::function(), silent)); if (idList.size() == 1) @@ -186,6 +190,8 @@ std::optional CIdentifierStorage::getIdentifier(const std::string & scope, void CIdentifierStorage::registerObject(const std::string & scope, const std::string & type, const std::string & name, si32 identifier) { + assert(state != ELoadingState::FINISHED); + ObjectData data; data.scope = scope; data.id = identifier; @@ -311,7 +317,9 @@ bool CIdentifierStorage::resolveIdentifier(const ObjectCallback & request) const void CIdentifierStorage::finalize() { - state = FINALIZING; + assert(state == ELoadingState::LOADING); + + state = ELoadingState::FINALIZING; bool errorsFound = false; while ( !scheduledRequests.empty() ) @@ -333,7 +341,7 @@ void CIdentifierStorage::finalize() logMod->error("All known identifiers were dumped into log file"); } assert(errorsFound == false); - state = FINISHED; + state = ELoadingState::FINISHED; } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 87bc0c3fa..f7405ce7f 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -17,7 +17,7 @@ class JsonNode; /// if possible, objects ID's should be in format ., camelCase e.g. "creature.grandElf" class DLL_LINKAGE CIdentifierStorage { - enum ELoadingState + enum class ELoadingState { LOADING, FINALIZING, @@ -63,7 +63,7 @@ class DLL_LINKAGE CIdentifierStorage std::multimap registeredObjects; mutable std::vector scheduledRequests; - ELoadingState state; + ELoadingState state = ELoadingState::LOADING; /// Check if identifier can be valid (camelCase, point as separator) static void checkIdentifier(std::string & ID); @@ -73,7 +73,7 @@ class DLL_LINKAGE CIdentifierStorage std::vector getPossibleIdentifiers(const ObjectCallback & callback) const; public: - CIdentifierStorage(); + CIdentifierStorage() = default; virtual ~CIdentifierStorage() = default; /// request identifier for specific object name. From 7e27ac707333b7c68eb5dcff1a82dfb69ee1973d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 22:36:00 +0300 Subject: [PATCH 0326/1248] IdentifierStorage is now separate handler in VLC --- lib/IHandlerBase.cpp | 2 +- lib/VCMI_Lib.cpp | 25 +++---------------- lib/VCMI_Lib.h | 4 ++- .../CObjectClassesHandler.cpp | 4 +-- lib/modding/CModHandler.cpp | 24 +----------------- lib/modding/CModHandler.h | 6 ----- lib/modding/IdentifierStorage.cpp | 22 ++++++++++++++++ lib/modding/IdentifierStorage.h | 2 +- 8 files changed, 34 insertions(+), 55 deletions(-) diff --git a/lib/IHandlerBase.cpp b/lib/IHandlerBase.cpp index ec51814a0..1512c0680 100644 --- a/lib/IHandlerBase.cpp +++ b/lib/IHandlerBase.cpp @@ -23,7 +23,7 @@ std::string IHandlerBase::getScopeBuiltin() void IHandlerBase::registerObject(const std::string & scope, const std::string & type_name, const std::string & name, si32 index) { - return VLC->modh->getIdentifiers().registerObject(scope, type_name, name, index); + return VLC->identifiersHandler->registerObject(scope, type_name, name, index); } VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 141b3acda..e20444152 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -111,7 +111,7 @@ const IBonusTypeHandler * LibClasses::getBth() const const CIdentifierStorage * LibClasses::identifiers() const { - return &modh->getIdentifiers(); + return identifiersHandler; } const spells::effects::Registry * LibClasses::spellEffects() const @@ -185,6 +185,7 @@ void LibClasses::loadModFilesystem(bool onlyEssential) { CStopWatch loadTime; modh = new CModHandler(); + identifiersHandler = new CIdentifierStorage(); modh->loadMods(onlyEssential); logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); @@ -212,49 +213,29 @@ void LibClasses::init(bool onlyEssential) modh->initializeConfig(); createHandler(generaltexth, "General text", pomtime); - createHandler(bth, "Bonus type", pomtime); - createHandler(roadTypeHandler, "Road", pomtime); createHandler(riverTypeHandler, "River", pomtime); createHandler(terrainTypeHandler, "Terrain", pomtime); - createHandler(heroh, "Hero", pomtime); - createHandler(arth, "Artifact", pomtime); - createHandler(creh, "Creature", pomtime); - createHandler(townh, "Town", pomtime); - createHandler(objh, "Object", pomtime); - createHandler(objtypeh, "Object types information", pomtime); - createHandler(spellh, "Spell", pomtime); - createHandler(skillh, "Skill", pomtime); - createHandler(terviewh, "Terrain view pattern", pomtime); - createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?) - #if SCRIPTING_ENABLED createHandler(scriptHandler, "Script", pomtime); #endif - createHandler(battlefieldsHandler, "Battlefields", pomtime); - createHandler(obstacleHandler, "Obstacles", pomtime); - logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); modh->load(); - modh->afterLoad(onlyEssential); - - //FIXME: make sure that everything is ok after game restart - //TODO: This should be done every time mod config changes } void LibClasses::clear() @@ -276,6 +257,7 @@ void LibClasses::clear() #endif delete battlefieldsHandler; delete generaltexth; + delete identifiersHandler; makeNull(); } @@ -298,6 +280,7 @@ void LibClasses::makeNull() scriptHandler = nullptr; #endif battlefieldsHandler = nullptr; + identifiersHandler = nullptr; } LibClasses::LibClasses() diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index b6a7e0c4c..2886eaee5 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -97,6 +97,7 @@ public: TerrainTypeHandler * terrainTypeHandler; RoadTypeHandler * roadTypeHandler; RiverTypeHandler * riverTypeHandler; + CIdentifierStorage * identifiersHandler; CTerrainViewPatternConfig * terviewh; CRmgTemplateStorage * tplh; @@ -122,8 +123,9 @@ public: template void serialize(Handler &h, const int version) { + h & identifiersHandler; // must be first - identifiers registry is used for handlers loading #if SCRIPTING_ENABLED - h & scriptHandler;//must be first (or second after modh), it can modify factories other handlers depends on + h & scriptHandler;//must be first (or second after identifiers), it can modify factories other handlers depends on if(!h.saving) { scriptsLoaded(); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index ffa63e29b..feca8b002 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -277,7 +277,7 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons { auto * object = loadFromJson(scope, data, name, objects.size()); objects.push_back(object); - VLC->modh->getIdentifiers().registerObject(scope, "object", name, object->id); + VLC->identifiersHandler->registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -285,7 +285,7 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons auto * object = loadFromJson(scope, data, name, index); assert(objects[(si32)index] == nullptr); // ensure that this id was not loaded before objects[static_cast(index)] = object; - VLC->modh->getIdentifiers().registerObject(scope, "object", name, object->id); + VLC->identifiersHandler->registerObject(scope, "object", name, object->id); } void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index a26ab4248..50ab6103a 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -41,25 +41,8 @@ static JsonNode loadModSettings(const std::string & path) CModHandler::CModHandler() : content(std::make_shared()) - , identifiers(std::make_unique()) , coreMod(std::make_unique()) { - //TODO: moddable spell schools - for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) - identifiers->registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); - - identifiers->registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); - - for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) - { - identifiers->registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); - } - - for(int i=0; iregisterObject(ModScope::scopeBuiltin(), "primSkill", NPrimarySkill::names[i], i); - identifiers->registerObject(ModScope::scopeBuiltin(), "primarySkill", NPrimarySkill::names[i], i); - } } CModHandler::~CModHandler() = default; @@ -462,7 +445,7 @@ void CModHandler::load() logMod->info("\tLoading mod data: %d ms", timer.getDiff()); VLC->creh->loadCrExpMod(); - identifiers->finalize(); + VLC->identifiersHandler->finalize(); logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); content->afterLoadFinalization(); @@ -534,9 +517,4 @@ void CModHandler::trySetActiveMods(std::vector saveActiveMods, const std std::swap(activeMods, newActiveMods); } -CIdentifierStorage & CModHandler::getIdentifiers() -{ - return *identifiers; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index eaa84d84a..8827fe035 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -54,13 +54,9 @@ class DLL_LINKAGE CModHandler : boost::noncopyable /// Attempt to set active mods according to provided list of mods from save, throws on failure void trySetActiveMods(std::vector saveActiveMods, const std::map & modList); - std::unique_ptr identifiers; - public: std::shared_ptr content; //(!)Do not serialize FIXME: make private - CIdentifierStorage & getIdentifiers(); - /// receives list of available mods and trying to load mod.json from all of them void initializeConfig(); void loadMods(bool onlyEssential = false); @@ -109,8 +105,6 @@ public: trySetActiveMods(saveActiveMods, modVersions); } - - h & identifiers; } }; diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index c9ffc82a0..b35ecba59 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -15,11 +15,33 @@ #include "../JsonNode.h" #include "../VCMI_Lib.h" +#include "../constants/StringConstants.h" +#include "../spells/CSpellHandler.h" #include VCMI_LIB_NAMESPACE_BEGIN +CIdentifierStorage::CIdentifierStorage() +{ + //TODO: moddable spell schools + for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) + registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); + + registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); + + for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); + } + + for(int i=0; i getPossibleIdentifiers(const ObjectCallback & callback) const; public: - CIdentifierStorage() = default; + CIdentifierStorage(); virtual ~CIdentifierStorage() = default; /// request identifier for specific object name. From 86a7f5f5cda4e64163407b601317bc2b5cd82c83 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:21:02 +0300 Subject: [PATCH 0327/1248] Removed getStr(bool), replaced with similar toString() --- AI/BattleAI/BattleAI.cpp | 2 +- AI/BattleAI/BattleEvaluator.cpp | 2 +- AI/BattleAI/StackWithBonuses.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 14 +++---- AI/VCAI/VCAI.cpp | 12 +++--- client/CPlayerInterface.cpp | 4 +- client/Client.cpp | 12 +++--- client/ClientCommandManager.cpp | 2 +- client/NetPacksClient.cpp | 6 +-- client/battle/BattleInterface.cpp | 2 +- client/lobby/CSelectionBase.cpp | 2 +- client/render/Graphics.cpp | 2 +- lib/CPlayerState.cpp | 14 +++++-- lib/CStack.cpp | 2 +- lib/StartInfo.cpp | 2 +- lib/battle/BattleInfo.cpp | 4 +- lib/battle/CBattleInfoEssentials.cpp | 4 +- lib/constants/EntityIdentifiers.cpp | 39 +++++--------------- lib/constants/EntityIdentifiers.h | 21 +++++------ lib/gameState/CGameState.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 8 ++-- mapeditor/graphics.cpp | 2 +- mapeditor/validator.cpp | 4 +- server/CGameHandler.cpp | 4 +- server/processors/HeroPoolProcessor.cpp | 6 +-- server/processors/PlayerMessageProcessor.cpp | 6 +-- server/queries/CQuery.cpp | 2 +- 27 files changed, 83 insertions(+), 99 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index efd2c6d88..f96d33084 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -244,7 +244,7 @@ void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2 void CBattleAI::print(const std::string &text) const { - logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); + logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); } std::optional CBattleAI::considerFleeingOrSurrendering() diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 64c505591..c67eab4db 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -703,7 +703,7 @@ void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSp void BattleEvaluator::print(const std::string & text) const { - logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); + logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); } diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 6c5f57e31..c80ec1dce 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -231,7 +231,7 @@ void StackWithBonuses::removeUnitBonus(const CSelector & selector) std::string StackWithBonuses::getDescription() const { std::ostringstream oss; - oss << unitOwner().getStr(); + oss << unitOwner().toString(); oss << " battle stack [" << unitId() << "]: " << getCount() << " of "; if(type) oss << type->getJsonKey(); diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 9ae840d4b..ad30802c3 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -192,7 +192,7 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic { LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); NET_EVENT_HANDLER; - logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost")); + logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); // some whitespace to flush stream logAi->debug(std::string(200, ' ')); @@ -201,12 +201,12 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic { if(victoryLossCheckResult.victory()) { - logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.getStr()); + logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.toString()); logAi->debug("Turn nr %d", myCb->getDate()); } else { - logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr()); + logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); } // some whitespace to flush stream @@ -776,7 +776,7 @@ void AIGateway::makeTurn() MAKING_TURN; auto day = cb->getDate(Date::DAY); - logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); + logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); boost::shared_lock gsLock(CGameState::mutex); setThreadName("AIGateway::makeTurn"); @@ -1096,7 +1096,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); bool won = br->winner == myCb->battleGetMySide(); - logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); + logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); if (queryID != QueryID::NONE) @@ -1421,7 +1421,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade void AIGateway::endTurn() { - logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr()); + logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); if(!status.haveTurn()) { logAi->error("Not having turn at the end of turn???"); @@ -1441,7 +1441,7 @@ void AIGateway::endTurn() } while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over - logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr()); + logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); } void AIGateway::buildArmyIn(const CGTownInstance * t) diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 231c77cd8..a150e7c6a 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -204,7 +204,7 @@ void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryL { LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); NET_EVENT_HANDLER; - logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost")); + logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); if(player == playerID) { if(victoryLossCheckResult.victory()) @@ -214,7 +214,7 @@ void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryL } else { - logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr()); + logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); } finish(); @@ -779,7 +779,7 @@ void VCAI::makeTurn() MAKING_TURN; auto day = cb->getDate(Date::DAY); - logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); + logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); boost::shared_lock gsLock(CGameState::mutex); setThreadName("VCAI::makeTurn"); @@ -1593,7 +1593,7 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID) assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); bool won = br->winner == myCb->battleGetMySide(); - logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); + logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); if (queryID != QueryID::NONE) @@ -2288,7 +2288,7 @@ HeroPtr VCAI::primaryHero() const void VCAI::endTurn() { - logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr()); + logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); if(!status.haveTurn()) { logAi->error("Not having turn at the end of turn???"); @@ -2300,7 +2300,7 @@ void VCAI::endTurn() } while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over - logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr()); + logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); } void VCAI::striveToGoal(Goals::TSubgoal basicGoal) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2240cd638..cddcfe2c9 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -125,7 +125,7 @@ struct HeroObjectRetriever CPlayerInterface::CPlayerInterface(PlayerColor Player): localState(std::make_unique(*this)) { - logGlobal->trace("\tHuman player interface for player %s being constructed", Player.getStr()); + logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); GH.defActionsDef = 0; @@ -147,7 +147,7 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): CPlayerInterface::~CPlayerInterface() { - logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.getStr()); + logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString()); delete showingDialog; delete cingconsole; if (LOCPLINT == this) diff --git a/client/Client.cpp b/client/Client.cpp index b1d89cfa4..931dd4e36 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -408,7 +408,7 @@ void CClient::initPlayerEnvironments() bool hasHumanPlayer = false; for(auto & color : allPlayers) { - logNetwork->info("Preparing environment for player %s", color.getStr()); + logNetwork->info("Preparing environment for player %s", color.toString()); playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); if(!hasHumanPlayer && gs->players[color].isHuman()) @@ -439,7 +439,7 @@ void CClient::initPlayerInterfaces() if(!vstd::contains(playerint, color)) { - logNetwork->info("Preparing interface for player %s", color.getStr()); + logNetwork->info("Preparing interface for player %s", color.toString()); if(playerInfo.second.isControlledByAI()) { bool alliedToHuman = false; @@ -448,12 +448,12 @@ void CClient::initPlayerInterfaces() alliedToHuman = true; auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); - logNetwork->info("Player %s will be lead by %s", color.getStr(), AiToGive); + logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); } else { - logNetwork->info("Player %s will be lead by human", color.getStr()); + logNetwork->info("Player %s will be lead by human", color.toString()); installNewPlayerInterface(std::make_shared(color), color); } } @@ -503,7 +503,7 @@ void CClient::installNewPlayerInterface(std::shared_ptr gameInte playerint[color] = gameInterface; - logGlobal->trace("\tInitializing the interface for player %s", color.getStr()); + logGlobal->trace("\tInitializing the interface for player %s", color.toString()); auto cb = std::make_shared(gs, color, this); battleCallbacks[color] = cb; gameInterface->initGameInterface(playerEnvironments.at(color), cb); @@ -519,7 +519,7 @@ void CClient::installNewBattleInterface(std::shared_ptr ba if(needCallback) { - logGlobal->trace("\tInitializing the battle interface for player %s", color.getStr()); + logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); auto cbc = std::make_shared(color, this); battleCallbacks[color] = cbc; battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 5a4c2f8cf..6f0e22010 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -97,7 +97,7 @@ void ClientCommandManager::handleGoSoloCommand() if(elem.second.human) { auto AiToGive = CSH->client->aiNameForPlayer(*CSH->client->getPlayerSettings(elem.first), false, false); - printCommandMessage("Player " + elem.first.getStr() + " will be lead by " + AiToGive, ELogLevel::INFO); + printCommandMessage("Player " + elem.first.toString() + " will be lead by " + AiToGive, ELogLevel::INFO); CSH->client->installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), elem.first); } } diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 2c692a31c..3a89d8e1b 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -874,7 +874,7 @@ void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) { - logNetwork->debug("Server gives turn to %s", pack.player.getStr()); + logNetwork->debug("Server gives turn to %s", pack.player.toString()); callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); @@ -882,12 +882,12 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) { - logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.getStr()); + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); } void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) { - logNetwork->debug("pack.player %s sends a message: %s", pack.player.getStr(), pack.text); + logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text); std::ostringstream str; if(pack.player.isSpectator()) diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index deb640985..2cd96c9d9 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -244,7 +244,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID sp auto side = curInt->cb->playerToSide(curInt->playerID); if(!side) { - logGlobal->error("Player %s is not in battle", curInt->playerID.getStr()); + logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); return; } diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 5701a17bc..9128a4e29 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -240,7 +240,7 @@ void InfoCard::changeSelection() int pid = p.first; if(pset) { - auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.getStr()); + auto name = boost::str(boost::format("%s (%d-%d %s)") % p.second.name % p.second.connection % pid % pset->color.toString()); labelGroupPlayersAssigned->add(24, 285 + (int)labelGroupPlayersAssigned->currentSize()*(int)graphics->fonts[FONT_SMALL]->getLineHeight(), name); } else diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index ea9b55122..d6710effe 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -151,7 +151,7 @@ void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) } else { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); + logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); return; } //FIXME: not all player colored images have player palette at last 32 indexes diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index 41d2676d9..faa021bbe 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -11,6 +11,8 @@ #include "CPlayerState.h" #include "gameState/QuestInfo.h" +#include "CGeneralTextHandler.h" +#include "VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +45,7 @@ PlayerState::~PlayerState() = default; std::string PlayerState::nodeName() const { - return "Player " + color.getStrCap(false); + return "Player " + color.toString(); } PlayerColor PlayerState::getId() const @@ -60,18 +62,22 @@ int32_t PlayerState::getIconIndex() const { return color.getNum(); } + std::string PlayerState::getJsonKey() const { - return color.getStr(false); + return color.toString(); } + std::string PlayerState::getNameTranslated() const { - return color.getStr(true); + return VLC->generaltexth->translate(getNameTextID()); } + std::string PlayerState::getNameTextID() const { - return color.getStr(false); + return TextIdentifier("core.plcolors", color.getNum()).get(); } + void PlayerState::registerIcons(const IconRegistar & cb) const { //We cannot register new icons for players diff --git a/lib/CStack.cpp b/lib/CStack.cpp index c8e63313c..9343340cf 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -162,7 +162,7 @@ const CGHeroInstance * CStack::getMyHero() const std::string CStack::nodeName() const { std::ostringstream oss; - oss << owner.getStr(); + oss << owner.toString(); oss << " battle stack [" << ID << "]: " << getCount() << " of "; if(type) oss << type->getNamePluralTextID(); diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 5625807ff..fd1bd8dcd 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -41,7 +41,7 @@ PlayerSettings & StartInfo::getIthPlayersSettings(const PlayerColor & no) { if(playerInfos.find(no) != playerInfos.end()) return playerInfos[no]; - logGlobal->error("Cannot find info about player %s. Throwing...", no.getStr()); + logGlobal->error("Cannot find info about player %s. Throwing...", no.toString()); throw std::runtime_error("Cannot find info about player"); } const PlayerSettings & StartInfo::getIthPlayersSettings(const PlayerColor & no) const diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index d5a714dcb..52c1b274e 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -532,7 +532,7 @@ const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const if(side.color == player) return side.hero; - logGlobal->error("Player %s is not in battle!", player.getStr()); + logGlobal->error("Player %s is not in battle!", player.toString()); return nullptr; } @@ -542,7 +542,7 @@ ui8 BattleInfo::whatSide(const PlayerColor & player) const if(sides[i].color == player) return i; - logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.getStr()); + logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.toString()); return -1; } diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index df60ba5f5..130d6ad20 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -164,7 +164,7 @@ BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() co if(*player == getBattle()->getSidePlayer(BattleSide::DEFENDER)) return BattlePerspective::RIGHT_SIDE; - logGlobal->error("Cannot find player %s in battle!", player->getStr()); + logGlobal->error("Cannot find player %s in battle!", player->toString()); return BattlePerspective::INVALID; } @@ -296,7 +296,7 @@ BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) co if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) return BattleSideOpt(BattleSide::DEFENDER); - logGlobal->warn("Cannot find side for player %s", player.getStr()); + logGlobal->warn("Cannot find side for player %s", player.toString()); return std::nullopt; } diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index bc0e92843..bb1eaa497 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -199,36 +199,15 @@ bool PlayerColor::isValidPlayer() const bool PlayerColor::isSpectator() const { - return num == SPECTATOR.num; -} - -std::string PlayerColor::getStr(bool L10n) const -{ - std::string ret = "unnamed"; - if(isValidPlayer()) - { - if(L10n) - ret = VLC->generaltexth->colors[num]; - else - ret = GameConstants::PLAYER_COLOR_NAMES[num]; - } - else if(L10n) - { - ret = VLC->generaltexth->allTexts[508]; - ret[0] = std::tolower(ret[0]); - } - - return ret; -} - -std::string PlayerColor::getStrCap(bool L10n) const -{ - std::string ret = getStr(L10n); - ret[0] = std::toupper(ret[0]); - return ret; -} - -si32 PlayerColor::decode(const std::string & identifier) + return num == SPECTATOR.num; +} + +std::string PlayerColor::toString() const +{ + return encode(num); +} + +si32 PlayerColor::decode(const std::string & identifier) { return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 1cdb4300f..6f242c564 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -236,17 +236,16 @@ public: static const PlayerColor NEUTRAL; //255 static const PlayerColor PLAYER_LIMIT; //player limit per map - bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - bool isSpectator() const; - - std::string getStr(bool L10n = false) const; - std::string getStrCap(bool L10n = false) const; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); -}; - -class TeamID : public Identifier + bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + bool isSpectator() const; + + std::string toString() const; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); +}; + +class TeamID : public Identifier { public: using Identifier::Identifier; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c81342752..f182570ca 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -136,7 +136,7 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return *RandomGeneratorUtil::nextItem(factionHeroes, getRandomGenerator()); } - logGlobal->warn("Cannot find free hero of appropriate faction for player %s - trying to get first available...", owner.getStr()); + logGlobal->warn("Cannot find free hero of appropriate faction for player %s - trying to get first available...", owner.toString()); if(!otherHeroes.empty()) { return *RandomGeneratorUtil::nextItem(otherHeroes, getRandomGenerator()); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 2c3862745..cbed7f3e7 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1297,12 +1297,12 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) if(htid.getNum() == -1) { object->powerRank = reader->readUInt8(); - logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().getStr()); + logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); } else { object->heroType = htid; - logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().getStr()); + logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); } return object; @@ -1774,9 +1774,9 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec } if (object->subID != -1) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().getStr()); + logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); else - logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().getStr()); + logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); reader->skipZero(16); return object; diff --git a/mapeditor/graphics.cpp b/mapeditor/graphics.cpp index ce797921c..ee4fa8ade 100644 --- a/mapeditor/graphics.cpp +++ b/mapeditor/graphics.cpp @@ -238,7 +238,7 @@ void Graphics::blueToPlayersAdv(QImage * sur, PlayerColor player) } else { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.getStr()); + logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); return; } //FIXME: not all player colored images have player palette at last 32 indexes diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 4b70ddc84..ab84a05d6 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -97,14 +97,14 @@ std::list Validator::validate(const CMap * map) if(o->getOwner() != PlayerColor::NEUTRAL && o->getOwner().getNum() < map->players.size()) { if(!map->players[o->getOwner().getNum()].canAnyonePlay()) - issues.emplace_back(QString(tr("Object %1 is assigned to non-playable player %2")).arg(o->instanceName.c_str(), o->getOwner().getStr().c_str()), true); + issues.emplace_back(QString(tr("Object %1 is assigned to non-playable player %2")).arg(o->instanceName.c_str(), o->getOwner().toString().c_str()), true); } //checking towns if(auto * ins = dynamic_cast(o.get())) { bool has = amountOfCastles.count(ins->getOwner().getNum()); if(!has && ins->getOwner() != PlayerColor::NEUTRAL) - issues.emplace_back(tr("Town %1 has undefined owner %2").arg(ins->instanceName.c_str(), ins->getOwner().getStr().c_str()), true); + issues.emplace_back(tr("Town %1 has undefined owner %2").arg(ins->instanceName.c_str(), ins->getOwner().toString().c_str()), true); if(has) ++amountOfCastles[ins->getOwner().getNum()]; } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index d6f700b6e..2d162a52c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1072,7 +1072,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo return false; } - logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.getStr(), hid.getNum(), h->pos.toString(), dst.toString()); + logGlobal->trace("Player %d (%s) wants to move hero %d from %s to %s", asker, asker.toString(), hid.getNum(), h->pos.toString(), dst.toString()); const int3 hmpos = h->convertToVisitablePos(dst); if (!gs->map->isInTheMap(hmpos)) @@ -3991,7 +3991,7 @@ bool CGameHandler::isBlockedByQueries(const CPack *pack, PlayerColor player) { complain(boost::str(boost::format( "\r\n| Player \"%s\" has to answer queries before attempting any further actions.\r\n| Top Query: \"%s\"\r\n") - % boost::to_upper_copy(player.getStr()) + % boost::to_upper_copy(player.toString()) % query->toString() )); return true; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index b9c9ae227..2e129098f 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -272,7 +272,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo { if(!player.isValidPlayer()) { - logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.getStr()); + logGlobal->error("Cannot pick hero for player %d. Wrong owner!", player.toString()); return nullptr; } @@ -285,7 +285,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo if(potentialClasses.empty()) { - logGlobal->error("There are no heroes available for player %s!", player.getStr()); + logGlobal->error("There are no heroes available for player %s!", player.toString()); return nullptr; } @@ -306,7 +306,7 @@ const CHeroClass * HeroPoolProcessor::pickClassFor(bool isNative, const PlayerCo if (possibleClasses.empty()) { - logGlobal->error("Cannot pick native hero for %s. Picking any...", player.getStr()); + logGlobal->error("Cannot pick native hero for %s. Picking any...", player.toString()); possibleClasses = potentialClasses; } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 26c94c835..6f5358c9c 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -92,7 +92,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st { for(auto & c : gameHandler->connections) { - if(c.first.getStr(false) == playername) + if(c.first.toString() == playername) playerToKick = c.first; } } @@ -113,7 +113,7 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st broadcastSystemMessage("No cheaters registered!"); for (auto const & entry : cheaters) - broadcastSystemMessage("Player " + entry.getStr() + " is cheater!"); + broadcastSystemMessage("Player " + entry.toString() + " is cheater!"); return true; } @@ -406,7 +406,7 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo if (words.front() == "ai" && i.second.human) continue; - if (words.front() != "all" && words.front() != i.first.getStr()) + if (words.front() != "all" && words.front() != i.first.toString()) continue; std::vector parameters = words; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index b8c1e072c..cc0025055 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -75,7 +75,7 @@ std::string CQuery::toString() const for(size_t i = 0; i < size; i++) { - names += boost::to_upper_copy(players[i].getStr()); + names += boost::to_upper_copy(players[i].toString()); if(i < size - 2) names += ", "; From 12c4f8d18c66534c32322cfd15c89d60af0a6db5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:21:57 +0300 Subject: [PATCH 0328/1248] Fixed serialization of PlayerColor in json --- client/gui/InterfaceObjectConfigurable.cpp | 2 +- lib/mapObjects/CGObjectInstance.cpp | 7 +------ lib/mapObjects/CQuest.cpp | 2 +- lib/modding/IdentifierStorage.cpp | 9 +++++---- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index dc97aba1a..a42ec6c07 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -238,7 +238,7 @@ PlayerColor InterfaceObjectConfigurable::readPlayerColor(const JsonNode & config { logGlobal->debug("Reading PlayerColor"); if(!config.isNull() && config.isString()) - return PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, config.String())); + return PlayerColor::decode(config.String()); logGlobal->debug("Unknown PlayerColor attribute"); return PlayerColor::CANNOT_DETERMINE; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 4b081c4e5..50e52f7b0 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -341,12 +341,7 @@ void CGObjectInstance::serializeJsonOptions(JsonSerializeFormat & handler) void CGObjectInstance::serializeJsonOwner(JsonSerializeFormat & handler) { - ui8 temp = tempOwner.getNum(); - - handler.serializeEnum("owner", temp, PlayerColor::NEUTRAL.getNum(), GameConstants::PLAYER_COLOR_NAMES); - - if(!handler.saving) - tempOwner = PlayerColor(temp); + handler.serializeId("owner", tempOwner, PlayerColor::NEUTRAL); } BattleField CGObjectInstance::getBattlefield() const diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 9e73793ca..22cfe4df6 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -534,7 +534,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi handler.serializeId("hero", m13489val, 0); break; case MISSION_PLAYER: - handler.serializeEnum("player", m13489val, PlayerColor::CANNOT_DETERMINE.getNum(), GameConstants::PLAYER_COLOR_NAMES); + handler.serializeId("player", m13489val, PlayerColor::NEUTRAL); break; default: logGlobal->error("Invalid quest mission type"); diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index b35ecba59..0cf1b9f22 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -31,9 +31,11 @@ CIdentifierStorage::CIdentifierStorage() registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) - { registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); - } + + for (int i = 0; i < std::size(GameConstants::PLAYER_COLOR_NAMES); ++i) + registerObject(ModScope::scopeBuiltin(), "playerColor", GameConstants::PLAYER_COLOR_NAMES[i], i); + for(int i=0; i CIdentifierStorage::getIdentifier(const std::string & scope, const std::string & type, const std::string & name, bool silent) const { - //TODO: RE-ENABLE - //assert(state != ELoadingState::LOADING); + assert(state != ELoadingState::LOADING); auto idList = getPossibleIdentifiers(ObjectCallback::fromNameAndType(scope, type, name, std::function(), silent)); From 9735fa6d06933a9fc2bc3f75d7ae8540de9189d8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:22:45 +0300 Subject: [PATCH 0329/1248] Fix regresssion - crash --- lib/rmg/CRmgTemplateStorage.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index 9e6423b9b..b8dfe8127 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -39,6 +39,7 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const { JsonDeserializer handler(nullptr, data); auto fullKey = scope + ":" + name; //actually it's not used + templates[fullKey] = std::make_unique(); templates[fullKey]->setId(fullKey); templates[fullKey]->serializeJson(handler); templates[fullKey]->setName(name); From b6a1a8f0da4095137016f6783e8bcc9562addd39 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:23:20 +0300 Subject: [PATCH 0330/1248] Json Serializer should now use identifers storage properly --- lib/constants/EntityIdentifiers.cpp | 61 +++++++++++++------ lib/constants/EntityIdentifiers.h | 57 ++++++++++-------- lib/rmg/CRmgTemplate.cpp | 49 ++++++--------- lib/serializer/JsonDeserializer.cpp | 14 +++++ lib/serializer/JsonDeserializer.h | 1 + lib/serializer/JsonSerializeFormat.h | 90 +++++++++++++++++----------- lib/serializer/JsonSerializer.cpp | 22 ++++++- lib/serializer/JsonSerializer.h | 1 + lib/serializer/JsonUpdater.cpp | 5 ++ lib/serializer/JsonUpdater.h | 1 + 10 files changed, 189 insertions(+), 112 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index bb1eaa497..1456e8a64 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -115,6 +115,11 @@ std::string HeroTypeID::encode(const si32 index) return VLC->heroTypes()->getByIndex(index)->getJsonKey(); } +std::string HeroTypeID::entityType() +{ + return "hero"; +} + const CArtifact * ArtifactIDBase::toArtifact() const { return dynamic_cast(toArtifact(VLC->artifacts())); @@ -125,7 +130,7 @@ const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) con return service->getByIndex(num); } -si32 ArtifactIDBase::decode(const std::string & identifier) +si32 ArtifactID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); if(rawId) @@ -134,11 +139,16 @@ si32 ArtifactIDBase::decode(const std::string & identifier) return -1; } -std::string ArtifactIDBase::encode(const si32 index) +std::string ArtifactID::encode(const si32 index) { return VLC->artifacts()->getByIndex(index)->getJsonKey(); } +std::string ArtifactID::entityType() +{ + return "artifact"; +} + const CCreature * CreatureIDBase::toCreature() const { return VLC->creh->objects.at(num); @@ -149,7 +159,7 @@ const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) c return creatures->getByIndex(num); } -si32 CreatureIDBase::decode(const std::string & identifier) +si32 CreatureID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); if(rawId) @@ -158,11 +168,16 @@ si32 CreatureIDBase::decode(const std::string & identifier) return -1; } -std::string CreatureIDBase::encode(const si32 index) +std::string CreatureID::encode(const si32 index) { return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); } +std::string CreatureID::entityType() +{ + return "creature"; +} + const CSpell * SpellIDBase::toSpell() const { if(num < 0 || num >= VLC->spellh->objects.size()) @@ -178,7 +193,7 @@ const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) cons return service->getByIndex(num); } -si32 SpellIDBase::decode(const std::string & identifier) +si32 SpellID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); if(rawId) @@ -187,11 +202,16 @@ si32 SpellIDBase::decode(const std::string & identifier) return -1; } -std::string SpellIDBase::encode(const si32 index) +std::string SpellID::encode(const si32 index) { return VLC->spells()->getByIndex(index)->getJsonKey(); } +std::string SpellID::entityType() +{ + return "spell"; +} + bool PlayerColor::isValidPlayer() const { return num >= 0 && num < PLAYER_LIMIT_I; @@ -199,15 +219,15 @@ bool PlayerColor::isValidPlayer() const bool PlayerColor::isSpectator() const { - return num == SPECTATOR.num; -} - -std::string PlayerColor::toString() const -{ - return encode(num); -} - -si32 PlayerColor::decode(const std::string & identifier) + return num == SPECTATOR.num; +} + +std::string PlayerColor::toString() const +{ + return encode(num); +} + +si32 PlayerColor::decode(const std::string & identifier) { return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); } @@ -217,6 +237,11 @@ std::string PlayerColor::encode(const si32 index) return GameConstants::PLAYER_COLOR_NAMES[index]; } +std::string PlayerColor::entityType() +{ + return "playerColor"; +} + si32 FactionID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); @@ -236,7 +261,7 @@ std::string FactionID::entityType() return "faction"; } -si32 TerrainIdBase::decode(const std::string & identifier) +si32 TerrainId::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); if(rawId) @@ -245,12 +270,12 @@ si32 TerrainIdBase::decode(const std::string & identifier) return static_cast(TerrainId::NONE); } -std::string TerrainIdBase::encode(const si32 index) +std::string TerrainId::encode(const si32 index) { return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); } -std::string TerrainIdBase::entityType() +std::string TerrainId::entityType() { return "terrain"; } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 6f242c564..587a91512 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -200,6 +200,7 @@ public: ///json serialization helpers static si32 decode(const std::string & identifier); static std::string encode(const si32 index); + static std::string entityType(); DLL_LINKAGE static const HeroTypeID NONE; }; @@ -236,16 +237,17 @@ public: static const PlayerColor NEUTRAL; //255 static const PlayerColor PLAYER_LIMIT; //player limit per map - bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - bool isSpectator() const; - - std::string toString() const; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); -}; - -class TeamID : public Identifier + bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + bool isSpectator() const; + + std::string toString() const; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class TeamID : public Identifier { public: using Identifier::Identifier; @@ -627,16 +629,17 @@ public: DLL_LINKAGE const CArtifact * toArtifact() const; DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); }; class ArtifactID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); }; class CreatureIDBase : public IdentifierBase @@ -666,16 +669,17 @@ public: DLL_LINKAGE const CCreature * toCreature() const; DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); }; class CreatureID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); }; class SpellIDBase : public IdentifierBase @@ -783,16 +787,17 @@ public: DLL_LINKAGE const CSpell * toSpell() const; //deprecated DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; - - ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); }; class SpellID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); }; class BattleFieldInfo; @@ -837,16 +842,16 @@ public: ROCK, ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK }; - - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); - static std::string entityType(); }; class TerrainId : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); }; class ObstacleInfo; diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 71e00fec7..3d96df035 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -65,35 +65,6 @@ void CTreasureInfo::serializeJson(JsonSerializeFormat & handler) namespace rmg { -//FIXME: This is never used, instead TerrainID is used -class TerrainEncoder -{ -public: - static si32 decode(const std::string & identifier) - { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "terrain", identifier); - } - - static std::string encode(const si32 index) - { - return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); - } -}; - -class ZoneEncoder -{ -public: - static si32 decode(const std::string & json) - { - return std::stoi(json); - } - - static std::string encode(si32 id) - { - return std::to_string(id); - } -}; - const TRmgTemplateZoneId ZoneOptions::NO_ZONE = -1; ZoneOptions::CTownInfo::CTownInfo() @@ -508,8 +479,24 @@ void ZoneConnection::serializeJson(JsonSerializeFormat & handler) "random" }; - handler.serializeId("a", zoneA, -1); - handler.serializeId("b", zoneB, -1); + if (handler.saving) + { + std::string zoneNameA = std::to_string(zoneA); + std::string zoneNameB = std::to_string(zoneB); + handler.serializeString("a", zoneNameA); + handler.serializeString("b", zoneNameB); + } + else + { + std::string zoneNameA; + std::string zoneNameB; + handler.serializeString("a", zoneNameA); + handler.serializeString("b", zoneNameB); + + zoneA = std::stoi(zoneNameA); + zoneB = std::stoi(zoneNameB); + } + handler.serializeInt("guard", guardStrength, 0); handler.serializeEnum("type", connectionType, connectionTypes); handler.serializeEnum("road", hasRoad, roadOptions); diff --git a/lib/serializer/JsonDeserializer.cpp b/lib/serializer/JsonDeserializer.cpp index 5cb7c1398..0c933aee2 100644 --- a/lib/serializer/JsonDeserializer.cpp +++ b/lib/serializer/JsonDeserializer.cpp @@ -107,6 +107,20 @@ void JsonDeserializer::serializeInternal(const std::string & fieldName, si32 & v value = rawValue; } +void JsonDeserializer::serializeInternal(const std::string & fieldName, std::vector & value) +{ + const JsonVector & data = currentObject->operator[](fieldName).Vector(); + + value.clear(); + value.reserve(data.size()); + + for(const JsonNode& elem : data) + { + std::string rawId = elem.String(); + value.push_back(rawId); + } +} + void JsonDeserializer::serializeInternal(std::string & value) { value = currentObject->String(); diff --git a/lib/serializer/JsonDeserializer.h b/lib/serializer/JsonDeserializer.h index e11892b27..9a4f52c7f 100644 --- a/lib/serializer/JsonDeserializer.h +++ b/lib/serializer/JsonDeserializer.h @@ -33,6 +33,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index 1d1ff4848..68533203a 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -11,6 +11,7 @@ #include "../JsonNode.h" #include "../modding/IdentifierStorage.h" +#include "../modding/ModScope.h" #include "../VCMI_Lib.h" VCMI_LIB_NAMESPACE_BEGIN @@ -271,36 +272,57 @@ public: template void serializeId(const std::string & fieldName, T & value, const U & defaultValue) { - doSerializeInternal(fieldName, value, defaultValue, &E::decode, &E::encode); + if (saving) + { + if (value != defaultValue) + { + std::string fieldValue = E::encode(value); + serializeString(fieldName, fieldValue); + } + } + else + { + std::string fieldValue; + serializeString(fieldName, fieldValue); + + if (!fieldValue.empty()) + { + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue, [&value](int32_t index){ + value = T(index); + }); + } + else + { + value = T(defaultValue); + } + } } ///si32-convertible identifier vector <-> Json array of string template void serializeIdArray(const std::string & fieldName, std::vector & value) { - std::vector temp; - - if(saving) + if (saving) { - temp.reserve(value.size()); + std::vector fieldValue; for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } + fieldValue.push_back(E::encode(vitem)); + + serializeInternal(fieldName, fieldValue); } - - serializeInternal(fieldName, temp, &E::decode, &E::encode); - if(!saving) + else { - value.clear(); - value.reserve(temp.size()); + std::vector fieldValue; + serializeInternal(fieldName, fieldValue); - for(const si32 item : temp) + value.resize(fieldValue.size()); + + for(size_t i = 0; i < fieldValue.size(); ++i) { - T vitem = static_cast(item); - value.push_back(vitem); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue[i], [&value, i](int32_t index){ + value[i] = T(index); + }); } } } @@ -309,28 +331,25 @@ public: template void serializeIdArray(const std::string & fieldName, std::set & value) { - std::vector temp; - - if(saving) + if (saving) { - temp.reserve(value.size()); + std::vector fieldValue; for(const T & vitem : value) - { - si32 item = static_cast(vitem); - temp.push_back(item); - } + fieldValue.push_back(U::encode(vitem)); + + serializeInternal(fieldName, fieldValue); } - - serializeInternal(fieldName, temp, &U::decode, &U::encode); - if(!saving) + else { - value.clear(); + std::vector fieldValue; + serializeInternal(fieldName, fieldValue); - for(const si32 item : temp) + for(size_t i = 0; i < fieldValue.size(); ++i) { - T vitem = static_cast(item); - value.insert(vitem); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), U::entityType(), fieldValue[i], [&value](int32_t index){ + value.insert(T(index)); + }); } } } @@ -340,9 +359,9 @@ public: void serializeInstance(const std::string & fieldName, T & value, const T & defaultValue) { const TDecoder decoder = std::bind(&IInstanceResolver::decode, instanceResolver, _1); - const TEncoder endoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); + const TEncoder encoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); - serializeId(fieldName, value, defaultValue, decoder, endoder); + serializeId(fieldName, value, defaultValue, decoder, encoder); } ///any serializable object <-> Json struct @@ -376,6 +395,9 @@ protected: ///Enum/Numeric <-> Json string enum virtual void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) = 0; + ///String vector <-> Json string vector + virtual void serializeInternal(const std::string & fieldName, std::vector & value) = 0; + virtual void pop() = 0; virtual void pushStruct(const std::string & fieldName) = 0; virtual void pushArray(const std::string & fieldName) = 0; diff --git a/lib/serializer/JsonSerializer.cpp b/lib/serializer/JsonSerializer.cpp index 4db9b5817..15a050d0e 100644 --- a/lib/serializer/JsonSerializer.cpp +++ b/lib/serializer/JsonSerializer.cpp @@ -63,9 +63,25 @@ void JsonSerializer::serializeInternal(const std::string & fieldName, std::vecto for(const si32 rawId : value) { - JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); - jsonElement.String() = encoder(rawId); - data.push_back(std::move(jsonElement)); + JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); + jsonElement.String() = encoder(rawId); + data.push_back(std::move(jsonElement)); + } +} + +void JsonSerializer::serializeInternal(const std::string & fieldName, std::vector & value) +{ + if(value.empty()) + return; + + JsonVector & data = currentObject->operator[](fieldName).Vector(); + data.reserve(value.size()); + + for(const auto & rawId : value) + { + JsonNode jsonElement(JsonNode::JsonType::DATA_STRING); + jsonElement.String() = rawId; + data.push_back(std::move(jsonElement)); } } diff --git a/lib/serializer/JsonSerializer.h b/lib/serializer/JsonSerializer.h index 18afa0909..e4fe9d87e 100644 --- a/lib/serializer/JsonSerializer.h +++ b/lib/serializer/JsonSerializer.h @@ -32,6 +32,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; diff --git a/lib/serializer/JsonUpdater.cpp b/lib/serializer/JsonUpdater.cpp index c0dce33a6..f80581f19 100644 --- a/lib/serializer/JsonUpdater.cpp +++ b/lib/serializer/JsonUpdater.cpp @@ -61,6 +61,11 @@ void JsonUpdater::serializeInternal(const std::string & fieldName, std::vector & value) +{ + // TODO +} + void JsonUpdater::serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) { const JsonNode & data = currentObject->operator[](fieldName); diff --git a/lib/serializer/JsonUpdater.h b/lib/serializer/JsonUpdater.h index 47e55ea74..b09fd6044 100644 --- a/lib/serializer/JsonUpdater.h +++ b/lib/serializer/JsonUpdater.h @@ -37,6 +37,7 @@ protected: void serializeInternal(const std::string & fieldName, double & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si64 & value, const std::optional & defaultValue) override; void serializeInternal(const std::string & fieldName, si32 & value, const std::optional & defaultValue, const std::vector & enumMap) override; + void serializeInternal(const std::string & fieldName, std::vector & value) override; void serializeInternal(std::string & value) override; void serializeInternal(int64_t & value) override; From 609f25f34456eedf9da3e0923d9a1819e97b921c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:43:16 +0300 Subject: [PATCH 0331/1248] Add sanity check --- lib/constants/EntityIdentifiers.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 1456e8a64..a9a5513dd 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -234,6 +234,12 @@ si32 PlayerColor::decode(const std::string & identifier) std::string PlayerColor::encode(const si32 index) { + if (index < 0 || index >= std::size(GameConstants::PLAYER_COLOR_NAMES)) + { + assert(0); + return "invalid"; + } + return GameConstants::PLAYER_COLOR_NAMES[index]; } From f30355839f7efea1dd14e8ed9520f1336f0896ef Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 4 Sep 2023 22:44:01 +0300 Subject: [PATCH 0332/1248] Fix build --- client/render/IRenderHandler.h | 2 ++ lib/filesystem/ResourcePath.h | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/render/IRenderHandler.h b/client/render/IRenderHandler.h index 5d0fed385..880295170 100644 --- a/client/render/IRenderHandler.h +++ b/client/render/IRenderHandler.h @@ -20,6 +20,8 @@ enum class EImageBlitMode; class IRenderHandler : public boost::noncopyable { public: + virtual ~IRenderHandler() = default; + /// Loads image using given path virtual std::shared_ptr loadImage(const ImagePath & path) = 0; virtual std::shared_ptr loadImage(const ImagePath & path, EImageBlitMode mode) = 0; diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index 0057d2338..baad69bbe 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -122,7 +122,7 @@ protected: }; template -class DLL_LINKAGE ResourcePathTempl : public ResourcePath +class ResourcePathTempl : public ResourcePath { template friend class ResourcePathTempl; From 9665ac33737988b1269d62c9d7469c215787126d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 5 Sep 2023 03:26:38 +0400 Subject: [PATCH 0333/1248] Refactoring map settings --- mapeditor/CMakeLists.txt | 23 +- mapeditor/mainwindow.cpp | 2 +- mapeditor/mapsettings.cpp | 1019 ------------------- mapeditor/mapsettings.h | 84 -- mapeditor/mapsettings.ui | 447 -------- mapeditor/mapsettings/abstractsettings.cpp | 138 +++ mapeditor/mapsettings/abstractsettings.h | 61 ++ mapeditor/mapsettings/generalsettings.cpp | 70 ++ mapeditor/mapsettings/generalsettings.h | 28 + mapeditor/mapsettings/generalsettings.ui | 118 +++ mapeditor/mapsettings/loseconditions.cpp | 262 +++++ mapeditor/mapsettings/loseconditions.h | 42 + mapeditor/mapsettings/loseconditions.ui | 69 ++ mapeditor/mapsettings/mapsettings.cpp | 119 +++ mapeditor/mapsettings/mapsettings.h | 36 + mapeditor/mapsettings/mapsettings.ui | 362 +++++++ mapeditor/mapsettings/modsettings.cpp | 154 +++ mapeditor/mapsettings/modsettings.h | 36 + mapeditor/mapsettings/modsettings.ui | 85 ++ mapeditor/mapsettings/victoryconditions.cpp | 426 ++++++++ mapeditor/mapsettings/victoryconditions.h | 33 + mapeditor/mapsettings/victoryconditions.ui | 79 ++ mapeditor/timedevent.cpp | 26 + mapeditor/timedevent.h | 27 + mapeditor/timedevent.ui | 134 +++ 25 files changed, 2326 insertions(+), 1554 deletions(-) delete mode 100644 mapeditor/mapsettings.cpp delete mode 100644 mapeditor/mapsettings.h delete mode 100644 mapeditor/mapsettings.ui create mode 100644 mapeditor/mapsettings/abstractsettings.cpp create mode 100644 mapeditor/mapsettings/abstractsettings.h create mode 100644 mapeditor/mapsettings/generalsettings.cpp create mode 100644 mapeditor/mapsettings/generalsettings.h create mode 100644 mapeditor/mapsettings/generalsettings.ui create mode 100644 mapeditor/mapsettings/loseconditions.cpp create mode 100644 mapeditor/mapsettings/loseconditions.h create mode 100644 mapeditor/mapsettings/loseconditions.ui create mode 100644 mapeditor/mapsettings/mapsettings.cpp create mode 100644 mapeditor/mapsettings/mapsettings.h create mode 100644 mapeditor/mapsettings/mapsettings.ui create mode 100644 mapeditor/mapsettings/modsettings.cpp create mode 100644 mapeditor/mapsettings/modsettings.h create mode 100644 mapeditor/mapsettings/modsettings.ui create mode 100644 mapeditor/mapsettings/victoryconditions.cpp create mode 100644 mapeditor/mapsettings/victoryconditions.h create mode 100644 mapeditor/mapsettings/victoryconditions.ui create mode 100644 mapeditor/timedevent.cpp create mode 100644 mapeditor/timedevent.h create mode 100644 mapeditor/timedevent.ui diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index ae8588ed9..2a6830c82 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -12,12 +12,18 @@ set(editor_SRCS generatorprogress.cpp mapview.cpp objectbrowser.cpp - mapsettings.cpp + mapsettings/abstractsettings.cpp + mapsettings/mapsettings.cpp + mapsettings/generalsettings.cpp + mapsettings/modsettings.cpp + mapsettings/victoryconditions.cpp + mapsettings/loseconditions.cpp playersettings.cpp playerparams.cpp scenelayer.cpp mapcontroller.cpp validator.cpp + timedevent.cpp inspector/inspector.cpp inspector/townbulidingswidget.cpp inspector/armywidget.cpp @@ -40,12 +46,18 @@ set(editor_HEADERS generatorprogress.h mapview.h objectbrowser.h - mapsettings.h + mapsettings/abstractsettings.h + mapsettings/mapsettings.h + mapsettings/generalsettings.h + mapsettings/modsettings.h + mapsettings/victoryconditions.h + mapsettings/loseconditions.h playersettings.h playerparams.h scenelayer.h mapcontroller.h validator.h + timedevent.h inspector/inspector.h inspector/townbulidingswidget.h inspector/armywidget.h @@ -59,10 +71,15 @@ set(editor_FORMS mainwindow.ui windownewmap.ui generatorprogress.ui - mapsettings.ui + mapsettings/mapsettings.ui + mapsettings/generalsettings.ui + mapsettings/modsettings.ui + mapsettings/victoryconditions.ui + mapsettings/loseconditions.ui playersettings.ui playerparams.ui validator.ui + timedevent.ui inspector/townbulidingswidget.ui inspector/armywidget.ui inspector/messagewidget.ui diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index bb99703e6..326b997ca 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -41,7 +41,7 @@ #include "windownewmap.h" #include "objectbrowser.h" #include "inspector/inspector.h" -#include "mapsettings.h" +#include "mapsettings/mapsettings.h" #include "playersettings.h" #include "validator.h" diff --git a/mapeditor/mapsettings.cpp b/mapeditor/mapsettings.cpp deleted file mode 100644 index 66239ff3c..000000000 --- a/mapeditor/mapsettings.cpp +++ /dev/null @@ -1,1019 +0,0 @@ -/* - * mapsettings.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 "mapsettings.h" -#include "ui_mapsettings.h" -#include "mainwindow.h" - -#include "../lib/CSkillHandler.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CArtHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGCreature.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/modding/CModHandler.h" -#include "../lib/modding/CModInfo.h" -#include "../lib/constants/StringConstants.h" -#include "inspector/townbulidingswidget.h" //to convert BuildingID to string - -//parses date for lose condition (1m 1w 1d) -int expiredDate(const QString & date) -{ - int result = 0; - for(auto component : date.split(" ")) - { - int days = component.left(component.lastIndexOf('d')).toInt(); - int weeks = component.left(component.lastIndexOf('w')).toInt(); - int months = component.left(component.lastIndexOf('m')).toInt(); - result += days > 0 ? days - 1 : 0; - result += (weeks > 0 ? weeks - 1 : 0) * 7; - result += (months > 0 ? months - 1 : 0) * 28; - } - return result; -} - -QString expiredDate(int date) -{ - QString result; - int m = date / 28; - int w = (date % 28) / 7; - int d = date % 7; - if(m) - result += QString::number(m) + "m"; - if(w) - { - if(!result.isEmpty()) - result += " "; - result += QString::number(w) + "w"; - } - if(d) - { - if(!result.isEmpty()) - result += " "; - result += QString::number(d) + "d"; - } - return result; -} - -int3 posFromJson(const JsonNode & json) -{ - return int3(json.Vector()[0].Integer(), json.Vector()[1].Integer(), json.Vector()[2].Integer()); -} - -std::vector linearJsonArray(const JsonNode & json) -{ - std::vector result; - if(json.getType() == JsonNode::JsonType::DATA_STRUCT) - result.push_back(json); - if(json.getType() == JsonNode::JsonType::DATA_VECTOR) - { - for(auto & node : json.Vector()) - { - auto subvector = linearJsonArray(node); - result.insert(result.end(), subvector.begin(), subvector.end()); - } - } - return result; -} - -void traverseNode(QTreeWidgetItem * item, std::function action) -{ - // Do something with item - action(item); - for (int i = 0; i < item->childCount(); ++i) - traverseNode(item->child(i), action); -} - -MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : - QDialog(parent), - ui(new Ui::MapSettings), - controller(ctrl) -{ - ui->setupUi(this); - - assert(controller.map()); - - ui->mapNameEdit->setText(tr(controller.map()->name.c_str())); - ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str())); - ui->heroLevelLimit->setValue(controller.map()->levelLimit); - ui->heroLevelLimitCheck->setChecked(controller.map()->levelLimit); - - show(); - - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); - ui->listAbilities->addItem(item); - } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedSpells[i] ? Qt::Checked : Qt::Unchecked); - ui->listSpells->addItem(item); - } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); - ui->listArts->addItem(item); - } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) - { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); - ui->listHeroes->addItem(item); - } - - //set difficulty - switch(controller.map()->difficulty) - { - case 0: - ui->diffRadio1->setChecked(true); - break; - - case 1: - ui->diffRadio2->setChecked(true); - break; - - case 2: - ui->diffRadio3->setChecked(true); - break; - - case 3: - ui->diffRadio4->setChecked(true); - break; - - case 4: - ui->diffRadio5->setChecked(true); - break; - }; - - //victory & loss messages - ui->victoryMessageEdit->setText(QString::fromStdString(controller.map()->victoryMessage.toString())); - ui->defeatMessageEdit->setText(QString::fromStdString(controller.map()->defeatMessage.toString())); - - //victory & loss conditions - const std::array conditionStringsWin = { - QT_TR_NOOP("No special victory"), - QT_TR_NOOP("Capture artifact"), - QT_TR_NOOP("Hire creatures"), - QT_TR_NOOP("Accumulate resources"), - QT_TR_NOOP("Construct building"), - QT_TR_NOOP("Capture town"), - QT_TR_NOOP("Defeat hero"), - QT_TR_NOOP("Transport artifact") - }; - const std::array conditionStringsLose = { - QT_TR_NOOP("No special loss"), - QT_TR_NOOP("Lose castle"), - QT_TR_NOOP("Lose hero"), - QT_TR_NOOP("Time expired"), - QT_TR_NOOP("Days without town") - }; - - for(auto & s : conditionStringsWin) - { - ui->victoryComboBox->addItem(QString::fromStdString(s)); - } - ui->standardVictoryCheck->setChecked(false); - ui->onlyForHumansCheck->setChecked(false); - - for(auto & s : conditionStringsLose) - { - ui->loseComboBox->addItem(QString::fromStdString(s)); - } - ui->standardLoseCheck->setChecked(false); - - auto conditionToJson = [](const EventCondition & event) -> JsonNode - { - JsonNode result; - result["condition"].Integer() = event.condition; - result["value"].Integer() = event.value; - result["objectType"].Integer() = event.objectType; - result["objectSubytype"].Integer() = event.objectSubtype; - result["objectInstanceName"].String() = event.objectInstanceName; - result["metaType"].Integer() = (ui8)event.metaType; - { - auto & position = result["position"].Vector(); - position.resize(3); - position[0].Float() = event.position.x; - position[1].Float() = event.position.y; - position[2].Float() = event.position.z; - } - return result; - }; - - for(auto & ev : controller.map()->triggeredEvents) - { - if(ev.effect.type == EventEffect::VICTORY) - { - if(ev.identifier == "standardVictory") - ui->standardVictoryCheck->setChecked(true); - - if(ev.identifier == "specialVictory") - { - auto readjson = ev.trigger.toJson(conditionToJson); - auto linearNodes = linearJsonArray(readjson); - - for(auto & json : linearNodes) - { - switch(json["condition"].Integer()) - { - case EventCondition::HAVE_ARTIFACT: { - ui->victoryComboBox->setCurrentIndex(1); - assert(victoryTypeWidget); - victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); - break; - } - - case EventCondition::HAVE_CREATURES: { - ui->victoryComboBox->setCurrentIndex(2); - assert(victoryTypeWidget); - assert(victoryValueWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - victoryValueWidget->setText(QString::number(json["value"].Integer())); - break; - } - - case EventCondition::HAVE_RESOURCES: { - ui->victoryComboBox->setCurrentIndex(3); - assert(victoryTypeWidget); - assert(victoryValueWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - victoryValueWidget->setText(QString::number(json["value"].Integer())); - break; - } - - case EventCondition::HAVE_BUILDING: { - ui->victoryComboBox->setCurrentIndex(4); - assert(victoryTypeWidget); - assert(victorySelectWidget); - auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); - victoryTypeWidget->setCurrentIndex(idx); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victorySelectWidget->findData(townIdx); - victorySelectWidget->setCurrentIndex(idx); - } - break; - } - - case EventCondition::CONTROL: { - ui->victoryComboBox->setCurrentIndex(5); - assert(victoryTypeWidget); - if(json["objectType"].Integer() == Obj::TOWN) - { - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victoryTypeWidget->findData(townIdx); - victoryTypeWidget->setCurrentIndex(idx); - } - } - //TODO: support control other objects (dwellings, mines) - break; - } - - case EventCondition::DESTROY: { - ui->victoryComboBox->setCurrentIndex(6); - assert(victoryTypeWidget); - if(json["objectType"].Integer() == Obj::HERO) - { - int heroIdx = getObjectByPos(posFromJson(json["position"])); - if(heroIdx >= 0) - { - auto idx = victoryTypeWidget->findData(heroIdx); - victoryTypeWidget->setCurrentIndex(idx); - } - } - //TODO: support control other objects (monsters) - break; - } - - case EventCondition::TRANSPORT: { - ui->victoryComboBox->setCurrentIndex(7); - assert(victoryTypeWidget); - assert(victorySelectWidget); - victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = victorySelectWidget->findData(townIdx); - victorySelectWidget->setCurrentIndex(idx); - } - break; - } - - case EventCondition::IS_HUMAN: { - ui->onlyForHumansCheck->setChecked(true); - break; - } - }; - } - } - } - - if(ev.effect.type == EventEffect::DEFEAT) - { - if(ev.identifier == "standardDefeat") - ui->standardLoseCheck->setChecked(true); - - if(ev.identifier == "specialDefeat") - { - auto readjson = ev.trigger.toJson(conditionToJson); - auto linearNodes = linearJsonArray(readjson); - - for(auto & json : linearNodes) - { - switch(json["condition"].Integer()) - { - case EventCondition::CONTROL: { - if(json["objectType"].Integer() == Obj::TOWN) - { - ui->loseComboBox->setCurrentIndex(1); - assert(loseTypeWidget); - int townIdx = getObjectByPos(posFromJson(json["position"])); - if(townIdx >= 0) - { - auto idx = loseTypeWidget->findData(townIdx); - loseTypeWidget->setCurrentIndex(idx); - } - } - if(json["objectType"].Integer() == Obj::HERO) - { - ui->loseComboBox->setCurrentIndex(2); - assert(loseTypeWidget); - int heroIdx = getObjectByPos(posFromJson(json["position"])); - if(heroIdx >= 0) - { - auto idx = loseTypeWidget->findData(heroIdx); - loseTypeWidget->setCurrentIndex(idx); - } - } - - break; - } - - case EventCondition::DAYS_PASSED: { - ui->loseComboBox->setCurrentIndex(3); - assert(loseValueWidget); - loseValueWidget->setText(expiredDate(json["value"].Integer())); - break; - } - - case EventCondition::DAYS_WITHOUT_TOWN: { - ui->loseComboBox->setCurrentIndex(4); - assert(loseValueWidget); - loseValueWidget->setText(QString::number(json["value"].Integer())); - break; - - case EventCondition::IS_HUMAN: - break; //ignore because always applicable for defeat conditions - } - - }; - } - } - } - } - - //mods management - //collect all active mods - QMap addedMods; - QSet modsToProcess; - ui->treeMods->blockSignals(true); - - auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) - { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); - item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, controller.map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); - //set parent check - if(parent && item->checkState(0) == Qt::Checked) - parent->setCheckState(0, Qt::Checked); - return item; - }; - - for(const auto & modName : VLC->modh->getActiveMods()) - { - QString qmodName = QString::fromStdString(modName); - if(qmodName.split(".").size() == 1) - { - const auto & modInfo = VLC->modh->getModInfo(modName); - addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo); - ui->treeMods->addTopLevelItem(addedMods[qmodName]); - } - else - { - modsToProcess.insert(qmodName); - } - } - - for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();) - { - auto qmodName = *qmodIter; - auto pieces = qmodName.split("."); - assert(pieces.size() > 1); - - QString qs; - for(int i = 0; i < pieces.size() - 1; ++i) - qs += pieces[i]; - - if(addedMods.count(qs)) - { - const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString()); - addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo); - modsToProcess.erase(qmodIter); - qmodIter = modsToProcess.begin(); - } - else - ++qmodIter; - } - ui->treeMods->blockSignals(false); -} - -MapSettings::~MapSettings() -{ - delete ui; -} - -std::string MapSettings::getTownName(int townObjectIdx) -{ - std::string name; - if(auto town = dynamic_cast(controller.map()->objects[townObjectIdx].get())) - { - auto * ctown = town->town; - if(!ctown) - ctown = VLC->townh->randomTown; - - name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; - } - return name; -} - -std::string MapSettings::getHeroName(int townObjectIdx) -{ - std::string name; - if(auto hero = dynamic_cast(controller.map()->objects[townObjectIdx].get())) - { - name = hero->getNameTranslated(); - } - return name; -} - -std::string MapSettings::getMonsterName(int monsterObjectIdx) -{ - std::string name; - [[maybe_unused]] auto monster = dynamic_cast(controller.map()->objects[monsterObjectIdx].get()); - if(monster) - { - //TODO: get proper name - //name = hero->name; - } - return name; -} - -void MapSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) -{ - //Mod management - auto widgetAction = [&](QTreeWidgetItem * item) - { - auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked); - }; - - for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) - { - QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); - traverseNode(item, widgetAction); - } -} - -void MapSettings::on_pushButton_clicked() -{ - controller.map()->name = ui->mapNameEdit->text().toStdString(); - controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); - if(ui->heroLevelLimitCheck->isChecked()) - controller.map()->levelLimit = ui->heroLevelLimit->value(); - else - controller.map()->levelLimit = 0; - controller.commitChangeWithoutRedraw(); - - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) - { - auto * item = ui->listAbilities->item(i); - controller.map()->allowedAbilities[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) - { - auto * item = ui->listSpells->item(i); - controller.map()->allowedSpells[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - { - auto * item = ui->listArts->item(i); - controller.map()->allowedArtifact[i] = item->checkState() == Qt::Checked; - } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) - { - auto * item = ui->listHeroes->item(i); - controller.map()->allowedHeroes[i] = item->checkState() == Qt::Checked; - } - - //set difficulty - if(ui->diffRadio1->isChecked()) controller.map()->difficulty = 0; - if(ui->diffRadio2->isChecked()) controller.map()->difficulty = 1; - if(ui->diffRadio3->isChecked()) controller.map()->difficulty = 2; - if(ui->diffRadio4->isChecked()) controller.map()->difficulty = 3; - if(ui->diffRadio5->isChecked()) controller.map()->difficulty = 4; - - //victory & loss messages - - controller.map()->victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); - controller.map()->defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); - - //victory & loss conditions - EventCondition victoryCondition(EventCondition::STANDARD_WIN); - EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); - defeatCondition.value = 7; - - //Victory condition - defeat all - TriggeredEvent standardVictory; - standardVictory.effect.type = EventEffect::VICTORY; - standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - standardVictory.identifier = "standardVictory"; - standardVictory.description.clear(); // TODO: display in quest window - standardVictory.onFulfill.appendTextID("core.genrltxt.659"); - standardVictory.trigger = EventExpression(victoryCondition); - - //Loss condition - 7 days without town - TriggeredEvent standardDefeat; - standardDefeat.effect.type = EventEffect::DEFEAT; - standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); - standardDefeat.identifier = "standardDefeat"; - standardDefeat.description.clear(); // TODO: display in quest window - standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); - standardDefeat.trigger = EventExpression(defeatCondition); - - controller.map()->triggeredEvents.clear(); - - //VICTORY - if(ui->victoryComboBox->currentIndex() == 0) - { - controller.map()->triggeredEvents.push_back(standardVictory); - controller.map()->victoryIconIndex = 11; - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); - } - else - { - int vicCondition = ui->victoryComboBox->currentIndex() - 1; - - TriggeredEvent specialVictory; - specialVictory.effect.type = EventEffect::VICTORY; - specialVictory.identifier = "specialVictory"; - specialVictory.description.clear(); // TODO: display in quest window - - controller.map()->victoryIconIndex = vicCondition; - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1]); - - switch(vicCondition) - { - case 0: { - EventCondition cond(EventCondition::HAVE_ARTIFACT); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); - specialVictory.onFulfill.appendTextID("core.genrltxt.280"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 1: { - EventCondition cond(EventCondition::HAVE_CREATURES); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - cond.value = victoryValueWidget->text().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); - specialVictory.onFulfill.appendTextID("core.genrltxt.276"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 2: { - EventCondition cond(EventCondition::HAVE_RESOURCES); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - cond.value = victoryValueWidget->text().toInt(); - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); - specialVictory.onFulfill.appendTextID("core.genrltxt.278"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 3: { - EventCondition cond(EventCondition::HAVE_BUILDING); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - int townIdx = victorySelectWidget->currentData().toInt(); - if(townIdx > -1) - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); - specialVictory.onFulfill.appendTextID("core.genrltxt.282"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 4: { - EventCondition cond(EventCondition::CONTROL); - assert(victoryTypeWidget); - cond.objectType = Obj::TOWN; - int townIdx = victoryTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); - specialVictory.onFulfill.appendTextID("core.genrltxt.249"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 5: { - EventCondition cond(EventCondition::DESTROY); - assert(victoryTypeWidget); - cond.objectType = Obj::HERO; - int heroIdx = victoryTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[heroIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); - specialVictory.onFulfill.appendTextID("core.genrltxt.252"); - specialVictory.trigger = EventExpression(cond); - break; - } - - case 6: { - EventCondition cond(EventCondition::TRANSPORT); - assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); - int townIdx = victorySelectWidget->currentData().toInt(); - if(townIdx > -1) - cond.position = controller.map()->objects[townIdx]->pos; - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); - specialVictory.onFulfill.appendTextID("core.genrltxt.292"); - specialVictory.trigger = EventExpression(cond); - break; - } - - } - - // if condition is human-only turn it into following construction: AllOf(human, condition) - if(ui->onlyForHumansCheck->isChecked()) - { - EventExpression::OperatorAll oper; - EventCondition notAI(EventCondition::IS_HUMAN); - notAI.value = 1; - oper.expressions.push_back(notAI); - oper.expressions.push_back(specialVictory.trigger.get()); - specialVictory.trigger = EventExpression(oper); - } - - // if normal victory allowed - add one more quest - if(ui->standardVictoryCheck->isChecked()) - { - controller.map()->victoryMessage.appendRawString(" / "); - controller.map()->victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); - controller.map()->triggeredEvents.push_back(standardVictory); - } - controller.map()->triggeredEvents.push_back(specialVictory); - } - - //DEFEAT - if(ui->loseComboBox->currentIndex() == 0) - { - controller.map()->triggeredEvents.push_back(standardDefeat); - controller.map()->defeatIconIndex = 3; - controller.map()->defeatMessage.appendTextID("core.lcdesc.0"); - } - else - { - int lossCondition = ui->loseComboBox->currentIndex() - 1; - - TriggeredEvent specialDefeat; - specialDefeat.effect.type = EventEffect::DEFEAT; - specialDefeat.identifier = "specialDefeat"; - specialDefeat.description.clear(); // TODO: display in quest window - - controller.map()->defeatIconIndex = lossCondition; - - switch(lossCondition) - { - case 0: { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - assert(loseTypeWidget); - int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - noneOf.expressions.push_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); - specialDefeat.trigger = EventExpression(noneOf); - controller.map()->defeatMessage.appendTextID("core.lcdesc.1"); - break; - } - - case 1: { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; - assert(loseTypeWidget); - int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = controller.map()->objects[townIdx]->pos; - noneOf.expressions.push_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); - specialDefeat.trigger = EventExpression(noneOf); - controller.map()->defeatMessage.appendTextID("core.lcdesc.2"); - break; - } - - case 2: { - EventCondition cond(EventCondition::DAYS_PASSED); - assert(loseValueWidget); - cond.value = expiredDate(loseValueWidget->text()); - specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); - specialDefeat.trigger = EventExpression(cond); - controller.map()->defeatMessage.appendTextID("core.lcdesc.3"); - break; - } - - case 3: { - EventCondition cond(EventCondition::DAYS_WITHOUT_TOWN); - assert(loseValueWidget); - cond.value = loseValueWidget->text().toInt(); - specialDefeat.onFulfill.appendTextID("core.genrltxt.7"); - specialDefeat.trigger = EventExpression(cond); - break; - } - } - - EventExpression::OperatorAll allOf; - EventCondition isHuman(EventCondition::IS_HUMAN); - isHuman.value = 1; - - allOf.expressions.push_back(specialDefeat.trigger.get()); - allOf.expressions.push_back(isHuman); - specialDefeat.trigger = EventExpression(allOf); - - if(ui->standardLoseCheck->isChecked()) - { - controller.map()->triggeredEvents.push_back(standardDefeat); - } - controller.map()->triggeredEvents.push_back(specialDefeat); - } - - //Mod management - auto widgetAction = [&](QTreeWidgetItem * item) - { - if(item->checkState(0) == Qt::Checked) - { - auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - controller.map()->mods[modName] = VLC->modh->getModInfo(modName).version; - } - }; - - controller.map()->mods.clear(); - for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) - { - QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); - traverseNode(item, widgetAction); - } - - close(); -} - -void MapSettings::on_victoryComboBox_currentIndexChanged(int index) -{ - delete victoryTypeWidget; - delete victoryValueWidget; - delete victorySelectWidget; - victoryTypeWidget = nullptr; - victoryValueWidget = nullptr; - victorySelectWidget = nullptr; - - if(index == 0) - { - ui->standardVictoryCheck->setChecked(true); - ui->standardVictoryCheck->setEnabled(false); - ui->onlyForHumansCheck->setChecked(false); - ui->onlyForHumansCheck->setEnabled(false); - return; - } - ui->onlyForHumansCheck->setEnabled(true); - ui->standardVictoryCheck->setEnabled(true); - - int vicCondition = index - 1; - switch(vicCondition) - { - case 0: { //EventCondition::HAVE_ARTIFACT - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); - break; - } - - case 1: { //EventCondition::HAVE_CREATURES - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < VLC->creh->objects.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->creh->objects[i]->getNamePluralTranslated()), QVariant::fromValue(i)); - - victoryValueWidget = new QLineEdit; - ui->victoryParamsLayout->addWidget(victoryValueWidget); - victoryValueWidget->setText("1"); - break; - } - - case 2: { //EventCondition::HAVE_RESOURCES - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - { - for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType) - { - auto resName = QString::fromStdString(GameConstants::RESOURCE_NAMES[resType]); - victoryTypeWidget->addItem(resName, QVariant::fromValue(resType)); - } - } - - victoryValueWidget = new QLineEdit; - ui->victoryParamsLayout->addWidget(victoryValueWidget); - victoryValueWidget->setText("1"); - break; - } - - case 3: { //EventCondition::HAVE_BUILDING - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - auto * ctown = VLC->townh->randomTown; - for(int bId : ctown->getAllBuildings()) - victoryTypeWidget->addItem(QString::fromStdString(defaultBuildingIdConversion(BuildingID(bId))), QVariant::fromValue(bId)); - - victorySelectWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victorySelectWidget); - victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); - for(int i : getObjectIndexes()) - victorySelectWidget->addItem(getTownName(i).c_str(), QVariant::fromValue(i)); - break; - } - - case 4: { //EventCondition::CONTROL (Obj::TOWN) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 5: { //EventCondition::DESTROY (Obj::HERO) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getHeroName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) - victoryTypeWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) - victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); - - victorySelectWidget = new QComboBox; - ui->victoryParamsLayout->addWidget(victorySelectWidget); - for(int i : getObjectIndexes()) - victorySelectWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - - //TODO: support this vectory type - // in order to do that, need to implement finding creature by position - // selecting from map would be the best user experience - /*case 7: { //EventCondition::DESTROY (Obj::MONSTER) - victoryTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes()) - victoryTypeWidget->addItem(tr(getMonsterName(i).c_str()), QVariant::fromValue(i)); - break; - }*/ - - - } -} - - -void MapSettings::on_loseComboBox_currentIndexChanged(int index) -{ - delete loseTypeWidget; - delete loseValueWidget; - delete loseSelectWidget; - loseTypeWidget = nullptr; - loseValueWidget = nullptr; - loseSelectWidget = nullptr; - - if(index == 0) - { - ui->standardLoseCheck->setChecked(true); - ui->standardLoseCheck->setEnabled(false); - return; - } - ui->standardLoseCheck->setEnabled(true); - - int loseCondition = index - 1; - switch(loseCondition) - { - case 0: { //EventCondition::CONTROL (Obj::TOWN) - loseTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes()) - loseTypeWidget->addItem(tr(getTownName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 1: { //EventCondition::CONTROL (Obj::HERO) - loseTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes()) - loseTypeWidget->addItem(tr(getHeroName(i).c_str()), QVariant::fromValue(i)); - break; - } - - case 2: { //EventCondition::DAYS_PASSED - loseValueWidget = new QLineEdit; - ui->loseParamsLayout->addWidget(loseValueWidget); - loseValueWidget->setText("2m 1w 1d"); - break; - } - - case 3: { //EventCondition::DAYS_WITHOUT_TOWN - loseValueWidget = new QLineEdit; - ui->loseParamsLayout->addWidget(loseValueWidget); - loseValueWidget->setText("7"); - break; - } - } -} - - -void MapSettings::on_heroLevelLimitCheck_toggled(bool checked) -{ - ui->heroLevelLimit->setEnabled(checked); -} - -void MapSettings::on_modResolution_map_clicked() -{ - updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller.map())); -} - - -void MapSettings::on_modResolution_full_clicked() -{ - updateModWidgetBasedOnMods(MapController::modAssessmentAll()); -} - -void MapSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column) -{ - //set state for children - for (int i = 0; i < item->childCount(); ++i) - item->child(i)->setCheckState(0, item->checkState(0)); - - //set state for parent - ui->treeMods->blockSignals(true); - if(item->checkState(0) == Qt::Checked) - { - while(item->parent()) - { - item->parent()->setCheckState(0, Qt::Checked); - item = item->parent(); - } - } - ui->treeMods->blockSignals(false); -} - diff --git a/mapeditor/mapsettings.h b/mapeditor/mapsettings.h deleted file mode 100644 index faf074043..000000000 --- a/mapeditor/mapsettings.h +++ /dev/null @@ -1,84 +0,0 @@ -/* - * mapsettings.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 -#include "mapcontroller.h" -#include "../lib/mapping/CMap.h" - -namespace Ui { -class MapSettings; -} - -class MapSettings : public QDialog -{ - Q_OBJECT - -public: - explicit MapSettings(MapController & controller, QWidget *parent = nullptr); - ~MapSettings(); - -private slots: - void on_pushButton_clicked(); - - void on_victoryComboBox_currentIndexChanged(int index); - - void on_loseComboBox_currentIndexChanged(int index); - - void on_heroLevelLimitCheck_toggled(bool checked); - - void on_modResolution_map_clicked(); - - void on_modResolution_full_clicked(); - - void on_treeMods_itemChanged(QTreeWidgetItem *item, int column); - -private: - - std::string getTownName(int townObjectIdx); - std::string getHeroName(int townObjectIdx); - std::string getMonsterName(int townObjectIdx); - - void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods); - - template - std::vector getObjectIndexes() const - { - std::vector result; - for(int i = 0; i < controller.map()->objects.size(); ++i) - { - if(auto town = dynamic_cast(controller.map()->objects[i].get())) - result.push_back(i); - } - return result; - } - - template - int getObjectByPos(const int3 & pos) - { - for(int i = 0; i < controller.map()->objects.size(); ++i) - { - if(auto town = dynamic_cast(controller.map()->objects[i].get())) - { - if(town->pos == pos) - return i; - } - } - return -1; - } - - Ui::MapSettings *ui; - MapController & controller; - - QComboBox * victoryTypeWidget = nullptr, * loseTypeWidget = nullptr; - QComboBox * victorySelectWidget = nullptr, * loseSelectWidget = nullptr; - QLineEdit * victoryValueWidget = nullptr, * loseValueWidget = nullptr; -}; diff --git a/mapeditor/mapsettings.ui b/mapeditor/mapsettings.ui deleted file mode 100644 index 5a314b917..000000000 --- a/mapeditor/mapsettings.ui +++ /dev/null @@ -1,447 +0,0 @@ - - - MapSettings - - - Qt::ApplicationModal - - - - 0 - 0 - 543 - 494 - - - - - 0 - 0 - - - - Map settings - - - - - - 0 - - - - General - - - - - - Map name - - - - - - - - - - Map description - - - - - - - - - - 10 - - - - - false - - - - 48 - 0 - - - - - - - - - 0 - 0 - - - - Limit maximum heroes level - - - - - - - - - Difficulty - - - - - - 1 - - - - - - - 2 - - - - - - - 3 - - - - - - - 4 - - - - - - - 5 - - - - - - - - - - - Mods - - - - - - Mandatory mods for playing this map - - - - - - - QAbstractScrollArea::AdjustIgnored - - - 320 - - - - Mod name - - - - - Version - - - - - - - - - - Automatic assignment - - - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - Map objects mods - - - false - - - - - - - Set all mods having a game content as mandatory - - - Full content mods - - - false - - - - - - - - - - Events - - - - - - 1 - - - - Victory - - - - - - 0 - - - 0 - - - - - Victory message - - - - - - - - - - - - - - - Only for human players - - - - - - - Allow standard victory - - - - - - - - 0 - 0 - - - - Parameters - - - - 12 - - - - - - - - - - - - Loss - - - - - - - - - 7 days without town - - - - - - - Defeat message - - - - - - - - - - - 0 - 0 - - - - Parameters - - - - - - - - - - - - - - - - - Abilities - - - - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Adjust - - - QListView::Batched - - - 30 - - - - - - - - Spells - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - Artifacts - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - Heroes - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::ScrollPerItem - - - true - - - QListView::Batched - - - 30 - - - - - - - - - - - Ok - - - - - - - - diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp new file mode 100644 index 000000000..3a1127a54 --- /dev/null +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -0,0 +1,138 @@ +/* + * abstractsettings.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 "abstractsettings.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGCreature.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CGCreature.h" + +//parses date for lose condition (1m 1w 1d) +int expiredDate(const QString & date) +{ + int result = 0; + for(auto component : date.split(" ")) + { + int days = component.left(component.lastIndexOf('d')).toInt(); + int weeks = component.left(component.lastIndexOf('w')).toInt(); + int months = component.left(component.lastIndexOf('m')).toInt(); + result += days > 0 ? days - 1 : 0; + result += (weeks > 0 ? weeks - 1 : 0) * 7; + result += (months > 0 ? months - 1 : 0) * 28; + } + return result; +} + +QString expiredDate(int date) +{ + QString result; + int m = date / 28; + int w = (date % 28) / 7; + int d = date % 7; + if(m) + result += QString::number(m) + "m"; + if(w) + { + if(!result.isEmpty()) + result += " "; + result += QString::number(w) + "w"; + } + if(d) + { + if(!result.isEmpty()) + result += " "; + result += QString::number(d) + "d"; + } + return result; +} + +int3 posFromJson(const JsonNode & json) +{ + return int3(json.Vector()[0].Integer(), json.Vector()[1].Integer(), json.Vector()[2].Integer()); +} + +std::vector linearJsonArray(const JsonNode & json) +{ + std::vector result; + if(json.getType() == JsonNode::JsonType::DATA_STRUCT) + result.push_back(json); + if(json.getType() == JsonNode::JsonType::DATA_VECTOR) + { + for(auto & node : json.Vector()) + { + auto subvector = linearJsonArray(node); + result.insert(result.end(), subvector.begin(), subvector.end()); + } + } + return result; +} + +AbstractSettings::AbstractSettings(QWidget *parent) + : QWidget{parent} +{ + +} + +std::string AbstractSettings::getTownName(const CMap & map, int objectIdx) +{ + std::string name; + if(auto town = dynamic_cast(map.objects[objectIdx].get())) + { + auto * ctown = town->town; + if(!ctown) + ctown = VLC->townh->randomTown; + + name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; + } + return name; +} + +std::string AbstractSettings::getHeroName(const CMap & map, int objectIdx) +{ + std::string name; + if(auto hero = dynamic_cast(map.objects[objectIdx].get())) + { + name = hero->getNameTranslated(); + } + return name; +} + +std::string AbstractSettings::getMonsterName(const CMap & map, int objectIdx) +{ + std::string name; + [[maybe_unused]] auto monster = dynamic_cast(map.objects[objectIdx].get()); + if(monster) + { + //TODO: get proper name + //name = hero->name; + } + return name; +} + +JsonNode AbstractSettings::conditionToJson(const EventCondition & event) +{ + JsonNode result; + result["condition"].Integer() = event.condition; + result["value"].Integer() = event.value; + result["objectType"].Integer() = event.objectType; + result["objectSubytype"].Integer() = event.objectSubtype; + result["objectInstanceName"].String() = event.objectInstanceName; + result["metaType"].Integer() = (ui8)event.metaType; + { + auto & position = result["position"].Vector(); + position.resize(3); + position[0].Float() = event.position.x; + position[1].Float() = event.position.y; + position[2].Float() = event.position.z; + } + return result; +}; diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h new file mode 100644 index 000000000..2e59f47b9 --- /dev/null +++ b/mapeditor/mapsettings/abstractsettings.h @@ -0,0 +1,61 @@ +#ifndef ABSTRACTSETTINGS_H +#define ABSTRACTSETTINGS_H + +#include +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +//parses date for lose condition (1m 1w 1d) +int expiredDate(const QString & date); +QString expiredDate(int date); +int3 posFromJson(const JsonNode & json); +std::vector linearJsonArray(const JsonNode & json); + +class AbstractSettings : public QWidget +{ + Q_OBJECT +public: + explicit AbstractSettings(QWidget *parent = nullptr); + virtual ~AbstractSettings() = default; + + virtual void initialize(const CMap & map) = 0; + virtual void update(CMap & map) = 0; + + std::string getTownName(const CMap & map, int objectIdx); + std::string getHeroName(const CMap & map, int objectIdx); + std::string getMonsterName(const CMap & map, int objectIdx); + + static JsonNode conditionToJson(const EventCondition & event); + + template + std::vector getObjectIndexes(const CMap & map) const + { + std::vector result; + for(int i = 0; i < map.objects.size(); ++i) + { + if(auto obj = dynamic_cast(map.objects[i].get())) + result.push_back(i); + } + return result; + } + + template + int getObjectByPos(const CMap & map, const int3 & pos) + { + for(int i = 0; i < map.objects.size(); ++i) + { + if(auto obj = dynamic_cast(map.objects[i].get())) + { + if(obj->pos == pos) + return i; + } + } + return -1; + } + +signals: + +}; + +#endif // ABSTRACTSETTINGS_H diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp new file mode 100644 index 000000000..db1280eab --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -0,0 +1,70 @@ +#include "StdInc.h" +#include "generalsettings.h" +#include "ui_generalsettings.h" + +GeneralSettings::GeneralSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::GeneralSettings) +{ + ui->setupUi(this); +} + +GeneralSettings::~GeneralSettings() +{ + delete ui; +} + +void GeneralSettings::initialize(const CMap & map) +{ + ui->mapNameEdit->setText(tr(map.name.c_str())); + ui->mapDescriptionEdit->setPlainText(tr(map.description.c_str())); + ui->heroLevelLimit->setValue(map.levelLimit); + ui->heroLevelLimitCheck->setChecked(map.levelLimit); + + //set difficulty + switch(map.difficulty) + { + case 0: + ui->diffRadio1->setChecked(true); + break; + + case 1: + ui->diffRadio2->setChecked(true); + break; + + case 2: + ui->diffRadio3->setChecked(true); + break; + + case 3: + ui->diffRadio4->setChecked(true); + break; + + case 4: + ui->diffRadio5->setChecked(true); + break; + }; +} + +void GeneralSettings::update(CMap & map) +{ + map.name = ui->mapNameEdit->text().toStdString(); + map.description = ui->mapDescriptionEdit->toPlainText().toStdString(); + if(ui->heroLevelLimitCheck->isChecked()) + map.levelLimit = ui->heroLevelLimit->value(); + else + map.levelLimit = 0; + + //set difficulty + if(ui->diffRadio1->isChecked()) map.difficulty = 0; + if(ui->diffRadio2->isChecked()) map.difficulty = 1; + if(ui->diffRadio3->isChecked()) map.difficulty = 2; + if(ui->diffRadio4->isChecked()) map.difficulty = 3; + if(ui->diffRadio5->isChecked()) map.difficulty = 4; +} + +void GeneralSettings::on_heroLevelLimitCheck_toggled(bool checked) +{ + ui->heroLevelLimit->setEnabled(checked); +} + diff --git a/mapeditor/mapsettings/generalsettings.h b/mapeditor/mapsettings/generalsettings.h new file mode 100644 index 000000000..d5c56a270 --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.h @@ -0,0 +1,28 @@ +#ifndef GENERALSETTINGS_H +#define GENERALSETTINGS_H + +#include "abstractsettings.h" + +namespace Ui { +class GeneralSettings; +} + +class GeneralSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit GeneralSettings(QWidget *parent = nullptr); + ~GeneralSettings(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_heroLevelLimitCheck_toggled(bool checked); + +private: + Ui::GeneralSettings *ui; +}; + +#endif // GENERALSETTINGS_H diff --git a/mapeditor/mapsettings/generalsettings.ui b/mapeditor/mapsettings/generalsettings.ui new file mode 100644 index 000000000..b4b4e6626 --- /dev/null +++ b/mapeditor/mapsettings/generalsettings.ui @@ -0,0 +1,118 @@ + + + GeneralSettings + + + + 0 + 0 + 651 + 481 + + + + Form + + + + + + Map name + + + + + + + + + + Map description + + + + + + + + + + 0 + + + + + false + + + + 48 + 0 + + + + + + + + + 0 + 0 + + + + Limit maximum heroes level + + + + + + + + + Difficulty + + + + + + 1 + + + + + + + 2 + + + + + + + 3 + + + + + + + 4 + + + + + + + 5 + + + + + + + + + + + diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp new file mode 100644 index 000000000..cf47c7776 --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -0,0 +1,262 @@ +#include "StdInc.h" +#include "loseconditions.h" +#include "ui_loseconditions.h" + +#include "../lib/CGeneralTextHandler.h" + +LoseConditions::LoseConditions(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::LoseConditions) +{ + ui->setupUi(this); +} + +LoseConditions::~LoseConditions() +{ + delete ui; +} + +void LoseConditions::initialize(const CMap & map) +{ + mapPointer = ↦ + + //loss messages + ui->defeatMessageEdit->setText(QString::fromStdString(map.defeatMessage.toString())); + + //loss conditions + const std::array conditionStringsLose = { + QT_TR_NOOP("No special loss"), + QT_TR_NOOP("Lose castle"), + QT_TR_NOOP("Lose hero"), + QT_TR_NOOP("Time expired"), + QT_TR_NOOP("Days without town") + }; + + for(auto & s : conditionStringsLose) + { + ui->loseComboBox->addItem(QString::fromStdString(s)); + } + ui->standardLoseCheck->setChecked(false); + + for(auto & ev : map.triggeredEvents) + { + if(ev.effect.type == EventEffect::DEFEAT) + { + if(ev.identifier == "standardDefeat") + ui->standardLoseCheck->setChecked(true); + + if(ev.identifier == "specialDefeat") + { + auto readjson = ev.trigger.toJson(AbstractSettings::conditionToJson); + auto linearNodes = linearJsonArray(readjson); + + for(auto & json : linearNodes) + { + switch(json["condition"].Integer()) + { + case EventCondition::CONTROL: { + if(json["objectType"].Integer() == Obj::TOWN) + { + ui->loseComboBox->setCurrentIndex(1); + assert(loseTypeWidget); + int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = loseTypeWidget->findData(townIdx); + loseTypeWidget->setCurrentIndex(idx); + } + } + if(json["objectType"].Integer() == Obj::HERO) + { + ui->loseComboBox->setCurrentIndex(2); + assert(loseTypeWidget); + int heroIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(heroIdx >= 0) + { + auto idx = loseTypeWidget->findData(heroIdx); + loseTypeWidget->setCurrentIndex(idx); + } + } + + break; + } + + case EventCondition::DAYS_PASSED: { + ui->loseComboBox->setCurrentIndex(3); + assert(loseValueWidget); + loseValueWidget->setText(expiredDate(json["value"].Integer())); + break; + } + + case EventCondition::DAYS_WITHOUT_TOWN: { + ui->loseComboBox->setCurrentIndex(4); + assert(loseValueWidget); + loseValueWidget->setText(QString::number(json["value"].Integer())); + break; + + case EventCondition::IS_HUMAN: + break; //ignore because always applicable for defeat conditions + } + + }; + } + } + } + } +} + +void LoseConditions::update(CMap & map) +{ + //loss messages + map.defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); + + //loss conditions + EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); + defeatCondition.value = 7; + + //Loss condition - 7 days without town + TriggeredEvent standardDefeat; + standardDefeat.effect.type = EventEffect::DEFEAT; + standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); + standardDefeat.identifier = "standardDefeat"; + standardDefeat.description.clear(); // TODO: display in quest window + standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); + standardDefeat.trigger = EventExpression(defeatCondition); + + //DEFEAT + if(ui->loseComboBox->currentIndex() == 0) + { + map.triggeredEvents.push_back(standardDefeat); + map.defeatIconIndex = 3; + map.defeatMessage.appendTextID("core.lcdesc.0"); + } + else + { + int lossCondition = ui->loseComboBox->currentIndex() - 1; + + TriggeredEvent specialDefeat; + specialDefeat.effect.type = EventEffect::DEFEAT; + specialDefeat.identifier = "specialDefeat"; + specialDefeat.description.clear(); // TODO: display in quest window + + map.defeatIconIndex = lossCondition; + + switch(lossCondition) + { + case 0: { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::TOWN; + assert(loseTypeWidget); + int townIdx = loseTypeWidget->currentData().toInt(); + cond.position = map.objects[townIdx]->pos; + noneOf.expressions.push_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); + specialDefeat.trigger = EventExpression(noneOf); + map.defeatMessage.appendTextID("core.lcdesc.1"); + break; + } + + case 1: { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::HERO; + assert(loseTypeWidget); + int townIdx = loseTypeWidget->currentData().toInt(); + cond.position = map.objects[townIdx]->pos; + noneOf.expressions.push_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); + specialDefeat.trigger = EventExpression(noneOf); + map.defeatMessage.appendTextID("core.lcdesc.2"); + break; + } + + case 2: { + EventCondition cond(EventCondition::DAYS_PASSED); + assert(loseValueWidget); + cond.value = expiredDate(loseValueWidget->text()); + specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); + specialDefeat.trigger = EventExpression(cond); + map.defeatMessage.appendTextID("core.lcdesc.3"); + break; + } + + case 3: { + EventCondition cond(EventCondition::DAYS_WITHOUT_TOWN); + assert(loseValueWidget); + cond.value = loseValueWidget->text().toInt(); + specialDefeat.onFulfill.appendTextID("core.genrltxt.7"); + specialDefeat.trigger = EventExpression(cond); + break; + } + } + + EventExpression::OperatorAll allOf; + EventCondition isHuman(EventCondition::IS_HUMAN); + isHuman.value = 1; + + allOf.expressions.push_back(specialDefeat.trigger.get()); + allOf.expressions.push_back(isHuman); + specialDefeat.trigger = EventExpression(allOf); + + if(ui->standardLoseCheck->isChecked()) + { + map.triggeredEvents.push_back(standardDefeat); + } + map.triggeredEvents.push_back(specialDefeat); + } + +} + +void LoseConditions::on_loseComboBox_currentIndexChanged(int index) +{ + delete loseTypeWidget; + delete loseValueWidget; + delete loseSelectWidget; + loseTypeWidget = nullptr; + loseValueWidget = nullptr; + loseSelectWidget = nullptr; + + if(index == 0) + { + ui->standardLoseCheck->setChecked(true); + ui->standardLoseCheck->setEnabled(false); + return; + } + ui->standardLoseCheck->setEnabled(true); + + int loseCondition = index - 1; + switch(loseCondition) + { + case 0: { //EventCondition::CONTROL (Obj::TOWN) + loseTypeWidget = new QComboBox; + ui->loseParamsLayout->addWidget(loseTypeWidget); + for(int i : getObjectIndexes(*mapPointer)) + loseTypeWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + break; + } + + case 1: { //EventCondition::CONTROL (Obj::HERO) + loseTypeWidget = new QComboBox; + ui->loseParamsLayout->addWidget(loseTypeWidget); + for(int i : getObjectIndexes(*mapPointer)) + loseTypeWidget->addItem(tr(getHeroName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + break; + } + + case 2: { //EventCondition::DAYS_PASSED + loseValueWidget = new QLineEdit; + ui->loseParamsLayout->addWidget(loseValueWidget); + loseValueWidget->setText("2m 1w 1d"); + break; + } + + case 3: { //EventCondition::DAYS_WITHOUT_TOWN + loseValueWidget = new QLineEdit; + ui->loseParamsLayout->addWidget(loseValueWidget); + loseValueWidget->setText("7"); + break; + } + } +} + diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h new file mode 100644 index 000000000..91605e45f --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.h @@ -0,0 +1,42 @@ +#ifndef LOSECONDITIONS_H +#define LOSECONDITIONS_H +/* + * loseconditions.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 + * + */ + +#include "abstractsettings.h" + +namespace Ui { +class LoseConditions; +} + +class LoseConditions : public AbstractSettings +{ + Q_OBJECT + +public: + explicit LoseConditions(QWidget *parent = nullptr); + ~LoseConditions(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_loseComboBox_currentIndexChanged(int index); + +private: + Ui::LoseConditions *ui; + const CMap * mapPointer = nullptr; + + QComboBox * loseTypeWidget = nullptr; + QComboBox * loseSelectWidget = nullptr; + QLineEdit * loseValueWidget = nullptr; +}; + +#endif // LOSECONDITIONS_H diff --git a/mapeditor/mapsettings/loseconditions.ui b/mapeditor/mapsettings/loseconditions.ui new file mode 100644 index 000000000..066a18c8e --- /dev/null +++ b/mapeditor/mapsettings/loseconditions.ui @@ -0,0 +1,69 @@ + + + LoseConditions + + + + 0 + 0 + 650 + 485 + + + + Form + + + + + + 0 + + + 0 + + + + + Defeat message + + + + + + + + + + + + + + + 7 days without town + + + + + + + + 0 + 0 + + + + Parameters + + + + + + + + + + + + + diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp new file mode 100644 index 000000000..ac91b0a71 --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -0,0 +1,119 @@ +/* + * mapsettings.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 "mapsettings.h" +#include "ui_mapsettings.h" +#include "mainwindow.h" + +#include "../../lib/CSkillHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CHeroHandler.h" + + +MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : + QDialog(parent), + ui(new Ui::MapSettings), + controller(ctrl) +{ + ui->setupUi(this); + + assert(controller.map()); + + show(); + + for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); + ui->listAbilities->addItem(item); + } + for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedSpells[i] ? Qt::Checked : Qt::Unchecked); + ui->listSpells->addItem(item); + } + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); + ui->listArts->addItem(item); + } + for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); + ui->listHeroes->addItem(item); + } + + ui->general->initialize(*controller.map()); + ui->mods->initialize(*controller.map()); + ui->victory->initialize(*controller.map()); + ui->lose->initialize(*controller.map()); + + //timed events + for(auto & ev : controller.map()->events) + { + QVariantMap descriptor; + descriptor["message"] = QVariant::fromValue(QString::fromStdString(ev.message)); + descriptor["players"] = QVariant::fromValue(ev.players); + descriptor["humanAffected"] = QVariant::fromValue(ev.humanAffected); + descriptor["computerAffected"] = QVariant::fromValue(ev.computerAffected); + descriptor["firstOccurence"] = QVariant::fromValue(ev.firstOccurence); + descriptor["nextOccurence"] = QVariant::fromValue(ev.nextOccurence); + + auto * item = new QListWidgetItem(QString::fromStdString(ev.name)); + item->setData(Qt::UserRole, descriptor); + ui->eventsList->addItem(item); + } +} + +MapSettings::~MapSettings() +{ + delete ui; +} + +void MapSettings::on_pushButton_clicked() +{ + auto updateMapArray = [](const QListWidget * widget, std::vector & arr) + { + for(int i = 0; i < arr.size(); ++i) + { + auto * item = widget->item(i); + arr[i] = item->checkState() == Qt::Checked; + } + }; + + updateMapArray(ui->listAbilities, controller.map()->allowedAbilities); + updateMapArray(ui->listSpells, controller.map()->allowedSpells); + updateMapArray(ui->listArts, controller.map()->allowedArtifact); + updateMapArray(ui->listHeroes, controller.map()->allowedHeroes); + + controller.map()->triggeredEvents.clear(); + + ui->general->update(*controller.map()); + ui->mods->update(*controller.map()); + ui->victory->update(*controller.map()); + ui->lose->update(*controller.map()); + + controller.commitChangeWithoutRedraw(); + + close(); +} diff --git a/mapeditor/mapsettings/mapsettings.h b/mapeditor/mapsettings/mapsettings.h new file mode 100644 index 000000000..fc4501531 --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.h @@ -0,0 +1,36 @@ +/* + * mapsettings.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 +#include "mapcontroller.h" +#include "../lib/mapping/CMap.h" + +namespace Ui { +class MapSettings; +} + +class MapSettings : public QDialog +{ + Q_OBJECT + +public: + explicit MapSettings(MapController & controller, QWidget *parent = nullptr); + ~MapSettings(); + +private slots: + void on_pushButton_clicked(); + +private: + + Ui::MapSettings *ui; + MapController & controller; +}; diff --git a/mapeditor/mapsettings/mapsettings.ui b/mapeditor/mapsettings/mapsettings.ui new file mode 100644 index 000000000..9ed0004ff --- /dev/null +++ b/mapeditor/mapsettings/mapsettings.ui @@ -0,0 +1,362 @@ + + + MapSettings + + + Qt::ApplicationModal + + + + 0 + 0 + 543 + 494 + + + + + 0 + 0 + + + + Map settings + + + + 0 + + + 3 + + + 3 + + + + + 0 + + + + General + + + + 12 + + + 12 + + + 12 + + + + + + + + + Mods + + + + 12 + + + 12 + + + 3 + + + + + + + + + Events + + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Victory + + + + 12 + + + 12 + + + 12 + + + + + + + + + Loss + + + + 12 + + + 12 + + + 12 + + + + + + + + + Timed + + + + 12 + + + 12 + + + 12 + + + + + + + Timed events + + + + + + + Add + + + + + + + Remove + + + + + + + + + + + + + + + + + Abilities + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Adjust + + + QListView::Batched + + + 30 + + + + + + + + Spells + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Artifacts + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + Heroes + + + + 12 + + + 12 + + + 12 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::ScrollPerItem + + + true + + + QListView::Batched + + + 30 + + + + + + + + + + + Ok + + + + + + + + GeneralSettings + QWidget +
    mapsettings/generalsettings.h
    + 1 +
    + + ModSettings + QWidget +
    mapsettings/modsettings.h
    + 1 +
    + + VictoryConditions + QWidget +
    mapsettings/victoryconditions.h
    + 1 +
    + + LoseConditions + QWidget +
    mapsettings/loseconditions.h
    + 1 +
    +
    + + +
    diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp new file mode 100644 index 000000000..6987087f5 --- /dev/null +++ b/mapeditor/mapsettings/modsettings.cpp @@ -0,0 +1,154 @@ +#include "StdInc.h" +#include "modsettings.h" +#include "ui_modsettings.h" +#include "../mapcontroller.h" +#include "../../lib/modding/CModHandler.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/modding/CModInfo.h" + +void traverseNode(QTreeWidgetItem * item, std::function action) +{ + // Do something with item + action(item); + for (int i = 0; i < item->childCount(); ++i) + traverseNode(item->child(i), action); +} + +ModSettings::ModSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::ModSettings) +{ + ui->setupUi(this); +} + +ModSettings::~ModSettings() +{ + delete ui; +} + +void ModSettings::initialize(const CMap & map) +{ + mapPointer = ↦ + + //mods management + //collect all active mods + QMap addedMods; + QSet modsToProcess; + ui->treeMods->blockSignals(true); + + auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) + { + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); + item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(0, map.mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + //set parent check + if(parent && item->checkState(0) == Qt::Checked) + parent->setCheckState(0, Qt::Checked); + return item; + }; + + for(const auto & modName : VLC->modh->getActiveMods()) + { + QString qmodName = QString::fromStdString(modName); + if(qmodName.split(".").size() == 1) + { + const auto & modInfo = VLC->modh->getModInfo(modName); + addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo); + ui->treeMods->addTopLevelItem(addedMods[qmodName]); + } + else + { + modsToProcess.insert(qmodName); + } + } + + for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();) + { + auto qmodName = *qmodIter; + auto pieces = qmodName.split("."); + assert(pieces.size() > 1); + + QString qs; + for(int i = 0; i < pieces.size() - 1; ++i) + qs += pieces[i]; + + if(addedMods.count(qs)) + { + const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString()); + addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo); + modsToProcess.erase(qmodIter); + qmodIter = modsToProcess.begin(); + } + else + ++qmodIter; + } + + ui->treeMods->blockSignals(false); +} + +void ModSettings::update(CMap & map) +{ + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + if(item->checkState(0) == Qt::Checked) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + map.mods[modName] = VLC->modh->getModInfo(modName).version; + } + }; + + map.mods.clear(); + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } +} + +void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) +{ + //Mod management + auto widgetAction = [&](QTreeWidgetItem * item) + { + auto modName = item->data(0, Qt::UserRole).toString().toStdString(); + item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked); + }; + + for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) + { + QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); + traverseNode(item, widgetAction); + } +} + +void ModSettings::on_modResolution_map_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentMap(*mapPointer)); +} + + +void ModSettings::on_modResolution_full_clicked() +{ + updateModWidgetBasedOnMods(MapController::modAssessmentAll()); +} + +void ModSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column) +{ + //set state for children + for (int i = 0; i < item->childCount(); ++i) + item->child(i)->setCheckState(0, item->checkState(0)); + + //set state for parent + ui->treeMods->blockSignals(true); + if(item->checkState(0) == Qt::Checked) + { + while(item->parent()) + { + item->parent()->setCheckState(0, Qt::Checked); + item = item->parent(); + } + } + ui->treeMods->blockSignals(false); +} diff --git a/mapeditor/mapsettings/modsettings.h b/mapeditor/mapsettings/modsettings.h new file mode 100644 index 000000000..0051174cf --- /dev/null +++ b/mapeditor/mapsettings/modsettings.h @@ -0,0 +1,36 @@ +#ifndef MODSETTINGS_H +#define MODSETTINGS_H + +#include "abstractsettings.h" + +namespace Ui { +class ModSettings; +} + +class ModSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit ModSettings(QWidget *parent = nullptr); + ~ModSettings(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_modResolution_map_clicked(); + + void on_modResolution_full_clicked(); + + void on_treeMods_itemChanged(QTreeWidgetItem *item, int column); + +private: + void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods); + +private: + Ui::ModSettings *ui; + const CMap * mapPointer = nullptr; +}; + +#endif // MODSETTINGS_H diff --git a/mapeditor/mapsettings/modsettings.ui b/mapeditor/mapsettings/modsettings.ui new file mode 100644 index 000000000..47f57e89c --- /dev/null +++ b/mapeditor/mapsettings/modsettings.ui @@ -0,0 +1,85 @@ + + + ModSettings + + + + 0 + 0 + 599 + 451 + + + + Form + + + + + + Mandatory mods to play this map + + + + + + + QAbstractScrollArea::AdjustIgnored + + + 320 + + + + Mod name + + + + + Version + + + + + + + + + + Automatic assignment + + + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + Map objects mods + + + false + + + + + + + Set all mods having a game content as mandatory + + + Full content mods + + + false + + + + + + + + + + diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp new file mode 100644 index 000000000..61d7b16ac --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -0,0 +1,426 @@ +#include "StdInc.h" +#include "victoryconditions.h" +#include "ui_victoryconditions.h" + +#include "../lib/CGeneralTextHandler.h" +#include "../lib/constants/StringConstants.h" + +#include "../inspector/townbulidingswidget.h" //to convert BuildingID to string + +VictoryConditions::VictoryConditions(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::VictoryConditions) +{ + ui->setupUi(this); +} + +void VictoryConditions::initialize(const CMap & map) +{ + mapPointer = ↦ + + //victory message + ui->victoryMessageEdit->setText(QString::fromStdString(map.victoryMessage.toString())); + + //victory conditions + const std::array conditionStringsWin = { + QT_TR_NOOP("No special victory"), + QT_TR_NOOP("Capture artifact"), + QT_TR_NOOP("Hire creatures"), + QT_TR_NOOP("Accumulate resources"), + QT_TR_NOOP("Construct building"), + QT_TR_NOOP("Capture town"), + QT_TR_NOOP("Defeat hero"), + QT_TR_NOOP("Transport artifact") + }; + + for(auto & s : conditionStringsWin) + { + ui->victoryComboBox->addItem(QString::fromStdString(s)); + } + ui->standardVictoryCheck->setChecked(false); + ui->onlyForHumansCheck->setChecked(false); + + for(auto & ev : map.triggeredEvents) + { + if(ev.effect.type == EventEffect::VICTORY) + { + if(ev.identifier == "standardVictory") + ui->standardVictoryCheck->setChecked(true); + + if(ev.identifier == "specialVictory") + { + auto readjson = ev.trigger.toJson(AbstractSettings::conditionToJson); + auto linearNodes = linearJsonArray(readjson); + + for(auto & json : linearNodes) + { + switch(json["condition"].Integer()) + { + case EventCondition::HAVE_ARTIFACT: { + ui->victoryComboBox->setCurrentIndex(1); + assert(victoryTypeWidget); + victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); + break; + } + + case EventCondition::HAVE_CREATURES: { + ui->victoryComboBox->setCurrentIndex(2); + assert(victoryTypeWidget); + assert(victoryValueWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + victoryValueWidget->setText(QString::number(json["value"].Integer())); + break; + } + + case EventCondition::HAVE_RESOURCES: { + ui->victoryComboBox->setCurrentIndex(3); + assert(victoryTypeWidget); + assert(victoryValueWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + victoryValueWidget->setText(QString::number(json["value"].Integer())); + break; + } + + case EventCondition::HAVE_BUILDING: { + ui->victoryComboBox->setCurrentIndex(4); + assert(victoryTypeWidget); + assert(victorySelectWidget); + auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); + victoryTypeWidget->setCurrentIndex(idx); + int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victorySelectWidget->findData(townIdx); + victorySelectWidget->setCurrentIndex(idx); + } + break; + } + + case EventCondition::CONTROL: { + ui->victoryComboBox->setCurrentIndex(5); + assert(victoryTypeWidget); + if(json["objectType"].Integer() == Obj::TOWN) + { + int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victoryTypeWidget->findData(townIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } + //TODO: support control other objects (dwellings, mines) + break; + } + + case EventCondition::DESTROY: { + ui->victoryComboBox->setCurrentIndex(6); + assert(victoryTypeWidget); + if(json["objectType"].Integer() == Obj::HERO) + { + int heroIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(heroIdx >= 0) + { + auto idx = victoryTypeWidget->findData(heroIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } + //TODO: support control other objects (monsters) + break; + } + + case EventCondition::TRANSPORT: { + ui->victoryComboBox->setCurrentIndex(7); + assert(victoryTypeWidget); + assert(victorySelectWidget); + victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); + int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + if(townIdx >= 0) + { + auto idx = victorySelectWidget->findData(townIdx); + victorySelectWidget->setCurrentIndex(idx); + } + break; + } + + case EventCondition::IS_HUMAN: { + ui->onlyForHumansCheck->setChecked(true); + break; + } + }; + } + } + } + } +} + +void VictoryConditions::update(CMap & map) +{ + //victory messages + map.victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); + + //victory conditions + EventCondition victoryCondition(EventCondition::STANDARD_WIN); + + //Victory condition - defeat all + TriggeredEvent standardVictory; + standardVictory.effect.type = EventEffect::VICTORY; + standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + standardVictory.identifier = "standardVictory"; + standardVictory.description.clear(); // TODO: display in quest window + standardVictory.onFulfill.appendTextID("core.genrltxt.659"); + standardVictory.trigger = EventExpression(victoryCondition); + + //VICTORY + if(ui->victoryComboBox->currentIndex() == 0) + { + map.triggeredEvents.push_back(standardVictory); + map.victoryIconIndex = 11; + map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); + } + else + { + int vicCondition = ui->victoryComboBox->currentIndex() - 1; + + TriggeredEvent specialVictory; + specialVictory.effect.type = EventEffect::VICTORY; + specialVictory.identifier = "specialVictory"; + specialVictory.description.clear(); // TODO: display in quest window + + map.victoryIconIndex = vicCondition; + map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1]); + + switch(vicCondition) + { + case 0: { + EventCondition cond(EventCondition::HAVE_ARTIFACT); + assert(victoryTypeWidget); + cond.objectType = victoryTypeWidget->currentData().toInt(); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); + specialVictory.onFulfill.appendTextID("core.genrltxt.280"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 1: { + EventCondition cond(EventCondition::HAVE_CREATURES); + assert(victoryTypeWidget); + cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.value = victoryValueWidget->text().toInt(); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); + specialVictory.onFulfill.appendTextID("core.genrltxt.276"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 2: { + EventCondition cond(EventCondition::HAVE_RESOURCES); + assert(victoryTypeWidget); + cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.value = victoryValueWidget->text().toInt(); + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); + specialVictory.onFulfill.appendTextID("core.genrltxt.278"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 3: { + EventCondition cond(EventCondition::HAVE_BUILDING); + assert(victoryTypeWidget); + cond.objectType = victoryTypeWidget->currentData().toInt(); + int townIdx = victorySelectWidget->currentData().toInt(); + if(townIdx > -1) + cond.position = map.objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); + specialVictory.onFulfill.appendTextID("core.genrltxt.282"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 4: { + EventCondition cond(EventCondition::CONTROL); + assert(victoryTypeWidget); + cond.objectType = Obj::TOWN; + int townIdx = victoryTypeWidget->currentData().toInt(); + cond.position = map.objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); + specialVictory.onFulfill.appendTextID("core.genrltxt.249"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 5: { + EventCondition cond(EventCondition::DESTROY); + assert(victoryTypeWidget); + cond.objectType = Obj::HERO; + int heroIdx = victoryTypeWidget->currentData().toInt(); + cond.position = map.objects[heroIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); + specialVictory.onFulfill.appendTextID("core.genrltxt.252"); + specialVictory.trigger = EventExpression(cond); + break; + } + + case 6: { + EventCondition cond(EventCondition::TRANSPORT); + assert(victoryTypeWidget); + cond.objectType = victoryTypeWidget->currentData().toInt(); + int townIdx = victorySelectWidget->currentData().toInt(); + if(townIdx > -1) + cond.position = map.objects[townIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); + specialVictory.onFulfill.appendTextID("core.genrltxt.292"); + specialVictory.trigger = EventExpression(cond); + break; + } + + } + + // if condition is human-only turn it into following construction: AllOf(human, condition) + if(ui->onlyForHumansCheck->isChecked()) + { + EventExpression::OperatorAll oper; + EventCondition notAI(EventCondition::IS_HUMAN); + notAI.value = 1; + oper.expressions.push_back(notAI); + oper.expressions.push_back(specialVictory.trigger.get()); + specialVictory.trigger = EventExpression(oper); + } + + // if normal victory allowed - add one more quest + if(ui->standardVictoryCheck->isChecked()) + { + map.victoryMessage.appendRawString(" / "); + map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); + map.triggeredEvents.push_back(standardVictory); + } + map.triggeredEvents.push_back(specialVictory); + } +} + +VictoryConditions::~VictoryConditions() +{ + delete ui; +} + +void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) +{ + delete victoryTypeWidget; + delete victoryValueWidget; + delete victorySelectWidget; + victoryTypeWidget = nullptr; + victoryValueWidget = nullptr; + victorySelectWidget = nullptr; + + if(index == 0) + { + ui->standardVictoryCheck->setChecked(true); + ui->standardVictoryCheck->setEnabled(false); + ui->onlyForHumansCheck->setChecked(false); + ui->onlyForHumansCheck->setEnabled(false); + return; + } + ui->onlyForHumansCheck->setEnabled(true); + ui->standardVictoryCheck->setEnabled(true); + + int vicCondition = index - 1; + switch(vicCondition) + { + case 0: { //EventCondition::HAVE_ARTIFACT + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < mapPointer->allowedArtifact.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); + break; + } + + case 1: { //EventCondition::HAVE_CREATURES + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < VLC->creh->objects.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->creh->objects[i]->getNamePluralTranslated()), QVariant::fromValue(i)); + + victoryValueWidget = new QLineEdit; + ui->victoryParamsLayout->addWidget(victoryValueWidget); + victoryValueWidget->setText("1"); + break; + } + + case 2: { //EventCondition::HAVE_RESOURCES + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + { + for(int resType = 0; resType < GameConstants::RESOURCE_QUANTITY; ++resType) + { + auto resName = QString::fromStdString(GameConstants::RESOURCE_NAMES[resType]); + victoryTypeWidget->addItem(resName, QVariant::fromValue(resType)); + } + } + + victoryValueWidget = new QLineEdit; + ui->victoryParamsLayout->addWidget(victoryValueWidget); + victoryValueWidget->setText("1"); + break; + } + + case 3: { //EventCondition::HAVE_BUILDING + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + auto * ctown = VLC->townh->randomTown; + for(int bId : ctown->getAllBuildings()) + victoryTypeWidget->addItem(QString::fromStdString(defaultBuildingIdConversion(BuildingID(bId))), QVariant::fromValue(bId)); + + victorySelectWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victorySelectWidget); + victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); + for(int i : getObjectIndexes(*mapPointer)) + victorySelectWidget->addItem(getTownName(*mapPointer, i).c_str(), QVariant::fromValue(i)); + break; + } + + case 4: { //EventCondition::CONTROL (Obj::TOWN) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*mapPointer)) + victoryTypeWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + break; + } + + case 5: { //EventCondition::DESTROY (Obj::HERO) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*mapPointer)) + victoryTypeWidget->addItem(tr(getHeroName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + break; + } + + case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) + victoryTypeWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i = 0; i < mapPointer->allowedArtifact.size(); ++i) + victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); + + victorySelectWidget = new QComboBox; + ui->victoryParamsLayout->addWidget(victorySelectWidget); + for(int i : getObjectIndexes(*mapPointer)) + victorySelectWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + break; + } + + + //TODO: support this vectory type + // in order to do that, need to implement finding creature by position + // selecting from map would be the best user experience + /*case 7: { //EventCondition::DESTROY (Obj::MONSTER) + victoryTypeWidget = new QComboBox; + ui->loseParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*mapPointer)) + victoryTypeWidget->addItem(tr(getMonsterName(i).c_str()), QVariant::fromValue(i)); + break; + }*/ + + + } +} + diff --git a/mapeditor/mapsettings/victoryconditions.h b/mapeditor/mapsettings/victoryconditions.h new file mode 100644 index 000000000..7a52104dd --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.h @@ -0,0 +1,33 @@ +#ifndef VICTORYCONDITIONS_H +#define VICTORYCONDITIONS_H + +#include "abstractsettings.h" + +namespace Ui { +class VictoryConditions; +} + +class VictoryConditions : public AbstractSettings +{ + Q_OBJECT + +public: + explicit VictoryConditions(QWidget *parent = nullptr); + ~VictoryConditions(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_victoryComboBox_currentIndexChanged(int index); + +private: + Ui::VictoryConditions *ui; + const CMap * mapPointer = nullptr; + + QComboBox * victoryTypeWidget = nullptr; + QComboBox * victorySelectWidget = nullptr; + QLineEdit * victoryValueWidget = nullptr; +}; + +#endif // VICTORYCONDITIONS_H diff --git a/mapeditor/mapsettings/victoryconditions.ui b/mapeditor/mapsettings/victoryconditions.ui new file mode 100644 index 000000000..98cb1dd0a --- /dev/null +++ b/mapeditor/mapsettings/victoryconditions.ui @@ -0,0 +1,79 @@ + + + VictoryConditions + + + + 0 + 0 + 622 + 503 + + + + Form + + + + + + 0 + + + 0 + + + + + Victory message + + + + + + + + + + + + + + + Only for human players + + + + + + + Allow standard victory + + + + + + + + 0 + 0 + + + + Parameters + + + + 12 + + + + + + + + + + + + diff --git a/mapeditor/timedevent.cpp b/mapeditor/timedevent.cpp new file mode 100644 index 000000000..5f0e470c7 --- /dev/null +++ b/mapeditor/timedevent.cpp @@ -0,0 +1,26 @@ +#include "timedevent.h" +#include "ui_timedevent.h" + +TimedEvent::TimedEvent(QWidget *parent) : + QDialog(parent), + ui(new Ui::TimedEvent) +{ + ui->setupUi(this); +} + +TimedEvent::~TimedEvent() +{ + delete ui; +} + +void TimedEvent::on_eventResources_clicked() +{ + +} + + +void TimedEvent::on_TimedEvent_finished(int result) +{ + +} + diff --git a/mapeditor/timedevent.h b/mapeditor/timedevent.h new file mode 100644 index 000000000..7f60c4c40 --- /dev/null +++ b/mapeditor/timedevent.h @@ -0,0 +1,27 @@ +#ifndef TIMEDEVENT_H +#define TIMEDEVENT_H + +#include + +namespace Ui { +class TimedEvent; +} + +class TimedEvent : public QDialog +{ + Q_OBJECT + +public: + explicit TimedEvent(QWidget *parent = nullptr); + ~TimedEvent(); + +private slots: + void on_eventResources_clicked(); + + void on_TimedEvent_finished(int result); + +private: + Ui::TimedEvent *ui; +}; + +#endif // TIMEDEVENT_H diff --git a/mapeditor/timedevent.ui b/mapeditor/timedevent.ui new file mode 100644 index 000000000..06e4d2e77 --- /dev/null +++ b/mapeditor/timedevent.ui @@ -0,0 +1,134 @@ + + + TimedEvent + + + Qt::NonModal + + + + 0 + 0 + 620 + 371 + + + + Timed event + + + true + + + + + + + + Event name + + + + + + + Type event message text + + + + + + + 0 + + + + + affects human + + + + + + + affects AI + + + + + + + + + 0 + + + + + + + Day of first occurance + + + + + + + + + + + + 0 + + + + + Repeat after (0 = no repeat) + + + + + + + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 90 + 16777215 + + + + + + + + Resources + + + + + + + + + + From 1a82280cb281d3fe8b12956f751c2bc9b554f281 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 5 Sep 2023 00:58:08 +0300 Subject: [PATCH 0334/1248] Fix build --- lib/constants/EntityIdentifiers.cpp | 2 ++ lib/rmg/CRmgTemplateStorage.cpp | 2 +- lib/rmg/CRmgTemplateStorage.h | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index a9a5513dd..7234190c9 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -224,6 +224,8 @@ bool PlayerColor::isSpectator() const std::string PlayerColor::toString() const { + if (num == -1) + return "neutral"; return encode(num); } diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index b8dfe8127..738d65c1a 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -39,7 +39,7 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const { JsonDeserializer handler(nullptr, data); auto fullKey = scope + ":" + name; //actually it's not used - templates[fullKey] = std::make_unique(); + templates[fullKey] = std::make_shared(); templates[fullKey]->setId(fullKey); templates[fullKey]->serializeJson(handler); templates[fullKey]->setName(name); diff --git a/lib/rmg/CRmgTemplateStorage.h b/lib/rmg/CRmgTemplateStorage.h index 8a129dff1..6ce5c1411 100644 --- a/lib/rmg/CRmgTemplateStorage.h +++ b/lib/rmg/CRmgTemplateStorage.h @@ -37,7 +37,7 @@ public: std::vector getTemplates() const; private: - std::map> templates; + std::map> templates; }; From b159d8a02873964e154e8518d5e1dcddf20012dc Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 5 Sep 2023 23:12:57 +0300 Subject: [PATCH 0335/1248] Fixes ownership checks for creature recruitment --- server/CGameHandler.cpp | 53 ++++++++++++++++++++++----------------- server/CGameHandler.h | 2 +- server/NetPacksServer.cpp | 5 ++-- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index d6f700b6e..02ba83744 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2370,28 +2370,40 @@ bool CGameHandler::razeStructure (ObjectInstanceID tid, BuildingID bid) return true; } -bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl) +bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dstid, CreatureID crid, ui32 cram, si32 fromLvl, PlayerColor player) { - const CGDwelling * dw = static_cast(getObj(objid)); - const CArmedInstance *dst = nullptr; - const CCreature *c = VLC->creh->objects.at(crid); + const CGDwelling * dwelling = dynamic_cast(getObj(objid)); + const CGTownInstance * town = dynamic_cast(getObj(objid)); + const CArmedInstance * army = dynamic_cast(getObj(dstid)); + const CGHeroInstance * hero = dynamic_cast(getObj(dstid)); + const CCreature * c = VLC->creh->objects.at(crid); + const bool warMachine = c->warMachine != ArtifactID::NONE; - //TODO: test for owning - //TODO: check if dst can recruit objects (e.g. hero is actually visiting object, town and source are same, etc) - dst = dynamic_cast(getObj(dstid)); + //TODO: check if hero is actually visiting object - assert(dw && dst); + COMPLAIN_RET_FALSE_IF(!dwelling || !army, "Cannot recruit: invalid object!"); + COMPLAIN_RET_FALSE_IF(dwelling->getOwner() != player && dwelling->getOwner() != PlayerColor::UNFLAGGABLE, "Cannot recruit: dwelling not owned!"); + + if (town) + { + COMPLAIN_RET_FALSE_IF(town != army && !hero, "Cannot recruit: invalid destination!"); + COMPLAIN_RET_FALSE_IF(hero != town->garrisonHero && hero != town->visitingHero, "Cannot recruit: can only recruit to town or hero in town!!"); + } + else + { + COMPLAIN_RET_FALSE_IF(!hero || hero->getOwner() != player, "Cannot recruit: can only recruit to owned hero!"); + } //verify bool found = false; int level = 0; - for (; level < dw->creatures.size(); level++) //iterate through all levels + for (; level < dwelling->creatures.size(); level++) //iterate through all levels { if ((fromLvl != -1) && (level !=fromLvl)) continue; - const auto &cur = dw->creatures.at(level); //current level info + const auto &cur = dwelling->creatures.at(level); //current level info int i = 0; for (; i < cur.second.size(); i++) //look for crid among available creatures list on current level if (cur.second.at(i) == crid) @@ -2404,10 +2416,10 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst break; } } - SlotID slot = dst->getSlotFor(crid); + SlotID slot = army->getSlotFor(crid); if ((!found && complain("Cannot recruit: no such creatures!")) - || ((si32)cram > VLC->creh->objects.at(crid)->maxAmount(getPlayerState(dst->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) + || ((si32)cram > VLC->creh->objects.at(crid)->maxAmount(getPlayerState(army->tempOwner)->resources) && complain("Cannot recruit: lack of resources!")) || (cram<=0 && complain("Cannot recruit: cram <= 0!")) || (!slot.validSlot() && !warMachine && complain("Cannot recruit: no available slot!"))) { @@ -2415,33 +2427,28 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } //recruit - giveResources(dst->tempOwner, -(c->getFullRecruitCost() * cram)); + giveResources(army->tempOwner, -(c->getFullRecruitCost() * cram)); SetAvailableCreatures sac; sac.tid = objid; - sac.creatures = dw->creatures; + sac.creatures = dwelling->creatures; sac.creatures[level].first -= cram; sendAndApply(&sac); if (warMachine) { - const CGHeroInstance *h = dynamic_cast(dst); - - COMPLAIN_RET_FALSE_IF(!h, "Only hero can buy war machines"); - ArtifactID artId = c->warMachine; - - COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!"); - const CArtifact * art = artId.toArtifact(); + COMPLAIN_RET_FALSE_IF(!hero, "Only hero can buy war machines"); + COMPLAIN_RET_FALSE_IF(artId == ArtifactID::CATAPULT, "Catapult cannot be recruited!"); COMPLAIN_RET_FALSE_IF(nullptr == art, "Invalid war machine artifact"); - return giveHeroNewArtifact(h, art); + return giveHeroNewArtifact(hero, art); } else { - addToSlot(StackLocation(dst, slot), c, cram); + addToSlot(StackLocation(army, slot), c, cram); } return true; } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 3e9ad7b1e..3ed17c6c2 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -197,7 +197,7 @@ public: bool garrisonSwap(ObjectInstanceID tid); bool swapGarrisonOnSiege(ObjectInstanceID tid) override; bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ); - bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level); + bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level, PlayerColor player); bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings bool razeStructure(ObjectInstanceID tid, BuildingID bid); bool disbandCreature( ObjectInstanceID id, SlotID pos ); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 20e0df5cd..a3af2eca4 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -104,8 +104,9 @@ void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) { - gh.throwIfWrongOwner(&pack, pack.tid); - result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level); + gh.throwIfWrongPlayer(&pack); + // ownership checks are inside recruitCreatures + result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level, pack.player); } void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) From 47f5dd14fadf8810c15e7fda2d5335db16b64d97 Mon Sep 17 00:00:00 2001 From: Evgeniy Meshcheryakov Date: Wed, 6 Sep 2023 09:12:23 +0300 Subject: [PATCH 0336/1248] Remove(reduce durations) of bonuses for tavern heroes --- lib/gameState/TavernHeroesPool.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 70f441f98..34269b3af 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -117,6 +117,10 @@ void TavernHeroesPool::onNewDay() if(!hero.second) continue; + hero.second->removeBonusesRecursive(Bonus::OneDay); + hero.second->reduceBonusDurations(Bonus::NDays); + hero.second->reduceBonusDurations(Bonus::OneWeek); + // do not access heroes who are not present in tavern of any players if (vstd::contains(unusedHeroes, hero.first)) continue; From e9c2d4e857dbe45126507aebbb2ca804cd2bffca Mon Sep 17 00:00:00 2001 From: Evgeniy Meshcheryakov Date: Wed, 6 Sep 2023 12:40:05 +0300 Subject: [PATCH 0337/1248] Blinded stack should not be able to get morale --- server/battles/BattleFlowProcessor.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index dc1977d19..38d842f97 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -484,6 +484,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CStack * next) && !next->waited() && !next->fear && next->alive() + && next->canMove() && nextStackMorale > 0) { auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); From fc4dfda00f5b0d8f67a6f35f7510d63841d6256e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 25 Aug 2023 18:23:15 +0300 Subject: [PATCH 0338/1248] Added support for concurrent battles to gamestate and server --- lib/CGameInfoCallback.cpp | 10 +- lib/CGameInfoCallback.h | 2 +- lib/NetPacks.h | 49 +++++ lib/NetPacksLib.cpp | 79 ++++---- lib/battle/BattleInfo.cpp | 2 +- lib/battle/BattleInfo.h | 5 +- lib/constants/EntityIdentifiers.cpp | 1 + lib/constants/EntityIdentifiers.h | 6 + lib/gameState/CGameState.cpp | 37 +++- lib/gameState/CGameState.h | 13 +- server/CGameHandler.cpp | 2 + server/NetPacksServer.cpp | 2 +- server/TurnTimerHandler.cpp | 69 ++++--- server/TurnTimerHandler.h | 11 +- server/battles/BattleActionProcessor.cpp | 235 +++++++++++------------ server/battles/BattleActionProcessor.h | 49 ++--- server/battles/BattleFlowProcessor.cpp | 183 +++++++++--------- server/battles/BattleFlowProcessor.h | 37 ++-- server/battles/BattleProcessor.cpp | 94 +++++---- server/battles/BattleProcessor.h | 17 +- server/battles/BattleResultProcessor.cpp | 79 ++++---- server/battles/BattleResultProcessor.h | 15 +- server/queries/BattleQueries.cpp | 4 +- test/game/CGameStateTest.cpp | 24 +-- 24 files changed, 581 insertions(+), 444 deletions(-) diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index efb208fd2..3eac1f6be 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -201,8 +201,10 @@ int32_t CGameInfoCallback::getSpellCost(const spells::Spell * sp, const CGHeroIn //boost::shared_lock lock(*gs->mx); ERROR_RET_VAL_IF(!canGetFullInfo(caster), "Cannot get info about caster!", -1); //if there is a battle - if(gs->curB) - return gs->curB->battleGetSpellCost(sp, caster); + auto casterBattle = gs->getBattle(caster->getOwner()); + + if(casterBattle) + return casterBattle->battleGetSpellCost(sp, caster); //if there is no battle return caster->getSpellCost(sp); @@ -303,7 +305,9 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if (infoLevel == InfoAboutHero::EInfoLevel::BASIC) { - if(gs->curB && gs->curB->playerHasAccessToHeroInfo(*player, h)) //if it's battle we can get enemy hero full data + auto ourBattle = gs->getBattle(*player); + + if(ourBattle && ourBattle->playerHasAccessToHeroInfo(*player, h)) //if it's battle we can get enemy hero full data infoLevel = InfoAboutHero::EInfoLevel::INBATTLE; else ERROR_RET_VAL_IF(!isVisible(h->visitablePos()), "That hero is not visible!", false); diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 775439b5d..e912b53e2 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -46,7 +46,7 @@ class CGDwelling; class CGTeleport; class CGTownInstance; -class DLL_LINKAGE IGameInfoCallback +class DLL_LINKAGE IGameInfoCallback : boost::noncopyable { public: //TODO: all other public methods of CGameInfoCallback diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 5c28c69af..060b15e06 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1483,12 +1483,14 @@ struct DLL_LINKAGE BattleStart : public CPackForClient { void applyGs(CGameState * gs) const; + BattleID battleID = BattleID::NONE; BattleInfo * info = nullptr; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & info; } }; @@ -1496,12 +1498,15 @@ struct DLL_LINKAGE BattleStart : public CPackForClient struct DLL_LINKAGE BattleNextRound : public CPackForClient { void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; si32 round = 0; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & round; } }; @@ -1510,6 +1515,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient { void applyGs(CGameState * gs) const; + BattleID battleID = BattleID::NONE; ui32 stack = 0; ui8 askPlayerInterface = true; @@ -1517,6 +1523,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & stack; h & askPlayerInterface; } @@ -1542,11 +1549,14 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient h & exp; } }; + + BattleID battleID = BattleID::NONE; std::array heroResult; ui8 winnerSide; template void serialize(Handler & h, const int version) { + h & battleID; h & heroResult; h & winnerSide; } @@ -1556,6 +1566,7 @@ struct DLL_LINKAGE BattleResult : public Query { void applyFirstCl(CClient * cl); + BattleID battleID = BattleID::NONE; EBattleResult result = EBattleResult::NORMAL; ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] std::map casualties[2]; //first => casualties of attackers - map crid => number @@ -1566,6 +1577,7 @@ struct DLL_LINKAGE BattleResult : public Query template void serialize(Handler & h, const int version) { + h & battleID; h & queryID; h & result; h & winner; @@ -1578,6 +1590,7 @@ struct DLL_LINKAGE BattleResult : public Query struct DLL_LINKAGE BattleLogMessage : public CPackForClient { + BattleID battleID = BattleID::NONE; std::vector lines; void applyGs(CGameState * gs); @@ -1587,12 +1600,14 @@ struct DLL_LINKAGE BattleLogMessage : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & lines; } }; struct DLL_LINKAGE BattleStackMoved : public CPackForClient { + BattleID battleID = BattleID::NONE; ui32 stack = 0; std::vector tilesToMove; int distance = 0; @@ -1605,6 +1620,7 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & stack; h & tilesToMove; h & distance; @@ -1617,12 +1633,14 @@ struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient void applyGs(CGameState * gs); void applyBattle(IBattleState * battleState); + BattleID battleID = BattleID::NONE; std::vector changedStacks; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & changedStacks; } }; @@ -1632,6 +1650,7 @@ struct BattleStackAttacked DLL_LINKAGE void applyGs(CGameState * gs); DLL_LINKAGE void applyBattle(IBattleState * battleState); + BattleID battleID = BattleID::NONE; ui32 stackAttacked = 0, attackerID = 0; ui32 killedAmount = 0; int64_t damageAmount = 0; @@ -1668,6 +1687,7 @@ struct BattleStackAttacked template void serialize(Handler & h, const int version) { + h & battleID; h & stackAttacked; h & attackerID; h & newState; @@ -1687,6 +1707,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient void applyGs(CGameState * gs); BattleUnitsChanged attackerChanges; + BattleID battleID = BattleID::NONE; std::vector bsa; ui32 stackAttacking = 0; ui32 flags = 0; //uses Eflags (below) @@ -1732,6 +1753,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & bsa; h & stackAttacking; h & flags; @@ -1751,12 +1773,14 @@ struct DLL_LINKAGE StartAction : public CPackForClient void applyFirstCl(CClient * cl); void applyGs(CGameState * gs); + BattleID battleID = BattleID::NONE; BattleAction ba; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & ba; } }; @@ -1765,14 +1789,19 @@ struct DLL_LINKAGE EndAction : public CPackForClient { virtual void visitTyped(ICPackVisitor & visitor) override; + BattleID battleID = BattleID::NONE; + template void serialize(Handler & h, const int version) { + h & battleID; } }; struct DLL_LINKAGE BattleSpellCast : public CPackForClient { void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; bool activeCast = true; ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender SpellID spellID; //id of spell @@ -1788,6 +1817,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & side; h & spellID; h & manaGained; @@ -1805,6 +1835,8 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient { void applyGs(CGameState * gs); void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; std::vector>> toAdd; std::vector>> toUpdate; std::vector>> toRemove; @@ -1813,6 +1845,7 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & toAdd; h & toUpdate; h & toRemove; @@ -1824,23 +1857,27 @@ struct DLL_LINKAGE StacksInjured : public CPackForClient void applyGs(CGameState * gs); void applyBattle(IBattleState * battleState); + BattleID battleID = BattleID::NONE; std::vector stacks; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & stacks; } }; struct DLL_LINKAGE BattleResultsApplied : public CPackForClient { + BattleID battleID = BattleID::NONE; PlayerColor player1, player2; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & player1; h & player2; } @@ -1851,12 +1888,14 @@ struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient void applyGs(CGameState * gs); void applyBattle(IBattleState * battleState); + BattleID battleID = BattleID::NONE; std::vector changes; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & battleID; h & changes; } }; @@ -1883,6 +1922,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient void applyGs(CGameState * gs); void applyBattle(IBattleState * battleState); + BattleID battleID = BattleID::NONE; std::vector< AttackInfo > attackedParts; int attacker = -1; //if -1, then a spell caused this @@ -1890,6 +1930,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & attackedParts; h & attacker; } @@ -1901,6 +1942,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient void applyGs(CGameState * gs) const; + BattleID battleID = BattleID::NONE; int stackID = 0; BattleStackProperty which = CASTS; int val = 0; @@ -1908,6 +1950,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & stackID; h & which; h & val; @@ -1923,6 +1966,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient { void applyGs(CGameState * gs) const; //effect + BattleID battleID = BattleID::NONE; int stackID = 0; int effect = 0; //use corresponding Bonus type int val = 0; @@ -1930,6 +1974,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient template void serialize(Handler & h, const int version) { + h & battleID; h & stackID; h & effect; h & val; @@ -1944,9 +1989,11 @@ struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient { void applyGs(CGameState * gs) const; + BattleID battleID = BattleID::NONE; EGateState state = EGateState::NONE; template void serialize(Handler & h, const int version) { + h & battleID; h & state; } @@ -2533,6 +2580,7 @@ struct DLL_LINKAGE MakeAction : public CPackForServer { } BattleAction ba; + BattleID battleID; virtual void visitTyped(ICPackVisitor & visitor) override; @@ -2540,6 +2588,7 @@ struct DLL_LINKAGE MakeAction : public CPackForServer { h & static_cast(*this); h & ba; + h & battleID; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 626e010d9..3207cd82d 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -34,11 +34,8 @@ #include "campaign/CampaignState.h" #include "GameSettings.h" - VCMI_LIB_NAMESPACE_BEGIN -#define THROW_IF_NO_BATTLE if (!gs->curB) throw std::runtime_error("Trying to apply pack when no battle!"); - void CPack::visit(ICPackVisitor & visitor) { visitBasic(visitor); @@ -962,7 +959,7 @@ void GiveBonus::applyGs(CGameState *gs) break; case ETarget::BATTLE: assert(Bonus::OneBattle(&bonus)); - cbsn = dynamic_cast(gs->curB.get()); + cbsn = dynamic_cast(gs->getBattle(BattleID(id))); break; } @@ -2115,26 +2112,29 @@ void CommanderLevelUp::applyGs(CGameState * gs) const void BattleStart::applyGs(CGameState * gs) const { - gs->curB = info; - gs->curB->localInit(); + assert(battleID == gs->nextBattleID); + + gs->currentBattles.emplace_back(info); + + info->battleID = gs->nextBattleID; + info->localInit(); + + vstd::next(gs->nextBattleID, 1); } void BattleNextRound::applyGs(CGameState * gs) const { - THROW_IF_NO_BATTLE - gs->curB->nextRound(round); + gs->getBattle(battleID)->nextRound(round); } void BattleSetActiveStack::applyGs(CGameState * gs) const { - THROW_IF_NO_BATTLE - gs->curB->nextTurn(stack); + gs->getBattle(battleID)->nextTurn(stack); } void BattleTriggerEffect::applyGs(CGameState * gs) const { - THROW_IF_NO_BATTLE - CStack * st = gs->curB->getStack(stackID); + CStack * st = gs->getBattle(battleID)->getStack(stackID); assert(st); switch(static_cast(effect)) { @@ -2173,8 +2173,8 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const void BattleUpdateGateState::applyGs(CGameState * gs) const { - if(gs->curB) - gs->curB->si.gateState = state; + if(gs->getBattle(battleID)) + gs->getBattle(battleID)->si.gateState = state; } void BattleResultAccepted::applyGs(CGameState * gs) const @@ -2214,7 +2214,13 @@ void BattleResultAccepted::applyGs(CGameState * gs) const CBonusSystemNode::treeHasChanged(); } - gs->curB.dellNull(); + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); } void BattleLogMessage::applyGs(CGameState *gs) @@ -2229,8 +2235,7 @@ void BattleLogMessage::applyBattle(IBattleState * battleState) void BattleStackMoved::applyGs(CGameState *gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void BattleStackMoved::applyBattle(IBattleState * battleState) @@ -2240,8 +2245,7 @@ void BattleStackMoved::applyBattle(IBattleState * battleState) void BattleStackAttacked::applyGs(CGameState * gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void BattleStackAttacked::applyBattle(IBattleState * battleState) @@ -2251,8 +2255,7 @@ void BattleStackAttacked::applyBattle(IBattleState * battleState) void BattleAttack::applyGs(CGameState * gs) { - THROW_IF_NO_BATTLE - CStack * attacker = gs->curB->getStack(stackAttacking); + CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking); assert(attacker); attackerChanges.applyGs(gs); @@ -2265,17 +2268,15 @@ void BattleAttack::applyGs(CGameState * gs) void StartAction::applyGs(CGameState *gs) { - THROW_IF_NO_BATTLE - - CStack *st = gs->curB->getStack(ba.stackNumber); + CStack *st = gs->getBattle(battleID)->getStack(ba.stackNumber); if(ba.actionType == EActionType::END_TACTIC_PHASE) { - gs->curB->tacticDistance = 0; + gs->getBattle(battleID)->tacticDistance = 0; return; } - if(gs->curB->tacticDistance) + if(gs->getBattle(battleID)->tacticDistance) { // moves in tactics phase do not affect creature status // (tactics stack queue is managed by client) @@ -2310,27 +2311,24 @@ void StartAction::applyGs(CGameState *gs) else { if(ba.actionType == EActionType::HERO_SPELL) - gs->curB->sides[ba.side].usedSpellsHistory.push_back(ba.spell); + gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell); } } void BattleSpellCast::applyGs(CGameState * gs) const { - THROW_IF_NO_BATTLE - if(castByHero) { if(side < 2) { - gs->curB->sides[side].castSpellsCount++; + gs->getBattle(battleID)->sides[side].castSpellsCount++; } } } void SetStackEffect::applyGs(CGameState *gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void SetStackEffect::applyBattle(IBattleState * battleState) @@ -2348,8 +2346,7 @@ void SetStackEffect::applyBattle(IBattleState * battleState) void StacksInjured::applyGs(CGameState *gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void StacksInjured::applyBattle(IBattleState * battleState) @@ -2360,8 +2357,7 @@ void StacksInjured::applyBattle(IBattleState * battleState) void BattleUnitsChanged::applyGs(CGameState *gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void BattleUnitsChanged::applyBattle(IBattleState * battleState) @@ -2391,8 +2387,7 @@ void BattleUnitsChanged::applyBattle(IBattleState * battleState) void BattleObstaclesChanged::applyGs(CGameState * gs) { - THROW_IF_NO_BATTLE; - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void BattleObstaclesChanged::applyBattle(IBattleState * battleState) @@ -2423,8 +2418,7 @@ CatapultAttack::~CatapultAttack() = default; void CatapultAttack::applyGs(CGameState * gs) { - THROW_IF_NO_BATTLE - applyBattle(gs->curB); + applyBattle(gs->getBattle(battleID)); } void CatapultAttack::visitTyped(ICPackVisitor & visitor) @@ -2450,8 +2444,7 @@ void CatapultAttack::applyBattle(IBattleState * battleState) void BattleSetStackProperty::applyGs(CGameState * gs) const { - THROW_IF_NO_BATTLE - CStack * stack = gs->curB->getStack(stackID); + CStack * stack = gs->getBattle(battleID)->getStack(stackID); switch(which) { case CASTS: @@ -2464,7 +2457,7 @@ void BattleSetStackProperty::applyGs(CGameState * gs) const } case ENCHANTER_COUNTER: { - auto & counter = gs->curB->sides[gs->curB->whatSide(stack->unitOwner())].enchanterCounter; + auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter; if(absolute) counter = val; else diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 52c1b274e..6d6be9913 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -26,7 +26,7 @@ VCMI_LIB_NAMESPACE_BEGIN ///BattleInfo -std::pair< std::vector, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) +std::pair< std::vector, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const { auto reachability = getReachability(stack); diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index e875cadfa..2b0006836 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -26,6 +26,8 @@ class BattleField; class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState { public: + BattleID battleID = BattleID(0); + enum BattleSide { ATTACKER = 0, @@ -49,6 +51,7 @@ public: template void serialize(Handler &h, const int version) { + h & battleID; h & sides; h & round; h & activeStack; @@ -137,7 +140,7 @@ public: using CBattleInfoEssentials::battleGetFightingHero; CGHeroInstance * battleGetFightingHero(ui8 side) const; - std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack); //returned value: pair; length may be different than number of elements in path since flying creatures jump between distant hexes + std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; //returned value: pair; length may be different than number of elements in path since flying creatures jump between distant hexes void calculateCasualties(std::map * casualties) const; //casualties are array of maps size 2 (attacker, defeneder), maps are (crid => amount) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 7234190c9..c27575339 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -38,6 +38,7 @@ VCMI_LIB_NAMESPACE_BEGIN +const BattleID BattleID::NONE = BattleID(-1); const QueryID QueryID::NONE = QueryID(-1); const HeroTypeID HeroTypeID::NONE = HeroTypeID(-1); const ObjectInstanceID ObjectInstanceID::NONE = ObjectInstanceID(-1); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 587a91512..76b363dc2 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -180,6 +180,12 @@ public: DLL_LINKAGE static const QueryID NONE; }; +class BattleID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const BattleID NONE; +}; class ObjectInstanceID : public Identifier { public: diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index f182570ca..093e82d0a 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -395,7 +395,6 @@ CGameState::CGameState() CGameState::~CGameState() { - curB.dellNull(); map.dellNull(); } @@ -1218,11 +1217,41 @@ void CGameState::initVisitingAndGarrisonedHeroes() } } +const BattleInfo * CGameState::getBattle(const PlayerColor & player) const +{ + if (!player.isValidPlayer()) + return nullptr; + + for (const auto & battlePtr : currentBattles) + if (battlePtr->sides[0].color == player || battlePtr->sides[1].color == player) + return battlePtr.get(); + + return nullptr; +} + +const BattleInfo * CGameState::getBattle(const BattleID & battle) const +{ + for (const auto & battlePtr : currentBattles) + if (battlePtr->battleID == battle) + return battlePtr.get(); + + return nullptr; +} + +BattleInfo * CGameState::getBattle(const BattleID & battle) +{ + for (const auto & battlePtr : currentBattles) + if (battlePtr->battleID == battle) + return battlePtr.get(); + + return nullptr; +} + BattleField CGameState::battleGetBattlefieldType(int3 tile, CRandomGenerator & rand) { - if(!tile.valid() && curB) - tile = curB->tile; - else if(!tile.valid() && !curB) + assert(tile.valid()); + + if(!tile.valid()) return BattleField::NONE; const TerrainTile &t = map->getTile(tile); diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index aa274b0ec..63123b5d9 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -83,6 +83,11 @@ class DLL_LINKAGE CGameState : public CNonConstInfoCallback friend class CGameStateCampaign; public: + /// List of currently ongoing battles + std::vector> currentBattles; + /// ID that can be allocated to next battle + BattleID nextBattleID = BattleID(0); + //we have here all heroes available on this map that are not hired std::unique_ptr heroesPool; @@ -98,7 +103,6 @@ public: void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) - ConstTransitivePtr curB; //current battle ui32 day; //total number of days in game ConstTransitivePtr map; std::map players; @@ -124,6 +128,13 @@ public: std::vector guardingCreatures (int3 pos) const; void updateRumor(); + /// Returns battle in which selected player is engaged, or nullptr if none. + /// Can NOT be used with neutral player, use battle by ID instead + const BattleInfo * getBattle(const PlayerColor & player) const; + /// Returns battle by its unique identifier, or nullptr if not found + const BattleInfo * getBattle(const BattleID & battle) const; + BattleInfo * getBattle(const BattleID & battle); + // ----- victory, loss condition checks ----- EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 2d162a52c..3ce15293d 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -40,6 +40,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/int3.h" +#include "../lib/battle/BattleInfo.h" #include "../lib/filesystem/FileInfo.h" #include "../lib/filesystem/Filesystem.h" #include "../lib/gameState/CGameState.h" @@ -1007,6 +1008,7 @@ void CGameHandler::run(bool resume) clockLast += clockDuration; turnTimerHandler.update(timePassed); boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + } } diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 20e0df5cd..a4c622948 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -276,7 +276,7 @@ void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) { gh.throwIfWrongPlayer(&pack); - result = gh.battles->makePlayerBattleAction(pack.player, pack.ba); + result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba); } void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 4ef71ad35..ba0bf651b 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -91,8 +91,8 @@ void TurnTimerHandler::update(int waitTime) if(gs->isPlayerMakingTurn(player)) onPlayerMakingTurn(player, waitTime); - if(gs->curB) - onBattleLoop(waitTime); + for (auto & battle : gs->currentBattles) + onBattleLoop(battle->battleID, waitTime); } } @@ -140,11 +140,11 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) } } -bool TurnTimerHandler::isPvpBattle() const +bool TurnTimerHandler::isPvpBattle(const BattleID & battleID) const { const auto * gs = gameHandler.gameState(); - auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); - auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); if(attacker.isValidPlayer() && defender.isValidPlayer()) { const auto * attackerState = gameHandler.getPlayerState(attacker); @@ -155,18 +155,18 @@ bool TurnTimerHandler::isPvpBattle() const return false; } -void TurnTimerHandler::onBattleStart() +void TurnTimerHandler::onBattleStart(const BattleID & battleID) { std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB) + if(!si || !gs) return; - auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); - auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); - bool pvpBattle = isPvpBattle(); + bool pvpBattle = isPvpBattle(battleID); for(auto i : {attacker, defender}) { @@ -183,18 +183,24 @@ void TurnTimerHandler::onBattleStart() } } -void TurnTimerHandler::onBattleEnd() +void TurnTimerHandler::onBattleEnd(const BattleID & battleID) { std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB) + if(!si || !gs) + { + assert(0); return; + } - auto attacker = gs->curB->getSidePlayer(BattleSide::ATTACKER); - auto defender = gs->curB->getSidePlayer(BattleSide::DEFENDER); + if (!si->turnTimerInfo.isBattleEnabled()) + return; - bool pvpBattle = isPvpBattle(); + auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); + auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); + + bool pvpBattle = isPvpBattle(battleID); for(auto i : {attacker, defender}) { @@ -216,15 +222,21 @@ void TurnTimerHandler::onBattleEnd() } } -void TurnTimerHandler::onBattleNextStack(const CStack & stack) +void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack & stack) { std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + if(!si || !gs || !gs->getBattle(battleID)) + { + assert(0); + return; + } + + if (!si->turnTimerInfo.isBattleEnabled()) return; - if(isPvpBattle()) + if(isPvpBattle(battleID)) { auto player = stack.getOwner(); @@ -237,29 +249,32 @@ void TurnTimerHandler::onBattleNextStack(const CStack & stack) } } -void TurnTimerHandler::onBattleLoop(int waitTime) +void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) { std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !gs->curB || !si->turnTimerInfo.isBattleEnabled()) + if(!si || !gs || !si->turnTimerInfo.isBattleEnabled()) + { + assert(0); return; + } ui8 side = 0; const CStack * stack = nullptr; - bool isTactisPhase = gs->curB->battleTacticDist() > 0; + bool isTactisPhase = gs->getBattle(battleID)->battleTacticDist() > 0; if(isTactisPhase) - side = gs->curB->battleGetTacticsSide(); + side = gs->getBattle(battleID)->battleGetTacticsSide(); else { - stack = gs->curB->battleGetStackByID(gs->curB->getActiveStackID()); + stack = gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->getActiveStackID()); if(!stack || !stack->getOwner().isValidPlayer()) return; side = stack->unitSide(); } - auto player = gs->curB->getSidePlayer(side); + auto player = gs->getBattle(battleID)->getSidePlayer(side); if(!player.isValidPlayer()) return; @@ -270,7 +285,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) auto & timer = timers[player]; if(timer.isActive && timer.isBattle && !timerCountDown(timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) { - if(isPvpBattle()) + if(isPvpBattle(battleID)) { if(timer.battleTimer > 0) { @@ -289,7 +304,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) doNothing.actionType = EActionType::DEFEND; doNothing.stackNumber = stack->unitId(); } - gameHandler.battles->makePlayerBattleAction(player, doNothing); + gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); } } else @@ -311,7 +326,7 @@ void TurnTimerHandler::onBattleLoop(int waitTime) BattleAction retreat; retreat.side = side; retreat.actionType = EActionType::RETREAT; //harsh punishment - gameHandler.battles->makePlayerBattleAction(player, retreat); + gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); } } } diff --git a/server/TurnTimerHandler.h b/server/TurnTimerHandler.h index 65538d9ca..a780b466c 100644 --- a/server/TurnTimerHandler.h +++ b/server/TurnTimerHandler.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CStack; class PlayerColor; +class BattleID; VCMI_LIB_NAMESPACE_END @@ -33,10 +34,10 @@ class TurnTimerHandler std::recursive_mutex mx; void onPlayerMakingTurn(PlayerColor player, int waitTime); - void onBattleLoop(int waitTime); + void onBattleLoop(const BattleID & battleID, int waitTime); bool timerCountDown(int & timerToApply, int initialTimer, PlayerColor player, int waitTime); - bool isPvpBattle() const; + bool isPvpBattle(const BattleID & battleID) const; void sendTimerUpdate(PlayerColor player); public: @@ -44,9 +45,9 @@ public: void onGameplayStart(PlayerColor player); void onPlayerGetTurn(PlayerColor player); - void onBattleStart(); - void onBattleNextStack(const CStack & stack); - void onBattleEnd(); + void onBattleStart(const BattleID & battle); + void onBattleNextStack(const BattleID & battle, const CStack & stack); + void onBattleEnd(const BattleID & battleID); void update(int waitTime); void setTimerEnabled(PlayerColor player, bool enabled); void setEndTurnAllowed(PlayerColor player, bool enabled); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index c08d4ce38..c869b8dcd 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -37,42 +37,42 @@ void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -bool BattleActionProcessor::doEmptyAction(const BattleAction & ba) +bool BattleActionProcessor::doEmptyAction(const BattleInfo & battle, const BattleAction & ba) { return true; } -bool BattleActionProcessor::doEndTacticsAction(const BattleAction & ba) +bool BattleActionProcessor::doEndTacticsAction(const BattleInfo & battle, const BattleAction & ba) { return true; } -bool BattleActionProcessor::doWaitAction(const BattleAction & ba) +bool BattleActionProcessor::doWaitAction(const BattleInfo & battle, const BattleAction & ba) { const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; return true; } -bool BattleActionProcessor::doRetreatAction(const BattleAction & ba) +bool BattleActionProcessor::doRetreatAction(const BattleInfo & battle, const BattleAction & ba) { - if (!gameHandler->gameState()->curB->battleCanFlee(gameHandler->gameState()->curB->sides.at(ba.side).color)) + if (!battle.battleCanFlee(battle.sides.at(ba.side).color)) { gameHandler->complain("Cannot retreat!"); return false; } - owner->setBattleResult(EBattleResult::ESCAPE, !ba.side); + owner->setBattleResult(battle, EBattleResult::ESCAPE, !ba.side); return true; } -bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba) +bool BattleActionProcessor::doSurrenderAction(const BattleInfo & battle, const BattleAction & ba) { - PlayerColor player = gameHandler->gameState()->curB->sides.at(ba.side).color; - int cost = gameHandler->gameState()->curB->battleGetSurrenderCost(player); + PlayerColor player = battle.sides.at(ba.side).color; + int cost = battle.battleGetSurrenderCost(player); if (cost < 0) { gameHandler->complain("Cannot surrender!"); @@ -86,13 +86,13 @@ bool BattleActionProcessor::doSurrenderAction(const BattleAction & ba) } gameHandler->giveResource(player, EGameResID::GOLD, -cost); - owner->setBattleResult(EBattleResult::SURRENDER, !ba.side); + owner->setBattleResult(battle, EBattleResult::SURRENDER, !ba.side); return true; } -bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) +bool BattleActionProcessor::doHeroSpellAction(const BattleInfo & battle, const BattleAction & ba) { - const CGHeroInstance *h = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + const CGHeroInstance *h = battle.battleGetFightingHero(ba.side); if (!h) { logGlobal->error("Wrong caster!"); @@ -106,7 +106,7 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) return false; } - spells::BattleCast parameters(gameHandler->gameState()->curB, h, spells::Mode::HERO, s); + spells::BattleCast parameters(&battle, h, spells::Mode::HERO, s); spells::detail::ProblemImpl problem; @@ -122,17 +122,17 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleAction & ba) return false; } - parameters.cast(gameHandler->spellEnv, ba.getTarget(gameHandler->gameState()->curB)); + parameters.cast(gameHandler->spellEnv, ba.getTarget(&battle)); return true; } -bool BattleActionProcessor::doWalkAction(const BattleAction & ba) +bool BattleActionProcessor::doWalkAction(const BattleInfo & battle, const BattleAction & ba) { const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + battle::Target target = ba.getTarget(&battle); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; if(target.size() < 1) @@ -141,7 +141,7 @@ bool BattleActionProcessor::doWalkAction(const BattleAction & ba) return false; } - int walkedTiles = moveStack(ba.stackNumber, target.at(0).hexValue); //move + int walkedTiles = moveStack(battle, ba.stackNumber, target.at(0).hexValue); //move if (!walkedTiles) { gameHandler->complain("Stack failed movement!"); @@ -150,11 +150,11 @@ bool BattleActionProcessor::doWalkAction(const BattleAction & ba) return true; } -bool BattleActionProcessor::doDefendAction(const BattleAction & ba) +bool BattleActionProcessor::doDefendAction(const BattleInfo & battle, const BattleAction & ba) { const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) @@ -199,12 +199,12 @@ bool BattleActionProcessor::doDefendAction(const BattleAction & ba) return true; } -bool BattleActionProcessor::doAttackAction(const BattleAction & ba) +bool BattleActionProcessor::doAttackAction(const BattleInfo & battle, const BattleAction & ba) { const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + battle::Target target = ba.getTarget(&battle); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; if(target.size() < 2) @@ -215,7 +215,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) BattleHex attackPos = target.at(0).hexValue; BattleHex destinationTile = target.at(1).hexValue; - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destinationTile, true); + const CStack * destinationStack = battle.battleGetStackByPos(destinationTile, true); if(!destinationStack) { @@ -224,7 +224,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) } BattleHex startingPos = stack->getPosition(); - int distance = moveStack(ba.stackNumber, attackPos); + int distance = moveStack(battle, ba.stackNumber, attackPos); logGlobal->trace("%s will attack %s", stack->nodeName(), destinationStack->nodeName()); @@ -256,7 +256,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) int totalAttacks = stack->totalAttacks.getMeleeValue(); //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); @@ -269,13 +269,13 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) //first strike if(i == 0 && firstStrike && retaliation) { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); } //move can cause death, eg. by walking into the moat, first strike can cause death or paralysis/petrification if(stack->alive() && !stack->hasBonusOfType(BonusType::NOT_ACTIVE) && destinationStack->alive()) { - makeAttack(stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack + makeAttack(battle, stack, destinationStack, (i ? 0 : distance), destinationTile, i==0, false, false);//no distance travelled on second attack } //counterattack @@ -285,7 +285,7 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) && (i == 0 && !firstStrike) && retaliation && destinationStack->ableToRetaliate()) { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, false, true); + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, false, true); } } @@ -296,18 +296,18 @@ bool BattleActionProcessor::doAttackAction(const BattleAction & ba) && startingPos == target.at(2).hexValue && stack->alive()) { - moveStack(ba.stackNumber, startingPos); + moveStack(battle, ba.stackNumber, startingPos); //NOTE: curStack->unitId() == ba.stackNumber (rev 1431) } return true; } -bool BattleActionProcessor::doShootAction(const BattleAction & ba) +bool BattleActionProcessor::doShootAction(const BattleInfo & battle, const BattleAction & ba) { const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + battle::Target target = ba.getTarget(&battle); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; if(target.size() < 1) @@ -318,9 +318,9 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba) auto destination = target.at(0).hexValue; - const CStack * destinationStack = gameHandler->gameState()->curB->battleGetStackByPos(destination); + const CStack * destinationStack = battle.battleGetStackByPos(destination); - if (!gameHandler->gameState()->curB->battleCanShoot(stack, destination)) + if (!battle.battleCanShoot(stack, destination)) { gameHandler->complain("Cannot shoot!"); return false; @@ -332,23 +332,23 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba) return false; } - makeAttack(stack, destinationStack, 0, destination, true, true, false); + makeAttack(battle, stack, destinationStack, 0, destination, true, true, false); //ranged counterattack if (destinationStack->hasBonusOfType(BonusType::RANGED_RETALIATION) && !stack->hasBonusOfType(BonusType::BLOCKS_RANGED_RETALIATION) && destinationStack->ableToRetaliate() - && gameHandler->gameState()->curB->battleCanShoot(destinationStack, stack->getPosition()) + && battle.battleCanShoot(destinationStack, stack->getPosition()) && stack->alive()) //attacker may have died (fire shield) { - makeAttack(destinationStack, stack, 0, stack->getPosition(), true, true, true); + makeAttack(battle, destinationStack, stack, 0, stack->getPosition(), true, true, true); } //allow more than one additional attack int totalRangedAttacks = stack->totalAttacks.getRangedValue(); //TODO: move to CUnitState - const auto * attackingHero = gameHandler->gameState()->curB->battleGetFightingHero(ba.side); + const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); @@ -362,19 +362,19 @@ bool BattleActionProcessor::doShootAction(const BattleAction & ba) && stack->shots.canUse() ) { - makeAttack(stack, destinationStack, 0, destination, false, true, false); + makeAttack(battle, stack, destinationStack, 0, destination, false, true, false); } } return true; } -bool BattleActionProcessor::doCatapultAction(const BattleAction & ba) +bool BattleActionProcessor::doCatapultAction(const BattleInfo & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); @@ -385,7 +385,7 @@ bool BattleActionProcessor::doCatapultAction(const BattleAction & ba) else { const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult + spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); parameters.setSpellLevel(shotLevel); parameters.cast(gameHandler->spellEnv, target); @@ -393,13 +393,13 @@ bool BattleActionProcessor::doCatapultAction(const BattleAction & ba) return true; } -bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) +bool BattleActionProcessor::doUnitSpellAction(const BattleInfo & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); SpellID spellID = ba.spell; - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); @@ -414,7 +414,7 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) else { const CSpell * spell = SpellID(spellID).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::CREATURE_ACTIVE, spell); + spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell); int32_t spellLvl = 0; if(spellcaster) vstd::amax(spellLvl, spellcaster->val); @@ -426,12 +426,12 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleAction & ba) return true; } -bool BattleActionProcessor::doHealAction(const BattleAction & ba) +bool BattleActionProcessor::doHealAction(const BattleInfo & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); - battle::Target target = ba.getTarget(gameHandler->gameState()->curB); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); + battle::Target target = ba.getTarget(&battle); - if (!canStackAct(stack)) + if (!canStackAct(battle, stack)) return false; if(target.size() < 1) @@ -446,7 +446,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba) if(target.at(0).unitValue) destStack = target.at(0).unitValue; else - destStack = gameHandler->gameState()->curB->battleGetUnitByPos(target.at(0).hexValue); + destStack = battle.battleGetUnitByPos(target.at(0).hexValue); if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) { @@ -455,7 +455,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba) else { const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent + spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent auto dest = battle::Destination(destStack, target.at(0).hexValue); parameters.setSpellLevel(0); parameters.cast(gameHandler->spellEnv, {dest}); @@ -463,7 +463,7 @@ bool BattleActionProcessor::doHealAction(const BattleAction & ba) return true; } -bool BattleActionProcessor::canStackAct(const CStack * stack) +bool BattleActionProcessor::canStackAct(const BattleInfo & battle, const CStack * stack) { if (!stack) { @@ -486,7 +486,7 @@ bool BattleActionProcessor::canStackAct(const CStack * stack) } else { - if (stack->unitId() != gameHandler->gameState()->curB->getActiveStackID()) + if (stack->unitId() != battle.getActiveStackID()) { gameHandler->complain("Action has to be about active stack!"); return false; @@ -495,46 +495,46 @@ bool BattleActionProcessor::canStackAct(const CStack * stack) return true; } -bool BattleActionProcessor::dispatchBattleAction(const BattleAction & ba) +bool BattleActionProcessor::dispatchBattleAction(const BattleInfo & battle, const BattleAction & ba) { switch(ba.actionType) { case EActionType::BAD_MORALE: case EActionType::NO_ACTION: - return doEmptyAction(ba); + return doEmptyAction(battle, ba); case EActionType::END_TACTIC_PHASE: - return doEndTacticsAction(ba); + return doEndTacticsAction(battle, ba); case EActionType::RETREAT: - return doRetreatAction(ba); + return doRetreatAction(battle, ba); case EActionType::SURRENDER: - return doSurrenderAction(ba); + return doSurrenderAction(battle, ba); case EActionType::HERO_SPELL: - return doHeroSpellAction(ba); + return doHeroSpellAction(battle, ba); case EActionType::WALK: - return doWalkAction(ba); + return doWalkAction(battle, ba); case EActionType::WAIT: - return doWaitAction(ba); + return doWaitAction(battle, ba); case EActionType::DEFEND: - return doDefendAction(ba); + return doDefendAction(battle, ba); case EActionType::WALK_AND_ATTACK: - return doAttackAction(ba); + return doAttackAction(battle, ba); case EActionType::SHOOT: - return doShootAction(ba); + return doShootAction(battle, ba); case EActionType::CATAPULT: - return doCatapultAction(ba); + return doCatapultAction(battle, ba); case EActionType::MONSTER_SPELL: - return doUnitSpellAction(ba); + return doUnitSpellAction(battle, ba); case EActionType::STACK_HEAL: - return doHealAction(ba); + return doHealAction(battle, ba); } gameHandler->complain("Unrecognized action type received!!"); return false; } -bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) +bool BattleActionProcessor::makeBattleActionImpl(const BattleInfo & battle, const BattleAction &ba) { logGlobal->trace("Making action: %s", ba.toString()); - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); // for these events client does not expects StartAction/EndAction wrapper if (!ba.isBattleEndAction()) @@ -543,7 +543,7 @@ bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) gameHandler->sendAndApply(&startAction); } - bool result = dispatchBattleAction(ba); + bool result = dispatchBattleAction(battle, ba); if (!ba.isBattleEndAction()) { @@ -557,19 +557,19 @@ bool BattleActionProcessor::makeBattleActionImpl(const BattleAction &ba) return result; } -int BattleActionProcessor::moveStack(int stack, BattleHex dest) +int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, BattleHex dest) { int ret = 0; const CStack *curStack = gameHandler->battleGetStackByID(stack); - const CStack *stackAtEnd = gameHandler->gameState()->curB->battleGetStackByPos(dest); + const CStack *stackAtEnd = battle.battleGetStackByPos(dest); assert(curStack); assert(dest < GameConstants::BFIELD_SIZE); - if (gameHandler->gameState()->curB->tacticDistance) + if (battle.tacticDistance) { - assert(gameHandler->gameState()->curB->isInTacticRange(dest)); + assert(battle.isInTacticRange(dest)); } auto start = curStack->getPosition(); @@ -600,7 +600,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) } bool canUseGate = false; - auto dbState = gameHandler->gameState()->curB->si.gateState; + auto dbState = battle.si.gateState; if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && dbState != EGateState::DESTROYED && dbState != EGateState::BLOCKED) @@ -608,13 +608,13 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) canUseGate = true; } - std::pair< std::vector, int > path = gameHandler->gameState()->curB->getPath(start, dest, curStack); + std::pair< std::vector, int > path = battle.getPath(start, dest, curStack); ret = path.second; int creSpeed = curStack->speed(0, true); - if (gameHandler->gameState()->curB->tacticDistance > 0 && creSpeed > 0) + if (battle.tacticDistance > 0 && creSpeed > 0) creSpeed = GameConstants::BFIELD_SIZE; bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) @@ -826,7 +826,7 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) else if (curStack->getPosition() == gateMayCloseAtHex) { gateMayCloseAtHex = BattleHex(); - owner->updateGateState(); + owner->updateGateState(battle); } } } @@ -848,10 +848,10 @@ int BattleActionProcessor::moveStack(int stack, BattleHex dest) return ret; } -void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) +void BattleActionProcessor::makeAttack(const BattleInfo & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) { if(first && !counter) - handleAttackBeforeCasting(ranged, attacker, defender); + handleAttackBeforeCasting(battle, ranged, attacker, defender); FireShieldInfo fireShield; BattleAttack bat; @@ -891,7 +891,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d bat.flags |= BattleAttack::DEATH_BLOW; } - const auto * owner = gameHandler->gameState()->curB->getHero(attacker->unitOwner()); + const auto * owner = battle.getHero(attacker->unitOwner()); if(owner) { int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); @@ -903,14 +903,14 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d // only primary target if(defender->alive()) - drainedLife += applyBattleEffects(bat, attackerState, fireShield, defender, distance, false); + drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, defender, distance, false); //multiple-hex normal attack - std::set attackedCreatures = gameHandler->gameState()->curB->getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target + std::set attackedCreatures = battle.getAttackedCreatures(attacker, targetHex, bat.shot()); //creatures other than primary target for(const CStack * stack : attackedCreatures) { if(stack != defender && stack->alive()) //do not hit same stack twice - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true); } std::shared_ptr bonus = attacker->getBonusLocalFirst(Selector::type()(BonusType::SPELL_LIKE_ATTACK)); @@ -927,7 +927,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d battle::Target target; target.emplace_back(defender, targetHex); - spells::BattleCast event(gameHandler->gameState()->curB, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); + spells::BattleCast event(&battle, attacker, spells::Mode::SPELL_LIKE_ATTACK, spell); event.setSpellLevel(bonus->val); auto attackedCreatures = spell->battleMechanics(&event)->getAffectedStacks(target); @@ -938,7 +938,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d { if(stack != defender && stack->alive()) //do not hit same stack twice { - drainedLife += applyBattleEffects(bat, attackerState, fireShield, stack, distance, true); + drainedLife += applyBattleEffects(battle, bat, attackerState, fireShield, stack, distance, true); } } @@ -1012,7 +1012,7 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d const CStack * actor = item.first; int64_t rawDamage = item.second; - const CGHeroInstance * actorOwner = gameHandler->gameState()->curB->getHero(actor->unitOwner()); + const CGHeroInstance * actorOwner = battle.getHero(actor->unitOwner()); if(actorOwner) { @@ -1055,10 +1055,10 @@ void BattleActionProcessor::makeAttack(const CStack * attacker, const CStack * d gameHandler->sendAndApply(&blm); - handleAfterAttackCasting(ranged, attacker, defender); + handleAfterAttackCasting(battle, ranged, attacker, defender); } -void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +void BattleActionProcessor::attackCasting(const BattleInfo & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) { if(attacker->hasBonusOfType(attackMode)) { @@ -1104,7 +1104,7 @@ void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, con spells::Target target; target.emplace_back(defender); - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); auto m = spell->battleMechanics(¶meters); @@ -1126,17 +1126,17 @@ void BattleActionProcessor::attackCasting(bool ranged, BonusType attackMode, con } } -void BattleActionProcessor::handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender) +void BattleActionProcessor::handleAttackBeforeCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender) { - attackCasting(ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? + attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? } -void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender) +void BattleActionProcessor::handleAfterAttackCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender) { if(!attacker->alive() || !defender->alive()) // can be already dead return; - attackCasting(ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); + attackCasting(battle, ranged, BonusType::SPELL_AFTER_ATTACK, attacker, defender); if(!defender->alive()) { @@ -1169,7 +1169,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * spells::AbilityCaster caster(attacker, 0); - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); spells::Target target; target.emplace_back(defender); parameters.setEffectValue(staredCreatures); @@ -1194,7 +1194,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * spells::AbilityCaster caster(attacker, 0); - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); spells::Target target; target.emplace_back(defender); @@ -1221,7 +1221,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * return; battle::UnitInfo resurrectInfo; - resurrectInfo.id = gameHandler->gameState()->curB->battleNextUnitId(); + resurrectInfo.id = battle.battleNextUnitId(); resurrectInfo.summoned = false; resurrectInfo.position = defender->getPosition(); resurrectInfo.side = defender->unitSide(); @@ -1286,7 +1286,7 @@ void BattleActionProcessor::handleAfterAttackCasting(bool ranged, const CStack * } } -int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +int64_t BattleActionProcessor::applyBattleEffects(const BattleInfo & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) { BattleStackAttacked bsa; if(secondary) @@ -1302,8 +1302,8 @@ int64_t BattleActionProcessor::applyBattleEffects(BattleAttack & bat, std::share bai.luckyStrike = bat.lucky(); bai.unluckyStrike = bat.unlucky(); - auto range = gameHandler->gameState()->curB->calculateDmgRange(bai); - bsa.damageAmount = gameHandler->gameState()->curB->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + auto range = battle.calculateDmgRange(bai); + bsa.damageAmount = battle.getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties } @@ -1390,22 +1390,17 @@ void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CS } } -bool BattleActionProcessor::makeAutomaticBattleAction(const BattleAction & ba) +bool BattleActionProcessor::makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba) { - return makeBattleActionImpl(ba); + return makeBattleActionImpl(battle, ba); } -bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) +bool BattleActionProcessor::makePlayerBattleAction(const BattleInfo & battle, PlayerColor player, const BattleAction &ba) { - const BattleInfo * battle = gameHandler->gameState()->curB; - - if(!battle && gameHandler->complain("Can not make action - there is no battle ongoing!")) - return false; - if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) return false; - if(battle->tacticDistance != 0) + if(battle.tacticDistance != 0) { if(!ba.isTacticsAction()) { @@ -1413,7 +1408,7 @@ bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const Bat return false; } - if(player != battle->sides[ba.side].color) + if(player != battle.sides[ba.side].color) { gameHandler->complain("Can not make actions in battles you are not part of!"); return false; @@ -1421,21 +1416,21 @@ bool BattleActionProcessor::makePlayerBattleAction(PlayerColor player, const Bat } else { - if (ba.isUnitAction() && ba.stackNumber != battle->getActiveStackID()) + if (ba.isUnitAction() && ba.stackNumber != battle.getActiveStackID()) { gameHandler->complain("Can not make actions - stack is not active!"); return false; } - auto active = battle->battleActiveUnit(); + auto active = battle.battleActiveUnit(); if(!active && gameHandler->complain("No active unit in battle!")) return false; - auto unitOwner = battle->battleGetOwner(active); + auto unitOwner = battle.battleGetOwner(active); if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) return false; } - return makeBattleActionImpl(ba); + return makeBattleActionImpl(battle, ba); } diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 10a08165b..e527751b1 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct BattleLogMessage; struct BattleAttack; class BattleAction; +class BattleInfo; struct BattleHex; class CStack; class PlayerColor; @@ -38,42 +39,42 @@ class BattleActionProcessor : boost::noncopyable BattleProcessor * owner; CGameHandler * gameHandler; - int moveStack(int stack, BattleHex dest); //returned value - travelled distance - void makeAttack(const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + int moveStack(const BattleInfo & battle, int stack, BattleHex dest); //returned value - travelled distance + void makeAttack(const BattleInfo & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - void handleAttackBeforeCasting(bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + void handleAttackBeforeCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(const BattleInfo & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + int64_t applyBattleEffects(const BattleInfo & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); - bool canStackAct(const CStack * stack); + bool canStackAct(const BattleInfo & battle, const CStack * stack); - bool doEmptyAction(const BattleAction & ba); - bool doEndTacticsAction(const BattleAction & ba); - bool doRetreatAction(const BattleAction & ba); - bool doSurrenderAction(const BattleAction & ba); - bool doHeroSpellAction(const BattleAction & ba); - bool doWalkAction(const BattleAction & ba); - bool doWaitAction(const BattleAction & ba); - bool doDefendAction(const BattleAction & ba); - bool doAttackAction(const BattleAction & ba); - bool doShootAction(const BattleAction & ba); - bool doCatapultAction(const BattleAction & ba); - bool doUnitSpellAction(const BattleAction & ba); - bool doHealAction(const BattleAction & ba); + bool doEmptyAction(const BattleInfo & battle, const BattleAction & ba); + bool doEndTacticsAction(const BattleInfo & battle, const BattleAction & ba); + bool doRetreatAction(const BattleInfo & battle, const BattleAction & ba); + bool doSurrenderAction(const BattleInfo & battle, const BattleAction & ba); + bool doHeroSpellAction(const BattleInfo & battle, const BattleAction & ba); + bool doWalkAction(const BattleInfo & battle, const BattleAction & ba); + bool doWaitAction(const BattleInfo & battle, const BattleAction & ba); + bool doDefendAction(const BattleInfo & battle, const BattleAction & ba); + bool doAttackAction(const BattleInfo & battle, const BattleAction & ba); + bool doShootAction(const BattleInfo & battle, const BattleAction & ba); + bool doCatapultAction(const BattleInfo & battle, const BattleAction & ba); + bool doUnitSpellAction(const BattleInfo & battle, const BattleAction & ba); + bool doHealAction(const BattleInfo & battle, const BattleAction & ba); - bool dispatchBattleAction(const BattleAction & ba); - bool makeBattleActionImpl(const BattleAction & ba); + bool dispatchBattleAction(const BattleInfo & battle, const BattleAction & ba); + bool makeBattleActionImpl(const BattleInfo & battle, const BattleAction & ba); public: explicit BattleActionProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool makeAutomaticBattleAction(const BattleAction & ba); - bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); + bool makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba); + bool makePlayerBattleAction(const BattleInfo & battle, PlayerColor player, const BattleAction & ba); }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index dc1977d19..1253b0b42 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -35,7 +35,7 @@ void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -void BattleFlowProcessor::summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +void BattleFlowProcessor::summonGuardiansHelper(const BattleInfo & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard { int x = targetPosition.getX(); int y = targetPosition.getY(); @@ -110,34 +110,33 @@ void BattleFlowProcessor::summonGuardiansHelper(std::vector & output, } } -void BattleFlowProcessor::tryPlaceMoats() +void BattleFlowProcessor::tryPlaceMoats(const BattleInfo & battle) { //Moat should be initialized here, because only here we can use spellcasting - if (gameHandler->gameState()->curB->town && gameHandler->gameState()->curB->town->fortLevel() >= CGTownInstance::CITADEL) + if (battle.town && battle.town->fortLevel() >= CGTownInstance::CITADEL) { - const auto * h = gameHandler->gameState()->curB->battleGetFightingHero(BattleSide::DEFENDER); + const auto * h = battle.battleGetFightingHero(BattleSide::DEFENDER); const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(gameHandler->gameState()->curB->getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(gameHandler->gameState()->curB, &moatCaster, spells::Mode::PASSIVE, gameHandler->gameState()->curB->town->town->moatAbility.toSpell()); + auto moatCaster = spells::SilentCaster(battle.getSidePlayer(BattleSide::DEFENDER), actualCaster); + auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, battle.town->town->moatAbility.toSpell()); auto target = spells::Target(); cast.cast(gameHandler->spellEnv, target); } } -void BattleFlowProcessor::onBattleStarted() +void BattleFlowProcessor::onBattleStarted(const BattleInfo & battle) { - gameHandler->setBattle(gameHandler->gameState()->curB); - assert(gameHandler->gameState()->curB); + gameHandler->setBattle(&battle); - tryPlaceMoats(); + tryPlaceMoats(battle); - gameHandler->turnTimerHandler.onBattleStart(); + gameHandler->turnTimerHandler.onBattleStart(battle.battleID); - if (gameHandler->gameState()->curB->tacticDistance == 0) - onTacticsEnded(); + if (battle.tacticDistance == 0) + onTacticsEnded(battle); } -void BattleFlowProcessor::trySummonGuardians(const CStack * stack) +void BattleFlowProcessor::trySummonGuardians(const BattleInfo & battle, const CStack * stack) { if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) return; @@ -156,14 +155,14 @@ void BattleFlowProcessor::trySummonGuardians(const CStack * stack) if (!guardianIsBig) targetHexes = stack->getSurroundingHexes(); else - summonGuardiansHelper(targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); + summonGuardiansHelper(battle, targetHexes, stack->getPosition(), stack->unitSide(), targetIsBig); for(auto hex : targetHexes) { if(accessibility.accessible(hex, guardianIsBig, stack->unitSide())) //without this multiple creatures can occupy one hex { battle::UnitInfo info; - info.id = gameHandler->gameState()->curB->battleNextUnitId(); + info.id = battle.battleNextUnitId(); info.count = std::max(1, (int)(stack->getCount() * 0.01 * summonInfo->val)); info.type = creatureData; info.side = stack->unitSide(); @@ -178,11 +177,11 @@ void BattleFlowProcessor::trySummonGuardians(const CStack * stack) } } -void BattleFlowProcessor::castOpeningSpells() +void BattleFlowProcessor::castOpeningSpells(const BattleInfo & battle) { for (int i = 0; i < 2; ++i) { - auto h = gameHandler->gameState()->curB->battleGetFightingHero(i); + auto h = battle.battleGetFightingHero(i); if (!h) continue; @@ -194,7 +193,7 @@ void BattleFlowProcessor::castOpeningSpells() const CSpell * spell = SpellID(b->subtype).toSpell(); - spells::BattleCast parameters(gameHandler->gameState()->curB, &caster, spells::Mode::PASSIVE, spell); + spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); parameters.setSpellLevel(3); parameters.setEffectDuration(b->val); parameters.massive = true; @@ -203,55 +202,55 @@ void BattleFlowProcessor::castOpeningSpells() } } -void BattleFlowProcessor::onTacticsEnded() +void BattleFlowProcessor::onTacticsEnded(const BattleInfo & battle) { //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = gameHandler->gameState()->curB->stacks; //use temporary variable to outclude summoned stacks added to gameHandler->gameState()->curB->stacks from processing + auto initialStacks = battle.stacks; //use temporary variable to outclude summoned stacks added to battle.stacks from processing for (CStack * stack : initialStacks) { - trySummonGuardians(stack); - stackEnchantedTrigger(stack); + trySummonGuardians(battle, stack); + stackEnchantedTrigger(battle, stack); } - castOpeningSpells(); + castOpeningSpells(battle); // it is possible that due to opening spells one side was eliminated -> check for end of battle - if (owner->checkBattleStateChanges()) + if (owner->checkBattleStateChanges(battle)) return; - startNextRound(true); - activateNextStack(); + startNextRound(battle, true); + activateNextStack(battle); } -void BattleFlowProcessor::startNextRound(bool isFirstRound) +void BattleFlowProcessor::startNextRound(const BattleInfo & battle, bool isFirstRound) { BattleNextRound bnr; - bnr.round = gameHandler->gameState()->curB->round + 1; + bnr.round = battle.round + 1; logGlobal->debug("Round %d", bnr.round); gameHandler->sendAndApply(&bnr); - auto obstacles = gameHandler->gameState()->curB->obstacles; //we copy container, because we're going to modify it + auto obstacles = battle.obstacles; //we copy container, because we're going to modify it for (auto &obstPtr : obstacles) { if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) if (sco->turnsRemaining == 0) - removeObstacle(*obstPtr); + removeObstacle(battle, *obstPtr); } - const BattleInfo & curB = *gameHandler->gameState()->curB; + const BattleInfo & curB = *&battle; for(auto stack : curB.stacks) { if(stack->alive() && !isFirstRound) - stackEnchantedTrigger(stack); + stackEnchantedTrigger(battle, stack); } } -const CStack * BattleFlowProcessor::getNextStack() +const CStack * BattleFlowProcessor::getNextStack(const BattleInfo & battle) { std::vector q; - gameHandler->gameState()->curB->battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" + battle.battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" if(q.empty()) return nullptr; @@ -284,31 +283,29 @@ const CStack * BattleFlowProcessor::getNextStack() return stack; } -void BattleFlowProcessor::activateNextStack() +void BattleFlowProcessor::activateNextStack(const BattleInfo & battle) { - const auto & curB = *gameHandler->gameState()->curB; - // Find next stack that requires manual control for (;;) { // battle has ended - if (owner->checkBattleStateChanges()) + if (owner->checkBattleStateChanges(battle)) return; - const CStack * next = getNextStack(); + const CStack * next = getNextStack(battle); if (!next) { // No stacks to move - start next round - startNextRound(false); - next = getNextStack(); + startNextRound(battle, false); + next = getNextStack(battle); if (!next) throw std::runtime_error("Failed to find valid stack to act!"); } BattleUnitsChanged removeGhosts; - for(auto stack : curB.stacks) + for(auto stack : battle.stacks) { if(stack->ghostPending) removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); @@ -317,20 +314,18 @@ void BattleFlowProcessor::activateNextStack() if(!removeGhosts.changedStacks.empty()) gameHandler->sendAndApply(&removeGhosts); - gameHandler->turnTimerHandler.onBattleNextStack(*next); + gameHandler->turnTimerHandler.onBattleNextStack(battle.battleID, *next); - if (!tryMakeAutomaticAction(next)) + if (!tryMakeAutomaticAction(battle, next)) { - setActiveStack(next); + setActiveStack(battle, next); break; } } } -bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) +bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, const CStack * next) { - const auto & curB = *gameHandler->gameState()->curB; - // check for bad morale => freeze int nextStackMorale = next->moraleVal(); if(!next->hadMorale && !next->waited() && nextStackMorale < 0) @@ -346,7 +341,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) ba.side = next->unitSide(); ba.stackNumber = next->unitId(); - makeAutomaticAction(next, ba); + makeAutomaticAction(battle, next, ba); return true; } } @@ -354,7 +349,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) if (next->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE)) //while in berserk { logGlobal->trace("Handle Berserk effect"); - std::pair attackInfo = curB.getNearestStack(next); + std::pair attackInfo = battle.getNearestStack(next); if (attackInfo.first != nullptr) { BattleAction attack; @@ -364,12 +359,12 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) attack.aimToHex(attackInfo.second); attack.aimToUnit(attackInfo.first); - makeAutomaticAction(next, attack); + makeAutomaticAction(battle, next, attack); logGlobal->trace("Attacked nearest target %s", attackInfo.first->getDescription()); } else { - makeStackDoNothing(next); + makeStackDoNothing(battle, next); logGlobal->trace("No target found"); } return true; @@ -390,12 +385,12 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) const battle::Unit * target = nullptr; - for(auto & elem : gameHandler->gameState()->curB->stacks) + for(auto & elem : battle.stacks) { if(elem->unitType()->getId() != CreatureID::CATAPULT && elem->unitOwner() != next->unitOwner() && elem->isValidTarget() - && gameHandler->gameState()->curB->battleCanShoot(next, elem->getPosition())) + && battle.battleCanShoot(next, elem->getPosition())) { target = elem; break; @@ -404,23 +399,23 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) if(target == nullptr) { - makeStackDoNothing(next); + makeStackDoNothing(battle, next); } else { attack.aimToUnit(target); - makeAutomaticAction(next, attack); + makeAutomaticAction(battle, next, attack); } return true; } if (next->unitType()->getId() == CreatureID::CATAPULT) { - const auto & attackableBattleHexes = curB.getAttackableBattleHexes(); + const auto & attackableBattleHexes = battle.getAttackableBattleHexes(); if (attackableBattleHexes.empty()) { - makeStackDoNothing(next); + makeStackDoNothing(battle, next); return true; } @@ -431,7 +426,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) attack.side = next->unitSide(); attack.stackNumber = next->unitId(); - makeAutomaticAction(next, attack); + makeAutomaticAction(battle, next, attack); return true; } } @@ -445,7 +440,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) if (possibleStacks.empty()) { - makeStackDoNothing(next); + makeStackDoNothing(battle, next); return true; } @@ -460,22 +455,22 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CStack * next) heal.side = next->unitSide(); heal.stackNumber = next->unitId(); - makeAutomaticAction(next, heal); + makeAutomaticAction(battle, next, heal); return true; } } - stackTurnTrigger(next); //various effects + stackTurnTrigger(battle, next); //various effects if(next->fear) { - makeStackDoNothing(next); //end immediately if stack was affected by fear + makeStackDoNothing(battle, next); //end immediately if stack was affected by fear return true; } return false; } -bool BattleFlowProcessor::rollGoodMorale(const CStack * next) +bool BattleFlowProcessor::rollGoodMorale(const BattleInfo & battle, const CStack * next) { //check for good morale auto nextStackMorale = next->moraleVal(); @@ -503,27 +498,25 @@ bool BattleFlowProcessor::rollGoodMorale(const CStack * next) return false; } -void BattleFlowProcessor::onActionMade(const BattleAction &ba) +void BattleFlowProcessor::onActionMade(const BattleInfo & battle, const BattleAction &ba) { - const auto & battle = gameHandler->gameState()->curB; - + const CStack * actedStack = battle.battleGetStackByID(ba.stackNumber, false); + const CStack * activeStack = battle.battleGetStackByID(battle.getActiveStackID(), false); if (ba.actionType == EActionType::END_TACTIC_PHASE) { - onTacticsEnded(); + onTacticsEnded(battle); return; } - const CStack * actedStack = battle->battleGetStackByID(ba.stackNumber, false); - const CStack * activeStack = battle->battleGetStackByID(battle->getActiveStackID(), false); //we're after action, all results applied // check whether action has ended the battle - if(owner->checkBattleStateChanges()) + if(owner->checkBattleStateChanges(battle)) return; // tactics - next stack will be selected by player - if(battle->tacticDistance != 0) + if(battle.tacticDistance != 0) return; if (ba.isUnitAction()) @@ -531,10 +524,10 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) assert(activeStack != nullptr); assert(actedStack != nullptr); - if (rollGoodMorale(actedStack)) + if (rollGoodMorale(battle, actedStack)) { // Good morale - same stack makes 2nd turn - setActiveStack(actedStack); + setActiveStack(battle, actedStack); return; } } @@ -544,36 +537,36 @@ void BattleFlowProcessor::onActionMade(const BattleAction &ba) { // this is action made by hero AND unit is alive (e.g. not killed by casted spell) // keep current active stack for next action - setActiveStack(activeStack); + setActiveStack(battle, activeStack); return; } } - activateNextStack(); + activateNextStack(battle); } -void BattleFlowProcessor::makeStackDoNothing(const CStack * next) +void BattleFlowProcessor::makeStackDoNothing(const BattleInfo & battle, const CStack * next) { BattleAction doNothing; doNothing.actionType = EActionType::NO_ACTION; doNothing.side = next->unitSide(); doNothing.stackNumber = next->unitId(); - makeAutomaticAction(next, doNothing); + makeAutomaticAction(battle, next, doNothing); } -bool BattleFlowProcessor::makeAutomaticAction(const CStack *stack, BattleAction &ba) +bool BattleFlowProcessor::makeAutomaticAction(const BattleInfo & battle, const CStack *stack, BattleAction &ba) { BattleSetActiveStack bsa; bsa.stack = stack->unitId(); bsa.askPlayerInterface = false; gameHandler->sendAndApply(&bsa); - bool ret = owner->makeAutomaticBattleAction(ba); + bool ret = owner->makeAutomaticBattleAction(battle, ba); return ret; } -void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) +void BattleFlowProcessor::stackEnchantedTrigger(const BattleInfo & battle, const CStack * st) { auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); for(auto b : bl) @@ -585,7 +578,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) const int32_t val = bl.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); const int32_t level = ((val > 3) ? (val - 3) : val); - spells::BattleCast battleCast(gameHandler->gameState()->curB, st, spells::Mode::PASSIVE, sp); + spells::BattleCast battleCast(&battle, st, spells::Mode::PASSIVE, sp); //this makes effect accumulate for at most 50 turns by default, but effect may be permanent and last till the end of battle battleCast.setEffectDuration(50); battleCast.setSpellLevel(level); @@ -593,7 +586,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) if(val > 3) { - for(auto s : gameHandler->gameState()->curB->battleGetAllStacks()) + for(auto s : battle.battleGetAllStacks()) if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied target.emplace_back(s); } @@ -605,14 +598,14 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CStack * st) } } -void BattleFlowProcessor::removeObstacle(const CObstacleInstance & obstacle) +void BattleFlowProcessor::removeObstacle(const BattleInfo & battle, const CObstacleInstance & obstacle) { BattleObstaclesChanged obsRem; obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); gameHandler->sendAndApply(&obsRem); } -void BattleFlowProcessor::stackTurnTrigger(const CStack *st) +void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CStack *st) { BattleTriggerEffect bte; bte.stackID = st->unitId(); @@ -626,13 +619,13 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) { bool unbind = true; BonusList bl = *(st->getBonuses(Selector::type()(BonusType::BIND_EFFECT))); - auto adjacent = gameHandler->gameState()->curB->battleAdjacentUnits(st); + auto adjacent = battle.battleAdjacentUnits(st); for (auto b : bl) { if(b->additionalInfo != CAddInfo::NONE) { - const CStack * stack = gameHandler->gameState()->curB->battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent + const CStack * stack = battle.battleGetStackByID(b->additionalInfo[0]); //binding stack must be alive and adjacent if(stack) { if(vstd::contains(adjacent, stack)) //binding stack is still present @@ -668,8 +661,8 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) } if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) { - const PlayerColor opponent = gameHandler->gameState()->curB->otherPlayer(gameHandler->gameState()->curB->battleGetOwner(st)); - const CGHeroInstance * opponentHero = gameHandler->gameState()->curB->getHero(opponent); + const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st)); + const CGHeroInstance * opponentHero = battle.getHero(opponent); if(opponentHero) { ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); @@ -686,7 +679,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) { bool fearsomeCreature = false; - for (CStack * stack : gameHandler->gameState()->curB->stacks) + for (CStack * stack : battle.stacks) { if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) { @@ -704,8 +697,8 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) } } BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = gameHandler->gameState()->curB->whatSide(st->unitOwner()); - if(st->canCast() && gameHandler->gameState()->curB->battleGetEnchanterCounter(side) == 0) + int side = battle.whatSide(st->unitOwner()); + if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) { bool cast = false; while(!bl.empty() && !cast) @@ -717,7 +710,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) { return b == bonus.get(); }); - spells::BattleCast parameters(gameHandler->gameState()->curB, st, spells::Mode::ENCHANTER, spell); + spells::BattleCast parameters(&battle, st, spells::Mode::ENCHANTER, spell); parameters.setSpellLevel(bonus->val); parameters.massive = true; parameters.smart = true; @@ -739,7 +732,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CStack *st) } } -void BattleFlowProcessor::setActiveStack(const CStack * stack) +void BattleFlowProcessor::setActiveStack(const BattleInfo & battle, const CStack * stack) { assert(stack); diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index 20d7a9a23..74dde97de 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -13,6 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CStack; struct BattleHex; class BattleAction; +class BattleInfo; struct CObstacleInstance; VCMI_LIB_NAMESPACE_END @@ -25,31 +26,31 @@ class BattleFlowProcessor : boost::noncopyable BattleProcessor * owner; CGameHandler * gameHandler; - const CStack * getNextStack(); + const CStack * getNextStack(const BattleInfo & battle); - bool rollGoodMorale(const CStack * stack); - bool tryMakeAutomaticAction(const CStack * stack); + bool rollGoodMorale(const BattleInfo & battle, const CStack * stack); + bool tryMakeAutomaticAction(const BattleInfo & battle, const CStack * stack); - void summonGuardiansHelper(std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); - void trySummonGuardians(const CStack * stack); - void tryPlaceMoats(); - void castOpeningSpells(); - void activateNextStack(); - void startNextRound(bool isFirstRound); + void summonGuardiansHelper(const BattleInfo & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void trySummonGuardians(const BattleInfo & battle, const CStack * stack); + void tryPlaceMoats(const BattleInfo & battle); + void castOpeningSpells(const BattleInfo & battle); + void activateNextStack(const BattleInfo & battle); + void startNextRound(const BattleInfo & battle, bool isFirstRound); - void stackEnchantedTrigger(const CStack * stack); - void removeObstacle(const CObstacleInstance & obstacle); - void stackTurnTrigger(const CStack * stack); - void setActiveStack(const CStack * stack); + void stackEnchantedTrigger(const BattleInfo & battle, const CStack * stack); + void removeObstacle(const BattleInfo & battle, const CObstacleInstance & obstacle); + void stackTurnTrigger(const BattleInfo & battle, const CStack * stack); + void setActiveStack(const BattleInfo & battle, const CStack * stack); - void makeStackDoNothing(const CStack * next); - bool makeAutomaticAction(const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + void makeStackDoNothing(const BattleInfo & battle, const CStack * next); + bool makeAutomaticAction(const BattleInfo & battle, const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) public: explicit BattleFlowProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - void onBattleStarted(); - void onTacticsEnded(); - void onActionMade(const BattleAction & ba); + void onBattleStarted(const BattleInfo & battle); + void onTacticsEnded(const BattleInfo & battle); + void onActionMade(const BattleInfo & battle, const BattleAction & ba); }; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 0ed52c6ef..a67319d35 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -54,8 +54,8 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, const CGTownInstance *town) //use hero=nullptr for no hero { - if(gameHandler->gameState()->curB) - gameHandler->gameState()->curB.dellNull(); + assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); + assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); engageIntoBattle(army1->tempOwner); engageIntoBattle(army2->tempOwner); @@ -67,10 +67,12 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm heroes[0] = hero1; heroes[1] = hero2; - resultProcessor->setupBattle(); - setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); + const auto * battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); //existing battle query for retying auto-combat if(lastBattleQuery) @@ -86,13 +88,13 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm } } - lastBattleQuery->bi = gameHandler->gameState()->curB; + lastBattleQuery->bi = battle; lastBattleQuery->result = std::nullopt; - lastBattleQuery->belligerents[0] = gameHandler->gameState()->curB->sides[0].armyObject; - lastBattleQuery->belligerents[1] = gameHandler->gameState()->curB->sides[1].armyObject; + lastBattleQuery->belligerents[0] = battle->sides[0].armyObject; + lastBattleQuery->belligerents[1] = battle->sides[1].armyObject; } - auto nextBattleQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + auto nextBattleQuery = std::make_shared(gameHandler, battle); for(int i : {0, 1}) { if(heroes[i]) @@ -102,7 +104,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm } gameHandler->queries->addQuery(nextBattleQuery); - flowProcessor->onBattleStarted(); + flowProcessor->onBattleStarted(*battle); } void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank) @@ -118,7 +120,7 @@ void BattleProcessor::startBattleI(const CArmedInstance *army1, const CArmedInst startBattleI(army1, army2, army2->visitablePos(), creatureBank); } -void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) +BattleID BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town) { const auto & t = *gameHandler->getTile(tile); TerrainId terrain = t.terType->getId(); @@ -132,6 +134,7 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co //send info about battles BattleStart bs; bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town); + bs.battleID = gameHandler->gameState()->nextBattleID; engageIntoBattle(bs.info->sides[0].color); engageIntoBattle(bs.info->sides[1].color); @@ -140,28 +143,30 @@ void BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2], co bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer(); gameHandler->sendAndApply(&bs); + + return bs.battleID; } -bool BattleProcessor::checkBattleStateChanges() +bool BattleProcessor::checkBattleStateChanges(const BattleInfo & battle) { //check if drawbridge state need to be changes if (gameHandler->battleGetSiegeLevel() > 0) - updateGateState(); + updateGateState(battle); - if (resultProcessor->battleIsEnding()) + if (resultProcessor->battleIsEnding(battle)) return true; //check if battle ended if (auto result = gameHandler->battleIsFinished()) { - setBattleResult(EBattleResult::NORMAL, *result); + setBattleResult(battle, EBattleResult::NORMAL, *result); return true; } return false; } -void BattleProcessor::updateGateState() +void BattleProcessor::updateGateState(const BattleInfo & battle) { // GATE_BRIDGE - leftmost tile, located over moat // GATE_OUTER - central tile, mostly covered by gate image @@ -178,17 +183,19 @@ void BattleProcessor::updateGateState() // - deals moat damage to attacker if bridge is closed (fortress only) bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty(); - bool hasStackAtGateInner = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr; - bool hasStackAtGateOuter = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr; - bool hasStackAtGateBridge = gameHandler->gameState()->curB->battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr; + bool hasStackAtGateInner = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr; + bool hasStackAtGateOuter = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr; + bool hasStackAtGateBridge = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr; bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) { return obst->obstacleType == CObstacleInstance::MOAT; }); BattleUpdateGateState db; - db.state = gameHandler->gameState()->curB->si.gateState; - if (gameHandler->gameState()->curB->si.wallState[EWallPart::GATE] == EWallState::DESTROYED) + db.state = battle.si.gateState; + db.battleID = battle.battleID; + + if (battle.si.wallState.at(EWallPart::GATE) == EWallState::DESTROYED) { db.state = EGateState::DESTROYED; } @@ -212,37 +219,54 @@ void BattleProcessor::updateGateState() db.state = EGateState::CLOSED; } - if (db.state != gameHandler->gameState()->curB->si.gateState) + if (db.state != battle.si.gateState) gameHandler->sendAndApply(&db); } -bool BattleProcessor::makePlayerBattleAction(PlayerColor player, const BattleAction &ba) +bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction &ba) { - bool result = actionsProcessor->makePlayerBattleAction(player, ba); - if (!resultProcessor->battleIsEnding() && gameHandler->gameState()->curB != nullptr) - flowProcessor->onActionMade(ba); + const auto * battle = gameHandler->gameState()->getBattle(battleID); + + if (!battle) + return false; + + bool result = actionsProcessor->makePlayerBattleAction(*battle, player, ba); + if (gameHandler->gameState()->getBattle(battleID) != nullptr && !resultProcessor->battleIsEnding(*battle)) + flowProcessor->onActionMade(*battle, ba); return result; } -void BattleProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +void BattleProcessor::setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide) { - resultProcessor->setBattleResult(resultType, victoriusSide); - resultProcessor->endBattle(gameHandler->gameState()->curB->tile, gameHandler->gameState()->curB->battleGetFightingHero(0), gameHandler->gameState()->curB->battleGetFightingHero(1)); + resultProcessor->setBattleResult(battle, resultType, victoriusSide); + resultProcessor->endBattle(battle); } -bool BattleProcessor::makeAutomaticBattleAction(const BattleAction &ba) +bool BattleProcessor::makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction &ba) { - return actionsProcessor->makeAutomaticBattleAction(ba); + return actionsProcessor->makeAutomaticBattleAction(battle, ba); } -void BattleProcessor::endBattleConfirm(const BattleInfo * battleInfo) +void BattleProcessor::endBattleConfirm(const BattleID & battleID) { - resultProcessor->endBattleConfirm(battleInfo); + auto battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + if (!battle) + return; + + resultProcessor->endBattleConfirm(*battle); } -void BattleProcessor::battleAfterLevelUp(const BattleResult &result) +void BattleProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult &result) { - resultProcessor->battleAfterLevelUp(result); + auto battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + if (!battle) + return; + + resultProcessor->battleAfterLevelUp(*battle, result); } void BattleProcessor::setGameHandler(CGameHandler * newGameHandler) diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 40cce6aee..c53dff89f 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -19,6 +19,7 @@ class BattleAction; class int3; class BattleInfo; struct BattleResult; +class BattleID; VCMI_LIB_NAMESPACE_END class CGameHandler; @@ -40,15 +41,15 @@ class BattleProcessor : boost::noncopyable std::unique_ptr resultProcessor; void onTacticsEnded(); - void updateGateState(); + void updateGateState(const BattleInfo & battle); void engageIntoBattle(PlayerColor player); - bool checkBattleStateChanges(); - void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); + bool checkBattleStateChanges(const BattleInfo & battle); + BattleID setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - bool makeAutomaticBattleAction(const BattleAction & ba); + bool makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba); - void setBattleResult(EBattleResult resultType, int victoriusSide); + void setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide); public: explicit BattleProcessor(CGameHandler * gameHandler); @@ -65,12 +66,12 @@ public: void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); /// Processing of incoming battle action netpack - bool makePlayerBattleAction(PlayerColor player, const BattleAction & ba); + bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba); /// Applies results of a battle once player agrees to them - void endBattleConfirm(const BattleInfo * battleInfo); + void endBattleConfirm(const BattleID & battleID); /// Applies results of a battle after potential levelup - void battleAfterLevelUp(const BattleResult & result); + void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result); template void serialize(Handler &h, const int version) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index bf0827949..6f9be2242 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -196,7 +196,7 @@ FinishingBattleHelper::FinishingBattleHelper() remainingBattleQueriesCount = 0; } -void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAttacker, const CGHeroInstance * heroDefender) +void BattleResultProcessor::endBattle(const BattleInfo & battle) { auto const & giveExp = [](BattleResult &r) { @@ -215,6 +215,10 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta LOG_TRACE(logGlobal); + auto * battleResult = battleResults.at(battle.battleID).get(); + const auto * heroAttacker = battle.getSideHero(BattleSide::ATTACKER); + const auto * heroDefender = battle.getSideHero(BattleSide::DEFENDER); + //Fill BattleResult structure with exp info giveExp(*battleResult); @@ -231,11 +235,11 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta if(heroDefender) battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(gameHandler->gameState()->curB->sides[0].color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sides[0].color)); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); - gameHandler->complain("Player " + boost::lexical_cast(gameHandler->gameState()->curB->sides[0].color) + " has no battle query at the top!"); + gameHandler->complain("Player " + boost::lexical_cast(battle.sides[0].color) + " has no battle query at the top!"); return; } @@ -243,12 +247,14 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta //Check how many battle gameHandler->queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; - finishingBattle = std::make_unique(battleQuery, queriedPlayers); + + assert(finishingBattles.count(battle.battleID) == 0); + finishingBattles[battle.battleID] = std::make_unique(battleQuery, queriedPlayers); // in battles against neutrals, 1st player can ask to replay battle manually - if (!gameHandler->gameState()->curB->sides[1].color.isValidPlayer()) + if (!battle.sides[1].color.isValidPlayer()) { - auto battleDialogQuery = std::make_shared(gameHandler, gameHandler->gameState()->curB); + auto battleDialogQuery = std::make_shared(gameHandler, &battle); battleResult->queryID = battleDialogQuery->queryID; gameHandler->queries->addQuery(battleDialogQuery); } @@ -263,25 +269,27 @@ void BattleResultProcessor::endBattle(int3 tile, const CGHeroInstance * heroAtta otherBattleQuery->result = battleQuery->result; } - gameHandler->turnTimerHandler.onBattleEnd(); - gameHandler->sendAndApply(battleResult.get()); //after this point casualties objects are destroyed + gameHandler->turnTimerHandler.onBattleEnd(battle.battleID); if (battleResult->queryID == QueryID::NONE) - endBattleConfirm(gameHandler->gameState()->curB); + endBattleConfirm(battle); } -void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) +void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) { - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battleInfo->sides.at(0).color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sides.at(0).color)); if(!battleQuery) { logGlobal->trace("No battle query, battle end was confirmed by another player"); return; } - const EBattleResult result = battleResult.get()->result; + auto * battleResult = battleResults.at(battle.battleID).get(); + auto * finishingBattle = finishingBattles.at(battle.battleID).get(); - CasualtiesAfterBattle cab1(battleInfo->sides.at(0), battleInfo), cab2(battleInfo->sides.at(1), battleInfo); //calculate casualties before deleting battle + const EBattleResult result = battleResult->result; + + CasualtiesAfterBattle cab1(battle.sides.at(0), &battle), cab2(battle.sides.at(1), &battle); //calculate casualties before deleting battle ChangeSpells cs; //for Eagle Eye if(!finishingBattle->isDraw() && finishingBattle->winnerHero) @@ -289,7 +297,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) { double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battleInfo->sides.at(!battleResult->winner).usedSpellsHistory) + for(auto & spellId : battle.sides.at(!battleResult->winner).usedSpellsHistory) { auto spell = spellId.toSpell(VLC->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) @@ -357,7 +365,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) } } } - for (auto armySlot : battleInfo->sides.at(!battleResult->winner).armyObject->stacks) + for (auto armySlot : battle.sides.at(!battleResult->winner).armyObject->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; for (auto artSlot : artifactsWorn) @@ -458,10 +466,10 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battleInfo->sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battleInfo->sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battleInfo->sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battleInfo->sides.at(1).hero); + raccepted.heroResult[0].army = const_cast(battle.sides.at(0).armyObject); + raccepted.heroResult[1].army = const_cast(battle.sides.at(1).armyObject); + raccepted.heroResult[0].hero = const_cast(battle.sides.at(0).hero); + raccepted.heroResult[1].hero = const_cast(battle.sides.at(1).hero); raccepted.heroResult[0].exp = battleResult->exp[0]; raccepted.heroResult[1].exp = battleResult->exp[1]; raccepted.winnerSide = finishingBattle->winnerSide; @@ -471,13 +479,16 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo * battleInfo) //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query } -void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) +void BattleResultProcessor::battleAfterLevelUp(const BattleInfo & battle, const BattleResult & result) { LOG_TRACE(logGlobal); - if(!finishingBattle) + assert(finishingBattles.count(battle.battleID) != 0); + if(finishingBattles.count(battle.battleID) == 0) return; + auto & finishingBattle = finishingBattles[battle.battleID]; + finishingBattle->remainingBattleQueriesCount--; logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); @@ -490,7 +501,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) // Still, it looks like a hole. // Necromancy if applicable. - const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(*battleResult) : CStackBasicDescriptor(); + const CStackBasicDescriptor raisedStack = finishingBattle->winnerHero ? finishingBattle->winnerHero->calculateNecromancy(result) : CStackBasicDescriptor(); // Give raised units to winner and show dialog, if any were raised, // units will be given after casualties are taken const SlotID necroSlot = raisedStack.type ? finishingBattle->winnerHero->getSlotFor(raisedStack.type) : SlotID(); @@ -528,25 +539,23 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleResult &result) gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); } - finishingBattle.reset(); - battleResult.reset(); + finishingBattles.erase(battle.battleID); + battleResults.erase(battle.battleID); } -void BattleResultProcessor::setBattleResult(EBattleResult resultType, int victoriusSide) +void BattleResultProcessor::setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide) { - battleResult = std::make_unique(); + assert(battleResults.count(battle.battleID) == 0); + + battleResults[battle.battleID] = std::make_unique(); + + auto & battleResult = battleResults[battle.battleID]; battleResult->result = resultType; battleResult->winner = victoriusSide; //surrendering side loses - gameHandler->gameState()->curB->calculateCasualties(battleResult->casualties); + battle.calculateCasualties(battleResult->casualties); } -void BattleResultProcessor::setupBattle() +bool BattleResultProcessor::battleIsEnding(const BattleInfo & battle) const { - finishingBattle.reset(); - battleResult.reset(); -} - -bool BattleResultProcessor::battleIsEnding() const -{ - return battleResult != nullptr; + return battleResults.count(battle.battleID) != 0; } diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index 9c85638ae..cf7f097ae 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -64,18 +64,17 @@ class BattleResultProcessor : boost::noncopyable // BattleProcessor * owner; CGameHandler * gameHandler; - std::unique_ptr battleResult; - std::unique_ptr finishingBattle; + std::map> battleResults; + std::map> finishingBattles; public: explicit BattleResultProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool battleIsEnding() const; + bool battleIsEnding(const BattleInfo & battle) const; - void setupBattle(); - void setBattleResult(EBattleResult resultType, int victoriusSide); - void endBattle(int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2); //ends battle - void endBattleConfirm(const BattleInfo * battleInfo); - void battleAfterLevelUp(const BattleResult & result); + void setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide); + void endBattle(const BattleInfo & battle); //ends battle + void endBattleConfirm(const BattleInfo & battle); + void battleAfterLevelUp(const BattleInfo & battle, const BattleResult & result); }; diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index e3deeef80..b7329ba29 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -49,7 +49,7 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { if(result) - gh->battles->battleAfterLevelUp(*result); + gh->battles->battleAfterLevelUp(bi->battleID, *result); } CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): @@ -70,6 +70,6 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) } else { - gh->battles->endBattleConfirm(bi); + gh->battles->endBattleConfirm(bi->battleID); } } diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 4b41d2e3e..39aac67d3 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -204,9 +204,9 @@ public: BattleStart bs; bs.info = battle; - ASSERT_EQ(gameState->curB, nullptr); + ASSERT_EQ(gameState->currentBattles.size(), 0); gameCallback->sendAndApply(&bs); - ASSERT_EQ(gameState->curB, battle); + ASSERT_EQ(gameState->currentBattles.size(), 1); } std::shared_ptr gameState; @@ -247,11 +247,11 @@ TEST_F(CGameStateTest, issue2765) { battle::UnitInfo info; - info.id = gameState->curB->battleNextUnitId(); + info.id = gameState->currentBattles.front()->battleNextUnitId(); info.count = 1; info.type = CreatureID(69); info.side = BattleSide::ATTACKER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -263,7 +263,7 @@ TEST_F(CGameStateTest, issue2765) const CStack * att = nullptr; const CStack * def = nullptr; - for(const CStack * s : gameState->curB->stacks) + for(const CStack * s : gameState->currentBattles.front()->stacks) { if(s->unitType()->getId() == CreatureID::BALLISTA && s->unitSide() == BattleSide::DEFENDER) def = s; @@ -292,7 +292,7 @@ TEST_F(CGameStateTest, issue2765) spells::AbilityCaster caster(att, 3); //here tested ballista, but this applied to all war machines - spells::BattleCast cast(gameState->curB, &caster, spells::Mode::PASSIVE, age); + spells::BattleCast cast(gameState->currentBattles.front().get(), &caster, spells::Mode::PASSIVE, age); spells::Target target; target.emplace_back(def); @@ -339,7 +339,7 @@ TEST_F(CGameStateTest, battleResurrection) startTestBattle(attacker, defender); - uint32_t unitId = gameState->curB->battleNextUnitId(); + uint32_t unitId = gameState->currentBattles.front()->battleNextUnitId(); { battle::UnitInfo info; @@ -347,7 +347,7 @@ TEST_F(CGameStateTest, battleResurrection) info.count = 10; info.type = CreatureID(13); info.side = BattleSide::ATTACKER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -358,11 +358,11 @@ TEST_F(CGameStateTest, battleResurrection) { battle::UnitInfo info; - info.id = gameState->curB->battleNextUnitId(); + info.id = gameState->currentBattles.front()->battleNextUnitId(); info.count = 10; info.type = CreatureID(13); info.side = BattleSide::DEFENDER; - info.position = gameState->curB->getAvaliableHex(info.type, info.side); + info.position = gameState->currentBattles.front()->getAvaliableHex(info.type, info.side); info.summoned = false; BattleUnitsChanged pack; @@ -371,7 +371,7 @@ TEST_F(CGameStateTest, battleResurrection) gameCallback->sendAndApply(&pack); } - CStack * unit = gameState->curB->getStack(unitId); + CStack * unit = gameState->currentBattles.front()->getStack(unitId); ASSERT_NE(unit, nullptr); @@ -390,7 +390,7 @@ TEST_F(CGameStateTest, battleResurrection) const CSpell * spell = SpellID(SpellID::RESURRECTION).toSpell(); ASSERT_NE(spell, nullptr); - spells::BattleCast cast(gameState->curB, attacker, spells::Mode::HERO, spell); + spells::BattleCast cast(gameState->currentBattles.front().get(), attacker, spells::Mode::HERO, spell); spells::Target target; target.emplace_back(unit); From 3a88180494f5594877cb6bd2321bacd7391f5474 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 28 Aug 2023 17:43:57 +0300 Subject: [PATCH 0339/1248] Separated game and battle callback (server & client only) --- AI/BattleAI/BattleEvaluator.cpp | 2 +- AI/BattleAI/StackWithBonuses.cpp | 23 ++++- AI/BattleAI/StackWithBonuses.h | 9 +- include/vcmi/Environment.h | 3 +- lib/NetPacks.h | 4 +- lib/NetPacksLib.cpp | 2 +- lib/battle/BattleInfo.cpp | 67 +++++++------ lib/battle/BattleInfo.h | 16 ++- lib/battle/BattleProxy.cpp | 15 ++- lib/battle/BattleProxy.h | 2 + lib/battle/CBattleInfoCallback.cpp | 21 ++++ lib/battle/CBattleInfoCallback.h | 2 + lib/battle/CBattleInfoEssentials.cpp | 15 ++- lib/battle/CBattleInfoEssentials.h | 3 +- lib/battle/CCallbackBase.cpp | 15 --- lib/battle/CCallbackBase.h | 11 +-- lib/battle/CPlayerBattleCallback.cpp | 8 +- lib/battle/IBattleInfoCallback.h | 4 + lib/battle/IBattleState.h | 12 ++- server/CGameHandler.cpp | 4 +- server/CGameHandler.h | 7 +- server/NetPacksServer.cpp | 2 +- server/battles/BattleActionProcessor.cpp | 119 ++++++++++++----------- server/battles/BattleActionProcessor.h | 50 +++++----- server/battles/BattleFlowProcessor.cpp | 105 ++++++++++---------- server/battles/BattleFlowProcessor.h | 42 ++++---- server/battles/BattleProcessor.cpp | 25 ++--- server/battles/BattleProcessor.h | 11 +-- server/battles/BattleResultProcessor.cpp | 118 +++++++++++++--------- server/battles/BattleResultProcessor.h | 12 +-- server/battles/ServerBattleCallback.cpp | 0 server/battles/ServerBattleCallback.h | 0 server/queries/BattleQueries.cpp | 42 ++++---- server/queries/BattleQueries.h | 12 ++- 34 files changed, 437 insertions(+), 346 deletions(-) create mode 100644 server/battles/ServerBattleCallback.cpp create mode 100644 server/battles/ServerBattleCallback.h diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index c67eab4db..ec65c194d 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -390,7 +390,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) for(auto & round : queue) { if(!firstRound) - state->nextRound(0);//todo: set actual value? + state->nextRound(); for(auto unit : round) { if(!vstd::contains(values, unit->unitId())) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index c80ec1dce..0a15c68d3 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -325,7 +325,7 @@ int32_t HypotheticBattle::getActiveStackID() const return activeUnitId; } -void HypotheticBattle::nextRound(int32_t roundNr) +void HypotheticBattle::nextRound() { //TODO:HypotheticBattle::nextRound for(auto unit : battleAliveUnits()) @@ -462,6 +462,24 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at return (damage.min + damage.max) / 2; } +std::vector HypotheticBattle::getUsedSpells(ui8 side) const +{ + // TODO + return {}; +} + +int3 HypotheticBattle::getLocation() const +{ + // TODO + return int3(-1, -1, -1); +} + +bool HypotheticBattle::isCreatureBank() const +{ + // TODO + return false; +} + int64_t HypotheticBattle::getTreeVersion() const { return getBonusBearer()->getTreeVersion() + bonusTreeVersion; @@ -552,8 +570,9 @@ const Services * HypotheticBattle::HypotheticEnvironment::services() const return env->services(); } -const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle() const +const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle(const BattleID & battleID) const { + assert(battleID == owner->getBattleID()); return owner; } diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index c7692434e..a69bbb887 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -110,11 +110,13 @@ public: std::shared_ptr getForUpdate(uint32_t id); + BattleID getBattleID() const override; + int32_t getActiveStackID() const override; battle::Units getUnitsIf(battle::UnitFilter predicate) const override; - void nextRound(int32_t roundNr) override; + void nextRound() override; void nextTurn(uint32_t unitId) override; void addUnit(uint32_t id, const JsonNode & data) override; @@ -136,6 +138,9 @@ public: uint32_t nextUnitId() const override; int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + std::vector getUsedSpells(ui8 side) const override; + int3 getLocation() const override; + bool isCreatureBank() const override; int64_t getTreeVersion() const; @@ -177,7 +182,7 @@ private: HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment); const Services * services() const override; - const BattleCb * battle() const override; + const BattleCb * battle(const BattleID & battleID) const override; const GameCb * game() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; diff --git a/include/vcmi/Environment.h b/include/vcmi/Environment.h index 8c456bca2..b127e57b3 100644 --- a/include/vcmi/Environment.h +++ b/include/vcmi/Environment.h @@ -16,6 +16,7 @@ class Services; class IGameInfoCallback; class IBattleInfoCallback; +class BattleID; namespace events { @@ -31,7 +32,7 @@ public: virtual ~Environment() = default; virtual const Services * services() const = 0; - virtual const BattleCb * battle() const = 0; + virtual const BattleCb * battle(const BattleID & battleID) const = 0; virtual const GameCb * game() const = 0; virtual vstd::CLoggerBase * logger() const = 0; virtual events::EventBus * eventBus() const = 0; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 060b15e06..e84320b19 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -39,6 +39,7 @@ struct StackLocation; struct ArtSlotInfo; struct QuestInfo; class IBattleState; +class BattleInfo; // This one teleport-specific, but has to be available everywhere in callbacks and netpacks // For now it's will be there till teleports code refactored and moved into own file @@ -1478,7 +1479,6 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query } }; -class BattleInfo; struct DLL_LINKAGE BattleStart : public CPackForClient { void applyGs(CGameState * gs) const; @@ -1500,14 +1500,12 @@ struct DLL_LINKAGE BattleNextRound : public CPackForClient void applyGs(CGameState * gs) const; BattleID battleID = BattleID::NONE; - si32 round = 0; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { h & battleID; - h & round; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 3207cd82d..68179dea8 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2124,7 +2124,7 @@ void BattleStart::applyGs(CGameState * gs) const void BattleNextRound::applyGs(CGameState * gs) const { - gs->getBattle(battleID)->nextRound(round); + gs->getBattle(battleID)->nextRound(); } void BattleSetActiveStack::applyGs(CGameState * gs) const diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 6d6be9913..ea3883b6e 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -26,37 +26,6 @@ VCMI_LIB_NAMESPACE_BEGIN ///BattleInfo -std::pair< std::vector, int > BattleInfo::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const -{ - auto reachability = getReachability(stack); - - if(reachability.predecessors[dest] == -1) //cannot reach destination - { - return std::make_pair(std::vector(), 0); - } - - //making the Path - std::vector path; - BattleHex curElem = dest; - while(curElem != start) - { - path.push_back(curElem); - curElem = reachability.predecessors[curElem]; - } - - return std::make_pair(path, reachability.distances[dest]); -} - -void BattleInfo::calculateCasualties(std::map * casualties) const -{ - for(const auto & st : stacks) //setting casualties - { - si32 killed = st->getKilled(); - if(killed > 0) - casualties[st->unitSide()][st->creatureId()] += killed; - } -} - CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) { PlayerColor owner = sides[side].color; @@ -560,10 +529,24 @@ BattleInfo::BattleInfo(): tacticsSide(0), tacticDistance(0) { - setBattle(this); setNodeType(BATTLE); } +BattleID BattleInfo::getBattleID() const +{ + return battleID; +} + +const IBattleInfo * BattleInfo::getBattle() const +{ + return this; +} + +std::optional BattleInfo::getPlayerID() const +{ + return std::nullopt; +} + BattleInfo::~BattleInfo() { for (auto & elem : stacks) @@ -689,14 +672,30 @@ int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attacker } } -void BattleInfo::nextRound(int32_t roundNr) +int3 BattleInfo::getLocation() const +{ + return tile; +} + +bool BattleInfo::isCreatureBank() const +{ + return creatureBank; +} + + +std::vector BattleInfo::getUsedSpells(ui8 side) const +{ + return sides.at(side).usedSpellsHistory; +} + +void BattleInfo::nextRound() { for(int i = 0; i < 2; ++i) { sides.at(i).castSpellsCount = 0; vstd::amax(--sides.at(i).enchanterCounter, 0); } - round = roundNr; + round += 1; for(CStack * s : stacks) { diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 2b0006836..ff30dbe76 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -75,9 +75,14 @@ public: BattleInfo(); virtual ~BattleInfo(); + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + ////////////////////////////////////////////////////////////////////////// // IBattleInfo + BattleID getBattleID() const override; + int32_t getActiveStackID() const override; TStacks getStacksIf(TStackFilter predicate) const override; @@ -109,10 +114,15 @@ public: int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + int3 getLocation() const override; + bool isCreatureBank() const override; + + std::vector getUsedSpells(ui8 side) const override; + ////////////////////////////////////////////////////////////////////////// // IBattleState - void nextRound(int32_t roundNr) override; + void nextRound() override; void nextTurn(uint32_t unitId) override; void addUnit(uint32_t id, const JsonNode & data) override; @@ -140,10 +150,6 @@ public: using CBattleInfoEssentials::battleGetFightingHero; CGHeroInstance * battleGetFightingHero(ui8 side) const; - std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; //returned value: pair; length may be different than number of elements in path since flying creatures jump between distant hexes - - void calculateCasualties(std::map * casualties) const; //casualties are array of maps size 2 (attacker, defeneder), maps are (crid => amount) - CStack * generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position); CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position); diff --git a/lib/battle/BattleProxy.cpp b/lib/battle/BattleProxy.cpp index f62a5a073..9ec4339d4 100644 --- a/lib/battle/BattleProxy.cpp +++ b/lib/battle/BattleProxy.cpp @@ -17,13 +17,20 @@ VCMI_LIB_NAMESPACE_BEGIN BattleProxy::BattleProxy(Subject subject_): subject(std::move(subject_)) -{ - setBattle(this); - player = subject->getPlayerID(); -} +{} BattleProxy::~BattleProxy() = default; +const IBattleInfo * BattleProxy::getBattle() const +{ + return this; +} + +std::optional BattleProxy::getPlayerID() const +{ + return subject->getPlayerID(); +} + int32_t BattleProxy::getActiveStackID() const { const auto * ret = subject->battleActiveUnit(); diff --git a/lib/battle/BattleProxy.h b/lib/battle/BattleProxy.h index da0880528..d52d4231e 100644 --- a/lib/battle/BattleProxy.h +++ b/lib/battle/BattleProxy.h @@ -24,6 +24,8 @@ public: ////////////////////////////////////////////////////////////////////////// // IBattleInfo + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; int32_t getActiveStackID() const override; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 505c69297..5655c017d 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -136,6 +136,27 @@ ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * return ESpellCastProblem::OK; } +std::pair< std::vector, int > CBattleInfoCallback::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const +{ + auto reachability = getReachability(stack); + + if(reachability.predecessors[dest] == -1) //cannot reach destination + { + return std::make_pair(std::vector(), 0); + } + + //making the Path + std::vector path; + BattleHex curElem = dest; + while(curElem != start) + { + path.push_back(curElem); + curElem = reachability.predecessors[curElem]; + } + + return std::make_pair(path, reachability.distances[dest]); +} + bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const { auto isTileBlocked = [&](BattleHex tile) diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index ae76c04d2..a2b197f2e 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -89,6 +89,8 @@ public: std::set battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const; bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const; + std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; + bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index 130d6ad20..36d827e80 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -17,6 +17,11 @@ VCMI_LIB_NAMESPACE_BEGIN +bool CBattleInfoEssentials::duringBattle() const +{ + return getBattle() != nullptr; +} + TerrainId CBattleInfoEssentials::battleTerrainType() const { RETURN_IF_NOT_BATTLE(TerrainId()); @@ -47,7 +52,7 @@ std::vector> CBattleInfoEssentials::bat } else { - if(!!player && *perspective != battleGetMySide()) + if(!!getPlayerID() && *perspective != battleGetMySide()) logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell"); } @@ -157,14 +162,14 @@ const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const { RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); - if(!player || player->isSpectator()) + if(!getPlayerID() || getPlayerID()->isSpectator()) return BattlePerspective::ALL_KNOWING; - if(*player == getBattle()->getSidePlayer(BattleSide::ATTACKER)) + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER)) return BattlePerspective::LEFT_SIDE; - if(*player == getBattle()->getSidePlayer(BattleSide::DEFENDER)) + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER)) return BattlePerspective::RIGHT_SIDE; - logGlobal->error("Cannot find player %s in battle!", player->toString()); + logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString()); return BattlePerspective::INVALID; } diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index d5d198150..18456c345 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -34,7 +34,7 @@ namespace BattlePerspective }; } -class DLL_LINKAGE CBattleInfoEssentials : public virtual CCallbackBase, public IBattleInfoCallback +class DLL_LINKAGE CBattleInfoEssentials : public IBattleInfoCallback { protected: bool battleDoWeKnowAbout(ui8 side) const; @@ -45,6 +45,7 @@ public: ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY }; + bool duringBattle() const; BattlePerspective::BattlePerspective battleGetMySide() const; const IBonusBearer * getBonusBearer() const override; diff --git a/lib/battle/CCallbackBase.cpp b/lib/battle/CCallbackBase.cpp index fb638793c..0693548b4 100644 --- a/lib/battle/CCallbackBase.cpp +++ b/lib/battle/CCallbackBase.cpp @@ -13,26 +13,11 @@ VCMI_LIB_NAMESPACE_BEGIN -bool CCallbackBase::duringBattle() const -{ - return getBattle() != nullptr; -} - -const IBattleInfo * CCallbackBase::getBattle() const -{ - return battle; -} - CCallbackBase::CCallbackBase(std::optional Player): player(std::move(Player)) { } -void CCallbackBase::setBattle(const IBattleInfo * B) -{ - battle = B; -} - std::optional CCallbackBase::getPlayerID() const { return player; diff --git a/lib/battle/CCallbackBase.h b/lib/battle/CCallbackBase.h index dc5ac95eb..16e408974 100644 --- a/lib/battle/CCallbackBase.h +++ b/lib/battle/CCallbackBase.h @@ -11,7 +11,7 @@ #include "../GameConstants.h" #define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } -#define ASSERT_IF_CALLED_WITH_PLAYER if(!player) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} +#define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} VCMI_LIB_NAMESPACE_BEGIN @@ -19,26 +19,17 @@ class IBattleInfo; class BattleInfo; class CBattleInfoEssentials; - //Basic class for various callbacks (interfaces called by players to get info about game and so forth) class DLL_LINKAGE CCallbackBase { - const IBattleInfo * battle = nullptr; //battle to which the player is engaged, nullptr if none or not applicable - protected: std::optional player; // not set gives access to all information, otherwise callback provides only information "visible" for player CCallbackBase(std::optional Player); CCallbackBase() = default; - const IBattleInfo * getBattle() const; - void setBattle(const IBattleInfo * B); - bool duringBattle() const; - public: std::optional getPlayerID() const; - - friend class CBattleInfoEssentials; }; diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index 6aaafcc12..08e9112c0 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -18,7 +18,7 @@ bool CPlayerBattleCallback::battleCanFlee() const { RETURN_IF_NOT_BATTLE(false); ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoEssentials::battleCanFlee(*player); + return CBattleInfoEssentials::battleCanFlee(*getPlayerID()); } TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const @@ -30,8 +30,8 @@ TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyA return battleGetStacksIf([=](const CStack * s){ const bool ownerMatches = (whose == MINE_AND_ENEMY) - || (whose == ONLY_MINE && s->unitOwner() == player) - || (whose == ONLY_ENEMY && s->unitOwner() != player); + || (whose == ONLY_MINE && s->unitOwner() == getPlayerID()) + || (whose == ONLY_ENEMY && s->unitOwner() != getPlayerID()); return ownerMatches && s->isValidTarget(!onlyAlive); }); @@ -41,7 +41,7 @@ int CPlayerBattleCallback::battleGetSurrenderCost() const { RETURN_IF_NOT_BATTLE(-3) ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoCallback::battleGetSurrenderCost(*player); + return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID()); } const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 984bf9093..cc03c4ab5 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -19,6 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct CObstacleInstance; class BattleField; +class IBattleInfo; namespace battle { @@ -54,6 +55,9 @@ public: virtual scripting::Pool * getContextPool() const = 0; #endif + virtual const IBattleInfo * getBattle() const = 0; + virtual std::optional getPlayerID() const = 0; + virtual TerrainId battleTerrainType() const = 0; virtual BattleField battleGetBattlefieldType() const = 0; diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 3f43eb0fc..8af3ad429 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -19,6 +19,7 @@ struct Bonus; class JsonNode; class JsonSerializeFormat; class BattleField; +class int3; namespace vstd { @@ -37,6 +38,8 @@ public: virtual ~IBattleInfo() = default; + virtual BattleID getBattleID() const = 0; + virtual int32_t getActiveStackID() const = 0; virtual TStacks getStacksIf(TStackFilter predicate) const = 0; @@ -55,6 +58,8 @@ public: virtual PlayerColor getSidePlayer(ui8 side) const = 0; virtual const CArmedInstance * getSideArmy(ui8 side) const = 0; virtual const CGHeroInstance * getSideHero(ui8 side) const = 0; + /// Returns list of all spells used by specified side (and that can be learned by opposite hero) + virtual std::vector getUsedSpells(ui8 side) const = 0; virtual uint32_t getCastSpells(ui8 side) const = 0; virtual int32_t getEnchanterCounter(ui8 side) const = 0; @@ -65,14 +70,15 @@ public: virtual uint32_t nextUnitId() const = 0; virtual int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const = 0; + + virtual int3 getLocation() const = 0; + virtual bool isCreatureBank() const = 0; }; class DLL_LINKAGE IBattleState : public IBattleInfo { public: - //TODO: add non-const API - - virtual void nextRound(int32_t roundNr) = 0; + virtual void nextRound() = 0; virtual void nextTurn(uint32_t unitId) = 0; virtual void addUnit(uint32_t id, const JsonNode & data) = 0; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3ce15293d..3ae02b83b 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -136,9 +136,9 @@ const Services * CGameHandler::services() const return VLC; } -const CGameHandler::BattleCb * CGameHandler::battle() const +const CGameHandler::BattleCb * CGameHandler::battle(const BattleID & battleID) const { - return this; + return gs->getBattle(battleID); } const CGameHandler::GameCb * CGameHandler::game() const diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 3e9ad7b1e..75a9cfd3a 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -12,7 +12,6 @@ #include #include "../lib/IGameCallback.h" -#include "../lib/battle/CBattleInfoCallback.h" #include "../lib/LoadProgress.h" #include "../lib/ScriptHandler.h" #include "TurnTimerHandler.h" @@ -53,14 +52,12 @@ class TurnOrderProcessor; class QueriesProcessor; class CObjectVisitQuery; -class CGameHandler : public IGameCallback, public CBattleInfoCallback, public Environment +class CGameHandler : public IGameCallback, public Environment { CVCMIServer * lobby; std::shared_ptr> applier; public: - using CCallbackBase::setBattle; - std::unique_ptr heroPool; std::unique_ptr battles; std::unique_ptr queries; @@ -84,7 +81,7 @@ public: TurnTimerHandler turnTimerHandler; const Services * services() const override; - const BattleCb * battle() const override; + const BattleCb * battle(const BattleID & battleID) const override; const GameCb * game() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index a4c622948..c173f2169 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -20,7 +20,7 @@ #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/gameState/CGameState.h" -#include "../lib/battle/BattleInfo.h" +#include "../lib/battle/IBattleState.h" #include "../lib/battle/BattleAction.h" #include "../lib/battle/Unit.h" #include "../lib/serializer/Connection.h" diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index c869b8dcd..fdda43ac6 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -17,7 +17,8 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CStack.h" #include "../../lib/GameSettings.h" -#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/IBattleState.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/NetPacks.h" @@ -37,19 +38,19 @@ void BattleActionProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -bool BattleActionProcessor::doEmptyAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba) { return true; } -bool BattleActionProcessor::doEndTacticsAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba) { return true; } -bool BattleActionProcessor::doWaitAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); if (!canStackAct(battle, stack)) return false; @@ -57,9 +58,9 @@ bool BattleActionProcessor::doWaitAction(const BattleInfo & battle, const Battle return true; } -bool BattleActionProcessor::doRetreatAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - if (!battle.battleCanFlee(battle.sides.at(ba.side).color)) + if (!battle.battleCanFlee(battle.sideToPlayer(ba.side))) { gameHandler->complain("Cannot retreat!"); return false; @@ -69,9 +70,9 @@ bool BattleActionProcessor::doRetreatAction(const BattleInfo & battle, const Bat return true; } -bool BattleActionProcessor::doSurrenderAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - PlayerColor player = battle.sides.at(ba.side).color; + PlayerColor player = battle.sideToPlayer(ba.side); int cost = battle.battleGetSurrenderCost(player); if (cost < 0) { @@ -90,7 +91,7 @@ bool BattleActionProcessor::doSurrenderAction(const BattleInfo & battle, const B return true; } -bool BattleActionProcessor::doHeroSpellAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba) { const CGHeroInstance *h = battle.battleGetFightingHero(ba.side); if (!h) @@ -127,9 +128,9 @@ bool BattleActionProcessor::doHeroSpellAction(const BattleInfo & battle, const B return true; } -bool BattleActionProcessor::doWalkAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); if (!canStackAct(battle, stack)) @@ -150,9 +151,9 @@ bool BattleActionProcessor::doWalkAction(const BattleInfo & battle, const Battle return true; } -bool BattleActionProcessor::doDefendAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); if (!canStackAct(battle, stack)) return false; @@ -199,9 +200,9 @@ bool BattleActionProcessor::doDefendAction(const BattleInfo & battle, const Batt return true; } -bool BattleActionProcessor::doAttackAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); if (!canStackAct(battle, stack)) @@ -302,9 +303,9 @@ bool BattleActionProcessor::doAttackAction(const BattleInfo & battle, const Batt return true; } -bool BattleActionProcessor::doShootAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba) { - const CStack * stack = gameHandler->battleGetStackByID(ba.stackNumber); + const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); if (!canStackAct(battle, stack)) @@ -369,7 +370,7 @@ bool BattleActionProcessor::doShootAction(const BattleInfo & battle, const Battl return true; } -bool BattleActionProcessor::doCatapultAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba) { const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); @@ -393,7 +394,7 @@ bool BattleActionProcessor::doCatapultAction(const BattleInfo & battle, const Ba return true; } -bool BattleActionProcessor::doUnitSpellAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba) { const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); @@ -406,8 +407,8 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleInfo & battle, const B std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); //TODO special bonus for genies ability - if (randSpellcaster && gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) - spellID = gameHandler->battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); + if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) + spellID = battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE); if (spellID == SpellID::NONE) gameHandler->complain("That stack can't cast spells!"); @@ -426,7 +427,7 @@ bool BattleActionProcessor::doUnitSpellAction(const BattleInfo & battle, const B return true; } -bool BattleActionProcessor::doHealAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba) { const CStack * stack = battle.battleGetStackByID(ba.stackNumber); battle::Target target = ba.getTarget(&battle); @@ -463,7 +464,7 @@ bool BattleActionProcessor::doHealAction(const BattleInfo & battle, const Battle return true; } -bool BattleActionProcessor::canStackAct(const BattleInfo & battle, const CStack * stack) +bool BattleActionProcessor::canStackAct(const CBattleInfoCallback & battle, const CStack * stack) { if (!stack) { @@ -476,9 +477,9 @@ bool BattleActionProcessor::canStackAct(const BattleInfo & battle, const CStack return false; } - if (gameHandler->battleTacticDist()) + if (battle.battleTacticDist()) { - if (stack && stack->unitSide() != gameHandler->battleGetTacticsSide()) + if (stack && stack->unitSide() != battle.battleGetTacticsSide()) { gameHandler->complain("This is not a stack of side that has tactics!"); return false; @@ -486,7 +487,7 @@ bool BattleActionProcessor::canStackAct(const BattleInfo & battle, const CStack } else { - if (stack->unitId() != battle.getActiveStackID()) + if (stack != battle.battleActiveUnit()) { gameHandler->complain("Action has to be about active stack!"); return false; @@ -495,7 +496,7 @@ bool BattleActionProcessor::canStackAct(const BattleInfo & battle, const CStack return true; } -bool BattleActionProcessor::dispatchBattleAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba) { switch(ba.actionType) { @@ -531,7 +532,7 @@ bool BattleActionProcessor::dispatchBattleAction(const BattleInfo & battle, cons return false; } -bool BattleActionProcessor::makeBattleActionImpl(const BattleInfo & battle, const BattleAction &ba) +bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction &ba) { logGlobal->trace("Making action: %s", ba.toString()); const CStack * stack = battle.battleGetStackByID(ba.stackNumber); @@ -552,22 +553,22 @@ bool BattleActionProcessor::makeBattleActionImpl(const BattleInfo & battle, cons } if(ba.actionType == EActionType::WAIT || ba.actionType == EActionType::DEFEND || ba.actionType == EActionType::SHOOT || ba.actionType == EActionType::MONSTER_SPELL) - gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); + battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *stack); return result; } -int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, BattleHex dest) +int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest) { int ret = 0; - const CStack *curStack = gameHandler->battleGetStackByID(stack); + const CStack *curStack = battle.battleGetStackByID(stack); const CStack *stackAtEnd = battle.battleGetStackByPos(dest); assert(curStack); assert(dest < GameConstants::BFIELD_SIZE); - if (battle.tacticDistance) + if (battle.battleGetTacticDist()) { assert(battle.isInTacticRange(dest)); } @@ -577,7 +578,7 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl return 0; //initing necessary tables - auto accessibility = gameHandler->getAccesibility(curStack); + auto accessibility = battle.getAccesibility(curStack); std::set passed; //Ignore obstacles on starting position passed.insert(curStack->getPosition()); @@ -600,8 +601,8 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl } bool canUseGate = false; - auto dbState = battle.si.gateState; - if(gameHandler->battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && + auto dbState = battle.battleGetGateState(); + if(battle.battleGetSiegeLevel() > 0 && curStack->unitSide() == BattleSide::DEFENDER && dbState != EGateState::DESTROYED && dbState != EGateState::BLOCKED) { @@ -614,10 +615,10 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl int creSpeed = curStack->speed(0, true); - if (battle.tacticDistance > 0 && creSpeed > 0) + if (battle.battleGetTacticDist() > 0 && creSpeed > 0) creSpeed = GameConstants::BFIELD_SIZE; - bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + bool hasWideMoat = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) { return obst->obstacleType == CObstacleInstance::MOAT; }); @@ -772,14 +773,14 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl } //if we walked onto something, finalize this portion of stack movement check into obstacle - if(!gameHandler->battleGetAllObstaclesOnPos(hex, false).empty()) + if(!battle.battleGetAllObstaclesOnPos(hex, false).empty()) obstacleHit = true; if (curStack->doubleWide()) { BattleHex otherHex = curStack->occupiedHex(hex); //two hex creature hit obstacle by backside - auto obstacle2 = gameHandler->battleGetAllObstaclesOnPos(otherHex, false); + auto obstacle2 = battle.battleGetAllObstaclesOnPos(otherHex, false); if(otherHex.isValid() && !obstacle2.empty()) obstacleHit = true; } @@ -805,7 +806,7 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl { if(stackIsMoving && start != curStack->getPosition()) { - stackIsMoving = gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + stackIsMoving = battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); passed.insert(curStack->getPosition()); if(curStack->doubleWide()) passed.insert(curStack->occupiedHex()); @@ -843,12 +844,12 @@ int BattleActionProcessor::moveStack(const BattleInfo & battle, int stack, Battl passed.clear(); //Just empty passed, obstacles will handled automatically } //handling obstacle on the final field (separate, because it affects both flying and walking stacks) - gameHandler->handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); + battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); return ret; } -void BattleActionProcessor::makeAttack(const BattleInfo & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) +void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter) { if(first && !counter) handleAttackBeforeCasting(battle, ranged, attacker, defender); @@ -891,7 +892,7 @@ void BattleActionProcessor::makeAttack(const BattleInfo & battle, const CStack * bat.flags |= BattleAttack::DEATH_BLOW; } - const auto * owner = battle.getHero(attacker->unitOwner()); + const auto * owner = battle.battleGetFightingHero(attacker->unitOwner()); if(owner) { int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); @@ -1012,7 +1013,7 @@ void BattleActionProcessor::makeAttack(const BattleInfo & battle, const CStack * const CStack * actor = item.first; int64_t rawDamage = item.second; - const CGHeroInstance * actorOwner = battle.getHero(actor->unitOwner()); + const CGHeroInstance * actorOwner = battle.battleGetFightingHero(actor->unitOwner()); if(actorOwner) { @@ -1058,7 +1059,7 @@ void BattleActionProcessor::makeAttack(const BattleInfo & battle, const CStack * handleAfterAttackCasting(battle, ranged, attacker, defender); } -void BattleActionProcessor::attackCasting(const BattleInfo & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) +void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender) { if(attacker->hasBonusOfType(attackMode)) { @@ -1126,12 +1127,12 @@ void BattleActionProcessor::attackCasting(const BattleInfo & battle, bool ranged } } -void BattleActionProcessor::handleAttackBeforeCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender) +void BattleActionProcessor::handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) { attackCasting(battle, ranged, BonusType::SPELL_BEFORE_ATTACK, attacker, defender); //no death stare / acid breath needed? } -void BattleActionProcessor::handleAfterAttackCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender) +void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender) { if(!attacker->alive() || !defender->alive()) // can be already dead return; @@ -1286,7 +1287,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const BattleInfo & battle, } } -int64_t BattleActionProcessor::applyBattleEffects(const BattleInfo & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) +int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary) { BattleStackAttacked bsa; if(secondary) @@ -1303,7 +1304,7 @@ int64_t BattleActionProcessor::applyBattleEffects(const BattleInfo & battle, Bat bai.unluckyStrike = bat.unlucky(); auto range = battle.calculateDmgRange(bai); - bsa.damageAmount = battle.getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); + bsa.damageAmount = battle.getBattle()->getActualDamage(range.damage, attackerState->getCount(), gameHandler->getRandomGenerator()); CStack::prepareAttacked(bsa, gameHandler->getRandomGenerator(), bai.defender->acquireState()); //calculate casualties } @@ -1390,17 +1391,17 @@ void BattleActionProcessor::addGenericKilledLog(BattleLogMessage & blm, const CS } } -bool BattleActionProcessor::makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba) +bool BattleActionProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba) { return makeBattleActionImpl(battle, ba); } -bool BattleActionProcessor::makePlayerBattleAction(const BattleInfo & battle, PlayerColor player, const BattleAction &ba) +bool BattleActionProcessor::makePlayerBattleAction(const CBattleInfoCallback & battle, PlayerColor player, const BattleAction &ba) { if (ba.side != 0 && ba.side != 1 && gameHandler->complain("Can not make action - invalid battle side!")) return false; - if(battle.tacticDistance != 0) + if(battle.battleGetTacticDist() != 0) { if(!ba.isTacticsAction()) { @@ -1408,7 +1409,7 @@ bool BattleActionProcessor::makePlayerBattleAction(const BattleInfo & battle, Pl return false; } - if(player != battle.sides[ba.side].color) + if(player != battle.sideToPlayer(ba.side)) { gameHandler->complain("Can not make actions in battles you are not part of!"); return false; @@ -1416,16 +1417,16 @@ bool BattleActionProcessor::makePlayerBattleAction(const BattleInfo & battle, Pl } else { - if (ba.isUnitAction() && ba.stackNumber != battle.getActiveStackID()) + auto active = battle.battleActiveUnit(); + if(!active && gameHandler->complain("No active unit in battle!")) + return false; + + if (ba.isUnitAction() && ba.stackNumber != active->unitId()) { gameHandler->complain("Can not make actions - stack is not active!"); return false; } - auto active = battle.battleActiveUnit(); - if(!active && gameHandler->complain("No active unit in battle!")) - return false; - auto unitOwner = battle.battleGetOwner(active); if(player != unitOwner && gameHandler->complain("Can not make actions in battles you are not part of!")) diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index e527751b1..11d83ebc9 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct BattleLogMessage; struct BattleAttack; class BattleAction; -class BattleInfo; +class CBattleInfoCallback; struct BattleHex; class CStack; class PlayerColor; @@ -39,42 +39,42 @@ class BattleActionProcessor : boost::noncopyable BattleProcessor * owner; CGameHandler * gameHandler; - int moveStack(const BattleInfo & battle, int stack, BattleHex dest); //returned value - travelled distance - void makeAttack(const BattleInfo & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); + int moveStack(const CBattleInfoCallback & battle, int stack, BattleHex dest); //returned value - travelled distance + void makeAttack(const CBattleInfoCallback & battle, const CStack * attacker, const CStack * defender, int distance, BattleHex targetHex, bool first, bool ranged, bool counter); - void handleAttackBeforeCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender); - void handleAfterAttackCasting(const BattleInfo & battle, bool ranged, const CStack * attacker, const CStack * defender); - void attackCasting(const BattleInfo & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); + void handleAttackBeforeCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); + void handleAfterAttackCasting(const CBattleInfoCallback & battle, bool ranged, const CStack * attacker, const CStack * defender); + void attackCasting(const CBattleInfoCallback & battle, bool ranged, BonusType attackMode, const battle::Unit * attacker, const battle::Unit * defender); // damage, drain life & fire shield; returns amount of drained life - int64_t applyBattleEffects(const BattleInfo & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); + int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); - bool canStackAct(const BattleInfo & battle, const CStack * stack); + bool canStackAct(const CBattleInfoCallback & battle, const CStack * stack); - bool doEmptyAction(const BattleInfo & battle, const BattleAction & ba); - bool doEndTacticsAction(const BattleInfo & battle, const BattleAction & ba); - bool doRetreatAction(const BattleInfo & battle, const BattleAction & ba); - bool doSurrenderAction(const BattleInfo & battle, const BattleAction & ba); - bool doHeroSpellAction(const BattleInfo & battle, const BattleAction & ba); - bool doWalkAction(const BattleInfo & battle, const BattleAction & ba); - bool doWaitAction(const BattleInfo & battle, const BattleAction & ba); - bool doDefendAction(const BattleInfo & battle, const BattleAction & ba); - bool doAttackAction(const BattleInfo & battle, const BattleAction & ba); - bool doShootAction(const BattleInfo & battle, const BattleAction & ba); - bool doCatapultAction(const BattleInfo & battle, const BattleAction & ba); - bool doUnitSpellAction(const BattleInfo & battle, const BattleAction & ba); - bool doHealAction(const BattleInfo & battle, const BattleAction & ba); + bool doEmptyAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doEndTacticsAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doRetreatAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doSurrenderAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doHeroSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doWalkAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doWaitAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doDefendAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doAttackAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doShootAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doCatapultAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doUnitSpellAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool doHealAction(const CBattleInfoCallback & battle, const BattleAction & ba); - bool dispatchBattleAction(const BattleInfo & battle, const BattleAction & ba); - bool makeBattleActionImpl(const BattleInfo & battle, const BattleAction & ba); + bool dispatchBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool makeBattleActionImpl(const CBattleInfoCallback & battle, const BattleAction & ba); public: explicit BattleActionProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba); - bool makePlayerBattleAction(const BattleInfo & battle, PlayerColor player, const BattleAction & ba); + bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); + bool makePlayerBattleAction(const CBattleInfoCallback & battle, PlayerColor player, const BattleAction & ba); }; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 1253b0b42..075d12723 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -16,7 +16,8 @@ #include "../../lib/CStack.h" #include "../../lib/GameSettings.h" -#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/IBattleState.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/NetPacks.h" @@ -35,7 +36,7 @@ void BattleFlowProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -void BattleFlowProcessor::summonGuardiansHelper(const BattleInfo & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard +void BattleFlowProcessor::summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex) //return hexes for summoning two hex monsters in output, target = unit to guard { int x = targetPosition.getX(); int y = targetPosition.getY(); @@ -110,39 +111,39 @@ void BattleFlowProcessor::summonGuardiansHelper(const BattleInfo & battle, std:: } } -void BattleFlowProcessor::tryPlaceMoats(const BattleInfo & battle) +void BattleFlowProcessor::tryPlaceMoats(const CBattleInfoCallback & battle) { + const auto * town = battle.battleGetDefendedTown(); + //Moat should be initialized here, because only here we can use spellcasting - if (battle.town && battle.town->fortLevel() >= CGTownInstance::CITADEL) + if (town && town->fortLevel() >= CGTownInstance::CITADEL) { const auto * h = battle.battleGetFightingHero(BattleSide::DEFENDER); const auto * actualCaster = h ? static_cast(h) : nullptr; - auto moatCaster = spells::SilentCaster(battle.getSidePlayer(BattleSide::DEFENDER), actualCaster); - auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, battle.town->town->moatAbility.toSpell()); + auto moatCaster = spells::SilentCaster(battle.sideToPlayer(BattleSide::DEFENDER), actualCaster); + auto cast = spells::BattleCast(&battle, &moatCaster, spells::Mode::PASSIVE, town->town->moatAbility.toSpell()); auto target = spells::Target(); cast.cast(gameHandler->spellEnv, target); } } -void BattleFlowProcessor::onBattleStarted(const BattleInfo & battle) +void BattleFlowProcessor::onBattleStarted(const CBattleInfoCallback & battle) { - gameHandler->setBattle(&battle); - tryPlaceMoats(battle); - gameHandler->turnTimerHandler.onBattleStart(battle.battleID); + gameHandler->turnTimerHandler.onBattleStart(battle.getBattle()->getBattleID()); - if (battle.tacticDistance == 0) + if (battle.battleGetTacticDist() == 0) onTacticsEnded(battle); } -void BattleFlowProcessor::trySummonGuardians(const BattleInfo & battle, const CStack * stack) +void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack) { if (!stack->hasBonusOfType(BonusType::SUMMON_GUARDIANS)) return; std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); - auto accessibility = gameHandler->getAccesibility(); + auto accessibility = battle.getAccesibility(); CreatureID creatureData = CreatureID(summonInfo->subtype); std::vector targetHexes; const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard @@ -177,7 +178,7 @@ void BattleFlowProcessor::trySummonGuardians(const BattleInfo & battle, const CS } } -void BattleFlowProcessor::castOpeningSpells(const BattleInfo & battle) +void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle) { for (int i = 0; i < 2; ++i) { @@ -202,12 +203,12 @@ void BattleFlowProcessor::castOpeningSpells(const BattleInfo & battle) } } -void BattleFlowProcessor::onTacticsEnded(const BattleInfo & battle) +void BattleFlowProcessor::onTacticsEnded(const CBattleInfoCallback & battle) { //initial stacks appearance triggers, e.g. built-in bonus spells - auto initialStacks = battle.stacks; //use temporary variable to outclude summoned stacks added to battle.stacks from processing + auto initialStacks = battle.battleGetAllStacks(true); - for (CStack * stack : initialStacks) + for (const CStack * stack : initialStacks) { trySummonGuardians(battle, stack); stackEnchantedTrigger(battle, stack); @@ -223,14 +224,14 @@ void BattleFlowProcessor::onTacticsEnded(const BattleInfo & battle) activateNextStack(battle); } -void BattleFlowProcessor::startNextRound(const BattleInfo & battle, bool isFirstRound) +void BattleFlowProcessor::startNextRound(const CBattleInfoCallback & battle, bool isFirstRound) { BattleNextRound bnr; - bnr.round = battle.round + 1; - logGlobal->debug("Round %d", bnr.round); + logGlobal->debug("Next round starts"); gameHandler->sendAndApply(&bnr); - auto obstacles = battle.obstacles; //we copy container, because we're going to modify it + // operate on copy - removing obstacles will invalidate iterator on 'battle' container + auto obstacles = battle.battleGetAllObstacles(); for (auto &obstPtr : obstacles) { if (const SpellCreatedObstacle *sco = dynamic_cast(obstPtr.get())) @@ -238,16 +239,14 @@ void BattleFlowProcessor::startNextRound(const BattleInfo & battle, bool isFirst removeObstacle(battle, *obstPtr); } - const BattleInfo & curB = *&battle; - - for(auto stack : curB.stacks) + for(auto stack : battle.battleGetAllStacks(true)) { if(stack->alive() && !isFirstRound) stackEnchantedTrigger(battle, stack); } } -const CStack * BattleFlowProcessor::getNextStack(const BattleInfo & battle) +const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & battle) { std::vector q; battle.battleGetTurnOrder(q, 1, 0, -1); //todo: get rid of "turn -1" @@ -283,7 +282,7 @@ const CStack * BattleFlowProcessor::getNextStack(const BattleInfo & battle) return stack; } -void BattleFlowProcessor::activateNextStack(const BattleInfo & battle) +void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle) { // Find next stack that requires manual control for (;;) @@ -305,16 +304,17 @@ void BattleFlowProcessor::activateNextStack(const BattleInfo & battle) BattleUnitsChanged removeGhosts; - for(auto stack : battle.stacks) - { - if(stack->ghostPending) - removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); - } + auto pendingGhosts = battle.battleGetStacksIf([](const CStack * stack){ + return stack->ghostPending; + }); + + for(auto stack : pendingGhosts) + removeGhosts.changedStacks.emplace_back(stack->unitId(), UnitChanges::EOperation::REMOVE); if(!removeGhosts.changedStacks.empty()) gameHandler->sendAndApply(&removeGhosts); - gameHandler->turnTimerHandler.onBattleNextStack(battle.battleID, *next); + gameHandler->turnTimerHandler.onBattleNextStack(battle.getBattle()->getBattleID(), *next); if (!tryMakeAutomaticAction(battle, next)) { @@ -324,7 +324,7 @@ void BattleFlowProcessor::activateNextStack(const BattleInfo & battle) } } -bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, const CStack * next) +bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & battle, const CStack * next) { // check for bad morale => freeze int nextStackMorale = next->moraleVal(); @@ -370,7 +370,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, cons return true; } - const CGHeroInstance * curOwner = gameHandler->battleGetOwnerHero(next); + const CGHeroInstance * curOwner = battle.battleGetOwnerHero(next); const int stackCreatureId = next->unitType()->getId(); if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) @@ -385,7 +385,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, cons const battle::Unit * target = nullptr; - for(auto & elem : battle.stacks) + for(auto & elem : battle.battleGetAllStacks(true)) { if(elem->unitType()->getId() != CreatureID::CATAPULT && elem->unitOwner() != next->unitOwner() @@ -433,7 +433,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, cons if (next->unitType()->getId() == CreatureID::FIRST_AID_TENT) { - TStacks possibleStacks = gameHandler->battleGetStacksIf([=](const CStack * s) + TStacks possibleStacks = battle.battleGetStacksIf([=](const CStack * s) { return s->unitOwner() == next->unitOwner() && s->canBeHealed(); }); @@ -470,7 +470,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const BattleInfo & battle, cons return false; } -bool BattleFlowProcessor::rollGoodMorale(const BattleInfo & battle, const CStack * next) +bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, const CStack * next) { //check for good morale auto nextStackMorale = next->moraleVal(); @@ -498,10 +498,10 @@ bool BattleFlowProcessor::rollGoodMorale(const BattleInfo & battle, const CStack return false; } -void BattleFlowProcessor::onActionMade(const BattleInfo & battle, const BattleAction &ba) +void BattleFlowProcessor::onActionMade(const CBattleInfoCallback & battle, const BattleAction &ba) { - const CStack * actedStack = battle.battleGetStackByID(ba.stackNumber, false); - const CStack * activeStack = battle.battleGetStackByID(battle.getActiveStackID(), false); + const auto * actedStack = battle.battleGetStackByID(ba.stackNumber, false); + const auto * activeStack = battle.battleActiveUnit(); if (ba.actionType == EActionType::END_TACTIC_PHASE) { onTacticsEnded(battle); @@ -516,7 +516,7 @@ void BattleFlowProcessor::onActionMade(const BattleInfo & battle, const BattleAc return; // tactics - next stack will be selected by player - if(battle.tacticDistance != 0) + if(battle.battleGetTacticDist() != 0) return; if (ba.isUnitAction()) @@ -545,7 +545,7 @@ void BattleFlowProcessor::onActionMade(const BattleInfo & battle, const BattleAc activateNextStack(battle); } -void BattleFlowProcessor::makeStackDoNothing(const BattleInfo & battle, const CStack * next) +void BattleFlowProcessor::makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next) { BattleAction doNothing; doNothing.actionType = EActionType::NO_ACTION; @@ -555,7 +555,7 @@ void BattleFlowProcessor::makeStackDoNothing(const BattleInfo & battle, const CS makeAutomaticAction(battle, next, doNothing); } -bool BattleFlowProcessor::makeAutomaticAction(const BattleInfo & battle, const CStack *stack, BattleAction &ba) +bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle, const CStack *stack, BattleAction &ba) { BattleSetActiveStack bsa; bsa.stack = stack->unitId(); @@ -566,7 +566,7 @@ bool BattleFlowProcessor::makeAutomaticAction(const BattleInfo & battle, const C return ret; } -void BattleFlowProcessor::stackEnchantedTrigger(const BattleInfo & battle, const CStack * st) +void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * st) { auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); for(auto b : bl) @@ -587,7 +587,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const BattleInfo & battle, const if(val > 3) { for(auto s : battle.battleGetAllStacks()) - if(gameHandler->battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied + if(battle.battleMatchOwner(st, s, true) && s->isValidTarget()) //all allied target.emplace_back(s); } else @@ -598,14 +598,14 @@ void BattleFlowProcessor::stackEnchantedTrigger(const BattleInfo & battle, const } } -void BattleFlowProcessor::removeObstacle(const BattleInfo & battle, const CObstacleInstance & obstacle) +void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle) { BattleObstaclesChanged obsRem; obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); gameHandler->sendAndApply(&obsRem); } -void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CStack *st) +void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, const CStack *st) { BattleTriggerEffect bte; bte.stackID = st->unitId(); @@ -662,7 +662,7 @@ void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CSta if(st->hasBonusOfType(BonusType::MANA_DRAIN) && !st->drainedMana) { const PlayerColor opponent = battle.otherPlayer(battle.battleGetOwner(st)); - const CGHeroInstance * opponentHero = battle.getHero(opponent); + const CGHeroInstance * opponentHero = battle.battleGetFightingHero(opponent); if(opponentHero) { ui32 manaDrained = st->valOfBonuses(BonusType::MANA_DRAIN); @@ -679,9 +679,9 @@ void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CSta if (st->isLiving() && !st->hasBonusOfType(BonusType::FEARLESS)) { bool fearsomeCreature = false; - for (CStack * stack : battle.stacks) + for (const CStack * stack : battle.battleGetAllStacks(true)) { - if (gameHandler->battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) + if (battle.battleMatchOwner(st, stack) && stack->alive() && stack->hasBonusOfType(BonusType::FEAR)) { fearsomeCreature = true; break; @@ -697,7 +697,7 @@ void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CSta } } BonusList bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTER))); - int side = battle.whatSide(st->unitOwner()); + int side = *battle.playerToSide(st->unitOwner()); if(st->canCast() && battle.battleGetEnchanterCounter(side) == 0) { bool cast = false; @@ -732,11 +732,10 @@ void BattleFlowProcessor::stackTurnTrigger(const BattleInfo & battle, const CSta } } -void BattleFlowProcessor::setActiveStack(const BattleInfo & battle, const CStack * stack) +void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack) { assert(stack); - logGlobal->trace("Activating %s", stack->nodeName()); BattleSetActiveStack sas; sas.stack = stack->unitId(); gameHandler->sendAndApply(&sas); diff --git a/server/battles/BattleFlowProcessor.h b/server/battles/BattleFlowProcessor.h index 74dde97de..929445701 100644 --- a/server/battles/BattleFlowProcessor.h +++ b/server/battles/BattleFlowProcessor.h @@ -13,8 +13,12 @@ VCMI_LIB_NAMESPACE_BEGIN class CStack; struct BattleHex; class BattleAction; -class BattleInfo; +class CBattleInfoCallback; struct CObstacleInstance; +namespace battle +{ +class Unit; +} VCMI_LIB_NAMESPACE_END class CGameHandler; @@ -26,31 +30,31 @@ class BattleFlowProcessor : boost::noncopyable BattleProcessor * owner; CGameHandler * gameHandler; - const CStack * getNextStack(const BattleInfo & battle); + const CStack * getNextStack(const CBattleInfoCallback & battle); - bool rollGoodMorale(const BattleInfo & battle, const CStack * stack); - bool tryMakeAutomaticAction(const BattleInfo & battle, const CStack * stack); + bool rollGoodMorale(const CBattleInfoCallback & battle, const CStack * stack); + bool tryMakeAutomaticAction(const CBattleInfoCallback & battle, const CStack * stack); - void summonGuardiansHelper(const BattleInfo & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); - void trySummonGuardians(const BattleInfo & battle, const CStack * stack); - void tryPlaceMoats(const BattleInfo & battle); - void castOpeningSpells(const BattleInfo & battle); - void activateNextStack(const BattleInfo & battle); - void startNextRound(const BattleInfo & battle, bool isFirstRound); + void summonGuardiansHelper(const CBattleInfoCallback & battle, std::vector & output, const BattleHex & targetPosition, ui8 side, bool targetIsTwoHex); + void trySummonGuardians(const CBattleInfoCallback & battle, const CStack * stack); + void tryPlaceMoats(const CBattleInfoCallback & battle); + void castOpeningSpells(const CBattleInfoCallback & battle); + void activateNextStack(const CBattleInfoCallback & battle); + void startNextRound(const CBattleInfoCallback & battle, bool isFirstRound); - void stackEnchantedTrigger(const BattleInfo & battle, const CStack * stack); - void removeObstacle(const BattleInfo & battle, const CObstacleInstance & obstacle); - void stackTurnTrigger(const BattleInfo & battle, const CStack * stack); - void setActiveStack(const BattleInfo & battle, const CStack * stack); + void stackEnchantedTrigger(const CBattleInfoCallback & battle, const CStack * stack); + void removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle); + void stackTurnTrigger(const CBattleInfoCallback & battle, const CStack * stack); + void setActiveStack(const CBattleInfoCallback & battle, const battle::Unit * stack); - void makeStackDoNothing(const BattleInfo & battle, const CStack * next); - bool makeAutomaticAction(const BattleInfo & battle, const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) + void makeStackDoNothing(const CBattleInfoCallback & battle, const CStack * next); + bool makeAutomaticAction(const CBattleInfoCallback & battle, const CStack * stack, BattleAction & ba); //used when action is taken by stack without volition of player (eg. unguided catapult attack) public: explicit BattleFlowProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - void onBattleStarted(const BattleInfo & battle); - void onTacticsEnded(const BattleInfo & battle); - void onActionMade(const BattleInfo & battle, const BattleAction & ba); + void onBattleStarted(const CBattleInfoCallback & battle); + void onTacticsEnded(const CBattleInfoCallback & battle); + void onActionMade(const CBattleInfoCallback & battle, const BattleAction & ba); }; diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index a67319d35..c5e025fde 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -19,6 +19,7 @@ #include "../queries/BattleQueries.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/BattleInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" @@ -147,17 +148,17 @@ BattleID BattleProcessor::setupBattle(int3 tile, const CArmedInstance *armies[2] return bs.battleID; } -bool BattleProcessor::checkBattleStateChanges(const BattleInfo & battle) +bool BattleProcessor::checkBattleStateChanges(const CBattleInfoCallback & battle) { //check if drawbridge state need to be changes - if (gameHandler->battleGetSiegeLevel() > 0) + if (battle.battleGetSiegeLevel() > 0) updateGateState(battle); if (resultProcessor->battleIsEnding(battle)) return true; //check if battle ended - if (auto result = gameHandler->battleIsFinished()) + if (auto result = battle.battleIsFinished()) { setBattleResult(battle, EBattleResult::NORMAL, *result); return true; @@ -166,7 +167,7 @@ bool BattleProcessor::checkBattleStateChanges(const BattleInfo & battle) return false; } -void BattleProcessor::updateGateState(const BattleInfo & battle) +void BattleProcessor::updateGateState(const CBattleInfoCallback & battle) { // GATE_BRIDGE - leftmost tile, located over moat // GATE_OUTER - central tile, mostly covered by gate image @@ -182,20 +183,20 @@ void BattleProcessor::updateGateState(const BattleInfo & battle) // - if Force Field is cast here, bridge can't open (but can close, in any town) // - deals moat damage to attacker if bridge is closed (fortress only) - bool hasForceFieldOnBridge = !gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty(); + bool hasForceFieldOnBridge = !battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), true).empty(); bool hasStackAtGateInner = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_INNER), false) != nullptr; bool hasStackAtGateOuter = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_OUTER), false) != nullptr; bool hasStackAtGateBridge = battle.battleGetUnitByPos(BattleHex(BattleHex::GATE_BRIDGE), false) != nullptr; - bool hasWideMoat = vstd::contains_if(gameHandler->battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) + bool hasWideMoat = vstd::contains_if(battle.battleGetAllObstaclesOnPos(BattleHex(BattleHex::GATE_BRIDGE), false), [](const std::shared_ptr & obst) { return obst->obstacleType == CObstacleInstance::MOAT; }); BattleUpdateGateState db; - db.state = battle.si.gateState; - db.battleID = battle.battleID; + db.state = battle.battleGetGateState(); + db.battleID = battle.getBattle()->getBattleID(); - if (battle.si.wallState.at(EWallPart::GATE) == EWallState::DESTROYED) + if (battle.battleGetWallState(EWallPart::GATE) == EWallState::DESTROYED) { db.state = EGateState::DESTROYED; } @@ -219,7 +220,7 @@ void BattleProcessor::updateGateState(const BattleInfo & battle) db.state = EGateState::CLOSED; } - if (db.state != battle.si.gateState) + if (db.state != battle.battleGetGateState()) gameHandler->sendAndApply(&db); } @@ -236,13 +237,13 @@ bool BattleProcessor::makePlayerBattleAction(const BattleID & battleID, PlayerCo return result; } -void BattleProcessor::setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide) +void BattleProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) { resultProcessor->setBattleResult(battle, resultType, victoriusSide); resultProcessor->endBattle(battle); } -bool BattleProcessor::makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction &ba) +bool BattleProcessor::makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction &ba) { return actionsProcessor->makeAutomaticBattleAction(battle, ba); } diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index c53dff89f..6dcb50de7 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -17,7 +17,7 @@ class CGTownInstance; class CArmedInstance; class BattleAction; class int3; -class BattleInfo; +class CBattleInfoCallback; struct BattleResult; class BattleID; VCMI_LIB_NAMESPACE_END @@ -40,16 +40,15 @@ class BattleProcessor : boost::noncopyable std::unique_ptr flowProcessor; std::unique_ptr resultProcessor; - void onTacticsEnded(); - void updateGateState(const BattleInfo & battle); + void updateGateState(const CBattleInfoCallback & battle); void engageIntoBattle(PlayerColor player); - bool checkBattleStateChanges(const BattleInfo & battle); + bool checkBattleStateChanges(const CBattleInfoCallback & battle); BattleID setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town); - bool makeAutomaticBattleAction(const BattleInfo & battle, const BattleAction & ba); + bool makeAutomaticBattleAction(const CBattleInfoCallback & battle, const BattleAction & ba); - void setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide); + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); public: explicit BattleProcessor(CGameHandler * gameHandler); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6f9be2242..01eebf3b9 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -18,7 +18,9 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CStack.h" #include "../../lib/GameSettings.h" -#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/IBattleState.h" +#include "../../lib/battle/SideInBattle.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/serializer/Cast.h" @@ -35,15 +37,19 @@ void BattleResultProcessor::setGameHandler(CGameHandler * newGameHandler) gameHandler = newGameHandler; } -CasualtiesAfterBattle::CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat): - army(battleSide.armyObject) +CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle): + army(battle.battleGetArmyObject(sideInBattle)) { heroWithDeadCommander = ObjectInstanceID(); - PlayerColor color = battleSide.color; + PlayerColor color = battle.sideToPlayer(sideInBattle); - for(CStack * st : bat->stacks) + for(const CStack * stConst : battle.battleGetAllStacks(true)) { + // Use const cast - in order to call non-const "takeResurrected" for proper calculation of casualties + // TODO: better solution + CStack * st = const_cast(stConst); + if(st->summoned) //don't take into account temporary summoned stacks continue; if(st->unitOwner() != color) //remove only our stacks @@ -181,11 +187,23 @@ FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr auto &result = *Query->result; auto &info = *Query->bi; - winnerHero = result.winner != 0 ? info.sides[1].hero : info.sides[0].hero; - loserHero = result.winner != 0 ? info.sides[0].hero : info.sides[1].hero; - victor = info.sides[result.winner].color; - loser = info.sides[!result.winner].color; + if (result.winner == BattleSide::ATTACKER) + { + winnerHero = info.getSideHero(BattleSide::ATTACKER); + loserHero = info.getSideHero(BattleSide::DEFENDER); + victor = info.getSidePlayer(BattleSide::ATTACKER); + loser = info.getSidePlayer(BattleSide::DEFENDER); + } + else + { + winnerHero = info.getSideHero(BattleSide::DEFENDER); + loserHero = info.getSideHero(BattleSide::ATTACKER); + victor = info.getSidePlayer(BattleSide::DEFENDER); + loser = info.getSidePlayer(BattleSide::ATTACKER); + } + winnerSide = result.winner; + this->remainingBattleQueriesCount = remainingBattleQueriesCount; } @@ -196,7 +214,7 @@ FinishingBattleHelper::FinishingBattleHelper() remainingBattleQueriesCount = 0; } -void BattleResultProcessor::endBattle(const BattleInfo & battle) +void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) { auto const & giveExp = [](BattleResult &r) { @@ -215,9 +233,9 @@ void BattleResultProcessor::endBattle(const BattleInfo & battle) LOG_TRACE(logGlobal); - auto * battleResult = battleResults.at(battle.battleID).get(); - const auto * heroAttacker = battle.getSideHero(BattleSide::ATTACKER); - const auto * heroDefender = battle.getSideHero(BattleSide::DEFENDER); + auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get(); + const auto * heroAttacker = battle.battleGetFightingHero(BattleSide::ATTACKER); + const auto * heroDefender = battle.battleGetFightingHero(BattleSide::DEFENDER); //Fill BattleResult structure with exp info giveExp(*battleResult); @@ -235,11 +253,11 @@ void BattleResultProcessor::endBattle(const BattleInfo & battle) if(heroDefender) battleResult->exp[1] = heroDefender->calculateXp(battleResult->exp[1]); - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sides[0].color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); if (!battleQuery) { logGlobal->error("Cannot find battle query!"); - gameHandler->complain("Player " + boost::lexical_cast(battle.sides[0].color) + " has no battle query at the top!"); + gameHandler->complain("Player " + boost::lexical_cast(battle.sideToPlayer(0)) + " has no battle query at the top!"); return; } @@ -248,13 +266,13 @@ void BattleResultProcessor::endBattle(const BattleInfo & battle) //Check how many battle gameHandler->queries were created (number of players blocked by battle) const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; - assert(finishingBattles.count(battle.battleID) == 0); - finishingBattles[battle.battleID] = std::make_unique(battleQuery, queriedPlayers); + assert(finishingBattles.count(battle.getBattle()->getBattleID()) == 0); + finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique(battleQuery, queriedPlayers); // in battles against neutrals, 1st player can ask to replay battle manually - if (!battle.sides[1].color.isValidPlayer()) + if (!battle.sideToPlayer(1).isValidPlayer()) { - auto battleDialogQuery = std::make_shared(gameHandler, &battle); + auto battleDialogQuery = std::make_shared(gameHandler, battle.getBattle()); battleResult->queryID = battleDialogQuery->queryID; gameHandler->queries->addQuery(battleDialogQuery); } @@ -275,21 +293,24 @@ void BattleResultProcessor::endBattle(const BattleInfo & battle) endBattleConfirm(battle); } -void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) +void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { - auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sides.at(0).color)); + auto battleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle.sideToPlayer(0))); if(!battleQuery) { logGlobal->trace("No battle query, battle end was confirmed by another player"); return; } - auto * battleResult = battleResults.at(battle.battleID).get(); - auto * finishingBattle = finishingBattles.at(battle.battleID).get(); + auto * battleResult = battleResults.at(battle.getBattle()->getBattleID()).get(); + auto * finishingBattle = finishingBattles.at(battle.getBattle()->getBattleID()).get(); const EBattleResult result = battleResult->result; - CasualtiesAfterBattle cab1(battle.sides.at(0), &battle), cab2(battle.sides.at(1), &battle); //calculate casualties before deleting battle + //calculate casualties before deleting battle + CasualtiesAfterBattle cab1(battle, BattleSide::ATTACKER); + CasualtiesAfterBattle cab2(battle, BattleSide::DEFENDER); + ChangeSpells cs; //for Eagle Eye if(!finishingBattle->isDraw() && finishingBattle->winnerHero) @@ -297,7 +318,7 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) { double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); - for(auto & spellId : battle.sides.at(!battleResult->winner).usedSpellsHistory) + for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) { auto spell = spellId.toSpell(VLC->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) @@ -365,7 +386,10 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) } } } - for (auto armySlot : battle.sides.at(!battleResult->winner).armyObject->stacks) + + auto loser = battle.otherSide(battleResult->winner); + + for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; for (auto artSlot : artifactsWorn) @@ -466,10 +490,10 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); BattleResultAccepted raccepted; - raccepted.heroResult[0].army = const_cast(battle.sides.at(0).armyObject); - raccepted.heroResult[1].army = const_cast(battle.sides.at(1).armyObject); - raccepted.heroResult[0].hero = const_cast(battle.sides.at(0).hero); - raccepted.heroResult[1].hero = const_cast(battle.sides.at(1).hero); + raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(0)); + raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(1)); + raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(0)); + raccepted.heroResult[1].hero = const_cast(battle.battleGetFightingHero(1)); raccepted.heroResult[0].exp = battleResult->exp[0]; raccepted.heroResult[1].exp = battleResult->exp[1]; raccepted.winnerSide = finishingBattle->winnerSide; @@ -479,15 +503,15 @@ void BattleResultProcessor::endBattleConfirm(const BattleInfo & battle) //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query } -void BattleResultProcessor::battleAfterLevelUp(const BattleInfo & battle, const BattleResult & result) +void BattleResultProcessor::battleAfterLevelUp(const CBattleInfoCallback & battle, const BattleResult & result) { LOG_TRACE(logGlobal); - assert(finishingBattles.count(battle.battleID) != 0); - if(finishingBattles.count(battle.battleID) == 0) + assert(finishingBattles.count(battle.getBattle()->getBattleID()) != 0); + if(finishingBattles.count(battle.getBattle()->getBattleID()) == 0) return; - auto & finishingBattle = finishingBattles[battle.battleID]; + auto & finishingBattle = finishingBattles[battle.getBattle()->getBattleID()]; finishingBattle->remainingBattleQueriesCount--; logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); @@ -517,8 +541,6 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleInfo & battle, const resultsApplied.player2 = finishingBattle->loser; gameHandler->sendAndApply(&resultsApplied); - gameHandler->setBattle(nullptr); - //handle victory/loss of engaged players std::set playerColors = {finishingBattle->loser, finishingBattle->victor}; gameHandler->checkVictoryLossConditions(playerColors); @@ -539,23 +561,29 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleInfo & battle, const gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); } - finishingBattles.erase(battle.battleID); - battleResults.erase(battle.battleID); + finishingBattles.erase(battle.getBattle()->getBattleID()); + battleResults.erase(battle.getBattle()->getBattleID()); } -void BattleResultProcessor::setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide) +void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) { - assert(battleResults.count(battle.battleID) == 0); + assert(battleResults.count(battle.getBattle()->getBattleID()) == 0); - battleResults[battle.battleID] = std::make_unique(); + battleResults[battle.getBattle()->getBattleID()] = std::make_unique(); - auto & battleResult = battleResults[battle.battleID]; + auto & battleResult = battleResults[battle.getBattle()->getBattleID()]; battleResult->result = resultType; battleResult->winner = victoriusSide; //surrendering side loses - battle.calculateCasualties(battleResult->casualties); + + for(const auto & st : battle.battleGetAllStacks(true)) //setting casualties + { + si32 killed = st->getKilled(); + if(killed > 0) + battleResult->casualties[st->unitSide()][st->creatureId()] += killed; + } } -bool BattleResultProcessor::battleIsEnding(const BattleInfo & battle) const +bool BattleResultProcessor::battleIsEnding(const CBattleInfoCallback & battle) const { - return battleResults.count(battle.battleID) != 0; + return battleResults.count(battle.getBattle()->getBattleID()) != 0; } diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index cf7f097ae..c69cd527e 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -31,7 +31,7 @@ struct CasualtiesAfterBattle TSummoned summoned; ObjectInstanceID heroWithDeadCommander; //TODO: unify stack locations - CasualtiesAfterBattle(const SideInBattle & battleSide, const BattleInfo * bat); + CasualtiesAfterBattle(const CBattleInfoCallback & battle, uint8_t sideInBattle); void updateArmy(CGameHandler * gh); }; @@ -71,10 +71,10 @@ public: explicit BattleResultProcessor(BattleProcessor * owner); void setGameHandler(CGameHandler * newGameHandler); - bool battleIsEnding(const BattleInfo & battle) const; + bool battleIsEnding(const CBattleInfoCallback & battle) const; - void setBattleResult(const BattleInfo & battle, EBattleResult resultType, int victoriusSide); - void endBattle(const BattleInfo & battle); //ends battle - void endBattleConfirm(const BattleInfo & battle); - void battleAfterLevelUp(const BattleInfo & battle, const BattleResult & result); + void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); + void endBattle(const CBattleInfoCallback & battle); //ends battle + void endBattleConfirm(const CBattleInfoCallback & battle); + void battleAfterLevelUp(const CBattleInfoCallback & battle, const BattleResult & result); }; diff --git a/server/battles/ServerBattleCallback.cpp b/server/battles/ServerBattleCallback.cpp new file mode 100644 index 000000000..e69de29bb diff --git a/server/battles/ServerBattleCallback.h b/server/battles/ServerBattleCallback.h new file mode 100644 index 000000000..e69de29bb diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index b7329ba29..9ff5b7800 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -14,7 +14,7 @@ #include "../CGameHandler.h" #include "../battles/BattleProcessor.h" -#include "../../lib/battle/BattleInfo.h" +#include "../../lib/battle/IBattleState.h" void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { @@ -22,16 +22,15 @@ void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisi objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); } -CBattleQuery::CBattleQuery(CGameHandler * owner, const BattleInfo * Bi): - CGhQuery(owner) +CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): + CGhQuery(owner), + bi(bi) { - belligerents[0] = Bi->sides[0].armyObject; - belligerents[1] = Bi->sides[1].armyObject; + belligerents[0] = bi->getSideArmy(0); + belligerents[1] = bi->getSideArmy(1); - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); + addPlayer(bi->getSidePlayer(0)); + addPlayer(bi->getSidePlayer(1)); } CBattleQuery::CBattleQuery(CGameHandler * owner): @@ -49,16 +48,15 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { if(result) - gh->battles->battleAfterLevelUp(bi->battleID, *result); + gh->battles->battleAfterLevelUp(bi->getBattleID(), *result); } -CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi): - CDialogQuery(owner) +CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi): + CDialogQuery(owner), + bi(bi) { - bi = Bi; - - for(auto & side : bi->sides) - addPlayer(side.color); + addPlayer(bi->getSidePlayer(0)); + addPlayer(bi->getSidePlayer(1)); } void CBattleDialogQuery::onRemoval(PlayerColor color) @@ -66,10 +64,18 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) assert(answer); if(*answer == 1) { - gh->startBattlePrimary(bi->sides[0].armyObject, bi->sides[1].armyObject, bi->tile, bi->sides[0].hero, bi->sides[1].hero, bi->creatureBank, bi->town); + gh->startBattlePrimary( + bi->getSideArmy(0), + bi->getSideArmy(1), + bi->getLocation(), + bi->getSideHero(0), + bi->getSideHero(1), + bi->isCreatureBank(), + bi->getDefendedTown() + ); } else { - gh->battles->endBattleConfirm(bi->battleID); + gh->battles->endBattleConfirm(bi->getBattleID()); } } diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index 03fce57a7..e661f7c90 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -13,17 +13,21 @@ #include "../../lib/NetPacks.h" +VCMI_LIB_NAMESPACE_BEGIN +class IBattleInfo; +VCMI_LIB_NAMESPACE_END + class CBattleQuery : public CGhQuery { public: std::array belligerents; std::array initialHeroMana; - const BattleInfo *bi; + const IBattleInfo *bi; std::optional result; CBattleQuery(CGameHandler * owner); - CBattleQuery(CGameHandler * owner, const BattleInfo * Bi); //TODO + CBattleQuery(CGameHandler * owner, const IBattleInfo * Bi); //TODO virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; virtual bool blocksPack(const CPack *pack) const override; virtual void onRemoval(PlayerColor color) override; @@ -32,9 +36,9 @@ public: class CBattleDialogQuery : public CDialogQuery { public: - CBattleDialogQuery(CGameHandler * owner, const BattleInfo * Bi); + CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * Bi); - const BattleInfo * bi; + const IBattleInfo * bi; virtual void onRemoval(PlayerColor color) override; }; From 036df2e0adc4d897ded148d966fdeeef652c3b00 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 28 Aug 2023 18:59:12 +0300 Subject: [PATCH 0340/1248] Support multiple battles in AI --- AI/BattleAI/BattleAI.cpp | 55 +++++++++++----------- AI/BattleAI/BattleAI.h | 12 ++--- AI/BattleAI/BattleEvaluator.cpp | 48 +++++++++---------- AI/BattleAI/BattleEvaluator.h | 9 ++-- AI/BattleAI/BattleExchangeVariant.h | 2 +- AI/EmptyAI/CEmptyAI.cpp | 10 ++-- AI/EmptyAI/CEmptyAI.h | 6 +-- AI/Nullkiller/AIGateway.cpp | 13 +++--- AI/Nullkiller/AIGateway.h | 6 +-- AI/StupidAI/StupidAI.cpp | 71 +++++++++++++++-------------- AI/StupidAI/StupidAI.h | 32 +++++++------ AI/VCAI/VCAI.cpp | 12 ++--- AI/VCAI/VCAI.h | 6 +-- CCallback.cpp | 4 +- CCallback.h | 31 ++++++++----- client/CPlayerInterface.cpp | 4 +- client/CPlayerInterface.h | 4 +- client/Client.cpp | 2 +- client/Client.h | 7 ++- lib/CGameInterface.cpp | 64 +++++++++++++------------- lib/CGameInterface.h | 39 ++++++++-------- lib/IGameEventsReceiver.h | 36 +++++++-------- 22 files changed, 245 insertions(+), 228 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index f96d33084..b94a56513 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -52,7 +52,8 @@ void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::share setCbc(CB); env = ENV; cb = CB; - playerID = *CB->getPlayerID(); //TODO should be sth in callback + assert(0);// FIXME: + // playerID = *CB->getPlayerID(); //TODO should be sth in callback wasWaitingForRealize = CB->waitTillRealize; wasUnlockingGs = CB->unlockGsWhenWaiting; CB->waitTillRealize = false; @@ -66,9 +67,9 @@ void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::share autobattlePreferences = autocombatPreferences; } -BattleAction CBattleAI::useHealingTent(const CStack *stack) +BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack) { - auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); + auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); std::map woundHpToStack; for(const auto * stack : healingTargets) { @@ -82,12 +83,12 @@ BattleAction CBattleAI::useHealingTent(const CStack *stack) return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack } -void CBattleAI::yourTacticPhase(int distance) +void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance) { - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); } -float getStrengthRatio(std::shared_ptr cb, int side) +static float getStrengthRatio(std::shared_ptr cb, int side) { auto stacks = cb->battleGetAllStacks(); auto our = 0, enemy = 0; @@ -108,7 +109,7 @@ float getStrengthRatio(std::shared_ptr cb, int side) return enemy == 0 ? 1.0f : static_cast(our) / enemy; } -void CBattleAI::activeStack(const CStack * stack ) +void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) { LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); @@ -128,12 +129,12 @@ void CBattleAI::activeStack(const CStack * stack ) { if(stack->creatureId() == CreatureID::CATAPULT) { - cb->battleMakeUnitAction(useCatapult(stack)); + cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack)); return; } if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) { - cb->battleMakeUnitAction(useHealingTent(stack)); + cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack)); return; } @@ -141,7 +142,7 @@ void CBattleAI::activeStack(const CStack * stack ) logAi->trace("Build evaluator and targets"); #endif - BattleEvaluator evaluator(env, cb, stack, playerID, side, getStrengthRatio(cb, side)); + BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); result = evaluator.selectStackAction(stack); @@ -157,9 +158,9 @@ void CBattleAI::activeStack(const CStack * stack ) logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); - if(auto action = considerFleeingOrSurrendering()) + if(auto action = considerFleeingOrSurrendering(battleID)) { - cb->battleMakeUnitAction(*action); + cb->battleMakeUnitAction(battleID, *action); return; } } @@ -183,17 +184,17 @@ void CBattleAI::activeStack(const CStack * stack ) logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); - cb->battleMakeUnitAction(result); + cb->battleMakeUnitAction(battleID, result); } -BattleAction CBattleAI::useCatapult(const CStack * stack) +BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack) { BattleAction attack; BattleHex targetHex = BattleHex::INVALID; - if(cb->battleGetGateState() == EGateState::CLOSED) + if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED) { - targetHex = cb->wallPartToBattleHex(EWallPart::GATE); + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE); } else { @@ -209,11 +210,11 @@ BattleAction CBattleAI::useCatapult(const CStack * stack) for(auto wallPart : wallParts) { - auto wallState = cb->battleGetWallState(wallPart); + auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) { - targetHex = cb->wallPartToBattleHex(wallPart); + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); break; } } @@ -234,7 +235,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack) return attack; } -void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) { LOG_TRACE(logAi); side = Side; @@ -247,17 +248,17 @@ void CBattleAI::print(const std::string &text) const logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); } -std::optional CBattleAI::considerFleeingOrSurrendering() +std::optional CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID) { BattleStateInfoForRetreat bs; - bs.canFlee = cb->battleCanFlee(); - bs.canSurrender = cb->battleCanSurrender(playerID); - bs.ourSide = cb->battleGetMySide(); - bs.ourHero = cb->battleGetMyHero(); + bs.canFlee = cb->getBattle(battleID)->battleCanFlee(); + bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID); + bs.ourSide = cb->getBattle(battleID)->battleGetMySide(); + bs.ourHero = cb->getBattle(battleID)->battleGetMyHero(); bs.enemyHero = nullptr; - for(auto stack : cb->battleGetAllStacks(false)) + for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false)) { if(stack->alive()) { @@ -266,7 +267,7 @@ std::optional CBattleAI::considerFleeingOrSurrendering() else { bs.enemyStacks.push_back(stack); - bs.enemyHero = cb->battleGetOwnerHero(stack); + bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack); } } } @@ -278,7 +279,7 @@ std::optional CBattleAI::considerFleeingOrSurrendering() return std::nullopt; } - auto result = cb->makeSurrenderRetreatDecision(bs); + auto result = cb->makeSurrenderRetreatDecision(battleID, bs); if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) { diff --git a/AI/BattleAI/BattleAI.h b/AI/BattleAI/BattleAI.h index a059e501c..497a77f3f 100644 --- a/AI/BattleAI/BattleAI.h +++ b/AI/BattleAI/BattleAI.h @@ -71,16 +71,16 @@ public: void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; - void activeStack(const CStack * stack) override; //called when it's turn of that stack - void yourTacticPhase(int distance) override; + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void yourTacticPhase(const BattleID & battleID, int distance) override; - std::optional considerFleeingOrSurrendering(); + std::optional considerFleeingOrSurrendering(const BattleID & battleID); void print(const std::string &text) const; - BattleAction useCatapult(const CStack *stack); - BattleAction useHealingTent(const CStack *stack); + BattleAction useCatapult(const BattleID & battleID, const CStack *stack); + BattleAction useHealingTent(const BattleID & battleID, const CStack *stack); - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override; //void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index ec65c194d..cae77e61f 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -53,12 +53,12 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL }) { - auto state = cb->battleGetWallState(wallPart); + auto state = cb->getBattle(battleID)->battleGetWallState(wallPart); if(state != EWallState::DESTROYED) continue; - auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart); + auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex((EWallPart)wallPart); auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); result.push_back(moatHex); @@ -70,15 +70,15 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const std::optional BattleEvaluator::findBestCreatureSpell(const CStack *stack) { //TODO: faerie dragon type spell should be selected by server - SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); + SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED); if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE) { const CSpell * spell = creatureSpellToCast.toSpell(); - if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack)) + if(spell->canBeCast(cb->getBattle(battleID).get(), spells::Mode::CREATURE_ACTIVE, stack)) { std::vector possibleCasts; - spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell); + spells::BattleCast temp(cb->getBattle(battleID).get(), stack, spells::Mode::CREATURE_ACTIVE, spell); for(auto & target : temp.findPotentialTargets()) { PossibleSpellcast ps; @@ -201,7 +201,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(score <= EvaluationResult::INEFFECTIVE_SCORE && !stack->hasBonusOfType(BonusType::FLYING) && stack->unitSide() == BattleSide::ATTACKER - && cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL) + && cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL) { auto brokenWallMoat = getBrokenWallMoatHexes(); @@ -228,8 +228,8 @@ uint64_t timeElapsed(std::chrono::time_point BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector hexes) { - auto reachability = cb->getReachability(stack); - auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); + auto reachability = cb->getBattle(battleID)->getReachability(stack); + auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked { @@ -325,16 +325,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector bool BattleEvaluator::canCastSpell() { - auto hero = cb->battleGetMyHero(); + auto hero = cb->getBattle(battleID)->battleGetMyHero(); if(!hero) return false; - return cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK; + return cb->getBattle(battleID)->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK; } bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) { - auto hero = cb->battleGetMyHero(); + auto hero = cb->getBattle(battleID)->battleGetMyHero(); if(!hero) return false; @@ -343,7 +343,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) std::vector possibleSpells; vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool { - return s->canBeCast(cb.get(), spells::Mode::HERO, hero); + return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero); }); LOGFL("I can cast %d spells.", possibleSpells.size()); @@ -358,7 +358,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) std::vector possibleCasts; for(auto spell : possibleSpells) { - spells::BattleCast temp(cb.get(), hero, spells::Mode::HERO, spell); + spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell); if(spell->getTargetType() == spells::AimType::LOCATION) continue; @@ -468,7 +468,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) ValueMap valueOfStack; ValueMap healthOfStack; - TStacks all = cb->battleGetAllStacks(false); + TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false); size_t ourRemainingTurns = 0; @@ -477,7 +477,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) healthOfStack[unit->unitId()] = unit->getAvailableHealth(); valueOfStack[unit->unitId()] = 0; - if(cb->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) + if(cb->getBattle(battleID)->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved()) ourRemainingTurns++; } @@ -494,12 +494,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) std::vector turnOrder; - cb->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once + cb->getBattle(battleID)->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once { bool enemyHadTurn = false; - auto state = std::make_shared(env.get(), cb); + auto state = std::make_shared(env.get(), cb->getBattle(battleID)); evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn); @@ -531,7 +531,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) logAi->trace("Evaluating %s", ps.spell->getNameTranslated()); #endif - auto state = std::make_shared(env.get(), cb); + auto state = std::make_shared(env.get(), cb->getBattle(battleID)); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); cast.castEval(state->getServerCallback(), ps.dest); @@ -540,7 +540,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool { - auto original = cb->battleGetUnitByID(u->unitId()); + auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId()); return !original || u->speed() != original->speed(); }); @@ -583,7 +583,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) if(oldHealth != newHealth) { auto damage = std::abs(oldHealth - newHealth); - auto originalDefender = cb->battleGetUnitByID(unit->unitId()); + auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId()); auto dpsReduce = AttackPossibility::calculateDamageReduce( nullptr, @@ -639,7 +639,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) spellcast.setTarget(castToPerform.dest); spellcast.side = side; spellcast.stackNumber = (!side) ? -1 : -2; - cb->battleMakeSpellAction(spellcast); + cb->battleMakeSpellAction(battleID, spellcast); activeActionMade = true; return true; @@ -656,8 +656,8 @@ void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSp using ValueMap = PossibleSpellcast::ValueMap; RNGStub rngStub; - HypotheticBattle state(env.get(), cb); - TStacks all = cb->battleGetAllStacks(false); + HypotheticBattle state(env.get(), cb->getBattle(battleID)); + TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false); ValueMap healthOfStack; ValueMap newHealthOfStack; @@ -686,7 +686,7 @@ void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSp auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId]; - if(localUnit->unitOwner() != getCbc()->getPlayerID()) + if(localUnit->unitOwner() != cb->getBattle(battleID)->getPlayerID()) healthDiff = -healthDiff; if(healthDiff < 0) diff --git a/AI/BattleAI/BattleEvaluator.h b/AI/BattleAI/BattleEvaluator.h index cb183f46b..6198d56a4 100644 --- a/AI/BattleAI/BattleEvaluator.h +++ b/AI/BattleAI/BattleEvaluator.h @@ -32,6 +32,7 @@ class BattleEvaluator bool activeActionMade = false; std::optional cachedAttack; PlayerColor playerID; + BattleID battleID; int side; float cachedScore; DamageCache damageCache; @@ -52,11 +53,12 @@ public: std::shared_ptr cb, const battle::Unit * activeStack, PlayerColor playerID, + BattleID battleID, int side, float strengthRatio) - :scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio) + :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID) { - hb = std::make_shared(env.get(), cb); + hb = std::make_shared(env.get(), cb->getBattle(battleID)); damageCache.buildDamageCache(hb, side); targets = std::make_unique(activeStack, damageCache, hb); @@ -70,9 +72,10 @@ public: DamageCache & damageCache, const battle::Unit * activeStack, PlayerColor playerID, + BattleID battleID, int side, float strengthRatio) - :scoreEvaluator(cb, env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio) + :scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID) { targets = std::make_unique(activeStack, damageCache, hb); cachedScore = EvaluationResult::INEFFECTIVE_SCORE; diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 19da2721a..36b8b2592 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -133,4 +133,4 @@ public: float getPositiveEffectMultiplier() { return 1; } float getNegativeEffectMultiplier() { return negativeEffectMultiplier; } -}; \ No newline at end of file +}; diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 4c2829308..0e0a35489 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -36,14 +36,14 @@ void CEmptyAI::yourTurn(QueryID queryID) cb->endTurn(); } -void CEmptyAI::activeStack(const CStack * stack) +void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack) { - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); } -void CEmptyAI::yourTacticPhase(int distance) +void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance) { - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); } void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) @@ -76,7 +76,7 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, cb->selectionMade(0, askID); } -std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { return std::nullopt; } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 297795839..1bc668e9e 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -24,15 +24,15 @@ public: void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void yourTurn(QueryID queryID) override; - void yourTacticPhase(int distance) override; - void activeStack(const CStack * stack) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; + void activeStack(const BattleID & battleID, const CStack * stack) override; void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; }; #define NAME "EmptyAI 0.1" diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index ad30802c3..83e0b1b28 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -21,6 +21,7 @@ #include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinaryDeserializer.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" +#include "../../lib/battle/BattleInfo.h" #include "AIGateway.h" #include "Goals/Goals.h" @@ -510,7 +511,7 @@ void AIGateway::showWorldViewEx(const std::vector & objectPositio NET_EVENT_HANDLER; } -std::optional AIGateway::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional AIGateway::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { LOG_TRACE(logAi); NET_EVENT_HANDLER; @@ -1080,22 +1081,22 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re } } -void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); + CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); } -void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) +void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); - bool won = br->winner == myCb->battleGetMySide(); + bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); @@ -1108,7 +1109,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID) answerQuery(queryID, confirmAction); }); } - CAdventureAI::battleEnd(br, queryID); + CAdventureAI::battleEnd(battleID, br, queryID); } void AIGateway::waitTillFree() diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index c4064f164..45021b419 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -167,10 +167,10 @@ public: void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; - void battleEnd(const BattleResult * br, QueryID queryID) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; + void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; void makeTurn(); diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index c8f495885..6cd6a15b2 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -14,6 +14,7 @@ #include "../../CCallback.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleInfo.h" static std::shared_ptr cbc; @@ -53,12 +54,12 @@ void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::share initBattleInterface(ENV, CB); } -void CStupidAI::actionFinished(const BattleAction &action) +void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action) { print("actionFinished called"); } -void CStupidAI::actionStarted(const BattleAction &action) +void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action) { print("actionStarted called"); } @@ -71,11 +72,11 @@ public: std::vector attackFrom; //for melee fight EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) {} - void calcDmg(const CStack * ourStack) + void calcDmg(const BattleID & battleID, const CStack * ourStack) { // FIXME: provide distance info for Jousting bonus DamageEstimation retal; - DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); + DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); adr = static_cast((retal.damage.min + retal.damage.max) / 2); } @@ -91,14 +92,14 @@ bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); } -static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2) +static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) { int shooters[2] = {0}; //count of shooters on hexes for(int i = 0; i < 2; i++) { for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) - if(const auto * s = cbc->battleGetUnitByPos(neighbour)) + if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour)) if(s->isShooter()) shooters[i]++; } @@ -106,16 +107,16 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl return shooters[0] < shooters[1]; } -void CStupidAI::yourTacticPhase(int distance) +void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance) { - cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); } -void CStupidAI::activeStack( const CStack * stack ) +void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) { //boost::this_thread::sleep_for(boost::chrono::seconds(2)); print("activeStack called for " + stack->nodeName()); - ReachabilityInfo dists = cb->getReachability(stack); + ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack); std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; if(stack->creatureId() == CreatureID::CATAPULT) @@ -128,24 +129,24 @@ void CStupidAI::activeStack( const CStack * stack ) attack.side = side; attack.stackNumber = stack->unitId(); - cb->battleMakeUnitAction(attack); + cb->battleMakeUnitAction(battleID, attack); return; } else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) { - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); return; } - for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY)) + for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY)) { - if(cb->battleCanShoot(stack, s->getPosition())) + if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition())) { enemiesShootable.push_back(s); } else { - std::vector avHexes = cb->battleGetAvailableHexes(stack, false); + std::vector avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false); for (BattleHex hex : avHexes) { @@ -168,21 +169,23 @@ void CStupidAI::activeStack( const CStack * stack ) } for ( auto & enemy : enemiesReachable ) - enemy.calcDmg( stack ); + enemy.calcDmg(battleID, stack); for ( auto & enemy : enemiesShootable ) - enemy.calcDmg( stack ); + enemy.calcDmg(battleID, stack); if(enemiesShootable.size()) { const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); - cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s)); + cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s)); return; } else if(enemiesReachable.size()) { const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); - cb->battleMakeUnitAction(BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters))); + BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);}); + + cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex)); return; } else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies @@ -194,26 +197,26 @@ void CStupidAI::activeStack( const CStack * stack ) if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) { - cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack))); + cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack))); return; } } - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); return; } -void CStupidAI::battleAttack(const BattleAttack *ba) +void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba) { print("battleAttack called"); } -void CStupidAI::battleStacksAttacked(const std::vector & bsa, bool ranged) +void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) { print("battleStacksAttacked called"); } -void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) +void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) { print("battleEnd called"); } @@ -223,38 +226,38 @@ void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) // print("battleResultsApplied called"); // } -void CStupidAI::battleNewRoundFirst(int round) +void CStupidAI::battleNewRoundFirst(const BattleID & battleID, int round) { print("battleNewRoundFirst called"); } -void CStupidAI::battleNewRound(int round) +void CStupidAI::battleNewRound(const BattleID & battleID, int round) { print("battleNewRound called"); } -void CStupidAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) +void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) { print("battleStackMoved called"); } -void CStupidAI::battleSpellCast(const BattleSpellCast *sc) +void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) { print("battleSpellCast called"); } -void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse) +void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) { print("battleStacksEffectsSet called"); } -void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) { print("battleStart called"); side = Side; } -void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca) +void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) { print("battleCatapultAttacked called"); } @@ -264,10 +267,10 @@ void CStupidAI::print(const std::string &text) const logAi->trace("CStupidAI [%p]: %s", this, text); } -BattleAction CStupidAI::goTowards(const CStack * stack, std::vector hexes) const +BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const { - auto reachability = cb->getReachability(stack); - auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); + auto reachability = cb->getBattle(battleID)->getReachability(stack); + auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked { diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 6b0d230ad..20776bbc4 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -11,6 +11,7 @@ #include "../../lib/battle/BattleHex.h" #include "../../lib/battle/ReachabilityInfo.h" +#include "../../lib/CGameInterface.h" class EnemyInfo; @@ -30,25 +31,26 @@ public: void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; - void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero - void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero - void activeStack(const CStack * stack) override; //called when it's turn of that stack - void yourTacticPhase(int distance) override; - void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack - void battleStacksAttacked(const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) - void battleEnd(const BattleResult *br, QueryID queryID) override; + void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void yourTacticPhase(const BattleID & battleID, int distance) override; + + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied - void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; - void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - void battleSpellCast(const BattleSpellCast *sc) override; - void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks + void battleNewRoundFirst(const BattleID & battleID, int round) override; //called at the beginning of each turn before changes are applied; + void battleNewRound(const BattleID & battleID, int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleTriggerEffect(const BattleTriggerEffect & bte) override; - void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right - void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack private: - BattleAction goTowards(const CStack * stack, std::vector hexes) const; + BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const; }; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index a150e7c6a..5070ca2d7 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1577,22 +1577,22 @@ void VCAI::completeGoal(Goals::TSubgoal goal) } -void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { NET_EVENT_HANDLER; assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); status.setBattle(ONGOING_BATTLE); const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); + CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); } -void VCAI::battleEnd(const BattleResult * br, QueryID queryID) +void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) { NET_EVENT_HANDLER; assert(status.getBattle() == ONGOING_BATTLE); status.setBattle(ENDING_BATTLE); - bool won = br->winner == myCb->battleGetMySide(); + bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); battlename.clear(); @@ -1605,7 +1605,7 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID) answerQuery(queryID, confirmAction); }); } - CAdventureAI::battleEnd(br, queryID); + CAdventureAI::battleEnd(battleID, br, queryID); } void VCAI::waitTillFree() @@ -2894,7 +2894,7 @@ bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) return true; } -std::optional VCAI::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { return std::nullopt; } diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index ea341b4af..5b5df2ffa 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -201,9 +201,9 @@ public: void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; - void battleEnd(const BattleResult * br, QueryID queryID) override; - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; + void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; void makeTurn(); void mainLoop(); diff --git a/CCallback.cpp b/CCallback.cpp index 1b1b034b9..43970c048 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -393,7 +393,7 @@ void CBattleCallback::battleMakeTacticAction( const BattleAction & action ) sendRequest(&ma); } -std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { - return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleState); + return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID< battleState); } diff --git a/CCallback.h b/CCallback.h index 062ef8185..95b3f4493 100644 --- a/CCallback.h +++ b/CCallback.h @@ -53,10 +53,12 @@ public: bool waitTillRealize = false; //if true, request functions will return after they are realized by server bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! //battle - virtual void battleMakeSpellAction(const BattleAction & action) = 0; - virtual void battleMakeUnitAction(const BattleAction & action) = 0; - virtual void battleMakeTacticAction(const BattleAction & action) = 0; - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0; + virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; + + virtual std::shared_ptr getBattle(const BattleID & battleID) = 0; }; class IGameActionCallback @@ -108,18 +110,25 @@ public: virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) = 0; }; -class CBattleCallback : public IBattleCallback, public CPlayerBattleCallback +class CBattleCallback : public IBattleCallback { + std::map> activeBattles; + protected: int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; public: CBattleCallback(std::optional Player, CClient * C); - void battleMakeSpellAction(const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack - void battleMakeUnitAction(const BattleAction & action) override; - void battleMakeTacticAction(const BattleAction & action) override; // performs tactic phase actions - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack + void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override; + void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + + std::shared_ptr getBattle(const BattleID & battleID) override; + + void onBattleStarted(const BattleID & battleID); + void onBattleEnded(const BattleID & battleID); #if SCRIPTING_ENABLED scripting::Pool * getContextPool() const override; @@ -129,9 +138,7 @@ public: friend class CClient; }; -class CCallback : public CPlayerSpecificInfoCallback, - public IGameActionCallback, - public CBattleCallback +class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback { public: CCallback(CGameState * GS, std::optional Player, CClient * C); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index cddcfe2c9..0866394fb 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1000,7 +1000,7 @@ void CPlayerInterface::battleGateStateChanged(const EGateState state) battleInt->gateStateChanged(state); } -void CPlayerInterface::yourTacticPhase(int distance) +void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance) { EVENT_HANDLER_CALLED_BY_CLIENT; } @@ -2126,7 +2126,7 @@ void CPlayerInterface::showWorldViewEx(const std::vector& objectP adventureInt->openWorldView(objectPositions, showTerrain ); } -std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) +std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { return std::nullopt; } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index c5048637f..4a74fd8d2 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -171,8 +171,8 @@ protected: // Call-ins from server, should not be called directly, but only via void battleObstaclesChanged(const std::vector & obstacles) override; void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack void battleGateStateChanged(const EGateState state) override; - void yourTacticPhase(int distance) override; - std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; public: // public interface for use by client via LOCPLINT access diff --git a/client/Client.cpp b/client/Client.cpp index 931dd4e36..44652aeb7 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -634,7 +634,7 @@ void CClient::battleStarted(const BattleInfo * info) auto tacticianColor = info->sides[info->tacticsSide].color; if (vstd::contains(battleints, tacticianColor)) - battleints[tacticianColor]->yourTacticPhase(info->tacticDistance); + battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); } } diff --git a/client/Client.h b/client/Client.h index 55dc5e708..48816798f 100644 --- a/client/Client.h +++ b/client/Client.h @@ -13,7 +13,6 @@ #include #include "../lib/IGameCallback.h" -#include "../lib/battle/CBattleInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN @@ -105,12 +104,12 @@ public: const Services * services() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; - const BattleCb * battle() const override; + const BattleCb * battle(const BattleID & battle) const override; const GameCb * game() const override; }; /// Class which handles client - server logic -class CClient : public IGameCallback, public CBattleInfoCallback, public Environment +class CClient : public IGameCallback, public Environment { public: std::map> playerint; @@ -124,7 +123,7 @@ public: ~CClient(); const Services * services() const override; - const BattleCb * battle() const override; + const BattleCb * battle(const BattleID & battle) const override; const GameCb * game() const override; vstd::CLoggerBase * logger() const override; events::EventBus * eventBus() const override; diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 5b052d3c8..3148aa65d 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -157,90 +157,90 @@ CGlobalAI::CGlobalAI() human = false; } -void CAdventureAI::battleNewRound(int round) +void CAdventureAI::battleNewRound(const BattleID & battleID, int round) { - battleAI->battleNewRound(round); + battleAI->battleNewRound(battleID, round); } -void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca) +void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) { - battleAI->battleCatapultAttacked(ca); + battleAI->battleCatapultAttacked(battleID, ca); } -void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, +void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) { assert(!battleAI); assert(cbc); battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); battleAI->initBattleInterface(env, cbc); - battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed); + battleAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); } -void CAdventureAI::battleStacksAttacked(const std::vector & bsa, bool ranged) +void CAdventureAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) { - battleAI->battleStacksAttacked(bsa, ranged); + battleAI->battleStacksAttacked(battleID, bsa, ranged); } -void CAdventureAI::actionStarted(const BattleAction & action) +void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & action) { - battleAI->actionStarted(action); + battleAI->actionStarted(battleID, action); } -void CAdventureAI::battleNewRoundFirst(int round) +void CAdventureAI::battleNewRoundFirst(const BattleID & battleID, int round) { - battleAI->battleNewRoundFirst(round); + battleAI->battleNewRoundFirst(battleID, round); } -void CAdventureAI::actionFinished(const BattleAction & action) +void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action) { - battleAI->actionFinished(action); + battleAI->actionFinished(battleID, action); } -void CAdventureAI::battleStacksEffectsSet(const SetStackEffect & sse) +void CAdventureAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) { - battleAI->battleStacksEffectsSet(sse); + battleAI->battleStacksEffectsSet(battleID, sse); } -void CAdventureAI::battleObstaclesChanged(const std::vector & obstacles) +void CAdventureAI::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) { - battleAI->battleObstaclesChanged(obstacles); + battleAI->battleObstaclesChanged(battleID, obstacles); } -void CAdventureAI::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) +void CAdventureAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) { - battleAI->battleStackMoved(stack, dest, distance, teleport); + battleAI->battleStackMoved(battleID, stack, dest, distance, teleport); } -void CAdventureAI::battleAttack(const BattleAttack * ba) +void CAdventureAI::battleAttack(const BattleID & battleID, const BattleAttack * ba) { - battleAI->battleAttack(ba); + battleAI->battleAttack(battleID, ba); } -void CAdventureAI::battleSpellCast(const BattleSpellCast * sc) +void CAdventureAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) { - battleAI->battleSpellCast(sc); + battleAI->battleSpellCast(battleID, sc); } -void CAdventureAI::battleEnd(const BattleResult * br, QueryID queryID) +void CAdventureAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) { - battleAI->battleEnd(br, queryID); + battleAI->battleEnd(battleID, br, queryID); battleAI.reset(); } -void CAdventureAI::battleUnitsChanged(const std::vector & units) +void CAdventureAI::battleUnitsChanged(const BattleID & battleID, const std::vector & units) { - battleAI->battleUnitsChanged(units); + battleAI->battleUnitsChanged(battleID, units); } -void CAdventureAI::activeStack(const CStack * stack) +void CAdventureAI::activeStack(const BattleID & battleID, const CStack * stack) { - battleAI->activeStack(stack); + battleAI->activeStack(battleID, stack); } -void CAdventureAI::yourTacticPhase(int distance) +void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) { - battleAI->yourTacticPhase(distance); + battleAI->yourTacticPhase(battleID, distance); } void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 24eb0a11f..2f9fe192f 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -80,8 +80,8 @@ public: virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences){}; //battle call-ins - virtual void activeStack(const CStack * stack)=0; //called when it's turn of that stack - virtual void yourTacticPhase(int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function + virtual void activeStack(const BattleID & battleID, const CStack * stack)=0; //called when it's turn of that stack + virtual void yourTacticPhase(const BattleID & battleID, int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function }; /// Central class for managing human player / AI interface logic @@ -109,7 +109,7 @@ public: virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; - virtual std::optional makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) = 0; + virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; virtual void saveGame(BinarySerializer & h, const int version) = 0; virtual void loadGame(BinaryDeserializer & h, const int version) = 0; @@ -144,22 +144,23 @@ public: virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used //battle interface - virtual void activeStack(const CStack * stack) override; - virtual void yourTacticPhase(int distance) override; - virtual void battleNewRound(int round) override; - virtual void battleCatapultAttacked(const CatapultAttack & ca) override; - virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; - virtual void battleStacksAttacked(const std::vector & bsa, bool ranged) override; - virtual void actionStarted(const BattleAction &action) override; - virtual void battleNewRoundFirst(int round) override; - virtual void actionFinished(const BattleAction &action) override; - virtual void battleStacksEffectsSet(const SetStackEffect & sse) override; - virtual void battleObstaclesChanged(const std::vector & obstacles) override; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - virtual void battleAttack(const BattleAttack *ba) override; - virtual void battleSpellCast(const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleResult *br, QueryID queryID) override; - virtual void battleUnitsChanged(const std::vector & units) override; + virtual void activeStack(const BattleID & battleID, const CStack * stack) override; + virtual void yourTacticPhase(const BattleID & battleID, int distance) override; + + virtual void battleNewRound(const BattleID & battleID, int round) override; + virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; + virtual void battleNewRoundFirst(const BattleID & battleID, int round) override; + virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; + virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; + virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; virtual void saveGame(BinarySerializer & h, const int version) override; virtual void loadGame(BinaryDeserializer & h, const int version) override; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index a19c9f11e..8139ee674 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -56,24 +56,24 @@ class UnitChanges; class DLL_LINKAGE IBattleEventsReceiver { public: - virtual void actionFinished(const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero - virtual void actionStarted(const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero - virtual void battleAttack(const BattleAttack *ba){}; //called when stack is performing attack - virtual void battleStacksAttacked(const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) - virtual void battleEnd(const BattleResult *br, QueryID queryID){}; - virtual void battleNewRoundFirst(int round){}; //called at the beginning of each turn before changes are applied; - virtual void battleNewRound(int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - virtual void battleLogMessage(const std::vector & lines){}; - virtual void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport){}; - virtual void battleSpellCast(const BattleSpellCast *sc){}; - virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks - virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects - virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start - virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right - virtual void battleUnitsChanged(const std::vector & units){}; - virtual void battleObstaclesChanged(const std::vector & obstacles){}; - virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack - virtual void battleGateStateChanged(const EGateState state){}; + virtual void actionFinished(const BattleID & battleID, const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero + virtual void actionStarted(const BattleID & battleID, const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){}; + virtual void battleNewRoundFirst(const BattleID & battleID, int round){}; //called at the beginning of each turn before changes are applied; + virtual void battleNewRound(const BattleID & battleID, int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + virtual void battleLogMessage(const BattleID & battleID, const std::vector & lines){}; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport){}; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){}; + virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse){};//called when a specific effect is set to stacks + virtual void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte){}; //called for various one-shot effects + virtual void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right + virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units){}; + virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles){}; + virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca){}; //called when catapult makes an attack + virtual void battleGateStateChanged(const BattleID & battleID, const EGateState state){}; }; class DLL_LINKAGE IGameEventsReceiver From 41210c1dbfdee507485112943d689afe548fc682 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 30 Aug 2023 22:07:02 +0300 Subject: [PATCH 0341/1248] Client-side support for multiple battles --- AI/EmptyAI/CEmptyAI.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 2 +- AI/StupidAI/StupidAI.cpp | 4 +- AI/StupidAI/StupidAI.h | 4 +- AI/VCAI/VCAI.cpp | 2 +- CCallback.cpp | 59 ++++++++++----- CCallback.h | 16 ++-- client/CPlayerInterface.cpp | 70 +++++++++--------- client/CPlayerInterface.h | 38 +++++----- client/Client.cpp | 39 +++++----- client/Client.h | 5 +- client/NetPacksClient.cpp | 64 ++++++++-------- client/battle/BattleActionsController.cpp | 38 +++++----- client/battle/BattleAnimationClasses.cpp | 2 +- client/battle/BattleEffectsController.cpp | 4 +- client/battle/BattleFieldController.cpp | 22 +++--- client/battle/BattleInterface.cpp | 54 ++++++++------ client/battle/BattleInterface.h | 11 ++- client/battle/BattleInterfaceClasses.cpp | 12 +-- client/battle/BattleObstacleController.cpp | 10 +-- client/battle/BattleSiegeController.cpp | 18 ++--- client/battle/BattleStacksController.cpp | 18 ++--- client/battle/BattleWindow.cpp | 24 +++--- client/windows/CSpellWindow.cpp | 2 +- client/windows/GUIClasses.cpp | 12 +-- cmake_modules/VCMI_lib.cmake | 2 - lib/CGameInfoCallback.cpp | 85 +++++++++++----------- lib/CGameInfoCallback.h | 10 ++- lib/CGameInterface.cpp | 8 +- lib/CGameInterface.h | 4 +- lib/IGameEventsReceiver.h | 4 +- lib/battle/CBattleInfoCallback.h | 1 - lib/battle/CBattleInfoEssentials.h | 1 - lib/battle/CCallbackBase.cpp | 27 ------- lib/battle/CCallbackBase.h | 36 --------- lib/battle/CPlayerBattleCallback.cpp | 18 +++++ lib/battle/CPlayerBattleCallback.h | 8 ++ lib/battle/IBattleInfoCallback.h | 2 + lib/pathfinder/CPathfinder.cpp | 2 +- 39 files changed, 370 insertions(+), 370 deletions(-) delete mode 100644 lib/battle/CCallbackBase.cpp delete mode 100644 lib/battle/CCallbackBase.h diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 0e0a35489..2ae81c102 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -27,7 +27,7 @@ void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_p cb = CB; env = ENV; human=false; - playerID = *cb->getMyColor(); + playerID = *cb->getPlayerID(); } void CEmptyAI::yourTurn(QueryID queryID) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 83e0b1b28..6ed2fdc90 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -536,7 +536,7 @@ void AIGateway::initGameInterface(std::shared_ptr env, std::shared_ cbc = CB; NET_EVENT_HANDLER; - playerID = *myCb->getMyColor(); + playerID = *myCb->getPlayerID(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 6cd6a15b2..5836b2b7a 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -226,12 +226,12 @@ void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, Que // print("battleResultsApplied called"); // } -void CStupidAI::battleNewRoundFirst(const BattleID & battleID, int round) +void CStupidAI::battleNewRoundFirst(const BattleID & battleID) { print("battleNewRoundFirst called"); } -void CStupidAI::battleNewRound(const BattleID & battleID, int round) +void CStupidAI::battleNewRound(const BattleID & battleID) { print("battleNewRound called"); } diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 20776bbc4..744214643 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -41,8 +41,8 @@ public: void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //void battleResultsApplied() override; //called when all effects of last battle are applied - void battleNewRoundFirst(const BattleID & battleID, int round) override; //called at the beginning of each turn before changes are applied; - void battleNewRound(const BattleID & battleID, int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 5070ca2d7..ddc952fbc 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -600,7 +600,7 @@ void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptrinit(CB.get()); NET_EVENT_HANDLER; //sets ah->rm->cb - playerID = *myCb->getMyColor(); + playerID = *myCb->getPlayerID(); myCb->waitTillRealize = true; myCb->unlockGsWhenWaiting = true; diff --git a/CCallback.cpp b/CCallback.cpp index 43970c048..542b781a1 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -203,7 +203,7 @@ bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) return true; } -void CBattleCallback::battleMakeSpellAction(const BattleAction & action) +void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) { assert(action.actionType == EActionType::HERO_SPELL); MakeAction mca(action); @@ -212,7 +212,7 @@ void CBattleCallback::battleMakeSpellAction(const BattleAction & action) int CBattleCallback::sendRequest(const CPackForServer * request) { - int requestID = cl->sendRequest(request, *player); + int requestID = cl->sendRequest(request, *getPlayerID()); if(waitTillRealize) { logGlobal->trace("We'll wait till request %d is answered.\n", requestID); @@ -226,8 +226,7 @@ int CBattleCallback::sendRequest(const CPackForServer * request) void CCallback::swapGarrisonHero( const CGTownInstance *town ) { - if(town->tempOwner == *player - || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) + if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) { GarrisonHeroSwap pack(town->id); sendRequest(&pack); @@ -236,7 +235,7 @@ void CCallback::swapGarrisonHero( const CGTownInstance *town ) void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) { - if(hero->tempOwner != player) return; + if(hero->tempOwner != *player) return; BuyArtifact pack(hero->id,aid); sendRequest(&pack); @@ -297,8 +296,8 @@ void CCallback::buildBoat( const IShipyard *obj ) sendRequest(&bb); } -CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C): - CBattleCallback(Player, C) +CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C) + : CBattleCallback(Player, C) { gs = GS; @@ -306,10 +305,7 @@ CCallback::CCallback(CGameState * GS, std::optional Player, CClient unlockGsWhenWaiting = false; } -CCallback::~CCallback() -{ -//trivial, but required. Don`t remove. -} +CCallback::~CCallback() = default; bool CCallback::canMoveBetween(const int3 &a, const int3 &b) { @@ -322,6 +318,11 @@ std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * return cl->getPathsInfo(h); } +std::optional CCallback::getPlayerID() const +{ + return CBattleCallback::getPlayerID(); +} + int3 CCallback::getGuardingCreaturePosition(int3 tile) { if (!gs->map->isInTheMap(tile)) @@ -371,23 +372,23 @@ scripting::Pool * CBattleCallback::getContextPool() const } #endif -CBattleCallback::CBattleCallback(std::optional Player, CClient * C) +CBattleCallback::CBattleCallback(std::optional player, CClient * C): + cl(C), + player(player) { - player = Player; - cl = C; } -void CBattleCallback::battleMakeUnitAction(const BattleAction & action) +void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) { - assert(!cl->gs->curB->tacticDistance); + assert(!cl->gs->getBattle(battleID)->tacticDistance); MakeAction ma; ma.ba = action; sendRequest(&ma); } -void CBattleCallback::battleMakeTacticAction( const BattleAction & action ) +void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action ) { - assert(cl->gs->curB->tacticDistance); + assert(cl->gs->getBattle(battleID)->tacticDistance); MakeAction ma; ma.ba = action; sendRequest(&ma); @@ -395,5 +396,25 @@ void CBattleCallback::battleMakeTacticAction( const BattleAction & action ) std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) { - return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID< battleState); + return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState); +} + +std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) +{ + return activeBattles.at(battleID); +} + +std::optional CBattleCallback::getPlayerID() const +{ + return player; +} + +void CBattleCallback::onBattleStarted(const IBattleInfo * info) +{ + activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); +} + +void CBattleCallback::onBattleEnded(const BattleID & battleID) +{ + activeBattles.erase(battleID); } diff --git a/CCallback.h b/CCallback.h index 95b3f4493..33d0c8b99 100644 --- a/CCallback.h +++ b/CCallback.h @@ -59,6 +59,7 @@ public: virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; virtual std::shared_ptr getBattle(const BattleID & battleID) = 0; + virtual std::optional getPlayerID() const = 0; }; class IGameActionCallback @@ -114,20 +115,23 @@ class CBattleCallback : public IBattleCallback { std::map> activeBattles; + std::optional player; + protected: int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) CClient *cl; public: - CBattleCallback(std::optional Player, CClient * C); + CBattleCallback(std::optional player, CClient * C); void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override; void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; std::shared_ptr getBattle(const BattleID & battleID) override; + std::optional getPlayerID() const override; - void onBattleStarted(const BattleID & battleID); + void onBattleStarted(const IBattleInfo * info); void onBattleEnded(const BattleID & battleID); #if SCRIPTING_ENABLED @@ -145,9 +149,11 @@ public: virtual ~CCallback(); //client-specific functionalities (pathfinding) - virtual bool canMoveBetween(const int3 &a, const int3 &b); - virtual int3 getGuardingCreaturePosition(int3 tile); - virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); + bool canMoveBetween(const int3 &a, const int3 &b); + int3 getGuardingCreaturePosition(int3 tile); + std::shared_ptr getPathsInfo(const CGHeroInstance * h); + + std::optional getPlayerID() const override; //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. void registerBattleInterface(std::shared_ptr battleEvents); diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 0866394fb..7e422d12d 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -658,7 +658,7 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build } } -void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) +void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) { // when battle starts, game will send battleStart pack *before* movement confirmation // and since network thread wait for battle intro to play, movement confirmation will only happen after intro @@ -670,7 +670,7 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat waitForAllDialogs(); } -void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) +void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) { EVENT_HANDLER_CALLED_BY_CLIENT; @@ -685,7 +685,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); autofightingAI->initBattleInterface(env, cb, autocombatPreferences); - autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false); + autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false); isAutoFightOn = true; cb->registerBattleInterface(autofightingAI); } @@ -697,7 +697,7 @@ void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet BATTLE_EVENT_POSSIBLE_RETURN; } -void CPlayerInterface::battleUnitsChanged(const std::vector & units) +void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector & units) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -708,7 +708,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector & units { case UnitChanges::EOperation::RESET_STATE: { - const CStack * stack = cb->battleGetStackByID(info.id ); + const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id ); if(!stack) { @@ -723,7 +723,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector & units break; case UnitChanges::EOperation::ADD: { - const CStack * unit = cb->battleGetStackByID(info.id); + const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id); if(!unit) { logGlobal->error("Invalid unit ID %d", info.id); @@ -739,7 +739,7 @@ void CPlayerInterface::battleUnitsChanged(const std::vector & units } } -void CPlayerInterface::battleObstaclesChanged(const std::vector & obstacles) +void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -751,7 +751,7 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector { if(change.operation == BattleChanges::EOperation::ADD) { - auto instance = cb->battleGetObstacleByID(change.id); + auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id); if(instance) newObstacles.push_back(instance); else @@ -770,7 +770,7 @@ void CPlayerInterface::battleObstaclesChanged(const std::vector battleInt->fieldController->redrawBackgroundWithHexes(); } -void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca) +void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -778,15 +778,15 @@ void CPlayerInterface::battleCatapultAttacked(const CatapultAttack & ca) battleInt->stackIsCatapulting(ca); } -void CPlayerInterface::battleNewRound(int round) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn +void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->newRound(round); + battleInt->newRound(); } -void CPlayerInterface::actionStarted(const BattleAction &action) +void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -794,7 +794,7 @@ void CPlayerInterface::actionStarted(const BattleAction &action) battleInt->startAction(action); } -void CPlayerInterface::actionFinished(const BattleAction &action) +void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -802,17 +802,17 @@ void CPlayerInterface::actionFinished(const BattleAction &action) battleInt->endAction(action); } -void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn of that stack +void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack { EVENT_HANDLER_CALLED_BY_CLIENT; logGlobal->trace("Awaiting command for %s", stack->nodeName()); - assert(!cb->battleIsFinished()); - if (cb->battleIsFinished()) + assert(!cb->getBattle(battleID)->battleIsFinished()); + if (cb->getBattle(battleID)->battleIsFinished()) { logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); return ; } @@ -823,7 +823,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn //FIXME: we want client rendering to proceed while AI is making actions // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells auto unlockPim = vstd::makeUnlockGuard(*pim); - autofightingAI->activeStack(stack); + autofightingAI->activeStack(battleID, stack); return; } @@ -835,7 +835,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn if(!battleInt) { // probably battle is finished already - cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); } { @@ -845,7 +845,7 @@ void CPlayerInterface::activeStack(const CStack * stack) //called when it's turn } } -void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) +void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; if(isAutoFightOn || autofightingAI) @@ -880,7 +880,7 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID) battleInt->battleFinished(*br, queryID); } -void CPlayerInterface::battleLogMessage(const std::vector & lines) +void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector & lines) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -888,28 +888,28 @@ void CPlayerInterface::battleLogMessage(const std::vector & lines) battleInt->displayBattleLog(lines); } -void CPlayerInterface::battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) +void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->stackMoved(stack, dest, distance, teleport); } -void CPlayerInterface::battleSpellCast( const BattleSpellCast *sc ) +void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->spellCast(sc); } -void CPlayerInterface::battleStacksEffectsSet( const SetStackEffect & sse ) +void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; battleInt->battleStacksEffectsSet(sse); } -void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte) +void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -923,7 +923,7 @@ void CPlayerInterface::battleTriggerEffect (const BattleTriggerEffect & bte) battleInt->windowObject->heroManaPointsChanged(manaDrainedHero); } } -void CPlayerInterface::battleStacksAttacked(const std::vector & bsa, bool ranged) +void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -931,8 +931,8 @@ void CPlayerInterface::battleStacksAttacked(const std::vector arg; for(auto & elem : bsa) { - const CStack * defender = cb->battleGetStackByID(elem.stackAttacked, false); - const CStack * attacker = cb->battleGetStackByID(elem.attackerID, false); + const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false); + const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false); assert(defender); @@ -955,13 +955,13 @@ void CPlayerInterface::battleStacksAttacked(const std::vectorstacksAreAttacked(arg); } -void CPlayerInterface::battleAttack(const BattleAttack * ba) +void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; StackAttackInfo info; - info.attacker = cb->battleGetStackByID(ba->stackAttacking); + info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking); info.defender = nullptr; info.indirectAttack = ba->shot(); info.lucky = ba->lucky(); @@ -979,11 +979,11 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba) if(!elem.isSecondary()) { assert(info.defender == nullptr); - info.defender = cb->battleGetStackByID(elem.stackAttacked); + info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked); } else { - info.secondaryDefender.push_back(cb->battleGetStackByID(elem.stackAttacked)); + info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked)); } } assert(info.defender != nullptr); @@ -992,7 +992,7 @@ void CPlayerInterface::battleAttack(const BattleAttack * ba) battleInt->stackAttacking(info); } -void CPlayerInterface::battleGateStateChanged(const EGateState state) +void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; @@ -1705,12 +1705,12 @@ void CPlayerInterface::tryDigging(const CGHeroInstance * h) showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); } -void CPlayerInterface::battleNewRoundFirst( int round ) +void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) { EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - battleInt->newRoundFirst(round); + battleInt->newRoundFirst(); } void CPlayerInterface::stopMovement() diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 4a74fd8d2..5319d5c63 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -152,25 +152,25 @@ protected: // Call-ins from server, should not be called directly, but only via void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; //for battles - void actionFinished(const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero - void actionStarted(const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero - void activeStack(const CStack * stack) override; //called when it's turn of that stack - void battleAttack(const BattleAttack *ba) override; //stack performs attack - void battleEnd(const BattleResult *br, QueryID queryID) override; //end of battle - void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied; used for HP regen handling - void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleLogMessage(const std::vector & lines) override; - void battleStackMoved(const CStack * stack, std::vector dest, int distance, bool teleport) override; - void battleSpellCast(const BattleSpellCast *sc) override; - void battleStacksEffectsSet(const SetStackEffect & sse) override; //called when a specific effect is set to stacks - void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect - void battleStacksAttacked(const std::vector & bsa, bool ranged) override; - void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right - void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right - void battleUnitsChanged(const std::vector & units) override; - void battleObstaclesChanged(const std::vector & obstacles) override; - void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack - void battleGateStateChanged(const EGateState state) override; + void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleLogMessage(const BattleID & battleID, const std::vector & lines) override; + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks + void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; + void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack + void battleGateStateChanged(const BattleID & battleID, const EGateState state) override; void yourTacticPhase(const BattleID & battleID, int distance) override; std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; diff --git a/client/Client.cpp b/client/Client.cpp index 44652aeb7..96a8a8173 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -122,9 +122,9 @@ events::EventBus * CPlayerEnvironment::eventBus() const return cl->eventBus();//always get actual value } -const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle() const +const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const { - return mainCallback.get(); + return mainCallback->getBattle(battleID).get(); } const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const @@ -153,9 +153,9 @@ const Services * CClient::services() const return VLC; //todo: this should be CGI } -const CClient::BattleCb * CClient::battle() const +const CClient::BattleCb * CClient::battle(const BattleID & battleID) const { - return this; + return nullptr; //todo? } const CClient::GameCb * CClient::game() const @@ -345,7 +345,7 @@ void CClient::serialize(BinaryDeserializer & h, const int version) void CClient::save(const std::string & fname) { - if(gs->curB) + if(!gs->currentBattles.empty()) { logNetwork->error("Game cannot be saved during battle!"); return; @@ -565,14 +565,12 @@ int CClient::sendRequest(const CPackForServer * request, PlayerColor player) void CClient::battleStarted(const BattleInfo * info) { - setBattle(info); - for(auto & battleCb : battleCallbacks) { if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) || !battleCb.first.isValidPlayer()) { - battleCb.second->setBattle(info); + battleCb.second->onBattleStarted(info); } } @@ -583,7 +581,7 @@ void CClient::battleStarted(const BattleInfo * info) auto callBattleStart = [&](PlayerColor color, ui8 side) { if(vstd::contains(battleints, color)) - battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); + battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); }; callBattleStart(leftSide.color, 0); @@ -601,11 +599,11 @@ void CClient::battleStarted(const BattleInfo * info) //Remove player interfaces for auto battle (quickCombat option) if(att && att->isAutoFightOn) { - if (att->cb->battleGetTacticDist()) + if (att->cb->getBattle(info->battleID)->battleGetTacticDist()) { - auto side = att->cb->playerToSide(att->playerID); + auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID); auto action = BattleAction::makeEndOFTacticPhase(*side); - att->cb->battleMakeTacticAction(action); + att->cb->battleMakeTacticAction(info->battleID, action); } att.reset(); @@ -623,7 +621,7 @@ void CClient::battleStarted(const BattleInfo * info) { //TODO: This certainly need improvement auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->setBattle(info); + spectratorInt->cb->onBattleStarted(info); boost::unique_lock un(*CPlayerInterface::pim); CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); } @@ -638,20 +636,17 @@ void CClient::battleStarted(const BattleInfo * info) } } -void CClient::battleFinished() +void CClient::battleFinished(const BattleID & battleID) { - for(auto & side : gs->curB->sides) + for(auto & side : gs->getBattle(battleID)->sides) if(battleCallbacks.count(side.color)) - battleCallbacks[side.color]->setBattle(nullptr); + battleCallbacks[side.color]->onBattleEnded(battleID); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->setBattle(nullptr); - - setBattle(nullptr); - gs->curB.dellNull(); + battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); } -void CClient::startPlayerBattleAction(PlayerColor color) +void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) { assert(vstd::contains(battleints, color)); @@ -661,7 +656,7 @@ void CClient::startPlayerBattleAction(PlayerColor color) auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human); assert(vstd::contains(battleints, color)); - battleints[color]->activeStack(gs->curB->battleGetStackByID(gs->curB->activeStack, false)); + battleints[color]->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); } } diff --git a/client/Client.h b/client/Client.h index 48816798f..6155b9486 100644 --- a/client/Client.h +++ b/client/Client.h @@ -24,6 +24,7 @@ class CGameInterface; class BinaryDeserializer; class BinarySerializer; class BattleAction; +class BattleInfo; template class CApplier; @@ -150,8 +151,8 @@ public: int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request void battleStarted(const BattleInfo * info); - void battleFinished(); - void startPlayerBattleAction(PlayerColor color); + void battleFinished(const BattleID & battleID); + void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); void invalidatePaths(); std::shared_ptr getPathsInfo(const CGHeroInstance * h); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 3a89d8e1b..6df37a4f0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -95,18 +95,18 @@ void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) //calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy template -void callBattleInterfaceIfPresentForBothSides(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) +void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args) { - assert(cl.gameState()->curB); + assert(cl.gameState()->getBattle(battleID)); - if (!cl.gameState()->curB) + if (!cl.gameState()->getBattle(battleID)) { logGlobal->error("Attempt to call battle interface without ongoing battle!"); return; } - callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[0].color, ptr, std::forward(args)...); - callOnlyThatBattleInterface(cl, cl.gameState()->curB->sides[1].color, ptr, std::forward(args)...); + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward(args)...); + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward(args)...); if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) { callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); @@ -714,11 +714,11 @@ void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack) { // Cannot use the usual code because curB is not set yet - callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); } @@ -729,12 +729,12 @@ void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRoundFirst, pack.round); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID); } void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleNewRound, pack.round); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID); } void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) @@ -742,56 +742,56 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & if(!pack.askPlayerInterface) return; - const CStack *activated = gs.curB->battleGetStackByID(pack.stack); + const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); PlayerColor playerToCall; //pack.player that will move activated stack if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) { - playerToCall = (gs.curB->sides[0].color == activated->unitOwner() - ? gs.curB->sides[1].color - : gs.curB->sides[0].color); + playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner() + ? gs.getBattle(pack.battleID)->sides[1].color + : gs.getBattle(pack.battleID)->sides[0].color); } else { playerToCall = activated->unitOwner(); } - cl.startPlayerBattleAction(playerToCall); + cl.startPlayerBattleAction(pack.battleID, playerToCall); } void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleLogMessage, pack.lines); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines); } void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleTriggerEffect, pack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack); } void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleGateStateChanged, pack.state); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state); } void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleEnd, &pack, pack.queryID); - cl.battleFinished(); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID); + cl.battleFinished(pack.battleID); } void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack) { - const CStack * movedStack = gs.curB->battleGetStackByID(pack.stack); - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStackMoved, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); + const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); } void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleAttack, &pack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack); // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.bsa, pack.shot()); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot()); } void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) @@ -801,23 +801,23 @@ void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) { cl.currentBattleAction = std::make_unique(pack.ba); - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionStarted, pack.ba); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba); } void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleSpellCast, &pack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack); } void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack) { //informing about effects - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksEffectsSet, pack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack); } void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleStacksAttacked, pack.stacks, false); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false); } void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) @@ -829,24 +829,24 @@ void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleUnitsChanged, pack.changedStacks); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks); } void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack) { //inform interfaces about removed obstacles - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleObstaclesChanged, pack.changes); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes); } void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) { //inform interfaces about catapult attack - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::battleCatapultAttacked, pack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack); } void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) { - callBattleInterfaceIfPresentForBothSides(cl, &IBattleEventsReceiver::actionFinished, *cl.currentBattleAction); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction); cl.currentBattleAction.reset(); } diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 55b01d2af..43eb0729b 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -171,7 +171,7 @@ void BattleActionsController::enterCreatureCastingMode() spells::Target target; target.emplace_back(); - spells::BattleCast cast(owner.curInt->cb.get(), caster, spells::Mode::CREATURE_ACTIVE, spell); + spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell); auto m = spell->battleMechanics(&cast); spells::detail::ProblemImpl ignored; @@ -207,7 +207,7 @@ std::vector BattleActionsController::getPossibleActi data.creatureSpellsToCast.push_back(spell->id); data.tacticsMode = owner.tacticsMode; - auto allActions = owner.curInt->cb->getClientActionsForStack(stack, data); + auto allActions = owner.getBattle()->getClientActionsForStack(stack, data); allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); @@ -231,7 +231,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac case PossiblePlayerBattleAction::OBSTACLE: if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) { - PlayerColor stackOwner = owner.curInt->cb->battleGetOwner(targetStack); + PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack); bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; @@ -300,12 +300,12 @@ void BattleActionsController::castThisSpell(SpellID spellID) //choosing possible targets const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; assert(castingHero); // code below assumes non-null hero - PossiblePlayerBattleAction spellSelMode = owner.curInt->cb->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); + PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location { heroSpellToCast->aimToHex(BattleHex::INVALID); - owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast); + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); endCastingSpell(); } else @@ -353,10 +353,10 @@ const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) { - const CStack * shere = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true); if(shere) return shere; - return owner.curInt->cb->battleGetStackByPos(hoveredHex, false); + return owner.getBattle()->battleGetStackByPos(hoveredHex, false); } void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) @@ -400,7 +400,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, } case PossiblePlayerBattleAction::SHOOT: - if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) + if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); else CCS->curh->set(Cursor::Combat::SHOOT); @@ -482,7 +482,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return { BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); @@ -493,7 +493,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle { const auto * shooter = owner.stacksController->getActiveStack(); - DamageEstimation estimation = owner.curInt->cb->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); @@ -593,7 +593,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::ATTACK: case PossiblePlayerBattleAction::WALK_AND_ATTACK: case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - if(owner.curInt->cb->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) + if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) { if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? return true; @@ -601,7 +601,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B return false; case PossiblePlayerBattleAction::SHOOT: - return owner.curInt->cb->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); + return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); case PossiblePlayerBattleAction::NO_LOCATION: return false; @@ -615,7 +615,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures { - int spellID = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); + int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); return spellID > -1; } return false; @@ -658,7 +658,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B { if(owner.stacksController->getActiveStack()->doubleWide()) { - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); + std::vector acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); if(vstd::contains(acc, targetHex)) owner.giveCommand(EActionType::WALK, targetHex); @@ -770,7 +770,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B heroSpellToCast->aimToHex(targetHex); break; } - owner.curInt->cb->battleMakeSpellAction(*heroSpellToCast); + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); endCastingSpell(); } selectedStack = nullptr; @@ -886,7 +886,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS { // faerie dragon can cast only one, randomly selected spell until their next move //TODO: faerie dragon type spell should be selected by server - const auto * spellToCast = owner.curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); if (spellToCast) creatureSpells.push_back(spellToCast); @@ -933,7 +933,7 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, target.emplace_back(targetStack); target.emplace_back(targetHex); - spells::BattleCast cast(owner.curInt->cb.get(), caster, mode, currentSpell); + spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell); auto m = currentSpell->battleMechanics(&cast); spells::detail::ProblemImpl problem; //todo: display problem in status bar @@ -943,7 +943,7 @@ bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const { - std::vector acc = owner.curInt->cb->battleGetAvailableHexes(stackToMove, false); + std::vector acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false); BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); if (vstd::contains(acc, myNumber)) @@ -1006,7 +1006,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex) return; } - auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true); + auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true); if (selectedStack != nullptr) GH.windows().createAndPushWindow(selectedStack, true); diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 3a89b0661..fa24a13a4 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -970,7 +970,7 @@ bool EffectAnimation::init() } else { - const auto * destStack = owner.getCurrentPlayerInterface()->cb->battleGetUnitByPos(battlehexes[i], false); + const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false); Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index bdaba92af..66ed2e52b 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -59,7 +59,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt { owner.checkForAnimations(); - const CStack * stack = owner.curInt->cb->battleGetStackByID(bte.stackID); + const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID); if(!stack) { logGlobal->error("Invalid stack ID %d", bte.stackID); @@ -98,7 +98,7 @@ void BattleEffectsController::startAction(const BattleAction & action) { owner.checkForAnimations(); - const CStack *stack = owner.curInt->cb->battleGetStackByID(action.stackNumber); + const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber); switch(action.actionType) { diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index 8eda33038..e85a18ad9 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -141,7 +141,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner): if(!owner.siegeController) { - auto bfieldType = owner.curInt->cb->battleGetBattlefieldType(); + auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); if(bfieldType == BattleField::NONE) logGlobal->error("Invalid battlefield returned for current battle"); @@ -284,7 +284,7 @@ void BattleFieldController::redrawBackgroundWithHexes() const CStack *activeStack = owner.stacksController->getActiveStack(); std::vector attackableHexes; if(activeStack) - occupiableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); + occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); // prepare background graphic with hexes and shaded hexes backgroundWithHexes->draw(background, Point(0,0)); @@ -339,7 +339,7 @@ std::set BattleFieldController::getHighlightedHexesForActiveStack() auto hoveredHex = getHoveredHex(); - std::set set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); + std::set set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); for(BattleHex hex : set) result.insert(hex); @@ -359,10 +359,10 @@ std::set BattleFieldController::getMovementRangeForHoveredStack() auto hoveredHex = getHoveredHex(); // add possible movement hexes for stack under mouse - const CStack * const hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); if(hoveredStack) { - std::vector v = owner.curInt->cb->battleGetAvailableHexes(hoveredStack, true, true, nullptr); + std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); for(BattleHex hex : v) result.insert(hex); } @@ -387,7 +387,7 @@ std::set BattleFieldController::getHighlightedHexesForSpellRange() if(caster && spell) //when casting spell { // printing shaded hex(es) - spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell); + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex); for(BattleHex shadedHex : shadedHexes) @@ -407,10 +407,10 @@ std::set BattleFieldController::getHighlightedHexesForMovementTarget( if(!stack) return {}; - std::vector availableHexes = owner.curInt->cb->battleGetAvailableHexes(stack, false, false, nullptr); + std::vector availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr); - auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); - if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex)) + auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex)) { if(isTileAttackable(hoveredHex)) { @@ -670,7 +670,7 @@ BattleHex BattleFieldController::getHoveredHex() const CStack* BattleFieldController::getHoveredStack() { auto hoveredHex = getHoveredHex(); - const CStack* hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); return hoveredStack; } @@ -856,7 +856,7 @@ bool BattleFieldController::isTileAttackable(const BattleHex & number) const void BattleFieldController::updateAccessibleHexes() { - auto accessibility = owner.curInt->cb->getAccesibility(); + auto accessibility = owner.getBattle()->getAccesibility(); for(int i = 0; i < accessibility.size(); i++) stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 2cd96c9d9..b857e565e 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -68,9 +68,9 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * } //hot-seat -> check tactics for both players (defender may be local human) - if(attackerInt && attackerInt->cb->battleGetTacticDist()) + if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist()) tacticianInterface = attackerInt; - else if(defenderInt && defenderInt->cb->battleGetTacticDist()) + else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist()) tacticianInterface = defenderInt; //if we found interface of player with tactics, then enter tactics mode @@ -80,7 +80,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * this->army1 = army1; this->army2 = army2; - const CGTownInstance *town = curInt->cb->battleGetDefendedTown(); + const CGTownInstance *town = getBattle()->battleGetDefendedTown(); if(town && town->hasFort()) siegeController.reset(new BattleSiegeController(*this, town)); @@ -223,12 +223,12 @@ void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) stacksController->stackAttacking(attackInfo); } -void BattleInterface::newRoundFirst( int round ) +void BattleInterface::newRoundFirst() { waitForAnimations(); } -void BattleInterface::newRound(int number) +void BattleInterface::newRound() { console->addText(CGI->generaltexth->allTexts[412]); } @@ -241,7 +241,7 @@ void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID sp actor = stacksController->getActiveStack(); } - auto side = curInt->cb->playerToSide(curInt->playerID); + auto side = getBattle()->playerToSide(curInt->playerID); if(!side) { logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); @@ -265,11 +265,11 @@ void BattleInterface::sendCommand(BattleAction command, const CStack * actor) { logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); stacksController->setActiveStack(nullptr); - curInt->cb->battleMakeUnitAction(command); + curInt->cb->battleMakeUnitAction(battleID, command); } else { - curInt->cb->battleMakeTacticAction(command); + curInt->cb->battleMakeTacticAction(battleID, command); stacksController->setActiveStack(nullptr); //next stack will be activated when action ends } @@ -368,13 +368,13 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) if ( sc->activeCast ) { - const CStack * casterStack = curInt->cb->battleGetStackByID(sc->casterStack); + const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack); if(casterStack != nullptr ) { addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); + stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); displaySpellCast(spell, casterStack->getPosition()); }); } @@ -385,7 +385,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, curInt->cb->battleGetStackByPos(targetedTile), spell)); + stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); }); } } @@ -397,7 +397,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) //queuing affect animation for(auto & elem : sc->affectedCres) { - auto stack = curInt->cb->battleGetStackByID(elem, false); + auto stack = getBattle()->battleGetStackByID(elem, false); assert(stack); if(stack) { @@ -409,7 +409,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) for(auto & elem : sc->reflectedCres) { - auto stack = curInt->cb->battleGetStackByID(elem, false); + auto stack = getBattle()->battleGetStackByID(elem, false); assert(stack); addToAnimationStage(EAnimationEvents::HIT, [=](){ effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); @@ -425,7 +425,7 @@ void BattleInterface::spellCast(const BattleSpellCast * sc) for(auto & elem : sc->resistedCres) { - auto stack = curInt->cb->battleGetStackByID(elem, false); + auto stack = getBattle()->battleGetStackByID(elem, false); assert(stack); addToAnimationStage(EAnimationEvents::HIT, [=](){ effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); @@ -487,7 +487,7 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp if (!animation.effectName.empty()) { - const CStack * destStack = getCurrentPlayerInterface()->cb->battleGetStackByPos(destinationTile, false); + const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false); if (destStack) stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); @@ -566,12 +566,22 @@ bool BattleInterface::makingTurn() const return stacksController->getActiveStack() != nullptr; } +BattleID BattleInterface::getBattleID() const +{ + return battleID; +} + +std::shared_ptr BattleInterface::getBattle() const +{ + return curInt->cb->getBattle(battleID); +} + void BattleInterface::endAction(const BattleAction &action) { // it is possible that tactics mode ended while opening music is still playing waitForAnimations(); - const CStack *stack = curInt->cb->battleGetStackByID(action.stackNumber); + const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber); // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast activateStack(); @@ -606,7 +616,7 @@ void BattleInterface::startAction(const BattleAction & action) if (!action.isUnitAction()) return; - assert(curInt->cb->battleGetStackByID(action.stackNumber)); + assert(getBattle()->battleGetStackByID(action.stackNumber)); windowObject->updateQueue(); effectsController->startAction(action); } @@ -616,10 +626,10 @@ void BattleInterface::tacticPhaseEnd() stacksController->setActiveStack(nullptr); tacticsMode = false; - auto side = tacticianInterface->cb->playerToSide(tacticianInterface->playerID); + auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); auto action = BattleAction::makeEndOFTacticPhase(*side); - tacticianInterface->cb->battleMakeTacticAction(action); + tacticianInterface->cb->battleMakeTacticAction(battleID, action); } static bool immobile(const CStack *s) @@ -635,7 +645,7 @@ void BattleInterface::tacticNextStack(const CStack * current) //no switching stacks when the current one is moving checkForAnimations(); - TStacks stacksOfMine = tacticianInterface->cb->battleGetStacks(CBattleCallback::ONLY_MINE); + TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE); vstd::erase_if (stacksOfMine, &immobile); if (stacksOfMine.empty()) { @@ -687,7 +697,7 @@ void BattleInterface::requestAutofightingAIToTakeAction() { assert(curInt->isAutoFightOn); - if(curInt->cb->battleIsFinished()) + if(getBattle()->battleIsFinished()) { return; // battle finished with spellcast } @@ -716,7 +726,7 @@ void BattleInterface::requestAutofightingAIToTakeAction() boost::thread aiThread([this, activeStack]() { setThreadName("autofightingAI"); - curInt->autofightingAI->activeStack(activeStack); + curInt->autofightingAI->activeStack(battleID, activeStack); }); aiThread.detach(); } diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index 4124c5f47..8ca2e8015 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -30,6 +30,7 @@ struct BattleTriggerEffect; struct BattleHex; struct InfoAboutHero; class ObstacleChanges; +class CPlayerBattleCallback; VCMI_LIB_NAMESPACE_END @@ -115,6 +116,9 @@ class BattleInterface /// if set to true, battle is still starting and waiting for intro sound to end / key press from player bool battleOpeningDelayActive; + /// ID of ongoing battle + BattleID battleID; + void playIntroSoundAndUnlockInterface(); void onIntroSoundPlayed(); public: @@ -149,6 +153,9 @@ public: bool makingTurn() const; + BattleID getBattleID() const; + std::shared_ptr getBattle() const; + BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); ~BattleInterface(); @@ -196,8 +203,8 @@ public: void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest - void newRoundFirst( int round ); - void newRound(int number); //caled when round is ended; number is the number of round + void newRoundFirst(); + void newRound(); //caled when round is ended; void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 94ea31b38..81c22b91d 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -289,7 +289,7 @@ void BattleHero::heroLeftClicked() if(!hero || !owner.makingTurn()) return; - if(owner.getCurrentPlayerInterface()->cb->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions + if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions { CCS->curh->set(Cursor::Map::POINTER); GH.windows().createAndPushWindow(hero, owner.getCurrentPlayerInterface()); @@ -502,7 +502,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface for(int i = 0; i < 2; i++) { - auto heroInfo = owner.cb->battleGetHeroInfo(i); + auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i); const int xs[] = {21, 392}; if(heroInfo.portrait >= 0) //attacking hero @@ -512,7 +512,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface } else { - auto stacks = owner.cb->battleGetAllStacks(); + auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks(); vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison { return stack->unitSide() != i || !stack->base; @@ -561,7 +561,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface } } //printing result description - bool weAreAttacker = !(owner.cb->battleGetMySide()); + bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won { int text = 304; @@ -584,7 +584,7 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface CCS->videoh->open("WIN3.BIK"); std::string str = CGI->generaltexth->allTexts[text]; - const CGHeroInstance * ourHero = owner.cb->battleGetMyHero(); + const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); if (ourHero) { str += CGI->generaltexth->allTexts[305]; @@ -714,7 +714,7 @@ void StackQueue::update() { std::vector queueData; - owner.getCurrentPlayerInterface()->cb->battleGetTurnOrder(queueData, stackBoxes.size(), 0); + owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0); size_t boxIndex = 0; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index d41b09a66..2c2cca455 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -31,7 +31,7 @@ BattleObstacleController::BattleObstacleController(BattleInterface & owner): owner(owner), timePassed(0.f) { - auto obst = owner.curInt->cb->battleGetAllObstacles(); + auto obst = owner.getBattle()->battleGetAllObstacles(); for(auto & elem : obst) { if ( elem->obstacleType == CObstacleInstance::MOAT ) @@ -99,9 +99,9 @@ void BattleObstacleController::obstaclePlaced(const std::vectorcb->playerToSide(owner.curInt->playerID); + auto side = owner.getBattle()->playerToSide(owner.curInt->playerID); - if(!oi->visibleForSide(side.value(), owner.curInt->cb->battleHasNativeStack(side.value()))) + if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value()))) continue; auto animation = std::make_shared(oi->getAppearAnimation()); @@ -127,7 +127,7 @@ void BattleObstacleController::obstaclePlaced(const std::vectorcb->battleGetAllObstacles()) + for(auto & obstacle : owner.getBattle()->battleGetAllObstacles()) { if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) { @@ -153,7 +153,7 @@ void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer) { - for (auto obstacle : owner.curInt->cb->battleGetAllObstacles()) + for (auto obstacle : owner.getBattle()->battleGetAllObstacles()) { if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) continue; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 63c8cab10..9be33e30a 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -133,9 +133,9 @@ bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) { case EWallVisual::MOAT: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); case EWallVisual::MOAT_BANK: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); - case EWallVisual::KEEP_BATTLEMENT: return town->hasBuilt(BuildingID::CITADEL) && owner.curInt->cb->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; - case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; - case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.curInt->cb->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; + case EWallVisual::KEEP_BATTLEMENT: return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; + case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; + case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; default: return true; } } @@ -220,7 +220,7 @@ Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) con void BattleSiegeController::gateStateChanged(const EGateState state) { - auto oldState = owner.curInt->cb->battleGetGateState(); + auto oldState = owner.getBattle()->battleGetGateState(); bool playSound = false; auto stateId = EWallState::NONE; switch(state) @@ -275,7 +275,7 @@ BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wal const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const { - for (auto & stack : owner.curInt->cb->battleGetAllStacks(true)) + for (auto & stack : owner.getBattle()->battleGetAllStacks(true)) { if ( stack->initialPosition == getTurretBattleHex(wallPiece)) return stack; @@ -318,15 +318,15 @@ bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const if (owner.tacticsMode) return false; - auto wallPart = owner.curInt->cb->battleHexToWallPart(hex); - return owner.curInt->cb->isWallPartAttackable(wallPart); + auto wallPart = owner.getBattle()->battleHexToWallPart(hex); + return owner.getBattle()->isWallPartAttackable(wallPart); } void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) { if (ca.attacker != -1) { - const CStack *stack = owner.curInt->cb->battleGetStackByID(ca.attacker); + const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker); for (auto attackInfo : ca.attackedParts) { owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); @@ -353,7 +353,7 @@ void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) if (wallId == EWallVisual::GATE) continue; - auto wallState = EWallState(owner.curInt->cb->battleGetWallState(attackInfo.attackedPart)); + auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart)); wallPieceImages[wallId] = IImage::createFromFile(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index f83e97782..d9a060031 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -94,7 +94,7 @@ BattleStacksController::BattleStacksController(BattleInterface & owner): amountNegative->adjustPalette(shifterNegative, ignoredMask); amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask); - std::vector stacks = owner.curInt->cb->battleGetAllStacks(true); + std::vector stacks = owner.getBattle()->battleGetAllStacks(true); for(const CStack * s : stacks) { stackAdded(s, true); @@ -126,7 +126,7 @@ BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) { - auto stacks = owner.curInt->cb->battleGetAllStacks(false); + auto stacks = owner.getBattle()->battleGetAllStacks(false); for (auto stack : stacks) { @@ -359,7 +359,7 @@ void BattleStacksController::initializeBattleAnimations() void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed) { - for (auto stack : owner.curInt->cb->battleGetAllStacks(true)) + for (auto stack : owner.getBattle()->battleGetAllStacks(true)) { if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks continue; @@ -552,9 +552,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorcb->isToReverse( - attacker, - defender); + bool mustReverse = owner.getBattle()->isToReverse(attacker, defender); if (attacker->unitSide() == BattleSide::ATTACKER) return !mustReverse; @@ -670,7 +668,7 @@ void BattleStacksController::endAction(const BattleAction & action) owner.checkForAnimations(); //check if we should reverse stacks - TStacks stacks = owner.curInt->cb->battleGetStacks(CBattleCallback::MINE_AND_ENEMY); + TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY); for (const CStack *s : stacks) { @@ -847,7 +845,7 @@ std::vector BattleStacksController::selectHoveredStacks() auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId(); if(hoveredQueueUnitId.has_value()) { - return { owner.curInt->cb->battleGetStackByID(hoveredQueueUnitId.value(), true) }; + return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) }; } auto hoveredHex = owner.fieldController->getHoveredHex(); @@ -867,14 +865,14 @@ std::vector BattleStacksController::selectHoveredStacks() spells::Target target; target.emplace_back(hoveredHex); - spells::BattleCast event(owner.curInt->cb.get(), caster, mode, spell); + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); auto mechanics = spell->battleMechanics(&event); return mechanics->getAffectedStacks(target); } if(hoveredHex.isValid()) { - const CStack * const stack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true); + const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); if (stack) return {stack}; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 79f558932..0c71ac31b 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -367,10 +367,10 @@ void BattleWindow::bSurrenderf() if (owner.actionsController->spellcastingModeActive()) return; - int cost = owner.curInt->cb->battleGetSurrenderCost(); + int cost = owner.getBattle()->battleGetSurrenderCost(); if(cost >= 0) { - std::string enemyHeroName = owner.curInt->cb->battleGetEnemyHero().name; + std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name; if(enemyHeroName.empty()) { logGlobal->warn("Surrender performed without enemy hero, should not happen!"); @@ -387,7 +387,7 @@ void BattleWindow::bFleef() if (owner.actionsController->spellcastingModeActive()) return; - if ( owner.curInt->cb->battleCanFlee() ) + if ( owner.getBattle()->battleCanFlee() ) { CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? @@ -398,10 +398,10 @@ void BattleWindow::bFleef() std::string heroName; //calculating fleeing hero's name if (owner.attackingHeroInstance) - if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getMyColor()) + if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) heroName = owner.attackingHeroInstance->getNameTranslated(); if (owner.defendingHeroInstance) - if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getMyColor()) + if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) heroName = owner.defendingHeroInstance->getNameTranslated(); //calculating text auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! @@ -419,7 +419,7 @@ void BattleWindow::reallyFlee() void BattleWindow::reallySurrender() { - if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.curInt->cb->battleGetSurrenderCost()) + if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost()) { owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! } @@ -509,7 +509,7 @@ void BattleWindow::bAutofightf() autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); - ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false); + ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false); owner.curInt->autofightingAI = ai; owner.curInt->cb->registerBattleInterface(ai); @@ -531,7 +531,7 @@ void BattleWindow::bSpellf() CCS->curh->set(Cursor::Map::POINTER); - ESpellCastProblem spellCastProblem = owner.curInt->cb->battleCanCastSpell(myHero, spells::Mode::HERO); + ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO); if(spellCastProblem == ESpellCastProblem::OK) { @@ -633,11 +633,11 @@ void BattleWindow::bTacticPhaseEnd() void BattleWindow::blockUI(bool on) { bool canCastSpells = false; - auto hero = owner.curInt->cb->battleGetMyHero(); + auto hero = owner.getBattle()->battleGetMyHero(); if(hero) { - ESpellCastProblem spellcastingProblem = owner.curInt->cb->battleCanCastSpell(hero, spells::Mode::HERO); + ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO); //if magic is blocked, we leave button active, so the message can be displayed after button click canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; @@ -646,8 +646,8 @@ void BattleWindow::blockUI(bool on) bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false; setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on); - setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.curInt->cb->battleCanFlee()); - setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.curInt->cb->battleGetSurrenderCost() < 0); + setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee()); + setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0); setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells); setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait); setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode); diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 288a696cb..61f708068 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -503,7 +503,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) else if(combatSpell) { spells::detail::ProblemImpl problem; - if(mySpell->canBeCast(problem, owner->myInt->cb.get(), spells::Mode::HERO, owner->myHero)) + if(mySpell->canBeCast(problem, owner->myInt->battleInt->getBattle().get(), spells::Mode::HERO, owner->myHero)) { owner->myInt->battleInt->castThisSpell(mySpell->id); owner->fexitb(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 74ceb4317..abbb0539b 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -677,8 +677,8 @@ std::function CExchangeController::onSwapArmy() { return [&]() { - if(left->tempOwner != cb->getMyColor() - || right->tempOwner != cb->getMyColor()) + if(left->tempOwner != cb->getPlayerID() + || right->tempOwner != cb->getPlayerID()) { return; } @@ -720,7 +720,7 @@ std::function CExchangeController::onMoveStackToLeft(SlotID slotID) { return [=]() { - if(right->tempOwner != cb->getMyColor()) + if(right->tempOwner != cb->getPlayerID()) { return; } @@ -733,7 +733,7 @@ std::function CExchangeController::onMoveStackToRight(SlotID slotID) { return [=]() { - if(left->tempOwner != cb->getMyColor()) + if(left->tempOwner != cb->getPlayerID()) { return; } @@ -778,7 +778,7 @@ void CExchangeController::moveArmy(bool leftToRight) const CGarrisonSlot * selection = this->view->getSelectedSlotID(); SlotID slot; - if(source->tempOwner != cb->getMyColor()) + if(source->tempOwner != cb->getPlayerID()) { return; } @@ -807,7 +807,7 @@ void CExchangeController::moveArtifacts(bool leftToRight) const CGHeroInstance * source = leftToRight ? left : right; const CGHeroInstance * target = leftToRight ? right : left; - if(source->tempOwner != cb->getMyColor()) + if(source->tempOwner != cb->getPlayerID()) { return; } diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index ab2fbea2c..8cdec99e6 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -15,7 +15,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/battle/BattleStateInfoForRetreat.cpp ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.cpp ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.cpp - ${MAIN_LIB_DIR}/battle/CCallbackBase.cpp ${MAIN_LIB_DIR}/battle/CObstacleInstance.cpp ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.cpp ${MAIN_LIB_DIR}/battle/CUnitState.cpp @@ -339,7 +338,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/battle/BattleProxy.h ${MAIN_LIB_DIR}/battle/CBattleInfoCallback.h ${MAIN_LIB_DIR}/battle/CBattleInfoEssentials.h - ${MAIN_LIB_DIR}/battle/CCallbackBase.h ${MAIN_LIB_DIR}/battle/CObstacleInstance.h ${MAIN_LIB_DIR}/battle/CPlayerBattleCallback.h ${MAIN_LIB_DIR}/battle/CUnitState.h diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 3eac1f6be..b50d59e9b 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -66,6 +66,11 @@ bool CGameInfoCallback::isAllowed(int32_t type, int32_t id) const } } +std::optional CGameInfoCallback::getPlayerID() const +{ + return std::nullopt; +} + const Player * CGameInfoCallback::getPlayer(PlayerColor color) const { return getPlayerState(color, false); @@ -151,7 +156,7 @@ const CGObjectInstance* CGameInfoCallback::getObj(ObjectInstanceID objid, bool v return nullptr; } - if(!isVisible(ret, player) && ret->tempOwner != player) + if(!isVisible(ret, getPlayerID()) && ret->tempOwner != getPlayerID()) { if(verbose) logGlobal->error("Cannot get object with id %d. Object is not visible.", oid); @@ -232,7 +237,7 @@ void CGameInfoCallback::getThievesGuildInfo(SThievesGuildInfo & thi, const CGObj if(obj->ID == Obj::TOWN || obj->ID == Obj::TAVERN) { int taverns = 0; - for(auto town : gs->players[*player].towns) + for(auto town : gs->players[*getPlayerID()].towns) { if(town->hasBuilt(BuildingID::TAVERN)) taverns++; @@ -254,7 +259,7 @@ int CGameInfoCallback::howManyTowns(PlayerColor Player) const bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown & dest, const CGObjectInstance * selectedObject) const { - ERROR_RET_VAL_IF(!isVisible(town, player), "Town is not visible!", false); //it's not a town or it's not visible for layer + ERROR_RET_VAL_IF(!isVisible(town, getPlayerID()), "Town is not visible!", false); //it's not a town or it's not visible for layer bool detailed = hasAccess(town->tempOwner); if(town->ID == Obj::TOWN) @@ -305,9 +310,9 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if (infoLevel == InfoAboutHero::EInfoLevel::BASIC) { - auto ourBattle = gs->getBattle(*player); + auto ourBattle = gs->getBattle(*getPlayerID()); - if(ourBattle && ourBattle->playerHasAccessToHeroInfo(*player, h)) //if it's battle we can get enemy hero full data + if(ourBattle && ourBattle->playerHasAccessToHeroInfo(*getPlayerID(), h)) //if it's battle we can get enemy hero full data infoLevel = InfoAboutHero::EInfoLevel::INBATTLE; else ERROR_RET_VAL_IF(!isVisible(h->visitablePos()), "That hero is not visible!", false); @@ -324,7 +329,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero dest.initFromHero(h, infoLevel); //DISGUISED bonus implementation - if(getPlayerRelations(*player, hero->tempOwner) == PlayerRelations::ENEMIES) + if(getPlayerRelations(*getPlayerID(), hero->tempOwner) == PlayerRelations::ENEMIES) { //todo: bonus cashing int disguiseLevel = h->valOfBonuses(Selector::typeSubtype(BonusType::DISGUISED, 0)); @@ -422,7 +427,7 @@ bool CGameInfoCallback::isVisible(int3 pos, const std::optional & P bool CGameInfoCallback::isVisible(int3 pos) const { - return isVisible(pos, player); + return isVisible(pos, getPlayerID()); } bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optional & Player) const @@ -432,7 +437,7 @@ bool CGameInfoCallback::isVisible(const CGObjectInstance * obj, const std::optio bool CGameInfoCallback::isVisible(const CGObjectInstance *obj) const { - return isVisible(obj, player); + return isVisible(obj, getPlayerID()); } // const CCreatureSet* CInfoCallback::getGarrison(const CGObjectInstance *obj) const // { @@ -464,7 +469,7 @@ std::vector CGameInfoCallback::getVisitableObjs(int3 for(const CGObjectInstance * obj : t->visitableObjects) { - if(player || obj->ID != Obj::EVENT) //hide events from players + if(getPlayerID() || obj->ID != Obj::EVENT) //hide events from players ret.push_back(obj); } @@ -500,7 +505,7 @@ std::vector CGameInfoCallback::getAvailableHeroes(const const CGTownInstance * town = getTown(townOrTavern->id); if(townOrTavern->ID == Obj::TAVERN || (town && town->hasBuilt(BuildingID::TAVERN))) - return gs->heroesPool->getHeroesFor(*player); + return gs->heroesPool->getHeroesFor(*getPlayerID()); return ret; } @@ -531,8 +536,8 @@ EDiggingStatus CGameInfoCallback::getTileDigStatus(int3 tile, bool verbose) cons //TODO: typedef? std::shared_ptr> CGameInfoCallback::getAllVisibleTiles() const { - assert(player.has_value()); - const auto * team = getPlayerTeam(player.value()); + assert(getPlayerID().has_value()); + const auto * team = getPlayerTeam(getPlayerID().value()); size_t width = gs->map->width; size_t height = gs->map->height; @@ -631,7 +636,7 @@ const CMapHeader * CGameInfoCallback::getMapHeader() const bool CGameInfoCallback::hasAccess(std::optional playerId) const { - return !player || player->isSpectator() || gs->getPlayerRelations(*playerId, *player) != PlayerRelations::ENEMIES; + return !getPlayerID() || getPlayerID()->isSpectator() || gs->getPlayerRelations(*playerId, *getPlayerID()) != PlayerRelations::ENEMIES; } EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbose) const @@ -713,23 +718,22 @@ bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const return gs->actingPlayers.count(player); } -CGameInfoCallback::CGameInfoCallback(CGameState * GS, std::optional Player): +CGameInfoCallback::CGameInfoCallback(CGameState * GS): gs(GS) { - player = std::move(Player); } std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const { //boost::shared_lock lock(*gs->mx); - return gs->getPlayerTeam(*player)->fogOfWarMap; + return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap; } int CPlayerSpecificInfoCallback::howManyTowns() const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return CGameInfoCallback::howManyTowns(*player); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return CGameInfoCallback::howManyTowns(*getPlayerID()); } std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo(bool onlyOur) const @@ -740,7 +744,7 @@ std::vector < const CGTownInstance *> CPlayerSpecificInfoCallback::getTownsInfo( { for(const auto & town : i.second.towns) { - if(i.first == player || (!onlyOur && isVisible(town, player))) + if(i.first == getPlayerID() || (!onlyOur && isVisible(town, getPlayerID()))) { ret.push_back(town); } @@ -755,8 +759,8 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo for(auto hero : gs->map->heroesOnMap) { // !player || // - why would we even get access to hero not owned by any player? - if((hero->tempOwner == *player) || - (isVisible(hero->visitablePos(), player) && !onlyOur) ) + if((hero->tempOwner == *getPlayerID()) || + (isVisible(hero->visitablePos(), getPlayerID()) && !onlyOur) ) { ret.push_back(hero); } @@ -764,18 +768,13 @@ std::vector < const CGHeroInstance *> CPlayerSpecificInfoCallback::getHeroesInfo return ret; } -std::optional CPlayerSpecificInfoCallback::getMyColor() const -{ - return player; -} - int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned) const { if (hero->inTownGarrison && !includeGarrisoned) return -1; size_t index = 0; - auto & heroes = gs->players[*player].heroes; + auto & heroes = gs->players[*getPlayerID()].heroes; for (auto & heroe : heroes) { @@ -790,13 +789,13 @@ int CPlayerSpecificInfoCallback::getHeroSerial(const CGHeroInstance * hero, bool int3 CPlayerSpecificInfoCallback::getGrailPos( double *outKnownRatio ) { - if (!player || CGObelisk::obeliskCount == 0) + if (!getPlayerID() || CGObelisk::obeliskCount == 0) { *outKnownRatio = 0.0; } else { - TeamID t = gs->getPlayerTeam(*player)->id; + TeamID t = gs->getPlayerTeam(*getPlayerID())->id; double visited = 0.0; if(CGObelisk::visited.count(t)) visited = static_cast(CGObelisk::visited[t]); @@ -811,7 +810,7 @@ std::vector < const CGObjectInstance * > CPlayerSpecificInfoCallback::getMyObjec std::vector < const CGObjectInstance * > ret; for(const CGObjectInstance * obj : gs->map->objects) { - if(obj && obj->tempOwner == player) + if(obj && obj->tempOwner == getPlayerID()) ret.push_back(obj); } return ret; @@ -821,7 +820,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() { ASSERT_IF_CALLED_WITH_PLAYER std::vector < const CGDwelling * > ret; - for(CGDwelling * dw : gs->getPlayerState(*player)->dwellings) + for(CGDwelling * dw : gs->getPlayerState(*getPlayerID())->dwellings) { ret.push_back(dw); } @@ -831,7 +830,7 @@ std::vector < const CGDwelling * > CPlayerSpecificInfoCallback::getMyDwellings() std::vector CPlayerSpecificInfoCallback::getMyQuests() const { std::vector ret; - for(const auto & quest : gs->getPlayerState(*player)->quests) + for(const auto & quest : gs->getPlayerState(*getPlayerID())->quests) { ret.push_back (quest); } @@ -841,14 +840,14 @@ std::vector CPlayerSpecificInfoCallback::getMyQuests() const int CPlayerSpecificInfoCallback::howManyHeroes(bool includeGarrisoned) const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return getHeroCount(*player,includeGarrisoned); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return getHeroCount(*getPlayerID(), includeGarrisoned); } const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, bool includeGarrisoned) const { ASSERT_IF_CALLED_WITH_PLAYER - const PlayerState *p = getPlayerState(*player); + const PlayerState *p = getPlayerState(*getPlayerID()); ERROR_RET_VAL_IF(!p, "No player info", nullptr); if (!includeGarrisoned) @@ -864,7 +863,7 @@ const CGHeroInstance* CPlayerSpecificInfoCallback::getHeroBySerial(int serialId, const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) const { ASSERT_IF_CALLED_WITH_PLAYER - const PlayerState *p = getPlayerState(*player); + const PlayerState *p = getPlayerState(*getPlayerID()); ERROR_RET_VAL_IF(!p, "No player info", nullptr); ERROR_RET_VAL_IF(serialId < 0 || serialId >= p->towns.size(), "No player info", nullptr); return p->towns[serialId]; @@ -873,15 +872,15 @@ const CGTownInstance* CPlayerSpecificInfoCallback::getTownBySerial(int serialId) int CPlayerSpecificInfoCallback::getResourceAmount(GameResID type) const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", -1); - return getResource(*player, type); + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", -1); + return getResource(*getPlayerID(), type); } TResources CPlayerSpecificInfoCallback::getResourceAmount() const { //boost::shared_lock lock(*gs->mx); - ERROR_RET_VAL_IF(!player, "Applicable only for player callbacks", TResources()); - return gs->players[*player].resources; + ERROR_RET_VAL_IF(!getPlayerID(), "Applicable only for player callbacks", TResources()); + return gs->players[*getPlayerID()].resources; } const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const @@ -892,11 +891,11 @@ const TeamState * CGameInfoCallback::getTeam( TeamID teamID ) const if (team != gs->teams.end()) { const TeamState *ret = &team->second; - if(!player.has_value()) //neutral (or invalid) player + if(!getPlayerID().has_value()) //neutral (or invalid) player return ret; else { - if (vstd::contains(ret->players, *player)) //specific player + if (vstd::contains(ret->players, *getPlayerID())) //specific player return ret; else { @@ -942,7 +941,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const { - gs->getTilesInRange(tiles, pos, radious, *player, -1, distanceFormula); + gs->getTilesInRange(tiles, pos, radious, *getPlayerID(), -1, distanceFormula); } void CGameInfoCallback::calculatePaths(const std::shared_ptr & config) diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index e912b53e2..e1a19d5c3 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -11,7 +11,8 @@ #include "int3.h" #include "ResourceSet.h" // for Res -#include "battle/CCallbackBase.h" + +#define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} VCMI_LIB_NAMESPACE_BEGIN @@ -57,6 +58,7 @@ public: virtual bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill //player + virtual std::optional getPlayerID() const = 0; virtual const Player * getPlayer(PlayerColor color) const = 0; // virtual int getResource(PlayerColor Player, EGameResID which) const = 0; // bool isVisible(int3 pos) const; @@ -123,13 +125,13 @@ public: // bool isTeleportEntrancePassable(const CGTeleport * obj, PlayerColor player) const; }; -class DLL_LINKAGE CGameInfoCallback : public virtual CCallbackBase, public IGameInfoCallback +class DLL_LINKAGE CGameInfoCallback : public IGameInfoCallback { protected: CGameState * gs;//todo: replace with protected const getter, only actual Server and Client objects should hold game state CGameInfoCallback() = default; - CGameInfoCallback(CGameState * GS, std::optional Player); + CGameInfoCallback(CGameState * GS); bool hasAccess(std::optional playerId) const; bool canGetFullInfo(const CGObjectInstance *obj) const; //true we player owns obj or ally owns obj or privileged mode @@ -142,6 +144,7 @@ public: bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill //player + std::optional getPlayerID() const override; const Player * getPlayer(PlayerColor color) const override; virtual const PlayerState * getPlayerState(PlayerColor color, bool verbose = true) const; virtual int getResource(PlayerColor Player, GameResID which) const; @@ -229,7 +232,6 @@ public: virtual int howManyTowns() const; virtual int howManyHeroes(bool includeGarrisoned = true) const; virtual int3 getGrailPos(double *outKnownRatio); - virtual std::optional getMyColor() const; virtual std::vector getTownsInfo(bool onlyOur = true) const; //true -> only owned; false -> all visible virtual int getHeroSerial(const CGHeroInstance * hero, bool includeGarrisoned=true) const; diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index 3148aa65d..c2e49742d 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -157,9 +157,9 @@ CGlobalAI::CGlobalAI() human = false; } -void CAdventureAI::battleNewRound(const BattleID & battleID, int round) +void CAdventureAI::battleNewRound(const BattleID & battleID) { - battleAI->battleNewRound(battleID, round); + battleAI->battleNewRound(battleID); } void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) @@ -187,9 +187,9 @@ void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & battleAI->actionStarted(battleID, action); } -void CAdventureAI::battleNewRoundFirst(const BattleID & battleID, int round) +void CAdventureAI::battleNewRoundFirst(const BattleID & battleID) { - battleAI->battleNewRoundFirst(battleID, round); + battleAI->battleNewRoundFirst(battleID); } void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action) diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index 2f9fe192f..b0b9e6ce4 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -147,12 +147,12 @@ public: virtual void activeStack(const BattleID & battleID, const CStack * stack) override; virtual void yourTacticPhase(const BattleID & battleID, int distance) override; - virtual void battleNewRound(const BattleID & battleID, int round) override; + virtual void battleNewRound(const BattleID & battleID) override; virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; - virtual void battleNewRoundFirst(const BattleID & battleID, int round) override; + virtual void battleNewRoundFirst(const BattleID & battleID) override; virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 8139ee674..e3d81ec76 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -61,8 +61,8 @@ public: virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){}; - virtual void battleNewRoundFirst(const BattleID & battleID, int round){}; //called at the beginning of each turn before changes are applied; - virtual void battleNewRound(const BattleID & battleID, int round){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + virtual void battleNewRoundFirst(const BattleID & battleID){}; //called at the beginning of each turn before changes are applied; + virtual void battleNewRound(const BattleID & battleID){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn virtual void battleLogMessage(const BattleID & battleID, const std::vector & lines){}; virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport){}; virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){}; diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index a2b197f2e..b0d30a0c3 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -11,7 +11,6 @@ #include -#include "CCallbackBase.h" #include "ReachabilityInfo.h" #include "BattleAttackInfo.h" diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index 18456c345..d1921e965 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -8,7 +8,6 @@ * */ #pragma once -#include "CCallbackBase.h" #include "IBattleInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/battle/CCallbackBase.cpp b/lib/battle/CCallbackBase.cpp deleted file mode 100644 index 0693548b4..000000000 --- a/lib/battle/CCallbackBase.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/* - * CCallbackBase.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 "CCallbackBase.h" -#include "IBattleState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CCallbackBase::CCallbackBase(std::optional Player): - player(std::move(Player)) -{ -} - -std::optional CCallbackBase::getPlayerID() const -{ - return player; -} - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CCallbackBase.h b/lib/battle/CCallbackBase.h deleted file mode 100644 index 16e408974..000000000 --- a/lib/battle/CCallbackBase.h +++ /dev/null @@ -1,36 +0,0 @@ -/* - * CCallbackBase.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 "../GameConstants.h" - -#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } -#define ASSERT_IF_CALLED_WITH_PLAYER if(!getPlayerID()) {logGlobal->error(BOOST_CURRENT_FUNCTION); assert(0);} - -VCMI_LIB_NAMESPACE_BEGIN - -class IBattleInfo; -class BattleInfo; -class CBattleInfoEssentials; - -//Basic class for various callbacks (interfaces called by players to get info about game and so forth) -class DLL_LINKAGE CCallbackBase -{ -protected: - std::optional player; // not set gives access to all information, otherwise callback provides only information "visible" for player - - CCallbackBase(std::optional Player); - CCallbackBase() = default; - -public: - std::optional getPlayerID() const; -}; - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index 08e9112c0..4412c9d28 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -11,9 +11,27 @@ #include "CPlayerBattleCallback.h" #include "../CStack.h" #include "../gameState/InfoAboutArmy.h" +#include "../CGameInfoCallback.h" VCMI_LIB_NAMESPACE_BEGIN +CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player): + battle(battle), + player(player) +{ + +} + +const IBattleInfo * CPlayerBattleCallback::getBattle() const +{ + return battle; +} + +std::optional CPlayerBattleCallback::getPlayerID() const +{ + return player; +} + bool CPlayerBattleCallback::battleCanFlee() const { RETURN_IF_NOT_BATTLE(false); diff --git a/lib/battle/CPlayerBattleCallback.h b/lib/battle/CPlayerBattleCallback.h index f903a37af..60fbbd3d5 100644 --- a/lib/battle/CPlayerBattleCallback.h +++ b/lib/battle/CPlayerBattleCallback.h @@ -16,7 +16,15 @@ class CGHeroInstance; class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback { + const IBattleInfo * battle; + PlayerColor player; + public: + CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player); + + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + bool battleCanFlee() const; //returns true if caller can flee from the battle TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index cc03c4ab5..d216bf454 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -15,6 +15,8 @@ #include +#define RETURN_IF_NOT_BATTLE(...) if(!duringBattle()) {logGlobal->error("%s called when no battle!", __FUNCTION__); return __VA_ARGS__; } + VCMI_LIB_NAMESPACE_BEGIN struct CObstacleInstance; diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 78a9b307a..cf4910ea3 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -456,7 +456,7 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const } CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options): - CGameInfoCallback(gs, std::optional()), + CGameInfoCallback(gs), turn(-1), hero(Hero), options(Options), From 747e28947a49cca9bb8cf211de3cc35e3e823429 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 16:22:58 +0300 Subject: [PATCH 0342/1248] Fix build --- CCallback.cpp | 7 ------- CCallback.h | 4 ---- client/Client.cpp | 5 ----- client/Client.h | 1 - lib/battle/CPlayerBattleCallback.cpp | 7 +++++++ lib/battle/CPlayerBattleCallback.h | 4 ++++ scripting/lua/LuaScriptingContext.cpp | 2 +- server/CGameHandler.cpp | 8 ++++---- server/CGameHandler.h | 2 +- 9 files changed, 17 insertions(+), 23 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 542b781a1..3b8e099ba 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -365,13 +365,6 @@ void CCallback::unregisterBattleInterface(std::shared_ptr cl->additionalBattleInts[*player] -= battleEvents; } -#if SCRIPTING_ENABLED -scripting::Pool * CBattleCallback::getContextPool() const -{ - return cl->getGlobalContextPool(); -} -#endif - CBattleCallback::CBattleCallback(std::optional player, CClient * C): cl(C), player(player) diff --git a/CCallback.h b/CCallback.h index 33d0c8b99..8acb0d6c5 100644 --- a/CCallback.h +++ b/CCallback.h @@ -134,10 +134,6 @@ public: void onBattleStarted(const IBattleInfo * info); void onBattleEnded(const BattleID & battleID); -#if SCRIPTING_ENABLED - scripting::Pool * getContextPool() const override; -#endif - friend class CCallback; friend class CClient; }; diff --git a/client/Client.cpp b/client/Client.cpp index 96a8a8173..632cc5ef3 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -693,11 +693,6 @@ scripting::Pool * CClient::getGlobalContextPool() const { return clientScripts.get(); } - -scripting::Pool * CClient::getContextPool() const -{ - return clientScripts.get(); -} #endif void CClient::reinitScripting() diff --git a/client/Client.h b/client/Client.h index 6155b9486..d87d8c0c2 100644 --- a/client/Client.h +++ b/client/Client.h @@ -220,7 +220,6 @@ public: #if SCRIPTING_ENABLED scripting::Pool * getGlobalContextPool() const override; - scripting::Pool * getContextPool() const override; #endif private: diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index 4412c9d28..89a2e2861 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -22,6 +22,13 @@ CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerC } +#if SCRIPTING_ENABLED +scripting::Pool * CPlayerBattleCallback::getContextPool() const +{ + return nullptr; //TODO cl->getGlobalContextPool(); +} +#endif + const IBattleInfo * CPlayerBattleCallback::getBattle() const { return battle; diff --git a/lib/battle/CPlayerBattleCallback.h b/lib/battle/CPlayerBattleCallback.h index 60fbbd3d5..27bbb16d3 100644 --- a/lib/battle/CPlayerBattleCallback.h +++ b/lib/battle/CPlayerBattleCallback.h @@ -22,6 +22,10 @@ class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback public: CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player); +#if SCRIPTING_ENABLED + scripting::Pool * getContextPool() const override; +#endif + const IBattleInfo * getBattle() const override; std::optional getPlayerID() const override; diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 4b3508610..4c41f981f 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -75,7 +75,7 @@ LuaContext::LuaContext(const Script * source, const Environment * env_): S.push(env->game()); lua_setglobal(L, "GAME"); - S.push(env->battle()); + S.push(env->battle(BattleID::NONE)); lua_setglobal(L, "BATTLE"); S.push(env->eventBus()); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3ae02b83b..e096a79b1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4094,10 +4094,10 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const return serverScripts.get(); } -scripting::Pool * CGameHandler::getContextPool() const -{ - return serverScripts.get(); -} +//scripting::Pool * CGameHandler::getContextPool() const +//{ +// return serverScripts.get(); +//} #endif void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_t subtype) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 75a9cfd3a..47f03e497 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -270,7 +270,7 @@ public: #if SCRIPTING_ENABLED scripting::Pool * getGlobalContextPool() const override; - scripting::Pool * getContextPool() const override; +// scripting::Pool * getContextPool() const override; #endif friend class CVCMIServer; From 9fa7a93fb064dacf2caef2c428bc917663594952 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 18:45:52 +0300 Subject: [PATCH 0343/1248] Properly pass battleID in all battle netpack's --- AI/BattleAI/StackWithBonuses.cpp | 5 +++ CCallback.cpp | 3 ++ client/Client.cpp | 4 +- client/battle/BattleInterface.cpp | 3 +- client/battle/BattleInterface.h | 2 +- lib/NetPacks.h | 20 +++++++++ lib/constants/EntityIdentifiers.h | 4 +- lib/spells/BattleSpellMechanics.cpp | 8 +++- lib/spells/BattleSpellMechanics.h | 2 +- lib/spells/effects/Catapult.cpp | 1 + lib/spells/effects/Clone.cpp | 5 +++ lib/spells/effects/Damage.cpp | 4 ++ lib/spells/effects/DemonSummon.cpp | 2 + lib/spells/effects/Dispel.cpp | 3 ++ lib/spells/effects/Heal.cpp | 4 ++ lib/spells/effects/Sacrifice.cpp | 1 + lib/spells/effects/Summon.cpp | 2 + lib/spells/effects/Timed.cpp | 3 ++ server/battles/BattleActionProcessor.cpp | 25 +++++++++++- server/battles/BattleActionProcessor.h | 2 +- server/battles/BattleFlowProcessor.cpp | 11 +++++ server/battles/BattleProcessor.cpp | 10 +---- server/battles/BattleResultProcessor.cpp | 52 ++++++++++++------------ server/battles/BattleResultProcessor.h | 6 +-- server/queries/BattleQueries.cpp | 6 +-- server/queries/BattleQueries.h | 2 +- 26 files changed, 136 insertions(+), 54 deletions(-) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 0a15c68d3..dd4ce6580 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -320,6 +320,11 @@ battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const return ret; } +BattleID HypotheticBattle::getBattleID() const +{ + return subject->getBattle()->getBattleID(); +} + int32_t HypotheticBattle::getActiveStackID() const { return activeUnitId; diff --git a/CCallback.cpp b/CCallback.cpp index 3b8e099ba..5d5e00073 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -207,6 +207,7 @@ void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const Bat { assert(action.actionType == EActionType::HERO_SPELL); MakeAction mca(action); + mca.battleID = battleID; sendRequest(&mca); } @@ -376,6 +377,7 @@ void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const Batt assert(!cl->gs->getBattle(battleID)->tacticDistance); MakeAction ma; ma.ba = action; + ma.battleID = battleID; sendRequest(&ma); } @@ -384,6 +386,7 @@ void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const Ba assert(cl->gs->getBattle(battleID)->tacticDistance); MakeAction ma; ma.ba = action; + ma.battleID = battleID; sendRequest(&ma); } diff --git a/client/Client.cpp b/client/Client.cpp index 632cc5ef3..b12b3e494 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -615,7 +615,7 @@ void CClient::battleStarted(const BattleInfo * info) if(att || def) { boost::unique_lock un(*CPlayerInterface::pim); - CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); } else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) { @@ -623,7 +623,7 @@ void CClient::battleStarted(const BattleInfo * info) auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); spectratorInt->cb->onBattleStarted(info); boost::unique_lock un(*CPlayerInterface::pim); - CPlayerInterface::battleInt = std::make_shared(leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); } } diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index b857e565e..fe2f2150b 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -45,7 +45,7 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/CThreadHelper.h" -BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, +BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, @@ -55,6 +55,7 @@ BattleInterface::BattleInterface(const CCreatureSet *army1, const CCreatureSet * , attackerInt(att) , defenderInt(defen) , curInt(att) + , battleID(battleID) , battleOpeningDelayActive(true) { if(spectatorInt) diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index 8ca2e8015..a4a1a54e7 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -156,7 +156,7 @@ public: BattleID getBattleID() const; std::shared_ptr getBattle() const; - BattleInterface(const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); + BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); ~BattleInterface(); void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player diff --git a/lib/NetPacks.h b/lib/NetPacks.h index e84320b19..2b1e83a54 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1492,6 +1492,7 @@ struct DLL_LINKAGE BattleStart : public CPackForClient { h & battleID; h & info; + assert(battleID != BattleID::NONE); } }; @@ -1506,6 +1507,7 @@ struct DLL_LINKAGE BattleNextRound : public CPackForClient template void serialize(Handler & h, const int version) { h & battleID; + assert(battleID != BattleID::NONE); } }; @@ -1524,6 +1526,7 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient h & battleID; h & stack; h & askPlayerInterface; + assert(battleID != BattleID::NONE); } }; @@ -1557,6 +1560,7 @@ struct DLL_LINKAGE BattleResultAccepted : public CPackForClient h & battleID; h & heroResult; h & winnerSide; + assert(battleID != BattleID::NONE); } }; @@ -1583,6 +1587,7 @@ struct DLL_LINKAGE BattleResult : public Query h & casualties[1]; h & exp; h & artifacts; + assert(battleID != BattleID::NONE); } }; @@ -1600,6 +1605,7 @@ struct DLL_LINKAGE BattleLogMessage : public CPackForClient { h & battleID; h & lines; + assert(battleID != BattleID::NONE); } }; @@ -1623,6 +1629,7 @@ struct DLL_LINKAGE BattleStackMoved : public CPackForClient h & tilesToMove; h & distance; h & teleporting; + assert(battleID != BattleID::NONE); } }; @@ -1640,6 +1647,7 @@ struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient { h & battleID; h & changedStacks; + assert(battleID != BattleID::NONE); } }; @@ -1693,6 +1701,7 @@ struct BattleStackAttacked h & killedAmount; h & damageAmount; h & spellID; + assert(battleID != BattleID::NONE); } bool operator<(const BattleStackAttacked & b) const { @@ -1758,6 +1767,7 @@ struct DLL_LINKAGE BattleAttack : public CPackForClient h & tile; h & spellID; h & attackerChanges; + assert(battleID != BattleID::NONE); } }; @@ -1780,6 +1790,7 @@ struct DLL_LINKAGE StartAction : public CPackForClient { h & battleID; h & ba; + assert(battleID != BattleID::NONE); } }; @@ -1826,6 +1837,7 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient h & casterStack; h & castByHero; h & activeCast; + assert(battleID != BattleID::NONE); } }; @@ -1847,6 +1859,7 @@ struct DLL_LINKAGE SetStackEffect : public CPackForClient h & toAdd; h & toUpdate; h & toRemove; + assert(battleID != BattleID::NONE); } }; @@ -1864,6 +1877,7 @@ struct DLL_LINKAGE StacksInjured : public CPackForClient { h & battleID; h & stacks; + assert(battleID != BattleID::NONE); } }; @@ -1878,6 +1892,7 @@ struct DLL_LINKAGE BattleResultsApplied : public CPackForClient h & battleID; h & player1; h & player2; + assert(battleID != BattleID::NONE); } }; @@ -1895,6 +1910,7 @@ struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient { h & battleID; h & changes; + assert(battleID != BattleID::NONE); } }; @@ -1931,6 +1947,7 @@ struct DLL_LINKAGE CatapultAttack : public CPackForClient h & battleID; h & attackedParts; h & attacker; + assert(battleID != BattleID::NONE); } }; @@ -1953,6 +1970,7 @@ struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient h & which; h & val; h & absolute; + assert(battleID != BattleID::NONE); } protected: @@ -1977,6 +1995,7 @@ struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient h & effect; h & val; h & additionalInfo; + assert(battleID != BattleID::NONE); } protected: @@ -1993,6 +2012,7 @@ struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient { h & battleID; h & state; + assert(battleID != BattleID::NONE); } protected: diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 76b363dc2..80fbd48d9 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -180,10 +180,10 @@ public: DLL_LINKAGE static const QueryID NONE; }; -class BattleID : public Identifier +class BattleID : public Identifier { public: - using Identifier::Identifier; + using Identifier::Identifier; DLL_LINKAGE static const BattleID NONE; }; class ObjectInstanceID : public Identifier diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 82a16a276..cff47bbc1 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -252,6 +252,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) sc.side = casterSide; sc.spellID = getSpellId(); + sc.battleID = battle()->getBattle()->getBattleID(); sc.tile = target.at(0).hexValue; sc.castByHero = mode == Mode::HERO; @@ -299,6 +300,7 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) beforeCast(sc, *server->getRNG(), target); BattleLogMessage castDescription; + castDescription.battleID = battle()->getBattle()->getBattleID(); switch (mode) { @@ -344,8 +346,9 @@ void BattleSpellMechanics::cast(ServerCallback * server, const Target & target) // send empty event to client // temporary(?) workaround to force animations to trigger - StacksInjured fake_event; - server->apply(&fake_event); + StacksInjured fakeEvent; + fakeEvent.battleID = battle()->getBattle()->getBattleID(); + server->apply(&fakeEvent); } void BattleSpellMechanics::beforeCast(BattleSpellCast & sc, vstd::RNG & rng, const Target & target) @@ -448,6 +451,7 @@ std::set BattleSpellMechanics::collectTargets() const void BattleSpellMechanics::doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector) { SetStackEffect sse; + sse.battleID = battle()->getBattle()->getBattleID(); for(const auto * unit : targets) { diff --git a/lib/spells/BattleSpellMechanics.h b/lib/spells/BattleSpellMechanics.h index b89f1f444..29ad70a94 100644 --- a/lib/spells/BattleSpellMechanics.h +++ b/lib/spells/BattleSpellMechanics.h @@ -73,7 +73,7 @@ private: std::set collectTargets() const; - static void doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector); + void doRemoveEffects(ServerCallback * server, const std::vector & targets, const CSelector & selector); std::set spellRangeInHexes(BattleHex centralHex) const; diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 10fa74a8e..321a834ae 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -188,6 +188,7 @@ int Catapult::getRandomDamage (ServerCallback * server) const void Catapult::removeTowerShooters(ServerCallback * server, const Mechanics * m) const { BattleUnitsChanged removeUnits; + removeUnits.battleID = m->battle()->getBattle()->getBattleID(); for (auto const wallPart : { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER }) { diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index 4d7a4d021..eb600091f 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -14,6 +14,7 @@ #include "../ISpellMechanics.h" #include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" #include "../../serializer/JsonSerializeFormat.h" @@ -60,6 +61,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg info.summoned = true; BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); server->apply(&pack); @@ -67,6 +69,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg //TODO: use BattleUnitsChanged with UPDATE operation BattleUnitsChanged cloneFlags; + cloneFlags.battleID = m->battle()->getBattle()->getBattleID(); const auto *cloneUnit = m->battle()->battleGetUnitByID(unitId); @@ -89,6 +92,8 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg server->apply(&cloneFlags); SetStackEffect sse; + sse.battleID = m->battle()->getBattle()->getBattleID(); + Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, SpellID::CLONE); //TODO: use special bonus type lifeTimeMarker.turnsRemain = m->getEffectDuration(); std::vector buffer; diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index ca36a3db9..9e29a56b1 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -34,6 +34,9 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar { StacksInjured stacksInjured; BattleLogMessage blm; + stacksInjured.battleID = m->battle()->getBattle()->getBattleID(); + blm.battleID = m->battle()->getBattle()->getBattleID(); + size_t targetIndex = 0; const battle::Unit * firstTarget = nullptr; const bool describe = server->describeChanges(); @@ -48,6 +51,7 @@ void Damage::apply(ServerCallback * server, const Mechanics * m, const EffectTar if(unit && unit->alive()) { BattleStackAttacked bsa; + bsa.battleID = m->battle()->getBattle()->getBattleID(); bsa.damageAmount = damageForTarget(targetIndex, m, unit); bsa.stackAttacked = unit->unitId(); bsa.attackerID = -1; diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index cd4661b88..b2be990fa 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -14,6 +14,7 @@ #include "../ISpellMechanics.h" #include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/BattleInfo.h" #include "../../battle/CUnitState.h" #include "../../serializer/JsonSerializeFormat.h" @@ -27,6 +28,7 @@ namespace effects void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const Destination & dest : target) { diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 62fb5bfd2..3d42872f7 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -18,6 +18,7 @@ #include "../../NetPacks.h" #include "../../battle/IBattleState.h" +#include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" @@ -33,6 +34,8 @@ void Dispel::apply(ServerCallback * server, const Mechanics * m, const EffectTar const bool describe = server->describeChanges(); SetStackEffect sse; BattleLogMessage blm; + blm.battleID = m->battle()->getBattle()->getBattleID(); + sse.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & t : target) { diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index e026d12b6..a4a2bc051 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -35,7 +35,11 @@ void Heal::apply(ServerCallback * server, const Mechanics * m, const EffectTarge void Heal::apply(int64_t value, ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleLogMessage logMessage; + logMessage.battleID = m->battle()->getBattle()->getBattleID(); + BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); + prepareHealEffect(value, pack, logMessage, *server->getRNG(), m, target); if(!pack.changedStacks.empty()) server->apply(&pack); diff --git a/lib/spells/effects/Sacrifice.cpp b/lib/spells/effects/Sacrifice.cpp index 7beb1a201..537697c5e 100644 --- a/lib/spells/effects/Sacrifice.cpp +++ b/lib/spells/effects/Sacrifice.cpp @@ -123,6 +123,7 @@ void Sacrifice::apply(ServerCallback * server, const Mechanics * m, const Effect Heal::apply(calculateHealEffectValue(m, victim), server, m, healTarget); BattleUnitsChanged removeUnits; + removeUnits.battleID = m->battle()->getBattle()->getBattleID(); removeUnits.changedStacks.emplace_back(victim->unitId(), UnitChanges::EOperation::REMOVE); server->apply(&removeUnits); } diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index c0eb0bbe1..a1b623293 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -14,6 +14,7 @@ #include "../ISpellMechanics.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../battle/BattleInfo.h" #include "../../battle/Unit.h" #include "../../NetPacks.h" #include "../../serializer/JsonSerializeFormat.h" @@ -87,6 +88,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar auto valueWithBonus = m->applySpecificSpellBonus(m->calculateRawEffectValue(0, m->getEffectPower()));//TODO: consider use base power too BattleUnitsChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & dest : target) { diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 8fac34e2d..947e264fd 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -15,6 +15,7 @@ #include "../../NetPacks.h" #include "../../battle/IBattleState.h" +#include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" @@ -116,6 +117,8 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg SetStackEffect sse; BattleLogMessage blm; + blm.battleID = m->battle()->getBattle()->getBattleID(); + sse.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & t : target) { diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index fdda43ac6..205ba8ae8 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -160,6 +160,8 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c //defensive stance, TODO: filter out spell boosts from bonus (stone skin etc.) SetStackEffect sse; + sse.battleID = battle.getBattle()->getBattleID(); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); @@ -188,6 +190,7 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c gameHandler->sendAndApply(&sse); BattleLogMessage message; + message.battleID = battle.getBattle()->getBattleID(); MetaString text; stack->addText(text, EMetaText::GENERAL_TXT, 120); @@ -541,6 +544,7 @@ bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & bat if (!ba.isBattleEndAction()) { StartAction startAction(ba); + startAction.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&startAction); } @@ -549,6 +553,7 @@ bool BattleActionProcessor::makeBattleActionImpl(const CBattleInfoCallback & bat if (!ba.isBattleEndAction()) { EndAction endAction; + endAction.battleID = battle.getBattle()->getBattleID(); gameHandler->sendAndApply(&endAction); } @@ -658,12 +663,14 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta occupyGateDrawbridgeHex(dest)) { BattleUpdateGateState db; + db.battleID = battle.getBattle()->getBattleID(); db.state = EGateState::OPENED; gameHandler->sendAndApply(&db); } //inform clients about move BattleStackMoved sm; + sm.battleID = battle.getBattle()->getBattleID(); sm.stack = curStack->unitId(); std::vector tiles; tiles.push_back(path.first[0]); @@ -793,6 +800,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta { //commit movement BattleStackMoved sm; + sm.battleID = battle.getBattle()->getBattleID(); sm.stack = curStack->unitId(); sm.distance = path.second; sm.teleporting = false; @@ -820,6 +828,7 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta if (curStack->alive()) { BattleUpdateGateState db; + db.battleID = battle.getBattle()->getBattleID(); db.state = EGateState::OPENED; gameHandler->sendAndApply(&db); } @@ -857,6 +866,9 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const FireShieldInfo fireShield; BattleAttack bat; BattleLogMessage blm; + blm.battleID = battle.getBattle()->getBattleID(); + bat.battleID = battle.getBattle()->getBattleID(); + bat.attackerChanges.battleID = battle.getBattle()->getBattleID(); bat.stackAttacking = attacker->unitId(); bat.tile = targetHex; @@ -966,6 +978,9 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if (drainedLife > 0) bat.flags |= BattleAttack::LIFE_DRAIN; + for (BattleStackAttacked & bsa : bat.bsa) + bsa.battleID = battle.getBattle()->getBattleID(); + gameHandler->sendAndApply(&bat); { @@ -1032,6 +1047,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { BattleStackAttacked bsa; + bsa.battleID = battle.getBattle()->getBattleID(); bsa.flags |= BattleStackAttacked::FIRE_SHIELD; bsa.stackAttacked = attacker->unitId(); //invert bsa.attackerID = defender->unitId(); @@ -1039,6 +1055,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const attacker->prepareAttacked(bsa, gameHandler->getRandomGenerator()); StacksInjured pack; + pack.battleID = battle.getBattle()->getBattleID(); pack.stacks.push_back(bsa); gameHandler->sendAndApply(&pack); @@ -1240,10 +1257,12 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & return; //wrong subtype BattleUnitsChanged addUnits; + addUnits.battleID = battle.getBattle()->getBattleID(); addUnits.changedStacks.emplace_back(resurrectInfo.id, UnitChanges::EOperation::ADD); resurrectInfo.save(addUnits.changedStacks.back().data); BattleUnitsChanged removeUnits; + removeUnits.battleID = battle.getBattle()->getBattleID(); removeUnits.changedStacks.emplace_back(defender->unitId(), UnitChanges::EOperation::REMOVE); gameHandler->sendAndApply(&removeUnits); gameHandler->sendAndApply(&addUnits); @@ -1280,10 +1299,11 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & defender->prepareAttacked(bsa, gameHandler->getRandomGenerator()); StacksInjured si; + si.battleID = battle.getBattle()->getBattleID(); si.stacks.push_back(bsa); gameHandler->sendAndApply(&si); - sendGenericKilledLog(defender, bsa.killedAmount, false); + sendGenericKilledLog(battle, defender, bsa.killedAmount, false); } } @@ -1354,11 +1374,12 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba return drainedLife; } -void BattleActionProcessor::sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple) +void BattleActionProcessor::sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple) { if(killed > 0) { BattleLogMessage blm; + blm.battleID = battle.getBattle()->getBattleID(); addGenericKilledLog(blm, defender, killed, multiple); gameHandler->sendAndApply(&blm); } diff --git a/server/battles/BattleActionProcessor.h b/server/battles/BattleActionProcessor.h index 11d83ebc9..34e40aa29 100644 --- a/server/battles/BattleActionProcessor.h +++ b/server/battles/BattleActionProcessor.h @@ -49,7 +49,7 @@ class BattleActionProcessor : boost::noncopyable // damage, drain life & fire shield; returns amount of drained life int64_t applyBattleEffects(const CBattleInfoCallback & battle, BattleAttack & bat, std::shared_ptr attackerState, FireShieldInfo & fireShield, const CStack * def, int distance, bool secondary); - void sendGenericKilledLog(const CStack * defender, int32_t killed, bool multiple); + void sendGenericKilledLog(const CBattleInfoCallback & battle, const CStack * defender, int32_t killed, bool multiple); void addGenericKilledLog(BattleLogMessage & blm, const CStack * defender, int32_t killed, bool multiple); bool canStackAct(const CBattleInfoCallback & battle, const CStack * stack); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 075d12723..92f9a860a 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -171,6 +171,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, info.summoned = true; BattleUnitsChanged pack; + pack.battleID = battle.getBattle()->getBattleID(); pack.changedStacks.emplace_back(info.id, UnitChanges::EOperation::ADD); info.save(pack.changedStacks.back().data); gameHandler->sendAndApply(&pack); @@ -227,6 +228,7 @@ void BattleFlowProcessor::onTacticsEnded(const CBattleInfoCallback & battle) void BattleFlowProcessor::startNextRound(const CBattleInfoCallback & battle, bool isFirstRound) { BattleNextRound bnr; + bnr.battleID = battle.getBattle()->getBattleID(); logGlobal->debug("Next round starts"); gameHandler->sendAndApply(&bnr); @@ -265,6 +267,7 @@ const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & bat if(stack && stack->alive() && !stack->waiting) { BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); bte.stackID = stack->unitId(); bte.effect = vstd::to_underlying(BonusType::HP_REGENERATION); @@ -303,6 +306,7 @@ void BattleFlowProcessor::activateNextStack(const CBattleInfoCallback & battle) } BattleUnitsChanged removeGhosts; + removeGhosts.battleID = battle.getBattle()->getBattleID(); auto pendingGhosts = battle.battleGetStacksIf([](const CStack * stack){ return stack->ghostPending; @@ -487,6 +491,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, con if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) { BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); bte.stackID = next->unitId(); bte.effect = vstd::to_underlying(BonusType::MORALE); bte.val = 1; @@ -558,6 +563,7 @@ void BattleFlowProcessor::makeStackDoNothing(const CBattleInfoCallback & battle, bool BattleFlowProcessor::makeAutomaticAction(const CBattleInfoCallback & battle, const CStack *stack, BattleAction &ba) { BattleSetActiveStack bsa; + bsa.battleID = battle.getBattle()->getBattleID(); bsa.stack = stack->unitId(); bsa.askPlayerInterface = false; gameHandler->sendAndApply(&bsa); @@ -601,6 +607,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & batt void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, const CObstacleInstance & obstacle) { BattleObstaclesChanged obsRem; + obsRem.battleID = battle.getBattle()->getBattleID(); obsRem.changes.emplace_back(obstacle.uniqueID, ObstacleChanges::EOperation::REMOVE); gameHandler->sendAndApply(&obsRem); } @@ -608,6 +615,7 @@ void BattleFlowProcessor::removeObstacle(const CBattleInfoCallback & battle, con void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, const CStack *st) { BattleTriggerEffect bte; + bte.battleID = battle.getBattle()->getBattleID(); bte.stackID = st->unitId(); bte.effect = -1; bte.val = 0; @@ -640,6 +648,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (unbind) { BattleSetStackProperty ssp; + ssp.battleID = battle.getBattle()->getBattleID(); ssp.which = BattleSetStackProperty::UNBIND; ssp.stackID = st->unitId(); gameHandler->sendAndApply(&ssp); @@ -721,6 +730,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c int cooldown = bonus->additionalInfo[0]; BattleSetStackProperty ssp; + ssp.battleID = battle.getBattle()->getBattleID(); ssp.which = BattleSetStackProperty::ENCHANTER_COUNTER; ssp.absolute = false; ssp.val = cooldown; @@ -737,6 +747,7 @@ void BattleFlowProcessor::setActiveStack(const CBattleInfoCallback & battle, con assert(stack); BattleSetActiveStack sas; + sas.battleID = battle.getBattle()->getBattleID(); sas.stack = stack->unitId(); gameHandler->sendAndApply(&sas); } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index c5e025fde..d4b36bf67 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -89,7 +89,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm } } - lastBattleQuery->bi = battle; + lastBattleQuery->battleID = battle->getBattleID(); lastBattleQuery->result = std::nullopt; lastBattleQuery->belligerents[0] = battle->sides[0].armyObject; lastBattleQuery->belligerents[1] = battle->sides[1].armyObject; @@ -261,13 +261,7 @@ void BattleProcessor::endBattleConfirm(const BattleID & battleID) void BattleProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult &result) { - auto battle = gameHandler->gameState()->getBattle(battleID); - assert(battle); - - if (!battle) - return; - - resultProcessor->battleAfterLevelUp(*battle, result); + resultProcessor->battleAfterLevelUp(battleID, result); } void BattleProcessor::setGameHandler(CGameHandler * newGameHandler) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 01eebf3b9..028f6d9bc 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -180,26 +180,21 @@ void CasualtiesAfterBattle::updateArmy(CGameHandler *gh) } } -FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr Query, int remainingBattleQueriesCount) +FinishingBattleHelper::FinishingBattleHelper(const CBattleInfoCallback & info, const BattleResult & result, int remainingBattleQueriesCount) { - assert(Query->result); - assert(Query->bi); - auto &result = *Query->result; - auto &info = *Query->bi; - if (result.winner == BattleSide::ATTACKER) { - winnerHero = info.getSideHero(BattleSide::ATTACKER); - loserHero = info.getSideHero(BattleSide::DEFENDER); - victor = info.getSidePlayer(BattleSide::ATTACKER); - loser = info.getSidePlayer(BattleSide::DEFENDER); + winnerHero = info.getBattle()->getSideHero(BattleSide::ATTACKER); + loserHero = info.getBattle()->getSideHero(BattleSide::DEFENDER); + victor = info.getBattle()->getSidePlayer(BattleSide::ATTACKER); + loser = info.getBattle()->getSidePlayer(BattleSide::DEFENDER); } else { - winnerHero = info.getSideHero(BattleSide::DEFENDER); - loserHero = info.getSideHero(BattleSide::ATTACKER); - victor = info.getSidePlayer(BattleSide::DEFENDER); - loser = info.getSidePlayer(BattleSide::ATTACKER); + winnerHero = info.getBattle()->getSideHero(BattleSide::DEFENDER); + loserHero = info.getBattle()->getSideHero(BattleSide::ATTACKER); + victor = info.getBattle()->getSidePlayer(BattleSide::DEFENDER); + loser = info.getBattle()->getSidePlayer(BattleSide::ATTACKER); } winnerSide = result.winner; @@ -207,12 +202,12 @@ FinishingBattleHelper::FinishingBattleHelper(std::shared_ptr this->remainingBattleQueriesCount = remainingBattleQueriesCount; } -FinishingBattleHelper::FinishingBattleHelper() -{ - winnerHero = loserHero = nullptr; - winnerSide = 0; - remainingBattleQueriesCount = 0; -} +//FinishingBattleHelper::FinishingBattleHelper() +//{ +// winnerHero = loserHero = nullptr; +// winnerSide = 0; +// remainingBattleQueriesCount = 0; +//} void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) { @@ -267,7 +262,7 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) const int queriedPlayers = battleQuery ? (int)boost::count(gameHandler->queries->allQueries(), battleQuery) : 0; assert(finishingBattles.count(battle.getBattle()->getBattleID()) == 0); - finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique(battleQuery, queriedPlayers); + finishingBattles[battle.getBattle()->getBattleID()] = std::make_unique(battle, *battleResult, queriedPlayers); // in battles against neutrals, 1st player can ask to replay battle manually if (!battle.sideToPlayer(1).isValidPlayer()) @@ -490,6 +485,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) gameHandler->changePrimSkill(finishingBattle->winnerHero, PrimarySkill::EXPERIENCE, battleResult->exp[finishingBattle->winnerSide]); BattleResultAccepted raccepted; + raccepted.battleID = battle.getBattle()->getBattleID(); raccepted.heroResult[0].army = const_cast(battle.battleGetArmyObject(0)); raccepted.heroResult[1].army = const_cast(battle.battleGetArmyObject(1)); raccepted.heroResult[0].hero = const_cast(battle.battleGetFightingHero(0)); @@ -503,15 +499,15 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) //--> continuation (battleAfterLevelUp) occurs after level-up gameHandler->queries are handled or on removing query } -void BattleResultProcessor::battleAfterLevelUp(const CBattleInfoCallback & battle, const BattleResult & result) +void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const BattleResult & result) { LOG_TRACE(logGlobal); - assert(finishingBattles.count(battle.getBattle()->getBattleID()) != 0); - if(finishingBattles.count(battle.getBattle()->getBattleID()) == 0) + assert(finishingBattles.count(battleID) != 0); + if(finishingBattles.count(battleID) == 0) return; - auto & finishingBattle = finishingBattles[battle.getBattle()->getBattleID()]; + auto & finishingBattle = finishingBattles[battleID]; finishingBattle->remainingBattleQueriesCount--; logGlobal->trace("Decremented gameHandler->queries count to %d", finishingBattle->remainingBattleQueriesCount); @@ -537,6 +533,7 @@ void BattleResultProcessor::battleAfterLevelUp(const CBattleInfoCallback & battl } BattleResultsApplied resultsApplied; + resultsApplied.battleID = battleID; resultsApplied.player1 = finishingBattle->victor; resultsApplied.player2 = finishingBattle->loser; gameHandler->sendAndApply(&resultsApplied); @@ -561,8 +558,8 @@ void BattleResultProcessor::battleAfterLevelUp(const CBattleInfoCallback & battl gameHandler->heroPool->onHeroEscaped(finishingBattle->victor, finishingBattle->winnerHero); } - finishingBattles.erase(battle.getBattle()->getBattleID()); - battleResults.erase(battle.getBattle()->getBattleID()); + finishingBattles.erase(battleID); + battleResults.erase(battleID); } void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide) @@ -572,6 +569,7 @@ void BattleResultProcessor::setBattleResult(const CBattleInfoCallback & battle, battleResults[battle.getBattle()->getBattleID()] = std::make_unique(); auto & battleResult = battleResults[battle.getBattle()->getBattleID()]; + battleResult->battleID = battle.getBattle()->getBattleID(); battleResult->result = resultType; battleResult->winner = victoriusSide; //surrendering side loses diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index c69cd527e..88ac0e795 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -37,8 +37,8 @@ struct CasualtiesAfterBattle struct FinishingBattleHelper { - FinishingBattleHelper(); - FinishingBattleHelper(std::shared_ptr Query, int RemainingBattleQueriesCount); +// FinishingBattleHelper(); + FinishingBattleHelper(const CBattleInfoCallback & battle, const BattleResult & result, int RemainingBattleQueriesCount); inline bool isDraw() const {return winnerSide == 2;} @@ -76,5 +76,5 @@ public: void setBattleResult(const CBattleInfoCallback & battle, EBattleResult resultType, int victoriusSide); void endBattle(const CBattleInfoCallback & battle); //ends battle void endBattleConfirm(const CBattleInfoCallback & battle); - void battleAfterLevelUp(const CBattleInfoCallback & battle, const BattleResult & result); + void battleAfterLevelUp(const BattleID & battleID, const BattleResult & result); }; diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 9ff5b7800..10af7c517 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -24,7 +24,7 @@ void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisi CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): CGhQuery(owner), - bi(bi) + battleID(bi->getBattleID()) { belligerents[0] = bi->getSideArmy(0); belligerents[1] = bi->getSideArmy(1); @@ -34,7 +34,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): } CBattleQuery::CBattleQuery(CGameHandler * owner): - CGhQuery(owner), bi(nullptr) + CGhQuery(owner) { belligerents[0] = belligerents[1] = nullptr; } @@ -48,7 +48,7 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { if(result) - gh->battles->battleAfterLevelUp(bi->getBattleID(), *result); + gh->battles->battleAfterLevelUp(battleID, *result); } CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi): diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index e661f7c90..f23620307 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -23,7 +23,7 @@ public: std::array belligerents; std::array initialHeroMana; - const IBattleInfo *bi; + BattleID battleID; std::optional result; CBattleQuery(CGameHandler * owner); From ef94e7a78a9982032f187a9fb29c183188ab637d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 19:09:28 +0300 Subject: [PATCH 0344/1248] Fix build --- AI/BattleAI/BattleAI.cpp | 3 +-- CCallback.h | 6 +++--- lib/battle/IBattleInfoCallback.h | 1 + 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index b94a56513..73a133467 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -52,8 +52,7 @@ void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::share setCbc(CB); env = ENV; cb = CB; - assert(0);// FIXME: - // playerID = *CB->getPlayerID(); //TODO should be sth in callback + playerID = *CB->getPlayerID(); wasWaitingForRealize = CB->waitTillRealize; wasUnlockingGs = CB->unlockGsWhenWaiting; CB->waitTillRealize = false; diff --git a/CCallback.h b/CCallback.h index 8acb0d6c5..3657e3581 100644 --- a/CCallback.h +++ b/CCallback.h @@ -145,9 +145,9 @@ public: virtual ~CCallback(); //client-specific functionalities (pathfinding) - bool canMoveBetween(const int3 &a, const int3 &b); - int3 getGuardingCreaturePosition(int3 tile); - std::shared_ptr getPathsInfo(const CGHeroInstance * h); + virtual bool canMoveBetween(const int3 &a, const int3 &b); + virtual int3 getGuardingCreaturePosition(int3 tile); + virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); std::optional getPlayerID() const override; diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index d216bf454..5996d5bda 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -56,6 +56,7 @@ public: #if SCRIPTING_ENABLED virtual scripting::Pool * getContextPool() const = 0; #endif + virtual ~IBattleInfoCallback() = default; virtual const IBattleInfo * getBattle() const = 0; virtual std::optional getPlayerID() const = 0; From 8bdddd13243433fe9b6fa20ed50afc98f9bbbbb0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 31 Aug 2023 23:36:39 +0300 Subject: [PATCH 0345/1248] Fix tests compilation --- server/battles/BattleResultProcessor.cpp | 2 +- test/battle/CBattleInfoCallbackTest.cpp | 12 +++++++++--- test/mock/BattleFake.cpp | 1 - test/mock/BattleFake.h | 11 +++++++++++ test/mock/mock_Environment.h | 2 +- test/mock/mock_IBattleInfoCallback.h | 2 ++ test/mock/mock_IGameInfoCallback.h | 1 + test/mock/mock_battle_IBattleState.h | 7 ++++++- test/scripting/ScriptFixture.cpp | 1 - 9 files changed, 31 insertions(+), 8 deletions(-) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 028f6d9bc..87e358f7a 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -282,7 +282,7 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) otherBattleQuery->result = battleQuery->result; } - gameHandler->turnTimerHandler.onBattleEnd(battle.battleID); + gameHandler->turnTimerHandler.onBattleEnd(battle.getBattle()->getBattleID()); if (battleResult->queryID == QueryID::NONE) endBattleConfirm(battle); diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index d05ab74f2..e99d2d3a9 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -124,6 +124,7 @@ public: { public: + const IBattleInfo * battle; #if SCRIPTING_ENABLED scripting::Pool * pool; @@ -137,9 +138,14 @@ public: { } - void setBattle(const IBattleInfo * battleInfo) + const IBattleInfo * getBattle() const override { - CBattleInfoCallback::setBattle(battleInfo); + return battle; + } + + std::optional getPlayerID() const override + { + return std::nullopt; } #if SCRIPTING_ENABLED @@ -170,7 +176,7 @@ public: void startBattle() { - subject.setBattle(&battleMock); + subject.battle = &battleMock; } void redirectUnitsToFake() diff --git a/test/mock/BattleFake.cpp b/test/mock/BattleFake.cpp index a2f5a9337..b00631d61 100644 --- a/test/mock/BattleFake.cpp +++ b/test/mock/BattleFake.cpp @@ -87,7 +87,6 @@ BattleFake::BattleFake() = default; void BattleFake::setUp() { - CBattleInfoCallback::setBattle(this); } void BattleFake::setupEmptyBattlefield() diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index 9c9287dac..cb265e008 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -88,6 +88,17 @@ public: pack->applyBattle(this); } + const IBattleInfo * getBattle() const override + { + return nullptr; + } + + std::optional getPlayerID() const override + { + return std::nullopt; + } + + protected: private: diff --git a/test/mock/mock_Environment.h b/test/mock/mock_Environment.h index ae03ef348..5b5829984 100644 --- a/test/mock/mock_Environment.h +++ b/test/mock/mock_Environment.h @@ -16,7 +16,7 @@ class EnvironmentMock : public Environment { public: MOCK_CONST_METHOD0(services, const Services *()); - MOCK_CONST_METHOD0(battle, const BattleCb *()); + MOCK_CONST_METHOD1(battle, const BattleCb *(const BattleID & battleID)); MOCK_CONST_METHOD0(game, const GameCb *()); MOCK_CONST_METHOD0(logger, ::vstd::CLoggerBase *()); MOCK_CONST_METHOD0(eventBus, ::events::EventBus *()); diff --git a/test/mock/mock_IBattleInfoCallback.h b/test/mock/mock_IBattleInfoCallback.h index 402439524..2bc00a19b 100644 --- a/test/mock/mock_IBattleInfoCallback.h +++ b/test/mock/mock_IBattleInfoCallback.h @@ -36,6 +36,8 @@ public: MOCK_CONST_METHOD0(battleActiveUnit, const battle::Unit *()); MOCK_CONST_METHOD0(getBonusBearer, IBonusBearer*()); + MOCK_CONST_METHOD0(getBattle, IBattleInfo*()); + MOCK_CONST_METHOD0(getPlayerID, std::optional()); MOCK_CONST_METHOD2(battleGetAllObstaclesOnPos, std::vector>(BattleHex, bool)); MOCK_CONST_METHOD2(getAllAffectedObstaclesByStack, std::vector>(const battle::Unit *, const std::set &)); diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index 53c2b1178..879f0c374 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -22,6 +22,7 @@ public: //player MOCK_CONST_METHOD1(getPlayer, const Player *(PlayerColor)); MOCK_CONST_METHOD0(getLocalPlayer, PlayerColor()); + MOCK_CONST_METHOD0(getPlayerID, std::optional()); //hero MOCK_CONST_METHOD1(getHero, const CGHeroInstance *(ObjectInstanceID)); diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index dbd30c0ce..483c13b23 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -11,6 +11,7 @@ #pragma once #include "../../lib/battle/IBattleState.h" +#include "../../lib/int3.h" class BattleStateMock : public IBattleState { @@ -34,8 +35,12 @@ public: MOCK_CONST_METHOD0(getBonusBearer, const IBonusBearer *()); MOCK_CONST_METHOD0(nextUnitId, uint32_t()); MOCK_CONST_METHOD3(getActualDamage, int64_t(const DamageRange &, int32_t, vstd::RNG &)); + MOCK_CONST_METHOD0(getBattleID, BattleID()); + MOCK_CONST_METHOD0(getLocation, int3()); + MOCK_CONST_METHOD0(isCreatureBank, bool()); + MOCK_CONST_METHOD1(getUsedSpells, std::vector(ui8)); - MOCK_METHOD1(nextRound, void(int32_t)); + MOCK_METHOD0(nextRound, void()); MOCK_METHOD1(nextTurn, void(uint32_t)); MOCK_METHOD2(addUnit, void(uint32_t, const JsonNode &)); MOCK_METHOD3(setUnitState, void(uint32_t, const JsonNode &, int64_t)); diff --git a/test/scripting/ScriptFixture.cpp b/test/scripting/ScriptFixture.cpp index 137214fc4..e350c6c61 100644 --- a/test/scripting/ScriptFixture.cpp +++ b/test/scripting/ScriptFixture.cpp @@ -64,7 +64,6 @@ void ScriptFixture::setUp() { pool = std::make_shared(); - EXPECT_CALL(environmentMock, battle()).WillRepeatedly(Return(&binfoMock)); EXPECT_CALL(environmentMock, game()).WillRepeatedly(Return(&infoMock)); EXPECT_CALL(environmentMock, logger()).WillRepeatedly(Return(&loggerMock)); EXPECT_CALL(environmentMock, eventBus()).WillRepeatedly(Return(&eventBus)); From 1f1f978328fbb06b42b0c1ef11c5c0117d4f56e9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 5 Sep 2023 17:22:11 +0300 Subject: [PATCH 0346/1248] Fixed battle replay --- lib/NetPacks.h | 13 ++++ lib/NetPacksLib.cpp | 13 +++- lib/registerTypes/RegisterTypes.h | 1 + lib/spells/effects/Catapult.cpp | 1 + server/TurnTimerHandler.cpp | 6 +- server/battles/BattleActionProcessor.cpp | 2 +- server/battles/BattleProcessor.cpp | 85 ++++++++++++++++-------- server/battles/BattleProcessor.h | 2 + server/battles/BattleResultProcessor.cpp | 3 +- server/queries/BattleQueries.cpp | 17 ++++- server/queries/BattleQueries.h | 1 + 11 files changed, 110 insertions(+), 34 deletions(-) diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 2b1e83a54..3fa84933f 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1530,6 +1530,19 @@ struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient } }; +struct DLL_LINKAGE BattleCancelled: public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + struct DLL_LINKAGE BattleResultAccepted : public CPackForClient { void applyGs(CGameState * gs) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 68179dea8..4b5fd4740 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2119,7 +2119,7 @@ void BattleStart::applyGs(CGameState * gs) const info->battleID = gs->nextBattleID; info->localInit(); - vstd::next(gs->nextBattleID, 1); + gs->nextBattleID = vstd::next(gs->nextBattleID, 1); } void BattleNextRound::applyGs(CGameState * gs) const @@ -2177,6 +2177,17 @@ void BattleUpdateGateState::applyGs(CGameState * gs) const gs->getBattle(battleID)->si.gateState = state; } +void BattleCancelled::applyGs(CGameState * gs) const +{ + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); +} + void BattleResultAccepted::applyGs(CGameState * gs) const { // Remove any "until next battle" bonuses diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 8c6feefd2..3079babf0 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -286,6 +286,7 @@ void registerTypesClientPacks2(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 321a834ae..5212278b1 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -137,6 +137,7 @@ void Catapult::applyTargeted(ServerCallback * server, const Mechanics * m, const attack.damageDealt = getRandomDamage(server); CatapultAttack ca; //package for clients + ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); ca.attackedParts.push_back(attack); server->apply(&ca); diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index ba0bf651b..489ee2fa6 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -254,12 +254,15 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) std::lock_guard guard(mx); const auto * gs = gameHandler.gameState(); const auto * si = gameHandler.getStartInfo(); - if(!si || !gs || !si->turnTimerInfo.isBattleEnabled()) + if(!si || !gs) { assert(0); return; } + if (!si->turnTimerInfo.isBattleEnabled()) + return; + ui8 side = 0; const CStack * stack = nullptr; bool isTactisPhase = gs->getBattle(battleID)->battleTacticDist() > 0; @@ -279,6 +282,7 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) return; const auto * state = gameHandler.getPlayerState(player); + assert(state && state->status != EPlayerStatus::INGAME); if(!state || state->status != EPlayerStatus::INGAME || !state->human) return; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 205ba8ae8..0cdd969c2 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -904,7 +904,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const bat.flags |= BattleAttack::DEATH_BLOW; } - const auto * owner = battle.battleGetFightingHero(attacker->unitOwner()); + const auto * owner = battle.battleGetFightingHero(attacker->unitSide()); if(owner) { int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index d4b36bf67..ea03a3433 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -51,33 +51,23 @@ void BattleProcessor::engageIntoBattle(PlayerColor player) gameHandler->sendAndApply(&pb); } -void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, +void BattleProcessor::restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, - const CGTownInstance *town) //use hero=nullptr for no hero + const CGTownInstance *town) { - assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); - assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); - - engageIntoBattle(army1->tempOwner); - engageIntoBattle(army2->tempOwner); - - static const CArmedInstance *armies[2]; - armies[0] = army1; - armies[1] = army2; - static const CGHeroInstance*heroes[2]; - heroes[0] = hero1; - heroes[1] = hero2; - - auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces - - const auto * battle = gameHandler->gameState()->getBattle(battleID); - assert(battle); + auto battle = gameHandler->gameState()->getBattle(battleID); auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + assert(lastBattleQuery); + //existing battle query for retying auto-combat if(lastBattleQuery) { + const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + for(int i : {0, 1}) { if(heroes[i]) @@ -89,21 +79,58 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm } } - lastBattleQuery->battleID = battle->getBattleID(); lastBattleQuery->result = std::nullopt; - lastBattleQuery->belligerents[0] = battle->sides[0].armyObject; - lastBattleQuery->belligerents[1] = battle->sides[1].armyObject; + + assert(lastBattleQuery->belligerents[0] == battle->sides[0].armyObject); + assert(lastBattleQuery->belligerents[1] == battle->sides[1].armyObject); } - auto nextBattleQuery = std::make_shared(gameHandler, battle); - for(int i : {0, 1}) + BattleCancelled bc; + bc.battleID = battleID; + gameHandler->sendAndApply(&bc); + + startBattlePrimary(army1, army2, tile, hero1, hero2, creatureBank, town); +} + +void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank, + const CGTownInstance *town) +{ + assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); + assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); + + engageIntoBattle(army1->tempOwner); + engageIntoBattle(army2->tempOwner); + + const CArmedInstance *armies[2]; + armies[0] = army1; + armies[1] = army2; + const CGHeroInstance*heroes[2]; + heroes[0] = hero1; + heroes[1] = hero2; + + auto battleID = setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces + + const auto * battle = gameHandler->gameState()->getBattle(battleID); + assert(battle); + + auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); + + if (lastBattleQuery) { - if(heroes[i]) - { - nextBattleQuery->initialHeroMana[i] = heroes[i]->mana; - } + lastBattleQuery->battleID = battleID; + } + else + { + auto newBattleQuery = std::make_shared(gameHandler, battle); + + // store initial mana to reset if battle has been restarted + for(int i : {0, 1}) + if(heroes[i]) + newBattleQuery->initialHeroMana[i] = heroes[i]->mana; + + gameHandler->queries->addQuery(newBattleQuery); } - gameHandler->queries->addQuery(nextBattleQuery); flowProcessor->onBattleStarted(*battle); } diff --git a/server/battles/BattleProcessor.h b/server/battles/BattleProcessor.h index 6dcb50de7..c7423eeb2 100644 --- a/server/battles/BattleProcessor.h +++ b/server/battles/BattleProcessor.h @@ -63,6 +63,8 @@ public: void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false); /// Starts battle between two armies (which can also be heroes) at position of 2nd object void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false); + /// Restart ongoing battle and end previous battle + void restartBattlePrimary(const BattleID & battleID, const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr); /// Processing of incoming battle action netpack bool makePlayerBattleAction(const BattleID & battleID, PlayerColor player, const BattleAction & ba); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 87e358f7a..627c2bb6c 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -278,11 +278,12 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) for(auto q : gameHandler->queries->allQueries()) { auto otherBattleQuery = std::dynamic_pointer_cast(q); - if(otherBattleQuery) + if(otherBattleQuery && otherBattleQuery->battleID == battle.getBattle()->getBattleID()) otherBattleQuery->result = battleQuery->result; } gameHandler->turnTimerHandler.onBattleEnd(battle.getBattle()->getBattleID()); + gameHandler->sendAndApply(battleResult); if (battleResult->queryID == QueryID::NONE) endBattleConfirm(battle); diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 10af7c517..8092c6bf9 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "BattleQueries.h" #include "MapQueries.h" +#include "QueriesProcessor.h" #include "../CGameHandler.h" #include "../battles/BattleProcessor.h" @@ -18,6 +19,8 @@ void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { + assert(result); + if(result) objectVisit.visitedObject->battleFinished(objectVisit.visitingHero, *result); } @@ -47,10 +50,21 @@ bool CBattleQuery::blocksPack(const CPack * pack) const void CBattleQuery::onRemoval(PlayerColor color) { + assert(result); + if(result) gh->battles->battleAfterLevelUp(battleID, *result); } +void CBattleQuery::onExposure(QueryPtr topQuery) +{ + // this method may be called in two cases: + // 1) when requesting battle replay (but before replay starts -> no valid result) + // 2) when aswering on levelup queries after accepting battle result -> valid result + if(result) + owner->popQuery(*this); +} + CBattleDialogQuery::CBattleDialogQuery(CGameHandler * owner, const IBattleInfo * bi): CDialogQuery(owner), bi(bi) @@ -64,7 +78,8 @@ void CBattleDialogQuery::onRemoval(PlayerColor color) assert(answer); if(*answer == 1) { - gh->startBattlePrimary( + gh->battles->restartBattlePrimary( + bi->getBattleID(), bi->getSideArmy(0), bi->getSideArmy(1), bi->getLocation(), diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index f23620307..a77805985 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -31,6 +31,7 @@ public: virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; virtual bool blocksPack(const CPack *pack) const override; virtual void onRemoval(PlayerColor color) override; + virtual void onExposure(QueryPtr topQuery) override; }; class CBattleDialogQuery : public CDialogQuery From 3675d887304c16037ae06c925b67155efd8071c9 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 3 Sep 2023 21:41:00 +0300 Subject: [PATCH 0347/1248] fixed highlighting slots --- client/widgets/CArtifactsOfHeroAltar.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 15 ++------- client/widgets/CArtifactsOfHeroBase.h | 1 - client/widgets/CArtifactsOfHeroKingdom.cpp | 2 ++ client/widgets/CArtifactsOfHeroMarket.cpp | 2 +- client/widgets/CWindowWithArtifacts.cpp | 38 +++++++++++++++++----- client/widgets/CWindowWithArtifacts.h | 1 + 7 files changed, 37 insertions(+), 24 deletions(-) diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 190eb0cbf..040da2eb8 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -58,7 +58,7 @@ void CArtifactsOfHeroAltar::updateBackpackSlots() void CArtifactsOfHeroAltar::scrollBackpack(int offset) { CArtifactsOfHeroBase::scrollBackpackForArtSet(offset, visibleArtSet); - safeRedraw(); + redraw(); } void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace) diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 3ee38c6d7..b8d175226 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -90,6 +90,8 @@ void CArtifactsOfHeroBase::init( rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT); leftBackpackRoll->block(true); rightBackpackRoll->block(true); + + setRedrawParent(true); } void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace) @@ -127,7 +129,7 @@ const CGHeroInstance * CArtifactsOfHeroBase::getHero() const void CArtifactsOfHeroBase::scrollBackpack(int offset) { scrollBackpackForArtSet(offset, *curHero); - safeRedraw(); + redraw(); } void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSet & artSet) @@ -172,17 +174,6 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe rightBackpackRoll->block(!scrollingPossible); } -void CArtifactsOfHeroBase::safeRedraw() -{ - if(isActive()) - { - if(parent) - parent->redraw(); - else - redraw(); - } -} - void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved) { for(auto artPlace : artWorn) diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index f7da73d79..d2d2a9a95 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -33,7 +33,6 @@ public: virtual void setHero(const CGHeroInstance * hero); virtual const CGHeroInstance * getHero() const; virtual void scrollBackpack(int offset); - virtual void safeRedraw(); virtual void markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved = true); virtual void unmarkSlots(); virtual ArtPlacePtr getArtPlace(const ArtifactPosition & slot); diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index 059f196fa..96c6f2cf9 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -39,6 +39,8 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto } leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1)); rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1)); + + setRedrawParent(true); } CArtifactsOfHeroKingdom::~CArtifactsOfHeroKingdom() diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index 74878b348..1f01775b3 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -37,5 +37,5 @@ void CArtifactsOfHeroMarket::scrollBackpack(int offset) } } } - safeRedraw(); + redraw(); } \ No newline at end of file diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 16b6dd950..7725c16c3 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -261,7 +261,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const auto pickedArtInst = std::get(curState.value()); assert(!pickedArtInst || destLoc.isHolder(std::get(curState.value()))); - auto artifactMovedBody = [this, withRedraw, &srcLoc, &destLoc, &pickedArtInst](auto artSetWeak) -> void + auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void { auto artSetPtr = artSetWeak.lock(); if(artSetPtr) @@ -269,12 +269,8 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const const auto hero = artSetPtr->getHero(); if(pickedArtInst) { - if(artSetPtr->isActive()) - { - CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); - if(srcLoc.isHolder(hero) || !std::is_same_v>) - artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); - } + markPossibleSlots(); + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); } else { @@ -296,7 +292,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const { cew->updateWidgets(); } - artSetPtr->safeRedraw(); + artSetPtr->redraw(); } // Make sure the status bar is updated so it does not display old text @@ -319,6 +315,7 @@ void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation & artLoc) void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation & artLoc) { + markPossibleSlots(); updateSlots(artLoc.slot); } @@ -333,7 +330,7 @@ void CWindowWithArtifacts::updateSlots(const ArtifactPosition & slot) else if(ArtifactUtils::isSlotBackpack(slot)) artSetPtr->updateBackpackSlots(); - artSetPtr->safeRedraw(); + artSetPtr->redraw(); } }; @@ -395,3 +392,26 @@ std::optional CWindowWithArtifacts::f } return res; } + +void CWindowWithArtifacts::markPossibleSlots() +{ + if(const auto pickedArtInst = getPickedArtifact()) + { + const auto heroArtOwner = getHeroPickedArtifact(); + auto artifactAssembledBody = [&pickedArtInst, &heroArtOwner](auto artSetWeak) -> void + { + if(auto artSetPtr = artSetWeak.lock()) + { + if(artSetPtr->isActive()) + { + const auto hero = artSetPtr->getHero(); + if(heroArtOwner == hero || !std::is_same_v>) + artSetPtr->markPossibleSlots(pickedArtInst, hero->tempOwner == LOCPLINT->playerID); + } + } + }; + + for(auto artSetWeak : artSets) + std::visit(artifactAssembledBody, artSetWeak); + } +} diff --git a/client/widgets/CWindowWithArtifacts.h b/client/widgets/CWindowWithArtifacts.h index c5180c688..85067e244 100644 --- a/client/widgets/CWindowWithArtifacts.h +++ b/client/widgets/CWindowWithArtifacts.h @@ -46,4 +46,5 @@ private: void updateSlots(const ArtifactPosition & slot); std::optional> getState(); std::optional findAOHbyRef(CArtifactsOfHeroBase & artsInst); + void markPossibleSlots(); }; From f1a6116088481af8af3e30d6d01e5c7985153a2b Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 3 Sep 2023 21:42:22 +0300 Subject: [PATCH 0348/1248] CHeroArea fix --- client/widgets/MiscWidgets.cpp | 20 ++++++++++++++++---- client/widgets/MiscWidgets.h | 14 +++++++++----- client/windows/GUIClasses.cpp | 10 ++++++---- client/windows/GUIClasses.h | 1 - 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 7e5ab637f..4eeb8226f 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -114,9 +114,10 @@ void LRClickableAreaWTextComp::showPopupWindow(const Point & cursorPosition) LRClickableAreaWText::showPopupWindow(cursorPosition); //only if with-component variant not occurred } -CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero) +CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero) : CIntObject(LCLICK | HOVER), - hero(_hero) + hero(hero), + clickFunctor(nullptr) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -126,13 +127,24 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero) pos.h = 64; if(hero) + { portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait); + clickFunctor = [hero]() -> void + { + LOCPLINT->openHeroWindow(hero); + }; + } +} + +void CHeroArea::addClickCallback(ClickFunctor callback) +{ + clickFunctor = callback; } void CHeroArea::clickPressed(const Point & cursorPosition) { - if(hero) - LOCPLINT->openHeroWindow(hero); + if(clickFunctor) + clickFunctor(); } void CHeroArea::hover(bool on) diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 5e6c6379a..804ff3db2 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -164,17 +164,21 @@ public: ~CMinorResDataBar(); }; -/// Opens hero window by left-clicking on it +/// Performs an action by left-clicking on it. Opens hero window by default class CHeroArea: public CIntObject { - const CGHeroInstance * hero; - std::shared_ptr portrait; - public: - CHeroArea(int x, int y, const CGHeroInstance * _hero); + using ClickFunctor = std::function; + CHeroArea(int x, int y, const CGHeroInstance * hero); + void addClickCallback(ClickFunctor callback); void clickPressed(const Point & cursorPosition) override; void hover(bool on) override; +private: + const CGHeroInstance * hero; + std::shared_ptr portrait; + ClickFunctor clickFunctor; + ClickFunctor showPopupHandler; }; /// Can interact on left and right mouse clicks diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 02e032315..67d3a9882 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -884,9 +884,6 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } - portraits[0] = std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInst[0]->portrait, 0, 257, 13); - portraits[1] = std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInst[1]->portrait, 0, 485, 13); - artifs[0] = std::make_shared(Point(-334, 150)); artifs[0]->setHero(heroInst[0]); artifs[1] = std::make_shared(Point(98, 150)); @@ -933,7 +930,12 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated()); } - heroAreas[b] = std::make_shared(257 + 228*b, 13, hero); + heroAreas[b] = std::make_shared(257 + 228 * b, 13, hero); + heroAreas[b]->addClickCallback([this, hero]() -> void + { + if(getPickedArtifact() == nullptr) + LOCPLINT->openHeroWindow(hero); + }); specialtyAreas[b] = std::make_shared(); specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 2e6f2e6df..fa1484d10 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -286,7 +286,6 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::array, 2> expValues; std::array, 2> manaImages; std::array, 2> manaValues; - std::array, 2> portraits; std::vector> primSkillAreas; std::array>, 2> secSkillAreas; From 1b810b5b9309fb331549f50b26f1bfed82757348 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 3 Sep 2023 21:43:41 +0300 Subject: [PATCH 0349/1248] combined arts assembling fixed --- CCallback.cpp | 6 +----- CCallback.h | 4 ++-- client/CPlayerInterface.cpp | 2 +- client/CPlayerInterface.h | 2 +- client/widgets/CArtifactHolder.cpp | 30 ++++++++++++++++++++++-------- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 5d5e00073..4768aaae8 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -168,14 +168,10 @@ bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation * @param assembleTo If assemble is true, this represents the artifact ID of the combination * artifact to assemble to. Otherwise it's not used. */ -bool CCallback::assembleArtifacts (const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { - if (player != hero->tempOwner) - return false; - AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); sendRequest(&aa); - return true; } void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) diff --git a/CCallback.h b/CCallback.h index 3657e3581..8fb7970f9 100644 --- a/CCallback.h +++ b/CCallback.h @@ -89,7 +89,7 @@ public: virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack //virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0; - virtual bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; + virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; virtual void eraseArtifactByClient(const ArtifactLocation & al)=0; virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; virtual void endTurn()=0; @@ -170,7 +170,7 @@ public: int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; bool dismissHero(const CGHeroInstance * hero) override; bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; - bool assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; + void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) override; void eraseArtifactByClient(const ArtifactLocation & al) override; bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index e277b9b30..ab28d7b8b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1288,7 +1288,7 @@ void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHer * into a combinational one on an artifact screen. Does not require the combination of * artifacts to be legal. */ -void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes) +void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes) { std::string text = artifact->getDescriptionTranslated(); text += "\n\n"; diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index a5e408ce4..5d91ef9e4 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -185,7 +185,7 @@ public: // public interface for use by client via LOCPLINT access void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); - void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); + void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); void waitWhileDialog(bool unlockPim = true); void waitForAllDialogs(bool unlockPim = true); void openTownWindow(const CGTownInstance * town); //shows townscreen diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index d27a2975f..b53237c9a 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -260,17 +260,28 @@ bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const Artif assert(hero); const auto art = hero->getArt(slot); assert(art); + + if(hero->tempOwner != LOCPLINT->playerID) + return false; + auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), ArtifactUtils::isSlotEquipment(slot)); - - for(const auto combinedArt : assemblyPossibilities) + if(!assemblyPossibilities.empty()) { - LOCPLINT->showArtifactAssemblyDialog( - art->artType, - combinedArt, - std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId())); + auto askThread = new boost::thread([hero, art, slot, assemblyPossibilities]() -> void + { + for(const auto combinedArt : assemblyPossibilities) + { + bool assembleConfirmed = false; + CFunctionList onYesHandlers([&assembleConfirmed]() -> void {assembleConfirmed = true; }); + onYesHandlers += std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId()); - if(assemblyPossibilities.size() > 2) - logGlobal->warn("More than one possibility of assembling on %s... taking only first", art->artType->getNameTranslated()); + LOCPLINT->showArtifactAssemblyDialog(art->artType, combinedArt, onYesHandlers); + LOCPLINT->waitWhileDialog(false); + if(assembleConfirmed) + break; + } + }); + askThread->detach(); return true; } return false; @@ -282,6 +293,9 @@ bool ArtifactUtilsClient::askToDisassemble(const CGHeroInstance * hero, const Ar const auto art = hero->getArt(slot); assert(art); + if(hero->tempOwner != LOCPLINT->playerID) + return false; + if(art->isCombined()) { if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->getConstituents().size() - 1)) From 40ea45498fb22e892474cc877c870572c16c5e1b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 7 Sep 2023 19:57:57 +0200 Subject: [PATCH 0350/1248] Rename --- config/schemas/mod.json | 2 +- launcher/modManager/cmodlist.cpp | 6 +++--- launcher/modManager/cmodlistview_moc.cpp | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index e4dd19ab3..5415d0275 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -22,7 +22,7 @@ "type" : "string", "description" : "Author of the mod. Can be nickname, real name or name of team" }, - "size": { + "downloadSize": { "type" : "number", "description" : "Approximate size of mod, compressed by zip algorithm, in Mb" }, diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index f2700e759..f3b59d81e 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -327,7 +327,7 @@ CModEntry CModList::getMod(QString modname) const //take valid download link, screenshots and mod size before assignment auto download = repo.value("download"); auto screenshots = repo.value("screenshots"); - auto size = repo.value("size"); + auto size = repo.value("downloadSize"); repo = repoValMap; if(repo.value("download").isNull()) { @@ -335,8 +335,8 @@ CModEntry CModList::getMod(QString modname) const if(repo.value("screenshots").isNull()) //taking screenshot from the downloadable version repo["screenshots"] = screenshots; } - if(repo.value("size").isNull()) - repo["size"] = size; + if(repo.value("downloadSize").isNull()) + repo["downloadSize"] = size; } } } diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index dc960f435..c61a99ecf 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -249,9 +249,9 @@ QString CModListView::genModInfoText(CModEntry & mod) result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); if(mod.getValue("localSizeBytes").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); - if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("size").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("size").toDouble())), lineTemplate.arg(tr("Download size"))); + result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("downloadSize"))); + if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid()) + result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size"))); result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg(tr("Authors"))); @@ -543,7 +543,7 @@ void CModListView::on_updateButton_clicked() auto mod = modModel->getMod(name); // update required mod, install missing (can be new dependency) if(mod.isUpdateable() || !mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } @@ -573,7 +573,7 @@ void CModListView::on_installButton_clicked() { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } @@ -863,7 +863,7 @@ void CModListView::doInstallMod(const QString & modName) { auto mod = modModel->getMod(name); if(!mod.isInstalled()) - downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("size").toDouble())); + downloadFile(name + ".zip", mod.getValue("download").toString(), "mods", mbToBytes(mod.getValue("downloadSize").toDouble())); } } From 9e14d8d17001d3840a8820562922a287554664fd Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 7 Sep 2023 20:19:31 +0200 Subject: [PATCH 0351/1248] Refactoring map settings --- mapeditor/CMakeLists.txt | 6 +++--- mapeditor/mapsettings/abstractsettings.h | 9 +++++++++ mapeditor/mapsettings/generalsettings.cpp | 9 +++++++++ mapeditor/mapsettings/generalsettings.h | 9 +++++++++ mapeditor/mapsettings/loseconditions.h | 9 +++++++++ mapeditor/mapsettings/mapsettings.ui | 4 ++-- mapeditor/{ => mapsettings}/timedevent.cpp | 10 ++++++++++ mapeditor/{ => mapsettings}/timedevent.h | 9 +++++++++ mapeditor/{ => mapsettings}/timedevent.ui | 0 mapeditor/mapsettings/victoryconditions.h | 9 +++++++++ 10 files changed, 69 insertions(+), 5 deletions(-) rename mapeditor/{ => mapsettings}/timedevent.cpp (55%) rename mapeditor/{ => mapsettings}/timedevent.h (62%) rename mapeditor/{ => mapsettings}/timedevent.ui (100%) diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 2a6830c82..3d83dda8a 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -16,6 +16,7 @@ set(editor_SRCS mapsettings/mapsettings.cpp mapsettings/generalsettings.cpp mapsettings/modsettings.cpp + mapsettings/timedevent.cpp mapsettings/victoryconditions.cpp mapsettings/loseconditions.cpp playersettings.cpp @@ -23,7 +24,6 @@ set(editor_SRCS scenelayer.cpp mapcontroller.cpp validator.cpp - timedevent.cpp inspector/inspector.cpp inspector/townbulidingswidget.cpp inspector/armywidget.cpp @@ -50,6 +50,7 @@ set(editor_HEADERS mapsettings/mapsettings.h mapsettings/generalsettings.h mapsettings/modsettings.h + mapsettings/timedevent.h mapsettings/victoryconditions.h mapsettings/loseconditions.h playersettings.h @@ -57,7 +58,6 @@ set(editor_HEADERS scenelayer.h mapcontroller.h validator.h - timedevent.h inspector/inspector.h inspector/townbulidingswidget.h inspector/armywidget.h @@ -74,12 +74,12 @@ set(editor_FORMS mapsettings/mapsettings.ui mapsettings/generalsettings.ui mapsettings/modsettings.ui + mapsettings/timedevent.ui mapsettings/victoryconditions.ui mapsettings/loseconditions.ui playersettings.ui playerparams.ui validator.ui - timedevent.ui inspector/townbulidingswidget.ui inspector/armywidget.ui inspector/messagewidget.ui diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 2e59f47b9..5985613eb 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -1,3 +1,12 @@ +/* + * abstractsettings.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 + * + */ #ifndef ABSTRACTSETTINGS_H #define ABSTRACTSETTINGS_H diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index db1280eab..347c9234f 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -1,3 +1,12 @@ +/* + * generalsettings.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 "generalsettings.h" #include "ui_generalsettings.h" diff --git a/mapeditor/mapsettings/generalsettings.h b/mapeditor/mapsettings/generalsettings.h index d5c56a270..025cd1e50 100644 --- a/mapeditor/mapsettings/generalsettings.h +++ b/mapeditor/mapsettings/generalsettings.h @@ -1,3 +1,12 @@ +/* + * generalsettings.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 + * + */ #ifndef GENERALSETTINGS_H #define GENERALSETTINGS_H diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h index 91605e45f..4f3907873 100644 --- a/mapeditor/mapsettings/loseconditions.h +++ b/mapeditor/mapsettings/loseconditions.h @@ -1,3 +1,12 @@ +/* + * loseconditions.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 + * + */ #ifndef LOSECONDITIONS_H #define LOSECONDITIONS_H /* diff --git a/mapeditor/mapsettings/mapsettings.ui b/mapeditor/mapsettings/mapsettings.ui index 9ed0004ff..a75ae1cab 100644 --- a/mapeditor/mapsettings/mapsettings.ui +++ b/mapeditor/mapsettings/mapsettings.ui @@ -35,7 +35,7 @@ - 0 + 2 @@ -92,7 +92,7 @@ - 0 + 2 diff --git a/mapeditor/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp similarity index 55% rename from mapeditor/timedevent.cpp rename to mapeditor/mapsettings/timedevent.cpp index 5f0e470c7..b199228a9 100644 --- a/mapeditor/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -1,3 +1,13 @@ +/* + * timedevent.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 "timedevent.h" #include "ui_timedevent.h" diff --git a/mapeditor/timedevent.h b/mapeditor/mapsettings/timedevent.h similarity index 62% rename from mapeditor/timedevent.h rename to mapeditor/mapsettings/timedevent.h index 7f60c4c40..05784a30c 100644 --- a/mapeditor/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -1,3 +1,12 @@ +/* + * timedevent.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 + * + */ #ifndef TIMEDEVENT_H #define TIMEDEVENT_H diff --git a/mapeditor/timedevent.ui b/mapeditor/mapsettings/timedevent.ui similarity index 100% rename from mapeditor/timedevent.ui rename to mapeditor/mapsettings/timedevent.ui diff --git a/mapeditor/mapsettings/victoryconditions.h b/mapeditor/mapsettings/victoryconditions.h index 7a52104dd..f47b6b293 100644 --- a/mapeditor/mapsettings/victoryconditions.h +++ b/mapeditor/mapsettings/victoryconditions.h @@ -1,3 +1,12 @@ +/* + * victoryconditions.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 + * + */ #ifndef VICTORYCONDITIONS_H #define VICTORYCONDITIONS_H From e8b9034149e109d37c00b430a14ccf9da44707f0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 7 Sep 2023 23:57:56 +0200 Subject: [PATCH 0352/1248] Add event settings ui --- mapeditor/CMakeLists.txt | 3 + mapeditor/mapsettings/eventsettings.cpp | 42 ++++++++++++++ mapeditor/mapsettings/eventsettings.h | 41 ++++++++++++++ mapeditor/mapsettings/eventsettings.ui | 74 +++++++++++++++++++++++++ 4 files changed, 160 insertions(+) create mode 100644 mapeditor/mapsettings/eventsettings.cpp create mode 100644 mapeditor/mapsettings/eventsettings.h create mode 100644 mapeditor/mapsettings/eventsettings.ui diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 3d83dda8a..8fd10a8e0 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -19,6 +19,7 @@ set(editor_SRCS mapsettings/timedevent.cpp mapsettings/victoryconditions.cpp mapsettings/loseconditions.cpp + mapsettings/eventsettings.cpp playersettings.cpp playerparams.cpp scenelayer.cpp @@ -53,6 +54,7 @@ set(editor_HEADERS mapsettings/timedevent.h mapsettings/victoryconditions.h mapsettings/loseconditions.h + mapsettings/eventsettings.h playersettings.h playerparams.h scenelayer.h @@ -77,6 +79,7 @@ set(editor_FORMS mapsettings/timedevent.ui mapsettings/victoryconditions.ui mapsettings/loseconditions.ui + mapsettings/eventsettings.ui playersettings.ui playerparams.ui validator.ui diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp new file mode 100644 index 000000000..d852d6ed1 --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -0,0 +1,42 @@ +/* + * eventsettings.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 "eventsettings.h" +#include "ui_eventsettings.h" + +EventSettings::EventSettings(QWidget *parent) : + QWidget(parent), + ui(new Ui::EventSettings) +{ + ui->setupUi(this); +} + +EventSettings::~EventSettings() +{ + delete ui; +} + +void EventSettings::on_timedEventAdd_clicked() +{ + +} + + +void EventSettings::on_timedEventRemove_clicked() +{ + +} + + +void EventSettings::on_eventsList_itemActivated(QListWidgetItem *item) +{ + +} + diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h new file mode 100644 index 000000000..d37c1ba79 --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.h @@ -0,0 +1,41 @@ +/* + * eventsettings.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 + * + */ +#ifndef EVENTSETTINGS_H +#define EVENTSETTINGS_H + +#include "abstractsettings.h" + +namespace Ui { +class EventSettings; +} + +class EventSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit EventSettings(QWidget *parent = nullptr); + ~EventSettings(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_timedEventAdd_clicked(); + + void on_timedEventRemove_clicked(); + + void on_eventsList_itemActivated(QListWidgetItem *item); + +private: + Ui::EventSettings *ui; +}; + +#endif // EVENTSETTINGS_H diff --git a/mapeditor/mapsettings/eventsettings.ui b/mapeditor/mapsettings/eventsettings.ui new file mode 100644 index 000000000..87f40d5d7 --- /dev/null +++ b/mapeditor/mapsettings/eventsettings.ui @@ -0,0 +1,74 @@ + + + EventSettings + + + + 0 + 0 + 672 + 456 + + + + Form + + + + + + + + Timed events + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + Remove + + + + + + + + + + + + + From 446c61cce10d102112b9fe057729dce836de18f0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 8 Sep 2023 01:43:01 +0200 Subject: [PATCH 0353/1248] Timed events implemented --- mapeditor/mapsettings/eventsettings.cpp | 83 ++++++++++++++++++++- mapeditor/mapsettings/loseconditions.cpp | 9 +++ mapeditor/mapsettings/loseconditions.h | 13 +--- mapeditor/mapsettings/mapsettings.cpp | 18 +---- mapeditor/mapsettings/mapsettings.ui | 33 ++------ mapeditor/mapsettings/modsettings.cpp | 9 +++ mapeditor/mapsettings/modsettings.h | 9 +++ mapeditor/mapsettings/timedevent.cpp | 43 ++++++++++- mapeditor/mapsettings/timedevent.h | 3 +- mapeditor/mapsettings/timedevent.ui | 4 +- mapeditor/mapsettings/victoryconditions.cpp | 9 +++ 11 files changed, 172 insertions(+), 61 deletions(-) diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index d852d6ed1..125295608 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -9,10 +9,60 @@ */ #include "StdInc.h" #include "eventsettings.h" +#include "timedevent.h" #include "ui_eventsettings.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/constants/NumericConstants.h" +#include "../../lib/constants/StringConstants.h" + +QVariant toVariant(const TResources & resources) +{ + QVariantMap result; + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + result[QString::fromStdString(GameConstants::RESOURCE_NAMES[i])] = QVariant::fromValue(resources[i]); + return result; +} + +TResources resourcesFromVariant(const QVariant & v) +{ + JsonNode vJson; + for(auto r : v.toMap().keys()) + vJson[r.toStdString()].Integer() = v.toMap().value(r).toInt(); + return TResources(vJson); + +} + +QVariant toVariant(const CMapEvent & event) +{ + QVariantMap result; + result["name"] = QString::fromStdString(event.name); + result["message"] = QString::fromStdString(event.message); + result["players"] = QVariant::fromValue(event.players); + result["humanAffected"] = QVariant::fromValue(event.humanAffected); + result["computerAffected"] = QVariant::fromValue(event.computerAffected); + result["firstOccurence"] = QVariant::fromValue(event.firstOccurence); + result["nextOccurence"] = QVariant::fromValue(event.nextOccurence); + result["resources"] = toVariant(event.resources); + return QVariant(result); +} + +CMapEvent eventFromVariant(const QVariant & variant) +{ + CMapEvent result; + auto v = variant.toMap(); + result.name = v.value("name").toString().toStdString(); + result.message = v.value("message").toString().toStdString(); + result.players = v.value("players").toInt(); + result.humanAffected = v.value("humanAffected").toInt(); + result.computerAffected = v.value("computerAffected").toInt(); + result.firstOccurence = v.value("firstOccurence").toInt(); + result.nextOccurence = v.value("nextOccurence").toInt(); + result.resources = resourcesFromVariant(v.value("resources")); + return result; +} EventSettings::EventSettings(QWidget *parent) : - QWidget(parent), + AbstractSettings(parent), ui(new Ui::EventSettings) { ui->setupUi(this); @@ -23,20 +73,45 @@ EventSettings::~EventSettings() delete ui; } +void EventSettings::initialize(const CMap & map) +{ + for(const auto & event : map.events) + { + auto * item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(Qt::UserRole, toVariant(event)); + ui->eventsList->addItem(item); + } +} + +void EventSettings::update(CMap & map) +{ + map.events.clear(); + for(int i = 0; i < ui->eventsList->count(); ++i) + { + const auto * item = ui->eventsList->item(i); + map.events.push_back(eventFromVariant(item->data(Qt::UserRole))); + } +} + void EventSettings::on_timedEventAdd_clicked() { - + CMapEvent event; + event.name = tr("New event").toStdString(); + auto * item = new QListWidgetItem(QString::fromStdString(event.name)); + item->setData(Qt::UserRole, toVariant(event)); + ui->eventsList->addItem(item); } void EventSettings::on_timedEventRemove_clicked() { - + if(auto * item = ui->eventsList->currentItem()) + ui->eventsList->removeItemWidget(item); } void EventSettings::on_eventsList_itemActivated(QListWidgetItem *item) { - + new TimedEvent(item, parentWidget()); } diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index cf47c7776..fe761df9b 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -1,3 +1,12 @@ +/* + * loseconditions.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 "loseconditions.h" #include "ui_loseconditions.h" diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h index 4f3907873..778e8c146 100644 --- a/mapeditor/mapsettings/loseconditions.h +++ b/mapeditor/mapsettings/loseconditions.h @@ -1,14 +1,3 @@ -/* - * loseconditions.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 - * - */ -#ifndef LOSECONDITIONS_H -#define LOSECONDITIONS_H /* * loseconditions.h, part of VCMI engine * @@ -19,6 +8,8 @@ * */ +#ifndef LOSECONDITIONS_H +#define LOSECONDITIONS_H #include "abstractsettings.h" namespace Ui { diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index ac91b0a71..1ab58f000 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -67,22 +67,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : ui->mods->initialize(*controller.map()); ui->victory->initialize(*controller.map()); ui->lose->initialize(*controller.map()); - - //timed events - for(auto & ev : controller.map()->events) - { - QVariantMap descriptor; - descriptor["message"] = QVariant::fromValue(QString::fromStdString(ev.message)); - descriptor["players"] = QVariant::fromValue(ev.players); - descriptor["humanAffected"] = QVariant::fromValue(ev.humanAffected); - descriptor["computerAffected"] = QVariant::fromValue(ev.computerAffected); - descriptor["firstOccurence"] = QVariant::fromValue(ev.firstOccurence); - descriptor["nextOccurence"] = QVariant::fromValue(ev.nextOccurence); - - auto * item = new QListWidgetItem(QString::fromStdString(ev.name)); - item->setData(Qt::UserRole, descriptor); - ui->eventsList->addItem(item); - } + ui->events->initialize(*controller.map()); } MapSettings::~MapSettings() @@ -112,6 +97,7 @@ void MapSettings::on_pushButton_clicked() ui->mods->update(*controller.map()); ui->victory->update(*controller.map()); ui->lose->update(*controller.map()); + ui->events->update(*controller.map()); controller.commitChangeWithoutRedraw(); diff --git a/mapeditor/mapsettings/mapsettings.ui b/mapeditor/mapsettings/mapsettings.ui index a75ae1cab..cdffdaa0e 100644 --- a/mapeditor/mapsettings/mapsettings.ui +++ b/mapeditor/mapsettings/mapsettings.ui @@ -147,32 +147,7 @@ 12 - - - - - Timed events - - - - - - - Add - - - - - - - Remove - - - - - - - + @@ -356,6 +331,12 @@
    mapsettings/loseconditions.h
    1 + + EventSettings + QWidget +
    mapsettings/eventsettings.h
    + 1 +
    diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index 6987087f5..2cc5fbe32 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -1,3 +1,12 @@ +/* + * modsettings.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 "modsettings.h" #include "ui_modsettings.h" diff --git a/mapeditor/mapsettings/modsettings.h b/mapeditor/mapsettings/modsettings.h index 0051174cf..6e5acd5b9 100644 --- a/mapeditor/mapsettings/modsettings.h +++ b/mapeditor/mapsettings/modsettings.h @@ -1,3 +1,12 @@ +/* + * modsettings.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 + * + */ #ifndef MODSETTINGS_H #define MODSETTINGS_H diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index b199228a9..b2ceeff06 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -10,12 +10,35 @@ #include "StdInc.h" #include "timedevent.h" #include "ui_timedevent.h" +#include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/constants/StringConstants.h" -TimedEvent::TimedEvent(QWidget *parent) : +TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : QDialog(parent), + target(t), ui(new Ui::TimedEvent) { ui->setupUi(this); + + const auto params = t->data(Qt::UserRole).toMap(); + ui->eventNameText->setText(params.value("name").toString()); + ui->eventMessageText->setPlainText(params.value("message").toString()); + ui->eventAffectsCpu->setChecked(params.value("computerAffected").toBool()); + ui->eventAffectsHuman->setChecked(params.value("humanAffected").toBool()); + ui->eventFirstOccurance->setValue(params.value("firstOccurence").toInt()); + ui->eventRepeatAfter->setValue(params.value("nextOccurence").toInt()); + + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + bool isAffected = (1 << i) & params.value("players").toInt(); + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); + ui->playersAffected->addItem(item); + } + //result.resources = resourcesFromVariant(v.value("resources")); + + show(); } TimedEvent::~TimedEvent() @@ -29,8 +52,26 @@ void TimedEvent::on_eventResources_clicked() } + void TimedEvent::on_TimedEvent_finished(int result) { + QVariantMap descriptor; + descriptor["name"] = ui->eventNameText->text(); + descriptor["message"] = ui->eventMessageText->toPlainText(); + descriptor["humanAffected"] = QVariant::fromValue(ui->eventAffectsHuman->isChecked()); + descriptor["computerAffected"] = QVariant::fromValue(ui->eventAffectsCpu->isChecked()); + descriptor["firstOccurence"] = QVariant::fromValue(ui->eventFirstOccurance->value()); + descriptor["nextOccurence"] = QVariant::fromValue(ui->eventRepeatAfter->value()); + int players = 0; + for(int i = 0; i < ui->playersAffected->count(); ++i) + { + auto * item = ui->playersAffected->item(i); + if(item->checkState() == Qt::Checked) + players |= 1 << i; + } + descriptor["players"] = QVariant::fromValue(players); + target->setData(Qt::UserRole, descriptor); + target->setText(ui->eventNameText->text()); } diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h index 05784a30c..e2b7129fa 100644 --- a/mapeditor/mapsettings/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -21,7 +21,7 @@ class TimedEvent : public QDialog Q_OBJECT public: - explicit TimedEvent(QWidget *parent = nullptr); + explicit TimedEvent(QListWidgetItem *, QWidget *parent = nullptr); ~TimedEvent(); private slots: @@ -31,6 +31,7 @@ private slots: private: Ui::TimedEvent *ui; + QListWidgetItem * target; }; #endif // TIMEDEVENT_H diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui index 06e4d2e77..805b8cd20 100644 --- a/mapeditor/mapsettings/timedevent.ui +++ b/mapeditor/mapsettings/timedevent.ui @@ -23,7 +23,7 @@ - + Event name @@ -103,7 +103,7 @@ 0 - + 0 diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index 61d7b16ac..f88eee931 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -1,3 +1,12 @@ +/* + * victoryconditions.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 + * + */ #include "StdInc.h" #include "victoryconditions.h" #include "ui_victoryconditions.h" From bb4905af4800c0ea3978380fb9eb7a5d84b75be7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 8 Sep 2023 01:54:29 +0200 Subject: [PATCH 0354/1248] Change resourceID with ResourcePath --- launcher/modManager/cmodmanager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 1188465ce..8ba55876b 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -89,7 +89,7 @@ void CModManager::loadMods() { //calculate mod size qint64 total = 0; - ResourceID resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); + ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY); if(CResourceHandler::get()->existsResource(resDir)) { for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next()) From ad8d7016b68049acea48f437150f6e5d5e54243c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 8 Sep 2023 03:21:31 +0200 Subject: [PATCH 0355/1248] Resources in time events --- mapeditor/mapsettings/eventsettings.cpp | 1 + mapeditor/mapsettings/timedevent.cpp | 45 +++++++++++--- mapeditor/mapsettings/timedevent.h | 5 +- mapeditor/mapsettings/timedevent.ui | 83 ++++++++++++++++++++++++- 4 files changed, 124 insertions(+), 10 deletions(-) diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 125295608..944c11ad2 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -100,6 +100,7 @@ void EventSettings::on_timedEventAdd_clicked() auto * item = new QListWidgetItem(QString::fromStdString(event.name)); item->setData(Qt::UserRole, toVariant(event)); ui->eventsList->addItem(item); + on_eventsList_itemActivated(item); } diff --git a/mapeditor/mapsettings/timedevent.cpp b/mapeditor/mapsettings/timedevent.cpp index b2ceeff06..a0aec1c17 100644 --- a/mapeditor/mapsettings/timedevent.cpp +++ b/mapeditor/mapsettings/timedevent.cpp @@ -20,6 +20,8 @@ TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : { ui->setupUi(this); + + const auto params = t->data(Qt::UserRole).toMap(); ui->eventNameText->setText(params.value("name").toString()); ui->eventMessageText->setPlainText(params.value("message").toString()); @@ -36,7 +38,17 @@ TimedEvent::TimedEvent(QListWidgetItem * t, QWidget *parent) : item->setCheckState(isAffected ? Qt::Checked : Qt::Unchecked); ui->playersAffected->addItem(item); } - //result.resources = resourcesFromVariant(v.value("resources")); + + ui->resources->setRowCount(GameConstants::RESOURCE_QUANTITY); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto name = QString::fromStdString(GameConstants::RESOURCE_NAMES[i]); + int val = params.value("resources").toMap().value(name).toInt(); + ui->resources->setItem(i, 0, new QTableWidgetItem(name)); + auto nval = new QTableWidgetItem(QString::number(val)); + nval->setFlags(nval->flags() | Qt::ItemIsEditable); + ui->resources->setItem(i, 1, nval); + } show(); } @@ -46,12 +58,6 @@ TimedEvent::~TimedEvent() delete ui; } -void TimedEvent::on_eventResources_clicked() -{ - -} - - void TimedEvent::on_TimedEvent_finished(int result) { @@ -71,7 +77,32 @@ void TimedEvent::on_TimedEvent_finished(int result) players |= 1 << i; } descriptor["players"] = QVariant::fromValue(players); + + auto res = target->data(Qt::UserRole).toMap().value("resources").toMap(); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) + { + auto * itemType = ui->resources->item(i, 0); + auto * itemQty = ui->resources->item(i, 1); + res[itemType->text()] = QVariant::fromValue(itemQty->text().toInt()); + } + descriptor["resources"] = res; + target->setData(Qt::UserRole, descriptor); target->setText(ui->eventNameText->text()); } + +void TimedEvent::on_pushButton_clicked() +{ + close(); +} + + +void TimedEvent::on_resources_itemDoubleClicked(QTableWidgetItem *item) +{ + if(item && item->column() == 1) + { + ui->resources->editItem(item); + } +} + diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h index e2b7129fa..b72480837 100644 --- a/mapeditor/mapsettings/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -25,10 +25,13 @@ public: ~TimedEvent(); private slots: - void on_eventResources_clicked(); void on_TimedEvent_finished(int result); + void on_pushButton_clicked(); + + void on_resources_itemDoubleClicked(QTableWidgetItem *item); + private: Ui::TimedEvent *ui; QListWidgetItem * target; diff --git a/mapeditor/mapsettings/timedevent.ui b/mapeditor/mapsettings/timedevent.ui index 805b8cd20..92ac597ab 100644 --- a/mapeditor/mapsettings/timedevent.ui +++ b/mapeditor/mapsettings/timedevent.ui @@ -102,6 +102,13 @@ 0 + + + + Affected players + + + @@ -112,19 +119,91 @@ - 90 + 120 16777215 - + Resources + + + + + 0 + 0 + + + + + 120 + 16777215 + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + 2 + + + false + + + true + + + 20 + + + 60 + + + true + + + false + + + 16 + + + 16 + + + + type + + + + + qty + + + + + + + + Ok + + + From c4e2417326416d1294cbe136c14af1a898622b90 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 8 Sep 2023 16:35:43 +0300 Subject: [PATCH 0356/1248] Fix crash on obstacle creation (e.g. moat) --- lib/battle/CBattleInfoCallback.cpp | 1 + lib/spells/effects/Moat.cpp | 1 + lib/spells/effects/Obstacle.cpp | 1 + lib/spells/effects/RemoveObstacle.cpp | 1 + 4 files changed, 4 insertions(+) diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 5655c017d..c8714652d 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -874,6 +874,7 @@ bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & s changedObstacle.revealed = true; BattleObstaclesChanged bocp; + bocp.battleID = getBattle()->getBattleID(); bocp.changes.emplace_back(spellObstacle.uniqueID, operation); changedObstacle.toInfo(bocp.changes.back(), operation); spellEnv.apply(&bocp); diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 0c8a6c93c..bb80b9d08 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -128,6 +128,7 @@ void Moat::placeObstacles(ServerCallback * server, const Mechanics * m, const Ef assert(m->casterSide == BattleSide::DEFENDER); // Moats are always cast by defender BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 720bc1fa0..731bb736c 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -270,6 +270,7 @@ void Obstacle::placeObstacles(ServerCallback * server, const Mechanics * m, cons const ObstacleSideOptions & options = sideOptions.at(m->casterSide); BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); auto all = m->battle()->battleGetAllObstacles(BattlePerspective::ALL_KNOWING); diff --git a/lib/spells/effects/RemoveObstacle.cpp b/lib/spells/effects/RemoveObstacle.cpp index 112435dcb..10c5ca57f 100644 --- a/lib/spells/effects/RemoveObstacle.cpp +++ b/lib/spells/effects/RemoveObstacle.cpp @@ -44,6 +44,7 @@ bool RemoveObstacle::applicable(Problem & problem, const Mechanics * m, const Ef void RemoveObstacle::apply(ServerCallback * server, const Mechanics * m, const EffectTarget & target) const { BattleObstaclesChanged pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); for(const auto & obstacle : getTargets(m, target, false)) { From 320cd6bf167e5ae347c599cb211416f453d224c1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 8 Sep 2023 17:54:34 +0200 Subject: [PATCH 0357/1248] Add rumors --- mapeditor/CMakeLists.txt | 3 + mapeditor/mapsettings/eventsettings.cpp | 2 +- mapeditor/mapsettings/eventsettings.ui | 12 +++ mapeditor/mapsettings/generalsettings.ui | 12 +++ mapeditor/mapsettings/loseconditions.ui | 12 +++ mapeditor/mapsettings/mapsettings.cpp | 2 + mapeditor/mapsettings/mapsettings.ui | 31 +++++- mapeditor/mapsettings/modsettings.ui | 12 +++ mapeditor/mapsettings/rumorsettings.cpp | 81 ++++++++++++++++ mapeditor/mapsettings/rumorsettings.h | 43 +++++++++ mapeditor/mapsettings/rumorsettings.ui | 105 +++++++++++++++++++++ mapeditor/mapsettings/victoryconditions.ui | 12 +++ 12 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 mapeditor/mapsettings/rumorsettings.cpp create mode 100644 mapeditor/mapsettings/rumorsettings.h create mode 100644 mapeditor/mapsettings/rumorsettings.ui diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 8fd10a8e0..59e83258d 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -20,6 +20,7 @@ set(editor_SRCS mapsettings/victoryconditions.cpp mapsettings/loseconditions.cpp mapsettings/eventsettings.cpp + mapsettings/rumorsettings.cpp playersettings.cpp playerparams.cpp scenelayer.cpp @@ -55,6 +56,7 @@ set(editor_HEADERS mapsettings/victoryconditions.h mapsettings/loseconditions.h mapsettings/eventsettings.h + mapsettings/rumorsettings.h playersettings.h playerparams.h scenelayer.h @@ -80,6 +82,7 @@ set(editor_FORMS mapsettings/victoryconditions.ui mapsettings/loseconditions.ui mapsettings/eventsettings.ui + mapsettings/rumorsettings.ui playersettings.ui playerparams.ui validator.ui diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 944c11ad2..22a28e09f 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -107,7 +107,7 @@ void EventSettings::on_timedEventAdd_clicked() void EventSettings::on_timedEventRemove_clicked() { if(auto * item = ui->eventsList->currentItem()) - ui->eventsList->removeItemWidget(item); + ui->eventsList->takeItem(ui->eventsList->row(item)); } diff --git a/mapeditor/mapsettings/eventsettings.ui b/mapeditor/mapsettings/eventsettings.ui index 87f40d5d7..cd9ea89b9 100644 --- a/mapeditor/mapsettings/eventsettings.ui +++ b/mapeditor/mapsettings/eventsettings.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/mapeditor/mapsettings/generalsettings.ui b/mapeditor/mapsettings/generalsettings.ui index b4b4e6626..e9759ff4f 100644 --- a/mapeditor/mapsettings/generalsettings.ui +++ b/mapeditor/mapsettings/generalsettings.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/mapeditor/mapsettings/loseconditions.ui b/mapeditor/mapsettings/loseconditions.ui index 066a18c8e..f96f2a635 100644 --- a/mapeditor/mapsettings/loseconditions.ui +++ b/mapeditor/mapsettings/loseconditions.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 1ab58f000..191583805 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -68,6 +68,7 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : ui->victory->initialize(*controller.map()); ui->lose->initialize(*controller.map()); ui->events->initialize(*controller.map()); + ui->rumors->initialize(*controller.map()); } MapSettings::~MapSettings() @@ -98,6 +99,7 @@ void MapSettings::on_pushButton_clicked() ui->victory->update(*controller.map()); ui->lose->update(*controller.map()); ui->events->update(*controller.map()); + ui->rumors->update(*controller.map()); controller.commitChangeWithoutRedraw(); diff --git a/mapeditor/mapsettings/mapsettings.ui b/mapeditor/mapsettings/mapsettings.ui index cdffdaa0e..360bea673 100644 --- a/mapeditor/mapsettings/mapsettings.ui +++ b/mapeditor/mapsettings/mapsettings.ui @@ -35,7 +35,7 @@ - 2 + 0 @@ -49,7 +49,7 @@ 12 - 12 + 3 @@ -92,7 +92,7 @@ - 2 + 0 @@ -151,6 +151,25 @@ + + + Rumors + + + + 12 + + + 12 + + + 12 + + + + + +
    @@ -337,6 +356,12 @@
    mapsettings/eventsettings.h
    1 + + RumorSettings + QWidget +
    mapsettings/rumorsettings.h
    + 1 +
    diff --git a/mapeditor/mapsettings/modsettings.ui b/mapeditor/mapsettings/modsettings.ui index 47f57e89c..0c3834d7a 100644 --- a/mapeditor/mapsettings/modsettings.ui +++ b/mapeditor/mapsettings/modsettings.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp new file mode 100644 index 000000000..988e22ac4 --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -0,0 +1,81 @@ +/* + * rumorsettings.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 "rumorsettings.h" +#include "ui_rumorsettings.h" + +RumorSettings::RumorSettings(QWidget *parent) : + AbstractSettings(parent), + ui(new Ui::RumorSettings) +{ + ui->setupUi(this); +} + +RumorSettings::~RumorSettings() +{ + delete ui; +} + +void RumorSettings::initialize(const CMap & map) +{ + for(auto & rumor : map.rumors) + { + auto * item = new QListWidgetItem(QString::fromStdString(rumor.name)); + item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text))); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->rumors->addItem(item); + } +} + +void RumorSettings::update(CMap & map) +{ + map.rumors.clear(); + for(int i = 0; i < ui->rumors->count(); ++i) + { + Rumor rumor; + rumor.name = ui->rumors->item(i)->text().toStdString(); + rumor.text = ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString(); + map.rumors.push_back(rumor); + } +} + +void RumorSettings::on_message_textChanged() +{ + if(auto item = ui->rumors->currentItem()) + item->setData(Qt::UserRole, QVariant(ui->message->toPlainText())); +} + +void RumorSettings::on_add_clicked() +{ + auto * item = new QListWidgetItem(tr("New rumor")); + item->setData(Qt::UserRole, QVariant("")); + item->setFlags(item->flags() | Qt::ItemIsEditable); + ui->rumors->addItem(item); + emit ui->rumors->itemActivated(item); +} + +void RumorSettings::on_remove_clicked() +{ + if(auto item = ui->rumors->currentItem()) + { + ui->message->setPlainText(""); + ui->rumors->takeItem(ui->rumors->row(item)); + } +} + + +void RumorSettings::on_rumors_itemSelectionChanged() +{ + if(auto item = ui->rumors->currentItem()) + ui->message->setPlainText(item->data(Qt::UserRole).toString()); +} + + + diff --git a/mapeditor/mapsettings/rumorsettings.h b/mapeditor/mapsettings/rumorsettings.h new file mode 100644 index 000000000..60bb64dfb --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.h @@ -0,0 +1,43 @@ +/* + * rumorsettings.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 + * + */ +#ifndef RUMORSETTINGS_H +#define RUMORSETTINGS_H + +#include "abstractsettings.h" + +namespace Ui { +class RumorSettings; +} + +class RumorSettings : public AbstractSettings +{ + Q_OBJECT + +public: + explicit RumorSettings(QWidget *parent = nullptr); + ~RumorSettings(); + + void initialize(const CMap & map) override; + void update(CMap & map) override; + +private slots: + void on_message_textChanged(); + + void on_add_clicked(); + + void on_remove_clicked(); + + void on_rumors_itemSelectionChanged(); + +private: + Ui::RumorSettings *ui; +}; + +#endif // RUMORSETTINGS_H diff --git a/mapeditor/mapsettings/rumorsettings.ui b/mapeditor/mapsettings/rumorsettings.ui new file mode 100644 index 000000000..67a66e670 --- /dev/null +++ b/mapeditor/mapsettings/rumorsettings.ui @@ -0,0 +1,105 @@ + + + RumorSettings + + + + 0 + 0 + 538 + 470 + + + + Form + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 10 + + + + + Tavern rumors + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 90 + 0 + + + + Add + + + + + + + + 90 + 0 + + + + + 0 + 16777215 + + + + Remove + + + + + + + + + QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed + + + QAbstractItemView::SelectRows + + + + + + + + + + + diff --git a/mapeditor/mapsettings/victoryconditions.ui b/mapeditor/mapsettings/victoryconditions.ui index 98cb1dd0a..2bd91bb40 100644 --- a/mapeditor/mapsettings/victoryconditions.ui +++ b/mapeditor/mapsettings/victoryconditions.ui @@ -14,6 +14,18 @@ Form + + 0 + + + 0 + + + 0 + + + 0 + From d6ff553cacde7ef8575ebbbaa352e7f8f3140a3e Mon Sep 17 00:00:00 2001 From: Nordsoft91 Date: Sat, 9 Sep 2023 02:14:27 +0200 Subject: [PATCH 0358/1248] Fix typo --- launcher/modManager/cmodlistview_moc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 4d95d906d..3a3c6527b 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -250,7 +250,7 @@ QString CModListView::genModInfoText(CModEntry & mod) result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg(tr("Latest version"))); if(mod.getValue("localSizeBytes").isValid()) - result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("downloadSize"))); + result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("localSizeBytes").toDouble()), lineTemplate.arg(tr("Size"))); if((mod.isAvailable() || mod.isUpdateable()) && mod.getValue("downloadSize").isValid()) result += replaceIfNotEmpty(CModEntry::sizeToString(mbToBytes(mod.getValue("downloadSize").toDouble())), lineTemplate.arg(tr("Download size"))); From 0db51e0517e988a47962b319c2aa8d13f8cfa2d2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 9 Sep 2023 00:20:30 +0200 Subject: [PATCH 0359/1248] Minor fixes for lobby --- launcher/lobby/lobby_moc.cpp | 14 +++++++++++++- launcher/lobby/lobby_moc.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index ad0d5dfa2..3498a99fc 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -189,10 +189,14 @@ void Lobby::serverCommand(const ServerCommand & command) try case CLIENTMODS: { protocolAssert(args.size() >= 1); + auto & clientModsMap = clientsModsMap[args[0]]; amount = args[1].toInt(); protocolAssert(amount * 2 == (args.size() - 2)); tagPoint = 2; + for(int i = 0; i < amount; ++i, tagPoint += 2) + clientModsMap[args[tagPoint]] = args[tagPoint + 1]; + break; } @@ -342,6 +346,7 @@ void Lobby::onDisconnected() authentificationStatus = AuthStatus::AUTH_NONE; session = ""; ui->chatWidget->setSession(session); + ui->chatWidget->setChannel("global"); ui->stackedWidget->setCurrentWidget(ui->sessionsPage); ui->connectButton->setChecked(false); ui->serverEdit->setEnabled(true); @@ -517,7 +522,14 @@ void Lobby::on_kickButton_clicked() void Lobby::on_buttonResolve_clicked() { QStringList toEnableList, toDisableList; - for(auto * item : ui->modsList->selectedItems()) + auto items = ui->modsList->selectedItems(); + if(items.empty()) + { + for(int i = 0; i < ui->modsList->count(); ++i) + items.push_back(ui->modsList->item(i)); + } + + for(auto * item : items) { auto modName = item->data(ModResolutionRoles::ModNameRole); if(modName.isNull()) diff --git a/launcher/lobby/lobby_moc.h b/launcher/lobby/lobby_moc.h index e4241d39c..23a8ddac1 100644 --- a/launcher/lobby/lobby_moc.h +++ b/launcher/lobby/lobby_moc.h @@ -74,6 +74,7 @@ private: QString username; QStringList gameArgs; QMap hostModsMap; + QMap> clientsModsMap; enum AuthStatus { From 18c44fc006599f885ae2740d7465d84344dee9cb Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 9 Sep 2023 02:16:00 +0200 Subject: [PATCH 0360/1248] Cache archive files --- launcher/modManager/cmodmanager.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 23a8fe061..5766287ad 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -22,15 +22,15 @@ namespace { -QString detectModArchive(QString path, QString modName) +QString detectModArchive(QString path, QString modName, std::vector & filesToExtract) { - auto files = ZipArchive::listFiles(qstringToPath(path)); + filesToExtract = ZipArchive::listFiles(qstringToPath(path)); QString modDirName; for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root { - for(auto file : files) + for(auto file : filesToExtract) { QString filename = QString::fromUtf8(file.c_str()); modDirName = filename.section('/', 0, folderLevel); @@ -275,11 +275,12 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) if(localMods.contains(modname)) return addError(modname, "Mod with such name is already installed"); - QString modDirName = ::detectModArchive(archivePath, modname); + std::vector filesToExtract; + QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract); if(!modDirName.size()) return addError(modname, "Mod archive is invalid or corrupted"); - if(!ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir))) + if(!ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract)) { removeModDir(destDir + modDirName); return addError(modname, "Failed to extract mod data"); From 52cbb613aec8ae69021e8b03eacb7425d6fe53e0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 9 Sep 2023 11:03:19 +0200 Subject: [PATCH 0361/1248] Use pragma once --- mapeditor/mapsettings/abstractsettings.h | 5 +---- mapeditor/mapsettings/eventsettings.h | 4 +--- mapeditor/mapsettings/generalsettings.h | 5 +---- mapeditor/mapsettings/loseconditions.h | 4 +--- mapeditor/mapsettings/modsettings.h | 5 +---- mapeditor/mapsettings/rumorsettings.h | 5 +---- mapeditor/mapsettings/timedevent.h | 5 +---- mapeditor/mapsettings/victoryconditions.h | 5 +---- 8 files changed, 8 insertions(+), 30 deletions(-) diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 5985613eb..827628778 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef ABSTRACTSETTINGS_H -#define ABSTRACTSETTINGS_H +#pragma once #include #include "../../lib/mapping/CMap.h" @@ -66,5 +65,3 @@ public: signals: }; - -#endif // ABSTRACTSETTINGS_H diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h index d37c1ba79..0560ec534 100644 --- a/mapeditor/mapsettings/eventsettings.h +++ b/mapeditor/mapsettings/eventsettings.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef EVENTSETTINGS_H -#define EVENTSETTINGS_H +#pragma once #include "abstractsettings.h" @@ -38,4 +37,3 @@ private: Ui::EventSettings *ui; }; -#endif // EVENTSETTINGS_H diff --git a/mapeditor/mapsettings/generalsettings.h b/mapeditor/mapsettings/generalsettings.h index 025cd1e50..8360dee66 100644 --- a/mapeditor/mapsettings/generalsettings.h +++ b/mapeditor/mapsettings/generalsettings.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef GENERALSETTINGS_H -#define GENERALSETTINGS_H +#pragma once #include "abstractsettings.h" @@ -33,5 +32,3 @@ private slots: private: Ui::GeneralSettings *ui; }; - -#endif // GENERALSETTINGS_H diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h index 778e8c146..b091df7f0 100644 --- a/mapeditor/mapsettings/loseconditions.h +++ b/mapeditor/mapsettings/loseconditions.h @@ -7,9 +7,8 @@ * Full text of license available in license.txt file, in main folder * */ +#pragma once -#ifndef LOSECONDITIONS_H -#define LOSECONDITIONS_H #include "abstractsettings.h" namespace Ui { @@ -39,4 +38,3 @@ private: QLineEdit * loseValueWidget = nullptr; }; -#endif // LOSECONDITIONS_H diff --git a/mapeditor/mapsettings/modsettings.h b/mapeditor/mapsettings/modsettings.h index 6e5acd5b9..8c30b88b0 100644 --- a/mapeditor/mapsettings/modsettings.h +++ b/mapeditor/mapsettings/modsettings.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef MODSETTINGS_H -#define MODSETTINGS_H +#pragma once #include "abstractsettings.h" @@ -41,5 +40,3 @@ private: Ui::ModSettings *ui; const CMap * mapPointer = nullptr; }; - -#endif // MODSETTINGS_H diff --git a/mapeditor/mapsettings/rumorsettings.h b/mapeditor/mapsettings/rumorsettings.h index 60bb64dfb..9a980f288 100644 --- a/mapeditor/mapsettings/rumorsettings.h +++ b/mapeditor/mapsettings/rumorsettings.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef RUMORSETTINGS_H -#define RUMORSETTINGS_H +#pragma once #include "abstractsettings.h" @@ -39,5 +38,3 @@ private slots: private: Ui::RumorSettings *ui; }; - -#endif // RUMORSETTINGS_H diff --git a/mapeditor/mapsettings/timedevent.h b/mapeditor/mapsettings/timedevent.h index b72480837..5aab63fc2 100644 --- a/mapeditor/mapsettings/timedevent.h +++ b/mapeditor/mapsettings/timedevent.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef TIMEDEVENT_H -#define TIMEDEVENT_H +#pragma once #include @@ -36,5 +35,3 @@ private: Ui::TimedEvent *ui; QListWidgetItem * target; }; - -#endif // TIMEDEVENT_H diff --git a/mapeditor/mapsettings/victoryconditions.h b/mapeditor/mapsettings/victoryconditions.h index f47b6b293..deba8f7a0 100644 --- a/mapeditor/mapsettings/victoryconditions.h +++ b/mapeditor/mapsettings/victoryconditions.h @@ -7,8 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ -#ifndef VICTORYCONDITIONS_H -#define VICTORYCONDITIONS_H +#pragma once #include "abstractsettings.h" @@ -38,5 +37,3 @@ private: QComboBox * victorySelectWidget = nullptr; QLineEdit * victoryValueWidget = nullptr; }; - -#endif // VICTORYCONDITIONS_H From d011e1ff0dc526986d5c1ab1bf3fe598f2ff53be Mon Sep 17 00:00:00 2001 From: Nordsoft91 Date: Sat, 9 Sep 2023 15:24:52 +0200 Subject: [PATCH 0362/1248] Update mod.json schema --- config/schemas/mod.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 5415d0275..48b2f0680 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -22,10 +22,6 @@ "type" : "string", "description" : "Author of the mod. Can be nickname, real name or name of team" }, - "downloadSize": { - "type" : "number", - "description" : "Approximate size of mod, compressed by zip algorithm, in Mb" - }, "changelog" : { "type" : "object", "description" : "List of changes/new features in each version", @@ -66,6 +62,10 @@ "type" : "string", "description" : "Author of the mod. Can be nickname, real name or name of team" }, + "downloadSize": { + "type" : "number", + "description" : "Approximate size of mod, compressed by zip algorithm, in Mb" + }, "contact" : { "type" : "string", "description" : "Home page of mod or link to forum thread" From 6e3817f18c28042c3d3aa428c1a6128027b99bca Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 01:54:55 +0200 Subject: [PATCH 0363/1248] Events serialization --- lib/mapping/CMap.cpp | 30 +++++++++++++++++++++++++++++- lib/mapping/CMapDefines.h | 9 +++++++-- lib/mapping/MapFormatH3M.cpp | 1 - lib/mapping/MapFormatJson.cpp | 12 +++++++++++- lib/mapping/MapFormatJson.h | 2 ++ 5 files changed, 49 insertions(+), 5 deletions(-) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 9805821a2..2d1de817f 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -56,9 +56,37 @@ bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const return firstOccurence <= other.firstOccurence; } -CCastleEvent::CCastleEvent() : town(nullptr) +void CMapEvent::serializeJson(JsonSerializeFormat & handler) { + handler.serializeString("name", name); + handler.serializeString("message", message); + handler.serializeInt("players", players); + handler.serializeInt("humanAffected", humanAffected); + handler.serializeInt("computerAffected", computerAffected); + handler.serializeInt("firstOccurence", firstOccurence); + handler.serializeInt("nextOccurence", nextOccurence); + resources.serializeJson(handler, "resources"); +} +void CCastleEvent::serializeJson(JsonSerializeFormat & handler) +{ + CMapEvent::serializeJson(handler); + { + std::vector temp(buildings.begin(), buildings.end()); + auto a = handler.enterArray("buildings"); + a.syncSize(temp); + for(int i = 0; i < temp.size(); ++i) + { + a.serializeInt(i, temp[i]); + buildings.insert(temp[i]); + } + } + { + auto a = handler.enterArray("creatures"); + a.syncSize(creatures); + for(int i = 0; i < creatures.size(); ++i) + a.serializeInt(i, creatures[i]); + } } TerrainTile::TerrainTile(): diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 14c7512b9..c0bce891a 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -19,6 +19,7 @@ class RiverType; class RoadType; class CGObjectInstance; class CGTownInstance; +class JsonSerializeFormat; /// The map event is an event which e.g. gives or takes resources of a specific /// amount to/from players and can appear regularly or once a time. @@ -26,6 +27,7 @@ class DLL_LINKAGE CMapEvent { public: CMapEvent(); + virtual ~CMapEvent() = default; bool earlierThan(const CMapEvent & other) const; bool earlierThanOrEqual(const CMapEvent & other) const; @@ -51,17 +53,18 @@ public: h & firstOccurence; h & nextOccurence; } + + virtual void serializeJson(JsonSerializeFormat & handler); }; /// The castle event builds/adds buildings/creatures for a specific town. class DLL_LINKAGE CCastleEvent: public CMapEvent { public: - CCastleEvent(); + CCastleEvent() = default; std::set buildings; std::vector creatures; - CGTownInstance * town; template void serialize(Handler & h, const int version) @@ -70,6 +73,8 @@ public: h & buildings; h & creatures; } + + void serializeJson(JsonSerializeFormat & handler) override; }; /// The terrain tile describes the terrain type and the visual representation of the terrain. diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index f1a9fc063..0b241c68c 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2097,7 +2097,6 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt for(int eventID = 0; eventID < eventsCount; ++eventID) { CCastleEvent event; - event.town = object; event.name = readBasicString(); event.message = readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 6465b6262..c8aed2682 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -342,7 +342,7 @@ namespace TerrainDetail ///CMapFormatJson const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 1; +const int CMapFormatJson::VERSION_MINOR = 2; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; @@ -775,6 +775,14 @@ void CMapFormatJson::serializeRumors(JsonSerializeFormat & handler) rumors.serializeStruct(map->rumors); } +void CMapFormatJson::serializeTimedEvents(JsonSerializeFormat & handler) +{ + auto events = handler.enterArray("events"); + std::vector temp(map->events.begin(), map->events.end()); + events.serializeStruct(temp); + map->events.assign(temp.begin(), temp.end()); +} + void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) { //todo:serializePredefinedHeroes @@ -816,6 +824,8 @@ void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) { serializeRumors(handler); + + serializeTimedEvents(handler); serializePredefinedHeroes(handler); diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 143810a25..d23d06be0 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -109,6 +109,8 @@ protected: void serializePredefinedHeroes(JsonSerializeFormat & handler); void serializeRumors(JsonSerializeFormat & handler); + + void serializeTimedEvents(JsonSerializeFormat & handler); ///common part of map attributes saving/loading void serializeOptions(JsonSerializeFormat & handler); From 2cb28178aecc8fc10d5b2a2bb0d975d0e786eb28 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 10 Sep 2023 20:52:35 +0200 Subject: [PATCH 0364/1248] code review --- client/adventureMap/AdventureMapInterface.cpp | 2 +- client/render/Canvas.cpp | 11 +++++++-- client/render/Canvas.h | 7 ++++-- client/renderSDL/SDL_Extensions.cpp | 24 ++++++++++--------- client/renderSDL/SDL_Extensions.h | 3 ++- 5 files changed, 30 insertions(+), 17 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index d77a939b6..188720cc8 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -175,7 +175,7 @@ void AdventureMapInterface::dim(Canvas & to) Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); if(backgroundDimLevel > 0) - to.drawColor(targetRect, colorToFill, false); + to.drawColorBlended(targetRect, colorToFill); return; } } diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index f087df4f8..908d5c5a6 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -166,11 +166,18 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG } } -void Canvas::drawColor(const Rect & target, const ColorRGBA & color, const bool replace) +void Canvas::drawColor(const Rect & target, const ColorRGBA & color) { Rect realTarget = target + renderArea.topLeft(); - CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color), replace); + CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); +} + +void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); } SDL_Surface * Canvas::getInternalSurface() diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 82ea82019..591e8fc07 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -93,8 +93,11 @@ public: /// renders multiple lines of text with specified parameters void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); - /// fills selected area with solid color, allows replacing or overdrawing - void drawColor(const Rect & target, const ColorRGBA & color, const bool replace = true); + /// fills selected area with solid color + void drawColor(const Rect & target, const ColorRGBA & color); + + /// fills selected area with blended color + void drawColorBlended(const Rect & target, const ColorRGBA & color); /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 459f3f200..f1717644e 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -799,21 +799,23 @@ void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) fillRect(dst, allSurface, color); } -void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color, const bool replace) +void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) +{ + SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); + + uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); + SDL_FillRect(dst, &newRect, sdlColor); +} + +void CSDL_Ext::fillRectBlended( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) { SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - // replacing or overdrawing (relevant for transparency) - if(replace) - SDL_FillRect(dst, &newRect, sdlColor); - else - { - SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); - SDL_FillRect(tmp, NULL, sdlColor); - SDL_BlitSurface(tmp, NULL, dst, &newRect); - SDL_FreeSurface(tmp); - } + SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); + SDL_FillRect(tmp, NULL, sdlColor); + SDL_BlitSurface(tmp, NULL, dst, &newRect); + SDL_FreeSurface(tmp); } STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index 630bd09e0..712ff5c79 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -58,7 +58,8 @@ using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); void fillSurface(SDL_Surface * dst, const SDL_Color & color); - void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color, const bool replace = true); + void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); + void fillRectBlended(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); void updateRect(SDL_Surface * surface, const Rect & rect); From 6a5e71aa63cdfd02efdcfd4df27a10e1f285a13b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kriszti=C3=A1n=20Szab=C3=B3?= Date: Mon, 11 Sep 2023 09:32:14 +0200 Subject: [PATCH 0365/1248] Award 500 experience for towns conquered during a battle. --- server/battles/BattleResultProcessor.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 627c2bb6c..beaeea918 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -241,6 +241,11 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) battleResult->exp[1] += 500; if(heroDefender) battleResult->exp[0] += 500; + + // Give 500 exp to winner if a town was conquered during the battle + const auto * defendedTown = battle.battleGetDefendedTown(); + if (defendedTown && battleResult->winner == BattleSide::ATTACKER) + battleResult->exp[BattleSide::ATTACKER] += 500; } if(heroAttacker) From fd3217c87520da29172efe25d58fb93ee4088f99 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 11 Sep 2023 13:20:16 +0300 Subject: [PATCH 0366/1248] Fix few more missing BattleID's --- lib/CStack.cpp | 1 + lib/spells/effects/Moat.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 9343340cf..ed18eb3a2 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -395,6 +395,7 @@ void CStack::spendMana(ServerCallback * server, const int spellCost) const logGlobal->warn("Unexpected spell cost %d for creature", spellCost); BattleSetStackProperty ssp; + ssp.battleID = battle->battleID; ssp.stackID = unitId(); ssp.which = BattleSetStackProperty::CASTS; ssp.val = -spellCost; diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index bb80b9d08..0ef725589 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -116,6 +116,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge for(auto & b : converted) { GiveBonus gb(GiveBonus::ETarget::BATTLE); + gb.id = m->battle()->getBattle()->getBattleID().getNum(); gb.bonus = b; server->apply(&gb); } From fc1ce85a7272a9f9274bbeacec806fcf07e429d8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 11 Sep 2023 12:57:10 +0200 Subject: [PATCH 0367/1248] basic voice support --- client/mainmenu/CPrologEpilogVideo.cpp | 3 +-- lib/campaign/CampaignHandler.cpp | 11 +++++++---- lib/campaign/CampaignHandler.h | 2 +- lib/campaign/CampaignScenarioPrologEpilog.h | 4 +++- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index b8b7ab49e..05a8248ba 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -29,8 +29,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); - // MPTODO: Custom campaign crashing on this? -// voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo)); + voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText); text->scrollTextTo(-100); diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index ab2cc63c5..209a9273b 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -168,6 +168,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) { ret.prologVideo = VideoPath::fromJson(identifier["video"]); ret.prologMusic = AudioPath::fromJson(identifier["music"]); + ret.prologVoice = AudioPath::fromJson(identifier["voice"]); ret.prologText = identifier["text"].String(); } return ret; @@ -403,8 +404,10 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader ret.hasPrologEpilog = reader.readUInt8(); if(ret.hasPrologEpilog) { - ret.prologVideo = CampaignHandler::prologVideoName(reader.readUInt8()); + ui8 index = reader.readUInt8(); + ret.prologVideo = CampaignHandler::prologVideoName(index); ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); + ret.prologVoice = CampaignHandler::prologVoiceName(index); ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier); } return ret; @@ -605,13 +608,13 @@ AudioPath CampaignHandler::prologMusicName(ui8 index) return AudioPath::builtinTODO(VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index)))); } -std::string CampaignHandler::prologVoiceName(ui8 index) +AudioPath CampaignHandler::prologVoiceName(ui8 index) { JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); auto audio = config["voice"].Vector(); if(index < audio.size()) - return audio[index].String(); - return ""; + return AudioPath::fromJson(audio[index]); + return AudioPath(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index 7a6d0fd1f..d7d0c4a8a 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -35,7 +35,7 @@ class DLL_LINKAGE CampaignHandler static VideoPath prologVideoName(ui8 index); static AudioPath prologMusicName(ui8 index); - static std::string prologVoiceName(ui8 index); + static AudioPath prologVoiceName(ui8 index); public: static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index 00b321188..ab67cc584 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -16,8 +16,9 @@ VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE CampaignScenarioPrologEpilog { bool hasPrologEpilog = false; - VideoPath prologVideo; // from CmpMovie.txt + VideoPath prologVideo; AudioPath prologMusic; // from CmpMusic.txt + AudioPath prologVoice; std::string prologText; template void serialize(Handler &h, const int formatVersion) @@ -25,6 +26,7 @@ struct DLL_LINKAGE CampaignScenarioPrologEpilog h & hasPrologEpilog; h & prologVideo; h & prologMusic; + h & prologVoice; h & prologText; } }; From 00f07f93d1cb78926be86fb8a89a51baaaec04cc Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:04:44 +0200 Subject: [PATCH 0368/1248] wait while audio playing; no audio on custom campaigns --- client/CMusicHandler.cpp | 9 +++++++++ client/CMusicHandler.h | 2 ++ client/mainmenu/CPrologEpilogVideo.cpp | 2 +- lib/campaign/CampaignHandler.cpp | 5 ++++- 4 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 273404229..dec68ceb6 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -190,6 +190,8 @@ int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) initCallback(channel); else initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + + channelPlaying[channel] = true; } else channel = -1; @@ -209,6 +211,11 @@ void CSoundHandler::stopSound(int handler) Mix_HaltChannel(handler); } +bool CSoundHandler::isSoundPlaying(int handler) +{ + return initialized && handler != -1 && channelPlaying[handler]; +} + // Sets the sound volume, from 0 (mute) to 100 void CSoundHandler::setVolume(ui32 percent) { @@ -252,6 +259,8 @@ void CSoundHandler::setCallback(int channel, std::function function) void CSoundHandler::soundFinishedCallback(int channel) { + channelPlaying[channel] = false; + boost::mutex::scoped_lock lockGuard(mutexCallbacks); if (callbacks.count(channel) == 0) diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 56e1e3d7e..f8b78a3aa 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -60,6 +60,7 @@ private: std::map ambientChannels; std::map channelVolumes; + std::map channelPlaying; void initCallback(int channel, const std::function & function); void initCallback(int channel); @@ -78,6 +79,7 @@ public: int playSound(const AudioPath & sound, int repeats=0, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); + bool isSoundPlaying(int handler); void setCallback(int channel, std::function function); void soundFinishedCallback(int channel); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 05a8248ba..2cf700c9e 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -50,7 +50,7 @@ void CPrologEpilogVideo::show(Canvas & to) else text->showAll(to); // blit text over video, if needed - if(text->textSize.y + 100 < positionCounter / 5) + if(text->textSize.y + 100 < positionCounter / 5 && !CCS->soundh->isSoundPlaying(voiceSoundHandle)) clickPressed(GH.getCursorPosition()); } diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 209a9273b..5ffe04cd6 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -404,10 +404,13 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader ret.hasPrologEpilog = reader.readUInt8(); if(ret.hasPrologEpilog) { + std::string originalCampaigns[] = { "DATA/GOOD1", "DATA/EVIL1", "DATA/GOOD2", "DATA/NEUTRAL1", "DATA/EVIL2", "DATA/GOOD3", "DATA/SECRET1", "DATA/AB", "DATA/BLOOD", "DATA/SLAYER", "DATA/FESTIVAL", "DATA/FIRE", "DATA/FOOL", "DATA/GEM", "DATA/GELU", "DATA/CRAG", "DATA/SANDRO", "DATA/YOG", "DATA/FINAL", "DATA/SECRET" }; + bool isOriginalCampaign = std::find(std::begin(originalCampaigns), std::end(originalCampaigns), header.getFilename()) != std::end(originalCampaigns); + ui8 index = reader.readUInt8(); ret.prologVideo = CampaignHandler::prologVideoName(index); ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); - ret.prologVoice = CampaignHandler::prologVoiceName(index); + ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier); } return ret; From fa19ed4e7ca0f7ffc215a7bd507c58e35b5936d4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:11:16 +0200 Subject: [PATCH 0369/1248] format --- client/mainmenu/CPrologEpilogVideo.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 2cf700c9e..d1022465c 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -29,7 +29,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); - voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); + voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText); text->scrollTextTo(-100); From 994da3fcf284308e17521c117ce658125b51af28 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 11 Sep 2023 14:44:07 +0200 Subject: [PATCH 0370/1248] code review --- client/CMusicHandler.cpp | 9 --------- client/CMusicHandler.h | 2 -- client/mainmenu/CPrologEpilogVideo.cpp | 7 ++++++- client/mainmenu/CPrologEpilogVideo.h | 2 ++ lib/campaign/CampaignHandler.cpp | 3 +-- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index dec68ceb6..273404229 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -190,8 +190,6 @@ int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) initCallback(channel); else initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - - channelPlaying[channel] = true; } else channel = -1; @@ -211,11 +209,6 @@ void CSoundHandler::stopSound(int handler) Mix_HaltChannel(handler); } -bool CSoundHandler::isSoundPlaying(int handler) -{ - return initialized && handler != -1 && channelPlaying[handler]; -} - // Sets the sound volume, from 0 (mute) to 100 void CSoundHandler::setVolume(ui32 percent) { @@ -259,8 +252,6 @@ void CSoundHandler::setCallback(int channel, std::function function) void CSoundHandler::soundFinishedCallback(int channel) { - channelPlaying[channel] = false; - boost::mutex::scoped_lock lockGuard(mutexCallbacks); if (callbacks.count(channel) == 0) diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index f8b78a3aa..56e1e3d7e 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -60,7 +60,6 @@ private: std::map ambientChannels; std::map channelVolumes; - std::map channelPlaying; void initCallback(int channel, const std::function & function); void initCallback(int channel); @@ -79,7 +78,6 @@ public: int playSound(const AudioPath & sound, int repeats=0, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); - bool isSoundPlaying(int handler); void setCallback(int channel, std::function function); void soundFinishedCallback(int channel); diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index d1022465c..b37383c03 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -30,6 +30,11 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); + auto onVoiceStop = [this]() + { + voiceStopped = true; + }; + CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop); text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText); text->scrollTextTo(-100); @@ -50,7 +55,7 @@ void CPrologEpilogVideo::show(Canvas & to) else text->showAll(to); // blit text over video, if needed - if(text->textSize.y + 100 < positionCounter / 5 && !CCS->soundh->isSoundPlaying(voiceSoundHandle)) + if(text->textSize.y + 100 < positionCounter / 5 && voiceStopped) clickPressed(GH.getCursorPosition()); } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index ec6de9055..756c2f5e7 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -23,6 +23,8 @@ class CPrologEpilogVideo : public CWindowObject std::shared_ptr text; + bool voiceStopped = false; + public: CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback); diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 5ffe04cd6..63fae7c05 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -404,8 +404,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader ret.hasPrologEpilog = reader.readUInt8(); if(ret.hasPrologEpilog) { - std::string originalCampaigns[] = { "DATA/GOOD1", "DATA/EVIL1", "DATA/GOOD2", "DATA/NEUTRAL1", "DATA/EVIL2", "DATA/GOOD3", "DATA/SECRET1", "DATA/AB", "DATA/BLOOD", "DATA/SLAYER", "DATA/FESTIVAL", "DATA/FIRE", "DATA/FOOL", "DATA/GEM", "DATA/GELU", "DATA/CRAG", "DATA/SANDRO", "DATA/YOG", "DATA/FINAL", "DATA/SECRET" }; - bool isOriginalCampaign = std::find(std::begin(originalCampaigns), std::end(originalCampaigns), header.getFilename()) != std::end(originalCampaigns); + bool isOriginalCampaign = boost::starts_with(header.getFilename(), "DATA/"); ui8 index = reader.readUInt8(); ret.prologVideo = CampaignHandler::prologVideoName(index); From cb3cc84c619cbb66c162922f9b53341c54eb14d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kriszti=C3=A1n=20Szab=C3=B3?= Date: Mon, 11 Sep 2023 19:51:23 +0200 Subject: [PATCH 0371/1248] Give bonus experience for capturing a town even if the enemy hero escaped. --- server/battles/BattleResultProcessor.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index beaeea918..c319fbb02 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -241,13 +241,13 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) battleResult->exp[1] += 500; if(heroDefender) battleResult->exp[0] += 500; - - // Give 500 exp to winner if a town was conquered during the battle - const auto * defendedTown = battle.battleGetDefendedTown(); - if (defendedTown && battleResult->winner == BattleSide::ATTACKER) - battleResult->exp[BattleSide::ATTACKER] += 500; } + // Give 500 exp to winner if a town was conquered during the battle + const auto * defendedTown = battle.battleGetDefendedTown(); + if (defendedTown && battleResult->winner == BattleSide::ATTACKER) + battleResult->exp[BattleSide::ATTACKER] += 500; + if(heroAttacker) battleResult->exp[0] = heroAttacker->calculateXp(battleResult->exp[0]);//scholar skill if(heroDefender) From e912a2db389132c09e15ef72d2e65be8f07d1767 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 16:56:41 +0200 Subject: [PATCH 0372/1248] Better army widget --- mapeditor/inspector/armywidget.cpp | 5 +- mapeditor/inspector/armywidget.h | 2 +- mapeditor/inspector/armywidget.ui | 347 ++++++++++++----------------- 3 files changed, 145 insertions(+), 209 deletions(-) diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 3878fea73..0cfd68d9a 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -31,7 +31,6 @@ ArmyWidget::ArmyWidget(CArmedInstance & a, QWidget *parent) : for(int i = 0; i < TOTAL_SLOTS; ++i) { - uiCounts[i]->setText("1"); uiSlots[i]->addItem(""); uiSlots[i]->setItemData(0, -1); @@ -64,7 +63,7 @@ void ArmyWidget::obtainData() { auto * creature = army.getCreature(SlotID(i)); uiSlots[i]->setCurrentIndex(searchItemIndex(i, creature->getId())); - uiCounts[i]->setText(QString::number(army.getStackCount(SlotID(i)))); + uiCounts[i]->setValue(army.getStackCount(SlotID(i))); } } @@ -88,7 +87,7 @@ bool ArmyWidget::commitChanges() else { isArmed = true; - int amount = uiCounts[i]->text().toInt(); + int amount = uiCounts[i]->value(); if(amount) { army.setCreature(SlotID(i), creId, amount); diff --git a/mapeditor/inspector/armywidget.h b/mapeditor/inspector/armywidget.h index e26953057..c4eaabe32 100644 --- a/mapeditor/inspector/armywidget.h +++ b/mapeditor/inspector/armywidget.h @@ -35,7 +35,7 @@ private: Ui::ArmyWidget *ui; CArmedInstance & army; - std::array uiCounts; + std::array uiCounts; std::array uiSlots; }; diff --git a/mapeditor/inspector/armywidget.ui b/mapeditor/inspector/armywidget.ui index a808f0c3a..a6f141752 100644 --- a/mapeditor/inspector/armywidget.ui +++ b/mapeditor/inspector/armywidget.ui @@ -20,8 +20,8 @@ Army settings - - + + 0 @@ -40,6 +40,16 @@ + + + + + 0 + 0 + + + + @@ -50,164 +60,6 @@ - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - Wide formation - - - - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - @@ -218,33 +70,8 @@ - - - - - 0 - 0 - - - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - + + 0 @@ -253,29 +80,14 @@ - - + + - + 0 0 - - - 30 - 0 - - - - - 50 - 16777215 - - - - Qt::ImhDigitsOnly - @@ -291,6 +103,131 @@ + + + + + 0 + 0 + + + + Wide formation + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + + + + + false + + + QAbstractSpinBox::PlusMinus + + + true + + + 9999 + + + From 5136305455bed6e214159482defd27644a8a9a38 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 16:57:41 +0200 Subject: [PATCH 0373/1248] Fix hero type for random hero --- mapeditor/inspector/inspector.cpp | 17 ++++++++++------- mapeditor/inspector/inspector.h | 3 ++- mapeditor/mapcontroller.cpp | 3 ++- mapeditor/mapview.cpp | 4 +++- mapeditor/validator.cpp | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index f4fcf0f0b..a655b96b3 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -135,12 +135,14 @@ void Initializer::initialize(CGHeroInstance * o) } } - if(!o->type) - o->type = VLC->heroh->objects.at(o->subID); - - o->gender = o->type->gender; - o->portrait = o->type->imageIndex; - o->randomizeArmy(o->type->heroClass->faction); + if(o->type) + { + // o->type = VLC->heroh->objects.at(o->subID); + + o->gender = o->type->gender; + o->portrait = o->type->imageIndex; + o->randomizeArmy(o->type->heroClass->faction); + } } void Initializer::initialize(CGTownInstance * o) @@ -250,7 +252,7 @@ void Inspector::updateProperties(CGHeroInstance * o) addProperty("Owner", o->tempOwner, o->ID == Obj::PRISON); //field is not editable for prison addProperty("Experience", o->exp, false); - addProperty("Hero class", o->type->heroClass->getNameTranslated(), true); + addProperty("Hero class", o->type ? o->type->heroClass->getNameTranslated() : "", true); { //Sex auto * delegate = new InspectorDelegate; @@ -261,6 +263,7 @@ void Inspector::updateProperties(CGHeroInstance * o) addProperty("Biography", o->biographyCustom, new MessageDelegate, false); addProperty("Portrait", o->portrait, false); + if(o->type) { //Hero type auto * delegate = new InspectorDelegate; for(int i = 0; i < VLC->heroh->objects.size(); ++i) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index f440a3374..4a1526518 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -126,7 +126,8 @@ protected: table->setRowCount(row + 1); table->setItem(row, 0, itemKey); table->setItem(row, 1, itemValue); - table->setItemDelegateForRow(row, delegate); + if(delegate) + table->setItemDelegateForRow(row, delegate); ++row; } } diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 0f1703926..f0eea1725 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -145,7 +145,8 @@ void MapController::repairMap() nih->subID = 0; } - nih->type = type; + if(obj->ID != Obj::RANDOM_HERO) + nih->type = type; if(nih->ID == Obj::HERO) //not prison nih->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 87775258a..715dea10b 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -397,6 +397,7 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) bool tab = false; if(sc->selectionObjectsView.selectionMode == SelectionObjectsLayer::MOVEMENT) { + tab = sc->selectionObjectsView.shift.isNull(); controller->commitObjectShift(sc->level); } else @@ -405,7 +406,6 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) sc->selectionObjectsView.shift = QPoint(0, 0); sc->selectionObjectsView.draw(); tab = true; - //check if we have only one object } auto selection = sc->selectionObjectsView.getSelection(); if(selection.size() == 1) @@ -463,7 +463,9 @@ void MapView::dropEvent(QDropEvent * event) QString errorMsg; if(controller->canPlaceObject(sc->level, sc->selectionObjectsView.newObject, errorMsg)) { + auto * obj = sc->selectionObjectsView.newObject; controller->commitObjectCreate(sc->level); + emit openObjectProperties(obj, false); } else { diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 9e0bb8985..e5cabf608 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -131,7 +131,7 @@ std::list Validator::validate(const CMap * map) if(!allHeroesOnMap.insert(ins->type).second) issues.emplace_back(QString(tr("Hero %1 has duplicate on map")).arg(ins->type->getNameTranslated().c_str()), false); } - else + else if(ins->ID != Obj::RANDOM_HERO) issues.emplace_back(QString(tr("Hero %1 has an empty type and must be removed")).arg(ins->instanceName.c_str()), true); } From d07fbc2204fb7d9a39049eae1f7bb492d4d2a6e6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 18:10:50 +0200 Subject: [PATCH 0374/1248] Improve bool fields in inspector --- mapeditor/inspector/inspector.cpp | 44 +++++++++++-------------------- mapeditor/inspector/inspector.h | 7 +++-- mapeditor/mainwindow.cpp | 5 +++- mapeditor/mainwindow.ui | 3 +++ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index a655b96b3..83880cbce 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -47,14 +47,6 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default //INIT_OBJ_TYPE(CGSeerHut); } -bool stringToBool(const QString & s) -{ - if(s == "TRUE") - return true; - //if(s == "FALSE") - return false; -} - void Initializer::initialize(CArmedInstance * o) { if(!o) return; @@ -236,7 +228,7 @@ void Inspector::updateProperties(CGGarrison * o) if(!o) return; addProperty("Owner", o->tempOwner, false); - addProperty("Removable units", o->removableUnits, InspectorDelegate::boolDelegate(), false); + addProperty("Removable units", o->removableUnits, false); } void Inspector::updateProperties(CGShipyard * o) @@ -345,8 +337,8 @@ void Inspector::updateProperties(CGCreature * o) delegate->options << "COMPLIANT" << "FRIENDLY" << "AGRESSIVE" << "HOSTILE" << "SAVAGE"; addProperty("Character", (CGCreature::Character)o->character, delegate, false); } - addProperty("Never flees", o->neverFlees, InspectorDelegate::boolDelegate(), false); - addProperty("Not growing", o->notGrowingTeam, InspectorDelegate::boolDelegate(), false); + addProperty("Never flees", o->neverFlees, false); + addProperty("Not growing", o->notGrowingTeam, false); addProperty("Artifact reward", o->gainedArtifact); //TODO: implement in setProperty addProperty("Army", PropertyEditorPlaceholder(), true); addProperty("Amount", o->stacks[SlotID(0)]->count, false); @@ -367,9 +359,9 @@ void Inspector::updateProperties(CGEvent * o) { if(!o) return; - addProperty("Remove after", o->removeAfterVisit, InspectorDelegate::boolDelegate(), false); - addProperty("Human trigger", o->humanActivate, InspectorDelegate::boolDelegate(), false); - addProperty("Cpu trigger", o->computerActivate, InspectorDelegate::boolDelegate(), false); + addProperty("Remove after", o->removeAfterVisit, false); + addProperty("Human trigger", o->humanActivate, false); + addProperty("Cpu trigger", o->computerActivate, false); //ui8 availableFor; //players whom this event is available for } @@ -499,13 +491,13 @@ void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & v if(!o) return; if(key == "Remove after") - o->removeAfterVisit = stringToBool(value.toString()); + o->removeAfterVisit = value.toBool(); if(key == "Human trigger") - o->humanActivate = stringToBool(value.toString()); + o->humanActivate = value.toBool(); if(key == "Cpu trigger") - o->computerActivate = stringToBool(value.toString()); + o->computerActivate = value.toBool(); } void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVariant & value) @@ -562,7 +554,7 @@ void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant if(!o) return; if(key == "Removable units") - o->removableUnits = stringToBool(value.toString()); + o->removableUnits = value.toBool(); } void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVariant & value) @@ -626,9 +618,9 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant o->character = CGCreature::Character::SAVAGE; } if(key == "Never flees") - o->neverFlees = stringToBool(value.toString()); + o->neverFlees = value.toBool(); if(key == "Not growing") - o->notGrowingTeam = stringToBool(value.toString()); + o->notGrowingTeam = value.toBool(); if(key == "Amount") o->stacks[SlotID(0)]->count = value.toString().toInt(); } @@ -693,7 +685,10 @@ QTableWidgetItem * Inspector::addProperty(int value) QTableWidgetItem * Inspector::addProperty(bool value) { - return new QTableWidgetItem(value ? "TRUE" : "FALSE"); + auto item = new QTableWidgetItem; + item->setFlags(item->flags() & ~Qt::ItemIsEditable | Qt::ItemIsUserCheckable); + item->setCheckState(value ? Qt::Checked : Qt::Unchecked); + return item; } QTableWidgetItem * Inspector::addProperty(const std::string & value) @@ -827,13 +822,6 @@ Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), * Delegates */ -InspectorDelegate * InspectorDelegate::boolDelegate() -{ - auto * d = new InspectorDelegate; - d->options << "TRUE" << "FALSE"; - return d; -} - QWidget * InspectorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { return new QComboBox(parent); diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 4a1526518..6ee27b4f7 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -107,7 +107,8 @@ protected: { auto * itemValue = addProperty(value); if(restricted) - itemValue->setFlags(Qt::NoItemFlags); + itemValue->setFlags(itemValue->flags() & ~Qt::ItemIsEnabled); + //itemValue->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable); QTableWidgetItem * itemKey = nullptr; if(keyItems.contains(key)) @@ -120,7 +121,6 @@ protected: else { itemKey = new QTableWidgetItem(key); - itemKey->setFlags(Qt::NoItemFlags); keyItems[key] = itemKey; table->setRowCount(row + 1); @@ -130,6 +130,7 @@ protected: table->setItemDelegateForRow(row, delegate); ++row; } + itemKey->setFlags(restricted ? Qt::NoItemFlags : Qt::ItemIsEnabled); } template @@ -153,8 +154,6 @@ class InspectorDelegate : public QStyledItemDelegate { Q_OBJECT public: - static InspectorDelegate * boolDelegate(); - using QStyledItemDelegate::QStyledItemDelegate; QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 5788fb577..f838f519b 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -1015,7 +1015,10 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) //set parameter Inspector inspector(controller.map(), obj, tableWidget); - inspector.setProperty(param, item->text()); + if(item->flags() & Qt::ItemIsUserCheckable) + inspector.setProperty(param, QVariant::fromValue(item->checkState() == Qt::Checked)); + else + inspector.setProperty(param, item->text()); controller.commitObjectChange(mapLevel); } diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 7247dcf7c..159a1ec98 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -390,6 +390,9 @@ 2 + + true + false From 445e39ad20ce0fb4801b5fac1655632e84af553f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 22:05:57 +0200 Subject: [PATCH 0375/1248] Improved inspector --- mapeditor/inspector/armywidget.cpp | 7 +- mapeditor/inspector/armywidget.ui | 6 + mapeditor/inspector/inspector.cpp | 277 +++++++++----------- mapeditor/inspector/inspector.h | 11 +- mapeditor/inspector/messagewidget.ui | 3 + mapeditor/inspector/questwidget.ui | 6 + mapeditor/inspector/rewardswidget.cpp | 7 +- mapeditor/inspector/rewardswidget.ui | 6 + mapeditor/inspector/townbulidingswidget.cpp | 4 - mapeditor/inspector/townbulidingswidget.ui | 3 + mapeditor/mainwindow.cpp | 7 +- 11 files changed, 157 insertions(+), 180 deletions(-) diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 0cfd68d9a..65740a54a 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -137,12 +137,7 @@ void ArmyDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, cons { if(auto * ed = qobject_cast(editor)) { - auto isArmed = ed->commitChanges(); - model->setData(index, "dummy"); - if(isArmed) - model->setData(index, "HAS ARMY"); - else - model->setData(index, ""); + ed->commitChanges(); } else { diff --git a/mapeditor/inspector/armywidget.ui b/mapeditor/inspector/armywidget.ui index a6f141752..a6a512393 100644 --- a/mapeditor/inspector/armywidget.ui +++ b/mapeditor/inspector/armywidget.ui @@ -2,6 +2,9 @@ ArmyWidget + + Qt::NonModal + 0 @@ -19,6 +22,9 @@ Army settings + + true + diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 83880cbce..edb2fb62c 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -18,6 +18,7 @@ #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/mapping/CMap.h" +#include "../lib/constants/StringConstants.h" #include "townbulidingswidget.h" #include "armywidget.h" @@ -25,6 +26,29 @@ #include "rewardswidget.h" #include "questwidget.h" +static QList> MissionIdentifiers +{ + {QObject::tr("None"), QVariant::fromValue(int(CQuest::Emission::MISSION_NONE))}, + {QObject::tr("Reach level"), QVariant::fromValue(int(CQuest::Emission::MISSION_LEVEL))}, + {QObject::tr("Stats"), QVariant::fromValue(int(CQuest::Emission::MISSION_PRIMARY_STAT))}, + {QObject::tr("Kill hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_HERO))}, + {QObject::tr("Kill monster"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_CREATURE))}, + {QObject::tr("Artifact"), QVariant::fromValue(int(CQuest::Emission::MISSION_ART))}, + {QObject::tr("Army"), QVariant::fromValue(int(CQuest::Emission::MISSION_ARMY))}, + {QObject::tr("Resources"), QVariant::fromValue(int(CQuest::Emission::MISSION_RESOURCES))}, + {QObject::tr("Hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_HERO))}, + {QObject::tr("Player"), QVariant::fromValue(int(CQuest::Emission::MISSION_PLAYER))}, +}; + +static QList> CharacterIdentifiers +{ + {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, + {QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY))}, + {QObject::tr("Agressive"), QVariant::fromValue(int(CGCreature::Character::AGRESSIVE))}, + {QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE))}, + {QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE))}, +}; + //===============IMPLEMENT OBJECT INITIALIZATION FUNCTIONS================ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : defaultPlayer(pl) { @@ -246,9 +270,9 @@ void Inspector::updateProperties(CGHeroInstance * o) addProperty("Experience", o->exp, false); addProperty("Hero class", o->type ? o->type->heroClass->getNameTranslated() : "", true); - { //Sex + { //Gender auto * delegate = new InspectorDelegate; - delegate->options << "MALE" << "FEMALE"; + delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}}; addProperty("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false); } addProperty("Name", o->nameCustom, false); @@ -263,7 +287,9 @@ void Inspector::updateProperties(CGHeroInstance * o) if(map->allowedHeroes.at(i)) { if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) - delegate->options << QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()); + { + delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); + } } } addProperty("Hero type", o->type->getNameTranslated(), delegate, false); @@ -296,7 +322,7 @@ void Inspector::updateProperties(CGArtifact * o) for(auto spell : VLC->spellh->objects) { if(map->allowedSpells.at(spell->id)) - delegate->options << QObject::tr(spell->getNameTranslated().c_str()); + delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); } @@ -334,7 +360,7 @@ void Inspector::updateProperties(CGCreature * o) addProperty("Message", o->message, false); { //Character auto * delegate = new InspectorDelegate; - delegate->options << "COMPLIANT" << "FRIENDLY" << "AGRESSIVE" << "HOSTILE" << "SAVAGE"; + delegate->options = CharacterIdentifiers; addProperty("Character", (CGCreature::Character)o->character, delegate, false); } addProperty("Never flees", o->neverFlees, false); @@ -371,7 +397,7 @@ void Inspector::updateProperties(CGSeerHut * o) { //Mission type auto * delegate = new InspectorDelegate; - delegate->options << "Reach level" << "Stats" << "Kill hero" << "Kill creature" << "Artifact" << "Army" << "Resources" << "Hero" << "Player"; + delegate->options = MissionIdentifiers; addProperty("Mission type", o->quest->missionType, delegate, false); } @@ -410,10 +436,10 @@ void Inspector::updateProperties() } auto * delegate = new InspectorDelegate(); - delegate->options << "NEUTRAL"; + delegate->options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum())}); for(int p = 0; p < map->players.size(); ++p) if(map->players[p].canAnyonePlay()) - delegate->options << QString("PLAYER %1").arg(p); + delegate->options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum())}); addProperty("Owner", obj->tempOwner, delegate, true); UPDATE_OBJ_PROPERTIES(CArmedInstance); @@ -436,20 +462,30 @@ void Inspector::updateProperties() } //===============IMPLEMENT PROPERTY UPDATE================================ +void Inspector::setProperty(const QString & key, const QTableWidgetItem * item) +{ + if(!item->data(Qt::UserRole).isNull()) + { + setProperty(key, item->data(Qt::UserRole)); + return; + } + + if(item->flags() & Qt::ItemIsUserCheckable) + { + setProperty(key, QVariant::fromValue(item->checkState() == Qt::Checked)); + return; + } + + setProperty(key, item->text()); +} + void Inspector::setProperty(const QString & key, const QVariant & value) { if(!obj) return; if(key == "Owner") - { - PlayerColor owner(value.toString().mid(6).toInt()); //receiving PLAYER N, N has index 6 - if(value == "NEUTRAL") - owner = PlayerColor::NEUTRAL; - if(value == "UNFLAGGABLE") - owner = PlayerColor::UNFLAGGABLE; - obj->tempOwner = owner; - } + obj->tempOwner = PlayerColor(value.toInt()); SET_PROPERTIES(CArmedInstance); SET_PROPERTIES(CGTownInstance); @@ -533,14 +569,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(o->storedArtifact && key == "Spell") { - for(auto spell : VLC->spellh->objects) - { - if(spell->getNameTranslated() == value.toString().toStdString()) - { - o->storedArtifact = ArtifactUtils::createScroll(spell->getId()); - break; - } - } + o->storedArtifact = ArtifactUtils::createScroll(SpellID(value.toInt())); } } @@ -562,13 +591,13 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari if(!o) return; if(key == "Gender") - o->gender = value.toString() == "MALE" ? EHeroGender::MALE : EHeroGender::FEMALE; + o->gender = EHeroGender(value.toInt()); if(key == "Name") o->nameCustom = value.toString().toStdString(); if(key == "Experience") - o->exp = value.toInt(); + o->exp = value.toString().toInt(); if(key == "Hero type") { @@ -604,19 +633,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(key == "Message") o->message = value.toString().toStdString(); if(key == "Character") - { - //COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 - if(value == "COMPLIANT") - o->character = CGCreature::Character::COMPLIANT; - if(value == "FRIENDLY") - o->character = CGCreature::Character::FRIENDLY; - if(value == "AGRESSIVE") - o->character = CGCreature::Character::AGRESSIVE; - if(value == "HOSTILE") - o->character = CGCreature::Character::HOSTILE; - if(value == "SAVAGE") - o->character = CGCreature::Character::SAVAGE; - } + o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") o->neverFlees = value.toBool(); if(key == "Not growing") @@ -630,27 +647,7 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(!o) return; if(key == "Mission type") - { - if(value == "Reach level") - o->quest->missionType = CQuest::Emission::MISSION_LEVEL; - if(value == "Stats") - o->quest->missionType = CQuest::Emission::MISSION_PRIMARY_STAT; - if(value == "Kill hero") - o->quest->missionType = CQuest::Emission::MISSION_KILL_HERO; - if(value == "Kill creature") - o->quest->missionType = CQuest::Emission::MISSION_KILL_CREATURE; - if(value == "Artifact") - o->quest->missionType = CQuest::Emission::MISSION_ART; - if(value == "Army") - o->quest->missionType = CQuest::Emission::MISSION_ARMY; - if(value == "Resources") - o->quest->missionType = CQuest::Emission::MISSION_RESOURCES; - if(value == "Hero") - o->quest->missionType = CQuest::Emission::MISSION_HERO; - if(value == "Player") - o->quest->missionType = CQuest::Emission::MISSION_PLAYER; - } - + o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") o->quest->firstVisitText = value.toString().toStdString(); if(key == "Next visit text") @@ -663,30 +660,38 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & //===============IMPLEMENT PROPERTY VALUE TYPE============================ QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value) { - return new QTableWidgetItem(QString::number(data_cast(value))); + auto * item = new QTableWidgetItem(QString::number(data_cast(value))); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(Inspector::PropertyEditorPlaceholder value) { - auto item = new QTableWidgetItem(""); - item->setData(Qt::UserRole, QString("PropertyEditor")); + auto item = new QTableWidgetItem("..."); + item->setFlags(Qt::NoItemFlags); return item; } QTableWidgetItem * Inspector::addProperty(unsigned int value) { - return new QTableWidgetItem(QString::number(value)); + auto * item = new QTableWidgetItem(QString::number(value)); + item->setFlags(Qt::NoItemFlags); + //item->setData(Qt::UserRole, QVariant::fromValue(value)); + return item; } QTableWidgetItem * Inspector::addProperty(int value) { - return new QTableWidgetItem(QString::number(value)); + auto * item = new QTableWidgetItem(QString::number(value)); + item->setFlags(Qt::NoItemFlags); + //item->setData(Qt::UserRole, QVariant::fromValue(value)); + return item; } QTableWidgetItem * Inspector::addProperty(bool value) { auto item = new QTableWidgetItem; - item->setFlags(item->flags() & ~Qt::ItemIsEditable | Qt::ItemIsUserCheckable); + item->setFlags(Qt::ItemIsUserCheckable); item->setCheckState(value ? Qt::Checked : Qt::Unchecked); return item; } @@ -698,118 +703,75 @@ QTableWidgetItem * Inspector::addProperty(const std::string & value) QTableWidgetItem * Inspector::addProperty(const QString & value) { - return new QTableWidgetItem(value); + auto * item = new QTableWidgetItem(value); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(const int3 & value) { - return new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z)); + auto * item = new QTableWidgetItem(QString("(%1, %2, %3)").arg(value.x, value.y, value.z)); + item->setFlags(Qt::NoItemFlags); + return item; } QTableWidgetItem * Inspector::addProperty(const PlayerColor & value) { - auto str = QString("PLAYER %1").arg(value.getNum()); + auto str = QObject::tr("UNFLAGGABLE"); if(value == PlayerColor::NEUTRAL) - str = "NEUTRAL"; - if(value == PlayerColor::UNFLAGGABLE) - str = "UNFLAGGABLE"; - return new QTableWidgetItem(str); + str = QObject::tr("neutral"); + + if(value.isValidPlayer()) + str = QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[value]); + + auto * item = new QTableWidgetItem(str); + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(value.getNum())); + return item; } QTableWidgetItem * Inspector::addProperty(const GameResID & value) { - QString str; - switch (value.toEnum()) { - case EGameResID::WOOD: - str = "WOOD"; - break; - case EGameResID::ORE: - str = "ORE"; - break; - case EGameResID::SULFUR: - str = "SULFUR"; - break; - case EGameResID::GEMS: - str = "GEMS"; - break; - case EGameResID::MERCURY: - str = "MERCURY"; - break; - case EGameResID::CRYSTAL: - str = "CRYSTAL"; - break; - case EGameResID::GOLD: - str = "GOLD"; - break; - default: - break; - } - return new QTableWidgetItem(str); + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[value.toEnum()])); + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(value.getNum())); + return item; } QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) { - QString str; - switch (value) { - case CGCreature::Character::COMPLIANT: - str = "COMPLIANT"; - break; - case CGCreature::Character::FRIENDLY: - str = "FRIENDLY"; - break; - case CGCreature::Character::AGRESSIVE: - str = "AGRESSIVE"; - break; - case CGCreature::Character::HOSTILE: - str = "HOSTILE"; - break; - case CGCreature::Character::SAVAGE: - str = "SAVAGE"; - break; - default: + auto * item = new QTableWidgetItem; + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(int(value))); + + for(auto & i : CharacterIdentifiers) + { + if(i.second.toInt() == value) + { + item->setText(i.first); break; + } } - return new QTableWidgetItem(str); + + return item; } QTableWidgetItem * Inspector::addProperty(CQuest::Emission value) { - QString str; - switch (value) { - case CQuest::Emission::MISSION_LEVEL: - str = "Reach level"; - break; - case CQuest::Emission::MISSION_PRIMARY_STAT: - str = "Stats"; - break; - case CQuest::Emission::MISSION_KILL_HERO: - str = "Kill hero"; - break; - case CQuest::Emission::MISSION_KILL_CREATURE: - str = "Kill creature"; - break; - case CQuest::Emission::MISSION_ART: - str = "Artifact"; - break; - case CQuest::Emission::MISSION_ARMY: - str = "Army"; - break; - case CQuest::Emission::MISSION_RESOURCES: - str = "Resources"; - break; - case CQuest::Emission::MISSION_HERO: - str = "Hero"; - break; - case CQuest::Emission::MISSION_PLAYER: - str = "Player"; - break; - case CQuest::Emission::MISSION_KEYMASTER: - str = "Key master"; - break; - default: + auto * item = new QTableWidgetItem; + item->setFlags(Qt::NoItemFlags); + item->setData(Qt::UserRole, QVariant::fromValue(int(value))); + + for(auto & i : MissionIdentifiers) + { + if(i.second.toInt() == value) + { + item->setText(i.first); break; + } } - return new QTableWidgetItem(str); + + return item; } //======================================================================== @@ -831,7 +793,11 @@ void InspectorDelegate::setEditorData(QWidget *editor, const QModelIndex &index) { if(QComboBox *ed = qobject_cast(editor)) { - ed->addItems(options); + for(auto & i : options) + { + ed->addItem(i.first); + ed->setItemData(ed->count() - 1, i.second); + } } else { @@ -846,7 +812,8 @@ void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, if(!options.isEmpty()) { QMap data; - data[0] = options[ed->currentIndex()]; + data[Qt::DisplayRole] = options[ed->currentIndex()].first; + data[Qt::UserRole] = options[ed->currentIndex()].second; model->setItemData(index, data); } } diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 6ee27b4f7..ad47282b7 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -96,6 +96,8 @@ protected: public: Inspector(CMap *, CGObjectInstance *, QTableWidget *); + void setProperty(const QString & key, const QTableWidgetItem * item); + void setProperty(const QString & key, const QVariant & value); void updateProperties(); @@ -106,9 +108,10 @@ protected: void addProperty(const QString & key, const T & value, QAbstractItemDelegate * delegate, bool restricted) { auto * itemValue = addProperty(value); - if(restricted) - itemValue->setFlags(itemValue->flags() & ~Qt::ItemIsEnabled); - //itemValue->setFlags(Qt::ItemIsEditable | Qt::ItemIsSelectable); + if(!restricted) + itemValue->setFlags(itemValue->flags() | Qt::ItemIsEnabled | Qt::ItemIsSelectable); + if(!(itemValue->flags() & Qt::ItemIsUserCheckable)) + itemValue->setFlags(itemValue->flags() | Qt::ItemIsEditable); QTableWidgetItem * itemKey = nullptr; if(keyItems.contains(key)) @@ -160,6 +163,6 @@ public: void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; - QStringList options; + QList> options; }; diff --git a/mapeditor/inspector/messagewidget.ui b/mapeditor/inspector/messagewidget.ui index 925d8073e..369d0dc84 100644 --- a/mapeditor/inspector/messagewidget.ui +++ b/mapeditor/inspector/messagewidget.ui @@ -2,6 +2,9 @@ MessageWidget + + Qt::NonModal + 0 diff --git a/mapeditor/inspector/questwidget.ui b/mapeditor/inspector/questwidget.ui index 0f2f7c69a..c5f2931b8 100644 --- a/mapeditor/inspector/questwidget.ui +++ b/mapeditor/inspector/questwidget.ui @@ -2,6 +2,9 @@ QuestWidget + + Qt::NonModal + 0 @@ -13,6 +16,9 @@ Mission goal + + true + diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index bcc974112..54774da11 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -396,12 +396,7 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c { if(auto * ed = qobject_cast(editor)) { - auto hasReward = ed->commitChanges(); - model->setData(index, "dummy"); - if(hasReward) - model->setData(index, "HAS REWARD"); - else - model->setData(index, ""); + ed->commitChanges(); } else { diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index 32dbcfe7d..e8bb6d41a 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -2,6 +2,9 @@ RewardsWidget + + Qt::NonModal + 0 @@ -13,6 +16,9 @@ Rewards + + true + diff --git a/mapeditor/inspector/townbulidingswidget.cpp b/mapeditor/inspector/townbulidingswidget.cpp index 29ee4e500..b4f4c87d3 100644 --- a/mapeditor/inspector/townbulidingswidget.cpp +++ b/mapeditor/inspector/townbulidingswidget.cpp @@ -246,10 +246,6 @@ void TownBuildingsDelegate::setModelData(QWidget *editor, QAbstractItemModel *mo { town.forbiddenBuildings = ed->getForbiddenBuildings(); town.builtBuildings = ed->getBuiltBuildings(); - - auto data = model->itemData(index); - model->setData(index, "dummy"); - model->setItemData(index, data); //dummy change to trigger signal } else { diff --git a/mapeditor/inspector/townbulidingswidget.ui b/mapeditor/inspector/townbulidingswidget.ui index 942c5d5f5..f9eece29d 100644 --- a/mapeditor/inspector/townbulidingswidget.ui +++ b/mapeditor/inspector/townbulidingswidget.ui @@ -2,6 +2,9 @@ TownBulidingsWidget + + Qt::NonModal + 0 diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index f838f519b..14b6dfcba 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -996,7 +996,7 @@ void MainWindow::loadInspector(CGObjectInstance * obj, bool switchTab) void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) { - if(!item->isSelected()) + if(!item->isSelected() && !(item->flags() & Qt::ItemIsUserCheckable)) return; int r = item->row(); @@ -1015,10 +1015,7 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) //set parameter Inspector inspector(controller.map(), obj, tableWidget); - if(item->flags() & Qt::ItemIsUserCheckable) - inspector.setProperty(param, QVariant::fromValue(item->checkState() == Qt::Checked)); - else - inspector.setProperty(param, item->text()); + inspector.setProperty(param, item); controller.commitObjectChange(mapLevel); } From a89a8019ab5854af42da930094d88d1ef3be48d8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 10 Sep 2023 23:39:54 +0200 Subject: [PATCH 0376/1248] Added skills customization --- lib/mapObjects/CGHeroInstance.cpp | 7 +- mapeditor/CMakeLists.txt | 3 + mapeditor/inspector/heroskillswidget.cpp | 149 +++++++++++++++++++ mapeditor/inspector/heroskillswidget.h | 59 ++++++++ mapeditor/inspector/heroskillswidget.ui | 174 +++++++++++++++++++++++ mapeditor/inspector/inspector.cpp | 4 + 6 files changed, 394 insertions(+), 2 deletions(-) create mode 100644 mapeditor/inspector/heroskillswidget.cpp create mode 100644 mapeditor/inspector/heroskillswidget.h create mode 100644 mapeditor/inspector/heroskillswidget.ui diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 7700decc8..722702ab7 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -999,8 +999,11 @@ int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) { - assert(!hasBonus(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(which)) - .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)))); + auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(which)) + .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); + if(hasBonus(sel)) + removeBonuses(sel); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), static_cast(which))); } diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 59e83258d..df2d26a3d 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -32,6 +32,7 @@ set(editor_SRCS inspector/messagewidget.cpp inspector/rewardswidget.cpp inspector/questwidget.cpp + inspector/heroskillswidget.cpp resourceExtractor/ResourceConverter.cpp ) @@ -68,6 +69,7 @@ set(editor_HEADERS inspector/messagewidget.h inspector/rewardswidget.h inspector/questwidget.h + inspector/heroskillswidget.h resourceExtractor/ResourceConverter.h ) @@ -91,6 +93,7 @@ set(editor_FORMS inspector/messagewidget.ui inspector/rewardswidget.ui inspector/questwidget.ui + inspector/heroskillswidget.ui ) set(editor_TS diff --git a/mapeditor/inspector/heroskillswidget.cpp b/mapeditor/inspector/heroskillswidget.cpp new file mode 100644 index 000000000..d1254e4b8 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.cpp @@ -0,0 +1,149 @@ +/* + * heroskillswidget.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 "heroskillswidget.h" +#include "ui_heroskillswidget.h" +#include "../../lib/constants/StringConstants.h" +#include "../../lib/CSkillHandler.h" +#include "inspector.h" + +static QList> LevelIdentifiers +{ + {QObject::tr("Beginner"), QVariant::fromValue(1)}, + {QObject::tr("Advanced"), QVariant::fromValue(2)}, + {QObject::tr("Expert"), QVariant::fromValue(3)}, +}; + +HeroSkillsWidget::HeroSkillsWidget(CGHeroInstance & h, QWidget *parent) : + QDialog(parent), + ui(new Ui::HeroSkillsWidget), + hero(h) +{ + ui->setupUi(this); + + ui->labelAttack->setText(QString::fromStdString(NPrimarySkill::names[0])); + ui->labelDefence->setText(QString::fromStdString(NPrimarySkill::names[1])); + ui->labelPower->setText(QString::fromStdString(NPrimarySkill::names[2])); + ui->labelKnowledge->setText(QString::fromStdString(NPrimarySkill::names[3])); + + auto * delegate = new InspectorDelegate; + for(auto s : VLC->skillh->objects) + delegate->options.push_back({QString::fromStdString(s->getNameTranslated()), QVariant::fromValue(s->getId().getNum())}); + ui->skills->setItemDelegateForColumn(0, delegate); + + delegate = new InspectorDelegate; + delegate->options = LevelIdentifiers; + ui->skills->setItemDelegateForColumn(1, delegate); +} + +HeroSkillsWidget::~HeroSkillsWidget() +{ + delete ui; +} + +void HeroSkillsWidget::on_addButton_clicked() +{ + ui->skills->setRowCount(ui->skills->rowCount() + 1); +} + +void HeroSkillsWidget::on_removeButton_clicked() +{ + ui->skills->removeRow(ui->skills->currentRow()); +} + +void HeroSkillsWidget::on_checkBox_toggled(bool checked) +{ + ui->skills->setEnabled(checked); + ui->addButton->setEnabled(checked); + ui->removeButton->setEnabled(checked); +} + +void HeroSkillsWidget::obtainData() +{ + ui->attack->setValue(hero.getPrimSkillLevel(PrimarySkill::ATTACK)); + ui->defence->setValue(hero.getPrimSkillLevel(PrimarySkill::DEFENSE)); + ui->power->setValue(hero.getPrimSkillLevel(PrimarySkill::SPELL_POWER)); + ui->knowledge->setValue(hero.getPrimSkillLevel(PrimarySkill::KNOWLEDGE)); + + if(!hero.secSkills.empty() && hero.secSkills.front().first.getNum() == -1) + return; + + ui->checkBox->setChecked(true); + ui->skills->setRowCount(hero.secSkills.size()); + + int i = 0; + for(auto & s : hero.secSkills) + { + auto * itemSkill = new QTableWidgetItem; + itemSkill->setText(QString::fromStdString(VLC->skillh->getById(s.first)->getNameTranslated())); + itemSkill->setData(Qt::UserRole, QVariant::fromValue(s.first.getNum())); + ui->skills->setItem(i, 0, itemSkill); + + auto * itemLevel = new QTableWidgetItem; + itemLevel->setText(LevelIdentifiers[s.second - 1].first); + itemLevel->setData(Qt::UserRole, LevelIdentifiers[s.second - 1].second); + ui->skills->setItem(i++, 1, itemLevel); + } +} + +void HeroSkillsWidget::commitChanges() +{ + hero.pushPrimSkill(PrimarySkill::ATTACK, ui->attack->value()); + hero.pushPrimSkill(PrimarySkill::DEFENSE, ui->defence->value()); + hero.pushPrimSkill(PrimarySkill::SPELL_POWER, ui->power->value()); + hero.pushPrimSkill(PrimarySkill::KNOWLEDGE, ui->knowledge->value()); + + hero.secSkills.clear(); + + if(!ui->checkBox->isChecked()) + { + hero.secSkills.push_back(std::make_pair(SecondarySkill(-1), -1)); + return; + } + + for(int i = 0; i < ui->skills->rowCount(); ++i) + { + if(ui->skills->item(i, 0) && ui->skills->item(i, 1)) + hero.secSkills.push_back(std::make_pair(SecondarySkill(ui->skills->item(i, 0)->data(Qt::UserRole).toInt()), ui->skills->item(i, 1)->data(Qt::UserRole).toInt())); + } +} + +HeroSkillsDelegate::HeroSkillsDelegate(CGHeroInstance & h): hero(h), QStyledItemDelegate() +{ +} + +QWidget * HeroSkillsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new HeroSkillsWidget(hero, parent); +} + +void HeroSkillsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void HeroSkillsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/heroskillswidget.h b/mapeditor/inspector/heroskillswidget.h new file mode 100644 index 000000000..110988767 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.h @@ -0,0 +1,59 @@ +/* + * heroskillswidget.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 +#include "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class HeroSkillsWidget; +} + +class HeroSkillsWidget : public QDialog +{ + Q_OBJECT + +public: + explicit HeroSkillsWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~HeroSkillsWidget(); + + void obtainData(); + void commitChanges(); + +private slots: + void on_addButton_clicked(); + + void on_removeButton_clicked(); + + void on_checkBox_toggled(bool checked); + +private: + Ui::HeroSkillsWidget *ui; + + CGHeroInstance & hero; + + std::set occupiedSkills; +}; + +class HeroSkillsDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + HeroSkillsDelegate(CGHeroInstance &); + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + +private: + CGHeroInstance & hero; +}; diff --git a/mapeditor/inspector/heroskillswidget.ui b/mapeditor/inspector/heroskillswidget.ui new file mode 100644 index 000000000..3cbcdfda1 --- /dev/null +++ b/mapeditor/inspector/heroskillswidget.ui @@ -0,0 +1,174 @@ + + + HeroSkillsWidget + + + + 0 + 0 + 464 + 301 + + + + Hero skills + + + true + + + + 4 + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + TextLabel + + + + + + + + + + + + 0 + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + 90 + 0 + + + + Add + + + + + + + false + + + + 90 + 0 + + + + Remove + + + + + + + + + false + + + Qt::ScrollBarAlwaysOff + + + QAbstractScrollArea::AdjustToContents + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + false + + + 120 + + + true + + + false + + + 21 + + + + Skill + + + + + Level + + + + + + + + Customize skills + + + + + + + + diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index edb2fb62c..30e755caf 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -25,6 +25,7 @@ #include "messagewidget.h" #include "rewardswidget.h" #include "questwidget.h" +#include "heroskillswidget.h" static QList> MissionIdentifiers { @@ -279,6 +280,9 @@ void Inspector::updateProperties(CGHeroInstance * o) addProperty("Biography", o->biographyCustom, new MessageDelegate, false); addProperty("Portrait", o->portrait, false); + auto * delegate = new HeroSkillsDelegate(*o); + addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); + if(o->type) { //Hero type auto * delegate = new InspectorDelegate; From 94c6adb28c1bbf51b24a094714dfdf14c65d66ee Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 14:01:32 +0200 Subject: [PATCH 0377/1248] Temporary fix for vic/loss conditions string --- mapeditor/mapsettings/loseconditions.cpp | 8 ++++---- mapeditor/mapsettings/loseconditions.ui | 6 +++++- mapeditor/mapsettings/victoryconditions.cpp | 6 +++--- mapeditor/mapsettings/victoryconditions.ui | 6 +++++- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index fe761df9b..3ea029991 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -137,7 +137,7 @@ void LoseConditions::update(CMap & map) { map.triggeredEvents.push_back(standardDefeat); map.defeatIconIndex = 3; - map.defeatMessage.appendTextID("core.lcdesc.0"); + map.defeatMessage = MetaString::createFromTextID("core.lcdesc.0"); } else { @@ -162,7 +162,7 @@ void LoseConditions::update(CMap & map) noneOf.expressions.push_back(cond); specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); specialDefeat.trigger = EventExpression(noneOf); - map.defeatMessage.appendTextID("core.lcdesc.1"); + map.defeatMessage = MetaString::createFromTextID("core.lcdesc.1"); break; } @@ -176,7 +176,7 @@ void LoseConditions::update(CMap & map) noneOf.expressions.push_back(cond); specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); specialDefeat.trigger = EventExpression(noneOf); - map.defeatMessage.appendTextID("core.lcdesc.2"); + map.defeatMessage = MetaString::createFromTextID("core.lcdesc.2"); break; } @@ -186,7 +186,7 @@ void LoseConditions::update(CMap & map) cond.value = expiredDate(loseValueWidget->text()); specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); specialDefeat.trigger = EventExpression(cond); - map.defeatMessage.appendTextID("core.lcdesc.3"); + map.defeatMessage = MetaString::createFromTextID("core.lcdesc.3"); break; } diff --git a/mapeditor/mapsettings/loseconditions.ui b/mapeditor/mapsettings/loseconditions.ui index f96f2a635..35d3cb7ce 100644 --- a/mapeditor/mapsettings/loseconditions.ui +++ b/mapeditor/mapsettings/loseconditions.ui @@ -42,7 +42,11 @@ - + + + true + + diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index f88eee931..be9ffd168 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -186,7 +186,7 @@ void VictoryConditions::update(CMap & map) { map.triggeredEvents.push_back(standardVictory); map.victoryIconIndex = 11; - map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); + map.victoryMessage = MetaString::createFromTextID("core.vcdesc.0"); } else { @@ -198,7 +198,7 @@ void VictoryConditions::update(CMap & map) specialVictory.description.clear(); // TODO: display in quest window map.victoryIconIndex = vicCondition; - map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[size_t(vicCondition) + 1]); + map.victoryMessage = MetaString::createFromTextID("core.vcdesc." + std::to_string(vicCondition + 1)); switch(vicCondition) { @@ -301,7 +301,7 @@ void VictoryConditions::update(CMap & map) if(ui->standardVictoryCheck->isChecked()) { map.victoryMessage.appendRawString(" / "); - map.victoryMessage.appendTextID(VLC->generaltexth->victoryConditions[0]); + map.victoryMessage.appendTextID("core.vcdesc.0"); map.triggeredEvents.push_back(standardVictory); } map.triggeredEvents.push_back(specialVictory); diff --git a/mapeditor/mapsettings/victoryconditions.ui b/mapeditor/mapsettings/victoryconditions.ui index 2bd91bb40..d1c86ea47 100644 --- a/mapeditor/mapsettings/victoryconditions.ui +++ b/mapeditor/mapsettings/victoryconditions.ui @@ -42,7 +42,11 @@ - + + + true + + From 082e1194ff52d489c79681d55a9521083aaff330 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 14:27:37 +0200 Subject: [PATCH 0378/1248] Little refactoring --- mapeditor/mapsettings/abstractsettings.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index 3a1127a54..6d21eb712 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -87,11 +87,10 @@ std::string AbstractSettings::getTownName(const CMap & map, int objectIdx) std::string name; if(auto town = dynamic_cast(map.objects[objectIdx].get())) { - auto * ctown = town->town; - if(!ctown) - ctown = VLC->townh->randomTown; - - name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; + name = town->getNameTranslated(); + + if(name.empty()) + name = town->getTown()->faction->getNameTranslated(); } return name; } From 19407954ed9a602247af6bc3719aab4d6fe59509 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 18:01:53 +0200 Subject: [PATCH 0379/1248] Object picker implemented --- mapeditor/mapsettings/abstractsettings.h | 6 +- mapeditor/mapview.cpp | 22 ++ mapeditor/mapview.h | 1 + mapeditor/playerparams.cpp | 50 +++- mapeditor/playerparams.h | 4 + mapeditor/playerparams.ui | 298 +++++++++++++---------- mapeditor/scenelayer.cpp | 77 ++++++ mapeditor/scenelayer.h | 33 ++- 8 files changed, 359 insertions(+), 132 deletions(-) diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 827628778..9f7a53fbc 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -30,9 +30,9 @@ public: virtual void initialize(const CMap & map) = 0; virtual void update(CMap & map) = 0; - std::string getTownName(const CMap & map, int objectIdx); - std::string getHeroName(const CMap & map, int objectIdx); - std::string getMonsterName(const CMap & map, int objectIdx); + static std::string getTownName(const CMap & map, int objectIdx); + static std::string getHeroName(const CMap & map, int objectIdx); + static std::string getMonsterName(const CMap & map, int objectIdx); static JsonNode conditionToJson(const EventCondition & event); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 715dea10b..dc2650884 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -198,6 +198,9 @@ void MapView::mousePressEvent(QMouseEvent *event) auto * sc = static_cast(scene()); if(!sc || !controller->map()) return; + + if(sc->objectPickerView.isVisible()) + return; mouseStart = mapToScene(event->pos()); tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level); @@ -338,6 +341,22 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) if(rubberBand) rubberBand->hide(); + + if(sc->objectPickerView.isVisible()) + { + if(event->button() == Qt::RightButton) + sc->objectPickerView.discard(); + + if(event->button() == Qt::LeftButton) + { + mouseStart = mapToScene(event->pos()); + tileStart = tilePrev = int3(mouseStart.x() / 32, mouseStart.y() / 32, sc->level); + if(auto * pickedObject = sc->selectionObjectsView.selectObjectAt(tileStart.x, tileStart.y)) + sc->objectPickerView.select(pickedObject); + } + + return; + } switch(selectionTool) { @@ -547,6 +566,7 @@ MapScene::MapScene(int lvl): terrainView(this), objectsView(this), selectionObjectsView(this), + objectPickerView(this), isTerrainSelected(false), isObjectSelected(false) { @@ -562,6 +582,7 @@ std::list MapScene::getAbstractLayers() &objectsView, &gridView, &passabilityView, + &objectPickerView, &selectionTerrainView, &selectionObjectsView }; @@ -575,6 +596,7 @@ void MapScene::updateViews() objectsView.show(true); selectionTerrainView.show(true); selectionObjectsView.show(true); + objectPickerView.show(true); } void MapScene::terrainSelected(bool anythingSelected) diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index 009f40ac6..b5d8d0e1c 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -66,6 +66,7 @@ public: TerrainLayer terrainView; ObjectsLayer objectsView; SelectionObjectsLayer selectionObjectsView; + ObjectPickerLayer objectPickerView; signals: void selected(bool anything); diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index 9734f8f68..d72ae766c 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "playerparams.h" #include "ui_playerparams.h" +#include "mapsettings/abstractsettings.h" #include "../lib/CTownHandler.h" #include "../lib/constants/StringConstants.h" @@ -87,7 +88,8 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent) { if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos) foundMainTown = townIndex; - const auto name = ctown->faction ? town->getObjectName() : town->getNameTranslated() + ", (random)"; + + const auto name = AbstractSettings::getTownName(*controller.map(), i); ui->mainTown->addItem(tr(name.c_str()), QVariant::fromValue(i)); ++townIndex; } @@ -186,3 +188,49 @@ void PlayerParams::on_playerColorCombo_activated(int index) } } + +void PlayerParams::on_townSelect_clicked() +{ + auto pred = [this](const CGObjectInstance * obj) -> bool + { + if(auto town = dynamic_cast(obj)) + return town->getOwner().getNum() == playerColor; + return false; + }; + + std::vector> pickers; + pickers.emplace_back(controller.scene(0)->objectPickerView); + if(controller.map()->twoLevel) + pickers.emplace_back(controller.scene(1)->objectPickerView); + + for(auto l : pickers) + { + l.get().highlight(pred); + l.get().update(); + QObject::connect(&l.get(), &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); + } + + dynamic_cast(parent()->parent()->parent()->parent())->hide(); +} + +void PlayerParams::onTownPicked(const CGObjectInstance * obj) +{ + dynamic_cast(parent()->parent()->parent()->parent())->show(); + + std::vector> pickers; + pickers.emplace_back(controller.scene(0)->objectPickerView); + if(controller.map()->twoLevel) + pickers.emplace_back(controller.scene(1)->objectPickerView); + + for(auto l : pickers) + { + l.get().clear(); + l.get().update(); + QObject::disconnect(&l.get(), &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); + } + + if(!obj) //discarded + return; + + +} diff --git a/mapeditor/playerparams.h b/mapeditor/playerparams.h index 2aaff5384..0faa030b1 100644 --- a/mapeditor/playerparams.h +++ b/mapeditor/playerparams.h @@ -28,6 +28,8 @@ public: PlayerInfo playerInfo; int playerColor; + + void onTownPicked(const CGObjectInstance *); private slots: void on_radioHuman_toggled(bool checked); @@ -46,6 +48,8 @@ private slots: void on_playerColorCombo_activated(int index); + void on_townSelect_clicked(); + private: Ui::PlayerParams *ui; diff --git a/mapeditor/playerparams.ui b/mapeditor/playerparams.ui index 811fd626e..d2947a2a2 100644 --- a/mapeditor/playerparams.ui +++ b/mapeditor/playerparams.ui @@ -6,7 +6,7 @@ 0 0 - 505 + 614 160 @@ -25,7 +25,7 @@ - + 0 @@ -49,136 +49,180 @@ - - - - - - 0 - 0 - - - + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + 0 + 0 + + + + CPU only + + + + + + + + 0 + 0 + + + + Human/CPU + + + + + + + Team + + + + + + + + 0 + 0 + + + + + - - - - - 0 - 0 - - - - Generate hero at main - - + + + + + + Color + + + + + + + + 0 + 0 + + + + + + + + Main town + + + + + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + (default) + + + + + + + + ... + + + + + + + + + + 0 + 0 + + + + Generate hero at main + + + + - - - - - 0 - 0 - - - - Random faction - - - - - - - Team - - - - - - - - 0 - 0 - - - - CPU only - - - - - - - - 0 - 0 - - - - Human/CPU - - - - - - - true - - - - 0 - 0 - - - - Qt::ClickFocus - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::NoSelection - - - - - - - - 0 - 0 - + + + + 0 - - (default) - + + + true + + + + 0 + 0 + + + + Qt::ClickFocus + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + - - - - - - Main town - - - - - - - - 0 - 0 - - - - - - - - Color - - + + + + + 0 + 0 + + + + Random faction + + + +
    diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 5b4076608..635667228 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -112,6 +112,83 @@ void PassabilityLayer::update() redraw(); } +ObjectPickerLayer::ObjectPickerLayer(MapSceneBase * s): AbstractLayer(s) +{ +} + +void ObjectPickerLayer::highlight(std::function predicate) +{ + if(!map) + return; + + for(int j = 0; j < map->height; ++j) + { + for(int i = 0; i < map->width; ++i) + { + auto tl = map->getTile(int3(i, j, scene->level)); + auto * obj = tl.topVisitableObj(); + if(!obj && !tl.blockingObjects.empty()) + obj = tl.blockingObjects.front(); + + if(obj && predicate(obj)) + possibleObjects.insert(obj); + } + } + + isActive = true; +} + +bool ObjectPickerLayer::isVisible() const +{ + return isShown && isActive; +} + +void ObjectPickerLayer::clear() +{ + possibleObjects.clear(); + isActive = false; +} + +void ObjectPickerLayer::update() +{ + if(!map) + return; + + pixmap.reset(new QPixmap(map->width * 32, map->height * 32)); + pixmap->fill(Qt::transparent); + if(isActive) + pixmap->fill(QColor(255, 255, 255, 128)); + + + QPainter painter(pixmap.get()); + painter.setCompositionMode(QPainter::CompositionMode_Source); + for(auto * obj : possibleObjects) + { + if(obj->pos.z != scene->level) + continue; + + for(auto & pos : obj->getBlockedPos()) + painter.fillRect(pos.x * 32, pos.y * 32, 32, 32, QColor(255, 211, 0, 64)); + } + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + redraw(); +} + +void ObjectPickerLayer::select(const CGObjectInstance * obj) +{ + if(obj && possibleObjects.count(obj)) + { + clear(); + emit selectionMade(obj); + } +} + +void ObjectPickerLayer::discard() +{ + clear(); + emit selectionMade(nullptr); +} + SelectionTerrainLayer::SelectionTerrainLayer(MapSceneBase * s): AbstractLayer(s) { } diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index 3d3cd6a3f..2b0a127fd 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -116,7 +116,7 @@ public: void update() override; - void draw(bool onlyDirty = true); //TODO: implement dirty + void draw(bool onlyDirty = true); void setDirty(int x, int y); void setDirty(const CGObjectInstance * object); @@ -127,6 +127,37 @@ private: }; +class ObjectPickerLayer: public AbstractLayer +{ + Q_OBJECT +public: + ObjectPickerLayer(MapSceneBase * s); + + void update() override; + bool isVisible() const; + + template + void highlight() + { + highlight([](const CGObjectInstance * o){ return dynamic_cast(o); }); + } + + void highlight(std::function predicate); + + void clear(); + + void select(const CGObjectInstance *); + void discard(); + +signals: + void selectionMade(const CGObjectInstance *); + +private: + bool isActive = false; + std::set possibleObjects; +}; + + class SelectionObjectsLayer: public AbstractLayer { Q_OBJECT From 727284c05e8a2f8484b32e306d80a06864b67ff0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 18:30:27 +0200 Subject: [PATCH 0380/1248] Bump editor version --- mapeditor/StdInc.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 817f9433c..778a04843 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -2,7 +2,7 @@ #include "../Global.h" -#define VCMI_EDITOR_VERSION "0.1" +#define VCMI_EDITOR_VERSION "0.2" #define VCMI_EDITOR_NAME "VCMI Map Editor" #include From 3024aaae30bb69b14f3210f12916eeda89b32c52 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 18:44:55 +0200 Subject: [PATCH 0381/1248] Town picker in player settings implemented --- mapeditor/playerparams.cpp | 42 +++++++++++++++++++------------------- mapeditor/playerparams.h | 2 +- mapeditor/scenelayer.cpp | 21 +++++++++++-------- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index d72ae766c..192256d1a 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -151,8 +151,7 @@ void PlayerParams::allowedFactionsCheck(QListWidgetItem * item) playerInfo.allowedFactions.erase(FactionID(item->data(Qt::UserRole).toInt())); } - -void PlayerParams::on_mainTown_activated(int index) +void PlayerParams::on_mainTown_currentIndexChanged(int index) { if(index == 0) //default { @@ -198,16 +197,12 @@ void PlayerParams::on_townSelect_clicked() return false; }; - std::vector> pickers; - pickers.emplace_back(controller.scene(0)->objectPickerView); - if(controller.map()->twoLevel) - pickers.emplace_back(controller.scene(1)->objectPickerView); - - for(auto l : pickers) + for(int lvl : {0, 1}) { - l.get().highlight(pred); - l.get().update(); - QObject::connect(&l.get(), &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(pred); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); } dynamic_cast(parent()->parent()->parent()->parent())->hide(); @@ -217,20 +212,25 @@ void PlayerParams::onTownPicked(const CGObjectInstance * obj) { dynamic_cast(parent()->parent()->parent()->parent())->show(); - std::vector> pickers; - pickers.emplace_back(controller.scene(0)->objectPickerView); - if(controller.map()->twoLevel) - pickers.emplace_back(controller.scene(1)->objectPickerView); - - for(auto l : pickers) + for(int lvl : {0, 1}) { - l.get().clear(); - l.get().update(); - QObject::disconnect(&l.get(), &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &PlayerParams::onTownPicked); } if(!obj) //discarded return; - + for(int i = 0; i < ui->mainTown->count(); ++i) + { + auto town = controller.map()->objects.at(ui->mainTown->itemData(i).toInt()); + if(town == obj) + { + ui->mainTown->setCurrentIndex(i); + break; + } + } } + diff --git a/mapeditor/playerparams.h b/mapeditor/playerparams.h index 0faa030b1..57601d2c3 100644 --- a/mapeditor/playerparams.h +++ b/mapeditor/playerparams.h @@ -36,7 +36,7 @@ private slots: void on_radioCpu_toggled(bool checked); - void on_mainTown_activated(int index); + void on_mainTown_currentIndexChanged(int index); void on_generateHero_stateChanged(int arg1); diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 635667228..6e05bfe09 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -121,17 +121,20 @@ void ObjectPickerLayer::highlight(std::function if(!map) return; - for(int j = 0; j < map->height; ++j) + if(scene->level == 0 || map->twoLevel) { - for(int i = 0; i < map->width; ++i) + for(int j = 0; j < map->height; ++j) { - auto tl = map->getTile(int3(i, j, scene->level)); - auto * obj = tl.topVisitableObj(); - if(!obj && !tl.blockingObjects.empty()) - obj = tl.blockingObjects.front(); - - if(obj && predicate(obj)) - possibleObjects.insert(obj); + for(int i = 0; i < map->width; ++i) + { + auto tl = map->getTile(int3(i, j, scene->level)); + auto * obj = tl.topVisitableObj(); + if(!obj && !tl.blockingObjects.empty()) + obj = tl.blockingObjects.front(); + + if(obj && predicate(obj)) + possibleObjects.insert(obj); + } } } From 1ac886ca71b2715bb8b8d85e05f20d1638b7b6fd Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 19:16:24 +0200 Subject: [PATCH 0382/1248] Refactor abstract settings to have map controller --- mapeditor/mapsettings/abstractsettings.cpp | 6 ++ mapeditor/mapsettings/abstractsettings.h | 13 ++-- mapeditor/mapsettings/eventsettings.cpp | 12 ++-- mapeditor/mapsettings/eventsettings.h | 4 +- mapeditor/mapsettings/generalsettings.cpp | 34 ++++----- mapeditor/mapsettings/generalsettings.h | 4 +- mapeditor/mapsettings/loseconditions.cpp | 65 ++++++++++------- mapeditor/mapsettings/loseconditions.h | 10 ++- mapeditor/mapsettings/mapsettings.cpp | 24 +++---- mapeditor/mapsettings/modsettings.cpp | 14 ++-- mapeditor/mapsettings/modsettings.h | 5 +- mapeditor/mapsettings/rumorsettings.cpp | 12 ++-- mapeditor/mapsettings/rumorsettings.h | 4 +- mapeditor/mapsettings/victoryconditions.cpp | 78 ++++++++++++--------- mapeditor/mapsettings/victoryconditions.h | 10 ++- 15 files changed, 172 insertions(+), 123 deletions(-) diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index 6d21eb712..c15c52cd9 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "abstractsettings.h" +#include "../mapcontroller.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGCreature.h" #include "../../lib/CTownHandler.h" @@ -82,6 +83,11 @@ AbstractSettings::AbstractSettings(QWidget *parent) } +void AbstractSettings::initialize(MapController & c) +{ + controller = &c; +} + std::string AbstractSettings::getTownName(const CMap & map, int objectIdx) { std::string name; diff --git a/mapeditor/mapsettings/abstractsettings.h b/mapeditor/mapsettings/abstractsettings.h index 9f7a53fbc..53ab653e5 100644 --- a/mapeditor/mapsettings/abstractsettings.h +++ b/mapeditor/mapsettings/abstractsettings.h @@ -20,6 +20,8 @@ QString expiredDate(int date); int3 posFromJson(const JsonNode & json); std::vector linearJsonArray(const JsonNode & json); +class MapController; + class AbstractSettings : public QWidget { Q_OBJECT @@ -27,8 +29,8 @@ public: explicit AbstractSettings(QWidget *parent = nullptr); virtual ~AbstractSettings() = default; - virtual void initialize(const CMap & map) = 0; - virtual void update(CMap & map) = 0; + virtual void initialize(MapController & controller); + virtual void update() = 0; static std::string getTownName(const CMap & map, int objectIdx); static std::string getHeroName(const CMap & map, int objectIdx); @@ -37,7 +39,7 @@ public: static JsonNode conditionToJson(const EventCondition & event); template - std::vector getObjectIndexes(const CMap & map) const + static std::vector getObjectIndexes(const CMap & map) { std::vector result; for(int i = 0; i < map.objects.size(); ++i) @@ -49,7 +51,7 @@ public: } template - int getObjectByPos(const CMap & map, const int3 & pos) + static int getObjectByPos(const CMap & map, const int3 & pos) { for(int i = 0; i < map.objects.size(); ++i) { @@ -62,6 +64,7 @@ public: return -1; } -signals: +protected: + MapController * controller = nullptr; }; diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 22a28e09f..6892d2903 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -11,6 +11,7 @@ #include "eventsettings.h" #include "timedevent.h" #include "ui_eventsettings.h" +#include "../mapcontroller.h" #include "../../lib/mapping/CMapDefines.h" #include "../../lib/constants/NumericConstants.h" #include "../../lib/constants/StringConstants.h" @@ -73,9 +74,10 @@ EventSettings::~EventSettings() delete ui; } -void EventSettings::initialize(const CMap & map) +void EventSettings::initialize(MapController & c) { - for(const auto & event : map.events) + AbstractSettings::initialize(c); + for(const auto & event : controller->map()->events) { auto * item = new QListWidgetItem(QString::fromStdString(event.name)); item->setData(Qt::UserRole, toVariant(event)); @@ -83,13 +85,13 @@ void EventSettings::initialize(const CMap & map) } } -void EventSettings::update(CMap & map) +void EventSettings::update() { - map.events.clear(); + controller->map()->events.clear(); for(int i = 0; i < ui->eventsList->count(); ++i) { const auto * item = ui->eventsList->item(i); - map.events.push_back(eventFromVariant(item->data(Qt::UserRole))); + controller->map()->events.push_back(eventFromVariant(item->data(Qt::UserRole))); } } diff --git a/mapeditor/mapsettings/eventsettings.h b/mapeditor/mapsettings/eventsettings.h index 0560ec534..ef29f0308 100644 --- a/mapeditor/mapsettings/eventsettings.h +++ b/mapeditor/mapsettings/eventsettings.h @@ -23,8 +23,8 @@ public: explicit EventSettings(QWidget *parent = nullptr); ~EventSettings(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; private slots: void on_timedEventAdd_clicked(); diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index 347c9234f..09ad221b3 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "generalsettings.h" #include "ui_generalsettings.h" +#include "../mapcontroller.h" GeneralSettings::GeneralSettings(QWidget *parent) : AbstractSettings(parent), @@ -23,15 +24,16 @@ GeneralSettings::~GeneralSettings() delete ui; } -void GeneralSettings::initialize(const CMap & map) +void GeneralSettings::initialize(MapController & c) { - ui->mapNameEdit->setText(tr(map.name.c_str())); - ui->mapDescriptionEdit->setPlainText(tr(map.description.c_str())); - ui->heroLevelLimit->setValue(map.levelLimit); - ui->heroLevelLimitCheck->setChecked(map.levelLimit); + AbstractSettings::initialize(c); + ui->mapNameEdit->setText(tr(controller->map()->name.c_str())); + ui->mapDescriptionEdit->setPlainText(tr(controller->map()->description.c_str())); + ui->heroLevelLimit->setValue(controller->map()->levelLimit); + ui->heroLevelLimitCheck->setChecked(controller->map()->levelLimit); //set difficulty - switch(map.difficulty) + switch(controller->map()->difficulty) { case 0: ui->diffRadio1->setChecked(true); @@ -55,21 +57,21 @@ void GeneralSettings::initialize(const CMap & map) }; } -void GeneralSettings::update(CMap & map) +void GeneralSettings::update() { - map.name = ui->mapNameEdit->text().toStdString(); - map.description = ui->mapDescriptionEdit->toPlainText().toStdString(); + controller->map()->name = ui->mapNameEdit->text().toStdString(); + controller->map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); if(ui->heroLevelLimitCheck->isChecked()) - map.levelLimit = ui->heroLevelLimit->value(); + controller->map()->levelLimit = ui->heroLevelLimit->value(); else - map.levelLimit = 0; + controller->map()->levelLimit = 0; //set difficulty - if(ui->diffRadio1->isChecked()) map.difficulty = 0; - if(ui->diffRadio2->isChecked()) map.difficulty = 1; - if(ui->diffRadio3->isChecked()) map.difficulty = 2; - if(ui->diffRadio4->isChecked()) map.difficulty = 3; - if(ui->diffRadio5->isChecked()) map.difficulty = 4; + if(ui->diffRadio1->isChecked()) controller->map()->difficulty = 0; + if(ui->diffRadio2->isChecked()) controller->map()->difficulty = 1; + if(ui->diffRadio3->isChecked()) controller->map()->difficulty = 2; + if(ui->diffRadio4->isChecked()) controller->map()->difficulty = 3; + if(ui->diffRadio5->isChecked()) controller->map()->difficulty = 4; } void GeneralSettings::on_heroLevelLimitCheck_toggled(bool checked) diff --git a/mapeditor/mapsettings/generalsettings.h b/mapeditor/mapsettings/generalsettings.h index 8360dee66..b95606d73 100644 --- a/mapeditor/mapsettings/generalsettings.h +++ b/mapeditor/mapsettings/generalsettings.h @@ -23,8 +23,8 @@ public: explicit GeneralSettings(QWidget *parent = nullptr); ~GeneralSettings(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; private slots: void on_heroLevelLimitCheck_toggled(bool checked); diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index 3ea029991..4d15a06fe 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "loseconditions.h" #include "ui_loseconditions.h" - +#include "../mapcontroller.h" #include "../lib/CGeneralTextHandler.h" LoseConditions::LoseConditions(QWidget *parent) : @@ -25,12 +25,12 @@ LoseConditions::~LoseConditions() delete ui; } -void LoseConditions::initialize(const CMap & map) +void LoseConditions::initialize(MapController & c) { - mapPointer = ↦ + AbstractSettings::initialize(c); //loss messages - ui->defeatMessageEdit->setText(QString::fromStdString(map.defeatMessage.toString())); + ui->defeatMessageEdit->setText(QString::fromStdString(controller->map()->defeatMessage.toString())); //loss conditions const std::array conditionStringsLose = { @@ -47,7 +47,7 @@ void LoseConditions::initialize(const CMap & map) } ui->standardLoseCheck->setChecked(false); - for(auto & ev : map.triggeredEvents) + for(auto & ev : controller->map()->triggeredEvents) { if(ev.effect.type == EventEffect::DEFEAT) { @@ -68,7 +68,7 @@ void LoseConditions::initialize(const CMap & map) { ui->loseComboBox->setCurrentIndex(1); assert(loseTypeWidget); - int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(townIdx >= 0) { auto idx = loseTypeWidget->findData(townIdx); @@ -79,7 +79,7 @@ void LoseConditions::initialize(const CMap & map) { ui->loseComboBox->setCurrentIndex(2); assert(loseTypeWidget); - int heroIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int heroIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(heroIdx >= 0) { auto idx = loseTypeWidget->findData(heroIdx); @@ -114,10 +114,10 @@ void LoseConditions::initialize(const CMap & map) } } -void LoseConditions::update(CMap & map) +void LoseConditions::update() { //loss messages - map.defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); + controller->map()->defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); //loss conditions EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); @@ -135,9 +135,9 @@ void LoseConditions::update(CMap & map) //DEFEAT if(ui->loseComboBox->currentIndex() == 0) { - map.triggeredEvents.push_back(standardDefeat); - map.defeatIconIndex = 3; - map.defeatMessage = MetaString::createFromTextID("core.lcdesc.0"); + controller->map()->triggeredEvents.push_back(standardDefeat); + controller->map()->defeatIconIndex = 3; + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.0"); } else { @@ -148,7 +148,7 @@ void LoseConditions::update(CMap & map) specialDefeat.identifier = "specialDefeat"; specialDefeat.description.clear(); // TODO: display in quest window - map.defeatIconIndex = lossCondition; + controller->map()->defeatIconIndex = lossCondition; switch(lossCondition) { @@ -158,11 +158,11 @@ void LoseConditions::update(CMap & map) cond.objectType = Obj::TOWN; assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = map.objects[townIdx]->pos; + cond.position = controller->map()->objects[townIdx]->pos; noneOf.expressions.push_back(cond); specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); specialDefeat.trigger = EventExpression(noneOf); - map.defeatMessage = MetaString::createFromTextID("core.lcdesc.1"); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.1"); break; } @@ -172,11 +172,11 @@ void LoseConditions::update(CMap & map) cond.objectType = Obj::HERO; assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); - cond.position = map.objects[townIdx]->pos; + cond.position = controller->map()->objects[townIdx]->pos; noneOf.expressions.push_back(cond); specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); specialDefeat.trigger = EventExpression(noneOf); - map.defeatMessage = MetaString::createFromTextID("core.lcdesc.2"); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.2"); break; } @@ -186,7 +186,7 @@ void LoseConditions::update(CMap & map) cond.value = expiredDate(loseValueWidget->text()); specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); specialDefeat.trigger = EventExpression(cond); - map.defeatMessage = MetaString::createFromTextID("core.lcdesc.3"); + controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.3"); break; } @@ -210,9 +210,9 @@ void LoseConditions::update(CMap & map) if(ui->standardLoseCheck->isChecked()) { - map.triggeredEvents.push_back(standardDefeat); + controller->map()->triggeredEvents.push_back(standardDefeat); } - map.triggeredEvents.push_back(specialDefeat); + controller->map()->triggeredEvents.push_back(specialDefeat); } } @@ -222,9 +222,11 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) delete loseTypeWidget; delete loseValueWidget; delete loseSelectWidget; + delete pickObjectButton; loseTypeWidget = nullptr; loseValueWidget = nullptr; loseSelectWidget = nullptr; + pickObjectButton = nullptr; if(index == 0) { @@ -240,16 +242,22 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) case 0: { //EventCondition::CONTROL (Obj::TOWN) loseTypeWidget = new QComboBox; ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes(*mapPointer)) - loseTypeWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + loseTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); + ui->loseParamsLayout->addWidget(pickObjectButton); break; } case 1: { //EventCondition::CONTROL (Obj::HERO) loseTypeWidget = new QComboBox; ui->loseParamsLayout->addWidget(loseTypeWidget); - for(int i : getObjectIndexes(*mapPointer)) - loseTypeWidget->addItem(tr(getHeroName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + loseTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); + ui->loseParamsLayout->addWidget(pickObjectButton); break; } @@ -269,3 +277,12 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) } } +void LoseConditions::onObjectSelect() +{ + +} + +void LoseConditions::onObjectPicked(const CGObjectInstance * obj) +{ + +} diff --git a/mapeditor/mapsettings/loseconditions.h b/mapeditor/mapsettings/loseconditions.h index b091df7f0..9ae7aa509 100644 --- a/mapeditor/mapsettings/loseconditions.h +++ b/mapeditor/mapsettings/loseconditions.h @@ -23,18 +23,22 @@ public: explicit LoseConditions(QWidget *parent = nullptr); ~LoseConditions(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; + +public slots: + void onObjectSelect(); + void onObjectPicked(const CGObjectInstance *); private slots: void on_loseComboBox_currentIndexChanged(int index); private: Ui::LoseConditions *ui; - const CMap * mapPointer = nullptr; QComboBox * loseTypeWidget = nullptr; QComboBox * loseSelectWidget = nullptr; QLineEdit * loseValueWidget = nullptr; + QToolButton * pickObjectButton = nullptr; }; diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 191583805..5b410cb56 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -63,12 +63,12 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : ui->listHeroes->addItem(item); } - ui->general->initialize(*controller.map()); - ui->mods->initialize(*controller.map()); - ui->victory->initialize(*controller.map()); - ui->lose->initialize(*controller.map()); - ui->events->initialize(*controller.map()); - ui->rumors->initialize(*controller.map()); + ui->general->initialize(controller); + ui->mods->initialize(controller); + ui->victory->initialize(controller); + ui->lose->initialize(controller); + ui->events->initialize(controller); + ui->rumors->initialize(controller); } MapSettings::~MapSettings() @@ -94,12 +94,12 @@ void MapSettings::on_pushButton_clicked() controller.map()->triggeredEvents.clear(); - ui->general->update(*controller.map()); - ui->mods->update(*controller.map()); - ui->victory->update(*controller.map()); - ui->lose->update(*controller.map()); - ui->events->update(*controller.map()); - ui->rumors->update(*controller.map()); + ui->general->update(); + ui->mods->update(); + ui->victory->update(); + ui->lose->update(); + ui->events->update(); + ui->rumors->update(); controller.commitChangeWithoutRedraw(); diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index 2cc5fbe32..f8121d918 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -35,9 +35,9 @@ ModSettings::~ModSettings() delete ui; } -void ModSettings::initialize(const CMap & map) +void ModSettings::initialize(MapController & c) { - mapPointer = ↦ + AbstractSettings::initialize(c); //mods management //collect all active mods @@ -50,7 +50,7 @@ void ModSettings::initialize(const CMap & map) auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(0, map.mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); + item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); //set parent check if(parent && item->checkState(0) == Qt::Checked) parent->setCheckState(0, Qt::Checked); @@ -96,7 +96,7 @@ void ModSettings::initialize(const CMap & map) ui->treeMods->blockSignals(false); } -void ModSettings::update(CMap & map) +void ModSettings::update() { //Mod management auto widgetAction = [&](QTreeWidgetItem * item) @@ -104,11 +104,11 @@ void ModSettings::update(CMap & map) if(item->checkState(0) == Qt::Checked) { auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - map.mods[modName] = VLC->modh->getModInfo(modName).version; + controller->map()->mods[modName] = VLC->modh->getModInfo(modName).version; } }; - map.mods.clear(); + controller->map()->mods.clear(); for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i) { QTreeWidgetItem *item = ui->treeMods->topLevelItem(i); @@ -134,7 +134,7 @@ void ModSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods) void ModSettings::on_modResolution_map_clicked() { - updateModWidgetBasedOnMods(MapController::modAssessmentMap(*mapPointer)); + updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller->map())); } diff --git a/mapeditor/mapsettings/modsettings.h b/mapeditor/mapsettings/modsettings.h index 8c30b88b0..5a1f83064 100644 --- a/mapeditor/mapsettings/modsettings.h +++ b/mapeditor/mapsettings/modsettings.h @@ -23,8 +23,8 @@ public: explicit ModSettings(QWidget *parent = nullptr); ~ModSettings(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; private slots: void on_modResolution_map_clicked(); @@ -38,5 +38,4 @@ private: private: Ui::ModSettings *ui; - const CMap * mapPointer = nullptr; }; diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp index 988e22ac4..a91691539 100644 --- a/mapeditor/mapsettings/rumorsettings.cpp +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "rumorsettings.h" #include "ui_rumorsettings.h" +#include "../mapcontroller.h" RumorSettings::RumorSettings(QWidget *parent) : AbstractSettings(parent), @@ -23,9 +24,10 @@ RumorSettings::~RumorSettings() delete ui; } -void RumorSettings::initialize(const CMap & map) +void RumorSettings::initialize(MapController & c) { - for(auto & rumor : map.rumors) + AbstractSettings::initialize(c); + for(auto & rumor : controller->map()->rumors) { auto * item = new QListWidgetItem(QString::fromStdString(rumor.name)); item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text))); @@ -34,15 +36,15 @@ void RumorSettings::initialize(const CMap & map) } } -void RumorSettings::update(CMap & map) +void RumorSettings::update() { - map.rumors.clear(); + controller->map()->rumors.clear(); for(int i = 0; i < ui->rumors->count(); ++i) { Rumor rumor; rumor.name = ui->rumors->item(i)->text().toStdString(); rumor.text = ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString(); - map.rumors.push_back(rumor); + controller->map()->rumors.push_back(rumor); } } diff --git a/mapeditor/mapsettings/rumorsettings.h b/mapeditor/mapsettings/rumorsettings.h index 9a980f288..e31e9c08b 100644 --- a/mapeditor/mapsettings/rumorsettings.h +++ b/mapeditor/mapsettings/rumorsettings.h @@ -23,8 +23,8 @@ public: explicit RumorSettings(QWidget *parent = nullptr); ~RumorSettings(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; private slots: void on_message_textChanged(); diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index be9ffd168..e354baac2 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -10,7 +10,7 @@ #include "StdInc.h" #include "victoryconditions.h" #include "ui_victoryconditions.h" - +#include "../mapcontroller.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/constants/StringConstants.h" @@ -23,12 +23,12 @@ VictoryConditions::VictoryConditions(QWidget *parent) : ui->setupUi(this); } -void VictoryConditions::initialize(const CMap & map) +void VictoryConditions::initialize(MapController & c) { - mapPointer = ↦ + AbstractSettings::initialize(c); //victory message - ui->victoryMessageEdit->setText(QString::fromStdString(map.victoryMessage.toString())); + ui->victoryMessageEdit->setText(QString::fromStdString(controller->map()->victoryMessage.toString())); //victory conditions const std::array conditionStringsWin = { @@ -49,7 +49,7 @@ void VictoryConditions::initialize(const CMap & map) ui->standardVictoryCheck->setChecked(false); ui->onlyForHumansCheck->setChecked(false); - for(auto & ev : map.triggeredEvents) + for(auto & ev : controller->map()->triggeredEvents) { if(ev.effect.type == EventEffect::VICTORY) { @@ -98,7 +98,7 @@ void VictoryConditions::initialize(const CMap & map) assert(victorySelectWidget); auto idx = victoryTypeWidget->findData(int(json["objectType"].Integer())); victoryTypeWidget->setCurrentIndex(idx); - int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(townIdx >= 0) { auto idx = victorySelectWidget->findData(townIdx); @@ -112,7 +112,7 @@ void VictoryConditions::initialize(const CMap & map) assert(victoryTypeWidget); if(json["objectType"].Integer() == Obj::TOWN) { - int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(townIdx >= 0) { auto idx = victoryTypeWidget->findData(townIdx); @@ -128,7 +128,7 @@ void VictoryConditions::initialize(const CMap & map) assert(victoryTypeWidget); if(json["objectType"].Integer() == Obj::HERO) { - int heroIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int heroIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(heroIdx >= 0) { auto idx = victoryTypeWidget->findData(heroIdx); @@ -144,7 +144,7 @@ void VictoryConditions::initialize(const CMap & map) assert(victoryTypeWidget); assert(victorySelectWidget); victoryTypeWidget->setCurrentIndex(json["objectType"].Integer()); - int townIdx = getObjectByPos(*mapPointer, posFromJson(json["position"])); + int townIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(townIdx >= 0) { auto idx = victorySelectWidget->findData(townIdx); @@ -164,10 +164,10 @@ void VictoryConditions::initialize(const CMap & map) } } -void VictoryConditions::update(CMap & map) +void VictoryConditions::update() { //victory messages - map.victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); + controller->map()->victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); //victory conditions EventCondition victoryCondition(EventCondition::STANDARD_WIN); @@ -184,9 +184,9 @@ void VictoryConditions::update(CMap & map) //VICTORY if(ui->victoryComboBox->currentIndex() == 0) { - map.triggeredEvents.push_back(standardVictory); - map.victoryIconIndex = 11; - map.victoryMessage = MetaString::createFromTextID("core.vcdesc.0"); + controller->map()->triggeredEvents.push_back(standardVictory); + controller->map()->victoryIconIndex = 11; + controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc.0"); } else { @@ -197,8 +197,8 @@ void VictoryConditions::update(CMap & map) specialVictory.identifier = "specialVictory"; specialVictory.description.clear(); // TODO: display in quest window - map.victoryIconIndex = vicCondition; - map.victoryMessage = MetaString::createFromTextID("core.vcdesc." + std::to_string(vicCondition + 1)); + controller->map()->victoryIconIndex = vicCondition; + controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc." + std::to_string(vicCondition + 1)); switch(vicCondition) { @@ -240,7 +240,7 @@ void VictoryConditions::update(CMap & map) cond.objectType = victoryTypeWidget->currentData().toInt(); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) - cond.position = map.objects[townIdx]->pos; + cond.position = controller->map()->objects[townIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); specialVictory.onFulfill.appendTextID("core.genrltxt.282"); specialVictory.trigger = EventExpression(cond); @@ -252,7 +252,7 @@ void VictoryConditions::update(CMap & map) assert(victoryTypeWidget); cond.objectType = Obj::TOWN; int townIdx = victoryTypeWidget->currentData().toInt(); - cond.position = map.objects[townIdx]->pos; + cond.position = controller->map()->objects[townIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); specialVictory.onFulfill.appendTextID("core.genrltxt.249"); specialVictory.trigger = EventExpression(cond); @@ -264,7 +264,7 @@ void VictoryConditions::update(CMap & map) assert(victoryTypeWidget); cond.objectType = Obj::HERO; int heroIdx = victoryTypeWidget->currentData().toInt(); - cond.position = map.objects[heroIdx]->pos; + cond.position = controller->map()->objects[heroIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); specialVictory.onFulfill.appendTextID("core.genrltxt.252"); specialVictory.trigger = EventExpression(cond); @@ -277,7 +277,7 @@ void VictoryConditions::update(CMap & map) cond.objectType = victoryTypeWidget->currentData().toInt(); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) - cond.position = map.objects[townIdx]->pos; + cond.position = controller->map()->objects[townIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); specialVictory.onFulfill.appendTextID("core.genrltxt.292"); specialVictory.trigger = EventExpression(cond); @@ -300,11 +300,11 @@ void VictoryConditions::update(CMap & map) // if normal victory allowed - add one more quest if(ui->standardVictoryCheck->isChecked()) { - map.victoryMessage.appendRawString(" / "); - map.victoryMessage.appendTextID("core.vcdesc.0"); - map.triggeredEvents.push_back(standardVictory); + controller->map()->victoryMessage.appendRawString(" / "); + controller->map()->victoryMessage.appendTextID("core.vcdesc.0"); + controller->map()->triggeredEvents.push_back(standardVictory); } - map.triggeredEvents.push_back(specialVictory); + controller->map()->triggeredEvents.push_back(specialVictory); } } @@ -339,7 +339,7 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) case 0: { //EventCondition::HAVE_ARTIFACT victoryTypeWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < mapPointer->allowedArtifact.size(); ++i) + for(int i = 0; i < controller->map()->allowedArtifact.size(); ++i) victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); break; } @@ -383,37 +383,37 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) victorySelectWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victorySelectWidget); victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); - for(int i : getObjectIndexes(*mapPointer)) - victorySelectWidget->addItem(getTownName(*mapPointer, i).c_str(), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + victorySelectWidget->addItem(getTownName(*controller->map(), i).c_str(), QVariant::fromValue(i)); break; } case 4: { //EventCondition::CONTROL (Obj::TOWN) victoryTypeWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes(*mapPointer)) - victoryTypeWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); break; } case 5: { //EventCondition::DESTROY (Obj::HERO) victoryTypeWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes(*mapPointer)) - victoryTypeWidget->addItem(tr(getHeroName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); break; } case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) victoryTypeWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victoryTypeWidget); - for(int i = 0; i < mapPointer->allowedArtifact.size(); ++i) + for(int i = 0; i < controller->map()->allowedArtifact.size(); ++i) victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); victorySelectWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victorySelectWidget); - for(int i : getObjectIndexes(*mapPointer)) - victorySelectWidget->addItem(tr(getTownName(*mapPointer, i).c_str()), QVariant::fromValue(i)); + for(int i : getObjectIndexes(*controller->map())) + victorySelectWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); break; } @@ -433,3 +433,13 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) } } + +void VictoryConditions::onObjectSelect() +{ + +} + +void VictoryConditions::onObjectPicked(const CGObjectInstance * obj) +{ + +} diff --git a/mapeditor/mapsettings/victoryconditions.h b/mapeditor/mapsettings/victoryconditions.h index deba8f7a0..5754b28ce 100644 --- a/mapeditor/mapsettings/victoryconditions.h +++ b/mapeditor/mapsettings/victoryconditions.h @@ -23,17 +23,21 @@ public: explicit VictoryConditions(QWidget *parent = nullptr); ~VictoryConditions(); - void initialize(const CMap & map) override; - void update(CMap & map) override; + void initialize(MapController & map) override; + void update() override; + +public slots: + void onObjectSelect(); + void onObjectPicked(const CGObjectInstance *); private slots: void on_victoryComboBox_currentIndexChanged(int index); private: Ui::VictoryConditions *ui; - const CMap * mapPointer = nullptr; QComboBox * victoryTypeWidget = nullptr; QComboBox * victorySelectWidget = nullptr; QLineEdit * victoryValueWidget = nullptr; + QToolButton * pickObjectButton = nullptr; }; From 6a0b26d40209295c05d51f1e6b86dea123c8482d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 11 Sep 2023 20:16:33 +0200 Subject: [PATCH 0383/1248] Object picker for victory/loss conditions --- mapeditor/mapsettings/loseconditions.cpp | 43 ++++++ mapeditor/mapsettings/victoryconditions.cpp | 138 +++++++++++++++++--- 2 files changed, 163 insertions(+), 18 deletions(-) diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index 4d15a06fe..017ad9e78 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -279,10 +279,53 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) void LoseConditions::onObjectSelect() { + int loseCondition = ui->loseComboBox->currentIndex() - 1; + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + switch(loseCondition) + { + case 0: { //EventCondition::CONTROL (Obj::TOWN) + l.highlight(); + break; + } + + case 1: { //EventCondition::CONTROL (Obj::HERO) + l.highlight(); + break; + } + default: + return; + } + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &LoseConditions::onObjectPicked); + } + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); } void LoseConditions::onObjectPicked(const CGObjectInstance * obj) { + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->show(); + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &LoseConditions::onObjectPicked); + } + + if(!obj) //discarded + return; + + for(int i = 0; i < loseTypeWidget->count(); ++i) + { + auto data = controller->map()->objects.at(loseTypeWidget->itemData(i).toInt()); + if(data == obj) + { + loseTypeWidget->setCurrentIndex(i); + break; + } + } } diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index e354baac2..248b637e1 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -13,6 +13,7 @@ #include "../mapcontroller.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/constants/StringConstants.h" +#include "../../lib/mapObjects/CGCreature.h" #include "../inspector/townbulidingswidget.h" //to convert BuildingID to string @@ -31,7 +32,7 @@ void VictoryConditions::initialize(MapController & c) ui->victoryMessageEdit->setText(QString::fromStdString(controller->map()->victoryMessage.toString())); //victory conditions - const std::array conditionStringsWin = { + const std::array conditionStringsWin = { QT_TR_NOOP("No special victory"), QT_TR_NOOP("Capture artifact"), QT_TR_NOOP("Hire creatures"), @@ -39,7 +40,8 @@ void VictoryConditions::initialize(MapController & c) QT_TR_NOOP("Construct building"), QT_TR_NOOP("Capture town"), QT_TR_NOOP("Defeat hero"), - QT_TR_NOOP("Transport artifact") + QT_TR_NOOP("Transport artifact"), + QT_TR_NOOP("Kill monster") }; for(auto & s : conditionStringsWin) @@ -124,10 +126,10 @@ void VictoryConditions::initialize(MapController & c) } case EventCondition::DESTROY: { - ui->victoryComboBox->setCurrentIndex(6); - assert(victoryTypeWidget); if(json["objectType"].Integer() == Obj::HERO) { + ui->victoryComboBox->setCurrentIndex(6); + assert(victoryTypeWidget); int heroIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); if(heroIdx >= 0) { @@ -135,7 +137,17 @@ void VictoryConditions::initialize(MapController & c) victoryTypeWidget->setCurrentIndex(idx); } } - //TODO: support control other objects (monsters) + if(json["objectType"].Integer() == Obj::MONSTER) + { + ui->victoryComboBox->setCurrentIndex(8); + assert(victoryTypeWidget); + int monsterIdx = getObjectByPos(*controller->map(), posFromJson(json["position"])); + if(monsterIdx >= 0) + { + auto idx = victoryTypeWidget->findData(monsterIdx); + victoryTypeWidget->setCurrentIndex(idx); + } + } break; } @@ -283,6 +295,18 @@ void VictoryConditions::update() specialVictory.trigger = EventExpression(cond); break; } + + case 7: { + EventCondition cond(EventCondition::DESTROY); + assert(victoryTypeWidget); + cond.objectType = Obj::MONSTER; + int monsterIdx = victoryTypeWidget->currentData().toInt(); + cond.position = controller->map()->objects[monsterIdx]->pos; + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); + specialVictory.onFulfill.appendTextID("core.genrltxt.286"); + specialVictory.trigger = EventExpression(cond); + break; + } } @@ -318,9 +342,11 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) delete victoryTypeWidget; delete victoryValueWidget; delete victorySelectWidget; + delete pickObjectButton; victoryTypeWidget = nullptr; victoryValueWidget = nullptr; victorySelectWidget = nullptr; + pickObjectButton = nullptr; if(index == 0) { @@ -385,6 +411,10 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) victorySelectWidget->addItem("Any town", QVariant::fromValue(-1)); for(int i : getObjectIndexes(*controller->map())) victorySelectWidget->addItem(getTownName(*controller->map(), i).c_str(), QVariant::fromValue(i)); + + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); break; } @@ -393,6 +423,9 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) ui->victoryParamsLayout->addWidget(victoryTypeWidget); for(int i : getObjectIndexes(*controller->map())) victoryTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); break; } @@ -401,6 +434,9 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) ui->victoryParamsLayout->addWidget(victoryTypeWidget); for(int i : getObjectIndexes(*controller->map())) victoryTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); break; } @@ -409,37 +445,103 @@ void VictoryConditions::on_victoryComboBox_currentIndexChanged(int index) ui->victoryParamsLayout->addWidget(victoryTypeWidget); for(int i = 0; i < controller->map()->allowedArtifact.size(); ++i) victoryTypeWidget->addItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated()), QVariant::fromValue(i)); - + victorySelectWidget = new QComboBox; ui->victoryParamsLayout->addWidget(victorySelectWidget); for(int i : getObjectIndexes(*controller->map())) victorySelectWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); break; } - - - //TODO: support this vectory type - // in order to do that, need to implement finding creature by position - // selecting from map would be the best user experience - /*case 7: { //EventCondition::DESTROY (Obj::MONSTER) + + case 7: { //EventCondition::DESTROY (Obj::MONSTER) victoryTypeWidget = new QComboBox; - ui->loseParamsLayout->addWidget(victoryTypeWidget); - for(int i : getObjectIndexes(*mapPointer)) - victoryTypeWidget->addItem(tr(getMonsterName(i).c_str()), QVariant::fromValue(i)); + ui->victoryParamsLayout->addWidget(victoryTypeWidget); + for(int i : getObjectIndexes(*controller->map())) + victoryTypeWidget->addItem(tr(getMonsterName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + pickObjectButton = new QToolButton; + connect(pickObjectButton, &QToolButton::clicked, this, &VictoryConditions::onObjectSelect); + ui->victoryParamsLayout->addWidget(pickObjectButton); break; - }*/ - - + } } } void VictoryConditions::onObjectSelect() { + int vicConditions = ui->victoryComboBox->currentIndex() - 1; + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + switch(vicConditions) + { + case 3: { //EventCondition::HAVE_BUILDING + l.highlight(); + break; + } + + case 4: { //EventCondition::CONTROL (Obj::TOWN) + l.highlight(); + break; + } + + case 5: { //EventCondition::DESTROY (Obj::HERO) + l.highlight(); + break; + } + + case 6: { //EventCondition::TRANSPORT (Obj::ARTEFACT) + l.highlight(); + break; + } + + case 7: { //EventCondition::DESTROY (Obj::MONSTER) + l.highlight(); + break; + } + default: + return; + } + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &VictoryConditions::onObjectPicked); + } + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->hide(); } void VictoryConditions::onObjectPicked(const CGObjectInstance * obj) { + dynamic_cast(parent()->parent()->parent()->parent()->parent()->parent()->parent())->show(); + for(int lvl : {0, 1}) + { + auto & l = controller->scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &VictoryConditions::onObjectPicked); + } + + if(!obj) //discarded + return; + + int vicConditions = ui->victoryComboBox->currentIndex() - 1; + QComboBox * w = victoryTypeWidget; + if(vicConditions == 3 || vicConditions == 6) + w = victorySelectWidget; + + for(int i = 0; i < w->count(); ++i) + { + if(w->itemData(i).toInt() < 0) + continue; + + auto data = controller->map()->objects.at(w->itemData(i).toInt()); + if(data == obj) + { + w->setCurrentIndex(i); + break; + } + } } From 8639d5ff7ce60838bb4a8159f80bfb050a8e12fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20=C4=8Cern=C3=BD?= Date: Tue, 12 Sep 2023 16:27:43 +0200 Subject: [PATCH 0384/1248] Remove dot in english.json for consistency --- Mods/vcmi/config/vcmi/english.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index b9541a459..b6ebf37a7 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -105,7 +105,7 @@ "vcmi.adventureOptions.mapScrollSpeed6.hover": "", "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow", "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous", "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", From 82eb2f8a2b3cde8bf1485a12f405ec94b015e871 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:30:48 +0300 Subject: [PATCH 0385/1248] Artifact assembling changes --- client/widgets/CArtifactHolder.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 4 +- client/widgets/CWindowWithArtifacts.cpp | 17 +++---- client/widgets/CWindowWithArtifacts.h | 2 +- lib/ArtifactUtils.cpp | 21 ++------ lib/ArtifactUtils.h | 2 +- lib/CArtHandler.cpp | 6 --- lib/CArtHandler.h | 1 - lib/NetPacksLib.cpp | 68 +++++++++++++++++-------- server/CGameHandler.cpp | 16 +++--- 10 files changed, 74 insertions(+), 65 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index b53237c9a..d0d2d5031 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -264,7 +264,7 @@ bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const Artif if(hero->tempOwner != LOCPLINT->playerID) return false; - auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId(), ArtifactUtils::isSlotEquipment(slot)); + auto assemblyPossibilities = ArtifactUtils::assemblyPossibilities(hero, art->getTypeId()); if(!assemblyPossibilities.empty()) { auto askThread = new boost::thread([hero, art, slot, assemblyPossibilities]() -> void diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index b8d175226..d184f69ea 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -261,8 +261,10 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit { arts.insert(std::pair(combinedArt, 0)); for(const auto part : combinedArt->getConstituents()) - if(artSet.hasArt(part->getId(), true)) + { + if(artSet.hasArt(part->getId(), false)) arts.at(combinedArt)++; + } } artPlace->addCombinedArtInfo(arts); } diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 7725c16c3..8f7b951bb 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -244,7 +244,7 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns void CWindowWithArtifacts::artifactRemoved(const ArtifactLocation & artLoc) { - updateSlots(artLoc.slot); + updateSlots(); } void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) @@ -310,26 +310,23 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const void CWindowWithArtifacts::artifactDisassembled(const ArtifactLocation & artLoc) { - updateSlots(artLoc.slot); + updateSlots(); } void CWindowWithArtifacts::artifactAssembled(const ArtifactLocation & artLoc) { markPossibleSlots(); - updateSlots(artLoc.slot); + updateSlots(); } -void CWindowWithArtifacts::updateSlots(const ArtifactPosition & slot) +void CWindowWithArtifacts::updateSlots() { - auto updateSlotBody = [slot](auto artSetWeak) -> void + auto updateSlotBody = [](auto artSetWeak) -> void { if(const auto artSetPtr = artSetWeak.lock()) { - if(ArtifactUtils::isSlotEquipment(slot)) - artSetPtr->updateWornSlots(); - else if(ArtifactUtils::isSlotBackpack(slot)) - artSetPtr->updateBackpackSlots(); - + artSetPtr->updateWornSlots(); + artSetPtr->updateBackpackSlots(); artSetPtr->redraw(); } }; diff --git a/client/widgets/CWindowWithArtifacts.h b/client/widgets/CWindowWithArtifacts.h index 85067e244..976b1a226 100644 --- a/client/widgets/CWindowWithArtifacts.h +++ b/client/widgets/CWindowWithArtifacts.h @@ -43,7 +43,7 @@ private: std::vector artSets; CloseCallback closeCallback; - void updateSlots(const ArtifactPosition & slot); + void updateSlots(); std::optional> getState(); std::optional findAOHbyRef(CArtifactsOfHeroBase & artsInst); void markPossibleSlots(); diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index eeb94c2c6..24bd404f3 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -116,7 +116,7 @@ DLL_LINKAGE bool ArtifactUtils::isBackpackFreeSlots(const CArtifactSet * target, } DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( - const CArtifactSet * artSet, const ArtifactID & aid, bool equipped) + const CArtifactSet * artSet, const ArtifactID & aid) { std::vector arts; const auto * art = aid.toArtifact(); @@ -130,23 +130,10 @@ DLL_LINKAGE std::vector ArtifactUtils::assemblyPossibilities( for(const auto constituent : artifact->getConstituents()) //check if all constituents are available { - if(equipped) + if(!artSet->hasArt(constituent->getId(), false, false, false)) { - // Search for equipped arts - if(!artSet->hasArt(constituent->getId(), true, false, false)) - { - possible = false; - break; - } - } - else - { - // Search in backpack - if(!artSet->hasArtBackpack(constituent->getId())) - { - possible = false; - break; - } + possible = false; + break; } } if(possible) diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index 2e70999b6..4b30f946d 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -36,7 +36,7 @@ namespace ArtifactUtils DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot); DLL_LINKAGE bool isSlotEquipment(const ArtifactPosition & slot); DLL_LINKAGE bool isBackpackFreeSlots(const CArtifactSet * target, const size_t reqSlots = 1); - DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid, bool equipped); + DLL_LINKAGE std::vector assemblyPossibilities(const CArtifactSet * artSet, const ArtifactID & aid); DLL_LINKAGE CArtifactInstance * createScroll(const SpellID & sid); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(CArtifact * art); DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 5eeebb5e4..f135c45fb 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -820,12 +820,6 @@ ArtifactPosition CArtifactSet::getArtPos(const ArtifactID & aid, bool onlyWorn, return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0]; } -ArtifactPosition CArtifactSet::getArtBackpackPos(const ArtifactID & aid) const -{ - const auto result = getBackpackArtPositions(aid); - return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0]; -} - std::vector CArtifactSet::getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const { std::vector result; diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index dad6e88e5..7201b95fa 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -260,7 +260,6 @@ public: /// (if more than one such artifact lower ID is returned) ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; ArtifactPosition getArtPos(const CArtifactInstance *art) const; - ArtifactPosition getArtBackpackPos(const ArtifactID & aid) const; std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; std::vector getBackpackArtPositions(const ArtifactID & aid) const; const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 4b5fd4740..4b2a57eeb 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1907,43 +1907,69 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) void AssembledArtifact::applyGs(CGameState *gs) { CArtifactSet * artSet = al.getHolderArtSet(); - [[maybe_unused]] const CArtifactInstance *transformedArt = al.getArt(); + const CArtifactInstance * transformedArt = al.getArt(); assert(transformedArt); - bool combineEquipped = !ArtifactUtils::isSlotBackpack(al.slot); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->artType->getId(), combineEquipped), [=](const CArtifact * art)->bool + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool { return art->getId() == builtArt->getId(); })); + const auto transformedArtSlot = artSet->getSlotByInstance(transformedArt); auto * combinedArt = new CArtifactInstance(builtArt); gs->map->addNewArtifactInstance(combinedArt); - // Retrieve all constituents - for(const CArtifact * constituent : builtArt->getConstituents()) - { - ArtifactPosition pos = combineEquipped ? artSet->getArtPos(constituent->getId(), true, false) : - artSet->getArtBackpackPos(constituent->getId()); - assert(pos != ArtifactPosition::PRE_FIRST); - CArtifactInstance * constituentInstance = artSet->getArt(pos); - //move constituent from hero to be part of new, combined artifact - constituentInstance->removeFrom(ArtifactLocation(al.artHolder, pos)); - if(combineEquipped) + // Find slots for all involved artifacts + std::vector slotsInvolved; + for(const auto constituent : builtArt->getConstituents()) + { + ArtifactPosition slot; + if(transformedArt->getTypeId() == constituent->getId()) + slot = transformedArtSlot; + else + slot = artSet->getArtPos(constituent->getId(), false, false); + + assert(slot != ArtifactPosition::PRE_FIRST); + slotsInvolved.emplace_back(slot); + } + std::sort(slotsInvolved.begin(), slotsInvolved.end(), std::greater<>()); + + // Find a slot for combined artifact + al.slot = transformedArtSlot; + for(const auto slot : slotsInvolved) + { + if(ArtifactUtils::isSlotEquipment(transformedArtSlot)) { + + if(ArtifactUtils::isSlotBackpack(slot)) + { + al.slot = ArtifactPosition::BACKPACK_START; + break; + } + if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), pos)) - al.slot = pos; - if(al.slot == pos) - pos = ArtifactPosition::PRE_FIRST; + && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), slot)) + al.slot = slot; } else { - al.slot = std::min(al.slot, pos); - pos = ArtifactPosition::PRE_FIRST; + if(ArtifactUtils::isSlotBackpack(slot)) + al.slot = std::min(al.slot, slot); } - combinedArt->addPart(constituentInstance, pos); } - //put new combined artifacts + // Delete parts from hero + for(const auto slot : slotsInvolved) + { + const auto constituentInstance = artSet->getArt(slot); + constituentInstance->removeFrom(ArtifactLocation(al.artHolder, slot)); + + if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) + combinedArt->addPart(constituentInstance, slot); + else + combinedArt->addPart(constituentInstance, ArtifactPosition::PRE_FIRST); + } + + // Put new combined artifacts combinedArt->putAt(al); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3ae1fdfd5..7493125a4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2787,7 +2787,7 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID * @param assembleTo If assemble is true, this represents the artifact ID of the combination * artifact to assemble to. Otherwise it's not used. */ -bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) { const CGHeroInstance * hero = getHero(heroID); const CArtifactInstance * destArtifact = hero->getArt(artifactSlot); @@ -2795,23 +2795,27 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition if(!destArtifact) COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); + const auto dstLoc = ArtifactLocation(hero, artifactSlot); if(assemble) { CArtifact * combinedArt = VLC->arth->objects[assembleTo]; if(!combinedArt->isCombined()) COMPLAIN_RET("assembleArtifacts: Artifact being attempted to assemble is not a combined artifacts!"); - if (!vstd::contains(ArtifactUtils::assemblyPossibilities(hero, destArtifact->getTypeId(), - ArtifactUtils::isSlotEquipment(artifactSlot)), combinedArt)) + if(!vstd::contains(ArtifactUtils::assemblyPossibilities(hero, destArtifact->getTypeId()), combinedArt)) { COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); } - + if(!destArtifact->canBePutAt(dstLoc) + && !destArtifact->canBePutAt(ArtifactLocation(hero, ArtifactPosition::BACKPACK_START))) + { + COMPLAIN_RET("assembleArtifacts: It's impossible to give the artholder requested artifact!"); + } if(ArtifactUtils::checkSpellbookIsNeeded(hero, assembleTo, artifactSlot)) giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); AssembledArtifact aa; - aa.al = ArtifactLocation(hero, artifactSlot); + aa.al = dstLoc; aa.builtArt = combinedArt; sendAndApply(&aa); } @@ -2825,7 +2829,7 @@ bool CGameHandler::assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition COMPLAIN_RET("assembleArtifacts: Artifact being attempted to disassemble but backpack is full!"); DisassembledArtifact da; - da.al = ArtifactLocation(hero, artifactSlot); + da.al = dstLoc; sendAndApply(&da); } From 89409da0c0b10ec6d02a3be949348c79a34264b7 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Tue, 12 Sep 2023 18:34:02 +0300 Subject: [PATCH 0386/1248] Reduced number of assembling asks --- server/CGameHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7493125a4..032eac0d4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2683,7 +2683,7 @@ bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocat } MoveArtifact ma(&src, &dst); - if(dst.slot == ArtifactPosition::TRANSITION_POS) + if(src.artHolder == dst.artHolder) ma.askAssemble = false; sendAndApply(&ma); } From 9cf274362e20b784e403bf36660b5d5e7c5a3d62 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 20:28:54 +0200 Subject: [PATCH 0387/1248] small folder optimation --- client/lobby/SelectionTab.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index e22c936d3..34375db26 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -345,7 +345,10 @@ void SelectionTab::clickDouble(const Point & cursorPosition) return; if(itemIndex >= 0 && curItems[itemIndex]->isFolder) + { + select(position); return; + } if(getLine() != -1) //double clicked scenarios list { @@ -370,6 +373,8 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) GH.windows().createAndPushWindow(text, ResourcePath(curItems[py]->fileURI), tabType); } + else + CRClickPopup::createAndPush(curItems[py]->folderName); } auto SelectionTab::checkSubfolder(std::string path) From ef3f0174ddac5ef5cac9c4ce4d8878f70e4c8a96 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 13 Sep 2023 01:40:07 +0200 Subject: [PATCH 0388/1248] Rewardable seer hut and quest gate --- lib/mapObjects/CQuest.cpp | 180 ++++-------------------- lib/mapObjects/CQuest.h | 21 ++- lib/mapObjects/CRewardableObject.h | 2 +- lib/mapping/MapFormatH3M.cpp | 132 +++++++++-------- lib/rmg/modificators/TreasurePlacer.cpp | 28 ++-- 5 files changed, 127 insertions(+), 236 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 22cfe4df6..0e44e999f 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -378,7 +378,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const } } -void CQuest::getCompletionText(MetaString &iwText, std::vector &components, bool isCustom, const CGHeroInstance * h) const +void CQuest::getCompletionText(MetaString &iwText) const { iwText.appendRawString(completedText); switch(missionType) @@ -567,11 +567,17 @@ void CGSeerHut::init(CRandomGenerator & rand) seerName = VLC->generaltexth->translate(seerNameID); quest->textOption = rand.nextInt(2); quest->completedOption = rand.nextInt(1, 3); + + configuration.canRefuse = true; + configuration.visitMode = Rewardable::EVisitMode::VISIT_ONCE; + configuration.selectMode = Rewardable::ESelectMode::SELECT_PLAYER; } void CGSeerHut::initObj(CRandomGenerator & rand) { init(rand); + + CRewardableObject::initObj(rand); quest->progress = CQuest::NOT_ACTIVE; if(quest->missionType) @@ -590,6 +596,10 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->progress = CQuest::COMPLETE; quest->firstVisitText = VLC->generaltexth->seerEmpty[quest->completedOption]; } + + quest->getCompletionText(configuration.onSelect); + for(auto & i : configuration.info) + quest->getCompletionText(i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const @@ -649,44 +659,6 @@ void IQuestObject::afterAddToMapCommon(CMap * map) const map->addNewQuestInstance(quest); } -void CGSeerHut::getCompletionText(MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h) const -{ - quest->getCompletionText (text, components, isCustom, h); - switch(rewardType) - { - case EXPERIENCE: - components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(rVal)), 0); - break; - case MANA_POINTS: - components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, rVal, 0); - break; - case MORALE_BONUS: - components.emplace_back(Component::EComponentType::MORALE, 0, rVal, 0); - break; - case LUCK_BONUS: - components.emplace_back(Component::EComponentType::LUCK, 0, rVal, 0); - break; - case RESOURCES: - components.emplace_back(Component::EComponentType::RESOURCE, rID, rVal, 0); - break; - case PRIMARY_SKILL: - components.emplace_back(Component::EComponentType::PRIM_SKILL, rID, rVal, 0); - break; - case SECONDARY_SKILL: - components.emplace_back(Component::EComponentType::SEC_SKILL, rID, rVal, 0); - break; - case ARTIFACT: - components.emplace_back(Component::EComponentType::ARTIFACT, rID, 0, 0); - break; - case SPELL: - components.emplace_back(Component::EComponentType::SPELL, rID, 0, 0); - break; - case CREATURE: - components.emplace_back(Component::EComponentType::CREATURE, rID, rVal, 0); - break; - } -} - void CGSeerHut::setPropertyDer (ui8 what, ui32 val) { switch(what) @@ -699,6 +671,7 @@ void CGSeerHut::setPropertyDer (ui8 what, ui32 val) void CGSeerHut::newTurn(CRandomGenerator & rand) const { + CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); @@ -738,12 +711,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const } if(!failRequirements) // propose completion, also on first visit { - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - - getCompletionText (bd.text, bd.components, isCustom, h); - - cb->showBlockingDialog (&bd); + CRewardableObject::onHeroVisit(h); return; } } @@ -788,108 +756,9 @@ int CGSeerHut::checkDirection() const } } -void CGSeerHut::finishQuest(const CGHeroInstance * h, ui32 accept) const +void CGSeerHut::completeQuest() const //reward { - if (accept) - { - switch (quest->missionType) - { - case CQuest::MISSION_ART: - for(auto & elem : quest->m5arts) - { - if(h->hasArt(elem)) - { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); - } - else - { - const auto * assembly = h->getAssemblyByConstituent(elem); - assert(assembly); - auto parts = assembly->getPartsInfo(); - - // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); - - // Disassemble this backpack artifact - for(const auto & ci : parts) - { - if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); - } - } - } - break; - case CQuest::MISSION_ARMY: - cb->takeCreatures(h->id, quest->m6creatures); - break; - case CQuest::MISSION_RESOURCES: - for (int i = 0; i < 7; ++i) - { - cb->giveResource(h->getOwner(), static_cast(i), -static_cast(quest->m7resources[i])); - } - break; - default: - break; - } - cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete - completeQuest(h); //make sure to remove QuestGuard at the very end - } -} - -void CGSeerHut::completeQuest (const CGHeroInstance * h) const //reward -{ - switch (rewardType) - { - case EXPERIENCE: - { - TExpType expVal = h->calculateXp(rVal); - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, expVal, false); - break; - } - case MANA_POINTS: - { - cb->setManaPoints(h->id, h->mana+rVal); - break; - } - case MORALE_BONUS: case LUCK_BONUS: - { - Bonus hb(BonusDuration::ONE_WEEK, (rewardType == 3 ? BonusType::MORALE : BonusType::LUCK), - BonusSource::OBJECT, rVal, h->id.getNum(), "", -1); - GiveBonus gb; - gb.id = h->id.getNum(); - gb.bonus = hb; - cb->giveHeroBonus(&gb); - } - break; - case RESOURCES: - cb->giveResource(h->getOwner(), static_cast(rID), rVal); - break; - case PRIMARY_SKILL: - cb->changePrimSkill(h, static_cast(rID), rVal, false); - break; - case SECONDARY_SKILL: - cb->changeSecSkill(h, SecondarySkill(rID), rVal, false); - break; - case ARTIFACT: - cb->giveHeroNewArtifact(h, VLC->arth->objects[rID],ArtifactPosition::FIRST_AVAILABLE); - break; - case SPELL: - { - std::set spell; - spell.insert (SpellID(rID)); - cb->changeSpells(h, true, spell); - } - break; - case CREATURE: - { - CCreatureSet creatures; - creatures.setCreature(SlotID(0), CreatureID(rID), rVal); - cb->giveCreatures(this, h, creatures, false); - } - break; - default: - break; - } + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete } const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const @@ -912,7 +781,9 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { - finishQuest(hero, answer); + CRewardableObject::blockingDialogAnswered(hero, answer); + if(answer) + completeQuest(); } void CGSeerHut::afterAddToMap(CMap* map) @@ -922,7 +793,7 @@ void CGSeerHut::afterAddToMap(CMap* map) void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) { - static const std::map REWARD_MAP = + /*static const std::map REWARD_MAP = { {NOTHING, ""}, {EXPERIENCE, "experience"}, @@ -949,15 +820,16 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) {"artifact", ARTIFACT}, {"spell", SPELL}, {"creature", CREATURE} - }; + };*/ //quest and reward quest->serializeJson(handler, "quest"); + CRewardableObject::serializeJsonOptions(handler); //only one reward is supported //todo: full reward format support after CRewardInfo integration - auto s = handler.enterStruct("reward"); + /*auto s = handler.enterStruct("reward"); std::string fullIdentifier; std::string metaTypeName; std::string scope; @@ -1079,7 +951,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) } } handler.serializeInt(fullIdentifier, rVal); - } + }*/ } void CGQuestGuard::init(CRandomGenerator & rand) @@ -1087,9 +959,13 @@ void CGQuestGuard::init(CRandomGenerator & rand) blockVisit = true; quest->textOption = rand.nextInt(3, 5); quest->completedOption = rand.nextInt(4, 5); + + configuration.info.push_back({}); + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.canRefuse = true; } -void CGQuestGuard::completeQuest(const CGHeroInstance *h) const +void CGQuestGuard::completeQuest() const { cb->removeObject(this); } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index e60135c56..963f29499 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -9,7 +9,7 @@ */ #pragma once -#include "CArmedInstance.h" +#include "CRewardableObject.h" #include "../ResourceSet.h" VCMI_LIB_NAMESPACE_BEGIN @@ -80,7 +80,7 @@ public: static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); virtual bool checkQuest (const CGHeroInstance * h) const; //determines whether the quest is complete or not virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; - virtual void getCompletionText (MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h = nullptr) const; + virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest (const CGHeroInstance * h) const {}; virtual void addReplacements(MetaString &out, const std::string &base) const; @@ -138,13 +138,9 @@ protected: void afterAddToMapCommon(CMap * map) const; }; -class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward +class DLL_LINKAGE CGSeerHut : public CRewardableObject, public IQuestObject //army is used when giving reward { public: - enum ERewardType {NOTHING, EXPERIENCE, MANA_POINTS, MORALE_BONUS, LUCK_BONUS, RESOURCES, PRIMARY_SKILL, SECONDARY_SKILL, ARTIFACT, SPELL, CREATURE}; - ERewardType rewardType = ERewardType::NOTHING; - si32 rID = -1; //reward ID - si32 rVal = -1; //reward value std::string seerName; void initObj(CRandomGenerator & rand) override; @@ -159,9 +155,8 @@ public: const CGHeroInstance *getHeroToKill(bool allowNull = false) const; const CGCreature *getCreatureToKill(bool allowNull = false) const; void getRolloverText (MetaString &text, bool onHover) const; - void getCompletionText(MetaString &text, std::vector &components, bool isCustom, const CGHeroInstance * h = nullptr) const; void finishQuest (const CGHeroInstance * h, ui32 accept) const; //common for both objects - virtual void completeQuest (const CGHeroInstance * h) const; + virtual void completeQuest() const; void afterAddToMap(CMap * map) override; @@ -169,9 +164,9 @@ public: { h & static_cast(*this); h & static_cast(*this); - h & rewardType; - h & rID; - h & rVal; + //h & rewardType; + //h & rID; + //h & rVal; h & seerName; } protected: @@ -186,7 +181,7 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: void init(CRandomGenerator & rand) override; - void completeQuest (const CGHeroInstance * h) const override; + void completeQuest() const override; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index ca8f918db..47ce27932 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../mapObjects/CArmedInstance.h" +#include "CArmedInstance.h" #include "../rewardable/Interface.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 0b241c68c..a3b4b16df 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1842,77 +1842,89 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) if(hut->quest->missionType) { - auto rewardType = static_cast(reader->readUInt8()); - hut->rewardType = rewardType; + auto rewardType = reader->readUInt8(); + Rewardable::Reward reward; switch(rewardType) { - case CGSeerHut::EXPERIENCE: - { - hut->rVal = reader->readUInt32(); - break; - } - case CGSeerHut::MANA_POINTS: - { - hut->rVal = reader->readUInt32(); - break; - } - case CGSeerHut::MORALE_BONUS: - { - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::LUCK_BONUS: - { - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::RESOURCES: - { - hut->rID = reader->readUInt8(); - hut->rVal = reader->readUInt32(); - - assert(hut->rID < features.resourcesCount); - assert((hut->rVal & 0x00ffffff) == hut->rVal); - break; - } - case CGSeerHut::PRIMARY_SKILL: - { - hut->rID = reader->readUInt8(); - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::SECONDARY_SKILL: - { - hut->rID = reader->readSkill(); - hut->rVal = reader->readUInt8(); - break; - } - case CGSeerHut::ARTIFACT: - { - hut->rID = reader->readArtifact(); - break; - } - case CGSeerHut::SPELL: - { - hut->rID = reader->readSpell(); - break; - } - case CGSeerHut::CREATURE: - { - hut->rID = reader->readCreature(); - hut->rVal = reader->readUInt16(); - break; - } - case CGSeerHut::NOTHING: + case 0: //NOTHING { // no-op break; } + case 1: //EXPERIENCE + { + reward.heroExperience = reader->readUInt32(); + break; + } + case 2: //MANA POINTS: + { + reward.manaDiff = reader->readUInt32(); + break; + } + case 3: //MORALE_BONUS + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), hut->id); + break; + } + case 4: //LUCK_BONUS + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), hut->id); + break; + } + case 5: //RESOURCES + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt32(); + + assert(rId < features.resourcesCount); + assert((rVal & 0x00ffffff) == rVal); + + reward.resources[rId] = rVal; + break; + } + case 6: //PRIMARY_SKILL + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt8(); + + reward.primary.at(rId) = rVal; + break; + } + case 7: //SECONDARY_SKILL + { + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; + break; + } + case 8: //ARTIFACT + { + reward.artifacts.push_back(reader->readArtifact()); + break; + } + case 9: //SPELL + { + reward.spells.push_back(reader->readSpell()); + break; + } + case 10: //CREATURE + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + break; + } default: { assert(0); } } + + hut->configuration.info.push_back({}); + hut->configuration.info.back().reward = reward; + hut->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; } else { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 3ba86e19f..0addaa481 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -441,9 +441,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - obj->rewardType = CGSeerHut::CREATURE; - obj->rID = creature->getId(); - obj->rVal = creaturesAmount; + + Rewardable::Reward reward; + reward.creatures.emplace_back(creature->getId(), creaturesAmount); + obj->configuration.info.push_back({}); + obj->configuration.info.back().reward = reward; + obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->quest->missionType = CQuest::MISSION_ART; @@ -490,10 +493,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - - obj->rewardType = CGSeerHut::EXPERIENCE; - obj->rID = 0; //unitialized? - obj->rVal = generator.getConfig().questRewardValues[i]; + + Rewardable::Reward reward; + reward.heroExperience = generator.getConfig().questRewardValues[i]; + obj->configuration.info.push_back({}); + obj->configuration.info.back().reward = reward; + obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); @@ -513,9 +518,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - obj->rewardType = CGSeerHut::RESOURCES; - obj->rID = GameResID(EGameResID::GOLD); - obj->rVal = generator.getConfig().questRewardValues[i]; + + Rewardable::Reward reward; + reward.resources[EGameResID::GOLD] = generator.getConfig().questRewardValues[i]; + obj->configuration.info.push_back({}); + obj->configuration.info.back().reward = reward; + obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); From 96d6a48f010918787c96567486eb90b9b8af8866 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 13 Sep 2023 02:14:35 +0200 Subject: [PATCH 0389/1248] Backward compatible json serialization --- lib/mapObjects/CQuest.cpp | 184 ++++++++++++-------------------------- 1 file changed, 55 insertions(+), 129 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 0e44e999f..6485c447a 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -793,103 +793,30 @@ void CGSeerHut::afterAddToMap(CMap* map) void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) { - /*static const std::map REWARD_MAP = - { - {NOTHING, ""}, - {EXPERIENCE, "experience"}, - {MANA_POINTS, "mana"}, - {MORALE_BONUS, "morale"}, - {LUCK_BONUS, "luck"}, - {RESOURCES, "resource"}, - {PRIMARY_SKILL, "primarySkill"}, - {SECONDARY_SKILL,"secondarySkill"}, - {ARTIFACT, "artifact"}, - {SPELL, "spell"}, - {CREATURE, "creature"} - }; - - static const std::map REWARD_RMAP = - { - {"experience", EXPERIENCE}, - {"mana", MANA_POINTS}, - {"morale", MORALE_BONUS}, - {"luck", LUCK_BONUS}, - {"resource", RESOURCES}, - {"primarySkill", PRIMARY_SKILL}, - {"secondarySkill",SECONDARY_SKILL}, - {"artifact", ARTIFACT}, - {"spell", SPELL}, - {"creature", CREATURE} - };*/ - //quest and reward quest->serializeJson(handler, "quest"); - CRewardableObject::serializeJsonOptions(handler); - - //only one reward is supported - //todo: full reward format support after CRewardInfo integration - - /*auto s = handler.enterStruct("reward"); - std::string fullIdentifier; - std::string metaTypeName; - std::string scope; - std::string identifier; if(handler.saving) { - si32 amount = rVal; - - metaTypeName = REWARD_MAP.at(rewardType); - switch (rewardType) - { - case NOTHING: - break; - case EXPERIENCE: - case MANA_POINTS: - case MORALE_BONUS: - case LUCK_BONUS: - identifier.clear(); - break; - case RESOURCES: - identifier = GameConstants::RESOURCE_NAMES[rID]; - break; - case PRIMARY_SKILL: - identifier = NPrimarySkill::names[rID]; - break; - case SECONDARY_SKILL: - identifier = CSkillHandler::encodeSkill(rID); - break; - case ARTIFACT: - identifier = ArtifactID(rID).toArtifact(VLC->artifacts())->getJsonKey(); - amount = 1; - break; - case SPELL: - identifier = SpellID(rID).toSpell(VLC->spells())->getJsonKey(); - amount = 1; - break; - case CREATURE: - identifier = CreatureID(rID).toCreature(VLC->creatures())->getJsonKey(); - break; - default: - assert(false); - break; - } - if(rewardType != NOTHING) - { - fullIdentifier = ModUtility::makeFullIdentifier(scope, metaTypeName, identifier); - handler.serializeInt(fullIdentifier, amount); - } + CRewardableObject::serializeJsonOptions(handler); } else { - rewardType = NOTHING; - + auto s = handler.enterStruct("reward"); + std::string fullIdentifier; + std::string metaTypeName; + std::string scope; + std::string identifier; + const JsonNode & rewardsJson = handler.getCurrent(); fullIdentifier.clear(); if(rewardsJson.Struct().empty()) + { + CRewardableObject::serializeJsonOptions(handler); return; + } else { auto iter = rewardsJson.Struct().begin(); @@ -897,61 +824,60 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) } ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - auto it = REWARD_RMAP.find(metaTypeName); - - if(it == REWARD_RMAP.end()) + if(!std::set{"resource", "primarySkill", "secondarySkill", "artifact", "spell", "creature", "experience", "mana", "morale", "luck"}.count(metaTypeName)) { - logGlobal->error("%s: invalid metatype in reward item %s", instanceName, fullIdentifier); + CRewardableObject::serializeJsonOptions(handler); return; } - else + + //backward compatibility + int val = 0; + handler.serializeInt(fullIdentifier, val); + + Rewardable::Reward reward; + if(metaTypeName == "experience") + reward.heroExperience = val; + if(metaTypeName == "mana") + reward.manaDiff = val; + if(metaTypeName == "morale") + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + if(metaTypeName == "luck") + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + if(metaTypeName == "resource") { - rewardType = it->second; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.resources[rawId] = val; } - - bool doRequest = false; - - switch (rewardType) + if(metaTypeName == "primarySkill") { - case NOTHING: - return; - case EXPERIENCE: - case MANA_POINTS: - case MORALE_BONUS: - case LUCK_BONUS: - break; - case PRIMARY_SKILL: - doRequest = true; - break; - case RESOURCES: - case SECONDARY_SKILL: - case ARTIFACT: - case SPELL: - case CREATURE: - doRequest = true; - break; - default: - assert(false); - break; + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.primary.at(rawId) = val; } - - if(doRequest) + if(metaTypeName == "secondarySkill") { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); - - if(rawId) - { - rID = rawId.value(); - } - else - { - rewardType = NOTHING;//fallback in case of error - return; - } + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.secondary[rawId] = val; } - handler.serializeInt(fullIdentifier, rVal); - }*/ + if(metaTypeName == "artifact") + { + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.artifacts.push_back(rawId); + } + if(metaTypeName == "spell") + { + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.spells.push_back(rawId); + } + if(metaTypeName == "creature") + { + auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); + reward.creatures.emplace_back(rawId, val); + } + + configuration.info.push_back({}); + configuration.info.back().reward = reward; + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + } } void CGQuestGuard::init(CRandomGenerator & rand) From abfd1394b7e099f9da697e4db778024a031f9db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20=C4=8Cern=C3=BD?= Date: Wed, 13 Sep 2023 08:19:20 +0200 Subject: [PATCH 0390/1248] Add dots to english.json for improved consistency --- Mods/vcmi/config/vcmi/english.json | 54 +++++++++++++++--------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index b6ebf37a7..d52eb4082 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -53,11 +53,11 @@ "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", "vcmi.settingsMainWindow.generalTab.hover" : "General", - "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior", + "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", "vcmi.settingsMainWindow.battleTab.hover" : "Battle", - "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles", + "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles.", "vcmi.settingsMainWindow.adventureTab.hover" : "Adventure Map", - "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes)", + "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes).", "vcmi.systemOptions.videoGroup" : "Video Settings", "vcmi.systemOptions.audioGroup" : "Audio Settings", @@ -73,55 +73,55 @@ "vcmi.systemOptions.resolutionMenu.hover" : "Select Resolution", "vcmi.systemOptions.resolutionMenu.help" : "Change in-game screen resolution.", "vcmi.systemOptions.scalingButton.hover" : "Interface Scaling: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface", + "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface.", "vcmi.systemOptions.scalingMenu.hover" : "Select Interface Scaling", "vcmi.systemOptions.scalingMenu.help" : "Change in-game interface scaling.", "vcmi.systemOptions.longTouchButton.hover" : "Long Touch Interval: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds", + "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds.", "vcmi.systemOptions.longTouchMenu.hover" : "Select Long Touch Interval", "vcmi.systemOptions.longTouchMenu.help" : "Change duration of long touch interval.", "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", "vcmi.systemOptions.framerateButton.hover" : "Show FPS", - "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window", + "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", "vcmi.adventureOptions.numericQuantities.hover" : "Numeric Creature Quantities", "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nShow the approximate quantities of enemy creatures in the numeric A-B format.", "vcmi.adventureOptions.forceMovementInfo.hover" : "Always Show Movement Cost", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information. (Instead of viewing it only while you hold down ALT key)", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information (Instead of viewing it only while you hold down ALT key).", "vcmi.adventureOptions.showGrid.hover" : "Show Grid", "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nShow the grid overlay, highlighting the borders between adventure map tiles.", "vcmi.adventureOptions.borderScroll.hover" : "Border Scrolling", "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.", "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", - "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", "vcmi.battleOptions.queueSizeSmallButton.hover": "SMALL", "vcmi.battleOptions.queueSizeBigButton.hover": "BIG", - "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue", - "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL", - "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels)", + "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL.", + "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow", - "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast", - "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous", + "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow.", + "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast.", + "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous.", "vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover", "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.", "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Show range limits for shooters", @@ -129,7 +129,7 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows", "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", @@ -171,21 +171,21 @@ "vcmi.logicalExpressions.noneOf" : "None of the following:", "vcmi.heroWindow.openCommander.hover" : "Open commander info window", - "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero", + "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", - "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management", + "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", - "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander", + "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", - "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander", + "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", "vcmi.creatureWindow.returnArtifact.hover" : "Return artifact", - "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack", + "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack.", "vcmi.questLog.hideComplete.hover" : "Hide complete quests", - "vcmi.questLog.hideComplete.help" : "Hide all completed quests", + "vcmi.questLog.hideComplete.help" : "Hide all completed quests.", "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", "vcmi.randomMapTab.widgets.templateLabel" : "Template", From 574047c55c5265f06b514aa3b4e3b4b14abb8f6c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 13 Sep 2023 09:18:46 +0200 Subject: [PATCH 0391/1248] Start making new serialization for rewardables --- lib/mapObjects/CRewardableObject.cpp | 5 ++++ lib/mapObjects/CRewardableObject.h | 2 ++ mapeditor/inspector/rewardswidget.cpp | 39 ++++++++++++++++++++++----- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 5d21cf278..8e0849f00 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -276,4 +276,9 @@ void CRewardableObject::initObj(CRandomGenerator & rand) CRewardableObject::CRewardableObject() {} +void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) +{ + +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 47ce27932..761e93702 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -31,6 +31,8 @@ protected: /// return true if this object was "cleared" before and no longer has rewards applicable to selected hero /// unlike wasVisited, this method uses information not available to player owner, for example, if object was cleared by another player before bool wasVisitedBefore(const CGHeroInstance * contextHero) const; + + void serializeJsonOptions(JsonSerializeFormat & handler) override; public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 54774da11..a20fd50c1 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -163,10 +163,21 @@ void RewardsWidget::obtainData() if(seerhut) { - switch(seerhut->rewardType) + for(auto & i : seerhut->configuration.info) + { + if(i.reward.heroExperience) + addReward(RewardType::EXPERIENCE, 0, i.reward.heroExperience); + if(i.reward.manaDiff) + addReward(RewardType::MANA, 0, i.reward.manaDiff); + for(auto & a : i.reward.artifacts) + addReward(RewardType::ARTIFACT, a.getNum(), 0); + for(auto & a : i.reward.creatures) + addReward(RewardType::CREATURE, a.getType()->getId().getNum(), a.getCount()); + } + /*switch(seerhut->rewardType) { case CGSeerHut::ERewardType::EXPERIENCE: - addReward(RewardType::EXPERIENCE, 0, seerhut->rVal); + break; case CGSeerHut::ERewardType::MANA_POINTS: @@ -182,7 +193,7 @@ void RewardsWidget::obtainData() break; case CGSeerHut::ERewardType::RESOURCES: - addReward(RewardType::RESOURCE, seerhut->rID, seerhut->rVal); + break; case CGSeerHut::ERewardType::PRIMARY_SKILL: @@ -207,7 +218,7 @@ void RewardsWidget::obtainData() default: break; - } + }*/ } } @@ -279,15 +290,29 @@ bool RewardsWidget::commitChanges() } if(seerhut) { + seerhut->configuration.info.clear(); for(int row = 0; row < rewards; ++row) { + Rewardable::Reward reward; haveRewards = true; int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); - seerhut->rewardType = CGSeerHut::ERewardType(typeId + 1); - seerhut->rID = listId; - seerhut->rVal = amount; + switch (typeId) { + case RewardType::EXPERIENCE: + reward.heroExperience = amount; + break; + + case RewardType::ARTIFACT: + reward.artifacts.push_back(listId); + break; + + case RewardType::CREATURE: + reward.creatures.emplace_back(listId, amount); + + default: + break; + } } } return haveRewards; From 06f01c3b825937f0797de4ca14ba2ad971cd9792 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 15 Sep 2023 10:06:06 +0200 Subject: [PATCH 0392/1248] Seerhut works as rewardable object --- lib/MetaString.cpp | 11 +++ lib/MetaString.h | 3 + lib/mapObjects/CQuest.cpp | 39 +++++------ lib/mapObjects/CRewardableObject.cpp | 4 +- lib/rewardable/Configuration.cpp | 27 ++++++++ lib/rewardable/Configuration.h | 8 ++- lib/rewardable/Interface.cpp | 5 ++ lib/rewardable/Interface.h | 2 + lib/rewardable/Limiter.cpp | 6 ++ lib/rewardable/Limiter.h | 2 + lib/rewardable/Reward.cpp | 96 +++++++++++++++++++++++++++ lib/rewardable/Reward.h | 2 + mapeditor/inspector/rewardswidget.cpp | 4 +- 13 files changed, 183 insertions(+), 26 deletions(-) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 78329a899..86a1badfe 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -19,6 +19,7 @@ #include "VCMI_Lib.h" #include "mapObjectConstructors/CObjectClassesHandler.h" #include "spells/CSpellHandler.h" +#include "serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -385,4 +386,14 @@ void MetaString::jsonDeserialize(const JsonNode & source) numbers.push_back(entry.Integer()); } +void MetaString::serializeJson(JsonSerializeFormat & handler) +{ + JsonNode attr; + if(handler.saving) + jsonSerialize(attr); + handler.serializeRaw("attributes", attr, std::nullopt); + if(!handler.saving) + jsonDeserialize(attr); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/MetaString.h b/lib/MetaString.h index c7329f629..2c6da6961 100644 --- a/lib/MetaString.h +++ b/lib/MetaString.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class JsonNode; class CreatureID; class CStackBasicDescriptor; +class JsonSerializeFormat; using TQuantity = si32; /// Strings classes that can be used as replacement in MetaString @@ -113,6 +114,8 @@ public: void jsonSerialize(JsonNode & dest) const; void jsonDeserialize(const JsonNode & dest); + + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler & h, const int version) { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 6485c447a..01d71bf19 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -802,35 +802,28 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) } else { - auto s = handler.enterStruct("reward"); - std::string fullIdentifier; - std::string metaTypeName; - std::string scope; - std::string identifier; - - const JsonNode & rewardsJson = handler.getCurrent(); - - fullIdentifier.clear(); - - if(rewardsJson.Struct().empty()) - { - CRewardableObject::serializeJsonOptions(handler); - return; - } - else - { - auto iter = rewardsJson.Struct().begin(); - fullIdentifier = iter->first; - } - - ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - if(!std::set{"resource", "primarySkill", "secondarySkill", "artifact", "spell", "creature", "experience", "mana", "morale", "luck"}.count(metaTypeName)) + if(handler.getCurrent()["reward"].isNull() || handler.getCurrent()["reward"].Struct().empty()) { CRewardableObject::serializeJsonOptions(handler); return; } //backward compatibility + auto s = handler.enterStruct("reward"); + const JsonNode & rewardsJson = handler.getCurrent(); + + std::string fullIdentifier; + std::string metaTypeName; + std::string scope; + std::string identifier; + + auto iter = rewardsJson.Struct().begin(); + fullIdentifier = iter->first; + + ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); + if(!std::set{"resource", "primarySkill", "secondarySkill", "artifact", "spell", "creature", "experience", "mana", "morale", "luck"}.count(metaTypeName)) + return; + int val = 0; handler.serializeInt(fullIdentifier, val); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 8e0849f00..31c17e6e7 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -17,6 +17,7 @@ #include "../NetPacks.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -278,7 +279,8 @@ CRewardableObject::CRewardableObject() void CRewardableObject::serializeJsonOptions(JsonSerializeFormat & handler) { - + CArmedInstance::serializeJsonOptions(handler); + handler.serializeStruct("rewardable", static_cast(*this)); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 386506e59..5e2430e62 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "Configuration.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,4 +24,30 @@ ui16 Rewardable::Configuration::getResetDuration() const return resetParameters.period; } +void Rewardable::ResetInfo::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("period", period); + handler.serializeBool("visitors", visitors); + handler.serializeBool("rewards", rewards); +} + +void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeStruct("limiter", limiter); + handler.serializeStruct("reward", reward); + handler.serializeStruct("message", message); + handler.serializeInt("visitType", visitType); +} + +void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeStruct("onSelect", onSelect); + handler.enterArray("info").serializeStruct(info); + handler.serializeEnum("selectMode", selectMode, std::vector{SelectModeString.begin(), SelectModeString.end()}); + handler.serializeEnum("visitMode", visitMode, std::vector{VisitModeString.begin(), VisitModeString.end()}); + handler.serializeStruct("resetParameters", resetParameters); + handler.serializeBool("canRefuse", canRefuse); + handler.serializeInt("infoWindowType", infoWindowType); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index a2069fc71..72377da04 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -65,7 +65,9 @@ struct DLL_LINKAGE ResetInfo /// if true - re-randomize rewards on a new week bool rewards; - + + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & period; @@ -84,6 +86,8 @@ struct DLL_LINKAGE VisitInfo /// Event to which this reward is assigned EEventType visitType; + + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler &h, const int version) { @@ -121,6 +125,8 @@ struct DLL_LINKAGE Configuration EVisitMode getVisitMode() const; ui16 getResetDuration() const; + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & info; diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 2ddedff3e..84cfdc8e3 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -147,4 +147,9 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re cb->removeAfterVisit(instance); } +void Rewardable::Interface::serializeJson(JsonSerializeFormat & handler) +{ + configuration.serializeJson(handler); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index 74b798d69..c675f82bc 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -44,6 +44,8 @@ public: Rewardable::Configuration configuration; + void serializeJson(JsonSerializeFormat & handler); + template void serialize(Handler &h, const int version) { h & configuration; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index bf3195edb..ef2d1d5c5 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -14,6 +14,7 @@ #include "../IGameCallback.h" #include "../CPlayerState.h" #include "../mapObjects/CGHeroInstance.h" +#include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -119,4 +120,9 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } +void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) +{ + +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index a443d1e2c..68639bb68 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -92,6 +92,8 @@ struct DLL_LINKAGE Limiter h & anyOf; h & noneOf; } + + void serializeJson(JsonSerializeFormat & handler); }; } diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 48caa35e5..186f4ba16 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -12,6 +12,9 @@ #include "Reward.h" #include "../mapObjects/CGHeroInstance.h" +#include "../serializer/JsonSerializeFormat.h" +#include "../constants/StringConstants.h" +#include "../CSkillHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -94,4 +97,97 @@ void Rewardable::Reward::loadComponents(std::vector & comps, } } +void Rewardable::Reward::serializeJson(JsonSerializeFormat & handler) +{ + resources.serializeJson(handler, "resources"); + handler.serializeBool("removeObject", removeObject); + handler.serializeInt("manaPercentage", manaPercentage); + handler.serializeInt("movePercentage", movePercentage); + handler.serializeInt("heroExperience", heroExperience); + handler.serializeInt("heroLevel", heroLevel); + handler.serializeInt("manaDiff", manaDiff); + handler.serializeInt("manaOverflowFactor", manaOverflowFactor); + handler.serializeInt("movePoints", movePoints); + handler.serializeIdArray("artifacts", artifacts); + handler.serializeIdArray("spells", spells); + handler.enterArray("creatures").serializeStruct(creatures); + { + auto a = handler.enterArray("primary"); + a.syncSize(primary); + for(int i = 0; i < primary.size(); ++i) + a.serializeInt(i, primary[i]); + } + + { + auto a = handler.enterArray("secondary"); + std::vector> fieldValue; + if(handler.saving) + { + for(auto & i : secondary) + { + auto key = VLC->skillh->encodeSkill(i.first); + auto value = NSecondarySkill::levels.at(i.second); + fieldValue.emplace_back(key, value); + } + } + a.syncSize(fieldValue); + for(int i = 0; i < fieldValue.size(); ++i) + { + auto e = a.enterStruct(i); + e->serializeString("skill", fieldValue[i].first); + e->serializeString("level", fieldValue[i].second); + } + if(!handler.saving) + { + for(auto & i : fieldValue) + { + const int skillId = VLC->skillh->decodeSkill(i.first); + if(skillId < 0) + { + logGlobal->error("Invalid secondary skill %s", i.first); + continue; + } + + const int level = vstd::find_pos(NSecondarySkill::levels, i.second); + if(level < 0) + { + logGlobal->error("Invalid secondary skill level%s", i.second); + continue; + } + + secondary[SecondarySkill(skillId)] = level; + } + + } + } + + { + auto a = handler.enterArray("creaturesChange"); + std::vector> fieldValue; + if(handler.saving) + { + for(auto & i : creaturesChange) + fieldValue.push_back(i); + } + a.syncSize(fieldValue); + for(int i = 0; i < fieldValue.size(); ++i) + { + auto e = a.enterStruct(i); + e->serializeId("creature", fieldValue[i].first, CreatureID{}); + e->serializeId("amount", fieldValue[i].second, CreatureID{}); + } + if(!handler.saving) + { + for(auto & i : fieldValue) + creaturesChange[i.first] = i.second; + } + } + + { + auto a = handler.enterStruct("spellCast"); + a->serializeId("spell", spellCast.first, SpellID{}); + a->serializeInt("level", spellCast.second); + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 16f93bb8d..3167a3546 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -110,6 +110,8 @@ struct DLL_LINKAGE Reward if(version >= 821) h & spellCast; } + + void serializeJson(JsonSerializeFormat & handler); }; } diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index a20fd50c1..79f3028d8 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -293,7 +293,9 @@ bool RewardsWidget::commitChanges() seerhut->configuration.info.clear(); for(int row = 0; row < rewards; ++row) { - Rewardable::Reward reward; + seerhut->configuration.info.emplace_back(); + seerhut->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + Rewardable::Reward & reward = seerhut->configuration.info.back().reward; haveRewards = true; int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; From 8d160101eddf3d9f81226e06fcf0d3596f179b98 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:49:35 +0200 Subject: [PATCH 0393/1248] alt mode to select instead of changing path --- client/adventureMap/AdventureMapInterface.cpp | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 188720cc8..065cd879b 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -537,8 +537,13 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) } else //remove old path and find a new one if we clicked on accessible tile { - LOCPLINT->localState->setPath(currentHero, mapPos); - onHeroChanged(currentHero); + if(canSelect && GH.isKeyboardCtrlDown()) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + else + { + LOCPLINT->localState->setPath(currentHero, mapPos); + onHeroChanged(currentHero); + } } } } //end of hero is selected "case" @@ -663,7 +668,12 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) if(LOCPLINT->localState->getCurrentArmy() == objAtTile) CCS->curh->set(Cursor::Map::HERO); else - CCS->curh->set(cursorExchange[turns]); + { + if(GH.isKeyboardCtrlDown()) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(cursorExchange[turns]); + } } else if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorVisit[turns]); From 6d69326c1a718ebc4fdc1ac434ea5dd8ad22ac00 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 23:43:06 +0200 Subject: [PATCH 0394/1248] alternative implementation --- client/adventureMap/AdventureMapInterface.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 065cd879b..60947eeb0 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -537,8 +537,11 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) } else //remove old path and find a new one if we clicked on accessible tile { - if(canSelect && GH.isKeyboardCtrlDown()) - LOCPLINT->localState->setSelection(static_cast(topBlocking)); + if(GH.isKeyboardCtrlDown()) + { + if(canSelect) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + } else { LOCPLINT->localState->setPath(currentHero, mapPos); @@ -616,7 +619,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) } } - if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN) + if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown()) { if(objAtTile) { @@ -668,12 +671,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos) if(LOCPLINT->localState->getCurrentArmy() == objAtTile) CCS->curh->set(Cursor::Map::HERO); else - { - if(GH.isKeyboardCtrlDown()) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(cursorExchange[turns]); - } + CCS->curh->set(cursorExchange[turns]); } else if(pathNode->layer == EPathfindingLayer::LAND) CCS->curh->set(cursorVisit[turns]); From 19693d251dcebd67c71bde6e17ac9e4f69c1833b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:49:13 +0200 Subject: [PATCH 0395/1248] comments --- client/adventureMap/AdventureMapInterface.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 60947eeb0..fc8288b0a 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -535,14 +535,14 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); return; } - else //remove old path and find a new one if we clicked on accessible tile + else { - if(GH.isKeyboardCtrlDown()) + if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected) { if(canSelect) LOCPLINT->localState->setSelection(static_cast(topBlocking)); } - else + else //remove old path and find a new one if we clicked on accessible tile { LOCPLINT->localState->setPath(currentHero, mapPos); onHeroChanged(currentHero); From 714852908e08c0e2fca8f42d01ff75ae782d1e70 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:21:14 +0200 Subject: [PATCH 0396/1248] Documentation --- docs/players/Game_Mechanics.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index c4c045e68..5291c267c 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -29,6 +29,9 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav # List of extended game functionality +## Adventure map +- [LCtrl] + LClick – perform a normal click (same as no hero is selected). This make it possible to select other hero instead of changing path of current hero. + ## Quick Army Management - [LShift] + LClick – splits a half units from the selected stack into an empty slot. From 03316c62bf02d9dc0e143721a0cf3971be8ccb3a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 23:17:44 +0200 Subject: [PATCH 0397/1248] Easy choosing for a secondary skill in Level Up Dialog by double clicking it --- client/widgets/CComponent.cpp | 10 ++++++++-- client/widgets/CComponent.h | 2 ++ client/windows/GUIClasses.cpp | 1 + 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 0ad59f1dd..4075ea084 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -269,6 +269,12 @@ void CSelectableComponent::clickPressed(const Point & cursorPosition) onSelect(); } +void CSelectableComponent::clickDouble(const Point & cursorPosition) +{ + if(onChoose) + onChoose(); +} + void CSelectableComponent::init() { selected = false; @@ -278,7 +284,7 @@ CSelectableComponent::CSelectableComponent(const Component &c, std::function onSelect; //function called on selection change + std::function onChoose; //function called on doubleclick void showAll(Canvas & to) override; void select(bool on); void clickPressed(const Point & cursorPosition) override; //call-in + void clickDouble(const Point & cursorPosition) override; //call-in CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); CSelectableComponent(const Component & c, std::function OnSelect = nullptr); }; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 67d3a9882..4b8b23c77 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -415,6 +415,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std for(auto & skill : skills) { auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); + comp->onChoose = std::bind(&CLevelWindow::close, this); comps.push_back(comp); } From 39838fdd3c8b292425318e7a98f7844411718cce Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:26:11 +0200 Subject: [PATCH 0398/1248] extend for all selections --- client/windows/InfoWindows.cpp | 7 +++++++ client/windows/InfoWindows.h | 1 + 2 files changed, 8 insertions(+) diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 2ae2ad3fb..f015c779b 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -106,6 +106,7 @@ CSelWindow::CSelWindow(const std::string &Text, PlayerColor player, int charperl addChild(comps[i].get()); components.push_back(comps[i]); comps[i]->onSelect = std::bind(&CSelWindow::selectionChange,this,i); + comps[i]->onChoose = std::bind(&CSelWindow::madeChoiceAndClose,this); if(i<8) comps[i]->assignedKey = vstd::next(EShortcut::SELECT_INDEX_1,i); } @@ -127,6 +128,12 @@ void CSelWindow::madeChoice() LOCPLINT->cb->selectionMade(ret+1,ID); } +void CSelWindow::madeChoiceAndClose() +{ + madeChoice(); + close(); +} + CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo & comps, const TButtonsInfo & Buttons) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index ea91777cf..ff6b880da 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -130,6 +130,7 @@ class CSelWindow : public CInfoWindow public: void selectionChange(unsigned to); void madeChoice(); //looks for selected component and calls callback + void madeChoiceAndClose(); CSelWindow(const std::string & text, PlayerColor player, int charperline, const std::vector> & comps, const std::vector > > &Buttons, QueryID askID); //notification - this class inherits important destructor from CInfoWindow From 42d2257f19bc966cab30c0b927fe04338f66b784 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 11 Sep 2023 22:21:47 +0200 Subject: [PATCH 0399/1248] VCMI credits --- client/mainmenu/CreditsScreen.cpp | 1 + client/mainmenu/CreditsScreen.h | 3 +++ 2 files changed, 4 insertions(+) diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp index 8848ec407..228530a6c 100644 --- a/client/mainmenu/CreditsScreen.cpp +++ b/client/mainmenu/CreditsScreen.cpp @@ -30,6 +30,7 @@ CreditsScreen::CreditsScreen(Rect rect) std::string text((char *)textFile.first.get(), textFile.second); size_t firstQuote = text.find('\"') + 1; text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote); + text = "{- VCMI -}\r\n\r\n{Contributors:}\r\n" + boost::algorithm::join(contributors, "\r\n") + "\r\n\r\n{Website:}\r\nhttps://vcmi.eu\r\n\r\n\r\n\r\n\r\n{- Heroes of Might and Magic III -}\r\n\r\n" + text; credits = std::make_shared(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, ETextAlignment::CENTER, Colors::WHITE, text); credits->scrollTextTo(-600); // move all text below the screen } diff --git a/client/mainmenu/CreditsScreen.h b/client/mainmenu/CreditsScreen.h index d6055cfd0..589753726 100644 --- a/client/mainmenu/CreditsScreen.h +++ b/client/mainmenu/CreditsScreen.h @@ -18,6 +18,9 @@ class CreditsScreen : public CIntObject int positionCounter; std::shared_ptr credits; + // update contributors with bash: curl -s "https://api.github.com/repos/vcmi/vcmi/contributors" | jq '.[].login' | tr '\n' '|' | sed 's/|/, /g' | sed 's/..$//' + std::vector contributors = { "IvanSavenko", "alexvins", "mwu-tow", "Nordsoft91", "mateuszbaran", "ArseniyShestakov", "nullkiller", "dydzio0614", "kambala-decapitator", "rilian-la-te", "DjWarmonger", "Laserlicht", "beegee1", "henningkoehlernz", "SoundSSGood", "vmarkovtsev", "krs0", "Mixaill", "Fayth", "ShubusCorporation", "rwesterlund", "Macron1Robot", "Chocimier", "Zyx-2000", "bwrsandman", "stopiccot", "viciious", "karol57", "heroesiiifan", "Adriankhl" }; + public: CreditsScreen(Rect rect); void show(Canvas & to) override; From 1c68937d95ea1b02cbe29116f858fe661709588a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 11:52:19 +0200 Subject: [PATCH 0400/1248] new approach --- AUTHORS | 86 ------------------------------- AUTHORS.h | 46 +++++++++++++++++ client/mainmenu/CreditsScreen.cpp | 15 +++++- client/mainmenu/CreditsScreen.h | 3 -- cmake_modules/VCMI_lib.cmake | 1 + lib/VCMI_lib.cbp | 1 + lib/VCMI_lib.vcxproj | 1 + lib/VCMI_lib.vcxproj.filters | 3 ++ 8 files changed, 66 insertions(+), 90 deletions(-) delete mode 100644 AUTHORS create mode 100644 AUTHORS.h diff --git a/AUTHORS b/AUTHORS deleted file mode 100644 index 6160a2eb4..000000000 --- a/AUTHORS +++ /dev/null @@ -1,86 +0,0 @@ -VCMI PROJECT CODE CONTRIBUTORS: - -Michał Urbańczyk aka Tow, - * project originator; programming, making releases, website -maintenance, reverse engineering, general support. - -Mateusz B. aka Tow dragon, - * general support, battle support, support for many Heroes 3 config files, reverse engineering, ERM/VERM parser and interpreter - -Stefan Pavlov aka Ste, - * minor fixes in pregame - -Yifeng Sun aka phoebus118, - * a part of .snd handling, minor fixes and updates - -Andrea Palmate aka afxgroup, - * GCC/AmigaOS4 compatibility updates and makefile - -Vadim Glazunov aka neweagle, - * minor GCC/Linux compatibility changes - -Rafal R. aka ambtrip, - * GeniusAI (battles) - -Lukasz Wychrystenko aka tezeriusz, - * minor GCC/Linux compatibility changes, code review - -Xiaomin Ding, - * smack videos player - -Tom Zielinski aka Warmonger, - * game objects, mechanics - -Frank Zago aka ubuntux, <> - * GCC/Linux compatibility changes, sound/music support, video support on Linux - -Trevor Standley aka tstandley, <> - * adventure map part of Genius AI - -Rickard Westerlund aka Onion Knight, - * battle functionality and general support - -Ivan Savenko, - * GCC/Linux support, client development, general support - -Benjamin Gentner aka beegee, <> - * battle support, programming - -Alexey aka Macron1Robot, <> - * minor modding changes - -Alexander Shishkin aka alexvins, - * MinGW platform support, modding related programming - -Arseniy Shestakov aka SXX, - * pathfinding improvements, programming - -Vadim Markovtsev, - * resolving problems with macOS, bug fixes - -Michał Kalinowski, - * refactoring code - -Dydzio, - * Small features, improvements and bug fixes in all VCMI parts - -Piotr Wójcik aka Chocimier, - * Various bug fixes - -Henning Koehler, - * skill modding, bonus updaters - -Andrzej Żak aka godric3 - * minor bug fixes and modding features - -Andrii Danylchenko - * Nullkiller AI, VCAI improvements - -Dmitry Orlov, - * special buildings support in fan towns, new features and bug fixes - -Andrey Cherkas aka nordsoft, - * new terrain support, rmg features, map editor, multiplayer improvements, bug fixes - -Andrey Filipenkov aka kambala-decapitator, - * iOS support, macOS improvements, various bug fixes diff --git a/AUTHORS.h b/AUTHORS.h new file mode 100644 index 000000000..337fcf541 --- /dev/null +++ b/AUTHORS.h @@ -0,0 +1,46 @@ +/* + * AUTHORS.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 + +//VCMI PROJECT CODE CONTRIBUTORS: +std::vector> contributors = { +// Task Name Aka E-Mail + { "Developing", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, + { "Developing", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, + { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, + { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, + { "Developing", "Andrea Palmate", "afxgroup", "andrea@amigasoft.net" }, + { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, + { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, + { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, + { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, + { "Developing", "Frank Zago", "ubuntux", "" }, + { "Developing", "Trevor Standley", "tstandley", "" }, + { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, + { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, + { "Developing", "Benjamin Gentner", "beegee", "" }, + { "Developing", "Alexey ", "Macron1Robot", "" }, + { "Developing", "Alexander Shishkin", "alexvins", "" }, + { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, + { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, + { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, + { "Developing", "", "Dydzio", "blood990@gmail.com" }, + { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, + { "Developing", "Henning Koehler", "", "henning.koehler.nz@gmail.com" }, + { "Developing", "Andrzej Żak", "godric3", "" }, + { "Developing", "Andrii Danylchenko", "", "" }, + { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, + { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, + { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, + { "Developing", "", "Laserlicht", "" }, + { "Testing", "", "Povelitel", "" }, + { "Testing", "", "Misiokles", "" }, +}; \ No newline at end of file diff --git a/client/mainmenu/CreditsScreen.cpp b/client/mainmenu/CreditsScreen.cpp index 228530a6c..bd36ce98b 100644 --- a/client/mainmenu/CreditsScreen.cpp +++ b/client/mainmenu/CreditsScreen.cpp @@ -19,6 +19,8 @@ #include "../../lib/filesystem/Filesystem.h" +#include "../../AUTHORS.h" + CreditsScreen::CreditsScreen(Rect rect) : CIntObject(LCLICK), positionCounter(0) { @@ -26,11 +28,22 @@ CreditsScreen::CreditsScreen(Rect rect) pos.h = rect.h; setRedrawParent(true); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + std::string contributorsText = ""; + std::string contributorsTask = ""; + for (auto & element : contributors) + { + if(element[0] != contributorsTask) + contributorsText += "\r\n{" + element[0] + ":}\r\n"; + contributorsText += (element[2] != "" ? element[2] : element[1]) + "\r\n"; + contributorsTask = element[0]; + } + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/CREDITS.TXT"))->readAll(); std::string text((char *)textFile.first.get(), textFile.second); size_t firstQuote = text.find('\"') + 1; text = text.substr(firstQuote, text.find('\"', firstQuote) - firstQuote); - text = "{- VCMI -}\r\n\r\n{Contributors:}\r\n" + boost::algorithm::join(contributors, "\r\n") + "\r\n\r\n{Website:}\r\nhttps://vcmi.eu\r\n\r\n\r\n\r\n\r\n{- Heroes of Might and Magic III -}\r\n\r\n" + text; + text = "{- VCMI -}\r\n\r\n" + contributorsText + "\r\n\r\n{Website:}\r\nhttps://vcmi.eu\r\n\r\n\r\n\r\n\r\n{- Heroes of Might and Magic III -}\r\n\r\n" + text; credits = std::make_shared(Rect(pos.w - 350, 0, 350, 600), FONT_CREDITS, ETextAlignment::CENTER, Colors::WHITE, text); credits->scrollTextTo(-600); // move all text below the screen } diff --git a/client/mainmenu/CreditsScreen.h b/client/mainmenu/CreditsScreen.h index 589753726..d6055cfd0 100644 --- a/client/mainmenu/CreditsScreen.h +++ b/client/mainmenu/CreditsScreen.h @@ -18,9 +18,6 @@ class CreditsScreen : public CIntObject int positionCounter; std::shared_ptr credits; - // update contributors with bash: curl -s "https://api.github.com/repos/vcmi/vcmi/contributors" | jq '.[].login' | tr '\n' '|' | sed 's/|/, /g' | sed 's/..$//' - std::vector contributors = { "IvanSavenko", "alexvins", "mwu-tow", "Nordsoft91", "mateuszbaran", "ArseniyShestakov", "nullkiller", "dydzio0614", "kambala-decapitator", "rilian-la-te", "DjWarmonger", "Laserlicht", "beegee1", "henningkoehlernz", "SoundSSGood", "vmarkovtsev", "krs0", "Mixaill", "Fayth", "ShubusCorporation", "rwesterlund", "Macron1Robot", "Chocimier", "Zyx-2000", "bwrsandman", "stopiccot", "viciious", "karol57", "heroesiiifan", "Adriankhl" }; - public: CreditsScreen(Rect rect); void show(Canvas & to) override; diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index b5a66826f..8c2a6a274 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -281,6 +281,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) set(lib_HEADERS ${MAIN_LIB_DIR}/../include/vstd/CLoggerBase.h ${MAIN_LIB_DIR}/../Global.h + ${MAIN_LIB_DIR}/../AUTHORS.h ${MAIN_LIB_DIR}/StdInc.h ${MAIN_LIB_DIR}/../include/vstd/ContainerUtils.h diff --git a/lib/VCMI_lib.cbp b/lib/VCMI_lib.cbp index 2195ab5c5..31c3ccba1 100644 --- a/lib/VCMI_lib.cbp +++ b/lib/VCMI_lib.cbp @@ -127,6 +127,7 @@ + diff --git a/lib/VCMI_lib.vcxproj b/lib/VCMI_lib.vcxproj index ea6777a05..060a9d38b 100644 --- a/lib/VCMI_lib.vcxproj +++ b/lib/VCMI_lib.vcxproj @@ -325,6 +325,7 @@ + diff --git a/lib/VCMI_lib.vcxproj.filters b/lib/VCMI_lib.vcxproj.filters index 3679e6a8c..08cd5df48 100644 --- a/lib/VCMI_lib.vcxproj.filters +++ b/lib/VCMI_lib.vcxproj.filters @@ -470,6 +470,9 @@ Header Files + + Header Files + Header Files From 0e7f48fee03d04699098e7c5225027ce70b2bcb4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:14:24 +0200 Subject: [PATCH 0401/1248] Update CMakeLists.txt --- CMakeLists.txt | 5 ----- 1 file changed, 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 9214a4040..d711ceec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -588,11 +588,6 @@ if(ANDROID) ) endif() - install(FILES AUTHORS - DESTINATION res/raw - RENAME authors.txt - ) - # zip internal assets - 'config' and 'Mods' dirs, save md5 of the zip install(CODE " cmake_path(ABSOLUTE_PATH CMAKE_INSTALL_PREFIX From 91e1246f758e29bbd42b903d7a831ae1826bbb39 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 12:37:53 +0200 Subject: [PATCH 0402/1248] Update DialogAuthors.java --- .../src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java index 84e555fca..4c6f7b794 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java @@ -40,7 +40,7 @@ public class DialogAuthors extends DialogFragment try { // to be checked if this should be converted to async load (not really a file operation so it should be okay) - final String authorsContent = FileUtil.read(getResources().openRawResource(R.raw.authors)); + final String authorsContent = "See ingame credits"; vcmiAuthorsView.setText(authorsContent); launcherAuthorsView.setText("Fay"); // TODO hardcoded for now } From e50476603f3d758710e24e69371218b53ffe1cbb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 13:04:43 +0200 Subject: [PATCH 0403/1248] Update DialogAuthors.java --- .../src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java index 4c6f7b794..790920528 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/content/DialogAuthors.java @@ -44,7 +44,7 @@ public class DialogAuthors extends DialogFragment vcmiAuthorsView.setText(authorsContent); launcherAuthorsView.setText("Fay"); // TODO hardcoded for now } - catch (final IOException e) + catch (final Exception e) { Log.e(this, "Could not load authors content", e); } From 8185a590097df96204d7e62914b03d82b6ea7b71 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:08:14 +0200 Subject: [PATCH 0404/1248] suggestion --- AUTHORS.h | 50 ++++++++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 24 deletions(-) diff --git a/AUTHORS.h b/AUTHORS.h index 337fcf541..05d517b14 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -12,35 +12,37 @@ //VCMI PROJECT CODE CONTRIBUTORS: std::vector> contributors = { // Task Name Aka E-Mail - { "Developing", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, - { "Developing", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, - { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, - { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, + { "Idea", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, + { "Idea", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, + { "Developing", "Andrea Palmate", "afxgroup", "andrea@amigasoft.net" }, - { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, - { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, - { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, - { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, - { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, - { "Developing", "Frank Zago", "ubuntux", "" }, - { "Developing", "Trevor Standley", "tstandley", "" }, - { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, - { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, - { "Developing", "Benjamin Gentner", "beegee", "" }, - { "Developing", "Alexey ", "Macron1Robot", "" }, { "Developing", "Alexander Shishkin", "alexvins", "" }, - { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, - { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, - { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, - { "Developing", "", "Dydzio", "blood990@gmail.com" }, - { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, - { "Developing", "Henning Koehler", "", "henning.koehler.nz@gmail.com" }, - { "Developing", "Andrzej Żak", "godric3", "" }, + { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, { "Developing", "Andrii Danylchenko", "", "" }, + { "Developing", "Benjamin Gentner", "beegee", "" }, + { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, - { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, + { "Developing", "", "Dydzio", "blood990@gmail.com" }, + { "Developing", "Andrzej Żak", "godric3", "" }, + { "Developing", "Henning Koehler", "", "henning.koehler.nz@gmail.com" }, + { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, { "Developing", "", "Laserlicht", "" }, + { "Developing", "Alexey ", "Macron1Robot", "" }, + { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, + { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, + { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, + { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, + { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, + { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, + { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, + { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, + { "Developing", "Trevor Standley", "tstandley", "" }, + { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, + { "Developing", "Frank Zago", "ubuntux", "" }, + { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, + { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + { "Testing", "", "Povelitel", "" }, { "Testing", "", "Misiokles", "" }, -}; \ No newline at end of file +}; From d1ce5aa6082b7fe19d3c8151bf477ab6cffebd4e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 17:13:21 +0200 Subject: [PATCH 0405/1248] Update AUTHORS.h --- AUTHORS.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.h b/AUTHORS.h index 05d517b14..f92ed4922 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -43,6 +43,6 @@ std::vector> contributors = { { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, - { "Testing", "", "Povelitel", "" }, { "Testing", "", "Misiokles", "" }, + { "Testing", "", "Povelitel", "" }, }; From 8cef380c321e9da7108fb7d4d793916ea44dedee Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:16:19 +0200 Subject: [PATCH 0406/1248] Update AUTHORS.h --- AUTHORS.h | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/AUTHORS.h b/AUTHORS.h index f92ed4922..67eaa4a87 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -24,22 +24,26 @@ std::vector> contributors = { { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, { "Developing", "", "Dydzio", "blood990@gmail.com" }, { "Developing", "Andrzej Żak", "godric3", "" }, - { "Developing", "Henning Koehler", "", "henning.koehler.nz@gmail.com" }, + { "Developing", "Henning Koehler", "henningkoehlernz", "henning.koehler.nz@gmail.com" }, { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, + { "Developing", "", "krs0", "" }, { "Developing", "", "Laserlicht", "" }, - { "Developing", "Alexey ", "Macron1Robot", "" }, + { "Developing", "Alexey", "Macron1Robot", "" }, { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, - { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, + { "Developing", "", "rilian-la-te", "" }, + { "Developing", "", "SoundSSGood", "" }, { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, + { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, { "Developing", "Trevor Standley", "tstandley", "" }, { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, { "Developing", "Frank Zago", "ubuntux", "" }, + { "Developing", "", "vmarkovtsev", "" }, { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, From 26782888df8f5fdb90a9361a7cd48bf327f5ca23 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 14 Sep 2023 12:39:11 +0200 Subject: [PATCH 0407/1248] adding --- AUTHORS.h | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.h b/AUTHORS.h index 67eaa4a87..bd0b5b698 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -47,6 +47,7 @@ std::vector> contributors = { { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + { "Testing", "Ben Yan", "by003", "" }, { "Testing", "", "Misiokles", "" }, { "Testing", "", "Povelitel", "" }, }; From dd4c95bd0460ccea03290c88064b66cd086459af Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:28:09 +0200 Subject: [PATCH 0408/1248] textcolor --- Global.h | 2 ++ client/widgets/TextControls.cpp | 26 ++++++++++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Global.h b/Global.h index 675a3eff4..2eb54db45 100644 --- a/Global.h +++ b/Global.h @@ -122,6 +122,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #include #include #include +#include #include #include #include @@ -147,6 +148,7 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); #define BOOST_ASIO_ENABLE_OLD_SERVICES #endif +#include #include #include #include diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index e1d1e5a84..24339b1b9 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -153,6 +153,15 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) //We should count delimiters length from string to correct centering later. delimitersCount *= f->getStringWidth(delimeters)/2; + std::smatch match; + std::regex expr("\\{(#[0-9a-fA-F]{6})"); + std::string::const_iterator searchStart( what.cbegin() ); + while(std::regex_search(searchStart, what.cend(), match, expr)) + { + delimitersCount += f->getStringWidth(match[1].str()); + searchStart = match.suffix().first; + } + // input is rect in which given text should be placed // calculate proper position for top-left corner of the text if(alignment == ETextAlignment::TOPLEFT) @@ -189,8 +198,21 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) { std::string toPrint = what.substr(begin, end - begin); - if(currDelimeter % 2) // Enclosed in {} text - set to yellow - to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); + if(currDelimeter % 2) // Enclosed in {} text - set to yellow or defined color + { + std::smatch match; + std::regex expr("^#([0-9a-fA-F]{6})"); + if(std::regex_search(toPrint, match, expr)) + { + ui8 rgb[3] = {0, 0, 0}; + std::string tmp = boost::algorithm::unhex(match[1].str()); + std::copy(tmp.begin(), tmp.end(), rgb); + + to.drawText(where, font, ColorRGBA(rgb[0], rgb[1], rgb[2]), ETextAlignment::TOPLEFT, toPrint.substr(7, toPrint.length() - 7)); + } + else + to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); + } else // Non-enclosed text, use default color to.drawText(where, font, color, ETextAlignment::TOPLEFT, toPrint); From d948cb72a0dbb5b4b148c18f2e898781d78377a9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:13:35 +0200 Subject: [PATCH 0409/1248] Alignment --- client/widgets/TextControls.cpp | 3 ++- client/windows/CMessage.cpp | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 24339b1b9..bfa75241d 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -208,7 +208,8 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) std::string tmp = boost::algorithm::unhex(match[1].str()); std::copy(tmp.begin(), tmp.end(), rgb); - to.drawText(where, font, ColorRGBA(rgb[0], rgb[1], rgb[2]), ETextAlignment::TOPLEFT, toPrint.substr(7, toPrint.length() - 7)); + toPrint = toPrint.substr(7, toPrint.length() - 6); + to.drawText(where, font, ColorRGBA(rgb[0], rgb[1], rgb[2]), ETextAlignment::TOPLEFT, toPrint); } else to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index d1c2bce2c..94917a8bc 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -147,7 +147,17 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi /* We don't count braces in string length. */ if (text[currPos] == '{') + { opened=true; + + std::smatch match; + std::regex expr("^\\{#([0-9a-fA-F]{6})$"); + std::string tmp = text.substr(currPos, 8); + if(std::regex_search(tmp, match, expr)) + { + currPos += 7; + } + } else if (text[currPos]=='}') opened=false; else From f776422c67c8f912a5c794c1d5655e5c3485c17f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 12 Sep 2023 16:27:31 +0200 Subject: [PATCH 0410/1248] color linesplit --- client/windows/CMessage.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 94917a8bc..27ba0be22 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -131,6 +131,7 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi ui32 wordBreak = -1; //last position for line break (last space character) ui32 currPos = 0; //current position in text bool opened = false; //set to true when opening brace is found + std::string color = ""; //color found size_t symbolSize = 0; // width of character, in bytes size_t glyphWidth = 0; // width of printable glyph, pixels @@ -151,15 +152,19 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi opened=true; std::smatch match; - std::regex expr("^\\{#([0-9a-fA-F]{6})$"); + std::regex expr("^\\{(#[0-9a-fA-F]{6})$"); std::string tmp = text.substr(currPos, 8); if(std::regex_search(tmp, match, expr)) { + color = match[1].str(); currPos += 7; } } else if (text[currPos]=='}') + { opened=false; + color = ""; + } else lineWidth += (ui32)glyphWidth; currPos += (ui32)symbolSize; @@ -206,7 +211,7 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi { /* Add an opening brace for the next line. */ if (text.length() != 0) - text.insert(0, "{"); + text.insert(0, "{" + color); } } From f5505c9f6db6ed779578f46c628355b888ad8399 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 02:04:59 +0200 Subject: [PATCH 0411/1248] support for predefined colors --- client/widgets/TextControls.cpp | 24 +++-- client/windows/CMessage.cpp | 39 +++++++- client/windows/CMessage.h | 4 + config/textColors.json | 170 ++++++++++++++++++++++++++++++++ 4 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 config/textColors.json diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index bfa75241d..88b9f38b9 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -154,11 +154,13 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) delimitersCount *= f->getStringWidth(delimeters)/2; std::smatch match; - std::regex expr("\\{(#[0-9a-fA-F]{6})"); + std::regex expr("\\{(.*?)\\|"); std::string::const_iterator searchStart( what.cbegin() ); while(std::regex_search(searchStart, what.cend(), match, expr)) { - delimitersCount += f->getStringWidth(match[1].str()); + std::string colorText = match[1].str(); + if(CMessage::parseColor(colorText).a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + delimitersCount += f->getStringWidth(colorText + "|"); searchStart = match.suffix().first; } @@ -201,15 +203,19 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) if(currDelimeter % 2) // Enclosed in {} text - set to yellow or defined color { std::smatch match; - std::regex expr("^#([0-9a-fA-F]{6})"); + std::regex expr("^(.*?)\\|"); if(std::regex_search(toPrint, match, expr)) { - ui8 rgb[3] = {0, 0, 0}; - std::string tmp = boost::algorithm::unhex(match[1].str()); - std::copy(tmp.begin(), tmp.end(), rgb); - - toPrint = toPrint.substr(7, toPrint.length() - 6); - to.drawText(where, font, ColorRGBA(rgb[0], rgb[1], rgb[2]), ETextAlignment::TOPLEFT, toPrint); + std::string colorText = match[1].str(); + ColorRGBA color = CMessage::parseColor(colorText); + + if(color.a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + { + toPrint = toPrint.substr(colorText.length() + 1, toPrint.length() - colorText.length()); + to.drawText(where, font, color, ETextAlignment::TOPLEFT, toPrint); + } + else + to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); } else to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 27ba0be22..dc8cba620 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -14,6 +14,7 @@ #include "../CGameInfo.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/TextOperations.h" +#include "../../lib/JsonNode.h" #include "../windows/InfoWindows.h" #include "../widgets/Buttons.h" @@ -152,12 +153,16 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi opened=true; std::smatch match; - std::regex expr("^\\{(#[0-9a-fA-F]{6})$"); - std::string tmp = text.substr(currPos, 8); + std::regex expr("^\\{(.*?)\\|"); + std::string tmp = text.substr(currPos); if(std::regex_search(tmp, match, expr)) { - color = match[1].str(); - currPos += 7; + std::string colorText = match[1].str(); + if(CMessage::parseColor(colorText).a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + { + color = colorText + "|"; + currPos += colorText.length() + 1; + } } } else if (text[currPos]=='}') @@ -222,6 +227,32 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi return ret; } +ColorRGBA CMessage::parseColor(std::string text) +{ + std::smatch match; + std::regex expr("^#([0-9a-fA-F]{6})$"); + ui8 rgb[3] = {0, 0, 0}; + if(std::regex_search(text, match, expr)) + { + std::string tmp = boost::algorithm::unhex(match[1].str()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + + const JsonNode config(JsonPath::builtin("CONFIG/textColors")); + auto colors = config["colors"].Struct(); + for(auto & color : colors) { + if(boost::algorithm::to_lower_copy(color.first) == boost::algorithm::to_lower_copy(text)) + { + std::string tmp = boost::algorithm::unhex(color.second.String()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + } + + return Colors::TRANSPARENCY; +} + std::string CMessage::guessHeader(const std::string & msg) { size_t begin = 0; diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 22fae44b5..6a38de781 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -15,6 +15,7 @@ struct SDL_Surface; class CInfoWindow; class CComponent; +class ColorRGBA; /// Class which draws formatted text messages and generates chat windows @@ -32,6 +33,9 @@ public: /// split text in lines static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); + /// parse color + static ColorRGBA parseColor(std::string text); + /// Try to guess a header of a message static std::string guessHeader(const std::string & msg); diff --git a/config/textColors.json b/config/textColors.json new file mode 100644 index 000000000..f15b0ac98 --- /dev/null +++ b/config/textColors.json @@ -0,0 +1,170 @@ +{ + "colors": { + "r": "F80000", + "g": "00FC00", + "b": "0000F8", + "y": "F8FC00", + "o": "F88000", + "p": "F800F8", + "c": "00FCF8", + "k": "000000", + "w": "FFFFFF", + "Regular": "F8F0D8", + "Highlight": "E8D478", + "H3Red": "F80000", + "H3Cyan": "00FCF8", + "H3Green": "00FC00", + "H3Blue": "0000F8", + "H3Yellow": "F8FC00", + "H3Orange": "F88000", + "H3Purple": "F800F8", + "H3Pink": "C07888", + "AliceBlue": "F0F8FF", + "AntiqueWhite": "FAEBD7", + "Aqua": "00FFFF", + "Aquamarine": "7FFFD4", + "Azure": "F0FFFF", + "Beige": "F5F5DC", + "Bisque": "FFE4C4", + "Black": "000000", + "BlanchedAlmond": "FFEBCD", + "Blue": "0000FF", + "BlueViolet": "8A2BE2", + "Brown": "A52A2A", + "BurlyWood": "DEB887", + "CadetBlue": "5F9EA0", + "Chartreuse": "7FFF00", + "Chocolate": "D2691E", + "Coral": "FF7F50", + "CornflowerBlue": "6495ED", + "Cornsilk": "FFF8DC", + "Crimson": "DC143C", + "Cyan": "00FFFF", + "DarkBlue": "00008B", + "DarkCyan": "008B8B", + "DarkGoldenRod": "B8860B", + "DarkGray": "A9A9A9", + "DarkGrey": "A9A9A9", + "DarkGreen": "A9A9A9", + "DarkKhaki": "006400", + "DarkMagenta": "8B008B", + "DarkOliveGreen": "556B2F", + "Darkorange": "FF8C00", + "DarkOrchid": "9932CC", + "DarkRed": "8B0000", + "DarkSalmon": "E9967A", + "DarkSeaGreen": "8FBC8F", + "DarkSlateBlue": "483D8B", + "DarkSlateGray": "2F4F4F", + "DarkSlateGrey": "2F4F4F", + "DarkTurquoise": "00CED1", + "DarkViolet": "9400D3", + "DeepPink": "FF1493", + "DeepSkyBlue": "00BFFF", + "DimGray": "696969", + "DimGrey": "696969", + "DodgerBlue": "1E90FF", + "FireBrick": "B22222", + "FloralWhite": "FFFAF0", + "ForestGreen": "228B22", + "Fuchsia": "FF00FF", + "Gainsboro": "DCDCDC", + "GhostWhite": "F8F8FF", + "Gold": "FFD700", + "GoldenRod": "DAA520", + "Gray": "808080", + "Grey": "808080", + "Green": "008000", + "GreenYellow": "ADFF2F", + "HoneyDew": "F0FFF0", + "HotPink": "FF69B4", + "IndianRed": "CD5C5C", + "Indigo": "4B0082", + "Ivory": "FFFFF0", + "Khaki": "F0E68C", + "Lavender": "E6E6FA", + "LavenderBlush": "FFF0F5", + "LawnGreen": "7CFC00", + "LemonChiffon": "FFFACD", + "LightBlue": "ADD8E6", + "LightCoral": "F08080", + "LightCyan": "E0FFFF", + "LightGoldenRodYellow": "FAFAD2", + "LightGray": "D3D3D3", + "LightGrey": "D3D3D3", + "LightGreen": "90EE90", + "LightPink": "FFB6C1", + "LightSalmon": "FFA07A", + "LightSeaGreen": "20B2AA", + "LightSkyBlue": "87CEFA", + "LightSlateGray": "778899", + "LightSlateGrey": "778899", + "LightSteelBlue": "B0C4DE", + "LightYellow": "FFFFE0", + "Lime": "00FF00", + "LimeGreen": "32CD32", + "Linen": "FAF0E6", + "Magenta": "FF00FF", + "Maroon": "800000", + "MediumAquaMarine": "66CDAA", + "MediumBlue": "0000CD", + "MediumOrchid": "BA55D3", + "MediumPurple": "9370D8", + "MediumSeaGreen": "3CB371", + "MediumSlateBlue": "7B68EE", + "MediumSpringGreen": "00FA9A", + "MediumTurquoise": "48D1CC", + "MediumVioletRed": "C71585", + "MidnightBlue": "191970", + "MintCream": "F5FFFA", + "MistyRose": "FFE4E1", + "Moccasin": "FFE4B5", + "NavajoWhite": "FFDEAD", + "Navy": "000080", + "OldLace": "FDF5E6", + "Olive": "808000", + "OliveDrab": "6B8E23", + "Orange": "FFA500", + "OrangeRed": "FF4500", + "Orchid": "DA70D6", + "PaleGoldenRod": "EEE8AA", + "PaleGreen": "98FB98", + "PaleTurquoise": "AFEEEE", + "PaleVioletRed": "D87093", + "PapayaWhip": "FFEFD5", + "PeachPuff": "FFDAB9", + "Peru": "CD853F", + "Pink": "FFC0CB", + "Plum": "DDA0DD", + "PowderBlue": "B0E0E6", + "Purple": "800080", + "Red": "FF0000", + "RosyBrown": "BC8F8F", + "RoyalBlue": "4169E1", + "SaddleBrown": "8B4513", + "Salmon": "FA8072", + "SandyBrown": "F4A460", + "SeaGreen": "2E8B57", + "SeaShell": "FFF5EE", + "Sienna": "A0522D", + "Silver": "C0C0C0", + "SkyBlue": "87CEEB", + "SlateBlue": "6A5ACD", + "SlateGray": "708090", + "SlateGrey": "708090", + "Snow": "FFFAFA", + "SpringGreen": "00FF7F", + "SteelBlue": "4682B4", + "Tan": "D2B48C", + "Teal": "008080", + "Thistle": "D8BFD8", + "Tomato": "FF6347", + "Turquoise": "40E0D0", + "Violet": "EE82EE", + "Wheat": "F5DEB3", + "White": "FFFFFF", + "WhiteSmoke": "F5F5F5", + "Yellow": "FFFF00", + "YellowGreen": "9ACD32" + } +} From 922a775108ea2d462da954fc59fa73c83c78c63a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 02:26:47 +0200 Subject: [PATCH 0412/1248] Update CMessage.h --- client/windows/CMessage.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 6a38de781..12c5d0077 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -15,7 +15,10 @@ struct SDL_Surface; class CInfoWindow; class CComponent; + +VCMI_LIB_NAMESPACE_BEGIN class ColorRGBA; +VCMI_LIB_NAMESPACE_END /// Class which draws formatted text messages and generates chat windows From 063c7f3ca0848c9dca9f8e2fef54892f8453124f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 16:02:54 +0200 Subject: [PATCH 0413/1248] move function; optional; json static --- client/render/Colors.cpp | 27 +++++++++++++++++++++++++++ client/render/Colors.h | 3 +++ client/widgets/TextControls.cpp | 7 +++---- client/windows/CMessage.cpp | 29 +---------------------------- client/windows/CMessage.h | 3 --- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/client/render/Colors.cpp b/client/render/Colors.cpp index c7f5c1f4b..0c239e879 100644 --- a/client/render/Colors.cpp +++ b/client/render/Colors.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "Colors.h" +#include "../../lib/JsonNode.h" const ColorRGBA Colors::YELLOW = { 229, 215, 123, ColorRGBA::ALPHA_OPAQUE }; const ColorRGBA Colors::WHITE = { 255, 243, 222, ColorRGBA::ALPHA_OPAQUE }; @@ -23,3 +24,29 @@ const ColorRGBA Colors::RED = {255, 0, 0, ColorRGBA::ALPHA_OPAQUE}; const ColorRGBA Colors::PURPLE = {255, 75, 125, ColorRGBA::ALPHA_OPAQUE}; const ColorRGBA Colors::BLACK = {0, 0, 0, ColorRGBA::ALPHA_OPAQUE}; const ColorRGBA Colors::TRANSPARENCY = {0, 0, 0, ColorRGBA::ALPHA_TRANSPARENT}; + +std::optional Colors::parseColor(std::string text) +{ + std::smatch match; + std::regex expr("^#([0-9a-fA-F]{6})$"); + ui8 rgb[3] = {0, 0, 0}; + if(std::regex_search(text, match, expr)) + { + std::string tmp = boost::algorithm::unhex(match[1].str()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + + static const JsonNode config(JsonPath::builtin("CONFIG/textColors")); + auto colors = config["colors"].Struct(); + for(auto & color : colors) { + if(boost::algorithm::to_lower_copy(color.first) == boost::algorithm::to_lower_copy(text)) + { + std::string tmp = boost::algorithm::unhex(color.second.String()); + std::copy(tmp.begin(), tmp.end(), rgb); + return ColorRGBA(rgb[0], rgb[1], rgb[2]); + } + } + + return std::nullopt; +} \ No newline at end of file diff --git a/client/render/Colors.h b/client/render/Colors.h index 5db94e4ed..a73b5a856 100644 --- a/client/render/Colors.h +++ b/client/render/Colors.h @@ -49,4 +49,7 @@ public: static const ColorRGBA BLACK; static const ColorRGBA TRANSPARENCY; + + /// parse color + static std::optional parseColor(std::string text); }; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 88b9f38b9..e6cce7c1a 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -159,7 +159,7 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) while(std::regex_search(searchStart, what.cend(), match, expr)) { std::string colorText = match[1].str(); - if(CMessage::parseColor(colorText).a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + if(auto c = Colors::parseColor(colorText)) delimitersCount += f->getStringWidth(colorText + "|"); searchStart = match.suffix().first; } @@ -207,12 +207,11 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what) if(std::regex_search(toPrint, match, expr)) { std::string colorText = match[1].str(); - ColorRGBA color = CMessage::parseColor(colorText); - if(color.a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + if(auto color = Colors::parseColor(colorText)) { toPrint = toPrint.substr(colorText.length() + 1, toPrint.length() - colorText.length()); - to.drawText(where, font, color, ETextAlignment::TOPLEFT, toPrint); + to.drawText(where, font, *color, ETextAlignment::TOPLEFT, toPrint); } else to.drawText(where, font, Colors::YELLOW, ETextAlignment::TOPLEFT, toPrint); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index dc8cba620..ce508fa66 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -14,7 +14,6 @@ #include "../CGameInfo.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/TextOperations.h" -#include "../../lib/JsonNode.h" #include "../windows/InfoWindows.h" #include "../widgets/Buttons.h" @@ -158,7 +157,7 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi if(std::regex_search(tmp, match, expr)) { std::string colorText = match[1].str(); - if(CMessage::parseColor(colorText).a != Colors::TRANSPARENCY.ALPHA_TRANSPARENT) + if(auto c = Colors::parseColor(colorText)) { color = colorText + "|"; currPos += colorText.length() + 1; @@ -227,32 +226,6 @@ std::vector CMessage::breakText( std::string text, size_t maxLineWi return ret; } -ColorRGBA CMessage::parseColor(std::string text) -{ - std::smatch match; - std::regex expr("^#([0-9a-fA-F]{6})$"); - ui8 rgb[3] = {0, 0, 0}; - if(std::regex_search(text, match, expr)) - { - std::string tmp = boost::algorithm::unhex(match[1].str()); - std::copy(tmp.begin(), tmp.end(), rgb); - return ColorRGBA(rgb[0], rgb[1], rgb[2]); - } - - const JsonNode config(JsonPath::builtin("CONFIG/textColors")); - auto colors = config["colors"].Struct(); - for(auto & color : colors) { - if(boost::algorithm::to_lower_copy(color.first) == boost::algorithm::to_lower_copy(text)) - { - std::string tmp = boost::algorithm::unhex(color.second.String()); - std::copy(tmp.begin(), tmp.end(), rgb); - return ColorRGBA(rgb[0], rgb[1], rgb[2]); - } - } - - return Colors::TRANSPARENCY; -} - std::string CMessage::guessHeader(const std::string & msg) { size_t begin = 0; diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 12c5d0077..430c87a5b 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -36,9 +36,6 @@ public: /// split text in lines static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); - /// parse color - static ColorRGBA parseColor(std::string text); - /// Try to guess a header of a message static std::string guessHeader(const std::string & msg); From 621d463694b056cdeabbea0ba60ef707d330264e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 13 Sep 2023 22:02:10 +0200 Subject: [PATCH 0414/1248] color are now html color names --- config/textColors.json | 333 +++++++++++++++++++++-------------------- 1 file changed, 167 insertions(+), 166 deletions(-) diff --git a/config/textColors.json b/config/textColors.json index f15b0ac98..6873a64ab 100644 --- a/config/textColors.json +++ b/config/textColors.json @@ -1,170 +1,171 @@ { "colors": { - "r": "F80000", - "g": "00FC00", - "b": "0000F8", - "y": "F8FC00", - "o": "F88000", - "p": "F800F8", - "c": "00FCF8", - "k": "000000", - "w": "FFFFFF", - "Regular": "F8F0D8", - "Highlight": "E8D478", - "H3Red": "F80000", - "H3Cyan": "00FCF8", - "H3Green": "00FC00", - "H3Blue": "0000F8", - "H3Yellow": "F8FC00", - "H3Orange": "F88000", - "H3Purple": "F800F8", - "H3Pink": "C07888", - "AliceBlue": "F0F8FF", - "AntiqueWhite": "FAEBD7", - "Aqua": "00FFFF", - "Aquamarine": "7FFFD4", - "Azure": "F0FFFF", - "Beige": "F5F5DC", - "Bisque": "FFE4C4", - "Black": "000000", - "BlanchedAlmond": "FFEBCD", - "Blue": "0000FF", - "BlueViolet": "8A2BE2", - "Brown": "A52A2A", - "BurlyWood": "DEB887", - "CadetBlue": "5F9EA0", - "Chartreuse": "7FFF00", - "Chocolate": "D2691E", - "Coral": "FF7F50", - "CornflowerBlue": "6495ED", - "Cornsilk": "FFF8DC", - "Crimson": "DC143C", - "Cyan": "00FFFF", - "DarkBlue": "00008B", - "DarkCyan": "008B8B", - "DarkGoldenRod": "B8860B", - "DarkGray": "A9A9A9", - "DarkGrey": "A9A9A9", - "DarkGreen": "A9A9A9", - "DarkKhaki": "006400", - "DarkMagenta": "8B008B", - "DarkOliveGreen": "556B2F", - "Darkorange": "FF8C00", - "DarkOrchid": "9932CC", - "DarkRed": "8B0000", - "DarkSalmon": "E9967A", - "DarkSeaGreen": "8FBC8F", - "DarkSlateBlue": "483D8B", - "DarkSlateGray": "2F4F4F", - "DarkSlateGrey": "2F4F4F", - "DarkTurquoise": "00CED1", - "DarkViolet": "9400D3", - "DeepPink": "FF1493", - "DeepSkyBlue": "00BFFF", - "DimGray": "696969", - "DimGrey": "696969", - "DodgerBlue": "1E90FF", - "FireBrick": "B22222", - "FloralWhite": "FFFAF0", - "ForestGreen": "228B22", - "Fuchsia": "FF00FF", - "Gainsboro": "DCDCDC", - "GhostWhite": "F8F8FF", - "Gold": "FFD700", - "GoldenRod": "DAA520", - "Gray": "808080", - "Grey": "808080", - "Green": "008000", - "GreenYellow": "ADFF2F", - "HoneyDew": "F0FFF0", - "HotPink": "FF69B4", - "IndianRed": "CD5C5C", - "Indigo": "4B0082", - "Ivory": "FFFFF0", - "Khaki": "F0E68C", - "Lavender": "E6E6FA", - "LavenderBlush": "FFF0F5", - "LawnGreen": "7CFC00", - "LemonChiffon": "FFFACD", - "LightBlue": "ADD8E6", - "LightCoral": "F08080", - "LightCyan": "E0FFFF", - "LightGoldenRodYellow": "FAFAD2", - "LightGray": "D3D3D3", - "LightGrey": "D3D3D3", - "LightGreen": "90EE90", - "LightPink": "FFB6C1", - "LightSalmon": "FFA07A", - "LightSeaGreen": "20B2AA", - "LightSkyBlue": "87CEFA", - "LightSlateGray": "778899", - "LightSlateGrey": "778899", - "LightSteelBlue": "B0C4DE", - "LightYellow": "FFFFE0", - "Lime": "00FF00", - "LimeGreen": "32CD32", - "Linen": "FAF0E6", - "Magenta": "FF00FF", - "Maroon": "800000", - "MediumAquaMarine": "66CDAA", - "MediumBlue": "0000CD", - "MediumOrchid": "BA55D3", - "MediumPurple": "9370D8", - "MediumSeaGreen": "3CB371", - "MediumSlateBlue": "7B68EE", - "MediumSpringGreen": "00FA9A", - "MediumTurquoise": "48D1CC", - "MediumVioletRed": "C71585", - "MidnightBlue": "191970", - "MintCream": "F5FFFA", - "MistyRose": "FFE4E1", - "Moccasin": "FFE4B5", - "NavajoWhite": "FFDEAD", - "Navy": "000080", - "OldLace": "FDF5E6", - "Olive": "808000", - "OliveDrab": "6B8E23", - "Orange": "FFA500", - "OrangeRed": "FF4500", - "Orchid": "DA70D6", - "PaleGoldenRod": "EEE8AA", - "PaleGreen": "98FB98", - "PaleTurquoise": "AFEEEE", - "PaleVioletRed": "D87093", - "PapayaWhip": "FFEFD5", - "PeachPuff": "FFDAB9", - "Peru": "CD853F", - "Pink": "FFC0CB", - "Plum": "DDA0DD", - "PowderBlue": "B0E0E6", - "Purple": "800080", - "Red": "FF0000", - "RosyBrown": "BC8F8F", - "RoyalBlue": "4169E1", - "SaddleBrown": "8B4513", - "Salmon": "FA8072", - "SandyBrown": "F4A460", - "SeaGreen": "2E8B57", - "SeaShell": "FFF5EE", - "Sienna": "A0522D", - "Silver": "C0C0C0", - "SkyBlue": "87CEEB", - "SlateBlue": "6A5ACD", - "SlateGray": "708090", - "SlateGrey": "708090", - "Snow": "FFFAFA", - "SpringGreen": "00FF7F", - "SteelBlue": "4682B4", - "Tan": "D2B48C", - "Teal": "008080", - "Thistle": "D8BFD8", - "Tomato": "FF6347", - "Turquoise": "40E0D0", - "Violet": "EE82EE", - "Wheat": "F5DEB3", - "White": "FFFFFF", - "WhiteSmoke": "F5F5F5", - "Yellow": "FFFF00", - "YellowGreen": "9ACD32" + "r": "F80000", + "g": "00FC00", + "b": "0000F8", + "y": "F8FC00", + "o": "F88000", + "p": "F800F8", + "c": "00FCF8", + "k": "000000", + "w": "FFFFFF", + "Regular": "F8F0D8", + "Highlight": "E8D478", + "H3Red": "F80000", + "H3Cyan": "00FCF8", + "H3Green": "00FC00", + "H3Blue": "0000F8", + "H3Yellow": "F8FC00", + "H3Orange": "F88000", + "H3Purple": "F800F8", + "H3Pink": "C07888", + "Black": "000000", + "Navy": "000080", + "DarkBlue": "00008B", + "MediumBlue": "0000CD", + "Blue": "0000FF", + "DarkGreen": "006400", + "Green": "008000", + "Teal": "008080", + "DarkCyan": "008B8B", + "DeepSkyBlue": "00BFFF", + "DarkTurquoise": "00CED1", + "MediumSpringGreen": "00FA9A", + "Lime": "00FF00", + "SpringGreen": "00FF7F", + "Aqua": "00FFFF", + "Cyan": "00FFFF", + "MidnightBlue": "191970", + "DodgerBlue": "1E90FF", + "LightSeaGreen": "20B2AA", + "ForestGreen": "228B22", + "SeaGreen": "2E8B57", + "DarkSlateGray": "2F4F4F", + "DarkSlateGrey": "2F4F4F", + "LimeGreen": "32CD32", + "MediumSeaGreen": "3CB371", + "Turquoise": "40E0D0", + "RoyalBlue": "4169E1", + "SteelBlue": "4682B4", + "DarkSlateBlue": "483D8B", + "MediumTurquoise": "48D1CC", + "Indigo": "4B0082", + "DarkOliveGreen": "556B2F", + "CadetBlue": "5F9EA0", + "CornflowerBlue": "6495ED", + "RebeccaPurple": "663399", + "MediumAquaMarine": "66CDAA", + "DimGray": "696969", + "DimGrey": "696969", + "SlateBlue": "6A5ACD", + "OliveDrab": "6B8E23", + "SlateGray": "708090", + "SlateGrey": "708090", + "LightSlateGray": "778899", + "LightSlateGrey": "778899", + "MediumSlateBlue": "7B68EE", + "LawnGreen": "7CFC00", + "Chartreuse": "7FFF00", + "Aquamarine": "7FFFD4", + "Maroon": "800000", + "Purple": "800080", + "Olive": "808000", + "Gray": "808080", + "Grey": "808080", + "SkyBlue": "87CEEB", + "LightSkyBlue": "87CEFA", + "BlueViolet": "8A2BE2", + "DarkRed": "8B0000", + "DarkMagenta": "8B008B", + "SaddleBrown": "8B4513", + "DarkSeaGreen": "8FBC8F", + "LightGreen": "90EE90", + "MediumPurple": "9370DB", + "DarkViolet": "9400D3", + "PaleGreen": "98FB98", + "DarkOrchid": "9932CC", + "YellowGreen": "9ACD32", + "Sienna": "A0522D", + "Brown": "A52A2A", + "DarkGray": "A9A9A9", + "DarkGrey": "A9A9A9", + "LightBlue": "ADD8E6", + "GreenYellow": "ADFF2F", + "PaleTurquoise": "AFEEEE", + "LightSteelBlue": "B0C4DE", + "PowderBlue": "B0E0E6", + "FireBrick": "B22222", + "DarkGoldenRod": "B8860B", + "MediumOrchid": "BA55D3", + "RosyBrown": "BC8F8F", + "DarkKhaki": "BDB76B", + "Silver": "C0C0C0", + "MediumVioletRed": "C71585", + "IndianRed": "CD5C5C", + "Peru": "CD853F", + "Chocolate": "D2691E", + "Tan": "D2B48C", + "LightGray": "D3D3D3", + "LightGrey": "D3D3D3", + "Thistle": "D8BFD8", + "Orchid": "DA70D6", + "GoldenRod": "DAA520", + "PaleVioletRed": "DB7093", + "Crimson": "DC143C", + "Gainsboro": "DCDCDC", + "Plum": "DDA0DD", + "BurlyWood": "DEB887", + "LightCyan": "E0FFFF", + "Lavender": "E6E6FA", + "DarkSalmon": "E9967A", + "Violet": "EE82EE", + "PaleGoldenRod": "EEE8AA", + "LightCoral": "F08080", + "Khaki": "F0E68C", + "AliceBlue": "F0F8FF", + "HoneyDew": "F0FFF0", + "Azure": "F0FFFF", + "SandyBrown": "F4A460", + "Wheat": "F5DEB3", + "Beige": "F5F5DC", + "WhiteSmoke": "F5F5F5", + "MintCream": "F5FFFA", + "GhostWhite": "F8F8FF", + "Salmon": "FA8072", + "AntiqueWhite": "FAEBD7", + "Linen": "FAF0E6", + "LightGoldenRodYellow": "FAFAD2", + "OldLace": "FDF5E6", + "Red": "FF0000", + "Fuchsia": "FF00FF", + "Magenta": "FF00FF", + "DeepPink": "FF1493", + "OrangeRed": "FF4500", + "Tomato": "FF6347", + "HotPink": "FF69B4", + "Coral": "FF7F50", + "DarkOrange": "FF8C00", + "LightSalmon": "FFA07A", + "Orange": "FFA500", + "LightPink": "FFB6C1", + "Pink": "FFC0CB", + "Gold": "FFD700", + "PeachPuff": "FFDAB9", + "NavajoWhite": "FFDEAD", + "Moccasin": "FFE4B5", + "Bisque": "FFE4C4", + "MistyRose": "FFE4E1", + "BlanchedAlmond": "FFEBCD", + "PapayaWhip": "FFEFD5", + "LavenderBlush": "FFF0F5", + "SeaShell": "FFF5EE", + "Cornsilk": "FFF8DC", + "LemonChiffon": "FFFACD", + "FloralWhite": "FFFAF0", + "Snow": "FFFAFA", + "Yellow": "FFFF00", + "LightYellow": "FFFFE0", + "Ivory": "FFFFF0", + "White": "FFFFFF" } } From c1c13cfafbae674aa3f09fae8b7a844ce5332f71 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 15 Sep 2023 15:29:41 +0200 Subject: [PATCH 0415/1248] Pandora and events work as rewardable object --- lib/mapObjects/CGPandoraBox.cpp | 520 +++++++----------------- lib/mapObjects/CGPandoraBox.h | 42 +- lib/mapObjects/CQuest.cpp | 30 +- lib/mapObjects/CQuest.h | 6 +- lib/mapObjects/CRewardableObject.cpp | 1 - lib/mapping/MapFormatH3M.cpp | 48 ++- lib/rmg/modificators/TreasurePlacer.cpp | 64 ++- 7 files changed, 236 insertions(+), 475 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 9a4dd89e8..d4787f306 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -25,297 +25,117 @@ VCMI_LIB_NAMESPACE_BEGIN +void CGPandoraBox::init() +{ + blockVisit = true; + + for(auto & i : configuration.info) + i.reward.removeObject = true; +} + void CGPandoraBox::initObj(CRandomGenerator & rand) { - blockVisit = (ID==Obj::PANDORAS_BOX); //block only if it's really pandora's box (events also derive from that class) - hasGuardians = stacks.size(); + init(); + + CRewardableObject::initObj(rand); } void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const { - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - bd.text.appendLocalString (EMetaText::ADVOB_TXT, 14); - cb->showBlockingDialog (&bd); -} - -void CGPandoraBox::giveContentsUpToExp(const CGHeroInstance *h) const -{ - afterSuccessfulVisit(); - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - - bool changesPrimSkill = false; - for(const auto & elem : primskills) + auto setText = [](MetaString & text, int tId, const CGHeroInstance * h) { - if(elem) + text.appendLocalString(EMetaText::ADVOB_TXT, tId); + text.replaceRawString(h->getNameTranslated()); + }; + + for(auto i : getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT)) + { + MetaString txt; + auto & r = configuration.info[i]; + + if(r.reward.spells.size() == 1) + setText(txt, 184, h); + else if(!r.reward.spells.empty()) + setText(txt, 188, h); + + if(r.reward.heroExperience || !r.reward.secondary.empty()) + setText(txt, 175, h); + + for(int ps : r.reward.primary) { - changesPrimSkill = true; - break; - } - } - - std::vector> unpossessedAbilities; //ability + ability level - int abilitiesRequiringSlot = 0; - - //filter out unnecessary secondary skills - for (int i = 0; i < abilities.size(); i++) - { - int curLev = h->getSecSkillLevel(abilities[i]); - bool abilityCanUseSlot = !curLev && ((h->secSkills.size() + abilitiesRequiringSlot) < GameConstants::SKILL_PER_HERO); //limit new abilities to number of slots - - if (abilityCanUseSlot) - abilitiesRequiringSlot++; - - if ((curLev && curLev < abilityLevels[i]) || abilityCanUseSlot) - { - unpossessedAbilities.emplace_back(abilities[i], abilityLevels[i]); - } - } - - if(gainedExp || changesPrimSkill || !unpossessedAbilities.empty()) - { - TExpType expVal = h->calculateXp(gainedExp); - //getText(iw,afterBattle,175,h); //wtf? - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 175); //%s learns something - iw.text.replaceRawString(h->getNameTranslated()); - - if(expVal) - iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(expVal), 0); - - for(int i=0; ishowInfoDialog(&iw); - - //give sec skills - for(const auto & abilityData : unpossessedAbilities) - cb->changeSecSkill(h, abilityData.first, abilityData.second, true); - - assert(h->secSkills.size() <= GameConstants::SKILL_PER_HERO); - - //give prim skills - for(int i=0; ichangePrimSkill(h,static_cast(i),primskills[i],false); - - assert(!cb->isVisitCoveredByAnotherQuery(this, h)); - - //give exp - if(expVal) - cb->changePrimSkill(h, PrimarySkill::EXPERIENCE, expVal, false); - } - //else { } //TODO:Create information that box was empty for now, and deliver to CGPandoraBox::giveContentsAfterExp or refactor - - if(!cb->isVisitCoveredByAnotherQuery(this, h)) - giveContentsAfterExp(h); - //Otherwise continuation occurs via post-level-up callback. -} - -void CGPandoraBox::giveContentsAfterExp(const CGHeroInstance *h) const -{ - bool hadGuardians = hasGuardians; //copy, because flag will be emptied after issuing first post-battle message - - std::string msg = message; //in case box is removed in the meantime - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - - //TODO: reuse this code for Scholar skill - if(!spells.empty()) - { - std::set spellsToGive; - - auto i = spells.cbegin(); - while (i != spells.cend()) - { - iw.components.clear(); - iw.text.clear(); - spellsToGive.clear(); - - for (; i != spells.cend(); i++) + if(ps) { - const auto * spell = (*i).toSpell(VLC->spells()); - if(h->canLearnSpell(spell)) - { - iw.components.emplace_back(Component::EComponentType::SPELL, *i, 0, 0); - spellsToGive.insert(*i); - } - if(spellsToGive.size() == 8) //display up to 8 spells at once - { - break; - } - } - if (!spellsToGive.empty()) - { - if (spellsToGive.size() > 1) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 188); //%s learns spells - } - else - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 184); //%s learns a spell - } - iw.text.replaceRawString(h->getNameTranslated()); - cb->changeSpells(h, true, spellsToGive); - cb->showInfoDialog(&iw); + setText(txt, 175, h); + break; } } - } - - if(manaDiff) - { - getText(iw,hadGuardians,manaDiff,176,177,h); - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 5, manaDiff, 0); - cb->showInfoDialog(&iw); - cb->setManaPoints(h->id, h->mana + manaDiff); - } - - if(moraleDiff) - { - getText(iw,hadGuardians,moraleDiff,178,179,h); - iw.components.emplace_back(Component::EComponentType::MORALE, 0, moraleDiff, 0); - cb->showInfoDialog(&iw); - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE,BonusType::MORALE,BonusSource::OBJECT,moraleDiff,id.getNum(),""); - gb.id = h->id.getNum(); - cb->giveHeroBonus(&gb); - } - - if(luckDiff) - { - getText(iw,hadGuardians,luckDiff,180,181,h); - iw.components.emplace_back(Component::EComponentType::LUCK, 0, luckDiff, 0); - cb->showInfoDialog(&iw); - GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE,BonusType::LUCK,BonusSource::OBJECT,luckDiff,id.getNum(),""); - gb.id = h->id.getNum(); - cb->giveHeroBonus(&gb); - } - - iw.components.clear(); - iw.text.clear(); - for(int i=0; ishowInfoDialog(&iw); - } - - iw.components.clear(); - iw.text.clear(); - for(int i=0; i 0) - iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0); - } - if(!iw.components.empty()) - { - getText(iw,hadGuardians,183,h); - cb->showInfoDialog(&iw); - } - - iw.components.clear(); - // getText(iw,afterBattle,183,h); - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure - iw.text.replaceRawString(h->getNameTranslated()); - for(const auto & elem : artifacts) - { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); - if(iw.components.size() >= 14) + + if(r.reward.manaDiff < 0) + setText(txt, 176, h); + if(r.reward.manaDiff > 0) + setText(txt, 177, h); + + for(auto b : r.reward.bonuses) { - cb->showInfoDialog(&iw); - iw.components.clear(); - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 183); //% has found treasure - once more? - iw.text.replaceRawString(h->getNameTranslated()); + if(b.type == BonusType::MORALE) + { + if(b.val < 0) + setText(txt, 178, h); + if(b.val > 0) + setText(txt, 179, h); + } + if(b.type == BonusType::LUCK) + { + if(b.val < 0) + setText(txt, 180, h); + if(b.val > 0) + setText(txt, 181, h); + } } - } - if(!iw.components.empty()) - { - cb->showInfoDialog(&iw); - } - - cb->giveResources(h->getOwner(), resources); - - for(const auto & elem : artifacts) - cb->giveHeroNewArtifact(h, VLC->arth->objects[elem],ArtifactPosition::FIRST_AVAILABLE); - - iw.components.clear(); - iw.text.clear(); - - if(creatures.stacksCount()) - { //this part is taken straight from creature bank - MetaString loot; - for(const auto & elem : creatures.Slots()) - { //build list of joined creatures - iw.components.emplace_back(*elem.second); - loot.appendRawString("%s"); - loot.replaceCreatureName(*elem.second); + + for(auto res : r.reward.resources) + { + if(res < 0) + setText(txt, 182, h); + if(res > 0) + setText(txt, 183, h); } - - if(creatures.stacksCount() == 1 && creatures.Slots().begin()->second->count == 1) - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 185); - else - iw.text.appendLocalString(EMetaText::ADVOB_TXT, 186); - - iw.text.replaceRawString(loot.buildList()); - iw.text.replaceRawString(h->getNameTranslated()); - - cb->showInfoDialog(&iw); - cb->giveCreatures(this, h, creatures, false); - } - if(!hasGuardians && !msg.empty()) - { - iw.text.appendRawString(msg); - cb->showInfoDialog(&iw); - } -} - -void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int text, const CGHeroInstance * h ) const -{ - if(afterBattle || message.empty()) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,text);//%s has lost treasure. - iw.text.replaceRawString(h->getNameTranslated()); - } - else - { - iw.text.appendRawString(message); - afterBattle = true; - } -} - -void CGPandoraBox::getText( InfoWindow &iw, bool &afterBattle, int val, int negative, int positive, const CGHeroInstance * h ) const -{ - iw.components.clear(); - iw.text.clear(); - if(afterBattle || message.empty()) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,val < 0 ? negative : positive); //%s's luck takes a turn for the worse / %s's luck increases - iw.text.replaceRawString(h->getNameTranslated()); - } - else - { - iw.text.appendRawString(message); - afterBattle = true; + + if(!r.reward.artifacts.empty()) + setText(txt, 183, h); + + if(!r.reward.creatures.empty()) + { + MetaString loot; + for(auto c : r.reward.creatures) + { + loot.appendRawString("%s"); + loot.replaceCreatureName(c); + } + + if(r.reward.creatures.size() == 1 && r.reward.creatures[0].count == 1) + txt.appendLocalString(EMetaText::ADVOB_TXT, 185); + else + txt.appendLocalString(EMetaText::ADVOB_TXT, 186); + + txt.replaceRawString(loot.buildList()); + txt.replaceRawString(h->getNameTranslated()); + } + + const_cast(r.message) = txt; } + + BlockingDialog bd (true, false); + bd.player = h->getOwner(); + bd.text.appendLocalString(EMetaText::ADVOB_TXT, 14); + cb->showBlockingDialog(&bd); } void CGPandoraBox::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const { if(result.winner == 0) { - giveContentsUpToExp(hero); + CRewardableObject::onHeroVisit(hero); } } @@ -328,117 +148,87 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe hero->showInfoDialog(16, 0, EInfoWindowMode::MODAL); cb->startBattleI(hero, this); //grants things after battle } - else if(message.empty() && resources.empty() - && primskills.empty() && abilities.empty() - && abilityLevels.empty() && artifacts.empty() - && spells.empty() && creatures.stacksCount() == 0 - && gainedExp == 0 && manaDiff == 0 && moraleDiff == 0 && luckDiff == 0) //if it gives nothing without battle + else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { hero->showInfoDialog(15); cb->removeObject(this); } else //if it gives something without battle { - giveContentsUpToExp(hero); + CRewardableObject::onHeroVisit(hero); } } } -void CGPandoraBox::heroLevelUpDone(const CGHeroInstance *hero) const -{ - giveContentsAfterExp(hero); -} - -void CGPandoraBox::afterSuccessfulVisit() const -{ - cb->removeAfterVisit(this); -} - void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "guards", 7); + CRewardableObject::serializeJsonOptions(handler); handler.serializeString("guardMessage", message); - - handler.serializeInt("experience", gainedExp, 0); - handler.serializeInt("mana", manaDiff, 0); - handler.serializeInt("morale", moraleDiff, 0); - handler.serializeInt("luck", luckDiff, 0); - - resources.serializeJson(handler, "resources"); - + + if(!handler.saving) { - bool haveSkills = false; - - if(handler.saving) - { - for(int primskill : primskills) - if(primskill != 0) - haveSkills = true; - } - else - { - primskills.resize(GameConstants::PRIMARY_SKILLS,0); - haveSkills = true; - } - - if(haveSkills) + //backward compatibility + CCreatureSet::serializeJson(handler, "guards", 7); + + Rewardable::VisitInfo vinfo; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + auto & reward = vinfo.reward; + int val; + handler.serializeInt("experience", reward.heroExperience, 0); + handler.serializeInt("mana", reward.manaDiff, 0); + handler.serializeInt("morale", val, 0); + if(val) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + handler.serializeInt("luck", val, 0); + if(val) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + + reward.resources.serializeJson(handler, "resources"); { auto s = handler.enterStruct("primarySkills"); - for(int idx = 0; idx < primskills.size(); idx ++) - handler.serializeInt(NPrimarySkill::names[idx], primskills[idx], 0); + for(int idx = 0; idx < reward.primary.size(); idx ++) + handler.serializeInt(NPrimarySkill::names[idx], reward.primary[idx], 0); } - } + + handler.serializeIdArray("artifacts", reward.artifacts); + handler.serializeIdArray("spells", reward.spells); - if(handler.saving) - { - if(!abilities.empty()) + handler.enterArray("creatures").serializeStruct(reward.creatures); + { auto s = handler.enterStruct("secondarySkills"); - - for(size_t idx = 0; idx < abilities.size(); idx++) + for(const auto & p : handler.getCurrent().Struct()) { - handler.serializeEnum(CSkillHandler::encodeSkill(abilities[idx]), abilityLevels[idx], NSecondarySkill::levels); + const std::string skillName = p.first; + const std::string levelId = p.second.String(); + + const int rawId = CSkillHandler::decodeSkill(skillName); + if(rawId < 0) + { + logGlobal->error("Invalid secondary skill %s", skillName); + continue; + } + + const int level = vstd::find_pos(NSecondarySkill::levels, levelId); + if(level < 0) + { + logGlobal->error("Invalid secondary skill level %s", levelId); + continue; + } + + reward.secondary[rawId] = level; } } + configuration.info.push_back(vinfo); } - else - { - auto s = handler.enterStruct("secondarySkills"); +} - const JsonNode & skillMap = handler.getCurrent(); - - abilities.clear(); - abilityLevels.clear(); - - for(const auto & p : skillMap.Struct()) - { - const std::string skillName = p.first; - const std::string levelId = p.second.String(); - - const int rawId = CSkillHandler::decodeSkill(skillName); - if(rawId < 0) - { - logGlobal->error("Invalid secondary skill %s", skillName); - continue; - } - - const int level = vstd::find_pos(NSecondarySkill::levels, levelId); - if(level < 0) - { - logGlobal->error("Invalid secondary skill level %s", levelId); - continue; - } - - abilities.emplace_back(rawId); - abilityLevels.push_back(level); - } - } - - - handler.serializeIdArray("artifacts", artifacts); - handler.serializeIdArray("spells", spells); - - creatures.serializeJson(handler, "creatures"); +void CGEvent::init() +{ + blockVisit = false; + + for(auto & i : configuration.info) + i.reward.removeObject = removeAfterVisit; } void CGEvent::onHeroVisit( const CGHeroInstance * h ) const @@ -470,27 +260,17 @@ void CGEvent::activated( const CGHeroInstance * h ) const } else { - giveContentsUpToExp(h); + CRewardableObject::onHeroVisit(h); } } -void CGEvent::afterSuccessfulVisit() const -{ - if(removeAfterVisit) - { - cb->removeAfterVisit(this); - } - else if(hasGuardians) - hasGuardians = false; -} - void CGEvent::serializeJsonOptions(JsonSerializeFormat & handler) { CGPandoraBox::serializeJsonOptions(handler); - handler.serializeBool("aIActivable", computerActivate, true, false, false); - handler.serializeBool("humanActivable", humanActivate, true, false, true); - handler.serializeBool("removeAfterVisit", removeAfterVisit, true, false, false); + handler.serializeBool("aIActivable", computerActivate, false); + handler.serializeBool("humanActivable", humanActivate, true); + handler.serializeBool("removeAfterVisit", removeAfterVisit, false); handler.serializeIdArray("availableFor", availableFor); } diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index 6cef83266..240e9e68d 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -9,63 +9,31 @@ */ #pragma once -#include "CArmedInstance.h" +#include "CRewardableObject.h" #include "../ResourceSet.h" VCMI_LIB_NAMESPACE_BEGIN struct InfoWindow; -class DLL_LINKAGE CGPandoraBox : public CArmedInstance +class DLL_LINKAGE CGPandoraBox : public CRewardableObject { public: std::string message; - mutable bool hasGuardians = false; //helper - after battle even though we have no stacks, allows us to know that there was battle - - //gained things: - ui32 gainedExp = 0; - si32 manaDiff = 0; //amount of gained / lost mana - si32 moraleDiff = 0; //morale modifier - si32 luckDiff = 0; //luck modifier - TResources resources;//gained / lost resources - std::vector primskills;//gained / lost prim skills - std::vector abilities; //gained abilities - std::vector abilityLevels; //levels of gained abilities - std::vector artifacts; //gained artifacts - std::vector spells; //gained spells - CCreatureSet creatures; //gained creatures void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - void heroLevelUpDone(const CGHeroInstance *hero) const override; template void serialize(Handler &h, const int version) { - h & static_cast(*this); + h & static_cast(*this); h & message; - h & hasGuardians; - h & gainedExp; - h & manaDiff; - h & moraleDiff; - h & luckDiff; - h & resources; - h & primskills; - h & abilities; - h & abilityLevels; - h & artifacts; - h & spells; - h & creatures; } protected: - void giveContentsUpToExp(const CGHeroInstance *h) const; - void giveContentsAfterExp(const CGHeroInstance *h) const; + virtual void init(); void serializeJsonOptions(JsonSerializeFormat & handler) override; -private: - void getText( InfoWindow &iw, bool &afterBattle, int val, int negative, int positive, const CGHeroInstance * h ) const; - void getText( InfoWindow &iw, bool &afterBattle, int text, const CGHeroInstance * h ) const; - virtual void afterSuccessfulVisit() const; }; class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects @@ -87,10 +55,10 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; protected: + void init() override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: void activated(const CGHeroInstance * h) const; - void afterSuccessfulVisit() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 01d71bf19..3dae6194b 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -794,20 +794,11 @@ void CGSeerHut::afterAddToMap(CMap* map) void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) { //quest and reward + CRewardableObject::serializeJsonOptions(handler); quest->serializeJson(handler, "quest"); - - if(handler.saving) + + if(!handler.saving) { - CRewardableObject::serializeJsonOptions(handler); - } - else - { - if(handler.getCurrent()["reward"].isNull() || handler.getCurrent()["reward"].Struct().empty()) - { - CRewardableObject::serializeJsonOptions(handler); - return; - } - //backward compatibility auto s = handler.enterStruct("reward"); const JsonNode & rewardsJson = handler.getCurrent(); @@ -827,7 +818,8 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) int val = 0; handler.serializeInt(fullIdentifier, val); - Rewardable::Reward reward; + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; if(metaTypeName == "experience") reward.heroExperience = val; if(metaTypeName == "mana") @@ -867,9 +859,8 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) reward.creatures.emplace_back(rawId, val); } - configuration.info.push_back({}); - configuration.info.back().reward = reward; - configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.info.push_back(vinfo); } } @@ -881,14 +872,10 @@ void CGQuestGuard::init(CRandomGenerator & rand) configuration.info.push_back({}); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.info.back().reward.removeObject = true; configuration.canRefuse = true; } -void CGQuestGuard::completeQuest() const -{ - cb->removeObject(this); -} - void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) { //quest only, do not call base class @@ -946,7 +933,6 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const void CGBorderGuard::initObj(CRandomGenerator & rand) { - //ui32 m13489val = subID; //store color as quest info blockVisit = true; } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 963f29499..7cb0df2bc 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -162,11 +162,8 @@ public: template void serialize(Handler &h, const int version) { - h & static_cast(*this); + h & static_cast(*this); h & static_cast(*this); - //h & rewardType; - //h & rID; - //h & rVal; h & seerName; } protected: @@ -181,7 +178,6 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: void init(CRandomGenerator & rand) override; - void completeQuest() const override; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 31c17e6e7..c07b6da44 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -271,7 +271,6 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const void CRewardableObject::initObj(CRandomGenerator & rand) { VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); - assert(!configuration.info.empty()); } CRewardableObject::CRewardableObject() diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index a3b4b16df..4d062659b 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1019,34 +1019,46 @@ CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition) void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition) { readMessageAndGuards(object->message, object, mapPosition); + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + + reward.heroExperience = reader->readUInt32(); + reward.manaDiff = reader->readInt32(); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), object->id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), object->id); - object->gainedExp = reader->readUInt32(); - object->manaDiff = reader->readInt32(); - object->moraleDiff = reader->readInt8(); - object->luckDiff = reader->readInt8(); - - reader->readResourses(object->resources); - - object->primskills.resize(GameConstants::PRIMARY_SKILLS); + reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) - object->primskills[x] = reader->readUInt8(); + reward.primary.at(x) = reader->readUInt8(); int gabn = reader->readUInt8(); //number of gained abilities for(int oo = 0; oo < gabn; ++oo) { - object->abilities.emplace_back(reader->readSkill()); - object->abilityLevels.push_back(reader->readUInt8()); + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; } int gart = reader->readUInt8(); //number of gained artifacts for(int oo = 0; oo < gart; ++oo) - object->artifacts.emplace_back(reader->readArtifact()); + reward.artifacts.push_back(reader->readArtifact()); int gspel = reader->readUInt8(); //number of gained spells for(int oo = 0; oo < gspel; ++oo) - object->spells.emplace_back(reader->readSpell()); + reward.spells.push_back(reader->readSpell()); int gcre = reader->readUInt8(); //number of gained creatures - readCreatureSet(&object->creatures, gcre); + for(int oo = 0; oo < gcre; ++oo) + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + object->configuration.info.push_back(vinfo); + reader->skipZero(8); } @@ -1843,7 +1855,8 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) if(hut->quest->missionType) { auto rewardType = reader->readUInt8(); - Rewardable::Reward reward; + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; switch(rewardType) { case 0: //NOTHING @@ -1922,9 +1935,8 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) } } - hut->configuration.info.push_back({}); - hut->configuration.info.back().reward = reward; - hut->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + hut->configuration.info.push_back(vinfo); } else { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 0addaa481..c92637aea 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -235,7 +235,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - obj->resources[EGameResID::GOLD] = i * 5000; + + Rewardable::VisitInfo reward; + reward.reward.resources[EGameResID::GOLD] = i * 5000; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); @@ -251,7 +256,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - obj->gainedExp = i * 5000; + + Rewardable::VisitInfo reward; + reward.reward.heroExperience = i * 5000; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); @@ -307,8 +317,12 @@ void TreasurePlacer::addAllPossibleObjects() { auto factory = VLC->objtypeh->getHandlerFor(Obj::PANDORAS_BOX, 0); auto * obj = dynamic_cast(factory->create()); - auto * stack = new CStackInstance(creature, creaturesAmount); - obj->creatures.putStack(SlotID(0), stack); + + Rewardable::VisitInfo reward; + reward.reward.creatures.emplace_back(creature, creaturesAmount); + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); + return obj; }; oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); @@ -333,10 +347,13 @@ void TreasurePlacer::addAllPossibleObjects() } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(12, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; @@ -362,10 +379,13 @@ void TreasurePlacer::addAllPossibleObjects() } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(15, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; @@ -390,10 +410,13 @@ void TreasurePlacer::addAllPossibleObjects() } RandomGeneratorUtil::randomShuffle(spells, zone.getRand()); + Rewardable::VisitInfo reward; for(int j = 0; j < std::min(60, static_cast(spells.size())); j++) { - obj->spells.push_back(spells[j]->id); + reward.reward.spells.push_back(spells[j]->id); } + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); return obj; }; @@ -442,11 +465,10 @@ void TreasurePlacer::addAllPossibleObjects() auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - Rewardable::Reward reward; - reward.creatures.emplace_back(creature->getId(), creaturesAmount); - obj->configuration.info.push_back({}); - obj->configuration.info.back().reward = reward; - obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + Rewardable::VisitInfo reward; + reward.reward.creatures.emplace_back(creature->getId(), creaturesAmount); + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); obj->quest->missionType = CQuest::MISSION_ART; @@ -494,11 +516,10 @@ void TreasurePlacer::addAllPossibleObjects() auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - Rewardable::Reward reward; - reward.heroExperience = generator.getConfig().questRewardValues[i]; - obj->configuration.info.push_back({}); - obj->configuration.info.back().reward = reward; - obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + Rewardable::VisitInfo reward; + reward.reward.heroExperience = generator.getConfig().questRewardValues[i]; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); @@ -519,11 +540,10 @@ void TreasurePlacer::addAllPossibleObjects() auto factory = VLC->objtypeh->getHandlerFor(Obj::SEER_HUT, randomAppearance); auto * obj = dynamic_cast(factory->create()); - Rewardable::Reward reward; - reward.resources[EGameResID::GOLD] = generator.getConfig().questRewardValues[i]; - obj->configuration.info.push_back({}); - obj->configuration.info.back().reward = reward; - obj->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + Rewardable::VisitInfo reward; + reward.reward.resources[EGameResID::GOLD] = generator.getConfig().questRewardValues[i]; + reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + obj->configuration.info.push_back(reward); obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); From a630b9c394b3371efad6fc66ca33569d472db409 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 15 Sep 2023 15:38:45 +0200 Subject: [PATCH 0416/1248] doc --- docs/players/Game_Mechanics.md | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index c4c045e68..494ec85b0 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -42,6 +42,31 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav Mouse click on castle icon in the town screen open quick recruitment window, where we can purhase in fast way units. +## Color support + +Additional color are supported for text fields (e.g. map description). Uses HTML color syntax (e.g. #abcdef) / HTML predefined colors (e.g. green). + +##### Original Heroes III Support + +`This is white` + +This is white + +`{This is yellow}` + +This is yellow + +##### New + +`{#ff0000|This is red}` + +This is red + +`{green|This is green}` + +This is green + + # Manuals and guides -- https://heroes.thelazy.net//index.php/Main_Page Wiki that aims to be a complete reference to Heroes of Might and Magic III. +- https://heroes.thelazy.net/index.php/Main_Page Wiki that aims to be a complete reference to Heroes of Might and Magic III. From 297cc12c841a35abbb5133fa24061085edf948cf Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 10:26:31 -0400 Subject: [PATCH 0417/1248] Prefer symlink word --- docs/players/Installation_macOS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/players/Installation_macOS.md b/docs/players/Installation_macOS.md index 6965c3c3a..2b15f6a14 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -16,4 +16,4 @@ Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi # Step 2: Installing Heroes III data files 1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Wineskin](https://github.com/Gcenx/WineskinServer). -2. Copy (or symlink) **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` +2. Symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` From de82a423411e1316d9d159da6b98db08d9079205 Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 11:28:44 -0400 Subject: [PATCH 0418/1248] Update Installation_macOS.md --- docs/players/Installation_macOS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/players/Installation_macOS.md b/docs/players/Installation_macOS.md index 2b15f6a14..9a9adcd97 100644 --- a/docs/players/Installation_macOS.md +++ b/docs/players/Installation_macOS.md @@ -16,4 +16,4 @@ Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi # Step 2: Installing Heroes III data files 1. Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Wineskin](https://github.com/Gcenx/WineskinServer). -2. Symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` +2. Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/` From 1af193516ff3c2f1306c6252e9fd84b24ef561bd Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 11:30:17 -0400 Subject: [PATCH 0419/1248] Update Installation_iOS.md --- docs/players/Installation_iOS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index 40e889697..46d88c99c 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -28,7 +28,7 @@ To play the game, you need to upload HoMM3 data files - **Data**, **Maps** and * If you have data somewhere on device or in shared folder or you have downloaded it, you can copy it directly on your iPhone/iPad using Files application. -Move or copy **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders. +Place **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders. ### Step 2.c: Installing data files with Xcode on macOS From 572a610376a0e6497189a510abc87847bb1882d1 Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 11:30:33 -0400 Subject: [PATCH 0420/1248] Update Installation_Windows.md --- docs/players/Installation_Windows.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/players/Installation_Windows.md b/docs/players/Installation_Windows.md index c8857b940..900b9e711 100644 --- a/docs/players/Installation_Windows.md +++ b/docs/players/Installation_Windows.md @@ -17,7 +17,7 @@ Install one of following into new folder, same as when installing new game: **Since VCMI 1.2 you can skip this step, just run VCMI launcher and it will help you with importing H3 data. For older releases you can follow this step.** - Install Heroes III from disk or using GOG installer. -- Copy "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` +- Place "Data", "Maps" and "Mp3" from Heroes III to: `Documents\My Games\vcmi\` Create this folder if it doesnt exist yet From d1b32365252cfaff0b12dbfeb08c3d26462f7ede Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 11:30:58 -0400 Subject: [PATCH 0421/1248] Update Installation_Linux.md --- docs/players/Installation_Linux.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index ef650d545..e5418a768 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -101,7 +101,7 @@ innoextract --output-dir=~/Downloads/HoMM3 "setup_heroes_of_might_and_magic_3_co ``` (note that installer file name might be different) -Once innoextract completes, start VCMI Launcher and choose to copy existing files. Select the ~/Downloads/HoMM3 directory. Once copy is complete, you can delete both offline installer files as well as ~/Downloads/HoMM3. +Once innoextract completes, start VCMI Launcher and choose to place existing files. Select the ~/Downloads/HoMM3 directory. Once placing is complete, you can delete both offline installer files as well as ~/Downloads/HoMM3. ## Install manually using existing Heroes III data @@ -118,4 +118,4 @@ Or, to start game directly avoiding Launcher: `vcmiclient` # Reporting bugs -Please report any issues with packages according to [Bug Reporting Guidelines](Bug_Reporting_Guidelines.md) \ No newline at end of file +Please report any issues with packages according to [Bug Reporting Guidelines](Bug_Reporting_Guidelines.md) From 86256429c67e5737d40fd6bce3138dea2dda401a Mon Sep 17 00:00:00 2001 From: William Entriken Date: Fri, 15 Sep 2023 11:31:15 -0400 Subject: [PATCH 0422/1248] Update Installation_Android.md --- docs/players/Installation_Android.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/players/Installation_Android.md b/docs/players/Installation_Android.md index 0c1fa2ac9..be77110ab 100644 --- a/docs/players/Installation_Android.md +++ b/docs/players/Installation_Android.md @@ -23,7 +23,7 @@ Installation is a two step process, at first you need to install game, then you ### I imported game data files, but music in game is missing -**Solution:** Try to run data import again or copy Mp3 folder from Heroes III manually to Android/data/is.xyz.vcmi/files/vcmi-data/Mp3 +**Solution:** Try to run data import again or place Mp3 folder from Heroes III manually to Android/data/is.xyz.vcmi/files/vcmi-data/Mp3 ### I installed google play version, but have a problem while installing daily builds @@ -39,4 +39,4 @@ Installation is a two step process, at first you need to install game, then you ## Other problems -Please report about gameplay problem: [Github](https://github.com/vcmi/vcmi/issues), [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) or [Discord](https://discord.gg/chBT42V). Make sure to specify your device and used version of Android. \ No newline at end of file +Please report about gameplay problem: [Github](https://github.com/vcmi/vcmi/issues), [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) or [Discord](https://discord.gg/chBT42V). Make sure to specify your device and used version of Android. From ffaf4c5b0104195a6f649f23f12c7386684d1532 Mon Sep 17 00:00:00 2001 From: krs Date: Fri, 15 Sep 2023 22:03:23 +0300 Subject: [PATCH 0423/1248] Fix broken skill schema --- config/schemas/skill.json | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/config/schemas/skill.json b/config/schemas/skill.json index 12a77f4af..10973ccbf 100644 --- a/config/schemas/skill.json +++ b/config/schemas/skill.json @@ -63,19 +63,18 @@ }, "gainChance" : { "description" : "Chance for the skill to be offered on level-up (heroClass may override)", - "type" : "object", - "required" : ["might", "magic"], - "properties" : { - "might" : { - "type" : "number", - "description" : "Chance for hero classes with might affinity" - }, - "magic" : { - "type" : "number", - "description" : "Chance for hero classes with magic affinity" - } + "type" : "object", + "required" : ["might", "magic"], + "properties" : { + "might" : { + "type" : "number", + "description" : "Chance for hero classes with might affinity" + }, + "magic" : { + "type" : "number", + "description" : "Chance for hero classes with magic affinity" } - ] + } }, "base" : { "type" : "object", From 68c61797f84e731c6d42eabf38caacd1650bbf9e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 15 Sep 2023 21:08:14 +0200 Subject: [PATCH 0424/1248] Implement select all option for rewardable objects --- lib/mapObjects/CGPandoraBox.cpp | 40 +++++++++++++++++++++++++--- lib/mapObjects/CRewardableObject.cpp | 3 +++ lib/rewardable/Configuration.h | 3 ++- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index d4787f306..fc87a763c 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -28,6 +28,7 @@ VCMI_LIB_NAMESPACE_BEGIN void CGPandoraBox::init() { blockVisit = true; + configuration.selectMode = Rewardable::SELECT_ALL; for(auto & i : configuration.info) i.reward.removeObject = true; @@ -169,33 +170,62 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { //backward compatibility CCreatureSet::serializeJson(handler, "guards", 7); + Rewardable::Reward reward; + + auto addReward = [this, &reward](bool condition) + { + if(condition) + { + configuration.info.emplace_back(); + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + configuration.info.back().reward = reward; + } + }; + + addReward(true); - Rewardable::VisitInfo vinfo; - vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - auto & reward = vinfo.reward; int val; handler.serializeInt("experience", reward.heroExperience, 0); + addReward(reward.heroExperience); + handler.serializeInt("mana", reward.manaDiff, 0); + addReward(reward.manaDiff); + handler.serializeInt("morale", val, 0); if(val) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + addReward(val); + handler.serializeInt("luck", val, 0); if(val) reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + addReward(val); reward.resources.serializeJson(handler, "resources"); + addReward(reward.resources.nonZero()); + { + bool updateReward = false; auto s = handler.enterStruct("primarySkills"); for(int idx = 0; idx < reward.primary.size(); idx ++) + { handler.serializeInt(NPrimarySkill::names[idx], reward.primary[idx], 0); + updateReward |= reward.primary[idx]; + } + addReward(updateReward); } handler.serializeIdArray("artifacts", reward.artifacts); + addReward(!reward.artifacts.empty()); + handler.serializeIdArray("spells", reward.spells); + addReward(!reward.spells.empty()); handler.enterArray("creatures").serializeStruct(reward.creatures); + addReward(!reward.creatures.empty()); { + bool updateReward = false; auto s = handler.enterStruct("secondarySkills"); for(const auto & p : handler.getCurrent().Struct()) { @@ -217,15 +247,17 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) } reward.secondary[rawId] = level; + updateReward = true; } + addReward(updateReward); } - configuration.info.push_back(vinfo); } } void CGEvent::init() { blockVisit = false; + configuration.selectMode = Rewardable::SELECT_ALL; for(auto & i : configuration.info) i.reward.removeObject = removeAfterVisit; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index c07b6da44..e4e3932c8 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -108,6 +108,9 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const case Rewardable::SELECT_RANDOM: // give random grantRewardWithMessage(*RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); break; + case Rewardable::SELECT_ALL: // give all rewards + for(auto i : rewards) + grantRewardWithMessage(i, i == rewards.size() - 1); } break; } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 72377da04..de8764f86 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -35,6 +35,7 @@ enum ESelectMode SELECT_FIRST, // first reward that matches limiters SELECT_PLAYER, // player can select from all allowed rewards SELECT_RANDOM, // one random reward from all mathing limiters + SELECT_ALL, // provides all allowed rewards matching limiters }; enum class EEventType @@ -45,7 +46,7 @@ enum class EEventType EVENT_NOT_AVAILABLE }; -const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom"}; +const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"}; const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "player"}; struct DLL_LINKAGE ResetInfo From 4af2d917c0439b97e97c15c08a842f7ed3f8a959 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 15 Sep 2023 21:13:07 +0200 Subject: [PATCH 0425/1248] Fix messages for pandoras --- lib/mapObjects/CGPandoraBox.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index fc87a763c..d6b9503be 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -45,6 +45,7 @@ void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const { auto setText = [](MetaString & text, int tId, const CGHeroInstance * h) { + text.clear(); text.appendLocalString(EMetaText::ADVOB_TXT, tId); text.replaceRawString(h->getNameTranslated()); }; @@ -179,6 +180,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) configuration.info.emplace_back(); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; configuration.info.back().reward = reward; + reward = Rewardable::Reward{}; } }; From 6c9b65c3916544043dbcff2725c38f25568ecf67 Mon Sep 17 00:00:00 2001 From: krs Date: Fri, 15 Sep 2023 22:15:10 +0300 Subject: [PATCH 0426/1248] Fix adventure map widget json --- config/widgets/adventureMap.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/config/widgets/adventureMap.json b/config/widgets/adventureMap.json index 36dd1e501..a398ad697 100644 --- a/config/widgets/adventureMap.json +++ b/config/widgets/adventureMap.json @@ -1,7 +1,7 @@ { "options" : { // player-colored images used for background - "imagesPlayerColored" : [ "AdvMap.pcx" ], + "imagesPlayerColored" : [ "AdvMap.pcx" ] }, "items": @@ -315,7 +315,7 @@ "item" : { "top" : 16, "left": 2, "width" : 48, "height" : 32 }, "itemsOffset" : { "x" : 0, "y" : 32 }, "itemsCount" : 5 - }, + } ] }, { @@ -331,7 +331,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 0, "bottom" : 0, "left" : 0, "right" : 0 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, // Town / Hero lists for large (664+) vertical resolution @@ -402,7 +402,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 192, "bottom" : 3, "right" : 57, "width" : 64 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, { @@ -418,7 +418,7 @@ "image" : "DiBoxBck.pcx", "area": { "top": 0, "bottom" : 0, "left" : 0, "right" : 0 }, "sourceArea": { "left" : 0, "top" : 0, "width" : 256, "height" : 256 } - }, + } ] }, { @@ -509,7 +509,7 @@ { "type": "adventureMapContainer", "hideWhen" : "mapLayerSurface", - "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 } + "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 }, "items" : [ { "type": "adventureMapButton", @@ -532,7 +532,7 @@ { "type": "adventureMapContainer", "hideWhen" : "mapLayerUnderground", - "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 } + "area": { "top" : 343, "left": 79, "width" : 32, "height" : 32 }, "items" : [ { "type": "adventureMapButton", From f80f59468875e56d60ff325b57be865513b7aa3c Mon Sep 17 00:00:00 2001 From: krs Date: Fri, 15 Sep 2023 23:13:43 +0300 Subject: [PATCH 0427/1248] Fix Market of Time missing Template --- config/objects/generic.json | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/config/objects/generic.json b/config/objects/generic.json index 49c6e01ca..a32cb0dda 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -548,7 +548,7 @@ "templates" : { "green" : { "animation" : "avxdent.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["grass", "swamp", "dirt"] }, - "brown" : { "animation" : "avxdend0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["sand", "lava", "rough", "snow", "subterra"] }, + "brown" : { "animation" : "avxdend0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "BA"], "allowedTerrains":["sand", "lava", "rough", "snow", "subterra"] } }, "rmg" : { "value" : 100, @@ -876,6 +876,21 @@ "index" : 0, "aiValue" : 0, "rmg" : { + }, + "templates" : { + "normal" : { + "animation" : "AVXMKTT0", + "editorAnimation" : "AVXMKTT0", + "visitableFrom" : [ + "---", + "--+", + "+++" + ], + "mask" : [ + "VV", + "BV" + ] + } } } } From 4f76eb3fce5c62d8be956f36a2d289fd4c249d4e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 15 Sep 2023 23:00:33 +0200 Subject: [PATCH 0428/1248] Add limiter serialization --- lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CRewardableObject.cpp | 1 + lib/rewardable/Limiter.cpp | 76 ++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index d6b9503be..1ddf2d997 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -45,7 +45,6 @@ void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const { auto setText = [](MetaString & text, int tId, const CGHeroInstance * h) { - text.clear(); text.appendLocalString(EMetaText::ADVOB_TXT, tId); text.replaceRawString(h->getNameTranslated()); }; @@ -124,7 +123,8 @@ void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const txt.replaceRawString(h->getNameTranslated()); } - const_cast(r.message) = txt; + if(r.message.empty()) + const_cast(r.message) = txt; } BlockingDialog bd (true, false); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index e4e3932c8..3b96dbe68 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -111,6 +111,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const case Rewardable::SELECT_ALL: // give all rewards for(auto i : rewards) grantRewardWithMessage(i, i == rewards.size() - 1); + break; } break; } diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index ef2d1d5c5..9d837ccb8 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -15,6 +15,8 @@ #include "../CPlayerState.h" #include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonSerializeFormat.h" +#include "../constants/StringConstants.h" +#include "../CSkillHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -122,7 +124,81 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) { + handler.serializeInt("dayOfWeek", dayOfWeek); + handler.serializeInt("daysPassed", daysPassed); + resources.serializeJson(handler, "resources"); + handler.serializeInt("manaPercentage", manaPercentage); + handler.serializeInt("heroExperience", heroExperience); + handler.serializeInt("heroLevel", heroLevel); + handler.serializeInt("manaPoints", manaPoints); + handler.serializeIdArray("artifacts", artifacts); + handler.enterArray("creatures").serializeStruct(creatures); + { + auto a = handler.enterArray("primary"); + a.syncSize(primary); + for(int i = 0; i < primary.size(); ++i) + a.serializeInt(i, primary[i]); + } + { + auto a = handler.enterArray("secondary"); + std::vector> fieldValue; + if(handler.saving) + { + for(auto & i : secondary) + { + auto key = VLC->skillh->encodeSkill(i.first); + auto value = NSecondarySkill::levels.at(i.second); + fieldValue.emplace_back(key, value); + } + } + a.syncSize(fieldValue); + for(int i = 0; i < fieldValue.size(); ++i) + { + auto e = a.enterStruct(i); + e->serializeString("skill", fieldValue[i].first); + e->serializeString("level", fieldValue[i].second); + } + if(!handler.saving) + { + for(auto & i : fieldValue) + { + const int skillId = VLC->skillh->decodeSkill(i.first); + if(skillId < 0) + { + logGlobal->error("Invalid secondary skill %s", i.first); + continue; + } + + const int level = vstd::find_pos(NSecondarySkill::levels, i.second); + if(level < 0) + { + logGlobal->error("Invalid secondary skill level%s", i.second); + continue; + } + + secondary[SecondarySkill(skillId)] = level; + } + + } + } + + //sublimiters + auto serializeSublimitersList = [&handler](const std::string & field, LimitersList & container) + { + auto a = handler.enterArray(field); + a.syncSize(container); + for(int i = 0; i < container.size(); ++i) + { + if(!handler.saving) + container[i] = std::make_shared(); + auto e = a.enterStruct(i); + container[i]->serializeJson(handler); + } + }; + serializeSublimitersList("allOf", allOf); + serializeSublimitersList("anyOf", anyOf); + serializeSublimitersList("noneOf", noneOf); } VCMI_LIB_NAMESPACE_END From f38f2d5b6a878f0e3751afa7d7b75f7af13c9ef7 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 15 Sep 2023 20:55:48 +0000 Subject: [PATCH 0429/1248] Add JSON validation as a CI stage --- .github/validate_json.py | 25 +++++++++++++++++++++++++ .github/workflows/github.yml | 5 +++++ 2 files changed, 30 insertions(+) create mode 100644 .github/validate_json.py diff --git a/.github/validate_json.py b/.github/validate_json.py new file mode 100644 index 000000000..0c95557bc --- /dev/null +++ b/.github/validate_json.py @@ -0,0 +1,25 @@ +import jstyleson +from pathlib import Path +from pprint import pprint + +errors = [] +for path in sorted(Path('.').glob('**/*.json')): + # because path is an object and not a string + path_str = str(path) + try: + with open(path_str, 'r') as file: + jstyleson.load(file) + print(f"Validation of {path_str} succeeded") + except Exception as exc: + print(f"Validation of {path_str} failed") + pprint(exc) + # https://stackoverflow.com/a/72850269/2278742 + if hasattr(exc, 'pos'): + position_msg = f"{path_str}:{exc.lineno}:{exc.colno}" + print(position_msg) + errors.append({"position": position_msg, "exception": exc}) + +if errors: + print("Summary of errors:") + pprint(errors) + raise Exception("Not all JSON files are valid") diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 29e3db01d..321e715c8 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -145,6 +145,11 @@ jobs: with: submodules: recursive + - name: Validate JSON + run: | + pip install jstyleson + python3 .github/validate_json.py + - name: Dependencies run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' env: From c74f4b83957b9c456e8d0aa8bee16d4126564bf0 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 16 Sep 2023 01:08:16 +0200 Subject: [PATCH 0430/1248] Update AUTHORS.h --- AUTHORS.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.h b/AUTHORS.h index bd0b5b698..97a716d04 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -47,7 +47,7 @@ std::vector> contributors = { { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, - { "Testing", "Ben Yan", "by003", "" }, + { "Testing", "Ben Yan", "by003", "benyan9110@gmail.com," }, { "Testing", "", "Misiokles", "" }, { "Testing", "", "Povelitel", "" }, }; From 2394612e284f1924fafd43187fc255b6d06e3be0 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 15 Sep 2023 23:31:43 +0000 Subject: [PATCH 0431/1248] JSON validation: Support various levels of strictness --- .github/validate_json.py | 40 +++++++++++++++++++++++++++++++----- .github/workflows/github.yml | 2 +- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/.github/validate_json.py b/.github/validate_json.py index 0c95557bc..8152c838f 100644 --- a/.github/validate_json.py +++ b/.github/validate_json.py @@ -1,6 +1,15 @@ import jstyleson from pathlib import Path from pprint import pprint +import yaml +import json5 +import re + +# 'json', 'json5' or 'yaml' +# json: strict, but doesn't preserve line numbers necessarily, since it strips comments before parsing +# json5: strict and preserves line numbers even for files will with line comments +# yaml: less strict, allows e.g. leading zeros +VALIDATION_TYPE = 'json5' errors = [] for path in sorted(Path('.').glob('**/*.json')): @@ -8,16 +17,37 @@ for path in sorted(Path('.').glob('**/*.json')): path_str = str(path) try: with open(path_str, 'r') as file: - jstyleson.load(file) + if VALIDATION_TYPE == 'json': + jstyleson.load(file) + if VALIDATION_TYPE == 'json5': + + json5.load(file) + elif VALIDATION_TYPE == 'yaml': + + file = file.read().replace("\t", " ") + file = file.replace("//", "#") + yaml.safe_load(file) print(f"Validation of {path_str} succeeded") except Exception as exc: print(f"Validation of {path_str} failed") pprint(exc) - # https://stackoverflow.com/a/72850269/2278742 + + error_pos = path_str + if hasattr(exc, 'pos'): - position_msg = f"{path_str}:{exc.lineno}:{exc.colno}" - print(position_msg) - errors.append({"position": position_msg, "exception": exc}) + # https://stackoverflow.com/a/72850269/2278742 + error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" + print(error_pos) + # error_msg = "position": position_msg, "exception": exc + if hasattr(exc, 'problem_mark'): + mark = exc.problem_mark + error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" + print(error_pos) + if VALIDATION_TYPE == 'json5': + pos = re.findall(r'\d+', str(exc)) + error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" + + errors.append({"error_pos": error_pos, "error_msg": exc}) if errors: print("Summary of errors:") diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 321e715c8..03bb4c0a1 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -147,7 +147,7 @@ jobs: - name: Validate JSON run: | - pip install jstyleson + pip3 install json5 jstyleson python3 .github/validate_json.py - name: Dependencies From 512816b2232dc96c2ca63bbdd6c0bb56ff6b0f20 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 15 Sep 2023 23:52:04 +0000 Subject: [PATCH 0432/1248] Only validate JSON on Linux --- .github/validate_json.py | 12 +++++------- .github/workflows/github.yml | 2 ++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/validate_json.py b/.github/validate_json.py index 8152c838f..e78d0601b 100644 --- a/.github/validate_json.py +++ b/.github/validate_json.py @@ -1,13 +1,14 @@ -import jstyleson +import re from pathlib import Path from pprint import pprint -import yaml + import json5 -import re +import jstyleson +import yaml # 'json', 'json5' or 'yaml' # json: strict, but doesn't preserve line numbers necessarily, since it strips comments before parsing -# json5: strict and preserves line numbers even for files will with line comments +# json5: strict and preserves line numbers even for files with line comments # yaml: less strict, allows e.g. leading zeros VALIDATION_TYPE = 'json5' @@ -20,10 +21,8 @@ for path in sorted(Path('.').glob('**/*.json')): if VALIDATION_TYPE == 'json': jstyleson.load(file) if VALIDATION_TYPE == 'json5': - json5.load(file) elif VALIDATION_TYPE == 'yaml': - file = file.read().replace("\t", " ") file = file.replace("//", "#") yaml.safe_load(file) @@ -38,7 +37,6 @@ for path in sorted(Path('.').glob('**/*.json')): # https://stackoverflow.com/a/72850269/2278742 error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" print(error_pos) - # error_msg = "position": position_msg, "exception": exc if hasattr(exc, 'problem_mark'): mark = exc.problem_mark error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 03bb4c0a1..4242f8e12 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -146,6 +146,8 @@ jobs: submodules: recursive - name: Validate JSON + # the Python yaml module doesn't seem to work on mac-arm + if: ${{ startsWith(matrix.preset, 'linux') }} run: | pip3 install json5 jstyleson python3 .github/validate_json.py From 991a755a1f57c382f6a2028d4ab68e072211479d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 16 Sep 2023 05:34:27 +0200 Subject: [PATCH 0433/1248] Added new rewardable widget --- mapeditor/inspector/inspector.cpp | 24 +- mapeditor/inspector/inspector.h | 3 + mapeditor/inspector/rewardswidget.cpp | 889 +++++++++------ mapeditor/inspector/rewardswidget.h | 75 +- mapeditor/inspector/rewardswidget.ui | 1488 +++++++++++++++++++++++-- 5 files changed, 2016 insertions(+), 463 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 30e755caf..9955d207b 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -67,6 +67,7 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default INIT_OBJ_TYPE(CGHeroInstance); INIT_OBJ_TYPE(CGSignBottle); INIT_OBJ_TYPE(CGLighthouse); + //INIT_OBJ_TYPE(CRewardableObject); //INIT_OBJ_TYPE(CGPandoraBox); //INIT_OBJ_TYPE(CGEvent); //INIT_OBJ_TYPE(CGSeerHut); @@ -375,14 +376,19 @@ void Inspector::updateProperties(CGCreature * o) //addProperty("Resources reward", o->resources); //TODO: implement in setProperty } +void Inspector::updateProperties(CRewardableObject * o) +{ + if(!o) return; + + auto * delegate = new RewardsDelegate(*map, *o); + addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); +} + void Inspector::updateProperties(CGPandoraBox * o) { if(!o) return; addProperty("Message", o->message, new MessageDelegate, false); - - auto * delegate = new RewardsPandoraDelegate(*map, *o); - addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); } void Inspector::updateProperties(CGEvent * o) @@ -413,11 +419,6 @@ void Inspector::updateProperties(CGSeerHut * o) auto * delegate = new QuestDelegate(*map, *o); addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); } - - { //Reward - auto * delegate = new RewardsSeerhutDelegate(*map, *o); - addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); - } } void Inspector::updateProperties() @@ -458,6 +459,7 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGHeroInstance); UPDATE_OBJ_PROPERTIES(CGSignBottle); UPDATE_OBJ_PROPERTIES(CGLighthouse); + UPDATE_OBJ_PROPERTIES(CRewardableObject); UPDATE_OBJ_PROPERTIES(CGPandoraBox); UPDATE_OBJ_PROPERTIES(CGEvent); UPDATE_OBJ_PROPERTIES(CGSeerHut); @@ -503,6 +505,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGShipyard); SET_PROPERTIES(CGSignBottle); SET_PROPERTIES(CGLighthouse); + SET_PROPERTIES(CRewardableObject); SET_PROPERTIES(CGPandoraBox); SET_PROPERTIES(CGEvent); SET_PROPERTIES(CGSeerHut); @@ -518,6 +521,11 @@ void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVarian if(!o) return; } +void Inspector::setProperty(CRewardableObject * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVariant & value) { if(!o) return; diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index ad47282b7..1e44964ca 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -17,6 +17,7 @@ #include "../lib/GameConstants.h" #include "../lib/mapObjects/CGCreature.h" #include "../lib/mapObjects/MapObjects.h" +#include "../lib/mapObjects/CRewardableObject.h" #include "../lib/ResourceSet.h" #define DECLARE_OBJ_TYPE(x) void initialize(x*); @@ -45,6 +46,7 @@ public: DECLARE_OBJ_TYPE(CGCreature); DECLARE_OBJ_TYPE(CGSignBottle); DECLARE_OBJ_TYPE(CGLighthouse); + //DECLARE_OBJ_TYPE(CRewardableObject); //DECLARE_OBJ_TYPE(CGEvent); //DECLARE_OBJ_TYPE(CGPandoraBox); //DECLARE_OBJ_TYPE(CGSeerHut); @@ -73,6 +75,7 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGCreature); DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle); DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse); + DECLARE_OBJ_PROPERTY_METHODS(CRewardableObject); DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); DECLARE_OBJ_PROPERTY_METHODS(CGEvent); DECLARE_OBJ_PROPERTY_METHODS(CGSeerHut); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 79f3028d8..0d0d23136 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -17,31 +17,153 @@ #include "../lib/CCreatureHandler.h" #include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" +#include "../lib/rewardable/Configuration.h" +#include "../lib/rewardable/Limiter.h" +#include "../lib/rewardable/Reward.h" +#include "../lib/mapObjects/CGPandoraBox.h" +#include "../lib/mapObjects/CQuest.h" -RewardsWidget::RewardsWidget(const CMap & m, CGPandoraBox & p, QWidget *parent) : +RewardsWidget::RewardsWidget(const CMap & m, CRewardableObject & p, QWidget *parent) : QDialog(parent), map(m), - pandora(&p), - seerhut(nullptr), + object(p), ui(new Ui::RewardsWidget) { ui->setupUi(this); - for(auto & type : rewardTypes) - ui->rewardType->addItem(QString::fromStdString(type)); -} - -RewardsWidget::RewardsWidget(const CMap & m, CGSeerHut & p, QWidget *parent) : - QDialog(parent), - map(m), - pandora(nullptr), - seerhut(&p), - ui(new Ui::RewardsWidget) -{ - ui->setupUi(this); + //fill core elements + for(const auto & s : Rewardable::VisitModeString) + ui->visitMode->addItem(QString::fromStdString(s)); - for(auto & type : rewardTypes) - ui->rewardType->addItem(QString::fromStdString(type)); + for(const auto & s : Rewardable::SelectModeString) + ui->selectMode->addItem(QString::fromStdString(s)); + + for(const std::string & s : {"AUTO", "MODAL", "INFO"}) + ui->windowMode->addItem(QString::fromStdString(s)); + + ui->lDayOfWeek->addItem(tr("None")); + for(int i = 1; i <= 7; ++i) + ui->lDayOfWeek->addItem(tr("Day %1").arg(i)); + + //fill resources + ui->rResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) + { + for(auto * w : {ui->rResources, ui->lResources}) + { + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + w->setItem(i, 0, item); + w->setCellWidget(i, 1, new QSpinBox); + } + } + + //fill artifacts + for(int i = 0; i < map.allowedArtifact.size(); ++i) + { + for(auto * w : {ui->rArtifacts, ui->lArtifacts}) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!map.allowedArtifact[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + w->addItem(item); + } + } + + //fill spells + for(int i = 0; i < map.allowedSpells.size(); ++i) + { + for(auto * w : {ui->rSpells, ui->lSpells}) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!map.allowedSpells[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + w->addItem(item); + } + + //spell cast + if(VLC->spells()->getByIndex(i)->isAdventure()) + { + ui->castSpell->addItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + ui->castSpell->setItemData(ui->castSpell->count() - 1, QVariant::fromValue(i)); + } + } + + //fill skills + ui->rSkills->setRowCount(map.allowedAbilities.size()); + ui->lSkills->setRowCount(map.allowedAbilities.size()); + for(int i = 0; i < map.allowedAbilities.size(); ++i) + { + for(auto * w : {ui->rSkills, ui->lSkills}) + { + auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + + auto * widget = new QComboBox; + for(auto & s : NSecondarySkill::levels) + widget->addItem(QString::fromStdString(s)); + + if(!map.allowedAbilities[i]) + { + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + widget->setEnabled(false); + } + + w->setItem(i, 0, item); + w->setCellWidget(i, 1, widget); + } + } + + //fill creatures + for(auto & creature : VLC->creh->objects) + { + for(auto * w : {ui->rCreatureId, ui->lCreatureId}) + { + w->addItem(QString::fromStdString(creature->getNameSingularTranslated())); + w->setItemData(w->count() - 1, creature->getIndex()); + } + } + + //fill spell cast + for(auto & s : NSecondarySkill::levels) + ui->castLevel->addItem(QString::fromStdString(s)); + on_castSpellCheck_toggled(false); + + //fill bonuses + for(auto & s : bonusDurationMap) + ui->bonusDuration->addItem(QString::fromStdString(s.first)); + for(auto & s : bonusNameMap) + ui->bonusType->addItem(QString::fromStdString(s.first)); + + //set default values + if(dynamic_cast(&object)) + { + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); + ui->visitMode->setEnabled(false); + ui->selectMode->setCurrentIndex(vstd::find_pos(Rewardable::SelectModeString, "selectAll")); + ui->selectMode->setEnabled(false); + ui->windowMode->setEnabled(false); + ui->canRefuse->setEnabled(false); + } + + if(dynamic_cast(&object)) + { + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); + ui->visitMode->setEnabled(false); + ui->windowMode->setEnabled(false); + ui->canRefuse->setChecked(true); + ui->canRefuse->setEnabled(false); + } + + //hide elements + ui->eventInfoGroup->hide(); } RewardsWidget::~RewardsWidget() @@ -49,364 +171,440 @@ RewardsWidget::~RewardsWidget() delete ui; } -QList RewardsWidget::getListForType(RewardType typeId) -{ - assert(typeId < rewardTypes.size()); - QList result; - - switch (typeId) { - case RewardType::RESOURCE: - //to convert string to index WOOD = 0, MERCURY, ORE, SULFUR, CRYSTAL, GEMS, GOLD, MITHRIL, - result.append("Wood"); - result.append("Mercury"); - result.append("Ore"); - result.append("Sulfur"); - result.append("Crystals"); - result.append("Gems"); - result.append("Gold"); - break; - - case RewardType::PRIMARY_SKILL: - for(auto s : NPrimarySkill::names) - result.append(QString::fromStdString(s)); - break; - - case RewardType::SECONDARY_SKILL: - for(int i = 0; i < map.allowedAbilities.size(); ++i) - { - if(map.allowedAbilities[i]) - result.append(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::ARTIFACT: - for(int i = 0; i < map.allowedArtifact.size(); ++i) - { - if(map.allowedArtifact[i]) - result.append(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::SPELL: - for(int i = 0; i < map.allowedSpells.size(); ++i) - { - if(map.allowedSpells[i]) - result.append(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); - } - break; - - case RewardType::CREATURE: - for(auto creature : VLC->creh->objects) - { - result.append(QString::fromStdString(creature->getNameSingularTranslated())); - } - break; - } - return result; -} - -void RewardsWidget::on_rewardType_activated(int index) -{ - ui->rewardList->clear(); - ui->rewardList->setEnabled(true); - assert(index < rewardTypes.size()); - - auto l = getListForType(RewardType(index)); - if(l.empty()) - ui->rewardList->setEnabled(false); - - for(auto & s : l) - ui->rewardList->addItem(s); -} void RewardsWidget::obtainData() { - if(pandora) - { - if(pandora->gainedExp > 0) - addReward(RewardType::EXPERIENCE, 0, pandora->gainedExp); - if(pandora->manaDiff) - addReward(RewardType::MANA, 0, pandora->manaDiff); - if(pandora->moraleDiff) - addReward(RewardType::MORALE, 0, pandora->moraleDiff); - if(pandora->luckDiff) - addReward(RewardType::LUCK, 0, pandora->luckDiff); - if(pandora->resources.nonZero()) - { - for(ResourceSet::nziterator resiter(pandora->resources); resiter.valid(); ++resiter) - addReward(RewardType::RESOURCE, resiter->resType, resiter->resVal); - } - for(int idx = 0; idx < pandora->primskills.size(); ++idx) - { - if(pandora->primskills[idx]) - addReward(RewardType::PRIMARY_SKILL, idx, pandora->primskills[idx]); - } - assert(pandora->abilities.size() == pandora->abilityLevels.size()); - for(int idx = 0; idx < pandora->abilities.size(); ++idx) - { - addReward(RewardType::SECONDARY_SKILL, pandora->abilities[idx].getNum(), pandora->abilityLevels[idx]); - } - for(auto art : pandora->artifacts) - { - addReward(RewardType::ARTIFACT, art.getNum(), 1); - } - for(auto spell : pandora->spells) - { - addReward(RewardType::SPELL, spell.getNum(), 1); - } - for(int i = 0; i < pandora->creatures.Slots().size(); ++i) - { - if(auto c = pandora->creatures.getCreature(SlotID(i))) - addReward(RewardType::CREATURE, c->getId(), pandora->creatures.getStackCount(SlotID(i))); - } - } + //common parameters + ui->visitMode->setCurrentIndex(object.configuration.visitMode); + ui->selectMode->setCurrentIndex(object.configuration.selectMode); + ui->windowMode->setCurrentIndex(int(object.configuration.infoWindowType)); + ui->onSelectText->setText(QString::fromStdString(object.configuration.onSelect.toString())); + ui->canRefuse->setChecked(object.configuration.canRefuse); - if(seerhut) - { - for(auto & i : seerhut->configuration.info) - { - if(i.reward.heroExperience) - addReward(RewardType::EXPERIENCE, 0, i.reward.heroExperience); - if(i.reward.manaDiff) - addReward(RewardType::MANA, 0, i.reward.manaDiff); - for(auto & a : i.reward.artifacts) - addReward(RewardType::ARTIFACT, a.getNum(), 0); - for(auto & a : i.reward.creatures) - addReward(RewardType::CREATURE, a.getType()->getId().getNum(), a.getCount()); - } - /*switch(seerhut->rewardType) - { - case CGSeerHut::ERewardType::EXPERIENCE: - - break; - - case CGSeerHut::ERewardType::MANA_POINTS: - addReward(RewardType::MANA, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::MORALE_BONUS: - addReward(RewardType::MORALE, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::LUCK_BONUS: - addReward(RewardType::LUCK, 0, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::RESOURCES: - - break; - - case CGSeerHut::ERewardType::PRIMARY_SKILL: - addReward(RewardType::PRIMARY_SKILL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::SECONDARY_SKILL: - addReward(RewardType::SECONDARY_SKILL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::ARTIFACT: - addReward(RewardType::ARTIFACT, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::SPELL: - addReward(RewardType::SPELL, seerhut->rID, seerhut->rVal); - break; - - case CGSeerHut::ERewardType::CREATURE: - addReward(RewardType::CREATURE, seerhut->rID, seerhut->rVal); - break; - - default: - break; - }*/ - } + //reset parameters + ui->resetPeriod->setValue(object.configuration.resetParameters.period); + ui->resetVisitors->setChecked(object.configuration.resetParameters.visitors); + ui->resetRewards->setChecked(object.configuration.resetParameters.rewards); + + ui->visitInfoList->clear(); + + for([[maybe_unused]] auto & a : object.configuration.info) + ui->visitInfoList->addItem(tr("Reward %1").arg(ui->visitInfoList->count() + 1)); + + if(ui->visitInfoList->currentItem()) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } bool RewardsWidget::commitChanges() { - bool haveRewards = false; - if(pandora) + //common parameters + object.configuration.visitMode = ui->visitMode->currentIndex(); + object.configuration.selectMode = ui->selectMode->currentIndex(); + object.configuration.infoWindowType = EInfoWindowMode(ui->windowMode->currentIndex()); + object.configuration.onSelect = MetaString::createFromRawString(ui->onSelectText->text().toStdString()); + object.configuration.canRefuse = ui->canRefuse->isChecked(); + + //reset parameters + object.configuration.resetParameters.period = ui->resetPeriod->value(); + object.configuration.resetParameters.visitors = ui->resetVisitors->isChecked(); + object.configuration.resetParameters.rewards = ui->resetRewards->isChecked(); + + if(ui->visitInfoList->currentItem()) + saveCurrentVisitInfo(ui->visitInfoList->currentRow()); + + return true; +} + +void RewardsWidget::saveCurrentVisitInfo(int index) +{ + auto & vinfo = object.configuration.info.at(index); + vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString()); + + vinfo.reward.heroLevel = ui->rHeroLevel->value(); + vinfo.reward.heroExperience = ui->rHeroExperience->value(); + vinfo.reward.manaDiff = ui->rManaDiff->value(); + vinfo.reward.manaPercentage = ui->rManaPercentage->value(); + vinfo.reward.manaOverflowFactor = ui->rOverflowFactor->value(); + vinfo.reward.movePoints = ui->rMovePoints->value(); + vinfo.reward.movePercentage = ui->rMovePercentage->value(); + vinfo.reward.removeObject = ui->removeObject->isChecked(); + vinfo.reward.primary[0] = ui->rAttack->value(); + vinfo.reward.primary[1] = ui->rDefence->value(); + vinfo.reward.primary[2] = ui->rPower->value(); + vinfo.reward.primary[3] = ui->rKnowledge->value(); + for(int i = 0; i < ui->rResources->rowCount(); ++i) { - pandora->abilities.clear(); - pandora->abilityLevels.clear(); - pandora->primskills.resize(GameConstants::PRIMARY_SKILLS, 0); - pandora->resources = ResourceSet(); - pandora->artifacts.clear(); - pandora->spells.clear(); - pandora->creatures.clearSlots(); - - for(int row = 0; row < rewards; ++row) + if(auto * widget = qobject_cast(ui->rResources->cellWidget(i, 1))) + vinfo.reward.resources[i] = widget->value(); + } + + vinfo.reward.artifacts.clear(); + for(int i = 0; i < ui->rArtifacts->count(); ++i) + { + if(ui->rArtifacts->item(i)->checkState() == Qt::Checked) + vinfo.reward.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + vinfo.reward.spells.clear(); + for(int i = 0; i < ui->rSpells->count(); ++i) + { + if(ui->rSpells->item(i)->checkState() == Qt::Checked) + vinfo.reward.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + vinfo.reward.secondary.clear(); + for(int i = 0; i < ui->rSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(i, 1))) { - haveRewards = true; - int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); - int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; - int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); - switch(typeId) + if(widget->currentIndex() > 0) + vinfo.reward.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + vinfo.reward.creatures.clear(); + for(int i = 0; i < ui->rCreatures->rowCount(); ++i) + { + int index = ui->rCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->rCreatures->cellWidget(i, 1))) + if(widget->value()) + vinfo.reward.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + vinfo.reward.spellCast.first = SpellID::NONE; + if(ui->castSpellCheck->isChecked()) + { + vinfo.reward.spellCast.first = VLC->spells()->getByIndex(ui->castSpell->itemData(ui->castSpell->currentIndex()).toInt())->getId(); + vinfo.reward.spellCast.second = ui->castLevel->currentIndex(); + } + + vinfo.reward.bonuses.clear(); + for(int i = 0; i < ui->bonuses->rowCount(); ++i) + { + auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString()); + auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString()); + auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt(); + vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, object.id); + } + + vinfo.limiter.dayOfWeek = ui->lDayOfWeek->currentIndex(); + vinfo.limiter.daysPassed = ui->lDaysPassed->value(); + vinfo.limiter.heroLevel = ui->lHeroLevel->value(); + vinfo.limiter.heroExperience = ui->lHeroExperience->value(); + vinfo.limiter.manaPoints = ui->lManaPoints->value(); + vinfo.limiter.manaPercentage = ui->lManaPercentage->value(); + vinfo.limiter.primary[0] = ui->lAttack->value(); + vinfo.limiter.primary[1] = ui->lDefence->value(); + vinfo.limiter.primary[2] = ui->lPower->value(); + vinfo.limiter.primary[3] = ui->lKnowledge->value(); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + vinfo.limiter.resources[i] = widget->value(); + } + + vinfo.limiter.artifacts.clear(); + for(int i = 0; i < ui->lArtifacts->count(); ++i) + { + if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) + vinfo.limiter.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + vinfo.limiter.spells.clear(); + for(int i = 0; i < ui->lSpells->count(); ++i) + { + if(ui->lSpells->item(i)->checkState() == Qt::Checked) + vinfo.limiter.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + vinfo.limiter.secondary.clear(); + for(int i = 0; i < ui->lSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) + { + if(widget->currentIndex() > 0) + vinfo.limiter.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + vinfo.limiter.creatures.clear(); + for(int i = 0; i < ui->lCreatures->rowCount(); ++i) + { + int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) + if(widget->value()) + vinfo.reward.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } +} + +void RewardsWidget::loadCurrentVisitInfo(int index) +{ + for(auto * w : {ui->rArtifacts, ui->rSpells, ui->lArtifacts, ui->lSpells}) + for(int i = 0; i < w->count(); ++i) + w->item(i)->setCheckState(Qt::Unchecked); + + for(auto * w : {ui->rSkills, ui->lSkills}) + for(int i = 0; i < w->rowCount(); ++i) + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(i, 1))) + widget->setCurrentIndex(0); + + ui->rCreatures->setRowCount(0); + ui->lCreatures->setRowCount(0); + ui->bonuses->setRowCount(0); + + const auto & vinfo = object.configuration.info.at(index); + ui->rewardMessage->setText(QString::fromStdString(vinfo.message.toString())); + + ui->rHeroLevel->setValue(vinfo.reward.heroLevel); + ui->rHeroExperience->setValue(vinfo.reward.heroExperience); + ui->rManaDiff->setValue(vinfo.reward.manaDiff); + ui->rManaPercentage->setValue(vinfo.reward.manaPercentage); + ui->rOverflowFactor->setValue(vinfo.reward.manaOverflowFactor); + ui->rMovePoints->setValue(vinfo.reward.movePoints); + ui->rMovePercentage->setValue(vinfo.reward.movePercentage); + ui->removeObject->setChecked(vinfo.reward.removeObject); + ui->rAttack->setValue(vinfo.reward.primary[0]); + ui->rDefence->setValue(vinfo.reward.primary[1]); + ui->rPower->setValue(vinfo.reward.primary[2]); + ui->rKnowledge->setValue(vinfo.reward.primary[3]); + for(int i = 0; i < ui->rResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->rResources->cellWidget(i, 1))) + widget->setValue(vinfo.reward.resources[i]); + } + + for(auto i : vinfo.reward.artifacts) + ui->rArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : vinfo.reward.spells) + ui->rArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : vinfo.reward.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->rSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); + } + for(auto & i : vinfo.reward.creatures) + { + int index = i.type->getIndex(); + ui->rCreatureId->setCurrentIndex(index); + ui->rCreatureAmount->setValue(i.count); + onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount); + } + + ui->castSpellCheck->setChecked(vinfo.reward.spellCast.first != SpellID::NONE); + if(ui->castSpellCheck->isChecked()) + { + int index = VLC->spells()->getById(vinfo.reward.spellCast.first)->getIndex(); + ui->castSpell->setCurrentIndex(index); + ui->castLevel->setCurrentIndex(vinfo.reward.spellCast.second); + } + + for(auto & i : vinfo.reward.bonuses) + { + auto dur = vstd::findKey(bonusDurationMap, i.duration); + for(int i = 0; i < ui->bonusDuration->count(); ++i) + { + if(ui->bonusDuration->itemText(i) == QString::fromStdString(dur)) { - case RewardType::EXPERIENCE: - pandora->gainedExp = amount; - break; - - case RewardType::MANA: - pandora->manaDiff = amount; - break; - - case RewardType::MORALE: - pandora->moraleDiff = amount; - break; - - case RewardType::LUCK: - pandora->luckDiff = amount; - break; - - case RewardType::RESOURCE: - pandora->resources[listId] = amount; - break; - - case RewardType::PRIMARY_SKILL: - pandora->primskills[listId] = amount; - break; - - case RewardType::SECONDARY_SKILL: - pandora->abilities.push_back(SecondarySkill(listId)); - pandora->abilityLevels.push_back(amount); - break; - - case RewardType::ARTIFACT: - pandora->artifacts.push_back(ArtifactID(listId)); - break; - - case RewardType::SPELL: - pandora->spells.push_back(SpellID(listId)); - break; - - case RewardType::CREATURE: - auto slot = pandora->creatures.getFreeSlot(); - if(slot != SlotID() && amount > 0) - pandora->creatures.addToSlot(slot, CreatureID(listId), amount); - break; + ui->bonusDuration->setCurrentIndex(i); + break; } } - } - if(seerhut) - { - seerhut->configuration.info.clear(); - for(int row = 0; row < rewards; ++row) + + auto typ = vstd::findKey(bonusNameMap, i.type); + for(int i = 0; i < ui->bonusType->count(); ++i) { - seerhut->configuration.info.emplace_back(); - seerhut->configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - Rewardable::Reward & reward = seerhut->configuration.info.back().reward; - haveRewards = true; - int typeId = ui->rewardsTable->item(row, 0)->data(Qt::UserRole).toInt(); - int listId = ui->rewardsTable->item(row, 1) ? ui->rewardsTable->item(row, 1)->data(Qt::UserRole).toInt() : 0; - int amount = ui->rewardsTable->item(row, 2)->data(Qt::UserRole).toInt(); - switch (typeId) { - case RewardType::EXPERIENCE: - reward.heroExperience = amount; - break; - - case RewardType::ARTIFACT: - reward.artifacts.push_back(listId); - break; - - case RewardType::CREATURE: - reward.creatures.emplace_back(listId, amount); - - default: - break; + if(ui->bonusType->itemText(i) == QString::fromStdString(typ)) + { + ui->bonusType->setCurrentIndex(i); + break; + } + } + + ui->bonusValue->setValue(i.val); + on_bonusAdd_clicked(); + } + + ui->lDayOfWeek->setCurrentIndex(vinfo.limiter.dayOfWeek); + ui->lDaysPassed->setValue(vinfo.limiter.daysPassed); + ui->lHeroLevel->setValue(vinfo.limiter.heroLevel); + ui->lHeroExperience->setValue(vinfo.limiter.heroExperience); + ui->lManaPoints->setValue(vinfo.limiter.manaPoints); + ui->lManaPercentage->setValue(vinfo.limiter.manaPercentage); + ui->lAttack->setValue(vinfo.reward.primary[0]); + ui->lDefence->setValue(vinfo.reward.primary[1]); + ui->lPower->setValue(vinfo.reward.primary[2]); + ui->lKnowledge->setValue(vinfo.reward.primary[3]); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + widget->setValue(vinfo.limiter.resources[i]); + } + + for(auto i : vinfo.limiter.artifacts) + ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : vinfo.limiter.spells) + ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : vinfo.limiter.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); + } + for(auto & i : vinfo.limiter.creatures) + { + int index = i.type->getIndex(); + ui->lCreatureId->setCurrentIndex(index); + ui->lCreatureAmount->setValue(i.count); + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); + } +} + +void RewardsWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) +{ + QTableWidgetItem * item = nullptr; + QSpinBox * widget = nullptr; + for(int i = 0; i < listWidget->rowCount(); ++i) + { + if(auto * cname = listWidget->item(i, 0)) + { + if(cname->data(Qt::UserRole).toInt() == comboWidget->currentData().toInt()) + { + item = cname; + widget = qobject_cast(listWidget->cellWidget(i, 1)); + break; } } } - return haveRewards; + + if(!item) + { + listWidget->setRowCount(listWidget->rowCount() + 1); + item = new QTableWidgetItem(comboWidget->currentText()); + listWidget->setItem(listWidget->rowCount() - 1, 0, item); + } + + item->setData(Qt::UserRole, comboWidget->currentData()); + + if(!widget) + { + widget = new QSpinBox; + widget->setRange(spinWidget->minimum(), spinWidget->maximum()); + listWidget->setCellWidget(listWidget->rowCount() - 1, 1, widget); + } + + widget->setValue(spinWidget->value()); } -void RewardsWidget::on_rewardList_activated(int index) +void RewardsWidget::on_addVisitInfo_clicked() { - ui->rewardAmount->setText(QStringLiteral("1")); + ui->visitInfoList->addItem(tr("Reward %1").arg(ui->visitInfoList->count() + 1)); + object.configuration.info.emplace_back(); } -void RewardsWidget::addReward(RewardsWidget::RewardType typeId, int listId, int amount) + +void RewardsWidget::on_removeVisitInfo_clicked() { - //for seerhut there could be the only one reward - if(!pandora && seerhut && rewards) + int index = ui->visitInfoList->currentRow(); + object.configuration.info.erase(std::next(object.configuration.info.begin(), index)); + ui->visitInfoList->blockSignals(true); + delete ui->visitInfoList->currentItem(); + ui->visitInfoList->blockSignals(false); + on_visitInfoList_itemSelectionChanged(); +} + +void RewardsWidget::on_selectMode_currentIndexChanged(int index) +{ + ui->onSelectText->setEnabled(index == vstd::find_pos(Rewardable::SelectModeString, "selectPlayer")); +} + +void RewardsWidget::on_resetPeriod_valueChanged(int arg1) +{ + ui->resetRewards->setEnabled(arg1); + ui->resetVisitors->setEnabled(arg1); +} + + +void RewardsWidget::on_visitInfoList_itemSelectionChanged() +{ + if(ui->visitInfoList->currentItem() == nullptr) + { + ui->eventInfoGroup->hide(); return; - - ui->rewardsTable->setRowCount(++rewards); - - auto itemType = new QTableWidgetItem(QString::fromStdString(rewardTypes[typeId])); - itemType->setData(Qt::UserRole, typeId); - ui->rewardsTable->setItem(rewards - 1, 0, itemType); - - auto l = getListForType(typeId); - if(!l.empty()) - { - auto itemCurr = new QTableWidgetItem(getListForType(typeId)[listId]); - itemCurr->setData(Qt::UserRole, listId); - ui->rewardsTable->setItem(rewards - 1, 1, itemCurr); } - QString am = QString::number(amount); - switch(ui->rewardType->currentIndex()) - { - case 6: - if(amount <= 1) - am = "Basic"; - if(amount == 2) - am = "Advanced"; - if(amount >= 3) - am = "Expert"; - break; - - case 7: - case 8: - am = ""; - amount = 1; - break; - } - auto itemCount = new QTableWidgetItem(am); - itemCount->setData(Qt::UserRole, amount); - ui->rewardsTable->setItem(rewards - 1, 2, itemCount); + ui->eventInfoGroup->show(); + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); +} + +void RewardsWidget::on_visitInfoList_currentItemChanged(QListWidgetItem * current, QListWidgetItem * previous) +{ + if(previous) + saveCurrentVisitInfo(ui->visitInfoList->row(previous)); } -void RewardsWidget::on_buttonAdd_clicked() +void RewardsWidget::on_rCreatureAdd_clicked() { - addReward(RewardType(ui->rewardType->currentIndex()), ui->rewardList->currentIndex(), ui->rewardAmount->text().toInt()); + onCreatureAdd(ui->rCreatures, ui->rCreatureId, ui->rCreatureAmount); } -void RewardsWidget::on_buttonRemove_clicked() +void RewardsWidget::on_rCreatureRemove_clicked() { - auto currentRow = ui->rewardsTable->currentRow(); - if(currentRow != -1) - { - ui->rewardsTable->removeRow(currentRow); - --rewards; - } -} - - -void RewardsWidget::on_buttonClear_clicked() -{ - ui->rewardsTable->clear(); - rewards = 0; -} - - -void RewardsWidget::on_rewardsTable_itemSelectionChanged() -{ - /*auto type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 0); - ui->rewardType->setCurrentIndex(type->data(Qt::UserRole).toInt()); - ui->rewardType->activated(ui->rewardType->currentIndex()); + std::set> rowsToRemove; + for(auto * i : ui->rCreatures->selectedItems()) + rowsToRemove.insert(i->row()); - type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 1); - ui->rewardList->setCurrentIndex(type->data(Qt::UserRole).toInt()); - ui->rewardList->activated(ui->rewardList->currentIndex()); - - type = ui->rewardsTable->item(ui->rewardsTable->currentRow(), 2); - ui->rewardAmount->setText(QString::number(type->data(Qt::UserRole).toInt()));*/ + for(auto i : rowsToRemove) + ui->rCreatures->removeRow(i); } + +void RewardsWidget::on_lCreatureAdd_clicked() +{ + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); +} + + +void RewardsWidget::on_lCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->lCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->lCreatures->removeRow(i); +} + +void RewardsWidget::on_castSpellCheck_toggled(bool checked) +{ + ui->castSpell->setEnabled(checked); + ui->castLevel->setEnabled(checked); +} + +void RewardsWidget::on_bonusAdd_clicked() +{ + auto * itemType = new QTableWidgetItem(ui->bonusType->currentText()); + auto * itemDur = new QTableWidgetItem(ui->bonusDuration->currentText()); + auto * itemVal = new QTableWidgetItem(QString::number(ui->bonusValue->value())); + itemVal->setData(Qt::UserRole, ui->bonusValue->value()); + + ui->bonuses->setRowCount(ui->bonuses->rowCount() + 1); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 0, itemDur); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 1, itemType); + ui->bonuses->setItem(ui->bonuses->rowCount() - 1, 2, itemVal); +} + +void RewardsWidget::on_bonusRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->bonuses->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->bonuses->removeRow(i); +} + + void RewardsDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if(auto * ed = qobject_cast(editor)) @@ -431,20 +629,11 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c } } -RewardsPandoraDelegate::RewardsPandoraDelegate(const CMap & m, CGPandoraBox & t): map(m), pandora(t), RewardsDelegate() +RewardsDelegate::RewardsDelegate(const CMap & m, CRewardableObject & t): map(m), object(t) { } -QWidget * RewardsPandoraDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +QWidget * RewardsDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { - return new RewardsWidget(map, pandora, parent); -} - -RewardsSeerhutDelegate::RewardsSeerhutDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), RewardsDelegate() -{ -} - -QWidget * RewardsSeerhutDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const -{ - return new RewardsWidget(map, seerhut, parent); + return new RewardsWidget(map, object, parent); } diff --git a/mapeditor/inspector/rewardswidget.h b/mapeditor/inspector/rewardswidget.h index 422c31768..2a5958d9e 100644 --- a/mapeditor/inspector/rewardswidget.h +++ b/mapeditor/inspector/rewardswidget.h @@ -10,88 +10,77 @@ #pragma once #include "../StdInc.h" #include -#include "../lib/mapObjects/CGPandoraBox.h" -#include "../lib/mapObjects/CQuest.h" +#include "../lib/mapObjects/CRewardableObject.h" namespace Ui { class RewardsWidget; } -const std::array rewardTypes{"Experience", "Mana", "Morale", "Luck", "Resource", "Primary skill", "Secondary skill", "Artifact", "Spell", "Creature"}; - class RewardsWidget : public QDialog { Q_OBJECT public: - enum RewardType - { - EXPERIENCE = 0, MANA, MORALE, LUCK, RESOURCE, PRIMARY_SKILL, SECONDARY_SKILL, ARTIFACT, SPELL, CREATURE - }; - explicit RewardsWidget(const CMap &, CGPandoraBox &, QWidget *parent = nullptr); - explicit RewardsWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr); + explicit RewardsWidget(const CMap &, CRewardableObject &, QWidget *parent = nullptr); ~RewardsWidget(); void obtainData(); bool commitChanges(); private slots: - void on_rewardType_activated(int index); + void on_addVisitInfo_clicked(); - void on_rewardList_activated(int index); + void on_removeVisitInfo_clicked(); - void on_buttonAdd_clicked(); + void on_selectMode_currentIndexChanged(int index); - void on_buttonRemove_clicked(); + void on_resetPeriod_valueChanged(int arg1); - void on_buttonClear_clicked(); + void on_visitInfoList_itemSelectionChanged(); - void on_rewardsTable_itemSelectionChanged(); + void on_visitInfoList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + + void on_rCreatureAdd_clicked(); + + void on_rCreatureRemove_clicked(); + + void on_lCreatureAdd_clicked(); + + void on_lCreatureRemove_clicked(); + + void on_castSpellCheck_toggled(bool checked); + + void on_bonusAdd_clicked(); + + void on_bonusRemove_clicked(); private: - void addReward(RewardType typeId, int listId, int amount); - QList getListForType(RewardType typeId); + + void saveCurrentVisitInfo(int index); + void loadCurrentVisitInfo(int index); + + void onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget); Ui::RewardsWidget *ui; - CGPandoraBox * pandora; - CGSeerHut * seerhut; + CRewardableObject & object; const CMap & map; - int rewards = 0; }; class RewardsDelegate : public QStyledItemDelegate { Q_OBJECT public: + RewardsDelegate(const CMap &, CRewardableObject &); + using QStyledItemDelegate::QStyledItemDelegate; void setEditorData(QWidget *editor, const QModelIndex &index) const override; void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; -}; -class RewardsPandoraDelegate : public RewardsDelegate -{ - Q_OBJECT -public: - RewardsPandoraDelegate(const CMap &, CGPandoraBox &); - QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - + private: - CGPandoraBox & pandora; - const CMap & map; -}; - -class RewardsSeerhutDelegate : public RewardsDelegate -{ - Q_OBJECT -public: - RewardsSeerhutDelegate(const CMap &, CGSeerHut &); - - QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; - -private: - CGSeerHut & seerhut; + CRewardableObject & object; const CMap & map; }; diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index e8bb6d41a..544eee0ff 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -9,8 +9,8 @@ 0 0 - 645 - 335 + 806 + 561 @@ -19,69 +19,1433 @@ true - - - - - Remove selected + + + 3 + + + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + -3 + + + + + + + + + Visit mode + + + + + + + + 1 + 0 + + + + + + + + + + + + Select mode + + + + + + + + 1 + 0 + + + + + + + + + + + + + On select text + + + + + + + Can refuse + + + + + + + Reset parameters + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Period + + + + + + + days + + + 99 + + + + + + + + + Reset visitors + + + + + + + Reset rewards + + + + + + + + + + + + Window type + + + + + + + + + + + + + + + 0 + 0 + + + Event info + + + + 6 + + + 6 + + + 6 + + + 6 + + + + + Message to be displayed on granting of this reward + + + + + + + 0 + + + + Reward + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + -999 + + + 999 + + + + + + + % + + + -100 + + + 1000 + + + + + + + Overflow + + + + + + + % + + + 100 + + + 100 + + + + + + + + + + + Movement + + + + + + + -999 + + + 999 + + + + + + + % + + + -100 + + + 1000 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Remove object + + + + + + + + + Primary skills + + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + -99 + + + + + + + Defence + + + + + + + -99 + + + + + + + Spell power + + + + + + + -99 + + + + + + + Knowledge + + + + + + + -99 + + + + + + + + + + 0 + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + + + + + + + + Bonuses + + + + 3 + + + 3 + + + 3 + + + + + + + Duration + + + + + + + + + + Type + + + + + + + + + + Value + + + + + + + -999 + + + 999 + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 150 + + + false + + + false + + + 21 + + + + Duration + + + + + Type + + + + + Value + + + + + + + + + Cast + + + + + + Cast an adventure map spell + + + + + + + + + Spell + + + + + + + + 0 + 0 + + + + + + + + + + + + Magic school level + + + + + + + + 0 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Limiter + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + Day of week + + + + + + + + 120 + 0 + + + + + + + + Days passed + + + + + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + 999 + + + + + + + % + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Primary skills + + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + + + + Defence + + + + + + + + + + Spell power + + + + + + + + + + Knowledge + + + + + + + + + + + + + 0 + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 21 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 21 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + 21 + + + + + + + + + + + + + + - - - - - 80 - 16777215 - - - - Qt::ImhDigitsOnly - - - - - - - Delete all - - - - - - - Add or change - - - - - - - QAbstractItemView::NoEditTriggers - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - - 3 - - - false - - - - - - - - - - - - From 033e7010cadbc7827511ff6bbce1f406797874e3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 16 Sep 2023 05:52:23 +0200 Subject: [PATCH 0434/1248] Fix for empty primary skills array --- mapeditor/inspector/rewardswidget.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 0d0d23136..f0ec0b756 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -228,6 +228,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) vinfo.reward.movePoints = ui->rMovePoints->value(); vinfo.reward.movePercentage = ui->rMovePercentage->value(); vinfo.reward.removeObject = ui->removeObject->isChecked(); + vinfo.reward.primary.resize(4); vinfo.reward.primary[0] = ui->rAttack->value(); vinfo.reward.primary[1] = ui->rDefence->value(); vinfo.reward.primary[2] = ui->rPower->value(); @@ -292,6 +293,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) vinfo.limiter.heroExperience = ui->lHeroExperience->value(); vinfo.limiter.manaPoints = ui->lManaPoints->value(); vinfo.limiter.manaPercentage = ui->lManaPercentage->value(); + vinfo.limiter.primary.resize(4); vinfo.limiter.primary[0] = ui->lAttack->value(); vinfo.limiter.primary[1] = ui->lDefence->value(); vinfo.limiter.primary[2] = ui->lPower->value(); From 10d783d8eebb1fa73ec7e06bba2da69b82eea1b4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 16 Sep 2023 05:59:16 +0200 Subject: [PATCH 0435/1248] Undefined behavior fix --- mapeditor/inspector/rewardswidget.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index f0ec0b756..24d5353f2 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -511,6 +511,7 @@ void RewardsWidget::on_removeVisitInfo_clicked() delete ui->visitInfoList->currentItem(); ui->visitInfoList->blockSignals(false); on_visitInfoList_itemSelectionChanged(); + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } void RewardsWidget::on_selectMode_currentIndexChanged(int index) @@ -534,13 +535,15 @@ void RewardsWidget::on_visitInfoList_itemSelectionChanged() } ui->eventInfoGroup->show(); - loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } void RewardsWidget::on_visitInfoList_currentItemChanged(QListWidgetItem * current, QListWidgetItem * previous) { if(previous) saveCurrentVisitInfo(ui->visitInfoList->row(previous)); + + if(current) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } From 6c15e5b4266886c1c399d2fa675d02e3ffe2354b Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 16 Sep 2023 05:13:16 +0000 Subject: [PATCH 0436/1248] Add include path of a more recent version of fuzzylite, which is required to build it with gcc 12.2.0 https://github.com/fuzzylite/fuzzylite/commit/a651f3d13e192352d7a02ee9da65ab591a963ab9 --- AI/Nullkiller/Engine/FuzzyEngines.h | 6 +++++- AI/Nullkiller/Engine/PriorityEvaluator.h | 6 +++++- AI/VCAI/FuzzyEngines.h | 6 +++++- cmake_modules/Findfuzzylite.cmake | 6 ++++-- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/AI/Nullkiller/Engine/FuzzyEngines.h b/AI/Nullkiller/Engine/FuzzyEngines.h index 1694d175c..1873b97a5 100644 --- a/AI/Nullkiller/Engine/FuzzyEngines.h +++ b/AI/Nullkiller/Engine/FuzzyEngines.h @@ -8,7 +8,11 @@ * */ #pragma once -#include +#if __has_include() +# include +#else +# include +#endif #include "../Goals/AbstractGoal.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index fb8085494..5ab2a6082 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -8,7 +8,11 @@ * */ #pragma once -#include "fl/Headers.h" +#if __has_include() +# include +#else +# include +#endif #include "../Goals/CGoal.h" #include "../Pathfinding/AIPathfinder.h" diff --git a/AI/VCAI/FuzzyEngines.h b/AI/VCAI/FuzzyEngines.h index f09920da7..63e8279bc 100644 --- a/AI/VCAI/FuzzyEngines.h +++ b/AI/VCAI/FuzzyEngines.h @@ -8,7 +8,11 @@ * */ #pragma once -#include +#if __has_include() +# include +#else +# include +#endif #include "Goals/AbstractGoal.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/cmake_modules/Findfuzzylite.cmake b/cmake_modules/Findfuzzylite.cmake index 4932a7eca..e95ec54e5 100644 --- a/cmake_modules/Findfuzzylite.cmake +++ b/cmake_modules/Findfuzzylite.cmake @@ -21,11 +21,13 @@ find_path(fuzzylite_INCLUDE_DIR fl/fuzzylite.h + fuzzylite/fuzzylite.h HINTS ENV FLDIR PATH_SUFFIXES fl - include/fl + include/fl + include/fuzzylite include ) @@ -62,4 +64,4 @@ if (NOT TARGET "fuzzylite::fuzzylite" AND fuzzylite_FOUND) IMPORTED_LOCATION "${fuzzylite_LIBRARY}") endif() -mark_as_advanced(fuzzylite_LIBRARY fuzzylite_INCLUDE_DIR) \ No newline at end of file +mark_as_advanced(fuzzylite_LIBRARY fuzzylite_INCLUDE_DIR) From a005829a571c69cdfd03a90396b63b3ee9691089 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 10 Sep 2023 11:44:28 +0300 Subject: [PATCH 0437/1248] NKAI: cancel checking building which needs itself as prerequisite --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index ae506f4af..386e71779 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -256,7 +256,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( { logAi->trace("cant build. Need other dwelling"); } - else + else if(missingBuildings[0] != toBuild) { logAi->trace("cant build. Need %d", missingBuildings[0].num); @@ -274,6 +274,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite( return prerequisite; } + else + { + logAi->trace("Cant build. The building requires itself as prerequisite"); + + return info; + } } } else From e0089c76ae17d51e94b162c8dd6573aaac5de086 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 16 Sep 2023 11:18:09 +0000 Subject: [PATCH 0438/1248] Add shebang, use elif and only run on linux-clang-test --- .github/validate_json.py | 16 +++++++++++----- .github/workflows/github.yml | 3 ++- 2 files changed, 13 insertions(+), 6 deletions(-) mode change 100644 => 100755 .github/validate_json.py diff --git a/.github/validate_json.py b/.github/validate_json.py old mode 100644 new mode 100755 index e78d0601b..82dab94ec --- a/.github/validate_json.py +++ b/.github/validate_json.py @@ -1,3 +1,5 @@ +#!/usr/bin/env python3 + import re from pathlib import Path from pprint import pprint @@ -20,7 +22,7 @@ for path in sorted(Path('.').glob('**/*.json')): with open(path_str, 'r') as file: if VALIDATION_TYPE == 'json': jstyleson.load(file) - if VALIDATION_TYPE == 'json5': + elif VALIDATION_TYPE == 'json5': json5.load(file) elif VALIDATION_TYPE == 'yaml': file = file.read().replace("\t", " ") @@ -33,17 +35,21 @@ for path in sorted(Path('.').glob('**/*.json')): error_pos = path_str + # create error position strings for each type of parser if hasattr(exc, 'pos'): + # 'json' # https://stackoverflow.com/a/72850269/2278742 error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" print(error_pos) - if hasattr(exc, 'problem_mark'): + elif VALIDATION_TYPE == 'json5': + # 'json5' + pos = re.findall(r'\d+', str(exc)) + error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" + elif hasattr(exc, 'problem_mark'): + # 'yaml' mark = exc.problem_mark error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" print(error_pos) - if VALIDATION_TYPE == 'json5': - pos = re.findall(r'\d+', str(exc)) - error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" errors.append({"error_pos": error_pos, "error_msg": exc}) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 4242f8e12..eecb0f737 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -147,7 +147,8 @@ jobs: - name: Validate JSON # the Python yaml module doesn't seem to work on mac-arm - if: ${{ startsWith(matrix.preset, 'linux') }} + # also, running it on multiple presets is redundant and slightly increases already long CI built times + if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | pip3 install json5 jstyleson python3 .github/validate_json.py From a3b60bf829079b6f5e4e7a6fffed1b4af7aaefa5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 16 Sep 2023 14:30:25 +0200 Subject: [PATCH 0439/1248] Fix warning --- lib/mapObjects/CGPandoraBox.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 1ddf2d997..4fa3e50df 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -212,7 +212,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) for(int idx = 0; idx < reward.primary.size(); idx ++) { handler.serializeInt(NPrimarySkill::names[idx], reward.primary[idx], 0); - updateReward |= reward.primary[idx]; + updateReward |= bool(reward.primary[idx]); } addReward(updateReward); } From 903be33bf388f512eb5d1c5e9863ef3f2653c6b6 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 16 Sep 2023 12:29:35 +0300 Subject: [PATCH 0440/1248] #2689 - fix approaching guards when closest tile is double-guarded --- lib/pathfinder/CPathfinder.cpp | 5 +++++ lib/pathfinder/CPathfinder.h | 2 ++ lib/pathfinder/PathfindingRules.cpp | 4 ++-- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index cf4910ea3..25ea77981 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -455,6 +455,11 @@ bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const return true; } +int CPathfinderHelper::getGuardiansCount(int3 tile) const +{ + return getGuardingCreatures(tile).size(); +} + CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options): CGameInfoCallback(gs), turn(-1), diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 4c196ee3b..f95b0cbf4 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -121,6 +121,8 @@ public: int movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const; bool passOneTurnLimitCheck(const PathNodeInfo & source) const; + + int getGuardiansCount(int3 tile) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 44dc9b330..48aaf7814 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -298,8 +298,8 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(source.guarded) { - if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) && - !destination.isGuardianTile) // Can step into tile of guard + if(!(pathfinderConfig->options.originalMovementRules && source.node->layer == EPathfindingLayer::AIR) + && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { return BlockingReason::SOURCE_GUARDED; } From 347ce01dbe750c79568c6f374185f21df83b1b00 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Sep 2023 22:37:42 +0300 Subject: [PATCH 0441/1248] Moved hero movement logic to a new class from PlayerInterface --- client/CMakeLists.txt | 2 + client/CPlayerInterface.cpp | 457 ++++++------------------------ client/CPlayerInterface.h | 10 +- client/HeroMovementController.cpp | 379 +++++++++++++++++++++++++ client/HeroMovementController.h | 58 ++++ 5 files changed, 527 insertions(+), 379 deletions(-) create mode 100644 client/HeroMovementController.cpp create mode 100644 client/HeroMovementController.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ae542146d..5a284d1aa 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -142,6 +142,7 @@ set(client_SRCS CVideoHandler.cpp Client.cpp ClientCommandManager.cpp + HeroMovementController.cpp NetPacksClient.cpp NetPacksLobbyClient.cpp ) @@ -304,6 +305,7 @@ set(client_HEADERS Client.h ClientCommandManager.h ClientNetPackVisitors.h + HeroMovementController.h LobbyClientNetPackVisitors.h resource.h ) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ab28d7b8b..ef8b5c2f9 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -12,104 +12,112 @@ #include +#include "CGameInfo.h" +#include "CMT.h" +#include "CMusicHandler.h" +#include "CServerHandler.h" +#include "HeroMovementController.h" +#include "PlayerLocalState.h" + #include "adventureMap/AdventureMapInterface.h" -#include "mapView/mapHandler.h" +#include "adventureMap/CInGameConsole.h" #include "adventureMap/CList.h" -#include "battle/BattleInterface.h" + #include "battle/BattleEffectsController.h" #include "battle/BattleFieldController.h" +#include "battle/BattleInterface.h" #include "battle/BattleInterfaceClasses.h" #include "battle/BattleWindow.h" -#include "../CCallback.h" -#include "windows/CCastleInterface.h" + #include "eventsSDL/InputHandler.h" -#include "mainmenu/CMainMenu.h" +#include "eventsSDL/NotificationHandler.h" + +#include "gui/CGuiHandler.h" #include "gui/CursorHandler.h" -#include "windows/CKingdomInterface.h" -#include "CGameInfo.h" -#include "PlayerLocalState.h" -#include "CMT.h" -#include "windows/CHeroWindow.h" -#include "windows/CCreatureWindow.h" -#include "windows/CQuestLog.h" -#include "windows/CPuzzleWindow.h" -#include "widgets/CComponent.h" -#include "widgets/CGarrisonInt.h" -#include "widgets/Buttons.h" -#include "windows/CTradeWindow.h" -#include "windows/CSpellWindow.h" -#include "../lib/CConfigHandler.h" -#include "windows/GUIClasses.h" +#include "gui/WindowHandler.h" + +#include "mainmenu/CMainMenu.h" + +#include "mapView/mapHandler.h" + #include "render/CAnimation.h" #include "render/IImage.h" + +#include "widgets/Buttons.h" +#include "widgets/CComponent.h" +#include "widgets/CGarrisonInt.h" + +#include "windows/CCastleInterface.h" +#include "windows/CCreatureWindow.h" +#include "windows/CHeroWindow.h" +#include "windows/CKingdomInterface.h" +#include "windows/CPuzzleWindow.h" +#include "windows/CQuestLog.h" +#include "windows/CSpellWindow.h" +#include "windows/CTradeWindow.h" +#include "windows/GUIClasses.h" +#include "windows/InfoWindows.h" + +#include "../CCallback.h" + #include "../lib/CArtHandler.h" +#include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/CStack.h" +#include "../lib/CStopWatch.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CTownHandler.h" +#include "../lib/CondSh.h" +#include "../lib/GameConstants.h" +#include "../lib/JsonNode.h" +#include "../lib/NetPacks.h" //todo: remove +#include "../lib/NetPacksBase.h" +#include "../lib/RoadHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/TerrainHandler.h" +#include "../lib/TextOperations.h" +#include "../lib/UnlockGuard.h" +#include "../lib/VCMIDirs.h" + #include "../lib/bonuses/CBonusSystemNode.h" #include "../lib/bonuses/Limiters.h" -#include "../lib/bonuses/Updaters.h" #include "../lib/bonuses/Propagators.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/serializer/BinarySerializer.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CTownHandler.h" +#include "../lib/bonuses/Updaters.h" + +#include "../lib/gameState/CGameState.h" + #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/mapObjects/MiscObjects.h" #include "../lib/mapObjects/ObjectTemplate.h" + #include "../lib/mapping/CMapHeader.h" + #include "../lib/pathfinder/CGPathNode.h" -#include "../lib/CStack.h" -#include "../lib/JsonNode.h" -#include "CMusicHandler.h" -#include "../lib/CondSh.h" -#include "../lib/NetPacksBase.h" -#include "../lib/NetPacks.h"//todo: remove -#include "../lib/VCMIDirs.h" -#include "../lib/CStopWatch.h" -#include "../lib/StartInfo.h" -#include "../lib/TextOperations.h" -#include "../lib/CPlayerState.h" -#include "../lib/GameConstants.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "windows/InfoWindows.h" -#include "../lib/UnlockGuard.h" -#include "../lib/RoadHandler.h" -#include "../lib/TerrainHandler.h" -#include "../lib/CThreadHelper.h" -#include "CServerHandler.h" -// FIXME: only needed for CGameState::mutex -#include "../lib/gameState/CGameState.h" -#include "eventsSDL/NotificationHandler.h" -#include "adventureMap/CInGameConsole.h" + +#include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/CTypeList.h" + +#include "../lib/spells/CSpellHandler.h" // The macro below is used to mark functions that are called by client when game state changes. // They all assume that CPlayerInterface::pim mutex is locked. #define EVENT_HANDLER_CALLED_BY_CLIENT -// The macro marks functions that are run on a new thread by client. -// They do not own any mutexes intiially. -#define THREAD_CREATED_BY_CLIENT - -#define RETURN_IF_QUICK_COMBAT \ +#define BATTLE_EVENT_POSSIBLE_RETURN \ + if (LOCPLINT != this) \ + return; \ if (isAutoFightOn && !battleInt) \ return; -#define BATTLE_EVENT_POSSIBLE_RETURN\ - if (LOCPLINT != this) \ - return; \ - RETURN_IF_QUICK_COMBAT - boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex; CPlayerInterface * LOCPLINT; std::shared_ptr CPlayerInterface::battleInt; -enum EMoveState {STOP_MOVE, WAITING_MOVE, CONTINUE_MOVE, DURING_MOVE}; -CondSh stillMoveHero(STOP_MOVE); //used during hero movement - struct HeroObjectRetriever { const CGHeroInstance * operator()(const ConstTransitivePtr &h) const @@ -126,8 +134,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): localState(std::make_unique(*this)) { logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); GH.defActionsDef = 0; LOCPLINT = this; playerID=Player; @@ -140,7 +146,6 @@ CPlayerInterface::CPlayerInterface(PlayerColor Player): firstCall = 1; //if loading will be overwritten in serialize autosaveCount = 0; isAutoFightOn = false; - duringMovement = false; ignoreEvents = false; numOfMovedArts = 0; } @@ -171,7 +176,7 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) EVENT_HANDLER_CALLED_BY_CLIENT; makingTurn = false; - stillMoveHero.setn(STOP_MOVE); + movementController->onPlayerTurnStarted(); if(GH.windows().findWindows().empty()) { @@ -341,90 +346,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) if (!hero) return; - if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) - { - if(hero->getRemovalSound() && hero->tempOwner == playerID) - CCS->soundh->playSound(hero->getRemovalSound().value()); - } - - std::unordered_set changedTiles { - hero->convertToVisitablePos(details.start), - hero->convertToVisitablePos(details.end) - }; - adventureInt->onMapTilesChanged(changedTiles); - adventureInt->onHeroMovementStarted(hero); - - bool directlyAttackingCreature = details.attackedFrom && localState->hasPath(hero) && localState->getPath(hero).endPos() == *details.attackedFrom; - - if(makingTurn && hero->tempOwner == playerID) //we are moving our hero - we may need to update assigned path - { - if(details.result == TryMoveHero::TELEPORTATION) - { - if(localState->hasPath(hero)) - { - assert(localState->getPath(hero).nodes.size() >= 2); - auto nodesIt = localState->getPath(hero).nodes.end() - 1; - - if((nodesIt)->coord == hero->convertToVisitablePos(details.start) - && (nodesIt - 1)->coord == hero->convertToVisitablePos(details.end)) - { - //path was between entrance and exit of teleport -> OK, erase node as usual - localState->removeLastNode(hero); - } - else - { - //teleport was not along current path, it'll now be invalid (hero is somewhere else) - localState->erasePath(hero); - - } - } - } - - if(hero->pos != details.end //hero didn't change tile but visit succeeded - || directlyAttackingCreature) // or creature was attacked from endangering tile. - { - localState->erasePath(hero); - } - else if(localState->hasPath(hero) && hero->pos == details.end) //&& hero is moving - { - if(details.start != details.end) //so we don't touch path when revisiting with spacebar - localState->removeLastNode(hero); - } - } - - if(details.stopMovement()) //hero failed to move - { - stillMoveHero.setn(STOP_MOVE); - adventureInt->onHeroChanged(hero); - return; - } - - CGI->mh->waitForOngoingAnimations(); - - //move finished - adventureInt->onHeroChanged(hero); - - //check if user cancelled movement - { - if (GH.input().ignoreEventsUntilInput()) - stillMoveHero.setn(STOP_MOVE); - } - - if (stillMoveHero.get() == WAITING_MOVE) - stillMoveHero.setn(DURING_MOVE); - - // Hero attacked creature directly, set direction to face it. - if (directlyAttackingCreature) { - // Get direction to attacker. - int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); - static const ui8 dirLookup[3][3] = { - { 1, 2, 3 }, - { 8, 0, 4 }, - { 7, 6, 5 } - }; - // FIXME: Avoid const_cast, make moveDir mutable in some other way? - const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; - } + movementController->heroMoved(hero, details); } void CPlayerInterface::heroKilled(const CGHeroInstance* hero) @@ -660,10 +582,7 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) { - // when battle starts, game will send battleStart pack *before* movement confirmation - // and since network thread wait for battle intro to play, movement confirmation will only happen after intro - // leading to several bugs, such as blocked input during intro - stillMoveHero.setn(STOP_MOVE); + movementController->onBattleStarted(); //Don't wait for dialogs when we are non-active hot-seat player if (LOCPLINT == this) @@ -914,7 +833,6 @@ void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const Batt EVENT_HANDLER_CALLED_BY_CLIENT; BATTLE_EVENT_POSSIBLE_RETURN; - RETURN_IF_QUICK_COMBAT; battleInt->effectsController->battleTriggerEffect(bte); if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN)) @@ -1131,12 +1049,7 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v void CPlayerInterface::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { EVENT_HANDLER_CALLED_BY_CLIENT; - int choosenExit = -1; - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - if (destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); - - cb->selectionMade(choosenExit, askID); + movementController->showTeleportDialog(channel, exits, impassable, askID); } void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) @@ -1257,12 +1170,10 @@ void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) if (showingDialog->get() || !dialogs.empty()) return; - setMovementStatus(true); - if (localState->isHeroSleeping(h)) localState->setHeroAwaken(h); - boost::thread moveHeroTask(std::bind(&CPlayerInterface::doMoveHero,this,h,path)); + movementController->doMoveHero(h, path); } void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) @@ -1270,7 +1181,7 @@ void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHer EVENT_HANDLER_CALLED_BY_CLIENT; auto onEnd = [=](){ cb->selectionMade(0, queryID); }; - if (stillMoveHero.get() == DURING_MOVE && localState->hasPath(down) && localState->getPath(down).nodes.size() > 1) //to ignore calls on passing through garrisons + if (movementController->isHeroMovingThroughGarrison(down)) { onEnd(); return; @@ -1315,18 +1226,12 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con void CPlayerInterface::requestRealized( PackageApplied *pa ) { EVENT_HANDLER_CALLED_BY_CLIENT; - if (pa->packType == typeList.getTypeID() && stillMoveHero.get() == DURING_MOVE - && destinationTeleport == ObjectInstanceID()) - stillMoveHero.setn(CONTINUE_MOVE); - if (destinationTeleport != ObjectInstanceID() - && pa->packType == typeList.getTypeID() - && stillMoveHero.get() == DURING_MOVE) - { // After teleportation via CGTeleport object is finished - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - stillMoveHero.setn(CONTINUE_MOVE); - } + if(pa->packType == typeList.getTypeID()) + movementController->onMoveHeroApplied(); + + if (pa->packType == typeList.getTypeID()) + movementController->onQueryReplyApplied(); } @@ -1714,8 +1619,9 @@ void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) void CPlayerInterface::stopMovement() { - if (stillMoveHero.get() == DURING_MOVE)//if we are in the middle of hero movement - stillMoveHero.setn(STOP_MOVE); //after showing dialog movement will be stopped + movementController->movementStopRequested(); + + } void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) @@ -1910,7 +1816,7 @@ void CPlayerInterface::proposeLoadingGame() bool CPlayerInterface::capturedAllEvents() { - if(duringMovement) + if(movementController->isHeroMoving()) { //just inform that we are capturing events. they will be processed by heroMoved() in client thread. return true; @@ -1928,197 +1834,6 @@ bool CPlayerInterface::capturedAllEvents() return false; } -void CPlayerInterface::setMovementStatus(bool value) -{ - duringMovement = value; - if (value) - { - CCS->curh->hide(); - } - else - { - CCS->curh->show(); - } -} - -void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path) -{ - setThreadName("doMoveHero"); - - int i = 1; - auto getObj = [&](int3 coord, bool ignoreHero) - { - return cb->getTile(h->convertToVisitablePos(coord))->topVisitableObj(ignoreHero); - }; - - auto isTeleportAction = [&](EPathNodeAction action) -> bool - { - if (action != EPathNodeAction::TELEPORT_NORMAL && - action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && - action != EPathNodeAction::TELEPORT_BATTLE) - { - return false; - } - - return true; - }; - - auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * - { - if (CGTeleport::isConnected(currentObject, nextObjectTop)) - return nextObjectTop; - if (nextObjectTop && nextObjectTop->ID == Obj::HERO && - CGTeleport::isConnected(currentObject, nextObject)) - { - return nextObject; - } - - return nullptr; - }; - - boost::unique_lock un(stillMoveHero.mx); - stillMoveHero.data = CONTINUE_MOVE; - auto doMovement = [&](int3 dst, bool transit) - { - stillMoveHero.data = WAITING_MOVE; - cb->moveHero(h, dst, transit); - while(stillMoveHero.data != STOP_MOVE && stillMoveHero.data != CONTINUE_MOVE) - stillMoveHero.cond.wait(un); - }; - - { - for (auto & elem : path.nodes) - elem.coord = h->convertFromVisitablePos(elem.coord); - - int soundChannel = -1; - AudioPath soundName; - - auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> AudioPath - { - if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) - return {}; - - if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) - return {}; - - if (moveType == EPathNodeAction::BLOCKING_VISIT) - return {}; - - // flying movement sound - if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) - return AudioPath::builtin("HORSE10.wav"); - - auto prevTile = cb->getTile(h->convertToVisitablePos(posPrev)); - auto nextTile = cb->getTile(h->convertToVisitablePos(posNext)); - - auto prevRoad = prevTile->roadType; - auto nextRoad = nextTile->roadType; - bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; - - if (movingOnRoad) - return nextTile->terType->horseSound; - else - return nextTile->terType->horseSoundPenalty; - }; - - auto canStop = [&](CGPathNode * node) -> bool - { - if (node->layer != EPathfindingLayer::LAND && node->layer != EPathfindingLayer::SAIL) - return false; - - if (node->accessible != EPathAccessibility::ACCESSIBLE) - return false; - - return true; - }; - - for (i=(int)path.nodes.size()-1; i>0 && (stillMoveHero.data == CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) - { - int3 prevCoord = path.nodes[i].coord; - int3 nextCoord = path.nodes[i-1].coord; - - auto prevObject = getObj(prevCoord, prevCoord == h->pos); - auto nextObjectTop = getObj(nextCoord, false); - auto nextObject = getObj(nextCoord, true); - auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject); - if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) - { - CCS->soundh->stopSound(soundChannel); - destinationTeleport = destTeleportObj->id; - destinationTeleportPos = nextCoord; - doMovement(h->pos, false); - if (path.nodes[i-1].action == EPathNodeAction::TELEPORT_BLOCKING_VISIT - || path.nodes[i-1].action == EPathNodeAction::TELEPORT_BATTLE) - { - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - } - if(i != path.nodes.size() - 1) - { - soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } - continue; - } - - if (path.nodes[i-1].turns) - { //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) - stillMoveHero.data = STOP_MOVE; - break; - } - - { - // Start a new sound for the hero movement or let the existing one carry on. - AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - - if(newSoundName != soundName) - { - soundName = newSoundName; - - CCS->soundh->stopSound(soundChannel); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } - } - - assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all - int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); - logGlobal->trace("Requesting hero movement to %s", endpos.toString()); - - bool useTransit = false; - if ((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless - && (CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i-2].coord, false)) - || CGTeleport::isTeleport(nextObjectTop))) - { // Hero should be able to go through object if it's allow transit - useTransit = true; - } - else if (path.nodes[i-1].layer == EPathfindingLayer::AIR) - useTransit = true; - - doMovement(endpos, useTransit); - - logGlobal->trace("Resuming %s", __FUNCTION__); - bool guarded = cb->isInTheMap(cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); - if ((!useTransit && guarded) || showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) - break; - } - - CCS->soundh->stopSound(soundChannel); - } - - //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement - if (!showingDialog->get()) - GH.fakeMouseMove(); - - CGI->mh->waitForOngoingAnimations(); - setMovementStatus(false); -} - void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions, bool showTerrain) { EVENT_HANDLER_CALLED_BY_CLIENT; diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 5d91ef9e4..ef0e6d94b 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -47,6 +47,7 @@ class KeyInterested; class MotionInterested; class PlayerLocalState; class TimeInterested; +class HeroMovementController; namespace boost { @@ -57,7 +58,6 @@ namespace boost /// Central class for managing user interface logic class CPlayerInterface : public CGameInterface, public IUpdateable { - bool duringMovement; bool ignoreEvents; size_t numOfMovedArts; @@ -67,9 +67,7 @@ class CPlayerInterface : public CGameInterface, public IUpdateable std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) - ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation - int3 destinationTeleportPos; - + std::unique_ptr movementController; public: // TODO: make private std::shared_ptr env; @@ -222,7 +220,6 @@ private: { owner.ignoreEvents = false; }; - }; void heroKilled(const CGHeroInstance* hero); @@ -231,9 +228,6 @@ private: void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close void initializeHeroTownList(); int getLastIndex(std::string namePrefix); - void doMoveHero(const CGHeroInstance *h, CGPath path); - void setMovementStatus(bool value); - }; /// Provides global access to instance of interface of currently active player diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp new file mode 100644 index 000000000..bac94e665 --- /dev/null +++ b/client/HeroMovementController.cpp @@ -0,0 +1,379 @@ +/* + * CPlayerInterface.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 "HeroMovementController.h" + +#include "CGameInfo.h" +#include "CMusicHandler.h" +#include "CPlayerInterface.h" +#include "PlayerLocalState.h" +#include "adventureMap/AdventureMapInterface.h" +#include "eventsSDL/InputHandler.h" +#include "gui/CGuiHandler.h" +#include "gui/CursorHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" + +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/MiscObjects.h" +#include "../lib/RoadHandler.h" +#include "../lib/TerrainHandler.h" +#include "../lib/NetPacks.h" +#include "../lib/CondSh.h" + +HeroMovementController::HeroMovementController() +{ + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + duringMovement = false; +} + +void HeroMovementController::setMovementStatus(bool value) +{ + duringMovement = value; + if (value) + { + CCS->curh->hide(); + } + else + { + CCS->curh->show(); + } +} + +bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero) const +{ + //to ignore calls on passing through garrisons + return (movementState == EMoveState::DURING_MOVE && LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nodes.size() > 1); +} + +bool HeroMovementController::isHeroMoving() const +{ + return duringMovement; +} + +void HeroMovementController::onMoveHeroApplied() +{ + assert(movementState == EMoveState::DURING_MOVE); + + if(movementState == EMoveState::DURING_MOVE) + { + assert(destinationTeleport == ObjectInstanceID::NONE); + movementState = EMoveState::CONTINUE_MOVE; + } +} + +void HeroMovementController::onQueryReplyApplied() +{ + assert(movementState == EMoveState::DURING_MOVE); + + if(movementState == EMoveState::DURING_MOVE) + { + // After teleportation via CGTeleport object is finished + assert(destinationTeleport != ObjectInstanceID::NONE); + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + movementState = EMoveState::CONTINUE_MOVE; + } +} + +void HeroMovementController::onPlayerTurnStarted() +{ + assert(movementState == EMoveState::STOP_MOVE); + movementState = EMoveState::STOP_MOVE; +} + +void HeroMovementController::onBattleStarted() +{ + // when battle starts, game will send battleStart pack *before* movement confirmation + // and since network thread wait for battle intro to play, movement confirmation will only happen after intro + // leading to several bugs, such as blocked input during intro + movementState = EMoveState::STOP_MOVE; +} + +void HeroMovementController::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + int choosenExit = -1; + auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); + if (destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) + choosenExit = vstd::find_pos(exits, neededExit); + + LOCPLINT->cb->selectionMade(choosenExit, askID); +} + +void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) +{ + if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) + { + if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) + CCS->soundh->playSound(hero->getRemovalSound().value()); + } + + std::unordered_set changedTiles { + hero->convertToVisitablePos(details.start), + hero->convertToVisitablePos(details.end) + }; + adventureInt->onMapTilesChanged(changedTiles); + adventureInt->onHeroMovementStarted(hero); + + bool directlyAttackingCreature = details.attackedFrom && LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).endPos() == *details.attackedFrom; + + if(LOCPLINT->makingTurn && hero->tempOwner == LOCPLINT->playerID) //we are moving our hero - we may need to update assigned path + { + if(details.result == TryMoveHero::TELEPORTATION) + { + if(LOCPLINT->localState->hasPath(hero)) + { + assert(LOCPLINT->localState->getPath(hero).nodes.size() >= 2); + auto nodesIt = LOCPLINT->localState->getPath(hero).nodes.end() - 1; + + if((nodesIt)->coord == hero->convertToVisitablePos(details.start) + && (nodesIt - 1)->coord == hero->convertToVisitablePos(details.end)) + { + //path was between entrance and exit of teleport -> OK, erase node as usual + LOCPLINT->localState->removeLastNode(hero); + } + else + { + //teleport was not along current path, it'll now be invalid (hero is somewhere else) + LOCPLINT->localState->erasePath(hero); + + } + } + } + + if(hero->pos != details.end //hero didn't change tile but visit succeeded + || directlyAttackingCreature) // or creature was attacked from endangering tile. + { + LOCPLINT->localState->erasePath(hero); + } + else if(LOCPLINT->localState->hasPath(hero) && hero->pos == details.end) //&& hero is moving + { + if(details.start != details.end) //so we don't touch path when revisiting with spacebar + LOCPLINT->localState->removeLastNode(hero); + } + } + + if(details.stopMovement()) //hero failed to move + { + movementState = EMoveState::STOP_MOVE; + adventureInt->onHeroChanged(hero); + return; + } + + CGI->mh->waitForOngoingAnimations(); + + //move finished + adventureInt->onHeroChanged(hero); + + //check if user cancelled movement + { + if (GH.input().ignoreEventsUntilInput()) + movementState = EMoveState::STOP_MOVE; + } + + if (movementState == EMoveState::WAITING_MOVE) + movementState = EMoveState::DURING_MOVE; + + // Hero attacked creature directly, set direction to face it. + if (directlyAttackingCreature) + { + // Get direction to attacker. + int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); + static const ui8 dirLookup[3][3] = + { + { 1, 2, 3 }, + { 8, 0, 4 }, + { 7, 6, 5 } + }; + // FIXME: Avoid const_cast, make moveDir mutable in some other way? + const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; + } +} + +void HeroMovementController::movementStopRequested() +{ + if (movementState == EMoveState::DURING_MOVE)//if we are in the middle of hero movement + movementState = EMoveState::STOP_MOVE; +} + +void HeroMovementController::doMoveHero(const CGHeroInstance * h, const CGPath & path) +{ + setMovementStatus(true); + + auto getObj = [&](int3 coord, bool ignoreHero) + { + return LOCPLINT->cb->getTile(h->convertToVisitablePos(coord))->topVisitableObj(ignoreHero); + }; + + auto isTeleportAction = [&](EPathNodeAction action) -> bool + { + if (action != EPathNodeAction::TELEPORT_NORMAL && + action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && + action != EPathNodeAction::TELEPORT_BATTLE) + { + return false; + } + + return true; + }; + + auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * + { + if (CGTeleport::isConnected(currentObject, nextObjectTop)) + return nextObjectTop; + + if (nextObjectTop && nextObjectTop->ID == Obj::HERO && CGTeleport::isConnected(currentObject, nextObject)) + { + return nextObject; + } + + return nullptr; + }; + + auto doMovement = [&](int3 dst, bool transit) + { + movementState = EMoveState::WAITING_MOVE; + LOCPLINT->cb->moveHero(h, dst, transit); + while(movementState != EMoveState::STOP_MOVE && movementState != EMoveState::CONTINUE_MOVE) + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); + }; + + auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> AudioPath + { + if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) + return {}; + + if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) + return {}; + + if (moveType == EPathNodeAction::BLOCKING_VISIT) + return {}; + + // flying movement sound + if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) + return AudioPath::builtin("HORSE10.wav"); + + auto prevTile = LOCPLINT->cb->getTile(h->convertToVisitablePos(posPrev)); + auto nextTile = LOCPLINT->cb->getTile(h->convertToVisitablePos(posNext)); + + auto prevRoad = prevTile->roadType; + auto nextRoad = nextTile->roadType; + bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; + + if (movingOnRoad) + return nextTile->terType->horseSound; + else + return nextTile->terType->horseSoundPenalty; + }; + + auto canStop = [&](const CGPathNode * node) -> bool + { + if (node->layer != EPathfindingLayer::LAND && node->layer != EPathfindingLayer::SAIL) + return false; + + if (node->accessible != EPathAccessibility::ACCESSIBLE) + return false; + + return true; + }; + + int i = 1; + movementState = EMoveState::CONTINUE_MOVE; + + int soundChannel = -1; + AudioPath soundName; + + for (i=(int)path.nodes.size()-1; i>0 && (movementState == EMoveState::CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) + { + int3 prevCoord = h->convertFromVisitablePos(path.nodes[i].coord); + int3 nextCoord = h->convertFromVisitablePos(path.nodes[i-1].coord); + + auto prevObject = getObj(prevCoord, prevCoord == h->pos); + auto nextObjectTop = getObj(nextCoord, false); + auto nextObject = getObj(nextCoord, true); + auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject); + if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) + { + CCS->soundh->stopSound(soundChannel); + destinationTeleport = destTeleportObj->id; + destinationTeleportPos = nextCoord; + doMovement(h->pos, false); + if (path.nodes[i-1].action == EPathNodeAction::TELEPORT_BLOCKING_VISIT + || path.nodes[i-1].action == EPathNodeAction::TELEPORT_BATTLE) + { + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + } + if(i != path.nodes.size() - 1) + { + soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + if (!soundName.empty()) + soundChannel = CCS->soundh->playSound(soundName, -1); + else + soundChannel = -1; + } + continue; + } + + if (path.nodes[i-1].turns) + { //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) + movementState = EMoveState::STOP_MOVE; + break; + } + + { + // Start a new sound for the hero movement or let the existing one carry on. + AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + + if(newSoundName != soundName) + { + soundName = newSoundName; + + CCS->soundh->stopSound(soundChannel); + if (!soundName.empty()) + soundChannel = CCS->soundh->playSound(soundName, -1); + else + soundChannel = -1; + } + } + + assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all + int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); + logGlobal->trace("Requesting hero movement to %s", endpos.toString()); + + bool useTransit = false; + if ((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless + && (CGTeleport::isConnected(nextObjectTop, getObj(h->convertFromVisitablePos(path.nodes[i-2].coord), false)) + || CGTeleport::isTeleport(nextObjectTop))) + { // Hero should be able to go through object if it's allow transit + useTransit = true; + } + else if (path.nodes[i-1].layer == EPathfindingLayer::AIR) + useTransit = true; + + doMovement(endpos, useTransit); + + logGlobal->trace("Resuming %s", __FUNCTION__); + bool guarded = LOCPLINT->cb->isInTheMap(LOCPLINT->cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); + if ((!useTransit && guarded) || LOCPLINT->showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) + break; + } + CCS->soundh->stopSound(soundChannel); + + //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement + if (!LOCPLINT->showingDialog->get()) + GH.fakeMouseMove(); + + CGI->mh->waitForOngoingAnimations(); + setMovementStatus(false); +} diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h new file mode 100644 index 000000000..60b62fe9f --- /dev/null +++ b/client/HeroMovementController.h @@ -0,0 +1,58 @@ +/* + * CPlayerInterface.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/constants/EntityIdentifiers.h" +#include "../lib/int3.h" + +VCMI_LIB_NAMESPACE_BEGIN +using TTeleportExitsList = std::vector>; + +class CGHeroInstance; + +struct CGPath; +struct TryMoveHero; +VCMI_LIB_NAMESPACE_END + +class HeroMovementController +{ + ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation + int3 destinationTeleportPos; + + bool duringMovement; + + + enum class EMoveState + { + STOP_MOVE, + WAITING_MOVE, + CONTINUE_MOVE, + DURING_MOVE + }; + + EMoveState movementState; +public: + HeroMovementController(); + + bool isHeroMovingThroughGarrison(const CGHeroInstance * hero) const; + bool isHeroMoving() const; + + void onMoveHeroApplied(); + void onQueryReplyApplied(); + + void onPlayerTurnStarted(); + void onBattleStarted(); + void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); + void heroMoved(const CGHeroInstance * hero, const TryMoveHero & details); + + void movementStopRequested(); + void doMoveHero(const CGHeroInstance * h, const CGPath & path); + void setMovementStatus(bool value); +}; From 80b80a0ae698d4cacb34b18900dc87a291d4e6ed Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 8 Sep 2023 18:02:36 +0300 Subject: [PATCH 0442/1248] Minor cleanup of hero movemen code --- client/CPlayerInterface.cpp | 22 +++++++-------- client/CPlayerInterface.h | 1 - client/HeroMovementController.cpp | 44 +++++++++++++++++++++-------- client/HeroMovementController.h | 12 +++++--- client/PlayerLocalState.h | 5 ++++ lib/mapObjects/CGObjectInstance.cpp | 23 +++++++++++++-- lib/mapObjects/CGObjectInstance.h | 7 ++++- 7 files changed, 81 insertions(+), 33 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ef8b5c2f9..88f30e7cf 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -980,7 +980,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector { CCS->soundh->playSound(static_cast(soundID)); showingDialog->set(true); - stopMovement(); // interrupt movement to show dialog + movementController->movementStopRequested(); // interrupt movement to show dialog GH.windows().pushWindow(temp); } else @@ -1003,7 +1003,7 @@ void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList un(*pim); - stopMovement(); + movementController->movementStopRequested(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); } @@ -1013,7 +1013,7 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); - stopMovement(); + movementController->movementStopRequested(); CCS->soundh->playSound(static_cast(soundID)); if (!selection && cancel) //simple yes/no dialog @@ -1160,6 +1160,11 @@ void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) { + assert(LOCPLINT->makingTurn); + assert(h); + assert(!showingDialog->get()); + assert(!dialogs.empty()); + LOG_TRACE(logGlobal); if (!LOCPLINT->makingTurn) return; @@ -1173,7 +1178,7 @@ void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) if (localState->isHeroSleeping(h)) localState->setHeroAwaken(h); - movementController->doMoveHero(h, path); + movementController->movementStartRequested(h, path); } void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) @@ -1181,7 +1186,7 @@ void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHer EVENT_HANDLER_CALLED_BY_CLIENT; auto onEnd = [=](){ cb->selectionMade(0, queryID); }; - if (movementController->isHeroMovingThroughGarrison(down)) + if (movementController->isHeroMovingThroughGarrison(down, up)) { onEnd(); return; @@ -1617,13 +1622,6 @@ void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) battleInt->newRoundFirst(); } -void CPlayerInterface::stopMovement() -{ - movementController->movementStopRequested(); - - -} - void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) { EVENT_HANDLER_CALLED_BY_CLIENT; diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index ef0e6d94b..85e553440 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -194,7 +194,6 @@ public: // public interface for use by client via LOCPLINT access void showInfoDialogAndWait(std::vector & components, const MetaString & text); void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); - void stopMovement(); void moveHero(const CGHeroInstance *h, const CGPath& path); void tryDigging(const CGHeroInstance *h); diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index bac94e665..597d977b1 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -30,6 +30,20 @@ #include "../lib/NetPacks.h" #include "../lib/CondSh.h" +// Code flow of movement process: +// - Player requests movement (e.g. press "move" button) +// -> calls doMoveHero (GUI thread) +// -> -> sends LOCPLINT->cb->moveHero +// - Server sends reply +// -> calls heroMoved (NETWORK thread) +// -> -> animation starts, sound starts +// -> -> waits for animation to finish +// -> -> if player have requested stop and path is not finished, calls doMoveHero again +// +// - Players requests movement stop (e.g. mouse click during movement) +// -> calls movementStopRequested +// -> -> sets flag that movement should be aborted + HeroMovementController::HeroMovementController() { destinationTeleport = ObjectInstanceID(); @@ -41,19 +55,23 @@ void HeroMovementController::setMovementStatus(bool value) { duringMovement = value; if (value) - { CCS->curh->hide(); - } else - { CCS->curh->show(); - } } -bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero) const +bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const { - //to ignore calls on passing through garrisons - return (movementState == EMoveState::DURING_MOVE && LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nodes.size() > 1); + assert(LOCPLINT->localState->hasPath(hero)); + assert(movementState == EMoveState::DURING_MOVE); + + if (!LOCPLINT->localState->hasPath(hero)) + return false; + + if (garrison->visitableAt(*LOCPLINT->localState->getLastTile(hero))) + return false; // hero want to enter garrison, not pass through it + + return true; } bool HeroMovementController::isHeroMoving() const @@ -94,6 +112,7 @@ void HeroMovementController::onPlayerTurnStarted() void HeroMovementController::onBattleStarted() { + assert(movementState == EMoveState::STOP_MOVE); // when battle starts, game will send battleStart pack *before* movement confirmation // and since network thread wait for battle intro to play, movement confirmation will only happen after intro // leading to several bugs, such as blocked input during intro @@ -102,11 +121,12 @@ void HeroMovementController::onBattleStarted() void HeroMovementController::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { - int choosenExit = -1; - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - if (destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); + assert(destinationTeleport != ObjectInstanceID::NONE); + auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); + assert(vstd::contains(exits, neededExit)); + + int choosenExit = vstd::find_pos(exits, neededExit); LOCPLINT->cb->selectionMade(choosenExit, askID); } @@ -206,7 +226,7 @@ void HeroMovementController::movementStopRequested() movementState = EMoveState::STOP_MOVE; } -void HeroMovementController::doMoveHero(const CGHeroInstance * h, const CGPath & path) +void HeroMovementController::movementStartRequested(const CGHeroInstance * h, const CGPath & path) { setMovementStatus(true); diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index 60b62fe9f..5837bf511 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN using TTeleportExitsList = std::vector>; class CGHeroInstance; +class CArmedInstance; struct CGPath; struct TryMoveHero; @@ -38,21 +39,24 @@ class HeroMovementController }; EMoveState movementState; + + void setMovementStatus(bool value); public: HeroMovementController(); - bool isHeroMovingThroughGarrison(const CGHeroInstance * hero) const; + // const queries + bool isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const; bool isHeroMoving() const; + // netpack handlers void onMoveHeroApplied(); void onQueryReplyApplied(); - void onPlayerTurnStarted(); void onBattleStarted(); void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); void heroMoved(const CGHeroInstance * hero, const TryMoveHero & details); + // UI handlers + void movementStartRequested(const CGHeroInstance * h, const CGPath & path); void movementStopRequested(); - void doMoveHero(const CGHeroInstance * h, const CGPath & path); - void setMovementStatus(bool value); }; diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index e88ee6ca3..31c919e57 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -76,6 +76,11 @@ public: void setPath(const CGHeroInstance * h, const CGPath & path); bool setPath(const CGHeroInstance * h, const int3 & destination); + /// return final node in a path, if exists + std::optional getLastTile(const CGHeroInstance * h) const; + /// return first path in a path, if exists + std::optional getNextTile(const CGHeroInstance * h) const; + const CGPath & getPath(const CGHeroInstance * h) const; bool hasPath(const CGHeroInstance * h) const; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 6f27161d3..6b5a0b274 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -64,15 +64,18 @@ void CGObjectInstance::setOwner(const PlayerColor & ow) { tempOwner = ow; } -int CGObjectInstance::getWidth() const//returns width of object graphic in tiles + +int CGObjectInstance::getWidth() const { return appearance->getWidth(); } -int CGObjectInstance::getHeight() const //returns height of object graphic in tiles + +int CGObjectInstance::getHeight() const { return appearance->getHeight(); } -bool CGObjectInstance::visitableAt(int x, int y) const //returns true if object is visitable at location (x, y) form left top tile of image (x, y in tiles) + +bool CGObjectInstance::visitableAt(int x, int y) const { return appearance->isVisitableAt(pos.x - x, pos.y - y); } @@ -86,6 +89,20 @@ bool CGObjectInstance::coveringAt(int x, int y) const return appearance->isVisibleAt(pos.x - x, pos.y - y); } +bool CGObjectInstance::visitableAt(const int3 & testPos) const +{ + return pos.z == testPos.z && appearance->isVisitableAt(pos.x - testPos.x, pos.y - testPos.y); +} +bool CGObjectInstance::blockingAt(const int3 & testPos) const +{ + return pos.z == testPos.z && appearance->isBlockedAt(pos.x - testPos.x, pos.y - testPos.y); +} + +bool CGObjectInstance::coveringAt(const int3 & testPos) const +{ + return pos.z == testPos.z && appearance->isVisibleAt(pos.x - testPos.x, pos.y - testPos.y); +} + std::set CGObjectInstance::getBlockedPos() const { std::set ret; diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 4360e610f..fdbefa545 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -60,12 +60,17 @@ public: int getWidth() const; //returns width of object graphic in tiles int getHeight() const; //returns height of object graphic in tiles - bool visitableAt(int x, int y) const; //returns true if object is visitable at location (x, y) (h3m pos) int3 visitablePos() const override; int3 getPosition() const override; int3 getTopVisiblePos() const; + bool visitableAt(int x, int y) const; //returns true if object is visitable at location (x, y) (h3m pos) bool blockingAt(int x, int y) const; //returns true if object is blocking location (x, y) (h3m pos) bool coveringAt(int x, int y) const; //returns true if object covers with picture location (x, y) (h3m pos) + + bool visitableAt(const int3 & pos) const; //returns true if object is visitable at location (x, y) (h3m pos) + bool blockingAt (const int3 & pos) const; //returns true if object is blocking location (x, y) (h3m pos) + bool coveringAt (const int3 & pos) const; //returns true if object covers with picture location (x, y) (h3m pos) + std::set getBlockedPos() const; //returns set of positions blocked by this object std::set getBlockedOffsets() const; //returns set of relative positions blocked by this object From 0afc244934205cb0ad4b3c93144cfe4ca50afa64 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 11 Sep 2023 12:54:25 +0300 Subject: [PATCH 0443/1248] Split massive methods into smaller chunks --- client/HeroMovementController.cpp | 229 ++++++++++++++++-------------- client/HeroMovementController.h | 8 ++ 2 files changed, 127 insertions(+), 110 deletions(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 597d977b1..f46aecd18 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -130,6 +130,42 @@ void HeroMovementController::showTeleportDialog(TeleportChannelID channel, TTele LOCPLINT->cb->selectionMade(choosenExit, askID); } +void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMoveHero & details) +{ + if (hero->tempOwner != LOCPLINT->playerID) + return; + + assert(LOCPLINT->makingTurn); + assert(LOCPLINT->localState->hasPath(hero)); + assert(LOCPLINT->localState->getNextTile(hero).has_value()); + + bool directlyAttackingCreature = details.attackedFrom.has_value() && LOCPLINT->localState->getLastTile(hero) == details.attackedFrom; + + auto desiredTarget = LOCPLINT->localState->getNextTile(hero); + auto actualTarget = hero->convertToVisitablePos(details.end); + + //don't erase path when revisiting with spacebar + bool heroChangedTile = details.start != details.end; + + if (desiredTarget && heroChangedTile) + { + if (*desiredTarget != actualTarget) + { + //invalidate path - movement was not along current path + //possible reasons: teleport, visit of object with "blocking visit" property + LOCPLINT->localState->erasePath(hero); + } + else + { + //movement along desired path - remove one node and keep rest of path + LOCPLINT->localState->removeLastNode(hero); + } + + if(directlyAttackingCreature) + LOCPLINT->localState->erasePath(hero); + } +} + void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) { if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) @@ -145,43 +181,7 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov adventureInt->onMapTilesChanged(changedTiles); adventureInt->onHeroMovementStarted(hero); - bool directlyAttackingCreature = details.attackedFrom && LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).endPos() == *details.attackedFrom; - - if(LOCPLINT->makingTurn && hero->tempOwner == LOCPLINT->playerID) //we are moving our hero - we may need to update assigned path - { - if(details.result == TryMoveHero::TELEPORTATION) - { - if(LOCPLINT->localState->hasPath(hero)) - { - assert(LOCPLINT->localState->getPath(hero).nodes.size() >= 2); - auto nodesIt = LOCPLINT->localState->getPath(hero).nodes.end() - 1; - - if((nodesIt)->coord == hero->convertToVisitablePos(details.start) - && (nodesIt - 1)->coord == hero->convertToVisitablePos(details.end)) - { - //path was between entrance and exit of teleport -> OK, erase node as usual - LOCPLINT->localState->removeLastNode(hero); - } - else - { - //teleport was not along current path, it'll now be invalid (hero is somewhere else) - LOCPLINT->localState->erasePath(hero); - - } - } - } - - if(hero->pos != details.end //hero didn't change tile but visit succeeded - || directlyAttackingCreature) // or creature was attacked from endangering tile. - { - LOCPLINT->localState->erasePath(hero); - } - else if(LOCPLINT->localState->hasPath(hero) && hero->pos == details.end) //&& hero is moving - { - if(details.start != details.end) //so we don't touch path when revisiting with spacebar - LOCPLINT->localState->removeLastNode(hero); - } - } + updatePath(hero, details); if(details.stopMovement()) //hero failed to move { @@ -196,36 +196,91 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov adventureInt->onHeroChanged(hero); //check if user cancelled movement - { - if (GH.input().ignoreEventsUntilInput()) - movementState = EMoveState::STOP_MOVE; - } + if (GH.input().ignoreEventsUntilInput()) + movementState = EMoveState::STOP_MOVE; if (movementState == EMoveState::WAITING_MOVE) movementState = EMoveState::DURING_MOVE; - // Hero attacked creature directly, set direction to face it. - if (directlyAttackingCreature) - { - // Get direction to attacker. - int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); - static const ui8 dirLookup[3][3] = - { - { 1, 2, 3 }, - { 8, 0, 4 }, - { 7, 6, 5 } - }; - // FIXME: Avoid const_cast, make moveDir mutable in some other way? - const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; - } +// // Hero attacked creature directly, set direction to face it. +// if (directlyAttackingCreature) +// { +// // Get direction to attacker. +// int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); +// static const ui8 dirLookup[3][3] = +// { +// { 1, 2, 3 }, +// { 8, 0, 4 }, +// { 7, 6, 5 } +// }; +// // FIXME: Avoid const_cast, make moveDir mutable in some other way? +// const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; +// } } void HeroMovementController::movementStopRequested() { + assert(movementState == EMoveState::DURING_MOVE); + if (movementState == EMoveState::DURING_MOVE)//if we are in the middle of hero movement movementState = EMoveState::STOP_MOVE; } +AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) +{ + if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) + return {}; + + if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) + return {}; + + if (moveType == EPathNodeAction::BLOCKING_VISIT) + return {}; + + // flying movement sound + if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) + return AudioPath::builtin("HORSE10.wav"); + + auto prevTile = LOCPLINT->cb->getTile(hero->convertToVisitablePos(posPrev)); + auto nextTile = LOCPLINT->cb->getTile(hero->convertToVisitablePos(posNext)); + + auto prevRoad = prevTile->roadType; + auto nextRoad = nextTile->roadType; + bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; + + if (movingOnRoad) + return nextTile->terType->horseSound; + else + return nextTile->terType->horseSoundPenalty; +}; + +void HeroMovementController::updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) +{ + + // Start a new sound for the hero movement or let the existing one carry on. + AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + + if(newSoundName != soundName) + { + soundName = newSoundName; + + CCS->soundh->stopSound(soundChannel); + if (!soundName.empty()) + soundChannel = CCS->soundh->playSound(soundName, -1); + else + soundChannel = -1; + } + + + int soundChannel = -1; + AudioPath soundName; +} + +void HeroMovementController::stopMovementSound() +{ + CCS->soundh->stopSound(soundChannel); +} + void HeroMovementController::movementStartRequested(const CGHeroInstance * h, const CGPath & path) { setMovementStatus(true); @@ -265,35 +320,10 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co movementState = EMoveState::WAITING_MOVE; LOCPLINT->cb->moveHero(h, dst, transit); while(movementState != EMoveState::STOP_MOVE && movementState != EMoveState::CONTINUE_MOVE) + { + assert(0); boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); - }; - - auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) -> AudioPath - { - if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) - return {}; - - if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) - return {}; - - if (moveType == EPathNodeAction::BLOCKING_VISIT) - return {}; - - // flying movement sound - if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) - return AudioPath::builtin("HORSE10.wav"); - - auto prevTile = LOCPLINT->cb->getTile(h->convertToVisitablePos(posPrev)); - auto nextTile = LOCPLINT->cb->getTile(h->convertToVisitablePos(posNext)); - - auto prevRoad = prevTile->roadType; - auto nextRoad = nextTile->roadType; - bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; - - if (movingOnRoad) - return nextTile->terType->horseSound; - else - return nextTile->terType->horseSoundPenalty; + } }; auto canStop = [&](const CGPathNode * node) -> bool @@ -310,9 +340,6 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co int i = 1; movementState = EMoveState::CONTINUE_MOVE; - int soundChannel = -1; - AudioPath soundName; - for (i=(int)path.nodes.size()-1; i>0 && (movementState == EMoveState::CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) { int3 prevCoord = h->convertFromVisitablePos(path.nodes[i].coord); @@ -324,7 +351,7 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject); if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) { - CCS->soundh->stopSound(soundChannel); + stopMovementSound(); destinationTeleport = destTeleportObj->id; destinationTeleportPos = nextCoord; doMovement(h->pos, false); @@ -334,14 +361,10 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co destinationTeleport = ObjectInstanceID(); destinationTeleportPos = int3(-1); } + if(i != path.nodes.size() - 1) - { - soundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } + updateMovementSound(h, prevCoord, nextCoord, path.nodes[i-1].action); + continue; } @@ -351,21 +374,7 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co break; } - { - // Start a new sound for the hero movement or let the existing one carry on. - AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); - - if(newSoundName != soundName) - { - soundName = newSoundName; - - CCS->soundh->stopSound(soundChannel); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); - else - soundChannel = -1; - } - } + updateMovementSound(h, prevCoord, nextCoord, path.nodes[i-1].action); assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); @@ -388,7 +397,7 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co if ((!useTransit && guarded) || LOCPLINT->showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) break; } - CCS->soundh->stopSound(soundChannel); + stopMovementSound(); //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement if (!LOCPLINT->showingDialog->get()) diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index 5837bf511..e62506c1b 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -11,6 +11,7 @@ #include "../lib/constants/EntityIdentifiers.h" #include "../lib/int3.h" +#include "../lib/filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN using TTeleportExitsList = std::vector>; @@ -20,6 +21,7 @@ class CArmedInstance; struct CGPath; struct TryMoveHero; +enum class EPathNodeAction : ui8; VCMI_LIB_NAMESPACE_END class HeroMovementController @@ -41,6 +43,12 @@ class HeroMovementController EMoveState movementState; void setMovementStatus(bool value); + + void updatePath(const CGHeroInstance * hero, const TryMoveHero & details); + + AudioPath getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); + void updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); + void stopMovementSound(); public: HeroMovementController(); From c8e6a7cd27535329b2215d31f9a278ba30bd2672 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 15 Sep 2023 22:18:36 +0300 Subject: [PATCH 0444/1248] Fixed most common cases of movement actions --- AI/EmptyAI/CEmptyAI.cpp | 2 +- AI/EmptyAI/CEmptyAI.h | 2 +- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/AIGateway.h | 2 +- AI/VCAI/VCAI.cpp | 2 +- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 24 +- client/CPlayerInterface.h | 2 +- client/HeroMovementController.cpp | 385 ++++++++---------- client/HeroMovementController.h | 39 +- client/NetPacksClient.cpp | 3 +- client/PlayerLocalState.h | 5 - client/adventureMap/AdventureMapInterface.cpp | 3 +- lib/CGameInterface.h | 2 +- lib/NetPacks.h | 8 +- lib/mapObjects/MiscObjects.cpp | 6 +- lib/pathfinder/CGPathNode.cpp | 18 + lib/pathfinder/CGPathNode.h | 19 + server/queries/MapQueries.cpp | 2 +- 19 files changed, 258 insertions(+), 270 deletions(-) diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 2ae81c102..1263bf61b 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -61,7 +61,7 @@ void CEmptyAI::showBlockingDialog(const std::string &text, const std::vectorselectionMade(0, askID); } -void CEmptyAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { cb->selectionMade(0, askID); } diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 1bc668e9e..2170e1661 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -29,7 +29,7 @@ public: void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; - void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 7b97f3ac7..fa11ffb07 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -654,7 +654,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done - void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; void saveGame(BinarySerializer & h, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 4717c585a..2c1d5c6e9 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -656,7 +656,7 @@ void VCAI::showBlockingDialog(const std::string & text, const std::vector skills, QueryID queryID) override; //TODO void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done - void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; void saveGame(BinarySerializer & h, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 88f30e7cf..f6d0a313a 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -131,7 +131,8 @@ struct HeroObjectRetriever }; CPlayerInterface::CPlayerInterface(PlayerColor Player): - localState(std::make_unique(*this)) + localState(std::make_unique(*this)), + movementController(std::make_unique()) { logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); GH.defActionsDef = 0; @@ -935,6 +936,9 @@ void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &t waitWhileDialog(); //Fix for mantis #98 adventureInt->showInfoBoxMessage(components, text, timer); + // abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on + movementController->movementAbortRequested(); + if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) CCS->soundh->playSound(static_cast(soundID)); return; @@ -980,7 +984,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector { CCS->soundh->playSound(static_cast(soundID)); showingDialog->set(true); - movementController->movementStopRequested(); // interrupt movement to show dialog + movementController->movementAbortRequested(); // interrupt movement to show dialog GH.windows().pushWindow(temp); } else @@ -1003,7 +1007,7 @@ void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList un(*pim); - movementController->movementStopRequested(); + movementController->movementAbortRequested(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); } @@ -1013,7 +1017,7 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); - movementController->movementStopRequested(); + movementController->movementAbortRequested(); CCS->soundh->playSound(static_cast(soundID)); if (!selection && cancel) //simple yes/no dialog @@ -1046,10 +1050,10 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v } } -void CPlayerInterface::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { EVENT_HANDLER_CALLED_BY_CLIENT; - movementController->showTeleportDialog(channel, exits, impassable, askID); + movementController->showTeleportDialog(hero, channel, exits, impassable, askID); } void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) @@ -1163,7 +1167,7 @@ void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) assert(LOCPLINT->makingTurn); assert(h); assert(!showingDialog->get()); - assert(!dialogs.empty()); + assert(dialogs.empty()); LOG_TRACE(logGlobal); if (!LOCPLINT->makingTurn) @@ -1230,16 +1234,10 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con void CPlayerInterface::requestRealized( PackageApplied *pa ) { - EVENT_HANDLER_CALLED_BY_CLIENT; - if(pa->packType == typeList.getTypeID()) movementController->onMoveHeroApplied(); - - if (pa->packType == typeList.getTypeID()) - movementController->onQueryReplyApplied(); } - void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) { heroExchangeStarted(hero1, hero2, QueryID(-1)); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 85e553440..d259f0f45 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -120,7 +120,7 @@ protected: // Call-ins from server, should not be called directly, but only via void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override; diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index f46aecd18..1f72529ad 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -30,45 +30,31 @@ #include "../lib/NetPacks.h" #include "../lib/CondSh.h" -// Code flow of movement process: -// - Player requests movement (e.g. press "move" button) -// -> calls doMoveHero (GUI thread) -// -> -> sends LOCPLINT->cb->moveHero -// - Server sends reply -// -> calls heroMoved (NETWORK thread) -// -> -> animation starts, sound starts -// -> -> waits for animation to finish -// -> -> if player have requested stop and path is not finished, calls doMoveHero again -// -// - Players requests movement stop (e.g. mouse click during movement) -// -> calls movementStopRequested -// -> -> sets flag that movement should be aborted - -HeroMovementController::HeroMovementController() +std::optional HeroMovementController::getLastTile(const CGHeroInstance * hero) const { - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - duringMovement = false; + if (!LOCPLINT->localState->hasPath(hero)) + return std::nullopt; + + return LOCPLINT->localState->getPath(hero).endPos(); } -void HeroMovementController::setMovementStatus(bool value) +std::optional HeroMovementController::getNextTile(const CGHeroInstance * hero) const { - duringMovement = value; - if (value) - CCS->curh->hide(); - else - CCS->curh->show(); + if (!LOCPLINT->localState->hasPath(hero)) + return std::nullopt; + + return LOCPLINT->localState->getPath(hero).nextNode().coord; } bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const { assert(LOCPLINT->localState->hasPath(hero)); - assert(movementState == EMoveState::DURING_MOVE); + assert(duringMovement); if (!LOCPLINT->localState->hasPath(hero)) return false; - if (garrison->visitableAt(*LOCPLINT->localState->getLastTile(hero))) + if (garrison->visitableAt(*getLastTile(hero))) return false; // hero want to enter garrison, not pass through it return true; @@ -79,55 +65,50 @@ bool HeroMovementController::isHeroMoving() const return duringMovement; } -void HeroMovementController::onMoveHeroApplied() -{ - assert(movementState == EMoveState::DURING_MOVE); - - if(movementState == EMoveState::DURING_MOVE) - { - assert(destinationTeleport == ObjectInstanceID::NONE); - movementState = EMoveState::CONTINUE_MOVE; - } -} - -void HeroMovementController::onQueryReplyApplied() -{ - assert(movementState == EMoveState::DURING_MOVE); - - if(movementState == EMoveState::DURING_MOVE) - { - // After teleportation via CGTeleport object is finished - assert(destinationTeleport != ObjectInstanceID::NONE); - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - movementState = EMoveState::CONTINUE_MOVE; - } -} - void HeroMovementController::onPlayerTurnStarted() { - assert(movementState == EMoveState::STOP_MOVE); - movementState = EMoveState::STOP_MOVE; + assert(duringMovement == false); + assert(stoppingMovement == false); + duringMovement = false; } void HeroMovementController::onBattleStarted() { - assert(movementState == EMoveState::STOP_MOVE); // when battle starts, game will send battleStart pack *before* movement confirmation // and since network thread wait for battle intro to play, movement confirmation will only happen after intro // leading to several bugs, such as blocked input during intro - movementState = EMoveState::STOP_MOVE; + // FIXME: should be on hero visit + assert(duringMovement == false); + assert(stoppingMovement == false); + duringMovement = false; } -void HeroMovementController::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { - assert(destinationTeleport != ObjectInstanceID::NONE); + if (!LOCPLINT->localState->hasPath(hero)) + { + assert(exits.size() == 1); // select random? Let server select? + LOCPLINT->cb->selectionMade(0, askID); + return; + } - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - assert(vstd::contains(exits, neededExit)); + const auto & heroPath = LOCPLINT->localState->getPath(hero); + const auto & nextNode = heroPath.nextNode(); - int choosenExit = vstd::find_pos(exits, neededExit); - LOCPLINT->cb->selectionMade(choosenExit, askID); + for (size_t i = 0; i < exits.size(); ++i) + { + const auto * teleporter = LOCPLINT->cb->getObj(exits[i].first); + + if (teleporter && teleporter->visitableAt(nextNode.coord)) + { + LOCPLINT->cb->selectionMade(i, askID); + return; + } + } + + assert(0); // exit not found? How? select random? Let server select? + LOCPLINT->cb->selectionMade(0, askID); + return; } void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMoveHero & details) @@ -135,13 +116,15 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo if (hero->tempOwner != LOCPLINT->playerID) return; + if (!LOCPLINT->localState->hasPath(hero)) + return; // may happen when hero teleports + assert(LOCPLINT->makingTurn); - assert(LOCPLINT->localState->hasPath(hero)); - assert(LOCPLINT->localState->getNextTile(hero).has_value()); + assert(getNextTile(hero).has_value()); - bool directlyAttackingCreature = details.attackedFrom.has_value() && LOCPLINT->localState->getLastTile(hero) == details.attackedFrom; + bool directlyAttackingCreature = details.attackedFrom.has_value() && getLastTile(hero) == details.attackedFrom; - auto desiredTarget = LOCPLINT->localState->getNextTile(hero); + auto desiredTarget = getNextTile(hero); auto actualTarget = hero->convertToVisitablePos(details.end); //don't erase path when revisiting with spacebar @@ -168,6 +151,8 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) { + //assert(duringMovement == true); // May be false if hero teleported + if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) { if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) @@ -185,8 +170,8 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov if(details.stopMovement()) //hero failed to move { - movementState = EMoveState::STOP_MOVE; - adventureInt->onHeroChanged(hero); + if (duringMovement) + endHeroMove(hero); return; } @@ -195,13 +180,6 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov //move finished adventureInt->onHeroChanged(hero); - //check if user cancelled movement - if (GH.input().ignoreEventsUntilInput()) - movementState = EMoveState::STOP_MOVE; - - if (movementState == EMoveState::WAITING_MOVE) - movementState = EMoveState::DURING_MOVE; - // // Hero attacked creature directly, set direction to face it. // if (directlyAttackingCreature) // { @@ -218,12 +196,51 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov // } } -void HeroMovementController::movementStopRequested() +void HeroMovementController::onMoveHeroApplied() { - assert(movementState == EMoveState::DURING_MOVE); + auto const * hero = LOCPLINT->localState->getCurrentHero(); - if (movementState == EMoveState::DURING_MOVE)//if we are in the middle of hero movement - movementState = EMoveState::STOP_MOVE; + assert(hero); + + //check if user cancelled movement + if (GH.input().ignoreEventsUntilInput()) + stoppingMovement = true; + + if (duringMovement) + { + bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0; + bool wantStop = stoppingMovement; + bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); + + if (!canMove) + { + endHeroMove(hero); + } + else if ( wantStop && canStop ) + { + endHeroMove(hero); + } + else + { + moveHeroOnce(hero, LOCPLINT->localState->getPath(hero)); + } + } +} + +void HeroMovementController::movementAbortRequested() +{ + if(duringMovement) + endHeroMove(LOCPLINT->localState->getCurrentHero()); +} + +void HeroMovementController::endHeroMove(const CGHeroInstance * hero) +{ + assert(duringMovement == true); + duringMovement = false; + stoppingMovement = false; + stopMovementSound(); + adventureInt->onHeroChanged(hero); + CCS->curh->show(); } AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) @@ -241,8 +258,8 @@ AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * her if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) return AudioPath::builtin("HORSE10.wav"); - auto prevTile = LOCPLINT->cb->getTile(hero->convertToVisitablePos(posPrev)); - auto nextTile = LOCPLINT->cb->getTile(hero->convertToVisitablePos(posNext)); + auto prevTile = LOCPLINT->cb->getTile(posPrev); + auto nextTile = LOCPLINT->cb->getTile(posNext); auto prevRoad = prevTile->roadType; auto nextRoad = nextTile->roadType; @@ -254,155 +271,93 @@ AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * her return nextTile->terType->horseSoundPenalty; }; -void HeroMovementController::updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) +void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3 posPrev, int3 nextCoord, EPathNodeAction moveType) { - // Start a new sound for the hero movement or let the existing one carry on. - AudioPath newSoundName = getMovementSoundFor(h, prevCoord, nextCoord, path.nodes[i-1].action); + AudioPath newSoundName = getMovementSoundFor(h, posPrev, nextCoord, moveType); - if(newSoundName != soundName) + if(newSoundName != currentMovementSoundName) { - soundName = newSoundName; + currentMovementSoundName = newSoundName; - CCS->soundh->stopSound(soundChannel); - if (!soundName.empty()) - soundChannel = CCS->soundh->playSound(soundName, -1); + if (currentMovementSoundChannel != -1) + CCS->soundh->stopSound(currentMovementSoundChannel); + + if (!currentMovementSoundName.empty()) + currentMovementSoundChannel = CCS->soundh->playSound(currentMovementSoundName, -1, true); else - soundChannel = -1; + currentMovementSoundChannel = -1; } - - - int soundChannel = -1; - AudioPath soundName; } void HeroMovementController::stopMovementSound() { - CCS->soundh->stopSound(soundChannel); + CCS->soundh->stopSound(currentMovementSoundChannel); + currentMovementSoundChannel = -1; + currentMovementSoundName = AudioPath(); +} + +bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const +{ + if (node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL) + return false; + + if (node.accessible != EPathAccessibility::ACCESSIBLE) + return false; + + return true; } void HeroMovementController::movementStartRequested(const CGHeroInstance * h, const CGPath & path) { - setMovementStatus(true); + assert(duringMovement == false); + duringMovement = true; - auto getObj = [&](int3 coord, bool ignoreHero) - { - return LOCPLINT->cb->getTile(h->convertToVisitablePos(coord))->topVisitableObj(ignoreHero); - }; - - auto isTeleportAction = [&](EPathNodeAction action) -> bool - { - if (action != EPathNodeAction::TELEPORT_NORMAL && - action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && - action != EPathNodeAction::TELEPORT_BATTLE) - { - return false; - } - - return true; - }; - - auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * - { - if (CGTeleport::isConnected(currentObject, nextObjectTop)) - return nextObjectTop; - - if (nextObjectTop && nextObjectTop->ID == Obj::HERO && CGTeleport::isConnected(currentObject, nextObject)) - { - return nextObject; - } - - return nullptr; - }; - - auto doMovement = [&](int3 dst, bool transit) - { - movementState = EMoveState::WAITING_MOVE; - LOCPLINT->cb->moveHero(h, dst, transit); - while(movementState != EMoveState::STOP_MOVE && movementState != EMoveState::CONTINUE_MOVE) - { - assert(0); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); - } - }; - - auto canStop = [&](const CGPathNode * node) -> bool - { - if (node->layer != EPathfindingLayer::LAND && node->layer != EPathfindingLayer::SAIL) - return false; - - if (node->accessible != EPathAccessibility::ACCESSIBLE) - return false; - - return true; - }; - - int i = 1; - movementState = EMoveState::CONTINUE_MOVE; - - for (i=(int)path.nodes.size()-1; i>0 && (movementState == EMoveState::CONTINUE_MOVE || !canStop(&path.nodes[i])); i--) - { - int3 prevCoord = h->convertFromVisitablePos(path.nodes[i].coord); - int3 nextCoord = h->convertFromVisitablePos(path.nodes[i-1].coord); - - auto prevObject = getObj(prevCoord, prevCoord == h->pos); - auto nextObjectTop = getObj(nextCoord, false); - auto nextObject = getObj(nextCoord, true); - auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject); - if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr) - { - stopMovementSound(); - destinationTeleport = destTeleportObj->id; - destinationTeleportPos = nextCoord; - doMovement(h->pos, false); - if (path.nodes[i-1].action == EPathNodeAction::TELEPORT_BLOCKING_VISIT - || path.nodes[i-1].action == EPathNodeAction::TELEPORT_BATTLE) - { - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - } - - if(i != path.nodes.size() - 1) - updateMovementSound(h, prevCoord, nextCoord, path.nodes[i-1].action); - - continue; - } - - if (path.nodes[i-1].turns) - { //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) - movementState = EMoveState::STOP_MOVE; - break; - } - - updateMovementSound(h, prevCoord, nextCoord, path.nodes[i-1].action); - - assert(h->pos.z == nextCoord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all - int3 endpos(nextCoord.x, nextCoord.y, h->pos.z); - logGlobal->trace("Requesting hero movement to %s", endpos.toString()); - - bool useTransit = false; - if ((i-2 >= 0) // Check there is node after next one; otherwise transit is pointless - && (CGTeleport::isConnected(nextObjectTop, getObj(h->convertFromVisitablePos(path.nodes[i-2].coord), false)) - || CGTeleport::isTeleport(nextObjectTop))) - { // Hero should be able to go through object if it's allow transit - useTransit = true; - } - else if (path.nodes[i-1].layer == EPathfindingLayer::AIR) - useTransit = true; - - doMovement(endpos, useTransit); - - logGlobal->trace("Resuming %s", __FUNCTION__); - bool guarded = LOCPLINT->cb->isInTheMap(LOCPLINT->cb->getGuardingCreaturePosition(endpos - int3(1, 0, 0))); - if ((!useTransit && guarded) || LOCPLINT->showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) - break; - } - stopMovementSound(); - - //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement - if (!LOCPLINT->showingDialog->get()) - GH.fakeMouseMove(); - - CGI->mh->waitForOngoingAnimations(); - setMovementStatus(false); + CCS->curh->show(); + moveHeroOnce(h, path); +} + +void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath & path) +{ + assert(duringMovement == true); + + const auto & currNode = path.currNode(); + const auto & nextNode = path.nextNode(); + + assert(nextNode.turns == 0); + assert(currNode.coord == h->visitablePos()); + + int3 nextCoord = h->convertFromVisitablePos(nextNode.coord); + + if (nextNode.isTeleportAction()) + { + stopMovementSound(); + LOCPLINT->cb->moveHero(h, nextCoord, false); + return; + } + else + { + updateMovementSound(h, currNode.coord, nextNode.coord, nextNode.action); + + assert(h->pos.z == nextNode.coord.z); // Z should change only if it's movement via teleporter and in this case this code shouldn't be executed at all + + logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString()); + + bool useTransit = nextNode.layer == EPathfindingLayer::AIR; + LOCPLINT->cb->moveHero(h, nextCoord, useTransit); + return; + } + +// bool guarded = LOCPLINT->cb->isInTheMap(LOCPLINT->cb->getGuardingCreaturePosition(nextCoord - int3(1, 0, 0))); +// if ((!useTransit && guarded) || LOCPLINT->showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) +// break; + +// stopMovementSound(); +// +// //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement +// if (!LOCPLINT->showingDialog->get()) +// GH.fakeMouseMove(); +// +// CGI->mh->waitForOngoingAnimations(); +// setMovementStatus(false); } diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index e62506c1b..e83311c97 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -18,6 +18,7 @@ using TTeleportExitsList = std::vector>; class CGHeroInstance; class CArmedInstance; +struct CGPathNode; struct CGPath; struct TryMoveHero; @@ -26,45 +27,45 @@ VCMI_LIB_NAMESPACE_END class HeroMovementController { - ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation - int3 destinationTeleportPos; + /// there is an ongoing movement loop, in one or another stage + bool duringMovement = false; + /// movement was requested to be terminated, e.g. by player or due to inability to move + bool stoppingMovement = false; - bool duringMovement; + AudioPath currentMovementSoundName; + int currentMovementSoundChannel = -1; + /// return final node in a path, if exists + std::optional getLastTile(const CGHeroInstance * h) const; + /// return first path in a path, if exists + std::optional getNextTile(const CGHeroInstance * h) const; - enum class EMoveState - { - STOP_MOVE, - WAITING_MOVE, - CONTINUE_MOVE, - DURING_MOVE - }; - - EMoveState movementState; - - void setMovementStatus(bool value); + bool canHeroStopAtNode(const CGPathNode & node) const; void updatePath(const CGHeroInstance * hero, const TryMoveHero & details); + /// Moves hero 1 tile / path node + void moveHeroOnce(const CGHeroInstance * h, const CGPath & path); + + void endHeroMove(const CGHeroInstance * h); + AudioPath getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); void updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); void stopMovementSound(); -public: - HeroMovementController(); +public: // const queries bool isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const; bool isHeroMoving() const; // netpack handlers void onMoveHeroApplied(); - void onQueryReplyApplied(); void onPlayerTurnStarted(); void onBattleStarted(); - void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); void heroMoved(const CGHeroInstance * hero, const TryMoveHero & details); // UI handlers void movementStartRequested(const CGHeroInstance * h, const CGPath & path); - void movementStopRequested(); + void movementAbortRequested(); }; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 6df37a4f0..de1960804 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -703,7 +703,8 @@ void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack) void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack) { - callOnlyThatInterface(cl, pack.player, &CGameInterface::showTeleportDialog, pack.channel, pack.exits, pack.impassable, pack.queryID); + const CGHeroInstance *h = cl.getHero(pack.hero); + callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID); } void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack) diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index 31c919e57..e88ee6ca3 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -76,11 +76,6 @@ public: void setPath(const CGHeroInstance * h, const CGPath & path); bool setPath(const CGHeroInstance * h, const int3 & destination); - /// return final node in a path, if exists - std::optional getLastTile(const CGHeroInstance * h) const; - /// return first path in a path, if exists - std::optional getNextTile(const CGHeroInstance * h) const; - const CGPath & getPath(const CGHeroInstance * h) const; bool hasPath(const CGHeroInstance * h) const; diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index fc8288b0a..3cf2209fd 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -531,7 +531,8 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) if(LOCPLINT->localState->hasPath(currentHero) && LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving { - if(!CGI->mh->hasOngoingAnimations()) + assert(!CGI->mh->hasOngoingAnimations()); + if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0) LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); return; } diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index b0b9e6ce4..cb849755a 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -103,7 +103,7 @@ public: // all stacks operations between these objects become allowed, interface has to call onEnd when done virtual void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) = 0; - virtual void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; + virtual void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; virtual void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) = 0; virtual void finish(){}; //if for some reason we want to end diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 3fa84933f..caa42bdb3 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1436,12 +1436,12 @@ struct DLL_LINKAGE TeleportDialog : public Query { TeleportDialog() = default; - TeleportDialog(const PlayerColor & Player, const TeleportChannelID & Channel) - : player(Player) + TeleportDialog(const ObjectInstanceID & hero, const TeleportChannelID & Channel) + : hero(hero) , channel(Channel) { } - PlayerColor player; + ObjectInstanceID hero; TeleportChannelID channel; TTeleportExitsList exits; bool impassable = false; @@ -1451,7 +1451,7 @@ struct DLL_LINKAGE TeleportDialog : public Query template void serialize(Handler & h, const int version) { h & queryID; - h & player; + h & hero; h & channel; h & exits; h & impassable; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 24c32a4e4..40767d08c 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -453,7 +453,7 @@ TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, int Su void CGMonolith::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(isEntrance()) { if(cb->isTeleportChannelBidirectional(channel) && 1 < cb->getTeleportChannelExits(channel).size()) @@ -527,7 +527,7 @@ void CGMonolith::initObj(CRandomGenerator & rand) void CGSubterraneanGate::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(cb->isTeleportChannelImpassable(channel)) { h->showInfoDialog(153);//Just inside the entrance you find a large pile of rubble blocking the tunnel. You leave discouraged. @@ -611,7 +611,7 @@ void CGSubterraneanGate::postInit() //matches subterranean gates into pairs void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const { - TeleportDialog td(h->tempOwner, channel); + TeleportDialog td(h->id, channel); if(cb->isTeleportChannelImpassable(channel)) { logGlobal->debug("Cannot find exit whirlpool for %d at %s", id.getNum(), pos.toString()); diff --git a/lib/pathfinder/CGPathNode.cpp b/lib/pathfinder/CGPathNode.cpp index cbcd949d5..057161295 100644 --- a/lib/pathfinder/CGPathNode.cpp +++ b/lib/pathfinder/CGPathNode.cpp @@ -24,6 +24,24 @@ static bool canSeeObj(const CGObjectInstance * obj) return obj != nullptr && obj->ID != Obj::EVENT; } +const CGPathNode & CGPath::currNode() const +{ + assert(nodes.size() > 1); + return nodes[nodes.size()-1]; +} + +const CGPathNode & CGPath::nextNode() const +{ + assert(nodes.size() > 1); + return nodes[nodes.size()-2]; +} + +const CGPathNode & CGPath::lastNode() const +{ + assert(nodes.size() > 1); + return nodes[0]; +} + int3 CGPath::startPos() const { return nodes[nodes.size()-1].coord; diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index db570be15..ad6e90383 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -143,6 +143,18 @@ struct DLL_LINKAGE CGPathNode return turns < 255; } + bool isTeleportAction() const + { + if (action != EPathNodeAction::TELEPORT_NORMAL && + action != EPathNodeAction::TELEPORT_BLOCKING_VISIT && + action != EPathNodeAction::TELEPORT_BATTLE) + { + return false; + } + + return true; + } + using TFibHeap = boost::heap::fibonacci_heap>>; TFibHeap::handle_type pqHandle; @@ -156,6 +168,13 @@ struct DLL_LINKAGE CGPath { std::vector nodes; //just get node by node + /// Starting position of path, matches location of hero + const CGPathNode & currNode() const; + /// First node in path, this is where hero will move next + const CGPathNode & nextNode() const; + /// Last node in path, this is what hero wants to reach in the end + const CGPathNode & lastNode() const; + int3 startPos() const; // start point int3 endPos() const; //destination point }; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 900c36cdd..fc4f1197a 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -171,7 +171,7 @@ CTeleportDialogQuery::CTeleportDialogQuery(CGameHandler * owner, const TeleportD CDialogQuery(owner) { this->td = td; - addPlayer(td.player); + addPlayer(gh->getHero(td.hero)->getOwner()); } CHeroLevelUpDialogQuery::CHeroLevelUpDialogQuery(CGameHandler * owner, const HeroLevelUp & Hlu, const CGHeroInstance * Hero): From 2355aab139acb090033aefdba0d58c09df6f27ac Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 15 Sep 2023 22:45:55 +0300 Subject: [PATCH 0445/1248] Correctly end movement on battle start --- client/HeroMovementController.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 1f72529ad..df6dcb65a 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -77,10 +77,7 @@ void HeroMovementController::onBattleStarted() // when battle starts, game will send battleStart pack *before* movement confirmation // and since network thread wait for battle intro to play, movement confirmation will only happen after intro // leading to several bugs, such as blocked input during intro - // FIXME: should be on hero visit - assert(duringMovement == false); - assert(stoppingMovement == false); - duringMovement = false; + movementAbortRequested(); } void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) @@ -343,7 +340,7 @@ void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath logGlobal->trace("Requesting hero movement to %s", nextNode.coord.toString()); - bool useTransit = nextNode.layer == EPathfindingLayer::AIR; + bool useTransit = nextNode.layer == EPathfindingLayer::AIR || nextNode.layer == EPathfindingLayer::WATER; LOCPLINT->cb->moveHero(h, nextCoord, useTransit); return; } From f08028a1582a9609a32d2cf0861dc2fbc36af01e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 16 Sep 2023 13:40:49 +0300 Subject: [PATCH 0446/1248] Fixed moving through garrisons & teleporters --- client/HeroMovementController.cpp | 42 ++++++++++++------------------- client/HeroMovementController.h | 1 + 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index df6dcb65a..3e52348c0 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -48,8 +48,8 @@ std::optional HeroMovementController::getNextTile(const CGHeroInstance * h bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const { - assert(LOCPLINT->localState->hasPath(hero)); - assert(duringMovement); + if(!duringMovement) + return false; if (!LOCPLINT->localState->hasPath(hero)) return false; @@ -70,6 +70,7 @@ void HeroMovementController::onPlayerTurnStarted() assert(duringMovement == false); assert(stoppingMovement == false); duringMovement = false; + currentlyMovingHero = nullptr; } void HeroMovementController::onBattleStarted() @@ -82,10 +83,12 @@ void HeroMovementController::onBattleStarted() void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { + assert(hero == currentlyMovingHero); + if (!LOCPLINT->localState->hasPath(hero)) { - assert(exits.size() == 1); // select random? Let server select? - LOCPLINT->cb->selectionMade(0, askID); + // Hero enters teleporter without specifying exit - select it randomly + LOCPLINT->cb->selectionMade(-1, askID); return; } @@ -103,8 +106,8 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel } } - assert(0); // exit not found? How? select random? Let server select? - LOCPLINT->cb->selectionMade(0, askID); + assert(0); // exit not found? How? + LOCPLINT->cb->selectionMade(-1, askID); return; } @@ -116,6 +119,7 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo if (!LOCPLINT->localState->hasPath(hero)) return; // may happen when hero teleports + assert(hero == currentlyMovingHero); assert(LOCPLINT->makingTurn); assert(getNextTile(hero).has_value()); @@ -148,8 +152,6 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) { - //assert(duringMovement == true); // May be false if hero teleported - if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) { if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) @@ -195,16 +197,15 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov void HeroMovementController::onMoveHeroApplied() { - auto const * hero = LOCPLINT->localState->getCurrentHero(); - - assert(hero); - //check if user cancelled movement if (GH.input().ignoreEventsUntilInput()) stoppingMovement = true; if (duringMovement) { + assert(currentlyMovingHero); + auto const * hero = currentlyMovingHero; + bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0; bool wantStop = stoppingMovement; bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); @@ -227,7 +228,7 @@ void HeroMovementController::onMoveHeroApplied() void HeroMovementController::movementAbortRequested() { if(duringMovement) - endHeroMove(LOCPLINT->localState->getCurrentHero()); + endHeroMove(currentlyMovingHero); } void HeroMovementController::endHeroMove(const CGHeroInstance * hero) @@ -235,6 +236,7 @@ void HeroMovementController::endHeroMove(const CGHeroInstance * hero) assert(duringMovement == true); duringMovement = false; stoppingMovement = false; + currentlyMovingHero = nullptr; stopMovementSound(); adventureInt->onHeroChanged(hero); CCS->curh->show(); @@ -309,6 +311,7 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co { assert(duringMovement == false); duringMovement = true; + currentlyMovingHero = h; CCS->curh->show(); moveHeroOnce(h, path); @@ -344,17 +347,4 @@ void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath LOCPLINT->cb->moveHero(h, nextCoord, useTransit); return; } - -// bool guarded = LOCPLINT->cb->isInTheMap(LOCPLINT->cb->getGuardingCreaturePosition(nextCoord - int3(1, 0, 0))); -// if ((!useTransit && guarded) || LOCPLINT->showingDialog->get() == true) // Abort movement if a guard was fought or there is a dialog to display (Mantis #1136) -// break; - -// stopMovementSound(); -// -// //Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement -// if (!LOCPLINT->showingDialog->get()) -// GH.fakeMouseMove(); -// -// CGI->mh->waitForOngoingAnimations(); -// setMovementStatus(false); } diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index e83311c97..4f5695085 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -32,6 +32,7 @@ class HeroMovementController /// movement was requested to be terminated, e.g. by player or due to inability to move bool stoppingMovement = false; + const CGHeroInstance * currentlyMovingHero = nullptr; AudioPath currentMovementSoundName; int currentMovementSoundChannel = -1; From b7de6854836373b1f77e24e94c1a933e782f3d34 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 16 Sep 2023 20:07:02 +0300 Subject: [PATCH 0447/1248] Fixed paths through teleport, formatting cleanup --- client/CPlayerInterface.cpp | 3 + client/HeroMovementController.cpp | 198 +++++++++++++++++------------- client/HeroMovementController.h | 14 ++- server/CGameHandler.cpp | 47 ++++--- 4 files changed, 151 insertions(+), 111 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index f6d0a313a..bcdbcf989 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1236,6 +1236,9 @@ void CPlayerInterface::requestRealized( PackageApplied *pa ) { if(pa->packType == typeList.getTypeID()) movementController->onMoveHeroApplied(); + + if(pa->packType == typeList.getTypeID()) + movementController->onQueryReplyApplied(); } void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 3e52348c0..c4588bc09 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -1,5 +1,5 @@ /* - * CPlayerInterface.cpp, part of VCMI engine + * HeroMovementController.cpp, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -24,37 +24,19 @@ #include "../lib/pathfinder/CGPathNode.h" #include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/MiscObjects.h" #include "../lib/RoadHandler.h" #include "../lib/TerrainHandler.h" #include "../lib/NetPacks.h" -#include "../lib/CondSh.h" - -std::optional HeroMovementController::getLastTile(const CGHeroInstance * hero) const -{ - if (!LOCPLINT->localState->hasPath(hero)) - return std::nullopt; - - return LOCPLINT->localState->getPath(hero).endPos(); -} - -std::optional HeroMovementController::getNextTile(const CGHeroInstance * hero) const -{ - if (!LOCPLINT->localState->hasPath(hero)) - return std::nullopt; - - return LOCPLINT->localState->getPath(hero).nextNode().coord; -} bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const { if(!duringMovement) return false; - if (!LOCPLINT->localState->hasPath(hero)) + if(!LOCPLINT->localState->hasPath(hero)) return false; - if (garrison->visitableAt(*getLastTile(hero))) + if(garrison->visitableAt(LOCPLINT->localState->getPath(hero).lastNode().coord)) return false; // hero want to enter garrison, not pass through it return true; @@ -83,9 +65,13 @@ void HeroMovementController::onBattleStarted() void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { - assert(hero == currentlyMovingHero); + // Player entered teleporter + // Check whether hero that has entered teleporter has paths that goes through teleporter and select appropriate exit + // othervice, ask server to select one randomly by sending invalid (-1) value as answer + assert(waitingForQueryApplyReply == false); + waitingForQueryApplyReply = true; - if (!LOCPLINT->localState->hasPath(hero)) + if(!LOCPLINT->localState->hasPath(hero)) { // Hero enters teleporter without specifying exit - select it randomly LOCPLINT->cb->selectionMade(-1, askID); @@ -95,12 +81,14 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel const auto & heroPath = LOCPLINT->localState->getPath(hero); const auto & nextNode = heroPath.nextNode(); - for (size_t i = 0; i < exits.size(); ++i) + for(size_t i = 0; i < exits.size(); ++i) { const auto * teleporter = LOCPLINT->cb->getObj(exits[i].first); - if (teleporter && teleporter->visitableAt(nextNode.coord)) + if(teleporter && teleporter->visitableAt(nextNode.coord)) { + // Remove this node from path - it will be covered by teleportation + //LOCPLINT->localState->removeLastNode(hero); LOCPLINT->cb->selectionMade(i, askID); return; } @@ -113,27 +101,28 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMoveHero & details) { - if (hero->tempOwner != LOCPLINT->playerID) + // Once hero moved (or attempted to move) we need to update path + // to make sure that it is still valid or remove it completely if destination has been reached + + if(hero->tempOwner != LOCPLINT->playerID) return; - if (!LOCPLINT->localState->hasPath(hero)) + if(!LOCPLINT->localState->hasPath(hero)) return; // may happen when hero teleports - assert(hero == currentlyMovingHero); assert(LOCPLINT->makingTurn); - assert(getNextTile(hero).has_value()); - bool directlyAttackingCreature = details.attackedFrom.has_value() && getLastTile(hero) == details.attackedFrom; + bool directlyAttackingCreature = details.attackedFrom.has_value() && LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom; - auto desiredTarget = getNextTile(hero); - auto actualTarget = hero->convertToVisitablePos(details.end); + int3 desiredTarget = LOCPLINT->localState->getPath(hero).nextNode().coord; + int3 actualTarget = hero->convertToVisitablePos(details.end); //don't erase path when revisiting with spacebar bool heroChangedTile = details.start != details.end; - if (desiredTarget && heroChangedTile) + if(heroChangedTile) { - if (*desiredTarget != actualTarget) + if(desiredTarget != actualTarget) { //invalidate path - movement was not along current path //possible reasons: teleport, visit of object with "blocking visit" property @@ -152,12 +141,20 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) { - if (details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) + // Server initiated movement -> start movement animation + // Note that this movement is not necessarily of owned heroes - other players movement will also pass through this method + + if(details.result == TryMoveHero::EMBARK || details.result == TryMoveHero::DISEMBARK) { if(hero->getRemovalSound() && hero->tempOwner == LOCPLINT->playerID) CCS->soundh->playSound(hero->getRemovalSound().value()); } + bool directlyAttackingCreature = + details.attackedFrom.has_value() && + LOCPLINT->localState->hasPath(hero) && + LOCPLINT->localState->getPath(hero).lastNode().coord == details.attackedFrom; + std::unordered_set changedTiles { hero->convertToVisitablePos(details.start), hero->convertToVisitablePos(details.end) @@ -167,61 +164,86 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov updatePath(hero, details); - if(details.stopMovement()) //hero failed to move + if(details.stopMovement()) { - if (duringMovement) + if(duringMovement) endHeroMove(hero); return; } + // We are in network thread + // Block netpack processing until movement animation is over CGI->mh->waitForOngoingAnimations(); //move finished adventureInt->onHeroChanged(hero); -// // Hero attacked creature directly, set direction to face it. -// if (directlyAttackingCreature) -// { -// // Get direction to attacker. -// int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); -// static const ui8 dirLookup[3][3] = -// { -// { 1, 2, 3 }, -// { 8, 0, 4 }, -// { 7, 6, 5 } -// }; -// // FIXME: Avoid const_cast, make moveDir mutable in some other way? -// const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; -// } + // Hero attacked creature, set direction to face it. + if(directlyAttackingCreature) + { + // Get direction to attacker. + int3 posOffset = *details.attackedFrom - details.end + int3(2, 1, 0); + static const ui8 dirLookup[3][3] = + { + { 1, 2, 3 }, + { 8, 0, 4 }, + { 7, 6, 5 } + }; + + //FIXME: better handling of this case without const_cast + const_cast(hero)->moveDir = dirLookup[posOffset.y][posOffset.x]; + } +} + +void HeroMovementController::onQueryReplyApplied() +{ + if(duringMovement) + { + // Server accepted our TeleportDialog query reply and moved hero + // Continue moving alongside our path, if any + + assert(waitingForQueryApplyReply); + waitingForQueryApplyReply = false; + onMoveHeroApplied(); + } } void HeroMovementController::onMoveHeroApplied() { - //check if user cancelled movement - if (GH.input().ignoreEventsUntilInput()) + // at this point, server have finished processing of hero movement request + // as well as all side effectes from movement, such as object visit or combat start + + // this was request to move alongside path from player, but either another player or teleport action + if(!duringMovement) + return; + + // hero has moved onto teleporter and activated it + // in this case next movement should be done only after query reply has been acknowledged + // and hero has been moved to teleport destination + if(waitingForQueryApplyReply) + return; + + if(GH.input().ignoreEventsUntilInput()) stoppingMovement = true; - if (duringMovement) + assert(currentlyMovingHero); + const auto * hero = currentlyMovingHero; + + bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0; + bool wantStop = stoppingMovement; + bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); + + if(!canMove) { - assert(currentlyMovingHero); - auto const * hero = currentlyMovingHero; - - bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0; - bool wantStop = stoppingMovement; - bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); - - if (!canMove) - { - endHeroMove(hero); - } - else if ( wantStop && canStop ) - { - endHeroMove(hero); - } - else - { - moveHeroOnce(hero, LOCPLINT->localState->getPath(hero)); - } + endHeroMove(hero); + } + else if(wantStop && canStop) + { + endHeroMove(hero); + } + else + { + moveHeroOnce(hero, LOCPLINT->localState->getPath(hero)); } } @@ -234,6 +256,7 @@ void HeroMovementController::movementAbortRequested() void HeroMovementController::endHeroMove(const CGHeroInstance * hero) { assert(duringMovement == true); + assert(currentlyMovingHero != nullptr); duringMovement = false; stoppingMovement = false; currentlyMovingHero = nullptr; @@ -244,17 +267,17 @@ void HeroMovementController::endHeroMove(const CGHeroInstance * hero) AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType) { - if (moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) + if(moveType == EPathNodeAction::TELEPORT_BATTLE || moveType == EPathNodeAction::TELEPORT_BLOCKING_VISIT || moveType == EPathNodeAction::TELEPORT_NORMAL) return {}; - if (moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) + if(moveType == EPathNodeAction::EMBARK || moveType == EPathNodeAction::DISEMBARK) return {}; - if (moveType == EPathNodeAction::BLOCKING_VISIT) + if(moveType == EPathNodeAction::BLOCKING_VISIT) return {}; // flying movement sound - if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) + if(hero->hasBonusOfType(BonusType::FLYING_MOVEMENT)) return AudioPath::builtin("HORSE10.wav"); auto prevTile = LOCPLINT->cb->getTile(posPrev); @@ -264,7 +287,7 @@ AudioPath HeroMovementController::getMovementSoundFor(const CGHeroInstance * her auto nextRoad = nextTile->roadType; bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD; - if (movingOnRoad) + if(movingOnRoad) return nextTile->terType->horseSound; else return nextTile->terType->horseSoundPenalty; @@ -279,10 +302,10 @@ void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3 { currentMovementSoundName = newSoundName; - if (currentMovementSoundChannel != -1) + if(currentMovementSoundChannel != -1) CCS->soundh->stopSound(currentMovementSoundChannel); - if (!currentMovementSoundName.empty()) + if(!currentMovementSoundName.empty()) currentMovementSoundChannel = CCS->soundh->playSound(currentMovementSoundName, -1, true); else currentMovementSoundChannel = -1; @@ -291,17 +314,18 @@ void HeroMovementController::updateMovementSound(const CGHeroInstance * h, int3 void HeroMovementController::stopMovementSound() { - CCS->soundh->stopSound(currentMovementSoundChannel); + if(currentMovementSoundChannel != -1) + CCS->soundh->stopSound(currentMovementSoundChannel); currentMovementSoundChannel = -1; currentMovementSoundName = AudioPath(); } bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const { - if (node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL) + if(node.layer != EPathfindingLayer::LAND && node.layer != EPathfindingLayer::SAIL) return false; - if (node.accessible != EPathAccessibility::ACCESSIBLE) + if(node.accessible != EPathAccessibility::ACCESSIBLE) return false; return true; @@ -313,12 +337,15 @@ void HeroMovementController::movementStartRequested(const CGHeroInstance * h, co duringMovement = true; currentlyMovingHero = h; - CCS->curh->show(); + CCS->curh->hide(); moveHeroOnce(h, path); } void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath & path) { + // Moves hero once, sends request to server and immediately returns + // movement alongside paths will be done on receiving response from server + assert(duringMovement == true); const auto & currNode = path.currNode(); @@ -329,9 +356,10 @@ void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath int3 nextCoord = h->convertFromVisitablePos(nextNode.coord); - if (nextNode.isTeleportAction()) + if(nextNode.isTeleportAction()) { stopMovementSound(); + logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString()); LOCPLINT->cb->moveHero(h, nextCoord, false); return; } diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index 4f5695085..544b2a533 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -1,5 +1,5 @@ /* - * CPlayerInterface.h, part of VCMI engine + * HeroMovementController.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -32,15 +32,12 @@ class HeroMovementController /// movement was requested to be terminated, e.g. by player or due to inability to move bool stoppingMovement = false; + bool waitingForQueryApplyReply = false; + const CGHeroInstance * currentlyMovingHero = nullptr; AudioPath currentMovementSoundName; int currentMovementSoundChannel = -1; - /// return final node in a path, if exists - std::optional getLastTile(const CGHeroInstance * h) const; - /// return first path in a path, if exists - std::optional getNextTile(const CGHeroInstance * h) const; - bool canHeroStopAtNode(const CGPathNode & node) const; void updatePath(const CGHeroInstance * hero, const TryMoveHero & details); @@ -56,11 +53,16 @@ class HeroMovementController public: // const queries + + /// Returns true if hero should move through garrison without displaying garrison dialog bool isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const; + + /// Returns true if there is an ongoing hero movement process bool isHeroMoving() const; // netpack handlers void onMoveHeroApplied(); + void onQueryReplyApplied(); void onPlayerTurnStarted(); void onBattleStarted(); void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 960b8031d..a4741258f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1111,29 +1111,36 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const bool standAtObstacle = t.blocked && !t.visitable; const bool standAtWater = !h->boat && t.terType->isWater() && (t.visitableObjects.empty() || !t.visitableObjects.back()->isCoastVisitable()); - //it's a rock or blocked and not visitable tile - //OR hero is on land and dest is water and (there is not present only one object - boat) - if (((!t.terType->isPassable() || (standAtObstacle && !canFly)) - && complain("Cannot move hero, destination tile is blocked!")) - || ((standAtWater && !canFly && !canWalkOnSea) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) - && complain("Cannot move hero, destination tile is on water!")) - || ((h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) - && complain("Cannot disembark hero, tile is blocked!")) - || ((distance(h->pos, dst) >= 1.5 && !teleporting) - && complain("Tiles are not neighboring!")) - || ((h->inTownGarrison) - && complain("Can not move garrisoned hero!")) - || (((int)h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) - && complain("Hero doesn't have any movement points left!")) - || ((transit && !canFly && !CGTeleport::isTeleport(t.topVisitableObj())) - && complain("Hero cannot transit over this tile!")) - /*|| (states.checkFlag(h->tempOwner, &PlayerStatus::engagedIntoBattle) - && complain("Cannot move hero during the battle"))*/) - { + auto const complainRet = [&](const std::string & message){ //send info about movement failure + complain(message); sendAndApply(&tmh); return false; - } + }; + + //it's a rock or blocked and not visitable tile + //OR hero is on land and dest is water and (there is not present only one object - boat) + if (!t.terType->isPassable() || (standAtObstacle && !canFly)) + complainRet("Cannot move hero, destination tile is blocked!"); + + //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) + if(standAtWater && !canFly && !canWalkOnSea) + complainRet("Cannot move hero, destination tile is on water!"); + + if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) + complainRet("Cannot disembark hero, tile is blocked!"); + + if(distance(h->pos, dst) >= 1.5 && !teleporting) + complainRet("Tiles are not neighboring!"); + + if(h->inTownGarrison) + complainRet("Can not move garrisoned hero!"); + + if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) + complainRet("Hero doesn't have any movement points left!"); + + if (transit && !canFly && !CGTeleport::isTeleport(t.topVisitableObj())) + complainRet("Hero cannot transit over this tile!"); //several generic blocks of code From 45840a952257fb81f54d214691d2c49df79c78a0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 14 Sep 2023 16:11:59 +0300 Subject: [PATCH 0448/1248] Updated release process docs to represent recent releases --- docs/maintainers/Release_Process.md | 76 +++++++++++++++++------------ 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/docs/maintainers/Release_Process.md b/docs/maintainers/Release_Process.md index 2e23e41b7..d9b27a7fe 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,48 +1,64 @@ < [Documentation](../Readme.md) / Release Process -## Branches +# Branches Our branching strategy is very similar to GitFlow -* `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `maj.min.patch` -* `beta` branch is for stabilization of ongoing release. We don't merge new features into `beta` branch - only bug fixes are acceptable. Hotfixes for already released software should be delivered to this branch as well. -* `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one step ahead of 'beta` +- `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `maj.min.patch` +- `beta` branch is for stabilization of ongoing release. Only safe changes should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta. +- `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta` -## Release process step-by-step -Assuming that all features planned to be released are implemented in `develop` branch and next release version will be `1.x.0` +# Release process step-by-step -### Start of stabilization stage -Should be done several weeks before planned release date +### Initial release setup (major releases only) +Should be done immediately after start of stabilization stage for previous release + +- Create project named `Release 1.X` +- Add all features and bugs that should be fixed as part of this release into this project + +### Start of stabilization stage (major releases only) +Should be done 2-4 weeks before planned release date. All major features should be finished at this point. -- Create [milestone](https://github.com/vcmi/vcmi/milestones) named `Release 1.x` -- Specify new milestone for all regression bugs and major bugs related to new functionality - Create `beta` branch from `develop` -- Bump vcmi version on `develop` branch -- Bump version for linux on `develop` branch +- Bump vcmi version in CMake on `develop` branch +- Bump version for Linux on `develop` branch +- Bump version and build ID for Android on `develop` branch + +### Initial release setup (patch releases only) + +- Bump vcmi version in CMake on `beta` branch +- Bump version for Linux on `beta` branch +- Bump version and build ID for Android on `beta` branch ### Release preparation stage -Should be done several days before planned release date +Should be done 1 week before release. Release date should be decided at this point. -- Make sure to announce codebase freeze deadline to all developers -- Update [release notes](https://github.com/vcmi/vcmi/blob/develop/ChangeLog.md) -- Update release date for Linux packaging. See [example](https://github.com/vcmi/vcmi/pull/1258) -- Update release date for Android packaging. See [example](https://github.com/vcmi/vcmi/pull/2090) -- Create draft release page, specify `1.x.0` as tag for `master` after publishing -- Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu +- Make sure to announce codebase freeze deadline (1 day before release) to all developers +- Create pull request for release preparation tasks targeting `beta`: +- - Update [changelog](https://github.com/vcmi/vcmi/blob/develop/ChangeLog.md) +- - Update release date for Linux packaging. See [example](https://github.com/vcmi/vcmi/pull/1258) +- - Update build ID for Android packaging. See [example](https://github.com/vcmi/vcmi/pull/2090) +- - Update downloads counter in readme.md. See [example](https://github.com/vcmi/vcmi/pull/2091) + +### Release preparation stage +Should be done 1 day before release. At this point beta branch is in full freeze. + +- Merge release preparation PR into `beta` +- Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages +- Create draft release page, specify `1.x.y` as tag for `master` after publishing +- Check that artifacts for all platforms have been build by CI `master` branch +- Attach build artifacts for all platforms to release page +- Manually extract Windows installer, remove `$PLUGINSDIR` directory and repackage data as .zip archive +- Attach produced zip archive to release page as an alternative Windows installer +- Upload built AAB to Google Play and send it for review - Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates) +- (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu ### Release publishing phase Should be done on release date -- Check that all items from milestone `Release 1.x` completed -- Merge `beta` into `master` -- Check that artifacts for all platforms are available on `master` branch - Trigger builds for new release on Ubuntu PPA -- Attach build artifacts for all platforms to release page -- Merge prepared pull requests +- Publish new release on Google Play - Publish release page - -### After release publish - -- Close `Release 1.x` milestone -- Write announcements in social networks - Merge `master` into `develop` -- Update downloads counter in readme.md. See [example](https://github.com/vcmi/vcmi/pull/2091) +- Merge prepared PR's for vcmi-updates and for website (if any) +- Close `Release 1.x` project +- Write announcements in social networks From 348eddcc1779bf0ccb204cb2b1eb37c873d31011 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 00:14:51 +0300 Subject: [PATCH 0449/1248] Fix looped playback of video files --- client/CVideoHandler.cpp | 4 +++- client/CVideoHandler.h | 12 +++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index cb2eba631..ea1d905bc 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -17,6 +17,7 @@ #include "renderSDL/SDL_Extensions.h" #include "CPlayerInterface.h" #include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CInputStream.h" #include @@ -367,7 +368,8 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo show(x,y,dst,update); else { - open(fname); + VideoPath filenameToReopen = fname; // create copy to backup this->fname + open(filenameToReopen); nextFrame(); // The y position is wrong at the first frame. diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 877147ff5..ec9f60278 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -15,7 +15,7 @@ struct SDL_Surface; struct SDL_Texture; -class IVideoPlayer +class IVideoPlayer : boost::noncopyable { public: virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes @@ -31,8 +31,6 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: - VideoPath fname; //name of current video file (empty if idle) - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { @@ -55,14 +53,16 @@ public: #ifndef DISABLE_VIDEO -#include "../lib/filesystem/CInputStream.h" - struct AVFormatContext; struct AVCodecContext; struct AVCodec; struct AVFrame; struct AVIOContext; +VCMI_LIB_NAMESPACE_BEGIN +class CInputStream; +VCMI_LIB_NAMESPACE_END + class CVideoPlayer : public IMainVideoPlayer { int stream; // stream index in video @@ -74,6 +74,8 @@ class CVideoPlayer : public IMainVideoPlayer AVIOContext * context; + VideoPath fname; //name of current video file (empty if idle) + // Destination. Either overlay or dest. SDL_Texture *texture; From ff96b1af0a73b2524fef15f59cb20dccc2c1c239 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 00:32:10 +0200 Subject: [PATCH 0450/1248] initial hero overview --- client/CMakeLists.txt | 2 + client/VCMI_client.cbp | 2 + client/VCMI_client.vcxproj | 2 + client/VCMI_client.vcxproj.filters | 6 ++ client/lobby/OptionsTab.cpp | 91 ++++++++++++++++-------------- client/lobby/OptionsTab.h | 8 +-- client/windows/CHeroOverview.cpp | 53 +++++++++++++++++ client/windows/CHeroOverview.h | 41 ++++++++++++++ 8 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 client/windows/CHeroOverview.cpp create mode 100644 client/windows/CHeroOverview.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ae542146d..41ad681ba 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -114,6 +114,7 @@ set(client_SRCS windows/CCastleInterface.cpp windows/CCreatureWindow.cpp + windows/CHeroOverview.cpp windows/CHeroWindow.cpp windows/CKingdomInterface.cpp windows/CMessage.cpp @@ -275,6 +276,7 @@ set(client_HEADERS windows/CCastleInterface.h windows/CCreatureWindow.h + windows/CHeroOverview.h windows/CHeroWindow.h windows/CKingdomInterface.h windows/CMessage.h diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp index 845082b67..5abaf79fd 100644 --- a/client/VCMI_client.cbp +++ b/client/VCMI_client.cbp @@ -225,6 +225,8 @@ + + diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj index 73de261cb..fd89da223 100644 --- a/client/VCMI_client.vcxproj +++ b/client/VCMI_client.vcxproj @@ -236,6 +236,7 @@ + @@ -301,6 +302,7 @@ + diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters index a93e38825..d5f5af0fa 100644 --- a/client/VCMI_client.vcxproj.filters +++ b/client/VCMI_client.vcxproj.filters @@ -22,6 +22,9 @@ windows + + windows + windows @@ -172,6 +175,9 @@ windows + + windows + windows diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 78c5c141a..504c97d81 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -30,6 +30,7 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../windows/CHeroOverview.h" #include "../eventsSDL/InputHandler.h" #include "../../lib/filesystem/Filesystem.h" @@ -274,12 +275,12 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall }; - auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); + auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); switch(type) { case TOWN: - switch(settings.castle) + switch(playerSettings.castle) { case PlayerSettings::NONE: return TOWN_NONE; @@ -289,7 +290,7 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + (big ? 0 : 2); } case HERO: - switch(settings.hero) + switch(playerSettings.hero) { case PlayerSettings::NONE: return HERO_NONE; @@ -297,15 +298,15 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) return HERO_RANDOM; default: { - if(settings.heroPortrait != HeroTypeID::NONE) - return settings.heroPortrait; - auto index = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); + if(playerSettings.heroPortrait != HeroTypeID::NONE) + return playerSettings.heroPortrait; + auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); return (*CGI->heroh)[index]->imageIndex; } } case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { case PlayerSettings::RANDOM: return RANDOM; @@ -363,7 +364,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() { case TOWN: { - switch(settings.castle) + switch(playerSettings.castle) { case PlayerSettings::NONE: return CGI->generaltexth->allTexts[523]; @@ -371,14 +372,14 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; default: { - auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); + auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); return (*CGI->townh)[factionIndex]->getNameTranslated(); } } } case HERO: { - switch(settings.hero) + switch(playerSettings.hero) { case PlayerSettings::NONE: return CGI->generaltexth->allTexts[523]; @@ -386,21 +387,21 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; default: { - if(!settings.heroName.empty()) - return settings.heroName; - auto index = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); + if(!playerSettings.heroName.empty()) + return playerSettings.heroName; + auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); return (*CGI->heroh)[index]->getNameTranslated(); } } } case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[522]; default: - return CGI->generaltexth->arraytxt[214 + settings.bonus]; + return CGI->generaltexth->arraytxt[214 + playerSettings.bonus]; } } } @@ -413,12 +414,12 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() switch(type) { case OptionsTab::TOWN: - return (settings.castle.getNum() < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; + return (playerSettings.castle.getNum() < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; case OptionsTab::HERO: - return (settings.hero.getNum() < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; + return (playerSettings.hero.getNum() < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; case OptionsTab::BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[86]; //{Random Bonus} @@ -435,8 +436,8 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() } std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { - auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); - auto heroIndex = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); + auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); + auto heroIndex = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); switch(type) { @@ -444,14 +445,14 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() return getName(); case HERO: { - if(settings.hero.getNum() >= 0) + if(playerSettings.hero.getNum() >= 0) return getName() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated(); return getName(); } case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { case PlayerSettings::GOLD: return CGI->generaltexth->allTexts[87]; //500-1000 @@ -479,7 +480,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() std::string OptionsTab::CPlayerSettingsHelper::getDescription() { - auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); + auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); switch(type) { @@ -489,7 +490,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getDescription() return CGI->generaltexth->allTexts[102]; case BONUS: { - switch(settings.bonus) + switch(playerSettings.bonus) { case PlayerSettings::RANDOM: return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus @@ -530,13 +531,13 @@ OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelp { break; case TOWN: - value = settings.castle; + value = playerSettings.castle; break; case HERO: - value = settings.hero; + value = playerSettings.hero; break; case BONUS: - value = settings.bonus; + value = playerSettings.bonus; } if(value == PlayerSettings::RANDOM) @@ -566,7 +567,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = settings.castle.getNum() >= CGI->townh->size() ? 0 : settings.castle.getNum(); + auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; @@ -583,7 +584,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = settings.hero.getNum() >= CGI->heroh->size() ? 0 : settings.hero.getNum(); + auto heroIndex = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); @@ -908,7 +909,10 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - GH.windows().createAndPushWindow(helper); + if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroName.empty()) + GH.windows().createAndPushWindow(helper.playerSettings.hero); + else + GH.windows().createAndPushWindow(helper); } else selectedHero = set.hero; @@ -963,9 +967,9 @@ void OptionsTab::SelectionWindow::showPopupWindow(const Point & cursorPosition) setElement(elem, false); } -OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type) +OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type) : Scrollable(LCLICK | SHOW_POPUP, position, Orientation::HORIZONTAL) - , CPlayerSettingsHelper(settings, type) + , CPlayerSettingsHelper(playerSettings, type) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -986,30 +990,33 @@ void OptionsTab::SelectedBox::update() void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) { // cases when we do not need to display a message - if(settings.castle.getNum() == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) + if(playerSettings.castle.getNum() == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) return; - if(settings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + if(playerSettings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(playerSettings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; - GH.windows().createAndPushWindow(*this); + if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroName.empty()) + GH.windows().createAndPushWindow(playerSettings.hero); + else + GH.windows().createAndPushWindow(*this); } void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { - PlayerInfo pi = SEL->getPlayerInfo(settings.color.getNum()); - const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(settings.color); + PlayerInfo pi = SEL->getPlayerInfo(playerSettings.color.getNum()); + const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(playerSettings.color); if(type == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) return; - if(type == SelType::HERO && ((pi.defaultHero() != -1 || settings.castle.getNum() < 0) || foreignPlayer)) + if(type == SelType::HERO && ((pi.defaultHero() != -1 || playerSettings.castle.getNum() < 0) || foreignPlayer)) return; if(type == SelType::BONUS && foreignPlayer) return; GH.input().hapticFeedback(); - GH.windows().createAndPushWindow(settings.color, type); + GH.windows().createAndPushWindow(playerSettings.color, type); } void OptionsTab::SelectedBox::scrollBy(int distance) @@ -1022,13 +1029,13 @@ void OptionsTab::SelectedBox::scrollBy(int distance) switch(CPlayerSettingsHelper::type) { case TOWN: - CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, playerSettings.color); break; case HERO: - CSH->setPlayerOption(LobbyChangePlayerOption::HERO, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::HERO, distance, playerSettings.color); break; case BONUS: - CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, distance, settings.color); + CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, distance, playerSettings.color); break; } diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 042b84bea..34cd58dc5 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -53,11 +53,11 @@ private: struct CPlayerSettingsHelper { - const PlayerSettings & settings; + const PlayerSettings & playerSettings; const SelType type; - CPlayerSettingsHelper(const PlayerSettings & settings, SelType type) - : settings(settings), type(type) + CPlayerSettingsHelper(const PlayerSettings & playerSettings, SelType type) + : playerSettings(playerSettings), type(type) {} /// visible image settings @@ -156,7 +156,7 @@ private: std::shared_ptr image; std::shared_ptr subtitle; - SelectedBox(Point position, PlayerSettings & settings, SelType type); + SelectedBox(Point position, PlayerSettings & playerSettings, SelType type); void showPopupWindow(const Point & cursorPosition) override; void clickReleased(const Point & cursorPosition) override; void scrollBy(int distance) override; diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp new file mode 100644 index 000000000..1c9fc253d --- /dev/null +++ b/client/windows/CHeroOverview.cpp @@ -0,0 +1,53 @@ +/* + * CHeroOverview.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 "CHeroOverview.h" + +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../render/Colors.h" +#include "../render/Graphics.h" +#include "../widgets/CComponent.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" + +CHeroOverview::CHeroOverview(const HeroTypeID & h) + : CWindowObject(BORDERED | RCLICK_POPUP), hero { h }, heroIndex { h.getNum() } +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + genHeroWindow(); + + center(); +} + +void CHeroOverview::genHeader() +{ + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); + updateShadow(); + + labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); + labelSubTitle = std::make_shared(pos.w / 2, 88, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getNameTranslated() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); + image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 24, 45); +} + +void CHeroOverview::genHeroWindow() +{ + pos = Rect(0, 0, 292, 226); + genHeader(); + labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + auto heroIndex = hero.getNum() >= CGI->heroh->size() ? 0 : hero.getNum(); + + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); + labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); +} \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h new file mode 100644 index 000000000..d0c5419f3 --- /dev/null +++ b/client/windows/CHeroOverview.h @@ -0,0 +1,41 @@ +/* + * CHeroOverview.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 "../windows/CWindowObject.h" + +class CLabel; +class CFilledTexture; +class CAnimImage; +class CComponentBox; +class CTextBox; + +class CHeroOverview : public CWindowObject +{ + const HeroTypeID & hero; + int heroIndex; + + std::shared_ptr backgroundTexture; + std::shared_ptr labelTitle; + std::shared_ptr labelSubTitle; + std::shared_ptr image; + + std::shared_ptr labelHeroSpeciality; + std::shared_ptr imageSpeciality; + std::shared_ptr labelSpecialityName; + + std::shared_ptr textBonusDescription; + + void genHeader(); + void genHeroWindow(); + +public: + CHeroOverview(const HeroTypeID & h); +}; \ No newline at end of file From 85deaf0b4b549d28b61dd1aa916c5ac73dae44c6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 01:49:17 +0300 Subject: [PATCH 0451/1248] Update docs according to review --- docs/maintainers/Release_Process.md | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/docs/maintainers/Release_Process.md b/docs/maintainers/Release_Process.md index d9b27a7fe..9c70164a2 100644 --- a/docs/maintainers/Release_Process.md +++ b/docs/maintainers/Release_Process.md @@ -1,10 +1,15 @@ < [Documentation](../Readme.md) / Release Process +# Versioning +For releases VCMI uses version numbering in form "1.X.Y", where: +- 'X' indicates major release. Different major versions are generally not compatible with each other. Save format is different, network protocol is different, mod format likely different. +- 'Y' indicates hotfix release. Despite its name this is usually not urgent, but planned release. Different hotfixes for same major version are fully compatible with each other. + # Branches -Our branching strategy is very similar to GitFlow -- `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `maj.min.patch` -- `beta` branch is for stabilization of ongoing release. Only safe changes should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta. -- `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta` +Our branching strategy is very similar to GitFlow: +- `master` branch has release commits. One commit - one release. Each release commit should be tagged with version `1.X.Y` when corresponding version is released. State of master branch represents state of latest public release. +- `beta` branch is for stabilization of ongoing release. Beta branch is created when new major release enters stabilization stage and is used for both major release itself as well as for subsequent hotfixes. Only changes that are safe, have minimal chance of regressions and improve player experience should be targeted into this branch. Breaking changes (e.g. save format changes) are forbidden in beta. +- `develop` branch is a main branch for ongoing development. Pull requests with new features should be targeted to this branch, `develop` version is one major release ahead of `beta`. # Release process step-by-step @@ -22,7 +27,7 @@ Should be done 2-4 weeks before planned release date. All major features should - Bump version for Linux on `develop` branch - Bump version and build ID for Android on `develop` branch -### Initial release setup (patch releases only) +### Initial release setup (hotfix releases only) - Bump vcmi version in CMake on `beta` branch - Bump version for Linux on `beta` branch @@ -44,11 +49,12 @@ Should be done 1 day before release. At this point beta branch is in full freeze - Merge release preparation PR into `beta` - Merge `beta` into `master`. This will trigger CI pipeline that will generate release packages - Create draft release page, specify `1.x.y` as tag for `master` after publishing -- Check that artifacts for all platforms have been build by CI `master` branch +- Check that artifacts for all platforms have been built by CI on `master` branch +- Download and rename all build artifacts to use form "VCMI-1.X.Y-Platform.xxx" - Attach build artifacts for all platforms to release page -- Manually extract Windows installer, remove `$PLUGINSDIR` directory and repackage data as .zip archive +- Manually extract Windows installer, remove `$PLUGINSDIR` directory which contains installer files and repackage data as .zip archive - Attach produced zip archive to release page as an alternative Windows installer -- Upload built AAB to Google Play and send it for review +- Upload built AAB to Google Play and send created release draft for review (usually takes several hours) - Prepare pull request for [vcmi-updates](https://github.com/vcmi/vcmi-updates) - (major releases only) Prepare pull request with release update for web site https://github.com/vcmi/VCMI.eu From 73fe924544e65b77ff2fd89d53d79d4b17b268b3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 02:14:03 +0200 Subject: [PATCH 0452/1248] draw background --- client/windows/CHeroOverview.cpp | 34 +++++++++++++++++++++++++++++--- client/windows/CHeroOverview.h | 4 +++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 1c9fc253d..d2663fd8c 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -12,8 +12,11 @@ #include "../CGameInfo.h" #include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" #include "../render/Colors.h" #include "../render/Graphics.h" +#include "../render/IImage.h" +#include "../renderSDL/RenderHandler.h" #include "../widgets/CComponent.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" @@ -36,14 +39,39 @@ void CHeroOverview::genHeader() backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); + int yOffset = 35; + int borderOffset = 5; + + ColorRGBA borderColor = Colors::WHITE; + + Canvas canvas = Canvas(Point(pos.w, pos.h)); + + // Image + canvas.drawBorder(Rect(borderOffset - 1, borderOffset + yOffset - 1, 58 + 2, 64 + 2), borderColor); + + // Namebox + canvas.drawColorBlended(Rect(64 + borderOffset, borderOffset + yOffset, 220, 64), ColorRGBA(0, 0, 0, 75)); + canvas.drawBorder(Rect(64 + borderOffset - 1, borderOffset + yOffset - 1, 220 + 2, 64 + 2), borderColor); + + // vertical line + canvas.drawLine(Point(295, borderOffset + yOffset - 1), Point(295, 445), borderColor, borderColor); + + std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); + backgroundShapes = std::make_shared(backgroundShapesImg, pos); + labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); - labelSubTitle = std::make_shared(pos.w / 2, 88, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getNameTranslated() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); - image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 24, 45); + + // Image + image = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); + + // Namebox + labelHeroName = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); + labelHeroClass = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); } void CHeroOverview::genHeroWindow() { - pos = Rect(0, 0, 292, 226); + pos = Rect(0, 0, 600, 600); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); auto heroIndex = hero.getNum() >= CGI->heroh->size() ? 0 : hero.getNum(); diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index d0c5419f3..a8bfd720d 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -23,10 +23,12 @@ class CHeroOverview : public CWindowObject int heroIndex; std::shared_ptr backgroundTexture; + std::shared_ptr backgroundShapes; std::shared_ptr labelTitle; - std::shared_ptr labelSubTitle; std::shared_ptr image; + std::shared_ptr labelHeroName; + std::shared_ptr labelHeroClass; std::shared_ptr labelHeroSpeciality; std::shared_ptr imageSpeciality; std::shared_ptr labelSpecialityName; From b674682fe3eb4e861d215b44c18d22d753b6096b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 03:21:29 +0200 Subject: [PATCH 0453/1248] left side --- client/windows/CHeroOverview.cpp | 59 +++++++++++++++++++++++++------- client/windows/CHeroOverview.h | 7 ++-- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index d2663fd8c..29428ef7b 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -25,7 +25,7 @@ #include "../../lib/CHeroHandler.h" CHeroOverview::CHeroOverview(const HeroTypeID & h) - : CWindowObject(BORDERED | RCLICK_POPUP), hero { h }, heroIndex { h.getNum() } + : CWindowObject(BORDERED | RCLICK_POPUP), /*hero { h },*/ heroIndex { h.getNum() } { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -41,41 +41,74 @@ void CHeroOverview::genHeader() int yOffset = 35; int borderOffset = 5; + int alpha = 75; - ColorRGBA borderColor = Colors::WHITE; + ColorRGBA borderColor = ColorRGBA(128, 100, 75); Canvas canvas = Canvas(Point(pos.w, pos.h)); - // Image + // hero image canvas.drawBorder(Rect(borderOffset - 1, borderOffset + yOffset - 1, 58 + 2, 64 + 2), borderColor); - // Namebox - canvas.drawColorBlended(Rect(64 + borderOffset, borderOffset + yOffset, 220, 64), ColorRGBA(0, 0, 0, 75)); + // hero name + canvas.drawColorBlended(Rect(64 + borderOffset, borderOffset + yOffset, 220, 64), ColorRGBA(0, 0, 0, alpha)); canvas.drawBorder(Rect(64 + borderOffset - 1, borderOffset + yOffset - 1, 220 + 2, 64 + 2), borderColor); // vertical line canvas.drawLine(Point(295, borderOffset + yOffset - 1), Point(295, 445), borderColor, borderColor); + // skill header + canvas.drawColorBlended(Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 2 * borderOffset + yOffset + 64 - 1, 284 + 2, 20 + 2), borderColor); + + // skill footer + canvas.drawColorBlended(Rect(borderOffset, 4 * borderOffset + yOffset + 64 + 20 + 44, 284, 20), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 4 * borderOffset + yOffset + 64 + 20 + 44 - 1, 284 + 2, 20 + 2), borderColor); + + // hero biography + canvas.drawColorBlended(Rect(borderOffset, 5 * borderOffset + yOffset + 64 + 20 + 44 + 20, 284, 130), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 5 * borderOffset + yOffset + 64 + 20 + 44 + 20 - 1, 284 + 2, 130 + 2), borderColor); + + // speciality name + canvas.drawColorBlended(Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130, 235, 44), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(2 * borderOffset + 44 - 1, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 - 1, 235 + 2, 44 + 2), borderColor); + + // speciality image + canvas.drawBorder(Rect(borderOffset - 1, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 - 1, 44 + 2, 44 + 2), borderColor); + + // speciality description + canvas.drawColorBlended(Rect(borderOffset, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44, 284, 85), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 + 2, 85 + 2), borderColor); + + std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); backgroundShapes = std::make_shared(backgroundShapesImg, pos); labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); - // Image + // hero image image = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); - // Namebox + // hero name labelHeroName = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); labelHeroClass = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); + + // hero biography + labelHeroBiography = std::make_shared(Rect(2 * borderOffset, 5 * borderOffset + borderOffset + yOffset + 64 + 20 + 44 + 20, 284 - 2 * borderOffset, 130 - 2 * borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getBiographyTranslated()); + + // speciality name + labelHeroSpeciality = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + labelSpecialityName = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); + + // speciality image + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130); + + // speciality description + labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); } void CHeroOverview::genHeroWindow() { - pos = Rect(0, 0, 600, 600); + pos = Rect(0, 0, 450, 450 + 35); genHeader(); - labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = hero.getNum() >= CGI->heroh->size() ? 0 : hero.getNum(); - - imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); - labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); } \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index a8bfd720d..ceb25b1f4 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -12,6 +12,7 @@ #include "../windows/CWindowObject.h" class CLabel; +class CMultiLineLabel; class CFilledTexture; class CAnimImage; class CComponentBox; @@ -19,7 +20,7 @@ class CTextBox; class CHeroOverview : public CWindowObject { - const HeroTypeID & hero; + //const HeroTypeID & hero; int heroIndex; std::shared_ptr backgroundTexture; @@ -28,12 +29,12 @@ class CHeroOverview : public CWindowObject std::shared_ptr image; std::shared_ptr labelHeroName; + std::shared_ptr labelHeroBiography; std::shared_ptr labelHeroClass; std::shared_ptr labelHeroSpeciality; std::shared_ptr imageSpeciality; std::shared_ptr labelSpecialityName; - - std::shared_ptr textBonusDescription; + std::shared_ptr labelSpecialityDescription; void genHeader(); void genHeroWindow(); From 79e872da68dd7dfea307f84b0b49cba5838c6e20 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 04:10:08 +0200 Subject: [PATCH 0454/1248] left side ready --- client/windows/CHeroOverview.cpp | 16 ++++++++++++++++ client/windows/CHeroOverview.h | 3 +++ 2 files changed, 19 insertions(+) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 29428ef7b..56b39d3bb 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -61,6 +61,10 @@ void CHeroOverview::genHeader() canvas.drawColorBlended(Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20), ColorRGBA(0, 0, 0, alpha)); canvas.drawBorder(Rect(borderOffset - 1, 2 * borderOffset + yOffset + 64 - 1, 284 + 2, 20 + 2), borderColor); + // skill + for(int i = 0; i < 4; i++) + canvas.drawBorder(Rect((284 / 4) * i + 21 - 1, 3 * borderOffset + yOffset + 64 + 20 + 1 - 1, 42 + 2, 42 + 2), borderColor); + // skill footer canvas.drawColorBlended(Rect(borderOffset, 4 * borderOffset + yOffset + 64 + 20 + 44, 284, 20), ColorRGBA(0, 0, 0, alpha)); canvas.drawBorder(Rect(borderOffset - 1, 4 * borderOffset + yOffset + 64 + 20 + 44 - 1, 284 + 2, 20 + 2), borderColor); @@ -93,6 +97,18 @@ void CHeroOverview::genHeader() labelHeroName = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); labelHeroClass = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); + // skills header + for(int i = 0; i < 4; i++) + labelSkillHeader.push_back(std::make_shared((284 / 4) * i + 42, 2 * borderOffset + yOffset + 64 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); + + // skill + for(int i = 0; i < 4; i++) + imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), (const int[]){0, 1, 2, 5}[i], 0, (284 / 4) * i + 21, 3 * borderOffset + yOffset + 64 + 20 + 1)); + + // skills footer + for(int i = 0; i < 4; i++) + labelSkillFooter.push_back(std::make_shared((284 / 4) * i + 42, 4 * borderOffset + yOffset + 64 + 20 + 44 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIndex]->heroClass->primarySkillInitial[i]))); + // hero biography labelHeroBiography = std::make_shared(Rect(2 * borderOffset, 5 * borderOffset + borderOffset + yOffset + 64 + 20 + 44 + 20, 284 - 2 * borderOffset, 130 - 2 * borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getBiographyTranslated()); diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index ceb25b1f4..0e12c090a 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -33,6 +33,9 @@ class CHeroOverview : public CWindowObject std::shared_ptr labelHeroClass; std::shared_ptr labelHeroSpeciality; std::shared_ptr imageSpeciality; + std::vector> labelSkillHeader; + std::vector> imageSkill; + std::vector> labelSkillFooter; std::shared_ptr labelSpecialityName; std::shared_ptr labelSpecialityDescription; From 7b22aefb12b156b55938b40b09c09f963226d3af Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 17 Sep 2023 13:03:42 +0200 Subject: [PATCH 0455/1248] Some minor fixes --- lib/mapObjects/CGPandoraBox.cpp | 52 +++++++++++++-------------- mapeditor/inspector/rewardswidget.cpp | 1 + mapeditor/inspector/rewardswidget.ui | 2 +- 3 files changed, 27 insertions(+), 28 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 4fa3e50df..68187441c 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -171,60 +171,58 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { //backward compatibility CCreatureSet::serializeJson(handler, "guards", 7); - Rewardable::Reward reward; + Rewardable::VisitInfo vinfo; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - auto addReward = [this, &reward](bool condition) + auto addReward = [this, &vinfo](bool condition) { if(condition) { - configuration.info.emplace_back(); - configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - configuration.info.back().reward = reward; - reward = Rewardable::Reward{}; + configuration.info.push_back(vinfo); + vinfo = Rewardable::VisitInfo{}; + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; } }; - - addReward(true); - + int val; - handler.serializeInt("experience", reward.heroExperience, 0); - addReward(reward.heroExperience); + handler.serializeInt("experience", vinfo.reward.heroExperience, 0); + addReward(vinfo.reward.heroExperience); - handler.serializeInt("mana", reward.manaDiff, 0); - addReward(reward.manaDiff); + handler.serializeInt("mana", vinfo.reward.manaDiff, 0); + addReward(vinfo.reward.manaDiff); handler.serializeInt("morale", val, 0); if(val) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); addReward(val); handler.serializeInt("luck", val, 0); if(val) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); addReward(val); - reward.resources.serializeJson(handler, "resources"); - addReward(reward.resources.nonZero()); + vinfo.reward.resources.serializeJson(handler, "resources"); + addReward(vinfo.reward.resources.nonZero()); { bool updateReward = false; auto s = handler.enterStruct("primarySkills"); - for(int idx = 0; idx < reward.primary.size(); idx ++) + for(int idx = 0; idx < vinfo.reward.primary.size(); idx ++) { - handler.serializeInt(NPrimarySkill::names[idx], reward.primary[idx], 0); - updateReward |= bool(reward.primary[idx]); + handler.serializeInt(NPrimarySkill::names[idx], vinfo.reward.primary[idx], 0); + updateReward |= bool(vinfo.reward.primary[idx]); } addReward(updateReward); } - handler.serializeIdArray("artifacts", reward.artifacts); - addReward(!reward.artifacts.empty()); + handler.serializeIdArray("artifacts", vinfo.reward.artifacts); + addReward(!vinfo.reward.artifacts.empty()); - handler.serializeIdArray("spells", reward.spells); - addReward(!reward.spells.empty()); + handler.serializeIdArray("spells", vinfo.reward.spells); + addReward(!vinfo.reward.spells.empty()); - handler.enterArray("creatures").serializeStruct(reward.creatures); - addReward(!reward.creatures.empty()); + handler.enterArray("creatures").serializeStruct(vinfo.reward.creatures); + addReward(!vinfo.reward.creatures.empty()); { bool updateReward = false; @@ -248,7 +246,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) continue; } - reward.secondary[rawId] = level; + vinfo.reward.secondary[rawId] = level; updateReward = true; } addReward(updateReward); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 24d5353f2..36813172f 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -218,6 +218,7 @@ bool RewardsWidget::commitChanges() void RewardsWidget::saveCurrentVisitInfo(int index) { auto & vinfo = object.configuration.info.at(index); + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString()); vinfo.reward.heroLevel = ui->rHeroLevel->value(); diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index 544eee0ff..c3681e258 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -49,7 +49,7 @@ QAbstractItemView::NoEditTriggers - -3 + 0
    From 59b5c46a32b6c5b9ba65aefb19e9aa95445b4e0f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 13:22:39 +0200 Subject: [PATCH 0456/1248] right side draft --- client/windows/CHeroOverview.cpp | 52 +++++++++++++++++++++++++++++--- client/windows/CHeroOverview.h | 19 ++++++++++-- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 56b39d3bb..3b089fe99 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -22,14 +22,17 @@ #include "../widgets/TextControls.h" #include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CCreatureHandler.h" #include "../../lib/CHeroHandler.h" CHeroOverview::CHeroOverview(const HeroTypeID & h) - : CWindowObject(BORDERED | RCLICK_POPUP), /*hero { h },*/ heroIndex { h.getNum() } + : CWindowObject(BORDERED | RCLICK_POPUP), hero { h } { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - genHeroWindow(); + heroIndex = hero.getNum(); + + genHeroWindow(); center(); } @@ -84,6 +87,15 @@ void CHeroOverview::genHeader() canvas.drawColorBlended(Rect(borderOffset, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44, 284, 85), ColorRGBA(0, 0, 0, alpha)); canvas.drawBorder(Rect(borderOffset - 1, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 + 2, 85 + 2), borderColor); + // army title + canvas.drawColorBlended(Rect(302, borderOffset + yOffset, 292, 30), ColorRGBA(0, 0, 0, alpha)); + + // army + for(int i = 0; i < 6; i++) + { + int space = (292 - 6 * 32) / 5; + canvas.drawColorBlended(Rect(302 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); + } std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); backgroundShapes = std::make_shared(backgroundShapesImg, pos); @@ -91,7 +103,7 @@ void CHeroOverview::genHeader() labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); // hero image - image = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); + imageHero = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); // hero name labelHeroName = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); @@ -121,10 +133,42 @@ void CHeroOverview::genHeader() // speciality description labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); + + // army title + labelArmyTitle = std::make_shared(302 + borderOffset, 2 * borderOffset + yOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Starting Army"); + + // army + int i = 0; + for(auto & army : (*CGI->heroh)[heroIndex]->initialArmy) + { + if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) + { + imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 300 + 5 + i * 32, 30 + 100)); // size 32x32 + labelArmyNames.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 40 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->creh)[army.creature]->getNameSingularTranslated())); + labelArmyCount.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 60 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); + i++; + } + } + + + // war machine title + labelWarMachineTitle = std::make_shared(300 + 5, 5 + 5 + 100 + 100, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "war m"); + + // war machine + i = 0; + for(auto & army : (*CGI->heroh)[heroIndex]->initialArmy) + { + if((*CGI->creh)[army.creature]->warMachine != ArtifactID::NONE) + { + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 300 + 5 + i * 32, 30 + 100 + 100)); // size 32x32 + labelWarMachineNames.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 40 + 100 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->creh)[army.creature]->getNameSingularTranslated())); + i++; + } + } } void CHeroOverview::genHeroWindow() { - pos = Rect(0, 0, 450, 450 + 35); + pos = Rect(0, 0, 600, 450 + 35); genHeader(); } \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 0e12c090a..2d464cb87 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -20,14 +20,13 @@ class CTextBox; class CHeroOverview : public CWindowObject { - //const HeroTypeID & hero; + const HeroTypeID & hero; int heroIndex; std::shared_ptr backgroundTexture; std::shared_ptr backgroundShapes; std::shared_ptr labelTitle; - std::shared_ptr image; - + std::shared_ptr imageHero; std::shared_ptr labelHeroName; std::shared_ptr labelHeroBiography; std::shared_ptr labelHeroClass; @@ -39,6 +38,20 @@ class CHeroOverview : public CWindowObject std::shared_ptr labelSpecialityName; std::shared_ptr labelSpecialityDescription; + std::shared_ptr labelArmyTitle; + std::vector> imageArmy; + std::vector> labelArmyCount; + std::vector> labelArmyNames; + + std::shared_ptr labelWarMachineTitle; + std::vector> imageWarMachine; + std::vector> labelWarMachineNames; + + std::shared_ptr labelSpellBookTitle; + std::shared_ptr imageSpellBook; + std::vector> imageSpells; + std::vector> labelSpellsNames; + void genHeader(); void genHeroWindow(); From cfc5bac5408ccc4167c35f93393d019d07e0fff0 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 14:31:07 +0200 Subject: [PATCH 0457/1248] layout --- client/windows/CHeroOverview.cpp | 81 ++++++++++++++++++++++++++++---- client/windows/CHeroOverview.h | 2 - 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 3b089fe99..fb8965f90 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -89,12 +89,59 @@ void CHeroOverview::genHeader() // army title canvas.drawColorBlended(Rect(302, borderOffset + yOffset, 292, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, borderOffset + yOffset - 1, 292 + 2, 30 + 2), borderColor); // army for(int i = 0; i < 6; i++) { - int space = (292 - 6 * 32) / 5; - canvas.drawColorBlended(Rect(302 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); + int space = (292 - 32 - 6 * 32) / 5; + canvas.drawColorBlended(Rect(302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + i * (32 + space) + 16 - 1, 2 * borderOffset + yOffset + 30 - 1, 32 + 2, 32 + 2), borderColor); + } + + // army footer + canvas.drawColorBlended(Rect(302, 3 * borderOffset + yOffset + 30 + 32, 292, 20), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 3 * borderOffset + yOffset + 30 + 32 - 1, 292 + 2, 20 + 2), borderColor); + + // warmachine title + canvas.drawColorBlended(Rect(302, 4 * borderOffset + yOffset + 30 + 32 + 20, 292, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 4 * borderOffset + yOffset + 30 + 32 + 20 - 1, 292 + 2, 30 + 2), borderColor); + + // warmachine + for(int i = 0; i < 6; i++) + { + int space = (292 - 32 - 6 * 32) / 5; + canvas.drawColorBlended(Rect(302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + i * (32 + space) + 16 - 1, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30 - 1, 32 + 2, 32 + 2), borderColor); + } + + // secskill title + canvas.drawColorBlended(Rect(302, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); + + // vertical line + canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32), Point(302 + (292 / 2), 445), borderColor, borderColor); + + // spell title + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); + + // secskill + for(int i = 0; i < 6; i++) + { + canvas.drawColorBlended(Rect(302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); + } + + // spell + for(int i = 0; i < 6; i++) + { + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); } std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); @@ -143,28 +190,46 @@ void CHeroOverview::genHeader() { if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) { - imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 300 + 5 + i * 32, 30 + 100)); // size 32x32 - labelArmyNames.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 40 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->creh)[army.creature]->getNameSingularTranslated())); - labelArmyCount.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 60 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); + int space = (292 - 32 - 6 * 32) / 5; + imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30)); + labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 30 + 32 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); i++; } } // war machine title - labelWarMachineTitle = std::make_shared(300 + 5, 5 + 5 + 100 + 100, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "war m"); + labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 28, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: War Machine"); // war machine i = 0; for(auto & army : (*CGI->heroh)[heroIndex]->initialArmy) { + int space = (292 - 32 - 6 * 32) / 5; + if(i == 0) + { + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30)); + i++; + } if((*CGI->creh)[army.creature]->warMachine != ArtifactID::NONE) { - imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 300 + 5 + i * 32, 30 + 100 + 100)); // size 32x32 - labelWarMachineNames.push_back(std::make_shared(300 + 5 + i * 32 + 16, 30 + 40 + 100 + 100, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->creh)[army.creature]->getNameSingularTranslated())); + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30)); i++; } } + + /*std::shared_ptr labelArmyTitle; + std::vector> imageArmy; + std::vector> labelArmyNames; + + std::shared_ptr labelWarMachineTitle; + std::vector> imageWarMachine; + std::vector> labelWarMachineNames; + + std::shared_ptr labelSpellBookTitle; + std::shared_ptr imageSpellBook; + std::vector> imageSpells; + std::vector> labelSpellsNames;*/ } void CHeroOverview::genHeroWindow() diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 2d464cb87..1c6182a55 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -41,11 +41,9 @@ class CHeroOverview : public CWindowObject std::shared_ptr labelArmyTitle; std::vector> imageArmy; std::vector> labelArmyCount; - std::vector> labelArmyNames; std::shared_ptr labelWarMachineTitle; std::vector> imageWarMachine; - std::vector> labelWarMachineNames; std::shared_ptr labelSpellBookTitle; std::shared_ptr imageSpellBook; From ee935e9fa7bbb1678f0b548de3165ed9b22d0f04 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 17 Sep 2023 12:36:57 +0000 Subject: [PATCH 0458/1248] Fix JSON files --- .../rangeHighlights/rangeHighlightsGreen.json | 20 +++++++++---------- .../rangeHighlights/rangeHighlightsRed.json | 20 +++++++++---------- test/testdata/erm/list-manipulation.json | 2 +- test/testdata/erm/std.json | 2 +- 4 files changed, 22 insertions(+), 22 deletions(-) diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json index a52f145a9..1c6b60d36 100644 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json +++ b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json @@ -2,20 +2,20 @@ "basepath" : "battle/rangeHighlights/green/", "images" : [ - { "frame" : 00, "file" : "empty.png"}, // 000001 -> 00 empty frame + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame // load single edges - { "frame" : 01, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 02, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 03, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 04, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 05, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 06, "file" : "left.png"}, //100000 -> 32 left + { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft + { "frame" : 2, "file" : "topLeft.png"}, //000010 -> 02 topRight + { "frame" : 3, "file" : "left.png"}, //000100 -> 04 right + { "frame" : 4, "file" : "topLeft.png"}, //001000 -> 08 bottomRight + { "frame" : 5, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft + { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left // load double edges - { "frame" : 07, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 08, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 09, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner + { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top + { "frame" : 8, "file" : "top.png"}, //011000 -> 24 bottom + { "frame" : 9, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner { "frame" : 10, "file" : "topLeftHalfCorner.png"}, //001100 -> 12 bottomRightHalfCorner { "frame" : 11, "file" : "topLeftHalfCorner.png"}, //110000 -> 48 bottomLeftHalfCorner { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner diff --git a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json index 15dac5b75..42350c073 100644 --- a/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json +++ b/Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json @@ -2,20 +2,20 @@ "basepath" : "battle/rangeHighlights/red/", "images" : [ - { "frame" : 00, "file" : "empty.png"}, // 000001 -> 00 empty frame + { "frame" : 0, "file" : "empty.png"}, // 000001 -> 00 empty frame // load single edges - { "frame" : 01, "file" : "topLeft.png"}, //000001 -> 01 topLeft - { "frame" : 02, "file" : "topLeft.png"}, //000010 -> 02 topRight - { "frame" : 03, "file" : "left.png"}, //000100 -> 04 right - { "frame" : 04, "file" : "topLeft.png"}, //001000 -> 08 bottomRight - { "frame" : 05, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft - { "frame" : 06, "file" : "left.png"}, //100000 -> 32 left + { "frame" : 1, "file" : "topLeft.png"}, //000001 -> 01 topLeft + { "frame" : 2, "file" : "topLeft.png"}, //000010 -> 02 topRight + { "frame" : 3, "file" : "left.png"}, //000100 -> 04 right + { "frame" : 4, "file" : "topLeft.png"}, //001000 -> 08 bottomRight + { "frame" : 5, "file" : "topLeft.png"}, //010000 -> 16 bottomLeft + { "frame" : 6, "file" : "left.png"}, //100000 -> 32 left // load double edges - { "frame" : 07, "file" : "top.png"}, //000011 -> 03 top - { "frame" : 08, "file" : "top.png"}, //011000 -> 24 bottom - { "frame" : 09, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner + { "frame" : 7, "file" : "top.png"}, //000011 -> 03 top + { "frame" : 8, "file" : "top.png"}, //011000 -> 24 bottom + { "frame" : 9, "file" : "topLeftHalfCorner.png"}, //000110 -> 06 topRightHalfCorner { "frame" : 10, "file" : "topLeftHalfCorner.png"}, //001100 -> 12 bottomRightHalfCorner { "frame" : 11, "file" : "topLeftHalfCorner.png"}, //110000 -> 48 bottomLeftHalfCorner { "frame" : 12, "file" : "topLeftHalfCorner.png"}, //100001 -> 33 topLeftHalfCorner diff --git a/test/testdata/erm/list-manipulation.json b/test/testdata/erm/list-manipulation.json index fc19ae42c..670224ead 100644 --- a/test/testdata/erm/list-manipulation.json +++ b/test/testdata/erm/list-manipulation.json @@ -5,7 +5,7 @@ "MDATA":{}, "Q":{}, "v":{ - "1":5 + "1":5, "2":5, "3":6 }, diff --git a/test/testdata/erm/std.json b/test/testdata/erm/std.json index efd95fad6..a504e39a2 100644 --- a/test/testdata/erm/std.json +++ b/test/testdata/erm/std.json @@ -5,7 +5,7 @@ "MDATA":{}, "Q":{}, "v":{ - "1":1 + "1":1, "2":40320 }, "z":{ From 44d72dce517b3e122ed07b2235cea7e0e26cc1c8 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 17 Sep 2023 15:06:49 +0200 Subject: [PATCH 0459/1248] Use enum in h3m map parser --- lib/mapping/MapFormatH3M.cpp | 71 ++++++++++++++++++++++-------------- lib/mapping/MapFormatH3M.h | 10 ++--- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index b2bdf69bc..58c181178 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -989,11 +989,11 @@ void CMapLoaderH3M::readObjectTemplates() } } -CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition) +CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) { auto * object = new CGEvent(); - readBoxContent(object, mapPosition); + readBoxContent(object, mapPosition, idToBeGiven); reader->readBitmaskPlayers(object->availableFor, false); object->computerActivate = reader->readBool(); @@ -1009,14 +1009,14 @@ CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition) return object; } -CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition) +CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) { auto * object = new CGPandoraBox(); - readBoxContent(object, mapPosition); + readBoxContent(object, mapPosition, idToBeGiven); return object; } -void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition) +void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) { readMessageAndGuards(object->message, object, mapPosition); Rewardable::VisitInfo vinfo; @@ -1024,8 +1024,8 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), object->id); - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), object->id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) @@ -1417,7 +1417,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptrid) { case Obj::EVENT: - return readEvent(mapPosition); + return readEvent(mapPosition, objectInstanceID); case Obj::HERO: case Obj::RANDOM_HERO: @@ -1440,7 +1440,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptrwarn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount); for(size_t i = 0; i < questsCount; ++i) - readSeerHutQuest(hut, position); + readSeerHutQuest(hut, position, idToBeGiven); if(features.levelHOTA3) { @@ -1818,7 +1818,7 @@ CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position) logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); for(size_t i = 0; i < repeateableQuestsCount; ++i) - readSeerHutQuest(hut, position); + readSeerHutQuest(hut, position, idToBeGiven); } reader->skipZero(2); @@ -1826,7 +1826,22 @@ CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position) return hut; } -void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) +enum class ESeerHutRewardType : uint8_t +{ + NOTHING = 0, + EXPERIENCE = 1, + MANA_POINTS = 2, + MORALE = 3, + LUCK = 4, + RESOURCES = 5, + PRIMARY_SKILL = 6, + SECONDARY_SKILL = 7, + ARTIFACT = 8, + SPELL = 9, + CREATURE = 10, +}; + +void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) { if(features.levelAB) { @@ -1854,37 +1869,37 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) if(hut->quest->missionType) { - auto rewardType = reader->readUInt8(); + auto rewardType = static_cast(reader->readUInt8()); Rewardable::VisitInfo vinfo; auto & reward = vinfo.reward; switch(rewardType) { - case 0: //NOTHING + case ESeerHutRewardType::NOTHING: { // no-op break; } - case 1: //EXPERIENCE + case ESeerHutRewardType::EXPERIENCE: { reward.heroExperience = reader->readUInt32(); break; } - case 2: //MANA POINTS: + case ESeerHutRewardType::MANA_POINTS: { reward.manaDiff = reader->readUInt32(); break; } - case 3: //MORALE_BONUS + case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), hut->id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); break; } - case 4: //LUCK_BONUS + case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), hut->id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); break; } - case 5: //RESOURCES + case ESeerHutRewardType::RESOURCES: { auto rId = reader->readUInt8(); auto rVal = reader->readUInt32(); @@ -1895,7 +1910,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) reward.resources[rId] = rVal; break; } - case 6: //PRIMARY_SKILL + case ESeerHutRewardType::PRIMARY_SKILL: { auto rId = reader->readUInt8(); auto rVal = reader->readUInt8(); @@ -1903,7 +1918,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) reward.primary.at(rId) = rVal; break; } - case 7: //SECONDARY_SKILL + case ESeerHutRewardType::SECONDARY_SKILL: { auto rId = reader->readSkill(); auto rVal = reader->readUInt8(); @@ -1911,17 +1926,17 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position) reward.secondary[rId] = rVal; break; } - case 8: //ARTIFACT + case ESeerHutRewardType::ARTIFACT: { reward.artifacts.push_back(reader->readArtifact()); break; } - case 9: //SPELL + case ESeerHutRewardType::SPELL: { reward.spells.push_back(reader->readSpell()); break; } - case 10: //CREATURE + case ESeerHutRewardType::CREATURE: { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index c0407a35a..8b7a10a08 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -158,10 +158,10 @@ private: /// Reads single object from input stream based on template CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readEvent(const int3 & objectPosition); + CGObjectInstance * readEvent(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readHero(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readSeerHut(const int3 & initialPos); + CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readSign(const int3 & position); CGObjectInstance * readWitchHut(); @@ -170,7 +170,7 @@ private: CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readPandora(const int3 & position); + CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readDwelling(const int3 & position); CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readShrine(); @@ -196,7 +196,7 @@ private: * * @param guard the quest guard where that quest should be applied to */ - void readBoxContent(CGPandoraBox * object, const int3 & position); + void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); /** * Reads a quest for the given quest guard. @@ -205,7 +205,7 @@ private: */ void readQuest(IQuestObject * guard, const int3 & position); - void readSeerHutQuest(CGSeerHut * hut, const int3 & position); + void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); /** * Reads events. From 69da4d59bf1676c4b6789e4e8775be0ef84a2900 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 15:28:05 +0200 Subject: [PATCH 0460/1248] basic function ready --- client/windows/CHeroOverview.cpp | 39 ++++++++++++++++++++++---------- client/windows/CHeroOverview.h | 7 ++++-- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index fb8965f90..77a0f8c61 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -24,6 +24,8 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CHeroHandler.h" +#include "../../lib/CSkillHandler.h" +#include "../../lib/spells/CSpellHandler.h" CHeroOverview::CHeroOverview(const HeroTypeID & h) : CWindowObject(BORDERED | RCLICK_POPUP), hero { h } @@ -197,9 +199,8 @@ void CHeroOverview::genHeader() } } - // war machine title - labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 28, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: War Machine"); + labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 27, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: War Machine"); // war machine i = 0; @@ -218,18 +219,32 @@ void CHeroOverview::genHeader() } } - /*std::shared_ptr labelArmyTitle; - std::vector> imageArmy; - std::vector> labelArmyNames; + // secskill title + labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Sek. Skills"); - std::shared_ptr labelWarMachineTitle; - std::vector> imageWarMachine; - std::vector> labelWarMachineNames; + // spell title + labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Spells"); - std::shared_ptr labelSpellBookTitle; - std::shared_ptr imageSpellBook; - std::vector> imageSpells; - std::vector> labelSpellsNames;*/ + // secskill + i = 0; + for(auto & skill : (*CGI->heroh)[heroIndex]->secSkillsInit) + { + imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex(), 0, 302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset))); + labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); + labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); + + i++; + } + + // spell + i = 0; + for(auto & spell : (*CGI->heroh)[heroIndex]->spells) + { + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), 0)); + labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); + + i++; + } } void CHeroOverview::genHeroWindow() diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 1c6182a55..b94d794a1 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -45,11 +45,14 @@ class CHeroOverview : public CWindowObject std::shared_ptr labelWarMachineTitle; std::vector> imageWarMachine; - std::shared_ptr labelSpellBookTitle; - std::shared_ptr imageSpellBook; + std::shared_ptr labelSpellTitle; std::vector> imageSpells; std::vector> labelSpellsNames; + std::shared_ptr labelSecSkillTitle; + std::vector> imageSecSkills; + std::vector> labelSecSkillsNames; + void genHeader(); void genHeroWindow(); From e3538f24abd2897373a4613ddc78a236f5219b0d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 17 Sep 2023 16:04:34 +0200 Subject: [PATCH 0461/1248] Fix components problem --- lib/mapObjectConstructors/CRewardableConstructor.cpp | 5 ----- lib/mapObjects/CGTownBuilding.cpp | 4 ---- lib/mapping/MapFormatH3M.cpp | 6 ++++-- lib/rewardable/Reward.cpp | 10 +++++++++- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 8d3bec191..ad87c06fe 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -51,11 +51,6 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG { bonus.source = BonusSource::OBJECT; bonus.sid = rewardableObject->ID; - //TODO: bonus.description = object->getObjectName(); - if (bonus.type == BonusType::MORALE) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); - if (bonus.type == BonusType::LUCK) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); } } assert(!rewardableObject->configuration.info.empty()); diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 098b519d5..0a8f952a6 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -307,10 +307,6 @@ void CTownRewardableBuilding::initObj(CRandomGenerator & rand) { bonus.source = BonusSource::TOWN_STRUCTURE; bonus.sid = bID; - if (bonus.type == BonusType::MORALE) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); - if (bonus.type == BonusType::LUCK) - rewardInfo.reward.extraComponents.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); } } } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 58c181178..5c95fffae 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1024,8 +1024,10 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, idToBeGiven); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, idToBeGiven); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 186f4ba16..07da516cd 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -61,7 +61,15 @@ void Rewardable::Reward::loadComponents(std::vector & comps, { for (auto comp : extraComponents) comps.push_back(comp); - + + for (auto & bonus : bonuses) + { + if (bonus.type == BonusType::MORALE) + comps.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); + if (bonus.type == BonusType::LUCK) + comps.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); + } + if (heroExperience) { comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); From a994fa00aa5461d3845e013b8d83bc9627db4962 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 16:15:17 +0200 Subject: [PATCH 0462/1248] fixes; translation --- Mods/vcmi/config/vcmi/english.json | 5 ++++ Mods/vcmi/config/vcmi/german.json | 5 ++++ client/windows/CHeroOverview.cpp | 38 +++++++++++++++++------------- client/windows/CHeroOverview.h | 1 - 4 files changed, 31 insertions(+), 18 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index d52eb4082..09bbd67fe 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -30,6 +30,11 @@ "vcmi.capitalColors.6" : "Teal", "vcmi.capitalColors.7" : "Pink", + "vcmi.heroOverview.startingArmy" : "Starting Armys", + "vcmi.heroOverview.warMachine" : "War Machines", + "vcmi.heroOverview.secondarySkills" : "Secondary Skills", + "vcmi.heroOverview.spells" : "Spells", + "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 74c1a02e4..12b563392 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -30,6 +30,11 @@ "vcmi.capitalColors.6" : "Türkis", "vcmi.capitalColors.7" : "Rosa", + "vcmi.heroOverview.startingArmy" : "Startarmeen", + "vcmi.heroOverview.warMachine" : "Kriegsmaschinen", + "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", + "vcmi.heroOverview.spells" : "Zaubersprüche", + "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 77a0f8c61..53b9ceb84 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -34,12 +34,14 @@ CHeroOverview::CHeroOverview(const HeroTypeID & h) heroIndex = hero.getNum(); + pos = Rect(0, 0, 600, 450 + 35); + genHeroWindow(); center(); } -void CHeroOverview::genHeader() +void CHeroOverview::genHeroWindow() { backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); @@ -60,7 +62,7 @@ void CHeroOverview::genHeader() canvas.drawBorder(Rect(64 + borderOffset - 1, borderOffset + yOffset - 1, 220 + 2, 64 + 2), borderColor); // vertical line - canvas.drawLine(Point(295, borderOffset + yOffset - 1), Point(295, 445), borderColor, borderColor); + canvas.drawLine(Point(295, borderOffset + yOffset - 1), Point(295, borderOffset + yOffset - 2 + 439), borderColor, borderColor); // skill header canvas.drawColorBlended(Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20), ColorRGBA(0, 0, 0, alpha)); @@ -122,7 +124,7 @@ void CHeroOverview::genHeader() canvas.drawBorder(Rect(302 - 1, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); // vertical line - canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32), Point(302 + (292 / 2), 445), borderColor, borderColor); + canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 2 + 254), borderColor, borderColor); // spell title canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); @@ -163,8 +165,9 @@ void CHeroOverview::genHeader() labelSkillHeader.push_back(std::make_shared((284 / 4) * i + 42, 2 * borderOffset + yOffset + 64 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); // skill + const int tmp[] = {0, 1, 2, 5}; for(int i = 0; i < 4; i++) - imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), (const int[]){0, 1, 2, 5}[i], 0, (284 / 4) * i + 21, 3 * borderOffset + yOffset + 64 + 20 + 1)); + imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), tmp[i], 0, (284 / 4) * i + 21, 3 * borderOffset + yOffset + 64 + 20 + 1)); // skills footer for(int i = 0; i < 4; i++) @@ -184,7 +187,7 @@ void CHeroOverview::genHeader() labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); // army title - labelArmyTitle = std::make_shared(302 + borderOffset, 2 * borderOffset + yOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Starting Army"); + labelArmyTitle = std::make_shared(302 + borderOffset, 2 * borderOffset + yOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); // army int i = 0; @@ -200,7 +203,7 @@ void CHeroOverview::genHeader() } // war machine title - labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 27, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: War Machine"); + labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 27, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); // war machine i = 0; @@ -220,19 +223,18 @@ void CHeroOverview::genHeader() } // secskill title - labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Sek. Skills"); + labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); // spell title - labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, "TODO: Spells"); + labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); // secskill i = 0; for(auto & skill : (*CGI->heroh)[heroIndex]->secSkillsInit) { - imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex(), 0, 302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset))); + imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset))); labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); - i++; } @@ -240,15 +242,17 @@ void CHeroOverview::genHeader() i = 0; for(auto & spell : (*CGI->heroh)[heroIndex]->spells) { + if(i == 0) + { + if((*CGI->heroh)[heroIndex]->haveSpellBook) + { + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), 0)); + } + i++; + } + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), 0)); labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); - i++; } -} - -void CHeroOverview::genHeroWindow() -{ - pos = Rect(0, 0, 600, 450 + 35); - genHeader(); } \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index b94d794a1..6957adee1 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -53,7 +53,6 @@ class CHeroOverview : public CWindowObject std::vector> imageSecSkills; std::vector> labelSecSkillsNames; - void genHeader(); void genHeroWindow(); public: From d33101187b1ebc6c0548566b5bce800c7aa75428 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 16:55:26 +0200 Subject: [PATCH 0463/1248] simplify --- client/windows/CHeroOverview.cpp | 120 +++++++++++++++---------------- client/windows/CHeroOverview.h | 8 ++- 2 files changed, 66 insertions(+), 62 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 53b9ceb84..857151849 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -34,24 +34,19 @@ CHeroOverview::CHeroOverview(const HeroTypeID & h) heroIndex = hero.getNum(); - pos = Rect(0, 0, 600, 450 + 35); + pos = Rect(0, 0, 600, 485); - genHeroWindow(); + genBackground(); + genControls(); center(); } -void CHeroOverview::genHeroWindow() +void CHeroOverview::genBackground() { backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); - int yOffset = 35; - int borderOffset = 5; - int alpha = 75; - - ColorRGBA borderColor = ColorRGBA(128, 100, 75); - Canvas canvas = Canvas(Point(pos.w, pos.h)); // hero image @@ -73,23 +68,23 @@ void CHeroOverview::genHeroWindow() canvas.drawBorder(Rect((284 / 4) * i + 21 - 1, 3 * borderOffset + yOffset + 64 + 20 + 1 - 1, 42 + 2, 42 + 2), borderColor); // skill footer - canvas.drawColorBlended(Rect(borderOffset, 4 * borderOffset + yOffset + 64 + 20 + 44, 284, 20), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 4 * borderOffset + yOffset + 64 + 20 + 44 - 1, 284 + 2, 20 + 2), borderColor); + canvas.drawColorBlended(Rect(borderOffset, 4 * borderOffset + yOffset + 128, 284, 20), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 4 * borderOffset + yOffset + 128 - 1, 284 + 2, 20 + 2), borderColor); // hero biography - canvas.drawColorBlended(Rect(borderOffset, 5 * borderOffset + yOffset + 64 + 20 + 44 + 20, 284, 130), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 5 * borderOffset + yOffset + 64 + 20 + 44 + 20 - 1, 284 + 2, 130 + 2), borderColor); + canvas.drawColorBlended(Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 5 * borderOffset + yOffset + 148 - 1, 284 + 2, 130 + 2), borderColor); // speciality name - canvas.drawColorBlended(Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130, 235, 44), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(2 * borderOffset + 44 - 1, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 - 1, 235 + 2, 44 + 2), borderColor); + canvas.drawColorBlended(Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(2 * borderOffset + 44 - 1, 6 * borderOffset + yOffset + 278 - 1, 235 + 2, 44 + 2), borderColor); // speciality image - canvas.drawBorder(Rect(borderOffset - 1, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 - 1, 44 + 2, 44 + 2), borderColor); + canvas.drawBorder(Rect(borderOffset - 1, 6 * borderOffset + yOffset + 278 - 1, 44 + 2, 44 + 2), borderColor); // speciality description - canvas.drawColorBlended(Rect(borderOffset, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44, 284, 85), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 + 2, 85 + 2), borderColor); + canvas.drawColorBlended(Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(borderOffset - 1, 7 * borderOffset + yOffset + 322 - 1, 284 + 2, 85 + 2), borderColor); // army title canvas.drawColorBlended(Rect(302, borderOffset + yOffset, 292, 30), ColorRGBA(0, 0, 0, alpha)); @@ -98,71 +93,74 @@ void CHeroOverview::genHeroWindow() // army for(int i = 0; i < 6; i++) { - int space = (292 - 32 - 6 * 32) / 5; - canvas.drawColorBlended(Rect(302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + i * (32 + space) + 16 - 1, 2 * borderOffset + yOffset + 30 - 1, 32 + 2, 32 + 2), borderColor); + int space = (260 - 6 * 32) / 5; + canvas.drawColorBlended(Rect(318 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(318 + i * (32 + space) - 1, 2 * borderOffset + yOffset + 30 - 1, 32 + 2, 32 + 2), borderColor); } // army footer - canvas.drawColorBlended(Rect(302, 3 * borderOffset + yOffset + 30 + 32, 292, 20), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 3 * borderOffset + yOffset + 30 + 32 - 1, 292 + 2, 20 + 2), borderColor); + canvas.drawColorBlended(Rect(302, 3 * borderOffset + yOffset + 62, 292, 20), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 3 * borderOffset + yOffset + 62 - 1, 292 + 2, 20 + 2), borderColor); // warmachine title - canvas.drawColorBlended(Rect(302, 4 * borderOffset + yOffset + 30 + 32 + 20, 292, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 4 * borderOffset + yOffset + 30 + 32 + 20 - 1, 292 + 2, 30 + 2), borderColor); + canvas.drawColorBlended(Rect(302, 4 * borderOffset + yOffset + 82, 292, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 4 * borderOffset + yOffset + 82 - 1, 292 + 2, 30 + 2), borderColor); // warmachine for(int i = 0; i < 6; i++) { int space = (292 - 32 - 6 * 32) / 5; - canvas.drawColorBlended(Rect(302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + i * (32 + space) + 16 - 1, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30 - 1, 32 + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 112, 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(318 + i * (32 + space) - 1, 5 * borderOffset + yOffset + 112 - 1, 32 + 2, 32 + 2), borderColor); } // secskill title - canvas.drawColorBlended(Rect(302, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); + canvas.drawColorBlended(Rect(302, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 6 * borderOffset + yOffset + 144 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); // vertical line - canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 2 + 254), borderColor, borderColor); + canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 2 + 254), borderColor, borderColor); // spell title - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 6 * borderOffset + yOffset + 144 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); // secskill for(int i = 0; i < 6; i++) { - canvas.drawColorBlended(Rect(302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); - canvas.drawColorBlended(Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); } // spell for(int i = 0; i < 6; i++) { - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); + canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); + canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); } std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); backgroundShapes = std::make_shared(backgroundShapesImg, pos); +} +void CHeroOverview::genControls() +{ labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); // hero image imageHero = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); // hero name - labelHeroName = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); - labelHeroClass = std::make_shared(64 + borderOffset + 110, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); + labelHeroName = std::make_shared(borderOffset + 174, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); + labelHeroClass = std::make_shared(borderOffset + 174, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); // skills header for(int i = 0; i < 4; i++) - labelSkillHeader.push_back(std::make_shared((284 / 4) * i + 42, 2 * borderOffset + yOffset + 64 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); + labelSkillHeader.push_back(std::make_shared((284 / 4) * i + 42, 2 * borderOffset + yOffset + 74, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); // skill const int tmp[] = {0, 1, 2, 5}; @@ -171,20 +169,20 @@ void CHeroOverview::genHeroWindow() // skills footer for(int i = 0; i < 4; i++) - labelSkillFooter.push_back(std::make_shared((284 / 4) * i + 42, 4 * borderOffset + yOffset + 64 + 20 + 44 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIndex]->heroClass->primarySkillInitial[i]))); + labelSkillFooter.push_back(std::make_shared((284 / 4) * i + 42, 4 * borderOffset + yOffset + 138, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIndex]->heroClass->primarySkillInitial[i]))); // hero biography - labelHeroBiography = std::make_shared(Rect(2 * borderOffset, 5 * borderOffset + borderOffset + yOffset + 64 + 20 + 44 + 20, 284 - 2 * borderOffset, 130 - 2 * borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getBiographyTranslated()); + labelHeroBiography = std::make_shared(Rect(2 * borderOffset, 5 * borderOffset + borderOffset + yOffset + 148, 284 - 2 * borderOffset, 130 - 2 * borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getBiographyTranslated()); // speciality name - labelHeroSpeciality = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - labelSpecialityName = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); + labelHeroSpeciality = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 278, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + labelSpecialityName = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 298, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); // speciality image - imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, 6 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130); + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, 6 * borderOffset + yOffset + 278); // speciality description - labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 64 + 20 + 44 + 20 + 130 + 44 - 1, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); + labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 321, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); // army title labelArmyTitle = std::make_shared(302 + borderOffset, 2 * borderOffset + yOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); @@ -197,13 +195,13 @@ void CHeroOverview::genHeroWindow() { int space = (292 - 32 - 6 * 32) / 5; imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30)); - labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 30 + 32 + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); + labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 72, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); i++; } } // war machine title - labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 30 + 32 + 27, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); + labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 89, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); // war machine i = 0; @@ -212,29 +210,29 @@ void CHeroOverview::genHeroWindow() int space = (292 - 32 - 6 * 32) / 5; if(i == 0) { - imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30)); + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 112)); i++; } if((*CGI->creh)[army.creature]->warMachine != ArtifactID::NONE) { - imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 30 + 32 + 20 + 30)); + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 112)); i++; } } // secskill title - labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); + labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 152, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); // spell title - labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 8, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); + labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 152, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); // secskill i = 0; for(auto & skill : (*CGI->heroh)[heroIndex]->secSkillsInit) { - imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset))); - labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); - labelSecSkillsNames.push_back(std::make_shared(302 + 32 + 2 * borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); + imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset))); + labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); + labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); i++; } @@ -246,13 +244,13 @@ void CHeroOverview::genHeroWindow() { if((*CGI->heroh)[heroIndex]->haveSpellBook) { - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), 0)); + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), 0)); } i++; } - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset), 32, 32), 0)); - labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 30 + 32 + 20 + 30 + 32 + 30 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), 0)); + labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); i++; } } \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 6957adee1..c63c88ea5 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -23,6 +23,11 @@ class CHeroOverview : public CWindowObject const HeroTypeID & hero; int heroIndex; + const int yOffset = 35; + const int borderOffset = 5; + const int alpha = 75; + const ColorRGBA borderColor = ColorRGBA(128, 100, 75); + std::shared_ptr backgroundTexture; std::shared_ptr backgroundShapes; std::shared_ptr labelTitle; @@ -53,7 +58,8 @@ class CHeroOverview : public CWindowObject std::vector> imageSecSkills; std::vector> labelSecSkillsNames; - void genHeroWindow(); + void genBackground(); + void genControls(); public: CHeroOverview(const HeroTypeID & h); From 1c20431035ac8e2095fb9a7bc75d2b138ee32bd4 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 17 Sep 2023 13:12:25 +0000 Subject: [PATCH 0464/1248] Use 64px and 32px as icon sizes in launcher sidebar --- launcher/icons/menu-game.png | Bin 46588 -> 5579 bytes launcher/mainwindow_moc.ui | 26 +++++++++++++------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/launcher/icons/menu-game.png b/launcher/icons/menu-game.png index cc345dd51b06c5800c6df66cabd1e86518dfa129..71ae6ca544537f3aabd3cb5465e7386d7d37ac49 100644 GIT binary patch literal 5579 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 0 900 - 536 + 640
    @@ -62,8 +62,8 @@ - 60 - 60 + 64 + 64 @@ -112,8 +112,8 @@ - 60 - 60 + 64 + 64 @@ -162,8 +162,8 @@ - 60 - 60 + 64 + 64 @@ -212,8 +212,8 @@ - 30 - 30 + 32 + 32 @@ -281,8 +281,8 @@ - 30 - 30 + 32 + 32 @@ -334,8 +334,8 @@ - 60 - 60 + 64 + 64 From a517e6ad8efd4732ec3cdcb72cd62037806c6701 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 17 Sep 2023 18:02:24 +0200 Subject: [PATCH 0465/1248] Message split for pandora box --- lib/mapObjects/CGPandoraBox.cpp | 246 ++++++++++++++------------ lib/mapObjects/CGPandoraBox.h | 4 + lib/mapObjects/CRewardableObject.cpp | 91 +++++----- lib/mapObjects/CRewardableObject.h | 3 + lib/rewardable/Configuration.h | 3 +- mapeditor/inspector/rewardswidget.cpp | 12 +- 6 files changed, 194 insertions(+), 165 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 68187441c..b7538195a 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -28,7 +28,6 @@ VCMI_LIB_NAMESPACE_BEGIN void CGPandoraBox::init() { blockVisit = true; - configuration.selectMode = Rewardable::SELECT_ALL; for(auto & i : configuration.info) i.reward.removeObject = true; @@ -41,92 +40,130 @@ void CGPandoraBox::initObj(CRandomGenerator & rand) CRewardableObject::initObj(rand); } -void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const +void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, bool markAsVisit) const { - auto setText = [](MetaString & text, int tId, const CGHeroInstance * h) + auto vi = configuration.info.at(index); + if(!vi.message.empty()) { - text.appendLocalString(EMetaText::ADVOB_TXT, tId); - text.replaceRawString(h->getNameTranslated()); - }; - - for(auto i : getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT)) - { - MetaString txt; - auto & r = configuration.info[i]; - - if(r.reward.spells.size() == 1) - setText(txt, 184, h); - else if(!r.reward.spells.empty()) - setText(txt, 188, h); - - if(r.reward.heroExperience || !r.reward.secondary.empty()) - setText(txt, 175, h); - - for(int ps : r.reward.primary) - { - if(ps) - { - setText(txt, 175, h); - break; - } - } - - if(r.reward.manaDiff < 0) - setText(txt, 176, h); - if(r.reward.manaDiff > 0) - setText(txt, 177, h); - - for(auto b : r.reward.bonuses) - { - if(b.type == BonusType::MORALE) - { - if(b.val < 0) - setText(txt, 178, h); - if(b.val > 0) - setText(txt, 179, h); - } - if(b.type == BonusType::LUCK) - { - if(b.val < 0) - setText(txt, 180, h); - if(b.val > 0) - setText(txt, 181, h); - } - } - - for(auto res : r.reward.resources) - { - if(res < 0) - setText(txt, 182, h); - if(res > 0) - setText(txt, 183, h); - } - - if(!r.reward.artifacts.empty()) - setText(txt, 183, h); - - if(!r.reward.creatures.empty()) - { - MetaString loot; - for(auto c : r.reward.creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(c); - } - - if(r.reward.creatures.size() == 1 && r.reward.creatures[0].count == 1) - txt.appendLocalString(EMetaText::ADVOB_TXT, 185); - else - txt.appendLocalString(EMetaText::ADVOB_TXT, 186); - - txt.replaceRawString(loot.buildList()); - txt.replaceRawString(h->getNameTranslated()); - } - - if(r.message.empty()) - const_cast(r.message) = txt; + CRewardableObject::grantRewardWithMessage(h, index, markAsVisit); + return; } + //split reward message for pandora box + auto setText = [](bool cond, int posId, int negId, const CGHeroInstance * h) + { + MetaString text; + text.appendLocalString(EMetaText::ADVOB_TXT, cond ? posId : negId); + text.replaceRawString(h->getNameTranslated()); + return text; + }; + + auto sendInfoWindow = [h](const MetaString & text, const Rewardable::Reward & reward) + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text = text; + reward.loadComponents(iw.components, h); + iw.type = EInfoWindowMode::MODAL; + if(!iw.components.empty()) + cb->showInfoDialog(&iw); + }; + + Rewardable::Reward temp; + temp.spells = vi.reward.spells; + temp.heroExperience = vi.reward.heroExperience; + temp.heroLevel = vi.reward.heroLevel; + temp.primary = vi.reward.primary; + temp.secondary = vi.reward.secondary; + temp.bonuses = vi.reward.bonuses; + temp.manaDiff = vi.reward.manaDiff; + temp.manaPercentage = vi.reward.manaPercentage; + + MetaString txt; + if(!vi.reward.spells.empty()) + txt = setText(temp.spells.size() == 1, 184, 188, h); + + if(vi.reward.heroExperience || vi.reward.heroLevel || !vi.reward.secondary.empty()) + txt = setText(true, 175, 175, h); + + for(int i : vi.reward.primary) + { + if(i) + { + txt = setText(true, 175, 175, h); + break; + } + } + + if(vi.reward.manaDiff || vi.reward.manaPercentage) + txt = setText(temp.manaDiff > 0, 177, 176, h); + + for(auto b : vi.reward.bonuses) + { + if(b.val && b.type == BonusType::MORALE) + txt = setText(b.val > 0, 179, 178, h); + if(b.val && b.type == BonusType::LUCK) + txt = setText(b.val > 0, 181, 180, h); + } + sendInfoWindow(txt, temp); + + //resource message + temp = Rewardable::Reward{}; + temp.resources = vi.reward.resources; + sendInfoWindow(setText(vi.reward.resources.marketValue() > 0, 183, 182, h), temp); + + //artifacts message + temp = Rewardable::Reward{}; + temp.artifacts = vi.reward.artifacts; + sendInfoWindow(setText(true, 183, 183, h), temp); + + //creatures message + temp = Rewardable::Reward{}; + temp.creatures = vi.reward.creatures; + txt.clear(); + if(!vi.reward.creatures.empty()) + { + MetaString loot; + for(auto c : vi.reward.creatures) + { + loot.appendRawString("%s"); + loot.replaceCreatureName(c); + } + + if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].count == 1) + txt.appendLocalString(EMetaText::ADVOB_TXT, 185); + else + txt.appendLocalString(EMetaText::ADVOB_TXT, 186); + + txt.replaceRawString(loot.buildList()); + txt.replaceRawString(h->getNameTranslated()); + } + sendInfoWindow(txt, temp); + + //everything else + temp = vi.reward; + temp.heroExperience = 0; + temp.heroLevel = 0; + temp.secondary.clear(); + temp.primary.clear(); + temp.resources.amin(0); + temp.resources.amax(0); + temp.manaDiff = 0; + temp.manaPercentage = 0; + temp.spells.clear(); + temp.creatures.clear(); + temp.bonuses.clear(); + temp.artifacts.clear(); + sendInfoWindow(setText(true, 175, 175, h), temp); + + // grant reward afterwards. Note that it may remove object + if(markAsVisit) + markAsVisited(h); + grantReward(index, h); +} + +void CGPandoraBox::onHeroVisit(const CGHeroInstance * h) const +{ BlockingDialog bd (true, false); bd.player = h->getOwner(); bd.text.appendLocalString(EMetaText::ADVOB_TXT, 14); @@ -171,61 +208,36 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { //backward compatibility CCreatureSet::serializeJson(handler, "guards", 7); - Rewardable::VisitInfo vinfo; + configuration.info.emplace_back(); + Rewardable::VisitInfo & vinfo = configuration.info.back(); vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - - auto addReward = [this, &vinfo](bool condition) - { - if(condition) - { - configuration.info.push_back(vinfo); - vinfo = Rewardable::VisitInfo{}; - vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - } - }; - int val; handler.serializeInt("experience", vinfo.reward.heroExperience, 0); - addReward(vinfo.reward.heroExperience); - handler.serializeInt("mana", vinfo.reward.manaDiff, 0); - addReward(vinfo.reward.manaDiff); + int val; handler.serializeInt("morale", val, 0); if(val) vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); - addReward(val); handler.serializeInt("luck", val, 0); if(val) vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); - addReward(val); vinfo.reward.resources.serializeJson(handler, "resources"); - addReward(vinfo.reward.resources.nonZero()); - { - bool updateReward = false; auto s = handler.enterStruct("primarySkills"); for(int idx = 0; idx < vinfo.reward.primary.size(); idx ++) { handler.serializeInt(NPrimarySkill::names[idx], vinfo.reward.primary[idx], 0); - updateReward |= bool(vinfo.reward.primary[idx]); } - addReward(updateReward); } handler.serializeIdArray("artifacts", vinfo.reward.artifacts); - addReward(!vinfo.reward.artifacts.empty()); - handler.serializeIdArray("spells", vinfo.reward.spells); - addReward(!vinfo.reward.spells.empty()); - handler.enterArray("creatures").serializeStruct(vinfo.reward.creatures); - addReward(!vinfo.reward.creatures.empty()); { - bool updateReward = false; auto s = handler.enterStruct("secondarySkills"); for(const auto & p : handler.getCurrent().Struct()) { @@ -247,9 +259,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) } vinfo.reward.secondary[rawId] = level; - updateReward = true; } - addReward(updateReward); } } } @@ -257,10 +267,18 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) void CGEvent::init() { blockVisit = false; - configuration.selectMode = Rewardable::SELECT_ALL; for(auto & i : configuration.info) + { i.reward.removeObject = removeAfterVisit; + if(!message.empty() && i.message.empty()) + i.message = MetaString::createFromRawString(message); + } +} + +void CGEvent::grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const +{ + CRewardableObject::grantRewardWithMessage(contextHero, rewardIndex, markAsVisit); } void CGEvent::onHeroVisit( const CGHeroInstance * h ) const diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index 240e9e68d..3d4cf3540 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -32,6 +32,8 @@ public: h & message; } protected: + void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const override; + virtual void init(); void serializeJsonOptions(JsonSerializeFormat & handler) override; }; @@ -55,6 +57,8 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; protected: + void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const override; + void init() override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 3b96dbe68..8df14fd7d 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -28,44 +28,45 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } +void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const +{ + auto vi = configuration.info.at(index); + logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); + // show message only if it is not empty or in infobox + if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty()) + { + InfoWindow iw; + iw.player = contextHero->tempOwner; + iw.text = vi.message; + vi.reward.loadComponents(iw.components, contextHero); + iw.type = configuration.infoWindowType; + if(!iw.components.empty() || !iw.text.toString().empty()) + cb->showInfoDialog(&iw); + } + // grant reward afterwards. Note that it may remove object + if(markAsVisit) + markAsVisited(contextHero); + grantReward(index, contextHero); +} + +void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndeces, const MetaString & dialog) const +{ + BlockingDialog sd(configuration.canRefuse, rewardIndeces.size() > 1); + sd.player = contextHero->tempOwner; + sd.text = dialog; + + if (rewardIndeces.size() > 1) + for (auto index : rewardIndeces) + sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); + + if (rewardIndeces.size() == 1) + configuration.info.at(rewardIndeces.front()).reward.loadComponents(sd.components, contextHero); + + cb->showBlockingDialog(&sd); +} + void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { - auto grantRewardWithMessage = [&](int index, bool markAsVisit) -> void - { - auto vi = configuration.info.at(index); - logGlobal->debug("Granting reward %d. Message says: %s", index, vi.message.toString()); - // show message only if it is not empty or in infobox - if (configuration.infoWindowType != EInfoWindowMode::MODAL || !vi.message.toString().empty()) - { - InfoWindow iw; - iw.player = h->tempOwner; - iw.text = vi.message; - vi.reward.loadComponents(iw.components, h); - iw.type = configuration.infoWindowType; - if(!iw.components.empty() || !iw.text.toString().empty()) - cb->showInfoDialog(&iw); - } - // grant reward afterwards. Note that it may remove object - if(markAsVisit) - markAsVisited(h); - grantReward(index, h); - }; - auto selectRewardsMessage = [&](const std::vector & rewards, const MetaString & dialog) -> void - { - BlockingDialog sd(configuration.canRefuse, rewards.size() > 1); - sd.player = h->tempOwner; - sd.text = dialog; - - if (rewards.size() > 1) - for (auto index : rewards) - sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(h)); - - if (rewards.size() == 1) - configuration.info.at(rewards.front()).reward.loadComponents(sd.components, h); - - cb->showBlockingDialog(&sd); - }; - if(!wasVisitedBefore(h)) { auto rewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT); @@ -83,7 +84,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { auto emptyRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_NOT_AVAILABLE); if (!emptyRewards.empty()) - grantRewardWithMessage(emptyRewards[0], false); + grantRewardWithMessage(h, emptyRewards[0], false); else logMod->warn("No applicable message for visiting empty object!"); break; @@ -91,26 +92,22 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const case 1: // one reward. Just give it with message { if (configuration.canRefuse) - selectRewardsMessage(rewards, configuration.info.at(rewards.front()).message); + selectRewardWthMessage(h, rewards, configuration.info.at(rewards.front()).message); else - grantRewardWithMessage(rewards.front(), true); + grantRewardWithMessage(h, rewards.front(), true); break; } default: // multiple rewards. Act according to select mode { switch (configuration.selectMode) { case Rewardable::SELECT_PLAYER: // player must select - selectRewardsMessage(rewards, configuration.onSelect); + selectRewardWthMessage(h, rewards, configuration.onSelect); break; case Rewardable::SELECT_FIRST: // give first available - grantRewardWithMessage(rewards.front(), true); + grantRewardWithMessage(h, rewards.front(), true); break; case Rewardable::SELECT_RANDOM: // give random - grantRewardWithMessage(*RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); - break; - case Rewardable::SELECT_ALL: // give all rewards - for(auto i : rewards) - grantRewardWithMessage(i, i == rewards.size() - 1); + grantRewardWithMessage(h, *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true); break; } break; @@ -129,7 +126,7 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); if (!visitedRewards.empty()) - grantRewardWithMessage(visitedRewards[0], false); + grantRewardWithMessage(h, visitedRewards[0], false); else logMod->warn("No applicable message for visiting already visited object!"); } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 761e93702..1c8e97c05 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -33,6 +33,9 @@ protected: bool wasVisitedBefore(const CGHeroInstance * contextHero) const; void serializeJsonOptions(JsonSerializeFormat & handler) override; + + virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; + virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndeces, const MetaString & dialog) const; public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index de8764f86..72377da04 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -35,7 +35,6 @@ enum ESelectMode SELECT_FIRST, // first reward that matches limiters SELECT_PLAYER, // player can select from all allowed rewards SELECT_RANDOM, // one random reward from all mathing limiters - SELECT_ALL, // provides all allowed rewards matching limiters }; enum class EEventType @@ -46,7 +45,7 @@ enum class EEventType EVENT_NOT_AVAILABLE }; -const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom", "selectAll"}; +const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom"}; const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "player"}; struct DLL_LINKAGE ResetInfo diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 36813172f..84dd7e09b 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -147,12 +147,19 @@ RewardsWidget::RewardsWidget(const CMap & m, CRewardableObject & p, QWidget *par { ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); ui->visitMode->setEnabled(false); - ui->selectMode->setCurrentIndex(vstd::find_pos(Rewardable::SelectModeString, "selectAll")); + ui->selectMode->setCurrentIndex(vstd::find_pos(Rewardable::SelectModeString, "selectFirst")); ui->selectMode->setEnabled(false); ui->windowMode->setEnabled(false); ui->canRefuse->setEnabled(false); } + if(auto * e = dynamic_cast(&object)) + { + ui->selectMode->setEnabled(true); + if(!e->removeAfterVisit) + ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "unlimited")); + } + if(dynamic_cast(&object)) { ui->visitMode->setCurrentIndex(vstd::find_pos(Rewardable::VisitModeString, "once")); @@ -512,7 +519,8 @@ void RewardsWidget::on_removeVisitInfo_clicked() delete ui->visitInfoList->currentItem(); ui->visitInfoList->blockSignals(false); on_visitInfoList_itemSelectionChanged(); - loadCurrentVisitInfo(ui->visitInfoList->currentRow()); + if(ui->visitInfoList->currentItem()) + loadCurrentVisitInfo(ui->visitInfoList->currentRow()); } void RewardsWidget::on_selectMode_currentIndexChanged(int index) From 05fad115b5232174d350e34d1a733e9f136c807b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 20:34:31 +0200 Subject: [PATCH 0466/1248] Added new Widgets an functions for simplification --- client/widgets/MiscWidgets.cpp | 28 ++++++++++++++++++++++++++++ client/widgets/MiscWidgets.h | 21 +++++++++++++++++++++ client/windows/CHeroOverview.cpp | 1 + client/windows/CHeroOverview.h | 2 ++ lib/Rect.h | 5 +++++ 5 files changed, 57 insertions(+) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index e0a5278e8..e3f11ebe2 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -580,3 +580,31 @@ void CCreaturePic::setAmount(int newAmount) else amount->setText(""); } + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color) : + color(color), colorLine(ColorRGBA()), drawLine(false) +{ + pos = position + pos.topLeft(); +} + +TransparentFilledRectangle::TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine) : + color(color), colorLine(colorLine), drawLine(true) +{ + pos = position + pos.topLeft(); +} + +void TransparentFilledRectangle::showAll(Canvas & to) +{ + to.drawColorBlended(pos, color); + if(drawLine) + to.drawBorder(pos, colorLine); +} + +SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) : + pos1(pos1), pos2(pos2), color(color) +{} + +void SimpleLine::showAll(Canvas & to) +{ + to.drawLine(pos1 + pos.topLeft(), pos2 + pos.topLeft(), color, color); +} \ No newline at end of file diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 4a8b79c8f..53b866207 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -226,3 +226,24 @@ public: MoraleLuckBox(bool Morale, const Rect &r, bool Small=false); }; + +class TransparentFilledRectangle : public CIntObject +{ + ColorRGBA color; + ColorRGBA colorLine; + bool drawLine; +public: + TransparentFilledRectangle(Rect position, ColorRGBA color); + TransparentFilledRectangle(Rect position, ColorRGBA color, ColorRGBA colorLine); + void showAll(Canvas & to) override; +}; + +class SimpleLine : public CIntObject +{ + Point pos1; + Point pos2; + ColorRGBA color; +public: + SimpleLine(Point pos1, Point pos2, ColorRGBA color); + void showAll(Canvas & to) override; +}; \ No newline at end of file diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 857151849..3db29159f 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -20,6 +20,7 @@ #include "../widgets/CComponent.h" #include "../widgets/Images.h" #include "../widgets/TextControls.h" +#include "../widgets/MiscWidgets.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CCreatureHandler.h" diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index c63c88ea5..37bf3bd91 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -17,6 +17,8 @@ class CFilledTexture; class CAnimImage; class CComponentBox; class CTextBox; +class TransparentFilledRectangle; +class SimpleLine; class CHeroOverview : public CWindowObject { diff --git a/lib/Rect.h b/lib/Rect.h index 50d514844..660a9f222 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -98,6 +98,11 @@ public: return Point(w,h); } + Rect resize(const int size) const + { + return Rect(x-size,y-size,w+size,h+size); + } + void moveTo(const Point & dest) { x = dest.x; From 21f37cfafb0afa38a8bb2c9205986d156046efcb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:41:55 +0200 Subject: [PATCH 0467/1248] simplified --- client/windows/CHeroOverview.cpp | 216 +++++++++++++------------------ client/windows/CHeroOverview.h | 8 +- lib/Rect.h | 2 +- 3 files changed, 99 insertions(+), 127 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 3db29159f..16855bd38 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -33,7 +33,7 @@ CHeroOverview::CHeroOverview(const HeroTypeID & h) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - heroIndex = hero.getNum(); + heroIdx = hero.getNum(); pos = Rect(0, 0, 600, 485); @@ -47,150 +47,91 @@ void CHeroOverview::genBackground() { backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); updateShadow(); +} - Canvas canvas = Canvas(Point(pos.w, pos.h)); +void CHeroOverview::genControls() +{ + Rect r = Rect(); + + labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); // hero image - canvas.drawBorder(Rect(borderOffset - 1, borderOffset + yOffset - 1, 58 + 2, 64 + 2), borderColor); + r = Rect(borderOffset, borderOffset + yOffset, 58, 64); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageHero = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIdx]->imageIndex, 0, r.x, r.y); // hero name - canvas.drawColorBlended(Rect(64 + borderOffset, borderOffset + yOffset, 220, 64), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(64 + borderOffset - 1, borderOffset + yOffset - 1, 220 + 2, 64 + 2), borderColor); + r = Rect(64 + borderOffset, borderOffset + yOffset, 220, 64); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroName = std::make_shared(r.x + 110, r.y + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIdx]->getNameTranslated()); + labelHeroClass = std::make_shared(r.x + 110, r.y + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIdx]->heroClass->getNameTranslated()); // vertical line - canvas.drawLine(Point(295, borderOffset + yOffset - 1), Point(295, borderOffset + yOffset - 2 + 439), borderColor, borderColor); + backgroundLines.push_back(std::make_shared(Point(295, borderOffset + yOffset - 1), Point(295, borderOffset + yOffset - 2 + 439), borderColor)); - // skill header - canvas.drawColorBlended(Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 2 * borderOffset + yOffset + 64 - 1, 284 + 2, 20 + 2), borderColor); + // skills header + r = Rect(borderOffset, 2 * borderOffset + yOffset + 64, 284, 20); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + for(int i = 0; i < 4; i++) + labelSkillHeader.push_back(std::make_shared((r.w / 4) * i + 42, r.y + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); // skill + const int tmp[] = {0, 1, 2, 5}; for(int i = 0; i < 4; i++) - canvas.drawBorder(Rect((284 / 4) * i + 21 - 1, 3 * borderOffset + yOffset + 64 + 20 + 1 - 1, 42 + 2, 42 + 2), borderColor); + { + r = Rect((284 / 4) * i + 21, 3 * borderOffset + yOffset + 85, 42, 42); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), tmp[i], 0, r.x, r.y)); + } - // skill footer - canvas.drawColorBlended(Rect(borderOffset, 4 * borderOffset + yOffset + 128, 284, 20), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 4 * borderOffset + yOffset + 128 - 1, 284 + 2, 20 + 2), borderColor); + // skills footer + r = Rect(borderOffset, 4 * borderOffset + yOffset + 128, 284, 20); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + for(int i = 0; i < 4; i++) + { + r = Rect((284 / 4) * i + 42, r.y, r.w, r.h); + labelSkillFooter.push_back(std::make_shared(r.x, r.y + 10, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIdx]->heroClass->primarySkillInitial[i]))); + } // hero biography - canvas.drawColorBlended(Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 5 * borderOffset + yOffset + 148 - 1, 284 + 2, 130 + 2), borderColor); + r = Rect(borderOffset, 5 * borderOffset + yOffset + 148, 284, 130); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroBiography = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getBiographyTranslated()); // speciality name - canvas.drawColorBlended(Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(2 * borderOffset + 44 - 1, 6 * borderOffset + yOffset + 278 - 1, 235 + 2, 44 + 2), borderColor); + r = Rect(2 * borderOffset + 44, 6 * borderOffset + yOffset + 278, 235, 44); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelHeroSpeciality = std::make_shared(r.x + borderOffset, r.y + borderOffset, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); + labelSpecialityName = std::make_shared(r.x + borderOffset, r.y + borderOffset + 20, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyNameTranslated()); // speciality image - canvas.drawBorder(Rect(borderOffset - 1, 6 * borderOffset + yOffset + 278 - 1, 44 + 2, 44 + 2), borderColor); + r = Rect(borderOffset, 6 * borderOffset + yOffset + 278, 44, 44); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIdx]->imageIndex, 0, r.x, r.y); // speciality description - canvas.drawColorBlended(Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(borderOffset - 1, 7 * borderOffset + yOffset + 322 - 1, 284 + 2, 85 + 2), borderColor); + r = Rect(borderOffset, 7 * borderOffset + yOffset + 322, 284, 85); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSpecialityDescription = std::make_shared(r.resize(-borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIdx]->getSpecialtyDescriptionTranslated()); // army title - canvas.drawColorBlended(Rect(302, borderOffset + yOffset, 292, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, borderOffset + yOffset - 1, 292 + 2, 30 + 2), borderColor); + r = Rect(302, borderOffset + yOffset, 292, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelArmyTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); + + // army numbers + r = Rect(302, 3 * borderOffset + yOffset + 62, 292, 20); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); // army for(int i = 0; i < 6; i++) { int space = (260 - 6 * 32) / 5; - canvas.drawColorBlended(Rect(318 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(318 + i * (32 + space) - 1, 2 * borderOffset + yOffset + 30 - 1, 32 + 2, 32 + 2), borderColor); + r = Rect(318 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } - - // army footer - canvas.drawColorBlended(Rect(302, 3 * borderOffset + yOffset + 62, 292, 20), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 3 * borderOffset + yOffset + 62 - 1, 292 + 2, 20 + 2), borderColor); - - // warmachine title - canvas.drawColorBlended(Rect(302, 4 * borderOffset + yOffset + 82, 292, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 4 * borderOffset + yOffset + 82 - 1, 292 + 2, 30 + 2), borderColor); - - // warmachine - for(int i = 0; i < 6; i++) - { - int space = (292 - 32 - 6 * 32) / 5; - canvas.drawColorBlended(Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 112, 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(318 + i * (32 + space) - 1, 5 * borderOffset + yOffset + 112 - 1, 32 + 2, 32 + 2), borderColor); - } - - // secskill title - canvas.drawColorBlended(Rect(302, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 6 * borderOffset + yOffset + 144 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); - - // vertical line - canvas.drawLine(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 2 + 254), borderColor, borderColor); - - // spell title - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 6 * borderOffset + yOffset + 144 - 1, (292 / 2) - 2 * borderOffset + 2, 30 + 2), borderColor); - - // secskill - for(int i = 0; i < 6; i++) - { - canvas.drawColorBlended(Rect(302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); - canvas.drawColorBlended(Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); - } - - // spell - for(int i = 0; i < 6; i++) - { - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, 32 + 2, 32 + 2), borderColor); - canvas.drawColorBlended(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32), ColorRGBA(0, 0, 0, alpha)); - canvas.drawBorder(Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset - 1, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 1, (292 / 2) - 32 - 3 * borderOffset + 2, 32 + 2), borderColor); - } - - std::shared_ptr backgroundShapesImg = GH.renderHandler().createImage(canvas.getInternalSurface()); - backgroundShapes = std::make_shared(backgroundShapesImg, pos); -} - -void CHeroOverview::genControls() -{ - labelTitle = std::make_shared(pos.w / 2 + 8, 21, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[77]); - - // hero image - imageHero = std::make_shared(AnimationPath::builtin("PortraitsLarge"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, borderOffset + yOffset); - - // hero name - labelHeroName = std::make_shared(borderOffset + 174, borderOffset + yOffset + 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, (*CGI->heroh)[heroIndex]->getNameTranslated()); - labelHeroClass = std::make_shared(borderOffset + 174, borderOffset + yOffset + 45, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated()); - - // skills header - for(int i = 0; i < 4; i++) - labelSkillHeader.push_back(std::make_shared((284 / 4) * i + 42, 2 * borderOffset + yOffset + 74, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[1 + i])); - - // skill - const int tmp[] = {0, 1, 2, 5}; - for(int i = 0; i < 4; i++) - imageSkill.push_back(std::make_shared(AnimationPath::builtin("PSKIL42"), tmp[i], 0, (284 / 4) * i + 21, 3 * borderOffset + yOffset + 64 + 20 + 1)); - - // skills footer - for(int i = 0; i < 4; i++) - labelSkillFooter.push_back(std::make_shared((284 / 4) * i + 42, 4 * borderOffset + yOffset + 138, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string((*CGI->heroh)[heroIndex]->heroClass->primarySkillInitial[i]))); - - // hero biography - labelHeroBiography = std::make_shared(Rect(2 * borderOffset, 5 * borderOffset + borderOffset + yOffset + 148, 284 - 2 * borderOffset, 130 - 2 * borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getBiographyTranslated()); - - // speciality name - labelHeroSpeciality = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 278, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - labelSpecialityName = std::make_shared(3 * borderOffset + 44, 7 * borderOffset + yOffset + 298, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); - - // speciality image - imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, borderOffset, 6 * borderOffset + yOffset + 278); - - // speciality description - labelSpecialityDescription = std::make_shared(Rect(2 * borderOffset, 8 * borderOffset + yOffset + 321, 284 - borderOffset, 85 - borderOffset), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyDescriptionTranslated()); - - // army title - labelArmyTitle = std::make_shared(302 + borderOffset, 2 * borderOffset + yOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); - - // army int i = 0; - for(auto & army : (*CGI->heroh)[heroIndex]->initialArmy) + for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) { if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) { @@ -202,11 +143,19 @@ void CHeroOverview::genControls() } // war machine title - labelWarMachineTitle = std::make_shared(302 + borderOffset, 4 * borderOffset + yOffset + 89, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); + r = Rect(302, 4 * borderOffset + yOffset + 82, 292, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelWarMachineTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); // war machine + for(int i = 0; i < 6; i++) + { + int space = (292 - 32 - 6 * 32) / 5; + r = Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 112, 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } i = 0; - for(auto & army : (*CGI->heroh)[heroIndex]->initialArmy) + for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) { int space = (292 - 32 - 6 * 32) / 5; if(i == 0) @@ -222,14 +171,28 @@ void CHeroOverview::genControls() } // secskill title - labelSecSkillTitle = std::make_shared(302 + borderOffset, 6 * borderOffset + yOffset + 152, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); + r = Rect(302, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSecSkillTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); + + // vertical line + backgroundLines.push_back(std::make_shared(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 2 + 254), borderColor)); // spell title - labelSpellTitle = std::make_shared(302 + (292 / 2) + 3 * borderOffset, 6 * borderOffset + yOffset + 152, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); + r = Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + labelSpellTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); // secskill + for(int i = 0; i < 6; i++) + { + r = Rect(302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + r = Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } i = 0; - for(auto & skill : (*CGI->heroh)[heroIndex]->secSkillsInit) + for(auto & skill : (*CGI->heroh)[heroIdx]->secSkillsInit) { imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset))); labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); @@ -238,12 +201,19 @@ void CHeroOverview::genControls() } // spell + for(int i = 0; i < 6; i++) + { + r = Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + r = Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32); + backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + } i = 0; - for(auto & spell : (*CGI->heroh)[heroIndex]->spells) + for(auto & spell : (*CGI->heroh)[heroIdx]->spells) { if(i == 0) { - if((*CGI->heroh)[heroIndex]->haveSpellBook) + if((*CGI->heroh)[heroIdx]->haveSpellBook) { imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), 0)); } diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 37bf3bd91..9710ec857 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -23,15 +23,17 @@ class SimpleLine; class CHeroOverview : public CWindowObject { const HeroTypeID & hero; - int heroIndex; + int heroIdx; const int yOffset = 35; const int borderOffset = 5; - const int alpha = 75; + const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); const ColorRGBA borderColor = ColorRGBA(128, 100, 75); std::shared_ptr backgroundTexture; - std::shared_ptr backgroundShapes; + std::vector> backgroundRectangles; + std::vector> backgroundLines; + std::shared_ptr labelTitle; std::shared_ptr imageHero; std::shared_ptr labelHeroName; diff --git a/lib/Rect.h b/lib/Rect.h index 660a9f222..b14d5e131 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -100,7 +100,7 @@ public: Rect resize(const int size) const { - return Rect(x-size,y-size,w+size,h+size); + return Rect(x-size,y-size,w+2*size,h+2*size); } void moveTo(const Point & dest) From 7978e5a9b7deb291e021ec8f686616cad04ec396 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 21:55:35 +0200 Subject: [PATCH 0468/1248] adjusted army and warmachine space --- client/windows/CHeroOverview.cpp | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 16855bd38..2ab4b9efd 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -124,9 +124,9 @@ void CHeroOverview::genControls() backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); // army - for(int i = 0; i < 6; i++) + int space = (260 - 7 * 32) / 6; + for(int i = 0; i < 7; i++) { - int space = (260 - 6 * 32) / 5; r = Rect(318 + i * (32 + space), 2 * borderOffset + yOffset + 30, 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } @@ -135,7 +135,6 @@ void CHeroOverview::genControls() { if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) { - int space = (292 - 32 - 6 * 32) / 5; imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30)); labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 72, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); i++; @@ -148,16 +147,15 @@ void CHeroOverview::genControls() labelWarMachineTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); // war machine - for(int i = 0; i < 6; i++) + space = (260 - 4 * 32) / 3; + for(int i = 0; i < 4; i++) { - int space = (292 - 32 - 6 * 32) / 5; r = Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 112, 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } i = 0; for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) { - int space = (292 - 32 - 6 * 32) / 5; if(i == 0) { imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 112)); @@ -188,7 +186,7 @@ void CHeroOverview::genControls() { r = Rect(302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); - r = Rect(302 + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32); + r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } i = 0; @@ -205,7 +203,7 @@ void CHeroOverview::genControls() { r = Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); - r = Rect(302 + (292 / 2) + 2 * borderOffset + 32 + borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), (292 / 2) - 32 - 3 * borderOffset, 32); + r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } i = 0; From 2960895041c4789b99134a44021118e0893b573d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 17 Sep 2023 22:19:45 +0200 Subject: [PATCH 0469/1248] Issues fixed --- lib/CCreatureSet.cpp | 5 +- lib/CCreatureSet.h | 7 ++- lib/mapObjects/CArmedInstance.cpp | 6 +++ lib/mapObjects/CArmedInstance.h | 3 ++ lib/mapObjects/CGHeroInstance.cpp | 5 +- lib/mapObjects/CGPandoraBox.cpp | 32 ++++++++++-- lib/mapObjects/CGTownInstance.cpp | 7 ++- lib/mapObjects/CQuest.cpp | 2 +- lib/mapObjects/CQuest.h | 2 +- lib/mapObjects/MiscObjects.cpp | 12 +++-- lib/rewardable/Limiter.cpp | 52 +++---------------- lib/rewardable/Reward.cpp | 74 +++++---------------------- lib/serializer/JsonSerializeFormat.h | 32 ++++++++++-- mapeditor/inspector/rewardswidget.cpp | 10 +++- 14 files changed, 117 insertions(+), 132 deletions(-) diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index ec09548c0..5bc12977f 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -624,12 +624,13 @@ void CCreatureSet::armyChanged() } -void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName, const std::optional fixedSize) +void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize) { if(handler.saving && stacks.empty()) return; - auto a = handler.enterArray(fieldName); + handler.serializeEnum("formation", formation, NArmyFormation::names); + auto a = handler.enterArray(armyFieldName); if(handler.saving) diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index e72d0eb14..2c4944f05 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -206,6 +206,11 @@ enum class EArmyFormation : uint8_t TIGHT }; +namespace NArmyFormation +{ + static const std::vector names{ "wide", "tight" }; +} + class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures { CCreatureSet(const CCreatureSet &) = delete; @@ -284,7 +289,7 @@ public: h & formation; } - void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName, const std::optional fixedSize = std::nullopt); + void serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize = std::nullopt); operator bool() const { diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 37f7fd950..bc13a206d 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -160,4 +160,10 @@ const IBonusBearer* CArmedInstance::getBonusBearer() const return this; } +void CArmedInstance::serializeJsonOptions(JsonSerializeFormat & handler) +{ + CGObjectInstance::serializeJsonOptions(handler); + CCreatureSet::serializeJson(handler, "army", 7); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.h b/lib/mapObjects/CArmedInstance.h index e14447c55..378dcf96c 100644 --- a/lib/mapObjects/CArmedInstance.h +++ b/lib/mapObjects/CArmedInstance.h @@ -18,6 +18,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BattleInfo; class CGameState; +class JsonSerializeFormat; class DLL_LINKAGE CArmedInstance: public CGObjectInstance, public CBonusSystemNode, public CCreatureSet, public IConstBonusProvider { @@ -48,6 +49,8 @@ public: { return this->tempOwner; } + + void serializeJsonOptions(JsonSerializeFormat & handler) override; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index e8cde5e39..569ae1701 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1709,10 +1709,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) setHeroTypeName(typeName); } - static const std::vector FORMATIONS = { "wide", "tight" }; - - CCreatureSet::serializeJson(handler, "army", 7); - handler.serializeEnum("formation", formation, FORMATIONS); + CArmedInstance::serializeJsonOptions(handler); { static constexpr int NO_PATROLING = -1; diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index b7538195a..48910fa20 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -28,9 +28,15 @@ VCMI_LIB_NAMESPACE_BEGIN void CGPandoraBox::init() { blockVisit = true; + configuration.info.emplace_back(); + configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; for(auto & i : configuration.info) + { i.reward.removeObject = true; + if(!message.empty() && i.message.empty()) + i.message = MetaString::createFromRawString(message); + } } void CGPandoraBox::initObj(CRandomGenerator & rand) @@ -202,14 +208,17 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { CRewardableObject::serializeJsonOptions(handler); + handler.serializeString("guardMessage", message); if(!handler.saving) { - //backward compatibility - CCreatureSet::serializeJson(handler, "guards", 7); - configuration.info.emplace_back(); - Rewardable::VisitInfo & vinfo = configuration.info.back(); + //backward compatibility for VCMI maps that use old Pandora Box format + if(!handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); + + bool hasSomething = false; + Rewardable::VisitInfo vinfo; vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; handler.serializeInt("experience", vinfo.reward.heroExperience, 0); @@ -230,6 +239,8 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) for(int idx = 0; idx < vinfo.reward.primary.size(); idx ++) { handler.serializeInt(NPrimarySkill::names[idx], vinfo.reward.primary[idx], 0); + if(vinfo.reward.primary[idx]) + hasSomething = true; } } @@ -261,6 +272,19 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) vinfo.reward.secondary[rawId] = level; } } + + hasSomething = hasSomething + || vinfo.reward.heroExperience + || vinfo.reward.manaDiff + || vinfo.reward.resources.nonZero() + || !vinfo.reward.bonuses.empty() + || !vinfo.reward.artifacts.empty() + || !vinfo.reward.secondary.empty() + || !vinfo.reward.artifacts.empty() + || !vinfo.reward.creatures.empty(); + + if(hasSomething) + configuration.info.push_back(vinfo); } } diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9363a29cb..b82fe8d68 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1093,11 +1093,10 @@ void CGTownInstance::reset() void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) { - static const std::vector FORMATIONS = { "wide", "tight" }; - CGObjectInstance::serializeJsonOwner(handler); - CCreatureSet::serializeJson(handler, "army", 7); - handler.serializeEnum("tightFormation", formation, FORMATIONS); + if(!handler.saving) + handler.serializeEnum("tightFormation", formation, NArmyFormation::names); //for old format + CArmedInstance::serializeJsonOptions(handler); handler.serializeString("name", name); { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 3dae6194b..13872ea81 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -799,7 +799,7 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(!handler.saving) { - //backward compatibility + //backward compatibility for VCMI maps that use old SeerHut format auto s = handler.enterStruct("reward"); const JsonNode & rewardsJson = handler.getCurrent(); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 7cb0df2bc..3885229dd 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -138,7 +138,7 @@ protected: void afterAddToMapCommon(CMap * map) const; }; -class DLL_LINKAGE CGSeerHut : public CRewardableObject, public IQuestObject //army is used when giving reward +class DLL_LINKAGE CGSeerHut : public CRewardableObject, public IQuestObject { public: std::string seerName; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 24c32a4e4..fe2b70f5e 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -196,7 +196,7 @@ void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) con void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "army", 7); + CArmedInstance::serializeJsonOptions(handler); if(isAbandoned()) { @@ -316,7 +316,9 @@ void CGResource::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGResource::serializeJsonOptions(JsonSerializeFormat & handler) { - CCreatureSet::serializeJson(handler, "guards", 7); + CArmedInstance::serializeJsonOptions(handler); + if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); handler.serializeInt("amount", amount, 0); handler.serializeString("guardMessage", message); } @@ -827,7 +829,9 @@ void CGArtifact::afterAddToMap(CMap * map) void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) { handler.serializeString("guardMessage", message); - CCreatureSet::serializeJson(handler, "guards" ,7); + CArmedInstance::serializeJsonOptions(handler); + if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) + CCreatureSet::serializeJson(handler, "guards", 7); if(handler.saving && ID == Obj::SPELL_SCROLL) { @@ -1233,7 +1237,7 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler) { handler.serializeBool("removableUnits", removableUnits); serializeJsonOwner(handler); - CCreatureSet::serializeJson(handler, "army", 7); + CArmedInstance::serializeJsonOptions(handler); } void CGMagi::reset() diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 9d837ccb8..bf3fb2911 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -133,56 +133,18 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); handler.enterArray("creatures").serializeStruct(creatures); - { - auto a = handler.enterArray("primary"); - a.syncSize(primary); - for(int i = 0; i < primary.size(); ++i) - a.serializeInt(i, primary[i]); - } - + handler.enterArray("primary").serializeArray(primary); { auto a = handler.enterArray("secondary"); - std::vector> fieldValue; - if(handler.saving) + std::vector> fieldValue(secondary.begin(), secondary.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - for(auto & i : secondary) - { - auto key = VLC->skillh->encodeSkill(i.first); - auto value = NSecondarySkill::levels.at(i.second); - fieldValue.emplace_back(key, value); - } - } + h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); + }); a.syncSize(fieldValue); - for(int i = 0; i < fieldValue.size(); ++i) - { - auto e = a.enterStruct(i); - e->serializeString("skill", fieldValue[i].first); - e->serializeString("level", fieldValue[i].second); - } - if(!handler.saving) - { - for(auto & i : fieldValue) - { - const int skillId = VLC->skillh->decodeSkill(i.first); - if(skillId < 0) - { - logGlobal->error("Invalid secondary skill %s", i.first); - continue; - } - - const int level = vstd::find_pos(NSecondarySkill::levels, i.second); - if(level < 0) - { - logGlobal->error("Invalid secondary skill level%s", i.second); - continue; - } - - secondary[SecondarySkill(skillId)] = level; - } - - } + secondary = std::map(fieldValue.begin(), fieldValue.end()); } - //sublimiters auto serializeSublimitersList = [&handler](const std::string & field, LimitersList & container) { diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 07da516cd..bbdbf3cdc 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -119,76 +119,28 @@ void Rewardable::Reward::serializeJson(JsonSerializeFormat & handler) handler.serializeIdArray("artifacts", artifacts); handler.serializeIdArray("spells", spells); handler.enterArray("creatures").serializeStruct(creatures); - { - auto a = handler.enterArray("primary"); - a.syncSize(primary); - for(int i = 0; i < primary.size(); ++i) - a.serializeInt(i, primary[i]); - } - + handler.enterArray("primary").serializeArray(primary); { auto a = handler.enterArray("secondary"); - std::vector> fieldValue; - if(handler.saving) + std::vector> fieldValue(secondary.begin(), secondary.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - for(auto & i : secondary) - { - auto key = VLC->skillh->encodeSkill(i.first); - auto value = NSecondarySkill::levels.at(i.second); - fieldValue.emplace_back(key, value); - } - } + h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); + }); a.syncSize(fieldValue); - for(int i = 0; i < fieldValue.size(); ++i) - { - auto e = a.enterStruct(i); - e->serializeString("skill", fieldValue[i].first); - e->serializeString("level", fieldValue[i].second); - } - if(!handler.saving) - { - for(auto & i : fieldValue) - { - const int skillId = VLC->skillh->decodeSkill(i.first); - if(skillId < 0) - { - logGlobal->error("Invalid secondary skill %s", i.first); - continue; - } - - const int level = vstd::find_pos(NSecondarySkill::levels, i.second); - if(level < 0) - { - logGlobal->error("Invalid secondary skill level%s", i.second); - continue; - } - - secondary[SecondarySkill(skillId)] = level; - } - - } + secondary = std::map(fieldValue.begin(), fieldValue.end()); } { auto a = handler.enterArray("creaturesChange"); - std::vector> fieldValue; - if(handler.saving) + std::vector> fieldValue(creaturesChange.begin(), creaturesChange.end()); + a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - for(auto & i : creaturesChange) - fieldValue.push_back(i); - } - a.syncSize(fieldValue); - for(int i = 0; i < fieldValue.size(); ++i) - { - auto e = a.enterStruct(i); - e->serializeId("creature", fieldValue[i].first, CreatureID{}); - e->serializeId("amount", fieldValue[i].second, CreatureID{}); - } - if(!handler.saving) - { - for(auto & i : fieldValue) - creaturesChange[i.first] = i.second; - } + h.serializeId("creature", e.first, CreatureID{}); + h.serializeId("amount", e.second, CreatureID{}); + }); + creaturesChange = std::map(fieldValue.begin(), fieldValue.end()); } { diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index 68533203a..c5b274005 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -74,18 +74,44 @@ public: ///String <-> Json string void serializeString(const size_t index, std::string & value); - ///vector of serializable <-> Json vector of structs + ///vector of anything int-convertible <-> Json vector of integers + template + void serializeArray(std::vector & value) + { + syncSize(value, JsonNode::JsonType::DATA_STRUCT); + + for(size_t idx = 0; idx < size(); idx++) + serializeInt(idx, value[idx]); + } + + ///vector of strings <-> Json vector of strings + void serializeArray(std::vector & value) + { + syncSize(value, JsonNode::JsonType::DATA_STRUCT); + + for(size_t idx = 0; idx < size(); idx++) + serializeString(idx, value[idx]); + } + + ///vector of anything with custom serializing function <-> Json vector of structs template - void serializeStruct(std::vector & value) + void serializeStruct(std::vector & value, std::function serializer) { syncSize(value, JsonNode::JsonType::DATA_STRUCT); for(size_t idx = 0; idx < size(); idx++) { auto s = enterStruct(idx); - value[idx].serializeJson(*owner); + serializer(*owner, value[idx]); } } + + ///vector of serializable <-> Json vector of structs + template + void serializeStruct(std::vector & value) + { + serializeStruct(value, [](JsonSerializeFormat & h, Element & e){e.serializeJson(h);}); + } void resize(const size_t newSize); void resize(const size_t newSize, JsonNode::JsonType type); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 84dd7e09b..60d70d0c1 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -208,7 +208,10 @@ bool RewardsWidget::commitChanges() object.configuration.visitMode = ui->visitMode->currentIndex(); object.configuration.selectMode = ui->selectMode->currentIndex(); object.configuration.infoWindowType = EInfoWindowMode(ui->windowMode->currentIndex()); - object.configuration.onSelect = MetaString::createFromRawString(ui->onSelectText->text().toStdString()); + if(ui->onSelectText->text().isEmpty()) + object.configuration.onSelect.clear(); + else + object.configuration.onSelect = MetaString::createFromRawString(ui->onSelectText->text().toStdString()); object.configuration.canRefuse = ui->canRefuse->isChecked(); //reset parameters @@ -226,7 +229,10 @@ void RewardsWidget::saveCurrentVisitInfo(int index) { auto & vinfo = object.configuration.info.at(index); vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString()); + if(ui->rewardMessage->text().isEmpty()) + vinfo.message.clear(); + else + vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString()); vinfo.reward.heroLevel = ui->rHeroLevel->value(); vinfo.reward.heroExperience = ui->rHeroExperience->value(); From 5125bcc67f8ea68a0b458d8f4176f1fbfebe6202 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 17 Sep 2023 23:01:18 +0200 Subject: [PATCH 0470/1248] added probabilities --- client/windows/CHeroOverview.cpp | 44 ++++++++++++++++++++------------ client/windows/CHeroOverview.h | 1 + 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/client/windows/CHeroOverview.cpp b/client/windows/CHeroOverview.cpp index 2ab4b9efd..00b522cc2 100644 --- a/client/windows/CHeroOverview.cpp +++ b/client/windows/CHeroOverview.cpp @@ -22,6 +22,7 @@ #include "../widgets/TextControls.h" #include "../widgets/MiscWidgets.h" +#include "../../lib/GameSettings.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CHeroHandler.h" @@ -120,9 +121,11 @@ void CHeroOverview::genControls() labelArmyTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.startingArmy")); // army numbers - r = Rect(302, 3 * borderOffset + yOffset + 62, 292, 20); + r = Rect(302, 3 * borderOffset + yOffset + 62, 292, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); + auto stacksCountChances = VLC->settings()->getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES); + // army int space = (260 - 7 * 32) / 6; for(int i = 0; i < 7; i++) @@ -131,18 +134,22 @@ void CHeroOverview::genControls() backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } int i = 0; + int iStack = 0; for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) { if((*CGI->creh)[army.creature]->warMachine == ArtifactID::NONE) { imageArmy.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 2 * borderOffset + yOffset + 30)); labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 72, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (army.minAmount == army.maxAmount) ? std::to_string(army.minAmount) : std::to_string(army.minAmount) + "-" + std::to_string(army.maxAmount))); + if(iStack(302 + i * (32 + space) + 32, 3 * borderOffset + yOffset + 86, FONT_SMALL, ETextAlignment::CENTER, grayedColor, std::to_string(stacksCountChances[iStack]) + "%")); i++; } + iStack++; } // war machine title - r = Rect(302, 4 * borderOffset + yOffset + 82, 292, 30); + r = Rect(302, 4 * borderOffset + yOffset + 94, 292, 30); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); labelWarMachineTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.warMachine")); @@ -150,41 +157,46 @@ void CHeroOverview::genControls() space = (260 - 4 * 32) / 3; for(int i = 0; i < 4; i++) { - r = Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 112, 32, 32); + r = Rect(318 + i * (32 + space), 5 * borderOffset + yOffset + 124, 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); } i = 0; + iStack = 0; for(auto & army : (*CGI->heroh)[heroIdx]->initialArmy) { if(i == 0) { - imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 112)); + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature.CATAPULT]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 124)); + labelArmyCount.push_back(std::make_shared(302 + i * (32 + space) + 51, 5 * borderOffset + yOffset + 144, FONT_SMALL, ETextAlignment::TOPLEFT, grayedColor, "100%")); i++; } if((*CGI->creh)[army.creature]->warMachine != ArtifactID::NONE) { - imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 112)); + imageWarMachine.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[army.creature]->getIconIndex(), 0, 302 + i * (32 + space) + 16, 5 * borderOffset + yOffset + 124)); + if(iStack(302 + i * (32 + space) + 51, 5 * borderOffset + yOffset + 144, FONT_SMALL, ETextAlignment::TOPLEFT, grayedColor, std::to_string(stacksCountChances[iStack]) + "%")); i++; } + iStack++; } // secskill title - r = Rect(302, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30); + r = Rect(302, 6 * borderOffset + yOffset + 156, (292 / 2) - 2 * borderOffset, 30); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); labelSecSkillTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.secondarySkills")); // vertical line - backgroundLines.push_back(std::make_shared(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 144 - 2 + 254), borderColor)); + backgroundLines.push_back(std::make_shared(Point(302 + (292 / 2), 6 * borderOffset + yOffset + 156 - 1), Point(302 + (292 / 2), 6 * borderOffset + yOffset + 156 - 2 + 254), borderColor)); // spell title - r = Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 144, (292 / 2) - 2 * borderOffset, 30); + r = Rect(302 + (292 / 2) + 2 * borderOffset, 6 * borderOffset + yOffset + 156, (292 / 2) - 2 * borderOffset, 30); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); labelSpellTitle = std::make_shared(r.x + borderOffset, r.y + borderOffset + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->translate("vcmi.heroOverview.spells")); // secskill for(int i = 0; i < 6; i++) { - r = Rect(302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); + r = Rect(302, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); @@ -192,16 +204,16 @@ void CHeroOverview::genControls() i = 0; for(auto & skill : (*CGI->heroh)[heroIdx]->secSkillsInit) { - imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset))); - labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); - labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); + imageSecSkills.push_back(std::make_shared(AnimationPath::builtin("SECSK32"), (*CGI->skillh)[skill.first]->getIconIndex() * 3 + skill.second + 2, 0, 302, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset))); + labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) - 5, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->levels[skill.second - 1])); + labelSecSkillsNames.push_back(std::make_shared(334 + 2 * borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) + 10, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->skillh)[skill.first]->getNameTranslated())); i++; } // spell for(int i = 0; i < 6; i++) { - r = Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32); + r = Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); r = Rect(r.x + 32 + borderOffset, r.y, (292 / 2) - 32 - 3 * borderOffset, r.h); backgroundRectangles.push_back(std::make_shared(r.resize(1), rectangleColor, borderColor)); @@ -213,13 +225,13 @@ void CHeroOverview::genControls() { if((*CGI->heroh)[heroIdx]->haveSpellBook) { - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), 0)); + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("ARTIFACT")), 0, Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); } i++; } - imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 174 + i * (32 + borderOffset), 32, 32), 0)); - labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 174 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); + imageSpells.push_back(std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("SPELLBON")), (*CGI->spellh)[spell]->getIconIndex(), Rect(302 + (292 / 2) + 2 * borderOffset, 7 * borderOffset + yOffset + 186 + i * (32 + borderOffset), 32, 32), 0)); + labelSpellsNames.push_back(std::make_shared(302 + (292 / 2) + 3 * borderOffset + 32 + borderOffset, 8 * borderOffset + yOffset + 186 + i * (32 + borderOffset) + 3, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, (*CGI->spellh)[spell]->getNameTranslated())); i++; } } \ No newline at end of file diff --git a/client/windows/CHeroOverview.h b/client/windows/CHeroOverview.h index 9710ec857..64c9b16e9 100644 --- a/client/windows/CHeroOverview.h +++ b/client/windows/CHeroOverview.h @@ -29,6 +29,7 @@ class CHeroOverview : public CWindowObject const int borderOffset = 5; const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75); const ColorRGBA borderColor = ColorRGBA(128, 100, 75); + const ColorRGBA grayedColor = ColorRGBA(158, 130, 105); std::shared_ptr backgroundTexture; std::vector> backgroundRectangles; From 7321bb1d25699e8f3331d61989726135a8609fb1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:05:39 +0200 Subject: [PATCH 0471/1248] support double tap --- client/eventsSDL/InputSourceTouch.cpp | 13 ++++++++++--- client/eventsSDL/InputSourceTouch.h | 9 +++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index 456d9cfc7..dcffb00ec 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -33,7 +33,7 @@ #include InputSourceTouch::InputSourceTouch() - : lastTapTimeTicks(0) + : lastTapTimeTicks(0), lastLeftClickTimeTicks(0) { params.useRelativeMode = settings["general"]["userRelativePointer"].Bool(); params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float(); @@ -181,8 +181,15 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) case TouchState::TAP_DOWN_SHORT: { GH.input().setCursorPosition(convertTouchToMouse(tfinger)); - GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); - GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + if(tfinger.timestamp - lastLeftClickTimeTicks < doubleTouchTimeMilliseconds && (convertTouchToMouse(tfinger) - lastLeftClickPosition).distance < doubleTouchToleranceDistance) + GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger); + else + { + GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); + GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + } + lastLeftClickTimeTicks = tfinger.timestamp; + lastLeftClickPosition = convertTouchToMouse(tfinger); state = TouchState::IDLE; break; } diff --git a/client/eventsSDL/InputSourceTouch.h b/client/eventsSDL/InputSourceTouch.h index 56d112dc7..19a8cca50 100644 --- a/client/eventsSDL/InputSourceTouch.h +++ b/client/eventsSDL/InputSourceTouch.h @@ -72,6 +72,12 @@ struct TouchInputParameters /// tap for period longer than specified here will be qualified as "long tap", triggering corresponding gesture uint32_t longTouchTimeMilliseconds = 750; + /// time span in where the second tap has to happen for qualifing as "double click" + uint32_t doubleTouchTimeMilliseconds = 500; + + /// max distance in where the second tap has to happen for qualifing as "double click" + uint32_t doubleTouchToleranceDistance = 50; + /// moving finger for distance larger than specified will be qualified as panning gesture instead of long press uint32_t panningSensitivityThreshold = 10; @@ -94,6 +100,9 @@ class InputSourceTouch uint32_t lastTapTimeTicks; Point lastTapPosition; + uint32_t lastLeftClickTimeTicks; + Point lastLeftClickPosition; + Point convertTouchToMouse(const SDL_TouchFingerEvent & current); Point convertTouchToMouse(float x, float y); From 3d04e9c9ed1b544f50d1e671353c424d92c7430a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 17:22:30 +0300 Subject: [PATCH 0472/1248] Cleaned up and fixed ownership checks for market netpack --- client/windows/CCastleInterface.cpp | 6 ++- client/windows/CTradeWindow.cpp | 20 +++++++- server/NetPacksServer.cpp | 73 ++++++++++++++++++----------- 3 files changed, 70 insertions(+), 29 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 31ebd1559..ed511a57a 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -698,7 +698,11 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil break; case BuildingID::MARKETPLACE: - GH.windows().createAndPushWindow(town, town->visitingHero); + // can't use allied marketplace + if (town->getOwner() == LOCPLINT->playerID) + GH.windows().createAndPushWindow(town, town->visitingHero); + else + enterBuilding(building); break; case BuildingID::BLACKSMITH: diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 74a24ced9..5ef2ca061 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -872,7 +872,25 @@ void CMarketplaceWindow::selectionChanged(bool side) bool CMarketplaceWindow::printButtonFor(EMarketMode M) const { - return market->allowsTrade(M) && M != mode && (hero || ( M != EMarketMode::CREATURE_RESOURCE && M != EMarketMode::RESOURCE_ARTIFACT && M != EMarketMode::ARTIFACT_RESOURCE )); + if (!market->allowsTrade(M)) + return false; + + if (M == mode) + return false; + + if ( M == EMarketMode::RESOURCE_RESOURCE || M == EMarketMode::RESOURCE_PLAYER) + { + auto * town = dynamic_cast(market); + + if (town) + return town->getOwner() == LOCPLINT->playerID; + else + return true; + } + else + { + return hero != nullptr; + } } void CMarketplaceWindow::garrisonChanged() diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index a2f70f294..e6507c277 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -155,35 +155,54 @@ void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { + const CGObjectInstance * object = gh.getObj(pack.marketId); + const CGHeroInstance * hero = gh.getHero(pack.heroId); + const IMarket * market = IMarket::castFrom(object); + gh.throwIfWrongPlayer(&pack); - const CGObjectInstance * market = gh.getObj(pack.marketId); - if(!market) + if(!object) gh.throwAndComplain(&pack, "Invalid market object"); - const CGHeroInstance * hero = gh.getHero(pack.heroId); - //market must be owned or visited - const IMarket * m = IMarket::castFrom(market); - - if(!m) + if(!market) gh.throwAndComplain(&pack, "market is not-a-market! :/"); - PlayerColor player = market->tempOwner; + bool heroCanBeInvalid = false; - if(!player.isValidPlayer()) - player = gh.getTile(market->visitablePos())->visitableObjects.back()->tempOwner; + if (pack.mode == EMarketMode::RESOURCE_RESOURCE || pack.mode == EMarketMode::RESOURCE_PLAYER) + { + // For resource exchange we must use our own market or visit neutral market + if (object->getOwner().isValidPlayer()) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } - if(!player.isValidPlayer()) - gh.throwAndComplain(&pack, "No player can use this market!"); + if (pack.mode == EMarketMode::CREATURE_UNDEAD) + { + // For skeleton transformer, if hero is null then object must be owned + if (!hero) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } - bool allyTownSkillTrade = (pack.mode == EMarketMode::RESOURCE_SKILL && gh.getPlayerRelations(player, hero->tempOwner) == PlayerRelations::ALLIES); + if (!heroCanBeInvalid) + { + gh.throwIfWrongOwner(&pack, pack.heroId); - if(hero && (!(player == hero->tempOwner || allyTownSkillTrade) - || hero->visitablePos() != market->visitablePos())) - gh.throwAndComplain(&pack, "This hero can't use this marketplace!"); + if (!hero) + gh.throwAndComplain(&pack, "Can not trade - no hero!"); - if(!allyTownSkillTrade) - gh.throwIfWrongPlayer(&pack, player); + // TODO: check that object is actually being visited (e.g. Query exists) + if (!object->visitableAt(hero->visitablePos().x, hero->visitablePos().y)) + gh.throwAndComplain(&pack, "Can not trade - object not visited!"); + + if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES) + gh.throwAndComplain(&pack, "Can not trade - market not owned!"); + } result = true; @@ -191,43 +210,43 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { case EMarketMode::RESOURCE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.tradeResources(m, pack.val[i], player, pack.r1[i], pack.r2[i]); + result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i], pack.r2[i]); break; case EMarketMode::RESOURCE_PLAYER: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sendResources(pack.val[i], player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); + result &= gh.sendResources(pack.val[i], pack.player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); break; case EMarketMode::CREATURE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellCreatures(pack.val[i], m, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellCreatures(pack.val[i], market, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); break; case EMarketMode::RESOURCE_ARTIFACT: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.buyArtifact(m, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); + result &= gh.buyArtifact(market, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); break; case EMarketMode::ARTIFACT_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellArtifact(m, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellArtifact(market, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); break; case EMarketMode::CREATURE_UNDEAD: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.transformInUndead(m, hero, SlotID(pack.r1[i])); + result &= gh.transformInUndead(market, hero, SlotID(pack.r1[i])); break; case EMarketMode::RESOURCE_SKILL: for(int i = 0; i < pack.r2.size(); ++i) - result &= gh.buySecSkill(m, hero, SecondarySkill(pack.r2[i])); + result &= gh.buySecSkill(market, hero, SecondarySkill(pack.r2[i])); break; case EMarketMode::CREATURE_EXP: { std::vector slotIDs(pack.r1.begin(), pack.r1.end()); std::vector count(pack.val.begin(), pack.val.end()); - result = gh.sacrificeCreatures(m, hero, slotIDs, count); + result = gh.sacrificeCreatures(market, hero, slotIDs, count); return; } case EMarketMode::ARTIFACT_EXP: { std::vector positions(pack.r1.begin(), pack.r1.end()); - result = gh.sacrificeArtifact(m, hero, positions); + result = gh.sacrificeArtifact(market, hero, positions); return; } default: From 71659f1423cb5c1a9498fcb21cdf635c906092c9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:43:16 +0200 Subject: [PATCH 0473/1248] fix --- client/eventsSDL/InputSourceTouch.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index dcffb00ec..fdf794ab1 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -181,15 +181,15 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) case TouchState::TAP_DOWN_SHORT: { GH.input().setCursorPosition(convertTouchToMouse(tfinger)); - if(tfinger.timestamp - lastLeftClickTimeTicks < doubleTouchTimeMilliseconds && (convertTouchToMouse(tfinger) - lastLeftClickPosition).distance < doubleTouchToleranceDistance) - GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger); + if(tfinger.timestamp - lastLeftClickTimeTicks < params.doubleTouchTimeMilliseconds && (convertTouchToMouse(tfinger) - lastLeftClickPosition).length() < params.doubleTouchToleranceDistance) + GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger)); else { GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + lastLeftClickTimeTicks = tfinger.timestamp; + lastLeftClickPosition = convertTouchToMouse(tfinger); } - lastLeftClickTimeTicks = tfinger.timestamp; - lastLeftClickPosition = convertTouchToMouse(tfinger); state = TouchState::IDLE; break; } From 95e8c7ce2ec7cea1d5f8a1ab4aad14fc2efe5d3a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 18 Sep 2023 16:45:05 +0200 Subject: [PATCH 0474/1248] suggestions --- Mods/vcmi/config/vcmi/english.json | 2 +- Mods/vcmi/config/vcmi/german.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 09bbd67fe..cc95f3e30 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -30,7 +30,7 @@ "vcmi.capitalColors.6" : "Teal", "vcmi.capitalColors.7" : "Pink", - "vcmi.heroOverview.startingArmy" : "Starting Armys", + "vcmi.heroOverview.startingArmy" : "Starting Units", "vcmi.heroOverview.warMachine" : "War Machines", "vcmi.heroOverview.secondarySkills" : "Secondary Skills", "vcmi.heroOverview.spells" : "Spells", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 12b563392..926501565 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -30,7 +30,7 @@ "vcmi.capitalColors.6" : "Türkis", "vcmi.capitalColors.7" : "Rosa", - "vcmi.heroOverview.startingArmy" : "Startarmeen", + "vcmi.heroOverview.startingArmy" : "Starteinheiten", "vcmi.heroOverview.warMachine" : "Kriegsmaschinen", "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", "vcmi.heroOverview.spells" : "Zaubersprüche", From f1c40466d35bfcf3a5f9727762657eaaa70f5538 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 18:17:26 +0300 Subject: [PATCH 0475/1248] Changes according to review --- client/CPlayerInterface.cpp | 12 +++++------ client/HeroMovementController.cpp | 34 ++++++++++++++++--------------- client/HeroMovementController.h | 10 ++++----- server/CGameHandler.cpp | 4 ++-- 4 files changed, 31 insertions(+), 29 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index bcdbcf989..844b7caa2 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -347,7 +347,7 @@ void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) if (!hero) return; - movementController->heroMoved(hero, details); + movementController->onTryMoveHero(hero, details); } void CPlayerInterface::heroKilled(const CGHeroInstance* hero) @@ -937,7 +937,7 @@ void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &t adventureInt->showInfoBoxMessage(components, text, timer); // abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on - movementController->movementAbortRequested(); + movementController->requestMovementAbort(); if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) CCS->soundh->playSound(static_cast(soundID)); @@ -984,7 +984,7 @@ void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector { CCS->soundh->playSound(static_cast(soundID)); showingDialog->set(true); - movementController->movementAbortRequested(); // interrupt movement to show dialog + movementController->requestMovementAbort(); // interrupt movement to show dialog GH.windows().pushWindow(temp); } else @@ -1007,7 +1007,7 @@ void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList un(*pim); - movementController->movementAbortRequested(); + movementController->requestMovementAbort(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); } @@ -1017,7 +1017,7 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); - movementController->movementAbortRequested(); + movementController->requestMovementAbort(); CCS->soundh->playSound(static_cast(soundID)); if (!selection && cancel) //simple yes/no dialog @@ -1182,7 +1182,7 @@ void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) if (localState->isHeroSleeping(h)) localState->setHeroAwaken(h); - movementController->movementStartRequested(h, path); + movementController->requestMovementStart(h, path); } void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index c4588bc09..4f332fdbe 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -60,11 +60,17 @@ void HeroMovementController::onBattleStarted() // when battle starts, game will send battleStart pack *before* movement confirmation // and since network thread wait for battle intro to play, movement confirmation will only happen after intro // leading to several bugs, such as blocked input during intro - movementAbortRequested(); + requestMovementAbort(); } void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) { + if (impassable || exits.empty()) //FIXME: why we even have this dialog in such case? + { + LOCPLINT->cb->selectionMade(-1, askID); + return; + } + // Player entered teleporter // Check whether hero that has entered teleporter has paths that goes through teleporter and select appropriate exit // othervice, ask server to select one randomly by sending invalid (-1) value as answer @@ -139,7 +145,7 @@ void HeroMovementController::updatePath(const CGHeroInstance * hero, const TryMo } } -void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMoveHero & details) +void HeroMovementController::onTryMoveHero(const CGHeroInstance * hero, const TryMoveHero & details) { // Server initiated movement -> start movement animation // Note that this movement is not necessarily of owned heroes - other players movement will also pass through this method @@ -167,7 +173,7 @@ void HeroMovementController::heroMoved(const CGHeroInstance * hero, const TryMov if(details.stopMovement()) { if(duringMovement) - endHeroMove(hero); + endMove(hero); return; } @@ -233,27 +239,23 @@ void HeroMovementController::onMoveHeroApplied() bool wantStop = stoppingMovement; bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode()); - if(!canMove) + if(!canMove || (wantStop && canStop)) { - endHeroMove(hero); - } - else if(wantStop && canStop) - { - endHeroMove(hero); + endMove(hero); } else { - moveHeroOnce(hero, LOCPLINT->localState->getPath(hero)); + moveOnce(hero, LOCPLINT->localState->getPath(hero)); } } -void HeroMovementController::movementAbortRequested() +void HeroMovementController::requestMovementAbort() { if(duringMovement) - endHeroMove(currentlyMovingHero); + endMove(currentlyMovingHero); } -void HeroMovementController::endHeroMove(const CGHeroInstance * hero) +void HeroMovementController::endMove(const CGHeroInstance * hero) { assert(duringMovement == true); assert(currentlyMovingHero != nullptr); @@ -331,17 +333,17 @@ bool HeroMovementController::canHeroStopAtNode(const CGPathNode & node) const return true; } -void HeroMovementController::movementStartRequested(const CGHeroInstance * h, const CGPath & path) +void HeroMovementController::requestMovementStart(const CGHeroInstance * h, const CGPath & path) { assert(duringMovement == false); duringMovement = true; currentlyMovingHero = h; CCS->curh->hide(); - moveHeroOnce(h, path); + moveOnce(h, path); } -void HeroMovementController::moveHeroOnce(const CGHeroInstance * h, const CGPath & path) +void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & path) { // Moves hero once, sends request to server and immediately returns // movement alongside paths will be done on receiving response from server diff --git a/client/HeroMovementController.h b/client/HeroMovementController.h index 544b2a533..53314afd8 100644 --- a/client/HeroMovementController.h +++ b/client/HeroMovementController.h @@ -43,9 +43,9 @@ class HeroMovementController void updatePath(const CGHeroInstance * hero, const TryMoveHero & details); /// Moves hero 1 tile / path node - void moveHeroOnce(const CGHeroInstance * h, const CGPath & path); + void moveOnce(const CGHeroInstance * h, const CGPath & path); - void endHeroMove(const CGHeroInstance * h); + void endMove(const CGHeroInstance * h); AudioPath getMovementSoundFor(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); void updateMovementSound(const CGHeroInstance * hero, int3 posPrev, int3 posNext, EPathNodeAction moveType); @@ -66,9 +66,9 @@ public: void onPlayerTurnStarted(); void onBattleStarted(); void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID); - void heroMoved(const CGHeroInstance * hero, const TryMoveHero & details); + void onTryMoveHero(const CGHeroInstance * hero, const TryMoveHero & details); // UI handlers - void movementStartRequested(const CGHeroInstance * h, const CGPath & path); - void movementAbortRequested(); + void requestMovementStart(const CGHeroInstance * h, const CGPath & path); + void requestMovementAbort(); }; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a4741258f..3c525b416 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1139,7 +1139,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) complainRet("Hero doesn't have any movement points left!"); - if (transit && !canFly && !CGTeleport::isTeleport(t.topVisitableObj())) + if (transit && !canFly && !(canWalkOnSea && t.terType->isWater())) complainRet("Hero cannot transit over this tile!"); //several generic blocks of code @@ -1259,7 +1259,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if (CGTeleport::isTeleport(t.topVisitableObj())) visitDest = DONT_VISIT_DEST; - if (canFly) + if (canFly || (canWalkOnSea && t.terType->isWater())) { lookForGuards = IGNORE_GUARDS; visitDest = DONT_VISIT_DEST; From 219f81d28b11e78b1b86d7bc8817c24820b6e28f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 01:13:58 +0300 Subject: [PATCH 0476/1248] Added explicit "PlayerEndsTurn" netpack - PlayerEndsTurn pack is now sent when player ends turn - YourTurn pack has been renamed to PlayerStartsTurn for consistency - PlayerStartsTurn will no longer replace list of acting players - PlayerEndsGame and PlayerEndsTurn will remove player from acting list --- client/CPlayerInterface.cpp | 8 +++++++- client/CPlayerInterface.h | 1 + client/ClientCommandManager.cpp | 2 +- client/ClientNetPackVisitors.h | 3 ++- client/NetPacksClient.cpp | 9 ++++++++- lib/IGameEventsReceiver.h | 1 + lib/NetPackVisitor.h | 3 ++- lib/NetPacks.h | 16 +++++++++++++++- lib/NetPacksLib.cpp | 22 ++++++++++++++++++---- lib/registerTypes/RegisterTypes.h | 3 ++- server/processors/TurnOrderProcessor.cpp | 12 ++++++++---- server/processors/TurnOrderProcessor.h | 2 +- 12 files changed, 66 insertions(+), 16 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 844b7caa2..c5be8c790 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -172,11 +172,17 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: adventureInt.reset(new AdventureMapInterface()); } +void CPlayerInterface::playerEndsTurn(PlayerColor player) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (player == playerID) + makingTurn = false; +} + void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; - makingTurn = false; movementController->onPlayerTurnStarted(); if(GH.windows().findWindows().empty()) diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index d259f0f45..8d5e83633 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -145,6 +145,7 @@ protected: // Call-ins from server, should not be called directly, but only via void playerBlocked(int reason, bool start) override; void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface + void playerEndsTurn(PlayerColor player) override; void saveGame(BinarySerializer & h, const int version) override; //saving void loadGame(BinaryDeserializer & h, const int version) override; //loading void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index df115bad9..068c701d0 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -470,7 +470,7 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier) { - YourTurn yt; + PlayerStartsTurn yt; yt.player = colorIdentifier; yt.queryID = -1; diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index a740e5731..46f33f998 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -56,6 +56,7 @@ public: void visitNewTurn(NewTurn & pack) override; void visitGiveBonus(GiveBonus & pack) override; void visitChangeObjPos(ChangeObjPos & pack) override; + void visitPlayerEndsTurn(PlayerEndsTurn & pack) override; void visitPlayerEndsGame(PlayerEndsGame & pack) override; void visitPlayerReinitInterface(PlayerReinitInterface & pack) override; void visitRemoveBonus(RemoveBonus & pack) override; @@ -93,7 +94,7 @@ public: void visitPackageApplied(PackageApplied & pack) override; void visitSystemMessage(SystemMessage & pack) override; void visitPlayerBlocked(PlayerBlocked & pack) override; - void visitYourTurn(YourTurn & pack) override; + void visitPlayerStartsTurn(PlayerStartsTurn & pack) override; void visitTurnTimeUpdate(TurnTimeUpdate & pack) override; void visitPlayerMessageClient(PlayerMessageClient & pack) override; void visitAdvmapSpellCast(AdvmapSpellCast & pack) override; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index de1960804..067b47cd7 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -873,7 +873,7 @@ void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED); } -void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) +void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack) { logNetwork->debug("Server gives turn to %s", pack.player.toString()); @@ -881,6 +881,13 @@ void ApplyClientNetPackVisitor::visitYourTurn(YourTurn & pack) callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); } +void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) +{ + logNetwork->debug("Server ends turn of %s", pack.player.toString()); + + callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player); +} + void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) { logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index ecfd18ff0..4ad8a6eb2 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -134,6 +134,7 @@ public: virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game virtual void playerStartsTurn(PlayerColor player){}; + virtual void playerEndsTurn(PlayerColor player){}; //TODO shouldn't be moved down the tree? virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){}; diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index 44f25dc1c..39c284864 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -26,7 +26,7 @@ public: virtual void visitSystemMessage(SystemMessage & pack) {} virtual void visitPlayerBlocked(PlayerBlocked & pack) {} virtual void visitPlayerCheated(PlayerCheated & pack) {} - virtual void visitYourTurn(YourTurn & pack) {} + virtual void visitPlayerStartsTurn(PlayerStartsTurn & pack) {} virtual void visitDaysWithoutTown(DaysWithoutTown & pack) {} virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} @@ -41,6 +41,7 @@ public: virtual void visitSetAvailableHeroes(SetAvailableHero & pack) {} virtual void visitGiveBonus(GiveBonus & pack) {} virtual void visitChangeObjPos(ChangeObjPos & pack) {} + virtual void visitPlayerEndsTurn(PlayerEndsTurn & pack) {}; virtual void visitPlayerEndsGame(PlayerEndsGame & pack) {} virtual void visitPlayerReinitInterface(PlayerReinitInterface & pack) {} virtual void visitRemoveBonus(RemoveBonus & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index caa42bdb3..481243828 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -163,7 +163,7 @@ struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient } }; -struct DLL_LINKAGE YourTurn : public Query +struct DLL_LINKAGE PlayerStartsTurn : public Query { void applyGs(CGameState * gs) const; @@ -434,6 +434,20 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient } }; +struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + } +}; + struct DLL_LINKAGE PlayerEndsGame : public CPackForClient { void applyGs(CGameState * gs) const; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 4d9bd2ffc..a0d48a579 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -100,9 +100,9 @@ void PlayerCheated::visitTyped(ICPackVisitor & visitor) visitor.visitPlayerCheated(*this); } -void YourTurn::visitTyped(ICPackVisitor & visitor) +void PlayerStartsTurn::visitTyped(ICPackVisitor & visitor) { - visitor.visitYourTurn(*this); + visitor.visitPlayerStartsTurn(*this); } void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) @@ -170,6 +170,11 @@ void ChangeObjPos::visitTyped(ICPackVisitor & visitor) visitor.visitChangeObjPos(*this); } +void PlayerEndsTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerEndsTurn(*this); +} + void PlayerEndsGame::visitTyped(ICPackVisitor & visitor) { visitor.visitPlayerEndsGame(*this); @@ -1071,6 +1076,9 @@ void PlayerEndsGame::applyGs(CGameState * gs) const { p->status = EPlayerStatus::LOSER; } + + // defeated player may be making turn right now + gs->actingPlayers.erase(player); } void PlayerReinitInterface::applyGs(CGameState *gs) @@ -2503,12 +2511,18 @@ void PlayerCheated::applyGs(CGameState * gs) const gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; } -void YourTurn::applyGs(CGameState * gs) const +void PlayerStartsTurn::applyGs(CGameState * gs) const { - gs->actingPlayers.clear(); + assert(gs->actingPlayers.count(player) == 0); gs->actingPlayers.insert(player); } +void PlayerEndsTurn::applyGs(CGameState * gs) const +{ + assert(gs->actingPlayers.count(player) == 1); + gs->actingPlayers.erase(player); +} + void DaysWithoutTown::applyGs(CGameState * gs) const { auto & playerState = gs->players[player]; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 3079babf0..5f6951331 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -231,7 +231,7 @@ void registerTypesClientPacks1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -245,6 +245,7 @@ void registerTypesClientPacks1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 164fa488f..e15b5f442 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -102,10 +102,10 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) auto turnQuery = std::make_shared(gameHandler, which); gameHandler->queries->addQuery(turnQuery); - YourTurn yt; - yt.player = which; - yt.queryID = turnQuery->queryID; - gameHandler->sendAndApply(&yt); + PlayerStartsTurn pst; + pst.player = which; + pst.queryID = turnQuery->queryID; + gameHandler->sendAndApply(&pst); assert(actingPlayers.size() == 1); // No simturns yet :( assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); @@ -119,6 +119,10 @@ void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) actingPlayers.erase(which); actedPlayers.insert(which); + PlayerEndsTurn pet; + pet.player = which; + gameHandler->sendAndApply(&pet); + if (!awaitingPlayers.empty()) tryStartTurnsForPlayers(); diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index af91bc547..39990c6ae 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -53,7 +53,7 @@ public: /// Ends player turn and removes this player from turn order void onPlayerEndsGame(PlayerColor which); - /// Start game (or resume from save) and send YourTurn pack to player(s) + /// Start game (or resume from save) and send PlayerStartsTurn pack to player(s) void onGameStarted(); template From 2d2c7ee8e03cb88f250bfaab17cf7f18526053b4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 18 Sep 2023 20:35:23 +0200 Subject: [PATCH 0477/1248] added tolerance --- client/eventsSDL/InputSourceMouse.cpp | 2 +- client/eventsSDL/InputSourceTouch.cpp | 5 ++- client/gui/EventDispatcher.cpp | 52 +++++++++++++++++---------- client/gui/EventDispatcher.h | 3 +- 4 files changed, 41 insertions(+), 21 deletions(-) diff --git a/client/eventsSDL/InputSourceMouse.cpp b/client/eventsSDL/InputSourceMouse.cpp index ec971c645..dc9d8227f 100644 --- a/client/eventsSDL/InputSourceMouse.cpp +++ b/client/eventsSDL/InputSourceMouse.cpp @@ -48,7 +48,7 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b { case SDL_BUTTON_LEFT: if(button.clicks > 1) - GH.events().dispatchMouseDoubleClick(position); + GH.events().dispatchMouseDoubleClick(position, 0); else GH.events().dispatchMouseLeftButtonPressed(position, 0); break; diff --git a/client/eventsSDL/InputSourceTouch.cpp b/client/eventsSDL/InputSourceTouch.cpp index fdf794ab1..ef80833f0 100644 --- a/client/eventsSDL/InputSourceTouch.cpp +++ b/client/eventsSDL/InputSourceTouch.cpp @@ -182,7 +182,10 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger) { GH.input().setCursorPosition(convertTouchToMouse(tfinger)); if(tfinger.timestamp - lastLeftClickTimeTicks < params.doubleTouchTimeMilliseconds && (convertTouchToMouse(tfinger) - lastLeftClickPosition).length() < params.doubleTouchToleranceDistance) - GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger)); + { + GH.events().dispatchMouseDoubleClick(convertTouchToMouse(tfinger), params.touchToleranceDistance); + GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance); + } else { GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance); diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 36e5822f2..3dc576f5e 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -116,25 +116,9 @@ void EventDispatcher::dispatchShortcutReleased(const std::vector & sh } } -void EventDispatcher::dispatchMouseDoubleClick(const Point & position) +void EventDispatcher::dispatchMouseDoubleClick(const Point & position, int tolerance) { - bool doubleClicked = false; - auto hlp = doubleClickInterested; - - for(auto & i : hlp) - { - if(!vstd::contains(doubleClickInterested, i)) - continue; - - if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK)) - { - i->clickDouble(position); - doubleClicked = true; - } - } - - if(!doubleClicked) - handleLeftButtonClick(position, 0, true); + handleDoubleButtonClick(position, tolerance); } void EventDispatcher::dispatchMouseLeftButtonPressed(const Point & position, int tolerance) @@ -246,6 +230,38 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc } } +void EventDispatcher::handleDoubleButtonClick(const Point & position, int tolerance) +{ + // WARNING: this approach is NOT SAFE + // 1) We allow (un)registering elements when list itself is being processed/iterated + // 2) To avoid iterator invalidation we create a copy of this list for processing + // HOWEVER it is completely possible (as in, actually happen, no just theory) to: + // 1) element gets unregistered and deleted from lclickable + // 2) element is completely deleted, as in - destructor called, memory freed + // 3) new element is created *with exactly same address(!) + // 4) new element is registered and code will incorrectly assume that this element is still registered + // POSSIBLE SOLUTION: make EventReceivers inherit from create_shared_from this and store weak_ptr's in lists + + AEventsReceiver * nearestElement = findElementInToleranceRange(doubleClickInterested, position, AEventsReceiver::DOUBLECLICK, tolerance); + bool doubleClicked = false; + auto hlp = doubleClickInterested; + + for(auto & i : hlp) + { + if(!vstd::contains(doubleClickInterested, i)) + continue; + + if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK) || i == nearestElement) + { + i->clickDouble(position); + doubleClicked = true; + } + } + + if(!doubleClicked) + handleLeftButtonClick(position, tolerance, true); +} + void EventDispatcher::dispatchMouseScrolled(const Point & distance, const Point & position) { EventReceiversList hlp = wheelInterested; diff --git a/client/gui/EventDispatcher.h b/client/gui/EventDispatcher.h index dee3d77cd..0ab25ed5c 100644 --- a/client/gui/EventDispatcher.h +++ b/client/gui/EventDispatcher.h @@ -36,6 +36,7 @@ class EventDispatcher EventReceiversList panningInterested; void handleLeftButtonClick(const Point & position, int tolerance, bool isPressed); + void handleDoubleButtonClick(const Point & position, int tolerance); AEventsReceiver * findElementInToleranceRange(const EventReceiversList & list, const Point & position, int eventToTest, int tolerance); template @@ -59,7 +60,7 @@ public: void dispatchMouseLeftButtonPressed(const Point & position, int tolerance); void dispatchMouseLeftButtonReleased(const Point & position, int tolerance); void dispatchMouseScrolled(const Point & distance, const Point & position); - void dispatchMouseDoubleClick(const Point & position); + void dispatchMouseDoubleClick(const Point & position, int tolerance); void dispatchMouseMoved(const Point & distance, const Point & position); void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance); From 996036bdf2653540f7e8d8be6886c7aa46293b17 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 18 Sep 2023 21:28:23 +0200 Subject: [PATCH 0478/1248] disable audio on lost focus --- client/eventsSDL/InputHandler.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 5dbb914c2..93524e142 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -24,6 +24,7 @@ #include "../CMT.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" +#include "../CMusicHandler.h" #include "../../lib/CConfigHandler.h" @@ -153,6 +154,18 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) } #endif break; + case SDL_WINDOWEVENT_FOCUS_GAINED: + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(settings["general"]["music"].Integer()); + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + } + break; + case SDL_WINDOWEVENT_FOCUS_LOST: + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(0); + CCS->soundh->setVolume(0); + } + break; } return; } From bcf32984cebc64714352c44192d04045189e9863 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 19 Sep 2023 01:08:49 +0200 Subject: [PATCH 0479/1248] always sound at chat message --- client/adventureMap/CInGameConsole.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 8ee0145ae..28ef32a34 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -105,7 +105,13 @@ void CInGameConsole::print(const std::string & txt) } GH.windows().totalRedraw(); // FIXME: ingame console has no parent widget set - CCS->soundh->playSound(AudioPath::builtin("CHAT")); + + int volume = CCS->soundh->getVolume(); + if(volume == 0) + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + int handle = CCS->soundh->playSound(AudioPath::builtin("CHAT")); + if(volume == 0) + CCS->soundh->setCallback(handle, [&]() { CCS->soundh->setVolume(0); }); } bool CInGameConsole::captureThisKey(EShortcut key) From 4ac3e3c2e57e8d93783921481f38e22daf5f6f68 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 19 Sep 2023 01:48:26 +0200 Subject: [PATCH 0480/1248] fix bonus selection --- client/lobby/OptionsTab.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 78c5c141a..29fbb21a7 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -624,7 +624,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) } allowedBonus.push_back(-1); // random - if(initialHero.getNum() >= -1) + if(initialHero.getNum() >= -1 || SEL->getPlayerInfo(color.getNum()).heroesNames.size() > 0) allowedBonus.push_back(0); // artifact allowedBonus.push_back(1); // gold if(initialFaction.getNum() >= 0) @@ -908,7 +908,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - GH.windows().createAndPushWindow(helper); + GH.windows().createAndPushWindow(helper); } else selectedHero = set.hero; @@ -918,7 +918,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { if(elem >= 4) return; - set.bonus = static_cast(elem-1); + set.bonus = static_cast(allowedBonus[elem]); if(set.bonus != PlayerSettings::NONE) { if(!doApply) @@ -991,7 +991,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) if(settings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; - GH.windows().createAndPushWindow(*this); + GH.windows().createAndPushWindow(*this); } void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) From babf8581124d8b943704a7584a02f8fb55712a91 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 19 Sep 2023 01:50:42 +0200 Subject: [PATCH 0481/1248] format --- client/lobby/OptionsTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 29fbb21a7..75ff2d095 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -908,7 +908,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - GH.windows().createAndPushWindow(helper); + GH.windows().createAndPushWindow(helper); } else selectedHero = set.hero; @@ -991,7 +991,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) if(settings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(settings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; - GH.windows().createAndPushWindow(*this); + GH.windows().createAndPushWindow(*this); } void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) From 6ae09d845874e9c93926a92a5c17642bcdd874b6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 19 Sep 2023 09:52:32 +0200 Subject: [PATCH 0482/1248] newday --- client/adventureMap/CInfoBar.cpp | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index ef23a86b9..fbdd164ca 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -228,14 +228,22 @@ CInfoBar::VisibleComponentInfo::VisibleComponentInfo(const std::vectorsoundh->getVolume(); + int handle = -1; + if(volume == 0) + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + if(LOCPLINT->cb->getDate(Date::DAY_OF_WEEK) != 1) // not first day of the week - CCS->soundh->playSound(soundBase::newDay); + handle = CCS->soundh->playSound(soundBase::newDay); else if(LOCPLINT->cb->getDate(Date::WEEK) != 1) // not first week in month - CCS->soundh->playSound(soundBase::newWeek); + handle = CCS->soundh->playSound(soundBase::newWeek); else if(LOCPLINT->cb->getDate(Date::MONTH) != 1) // not first month - CCS->soundh->playSound(soundBase::newMonth); + handle = CCS->soundh->playSound(soundBase::newMonth); else - CCS->soundh->playSound(soundBase::newDay); + handle = CCS->soundh->playSound(soundBase::newDay); + + if(volume == 0) + CCS->soundh->setCallback(handle, [&]() { CCS->soundh->setVolume(0); }); } void CInfoBar::reset() From a2174dc83fdf55d535aa573caa89e77d02945126 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 19 Sep 2023 11:20:16 +0200 Subject: [PATCH 0483/1248] fix window open while sound playing --- client/adventureMap/CInGameConsole.cpp | 3 ++- client/adventureMap/CInfoBar.cpp | 3 ++- client/render/IScreenHandler.h | 3 +++ client/renderSDL/ScreenHandler.cpp | 6 ++++++ client/renderSDL/ScreenHandler.h | 3 +++ 5 files changed, 16 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/CInGameConsole.cpp b/client/adventureMap/CInGameConsole.cpp index 28ef32a34..555923631 100644 --- a/client/adventureMap/CInGameConsole.cpp +++ b/client/adventureMap/CInGameConsole.cpp @@ -22,6 +22,7 @@ #include "../gui/TextAlignment.h" #include "../render/Colors.h" #include "../render/Canvas.h" +#include "../render/IScreenHandler.h" #include "../adventureMap/AdventureMapInterface.h" #include "../windows/CMessage.h" @@ -111,7 +112,7 @@ void CInGameConsole::print(const std::string & txt) CCS->soundh->setVolume(settings["general"]["sound"].Integer()); int handle = CCS->soundh->playSound(AudioPath::builtin("CHAT")); if(volume == 0) - CCS->soundh->setCallback(handle, [&]() { CCS->soundh->setVolume(0); }); + CCS->soundh->setCallback(handle, [&]() { if(!GH.screenHandler().hasFocus()) CCS->soundh->setVolume(0); }); } bool CInGameConsole::captureThisKey(EShortcut key) diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index fbdd164ca..bdcf76766 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -25,6 +25,7 @@ #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../render/IScreenHandler.h" #include "../../CCallback.h" #include "../../lib/CConfigHandler.h" @@ -243,7 +244,7 @@ void CInfoBar::playNewDaySound() handle = CCS->soundh->playSound(soundBase::newDay); if(volume == 0) - CCS->soundh->setCallback(handle, [&]() { CCS->soundh->setVolume(0); }); + CCS->soundh->setCallback(handle, [&]() { if(!GH.screenHandler().hasFocus()) CCS->soundh->setVolume(0); }); } void CInfoBar::reset() diff --git a/client/render/IScreenHandler.h b/client/render/IScreenHandler.h index 51cc3dda2..49e5cd95e 100644 --- a/client/render/IScreenHandler.h +++ b/client/render/IScreenHandler.h @@ -40,4 +40,7 @@ public: /// Dimensions of render output virtual Point getRenderResolution() const = 0; + + /// Window has focus + virtual bool hasFocus() = 0; }; diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index 623289074..a513454b8 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -565,3 +565,9 @@ std::vector ScreenHandler::getSupportedResolutions( int displayIndex) con return result; } + +bool ScreenHandler::hasFocus() +{ + ui32 flags = SDL_GetWindowFlags(mainWindow); + return flags & SDL_WINDOW_INPUT_FOCUS; +} \ No newline at end of file diff --git a/client/renderSDL/ScreenHandler.h b/client/renderSDL/ScreenHandler.h index c7a057144..fb3d6a334 100644 --- a/client/renderSDL/ScreenHandler.h +++ b/client/renderSDL/ScreenHandler.h @@ -86,6 +86,9 @@ public: /// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS Point getRenderResolution() const final; + /// Window has focus + bool hasFocus() final; + std::vector getSupportedResolutions() const final; std::vector getSupportedResolutions(int displayIndex) const; std::tuple getSupportedScalingRange() const final; From a83f290e1397c620fa25694f8d18069afce9d9b1 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:09:17 +0300 Subject: [PATCH 0484/1248] bulk move artifacts only equipped, only backpack --- CCallback.cpp | 4 +-- CCallback.h | 4 +-- client/windows/GUIClasses.cpp | 16 +++++++--- lib/CArtHandler.cpp | 26 ++++++++++------ lib/CArtHandler.h | 6 ++-- lib/CArtifactInstance.cpp | 12 +++++++- lib/CArtifactInstance.h | 2 ++ lib/CCreatureSet.cpp | 7 ++--- lib/CCreatureSet.h | 2 +- lib/NetPacks.h | 8 ++++- lib/mapObjects/CGHeroInstance.cpp | 4 +-- lib/mapObjects/CGHeroInstance.h | 2 +- lib/mapping/MapFormatH3M.cpp | 6 ++-- server/CGameHandler.cpp | 49 +++++++++++++++++++------------ server/CGameHandler.h | 2 +- server/NetPacksServer.cpp | 2 +- 16 files changed, 99 insertions(+), 53 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 4768aaae8..7f6db4874 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -174,9 +174,9 @@ void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition sendRequest(&aa); } -void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) +void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) { - BulkExchangeArtifacts bma(srcHero, dstHero, swap); + BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack); sendRequest(&bma); } diff --git a/CCallback.h b/CCallback.h index 8fb7970f9..2e94c9f2e 100644 --- a/CCallback.h +++ b/CCallback.h @@ -108,7 +108,7 @@ public: // Moves all artifacts from one hero to another - virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) = 0; + virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0; }; class CBattleCallback : public IBattleCallback @@ -171,7 +171,7 @@ public: bool dismissHero(const CGHeroInstance * hero) override; bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; - void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) override; + void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override; void eraseArtifactByClient(const ArtifactLocation & al) override; bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 4b8b23c77..8f7126005 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -654,7 +654,12 @@ std::function CExchangeController::onSwapArtifacts() { return [&]() { - cb->bulkMoveArtifacts(left->id, right->id, true); + if(GH.isKeyboardCtrlDown()) + cb->bulkMoveArtifacts(left->id, right->id, true, true, false); + else if(GH.isKeyboardShiftDown()) + cb->bulkMoveArtifacts(left->id, right->id, true, false, true); + else + cb->bulkMoveArtifacts(left->id, right->id, true); }; } @@ -810,11 +815,14 @@ void CExchangeController::moveArtifacts(bool leftToRight) const CGHeroInstance * target = leftToRight ? right : left; if(source->tempOwner != cb->getPlayerID()) - { return; - } - cb->bulkMoveArtifacts(source->id, target->id, false); + if(GH.isKeyboardCtrlDown()) + cb->bulkMoveArtifacts(source->id, target->id, false, true, false); + else if(GH.isKeyboardShiftDown()) + cb->bulkMoveArtifacts(source->id, target->id, false, false, true); + else + cb->bulkMoveArtifacts(source->id, target->id, false); } void CExchangeController::moveArtifact( diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 5eeebb5e4..66ccfe060 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -9,7 +9,6 @@ */ #include "StdInc.h" -#include "CArtHandler.h" #include "ArtifactUtils.h" #include "CGeneralTextHandler.h" @@ -925,8 +924,10 @@ unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, boo return 0; } -void CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) +CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) { + ArtPlacementMap resArtPlacement; + setNewArtSlot(slot, art, false); if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) { @@ -938,19 +939,26 @@ void CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) mainPart = part.art; break; } - - for(auto & part : art->getPartsInfo()) + + for(const auto & part : art->getPartsInfo()) { if(part.art != mainPart) { - if(!part.art->artType->canBePutAt(this, part.slot)) - part.slot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); + auto partSlot = part.slot; + if(!part.art->artType->canBePutAt(this, partSlot)) + partSlot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); - assert(ArtifactUtils::isSlotEquipment(part.slot)); - setNewArtSlot(part.slot, part.art, true); + assert(ArtifactUtils::isSlotEquipment(partSlot)); + setNewArtSlot(partSlot, part.art, true); + resArtPlacement.emplace(std::make_pair(part.art, partSlot)); + } + else + { + resArtPlacement.emplace(std::make_pair(part.art, part.slot)); } } } + return resArtPlacement; } void CArtifactSet::removeArtifact(ArtifactPosition slot) @@ -1031,7 +1039,7 @@ bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockChe return true; //no slot means not used } -void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, CArtifactInstance * art, bool locked) +void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked) { assert(!vstd::contains(artifactsWorn, slot)); diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index dad6e88e5..37056f426 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -246,11 +246,13 @@ struct DLL_LINKAGE ArtSlotInfo class DLL_LINKAGE CArtifactSet { public: + using ArtPlacementMap = std::map; + std::vector artifactsInBackpack; //hero's artifacts from bag std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 std::vector artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange - void setNewArtSlot(const ArtifactPosition & slot, CArtifactInstance * art, bool locked); + void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked); void eraseArtSlot(const ArtifactPosition & slot); const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const; @@ -275,7 +277,7 @@ public: unsigned getArtPosCount(const ArtifactID & aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const; virtual ArtBearer::ArtBearer bearerType() const = 0; - virtual void putArtifact(ArtifactPosition slot, CArtifactInstance * art); + virtual ArtPlacementMap putArtifact(ArtifactPosition slot, CArtifactInstance * art); virtual void removeArtifact(ArtifactPosition slot); virtual ~CArtifactSet(); diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 81d475ad6..2c99d90b8 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -55,6 +55,16 @@ const std::vector & CCombinedArtifactInstan return partsInfo; } +void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap) +{ + if(!placementMap.empty()) + for(auto& part : partsInfo) + { + assert(placementMap.find(part.art) != placementMap.end()); + part.slot = placementMap.at(part.art); + } +} + SpellID CScrollArtifactInstance::getScrollSpellID() const { auto artInst = static_cast(this); @@ -163,7 +173,7 @@ bool CArtifactInstance::isCombined() const void CArtifactInstance::putAt(const ArtifactLocation & al) { - al.getHolderArtSet()->putArtifact(al.slot, this); + addPlacementMap(al.getHolderArtSet()->putArtifact(al.slot, this)); } void CArtifactInstance::removeFrom(const ArtifactLocation & al) diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 66e405328..30ef2865b 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -12,6 +12,7 @@ #include "bonuses/CBonusSystemNode.h" #include "GameConstants.h" #include "ConstTransitivePtr.h" +#include "CArtHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -39,6 +40,7 @@ public: bool isPart(const CArtifactInstance * supposedPart) const; std::vector & getPartsInfo(); const std::vector & getPartsInfo() const; + void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap); template void serialize(Handler & h, const int version) { diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index ec09548c0..e9b81c4f6 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -867,14 +867,13 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const return ArtBearer::CREATURE; } -void CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) +CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { assert(!getArt(pos)); assert(art->artType->canBePutAt(this, pos)); - CArtifactSet::putArtifact(pos, art); - if(ArtifactUtils::isSlotEquipment(pos)) - attachTo(*art); + attachTo(*art); + return CArtifactSet::putArtifact(pos, art); } void CStackInstance::removeArtifact(ArtifactPosition pos) diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index e72d0eb14..d15d6c35e 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -121,7 +121,7 @@ public: void setArmyObj(const CArmedInstance *ArmyObj); virtual void giveStackExp(TExpType exp); bool valid(bool allowUnrandomized) const; - void putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet + ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet void removeArtifact(ArtifactPosition pos) override; ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet virtual std::string nodeName() const override; //from CBonusSystemnode diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 3fa84933f..0075c9642 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -2413,12 +2413,16 @@ struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer ObjectInstanceID srcHero; ObjectInstanceID dstHero; bool swap = false; + bool equipped = true; + bool backpack = true; BulkExchangeArtifacts() = default; - BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap) + BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) : srcHero(srcHero) , dstHero(dstHero) , swap(swap) + , equipped(equipped) + , backpack(backpack) { } @@ -2430,6 +2434,8 @@ struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer h & srcHero; h & dstHero; h & swap; + h & equipped; + h & backpack; } }; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index e8cde5e39..c0a4bb0c6 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1066,13 +1066,13 @@ std::string CGHeroInstance::getBiographyTextID() const return ""; } -void CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance *art) +CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { assert(art->artType->canBePutAt(this, pos)); - CArtifactSet::putArtifact(pos, art); if(ArtifactUtils::isSlotEquipment(pos)) attachTo(*art); + return CArtifactSet::putArtifact(pos, art); } void CGHeroInstance::removeArtifact(ArtifactPosition pos) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 65be37cef..c8aaad4fa 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -229,7 +229,7 @@ public: void initHero(CRandomGenerator & rand); void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); - void putArtifact(ArtifactPosition pos, CArtifactInstance * art) override; + ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override; void removeArtifact(ArtifactPosition pos) override; void initExp(CRandomGenerator & rand); void initArmy(CRandomGenerator & rand, IArmyDescriptor *dst = nullptr); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 3352f8b1c..175728fbf 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -927,10 +927,10 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor // Artifact seems to be missing in game, so skip artifacts that don't fit target slot auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - auto artifactPos = ArtifactPosition(slot); - if(artifact->canBePutAt(ArtifactLocation(hero, artifactPos))) + auto dstLoc = ArtifactLocation(hero, ArtifactPosition(slot)); + if(artifact->canBePutAt(dstLoc)) { - hero->putArtifact(artifactPos, artifact); + artifact->putAt(dstLoc); } else { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 960b8031d..cffa32d1b 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2690,7 +2690,7 @@ bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocat return true; } -bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap) +bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) { // Make sure exchange is even possible between the two heroes. if(!isAllowedExchange(srcHero, dstHero)) @@ -2745,34 +2745,45 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID slots.push_back(BulkMoveArtifacts::LinkedSlots(slot, slot)); } }; - // Move over artifacts that are worn srcHero -> dstHero - moveArtsWorn(psrcHero, pdstHero, slotsSrcDst); - artFittingSet.artifactsWorn.clear(); - // Move over artifacts that are worn dstHero -> srcHero - moveArtsWorn(pdstHero, psrcHero, slotsDstSrc); - // Move over artifacts that are in backpack srcHero -> dstHero - moveArtsInBackpack(psrcHero, slotsSrcDst); - // Move over artifacts that are in backpack dstHero -> srcHero - moveArtsInBackpack(pdstHero, slotsDstSrc); + if(equipped) + { + // Move over artifacts that are worn srcHero -> dstHero + moveArtsWorn(psrcHero, pdstHero, slotsSrcDst); + artFittingSet.artifactsWorn.clear(); + // Move over artifacts that are worn dstHero -> srcHero + moveArtsWorn(pdstHero, psrcHero, slotsDstSrc); + } + if(backpack) + { + // Move over artifacts that are in backpack srcHero -> dstHero + moveArtsInBackpack(psrcHero, slotsSrcDst); + // Move over artifacts that are in backpack dstHero -> srcHero + moveArtsInBackpack(pdstHero, slotsDstSrc); + } } else { artFittingSet.artifactsInBackpack = pdstHero->artifactsInBackpack; artFittingSet.artifactsWorn = pdstHero->artifactsWorn; - - // Move over artifacts that are worn - for(auto & artInfo : psrcHero->artifactsWorn) + if(equipped) { - if(ArtifactUtils::isArtRemovable(artInfo)) + // Move over artifacts that are worn + for(auto & artInfo : psrcHero->artifactsWorn) { - moveArtifact(psrcHero->getArt(artInfo.first), artInfo.first, pdstHero, slotsSrcDst); + if(ArtifactUtils::isArtRemovable(artInfo)) + { + moveArtifact(psrcHero->getArt(artInfo.first), artInfo.first, pdstHero, slotsSrcDst); + } } } - // Move over artifacts that are in backpack - for(auto & slotInfo : psrcHero->artifactsInBackpack) + if(backpack) { - moveArtifact(psrcHero->getArt(psrcHero->getArtPos(slotInfo.artifact)), - psrcHero->getArtPos(slotInfo.artifact), pdstHero, slotsSrcDst); + // Move over artifacts that are in backpack + for(auto & slotInfo : psrcHero->artifactsInBackpack) + { + moveArtifact(psrcHero->getArt(psrcHero->getArtPos(slotInfo.artifact)), + psrcHero->getArtPos(slotInfo.artifact), pdstHero, slotsSrcDst); + } } } sendAndApply(&ma); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 3103003e3..53a395362 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -131,7 +131,7 @@ public: void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; void removeArtifact(const ArtifactLocation &al) override; bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; - bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap); + bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); bool eraseArtifactByClient(const ArtifactLocation & al); void synchronizeArtifactHandlerLists(); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index a2f70f294..286935383 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -132,7 +132,7 @@ void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) { gh.throwIfWrongOwner(&pack, pack.srcHero); - result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap); + result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); } void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) From f47def3588a5fd51d6a8471ed7ea9dbd15526e47 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 17 Sep 2023 18:40:14 +0300 Subject: [PATCH 0485/1248] refactoring --- client/CMakeLists.txt | 2 + client/widgets/CArtifactHolder.cpp | 4 +- client/widgets/CArtifactHolder.h | 6 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 18 +- client/widgets/CArtifactsOfHeroBase.h | 12 +- client/widgets/CArtifactsOfHeroKingdom.cpp | 4 +- client/widgets/CExchangeController.cpp | 119 +++++++++ client/widgets/CExchangeController.h | 30 +++ client/widgets/CWindowWithArtifacts.cpp | 2 +- client/windows/GUIClasses.cpp | 280 +++----------------- client/windows/GUIClasses.h | 36 +-- lib/ArtifactUtils.cpp | 1 - lib/CArtifactInstance.cpp | 6 - lib/CArtifactInstance.h | 1 - lib/mapping/CMap.cpp | 2 +- lib/mapping/CMap.h | 2 +- 17 files changed, 213 insertions(+), 314 deletions(-) create mode 100644 client/widgets/CExchangeController.cpp create mode 100644 client/widgets/CExchangeController.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ae542146d..bd05be4d1 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -94,6 +94,7 @@ set(client_SRCS widgets/Buttons.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp + widgets/CExchangeController.cpp widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp widgets/ComboBox.cpp @@ -255,6 +256,7 @@ set(client_HEADERS widgets/Buttons.h widgets/CArtifactHolder.h widgets/CComponent.h + widgets/CExchangeController.h widgets/CGarrisonInt.h widgets/CreatureCostBox.h widgets/ComboBox.h diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index b53237c9a..afb385b2d 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -186,8 +186,8 @@ void CHeroArtPlace::clickPressed(const Point & cursorPosition) void CHeroArtPlace::showPopupWindow(const Point & cursorPosition) { - if(rightClickCallback) - rightClickCallback(*this); + if(showPopupCallback) + showPopupCallback(*this); } void CHeroArtPlace::showAll(Canvas & to) diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index d8cc43aba..24995f48f 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -65,11 +65,11 @@ public: class CHeroArtPlace: public CArtPlace { public: - using ClickHandler = std::function; + using ClickFunctor = std::function; ArtifactPosition slot; - ClickHandler leftClickCallback; - ClickHandler rightClickCallback; + ClickFunctor leftClickCallback; + ClickFunctor showPopupCallback; CHeroArtPlace(Point position, const CArtifactInstance * Art = nullptr); void lockSlot(bool on); diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 20c4c53cc..0b38d5dfa 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -42,7 +42,7 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position) artPlace = std::make_shared(pos); artPlace->setArtifact(nullptr); artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); artPlaceIdx++; } diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index b8d175226..dd0723fd5 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -55,10 +55,10 @@ void CArtifactsOfHeroBase::setPutBackPickedArtifactCallback(PutBackPickedArtCall } void CArtifactsOfHeroBase::init( - CHeroArtPlace::ClickHandler lClickCallback, - CHeroArtPlace::ClickHandler rClickCallback, + CHeroArtPlace::ClickFunctor lClickCallback, + CHeroArtPlace::ClickFunctor showPopupCallback, const Point & position, - BpackScrollHandler scrollHandler) + BpackScrollFunctor scrollCallback) { // CArtifactsOfHeroBase::init may be transform to CArtifactsOfHeroBase::CArtifactsOfHeroBase if OBJECT_CONSTRUCTION_CAPTURING is removed OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); @@ -78,16 +78,16 @@ void CArtifactsOfHeroBase::init( artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); artPlace.second->leftClickCallback = lClickCallback; - artPlace.second->rightClickCallback = rClickCallback; + artPlace.second->showPopupCallback = showPopupCallback; } for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); artPlace->leftClickCallback = lClickCallback; - artPlace->rightClickCallback = rClickCallback; + artPlace->showPopupCallback = showPopupCallback; } - leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollHandler]() { scrollHandler(-1); }, EShortcut::MOVE_LEFT); - rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollHandler]() { scrollHandler(+1); }, EShortcut::MOVE_RIGHT); + leftBackpackRoll = std::make_shared(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(-1);}, EShortcut::MOVE_LEFT); + rightBackpackRoll = std::make_shared(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(+1);}, EShortcut::MOVE_RIGHT); leftBackpackRoll->block(true); rightBackpackRoll->block(true); @@ -102,8 +102,8 @@ void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace) void CArtifactsOfHeroBase::rightClickArtPlace(CHeroArtPlace & artPlace) { - if(rightClickCallback) - rightClickCallback(*this, artPlace); + if(showPopupCallback) + showPopupCallback(*this, artPlace); } void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero) diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index d2d2a9a95..aeacc10fa 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -15,15 +15,15 @@ class CArtifactsOfHeroBase : public CIntObject { protected: using ArtPlacePtr = std::shared_ptr; - using BpackScrollHandler = std::function; + using BpackScrollFunctor = std::function; public: using ArtPlaceMap = std::map; - using ClickHandler = std::function; + using ClickFunctor = std::function; using PutBackPickedArtCallback = std::function; - ClickHandler leftClickCallback; - ClickHandler rightClickCallback; + ClickFunctor leftClickCallback; + ClickFunctor showPopupCallback; CArtifactsOfHeroBase(); virtual void putBackPickedArtifact(); @@ -61,8 +61,8 @@ protected: Point(381,296) //18 }; - virtual void init(CHeroArtPlace::ClickHandler lClickCallback, CHeroArtPlace::ClickHandler rClickCallback, - const Point & position, BpackScrollHandler scrollHandler); + virtual void init(CHeroArtPlace::ClickFunctor lClickCallback, CHeroArtPlace::ClickFunctor showPopupCallback, + const Point & position, BpackScrollFunctor scrollCallback); // Assigns an artifacts to an artifact place depending on it's new slot ID virtual void setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet); virtual void scrollBackpackForArtSet(int offset, const CArtifactSet & artSet); diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index 96c6f2cf9..ff365e0ef 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -29,13 +29,13 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto artPlace.second->slot = artPlace.first; artPlace.second->setArtifact(nullptr); artPlace.second->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace.second->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace.second->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); } for(auto artPlace : backpack) { artPlace->setArtifact(nullptr); artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1); - artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); + artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1); } leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1)); rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1)); diff --git a/client/widgets/CExchangeController.cpp b/client/widgets/CExchangeController.cpp new file mode 100644 index 000000000..10888ea5b --- /dev/null +++ b/client/widgets/CExchangeController.cpp @@ -0,0 +1,119 @@ +/* + * CExchangeController.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 "CExchangeController.h" + +#include "../CPlayerInterface.h" + +#include "../widgets/CGarrisonInt.h" + +#include "../../CCallback.h" + +#include "../lib/mapObjects/CGHeroInstance.h" + +CExchangeController::CExchangeController(ObjectInstanceID hero1, ObjectInstanceID hero2) + : left(LOCPLINT->cb->getHero(hero1)) + , right(LOCPLINT->cb->getHero(hero2)) +{ +} + +void CExchangeController::swapArmy() +{ + auto getStacks = [](const CArmedInstance * source) -> std::vector> + { + auto slots = source->Slots(); + return std::vector>(slots.begin(), slots.end()); + }; + + auto leftSlots = getStacks(left); + auto rightSlots = getStacks(right); + + auto i = leftSlots.begin(), j = rightSlots.begin(); + + for(; i != leftSlots.end() && j != rightSlots.end(); i++, j++) + { + LOCPLINT->cb->swapCreatures(left, right, i->first, j->first); + } + + if(i != leftSlots.end()) + { + auto freeSlots = right->getFreeSlots(); + auto slot = freeSlots.begin(); + + for(; i != leftSlots.end() && slot != freeSlots.end(); i++, slot++) + { + LOCPLINT->cb->swapCreatures(left, right, i->first, *slot); + } + } + else if(j != rightSlots.end()) + { + auto freeSlots = left->getFreeSlots(); + auto slot = freeSlots.begin(); + + for(; j != rightSlots.end() && slot != freeSlots.end(); j++, slot++) + { + LOCPLINT->cb->swapCreatures(left, right, *slot, j->first); + } + } +} + +void CExchangeController::moveArmy(bool leftToRight, std::optional heldSlot) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + + if(!heldSlot.has_value()) + { + auto weakestSlot = vstd::minElementByFun(source->Slots(), + [](const std::pair & s) -> int + { + return s.second->getCreatureID().toCreature()->getAIValue(); + }); + heldSlot = weakestSlot->first; + } + LOCPLINT->cb->bulkMoveArmy(source->id, target->id, heldSlot.value()); +} + +void CExchangeController::moveStack(bool leftToRight, SlotID sourceSlot) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + auto creature = source->getCreature(sourceSlot); + + if(creature == nullptr) + return; + + SlotID targetSlot = target->getSlotFor(creature); + if(targetSlot.validSlot()) + { + if(source->stacksCount() == 1 && source->needsLastStack()) + { + LOCPLINT->cb->splitStack(source, target, sourceSlot, targetSlot, + target->getStackCount(targetSlot) + source->getStackCount(sourceSlot) - 1); + } + else + { + LOCPLINT->cb->mergeOrSwapStacks(source, target, sourceSlot, targetSlot); + } + } +} + +void CExchangeController::swapArtifacts(bool equipped, bool baclpack) +{ + LOCPLINT->cb->bulkMoveArtifacts(left->id, right->id, true, equipped, baclpack); +} + +void CExchangeController::moveArtifacts(bool leftToRight, bool equipped, bool baclpack) +{ + const auto source = leftToRight ? left : right; + const auto target = leftToRight ? right : left; + + LOCPLINT->cb->bulkMoveArtifacts(source->id, target->id, false, equipped, baclpack); +} diff --git a/client/widgets/CExchangeController.h b/client/widgets/CExchangeController.h new file mode 100644 index 000000000..928725f1b --- /dev/null +++ b/client/widgets/CExchangeController.h @@ -0,0 +1,30 @@ +/* + * CExchangeController.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 "../windows/CWindowObject.h" + #include "CWindowWithArtifacts.h" + +class CCallback; + +class CExchangeController +{ +public: + CExchangeController(ObjectInstanceID hero1, ObjectInstanceID hero2); + void swapArmy(); + void moveArmy(bool leftToRight, std::optional heldSlot); + void moveStack(bool leftToRight, SlotID sourceSlot); + void swapArtifacts(bool equipped, bool baclpack); + void moveArtifacts(bool leftToRight, bool equipped, bool baclpack); + +private: + const CGHeroInstance * left; + const CGHeroInstance * right; +}; diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 7725c16c3..e37815019 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -38,7 +38,7 @@ void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) { auto artSet = artSetWeak.lock(); artSet->leftClickCallback = std::bind(&CWindowWithArtifacts::leftClickArtPlaceHero, this, _1, _2); - artSet->rightClickCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2); + artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2); artSet->setPutBackPickedArtifactCallback(artPutBackHandler); }, artSet); } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 8f7126005..071663af0 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -19,62 +19,40 @@ #include "../CMusicHandler.h" #include "../CPlayerInterface.h" #include "../CVideoHandler.h" -#include "../CServerHandler.h" - -#include "../battle/BattleInterfaceClasses.h" -#include "../battle/BattleInterface.h" #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" -#include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" -#include "../widgets/MiscWidgets.h" #include "../widgets/CreatureCostBox.h" #include "../widgets/Buttons.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" #include "../widgets/ObjectLists.h" -#include "../lobby/CSavingScreen.h" #include "../render/Canvas.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" -#include "../CMT.h" #include "../../CCallback.h" -#include "../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../lib/mapObjectConstructors/CommonConstructors.h" #include "../lib/mapObjects/CGHeroInstance.h" #include "../lib/mapObjects/CGMarket.h" -#include "../lib/ArtifactUtils.h" #include "../lib/mapObjects/CGTownInstance.h" #include "../lib/mapObjects/ObjectTemplate.h" #include "../lib/gameState/CGameState.h" -#include "../lib/gameState/InfoAboutArmy.h" #include "../lib/gameState/SThievesGuildInfo.h" -#include "../lib/CArtHandler.h" -#include "../lib/CBuildingHandler.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CCreatureHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/GameSettings.h" #include "../lib/CondSh.h" #include "../lib/CSkillHandler.h" -#include "../lib/spells/CSpellHandler.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/CStopWatch.h" -#include "../lib/CTownHandler.h" -#include "../lib/GameConstants.h" -#include "../lib/bonuses/Bonus.h" -#include "../lib/NetPacksBase.h" -#include "../lib/StartInfo.h" #include "../lib/TextOperations.h" CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) @@ -623,223 +601,9 @@ static bool isQuickExchangeLayoutAvailable() return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG)); } -CExchangeController::CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2) - :left(LOCPLINT->cb->getHero(hero1)), right(LOCPLINT->cb->getHero(hero2)), cb(LOCPLINT->cb), view(view) -{ -} - -std::function CExchangeController::onMoveArmyToLeft() -{ - return [&]() { moveArmy(false); }; -} - -std::function CExchangeController::onMoveArmyToRight() -{ - return [&]() { moveArmy(true); }; -} - -std::vector getBackpackArts(const CGHeroInstance * hero) -{ - std::vector result; - - for(auto slot : hero->artifactsInBackpack) - { - result.push_back(slot.artifact); - } - - return result; -} - -std::function CExchangeController::onSwapArtifacts() -{ - return [&]() - { - if(GH.isKeyboardCtrlDown()) - cb->bulkMoveArtifacts(left->id, right->id, true, true, false); - else if(GH.isKeyboardShiftDown()) - cb->bulkMoveArtifacts(left->id, right->id, true, false, true); - else - cb->bulkMoveArtifacts(left->id, right->id, true); - }; -} - -std::function CExchangeController::onMoveArtifactsToLeft() -{ - return [&]() { moveArtifacts(false); }; -} - -std::function CExchangeController::onMoveArtifactsToRight() -{ - return [&]() { moveArtifacts(true); }; -} - -std::vector> getStacks(const CArmedInstance * source) -{ - auto slots = source->Slots(); - - return std::vector>(slots.begin(), slots.end()); -} - -std::function CExchangeController::onSwapArmy() -{ - return [&]() - { - if(left->tempOwner != cb->getPlayerID() - || right->tempOwner != cb->getPlayerID()) - { - return; - } - - auto leftSlots = getStacks(left); - auto rightSlots = getStacks(right); - - auto i = leftSlots.begin(), j = rightSlots.begin(); - - for(; i != leftSlots.end() && j != rightSlots.end(); i++, j++) - { - cb->swapCreatures(left, right, i->first, j->first); - } - - if(i != leftSlots.end()) - { - auto freeSlots = right->getFreeSlots(); - auto slot = freeSlots.begin(); - - for(; i != leftSlots.end() && slot != freeSlots.end(); i++, slot++) - { - cb->swapCreatures(left, right, i->first, *slot); - } - } - else if(j != rightSlots.end()) - { - auto freeSlots = left->getFreeSlots(); - auto slot = freeSlots.begin(); - - for(; j != rightSlots.end() && slot != freeSlots.end(); j++, slot++) - { - cb->swapCreatures(left, right, *slot, j->first); - } - } - }; -} - -std::function CExchangeController::onMoveStackToLeft(SlotID slotID) -{ - return [=]() - { - if(right->tempOwner != cb->getPlayerID()) - { - return; - } - - moveStack(right, left, slotID); - }; -} - -std::function CExchangeController::onMoveStackToRight(SlotID slotID) -{ - return [=]() - { - if(left->tempOwner != cb->getPlayerID()) - { - return; - } - - moveStack(left, right, slotID); - }; -} - -void CExchangeController::moveStack( - const CGHeroInstance * source, - const CGHeroInstance * target, - SlotID sourceSlot) -{ - auto creature = source->getCreature(sourceSlot); - if(creature == nullptr) - return; - - SlotID targetSlot = target->getSlotFor(creature); - - if(targetSlot.validSlot()) - { - if(source->stacksCount() == 1 && source->needsLastStack()) - { - cb->splitStack( - source, - target, - sourceSlot, - targetSlot, - target->getStackCount(targetSlot) + source->getStackCount(sourceSlot) - 1); - } - else - { - cb->mergeOrSwapStacks(source, target, sourceSlot, targetSlot); - } - } -} - -void CExchangeController::moveArmy(bool leftToRight) -{ - const CGHeroInstance * source = leftToRight ? left : right; - const CGHeroInstance * target = leftToRight ? right : left; - const CGarrisonSlot * selection = this->view->getSelectedSlotID(); - SlotID slot; - - if(source->tempOwner != cb->getPlayerID()) - { - return; - } - - if(selection && selection->our() && selection->getObj() == source) - { - slot = selection->getSlot(); - } - else - { - auto weakestSlot = vstd::minElementByFun( - source->Slots(), - [](const std::pair & s) -> int - { - return s.second->getCreatureID().toCreature()->getAIValue(); - }); - - slot = weakestSlot->first; - } - - cb->bulkMoveArmy(source->id, target->id, slot); -} - -void CExchangeController::moveArtifacts(bool leftToRight) -{ - const CGHeroInstance * source = leftToRight ? left : right; - const CGHeroInstance * target = leftToRight ? right : left; - - if(source->tempOwner != cb->getPlayerID()) - return; - - if(GH.isKeyboardCtrlDown()) - cb->bulkMoveArtifacts(source->id, target->id, false, true, false); - else if(GH.isKeyboardShiftDown()) - cb->bulkMoveArtifacts(source->id, target->id, false, false, true); - else - cb->bulkMoveArtifacts(source->id, target->id, false); -} - -void CExchangeController::moveArtifact( - const CGHeroInstance * source, - const CGHeroInstance * target, - ArtifactPosition srcPosition) -{ - auto srcLocation = ArtifactLocation(source, srcPosition); - auto dstLocation = ArtifactLocation(target, - ArtifactUtils::getArtAnyPosition(target, source->getArt(srcPosition)->getTypeId())); - - cb->swapArtifacts(srcLocation, dstLocation); -} - CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) : CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")), - controller(this, hero1, hero2), + controller(hero1, hero2), moveStackLeftButtons(), moveStackRightButtons() { @@ -990,12 +754,38 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, if(qeLayout) { - moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToRight()); - echangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), controller.onSwapArmy()); - moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), controller.onMoveArmyToLeft()); - moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToRight()); - echangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), controller.onSwapArtifacts()); - moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), controller.onMoveArtifactsToLeft()); + auto moveArtifacts = [this](std::function moveRoutine) -> void + { + bool moveEquipped = true; + bool moveBackpack = true; + + if(GH.isKeyboardCtrlDown()) + moveBackpack = false; + else if(GH.isKeyboardShiftDown()) + moveEquipped = false; + moveRoutine(moveEquipped, moveBackpack); + }; + + auto moveArmy = [this](bool leftToRight) -> void + { + std::optional slotId = std::nullopt; + if(auto slot = getSelectedSlotID()) + slotId = slot->getSlot(); + controller.moveArmy(leftToRight, slotId); + }; + + moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, true)); + echangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), + std::bind(&CExchangeController::swapArmy, &controller)); + moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, false)); + moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);})); + echangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); + moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); for(int i = 0; i < GameConstants::ARMY_SIZE; i++) { @@ -1004,14 +794,14 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, Point(484 + 35 * i, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - controller.onMoveStackToLeft(SlotID(i)))); + std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); moveStackRightButtons.push_back( std::make_shared( Point(66 + 35 * i, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - controller.onMoveStackToRight(SlotID(i)))); + std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); } } diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index fa1484d10..a94dd3b43 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -9,10 +9,8 @@ */ #pragma once -#include "CWindowObject.h" -#include "../lib/GameConstants.h" #include "../lib/ResourceSet.h" -#include "../lib/int3.h" +#include "../widgets/CExchangeController.h" #include "../widgets/CWindowWithArtifacts.h" #include "../widgets/Images.h" @@ -28,16 +26,13 @@ class CreatureCostBox; class CCreaturePic; class MoraleLuckBox; class CHeroArea; -class CMinorResDataBar; class CSlider; class CComponentBox; class CTextInput; class CListBox; class CLabelGroup; -class CToggleButton; class CGStatusBar; class CTextBox; -class CResDataBar; class CGarrisonInt; class CGarrisonSlot; @@ -246,35 +241,6 @@ public: void show(Canvas & to) override; }; -class CCallback; -class CExchangeWindow; - -class CExchangeController -{ -private: - const CGHeroInstance * left; - const CGHeroInstance * right; - std::shared_ptr cb; - CExchangeWindow * view; - -public: - CExchangeController(CExchangeWindow * view, ObjectInstanceID hero1, ObjectInstanceID hero2); - std::function onMoveArmyToRight(); - std::function onSwapArmy(); - std::function onMoveArmyToLeft(); - std::function onSwapArtifacts(); - std::function onMoveArtifactsToLeft(); - std::function onMoveArtifactsToRight(); - std::function onMoveStackToLeft(SlotID slotID); - std::function onMoveStackToRight(SlotID slotID); - -private: - void moveArmy(bool leftToRight); - void moveArtifacts(bool leftToRight); - void moveArtifact(const CGHeroInstance * source, const CGHeroInstance * target, ArtifactPosition srcPosition); - void moveStack(const CGHeroInstance * source, const CGHeroInstance * target, SlotID sourceSlot); -}; - class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts { std::array, 2> titles; diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index eeb94c2c6..ba8c7823a 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -171,7 +171,6 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifa auto * artInst = new CArtifactInstance(art); if(art->isCombined()) { - assert(art->isCombined()); for(const auto & part : art->getConstituents()) artInst->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); } diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 2c99d90b8..13ca20d64 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -44,12 +44,6 @@ bool CCombinedArtifactInstance::isPart(const CArtifactInstance * supposedPart) c return false; } -std::vector & CCombinedArtifactInstance::getPartsInfo() -{ - // TODO romove this func. encapsulation violation - return partsInfo; -} - const std::vector & CCombinedArtifactInstance::getPartsInfo() const { return partsInfo; diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 30ef2865b..2c87adf08 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -38,7 +38,6 @@ public: void addPart(CArtifactInstance * art, const ArtifactPosition & slot); // Checks if supposed part inst is part of this combined art inst bool isPart(const CArtifactInstance * supposedPart) const; - std::vector & getPartsInfo(); const std::vector & getPartsInfo() const; void addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap); diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 2d1de817f..811a52e02 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -492,7 +492,7 @@ void CMap::checkForObjectives() } } -void CMap::addNewArtifactInstance(CArtifactInstance * art) +void CMap::addNewArtifactInstance(ConstTransitivePtr art) { art->setId(static_cast(artInstances.size())); artInstances.emplace_back(art); diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 4e3402eb5..c3cd0bfdb 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -94,7 +94,7 @@ public: void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); void calculateGuardingGreaturePositions(); - void addNewArtifactInstance(CArtifactInstance * art); + void addNewArtifactInstance(ConstTransitivePtr art); void eraseArtifactInstance(CArtifactInstance * art); void addNewQuestInstance(CQuest * quest); From 9cc623c981aa87ca1c7d83c2096af2c60d468282 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 18 Sep 2023 22:58:08 +0300 Subject: [PATCH 0486/1248] Fixed move checks. Backpack buttons. --- client/widgets/CWindowWithArtifacts.cpp | 7 ++++- client/widgets/CWindowWithArtifacts.h | 3 +- client/windows/CHeroBackpackWindow.cpp | 2 +- client/windows/CHeroWindow.cpp | 2 +- client/windows/CKingdomInterface.cpp | 2 +- client/windows/CTradeWindow.cpp | 4 +-- client/windows/GUIClasses.cpp | 39 +++++++++++++++++++++---- client/windows/GUIClasses.h | 6 ++-- server/NetPacksServer.cpp | 4 ++- 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index e37815019..0b42a1ca2 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -27,13 +27,18 @@ #include "../../lib/mapObjects/CGHeroInstance.h" void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) +{ + artSets.emplace_back(artSet); +} + +void CWindowWithArtifacts::addSetAndCallbacks(CArtifactsOfHeroPtr artSet) { CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackHandler = []() -> void { CCS->curh->dragAndDropCursor(nullptr); }; - artSets.emplace_back(artSet); + addSet(artSet); std::visit([this, artPutBackHandler](auto artSetWeak) { auto artSet = artSetWeak.lock(); diff --git a/client/widgets/CWindowWithArtifacts.h b/client/widgets/CWindowWithArtifacts.h index 85067e244..f603ad7f5 100644 --- a/client/widgets/CWindowWithArtifacts.h +++ b/client/widgets/CWindowWithArtifacts.h @@ -28,6 +28,7 @@ public: using CloseCallback = std::function; void addSet(CArtifactsOfHeroPtr artSet); + void addSetAndCallbacks(CArtifactsOfHeroPtr artSet); void addCloseCallback(CloseCallback callback); const CGHeroInstance * getHeroPickedArtifact(); const CArtifactInstance * getPickedArtifact(); @@ -39,7 +40,7 @@ public: void artifactDisassembled(const ArtifactLocation & artLoc) override; void artifactAssembled(const ArtifactLocation & artLoc) override; -private: +protected: std::vector artSets; CloseCallback closeCallback; diff --git a/client/windows/CHeroBackpackWindow.cpp b/client/windows/CHeroBackpackWindow.cpp index 6f4203970..a4ff6d1c8 100644 --- a/client/windows/CHeroBackpackWindow.cpp +++ b/client/windows/CHeroBackpackWindow.cpp @@ -28,7 +28,7 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero) arts = std::make_shared(Point(windowMargin, windowMargin)); arts->setHero(hero); - addSet(arts); + addSetAndCallbacks(arts); addCloseCallback(std::bind(&CHeroBackpackWindow::close, this)); diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 588785b58..5e80706e8 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -219,7 +219,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) { arts = std::make_shared(Point(-65, -8)); arts->setHero(curHero); - addSet(arts); + addSetAndCallbacks(arts); } int serial = LOCPLINT->cb->getHeroSerial(curHero, false); diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 4bcae8c91..a844b9ace 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -706,7 +706,7 @@ std::shared_ptr CKingdHeroList::createHeroItem(size_t index) if(index < heroesList.size()) { auto hero = std::make_shared(heroesList[index]); - addSet(hero->heroArts); + addSetAndCallbacks(hero->heroArts); return hero; } else diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 74a24ced9..f377b1eb8 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -680,7 +680,7 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta arts = std::make_shared(Point(-361, 46)); arts->selectArtCallback = std::bind(&CTradeWindow::artifactSelected, this, _1); arts->setHero(hero); - addSet(arts); + addSetAndCallbacks(arts); } initItems(false); initItems(true); @@ -1115,7 +1115,7 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, arts = std::make_shared(Point(-365, -12)); arts->setHero(hero); - addSet(arts); + addSetAndCallbacks(arts); initItems(true); initItems(false); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 071663af0..76a9d8a2c 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -12,6 +12,7 @@ #include "CCastleInterface.h" #include "CCreatureWindow.h" +#include "CHeroBackpackWindow.h" #include "CHeroWindow.h" #include "InfoWindows.h" @@ -662,8 +663,8 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, artifs[1] = std::make_shared(Point(98, 150)); artifs[1]->setHero(heroInst[1]); - addSet(artifs[0]); - addSet(artifs[1]); + addSetAndCallbacks(artifs[0]); + addSetAndCallbacks(artifs[1]); for(int g=0; g<4; ++g) { @@ -754,7 +755,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, if(qeLayout) { - auto moveArtifacts = [this](std::function moveRoutine) -> void + auto moveArtifacts = [this](const std::function moveRoutine) -> void { bool moveEquipped = true; bool moveBackpack = true; @@ -766,7 +767,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, moveRoutine(moveEquipped, moveBackpack); }; - auto moveArmy = [this](bool leftToRight) -> void + auto moveArmy = [this](const bool leftToRight) -> void { std::optional slotId = std::nullopt; if(auto slot = getSelectedSlotID()) @@ -774,18 +775,42 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, controller.moveArmy(leftToRight, slotId); }; + auto openBackpack = [this](const CGHeroInstance * hero) -> void + { + GH.windows().createAndPushWindow(hero); + for(auto artSet : artSets) + GH.windows().topWindow()->addSet(artSet); + }; + moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), std::bind(moveArmy, true)); - echangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), + exchangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), std::bind(&CExchangeController::swapArmy, &controller)); moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), std::bind(moveArmy, false)); moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);})); - echangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), + exchangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); + backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[0])); + backpackButtonLeft->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[1])); + backpackButtonRight->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + + auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID(); + auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID(); + moveAllGarrButtonLeft->block(leftHeroBlock); + exchangeGarrButton->block(leftHeroBlock || rightHeroBlock); + moveAllGarrButtonRight->block(rightHeroBlock); + moveArtifactsButtonLeft->block(leftHeroBlock); + exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock); + moveArtifactsButtonRight->block(rightHeroBlock); + backpackButtonLeft->block(leftHeroBlock); + backpackButtonRight->block(rightHeroBlock); for(int i = 0; i < GameConstants::ARMY_SIZE; i++) { @@ -795,6 +820,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); + moveStackLeftButtons.back()->block(leftHeroBlock); moveStackRightButtons.push_back( std::make_shared( @@ -802,6 +828,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); + moveStackLeftButtons.back()->block(rightHeroBlock); } } diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index a94dd3b43..a73669570 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -269,13 +269,15 @@ class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public std::shared_ptr garr; std::shared_ptr moveAllGarrButtonLeft; - std::shared_ptr echangeGarrButton; + std::shared_ptr exchangeGarrButton; std::shared_ptr moveAllGarrButtonRight; std::shared_ptr moveArtifactsButtonLeft; - std::shared_ptr echangeArtifactsButton; + std::shared_ptr exchangeArtifactsButton; std::shared_ptr moveArtifactsButtonRight; std::vector> moveStackLeftButtons; std::vector> moveStackRightButtons; + std::shared_ptr backpackButtonLeft; + std::shared_ptr backpackButtonRight; CExchangeController controller; public: diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 286935383..0035da264 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -68,7 +68,7 @@ void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) { - gh.throwIfWrongPlayer(&pack); + gh.throwIfWrongOwner(&pack, pack.srcArmy); result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); } @@ -132,6 +132,8 @@ void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) { gh.throwIfWrongOwner(&pack, pack.srcHero); + if(pack.swap) + gh.throwIfWrongOwner(&pack, pack.dstHero); result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); } From 6280e778dccb51c7109adbd940e78202088a2d6f Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Tue, 19 Sep 2023 14:10:25 +0300 Subject: [PATCH 0487/1248] fix build --- client/widgets/CExchangeController.cpp | 2 +- client/windows/GUIClasses.cpp | 2 +- lib/CArtifactInstance.cpp | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/client/widgets/CExchangeController.cpp b/client/widgets/CExchangeController.cpp index 10888ea5b..23c9019d8 100644 --- a/client/widgets/CExchangeController.cpp +++ b/client/widgets/CExchangeController.cpp @@ -7,7 +7,7 @@ * Full text of license available in license.txt file, in main folder * */ - +#include "StdInc.h" #include "CExchangeController.h" #include "../CPlayerInterface.h" diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 76a9d8a2c..419dea97f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -755,7 +755,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, if(qeLayout) { - auto moveArtifacts = [this](const std::function moveRoutine) -> void + auto moveArtifacts = [](const std::function moveRoutine) -> void { bool moveEquipped = true; bool moveBackpack = true; diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 13ca20d64..8c178eda0 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -52,7 +52,7 @@ const std::vector & CCombinedArtifactInstan void CCombinedArtifactInstance::addPlacementMap(CArtifactSet::ArtPlacementMap & placementMap) { if(!placementMap.empty()) - for(auto& part : partsInfo) + for(auto & part : partsInfo) { assert(placementMap.find(part.art) != placementMap.end()); part.slot = placementMap.at(part.art); @@ -167,7 +167,8 @@ bool CArtifactInstance::isCombined() const void CArtifactInstance::putAt(const ArtifactLocation & al) { - addPlacementMap(al.getHolderArtSet()->putArtifact(al.slot, this)); + auto placementMap = al.getHolderArtSet()->putArtifact(al.slot, this); + addPlacementMap(placementMap); } void CArtifactInstance::removeFrom(const ArtifactLocation & al) From 326791886d65ee65f1325f6846e84305a2c7a4f9 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 19 Sep 2023 17:11:03 +0200 Subject: [PATCH 0488/1248] Rename and events modal mode always --- lib/mapObjects/CGPandoraBox.cpp | 1 + lib/mapObjects/CRewardableObject.cpp | 12 ++++++------ lib/mapObjects/CRewardableObject.h | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 48910fa20..5a4551347 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -291,6 +291,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) void CGEvent::init() { blockVisit = false; + configuration.infoWindowType = EInfoWindowMode::MODAL; for(auto & i : configuration.info) { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 8df14fd7d..ad3ebc5c6 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -49,18 +49,18 @@ void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHer grantReward(index, contextHero); } -void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndeces, const MetaString & dialog) const +void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const { - BlockingDialog sd(configuration.canRefuse, rewardIndeces.size() > 1); + BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1); sd.player = contextHero->tempOwner; sd.text = dialog; - if (rewardIndeces.size() > 1) - for (auto index : rewardIndeces) + if (rewardIndices.size() > 1) + for (auto index : rewardIndices) sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); - if (rewardIndeces.size() == 1) - configuration.info.at(rewardIndeces.front()).reward.loadComponents(sd.components, contextHero); + if (rewardIndices.size() == 1) + configuration.info.at(rewardIndices.front()).reward.loadComponents(sd.components, contextHero); cb->showBlockingDialog(&sd); } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 1c8e97c05..86040a89b 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -35,7 +35,7 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; - virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndeces, const MetaString & dialog) const; + virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) From 3cdc3daa2c63b00b00f88a4e20afbec2a45f536d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 16:31:06 +0300 Subject: [PATCH 0489/1248] Fix crash on handling invalid player request --- server/CGameHandler.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3c525b416..91d6251d7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2035,10 +2035,18 @@ bool CGameHandler::bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player) { - const CArmedInstance * s1 = static_cast(getObjInstance(id1)), - * s2 = static_cast(getObjInstance(id2)); - const CCreatureSet &S1 = *s1, &S2 = *s2; + const CArmedInstance * s1 = static_cast(getObjInstance(id1)); + const CArmedInstance * s2 = static_cast(getObjInstance(id2)); + const CCreatureSet & S1 = *s1; + const CCreatureSet & S2 = *s2; StackLocation sl1(s1, p1), sl2(s2, p2); + + if (s1 == nullptr || s2 == nullptr) + { + complain("Cannot exchange stacks between non-existing objects!!\n"); + return false; + } + if (!sl1.slot.validSlot() || !sl2.slot.validSlot()) { complain(complainInvalidSlot); From 8c0d78f1d9b969425f2252b59a740895edaeb5f9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 22:09:55 +0300 Subject: [PATCH 0490/1248] Added initiator-player to packs that add/remove/move objects --- AI/Nullkiller/AIGateway.cpp | 2 +- AI/Nullkiller/AIGateway.h | 2 +- AI/VCAI/VCAI.cpp | 2 +- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 4 +-- client/CPlayerInterface.h | 2 +- client/Client.h | 6 ++-- client/NetPacksClient.cpp | 41 ++++++++++++++---------- client/adventureMap/MapAudioPlayer.cpp | 8 ++--- client/adventureMap/MapAudioPlayer.h | 9 +++--- client/mapView/IMapRendererObserver.h | 9 +++--- client/mapView/MapViewController.cpp | 40 ++++++++++++++--------- client/mapView/MapViewController.h | 13 ++++---- client/mapView/mapHandler.cpp | 16 ++++----- client/mapView/mapHandler.h | 8 ++--- lib/IGameCallback.h | 6 ++-- lib/IGameEventsReceiver.h | 2 +- lib/NetPacks.h | 20 +++++++++--- lib/NetPacksLib.cpp | 8 ++--- lib/mapObjects/CGCreature.cpp | 6 ++-- lib/mapObjects/CGPandoraBox.cpp | 2 +- lib/mapObjects/CQuest.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 8 ++--- lib/spells/AdventureSpellMechanics.cpp | 5 ++- server/CGameHandler.cpp | 26 ++++++++------- server/CGameHandler.h | 6 ++-- server/NetPacksServer.cpp | 2 +- server/battles/BattleResultProcessor.cpp | 6 ++-- server/processors/HeroPoolProcessor.cpp | 2 +- server/queries/MapQueries.cpp | 2 +- 30 files changed, 152 insertions(+), 115 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index fa11ffb07..1cd731f43 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -349,7 +349,7 @@ void AIGateway::newObject(const CGObjectInstance * obj) //to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight //see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes -void AIGateway::objectRemoved(const CGObjectInstance * obj) +void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) { LOG_TRACE(logAi); NET_EVENT_HANDLER; diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 7f5c99814..261a368b5 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -156,7 +156,7 @@ public: void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; - void objectRemoved(const CGObjectInstance * obj) override; + void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 2c1d5c6e9..a4fea7d24 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -385,7 +385,7 @@ void VCAI::newObject(const CGObjectInstance * obj) //to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight //see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes -void VCAI::objectRemoved(const CGObjectInstance * obj) +void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) { LOG_TRACE(logAi); NET_EVENT_HANDLER; diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 91f9f3ba5..4abdbcd62 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -189,7 +189,7 @@ public: void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; void requestRealized(PackageApplied * pa) override; void receivedResource() override; - void objectRemoved(const CGObjectInstance * obj) override; + void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index c5be8c790..efde3aae0 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1391,10 +1391,10 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) CCS->curh->show(); } -void CPlayerInterface::objectRemoved(const CGObjectInstance * obj) +void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) { EVENT_HANDLER_CALLED_BY_CLIENT; - if(LOCPLINT->cb->isPlayerMakingTurn(playerID) && obj->getRemovalSound()) + if(playerID == initiator && obj->getRemovalSound()) { waitWhileDialog(); CCS->soundh->playSound(obj->getRemovalSound().value()); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 8d5e83633..33651892b 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -140,7 +140,7 @@ protected: // Call-ins from server, should not be called directly, but only via void centerView (int3 pos, int focusTime) override; void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; void objectPropertyChanged(const SetObjectProperty * sop) override; - void objectRemoved(const CGObjectInstance *obj) override; + void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override; void objectRemovedAfter() override; void playerBlocked(int reason, bool start) override; void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; diff --git a/client/Client.h b/client/Client.h index d87d8c0c2..5952404a4 100644 --- a/client/Client.h +++ b/client/Client.h @@ -161,8 +161,8 @@ public: friend class CBattleCallback; //handling players actions void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; - bool removeObject(const CGObjectInstance * obj) override {return false;}; - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override {}; + bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; @@ -204,7 +204,7 @@ public: void setMovePoints(SetMovePoints * smp) override {}; void setManaPoints(ObjectInstanceID hid, int val) override {}; void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; - void changeObjPos(ObjectInstanceID objid, int3 newPos) override {}; + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}; void sendAndApply(CPackForClient * pack) override {}; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 067b47cd7..b26c07dbc 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -368,15 +368,16 @@ void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); if(CGI->mh) - CGI->mh->onObjectFadeOut(obj); + CGI->mh->onObjectFadeOut(obj, pack.initiator); CGI->mh->waitForOngoingAnimations(); } + void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); if(CGI->mh) - CGI->mh->onObjectFadeIn(obj); + CGI->mh->onObjectFadeIn(obj, pack.initiator); CGI->mh->waitForOngoingAnimations(); cl.invalidatePaths(); @@ -447,10 +448,10 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) { - const CGObjectInstance *o = cl.getObj(pack.id); + const CGObjectInstance *o = cl.getObj(pack.objectID); if(CGI->mh) - CGI->mh->onObjectFadeOut(o); + CGI->mh->onObjectFadeOut(o, pack.initiator); //notify interfaces about removal for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) @@ -458,7 +459,7 @@ void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW //TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) - i->second->objectRemoved(o); + i->second->objectRemoved(o, pack.initiator); } CGI->mh->waitForOngoingAnimations(); @@ -543,12 +544,12 @@ void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) CGTownInstance *town = gs.getTown(pack.tid); for(const auto & id : pack.bid) { - callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 1); + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1); } // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town); - CGI->mh->onObjectInstantAdd(town); + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); } void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) @@ -556,12 +557,12 @@ void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) CGTownInstance * town = gs.getTown(pack.tid); for(const auto & id : pack.bid) { - callInterfaceIfPresent(cl, town->tempOwner, &IGameEventsReceiver::buildChanged, town, id, 2); + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2); } // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town); - CGI->mh->onObjectInstantAdd(town); + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); } void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) @@ -609,17 +610,17 @@ void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) { if(const CGTownInstance *t = gs.getTown(pack.tid)) - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroInGarrisonChange, t); + callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t); } if(CGI->mh) - CGI->mh->onObjectInstantAdd(h); + CGI->mh->onObjectInstantAdd(h, h->getOwner()); } void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack) { CGHeroInstance *h = gs.getHero(pack.id); if(CGI->mh) - CGI->mh->onObjectInstantAdd(h); + CGI->mh->onObjectInstantAdd(h, h->getOwner()); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); } @@ -646,7 +647,10 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & // invalidate section of map view with our object and force an update with new flag color if (pack.what == ObjProperty::OWNER) - CGI->mh->onObjectInstantRemove(gs.getObjInstance(pack.id)); + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantRemove(object, object->getOwner()); + } } void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) @@ -660,7 +664,10 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) // invalidate section of map view with our object and force an update with new flag color if (pack.what == ObjProperty::OWNER) - CGI->mh->onObjectInstantAdd(gs.getObjInstance(pack.id)); + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantAdd(object, object->getOwner()); + } } void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack) @@ -996,7 +1003,7 @@ void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); if(CGI->mh) - CGI->mh->onObjectFadeIn(obj); + CGI->mh->onObjectFadeIn(obj, pack.initiator); for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) { diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index fafbe9c18..4787655f8 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -50,22 +50,22 @@ void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 update(); } -void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj) +void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) { addObject(obj); } -void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj) +void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) { removeObject(obj); } -void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj) +void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) { addObject(obj); } -void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj) +void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) { removeObject(obj); } diff --git a/client/adventureMap/MapAudioPlayer.h b/client/adventureMap/MapAudioPlayer.h index f6c752887..edb83b051 100644 --- a/client/adventureMap/MapAudioPlayer.h +++ b/client/adventureMap/MapAudioPlayer.h @@ -15,6 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class ObjectInstanceID; class CArmedInstance; +class PlayerColor; VCMI_LIB_NAMESPACE_END class MapAudioPlayer : public IMapObjectObserver @@ -38,10 +39,10 @@ class MapAudioPlayer : public IMapObjectObserver protected: // IMapObjectObserver impl bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj) override; - void onObjectFadeOut(const CGObjectInstance * obj) override; - void onObjectInstantAdd(const CGObjectInstance * obj) override; - void onObjectInstantRemove(const CGObjectInstance * obj) override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; diff --git a/client/mapView/IMapRendererObserver.h b/client/mapView/IMapRendererObserver.h index 10e36807b..f7ba8d5f3 100644 --- a/client/mapView/IMapRendererObserver.h +++ b/client/mapView/IMapRendererObserver.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN class int3; class CGObjectInstance; class CGHeroInstance; +class PlayerColor; VCMI_LIB_NAMESPACE_END @@ -26,16 +27,16 @@ public: virtual bool hasOngoingAnimations() = 0; /// Plays fade-in animation and adds object to map - virtual void onObjectFadeIn(const CGObjectInstance * obj) = 0; + virtual void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; /// Plays fade-out animation and removed object from map - virtual void onObjectFadeOut(const CGObjectInstance * obj) = 0; + virtual void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; /// Adds object to map instantly, with no animation - virtual void onObjectInstantAdd(const CGObjectInstance * obj) = 0; + virtual void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; /// Removes object from map instantly, with no animation - virtual void onObjectInstantRemove(const CGObjectInstance * obj) = 0; + virtual void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; /// Perform hero movement animation, moving hero across terrain virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 6785d37ea..424c31518 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -168,7 +168,7 @@ void MapViewController::tick(uint32_t timeDelta) if(!hero) hero = boat->hero; - double heroMoveTime = LOCPLINT->makingTurn ? + double heroMoveTime = LOCPLINT->playerID == hero->getOwner() ? settings["adventure"]["heroMoveTime"].Float() : settings["adventure"]["enemyMoveTime"].Float(); @@ -267,31 +267,35 @@ void MapViewController::afterRender() } } -bool MapViewController::isEventInstant(const CGObjectInstance * obj) +bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) { - if (!isEventVisible(obj)) + if (!isEventVisible(obj, initiator)) return true; - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() <= 0) + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() <= 0) return true; // instant movement speed - if(LOCPLINT->makingTurn && settings["adventure"]["heroMoveTime"].Float() <= 0) + if(initiator == LOCPLINT->playerID && settings["adventure"]["heroMoveTime"].Float() <= 0) return true; // instant movement speed return false; } -bool MapViewController::isEventVisible(const CGObjectInstance * obj) +bool MapViewController::isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator) { if(adventureContext == nullptr) return false; - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() < 0) + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) return false; // enemy move speed set to "hidden/none" if(!GH.windows().isTopWindow(adventureInt)) return false; + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && initiator != LOCPLINT->playerID) + return false; + if(obj->isVisitable()) return context->isVisible(obj->visitablePos()); else @@ -303,12 +307,16 @@ bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & if(adventureContext == nullptr) return false; - if(!LOCPLINT->makingTurn && settings["adventure"]["enemyMoveTime"].Float() < 0) + if(obj->getOwner() != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) return false; // enemy move speed set to "hidden/none" if(!GH.windows().isTopWindow(adventureInt)) return false; + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && obj->getOwner() != LOCPLINT->playerID) + return false; + if(context->isVisible(obj->convertToVisitablePos(from))) return true; @@ -394,7 +402,7 @@ void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int { if(isEventVisible(obj, from, dest)) { - if (!isEventInstant(obj)) + if (!isEventInstant(obj, obj->getOwner())) fadeOutObject(obj); setViewCenter(obj->getSightCenter()); } @@ -418,39 +426,39 @@ void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const i { if(isEventVisible(obj, from, dest)) { - if (!isEventInstant(obj)) + if (!isEventInstant(obj, obj->getOwner())) fadeInObject(obj); setViewCenter(obj->getSightCenter()); } addObject(obj); } -void MapViewController::onObjectFadeIn(const CGObjectInstance * obj) +void MapViewController::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) { assert(!hasOngoingAnimations()); - if(isEventVisible(obj) && !isEventInstant(obj) ) + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) fadeInObject(obj); addObject(obj); } -void MapViewController::onObjectFadeOut(const CGObjectInstance * obj) +void MapViewController::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) { assert(!hasOngoingAnimations()); - if(isEventVisible(obj) && !isEventInstant(obj) ) + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) fadeOutObject(obj); else removeObject(obj); } -void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj) +void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) { addObject(obj); }; -void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj) +void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) { removeObject(obj); }; diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index 3a23b917e..f7f4d079e 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct ObjectPosInfo; +class PlayerColor; VCMI_LIB_NAMESPACE_END struct MapRendererContextState; @@ -55,8 +56,8 @@ private: Point targetTileSize = Point(32, 32); bool wasInDeadZone = true; - bool isEventInstant(const CGObjectInstance * obj); - bool isEventVisible(const CGObjectInstance * obj); + bool isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator); + bool isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator); bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); void fadeOutObject(const CGObjectInstance * obj); @@ -67,10 +68,10 @@ private: // IMapObjectObserver impl bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj) override; - void onObjectFadeOut(const CGObjectInstance * obj) override; - void onObjectInstantAdd(const CGObjectInstance * obj) override; - void onObjectInstantRemove(const CGObjectInstance * obj) override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index 2bb0a3aae..5b3b197f5 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -144,16 +144,16 @@ bool CMapHandler::isInMap(const int3 & tile) return map->isInTheMap(tile); } -void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj) +void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) { for(auto * observer : observers) - observer->onObjectFadeIn(obj); + observer->onObjectFadeIn(obj, initiator); } -void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj) +void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) { for(auto * observer : observers) - observer->onObjectFadeOut(obj); + observer->onObjectFadeOut(obj, initiator); } void CMapHandler::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) @@ -180,16 +180,16 @@ void CMapHandler::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & observer->onAfterHeroDisembark(obj, from, dest); } -void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj) +void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) { for(auto * observer : observers) - observer->onObjectInstantAdd(obj); + observer->onObjectInstantAdd(obj, initiator); } -void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj) +void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) { for(auto * observer : observers) - observer->onObjectInstantRemove(obj); + observer->onObjectInstantRemove(obj, initiator); } void CMapHandler::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) diff --git a/client/mapView/mapHandler.h b/client/mapView/mapHandler.h index 179998eab..e18f58e91 100644 --- a/client/mapView/mapHandler.h +++ b/client/mapView/mapHandler.h @@ -47,10 +47,10 @@ public: bool isInMap(const int3 & tile); /// see MapObjectObserver interface - void onObjectFadeIn(const CGObjectInstance * obj); - void onObjectFadeOut(const CGObjectInstance * obj); - void onObjectInstantAdd(const CGObjectInstance * obj); - void onObjectInstantRemove(const CGObjectInstance * obj); + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator); void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 30bed8824..1c5c7b4b9 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -89,8 +89,8 @@ public: virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; - virtual bool removeObject(const CGObjectInstance * obj)=0; - virtual void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) = 0; + virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; @@ -132,7 +132,7 @@ public: virtual void setMovePoints(SetMovePoints * smp)=0; virtual void setManaPoints(ObjectInstanceID hid, int val)=0; virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0; - virtual void changeObjPos(ObjectInstanceID objid, int3 newPos)=0; + virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; virtual void sendAndApply(CPackForClient * pack) = 0; virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 4ad8a6eb2..065bd7f2a 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -129,7 +129,7 @@ public: virtual void requestRealized(PackageApplied *pa){}; virtual void beforeObjectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged virtual void objectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged - virtual void objectRemoved(const CGObjectInstance *obj){}; //eg. collected resource, picked artifact, beaten hero + virtual void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator){}; //eg. collected resource, picked artifact, beaten hero virtual void objectRemovedAfter(){}; //eg. collected resource, picked artifact, beaten hero virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 481243828..093330040 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -424,6 +424,8 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient ObjectInstanceID objid; /// New position of visitable tile of an object int3 nPos; + /// Player that initiated this action, if any + PlayerColor initiator; virtual void visitTyped(ICPackVisitor & visitor) override; @@ -431,6 +433,7 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient { h & objid; h & nPos; + h & initiator; } }; @@ -613,19 +616,25 @@ struct DLL_LINKAGE ChangeFormation : public CPackForClient struct DLL_LINKAGE RemoveObject : public CPackForClient { RemoveObject() = default; - RemoveObject(const ObjectInstanceID & ID) - : id(ID) + RemoveObject(const ObjectInstanceID & objectID, const PlayerColor & initiator) + : objectID(objectID) + , initiator(initiator) { } void applyGs(CGameState * gs); virtual void visitTyped(ICPackVisitor & visitor) override; - ObjectInstanceID id; + /// ID of removed object + ObjectInstanceID objectID; + + /// Player that initiated this action, if any + PlayerColor initiator; template void serialize(Handler & h, const int version) { - h & id; + h & objectID; + h & initiator; } }; @@ -803,6 +812,8 @@ struct DLL_LINKAGE NewObject : public CPackForClient ui32 subID = 0; /// Position of visitable tile of created object int3 targetPos; + /// Which player initiated creation of this object + PlayerColor initiator; ObjectInstanceID createdObjectID; //used locally, filled during applyGs @@ -813,6 +824,7 @@ struct DLL_LINKAGE NewObject : public CPackForClient h & ID; h & subID; h & targetPos; + h & initiator; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index a0d48a579..5fd1116ad 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1118,8 +1118,8 @@ void RemoveBonus::applyGs(CGameState *gs) void RemoveObject::applyGs(CGameState *gs) { - CGObjectInstance *obj = gs->getObjInstance(id); - logGlobal->debug("removing object id=%d; address=%x; name=%s", id, (intptr_t)obj, obj->getObjectName()); + CGObjectInstance *obj = gs->getObjInstance(objectID); + logGlobal->debug("removing object id=%d; address=%x; name=%s", objectID, (intptr_t)obj, obj->getObjectName()); //unblock tiles gs->map->removeBlockVisTiles(obj); @@ -1159,7 +1159,7 @@ void RemoveObject::applyGs(CGameState *gs) //return hero to the pool, so he may reappear in tavern gs->heroesPool->addHeroToPool(beatenHero); - gs->map->objects[id.getNum()] = nullptr; + gs->map->objects[objectID.getNum()] = nullptr; //If hero on Boat is removed, the Boat disappears if(beatenHero->boat) @@ -1210,7 +1210,7 @@ void RemoveObject::applyGs(CGameState *gs) event.trigger = event.trigger.morph(patcher); } gs->map->instanceNames.erase(obj->instanceName); - gs->map->objects[id.getNum()].dellNull(); + gs->map->objects[objectID.getNum()].dellNull(); gs->map->calculateGuardingGreaturePositions(); } diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index bcb475aed..7da445fca 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -299,7 +299,7 @@ void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const } else { - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } } @@ -400,12 +400,12 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & if(result.winner == 0) { giveReward(hero); - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else if(result.winner > 1) // draw { // guarded reward is lost forever on draw - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else { diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 5a4551347..de2a00448 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -196,7 +196,7 @@ void CGPandoraBox::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answe else if(getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT).empty()) { hero->showInfoDialog(15); - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } else //if it gives something without battle { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 13872ea81..377786a5a 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -980,7 +980,7 @@ void CGBorderGuard::onHeroVisit(const CGHeroInstance * h) const void CGBorderGuard::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const { if (answer) - cb->removeObject(this); + cb->removeObject(this, hero->getOwner()); } void CGBorderGuard::afterAddToMap(CMap * map) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 70b1ab59d..ebb507a1b 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -299,7 +299,7 @@ void CGResource::collectRes(const PlayerColor & player) const sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); cb->showInfoDialog(&sii); - cb->removeObject(this); + cb->removeObject(this, player); } void CGResource::battleFinished(const CGHeroInstance *hero, const BattleResult &result) const @@ -797,7 +797,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const void CGArtifact::pick(const CGHeroInstance * h) const { if(cb->giveHeroArtifact(h, storedArtifact, ArtifactPosition::FIRST_AVAILABLE)) - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } BattleField CGArtifact::getBattlefield() const @@ -1063,7 +1063,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const cb->showInfoDialog(&iw); if(ID == Obj::OCEAN_BOTTLE) - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) @@ -1113,7 +1113,7 @@ void CGScholar::onHeroVisit( const CGHeroInstance * h ) const } cb->showInfoDialog(&iw); - cb->removeObject(this); + cb->removeObject(this, h->getOwner()); } void CGScholar::initObj(CRandomGenerator & rand) diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 7802c4fa2..c8fa795c0 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -202,6 +202,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment ChangeObjPos cop; cop.objid = nearest->id; cop.nPos = summonPos; + cop.initiator = parameters.caster->getCasterOwner(); env->apply(&cop); } else if(schoolLevel < 2) //none or basic level -> cannot create boat :( @@ -217,6 +218,7 @@ ESpellCastResult SummonBoatMechanics::applyAdventureEffects(SpellCastEnvironment no.ID = Obj::BOAT; no.subID = BoatId::NECROPOLIS; no.targetPos = summonPos; + no.initiator = parameters.caster->getCasterOwner(); env->apply(&no); } return ESpellCastResult::OK; @@ -257,7 +259,8 @@ ESpellCastResult ScuttleBoatMechanics::applyAdventureEffects(SpellCastEnvironmen } RemoveObject ro; - ro.id = t->visitableObjects.back()->id; + ro.initiator = parameters.caster->getCasterOwner(); + ro.objectID = t->visitableObjects.back()->id; env->apply(&ro); return ESpellCastResult::OK; } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 91d6251d7..43dee22d4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1045,7 +1045,7 @@ void CGameHandler::giveSpells(const CGTownInstance *t, const CGHeroInstance *h) sendAndApply(&cs); } -bool CGameHandler::removeObject(const CGObjectInstance * obj) +bool CGameHandler::removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) { if (!obj || !getObj(obj->id)) { @@ -1054,7 +1054,8 @@ bool CGameHandler::removeObject(const CGObjectInstance * obj) } RemoveObject ro; - ro.id = obj->id; + ro.objectID = obj->id; + ro.initiator = initiator; sendAndApply(&ro); checkVictoryLossConditionsForAll(); //eg if monster escaped (removing objs after battle is done dircetly by endBattle, not this function) @@ -1110,8 +1111,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const bool standAtObstacle = t.blocked && !t.visitable; const bool standAtWater = !h->boat && t.terType->isWater() && (t.visitableObjects.empty() || !t.visitableObjects.back()->isCoastVisitable()); - - auto const complainRet = [&](const std::string & message){ + + const auto complainRet = [&](const std::string & message) + { //send info about movement failure complain(message); sendAndApply(&tmh); @@ -1504,11 +1506,12 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta changeFogOfWar(h->pos, h->getSightRadius(), player, false); } -void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos) +void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) { ChangeObjPos cop; cop.objid = objid; cop.nPos = newPos; + cop.initiator = initiator; sendAndApply(&cop); } @@ -3462,7 +3465,7 @@ bool CGameHandler::buildBoat(ObjectInstanceID objid, PlayerColor playerID) } giveResources(playerID, -boatCost); - createObject(tile, Obj::BOAT, obj->getBoatType().getNum()); + createObject(tile, playerID, Obj::BOAT, obj->getBoatType().getNum()); return true; } @@ -3538,7 +3541,7 @@ void CGameHandler::checkVictoryLossConditionsForPlayer(PlayerColor player) for (auto h : hlp) //eliminate heroes { if (h.get()) - removeObject(h); + removeObject(h, player); } //player lost -> all his objects become unflagged (neutral) @@ -3587,7 +3590,7 @@ bool CGameHandler::dig(const CGHeroInstance *h) if (h->diggingStatus() != EDiggingStatus::CAN_DIG) //checks for terrain and movement COMPLAIN_RETF("Hero cannot dig (error code %d)!", static_cast(h->diggingStatus())); - createObject(h->visitablePos(), Obj::HOLE, 0 ); + createObject(h->visitablePos(), h->getOwner(), Obj::HOLE, 0 ); //take MPs SetMovePoints smp; @@ -3981,7 +3984,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) { auto count = cre->getRandomAmount(std::rand); - createObject(*tile, Obj::MONSTER, creatureID); + createObject(*tile, PlayerColor::NEUTRAL, Obj::MONSTER, creatureID); auto monsterId = getTopObj(*tile)->id; setObjProperty(monsterId, ObjProperty::MONSTER_COUNT, count); @@ -4123,11 +4126,12 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const //} #endif -void CGameHandler::createObject(const int3 & visitablePosition, Obj type, int32_t subtype) +void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype) { NewObject no; no.ID = type; - no.subID= subtype; + no.subID = subtype; + no.initiator = initiator; no.targetPos = visitablePosition; sendAndApply(&no); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 3103003e3..b9e126dd7 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -100,8 +100,8 @@ public: //from IGameCallback //do sth void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; - bool removeObject(const CGObjectInstance * obj) override; - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype ) override; + bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; @@ -145,7 +145,7 @@ public: void setMovePoints(SetMovePoints * smp) override; void setManaPoints(ObjectInstanceID hid, int val) override; void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override; - void changeObjPos(ObjectInstanceID objid, int3 newPos) override; + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index a2f70f294..377b5d403 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -44,7 +44,7 @@ void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) { gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.removeObject(gh.getObj(pack.hid)); + result = gh.removeObject(gh.getObj(pack.hid), pack.player); } void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index c319fbb02..cec5a5b5e 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -469,12 +469,12 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(finishingBattle->loserHero) //remove beaten hero { - RemoveObject ro(finishingBattle->loserHero->id); + RemoveObject ro(finishingBattle->loserHero->id, battle.battleGetArmyObject(0)->getOwner()); gameHandler->sendAndApply(&ro); } if(finishingBattle->isDraw() && finishingBattle->winnerHero) //for draw case both heroes should be removed { - RemoveObject ro(finishingBattle->winnerHero->id); + RemoveObject ro(finishingBattle->winnerHero->id, battle.battleGetArmyObject(0)->getOwner()); gameHandler->sendAndApply(&ro); } @@ -557,7 +557,7 @@ void BattleResultProcessor::battleAfterLevelUp(const BattleID & battleID, const if (result.winner != 2 && finishingBattle->winnerHero && finishingBattle->winnerHero->stacks.empty() && (!finishingBattle->winnerHero->commander || !finishingBattle->winnerHero->commander->alive)) { - RemoveObject ro(finishingBattle->winnerHero->id); + RemoveObject ro(finishingBattle->winnerHero->id, finishingBattle->winnerHero->getOwner()); gameHandler->sendAndApply(&ro); if (VLC->settings()->getBoolean(EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS)) diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 2e129098f..f4a32c800 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -203,7 +203,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) { //Create a new boat for hero - gameHandler->createObject(targetPos , Obj::BOAT, recruitedHero->getBoatType().getNum()); + gameHandler->createObject(targetPos, player, Obj::BOAT, recruitedHero->getBoatType().getNum()); hr.boatId = gameHandler->getTopObj(targetPos)->id; } diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index fc4f1197a..8772272d6 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -61,7 +61,7 @@ void CObjectVisitQuery::onRemoval(PlayerColor color) //TODO or should it be destructor? //Can object visit affect 2 players and what would be desired behavior? if(removeObjectAfterVisit) - gh->removeObject(visitedObject); + gh->removeObject(visitedObject, color); } void CObjectVisitQuery::onExposure(QueryPtr topQuery) From a16d47037e72aa1608c7d78560c85f0a772386ab Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 19:20:22 +0300 Subject: [PATCH 0491/1248] Fix build --- mapeditor/inspector/rewardswidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 60d70d0c1..ffecec0e1 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -38,7 +38,7 @@ RewardsWidget::RewardsWidget(const CMap & m, CRewardableObject & p, QWidget *par for(const auto & s : Rewardable::SelectModeString) ui->selectMode->addItem(QString::fromStdString(s)); - for(const std::string & s : {"AUTO", "MODAL", "INFO"}) + for(auto s : {"AUTO", "MODAL", "INFO"}) ui->windowMode->addItem(QString::fromStdString(s)); ui->lDayOfWeek->addItem(tr("None")); From 2af7d7c08528e454e1acf26afd07854e2c247395 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 17:02:15 +0300 Subject: [PATCH 0492/1248] Ignore garrison update events of another towns for our castleInt --- client/CPlayerInterface.cpp | 27 +++++++++------------------ client/windows/CCastleInterface.cpp | 5 +++++ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index efde3aae0..135bd1f81 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -479,24 +479,14 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) adventureInt->onHeroChanged(nullptr); adventureInt->onTownChanged(town); - if(castleInt) - { - castleInt->garr->selectSlot(nullptr); - castleInt->garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); - castleInt->garr->setArmy(town->visitingHero, EGarrisonType::LOWER); - castleInt->garr->recreateSlots(); - castleInt->heroes->update(); - - // Perform totalRedraw to update hero list on adventure map - GH.windows().totalRedraw(); - } + for (auto gh : GH.windows().findWindows()) + gh->updateGarrisons(); for (auto ki : GH.windows().findWindows()) - { ki->townChanged(town); - ki->updateGarrisons(); - ki->redraw(); - } + + // Perform totalRedraw to update hero list on adventure map, if any dialogs are open + GH.windows().totalRedraw(); } void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) @@ -1126,7 +1116,8 @@ void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) fortScreen->creaturesChangedEventHandler(); for (auto castleInterface : GH.windows().findWindows()) - castleInterface->creaturesChangedEventHandler(); + if(castleInterface->town == town) + castleInterface->creaturesChangedEventHandler(); if (townObj) for (auto ki : GH.windows().findWindows()) @@ -1414,9 +1405,9 @@ void CPlayerInterface::objectRemovedAfter() EVENT_HANDLER_CALLED_BY_CLIENT; adventureInt->onMapTilesChanged(boost::none); - // visiting or garrisoned hero removed - recreate castle window + // visiting or garrisoned hero removed - update window if (castleInt) - openTownWindow(castleInt->town); + castleInt->updateGarrisons(); for (auto ki : GH.windows().findWindows()) ki->heroRemoved(); diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 31ebd1559..3b56eba39 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1213,7 +1213,12 @@ CCastleInterface::~CCastleInterface() void CCastleInterface::updateGarrisons() { + garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); + garr->setArmy(town->visitingHero, EGarrisonType::LOWER); garr->recreateSlots(); + heroes->update(); + + redraw(); } void CCastleInterface::close() From d3ced6b703494c6d3840ea373edd14236cd2d8d2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 19:51:31 +0300 Subject: [PATCH 0493/1248] Block revisit object shortcut if no visit is possible --- client/adventureMap/AdventureMapShortcuts.cpp | 14 +++++++++++++- client/adventureMap/AdventureMapShortcuts.h | 1 + 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index faae694fb..9087b835f 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -79,7 +79,7 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_DIG_GRAIL, optionHeroSelected(), [this]() { this->digGrail(); } }, { EShortcut::ADVENTURE_VIEW_PUZZLE, optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } }, { EShortcut::GAME_RESTART_GAME, optionInMapView(), [this]() { this->restartGame(); } }, - { EShortcut::ADVENTURE_VISIT_OBJECT, optionHeroSelected(), [this]() { this->visitObject(); } }, + { EShortcut::ADVENTURE_VISIT_OBJECT, optionCanVisitObject(), [this]() { this->visitObject(); } }, { EShortcut::ADVENTURE_VIEW_SELECTED, optionInMapView(), [this]() { this->openObject(); } }, { EShortcut::GAME_OPEN_MARKETPLACE, optionInMapView(), [this]() { this->showMarketplace(); } }, { EShortcut::ADVENTURE_ZOOM_IN, optionSidePanelActive(),[this]() { this->zoom(+1); } }, @@ -422,6 +422,18 @@ bool AdventureMapShortcuts::optionHeroAwake() return optionInMapView() && hero && !LOCPLINT->localState->isHeroSleeping(hero); } +bool AdventureMapShortcuts::optionCanVisitObject() +{ + if (!optionHeroSelected()) + return false; + + auto * hero = LOCPLINT->localState->getCurrentHero(); + auto objects = LOCPLINT->cb->getVisitableObjs(hero->visitablePos()); + + assert(vstd::contains(objects,hero)); + return objects.size() > 1; // there is object other than our hero +} + bool AdventureMapShortcuts::optionHeroSelected() { return optionInMapView() && LOCPLINT->localState->getCurrentHero() != nullptr; diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index 8e86779a7..30775c0c0 100644 --- a/client/adventureMap/AdventureMapShortcuts.h +++ b/client/adventureMap/AdventureMapShortcuts.h @@ -77,6 +77,7 @@ public: bool optionHeroSelected(); bool optionHeroCanMove(); bool optionHasNextHero(); + bool optionCanVisitObject(); bool optionSpellcasting(); bool optionInMapView(); bool optionInWorldView(); From 371d798302cca06a126176b142ec659347e3abe7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 20:09:18 +0300 Subject: [PATCH 0494/1248] Fix tests --- test/mock/mock_IGameCallback.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 6ec37e5c1..9a478c4f4 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -40,8 +40,8 @@ public: void showInfoDialog(const std::string & msg, PlayerColor player) override {} void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {} - bool removeObject(const CGObjectInstance * obj) override {return false;} - void createObject(const int3 & visitablePosition, Obj type, int32_t subtype = 0) override {}; + bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;} + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) override {}; void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} @@ -83,7 +83,7 @@ public: void setMovePoints(SetMovePoints * smp) override {} void setManaPoints(ObjectInstanceID hid, int val) override {} void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {} - void changeObjPos(ObjectInstanceID objid, int3 newPos) override {} + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override {} From 75d97e86e4f37ef0a0f0d6981c230d046e8786f1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 19 Sep 2023 23:38:00 +0200 Subject: [PATCH 0495/1248] Show progress for mod extraction --- launcher/modManager/cdownloadmanager_moc.cpp | 2 +- launcher/modManager/cmodlistview_moc.cpp | 13 ++++++++++--- launcher/modManager/cmodmanager.cpp | 15 +++++++++++++-- launcher/modManager/cmodmanager.h | 3 +++ 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/launcher/modManager/cdownloadmanager_moc.cpp b/launcher/modManager/cdownloadmanager_moc.cpp index 3a9c86471..a0e3eddaa 100644 --- a/launcher/modManager/cdownloadmanager_moc.cpp +++ b/launcher/modManager/cdownloadmanager_moc.cpp @@ -131,7 +131,7 @@ void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 byte entry.file->write(entry.reply->readAll()); entry.bytesReceived = bytesReceived; - if(bytesTotal) + if(bytesTotal > entry.totalSize) entry.totalSize = bytesTotal; quint64 total = 0; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 8adeb05e7..f4e8c7be5 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -590,6 +590,8 @@ void CModListView::downloadFile(QString file, QString url, QString description, connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)), this, SLOT(downloadFinished(QStringList,QStringList,QStringList))); + connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)), + this, SLOT(downloadProgress(qint64,qint64))); connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged); @@ -606,6 +608,7 @@ void CModListView::downloadFile(QString file, QString url, QString description, void CModListView::downloadProgress(qint64 current, qint64 max) { // display progress, in megabytes + ui->progressBar->setVisible(true); ui->progressBar->setMaximum(max / (1024 * 1024)); ui->progressBar->setValue(current / (1024 * 1024)); } @@ -640,15 +643,16 @@ void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFi doInstallFiles = true; } - // remove progress bar after some delay so user can see that download was complete and not interrupted. - QTimer::singleShot(1000, this, SLOT(hideProgressBar())); - dlManager->deleteLater(); dlManager = nullptr; + + ui->progressBar->setMaximum(0); + ui->progressBar->setValue(0); if(doInstallFiles) installFiles(savedFiles); + hideProgressBar(); emit modsChanged(); } @@ -751,7 +755,10 @@ void CModListView::installMods(QStringList archives) } for(int i = 0; i < modNames.size(); i++) + { + ui->progressBar->setFormat(tr("Installing mod %1").arg(modNames[i])); manager->installMod(modNames[i], archives[i]); + } std::function enableMod; diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 8c7ddcae4..4e0c37e59 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -284,8 +284,19 @@ bool CModManager::doInstallMod(QString modname, QString archivePath) QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract); if(!modDirName.size()) return addError(modname, "Mod archive is invalid or corrupted"); - - if(!ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract)) + + auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesToExtract]() + { + return ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract); + }); + + while(futureExtract.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready) + { + emit extractionProgress(0, 0); + qApp->processEvents(); + } + + if(!futureExtract.get()) { removeModDir(destDir + modDirName); return addError(modname, "Failed to extract mod data"); diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/cmodmanager.h index d2bc6271d..987d1a580 100644 --- a/launcher/modManager/cmodmanager.h +++ b/launcher/modManager/cmodmanager.h @@ -53,4 +53,7 @@ public: bool canUninstallMod(QString mod); bool canEnableMod(QString mod); bool canDisableMod(QString mod); + +signals: + void extractionProgress(qint64 currentAmount, qint64 maxAmount); }; From 34182069f51f5a780d08dc85f73acf074da7fc68 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 20 Sep 2023 03:13:54 +0200 Subject: [PATCH 0496/1248] campaignset --- client/CServerHandler.cpp | 2 ++ client/mainmenu/CCampaignScreen.cpp | 19 ++++++++++--------- client/mainmenu/CCampaignScreen.h | 10 ++++++---- client/mainmenu/CMainMenu.cpp | 10 ++++++---- client/mainmenu/CMainMenu.h | 4 ++-- lib/campaign/CampaignState.h | 3 +++ 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index af1238c8c..c58408076 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -689,6 +689,8 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) GH.windows().pushWindow(CMM->menu); CMM->openCampaignLobby(ourCampaign); } + else + CMM->openCampaignScreen(ourCampaign->campaignSet); }; if(epilogue.hasPrologEpilog) { diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 8a1282e2d..f4404023d 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -47,12 +47,12 @@ #include "../../lib/mapObjects/CGHeroInstance.h" -CCampaignScreen::CCampaignScreen(const JsonNode & config) - : CWindowObject(BORDERED) +CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) + : CWindowObject(BORDERED), campaignSet(name) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - for(const JsonNode & node : config["images"].Vector()) + for(const JsonNode & node : config[name]["images"].Vector()) images.push_back(CMainMenu::createPicture(node)); if(!images.empty()) @@ -63,14 +63,14 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config) pos = images[0]->pos; // fix height\width of this window } - if(!config["exitbutton"].isNull()) + if(!config[name]["exitbutton"].isNull()) { - buttonBack = createExitButton(config["exitbutton"]); + buttonBack = createExitButton(config[name]["exitbutton"]); buttonBack->hoverable = true; } - for(const JsonNode & node : config["items"].Vector()) - campButtons.push_back(std::make_shared(node)); + for(const JsonNode & node : config[name]["items"].Vector()) + campButtons.push_back(std::make_shared(node, campaignSet)); } void CCampaignScreen::activate() @@ -89,7 +89,8 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), AnimationPath::fromJson(button["name"]), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); } -CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config) +CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std::string set) + : set(set) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -134,7 +135,7 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to) void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) { CCS->videoh->close(); - CMainMenu::openCampaignLobby(campFile); + CMainMenu::openCampaignLobby(campFile, set); } void CCampaignScreen::CCampaignButton::hover(bool on) diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index f430b3e9a..4e450d7dd 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -40,14 +40,18 @@ private: VideoPath video; // the resource name of the video std::string hoverText; + std::string set; + void clickReleased(const Point & cursorPosition) override; void hover(bool on) override; public: - CCampaignButton(const JsonNode & config); + CCampaignButton(const JsonNode & config, std::string set); void show(Canvas & to) override; }; + std::string campaignSet; + std::vector> campButtons; std::vector> images; std::shared_ptr buttonBack; @@ -55,9 +59,7 @@ private: std::shared_ptr createExitButton(const JsonNode & button); public: - enum CampaignSet {ROE, AB, SOD, WOG}; - - CCampaignScreen(const JsonNode & config); + CCampaignScreen(const JsonNode & config, std::string name); void activate() override; }; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 0327ff4ac..af0765360 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -349,14 +349,16 @@ void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vec GH.windows().createAndPushWindow(host); } -void CMainMenu::openCampaignLobby(const std::string & campaignFileName) +void CMainMenu::openCampaignLobby(const std::string & campaignFileName, std::string campaignSet) { auto ourCampaign = CampaignHandler::getCampaign(campaignFileName); - openCampaignLobby(ourCampaign); + openCampaignLobby(ourCampaign, campaignSet); } -void CMainMenu::openCampaignLobby(std::shared_ptr campaign) +void CMainMenu::openCampaignLobby(std::shared_ptr campaign, std::string campaignSet) { + campaign->campaignSet = campaignSet; + CSH->resetStateForLobby(StartInfo::CAMPAIGN); CSH->screenType = ESelectionScreen::campaignList; CSH->campaignStateToSend = campaign; @@ -367,7 +369,7 @@ void CMainMenu::openCampaignScreen(std::string name) { if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name)) { - GH.windows().createAndPushWindow(CMainMenuConfig::get().getCampaigns()[name]); + GH.windows().createAndPushWindow(CMainMenuConfig::get().getCampaigns(), name); return; } logGlobal->error("Unknown campaign set: %s", name); diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 327fdb908..562271704 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -149,8 +149,8 @@ public: void onScreenResize() override; void update() override; static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); - static void openCampaignLobby(const std::string & campaignFileName); - static void openCampaignLobby(std::shared_ptr campaign); + static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); + static void openCampaignLobby(std::shared_ptr campaign, std::string campaignSet = ""); static void startTutorial(); void openCampaignScreen(std::string name); diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 247d86fa4..4c091fa16 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -293,6 +293,8 @@ public: static JsonNode crossoverSerialize(CGHeroInstance * hero); static CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map); + std::string campaignSet = ""; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -302,6 +304,7 @@ public: h & mapsConquered; h & currentMap; h & chosenCampaignBonuses; + h & campaignSet; } }; From 0de912e67b7b581b586b6f9e7a6853363722dfb4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 20 Sep 2023 03:50:35 +0200 Subject: [PATCH 0497/1248] Add basic system to add more advantages for ai player --- config/difficulty.json | 89 ++++++++++++++++++++++++++++++ config/startres.json | 31 ----------- lib/CPlayerState.h | 2 + lib/gameState/CGameState.cpp | 30 +++++++++- lib/gameState/CGameState.h | 1 + server/battles/BattleProcessor.cpp | 14 +++++ 6 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 config/difficulty.json delete mode 100644 config/startres.json diff --git a/config/difficulty.json b/config/difficulty.json new file mode 100644 index 000000000..97d7dbe0d --- /dev/null +++ b/config/difficulty.json @@ -0,0 +1,89 @@ +// Starting resources, ordered by difficulty level (0 to 4) +{ + "startres": + [ + { + "human": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + "ai": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 } + }, + + { + "human": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, + "ai": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 } + }, + + { + "human": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, + "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + }, + + { + "human": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, + "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + }, + + { + "human": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + } + ], + "battleBonus": + [ + { + }, + + { + "ai": + [ + { + "type" : "STACK_HEALTH", + "val" : 50, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" + } + ], + }, + + { + "ai": + [ + { + "type" : "STACK_HEALTH", + "val" : 100, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" + } + ], + }, + + { + "ai": + [ + { + "type" : "STACK_HEALTH", + "val" : 150, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" + }, + ], + }, + + { + "ai": + [ + { + "type" : "STACK_HEALTH", + "val" : 200, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" + }, + + ], + }, + ] +} + diff --git a/config/startres.json b/config/startres.json deleted file mode 100644 index dc96714e1..000000000 --- a/config/startres.json +++ /dev/null @@ -1,31 +0,0 @@ -// Starting resources, ordered by difficulty level (0 to 4) -{ - "difficulty": - [ - { - "human": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - "ai": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 } - }, - - { - "human": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, - "ai": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 } - }, - - { - "human": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - }, - - { - "human": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } - } - ] -} - diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index ac8c1d4c6..b870359df 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -37,6 +37,7 @@ public: std::vector > towns; std::vector > dwellings; //used for town growth std::vector quests; //store info about all received quests + std::vector battleBonuses; //additional bonuses to be added during battle with neutrals bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory EPlayerStatus status; @@ -82,6 +83,7 @@ public: h & visitedObjects; h & status; h & daysWithoutCastle; + h & battleBonuses; h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index d51e36c66..3ceaf136d 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -450,6 +450,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initPlayerStates(); if (campaign) campaign->placeCampaignHeroes(); + initBattleBonuses(); removeHeroPlaceholders(); initGrailPosition(); initRandomFactionsForPlayers(); @@ -657,6 +658,31 @@ void CGameState::initGlobalBonuses() VLC->creh->loadCrExpBon(globalEffects); } +void CGameState::initBattleBonuses() +{ + logGlobal->debug("\tLoading battle bonuses up resources"); + const JsonNode config(JsonPath::builtin("config/difficulty.json")); + const JsonVector &vector = config["battleBonus"].Vector(); + const JsonNode &level = vector[scenarioOps->difficulty]; + const JsonNode & bonusesAI(level["ai"]); + const JsonNode & bonusesHuman(level["human"]); + + for (auto & elem : players) + { + PlayerState &p = elem.second; + if(p.human) + { + for(auto & jsonBonus : bonusesHuman.Vector()) + p.battleBonuses.push_back(*JsonUtils::parseBonus(jsonBonus)); + } + else + { + for(auto & jsonBonus : bonusesAI.Vector()) + p.battleBonuses.push_back(*JsonUtils::parseBonus(jsonBonus)); + } + } +} + void CGameState::initGrailPosition() { logGlobal->debug("\tPicking grail position"); @@ -816,8 +842,8 @@ void CGameState::removeHeroPlaceholders() void CGameState::initStartingResources() { logGlobal->debug("\tSetting up resources"); - const JsonNode config(JsonPath::builtin("config/startres.json")); - const JsonVector &vector = config["difficulty"].Vector(); + const JsonNode config(JsonPath::builtin("config/difficulty.json")); + const JsonVector &vector = config["startres"].Vector(); const JsonNode &level = vector[scenarioOps->difficulty]; TResources startresAI(level["ai"]); diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 63123b5d9..23c2f2803 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -184,6 +184,7 @@ private: void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); void checkMapChecksum(); void initGlobalBonuses(); + void initBattleBonuses(); void initGrailPosition(); void initRandomFactionsForPlayers(); void randomizeMapObjects(); diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index ea03a3433..19fd0426b 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -24,6 +24,7 @@ #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" #include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/CPlayerState.h" BattleProcessor::BattleProcessor(CGameHandler * gameHandler) : gameHandler(gameHandler) @@ -113,6 +114,19 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm const auto * battle = gameHandler->gameState()->getBattle(battleID); assert(battle); + + //add battle bonuses based from player state only when attacks neutral creatures + const auto * attackerInfo = gameHandler->getPlayerState(army1->getOwner(), false); + if(attackerInfo && army2->getOwner() == PlayerColor::NEUTRAL) + { + for(auto bonus : attackerInfo->battleBonuses) + { + GiveBonus giveBonus(GiveBonus::ETarget::HERO); + giveBonus.id = hero1->id.getNum(); + giveBonus.bonus = bonus; + gameHandler->sendAndApply(&giveBonus); + } + } auto lastBattleQuery = std::dynamic_pointer_cast(gameHandler->queries->topQuery(battle->sides[0].color)); From cb3bbc6680dae8578309735e9c82c1b2008339e5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 20 Sep 2023 16:12:26 +0200 Subject: [PATCH 0498/1248] Restructured difficulty config --- config/difficulty.json | 115 +++++++++++++------------------- lib/constants/StringConstants.h | 2 + lib/gameState/CGameState.cpp | 68 ++++++++----------- lib/gameState/CGameState.h | 3 +- 4 files changed, 79 insertions(+), 109 deletions(-) diff --git a/config/difficulty.json b/config/difficulty.json index 97d7dbe0d..2ee96a7ad 100644 --- a/config/difficulty.json +++ b/config/difficulty.json @@ -1,89 +1,70 @@ -// Starting resources, ordered by difficulty level (0 to 4) +//Configured difficulty { - "startres": - [ + "human": + { + "pawn": { - "human": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - "ai": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 } + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "knight": { - "human": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, - "ai": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 } + "resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "rook": { - "human": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "queen": { - "human": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "king": { - "human": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, - "ai": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 } + "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] } - ], - "battleBonus": - [ + }, + "ai": + { + "pawn": { + "resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "knight": { - "ai": - [ - { - "type" : "STACK_HEALTH", - "val" : 50, - "valueType" : "PERCENT_TO_ALL", - "duration" : "ONE_BATTLE", - "sourceType" : "OTHER" - } - ], + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "rook": { - "ai": - [ - { - "type" : "STACK_HEALTH", - "val" : 100, - "valueType" : "PERCENT_TO_ALL", - "duration" : "ONE_BATTLE", - "sourceType" : "OTHER" - } - ], + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "queen": { - "ai": - [ - { - "type" : "STACK_HEALTH", - "val" : 150, - "valueType" : "PERCENT_TO_ALL", - "duration" : "ONE_BATTLE", - "sourceType" : "OTHER" - }, - ], + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] }, - + "king": { - "ai": - [ - { - "type" : "STACK_HEALTH", - "val" : 200, - "valueType" : "PERCENT_TO_ALL", - "duration" : "ONE_BATTLE", - "sourceType" : "OTHER" - }, - - ], - }, - ] + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, } diff --git a/lib/constants/StringConstants.h b/lib/constants/StringConstants.h index cf8d12a6a..d7b40fd92 100644 --- a/lib/constants/StringConstants.h +++ b/lib/constants/StringConstants.h @@ -27,6 +27,8 @@ namespace GameConstants }; const std::string ALIGNMENT_NAMES [3] = {"good", "evil", "neutral"}; + + const std::string DIFFICULTY_NAMES [5] = {"pawn", "knight", "rook", "queen", "king"}; } namespace NPrimarySkill diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 3ceaf136d..a3e688675 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -29,6 +29,7 @@ #include "../VCMI_Lib.h" #include "../battle/BattleInfo.h" #include "../campaign/CampaignState.h" +#include "../constants/StringConstants.h" #include "../filesystem/ResourcePath.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -450,13 +451,12 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initPlayerStates(); if (campaign) campaign->placeCampaignHeroes(); - initBattleBonuses(); removeHeroPlaceholders(); initGrailPosition(); initRandomFactionsForPlayers(); randomizeMapObjects(); placeStartingHeroes(); - initStartingResources(); + initDifficulty(); initHeroes(); initStartingBonus(); initTowns(); @@ -658,29 +658,41 @@ void CGameState::initGlobalBonuses() VLC->creh->loadCrExpBon(globalEffects); } -void CGameState::initBattleBonuses() +void CGameState::initDifficulty() { - logGlobal->debug("\tLoading battle bonuses up resources"); + logGlobal->debug("\tLoading difficulty settings"); const JsonNode config(JsonPath::builtin("config/difficulty.json")); + const JsonVector &vector = config["battleBonus"].Vector(); const JsonNode &level = vector[scenarioOps->difficulty]; - const JsonNode & bonusesAI(level["ai"]); - const JsonNode & bonusesHuman(level["human"]); + const JsonNode & difficultyAI(level["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + const JsonNode & difficultyHuman(level["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + + auto setDifficulty = [](PlayerState & state, const JsonNode & json) + { + //set starting resources + state.resources = TResources(json["resources"]); + + //set global bonuses + for(auto & jsonBonus : json["globalBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.addNewBonus(bonus); + + //set battle bonuses + for(auto & jsonBonus : json["battleBonuses"].Vector()) + if(auto bonus = JsonUtils::parseBonus(jsonBonus)) + state.battleBonuses.push_back(*bonus); + + }; for (auto & elem : players) { PlayerState &p = elem.second; - if(p.human) - { - for(auto & jsonBonus : bonusesHuman.Vector()) - p.battleBonuses.push_back(*JsonUtils::parseBonus(jsonBonus)); - } - else - { - for(auto & jsonBonus : bonusesAI.Vector()) - p.battleBonuses.push_back(*JsonUtils::parseBonus(jsonBonus)); - } + setDifficulty(p, p.human ? difficultyHuman : difficultyAI); } + + if (campaign) + campaign->initStartingResources(); } void CGameState::initGrailPosition() @@ -839,30 +851,6 @@ void CGameState::removeHeroPlaceholders() } } -void CGameState::initStartingResources() -{ - logGlobal->debug("\tSetting up resources"); - const JsonNode config(JsonPath::builtin("config/difficulty.json")); - const JsonVector &vector = config["startres"].Vector(); - const JsonNode &level = vector[scenarioOps->difficulty]; - - TResources startresAI(level["ai"]); - TResources startresHuman(level["human"]); - - for (auto & elem : players) - { - PlayerState &p = elem.second; - - if (p.human) - p.resources = startresHuman; - else - p.resources = startresAI; - } - - if (campaign) - campaign->initStartingResources(); -} - void CGameState::initHeroes() { for(auto hero : map->heroesOnMap) //heroes instances initialization diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 23c2f2803..8ba75e2f9 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -184,7 +184,6 @@ private: void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); void checkMapChecksum(); void initGlobalBonuses(); - void initBattleBonuses(); void initGrailPosition(); void initRandomFactionsForPlayers(); void randomizeMapObjects(); @@ -193,7 +192,7 @@ private: void placeStartingHeroes(); void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); void removeHeroPlaceholders(); - void initStartingResources(); + void initDifficulty(); void initHeroes(); void placeHeroesInTowns(); void initFogOfWar(); From f8b8ab09654671b0aef515d2a80e0138e0e31c02 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 20 Sep 2023 16:35:28 +0200 Subject: [PATCH 0499/1248] Add documentation for difficulty.json --- docs/modders/Difficulty.md | 67 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 docs/modders/Difficulty.md diff --git a/docs/modders/Difficulty.md b/docs/modders/Difficulty.md new file mode 100644 index 000000000..75c44d768 --- /dev/null +++ b/docs/modders/Difficulty.md @@ -0,0 +1,67 @@ +< [Documentation](../Readme.md) / [Modding](Readme.md) / Difficulty + + +Since VCMI 1.4.0 there are more capabilities to configure difficulty parameters. +It means, that modders can give different bonuses to AI or human players depending on selected difficulty + +Difficulty configuration is located in [config/difficulty.json](../config/difficulty.json) file and can be overriden by mods. + +## Format summary + +``` javascript +{ + "human": //parameters impacting human players only + { + "pawn": //parameters for specific difficulty + { + //starting resources + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + //bonuses will be given to player globaly + "globalBonuses": [], + //bonuses will be given to player every battle + "battleBonuses": [] + }, + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + }, + "ai": //parameters impacting AI players only + { + "pawn": {}, //parameters for specific difficulty + "knight": {}, + "rook": {}, + "queen": {}, + "king": {}, + } +} +``` + +## Bonuses + +It's possible to specify bonuses of two types: `globalBonuses` and `battleBonuses`. + +Both are arrays containing any amount of bonuses, each can be described as usual bonus. See details in [bonus documenation](Bonus_Format.md). + +`globalBonuses` are given to player on the begining and depending on bonus configuration, it can behave diffierently. + +`battleBonuses` are given to player during the battles, but *only for battles with neutral forces*. So it won't be provided to player for PvP battles and battles versus AI heroes/castles/garrisons. To avoid cumulative effects or unexpected behavior it's recommended to specify bonus `duration` as `ONE_BATTLE`. + +For both types of bonuses, `source` should be specified as `OTHER`. + +## Example + +```js +{ //will give 150% extra health to all players' creatures if specified in "battleBonuses" array + "type" : "STACK_HEALTH", + "val" : 150, + "valueType" : "PERCENT_TO_ALL", + "duration" : "ONE_BATTLE", + "sourceType" : "OTHER" +}, +``` + +## Compatibility + +Starting from VCMI 1.4 `startres.json` is not available anymore and will be ignored if present in any mod. +Thus, `Resourceful AI` mod of version 1.2 won't work anymore. \ No newline at end of file From 921569e02e97597e74f47023c10a8a483c3b1f62 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 21:58:47 +0300 Subject: [PATCH 0500/1248] Fixed "Long Live the King", 1st scenario victory condition --- config/mapOverrides.json | 40 +++++++++++++++++++++++++++++++++++ lib/MetaString.cpp | 7 +++--- lib/mapping/MapFormatJson.cpp | 5 +++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/config/mapOverrides.json b/config/mapOverrides.json index c275c9ee7..9a520fe7c 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -75,6 +75,46 @@ "victoryIconIndex" : 11, "victoryString" : "core.vcdesc.0" }, + "data/evil2:0" : { // A Gryphon's Heart + "defeatIconIndex" : 2, + "defeatString" : "core.lcdesc.3", + "triggeredEvents" : { + "specialDefeat" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "daysPassed", { "value" : 84 } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.5", + "type" : "defeat" + }, + "message" : "core.genrltxt.254" + }, + "specialVictory" : { + "condition" : [ + "allOf", + [ "isHuman", { "value" : 1 } ], + [ "transport", { "position" : [ 16, 23, 0 ], "type" : 84 } ] + ], + "effect" : { + "messageToSend" : "core.genrltxt.293", + "type" : "victory" + }, + "message" : "core.genrltxt.292" + }, + "standardDefeat" : { + "condition" : [ "daysWithoutTown", { "value" : 7 } ], + "effect" : { + "messageToSend" : "core.genrltxt.8", + "type" : "defeat" + }, + "message" : "core.genrltxt.7" + } + }, + "victoryIconIndex" : 10, + "victoryString" : "core.vcdesc.11" + }, "data/secret1:0" : { // The Grail "defeatIconIndex" : 2, "defeatString" : "core.lcdesc.3", diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 86a1badfe..3c9eac22c 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -388,12 +388,11 @@ void MetaString::jsonDeserialize(const JsonNode & source) void MetaString::serializeJson(JsonSerializeFormat & handler) { - JsonNode attr; if(handler.saving) - jsonSerialize(attr); - handler.serializeRaw("attributes", attr, std::nullopt); + jsonSerialize(const_cast(handler.getCurrent())); + if(!handler.saving) - jsonDeserialize(attr); + jsonDeserialize(handler.getCurrent()); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c8aed2682..42c76415b 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -870,6 +870,11 @@ void CMapPatcher::readPatchData() { JsonDeserializer handler(mapObjectResolver.get(), input); readTriggeredEvents(handler); + + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + handler.serializeStruct("victoryString", mapHeader->victoryMessage); + handler.serializeStruct("defeatString", mapHeader->defeatMessage); } ///CMapLoaderJson From 02dfecd38bc9619afab7bcfd4156951fe6915c22 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 21:59:16 +0300 Subject: [PATCH 0501/1248] Fixed hota maps templates loading --- lib/mapping/MapIdentifiersH3M.cpp | 12 ++++++------ lib/mapping/MapIdentifiersH3M.h | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 05c35020c..9935519d8 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -53,11 +53,11 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) for (auto entryTemplate : mapping["templates"].Struct()) { - std::string h3mName = boost::to_lower_copy(entryTemplate.second.String()); - std::string vcmiName = boost::to_lower_copy(entryTemplate.first); + AnimationPath h3mName = AnimationPath::builtinTODO(entryTemplate.second.String()); + AnimationPath vcmiName = AnimationPath::builtinTODO(entryTemplate.first); - if (!CResourceHandler::get()->existsResource(AnimationPath::builtinTODO("SPRITES/" + vcmiName))) - logMod->warn("Template animation file %s was not found!", vcmiName); + if (!CResourceHandler::get()->existsResource(vcmiName.addPrefix("SPRITES/"))) + logMod->warn("Template animation file %s was not found!", vcmiName.getOriginalName()); mappingObjectTemplate[h3mName] = vcmiName; } @@ -108,10 +108,10 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) void MapIdentifiersH3M::remapTemplate(ObjectTemplate & objectTemplate) { - std::string name = boost::to_lower_copy(objectTemplate.animationFile.getName()); + auto name = objectTemplate.animationFile; if (mappingObjectTemplate.count(name)) - objectTemplate.animationFile = AnimationPath::builtinTODO(mappingObjectTemplate.at(name)); + objectTemplate.animationFile = mappingObjectTemplate.at(name); ObjectTypeIdentifier objectType{ objectTemplate.id, objectTemplate.subid}; diff --git a/lib/mapping/MapIdentifiersH3M.h b/lib/mapping/MapIdentifiersH3M.h index 71cea67dc..581a80fda 100644 --- a/lib/mapping/MapIdentifiersH3M.h +++ b/lib/mapping/MapIdentifiersH3M.h @@ -11,6 +11,7 @@ #pragma once #include "../GameConstants.h" +#include "../filesystem/ResourcePath.h" VCMI_LIB_NAMESPACE_BEGIN @@ -43,7 +44,7 @@ class MapIdentifiersH3M std::map mappingArtifact; std::map mappingSecondarySkill; - std::map mappingObjectTemplate; + std::map mappingObjectTemplate; std::map mappingObjectIndex; template From 3f35ed000cf73f8ed435d4addbc21c53d2d6e075 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 22:00:03 +0300 Subject: [PATCH 0502/1248] Fixed parsing of some user-made H3M maps --- lib/mapping/MapFeaturesH3M.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 62 +++++++++++++++++++++------------- lib/mapping/MapReaderH3M.cpp | 21 ++++++++++-- 3 files changed, 58 insertions(+), 27 deletions(-) diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 4be3af4d4..c0a7d5f1f 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -49,7 +49,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() result.factionsCount = 8; result.heroesCount = 128; - result.heroesPortraitsCount = 128; + result.heroesPortraitsCount = 130; // +General Kendal, +Catherine (portrait-only in RoE) result.artifactsCount = 127; result.resourcesCount = 7; result.creaturesCount = 118; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 5c95fffae..6f4a0a38e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -44,6 +44,7 @@ static std::string convertMapName(std::string input) { boost::algorithm::to_lower(input); boost::algorithm::trim(input); + boost::algorithm::erase_all(input, "."); size_t slashPos = input.find_last_of('/'); @@ -345,27 +346,11 @@ void CMapLoaderH3M::readVictoryLossConditions() bool allowNormalVictory = reader->readBool(); bool appliesToAI = reader->readBool(); - if(allowNormalVictory) - { - size_t playersOnMap = boost::range::count_if( - mapHeader->players, - [](const PlayerInfo & info) - { - return info.canAnyonePlay(); - } - ); - - if(playersOnMap == 1) - { - logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); - allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! - } - } - switch(vicCondition) { case EVictoryConditionType::ARTIFACT: { + assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_ARTIFACT); cond.objectType = reader->readArtifact(); @@ -404,6 +389,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BUILDCITY: { + assert(appliesToAI == true); // not selectable in editor EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); @@ -421,6 +407,8 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BUILDGRAIL: { + assert(allowNormalVictory == true); // not selectable in editor + assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_BUILDING); cond.objectType = BuildingID::GRAIL; cond.position = reader->readInt3(); @@ -436,6 +424,10 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BEATHERO: { + if (!allowNormalVictory) + logGlobal->warn("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); + allowNormalVictory = true; // H3 behavior + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::HERO; cond.position = reader->readInt3(); @@ -462,6 +454,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::BEATMONSTER: { + assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::MONSTER; cond.position = reader->readInt3(); @@ -500,6 +493,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::TRANSPORTITEM: { + assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::TRANSPORT); cond.objectType = reader->readUInt8(); cond.position = reader->readInt3(); @@ -513,6 +507,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: { + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); cond.objectType = Obj::MONSTER; @@ -526,6 +521,7 @@ void CMapLoaderH3M::readVictoryLossConditions() } case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: { + assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DAYS_PASSED); cond.value = reader->readUInt32(); @@ -541,6 +537,24 @@ void CMapLoaderH3M::readVictoryLossConditions() assert(0); } + if(allowNormalVictory) + { + size_t playersOnMap = boost::range::count_if( + mapHeader->players, + [](const PlayerInfo & info) + { + return info.canAnyonePlay(); + } + ); + + assert(playersOnMap > 1); + if(playersOnMap == 1) + { + logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); + allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! + } + } + // if condition is human-only turn it into following construction: AllOf(human, condition) if(!appliesToAI) { @@ -883,7 +897,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) { - logGlobal->warn("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); + logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); hero->artifactsInBackpack.clear(); while(!hero->artifactsWorn.empty()) @@ -1551,6 +1565,9 @@ void CMapLoaderH3M::readObjects() newObject->appearance = objectTemplate; assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); + if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) + logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString()); + { //TODO: define valid typeName and subtypeName for H3M maps //boost::format fmt("%s_%d"); @@ -1703,7 +1720,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec if(!object->secSkills.empty()) { if(object->secSkills[0].first != SecondarySkill::DEFAULT) - logGlobal->warn("Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); + logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); object->secSkills.clear(); } @@ -1775,7 +1792,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); if(ps->size()) { - logGlobal->warn("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); + logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); for(const auto & b : *ps) object->removeBonus(b); } @@ -1907,8 +1924,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con auto rVal = reader->readUInt32(); assert(rId < features.resourcesCount); - assert((rVal & 0x00ffffff) == rVal); - + reward.resources[rId] = rVal; break; } @@ -2172,7 +2188,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt uint8_t alignment = reader->readUInt8(); - if(alignment != PlayerColor::NEUTRAL.getNum()) + if(alignment != 255) { if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) { diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index f7328b9de..306279104 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -113,7 +113,12 @@ int32_t MapReaderH3M::readHeroPortrait() if(result.getNum() == features.heroIdentifierInvalid) return int32_t(-1); - assert(result.getNum() < features.heroesPortraitsCount); + if (result.getNum() >= features.heroesPortraitsCount) + { + logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); + return int32_t(-1); + } + return remapper.remapPortrrait(result); } @@ -199,7 +204,12 @@ PlayerColor MapReaderH3M::readPlayer() if (value == 255) return PlayerColor::NEUTRAL; - assert(value < PlayerColor::PLAYER_LIMIT_I); + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + return PlayerColor(value); } @@ -210,7 +220,12 @@ PlayerColor MapReaderH3M::readPlayer32() if (value == 255) return PlayerColor::NEUTRAL; - assert(value < PlayerColor::PLAYER_LIMIT_I); + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + return PlayerColor(value); } From 52b86eb9c128755545dc9c13018b1a4137efbf4d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 20 Sep 2023 21:18:13 +0200 Subject: [PATCH 0503/1248] get back to campaignmenu --- client/CServerHandler.cpp | 4 ++++ client/mainmenu/CCampaignScreen.cpp | 6 +++--- client/mainmenu/CCampaignScreen.h | 6 +++--- client/mainmenu/CMainMenu.cpp | 7 +++---- client/mainmenu/CMainMenu.h | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index c58408076..1df5a852a 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -690,7 +690,11 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) CMM->openCampaignLobby(ourCampaign); } else + { + GH.windows().pushWindow(CMM); + GH.windows().pushWindow(CMM->menu); CMM->openCampaignScreen(ourCampaign->campaignSet); + } }; if(epilogue.hasPrologEpilog) { diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index f4404023d..c4ea92120 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -89,8 +89,8 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), AnimationPath::fromJson(button["name"]), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); } -CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std::string set) - : set(set) +CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std::string campaignSet) + : campaignSet(campaignSet) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -135,7 +135,7 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to) void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition) { CCS->videoh->close(); - CMainMenu::openCampaignLobby(campFile, set); + CMainMenu::openCampaignLobby(campFile, campaignSet); } void CCampaignScreen::CCampaignButton::hover(bool on) diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index 4e450d7dd..c9abc2a59 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -40,13 +40,13 @@ private: VideoPath video; // the resource name of the video std::string hoverText; - std::string set; + std::string campaignSet; void clickReleased(const Point & cursorPosition) override; void hover(bool on) override; public: - CCampaignButton(const JsonNode & config, std::string set); + CCampaignButton(const JsonNode & config, std::string campaignSet); void show(Canvas & to) override; }; @@ -59,7 +59,7 @@ private: std::shared_ptr createExitButton(const JsonNode & button); public: - CCampaignScreen(const JsonNode & config, std::string name); + CCampaignScreen(const JsonNode & config, std::string campaignSet); void activate() override; }; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index af0765360..119763fed 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -352,13 +352,12 @@ void CMainMenu::openLobby(ESelectionScreen screenType, bool host, const std::vec void CMainMenu::openCampaignLobby(const std::string & campaignFileName, std::string campaignSet) { auto ourCampaign = CampaignHandler::getCampaign(campaignFileName); - openCampaignLobby(ourCampaign, campaignSet); + ourCampaign->campaignSet = campaignSet; + openCampaignLobby(ourCampaign); } -void CMainMenu::openCampaignLobby(std::shared_ptr campaign, std::string campaignSet) +void CMainMenu::openCampaignLobby(std::shared_ptr campaign) { - campaign->campaignSet = campaignSet; - CSH->resetStateForLobby(StartInfo::CAMPAIGN); CSH->screenType = ESelectionScreen::campaignList; CSH->campaignStateToSend = campaign; diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index 562271704..f0e246c60 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -150,7 +150,7 @@ public: void update() override; static void openLobby(ESelectionScreen screenType, bool host, const std::vector * names, ELoadMode loadMode); static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); - static void openCampaignLobby(std::shared_ptr campaign, std::string campaignSet = ""); + static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); void openCampaignScreen(std::string name); From dfb5ccbeafb4910cd6a79f7bca8f2d97b301f59e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 20 Sep 2023 22:18:53 +0200 Subject: [PATCH 0504/1248] add persistent storage & completed campaign support --- client/CServerHandler.cpp | 3 +++ client/mainmenu/CCampaignScreen.cpp | 3 +++ lib/CConfigHandler.cpp | 24 +++++++++++++++++------- lib/CConfigHandler.h | 6 +++++- lib/VCMI_Lib.cpp | 1 + 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 1df5a852a..01e05ed79 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -683,6 +683,9 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto finisher = [=]() { + Settings entry = persistent.write["campaign"][ourCampaign->campaignSet][ourCampaign->getFilename()]["completed"]; + entry->Bool() = true; + if(!ourCampaign->isCampaignFinished()) { GH.windows().pushWindow(CMM); diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index c4ea92120..731750016 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -107,6 +107,9 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std:: auto header = CampaignHandler::getHeader(campFile); hoverText = header->getName(); + if(persistent["campaign"][campaignSet][header->getFilename()]["completed"].Bool()) + status = CCampaignScreen::COMPLETED; + if(status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 6e507dbe7..f82807876 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN SettingsStorage settings; +SettingsStorage persistent; template SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): @@ -53,18 +54,26 @@ SettingsStorage::SettingsStorage(): { } -void SettingsStorage::init() +void SettingsStorage::init(bool p) { - JsonPath confName = JsonPath::builtin("config/settings.json"); + persistentStorage = p; + cfgName = "config/settings.json"; + if(persistentStorage) + cfgName = "config/persistent.json"; + + JsonPath confName = JsonPath::builtin(cfgName); JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); // Probably new install. Create config file to save settings to if (!CResourceHandler::get("local")->existsResource(confName)) - CResourceHandler::get("local")->createResource("config/settings.json"); + CResourceHandler::get("local")->createResource(cfgName); - JsonUtils::maximize(config, "vcmi:settings"); - JsonUtils::validate(config, "vcmi:settings", "settings"); + if(!persistentStorage) + { + JsonUtils::maximize(config, "vcmi:settings"); + JsonUtils::validate(config, "vcmi:settings", "settings"); + } } void SettingsStorage::invalidateNode(const std::vector &changedPath) @@ -74,9 +83,10 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath JsonNode savedConf = config; savedConf.Struct().erase("session"); - JsonUtils::minimize(savedConf, "vcmi:settings"); + if(!persistentStorage) + JsonUtils::minimize(savedConf, "vcmi:settings"); - std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin("config/settings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(cfgName))->c_str(), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/CConfigHandler.h b/lib/CConfigHandler.h index 6a526f512..187c7c1fc 100644 --- a/lib/CConfigHandler.h +++ b/lib/CConfigHandler.h @@ -35,6 +35,9 @@ class DLL_LINKAGE SettingsStorage std::set listeners; JsonNode config; + bool persistentStorage; + std::string cfgName; + JsonNode & getNode(const std::vector & path); // Calls all required listeners @@ -45,7 +48,7 @@ class DLL_LINKAGE SettingsStorage public: // Initialize config structure SettingsStorage(); - void init(); + void init(bool persistent = false); // Get write access to config node at path const NodeAccessor write; @@ -113,5 +116,6 @@ public: }; extern DLL_LINKAGE SettingsStorage settings; +extern DLL_LINKAGE SettingsStorage persistent; VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index e20444152..0eef646c6 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -53,6 +53,7 @@ DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool VLC = new LibClasses(); VLC->loadFilesystem(extractArchives); settings.init(); + persistent.init(true); VLC->loadModFilesystem(onlyEssential); } From 48231f5f4ff1cff0843f3b018d6bd78a3fec34c0 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 20 Sep 2023 22:28:45 +0200 Subject: [PATCH 0505/1248] save results only if from set --- client/CServerHandler.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 01e05ed79..3a55d2b0d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -683,8 +683,11 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog; auto finisher = [=]() { - Settings entry = persistent.write["campaign"][ourCampaign->campaignSet][ourCampaign->getFilename()]["completed"]; - entry->Bool() = true; + if(ourCampaign->campaignSet != "") + { + Settings entry = persistent.write["campaign"][ourCampaign->campaignSet][ourCampaign->getFilename()]["completed"]; + entry->Bool() = true; + } if(!ourCampaign->isCampaignFinished()) { From 4d8414bd3d2d381d74b33632ddd1b871745c0180 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 20 Sep 2023 23:06:32 +0200 Subject: [PATCH 0506/1248] unlock functionality --- client/mainmenu/CCampaignScreen.cpp | 16 +++++++-- client/mainmenu/CCampaignScreen.h | 2 +- config/campaignSets.json | 53 +++++++++++------------------ 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 731750016..bae48a891 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -70,7 +70,7 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name) } for(const JsonNode & node : config[name]["items"].Vector()) - campButtons.push_back(std::make_shared(node, campaignSet)); + campButtons.push_back(std::make_shared(node, config, campaignSet)); } void CCampaignScreen::activate() @@ -89,7 +89,7 @@ std::shared_ptr CCampaignScreen::createExitButton(const JsonNode & butt return std::make_shared(Point((int)button["x"].Float(), (int)button["y"].Float()), AnimationPath::fromJson(button["name"]), help, [=](){ close();}, EShortcut::GLOBAL_CANCEL); } -CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std::string campaignSet) +CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet) : campaignSet(campaignSet) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -102,7 +102,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std:: campFile = config["file"].String(); video = VideoPath::fromJson(config["video"]); - status = config["open"].Bool() ? CCampaignScreen::ENABLED : CCampaignScreen::DISABLED; + status = CCampaignScreen::ENABLED; auto header = CampaignHandler::getHeader(campFile); hoverText = header->getName(); @@ -110,6 +110,16 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, std:: if(persistent["campaign"][campaignSet][header->getFilename()]["completed"].Bool()) status = CCampaignScreen::COMPLETED; + for(const JsonNode & node : parentConfig[campaignSet]["items"].Vector()) + { + for(const JsonNode & requirement : config["requires"].Vector()) + { + if(node["id"].Integer() == requirement.Integer()) + if(!persistent["campaign"][campaignSet][node["file"].String()]["completed"].Bool()) + status = CCampaignScreen::DISABLED; + } + } + if(status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); diff --git a/client/mainmenu/CCampaignScreen.h b/client/mainmenu/CCampaignScreen.h index c9abc2a59..e19395974 100644 --- a/client/mainmenu/CCampaignScreen.h +++ b/client/mainmenu/CCampaignScreen.h @@ -46,7 +46,7 @@ private: void hover(bool on) override; public: - CCampaignButton(const JsonNode & config, std::string campaignSet); + CCampaignButton(const JsonNode & config, const JsonNode & parentConfig, std::string campaignSet); void show(Canvas & to) override; }; diff --git a/config/campaignSets.json b/config/campaignSets.json index 1a0cd3194..eccafe67d 100644 --- a/config/campaignSets.json +++ b/config/campaignSets.json @@ -5,13 +5,13 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/GOOD1.H3C", "image":"CAMPGD1S", "video":"CGOOD1", "open": true }, - { "x":539, "y":72, "file":"DATA/EVIL1.H3C", "image":"CAMPEV1S", "video":"CEVIL1", "open": true }, - { "x":43, "y":245, "file":"DATA/GOOD2.H3C", "image":"CAMPGD2S", "video":"CGOOD2", "open": true }, - { "x":313, "y":244, "file":"DATA/NEUTRAL1.H3C", "image":"CAMPNEUS", "video":"CNEUTRAL", "open": true }, - { "x":586, "y":246, "file":"DATA/EVIL2.H3C", "image":"CAMPEV2S", "video":"CEVIL2", "open": true }, - { "x":34, "y":417, "file":"DATA/GOOD3.H3C", "image":"CAMPGD3S", "video":"CGOOD3", "open": true }, - { "x":404, "y":414, "file":"DATA/SECRET1.H3C", "image":"CAMPSCTS", "video":"CSECRET", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/GOOD1", "image":"CAMPGD1S", "video":"CGOOD1", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/EVIL1", "image":"CAMPEV1S", "video":"CEVIL1", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/GOOD2", "image":"CAMPGD2S", "video":"CGOOD2", "requires": [1, 2, 4] }, + { "id": 4, "x":313, "y":244, "file":"DATA/NEUTRAL1", "image":"CAMPNEUS", "video":"CNEUTRAL", "requires": [] }, + { "id": 5, "x":586, "y":246, "file":"DATA/EVIL2", "image":"CAMPEV2S", "video":"CEVIL2", "requires": [1, 2, 4] }, + { "id": 6, "x":34, "y":417, "file":"DATA/GOOD3", "image":"CAMPGD3S", "video":"CGOOD3", "requires": [3, 5] }, + { "id": 7, "x":404, "y":414, "file":"DATA/SECRET1", "image":"CAMPSCTS", "video":"CSECRET", "requires": [6] } ] }, "ab" : @@ -25,12 +25,12 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/AB.H3C", "image":"CAMP1AB7", "video":"C1ab7", "open": true }, - { "x":539, "y":72, "file":"DATA/BLOOD.H3C", "image":"CAMP1DB2", "video":"C1db2", "open": true }, - { "x":43, "y":245, "file":"DATA/SLAYER.H3C", "image":"CAMP1DS1", "video":"C1ds1", "open": true }, - { "x":313, "y":244, "file":"DATA/FESTIVAL.H3C", "image":"CAMP1FL3", "video":"C1fl3", "open": true }, - { "x":586, "y":246, "file":"DATA/FIRE.H3C", "image":"CAMP1PF2", "video":"C1pf2", "open": true }, - { "x":34, "y":417, "file":"DATA/FOOL.H3C", "image":"CAMP1FW1", "video":"C1fw1", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/AB", "image":"CAMP1AB7", "video":"C1ab7", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/BLOOD", "image":"CAMP1DB2", "video":"C1db2", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/SLAYER", "image":"CAMP1DS1", "video":"C1ds1", "requires": [] }, + { "id": 4, "x":313, "y":244, "file":"DATA/FESTIVAL", "image":"CAMP1FL3", "video":"C1fl3", "requires": [] }, + { "id": 5, "x":586, "y":246, "file":"DATA/FIRE", "image":"CAMP1PF2", "video":"C1pf2", "requires": [] }, + { "id": 6, "x":34, "y":417, "file":"DATA/FOOL", "image":"CAMP1FW1", "video":"C1fw1", "requires": [1, 2, 3, 4, 5] } ] }, "sod": @@ -39,26 +39,13 @@ "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" }, "items": [ - { "x":90, "y":72, "file":"DATA/GEM.H3C", "image":"CAMPNB1", "video":"NEW", "open": true }, - { "x":539, "y":72, "file":"DATA/GELU.H3C", "image":"CAMPEL1", "video":"ELIXIR", "open": true }, - { "x":43, "y":245, "file":"DATA/CRAG.H3C", "image":"CAMPHS1", "video":"HACK", "open": true }, - { "x":313, "y":244, "file":"DATA/SANDRO.H3C", "image":"CAMPRN1", "video":"RISE", "open": true }, - { "x":586, "y":246, "file":"DATA/YOG.H3C", "image":"CAMPBB1", "video":"BIRTH", "open": true }, - { "x":34, "y":417, "file":"DATA/FINAL.H3C", "image":"CAMPUA1", "video":"UNHOLY", "open": true }, - { "x":404, "y":414, "file":"DATA/SECRET.H3C", "image":"CAMPSP1", "video":"SPECTRE", "open": true } + { "id": 1, "x":90, "y":72, "file":"DATA/GEM", "image":"CAMPNB1", "video":"NEW", "requires": [] }, + { "id": 2, "x":539, "y":72, "file":"DATA/GELU", "image":"CAMPEL1", "video":"ELIXIR", "requires": [] }, + { "id": 3, "x":43, "y":245, "file":"DATA/CRAG", "image":"CAMPHS1", "video":"HACK", "requires": [] }, + { "id": 4, "x":313, "y":244, "file":"DATA/SANDRO", "image":"CAMPRN1", "video":"RISE", "requires": [1, 2, 3, 5] }, + { "id": 5, "x":586, "y":246, "file":"DATA/YOG", "image":"CAMPBB1", "video":"BIRTH", "requires": [] }, + { "id": 6, "x":34, "y":417, "file":"DATA/FINAL", "image":"CAMPUA1", "video":"UNHOLY", "requires": [4] }, + { "id": 7, "x":404, "y":414, "file":"DATA/SECRET", "image":"CAMPSP1", "video":"SPECTRE", "requires": [6] } ] } -// "wog" : -// { -// /// wog campaigns, currently has no assigned button in campaign screen and thus unused -// "images" : [ {"x": 0, "y": 0, "name":"CAMPZALL"} ], -// "exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN", "hotkey" : 27}, -// "items": -// [ -// { "x":90, "y":72, "file":"DATA/ZC1.H3C", "image":"CAMPZ01", "open": true}, -// { "x":539, "y":72, "file":"DATA/ZC2.H3C", "image":"CAMPZ02", "open": true}, -// { "x":43, "y":245, "file":"DATA/ZC3.H3C", "image":"CAMPZ03", "open": true}, -// { "x":311, "y":242, "file":"DATA/ZC4.H3C", "image":"CAMPZ04", "open": true} -// ] -// } } From a05f8339aef8ba4ccb9a0a8b258e68b5b11f2642 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 21 Sep 2023 04:31:08 +0200 Subject: [PATCH 0507/1248] Proper mod compatibility check system --- client/CServerHandler.cpp | 3 + lib/StartInfo.cpp | 2 +- lib/mapping/CMapHeader.h | 7 +-- lib/mapping/CMapService.cpp | 5 +- lib/mapping/CMapService.h | 5 +- lib/mapping/MapFormatJson.cpp | 21 +++++-- lib/modding/CModHandler.cpp | 82 ++++++++++++++++----------- lib/modding/CModHandler.h | 23 ++++---- lib/modding/CModInfo.cpp | 28 +++++---- lib/modding/CModInfo.h | 37 +++++++++--- lib/modding/ContentTypeHandler.cpp | 9 +-- mapeditor/mainwindow.cpp | 2 +- mapeditor/mapcontroller.cpp | 4 +- mapeditor/mapcontroller.h | 4 +- mapeditor/mapsettings/modsettings.cpp | 4 +- mapeditor/validator.cpp | 2 +- 16 files changed, 149 insertions(+), 89 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index af1238c8c..06f5025b4 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -704,6 +704,9 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) void CServerHandler::showServerError(const std::string & txt) const { + if(auto w = GH.windows().topWindow()) + GH.windows().popWindow(w); + CInfoWindow::showInfoDialog(txt, {}); } diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 7e3a5b91c..c8100987a 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -76,7 +76,7 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); ModIncompatibility::ModList modList; for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); + modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) throw ModIncompatibility(std::move(modList)); diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8b04c1f2e..64ea09b7b 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,7 +10,7 @@ #pragma once -#include "../modding/CModVersion.h" +#include "../modding/CModInfo.h" #include "../LogicalExpression.h" #include "../int3.h" #include "../MetaString.h" @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; enum class EMapFormat : uint8_t; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /// The hero name struct consists of the hero id and the hero name. struct DLL_LINKAGE SHeroName @@ -249,8 +249,7 @@ public: void serialize(Handler & h, const int Version) { h & version; - if(Version >= 821) - h & mods; + h & mods; h & name; h & description; h & width; diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 8b52b0a72..5f5e9aff0 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -92,14 +92,15 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) { - ModCompatibilityInfo modCompatibilityInfo; const auto & activeMods = VLC->modh->getActiveMods(); + + ModCompatibilityInfo modCompatibilityInfo; for(const auto & mapMod : map.mods) { if(vstd::contains(activeMods, mapMod.first)) { const auto & modInfo = VLC->modh->getModInfo(mapMod.first); - if(modInfo.version.compatible(mapMod.second)) + if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) continue; } diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index 9a8a33c7f..ed791197b 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -10,6 +10,8 @@ #pragma once +#include "../modding/CModInfo.h" + VCMI_LIB_NAMESPACE_BEGIN class ResourcePath; @@ -17,12 +19,11 @@ class ResourcePath; class CMap; class CMapHeader; class CInputStream; -struct CModVersion; class IMapLoader; class IMapPatcher; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /** * The map service provides loading of VCMI/H3 map files. It can diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c8aed2682..3e4c88c12 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -342,7 +342,7 @@ namespace TerrainDetail ///CMapFormatJson const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 2; +const int CMapFormatJson::VERSION_MINOR = 3; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; @@ -953,7 +953,18 @@ void CMapLoaderJson::readHeader(const bool complete) if(!header["mods"].isNull()) { for(auto & mod : header["mods"].Vector()) - mapHeader->mods[mod["name"].String()] = CModVersion::fromString(mod["version"].String()); + { + CModInfo::VerificationInfo info; + info.version = CModVersion::fromString(mod["version"].String()); + info.checksum = mod["checksum"].Integer(); + info.name = mod["name"].String(); + info.impactsGameplay = true; + + if(!mod["modId"].isNull()) + mapHeader->mods[mod["modId"].String()] = info; + else + mapHeader->mods[mod["name"].String()] = info; + } } //todo: multilevel map load support @@ -1294,8 +1305,10 @@ void CMapSaverJson::writeHeader() for(const auto & mod : mapHeader->mods) { JsonNode modWriter; - modWriter["name"].String() = mod.first; - modWriter["version"].String() = mod.second.toString(); + modWriter["modId"].String() = mod.first; + modWriter["name"].String() = mod.second.name; + modWriter["version"].String() = mod.second.version.toString(); + modWriter["checksum"].Integer() = mod.second.checksum; mods.Vector().push_back(modWriter); } diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 19049e1ee..ea8c7e681 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -56,7 +56,7 @@ bool CModHandler::hasCircularDependency(const TModID & modID, std::set c if (vstd::contains(currentList, modID)) { logMod->error("Error: Circular dependency detected! Printing dependency list:"); - logMod->error("\t%s -> ", mod.name); + logMod->error("\t%s -> ", mod.getVerificationInfo().name); return true; } @@ -67,7 +67,7 @@ bool CModHandler::hasCircularDependency(const TModID & modID, std::set c { if (hasCircularDependency(dependency, currentList)) { - logMod->error("\t%s ->\n", mod.name); // conflict detected, print dependency list + logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list return true; } } @@ -129,7 +129,7 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.name, dependency); + logMod->error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.getVerificationInfo().name, dependency); } } return sortedValidMods; @@ -212,7 +212,6 @@ void CModHandler::loadMods(bool onlyEssential) } coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); - coreMod->name = "Original game files"; } std::vector CModHandler::getAllMods() @@ -352,7 +351,7 @@ void CModHandler::initializeConfig() CModVersion CModHandler::getModVersion(TModID modName) const { if (allMods.count(modName)) - return allMods.at(modName).version; + return allMods.at(modName).getVerificationInfo().version; return {}; } @@ -462,6 +461,7 @@ void CModHandler::afterLoad(bool onlyEssential) modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); } modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); + modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; if(!onlyEssential) { @@ -471,47 +471,63 @@ void CModHandler::afterLoad(bool onlyEssential) } -void CModHandler::trySetActiveMods(std::vector saveActiveMods, const std::map & modList) +void CModHandler::trySetActiveMods(const std::vector> & modList) { - std::vector newActiveMods; - - ModIncompatibility::ModList missingMods; - + auto hasMod = [&modList](const TModID & m) -> bool + { + for(auto & i : modList) + if(i.first == m) + return true; + return false; + }; + for(const auto & m : activeMods) { - if (vstd::contains(saveActiveMods, m)) + if(hasMod(m)) continue; - auto & modInfo = allMods.at(m); - if(modInfo.checkModGameplayAffecting()) - missingMods.emplace_back(m, modInfo.version.toString()); + if(getModInfo(m).checkModGameplayAffecting()) + allMods[m].setEnabled(false); } - for(const auto & m : saveActiveMods) + std::vector newActiveMods; + ModIncompatibility::ModList missingMods; + + for(const auto & infoPair : modList) { - const CModVersion & mver = modList.at(m); - - if (allMods.count(m) == 0) + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + if(!allMods.count(remoteModId)) { - missingMods.emplace_back(m, mver.toString()); + if(remoteModInfo.impactsGameplay) + missingMods.push_back({remoteModInfo.name, remoteModInfo.version.toString()}); //mod is not installed continue; } + + auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - auto & modInfo = allMods.at(m); - - bool modAffectsGameplay = modInfo.checkModGameplayAffecting(); - bool modVersionCompatible = modInfo.version.isNull() || mver.isNull() || modInfo.version.compatible(mver); - bool modEnabledLocally = vstd::contains(activeMods, m); - bool modCanBeEnabled = modEnabledLocally && modVersionCompatible; - - allMods[m].setEnabled(modCanBeEnabled); - - if (modCanBeEnabled) - newActiveMods.push_back(m); - - if (!modCanBeEnabled && modAffectsGameplay) - missingMods.emplace_back(m, mver.toString()); + bool modAffectsGameplay = getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + { + newActiveMods.push_back(remoteModId); + continue; + } + + if(modAffectsGameplay || remoteModInfo.impactsGameplay) + missingMods.push_back({remoteModInfo.name, remoteModInfo.version.toString()}); //incompatible mod impacts gameplay } + + if(!missingMods.empty()) + throw ModIncompatibility(std::move(missingMods)); + + for(auto & m : newActiveMods) + allMods[m].setEnabled(true); std::swap(activeMods, newActiveMods); } diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 93ec72775..2062909bf 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -9,13 +9,12 @@ */ #pragma once -#include "CModVersion.h" +#include "CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN class CModHandler; class CModIndentifier; -class CModInfo; class JsonNode; class IHandlerBase; class CIdentifierStorage; @@ -52,7 +51,7 @@ class DLL_LINKAGE CModHandler : boost::noncopyable CModVersion getModVersion(TModID modName) const; /// Attempt to set active mods according to provided list of mods from save, throws on failure - void trySetActiveMods(std::vector saveActiveMods, const std::map & modList); + void trySetActiveMods(const std::vector> & modList); public: std::shared_ptr content; //(!)Do not serialize FIXME: make private @@ -88,22 +87,22 @@ public: { h & activeMods; for(const auto & m : activeMods) - { - CModVersion version = getModVersion(m); - h & version; - } + h & getModInfo(m).getVerificationInfo(); } else { loadMods(); std::vector saveActiveMods; - std::map modVersions; h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } - for(const auto & m : saveActiveMods) - h & modVersions[m]; - - trySetActiveMods(saveActiveMods, modVersions); + trySetActiveMods(saveModInfos); } } }; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 1901e8923..d0ea96546 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -23,7 +23,6 @@ static JsonNode addMeta(JsonNode config, const std::string & meta) } CModInfo::CModInfo(): - checksum(0), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING) @@ -33,17 +32,17 @@ CModInfo::CModInfo(): CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): identifier(identifier), - name(config["name"].String()), description(config["description"].String()), dependencies(config["depends"].convertTo>()), conflicts(config["conflicts"].convertTo>()), - checksum(0), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING), config(addMeta(config, identifier)) { - version = CModVersion::fromString(config["version"].String()); + verificationInfo.name = config["name"].String(); + verificationInfo.version = CModVersion::fromString(config["version"].String()); + if(!config["compatibility"].isNull()) { vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); @@ -61,7 +60,7 @@ CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode CModInfo::saveLocalData() const { std::ostringstream stream; - stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << checksum; + stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << verificationInfo.checksum; JsonNode conf; conf["active"].Bool() = explicitlyEnabled; @@ -83,9 +82,9 @@ JsonPath CModInfo::getModFile(const std::string & name) void CModInfo::updateChecksum(ui32 newChecksum) { // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != checksum) + if (newChecksum != verificationInfo.checksum) { - checksum = newChecksum; + verificationInfo.checksum = newChecksum; validation = PENDING; } } @@ -95,7 +94,7 @@ void CModInfo::loadLocalData(const JsonNode & data) bool validated = false; implicitlyEnabled = true; explicitlyEnabled = !config["keepDisabled"].Bool(); - checksum = 0; + verificationInfo.checksum = 0; if (data.getType() == JsonNode::JsonType::DATA_BOOL) { explicitlyEnabled = data.Bool(); @@ -104,7 +103,7 @@ void CModInfo::loadLocalData(const JsonNode & data) { explicitlyEnabled = data["active"].Bool(); validated = data["validated"].Bool(); - checksum = strtol(data["checksum"].String().c_str(), nullptr, 16); + updateChecksum(strtol(data["checksum"].String().c_str(), nullptr, 16)); } //check compatibility @@ -112,13 +111,13 @@ void CModInfo::loadLocalData(const JsonNode & data) implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); + logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name); if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment { if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) { - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", name); + logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); implicitlyEnabled = false; } } @@ -127,6 +126,8 @@ void CModInfo::loadLocalData(const JsonNode & data) validation = validated ? PASSED : PENDING; else validation = validated ? PASSED : FAILED; + + verificationInfo.impactsGameplay = checkModGameplayAffecting(); } bool CModInfo::checkModGameplayAffecting() const @@ -171,6 +172,11 @@ bool CModInfo::checkModGameplayAffecting() const return *modGameplayAffecting; } +const CModInfo::VerificationInfo & CModInfo::getVerificationInfo() const +{ + return verificationInfo; +} + bool CModInfo::isEnabled() const { return implicitlyEnabled && explicitlyEnabled; diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 6e8a5012d..5fd687b75 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -29,17 +29,37 @@ public: FAILED, PASSED }; + + struct VerificationInfo + { + /// human-readable mod name + std::string name; + + /// version of the mod + CModVersion version; + + /// CRC-32 checksum of the mod + ui32 checksum = 0; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h, const int v) + { + h & name; + h & version; + h & checksum; + h & impactsGameplay; + } + }; /// identifier, identical to name of folder with mod std::string identifier; - /// human-readable strings - std::string name; + /// detailed mod description std::string description; - /// version of the mod - CModVersion version; - /// Base language of mod, all mod strings are assumed to be in this language std::string baseLanguage; @@ -52,9 +72,6 @@ public: /// list of mods that can't be used in the same time as this one std::set conflicts; - /// CRC-32 checksum of the mod - ui32 checksum; - EValidationStatus validation; JsonNode config; @@ -73,6 +90,8 @@ public: /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects bool checkModGameplayAffecting() const; + + const VerificationInfo & getVerificationInfo() const; private: /// true if mod is enabled by user, e.g. in Launcher UI @@ -80,6 +99,8 @@ private: /// true if mod can be loaded - compatible and has no missing deps bool implicitlyEnabled; + + VerificationInfo verificationInfo; void loadLocalData(const JsonNode & data); }; diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index afcfd0ff9..9d018f3e9 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -212,7 +212,8 @@ void CContentHandler::preloadData(CModInfo & mod) bool validate = (mod.validation != CModInfo::PASSED); // print message in format [<8-symbols checksum>] - logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); + auto & info = mod.getVerificationInfo(); + logMod->info("\t\t[%08x]%s", info.checksum, info.name); if (validate && mod.identifier != ModScope::scopeBuiltin()) { @@ -233,12 +234,12 @@ void CContentHandler::load(CModInfo & mod) if (validate) { if (mod.validation != CModInfo::FAILED) - logMod->info("\t\t[DONE] %s", mod.name); + logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name); else - logMod->error("\t\t[FAIL] %s", mod.name); + logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name); } else - logMod->info("\t\t[SKIP] %s", mod.name); + logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name); } const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 14b6dfcba..3d23b923f 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -338,7 +338,7 @@ bool MainWindow::openMap(const QString & filenameSelect) auto missingMods = CMapService::verifyMapHeaderMods(*header); ModIncompatibility::ModList modList; for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); + modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) throw ModIncompatibility(std::move(modList)); diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index f0eea1725..b1b86212a 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -588,7 +588,7 @@ ModCompatibilityInfo MapController::modAssessmentAll() auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } } return result; @@ -605,7 +605,7 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } //TODO: terrains? return result; diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index ad2cecad0..979c2c6d2 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,10 +13,10 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/modding/CModVersion.h" +#include "../lib/modding/CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index f8121d918..5926542e5 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -47,7 +47,7 @@ void ModSettings::initialize(MapController & c) auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getVerificationInfo().name), QString::fromStdString(modInfo.getVerificationInfo().version.toString())}); item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); @@ -104,7 +104,7 @@ void ModSettings::update() if(item->checkState(0) == Qt::Checked) { auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - controller->map()->mods[modName] = VLC->modh->getModInfo(modName).version; + controller->map()->mods[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } }; diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index e5cabf608..25783058d 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -174,7 +174,7 @@ std::list Validator::validate(const CMap * map) { if(!map->mods.count(mod.first)) { - issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true); + issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true); } } } From 5fd2eee3e84d247d2128c6755c61ed9b630c23a8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:27:06 +0200 Subject: [PATCH 0508/1248] code review --- client/CServerHandler.cpp | 2 +- client/mainmenu/CCampaignScreen.cpp | 7 +++++-- lib/CConfigHandler.cpp | 26 ++++++++++++-------------- lib/CConfigHandler.h | 8 ++++---- lib/VCMI_Lib.cpp | 4 ++-- lib/campaign/CampaignState.h | 2 +- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 3a55d2b0d..ead195106 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -685,7 +685,7 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) { if(ourCampaign->campaignSet != "") { - Settings entry = persistent.write["campaign"][ourCampaign->campaignSet][ourCampaign->getFilename()]["completed"]; + Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()]; entry->Bool() = true; } diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index bae48a891..004539454 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -107,7 +107,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const auto header = CampaignHandler::getHeader(campFile); hoverText = header->getName(); - if(persistent["campaign"][campaignSet][header->getFilename()]["completed"].Bool()) + if(persistentStorage["completedCampaigns"][header->getFilename()].Bool()) status = CCampaignScreen::COMPLETED; for(const JsonNode & node : parentConfig[campaignSet]["items"].Vector()) @@ -115,11 +115,14 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const for(const JsonNode & requirement : config["requires"].Vector()) { if(node["id"].Integer() == requirement.Integer()) - if(!persistent["campaign"][campaignSet][node["file"].String()]["completed"].Bool()) + if(!persistentStorage["completedCampaigns"][node["file"].String()].Bool()) status = CCampaignScreen::DISABLED; } } + if(persistentStorage["unlockAllCampaigns"].Bool()) + status = CCampaignScreen::ENABLED; + if(status != CCampaignScreen::DISABLED) { addUsedEvents(LCLICK | HOVER); diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index f82807876..48a761c83 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN SettingsStorage settings; -SettingsStorage persistent; +SettingsStorage persistentStorage; template SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): @@ -54,25 +54,23 @@ SettingsStorage::SettingsStorage(): { } -void SettingsStorage::init(bool p) +void SettingsStorage::init(const std::string & dataFilename, const std::string & schema) { - persistentStorage = p; - cfgName = "config/settings.json"; - if(persistentStorage) - cfgName = "config/persistent.json"; + this->dataFilename = dataFilename; + this->schema = schema; - JsonPath confName = JsonPath::builtin(cfgName); + JsonPath confName = JsonPath::builtin(dataFilename); JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); // Probably new install. Create config file to save settings to if (!CResourceHandler::get("local")->existsResource(confName)) - CResourceHandler::get("local")->createResource(cfgName); + CResourceHandler::get("local")->createResource(dataFilename); - if(!persistentStorage) + if(schema != "") { - JsonUtils::maximize(config, "vcmi:settings"); - JsonUtils::validate(config, "vcmi:settings", "settings"); + JsonUtils::maximize(config, schema); + JsonUtils::validate(config, schema, "settings"); } } @@ -83,10 +81,10 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath JsonNode savedConf = config; savedConf.Struct().erase("session"); - if(!persistentStorage) - JsonUtils::minimize(savedConf, "vcmi:settings"); + if(schema != "") + JsonUtils::minimize(savedConf, schema); - std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(cfgName))->c_str(), std::ofstream::out | std::ofstream::trunc); + std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); file << savedConf.toJson(); } diff --git a/lib/CConfigHandler.h b/lib/CConfigHandler.h index 187c7c1fc..6eeae392b 100644 --- a/lib/CConfigHandler.h +++ b/lib/CConfigHandler.h @@ -35,8 +35,8 @@ class DLL_LINKAGE SettingsStorage std::set listeners; JsonNode config; - bool persistentStorage; - std::string cfgName; + std::string dataFilename; + std::string schema; JsonNode & getNode(const std::vector & path); @@ -48,7 +48,7 @@ class DLL_LINKAGE SettingsStorage public: // Initialize config structure SettingsStorage(); - void init(bool persistent = false); + void init(const std::string & dataFilename, const std::string & schema); // Get write access to config node at path const NodeAccessor write; @@ -116,6 +116,6 @@ public: }; extern DLL_LINKAGE SettingsStorage settings; -extern DLL_LINKAGE SettingsStorage persistent; +extern DLL_LINKAGE SettingsStorage persistentStorage; VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 0eef646c6..8c243c91c 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -52,8 +52,8 @@ DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool console = Console; VLC = new LibClasses(); VLC->loadFilesystem(extractArchives); - settings.init(); - persistent.init(true); + settings.init("config/settings.json", "vcmi:settings"); + persistentStorage.init("config/persistentStorage.json", ""); VLC->loadModFilesystem(onlyEssential); } diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 4c091fa16..53b6d8e4c 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -293,7 +293,7 @@ public: static JsonNode crossoverSerialize(CGHeroInstance * hero); static CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map); - std::string campaignSet = ""; + std::string campaignSet; template void serialize(Handler &h, const int version) { From 530e4533ce69b9f30d335f5a5b4c2a5226d5c19e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 21 Sep 2023 22:05:15 +0200 Subject: [PATCH 0509/1248] Update mainwindow_moc.cpp --- launcher/mainwindow_moc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/mainwindow_moc.cpp b/launcher/mainwindow_moc.cpp index 6bb00171a..55389163d 100644 --- a/launcher/mainwindow_moc.cpp +++ b/launcher/mainwindow_moc.cpp @@ -45,7 +45,7 @@ void MainWindow::load() QDir::addSearchPath("icons", pathToQString(VCMIDirs::get().userDataPath() / "launcher" / "icons")); #endif - settings.init(); + settings.init("config/settings.json", "vcmi:settings"); } void MainWindow::computeSidePanelSizes() From 4691907f9c5d670f8888e345be80017e267dab81 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 21 Sep 2023 22:28:29 +0200 Subject: [PATCH 0510/1248] Show root mods only --- client/CServerHandler.cpp | 3 ++- client/NetPacksLobbyClient.cpp | 2 -- lib/mapping/MapFormatJson.cpp | 2 ++ lib/modding/CModHandler.cpp | 46 +++++++++++++++++++++++----------- lib/modding/CModInfo.cpp | 3 +++ lib/modding/CModInfo.h | 4 +++ 6 files changed, 43 insertions(+), 17 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 06f5025b4..5f7097f81 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -572,7 +572,8 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const void CServerHandler::sendStartGame(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); - + GH.windows().createAndPushWindow(); + LobbyStartGame lsg; if(client) { diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index b5356ccf5..7dd44cc88 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -149,8 +149,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress w->tick(0); w->redraw(); } - else - GH.windows().createAndPushWindow(); } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 3e4c88c12..d559c5c94 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -958,6 +958,7 @@ void CMapLoaderJson::readHeader(const bool complete) info.version = CModVersion::fromString(mod["version"].String()); info.checksum = mod["checksum"].Integer(); info.name = mod["name"].String(); + info.parent = mod["parent"].String(); info.impactsGameplay = true; if(!mod["modId"].isNull()) @@ -1307,6 +1308,7 @@ void CMapSaverJson::writeHeader() JsonNode modWriter; modWriter["modId"].String() = mod.first; modWriter["name"].String() = mod.second.name; + modWriter["parent"].String() = mod.second.parent; modWriter["version"].String() = mod.second.version.toString(); modWriter["checksum"].Integer() = mod.second.checksum; mods.Vector().push_back(modWriter); diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index ea8c7e681..553c41d98 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -473,41 +473,46 @@ void CModHandler::afterLoad(bool onlyEssential) void CModHandler::trySetActiveMods(const std::vector> & modList) { - auto hasMod = [&modList](const TModID & m) -> bool + auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* { for(auto & i : modList) if(i.first == m) - return true; - return false; + return &i.second; + return nullptr; }; for(const auto & m : activeMods) { - if(hasMod(m)) + if(searchVerificationInfo(m)) continue; + //TODO: support actual disabling of these mods if(getModInfo(m).checkModGameplayAffecting()) allMods[m].setEnabled(false); } - std::vector newActiveMods; - ModIncompatibility::ModList missingMods; + std::vector newActiveMods, missingMods; + ModIncompatibility::ModList missingModsResult; for(const auto & infoPair : modList) { auto & remoteModId = infoPair.first; auto & remoteModInfo = infoPair.second; + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + if(!allMods.count(remoteModId)) { - if(remoteModInfo.impactsGameplay) - missingMods.push_back({remoteModInfo.name, remoteModInfo.version.toString()}); //mod is not installed + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed continue; } auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - - bool modAffectsGameplay = getModInfo(remoteModId).checkModGameplayAffecting(); + modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); bool modVersionCompatible = localModInfo.version.isNull() || remoteModInfo.version.isNull() || localModInfo.version.compatible(remoteModInfo.version); @@ -519,13 +524,26 @@ void CModHandler::trySetActiveMods(const std::vectorparent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + if(!missingModsResult.empty()) + throw ModIncompatibility(std::move(missingModsResult)); + + //TODO: support actual enabling of these mods for(auto & m : newActiveMods) allMods[m].setEnabled(true); diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index d0ea96546..d90ce08ae 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -42,6 +42,9 @@ CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const { verificationInfo.name = config["name"].String(); verificationInfo.version = CModVersion::fromString(config["version"].String()); + verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); + if(verificationInfo.parent == identifier) + verificationInfo.parent.clear(); if(!config["compatibility"].isNull()) { diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 5fd687b75..7469e1f19 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -41,6 +41,9 @@ public: /// CRC-32 checksum of the mod ui32 checksum = 0; + /// parent mod ID, empty if root-level mod + TModID parent; + /// for serialization purposes bool impactsGameplay = true; @@ -50,6 +53,7 @@ public: h & name; h & version; h & checksum; + h & parent; h & impactsGameplay; } }; From 60eef59bc959c11b066e67aef3fcfb956b0ec6bc Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 21 Sep 2023 22:38:01 +0200 Subject: [PATCH 0511/1248] Show root mods for maps --- lib/mapping/CMapService.cpp | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 5f5e9aff0..0d119bd5a 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -94,7 +94,7 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) { const auto & activeMods = VLC->modh->getActiveMods(); - ModCompatibilityInfo modCompatibilityInfo; + ModCompatibilityInfo missingMods, missingModsFiltered; for(const auto & mapMod : map.mods) { if(vstd::contains(activeMods, mapMod.first)) @@ -103,10 +103,18 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) continue; } - - modCompatibilityInfo[mapMod.first] = mapMod.second; - } - return modCompatibilityInfo; + missingMods[mapMod.first] = mapMod.second; + } + + //filter child mods + for(const auto & mapMod : missingMods) + { + if(!mapMod.second.parent.empty() && missingMods.count(mapMod.second.parent)) + continue; + missingModsFiltered.insert(mapMod); + } + + return missingModsFiltered; } std::unique_ptr CMapService::getStreamFromFS(const ResourcePath & name) From 9e78f9c69c25e1d3e9d402a7ce3db5fe89278c6d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 21 Sep 2023 23:41:00 +0200 Subject: [PATCH 0512/1248] CodeReview --- client/CServerHandler.cpp | 11 +++-------- lib/CConfigHandler.cpp | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ead195106..291fe5b70 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -689,18 +689,13 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) entry->Bool() = true; } + GH.windows().pushWindow(CMM); + GH.windows().pushWindow(CMM->menu); + if(!ourCampaign->isCampaignFinished()) - { - GH.windows().pushWindow(CMM); - GH.windows().pushWindow(CMM->menu); CMM->openCampaignLobby(ourCampaign); - } else - { - GH.windows().pushWindow(CMM); - GH.windows().pushWindow(CMM->menu); CMM->openCampaignScreen(ourCampaign->campaignSet); - } }; if(epilogue.hasPrologEpilog) { diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index 48a761c83..af4a99eca 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -67,7 +67,7 @@ void SettingsStorage::init(const std::string & dataFilename, const std::string & if (!CResourceHandler::get("local")->existsResource(confName)) CResourceHandler::get("local")->createResource(dataFilename); - if(schema != "") + if(!schema.empty()) { JsonUtils::maximize(config, schema); JsonUtils::validate(config, schema, "settings"); @@ -81,7 +81,7 @@ void SettingsStorage::invalidateNode(const std::vector &changedPath JsonNode savedConf = config; savedConf.Struct().erase("session"); - if(schema != "") + if(!schema.empty()) JsonUtils::minimize(savedConf, schema); std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); From 099109b7d22f7a0a16877ef590045009b93f1a86 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 22 Sep 2023 00:52:19 +0200 Subject: [PATCH 0513/1248] Minor fixes --- lib/gameState/CGameState.cpp | 6 ++---- server/battles/BattleProcessor.cpp | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index a3e688675..45551e296 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -663,10 +663,8 @@ void CGameState::initDifficulty() logGlobal->debug("\tLoading difficulty settings"); const JsonNode config(JsonPath::builtin("config/difficulty.json")); - const JsonVector &vector = config["battleBonus"].Vector(); - const JsonNode &level = vector[scenarioOps->difficulty]; - const JsonNode & difficultyAI(level["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); - const JsonNode & difficultyHuman(level["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); + const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); auto setDifficulty = [](PlayerState & state, const JsonNode & json) { diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 19fd0426b..9a443b0c9 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -117,7 +117,7 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm //add battle bonuses based from player state only when attacks neutral creatures const auto * attackerInfo = gameHandler->getPlayerState(army1->getOwner(), false); - if(attackerInfo && army2->getOwner() == PlayerColor::NEUTRAL) + if(attackerInfo && !army2->getOwner().isValidPlayer()) { for(auto bonus : attackerInfo->battleBonuses) { From 16b147d588134efac346fe63ec0efbb8040c34d4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 22 Sep 2023 01:12:10 +0200 Subject: [PATCH 0514/1248] Fix negative spell cost --- lib/battle/CBattleInfoCallback.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index c8714652d..b8876b946 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1573,7 +1573,7 @@ int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const } } - return ret - manaReduction + manaIncrease; + return std::max(0, ret - manaReduction + manaIncrease); } bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const From 92b0e2b400cb41b1268318b51608e3cae9f1fdb8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 01:39:35 +0200 Subject: [PATCH 0515/1248] initial highscore support --- Mods/vcmi/config/vcmi/czech.json | 1 - Mods/vcmi/config/vcmi/english.json | 634 +++++++++++++-------------- Mods/vcmi/config/vcmi/french.json | 1 - Mods/vcmi/config/vcmi/german.json | 1 - Mods/vcmi/config/vcmi/polish.json | 1 - Mods/vcmi/config/vcmi/ukrainian.json | 1 - client/CMakeLists.txt | 2 + client/VCMI_client.cbp | 2 + client/VCMI_client.vcxproj | 2 + client/VCMI_client.vcxproj.filters | 2 + client/mainmenu/CHighScoreScreen.cpp | 161 +++++++ client/mainmenu/CHighScoreScreen.h | 37 ++ client/mainmenu/CMainMenu.cpp | 9 +- client/mainmenu/CMainMenu.h | 1 + config/highscoreCreatures.json | 122 ++++++ lib/constants/EntityIdentifiers.h | 32 +- 16 files changed, 652 insertions(+), 357 deletions(-) create mode 100644 client/mainmenu/CHighScoreScreen.cpp create mode 100644 client/mainmenu/CHighScoreScreen.h create mode 100644 config/highscoreCreatures.json diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index dcbb8ccaf..7784defac 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu", "vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice", - "vcmi.mainMenu.highscoresNotImplemented" : "Omlouvám se, menu nejvyšší skóre ještě není implementováno\n", "vcmi.mainMenu.serverConnecting" : "Připojování...", "vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:", "vcmi.mainMenu.serverClosing" : "Zavírání...", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index cc95f3e30..83e81dee6 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -1,375 +1,339 @@ { - "vcmi.adventureMap.monsterThreat.title" : "\n\nThreat: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "Effortless", - "vcmi.adventureMap.monsterThreat.levels.1" : "Very Weak", - "vcmi.adventureMap.monsterThreat.levels.2" : "Weak", - "vcmi.adventureMap.monsterThreat.levels.3" : "A bit weaker", - "vcmi.adventureMap.monsterThreat.levels.4" : "Equal", - "vcmi.adventureMap.monsterThreat.levels.5" : "A bit stronger", - "vcmi.adventureMap.monsterThreat.levels.6" : "Strong", - "vcmi.adventureMap.monsterThreat.levels.7" : "Very Strong", - "vcmi.adventureMap.monsterThreat.levels.8" : "Challenging", - "vcmi.adventureMap.monsterThreat.levels.9" : "Overpowering", - "vcmi.adventureMap.monsterThreat.levels.10" : "Deadly", + "vcmi.adventureMap.monsterThreat.title" : "\n\nDiscussion : ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Sans effort", + "vcmi.adventureMap.monsterThreat.levels.1" : "Très faible", + "vcmi.adventureMap.monsterThreat.levels.2" : "Faible", + "vcmi.adventureMap.monsterThreat.levels.3" : "Un peu plus faible", + "vcmi.adventureMap.monsterThreat.levels.4" : "Égal", + "vcmi.adventureMap.monsterThreat.levels.5" : "Un peu plus fort", + "vcmi.adventureMap.monsterThreat.levels.6" : "Fort", + "vcmi.adventureMap.monsterThreat.levels.7" : "Très fort", + "vcmi.adventureMap.monsterThreat.levels.8" : "Difficile", + "vcmi.adventureMap.monsterThreat.levels.9" : "Surpuissant", + "vcmi.adventureMap.monsterThreat.levels.10" : "Mortel", "vcmi.adventureMap.monsterThreat.levels.11" : "Impossible", - "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart the game?", - "vcmi.adventureMap.noTownWithMarket" : "There are no available marketplaces!", - "vcmi.adventureMap.noTownWithTavern" : "There are no available towns with taverns!", - "vcmi.adventureMap.spellUnknownProblem" : "There is an unknown problem with this spell! No more information is available.", - "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", - "vcmi.adventureMap.moveCostDetails" : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING", + "vcmi.adventureMap.confirmRestartGame" : "Êtes-vous sûr de vouloir redémarrer le jeu ?", + "vcmi.adventureMap.noTownWithMarket" : "Il n'y a pas de marchés disponibles !", + "vcmi.adventureMap.noTownWithTavern" : "Il n'y a pas de villes disponibles avec des tavernes !", + "vcmi.adventureMap.spellUnknownProblem" : "Il y a un problème inconnu avec ce sort ! Pas plus d'informations sont disponibles.", + "vcmi.adventureMap.playerAttacked" : "Le joueur a été attaqué : %s", + "vcmi.adventureMap.moveCostDetails" : "Points de mouvement - Coût : %TURNS tours + %POINTS points, Points restants : %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Points de mouvement - Coût : %POINTS points, Points restants : %REMAINING", - "vcmi.capitalColors.0" : "Red", - "vcmi.capitalColors.1" : "Blue", - "vcmi.capitalColors.2" : "Tan", - "vcmi.capitalColors.3" : "Green", + "vcmi.capitalColors.0" : "Rouge", + "vcmi.capitalColors.1" : "Bleu", + "vcmi.capitalColors.2" : "Ocre", + "vcmi.capitalColors.3" : "Vert", "vcmi.capitalColors.4" : "Orange", - "vcmi.capitalColors.5" : "Purple", - "vcmi.capitalColors.6" : "Teal", - "vcmi.capitalColors.7" : "Pink", - - "vcmi.heroOverview.startingArmy" : "Starting Units", - "vcmi.heroOverview.warMachine" : "War Machines", - "vcmi.heroOverview.secondarySkills" : "Secondary Skills", - "vcmi.heroOverview.spells" : "Spells", - - "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", - "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", - "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", - "vcmi.radialWheel.splitUnitEqually" : "Split creatures equally", - "vcmi.radialWheel.moveUnit" : "Move creatures to another army", - "vcmi.radialWheel.splitUnit" : "Split creature to another slot", + "vcmi.capitalColors.5" : "Violet", + "vcmi.capitalColors.6" : "Turquoise", + "vcmi.capitalColors.7" : "Rose", - "vcmi.mainMenu.highscoresNotImplemented" : "Sorry, high scores menu is not implemented yet\n", - "vcmi.mainMenu.serverConnecting" : "Connecting...", - "vcmi.mainMenu.serverAddressEnter" : "Enter address:", - "vcmi.mainMenu.serverClosing" : "Closing...", - "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", - "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", - "vcmi.mainMenu.playerName" : "Player", - - "vcmi.lobby.filename" : "Filename", - "vcmi.lobby.creationDate" : "Creation date", + "vcmi.mainMenu.serverConnecting" : "Connexion...", + "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", + "vcmi.mainMenu.serverClosing" : "Fermeture...", + "vcmi.mainMenu.hostTCP" : "Hôte TCP/IP jeu", + "vcmi.mainMenu.joinTCP" : "Rejoindre TCP/IP jeu", + "vcmi.mainMenu.playerName" : "Joueur", - "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", - "vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:", - "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", + "vcmi.server.errors.modsIncompatibility" : "Les mods suivants sont nécessaires pour charger le jeu :", + "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", - "vcmi.settingsMainWindow.generalTab.hover" : "General", - "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", - "vcmi.settingsMainWindow.battleTab.hover" : "Battle", - "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles.", - "vcmi.settingsMainWindow.adventureTab.hover" : "Adventure Map", - "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes).", + "vcmi.settingsMainWindow.generalTab.hover" : "Général", + "vcmi.settingsMainWindow.generalTab.help" : "Passe à l'onglet Options générales, qui contient des paramètres liés au comportement général du client de jeu", + "vcmi.settingsMainWindow.battleTab.hover" : "Bataille", + "vcmi.settingsMainWindow.battleTab.help" : "Passe à l'onglet Options de combat, ce qui permet de configurer le comportement du jeu pendant les batailles", + "vcmi.settingsMainWindow.adventureTab.hover" : "Carte d'aventure", + "vcmi.settingsMainWindow.adventureTab.help" : "Passe à l'onglet Options de carte d'aventure (la carte d'aventure est la section du jeu où les joueurs peuvent contrôler les mouvements de leurs héros)", - "vcmi.systemOptions.videoGroup" : "Video Settings", - "vcmi.systemOptions.audioGroup" : "Audio Settings", - "vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now - "vcmi.systemOptions.townsGroup" : "Town Screen", + "vcmi.systemOptions.videoGroup" : "Paramètres Vidéo", + "vcmi.systemOptions.audioGroup" : "Paramètres Audio", + "vcmi.systemOptions.otherGroup" : "Autres Paramètres", // Unused right now + "vcmi.systemOptions.townsGroup" : "Écran de la Ville", - "vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.", - "vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{Fullscreen}\n\nIf selected, VCMI will run in exclusive fullscreen mode. In this mode, game will change resolution of monitor to selected resolution.", - "vcmi.systemOptions.resolutionButton.hover" : "Resolution: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Select Resolution}\n\nChange in-game screen resolution.", - "vcmi.systemOptions.resolutionMenu.hover" : "Select Resolution", - "vcmi.systemOptions.resolutionMenu.help" : "Change in-game screen resolution.", - "vcmi.systemOptions.scalingButton.hover" : "Interface Scaling: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface.", - "vcmi.systemOptions.scalingMenu.hover" : "Select Interface Scaling", - "vcmi.systemOptions.scalingMenu.help" : "Change in-game interface scaling.", - "vcmi.systemOptions.longTouchButton.hover" : "Long Touch Interval: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds.", - "vcmi.systemOptions.longTouchMenu.hover" : "Select Long Touch Interval", - "vcmi.systemOptions.longTouchMenu.help" : "Change duration of long touch interval.", - "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", - "vcmi.systemOptions.framerateButton.hover" : "Show FPS", - "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Plein écran (sans bord)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nSi sélectionné, VCMI fonctionnera en mode plein écran sans bordure. Dans ce mode, le jeu utilisera toujours la même résolution de le bureau, ignorant la résolution sélectionnée.", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Plein écran (exclusif)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Fullscreen}\n\nSi sélectionné, VCMI s'exécutera en mode plein écran exclusif. Dans ce mode, le jeu modifiera la résolution du moniteur en résolution sélectionnée.", + "vcmi.systemOptions.resolutionButton.hover" : "Résolution : %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Select Resolution}\n\nChanger la résolution d'écran dans le jeu.", + "vcmi.systemOptions.resolutionMenu.hover" : "Sélectionner la résolution", + "vcmi.systemOptions.resolutionMenu.help" : "Changer la résolution d'écran dans le jeu.", + "vcmi.systemOptions.scalingButton.hover" : "Échelle d'interface : %p%", + "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanger l'échelle de l'interface dans le jeu", + "vcmi.systemOptions.scalingMenu.hover" : "Sélectionner la mise à l'échelle de l'interface", + "vcmi.systemOptions.scalingMenu.help" : "Changer la mise à l'échelle de l'interface dans le jeu.", + "vcmi.systemOptions.longTouchButton.hover" : "Intervalle de touche long : %d ms", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nAu moment d'utiliser l'écran tactile, les fenêtres contextuelles apparaîtront après avoir touché l'écran pour une durée spécifiée, en millisecondes", + "vcmi.systemOptions.longTouchMenu.hover" : "Sélectionner l'intervalle de touche long", + "vcmi.systemOptions.longTouchMenu.help" : "Changer la durée de l'intervalle de touche long.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d millisecondes", + "vcmi.systemOptions.framerateButton.hover" : "Afficher les FPS", + "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nAfficher/masquer le compteur de Frames Par Seconde dans le coin de la fenêtre du jeu", - "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", - "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", - "vcmi.adventureOptions.numericQuantities.hover" : "Numeric Creature Quantities", - "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nShow the approximate quantities of enemy creatures in the numeric A-B format.", - "vcmi.adventureOptions.forceMovementInfo.hover" : "Always Show Movement Cost", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information (Instead of viewing it only while you hold down ALT key).", - "vcmi.adventureOptions.showGrid.hover" : "Show Grid", - "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nShow the grid overlay, highlighting the borders between adventure map tiles.", - "vcmi.adventureOptions.borderScroll.hover" : "Border Scrolling", - "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.", - "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", - "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", + "vcmi.adventureOptions.infoBarPick.hover" : "Afficher les messages dans le panneau d'information", + "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nAutant que possible, les messages de jeu de la visite des objets de la carte seront affichés dans le panneau d'informations, au lieu de faire apparaître dans une fenêtre séparée.", + "vcmi.adventureOptions.numericQuantities.hover" : "Dénombrement de créatures", + "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nAfficher les estimation de nombre de créatures ennemies au format numérique A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Toujours afficher le coût de mouvement", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nToujours afficher les données des points de mouvement dans les informations sur la barre d'état (au lieu de les voir que pen maintenant la touche alt).", + "vcmi.adventureOptions.showGrid.hover" : "Afficher la grille", + "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nMontrer la superposition de la grille, mettant en évidence les frontières entre les carreaux de carte d'aventure.", + "vcmi.adventureOptions.borderScroll.hover" : "Défilement de bord", + "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nFait défiler la carte aventure lorsque le curseur est sur le bord de la fenêtre. Peut être désactivé en maintenant la touche Ctrl.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Régler la vitesse de défilement de la carte sur très lent", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Régler la vitesse de défilement de la carte très rapide", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Régler la vitesse de défilement de la carte sur instantanément.", - "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", - "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", + "vcmi.battleOptions.queueSizeLabel.hover": "Afficher l'ordre de fil de tour", + "vcmi.battleOptions.queueSizeNoneButton.hover": "Désactivé", "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "SMALL", - "vcmi.battleOptions.queueSizeBigButton.hover": "BIG", - "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue.", - "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).", - "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL.", - "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).", + "vcmi.battleOptions.queueSizeSmallButton.hover": "PETITE", + "vcmi.battleOptions.queueSizeBigButton.hover": "GRANDE", + "vcmi.battleOptions.queueSizeNoneButton.help": "Ne pas afficher l'ordre de fil de tour", + "vcmi.battleOptions.queueSizeAutoButton.help": "Ajuster automatiquement la taille de l'ordre de fil de tour en fonction de la résolution du jeu (la PETITE taille est utilisée lorsque vous jouez au jeu sur une hauteur de résolution inférieure à 700 pixels, la GRANDE taille est utilisée au sinon)", + "vcmi.battleOptions.queueSizeSmallButton.help": "Régler la taille de l'ordre de fil de tour sur PETITE", + "vcmi.battleOptions.queueSizeBigButton.help": "Régler la taille de l'ordre de fil de tour à GRANDE (non prise en charge si la hauteur de la résolution du jeu est inférieure à 700 pixels)", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow.", - "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast.", - "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous.", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Show range limits for shooters", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nShow shooter's range limits when you hover over it.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", + "vcmi.battleOptions.animationsSpeed1.help": "Régler la vitesse d'animation très lente", + "vcmi.battleOptions.animationsSpeed5.help": "Régler la vitesse d'animation très rapide", + "vcmi.battleOptions.animationsSpeed6.help": "Régler la vitesse d'animation sur instantané", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Mise en surbrillance du mouvement au survol", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nMettre en surbrillance la plage de mouvement de l'unité lorsque vous la survolez.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Afficher les limites de portée pour les tireurs", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nAfficher les limites de portée du tireur lorsque vous le survolez.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Ignorer la musique d'introduction", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAutoriser les actions pendant la musique d'intro qui joue au début de chaque bataille", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Appuyez sur n'importe quelle touche pour commencer la bataille immédiatement", - "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", - "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Shoot %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Shoot %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots" : "%d shots left", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d shot left", - "vcmi.battleWindow.damageEstimation.damage" : "%d damage", - "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", - "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", + "vcmi.battleWindow.damageEstimation.melee" : "Attaque %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Attaque %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Tir sur %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Tir sur %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d tirs restants", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d tir restant", + "vcmi.battleWindow.damageEstimation.damage" : "%d dégâts", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d dégâts", + "vcmi.battleWindow.damageEstimation.kills" : "%d va mourir", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d va mourir", - "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", + "vcmi.battleResultsWindow.applyResultsLabel" : "Appliquer le résultat de la bataille", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\nShow creatures' weekly growth instead of available amount in town summary (bottom-left corner of town screen).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Compact Creature Info", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\nShow smaller information for town creatures in town summary (bottom-left corner of town screen).", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Afficher les créatures disponibles", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nAfficher le nombre de créatures disponibles au recrutement au lieu de leur croissance dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Afficher la croissance hebdomadaire des créatures", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\nAfficher la croissance hebdomadaire des créatures au lieu de la quantité disponible dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Infos compactes sur la créature", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\nAfficher des informations plus petites pour les créatures de la ville dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", - "vcmi.townHall.missingBase" : "Base building %s must be built first", - "vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!", - "vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", - "vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", - "vcmi.townHall.greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", - "vcmi.townHall.greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).", - "vcmi.townHall.greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).", - "vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", - "vcmi.townHall.hasNotProduced" : "The %s has not produced anything yet.", - "vcmi.townHall.hasProduced" : "The %s produced %d %s this week.", - "vcmi.townHall.greetingCustomBonus" : "%s gives you +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " until next battle.", - "vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.", + "vcmi.townHall.missingBase" : "Le bâtiment de base %s doit être construit avant", + "vcmi.townHall.noCreaturesToRecruit" : "Il n'y a aucune créature à recruter !", + "vcmi.townHall.greetingManaVortex" : "Alors que vous approchez du %s, votre corps est rempli d'une nouvelle énergie. Vous avez doublé vos points de sort normaux.", + "vcmi.townHall.greetingKnowledge" : "Vous étudiez les glyphes sur le %s et découvrez le fonctionnement de diverses magies (+1 Connaissance).", + "vcmi.townHall.greetingSpellPower" : "Le %s vous apprend de nouvelles façons de concentrer vos pouvoirs magiques (+1 Pouvoir).", + "vcmi.townHall.greetingExperience" : "Une visite au %s vous apprend de nombreuses nouvelles compétences (+1000 Expérience).", + "vcmi.townHall.greetingAttack" : "Un peu de temps passé au %s vous permet d'apprendre des compétences de combat plus efficaces (+1 compétence d'attaque).", + "vcmi.townHall.greetingDefence" : "En passant du temps dans le %s, les guerriers expérimentés qui s'y trouvent vous enseignent des compétences défensives supplémentaires (+1 Défense).", + "vcmi.townHall.hasNotProduced" : "Le %s n'a encore rien produit.", + "vcmi.townHall.hasProduced" : "Le %s a produit %d %s cette semaine.", + "vcmi.townHall.greetingCustomBonus" : "%s vous offre +%d %s%s", + "vcmi.townHall.greetingCustomUntil" : " jusqu'à la prochaine bataille.", + "vcmi.townHall.greetingInTownMagicWell" : "%s a restauré vos points de sort au maximum.", - "vcmi.logicalExpressions.anyOf" : "Any of the following:", - "vcmi.logicalExpressions.allOf" : "All of the following:", - "vcmi.logicalExpressions.noneOf" : "None of the following:", + "vcmi.logicalExpressions.anyOf" : "L'un des éléments suivants :", + "vcmi.logicalExpressions.allOf" : "Tous les éléments suivants :", + "vcmi.logicalExpressions.noneOf" : "Aucun des éléments suivants :", - "vcmi.heroWindow.openCommander.hover" : "Open commander info window", - "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", - "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", - "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", + "vcmi.heroWindow.openCommander.hover" : "Ouvrir la fenêtre d'informations du commandant", + "vcmi.heroWindow.openCommander.help" : "Affiche des détails sur le commandant de ce héros", - "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", + "vcmi.commanderWindow.artifactMessage" : "Voulez-vous rendre cet artefact au héros ?", - "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", - "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", - "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", - "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", - "vcmi.creatureWindow.returnArtifact.hover" : "Return artifact", - "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack.", + "vcmi.creatureWindow.showBonuses.hover" : "Passer en vue bonus", + "vcmi.creatureWindow.showBonuses.help" : "Afficher tous les bonus actifs du commandant", + "vcmi.creatureWindow.showSkills.hover" : "Passer à la vue des compétences", + "vcmi.creatureWindow.showSkills.help" : "Afficher toutes les compétences apprises du commandant", + "vcmi.creatureWindow.returnArtifact.hover" : "Remettre l'artefact", + "vcmi.creatureWindow.returnArtifact.help" : "Cliquez sur ce bouton pour remettre l'artefact dans le sac à dos du héros", - "vcmi.questLog.hideComplete.hover" : "Hide complete quests", - "vcmi.questLog.hideComplete.help" : "Hide all completed quests.", - - "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", - "vcmi.randomMapTab.widgets.templateLabel" : "Template", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", - "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", - - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", - "vcmi.optionsTab.widgets.labelTimer" : "Timer", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", + "vcmi.questLog.hideComplete.hover" : "Masquer les quêtes terminées", + "vcmi.questLog.hideComplete.help" : "Masquer toutes les quêtes terminées", + "vcmi.randomMapTab.widgets.templateLabel" : "Modèle", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuration...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alignements d'équipe", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Types de routes", // Custom victory conditions for H3 campaigns and HotA maps - "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", - "vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "The enemy has defeated all of the monsters plaguing this land and claims victory!", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Congratulations! You have defeated all of the monsters plaguing this land and can claim victory!", - "vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts", - "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance", + "vcmi.map.victoryCondition.daysPassed.toOthers" : "L'ennemi a réussi à survivre jusqu'à ce jour. La victoire est à eux !", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Félicitations ! Vous avez réussi à survivre. La victoire est à vous !", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "L'ennemi a vaincu tous les monstres qui sévissent sur cette terre et revendique la victoire !", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Félicitations ! Vous avez vaincu tous les monstres qui affligent ce pays et vous pouvez revendiquer la victoire !", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Acquérir trois artefacts", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Félicitations ! Tous vos ennemis ont été vaincus et vous avez l'alliance angélique ! La victoire est à vous !", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Vaincre tous les ennemis et créer une alliance angélique", // few strings from WoG used by vcmi - "vcmi.stackExperience.description" : "» S t a c k E x p e r i e n c e D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i", - "vcmi.stackExperience.rank.0" : "Basic", + "vcmi.stackExperience.description" : "» D é t a i l s d e P i l e d ' E x p é r i e n c e «\n\nType de créature ................ : %s\nRang d'expérience ............... : %s (%i)\nPoints d'expérience ............. : %i\nPoints d'XP au rang suivant ..... : %i\nExpérience maximum par bataille . : %i%% (%i)\nNb de créatures dans la pile .... : %i\nMaximum de nouvelles recrues\n sans perdre le rang actuel ..... : %i\nMultiplicateur d'expérience ..... : %.2f\nMultiplicateur d'amélioration ... : %.2f\nExpérience après le rang 10 ..... : %i\nMaximum de Nouvelles Recrues pour\n rester au Rang 10 si\n l'Expérience Maximum ........... : %i", + "vcmi.stackExperience.rank.0" : "Basique", "vcmi.stackExperience.rank.1" : "Novice", - "vcmi.stackExperience.rank.2" : "Trained", - "vcmi.stackExperience.rank.3" : "Skilled", - "vcmi.stackExperience.rank.4" : "Proven", - "vcmi.stackExperience.rank.5" : "Veteran", - "vcmi.stackExperience.rank.6" : "Adept", + "vcmi.stackExperience.rank.2" : "Formé", + "vcmi.stackExperience.rank.3" : "Qualifié", + "vcmi.stackExperience.rank.4" : "Éprouvé", + "vcmi.stackExperience.rank.5" : "Vétéran", + "vcmi.stackExperience.rank.6" : "Adepte", "vcmi.stackExperience.rank.7" : "Expert", - "vcmi.stackExperience.rank.8" : "Elite", - "vcmi.stackExperience.rank.9" : "Master", - "vcmi.stackExperience.rank.10" : "Ace", + "vcmi.stackExperience.rank.8" : "Élite", + "vcmi.stackExperience.rank.9" : "Maître", + "vcmi.stackExperience.rank.10" : "As", - "core.bonus.ADDITIONAL_ATTACK.name": "Double Strike", - "core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice", - "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", - "core.bonus.ADDITIONAL_RETALIATION.description": "May retaliate ${val} extra times", - "core.bonus.AIR_IMMUNITY.name": "Air immunity", - "core.bonus.AIR_IMMUNITY.description": "Immune to all spells from the school of Air magic", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Attack all around", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Attacks all adjacent enemies", - "core.bonus.BLOCKS_RETALIATION.name": "No retaliation", - "core.bonus.BLOCKS_RETALIATION.description": "Enemy cannot retaliate", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "No ranged retaliation", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Enemy cannot retaliate by using a ranged attack", - "core.bonus.CATAPULT.name": "Catapult", - "core.bonus.CATAPULT.description": "Attacks siege walls", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduce Casting Cost (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduces the spellcasting cost for the hero by ${val}", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Magic Damper (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Increases spellcasting cost of enemy spells by ${val}", - "core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", - "core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge", - "core.bonus.DARKNESS.name": "Darkness cover", - "core.bonus.DARKNESS.description": "Creates a shroud of darkness with a ${val} radius", - "core.bonus.DEATH_STARE.name": "Death Stare (${val}%)", - "core.bonus.DEATH_STARE.description": "Has a ${val}% chance to kill a single creature", - "core.bonus.DEFENSIVE_STANCE.name": "Defense Bonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Defense when defending", + "core.bonus.ADDITIONAL_ATTACK.name": "Double frappe", + "core.bonus.ADDITIONAL_ATTACK.description": "Attaque deux fois", + "core.bonus.ADDITIONAL_RETALIATION.name": "Représailles supplémentaires", + "core.bonus.ADDITIONAL_RETALIATION.description": "Peut riposter ${val} fois de plus", + "core.bonus.AIR_IMMUNITY.name": "Immunité aérienne", + "core.bonus.AIR_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de l'Air", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Attaque tout autour", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Attaque tous les ennemis adjacents", + "core.bonus.BLOCKS_RETALIATION.name": "Pas de représailles", + "core.bonus.BLOCKS_RETALIATION.description": "L'ennemi ne peut pas riposter", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Pas de représailles à distance", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "L'ennemi ne peut pas riposter en utilisant une attaque à distance", + "core.bonus.CATAPULT.name": "Catapulte", + "core.bonus.CATAPULT.description": "Attaque les murs de siège", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Réduire le coût de lancement (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Réduit le coût d'incantation du héros de ${val}", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Amortisseur magique (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Augmente le coût d'incantation des sorts ennemis de ${val}", + "core.bonus.CHARGE_IMMUNITY.name": "Immunisé contre la charge", + "core.bonus.CHARGE_IMMUNITY.description": "Immunisé contre la charge du cavalier et du champion", + "core.bonus.DARKNESS.name": "Couverture de ténèbres", + "core.bonus.DARKNESS.description": "Crée un linceul de ténèbres avec un rayon de ${val}", + "core.bonus.DEATH_STARE.name": "Regard mortel (${val}%)", + "core.bonus.DEATH_STARE.description": "A ${val}% de chances de tuer une seule créature", + "core.bonus.DEFENSIVE_STANCE.name": "Bonus de défense", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Défense en défense", "core.bonus.DESTRUCTION.name": "Destruction", - "core.bonus.DESTRUCTION.description": "Has ${val}% chance to kill extra units after attack", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Death Blow", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking", + "core.bonus.DESTRUCTION.description": "A ${val} % de chances de tuer des unités supplémentaires après l'attaque", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Coup mortel", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "A ${val}% de chances d'infliger des dégâts de base doublés en attaquant", "core.bonus.DRAGON_NATURE.name": "Dragon", - "core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature", - "core.bonus.EARTH_IMMUNITY.name": "Earth immunity", - "core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic", - "core.bonus.ENCHANTER.name": "Enchanter", - "core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn", - "core.bonus.ENCHANTED.name": "Enchanted", - "core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored", - "core.bonus.FIRE_IMMUNITY.name": "Fire immunity", - "core.bonus.FIRE_IMMUNITY.description": "Immune to all spells from the school of Fire magic", - "core.bonus.FIRE_SHIELD.name": "Fire Shield (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Reflects part of melee damage", - "core.bonus.FIRST_STRIKE.name": "First Strike", - "core.bonus.FIRST_STRIKE.description": "This creature retaliates before being attacked", - "core.bonus.FEAR.name": "Fear", - "core.bonus.FEAR.description": "Causes Fear on an enemy stack", - "core.bonus.FEARLESS.name": "Fearless", - "core.bonus.FEARLESS.description": "Immune to Fear ability", - "core.bonus.FLYING.name": "Fly", - "core.bonus.FLYING.description": "Flies when moving (ignores obstacles)", - "core.bonus.FREE_SHOOTING.name": "Shoot Close", - "core.bonus.FREE_SHOOTING.description": "Can use ranged attacks at melee range", - "core.bonus.GARGOYLE.name": "Gargoyle", - "core.bonus.GARGOYLE.description": "Cannot be raised or healed", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduces physical damage from ranged or melee attacks", - "core.bonus.HATE.name": "Hates ${subtype.creature}", - "core.bonus.HATE.description": "Does ${val}% more damage to ${subtype.creature}", - "core.bonus.HEALER.name": "Healer", - "core.bonus.HEALER.description": "Heals allied units", - "core.bonus.HP_REGENERATION.name": "Regeneration", - "core.bonus.HP_REGENERATION.description": "Heals ${val} hit points every round", - "core.bonus.JOUSTING.name": "Champion charge", - "core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled", - "core.bonus.KING.name": "King", - "core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Unable to target units farther than ${val} hexes", - "core.bonus.LIFE_DRAIN.name": "Drain life (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Drains ${val}% of damage dealt", - "core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", - "core.bonus.MANA_CHANNELING.description": "Gives your hero ${val}% of the mana spent by the enemy", - "core.bonus.MANA_DRAIN.name": "Mana Drain", - "core.bonus.MANA_DRAIN.description": "Drains ${val} mana every turn", - "core.bonus.MAGIC_MIRROR.name": "Magic Mirror (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "Has a ${val}% chance to redirect an offensive spell to an enemy unit", - "core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "Has a ${val}% chance to resist an enemy spell", - "core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity", - "core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells", - "core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty", - "core.bonus.NO_DISTANCE_PENALTY.description": "Does full damage at any distance", - "core.bonus.NO_MELEE_PENALTY.name": "No melee penalty", - "core.bonus.NO_MELEE_PENALTY.description": "Creature has no Melee Penalty", - "core.bonus.NO_MORALE.name": "Neutral Morale", - "core.bonus.NO_MORALE.description": "Creature is immune to morale effects", - "core.bonus.NO_WALL_PENALTY.name": "No wall penalty", - "core.bonus.NO_WALL_PENALTY.description": "Full damage during siege", - "core.bonus.NON_LIVING.name": "Non living", - "core.bonus.NON_LIVING.description": "Immunity to many effects", - "core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster", - "core.bonus.RANDOM_SPELLCASTER.description": "Can cast random spell", - "core.bonus.RANGED_RETALIATION.name": "Ranged retaliation", - "core.bonus.RANGED_RETALIATION.description": "Can perform ranged counterattack", - "core.bonus.RECEPTIVE.name": "Receptive", - "core.bonus.RECEPTIVE.description": "No Immunity to Friendly Spells", - "core.bonus.REBIRTH.name": "Rebirth (${val}%)", - "core.bonus.REBIRTH.description": "${val}% of stack will rise after death", - "core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return", - "core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack", - "core.bonus.SHOOTER.name": "Ranged", - "core.bonus.SHOOTER.description": "Creature can shoot", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "This creature's ranged attacks strike all targets in a small area", - "core.bonus.SOUL_STEAL.name": "Soul Steal", - "core.bonus.SOUL_STEAL.description": "Gains ${val} new creatures for each enemy killed", - "core.bonus.SPELLCASTER.name": "Spellcaster", - "core.bonus.SPELLCASTER.description": "Can cast ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Cast After Attack", - "core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack", - "core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from spells reduced by ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Spell immunity", - "core.bonus.SPELL_IMMUNITY.description": "Immune to ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack", - "core.bonus.SPELL_LIKE_ATTACK.description": "Attacks with ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura of Resistance", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Nearby stacks get ${val}% magic resistance", - "core.bonus.SUMMON_GUARDIANS.name": "Summon guardians", - "core.bonus.SUMMON_GUARDIANS.description": "At the start of battle summons ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergizable", - "core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Breath Attack (2-hex range)", - "core.bonus.THREE_HEADED_ATTACK.name": "Three-headed attack", - "core.bonus.THREE_HEADED_ATTACK.description": "Attacks three adjacent units", + "core.bonus.DRAGON_NATURE.description": "La créature a une nature de dragon", + "core.bonus.EARTH_IMMUNITY.name": "Immunité terrestre", + "core.bonus.EARTH_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de la Terre", + "core.bonus.ENCHANTER.name": "Enchanteur", + "core.bonus.ENCHANTER.description": "Peut lancer en masse ${subtype.spell} à chaque tour", + "core.bonus.ENCHANTED.name": "Enchanté", + "core.bonus.ENCHANTED.description": "Affecté par ${subtype.spell} permanent", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorer la défense (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Lors de l'attaque, ${val}% de la défense du défenseur est ignorée", + "core.bonus.FIRE_IMMUNITY.name": "Immunité au feu", + "core.bonus.FIRE_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie du Feu", + "core.bonus.FIRE_SHIELD.name": "Bouclier de feu (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Reflète une partie des dégâts de mêlée", + "core.bonus.FIRST_STRIKE.name": "Premier coup", + "core.bonus.FIRST_STRIKE.description": "Cette créature riposte avant d'être attaquée", + "core.bonus.FEAR.name": "Peur", + "core.bonus.FEAR.description": "Provoque la peur sur une pile ennemie", + "core.bonus.FEARLESS.name": "Intrépide", + "core.bonus.FEARLESS.description": "Immunité à la peur", + "core.bonus.FLYING.name": "Vol", + "core.bonus.FLYING.description": "Vole en se déplaçant (ignore les obstacles)", + "core.bonus.FREE_SHOOTING.name": "Tirer de près", + "core.bonus.FREE_SHOOTING.description": "Peut utiliser des attaques à distance au corps à corps", + "core.bonus.GARGOYLE.name": "Gargouille", + "core.bonus.GARGOYLE.description": "Ne peut pas être réanimé ou soigné", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Réduit les dégâts (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Réduit les dégâts physiques des attaques à distance ou de mêlée", + "core.bonus.HATE.name": "Déteste ${subtype.creature}", + "core.bonus.HATE.description": "Inflige ${val} % de dégâts supplémentaires à ${subtype.creature}", + "core.bonus.HEALER.name": "Guérisseur", + "core.bonus.HEALER.description": "Soigne les unités alliées", + "core.bonus.HP_REGENERATION.name": "Régénération", + "core.bonus.HP_REGENERATION.description": "Soigne ${val} points de vie à chaque tour", + "core.bonus.JOUSTING.name": "Charge de champion", + "core.bonus.JOUSTING.description": "+${val}% de dégâts pour chaque hexagone parcouru", + "core.bonus.KING.name": "Roi", + "core.bonus.KING.description": "Vulnérable au niveau POURFENDEUR ${val} ou supérieur", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Immunité aux sorts 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immunisé aux sorts de niveaux 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Portée de tir limitée", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Impossible de cibler des unités plus loin que ${val} hexagones", + "core.bonus.LIFE_DRAIN.name": "Durée de vie du drain (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Draine ${val}% des dégâts infligés", + "core.bonus.MANA_CHANNELING.name": "Chaîne magique ${val}%", + "core.bonus.MANA_CHANNELING.description": "Donne à votre héros ${val}% du mana dépensé par l'ennemi", + "core.bonus.MANA_DRAIN.name": "Drain de mana", + "core.bonus.MANA_DRAIN.description": "Draine ${val} mana à chaque tour", + "core.bonus.MAGIC_MIRROR.name": "Miroir magique (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "A ${val} % de chances de rediriger un sort offensif vers une unité ennemie", + "core.bonus.MAGIC_RESISTANCE.name": "Résistance magique (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "A ${val}% de chances de résister à un sort ennemi", + "core.bonus.MIND_IMMUNITY.name": "Immunité contre les sorts de l'esprit", + "core.bonus.MIND_IMMUNITY.description": "Immunisé contre les sorts de type Mental", + "core.bonus.NO_DISTANCE_PENALTY.name": "Aucune pénalité de distance", + "core.bonus.NO_DISTANCE_PENALTY.description": "Inflige des dégâts complets à n'importe quelle distance", + "core.bonus.NO_MELEE_PENALTY.name": "Aucune pénalité de mêlée", + "core.bonus.NO_MELEE_PENALTY.description": "La créature n'a pas de pénalité de mêlée", + "core.bonus.NO_MORALE.name": "Moral neutre", + "core.bonus.NO_MORALE.description": "La créature est immunisée contre les effets de moral", + "core.bonus.NO_WALL_PENALTY.name": "Aucune pénalité de mur", + "core.bonus.NO_WALL_PENALTY.description": "Dégâts complets pendant le siège", + "core.bonus.NON_LIVING.name": "Non vivant", + "core.bonus.NON_LIVING.description": "Immunité à de nombreux effets", + "core.bonus.RANDOM_SPELLCASTER.name": "Lanceur de sorts aléatoire", + "core.bonus.RANDOM_SPELLCASTER.description": "Peut lancer un sort aléatoire", + "core.bonus.RANGED_RETALIATION.name": "Représailles à distance", + "core.bonus.RANGED_RETALIATION.description": "Peut effectuer une contre-attaque à distance", + "core.bonus.RECEPTIVE.name": "Réceptif", + "core.bonus.RECEPTIVE.description": "Pas d'immunité aux sorts amicaux", + "core.bonus.REBIRTH.name": "Renaissance (${val}%)", + "core.bonus.REBIRTH.description": "${val}% de la pile augmentera après la mort", + "core.bonus.RETURN_AFTER_STRIKE.name": "Attaque et retour", + "core.bonus.RETURN_AFTER_STRIKE.description": "Revient après une attaque au corps à corps", + "core.bonus.SHOOTER.name": "Distance", + "core.bonus.SHOOTER.description": "La créature peut tirer", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Tirer tout autour", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Les attaques à distance de cette créature touchent toutes les cibles dans une petite zone", + "core.bonus.SOUL_STEAL.name": "Vol d'âme", + "core.bonus.SOUL_STEAL.description": "Gagne ${val} nouvelles créatures pour chaque ennemi tué", + "core.bonus.SPELLCASTER.name": "Lanceur de sorts", + "core.bonus.SPELLCASTER.description": "Peut lancer ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Lancer après l'attaque", + "core.bonus.SPELL_AFTER_ATTACK.description": "A ${val}% de chances de lancer ${subtype.spell} après avoir attaqué", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Lancer avant l'attaque", + "core.bonus.SPELL_BEFORE_ATTACK.description": "A ${val}% de chances de lancer ${subtype.spell} avant qu'il n'attaque", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Résistance aux sorts", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Dégâts des sorts réduits de ${val}%.", + "core.bonus.SPELL_IMMUNITY.name": "Immunité aux sorts", + "core.bonus.SPELL_IMMUNITY.description": "Immunisé contre ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "Attaque semblable à un sort", + "core.bonus.SPELL_LIKE_ATTACK.description": "Attaque avec ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura de résistance", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Les piles à proximité obtiennent ${val}% de résistance magique", + "core.bonus.SUMMON_GUARDIANS.name": "Invoquer des gardiens", + "core.bonus.SUMMON_GUARDIANS.description": "Au début de la bataille, invoque ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Synergique", + "core.bonus.SYNERGY_TARGET.description": "Cette créature est vulnérable à l'effet de synergie", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Souffle", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Attaque de souffle (portée de 2 hexagones)", + "core.bonus.THREE_HEADED_ATTACK.name": "Attaque à trois têtes", + "core.bonus.THREE_HEADED_ATTACK.description": "Attaque trois unités adjacentes", "core.bonus.TRANSMUTATION.name": "Transmutation", - "core.bonus.TRANSMUTATION.description": "${val}% chance to transform attacked unit to a different type", - "core.bonus.UNDEAD.name": "Undead", - "core.bonus.UNDEAD.description": "Creature is Undead", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Unlimited retaliations", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Can retaliate against an unlimited number of attacks", - "core.bonus.WATER_IMMUNITY.name": "Water immunity", - "core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic", - "core.bonus.WIDE_BREATH.name": "Wide breath", - "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)" + "core.bonus.TRANSMUTATION.description": "${val}% de chances de transformer l'unité attaquée en un type différent", + "core.bonus.UNDEAD.name": "Morts-vivants", + "core.bonus.UNDEAD.description": "La créature est un mort-vivant", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Représailles illimitées", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Peut riposter contre un nombre illimité d'attaques", + "core.bonus.WATER_IMMUNITY.name": "Immunité à l'eau", + "core.bonus.WATER_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de l'Eau", + "core.bonus.WIDE_BREATH.name": "Large souffle", + "core.bonus.WIDE_BREATH.description": "Attaque à souffle large (plusieurs hexagones)" } diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 5d3e0a532..83e81dee6 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -30,7 +30,6 @@ "vcmi.capitalColors.6" : "Turquoise", "vcmi.capitalColors.7" : "Rose", - "vcmi.mainMenu.highscoresNotImplemented" : "Désolé, le menu des meilleurs scores n'est pas encore implémenté\n", "vcmi.mainMenu.serverConnecting" : "Connexion...", "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", "vcmi.mainMenu.serverClosing" : "Fermeture...", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 926501565..0198d30fd 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -41,7 +41,6 @@ "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", - "vcmi.mainMenu.highscoresNotImplemented" : "Die Highscores sind aktuell noch nicht implementiert\n", "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", "vcmi.mainMenu.serverClosing" : "Trenne...", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index f382a9578..be96f40e2 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -36,7 +36,6 @@ "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", - "vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n", "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", "vcmi.mainMenu.serverClosing" : "Zamykanie...", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 78f16b9d4..0ad0bd9d2 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -37,7 +37,6 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", - "vcmi.mainMenu.highscoresNotImplemented" : "Вибачте, таблицю рекордів ще не реалізовано\n", "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", "vcmi.mainMenu.serverClosing" : "Завершення...", diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 7e0731cb2..9abc1e961 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -59,6 +59,7 @@ set(client_SRCS mainmenu/CMainMenu.cpp mainmenu/CPrologEpilogVideo.cpp mainmenu/CreditsScreen.cpp + mainmenu/CHighScoreScreen.cpp mapView/MapRenderer.cpp mapView/MapRendererContext.cpp @@ -213,6 +214,7 @@ set(client_HEADERS mainmenu/CMainMenu.h mainmenu/CPrologEpilogVideo.h mainmenu/CreditsScreen.h + mainmenu/CHighScoreScreen.h mapView/IMapRendererContext.h mapView/IMapRendererObserver.h diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp index 5abaf79fd..57fc618b5 100644 --- a/client/VCMI_client.cbp +++ b/client/VCMI_client.cbp @@ -198,6 +198,8 @@ + + diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj index fd89da223..3fe7331f9 100644 --- a/client/VCMI_client.vcxproj +++ b/client/VCMI_client.vcxproj @@ -217,6 +217,7 @@ + @@ -286,6 +287,7 @@ + diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters index d5f5af0fa..8831d0480 100644 --- a/client/VCMI_client.vcxproj.filters +++ b/client/VCMI_client.vcxproj.filters @@ -132,6 +132,7 @@ + @@ -290,5 +291,6 @@ + \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp new file mode 100644 index 000000000..d2874aee3 --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -0,0 +1,161 @@ +/* + * CHighScoreScreen.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 "CHighScoreScreen.h" +#include "../gui/CGuiHandler.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../windows/InfoWindows.h" +#include "../render/Canvas.h" + +#include "../CGameInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/constants/EntityIdentifiers.h" + +CHighScoreScreen::CHighScoreScreen() + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + addHighScores(); + addButtons(); + + // TODO: remove; only for testing + for (int i = 0; i < 11; i++) + { + Settings entry = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["player"]; + entry->String() = "test"; + Settings entry1 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; + entry1->String() = "test"; + Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["points"]; + entry2->Integer() = 100; + + Settings entry3 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["player"]; + entry3->String() = "test"; + Settings entry4 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["land"]; + entry4->String() = "test"; + Settings entry5 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["days"]; + entry5->Integer() = 123; + Settings entry6 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["points"]; + entry6->Integer() = 100; + } +} + +void CHighScoreScreen::addButtons() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + buttons.clear(); + + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaginClick(); })); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonStandardClick(); })); + buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); + buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); +} + +void CHighScoreScreen::addHighScores() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::STANDARD ? "HISCORE" : "HISCORE2")); + + texts.clear(); + + static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + auto creatures = configCreatures["creatures"].Vector(); + for(auto & creature : creatures) { + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[CreatureID::decode(creature["creature"].String())]->getIconIndex(), 0, 10, 10)); + } + + // Header + texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); + texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); + + if(highscorepage == HighScorePage::STANDARD) + { + texts.push_back(std::make_shared(400, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); + texts.push_back(std::make_shared(555, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); + texts.push_back(std::make_shared(625, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + } + else + { + texts.push_back(std::make_shared(410, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); + texts.push_back(std::make_shared(590, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + } + + // Content + int y = 65; + auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::STANDARD ? "standard" : "campaign"]; + for (int i = 0; i < 11; i++) + { + auto & curData = data[std::to_string(i)]; + + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(i))); + texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["player"].String())); + + if(highscorepage == HighScorePage::STANDARD) + { + texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["land"].String())); + texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); + } + else + { + texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["campaign"].String())); + texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); + } + } +} + +void CHighScoreScreen::buttonCampaginClick() +{ + highscorepage = HighScorePage::CAMPAIGN; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonStandardClick() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + highscorepage = HighScorePage::STANDARD; + addHighScores(); + addButtons(); + redraw(); +} + +void CHighScoreScreen::buttonResetClick() +{ + CInfoWindow::showYesNoDialog( + CGI->generaltexth->allTexts[666], + {}, + [this]() + { + Settings entry = persistentStorage.write["highscore"]; + entry->clear(); + addHighScores(); + addButtons(); + redraw(); + }, + 0 + ); +} + +void CHighScoreScreen::buttonExitClick() +{ + close(); +} diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h new file mode 100644 index 000000000..418b1a43c --- /dev/null +++ b/client/mainmenu/CHighScoreScreen.h @@ -0,0 +1,37 @@ +/* + * CHighScoreScreen.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 "../windows/CWindowObject.h" + +class CButton; +class CLabel; +class CAnimImage; + +class CHighScoreScreen : public CWindowObject +{ + enum HighScorePage { STANDARD, CAMPAIGN }; + + void addButtons(); + void addHighScores(); + + void buttonCampaginClick(); + void buttonStandardClick(); + void buttonResetClick(); + void buttonExitClick(); + + HighScorePage highscorepage = HighScorePage::STANDARD; + + std::shared_ptr background; + std::vector> buttons; + std::vector> texts; + std::vector> images; +public: + CHighScoreScreen(); +}; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 119763fed..d922eda25 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -12,6 +12,7 @@ #include "CCampaignScreen.h" #include "CreditsScreen.h" +#include "CHighScoreScreen.h" #include "../lobby/CBonusSelection.h" #include "../lobby/CSelectionBase.h" @@ -210,7 +211,7 @@ static std::function genCommand(CMenuScreen * menu, std::vectorgeneraltexth->translate("vcmi.mainMenu.highscoresNotImplemented"), std::vector>(), PlayerColor(1)); + return std::bind(CMainMenu::openHighScoreScreen); } } } @@ -389,6 +390,12 @@ void CMainMenu::startTutorial() CSH->startMapAfterConnection(mapInfo); } +void CMainMenu::openHighScoreScreen() +{ + GH.windows().createAndPushWindow(); + return; +} + std::shared_ptr CMainMenu::create() { if(!CMM) diff --git a/client/mainmenu/CMainMenu.h b/client/mainmenu/CMainMenu.h index f0e246c60..ea9010797 100644 --- a/client/mainmenu/CMainMenu.h +++ b/client/mainmenu/CMainMenu.h @@ -152,6 +152,7 @@ public: static void openCampaignLobby(const std::string & campaignFileName, std::string campaignSet = ""); static void openCampaignLobby(std::shared_ptr campaign); static void startTutorial(); + static void openHighScoreScreen(); void openCampaignScreen(std::string name); static std::shared_ptr create(); diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json new file mode 100644 index 000000000..51392069b --- /dev/null +++ b/config/highscoreCreatures.json @@ -0,0 +1,122 @@ +{ + "creatures": [ + { "min" : 1, "max" : 4, "creature": "core:imp" }, + { "min" : 5, "max" : 8, "creature": "core:gremlin" }, + { "min" : 9, "max" : 12, "creature": "core:gnoll" }, + { "min" : 13, "max" : 16, "creature": "core:troglodyte" }, + { "min" : 17, "max" : 20, "creature": "core:familiar" }, + { "min" : 21, "max" : 24, "creature": "core:skeleton" }, + { "min" : 25, "max" : 28, "creature": "core:goblin" }, + { "min" : 29, "max" : 32, "creature": "core:masterGremlin" }, + { "min" : 33, "max" : 36, "creature": "core:hobgoblin" }, + { "min" : 37, "max" : 40, "creature": "core:pikeman" }, + { "min" : 41, "max" : 44, "creature": "core:infernoTroglodyte" }, + { "min" : 45, "max" : 48, "creature": "core:skeletonWarrior" }, + { "min" : 49, "max" : 52, "creature": "core:gnollMarauder" }, + { "min" : 53, "max" : 56, "creature": "core:walkingDead" }, + { "min" : 57, "max" : 60, "creature": "core:centaur" }, + { "min" : 61, "max" : 64, "creature": "core:herberdier" }, + { "min" : 65, "max" : 68, "creature": "core:archer" }, + { "min" : 69, "max" : 72, "creature": "core:lizardman" }, + { "min" : 73, "max" : 76, "creature": "core:zombie" }, + { "min" : 77, "max" : 80, "creature": "core:wolfRider" }, + { "min" : 81, "max" : 84, "creature": "core:centaurCaptian" }, + { "min" : 85, "max" : 88, "creature": "core:dwarf" }, + { "min" : 89, "max" : 92, "creature": "core:harpy" }, + { "min" : 93, "max" : 96, "creature": "core:lizardWarrior" }, + { "min" : 97, "max" : 100, "creature": "core:gog" }, + { "min" : 101, "max" : 104, "creature": "core:stoneGargoyle" }, + { "min" : 105, "max" : 108, "creature": "core:masksman" }, + { "min" : 109, "max" : 112, "creature": "core:orc" }, + { "min" : 113, "max" : 116, "creature": "core:obsidianGargoyle" }, + { "min" : 117, "max" : 120, "creature": "core:wofRaider" }, + { "min" : 121, "max" : 124, "creature": "core:battleDwarf" }, + { "min" : 125, "max" : 128, "creature": "core:elf" }, + { "min" : 129, "max" : 132, "creature": "core:harpyHag" }, + { "min" : 133, "max" : 136, "creature": "core:magog" }, + { "min" : 137, "max" : 140, "creature": "core:orcChieftain" }, + { "min" : 141, "max" : 144, "creature": "core:stoneGolem" }, + { "min" : 145, "max" : 148, "creature": "core:wight" }, + { "min" : 149, "max" : 152, "creature": "core:serpentFly" }, + { "min" : 153, "max" : 156, "creature": "core:dragonFly" }, + { "min" : 157, "max" : 160, "creature": "core:wraith" }, + { "min" : 161, "max" : 164, "creature": "core:waterElemental" }, + { "min" : 165, "max" : 168, "creature": "core:earthElemental" }, + { "min" : 169, "max" : 172, "creature": "core:grandElf" }, + { "min" : 173, "max" : 176, "creature": "core:beholder" }, + { "min" : 177, "max" : 180, "creature": "core:fireElemental" }, + { "min" : 181, "max" : 184, "creature": "core:griffin" }, + { "min" : 185, "max" : 187, "creature": "core:airElemental" }, + { "min" : 188, "max" : 190, "creature": "core:hellHound" }, + { "min" : 191, "max" : 193, "creature": "core:evilEye" }, + { "min" : 194, "max" : 196, "creature": "core:cerberus" }, + { "min" : 197, "max" : 199, "creature": "core:ironGolem" }, + { "min" : 200, "max" : 202, "creature": "core:orge" }, + { "min" : 203, "max" : 205, "creature": "core:swordman" }, + { "min" : 206, "max" : 208, "creature": "core:demon" }, + { "min" : 209, "max" : 211, "creature": "core:royalGriffin" }, + { "min" : 212, "max" : 214, "creature": "core:hornedDemon" }, + { "min" : 215, "max" : 217, "creature": "core:monk" }, + { "min" : 218, "max" : 220, "creature": "core:dendroidGuard" }, + { "min" : 221, "max" : 223, "creature": "core:medusa" }, + { "min" : 224, "max" : 226, "creature": "core:pegasus" }, + { "min" : 227, "max" : 229, "creature": "core:silverPegasus" }, + { "min" : 230, "max" : 232, "creature": "core:basilisk" }, + { "min" : 233, "max" : 235, "creature": "core:vampire" }, + { "min" : 236, "max" : 238, "creature": "core:mage" }, + { "min" : 239, "max" : 241, "creature": "core:medusaQueen" }, + { "min" : 242, "max" : 244, "creature": "core:crusider" }, + { "min" : 245, "max" : 247, "creature": "core:goldGolem" }, + { "min" : 248, "max" : 250, "creature": "core:orgeMage" }, + { "min" : 251, "max" : 253, "creature": "core:archMage" }, + { "min" : 254, "max" : 256, "creature": "core:greaterBasilisk" }, + { "min" : 257, "max" : 259, "creature": "core:zealot" }, + { "min" : 260, "max" : 262, "creature": "core:pitFiend" }, + { "min" : 263, "max" : 265, "creature": "core:diamondGolem" }, + { "min" : 266, "max" : 268, "creature": "core:vampireLord" }, + { "min" : 269, "max" : 271, "creature": "core:dendroidSolider" }, + { "min" : 272, "max" : 274, "creature": "core:minotaur" }, + { "min" : 275, "max" : 277, "creature": "core:lich" }, + { "min" : 278, "max" : 280, "creature": "core:genie" }, + { "min" : 281, "max" : 283, "creature": "core:gorgon" }, + { "min" : 284, "max" : 286, "creature": "core:masterGenie" }, + { "min" : 287, "max" : 289, "creature": "core:roc" }, + { "min" : 290, "max" : 292, "creature": "core:mightyGorgon" }, + { "min" : 293, "max" : 295, "creature": "core:minotaurKing" }, + { "min" : 296, "max" : 298, "creature": "core:powerLich" }, + { "min" : 299, "max" : 301, "creature": "core:thunderbird" }, + { "min" : 302, "max" : 304, "creature": "core:pitLord" }, + { "min" : 305, "max" : 307, "creature": "core:cyclops" }, + { "min" : 308, "max" : 310, "creature": "core:wyvern" }, + { "min" : 311, "max" : 313, "creature": "core:cyclopsKing" }, + { "min" : 314, "max" : 316, "creature": "core:wyvernMonarch" }, + { "min" : 317, "max" : 319, "creature": "core:manticore" }, + { "min" : 320, "max" : 322, "creature": "core:scorpicore" }, + { "min" : 323, "max" : 325, "creature": "core:efreeti" }, + { "min" : 326, "max" : 328, "creature": "core:unicorn" }, + { "min" : 329, "max" : 331, "creature": "core:efreetSultan" }, + { "min" : 332, "max" : 334, "creature": "core:cavalier" }, + { "min" : 335, "max" : 337, "creature": "core:naga" }, + { "min" : 338, "max" : 340, "creature": "core:warUnicorn" }, + { "min" : 341, "max" : 343, "creature": "core:blackKnight" }, + { "min" : 344, "max" : 346, "creature": "core:champion" }, + { "min" : 347, "max" : 349, "creature": "core:dreadKnight" }, + { "min" : 350, "max" : 352, "creature": "core:nagaQueen" }, + { "min" : 353, "max" : 355, "creature": "core:behemoth" }, + { "min" : 356, "max" : 358, "creature": "core:boneDragon" }, + { "min" : 359, "max" : 361, "creature": "core:giant" }, + { "min" : 362, "max" : 364, "creature": "core:hydra" }, + { "min" : 365, "max" : 367, "creature": "core:ghostDragon" }, + { "min" : 368, "max" : 370, "creature": "core:redDragon" }, + { "min" : 371, "max" : 373, "creature": "core:greenDragon" }, + { "min" : 374, "max" : 376, "creature": "core:angle" }, + { "min" : 377, "max" : 379, "creature": "core:devil" }, + { "min" : 380, "max" : 382, "creature": "core:chaosHydra" }, + { "min" : 383, "max" : 385, "creature": "core:ancientBehemoth" }, + { "min" : 386, "max" : 388, "creature": "core:archDevil" }, + { "min" : 389, "max" : 391, "creature": "core:titan" }, + { "min" : 392, "max" : 394, "creature": "core:goldDragon" }, + { "min" : 395, "max" : 397, "creature": "core:blackDragon" }, + { "min" : 398, "max" : 400, "creature": "core:archAngle " } + ] +} \ No newline at end of file diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 80fbd48d9..1b5932626 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -204,8 +204,8 @@ class HeroTypeID : public Identifier public: using Identifier::Identifier; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); DLL_LINKAGE static const HeroTypeID NONE; @@ -248,8 +248,8 @@ public: std::string toString() const; - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string& identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -333,8 +333,8 @@ public: static const FactionID CONFLUX; static const FactionID NEUTRAL; - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string& identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -603,8 +603,8 @@ public: static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); }; class ArtifactPosition : public IdentifierWithEnum @@ -643,8 +643,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -683,8 +683,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -801,8 +801,8 @@ public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -855,8 +855,8 @@ class TerrainId : public IdentifierWithEnum public: using IdentifierWithEnum::IdentifierWithEnum; - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; From 909b06f7c15a98b5c5341f5cb0a388ff052facb7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 02:19:31 +0200 Subject: [PATCH 0516/1248] draw images --- client/mainmenu/CHighScoreScreen.cpp | 14 ++++++++---- config/highscoreCreatures.json | 34 ++++++++++++++-------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index d2874aee3..e7950ed13 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -42,7 +42,7 @@ CHighScoreScreen::CHighScoreScreen() Settings entry1 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; entry1->String() = "test"; Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["points"]; - entry2->Integer() = 100; + entry2->Integer() = std::rand() % 400 * 5; Settings entry3 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["player"]; entry3->String() = "test"; @@ -51,7 +51,7 @@ CHighScoreScreen::CHighScoreScreen() Settings entry5 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["days"]; entry5->Integer() = 123; Settings entry6 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["points"]; - entry6->Integer() = 100; + entry6->Integer() = std::rand() % 400; } } @@ -74,12 +74,10 @@ void CHighScoreScreen::addHighScores() background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::STANDARD ? "HISCORE" : "HISCORE2")); texts.clear(); + images.clear(); static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); auto creatures = configCreatures["creatures"].Vector(); - for(auto & creature : creatures) { - images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[CreatureID::decode(creature["creature"].String())]->getIconIndex(), 0, 10, 10)); - } // Header texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); @@ -118,6 +116,12 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["campaign"].String())); texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); } + + int divide = (highscorepage == HighScorePage::STANDARD) ? 1 : 5; + for(auto & creature : creatures) { + if(curData["points"].Integer() / divide <= creature["max"].Integer() && curData["points"].Integer() / divide >= creature["min"].Integer()) + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[CreatureID::decode(creature["creature"].String())]->getIconIndex(), 0, 670, y - 15 + i * 50)); + } } } diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json index 51392069b..455c20378 100644 --- a/config/highscoreCreatures.json +++ b/config/highscoreCreatures.json @@ -10,28 +10,28 @@ { "min" : 29, "max" : 32, "creature": "core:masterGremlin" }, { "min" : 33, "max" : 36, "creature": "core:hobgoblin" }, { "min" : 37, "max" : 40, "creature": "core:pikeman" }, - { "min" : 41, "max" : 44, "creature": "core:infernoTroglodyte" }, + { "min" : 41, "max" : 44, "creature": "core:infernalTroglodyte" }, { "min" : 45, "max" : 48, "creature": "core:skeletonWarrior" }, { "min" : 49, "max" : 52, "creature": "core:gnollMarauder" }, { "min" : 53, "max" : 56, "creature": "core:walkingDead" }, { "min" : 57, "max" : 60, "creature": "core:centaur" }, - { "min" : 61, "max" : 64, "creature": "core:herberdier" }, + { "min" : 61, "max" : 64, "creature": "core:halberdier" }, { "min" : 65, "max" : 68, "creature": "core:archer" }, { "min" : 69, "max" : 72, "creature": "core:lizardman" }, { "min" : 73, "max" : 76, "creature": "core:zombie" }, - { "min" : 77, "max" : 80, "creature": "core:wolfRider" }, - { "min" : 81, "max" : 84, "creature": "core:centaurCaptian" }, + { "min" : 77, "max" : 80, "creature": "core:goblinWolfRider" }, + { "min" : 81, "max" : 84, "creature": "core:centaurCaptain" }, { "min" : 85, "max" : 88, "creature": "core:dwarf" }, { "min" : 89, "max" : 92, "creature": "core:harpy" }, { "min" : 93, "max" : 96, "creature": "core:lizardWarrior" }, { "min" : 97, "max" : 100, "creature": "core:gog" }, { "min" : 101, "max" : 104, "creature": "core:stoneGargoyle" }, - { "min" : 105, "max" : 108, "creature": "core:masksman" }, + { "min" : 105, "max" : 108, "creature": "core:sharpshooter" }, { "min" : 109, "max" : 112, "creature": "core:orc" }, { "min" : 113, "max" : 116, "creature": "core:obsidianGargoyle" }, - { "min" : 117, "max" : 120, "creature": "core:wofRaider" }, + { "min" : 117, "max" : 120, "creature": "core:hobgoblinWolfRider" }, { "min" : 121, "max" : 124, "creature": "core:battleDwarf" }, - { "min" : 125, "max" : 128, "creature": "core:elf" }, + { "min" : 125, "max" : 128, "creature": "core:woodElf" }, { "min" : 129, "max" : 132, "creature": "core:harpyHag" }, { "min" : 133, "max" : 136, "creature": "core:magog" }, { "min" : 137, "max" : 140, "creature": "core:orcChieftain" }, @@ -51,8 +51,8 @@ { "min" : 191, "max" : 193, "creature": "core:evilEye" }, { "min" : 194, "max" : 196, "creature": "core:cerberus" }, { "min" : 197, "max" : 199, "creature": "core:ironGolem" }, - { "min" : 200, "max" : 202, "creature": "core:orge" }, - { "min" : 203, "max" : 205, "creature": "core:swordman" }, + { "min" : 200, "max" : 202, "creature": "core:ogre" }, + { "min" : 203, "max" : 205, "creature": "core:swordsman" }, { "min" : 206, "max" : 208, "creature": "core:demon" }, { "min" : 209, "max" : 211, "creature": "core:royalGriffin" }, { "min" : 212, "max" : 214, "creature": "core:hornedDemon" }, @@ -65,16 +65,16 @@ { "min" : 233, "max" : 235, "creature": "core:vampire" }, { "min" : 236, "max" : 238, "creature": "core:mage" }, { "min" : 239, "max" : 241, "creature": "core:medusaQueen" }, - { "min" : 242, "max" : 244, "creature": "core:crusider" }, + { "min" : 242, "max" : 244, "creature": "core:crusader" }, { "min" : 245, "max" : 247, "creature": "core:goldGolem" }, - { "min" : 248, "max" : 250, "creature": "core:orgeMage" }, + { "min" : 248, "max" : 250, "creature": "core:ogreMage" }, { "min" : 251, "max" : 253, "creature": "core:archMage" }, { "min" : 254, "max" : 256, "creature": "core:greaterBasilisk" }, { "min" : 257, "max" : 259, "creature": "core:zealot" }, { "min" : 260, "max" : 262, "creature": "core:pitFiend" }, { "min" : 263, "max" : 265, "creature": "core:diamondGolem" }, { "min" : 266, "max" : 268, "creature": "core:vampireLord" }, - { "min" : 269, "max" : 271, "creature": "core:dendroidSolider" }, + { "min" : 269, "max" : 271, "creature": "core:dendroidSoldier" }, { "min" : 272, "max" : 274, "creature": "core:minotaur" }, { "min" : 275, "max" : 277, "creature": "core:lich" }, { "min" : 278, "max" : 280, "creature": "core:genie" }, @@ -86,13 +86,13 @@ { "min" : 296, "max" : 298, "creature": "core:powerLich" }, { "min" : 299, "max" : 301, "creature": "core:thunderbird" }, { "min" : 302, "max" : 304, "creature": "core:pitLord" }, - { "min" : 305, "max" : 307, "creature": "core:cyclops" }, + { "min" : 305, "max" : 307, "creature": "core:cyclop" }, { "min" : 308, "max" : 310, "creature": "core:wyvern" }, - { "min" : 311, "max" : 313, "creature": "core:cyclopsKing" }, + { "min" : 311, "max" : 313, "creature": "core:cyclopKing" }, { "min" : 314, "max" : 316, "creature": "core:wyvernMonarch" }, { "min" : 317, "max" : 319, "creature": "core:manticore" }, { "min" : 320, "max" : 322, "creature": "core:scorpicore" }, - { "min" : 323, "max" : 325, "creature": "core:efreeti" }, + { "min" : 323, "max" : 325, "creature": "core:efreet" }, { "min" : 326, "max" : 328, "creature": "core:unicorn" }, { "min" : 329, "max" : 331, "creature": "core:efreetSultan" }, { "min" : 332, "max" : 334, "creature": "core:cavalier" }, @@ -109,7 +109,7 @@ { "min" : 365, "max" : 367, "creature": "core:ghostDragon" }, { "min" : 368, "max" : 370, "creature": "core:redDragon" }, { "min" : 371, "max" : 373, "creature": "core:greenDragon" }, - { "min" : 374, "max" : 376, "creature": "core:angle" }, + { "min" : 374, "max" : 376, "creature": "core:angel" }, { "min" : 377, "max" : 379, "creature": "core:devil" }, { "min" : 380, "max" : 382, "creature": "core:chaosHydra" }, { "min" : 383, "max" : 385, "creature": "core:ancientBehemoth" }, @@ -117,6 +117,6 @@ { "min" : 389, "max" : 391, "creature": "core:titan" }, { "min" : 392, "max" : 394, "creature": "core:goldDragon" }, { "min" : 395, "max" : 397, "creature": "core:blackDragon" }, - { "min" : 398, "max" : 400, "creature": "core:archAngle " } + { "min" : 398, "max" : 400, "creature": "core:archangel" } ] } \ No newline at end of file From fe0adfa4bd6c6fd2fda63d466ba7eb5213622c90 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 02:21:23 +0200 Subject: [PATCH 0517/1248] fix --- Mods/vcmi/config/vcmi/english.json | 633 +++++++++++++++-------------- 1 file changed, 334 insertions(+), 299 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 83e81dee6..5622649b3 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -1,339 +1,374 @@ { - "vcmi.adventureMap.monsterThreat.title" : "\n\nDiscussion : ", - "vcmi.adventureMap.monsterThreat.levels.0" : "Sans effort", - "vcmi.adventureMap.monsterThreat.levels.1" : "Très faible", - "vcmi.adventureMap.monsterThreat.levels.2" : "Faible", - "vcmi.adventureMap.monsterThreat.levels.3" : "Un peu plus faible", - "vcmi.adventureMap.monsterThreat.levels.4" : "Égal", - "vcmi.adventureMap.monsterThreat.levels.5" : "Un peu plus fort", - "vcmi.adventureMap.monsterThreat.levels.6" : "Fort", - "vcmi.adventureMap.monsterThreat.levels.7" : "Très fort", - "vcmi.adventureMap.monsterThreat.levels.8" : "Difficile", - "vcmi.adventureMap.monsterThreat.levels.9" : "Surpuissant", - "vcmi.adventureMap.monsterThreat.levels.10" : "Mortel", + "vcmi.adventureMap.monsterThreat.title" : "\n\nThreat: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Effortless", + "vcmi.adventureMap.monsterThreat.levels.1" : "Very Weak", + "vcmi.adventureMap.monsterThreat.levels.2" : "Weak", + "vcmi.adventureMap.monsterThreat.levels.3" : "A bit weaker", + "vcmi.adventureMap.monsterThreat.levels.4" : "Equal", + "vcmi.adventureMap.monsterThreat.levels.5" : "A bit stronger", + "vcmi.adventureMap.monsterThreat.levels.6" : "Strong", + "vcmi.adventureMap.monsterThreat.levels.7" : "Very Strong", + "vcmi.adventureMap.monsterThreat.levels.8" : "Challenging", + "vcmi.adventureMap.monsterThreat.levels.9" : "Overpowering", + "vcmi.adventureMap.monsterThreat.levels.10" : "Deadly", "vcmi.adventureMap.monsterThreat.levels.11" : "Impossible", - "vcmi.adventureMap.confirmRestartGame" : "Êtes-vous sûr de vouloir redémarrer le jeu ?", - "vcmi.adventureMap.noTownWithMarket" : "Il n'y a pas de marchés disponibles !", - "vcmi.adventureMap.noTownWithTavern" : "Il n'y a pas de villes disponibles avec des tavernes !", - "vcmi.adventureMap.spellUnknownProblem" : "Il y a un problème inconnu avec ce sort ! Pas plus d'informations sont disponibles.", - "vcmi.adventureMap.playerAttacked" : "Le joueur a été attaqué : %s", - "vcmi.adventureMap.moveCostDetails" : "Points de mouvement - Coût : %TURNS tours + %POINTS points, Points restants : %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Points de mouvement - Coût : %POINTS points, Points restants : %REMAINING", + "vcmi.adventureMap.confirmRestartGame" : "Are you sure you want to restart the game?", + "vcmi.adventureMap.noTownWithMarket" : "There are no available marketplaces!", + "vcmi.adventureMap.noTownWithTavern" : "There are no available towns with taverns!", + "vcmi.adventureMap.spellUnknownProblem" : "There is an unknown problem with this spell! No more information is available.", + "vcmi.adventureMap.playerAttacked" : "Player has been attacked: %s", + "vcmi.adventureMap.moveCostDetails" : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING", - "vcmi.capitalColors.0" : "Rouge", - "vcmi.capitalColors.1" : "Bleu", - "vcmi.capitalColors.2" : "Ocre", - "vcmi.capitalColors.3" : "Vert", + "vcmi.capitalColors.0" : "Red", + "vcmi.capitalColors.1" : "Blue", + "vcmi.capitalColors.2" : "Tan", + "vcmi.capitalColors.3" : "Green", "vcmi.capitalColors.4" : "Orange", - "vcmi.capitalColors.5" : "Violet", - "vcmi.capitalColors.6" : "Turquoise", - "vcmi.capitalColors.7" : "Rose", + "vcmi.capitalColors.5" : "Purple", + "vcmi.capitalColors.6" : "Teal", + "vcmi.capitalColors.7" : "Pink", + + "vcmi.heroOverview.startingArmy" : "Starting Units", + "vcmi.heroOverview.warMachine" : "War Machines", + "vcmi.heroOverview.secondarySkills" : "Secondary Skills", + "vcmi.heroOverview.spells" : "Spells", + + "vcmi.radialWheel.mergeSameUnit" : "Merge same creatures", + "vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures", + "vcmi.radialWheel.splitSingleUnit" : "Split off single creature", + "vcmi.radialWheel.splitUnitEqually" : "Split creatures equally", + "vcmi.radialWheel.moveUnit" : "Move creatures to another army", + "vcmi.radialWheel.splitUnit" : "Split creature to another slot", - "vcmi.mainMenu.serverConnecting" : "Connexion...", - "vcmi.mainMenu.serverAddressEnter" : "Entrez l'adresse :", - "vcmi.mainMenu.serverClosing" : "Fermeture...", - "vcmi.mainMenu.hostTCP" : "Hôte TCP/IP jeu", - "vcmi.mainMenu.joinTCP" : "Rejoindre TCP/IP jeu", - "vcmi.mainMenu.playerName" : "Joueur", + "vcmi.mainMenu.serverConnecting" : "Connecting...", + "vcmi.mainMenu.serverAddressEnter" : "Enter address:", + "vcmi.mainMenu.serverClosing" : "Closing...", + "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", + "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", + "vcmi.mainMenu.playerName" : "Player", + + "vcmi.lobby.filename" : "Filename", + "vcmi.lobby.creationDate" : "Creation date", - "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", - "vcmi.server.errors.modsIncompatibility" : "Les mods suivants sont nécessaires pour charger le jeu :", - "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", + "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", + "vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:", + "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", - "vcmi.settingsMainWindow.generalTab.hover" : "Général", - "vcmi.settingsMainWindow.generalTab.help" : "Passe à l'onglet Options générales, qui contient des paramètres liés au comportement général du client de jeu", - "vcmi.settingsMainWindow.battleTab.hover" : "Bataille", - "vcmi.settingsMainWindow.battleTab.help" : "Passe à l'onglet Options de combat, ce qui permet de configurer le comportement du jeu pendant les batailles", - "vcmi.settingsMainWindow.adventureTab.hover" : "Carte d'aventure", - "vcmi.settingsMainWindow.adventureTab.help" : "Passe à l'onglet Options de carte d'aventure (la carte d'aventure est la section du jeu où les joueurs peuvent contrôler les mouvements de leurs héros)", + "vcmi.settingsMainWindow.generalTab.hover" : "General", + "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", + "vcmi.settingsMainWindow.battleTab.hover" : "Battle", + "vcmi.settingsMainWindow.battleTab.help" : "Switches to Battle Options tab, which allows configuring game behavior during battles.", + "vcmi.settingsMainWindow.adventureTab.hover" : "Adventure Map", + "vcmi.settingsMainWindow.adventureTab.help" : "Switches to Adventure Map Options tab (adventure map is the section of the game where players can control the movements of their heroes).", - "vcmi.systemOptions.videoGroup" : "Paramètres Vidéo", - "vcmi.systemOptions.audioGroup" : "Paramètres Audio", - "vcmi.systemOptions.otherGroup" : "Autres Paramètres", // Unused right now - "vcmi.systemOptions.townsGroup" : "Écran de la Ville", + "vcmi.systemOptions.videoGroup" : "Video Settings", + "vcmi.systemOptions.audioGroup" : "Audio Settings", + "vcmi.systemOptions.otherGroup" : "Other Settings", // unused right now + "vcmi.systemOptions.townsGroup" : "Town Screen", - "vcmi.systemOptions.fullscreenBorderless.hover" : "Plein écran (sans bord)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nSi sélectionné, VCMI fonctionnera en mode plein écran sans bordure. Dans ce mode, le jeu utilisera toujours la même résolution de le bureau, ignorant la résolution sélectionnée.", - "vcmi.systemOptions.fullscreenExclusive.hover" : "Plein écran (exclusif)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{Fullscreen}\n\nSi sélectionné, VCMI s'exécutera en mode plein écran exclusif. Dans ce mode, le jeu modifiera la résolution du moniteur en résolution sélectionnée.", - "vcmi.systemOptions.resolutionButton.hover" : "Résolution : %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Select Resolution}\n\nChanger la résolution d'écran dans le jeu.", - "vcmi.systemOptions.resolutionMenu.hover" : "Sélectionner la résolution", - "vcmi.systemOptions.resolutionMenu.help" : "Changer la résolution d'écran dans le jeu.", - "vcmi.systemOptions.scalingButton.hover" : "Échelle d'interface : %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanger l'échelle de l'interface dans le jeu", - "vcmi.systemOptions.scalingMenu.hover" : "Sélectionner la mise à l'échelle de l'interface", - "vcmi.systemOptions.scalingMenu.help" : "Changer la mise à l'échelle de l'interface dans le jeu.", - "vcmi.systemOptions.longTouchButton.hover" : "Intervalle de touche long : %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nAu moment d'utiliser l'écran tactile, les fenêtres contextuelles apparaîtront après avoir touché l'écran pour une durée spécifiée, en millisecondes", - "vcmi.systemOptions.longTouchMenu.hover" : "Sélectionner l'intervalle de touche long", - "vcmi.systemOptions.longTouchMenu.help" : "Changer la durée de l'intervalle de touche long.", - "vcmi.systemOptions.longTouchMenu.entry" : "%d millisecondes", - "vcmi.systemOptions.framerateButton.hover" : "Afficher les FPS", - "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nAfficher/masquer le compteur de Frames Par Seconde dans le coin de la fenêtre du jeu", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Fullscreen (borderless)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Borderless Fullscreen}\n\nIf selected, VCMI will run in borderless fullscreen mode. In this mode, game will always use same resolution as desktop, ignoring selected resolution.", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Fullscreen (exclusive)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Fullscreen}\n\nIf selected, VCMI will run in exclusive fullscreen mode. In this mode, game will change resolution of monitor to selected resolution.", + "vcmi.systemOptions.resolutionButton.hover" : "Resolution: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Select Resolution}\n\nChange in-game screen resolution.", + "vcmi.systemOptions.resolutionMenu.hover" : "Select Resolution", + "vcmi.systemOptions.resolutionMenu.help" : "Change in-game screen resolution.", + "vcmi.systemOptions.scalingButton.hover" : "Interface Scaling: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Interface Scaling}\n\nChanges scaling of in-game interface.", + "vcmi.systemOptions.scalingMenu.hover" : "Select Interface Scaling", + "vcmi.systemOptions.scalingMenu.help" : "Change in-game interface scaling.", + "vcmi.systemOptions.longTouchButton.hover" : "Long Touch Interval: %d ms", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Long Touch Interval}\n\nWhen using touchscreen, popup windows will appear after touching screen for specified duration, in milliseconds.", + "vcmi.systemOptions.longTouchMenu.hover" : "Select Long Touch Interval", + "vcmi.systemOptions.longTouchMenu.help" : "Change duration of long touch interval.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d milliseconds", + "vcmi.systemOptions.framerateButton.hover" : "Show FPS", + "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", - "vcmi.adventureOptions.infoBarPick.hover" : "Afficher les messages dans le panneau d'information", - "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nAutant que possible, les messages de jeu de la visite des objets de la carte seront affichés dans le panneau d'informations, au lieu de faire apparaître dans une fenêtre séparée.", - "vcmi.adventureOptions.numericQuantities.hover" : "Dénombrement de créatures", - "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nAfficher les estimation de nombre de créatures ennemies au format numérique A-B.", - "vcmi.adventureOptions.forceMovementInfo.hover" : "Toujours afficher le coût de mouvement", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nToujours afficher les données des points de mouvement dans les informations sur la barre d'état (au lieu de les voir que pen maintenant la touche alt).", - "vcmi.adventureOptions.showGrid.hover" : "Afficher la grille", - "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nMontrer la superposition de la grille, mettant en évidence les frontières entre les carreaux de carte d'aventure.", - "vcmi.adventureOptions.borderScroll.hover" : "Défilement de bord", - "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nFait défiler la carte aventure lorsque le curseur est sur le bord de la fenêtre. Peut être désactivé en maintenant la touche Ctrl.", + "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", + "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", + "vcmi.adventureOptions.numericQuantities.hover" : "Numeric Creature Quantities", + "vcmi.adventureOptions.numericQuantities.help" : "{Numeric Creature Quantities}\n\nShow the approximate quantities of enemy creatures in the numeric A-B format.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Always Show Movement Cost", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information (Instead of viewing it only while you hold down ALT key).", + "vcmi.adventureOptions.showGrid.hover" : "Show Grid", + "vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nShow the grid overlay, highlighting the borders between adventure map tiles.", + "vcmi.adventureOptions.borderScroll.hover" : "Border Scrolling", + "vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Régler la vitesse de défilement de la carte sur très lent", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Régler la vitesse de défilement de la carte très rapide", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Régler la vitesse de défilement de la carte sur instantanément.", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Set the map scrolling speed to very slow.", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Set the map scrolling speed to very fast.", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Set the map scrolling speed to instantaneous.", - "vcmi.battleOptions.queueSizeLabel.hover": "Afficher l'ordre de fil de tour", - "vcmi.battleOptions.queueSizeNoneButton.hover": "Désactivé", + "vcmi.battleOptions.queueSizeLabel.hover": "Show Turn Order Queue", + "vcmi.battleOptions.queueSizeNoneButton.hover": "OFF", "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "PETITE", - "vcmi.battleOptions.queueSizeBigButton.hover": "GRANDE", - "vcmi.battleOptions.queueSizeNoneButton.help": "Ne pas afficher l'ordre de fil de tour", - "vcmi.battleOptions.queueSizeAutoButton.help": "Ajuster automatiquement la taille de l'ordre de fil de tour en fonction de la résolution du jeu (la PETITE taille est utilisée lorsque vous jouez au jeu sur une hauteur de résolution inférieure à 700 pixels, la GRANDE taille est utilisée au sinon)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Régler la taille de l'ordre de fil de tour sur PETITE", - "vcmi.battleOptions.queueSizeBigButton.help": "Régler la taille de l'ordre de fil de tour à GRANDE (non prise en charge si la hauteur de la résolution du jeu est inférieure à 700 pixels)", + "vcmi.battleOptions.queueSizeSmallButton.hover": "SMALL", + "vcmi.battleOptions.queueSizeBigButton.hover": "BIG", + "vcmi.battleOptions.queueSizeNoneButton.help": "Do not display Turn Order Queue.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Automatically adjust the size of the turn order queue based on the game's resolution(SMALL size is used when playing the game on a resolution with a height lower than 700 pixels, BIG size is used otherwise).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Sets turn order queue size to SMALL.", + "vcmi.battleOptions.queueSizeBigButton.help": "Sets turn order queue size to BIG (not supported if game resolution height is less than 700 pixels).", "vcmi.battleOptions.animationsSpeed1.hover": "", "vcmi.battleOptions.animationsSpeed5.hover": "", "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Régler la vitesse d'animation très lente", - "vcmi.battleOptions.animationsSpeed5.help": "Régler la vitesse d'animation très rapide", - "vcmi.battleOptions.animationsSpeed6.help": "Régler la vitesse d'animation sur instantané", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Mise en surbrillance du mouvement au survol", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nMettre en surbrillance la plage de mouvement de l'unité lorsque vous la survolez.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Afficher les limites de portée pour les tireurs", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nAfficher les limites de portée du tireur lorsque vous le survolez.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Ignorer la musique d'introduction", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAutoriser les actions pendant la musique d'intro qui joue au début de chaque bataille", - "vcmi.battleWindow.pressKeyToSkipIntro" : "Appuyez sur n'importe quelle touche pour commencer la bataille immédiatement", + "vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow.", + "vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast.", + "vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous.", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Show range limits for shooters", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Show range limits for shooters on Hover}\n\nShow shooter's range limits when you hover over it.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Show heroes statistics windows", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", - "vcmi.battleWindow.damageEstimation.melee" : "Attaque %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Attaque %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Tir sur %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Tir sur %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots" : "%d tirs restants", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d tir restant", - "vcmi.battleWindow.damageEstimation.damage" : "%d dégâts", - "vcmi.battleWindow.damageEstimation.damage.1" : "%d dégâts", - "vcmi.battleWindow.damageEstimation.kills" : "%d va mourir", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d va mourir", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", + "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Attack %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Shoot %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Shoot %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d shots left", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d shot left", + "vcmi.battleWindow.damageEstimation.damage" : "%d damage", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d damage", + "vcmi.battleWindow.damageEstimation.kills" : "%d will perish", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d will perish", - "vcmi.battleResultsWindow.applyResultsLabel" : "Appliquer le résultat de la bataille", + "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Afficher les créatures disponibles", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nAfficher le nombre de créatures disponibles au recrutement au lieu de leur croissance dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Afficher la croissance hebdomadaire des créatures", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\nAfficher la croissance hebdomadaire des créatures au lieu de la quantité disponible dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Infos compactes sur la créature", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\nAfficher des informations plus petites pour les créatures de la ville dans le résumé de la ville (coin inférieur gauche de l'écran de la ville).", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Show Weekly Growth of Creatures}\n\nShow creatures' weekly growth instead of available amount in town summary (bottom-left corner of town screen).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Compact Creature Info", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Compact Creature Info}\n\nShow smaller information for town creatures in town summary (bottom-left corner of town screen).", - "vcmi.townHall.missingBase" : "Le bâtiment de base %s doit être construit avant", - "vcmi.townHall.noCreaturesToRecruit" : "Il n'y a aucune créature à recruter !", - "vcmi.townHall.greetingManaVortex" : "Alors que vous approchez du %s, votre corps est rempli d'une nouvelle énergie. Vous avez doublé vos points de sort normaux.", - "vcmi.townHall.greetingKnowledge" : "Vous étudiez les glyphes sur le %s et découvrez le fonctionnement de diverses magies (+1 Connaissance).", - "vcmi.townHall.greetingSpellPower" : "Le %s vous apprend de nouvelles façons de concentrer vos pouvoirs magiques (+1 Pouvoir).", - "vcmi.townHall.greetingExperience" : "Une visite au %s vous apprend de nombreuses nouvelles compétences (+1000 Expérience).", - "vcmi.townHall.greetingAttack" : "Un peu de temps passé au %s vous permet d'apprendre des compétences de combat plus efficaces (+1 compétence d'attaque).", - "vcmi.townHall.greetingDefence" : "En passant du temps dans le %s, les guerriers expérimentés qui s'y trouvent vous enseignent des compétences défensives supplémentaires (+1 Défense).", - "vcmi.townHall.hasNotProduced" : "Le %s n'a encore rien produit.", - "vcmi.townHall.hasProduced" : "Le %s a produit %d %s cette semaine.", - "vcmi.townHall.greetingCustomBonus" : "%s vous offre +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " jusqu'à la prochaine bataille.", - "vcmi.townHall.greetingInTownMagicWell" : "%s a restauré vos points de sort au maximum.", + "vcmi.townHall.missingBase" : "Base building %s must be built first", + "vcmi.townHall.noCreaturesToRecruit" : "There are no creatures to recruit!", + "vcmi.townHall.greetingManaVortex" : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.", + "vcmi.townHall.greetingKnowledge" : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).", + "vcmi.townHall.greetingSpellPower" : "The %s teaches you new ways to focus your magical powers (+1 Power).", + "vcmi.townHall.greetingExperience" : "A visit to the %s teaches you many new skills (+1000 Experience).", + "vcmi.townHall.greetingAttack" : "Some time spent at the %s allows you to learn more effective combat skills (+1 Attack Skill).", + "vcmi.townHall.greetingDefence" : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).", + "vcmi.townHall.hasNotProduced" : "The %s has not produced anything yet.", + "vcmi.townHall.hasProduced" : "The %s produced %d %s this week.", + "vcmi.townHall.greetingCustomBonus" : "%s gives you +%d %s%s", + "vcmi.townHall.greetingCustomUntil" : " until next battle.", + "vcmi.townHall.greetingInTownMagicWell" : "%s has restored your spell points to maximum.", - "vcmi.logicalExpressions.anyOf" : "L'un des éléments suivants :", - "vcmi.logicalExpressions.allOf" : "Tous les éléments suivants :", - "vcmi.logicalExpressions.noneOf" : "Aucun des éléments suivants :", + "vcmi.logicalExpressions.anyOf" : "Any of the following:", + "vcmi.logicalExpressions.allOf" : "All of the following:", + "vcmi.logicalExpressions.noneOf" : "None of the following:", - "vcmi.heroWindow.openCommander.hover" : "Ouvrir la fenêtre d'informations du commandant", - "vcmi.heroWindow.openCommander.help" : "Affiche des détails sur le commandant de ce héros", + "vcmi.heroWindow.openCommander.hover" : "Open commander info window", + "vcmi.heroWindow.openCommander.help" : "Shows details about the commander of this hero.", + "vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window", + "vcmi.heroWindow.openBackpack.help" : "Opens window that allows easier artifact backpack management.", - "vcmi.commanderWindow.artifactMessage" : "Voulez-vous rendre cet artefact au héros ?", + "vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?", - "vcmi.creatureWindow.showBonuses.hover" : "Passer en vue bonus", - "vcmi.creatureWindow.showBonuses.help" : "Afficher tous les bonus actifs du commandant", - "vcmi.creatureWindow.showSkills.hover" : "Passer à la vue des compétences", - "vcmi.creatureWindow.showSkills.help" : "Afficher toutes les compétences apprises du commandant", - "vcmi.creatureWindow.returnArtifact.hover" : "Remettre l'artefact", - "vcmi.creatureWindow.returnArtifact.help" : "Cliquez sur ce bouton pour remettre l'artefact dans le sac à dos du héros", + "vcmi.creatureWindow.showBonuses.hover" : "Switch to bonuses view", + "vcmi.creatureWindow.showBonuses.help" : "Display all active bonuses of the commander.", + "vcmi.creatureWindow.showSkills.hover" : "Switch to skills view", + "vcmi.creatureWindow.showSkills.help" : "Display all learned skills of the commander.", + "vcmi.creatureWindow.returnArtifact.hover" : "Return artifact", + "vcmi.creatureWindow.returnArtifact.help" : "Click this button to return the artifact to the hero's backpack.", - "vcmi.questLog.hideComplete.hover" : "Masquer les quêtes terminées", - "vcmi.questLog.hideComplete.help" : "Masquer toutes les quêtes terminées", + "vcmi.questLog.hideComplete.hover" : "Hide complete quests", + "vcmi.questLog.hideComplete.help" : "Hide all completed quests.", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Random)", + "vcmi.randomMapTab.widgets.templateLabel" : "Template", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", + + "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", + "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", + "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", + "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", + "vcmi.optionsTab.widgets.labelTimer" : "Timer", + "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", + "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", - "vcmi.randomMapTab.widgets.templateLabel" : "Modèle", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuration...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alignements d'équipe", - "vcmi.randomMapTab.widgets.roadTypesLabel" : "Types de routes", // Custom victory conditions for H3 campaigns and HotA maps - "vcmi.map.victoryCondition.daysPassed.toOthers" : "L'ennemi a réussi à survivre jusqu'à ce jour. La victoire est à eux !", - "vcmi.map.victoryCondition.daysPassed.toSelf" : "Félicitations ! Vous avez réussi à survivre. La victoire est à vous !", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "L'ennemi a vaincu tous les monstres qui sévissent sur cette terre et revendique la victoire !", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Félicitations ! Vous avez vaincu tous les monstres qui affligent ce pays et vous pouvez revendiquer la victoire !", - "vcmi.map.victoryCondition.collectArtifacts.message" : "Acquérir trois artefacts", - "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Félicitations ! Tous vos ennemis ont été vaincus et vous avez l'alliance angélique ! La victoire est à vous !", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Vaincre tous les ennemis et créer une alliance angélique", + "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulations! You have managed to survive. Victory is yours!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "The enemy has defeated all of the monsters plaguing this land and claims victory!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Congratulations! You have defeated all of the monsters plaguing this land and can claim victory!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Acquire Three Artifacts", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulations! All your enemies have been defeated and you have Angelic Alliance! Victory is yours!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Defeat All Enemies and create Angelic Alliance", // few strings from WoG used by vcmi - "vcmi.stackExperience.description" : "» D é t a i l s d e P i l e d ' E x p é r i e n c e «\n\nType de créature ................ : %s\nRang d'expérience ............... : %s (%i)\nPoints d'expérience ............. : %i\nPoints d'XP au rang suivant ..... : %i\nExpérience maximum par bataille . : %i%% (%i)\nNb de créatures dans la pile .... : %i\nMaximum de nouvelles recrues\n sans perdre le rang actuel ..... : %i\nMultiplicateur d'expérience ..... : %.2f\nMultiplicateur d'amélioration ... : %.2f\nExpérience après le rang 10 ..... : %i\nMaximum de Nouvelles Recrues pour\n rester au Rang 10 si\n l'Expérience Maximum ........... : %i", - "vcmi.stackExperience.rank.0" : "Basique", + "vcmi.stackExperience.description" : "» S t a c k E x p e r i e n c e D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i", + "vcmi.stackExperience.rank.0" : "Basic", "vcmi.stackExperience.rank.1" : "Novice", - "vcmi.stackExperience.rank.2" : "Formé", - "vcmi.stackExperience.rank.3" : "Qualifié", - "vcmi.stackExperience.rank.4" : "Éprouvé", - "vcmi.stackExperience.rank.5" : "Vétéran", - "vcmi.stackExperience.rank.6" : "Adepte", + "vcmi.stackExperience.rank.2" : "Trained", + "vcmi.stackExperience.rank.3" : "Skilled", + "vcmi.stackExperience.rank.4" : "Proven", + "vcmi.stackExperience.rank.5" : "Veteran", + "vcmi.stackExperience.rank.6" : "Adept", "vcmi.stackExperience.rank.7" : "Expert", - "vcmi.stackExperience.rank.8" : "Élite", - "vcmi.stackExperience.rank.9" : "Maître", - "vcmi.stackExperience.rank.10" : "As", + "vcmi.stackExperience.rank.8" : "Elite", + "vcmi.stackExperience.rank.9" : "Master", + "vcmi.stackExperience.rank.10" : "Ace", - "core.bonus.ADDITIONAL_ATTACK.name": "Double frappe", - "core.bonus.ADDITIONAL_ATTACK.description": "Attaque deux fois", - "core.bonus.ADDITIONAL_RETALIATION.name": "Représailles supplémentaires", - "core.bonus.ADDITIONAL_RETALIATION.description": "Peut riposter ${val} fois de plus", - "core.bonus.AIR_IMMUNITY.name": "Immunité aérienne", - "core.bonus.AIR_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de l'Air", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Attaque tout autour", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Attaque tous les ennemis adjacents", - "core.bonus.BLOCKS_RETALIATION.name": "Pas de représailles", - "core.bonus.BLOCKS_RETALIATION.description": "L'ennemi ne peut pas riposter", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Pas de représailles à distance", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "L'ennemi ne peut pas riposter en utilisant une attaque à distance", - "core.bonus.CATAPULT.name": "Catapulte", - "core.bonus.CATAPULT.description": "Attaque les murs de siège", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Réduire le coût de lancement (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Réduit le coût d'incantation du héros de ${val}", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Amortisseur magique (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Augmente le coût d'incantation des sorts ennemis de ${val}", - "core.bonus.CHARGE_IMMUNITY.name": "Immunisé contre la charge", - "core.bonus.CHARGE_IMMUNITY.description": "Immunisé contre la charge du cavalier et du champion", - "core.bonus.DARKNESS.name": "Couverture de ténèbres", - "core.bonus.DARKNESS.description": "Crée un linceul de ténèbres avec un rayon de ${val}", - "core.bonus.DEATH_STARE.name": "Regard mortel (${val}%)", - "core.bonus.DEATH_STARE.description": "A ${val}% de chances de tuer une seule créature", - "core.bonus.DEFENSIVE_STANCE.name": "Bonus de défense", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Défense en défense", + "core.bonus.ADDITIONAL_ATTACK.name": "Double Strike", + "core.bonus.ADDITIONAL_ATTACK.description": "Attacks twice", + "core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations", + "core.bonus.ADDITIONAL_RETALIATION.description": "May retaliate ${val} extra times", + "core.bonus.AIR_IMMUNITY.name": "Air immunity", + "core.bonus.AIR_IMMUNITY.description": "Immune to all spells from the school of Air magic", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Attack all around", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Attacks all adjacent enemies", + "core.bonus.BLOCKS_RETALIATION.name": "No retaliation", + "core.bonus.BLOCKS_RETALIATION.description": "Enemy cannot retaliate", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "No ranged retaliation", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Enemy cannot retaliate by using a ranged attack", + "core.bonus.CATAPULT.name": "Catapult", + "core.bonus.CATAPULT.description": "Attacks siege walls", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduce Casting Cost (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduces the spellcasting cost for the hero by ${val}", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Magic Damper (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Increases spellcasting cost of enemy spells by ${val}", + "core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", + "core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge", + "core.bonus.DARKNESS.name": "Darkness cover", + "core.bonus.DARKNESS.description": "Creates a shroud of darkness with a ${val} radius", + "core.bonus.DEATH_STARE.name": "Death Stare (${val}%)", + "core.bonus.DEATH_STARE.description": "Has a ${val}% chance to kill a single creature", + "core.bonus.DEFENSIVE_STANCE.name": "Defense Bonus", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Defense when defending", "core.bonus.DESTRUCTION.name": "Destruction", - "core.bonus.DESTRUCTION.description": "A ${val} % de chances de tuer des unités supplémentaires après l'attaque", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Coup mortel", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "A ${val}% de chances d'infliger des dégâts de base doublés en attaquant", + "core.bonus.DESTRUCTION.description": "Has ${val}% chance to kill extra units after attack", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Death Blow", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking", "core.bonus.DRAGON_NATURE.name": "Dragon", - "core.bonus.DRAGON_NATURE.description": "La créature a une nature de dragon", - "core.bonus.EARTH_IMMUNITY.name": "Immunité terrestre", - "core.bonus.EARTH_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de la Terre", - "core.bonus.ENCHANTER.name": "Enchanteur", - "core.bonus.ENCHANTER.description": "Peut lancer en masse ${subtype.spell} à chaque tour", - "core.bonus.ENCHANTED.name": "Enchanté", - "core.bonus.ENCHANTED.description": "Affecté par ${subtype.spell} permanent", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignorer la défense (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Lors de l'attaque, ${val}% de la défense du défenseur est ignorée", - "core.bonus.FIRE_IMMUNITY.name": "Immunité au feu", - "core.bonus.FIRE_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie du Feu", - "core.bonus.FIRE_SHIELD.name": "Bouclier de feu (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Reflète une partie des dégâts de mêlée", - "core.bonus.FIRST_STRIKE.name": "Premier coup", - "core.bonus.FIRST_STRIKE.description": "Cette créature riposte avant d'être attaquée", - "core.bonus.FEAR.name": "Peur", - "core.bonus.FEAR.description": "Provoque la peur sur une pile ennemie", - "core.bonus.FEARLESS.name": "Intrépide", - "core.bonus.FEARLESS.description": "Immunité à la peur", - "core.bonus.FLYING.name": "Vol", - "core.bonus.FLYING.description": "Vole en se déplaçant (ignore les obstacles)", - "core.bonus.FREE_SHOOTING.name": "Tirer de près", - "core.bonus.FREE_SHOOTING.description": "Peut utiliser des attaques à distance au corps à corps", - "core.bonus.GARGOYLE.name": "Gargouille", - "core.bonus.GARGOYLE.description": "Ne peut pas être réanimé ou soigné", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Réduit les dégâts (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Réduit les dégâts physiques des attaques à distance ou de mêlée", - "core.bonus.HATE.name": "Déteste ${subtype.creature}", - "core.bonus.HATE.description": "Inflige ${val} % de dégâts supplémentaires à ${subtype.creature}", - "core.bonus.HEALER.name": "Guérisseur", - "core.bonus.HEALER.description": "Soigne les unités alliées", - "core.bonus.HP_REGENERATION.name": "Régénération", - "core.bonus.HP_REGENERATION.description": "Soigne ${val} points de vie à chaque tour", - "core.bonus.JOUSTING.name": "Charge de champion", - "core.bonus.JOUSTING.description": "+${val}% de dégâts pour chaque hexagone parcouru", - "core.bonus.KING.name": "Roi", - "core.bonus.KING.description": "Vulnérable au niveau POURFENDEUR ${val} ou supérieur", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Immunité aux sorts 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immunisé aux sorts de niveaux 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Portée de tir limitée", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Impossible de cibler des unités plus loin que ${val} hexagones", - "core.bonus.LIFE_DRAIN.name": "Durée de vie du drain (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Draine ${val}% des dégâts infligés", - "core.bonus.MANA_CHANNELING.name": "Chaîne magique ${val}%", - "core.bonus.MANA_CHANNELING.description": "Donne à votre héros ${val}% du mana dépensé par l'ennemi", - "core.bonus.MANA_DRAIN.name": "Drain de mana", - "core.bonus.MANA_DRAIN.description": "Draine ${val} mana à chaque tour", - "core.bonus.MAGIC_MIRROR.name": "Miroir magique (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "A ${val} % de chances de rediriger un sort offensif vers une unité ennemie", - "core.bonus.MAGIC_RESISTANCE.name": "Résistance magique (${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "A ${val}% de chances de résister à un sort ennemi", - "core.bonus.MIND_IMMUNITY.name": "Immunité contre les sorts de l'esprit", - "core.bonus.MIND_IMMUNITY.description": "Immunisé contre les sorts de type Mental", - "core.bonus.NO_DISTANCE_PENALTY.name": "Aucune pénalité de distance", - "core.bonus.NO_DISTANCE_PENALTY.description": "Inflige des dégâts complets à n'importe quelle distance", - "core.bonus.NO_MELEE_PENALTY.name": "Aucune pénalité de mêlée", - "core.bonus.NO_MELEE_PENALTY.description": "La créature n'a pas de pénalité de mêlée", - "core.bonus.NO_MORALE.name": "Moral neutre", - "core.bonus.NO_MORALE.description": "La créature est immunisée contre les effets de moral", - "core.bonus.NO_WALL_PENALTY.name": "Aucune pénalité de mur", - "core.bonus.NO_WALL_PENALTY.description": "Dégâts complets pendant le siège", - "core.bonus.NON_LIVING.name": "Non vivant", - "core.bonus.NON_LIVING.description": "Immunité à de nombreux effets", - "core.bonus.RANDOM_SPELLCASTER.name": "Lanceur de sorts aléatoire", - "core.bonus.RANDOM_SPELLCASTER.description": "Peut lancer un sort aléatoire", - "core.bonus.RANGED_RETALIATION.name": "Représailles à distance", - "core.bonus.RANGED_RETALIATION.description": "Peut effectuer une contre-attaque à distance", - "core.bonus.RECEPTIVE.name": "Réceptif", - "core.bonus.RECEPTIVE.description": "Pas d'immunité aux sorts amicaux", - "core.bonus.REBIRTH.name": "Renaissance (${val}%)", - "core.bonus.REBIRTH.description": "${val}% de la pile augmentera après la mort", - "core.bonus.RETURN_AFTER_STRIKE.name": "Attaque et retour", - "core.bonus.RETURN_AFTER_STRIKE.description": "Revient après une attaque au corps à corps", - "core.bonus.SHOOTER.name": "Distance", - "core.bonus.SHOOTER.description": "La créature peut tirer", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Tirer tout autour", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Les attaques à distance de cette créature touchent toutes les cibles dans une petite zone", - "core.bonus.SOUL_STEAL.name": "Vol d'âme", - "core.bonus.SOUL_STEAL.description": "Gagne ${val} nouvelles créatures pour chaque ennemi tué", - "core.bonus.SPELLCASTER.name": "Lanceur de sorts", - "core.bonus.SPELLCASTER.description": "Peut lancer ${subtype.spell}", - "core.bonus.SPELL_AFTER_ATTACK.name": "Lancer après l'attaque", - "core.bonus.SPELL_AFTER_ATTACK.description": "A ${val}% de chances de lancer ${subtype.spell} après avoir attaqué", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Lancer avant l'attaque", - "core.bonus.SPELL_BEFORE_ATTACK.description": "A ${val}% de chances de lancer ${subtype.spell} avant qu'il n'attaque", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Résistance aux sorts", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Dégâts des sorts réduits de ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Immunité aux sorts", - "core.bonus.SPELL_IMMUNITY.description": "Immunisé contre ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "Attaque semblable à un sort", - "core.bonus.SPELL_LIKE_ATTACK.description": "Attaque avec ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura de résistance", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Les piles à proximité obtiennent ${val}% de résistance magique", - "core.bonus.SUMMON_GUARDIANS.name": "Invoquer des gardiens", - "core.bonus.SUMMON_GUARDIANS.description": "Au début de la bataille, invoque ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergique", - "core.bonus.SYNERGY_TARGET.description": "Cette créature est vulnérable à l'effet de synergie", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Souffle", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Attaque de souffle (portée de 2 hexagones)", - "core.bonus.THREE_HEADED_ATTACK.name": "Attaque à trois têtes", - "core.bonus.THREE_HEADED_ATTACK.description": "Attaque trois unités adjacentes", + "core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature", + "core.bonus.EARTH_IMMUNITY.name": "Earth immunity", + "core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic", + "core.bonus.ENCHANTER.name": "Enchanter", + "core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn", + "core.bonus.ENCHANTED.name": "Enchanted", + "core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored", + "core.bonus.FIRE_IMMUNITY.name": "Fire immunity", + "core.bonus.FIRE_IMMUNITY.description": "Immune to all spells from the school of Fire magic", + "core.bonus.FIRE_SHIELD.name": "Fire Shield (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Reflects part of melee damage", + "core.bonus.FIRST_STRIKE.name": "First Strike", + "core.bonus.FIRST_STRIKE.description": "This creature retaliates before being attacked", + "core.bonus.FEAR.name": "Fear", + "core.bonus.FEAR.description": "Causes Fear on an enemy stack", + "core.bonus.FEARLESS.name": "Fearless", + "core.bonus.FEARLESS.description": "Immune to Fear ability", + "core.bonus.FLYING.name": "Fly", + "core.bonus.FLYING.description": "Flies when moving (ignores obstacles)", + "core.bonus.FREE_SHOOTING.name": "Shoot Close", + "core.bonus.FREE_SHOOTING.description": "Can use ranged attacks at melee range", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Cannot be raised or healed", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduces physical damage from ranged or melee attacks", + "core.bonus.HATE.name": "Hates ${subtype.creature}", + "core.bonus.HATE.description": "Does ${val}% more damage to ${subtype.creature}", + "core.bonus.HEALER.name": "Healer", + "core.bonus.HEALER.description": "Heals allied units", + "core.bonus.HP_REGENERATION.name": "Regeneration", + "core.bonus.HP_REGENERATION.description": "Heals ${val} hit points every round", + "core.bonus.JOUSTING.name": "Champion charge", + "core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled", + "core.bonus.KING.name": "King", + "core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Unable to target units farther than ${val} hexes", + "core.bonus.LIFE_DRAIN.name": "Drain life (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Drains ${val}% of damage dealt", + "core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", + "core.bonus.MANA_CHANNELING.description": "Gives your hero ${val}% of the mana spent by the enemy", + "core.bonus.MANA_DRAIN.name": "Mana Drain", + "core.bonus.MANA_DRAIN.description": "Drains ${val} mana every turn", + "core.bonus.MAGIC_MIRROR.name": "Magic Mirror (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "Has a ${val}% chance to redirect an offensive spell to an enemy unit", + "core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "Has a ${val}% chance to resist an enemy spell", + "core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity", + "core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells", + "core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty", + "core.bonus.NO_DISTANCE_PENALTY.description": "Does full damage at any distance", + "core.bonus.NO_MELEE_PENALTY.name": "No melee penalty", + "core.bonus.NO_MELEE_PENALTY.description": "Creature has no Melee Penalty", + "core.bonus.NO_MORALE.name": "Neutral Morale", + "core.bonus.NO_MORALE.description": "Creature is immune to morale effects", + "core.bonus.NO_WALL_PENALTY.name": "No wall penalty", + "core.bonus.NO_WALL_PENALTY.description": "Full damage during siege", + "core.bonus.NON_LIVING.name": "Non living", + "core.bonus.NON_LIVING.description": "Immunity to many effects", + "core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster", + "core.bonus.RANDOM_SPELLCASTER.description": "Can cast random spell", + "core.bonus.RANGED_RETALIATION.name": "Ranged retaliation", + "core.bonus.RANGED_RETALIATION.description": "Can perform ranged counterattack", + "core.bonus.RECEPTIVE.name": "Receptive", + "core.bonus.RECEPTIVE.description": "No Immunity to Friendly Spells", + "core.bonus.REBIRTH.name": "Rebirth (${val}%)", + "core.bonus.REBIRTH.description": "${val}% of stack will rise after death", + "core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return", + "core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack", + "core.bonus.SHOOTER.name": "Ranged", + "core.bonus.SHOOTER.description": "Creature can shoot", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "This creature's ranged attacks strike all targets in a small area", + "core.bonus.SOUL_STEAL.name": "Soul Steal", + "core.bonus.SOUL_STEAL.description": "Gains ${val} new creatures for each enemy killed", + "core.bonus.SPELLCASTER.name": "Spellcaster", + "core.bonus.SPELLCASTER.description": "Can cast ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Cast After Attack", + "core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack", + "core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from spells reduced by ${val}%.", + "core.bonus.SPELL_IMMUNITY.name": "Spell immunity", + "core.bonus.SPELL_IMMUNITY.description": "Immune to ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack", + "core.bonus.SPELL_LIKE_ATTACK.description": "Attacks with ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura of Resistance", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Nearby stacks get ${val}% magic resistance", + "core.bonus.SUMMON_GUARDIANS.name": "Summon guardians", + "core.bonus.SUMMON_GUARDIANS.description": "At the start of battle summons ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Synergizable", + "core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Breath Attack (2-hex range)", + "core.bonus.THREE_HEADED_ATTACK.name": "Three-headed attack", + "core.bonus.THREE_HEADED_ATTACK.description": "Attacks three adjacent units", "core.bonus.TRANSMUTATION.name": "Transmutation", - "core.bonus.TRANSMUTATION.description": "${val}% de chances de transformer l'unité attaquée en un type différent", - "core.bonus.UNDEAD.name": "Morts-vivants", - "core.bonus.UNDEAD.description": "La créature est un mort-vivant", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Représailles illimitées", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Peut riposter contre un nombre illimité d'attaques", - "core.bonus.WATER_IMMUNITY.name": "Immunité à l'eau", - "core.bonus.WATER_IMMUNITY.description": "Immunisé contre tous les sorts de l'école de magie de l'Eau", - "core.bonus.WIDE_BREATH.name": "Large souffle", - "core.bonus.WIDE_BREATH.description": "Attaque à souffle large (plusieurs hexagones)" + "core.bonus.TRANSMUTATION.description": "${val}% chance to transform attacked unit to a different type", + "core.bonus.UNDEAD.name": "Undead", + "core.bonus.UNDEAD.description": "Creature is Undead", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Unlimited retaliations", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Can retaliate against an unlimited number of attacks", + "core.bonus.WATER_IMMUNITY.name": "Water immunity", + "core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic", + "core.bonus.WIDE_BREATH.name": "Wide breath", + "core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)" } From 2a2f0a657e9120a56fa7d9ad99c9e5f1f7a64d0a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 22 Sep 2023 13:34:00 +0200 Subject: [PATCH 0518/1248] Fix battle bonuses serialization --- lib/CPlayerState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index faa021bbe..b521b5d13 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -39,6 +39,7 @@ PlayerState::PlayerState(PlayerState && other) noexcept: std::swap(towns, other.towns); std::swap(dwellings, other.dwellings); std::swap(quests, other.quests); + std::swap(battleBonuses, other.battleBonuses); } PlayerState::~PlayerState() = default; From 5b24fe289bc577feeabd5178b69636466959eb13 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 22 Sep 2023 14:46:43 +0200 Subject: [PATCH 0519/1248] Assembling difficulty json instead of overriding --- lib/gameState/CGameState.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 45551e296..f5657821c 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -661,7 +661,7 @@ void CGameState::initGlobalBonuses() void CGameState::initDifficulty() { logGlobal->debug("\tLoading difficulty settings"); - const JsonNode config(JsonPath::builtin("config/difficulty.json")); + const JsonNode config = JsonUtils::assembleFromFiles("config/difficulty.json"); const JsonNode & difficultyAI(config["ai"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); const JsonNode & difficultyHuman(config["human"][GameConstants::DIFFICULTY_NAMES[scenarioOps->difficulty]]); From a4cb74f0dc0801524b10a70c96a7570cba5fdad8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 20:39:20 +0200 Subject: [PATCH 0520/1248] highscore input window --- client/CPlayerInterface.cpp | 5 +- client/CServerHandler.cpp | 4 + client/mainmenu/CHighScoreScreen.cpp | 111 +++++++++++-- client/mainmenu/CHighScoreScreen.h | 38 ++++- config/highscoreCreatures.json | 236 +++++++++++++-------------- 5 files changed, 262 insertions(+), 132 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 135bd1f81..904ba2e14 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -37,6 +37,7 @@ #include "gui/WindowHandler.h" #include "mainmenu/CMainMenu.h" +#include "mainmenu/CHighScoreScreen.h" #include "mapView/mapHandler.h" @@ -1690,11 +1691,13 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) else { GH.dispatchMainThread( - []() + [won]() { CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("main"); + if(won) + GH.windows().createAndPushWindow(); } ); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 291fe5b70..a627af56d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -22,6 +22,7 @@ #include "mainmenu/CMainMenu.h" #include "mainmenu/CPrologEpilogVideo.h" +#include "mainmenu/CHighScoreScreen.h" #ifdef VCMI_ANDROID #include "../lib/CAndroidVMHelper.h" @@ -695,7 +696,10 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) if(!ourCampaign->isCampaignFinished()) CMM->openCampaignLobby(ourCampaign); else + { CMM->openCampaignScreen(ourCampaign->campaignSet); + GH.windows().createAndPushWindow(); + } }; if(epilogue.hasPrologEpilog) { diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index e7950ed13..90b06f113 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -12,6 +12,8 @@ #include "CHighScoreScreen.h" #include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../gui/Shortcut.h" #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" @@ -19,6 +21,7 @@ #include "../render/Canvas.h" #include "../CGameInfo.h" +#include "../CVideoHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" @@ -44,13 +47,13 @@ CHighScoreScreen::CHighScoreScreen() Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["points"]; entry2->Integer() = std::rand() % 400 * 5; - Settings entry3 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["player"]; + Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["player"]; entry3->String() = "test"; - Settings entry4 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["land"]; + Settings entry4 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; entry4->String() = "test"; - Settings entry5 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["days"]; + Settings entry5 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["days"]; entry5->Integer() = 123; - Settings entry6 = persistentStorage.write["highscore"]["standard"][std::to_string(i)]["points"]; + Settings entry6 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["points"]; entry6->Integer() = std::rand() % 400; } } @@ -71,7 +74,7 @@ void CHighScoreScreen::addHighScores() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::STANDARD ? "HISCORE" : "HISCORE2")); + background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); texts.clear(); images.clear(); @@ -83,7 +86,7 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); - if(highscorepage == HighScorePage::STANDARD) + if(highscorepage == HighScorePage::SCENARIO) { texts.push_back(std::make_shared(400, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); texts.push_back(std::make_shared(555, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); @@ -97,15 +100,15 @@ void CHighScoreScreen::addHighScores() // Content int y = 65; - auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::STANDARD ? "standard" : "campaign"]; + auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; for (int i = 0; i < 11; i++) { auto & curData = data[std::to_string(i)]; - texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(i))); + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(i+1))); texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["player"].String())); - if(highscorepage == HighScorePage::STANDARD) + if(highscorepage == HighScorePage::SCENARIO) { texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["land"].String())); texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["days"].Integer()))); @@ -117,7 +120,7 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); } - int divide = (highscorepage == HighScorePage::STANDARD) ? 1 : 5; + int divide = (highscorepage == HighScorePage::SCENARIO) ? 1 : 5; for(auto & creature : creatures) { if(curData["points"].Integer() / divide <= creature["max"].Integer() && curData["points"].Integer() / divide >= creature["min"].Integer()) images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[CreatureID::decode(creature["creature"].String())]->getIconIndex(), 0, 670, y - 15 + i * 50)); @@ -136,7 +139,7 @@ void CHighScoreScreen::buttonCampaginClick() void CHighScoreScreen::buttonStandardClick() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - highscorepage = HighScorePage::STANDARD; + highscorepage = HighScorePage::SCENARIO; addHighScores(); addButtons(); redraw(); @@ -163,3 +166,89 @@ void CHighScoreScreen::buttonExitClick() { close(); } + +CHighScoreInputScreen::CHighScoreInputScreen() + : CWindowObject(BORDERED) +{ + addUsedEvents(LCLICK); + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos = center(Rect(0, 0, 800, 600)); + updateShadow(); + + int border = 100; + int textareaW = ((pos.w - 2 * border) / 4); + std::vector t = { "438", "439", "440", "441", "676" }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); +} + +void CHighScoreInputScreen::addEntry(std::string text) { + +} + +void CHighScoreInputScreen::show(Canvas & to) +{ + CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); + redraw(); + + CIntObject::show(to); +} + +void CHighScoreInputScreen::activate() +{ + CCS->videoh->open(VideoPath::builtin("HSLOOP.SMK")); + CIntObject::activate(); +} + +void CHighScoreInputScreen::deactivate() +{ + CCS->videoh->close(); + CIntObject::deactivate(); +} + +void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(!input) + { + input = std::make_shared( + [&] (std::string text) + { + if(!text.empty()) + { + addEntry(text); + } + close(); + }); + } +} + +CHighScoreInput::CHighScoreInput(std::function readyCB) + : CWindowObject(0), ready(readyCB) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = center(Rect(0, 0, 232, 212)); + updateShadow(); + + background = std::make_shared(ImagePath::builtin("HIGHNAME")); + text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); + + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput->setText(settings["general"]["playerName"].String()); +} + +void CHighScoreInput::okay() +{ + ready(textInput->getText()); +} + +void CHighScoreInput::abort() +{ + ready(""); +} \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 418b1a43c..6c01c35e9 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -12,11 +12,13 @@ class CButton; class CLabel; +class CMultiLineLabel; class CAnimImage; +class CTextInput; class CHighScoreScreen : public CWindowObject { - enum HighScorePage { STANDARD, CAMPAIGN }; + enum HighScorePage { SCENARIO, CAMPAIGN }; void addButtons(); void addHighScores(); @@ -26,7 +28,7 @@ class CHighScoreScreen : public CWindowObject void buttonResetClick(); void buttonExitClick(); - HighScorePage highscorepage = HighScorePage::STANDARD; + HighScorePage highscorepage = HighScorePage::SCENARIO; std::shared_ptr background; std::vector> buttons; @@ -35,3 +37,35 @@ class CHighScoreScreen : public CWindowObject public: CHighScoreScreen(); }; + +class CHighScoreInput : public CWindowObject +{ + std::shared_ptr background; + std::shared_ptr text; + std::shared_ptr buttonOk; + std::shared_ptr buttonCancel; + std::shared_ptr statusBar; + std::shared_ptr textInput; + + std::function ready; + + void okay(); + void abort(); +public: + CHighScoreInput(std::function readyCB); +}; + +class CHighScoreInputScreen : public CWindowObject +{ + std::vector> texts; + std::shared_ptr input; +public: + CHighScoreInputScreen(); + + void addEntry(std::string text); + + void show(Canvas & to) override; + void activate() override; + void deactivate() override; + void clickPressed(const Point & cursorPosition) override; +}; \ No newline at end of file diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json index 455c20378..9f6dcd2ad 100644 --- a/config/highscoreCreatures.json +++ b/config/highscoreCreatures.json @@ -1,122 +1,122 @@ { "creatures": [ - { "min" : 1, "max" : 4, "creature": "core:imp" }, - { "min" : 5, "max" : 8, "creature": "core:gremlin" }, - { "min" : 9, "max" : 12, "creature": "core:gnoll" }, - { "min" : 13, "max" : 16, "creature": "core:troglodyte" }, - { "min" : 17, "max" : 20, "creature": "core:familiar" }, - { "min" : 21, "max" : 24, "creature": "core:skeleton" }, - { "min" : 25, "max" : 28, "creature": "core:goblin" }, - { "min" : 29, "max" : 32, "creature": "core:masterGremlin" }, - { "min" : 33, "max" : 36, "creature": "core:hobgoblin" }, - { "min" : 37, "max" : 40, "creature": "core:pikeman" }, - { "min" : 41, "max" : 44, "creature": "core:infernalTroglodyte" }, - { "min" : 45, "max" : 48, "creature": "core:skeletonWarrior" }, - { "min" : 49, "max" : 52, "creature": "core:gnollMarauder" }, - { "min" : 53, "max" : 56, "creature": "core:walkingDead" }, - { "min" : 57, "max" : 60, "creature": "core:centaur" }, - { "min" : 61, "max" : 64, "creature": "core:halberdier" }, - { "min" : 65, "max" : 68, "creature": "core:archer" }, - { "min" : 69, "max" : 72, "creature": "core:lizardman" }, - { "min" : 73, "max" : 76, "creature": "core:zombie" }, - { "min" : 77, "max" : 80, "creature": "core:goblinWolfRider" }, - { "min" : 81, "max" : 84, "creature": "core:centaurCaptain" }, - { "min" : 85, "max" : 88, "creature": "core:dwarf" }, - { "min" : 89, "max" : 92, "creature": "core:harpy" }, - { "min" : 93, "max" : 96, "creature": "core:lizardWarrior" }, - { "min" : 97, "max" : 100, "creature": "core:gog" }, - { "min" : 101, "max" : 104, "creature": "core:stoneGargoyle" }, - { "min" : 105, "max" : 108, "creature": "core:sharpshooter" }, - { "min" : 109, "max" : 112, "creature": "core:orc" }, - { "min" : 113, "max" : 116, "creature": "core:obsidianGargoyle" }, - { "min" : 117, "max" : 120, "creature": "core:hobgoblinWolfRider" }, - { "min" : 121, "max" : 124, "creature": "core:battleDwarf" }, - { "min" : 125, "max" : 128, "creature": "core:woodElf" }, - { "min" : 129, "max" : 132, "creature": "core:harpyHag" }, - { "min" : 133, "max" : 136, "creature": "core:magog" }, - { "min" : 137, "max" : 140, "creature": "core:orcChieftain" }, - { "min" : 141, "max" : 144, "creature": "core:stoneGolem" }, - { "min" : 145, "max" : 148, "creature": "core:wight" }, - { "min" : 149, "max" : 152, "creature": "core:serpentFly" }, - { "min" : 153, "max" : 156, "creature": "core:dragonFly" }, - { "min" : 157, "max" : 160, "creature": "core:wraith" }, - { "min" : 161, "max" : 164, "creature": "core:waterElemental" }, - { "min" : 165, "max" : 168, "creature": "core:earthElemental" }, - { "min" : 169, "max" : 172, "creature": "core:grandElf" }, - { "min" : 173, "max" : 176, "creature": "core:beholder" }, - { "min" : 177, "max" : 180, "creature": "core:fireElemental" }, - { "min" : 181, "max" : 184, "creature": "core:griffin" }, - { "min" : 185, "max" : 187, "creature": "core:airElemental" }, - { "min" : 188, "max" : 190, "creature": "core:hellHound" }, - { "min" : 191, "max" : 193, "creature": "core:evilEye" }, - { "min" : 194, "max" : 196, "creature": "core:cerberus" }, - { "min" : 197, "max" : 199, "creature": "core:ironGolem" }, - { "min" : 200, "max" : 202, "creature": "core:ogre" }, - { "min" : 203, "max" : 205, "creature": "core:swordsman" }, - { "min" : 206, "max" : 208, "creature": "core:demon" }, - { "min" : 209, "max" : 211, "creature": "core:royalGriffin" }, - { "min" : 212, "max" : 214, "creature": "core:hornedDemon" }, - { "min" : 215, "max" : 217, "creature": "core:monk" }, - { "min" : 218, "max" : 220, "creature": "core:dendroidGuard" }, - { "min" : 221, "max" : 223, "creature": "core:medusa" }, - { "min" : 224, "max" : 226, "creature": "core:pegasus" }, - { "min" : 227, "max" : 229, "creature": "core:silverPegasus" }, - { "min" : 230, "max" : 232, "creature": "core:basilisk" }, - { "min" : 233, "max" : 235, "creature": "core:vampire" }, - { "min" : 236, "max" : 238, "creature": "core:mage" }, - { "min" : 239, "max" : 241, "creature": "core:medusaQueen" }, - { "min" : 242, "max" : 244, "creature": "core:crusader" }, - { "min" : 245, "max" : 247, "creature": "core:goldGolem" }, - { "min" : 248, "max" : 250, "creature": "core:ogreMage" }, - { "min" : 251, "max" : 253, "creature": "core:archMage" }, - { "min" : 254, "max" : 256, "creature": "core:greaterBasilisk" }, - { "min" : 257, "max" : 259, "creature": "core:zealot" }, - { "min" : 260, "max" : 262, "creature": "core:pitFiend" }, - { "min" : 263, "max" : 265, "creature": "core:diamondGolem" }, - { "min" : 266, "max" : 268, "creature": "core:vampireLord" }, - { "min" : 269, "max" : 271, "creature": "core:dendroidSoldier" }, - { "min" : 272, "max" : 274, "creature": "core:minotaur" }, - { "min" : 275, "max" : 277, "creature": "core:lich" }, - { "min" : 278, "max" : 280, "creature": "core:genie" }, - { "min" : 281, "max" : 283, "creature": "core:gorgon" }, - { "min" : 284, "max" : 286, "creature": "core:masterGenie" }, - { "min" : 287, "max" : 289, "creature": "core:roc" }, - { "min" : 290, "max" : 292, "creature": "core:mightyGorgon" }, - { "min" : 293, "max" : 295, "creature": "core:minotaurKing" }, - { "min" : 296, "max" : 298, "creature": "core:powerLich" }, - { "min" : 299, "max" : 301, "creature": "core:thunderbird" }, - { "min" : 302, "max" : 304, "creature": "core:pitLord" }, - { "min" : 305, "max" : 307, "creature": "core:cyclop" }, - { "min" : 308, "max" : 310, "creature": "core:wyvern" }, - { "min" : 311, "max" : 313, "creature": "core:cyclopKing" }, - { "min" : 314, "max" : 316, "creature": "core:wyvernMonarch" }, - { "min" : 317, "max" : 319, "creature": "core:manticore" }, - { "min" : 320, "max" : 322, "creature": "core:scorpicore" }, - { "min" : 323, "max" : 325, "creature": "core:efreet" }, - { "min" : 326, "max" : 328, "creature": "core:unicorn" }, - { "min" : 329, "max" : 331, "creature": "core:efreetSultan" }, - { "min" : 332, "max" : 334, "creature": "core:cavalier" }, - { "min" : 335, "max" : 337, "creature": "core:naga" }, - { "min" : 338, "max" : 340, "creature": "core:warUnicorn" }, - { "min" : 341, "max" : 343, "creature": "core:blackKnight" }, - { "min" : 344, "max" : 346, "creature": "core:champion" }, - { "min" : 347, "max" : 349, "creature": "core:dreadKnight" }, - { "min" : 350, "max" : 352, "creature": "core:nagaQueen" }, - { "min" : 353, "max" : 355, "creature": "core:behemoth" }, - { "min" : 356, "max" : 358, "creature": "core:boneDragon" }, - { "min" : 359, "max" : 361, "creature": "core:giant" }, - { "min" : 362, "max" : 364, "creature": "core:hydra" }, - { "min" : 365, "max" : 367, "creature": "core:ghostDragon" }, - { "min" : 368, "max" : 370, "creature": "core:redDragon" }, - { "min" : 371, "max" : 373, "creature": "core:greenDragon" }, - { "min" : 374, "max" : 376, "creature": "core:angel" }, - { "min" : 377, "max" : 379, "creature": "core:devil" }, - { "min" : 380, "max" : 382, "creature": "core:chaosHydra" }, - { "min" : 383, "max" : 385, "creature": "core:ancientBehemoth" }, - { "min" : 386, "max" : 388, "creature": "core:archDevil" }, - { "min" : 389, "max" : 391, "creature": "core:titan" }, - { "min" : 392, "max" : 394, "creature": "core:goldDragon" }, - { "min" : 395, "max" : 397, "creature": "core:blackDragon" }, - { "min" : 398, "max" : 400, "creature": "core:archangel" } + { "min" : 1, "max" : 4, "creature": "imp" }, + { "min" : 5, "max" : 8, "creature": "gremlin" }, + { "min" : 9, "max" : 12, "creature": "gnoll" }, + { "min" : 13, "max" : 16, "creature": "troglodyte" }, + { "min" : 17, "max" : 20, "creature": "familiar" }, + { "min" : 21, "max" : 24, "creature": "skeleton" }, + { "min" : 25, "max" : 28, "creature": "goblin" }, + { "min" : 29, "max" : 32, "creature": "masterGremlin" }, + { "min" : 33, "max" : 36, "creature": "hobgoblin" }, + { "min" : 37, "max" : 40, "creature": "pikeman" }, + { "min" : 41, "max" : 44, "creature": "infernalTroglodyte" }, + { "min" : 45, "max" : 48, "creature": "skeletonWarrior" }, + { "min" : 49, "max" : 52, "creature": "gnollMarauder" }, + { "min" : 53, "max" : 56, "creature": "walkingDead" }, + { "min" : 57, "max" : 60, "creature": "centaur" }, + { "min" : 61, "max" : 64, "creature": "halberdier" }, + { "min" : 65, "max" : 68, "creature": "archer" }, + { "min" : 69, "max" : 72, "creature": "lizardman" }, + { "min" : 73, "max" : 76, "creature": "zombie" }, + { "min" : 77, "max" : 80, "creature": "goblinWolfRider" }, + { "min" : 81, "max" : 84, "creature": "centaurCaptain" }, + { "min" : 85, "max" : 88, "creature": "dwarf" }, + { "min" : 89, "max" : 92, "creature": "harpy" }, + { "min" : 93, "max" : 96, "creature": "lizardWarrior" }, + { "min" : 97, "max" : 100, "creature": "gog" }, + { "min" : 101, "max" : 104, "creature": "stoneGargoyle" }, + { "min" : 105, "max" : 108, "creature": "sharpshooter" }, + { "min" : 109, "max" : 112, "creature": "orc" }, + { "min" : 113, "max" : 116, "creature": "obsidianGargoyle" }, + { "min" : 117, "max" : 120, "creature": "hobgoblinWolfRider" }, + { "min" : 121, "max" : 124, "creature": "battleDwarf" }, + { "min" : 125, "max" : 128, "creature": "woodElf" }, + { "min" : 129, "max" : 132, "creature": "harpyHag" }, + { "min" : 133, "max" : 136, "creature": "magog" }, + { "min" : 137, "max" : 140, "creature": "orcChieftain" }, + { "min" : 141, "max" : 144, "creature": "stoneGolem" }, + { "min" : 145, "max" : 148, "creature": "wight" }, + { "min" : 149, "max" : 152, "creature": "serpentFly" }, + { "min" : 153, "max" : 156, "creature": "dragonFly" }, + { "min" : 157, "max" : 160, "creature": "wraith" }, + { "min" : 161, "max" : 164, "creature": "waterElemental" }, + { "min" : 165, "max" : 168, "creature": "earthElemental" }, + { "min" : 169, "max" : 172, "creature": "grandElf" }, + { "min" : 173, "max" : 176, "creature": "beholder" }, + { "min" : 177, "max" : 180, "creature": "fireElemental" }, + { "min" : 181, "max" : 184, "creature": "griffin" }, + { "min" : 185, "max" : 187, "creature": "airElemental" }, + { "min" : 188, "max" : 190, "creature": "hellHound" }, + { "min" : 191, "max" : 193, "creature": "evilEye" }, + { "min" : 194, "max" : 196, "creature": "cerberus" }, + { "min" : 197, "max" : 199, "creature": "ironGolem" }, + { "min" : 200, "max" : 202, "creature": "ogre" }, + { "min" : 203, "max" : 205, "creature": "swordsman" }, + { "min" : 206, "max" : 208, "creature": "demon" }, + { "min" : 209, "max" : 211, "creature": "royalGriffin" }, + { "min" : 212, "max" : 214, "creature": "hornedDemon" }, + { "min" : 215, "max" : 217, "creature": "monk" }, + { "min" : 218, "max" : 220, "creature": "dendroidGuard" }, + { "min" : 221, "max" : 223, "creature": "medusa" }, + { "min" : 224, "max" : 226, "creature": "pegasus" }, + { "min" : 227, "max" : 229, "creature": "silverPegasus" }, + { "min" : 230, "max" : 232, "creature": "basilisk" }, + { "min" : 233, "max" : 235, "creature": "vampire" }, + { "min" : 236, "max" : 238, "creature": "mage" }, + { "min" : 239, "max" : 241, "creature": "medusaQueen" }, + { "min" : 242, "max" : 244, "creature": "crusader" }, + { "min" : 245, "max" : 247, "creature": "goldGolem" }, + { "min" : 248, "max" : 250, "creature": "ogreMage" }, + { "min" : 251, "max" : 253, "creature": "archMage" }, + { "min" : 254, "max" : 256, "creature": "greaterBasilisk" }, + { "min" : 257, "max" : 259, "creature": "zealot" }, + { "min" : 260, "max" : 262, "creature": "pitFiend" }, + { "min" : 263, "max" : 265, "creature": "diamondGolem" }, + { "min" : 266, "max" : 268, "creature": "vampireLord" }, + { "min" : 269, "max" : 271, "creature": "dendroidSoldier" }, + { "min" : 272, "max" : 274, "creature": "minotaur" }, + { "min" : 275, "max" : 277, "creature": "lich" }, + { "min" : 278, "max" : 280, "creature": "genie" }, + { "min" : 281, "max" : 283, "creature": "gorgon" }, + { "min" : 284, "max" : 286, "creature": "masterGenie" }, + { "min" : 287, "max" : 289, "creature": "roc" }, + { "min" : 290, "max" : 292, "creature": "mightyGorgon" }, + { "min" : 293, "max" : 295, "creature": "minotaurKing" }, + { "min" : 296, "max" : 298, "creature": "powerLich" }, + { "min" : 299, "max" : 301, "creature": "thunderbird" }, + { "min" : 302, "max" : 304, "creature": "pitLord" }, + { "min" : 305, "max" : 307, "creature": "cyclop" }, + { "min" : 308, "max" : 310, "creature": "wyvern" }, + { "min" : 311, "max" : 313, "creature": "cyclopKing" }, + { "min" : 314, "max" : 316, "creature": "wyvernMonarch" }, + { "min" : 317, "max" : 319, "creature": "manticore" }, + { "min" : 320, "max" : 322, "creature": "scorpicore" }, + { "min" : 323, "max" : 325, "creature": "efreet" }, + { "min" : 326, "max" : 328, "creature": "unicorn" }, + { "min" : 329, "max" : 331, "creature": "efreetSultan" }, + { "min" : 332, "max" : 334, "creature": "cavalier" }, + { "min" : 335, "max" : 337, "creature": "naga" }, + { "min" : 338, "max" : 340, "creature": "warUnicorn" }, + { "min" : 341, "max" : 343, "creature": "blackKnight" }, + { "min" : 344, "max" : 346, "creature": "champion" }, + { "min" : 347, "max" : 349, "creature": "dreadKnight" }, + { "min" : 350, "max" : 352, "creature": "nagaQueen" }, + { "min" : 353, "max" : 355, "creature": "behemoth" }, + { "min" : 356, "max" : 358, "creature": "boneDragon" }, + { "min" : 359, "max" : 361, "creature": "giant" }, + { "min" : 362, "max" : 364, "creature": "hydra" }, + { "min" : 365, "max" : 367, "creature": "ghostDragon" }, + { "min" : 368, "max" : 370, "creature": "redDragon" }, + { "min" : 371, "max" : 373, "creature": "greenDragon" }, + { "min" : 374, "max" : 376, "creature": "angel" }, + { "min" : 377, "max" : 379, "creature": "devil" }, + { "min" : 380, "max" : 382, "creature": "chaosHydra" }, + { "min" : 383, "max" : 385, "creature": "ancientBehemoth" }, + { "min" : 386, "max" : 388, "creature": "archDevil" }, + { "min" : 389, "max" : 391, "creature": "titan" }, + { "min" : 392, "max" : 394, "creature": "goldDragon" }, + { "min" : 395, "max" : 397, "creature": "blackDragon" }, + { "min" : 398, "max" : 400, "creature": "archangel" } ] } \ No newline at end of file From 30c6cf3b98bc85281edfb7eb26e25f5f37a7c4eb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 21:45:12 +0200 Subject: [PATCH 0521/1248] video & audio correction; win/lose --- client/CPlayerInterface.cpp | 3 +- client/CServerHandler.cpp | 2 +- client/CVideoHandler.cpp | 4 ++- client/CVideoHandler.h | 4 +-- client/mainmenu/CHighScoreScreen.cpp | 50 ++++++++++++++++++++++------ client/mainmenu/CHighScoreScreen.h | 5 ++- 6 files changed, 51 insertions(+), 17 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 904ba2e14..7c3f53d35 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1696,8 +1696,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("main"); - if(won) - GH.windows().createAndPushWindow(); + GH.windows().createAndPushWindow(won); } ); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index a627af56d..f134a5acb 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -698,7 +698,7 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(); + GH.windows().createAndPushWindow(true); } }; if(epilogue.hasPrologEpilog) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index ea1d905bc..954a05455 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -349,7 +349,7 @@ void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) show(x, y, dst, update); } -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update ) +void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function restart) { if (sws == nullptr) return; @@ -368,6 +368,8 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo show(x,y,dst,update); else { + if(restart) + restart(); VideoPath filenameToReopen = fname; // create copy to backup this->fname open(filenameToReopen); nextFrame(); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index ec9f60278..11317859b 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -31,7 +31,7 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true){} + virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0){} virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { return false; @@ -101,7 +101,7 @@ public: void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 90b06f113..251d9c11a 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -22,6 +22,7 @@ #include "../CGameInfo.h" #include "../CVideoHandler.h" +#include "../CMusicHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" @@ -167,8 +168,8 @@ void CHighScoreScreen::buttonExitClick() close(); } -CHighScoreInputScreen::CHighScoreInputScreen() - : CWindowObject(BORDERED) +CHighScoreInputScreen::CHighScoreInputScreen(bool won) + : CWindowObject(BORDERED), won(won) { addUsedEvents(LCLICK); @@ -176,11 +177,20 @@ CHighScoreInputScreen::CHighScoreInputScreen() pos = center(Rect(0, 0, 800, 600)); updateShadow(); - int border = 100; - int textareaW = ((pos.w - 2 * border) / 4); - std::vector t = { "438", "439", "440", "441", "676" }; - for (int i = 0; i < 5; i++) - texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + if(won) + { + int border = 100; + int textareaW = ((pos.w - 2 * border) / 4); + std::vector t = { "438", "439", "440", "441", "676" }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + + CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); + } + else + CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); + + video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; } void CHighScoreInputScreen::addEntry(std::string text) { @@ -189,7 +199,18 @@ void CHighScoreInputScreen::addEntry(std::string text) { void CHighScoreInputScreen::show(Canvas & to) { - CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false); + CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, + [&]() + { + if(won) + { + CCS->videoh->close(); + video = "HSLOOP.SMK"; + CCS->videoh->open(VideoPath::builtin(video)); + } + else + close(); + }); redraw(); CIntObject::show(to); @@ -197,7 +218,7 @@ void CHighScoreInputScreen::show(Canvas & to) void CHighScoreInputScreen::activate() { - CCS->videoh->open(VideoPath::builtin("HSLOOP.SMK")); + CCS->videoh->open(VideoPath::builtin(video)); CIntObject::activate(); } @@ -211,6 +232,12 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + if(!won) + { + close(); + return; + } + if(!input) { input = std::make_shared( @@ -219,8 +246,11 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) if(!text.empty()) { addEntry(text); + close(); + GH.windows().createAndPushWindow(); } - close(); + else + close(); }); } } diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 6c01c35e9..bfed549ba 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -59,8 +59,11 @@ class CHighScoreInputScreen : public CWindowObject { std::vector> texts; std::shared_ptr input; + + std::string video; + bool won; public: - CHighScoreInputScreen(); + CHighScoreInputScreen(bool won); void addEntry(std::string text); From b0e5a11e62e9eb78175924e8d3344455c78609a1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 22 Sep 2023 22:09:01 +0200 Subject: [PATCH 0522/1248] fix win build --- lib/constants/EntityIdentifiers.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 1b5932626..8e03b635c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -248,8 +248,8 @@ public: std::string toString() const; - DLL_LINKAGE static si32 decode(const std::string& identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); static std::string entityType(); }; @@ -333,8 +333,8 @@ public: static const FactionID CONFLUX; static const FactionID NEUTRAL; - DLL_LINKAGE static si32 decode(const std::string& identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); static std::string entityType(); }; From 1a0f5cf159393e6e72fc441470948248325a8251 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 00:21:36 +0200 Subject: [PATCH 0523/1248] calc preperation --- client/CPlayerInterface.cpp | 27 ++++++++++++++++++++++++--- client/CServerHandler.cpp | 10 +++++++--- client/CServerHandler.h | 6 +++++- client/mainmenu/CHighScoreScreen.cpp | 7 ++++++- client/mainmenu/CHighScoreScreen.h | 20 +++++++++++++++++++- config/highscoreCreatures.json | 2 +- 6 files changed, 62 insertions(+), 10 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 7c3f53d35..28f525432 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -66,6 +66,7 @@ #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" +#include "../lib/gameState/CGameState.h" #include "../lib/CStack.h" #include "../lib/CStopWatch.h" #include "../lib/CThreadHelper.h" @@ -1686,17 +1687,37 @@ void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) void CPlayerInterface::requestReturningToMainMenu(bool won) { + HighScoreParameter param; + param.difficulty = cb->getStartInfo()->difficulty; + param.day = cb->getDate(); + param.townAmount = cb->howManyTowns(); + param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->enteredWinningCheatCode; + param.hasGrail = false; + for(const CGHeroInstance * h : cb->getHeroesInfo()) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + param.allDefeated = true; + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + auto ps = cb->getPlayerState(player); + if(ps && player != *cb->getPlayerID()) + if(!ps->checkVanquished()) + param.allDefeated = false; + } + HighScoreCalculation calc; + calc.parameters.push_back(param); + if(won && cb->getStartInfo()->campState) - CSH->startCampaignScenario(cb->getStartInfo()->campState); + CSH->startCampaignScenario(param, cb->getStartInfo()->campState); else { GH.dispatchMainThread( - [won]() + [won, calc]() { CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("main"); - GH.windows().createAndPushWindow(won); + GH.windows().createAndPushWindow(won, calc); } ); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index f134a5acb..597af86ea 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -669,14 +669,18 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) saveSession->Bool() = false; } -void CServerHandler::startCampaignScenario(std::shared_ptr cs) +void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared_ptr cs) { std::shared_ptr ourCampaign = cs; if (!cs) + { ourCampaign = si->campState; + calc.parameters.clear(); + } + calc.parameters.push_back(param); - GH.dispatchMainThread([ourCampaign]() + GH.dispatchMainThread([ourCampaign, this]() { CSH->campaignServerRestartLock.set(true); CSH->endGameplay(); @@ -698,7 +702,7 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(true); + GH.windows().createAndPushWindow(true, calc); } }; if(epilogue.hasPrologEpilog) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 12a130f83..467092e9f 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -14,6 +14,8 @@ #include "../lib/StartInfo.h" #include "../lib/CondSh.h" +#include "mainmenu/CHighScoreScreen.h" + VCMI_LIB_NAMESPACE_BEGIN class CConnection; @@ -86,6 +88,8 @@ class CServerHandler : public IServerAPI, public LobbyInfo std::vector myNames; + HighScoreCalculation calc; + void threadHandleConnection(); void threadRunServer(); void onServerFinished(); @@ -159,7 +163,7 @@ public: void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr); void endGameplay(bool closeConnection = true, bool restart = false); - void startCampaignScenario(std::shared_ptr cs = {}); + void startCampaignScenario(HighScoreParameter param, std::shared_ptr cs = {}); void showServerError(const std::string & txt) const; // TODO: LobbyState must be updated within game so we should always know how many player interfaces our client handle diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 251d9c11a..e47ed1a41 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -28,6 +28,11 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/constants/EntityIdentifiers.h" +int HighScoreCalculation::calculate() +{ + return 0; +} + CHighScoreScreen::CHighScoreScreen() : CWindowObject(BORDERED) { @@ -168,7 +173,7 @@ void CHighScoreScreen::buttonExitClick() close(); } -CHighScoreInputScreen::CHighScoreInputScreen(bool won) +CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) : CWindowObject(BORDERED), won(won) { addUsedEvents(LCLICK); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index bfed549ba..d7222af07 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -16,6 +16,24 @@ class CMultiLineLabel; class CAnimImage; class CTextInput; +class HighScoreParameter +{ +public: + int difficulty; + int day; + int townAmount; + bool usedCheat; + bool hasGrail; + bool allDefeated; +}; + +class HighScoreCalculation +{ +public: + std::vector parameters = std::vector(); + int calculate(); +}; + class CHighScoreScreen : public CWindowObject { enum HighScorePage { SCENARIO, CAMPAIGN }; @@ -63,7 +81,7 @@ class CHighScoreInputScreen : public CWindowObject std::string video; bool won; public: - CHighScoreInputScreen(bool won); + CHighScoreInputScreen(bool won, HighScoreCalculation calc); void addEntry(std::string text); diff --git a/config/highscoreCreatures.json b/config/highscoreCreatures.json index 9f6dcd2ad..b0feda288 100644 --- a/config/highscoreCreatures.json +++ b/config/highscoreCreatures.json @@ -117,6 +117,6 @@ { "min" : 389, "max" : 391, "creature": "titan" }, { "min" : 392, "max" : 394, "creature": "goldDragon" }, { "min" : 395, "max" : 397, "creature": "blackDragon" }, - { "min" : 398, "max" : 400, "creature": "archangel" } + { "min" : 398, "max" : 500, "creature": "archangel" } ] } \ No newline at end of file From dce1ac15384baaea6bd701ac918da5873319655f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 23 Sep 2023 00:32:48 +0200 Subject: [PATCH 0524/1248] Redesign mod incompatibility message --- Mods/vcmi/config/vcmi/chinese.json | 6 ++-- Mods/vcmi/config/vcmi/czech.json | 6 ++-- Mods/vcmi/config/vcmi/english.json | 7 +++-- Mods/vcmi/config/vcmi/french.json | 6 ++-- Mods/vcmi/config/vcmi/german.json | 6 ++-- Mods/vcmi/config/vcmi/polish.json | 6 ++-- Mods/vcmi/config/vcmi/russian.json | 6 ++-- Mods/vcmi/config/vcmi/spanish.json | 6 ++-- Mods/vcmi/config/vcmi/ukrainian.json | 6 ++-- client/CServerHandler.cpp | 15 +++++++--- client/lobby/CLobbyScreen.cpp | 1 - lib/StartInfo.cpp | 4 +-- lib/modding/CModHandler.cpp | 22 +++++++++++---- lib/modding/ModIncompatibility.h | 41 +++++++++++++++++++--------- mapeditor/mainwindow.cpp | 7 +++-- server/CGameHandler.cpp | 13 +++++++-- 16 files changed, 101 insertions(+), 57 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 666b2168b..a62b3a606 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -30,9 +30,9 @@ "vcmi.capitalColors.6" : "褐色", "vcmi.capitalColors.7" : "粉色", - "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", - "vcmi.server.errors.modsIncompatibility" : "需要加载的MOD列表:", - "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", + "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", + "vcmi.server.errors.modsToEnable" : "{需要加载的MOD列表}", + "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", "vcmi.settingsMainWindow.generalTab.hover" : "常规", "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 设置游戏客户端呈现", diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index dcbb8ccaf..04aa13f30 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -48,9 +48,9 @@ "vcmi.lobby.filename" : "Název souboru", "vcmi.lobby.creationDate" : "Datum vytvoření", - "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", - "vcmi.server.errors.modsIncompatibility" : "Následující modifikace jsou nutné pro načtení hry:", - "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", + "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", + "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", + "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", "vcmi.settingsMainWindow.generalTab.hover" : "Obecné", "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index cc95f3e30..5e2279dfe 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -53,9 +53,10 @@ "vcmi.lobby.filename" : "Filename", "vcmi.lobby.creationDate" : "Creation date", - "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", - "vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:", - "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", + "vcmi.server.errors.modsToEnable" : "{Following mods are required}", + "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", + "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 5d3e0a532..69112035d 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -38,9 +38,9 @@ "vcmi.mainMenu.joinTCP" : "Rejoindre TCP/IP jeu", "vcmi.mainMenu.playerName" : "Joueur", - "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", - "vcmi.server.errors.modsIncompatibility" : "Les mods suivants sont nécessaires pour charger le jeu :", - "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", + "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", + "vcmi.server.errors.modsToEnable" : "{Les mods suivants sont nécessaires pour charger le jeu}", + "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", "vcmi.settingsMainWindow.generalTab.hover" : "Général", "vcmi.settingsMainWindow.generalTab.help" : "Passe à l'onglet Options générales, qui contient des paramètres liés au comportement général du client de jeu", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 926501565..fe6f63828 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -52,9 +52,9 @@ "vcmi.lobby.filename" : "Dateiname", "vcmi.lobby.creationDate" : "Erstellungsdatum", - "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", - "vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:", - "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", + "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", + "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index f382a9578..af4eac0c6 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -47,9 +47,9 @@ "vcmi.lobby.filename" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", - "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", - "vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:", - "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", + "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", + "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", + "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 51d13ac0d..63d1a2d48 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -21,9 +21,9 @@ "vcmi.adventureMap.moveCostDetails" : "Очки движения - Стоимость: %TURNS ходов + %POINTS очков, Останется: %REMAINING очков", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки движения - Стоимость: %POINTS очков, Останется: %REMAINING очков", - "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", - "vcmi.server.errors.modsIncompatibility" : "Требуемые моды для загрузки игры:", - "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", + "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", + "vcmi.server.errors.modsToEnable" : "{Требуемые моды для загрузки игры}", + "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", "vcmi.settingsMainWindow.generalTab.hover" : "Общее", "vcmi.settingsMainWindow.generalTab.help" : "Переключиться на вкладку \"Общее\", содержащее общие настройки клиента игры", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index ffc43609b..e18986ebc 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -30,9 +30,9 @@ "vcmi.capitalColors.6" : "Turquesa", "vcmi.capitalColors.7" : "Rosa", - "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", - "vcmi.server.errors.modsIncompatibility" : "Mods necesarios para cargar el juego:", - "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", + "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", + "vcmi.server.errors.modsToEnable" : "{Mods necesarios para cargar el juego}", + "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 78f16b9d4..315a837ad 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -48,9 +48,9 @@ "vcmi.lobby.filename" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", - "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", - "vcmi.server.errors.modsIncompatibility" : "Потрібні модифікації для завантаження гри:", - "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", + "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", + "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", + "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 5f7097f81..3a0943f01 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -552,10 +552,17 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const catch(ModIncompatibility & e) { logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); - - auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); - + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } showServerError(errorMsg); return false; } diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index dba561388..f2256f91f 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -29,7 +29,6 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" -#include "../../lib/modding/ModIncompatibility.h" #include "../../lib/rmg/CMapGenOptions.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index c8100987a..f3abd2d92 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -74,12 +74,12 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.529")); auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); - ModIncompatibility::ModList modList; + ModIncompatibility::ModListWithVersion modList; for(const auto & m : missingMods) modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) - throw ModIncompatibility(std::move(modList)); + throw ModIncompatibility(modList); //there must be at least one human player before game can be started std::map::const_iterator i; diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 553c41d98..f872bc642 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -481,6 +481,10 @@ void CModHandler::trySetActiveMods(const std::vector newActiveMods, missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + for(const auto & m : activeMods) { if(searchVerificationInfo(m)) @@ -488,11 +492,11 @@ void CModHandler::trySetActiveMods(const std::vector newActiveMods, missingMods; - ModIncompatibility::ModList missingModsResult; for(const auto & infoPair : modList) { @@ -539,9 +543,17 @@ void CModHandler::trySetActiveMods(const std::vectorname, vInfo->version.toString()}); } } + for(auto & m : excessiveMods) + { + auto & vInfo = getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } - if(!missingModsResult.empty()) - throw ModIncompatibility(std::move(missingModsResult)); + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); //TODO: support actual enabling of these mods for(auto & m : newActiveMods) diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index ee7e2e2c4..199e4098e 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -14,29 +14,44 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE ModIncompatibility: public std::exception { public: - using StringPair = std::pair; - using ModList = std::list; + using ModListWithVersion = std::vector>; + using ModList = std::vector; - ModIncompatibility(ModList && _missingMods): - missingMods(std::move(_missingMods)) + ModIncompatibility(const ModListWithVersion & _missingMods) { std::ostringstream _ss; - for(const auto & m : missingMods) + for(const auto & m : _missingMods) _ss << m.first << ' ' << m.second << std::endl; - message = _ss.str(); + messageMissingMods = _ss.str(); } - + + ModIncompatibility(const ModListWithVersion & _missingMods, ModList & _excessiveMods) + : ModIncompatibility(_missingMods) + { + std::ostringstream _ss; + for(const auto & m : _excessiveMods) + _ss << m << std::endl; + messageExcessiveMods = _ss.str(); + } + const char * what() const noexcept override { - return message.c_str(); + static const std::string w("Mod incompatibility exception"); + return w.c_str(); + } + + const std::string & whatMissing() const noexcept + { + return messageMissingMods; + } + + const std::string & whatExcessive() const noexcept + { + return messageExcessiveMods; } private: - //list of mods required to load the game - // first: mod name - // second: mod version - const ModList missingMods; - std::string message; + std::string messageMissingMods, messageExcessiveMods; }; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 3d23b923f..8242ee5ec 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -336,19 +336,20 @@ bool MainWindow::openMap(const QString & filenameSelect) if(auto header = mapService.loadMapHeader(resId)) { auto missingMods = CMapService::verifyMapHeaderMods(*header); - ModIncompatibility::ModList modList; + ModIncompatibility::ModListWithVersion modList; for(const auto & m : missingMods) modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) - throw ModIncompatibility(std::move(modList)); + throw ModIncompatibility(modList); controller.setMap(mapService.loadMap(resId)); } } catch(const ModIncompatibility & e) { - QMessageBox::warning(this, "Mods requiered", e.what()); + assert(e.whatExcessive().empty()); + QMessageBox::warning(this, "Mods are requiered", QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3c525b416..ad792ce12 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1765,8 +1765,17 @@ bool CGameHandler::load(const std::string & filename) catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } lobby->announceMessage(errorMsg); return false; } From 901a33bf1dde73ae6399339a46589664e8975623 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:03:15 +0200 Subject: [PATCH 0525/1248] calculation --- client/mainmenu/CHighScoreScreen.cpp | 45 ++++++++++++++++++++++++++-- client/mainmenu/CHighScoreScreen.h | 3 +- 2 files changed, 44 insertions(+), 4 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index e47ed1a41..f9651b6b2 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -28,9 +28,44 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/constants/EntityIdentifiers.h" -int HighScoreCalculation::calculate() +auto HighScoreCalculation::calculate() { - return 0; + struct result + { + int basic; + int total; + int sumDays; + }; + + std::vector scoresBasic; + std::vector scoresTotal; + double sumBasic = 0; + double sumTotal = 0; + int sumDays = 0; + for(auto & param : parameters) + { + double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); + scoresBasic.push_back(static_cast(tmp)); + sumBasic += tmp; + if(param.difficulty == 0) + tmp *= 0.8; + if(param.difficulty == 1) + tmp *= 1.0; + if(param.difficulty == 2) + tmp *= 1.3; + if(param.difficulty == 3) + tmp *= 1.6; + if(param.difficulty == 4) + tmp *= 2.0; + scoresTotal.push_back(static_cast(tmp)); + sumTotal += tmp; + sumDays += param.day; + } + + if(scoresBasic.size() == 1) + return result { scoresBasic[0], scoresTotal[0], sumDays }; + + return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays }; } CHighScoreScreen::CHighScoreScreen() @@ -174,7 +209,7 @@ void CHighScoreScreen::buttonExitClick() } CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) - : CWindowObject(BORDERED), won(won) + : CWindowObject(BORDERED), won(won), calc(calc) { addUsedEvents(LCLICK); @@ -189,6 +224,10 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc std::vector t = { "438", "439", "440", "441", "676" }; for (int i = 0; i < 5; i++) texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + + t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), "TODO" }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); } diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index d7222af07..c5ca6fe57 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -31,7 +31,7 @@ class HighScoreCalculation { public: std::vector parameters = std::vector(); - int calculate(); + auto calculate(); }; class CHighScoreScreen : public CWindowObject @@ -80,6 +80,7 @@ class CHighScoreInputScreen : public CWindowObject std::string video; bool won; + HighScoreCalculation calc; public: CHighScoreInputScreen(bool won, HighScoreCalculation calc); From 61141a040641e5c13539a2707b387734a7d5df08 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 23 Sep 2023 01:10:45 +0200 Subject: [PATCH 0526/1248] Fix gcc compiling --- lib/modding/ModIncompatibility.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index 199e4098e..23af7deaf 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -15,7 +15,7 @@ class DLL_LINKAGE ModIncompatibility: public std::exception { public: using ModListWithVersion = std::vector>; - using ModList = std::vector; + using ModList = std::vector; ModIncompatibility(const ModListWithVersion & _missingMods) { From d3f007453d32bf143c89134966d6bc1d10f006e4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:36:01 +0200 Subject: [PATCH 0527/1248] win screen ready --- client/CPlayerInterface.cpp | 2 +- client/CServerHandler.cpp | 1 + client/mainmenu/CHighScoreScreen.cpp | 33 ++++++++++++++++++---------- client/mainmenu/CHighScoreScreen.h | 3 +++ 4 files changed, 27 insertions(+), 12 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 28f525432..cc85e6144 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -66,7 +66,6 @@ #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/CPlayerState.h" -#include "../lib/gameState/CGameState.h" #include "../lib/CStack.h" #include "../lib/CStopWatch.h" #include "../lib/CThreadHelper.h" @@ -1706,6 +1705,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) } HighScoreCalculation calc; calc.parameters.push_back(param); + calc.isCampaign = false; if(won && cb->getStartInfo()->campState) CSH->startCampaignScenario(param, cb->getStartInfo()->campState); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 597af86ea..9c105a9ed 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -676,6 +676,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared if (!cs) { ourCampaign = si->campState; + calc.isCampaign = true; calc.parameters.clear(); } calc.parameters.push_back(param); diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index f9651b6b2..86925bdf8 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -35,6 +35,7 @@ auto HighScoreCalculation::calculate() int basic; int total; int sumDays; + bool cheater; }; std::vector scoresBasic; @@ -42,6 +43,7 @@ auto HighScoreCalculation::calculate() double sumBasic = 0; double sumTotal = 0; int sumDays = 0; + bool cheater = false; for(auto & param : parameters) { double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); @@ -60,12 +62,27 @@ auto HighScoreCalculation::calculate() scoresTotal.push_back(static_cast(tmp)); sumTotal += tmp; sumDays += param.day; + if(param.usedCheat) + cheater = true; } if(scoresBasic.size() == 1) - return result { scoresBasic[0], scoresTotal[0], sumDays }; + return result { scoresBasic[0], scoresTotal[0], sumDays , cheater}; - return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays }; + return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays, cheater }; +} + +CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) +{ + static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + auto creatures = configCreatures["creatures"].Vector(); + int divide = campaign ? 5 : 1; + + for(auto & creature : creatures) + if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer()) + return CreatureID::decode(creature["creature"].String()); + + return -1; } CHighScoreScreen::CHighScoreScreen() @@ -120,9 +137,6 @@ void CHighScoreScreen::addHighScores() texts.clear(); images.clear(); - static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); - auto creatures = configCreatures["creatures"].Vector(); - // Header texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); @@ -161,11 +175,7 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); } - int divide = (highscorepage == HighScorePage::SCENARIO) ? 1 : 5; - for(auto & creature : creatures) { - if(curData["points"].Integer() / divide <= creature["max"].Integer() && curData["points"].Integer() / divide >= creature["min"].Integer()) - images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[CreatureID::decode(creature["creature"].String())]->getIconIndex(), 0, 670, y - 15 + i * 50)); - } + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); } } @@ -225,7 +235,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc for (int i = 0; i < 5; i++) texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); - t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), "TODO" }; + std::string creatureName = (calc.calculate().cheater) ? CGI->generaltexth->translate("core.genrltxt.260") : (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(calc.calculate().total, calc.isCampaign)]->getNameSingularTranslated(); + t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), creatureName }; for (int i = 0; i < 5; i++) texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index c5ca6fe57..6be872569 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -31,7 +31,10 @@ class HighScoreCalculation { public: std::vector parameters = std::vector(); + bool isCampaign = false; + auto calculate(); + static CreatureID getCreatureForPoints(int points, bool campaign); }; class CHighScoreScreen : public CWindowObject From 049f90159d7e238ab06f6d8f6695565d9d946117 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:53:45 +0200 Subject: [PATCH 0528/1248] highlight preparation --- client/mainmenu/CHighScoreScreen.cpp | 21 ++++++++++++--------- client/mainmenu/CHighScoreScreen.h | 4 +++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 86925bdf8..5ca8f612c 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -85,8 +85,8 @@ CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) return -1; } -CHighScoreScreen::CHighScoreScreen() - : CWindowObject(BORDERED) +CHighScoreScreen::CHighScoreScreen(int highlighted) + : CWindowObject(BORDERED), highlighted(highlighted) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 800, 600)); @@ -95,6 +95,8 @@ CHighScoreScreen::CHighScoreScreen() addHighScores(); addButtons(); + // TODO write also datetime for RMB menu + // TODO: remove; only for testing for (int i = 0; i < 11; i++) { @@ -159,20 +161,21 @@ void CHighScoreScreen::addHighScores() for (int i = 0; i < 11; i++) { auto & curData = data[std::to_string(i)]; + ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; - texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(i+1))); - texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["player"].String())); + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); + texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["player"].String())); if(highscorepage == HighScorePage::SCENARIO) { - texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["land"].String())); - texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["days"].Integer()))); - texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); + texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["land"].String())); + texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } else { - texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, curData["campaign"].String())); - texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, std::to_string(curData["points"].Integer()))); + texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["campaign"].String())); + texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 6be872569..2062647f6 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -55,8 +55,10 @@ class CHighScoreScreen : public CWindowObject std::vector> buttons; std::vector> texts; std::vector> images; + + int highlighted; public: - CHighScoreScreen(); + CHighScoreScreen(int highlighted = -1); }; class CHighScoreInput : public CWindowObject From e18a4a09a986ba97e3148dddfdd9b2f8acfe7782 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 02:14:45 +0200 Subject: [PATCH 0529/1248] forward declaration --- client/CServerHandler.cpp | 8 ++++---- client/CServerHandler.h | 7 ++++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 9c105a9ed..6e18ebcb6 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -676,10 +676,10 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared if (!cs) { ourCampaign = si->campState; - calc.isCampaign = true; - calc.parameters.clear(); + calc->isCampaign = true; + calc->parameters.clear(); } - calc.parameters.push_back(param); + calc->parameters.push_back(param); GH.dispatchMainThread([ourCampaign, this]() { @@ -703,7 +703,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(true, calc); + GH.windows().createAndPushWindow(true, *calc); } }; if(epilogue.hasPrologEpilog) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 467092e9f..0b8b3f146 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -14,8 +14,6 @@ #include "../lib/StartInfo.h" #include "../lib/CondSh.h" -#include "mainmenu/CHighScoreScreen.h" - VCMI_LIB_NAMESPACE_BEGIN class CConnection; @@ -37,6 +35,9 @@ VCMI_LIB_NAMESPACE_END class CClient; class CBaseForLobbyApply; +class HighScoreCalculation; +class HighScoreParameter; + // TODO: Add mutex so we can't set CONNECTION_CANCELLED if client already connected, but thread not setup yet enum class EClientState : ui8 { @@ -88,7 +89,7 @@ class CServerHandler : public IServerAPI, public LobbyInfo std::vector myNames; - HighScoreCalculation calc; + std::shared_ptr calc; void threadHandleConnection(); void threadRunServer(); From a5492b30c3b529a700371411bf6d246c44d369b9 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 23 Sep 2023 02:35:21 +0200 Subject: [PATCH 0530/1248] Add pre-battle message --- lib/mapObjects/CGCreature.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 7da445fca..a2891c2cb 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -103,6 +103,16 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { + //show message + if(!message.empty()) + { + InfoWindow iw; + iw.player = h->tempOwner; + iw.text.appendRawString(message); + iw.type = EInfoWindowMode::MODAL; + cb->showInfoDialog(&iw); + } + int action = takenAction(h); switch( action ) //decide what we do... { From 5f2023c8d55539fb6719181171bc93af07ecd34e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 23 Sep 2023 04:33:08 +0200 Subject: [PATCH 0531/1248] Fix typo --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index bac669ae7..66a86592d 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -393,7 +393,7 @@ void ArmyManager::update() } } - for(auto army : totalArmy) + for(auto & army : totalArmy) { army.second.creature = army.first.toCreature(); army.second.power = evaluateStackPower(army.second.creature, army.second.count); From 59e1ec3ae7d499d15a4aa1a0abc0ce9b1052ed35 Mon Sep 17 00:00:00 2001 From: An Vu Date: Sat, 23 Sep 2023 10:59:38 +0700 Subject: [PATCH 0532/1248] add vietnamese --- Mods/vcmi/config/vcmi/vietnamese.json | 372 ++++++++++++++++++++++++++ Mods/vcmi/mod.json | 30 ++- 2 files changed, 392 insertions(+), 10 deletions(-) create mode 100644 Mods/vcmi/config/vcmi/vietnamese.json diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json new file mode 100644 index 000000000..77fa7c519 --- /dev/null +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -0,0 +1,372 @@ +{ + "vcmi.adventureMap.monsterThreat.title": "\n\nMức độ: ", + "vcmi.adventureMap.monsterThreat.levels.0": "Nhẹ nhàng", + "vcmi.adventureMap.monsterThreat.levels.1": "Rất yếu", + "vcmi.adventureMap.monsterThreat.levels.2": "Yếu", + "vcmi.adventureMap.monsterThreat.levels.3": "Yếu hơn", + "vcmi.adventureMap.monsterThreat.levels.4": "Ngang bằng", + "vcmi.adventureMap.monsterThreat.levels.5": "Nhỉnh hơn", + "vcmi.adventureMap.monsterThreat.levels.6": "Mạnh", + "vcmi.adventureMap.monsterThreat.levels.7": "Rất mạnh", + "vcmi.adventureMap.monsterThreat.levels.8": "Thách thức", + "vcmi.adventureMap.monsterThreat.levels.9": "Áp đảo", + "vcmi.adventureMap.monsterThreat.levels.10": "Chết chóc", + "vcmi.adventureMap.monsterThreat.levels.11": "Bất khả diệt", + + "vcmi.adventureMap.confirmRestartGame": "Bạn muốn chơi lại?", + "vcmi.adventureMap.noTownWithMarket": "Chợ không có sẵn!", + "vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!", + "vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.", + "vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s", + "vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING", + + "vcmi.capitalColors.0": "Đỏ", + "vcmi.capitalColors.1": "Xanh dương", + "vcmi.capitalColors.2": "Nâu", + "vcmi.capitalColors.3": "Xanh lá", + "vcmi.capitalColors.4": "Cam", + "vcmi.capitalColors.5": "Tím", + "vcmi.capitalColors.6": "Xanh đậm", + "vcmi.capitalColors.7": "Hồng", + + "vcmi.heroOverview.startingArmy": "Lính ban đầu", + "vcmi.heroOverview.warMachine": "Chiến cơ", + "vcmi.heroOverview.secondarySkills": "Kĩ năng", + "vcmi.heroOverview.spells": "Phép", + + "vcmi.radialWheel.mergeSameUnit": "Sáp nhập cùng loài", + "vcmi.radialWheel.fillSingleUnit": "Làm đầy với từng loài", + "vcmi.radialWheel.splitSingleUnit": "Tách 1 loài", + "vcmi.radialWheel.splitUnitEqually": "Chia quái bằng nhau", + "vcmi.radialWheel.moveUnit": "Di chuyển quái đến đội khác", + "vcmi.radialWheel.splitUnit": "Chia quái đến ô khác", + + "vcmi.mainMenu.highscoresNotImplemented": "Xin lỗi, bảng xếp hạng chưa được làm đầy đủ\n", + "vcmi.mainMenu.serverConnecting": "Đang kết nối...", + "vcmi.mainMenu.serverAddressEnter": "Nhập địa chỉ:", + "vcmi.mainMenu.serverClosing": "Đang hủy kết nối...", + "vcmi.mainMenu.hostTCP": "Chủ phòng TCP/IP", + "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP", + "vcmi.mainMenu.playerName": "Người chơi", + + "vcmi.lobby.filename": "Tên tập tin", + "vcmi.lobby.creationDate": "Ngày tạo", + + "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới", + "vcmi.server.errors.modsIncompatibility": "Các bản sửa đổi cần để tải trò chơi:", + "vcmi.server.confirmReconnect": "Bạn có muốn kết nối lại phiên trước?", + + "vcmi.settingsMainWindow.generalTab.hover": "Chung", + "vcmi.settingsMainWindow.generalTab.help": "Chuyển sang bảng Chung, chứa các cài đặt liên quan đến phần chung trò chơi", + "vcmi.settingsMainWindow.battleTab.hover": "Chiến đấu", + "vcmi.settingsMainWindow.battleTab.help": "Chuyển sang bảng Chiến đấu, cho phép thiết lập hành vi trong trận đánh", + "vcmi.settingsMainWindow.adventureTab.hover": "Phiêu lưu", + "vcmi.settingsMainWindow.adventureTab.help": "Chuyển sang bảng Phiêu lưu (bản đồ phiêu lưu là nơi mà người chơi di chuyển tướng của họ)", + + "vcmi.systemOptions.videoGroup": "Thiết lập phim ảnh", + "vcmi.systemOptions.audioGroup": "Thiết lập âm thanh", + "vcmi.systemOptions.otherGroup": "Thiết lập khác", + "vcmi.systemOptions.townsGroup": "Thành phố", + + "vcmi.systemOptions.fullscreenBorderless.hover": "Toàn màn hình (không viền)", + "vcmi.systemOptions.fullscreenBorderless.help": "{Toàn màn hình không viền}\n\nNếu chọn, VCMI sẽ chạy chế độ toàn màn hình không viền. Ở chế độ này, trò chơi sẽ luôn dùng độ phân giải của màn hình, bỏ qua độ phân giải đã chọn.", + "vcmi.systemOptions.fullscreenExclusive.hover": "Toàn màn hình (riêng biệt)", + "vcmi.systemOptions.fullscreenExclusive.help": "{Toàn màn hình}\n\nNếu chọn, VCMI sẽ chạy chế độ dành riêng cho toàn màn hình. Ở chế độ này, trò chơi sẽ chuyển độ phân giải của màn hình sang độ phân giải được chọn.", + "vcmi.systemOptions.resolutionButton.hover": "Độ phân giải: %wx%h", + "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải", + "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.", + "vcmi.systemOptions.scalingButton.hover": "Tỉ lệ giao diện: %p%", + "vcmi.systemOptions.scalingButton.help": "{Tỉ lệ giao diện}\n\nĐổi tỉ lệ giao diện trong trò chơi.", + "vcmi.systemOptions.scalingMenu.hover": "Chọn tỉ lệ giao diện", + "vcmi.systemOptions.scalingMenu.help": "Đổi tỉ lệ giao diện trong trò chơi.", + "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", + "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.", + "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ", + "vcmi.systemOptions.longTouchMenu.help": "Đổi khoảng thời gian chạm giữ.", + "vcmi.systemOptions.longTouchMenu.entry": "%d mili giây", + "vcmi.systemOptions.framerateButton.hover": "Hiện FPS", + "vcmi.systemOptions.framerateButton.help": "{Hiện FPS}\n\nHiện khung hình mỗi giây ở góc cửa sổ trò chơi", + "vcmi.systemOptions.hapticFeedbackButton.hover": "Rung khi chạm", + "vcmi.systemOptions.hapticFeedbackButton.help": "{Rung khi chạm}\n\nBật/ tắt chế độ rung khi chạm.", + + "vcmi.adventureOptions.infoBarPick.hover": "Hiện thông báo ở bảng thông tin", + "vcmi.adventureOptions.infoBarPick.help": "{Hiện thông báo ở bảng thông tin}\n\nThông báo từ các điểm đến thăm sẽ hiện ở bảng thông tin, thay vì trong cửa sổ bật lên.", + "vcmi.adventureOptions.numericQuantities.hover": "Số lượng quái", + "vcmi.adventureOptions.numericQuantities.help": "{Số lượng quái}\n\nHiện lượng quái đối phương dạng số A-B.", + "vcmi.adventureOptions.forceMovementInfo.hover": "Luôn hiện chi phí di chuyển", + "vcmi.adventureOptions.forceMovementInfo.help": "{Luôn hiện chi phí di chuyển}\n\nLuôn hiện điểm di chuyển trong thanh trạng thái. (Thay vì chỉ xem khi nhấn giữ phím ALT)", + "vcmi.adventureOptions.showGrid.hover": "Hiện ô kẻ", + "vcmi.adventureOptions.showGrid.help": "{Hiện ô kẻ}\n\nHiện đường biên giữa các ô trên bản đồ phiêu lưu.", + "vcmi.adventureOptions.borderScroll.hover": "Cuộn ở biên", + "vcmi.adventureOptions.borderScroll.help": "{Cuộn ở biên}\n\nCuộn bản đồ phiêu lưu ở biên. Nhấn giữ phím CTRL để tắt chức năng.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover": "Quản lí quái ở bảng thông tin", + "vcmi.adventureOptions.infoBarCreatureManagement.help": "{Quản lí quái ở bảng thông tin}\n\nCho phép sắp xếp quái ở bảng thông tin thay vì luân chuyển giữa các mục mặc định.", + "vcmi.adventureOptions.leftButtonDrag.hover": "Chuột trái kéo bản đồ", + "vcmi.adventureOptions.leftButtonDrag.help": "{Chuột trái kéo bản đồ}\n\nGiữ chuột trái khi di chuyển sẽ dịch chuyển bản đồ phiêu lưu.", + "vcmi.adventureOptions.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Đặt tốc độ cuộn bản đồ sang rất chậm", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Đặt tốc độ cuộn bản đồ sang rất nhanh", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Đặt tốc độ cuộn bản đồ sang tức thời.", + + "vcmi.battleOptions.queueSizeLabel.hover": "Hiện thứ tự lượt", + "vcmi.battleOptions.queueSizeNoneButton.hover": "TẮT", + "vcmi.battleOptions.queueSizeAutoButton.hover": "TỰ ĐỘNG", + "vcmi.battleOptions.queueSizeSmallButton.hover": "NHỎ", + "vcmi.battleOptions.queueSizeBigButton.hover": "LỚN", + "vcmi.battleOptions.queueSizeNoneButton.help": "Không hiện thứ tự lượt.", + "vcmi.battleOptions.queueSizeAutoButton.help": "Tự động điều chỉnh kích thước thứ tự lượt theo độ phân giải (NHỎ được dùng khi chiều cao thấp hơn 700 px, ngược lại dùng LỚN).", + "vcmi.battleOptions.queueSizeSmallButton.help": "Đặt kích thước thứ tự lượt sang NHỎ.", + "vcmi.battleOptions.queueSizeBigButton.help": "Đặt kích thước thứ tự lượt sang LỚN (không hỗ trợ nếu chiều cao nhỏ hơn 700 px).", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Đặt tốc độ hoạt ảnh sang rất chậm", + "vcmi.battleOptions.animationsSpeed5.help": "Đặt tốc độ hoạt ảnh sang rất nhanh", + "vcmi.battleOptions.animationsSpeed6.help": "Đặt tốc độ hoạt ảnh sang tức thời", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Hiện di chuyển khi di chuột", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Hiện di chuyển khi di chuột}\n\nHiện giới hạn di chuyển của quái khi di chuột lên chúng.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Hiện tầm bắn của cung thủ", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Hiện tầm bắn của cung thủ khi di chuột}\n\nHiện tầm bắn của cung thủ khi di chuột lên chúng.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Hiện thông số tướng", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Hiện thông số tướng}\n\nBật/ tắt bảng chỉ số cơ bản và năng lượng của tướng.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Bỏ qua nhạc dạo đầu", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Bỏ qua nhạc dạo đầu}\n\nKhông cần chờ hết nhạc khởi đầu mỗi trận đánh", + + "vcmi.battleWindow.pressKeyToSkipIntro": "Nhấn phím bất kì để bắt đầu trận đánh", + "vcmi.battleWindow.damageEstimation.melee": "Tấn công %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills": "Tấn công %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged": "Bắn %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills": "Bắn %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.shots.1": "Còn %d lần", + "vcmi.battleWindow.damageEstimation.damage": "%d sát thương", + "vcmi.battleWindow.damageEstimation.damage.1": "%d sát thương", + "vcmi.battleWindow.damageEstimation.kills": "%d sẽ bị diệt", + "vcmi.battleWindow.damageEstimation.kills.1": "%d sẽ bị diệt", + + "vcmi.battleResultsWindow.applyResultsLabel": "Dùng kết quả trận đánh", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover": "Hiện quái được mua", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help": "{Hiện quái được mua}\n\nHiện quái được mua thay vì sinh trưởng trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover": "Hiện sinh trưởng quái hàng tuần", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help": "{Hiện sinh trưởng quái hàng tuần}\n\nHiện sinh trưởng quái thay vì lượng có sẵn trong sơ lược thành (góc trái dưới màn hình thành phố).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Thu gọn thông tin quái", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Thu gọn thông tin quái}\n\nHiện thông tin quái nhỏ hơn trong sơ lược thành (góc trái dưới màn hình thành phố).", + + "vcmi.townHall.missingBase": "Căn cứ %s phải được xây trước", + "vcmi.townHall.noCreaturesToRecruit": "Không có quái để chiêu mộ!", + "vcmi.townHall.greetingManaVortex": "%s giúp cơ thể bạn tràn đầy năng lượng mới. Bạn được gấp đôi năng lượng tối đa.", + "vcmi.townHall.greetingKnowledge": "Bạn học chữ khắc trên %s và thấu hiểu cách vận hành của nhiều ma thuật (+1 Trí).", + "vcmi.townHall.greetingSpellPower": "%s dạy bạn hướng mới tập trung sức mạnh ma thuật (+1 Lực).", + "vcmi.townHall.greetingExperience": "Viếng thăm %s dạy bạn nhiều kĩ năng mới (+1000 Kinh nghiệm).", + "vcmi.townHall.greetingAttack": "Thời gian ở %s giúp bạn học nhiều kĩ năng chiến đấu hiệu quả (+1 Công).", + "vcmi.townHall.greetingDefence": "Thời gian ở %s, các chiến binh lão luyện tại đó dạy bạn nhiều kĩ năng phòng thủ (+1 Thủ).", + "vcmi.townHall.hasNotProduced": "%s chưa tạo được cái gì.", + "vcmi.townHall.hasProduced": "%s tạo %d %s tuần này.", + "vcmi.townHall.greetingCustomBonus": "%s cho bạn +%d %s%s", + "vcmi.townHall.greetingCustomUntil": " đến trận đánh tiếp theo.", + "vcmi.townHall.greetingInTownMagicWell": "%s đã hồi phục năng lượng tối đa của bạn.", + + "vcmi.logicalExpressions.anyOf": "Bất kì cái sau:", + "vcmi.logicalExpressions.allOf": "Tất cả cái sau:", + "vcmi.logicalExpressions.noneOf": "Không có những cái sau:", + + "vcmi.heroWindow.openCommander.hover": "Mở cửa sổ thông tin chỉ huy", + "vcmi.heroWindow.openCommander.help": "Hiện chi tiết về chỉ huy tướng này", + "vcmi.heroWindow.openBackpack.hover": "Mở hành lí", + "vcmi.heroWindow.openBackpack.help": "Hành lí cho phép quản lí vật phẩm dễ dàng hơn.", + + "vcmi.commanderWindow.artifactMessage": "Bạn muốn trả lại vật phẩm này cho tướng?", + + "vcmi.creatureWindow.showBonuses.hover": "Chuyển sang phần tăng thêm", + "vcmi.creatureWindow.showBonuses.help": "Hiện thuộc tính tăng thêm của chỉ huy", + "vcmi.creatureWindow.showSkills.hover": "Chuyển sang phần kĩ năng", + "vcmi.creatureWindow.showSkills.help": "Hiện kĩ năng đã học của chỉ huy", + "vcmi.creatureWindow.returnArtifact.hover": "Trả vật phẩm", + "vcmi.creatureWindow.returnArtifact.help": "Nhấn nút này để trả vật phẩm cho tướng", + + "vcmi.questLog.hideComplete.hover": "Ẩn nhiệm vụ đã làm", + "vcmi.questLog.hideComplete.help": "Ẩn các nhiệm vụ đã hoàn thành", + + "vcmi.randomMapTab.widgets.defaultTemplate": "(mặc định)", + "vcmi.randomMapTab.widgets.templateLabel": "Mẫu", + "vcmi.randomMapTab.widgets.teamAlignmentsButton": "Cài đặt...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội", + "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá", + + "vcmi.optionsTab.widgets.chessFieldBase.help": "{Thời gian thêm}\n\nBắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", + "vcmi.optionsTab.widgets.chessFieldTurn.help": "{Thời gian lượt}\n\nBắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", + "vcmi.optionsTab.widgets.chessFieldBattle.help": "{Thời gian trận đánh}\n\nĐếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", + "vcmi.optionsTab.widgets.chessFieldCreature.help": "{Thời gian lính}\n\nBắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", + "vcmi.optionsTab.widgets.labelTimer": "Đồng hồ", + "vcmi.optionsTab.widgets.timerModeSwitch.classic": "Đồng hồ cơ bản", + "vcmi.optionsTab.widgets.timerModeSwitch.chess": "Đồng hồ đánh cờ", + + "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", + "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers": "Đối thủ đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf": "Chúc mừng! Bạn đã diệt tất cả quái gây hại vùng này và giành chiến thắng!", + "vcmi.map.victoryCondition.collectArtifacts.message": "Đoạt 3 vật phẩm", + "vcmi.map.victoryCondition.angelicAlliance.toSelf": "Chúc mừng! Tất cả đối thủ bị đánh bại và bạn có Angelic Alliance! Chiến thắng thuộc về bạn!", + "vcmi.map.victoryCondition.angelicAlliance.message": "Đánh bại tất cả đối thủ và tạo Angelic Alliance", + + "vcmi.stackExperience.description": "» K I N H N G H I Ệ M «\n\nLoại Quái ................... : %s\nCấp Kinh Nghiệm ................. : %s (%i)\nĐiểm Kinh Nghiệm ............... : %i\nĐiểm Kinh Nghiệm Để Lên Cấp .. : %i\nKinh Nghiệm Tối Đa Mỗi Trận Đánh ... : %i%% (%i)\nSố Lượng Quái .... : %i\nTối Đa Mua Mới\n không bị giảm cấp .... : %i\nHệ Số Kinh Nghiệm ........... : %.2f\nHệ Số Nâng Cấp .............. : %.2f\nKinh Nghiệm Sau Cấp 10 ........ : %i\nTối Đa Mua Mới để vẫn ở\n Mức Tối Đa Kinh Nghiệm Cấp 10 : %i", + "vcmi.stackExperience.rank.0": "Lính Mới", + "vcmi.stackExperience.rank.1": "Tập Sự", + "vcmi.stackExperience.rank.2": "Lành Nghề", + "vcmi.stackExperience.rank.3": "Khéo Léo", + "vcmi.stackExperience.rank.4": "Thông Thạo", + "vcmi.stackExperience.rank.5": "Kì Cựu", + "vcmi.stackExperience.rank.6": "Lão Luyện", + "vcmi.stackExperience.rank.7": "Chuyên Gia", + "vcmi.stackExperience.rank.8": "Tinh Hoa", + "vcmi.stackExperience.rank.9": "Bậc Thầy", + "vcmi.stackExperience.rank.10": "Thiên Tài", + + "core.bonus.ADDITIONAL_ATTACK.name": "Đánh 2 lần", + "core.bonus.ADDITIONAL_ATTACK.description": "Tấn công 2 lần", + "core.bonus.ADDITIONAL_RETALIATION.name": "Thêm phản công", + "core.bonus.ADDITIONAL_RETALIATION.description": "Phản công thêm ${val} lần", + "core.bonus.AIR_IMMUNITY.name": "Kháng Khí", + "core.bonus.AIR_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Khí", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Đánh xung quanh", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Tấn công tất cả đối phương xung quanh", + "core.bonus.BLOCKS_RETALIATION.name": "Ngăn phản công", + "core.bonus.BLOCKS_RETALIATION.description": "Đối phương không thể phản công", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Ngăn bắn phản công", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Đối phương không thể bắn phản công", + "core.bonus.CATAPULT.name": "Công thành", + "core.bonus.CATAPULT.description": "Tấn công tường thành", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Giảm yêu cầu năng lượng (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Giảm ${val} năng lượng cần làm phép", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tăng yêu cầu năng lượng (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Tăng ${val} năng lượng đối phương cần làm phép", + "core.bonus.CHARGE_IMMUNITY.name": "Kháng đột kích", + "core.bonus.CHARGE_IMMUNITY.description": "Kháng đột kích của Cavalier và Champion", + "core.bonus.DARKNESS.name": "Màn tối", + "core.bonus.DARKNESS.description": "Tạo màn bóng tối bán kính ${val}", + "core.bonus.DEATH_STARE.name": "Cái nhìn chết chóc (${val}%)", + "core.bonus.DEATH_STARE.description": "${val}% cơ hội diệt thêm quái", + "core.bonus.DEFENSIVE_STANCE.name": "Tăng Thủ", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Thủ khi đang thế thủ", + "core.bonus.DESTRUCTION.name": "Hủy diệt", + "core.bonus.DESTRUCTION.description": "${val}% cơ hội diệt thêm quái", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Đòn chí mạng", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% cơ hội nhân đôi sát thương khi tấn công", + "core.bonus.DRAGON_NATURE.name": "Rồng", + "core.bonus.DRAGON_NATURE.description": "Quái có chất Rồng", + "core.bonus.EARTH_IMMUNITY.name": "Kháng Đất", + "core.bonus.EARTH_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Đất", + "core.bonus.ENCHANTER.name": "Bùa chú", + "core.bonus.ENCHANTER.description": "Ếm ${subtype.spell} mỗi lượt", + "core.bonus.ENCHANTED.name": "Chúc phúc", + "core.bonus.ENCHANTED.description": "Nhận phép vĩnh cửu ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Xuyên giáp (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Khi tấn công, bỏ qua ${val}% Thủ", + "core.bonus.FIRE_IMMUNITY.name": "Kháng Lửa", + "core.bonus.FIRE_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Lửa", + "core.bonus.FIRE_SHIELD.name": "Khiên lửa (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Phản ${val}% sát thương cận chiến", + "core.bonus.FIRST_STRIKE.name": "Đánh trước", + "core.bonus.FIRST_STRIKE.description": "Quái phản công trước khi bị tấn công", + "core.bonus.FEAR.name": "Hãi hùng", + "core.bonus.FEAR.description": "Gây Hoảng Sợ lên quái đối phương", + "core.bonus.FEARLESS.name": "Can đảm", + "core.bonus.FEARLESS.description": "Không bị hoảng sợ", + "core.bonus.FLYING.name": "Bay", + "core.bonus.FLYING.description": "Vượt chướng ngại vật", + "core.bonus.FREE_SHOOTING.name": "Bắn gần", + "core.bonus.FREE_SHOOTING.description": "Bắn kể cả khi cận chiến", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Không thể hồi sinh", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Giảm sát thương (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Giảm ${val}% sát thương vật lí", + "core.bonus.HATE.name": "Ghét ${subtype.creature}", + "core.bonus.HATE.description": "Tăng thêm ${val}% sát thương cho ${subtype.creature}", + "core.bonus.HEALER.name": "Hồi máu", + "core.bonus.HEALER.description": "Hồi máu đồng đội", + "core.bonus.HP_REGENERATION.name": "Tự hồi máu", + "core.bonus.HP_REGENERATION.description": "Hồi ${SHval} máu mỗi lượt", + "core.bonus.JOUSTING.name": "Đột kích", + "core.bonus.JOUSTING.description": "+${val}% sát thương cho mỗi ô đi qua", + "core.bonus.KING.name": "Khổng lồ", + "core.bonus.KING.description": "Dễ tổn thương bởi Diệt Khổng Lồ cấp ${val} hoặc cao hơn", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Kháng phép 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Kháng phép cấp 1 - ${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name": "Tầm bắn", + "core.bonus.LIMITED_SHOOTING_RANGE.description": "Không thể nhắm bắn quái xa hơn ${val} ô", + "core.bonus.LIFE_DRAIN.name": "Hút máu (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Hồi máu ${val}% sát thương gây ra", + "core.bonus.MANA_CHANNELING.name": "Chuyển năng lượng ${val}%", + "core.bonus.MANA_CHANNELING.description": "Cho tướng ${val}% năng lượng dùng bởi đối phương", + "core.bonus.MANA_DRAIN.name": "Hút năng lượng", + "core.bonus.MANA_DRAIN.description": "Hút ${val} năng lượng mỗi lượt", + "core.bonus.MAGIC_MIRROR.name": "Phản phép (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "${val}% cơ hội phản phép tấn công đến quái đối phương", + "core.bonus.MAGIC_RESISTANCE.name": "Né phép (${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "${val}% cơ hội tránh phép của đối phương", + "core.bonus.MIND_IMMUNITY.name": "Kháng phép tinh thần", + "core.bonus.MIND_IMMUNITY.description": "Kháng ma thuật về tinh thần", + "core.bonus.NO_DISTANCE_PENALTY.name": "Bắn xa", + "core.bonus.NO_DISTANCE_PENALTY.description": "Gây trọn sát thương ở bất kì khoảng cách nào", + "core.bonus.NO_MELEE_PENALTY.name": "Đánh gần", + "core.bonus.NO_MELEE_PENALTY.description": "Quái không bị giảm sát thương khi cận chiến", + "core.bonus.NO_MORALE.name": "Bình tĩnh", + "core.bonus.NO_MORALE.description": "Quái không ảnh hưởng bởi sĩ khí", + "core.bonus.NO_WALL_PENALTY.name": "Bỏ qua tường", + "core.bonus.NO_WALL_PENALTY.description": "Gây trọn sát thương khi công thành", + "core.bonus.NON_LIVING.name": "Vô sinh", + "core.bonus.NON_LIVING.description": "Kháng nhiều hiệu ứng", + "core.bonus.RANDOM_SPELLCASTER.name": "Ếm ngẫu nhiên", + "core.bonus.RANDOM_SPELLCASTER.description": "Ếm phép ngẫu nhiên", + "core.bonus.RANGED_RETALIATION.name": "Phản công tầm xa", + "core.bonus.RANGED_RETALIATION.description": "Phản công khi bị bắn", + "core.bonus.RECEPTIVE.name": "Tiếp thu", + "core.bonus.RECEPTIVE.description": "Không kháng phép có lợi", + "core.bonus.REBIRTH.name": "Tái sinh (${val}%)", + "core.bonus.REBIRTH.description": "${val}% số lượng sẽ hồi sinh sau khi chết", + "core.bonus.RETURN_AFTER_STRIKE.name": "Du kích", + "core.bonus.RETURN_AFTER_STRIKE.description": "Trở về sau khi đánh", + "core.bonus.SHOOTER.name": "Cung thủ", + "core.bonus.SHOOTER.description": "Quái có thể tấn công tầm xa", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Bắn xung quanh", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Bắn tất cả quái trong phạm vi nhỏ", + "core.bonus.SOUL_STEAL.name": "Hút hồn", + "core.bonus.SOUL_STEAL.description": "Tăng ${val} quái mới với mỗi quái đối phương bị diệt", + "core.bonus.SPELLCASTER.name": "Pháp sư", + "core.bonus.SPELLCASTER.description": "Có thể ếm phép ${subtype.spell}", + "core.bonus.SPELL_AFTER_ATTACK.name": "Ếm sau khi đánh", + "core.bonus.SPELL_AFTER_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} sau khi tấn công", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Ếm trước khi đánh", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% cơ hội ếm phép ${subtype.spell} trước khi tấn công", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Kháng phép", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Sát thương phép giảm ${val}%", + "core.bonus.SPELL_IMMUNITY.name": "Miễn dịch", + "core.bonus.SPELL_IMMUNITY.description": "Miễn dịch với phép ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "Đánh phép", + "core.bonus.SPELL_LIKE_ATTACK.description": "Tấn công bằng phép ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Hào quang kháng phép", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Quái ở gần nhận ${val}% kháng ma thuật", + "core.bonus.SUMMON_GUARDIANS.name": "Gọi bảo vệ", + "core.bonus.SUMMON_GUARDIANS.description": "Đầu trận gọi quái ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Hợp lực", + "core.bonus.SYNERGY_TARGET.description": "Quái này dễ bị ảnh hưởng hợp lực", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Hơi thở", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Tấn công 2 ô", + "core.bonus.THREE_HEADED_ATTACK.name": "Ba đầu", + "core.bonus.THREE_HEADED_ATTACK.description": "Tấn công cả quái liền kề mục tiêu", + "core.bonus.TRANSMUTATION.name": "Biến đổi", + "core.bonus.TRANSMUTATION.description": "${val}% cơ hội biến đổi quái mục tiêu thành dạng khác", + "core.bonus.UNDEAD.name": "Thây ma", + "core.bonus.UNDEAD.description": "Quái là thây ma", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Phản công vô hạn", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Không giới hạn số lần phản công", + "core.bonus.WATER_IMMUNITY.name": "Kháng Nước", + "core.bonus.WATER_IMMUNITY.description": "Miễn dịch tất cả phép thuộc tính Nước", + "core.bonus.WIDE_BREATH.name": "Hơi thở sâu", + "core.bonus.WIDE_BREATH.description": "Tấn công nhiều ô" +} diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index eaf2f423e..e1eeccc8a 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -5,7 +5,7 @@ "chinese" : { "name" : "VCMI essential files", "description" : "Essential files required for VCMI to run correctly", - + "skipValidation" : true, "translations" : [ "config/vcmi/chinese.json" @@ -15,7 +15,7 @@ "czech" : { "name" : "Nezbytné soubory VCMI", "description" : "Nezbytné soubory pro správný běh VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/czech.json" @@ -32,23 +32,23 @@ "config/vcmi/french.json" ] }, - + "german" : { "name" : "VCMI - grundlegende Dateien", "description" : "Grundlegende Dateien, die für die korrekte Ausführung von VCMI erforderlich sind", "author" : "VCMI-Team", - + "skipValidation" : true, "translations" : [ "config/vcmi/german.json" ] }, - + "polish" : { "name" : "Podstawowe pliki VCMI", "description" : "Dodatkowe pliki wymagane do prawidłowego działania VCMI", "author" : "Zespół VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/polish.json" @@ -70,7 +70,7 @@ "name" : "VCMI - ключові файли", "description" : "Ключові файли необхідні для повноцінної роботи VCMI", "author" : "Команда VCMI", - + "skipValidation" : true, "translations" : [ "config/vcmi/ukrainian.json" @@ -88,18 +88,28 @@ ] }, + "vietnamese": { + "name": "VCMI essential files", + "description": "Các tập tin cần thiết để chạy VCMI", + "author": "Vũ Đắc Hoàng Ân", + "skipValidation": true, + "translations": [ + "config/vcmi/vietnamese.json" + ] + }, + "version" : "1.3", "author" : "VCMI Team", "contact" : "http://forum.vcmi.eu/index.php", "modType" : "Graphical", - + "factions" : [ "config/vcmi/towerFactions" ], "creatures" : [ "config/vcmi/towerCreature" ], - + "translations" : [ "config/vcmi/english.json" ], - + "templates" : [ "config/vcmi/rmg/hdmod/aroundamarsh.JSON", "config/vcmi/rmg/hdmod/balance.JSON", From d2398b804ad98534a7d535d36404136c4f401cb9 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 13:14:45 +0200 Subject: [PATCH 0533/1248] implement adding --- client/CPlayerInterface.cpp | 1 + client/CServerHandler.cpp | 1 + client/mainmenu/CHighScoreScreen.cpp | 79 ++++++++++++++++++---------- client/mainmenu/CHighScoreScreen.h | 8 ++- 4 files changed, 59 insertions(+), 30 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index cc85e6144..80dc8475a 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1703,6 +1703,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) if(!ps->checkVanquished()) param.allDefeated = false; } + param.land = cb->getMapHeader()->name; HighScoreCalculation calc; calc.parameters.push_back(param); calc.isCampaign = false; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 6e18ebcb6..0b0133dcb 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -679,6 +679,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared calc->isCampaign = true; calc->parameters.clear(); } + param.campaign = cs->getName(); calc->parameters.push_back(param); GH.dispatchMainThread([ourCampaign, this]() diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 5ca8f612c..9540bbad7 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -27,6 +27,9 @@ #include "../../lib/CConfigHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/constants/EntityIdentifiers.h" +#include "../../lib/TextOperations.h" + +#include "vstd/DateUtils.h" auto HighScoreCalculation::calculate() { @@ -85,8 +88,8 @@ CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) return -1; } -CHighScoreScreen::CHighScoreScreen(int highlighted) - : CWindowObject(BORDERED), highlighted(highlighted) +CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) + : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 800, 600)); @@ -94,28 +97,6 @@ CHighScoreScreen::CHighScoreScreen(int highlighted) addHighScores(); addButtons(); - - // TODO write also datetime for RMB menu - - // TODO: remove; only for testing - for (int i = 0; i < 11; i++) - { - Settings entry = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["player"]; - entry->String() = "test"; - Settings entry1 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; - entry1->String() = "test"; - Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["points"]; - entry2->Integer() = std::rand() % 400 * 5; - - Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["player"]; - entry3->String() = "test"; - Settings entry4 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; - entry4->String() = "test"; - Settings entry5 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["days"]; - entry5->Integer() = 123; - Settings entry6 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["points"]; - entry6->Integer() = std::rand() % 400; - } } void CHighScoreScreen::addButtons() @@ -164,17 +145,23 @@ void CHighScoreScreen::addHighScores() ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); - texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["player"].String())); + std::string tmp = curData["player"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); + texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); if(highscorepage == HighScorePage::SCENARIO) { - texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["land"].String())); + std::string tmp = curData["land"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } else { - texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, curData["campaign"].String())); + std::string tmp = curData["campaign"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } @@ -252,7 +239,43 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc } void CHighScoreInputScreen::addEntry(std::string text) { + for (int i = 0; i < 11; i++) + { + JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; + + if(node["points"].isNull() || node["points"].Integer() <= calc.calculate().total) + { + // move following entries down + for (int j = 10; j > i; j--) + { + JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j)]; + Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j + 1)]; + entry->Struct() = node.Struct(); + } + Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["player"]; + entry->String() = text; + if(calc.isCampaign) + { + Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; + entry2->String() = calc.parameters[0].campaign; + } + else + { + Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; + entry3->String() = calc.parameters[0].land; + } + Settings entry4 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["days"]; + entry4->Integer() = calc.calculate().sumDays; + Settings entry5 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["points"]; + entry5->Integer() = calc.calculate().total; + + Settings entry6 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["datetime"]; + entry6->String() = vstd::getFormattedDateTime(std::time(0)); + + return; + } + } } void CHighScoreInputScreen::show(Canvas & to) @@ -305,7 +328,7 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { addEntry(text); close(); - GH.windows().createAndPushWindow(); + GH.windows().createAndPushWindow(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO); } else close(); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 2062647f6..3fa402000 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -25,6 +25,8 @@ public: bool usedCheat; bool hasGrail; bool allDefeated; + std::string campaign; + std::string land; }; class HighScoreCalculation @@ -39,8 +41,10 @@ public: class CHighScoreScreen : public CWindowObject { +public: enum HighScorePage { SCENARIO, CAMPAIGN }; +private: void addButtons(); void addHighScores(); @@ -49,7 +53,7 @@ class CHighScoreScreen : public CWindowObject void buttonResetClick(); void buttonExitClick(); - HighScorePage highscorepage = HighScorePage::SCENARIO; + HighScorePage highscorepage; std::shared_ptr background; std::vector> buttons; @@ -58,7 +62,7 @@ class CHighScoreScreen : public CWindowObject int highlighted; public: - CHighScoreScreen(int highlighted = -1); + CHighScoreScreen(HighScorePage highscorepage = HighScorePage::SCENARIO, int highlighted = -1); }; class CHighScoreInput : public CWindowObject From e3edcb6cd852161efa495b58c52da49a297e19c8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 14:51:39 +0200 Subject: [PATCH 0534/1248] rmb; bugfix; tabs --- client/CServerHandler.cpp | 9 +- client/mainmenu/CHighScoreScreen.cpp | 458 ++++++++++++++------------- client/mainmenu/CHighScoreScreen.h | 82 ++--- 3 files changed, 289 insertions(+), 260 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 0b0133dcb..f8758cc00 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -600,6 +600,8 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta CMM->disable(); client = new CClient(); + calc = nullptr; + switch(si->mode) { case StartInfo::NEW_GAME: @@ -674,8 +676,11 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared std::shared_ptr ourCampaign = cs; if (!cs) - { ourCampaign = si->campState; + + if(calc == nullptr) + { + calc = std::make_shared(); calc->isCampaign = true; calc->parameters.clear(); } @@ -950,7 +955,7 @@ void CServerHandler::threadRunServer() } comm += " > \"" + logName + '\"'; - logGlobal->info("Server command line: %s", comm); + logGlobal->info("Server command line: %s", comm); #ifdef VCMI_WINDOWS int result = -1; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 9540bbad7..f24a5ea50 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -33,333 +33,355 @@ auto HighScoreCalculation::calculate() { - struct result - { - int basic; - int total; - int sumDays; - bool cheater; - }; - - std::vector scoresBasic; - std::vector scoresTotal; - double sumBasic = 0; - double sumTotal = 0; - int sumDays = 0; - bool cheater = false; - for(auto & param : parameters) - { - double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); - scoresBasic.push_back(static_cast(tmp)); - sumBasic += tmp; - if(param.difficulty == 0) - tmp *= 0.8; - if(param.difficulty == 1) - tmp *= 1.0; - if(param.difficulty == 2) - tmp *= 1.3; - if(param.difficulty == 3) - tmp *= 1.6; - if(param.difficulty == 4) - tmp *= 2.0; - scoresTotal.push_back(static_cast(tmp)); - sumTotal += tmp; - sumDays += param.day; - if(param.usedCheat) - cheater = true; - } + struct result + { + int basic; + int total; + int sumDays; + bool cheater; + }; + + std::vector scoresBasic; + std::vector scoresTotal; + double sumBasic = 0; + double sumTotal = 0; + int sumDays = 0; + bool cheater = false; + for(auto & param : parameters) + { + double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); + scoresBasic.push_back(static_cast(tmp)); + sumBasic += tmp; + if(param.difficulty == 0) + tmp *= 0.8; + if(param.difficulty == 1) + tmp *= 1.0; + if(param.difficulty == 2) + tmp *= 1.3; + if(param.difficulty == 3) + tmp *= 1.6; + if(param.difficulty == 4) + tmp *= 2.0; + scoresTotal.push_back(static_cast(tmp)); + sumTotal += tmp; + sumDays += param.day; + if(param.usedCheat) + cheater = true; + } - if(scoresBasic.size() == 1) - return result { scoresBasic[0], scoresTotal[0], sumDays , cheater}; + if(scoresBasic.size() == 1) + return result { scoresBasic[0], scoresTotal[0], sumDays , cheater}; - return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays, cheater }; + return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays, cheater }; } CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) { - static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); - auto creatures = configCreatures["creatures"].Vector(); - int divide = campaign ? 5 : 1; + static const JsonNode configCreatures(JsonPath::builtin("CONFIG/highscoreCreatures.json")); + auto creatures = configCreatures["creatures"].Vector(); + int divide = campaign ? 5 : 1; - for(auto & creature : creatures) - if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer()) - return CreatureID::decode(creature["creature"].String()); + for(auto & creature : creatures) + if(points / divide <= creature["max"].Integer() && points / divide >= creature["min"].Integer()) + return CreatureID::decode(creature["creature"].String()); - return -1; + return -1; } CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) : CWindowObject(BORDERED), highscorepage(highscorepage), highlighted(highlighted) { + addUsedEvents(SHOW_POPUP); + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 800, 600)); updateShadow(); - addHighScores(); - addButtons(); + addHighScores(); + addButtons(); +} + +void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) +{ + for (int i = 0; i < 11; i++) + { + Rect r = Rect(80, 40 + i * 50, 635, 50); + if(r.isInside(cursorPosition - pos)) + { + std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][std::to_string(i)]["datetime"].String(); + if(!tmp.empty()) + CRClickPopup::createAndPush(tmp); + } + } } void CHighScoreScreen::addButtons() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - buttons.clear(); + + buttons.clear(); - buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaginClick(); })); - buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonStandardClick(); })); - buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); - buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaginClick(); })); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonStandardClick(); })); + buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); + buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); } void CHighScoreScreen::addHighScores() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); + background = std::make_shared(ImagePath::builtin(highscorepage == HighScorePage::SCENARIO ? "HISCORE" : "HISCORE2")); - texts.clear(); - images.clear(); + texts.clear(); + images.clear(); - // Header - texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); - texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); + // Header + texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); + texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); - if(highscorepage == HighScorePage::SCENARIO) - { - texts.push_back(std::make_shared(400, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); - texts.push_back(std::make_shared(555, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); - texts.push_back(std::make_shared(625, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); - } - else - { - texts.push_back(std::make_shared(410, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); - texts.push_back(std::make_shared(590, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); - } + if(highscorepage == HighScorePage::SCENARIO) + { + texts.push_back(std::make_shared(400, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); + texts.push_back(std::make_shared(555, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); + texts.push_back(std::make_shared(625, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + } + else + { + texts.push_back(std::make_shared(410, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); + texts.push_back(std::make_shared(590, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + } - // Content - int y = 65; - auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; - for (int i = 0; i < 11; i++) - { - auto & curData = data[std::to_string(i)]; - ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; + // Content + int y = 65; + auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; + for (int i = 0; i < 11; i++) + { + auto & curData = data[std::to_string(i)]; + ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; - texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); - std::string tmp = curData["player"].String(); - TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); - texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - - if(highscorepage == HighScorePage::SCENARIO) - { - std::string tmp = curData["land"].String(); - TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); - texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); - texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); - } - else - { - std::string tmp = curData["campaign"].String(); - TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); - texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); - } + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); + std::string tmp = curData["player"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); + texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); - } + if(highscorepage == HighScorePage::SCENARIO) + { + std::string tmp = curData["land"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + else + { + std::string tmp = curData["campaign"].String(); + TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); + texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + } + + if(curData["points"].Integer() > 0) + images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); + } } void CHighScoreScreen::buttonCampaginClick() { - highscorepage = HighScorePage::CAMPAIGN; - addHighScores(); - addButtons(); - redraw(); + highscorepage = HighScorePage::CAMPAIGN; + addHighScores(); + addButtons(); + redraw(); } void CHighScoreScreen::buttonStandardClick() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - highscorepage = HighScorePage::SCENARIO; - addHighScores(); - addButtons(); - redraw(); + highscorepage = HighScorePage::SCENARIO; + addHighScores(); + addButtons(); + redraw(); } void CHighScoreScreen::buttonResetClick() { - CInfoWindow::showYesNoDialog( - CGI->generaltexth->allTexts[666], - {}, - [this]() - { - Settings entry = persistentStorage.write["highscore"]; - entry->clear(); - addHighScores(); - addButtons(); - redraw(); - }, - 0 - ); + CInfoWindow::showYesNoDialog( + CGI->generaltexth->allTexts[666], + {}, + [this]() + { + Settings entry = persistentStorage.write["highscore"]; + entry->clear(); + addHighScores(); + addButtons(); + redraw(); + }, + 0 + ); } void CHighScoreScreen::buttonExitClick() { - close(); + close(); } CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) : CWindowObject(BORDERED), won(won), calc(calc) { - addUsedEvents(LCLICK); + addUsedEvents(LCLICK); - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 800, 600)); updateShadow(); - if(won) - { - int border = 100; - int textareaW = ((pos.w - 2 * border) / 4); - std::vector t = { "438", "439", "440", "441", "676" }; - for (int i = 0; i < 5; i++) - texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); + if(won) + { + int border = 100; + int textareaW = ((pos.w - 2 * border) / 4); + std::vector t = { "438", "439", "440", "441", "676" }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); - std::string creatureName = (calc.calculate().cheater) ? CGI->generaltexth->translate("core.genrltxt.260") : (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(calc.calculate().total, calc.isCampaign)]->getNameSingularTranslated(); - t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), creatureName }; - for (int i = 0; i < 5; i++) - texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); + std::string creatureName = (calc.calculate().cheater) ? CGI->generaltexth->translate("core.genrltxt.260") : (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(calc.calculate().total, calc.isCampaign)]->getNameSingularTranslated(); + t = { std::to_string(calc.calculate().sumDays), std::to_string(calc.calculate().basic), CGI->generaltexth->translate("core.arraytxt." + std::to_string((142 + calc.parameters[0].difficulty))), std::to_string(calc.calculate().total), creatureName }; + for (int i = 0; i < 5; i++) + texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 530, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, t[i])); - CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); - } - else - CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); + CCS->musich->playMusic(AudioPath::builtin("music/Win Scenario"), true, true); + } + else + CCS->musich->playMusic(AudioPath::builtin("music/UltimateLose"), false, true); - video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; + video = won ? "HSANIM.SMK" : "LOSEGAME.SMK"; } -void CHighScoreInputScreen::addEntry(std::string text) { - for (int i = 0; i < 11; i++) - { - JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; - - if(node["points"].isNull() || node["points"].Integer() <= calc.calculate().total) - { - // move following entries down - for (int j = 10; j > i; j--) - { - JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j)]; - Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j + 1)]; - entry->Struct() = node.Struct(); - } +int CHighScoreInputScreen::addEntry(std::string text) { + for (int i = 0; i < 11; i++) + { + if(calc.calculate().cheater) + i = 10; - Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["player"]; - entry->String() = text; - if(calc.isCampaign) - { - Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; - entry2->String() = calc.parameters[0].campaign; - } - else - { - Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; - entry3->String() = calc.parameters[0].land; - } - Settings entry4 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["days"]; - entry4->Integer() = calc.calculate().sumDays; - Settings entry5 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["points"]; - entry5->Integer() = calc.calculate().total; + JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; + + if(node["points"].isNull() || node["points"].Integer() <= calc.calculate().total) + { + // move following entries down + for (int j = 10; j + 1 >= i; j--) + { + JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j - 1)]; + Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j)]; + entry->Struct() = node.Struct(); + } - Settings entry6 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["datetime"]; - entry6->String() = vstd::getFormattedDateTime(std::time(0)); + Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["player"]; + entry->String() = text; + if(calc.isCampaign) + { + Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; + entry2->String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; + } + else + { + Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; + entry3->String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; + } + Settings entry4 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["days"]; + entry4->Integer() = calc.calculate().sumDays; + Settings entry5 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["points"]; + entry5->Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; - return; - } - } + Settings entry6 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["datetime"]; + entry6->String() = vstd::getFormattedDateTime(std::time(0)); + + return i; + } + } + + return -1; } void CHighScoreInputScreen::show(Canvas & to) { CCS->videoh->update(pos.x, pos.y, to.getInternalSurface(), true, false, - [&]() - { - if(won) - { - CCS->videoh->close(); - video = "HSLOOP.SMK"; - CCS->videoh->open(VideoPath::builtin(video)); - } - else - close(); - }); - redraw(); + [&]() + { + if(won) + { + CCS->videoh->close(); + video = "HSLOOP.SMK"; + CCS->videoh->open(VideoPath::builtin(video)); + } + else + close(); + }); + redraw(); CIntObject::show(to); } void CHighScoreInputScreen::activate() { - CCS->videoh->open(VideoPath::builtin(video)); + CCS->videoh->open(VideoPath::builtin(video)); CIntObject::activate(); } void CHighScoreInputScreen::deactivate() { - CCS->videoh->close(); + CCS->videoh->close(); CIntObject::deactivate(); } void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - if(!won) - { - close(); - return; - } + if(!won) + { + close(); + return; + } - if(!input) - { - input = std::make_shared( - [&] (std::string text) - { - if(!text.empty()) - { - addEntry(text); - close(); - GH.windows().createAndPushWindow(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO); - } - else - close(); - }); - } + if(!input) + { + input = std::make_shared( + [&] (std::string text) + { + if(!text.empty()) + { + int pos = addEntry(text); + close(); + GH.windows().createAndPushWindow(calc.isCampaign ? CHighScoreScreen::HighScorePage::CAMPAIGN : CHighScoreScreen::HighScorePage::SCENARIO, pos); + } + else + close(); + }); + } } CHighScoreInput::CHighScoreInput(std::function readyCB) : CWindowObject(0), ready(readyCB) { - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 232, 212)); updateShadow(); - background = std::make_shared(ImagePath::builtin("HIGHNAME")); - text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); + background = std::make_shared(ImagePath::builtin("HIGHNAME")); + text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); - buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); - buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); + buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); - textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); - textInput->setText(settings["general"]["playerName"].String()); + textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); + textInput->setText(settings["general"]["playerName"].String()); } void CHighScoreInput::okay() { - ready(textInput->getText()); + ready(textInput->getText()); } void CHighScoreInput::abort() { - ready(""); + ready(""); } \ No newline at end of file diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 3fa402000..d622df7a8 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -19,81 +19,83 @@ class CTextInput; class HighScoreParameter { public: - int difficulty; - int day; - int townAmount; - bool usedCheat; - bool hasGrail; - bool allDefeated; - std::string campaign; - std::string land; + int difficulty; + int day; + int townAmount; + bool usedCheat; + bool hasGrail; + bool allDefeated; + std::string campaign; + std::string land; }; class HighScoreCalculation { public: - std::vector parameters = std::vector(); - bool isCampaign = false; + std::vector parameters = std::vector(); + bool isCampaign = false; - auto calculate(); - static CreatureID getCreatureForPoints(int points, bool campaign); + auto calculate(); + static CreatureID getCreatureForPoints(int points, bool campaign); }; class CHighScoreScreen : public CWindowObject { public: - enum HighScorePage { SCENARIO, CAMPAIGN }; + enum HighScorePage { SCENARIO, CAMPAIGN }; private: - void addButtons(); - void addHighScores(); - - void buttonCampaginClick(); - void buttonStandardClick(); - void buttonResetClick(); - void buttonExitClick(); + void addButtons(); + void addHighScores(); + + void buttonCampaginClick(); + void buttonStandardClick(); + void buttonResetClick(); + void buttonExitClick(); - HighScorePage highscorepage; + void showPopupWindow(const Point & cursorPosition) override; - std::shared_ptr background; - std::vector> buttons; - std::vector> texts; - std::vector> images; + HighScorePage highscorepage; - int highlighted; + std::shared_ptr background; + std::vector> buttons; + std::vector> texts; + std::vector> images; + + int highlighted; public: CHighScoreScreen(HighScorePage highscorepage = HighScorePage::SCENARIO, int highlighted = -1); }; class CHighScoreInput : public CWindowObject { - std::shared_ptr background; - std::shared_ptr text; - std::shared_ptr buttonOk; + std::shared_ptr background; + std::shared_ptr text; + std::shared_ptr buttonOk; std::shared_ptr buttonCancel; std::shared_ptr statusBar; - std::shared_ptr textInput; + std::shared_ptr textInput; - std::function ready; - - void okay(); - void abort(); + std::function ready; + + void okay(); + void abort(); public: CHighScoreInput(std::function readyCB); }; class CHighScoreInputScreen : public CWindowObject { - std::vector> texts; - std::shared_ptr input; + std::vector> texts; + std::shared_ptr input; - std::string video; - bool won; - HighScoreCalculation calc; + std::string video; + bool won; + HighScoreCalculation calc; public: CHighScoreInputScreen(bool won, HighScoreCalculation calc); - void addEntry(std::string text); + int addEntry(std::string text); void show(Canvas & to) override; void activate() override; From 06895e6733f2b8d9508702dea6432fbae35b0c53 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 15:11:48 +0200 Subject: [PATCH 0535/1248] fine tuning --- client/mainmenu/CHighScoreScreen.cpp | 33 ++++++++++++++++------------ client/mainmenu/CHighScoreScreen.h | 1 + 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index f24a5ea50..b2f2ad834 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -138,22 +138,22 @@ void CHighScoreScreen::addHighScores() // Header texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); - texts.push_back(std::make_shared(220, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); + texts.push_back(std::make_shared(225, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); if(highscorepage == HighScorePage::SCENARIO) { - texts.push_back(std::make_shared(400, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); - texts.push_back(std::make_shared(555, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); - texts.push_back(std::make_shared(625, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); + texts.push_back(std::make_shared(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); + texts.push_back(std::make_shared(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); } else { - texts.push_back(std::make_shared(410, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); - texts.push_back(std::make_shared(590, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); + texts.push_back(std::make_shared(592, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); } // Content - int y = 65; + int y = 66; auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; for (int i = 0; i < 11; i++) { @@ -163,22 +163,22 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); std::string tmp = curData["player"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); - texts.push_back(std::make_shared(220, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(225, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); if(highscorepage == HighScorePage::SCENARIO) { std::string tmp = curData["land"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); - texts.push_back(std::make_shared(400, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - texts.push_back(std::make_shared(555, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); - texts.push_back(std::make_shared(625, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(557, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); + texts.push_back(std::make_shared(627, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } else { std::string tmp = curData["campaign"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); - texts.push_back(std::make_shared(410, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); - texts.push_back(std::make_shared(590, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); + texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); + texts.push_back(std::make_shared(592, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } if(curData["points"].Integer() > 0) @@ -228,7 +228,7 @@ void CHighScoreScreen::buttonExitClick() CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) : CWindowObject(BORDERED), won(won), calc(calc) { - addUsedEvents(LCLICK); + addUsedEvents(LCLICK | KEYBOARD); OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 800, 600)); @@ -358,6 +358,11 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) } } +void CHighScoreInputScreen::keyPressed(EShortcut key) +{ + clickPressed(Point()); +} + CHighScoreInput::CHighScoreInput(std::function readyCB) : CWindowObject(0), ready(readyCB) { diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index d622df7a8..b3e1ac449 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -101,4 +101,5 @@ public: void activate() override; void deactivate() override; void clickPressed(const Point & cursorPosition) override; + void keyPressed(EShortcut key) override; }; \ No newline at end of file From ca96749c45a0329a95d96777324acdac70ae648f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 16:28:28 +0200 Subject: [PATCH 0536/1248] workaround for missing video --- client/mainmenu/CHighScoreScreen.cpp | 11 ++++++++++- client/mainmenu/CHighScoreScreen.h | 3 +++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index b2f2ad834..8d6a0e624 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -17,6 +17,7 @@ #include "../widgets/TextControls.h" #include "../widgets/Buttons.h" #include "../widgets/Images.h" +#include "../widgets/MiscWidgets.h" #include "../windows/InfoWindows.h" #include "../render/Canvas.h" @@ -234,6 +235,8 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc pos = center(Rect(0, 0, 800, 600)); updateShadow(); + background = std::make_shared(Rect(0, 0, pos.w, pos.h), Colors::BLACK); + if(won) { int border = 100; @@ -321,7 +324,13 @@ void CHighScoreInputScreen::show(Canvas & to) void CHighScoreInputScreen::activate() { - CCS->videoh->open(VideoPath::builtin(video)); + if(!CCS->videoh->open(VideoPath::builtin(video))) + { + if(!won) + close(); + } + else + background = nullptr; CIntObject::activate(); } diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index b3e1ac449..e995e7b58 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -16,6 +16,8 @@ class CMultiLineLabel; class CAnimImage; class CTextInput; +class TransparentFilledRectangle; + class HighScoreParameter { public: @@ -88,6 +90,7 @@ class CHighScoreInputScreen : public CWindowObject { std::vector> texts; std::shared_ptr input; + std::shared_ptr background; std::string video; bool won; From 455ead7b4ae75f4373264b0a4fd056147b1bc170 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 17:27:00 +0200 Subject: [PATCH 0537/1248] fix datetime format (android) --- .../src/main/java/eu/vcmi/vcmi/NativeMethods.java | 9 +++++++++ lib/vstd/DateUtils.cpp | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 55ca15691..107b8b729 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -153,6 +153,15 @@ public class NativeMethods } } + @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) + public static String getFormattedDateTime() + { + String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); + String currentTime = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()); + + return currentDate + " " + currentTime; + } + private static void internalProgressDisplay(final boolean show) { final Context ctx = SDL.getContext(); diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index da098aa20..ebf7a5722 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -1,6 +1,10 @@ #include "StdInc.h" #include +#if defined(VCMI_ANDROID) +#include "../CAndroidVMHelper.h" +#endif + VCMI_LIB_NAMESPACE_BEGIN namespace vstd @@ -8,6 +12,11 @@ namespace vstd DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) { +#if defined(VCMI_ANDROID) + CAndroidVMHelper vmHelper; + return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); +#endif + std::tm tm = *std::localtime(&dt); std::stringstream s; try From 63b91f7b87804e5d75a554970adcb75ab4b6092a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 17:41:36 +0200 Subject: [PATCH 0538/1248] imports --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 107b8b729..f2110e0e7 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -17,6 +17,9 @@ import org.libsdl.app.SDLActivity; import java.io.File; import java.lang.ref.WeakReference; +import java.util.Date;; +import java.util.Locale; +import java.text.SimpleDateFormat import eu.vcmi.vcmi.util.Log; From 006431c8989b9116314711ab4fb46a338ec20674 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 17:50:42 +0200 Subject: [PATCH 0539/1248] fix --- .../vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index f2110e0e7..ee62d9574 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -17,9 +17,9 @@ import org.libsdl.app.SDLActivity; import java.io.File; import java.lang.ref.WeakReference; -import java.util.Date;; +import java.util.Date; import java.util.Locale; -import java.text.SimpleDateFormat +import java.text.SimpleDateFormat; import eu.vcmi.vcmi.util.Log; From 09cf8376db9c86c02eb83985cb17edbf58b25507 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 18:58:32 +0200 Subject: [PATCH 0540/1248] date is already with time --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index ee62d9574..756fd510f 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -160,9 +160,8 @@ public class NativeMethods public static String getFormattedDateTime() { String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); - String currentTime = new SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(new Date()); - return currentDate + " " + currentTime; + return currentDate; } private static void internalProgressDisplay(final boolean show) From a4769782c515dbc6b59968fb1d7d809f8d847f06 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:01:18 +0200 Subject: [PATCH 0541/1248] correct format --- lib/vstd/DateUtils.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index ebf7a5722..02f0cd377 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -13,8 +13,8 @@ namespace vstd DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) { #if defined(VCMI_ANDROID) - CAndroidVMHelper vmHelper; - return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); + CAndroidVMHelper vmHelper; + return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); #endif std::tm tm = *std::localtime(&dt); From 6f8b62d77dded438cc63d1b9f0e8126b6b44feb2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 20:41:30 +0200 Subject: [PATCH 0542/1248] codereview --- client/CPlayerInterface.cpp | 14 +++---- client/CServerHandler.cpp | 14 +++---- client/CServerHandler.h | 2 +- client/CVideoHandler.cpp | 6 +-- client/CVideoHandler.h | 2 +- client/mainmenu/CHighScoreScreen.cpp | 59 +++++++++++----------------- client/mainmenu/CHighScoreScreen.h | 5 +-- lib/CPlayerState.cpp | 3 +- lib/CPlayerState.h | 2 + lib/NetPacksLib.cpp | 1 + 10 files changed, 50 insertions(+), 58 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 80dc8475a..5bd29f8e4 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1690,7 +1690,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) param.difficulty = cb->getStartInfo()->difficulty; param.day = cb->getDate(); param.townAmount = cb->howManyTowns(); - param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->enteredWinningCheatCode; + param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated; param.hasGrail = false; for(const CGHeroInstance * h : cb->getHeroesInfo()) if(h->hasArt(ArtifactID::GRAIL)) @@ -1698,27 +1698,27 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) param.allDefeated = true; for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) { - auto ps = cb->getPlayerState(player); + auto ps = cb->getPlayerState(player, false); if(ps && player != *cb->getPlayerID()) if(!ps->checkVanquished()) param.allDefeated = false; } param.land = cb->getMapHeader()->name; - HighScoreCalculation calc; - calc.parameters.push_back(param); - calc.isCampaign = false; + HighScoreCalculation highScoreCalc; + highScoreCalc.parameters.push_back(param); + highScoreCalc.isCampaign = false; if(won && cb->getStartInfo()->campState) CSH->startCampaignScenario(param, cb->getStartInfo()->campState); else { GH.dispatchMainThread( - [won, calc]() + [won, highScoreCalc]() { CSH->endGameplay(); GH.defActionsDef = 63; CMM->menu->switchToTab("main"); - GH.windows().createAndPushWindow(won, calc); + GH.windows().createAndPushWindow(won, highScoreCalc); } ); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index f8758cc00..96ec74205 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -600,7 +600,7 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta CMM->disable(); client = new CClient(); - calc = nullptr; + highScoreCalc = nullptr; switch(si->mode) { @@ -678,14 +678,14 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared if (!cs) ourCampaign = si->campState; - if(calc == nullptr) + if(highScoreCalc == nullptr) { - calc = std::make_shared(); - calc->isCampaign = true; - calc->parameters.clear(); + highScoreCalc = std::make_shared(); + highScoreCalc->isCampaign = true; + highScoreCalc->parameters.clear(); } param.campaign = cs->getName(); - calc->parameters.push_back(param); + highScoreCalc->parameters.push_back(param); GH.dispatchMainThread([ourCampaign, this]() { @@ -709,7 +709,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared else { CMM->openCampaignScreen(ourCampaign->campaignSet); - GH.windows().createAndPushWindow(true, *calc); + GH.windows().createAndPushWindow(true, *highScoreCalc); } }; if(epilogue.hasPrologEpilog) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 0b8b3f146..9fcccc20c 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -89,7 +89,7 @@ class CServerHandler : public IServerAPI, public LobbyInfo std::vector myNames; - std::shared_ptr calc; + std::shared_ptr highScoreCalc; void threadHandleConnection(); void threadRunServer(); diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 954a05455..10bd54bf0 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -349,7 +349,7 @@ void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) show(x, y, dst, update); } -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function restart) +void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) { if (sws == nullptr) return; @@ -368,8 +368,8 @@ void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, boo show(x,y,dst,update); else { - if(restart) - restart(); + if(onVideoRestart) + onVideoRestart(); VideoPath filenameToReopen = fname; // create copy to backup this->fname open(filenameToReopen); nextFrame(); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 11317859b..d447d64e6 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -101,7 +101,7 @@ public: void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 8d6a0e624..d16715f57 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -122,8 +122,8 @@ void CHighScoreScreen::addButtons() buttons.clear(); - buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaginClick(); })); - buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonStandardClick(); })); + buttons.push_back(std::make_shared(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); })); + buttons.push_back(std::make_shared(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); })); buttons.push_back(std::make_shared(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); })); buttons.push_back(std::make_shared(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); })); } @@ -138,19 +138,19 @@ void CHighScoreScreen::addHighScores() images.clear(); // Header - texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); - texts.push_back(std::make_shared(225, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); + texts.push_back(std::make_shared(115, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.433"))); // rank + texts.push_back(std::make_shared(225, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.434"))); // player if(highscorepage == HighScorePage::SCENARIO) { - texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); - texts.push_back(std::make_shared(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); - texts.push_back(std::make_shared(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.435"))); // land + texts.push_back(std::make_shared(557, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.436"))); // days + texts.push_back(std::make_shared(627, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score } else { - texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); - texts.push_back(std::make_shared(592, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); + texts.push_back(std::make_shared(405, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.672"))); // campaign + texts.push_back(std::make_shared(592, 20, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.75"))); // score } // Content @@ -161,7 +161,7 @@ void CHighScoreScreen::addHighScores() auto & curData = data[std::to_string(i)]; ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; - texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i+1))); + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i + 1))); std::string tmp = curData["player"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); texts.push_back(std::make_shared(225, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); @@ -187,7 +187,7 @@ void CHighScoreScreen::addHighScores() } } -void CHighScoreScreen::buttonCampaginClick() +void CHighScoreScreen::buttonCampaignClick() { highscorepage = HighScorePage::CAMPAIGN; addHighScores(); @@ -195,7 +195,7 @@ void CHighScoreScreen::buttonCampaginClick() redraw(); } -void CHighScoreScreen::buttonStandardClick() +void CHighScoreScreen::buttonScenarioClick() { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; highscorepage = HighScorePage::SCENARIO; @@ -241,7 +241,7 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc { int border = 100; int textareaW = ((pos.w - 2 * border) / 4); - std::vector t = { "438", "439", "440", "441", "676" }; + std::vector t = { "438", "439", "440", "441", "676" }; // time, score, difficulty, final score, rank for (int i = 0; i < 5; i++) texts.push_back(std::make_shared(Rect(textareaW * i + border - (textareaW / 2), 450, textareaW, 100), FONT_HIGH_SCORE, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt." + t[i]))); @@ -264,37 +264,27 @@ int CHighScoreInputScreen::addEntry(std::string text) { if(calc.calculate().cheater) i = 10; - JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; + JsonNode baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"]; - if(node["points"].isNull() || node["points"].Integer() <= calc.calculate().total) + if(baseNode[std::to_string(i)]["points"].isNull() || baseNode[std::to_string(i)]["points"].Integer() <= calc.calculate().total) { // move following entries down for (int j = 10; j + 1 >= i; j--) { - JsonNode node = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j - 1)]; + JsonNode node = baseNode[std::to_string(j - 1)]; Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j)]; entry->Struct() = node.Struct(); } - Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["player"]; - entry->String() = text; + Settings currentEntry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; + currentEntry["player"].String() = text; if(calc.isCampaign) - { - Settings entry2 = persistentStorage.write["highscore"]["campaign"][std::to_string(i)]["campaign"]; - entry2->String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; - } + currentEntry["campaign"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; else - { - Settings entry3 = persistentStorage.write["highscore"]["scenario"][std::to_string(i)]["land"]; - entry3->String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; - } - Settings entry4 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["days"]; - entry4->Integer() = calc.calculate().sumDays; - Settings entry5 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["points"]; - entry5->Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; - - Settings entry6 = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]["datetime"]; - entry6->String() = vstd::getFormattedDateTime(std::time(0)); + currentEntry["land"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; + currentEntry["days"].Integer() = calc.calculate().sumDays; + currentEntry["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; + currentEntry["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); return i; } @@ -373,14 +363,13 @@ void CHighScoreInputScreen::keyPressed(EShortcut key) } CHighScoreInput::CHighScoreInput(std::function readyCB) - : CWindowObject(0), ready(readyCB) + : CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = center(Rect(0, 0, 232, 212)); updateShadow(); - background = std::make_shared(ImagePath::builtin("HIGHNAME")); text = std::make_shared(Rect(15, 15, 202, 202), FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE, CGI->generaltexth->translate("core.genrltxt.96")); buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index e995e7b58..00698d4e1 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -50,8 +50,8 @@ private: void addButtons(); void addHighScores(); - void buttonCampaginClick(); - void buttonStandardClick(); + void buttonCampaignClick(); + void buttonScenarioClick(); void buttonResetClick(); void buttonExitClick(); @@ -71,7 +71,6 @@ public: class CHighScoreInput : public CWindowObject { - std::shared_ptr background; std::shared_ptr text; std::shared_ptr buttonOk; std::shared_ptr buttonCancel; diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index faa021bbe..8061901fa 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN PlayerState::PlayerState() - : color(-1), human(false), enteredWinningCheatCode(false), + : color(-1), human(false), cheated(false), enteredWinningCheatCode(false), enteredLosingCheatCode(false), status(EPlayerStatus::INGAME) { setNodeType(PLAYER); @@ -29,6 +29,7 @@ PlayerState::PlayerState(PlayerState && other) noexcept: human(other.human), team(other.team), resources(other.resources), + cheated(other.cheated), enteredWinningCheatCode(other.enteredWinningCheatCode), enteredLosingCheatCode(other.enteredLosingCheatCode), status(other.status), diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index ac8c1d4c6..560c3d636 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -38,6 +38,7 @@ public: std::vector > dwellings; //used for town growth std::vector quests; //store info about all received quests + bool cheated; bool enteredWinningCheatCode, enteredLosingCheatCode; //if true, this player has entered cheat codes for loss / victory EPlayerStatus status; std::optional daysWithoutCastle; @@ -82,6 +83,7 @@ public: h & visitedObjects; h & status; h & daysWithoutCastle; + h & cheated; h & enteredLosingCheatCode; h & enteredWinningCheatCode; h & static_cast(*this); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5fd1116ad..7a36f2abc 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2509,6 +2509,7 @@ void PlayerCheated::applyGs(CGameState * gs) const gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; + gs->getPlayerState(player)->cheated = true; } void PlayerStartsTurn::applyGs(CGameState * gs) const From c49d38b855ef8e374d9f9477b23081575a185710 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 21:37:40 +0200 Subject: [PATCH 0543/1248] cheats + grail in town --- client/CPlayerInterface.cpp | 3 +++ server/processors/PlayerMessageProcessor.cpp | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 5bd29f8e4..58b8414c7 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1695,6 +1695,9 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) for(const CGHeroInstance * h : cb->getHeroesInfo()) if(h->hasArt(ArtifactID::GRAIL)) param.hasGrail = true; + for(const CGTownInstance * t : cb->getTownInfo()) + if(t->builtBuildings.find(BuildingID::GRAIL)) + param.hasGrail = true; param.allDefeated = true; for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) { diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 6f5358c9c..7c5e1e21d 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -513,7 +513,13 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla assert(callbacks.count(cheatName)); if (callbacks.count(cheatName)) + { + PlayerCheated pc; + pc.player = player; + gameHandler->sendAndApply(&pc); + callbacks.at(cheatName)(); + } } void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const std::string & message) From 8a0565eb9b3b8ec4478af0f7252c30dfb267257d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 22:15:05 +0200 Subject: [PATCH 0544/1248] use vector --- client/CPlayerInterface.cpp | 4 +- client/mainmenu/CHighScoreScreen.cpp | 59 +++++++++++++++------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 58b8414c7..ec1830637 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1695,8 +1695,8 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) for(const CGHeroInstance * h : cb->getHeroesInfo()) if(h->hasArt(ArtifactID::GRAIL)) param.hasGrail = true; - for(const CGTownInstance * t : cb->getTownInfo()) - if(t->builtBuildings.find(BuildingID::GRAIL)) + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->builtBuildings.count(BuildingID::GRAIL)) param.hasGrail = true; param.allDefeated = true; for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index d16715f57..c9775bf60 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -109,7 +109,7 @@ void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) Rect r = Rect(80, 40 + i * 50, 635, 50); if(r.isInside(cursorPosition - pos)) { - std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][std::to_string(i)]["datetime"].String(); + std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][i]["datetime"].String(); if(!tmp.empty()) CRClickPopup::createAndPush(tmp); } @@ -158,7 +158,7 @@ void CHighScoreScreen::addHighScores() auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; for (int i = 0; i < 11; i++) { - auto & curData = data[std::to_string(i)]; + auto & curData = data[i]; ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i + 1))); @@ -259,38 +259,41 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc } int CHighScoreInputScreen::addEntry(std::string text) { - for (int i = 0; i < 11; i++) + std::vector baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"].Vector(); + + auto sortFunctor = [](const JsonNode & left, const JsonNode & right) { - if(calc.calculate().cheater) - i = 10; + return left["points"].Integer() > right["points"].Integer(); + }; - JsonNode baseNode = persistentStorage["highscore"][calc.isCampaign ? "campaign" : "scenario"]; - - if(baseNode[std::to_string(i)]["points"].isNull() || baseNode[std::to_string(i)]["points"].Integer() <= calc.calculate().total) + JsonNode newNode = JsonNode(); + newNode["player"].String() = text; + if(calc.isCampaign) + newNode["campaign"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; + else + newNode["land"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; + newNode["days"].Integer() = calc.calculate().sumDays; + newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; + newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); + newNode["posFlag"].Bool() = true; + + baseNode.push_back(newNode); + boost::range::sort(baseNode, sortFunctor); + + Settings s = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"]; + int pos = -1; + for (int i = 0; i < baseNode.size(); i++) + { + if(!baseNode[i]["posFlag"].isNull()) { - // move following entries down - for (int j = 10; j + 1 >= i; j--) - { - JsonNode node = baseNode[std::to_string(j - 1)]; - Settings entry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(j)]; - entry->Struct() = node.Struct(); - } - - Settings currentEntry = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"][std::to_string(i)]; - currentEntry["player"].String() = text; - if(calc.isCampaign) - currentEntry["campaign"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; - else - currentEntry["land"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; - currentEntry["days"].Integer() = calc.calculate().sumDays; - currentEntry["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; - currentEntry["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); - - return i; + baseNode[i]["posFlag"].clear(); + pos = i; } } + + s->Vector() = baseNode; - return -1; + return pos; } void CHighScoreInputScreen::show(Canvas & to) From 96df11a6f2ccc6e40cb8ac5fbb55587829657056 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 22:45:38 +0200 Subject: [PATCH 0545/1248] always show actual entry --- client/mainmenu/CHighScoreScreen.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index c9775bf60..01ca631a5 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -158,10 +158,12 @@ void CHighScoreScreen::addHighScores() auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; for (int i = 0; i < 11; i++) { - auto & curData = data[i]; - ColorRGBA color = (i == highlighted) ? Colors::YELLOW : Colors::WHITE; + bool currentGameNotInListEntry = (i == 10 && highlighted > 10); + auto & curData = data[currentGameNotInListEntry ? highlighted : i]; - texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(i + 1))); + ColorRGBA color = (i == highlighted || currentGameNotInListEntry) ? Colors::YELLOW : Colors::WHITE; + + texts.push_back(std::make_shared(115, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string((currentGameNotInListEntry ? highlighted : i) + 1))); std::string tmp = curData["player"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 13)); texts.push_back(std::make_shared(225, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); From 41b03e7c5bfa354989f3fbeca16862a68ac56243 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 23:11:17 +0200 Subject: [PATCH 0546/1248] fixed some edge cases --- client/mainmenu/CHighScoreScreen.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 01ca631a5..9521c00d3 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -106,10 +106,12 @@ void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) { for (int i = 0; i < 11; i++) { + bool currentGameNotInListEntry = (i == 10 && highlighted > 10); + Rect r = Rect(80, 40 + i * 50, 635, 50); if(r.isInside(cursorPosition - pos)) { - std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][i]["datetime"].String(); + std::string tmp = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"][currentGameNotInListEntry ? highlighted : i]["datetime"].String(); if(!tmp.empty()) CRClickPopup::createAndPush(tmp); } @@ -184,7 +186,7 @@ void CHighScoreScreen::addHighScores() texts.push_back(std::make_shared(592, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); } - if(curData["points"].Integer() > 0) + if(curData["points"].Integer() > 0 && curData["points"].Integer() <= ((highscorepage == HighScorePage::CAMPAIGN) ? 2500 : 500)) images.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), (*CGI->creh)[HighScoreCalculation::getCreatureForPoints(curData["points"].Integer(), highscorepage == HighScorePage::CAMPAIGN)]->getIconIndex(), 0, 670, y - 15 + i * 50)); } } @@ -265,6 +267,8 @@ int CHighScoreInputScreen::addEntry(std::string text) { auto sortFunctor = [](const JsonNode & left, const JsonNode & right) { + if(left["points"].Integer() == right["points"].Integer()) + return left["posFlag"].Integer() > right["posFlag"].Integer(); return left["points"].Integer() > right["points"].Integer(); }; @@ -282,7 +286,6 @@ int CHighScoreInputScreen::addEntry(std::string text) { baseNode.push_back(newNode); boost::range::sort(baseNode, sortFunctor); - Settings s = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"]; int pos = -1; for (int i = 0; i < baseNode.size(); i++) { @@ -292,7 +295,8 @@ int CHighScoreInputScreen::addEntry(std::string text) { pos = i; } } - + + Settings s = persistentStorage.write["highscore"][calc.isCampaign ? "campaign" : "scenario"]; s->Vector() = baseNode; return pos; From e69b91fbdd238829c3096f82a3d1ccee446b17d3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 23 Sep 2023 23:44:12 +0200 Subject: [PATCH 0547/1248] fix layer problem --- client/mainmenu/CMainMenu.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 119763fed..09d064f5d 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -108,7 +108,12 @@ std::shared_ptr CMenuScreen::createTab(size_t index) void CMenuScreen::show(Canvas & to) { if(!config["video"].isNull()) + { CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false); + tabs->setRedrawParent(false); + tabs->redraw(); + tabs->setRedrawParent(true); + } CIntObject::show(to); } From b143c2786fcbc80c4ae98748149500b6b27ce12e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 00:58:48 +0200 Subject: [PATCH 0548/1248] use dimming to hide screen --- client/adventureMap/AdventureMapInterface.cpp | 8 +++++++- client/adventureMap/AdventureMapInterface.h | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 3cf2209fd..e2d361f08 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -50,7 +50,8 @@ AdventureMapInterface::AdventureMapInterface(): mapAudio(new MapAudioPlayer()), spellBeingCasted(nullptr), scrollingWasActive(false), - scrollingWasBlocked(false) + scrollingWasBlocked(false), + isHotseatMessage(false) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos.x = pos.y = 0; @@ -172,6 +173,8 @@ void AdventureMapInterface::dim(Canvas & to) if (!casted && !window->isPopupWindow()) { int backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); + if(isHotseatMessage) + backgroundDimLevel = 255; Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); if(backgroundDimLevel > 0) @@ -335,6 +338,7 @@ void AdventureMapInterface::onMapTilesChanged(boost::optional Date: Sun, 24 Sep 2023 01:30:03 +0200 Subject: [PATCH 0549/1248] no invalid json --- lib/CConfigHandler.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index af4a99eca..c741e0e5e 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -65,7 +65,11 @@ void SettingsStorage::init(const std::string & dataFilename, const std::string & // Probably new install. Create config file to save settings to if (!CResourceHandler::get("local")->existsResource(confName)) + { CResourceHandler::get("local")->createResource(dataFilename); + if(schema.empty()) + invalidateNode(std::vector()); + } if(!schema.empty()) { From 68e536c290c970d5176cbf8f64433f25d0b3fc9f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 02:00:42 +0200 Subject: [PATCH 0550/1248] code review --- client/mainmenu/CHighScoreScreen.cpp | 54 ++++++++++------------------ client/mainmenu/CHighScoreScreen.h | 2 ++ 2 files changed, 21 insertions(+), 35 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 9521c00d3..3fa3b65fa 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -34,46 +34,30 @@ auto HighScoreCalculation::calculate() { - struct result + struct Result { - int basic; - int total; - int sumDays; - bool cheater; + int basic = 0; + int total = 0; + int sumDays = 0; + bool cheater = false; }; - std::vector scoresBasic; - std::vector scoresTotal; - double sumBasic = 0; - double sumTotal = 0; - int sumDays = 0; - bool cheater = false; + Result firstResult, summary; + const std::array difficultyMultipliers{0.8, 1.0, 1.3, 1.6, 2.0}; for(auto & param : parameters) { double tmp = 200 - (param.day + 10) / (param.townAmount + 5) + (param.allDefeated ? 25 : 0) + (param.hasGrail ? 25 : 0); - scoresBasic.push_back(static_cast(tmp)); - sumBasic += tmp; - if(param.difficulty == 0) - tmp *= 0.8; - if(param.difficulty == 1) - tmp *= 1.0; - if(param.difficulty == 2) - tmp *= 1.3; - if(param.difficulty == 3) - tmp *= 1.6; - if(param.difficulty == 4) - tmp *= 2.0; - scoresTotal.push_back(static_cast(tmp)); - sumTotal += tmp; - sumDays += param.day; - if(param.usedCheat) - cheater = true; + firstResult = Result{static_cast(tmp), static_cast(tmp * difficultyMultipliers.at(param.difficulty)), param.day, param.usedCheat}; + summary.basic += firstResult.basic * 5.0 / parameters.size(); + summary.total += firstResult.total * 5.0 / parameters.size(); + summary.sumDays += firstResult.sumDays; + summary.cheater |= firstResult.cheater; } - if(scoresBasic.size() == 1) - return result { scoresBasic[0], scoresTotal[0], sumDays , cheater}; + if(parameters.size() == 1) + return firstResult; - return result { static_cast((sumBasic / parameters.size()) * 5.0), static_cast((sumTotal / parameters.size()) * 5.0), sumDays, cheater }; + return summary; } CreatureID HighScoreCalculation::getCreatureForPoints(int points, bool campaign) @@ -104,9 +88,9 @@ CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted) void CHighScoreScreen::showPopupWindow(const Point & cursorPosition) { - for (int i = 0; i < 11; i++) + for (int i = 0; i < screenRows; i++) { - bool currentGameNotInListEntry = (i == 10 && highlighted > 10); + bool currentGameNotInListEntry = i == (screenRows - 1) && highlighted > (screenRows - 1); Rect r = Rect(80, 40 + i * 50, 635, 50); if(r.isInside(cursorPosition - pos)) @@ -158,9 +142,9 @@ void CHighScoreScreen::addHighScores() // Content int y = 66; auto & data = persistentStorage["highscore"][highscorepage == HighScorePage::SCENARIO ? "scenario" : "campaign"]; - for (int i = 0; i < 11; i++) + for (int i = 0; i < screenRows; i++) { - bool currentGameNotInListEntry = (i == 10 && highlighted > 10); + bool currentGameNotInListEntry = (i == (screenRows - 1) && highlighted > (screenRows - 1)); auto & curData = data[currentGameNotInListEntry ? highlighted : i]; ColorRGBA color = (i == highlighted || currentGameNotInListEntry) ? Colors::YELLOW : Colors::WHITE; diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 00698d4e1..6c9ae2513 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -64,6 +64,8 @@ private: std::vector> texts; std::vector> images; + const int screenRows = 11; + int highlighted; public: CHighScoreScreen(HighScorePage highscorepage = HighScorePage::SCENARIO, int highlighted = -1); From c73c9d773ceb19d17e815375b334fc30d9aca8bb Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 24 Sep 2023 03:15:10 +0200 Subject: [PATCH 0551/1248] Fix issue with impossible room ready status --- launcher/lobby/lobby_moc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/lobby/lobby_moc.cpp b/launcher/lobby/lobby_moc.cpp index 3498a99fc..308148d3c 100644 --- a/launcher/lobby/lobby_moc.cpp +++ b/launcher/lobby/lobby_moc.cpp @@ -153,11 +153,11 @@ void Lobby::serverCommand(const ServerCommand & command) try case JOINED: case KICKED: protocolAssert(args.size() == 2); - session = ""; - ui->chatWidget->setSession(session); if(args[1] == username) { hostModsMap.clear(); + session = ""; + ui->chatWidget->setSession(session); ui->buttonReady->setText("Ready"); ui->optNewGame->setChecked(true); session = args[0]; From 72e4941982a190e49059f5b41cc73669a727aade Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 12:08:27 +0200 Subject: [PATCH 0552/1248] dimlevel as class variable --- client/adventureMap/AdventureMapInterface.cpp | 10 ++++------ client/adventureMap/AdventureMapInterface.h | 4 ++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index e2d361f08..7bd085cd5 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -51,7 +51,7 @@ AdventureMapInterface::AdventureMapInterface(): spellBeingCasted(nullptr), scrollingWasActive(false), scrollingWasBlocked(false), - isHotseatMessage(false) + backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos.x = pos.y = 0; @@ -172,9 +172,6 @@ void AdventureMapInterface::dim(Canvas & to) std::shared_ptr casted = std::dynamic_pointer_cast(window); if (!casted && !window->isPopupWindow()) { - int backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); - if(isHotseatMessage) - backgroundDimLevel = 255; Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); if(backgroundDimLevel > 0) @@ -338,7 +335,8 @@ void AdventureMapInterface::onMapTilesChanged(boost::optional Date: Sun, 24 Sep 2023 12:54:35 +0200 Subject: [PATCH 0553/1248] another approach --- .../main/java/eu/vcmi/vcmi/NativeMethods.java | 11 ----- lib/Languages.h | 43 ++++++++++--------- lib/vstd/DateUtils.cpp | 16 +++---- 3 files changed, 31 insertions(+), 39 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 756fd510f..55ca15691 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -17,9 +17,6 @@ import org.libsdl.app.SDLActivity; import java.io.File; import java.lang.ref.WeakReference; -import java.util.Date; -import java.util.Locale; -import java.text.SimpleDateFormat; import eu.vcmi.vcmi.util.Log; @@ -156,14 +153,6 @@ public class NativeMethods } } - @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) - public static String getFormattedDateTime() - { - String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); - - return currentDate; - } - private static void internalProgressDisplay(final boolean show) { final Context ctx = SDL.getContext(); diff --git a/lib/Languages.h b/lib/Languages.h index d00ce6ff7..a8b146f58 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -57,6 +57,9 @@ struct Options /// primary IETF language tag std::string tagIETF; + /// POSIX locale + std::string locale; + /// VCMI supports translations into this language bool hasTranslation = false; }; @@ -65,27 +68,27 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, - { "french", "French", "Français", "CP1252", "fr", true }, - { "german", "German", "Deutsch", "CP1252", "de", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", true }, - { "korean", "Korean", "한국어", "CP949", "ko", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", "cs_CZ.UTF-8", true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", "zh_CN.UTF-8", true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", "en_US.UTF-8", true }, + { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fi_FI.UTF-8", true }, + { "french", "French", "Français", "CP1252", "fr", "fr_FR.UTF-8", true }, + { "german", "German", "Deutsch", "CP1252", "de", "de_DE.UTF-8", true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hu_HU.UTF-8", true }, + { "italian", "Italian", "Italiano", "CP1250", "it", "it_IT.UTF-8", true }, + { "korean", "Korean", "한국어", "CP949", "ko", "ko_KR.UTF-8", true }, + { "polish", "Polish", "Polski", "CP1250", "pl", "pl_PL.UTF-8", true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", "pt_BR.UTF-8", true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", "ru_RU.UTF-8", true }, + { "spanish", "Spanish", "Español", "CP1252", "es", "es_ES.UTF-8", true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", "sv_SE.UTF-8", true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tr_TR.UTF-8", true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "uk_UA.UTF-8", true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vi_VN.UTF-8", true }, // Fan translation uses special encoding - { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", false } + { "other_cp1250", "Other (East European)", "", "CP1250", "", "", false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", "", false } } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index 02f0cd377..a071923b5 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -1,9 +1,8 @@ #include "StdInc.h" #include -#if defined(VCMI_ANDROID) -#include "../CAndroidVMHelper.h" -#endif +#include "../CConfigHandler.h" +#include "../Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -12,16 +11,17 @@ namespace vstd DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) { -#if defined(VCMI_ANDROID) - CAndroidVMHelper vmHelper; - return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); -#endif + std::string lang = settings["general"]["language"].String(); + std::string locale = Languages::getLanguageOptions(lang).locale; std::tm tm = *std::localtime(&dt); std::stringstream s; try { - s.imbue(std::locale("")); + if(locale.empty()) + s.imbue(std::locale("")); + else + s.imbue(std::locale(locale)); } catch(const std::runtime_error & e) { From e93a04409e50da4abace60d92d49a73ae228b5ef Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 15:02:59 +0200 Subject: [PATCH 0554/1248] Revert "another approach" This reverts commit 2f64457a9a5b447fcc4a0060edeece0e4074b01a. --- .../main/java/eu/vcmi/vcmi/NativeMethods.java | 11 +++++ lib/Languages.h | 43 +++++++++---------- lib/vstd/DateUtils.cpp | 16 +++---- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java index 55ca15691..756fd510f 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java @@ -17,6 +17,9 @@ import org.libsdl.app.SDLActivity; import java.io.File; import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.Locale; +import java.text.SimpleDateFormat; import eu.vcmi.vcmi.util.Log; @@ -153,6 +156,14 @@ public class NativeMethods } } + @SuppressWarnings(Const.JNI_METHOD_SUPPRESS) + public static String getFormattedDateTime() + { + String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date()); + + return currentDate; + } + private static void internalProgressDisplay(final boolean show) { final Context ctx = SDL.getContext(); diff --git a/lib/Languages.h b/lib/Languages.h index a8b146f58..d00ce6ff7 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -57,9 +57,6 @@ struct Options /// primary IETF language tag std::string tagIETF; - /// POSIX locale - std::string locale; - /// VCMI supports translations into this language bool hasTranslation = false; }; @@ -68,27 +65,27 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", "cs_CZ.UTF-8", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", "zh_CN.UTF-8", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", "en_US.UTF-8", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", "fi_FI.UTF-8", true }, - { "french", "French", "Français", "CP1252", "fr", "fr_FR.UTF-8", true }, - { "german", "German", "Deutsch", "CP1252", "de", "de_DE.UTF-8", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", "hu_HU.UTF-8", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", "it_IT.UTF-8", true }, - { "korean", "Korean", "한국어", "CP949", "ko", "ko_KR.UTF-8", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", "pl_PL.UTF-8", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", "pt_BR.UTF-8", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", "ru_RU.UTF-8", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", "es_ES.UTF-8", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", "sv_SE.UTF-8", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", "tr_TR.UTF-8", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", "uk_UA.UTF-8", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", "vi_VN.UTF-8", true }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", true }, + { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, + { "french", "French", "Français", "CP1252", "fr", true }, + { "german", "German", "Deutsch", "CP1252", "de", true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, + { "italian", "Italian", "Italiano", "CP1250", "it", true }, + { "korean", "Korean", "한국어", "CP949", "ko", true }, + { "polish", "Polish", "Polski", "CP1250", "pl", true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", true }, + { "spanish", "Spanish", "Español", "CP1252", "es", true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding - { "other_cp1250", "Other (East European)", "", "CP1250", "", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", "", false } + { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", false } } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); diff --git a/lib/vstd/DateUtils.cpp b/lib/vstd/DateUtils.cpp index a071923b5..02f0cd377 100644 --- a/lib/vstd/DateUtils.cpp +++ b/lib/vstd/DateUtils.cpp @@ -1,8 +1,9 @@ #include "StdInc.h" #include -#include "../CConfigHandler.h" -#include "../Languages.h" +#if defined(VCMI_ANDROID) +#include "../CAndroidVMHelper.h" +#endif VCMI_LIB_NAMESPACE_BEGIN @@ -11,17 +12,16 @@ namespace vstd DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt) { - std::string lang = settings["general"]["language"].String(); - std::string locale = Languages::getLanguageOptions(lang).locale; +#if defined(VCMI_ANDROID) + CAndroidVMHelper vmHelper; + return vmHelper.callStaticStringMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "getFormattedDateTime"); +#endif std::tm tm = *std::localtime(&dt); std::stringstream s; try { - if(locale.empty()) - s.imbue(std::locale("")); - else - s.imbue(std::locale(locale)); + s.imbue(std::locale("")); } catch(const std::runtime_error & e) { From 7764dac38629cfcfb13ead8c3183eeefa2323a60 Mon Sep 17 00:00:00 2001 From: utzuro Date: Sun, 24 Sep 2023 22:22:14 +0900 Subject: [PATCH 0555/1248] fix some ukrainian strings in the android launcher --- .../vcmi-app/src/main/res/values-uk/strings.xml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/android/vcmi-app/src/main/res/values-uk/strings.xml b/android/vcmi-app/src/main/res/values-uk/strings.xml index cc65cace0..5ae3cecce 100644 --- a/android/vcmi-app/src/main/res/values-uk/strings.xml +++ b/android/vcmi-app/src/main/res/values-uk/strings.xml @@ -16,8 +16,8 @@ Поточна версія лаунчера: %1$s Не вдалося знайти файли Героїв у \'%1$s\'. Скопіюйте файли Героїв 3 туди вручну або скористайтеся кнопкою копіювання і перезапустіть гру. Відносна швидкість керування курсором - Громкість звуків - Громкость музики + Гучність звуків + Гучність музики Комп’ютерний гравець Змінити программу комп’ютерного гравця Не вдалося створити папку VCMI з даними в %1$s. @@ -33,18 +33,18 @@ Завантажити список модів Звичайне Відносне - О VCMI + Про VCMI Встановлені моди Не вдалося завантажити мод в папку \'%1$s\' Видалення %1$s Ви впевнені, що хочете видалити %1$s - О VCMI + Про VCMI Версія VCMI: %1$s Версія лаунчера: %1$s - Проект + Проєкт Legal Сайт: %1$s - Репозиторій проекту: %1$s + Репозиторій проєкту: %1$s Репозиторій лаунчера: %1$s Автори Не вдалося відкрити сторінку (ймовірно, не вдалося знайти браузер) @@ -54,6 +54,6 @@ Експортувати дані VCMI Зробити копію даних VCMI перед видаленням гри або перенести сейв-файли на версію для інших платформ Завантажити дані VCMI у внутрішнє сховище - Копіювати дані VCMI у внутрішнє сховище. Ви можете завантажити стару папку vcmi-data від версії 0.99 чи файли героїв + Копіювати дані VCMI у внутрішнє сховище. Ви можете завантажити стару теку vcmi-data від версії 0.99 чи файли героїв Копіюємо %1$s
    From 9e71a0c49594eb6918da4232e6db2e5cd7f5dfb7 Mon Sep 17 00:00:00 2001 From: utzuro Date: Sun, 24 Sep 2023 22:39:40 +0900 Subject: [PATCH 0556/1248] add small translation fixes --- android/vcmi-app/src/main/res/values-uk/strings.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/vcmi-app/src/main/res/values-uk/strings.xml b/android/vcmi-app/src/main/res/values-uk/strings.xml index 5ae3cecce..131489e1e 100644 --- a/android/vcmi-app/src/main/res/values-uk/strings.xml +++ b/android/vcmi-app/src/main/res/values-uk/strings.xml @@ -6,7 +6,7 @@ VCMI сервер Поточна версія VCMI: %1$s Моди - Додати нові замки, істот, об’єкти, розширення + Додати нові замки, істоти, об’єкти, розширення Мова Поточна: невідомо Поточна: %1$s @@ -22,7 +22,7 @@ Змінити программу комп’ютерного гравця Не вдалося створити папку VCMI з даними в %1$s. Не вдалося розпакувати файли ресурсів. Спробуйте перевстановити програму. - Не вдалося оновити файлы ресурсів. Спробуйте перевстановити програму. + Не вдалося оновити файли ресурсів. Спробуйте перевстановити програму. VCMI необхідні права для запису контенту до зовнішнього сховища. Не вдалося отримати права. Не вдалося зберегти налаштування; причина: %1$s @@ -35,7 +35,7 @@ Відносне Про VCMI Встановлені моди - Не вдалося завантажити мод в папку \'%1$s\' + Не вдалося завантажити мод в теку \'%1$s\' Видалення %1$s Ви впевнені, що хочете видалити %1$s Про VCMI From 96907590ba978de9dda9026b9dfe71df0d6d8e01 Mon Sep 17 00:00:00 2001 From: utzuro Date: Sun, 24 Sep 2023 22:41:43 +0900 Subject: [PATCH 0557/1248] fix translation word --- android/vcmi-app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/res/values-uk/strings.xml b/android/vcmi-app/src/main/res/values-uk/strings.xml index 131489e1e..7964551e7 100644 --- a/android/vcmi-app/src/main/res/values-uk/strings.xml +++ b/android/vcmi-app/src/main/res/values-uk/strings.xml @@ -20,7 +20,7 @@ Гучність музики Комп’ютерний гравець Змінити программу комп’ютерного гравця - Не вдалося створити папку VCMI з даними в %1$s. + Не вдалося створити теку VCMI з даними в %1$s. Не вдалося розпакувати файли ресурсів. Спробуйте перевстановити програму. Не вдалося оновити файли ресурсів. Спробуйте перевстановити програму. VCMI необхідні права для запису контенту до зовнішнього сховища. From 240151374d62b4030168d82b3390c9431893ef71 Mon Sep 17 00:00:00 2001 From: utzuro Date: Sun, 24 Sep 2023 22:44:35 +0900 Subject: [PATCH 0558/1248] fix word in the translation --- android/vcmi-app/src/main/res/values-uk/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/res/values-uk/strings.xml b/android/vcmi-app/src/main/res/values-uk/strings.xml index 7964551e7..fdad3813e 100644 --- a/android/vcmi-app/src/main/res/values-uk/strings.xml +++ b/android/vcmi-app/src/main/res/values-uk/strings.xml @@ -19,7 +19,7 @@ Гучність звуків Гучність музики Комп’ютерний гравець - Змінити программу комп’ютерного гравця + Змінити програму комп’ютерного гравця Не вдалося створити теку VCMI з даними в %1$s. Не вдалося розпакувати файли ресурсів. Спробуйте перевстановити програму. Не вдалося оновити файли ресурсів. Спробуйте перевстановити програму. From b8eba01b7d19adb8792a282976550b668b687954 Mon Sep 17 00:00:00 2001 From: An Vu Date: Sun, 24 Sep 2023 21:09:09 +0700 Subject: [PATCH 0559/1248] add Launcher and Map Editor --- Mods/vcmi/config/vcmi/vietnamese.json | 8 +- launcher/CMakeLists.txt | 1 + launcher/translation/vietnamese.ts | 1121 +++++++++++++++++++++++++ mapeditor/CMakeLists.txt | 1 + mapeditor/translation/vietnamese.ts | 1018 ++++++++++++++++++++++ 5 files changed, 2145 insertions(+), 4 deletions(-) create mode 100644 launcher/translation/vietnamese.ts create mode 100644 mapeditor/translation/vietnamese.ts diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json index 77fa7c519..9926fce10 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -77,10 +77,10 @@ "vcmi.systemOptions.resolutionButton.help": "{Chọn độ phân giải}\n\nĐổi độ phân giải trong trò chơi.", "vcmi.systemOptions.resolutionMenu.hover": "Chọn độ phân giải", "vcmi.systemOptions.resolutionMenu.help": "Đổi độ phân giải trong trò chơi.", - "vcmi.systemOptions.scalingButton.hover": "Tỉ lệ giao diện: %p%", - "vcmi.systemOptions.scalingButton.help": "{Tỉ lệ giao diện}\n\nĐổi tỉ lệ giao diện trong trò chơi.", - "vcmi.systemOptions.scalingMenu.hover": "Chọn tỉ lệ giao diện", - "vcmi.systemOptions.scalingMenu.help": "Đổi tỉ lệ giao diện trong trò chơi.", + "vcmi.systemOptions.scalingButton.hover": "Phóng đại giao diện: %p%", + "vcmi.systemOptions.scalingButton.help": "{Phóng đại giao diện}\n\nĐổi độ phóng đại giao diện trong trò chơi.", + "vcmi.systemOptions.scalingMenu.hover": "Chọn độ phóng đại giao diện", + "vcmi.systemOptions.scalingMenu.help": "Đổi độ phóng đại giao diện trong trò chơi.", "vcmi.systemOptions.longTouchButton.hover": "Khoảng thời gian chạm giữ: %d ms", "vcmi.systemOptions.longTouchButton.help": "{Khoảng thời gian chạm giữ}\n\nKhi dùng màn hình cảm ứng, cửa sổ sẽ bật lên sau khi chạm màn hình trong 1 khoảng thời gian xác định, theo mili giây.", "vcmi.systemOptions.longTouchMenu.hover": "Chọn khoảng thời gian chạm giữ", diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 9fa61ab82..f21ad852b 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -66,6 +66,7 @@ set(launcher_TS translation/russian.ts translation/spanish.ts translation/ukrainian.ts + translation/vietnamese.ts ) if(APPLE_IOS) diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts new file mode 100644 index 000000000..79c7518ad --- /dev/null +++ b/launcher/translation/vietnamese.ts @@ -0,0 +1,1121 @@ + + + + + AboutProjectView + + + VCMI on Discord + VCMI trên Discord + + + + Have a question? Found a bug? Want to help? Join us! + Có thắc mắc? Gặp lỗi? Cần giúp đỡ? Tham gia cùng chúng tôi! + + + + VCMI on Github + VCMI trên Github + + + + Our Community + Cộng đồng + + + + VCMI on Slack + VCMI trên Slack + + + + Build Information + Thông tin bản dựng + + + + User data directory + Đường dẫn đữ liệu người dùng + + + + + + Open + Mở + + + + Check for updates + Kiểm tra cập nhật + + + + Game version + Phiên bản trò chơi + + + + Log files directory + Đường dẫn nhật kí + + + + Data Directories + Đường dẫn dữ liệu + + + + Game data directory + Đường dẫn dữ liệu trò chơi + + + + Operating System + Hệ điều hành + + + + Project homepage + Trang chủ + + + + Report a bug + Báo lỗi + + + + CModListModel + + + Translation + Bản dịch + + + + Town + Thành phố + + + + Test + Kiểm tra + + + + Templates + Mẫu + + + + Spells + Phép + + + + Music + Nhạc + + + + Sounds + Âm thanh + + + + Skills + Kĩ năng + + + + + Other + Khác + + + + Objects + Đối tượng + + + + + Mechanics + Cơ chế + + + + + Interface + Giao diện + + + + Heroes + Tướng + + + + + Graphical + Đồ họa + + + + Expansion + Bản mở rộng + + + + Creatures + Quái + + + + Artifacts + Vật phẩm + + + + AI + Trí tuệ nhân tạo + + + + Name + Tên + + + + Type + Loại + + + + Version + Phiên bản + + + + CModListView + + + Filter + Bộ lọc + + + + All mods + Tất cả + + + + Downloadable + Có thể tải về + + + + Installed + Đã cài đặt + + + + Updatable + Cập nhật mới + + + + Active + Bật + + + + Inactive + Tắt + + + + Download && refresh repositories + Tải lại + + + + + Description + Mô tả + + + + Changelog + Các thay đổi + + + + Screenshots + Hình ảnh + + + + Uninstall + Gỡ bỏ + + + + Enable + Bật + + + + Disable + Tắt + + + + Update + Cập nhật + + + + Install + Cài đặt + + + + %p% (%v KB out of %m KB) + %p% (%v KB trong số %m KB) + + + + Abort + Hủy + + + + Mod name + Tên bản sửa đổi + + + + Installed version + Phiên bản cài đặt + + + + Latest version + Phiên bản mới nhất + + + + Download size + Kích thước tải về + + + + Authors + Tác giả + + + + License + Giấy phép + + + + Contact + Liên hệ + + + + Compatibility + Tương thích + + + + + Required VCMI version + Cần phiên bản VCMI + + + + Supported VCMI version + Hỗ trợ phiên bản VCMI + + + + Supported VCMI versions + Phiên bản VCMI hỗ trợ + + + + Languages + Ngôn ngữ + + + + Required mods + Cần các bản sửa đổi + + + + Conflicting mods + Bản sửa đổi không tương thích + + + + This mod can not be installed or enabled because the following dependencies are not present + Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau + + + + This mod can not be enabled because the following mods are incompatible with it + Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau + + + + This mod cannot be disabled because it is required by the following mods + Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau + + + + This mod cannot be uninstalled or updated because it is required by the following mods + Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau + + + + This is a submod and it cannot be installed or uninstalled separately from its parent mod + Đây là bản con, không thể cài đặt hoặc gỡ bỏ tách biệt với bản cha + + + + Notes + Ghi chú + + + + Screenshot %1 + Hình ảnh %1 + + + + Mod is incompatible + Bản sửa đổi này không tương thích + + + + CSettingsView + + + + + Off + Tắt + + + + + Artificial Intelligence + Trí tuệ nhân tạo + + + + + Mod Repositories + Nguồn bản sửa đổi + + + + Interface Scaling + Phóng đại giao diện + + + + Neutral AI in battles + Máy hoang dã trong trận đánh + + + + Enemy AI in battles + Máy đối thủ trong trận đánh + + + + Additional repository + Nguồn bổ sung + + + + Adventure Map Allies + Máy liên minh ở bản đồ phiêu lưu + + + + Adventure Map Enemies + Máy đối thủ ở bản đồ phiêu lưu + + + + Windowed + Cửa sổ + + + + Borderless fullscreen + Toàn màn hình không viền + + + + Exclusive fullscreen + Toàn màn hình riêng biệt + + + + Autosave limit (0 = off) + Giới hạn lưu tự động (0 = không giới hạn) + + + + Friendly AI in battles + Máy liên minh trong trận đánh + + + + Framerate Limit + Giới hạn khung hình + + + + Autosave prefix + Thêm tiền tố vào lưu tự động + + + + empty = map name prefix + Rỗng = tên bản đồ + + + + Refresh now + Làm mới + + + + Default repository + Nguồn mặc định + + + + + + On + Bật + + + + Cursor + Con trỏ + + + + Heroes III Data Language + Ngôn ngữ dữ liệu Heroes III + + + + Select display mode for game + +Windowed - game will run inside a window that covers part of your screen + +Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. + +Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. + Chọn chế độ hiện thị + +Cửa sổ - Trò chơi chạy trong 1 cửa sổ + +Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung độ phân giải hiện tại + +Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng độ phân giải được chọn. + + + + Reserved screen area + Diện tích màn hình dành riêng + + + + Hardware + Phần cứng + + + + Software + Phần mềm + + + + Heroes III Translation + Bản dịch Heroes III + + + + Check on startup + Kiểm tra khi khởi động + + + + Fullscreen + Toàn màn hình + + + + + General + Chung + + + + VCMI Language + Ngôn ngữ VCMI + + + + Resolution + Độ phân giải + + + + Autosave + Tự động lưu + + + + Display index + Mục hiện thị + + + + Network port + Cổng mạng + + + + + Video + Phim ảnh + + + + Show intro + Hiện thị giới thiệu + + + + Active + Bật + + + + Disabled + Tắt + + + + Enable + Bật + + + + Not Installed + Chưa cài đặt + + + + Install + Cài đặt + + + + FirstLaunchView + + + Language + Ngôn ngữ + + + + Heroes III Data + Dữ liệu Heroes III + + + + Mods Preset + Bản thiết lập trước + + + + Select your language + Chọn ngôn ngữ + + + + Have a question? Found a bug? Want to help? Join us! + Có thắc mắc? Gặp lỗi? Cần giúp đỡ? Tham gia cùng chúng tôi! + + + + Thank you for installing VCMI! + +Before you can start playing, there are a few more steps that need to be completed. + +Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death. + +Heroes® of Might and Magic® III HD is currently not supported! + Cảm ơn bạn đã cài VCMI! + +Trước khi bắt đầu, còn vài bước cần hoàn thành. + +Để chạy VCMI, bạn cần có dữ liệu gốc Heroes® of Might and Magic® III: Complete hoặc The Shadow of Death. + +Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD! + + + + Locate Heroes III data files + Định vị tệp dữ liệu Heroes III + + + + If you don't have a copy of Heroes III installed, you can use our automatic installation tool 'vcmibuilder', which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions. + Nếu không có bản sao Heroes III, bạn có thể sử dụng 'vcmibuilder', cái mà chỉ cần bản cài GoG.com Heroes III. Tham khảo trang wiki của chúng tôi để biết thêm chi tiết. + + + + To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories. + Để chạy VCMI, dữ liệu Heroes III cần được đặt ở 1 trong những đường dẫn cho trước. Sao chép dữ liệu Heroes III đến 1 trong những đường dẫn này. + + + + Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically. + Thay vào đó, bạn có thể cung cấp đường dẫn cài đặt dữ liệu Heroes III và VCMI sẽ tự sao chép dữ liệu. + + + + Your Heroes III data files have been successfully found. + Dữ liệu Heroes III đã được tìm thấy. + + + + The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually + Tự nhận diện ngôn ngữ Heroes III thất bại. Chọn ngôn ngữ Heroes III thủ công + + + + Interface Improvements + Cải thiện giao diện + + + + Install a translation of Heroes III in your preferred language + Cài ngôn ngữ Heroes III + + + + Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher + Tùy chọn, bạn có thể cài bản sửa đổi bổ sung bây giờ, hoặc bất kì lúc nào bằng VCMI Launcher + + + + Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles + Cài đặt bản sửa đổi cung cấp nhiều cải tiến giao diện cho bản đồ ngẫu nhiên và thao tác trong trận đánh + + + + Install compatible version of "Horn of the Abyss", a fan-made Heroes III expansion ported by the VCMI team + Cài đặt phiên bản tương thích Horn of the Abyss, bản mở rộng Heroes III người hâm mộ tự làm, được nhóm VCMI chuyển qua + + + + Install compatible version of "In The Wake of Gods", a fan-made Heroes III expansion + Cài đặt phiên bản tương thích In The Wake of Gods, bản mở rộng Heroes III người hâm mộ tự làm + + + + Finish + Hoàn thành + + + + VCMI on Github + VCMI trên Github + + + + VCMI on Slack + VCMI trên Slack + + + + VCMI on Discord + VCMI trên Discord + + + + + Next + Tiếp theo + + + + Open help in browser + Mở trợ giúp trên trình duyệt + + + + Search again + Tìm kiếm lại + + + + Heroes III data files + Tệp dữ liệu Heroes III + + + + Copy existing data + Sao chép dữ liệu đang có + + + + Your Heroes III language has been successfully detected. + Ngôn ngữ Heroes III đã được nhận diện. + + + + Heroes III language + Ngôn ngữ Heroes III + + + + + Back + Quay lại + + + + Install VCMI Mod Preset + Cài đặt bản sửa đổi VCMI thiết lập trước + + + + Horn of the Abyss + Horn of the Abyss + + + + Heroes III Translation + Bản dịch Heroes III + + + + In The Wake of Gods + In The Wake of Gods + + + + ImageViewer + + + Image Viewer + Trình xem ảnh + + + + Language + + + Czech + Tiếng Séc + + + + Chinese + Tiếng Trung + + + + English + Tiếng Anh + + + + Finnish + Tiếng Phần Lan + + + + French + Tiếng Pháp + + + + German + Tiếng Đức + + + + Hungarian + Tiếng Hungary + + + + Italian + Tiếng Ý + + + + Korean + Tiếng Hàn + + + + Polish + Tiếng Ba Lan + + + + Portuguese + Tiếng Bồ Đào Nha + + + + Russian + Tiếng Nga + + + + Spanish + Tiếng Tây Ban Nha + + + + Swedish + Tiếng Thụy Điển + + + + Turkish + Tiếng Thổ Nhĩ Kì + + + + Ukrainian + Tiếng Ukraina + + + + Vietnamese + Tiếng Việt + + + + Other (East European) + Khác (Đông Âu) + + + + Other (Cyrillic Script) + Khác (Chữ Kirin) + + + + Other (West European) + Khác (Tây Âu) + + + + Auto (%1) + Tự động (%1) + + + + Lobby + + + + Connect + Kết nối + + + + Username + Tên đăng nhập + + + + Server + Máy chủ + + + + Lobby chat + Trò chuyện + + + + Session + Phiên + + + + Players + Người chơi + + + + Resolve + Phân tích + + + + New game + Tạo mới + + + + Load game + Tải lại + + + + New room + Tạo phòng + + + + Players in lobby + Người chơi trong sảnh + + + + Join room + Vào phòng + + + + Ready + Sẵn sàng + + + + Mods mismatch + Bản sửa đổi chưa giống + + + + Leave + Rời khỏi + + + + Kick player + Mời ra + + + + Players in the room + Người chơi trong phòng + + + + Disconnect + Thoát + + + + No issues detected + Không có vấn đề + + + + LobbyRoomRequest + + + Room settings + Cài đặt phòng + + + + Room name + Tên phòng + + + + Maximum players + Số người chơi tối đa + + + + Password (optional) + Mật khẩu (tùy chọn) + + + + MainWindow + + + VCMI Launcher + VCMI Launcher + + + + Settings + Cài đặt + + + + Help + + + + + Map Editor + Tạo bản đồ + + + + Start game + Chơi ngay + + + + Lobby + Sảnh + + + + Mods + Bản sửa đổi + + + + UpdateDialog + + + You have latest version + Bạn đã có phiên bản mới nhất + + + + + Close + Đóng + + + + Check updates on startup + Cập nhật khi khởi động + + + diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index df2d26a3d..4041bcd1e 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -104,6 +104,7 @@ set(editor_TS translation/russian.ts translation/spanish.ts translation/ukrainian.ts + translation/vietnamese.ts ) assign_source_group(${editor_SRCS} ${editor_HEADERS} mapeditor.rc) diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts new file mode 100644 index 000000000..fc5e130ac --- /dev/null +++ b/mapeditor/translation/vietnamese.ts @@ -0,0 +1,1018 @@ + + + + + ArmyWidget + + + Army settings + Cài đặt quân + + + + Wide formation + Đội hình rộng + + + + Tight formation + Đội hình kín + + + + GeneratorProgress + + + Generating map + Tạo bản đồ + + + + MainWindow + + + VCMI Map Editor + Bộ tạo bản đồ VCMI + + + + File + Tập tin + + + + Map + Bản đồ + + + + Edit + Hiệu chỉnh + + + + View + Xem + + + + Player + Người chơi + + + + Toolbar + Thanh công cụ + + + + Minimap + Bản đồ nhỏ + + + + Map Objects View + Xem đối tượng bản đồ + + + + Browser + Duyệt + + + + Inspector + Giám định + + + + Property + Đặc tính + + + + Value + Giá trị + + + + Terrains View + Xem địa hình + + + + Brush + Quét + + + + Terrains + Địa hình + + + + Roads + Đường + + + + Rivers + Sông + + + + Open + Mở + + + + Save + Lưu + + + + New + Tạo mới + + + + Save as + Lưu vào + + + + Ctrl+Shift+S + Ctrl+Shift+S + + + + U/G + U/G + + + + + View underground + Xem hang ngầm + + + + Pass + Đi qua + + + + Cut + Cắt + + + + Copy + Sao chép + + + + Paste + Dán + + + + Fill + Làm đầy + + + + Fills the selection with obstacles + Làm đầy vùng chọn với vật cản + + + + Grid + Đường kẻ + + + + General + Chung + + + + Map title and description + Tên bản đồ và mô tả + + + + Players settings + Cài đặt người chơi + + + + + Undo + Hoàn tác + + + + Redo + Làm lại + + + + Erase + Xóa + + + + Neutral + Trung lập + + + + Validate + Hiệu lực + + + + + + + Update appearance + Cập nhật hiện thị + + + + Recreate obstacles + Tạo lại vật cản + + + + Player 1 + Người chơi 1 + + + + Player 2 + Người chơi 2 + + + + Player 3 + Người chơi 3 + + + + Player 4 + Người chơi 4 + + + + Player 5 + Người chơi 5 + + + + Player 6 + Người chơi 6 + + + + Player 7 + Người chơi 7 + + + + Player 8 + Người chơi 8 + + + + Export as... + Xuất thành... + + + + Confirmation + Xác nhận + + + + Unsaved changes will be lost, are you sure? + Thay đổi chưa lưu sẽ bị mất, bạn có chắc chắn? + + + + Failed to open map + Không thể mở bản đồ + + + + Cannot open map from this folder + Không thể mở bản đồ từ thư mục này + + + + Open map + Mở bản đồ + + + + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) + Tất cả bản đồ hỗ trợ (*.vmap *.h3m);;Bản đồ VCMI (*.vmap);;Bản đồ HoMM3 (*.h3m) + + + + + Save map + Lưu bản đồ + + + + + VCMI maps (*.vmap) + Bản đồ VCMI (*.vmap) + + + + Type + Loại + + + + View surface + Xem bề mặt + + + + No objects selected + Không mục tiêu được chọn + + + + This operation is irreversible. Do you want to continue? + Thao tác này không thể đảo ngược. Bạn muốn tiếp tục? + + + + Errors occured. %1 objects were not updated + Xảy ra lỗi. %1 mục tiêu không được cập nhật + + + + Save to image + Lưu thành ảnh + + + + MapSettings + + + Map settings + Cài đặt bản đồ + + + + General + Chung + + + + Map name + Tên bản đồ + + + + Map description + Mô tả bản đồ + + + + Limit maximum heroes level + Giới hạn cấp tướng tối đa + + + + Difficulty + Độ khó + + + + Mods + Bản sửa đổi + + + + Mandatory mods for playing this map + Bản sửa đổi cần để chơi bản đồ này + + + + Mod name + Tên bản sửa đổi + + + + Version + Phiên bản + + + + Automatic assignment + Gán tự động + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi + + + + Map objects mods + Bản sửa đổi vật thể trên bản đồ + + + + Set all mods having a game content as mandatory + Tập bản sửa đổi cần cho nội dung trò chơi + + + + Full content mods + Bản sửa đổi nội dung đầy đủ + + + + Events + Sự kiện + + + + Victory + Chiến thắng + + + + Victory message + Thông báo chiến thắng + + + + Only for human players + Chỉ cho người + + + + Allow standard victory + Cho phép chiến thắng thông thường + + + + + Parameters + Tham số + + + + Loss + Thất bại + + + + 7 days without town + 7 ngày không có thành + + + + Defeat message + Thông báo thất bại + + + + Abilities + Năng lực + + + + Spells + Phép + + + + Artifacts + Vật phẩm + + + + Heroes + Tướng + + + + Ok + Đồng ý + + + + No special victory + Không có chiến thắng đặc biệt + + + + Capture artifact + Đoạt vật phẩm + + + + Hire creatures + Thuê quái + + + + Accumulate resources + Cộng dồn tài nguyên + + + + Construct building + Xây công trình + + + + Capture town + Đoạt thành + + + + Defeat hero + Đánh bại tướng + + + + Transport artifact + Vận chuyển vật phẩm + + + + No special loss + Không có thất bại đặc biệt + + + + Lose castle + Mất thành + + + + Lose hero + Mất tướng + + + + Time expired + Hết thời gian + + + + Days without town + Số ngày không có thành + + + + MapView + + + Can't place object + Không thể đặt vật thể + + + + MessageWidget + + + Message + Thông báo + + + + PlayerParams + + + Human/CPU + Người/Máy + + + + CPU only + Chỉ máy + + + + Team + Phe + + + + Main town + Thành chính + + + + Color + Màu + + + + Random faction + Thành ngẫu nhiên + + + + Generate hero at main + Tạo tướng ban đầu + + + + (default) + (mặc định) + + + + Player ID: %1 + ID người chơi: %1 + + + + PlayerSettings + + + Player settings + Cài đặt người chơi + + + + Players + Người chơi + + + + 1 + 1 + + + + Ok + Đồng ý + + + + QuestWidget + + + Mission goal + Mục tiêu nhiệm vụ + + + + RewardsWidget + + + Rewards + Phần thưởng + + + + Remove selected + Bỏ chọn + + + + Delete all + Xóa tất cả + + + + Add or change + Thêm hoặc sửa + + + + TownBulidingsWidget + + + Buildings + Công trình + + + + Validator + + + Map validation results + Kết quả kiểm định bản đồ + + + + Map is not loaded + Bản đồ không thể tải + + + + No factions allowed for player %1 + Không có tộc được phép cho người chơi %1 + + + + No players allowed to play this map + Không có người chơi được phép chơi bản đồ này + + + + Map is allowed for one player and cannot be started + Bản đồ cho phép 1 người chơi nhưng không thể bắt đầu + + + + No human players allowed to play this map + Không có người nào được phép chơi bản đồ này + + + + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner + Thực thể %1 không gắn cờ nhưng phải có quái trung lập hoặc người chơi sở hữu + + + + Object %1 is assigned to non-playable player %2 + Vật thể %1 được gán cho người không thể chơi %2 + + + + Town %1 has undefined owner %2 + Thành %1 có chủ nhân không xác định %2 + + + + Prison %1 must be a NEUTRAL + Nhà giam %1 phải trung lập + + + + Hero %1 must have an owner + Tướng %1 phải có chủ + + + + Hero %1 is prohibited by map settings + Tướng %1 bị cấm bởi bản đồ + + + + Hero %1 has duplicate on map + Tướng %1 bị trùng trên bản đồ + + + + Hero %1 has an empty type and must be removed + Tướng %1 có kiểu rỗng và phải được xóa + + + + Spell scroll %1 is prohibited by map settings + Cuộn phép %1 bị cấm bởi bản đồ + + + + Spell scroll %1 doesn't have instance assigned and must be removed + Cuộn phép %1 không có đối tượng được gán và phải được xóa + + + + Artifact %1 is prohibited by map settings + Vật phẩm %1 bị cấm bởi bản đồ + + + + Player %1 doesn't have any starting town + Người chơi %1 không có thành khởi đầu nào + + + + Map name is not specified + Tên bản đồ không có + + + + Map description is not specified + Mô tả bản đồ không có + + + + Map contains object from mod "%1", but doesn't require it + Bản đồ chứa đối tượng từ bản mở rộng "%1", nhưng bản mở rộng đó không được yêu cầu + + + + Exception occurs during validation: %1 + Ngoại lệ xuất hiện trong quá trình phê chuẩn: %1 + + + + Unknown exception occurs during validation + Ngoại lệ chưa biết xuất hiện trong quá trình phê chuẩn: %1 + + + + WindowNewMap + + + Create new map + Tạo bản đồ mới + + + + Map size + Độ lớn bản đồ + + + + Two level map + Bản đồ 2 tầng + + + + Height + Cao + + + + Width + Rộng + + + + S (36x36) + Nhỏ (36x36) + + + + M (72x72) + Vừa (72x72) + + + + L (108x108) + Lớn (108x108) + + + + XL (144x144) + Rất lớn (144x144) + + + + Random map + Bản đồ ngẫu nhiên + + + + Players + Người chơi + + + + 0 + 0 + + + + Human/Computer + Người/Máy + + + + + + + Random + Ngẫu nhiên + + + + Computer only + Chỉ máy + + + + Human teams + Đội người + + + + Computer teams + Đội máy + + + + Monster strength + Sức mạnh quái + + + + Weak + Yếu + + + + + Normal + Trung bình + + + + Strong + Mạnh + + + + Water content + Có nước + + + + None + Không + + + + Islands + Các đảo + + + + Template + Mẫu + + + + Custom seed + Tùy chỉnh ban đầu + + + + Generate random map + Tạo bản đồ ngẫu nhiên + + + + Ok + Đồng ý + + + + Cancel + Hủy + + + + No template + Không dùng mẫu + + + + No template for parameters scecified. Random map cannot be generated. + Không có mẫu cho tham số chỉ định. Bản đồ ngẫu nhiên không thể tạo + + + + RMG failure + Tạo bản đồ ngẫu nhiên thất bại + + + + main + + + Filepath of the map to open. + Đường dẫn bản đồ + + + + Extract original H3 archives into a separate folder. + Giải nén dữ liệu H3 gốc vào 1 thư mục riêng. + + + + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. + Từ dữ liệu giải nén, chia TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 và Un44 thành những hình PNG riêng lẻ. + + + + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. + Từ dữ liệu giải nén, chuyển đổi các hình đơn (được tìm thấy trong thư mục Images) từ .pcx sang .png. + + + + Delete original files, for the ones splitted / converted. + Xóa các tập tin gốc đã được phân chia / chuyển đổi. + + + From 240d291de928e40642c47ca56df04024fa5aac4f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 22:24:51 +0200 Subject: [PATCH 0560/1248] big spellbook --- client/windows/CSpellWindow.cpp | 46 +++++++++++++++++++++++++++++++-- client/windows/CSpellWindow.h | 4 +++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index d1418eca2..c1405cd59 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -31,6 +31,9 @@ #include "../adventureMap/AdventureMapInterface.h" #include "../render/CAnimation.h" #include "../render/IRenderHandler.h" +#include "../render/IImage.h" +#include "../render/IImageLoader.h" +#include "../render/Canvas.h" #include "../../CCallback.h" @@ -94,14 +97,22 @@ public: } spellsorter; CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED, ImagePath::builtin("SpelBack")), + CWindowObject(PLAYER_COLORED), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), - myInt(_myInt) + myInt(_myInt), + isBigSpellbook(true) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(isBigSpellbook) + background = std::make_shared(createBigSpellBook(), Point(0, 0)); + else + background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); + pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); + //initializing castable spells mySpells.reserve(CGI->spellh->objects.size()); for(const CSpell * spell : CGI->spellh->objects) @@ -236,6 +247,37 @@ CSpellWindow::~CSpellWindow() { } +std::shared_ptr CSpellWindow::createBigSpellBook() +{ + std::shared_ptr img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack")); + Canvas canvas = Canvas(Point(800, 600)); + // edges + canvas.draw(img, Point(0, 0), Rect(10, 38, 90, 45)); + canvas.draw(img, Point(0, 460), Rect(10, 400, 90, 141)); + canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); + canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); + // left / right + Canvas tmp1 = Canvas(Point(90, 355 - 45)); + tmp1.draw(img, Point(0, 0), Rect(10, 38 + 45, 90, 355 - 45)); + canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); + Canvas tmp2 = Canvas(Point(95, 355 - 45)); + tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); + canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415)); + // top / bottom + Canvas tmp3 = Canvas(Point(409, 45)); + tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45)); + canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45)); + Canvas tmp4 = Canvas(Point(409, 141)); + tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141)); + canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); + // middle + Canvas tmp5 = Canvas(Point(409, 141)); + tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 10, 400 - 38)); + canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); + + return GH.renderHandler().createImage(canvas.getInternalSurface()); +} + void CSpellWindow::fexitb() { (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 9d041bdfc..f060512b3 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -80,6 +80,8 @@ class CSpellWindow : public CWindowObject std::vector> interactiveAreas; + bool isBigSpellbook; + int sitesPerTabAdv[5]; int sitesPerTabBattle[5]; @@ -97,6 +99,8 @@ class CSpellWindow : public CWindowObject void turnPageLeft(); void turnPageRight(); + std::shared_ptr createBigSpellBook(); + public: CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); ~CSpellWindow(); From 8b5c9db21b0793842c0a356d5ffe01e5800f9c6e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 24 Sep 2023 23:15:09 +0200 Subject: [PATCH 0561/1248] basic offset --- client/windows/CSpellWindow.cpp | 51 +++++++++++++++++---------------- client/windows/CSpellWindow.h | 6 +++- 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index c1405cd59..c8af8c89d 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -110,7 +110,10 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m if(isBigSpellbook) background = std::make_shared(createBigSpellBook(), Point(0, 0)); else + { background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); + offL = offR = offT = offB = 0; + } pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); //initializing castable spells @@ -178,13 +181,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m //numbers of spell pages computed - leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97, 77); - rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487, 72); + leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT); + rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT); spellIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("Spells")); - schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524, 88); - schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117, 74); + schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524 + offR, 88); + schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT); schoolBorders[0] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevA.def")); schoolBorders[1] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevF.def")); @@ -193,42 +196,42 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m for(auto item : schoolBorders) item->preload(); - mana = std::make_shared(435, 426, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); + mana = std::make_shared(435 + offL, 426 + offR, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); - interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); - interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); - interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); - interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x, 405 + pos.y, 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 94 + pos.y, 36, 56), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 151 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 210 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 270 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x, 330 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); + interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); + interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); + interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); + interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); + interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 94 + pos.y, 36, 56), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); + interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 151 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); + interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 210 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); + interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 270 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); + interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 330 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); - interactiveAreas.push_back(std::make_shared( Rect( 97 + pos.x, 77 + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); - interactiveAreas.push_back(std::make_shared( Rect( 487 + pos.x, 72 + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); + interactiveAreas.push_back(std::make_shared( Rect( 97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); + interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); //areas for spells - int xpos = 117 + pos.x, ypos = 90 + pos.y; + int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; - for(int v=0; v<12; ++v) + for(int v=0; v<21; ++v) { spellAreas[v] = std::make_shared( Rect(xpos, ypos, 65, 78), this); - if(v == 5) //to right page + if(v == 11) //to right page { - xpos = 336 + pos.x; ypos = 90 + pos.y; + xpos = offL + 336 + pos.x; ypos = 90 + offT + pos.y; } else { - if(v%2 == 0) + if(v%3 == 0 || v%3 == 1) { xpos+=85; } else { - xpos -= 85; ypos+=97; + xpos -= 2*85; ypos+=97; } } } @@ -439,13 +442,13 @@ void CSpellWindow::setCurrentPage(int value) void CSpellWindow::turnPageLeft() { - if(settings["video"]["spellbookAnimation"].Bool()) + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); } void CSpellWindow::turnPageRight() { - if(settings["video"]["spellbookAnimation"].Bool()) + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); } diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index f060512b3..9855c005c 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -74,13 +74,17 @@ class CSpellWindow : public CWindowObject std::shared_ptr schoolTab; std::shared_ptr schoolPicture; - std::array, 12> spellAreas; + std::array, 21> spellAreas; std::shared_ptr mana; std::shared_ptr statusBar; std::vector> interactiveAreas; bool isBigSpellbook; + int offL = -11; + int offR = 195; + int offT = -37; + int offB = 56; int sitesPerTabAdv[5]; int sitesPerTabBattle[5]; From 2a3e371cafb46480c0500bdf1601f1ca728e905d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 24 Sep 2023 23:34:46 +0200 Subject: [PATCH 0562/1248] Remove any modification of enabled mods during game loading --- lib/modding/CModHandler.cpp | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index f872bc642..12187990a 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -481,7 +481,7 @@ void CModHandler::trySetActiveMods(const std::vector newActiveMods, missingMods, excessiveMods; + std::vector missingMods, excessiveMods; ModIncompatibility::ModListWithVersion missingModsResult; ModIncompatibility::ModList excessiveModsResult; @@ -492,10 +492,7 @@ void CModHandler::trySetActiveMods(const std::vector Date: Sun, 24 Sep 2023 23:50:50 +0200 Subject: [PATCH 0563/1248] offsets --- client/windows/CSpellWindow.cpp | 59 +++++++++++++++++---------------- client/windows/CSpellWindow.h | 4 ++- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index c8af8c89d..92d6f06fa 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -112,7 +112,8 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m else { background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); - offL = offR = offT = offB = 0; + offL = offR = offT = offB = offRM = 0; + spellsPerPage = 12; } pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); @@ -142,39 +143,39 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m ++sitesPerOurTab[school]; }); } - if(sitesPerTabAdv[4] % 12 == 0) - sitesPerTabAdv[4]/=12; + if(sitesPerTabAdv[4] % spellsPerPage == 0) + sitesPerTabAdv[4]/=spellsPerPage; else - sitesPerTabAdv[4] = sitesPerTabAdv[4]/12 + 1; + sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; for(int v=0; v<4; ++v) { - if(sitesPerTabAdv[v] <= 10) + if(sitesPerTabAdv[v] <= spellsPerPage - 2) sitesPerTabAdv[v] = 1; else { - if((sitesPerTabAdv[v] - 10) % 12 == 0) - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - 10) / 12 + 1; + if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; else - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - 10) / 12 + 2; + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; } } - if(sitesPerTabBattle[4] % 12 == 0) - sitesPerTabBattle[4]/=12; + if(sitesPerTabBattle[4] % spellsPerPage == 0) + sitesPerTabBattle[4]/=spellsPerPage; else - sitesPerTabBattle[4] = sitesPerTabBattle[4]/12 + 1; + sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; for(int v=0; v<4; ++v) { - if(sitesPerTabBattle[v] <= 10) + if(sitesPerTabBattle[v] <= spellsPerPage - 2) sitesPerTabBattle[v] = 1; else { - if((sitesPerTabBattle[v] - 10) % 12 == 0) - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - 10) / 12 + 1; + if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; else - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - 10) / 12 + 2; + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; } } @@ -215,13 +216,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m //areas for spells int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; - for(int v=0; v<21; ++v) + for(int v=0; v( Rect(xpos, ypos, 65, 78), this); - if(v == 11) //to right page + if(v == (spellsPerPage / 2) - 1) //to right page { - xpos = offL + 336 + pos.x; ypos = 90 + offT + pos.y; + xpos = offRM + 336 + pos.x; ypos = 90 + offT + pos.y; } else { @@ -371,29 +372,29 @@ void CSpellWindow::computeSpellsPerArea() if(selectedTab == 4) { - if(spellsCurSite.size() > 12) + if(spellsCurSite.size() > spellsPerPage) { - spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*12, spellsCurSite.end()); - if(spellsCurSite.size() > 12) + spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*spellsPerPage, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) { - spellsCurSite.erase(spellsCurSite.begin()+12, spellsCurSite.end()); + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); } } } else //selectedTab == 0, 1, 2 or 3 { - if(spellsCurSite.size() > 10) + if(spellsCurSite.size() > spellsPerPage - 2) { if(currentPage == 0) { - spellsCurSite.erase(spellsCurSite.begin()+10, spellsCurSite.end()); + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage-2, spellsCurSite.end()); } else { - spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*12 + 10, spellsCurSite.end()); - if(spellsCurSite.size() > 12) + spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*spellsPerPage + spellsPerPage-2, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) { - spellsCurSite.erase(spellsCurSite.begin()+12, spellsCurSite.end()); + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); } } } @@ -401,7 +402,7 @@ void CSpellWindow::computeSpellsPerArea() //applying if(selectedTab == 4 || currentPage != 0) { - for(size_t c=0; c<12; ++c) + for(size_t c=0; csetSpell(nullptr); spellAreas[1]->setSpell(nullptr); - for(size_t c=0; c<10; ++c) + for(size_t c=0; csetSpell(spellsCurSite[c]); diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 9855c005c..65e32413b 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -74,15 +74,17 @@ class CSpellWindow : public CWindowObject std::shared_ptr schoolTab; std::shared_ptr schoolPicture; - std::array, 21> spellAreas; + std::array, 24> spellAreas; std::shared_ptr mana; std::shared_ptr statusBar; std::vector> interactiveAreas; bool isBigSpellbook; + int spellsPerPage = 24; int offL = -11; int offR = 195; + int offRM = 110; int offT = -37; int offB = 56; From 3e4a5b7d804d0962d0dd028b75712f6522415acd Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 00:31:32 +0200 Subject: [PATCH 0564/1248] border and variable --- client/windows/CSpellWindow.cpp | 23 +++++++++++++---------- client/windows/CSpellWindow.h | 2 +- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 92d6f06fa..fa8b8e4f5 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -96,19 +96,22 @@ public: } } spellsorter; -CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED), +CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, bool isBigSpellbook): + CWindowObject(PLAYER_COLORED | (isBigSpellbook ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(true) + isBigSpellbook(isBigSpellbook) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); if(isBigSpellbook) + { background = std::make_shared(createBigSpellBook(), Point(0, 0)); + updateShadow(); + } else { background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); @@ -197,13 +200,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m for(auto item : schoolBorders) item->preload(); - mana = std::make_shared(435 + offL, 426 + offR, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); + mana = std::make_shared(435 + (isBigSpellbook ? 157 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); - interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); - interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); - interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); - interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x, 405 + pos.y + offB, 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); + interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); + interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); + interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); + interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 94 + pos.y, 36, 56), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 151 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 210 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); @@ -226,13 +229,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m } else { - if(v%3 == 0 || v%3 == 1) + if(v%(isBigSpellbook ? 3 : 2) == 0 || (v%3 == 1 && isBigSpellbook)) { xpos+=85; } else { - xpos -= 2*85; ypos+=97; + xpos -= (isBigSpellbook ? 2 : 1)*85; ypos+=97; } } } diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 65e32413b..efe1376f7 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -108,7 +108,7 @@ class CSpellWindow : public CWindowObject std::shared_ptr createBigSpellBook(); public: - CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); + CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true, bool isBigSpellbook = true); ~CSpellWindow(); void fexitb(); From 28f4046937e69cdecb106912bfdddb9ef9d30c28 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 01:04:06 +0200 Subject: [PATCH 0565/1248] ready --- client/widgets/TextControls.cpp | 3 +++ client/windows/CSpellWindow.cpp | 22 +++++++++++++--------- client/windows/CSpellWindow.h | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index e6cce7c1a..a128070e2 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -461,6 +461,9 @@ CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) addUsedEvents(LCLICK); OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + if(name.empty()) + return; auto backgroundImage = std::make_shared(name); background = backgroundImage; pos = background->pos; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index fa8b8e4f5..f65c7f5a2 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -96,14 +96,14 @@ public: } } spellsorter; -CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, bool isBigSpellbook): - CWindowObject(PLAYER_COLORED | (isBigSpellbook ? BORDERED : 0)), +CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): + CWindowObject(PLAYER_COLORED | (settings["general"]["enableUiEnhancements"].Bool() ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(isBigSpellbook) + isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -200,8 +200,12 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m for(auto item : schoolBorders) item->preload(); - mana = std::make_shared(435 + (isBigSpellbook ? 157 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); - statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); + mana = std::make_shared(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); + + if(isBigSpellbook) + statusBar = CGStatusBar::create(400, 587, ImagePath::builtin("")); + else + statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); @@ -259,13 +263,13 @@ std::shared_ptr CSpellWindow::createBigSpellBook() std::shared_ptr img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack")); Canvas canvas = Canvas(Point(800, 600)); // edges - canvas.draw(img, Point(0, 0), Rect(10, 38, 90, 45)); - canvas.draw(img, Point(0, 460), Rect(10, 400, 90, 141)); + canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); + canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); // left / right Canvas tmp1 = Canvas(Point(90, 355 - 45)); - tmp1.draw(img, Point(0, 0), Rect(10, 38 + 45, 90, 355 - 45)); + tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45)); canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); Canvas tmp2 = Canvas(Point(95, 355 - 45)); tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); @@ -279,7 +283,7 @@ std::shared_ptr CSpellWindow::createBigSpellBook() canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); // middle Canvas tmp5 = Canvas(Point(409, 141)); - tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 10, 400 - 38)); + tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); return GH.renderHandler().createImage(canvas.getInternalSurface()); diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index efe1376f7..65e32413b 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -108,7 +108,7 @@ class CSpellWindow : public CWindowObject std::shared_ptr createBigSpellBook(); public: - CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true, bool isBigSpellbook = true); + CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); ~CSpellWindow(); void fexitb(); From 5061792cce8f135f28d04991548d59aa8a241587 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 13:11:08 +0200 Subject: [PATCH 0566/1248] enableUiEnhancements button --- Mods/vcmi/config/vcmi/english.json | 2 ++ Mods/vcmi/config/vcmi/german.json | 2 ++ client/windows/settings/GeneralOptionsTab.cpp | 8 ++++++++ config/widgets/settings/generalOptionsTab.json | 8 ++++++++ 4 files changed, 20 insertions(+) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index cc95f3e30..bbe29be80 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -90,6 +90,8 @@ "vcmi.systemOptions.framerateButton.help" : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Enhancements", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 926501565..067594e7d 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -89,6 +89,8 @@ "vcmi.systemOptions.framerateButton.help" : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um.", "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 2851873bc..f32d568f8 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -157,6 +157,10 @@ GeneralOptionsTab::GeneralOptionsTab() { setBoolSetting("general", "hapticFeedback", value); }); + addCallback("enableUiEnhancementsChanged", [](bool value) + { + setBoolSetting("general", "enableUiEnhancements", value); + }); //moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content addCallback("availableCreaturesAsDwellingChanged", [=](int value) @@ -198,6 +202,10 @@ GeneralOptionsTab::GeneralOptionsTab() if (hapticFeedbackCheckbox) hapticFeedbackCheckbox->setSelected(settings["general"]["hapticFeedback"].Bool()); + std::shared_ptr enableUiEnhancementsCheckbox = widget("enableUiEnhancementsCheckbox"); + if (enableUiEnhancementsCheckbox) + enableUiEnhancementsCheckbox->setSelected(settings["general"]["enableUiEnhancements"].Bool()); + std::shared_ptr musicSlider = widget("musicSlider"); musicSlider->scrollTo(CCS->musich->getVolume()); diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index f8a5a4602..cd808eafe 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -61,6 +61,9 @@ { "text": "vcmi.systemOptions.hapticFeedbackButton.hover", "created" : "mobile" + }, + { + "text": "vcmi.systemOptions.enableUiEnhancementsButton.hover" } ] }, @@ -116,6 +119,11 @@ "help": "vcmi.systemOptions.hapticFeedbackButton", "callback": "hapticFeedbackChanged", "created" : "mobile" + }, + { + "name": "enableUiEnhancementsCheckbox", + "help": "vcmi.systemOptions.enableUiEnhancementsButton", + "callback": "enableUiEnhancementsChanged" } ] }, From 7cf654992f9d70b630ec29d6341443e480067f88 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 16:34:36 +0200 Subject: [PATCH 0567/1248] code review --- client/widgets/TextControls.cpp | 2 +- client/widgets/TextControls.h | 2 +- client/windows/CSpellWindow.cpp | 21 ++++++++++++++------- client/windows/CSpellWindow.h | 12 ++++++------ 4 files changed, 22 insertions(+), 15 deletions(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index a128070e2..61bd1c16e 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -462,7 +462,7 @@ CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - if(name.empty()) + if(name.empty()) // without background return; auto backgroundImage = std::make_shared(name); background = backgroundImage; diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index c1623835b..36b94cdca 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -125,7 +125,7 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const ColorRGBA & Color = Colors::WHITE); - CGStatusBar(int x, int y, const ImagePath & name, int maxw = -1); + CGStatusBar(int x, int y, const ImagePath & name = ImagePath::builtin(""), int maxw = -1); //make CLabel API private using CLabel::getText; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index f65c7f5a2..8d28b145e 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -103,7 +103,13 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()) + isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()), + spellsPerPage(24), + offL(-11), + offR(195), + offRM(110), + offT(-37), + offB(56) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -203,19 +209,20 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m mana = std::make_shared(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); if(isBigSpellbook) - statusBar = CGStatusBar::create(400, 587, ImagePath::builtin("")); + statusBar = CGStatusBar::create(400, 587); else statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); + Rect schoolRect( 549 + pos.x + offR, 94 + pos.y, 45, 35); interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 94 + pos.y, 36, 56), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 151 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 210 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 270 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); - interactiveAreas.push_back(std::make_shared( Rect( 549 + pos.x + offR, 330 + pos.y, 45, 35), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 0), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 57), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); interactiveAreas.push_back(std::make_shared( Rect( 97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index 65e32413b..bbfe1b638 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -81,12 +81,12 @@ class CSpellWindow : public CWindowObject std::vector> interactiveAreas; bool isBigSpellbook; - int spellsPerPage = 24; - int offL = -11; - int offR = 195; - int offRM = 110; - int offT = -37; - int offB = 56; + int spellsPerPage; + int offL; + int offR; + int offRM; + int offT; + int offB; int sitesPerTabAdv[5]; int sitesPerTabBattle[5]; From c3373ea34c816fe52eff4f7f48781913f3ed8906 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 18:06:40 +0200 Subject: [PATCH 0568/1248] code review --- client/CPlayerInterface.cpp | 2 +- client/CServerHandler.cpp | 2 +- client/mainmenu/CHighScoreScreen.cpp | 8 ++--- client/mainmenu/CHighScoreScreen.h | 6 ++-- client/mainmenu/CMainMenu.cpp | 2 +- server/processors/PlayerMessageProcessor.cpp | 31 ++++++++++++-------- server/processors/PlayerMessageProcessor.h | 3 -- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ec1830637..cb7cb5122 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1706,7 +1706,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) if(!ps->checkVanquished()) param.allDefeated = false; } - param.land = cb->getMapHeader()->name; + param.scenarioName = cb->getMapHeader()->name; HighScoreCalculation highScoreCalc; highScoreCalc.parameters.push_back(param); highScoreCalc.isCampaign = false; diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 96ec74205..f0fdeff30 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -684,7 +684,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared highScoreCalc->isCampaign = true; highScoreCalc->parameters.clear(); } - param.campaign = cs->getName(); + param.campaignName = cs->getName(); highScoreCalc->parameters.push_back(param); GH.dispatchMainThread([ourCampaign, this]() diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 3fa3b65fa..64da9d598 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -156,7 +156,7 @@ void CHighScoreScreen::addHighScores() if(highscorepage == HighScorePage::SCENARIO) { - std::string tmp = curData["land"].String(); + std::string tmp = curData["scenarioName"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); texts.push_back(std::make_shared(557, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["days"].Integer()))); @@ -164,7 +164,7 @@ void CHighScoreScreen::addHighScores() } else { - std::string tmp = curData["campaign"].String(); + std::string tmp = curData["campaignName"].String(); TextOperations::trimRightUnicode(tmp, std::max(0, (int)TextOperations::getUnicodeCharactersCount(tmp) - 25)); texts.push_back(std::make_shared(405, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, tmp)); texts.push_back(std::make_shared(592, y + i * 50, FONT_MEDIUM, ETextAlignment::CENTER, color, std::to_string(curData["points"].Integer()))); @@ -259,9 +259,9 @@ int CHighScoreInputScreen::addEntry(std::string text) { JsonNode newNode = JsonNode(); newNode["player"].String() = text; if(calc.isCampaign) - newNode["campaign"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaign; + newNode["campaignName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].campaignName; else - newNode["land"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].land; + newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName; newNode["days"].Integer() = calc.calculate().sumDays; newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 6c9ae2513..2fb31a949 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -27,8 +27,8 @@ public: bool usedCheat; bool hasGrail; bool allDefeated; - std::string campaign; - std::string land; + std::string campaignName; + std::string scenarioName; }; class HighScoreCalculation @@ -68,7 +68,7 @@ private: int highlighted; public: - CHighScoreScreen(HighScorePage highscorepage = HighScorePage::SCENARIO, int highlighted = -1); + CHighScoreScreen(HighScorePage highscorepage, int highlighted = -1); }; class CHighScoreInput : public CWindowObject diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index d922eda25..b5732c621 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -392,7 +392,7 @@ void CMainMenu::startTutorial() void CMainMenu::openHighScoreScreen() { - GH.windows().createAndPushWindow(); + GH.windows().createAndPushWindow(CHighScoreScreen::HighScorePage::SCENARIO); return; } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 7c5e1e21d..1e987df1d 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -109,11 +109,18 @@ bool PlayerMessageProcessor::handleHostCommand(PlayerColor player, const std::st } if(words.size() == 2 && words[1] == "cheaters") { - if (cheaters.empty()) - broadcastSystemMessage("No cheaters registered!"); + int playersCheated = 0; + for (const auto & player : gameHandler->gameState()->players) + { + if(player.second.cheated) + { + broadcastSystemMessage("Player " + player.first.toString() + " is cheater!"); + playersCheated++; + } + } - for (auto const & entry : cheaters) - broadcastSystemMessage("Player " + entry.toString() + " is cheater!"); + if (!playersCheated) + broadcastSystemMessage("No cheaters registered!"); return true; } @@ -411,7 +418,10 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo std::vector parameters = words; - cheaters.insert(i.first); + PlayerCheated pc; + pc.player = i.first; + gameHandler->sendAndApply(&pc); + playerTargetedCheat = true; parameters.erase(parameters.begin()); @@ -430,7 +440,10 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo if (!playerTargetedCheat) executeCheatCode(cheatName, player, currObj, words); - cheaters.insert(player); + PlayerCheated pc; + pc.player = player; + gameHandler->sendAndApply(&pc); + return true; } @@ -513,13 +526,7 @@ void PlayerMessageProcessor::executeCheatCode(const std::string & cheatName, Pla assert(callbacks.count(cheatName)); if (callbacks.count(cheatName)) - { - PlayerCheated pc; - pc.player = player; - gameHandler->sendAndApply(&pc); - callbacks.at(cheatName)(); - } } void PlayerMessageProcessor::sendSystemMessage(std::shared_ptr connection, const std::string & message) diff --git a/server/processors/PlayerMessageProcessor.h b/server/processors/PlayerMessageProcessor.h index d8f9e9878..47d2a8a4a 100644 --- a/server/processors/PlayerMessageProcessor.h +++ b/server/processors/PlayerMessageProcessor.h @@ -21,8 +21,6 @@ class CGameHandler; class PlayerMessageProcessor { - std::set cheaters; - void executeCheatCode(const std::string & cheatName, PlayerColor player, ObjectInstanceID currObj, const std::vector & arguments ); bool handleCheatCode(const std::string & cheatFullCommand, PlayerColor player, ObjectInstanceID currObj); bool handleHostCommand(PlayerColor player, const std::string & message); @@ -60,6 +58,5 @@ public: template void serialize(Handler &h, const int version) { - h & cheaters; } }; From 6225d8585d215569c4a919171bea1915c306d1c8 Mon Sep 17 00:00:00 2001 From: Andreas Grois Date: Mon, 25 Sep 2023 21:26:23 +0200 Subject: [PATCH 0569/1248] Make ERM compile again. This is not a proper clean up. It does not bring the code up to the current state of the rest of the codebase. However, the module now compiles again. --- scripting/erm/ERMInterpreter.cpp | 279 +++++++++++++++++-------------- scripting/erm/ERMInterpreter.h | 13 +- scripting/erm/ERMParser.h | 1 + 3 files changed, 151 insertions(+), 142 deletions(-) diff --git a/scripting/erm/ERMInterpreter.cpp b/scripting/erm/ERMInterpreter.cpp index 663cb3572..e4f60105a 100644 --- a/scripting/erm/ERMInterpreter.cpp +++ b/scripting/erm/ERMInterpreter.cpp @@ -163,7 +163,7 @@ namespace ERMConverter Variable operator()(const TVarExpNotMacro & val) const { if(val.val.has_value()) - return Variable(val.varsym, val.val.get()); + return Variable(val.varsym, *val.val); else return Variable(val.varsym, 0); } @@ -392,7 +392,7 @@ namespace ERMConverter if(trig.params.has_value()) { - for(auto & p : trig.params.get()) + for(auto & p : *trig.params) optionParams.push_back(std::visit(BodyOption(), p)); } @@ -572,7 +572,7 @@ namespace ERMConverter { if(option.params.has_value()) { - for(auto & p : option.params.get()) + for(auto & p : *option.params) { std::string macroName = std::visit(MC_S(), p); @@ -739,7 +739,7 @@ namespace ERMConverter if(trig.params.has_value()) { - for(auto & p : trig.params.get()) + for(auto & p : *trig.params) optionParams.push_back(std::visit(BodyOption(), p)); } @@ -759,10 +759,10 @@ namespace ERMConverter break; case 'H': //checking if string is empty { - if(!trig.params.has_value() || trig.params.get().size() != 1) + if(!trig.params.has_value() || trig.params->size() != 1) throw EScriptExecError("VR:H option takes exactly 1 parameter!"); - std::string opt = std::visit(VR_H(), trig.params.get()[0]); + std::string opt = std::visit(VR_H(), (*trig.params)[0]); boost::format fmt("ERM.VR(%s):H(%s)"); fmt % v.str() % opt; putLine(fmt.str()); @@ -770,10 +770,10 @@ namespace ERMConverter break; case 'U': { - if(!trig.params.has_value() || trig.params.get().size() != 1) + if(!trig.params.has_value() || trig.params->size() != 1) throw EScriptExecError("VR:H/U need 1 parameter!"); - std::string opt = std::visit(VR_S(), trig.params.get()[0]); + std::string opt = std::visit(VR_S(), (*trig.params)[0]); boost::format fmt("ERM.VR(%s):%c(%s)"); fmt % v.str() % (trig.optionCode) % opt; putLine(fmt.str()); @@ -781,10 +781,10 @@ namespace ERMConverter break; case 'M': //string operations { - if(!trig.params.has_value() || trig.params.get().size() < 2) + if(!trig.params.has_value() || trig.params->size() < 2) throw EScriptExecError("VR:M needs at least 2 parameters!"); - std::string opt = std::visit(VR_X(), trig.params.get()[0]); + std::string opt = std::visit(VR_X(), (*trig.params)[0]); int paramIndex = 1; if(opt == "3") @@ -795,16 +795,16 @@ namespace ERMConverter } else { - auto target = std::visit(VR_X(), trig.params.get()[paramIndex++]); + auto target = std::visit(VR_X(), (*trig.params)[paramIndex++]); boost::format fmt("%s = ERM.VR(%s):M%s("); fmt % target % v.str() % opt; put(fmt.str()); } - for(int i = paramIndex; i < trig.params.get().size(); i++) + for(int i = paramIndex; i < trig.params->size(); i++) { - opt = std::visit(VR_X(), trig.params.get()[i]); + opt = std::visit(VR_X(), (*trig.params)[i]); if(i > paramIndex) put(","); put(opt); } @@ -814,10 +814,10 @@ namespace ERMConverter break; case 'X': //bit xor { - if(!trig.params.has_value() || trig.params.get().size() != 1) + if(!trig.params.has_value() || trig.params->size() != 1) throw EScriptExecError("VR:X option takes exactly 1 parameter!"); - std::string opt = std::visit(VR_X(), trig.params.get()[0]); + std::string opt = std::visit(VR_X(), (*trig.params)[0]); boost::format fmt("%s = bit.bxor(%s, %s)"); fmt % v.str() % v.str() % opt;putLine(fmt.str()); @@ -831,10 +831,10 @@ namespace ERMConverter break; case 'S': //setting variable { - if(!trig.params.has_value() || trig.params.get().size() != 1) + if(!trig.params.has_value() || trig.params->size() != 1) throw EScriptExecError("VR:S option takes exactly 1 parameter!"); - std::string opt = std::visit(VR_S(), trig.params.get()[0]); + std::string opt = std::visit(VR_S(), (*trig.params)[0]); put(v.str()); put(" = "); put(opt); @@ -849,10 +849,10 @@ namespace ERMConverter break; case 'V': //convert string to value { - if(!trig.params.has_value() || trig.params.get().size() != 1) + if(!trig.params.has_value() || trig.params->size() != 1) throw EScriptExecError("VR:V option takes exactly 1 parameter!"); - std::string opt = std::visit(VR_X(), trig.params.get()[0]); + std::string opt = std::visit(VR_X(), (*trig.params)[0]); boost::format fmt("%s = tostring(%s)"); fmt % v.str() % opt; putLine(fmt.str()); @@ -877,7 +877,7 @@ namespace ERMConverter { if(body.has_value()) { - const ERM::Tbody & bo = body.get(); + const ERM::Tbody & bo = *body; for(int g=0; gget(), op); } } @@ -1067,7 +1067,7 @@ namespace ERMConverter break; } - convertConditionInner(cond.rhs.get().get(), cond.ctype); + convertConditionInner(cond.rhs->get(), cond.ctype); } putLine(" then "); @@ -1081,7 +1081,7 @@ namespace ERMConverter if(name=="if") { if(condition.has_value()) - convertCondition(condition.get()); + convertCondition(*condition); else putLine("if true then"); } @@ -1097,7 +1097,7 @@ namespace ERMConverter { if(condition.has_value()) { - convertCondition(condition.get()); + convertCondition(*condition); convert(name, identifier, body); putLine("end"); } @@ -1181,7 +1181,7 @@ namespace ERMConverter { (*out) << "{}"; } - void operator()(const VNode & opt) const; + void operator()(const boost::recursive_wrapper & opt) const; void operator()(const VSymbol & opt) const { @@ -1192,7 +1192,7 @@ namespace ERMConverter TLiteralEval tmp; (*out) << std::visit(tmp, opt); } - void operator()(ERM const ::Tcommand & opt) const + void operator()(const ERM::Tcommand & opt) const { //this is how FP works, evaluation == producing side effects //TODO: can we evaluate to smth more useful? @@ -1202,9 +1202,9 @@ namespace ERMConverter } }; - void VOptionEval::operator()(const VNode & opt) const + void VOptionEval::operator()(const boost::recursive_wrapper & opt) const { - VNode tmpn(opt); + VNode tmpn(opt.get()); (*out) << "{"; @@ -1375,35 +1375,35 @@ struct ScriptScanner } void operator()(const TERMline & cmd) const { - if(cmd.which() == 0) //TCommand + if(std::holds_alternative(cmd)) //TCommand { Tcommand tcmd = std::get(cmd); - switch (tcmd.cmd.which()) + struct Visitor { - case 0: //trigger + void operator()(const ERM::Ttrigger& t) const { Trigger trig; - trig.line = lp; - interpreter->triggers[ TriggerType(std::get(tcmd.cmd).name) ].push_back(trig); + trig.line = l; + i->triggers[ TriggerType(t.name) ].push_back(trig); } - break; - case 1: //instruction + void operator()(const ERM::Tinstruction&) const { - interpreter->instructions.push_back(lp); + i->instructions.push_back(l); } - break; - case 3: //post trigger + void operator()(const ERM::Treceiver&) const {} + void operator()(const ERM::TPostTrigger& pt) const { Trigger trig; - trig.line = lp; - interpreter->postTriggers[ TriggerType(std::get(tcmd.cmd).name) ].push_back(trig); + trig.line = l; + i->postTriggers[ TriggerType(pt.name) ].push_back(trig); } - break; - default: - break; - } - } + const decltype(interpreter)& i; + const LinePointer& l; + }; + Visitor v{interpreter, lp}; + std::visit(v, tcmd.cmd); + } } }; @@ -1421,68 +1421,85 @@ ERMInterpreter::~ERMInterpreter() bool ERMInterpreter::isATrigger( const ERM::TLine & line ) { - switch(line.which()) + if(std::holds_alternative(line)) { - case 0: //v-exp - { - TVExp vexp = std::get(line); - if(vexp.children.empty()) - return false; + TVExp vexp = std::get(line); + if(vexp.children.empty()) + return false; - switch (getExpType(vexp.children[0])) - { - case SYMBOL: - return false; - break; - case TCMD: - return isCMDATrigger( std::get(vexp.children[0]) ); - break; - default: - return false; - break; - } - } - break; - case 1: //erm + switch (getExpType(vexp.children[0])) { - TERMline ermline = std::get(line); - switch(ermline.which()) - { - case 0: //tcmd - return isCMDATrigger( std::get(ermline) ); - break; - default: - return false; - break; - } + case SYMBOL: + return false; + break; + case TCMD: + return isCMDATrigger( std::get(vexp.children[0]) ); + break; + default: + return false; + break; } - break; - default: - assert(0); //it should never happen - break; } - assert(0); + else if(std::holds_alternative(line)) + { + TERMline ermline = std::get(line); + return std::holds_alternative(ermline) && isCMDATrigger( std::get(ermline) ); + } + else + { + assert(0); + } return false; } ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) { - //MAINTENANCE: keep it correct! - return static_cast(opt.which()); + struct Visitor + { + ERM::EVOtions operator()(const boost::recursive_wrapper&) const + { + return ERM::EVOtions::VEXP; + } + ERM::EVOtions operator()(const ERM::TSymbol&) const + { + return ERM::EVOtions::SYMBOL; + } + ERM::EVOtions operator()(char) const + { + return ERM::EVOtions::CHAR; + } + ERM::EVOtions operator()(double) const + { + return ERM::EVOtions::DOUBLE; + } + ERM::EVOtions operator()(int) const + { + return ERM::EVOtions::INT; + } + ERM::EVOtions operator()(const ERM::Tcommand&) const + { + return ERM::EVOtions::TCMD; + } + ERM::EVOtions operator()(const ERM::TStringConstant&) const + { + return ERM::EVOtions::STRINGC; + } + }; + const Visitor v; + return std::visit(v, opt); } bool ERMInterpreter::isCMDATrigger(const ERM::Tcommand & cmd) { - switch (cmd.cmd.which()) + struct Visitor { - case 0: //trigger - case 3: //post trigger - return true; - break; - default: - return false; - break; - } + bool operator()(const ERM::Ttrigger&) const { return true; } + bool operator()(const ERM::TPostTrigger&) const { return true; } + bool operator()(const ERM::Tinstruction&) const { return false; } + bool operator()(const ERM::Treceiver&) const { return false; } + }; + const Visitor v; + return std::visit(v, cmd.cmd); } ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) @@ -1492,17 +1509,17 @@ ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) { - if(line.which() == 1) + if(std::holds_alternative(line)) { ERM::TERMline &tl = std::get(line); - if(tl.which() == 0) + if(std::holds_alternative(tl)) { ERM::Tcommand &tcm = std::get(tl); - if(tcm.cmd.which() == 0) + if(std::holds_alternative(tcm.cmd)) { return std::get(tcm.cmd); } - else if(tcm.cmd.which() == 3) + else if(std::holds_alternative(tcm.cmd)) { return std::get(tcm.cmd); } @@ -1569,6 +1586,40 @@ namespace VERMInterpreter { VOption convertToVOption(const ERM::TVOption & tvo) { + struct OptionConverterVisitor + { + VOption operator()(const boost::recursive_wrapper& cmd) const + { + return boost::recursive_wrapper(VNode(cmd.get())); + } + VOption operator()(const ERM::TSymbol & cmd) const + { + if(cmd.symModifier.empty()) + return VSymbol(cmd.sym); + else + return boost::recursive_wrapper(VNode(cmd)); + } + VOption operator()(const char & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const double & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const int & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const ERM::Tcommand & cmd) const + { + return cmd; + } + VOption operator()(const ERM::TStringConstant & cmd) const + { + return TLiteral(cmd.str); + } + }; return std::visit(OptionConverterVisitor(), tvo); } @@ -1706,38 +1757,6 @@ namespace VERMInterpreter return ret; } - VOption OptionConverterVisitor::operator()(ERM const ::TVExp & cmd) const - { - return VNode(cmd); - } - VOption OptionConverterVisitor::operator()(ERM const ::TSymbol & cmd) const - { - if(cmd.symModifier.empty()) - return VSymbol(cmd.sym); - else - return VNode(cmd); - } - VOption OptionConverterVisitor::operator()(const char & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(const double & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(const int & cmd) const - { - return TLiteral(cmd); - } - VOption OptionConverterVisitor::operator()(ERM const ::Tcommand & cmd) const - { - return cmd; - } - VOption OptionConverterVisitor::operator()(ERM const ::TStringConstant & cmd) const - { - return TLiteral(cmd.str); - } - VermTreeIterator VOptionList::cdr() { VermTreeIterator ret(*this); diff --git a/scripting/erm/ERMInterpreter.h b/scripting/erm/ERMInterpreter.h index baf3d317e..ed2550977 100644 --- a/scripting/erm/ERMInterpreter.h +++ b/scripting/erm/ERMInterpreter.h @@ -134,7 +134,7 @@ namespace VERMInterpreter "TH", "TM" }; - for(int i=0; i(i); @@ -278,17 +278,6 @@ namespace VERMInterpreter VermTreeIterator cdr(); }; - struct OptionConverterVisitor - { - VOption operator()(ERM const ::TVExp & cmd) const; - VOption operator()(ERM const ::TSymbol & cmd) const; - VOption operator()(const char & cmd) const; - VOption operator()(const double & cmd) const; - VOption operator()(const int & cmd) const; - VOption operator()(ERM const ::Tcommand & cmd) const; - VOption operator()(ERM const ::TStringConstant & cmd) const; - }; - struct VNode { private: diff --git a/scripting/erm/ERMParser.h b/scripting/erm/ERMParser.h index 29d92e61b..95b2ccbbf 100644 --- a/scripting/erm/ERMParser.h +++ b/scripting/erm/ERMParser.h @@ -10,6 +10,7 @@ #pragma once #include +#include namespace spirit = boost::spirit; From 096036dc9f3a8fed98053b0e41f6582534c898f2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 22:58:59 +0200 Subject: [PATCH 0570/1248] spell icon --- client/widgets/CArtifactHolder.cpp | 27 +++++++++++++++++++++++-- client/widgets/CArtifactHolder.h | 1 + client/widgets/CWindowWithArtifacts.cpp | 15 +++++++++++++- 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index b53237c9a..3b8b563d4 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -18,6 +18,7 @@ #include "../windows/GUIClasses.h" #include "../render/Canvas.h" #include "../render/Colors.h" +#include "../render/IRenderHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" @@ -25,6 +26,7 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/CConfigHandler.h" void CArtPlace::setInternals(const CArtifactInstance * artInst) { @@ -33,12 +35,12 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) if(!artInst) { image->disable(); + imageSpell->disable(); text.clear(); hoverText = CGI->generaltexth->allTexts[507]; return; } - image->enable(); - image->setFrame(artInst->artType->getIconIndex()); + if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL) { auto spellID = artInst->getScrollSpellID(); @@ -48,14 +50,31 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) baseType = CComponent::spell; type = spellID; bonusValue = 0; + + if(!settings["general"]["enableUiEnhancements"].Bool()) + { + image->enable(); + imageSpell->disable(); + image->setFrame(artInst->artType->getIconIndex()); + } + else + { + imageSpell->enable(); + image->disable(); + imageSpell->setFrame(spellID.num); + } } } else { + image->enable(); + imageSpell->disable(); + image->setFrame(artInst->artType->getIconIndex()); baseType = CComponent::artifact; type = artInst->getTypeId(); bonusValue = 0; } + text = artInst->getDescription(); } @@ -247,9 +266,13 @@ void CHeroArtPlace::createImage() else if(ourArt) imageIndex = ourArt->artType->getIconIndex(); + imageSpell = std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("spellscr")), 0, Rect(0, 5, 44, 34)); image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); if(!ourArt) + { image->disable(); + imageSpell->disable(); + } selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); selection->disable(); diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index d8cc43aba..f4403c1ca 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -34,6 +34,7 @@ class CArtPlace : public LRClickableAreaWTextComp { protected: std::shared_ptr image; + std::shared_ptr imageSpell; const CArtifactInstance * ourArt; void setInternals(const CArtifactInstance * artInst); diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 7725c16c3..4941db0c6 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -14,6 +14,10 @@ #include "../gui/CursorHandler.h" #include "../gui/WindowHandler.h" +#include "../render/IRenderHandler.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" + #include "CComponent.h" #include "../windows/CHeroWindow.h" @@ -270,7 +274,16 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const if(pickedArtInst) { markPossibleSlots(); - CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); + + if(pickedArtInst->getTypeId() == ArtifactID::SPELL_SCROLL && pickedArtInst->getScrollSpellID().num >= 0 && settings["general"]["enableUiEnhancements"].Bool()) + { + auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin("spellscr")); + anim->load(pickedArtInst->getScrollSpellID().num); + std::shared_ptr img = anim->getImage(pickedArtInst->getScrollSpellID().num); + CCS->curh->dragAndDropCursor(img->scaleFast(Point(44, 34))); + } + else + CCS->curh->dragAndDropCursor(AnimationPath::builtin("artifact"), pickedArtInst->artType->getIconIndex()); } else { From 6b5f6030e24d9c96b84661779b9120d18d734774 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:02:16 +0200 Subject: [PATCH 0571/1248] header --- client/widgets/CWindowWithArtifacts.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 4941db0c6..eddcf697d 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -29,6 +29,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/CConfigHandler.h" void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) { From ab556153590e87a116148d99d37733fdfd141249 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 10:28:10 +0200 Subject: [PATCH 0572/1248] remove double code --- client/widgets/CArtifactHolder.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 3b8b563d4..6747d2454 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -41,6 +41,10 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) return; } + image->enable(); + imageSpell->disable(); + image->setFrame(artInst->artType->getIconIndex()); + if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL) { auto spellID = artInst->getScrollSpellID(); @@ -51,13 +55,7 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) type = spellID; bonusValue = 0; - if(!settings["general"]["enableUiEnhancements"].Bool()) - { - image->enable(); - imageSpell->disable(); - image->setFrame(artInst->artType->getIconIndex()); - } - else + if(settings["general"]["enableUiEnhancements"].Bool()) { imageSpell->enable(); image->disable(); @@ -67,9 +65,6 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) } else { - image->enable(); - imageSpell->disable(); - image->setFrame(artInst->artType->getIconIndex()); baseType = CComponent::artifact; type = artInst->getTypeId(); bonusValue = 0; From 02c82cb35bdfc905644a9400f4fb2d0dfc4330c6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 12:01:16 +0200 Subject: [PATCH 0573/1248] order --- server/processors/PlayerMessageProcessor.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 1e987df1d..7cadfda29 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -437,13 +437,13 @@ bool PlayerMessageProcessor::handleCheatCode(const std::string & cheat, PlayerCo executeCheatCode(cheatName, i.first, h->id, parameters); } - if (!playerTargetedCheat) - executeCheatCode(cheatName, player, currObj, words); - PlayerCheated pc; pc.player = player; gameHandler->sendAndApply(&pc); + if (!playerTargetedCheat) + executeCheatCode(cheatName, player, currObj, words); + return true; } From b2f30f78faf86d3ede9d18237362fdd49ac4fcf5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 22:09:16 +0300 Subject: [PATCH 0574/1248] Allow one AI and human to act simultaneously --- server/CGameHandler.cpp | 5 +++ server/CGameHandler.h | 1 + server/processors/TurnOrderProcessor.cpp | 41 ++++++++++++++++++++---- server/processors/TurnOrderProcessor.h | 6 ++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 43dee22d4..8fc6e1a92 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2188,6 +2188,11 @@ bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr return connections.at(player).count(c); } +bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const +{ + return connections.at(left) == connections.at(right); +} + bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos) { const CArmedInstance * s1 = static_cast(getObjInstance(id)); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index b9e126dd7..b1bf301f5 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -176,6 +176,7 @@ public: void handleClientDisconnection(std::shared_ptr c); void handleReceivedPack(CPackForServer * pack); bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; + bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index e15b5f442..028a8fce0 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -24,9 +24,41 @@ TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): } +int TurnOrderProcessor::simturnsTurnsLimit() const +{ + // TODO + return 7; +} + +bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const +{ + // TODO + return false; +} + bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor waiting) const { - return false; + const auto * activeInfo = gameHandler->getPlayerState(active, false); + const auto * waitingInfo = gameHandler->getPlayerState(waiting, false); + + assert(active != waiting); + assert(activeInfo); + assert(waitingInfo); + + if (gameHandler->getDate(Date::DAY) > simturnsTurnsLimit()) + return false; + + if (gameHandler->hasBothPlayersAtSameConnection(active, waiting)) + { + // only one AI and one human can play simultaneoulsy from single connection + if (activeInfo->human == waitingInfo->human) + return false; + } + + if (playersInContact(active, waiting)) + return false; + + return true; } bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) const @@ -107,8 +139,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) pst.queryID = turnQuery->queryID; gameHandler->sendAndApply(&pst); - assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); + assert(!actingPlayers.empty()); } void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) @@ -130,8 +161,6 @@ void TurnOrderProcessor::doEndPlayerTurn(PlayerColor which) doStartNewDay(); assert(!actingPlayers.empty()); - assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); } void TurnOrderProcessor::addPlayer(PlayerColor which) @@ -152,8 +181,6 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) doStartNewDay(); assert(!actingPlayers.empty()); - assert(actingPlayers.size() == 1); // No simturns yet :( - assert(gameHandler->isPlayerMakingTurn(*actingPlayers.begin())); } bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index 39990c6ae..569597e16 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -21,6 +21,12 @@ class TurnOrderProcessor : boost::noncopyable std::set actingPlayers; std::set actedPlayers; + /// Returns date on which simturns must end unconditionally + int simturnsTurnsLimit() const; + + /// Returns true if players are close enough to each other for their heroes to meet on this turn + bool playersInContact(PlayerColor left, PlayerColor right) const; + /// Returns true if waiting player can act alongside with currently acting player bool canActSimultaneously(PlayerColor active, PlayerColor waiting) const; From 64c82c9133538f26af6ef59d400696bee9c02bed Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 22:09:35 +0300 Subject: [PATCH 0575/1248] Fixed UI locking in simturns --- client/CPlayerInterface.cpp | 45 ++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 135bd1f81..67c7c4d8e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -170,6 +170,13 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: // always recreate advmap interface to avoid possible memory-corruption bugs adventureInt.reset(new AdventureMapInterface()); + + if(GH.windows().findWindows().empty()) + { + // after map load - remove all active windows and replace them with adventure map + GH.windows().clear(); + GH.windows().pushWindow(adventureInt); + } } void CPlayerInterface::playerEndsTurn(PlayerColor player) @@ -182,32 +189,14 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player) void CPlayerInterface::playerStartsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; - - movementController->onPlayerTurnStarted(); - - if(GH.windows().findWindows().empty()) - { - // after map load - remove all active windows and replace them with adventure map - GH.windows().clear(); - GH.windows().pushWindow(adventureInt); - } - - // close window from another player - if(auto w = GH.windows().topWindow()) - if(w->ID == QueryID::NONE && player != playerID) - w->close(); - - // remove all dialogs that do not expect query answer - while (!GH.windows().topWindow() && !GH.windows().topWindow()) - GH.windows().popWindows(1); - if (player != playerID && LOCPLINT == this) { waitWhileDialog(); bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman(); - adventureInt->onEnemyTurnStarted(player, isHuman); + if (makingTurn == false) + adventureInt->onEnemyTurnStarted(player, isHuman); } } @@ -261,6 +250,21 @@ void CPlayerInterface::yourTurn(QueryID queryID) LOCPLINT = this; GH.curInt = this; + // close window from another player + if(auto w = GH.windows().topWindow()) + { + assert(0);// what is this? + if(w->ID == QueryID::NONE) + w->close(); + } + + // remove all dialogs that do not expect query answer + while (!GH.windows().topWindow() && !GH.windows().topWindow()) + { + assert(0);// what is this? + GH.windows().popWindows(1); + } + NotificationHandler::notify("Your turn"); if(settings["general"]["startTurnAutosave"].Bool()) { @@ -335,6 +339,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID) } cb->selectionMade(0, queryID); + movementController->onPlayerTurnStarted(); } void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) From 64c43c91dcdd8de80e0678183e944b2924955c80 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 19:46:27 +0300 Subject: [PATCH 0576/1248] Added hardcoded min/max simturns duration --- server/processors/TurnOrderProcessor.cpp | 17 +++++++++++++---- server/processors/TurnOrderProcessor.h | 5 ++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 028a8fce0..3ccf3b7c7 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -24,7 +24,13 @@ TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): } -int TurnOrderProcessor::simturnsTurnsLimit() const +int TurnOrderProcessor::simturnsTurnsMaxLimit() const +{ + // TODO + return 28; +} + +int TurnOrderProcessor::simturnsTurnsMinLimit() const { // TODO return 7; @@ -45,9 +51,6 @@ bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor wa assert(activeInfo); assert(waitingInfo); - if (gameHandler->getDate(Date::DAY) > simturnsTurnsLimit()) - return false; - if (gameHandler->hasBothPlayersAtSameConnection(active, waiting)) { // only one AI and one human can play simultaneoulsy from single connection @@ -55,6 +58,12 @@ bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor wa return false; } + if (gameHandler->getDate(Date::DAY) < simturnsTurnsMinLimit()) + return true; + + if (gameHandler->getDate(Date::DAY) > simturnsTurnsMaxLimit()) + return false; + if (playersInContact(active, waiting)) return false; diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index 569597e16..8812f0099 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -22,7 +22,10 @@ class TurnOrderProcessor : boost::noncopyable std::set actedPlayers; /// Returns date on which simturns must end unconditionally - int simturnsTurnsLimit() const; + int simturnsTurnsMaxLimit() const; + + /// Returns date until which simturns must play unconditionally + int simturnsTurnsMinLimit() const; /// Returns true if players are close enough to each other for their heroes to meet on this turn bool playersInContact(PlayerColor left, PlayerColor right) const; From 56074e18a9e593aed284c631ebef111b9eb7d6c1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 19:50:07 +0300 Subject: [PATCH 0577/1248] Block turn end button after player ends turn --- client/adventureMap/AdventureMapShortcuts.cpp | 7 ++++++- client/adventureMap/AdventureMapShortcuts.h | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 9087b835f..30edda987 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -71,7 +71,7 @@ std::vector AdventureMapShortcuts::getShortcuts() { EShortcut::ADVENTURE_GAME_OPTIONS, optionInMapView(), [this]() { this->adventureOptions(); } }, { EShortcut::GLOBAL_OPTIONS, optionInMapView(), [this]() { this->systemOptions(); } }, { EShortcut::ADVENTURE_NEXT_HERO, optionHasNextHero(), [this]() { this->nextHero(); } }, - { EShortcut::GAME_END_TURN, optionInMapView(), [this]() { this->endTurn(); } }, + { EShortcut::GAME_END_TURN, optionCanEndTurn(), [this]() { this->endTurn(); } }, { EShortcut::ADVENTURE_THIEVES_GUILD, optionInMapView(), [this]() { this->showThievesGuild(); } }, { EShortcut::ADVENTURE_VIEW_SCENARIO, optionInMapView(), [this]() { this->showScenarioInfo(); } }, { EShortcut::GAME_SAVE_GAME, optionInMapView(), [this]() { this->saveGame(); } }, @@ -453,6 +453,11 @@ bool AdventureMapShortcuts::optionHasNextHero() return optionInMapView() && nextSuitableHero != nullptr; } +bool AdventureMapShortcuts::optionCanEndTurn() +{ + return optionInMapView() && LOCPLINT->makingTurn; +} + bool AdventureMapShortcuts::optionSpellcasting() { return state == EAdventureState::CASTING_SPELL; diff --git a/client/adventureMap/AdventureMapShortcuts.h b/client/adventureMap/AdventureMapShortcuts.h index 30775c0c0..435c321cc 100644 --- a/client/adventureMap/AdventureMapShortcuts.h +++ b/client/adventureMap/AdventureMapShortcuts.h @@ -78,6 +78,7 @@ public: bool optionHeroCanMove(); bool optionHasNextHero(); bool optionCanVisitObject(); + bool optionCanEndTurn(); bool optionSpellcasting(); bool optionInMapView(); bool optionInWorldView(); From c8a96647c3cae52a0cd004f050cc4358486c784d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 22:26:38 +0300 Subject: [PATCH 0578/1248] Add turn limit option to pathfinder --- lib/pathfinder/CPathfinder.cpp | 2 ++ lib/pathfinder/PathfinderOptions.cpp | 1 + lib/pathfinder/PathfinderOptions.h | 3 +++ 3 files changed, 6 insertions(+) diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 25ea77981..1cb3b7163 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -121,6 +121,8 @@ void CPathfinder::calculatePaths() movement = hlp->getMaxMovePoints(source.node->layer); if(!hlp->passOneTurnLimitCheck(source)) continue; + if(turn >= hlp->options.turnLimit) + continue; } source.isInitialPosition = source.nodeHero == hlp->hero; diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 02b5d8b6a..42af2f53b 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -30,6 +30,7 @@ PathfinderOptions::PathfinderOptions() , lightweightFlyingMode(false) , oneTurnSpecialLayersLimit(true) , originalMovementRules(false) + , turnLimit(std::numeric_limits::max()) { } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 32859ac82..f26cf6273 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -68,6 +68,9 @@ struct DLL_LINKAGE PathfinderOptions /// I find it's reasonable limitation, but it's will make some movements more expensive than in H3. bool originalMovementRules; + /// Max number of turns to compute. Default = infinite + uint8_t turnLimit; + PathfinderOptions(); }; From 541f7590c5b00ee170ce0dc0cc674e0198019541 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 22:26:56 +0300 Subject: [PATCH 0579/1248] Add simple detection of player-player contact --- server/processors/TurnOrderProcessor.cpp | 52 +++++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 3ccf3b7c7..ed5d61ac7 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -17,6 +17,8 @@ #include "../../lib/CPlayerState.h" #include "../../lib/NetPacks.h" +#include "../../lib/pathfinder/CPathfinder.h" +#include "../../lib/pathfinder/PathfinderOptions.h" TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): gameHandler(owner) @@ -33,12 +35,58 @@ int TurnOrderProcessor::simturnsTurnsMaxLimit() const int TurnOrderProcessor::simturnsTurnsMinLimit() const { // TODO - return 7; + return 0; } bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const { - // TODO + // TODO: refactor, cleanup and optimize + + boost::multi_array leftReachability; + boost::multi_array rightReachability; + + int3 mapSize = gameHandler->getMapSize(); + + leftReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + rightReachability.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + const auto * leftInfo = gameHandler->getPlayerState(left, false); + const auto * rightInfo = gameHandler->getPlayerState(right, false); + + for(const auto & hero : leftInfo->heroes) + { + CPathsInfo out(mapSize, hero); + auto config = std::make_shared(out, gameHandler->gameState(), hero); + CPathfinder pathfinder(gameHandler->gameState(), config); + pathfinder.calculatePaths(); + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (out.getNode({x,y,z})->reachable()) + leftReachability[z][x][y] = true; + } + + for(const auto & hero : rightInfo->heroes) + { + CPathsInfo out(mapSize, hero); + auto config = std::make_shared(out, gameHandler->gameState(), hero); + CPathfinder pathfinder(gameHandler->gameState(), config); + pathfinder.calculatePaths(); + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (out.getNode({x,y,z})->reachable()) + rightReachability[z][x][y] = true; + } + + for (int z = 0; z < mapSize.z; ++z) + for (int y = 0; y < mapSize.y; ++y) + for (int x = 0; x < mapSize.x; ++x) + if (leftReachability[z][x][y] && rightReachability[z][x][y]) + return true; + return false; } From d257fb37f0587ae512af649e882254bddd9d78a6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 23:17:25 +0300 Subject: [PATCH 0580/1248] Use optional instead of Json for queries --- CCallback.cpp | 6 ++---- CCallback.h | 4 ++-- client/CPlayerInterface.cpp | 7 ++----- lib/NetPacks.h | 4 ++-- lib/pathfinder/CPathfinder.h | 2 +- lib/spells/AdventureSpellMechanics.cpp | 6 +++--- lib/spells/ISpellMechanics.h | 2 +- server/CGameHandler.cpp | 5 +++-- server/CGameHandler.h | 2 +- server/ServerSpellCastEnvironment.cpp | 4 ++-- server/ServerSpellCastEnvironment.h | 4 ++-- server/queries/BattleQueries.cpp | 4 ++-- server/queries/BattleQueries.h | 2 +- server/queries/CQuery.cpp | 27 +++++++++++--------------- server/queries/CQuery.h | 27 +++++++++----------------- server/queries/MapQueries.cpp | 6 +++--- server/queries/MapQueries.h | 6 +++--- test/game/CGameStateTest.cpp | 2 +- 18 files changed, 51 insertions(+), 69 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index 4768aaae8..d70af295b 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -41,12 +41,10 @@ bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) int CCallback::selectionMade(int selection, QueryID queryID) { - JsonNode reply(JsonNode::JsonType::DATA_INTEGER); - reply.Integer() = selection; - return sendQueryReply(reply, queryID); + return sendQueryReply(selection, queryID); } -int CCallback::sendQueryReply(const JsonNode & reply, QueryID queryID) +int CCallback::sendQueryReply(std::optional reply, QueryID queryID) { ASSERT_IF_CALLED_WITH_PLAYER if(queryID == QueryID(-1)) diff --git a/CCallback.h b/CCallback.h index 8fb7970f9..32b180828 100644 --- a/CCallback.h +++ b/CCallback.h @@ -82,7 +82,7 @@ public: virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; virtual int selectionMade(int selection, QueryID queryID) =0; - virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0; + virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second @@ -159,7 +159,7 @@ public: bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); int selectionMade(int selection, QueryID queryID) override; - int sendQueryReply(const JsonNode & reply, QueryID queryID) override; + int sendQueryReply(std::optional reply, QueryID queryID) override; int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 67c7c4d8e..77735e69e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1063,15 +1063,12 @@ void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component auto selectCallback = [=](int selection) { - JsonNode reply(JsonNode::JsonType::DATA_INTEGER); - reply.Integer() = selection; - cb->sendQueryReply(reply, askID); + cb->sendQueryReply(selection, askID); }; auto cancelCallback = [=]() { - JsonNode reply(JsonNode::JsonType::DATA_NULL); - cb->sendQueryReply(reply, askID); + cb->sendQueryReply(std::nullopt, askID); }; const std::string localTitle = title.toString(); diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 093330040..5480dc867 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -2609,14 +2609,14 @@ struct DLL_LINKAGE BuildBoat : public CPackForServer struct DLL_LINKAGE QueryReply : public CPackForServer { QueryReply() = default; - QueryReply(const QueryID & QID, const JsonNode & Reply) + QueryReply(const QueryID & QID, std::optional Reply) : qid(QID) , reply(Reply) { } QueryID qid; PlayerColor player; - JsonNode reply; + std::optional reply; virtual void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index f95b0cbf4..0071f41eb 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -19,7 +19,7 @@ class CGWhirlpool; struct TurnInfo; struct PathfinderOptions; -class CPathfinder +class DLL_LINKAGE CPathfinder { public: friend class CPathfinderHelper; diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index c8fa795c0..5c6dac7a5 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -482,11 +482,11 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons if(!parameters.pos.valid() && parameters.caster->getSpellSchoolLevel(owner) >= 2) { - auto queryCallback = [=](const JsonNode & reply) -> void + auto queryCallback = [=](std::optional reply) -> void { - if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) + if(reply.has_value()) { - ObjectInstanceID townId(static_cast(reply.Integer())); + ObjectInstanceID townId(*reply); const CGObjectInstance * o = env->getCb()->getObj(townId, true); if(o == nullptr) diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index 7d6e395ca..e3185acd9 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -56,7 +56,7 @@ public: virtual bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) = 0; //TODO: remove - virtual void genericQuery(Query * request, PlayerColor color, std::function callback) = 0;//TODO: type safety on query, use generic query packet when implemented + virtual void genericQuery(Query * request, PlayerColor color, std::function)> callback) = 0;//TODO: type safety on query, use generic query packet when implemented }; namespace spells diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8fc6e1a92..b381521dd 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3120,12 +3120,13 @@ bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) return true; } -bool CGameHandler::queryReply(QueryID qid, const JsonNode & answer, PlayerColor player) +bool CGameHandler::queryReply(QueryID qid, std::optional answer, PlayerColor player) { boost::unique_lock lock(gsm); logGlobal->trace("Player %s attempts answering query %d with answer:", player, qid); - logGlobal->trace(answer.toJson()); + if (answer) + logGlobal->trace("%d", *answer); auto topQuery = queries->topQuery(player); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index b1bf301f5..8309feae1 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -178,7 +178,7 @@ public: bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; - bool queryReply( QueryID qid, const JsonNode & answer, PlayerColor player ); + bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); bool setFormation( ObjectInstanceID hid, ui8 formation ); bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 4f0fe8cba..5f856cae0 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -93,9 +93,9 @@ bool ServerSpellCastEnvironment::moveHero(ObjectInstanceID hid, int3 dst, bool t return gh->moveHero(hid, dst, teleporting, false); } -void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function callback) +void ServerSpellCastEnvironment::genericQuery(Query * request, PlayerColor color, std::function)> callback) { - auto query = std::make_shared(gh->queries.get(), color, callback); + auto query = std::make_shared(gh, color, callback); request->queryID = query->queryID; gh->queries->addQuery(query); gh->sendAndApply(request); diff --git a/server/ServerSpellCastEnvironment.h b/server/ServerSpellCastEnvironment.h index 7556783e2..8894160d3 100644 --- a/server/ServerSpellCastEnvironment.h +++ b/server/ServerSpellCastEnvironment.h @@ -36,7 +36,7 @@ public: const CMap * getMap() const override; const CGameInfoCallback * getCb() const override; bool moveHero(ObjectInstanceID hid, int3 dst, bool teleporting) override; - void genericQuery(Query * request, PlayerColor color, std::function callback) override; + void genericQuery(Query * request, PlayerColor color, std::function)> callback) override; private: CGameHandler * gh; -}; \ No newline at end of file +}; diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 8092c6bf9..6b18f90f8 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -26,7 +26,7 @@ void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisi } CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): - CGhQuery(owner), + CQuery(owner), battleID(bi->getBattleID()) { belligerents[0] = bi->getSideArmy(0); @@ -37,7 +37,7 @@ CBattleQuery::CBattleQuery(CGameHandler * owner, const IBattleInfo * bi): } CBattleQuery::CBattleQuery(CGameHandler * owner): - CGhQuery(owner) + CQuery(owner) { belligerents[0] = belligerents[1] = nullptr; } diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index a77805985..ca2077995 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class IBattleInfo; VCMI_LIB_NAMESPACE_END -class CBattleQuery : public CGhQuery +class CBattleQuery : public CQuery { public: std::array belligerents; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index cc0025055..c167c7b8a 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -45,8 +45,9 @@ std::ostream & operator<<(std::ostream & out, QueryPtr query) return out << "[" << query.get() << "] " << query->toString(); } -CQuery::CQuery(QueriesProcessor * Owner): - owner(Owner) +CQuery::CQuery(CGameHandler * gameHandler) + : owner(gameHandler->queries.get()) + , gh(gameHandler) { boost::unique_lock l(QueriesProcessor::mx); @@ -127,7 +128,7 @@ void CQuery::onAdded(PlayerColor color) } -void CQuery::setReply(const JsonNode & reply) +void CQuery::setReply(std::optional reply) { } @@ -141,14 +142,8 @@ bool CQuery::blockAllButReply(const CPack * pack) const return true; } -CGhQuery::CGhQuery(CGameHandler * owner): - CQuery(owner->queries.get()), gh(owner) -{ - -} - CDialogQuery::CDialogQuery(CGameHandler * owner): - CGhQuery(owner) + CQuery(owner) { } @@ -163,14 +158,14 @@ bool CDialogQuery::blocksPack(const CPack * pack) const return blockAllButReply(pack); } -void CDialogQuery::setReply(const JsonNode & reply) +void CDialogQuery::setReply(std::optional reply) { - if(reply.getType() == JsonNode::JsonType::DATA_INTEGER) - answer = reply.Integer(); + if(reply.has_value()) + answer = *reply; } -CGenericQuery::CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback): - CQuery(Owner), callback(Callback) +CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback): + CQuery(gh), callback(Callback) { addPlayer(color); } @@ -190,7 +185,7 @@ void CGenericQuery::onExposure(QueryPtr topQuery) //do nothing } -void CGenericQuery::setReply(const JsonNode & reply) +void CGenericQuery::setReply(std::optional reply) { this->reply = reply; } diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index 9eddbccdc..5c17c44bc 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -10,7 +10,6 @@ #pragma once #include "../../lib/GameConstants.h" -#include "../../lib/JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN @@ -39,8 +38,7 @@ public: std::vector players; //players that are affected (often "blocked") by query QueryID queryID; - CQuery(QueriesProcessor * Owner); - + CQuery(CGameHandler * gh); virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. @@ -53,11 +51,12 @@ public: virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; - virtual void setReply(const JsonNode & reply); + virtual void setReply(std::optional reply); virtual ~CQuery(); protected: QueriesProcessor * owner; + CGameHandler * gh; void addPlayer(PlayerColor color); bool blockAllButReply(const CPack * pack) const; }; @@ -65,21 +64,13 @@ protected: std::ostream &operator<<(std::ostream &out, const CQuery &query); std::ostream &operator<<(std::ostream &out, QueryPtr query); -class CGhQuery : public CQuery -{ -public: - CGhQuery(CGameHandler * owner); -protected: - CGameHandler * gh; -}; - -class CDialogQuery : public CGhQuery +class CDialogQuery : public CQuery { public: CDialogQuery(CGameHandler * owner); virtual bool endsByPlayerAnswer() const override; virtual bool blocksPack(const CPack *pack) const override; - void setReply(const JsonNode & reply) override; + void setReply(std::optional reply) override; protected: std::optional answer; }; @@ -87,14 +78,14 @@ protected: class CGenericQuery : public CQuery { public: - CGenericQuery(QueriesProcessor * Owner, PlayerColor color, std::function Callback); + CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback); bool blocksPack(const CPack * pack) const override; bool endsByPlayerAnswer() const override; void onExposure(QueryPtr topQuery) override; - void setReply(const JsonNode & reply) override; + void setReply(std::optional reply) override; void onRemoval(PlayerColor color) override; private: - std::function callback; - JsonNode reply; + std::function)> callback; + std::optional reply; }; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 8772272d6..c19d510fd 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -16,7 +16,7 @@ #include "../../lib/serializer/Cast.h" PlayerStartsTurnQuery::PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player): - CGhQuery(owner) + CQuery(owner) { addPlayer(player); } @@ -42,7 +42,7 @@ bool PlayerStartsTurnQuery::endsByPlayerAnswer() const } CObjectVisitQuery::CObjectVisitQuery(CGameHandler * owner, const CGObjectInstance * Obj, const CGHeroInstance * Hero, int3 Tile): - CGhQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) + CQuery(owner), visitedObject(Obj), visitingHero(Hero), tile(Tile), removeObjectAfterVisit(false) { addPlayer(Hero->tempOwner); } @@ -213,7 +213,7 @@ void CCommanderLevelUpDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQu } CHeroMovementQuery::CHeroMovementQuery(CGameHandler * owner, const TryMoveHero & Tmh, const CGHeroInstance * Hero, bool VisitDestAfterVictory): - CGhQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) + CQuery(owner), tmh(Tmh), visitDestAfterVictory(VisitDestAfterVictory), hero(Hero) { players.push_back(hero->tempOwner); } diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index dc6890f75..e2b21cae0 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -17,7 +17,7 @@ class TurnTimerHandler; //Created when player starts turn //Removed when player accepts a turn -class PlayerStartsTurnQuery : public CGhQuery +class PlayerStartsTurnQuery : public CQuery { public: PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player); @@ -30,7 +30,7 @@ public: //Created when hero visits object. //Removed when query above is resolved (or immediately after visit if no queries were created) -class CObjectVisitQuery : public CGhQuery +class CObjectVisitQuery : public CQuery { public: const CGObjectInstance *visitedObject; @@ -47,7 +47,7 @@ public: //Created when hero attempts move and something happens //(not necessarily position change, could be just an object interaction). -class CHeroMovementQuery : public CGhQuery +class CHeroMovementQuery : public CQuery { public: TryMoveHero tmh; diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index efee6774d..2b6498943 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -126,7 +126,7 @@ public: return false; } - void genericQuery(Query * request, PlayerColor color, std::function callback) override + void genericQuery(Query * request, PlayerColor color, std::function)> callback) override { //todo: } From b807c3855bcf0642f246251fb60fb89dac4015ff Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 19 Sep 2023 23:44:03 +0300 Subject: [PATCH 0581/1248] Add proper visitation checks for netpacks --- server/CGameHandler.cpp | 39 +++++++++++++++++++++++++ server/CGameHandler.h | 3 ++ server/processors/HeroPoolProcessor.cpp | 3 ++ 3 files changed, 45 insertions(+) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b381521dd..08cf44e46 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2418,6 +2418,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } else { + COMPLAIN_RET_FALSE_IF(isVisitActiveForHero(dwelling, hero), "Cannot recruit: can only recruit by visiting hero!"); COMPLAIN_RET_FALSE_IF(!hero || hero->getOwner() != player, "Cannot recruit: can only recruit to owned hero!"); } @@ -4084,8 +4085,46 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p sendAndApply(&fow); } +bool CGameHandler::isVisitActiveForAny(const CGObjectInstance *obj) +{ + for (auto const & query : queries->allQueries()) + { + auto visit = std::dynamic_pointer_cast(query); + if (visit && visit->visitedObject == obj) + return true; + } + return false; +} + +bool CGameHandler::isVisitActiveForPlayer(const CGObjectInstance *obj, PlayerColor player) +{ + for (auto const & query : queries->allQueries()) + { + auto visit = std::dynamic_pointer_cast(query); + if (visit && visit->visitedObject == obj && visit->visitingHero->getOwner() == player) + return true; + } + return false; +} + +bool CGameHandler::isVisitActiveForHero(const CGObjectInstance *obj, const CGHeroInstance *hero) +{ + for (auto const & query : queries->allQueries()) + { + auto visit = std::dynamic_pointer_cast(query); + if (visit && visit->visitedObject == obj && visit->visitingHero == hero) + return true; + } + return false; +} + bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { + assert(isVisitActiveForHero(obj, hero)); + // Check top query of targeted player: + // If top query is NOT visit to targeted object then we assume that + // visitation query is covered by other query that must be answered first + if (auto topQuery = queries->topQuery(hero->getOwner())) if (auto visit = std::dynamic_pointer_cast(topQuery)) return !(visit->visitedObject == obj && visit->visitingHero == hero); diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 8309feae1..8dfe8d06c 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -153,6 +153,9 @@ public: void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; + bool isVisitActiveForAny(const CGObjectInstance *obj); + bool isVisitActiveForPlayer(const CGObjectInstance *obj, PlayerColor player); + bool isVisitActiveForHero(const CGObjectInstance *obj, const CGHeroInstance *hero); bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; void showInfoDialog(InfoWindow * iw) override; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index f4a32c800..6dfd6533d 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -174,6 +174,9 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if(mapObject->ID == Obj::TAVERN) { + if (!gameHandler->isVisitActiveForPlayer(mapObject, player) && gameHandler->complain("Can't buy hero in tavern you are not visiting!")) + return false; + if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!")) return false; } From 623fb2a63e641d8d254ce13e5fee09e7fe688e69 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 00:05:11 +0300 Subject: [PATCH 0582/1248] Do not allow visiting objects blocked by visit of another player --- server/CGameHandler.cpp | 33 +++++++++++++++++++----- server/processors/TurnOrderProcessor.cpp | 5 ++++ server/processors/TurnOrderProcessor.h | 2 ++ 3 files changed, 34 insertions(+), 6 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 08cf44e46..d32833163 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1086,8 +1086,18 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const TerrainTile t = *getTile(hmpos); const int3 guardPos = gs->guardingCreaturePosition(hmpos); + CGObjectInstance * objectToVisit = nullptr; + CGObjectInstance * guardian = nullptr; - const bool embarking = !h->boat && !t.visitableObjects.empty() && t.visitableObjects.back()->ID == Obj::BOAT; + if (!t.visitableObjects.empty()) + objectToVisit = t.visitableObjects.back(); + + if (isInTheMap(guardPos)) + guardian = getTile(guardPos)->visitableObjects.back(); + + assert(guardian == nullptr || dynamic_cast(guardian) != nullptr); + + const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT; const bool disembarking = h->boat && t.terType->isLand() && (dst == h->pos @@ -1110,7 +1120,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining()); const bool standAtObstacle = t.blocked && !t.visitable; - const bool standAtWater = !h->boat && t.terType->isWater() && (t.visitableObjects.empty() || !t.visitableObjects.back()->isCoastVisitable()); + const bool standAtWater = !h->boat && t.terType->isWater() && (objectToVisit || !objectToVisit->isCoastVisitable()); const auto complainRet = [&](const std::string & message) { @@ -1120,6 +1130,18 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo return false; }; + if (guardian && isVisitActiveForAny(guardian)) + complainRet("Cannot move hero, destination monster is busy!"); + + if (objectToVisit && isVisitActiveForAny(objectToVisit)) + complainRet("Cannot move hero, destination object is busy!"); + + if (objectToVisit && + objectToVisit->getOwner().isValidPlayer() && + getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES && + !turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner())) + complainRet("Cannot move hero, destination player is busy!"); + //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) if (!t.terType->isPassable() || (standAtObstacle && !canFly)) @@ -1179,8 +1201,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo } else if (lookForGuards == CHECK_FOR_GUARDS && isInTheMap(guardPos)) { - const TerrainTile &guardTile = *gs->getTile(guardPos); - objectVisited(guardTile.visitableObjects.back(), h); + objectVisited(guardian, h); moveQuery->visitDestAfterVictory = visitDest==VISIT_DEST; } @@ -1238,9 +1259,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo // visit town for town portal \ castle gates // do not use generic visitObjectOnTile to avoid double-teleporting // if this moveHero call was triggered by teleporter - if (!t.visitableObjects.empty()) + if (objectToVisit) { - if (CGTownInstance * town = dynamic_cast(t.visitableObjects.back())) + if (CGTownInstance * town = dynamic_cast(objectToVisit)) town->onHeroVisit(h); } diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index ed5d61ac7..81790838c 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -90,6 +90,11 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c return false; } +bool TurnOrderProcessor::isContactAllowed(PlayerColor active, PlayerColor waiting) const +{ + return true; +} + bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor waiting) const { const auto * activeInfo = gameHandler->getPlayerState(active, false); diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index 8812f0099..e7625307a 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -53,6 +53,8 @@ class TurnOrderProcessor : boost::noncopyable public: TurnOrderProcessor(CGameHandler * owner); + bool isContactAllowed(PlayerColor left, PlayerColor right) const; + /// Add new player to handle (e.g. on game start) void addPlayer(PlayerColor which); From a9dbd08dec933ff533e8b3fa81babbd5c850b043 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 13:53:06 +0300 Subject: [PATCH 0583/1248] Compute player contacts only once on turn start --- server/processors/TurnOrderProcessor.cpp | 31 +++++++++++++++++++++--- server/processors/TurnOrderProcessor.h | 25 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 81790838c..08f471bfe 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -38,6 +38,26 @@ int TurnOrderProcessor::simturnsTurnsMinLimit() const return 0; } +void TurnOrderProcessor::updateContactStatus() +{ + blockedContacts.clear(); + + assert(actedPlayers.empty()); + assert(actingPlayers.empty()); + + for (auto left : awaitingPlayers) + { + for(auto right : awaitingPlayers) + { + if (left == right) + continue; + + if (computeCanActSimultaneously(left, right)) + blockedContacts.push_back({left, right}); + } + } +} + bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const { // TODO: refactor, cleanup and optimize @@ -92,10 +112,11 @@ bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) c bool TurnOrderProcessor::isContactAllowed(PlayerColor active, PlayerColor waiting) const { - return true; + assert(active != waiting); + return !vstd::contains(blockedContacts, PlayerPair{active, waiting}); } -bool TurnOrderProcessor::canActSimultaneously(PlayerColor active, PlayerColor waiting) const +bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const { const auto * activeInfo = gameHandler->getPlayerState(active, false); const auto * waitingInfo = gameHandler->getPlayerState(waiting, false); @@ -155,7 +176,7 @@ bool TurnOrderProcessor::canStartTurn(PlayerColor which) const for (auto player : actingPlayers) { - if (!canActSimultaneously(player, which)) + if (player != which && isContactAllowed(player, which)) return false; } @@ -180,6 +201,7 @@ void TurnOrderProcessor::doStartNewDay() std::swap(actedPlayers, awaitingPlayers); gameHandler->onNewTurn(); + updateContactStatus(); tryStartTurnsForPlayers(); } @@ -277,6 +299,9 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) void TurnOrderProcessor::onGameStarted() { + if (actingPlayers.empty()) + updateContactStatus(); + // this may be game load - send notification to players that they can act auto actingPlayersCopy = actingPlayers; for (auto player : actingPlayersCopy) diff --git a/server/processors/TurnOrderProcessor.h b/server/processors/TurnOrderProcessor.h index e7625307a..378ed007f 100644 --- a/server/processors/TurnOrderProcessor.h +++ b/server/processors/TurnOrderProcessor.h @@ -17,6 +17,26 @@ class TurnOrderProcessor : boost::noncopyable { CGameHandler * gameHandler; + struct PlayerPair + { + PlayerColor a; + PlayerColor b; + + bool operator == (const PlayerPair & other) const + { + return (a == other.a && b == other.b) || (a == other.b && b == other.a); + } + + template + void serialize(Handler & h, const int version) + { + h & a; + h & b; + } + }; + + std::vector blockedContacts; + std::set awaitingPlayers; std::set actingPlayers; std::set actedPlayers; @@ -31,7 +51,7 @@ class TurnOrderProcessor : boost::noncopyable bool playersInContact(PlayerColor left, PlayerColor right) const; /// Returns true if waiting player can act alongside with currently acting player - bool canActSimultaneously(PlayerColor active, PlayerColor waiting) const; + bool computeCanActSimultaneously(PlayerColor active, PlayerColor waiting) const; /// Returns true if left player must act before right player bool mustActBefore(PlayerColor left, PlayerColor right) const; @@ -42,6 +62,8 @@ class TurnOrderProcessor : boost::noncopyable /// Starts turn for all players that can start turn void tryStartTurnsForPlayers(); + void updateContactStatus(); + void doStartNewDay(); void doStartPlayerTurn(PlayerColor which); void doEndPlayerTurn(PlayerColor which); @@ -70,6 +92,7 @@ public: template void serialize(Handler & h, const int version) { + h & blockedContacts; h & awaitingPlayers; h & actingPlayers; h & actedPlayers; From 3ea807fb8d78e30dda959def4fc1bbf625b7e032 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 20 Sep 2023 22:48:48 +0300 Subject: [PATCH 0584/1248] Fixed movement through teleporters by AI --- lib/mapObjects/MiscObjects.cpp | 2 +- server/CGameHandler.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ebb507a1b..5b265795a 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -372,7 +372,7 @@ ObjectInstanceID CGTeleport::getRandomExit(const CGHeroInstance * h) const bool CGTeleport::isTeleport(const CGObjectInstance * obj) { - return ((dynamic_cast(obj))); + return dynamic_cast(obj) != nullptr; } bool CGTeleport::isConnected(const CGTeleport * src, const CGTeleport * dst) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index d32833163..69c27c891 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1163,7 +1163,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) complainRet("Hero doesn't have any movement points left!"); - if (transit && !canFly && !(canWalkOnSea && t.terType->isWater())) + if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()) && !CGTeleport::isTeleport(objectToVisit)) complainRet("Hero cannot transit over this tile!"); //several generic blocks of code @@ -1195,7 +1195,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo tmh.result = result; sendAndApply(&tmh); - if (visitDest == VISIT_DEST && t.topVisitableObj() && t.topVisitableObj()->id == h->id) + if (visitDest == VISIT_DEST && objectToVisit && objectToVisit->id == h->id) { // Hero should be always able to visit any object he staying on even if there guards around visitObjectOnTile(t, h); } @@ -1279,7 +1279,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo EVisitDest visitDest = VISIT_DEST; if (transit) { - if (CGTeleport::isTeleport(t.topVisitableObj())) + if (CGTeleport::isTeleport(objectToVisit)) visitDest = DONT_VISIT_DEST; if (canFly || (canWalkOnSea && t.terType->isWater())) @@ -1292,7 +1292,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo return true; if(h->boat && !h->boat->onboardAssaultAllowed) - lookForGuards = IGNORE_GUARDS; + lookForGuards = IGNORE_GUARDS; turnTimerHandler.setEndTurnAllowed(h->getOwner(), !standAtWater && !standAtObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); From 5b21a5ffbcf8800ace5cc2ef43e31f43c7a1ed5e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 22 Sep 2023 19:57:43 +0300 Subject: [PATCH 0585/1248] Simturn duration is now part of StartInfo --- client/CServerHandler.cpp | 7 +++++++ client/CServerHandler.h | 2 ++ client/lobby/CSelectionBase.h | 1 + lib/NetPackVisitor.h | 1 + lib/NetPacksLib.cpp | 5 +++++ lib/NetPacksLobby.h | 12 ++++++++++++ lib/StartInfo.h | 20 ++++++++++++++++++++ lib/registerTypes/RegisterTypes.h | 1 + server/LobbyNetPackVisitors.h | 3 ++- server/NetPacksLobbyServer.cpp | 6 ++++++ server/processors/TurnOrderProcessor.cpp | 9 +++++---- 11 files changed, 62 insertions(+), 5 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 291fe5b70..690051380 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -477,6 +477,13 @@ void CServerHandler::setDifficulty(int to) const sendLobbyPack(lsd); } +void CServerHandler::setSimturnsInfo(const SimturnsInfo & info) const +{ + LobbySetSimturns pack; + pack.simturnsInfo = info; + sendLobbyPack(pack); +} + void CServerHandler::setTurnTimerInfo(const TurnTimerInfo & info) const { LobbySetTurnTime lstt; diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 12a130f83..5e520d9ed 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -66,6 +66,7 @@ public: virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; + virtual void setSimturnsInfo(const SimturnsInfo &) const = 0; virtual void sendMessage(const std::string & txt) const = 0; virtual void sendGuiAction(ui8 action) const = 0; // TODO: possibly get rid of it? virtual void sendStartGame(bool allowOnlyAI = false) const = 0; @@ -148,6 +149,7 @@ public: void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override; + void setSimturnsInfo(const SimturnsInfo &) const override; void sendMessage(const std::string & txt) const override; void sendGuiAction(ui8 action) const override; void sendRestartGame() const override; diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 76f222a10..5436a959e 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -59,6 +59,7 @@ public: std::shared_ptr buttonOptions; std::shared_ptr buttonStart; std::shared_ptr buttonBack; + std::shared_ptr buttonSimturns; std::shared_ptr tabSel; std::shared_ptr tabOpt; diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index 39c284864..c32b24779 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -158,6 +158,7 @@ public: virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) {} virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {} virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {} + virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {} virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {} virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) {} virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) {} diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 5fd1116ad..c870b0093 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -750,6 +750,11 @@ void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) visitor.visitLobbySetPlayer(*this); } +void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetSimturns(*this); +} + void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbySetTurnTime(*this); diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index 8c19d87c9..e75929077 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -250,6 +250,18 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer } }; +struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer +{ + SimturnsInfo simturnsInfo; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & simturnsInfo; + } +}; + struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer { TurnTimerInfo turnTimerInfo; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 7c5f46e93..b1addd4b2 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -23,6 +23,24 @@ class CMapInfo; struct PlayerInfo; class PlayerColor; +struct DLL_LINKAGE SimturnsInfo +{ + /// Minimal number of turns that must be played simultaneously even if contact has been detected + int requiredTurns = 0; + /// Maximum number of turns that might be played simultaneously unless contact is detected + int optionalTurns = 0; + /// If set to true, human and 1 AI can act at the same time + bool allowHumanWithAI = false; + + template + void serialize(Handler &h, const int version) + { + h & requiredTurns; + h & optionalTurns; + h & allowHumanWithAI; + } +}; + /// Struct which describes the name, the color, the starting bonus of a player struct DLL_LINKAGE PlayerSettings { @@ -84,6 +102,7 @@ struct DLL_LINKAGE StartInfo ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant std::string startTimeIso8601; + SimturnsInfo simturnsInfo; TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame bool createRandomMap() const { return mapGenOptions != nullptr; } @@ -108,6 +127,7 @@ struct DLL_LINKAGE StartInfo h & seedPostInit; h & mapfileChecksum; h & startTimeIso8601; + h & simturnsInfo; h & turnTimerInfo; h & mapname; h & mapGenOptions; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 5f6951331..3d2d1df56 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -399,6 +399,7 @@ void registerTypesLobbyPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); } diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index 251f1efcb..fba117a5f 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -87,6 +87,7 @@ public: virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override; virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; + virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override; virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; virtual void visitLobbyForceSetPlayer(LobbyForceSetPlayer & pack) override; -}; \ No newline at end of file +}; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 2f2695c11..dbb68d617 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -391,6 +391,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack) result = true; } +void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack) +{ + srv.si->simturnsInfo = pack.simturnsInfo; + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbySetTurnTime(LobbySetTurnTime & pack) { srv.si->turnTimerInfo = pack.turnTimerInfo; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 08f471bfe..d5eb1f0bd 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -28,14 +28,12 @@ TurnOrderProcessor::TurnOrderProcessor(CGameHandler * owner): int TurnOrderProcessor::simturnsTurnsMaxLimit() const { - // TODO - return 28; + return gameHandler->getStartInfo()->simturnsInfo.optionalTurns; } int TurnOrderProcessor::simturnsTurnsMinLimit() const { - // TODO - return 0; + return gameHandler->getStartInfo()->simturnsInfo.requiredTurns; } void TurnOrderProcessor::updateContactStatus() @@ -127,6 +125,9 @@ bool TurnOrderProcessor::computeCanActSimultaneously(PlayerColor active, PlayerC if (gameHandler->hasBothPlayersAtSameConnection(active, waiting)) { + if (!gameHandler->getStartInfo()->simturnsInfo.allowHumanWithAI) + return false; + // only one AI and one human can play simultaneoulsy from single connection if (activeInfo->human == waitingInfo->human) return false; From 54adeef29fbfe746e134b25fd966813616529f54 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 22 Sep 2023 21:41:32 +0300 Subject: [PATCH 0586/1248] Added temporary UI for simturns testing --- client/lobby/OptionsTab.cpp | 18 +++++++++++++++++ config/widgets/optionsTab.json | 36 +++++++++++++++++++++++++++++----- lib/StartInfo.h | 2 +- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index a15aafd6f..cfa94763c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -58,6 +58,12 @@ OptionsTab::OptionsTab() : humanPlayers(0) CSH->setTurnTimerInfo(tinfo); } }); + + addCallback("setSimturnDuration", [&](int index){ + SimturnsInfo info; + info.optionalTurns = index; + CSH->setSimturnsInfo(info); + }); //helper function to parse string containing time to integer reflecting time in seconds //assumed that input string can be modified by user, function shall support user's intention @@ -214,6 +220,18 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } + + //Simultaneous turns + if(auto turnSlider = widget("labelSimturnsDurationValue")) + turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); + + if(auto w = widget("labelSimturnsDurationValue")) + { + MetaString message; + message.appendRawString("Simturns: up to %d days"); + message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); + w->setText(message.toString()); + } const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; diff --git a/config/widgets/optionsTab.json b/config/widgets/optionsTab.json index ca3c936f2..bd8c76126 100644 --- a/config/widgets/optionsTab.json +++ b/config/widgets/optionsTab.json @@ -73,16 +73,41 @@ "adoptHeight": true }, - // timer { + "name": "simturnsDuration", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 55, "y": 537}, + "size": 194, + "callback": "setSimturnDuration", + "itemsVisible": 1, + "itemsTotal": 28, + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + + { + "name": "labelSimturnsDurationValue", "type": "label", "font": "small", "alignment": "center", - "color": "yellow", - "text": "core.genrltxt.521", - "position": {"x": 222, "y": 544} + "color": "white", + "text": "", + "position": {"x": 319, "y": 545} }, + // timer + //{ + // "type": "label", + // "font": "small", + // "alignment": "center", + // "color": "yellow", + // "text": "core.genrltxt.521", + // "position": {"x": 222, "y": 544} + //}, + { "name": "labelTurnDurationValue", "type": "label", @@ -104,7 +129,8 @@ "itemsTotal": 11, "selected": 11, "style": "blue", - "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + //"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, "panningStep": 20 }, ], diff --git a/lib/StartInfo.h b/lib/StartInfo.h index b1addd4b2..a558c4d32 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -30,7 +30,7 @@ struct DLL_LINKAGE SimturnsInfo /// Maximum number of turns that might be played simultaneously unless contact is detected int optionalTurns = 0; /// If set to true, human and 1 AI can act at the same time - bool allowHumanWithAI = false; + bool allowHumanWithAI = true; template void serialize(Handler &h, const int version) From 49c148502b84e95369c276dd1aae745c0682c568 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 26 Sep 2023 15:54:24 +0300 Subject: [PATCH 0587/1248] Correctly close dialogs on end of turn --- client/CPlayerInterface.cpp | 40 +++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 77735e69e..23e2240e0 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -183,7 +183,32 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player) { EVENT_HANDLER_CALLED_BY_CLIENT; if (player == playerID) + { makingTurn = false; + + // remove all active dialogs that do not expect query answer + for (;;) + { + auto adventureWindow = GH.windows().topWindow(); + auto infoWindow = GH.windows().topWindow(); + + if(adventureWindow != nullptr) + break; + + if(infoWindow && infoWindow->ID != QueryID::NONE) + break; + + if (infoWindow) + infoWindow->close(); + else + GH.windows().popWindows(1); + } + + // remove all pending dialogs that do not expect query answer + vstd::erase_if(dialogs, [](const std::shared_ptr & window){ + return window->ID == QueryID::NONE; + }); + } } void CPlayerInterface::playerStartsTurn(PlayerColor player) @@ -250,21 +275,6 @@ void CPlayerInterface::yourTurn(QueryID queryID) LOCPLINT = this; GH.curInt = this; - // close window from another player - if(auto w = GH.windows().topWindow()) - { - assert(0);// what is this? - if(w->ID == QueryID::NONE) - w->close(); - } - - // remove all dialogs that do not expect query answer - while (!GH.windows().topWindow() && !GH.windows().topWindow()) - { - assert(0);// what is this? - GH.windows().popWindows(1); - } - NotificationHandler::notify("Your turn"); if(settings["general"]["startTurnAutosave"].Bool()) { From 0a0c01d6398744281116fa256bbf5558f866dedb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 26 Sep 2023 15:55:07 +0300 Subject: [PATCH 0588/1248] Replaced 'isVisitActiveFor' methods with single 'getVisitingHero' --- server/CGameHandler.cpp | 66 ++++++++++--------------- server/CGameHandler.h | 5 +- server/processors/HeroPoolProcessor.cpp | 7 ++- 3 files changed, 35 insertions(+), 43 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 69c27c891..ae7ddfadd 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1130,41 +1130,41 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo return false; }; - if (guardian && isVisitActiveForAny(guardian)) - complainRet("Cannot move hero, destination monster is busy!"); + if (guardian && getVisitingHero(guardian) != nullptr) + return complainRet("Cannot move hero, destination monster is busy!"); - if (objectToVisit && isVisitActiveForAny(objectToVisit)) - complainRet("Cannot move hero, destination object is busy!"); + if (objectToVisit && getVisitingHero(objectToVisit) != nullptr) + return complainRet("Cannot move hero, destination object is busy!"); if (objectToVisit && objectToVisit->getOwner().isValidPlayer() && getPlayerRelations(objectToVisit->getOwner(), h->getOwner()) == PlayerRelations::ENEMIES && !turnOrder->isContactAllowed(objectToVisit->getOwner(), h->getOwner())) - complainRet("Cannot move hero, destination player is busy!"); + return complainRet("Cannot move hero, destination player is busy!"); //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) if (!t.terType->isPassable() || (standAtObstacle && !canFly)) - complainRet("Cannot move hero, destination tile is blocked!"); + return complainRet("Cannot move hero, destination tile is blocked!"); //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) if(standAtWater && !canFly && !canWalkOnSea) - complainRet("Cannot move hero, destination tile is on water!"); + return complainRet("Cannot move hero, destination tile is on water!"); if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) - complainRet("Cannot disembark hero, tile is blocked!"); + return complainRet("Cannot disembark hero, tile is blocked!"); if(distance(h->pos, dst) >= 1.5 && !teleporting) - complainRet("Tiles are not neighboring!"); + return complainRet("Tiles are not neighboring!"); if(h->inTownGarrison) - complainRet("Can not move garrisoned hero!"); + return complainRet("Can not move garrisoned hero!"); if(h->movementPointsRemaining() < cost && dst != h->pos && !teleporting) - complainRet("Hero doesn't have any movement points left!"); + return complainRet("Hero doesn't have any movement points left!"); if (transit && !canFly && !(canWalkOnSea && t.terType->isWater()) && !CGTeleport::isTeleport(objectToVisit)) - complainRet("Hero cannot transit over this tile!"); + return complainRet("Hero cannot transit over this tile!"); //several generic blocks of code @@ -2439,7 +2439,7 @@ bool CGameHandler::recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst } else { - COMPLAIN_RET_FALSE_IF(isVisitActiveForHero(dwelling, hero), "Cannot recruit: can only recruit by visiting hero!"); + COMPLAIN_RET_FALSE_IF(getVisitingHero(dwelling) != hero, "Cannot recruit: can only recruit by visiting hero!"); COMPLAIN_RET_FALSE_IF(!hero || hero->getOwner() != player, "Cannot recruit: can only recruit to owned hero!"); } @@ -3407,6 +3407,12 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta logGlobal->debug("%s visits %s (%d:%d)", h->nodeName(), obj->getObjectName(), obj->ID, obj->subID); + if (getVisitingHero(obj) != nullptr) + { + logGlobal->error("Attempt to visit object that is being visited by another hero!"); + throw std::runtime_error("Can not visit object that is being visited"); + } + std::shared_ptr visitQuery; auto startVisit = [&](ObjectVisitStarted & event) @@ -4106,42 +4112,24 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p sendAndApply(&fow); } -bool CGameHandler::isVisitActiveForAny(const CGObjectInstance *obj) +const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj) { + assert(obj); + for (auto const & query : queries->allQueries()) { auto visit = std::dynamic_pointer_cast(query); if (visit && visit->visitedObject == obj) - return true; + return visit->visitingHero; } - return false; -} - -bool CGameHandler::isVisitActiveForPlayer(const CGObjectInstance *obj, PlayerColor player) -{ - for (auto const & query : queries->allQueries()) - { - auto visit = std::dynamic_pointer_cast(query); - if (visit && visit->visitedObject == obj && visit->visitingHero->getOwner() == player) - return true; - } - return false; -} - -bool CGameHandler::isVisitActiveForHero(const CGObjectInstance *obj, const CGHeroInstance *hero) -{ - for (auto const & query : queries->allQueries()) - { - auto visit = std::dynamic_pointer_cast(query); - if (visit && visit->visitedObject == obj && visit->visitingHero == hero) - return true; - } - return false; + return nullptr; } bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) { - assert(isVisitActiveForHero(obj, hero)); + assert(obj); + assert(hero); + assert(getVisitingHero(obj) == hero); // Check top query of targeted player: // If top query is NOT visit to targeted object then we assume that // visitation query is covered by other query that must be answered first diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 8dfe8d06c..0522fbd58 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -153,9 +153,8 @@ public: void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; - bool isVisitActiveForAny(const CGObjectInstance *obj); - bool isVisitActiveForPlayer(const CGObjectInstance *obj, PlayerColor player); - bool isVisitActiveForHero(const CGObjectInstance *obj, const CGHeroInstance *hero); + /// Returns hero that is currently visiting this object, or nullptr if no visit is active + const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; void showInfoDialog(InfoWindow * iw) override; diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 6dfd6533d..c2ad4cdd2 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -174,8 +174,13 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy if(mapObject->ID == Obj::TAVERN) { - if (!gameHandler->isVisitActiveForPlayer(mapObject, player) && gameHandler->complain("Can't buy hero in tavern you are not visiting!")) + const CGHeroInstance * visitor = gameHandler->getVisitingHero(mapObject); + + if (!visitor || visitor->getOwner() != player) + { + gameHandler->complain("Can't buy hero in tavern not being visited!"); return false; + } if(gameHandler->getTile(mapObject->visitablePos())->visitableObjects.back() != mapObject && gameHandler->complain("Tavern entry must be unoccupied!")) return false; From a7ebbf868080a3afb2806538062dd9e6e35b2fe1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 20:28:13 +0300 Subject: [PATCH 0589/1248] Updated bonus documentation up to SPELL_AFTER_ATTACK --- docs/modders/Bonus/Bonus_Types.md | 193 +++++++++++++++--------------- lib/bonuses/BonusEnum.h | 5 +- 2 files changed, 99 insertions(+), 99 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 873d43e37..c633cda7b 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -449,232 +449,243 @@ Affected unit will ignore specified percentage of attacked unit defence (Behemot ### GENERAL_DAMAGE_REDUCTION -- subtype - 0 - shield (melee) , 1 - air shield effect (ranged), -1 - - armorer secondary skill (all, since 1.2) +Affected units will receive reduced damage from attacks by other units + +- val: damage reduction, percentage +- subtype: 0 - melee damage (Shield spell), 1 - ranged damage (Air Shield), -1 - all damage (Armorer skill) ### PERCENTAGE_DAMAGE_BOOST -- subtype: -1 - any damage (not used), 0 - melee damage (offence), 1 - - ranged damage (archery) +Affected units will deal increased damage when attacking other units + +- val: damage increase, percentage +- subtype: 0 - melee damage (Offense skill), 1 - ranged damage (Archery skill) ### GENERAL_ATTACK_REDUCTION -eg. while stoned or blinded - in % +Affected units will deal reduced damage when attacking other units (Blind or Paralyze spells) -- subtype: -1 - any damage, 0 - melee damage, 1 - ranged damage +- val: damage reduction, percentage ### DEFENSIVE_STANCE -WoG ability. +Affected units will receive increased bonus to defence while defending -- val - bonus to defense while defending +- val: additional bonus to defence, in skill points ### NO_DISTANCE_PENALTY +Affected unit will have no distance penalty when shooting. Does not negates wall penalties in sieges + ### NO_MELEE_PENALTY -Ranged stack does full damage in melee. +Affected ranged unit will deal full damage in melee attacks ### NO_WALL_PENALTY +Affected unit will deal full damage when shooting over walls in sieges. Does not negates distance penalty + ### FREE_SHOOTING -stacks can shoot even if otherwise blocked (sharpshooter's bow effect) +Affected unit can use ranged attack even when blocked by enemy unit. (Sharpshooter's Bow) ### BLOCKS_RETALIATION -eg. naga +Affected unit will never receive retaliations when attacking ### SOUL_STEAL -WoG ability. Gains new creatures for each enemy killed +Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed -- subtype: 0 - gained units survive after battle, 1 - they do not +- subtype: 0 - gained units disappear after battle, 1 - gained units are permanent ### TRANSMUTATION -WoG ability. % chance to transform attacked unit to other type +Affected units have chance to transform attacked unit to other creature type -- val: chance to trigger in % -- subtype: 0 - resurrection based on HP, 1 - based on unit count -- addInfo: additional info - target creature ID (attacker default) +- val: chance for ability to trigger, percentage +- subtype: 0 - transformed unit will have same HP pool as original stack, 1 - transformed unit will have same number of units as original stack +- addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS -WoG ability. Summon guardians surrounding desired stack +When battle starts, affected units will be surrounded from all side with summoned units -- val - amount in % of stack count -- subtype = creature ID +- val: amount of units to summon, per stack, percentage of summoner stack size +- subtype: identifier of creature to summon ### RANGED_RETALIATION -Allows shooters to perform ranged retaliation - -- no additional parameters +Affected units will retaliate against ranged attacks, if able ### BLOCKS_RANGED_RETALIATION -Disallows ranged retaliation for shooter unit, BLOCKS_RETALIATION bonus -is for melee retaliation only - -- no additional parameters +Affected unit will never receive counterattack in ranged attacks. Counters RANGED_RETALIATION bonus ### WIDE_BREATH -Dragon breath affecting multiple nearby hexes - -- no additional parameters +Affected unit will attack units on all hexes that surround attacked hex ### FIRST_STRIKE -First counterattack, then attack if possible - -- no additional parameters +Affected unit will retaliate before enemy attacks, if able ### SHOOTS_ALL_ADJACENT -H4 Cyclops-like shoot (attacks all hexes neighboring with target) -without spell-like mechanics - -- no additional parameters +Affected unit will attack units on all hexes that surround attacked hex in ranged attacks ### DESTRUCTION -Kills extra units after hit +Affected unit will kills additional units after attack +- val: chance to trigger, percentage - subtype: 0 - kill percentage of units, 1 - kill amount -- val: chance in percent to trigger -- addInfo: amount/percentage to kill +- addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE -Limits shooting range and/or changes long range penalty +Affected unit can use ranged attacks only within specified range - val: max shooting range in hexes -- addInfo: optional - sets range for "broken arrow" half damage - mechanic - default is 10 +- addInfo: optional, range at which ranged penalty will trigger (default is 10) ## Special abilities ### CATAPULT -- subtype: ability to use when catapulting (usually it contains ballistics parameters, ability for standard catapult and affected by ballistics is core:spell.catapultShot) +Affected unit can attack walls during siege battles + +- subtype: spell that describes attack parameters ### CATAPULT_EXTRA_SHOTS -- subtype: ability to be affected. Ability of CATAPULT bonus should match. Used for ballistics secondary skill with subtype of core:spell.catapultShot. -- val: ability level to be used with catapult. Additional shots configured in ability level, not here. +Defines spell mastery level for spell used by CATAPULT bonus +- subtype: affected spell +- val: spell mastery level to use ### MANUAL_CONTROL -- manually control warmachine with -- id = subtype -- val = chance +Hero can control war machine affected by this bonus + +- id: creature identifier of affected war machine +- val: chance to control unit, percentage ### CHANGES_SPELL_COST_FOR_ALLY -eg. mage +Affected units will decrease spell cost for their hero (Mage). If multiple units have this bonus only best value will be used -- value - reduction in mana points +- val: reduction in mana points. ### CHANGES_SPELL_COST_FOR_ENEMY -eg. Silver Pegasus +Affected units will increase spell cost for enemy hero (Silver Pegasus). If multiple units have this bonus only best value will be used -- value - mana points penalty +- val: increase in mana points. ### SPELL_RESISTANCE_AURA -eg. unicorns +All units adjacent to affected unit will receive additional spell resistance bonus. If multiple adjacent units have this bonus only best value will be used -- value - resistance bonus in % for adjacent creatures +- val: additional resistance bonus, percentage ### HP_REGENERATION -creature regenerates val HP every new round +Affected unit will regenerate portion of its health points on its turn -- val: amount of HP regeneration per round +- val: amount of health points gained per round ### MANA_DRAIN -value - spell points per turn +Affected unit will drain specified amount of mana points from enemy hero on each turn + +val: spell points per turn to take ### MANA_CHANNELING -eg. familiar +Affected unit will give his hero specified portion of mana points spent by enemy hero -- value in % +- val: spell points to give, percentage ### LIFE_DRAIN -- val - precentage of life drained +Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage + +- val: percentage of damage that will be converted into health points ### DOUBLE_DAMAGE_CHANCE -eg. dread knight +Affected unit has chance to deal double damage on attack -- value in % +- val: chance to trigger, percentage ### FEAR +If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance. Blocked by FEARLESS bonus. + ### HEALER -First aid tent +Affected unit acts as healing tent and can heal allied units on each turn -- subtype: ability used for healing. +- subtype: identifier of spell that will be used for healing action ### FIRE_SHIELD +When affected unit is attacked, portion of received damage will be also dealt to the attacked. Units immune to fire magic will not receive this damage. Only melee attacks will trigger this bonus + +- val: amount to deal in return, percentage + ### MAGIC_MIRROR -- value - chance of redirecting in % +If affected unit is targeted by a spell it will reflect spell to a random enemy unit + +- val: chance to trigger, percentage ### ACID_BREATH -- val - damage per creature after attack, -- additional info - chance in percent +Affected unit will deal additional damage after attack + +- val - additional damage to deal, multiplied by unit stack size +- additional info: chance to trigger, percentage ### DEATH_STARE -- subtype: 0 - gorgon, 1 - commander -- val: if subtype is 0, its the (average) percentage of killed - creatures related to size of attacking stack +Affected unit will kill additional units after attack + +- subtype: 0 - random amount (Gorgons), 1 - fixed amount (Commanders) +- val: +- - for subtype 0: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula +- - for subtype 1: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val ### SPECIAL_CRYSTAL_GENERATION -Crystal dragon crystal generation +If player has affected unit under his control in any army, he will receive additional 3 crystals on new week (Crystal Dragons) ### NO_SPELLCAST_BY_DEFAULT -Spellcast will not be default attack option for this creature +Affected unit will not use spellcast as default attack option ## Creature spellcasting and activated abilities ### SPELLCASTER -Creature gain activated ability to cast a spell. Example: Archangel, -Faerie Dragon +Affected units can cast a spell as targeted action (Archangel, Faerie Dragon). Use CASTS bonus to specify how many times per combat creature can use spellcasting. Use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER bonuses to set spell power. -- subtype - spell id -- value - level of school -- additional info - weighted chance - -use SPECIFIC_SPELL_POWER, CREATURE_SPELL_POWER or CREATURE_ENCHANT_POWER -for calculating the power (since 1.2 those bonuses can be used for -calculating CATAPULT and HEALER bonuses) +- subtype: spell identifier +- value: spell mastery level +- additional info: weighted chance to select this spell. Can be omitted for always available spells ### ENCHANTER -for Enchanter spells +Affected unit will cast specified spell before his turn (Enchanter) -- val - skill level -- subtype - spell id -- additionalInfo - cooldown (number of turns) +- val - spell mastery level +- subtype - spell identifier +- additionalInfo - cooldown before next cast, in number of turns ### RANDOM_SPELLCASTER -eg. master genie +Affected unit can cast randomly selected beneficial spell on its turn (Master Genie) - val - spell mastery level @@ -832,14 +843,6 @@ subid - spell id ### GENERAL_DAMAGE_PREMY -### ADDITIONAL_UNITS -val of units with id = subtype will be added to hero's army at the beginning of battle - -### SPOILS_OF_WAR -val * 10^-6 * gained exp resources of subtype will be given to hero after battle - -### BLOCK - ### DISGUISED subtype - spell level diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 7f1489e79..91105b774 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -130,9 +130,6 @@ class JsonNode; BONUS_NAME(CREATURE_ENCHANT_POWER) /* total duration of spells cast by creature */ \ BONUS_NAME(ENCHANTED) /* permanently enchanted with spell subID of level = val, if val > 3 then spell is mass and has level of val-3*/ \ BONUS_NAME(REBIRTH) /* val - percent of life restored, subtype = 0 - regular, 1 - at least one unit (sacred Phoenix) */\ - BONUS_NAME(ADDITIONAL_UNITS) /*val of units with id = subtype will be added to hero's army at the beginning of battle */\ - BONUS_NAME(SPOILS_OF_WAR) /*val * 10^-6 * gained exp resources of subtype will be given to hero after battle*/\ - BONUS_NAME(BLOCK)\ BONUS_NAME(DISGUISED) /* subtype - spell level */\ BONUS_NAME(VISIONS) /* subtype - spell level */\ BONUS_NAME(NO_TERRAIN_PENALTY) /* subtype - terrain type */\ @@ -255,4 +252,4 @@ extern DLL_LINKAGE const std::map bonusSourceMap; extern DLL_LINKAGE const std::map bonusDurationMap; extern DLL_LINKAGE const std::map bonusLimitEffect; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END From 524c476264b1b948af333221caf87cf6d628f717 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 22:15:08 +0300 Subject: [PATCH 0590/1248] Removed no longer used bonuses --- lib/CCreatureHandler.cpp | 3 --- lib/bonuses/BonusEnum.h | 1 - 2 files changed, 4 deletions(-) diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index c8ddeb04f..b65227d77 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -716,9 +716,6 @@ void CCreatureHandler::loadCrExpMod() expBonParser.endLine(); } - //skeleton gets exp penalty - objects[56].get()->addBonus(-50, BonusType::EXP_MULTIPLIER, -1); - objects[57].get()->addBonus(-50, BonusType::EXP_MULTIPLIER, -1); //exp for tier >7, rank 11 expRanks[0].push_back(147000); expAfterUpgrade = 75; //percent diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 91105b774..9030b14a7 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -117,7 +117,6 @@ class JsonNode; BONUS_NAME(SPECIAL_UPGRADE) /*subtype = base, additionalInfo = target */\ BONUS_NAME(DRAGON_NATURE) \ BONUS_NAME(CREATURE_DAMAGE)/*subtype 0 = both, 1 = min, 2 = max*/\ - BONUS_NAME(EXP_MULTIPLIER)/* val - percent of additional exp gained by stack/commander (base value 100)*/\ BONUS_NAME(SHOTS)\ BONUS_NAME(DEATH_STARE) /*subtype 0 - gorgon, 1 - commander*/\ BONUS_NAME(POISON) /*val - max health penalty from poison possible*/\ From 9c4475385be6ffe070cb3a7f82423e71b703f6eb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 17 Sep 2023 23:07:29 +0300 Subject: [PATCH 0591/1248] Updated most of remaining bonus type docs --- docs/modders/Bonus/Bonus_Types.md | 165 +++++++++++++++++++----------- 1 file changed, 105 insertions(+), 60 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index c633cda7b..3e6cfbe97 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -226,6 +226,14 @@ TODO: blesses and curses with id = val dependent on unit's level - subtype: 0 or 1 for Coronius +### SPECIAL_ADD_VALUE_ENCHANT + +TODO: specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add + +### SPECIAL_FIXED_VALUE_ENCHANT + +TODO: specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. + ### SPECIAL_UPGRADE Allows creature upgrade for affected armies @@ -689,8 +697,16 @@ Affected unit can cast randomly selected beneficial spell on its turn (Master Ge - val - spell mastery level +### CASTS + +Determines how many times per combat affected creature can cast its targeted spell + +- val: number of casts available per combat + ### SPELL_AFTER_ATTACK +TODO: + - subtype - spell id - value - chance % - additional info - \[X, Y\] @@ -699,167 +715,196 @@ Affected unit can cast randomly selected beneficial spell on its turn (Master Ge ### SPELL_BEFORE_ATTACK +TODO: + - subtype - spell id - value - chance % - additional info - \[X, Y\] - X - spell level - Y = 0 - all attacks, 1 - shot only, 2 - melee only -### CASTS - -how many times creature can cast activated spell - ### SPECIFIC_SPELL_POWER -- value used for Thunderbolt and Resurrection casted by units, also - for Healing secondary skill (for core:spell.firstAid used by First - Aid tent) +TODO: + +- value used for Thunderbolt and Resurrection casted by units, also for Healing secondary skill (for core:spell.firstAid used by First Aid tent) - subtype - spell id ### CREATURE_SPELL_POWER +TODO: + - value per unit, divided by 100 (so faerie Dragons have 500) ### CREATURE_ENCHANT_POWER +TODO: + total duration of spells casted by creature ### REBIRTH +Affected stack will resurrect after death + +TODO: recheck math + - val - percent of total stack HP restored - subtype = 0 - regular, 1 - at least one unit (sacred Phoenix) ### ENCHANTED -Stack is permanently enchanted with spell subID of skill level = val, if val > 3 then spell is mass and has level of val-3. Enchantment is refreshed every turn. +Affected unit is permanently enchanted with a spell, that is cast again every turn + +- subtype: spell identifier +- val: spell mastery level. If above 3, then spell has mass effect with mastery level of (val-3) ## Spell immunities ### LEVEL_SPELL_IMMUNITY -creature is immune to all spell with level below or equal to value of this bonus +Affected unit is immune to all spell with level below or equal to value of this bonus + +- val: level to which this unit is immune to + +TODO: additional info? ### MAGIC_RESISTANCE -- value - percent +Affected unit has a chance to resist hostile spell and avoid its effects + +- val: chance to trigger, percentage ### SPELL_DAMAGE_REDUCTION -eg. golems +Affected unit receives decreased damage from spells of specific school (Golems) -- value - reduction in % -- subtype - spell school; -1 - all, 0 - air, 1 - fire, 2 - water, 3 - - earth +- val: reduction to damage, percentage +- subtype - spell school identifier ### MORE_DAMAGE_FROM_SPELL -- value - damage increase in % -- subtype - spell id +Affected unit receives increased damage from specific spell -### FIRE_IMMUNITY,WATER_IMMUNITY, EARTH_IMMUNITY, AIR_IMMUNITY - -- subtype 0 - all, 1 - all except positive, 2 - only damage spells +- val: increase to damage, percentage +- subtype: spell identifer ### MIND_IMMUNITY -Creature is immune to all mind spells. +Affected creature is immune to all mind spells and receives reduced damage from Mind Elemental ### SPELL_IMMUNITY -- subtype - spell id -- ainfo - 0 - normal, 1 - absolute +Affected unit is completely immune to effects of specific spell + +- subid: identifier of spell to which unit is immune ### RECEPTIVE -WoG ability. Creature accepts all friendly spells even though it would -be normally immune to it. +Affected unit can be affected by all friendly spells even it would be normally immune to such spell. # Spell effects ### POISON +TODO: describe + - val - max health penalty from poison possible ### SLAYER -- value - spell level +Affected unit will deal increased damage to creatures with KING bonus -### BIND_EFFECT - -doesn't do anything particular, works as a marker +- val: skill mastery of Slayer spell, only creatures with lower or equal value of KING bonus are affected ### FORGETFULL -forgetfulness spell effect +Affected unit has its ranged attack power reduced (Forgetfulness) -- value - level +- val: if 0 or 1, damage is reduced by 50%. If greater than 1 then creature can not use ranged attacks ### NOT_ACTIVE +Affected unit can not act and is excluded from turn order (Blind, Stone Gaze, Paralyze) + ### ALWAYS_MINIMUM_DAMAGE -unit does its minimum damage from range +Affected creature always deals its minimum damage -- subtype: -1 - any attack, 0 - melee, 1 - ranged -- value: additional damage penalty (it'll subtracted from dmg) -- additional info - multiplicative anti-bonus for dmg in % \[eg 20 - means that creature will inflict 80% of normal minimal dmg\] +- val: additional decrease to unit's minimum damage, points. Can not reduce damage to zero ### ALWAYS_MAXIMUM_DAMAGE -eg. bless effect +Affected creature always deals its maximum damage. If unit is also affected by ALWAYS_MINIMUM_DAMAGE then only additional bonus to damage will be applied -- subtype: -1 - any attack, 0 - melee, 1 - ranged -- value: additional damage -- additional info - multiplicative bonus for dmg in % +- val: additional increase to unit's maximum damage, points ### ATTACKS_NEAREST_CREATURE -while in berserk +Affected unit can not be controlled by player and instead it will attempt to move and attack nearest unit, friend or foe ### IN_FRENZY -- value - level +Affected unit's defence is reduced to 0 and is transferred to attack with specified multiplier + +- val: multiplier factor with which defence is transferred to attack (percentage) ### HYPNOTIZED +Affected unit is considered to be hypnotized and will be controlled by enemy player + ### NO_RETALIATION -Eg. when blinded or paralyzed. +Affected unit will never retaliate to an attack (Blind, Paralyze) -# Undocumented - -### LEVEL_COUNTER -for commander artifacts +# Others ### BLOCK_MAGIC_ABOVE -blocks casting spells of the level > value + +Blocks casting spells of the level above specified one in battles affected by this bonus + +- val: level above which spellcasting is blocked ### BLOCK_ALL_MAGIC -blocks casting spells -### SPELL_IMMUNITY -subid - spell id +Blocks casting of all spells in battles affected by this bonus ### GENERAL_DAMAGE_PREMY +Affected unit will deal more damage in all attacks (Adela specialty) + +- val: additional damage, percentage + ### DISGUISED -subtype - spell level + +Affected heroes will be under effect of Disguise spell, hiding some of their information from opposing players + +- subtype: spell mastery level ### VISIONS -subtype - spell level -### SYNERGY_TARGET -dummy skill for alternative upgrades mod +Affected heroes will be under effect of Visions spell, revealing information of enemy objects in specific range + +- val: multiplier to effect range. Information is revealed within (val \* hero spell power) range ### BLOCK_MAGIC_BELOW -blocks casting spells of the level < value -### SPECIAL_ADD_VALUE_ENCHANT -specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add +Blocks casting spells of the level below specified one in battles affected by this bonus -### SPECIAL_FIXED_VALUE_ENCHANT -specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. +- val: level below which spellcasting is blocked + +### BIND_EFFECT + +Dummy bonus that acts as marker for Dendroid's Bind ability + +### SYNERGY_TARGET + +Dummy skill for alternative upgrades mod ### TOWN_MAGIC_WELL -one-time pseudo-bonus to implement Magic Well in the town + +Internal bonus, do not use + +### LEVEL_COUNTER + +Internal bonus, do not use + From 27b75dbbeaec7f985baf9a8b2a08495ce43face5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 00:30:04 +0300 Subject: [PATCH 0592/1248] Fixed silent errors on invalid Json references in schemas --- config/schemas/townBuilding.json | 2 +- lib/JsonDetail.cpp | 19 +++++++++++++++---- lib/JsonNode.cpp | 14 ++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/config/schemas/townBuilding.json b/config/schemas/townBuilding.json index 43b1344ee..7a86acdab 100644 --- a/config/schemas/townBuilding.json +++ b/config/schemas/townBuilding.json @@ -17,7 +17,7 @@ "additionalItems" : { "description" : "Following items that contain expression elements", - "$ref" : "#/definitions/buidingRequirement" + "$ref" : "#/definitions/buildingRequirement" } } }, diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index a395fe4b8..9a873b077 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -728,7 +728,11 @@ namespace //node must be validated using schema pointed by this reference and not by data here //Local reference. Turn it into more easy to handle remote ref if (boost::algorithm::starts_with(URI, "#")) - URI = validator.usedSchemas.back() + URI; + { + const std::string name = validator.usedSchemas.back(); + const std::string nameClean = name.substr(0, name.find('#')); + URI = nameClean + URI; + } return check(URI, data, validator); } @@ -739,9 +743,16 @@ namespace auto checker = formats.find(schema.String()); if (checker != formats.end()) { - std::string result = checker->second(data); - if (!result.empty()) - errors += validator.makeErrorMessage(result); + if (data.isString()) + { + std::string result = checker->second(data); + if (!result.empty()) + errors += validator.makeErrorMessage(result); + } + else + { + errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); + } } else errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 223e03ca2..5b6b27957 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1279,9 +1279,19 @@ const JsonNode & JsonUtils::getSchema(const std::string & URI) // check if json pointer if present (section after hash in string) if(posHash == std::string::npos || posHash == URI.size() - 1) - return getSchemaByName(filename); + { + auto const & result = getSchemaByName(filename); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } else - return getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); + { + auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } } void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) From 43020c81089a27d4e498989159901f9fce6ae69e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 00:30:19 +0300 Subject: [PATCH 0593/1248] Added value missing from schema --- config/schemas/spell.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 7de694c37..5a3b55c14 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -21,7 +21,8 @@ "type" : "object", "properties" : { "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, - "defName" : {"type" : "string", "format" : "defFile"} + "defName" : {"type" : "string", "format" : "defFile"}, + "effectName" : { "type" : "string" } }, "additionalProperties" : false } From 52cb4505e12b8a40db29f0a545c2100068a82aca Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 00:30:42 +0300 Subject: [PATCH 0594/1248] RMG schema now actually works --- config/schemas/template.json | 97 +++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/config/schemas/template.json b/config/schemas/template.json index 5e0e8934f..7bd9ca11c 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -8,16 +8,35 @@ "zone" : { "type" : "object", "required" : ["type", "monsters", "size"], + "additionalProperties" : false, "properties" : { - "type" : {"$ref" : "#/definitions/type"}, - "size" : {"$ref" : "#/definitions/size"}, - "playerTowns" : {"$ref" : "#/definitions/playerTowns"}, - "neuralTowns" : {"$ref" : "#/definitions/neuralTowns"}, - "townsAreSameType" : {"$ref" : "#/definitions/townsAreSameType"}, - "terrainTypes": {"$ref" : "#/definitions/terrains"}, - "bannedTerrains": {"$ref" : "#/definitions/terrains"}, - "monsters" : {"$ref" : "#/definitions/monsters"}, - "bannedMonsters" : {"$ref" : "#/definitions/monsters"}, + "type" : { + "type" : "string", + "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + }, + "size" : { "type" : "number", "minimum" : 1 }, + "owner" : {}, + "playerTowns" : {"$ref" : "#/definitions/towns"}, + "neutralTowns" : {"$ref" : "#/definitions/towns"}, + "matchTerrainToTown" : { "type" : "boolean"}, + "minesLikeZone" : { "type" : "number" }, + "terrainTypeLikeZone" : { "type" : "number" }, + "treasureLikeZone" : { "type" : "number" }, + + "terrainTypes": {"$ref" : "#/definitions/stringArray"}, + "bannedTerrains": {"$ref" : "#/definitions/stringArray"}, + + "townsAreSameType" : { "type" : "boolean"}, + "allowedMonsters" : {"$ref" : "#/definitions/stringArray"}, + "bannedMonsters" : {"$ref" : "#/definitions/stringArray"}, + "allowedTowns" : {"$ref" : "#/definitions/stringArray"}, + "bannedTowns" : {"$ref" : "#/definitions/stringArray"}, + + "monsters" : { + "type" : "string", + "enum" : ["weak", "normal", "strong", "none"] + }, + "mines" : {"$ref" : "#/definitions/mines"}, "treasure" : { "type" : "array", @@ -27,21 +46,39 @@ "min" : {"type" : "number", "minimum" : 0}, "max" : {"type" : "number", "minimum" : 0}, "density" : {"type" : "number", "minimum" : 1} - }, - "additionalProperties" : false + }, + "additionalProperties" : false } - } - } + } + } }, - "type" : { - "type" : "string", + + "towns" : { + "type" : "object", "additionalProperties" : false, - "enum" : ["playerStart", "cpuStart", "treasure", "junction"] + "properties" : { + "towns" : { "type" : "number" }, + "castles" : { "type" : "number" }, + "townDensity" : { "type" : "number" }, + "castleDensity" : { "type" : "number" } + } }, - "size" : { - "type" : "number", - "minimum" : 1, - "additionalProperties" : false + "stringArray" : { + "type" : "array", + "items" : { "type" : "string" } + }, + "mines" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "gold" : { "type" : "number"}, + "wood" : { "type" : "number"}, + "ore" : { "type" : "number"}, + "mercury" : { "type" : "number"}, + "sulfur" : { "type" : "number"}, + "crystal" : { "type" : "number"}, + "gems" : { "type" : "number"} + } }, "connection" : { @@ -74,16 +111,30 @@ "type" : "string" } }, + + "additionalProperties" : false, "properties" : { - "required" : ["zones", "connections"], - "additionalProperties" : false, + "required" : ["zones", "connections", "minSize", "maxSize", "players"], + + "players" : { + "type": "string" + }, + "cpu" : { + "type": "string" + }, + "minSize" : { + "type": "string" + }, + "maxSize" : { + "type": "string" + }, "description" : { "type": "string" }, "zones" : { "type" : "object", - "additionalProperties" : {"$ref" : "#/definitions/zone" } + "additionalProperties" : {"$ref" : "#/definitions/zone" } }, "connections" : { "type" : "array", From dfc72134f285c5e03b7c16aea01621034378b589 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 14:03:21 +0300 Subject: [PATCH 0595/1248] Removed unused property --- config/schemas/creature.json | 4 ---- lib/CCreatureHandler.cpp | 4 ++-- lib/CCreatureHandler.h | 10 +--------- 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/config/schemas/creature.json b/config/schemas/creature.json index 58a09ca12..d5bc82167 100644 --- a/config/schemas/creature.json +++ b/config/schemas/creature.json @@ -250,10 +250,6 @@ "timeBetweenFidgets" : { "type" : "number", "description" : "How often creature will play idling animation" - }, - "troopCountLocationOffset" : { - "type" : "number", - "description" : "Position of troop count label?" } } }, diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index b65227d77..91061b70d 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -844,7 +844,8 @@ void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser missile["frameAngles"].Vector().push_back(entry); } - graphics["troopCountLocationOffset"].Float() = parser.readNumber(); + // Unused property "troopCountLocationOffset" + parser.readNumber(); missile["attackClimaxFrame"].Float() = parser.readNumber(); @@ -857,7 +858,6 @@ void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const { cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); - cre->animation.troopCountLocationOffset = static_cast(graphics["troopCountLocationOffset"].Float()); const JsonNode & animationTime = graphics["animationTime"]; cre->animation.walkAnimationTime = animationTime["walk"].Float(); diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index b6cc5b11f..654fe9d22 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -96,7 +96,7 @@ public: upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; std::vector missleFrameAngles; - int troopCountLocationOffset, attackClimaxFrame; + int attackClimaxFrame; AnimationPath projectileImageName; std::vector projectileRay; @@ -108,13 +108,6 @@ public: h & idleAnimationTime; h & walkAnimationTime; h & attackAnimationTime; - - if (version < 814) - { - float unused = 0.f; - h & unused; - } - h & upperRightMissleOffsetX; h & rightMissleOffsetX; h & lowerRightMissleOffsetX; @@ -122,7 +115,6 @@ public: h & rightMissleOffsetY; h & lowerRightMissleOffsetY; h & missleFrameAngles; - h & troopCountLocationOffset; h & attackClimaxFrame; h & projectileImageName; h & projectileRay; From 0b044663d9f729ead33fa08c03144d87c3270b6c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 14:03:43 +0300 Subject: [PATCH 0596/1248] Updated docs --- docs/modders/Campaign_Format.md | 1 + docs/modders/Mod_File_Format.md | 17 +++++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/docs/modders/Campaign_Format.md b/docs/modders/Campaign_Format.md index 32500d269..2458fb320 100644 --- a/docs/modders/Campaign_Format.md +++ b/docs/modders/Campaign_Format.md @@ -88,6 +88,7 @@ Prolog and epilog properties are optional { "video": "NEUTRALA.smk", //video to show "music": "musicFile.ogg", //music to play, should be located in music directory + "voice": "musicFile.wav", //voice to play, should be located in sounds directory "text": "some long text" //text to be shown } ``` diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index cb47e5a98..ca048a2c8 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -178,6 +178,23 @@ These are fields that are present only in local mod.json file } ``` +## Mod repository fields + +These are fields that are present only in remote repository and are generally not used in mod.json + +```jsonc +{ + // URL to mod.json that describes this mod + "mod" : "https://raw.githubusercontent.com/vcmi-mods/vcmi-extras/vcmi-1.4/mod.json", + + // URL that player can use to download mod + "download" : "https://github.com/vcmi-mods/vcmi-extras/archive/refs/heads/vcmi-1.4.zip", + + // Approximate size of download, megabytes + "downloadSize" : 4.496 +} +``` + ## Notes For mod description it is possible to use certain subset of HTML as From 7b8ca9fd40245c3bf7f934b16e94905da8846b8f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 14:04:15 +0300 Subject: [PATCH 0597/1248] Add missing schema for "rmg" section of object description --- config/schemas/objectType.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/config/schemas/objectType.json b/config/schemas/objectType.json index 5a955fa78..272e5d266 100644 --- a/config/schemas/objectType.json +++ b/config/schemas/objectType.json @@ -13,8 +13,27 @@ "type" : "number" }, "base" : { + "additionalProperties" : true, // Not validated on its own - instead data copied to main object and validated as part of it "type" : "object" }, + "rmg" : { + "additionalProperties" : false, + "type" : "object", + "properties" : { + "value" : { + "type" : "number" + }, + "mapLimit" : { + "type" : "number" + }, + "zoneLimit" : { + "type" : "number" + }, + "rarity" : { + "type" : "number" + } + } + }, "templates" : { "type" : "object", "additionalProperties" : { From 9710a6b7b362e379c90541ca8b6b90d889ed2de9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 14:06:56 +0300 Subject: [PATCH 0598/1248] Updated terrain/river/road schema, added docs --- config/schemas/river.json | 4 +- config/schemas/terrain.json | 16 ++-- docs/modders/Entities_Format/River_Format.md | 30 +++++++- docs/modders/Entities_Format/Road_Format.md | 19 ++++- .../modders/Entities_Format/Terrain_Format.md | 77 ++++++++++++++++++- lib/TerrainHandler.cpp | 1 - lib/TerrainHandler.h | 2 +- 7 files changed, 134 insertions(+), 15 deletions(-) diff --git a/config/schemas/river.json b/config/schemas/river.json index 38e0d780e..db88affaf 100644 --- a/config/schemas/river.json +++ b/config/schemas/river.json @@ -9,7 +9,7 @@ "shortIdentifier" : { "type" : "string", - "description" : "Two-letters unique indentifier for this road. Used in map format" + "description" : "Two-letters unique indentifier for this river. Used in map format" }, "text" : { @@ -29,7 +29,7 @@ }, "paletteAnimation" : { "type" : "array", - "description" : "If defined, terrain will be animated using palette color cycling effect", + "description" : "If defined, river will be animated using palette color cycling effect", "items" : { "type" : "object", diff --git a/config/schemas/terrain.json b/config/schemas/terrain.json index e9e5f2b91..bc518106b 100644 --- a/config/schemas/terrain.json +++ b/config/schemas/terrain.json @@ -22,7 +22,7 @@ "description" : "Type of this terrain. Can be land, water, subterranean or rock", "items" : { - "enum" : ["LAND", "WATER", "SUB", "ROCK", "SURFACE"], + "enum" : ["WATER", "SUB", "ROCK", "SURFACE"], "type" : "string" } }, @@ -67,7 +67,7 @@ "battleFields" : { "type" : "array", - "description" : "array of battleFields for this terrain", + "description" : "List of battleFields that can be used on this terrain", "items" : { "type" : "string" @@ -76,7 +76,7 @@ "minimapUnblocked" : { "type" : "array", - "description" : "Color of terrain on minimap without unpassable objects", + "description" : "Color of terrain on minimap without unpassable objects. RGB triplet, 0-255 range", "minItems" : 3, "maxItems" : 3, "items" : @@ -87,7 +87,7 @@ "minimapBlocked" : { "type" : "array", - "description" : "Color of terrain on minimap with unpassable objects", + "description" : "Color of terrain on minimap with unpassable objects. RGB triplet, 0-255 range", "minItems" : 3, "maxItems" : 3, "items" : @@ -104,14 +104,14 @@ "sounds" : { "type" : "object", - "description" : "list of sounds for this terrain", + "description" : "List of sounds for this terrain", "additionalProperties" : false, "properties" : { "ambient" : { "type" : "array", - "description" : "list of ambient sounds for this terrain", + "description" : "List of ambient sounds for this terrain", "items" : { "type" : "string", @@ -135,7 +135,7 @@ "prohibitTransitions" : { "type" : "array", - "description" : "array or terrain names, which is prohibited to make transition from/to", + "description" : "List or terrain names, which is prohibited to make transition from/to", "items" : { "type" : "string" @@ -149,7 +149,7 @@ "terrainViewPatterns" : { "type" : "string", - "description" : "Can be normal, dirt, water, rock" + "description" : "Represents layout of tile orientations in terrain tiles file" }, "index" : { diff --git a/docs/modders/Entities_Format/River_Format.md b/docs/modders/Entities_Format/River_Format.md index 8b157aa90..1b4f96cda 100644 --- a/docs/modders/Entities_Format/River_Format.md +++ b/docs/modders/Entities_Format/River_Format.md @@ -1,3 +1,31 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / River Format -TODO \ No newline at end of file +## Format + +```jsonc +"newRiver" : +{ + // Two-letters unique indentifier for this river. Used in map format + "shortIdentifier" : "mr", + + // Human-readable name of the river + "text" : "My Road", + + // Name of file with river graphics + "tilesFilename" : "myRiver.def" + + // Name of file with river delta graphics + // TODO: describe how exactly to use this tricky field + "delta" : "", + + // If defined, river will be animated using palette color cycling effect + // Game will cycle "length" colors starting from "start" (zero-based index) on each animation update every 180ms + // Color numbering uses palette color indexes, as seen in image editor + // Note that some tools for working with .def files may reorder palette. + // To avoid this, it is possible to use json with indexed png images instead of def files + "paletteAnimation" : [ + { "start" : 10, "length" : 5 }, + ... + ] +} +``` \ No newline at end of file diff --git a/docs/modders/Entities_Format/Road_Format.md b/docs/modders/Entities_Format/Road_Format.md index a15c0af36..c615decdf 100644 --- a/docs/modders/Entities_Format/Road_Format.md +++ b/docs/modders/Entities_Format/Road_Format.md @@ -1,3 +1,20 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Road Format -TODO \ No newline at end of file +## Format + +```jsonc +"newRoad" : +{ + // Two-letters unique indentifier for this road. Used in map format + "shortIdentifier" : "mr", + + // Human-readable name of the road + "text" : "My Road", + + // Name of file with road graphics + "tilesFilename" : "myRoad.def" + + // How many movement points needed to move hero + "moveCost" : 66 +} +``` \ No newline at end of file diff --git a/docs/modders/Entities_Format/Terrain_Format.md b/docs/modders/Entities_Format/Terrain_Format.md index f8361514c..22a856aa1 100644 --- a/docs/modders/Entities_Format/Terrain_Format.md +++ b/docs/modders/Entities_Format/Terrain_Format.md @@ -1,3 +1,78 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Terrain Format -TODO \ No newline at end of file +## Format + +```jsonc +"newTerrain" : +{ + // Two-letters unique indentifier for this terrain. Used in map format + "shortIdentifier" : "mt", + + // Human-readable name of the terrain + "text" : "My Road", + + // Type(s) of this terrain. + // WATER - this terrain is water-like terrains that requires boat for movement + // ROCK - this terrain is unpassable "rock" terrain that is used for inacessible parts of underground layer + // SUB - this terrain can be placed in underground map layer by RMG + // SURFACE - this terrain can be placed in surface map layer by RMG + "type" : [ "WATER", "SUB", "ROCK", "SURFACE" ], + + // Name of file with road graphics + "tiles" : "myRoad.def", + + // How many movement points needed to move hero on this terrain + "moveCost" : 150, + + // The name of rock type terrain which will be used as borders in the underground + // By default, H3 terrain "rock" will be used + "rockTerrain" : "rock", + + // River type which should be used for that terrain + "river" : "", + + // If defined, terrain will be animated using palette color cycling effect + // Game will cycle "length" colors starting from "start" (zero-based index) on each animation update every 180ms + // Color numbering uses palette color indexes, as seen in image editor + // Note that some tools for working with .def files may reorder palette. + // To avoid this, it is possible to use json with indexed png images instead of def files + "paletteAnimation" : [ + { "start" : 10, "length" : 5 }, + ... + ], + + // List of battleFields that can be used on this terrain + "battleFields" : [ ] + + // Color of terrain on minimap without unpassable objects. RGB triplet, 0-255 range + "minimapUnblocked" : [ 150, 100, 50 ], + + // Color of terrain on minimap with unpassable objects. RGB triplet, 0-255 range + "minimapBlocked" : [ 150, 100, 50 ], + + // Music filename to play on this terrain on adventure map + "music" : "", + + "sounds" : { + // List of ambient sounds for this terrain + "ambient" : [ "" ] + }, + + // Hero movement sound for this terrain, version for moving on tiles with road + "horseSound" : "", + + // Hero movement sound for this terrain, version for moving on tiles without road + "horseSoundPenalty" : "", + + // List or terrain names, which is prohibited to make transition from/to + "prohibitTransitions" : [ "" ], + + // If sand/dirt transition required from/to other terrains + "transitionRequired" : false, + + // Represents layout of tile orientations in terrain tiles file + // Can be normal, dirt, water, rock, or hota + "terrainViewPatterns" : "", + +} +``` \ No newline at end of file diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index a6050cb40..2c33519d1 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -58,7 +58,6 @@ TerrainType * TerrainTypeHandler::loadFromJson( const std::string & scope, const { //Set bits const auto & s = node.String(); - if (s == "LAND") info->passabilityType |= TerrainType::PassabilityType::LAND; if (s == "WATER") info->passabilityType |= TerrainType::PassabilityType::WATER; if (s == "ROCK") info->passabilityType |= TerrainType::PassabilityType::ROCK; if (s == "SURFACE") info->passabilityType |= TerrainType::PassabilityType::SURFACE; diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 7455a0cbe..60d649d3c 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -44,7 +44,7 @@ class DLL_LINKAGE TerrainType : public EntityT enum PassabilityType : ui8 { - LAND = 1, + //LAND = 1, WATER = 2, SURFACE = 4, SUBTERRANEAN = 8, From f4ee9424e4011d9647e8e7baa938bd2de21173a6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 14:10:41 +0300 Subject: [PATCH 0599/1248] Removed unused properties --- lib/mapObjectConstructors/CBankInstanceConstructor.cpp | 5 ----- lib/mapObjectConstructors/CBankInstanceConstructor.h | 6 ------ 2 files changed, 11 deletions(-) diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index ba8e4ea2c..ed10802c3 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -39,10 +39,7 @@ BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRan BankConfig bc; bc.chance = static_cast(level["chance"].Float()); - bc.guards = JsonRandom::loadCreatures(level["guards"], rng); - bc.upgradeChance = static_cast(level["upgrade_chance"].Float()); - bc.combatValue = static_cast(level["combat_value"].Float()); std::vector spells; IObjectInterface::cb->getAllowedSpells(spells); @@ -52,8 +49,6 @@ BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRan bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng); bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, spells); - bc.value = static_cast(level["value"].Float()); - return bc; } diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.h b/lib/mapObjectConstructors/CBankInstanceConstructor.h index f09d256a2..f926a65f2 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.h +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.h @@ -20,10 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct BankConfig { - ui32 value = 0; //overall value of given things ui32 chance = 0; //chance for this level being chosen - ui32 upgradeChance = 0; //chance for creatures to be in upgraded versions - ui32 combatValue = 0; //how hard are guards of this level std::vector guards; //creature ID, amount ResourceSet resources; //resources given in case of victory std::vector creatures; //creatures granted in case of victory (creature ID, amount) @@ -33,13 +30,10 @@ struct BankConfig template void serialize(Handler &h, const int version) { h & chance; - h & upgradeChance; h & guards; - h & combatValue; h & resources; h & creatures; h & artifacts; - h & value; h & spells; } }; From 9f894d6ca089f048d8ec16694b348ef86a672c41 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 18 Sep 2023 15:36:35 +0300 Subject: [PATCH 0600/1248] Updated RMG template schema --- config/schemas/template.json | 12 +++-- docs/modders/Random_Map_Template.md | 68 +++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/config/schemas/template.json b/config/schemas/template.json index 7bd9ca11c..c91f3bd4d 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -99,7 +99,6 @@ "type": { "type" : "string", - "additionalProperties" : false, "enum" : ["wide", "fictive", "repulsive"] } } @@ -107,7 +106,6 @@ "waterContent" : { "enum" : ["none", "normal", "islands"], - "additionalProperties" : false, "type" : "string" } }, @@ -118,29 +116,37 @@ "required" : ["zones", "connections", "minSize", "maxSize", "players"], "players" : { + "description" : "Number of players that will be present on map (human or AI)", "type": "string" }, "cpu" : { + "description" : "Optional, number of AI-only players", "type": "string" }, "minSize" : { + "description" : "Minimal size of the map, e.g. 'm+u' or '120x120x1", "type": "string" }, "maxSize" : { + "description" : "Maximal size of the map, e.g. 'm+u' or '120x120x1", "type": "string" }, - "description" : { + "name" : { + "description" : "Optional name - useful to have several template variations with same name", "type": "string" }, "zones" : { + "description" : "List of named zones", "type" : "object", "additionalProperties" : {"$ref" : "#/definitions/zone" } }, "connections" : { + "description" : "List of connections between zones", "type" : "array", "items" : {"$ref" : "#/definitions/connection"} }, "allowedWaterContent" : { + "description" : "Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified", "type" : "array", "items" : {"$ref" : "#/definitions/waterContent"} } diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 886ce5414..5a1df570d 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -6,9 +6,8 @@ /// Unique template name "Triangle" : { - //optional name - useful to have several template variations with same name (since 0.99) + //Optional name - useful to have several template variations with same name "name" : "Custom template name", - "description" : "Brief description of template, recommended setting or rules". /// Minimal and maximal size of the map. Possible formats: /// Size code: s, m, l or xl for size with optional suffix "+u" for underground @@ -19,7 +18,7 @@ /// Number of players that will be present on map (human or AI) "players" : "2-4", - /// Number of AI-only players + /// Optional, number of AI-only players "cpu" : "2", ///Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified @@ -37,9 +36,9 @@ { "a" : "zoneA", "b" : "zoneB", "guard" : 5000, "road" : "false" }, { "a" : "zoneA", "b" : "zoneC", "guard" : 5000, "road" : "random" }, { "a" : "zoneB", "b" : "zoneC", "type" : "wide" } - //"type" can be "guarded" (default), "wide", "fictive" or "repulsive" - //"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual - - //they do not create actual path, but only attract or repulse zones, respectively + //"type" can be "guarded" (default), "wide", "fictive" or "repulsive" + //"wide" connections have no border, or guard. "fictive" and "repulsive" connections are virtual - + //they do not create actual path, but only attract or repulse zones, respectively ] } ``` @@ -48,38 +47,71 @@ ``` javascript { - "type" : "playerStart", //"cpuStart" "treasure" "junction" - "size" : 2, //relative size of zone - "owner" : 1, //player owned this zone + // Type of this zone. Possible values are: + // "playerStart", "cpuStart", "treasure", "junction" + "type" : "playerStart", + + // relative size of zone + "size" : 2, + + // index of player that owns this zone + "owner" : 1, + + // castles and towns owned by player in this zone "playerTowns" : { "castles" : 1 - //"towns" : 1 + "towns" : 1 }, + + // castles and towns that are neutral on game start in this zone "neutralTowns" : { //"castles" : 1 "towns" : 1 }, + + // if true, all towns generated in this zone will belong to the same faction "townsAreSameType" : true, - "monsters" : "normal", //"weak" "strong", "none" - All treasures will be unguarded + + //"weak" "strong", "none" - All treasures will be unguarded + "monsters" : "normal", - "terrainTypes" : [ "sand" ], //possible terrain types. All terrains will be available if not specified - "bannedTerrains" : ["lava", "asphalt"] //optional + //possible terrain types. All terrains will be available if not specified + "terrainTypes" : [ "sand" ], + + //optional, list of explicitly banned terrain types + "bannedTerrains" : ["lava", "asphalt"] - "matchTerrainToTown" : false, //if true, terrain for this zone will match native terrain of player faction + // if true, terrain for this zone will match native terrain of player faction. Used only in owned zones + "matchTerrainToTown" : false, + + // Mines will have same configuration as in linked zone "minesLikeZone" : 1, + + // Treasures will have same configuration as in linked zone "treasureLikeZone" : 1 + + // Terrain type will have same configuration as in linked zone "terrainTypeLikeZone" : 3 - "allowedMonsters" : ["inferno", "necropolis"] //factions of monsters allowed on this zone - "bannedMonsters" : ["fortress", "stronghold", "conflux"] //These monsers will never appear in the zone - "allowedTowns" : ["castle", "tower", "rampart"] //towns allowed on this terrain - "bannedTowns" : ["necropolis"] //towns will never spawn on this terrain + // factions of monsters allowed on this zone + "allowedMonsters" : ["inferno", "necropolis"] + + // These monsers will never appear in the zone + "bannedMonsters" : ["fortress", "stronghold", "conflux"] + + // towns allowed on this terrain + "allowedTowns" : ["castle", "tower", "rampart"] + + // towns will never spawn on this terrain + "bannedTowns" : ["necropolis"] + // List of mines that will be added to this zone "mines" : { "wood" : 1, "ore" : 1, }, + // List of treasures that will be placed in this zone "treasure" : [ { "min" : 2100, From ddda6c0c010ed959fda319b5a0fcaa6217c91e5b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 23 Sep 2023 16:38:17 +0300 Subject: [PATCH 0601/1248] Updated localization docs --- docs/modders/Mod_File_Format.md | 19 +++++ docs/modders/Translations.md | 125 ++++++++++++++++++++++++-------- 2 files changed, 115 insertions(+), 29 deletions(-) diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index ca048a2c8..900980457 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -32,6 +32,9 @@ // Type of mod, list of all possible values: // "Translation", "Town", "Test", "Templates", "Spells", "Music", "Sounds", "Skills", "Other", "Objects", // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Artifacts", "AI" + // + // Some mod types have additional effects on your mod: + // Translation: mod of this type is only active if player uses base language of this mod. See "language" property. "modType" : "Graphical", // Base language of the mod, before applying localizations. By default vcmi assumes English @@ -178,6 +181,22 @@ These are fields that are present only in local mod.json file } ``` +## Translation fields + +In addition to field listed above, it is possible to add following block for any language supported by VCMI. If such block is present, Launcher will use this information for displaying translated mod information and game will use provided json files to translate mod to specified language. +See [Translations](Translations.md) for more information + +``` + "" : { + "name" : "", + "description" : "", + "author" : "", + "translations" : [ + "config//.json" + ] + }, +``` + ## Mod repository fields These are fields that are present only in remote repository and are generally not used in mod.json diff --git a/docs/modders/Translations.md b/docs/modders/Translations.md index 40f62f6ac..a4533d864 100644 --- a/docs/modders/Translations.md +++ b/docs/modders/Translations.md @@ -1,64 +1,131 @@ < [Documentation](../Readme.md) / [Modding](Readme.md) / Translations -## For translators -### Adding new languages -New languages require minor changes in the source code. Please contact someone from vcmi team if you wish to translate game into a new language +# Translation system -### Translation of Launcher and Map Editor -TODO: write me +## List of currently supported languages -### Translation of game content -TODO: write me +This is list of all languages that are currently supported by VCMI. If your languages is missing from the list and you wish to translate VCMI - please contact our team and we'll add support for your language in next release. -## For modders +- Czech +- Chinese (Simplified) +- English +- Finnish +- French +- German +- Hungarian +- Italian +- Korean +- Polish +- Portuguese (Brazilian) +- Russian +- Spanish +- Swedish +- Turkish +- Ukrainian +- Vietnamese -### Adding new translation to mod -TODO: write me +## Translating Heroes III data -### Translation of mod information +VCMI allows translating game data into languages other than English. In order to translate Heroes III in your language easiest approach is to: + +- Copy existing translation, such as English translation from here: https://github.com/vcmi-mods/h3-for-vcmi-englisation +- Rename mod to indicate your language, preferred form is "(language)-translation" +- Update mod.json to match your mod +- Translate all texts strings from game.json, campaigns.json and maps.json + +If you have already existing Heroes III translation you can: + +- Install VCMI and select your localized Heroes III data files for VCMI data files +- Launch VCMI_Client.exe directly from game install directory +- In console window, type `convert txt` + +This will export all strings from game into `Documents/My Games/VCMI/VCMI_Client_log.txt` which you can then use to update json files in your translation + +## Translating VCMI data + +VCMI contains several new strings, to cover functionality not existing in Heroes III. It can be roughly split into following parts: + +- In-game texts, most noticeably - in-game settings menu. +- Game Launcher +- Map Editor + +Before you start, make sure that you have copy of VCMI source code. If you are not familiar with git, you can use Github Desktop to clone VCMI repository. + +### Translation of in-game data + +In order to translate in-game data you need: +- Add section with your language to `/Mods/VCMI/mod.json`, similar to other languages +- Copy English translation file in `/Mods/VCMI/config/vcmi/english.json` and rename it to name of your language. Note that while you can copy any language other than English, other files might not be up to date and may have missing strings. +- Translate copied file to your language. + +After this, you can set language in Launcher to your language and start game. All translated strings should show up in your language. + +### Translation of Launcher and Editor + +VCMI Launcher and Map Editor use translation system provided by Qt framework so it requires slightly different approach than in-game translations: + +- Install Qt Linguist. You can find find standalone version here: https://download.qt.io/linguist_releases/ +- Open `/launcher/translation/` directory, copy english.ts file and rename it to your language +- Launch Qt Linguist, select Open and navigate to your copied file +- Select any untranslated string, enter translation in field below, and click "Done and Next" (Ctrl+Return) to navigate to next untranslated string +- Once translation has been finished, save resulting file. + +Translation of Map Editor is identical, except for location of translation files. Open `/editor/translation/` instead to translate Map Editor + +### Submitting changes + +Once you have finished with translation you need to submit these changes to vcmi team using git or Github Desktop +- Commit all your changed files +- Push changes to your forked repository +- Create pull request in VCMI repository with your changes + +If everything is OK, your changes will be accepted and will be part of next release. + +## Translating mods + +### Exporting translation + +TODO + +### Translating mod information In order to display information in Launcher in language selected by user add following block into your mod.json: ``` "" : { "name" : "", "description" : "", "author" : "", - "modType" : "", + "translations" : [ + "config//.json" + ] }, ``` -List of currently supported values for language parameter: -- english -- german -- polish -- russian -- ukrainian - However, normally you don't need to use block for English. Instead, English text should remain in root section of your mod.json file, to be used when game can not find translated version. -### Translation of mod content -TODO: write me +# Developers documentation -### Searching for missing translation strings -TODO: write me - -## For developers ### Adding new languages In order to add new language it needs to be added in multiple locations in source code: - Generate new .ts files for launcher and map editor, either by running `lupdate` with name of new .ts or by copying english.ts and editing language tag in the header. -- Add new language into Launcher UI drop-down selector and name new string using such format: -`" ()"` -- Add new language into array of possible values for said selector +- Add new language into lib/Languages.h entry. This will trigger static_assert's in places that needs an update in code +- Add new language into json schemas validation list - settings schema and mod schema +- Add new language into mod json format - in order to allow translation into new language Also, make full search for a name of an existing language to ensure that there are not other places not referenced here + ### Updating translation of Launcher and Map Editor to include new strings + At the moment, build system will generate binary translation files (.qs) that can be opened by Qt. However, any new or changed lines will not be added into existing .ts files. In order to update .ts files manually, open command line shell in `mapeditor` or `launcher` source directories and execute command ``` lupdate -no-obsolete * -ts translation/*.ts ``` -This will remove any no longer existing lines from translation and add any new lines for all translations. + +This will remove any no longer existing lines from translation and add any new lines for all translations. If you want to keep old lines, remove `-no-obsolete` key from the command There *may* be a way to do the same via QtCreator UI or via CMake, if you find one feel free to update this information. + ### Updating translation of Launcher and Map Editor using new .ts file from translators + Generally, this should be as simple as overwriting old files. Things that may be necessary if translation update is not visible in executable: - Rebuild subproject (map editor/launcher). - Regenerate translations via `lupdate -no-obsolete * -ts translation/*.ts` \ No newline at end of file From 403acc0cf1a4836b5c49850fb15ce1e098a1e6d5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 23 Sep 2023 19:21:12 +0300 Subject: [PATCH 0602/1248] Update spell docs --- config/schemas/spell.json | 139 ++++++++-------- docs/modders/Entities_Format/Spell_Format.md | 160 ++++++++++--------- lib/spells/CSpellHandler.cpp | 2 +- 3 files changed, 159 insertions(+), 142 deletions(-) diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 5a3b55c14..659a26150 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -130,99 +130,100 @@ "additionalProperties" : false, "properties" : { "index" : { - "type" : "number", - "description" : "numeric id of spell required only for original spells, prohibited for new spells" + "type" : "number", + "description" : "numeric id of spell required only for original spells, prohibited for new spells" }, "name" : { "type" : "string", - "description" : "Localizable name" + "description" : "Localizable name of this spell" }, "type" : { - "type" : "string", - "enum" : ["adventure", "combat", "ability"], - "description" : "Spell type" + "type" : "string", + "enum" : ["adventure", "combat", "ability"], + "description" : "Spell type" }, "school" : { - "type" : "object", - "description" : "Spell schools", - "additionalProperties" : false, - "properties" : { - "air" : {"type" : "boolean"}, - "fire" : {"type" : "boolean"}, - "earth" : {"type" : "boolean"}, - "water" : {"type" : "boolean"} - } + "type" : "object", + "description" : "List of spell schools this spell belongs to", + "additionalProperties" : false, + "properties" : { + "air" : {"type" : "boolean"}, + "fire" : {"type" : "boolean"}, + "earth" : {"type" : "boolean"}, + "water" : {"type" : "boolean"} + } }, "level" : { - "type" : "number", - "description" : "Spell level", - "minimum" : 0, - "maximum" : 5 + "type" : "number", + "description" : "Spell level", + "minimum" : 0, + "maximum" : 5 }, "power" : { - "type" : "number", - "description" : "Base power" + "type" : "number", + "description" : "Base power of the spell" }, "defaultGainChance" : { - "type" : "number", - "description" : "Gain chance by default for all factions" + "type" : "number", + "description" : "Gain chance by default for all factions" }, "gainChance" : { - "type" : "object", - "description" : "Chance in % to gain for faction. NOTE: this field is merged with faction config", - "additionalProperties" : { - "type" : "number", - "minimum" : 0 - } + "type" : "object", + "description" : "Chance for this spell to appear in Mage Guild of a specific faction", + "additionalProperties" : { + "type" : "number", + "minimum" : 0 + } }, "targetType" : { - "type" : "string", - "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", - "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] + "type" : "string", + "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", + "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] }, "counters" : { - "$ref" : "#/definitions/flags", - "description" : "Flags structure ids of countering spells" + "$ref" : "#/definitions/flags", + "description" : "Flags structure ids of countering spells" }, "flags" : { - "type" : "object", - "description" : "Various flags", - "additionalProperties" : false, - "properties" : { - "indifferent" : { - "type" : "boolean", - "description" : "Spell is indifferent for target" - }, - "negative" : { - "type" : "boolean", - "description" : "Spell is negative for target" - }, - "positive" : { - "type" : "boolean", - "description" : "Spell is positive for target" - }, - "damage" : { - "type" : "boolean", - "description" : "Spell does damage (direct or indirect)" - }, - "offensive" : { - "type" : "boolean", - "description" : "Spell does direct damage (implicitly sets damage and negative)" - }, - "rising" : { - "type" : "boolean", - "description" : "Rising spell (implicitly sets positive)" - }, - "special" : { - "type" : "boolean", - "description" : "Special spell. Can be given only by BonusType::SPELL" - }, - "nonMagical" : { + "type" : "object", + "description" : "Various flags", + "additionalProperties" : false, + "properties" : { + "indifferent" : { "type" : "boolean", - "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." + "description" : "Spell is indifferent for target" + }, + "negative" : { + "type" : "boolean", + "description" : "Spell is negative for target" + }, + "positive" : { + "type" : "boolean", + "description" : "Spell is positive for target" + }, + "damage" : { + "type" : "boolean", + "description" : "Spell does damage (direct or indirect)" + }, + "offensive" : { + "type" : "boolean", + "description" : "Spell does direct damage (implicitly sets damage and negative)" + }, + "rising" : { + "type" : "boolean", + "description" : "Rising spell (implicitly sets positive)" + }, + "special" : { + "type" : "boolean", + "description" : "Special spell. Can be given only by BonusType::SPELL" + }, + "nonMagical" : { + "type" : "boolean", + "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." } } + } }, "immunity" : { "$ref" : "#/definitions/flags", @@ -312,7 +313,7 @@ }, "onlyOnWaterMap" : { "type" : "boolean", - "description" : "It true, spell won't be available on a map without water" + "description" : "If true, spell won't be available on a map without water" } }, } diff --git a/docs/modders/Entities_Format/Spell_Format.md b/docs/modders/Entities_Format/Spell_Format.md index 3bd3882e3..350851acf 100644 --- a/docs/modders/Entities_Format/Spell_Format.md +++ b/docs/modders/Entities_Format/Spell_Format.md @@ -5,63 +5,79 @@ ``` javascript { "spellName": - { //numeric id of spell required only for original spells, prohibited for new spells - "index": 0, - //Original Heroes 3 info - //Mandatory, spell type - "type": "adventure",//"adventure", "combat", "ability" + { + // Mandatory. Spell type + // Allowed values: "adventure", "combat", "ability" + "type": "adventure", - //Mandatory, spell target type - "targetType":"NO_TARGET",//"CREATURE","OBSTACLE"."LOCATION" + // Mandatory. Spell target type + // "NO_TARGET" - instant cast no aiming (e.g. Armageddon) + // "CREATURE" - target is unit (e.g. Resurrection) + // "OBSTACLE" - target is obstacle (e.g. Remove Obstacle) + // "LOCATION" - target is location (e.g. Fire Wall) + "targetType":"NO_TARGET", - //Mandatory + // Localizable name of this spell "name": "Localizable name", - //Mandatory, flags structure of school names, Spell schools this spell belongs to + + // Mandatory. List of spell schools this spell belongs to "school": {"air":true, "earth":true, "fire":true, "water":true}, - //number, mandatory, Spell level, value in range 1-5 + + // Mandatory. Spell level, value in range 1-5, or 0 for abilities "level": 1, - //Mandatory, base power + + // Mandatory. Base power of the spell "power": 10, - //Mandatory, default chance for this spell to appear in Mage Guilds - //Used only if chance for a faction is not set in gainChance field + + // Mandatory. Default chance for this spell to appear in Mage Guilds + // Used only if chance for a faction is not set in gainChance field "defaultGainChance": 0, - //Optional, chance for it to appear in Mage Guild of a specific faction - //NOTE: this field is linker with faction configuration + + // Chance for this spell to appear in Mage Guild of a specific faction + // Symmetric property of "guildSpells" property in towns "gainChance": { - "factionName": 3 + "factionName" : 3 }, - //VCMI info - + "animation":{}, - //countering spells, flags structure of spell ids (spell. prefix is required) - "counters": {"spell.spellID1":true, ...} + // List of spells that will be countered by this spell + "counters": { + "spellID" : true, + ... + }, - //Mandatory,flags structure: - // indifferent, negative, positive - Positiveness of spell for target (required) - // damage - spell does damage (direct or indirect) - // offensive - direct damage (implicitly sets damage and negative) - // rising - rising spell (implicitly sets positive) - // summoning //todo: - // special - can be obtained only with bonus::SPELL + //Mandatory. List of flags that describe this spell + // positive - this spell is positive to target (buff) + // negative - this spell is negative to target (debuff) + // indifferent - spell is neither positive, nor negative + // damage - spell does damage (direct or indirect) + // offensive - direct damage (implicitly sets damage and negative) + // rising - rising spell (implicitly sets positive) + // special - this spell is normally unavailable and can only be received explicitly, e.g. from bonus SPELL + // nonMagical - this spell is not affected by Sorcery or magic resistance. School resistances apply. + "flags" : { + "positive": true, + }, + + // If true, spell won't be available on a map without water + "onlyOnWaterMap" : true, - "flags" : {"flag1": true, "flag2": true}, - - //DEPRECATED | optional| no default | flags structure of bonus names,any one of these bonus grants immunity. Negatable by the Orb. + //TODO: DEPRECATED | optional| no default | flags structure of bonus names,any one of these bonus grants immunity. Negatable by the Orb. "immunity": {"BONUS_NAME":true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names + //TODO: DEPRECATED | optional| no default | flags structure of bonus names //any one of these bonus grants immunity, cant be negated "absoluteImmunity": {"BONUS_NAME": true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Negatable by the Orb. + //TODO: DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Negatable by the Orb. "limit": {"BONUS_NAME": true, ...}, - //DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Cant be negated + //TODO: DEPRECATED | optional| no default | flags structure of bonus names, presence of all bonuses required to be affected by. Cant be negated "absoluteLimit": {"BONUS_NAME": true, ...}, - //[WIP] optional | default no limit no immunity + //TODO: optional | default no limit no immunity // "targetCondition" { //at least one required to be affected @@ -86,44 +102,34 @@ } } - - //graphics; mandatory; object; "graphics": { - // ! will be moved to bonus type config in next bonus config version - // iconImmune - OPTIONAL; string; - //resource path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES) + // resource path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES) "iconImmune":"ZVS/LIB1.RES/E_SPMET", - - // iconScenarioBonus- mandatory, string, image resource path - //resource path of icon for scenario bonus + // resource path of icon for scenario bonus "iconScenarioBonus": "MYSPELL_B", - // iconEffect- mandatory, string, image resource path - //resource path of icon for spell effects during battle + // resource path of icon for spell effects during battle "iconEffect": "MYSPELL_E", - // iconBook- mandatory, string, image resource path - //resource path of icon for spellbook + // resource path of icon for spellbook "iconBook": "MYSPELL_E", - // iconScroll- mandatory, string, image resource path - //resource path of icon for spell scrolls + // resource path of icon for spell scrolls "iconScroll": "MYSPELL_E" }, - //OPTIONAL; object; TODO "sounds": { - //OPTIONAL; resourse path, casting sound + //Resourse path of cast sound "cast":"LIGHTBLT" }, - //Mandatory structure - //configuration for no skill, basic, adv, expert + // Mandatory structure + // configuration for no skill, basic, adv, expert "levels":{ "base": {Spell level base format}, "none": {Spell level format}, @@ -138,8 +144,9 @@ # Animation format -``` javascript +TODO +``` javascript { "projectile": [ {"minimumAngle": 0 ,"defName":"C20SPX4"}, @@ -148,6 +155,7 @@ {"minimumAngle": 1.20 ,"defName":"C20SPX1"}, {"minimumAngle": 1.50 ,"defName":"C20SPX0"} ], + "cast" : [] "hit":["C20SPX"], "affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"] } @@ -162,28 +170,25 @@ Json object with data common for all levels can be put here. These configuration This will make spell affect single target on all levels except expert, where it is massive spell. ``` javascript - "base":{ - - "range": 0 + "range": 0 }, "expert":{ -"range": "X" + "range": "X" } ``` # Spell level format +TODO + ``` javascript { - //Mandatory, localizable description - //Use {xxx} for formatting + //Mandatory, localizable description. Use {xxx} for formatting "description": "", - - //Mandatory, number, - //cost in mana points + //Mandatory, cost in mana points "cost": 1, //Mandatory, number @@ -194,14 +199,13 @@ This will make spell affect single target on all levels except expert, where it //Mandatory, flags structure //TODO // modifiers make sense for creature target - // - // "targetModifier": { "smart": false, //true: friendly/hostile based on positiveness; false: all targets "clearTarget": false, "clearAffected": false, - } + }, + //Mandatory //spell range description in SRSL // range "X" + smart modifier = enchanter casting, expert massive spells @@ -246,6 +250,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## Special effect common format +TODO + ``` javascript "mod:effectId":{ @@ -265,6 +271,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## catapult +TODO + ``` javascript "mod:effectId":{ @@ -282,6 +290,8 @@ Configurable spells ignore *offensive* flag, *effects* and *cumulativeEffects*. ## Clone +TODO + Configurable version of Clone spell. ``` javascript @@ -296,6 +306,8 @@ Configurable version of Clone spell. ## Damage effect +TODO + If effect is automatic, spell behave like offensive spell (uses power, levelPower etc) ``` javascript @@ -313,34 +325,36 @@ If effect is automatic, spell behave like offensive spell (uses power, levelPowe ## Dispel -documetation +TODO ## Heal -documetation +TODO ## Obstacle -documetation +TODO ## Remove obstacle -documetation +TODO ## Sacrifice -documetation +TODO ## Summon -documetation +TODO ## Teleport -documetation +TODO ## Timed +TODO + If effect is automatic, spell behave like \[de\]buff spell (effect and cumulativeEffects ignored) @@ -362,6 +376,8 @@ cumulativeEffects ignored) ## Targets, ranges, modifiers +TODO + - CREATURE target (only battle spells) - range 0: smart assumed single creature target - range "X" + smart modifier = enchanter casting, expert massive spells diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 7e5478212..f80749aeb 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -734,7 +734,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & { if(counteredSpell.second.Bool()) { - VLC->identifiers()->requestIdentifier(counteredSpell.second.meta, counteredSpell.first, [=](si32 id) + VLC->identifiers()->requestIdentifier(counteredSpell.second.meta, "spell", counteredSpell.first, [=](si32 id) { spell->counteredSpells.emplace_back(id); }); From 43246477d4fde2ad080e7a9f60766a2531e2029d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 23 Sep 2023 19:24:20 +0300 Subject: [PATCH 0603/1248] Update bonuses docs --- docs/modders/Bonus_Format.md | 70 +++++++++++++++----------------- docs/modders/Building_Bonuses.md | 2 + lib/bonuses/Bonus.cpp | 2 - 3 files changed, 34 insertions(+), 40 deletions(-) diff --git a/docs/modders/Bonus_Format.md b/docs/modders/Bonus_Format.md index bd51188a7..0075bbdb9 100644 --- a/docs/modders/Bonus_Format.md +++ b/docs/modders/Bonus_Format.md @@ -6,29 +6,59 @@ All parameters but type are optional. ``` javascript { + // Type of the bonus. See Bonus Types for full list "type": "BONUS_TYPE", + + // Subtype of the bonus. Function depends on bonus type. "subtype": 0, + + // Value of the bonus. Function depends on bonus type. "val" : 0, + + // Describes how this bonus is accumulated with other bonuses of the same type "valueType": "VALUE_TYPE", + + // Additional info that bonus might need. Function depends on bonus type. "addInfo" : 0, // or [1, 2, ...] + // How long this bonus should be active until removal. + // May use multiple triggers, in which case first event will remove this bonus "duration" : "BONUS_DURATION", //or ["BONUS_DURATION1", "BONUS_DURATION2", ...]" + + // How long this bonus should remain, in days or battle turns (depending on bonus duration) "turns" : 0, + // TODO "targetSourceType" : "SOURCE_TYPE", + + // TODO "sourceType" : "SOURCE_TYPE", + + // TODO "sourceID" : 0, + + // TODO "effectRange" : "EFFECT_RANGE", + // TODO "limiters" : [ "PREDEFINED_LIMITER", optional_parameters (...), //whhich one is preferred? {"type" : LIMITER_TYPE, "parameters" : [1,2,3]} ], + // TODO "propagator" : ["PROPAGATOR_TYPE", optional_parameters (...)], + + // TODO "updater" : {Bonus Updater}, + + // TODO "propagationUpdater" : {Bonus Updater, but works during propagation}, + + // TODO "description" : "", + + // TODO "stacking" : "" } ``` @@ -47,44 +77,8 @@ All parameters but type are optional. ## Subtype resolution All string identifiers of items can be used in "subtype" field. This allows cross-referencing between the mods and make config file more readable. - -### Available prefixes - -- creature. -- artifact. -- skill: -``` javascript - "pathfinding", "archery", "logistics", "scouting", "diplomacy", - "navigation", "leadership", "wisdom", "mysticism", "luck", - "ballistics", "eagleEye", "necromancy", "estates", "fireMagic", - "airMagic", "waterMagic", "earthMagic", "scholar", "tactics", - "artillery", "learning", "offence", "armorer", "intelligence", - "sorcery", "resistance", "firstAid" -``` - -- resource: -``` javascript - "wood", "mercury", "ore", "sulfur", "crystal", "gems", "gold", "mithril" -``` - -- hero. -- faction. -- spell. -- primarySkill -``` javascript - "attack", "defence", "spellpower", "knowledge" -``` - -- terrain: -``` javascript - "dirt", "sand", "grass", "snow", "swamp", "rough", "subterra", "lava", "water", "rock" -``` - -- spellSchool -```javascript -"any", "fire", "air", "water", "earth" -``` - +See [Game Identifiers](Game_Identifiers.md) for full list of available identifiers + ### Example ``` javascript diff --git a/docs/modders/Building_Bonuses.md b/docs/modders/Building_Bonuses.md index 59804a915..b52aac1ee 100644 --- a/docs/modders/Building_Bonuses.md +++ b/docs/modders/Building_Bonuses.md @@ -3,6 +3,8 @@ Work-in-progress page do describe all bonuses provided by town buildings for future configuration. +TODO: This page is outdated and may not represent VCMI 1.3 state + ## unique buildings Hardcoded functionalities, selectable but not configurable. In future diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index ff81c18eb..6ae6bacfc 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -177,8 +177,6 @@ JsonNode Bonus::toJsonNode() const root["subtype"] = subtypeToJson(type, subtype); if(additionalInfo != CAddInfo::NONE) root["addInfo"] = additionalInfoToJson(type, additionalInfo); - if(turnsRemain != 0) - root["turns"].Integer() = turnsRemain; if(source != BonusSource::OTHER) root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); if(targetSourceType != BonusSource::OTHER) From 8210a2cafd891756ce92897276facab82854912f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 23 Sep 2023 19:26:15 +0300 Subject: [PATCH 0604/1248] Added list of all identifiers from base game --- config/schemas/spell.json | 63 ++- docs/modders/Game_Identifiers.md | 778 ++++++++++++++++++++++++++++++ docs/modders/Translations.md | 8 + lib/modding/IdentifierStorage.cpp | 41 +- lib/modding/IdentifierStorage.h | 3 + 5 files changed, 855 insertions(+), 38 deletions(-) create mode 100644 docs/modders/Game_Identifiers.md diff --git a/config/schemas/spell.json b/config/schemas/spell.json index 659a26150..cd2b55843 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -190,38 +190,37 @@ "description" : "Various flags", "additionalProperties" : false, "properties" : { - "indifferent" : { - "type" : "boolean", - "description" : "Spell is indifferent for target" - }, - "negative" : { - "type" : "boolean", - "description" : "Spell is negative for target" - }, - "positive" : { - "type" : "boolean", - "description" : "Spell is positive for target" - }, - "damage" : { - "type" : "boolean", - "description" : "Spell does damage (direct or indirect)" - }, - "offensive" : { - "type" : "boolean", - "description" : "Spell does direct damage (implicitly sets damage and negative)" - }, - "rising" : { - "type" : "boolean", - "description" : "Rising spell (implicitly sets positive)" - }, - "special" : { - "type" : "boolean", - "description" : "Special spell. Can be given only by BonusType::SPELL" - }, - "nonMagical" : { + "indifferent" : { "type" : "boolean", - "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." - } + "description" : "Spell is indifferent for target" + }, + "negative" : { + "type" : "boolean", + "description" : "Spell is negative for target" + }, + "positive" : { + "type" : "boolean", + "description" : "Spell is positive for target" + }, + "damage" : { + "type" : "boolean", + "description" : "Spell does damage (direct or indirect)" + }, + "offensive" : { + "type" : "boolean", + "description" : "Spell does direct damage (implicitly sets damage and negative)" + }, + "rising" : { + "type" : "boolean", + "description" : "Rising spell (implicitly sets positive)" + }, + "special" : { + "type" : "boolean", + "description" : "Special spell. Can be given only by BonusType::SPELL" + }, + "nonMagical" : { + "type" : "boolean", + "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." } } }, @@ -315,5 +314,5 @@ "type" : "boolean", "description" : "If true, spell won't be available on a map without water" } - }, + } } diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md new file mode 100644 index 000000000..be3ae9865 --- /dev/null +++ b/docs/modders/Game_Identifiers.md @@ -0,0 +1,778 @@ +< [Documentation](../Readme.md) / [Modding](Readme.md) / Game Identifiers + +## List of all game identifiers + +This is a list of all game identifiers available to modders. Note that only identifiers from base game have been included. For identifiers from mods please look up corresponding mod + +### artifact + +- artifact.admiralsHat +- artifact.ambassadorsSash +- artifact.ammoCart +- artifact.amuletOfTheUndertaker +- artifact.angelFeatherArrows +- artifact.angelWings +- artifact.angelicAlliance +- artifact.armageddonsBlade +- artifact.armorOfTheDamned +- artifact.armorOfWonder +- artifact.armsOfLegion +- artifact.badgeOfCourage +- artifact.ballista +- artifact.birdOfPerception +- artifact.blackshardOfTheDeadKnight +- artifact.bootsOfLevitation +- artifact.bootsOfPolarity +- artifact.bootsOfSpeed +- artifact.bowOfElvenCherrywood +- artifact.bowOfTheSharpshooter +- artifact.bowstringOfTheUnicornsMane +- artifact.breastplateOfBrimstone +- artifact.breastplateOfPetrifiedWood +- artifact.bucklerOfTheGnollKing +- artifact.capeOfConjuring +- artifact.capeOfVelocity +- artifact.cardsOfProphecy +- artifact.catapult +- artifact.celestialNecklaceOfBliss +- artifact.centaurAxe +- artifact.charmOfMana +- artifact.cloakOfTheUndeadKing +- artifact.cloverOfFortune +- artifact.collarOfConjuring +- artifact.cornucopia +- artifact.crestOfValor +- artifact.crownOfDragontooth +- artifact.crownOfTheSupremeMagi +- artifact.deadMansBoots +- artifact.diplomatsRing +- artifact.dragonScaleArmor +- artifact.dragonScaleShield +- artifact.dragonWingTabard +- artifact.dragonboneGreaves +- artifact.elixirOfLife +- artifact.emblemOfCognizance +- artifact.endlessBagOfGold +- artifact.endlessPurseOfGold +- artifact.endlessSackOfGold +- artifact.equestriansGloves +- artifact.everflowingCrystalCloak +- artifact.everpouringVialOfMercury +- artifact.eversmokingRingOfSulfur +- artifact.firstAidTent +- artifact.garnitureOfInterference +- artifact.glyphOfGallantry +- artifact.goldenBow +- artifact.grail +- artifact.greaterGnollsFlail +- artifact.headOfLegion +- artifact.hellstormHelmet +- artifact.helmOfChaos +- artifact.helmOfHeavenlyEnlightenment +- artifact.helmOfTheAlabasterUnicorn +- artifact.hourglassOfTheEvilHour +- artifact.inexhaustibleCartOfLumber +- artifact.inexhaustibleCartOfOre +- artifact.ladybirdOfLuck +- artifact.legsOfLegion +- artifact.lionsShieldOfCourage +- artifact.loinsOfLegion +- artifact.mysticOrbOfMana +- artifact.necklaceOfDragonteeth +- artifact.necklaceOfOceanGuidance +- artifact.necklaceOfSwiftness +- artifact.ogresClubOfHavoc +- artifact.orbOfDrivingRain +- artifact.orbOfInhibition +- artifact.orbOfSilt +- artifact.orbOfTempestuousFire +- artifact.orbOfTheFirmament +- artifact.orbOfVulnerability +- artifact.pendantOfCourage +- artifact.pendantOfDeath +- artifact.pendantOfDispassion +- artifact.pendantOfFreeWill +- artifact.pendantOfHoliness +- artifact.pendantOfLife +- artifact.pendantOfNegativity +- artifact.pendantOfSecondSight +- artifact.pendantOfTotalRecall +- artifact.powerOfTheDragonFather +- artifact.quietEyeOfTheDragon +- artifact.recantersCloak +- artifact.redDragonFlameTongue +- artifact.ribCage +- artifact.ringOfConjuring +- artifact.ringOfInfiniteGems +- artifact.ringOfLife +- artifact.ringOfTheMagi +- artifact.ringOfTheWayfarer +- artifact.ringOfVitality +- artifact.sandalsOfTheSaint +- artifact.scalesOfTheGreaterBasilisk +- artifact.seaCaptainsHat +- artifact.sentinelsShield +- artifact.shacklesOfWar +- artifact.shieldOfTheDamned +- artifact.shieldOfTheDwarvenLords +- artifact.shieldOfTheYawningDead +- artifact.skullHelmet +- artifact.speculum +- artifact.spellBook +- artifact.spellScroll +- artifact.spellbindersHat +- artifact.sphereOfPermanence +- artifact.spiritOfOppression +- artifact.spyglass +- artifact.statesmansMedal +- artifact.statueOfLegion +- artifact.stillEyeOfTheDragon +- artifact.stoicWatchman +- artifact.surcoatOfCounterpoise +- artifact.swordOfHellfire +- artifact.swordOfJudgement +- artifact.talismanOfMana +- artifact.targOfTheRampagingOgre +- artifact.thunderHelmet +- artifact.titansCuirass +- artifact.titansGladius +- artifact.titansThunder +- artifact.tomeOfAirMagic +- artifact.tomeOfEarthMagic +- artifact.tomeOfFireMagic +- artifact.tomeOfWaterMagic +- artifact.torsoOfLegion +- artifact.tunicOfTheCyclopsKing +- artifact.unusedArtifact1 +- artifact.unusedArtifact2 +- artifact.unusedArtifact3 +- artifact.vampiresCowl +- artifact.vialOfDragonBlood +- artifact.vialOfLifeblood +- artifact.wizardsWell + +### battlefield + +- battlefield.clover_field +- battlefield.cursed_ground +- battlefield.dirt_birches +- battlefield.dirt_hills +- battlefield.dirt_pines +- battlefield.evil_fog +- battlefield.fiery_fields +- battlefield.grass_hills +- battlefield.grass_pines +- battlefield.holy_ground +- battlefield.lava +- battlefield.lucid_pools +- battlefield.magic_clouds +- battlefield.magic_plains +- battlefield.rocklands +- battlefield.rough +- battlefield.sand_mesas +- battlefield.sand_shore +- battlefield.ship +- battlefield.ship_to_ship +- battlefield.snow_mountains +- battlefield.snow_trees +- battlefield.subterranean +- battlefield.swamp_trees + +### creature + +- creature.airElemental +- creature.airElementals +- creature.ammoCart +- creature.ancientBehemoth +- creature.angel +- creature.apprenticeGremlin +- creature.archDevil +- creature.archMage +- creature.archangel +- creature.archer +- creature.arrowTower +- creature.azureDragon +- creature.ballista +- creature.basilisk +- creature.battleDwarf +- creature.behemoth +- creature.beholder +- creature.blackDragon +- creature.blackKnight +- creature.boar +- creature.boneDragon +- creature.catapult +- creature.cavalier +- creature.centaur +- creature.centaurCaptain +- creature.cerberus +- creature.champion +- creature.chaosHydra +- creature.crusader +- creature.crystalDragon +- creature.cyclop +- creature.cyclopKing +- creature.demon +- creature.dendroidGuard +- creature.dendroidSoldier +- creature.devil +- creature.diamondGolem +- creature.dragonFly +- creature.dreadKnight +- creature.dwarf +- creature.earthElemental +- creature.efreet +- creature.efreetSultan +- creature.enchanter +- creature.enchanters +- creature.energyElemental +- creature.evilEye +- creature.fairieDragon +- creature.familiar +- creature.fireDragonFly +- creature.fireElemental +- creature.firebird +- creature.firstAidTent +- creature.genie +- creature.ghostDragon +- creature.giant +- creature.gnoll +- creature.gnollMarauder +- creature.goblin +- creature.goblinWolfRider +- creature.goblins +- creature.gog +- creature.goldDragon +- creature.goldGolem +- creature.gorgon +- creature.grandElf +- creature.greaterBasilisk +- creature.greenDragon +- creature.gremlin +- creature.griffin +- creature.halberdier +- creature.halfling +- creature.harpy +- creature.harpyHag +- creature.hellHound +- creature.hobgoblin +- creature.hobgoblinWolfRider +- creature.hornedDemon +- creature.hydra +- creature.iceElemental +- creature.imp +- creature.infernalTroglodyte +- creature.ironGolem +- creature.lich +- creature.lightCrossbowman +- creature.lizardWarrior +- creature.lizardman +- creature.mage +- creature.magicElemental +- creature.magmaElemental +- creature.magog +- creature.manticore +- creature.marksman +- creature.masterGenie +- creature.masterGremlin +- creature.medusa +- creature.medusaQueen +- creature.mightyGorgon +- creature.minotaur +- creature.minotaurKing +- creature.monk +- creature.mummy +- creature.naga +- creature.nagaQueen +- creature.nomad +- creature.obsidianGargoyle +- creature.ogre +- creature.ogreMage +- creature.orc +- creature.orcChieftain +- creature.peasant +- creature.pegasus +- creature.phoenix +- creature.pikeman +- creature.pitFiend +- creature.pitLord +- creature.pixie +- creature.pixies +- creature.powerLich +- creature.primitiveLizardman +- creature.psychicElemental +- creature.redDragon +- creature.roc +- creature.rogue +- creature.royalGriffin +- creature.rustDragon +- creature.scorpicore +- creature.serpentFly +- creature.sharpshooter +- creature.sharpshooters +- creature.silverPegasus +- creature.skeleton +- creature.skeletonWarrior +- creature.sprite +- creature.stoneGargoyle +- creature.stoneGolem +- creature.stormElemental +- creature.swordsman +- creature.thunderbird +- creature.titan +- creature.troglodyte +- creature.troll +- creature.unicorn +- creature.unused122 +- creature.unused124 +- creature.unused126 +- creature.unused128 +- creature.vampire +- creature.vampireLord +- creature.walkingDead +- creature.warUnicorn +- creature.waterElemental +- creature.waterElementals +- creature.wight +- creature.woodElf +- creature.wraith +- creature.wyvern +- creature.wyvernMonarch +- creature.zealot +- creature.zombie +- creature.zombieLord + +### faction + +- faction.castle +- faction.conflux +- faction.dungeon +- faction.fortress +- faction.inferno +- faction.necropolis +- faction.neutral +- faction.rampart +- faction.stronghold +- faction.tower + +### hero + +- hero.adela +- hero.adelaide +- hero.adrienne +- hero.aenain +- hero.aeris +- hero.aine +- hero.aislinn +- hero.ajit +- hero.alagar +- hero.alamar +- hero.alkin +- hero.andra +- hero.arlach +- hero.ash +- hero.astral +- hero.axsis +- hero.ayden +- hero.boragus +- hero.brissa +- hero.broghild +- hero.bron +- hero.caitlin +- hero.calh +- hero.calid +- hero.catherine +- hero.charna +- hero.christian +- hero.ciele +- hero.clancy +- hero.clavius +- hero.coronius +- hero.cragHack +- hero.cuthbert +- hero.cyra +- hero.dace +- hero.damacon +- hero.daremyth +- hero.darkstorn +- hero.deemer +- hero.dessa +- hero.dracon +- hero.drakon +- hero.edric +- hero.elleshar +- hero.erdamon +- hero.fafner +- hero.fiona +- hero.fiur +- hero.galthran +- hero.gelare +- hero.gelu +- hero.gem +- hero.geon +- hero.gerwulf +- hero.gird +- hero.gretchin +- hero.grindan +- hero.gundula +- hero.gunnar +- hero.gurnisson +- hero.halon +- hero.ignatius +- hero.ignissa +- hero.ingham +- hero.inteus +- hero.iona +- hero.isra +- hero.ivor +- hero.jabarkas +- hero.jaegar +- hero.jeddite +- hero.jenova +- hero.josephine +- hero.kalt +- hero.kilgor +- hero.korbac +- hero.krellion +- hero.kyrre +- hero.labetha +- hero.lacus +- hero.lordHaart +- hero.lorelei +- hero.loynis +- hero.luna +- hero.malcom +- hero.malekith +- hero.marius +- hero.melodia +- hero.mephala +- hero.merist +- hero.mirlanda +- hero.moandor +- hero.monere +- hero.mutare +- hero.mutareDrake +- hero.nagash +- hero.neela +- hero.nimbus +- hero.nymus +- hero.octavia +- hero.olema +- hero.oris +- hero.orrin +- hero.pasis +- hero.piquedram +- hero.pyre +- hero.rashka +- hero.rion +- hero.rissa +- hero.roland +- hero.rosic +- hero.ryland +- hero.sandro +- hero.sanya +- hero.saurug +- hero.sephinroth +- hero.septienna +- hero.serena +- hero.shakti +- hero.shiva +- hero.sirMullich +- hero.solmyr +- hero.sorsha +- hero.straker +- hero.styg +- hero.sylvia +- hero.synca +- hero.tamika +- hero.tazar +- hero.terek +- hero.thane +- hero.thant +- hero.theodorus +- hero.thorgrim +- hero.thunar +- hero.tiva +- hero.torosar +- hero.tyraxor +- hero.tyris +- hero.ufretin +- hero.uland +- hero.undeadHaart +- hero.valeska +- hero.verdish +- hero.vey +- hero.vidomina +- hero.vokial +- hero.voy +- hero.wystan +- hero.xarfax +- hero.xeron +- hero.xsi +- hero.xyron +- hero.yog +- hero.zubin +- hero.zydar + +### heroClass + +- heroClass.alchemist +- heroClass.barbarian +- heroClass.battlemage +- heroClass.beastmaster +- heroClass.cleric +- heroClass.deathknight +- heroClass.demoniac +- heroClass.druid +- heroClass.elementalist +- heroClass.heretic +- heroClass.knight +- heroClass.necromancer +- heroClass.overlord +- heroClass.planeswalker +- heroClass.ranger +- heroClass.warlock +- heroClass.witch +- heroClass.wizard + +### playerColor + +- playerColor.blue +- playerColor.green +- playerColor.orange +- playerColor.pink +- playerColor.purple +- playerColor.red +- playerColor.tan +- playerColor.teal + +### primSkill + +Deprected, please use primarySkill instead + +- primSkill.attack +- primSkill.defence +- primSkill.knowledge +- primSkill.spellpower + +### primarySkill + +- primarySkill.attack +- primarySkill.defence +- primarySkill.knowledge +- primarySkill.spellpower + +### resource + +- resource.crystal +- resource.gems +- resource.gold +- resource.mercury +- resource.mithril +- resource.ore +- resource.sulfur +- resource.wood + +### river + +- river.iceRiver +- river.lavaRiver +- river.mudRiver +- river.waterRiver + +### road + +- road.cobblestoneRoad +- road.dirtRoad +- road.gravelRoad + +### secondarySkill + +- secondarySkill.airMagic +- secondarySkill.archery +- secondarySkill.armorer +- secondarySkill.artillery +- secondarySkill.ballistics +- secondarySkill.diplomacy +- secondarySkill.eagleEye +- secondarySkill.earthMagic +- secondarySkill.estates +- secondarySkill.fireMagic +- secondarySkill.firstAid +- secondarySkill.intelligence +- secondarySkill.leadership +- secondarySkill.learning +- secondarySkill.logistics +- secondarySkill.luck +- secondarySkill.mysticism +- secondarySkill.navigation +- secondarySkill.necromancy +- secondarySkill.offence +- secondarySkill.pathfinding +- secondarySkill.resistance +- secondarySkill.scholar +- secondarySkill.scouting +- secondarySkill.sorcery +- secondarySkill.tactics +- secondarySkill.waterMagic +- secondarySkill.wisdom + +### skill + +Deprected, please use secondarySkill instead + +- skill.airMagic +- skill.archery +- skill.armorer +- skill.artillery +- skill.ballistics +- skill.diplomacy +- skill.eagleEye +- skill.earthMagic +- skill.estates +- skill.fireMagic +- skill.firstAid +- skill.intelligence +- skill.leadership +- skill.learning +- skill.logistics +- skill.luck +- skill.mysticism +- skill.navigation +- skill.necromancy +- skill.offence +- skill.pathfinding +- skill.resistance +- skill.scholar +- skill.scouting +- skill.sorcery +- skill.tactics +- skill.waterMagic +- skill.wisdom + +### spell + +- spell.acidBreath +- spell.acidBreathDamage +- spell.age +- spell.airElemental +- spell.airShield +- spell.animateDead +- spell.antiMagic +- spell.armageddon +- spell.berserk +- spell.bind +- spell.bless +- spell.blind +- spell.bloodlust +- spell.castleMoat +- spell.castleMoatTrigger +- spell.catapultShot +- spell.chainLightning +- spell.clone +- spell.counterstrike +- spell.cure +- spell.curse +- spell.cyclopsShot +- spell.deathCloud +- spell.deathRipple +- spell.deathStare +- spell.destroyUndead +- spell.dimensionDoor +- spell.disease +- spell.disguise +- spell.dispel +- spell.dispelHelpful +- spell.disruptingRay +- spell.dungeonMoat +- spell.dungeonMoatTrigger +- spell.earthElemental +- spell.earthquake +- spell.fireElemental +- spell.fireShield +- spell.fireWall +- spell.fireWallTrigger +- spell.fireball +- spell.firstAid +- spell.fly +- spell.forceField +- spell.forgetfulness +- spell.fortressMoat +- spell.fortressMoatTrigger +- spell.fortune +- spell.frenzy +- spell.frostRing +- spell.haste +- spell.hypnotize +- spell.iceBolt +- spell.implosion +- spell.inferno +- spell.infernoMoat +- spell.infernoMoatTrigger +- spell.landMine +- spell.landMineTrigger +- spell.lightningBolt +- spell.magicArrow +- spell.magicMirror +- spell.meteorShower +- spell.mirth +- spell.misfortune +- spell.necropolisMoat +- spell.necropolisMoatTrigger +- spell.paralyze +- spell.poison +- spell.prayer +- spell.precision +- spell.protectAir +- spell.protectEarth +- spell.protectFire +- spell.protectWater +- spell.quicksand +- spell.rampartMoat +- spell.rampartMoatTrigger +- spell.removeObstacle +- spell.resurrection +- spell.sacrifice +- spell.scuttleBoat +- spell.shield +- spell.slayer +- spell.slow +- spell.sorrow +- spell.stoneGaze +- spell.stoneSkin +- spell.strongholdMoat +- spell.strongholdMoatTrigger +- spell.summonBoat +- spell.summonDemons +- spell.teleport +- spell.thunderbolt +- spell.titanBolt +- spell.towerMoat +- spell.townPortal +- spell.viewAir +- spell.viewEarth +- spell.visions +- spell.waterElemental +- spell.waterWalk +- spell.weakness + +### spellSchool + +- spellSchool.air +- spellSchool.any +- spellSchool.earth +- spellSchool.fire +- spellSchool.water + +### terrain + +- terrain.dirt +- terrain.grass +- terrain.lava +- terrain.rock +- terrain.rough +- terrain.sand +- terrain.snow +- terrain.subterra +- terrain.swamp +- terrain.water diff --git a/docs/modders/Translations.md b/docs/modders/Translations.md index a4533d864..23b378797 100644 --- a/docs/modders/Translations.md +++ b/docs/modders/Translations.md @@ -48,6 +48,7 @@ VCMI contains several new strings, to cover functionality not existing in Heroes - In-game texts, most noticeably - in-game settings menu. - Game Launcher - Map Editor +- Android Launcher Before you start, make sure that you have copy of VCMI source code. If you are not familiar with git, you can use Github Desktop to clone VCMI repository. @@ -72,6 +73,13 @@ VCMI Launcher and Map Editor use translation system provided by Qt framework so Translation of Map Editor is identical, except for location of translation files. Open `/editor/translation/` instead to translate Map Editor +TODO: how to test translation locally + +### Translation of Android Launcher + +TODO +see https://github.com/vcmi/vcmi/blob/develop/android/vcmi-app/src/main/res/values/strings.xml + ### Submitting changes Once you have finished with translation you need to submit these changes to vcmi team using git or Github Desktop diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 0cf1b9f22..a797ebb78 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -355,16 +355,45 @@ void CIdentifierStorage::finalize() errorsFound = true; } + debugDumpIdentifiers(); + if (errorsFound) - { - for(const auto & object : registeredObjects) - { - logMod->trace("%s : %s -> %d", object.second.scope, object.first, object.second.id); - } logMod->error("All known identifiers were dumped into log file"); - } + assert(errorsFound == false); state = ELoadingState::FINISHED; + +} + +void CIdentifierStorage::debugDumpIdentifiers() +{ + logMod->trace("List of all registered objects:"); + + std::map> objectList; + + for(const auto & object : registeredObjects) + { + size_t categoryLength = object.first.find('.'); + assert(categoryLength != std::string::npos); + + std::string objectCategory = object.first.substr(0, categoryLength); + std::string objectName = object.first.substr(categoryLength + 1); + + objectList[objectCategory].push_back("[" + object.second.scope + "] " + objectName); + } + + for(auto & category : objectList) + boost::range::sort(category.second); + + for(const auto & category : objectList) + { + logMod->trace(""); + logMod->trace("### %s", category.first); + logMod->trace(""); + + for(const auto & entry : category.second) + logMod->trace("- " + entry); + } } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 562b1d064..004d724c7 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -65,6 +65,9 @@ class DLL_LINKAGE CIdentifierStorage ELoadingState state = ELoadingState::LOADING; + /// Helper method that dumps all registered identifier into log file + void debugDumpIdentifiers(); + /// Check if identifier can be valid (camelCase, point as separator) static void checkIdentifier(std::string & ID); From 0ab766479d992fef141d58e19f8ed205ea220be0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 26 Sep 2023 13:22:36 +0300 Subject: [PATCH 0605/1248] One more documentation update iteration --- config/commanders.json | 2 +- config/schemas/battlefield.json | 8 ++--- config/schemas/hero.json | 7 +++-- config/schemas/heroClass.json | 7 ++++- config/schemas/obstacle.json | 4 +-- docs/modders/Animation_Format.md | 27 ++++++++++++++++ docs/modders/Bonus/Bonus_Updaters.md | 2 ++ .../Entities_Format/Battle_Obstacle_Format.md | 26 +++++++++++++++- .../Entities_Format/Battlefield_Format.md | 18 ++++++++++- .../Entities_Format/Hero_Class_Format.md | 4 +-- .../Entities_Format/Hero_Type_Format.md | 7 +++-- docs/modders/Map_Objects/Boat.md | 31 ++++++++++++++++++- .../CommonConstructors.cpp | 3 ++ 13 files changed, 128 insertions(+), 18 deletions(-) diff --git a/config/commanders.json b/config/commanders.json index a87159e08..f789003a8 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -29,7 +29,7 @@ {"ability": ["BLOCKS_RETALIATION", 0, 1, 0 ], "skills": [0,5]}, {"ability": ["UNLIMITED_RETALIATIONS", 0, 0, 0 ], "skills": [1, 2]}, {"ability": ["ATTACKS_ALL_ADJACENT", 0, 0, 0 ], "skills": [1, 3]}, - {"ability": ["BLOCK", 30, 0, 0 ], "skills": [1, 4]}, + {"ability": ["NONE", 30, 0, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn {"ability": ["FIRE_SHIELD", 1, 1, 0 ], "skills": [1, 5]}, {"ability": ["ADDITIONAL_ATTACK", 1, 0, 0 ], "skills": [2, 3]}, {"ability": ["HP_REGENERATION", 50, 0, 0 ], "skills": [2, 4]}, diff --git a/config/schemas/battlefield.json b/config/schemas/battlefield.json index 4fa7cf1f5..3135c2fb4 100644 --- a/config/schemas/battlefield.json +++ b/config/schemas/battlefield.json @@ -8,7 +8,7 @@ "properties" : { "name" : { "type" : "string", - "description" : "Name of the battleground" + "description" : "Human-readable name of the battlefield" }, "isSpecial" : { "type" : "boolean", @@ -16,17 +16,17 @@ }, "bonuses": { "type":"array", - "description": "Bonuses provided by this battleground using bonus system", + "description": "List of bonuses that will affect all battles on this battlefield", "items": { "$ref" : "bonus.json" } }, "graphics" : { "type" : "string", "format" : "imageFile", - "description" : "BMP battleground resource" + "description" : "Background image for this battlefield" }, "impassableHexes" : { "type" : "array", - "description" : "Battle hexes always impassable for this type of battlefield (ship to ship for instance)", + "description" : "List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles)", "items" : { "type" : "number" } diff --git a/config/schemas/hero.json b/config/schemas/hero.json index 3b434ae07..419836386 100644 --- a/config/schemas/hero.json +++ b/config/schemas/hero.json @@ -114,16 +114,17 @@ "properties" : { "base" : { "type" : "object", - "description" : "Will be merged with all bonuses." + "additionalProperties" : true, + "description" : "Section that will be added into every bonus instance, for use in specialties with multiple similar bonuses." }, "bonuses" : { "type" : "object", - "description" : "Set of bonuses", + "description" : "List of bonuses added by this specialty. See bonus format for more details", "additionalProperties" : { "$ref" : "bonus.json" } }, "creature" : { "type" : "string", - "description" : "Name of base creature to grant standard specialty to." + "description" : "Shortcut for defining creature specialty, using standard H3 rules." } } }, diff --git a/config/schemas/heroClass.json b/config/schemas/heroClass.json index c090a1816..9efdddaca 100644 --- a/config/schemas/heroClass.json +++ b/config/schemas/heroClass.json @@ -54,10 +54,15 @@ } }, "mapObject" : { + // TODO: this entry should be merged with corresponding base entry in hero object type and validated as objectType + // "$ref" : "objectType.json", + "type" : "object", "properties" : { "filters" : { "type" : "object", - "additionalProperties" : { "type" : "array" } + "additionalProperties" : { + "type" : "array" + } } } }, diff --git a/config/schemas/obstacle.json b/config/schemas/obstacle.json index 780f224f9..bbda0e589 100644 --- a/config/schemas/obstacle.json +++ b/config/schemas/obstacle.json @@ -16,12 +16,12 @@ "properties" : { "allowedTerrains" : { "type" : "array", - "description" : "Obstacles can be place on specified terrains only", + "description" : "List of terrains on which this obstacle can be used", "items" : { "type" : "string" } }, "specialBattlefields" : { "type" : "array", - "description" : "Obstacles can be placed on specified specified battlefields", + "description" : "List of special battlefields on which this obstacle can be used", "items" : { "type" : "string" } }, "absolute" : { diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 5cd6ef760..8e3f20a44 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -53,6 +53,33 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def } ``` +# Examples + +## Replacing a button + +This json file will allow replacing .def file for a button with png images. Buttons require following images: +0. Active state. Button is active and can be pressed by player +1. Pressed state. Player pressed button but have not released it yet +2. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image +3. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off + +```javascript + "basepath" : "interface/MyButton", // all images are located in this directory + + "images" : + [ + {"frame" : 0, "file" : "active.png" }, + {"frame" : 1, "file" : "pressed.png" }, + {"frame" : 2, "file" : "blocked.png" }, + {"frame" : 3, "file" : "highlighted.png" }, + ] +} +``` + +## Replacing creature animation + +TODO + # Creature animation groups Animation for creatures consist from multiple groups, with each group diff --git a/docs/modders/Bonus/Bonus_Updaters.md b/docs/modders/Bonus/Bonus_Updaters.md index 3839d6653..372bc4472 100644 --- a/docs/modders/Bonus/Bonus_Updaters.md +++ b/docs/modders/Bonus/Bonus_Updaters.md @@ -1,5 +1,7 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / [Bonus Format](../Bonus_Format.md) / Bonus Updaters +TODO: this page may be incorrect or outdated + # List of Bonus Updaters Updaters come in two forms: simple and complex. Simple updaters take no diff --git a/docs/modders/Entities_Format/Battle_Obstacle_Format.md b/docs/modders/Entities_Format/Battle_Obstacle_Format.md index e0d64eeb4..8454539d1 100644 --- a/docs/modders/Entities_Format/Battle_Obstacle_Format.md +++ b/docs/modders/Entities_Format/Battle_Obstacle_Format.md @@ -1,3 +1,27 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Battle Obstacle Format -TODO \ No newline at end of file +```jsonc + // List of terrains on which this obstacle can be used + "allowedTerrains" : [] + + // List of special battlefields on which this obstacle can be used + "specialBattlefields" : [] + + // If set to true, this obstacle will use absolute coordinates. Only one such obstacle can appear on the battlefield + "absolute" : false + + // Width of an obstacle, in hexes + "width" : 1 + + // Height of an obstacle, in hexes + "height" : 1 + + // List of tiles blocked by an obstacles. For non-absolute obstacles uses relative hex indices + "blockedTiles" : [ 0, 20, 50 ] + + // For absolute obstacle - image with static obstacle. For non-absolute - animation with an obstacle + "animation" : "", + + // If set to true, obstacle will appear in front of units or other battlefield objects + "foreground" : false +``` \ No newline at end of file diff --git a/docs/modders/Entities_Format/Battlefield_Format.md b/docs/modders/Entities_Format/Battlefield_Format.md index bdbc098c5..616b262a9 100644 --- a/docs/modders/Entities_Format/Battlefield_Format.md +++ b/docs/modders/Entities_Format/Battlefield_Format.md @@ -1,3 +1,19 @@ < [Documentation](../../Readme.md) / [Modding](../Readme.md) / Entities Format / Battlefield Format -TODO \ No newline at end of file +```jsonc + // Human-readable name of the battlefield + "name" : "" + + // If set to true, obstacles will be taken from "specialBattlefields" property of an obstacle + // If set to false, obstacles will be taken from "allowedTerrains" instead + "isSpecial" : false + + // List of bonuses that will affect all battles on this battlefield + "bonuses" : { BONUS_FORMAT } + + // Background image for this battlefield + "graphics" : "" + + // List of battle hexes that will be always blocked on this battlefield (e.g. ship to ship battles) + "impassableHexes" : [ 10, 20, 50 ] +``` \ No newline at end of file diff --git a/docs/modders/Entities_Format/Hero_Class_Format.md b/docs/modders/Entities_Format/Hero_Class_Format.md index 4601cb423..a67b1c45f 100644 --- a/docs/modders/Entities_Format/Hero_Class_Format.md +++ b/docs/modders/Entities_Format/Hero_Class_Format.md @@ -26,14 +26,14 @@ In order to make functional hero class you also need: } }, - // Description of map object representing this hero class. See map template format for details + // Description of map object representing this hero class. "mapObject" : { // Optional, hero ID-base filter, using same rules as building requirements "filters" : { "mutare" : [ "anyOf", [ "mutare" ], [ "mutareDrake" ]] }, - // List of templates used for this object, normally - only one is needed + // List of templates used for this object, normally - only one is needed. See map template format for details "templates" : { "normal" : { "animation" : "AH00_.def" } } diff --git a/docs/modders/Entities_Format/Hero_Type_Format.md b/docs/modders/Entities_Format/Hero_Type_Format.md index def2a12cb..646c35611 100644 --- a/docs/modders/Entities_Format/Hero_Type_Format.md +++ b/docs/modders/Entities_Format/Hero_Type_Format.md @@ -119,14 +119,17 @@ In order to make functional hero you also need: // Description of specialty mechanics using bonuses (with updaters) "specialty" : { - // to be merged with all bonuses, use for specialties with multiple similar bonuses (optional) + + // Optional. Section that will be added into every bonus instance, for use in specialties with multiple similar bonuses "base" : {common bonus properties}, + + // List of bonuses added by this specialty. See bonus format for more details "bonuses" : { // use updaters for bonuses that grow with level "someBonus" : {Bonus Format}, "anotherOne" : {Bonus Format} }, - // adds creature specialty following the HMM3 default formula + // Optional. Shortcut for defining creature specialty, using standard H3 rules "creature" : "griffin" } } diff --git a/docs/modders/Map_Objects/Boat.md b/docs/modders/Map_Objects/Boat.md index 30404ce4c..92b5057ef 100644 --- a/docs/modders/Map_Objects/Boat.md +++ b/docs/modders/Map_Objects/Boat.md @@ -1 +1,30 @@ -TODO \ No newline at end of file +< [Documentation](../../Readme.md) / [Modding](../Readme.md) / [Map Object Format](../Map_Object_Format.md) / Boat + +``` javascript +{ + // Layer on which this boat moves. Possible values: + // "land" - same rules as movement of hero on land + // "sail" - object can move on water and interact with objects on water + // "water" - object can walk on water but can not interact with objects + // "air" - object can fly across any terrain but can not interact with objects + "layer" : "", + + // If set to true, it is possible to attack wandering monsters from boat + "onboardAssaultAllowed" : true; + + // If set to true, it is possible to visit object (e.g. pick up resources) from boat + "onboardVisitAllowed" : true; + + // Path to file that contains animated boat movement + "actualAnimation" : "", + + // Path to file that contains animated overlay animation, such as waves around boat + "overlayAnimation" : "", + + // Path to 8 files that contain animated flag of the boat. 8 files, one per each player + "flagAnimations" : ["", "" ], + + // List of bonuses that will be granted to hero located in the boat + "bonuses" : { BONUS_FORMAT } +} +``` \ No newline at end of file diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index eab678f1d..7cea90465 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -180,6 +180,9 @@ void BoatInstanceConstructor::initTypeData(const JsonNode & input) int pos = vstd::find_pos(NPathfindingLayer::names, input["layer"].String()); if(pos != -1) layer = EPathfindingLayer(pos); + else + logMod->error("Unknown layer %s found in boat!", input["layer"].String()); + onboardAssaultAllowed = input["onboardAssaultAllowed"].Bool(); onboardVisitAllowed = input["onboardVisitAllowed"].Bool(); actualAnimation = AnimationPath::fromJson(input["actualAnimation"]); From 67a98d086fe3309f5e7ee505d34838fe5f3e0ade Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 26 Sep 2023 16:00:33 +0300 Subject: [PATCH 0606/1248] Clarified animations format --- docs/modders/Animation_Format.md | 34 +++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/docs/modders/Animation_Format.md b/docs/modders/Animation_Format.md index 8e3f20a44..1f1ff2205 100644 --- a/docs/modders/Animation_Format.md +++ b/docs/modders/Animation_Format.md @@ -34,7 +34,7 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def ... ], - // Allow overriding individual frames in file + // Alternative to "sequences". Allow overriding individual frames in file. Generally should not be used in the same time as "sequences" "images" : [ { @@ -58,10 +58,10 @@ VCMI allows overriding HoMM3 .def files with .json replacement. Compared to .def ## Replacing a button This json file will allow replacing .def file for a button with png images. Buttons require following images: -0. Active state. Button is active and can be pressed by player -1. Pressed state. Player pressed button but have not released it yet -2. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image -3. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off +1. Active state. Button is active and can be pressed by player +2. Pressed state. Player pressed button but have not released it yet +3. Blocked state. Button is blocked and can not be interacted with. Note that some buttons are never blocked and can be used without this image +4. Highlighted state. This state is used by only some buttons and only in some cases. For example, in main menu buttons will appear highlighted when mouse cursor is on top of the image. Another example is buttons that can be selected, such as settings that can be toggled on or off ```javascript "basepath" : "interface/MyButton", // all images are located in this directory @@ -76,6 +76,30 @@ This json file will allow replacing .def file for a button with png images. Butt } ``` +## Replacing simple animation + +This json file allows defining one animation sequence, for example for adventure map objects or for town buildings. + +```javascript + "basepath" : "myTown/myBuilding", // all images are located in this directory + + "sequences" : + [ + { + "group" : 0, + "frames" : [ + "frame01.png", + "frame02.png", + "frame03.png", + "frame04.png", + "frame05.png" + ... + ] + } + ] +} +``` + ## Replacing creature animation TODO From d40eb03d5b01068422bbf84eb0c4303f7eecf85f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:44:35 +0200 Subject: [PATCH 0607/1248] description --- Mods/vcmi/config/vcmi/english.json | 2 +- Mods/vcmi/config/vcmi/german.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index bbe29be80..1ce9f4b5f 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -91,7 +91,7 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Enhancements", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a larger spell book, backpack, etc. Disable to have a more classic experience.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 067594e7d..47ca4d50b 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -90,7 +90,7 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", From 056cba55a890f40b48bd6dc513482a10869ba433 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 26 Sep 2023 15:45:46 +0200 Subject: [PATCH 0608/1248] Show loading screen immediately on restart --- client/CServerHandler.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 3a0943f01..1bd5c58b7 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -537,6 +537,8 @@ void CServerHandler::sendGuiAction(ui8 action) const void CServerHandler::sendRestartGame() const { + GH.windows().createAndPushWindow(); + LobbyEndGame endGame; endGame.closeConnection = false; endGame.restart = true; From 93e2826e3ea8387503f5dc30badfa63e67659b29 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:53:44 +0200 Subject: [PATCH 0609/1248] new constructor --- client/widgets/TextControls.cpp | 7 +++++++ client/widgets/TextControls.h | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 61bd1c16e..aed145d6a 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -454,6 +454,13 @@ CGStatusBar::CGStatusBar(std::shared_ptr background_, EFonts Font, E autoRedraw = false; } +CGStatusBar::CGStatusBar(int x, int y) + : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) + , enteringText(false) +{ + addUsedEvents(LCLICK); +} + CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) , enteringText(false) diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 36b94cdca..888ae95da 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -125,7 +125,8 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this background_, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::CENTER, const ColorRGBA & Color = Colors::WHITE); - CGStatusBar(int x, int y, const ImagePath & name = ImagePath::builtin(""), int maxw = -1); + CGStatusBar(int x, int y, const ImagePath & name, int maxw = -1); + CGStatusBar(int x, int y); //make CLabel API private using CLabel::getText; From ac5f552219ef85166493dc63bde14baf7cb2d29d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 15:56:39 +0200 Subject: [PATCH 0610/1248] fix --- client/widgets/TextControls.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index aed145d6a..d534f4002 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -458,6 +458,7 @@ CGStatusBar::CGStatusBar(int x, int y) : CLabel(x, y, FONT_SMALL, ETextAlignment::CENTER) , enteringText(false) { + // without background addUsedEvents(LCLICK); } @@ -469,8 +470,6 @@ CGStatusBar::CGStatusBar(int x, int y, const ImagePath & name, int maxw) OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - if(name.empty()) // without background - return; auto backgroundImage = std::make_shared(name); background = backgroundImage; pos = background->pos; From 42030ed10fa42f7966f70294f9698a26b1660ca5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 26 Sep 2023 17:28:50 +0300 Subject: [PATCH 0611/1248] Fix hotseat --- client/CPlayerInterface.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 23e2240e0..3be85d404 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -167,16 +167,6 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: CCS->musich->loadTerrainMusicThemes(); initializeHeroTownList(); - - // always recreate advmap interface to avoid possible memory-corruption bugs - adventureInt.reset(new AdventureMapInterface()); - - if(GH.windows().findWindows().empty()) - { - // after map load - remove all active windows and replace them with adventure map - GH.windows().clear(); - GH.windows().pushWindow(adventureInt); - } } void CPlayerInterface::playerEndsTurn(PlayerColor player) @@ -213,6 +203,16 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player) void CPlayerInterface::playerStartsTurn(PlayerColor player) { + if(GH.windows().findWindows().empty()) + { + // after map load - remove all active windows and replace them with adventure map + // always recreate advmap interface to avoid possible memory-corruption bugs + adventureInt.reset(new AdventureMapInterface()); + + GH.windows().clear(); + GH.windows().pushWindow(adventureInt); + } + EVENT_HANDLER_CALLED_BY_CLIENT; if (player != playerID && LOCPLINT == this) { From 73ea52c615b356cbf92d7883a7ea29b36be049b3 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 23 Sep 2023 23:55:29 +0200 Subject: [PATCH 0612/1248] Enable VSync to prevent screen tearing while scrolling across map Fixes #2935 --- client/renderSDL/ScreenHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index a513454b8..f4b261b31 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -275,7 +275,7 @@ void ScreenHandler::initializeWindow() } //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible - mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), 0); + mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), SDL_RENDERER_PRESENTVSYNC); if(mainRenderer == nullptr) throw std::runtime_error("Unable to create renderer\n"); @@ -570,4 +570,4 @@ bool ScreenHandler::hasFocus() { ui32 flags = SDL_GetWindowFlags(mainWindow); return flags & SDL_WINDOW_INPUT_FOCUS; -} \ No newline at end of file +} From 23d1638d702a1444363d72f70c0a65163c7eac9b Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 16:06:01 +0200 Subject: [PATCH 0613/1248] Add VSync to settings --- client/renderSDL/ScreenHandler.cpp | 10 +- config/schemas/settings.json | 7 +- launcher/settingsView/csettingsview_moc.cpp | 7 + launcher/settingsView/csettingsview_moc.h | 2 + launcher/settingsView/csettingsview_moc.ui | 961 ++++++++++---------- 5 files changed, 508 insertions(+), 479 deletions(-) diff --git a/client/renderSDL/ScreenHandler.cpp b/client/renderSDL/ScreenHandler.cpp index f4b261b31..1371c51c7 100644 --- a/client/renderSDL/ScreenHandler.cpp +++ b/client/renderSDL/ScreenHandler.cpp @@ -274,8 +274,14 @@ void ScreenHandler::initializeWindow() handleFatalError(message, true); } - //create first available renderer if preferred not set. Use no flags, so HW accelerated will be preferred but SW renderer also will possible - mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), SDL_RENDERER_PRESENTVSYNC); + // create first available renderer if no preferred one is set + // use no SDL_RENDERER_SOFTWARE or SDL_RENDERER_ACCELERATED flag, so HW accelerated will be preferred but SW renderer will also be possible + uint32_t rendererFlags = 0; + if(settings["video"]["vsync"].Bool()) + { + rendererFlags |= SDL_RENDERER_PRESENTVSYNC; + } + mainRenderer = SDL_CreateRenderer(mainWindow, getPreferredRenderingDriver(), rendererFlags); if(mainRenderer == nullptr) throw std::runtime_error("Unable to create renderer\n"); diff --git a/config/schemas/settings.json b/config/schemas/settings.json index ee7c69213..d0dedb7fe 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -149,7 +149,8 @@ "driver", "displayIndex", "showfps", - "targetfps" + "targetfps", + "vsync" ], "properties" : { "resolution" : { @@ -207,6 +208,10 @@ "targetfps" : { "type" : "number", "default" : 60 + }, + "vsync" : { + "type" : "boolean", + "default" : true } } }, diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 9f00a369f..9eea8ee6d 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -83,6 +83,7 @@ void CSettingsView::loadSettings() ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float()); + ui->checkBoxVSync->setChecked(settings["video"]["vsync"].Bool()); ui->spinBoxReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100)); ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String())); @@ -494,6 +495,12 @@ void CSettingsView::on_spinBoxFramerateLimit_valueChanged(int arg1) node->Float() = arg1; } +void CSettingsView::on_checkBoxVSync_stateChanged(int arg1) +{ + Settings node = settings.write["video"]["vsync"]; + node->Bool() = arg1; +} + void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1) { Settings node = settings.write["server"]["playerAI"]; diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 5cc14c505..466901389 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -62,6 +62,8 @@ private slots: void on_spinBoxFramerateLimit_valueChanged(int arg1); + void on_checkBoxVSync_stateChanged(int arg1); + void on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1); void on_comboBoxAlliedPlayerAI_currentTextChanged(const QString &arg1); diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 186689a6c..deae0833c 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -42,7 +42,6 @@ - 75 true @@ -107,20 +106,208 @@ 0 - -197 + -356 610 - 768 + 873 - - + + + + Heroes III Translation + + + + + + + + true + + + + General + + + + + + + Fullscreen + + + + + + + Interface Scaling + + + + + + + Autosave prefix + + + + + + + Adventure Map Allies + + + + + + + + true + + + + Video + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + + true + + + + Artificial Intelligence + + + + + + + empty = map name prefix + + + + + + + true + + + + + + true + + + + + + + + + Autosave limit (0 = off) + + + + + + + + + + Additional repository + + + + + + + % + + + 0 + + + 25 + + + 1 + + + 0 + + + + + + + Show intro + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Enemy AI in battles + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + false @@ -140,470 +327,13 @@ - - - - Display index - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - Autosave limit (0 = off) - - - - - - - Fullscreen - - - - - - - Network port - - - - - - - - 75 - true - - - - General - - - - - - - Autosave - - - - - - - - Hardware - - - - - Software - - - - - - - - Show intro - - - - - - - - - - true - - - - - - - true - - - - - - true - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - - - - Framerate Limit - - - - - - - - - - Adventure Map Enemies - - - - - - - Default repository - - - - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - - 75 - true - - - - Video - - - - - - - Autosave prefix - - - - - - - Resolution - - - - + - - - - - - - true - - - - - - - Cursor - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - Refresh now - - - - - - - - - - - - - - Heroes III Translation - - - - - - - 1 - - - - Off - - - - - On - - - - - - - - Enemy AI in battles - - - - - - - - 75 - true - - - - Mod Repositories - - - - - - - Additional repository - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - 50 - - - 400 - - - 10 - - - - - - - Heroes III Data Language - - - - - - - - - - Neutral AI in battles - - - - - - - Interface Scaling - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - - - - - - - - Friendly AI in battles - - - - - - - VCMI Language - - - - - - - - - - Check on startup - - - - - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - Adventure Map Allies - - - - - - - - 75 - true - - - - Artificial Intelligence - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - empty = map name prefix - - - @@ -635,6 +365,266 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use + + + + + + + Display index + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + + + + + + + + Default repository + + + + + + + Heroes III Data Language + + + + + + + + + + true + + + + + + + Framerate Limit + + + + + + + Friendly AI in battles + + + + + + + VCMI Language + + + + + + + + + + true + + + + + + + + + + 50 + + + 400 + + + 10 + + + + + + + + + + + true + + + + Mod Repositories + + + + + + + + + + + + + + Resolution + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + Autosave + + + + + + + Cursor + + + + + + + + Hardware + + + + + Software + + + + + + + + + + + Network port + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + 1 + + + + Off + + + + + On + + + + + + + + Refresh now + + + + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Check on startup + + + + + + + Neutral AI in battles + + + @@ -642,22 +632,41 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - % + + + + Adventure Map Enemies - - 0 - - - 25 - - + + + + + 1 - - 0 + + + Off + + + + + On + + + + + + + + VSync + + + + + + + From bfa5ef7990f7213a4877d400c5f3f86a4483ba10 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 17:28:44 +0200 Subject: [PATCH 0614/1248] Don't sleep in FramerateManager::framerateDelay() if VSync is enabled --- client/gui/FramerateManager.cpp | 16 +++++++++++----- client/gui/FramerateManager.h | 2 ++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 59aa653e7..0e81ea8c8 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -11,22 +11,28 @@ #include "StdInc.h" #include "FramerateManager.h" +#include "../../lib/CConfigHandler.h" + FramerateManager::FramerateManager(int targetFrameRate) : targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate) , lastFrameIndex(0) , lastFrameTimes({}) - , lastTimePoint (Clock::now()) + , lastTimePoint(Clock::now()) + , vsyncEnabled(settings["video"]["vsync"].Bool()) { boost::range::fill(lastFrameTimes, targetFrameTime); } void FramerateManager::framerateDelay() { - Duration timeSpentBusy = Clock::now() - lastTimePoint; + if(!vsyncEnabled) + { + Duration timeSpentBusy = Clock::now() - lastTimePoint; - // FPS is higher than it should be, then wait some time - if(timeSpentBusy < targetFrameTime) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + // FPS is higher than it should be, then wait some time + if(timeSpentBusy < targetFrameTime) + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + } // compute actual timeElapsed taking into account actual sleep interval // limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) diff --git a/client/gui/FramerateManager.h b/client/gui/FramerateManager.h index 818e55f94..d653bd667 100644 --- a/client/gui/FramerateManager.h +++ b/client/gui/FramerateManager.h @@ -25,6 +25,8 @@ class FramerateManager /// index of last measured frome in lastFrameTimes array ui32 lastFrameIndex; + bool vsyncEnabled; + public: FramerateManager(int targetFramerate); From e33127a1f74aa1e05b8991593a6d58241ec76bea Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 17:38:41 +0200 Subject: [PATCH 0615/1248] Launcher setting: Disable spinBoxFramerateLimit if VSync is enabled --- launcher/settingsView/csettingsview_moc.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 9eea8ee6d..5ac6648c2 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -83,6 +83,7 @@ void CSettingsView::loadSettings() ui->spinBoxInterfaceScaling->setValue(settings["video"]["resolution"]["scaling"].Float()); ui->spinBoxFramerateLimit->setValue(settings["video"]["targetfps"].Float()); + ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); ui->checkBoxVSync->setChecked(settings["video"]["vsync"].Bool()); ui->spinBoxReservedArea->setValue(std::round(settings["video"]["reservedWidth"].Float() * 100)); @@ -499,6 +500,7 @@ void CSettingsView::on_checkBoxVSync_stateChanged(int arg1) { Settings node = settings.write["video"]["vsync"]; node->Bool() = arg1; + ui->spinBoxFramerateLimit->setDisabled(settings["video"]["vsync"].Bool()); } void CSettingsView::on_comboBoxEnemyPlayerAI_currentTextChanged(const QString &arg1) From 012f4b91b2e207de48a6e444c47af41056aeb868 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:43:47 +0200 Subject: [PATCH 0616/1248] Update generalOptionsTab.json --- config/widgets/settings/generalOptionsTab.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index cd808eafe..22dc40fcb 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -7,7 +7,7 @@ "name": "lineLabelsEnd", "type": "texture", "image": "settingsWindow/lineHorizontal", - "rect": { "x" : 5, "y" : 289, "w": 365, "h": 3} + "rect": { "x" : 5, "y" : 319, "w": 365, "h": 3} }, { "type" : "labelTitle", @@ -176,7 +176,7 @@ { "type" : "verticalLayout", "customType" : "labelDescription", - "position": {"x": 45, "y": 325}, + "position": {"x": 45, "y": 355}, "items" : [ { "text": "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover", From bb358148d8fb9a409144455be067b09aa160b530 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 20:58:24 +0200 Subject: [PATCH 0617/1248] Update generalOptionsTab.json --- config/widgets/settings/generalOptionsTab.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index 22dc40fcb..e34131c6a 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -7,7 +7,7 @@ "name": "lineLabelsEnd", "type": "texture", "image": "settingsWindow/lineHorizontal", - "rect": { "x" : 5, "y" : 319, "w": 365, "h": 3} + "rect": { "x" : 5, "y" : 349, "w": 365, "h": 3} }, { "type" : "labelTitle", @@ -21,7 +21,7 @@ }, { "type" : "labelTitle", - "position": {"x": 10, "y": 295}, + "position": {"x": 10, "y": 355}, "text": "vcmi.systemOptions.townsGroup" }, /////////////////////////////////////// Left section - Video Settings @@ -176,7 +176,7 @@ { "type" : "verticalLayout", "customType" : "labelDescription", - "position": {"x": 45, "y": 355}, + "position": {"x": 45, "y": 385}, "items" : [ { "text": "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover", @@ -189,7 +189,7 @@ { "name": "availableCreaturesAsDwellingPicker", "type": "toggleGroup", - "position": {"x": 10, "y": 323}, + "position": {"x": 10, "y": 383}, "items": [ { @@ -211,7 +211,7 @@ "name": "compactTownCreatureInfoLabel", "type" : "verticalLayout", "customType" : "labelDescription", - "position": {"x": 45, "y": 385}, + "position": {"x": 45, "y": 445}, "items" : [ { "text": "vcmi.otherOptions.compactTownCreatureInfo.hover", @@ -222,7 +222,7 @@ "name": "compactTownCreatureInfoCheckbox", "type": "checkbox", "help": "vcmi.otherOptions.compactTownCreatureInfo", - "position": {"x": 10, "y": 383}, + "position": {"x": 10, "y": 443}, "callback": "compactTownCreatureInfoChanged" } ] From 2738dc3190d66504343e9defd242817318b27a67 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 21:12:04 +0200 Subject: [PATCH 0618/1248] better approach for redrawing --- client/mainmenu/CMainMenu.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 09d064f5d..80021dbec 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -94,7 +94,8 @@ CMenuScreen::CMenuScreen(const JsonNode & configNode) menuNameToEntry.push_back("credits"); tabs = std::make_shared(std::bind(&CMenuScreen::createTab, this, _1)); - tabs->setRedrawParent(true); + if(config["video"].isNull()) + tabs->setRedrawParent(true); } std::shared_ptr CMenuScreen::createTab(size_t index) @@ -109,10 +110,10 @@ void CMenuScreen::show(Canvas & to) { if(!config["video"].isNull()) { + // redraw order: background -> video -> buttons and pictures + background->redraw(); CCS->videoh->update((int)config["video"]["x"].Float() + pos.x, (int)config["video"]["y"].Float() + pos.y, to.getInternalSurface(), true, false); - tabs->setRedrawParent(false); tabs->redraw(); - tabs->setRedrawParent(true); } CIntObject::show(to); } From 242e0ffa4ac34d6aec80c6256ab6e30174419ba6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 26 Sep 2023 22:06:04 +0200 Subject: [PATCH 0619/1248] fix playername --- client/CPlayerInterface.cpp | 1 + client/mainmenu/CHighScoreScreen.cpp | 6 +++--- client/mainmenu/CHighScoreScreen.h | 3 ++- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index cb7cb5122..2d4110413 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1707,6 +1707,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) param.allDefeated = false; } param.scenarioName = cb->getMapHeader()->name; + param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; HighScoreCalculation highScoreCalc; highScoreCalc.parameters.push_back(param); highScoreCalc.isCampaign = false; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 64da9d598..9d39343a5 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -335,7 +335,7 @@ void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) if(!input) { - input = std::make_shared( + input = std::make_shared(calc.parameters[0].playerName, [&] (std::string text) { if(!text.empty()) @@ -355,7 +355,7 @@ void CHighScoreInputScreen::keyPressed(EShortcut key) clickPressed(Point()); } -CHighScoreInput::CHighScoreInput(std::function readyCB) +CHighScoreInput::CHighScoreInput(std::string playerName, std::function readyCB) : CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; @@ -369,7 +369,7 @@ CHighScoreInput::CHighScoreInput(std::function readyCB) buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); textInput = std::make_shared(Rect(18, 104, 200, 25), FONT_SMALL, 0); - textInput->setText(settings["general"]["playerName"].String()); + textInput->setText(playerName); } void CHighScoreInput::okay() diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 2fb31a949..89e4bdf21 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -29,6 +29,7 @@ public: bool allDefeated; std::string campaignName; std::string scenarioName; + std::string playerName; }; class HighScoreCalculation @@ -84,7 +85,7 @@ class CHighScoreInput : public CWindowObject void okay(); void abort(); public: - CHighScoreInput(std::function readyCB); + CHighScoreInput(std::string playerName, std::function readyCB); }; class CHighScoreInputScreen : public CWindowObject From e2eeec96a9f9a07e2d5a0a74328c076c08753bc9 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 22:53:45 +0200 Subject: [PATCH 0620/1248] Fix stuttering animations when using high frame rate limit or vsync --- client/gui/FramerateManager.cpp | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 0e81ea8c8..5291c3920 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -12,27 +12,36 @@ #include "FramerateManager.h" #include "../../lib/CConfigHandler.h" +#include FramerateManager::FramerateManager(int targetFrameRate) - : targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate) - , lastFrameIndex(0) + : lastFrameIndex(0) , lastFrameTimes({}) , lastTimePoint(Clock::now()) , vsyncEnabled(settings["video"]["vsync"].Bool()) { + if(vsyncEnabled) + { + static int display_in_use = 0; + SDL_DisplayMode mode; + SDL_GetCurrentDisplayMode(display_in_use, &mode); + int displayRefreshRate = mode.refresh_rate; + logGlobal->info("Display refresh rate is %d", displayRefreshRate); + targetFrameTime = Duration(boost::chrono::seconds(1)) / displayRefreshRate; + } else + { + targetFrameTime = Duration(boost::chrono::seconds(1)) / targetFrameRate; + } boost::range::fill(lastFrameTimes, targetFrameTime); } void FramerateManager::framerateDelay() { - if(!vsyncEnabled) - { - Duration timeSpentBusy = Clock::now() - lastTimePoint; + Duration timeSpentBusy = Clock::now() - lastTimePoint; - // FPS is higher than it should be, then wait some time - if(timeSpentBusy < targetFrameTime) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); - } + // FPS is higher than it should be, then wait some time + if(timeSpentBusy < targetFrameTime) + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); // compute actual timeElapsed taking into account actual sleep interval // limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) From ecc7648ef878728014d608bc36a969aacb6a9d1d Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 23:05:48 +0200 Subject: [PATCH 0621/1248] Use display index from settings --- client/gui/FramerateManager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 5291c3920..8dc4a075f 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -22,7 +22,7 @@ FramerateManager::FramerateManager(int targetFrameRate) { if(vsyncEnabled) { - static int display_in_use = 0; + static int display_in_use = settings["video"]["displayIndex"].Integer(); SDL_DisplayMode mode; SDL_GetCurrentDisplayMode(display_in_use, &mode); int displayRefreshRate = mode.refresh_rate; From 0d11c5197dfd63b8b691b54e5a916718cd6510b6 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Tue, 26 Sep 2023 23:48:53 +0200 Subject: [PATCH 0622/1248] Remove superfluous vsyncEnabled attribute --- client/gui/FramerateManager.cpp | 3 +-- client/gui/FramerateManager.h | 2 -- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 8dc4a075f..a23db55c4 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -18,9 +18,8 @@ FramerateManager::FramerateManager(int targetFrameRate) : lastFrameIndex(0) , lastFrameTimes({}) , lastTimePoint(Clock::now()) - , vsyncEnabled(settings["video"]["vsync"].Bool()) { - if(vsyncEnabled) + if(settings["video"]["vsync"].Bool()) { static int display_in_use = settings["video"]["displayIndex"].Integer(); SDL_DisplayMode mode; diff --git a/client/gui/FramerateManager.h b/client/gui/FramerateManager.h index d653bd667..818e55f94 100644 --- a/client/gui/FramerateManager.h +++ b/client/gui/FramerateManager.h @@ -25,8 +25,6 @@ class FramerateManager /// index of last measured frome in lastFrameTimes array ui32 lastFrameIndex; - bool vsyncEnabled; - public: FramerateManager(int targetFramerate); From f874d27797d7a2961e87692f2cd0b4d656e727c6 Mon Sep 17 00:00:00 2001 From: Maxim Korotkov Date: Tue, 26 Sep 2023 15:48:21 +0300 Subject: [PATCH 0623/1248] AI: removed redundant checks The pointers "up" and "down" were checked against nullptr but it's always non-null Also added curly braces for if statement. It's good practice for avoiding shooting yourself to the foot Signed-off-by: Maxim Korotkov --- AI/Nullkiller/AIGateway.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 1cd731f43..74f101883 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -702,8 +702,8 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); NET_EVENT_HANDLER; - std::string s1 = up ? up->nodeName() : "NONE"; - std::string s2 = down ? down->nodeName() : "NONE"; + std::string s1 = up->nodeName(); + std::string s2 = down->nodeName(); status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); @@ -711,7 +711,9 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan requestActionASAP([=]() { if(removableUnits && up->tempOwner == down->tempOwner) + { pickBestCreatures(down, up); + } answerQuery(queryID, 0); }); From 56061d116eefe9174be3f291d4a41c045afda3c6 Mon Sep 17 00:00:00 2001 From: Maxim Korotkov Date: Tue, 26 Sep 2023 16:11:40 +0300 Subject: [PATCH 0624/1248] vcai: Combined cases with the same actions --- AI/VCAI/Goals/CollectRes.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index f5ec1d873..27e168fa6 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -60,14 +60,11 @@ TGoalVec CollectRes::getAllPossibleSubgoals() return false; } break; - case Obj::WATER_WHEEL: - if (resID != GameResID(EGameResID::GOLD)) - return false; - break; case Obj::MYSTICAL_GARDEN: if ((resID != GameResID(EGameResID::GOLD)) && (resID != GameResID(EGameResID::GEMS))) return false; break; + case Obj::WATER_WHEEL: case Obj::LEAN_TO: case Obj::WAGON: if (resID != GameResID(EGameResID::GOLD)) From 59acf639da870879cfc298680627c8557bb9d9ad Mon Sep 17 00:00:00 2001 From: Maxim Korotkov Date: Tue, 26 Sep 2023 16:39:16 +0300 Subject: [PATCH 0625/1248] client: render: fixed copy-past errror in genFromJson() Signed-off-by: Maxim Korotkov --- client/render/ColorFilter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index 327813f64..3f13db8a2 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -138,7 +138,7 @@ ColorFilter ColorFilter::genFromJson(const JsonNode & entry) r.a = entry["red"].Vector()[3].Float(); } - if (!entry["red"].isNull()) + if (!entry["green"].isNull()) { g.r = entry["green"].Vector()[0].Float(); g.g = entry["green"].Vector()[1].Float(); @@ -146,7 +146,7 @@ ColorFilter ColorFilter::genFromJson(const JsonNode & entry) g.a = entry["green"].Vector()[3].Float(); } - if (!entry["red"].isNull()) + if (!entry["blue"].isNull()) { b.r = entry["blue"].Vector()[0].Float(); b.g = entry["blue"].Vector()[1].Float(); From ade7a035f5367bae5e298ca7e77db819106e3857 Mon Sep 17 00:00:00 2001 From: Maxim Korotkov Date: Tue, 26 Sep 2023 17:04:43 +0300 Subject: [PATCH 0626/1248] artifactUtils: reduced cyclomatic complexity in check Signed-off-by: Maxim Korotkov --- lib/ArtifactUtils.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index eeb94c2c6..7654a8b29 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -87,10 +87,9 @@ DLL_LINKAGE bool ArtifactUtils::checkSpellbookIsNeeded(const CGHeroInstance * he // Titan's Thunder creates new spellbook on equip if(artID == ArtifactID::TITANS_THUNDER && slot == ArtifactPosition::RIGHT_HAND) { - if(heroPtr) + if(heroPtr && !heroPtr->hasSpellbook()) { - if(heroPtr && !heroPtr->hasSpellbook()) - return true; + return true; } } return false; From 764fbebb356cd7eb33148eaf58c315b96ccd592d Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Wed, 27 Sep 2023 15:12:50 +0300 Subject: [PATCH 0627/1248] make script executable --- client/icons/generate_icns.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 client/icons/generate_icns.py diff --git a/client/icons/generate_icns.py b/client/icons/generate_icns.py old mode 100644 new mode 100755 From 0db567da37a6ef1a2f6fe15070de5328d752b50b Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Wed, 27 Sep 2023 15:25:41 +0300 Subject: [PATCH 0628/1248] improve layout of some dev docs --- docs/developers/Building_Android.md | 28 ++++---- docs/developers/Building_iOS.md | 30 ++++---- docs/developers/Building_macOS.md | 103 +++++++++++++--------------- 3 files changed, 81 insertions(+), 80 deletions(-) diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index cbd6a14f7..6800dc7e2 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -10,17 +10,17 @@ The following instructions apply to **v1.2 and later**. For earlier versions the 2. JDK 11, not necessarily from Oracle 3. Android command line tools or Android Studio for your OS: https://developer.android.com/studio/ 4. Android NDK version **r25c (25.2.9519653)**, there're multiple ways to obtain it: -- - install with Android Studio -- - install with `sdkmanager` command line tool -- - download from https://developer.android.com/ndk/downloads -- - download with Conan, see [#NDK and Conan](#ndk-and-conan) -5. (optional) Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases + - install with Android Studio + - install with `sdkmanager` command line tool + - download from https://developer.android.com/ndk/downloads + - download with Conan, see [#NDK and Conan](#ndk-and-conan) +5. (optional) Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases ## Obtaining source code Clone https://github.com/vcmi/vcmi with submodules. Example for command line: -```sh +``` git clone --recurse-submodules https://github.com/vcmi/vcmi.git ``` @@ -30,24 +30,24 @@ We use Conan package manager to build/consume dependencies, find detailed usage branch](https://github.com/vcmi/vcmi/tree/master/docs/conan.md). On the step where you need to replace **PROFILE**, choose: -- `android-32` to build for 32-bit architecture (armeabi-v7a) -- `android-64` to build for 64-bit architecture (aarch64-v8a) +- `android-32` to build for 32-bit architecture (armeabi-v7a) +- `android-64` to build for 64-bit architecture (aarch64-v8a) ### NDK and Conan Conan must be aware of the NDK location when you execute `conan install`. There're multiple ways to achieve that as written in the [Conan docs](https://docs.conan.io/1/integrations/cross_platform/android.html): -- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. You need to create your own Conan profile that imports our Android profile and adds 2 new lines (you can of course just copy everything from our profile into yours without including) and then pass this new profile to `conan install`: +- the easiest is to download NDK from Conan (option 1 in the docs), then all the magic happens automatically. You need to create your own Conan profile that imports our Android profile and adds 2 new lines (you can of course just copy everything from our profile into yours without including) and then pass this new profile to `conan install`: -```sh +``` include(/path/to/vcmi/CI/conan/android-64) [tool_requires] android-ndk/r25c ``` -- to use an already installed NDK, you can simply pass it on the command line to `conan install`: +- to use an already installed NDK, you can simply pass it on the command line to `conan install`: -```sh +``` conan install -c tools.android:ndk_path=/path/to/ndk ... ``` @@ -59,7 +59,7 @@ Building for Android is a 2-step process. First, native C++ code is compiled to This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset. Example: -```sh +``` cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug --toolchain ... cmake --build ../build ``` @@ -83,4 +83,4 @@ cd android APK will appear in `android/vcmi-app/build/outputs/apk/debug` directory which you can then install to your device with `adb install -r /path/to/apk` (adb command is from Android command line tools). -If you wish to build and install to your device in single action, use `installDebug` instead of `assembleDebug`. \ No newline at end of file +If you wish to build and install to your device in single action, use `installDebug` instead of `assembleDebug`. diff --git a/docs/developers/Building_iOS.md b/docs/developers/Building_iOS.md index 7b06136b4..22a67d85a 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -2,22 +2,24 @@ ## Requirements -1. **macOS** -2. Xcode: -3. CMake 3.21+: `brew install --cask cmake` or get from +1. **macOS** +2. Xcode: +3. CMake 3.21+: `brew install --cask cmake` or get from ## Obtaining source code Clone with submodules. Example for command line: -`git clone --recurse-submodules https://github.com/vcmi/vcmi.git` +``` +git clone --recurse-submodules https://github.com/vcmi/vcmi.git +``` ## Obtaining dependencies There are 2 ways to get prebuilt dependencies: -- [Conan package manager](https://github.com/vcmi/vcmi/tree/develop/docs/conan.md) - recommended. Note that the link points to the cutting-edge state in `develop` branch, for the latest release check the same document in the [master branch (https://github.com/vcmi/vcmi/tree/master/docs/conan.md). -- [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) - can be used if you have Xcode 11/12 or to build for simulator / armv7 device +- [Conan package manager](https://github.com/vcmi/vcmi/tree/develop/docs/conan.md) - recommended. Note that the link points to the cutting-edge state in `develop` branch, for the latest release check the same document in the [master branch (https://github.com/vcmi/vcmi/tree/master/docs/conan.md). +- [legacy manually built libraries](https://github.com/vcmi/vcmi-ios-deps) - can be used if you have Xcode 11/12 or to build for simulator / armv7 device ## Configuring project @@ -25,15 +27,17 @@ Only Xcode generator (`-G Xcode`) is supported! As a minimum, you must pass the following variables to CMake: -- `BUNDLE_IDENTIFIER_PREFIX`: unique bundle identifier prefix, something like `com.MY-NAME` -- (if using legacy dependencies) `CMAKE_PREFIX_PATH`: path to the downloaded dependencies, e.g. `~/Downloads/vcmi-ios-depends/build/iphoneos` +- `BUNDLE_IDENTIFIER_PREFIX`: unique bundle identifier prefix, something like `com.MY-NAME` +- (if using legacy dependencies) `CMAKE_PREFIX_PATH`: path to the downloaded dependencies, e.g. `~/Downloads/vcmi-ios-depends/build/iphoneos` There're a few [CMake presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html): for device (Conan and legacy dependencies) and for simulator, named `ios-device-conan`, `ios-device` and `ios-simulator` respectively. You can also create your local "user preset" to avoid typing variables each time, see example [here](https://gist.github.com/kambala-decapitator/59438030c34b53aed7d3895aaa48b718). Open terminal and `cd` to the directory with source code. Configuration example for device with Conan: -`cmake --preset ios-device-conan \` -` -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME` +``` +cmake --preset ios-device-conan \ + -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME +``` By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. @@ -53,7 +57,9 @@ You must also install game files, see [Installation on iOS](../players/Installat ### From command line -`cmake --build `` --target vcmiclient -- -quiet` +``` +cmake --build --target vcmiclient -- -quiet +``` You can pass additional xcodebuild options after the `--`. Here `-quiet` is passed to reduce amount of output. @@ -67,4 +73,4 @@ Invoke `cpack` after building: `cpack -C Release` -This will generate file with extension **ipa** if you use CMake 3.25+and **zip** otherwise (simply change extension to ipa). \ No newline at end of file +This will generate file with extension **ipa** if you use CMake 3.25+and **zip** otherwise (simply change extension to ipa). diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index a84297852..d6557e5f2 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -2,18 +2,20 @@ # Requirements -1. C++ toolchain, either of: - - Xcode Command Line Tools (aka CLT): `sudo xcode-select --install` - - Xcode IDE: - - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) -2. CMake: `brew install --cask cmake` or get from -3. (optional) Ninja: `brew install ninja` or get from +1. C++ toolchain, either of: + - Xcode Command Line Tools (aka CLT): `sudo xcode-select --install` + - Xcode IDE: + - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) +2. CMake: `brew install --cask cmake` or get from +3. (optional) Ninja: `brew install ninja` or get from # Obtaining source code Clone with submodules. Example for command line: -`git clone --recurse-submodules https://github.com/vcmi/vcmi.git` +``` +git clone --recurse-submodules https://github.com/vcmi/vcmi.git +``` # Obtaining dependencies @@ -25,51 +27,48 @@ Please find detailed instructions in [VCMI repository](https://github.com/vcmi/v On the step where you need to replace **PROFILE**, choose: -- if you're on an Intel Mac: `macos-intel` -- if you're on an Apple Silicon Mac: `macos-arm` +- if you're on an Intel Mac: `macos-intel` +- if you're on an Apple Silicon Mac: `macos-arm` Note: if you wish to build 1.0 release in non-`Release` configuration, you should define `USE_CONAN_WITH_ALL_CONFIGS=1` environment variable when executing `conan install`. ## Homebrew -1. [Install Homebrew](https://brew.sh/) -2. Install dependencies: -`brew install boost minizip sdl2 sdl2_image sdl2_mixer sdl2_ttf tbb` -3. If you want to watch in-game videos, also install FFmpeg: -`brew install ffmpeg@4` -4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher): - - `brew install qt@5` for Qt 5 or `brew install qt` for Qt 6 - - using [Qt Online Installer](https://www.qt.io/download) - choose **Go open source** +1. [Install Homebrew](https://brew.sh/) +2. Install dependencies: `brew install boost minizip sdl2 sdl2_image sdl2_mixer sdl2_ttf tbb` +3. If you want to watch in-game videos, also install FFmpeg: `brew install ffmpeg@4` +4. Install Qt dependency in either of the ways (note that you can skip this if you're not going to build Launcher): + - `brew install qt@5` for Qt 5 or `brew install qt` for Qt 6 + - using [Qt Online Installer](https://www.qt.io/download) - choose **Go open source** # Preparing build environment This applies only to Xcode-based toolchain. If `xcrun -f clang` prints errors, then use either of the following ways: -- select an Xcode instance from Xcode application - Preferences - Locations - Command Line Tools -- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path: for example, - `sudo xcode-select -s /Library/Developer/CommandLineTools` -- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path: for example, `export DEVELOPER_DIR=/Applications/Xcode.app` +- select an Xcode instance from Xcode application - Preferences - Locations - Command Line Tools +- use `xcode-select` utility to set Xcode or Xcode Command Line Tools path: for example, `sudo xcode-select -s /Library/Developer/CommandLineTools` +- set `DEVELOPER_DIR` environment variable pointing to Xcode or Xcode Command Line Tools path: for example, `export DEVELOPER_DIR=/Applications/Xcode.app` # Configuring project for building Note that if you wish to use Qt Creator IDE, you should skip this step and configure respective variables inside the IDE. -1. In Terminal `cd` to the source code directory -2. Start assembling CMake invocation: type `cmake -S . -B BUILD_DIR` where *BUILD_DIR* can be any path, **don't press Return** -3. Decide which CMake generator you want to use: - - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'` - - Ninja (if you have installed it): pass `-G Ninja` - - Xcode IDE (if you have installed it): pass `-G Xcode` -4. If you picked Makefiles or Ninja, pick desired *build type* - either of Debug / RelWithDebInfo / Release / MinSizeRel - and pass it in `CMAKE_BUILD_TYPE` option, for example: `-D CMAKE_BUILD_TYPE=Release`. If you don't pass this option, `RelWithDebInfo` will be used. -5. If you don't want to build Launcher, pass `-D ENABLE_LAUNCHER=OFF` -6. You can also pass `-Wno-dev` if you're not interested in CMake developer warnings -7. Next step depends on the dependency manager you have picked: - - Conan: pass `-D CMAKE_TOOLCHAIN_FILE=conan-generated/conan_toolchain.cmake` where **conan-generated** must be replaced with your directory choice - - Homebrew: if you installed FFmpeg or Qt 5, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon): - - if you installed FFmpeg, insert `$(brew --prefix ffmpeg@4)` - - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)` - - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64` - - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"` +1. In Terminal `cd` to the source code directory +2. Start assembling CMake invocation: type `cmake -S . -B BUILD_DIR` where *BUILD_DIR* can be any path, **don't press Return** +3. Decide which CMake generator you want to use: + - Makefiles: no extra option needed or pass `-G 'Unix Makefiles'` + - Ninja (if you have installed it): pass `-G Ninja` + - Xcode IDE (if you have installed it): pass `-G Xcode` +4. If you picked Makefiles or Ninja, pick desired *build type* - either of Debug / RelWithDebInfo / Release / MinSizeRel - and pass it in `CMAKE_BUILD_TYPE` option, for example: `-D CMAKE_BUILD_TYPE=Release`. If you don't pass this option, `RelWithDebInfo` will be used. +5. If you don't want to build Launcher, pass `-D ENABLE_LAUNCHER=OFF` +6. You can also pass `-Wno-dev` if you're not interested in CMake developer warnings +7. Next step depends on the dependency manager you have picked: + - Conan: pass `-D CMAKE_TOOLCHAIN_FILE=conan-generated/conan_toolchain.cmake` where **conan-generated** must be replaced with your directory choice + - Homebrew: if you installed FFmpeg or Qt 5, you need to pass `-D "CMAKE_PREFIX_PATH="` variable. See below what you can insert after `=` (but **before the closing quote**), multiple values must be separated with `;` (semicolon): + - if you installed FFmpeg, insert `$(brew --prefix ffmpeg@4)` + - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)` + - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64` + - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"` 8. now press Return # Building project @@ -82,10 +81,10 @@ Open `VCMI.xcodeproj` from the build directory, select `vcmiclient` scheme and h ## From command line -`cmake --build ` +`cmake --build ` -- If using Makefiles generator, you'd want to utilize all your CPU cores by appending `-- -j$(sysctl -n hw.ncpu)` to the above -- If using Xcode generator, you can also choose which configuration to build by appending `--config ` to the above, for example: `--config Debug` +- If using Makefiles generator, you'd want to utilize all your CPU cores by appending `-- -j$(sysctl -n hw.ncpu)` to the above +- If using Xcode generator, you can also choose which configuration to build by appending `--config ` to the above, for example: `--config Debug` # Packaging project into DMG file @@ -97,27 +96,23 @@ If you use Conan, it's expected that you use **conan-generated** directory at st You can run VCMI from DMG, but it's will also work from your IDE be it Xcode or Qt Creator. -Alternatively you can run binaries directly from "bin" directory: +Alternatively you can run binaries directly from the **bin** directory: - BUILD_DIR/bin/vcmilauncher - BUILD_DIR/bin/vcmiclient - BUILD_DIR/bin/vcmiserver +- BUILD_DIR/bin/vcmilauncher +- BUILD_DIR/bin/vcmiclient +- BUILD_DIR/bin/vcmiserver -CMake include commands to copy all needed assets from source directory into "bin" on each build. They'll work when you build from Xcode too. +CMake include commands to copy all needed assets from source directory into the **bin** directory on each build. They'll work when you build from Xcode too. # Some useful debugging tips Anyone who might want to debug builds, but new to macOS could find following commands useful: -- To attach DMG file from command line use -`hdiutil attach vcmi-1.0.dmg` -- Detach volume: -`hdiutil detach /Volumes/vcmi-1.0` -- To view dependency paths -`otool -L /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` -- To display load commands such as LC_RPATH -`otool -l /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` +- To attach DMG file from command line use `hdiutil attach vcmi-1.0.dmg` +- Detach volume: `hdiutil detach /Volumes/vcmi-1.0` +- To view dependency paths: `otool -L /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` +- To display load commands such as `LC_RPATH`: `otool -l /Volumes/vcmi-1.0/VCMI.app/Contents/MacOS/vcmiclient` # Troubleshooting -In case of troubles you can always consult our CI build scripts or contact the dev team via slack \ No newline at end of file +In case of troubles you can always consult our CI build scripts or contact the dev team via slack. From f52562eeb7d0db41a7de2c6e82da2dd0512eec71 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 16:09:52 +0200 Subject: [PATCH 0629/1248] Fix freezing of hero and long enemy turns without sleeping in FramerateManager::framerateDelay() --- client/gui/CGuiHandler.cpp | 3 +-- client/gui/FramerateManager.cpp | 27 +++++++++++---------------- client/gui/FramerateManager.h | 2 ++ 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index ae4f9bdab..4bda96ae6 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -129,11 +129,10 @@ void CGuiHandler::renderFrame() CCS->curh->render(); - SDL_RenderPresent(mainRenderer); - windows().onFrameRendered(); } + SDL_RenderPresent(mainRenderer); framerate().framerateDelay(); // holds a constant FPS } diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index a23db55c4..26757f96f 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -15,22 +15,12 @@ #include FramerateManager::FramerateManager(int targetFrameRate) - : lastFrameIndex(0) + : targetFrameTime(Duration(boost::chrono::seconds(1)) / targetFrameRate) + , lastFrameIndex(0) , lastFrameTimes({}) , lastTimePoint(Clock::now()) + , vsyncEnabled(settings["video"]["vsync"].Bool()) { - if(settings["video"]["vsync"].Bool()) - { - static int display_in_use = settings["video"]["displayIndex"].Integer(); - SDL_DisplayMode mode; - SDL_GetCurrentDisplayMode(display_in_use, &mode); - int displayRefreshRate = mode.refresh_rate; - logGlobal->info("Display refresh rate is %d", displayRefreshRate); - targetFrameTime = Duration(boost::chrono::seconds(1)) / displayRefreshRate; - } else - { - targetFrameTime = Duration(boost::chrono::seconds(1)) / targetFrameRate; - } boost::range::fill(lastFrameTimes, targetFrameTime); } @@ -38,9 +28,14 @@ void FramerateManager::framerateDelay() { Duration timeSpentBusy = Clock::now() - lastTimePoint; - // FPS is higher than it should be, then wait some time - if(timeSpentBusy < targetFrameTime) - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + if(!vsyncEnabled) + { + // if FPS is higher than it should be, then wait some time + if(timeSpentBusy < targetFrameTime) + { + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); + } + } // compute actual timeElapsed taking into account actual sleep interval // limit it to 100 ms to avoid breaking animation in case of huge lag (e.g. triggered breakpoint) diff --git a/client/gui/FramerateManager.h b/client/gui/FramerateManager.h index 818e55f94..d653bd667 100644 --- a/client/gui/FramerateManager.h +++ b/client/gui/FramerateManager.h @@ -25,6 +25,8 @@ class FramerateManager /// index of last measured frome in lastFrameTimes array ui32 lastFrameIndex; + bool vsyncEnabled; + public: FramerateManager(int targetFramerate); From c3309985183ebbaf74f50c14a666316c47d58543 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 17:17:06 +0300 Subject: [PATCH 0630/1248] Fix freeze on moving through teleporters without set path --- client/HeroMovementController.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 4f332fdbe..313d8b653 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -203,15 +203,12 @@ void HeroMovementController::onTryMoveHero(const CGHeroInstance * hero, const Tr void HeroMovementController::onQueryReplyApplied() { - if(duringMovement) - { - // Server accepted our TeleportDialog query reply and moved hero - // Continue moving alongside our path, if any + waitingForQueryApplyReply = false; - assert(waitingForQueryApplyReply); - waitingForQueryApplyReply = false; + // Server accepted our TeleportDialog query reply and moved hero + // Continue moving alongside our path, if any + if(duringMovement) onMoveHeroApplied(); - } } void HeroMovementController::onMoveHeroApplied() From d6b9fa8fbd7fe9b9b16822ec74b881b1f8273563 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 18:33:52 +0300 Subject: [PATCH 0631/1248] Replaced CPlayerInterface::pim with CGuiHandler::interfaceLock - Removed CPlayerInterface::pim since this lock does not actually protects LOCPLINT but rather entire game UI state - added more logical CGuiHandler::interfaceLock - interface lock is now non-recursive and is locked only once by initial caller that want to access GUI --- client/CMT.cpp | 6 +- client/CPlayerInterface.cpp | 20 ++---- client/CPlayerInterface.h | 1 - client/CServerHandler.cpp | 4 +- client/Client.cpp | 11 +--- client/ClientCommandManager.cpp | 19 ++---- client/battle/BattleInterface.cpp | 3 +- client/eventsSDL/InputHandler.cpp | 65 ++++++++++++------- client/gui/CGuiHandler.cpp | 4 +- client/gui/CGuiHandler.h | 2 + client/mainmenu/CMainMenu.cpp | 2 - client/mapView/mapHandler.cpp | 3 +- client/windows/settings/GeneralOptionsTab.cpp | 3 - docs/players/Cheat_Codes.md | 1 - 14 files changed, 65 insertions(+), 79 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index ee4ae11cc..64b2a4403 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -473,8 +473,6 @@ static void quitApplication() vstd::clear_pointer(console);// should be removed after everything else since used by logging - boost::this_thread::sleep_for(boost::chrono::milliseconds(750));//??? - if(!settings["session"]["headless"].Bool()) GH.screenHandler().close(); @@ -486,6 +484,10 @@ static void quitApplication() } std::cout << "Ending...\n"; + + // this method is always called from event/network threads, which keep interface mutex locked + // unlock it here to avoid assertion failure on GH descruction in exit() + GH.interfaceMutex.unlock(); exit(0); } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 09bdc174f..2e571bce5 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -113,8 +113,6 @@ if (isAutoFightOn && !battleInt) \ return; -boost::recursive_mutex * CPlayerInterface::pim = new boost::recursive_mutex; - CPlayerInterface * LOCPLINT; std::shared_ptr CPlayerInterface::battleInt; @@ -534,7 +532,6 @@ void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID i void CPlayerInterface::garrisonsChanged(std::vector objs) { - boost::unique_lock un(*pim); for (auto object : objs) { auto * hero = dynamic_cast(object); @@ -754,7 +751,7 @@ void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * sta { //FIXME: we want client rendering to proceed while AI is making actions // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells - auto unlockPim = vstd::makeUnlockGuard(*pim); + auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); autofightingAI->activeStack(battleID, stack); return; } @@ -770,11 +767,7 @@ void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * sta cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); } - { - boost::unique_lock un(*pim); - battleInt->stackActivated(stack); - //Regeneration & mana drain go there - } + battleInt->stackActivated(stack); } void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) @@ -1017,8 +1010,6 @@ void CPlayerInterface::showInfoDialogAndWait(std::vector & components void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) { - boost::unique_lock un(*pim); - movementController->requestMovementAbort(); LOCPLINT->showingDialog->setn(true); CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); @@ -1116,7 +1107,6 @@ void CPlayerInterface::tileHidden(const std::unordered_set &pos) void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) { - boost::unique_lock un(*pim); GH.windows().createAndPushWindow(hero); } @@ -1349,7 +1339,7 @@ void CPlayerInterface::waitWhileDialog(bool unlockPim) return; } - auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); + auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, unlockPim); boost::unique_lock un(showingDialog->mx); while(showingDialog->data) showingDialog->cond.wait(un); @@ -1387,8 +1377,8 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) { GH.windows().totalRedraw(); { - auto unlockPim = vstd::makeUnlockGuard(*pim); IgnoreEvents ignore(*this); + auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); } } @@ -1825,7 +1815,7 @@ void CPlayerInterface::waitForAllDialogs(bool unlockPim) { while(!dialogs.empty()) { - auto unlock = vstd::makeUnlockGuardIf(*pim, unlockPim); + auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, unlockPim); boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); } waitWhileDialog(unlockPim); diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 33651892b..6048e97de 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -76,7 +76,6 @@ public: // TODO: make private //minor interfaces CondSh *showingDialog; //indicates if dialog box is displayed - static boost::recursive_mutex *pim; bool makingTurn; //if player is already making his turn CCastleInterface * castleInt; //nullptr if castle window isn't opened diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 952b6feb1..5cb875bc1 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -90,7 +90,7 @@ template class CApplyOnLobby : public CBaseForLobbyApply public: bool applyOnLobbyHandler(CServerHandler * handler, void * pack) const override { - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); @@ -296,7 +296,7 @@ void CServerHandler::applyPacksOnLobbyScreen() boost::unique_lock lock(*mx); while(!packsForLobbyScreen.empty()) { - boost::unique_lock guiLock(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); CPackForLobby * pack = packsForLobbyScreen.front(); packsForLobbyScreen.pop_front(); CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier diff --git a/client/Client.cpp b/client/Client.cpp index 48343b9e8..0dfa1b80b 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -367,7 +367,6 @@ void CClient::endGame() GH.curInt = nullptr; { - boost::unique_lock un(*CPlayerInterface::pim); logNetwork->info("Ending current game!"); removeGUI(); @@ -499,8 +498,6 @@ std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) { - boost::unique_lock un(*CPlayerInterface::pim); - playerint[color] = gameInterface; logGlobal->trace("\tInitializing the interface for player %s", color.toString()); @@ -513,8 +510,6 @@ void CClient::installNewPlayerInterface(std::shared_ptr gameInte void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) { - boost::unique_lock un(*CPlayerInterface::pim); - battleints[color] = battleInterface; if(needCallback) @@ -531,7 +526,7 @@ void CClient::handlePack(CPack * pack) CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier if(apply) { - boost::unique_lock guiLock(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); apply->applyOnClBefore(this, pack); logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); gs->apply(pack); @@ -614,7 +609,6 @@ void CClient::battleStarted(const BattleInfo * info) { if(att || def) { - boost::unique_lock un(*CPlayerInterface::pim); CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); } else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) @@ -622,7 +616,6 @@ void CClient::battleStarted(const BattleInfo * info) //TODO: This certainly need improvement auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); spectratorInt->cb->onBattleStarted(info); - boost::unique_lock un(*CPlayerInterface::pim); CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); } } @@ -653,7 +646,7 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col if(vstd::contains(battleints, color)) { // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlock = vstd::makeUnlockGuardIf(*CPlayerInterface::pim, !battleints[color]->human); + auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, !battleints[color]->human); assert(vstd::contains(battleints, color)); battleints[color]->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 068c701d0..f08c9a755 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -74,7 +74,8 @@ void ClientCommandManager::handleGoSoloCommand() { Settings session = settings.write["session"]; - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(!CSH->client) { printCommandMessage("Game is not in playing state"); @@ -120,7 +121,8 @@ void ClientCommandManager::handleControlaiCommand(std::istringstream& singleWord singleWordBuffer >> colorName; boost::to_lower(colorName); - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(!CSH->client) { printCommandMessage("Game is not in playing state"); @@ -416,14 +418,6 @@ void ClientCommandManager::handleSetCommand(std::istringstream& singleWordBuffer } } -void ClientCommandManager::handleUnlockCommand(std::istringstream& singleWordBuffer) -{ - std::string mxname; - singleWordBuffer >> mxname; - if(mxname == "pim" && LOCPLINT) - LOCPLINT->pim->unlock(); -} - void ClientCommandManager::handleCrashCommand() { int* ptr = nullptr; @@ -460,7 +454,7 @@ void ClientCommandManager::printCommandMessage(const std::string &commandMessage if(currentCallFromIngameConsole) { - boost::unique_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if(LOCPLINT && LOCPLINT->cingconsole) { LOCPLINT->cingconsole->print(commandMessage); @@ -547,9 +541,6 @@ void ClientCommandManager::processCommand(const std::string & message, bool call else if (commandName == "set") handleSetCommand(singleWordBuffer); - else if(commandName == "unlock") - handleUnlockCommand(singleWordBuffer); - else if(commandName == "crash") handleCrashCommand(); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 484a68be9..53b164184 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -106,7 +106,6 @@ void BattleInterface::playIntroSoundAndUnlockInterface() { auto onIntroPlayed = [this]() { - boost::unique_lock un(*CPlayerInterface::pim); if(LOCPLINT->battleInt) onIntroSoundPlayed(); }; @@ -781,7 +780,7 @@ void BattleInterface::onAnimationsFinished() void BattleInterface::waitForAnimations() { { - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); + auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); ongoingAnimationsState.waitUntil(false); } diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 93524e142..23aa2ea25 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -113,32 +113,41 @@ bool InputHandler::ignoreEventsUntilInput() void InputHandler::preprocessEvent(const SDL_Event & ev) { - if((ev.type==SDL_QUIT) ||(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT))) + if(ev.type == SDL_QUIT) { -#ifdef VCMI_ANDROID + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); handleQuit(false); -#else - handleQuit(); -#endif return; } -#ifdef VCMI_ANDROID - else if (ev.type == SDL_KEYDOWN && ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK) + else if(ev.type == SDL_KEYDOWN) { - handleQuit(true); - } -#endif - else if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym==SDLK_F4) - { - boost::unique_lock lock(*CPlayerInterface::pim); - Settings full = settings.write["video"]["fullscreen"]; - full->Bool() = !full->Bool(); + if(ev.key.keysym.sym == SDLK_F4 && (ev.key.keysym.mod & KMOD_ALT)) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + handleQuit(true); + return; + } - GH.onScreenResize(); - return; + if(ev.key.keysym.scancode == SDL_SCANCODE_AC_BACK) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + handleQuit(true); + return; + } + + if(ev.type == SDL_KEYDOWN && ev.key.keysym.sym == SDLK_F4) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + Settings full = settings.write["video"]["fullscreen"]; + full->Bool() = !full->Bool(); + + GH.onScreenResize(); + return; + } } else if(ev.type == SDL_USEREVENT) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); handleUserEvent(ev.user); return; @@ -149,21 +158,27 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) case SDL_WINDOWEVENT_RESTORED: #ifndef VCMI_IOS { - boost::unique_lock lock(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); GH.onScreenResize(); } #endif break; case SDL_WINDOWEVENT_FOCUS_GAINED: - if(settings["general"]["enableUiEnhancements"].Bool()) { - CCS->musich->setVolume(settings["general"]["music"].Integer()); - CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(settings["general"]["music"].Integer()); + CCS->soundh->setVolume(settings["general"]["sound"].Integer()); + } } break; case SDL_WINDOWEVENT_FOCUS_LOST: - if(settings["general"]["enableUiEnhancements"].Bool()) { - CCS->musich->setVolume(0); - CCS->soundh->setVolume(0); + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + if(settings["general"]["enableUiEnhancements"].Bool()) { + CCS->musich->setVolume(0); + CCS->soundh->setVolume(0); + } } break; } @@ -171,6 +186,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) } else if(ev.type == SDL_SYSWMEVENT) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if(!settings["session"]["headless"].Bool() && settings["general"]["notifications"].Bool()) { NotificationHandler::handleSdlEvent(ev); @@ -180,6 +196,7 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) //preprocessing if(ev.type == SDL_MOUSEMOTION) { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if (CCS && CCS->curh) CCS->curh->cursorMove(ev.motion.x, ev.motion.y); } diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index ae4f9bdab..1a964cc7b 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -95,8 +95,6 @@ void CGuiHandler::handleEvents() void CGuiHandler::fakeMouseMove() { dispatchMainThread([](){ - assert(CPlayerInterface::pim); - boost::unique_lock lock(*CPlayerInterface::pim); GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); }); } @@ -114,7 +112,7 @@ void CGuiHandler::stopTextInput() void CGuiHandler::renderFrame() { { - boost::recursive_mutex::scoped_lock un(*CPlayerInterface::pim); + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); if(nullptr != curInt) curInt->update(); diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index e6d6aa963..20d52f74d 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -48,6 +48,8 @@ private: std::unique_ptr inputHandlerInstance; public: + boost::mutex interfaceMutex; + /// returns current position of mouse cursor, relative to vcmi window const Point & getCursorPosition() const; diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 34bda33d8..97b672df7 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -304,8 +304,6 @@ CMainMenu::CMainMenu() CMainMenu::~CMainMenu() { - boost::unique_lock lock(*CPlayerInterface::pim); - if(GH.curInt == this) GH.curInt = nullptr; } diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index 5b3b197f5..2de09bf59 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -15,6 +15,7 @@ #include "../CCallback.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/TerrainHandler.h" @@ -37,7 +38,7 @@ void CMapHandler::waitForOngoingAnimations() { while(CGI->mh->hasOngoingAnimations()) { - auto unlockPim = vstd::makeUnlockGuard(*CPlayerInterface::pim); + auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); } } diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index f32d568f8..49e490249 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -289,7 +289,6 @@ void GeneralOptionsTab::setGameResolution(int index) widget("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y)); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } @@ -314,7 +313,6 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive) updateResolutionSelector(); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } @@ -374,7 +372,6 @@ void GeneralOptionsTab::setGameScaling(int index) widget("scalingLabel")->setText(scalingToLabelString(scaling)); GH.dispatchMainThread([](){ - boost::unique_lock lock(*CPlayerInterface::pim); GH.onScreenResize(); }); } diff --git a/docs/players/Cheat_Codes.md b/docs/players/Cheat_Codes.md index c54299e5a..e92f674b6 100644 --- a/docs/players/Cheat_Codes.md +++ b/docs/players/Cheat_Codes.md @@ -131,6 +131,5 @@ Below a list of supported commands, with their arguments wrapped in `<>` `activate <0/1/2>` - activate game windows (no current use, apparently broken long ago) `redraw` - force full graphical redraw `screen` - show value of screenBuf variable, which prints "screen" when adventure map has current focus, "screen2" otherwise, and dumps values of both screen surfaces to .bmp files -`unlock pim` - unlocks specific mutex known in VCMI code as "pim" `not dialog` - set the state indicating if dialog box is active to "no" `tell hs ` - write what artifact is present on artifact slot with specified ID for hero with specified ID. (must be called during gameplay) From 0dcfd6e65cd4ef2ed9abbad6e19032c3ce5970cf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 18:44:08 +0300 Subject: [PATCH 0632/1248] Removed optional locking in waitWhileDialog method --- client/CPlayerInterface.cpp | 10 +++++----- client/CPlayerInterface.h | 4 ++-- client/widgets/CArtifactHolder.cpp | 3 ++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 2e571bce5..54ea7a27d 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1331,7 +1331,7 @@ void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const C GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb); } -void CPlayerInterface::waitWhileDialog(bool unlockPim) +void CPlayerInterface::waitWhileDialog() { if (GH.amIGuiThread()) { @@ -1339,7 +1339,7 @@ void CPlayerInterface::waitWhileDialog(bool unlockPim) return; } - auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, unlockPim); + auto unlock = vstd::makeUnlockGuard(GH.interfaceMutex); boost::unique_lock un(showingDialog->mx); while(showingDialog->data) showingDialog->cond.wait(un); @@ -1811,14 +1811,14 @@ void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) artWin->artifactDisassembled(al); } -void CPlayerInterface::waitForAllDialogs(bool unlockPim) +void CPlayerInterface::waitForAllDialogs() { while(!dialogs.empty()) { - auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, unlockPim); + auto unlock = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); } - waitWhileDialog(unlockPim); + waitWhileDialog(); } void CPlayerInterface::proposeLoadingGame() diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 6048e97de..fdcad9ff2 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -184,8 +184,8 @@ public: // public interface for use by client via LOCPLINT access void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); - void waitWhileDialog(bool unlockPim = true); - void waitForAllDialogs(bool unlockPim = true); + void waitWhileDialog(); + void waitForAllDialogs(); void openTownWindow(const CGTownInstance * town); //shows townscreen void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index b73b6b795..f4cd8e979 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -287,6 +287,7 @@ bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const Artif { auto askThread = new boost::thread([hero, art, slot, assemblyPossibilities]() -> void { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); for(const auto combinedArt : assemblyPossibilities) { bool assembleConfirmed = false; @@ -294,7 +295,7 @@ bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const Artif onYesHandlers += std::bind(&CCallback::assembleArtifacts, LOCPLINT->cb.get(), hero, slot, true, combinedArt->getId()); LOCPLINT->showArtifactAssemblyDialog(art->artType, combinedArt, onYesHandlers); - LOCPLINT->waitWhileDialog(false); + LOCPLINT->waitWhileDialog(); if(assembleConfirmed) break; } From 195320dcf2ea608b4263885fced730a64440922f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 18:49:30 +0300 Subject: [PATCH 0633/1248] Removed remaining references to 'pim' name --- client/CMT.cpp | 2 +- client/CPlayerInterface.cpp | 10 +++++----- client/Client.cpp | 14 ++++++++------ client/ClientCommandManager.h | 3 --- client/battle/BattleInterface.cpp | 2 +- client/mapView/mapHandler.cpp | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 64b2a4403..84d808dc5 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -486,7 +486,7 @@ static void quitApplication() std::cout << "Ending...\n"; // this method is always called from event/network threads, which keep interface mutex locked - // unlock it here to avoid assertion failure on GH descruction in exit() + // unlock it here to avoid assertion failure on GH destruction in exit() GH.interfaceMutex.unlock(); exit(0); } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 54ea7a27d..6d03659f7 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -104,7 +104,7 @@ #include "../lib/spells/CSpellHandler.h" // The macro below is used to mark functions that are called by client when game state changes. -// They all assume that CPlayerInterface::pim mutex is locked. +// They all assume that interface mutex is locked. #define EVENT_HANDLER_CALLED_BY_CLIENT #define BATTLE_EVENT_POSSIBLE_RETURN \ @@ -751,7 +751,7 @@ void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * sta { //FIXME: we want client rendering to proceed while AI is making actions // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells - auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); autofightingAI->activeStack(battleID, stack); return; } @@ -1339,7 +1339,7 @@ void CPlayerInterface::waitWhileDialog() return; } - auto unlock = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); boost::unique_lock un(showingDialog->mx); while(showingDialog->data) showingDialog->cond.wait(un); @@ -1378,7 +1378,7 @@ void CPlayerInterface::centerView (int3 pos, int focusTime) GH.windows().totalRedraw(); { IgnoreEvents ignore(*this); - auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); } } @@ -1815,7 +1815,7 @@ void CPlayerInterface::waitForAllDialogs() { while(!dialogs.empty()) { - auto unlock = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); } waitWhileDialog(); diff --git a/client/Client.cpp b/client/Client.cpp index 0dfa1b80b..cfc036fed 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -641,15 +641,17 @@ void CClient::battleFinished(const BattleID & battleID) void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) { - assert(vstd::contains(battleints, color)); + auto battleint = battleints.at(color); - if(vstd::contains(battleints, color)) + if (!battleint->human) { // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlock = vstd::makeUnlockGuardIf(GH.interfaceMutex, !battleints[color]->human); - - assert(vstd::contains(battleints, color)); - battleints[color]->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } + else + { + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); } } diff --git a/client/ClientCommandManager.h b/client/ClientCommandManager.h index 409ccc6ff..ecb555212 100644 --- a/client/ClientCommandManager.h +++ b/client/ClientCommandManager.h @@ -78,9 +78,6 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a // set - sets special temporary settings that reset on game quit. void handleSetCommand(std::istringstream& singleWordBuffer); - // Unlocks specific mutex known in VCMI code as "pim" - void handleUnlockCommand(std::istringstream& singleWordBuffer); - // Crashes the game forcing an exception void handleCrashCommand(); diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 53b164184..b0e383a66 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -780,7 +780,7 @@ void BattleInterface::onAnimationsFinished() void BattleInterface::waitForAnimations() { { - auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); ongoingAnimationsState.waitUntil(false); } diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index 2de09bf59..2b83b2fba 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -38,7 +38,7 @@ void CMapHandler::waitForOngoingAnimations() { while(CGI->mh->hasOngoingAnimations()) { - auto unlockPim = vstd::makeUnlockGuard(GH.interfaceMutex); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); } } From 03a939fd521ecbc38b19566463ee6c2dda3d5e81 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 19:13:26 +0300 Subject: [PATCH 0634/1248] Remove redundant thread name - this method is not thread entry point --- client/CServerHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 5cb875bc1..e486a9351 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -611,8 +611,6 @@ void CServerHandler::startMapAfterConnection(std::shared_ptr to) void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState) { - setThreadName("startGameplay"); - if(CMM) CMM->disable(); client = new CClient(); From 71ad9d670747f68c1ed05d3ca93bffa548fda82c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 21:23:54 +0300 Subject: [PATCH 0635/1248] Fix crash on starting map as non-red player --- client/CPlayerInterface.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 09bdc174f..29b23c448 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -166,8 +166,9 @@ void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std:: env = ENV; CCS->musich->loadTerrainMusicThemes(); - initializeHeroTownList(); + + adventureInt.reset(new AdventureMapInterface()); } void CPlayerInterface::playerEndsTurn(PlayerColor player) @@ -207,9 +208,6 @@ void CPlayerInterface::playerStartsTurn(PlayerColor player) if(GH.windows().findWindows().empty()) { // after map load - remove all active windows and replace them with adventure map - // always recreate advmap interface to avoid possible memory-corruption bugs - adventureInt.reset(new AdventureMapInterface()); - GH.windows().clear(); GH.windows().pushWindow(adventureInt); } From 92fdaa20b16211172ed699f1ecce2c7e93364e46 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 21:24:25 +0300 Subject: [PATCH 0636/1248] Fix crash on loading Seer Hut with kill monster mission --- lib/mapObjects/CQuest.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 377786a5a..6226bfe88 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -557,6 +557,10 @@ void CGSeerHut::setObjToKill() quest->heroName = getHeroToKill(false)->getNameTranslated(); quest->heroPortrait = getHeroToKill(false)->portrait; } + + quest->getCompletionText(configuration.onSelect); + for(auto & i : configuration.info) + quest->getCompletionText(i.message); } void CGSeerHut::init(CRandomGenerator & rand) @@ -596,10 +600,6 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->progress = CQuest::COMPLETE; quest->firstVisitText = VLC->generaltexth->seerEmpty[quest->completedOption]; } - - quest->getCompletionText(configuration.onSelect); - for(auto & i : configuration.info) - quest->getCompletionText(i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const From f4c33f49eec22c5264cfbf71712e759861795c79 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 21:24:37 +0300 Subject: [PATCH 0637/1248] Removed excessive warning --- lib/mapping/MapFormatH3M.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 9da89c494..d2b264f20 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -425,7 +425,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::BEATHERO: { if (!allowNormalVictory) - logGlobal->warn("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); + logGlobal->debug("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); allowNormalVictory = true; // H3 behavior assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); From 16424d2fc00e0bcf7603f5f7a0de1eb3c8a77fb9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 21:25:04 +0300 Subject: [PATCH 0638/1248] Fixed boat boarding --- server/CGameHandler.cpp | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 744152a9a..82c7543d7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1119,8 +1119,9 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER); const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining()); - const bool standAtObstacle = t.blocked && !t.visitable; - const bool standAtWater = !h->boat && t.terType->isWater() && (objectToVisit || !objectToVisit->isCoastVisitable()); + const bool movingOntoObstacle = t.blocked && !t.visitable; + const bool objectCoastVisitable = objectToVisit && objectToVisit->isCoastVisitable(); + const bool movingOntoWater = !h->boat && t.terType->isWater() && !objectCoastVisitable; const auto complainRet = [&](const std::string & message) { @@ -1144,11 +1145,11 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo //it's a rock or blocked and not visitable tile //OR hero is on land and dest is water and (there is not present only one object - boat) - if (!t.terType->isPassable() || (standAtObstacle && !canFly)) + if (!t.terType->isPassable() || (movingOntoObstacle && !canFly)) return complainRet("Cannot move hero, destination tile is blocked!"); //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276) - if(standAtWater && !canFly && !canWalkOnSea) + if(movingOntoWater && !canFly && !canWalkOnSea) return complainRet("Cannot move hero, destination tile is on water!"); if(h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked) @@ -1294,7 +1295,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if(h->boat && !h->boat->onboardAssaultAllowed) lookForGuards = IGNORE_GUARDS; - turnTimerHandler.setEndTurnAllowed(h->getOwner(), !standAtWater && !standAtObstacle); + turnTimerHandler.setEndTurnAllowed(h->getOwner(), !movingOntoWater && !movingOntoObstacle); doMove(TryMoveHero::SUCCESS, lookForGuards, visitDest, LEAVING_TILE); return true; } From 9b482a2b1e1afda735b173a580abec97b44d8b59 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 27 Sep 2023 21:25:27 +0300 Subject: [PATCH 0639/1248] Fix boat creation for heroes recruited on water --- lib/mapObjects/CGHeroInstance.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index e82688891..e490de9f0 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -487,7 +487,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const if (!boat) { //Create a new boat for hero - cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum()); + cb->createObject(boatPos, h->getOwner(), Obj::BOAT, getBoatType().getNum()); boatId = cb->getTopObj(boatPos)->id; } } From 03c099d4fd6c1d48a364cff061a9ebe76dfc0e72 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 22:53:13 +0200 Subject: [PATCH 0640/1248] First steps --- client/CPlayerInterface.cpp | 4 +- client/CServerHandler.cpp | 2 +- client/lobby/CBonusSelection.cpp | 69 +++++++++++++---------------- client/lobby/CSelectionBase.cpp | 4 +- client/lobby/RandomMapTab.cpp | 4 +- client/lobby/SelectionTab.cpp | 10 ++--- client/mainmenu/CCampaignScreen.cpp | 2 +- lib/StartInfo.cpp | 4 +- lib/campaign/CampaignHandler.cpp | 10 ++--- lib/campaign/CampaignState.cpp | 10 ++--- lib/campaign/CampaignState.h | 11 ++--- lib/mapObjects/CGHeroInstance.cpp | 5 --- lib/mapping/CMapHeader.h | 4 +- lib/mapping/CMapInfo.cpp | 18 ++++---- lib/mapping/CMapInfo.h | 4 +- lib/mapping/MapFormatH3M.cpp | 6 +-- lib/mapping/MapFormatJson.cpp | 4 +- lib/rmg/CMapGenerator.cpp | 4 +- 18 files changed, 83 insertions(+), 92 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 09bdc174f..63019f00e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -239,7 +239,7 @@ void CPlayerInterface::performAutosave() prefix = settings["general"]["savePrefix"].String(); if(prefix.empty()) { - std::string name = cb->getMapHeader()->name; + std::string name = cb->getMapHeader()->name.toString(); int txtlen = TextOperations::getUnicodeCharactersCount(name); TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); @@ -1718,7 +1718,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) if(!ps->checkVanquished()) param.allDefeated = false; } - param.scenarioName = cb->getMapHeader()->name; + param.scenarioName = cb->getMapHeader()->name.toString(); param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; HighScoreCalculation highScoreCalc; highScoreCalc.parameters.push_back(param); diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 952b6feb1..3ffbb8023 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -701,7 +701,7 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared highScoreCalc->isCampaign = true; highScoreCalc->parameters.clear(); } - param.campaignName = cs->getName(); + param.campaignName = cs->getNameTranslated(); highScoreCalc->parameters.push_back(param); GH.dispatchMainThread([ourCampaign, this]() diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index d25778e9e..e63167a0c 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -79,9 +79,9 @@ CBonusSelection::CBonusSelection() iconsMapSizes = std::make_shared(AnimationPath::builtin("SCNRMPSZ"), 4, 0, 735, 26); labelCampaignDescription = std::make_shared(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]); - campaignDescription = std::make_shared(getCampaign()->getDescription(), Rect(480, 86, 286, 117), 1); + campaignDescription = std::make_shared(getCampaign()->getDescriptionTranslated(), Rect(480, 86, 286, 117), 1); - mapName = std::make_shared(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getName()); + mapName = std::make_shared(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getNameTranslated()); labelMapDescription = std::make_shared(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]); mapDescription = std::make_shared("", Rect(480, 278, 292, 108), 1); @@ -146,18 +146,18 @@ void CBonusSelection::createBonusesIcons() std::string picName = bonusPics[bonusType]; size_t picNumber = bonDescs[i].info2; - std::string desc; + MetaString desc; switch(bonDescs[i].type) { case CampaignBonusType::SPELL: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); + desc.replaceLocalString(EMetaText::SPELL_NAME, bonDescs[i].info2); break; case CampaignBonusType::MONSTER: picNumber = bonDescs[i].info2 + 2; - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info3)); - boost::algorithm::replace_first(desc, "%s", CGI->creatures()->getByIndex(bonDescs[i].info2)->getNamePluralTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + desc.replaceNumber(bonDescs[i].info3); + desc.replaceLocalString(EMetaText::CRE_PL_NAMES, bonDescs[i].info2); break; case CampaignBonusType::BUILDING: { @@ -182,17 +182,16 @@ void CBonusSelection::createBonusesIcons() picNumber = -1; if(vstd::contains((*CGI->townh)[faction]->town->buildings, buildID)) - desc = (*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTranslated(); - + desc.appendTextID((*CGI->townh)[faction]->town->buildings.find(buildID)->second->getNameTextID()); break; } case CampaignBonusType::ARTIFACT: - desc = CGI->generaltexth->allTexts[715]; - boost::algorithm::replace_first(desc, "%s", CGI->artifacts()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); + desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); break; case CampaignBonusType::SPELL_SCROLL: - desc = CGI->generaltexth->allTexts[716]; - boost::algorithm::replace_first(desc, "%s", CGI->spells()->getByIndex(bonDescs[i].info2)->getNameTranslated()); + desc.appendLocalString(EMetaText::GENERAL_TXT, 716); + desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); break; case CampaignBonusType::PRIMARY_SKILL: { @@ -211,7 +210,7 @@ void CBonusSelection::createBonusesIcons() } } picNumber = leadingSkill; - desc = CGI->generaltexth->allTexts[715]; + desc.appendLocalString(EMetaText::GENERAL_TXT, 715); std::string substitute; //text to be printed instead of %s for(int v = 0; v < toPrint.size(); ++v) @@ -224,14 +223,13 @@ void CBonusSelection::createBonusesIcons() } } - boost::algorithm::replace_first(desc, "%s", substitute); + desc.replaceRawString(substitute); break; } case CampaignBonusType::SECONDARY_SKILL: - desc = CGI->generaltexth->allTexts[718]; - - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->levels[bonDescs[i].info3 - 1]); //skill level - boost::algorithm::replace_first(desc, "%s", CGI->skillh->getByIndex(bonDescs[i].info2)->getNameTranslated()); //skill name + desc.appendLocalString(EMetaText::GENERAL_TXT, 718); + desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get()); + desc.replaceLocalString(EMetaText::SEC_SKILL_NAME, bonDescs[i].info2); picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; break; @@ -258,18 +256,17 @@ void CBonusSelection::createBonusesIcons() } picNumber = serialResID; - desc = CGI->generaltexth->allTexts[717]; - boost::algorithm::replace_first(desc, "%d", std::to_string(bonDescs[i].info2)); - std::string replacement; + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + desc.replaceNumber(bonDescs[i].info2); + if(serialResID <= 6) { - replacement = CGI->generaltexth->restypes[serialResID]; + desc.replaceLocalString(EMetaText::RES_NAMES, serialResID); } else { - replacement = CGI->generaltexth->allTexts[714 + serialResID]; + desc.replaceLocalString(EMetaText::GENERAL_TXT, 714 + serialResID); } - boost::algorithm::replace_first(desc, "%s", replacement); break; } case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO: @@ -278,31 +275,29 @@ void CBonusSelection::createBonusesIcons() if(!superhero) logGlobal->warn("No superhero! How could it be transferred?"); picNumber = superhero ? superhero->portrait : 0; - desc = CGI->generaltexth->allTexts[719]; - - boost::algorithm::replace_first(desc, "%s", getCampaign()->scenario(static_cast(bonDescs[i].info2)).scenarioName); + desc.appendLocalString(EMetaText::GENERAL_TXT, 719); + desc.replaceRawString(getCampaign()->scenario(static_cast(bonDescs[i].info2)).scenarioName.toString()); break; } case CampaignBonusType::HERO: - desc = CGI->generaltexth->allTexts[718]; - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->capColors[bonDescs[i].info1]); //hero's color - + desc.appendLocalString(EMetaText::GENERAL_TXT, 718); + desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get()); if(bonDescs[i].info2 == 0xFFFF) { - boost::algorithm::replace_first(desc, "%s", CGI->generaltexth->allTexts[101]); //hero's name + desc.replaceLocalString(EMetaText::GENERAL_TXT, 101); picNumber = -1; picName = "CBONN1A3.BMP"; } else { - boost::algorithm::replace_first(desc, "%s", CGI->heroh->objects[bonDescs[i].info2]->getNameTranslated()); + desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID()); } break; } - std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc, desc)); + std::shared_ptr bonusButton = std::make_shared(Point(475 + i * 68, 455), AnimationPath(), CButton::tooltip(desc.toString(), desc.toString())); if(picNumber != -1) picName += ":" + std::to_string(picNumber); @@ -355,8 +350,8 @@ void CBonusSelection::updateAfterStateChange() if(!CSH->mi) return; iconsMapSizes->setFrame(CSH->mi->getMapSizeIconId()); - mapName->setText(CSH->mi->getName()); - mapDescription->setText(CSH->mi->getDescription()); + mapName->setText(CSH->mi->getNameTranslated()); + mapDescription->setText(CSH->mi->getDescriptionTranslated()); for(size_t i = 0; i < difficultyIcons.size(); i++) { if(i == CSH->si->difficulty) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 4982d8f76..dbc8793f4 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -200,8 +200,8 @@ void InfoCard::changeSelection() return; labelSaveDate->setText(mapInfo->date); - mapName->setText(mapInfo->getName()); - mapDescription->setText(mapInfo->getDescription()); + mapName->setText(mapInfo->getNameTranslated()); + mapDescription->setText(mapInfo->getDescriptionTranslated()); mapDescription->label->scrollTextTo(0, false); if(mapDescription->slider) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index aaaacea86..b5bdd9a61 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -163,8 +163,8 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->isRandomMap = true; mapInfo->mapHeader = std::make_unique(); mapInfo->mapHeader->version = EMapFormat::VCMI; - mapInfo->mapHeader->name = CGI->generaltexth->allTexts[740]; - mapInfo->mapHeader->description = CGI->generaltexth->allTexts[741]; + mapInfo->mapHeader->name.appendLocalString(EMetaText::GENERAL_TXT, 740); + mapInfo->mapHeader->description.appendLocalString(EMetaText::GENERAL_TXT, 741); mapInfo->mapHeader->difficulty = 1; // Normal mapInfo->mapHeader->height = mapGenOptions->getHeight(); mapInfo->mapHeader->width = mapGenOptions->getWidth(); diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 34375db26..9e0f87052 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -111,11 +111,11 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh return (a->victoryIconIndex < b->victoryIconIndex); break; case _name: //by name - return boost::ilexicographical_compare(a->name, b->name); + return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); case _fileName: //by filename return boost::ilexicographical_compare(aaa->fileURI, bbb->fileURI); default: - return boost::ilexicographical_compare(a->name, b->name); + return boost::ilexicographical_compare(a->name.toString(), b->name.toString()); } } else //if we are sorting campaigns @@ -125,9 +125,9 @@ bool mapSorter::operator()(const std::shared_ptr aaa, const std::sh case _numOfMaps: //by number of maps in campaign return aaa->campaign->scenariosCount() < bbb->campaign->scenariosCount(); case _name: //by name - return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName()); + return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated()); default: - return boost::ilexicographical_compare(aaa->campaign->getName(), bbb->campaign->getName()); + return boost::ilexicographical_compare(aaa->campaign->getNameTranslated(), bbb->campaign->getNameTranslated()); } } } @@ -367,7 +367,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(!curItems[py]->isFolder) { - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); + std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getNameTranslated() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); diff --git a/client/mainmenu/CCampaignScreen.cpp b/client/mainmenu/CCampaignScreen.cpp index 004539454..4078908e1 100644 --- a/client/mainmenu/CCampaignScreen.cpp +++ b/client/mainmenu/CCampaignScreen.cpp @@ -105,7 +105,7 @@ CCampaignScreen::CCampaignButton::CCampaignButton(const JsonNode & config, const status = CCampaignScreen::ENABLED; auto header = CampaignHandler::getHeader(campFile); - hoverText = header->getName(); + hoverText = header->getNameTranslated(); if(persistentStorage["completedCampaigns"][header->getFilename()].Bool()) status = CCampaignScreen::COMPLETED; diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index f3abd2d92..7e14bb8bb 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -62,8 +62,8 @@ PlayerSettings * StartInfo::getPlayersSettings(const ui8 connectedPlayerId) std::string StartInfo::getCampaignName() const { - if(!campState->getName().empty()) - return campState->getName(); + if(!campState->getNameTranslated().empty()) + return campState->getNameTranslated(); else return VLC->generaltexth->allTexts[508]; } diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 63fae7c05..ad1e9146f 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -134,7 +134,7 @@ std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::st return ""; VLC->generaltexth->registerString(modName, stringID, input); - return VLC->generaltexth->translate(stringID.get()); + return stringID.get(); } void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding) @@ -149,8 +149,8 @@ void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader ret.version = CampaignVersion::VCMI; ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); ret.numberOfScenarios = reader["scenarios"].Vector().size(); - ret.name = reader["name"].String(); - ret.description = reader["description"].String(); + ret.name.appendTextID(reader["name"].String()); + ret.description.appendTextID(reader["description"].String()); ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); ret.music = AudioPath::fromJson(reader["music"]); ret.filename = filename; @@ -383,8 +383,8 @@ void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader ret.version = static_cast(reader.readUInt32()); ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] ret.loadLegacyData(campId); - ret.name = readLocalizedString(reader, filename, modName, encoding, "name"); - ret.description = readLocalizedString(reader, filename, modName, encoding, "description"); + ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); + ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); if (ret.version > CampaignVersion::RoE) ret.difficultyChoosenByPlayer = reader.readInt8(); else diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index f8bfe0271..082ddf7ee 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -134,14 +134,14 @@ bool CampaignHeader::formatVCMI() const return version == CampaignVersion::VCMI; } -std::string CampaignHeader::getDescription() const +std::string CampaignHeader::getDescriptionTranslated() const { - return description; + return description.toString(); } -std::string CampaignHeader::getName() const +std::string CampaignHeader::getNameTranslated() const { - return name; + return name.toString(); } std::string CampaignHeader::getFilename() const @@ -267,7 +267,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe return a->getHeroStrength() > b->getHeroStrength(); }); - logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast(*currentMap), getFilename(), getName()); + logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast(*currentMap), getFilename(), getNameTranslated()); mapsConquered.push_back(*currentMap); auto reservedHeroes = getReservedHeroes(); diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index 53b6d8e4c..fc5f1cbdf 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -10,6 +10,7 @@ #pragma once #include "../lib/GameConstants.h" +#include "../lib/MetaString.h" #include "../lib/filesystem/ResourcePath.h" #include "CampaignConstants.h" #include "CampaignScenarioPrologEpilog.h" @@ -74,8 +75,8 @@ class DLL_LINKAGE CampaignHeader : public boost::noncopyable CampaignVersion version = CampaignVersion::NONE; CampaignRegions campaignRegions; - std::string name; - std::string description; + MetaString name; + MetaString description; AudioPath music; std::string filename; std::string modName; @@ -90,8 +91,8 @@ public: bool playerSelectedDifficulty() const; bool formatVCMI() const; - std::string getDescription() const; - std::string getName() const; + std::string getDescriptionTranslated() const; + std::string getNameTranslated() const; std::string getFilename() const; std::string getModName() const; std::string getEncoding() const; @@ -176,7 +177,7 @@ struct DLL_LINKAGE CampaignTravel struct DLL_LINKAGE CampaignScenario { std::string mapName; //*.h3m - std::string scenarioName; //from header. human-readble + MetaString scenarioName; //from header std::set preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c) ui8 regionColor = 0; ui8 difficulty = 0; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index e82688891..9b2361c72 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1030,8 +1030,6 @@ si32 CGHeroInstance::manaLimit() const std::string CGHeroInstance::getNameTranslated() const { - if (!nameCustom.empty()) - return nameCustom; return VLC->generaltexth->translate(getNameTextID()); } @@ -1049,9 +1047,6 @@ std::string CGHeroInstance::getNameTextID() const std::string CGHeroInstance::getBiographyTranslated() const { - if (!biographyCustom.empty()) - return biographyCustom; - return VLC->generaltexth->translate(getBiographyTextID()); } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 64ea09b7b..3c43b03e4 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -223,8 +223,8 @@ public: si32 height; /// The default value is 72. si32 width; /// The default value is 72. bool twoLevel; /// The default value is true. - std::string name; - std::string description; + MetaString name; + MetaString description; ui8 difficulty; /// The default value is 1 representing a normal map difficulty. /// Specifies the maximum level to reach for a hero. A value of 0 states that there is no /// maximum level for heroes. This is the default value. diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index e36ba25c2..f037ef7d4 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -100,12 +100,12 @@ void CMapInfo::countPlayers() amountOfHumanPlayersInSave++; } -std::string CMapInfo::getName() const +std::string CMapInfo::getNameTranslated() const { - if(campaign && !campaign->getName().empty()) - return campaign->getName(); - else if(mapHeader && mapHeader->name.length()) - return mapHeader->name; + if(campaign && !campaign->getNameTranslated().empty()) + return campaign->getNameTranslated(); + else if(mapHeader && !mapHeader->name.empty()) + return mapHeader->name.toString(); else return VLC->generaltexth->allTexts[508]; } @@ -121,16 +121,16 @@ std::string CMapInfo::getNameForList() const } else { - return getName(); + return getNameTranslated(); } } -std::string CMapInfo::getDescription() const +std::string CMapInfo::getDescriptionTranslated() const { if(campaign) - return campaign->getDescription(); + return campaign->getDescriptionTranslated(); else - return mapHeader->description; + return mapHeader->description.toString(); } int CMapInfo::getMapSizeIconId() const diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 33d3874a1..6ac7ccea9 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -50,9 +50,9 @@ public: void campaignInit(); void countPlayers(); // TODO: Those must be on client-side - std::string getName() const; + std::string getNameTranslated() const; std::string getNameForList() const; - std::string getDescription() const; + std::string getDescriptionTranslated() const; int getMapSizeIconId() const; int getMapSizeFormatIconId() const; std::string getMapSizeName() const; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 9da89c494..751de6ddc 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -180,8 +180,8 @@ void CMapLoaderH3M::readHeader() mapHeader->areAnyPlayers = reader->readBool(); mapHeader->height = mapHeader->width = reader->readInt32(); mapHeader->twoLevel = reader->readBool(); - mapHeader->name = readLocalizedString("header.name"); - mapHeader->description = readLocalizedString("header.description"); + mapHeader->name.appendTextID(readLocalizedString("header.name")); + mapHeader->description.appendTextID(readLocalizedString("header.description")); mapHeader->difficulty = reader->readInt8(); if(features.levelAB) @@ -2275,7 +2275,7 @@ std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIden return ""; VLC->generaltexth->registerString(modName, fullIdentifier, mapString); - return VLC->generaltexth->translate(fullIdentifier.get()); + return fullIdentifier.get(); } void CMapLoaderH3M::afterRead() diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 66de3989e..ba3c044ff 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -413,8 +413,8 @@ void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) { - handler.serializeString("name", mapHeader->name); - handler.serializeString("description", mapHeader->description); + handler.serializeStruct("name", mapHeader->name); + handler.serializeStruct("description", mapHeader->description); handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); //todo: support arbitrary percentage diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 30dcd24fe..4a3e919d8 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -407,8 +407,8 @@ void CMapGenerator::addHeaderInfo() m.width = mapGenOptions.getWidth(); m.height = mapGenOptions.getHeight(); m.twoLevel = mapGenOptions.getHasTwoLevels(); - m.name = VLC->generaltexth->allTexts[740]; - m.description = getMapDescription(); + m.name.appendLocalString(EMetaText::GENERAL_TXT, 740); + m.description.appendRawString(getMapDescription()); m.difficulty = 1; addPlayerInfo(); m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE); From ab373f08abbc1747d70e2d32ed19e9662b975dee Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 23:11:11 +0200 Subject: [PATCH 0641/1248] Use meta string for messages --- lib/MetaString.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 8 ++++---- lib/mapObjects/CGPandoraBox.h | 2 +- lib/mapObjects/MiscObjects.cpp | 30 +++++++++++++++--------------- lib/mapObjects/MiscObjects.h | 6 +++--- lib/mapping/MapFormatH3M.cpp | 6 +++--- lib/mapping/MapFormatH3M.h | 3 ++- 7 files changed, 29 insertions(+), 28 deletions(-) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 3c9eac22c..93665a0e9 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -102,7 +102,7 @@ void MetaString::clear() bool MetaString::empty() const { - return message.empty(); + return message.empty() || toString().empty(); } std::string MetaString::getLocalString(const std::pair & txt) const diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index de2a00448..ece2b57bc 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -35,7 +35,7 @@ void CGPandoraBox::init() { i.reward.removeObject = true; if(!message.empty() && i.message.empty()) - i.message = MetaString::createFromRawString(message); + i.message = message; } } @@ -209,7 +209,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) { CRewardableObject::serializeJsonOptions(handler); - handler.serializeString("guardMessage", message); + handler.serializeStruct("guardMessage", message); if(!handler.saving) { @@ -297,7 +297,7 @@ void CGEvent::init() { i.reward.removeObject = removeAfterVisit; if(!message.empty() && i.message.empty()) - i.message = MetaString::createFromRawString(message); + i.message = message; } } @@ -327,7 +327,7 @@ void CGEvent::activated( const CGHeroInstance * h ) const InfoWindow iw; iw.player = h->tempOwner; if(!message.empty()) - iw.text.appendRawString(message); + iw.text = message; else iw.text.appendLocalString(EMetaText::ADVOB_TXT, 16); cb->showInfoDialog(&iw); diff --git a/lib/mapObjects/CGPandoraBox.h b/lib/mapObjects/CGPandoraBox.h index 3d4cf3540..4bbd5b2af 100644 --- a/lib/mapObjects/CGPandoraBox.h +++ b/lib/mapObjects/CGPandoraBox.h @@ -19,7 +19,7 @@ struct InfoWindow; class DLL_LINKAGE CGPandoraBox : public CRewardableObject { public: - std::string message; + MetaString message; void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 5b265795a..818521339 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -268,7 +268,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - ynd.text.appendRawString(message); + ynd.text = message; cb->showBlockingDialog(&ynd); } else @@ -288,7 +288,7 @@ void CGResource::collectRes(const PlayerColor & player) const if(!message.empty()) { sii.type = EInfoWindowMode::AUTO; - sii.text.appendRawString(message); + sii.text = message; } else { @@ -320,7 +320,7 @@ void CGResource::serializeJsonOptions(JsonSerializeFormat & handler) if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) CCreatureSet::serializeJson(handler, "guards", 7); handler.serializeInt("amount", amount, 0); - handler.serializeString("guardMessage", message); + handler.serializeStruct("guardMessage", message); } bool CGTeleport::isEntrance() const @@ -728,8 +728,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const case Obj::ARTIFACT: { iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0); - if(message.length()) - iw.text.appendRawString(message); + if(!message.empty()) + iw.text = message; else iw.text.appendLocalString(EMetaText::ART_EVNTS, subID); } @@ -738,8 +738,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const { int spellID = storedArtifact->getScrollSpellID(); iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0); - if(message.length()) - iw.text.appendRawString(message); + if(!message.empty()) + iw.text = message; else { iw.text.appendLocalString(EMetaText::ADVOB_TXT,135); @@ -764,8 +764,8 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - if(message.length()) - ynd.text.appendRawString(message); + if(!message.empty()) + ynd.text = message; else { // TODO: Guard text is more complex in H3, see mantis issue 2325 for details @@ -779,11 +779,11 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const break; case Obj::SPELL_SCROLL: { - if(message.length()) + if(!message.empty()) { BlockingDialog ynd(true,false); ynd.player = h->getOwner(); - ynd.text.appendRawString(message); + ynd.text = message; cb->showBlockingDialog(&ynd); } else @@ -828,7 +828,7 @@ void CGArtifact::afterAddToMap(CMap * map) void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) { - handler.serializeString("guardMessage", message); + handler.serializeStruct("guardMessage", message); CArmedInstance::serializeJsonOptions(handler); if(!handler.saving && !handler.getCurrent()["guards"].Vector().empty()) CCreatureSet::serializeJson(handler, "guards", 7); @@ -1046,7 +1046,7 @@ void CGSignBottle::initObj(CRandomGenerator & rand) { auto vector = VLC->generaltexth->findStringsWithPrefix("core.randsign"); std::string messageIdentifier = *RandomGeneratorUtil::nextItem(vector, rand); - message = VLC->generaltexth->translate(messageIdentifier); + message.appendTextID(TextIdentifier("core", "randsign", messageIdentifier).get()); } if(ID == Obj::OCEAN_BOTTLE) @@ -1059,7 +1059,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->getOwner(); - iw.text.appendRawString(message); + iw.text = message; cb->showInfoDialog(&iw); if(ID == Obj::OCEAN_BOTTLE) @@ -1068,7 +1068,7 @@ void CGSignBottle::onHeroVisit( const CGHeroInstance * h ) const void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) { - handler.serializeString("text", message); + handler.serializeStruct("text", message); } void CGScholar::onHeroVisit( const CGHeroInstance * h ) const diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index d35479513..1b4397794 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -43,7 +43,7 @@ public: class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles { public: - std::string message; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; @@ -119,7 +119,7 @@ class DLL_LINKAGE CGArtifact : public CArmedInstance { public: CArtifactInstance * storedArtifact = nullptr; - std::string message; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; @@ -149,7 +149,7 @@ public: static constexpr ui32 RANDOM_AMOUNT = 0; ui32 amount = RANDOM_AMOUNT; //0 if random - std::string message; + MetaString message; void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 751de6ddc..90b6b8dfc 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1135,7 +1135,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) { auto * object = new CGSignBottle(); - object->message = readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message")); + object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); reader->skipZero(4); return object; } @@ -2247,12 +2247,12 @@ void CMapLoaderH3M::readEvents() } } -void CMapLoaderH3M::readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position) +void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position) { bool hasMessage = reader->readBool(); if(hasMessage) { - message = readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message")); + message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message"))); bool hasGuards = reader->readBool(); if(hasGuards) readCreatureSet(guards, 7); diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 8b7a10a08..6a0180b54 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class MapReaderH3M; +class MetaString; class CArtifactInstance; class CGObjectInstance; class CGSeerHut; @@ -215,7 +216,7 @@ private: /** * read optional message and optional guards */ - void readMessageAndGuards(std::string & message, CCreatureSet * guards, const int3 & position); + void readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position); /// reads string from input stream and converts it to unicode std::string readBasicString(); From 0ac893b80f57cc75dd1a7a73953ca1bb21c72731 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 23:22:45 +0200 Subject: [PATCH 0642/1248] Rumors meta string --- client/windows/GUIClasses.cpp | 3 +-- lib/CGameInfoCallback.cpp | 19 ++++++++++--------- lib/mapping/CMap.cpp | 2 +- lib/mapping/CMap.h | 3 ++- lib/mapping/MapFormatH3M.cpp | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 419dea97f..8bf9052c1 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -456,8 +456,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) heroDescription = std::make_shared("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); heroesForHire = std::make_shared(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]); - auto rumorText = boost::str(boost::format(CGI->generaltexth->allTexts[216]) % LOCPLINT->cb->getTavernRumor(tavernObj)); - rumor = std::make_shared(rumorText, Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + rumor = std::make_shared(LOCPLINT->cb->getTavernRumor(tavernObj), Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); cancel = std::make_shared(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index b50d59e9b..bc3dfebe3 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -649,33 +649,34 @@ EPlayerStatus CGameInfoCallback::getPlayerStatus(PlayerColor player, bool verbos std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTavern) const { - std::string text; + MetaString text; + text.appendLocalString(EMetaText::GENERAL_TXT, 216); + std::string extraText; if(gs->rumor.type == RumorState::TYPE_NONE) - return text; + return text.toString(); auto rumor = gs->rumor.last[gs->rumor.type]; switch(gs->rumor.type) { case RumorState::TYPE_SPECIAL: + text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first); if(rumor.first == RumorState::RUMOR_GRAIL) - extraText = VLC->generaltexth->arraytxt[158 + rumor.second]; + text.replaceTextID(TextIdentifier("core", "genrltxt", "arraytxt", 158 + rumor.second).get()); else - extraText = VLC->generaltexth->capColors[rumor.second]; - - text = boost::str(boost::format(VLC->generaltexth->allTexts[rumor.first]) % extraText); + text.replaceTextID(TextIdentifier("core", "genrltxt", "capitalColors", rumor.second).get()); break; case RumorState::TYPE_MAP: - text = gs->map->rumors[rumor.first].text; + text.replaceRawString(gs->map->rumors[rumor.first].text.toString()); break; case RumorState::TYPE_RAND: - text = VLC->generaltexth->tavernRumors[rumor.first]; + text.replaceTextID(TextIdentifier("core", "genrltxt", "randtvrn", rumor.first).get()); break; } - return text; + return text.toString(); } PlayerRelations CGameInfoCallback::getPlayerRelations( PlayerColor color1, PlayerColor color2 ) const diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 811a52e02..71feca889 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -32,7 +32,7 @@ VCMI_LIB_NAMESPACE_BEGIN void Rumor::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); - handler.serializeString("text", text); + handler.serializeStruct("text", text); } DisposedHero::DisposedHero() : heroId(0), portrait(255) diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index c3cd0bfdb..abed40d06 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -11,6 +11,7 @@ #pragma once #include "CMapHeader.h" +#include "../MetaString.h" #include "../mapObjects/MiscObjects.h" // To serialize static props #include "../mapObjects/CQuest.h" // To serialize static props #include "../mapObjects/CGTownInstance.h" // To serialize static props @@ -36,7 +37,7 @@ struct TeleportChannel; struct DLL_LINKAGE Rumor { std::string name; - std::string text; + MetaString text; Rumor() = default; ~Rumor() = default; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 90b6b8dfc..0d1e00ed5 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -807,7 +807,7 @@ void CMapLoaderH3M::readRumors() { Rumor ourRumor; ourRumor.name = readBasicString(); - ourRumor.text = readLocalizedString(TextIdentifier("header", "rumor", it, "text")); + ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text"))); map->rumors.push_back(ourRumor); } } From f9f79255c57ba0e78e8b6e0a4e3058b9aae4ad50 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 23:25:19 +0200 Subject: [PATCH 0643/1248] Creature message meta string --- lib/mapObjects/CGCreature.cpp | 4 ++-- lib/mapObjects/CGCreature.h | 3 ++- lib/mapping/MapFormatH3M.cpp | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index a2891c2cb..21ddb93f6 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -108,7 +108,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->tempOwner; - iw.text.appendRawString(message); + iw.text = message; iw.type = EInfoWindowMode::MODAL; cb->showInfoDialog(&iw); } @@ -578,7 +578,7 @@ void CGCreature::serializeJsonOptions(JsonSerializeFormat & handler) handler.serializeBool("noGrowing", notGrowingTeam); handler.serializeBool("neverFlees", neverFlees); - handler.serializeString("rewardMessage", message); + handler.serializeStruct("rewardMessage", message); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 76df95aed..ac599465a 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -11,6 +11,7 @@ #include "CArmedInstance.h" #include "../ResourceSet.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -27,7 +28,7 @@ public: ui32 identifier; //unique code for this monster (used in missions) si8 character; //character of this set of creatures (0 - the most friendly, 4 - the most hostile) => on init changed to -4 (compliant) ... 10 value (savage) - std::string message; //message printed for attacking hero + MetaString message; //message printed for attacking hero TResources resources; // resources given to hero that has won with monsters ArtifactID gainedArtifact; //ID of artifact gained to hero, -1 if none bool neverFlees; //if true, the troops will never flee diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 0d1e00ed5..271e8e068 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1099,7 +1099,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob bool hasMessage = reader->readBool(); if(hasMessage) { - object->message = readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message")); + object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); reader->readResourses(object->resources); object->gainedArtifact = reader->readArtifact(); } From 00c8c2eb82181fc13df9b1555ce518205ac53fcc Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 23:28:17 +0200 Subject: [PATCH 0644/1248] Event message meta string --- lib/mapping/CMap.cpp | 2 +- lib/mapping/CMapDefines.h | 3 ++- lib/mapping/MapFormatH3M.cpp | 4 ++-- server/CGameHandler.cpp | 4 ++-- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 71feca889..fd9062164 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -59,7 +59,7 @@ bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const void CMapEvent::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); - handler.serializeString("message", message); + handler.serializeStruct("message", message); handler.serializeInt("players", players); handler.serializeInt("humanAffected", humanAffected); handler.serializeInt("computerAffected", computerAffected); diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index c0bce891a..250e2eab4 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -13,6 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN #include "../ResourceSet.h" +#include "../MetaString.h" class TerrainType; class RiverType; @@ -33,7 +34,7 @@ public: bool earlierThanOrEqual(const CMapEvent & other) const; std::string name; - std::string message; + MetaString message; TResources resources; ui8 players; // affected players, bit field? ui8 humanAffected; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 271e8e068..828bfb3e9 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2155,7 +2155,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt { CCastleEvent event; event.name = readBasicString(); - event.message = readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description")); + event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); reader->readResourses(event.resources); @@ -2225,7 +2225,7 @@ void CMapLoaderH3M::readEvents() { CMapEvent event; event.name = readBasicString(); - event.message = readLocalizedString(TextIdentifier("event", eventID, "description")); + event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); reader->readResourses(event.resources); event.players = reader->readUInt8(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 744152a9a..4d91d2378 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3216,7 +3216,7 @@ void CGameHandler::handleTimeEvents() //prepare dialog InfoWindow iw; iw.player = color; - iw.text.appendRawString(ev.message); + iw.text = ev.message; for (int i=0; i Date: Wed, 27 Sep 2023 23:49:27 +0200 Subject: [PATCH 0645/1248] Seerhut meta strings --- lib/mapObjects/CQuest.cpp | 54 ++++++++++++++++++------------------ lib/mapObjects/CQuest.h | 3 +- lib/mapping/MapFormatH3M.cpp | 6 ++-- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 377786a5a..bf0b3f833 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -181,20 +181,20 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const { - std::string text; + MetaString text; bool failRequirements = (h ? !checkQuest(h) : true); if(firstVisit) { isCustom = isCustomFirst; text = firstVisitText; - iwText.appendRawString(text); + iwText.appendRawString(text.toString()); } else if(failRequirements) { isCustom = isCustomNext; text = nextVisitText; - iwText.appendRawString(text); + iwText.appendRawString(text.toString()); } switch (missionType) { @@ -223,7 +223,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_KILL_HERO: components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); if(!isCustom) - addReplacements(iwText, text); + addReplacements(iwText, text.toString()); break; case MISSION_HERO: //FIXME: portrait may not match hero, if custom portrait was set in map editor @@ -236,7 +236,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components components.emplace_back(stackToKill); if(!isCustom) { - addReplacements(iwText, text); + addReplacements(iwText, text.toString()); } } break; @@ -286,7 +286,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_PLAYER: components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0); if(!isCustom) - iwText.replaceRawString(VLC->generaltexth->colors[m13489val]); + iwText.replaceLocalString(EMetaText::COLOR, m13489val); break; } } @@ -380,7 +380,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const void CQuest::getCompletionText(MetaString &iwText) const { - iwText.appendRawString(completedText); + iwText.appendRawString(completedText.toString()); switch(missionType) { case CQuest::MISSION_LEVEL: @@ -388,22 +388,22 @@ void CQuest::getCompletionText(MetaString &iwText) const iwText.replaceNumber(m13489val); break; case CQuest::MISSION_PRIMARY_STAT: - if (vstd::contains (completedText,'%')) //there's one case when there's nothing to replace + { + MetaString loot; + assert(m2stats.size() <= 4); + for (int i = 0; i < m2stats.size(); ++i) { - MetaString loot; - for (int i = 0; i < 4; ++i) + if (m2stats[i]) { - if (m2stats[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } + loot.appendRawString("%d %s"); + loot.replaceNumber(m2stats[i]); + loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); } + if (!isCustomComplete) + iwText.replaceRawString(loot.buildList()); break; + } case CQuest::MISSION_ART: { MetaString loot; @@ -447,7 +447,7 @@ void CQuest::getCompletionText(MetaString &iwText) const case MISSION_KILL_HERO: case MISSION_KILL_CREATURE: if (!isCustomComplete) - addReplacements(iwText, completedText); + addReplacements(iwText, completedText.toString()); break; case MISSION_HERO: if (!isCustomComplete) @@ -470,9 +470,9 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi { auto q = handler.enterStruct(fieldName); - handler.serializeString("firstVisitText", firstVisitText); - handler.serializeString("nextVisitText", nextVisitText); - handler.serializeString("completedText", completedText); + handler.serializeStruct("firstVisitText", firstVisitText); + handler.serializeStruct("nextVisitText", nextVisitText); + handler.serializeStruct("completedText", completedText); if(!handler.saving) { @@ -585,16 +585,16 @@ void CGSeerHut::initObj(CRandomGenerator & rand) std::string questName = quest->missionName(quest->missionType); if(!quest->isCustomFirst) - quest->firstVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(0), quest->textOption); + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get()); if(!quest->isCustomNext) - quest->nextVisitText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(1), quest->textOption); + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); if(!quest->isCustomComplete) - quest->completedText = VLC->generaltexth->translate("core.seerhut.quest." + questName + "." + quest->missionState(2), quest->textOption); + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); } else { quest->progress = CQuest::COMPLETE; - quest->firstVisitText = VLC->generaltexth->seerEmpty[quest->completedOption]; + quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } quest->getCompletionText(configuration.onSelect); @@ -639,7 +639,7 @@ void CQuest::addReplacements(MetaString &out, const std::string &base) const } break; case MISSION_KILL_HERO: - out.replaceRawString(heroName); + out.replaceTextID(heroName); break; } } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3885229dd..3f4d6b793 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -11,6 +11,7 @@ #include "CRewardableObject.h" #include "../ResourceSet.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -70,7 +71,7 @@ public: std::string heroName; //backup of hero name si32 heroPortrait; - std::string firstVisitText, nextVisitText, completedText; + MetaString firstVisitText, nextVisitText, completedText; bool isCustomFirst; bool isCustomNext; bool isCustomComplete; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 828bfb3e9..ba6fcaba2 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2072,9 +2072,9 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } guard->quest->lastDay = reader->readInt32(); - guard->quest->firstVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit")); - guard->quest->nextVisitText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit")); - guard->quest->completedText = readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed")); + guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit"))); + guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit"))); + guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed"))); guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); guard->quest->isCustomComplete = !guard->quest->completedText.empty(); From 0c94a4d8910dbf36c2a5e1c7e47ec308bd99d24b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 27 Sep 2023 23:57:05 +0200 Subject: [PATCH 0646/1248] Town name switched to id --- lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 20 ++++++++++---------- lib/mapObjects/CGTownInstance.h | 8 ++++---- lib/mapping/MapFormatH3M.cpp | 2 +- mapeditor/inspector/inspector.cpp | 2 +- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index f5657821c..dd3c72a5e 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1020,7 +1020,7 @@ void CGameState::initTowns() if(vti->getNameTranslated().empty()) { size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1); - vti->setNameTranslated(vti->getTown()->getRandomNameTranslated(nameID)); + vti->setNameTextId(vti->getTown()->getRandomNameTextID(nameID)); } static const BuildingID basicDwellings[] = { BuildingID::DWELL_FIRST, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3, BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 }; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index b82fe8d68..bf8b25124 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -327,7 +327,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const } else { - logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), name); + logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), nameTextId); } } @@ -337,15 +337,15 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const if(visitingHero == h) { cb->stopHeroVisitCastle(this, h); - logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), name); + logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), nameTextId); } else - logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), name); + logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), nameTextId); } std::string CGTownInstance::getObjectName() const { - return name + ", " + town->faction->getNameTranslated(); + return nameTextId + ", " + town->faction->getNameTranslated(); } bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const @@ -767,7 +767,7 @@ void CGTownInstance::updateAppearance() std::string CGTownInstance::nodeName() const { - return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + name; + return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + nameTextId; } void CGTownInstance::deserializationFix() @@ -915,12 +915,12 @@ CBonusSystemNode & CGTownInstance::whatShouldBeAttached() std::string CGTownInstance::getNameTranslated() const { - return name; + return VLC->generaltexth->translate(nameTextId); } -void CGTownInstance::setNameTranslated( const std::string & newName ) +void CGTownInstance::setNameTextId( const std::string & newName ) { - name = newName; + nameTextId = newName; } const CArmedInstance * CGTownInstance::getUpperArmy() const @@ -980,7 +980,7 @@ TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const return town->buildings.at(buildingID)->resources; else { - logGlobal->error("Town %s at %s has no possible building %d!", name, pos.toString(), buildingID.toEnum()); + logGlobal->error("Town %s at %s has no possible building %d!", nameTextId, pos.toString(), buildingID.toEnum()); return TResources(); } @@ -1097,7 +1097,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!handler.saving) handler.serializeEnum("tightFormation", formation, NArmyFormation::names); //for old format CArmedInstance::serializeJsonOptions(handler); - handler.serializeString("name", name); + handler.serializeString("name", nameTextId); { auto decodeBuilding = [this](const std::string & identifier) -> si32 diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 6f47cf525..07da57644 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -44,7 +44,7 @@ struct DLL_LINKAGE GrowthInfo class DLL_LINKAGE CGTownInstance : public CGDwelling, public IShipyard, public IMarket, public INativeTerrainProvider, public ICreatureUpgrader { - std::string name; // name of town + std::string nameTextId; // name of town public: using CGDwelling::getPosition; @@ -73,7 +73,7 @@ public: template void serialize(Handler &h, const int version) { h & static_cast(*this); - h & name; + h & nameTextId; h & builded; h & destroyed; h & identifier; @@ -102,7 +102,7 @@ public: { if(!town->buildings.count(building) || !town->buildings.at(building)) { - logGlobal->error("#1444-like issue in CGTownInstance::serialize. From town %s at %s removing the bogus builtBuildings item %s", name, pos.toString(), building); + logGlobal->error("#1444-like issue in CGTownInstance::serialize. From town %s at %s removing the bogus builtBuildings item %s", nameTextId, pos.toString(), building); return true; } return false; @@ -126,7 +126,7 @@ public: const CArmedInstance *getUpperArmy() const; //garrisoned hero if present or the town itself std::string getNameTranslated() const; - void setNameTranslated(const std::string & newName); + void setNameTextId(const std::string & newName); ////////////////////////////////////////////////////////////////////////// diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index ba6fcaba2..90aeba11e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2094,7 +2094,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt bool hasName = reader->readBool(); if(hasName) - object->setNameTranslated(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); + object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); bool hasGarrison = reader->readBool(); if(hasGarrison) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 9955d207b..b32b9092e 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -553,7 +553,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTranslated(value.toString().toStdString()); + o->setNameTextId(value.toString().toStdString()); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) From 486091a91580f788712a1c0ff207a8737d5b259a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 00:00:32 +0200 Subject: [PATCH 0647/1248] Heroes switched to text id --- lib/mapObjects/CGHeroInstance.cpp | 12 ++++++------ lib/mapObjects/CGHeroInstance.h | 8 ++++---- lib/mapping/MapFormatH3M.cpp | 8 ++++---- lib/mapping/MapFormatJson.cpp | 2 +- mapeditor/inspector/inspector.cpp | 6 +++--- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 9b2361c72..f7735e053 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1035,8 +1035,8 @@ std::string CGHeroInstance::getNameTranslated() const std::string CGHeroInstance::getNameTextID() const { - if (!nameCustom.empty()) - return nameCustom; + if (!nameCustomTextId.empty()) + return nameCustomTextId; if (type) return type->getNameTextID(); @@ -1052,8 +1052,8 @@ std::string CGHeroInstance::getBiographyTranslated() const std::string CGHeroInstance::getBiographyTextID() const { - if (!biographyCustom.empty()) - return biographyCustom; + if (!biographyCustomTextId.empty()) + return biographyCustomTextId; if (type) return type->getBiographyTextID(); @@ -1515,7 +1515,7 @@ void CGHeroInstance::updateFrom(const JsonNode & data) void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { - handler.serializeString("biography", biographyCustom); + handler.serializeString("biography", biographyCustomTextId); handler.serializeInt("experience", exp, 0); if(!handler.saving && exp != UNINITIALIZED_EXPERIENCE) //do not gain levels if experience is not initialized @@ -1526,7 +1526,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) } } - handler.serializeString("name", nameCustom); + handler.serializeString("name", nameCustomTextId); handler.serializeInt("gender", gender, 0); { diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index c8aaad4fa..43a600e15 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -74,8 +74,8 @@ public: std::vector > secSkills; //first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert); if hero has ability (-1, -1) it meansthat it should have default secondary abilities EHeroGender gender; - std::string nameCustom; - std::string biographyCustom; + std::string nameCustomTextId; + std::string biographyCustomTextId; bool inTownGarrison; // if hero is in town garrison ConstTransitivePtr visitedTown; //set if hero is visiting town or in the town garrison @@ -319,8 +319,8 @@ public: h & static_cast(*this); h & exp; h & level; - h & nameCustom; - h & biographyCustom; + h & nameCustomTextId; + h & biographyCustomTextId; h & portrait; h & mana; h & secSkills; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 90aeba11e..3cbbad65d 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -860,7 +860,7 @@ void CMapLoaderH3M::readPredefinedHeroes() bool hasCustomBio = reader->readBool(); if(hasCustomBio) - hero->biographyCustom = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); + hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); // 0xFF is default, 00 male, 01 female hero->gender = static_cast(reader->readUInt8()); @@ -1685,7 +1685,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec { if(elem.heroId.getNum() == object->subID) { - object->nameCustom = elem.name; + object->nameCustomTextId = elem.name; object->portrait = elem.portrait; break; } @@ -1693,7 +1693,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec bool hasName = reader->readBool(); if(hasName) - object->nameCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); if(features.levelSOD) { @@ -1748,7 +1748,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec { bool hasCustomBiography = reader->readBool(); if(hasCustomBiography) - object->biographyCustom = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); + object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); object->gender = static_cast(reader->readUInt8()); assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index ba3c044ff..52e981733 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -529,7 +529,7 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) if(hero) { auto heroData = handler.enterStruct(hero->instanceName); - heroData->serializeString("name", hero->nameCustom); + heroData->serializeString("name", hero->nameCustomTextId); if(hero->ID == Obj::HERO) { diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b32b9092e..8acaa91ae 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -277,8 +277,8 @@ void Inspector::updateProperties(CGHeroInstance * o) delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}}; addProperty("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false); } - addProperty("Name", o->nameCustom, false); - addProperty("Biography", o->biographyCustom, new MessageDelegate, false); + addProperty("Name", o->nameCustomTextId, false); + addProperty("Biography", o->biographyCustomTextId, new MessageDelegate, false); addProperty("Portrait", o->portrait, false); auto * delegate = new HeroSkillsDelegate(*o); @@ -606,7 +606,7 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustom = value.toString().toStdString(); + o->nameCustomTextId = value.toString().toStdString(); if(key == "Experience") o->exp = value.toString().toInt(); From 5b97c323d3c833309ea053f58cbaa8f89db1a129 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 00:04:05 +0200 Subject: [PATCH 0648/1248] Rename hero strings to text id --- client/lobby/OptionsTab.cpp | 8 ++++---- lib/StartInfo.h | 4 ++-- lib/mapping/CMapHeader.cpp | 2 +- lib/mapping/CMapHeader.h | 4 ++-- lib/mapping/MapFormatH3M.cpp | 2 +- lib/mapping/MapFormatJson.cpp | 2 +- server/CVCMIServer.cpp | 2 +- test/game/CGameStateTest.cpp | 2 +- test/map/MapComparer.cpp | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index cfa94763c..16541525c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -405,8 +405,8 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; default: { - if(!playerSettings.heroName.empty()) - return playerSettings.heroName; + if(!playerSettings.heroNameTextId.empty()) + return playerSettings.heroNameTextId; auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); return (*CGI->heroh)[index]->getNameTranslated(); } @@ -927,7 +927,7 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroName.empty()) + if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroNameTextId.empty()) GH.windows().createAndPushWindow(helper.playerSettings.hero); else GH.windows().createAndPushWindow(helper); @@ -1013,7 +1013,7 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) if(playerSettings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(playerSettings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) return; - if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroName.empty()) + if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroNameTextId.empty()) GH.windows().createAndPushWindow(playerSettings.hero); else GH.windows().createAndPushWindow(*this); diff --git a/lib/StartInfo.h b/lib/StartInfo.h index a558c4d32..5c4797acc 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -59,7 +59,7 @@ struct DLL_LINKAGE PlayerSettings HeroTypeID hero; HeroTypeID heroPortrait; //-1 if default, else ID - std::string heroName; + std::string heroNameTextId; PlayerColor color; //from 0 - enum EHandicap {NO_HANDICAP, MILD, SEVERE}; EHandicap handicap;//0-no, 1-mild, 2-severe @@ -73,7 +73,7 @@ struct DLL_LINKAGE PlayerSettings h & castle; h & hero; h & heroPortrait; - h & heroName; + h & heroNameTextId; h & bonus; h & color; h & handicap; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 98a8afdb6..9d7cd718a 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -62,7 +62,7 @@ bool PlayerInfo::canAnyonePlay() const bool PlayerInfo::hasCustomMainHero() const { - return !mainCustomHeroName.empty() && mainCustomHeroPortrait != -1; + return !mainCustomHeroNameTextId.empty() && mainCustomHeroPortrait != -1; } EventCondition::EventCondition(EWinLoseType condition): diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 3c43b03e4..9866e095d 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -63,7 +63,7 @@ struct DLL_LINKAGE PlayerInfo bool hasRandomHero; /// The default value is -1. si32 mainCustomHeroPortrait; - std::string mainCustomHeroName; + std::string mainCustomHeroNameTextId; /// ID of custom hero (only if portrait and hero name are set, otherwise unpredicted value), -1 if none (not always -1) si32 mainCustomHeroId; @@ -84,7 +84,7 @@ struct DLL_LINKAGE PlayerInfo h & allowedFactions; h & isFactionRandom; h & mainCustomHeroPortrait; - h & mainCustomHeroName; + h & mainCustomHeroNameTextId; h & heroesNames; h & hasMainTown; h & generateHeroAtMainTown; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 3cbbad65d..a9e42b9db 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -253,7 +253,7 @@ void CMapLoaderH3M::readPlayerInfo() if(playerInfo.mainCustomHeroId != -1) { playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); - playerInfo.mainCustomHeroName = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); + playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); } if(features.levelAB) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 52e981733..b6da8d13b 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -571,7 +571,7 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) if(instanceName == info.mainHeroInstance) { //this is main hero - info.mainCustomHeroName = hname.heroName; + info.mainCustomHeroNameTextId = hname.heroName; info.hasRandomHero = (hname.heroId == -1); info.mainCustomHeroId = hname.heroId; info.mainCustomHeroPortrait = -1; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 5b3eecdef..11e934ee5 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -730,7 +730,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; - pset.heroName = pinfo.mainCustomHeroName; + pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; pset.heroPortrait = pinfo.mainCustomHeroPortrait; } diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 2b6498943..e571d2d25 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -170,7 +170,7 @@ public: if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; - pset.heroName = pinfo.mainCustomHeroName; + pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; pset.heroPortrait = pinfo.mainCustomHeroPortrait; } diff --git a/test/map/MapComparer.cpp b/test/map/MapComparer.cpp index cfc908ead..62ffcabfb 100644 --- a/test/map/MapComparer.cpp +++ b/test/map/MapComparer.cpp @@ -76,7 +76,7 @@ void checkEqual(const PlayerInfo & actual, const PlayerInfo & expected) VCMI_CHECK_FIELD_EQUAL(isFactionRandom); VCMI_CHECK_FIELD_EQUAL(mainCustomHeroPortrait); - VCMI_CHECK_FIELD_EQUAL(mainCustomHeroName); + VCMI_CHECK_FIELD_EQUAL(mainCustomHeroNameTextId); VCMI_CHECK_FIELD_EQUAL(mainCustomHeroId); From 6da605ff831fe109f88ea04ec58a71528bc75cd7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 00:10:28 +0200 Subject: [PATCH 0649/1248] Campaign meta strings --- client/lobby/CBonusSelection.cpp | 6 +++--- client/mainmenu/CPrologEpilogVideo.cpp | 2 +- lib/campaign/CampaignHandler.cpp | 8 ++++---- lib/campaign/CampaignScenarioPrologEpilog.h | 3 ++- lib/campaign/CampaignState.h | 2 +- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index e63167a0c..4237cdb90 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -509,9 +509,9 @@ void CBonusSelection::CRegion::clickReleased(const Point & cursorPosition) void CBonusSelection::CRegion::showPopupWindow(const Point & cursorPosition) { // FIXME: For some reason "down" is only ever contain indeterminate_value - auto text = CSH->si->campState->scenario(idOfMapAndRegion).regionText; - if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && text.size()) + auto & text = CSH->si->campState->scenario(idOfMapAndRegion).regionText; + if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && !text.empty()) { - CRClickPopup::createAndPush(text); + CRClickPopup::createAndPush(text.toString()); } } diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index b37383c03..33231d4da 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -36,7 +36,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f }; CCS->soundh->setCallback(voiceSoundHandle, onVoiceStop); - text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText); + text = std::make_shared(Rect(100, 500, 600, 100), EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::METALLIC_GOLD, spe.prologText.toString()); text->scrollTextTo(-100); } diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index ad1e9146f..2efc2fac2 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -169,7 +169,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) ret.prologVideo = VideoPath::fromJson(identifier["video"]); ret.prologMusic = AudioPath::fromJson(identifier["music"]); ret.prologVoice = AudioPath::fromJson(identifier["voice"]); - ret.prologText = identifier["text"].String(); + ret.prologText.jsonDeserialize(identifier["text"]); } return ret; }; @@ -181,7 +181,7 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) ret.regionColor = reader["color"].Integer(); ret.difficulty = reader["difficulty"].Integer(); - ret.regionText = reader["regionText"].String(); + ret.regionText.jsonDeserialize(reader["regionText"]); ret.prolog = prologEpilogReader(reader["prolog"]); ret.epilog = prologEpilogReader(reader["epilog"]); @@ -410,7 +410,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader ret.prologVideo = CampaignHandler::prologVideoName(index); ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); - ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier); + ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier)); } return ret; }; @@ -428,7 +428,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader } ret.regionColor = reader.readUInt8(); ret.difficulty = reader.readUInt8(); - ret.regionText = readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region"); + ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); diff --git a/lib/campaign/CampaignScenarioPrologEpilog.h b/lib/campaign/CampaignScenarioPrologEpilog.h index ab67cc584..64611be70 100644 --- a/lib/campaign/CampaignScenarioPrologEpilog.h +++ b/lib/campaign/CampaignScenarioPrologEpilog.h @@ -10,6 +10,7 @@ #pragma once #include "../filesystem/ResourcePath.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN @@ -19,7 +20,7 @@ struct DLL_LINKAGE CampaignScenarioPrologEpilog VideoPath prologVideo; AudioPath prologMusic; // from CmpMusic.txt AudioPath prologVoice; - std::string prologText; + MetaString prologText; template void serialize(Handler &h, const int formatVersion) { diff --git a/lib/campaign/CampaignState.h b/lib/campaign/CampaignState.h index fc5f1cbdf..cbe133467 100644 --- a/lib/campaign/CampaignState.h +++ b/lib/campaign/CampaignState.h @@ -182,7 +182,7 @@ struct DLL_LINKAGE CampaignScenario ui8 regionColor = 0; ui8 difficulty = 0; - std::string regionText; + MetaString regionText; CampaignScenarioPrologEpilog prolog; CampaignScenarioPrologEpilog epilog; From 3c2549d905460264af856a93cdcf9f26bf854c2f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 02:51:44 +0200 Subject: [PATCH 0650/1248] Fix town name --- lib/mapObjects/CGTownInstance.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index bf8b25124..7c9d97a7b 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -327,7 +327,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const } else { - logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), nameTextId); + logGlobal->error("%s visits allied town of %s from different pos?", h->getNameTranslated(), getNameTranslated()); } } @@ -337,15 +337,15 @@ void CGTownInstance::onHeroLeave(const CGHeroInstance * h) const if(visitingHero == h) { cb->stopHeroVisitCastle(this, h); - logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), nameTextId); + logGlobal->trace("%s correctly left town %s", h->getNameTranslated(), getNameTranslated()); } else - logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), nameTextId); + logGlobal->warn("Warning, %s tries to leave the town %s but hero is not inside.", h->getNameTranslated(), getNameTranslated()); } std::string CGTownInstance::getObjectName() const { - return nameTextId + ", " + town->faction->getNameTranslated(); + return getNameTranslated() + ", " + town->faction->getNameTranslated(); } bool CGTownInstance::townEnvisagesBuilding(BuildingSubID::EBuildingSubID subId) const @@ -767,7 +767,7 @@ void CGTownInstance::updateAppearance() std::string CGTownInstance::nodeName() const { - return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + nameTextId; + return "Town (" + (town ? town->faction->getNameTranslated() : "unknown") + ") of " + getNameTranslated(); } void CGTownInstance::deserializationFix() @@ -980,7 +980,7 @@ TResources CGTownInstance::getBuildingCost(const BuildingID & buildingID) const return town->buildings.at(buildingID)->resources; else { - logGlobal->error("Town %s at %s has no possible building %d!", nameTextId, pos.toString(), buildingID.toEnum()); + logGlobal->error("Town %s at %s has no possible building %d!", getNameTranslated(), pos.toString(), buildingID.toEnum()); return TResources(); } From 40af83a55cde1877f8648cf3985c6592b8aa8c06 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 02:51:58 +0200 Subject: [PATCH 0651/1248] Fix quest --- lib/mapObjects/CQuest.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index bf0b3f833..bb0f69186 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -632,10 +632,13 @@ void CQuest::addReplacements(MetaString &out, const std::string &base) const switch(missionType) { case MISSION_KILL_CREATURE: - out.replaceCreatureName(stackToKill); - if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster + if(stackToKill.type) { - out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); + out.replaceCreatureName(stackToKill); + if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster + { + out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); + } } break; case MISSION_KILL_HERO: From 909812668478452a4d6654f1b08e64863ef19f92 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 02:52:49 +0200 Subject: [PATCH 0652/1248] Separate map text identifiers from global --- lib/CGeneralTextHandler.cpp | 115 ++++++++++++++++++++--------------- lib/CGeneralTextHandler.h | 81 ++++++++++++++++++------ lib/mapping/CMapHeader.cpp | 6 ++ lib/mapping/CMapHeader.h | 6 +- lib/mapping/MapFormatH3M.cpp | 2 +- 5 files changed, 141 insertions(+), 69 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 3e54d7cf7..6188a7941 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -247,22 +247,38 @@ bool CLegacyConfigParser::endLine() return curr < end; } -void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) +void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) { - CLegacyConfigParser parser(TextPath::builtin(sourceName)); - size_t index = 0; - do - { - registerString( "core", {sourceID, index}, parser.readString()); - index += 1; - } - while (parser.endLine()); + assert(!modContext.empty()); + assert(!language.empty()); + + // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment + auto & entry = stringsLocalizations[UID.get()]; + + entry.overrideLanguage = language; + entry.overrideValue = localized; + if (entry.modContext.empty()) + entry.modContext = modContext; } -const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & identifier) const +void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) +{ + subContainers.insert(&container); +} + +void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) +{ + subContainers.erase(&container); +} + +const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const { if(stringsLocalizations.count(identifier.get()) == 0) { + for(const auto * container : subContainers) + if(container->identifierExists(identifier)) + return container->deserialize(identifier); + logGlobal->error("Unable to find localization for string '%s'", identifier.get()); return identifier.get(); } @@ -274,7 +290,7 @@ const std::string & CGeneralTextHandler::deserialize(const TextIdentifier & iden return entry.baseValue; } -void CGeneralTextHandler::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) { assert(!modContext.empty()); assert(!getModLanguage(modContext).empty()); @@ -307,21 +323,7 @@ void CGeneralTextHandler::registerString(const std::string & modContext, const T } } -void CGeneralTextHandler::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) -{ - assert(!modContext.empty()); - assert(!language.empty()); - - // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment - auto & entry = stringsLocalizations[UID.get()]; - - entry.overrideLanguage = language; - entry.overrideValue = localized; - if (entry.modContext.empty()) - entry.modContext = modContext; -} - -bool CGeneralTextHandler::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const +bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const { bool allPresent = true; @@ -372,12 +374,50 @@ bool CGeneralTextHandler::validateTranslation(const std::string & language, cons return allPresent && allFound; } -void CGeneralTextHandler::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config) +void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config) { for(const auto & node : config.Struct()) registerStringOverride(modContext, language, node.first, node.second.String()); } +bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const +{ + return stringsLocalizations.count(UID.get()); +} + +void TextLocalizationContainer::dumpAllTexts() +{ + logGlobal->info("BEGIN TEXT EXPORT"); + for(const auto & entry : stringsLocalizations) + { + if (!entry.second.overrideValue.empty()) + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); + else + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); + } + + logGlobal->info("END TEXT EXPORT"); +} + +std::string TextLocalizationContainer::getModLanguage(const std::string & modContext) +{ + if (modContext == "core") + return CGeneralTextHandler::getInstalledLanguage(); + return VLC->modh->getModLanguage(modContext); +} + +void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) +{ + CLegacyConfigParser parser(TextPath::builtin(sourceName)); + size_t index = 0; + do + { + registerString( "core", {sourceID, index}, parser.readString()); + index += 1; + } + while (parser.endLine()); +} + CGeneralTextHandler::CGeneralTextHandler(): victoryConditions(*this, "core.vcdesc" ), lossCondtions (*this, "core.lcdesc" ), @@ -591,20 +631,6 @@ int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t c return textIndex + 1; } -void CGeneralTextHandler::dumpAllTexts() -{ - logGlobal->info("BEGIN TEXT EXPORT"); - for(const auto & entry : stringsLocalizations) - { - if (!entry.second.overrideValue.empty()) - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); - else - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); - } - - logGlobal->info("END TEXT EXPORT"); -} - size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const { assert(campaignID < scenariosCountPerCampaign.size()); @@ -614,13 +640,6 @@ size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const return 0; } -std::string CGeneralTextHandler::getModLanguage(const std::string & modContext) -{ - if (modContext == "core") - return getInstalledLanguage(); - return VLC->modh->getModLanguage(modContext); -} - std::string CGeneralTextHandler::getPreferredLanguage() { assert(!settings["general"]["language"].String().empty()); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 6bbbc98c1..82b68e66b 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -113,9 +113,9 @@ public: {} }; -/// Handles all text-related data in game -class DLL_LINKAGE CGeneralTextHandler +class DLL_LINKAGE TextLocalizationContainer { +protected: struct StringState { /// Human-readable string that was added on registration @@ -132,21 +132,26 @@ class DLL_LINKAGE CGeneralTextHandler /// ID of mod that created this string std::string modContext; + + template + void serialize(Handler & h, const int Version) + { + h & baseValue; + h & baseLanguage; + h & modContext; + } }; - + /// map identifier -> localization std::unordered_map stringsLocalizations; - - void readToVector(const std::string & sourceID, const std::string & sourceName); - - /// number of scenarios in specific campaign. TODO: move to a better location - std::vector scenariosCountPerCampaign; - - std::string getModLanguage(const std::string & modContext); - + + std::set subContainers; + /// add selected string to internal storage as high-priority strings void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); - + + std::string getModLanguage(const std::string & modContext); + public: /// validates translation of specified language for specified mod /// returns true if localization is valid and complete @@ -157,13 +162,12 @@ public: /// Any entries loaded by this will have priority over texts registered normally void loadTranslationOverrides(const std::string & language, const std::string & modContext, JsonNode const & file); + // returns true if identifier with such name was registered, even if not translated to current language + bool identifierExists(const TextIdentifier & UID) const; + /// add selected string to internal storage void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); - - // returns true if identifier with such name was registered, even if not translated to current language - // not required right now, can be added if necessary - // bool identifierExists( const std::string identifier) const; - + /// returns translated version of a string that can be displayed to user template std::string translate(std::string arg1, Args ... args) const @@ -174,10 +178,51 @@ public: /// converts identifier into user-readable string const std::string & deserialize(const TextIdentifier & identifier) const; - + /// Debug method, dumps all currently known texts into console using Json-like format void dumpAllTexts(); + + /// Add or override subcontainer which can store identifiers + void addSubContainer(const TextLocalizationContainer & container); + + /// Remove subcontainer with give name + void removeSubContainer(const TextLocalizationContainer & container); + + template + void serialize(Handler & h, const int Version) + { + std::string key; + auto sz = stringsLocalizations.size(); + h & sz; + if(h.saving) + { + for(auto s : stringsLocalizations) + { + key = s.first; + h & key; + h & s.second; + } + } + else + { + for(size_t i = 0; i < sz; ++i) + { + h & key; + h & stringsLocalizations[key]; + } + } + } +}; +/// Handles all text-related data in game +class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer +{ + void readToVector(const std::string & sourceID, const std::string & sourceName); + + /// number of scenarios in specific campaign. TODO: move to a better location + std::vector scenariosCountPerCampaign; + +public: LegacyTextContainer allTexts; LegacyTextContainer arraytxt; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 9d7cd718a..345974bc2 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -127,6 +127,12 @@ CMapHeader::CMapHeader() : version(EMapFormat::VCMI), height(72), width(72), setupEvents(); allowedHeroes = VLC->heroh->getDefaultAllowed(); players.resize(PlayerColor::PLAYER_LIMIT_I); + VLC->generaltexth->addSubContainer(*this); +} + +CMapHeader::~CMapHeader() +{ + VLC->generaltexth->removeSubContainer(*this); } ui8 CMapHeader::levels() const diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 9866e095d..5250d575b 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -14,6 +14,7 @@ #include "../LogicalExpression.h" #include "../int3.h" #include "../MetaString.h" +#include "../CGeneralTextHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -199,7 +200,7 @@ struct DLL_LINKAGE TriggeredEvent }; /// The map header holds information about loss/victory condition,map format, version, players, height, width,... -class DLL_LINKAGE CMapHeader +class DLL_LINKAGE CMapHeader: public TextLocalizationContainer { void setupEvents(); public: @@ -213,7 +214,7 @@ public: static const int MAP_SIZE_GIANT = 252; CMapHeader(); - virtual ~CMapHeader() = default; + virtual ~CMapHeader(); ui8 levels() const; @@ -248,6 +249,7 @@ public: template void serialize(Handler & h, const int Version) { + h & static_cast(*this); h & version; h & mods; h & name; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index a9e42b9db..5b44974ef 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2274,7 +2274,7 @@ std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIden if(mapString.empty()) return ""; - VLC->generaltexth->registerString(modName, fullIdentifier, mapString); + mapHeader->registerString(modName, fullIdentifier, mapString); return fullIdentifier.get(); } From 65f696b01896e40108ea50c5b4ecf4fd8a5a3bb5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 03:02:46 +0200 Subject: [PATCH 0653/1248] Cosmetic changes for json map reader --- lib/mapping/MapFormatJson.cpp | 13 +++++++------ lib/mapping/MapFormatJson.h | 1 + 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index b6da8d13b..6f739eb1c 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -346,6 +346,7 @@ const int CMapFormatJson::VERSION_MINOR = 3; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; +const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"}; CMapFormatJson::CMapFormatJson(): fileVersionMajor(0), fileVersionMinor(0), @@ -424,10 +425,10 @@ void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes); -// handler.serializeString("victoryString", mapHeader->victoryMessage); + handler.serializeStruct("victoryMessage", mapHeader->victoryMessage); handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); -// handler.serializeString("defeatString", mapHeader->defeatMessage); + handler.serializeStruct("defeatMessage", mapHeader->defeatMessage); handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); } @@ -1122,12 +1123,12 @@ void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index) void CMapLoaderJson::readTerrain() { { - const JsonNode surface = getFromArchive("surface_terrain.json"); + const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]); readTerrainLevel(surface, 0); } if(map->twoLevel) { - const JsonNode underground = getFromArchive("underground_terrain.json"); + const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]); readTerrainLevel(underground, 1); } @@ -1388,12 +1389,12 @@ void CMapSaverJson::writeTerrain() //todo: multilevel map save support JsonNode surface = writeTerrainLevel(0); - addToArchive(surface, "surface_terrain.json"); + addToArchive(surface, TERRAIN_FILE_NAMES[0]); if(map->twoLevel) { JsonNode underground = writeTerrainLevel(1); - addToArchive(underground, "underground_terrain.json"); + addToArchive(underground, TERRAIN_FILE_NAMES[1]); } } diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index d23d06be0..af89f89eb 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -42,6 +42,7 @@ public: static const std::string HEADER_FILE_NAME; static const std::string OBJECTS_FILE_NAME; + static const std::string TERRAIN_FILE_NAMES[2]; int fileVersionMajor; int fileVersionMinor; From ba1dbbbb1d6c6c1d411b0f179f47c90895bff90d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 04:23:14 +0200 Subject: [PATCH 0654/1248] New version for map format --- lib/CGeneralTextHandler.cpp | 25 +++++++++++--- lib/CGeneralTextHandler.h | 4 +++ lib/mapping/CMapHeader.h | 3 ++ lib/mapping/MapFormatJson.cpp | 65 +++++++++++++++++++++++++++++++++-- lib/mapping/MapFormatJson.h | 11 ++++++ 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 6188a7941..360b69157 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -11,6 +11,7 @@ #include "CGeneralTextHandler.h" #include "filesystem/Filesystem.h" +#include "serializer/JsonSerializeFormat.h" #include "CConfigHandler.h" #include "GameSettings.h" #include "mapObjects/CQuest.h" @@ -290,10 +291,10 @@ const std::string & TextLocalizationContainer::deserialize(const TextIdentifier return entry.baseValue; } -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) { assert(!modContext.empty()); - assert(!getModLanguage(modContext).empty()); + assert(!Languages::getLanguageOptions(language).identifier.empty()); assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string //assert(stringsLocalizations.count(UID.get()) == 0); // registering already registered string? @@ -303,7 +304,7 @@ void TextLocalizationContainer::registerString(const std::string & modContext, c if(value.baseLanguage.empty()) { - value.baseLanguage = getModLanguage(modContext); + value.baseLanguage = language; value.baseValue = localized; } else @@ -315,7 +316,7 @@ void TextLocalizationContainer::registerString(const std::string & modContext, c else { StringState result; - result.baseLanguage = getModLanguage(modContext); + result.baseLanguage = language; result.baseValue = localized; result.modContext = modContext; @@ -323,6 +324,12 @@ void TextLocalizationContainer::registerString(const std::string & modContext, c } } +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) +{ + assert(!getModLanguage(modContext).empty()); + registerString(modContext, UID, localized, getModLanguage(modContext)); +} + bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const { bool allPresent = true; @@ -406,6 +413,16 @@ std::string TextLocalizationContainer::getModLanguage(const std::string & modCon return VLC->modh->getModLanguage(modContext); } +void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const +{ + for(auto & s : stringsLocalizations) + { + dest.Struct()[s.first].String() = s.second.baseValue; + if(!s.second.overrideValue.empty()) + dest.Struct()[s.first].String() = s.second.overrideValue; + } +} + void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) { CLegacyConfigParser parser(TextPath::builtin(sourceName)); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 82b68e66b..3807ddc7a 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -15,6 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; class JsonNode; +class JsonSerializeFormat; /// Parser for any text files from H3 class DLL_LINKAGE CLegacyConfigParser @@ -167,6 +168,7 @@ public: /// add selected string to internal storage void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); + void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); /// returns translated version of a string that can be displayed to user template @@ -188,6 +190,8 @@ public: /// Remove subcontainer with give name void removeSubContainer(const TextLocalizationContainer & container); + void jsonSerialize(JsonNode & dest) const; + template void serialize(Handler & h, const int Version) { diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 5250d575b..737eb98e0 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -270,6 +270,9 @@ public: h & defeatMessage; h & defeatIconIndex; } + + /// do not serialize, used only in map editor to write translations properly + JsonNode mapEditorTranslations; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 6f739eb1c..c0dafdeec 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -35,6 +35,7 @@ #include "../constants/StringConstants.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" +#include "../Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -341,8 +342,8 @@ namespace TerrainDetail } ///CMapFormatJson -const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 3; +const int CMapFormatJson::VERSION_MAJOR = 2; +const int CMapFormatJson::VERSION_MINOR = 0; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; @@ -906,6 +907,11 @@ std::unique_ptr CMapLoaderJson::loadMapHeader() return result; } +bool CMapLoaderJson::isExistArchive(const std::string & archiveFilename) +{ + return loader.existsResource(JsonPath::builtin(archiveFilename)); +} + JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) { JsonPath resource = JsonPath::builtin(archiveFilename); @@ -938,7 +944,7 @@ void CMapLoaderJson::readHeader(const bool complete) fileVersionMajor = static_cast(header["versionMajor"].Integer()); - if(fileVersionMajor != VERSION_MAJOR) + if(fileVersionMajor > VERSION_MAJOR) { logGlobal->error("Unsupported map format version: %d", fileVersionMajor); throw std::runtime_error("Unsupported map format version"); @@ -998,6 +1004,8 @@ void CMapLoaderJson::readHeader(const bool complete) if(complete) readOptions(handler); + + readTranslations(); } void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile) @@ -1259,6 +1267,36 @@ void CMapLoaderJson::readObjects() }); } +void CMapLoaderJson::readTranslations() +{ + auto language = CGeneralTextHandler::getPreferredLanguage(); + JsonNode data; + + if(!isExistArchive(language + ".json")) + { + //english is preferrable + language = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; + std::list options{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; + while(!isExistArchive(language + ".json") && !options.empty()) + { + language = options.front().identifier; + options.pop_front(); + } + + if(!isExistArchive(language + ".json")) + { + logGlobal->info("Map doesn't have any translation"); + return; + } + } + + data = getFromArchive(language + ".json"); + + for(auto & s : data.Struct()) + mapHeader->registerString("map", TextIdentifier(s.first), s.second.String(), language); +} + + ///CMapSaverJson CMapSaverJson::CMapSaverJson(CInputOutputStream * stream) : buffer(stream) @@ -1340,6 +1378,12 @@ void CMapSaverJson::writeHeader() writeTeams(handler); writeOptions(handler); + + for(auto & s : mapHeader->mapEditorTranslations.Struct()) + { + mapHeader->loadTranslationOverrides(s.first, "map", s.second); + writeTranslations(s.first); + } addToArchive(header, HEADER_FILE_NAME); } @@ -1440,5 +1484,20 @@ void CMapSaverJson::writeObjects() addToArchive(data, OBJECTS_FILE_NAME); } +void CMapSaverJson::writeTranslations(const std::string & language) +{ + if(Languages::getLanguageOptions(language).identifier.empty()) + { + logGlobal->error("Serializing of unsupported language %s is not permitted", language); + return; + } + + logGlobal->trace("Saving translations, language: %s", language); + JsonNode data(JsonNode::JsonType::DATA_STRUCT); + + mapHeader->jsonSerialize(data); + + addToArchive(data, language + ".json"); +} VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index af89f89eb..8f0a6a39f 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -202,6 +202,11 @@ public: * Reads complete map. */ void readMap(); + + /** + * Reads texts and translations + */ + void readTranslations(); static void readTerrainTile(const std::string & src, TerrainTile & tile); @@ -214,6 +219,7 @@ public: */ void readObjects(); + bool isExistArchive(const std::string & archiveFilename); JsonNode getFromArchive(const std::string & archiveFilename); private: @@ -249,6 +255,11 @@ public: * Saves header to zip archive */ void writeHeader(); + + /** + * Saves texts and translations to zip archive + */ + void writeTranslations(const std::string & language); /** * Encodes one tile into string From de1bb1be19c82c1bc853e612149fb3b27dcc2e0d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 04:33:33 +0200 Subject: [PATCH 0655/1248] Disable map editor --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d711ceec9..c82b84508 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,7 +52,7 @@ option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF) option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF) if(NOT ANDROID) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) - option(ENABLE_EDITOR "Enable compilation of map editor" ON) +# option(ENABLE_EDITOR "Enable compilation of map editor" ON) disable map editor until it is ready endif() option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON) option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON) From 67e120044810b17e434249fa518b4620a4af0d35 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 12:46:28 +0200 Subject: [PATCH 0656/1248] Remove forward declaration --- lib/IGameCallback.h | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 1c5c7b4b9..cdbb40209 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -20,7 +20,6 @@ struct SetMovePoints; struct GiveBonus; struct BlockingDialog; struct TeleportDialog; -class MetaString; struct StackLocation; struct ArtifactLocation; class CCreatureSet; From a710c88b07e97508a1be8d8415a739b4e8570204 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:09:01 +0200 Subject: [PATCH 0657/1248] Proper map translations loading --- lib/mapping/CMapHeader.cpp | 28 ++++++++++++++++++ lib/mapping/CMapHeader.h | 11 +++++-- lib/mapping/MapFormatJson.cpp | 56 ++++++++++------------------------- lib/mapping/MapFormatJson.h | 2 +- 4 files changed, 53 insertions(+), 44 deletions(-) diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 345974bc2..cd3193f41 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -16,6 +16,7 @@ #include "../CTownHandler.h" #include "../CGeneralTextHandler.h" #include "../CHeroHandler.h" +#include "../Languages.h" VCMI_LIB_NAMESPACE_BEGIN @@ -140,4 +141,31 @@ ui8 CMapHeader::levels() const return (twoLevel ? 2 : 1); } +void CMapHeader::registerMapStrings() +{ + auto language = CGeneralTextHandler::getPreferredLanguage(); + JsonNode data; + + if(translations[language].isNull()) + { + //english is preferrable + language = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; + std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; + while(translations[language].isNull() && !languages.empty()) + { + language = languages.front().identifier; + languages.pop_front(); + } + + if(!translations[language].isNull()) + { + logGlobal->info("Map %s doesn't have any translation", name.toString()); + return; + } + } + + for(auto & s : translations[language].Struct()) + registerString("map", TextIdentifier(s.first), s.second.String(), language); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 737eb98e0..9f08747f2 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -245,6 +245,11 @@ public: /// "main quests" of the map that describe victory and loss conditions std::vector triggeredEvents; + + /// translations for map to be transferred over network + JsonNode translations; + + void registerMapStrings(); template void serialize(Handler & h, const int Version) @@ -269,10 +274,10 @@ public: h & victoryIconIndex; h & defeatMessage; h & defeatIconIndex; + h & translations; + if(!h.saving) + registerMapStrings(); } - - /// do not serialize, used only in map editor to write translations properly - JsonNode mapEditorTranslations; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c0dafdeec..5ab9f6297 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1269,31 +1269,12 @@ void CMapLoaderJson::readObjects() void CMapLoaderJson::readTranslations() { - auto language = CGeneralTextHandler::getPreferredLanguage(); - JsonNode data; - - if(!isExistArchive(language + ".json")) + std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; + for(auto & language : Languages::getLanguageList()) { - //english is preferrable - language = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; - std::list options{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; - while(!isExistArchive(language + ".json") && !options.empty()) - { - language = options.front().identifier; - options.pop_front(); - } - - if(!isExistArchive(language + ".json")) - { - logGlobal->info("Map doesn't have any translation"); - return; - } + if(isExistArchive(language.identifier + ".json")) + mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json"); } - - data = getFromArchive(language + ".json"); - - for(auto & s : data.Struct()) - mapHeader->registerString("map", TextIdentifier(s.first), s.second.String(), language); } @@ -1378,12 +1359,8 @@ void CMapSaverJson::writeHeader() writeTeams(handler); writeOptions(handler); - - for(auto & s : mapHeader->mapEditorTranslations.Struct()) - { - mapHeader->loadTranslationOverrides(s.first, "map", s.second); - writeTranslations(s.first); - } + + writeTranslations(); addToArchive(header, HEADER_FILE_NAME); } @@ -1484,20 +1461,19 @@ void CMapSaverJson::writeObjects() addToArchive(data, OBJECTS_FILE_NAME); } -void CMapSaverJson::writeTranslations(const std::string & language) +void CMapSaverJson::writeTranslations() { - if(Languages::getLanguageOptions(language).identifier.empty()) + for(auto & s : mapHeader->translations.Struct()) { - logGlobal->error("Serializing of unsupported language %s is not permitted", language); - return; + auto & language = s.first; + if(Languages::getLanguageOptions(language).identifier.empty()) + { + logGlobal->error("Serializing of unsupported language %s is not permitted", language); + continue;; + } + logGlobal->trace("Saving translations, language: %s", language); + addToArchive(s.second, language + ".json"); } - - logGlobal->trace("Saving translations, language: %s", language); - JsonNode data(JsonNode::JsonType::DATA_STRUCT); - - mapHeader->jsonSerialize(data); - - addToArchive(data, language + ".json"); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 8f0a6a39f..9f5ee3e36 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -259,7 +259,7 @@ public: /** * Saves texts and translations to zip archive */ - void writeTranslations(const std::string & language); + void writeTranslations(); /** * Encodes one tile into string From d6616008587f520e39be07dfc7722fe28d5efc9c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:14:23 +0200 Subject: [PATCH 0658/1248] Add header --- lib/pathfinder/CGPathNode.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pathfinder/CGPathNode.cpp b/lib/pathfinder/CGPathNode.cpp index 057161295..701c2f5bb 100644 --- a/lib/pathfinder/CGPathNode.cpp +++ b/lib/pathfinder/CGPathNode.cpp @@ -15,6 +15,7 @@ #include "../gameState/CGameState.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapping/CMapDefines.h" +#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN From 1dbecc2e186f8b7900eaaba5e90c9aadb36685e0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:28:04 +0200 Subject: [PATCH 0659/1248] Revert "Add header" This reverts commit d6616008587f520e39be07dfc7722fe28d5efc9c. --- lib/pathfinder/CGPathNode.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pathfinder/CGPathNode.cpp b/lib/pathfinder/CGPathNode.cpp index 701c2f5bb..057161295 100644 --- a/lib/pathfinder/CGPathNode.cpp +++ b/lib/pathfinder/CGPathNode.cpp @@ -15,7 +15,6 @@ #include "../gameState/CGameState.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapping/CMapDefines.h" -#include "../MetaString.h" VCMI_LIB_NAMESPACE_BEGIN From 9df85192c92c8482b45e0852c97c25a67973bc75 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:28:15 +0200 Subject: [PATCH 0660/1248] Revert "Disable map editor" This reverts commit de1bb1be19c82c1bc853e612149fb3b27dcc2e0d. --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index c82b84508..d711ceec9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,7 +52,7 @@ option(ENABLE_ERM "Enable compilation of ERM scripting module" OFF) option(ENABLE_LUA "Enable compilation of LUA scripting module" OFF) if(NOT ANDROID) option(ENABLE_LAUNCHER "Enable compilation of launcher" ON) -# option(ENABLE_EDITOR "Enable compilation of map editor" ON) disable map editor until it is ready + option(ENABLE_EDITOR "Enable compilation of map editor" ON) endif() option(ENABLE_TRANSLATIONS "Enable generation of translations for launcher and editor" ON) option(ENABLE_NULLKILLER_AI "Enable compilation of Nullkiller AI library" ON) From 56eefab2551004e54b526ec761d110ef4b8e8ed5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:28:24 +0200 Subject: [PATCH 0661/1248] Fix headers --- lib/mapping/CMapDefines.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index 250e2eab4..f36a0889e 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -10,11 +10,11 @@ #pragma once -VCMI_LIB_NAMESPACE_BEGIN - #include "../ResourceSet.h" #include "../MetaString.h" +VCMI_LIB_NAMESPACE_BEGIN + class TerrainType; class RiverType; class RoadType; From 232b3e8cf63e7a62180f4ba102c686c7eba5568b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 13:28:37 +0200 Subject: [PATCH 0662/1248] Compile map editor --- mapeditor/inspector/inspector.cpp | 19 ++++++++++++------- mapeditor/inspector/inspector.h | 2 ++ mapeditor/mapsettings/eventsettings.cpp | 4 ++-- mapeditor/mapsettings/generalsettings.cpp | 8 ++++---- mapeditor/mapsettings/rumorsettings.cpp | 4 ++-- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 8acaa91ae..35d2d6407 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message.appendRawString(value.toString().toStdString()); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message.appendRawString(value.toString().toStdString()); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message.appendRawString(value.toString().toStdString()); if(o->storedArtifact && key == "Spell") { @@ -643,7 +643,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = value.toString().toStdString(); + o->message.appendRawString(value.toString().toStdString()); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -661,11 +661,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText = value.toString().toStdString(); + o->quest->firstVisitText.appendRawString(value.toString().toStdString()); if(key == "Next visit text") - o->quest->nextVisitText = value.toString().toStdString(); + o->quest->nextVisitText.appendRawString(value.toString().toStdString()); if(key == "Completed text") - o->quest->completedText = value.toString().toStdString(); + o->quest->completedText.appendRawString(value.toString().toStdString()); } @@ -713,6 +713,11 @@ QTableWidgetItem * Inspector::addProperty(const std::string & value) return addProperty(QString::fromStdString(value)); } +QTableWidgetItem * Inspector::addProperty(const MetaString & value) +{ + return addProperty(value.toString()); +} + QTableWidgetItem * Inspector::addProperty(const QString & value) { auto * item = new QTableWidgetItem(value); diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 1e44964ca..e6409d190 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -19,6 +19,7 @@ #include "../lib/mapObjects/MapObjects.h" #include "../lib/mapObjects/CRewardableObject.h" #include "../lib/ResourceSet.h" +#include "../lib/MetaString.h" #define DECLARE_OBJ_TYPE(x) void initialize(x*); #define DECLARE_OBJ_PROPERTY_METHODS(x) \ @@ -83,6 +84,7 @@ protected: //===============DECLARE PROPERTY VALUE TYPE============================== QTableWidgetItem * addProperty(unsigned int value); QTableWidgetItem * addProperty(int value); + QTableWidgetItem * addProperty(const MetaString & value); QTableWidgetItem * addProperty(const std::string & value); QTableWidgetItem * addProperty(const QString & value); QTableWidgetItem * addProperty(const int3 & value); diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 6892d2903..047c9e1ae 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -37,7 +37,7 @@ QVariant toVariant(const CMapEvent & event) { QVariantMap result; result["name"] = QString::fromStdString(event.name); - result["message"] = QString::fromStdString(event.message); + result["message"] = QString::fromStdString(event.message.toString()); result["players"] = QVariant::fromValue(event.players); result["humanAffected"] = QVariant::fromValue(event.humanAffected); result["computerAffected"] = QVariant::fromValue(event.computerAffected); @@ -52,7 +52,7 @@ CMapEvent eventFromVariant(const QVariant & variant) CMapEvent result; auto v = variant.toMap(); result.name = v.value("name").toString().toStdString(); - result.message = v.value("message").toString().toStdString(); + result.message.appendRawString(v.value("message").toString().toStdString()); result.players = v.value("players").toInt(); result.humanAffected = v.value("humanAffected").toInt(); result.computerAffected = v.value("computerAffected").toInt(); diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index 09ad221b3..12322f73b 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -27,8 +27,8 @@ GeneralSettings::~GeneralSettings() void GeneralSettings::initialize(MapController & c) { AbstractSettings::initialize(c); - ui->mapNameEdit->setText(tr(controller->map()->name.c_str())); - ui->mapDescriptionEdit->setPlainText(tr(controller->map()->description.c_str())); + ui->mapNameEdit->setText(QString::fromStdString(controller->map()->name.toString())); + ui->mapDescriptionEdit->setPlainText(QString::fromStdString(controller->map()->description.toString())); ui->heroLevelLimit->setValue(controller->map()->levelLimit); ui->heroLevelLimitCheck->setChecked(controller->map()->levelLimit); @@ -59,8 +59,8 @@ void GeneralSettings::initialize(MapController & c) void GeneralSettings::update() { - controller->map()->name = ui->mapNameEdit->text().toStdString(); - controller->map()->description = ui->mapDescriptionEdit->toPlainText().toStdString(); + controller->map()->name.appendRawString(ui->mapNameEdit->text().toStdString()); + controller->map()->description.appendRawString(ui->mapDescriptionEdit->toPlainText().toStdString()); if(ui->heroLevelLimitCheck->isChecked()) controller->map()->levelLimit = ui->heroLevelLimit->value(); else diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp index a91691539..da122be93 100644 --- a/mapeditor/mapsettings/rumorsettings.cpp +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -30,7 +30,7 @@ void RumorSettings::initialize(MapController & c) for(auto & rumor : controller->map()->rumors) { auto * item = new QListWidgetItem(QString::fromStdString(rumor.name)); - item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text))); + item->setData(Qt::UserRole, QVariant(QString::fromStdString(rumor.text.toString()))); item->setFlags(item->flags() | Qt::ItemIsEditable); ui->rumors->addItem(item); } @@ -43,7 +43,7 @@ void RumorSettings::update() { Rumor rumor; rumor.name = ui->rumors->item(i)->text().toStdString(); - rumor.text = ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString(); + rumor.text.appendRawString(ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString()); controller->map()->rumors.push_back(rumor); } } From 60df49236f49e2bb4908fd497b6ef977bc0891fa Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 14:03:58 +0200 Subject: [PATCH 0663/1248] Inspector widgets set string ID --- lib/CGeneralTextHandler.cpp | 23 ++++++-------------- mapeditor/inspector/inspector.cpp | 36 +++++++++++++++++++++---------- mapeditor/inspector/inspector.h | 4 ++++ 3 files changed, 36 insertions(+), 27 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 360b69157..09d046ef7 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -301,26 +301,17 @@ void TextLocalizationContainer::registerString(const std::string & modContext, c if(stringsLocalizations.count(UID.get()) > 0) { auto & value = stringsLocalizations[UID.get()]; - - if(value.baseLanguage.empty()) - { - value.baseLanguage = language; - value.baseValue = localized; - } - else - { - if(value.baseValue != localized) - logMod->warn("Duplicate registered string '%s' found! Old value: '%s', new value: '%s'", UID.get(), value.baseValue, localized); - } + value.baseLanguage = language; + value.baseValue = localized; } else { - StringState result; - result.baseLanguage = language; - result.baseValue = localized; - result.modContext = modContext; + StringState value; + value.baseLanguage = language; + value.baseValue = localized; + value.modContext = modContext; - stringsLocalizations[UID.get()] = result; + stringsLocalizations[UID.get()] = value; } } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 35d2d6407..c577770c0 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -277,8 +277,8 @@ void Inspector::updateProperties(CGHeroInstance * o) delegate->options = {{"MALE", QVariant::fromValue(int(EHeroGender::MALE))}, {"FEMALE", QVariant::fromValue(int(EHeroGender::FEMALE))}}; addProperty("Gender", (o->gender == EHeroGender::FEMALE ? "FEMALE" : "MALE"), delegate , false); } - addProperty("Name", o->nameCustomTextId, false); - addProperty("Biography", o->biographyCustomTextId, new MessageDelegate, false); + addProperty("Name", o->getNameTranslated(), false); + addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false); addProperty("Portrait", o->portrait, false); auto * delegate = new HeroSkillsDelegate(*o); @@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendRawString(value.toString().toStdString()); + o->message.appendTextID(mapWriteStringId(TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -553,7 +553,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(value.toString().toStdString()); + o->setNameTextId(mapWriteStringId(TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendRawString(value.toString().toStdString()); + o->message.appendTextID(mapWriteStringId(TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendRawString(value.toString().toStdString()); + o->message.appendTextID(mapWriteStringId(TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -606,7 +606,10 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = value.toString().toStdString(); + o->nameCustomTextId = mapWriteStringId(TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + + if(key == "Biography") + o->biographyCustomTextId = mapWriteStringId(TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -643,7 +646,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendRawString(value.toString().toStdString()); + o->message.appendTextID(mapWriteStringId(TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -661,11 +664,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText.appendRawString(value.toString().toStdString()); + o->quest->firstVisitText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText.appendRawString(value.toString().toStdString()); + o->quest->nextVisitText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText.appendRawString(value.toString().toStdString()); + o->quest->completedText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); } @@ -713,6 +716,11 @@ QTableWidgetItem * Inspector::addProperty(const std::string & value) return addProperty(QString::fromStdString(value)); } +QTableWidgetItem * Inspector::addProperty(const TextIdentifier & value) +{ + return addProperty(VLC->generaltexth->translate(value.get())); +} + QTableWidgetItem * Inspector::addProperty(const MetaString & value) { return addProperty(value.toString()); @@ -797,6 +805,12 @@ Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), { } +std::string Inspector::mapWriteStringId(const TextIdentifier & stringIdentifier, const std::string & localized) +{ + map->registerString("map", stringIdentifier, localized); + return stringIdentifier.get(); +} + /* * Delegates */ diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index e6409d190..3c097b5d1 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -18,6 +18,7 @@ #include "../lib/mapObjects/CGCreature.h" #include "../lib/mapObjects/MapObjects.h" #include "../lib/mapObjects/CRewardableObject.h" +#include "../lib/CGeneralTextHandler.h" #include "../lib/ResourceSet.h" #include "../lib/MetaString.h" @@ -85,6 +86,7 @@ protected: QTableWidgetItem * addProperty(unsigned int value); QTableWidgetItem * addProperty(int value); QTableWidgetItem * addProperty(const MetaString & value); + QTableWidgetItem * addProperty(const TextIdentifier & value); QTableWidgetItem * addProperty(const std::string & value); QTableWidgetItem * addProperty(const QString & value); QTableWidgetItem * addProperty(const int3 & value); @@ -146,6 +148,8 @@ protected: { addProperty(key, value, nullptr, restricted); } + + std::string mapWriteStringId(const TextIdentifier & stringIdentifier, const std::string & localized); protected: int row = 0; From 98fde9ab1ddc6b8ed37613f95296211d93ca0300 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 14:38:31 +0200 Subject: [PATCH 0664/1248] Add string IDs in map editor --- lib/mapping/CMapHeader.cpp | 12 ++++++++++++ lib/mapping/CMapHeader.h | 4 ++++ lib/modding/CModHandler.cpp | 4 +++- mapeditor/inspector/inspector.cpp | 26 ++++++++++---------------- mapeditor/inspector/inspector.h | 2 -- mapeditor/inspector/rewardswidget.cpp | 8 ++++---- mapeditor/inspector/rewardswidget.h | 8 ++++---- 7 files changed, 37 insertions(+), 27 deletions(-) diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index cd3193f41..fe0583731 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -168,4 +168,16 @@ void CMapHeader::registerMapStrings() registerString("map", TextIdentifier(s.first), s.second.String(), language); } +std::string mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) +{ + return mapRegisterLocalizedString(mapHeader, UID, localized, VLC->generaltexth->getPreferredLanguage()); +} + +std::string mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) +{ + mapHeader.registerString("map", UID, localized, language); + mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; + return UID.get(); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 9f08747f2..fc915c09c 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -280,4 +280,8 @@ public: } }; +/// wrapper functions to register string into the map and stores its translation +std::string DLL_LINKAGE mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized); +std::string DLL_LINKAGE mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language); + VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 12187990a..1414817f5 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -319,8 +319,10 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) std::string CModHandler::getModLanguage(const TModID& modId) const { - if ( modId == "core") + if(modId == "core") return VLC->generaltexth->getInstalledLanguage(); + if(modId == "map") + return VLC->generaltexth->getPreferredLanguage(); return allMods.at(modId).baseLanguage; } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index c577770c0..5f0e6ef54 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapWriteStringId(TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -553,7 +553,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(mapWriteStringId(TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + o->setNameTextId(mapRegisterLocalizedString(*map, TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapWriteStringId(TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapWriteStringId(TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -606,10 +606,10 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = mapWriteStringId(TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + o->nameCustomTextId = mapRegisterLocalizedString(*map, TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); if(key == "Biography") - o->biographyCustomTextId = mapWriteStringId(TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); + o->biographyCustomTextId = mapRegisterLocalizedString(*map, TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -646,7 +646,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapWriteStringId(TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -664,11 +664,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText.appendTextID(mapWriteStringId(TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); } @@ -805,12 +805,6 @@ Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), { } -std::string Inspector::mapWriteStringId(const TextIdentifier & stringIdentifier, const std::string & localized) -{ - map->registerString("map", stringIdentifier, localized); - return stringIdentifier.get(); -} - /* * Delegates */ diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 3c097b5d1..c31f41818 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -149,8 +149,6 @@ protected: addProperty(key, value, nullptr, restricted); } - std::string mapWriteStringId(const TextIdentifier & stringIdentifier, const std::string & localized); - protected: int row = 0; QTableWidget * table; diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index ffecec0e1..b8990b135 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -23,7 +23,7 @@ #include "../lib/mapObjects/CGPandoraBox.h" #include "../lib/mapObjects/CQuest.h" -RewardsWidget::RewardsWidget(const CMap & m, CRewardableObject & p, QWidget *parent) : +RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : QDialog(parent), map(m), object(p), @@ -211,7 +211,7 @@ bool RewardsWidget::commitChanges() if(ui->onSelectText->text().isEmpty()) object.configuration.onSelect.clear(); else - object.configuration.onSelect = MetaString::createFromRawString(ui->onSelectText->text().toStdString()); + object.configuration.onSelect = MetaString::createFromTextID(mapRegisterLocalizedString(map, TextIdentifier("reward", object.instanceName, "onSelect"), ui->onSelectText->text().toStdString())); object.configuration.canRefuse = ui->canRefuse->isChecked(); //reset parameters @@ -232,7 +232,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) if(ui->rewardMessage->text().isEmpty()) vinfo.message.clear(); else - vinfo.message = MetaString::createFromRawString(ui->rewardMessage->text().toStdString()); + vinfo.message = MetaString::createFromTextID(mapRegisterLocalizedString(map, TextIdentifier("reward", object.instanceName, "info", index, "message"), ui->rewardMessage->text().toStdString())); vinfo.reward.heroLevel = ui->rHeroLevel->value(); vinfo.reward.heroExperience = ui->rHeroExperience->value(); @@ -649,7 +649,7 @@ void RewardsDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, c } } -RewardsDelegate::RewardsDelegate(const CMap & m, CRewardableObject & t): map(m), object(t) +RewardsDelegate::RewardsDelegate(CMap & m, CRewardableObject & t): map(m), object(t) { } diff --git a/mapeditor/inspector/rewardswidget.h b/mapeditor/inspector/rewardswidget.h index 2a5958d9e..aeffe17e2 100644 --- a/mapeditor/inspector/rewardswidget.h +++ b/mapeditor/inspector/rewardswidget.h @@ -22,7 +22,7 @@ class RewardsWidget : public QDialog public: - explicit RewardsWidget(const CMap &, CRewardableObject &, QWidget *parent = nullptr); + explicit RewardsWidget(CMap &, CRewardableObject &, QWidget *parent = nullptr); ~RewardsWidget(); void obtainData(); @@ -64,14 +64,14 @@ private: Ui::RewardsWidget *ui; CRewardableObject & object; - const CMap & map; + CMap & map; }; class RewardsDelegate : public QStyledItemDelegate { Q_OBJECT public: - RewardsDelegate(const CMap &, CRewardableObject &); + RewardsDelegate(CMap &, CRewardableObject &); using QStyledItemDelegate::QStyledItemDelegate; @@ -82,5 +82,5 @@ public: private: CRewardableObject & object; - const CMap & map; + CMap & map; }; From dbf3cad796042bd50e8b0b2edd55a8b2fb25dad7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 14:49:47 +0200 Subject: [PATCH 0665/1248] Register all text IDs in map editor --- mapeditor/mapsettings/eventsettings.cpp | 6 +++--- mapeditor/mapsettings/generalsettings.cpp | 4 ++-- mapeditor/mapsettings/loseconditions.cpp | 4 ++-- mapeditor/mapsettings/rumorsettings.cpp | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 047c9e1ae..60cd97982 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -47,12 +47,12 @@ QVariant toVariant(const CMapEvent & event) return QVariant(result); } -CMapEvent eventFromVariant(const QVariant & variant) +CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant) { CMapEvent result; auto v = variant.toMap(); result.name = v.value("name").toString().toStdString(); - result.message.appendRawString(v.value("message").toString().toStdString()); + result.message.appendTextID(mapRegisterLocalizedString(mapHeader, TextIdentifier("header", "event", result.name, "message"), v.value("message").toString().toStdString())); result.players = v.value("players").toInt(); result.humanAffected = v.value("humanAffected").toInt(); result.computerAffected = v.value("computerAffected").toInt(); @@ -91,7 +91,7 @@ void EventSettings::update() for(int i = 0; i < ui->eventsList->count(); ++i) { const auto * item = ui->eventsList->item(i); - controller->map()->events.push_back(eventFromVariant(item->data(Qt::UserRole))); + controller->map()->events.push_back(eventFromVariant(*controller->map(), item->data(Qt::UserRole))); } } diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index 12322f73b..8f74867dc 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -59,8 +59,8 @@ void GeneralSettings::initialize(MapController & c) void GeneralSettings::update() { - controller->map()->name.appendRawString(ui->mapNameEdit->text().toStdString()); - controller->map()->description.appendRawString(ui->mapDescriptionEdit->toPlainText().toStdString()); + controller->map()->name.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); + controller->map()->description.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); if(ui->heroLevelLimitCheck->isChecked()) controller->map()->levelLimit = ui->heroLevelLimit->value(); else diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index 017ad9e78..c66c0f936 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -243,7 +243,7 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) loseTypeWidget = new QComboBox; ui->loseParamsLayout->addWidget(loseTypeWidget); for(int i : getObjectIndexes(*controller->map())) - loseTypeWidget->addItem(tr(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + loseTypeWidget->addItem(QString::fromStdString(getTownName(*controller->map(), i).c_str()), QVariant::fromValue(i)); pickObjectButton = new QToolButton; connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); ui->loseParamsLayout->addWidget(pickObjectButton); @@ -254,7 +254,7 @@ void LoseConditions::on_loseComboBox_currentIndexChanged(int index) loseTypeWidget = new QComboBox; ui->loseParamsLayout->addWidget(loseTypeWidget); for(int i : getObjectIndexes(*controller->map())) - loseTypeWidget->addItem(tr(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); + loseTypeWidget->addItem(QString::fromStdString(getHeroName(*controller->map(), i).c_str()), QVariant::fromValue(i)); pickObjectButton = new QToolButton; connect(pickObjectButton, &QToolButton::clicked, this, &LoseConditions::onObjectSelect); ui->loseParamsLayout->addWidget(pickObjectButton); diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp index da122be93..6527c67f1 100644 --- a/mapeditor/mapsettings/rumorsettings.cpp +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -43,7 +43,7 @@ void RumorSettings::update() { Rumor rumor; rumor.name = ui->rumors->item(i)->text().toStdString(); - rumor.text.appendRawString(ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString()); + rumor.text.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "rumor", i, "text"), ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString())); controller->map()->rumors.push_back(rumor); } } From db93b8eaa1e69d8f96b9ae4110de965d675eea8b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 15:29:43 +0200 Subject: [PATCH 0666/1248] UI draft for map translations --- mapeditor/CMakeLists.txt | 3 + mapeditor/icons/translations.png | Bin 0 -> 1864 bytes mapeditor/mainwindow.cpp | 9 +++ mapeditor/mainwindow.h | 2 + mapeditor/mainwindow.ui | 25 ++++++-- mapeditor/mapsettings/translations.cpp | 43 +++++++++++++ mapeditor/mapsettings/translations.h | 38 +++++++++++ mapeditor/mapsettings/translations.ui | 84 +++++++++++++++++++++++++ 8 files changed, 200 insertions(+), 4 deletions(-) create mode 100644 mapeditor/icons/translations.png create mode 100644 mapeditor/mapsettings/translations.cpp create mode 100644 mapeditor/mapsettings/translations.h create mode 100644 mapeditor/mapsettings/translations.ui diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 4041bcd1e..3a2c6191b 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -21,6 +21,7 @@ set(editor_SRCS mapsettings/loseconditions.cpp mapsettings/eventsettings.cpp mapsettings/rumorsettings.cpp + mapsettings/translations.cpp playersettings.cpp playerparams.cpp scenelayer.cpp @@ -58,6 +59,7 @@ set(editor_HEADERS mapsettings/loseconditions.h mapsettings/eventsettings.h mapsettings/rumorsettings.h + mapsettings/translations.h playersettings.h playerparams.h scenelayer.h @@ -85,6 +87,7 @@ set(editor_FORMS mapsettings/loseconditions.ui mapsettings/eventsettings.ui mapsettings/rumorsettings.ui + mapsettings/translations.ui playersettings.ui playerparams.ui validator.ui diff --git a/mapeditor/icons/translations.png b/mapeditor/icons/translations.png new file mode 100644 index 0000000000000000000000000000000000000000..0b031738683f700600f137a7ad8eea4deddda41d GIT binary patch literal 1864 zcmV-O2eBE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096103e_P00aO4 z0096103ZMW0056pK*<0A0un=0Jww8?A#Gota^B9I<)cA#c07pSP@Cm-B zu# zkqYSqO{`*6ge$ceCCr8o#%F9i1{JvjqI7g(R}7l3iB&|j^8e63^E33SDmbcAXl3ug zccJJG(6u^N&C2yq^7EFjb2 z9QcVZ>gj+5{d=&Vw#d~UGQ^2QrY@t-HEIzJF$1Ff4fvAaEEoY@XyJDOw{_C0b)hRZ z#RY3CcGzNtjXF0X?S{T1%3o$3^y^VuJh#}GDS8KD-*NH%mgWuStC!9C?Q0y}AD#P0 z*kHtYmJ;iNyU>s{sb4C$SsiclP<^uS?@jJ%G?6Av{;^2=Q_(^03(#%Xdq7`M*8+3; zvBRLYY$0h?y5TBHBvm1xoU6$Pq5afy{5=sKHRi^xjfLn!zp=sf5lT}p4>ZOdCFETw zactionMapSettings->setEnabled(true); ui->actionPlayers_settings->setEnabled(true); + ui->actionTranslations->setEnabled(true); //set minimal players count if(isNew) @@ -1239,3 +1241,10 @@ void MainWindow::on_actionExport_triggered() } } + +void MainWindow::on_actionTranslations_triggered() +{ + auto translationsDialog = new Translations(*controller.map(), this); + translationsDialog->show(); +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index f509b0882..3954ca173 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -117,6 +117,8 @@ private slots: void on_actionExport_triggered(); + void on_actionTranslations_triggered(); + public slots: void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected); diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 159a1ec98..6f7b0da5f 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -51,7 +51,7 @@ 0 0 1024 - 22 + 37 @@ -70,6 +70,7 @@ + @@ -140,6 +141,7 @@ + @@ -754,7 +756,7 @@ 0 0 128 - 251 + 236 @@ -797,7 +799,7 @@ 0 0 128 - 251 + 236 @@ -833,7 +835,7 @@ 0 0 128 - 251 + 236 @@ -1237,6 +1239,21 @@ Export as... + + + false + + + + icons:translations.pngicons:translations.png + + + Translations + + + Ctrl+T + + diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp new file mode 100644 index 000000000..e8f181551 --- /dev/null +++ b/mapeditor/mapsettings/translations.cpp @@ -0,0 +1,43 @@ +/* + * translations.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 "translations.h" +#include "ui_translations.h" + +Translations::Translations(CMapHeader & mh, QWidget *parent) : + QDialog(parent), + ui(new Ui::Translations), + mapHeader(mh) +{ + ui->setupUi(this); +} + +Translations::~Translations() +{ + delete ui; +} + +void Translations::on_languageSelect_currentIndexChanged(int index) +{ + +} + + +void Translations::on_supportedCheck_toggled(bool checked) +{ + +} + + +void Translations::on_translationsTable_itemChanged(QTableWidgetItem *item) +{ + +} + diff --git a/mapeditor/mapsettings/translations.h b/mapeditor/mapsettings/translations.h new file mode 100644 index 000000000..1444e6f25 --- /dev/null +++ b/mapeditor/mapsettings/translations.h @@ -0,0 +1,38 @@ +/* + * translations.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 +#include "../lib/mapping/CMapHeader.h" + +namespace Ui { +class Translations; +} + +class Translations : public QDialog +{ + Q_OBJECT + +public: + explicit Translations(CMapHeader & mapHeader, QWidget *parent = nullptr); + ~Translations(); + +private slots: + void on_languageSelect_currentIndexChanged(int index); + + void on_supportedCheck_toggled(bool checked); + + void on_translationsTable_itemChanged(QTableWidgetItem *item); + +private: + Ui::Translations *ui; + CMapHeader & mapHeader; +}; diff --git a/mapeditor/mapsettings/translations.ui b/mapeditor/mapsettings/translations.ui new file mode 100644 index 000000000..41b6b8578 --- /dev/null +++ b/mapeditor/mapsettings/translations.ui @@ -0,0 +1,84 @@ + + + Translations + + + + 0 + 0 + 989 + 641 + + + + Map translations + + + true + + + + + + + + + 0 + 0 + + + + Language + + + + + + + + 0 + 0 + + + + + + + + Suppported + + + + + + + + + 240 + + + true + + + false + + + 24 + + + + String ID + + + + + Text + + + + + + + + + From ab85e724eb0ba7c2d93537d2d694248a9a2aea85 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 16:20:21 +0200 Subject: [PATCH 0667/1248] Read-only UI logic for translations --- mapeditor/mapsettings/translations.cpp | 93 +++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 2 deletions(-) diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index e8f181551..6d4f6fc43 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -8,8 +8,12 @@ * */ +#include "StdInc.h" #include "translations.h" #include "ui_translations.h" +#include "../../lib/Languages.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/VCMI_Lib.h" Translations::Translations(CMapHeader & mh, QWidget *parent) : QDialog(parent), @@ -17,6 +21,15 @@ Translations::Translations(CMapHeader & mh, QWidget *parent) : mapHeader(mh) { ui->setupUi(this); + + //fill languages list + for(auto & language : Languages::getLanguageList()) + { + ui->languageSelect->addItem(QString("%1 (%2)").arg(QString::fromStdString(language.nameEnglish), QString::fromStdString(language.nameNative))); + ui->languageSelect->setItemData(ui->languageSelect->count() - 1, QVariant(QString::fromStdString(language.identifier))); + if(language.identifier == VLC->generaltexth->getPreferredLanguage()) + ui->languageSelect->setCurrentIndex(ui->languageSelect->count() - 1); + } } Translations::~Translations() @@ -26,13 +39,89 @@ Translations::~Translations() void Translations::on_languageSelect_currentIndexChanged(int index) { - + auto language = ui->languageSelect->currentData().toString().toStdString(); + auto & translation = mapHeader.translations[language]; + bool hasLanguage = !translation.isNull(); + ui->supportedCheck->blockSignals(true); + ui->supportedCheck->setChecked(hasLanguage); + ui->supportedCheck->blockSignals(false); + ui->translationsTable->setEnabled(hasLanguage); + if(hasLanguage) + { + ui->translationsTable->blockSignals(true); + ui->translationsTable->setRowCount(translation.Struct().size()); + int i = 0; + for(auto & s : translation.Struct()) + { + auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); + auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); + wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); + wText->setFlags(wId->flags() | Qt::ItemIsEditable); + ui->translationsTable->setItem(i, 0, wId); + ui->translationsTable->setItem(i++, 0, wText); + } + ui->translationsTable->blockSignals(false); + } } void Translations::on_supportedCheck_toggled(bool checked) { - + auto language = ui->languageSelect->currentData().toString().toStdString(); + auto & translation = mapHeader.translations[language]; + bool hasLanguage = !translation.isNull(); + bool hasRecord = !translation.Struct().empty(); + + if(checked) + { + if(!hasLanguage) + translation = JsonNode(JsonNode::JsonType::DATA_STRUCT); + + //copy from default language + translation = mapHeader.translations[VLC->generaltexth->getPreferredLanguage()]; + hasLanguage = !translation.isNull(); + + ui->translationsTable->blockSignals(true); + ui->translationsTable->setRowCount(translation.Struct().size()); + int i = 0; + for(auto & s : translation.Struct()) + { + auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); + auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); + wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); + wText->setFlags(wId->flags() | Qt::ItemIsEditable); + ui->translationsTable->setItem(i, 0, wId); + ui->translationsTable->setItem(i++, 0, wText); + } + ui->translationsTable->blockSignals(false); + ui->translationsTable->setEnabled(hasLanguage); + } + else + { + bool canRemove = language != VLC->generaltexth->getPreferredLanguage(); + if(!canRemove) + { + QMessageBox::information(this, tr("Remove translation"), tr("Default language cannot be removed")); + } + else if(hasRecord) + { + auto sure = QMessageBox::question(this, tr("Remove translation"), tr("This language has text records which will be removed. Continue?")); + canRemove = sure != QMessageBox::No; + } + + if(!canRemove) + { + ui->supportedCheck->blockSignals(true); + ui->supportedCheck->setChecked(true); + ui->supportedCheck->blockSignals(false); + return; + } + ui->translationsTable->blockSignals(true); + ui->translationsTable->clear(); + translation = JsonNode(); + ui->translationsTable->blockSignals(false); + ui->translationsTable->setEnabled(false); + } } From 38d12bbe8c2d3c3e62ae40f5610a67b7e72c9dd7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 22:48:52 +0200 Subject: [PATCH 0668/1248] Final changes for map editor translations --- mapeditor/icons/translations.png | Bin 1864 -> 2113 bytes mapeditor/mapsettings/translations.cpp | 84 +++++++++++++------------ mapeditor/mapsettings/translations.h | 2 + 3 files changed, 46 insertions(+), 40 deletions(-) diff --git a/mapeditor/icons/translations.png b/mapeditor/icons/translations.png index 0b031738683f700600f137a7ad8eea4deddda41d..1935db72fc3bfee1f6fafcbb6da3b6d95694351e 100644 GIT binary patch delta 975 zcmV;=12Fu^4#5zxfdqd8s!2paR9FecS4&8gQ52prp;b^&lz})1K~SJE5h%8;D0>)I zAoQeKw2F2`)J9rGMD&0%7lErbl}6OA3_)XPh=L-VN3}4Th+Ytt{e9o{oSEbJ|Ns1> zkq18Sx!*nKp7Wpk8e_3pmUwymd_I4toMXSCp<#<3|4%syz8HU#5}Scbz;mEJC^3x9 zNNVXFb@DK~X;(-lU5PxN0p9>m@CWTMa0;jgJ_El3uq`Y(_ymgE1G!wTKaog0g4H`< zy;wJ+B~*SwIRgwSf%cy0ok(E6;hh?#DsBnl@wkTx*3t`3Ph=t+T&UPnC=axQmeo>O z2j98U=|MZ9RSADhM2uF!;iljPT7t`=To-g!U@KtdH#$TI=RqaMWIkO+wk_m@x4uqy z6@bRcKgg(ZKMY5`qF@=9(1^hbg~EblGRdlXs6Z?ER4O$n9J@|xSsu*}`W~=Z8JmOl zDv;(l#)OX6AW$1DTa5KkI-NG%3DtcqlaP`5Q!M@$Ayt3qMm$|Vu5AHmma&R^z~(AG zIu|#&?+xpGWRUB`eQ;gCdYuD40z45_B>;Q9;6j7fFgTXrDC}_!hUY06P@zEo910!z$2Am{t z>@&nh=!<_$f68mhF$sPc>RW&@3ghGB!EB=nV8r%d_JwWpWVx+lJ#&4Uz*ga1HGtKp!i< zvi>3obYhe^ODl51bM4$!OI{>_HZeLndRGatOlZ?Zge{d#WTuK~OU|V5)92?E`ki=suoIp3I=^ums8sKi{J-`P{rB0NKXm`N3j&p=2zyKOZ zTK=!l1N3q-a%@c^``Cxv$0KweZ^_6+xAYgbFVW-X+*x8HY7)?vmrLV*^C4hAbl&|O z-=)bxFN$y{K+tB;&!%V`q|iwr6S;&WfhI?AO?-5M{Il6CpJ&>wfX`Uf$B^YVB@c=d xnfHB8qk3xzJ-uHd)dz(9y`PZ*= zp7WmPdCv23-}jbEld9c;d4$9^7=S0R5L%+PmW0jg)cA#c07pSP@Cm-B zu# zkqYSqO{`*6ge!lw7$wYx55{L~JO&lH1EO?vVpj~BuZdMewDSMZKl3y6swz0DQfOuG z!FQqP4$!qaR?W)wQ1bJZuX8#GXVgGz;3nLFjnIsA7c{wF&YCg&2p03?3LJx12By>l zdKIr#qe-PsuJOEpBltsbQo$e{!&Vp!u_EqXk`KY=v`&A11ZPQj4fW3IOX$1>e{N`J zt!_Sf=RtGmF13TC*LhXG1J%GpJH7+A8k~4Hd2u_l7O;<8AAb)1cAMjp*)fwfK+JQ$ z8>bxji7)EufCc?~u%EWb)gCg$iA1I@qs}#I5e+c|qWlf`lHe>D0bOX}cLBF`(yDc# zD>lUiYb$?t*kXl^IyWNihQ1@pUuGTj>rq=gx7e5|dIw_Paq<0@<_+enm(BX^YaHDl zo%=`FV8nTr66=Dy(2z8#Un;j*9dGkceX{WHP3~$mktR+4u}J$<(LwGD&~4XyKwnVT z0(1JY!=ScoA!${*;VMcbRUx39tH}qU{nT>&JrPVEHRi^xjfLn!zp=sf5lT}p4>ZOd zCFETwlanguageSelect->blockSignals(true); ui->languageSelect->addItem(QString("%1 (%2)").arg(QString::fromStdString(language.nameEnglish), QString::fromStdString(language.nameNative))); ui->languageSelect->setItemData(ui->languageSelect->count() - 1, QVariant(QString::fromStdString(language.identifier))); + ui->languageSelect->blockSignals(false); if(language.identifier == VLC->generaltexth->getPreferredLanguage()) ui->languageSelect->setCurrentIndex(ui->languageSelect->count() - 1); } @@ -37,31 +39,38 @@ Translations::~Translations() delete ui; } +void Translations::fillTranslationsTable(const std::string & language) +{ + auto & translation = mapHeader.translations[language]; + ui->translationsTable->blockSignals(true); + ui->translationsTable->setRowCount(0); + ui->translationsTable->setRowCount(translation.Struct().size()); + int i = 0; + for(auto & s : translation.Struct()) + { + auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); + auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); + wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); + wText->setFlags(wId->flags() | Qt::ItemIsEditable); + ui->translationsTable->setItem(i, 0, wId); + ui->translationsTable->setItem(i++, 1, wText); + } + ui->translationsTable->resizeColumnToContents(0); + ui->translationsTable->blockSignals(false); +} + void Translations::on_languageSelect_currentIndexChanged(int index) { auto language = ui->languageSelect->currentData().toString().toStdString(); - auto & translation = mapHeader.translations[language]; - bool hasLanguage = !translation.isNull(); + bool hasLanguage = !mapHeader.translations[language].isNull(); ui->supportedCheck->blockSignals(true); ui->supportedCheck->setChecked(hasLanguage); ui->supportedCheck->blockSignals(false); ui->translationsTable->setEnabled(hasLanguage); if(hasLanguage) - { - ui->translationsTable->blockSignals(true); - ui->translationsTable->setRowCount(translation.Struct().size()); - int i = 0; - for(auto & s : translation.Struct()) - { - auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); - auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); - wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); - wText->setFlags(wId->flags() | Qt::ItemIsEditable); - ui->translationsTable->setItem(i, 0, wId); - ui->translationsTable->setItem(i++, 0, wText); - } - ui->translationsTable->blockSignals(false); - } + fillTranslationsTable(language); + else + ui->translationsTable->setRowCount(0); } @@ -69,32 +78,15 @@ void Translations::on_supportedCheck_toggled(bool checked) { auto language = ui->languageSelect->currentData().toString().toStdString(); auto & translation = mapHeader.translations[language]; - bool hasLanguage = !translation.isNull(); bool hasRecord = !translation.Struct().empty(); if(checked) { - if(!hasLanguage) - translation = JsonNode(JsonNode::JsonType::DATA_STRUCT); - //copy from default language translation = mapHeader.translations[VLC->generaltexth->getPreferredLanguage()]; - hasLanguage = !translation.isNull(); - ui->translationsTable->blockSignals(true); - ui->translationsTable->setRowCount(translation.Struct().size()); - int i = 0; - for(auto & s : translation.Struct()) - { - auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); - auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); - wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); - wText->setFlags(wId->flags() | Qt::ItemIsEditable); - ui->translationsTable->setItem(i, 0, wId); - ui->translationsTable->setItem(i++, 0, wText); - } - ui->translationsTable->blockSignals(false); - ui->translationsTable->setEnabled(hasLanguage); + fillTranslationsTable(language); + ui->translationsTable->setEnabled(true); } else { @@ -117,16 +109,28 @@ void Translations::on_supportedCheck_toggled(bool checked) return; } ui->translationsTable->blockSignals(true); - ui->translationsTable->clear(); - translation = JsonNode(); + ui->translationsTable->setRowCount(0); + translation = JsonNode(JsonNode::JsonType::DATA_NULL); ui->translationsTable->blockSignals(false); ui->translationsTable->setEnabled(false); } } -void Translations::on_translationsTable_itemChanged(QTableWidgetItem *item) +void Translations::on_translationsTable_itemChanged(QTableWidgetItem * item) { - + assert(item->column() == 1); + + auto language = ui->languageSelect->currentData().toString().toStdString(); + auto & translation = mapHeader.translations[language]; + + assert(!translation.isNull()); + + auto textId = ui->translationsTable->item(item->row(), 0)->text().toStdString(); + assert(!textId.empty()); + if(textId.empty()) + return; + + translation[textId].String() = item->text().toStdString(); } diff --git a/mapeditor/mapsettings/translations.h b/mapeditor/mapsettings/translations.h index 1444e6f25..27b9c8218 100644 --- a/mapeditor/mapsettings/translations.h +++ b/mapeditor/mapsettings/translations.h @@ -20,6 +20,8 @@ class Translations; class Translations : public QDialog { Q_OBJECT + + void fillTranslationsTable(const std::string & language); public: explicit Translations(CMapHeader & mapHeader, QWidget *parent = nullptr); From ae073ee35d6b04ad0268b84174646343e198b5a6 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 28 Sep 2023 23:15:36 +0200 Subject: [PATCH 0669/1248] Remove unused identifiers --- lib/mapping/MapFormatJson.cpp | 1 + mapeditor/mainwindow.cpp | 2 ++ mapeditor/mapsettings/translations.cpp | 39 ++++++++++++++++++++++++++ mapeditor/mapsettings/translations.h | 6 +++- 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 5ab9f6297..beb31cf7a 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1275,6 +1275,7 @@ void CMapLoaderJson::readTranslations() if(isExistArchive(language.identifier + ".json")) mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json"); } + mapHeader->registerMapStrings(); } diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 5a18d1fc3..f4048c8e3 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -400,6 +400,8 @@ void MainWindow::saveMap() else QMessageBox::information(this, "Map validation", "Map has some errors. Open Validator from the Map menu to see issues found"); } + + Translations::cleanupRemovedItems(*controller.map()); CMapService mapService; try diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index 0b77b4cfc..fd2ae9130 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -15,6 +15,44 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/VCMI_Lib.h" +void Translations::cleanupRemovedItems(CMap & map) +{ + std::set existingObjects; + for(auto object : map.objects) + existingObjects.insert(object->instanceName); + + for(auto & translations : map.translations.Struct()) + { + auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + for(auto & s : translations.second.Struct()) + { + for(auto part : QString::fromStdString(s.first).split('.')) + { + if(existingObjects.count(part.toStdString())) + { + updateTranslations.Struct()[s.first] = s.second; + break; + } + } + } + translations.second = updateTranslations; + } +} + +void Translations::cleanupRemovedItems(CMap & map, const std::string & pattern) +{ + for(auto & translations : map.translations.Struct()) + { + auto updateTranslations = JsonNode(JsonNode::JsonType::DATA_STRUCT); + for(auto & s : translations.second.Struct()) + { + if(s.first.find(pattern) == std::string::npos) + updateTranslations.Struct()[s.first] = s.second; + } + translations.second = updateTranslations; + } +} + Translations::Translations(CMapHeader & mh, QWidget *parent) : QDialog(parent), ui(new Ui::Translations), @@ -41,6 +79,7 @@ Translations::~Translations() void Translations::fillTranslationsTable(const std::string & language) { + Translations::cleanupRemovedItems(dynamic_cast(mapHeader)); auto & translation = mapHeader.translations[language]; ui->translationsTable->blockSignals(true); ui->translationsTable->setRowCount(0); diff --git a/mapeditor/mapsettings/translations.h b/mapeditor/mapsettings/translations.h index 27b9c8218..af63600c8 100644 --- a/mapeditor/mapsettings/translations.h +++ b/mapeditor/mapsettings/translations.h @@ -11,7 +11,7 @@ #pragma once #include -#include "../lib/mapping/CMapHeader.h" +#include "../lib/mapping/CMap.h" namespace Ui { class Translations; @@ -26,6 +26,10 @@ class Translations : public QDialog public: explicit Translations(CMapHeader & mapHeader, QWidget *parent = nullptr); ~Translations(); + + //removes unused string IDs from map translations + static void cleanupRemovedItems(CMap & map); + static void cleanupRemovedItems(CMap & map, const std::string & pattern); private slots: void on_languageSelect_currentIndexChanged(int index); From 293214bb610c3b24874b3a3527bcc9050624477e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 29 Sep 2023 00:08:25 +0200 Subject: [PATCH 0670/1248] height fix --- client/renderSDL/CTrueTypeFont.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/renderSDL/CTrueTypeFont.cpp b/client/renderSDL/CTrueTypeFont.cpp index 76bf4ed96..b9d19aa12 100644 --- a/client/renderSDL/CTrueTypeFont.cpp +++ b/client/renderSDL/CTrueTypeFont.cpp @@ -72,7 +72,7 @@ CTrueTypeFont::~CTrueTypeFont() = default; size_t CTrueTypeFont::getLineHeight() const { if (fallbackFont) - fallbackFont->getLineHeight(); + return fallbackFont->getLineHeight(); return TTF_FontHeight(font.get()); } From 70796d232bdcc4db104e6e3a45afafd55b0311f7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 29 Sep 2023 00:24:45 +0200 Subject: [PATCH 0671/1248] Full support of maps translations --- lib/mapping/CMapHeader.cpp | 9 ++++--- lib/mapping/CMapHeader.h | 4 +-- lib/mapping/MapFormatH3M.cpp | 3 +-- mapeditor/inspector/inspector.cpp | 20 +++++++------- mapeditor/inspector/rewardswidget.cpp | 4 +-- mapeditor/mapsettings/eventsettings.cpp | 2 +- mapeditor/mapsettings/generalsettings.cpp | 4 +-- mapeditor/mapsettings/rumorsettings.cpp | 2 +- mapeditor/mapsettings/translations.cpp | 32 +++++++++++++++++------ mapeditor/mapsettings/translations.h | 1 + 10 files changed, 49 insertions(+), 32 deletions(-) diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index fe0583731..3f39bac7f 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -15,6 +15,7 @@ #include "../VCMI_Lib.h" #include "../CTownHandler.h" #include "../CGeneralTextHandler.h" +#include "../modding/CModHandler.h" #include "../CHeroHandler.h" #include "../Languages.h" @@ -168,14 +169,14 @@ void CMapHeader::registerMapStrings() registerString("map", TextIdentifier(s.first), s.second.String(), language); } -std::string mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) +std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized) { - return mapRegisterLocalizedString(mapHeader, UID, localized, VLC->generaltexth->getPreferredLanguage()); + return mapRegisterLocalizedString(modContext, mapHeader, UID, localized, VLC->modh->getModLanguage(modContext)); } -std::string mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) +std::string mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language) { - mapHeader.registerString("map", UID, localized, language); + mapHeader.registerString(modContext, UID, localized, language); mapHeader.translations.Struct()[language].Struct()[UID.get()].String() = localized; return UID.get(); } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index fc915c09c..8977a71d2 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -281,7 +281,7 @@ public: }; /// wrapper functions to register string into the map and stores its translation -std::string DLL_LINKAGE mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized); -std::string DLL_LINKAGE mapRegisterLocalizedString(CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language); +std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized); +std::string DLL_LINKAGE mapRegisterLocalizedString(const std::string & modContext, CMapHeader & mapHeader, const TextIdentifier & UID, const std::string & localized, const std::string & language); VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 5b44974ef..c6dcf2df0 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2274,8 +2274,7 @@ std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIden if(mapString.empty()) return ""; - mapHeader->registerString(modName, fullIdentifier, mapString); - return fullIdentifier.get(); + return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString); } void CMapLoaderH3M::afterRead() diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 5f0e6ef54..8f2580308 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -553,7 +553,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(mapRegisterLocalizedString(*map, TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + o->setNameTextId(mapRegisterLocalizedString("map", *map, TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -606,10 +606,10 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = mapRegisterLocalizedString(*map, TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + o->nameCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); if(key == "Biography") - o->biographyCustomTextId = mapRegisterLocalizedString(*map, TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); + o->biographyCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -646,7 +646,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -664,11 +664,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText.appendTextID(mapRegisterLocalizedString(*map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); } diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index b8990b135..cfd900326 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -211,7 +211,7 @@ bool RewardsWidget::commitChanges() if(ui->onSelectText->text().isEmpty()) object.configuration.onSelect.clear(); else - object.configuration.onSelect = MetaString::createFromTextID(mapRegisterLocalizedString(map, TextIdentifier("reward", object.instanceName, "onSelect"), ui->onSelectText->text().toStdString())); + object.configuration.onSelect = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "onSelect"), ui->onSelectText->text().toStdString())); object.configuration.canRefuse = ui->canRefuse->isChecked(); //reset parameters @@ -232,7 +232,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) if(ui->rewardMessage->text().isEmpty()) vinfo.message.clear(); else - vinfo.message = MetaString::createFromTextID(mapRegisterLocalizedString(map, TextIdentifier("reward", object.instanceName, "info", index, "message"), ui->rewardMessage->text().toStdString())); + vinfo.message = MetaString::createFromTextID(mapRegisterLocalizedString("map", map, TextIdentifier("reward", object.instanceName, "info", index, "message"), ui->rewardMessage->text().toStdString())); vinfo.reward.heroLevel = ui->rHeroLevel->value(); vinfo.reward.heroExperience = ui->rHeroExperience->value(); diff --git a/mapeditor/mapsettings/eventsettings.cpp b/mapeditor/mapsettings/eventsettings.cpp index 60cd97982..a128aa6ef 100644 --- a/mapeditor/mapsettings/eventsettings.cpp +++ b/mapeditor/mapsettings/eventsettings.cpp @@ -52,7 +52,7 @@ CMapEvent eventFromVariant(CMapHeader & mapHeader, const QVariant & variant) CMapEvent result; auto v = variant.toMap(); result.name = v.value("name").toString().toStdString(); - result.message.appendTextID(mapRegisterLocalizedString(mapHeader, TextIdentifier("header", "event", result.name, "message"), v.value("message").toString().toStdString())); + result.message.appendTextID(mapRegisterLocalizedString("map", mapHeader, TextIdentifier("header", "event", result.name, "message"), v.value("message").toString().toStdString())); result.players = v.value("players").toInt(); result.humanAffected = v.value("humanAffected").toInt(); result.computerAffected = v.value("computerAffected").toInt(); diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index 8f74867dc..dd5cff493 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -59,8 +59,8 @@ void GeneralSettings::initialize(MapController & c) void GeneralSettings::update() { - controller->map()->name.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); - controller->map()->description.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); + controller->map()->name.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); + controller->map()->description.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); if(ui->heroLevelLimitCheck->isChecked()) controller->map()->levelLimit = ui->heroLevelLimit->value(); else diff --git a/mapeditor/mapsettings/rumorsettings.cpp b/mapeditor/mapsettings/rumorsettings.cpp index 6527c67f1..7d47cc6c9 100644 --- a/mapeditor/mapsettings/rumorsettings.cpp +++ b/mapeditor/mapsettings/rumorsettings.cpp @@ -43,7 +43,7 @@ void RumorSettings::update() { Rumor rumor; rumor.name = ui->rumors->item(i)->text().toStdString(); - rumor.text.appendTextID(mapRegisterLocalizedString(*controller->map(), TextIdentifier("header", "rumor", i, "text"), ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString())); + rumor.text.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "rumor", i, "text"), ui->rumors->item(i)->data(Qt::UserRole).toString().toStdString())); controller->map()->rumors.push_back(rumor); } } diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index fd2ae9130..b314c90c8 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -28,7 +28,7 @@ void Translations::cleanupRemovedItems(CMap & map) { for(auto part : QString::fromStdString(s.first).split('.')) { - if(existingObjects.count(part.toStdString())) + if(part == "map" || existingObjects.count(part.toStdString())) { updateTranslations.Struct()[s.first] = s.second; break; @@ -61,15 +61,31 @@ Translations::Translations(CMapHeader & mh, QWidget *parent) : ui->setupUi(this); //fill languages list + std::set indexFoundLang; + int foundLang = -1; + ui->languageSelect->blockSignals(true); for(auto & language : Languages::getLanguageList()) { - ui->languageSelect->blockSignals(true); ui->languageSelect->addItem(QString("%1 (%2)").arg(QString::fromStdString(language.nameEnglish), QString::fromStdString(language.nameNative))); ui->languageSelect->setItemData(ui->languageSelect->count() - 1, QVariant(QString::fromStdString(language.identifier))); - ui->languageSelect->blockSignals(false); + if(mapHeader.translations.Struct().count(language.identifier) && !mapHeader.translations[language.identifier].Struct().empty()) + indexFoundLang.insert(ui->languageSelect->count() - 1); if(language.identifier == VLC->generaltexth->getPreferredLanguage()) - ui->languageSelect->setCurrentIndex(ui->languageSelect->count() - 1); + foundLang = ui->languageSelect->count() - 1; } + ui->languageSelect->blockSignals(false); + + if(foundLang >= 0 && !indexFoundLang.empty() && !indexFoundLang.count(foundLang)) + { + foundLang = *indexFoundLang.begin(); + mapPreferredLanguage = ui->languageSelect->itemData(foundLang).toString().toStdString(); + } + + if(foundLang >= 0) + ui->languageSelect->setCurrentIndex(foundLang); + + if(mapPreferredLanguage.empty()) + mapPreferredLanguage = VLC->generaltexth->getPreferredLanguage(); } Translations::~Translations() @@ -101,7 +117,7 @@ void Translations::fillTranslationsTable(const std::string & language) void Translations::on_languageSelect_currentIndexChanged(int index) { auto language = ui->languageSelect->currentData().toString().toStdString(); - bool hasLanguage = !mapHeader.translations[language].isNull(); + bool hasLanguage = mapHeader.translations.Struct().count(language); ui->supportedCheck->blockSignals(true); ui->supportedCheck->setChecked(hasLanguage); ui->supportedCheck->blockSignals(false); @@ -122,21 +138,21 @@ void Translations::on_supportedCheck_toggled(bool checked) if(checked) { //copy from default language - translation = mapHeader.translations[VLC->generaltexth->getPreferredLanguage()]; + translation = mapHeader.translations[mapPreferredLanguage]; fillTranslationsTable(language); ui->translationsTable->setEnabled(true); } else { - bool canRemove = language != VLC->generaltexth->getPreferredLanguage(); + bool canRemove = language != mapPreferredLanguage; if(!canRemove) { QMessageBox::information(this, tr("Remove translation"), tr("Default language cannot be removed")); } else if(hasRecord) { - auto sure = QMessageBox::question(this, tr("Remove translation"), tr("This language has text records which will be removed. Continue?")); + auto sure = QMessageBox::question(this, tr("Remove translation"), tr("All existing text records for this language will be removed. Continue?")); canRemove = sure != QMessageBox::No; } diff --git a/mapeditor/mapsettings/translations.h b/mapeditor/mapsettings/translations.h index af63600c8..85e70ba1b 100644 --- a/mapeditor/mapsettings/translations.h +++ b/mapeditor/mapsettings/translations.h @@ -41,4 +41,5 @@ private slots: private: Ui::Translations *ui; CMapHeader & mapHeader; + std::string mapPreferredLanguage; }; From 9631176e5828a82bd5b30396ce0691c57377bb44 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 29 Sep 2023 00:28:09 +0200 Subject: [PATCH 0672/1248] Fix tests --- test/map/CMapFormatTest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index 26082c8c9..647a76907 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -65,7 +65,7 @@ TEST(MapFormat, Random) CMapGenerator gen(opt, TEST_RANDOM_SEED); std::unique_ptr initialMap = gen.generate(); - initialMap->name = "Test"; + initialMap->name.appendRawString("Test"); SCOPED_TRACE("MapFormat_Random generated"); CMemoryBuffer serializeBuffer; From f3fa0f8652a140f938263685c21a379a96dbbcae Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 29 Sep 2023 19:49:18 +0200 Subject: [PATCH 0673/1248] Allow to reconnect to proxy server --- client/CServerHandler.cpp | 3 +++ server/CVCMIServer.cpp | 16 ++++++++-------- server/CVCMIServer.h | 3 ++- server/NetPacksLobbyServer.cpp | 6 ++++++ 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 952b6feb1..356262976 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -417,6 +417,9 @@ void CServerHandler::sendClientDisconnecting() logNetwork->info("Sent leaving signal to the server"); } sendLobbyPack(lcd); + + c->close(); + c.reset(); } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 5b3eecdef..45dac8b32 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -211,21 +211,20 @@ void CVCMIServer::establishRemoteConnections() uuid = cmdLineOptions["lobby-uuid"].as(); int numOfConnections = cmdLineOptions["connections"].as(); - auto address = cmdLineOptions["lobby"].as(); - int port = cmdLineOptions["lobby-port"].as(); - logGlobal->info("Server is connecting to remote at %s:%d with uuid %s %d times", address, port, uuid, numOfConnections); - for(int i = 0; i < numOfConnections; ++i) - connectToRemote(address, port); + connectToRemote(); } -void CVCMIServer::connectToRemote(const std::string & addr, int port) +void CVCMIServer::connectToRemote() { std::shared_ptr c; try { - logNetwork->info("Establishing connection..."); - c = std::make_shared(addr, port, SERVER_NAME, uuid); + auto address = cmdLineOptions["lobby"].as(); + int port = cmdLineOptions["lobby-port"].as(); + + logNetwork->info("Establishing connection to remote at %s:%d with uuid %s", address, port, uuid); + c = std::make_shared(address, port, SERVER_NAME, uuid); } catch(...) { @@ -235,6 +234,7 @@ void CVCMIServer::connectToRemote(const std::string & addr, int port) if(c) { connections.insert(c); + remoteConnections.insert(c); c->handler = std::make_shared(&CVCMIServer::threadHandleClient, this, c); } } diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 450fe3732..4e3295aa0 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -64,6 +64,7 @@ public: boost::program_options::variables_map cmdLineOptions; std::set> connections; + std::set> remoteConnections; std::set> hangingConnections; //keep connections of players disconnected during the game std::atomic currentClientId; @@ -78,7 +79,7 @@ public: void startGameImmidiately(); void establishRemoteConnections(); - void connectToRemote(const std::string & addr, int port); + void connectToRemote(); void startAsyncAccept(); void connectionAccepted(const boost::system::error_code & ec); void threadHandleClient(std::shared_ptr c); diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index dbb68d617..804874f94 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -189,6 +189,12 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb srv.addToAnnounceQueue(std::move(ph)); } srv.updateAndPropagateLobbyState(); + + if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c)) + { + srv.remoteConnections -= pack.c; + srv.connectToRemote(); + } } void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack) From be17ed1b62a3a2807ec7e203c2e655d5a3944972 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 29 Sep 2023 23:55:12 +0200 Subject: [PATCH 0674/1248] Enable ccache for CI --- .github/workflows/github.yml | 17 ++++++++++++++++- CMakePresets.json | 8 ++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index eecb0f737..4319c74e2 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -158,6 +158,15 @@ jobs: env: VCMI_BUILD_PLATFORM: x64 + - name: ccache + if: startsWith(matrix.preset, 'ios') != true + uses: hendrikmuhs/ccache-action@v1.2 + with: + key: ${{ matrix.preset }} + # actual cache takes up less space, at most ~1 GB + max-size: "5G" + verbose: 2 + - uses: actions/setup-python@v4 if: "${{ matrix.conan_profile != '' }}" with: @@ -193,7 +202,13 @@ jobs: env: PULL_REQUEST: ${{ github.event.pull_request.number }} - - name: CMake Preset + - name: CMake Preset with ccache + if: startsWith(matrix.preset, 'ios') != true + run: | + cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }} + + - name: CMake Preset without ccache + if: startsWith(matrix.preset, 'ios') run: | cmake --preset ${{ matrix.preset }} diff --git a/CMakePresets.json b/CMakePresets.json index 39c752e94..ce2781c07 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -25,7 +25,8 @@ "CMAKE_BUILD_TYPE": "RelWithDebInfo", "ENABLE_TEST": "OFF", "ENABLE_STRICT_COMPILATION": "ON", - "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}" + "ENABLE_GITVERSION": "$env{VCMI_PACKAGE_GITVERSION}", + "ENABLE_PCH" : "OFF" } }, { @@ -217,7 +218,10 @@ "inherits": [ "base-ios-release", "ios-device-conan" - ] + ], + "cacheVariables": { + "ENABLE_PCH" : "ON" + } }, { "name": "ios-release-legacy", From ce62ab3e66c91a1ef1ff4689fa834a459e04d2f1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 01:19:18 +0200 Subject: [PATCH 0675/1248] Select random object template instead of first --- lib/rmg/RmgObject.cpp | 21 +++++++++++---------- lib/rmg/RmgObject.h | 11 ++++++----- lib/rmg/modificators/ConnectionsPlacer.cpp | 4 ++-- lib/rmg/modificators/ObjectManager.cpp | 10 +++++----- lib/rmg/modificators/RiverPlacer.cpp | 2 +- lib/rmg/modificators/TownPlacer.cpp | 2 +- lib/rmg/modificators/WaterProxy.cpp | 4 ++-- 7 files changed, 28 insertions(+), 26 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 31e287e06..9934602a5 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -111,18 +111,18 @@ void Object::Instance::setPositionRaw(const int3 & position) dObject.pos = dPosition + dParent.getPosition(); } -void Object::Instance::setAnyTemplate() +void Object::Instance::setAnyTemplate(CRandomGenerator & rng) { auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(); if(templates.empty()) throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); - dObject.appearance = templates.front(); + dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); setPosition(getPosition(false)); } -void Object::Instance::setTemplate(TerrainId terrain) +void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) { auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrain); if (templates.empty()) @@ -130,7 +130,8 @@ void Object::Instance::setTemplate(TerrainId terrain) auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); } - dObject.appearance = templates.front(); + + dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); setPosition(getPosition(false)); } @@ -280,10 +281,10 @@ void Object::setPosition(const int3 & position) i.setPositionRaw(i.getPosition()); } -void Object::setTemplate(const TerrainId & terrain) +void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng) { for(auto& i : dInstances) - i.setTemplate(terrain); + i.setTemplate(terrain, rng); } const Area & Object::getArea() const @@ -325,7 +326,7 @@ void rmg::Object::setGuardedIfMonster(const Instance& object) } } -void Object::Instance::finalize(RmgMap & map) +void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) { if(!map.isOnMap(getPosition(true))) throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); @@ -341,7 +342,7 @@ void Object::Instance::finalize(RmgMap & map) } else { - setTemplate(terrainType->getId()); + setTemplate(terrainType->getId(), rng); } } @@ -362,14 +363,14 @@ void Object::Instance::finalize(RmgMap & map) map.getMapProxy()->insertObject(&dObject); } -void Object::finalize(RmgMap & map) +void Object::finalize(RmgMap & map, CRandomGenerator & rng) { if(dInstances.empty()) throw rmgException("Cannot finalize object without instances"); for(auto & dInstance : dInstances) { - dInstance.finalize(map); + dInstance.finalize(map, rng); } } diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index d444a90b4..2ba78a29a 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; +class CRandomGenerator; class RmgMap; namespace rmg { @@ -35,8 +36,8 @@ public: int3 getVisitablePosition() const; bool isVisitableFrom(const int3 & tile) const; const Area & getAccessibleArea() const; - void setTemplate(TerrainId terrain); //cache invalidation - void setAnyTemplate(); //cache invalidation + void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation + void setAnyTemplate(CRandomGenerator &); //cache invalidation int3 getTopTile() const; int3 getPosition(bool isAbsolute = false) const; @@ -45,7 +46,7 @@ public: const CGObjectInstance & object() const; CGObjectInstance & object(); - void finalize(RmgMap & map); //cache invalidation + void finalize(RmgMap & map, CRandomGenerator &); //cache invalidation void clear(); private: @@ -73,7 +74,7 @@ public: const int3 & getPosition() const; void setPosition(const int3 & position); - void setTemplate(const TerrainId & terrain); + void setTemplate(const TerrainId & terrain, CRandomGenerator &); const Area & getArea() const; //lazy cache invalidation const int3 getVisibleTop() const; @@ -81,7 +82,7 @@ public: bool isGuarded() const; void setGuardedIfMonster(const Instance & object); - void finalize(RmgMap & map); + void finalize(RmgMap & map, CRandomGenerator &); void clear(); private: diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 5526d2731..44b1e11d2 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -316,8 +316,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c auto * gate2 = factory->create(); rmg::Object rmgGate1(*gate1); rmg::Object rmgGate2(*gate2); - rmgGate1.setTemplate(zone.getTerrainType()); - rmgGate2.setTemplate(otherZone->getTerrainType()); + rmgGate1.setTemplate(zone.getTerrainType(), zone.getRand()); + rmgGate2.setTemplate(otherZone->getTerrainType(), zone.getRand()); bool guarded1 = manager.addGuard(rmgGate1, connection.getGuardStrength(), true); bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true); int minDist = 3; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 02267aaa8..9d34b3833 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -354,7 +354,7 @@ bool ObjectManager::createRequiredObjects() for(const auto & objInfo : requiredObjects) { rmg::Object rmgObject(*objInfo.obj); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY)); Zone::Lock lock(zone.areaMutex); @@ -394,7 +394,7 @@ bool ObjectManager::createRequiredObjects() auto possibleArea = zone.areaPossible(); rmg::Object rmgObject(*objInfo.obj); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); bool guarded = addGuard(rmgObject, objInfo.guardStrength, (objInfo.obj->ID == Obj::MONOLITH_TWO_WAY)); auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, [this, &rmgObject](const int3 & tile) @@ -480,7 +480,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD if (!monster->object().appearance) { //Needed to determine visitable offset - monster->setAnyTemplate(); + monster->setAnyTemplate(zone.getRand()); } object.getPosition(); auto visitableOffset = monster->object().getVisitableOffset(); @@ -492,7 +492,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD int3 parentOffset = monster->getPosition(true) - monster->getPosition(false); monster->setPosition(fixedPos - parentOffset); } - object.finalize(map); + object.finalize(map, zone.getRand()); Zone::Lock lock(zone.areaMutex); zone.areaPossible().subtract(object.getArea()); @@ -689,7 +689,7 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard }); auto & instance = object.addInstance(*guard); - instance.setAnyTemplate(); //terrain is irrelevant for monsters, but monsters need some template now + instance.setAnyTemplate(zone.getRand()); //terrain is irrelevant for monsters, but monsters need some template now //Fix HoTA monsters with offset template auto visitableOffset = instance.object().getVisitableOffset(); diff --git a/lib/rmg/modificators/RiverPlacer.cpp b/lib/rmg/modificators/RiverPlacer.cpp index 0738bdc72..d9c7af3e1 100644 --- a/lib/rmg/modificators/RiverPlacer.cpp +++ b/lib/rmg/modificators/RiverPlacer.cpp @@ -397,7 +397,7 @@ void RiverPlacer::connectRiver(const int3 & tile) { auto * obj = handler->create(templ); rmg::Object deltaObj(*obj, deltaPositions[pos]); - deltaObj.finalize(map); + deltaObj.finalize(map, zone.getRand()); } } } diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index 1f046588e..76bf3bbec 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -140,7 +140,7 @@ int3 TownPlacer::placeMainTown(ObjectManager & manager, CGTownInstance & town) { //towns are big objects and should be centered around visitable position rmg::Object rmgObject(town); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); int3 position(-1, -1, -1); { diff --git a/lib/rmg/modificators/WaterProxy.cpp b/lib/rmg/modificators/WaterProxy.cpp index 023a13af1..839511d7c 100644 --- a/lib/rmg/modificators/WaterProxy.cpp +++ b/lib/rmg/modificators/WaterProxy.cpp @@ -254,7 +254,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout auto * boat = dynamic_cast(VLC->objtypeh->getHandlerFor(Obj::BOAT, *RandomGeneratorUtil::nextItem(sailingBoatTypes, zone.getRand()))->create()); rmg::Object rmgObject(*boat); - rmgObject.setTemplate(zone.getTerrainType()); + rmgObject.setTemplate(zone.getTerrainType(), zone.getRand()); auto waterAvailable = zone.areaPossible() + zone.freePaths(); rmg::Area coast = lake.neighbourZones.at(land.getId()); //having land tiles @@ -319,7 +319,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool shipyard->tempOwner = PlayerColor::NEUTRAL; rmg::Object rmgObject(*shipyard); - rmgObject.setTemplate(land.getTerrainType()); + rmgObject.setTemplate(land.getTerrainType(), zone.getRand()); bool guarded = manager->addGuard(rmgObject, guard); auto waterAvailable = zone.areaPossible() + zone.freePaths(); From 71ea057ad1b651851648820f186098509417023a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 30 Sep 2023 01:27:30 +0200 Subject: [PATCH 0676/1248] CI: Install all packages at once on Linux --- CI/linux-qt6/before_install.sh | 11 +++++------ CI/linux/before_install.sh | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CI/linux-qt6/before_install.sh b/CI/linux-qt6/before_install.sh index 756b42eb3..689101138 100644 --- a/CI/linux-qt6/before_install.sh +++ b/CI/linux-qt6/before_install.sh @@ -3,9 +3,8 @@ sudo apt-get update # Dependencies -sudo apt-get install libboost-all-dev -sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev -sudo apt-get install qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools -sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev -# Optional dependencies -sudo apt-get install libminizip-dev libfuzzylite-dev +sudo apt-get install libboost-all-dev \ +libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ +qt6-base-dev qt6-base-dev-tools qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools \ +ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ +libminizip-dev libfuzzylite-dev # Optional dependencies diff --git a/CI/linux/before_install.sh b/CI/linux/before_install.sh index 8b0c75d59..e08075d7d 100644 --- a/CI/linux/before_install.sh +++ b/CI/linux/before_install.sh @@ -3,9 +3,8 @@ sudo apt-get update # Dependencies -sudo apt-get install libboost-all-dev -sudo apt-get install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev -sudo apt-get install qtbase5-dev -sudo apt-get install ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev -# Optional dependencies -sudo apt-get install libminizip-dev libfuzzylite-dev qttools5-dev +sudo apt-get install libboost-all-dev \ +libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev \ +qtbase5-dev \ +ninja-build zlib1g-dev libavformat-dev libswscale-dev libtbb-dev libluajit-5.1-dev \ +libminizip-dev libfuzzylite-dev qttools5-dev # Optional dependencies From 1502ea22506e5fe1176f10e2eb1e458a86b99fe2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 01:33:12 +0200 Subject: [PATCH 0677/1248] Fix pandora mana problem --- lib/mapObjects/CGPandoraBox.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index de2a00448..9d1a9874c 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -101,7 +101,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b } } - if(vi.reward.manaDiff || vi.reward.manaPercentage) + if(vi.reward.manaDiff || vi.reward.manaPercentage >= 0) txt = setText(temp.manaDiff > 0, 177, 176, h); for(auto b : vi.reward.bonuses) @@ -155,7 +155,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b temp.resources.amin(0); temp.resources.amax(0); temp.manaDiff = 0; - temp.manaPercentage = 0; + temp.manaPercentage = -1; temp.spells.clear(); temp.creatures.clear(); temp.bonuses.clear(); From 53dadc6dc37544fcd5c9cc86f9d5f1e43b586f00 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 03:42:29 +0200 Subject: [PATCH 0678/1248] Add map converter --- mapeditor/mainwindow.cpp | 82 ++++++++++++++++++++++++++-------------- mapeditor/mainwindow.h | 6 ++- mapeditor/mainwindow.ui | 6 +++ 3 files changed, 64 insertions(+), 30 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index f4048c8e3..90bad7b69 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -313,7 +313,7 @@ void MainWindow::initializeMap(bool isNew) onPlayersChanged(); } -bool MainWindow::openMap(const QString & filenameSelect) +std::unique_ptr MainWindow::openMapInternal(const QString & filenameSelect) { QFileInfo fi(filenameSelect); std::string fname = fi.fileName().toStdString(); @@ -327,26 +327,30 @@ bool MainWindow::openMap(const QString & filenameSelect) CResourceHandler::addFilesystem("local", "mapEditor", mapEditorFilesystem); if(!CResourceHandler::get("mapEditor")->existsResource(resId)) - { - QMessageBox::warning(this, tr("Failed to open map"), tr("Cannot open map from this folder")); - return false; - } + throw std::runtime_error("Cannot open map from this folder"); CMapService mapService; + if(auto header = mapService.loadMapHeader(resId)) + { + auto missingMods = CMapService::verifyMapHeaderMods(*header); + ModIncompatibility::ModListWithVersion modList; + for(const auto & m : missingMods) + modList.push_back({m.second.name, m.second.version.toString()}); + + if(!modList.empty()) + throw ModIncompatibility(modList); + + return mapService.loadMap(resId); + } + else + throw std::runtime_error("Corrupted map"); +} + +bool MainWindow::openMap(const QString & filenameSelect) +{ try { - if(auto header = mapService.loadMapHeader(resId)) - { - auto missingMods = CMapService::verifyMapHeaderMods(*header); - ModIncompatibility::ModListWithVersion modList; - for(const auto & m : missingMods) - modList.push_back({m.second.name, m.second.version.toString()}); - - if(!modList.empty()) - throw ModIncompatibility(modList); - - controller.setMap(mapService.loadMap(resId)); - } + controller.setMap(openMapInternal(filenameSelect)); } catch(const ModIncompatibility & e) { @@ -356,7 +360,7 @@ bool MainWindow::openMap(const QString & filenameSelect) } catch(const std::exception & e) { - QMessageBox::critical(this, "Failed to open map", e.what()); + QMessageBox::critical(this, "Failed to open map", tr(e.what())); return false; } @@ -423,7 +427,7 @@ void MainWindow::on_actionSave_as_triggered() if(!controller.map()) return; - auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); + auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), lastSavingDir, tr("VCMI maps (*.vmap)")); if(filenameSelect.isNull()) return; @@ -432,6 +436,7 @@ void MainWindow::on_actionSave_as_triggered() return; filename = filenameSelect; + lastSavingDir = filenameSelect.remove(QUrl(filenameSelect).fileName()); saveMap(); } @@ -449,16 +454,9 @@ void MainWindow::on_actionSave_triggered() return; if(filename.isNull()) - { - auto filenameSelect = QFileDialog::getSaveFileName(this, tr("Save map"), "", tr("VCMI maps (*.vmap)")); - - if(filenameSelect.isNull()) - return; - - filename = filenameSelect; - } - - saveMap(); + on_actionSave_as_triggered(); + else + saveMap(); } void MainWindow::terrainButtonClicked(TerrainId terrain) @@ -1250,3 +1248,29 @@ void MainWindow::on_actionTranslations_triggered() translationsDialog->show(); } +void MainWindow::on_actionh3m_coverter_triggered() +{ + auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to covert"), + QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()), + tr("HoMM3 maps(*.h3m)")); + if(mapFiles.empty()) + return; + + auto saveDirectory = QFileDialog::getExistingDirectory(this, tr("Choose directory to save coverted maps"), QCoreApplication::applicationDirPath()); + if(saveDirectory.isEmpty()) + return; + + try + { + for(auto & m : mapFiles) + { + CMapService mapService; + mapService.saveMap(openMapInternal(m), (saveDirectory + QFileInfo(m).fileName()).toStdString()); + } + } + catch(const std::exception & e) + { + QMessageBox::critical(this, tr("Failed to convert the map. Abort operation"), tr(e.what())); + } +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 3954ca173..74b051d80 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -31,6 +31,8 @@ class MainWindow : public QMainWindow #ifdef ENABLE_QT_TRANSLATIONS QTranslator translator; #endif + + std::unique_ptr openMapInternal(const QString &); public: explicit MainWindow(QWidget *parent = nullptr); @@ -118,6 +120,8 @@ private slots: void on_actionExport_triggered(); void on_actionTranslations_triggered(); + + void on_actionh3m_coverter_triggered(); public slots: @@ -155,7 +159,7 @@ private: ObjectBrowserProxyModel * objectBrowser = nullptr; QGraphicsScene * scenePreview; - QString filename; + QString filename, lastSavingDir; bool unsaved = false; QStandardItemModel objectsModel; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 6f7b0da5f..6017a6a9e 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -63,6 +63,7 @@ + @@ -1254,6 +1255,11 @@ Ctrl+T + + + h3m coverter + + From 30181789562855179e653ff39d7f8caf82b107ed Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 03:44:13 +0200 Subject: [PATCH 0679/1248] Remember folder for images as well --- mapeditor/mainwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 90bad7b69..0dcf519d2 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -1231,7 +1231,7 @@ void MainWindow::on_actionPaste_triggered() void MainWindow::on_actionExport_triggered() { - QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), QCoreApplication::applicationDirPath(), "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); + QString fileName = QFileDialog::getSaveFileName(this, tr("Save to image"), lastSavingDir, "BMP (*.bmp);;JPEG (*.jpeg);;PNG (*.png)"); if(!fileName.isNull()) { QImage image(ui->mapView->scene()->sceneRect().size().toSize(), QImage::Format_RGB888); From cb02e221f3bbdbb89265da428417f8db45a2b657 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 04:33:41 +0200 Subject: [PATCH 0680/1248] Couple fixes --- mapeditor/mapcontroller.cpp | 14 ++++++++++---- mapeditor/windownewmap.cpp | 5 +++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index b1b86212a..0007da3b1 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -105,7 +105,9 @@ void MapController::repairMap() } //fix owners for objects - for(auto obj : _map->objects) + auto allImpactedObjects(_map->objects); + allImpactedObjects.insert(allImpactedObjects.end(), _map->predefinedHeroes.begin(), _map->predefinedHeroes.end()); + for(auto obj : allImpactedObjects) { //setup proper names (hero name will be fixed later if(obj->ID != Obj::HERO && obj->ID != Obj::PRISON && (obj->typeName.empty() || obj->subTypeName.empty())) @@ -154,12 +156,16 @@ void MapController::repairMap() if(nih->spellbookContainsSpell(SpellID::PRESET)) { nih->removeSpellFromSpellbook(SpellID::PRESET); - } - else - { for(auto spellID : type->spells) nih->addSpellToSpellbook(spellID); } + if(nih->spellbookContainsSpell(SpellID::SPELLBOOK_PRESET)) + { + nih->removeSpellFromSpellbook(SpellID::SPELLBOOK_PRESET); + if(!nih->getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) + nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + } + //fix portrait if(nih->portrait < 0 || nih->portrait == 255) nih->portrait = type->imageIndex; diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 6fdd404b8..68e3b70a7 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -90,6 +90,11 @@ void WindowNewMap::loadUserSettings() { ui->heightTxt->setText(height.toString()); } + for(auto & sz : mapSizes) + { + if(sz.second.first == width.toInt() && sz.second.second == height.toInt()) + ui->sizeCombo->setCurrentIndex(sz.first); + } auto twoLevel = s.value(newMapTwoLevel); if (twoLevel.isValid()) { From 2dd0d76412d0accd7de585a50c146a9b2a27527a Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 16 Sep 2023 12:33:02 +0300 Subject: [PATCH 0681/1248] NKAI: water and air walking --- .../Analyzers/DangerHitMapAnalyzer.cpp | 8 +- AI/Nullkiller/CMakeLists.txt | 2 + AI/Nullkiller/Pathfinding/AINodeStorage.h | 2 +- .../Pathfinding/AIPathfinderConfig.cpp | 1 + .../AdventureSpellCastMovementActions.cpp | 82 ++++++++++++ .../AdventureSpellCastMovementActions.h | 58 ++++++++ .../Pathfinding/Actions/BoatActions.cpp | 4 +- .../Pathfinding/Actions/BoatActions.h | 4 +- .../Pathfinding/Actions/SpecialAction.h | 6 + .../Rules/AILayerTransitionRule.cpp | 124 +++++++++++++----- .../Pathfinding/Rules/AILayerTransitionRule.h | 9 +- lib/pathfinder/CPathfinder.cpp | 13 ++ lib/pathfinder/CPathfinder.h | 2 + lib/pathfinder/PathfinderOptions.cpp | 1 + lib/pathfinder/PathfinderOptions.h | 5 + 15 files changed, 282 insertions(+), 39 deletions(-) create mode 100644 AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp create mode 100644 AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 3651e567f..aa2f91e59 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap() if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES) continue; - ai->pathfinder->updatePaths(pair.second, PathfinderSettings()); + PathfinderSettings ps; + + ps.mainTurnDistanceLimit = 10; + ps.scoutTurnDistanceLimit = 10; + ps.useHeroChain = false; + + ai->pathfinder->updatePaths(pair.second, ps); boost::this_thread::interruption_point(); diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index a6560989f..a22641684 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -9,6 +9,7 @@ set(Nullkiller_SRCS Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BoatActions.cpp Pathfinding/Actions/TownPortalAction.cpp + Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Rules/AILayerTransitionRule.cpp Pathfinding/Rules/AIMovementAfterDestinationRule.cpp Pathfinding/Rules/AIMovementToDestinationRule.cpp @@ -69,6 +70,7 @@ set(Nullkiller_HEADERS Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BoatActions.h Pathfinding/Actions/TownPortalAction.h + Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Rules/AILayerTransitionRule.h Pathfinding/Rules/AIMovementAfterDestinationRule.h Pathfinding/Rules/AIMovementToDestinationRule.h diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index c127f294b..71464509d 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -45,7 +45,7 @@ struct AIPathNode : public CGPathNode { uint64_t danger; uint64_t armyLoss; - uint32_t manaCost; + int32_t manaCost; const AIPathNode * chainOther; std::shared_ptr specialAction; const ChainActor * actor; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index b7314b3d1..2259ef029 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -44,6 +44,7 @@ namespace AIPathfinding std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage) { + options.canUseCast = true; } AIPathfinderConfig::~AIPathfinderConfig() = default; diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp new file mode 100644 index 000000000..ff4934b36 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp @@ -0,0 +1,82 @@ +/* +* AdventureSpellCastMovementActions.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 "../../AIGateway.h" +#include "../../Goals/AdventureSpellCast.h" +#include "../../Goals/CaptureObject.h" +#include "../../Goals/Invalid.h" +#include "../../Goals/BuildBoat.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "AdventureSpellCastMovementActions.h" + +namespace NKAI +{ + +namespace AIPathfinding +{ + AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero) + :spellToCast(spellToCast), hero(hero) + { + manaCost = hero->getSpellCost(spellToCast.toSpell()); + } + + WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero) + :AdventureCastAction(SpellID::WATER_WALK, hero) + { } + + AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero) + : AdventureCastAction(SpellID::FLY, hero) + { + } + + void AdventureCastAction::applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const + { + dstMode->manaCost = srcNode->manaCost + manaCost; + dstMode->theNodeBefore = source.node; + } + + void AdventureCastAction::execute(const CGHeroInstance * hero) const + { + assert(hero == this->hero); + + Goals::AdventureSpellCast(hero, spellToCast).accept(ai); + } + + bool AdventureCastAction::canAct(const AIPathNode * source) const + { + assert(hero == this->hero); + + auto hero = source->actor->hero; + +#ifdef VCMI_TRACE_PATHFINDER + logAi->trace( + "Hero %s has %d mana and needed %d and already spent %d", + hero->name, + hero->mana, + getManaCost(hero), + source->manaCost); +#endif + + return hero->mana >= source->manaCost + manaCost; + } + + std::string AdventureCastAction::toString() const + { + return "Cast " + spellToCast.toSpell()->getNameTranslated() + " by " + hero->getNameTranslated(); + } +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h new file mode 100644 index 000000000..0667e400a --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h @@ -0,0 +1,58 @@ +/* +* AdventureSpellCastMovementActions.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 "SpecialAction.h" +#include "../../../../lib/mapObjects/MapObjects.h" + +namespace NKAI +{ + +namespace AIPathfinding +{ + class AdventureCastAction : public SpecialAction + { + private: + SpellID spellToCast; + const CGHeroInstance * hero; + int manaCost; + + public: + AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero); + + virtual void execute(const CGHeroInstance * hero) const override; + + virtual void applyOnDestination( + const CGHeroInstance * hero, + CDestinationNodeInfo & destination, + const PathNodeInfo & source, + AIPathNode * dstMode, + const AIPathNode * srcNode) const override; + + virtual bool canAct(const AIPathNode * source) const override; + + virtual std::string toString() const override; + }; + + class WaterWalkingAction : public AdventureCastAction + { + public: + WaterWalkingAction(const CGHeroInstance * hero); + }; + + class AirWalkingAction : public AdventureCastAction + { + public: + AirWalkingAction(const CGHeroInstance * hero); + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp index bbd1e5297..7a6ab3f10 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp @@ -114,7 +114,7 @@ namespace AIPathfinding source->manaCost); #endif - return hero->mana >= (si32)(source->manaCost + getManaCost(hero)); + return hero->mana >= source->manaCost + getManaCost(hero); } std::string SummonBoatAction::toString() const @@ -122,7 +122,7 @@ namespace AIPathfinding return "Summon Boat"; } - uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const + int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const { SpellID summonBoat = SpellID::SUMMON_BOAT; diff --git a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h index 92249eb78..5e6ca50d4 100644 --- a/AI/Nullkiller/Pathfinding/Actions/BoatActions.h +++ b/AI/Nullkiller/Pathfinding/Actions/BoatActions.h @@ -20,8 +20,6 @@ namespace AIPathfinding { class VirtualBoatAction : public SpecialAction { - public: - virtual const ChainActor * getActor(const ChainActor * sourceActor) const = 0; }; class SummonBoatAction : public VirtualBoatAction @@ -43,7 +41,7 @@ namespace AIPathfinding virtual std::string toString() const override; private: - uint32_t getManaCost(const CGHeroInstance * hero) const; + int32_t getManaCost(const CGHeroInstance * hero) const; }; class BuildBoatAction : public VirtualBoatAction diff --git a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h index c14589e75..77270ce1a 100644 --- a/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h +++ b/AI/Nullkiller/Pathfinding/Actions/SpecialAction.h @@ -22,6 +22,7 @@ namespace NKAI { struct AIPathNode; +class ChainActor; class SpecialAction { @@ -54,6 +55,11 @@ public: { return {}; } + + virtual const ChainActor * getActor(const ChainActor * sourceActor) const + { + return sourceActor; + } }; class CompositeAction : public SpecialAction diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index f555b18c3..7dbac9010 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -10,6 +10,8 @@ #include "StdInc.h" #include "AILayerTransitionRule.h" #include "../../Engine/Nullkiller.h" +#include "../../../../lib/pathfinder/CPathfinder.h" +#include "../../../../lib/pathfinder/TurnInfo.h" namespace NKAI { @@ -31,23 +33,79 @@ namespace AIPathfinding if(!destination.blocked) { - return; + if(source.node->layer == EPathfindingLayer::LAND + && (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER)) + { + if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer)) + return; + else + destination.blocked = true; + } + else + { + return; + } } if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { std::shared_ptr virtualBoat = findVirtualBoat(destination, source); - if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) + if(virtualBoat && tryUseSpecialAction(destination, source, virtualBoat, EPathNodeAction::EMBARK)) { #if NKAI_PATHFINDER_TRACE_LEVEL >= 1 logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER) + { + auto action = waterWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting water walk while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); +#endif + } + } + + if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR) + { + auto action = airWalkingActions.find(nodeStorage->getHero(source.node)); + + if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL)) + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 + logAi->trace("Casting fly while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); #endif } } } void AILayerTransitionRule::setup() + { + SpellID waterWalk = SpellID::WATER_WALK; + SpellID airWalk = SpellID::FLY; + + for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) + { + if(hero->canCastThisSpell(waterWalk.toSpell())) + { + waterWalkingActions[hero] = std::make_shared(hero); + } + + if(hero->canCastThisSpell(airWalk.toSpell())) + { + airWalkingActions[hero] = std::make_shared(hero); + } + } + + collectVirtualBoats(); + } + + void AILayerTransitionRule::collectVirtualBoats() { std::vector shipyards; @@ -113,50 +171,56 @@ namespace AIPathfinding return virtualBoat; } - bool AILayerTransitionRule::tryEmbarkVirtualBoat( + bool AILayerTransitionRule::tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const + std::shared_ptr specialAction, + EPathNodeAction targetAction) const { bool result = false; - nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) + if(!specialAction->canAct(nodeStorage->getAINode(source.node))) { - auto boatNodeOptional = nodeStorage->getOrCreateNode( - node->coord, - node->layer, - virtualBoat->getActor(node->actor)); + return false; + } - if(boatNodeOptional) + nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { - AIPathNode * boatNode = boatNodeOptional.value(); + auto castNodeOptional = nodeStorage->getOrCreateNode( + node->coord, + node->layer, + specialAction->getActor(node->actor)); - if(boatNode->action == EPathNodeAction::UNKNOWN) + if(castNodeOptional) { - boatNode->addSpecialAction(virtualBoat); - destination.blocked = false; - destination.action = EPathNodeAction::EMBARK; - destination.node = boatNode; - result = true; + AIPathNode * castNode = castNodeOptional.value(); + + if(castNode->action == EPathNodeAction::UNKNOWN) + { + castNode->addSpecialAction(specialAction); + destination.blocked = false; + destination.action = targetAction; + destination.node = castNode; + result = true; + } + else + { +#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 + logAi->trace( + "Special transition node already allocated. Blocked moving %s -> %s", + source.coord.toString(), + destination.coord.toString()); +#endif + } } else { -#if NKAI_PATHFINDER_TRACE_LEVEL >= 1 - logAi->trace( - "Special transition node already allocated. Blocked moving %s -> %s", + logAi->debug( + "Can not allocate special transition node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); -#endif } - } - else - { - logAi->debug( - "Can not allocate special transition node while moving %s -> %s", - source.coord.toString(), - destination.coord.toString()); - } - }); + }); return result; } diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h index f7d5e27b8..243cb96a9 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h @@ -13,6 +13,7 @@ #include "../AINodeStorage.h" #include "../../AIGateway.h" #include "../Actions/BoatActions.h" +#include "../Actions/AdventureSpellCastMovementActions.h" #include "../../../../CCallback.h" #include "../../../../lib/mapObjects/MapObjects.h" #include "../../../../lib/pathfinder/PathfindingRules.h" @@ -29,6 +30,8 @@ namespace AIPathfinding std::map> virtualBoats; std::shared_ptr nodeStorage; std::map> summonableVirtualBoats; + std::map> waterWalkingActions; + std::map> airWalkingActions; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr nodeStorage); @@ -41,15 +44,17 @@ namespace AIPathfinding private: void setup(); + void collectVirtualBoats(); std::shared_ptr findVirtualBoat( CDestinationNodeInfo & destination, const PathNodeInfo & source) const; - bool tryEmbarkVirtualBoat( + bool tryUseSpecialAction( CDestinationNodeInfo & destination, const PathNodeInfo & source, - std::shared_ptr virtualBoat) const; + std::shared_ptr specialAction, + EPathNodeAction targetAction) const; }; } diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 1cb3b7163..8044535fb 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -20,6 +20,7 @@ #include "../TerrainHandler.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapping/CMap.h" +#include "spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -472,6 +473,12 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her turnsInfo.reserve(16); updateTurnInfo(); initializePatrol(); + + SpellID flySpell = SpellID::FLY; + canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); + + SpellID waterWalk = SpellID::WATER_WALK; + canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell()); } CPathfinderHelper::~CPathfinderHelper() @@ -501,12 +508,18 @@ bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const if(!options.useFlying) return false; + if(canCastFly && options.canUseCast) + return true; + break; case EPathfindingLayer::WATER: if(!options.useWaterWalking) return false; + if(canCastWaterWalk && options.canUseCast) + return true; + break; } diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 0071f41eb..a52de7b65 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -72,6 +72,8 @@ public: const CGHeroInstance * hero; std::vector turnsInfo; const PathfinderOptions & options; + bool canCastFly; + bool canCastWaterWalk; CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options); virtual ~CPathfinderHelper(); diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 42af2f53b..4c83acafc 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -31,6 +31,7 @@ PathfinderOptions::PathfinderOptions() , oneTurnSpecialLayersLimit(true) , originalMovementRules(false) , turnLimit(std::numeric_limits::max()) + , canUseCast(false) { } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index f26cf6273..96d75cb2a 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -71,6 +71,11 @@ struct DLL_LINKAGE PathfinderOptions /// Max number of turns to compute. Default = infinite uint8_t turnLimit; + /// + /// For AI. Allows water walk and fly layers if hero can cast appropriate spells + /// + bool canUseCast; + PathfinderOptions(); }; From ebe155fa95af9fb27c4ebab0e5e5ffd88d5feabb Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 24 Sep 2023 13:07:42 +0300 Subject: [PATCH 0682/1248] NKAI: mana recovery --- AI/Nullkiller/Analyzers/HeroManager.cpp | 35 ++++++++++ AI/Nullkiller/Analyzers/HeroManager.h | 2 + AI/Nullkiller/Analyzers/ObjectClusterizer.cpp | 22 +++--- .../Behaviors/StayAtTownBehavior.cpp | 70 +++++++++++++++++++ AI/Nullkiller/Behaviors/StayAtTownBehavior.h | 39 +++++++++++ AI/Nullkiller/CMakeLists.txt | 4 ++ AI/Nullkiller/Engine/Nullkiller.cpp | 4 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 26 +++++++ AI/Nullkiller/Engine/PriorityEvaluator.h | 1 + AI/Nullkiller/Goals/AbstractGoal.h | 4 +- AI/Nullkiller/Goals/StayAtTown.cpp | 52 ++++++++++++++ AI/Nullkiller/Goals/StayAtTown.h | 36 ++++++++++ AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 +- AI/Nullkiller/Pathfinding/AINodeStorage.h | 1 + 14 files changed, 289 insertions(+), 11 deletions(-) create mode 100644 AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp create mode 100644 AI/Nullkiller/Behaviors/StayAtTownBehavior.h create mode 100644 AI/Nullkiller/Goals/StayAtTown.cpp create mode 100644 AI/Nullkiller/Goals/StayAtTown.h diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index b896ab728..4397620b6 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -190,6 +190,41 @@ bool HeroManager::heroCapReached() const || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); } +float HeroManager::getMagicStrength(const CGHeroInstance * hero) const +{ + auto hasFly = hero->spellbookContainsSpell(SpellID::FLY); + auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL); + auto manaLimit = hero->manaLimit(); + auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER); + auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0; + + auto score = 0.0f; + + for(auto spellId : hero->getSpellsInSpellbook()) + { + auto spell = spellId.toSpell(); + auto schoolLevel = hero->getSpellSchoolLevel(spell); + + score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f; + } + + vstd::amin(score, 1); + + score *= std::min(1.0f, spellPower / 10.0f); + + if(hasFly) + score += 0.3f; + + if(hasTownPortal && hasEarth) + score += 0.6f; + + vstd::amin(score, 1); + + score *= std::min(1.0f, manaLimit / 100.0f); + + return std::min(score, 1.0f); +} + bool HeroManager::canRecruitHero(const CGTownInstance * town) const { if(!town) diff --git a/AI/Nullkiller/Analyzers/HeroManager.h b/AI/Nullkiller/Analyzers/HeroManager.h index 1009fd31e..a7744ad1f 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.h +++ b/AI/Nullkiller/Analyzers/HeroManager.h @@ -34,6 +34,7 @@ public: virtual bool heroCapReached() const = 0; virtual const CGHeroInstance * findHeroWithGrail() const = 0; virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0; + virtual float getMagicStrength(const CGHeroInstance * hero) const = 0; }; class DLL_EXPORT ISecondarySkillRule @@ -76,6 +77,7 @@ public: bool heroCapReached() const override; const CGHeroInstance * findHeroWithGrail() const override; const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override; + float getMagicStrength(const CGHeroInstance * hero) const override; private: float evaluateFightingStrength(const CGHeroInstance * hero) const; diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 5e2b41977..a228f1b4d 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -94,16 +94,22 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons { for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++) { - auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); - auto blockers = ai->cb->getVisitableObjs(node->coord); - - if(guardPos.valid()) - { - auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + std::vector blockers = {}; - if(guard) + if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL) + { + auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); + + blockers = ai->cb->getVisitableObjs(node->coord); + + if(guardPos.valid()) { - blockers.insert(blockers.begin(), guard); + auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord)); + + if(guard) + { + blockers.insert(blockers.begin(), guard); + } } } diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp new file mode 100644 index 000000000..2f7c89b2f --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp @@ -0,0 +1,70 @@ +/* +* StartupBehavior.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 "StayAtTownBehavior.h" +#include "../AIGateway.h" +#include "../AIUtility.h" +#include "../Goals/StayAtTown.h" +#include "../Goals/Composition.h" +#include "../Goals/ExecuteHeroChain.h" +#include "lib/mapObjects/MapObjects.h" //for victory conditions +#include "../Engine/Nullkiller.h" + +namespace NKAI +{ + +using namespace Goals; + +std::string StayAtTownBehavior::toString() const +{ + return "StayAtTownBehavior"; +} + +Goals::TGoalVec StayAtTownBehavior::decompose() const +{ + Goals::TGoalVec tasks; + auto towns = cb->getTownsInfo(); + + if(!towns.size()) + return tasks; + + for(auto town : towns) + { + if(!town->hasBuilt(BuildingID::MAGES_GUILD_1)) + continue; + + auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos()); + + for(auto & path : paths) + { + if(town->visitingHero && town->visitingHero.get() != path.targetHero) + continue; + + if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1) + { + if(path.targetHero->mana == path.targetHero->manaLimit()) + continue; + + Composition stayAtTown; + + stayAtTown.addNextSequence({ + sptr(ExecuteHeroChain(path)), + sptr(StayAtTown(town, path)) + }); + + tasks.push_back(sptr(stayAtTown)); + } + } + } + + return tasks; +} + +} diff --git a/AI/Nullkiller/Behaviors/StayAtTownBehavior.h b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h new file mode 100644 index 000000000..260cf136a --- /dev/null +++ b/AI/Nullkiller/Behaviors/StayAtTownBehavior.h @@ -0,0 +1,39 @@ +/* +* StayAtTownBehavior.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/VCMI_Lib.h" +#include "../Goals/CGoal.h" +#include "../AIUtility.h" + +namespace NKAI +{ +namespace Goals +{ + class StayAtTownBehavior : public CGoal + { + public: + StayAtTownBehavior() + :CGoal(STAY_AT_TOWN_BEHAVIOR) + { + } + + virtual TGoalVec decompose() const override; + virtual std::string toString() const override; + + virtual bool operator==(const StayAtTownBehavior & other) const override + { + return true; + } + }; +} + + +} diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index a22641684..042cb5a0d 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -35,6 +35,7 @@ set(Nullkiller_SRCS Goals/ExecuteHeroChain.cpp Goals/ExchangeSwapTownHeroes.cpp Goals/CompleteQuest.cpp + Goals/StayAtTown.cpp Markers/ArmyUpgrade.cpp Markers/HeroExchange.cpp Markers/UnlockCluster.cpp @@ -53,6 +54,7 @@ set(Nullkiller_SRCS Behaviors/BuildingBehavior.cpp Behaviors/GatherArmyBehavior.cpp Behaviors/ClusterBehavior.cpp + Behaviors/StayAtTownBehavior.cpp Helpers/ArmyFormation.cpp AIGateway.cpp ) @@ -99,6 +101,7 @@ set(Nullkiller_HEADERS Goals/ExchangeSwapTownHeroes.h Goals/CompleteQuest.h Goals/Goals.h + Goals/StayAtTown.h Markers/ArmyUpgrade.h Markers/HeroExchange.h Markers/UnlockCluster.h @@ -117,6 +120,7 @@ set(Nullkiller_HEADERS Behaviors/BuildingBehavior.h Behaviors/GatherArmyBehavior.h Behaviors/ClusterBehavior.h + Behaviors/StayAtTownBehavior.h Helpers/ArmyFormation.h AIGateway.h ) diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index b69073f33..3d9a78c33 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -18,6 +18,7 @@ #include "../Behaviors/BuildingBehavior.h" #include "../Behaviors/GatherArmyBehavior.h" #include "../Behaviors/ClusterBehavior.h" +#include "../Behaviors/StayAtTownBehavior.h" #include "../Goals/Invalid.h" #include "../Goals/Composition.h" @@ -262,7 +263,8 @@ void Nullkiller::makeTurn() choseBestTask(sptr(CaptureObjectsBehavior()), 1), choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH), choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH), - choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH) + choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH), + choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH) }; if(cb->getDate(Date::DAY) == 1) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 202644db3..1888c876a 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -22,6 +22,7 @@ #include "../../../lib/filesystem/Filesystem.h" #include "../Goals/ExecuteHeroChain.h" #include "../Goals/BuildThis.h" +#include "../Goals/StayAtTown.h" #include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/DismissHero.h" #include "../Markers/UnlockCluster.h" @@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward( : 0; case Obj::PANDORAS_BOX: return 5000; + case Obj::MAGIC_WELL: + case Obj::MAGIC_SPRING: + return getManaRecoveryArmyReward(hero); default: return 0; } @@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const return result; } +uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const +{ + return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast(hero->mana) / hero->manaLimit())); +} + float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const { if(!target) @@ -693,6 +702,22 @@ public: } }; +class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder +{ +public: + virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override + { + if(task->goalType != Goals::STAY_AT_TOWN) + return; + + Goals::StayAtTown & stayAtTown = dynamic_cast(*task); + + evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get()); + evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); + evaluationContext.movementCost += stayAtTown.getMovementWasted(); + } +}; + void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength) { HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn); @@ -998,6 +1023,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai) evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared()); evaluationContextBuilders.push_back(std::make_shared(ai)); + evaluationContextBuilders.push_back(std::make_shared()); } EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 5ab2a6082..4853e4aed 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -49,6 +49,7 @@ public: uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const; uint64_t townArmyGrowth(const CGTownInstance * town) const; + uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const; }; struct DLL_EXPORT EvaluationContext diff --git a/AI/Nullkiller/Goals/AbstractGoal.h b/AI/Nullkiller/Goals/AbstractGoal.h index a5b170c9f..d089083bf 100644 --- a/AI/Nullkiller/Goals/AbstractGoal.h +++ b/AI/Nullkiller/Goals/AbstractGoal.h @@ -71,7 +71,9 @@ namespace Goals ARMY_UPGRADE, DEFEND_TOWN, CAPTURE_OBJECT, - SAVE_RESOURCES + SAVE_RESOURCES, + STAY_AT_TOWN_BEHAVIOR, + STAY_AT_TOWN }; class DLL_EXPORT TSubgoal : public std::shared_ptr diff --git a/AI/Nullkiller/Goals/StayAtTown.cpp b/AI/Nullkiller/Goals/StayAtTown.cpp new file mode 100644 index 000000000..82342cb51 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.cpp @@ -0,0 +1,52 @@ +/* +* ArmyUpgrade.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 "StayAtTown.h" +#include "../AIGateway.h" +#include "../Engine/Nullkiller.h" +#include "../AIUtility.h" + +namespace NKAI +{ + +using namespace Goals; + +StayAtTown::StayAtTown(const CGTownInstance * town, AIPath & path) + : ElementarGoal(Goals::STAY_AT_TOWN) +{ + sethero(path.targetHero); + settown(town); + movementWasted = static_cast(hero->movementPointsRemaining()) / hero->movementPointsLimit(!hero->boat) - path.movementCost(); + vstd::amax(movementWasted, 0); +} + +bool StayAtTown::operator==(const StayAtTown & other) const +{ + return hero == other.hero && town == other.town; +} + +std::string StayAtTown::toString() const +{ + return "Stay at town " + town->getNameTranslated() + + " hero " + hero->getNameTranslated() + + ", mana: " + std::to_string(hero->mana); +} + +void StayAtTown::accept(AIGateway * ai) +{ + if(hero->visitedTown != town) + { + logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated()); + } + + ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE); +} + +} diff --git a/AI/Nullkiller/Goals/StayAtTown.h b/AI/Nullkiller/Goals/StayAtTown.h new file mode 100644 index 000000000..880881386 --- /dev/null +++ b/AI/Nullkiller/Goals/StayAtTown.h @@ -0,0 +1,36 @@ +/* +* ArmyUpgrade.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 "../Goals/CGoal.h" +#include "../Pathfinding/AINodeStorage.h" +#include "../Analyzers/ArmyManager.h" +#include "../Analyzers/DangerHitMapAnalyzer.h" + +namespace NKAI +{ +namespace Goals +{ + class DLL_EXPORT StayAtTown : public ElementarGoal + { + private: + float movementWasted; + + public: + StayAtTown(const CGTownInstance * town, AIPath & path); + + virtual bool operator==(const StayAtTown & other) const override; + virtual std::string toString() const override; + void accept(AIGateway * ai) override; + float getMovementWasted() const { return movementWasted; } + }; +} + +} diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 7707033eb..067525c3e 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -279,9 +279,10 @@ void AINodeStorage::commit( #if NKAI_PATHFINDER_TRACE_LEVEL >= 2 logAi->trace( - "Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", + "Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld", source->coord.toString(), destination->coord.toString(), + destination->layer, destination->getCost(), std::to_string(destination->turns), destination->moveRemains, @@ -1343,6 +1344,7 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa pathNode.coord = node->coord; pathNode.parentIndex = parentIndex; pathNode.actionIsBlocked = false; + pathNode.layer = node->layer; if(pathNode.specialAction) { diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 71464509d..068304955 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -65,6 +65,7 @@ struct AIPathNodeInfo float cost; uint8_t turns; int3 coord; + EPathfindingLayer layer; uint64_t danger; const CGHeroInstance * targetHero; int parentIndex; From 718bafc8aff1ec01ee7e518e61ebf510174384ee Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 15:05:12 +0200 Subject: [PATCH 0683/1248] map overview rework --- client/CMakeLists.txt | 2 + client/lobby/SelectionTab.cpp | 122 +------------------- client/lobby/SelectionTab.h | 19 --- client/windows/CMapOverview.cpp | 197 ++++++++++++++++++++++++++++++++ client/windows/CMapOverview.h | 58 ++++++++++ 5 files changed, 259 insertions(+), 139 deletions(-) create mode 100644 client/windows/CMapOverview.cpp create mode 100644 client/windows/CMapOverview.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ed53ca99a..2912d02cf 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -119,6 +119,7 @@ set(client_SRCS windows/CHeroOverview.cpp windows/CHeroWindow.cpp windows/CKingdomInterface.cpp + windows/CMapOverview.cpp windows/CMessage.cpp windows/CPuzzleWindow.cpp windows/CQuestLog.cpp @@ -285,6 +286,7 @@ set(client_HEADERS windows/CHeroWindow.h windows/CKingdomInterface.h windows/CMessage.h + windows/CMapOverview.h windows/CPuzzleWindow.h windows/CQuestLog.h windows/CSpellWindow.h diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 34375db26..8a3b4b494 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -27,11 +27,10 @@ #include "../widgets/TextControls.h" #include "../windows/GUIClasses.h" #include "../windows/InfoWindows.h" +#include "../windows/CMapOverview.h" #include "../render/CAnimation.h" -#include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" -#include "../render/Graphics.h" #include "../../CCallback.h" @@ -41,8 +40,6 @@ #include "../../lib/GameSettings.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/campaign/CampaignState.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/mapping/CMapService.h" #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" @@ -371,7 +368,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) if(curItems[py]->date != "") text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - GH.windows().createAndPushWindow(text, ResourcePath(curItems[py]->fileURI), tabType); + GH.windows().createAndPushWindow(text, ResourcePath(curItems[py]->fileURI), tabType); } else CRClickPopup::createAndPush(curItems[py]->folderName); @@ -822,121 +819,6 @@ std::unordered_set SelectionTab::getFiles(std::string dirURI, ERes return ret; } -SelectionTab::CMapInfoTooltipBox::CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType) - : CWindowObject(BORDERED | RCLICK_POPUP) -{ - drawPlayerElements = tabType == ESelectionScreen::newGame; - renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); - - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - std::vector> mapLayerImages; - if(renderImage) - mapLayerImages = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), IMAGE_SIZE); - - if(mapLayerImages.size() == 0) - renderImage = false; - - pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000); - - auto drawLabel = [&]() { - label = std::make_shared(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - if(!label->slider) - label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y)); - }; - drawLabel(); - - int textHeight = std::min(350, label->label->textSize.y); - pos.h = BORDER + textHeight + BORDER; - if(renderImage) - pos.h += IMAGE_SIZE + BORDER; - backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); - updateShadow(); - - drawLabel(); - - if(renderImage) - { - if(mapLayerImages.size() == 1) - image1 = std::make_shared(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER)); - else - { - image1 = std::make_shared(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER)); - image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER)); - } - } - - center(GH.getCursorPosition()); //center on mouse -#ifdef VCMI_MOBILE - moveBy({0, -pos.h / 2}); -#endif - fitToScreen(10); -} - -Canvas SelectionTab::CMapInfoTooltipBox::createMinimapForLayer(std::unique_ptr & map, int layer) -{ - Canvas canvas = Canvas(Point(map->width, map->height)); - - for (int y = 0; y < map->height; ++y) - for (int x = 0; x < map->width; ++x) - { - TerrainTile & tile = map->getTile(int3(x, y, layer)); - - ColorRGBA color = tile.terType->minimapUnblocked; - if (tile.blocked && (!tile.visitable)) - color = tile.terType->minimapBlocked; - - if(drawPlayerElements) - // if object at tile is owned - it will be colored as its owner - for (const CGObjectInstance *obj : tile.blockingObjects) - { - PlayerColor player = obj->getOwner(); - if(player == PlayerColor::NEUTRAL) - { - color = graphics->neutralColor; - break; - } - if (player.isValidPlayer()) - { - color = graphics->playerColors[player.getNum()]; - break; - } - } - - canvas.drawPoint(Point(x, y), color); - } - - return canvas; -} - -std::vector> SelectionTab::CMapInfoTooltipBox::createMinimaps(ResourcePath resource, int size) -{ - std::vector> ret = std::vector>(); - - CMapService mapService; - std::unique_ptr map; - try - { - map = mapService.loadMap(resource); - } - catch (...) - { - return ret; - } - - for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) - { - Canvas canvas = createMinimapForLayer(map, i); - Canvas canvasScaled = Canvas(Point(size, size)); - canvasScaled.drawScaled(canvas, Point(0, 0), Point(size, size)); - std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); - - ret.push_back(img); - } - - return ret; -} - SelectionTab::ListItem::ListItem(Point position, std::shared_ptr iconsFormats, std::shared_ptr iconsVictory, std::shared_ptr iconsLoss) : CIntObject(LCLICK, position) { diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index e4d67ef83..50f501769 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -67,25 +67,6 @@ class SelectionTab : public CIntObject // FIXME: CSelectionBase use them too! std::shared_ptr iconsVictoryCondition; std::shared_ptr iconsLossCondition; - - class CMapInfoTooltipBox : public CWindowObject - { - const int IMAGE_SIZE = 169; - const int BORDER = 30; - - bool drawPlayerElements; - bool renderImage; - - std::shared_ptr backgroundTexture; - std::shared_ptr label; - std::shared_ptr image1; - std::shared_ptr image2; - - Canvas createMinimapForLayer(std::unique_ptr & map, int layer); - std::vector> createMinimaps(ResourcePath resource, int size); - public: - CMapInfoTooltipBox(std::string text, ResourcePath resource, ESelectionScreen tabType); - }; public: std::vector> allItems; std::vector> curItems; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp new file mode 100644 index 000000000..7f05e68b1 --- /dev/null +++ b/client/windows/CMapOverview.cpp @@ -0,0 +1,197 @@ +/* + * CMapOverview.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 "CMapOverview.h" + +#include "../lobby/SelectionTab.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/CComponent.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/TextControls.h" +#include "../windows/GUIClasses.h" +#include "../windows/InfoWindows.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/Graphics.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/campaign/CampaignState.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/mapping/CMapService.h" +#include "../../lib/mapping/CMapInfo.h" +#include "../../lib/mapping/CMapHeader.h" +#include "../../lib/mapping/MapFormat.h" +#include "../../lib/TerrainHandler.h" + +CMapOverview::CMapOverview(std::string text, ResourcePath resource, ESelectionScreen tabType) + : CWindowObject(BORDERED | RCLICK_POPUP) +{ + + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(0, 0, 400, 300); + + widget = std::make_shared(text, resource, tabType); + + updateShadow(); + + /* + std::vector> mapLayerImages; + if(renderImage) + mapLayerImages = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), IMAGE_SIZE); + + if(mapLayerImages.size() == 0) + renderImage = false; + + pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000); + + auto drawLabel = [&]() { + label = std::make_shared(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + if(!label->slider) + label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y)); + }; + drawLabel(); + + int textHeight = std::min(350, label->label->textSize.y); + pos.h = BORDER + textHeight + BORDER; + if(renderImage) + pos.h += IMAGE_SIZE + BORDER; + backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); + updateShadow(); + + drawLabel(); + + if(renderImage) + { + if(mapLayerImages.size() == 1) + image1 = std::make_shared(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER)); + else + { + image1 = std::make_shared(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER)); + image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER)); + } + } + */ + + center(GH.getCursorPosition()); //center on mouse +#ifdef VCMI_MOBILE + moveBy({0, -pos.h / 2}); +#endif + fitToScreen(10); +} + +Canvas CMapOverview::CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const +{ + Canvas canvas = Canvas(Point(map->width, map->height)); + + for (int y = 0; y < map->height; ++y) + for (int x = 0; x < map->width; ++x) + { + TerrainTile & tile = map->getTile(int3(x, y, layer)); + + ColorRGBA color = tile.terType->minimapUnblocked; + if (tile.blocked && (!tile.visitable)) + color = tile.terType->minimapBlocked; + + if(drawPlayerElements) + // if object at tile is owned - it will be colored as its owner + for (const CGObjectInstance *obj : tile.blockingObjects) + { + PlayerColor player = obj->getOwner(); + if(player == PlayerColor::NEUTRAL) + { + color = graphics->neutralColor; + break; + } + if (player.isValidPlayer()) + { + color = graphics->playerColors[player.getNum()]; + break; + } + } + + canvas.drawPoint(Point(x, y), color); + } + + return canvas; +} + +std::vector> CMapOverview::CMapOverviewWidget::createMinimaps(ResourcePath resource, Point size) const +{ + std::vector> ret = std::vector>(); + + CMapService mapService; + std::unique_ptr map; + try + { + map = mapService.loadMap(resource); + } + catch (...) + { + return ret; + } + + for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) + { + Canvas canvas = createMinimapForLayer(map, i); + Canvas canvasScaled = Canvas(size); + canvasScaled.drawScaled(canvas, Point(0, 0), size); + std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); + + ret.push_back(img); + } + + return ret; +} + +std::shared_ptr CMapOverview::CMapOverviewWidget::CMapOverviewWidget::buildDrawTransparentRect(const JsonNode & config) const +{ + logGlobal->debug("Building widget drawTransparentRect"); + + auto rect = readRect(config["rect"]); + auto color = readColor(config["color"]); + if(!config["colorLine"].isNull()) + { + auto colorLine = readColor(config["colorLine"]); + return std::make_shared(rect, color, colorLine); + } + return std::make_shared(rect, color); +} + +std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const +{ + logGlobal->debug("Building widget drawMinimap"); + + auto rect = readRect(config["rect"]); + auto id = config["id"].Integer(); + const std::vector> images = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), Point(rect.w, rect.h)); + + return std::make_shared(images[id], Point(rect.x, rect.y)); +} + +CMapOverview::CMapOverviewWidget::CMapOverviewWidget(std::string text, ResourcePath resource, ESelectionScreen tabType): + InterfaceObjectConfigurable(), resource(resource) +{ + drawPlayerElements = tabType == ESelectionScreen::newGame; + renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); + + const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); + + REGISTER_BUILDER("drawTransparentRect", &CMapOverview::CMapOverviewWidget::buildDrawTransparentRect); + REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); + + build(config); +} \ No newline at end of file diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h new file mode 100644 index 000000000..cd211a87d --- /dev/null +++ b/client/windows/CMapOverview.h @@ -0,0 +1,58 @@ +/* + * CMapOverview.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 + +VCMI_LIB_NAMESPACE_BEGIN +class CMap; +VCMI_LIB_NAMESPACE_END +#include "CWindowObject.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../gui/InterfaceObjectConfigurable.h" + +class CSlider; +class CLabel; +class CPicture; +class CFilledTexture; +class CTextBox; +class IImage; +class Canvas; +class TransparentFilledRectangle; +enum ESelectionScreen : ui8; + +class CMapOverview : public CWindowObject +{ + //const int IMAGE_SIZE = 169; + //const int BORDER = 30; + + std::shared_ptr backgroundTexture; + std::shared_ptr label; + std::shared_ptr image1; + std::shared_ptr image2; + + class CMapOverviewWidget : public InterfaceObjectConfigurable + { + ResourcePath resource; + + bool drawPlayerElements; + bool renderImage; + Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; + std::vector> createMinimaps(ResourcePath resource, Point size) const; + + std::shared_ptr buildDrawTransparentRect(const JsonNode & config) const; + std::shared_ptr buildDrawMinimap(const JsonNode & config) const; + public: + CMapOverviewWidget(std::string text, ResourcePath resource, ESelectionScreen tabType); + }; + + std::shared_ptr widget; + +public: + CMapOverview(std::string text, ResourcePath resource, ESelectionScreen tabType); +}; \ No newline at end of file From 79e66c38df63d80bd92940e08eb1f56d4bdcb664 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:14:22 +0200 Subject: [PATCH 0684/1248] layout --- client/lobby/SelectionTab.cpp | 8 +- client/windows/CMapOverview.cpp | 102 +++++++++++++------------ client/windows/CMapOverview.h | 22 +++--- config/widgets/mapOverview.json | 130 ++++++++++++++++++++++++++++++++ 4 files changed, 197 insertions(+), 65 deletions(-) create mode 100644 config/widgets/mapOverview.json diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 8a3b4b494..c66deb246 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -363,13 +363,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition) return; if(!curItems[py]->isFolder) - { - std::string text = boost::str(boost::format("{%1%}\r\n\r\n%2%:\r\n%3%") % curItems[py]->getName() % CGI->generaltexth->translate("vcmi.lobby.filename") % curItems[py]->fullFileURI); - if(curItems[py]->date != "") - text += boost::str(boost::format("\r\n\r\n%1%:\r\n%2%") % CGI->generaltexth->translate("vcmi.lobby.creationDate") % curItems[py]->date); - - GH.windows().createAndPushWindow(text, ResourcePath(curItems[py]->fileURI), tabType); - } + GH.windows().createAndPushWindow(curItems[py]->getName(), curItems[py]->fullFileURI, curItems[py]->date, ResourcePath(curItems[py]->fileURI), tabType); else CRClickPopup::createAndPush(curItems[py]->folderName); } diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 7f05e68b1..3ac103963 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -36,56 +36,19 @@ #include "../../lib/mapping/MapFormat.h" #include "../../lib/TerrainHandler.h" -CMapOverview::CMapOverview(std::string text, ResourcePath resource, ESelectionScreen tabType) - : CWindowObject(BORDERED | RCLICK_POPUP) +CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType) + : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(0, 0, 400, 300); + const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); + pos = Rect(0, 0, config["items"][0]["rect"]["w"].Integer(), config["items"][0]["rect"]["h"].Integer()); - widget = std::make_shared(text, resource, tabType); + widget = std::make_shared(*this); updateShadow(); - /* - std::vector> mapLayerImages; - if(renderImage) - mapLayerImages = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), IMAGE_SIZE); - - if(mapLayerImages.size() == 0) - renderImage = false; - - pos = Rect(0, 0, 3 * BORDER + 2 * IMAGE_SIZE, 2000); - - auto drawLabel = [&]() { - label = std::make_shared(text, Rect(BORDER, BORDER, BORDER + 2 * IMAGE_SIZE, 350), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - if(!label->slider) - label->resize(Point(BORDER + 2 * IMAGE_SIZE, label->label->textSize.y)); - }; - drawLabel(); - - int textHeight = std::min(350, label->label->textSize.y); - pos.h = BORDER + textHeight + BORDER; - if(renderImage) - pos.h += IMAGE_SIZE + BORDER; - backgroundTexture = std::make_shared(ImagePath::builtin("DIBOXBCK"), pos); - updateShadow(); - - drawLabel(); - - if(renderImage) - { - if(mapLayerImages.size() == 1) - image1 = std::make_shared(mapLayerImages[0], Point(BORDER + (BORDER + IMAGE_SIZE) / 2, textHeight + 2 * BORDER)); - else - { - image1 = std::make_shared(mapLayerImages[0], Point(BORDER, textHeight + 2 * BORDER)); - image2 = std::make_shared(mapLayerImages[1], Point(BORDER + IMAGE_SIZE + BORDER, textHeight + 2 * BORDER)); - } - } - */ - center(GH.getCursorPosition()); //center on mouse #ifdef VCMI_MOBILE moveBy({0, -pos.h / 2}); @@ -177,21 +140,66 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con auto rect = readRect(config["rect"]); auto id = config["id"].Integer(); - const std::vector> images = createMinimaps(ResourcePath(resource.getName(), EResType::MAP), Point(rect.w, rect.h)); + + if(!renderImage) + return nullptr; + + const std::vector> images = createMinimaps(ResourcePath(parent.resource.getName(), EResType::MAP), Point(rect.w, rect.h)); + + if(id >= images.size()) + return nullptr; return std::make_shared(images[id], Point(rect.x, rect.y)); } -CMapOverview::CMapOverviewWidget::CMapOverviewWidget(std::string text, ResourcePath resource, ESelectionScreen tabType): - InterfaceObjectConfigurable(), resource(resource) +std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawPath(const JsonNode & config) const { - drawPlayerElements = tabType == ESelectionScreen::newGame; - renderImage = tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); + logGlobal->debug("Building widget drawPath"); + + auto rect = readRect(config["rect"]); + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + + return std::make_shared(parent.fileName, rect, 0, font, alignment, color); +} + +std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawString(const JsonNode & config) const +{ + logGlobal->debug("Building widget drawString"); + + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + std::string text = ""; + if("mapname" == config["text"].String()) + text = parent.mapName; + if("date" == config["text"].String()) + if(parent.date.empty()) + { + //std::time_t time = + } + else + text = parent.date; + auto position = readPosition(config["position"]); + return std::make_shared(position.x, position.y, font, alignment, color, text); +} + +CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): + InterfaceObjectConfigurable(), parent(parent) +{ + drawPlayerElements = parent.tabType == ESelectionScreen::newGame; + renderImage = parent.tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); REGISTER_BUILDER("drawTransparentRect", &CMapOverview::CMapOverviewWidget::buildDrawTransparentRect); REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); + REGISTER_BUILDER("drawPath", &CMapOverview::CMapOverviewWidget::buildDrawPath); + REGISTER_BUILDER("drawString", &CMapOverview::CMapOverviewWidget::buildDrawString); + + drawPlayerElements = parent.tabType == ESelectionScreen::newGame; + renderImage = parent.tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); build(config); } \ No newline at end of file diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index cd211a87d..782b8b2b6 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -28,17 +28,9 @@ enum ESelectionScreen : ui8; class CMapOverview : public CWindowObject { - //const int IMAGE_SIZE = 169; - //const int BORDER = 30; - - std::shared_ptr backgroundTexture; - std::shared_ptr label; - std::shared_ptr image1; - std::shared_ptr image2; - class CMapOverviewWidget : public InterfaceObjectConfigurable { - ResourcePath resource; + CMapOverview& parent; bool drawPlayerElements; bool renderImage; @@ -47,12 +39,20 @@ class CMapOverview : public CWindowObject std::shared_ptr buildDrawTransparentRect(const JsonNode & config) const; std::shared_ptr buildDrawMinimap(const JsonNode & config) const; + std::shared_ptr buildDrawPath(const JsonNode & config) const; + std::shared_ptr buildDrawString(const JsonNode & config) const; public: - CMapOverviewWidget(std::string text, ResourcePath resource, ESelectionScreen tabType); + CMapOverviewWidget(CMapOverview& parent); }; std::shared_ptr widget; public: - CMapOverview(std::string text, ResourcePath resource, ESelectionScreen tabType); + ResourcePath resource; + std::string mapName; + std::string fileName; + std::string date; + ESelectionScreen tabType; + + CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType); }; \ No newline at end of file diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json new file mode 100644 index 000000000..b626cf878 --- /dev/null +++ b/config/widgets/mapOverview.json @@ -0,0 +1,130 @@ +{ + "items": + [ + { + "name": "background", + "type": "texture", + "image": "DIBOXBCK", + "rect": {"w": 428, "h": 500} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 5, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.filename", + "position": {"x": 214, "y": 15} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 30, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "drawString", + "font": "small", + "alignment": "center", + "color": "white", + "text": "mapname", + "position": {"x": 214, "y": 40} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 55, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.filename", + "position": {"x": 214, "y": 65} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 29, "y": 79, "w": 171, "h": 171}, + "color": [0, 0, 0, 255], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "drawMinimap", + "id": 0, + "rect": {"x": 30, "y": 80, "w": 169, "h": 169} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 228, "y": 79, "w": 171, "h": 171}, + "color": [0, 0, 0, 255], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "drawMinimap", + "id": 1, + "rect": {"x": 229, "y": 80, "w": 169, "h": 169} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 254, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.creationDate", + "position": {"x": 214, "y": 264} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 279, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "drawString", + "font": "small", + "alignment": "center", + "color": "white", + "text": "date", + "position": {"x": 214, "y": 289} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 309, "w": 418, "h": 20}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "label", + "font": "medium", + "alignment": "center", + "color": "yellow", + "text": "vcmi.lobby.filename", + "position": {"x": 214, "y": 319} + }, + { + "type": "drawTransparentRect", + "rect": {"x": 5, "y": 334, "w": 418, "h": 45}, + "color": [0, 0, 0, 75], + "colorLine": [128, 100, 75, 255] + }, + { + "type": "drawPath", + "font": "small", + "alignment": "center", + "color": "white", + "rect": {"x": 10, "y": 339, "w": 408, "h": 35} + } + ] +} From c6adcc40f5bed57b95c8dabbca89961da8dfd173 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 17:56:51 +0200 Subject: [PATCH 0685/1248] add date; cleanup --- Mods/vcmi/config/vcmi/english.json | 2 ++ Mods/vcmi/config/vcmi/german.json | 4 +++- client/windows/CMapOverview.cpp | 8 +++++++- client/windows/CMapOverview.h | 10 +++++----- config/widgets/mapOverview.json | 6 +++--- 5 files changed, 20 insertions(+), 10 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index a14081798..e5a8b3975 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -51,6 +51,8 @@ "vcmi.lobby.filename" : "Filename", "vcmi.lobby.creationDate" : "Creation date", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Map preview", "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index abe112ebd..3548c4ea6 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -50,6 +50,8 @@ "vcmi.lobby.filename" : "Dateiname", "vcmi.lobby.creationDate" : "Erstellungsdatum", + "vcmi.lobby.scenarioName" : "Szenario-Name", + "vcmi.lobby.mapPreview" : "Kartenvorschau", "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", @@ -361,4 +363,4 @@ "core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule", "core.bonus.WIDE_BREATH.name": "Breiter Atem", "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)" -} +} \ No newline at end of file diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 3ac103963..66b555b22 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -13,6 +13,8 @@ #include "../lobby/SelectionTab.h" +#include + #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" @@ -35,6 +37,7 @@ #include "../../lib/mapping/CMapHeader.h" #include "../../lib/mapping/MapFormat.h" #include "../../lib/TerrainHandler.h" +#include "../../lib/filesystem/Filesystem.h" CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType) : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType) @@ -175,12 +178,15 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawString(const if("mapname" == config["text"].String()) text = parent.mapName; if("date" == config["text"].String()) + { if(parent.date.empty()) { - //std::time_t time = + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(parent.resource.getName(), EResType::MAP))); + text = vstd::getFormattedDateTime(time); } else text = parent.date; + } auto position = readPosition(config["position"]); return std::make_shared(position.x, position.y, font, alignment, color, text); } diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 782b8b2b6..545ea4bcb 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -48,11 +48,11 @@ class CMapOverview : public CWindowObject std::shared_ptr widget; public: - ResourcePath resource; - std::string mapName; - std::string fileName; - std::string date; - ESelectionScreen tabType; + const ResourcePath resource; + const std::string mapName; + const std::string fileName; + const std::string date; + const ESelectionScreen tabType; CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType); }; \ No newline at end of file diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index b626cf878..e397d8909 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -5,7 +5,7 @@ "name": "background", "type": "texture", "image": "DIBOXBCK", - "rect": {"w": 428, "h": 500} + "rect": {"w": 428, "h": 384} }, { "type": "drawTransparentRect", @@ -18,7 +18,7 @@ "font": "medium", "alignment": "center", "color": "yellow", - "text": "vcmi.lobby.filename", + "text": "vcmi.lobby.scenarioName", "position": {"x": 214, "y": 15} }, { @@ -46,7 +46,7 @@ "font": "medium", "alignment": "center", "color": "yellow", - "text": "vcmi.lobby.filename", + "text": "vcmi.lobby.mapPreview", "position": {"x": 214, "y": 65} }, { From 8b835c92538de65b046d2b2f6ddc945d5cf1d0f7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 20:03:33 +0200 Subject: [PATCH 0686/1248] load game support --- client/windows/CMapOverview.cpp | 39 ++++++++++++++++++++++++++++----- client/windows/CMapOverview.h | 1 + lib/StartInfo.h | 4 +++- server/CVCMIServer.cpp | 2 ++ 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 66b555b22..0f2afef00 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -39,6 +39,10 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/filesystem/Filesystem.h" +#include "../../lib/serializer/BinaryDeserializer.h" +#include "../../lib/StartInfo.h" +#include "../../lib/rmg/CMapGenOptions.h" + CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType) : CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType) { @@ -110,6 +114,13 @@ std::vector> CMapOverview::CMapOverviewWidget::createMin return ret; } + return createMinimaps(map, size); +} + +std::vector> CMapOverview::CMapOverviewWidget::createMinimaps(std::unique_ptr & map, Point size) const +{ + std::vector> ret = std::vector>(); + for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) { Canvas canvas = createMinimapForLayer(map, i); @@ -147,7 +158,28 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con if(!renderImage) return nullptr; - const std::vector> images = createMinimaps(ResourcePath(parent.resource.getName(), EResType::MAP), Point(rect.w, rect.h)); + ResourcePath res = ResourcePath(parent.resource.getName(), EResType::MAP); + std::unique_ptr campaignMap = nullptr; + if(parent.tabType != ESelectionScreen::newGame) + { + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(parent.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + std::unique_ptr mapHeader = std::make_unique(); + StartInfo * startInfo; + lf >> *(mapHeader) >> startInfo; + + if(startInfo->campState) + campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); + res = ResourcePath(startInfo->fileURI, EResType::MAP); + } + + std::vector> images; + if(!campaignMap) + images = createMinimaps(res, Point(rect.w, rect.h)); + else + images = createMinimaps(campaignMap, Point(rect.w, rect.h)); + if(id >= images.size()) return nullptr; @@ -195,7 +227,7 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): InterfaceObjectConfigurable(), parent(parent) { drawPlayerElements = parent.tabType == ESelectionScreen::newGame; - renderImage = parent.tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); + renderImage = settings["lobby"]["mapPreview"].Bool(); const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); @@ -204,8 +236,5 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): REGISTER_BUILDER("drawPath", &CMapOverview::CMapOverviewWidget::buildDrawPath); REGISTER_BUILDER("drawString", &CMapOverview::CMapOverviewWidget::buildDrawString); - drawPlayerElements = parent.tabType == ESelectionScreen::newGame; - renderImage = parent.tabType == ESelectionScreen::newGame && settings["lobby"]["mapPreview"].Bool(); - build(config); } \ No newline at end of file diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 545ea4bcb..7c70210d8 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -36,6 +36,7 @@ class CMapOverview : public CWindowObject bool renderImage; Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; std::vector> createMinimaps(ResourcePath resource, Point size) const; + std::vector> createMinimaps(std::unique_ptr & map, Point size) const; std::shared_ptr buildDrawTransparentRect(const JsonNode & config) const; std::shared_ptr buildDrawMinimap(const JsonNode & config) const; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index a558c4d32..b4031fa42 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -102,6 +102,7 @@ struct DLL_LINKAGE StartInfo ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet ui32 mapfileChecksum; //0 if not relevant std::string startTimeIso8601; + std::string fileURI; SimturnsInfo simturnsInfo; TurnTimerInfo turnTimerInfo; std::string mapname; // empty for random map, otherwise name of the map or savegame @@ -127,6 +128,7 @@ struct DLL_LINKAGE StartInfo h & seedPostInit; h & mapfileChecksum; h & startTimeIso8601; + h & fileURI; h & simturnsInfo; h & turnTimerInfo; h & mapname; @@ -135,7 +137,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))) + mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))), fileURI("") { } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 5b3eecdef..3d99c1fbd 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -318,6 +318,7 @@ bool CVCMIServer::prepareToStartGame() case StartInfo::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); + si->fileURI = mi->fileURI; si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); gh->init(si.get(), progressTracking); @@ -326,6 +327,7 @@ bool CVCMIServer::prepareToStartGame() case StartInfo::NEW_GAME: logNetwork->info("Preparing to start new game"); si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); + si->fileURI = mi->fileURI; gh->init(si.get(), progressTracking); break; From 73a05d82e136ed5d3177fe62c2205bbf7890d339 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 20:18:12 +0200 Subject: [PATCH 0687/1248] fix --- client/VCMI_client.cbp | 2 ++ client/VCMI_client.vcxproj | 2 ++ client/VCMI_client.vcxproj.filters | 6 ++++++ config/widgets/mapOverview.json | 8 ++++---- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/client/VCMI_client.cbp b/client/VCMI_client.cbp index 57fc618b5..081be52ae 100644 --- a/client/VCMI_client.cbp +++ b/client/VCMI_client.cbp @@ -229,6 +229,8 @@ + + diff --git a/client/VCMI_client.vcxproj b/client/VCMI_client.vcxproj index 3fe7331f9..997bdba6a 100644 --- a/client/VCMI_client.vcxproj +++ b/client/VCMI_client.vcxproj @@ -238,6 +238,7 @@ + @@ -305,6 +306,7 @@ + diff --git a/client/VCMI_client.vcxproj.filters b/client/VCMI_client.vcxproj.filters index 8831d0480..9b717e834 100644 --- a/client/VCMI_client.vcxproj.filters +++ b/client/VCMI_client.vcxproj.filters @@ -25,6 +25,9 @@ windows + + windows + windows @@ -179,6 +182,9 @@ windows + + windows + windows diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index e397d8909..0c98d57a5 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -101,7 +101,7 @@ }, { "type": "drawTransparentRect", - "rect": {"x": 5, "y": 309, "w": 418, "h": 20}, + "rect": {"x": 5, "y": 304, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] }, @@ -111,11 +111,11 @@ "alignment": "center", "color": "yellow", "text": "vcmi.lobby.filename", - "position": {"x": 214, "y": 319} + "position": {"x": 214, "y": 314} }, { "type": "drawTransparentRect", - "rect": {"x": 5, "y": 334, "w": 418, "h": 45}, + "rect": {"x": 5, "y": 329, "w": 418, "h": 45}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] }, @@ -124,7 +124,7 @@ "font": "small", "alignment": "center", "color": "white", - "rect": {"x": 10, "y": 339, "w": 408, "h": 35} + "rect": {"x": 10, "y": 334, "w": 408, "h": 35} } ] } From 2eb4413978e65f1f70d5ad15a525363fa08b6fb2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 20:44:46 +0200 Subject: [PATCH 0688/1248] fix height --- config/widgets/mapOverview.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index 0c98d57a5..8ce7a2b97 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -5,7 +5,7 @@ "name": "background", "type": "texture", "image": "DIBOXBCK", - "rect": {"w": 428, "h": 384} + "rect": {"w": 428, "h": 379} }, { "type": "drawTransparentRect", From 61aae7bccc0c892b4a25d644ca74cea2ec37e0ab Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 22:49:22 +0200 Subject: [PATCH 0689/1248] code review --- client/gui/InterfaceObjectConfigurable.cpp | 29 ++++++++ client/gui/InterfaceObjectConfigurable.h | 4 ++ client/windows/CMapOverview.cpp | 82 +++++++--------------- client/windows/CMapOverview.h | 7 +- config/widgets/mapOverview.json | 32 +++++---- 5 files changed, 77 insertions(+), 77 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 3a1fd45cb..24d20d3fc 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -56,6 +56,8 @@ InterfaceObjectConfigurable::InterfaceObjectConfigurable(int used, Point offset) REGISTER_BUILDER("layout", &InterfaceObjectConfigurable::buildLayout); REGISTER_BUILDER("comboBox", &InterfaceObjectConfigurable::buildComboBox); REGISTER_BUILDER("textInput", &InterfaceObjectConfigurable::buildTextInput); + REGISTER_BUILDER("transparentFilledRectangle", &InterfaceObjectConfigurable::buildTransparentFilledRectangle); + REGISTER_BUILDER("textBox", &InterfaceObjectConfigurable::buildTextBox); } void InterfaceObjectConfigurable::registerBuilder(const std::string & type, BuilderFunction f) @@ -684,6 +686,33 @@ std::shared_ptr InterfaceObjectConfigurable::buildAnimation(const return anim; } +std::shared_ptr InterfaceObjectConfigurable::buildTransparentFilledRectangle(const JsonNode & config) const +{ + logGlobal->debug("Building widget TransparentFilledRectangle"); + + auto rect = readRect(config["rect"]); + auto color = readColor(config["color"]); + if(!config["colorLine"].isNull()) + { + auto colorLine = readColor(config["colorLine"]); + return std::make_shared(rect, color, colorLine); + } + return std::make_shared(rect, color); +} + +std::shared_ptr InterfaceObjectConfigurable::buildTextBox(const JsonNode & config) const +{ + logGlobal->debug("Building widget CTextBox"); + + auto rect = readRect(config["rect"]); + auto font = readFont(config["font"]); + auto alignment = readTextAlignment(config["alignment"]); + auto color = readColor(config["color"]); + auto text = readText(config["text"]); + + return std::make_shared(text, rect, 0, font, alignment, color); +} + std::shared_ptr InterfaceObjectConfigurable::buildWidget(JsonNode config) const { assert(!config.isNull()); diff --git a/client/gui/InterfaceObjectConfigurable.h b/client/gui/InterfaceObjectConfigurable.h index 30b96bfb4..cc812299e 100644 --- a/client/gui/InterfaceObjectConfigurable.h +++ b/client/gui/InterfaceObjectConfigurable.h @@ -29,6 +29,8 @@ class CShowableAnim; class CFilledTexture; class ComboBox; class CTextInput; +class TransparentFilledRectangle; +class CTextBox; #define REGISTER_BUILDER(type, method) registerBuilder(type, std::bind(method, this, std::placeholders::_1)) @@ -105,6 +107,8 @@ protected: std::shared_ptr buildLayout(const JsonNode &); std::shared_ptr buildComboBox(const JsonNode &); std::shared_ptr buildTextInput(const JsonNode &) const; + std::shared_ptr buildTransparentFilledRectangle(const JsonNode & config) const; + std::shared_ptr buildTextBox(const JsonNode & config) const; //composite widgets std::shared_ptr buildWidget(JsonNode config) const; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 0f2afef00..bf4d893de 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -134,20 +134,6 @@ std::vector> CMapOverview::CMapOverviewWidget::createMin return ret; } -std::shared_ptr CMapOverview::CMapOverviewWidget::CMapOverviewWidget::buildDrawTransparentRect(const JsonNode & config) const -{ - logGlobal->debug("Building widget drawTransparentRect"); - - auto rect = readRect(config["rect"]); - auto color = readColor(config["color"]); - if(!config["colorLine"].isNull()) - { - auto colorLine = readColor(config["colorLine"]); - return std::make_shared(rect, color, colorLine); - } - return std::make_shared(rect, color); -} - std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const { logGlobal->debug("Building widget drawMinimap"); @@ -158,11 +144,11 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con if(!renderImage) return nullptr; - ResourcePath res = ResourcePath(parent.resource.getName(), EResType::MAP); + ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); std::unique_ptr campaignMap = nullptr; - if(parent.tabType != ESelectionScreen::newGame) + if(p.tabType != ESelectionScreen::newGame) { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(parent.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); lf.checkMagicBytes(SAVEGAME_MAGIC); std::unique_ptr mapHeader = std::make_unique(); @@ -187,54 +173,34 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con return std::make_shared(images[id], Point(rect.x, rect.y)); } -std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawPath(const JsonNode & config) const -{ - logGlobal->debug("Building widget drawPath"); - - auto rect = readRect(config["rect"]); - auto font = readFont(config["font"]); - auto alignment = readTextAlignment(config["alignment"]); - auto color = readColor(config["color"]); - - return std::make_shared(parent.fileName, rect, 0, font, alignment, color); -} - -std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawString(const JsonNode & config) const -{ - logGlobal->debug("Building widget drawString"); - - auto font = readFont(config["font"]); - auto alignment = readTextAlignment(config["alignment"]); - auto color = readColor(config["color"]); - std::string text = ""; - if("mapname" == config["text"].String()) - text = parent.mapName; - if("date" == config["text"].String()) - { - if(parent.date.empty()) - { - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(parent.resource.getName(), EResType::MAP))); - text = vstd::getFormattedDateTime(time); - } - else - text = parent.date; - } - auto position = readPosition(config["position"]); - return std::make_shared(position.x, position.y, font, alignment, color, text); -} - CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): - InterfaceObjectConfigurable(), parent(parent) + InterfaceObjectConfigurable(), p(parent) { - drawPlayerElements = parent.tabType == ESelectionScreen::newGame; + drawPlayerElements = p.tabType == ESelectionScreen::newGame; renderImage = settings["lobby"]["mapPreview"].Bool(); const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); - REGISTER_BUILDER("drawTransparentRect", &CMapOverview::CMapOverviewWidget::buildDrawTransparentRect); REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); - REGISTER_BUILDER("drawPath", &CMapOverview::CMapOverviewWidget::buildDrawPath); - REGISTER_BUILDER("drawString", &CMapOverview::CMapOverviewWidget::buildDrawString); build(config); + + if(auto w = widget("fileName")) + { + w->setText(p.fileName); + } + if(auto w = widget("mapName")) + { + w->setText(p.mapName); + } + if(auto w = widget("date")) + { + if(p.date.empty()) + { + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::MAP))); + w->setText(vstd::getFormattedDateTime(time)); + } + else + w->setText(p.date); + } } \ No newline at end of file diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 7c70210d8..0628852d7 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -30,7 +30,7 @@ class CMapOverview : public CWindowObject { class CMapOverviewWidget : public InterfaceObjectConfigurable { - CMapOverview& parent; + CMapOverview& p; bool drawPlayerElements; bool renderImage; @@ -38,12 +38,9 @@ class CMapOverview : public CWindowObject std::vector> createMinimaps(ResourcePath resource, Point size) const; std::vector> createMinimaps(std::unique_ptr & map, Point size) const; - std::shared_ptr buildDrawTransparentRect(const JsonNode & config) const; std::shared_ptr buildDrawMinimap(const JsonNode & config) const; - std::shared_ptr buildDrawPath(const JsonNode & config) const; - std::shared_ptr buildDrawString(const JsonNode & config) const; public: - CMapOverviewWidget(CMapOverview& parent); + CMapOverviewWidget(CMapOverview& p); }; std::shared_ptr widget; diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index 8ce7a2b97..bed9130f5 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -8,7 +8,7 @@ "rect": {"w": 428, "h": 379} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 5, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] @@ -22,21 +22,22 @@ "position": {"x": 214, "y": 15} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 30, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] }, { - "type": "drawString", + "type": "label", + "name": "mapName", "font": "small", "alignment": "center", "color": "white", - "text": "mapname", + "text": "", "position": {"x": 214, "y": 40} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 55, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] @@ -50,7 +51,7 @@ "position": {"x": 214, "y": 65} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 29, "y": 79, "w": 171, "h": 171}, "color": [0, 0, 0, 255], "colorLine": [128, 100, 75, 255] @@ -61,7 +62,7 @@ "rect": {"x": 30, "y": 80, "w": 169, "h": 169} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 228, "y": 79, "w": 171, "h": 171}, "color": [0, 0, 0, 255], "colorLine": [128, 100, 75, 255] @@ -72,7 +73,7 @@ "rect": {"x": 229, "y": 80, "w": 169, "h": 169} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 254, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] @@ -86,21 +87,22 @@ "position": {"x": 214, "y": 264} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 279, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] }, { - "type": "drawString", + "type": "label", + "name": "date", "font": "small", "alignment": "center", "color": "white", - "text": "date", + "text": "", "position": {"x": 214, "y": 289} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 304, "w": 418, "h": 20}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] @@ -114,16 +116,18 @@ "position": {"x": 214, "y": 314} }, { - "type": "drawTransparentRect", + "type": "transparentFilledRectangle", "rect": {"x": 5, "y": 329, "w": 418, "h": 45}, "color": [0, 0, 0, 75], "colorLine": [128, 100, 75, 255] }, { - "type": "drawPath", + "type": "textBox", + "name": "fileName", "font": "small", "alignment": "center", "color": "white", + "text": "", "rect": {"x": 10, "y": 334, "w": 408, "h": 35} } ] From 3ea7988883c2cee6f726da95375fd6958625a4ad Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 30 Sep 2023 23:06:38 +0200 Subject: [PATCH 0690/1248] Treasure placement uses random templates now --- lib/rmg/modificators/ObjectDistributor.cpp | 19 +--- lib/rmg/modificators/TreasurePlacer.cpp | 125 +++++++++++---------- lib/rmg/modificators/TreasurePlacer.h | 6 +- 3 files changed, 70 insertions(+), 80 deletions(-) diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 92570b791..7ed97708e 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -79,25 +79,14 @@ void ObjectDistributor::distributeLimitedObjects() for (auto& zone : matchingZones) { - //We already know there are some templates - auto templates = handler->getTemplates(zone->getTerrainType()); - - //FIXME: Templates empty?! Maybe zone changed terrain type over time? - - //Assume the template with fewest terrains is the most suitable - auto temp = *boost::min_element(templates, [](std::shared_ptr lhs, std::shared_ptr rhs) -> bool + oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * { - return lhs->getAllowedTerrains().size() < rhs->getAllowedTerrains().size(); - }); - - oi.generateObject = [temp]() -> CGObjectInstance * - { - return VLC->objtypeh->getHandlerFor(temp->id, temp->subid)->create(temp); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; - oi.templ = temp; + oi.setTemplates(primaryID, secondaryID, zone->getTerrainType()); //Rounding up will make sure all possible objects are exhausted uint32_t mapLimit = rmgInfo.mapLimit.value(); @@ -109,7 +98,7 @@ void ObjectDistributor::distributeLimitedObjects() rmgInfo.setMapLimit(mapLimit - oi.maxPerZone); //Don't add objects with 0 count remaining - if (oi.maxPerZone) + if(oi.maxPerZone && !oi.templates.empty()) { zone->getModificator()->addObjectToRandomPool(oi); } diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index c92637aea..eff834612 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -72,26 +72,16 @@ void TreasurePlacer::addAllPossibleObjects() continue; } - auto templates = handler->getTemplates(zone.getTerrainType()); - if (templates.empty()) - continue; - - //TODO: Reuse chooseRandomAppearance (eg. WoG treasure chests) - //Assume the template with fewest terrains is the most suitable - auto temp = *boost::min_element(templates, [](std::shared_ptr lhs, std::shared_ptr rhs) -> bool + oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * { - return lhs->getAllowedTerrains().size() < rhs->getAllowedTerrains().size(); - }); - - oi.generateObject = [temp]() -> CGObjectInstance * - { - return VLC->objtypeh->getHandlerFor(temp->id, temp->subid)->create(temp); + return VLC->objtypeh->getHandlerFor(primaryID, secondaryID)->create(); }; oi.value = rmgInfo.value; oi.probability = rmgInfo.rarity; - oi.templ = temp; + oi.setTemplates(primaryID, secondaryID, zone.getTerrainType()); oi.maxPerZone = rmgInfo.zoneLimit; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } } @@ -125,18 +115,18 @@ void TreasurePlacer::addAllPossibleObjects() obj->exp = generator.getConfig().prisonExperience[i]; obj->setOwner(PlayerColor::NEUTRAL); generator.banHero(hid); - obj->appearance = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0)->getTemplates(zone.getTerrainType()).front(); //can't init template with hero subID return obj; }; - oi.setTemplate(Obj::PRISON, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PRISON, 0, zone.getTerrainType()); oi.value = generator.getConfig().prisonValues[i]; oi.probability = 30; //Distribute all allowed prisons, starting from the most valuable oi.maxPerZone = (std::ceil((float)prisonsLeft / (i + 1))); prisonsLeft -= oi.maxPerZone; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } @@ -183,22 +173,16 @@ void TreasurePlacer::addAllPossibleObjects() auto nativeZonesCount = static_cast(map.getZoneCount(cre->getFaction())); oi.value = static_cast(cre->getAIValue() * cre->getGrowth() * (1 + (nativeZonesCount / map.getTotalZoneCount()) + (nativeZonesCount / 2))); oi.probability = 40; - - for(const auto & tmplate : dwellingHandler->getTemplates()) + + oi.generateObject = [secondaryID, dwellingType]() -> CGObjectInstance * { - if(tmplate->canBePlacedAt(zone.getTerrainType())) - { - oi.generateObject = [tmplate, secondaryID, dwellingType]() -> CGObjectInstance * - { - auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(tmplate); - obj->tempOwner = PlayerColor::NEUTRAL; - return obj; - }; - - oi.templ = tmplate; - addObjectToRandomPool(oi); - } - } + auto * obj = VLC->objtypeh->getHandlerFor(dwellingType, secondaryID)->create(); + obj->tempOwner = PlayerColor::NEUTRAL; + return obj; + }; + oi.setTemplates(dwellingType, secondaryID, zone.getTerrainType()); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } } } @@ -222,10 +206,11 @@ void TreasurePlacer::addAllPossibleObjects() obj->storedArtifact = a; return obj; }; - oi.setTemplate(Obj::SPELL_SCROLL, 0, zone.getTerrainType()); + oi.setTemplates(Obj::SPELL_SCROLL, 0, zone.getTerrainType()); oi.value = generator.getConfig().scrollValues[i]; oi.probability = 30; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with gold @@ -243,10 +228,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = i * generator.getConfig().pandoraMultiplierGold; oi.probability = 5; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with experience @@ -264,10 +250,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = i * generator.getConfig().pandoraMultiplierExperience; oi.probability = 20; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //pandora box with creatures @@ -325,10 +312,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = static_cast((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) / 3); oi.probability = 3; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //Pandora with 12 spells of certain level @@ -357,10 +345,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = (i + 1) * generator.getConfig().pandoraMultiplierSpells; //5000 - 15000 oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } //Pandora with 15 spells of certain school @@ -389,10 +378,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = generator.getConfig().pandoraSpellSchool; oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); } // Pandora box with 60 random spells @@ -420,10 +410,11 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - oi.setTemplate(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); + oi.setTemplates(Obj::PANDORAS_BOX, 0, zone.getTerrainType()); oi.value = generator.getConfig().pandoraSpell60; oi.probability = 2; - addObjectToRandomPool(oi); + if(!oi.templates.empty()) + addObjectToRandomPool(oi); //Seer huts with creatures or generic rewards @@ -483,7 +474,7 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; oi.probability = 3; - oi.setTemplate(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); + oi.setTemplates(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); oi.value = static_cast(((2 * (creature->getAIValue()) * creaturesAmount * (1 + static_cast(map.getZoneCount(creature->getFaction())) / map.getTotalZoneCount())) - 4000) / 3); if (oi.value > zone.getMaxTreasureValue()) { @@ -491,7 +482,8 @@ void TreasurePlacer::addAllPossibleObjects() } else { - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); } } @@ -500,7 +492,7 @@ void TreasurePlacer::addAllPossibleObjects() { int randomAppearance = chooseRandomAppearance(zone.getRand(), Obj::SEER_HUT, zone.getTerrainType()); - oi.setTemplate(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); + oi.setTemplates(Obj::SEER_HUT, randomAppearance, zone.getTerrainType()); oi.value = generator.getConfig().questValues[i]; if (oi.value > zone.getMaxTreasureValue()) { @@ -533,7 +525,8 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); oi.generateObject = [i, randomAppearance, this, qap]() -> CGObjectInstance * { @@ -557,7 +550,8 @@ void TreasurePlacer::addAllPossibleObjects() return obj; }; - possibleSeerHuts.push_back(oi); + if(!oi.templates.empty()) + possibleSeerHuts.push_back(oi); } if (possibleSeerHuts.empty()) @@ -610,7 +604,12 @@ std::vector TreasurePlacer::prepareTreasurePile(const CTreasureInfo if(!oi) //fail break; - if(oi->templ->isVisitableFromTop()) + bool visitableFromTop = true; + for(auto & t : oi->templates) + if(!t->isVisitableFromTop()) + visitableFromTop = false; + + if(visitableFromTop) { objectInfos.push_back(oi); } @@ -641,7 +640,10 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector accessibleArea.add(int3()); auto * object = oi->generateObject(); - object->appearance = oi->templ; + if(oi->templates.empty()) + continue; + + object->appearance = *RandomGeneratorUtil::nextItem(oi->templates, zone.getRand()); auto & instance = rmgObject.addInstance(*object); do @@ -717,7 +719,12 @@ ObjectInfo * TreasurePlacer::getRandomObject(ui32 desiredValue, ui32 currentValu if(oi.value > maxVal) break; //this assumes values are sorted in ascending order - if(!oi.templ->isVisitableFromTop() && !allowLargeObjects) + bool visitableFromTop = true; + for(auto & t : oi.templates) + if(!t->isVisitableFromTop()) + visitableFromTop = false; + + if(!visitableFromTop && !allowLargeObjects) continue; if(oi.value >= minValue && oi.maxPerZone > 0) @@ -921,17 +928,13 @@ char TreasurePlacer::dump(const int3 & t) return Modificator::dump(t); } -void ObjectInfo::setTemplate(si32 type, si32 subtype, TerrainId terrainType) +void ObjectInfo::setTemplates(si32 type, si32 subtype, TerrainId terrainType) { auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); if(!templHandler) return; - auto templates = templHandler->getTemplates(terrainType); - if(templates.empty()) - return; - - templ = templates.front(); + templates = templHandler->getTemplates(terrainType); } VCMI_LIB_NAMESPACE_END diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index 822bc793b..ef4881e25 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -22,16 +22,14 @@ class CRandomGenerator; struct ObjectInfo { - std::shared_ptr templ; + std::vector> templates; ui32 value = 0; ui16 probability = 0; ui32 maxPerZone = 1; //ui32 maxPerMap; //unused std::function generateObject; - void setTemplate(si32 type, si32 subtype, TerrainId terrain); - - bool operator==(const ObjectInfo& oi) const { return (templ == oi.templ); } + void setTemplates(si32 type, si32 subtype, TerrainId terrain); }; class TreasurePlacer: public Modificator From 58dfc55ca3314cfbbc5b017a427233b7b20d9256 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:50:27 +0200 Subject: [PATCH 0691/1248] only calc minimaps once --- client/windows/CMapOverview.cpp | 55 ++++++++++++++------------------- client/windows/CMapOverview.h | 6 ++-- 2 files changed, 28 insertions(+), 33 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index bf4d893de..7fdad167e 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -99,9 +99,9 @@ Canvas CMapOverview::CMapOverviewWidget::createMinimapForLayer(std::unique_ptr> CMapOverview::CMapOverviewWidget::createMinimaps(ResourcePath resource, Point size) const +std::vector CMapOverview::CMapOverviewWidget::createMinimaps(ResourcePath resource) const { - std::vector> ret = std::vector>(); + std::vector ret = std::vector(); CMapService mapService; std::unique_ptr map; @@ -114,22 +114,15 @@ std::vector> CMapOverview::CMapOverviewWidget::createMin return ret; } - return createMinimaps(map, size); + return createMinimaps(map); } -std::vector> CMapOverview::CMapOverviewWidget::createMinimaps(std::unique_ptr & map, Point size) const +std::vector CMapOverview::CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const { - std::vector> ret = std::vector>(); + std::vector ret = std::vector(); for(int i = 0; i < (map->twoLevel ? 2 : 1); i++) - { - Canvas canvas = createMinimapForLayer(map, i); - Canvas canvasScaled = Canvas(size); - canvasScaled.drawScaled(canvas, Point(0, 0), size); - std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); - - ret.push_back(img); - } + ret.push_back(createMinimapForLayer(map, i)); return ret; } @@ -141,9 +134,23 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con auto rect = readRect(config["rect"]); auto id = config["id"].Integer(); - if(!renderImage) + if(id >= minimaps.size()) return nullptr; + Canvas canvasScaled = Canvas(Point(rect.w, rect.h)); + canvasScaled.drawScaled(minimaps[id], Point(0, 0), Point(rect.w, rect.h)); + std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); + + return std::make_shared(img, Point(rect.x, rect.y)); +} + +CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): + InterfaceObjectConfigurable(), p(parent) +{ + drawPlayerElements = p.tabType == ESelectionScreen::newGame; + renderImage = settings["lobby"]["mapPreview"].Bool(); + + // create minimaps ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); std::unique_ptr campaignMap = nullptr; if(p.tabType != ESelectionScreen::newGame) @@ -159,26 +166,12 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); res = ResourcePath(startInfo->fileURI, EResType::MAP); } - - std::vector> images; if(!campaignMap) - images = createMinimaps(res, Point(rect.w, rect.h)); + minimaps = createMinimaps(res); else - images = createMinimaps(campaignMap, Point(rect.w, rect.h)); - - - if(id >= images.size()) - return nullptr; - - return std::make_shared(images[id], Point(rect.x, rect.y)); -} - -CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): - InterfaceObjectConfigurable(), p(parent) -{ - drawPlayerElements = p.tabType == ESelectionScreen::newGame; - renderImage = settings["lobby"]["mapPreview"].Bool(); + minimaps = createMinimaps(campaignMap); + // config const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 0628852d7..c9d4197f9 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -34,9 +34,11 @@ class CMapOverview : public CWindowObject bool drawPlayerElements; bool renderImage; + std::vector minimaps; + Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; - std::vector> createMinimaps(ResourcePath resource, Point size) const; - std::vector> createMinimaps(std::unique_ptr & map, Point size) const; + std::vector createMinimaps(ResourcePath resource) const; + std::vector createMinimaps(std::unique_ptr & map) const; std::shared_ptr buildDrawMinimap(const JsonNode & config) const; public: From 182d762135a0eea463e9406d77c59dc84f8dee56 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 30 Sep 2023 23:58:49 +0200 Subject: [PATCH 0692/1248] docs --- docs/modders/Configurable_Widgets.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/modders/Configurable_Widgets.md b/docs/modders/Configurable_Widgets.md index d13af3a64..9d3234e10 100644 --- a/docs/modders/Configurable_Widgets.md +++ b/docs/modders/Configurable_Widgets.md @@ -517,6 +517,22 @@ Configurable object has following structure: `"text"`: [text](#text), +### TextBox + +`"type": "textBox"` + +`"name": "string"` optional, object name + +`"font"`: [font](#font) + +`"alignment"`: [alignment](#text-alignment), + +`"color"`: [color](#color), + +`"text"`: [text](#text), + +`"rect"`: [rect](#rect) + ### Picture `"type": "picture"` @@ -559,6 +575,18 @@ Filling area with texture `"rect"`: [rect](#rect) +### TransparentFilledRectangle + +`"type": "transparentFilledRectangle"` + +`"name": "string"` optional, object name + +`"color"`: [color](#color) fill color of rectangle (supports transparency) + +`"colorLine"`: [color](#color) optional, 1px border color + +`"rect"`: [rect](#rect) + ### Animation `"type": "animation"` From 65c21064b5620fe72caa8e1c7c7c2e19e8209c9c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:18:25 +0200 Subject: [PATCH 0693/1248] added var --- client/windows/CMapOverview.cpp | 46 ++++++++++++++++----------------- client/windows/CMapOverview.h | 1 - config/widgets/mapOverview.json | 7 ++++- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 7fdad167e..1a154ac2b 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -148,32 +148,32 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): InterfaceObjectConfigurable(), p(parent) { drawPlayerElements = p.tabType == ESelectionScreen::newGame; - renderImage = settings["lobby"]["mapPreview"].Bool(); - // create minimaps - ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); - std::unique_ptr campaignMap = nullptr; - if(p.tabType != ESelectionScreen::newGame) - { - CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); - lf.checkMagicBytes(SAVEGAME_MAGIC); - - std::unique_ptr mapHeader = std::make_unique(); - StartInfo * startInfo; - lf >> *(mapHeader) >> startInfo; - - if(startInfo->campState) - campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); - res = ResourcePath(startInfo->fileURI, EResType::MAP); - } - if(!campaignMap) - minimaps = createMinimaps(res); - else - minimaps = createMinimaps(campaignMap); - - // config const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); + if(settings["lobby"]["mapPreview"].Bool()) + { + ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); + std::unique_ptr campaignMap = nullptr; + if(p.tabType != ESelectionScreen::newGame && config["variables"]["mapPreviewForSaves"].Bool()) + { + CLoadFile lf(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::SAVEGAME)), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + std::unique_ptr mapHeader = std::make_unique(); + StartInfo * startInfo; + lf >> *(mapHeader) >> startInfo; + + if(startInfo->campState) + campaignMap = startInfo->campState->getMap(*startInfo->campState->currentScenario()); + res = ResourcePath(startInfo->fileURI, EResType::MAP); + } + if(!campaignMap) + minimaps = createMinimaps(res); + else + minimaps = createMinimaps(campaignMap); + } + REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); build(config); diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index c9d4197f9..872e35292 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -33,7 +33,6 @@ class CMapOverview : public CWindowObject CMapOverview& p; bool drawPlayerElements; - bool renderImage; std::vector minimaps; Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index bed9130f5..d28c2cfc2 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -130,5 +130,10 @@ "text": "", "rect": {"x": 10, "y": 334, "w": 408, "h": 35} } - ] + ], + + "variables": + { + "mapPreviewForSaves": true + } } From 648a37310fc3e50e6f4470d0da9964f2a1d9ab3a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:35:11 +0200 Subject: [PATCH 0694/1248] description --- Mods/vcmi/config/vcmi/english.json | 2 ++ Mods/vcmi/config/vcmi/german.json | 2 ++ client/windows/CMapOverview.cpp | 5 +++++ config/widgets/mapOverview.json | 17 +++++++++++++++++ 4 files changed, 26 insertions(+) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index e5a8b3975..a49161520 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -53,6 +53,8 @@ "vcmi.lobby.creationDate" : "Creation date", "vcmi.lobby.scenarioName" : "Scenario name", "vcmi.lobby.mapPreview" : "Map preview", + "vcmi.lobby.noPreview" : "no preview", + "vcmi.lobby.noUnderground" : "no underground", "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", "vcmi.server.errors.modsToEnable" : "{Following mods are required}", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 3548c4ea6..cc35abdb9 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -52,6 +52,8 @@ "vcmi.lobby.creationDate" : "Erstellungsdatum", "vcmi.lobby.scenarioName" : "Szenario-Name", "vcmi.lobby.mapPreview" : "Kartenvorschau", + "vcmi.lobby.noPreview" : "Keine Vorschau", + "vcmi.lobby.noUnderground" : "Kein Untergrund", "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 1a154ac2b..3e1aa5847 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -196,4 +196,9 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): else w->setText(p.date); } + if(auto w = widget("noUnderground")) + { + if(minimaps.size() == 0) + w->setText(""); + } } \ No newline at end of file diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index d28c2cfc2..c4e246c8a 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -56,6 +56,14 @@ "color": [0, 0, 0, 255], "colorLine": [128, 100, 75, 255] }, + { + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.lobby.noPreview", + "position": {"x": 114, "y": 164} + }, { "type": "drawMinimap", "id": 0, @@ -67,6 +75,15 @@ "color": [0, 0, 0, 255], "colorLine": [128, 100, 75, 255] }, + { + "type": "label", + "name": "noUnderground", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.lobby.noUnderground", + "position": {"x": 313, "y": 164} + }, { "type": "drawMinimap", "id": 1, From 23b8a321f6a8732ee552d4ef4970af3caa8ac7d4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 00:46:29 +0200 Subject: [PATCH 0695/1248] file ending --- Mods/vcmi/config/vcmi/german.json | 2 +- client/windows/CMapOverview.cpp | 2 +- client/windows/CMapOverview.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index cc35abdb9..0afd45188 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -365,4 +365,4 @@ "core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule", "core.bonus.WIDE_BREATH.name": "Breiter Atem", "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)" -} \ No newline at end of file +} diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 3e1aa5847..e0f7fb58c 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -201,4 +201,4 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): if(minimaps.size() == 0) w->setText(""); } -} \ No newline at end of file +} diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 872e35292..5c9364508 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -54,4 +54,4 @@ public: const ESelectionScreen tabType; CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType); -}; \ No newline at end of file +}; From 7f4361eb10f2057a58683108e47f5203bbb72ca4 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 01:11:11 +0200 Subject: [PATCH 0696/1248] name -> path --- Mods/vcmi/config/vcmi/czech.json | 2 +- Mods/vcmi/config/vcmi/english.json | 2 +- Mods/vcmi/config/vcmi/german.json | 2 +- Mods/vcmi/config/vcmi/polish.json | 2 +- Mods/vcmi/config/vcmi/ukrainian.json | 2 +- Mods/vcmi/config/vcmi/vietnamese.json | 2 +- config/widgets/mapOverview.json | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index 1e9f8b2e8..f1c561b97 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -44,7 +44,7 @@ "vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP", "vcmi.mainMenu.playerName" : "Hráč", - "vcmi.lobby.filename" : "Název souboru", + "vcmi.lobby.filepath" : "Název souboru", "vcmi.lobby.creationDate" : "Datum vytvoření", "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index a49161520..c5cba2fa1 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -49,7 +49,7 @@ "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", "vcmi.mainMenu.playerName" : "Player", - "vcmi.lobby.filename" : "Filename", + "vcmi.lobby.filepath" : "File path", "vcmi.lobby.creationDate" : "Creation date", "vcmi.lobby.scenarioName" : "Scenario name", "vcmi.lobby.mapPreview" : "Map preview", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 0afd45188..121deacb2 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -48,7 +48,7 @@ "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", "vcmi.mainMenu.playerName" : "Spieler", - "vcmi.lobby.filename" : "Dateiname", + "vcmi.lobby.filepath" : "Dateipfad", "vcmi.lobby.creationDate" : "Erstellungsdatum", "vcmi.lobby.scenarioName" : "Szenario-Name", "vcmi.lobby.mapPreview" : "Kartenvorschau", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 51c96dac4..1526e83e3 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -43,7 +43,7 @@ "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", "vcmi.mainMenu.playerName" : "Gracz", - "vcmi.lobby.filename" : "Nazwa pliku", + "vcmi.lobby.filepath" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 4f5228821..9773fc7af 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -44,7 +44,7 @@ "vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри", "vcmi.mainMenu.playerName" : "Гравець", - "vcmi.lobby.filename" : "Назва файлу", + "vcmi.lobby.filepath" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json index 9926fce10..890e699d1 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -50,7 +50,7 @@ "vcmi.mainMenu.joinTCP": "Tham gia TCP/IP", "vcmi.mainMenu.playerName": "Người chơi", - "vcmi.lobby.filename": "Tên tập tin", + "vcmi.lobby.filepath": "Tên tập tin", "vcmi.lobby.creationDate": "Ngày tạo", "vcmi.server.errors.existingProcess": "1 chương trình VCMI khác đang chạy. Tắt nó trước khi mở cái mới", diff --git a/config/widgets/mapOverview.json b/config/widgets/mapOverview.json index c4e246c8a..d451afda4 100644 --- a/config/widgets/mapOverview.json +++ b/config/widgets/mapOverview.json @@ -129,7 +129,7 @@ "font": "medium", "alignment": "center", "color": "yellow", - "text": "vcmi.lobby.filename", + "text": "vcmi.lobby.filepath", "position": {"x": 214, "y": 314} }, { From e9855de10132044fa01b929c0e057bf3da9f0465 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 02:35:02 +0200 Subject: [PATCH 0697/1248] Layout fixes --- mapeditor/mainwindow.ui | 110 ++++++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 16 deletions(-) diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 6017a6a9e..73a73c89f 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -14,20 +14,20 @@ VCMI Map Editor - + - 2 + 0 - 2 + 0 - 2 + 0 - 2 + 0 - + @@ -159,7 +159,7 @@ - 192 + 524287 214 @@ -222,7 +222,7 @@ - + 0 0 @@ -247,7 +247,7 @@ - + 0 0 @@ -274,7 +274,7 @@ - 0 + 1 @@ -432,11 +432,17 @@ 128 - 496 + 192 + + + + + 524287 + 192 - Terrains View + Tools 1 @@ -476,7 +482,7 @@ - + 0 0 @@ -491,6 +497,18 @@ Brush + + 0 + + + 0 + + + 0 + + + 0 + @@ -734,6 +752,36 @@ + + + + + + + 0 + 0 + + + + Painting + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + @@ -757,7 +805,7 @@ 0 0 128 - 236 + 192 @@ -800,7 +848,7 @@ 0 0 128 - 236 + 192 @@ -836,7 +884,7 @@ 0 0 128 - 236 + 192 @@ -868,6 +916,36 @@ + + + + + + + 524287 + 150 + + + + Preview + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + From 4e5580f3d902a783f92d4865864e411de2064c30 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 02:39:03 +0200 Subject: [PATCH 0698/1248] Fix typo --- mapeditor/mainwindow.cpp | 6 +++--- mapeditor/mainwindow.h | 2 +- mapeditor/mainwindow.ui | 9 ++++++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 0dcf519d2..04f54bc8f 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -1248,15 +1248,15 @@ void MainWindow::on_actionTranslations_triggered() translationsDialog->show(); } -void MainWindow::on_actionh3m_coverter_triggered() +void MainWindow::on_actionh3m_converter_triggered() { - auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to covert"), + auto mapFiles = QFileDialog::getOpenFileNames(this, tr("Select maps to convert"), QString::fromStdString(VCMIDirs::get().userCachePath().make_preferred().string()), tr("HoMM3 maps(*.h3m)")); if(mapFiles.empty()) return; - auto saveDirectory = QFileDialog::getExistingDirectory(this, tr("Choose directory to save coverted maps"), QCoreApplication::applicationDirPath()); + auto saveDirectory = QFileDialog::getExistingDirectory(this, tr("Choose directory to save converted maps"), QCoreApplication::applicationDirPath()); if(saveDirectory.isEmpty()) return; diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 74b051d80..6ff39d165 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -121,7 +121,7 @@ private slots: void on_actionTranslations_triggered(); - void on_actionh3m_coverter_triggered(); + void on_actionh3m_converter_triggered(); public slots: diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 73a73c89f..01aae4a05 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -63,7 +63,7 @@ - + @@ -1333,9 +1333,12 @@ Ctrl+T - + - h3m coverter + h3m converter + + + h3m converter From 9c5725da6609d72c7644a55a6fe41d7234ade764 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 13:32:35 +0200 Subject: [PATCH 0699/1248] Convertion finished --- mapeditor/mainwindow.cpp | 5 ++++- mapeditor/mapcontroller.cpp | 32 ++++++++++++++++++++------------ mapeditor/mapcontroller.h | 3 ++- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 04f54bc8f..6e7a72125 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -1265,8 +1265,11 @@ void MainWindow::on_actionh3m_converter_triggered() for(auto & m : mapFiles) { CMapService mapService; - mapService.saveMap(openMapInternal(m), (saveDirectory + QFileInfo(m).fileName()).toStdString()); + auto map = openMapInternal(m); + controller.repairMap(map.get()); + mapService.saveMap(map, (saveDirectory + '/' + QFileInfo(m).completeBaseName() + ".vmap").toStdString()); } + QMessageBox::information(this, tr("Operation completed"), tr("Successfully converted %1 maps").arg(mapFiles.size())); } catch(const std::exception & e) { diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 0007da3b1..4aaeff622 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -86,27 +86,35 @@ MinimapScene * MapController::miniScene(int level) void MapController::repairMap() { + repairMap(map()); +} + +void MapController::repairMap(CMap * map) const +{ + if(!map) + return; + //there might be extra skills, arts and spells not imported from map - if(VLC->skillh->getDefaultAllowed().size() > map()->allowedAbilities.size()) + if(VLC->skillh->getDefaultAllowed().size() > map->allowedAbilities.size()) { - map()->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size()); + map->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size()); } - if(VLC->arth->getDefaultAllowed().size() > map()->allowedArtifact.size()) + if(VLC->arth->getDefaultAllowed().size() > map->allowedArtifact.size()) { - map()->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size()); + map->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size()); } - if(VLC->spellh->getDefaultAllowed().size() > map()->allowedSpells.size()) + if(VLC->spellh->getDefaultAllowed().size() > map->allowedSpells.size()) { - map()->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size()); + map->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size()); } - if(VLC->heroh->getDefaultAllowed().size() > map()->allowedHeroes.size()) + if(VLC->heroh->getDefaultAllowed().size() > map->allowedHeroes.size()) { - map()->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); + map->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); } //fix owners for objects - auto allImpactedObjects(_map->objects); - allImpactedObjects.insert(allImpactedObjects.end(), _map->predefinedHeroes.begin(), _map->predefinedHeroes.end()); + auto allImpactedObjects(map->objects); + allImpactedObjects.insert(allImpactedObjects.end(), map->predefinedHeroes.begin(), map->predefinedHeroes.end()); for(auto obj : allImpactedObjects) { //setup proper names (hero name will be fixed later @@ -131,7 +139,7 @@ void MapController::repairMap() //fix hero instance if(auto * nih = dynamic_cast(obj.get())) { - map()->allowedHeroes.at(nih->subID) = true; + map->allowedHeroes.at(nih->subID) = true; auto type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); //TODO: find a way to get proper type name @@ -202,7 +210,7 @@ void MapController::repairMap() art->storedArtifact = a; } else - map()->allowedArtifact.at(art->subID) = true; + map->allowedArtifact.at(art->subID) = true; } } } diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 979c2c6d2..18d660a3a 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -30,8 +30,9 @@ public: ~MapController(); void setMap(std::unique_ptr); - void initObstaclePainters(CMap* map); + void initObstaclePainters(CMap * map); + void repairMap(CMap * map) const; void repairMap(); const std::unique_ptr & getMapUniquePtr() const; //to be used for map saving From 7d56b704a24e215d92f5ca48c3d3e2bdc0619c17 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 13:38:16 +0200 Subject: [PATCH 0700/1248] Fix events/rumors identifiers --- mapeditor/mapcontroller.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 4aaeff622..2b4eb9a8f 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -112,6 +112,16 @@ void MapController::repairMap(CMap * map) const map->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); } + //make sure events/rumors has name to have proper identifiers + int emptyNameId = 1; + for(auto & e : map->events) + if(e.name.empty()) + e.name = "event_" + std::to_string(emptyNameId++); + emptyNameId = 1; + for(auto & e : map->rumors) + if(e.name.empty()) + e.name = "rumor_" + std::to_string(emptyNameId++); + //fix owners for objects auto allImpactedObjects(map->objects); allImpactedObjects.insert(allImpactedObjects.end(), map->predefinedHeroes.begin(), map->predefinedHeroes.end()); From f6093a84f0ff328030693e0dd4f11d1e761d5f4c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 14:00:08 +0200 Subject: [PATCH 0701/1248] Add newline symbol --- mapeditor/mapsettings/translations.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index b314c90c8..ab8a76bcc 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -58,6 +58,7 @@ Translations::Translations(CMapHeader & mh, QWidget *parent) : ui(new Ui::Translations), mapHeader(mh) { + setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); //fill languages list @@ -90,6 +91,7 @@ Translations::Translations(CMapHeader & mh, QWidget *parent) : Translations::~Translations() { + mapHeader.registerMapStrings(); delete ui; } @@ -103,8 +105,11 @@ void Translations::fillTranslationsTable(const std::string & language) int i = 0; for(auto & s : translation.Struct()) { + auto textLines = QString::fromStdString(s.second.String()); + textLines = textLines.replace('\n', "\\n"); + auto * wId = new QTableWidgetItem(QString::fromStdString(s.first)); - auto * wText = new QTableWidgetItem(QString::fromStdString(s.second.String())); + auto * wText = new QTableWidgetItem(textLines); wId->setFlags(wId->flags() & ~Qt::ItemIsEditable); wText->setFlags(wId->flags() | Qt::ItemIsEditable); ui->translationsTable->setItem(i, 0, wId); @@ -186,6 +191,8 @@ void Translations::on_translationsTable_itemChanged(QTableWidgetItem * item) if(textId.empty()) return; - translation[textId].String() = item->text().toStdString(); + auto textLines = item->text(); + textLines = textLines.replace("\\n", "\n"); + translation[textId].String() = textLines.toStdString(); } From 65e63bdf691e58617b84067d05d12e51fc2d3d6c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 14:04:46 +0200 Subject: [PATCH 0702/1248] Fix text duplicating --- mapeditor/inspector/inspector.cpp | 14 +++++++------- mapeditor/mapsettings/generalsettings.cpp | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 8f2580308..eb2925953 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -531,7 +531,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -561,7 +561,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -577,7 +577,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -646,7 +646,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -664,11 +664,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText.appendTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); } diff --git a/mapeditor/mapsettings/generalsettings.cpp b/mapeditor/mapsettings/generalsettings.cpp index dd5cff493..a15599e84 100644 --- a/mapeditor/mapsettings/generalsettings.cpp +++ b/mapeditor/mapsettings/generalsettings.cpp @@ -59,8 +59,8 @@ void GeneralSettings::initialize(MapController & c) void GeneralSettings::update() { - controller->map()->name.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); - controller->map()->description.appendTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); + controller->map()->name = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "name"), ui->mapNameEdit->text().toStdString())); + controller->map()->description = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "description"), ui->mapDescriptionEdit->toPlainText().toStdString())); if(ui->heroLevelLimitCheck->isChecked()) controller->map()->levelLimit = ui->heroLevelLimit->value(); else From f42e57c9c551411bd2a8412e7e397b169fc74452 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 14:42:32 +0200 Subject: [PATCH 0703/1248] Update Building_Linux.md --- docs/developers/Building_Linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index d274ac35c..0add5e91e 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -25,7 +25,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++ 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 qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev` +`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 qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev ninja-build` Alternatively if you have VCMI installed from repository or PPA you can use: From 213d0c355337962b19ac70f8fa9fc8d97a8a852c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 16:40:26 +0200 Subject: [PATCH 0704/1248] Capture events in chat box --- client/lobby/CSelectionBase.cpp | 5 +++++ client/lobby/CSelectionBase.h | 1 + 2 files changed, 6 insertions(+) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 4982d8f76..a9444deea 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -327,6 +327,11 @@ CChatBox::CChatBox(const Rect & rect) chatHistory->label->color = Colors::GREEN; } +bool CChatBox::captureThisKey(EShortcut key) +{ + return !inputBox->getText().empty() && key == EShortcut::GLOBAL_ACCEPT; +} + void CChatBox::keyPressed(EShortcut key) { if(key == EShortcut::GLOBAL_ACCEPT && inputBox->getText().size()) diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 5436a959e..7b714fb64 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -122,6 +122,7 @@ public: CChatBox(const Rect & rect); void keyPressed(EShortcut key) override; + bool captureThisKey(EShortcut key) override; void addNewMessage(const std::string & text); }; From cbd014ac88031ae6d5ab6b310001910462fa0174 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 17:35:56 +0200 Subject: [PATCH 0705/1248] code review --- client/windows/CMapOverview.cpp | 15 ++++++++------- client/windows/CMapOverview.h | 34 +++++++++++++++++---------------- 2 files changed, 26 insertions(+), 23 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index e0f7fb58c..4db99c772 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -63,7 +63,7 @@ CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::strin fitToScreen(10); } -Canvas CMapOverview::CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const +Canvas CMapOverviewWidget::createMinimapForLayer(std::unique_ptr & map, int layer) const { Canvas canvas = Canvas(Point(map->width, map->height)); @@ -99,7 +99,7 @@ Canvas CMapOverview::CMapOverviewWidget::createMinimapForLayer(std::unique_ptr CMapOverview::CMapOverviewWidget::createMinimaps(ResourcePath resource) const +std::vector CMapOverviewWidget::createMinimaps(ResourcePath resource) const { std::vector ret = std::vector(); @@ -109,15 +109,16 @@ std::vector CMapOverview::CMapOverviewWidget::createMinimaps(ResourcePat { map = mapService.loadMap(resource); } - catch (...) + catch (const std::exception & e) { + logGlobal->warn("Failed to generate map preview! %s", e.what()); return ret; } return createMinimaps(map); } -std::vector CMapOverview::CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const +std::vector CMapOverviewWidget::createMinimaps(std::unique_ptr & map) const { std::vector ret = std::vector(); @@ -127,7 +128,7 @@ std::vector CMapOverview::CMapOverviewWidget::createMinimaps(std::unique return ret; } -std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const +std::shared_ptr CMapOverviewWidget::buildDrawMinimap(const JsonNode & config) const { logGlobal->debug("Building widget drawMinimap"); @@ -144,7 +145,7 @@ std::shared_ptr CMapOverview::CMapOverviewWidget::buildDrawMinimap(con return std::make_shared(img, Point(rect.x, rect.y)); } -CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): +CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): InterfaceObjectConfigurable(), p(parent) { drawPlayerElements = p.tabType == ESelectionScreen::newGame; @@ -174,7 +175,7 @@ CMapOverview::CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): minimaps = createMinimaps(campaignMap); } - REGISTER_BUILDER("drawMinimap", &CMapOverview::CMapOverviewWidget::buildDrawMinimap); + REGISTER_BUILDER("drawMinimap", &CMapOverviewWidget::buildDrawMinimap); build(config); diff --git a/client/windows/CMapOverview.h b/client/windows/CMapOverview.h index 5c9364508..1602e80c3 100644 --- a/client/windows/CMapOverview.h +++ b/client/windows/CMapOverview.h @@ -26,24 +26,26 @@ class Canvas; class TransparentFilledRectangle; enum ESelectionScreen : ui8; +class CMapOverview; + +class CMapOverviewWidget : public InterfaceObjectConfigurable +{ + CMapOverview& p; + + bool drawPlayerElements; + std::vector minimaps; + + Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; + std::vector createMinimaps(ResourcePath resource) const; + std::vector createMinimaps(std::unique_ptr & map) const; + + std::shared_ptr buildDrawMinimap(const JsonNode & config) const; +public: + CMapOverviewWidget(CMapOverview& p); +}; + class CMapOverview : public CWindowObject { - class CMapOverviewWidget : public InterfaceObjectConfigurable - { - CMapOverview& p; - - bool drawPlayerElements; - std::vector minimaps; - - Canvas createMinimapForLayer(std::unique_ptr & map, int layer) const; - std::vector createMinimaps(ResourcePath resource) const; - std::vector createMinimaps(std::unique_ptr & map) const; - - std::shared_ptr buildDrawMinimap(const JsonNode & config) const; - public: - CMapOverviewWidget(CMapOverview& p); - }; - std::shared_ptr widget; public: From 10eb19758a3334b73796fa0164a1e6d145c6d2ed Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 1 Oct 2023 18:00:36 +0200 Subject: [PATCH 0706/1248] Code review fixes --- lib/mapping/CMapHeader.cpp | 67 ++++++++++++++++++++++++++++---------- lib/mapping/CMapInfo.h | 2 +- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 3f39bac7f..bdd6de2e4 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -144,28 +144,59 @@ ui8 CMapHeader::levels() const void CMapHeader::registerMapStrings() { - auto language = CGeneralTextHandler::getPreferredLanguage(); - JsonNode data; - - if(translations[language].isNull()) + //get supported languages. Assuming that translation containing most strings is the base language + std::set mapLanguages, mapBaseLanguages; + int maxStrings = 0; + for(auto & translation : translations.Struct()) { - //english is preferrable - language = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; - std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; - while(translations[language].isNull() && !languages.empty()) - { - language = languages.front().identifier; - languages.pop_front(); - } + if(translation.first.empty() || !translation.second.isStruct() || translation.second.Struct().empty()) + continue; - if(!translations[language].isNull()) - { - logGlobal->info("Map %s doesn't have any translation", name.toString()); - return; - } + if(translation.second.Struct().size() > maxStrings) + maxStrings = translation.second.Struct().size(); + mapLanguages.insert(translation.first); } + + if(maxStrings == 0 || mapBaseLanguages.empty()) + { + logGlobal->info("Map %s doesn't have any supported translation", name.toString()); + return; + } + + //identifying base languages + for(auto & translation : translations.Struct()) + { + if(translation.second.isStruct() && translation.second.Struct().size() == maxStrings) + mapBaseLanguages.insert(translation.first); + } + + std::string baseLanguage, language; + //english is preferrable as base language + if(mapBaseLanguages.count(Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier)) + baseLanguage = Languages::getLanguageOptions(Languages::ELanguages::ENGLISH).identifier; + else + baseLanguage = *mapBaseLanguages.begin(); - for(auto & s : translations[language].Struct()) + if(mapBaseLanguages.count(CGeneralTextHandler::getPreferredLanguage())) + { + language = CGeneralTextHandler::getPreferredLanguage(); //preferred language is base language - use it + baseLanguage = language; + } + else + { + if(mapLanguages.count(CGeneralTextHandler::getPreferredLanguage())) + language = CGeneralTextHandler::getPreferredLanguage(); + else + language = baseLanguage; //preferred language is not supported, use base language + } + + assert(!language.empty()); + + JsonNode data = translations[baseLanguage]; + if(language != baseLanguage) + JsonUtils::mergeCopy(data, translations[language]); + + for(auto & s : data.Struct()) registerString("map", TextIdentifier(s.first), s.second.String(), language); } diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index 6ac7ccea9..ba09a3c15 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -49,7 +49,7 @@ public: void saveInit(const ResourcePath & file); void campaignInit(); void countPlayers(); - // TODO: Those must be on client-side + std::string getNameTranslated() const; std::string getNameForList() const; std::string getDescriptionTranslated() const; From 46d785c371ba9c4916b2d5125c6a3964f7efc3c4 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 1 Oct 2023 14:45:31 +0200 Subject: [PATCH 0707/1248] Fix iOS build with ENABLE_PCH=OFF https://github.com/vcmi/vcmi/issues/2991#issuecomment-1742066891 Fixes #2991 --- .github/workflows/github.yml | 7 ------- CMakePresets.json | 6 +----- client/CFocusableHelper.cpp | 1 + client/ios/startSDL.mm | 1 + client/ios/utils.mm | 1 + 5 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 4319c74e2..1fbc2a4b7 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -159,7 +159,6 @@ jobs: VCMI_BUILD_PLATFORM: x64 - name: ccache - if: startsWith(matrix.preset, 'ios') != true uses: hendrikmuhs/ccache-action@v1.2 with: key: ${{ matrix.preset }} @@ -203,15 +202,9 @@ jobs: PULL_REQUEST: ${{ github.event.pull_request.number }} - name: CMake Preset with ccache - if: startsWith(matrix.preset, 'ios') != true run: | cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }} - - name: CMake Preset without ccache - if: startsWith(matrix.preset, 'ios') - run: | - cmake --preset ${{ matrix.preset }} - - name: Build Preset run: | cmake --build --preset ${{matrix.preset}} diff --git a/CMakePresets.json b/CMakePresets.json index ce2781c07..34a93a199 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -84,7 +84,6 @@ "cacheVariables": { "CMAKE_BUILD_TYPE": "Debug", "ENABLE_LUA" : "ON", - "ENABLE_PCH" : "OFF", "CMAKE_C_COMPILER": "/usr/bin/gcc", "CMAKE_CXX_COMPILER": "/usr/bin/g++" } @@ -218,10 +217,7 @@ "inherits": [ "base-ios-release", "ios-device-conan" - ], - "cacheVariables": { - "ENABLE_PCH" : "ON" - } + ] }, { "name": "ios-release-legacy", diff --git a/client/CFocusableHelper.cpp b/client/CFocusableHelper.cpp index e4e3c6cfe..c668f825b 100644 --- a/client/CFocusableHelper.cpp +++ b/client/CFocusableHelper.cpp @@ -9,6 +9,7 @@ */ #include "CFocusableHelper.h" #include "../Global.h" +#include "StdInc.h" #include "widgets/TextControls.h" void removeFocusFromActiveInput() diff --git a/client/ios/startSDL.mm b/client/ios/startSDL.mm index 24ad9a888..0b3b8028e 100644 --- a/client/ios/startSDL.mm +++ b/client/ios/startSDL.mm @@ -8,6 +8,7 @@ * */ #import "startSDL.h" +#include "StdInc.h" #import "GameChatKeyboardHandler.h" #include "../Global.h" diff --git a/client/ios/utils.mm b/client/ios/utils.mm index 42cca084e..2a04be7b3 100644 --- a/client/ios/utils.mm +++ b/client/ios/utils.mm @@ -8,6 +8,7 @@ * */ +#include "StdInc.h" #include "utils.h" #import From 12ca3ea0ec3373e172d1dd44a7553ffe007a63f7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 1 Oct 2023 19:44:04 +0200 Subject: [PATCH 0708/1248] pos from texture --- client/windows/CMapOverview.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 4db99c772..ffa7aeb03 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -49,9 +49,6 @@ CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::strin OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); - pos = Rect(0, 0, config["items"][0]["rect"]["w"].Integer(), config["items"][0]["rect"]["h"].Integer()); - widget = std::make_shared(*this); updateShadow(); @@ -179,6 +176,10 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): build(config); + if(auto w = widget("background")) + { + p.pos = w->pos; + } if(auto w = widget("fileName")) { w->setText(p.fileName); From d14c872164e34e3792a4de57e688bfee36a621ec Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 1 Oct 2023 21:59:15 +0200 Subject: [PATCH 0709/1248] iOS CI build: Make XCode work with ccache --- CMakeLists.txt | 18 ++++++++++++++++++ ios/launch-c.in | 3 +++ ios/launch-cxx.in | 3 +++ 3 files changed, 24 insertions(+) create mode 100644 ios/launch-c.in create mode 100644 ios/launch-cxx.in diff --git a/CMakeLists.txt b/CMakeLists.txt index d711ceec9..10ad46630 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,6 +222,24 @@ if(APPLE_IOS) set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED_FOR_APPS YES) set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUNDLE_IDENTIFIER_PREFIX}.$(PRODUCT_NAME)") set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2") + + if(CMAKE_CXX_COMPILER_LAUNCHER) + find_program(CCACHE "ccache") + if(CCACHE) + # https://stackoverflow.com/a/36515503/2278742 + # Set up wrapper scripts + configure_file(ios/launch-c.in ios/launch-c) + configure_file(ios/launch-cxx.in ios/launch-cxx) + execute_process(COMMAND chmod a+rx + "${CMAKE_BINARY_DIR}/ios/launch-c" + "${CMAKE_BINARY_DIR}/ios/launch-cxx") + # Set Xcode project attributes to route compilation through our scripts + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/ios/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/ios/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/ios/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/ios/launch-cxx") + endif() + endif() endif() if(APPLE_MACOS) diff --git a/ios/launch-c.in b/ios/launch-c.in new file mode 100644 index 000000000..59ced1e88 --- /dev/null +++ b/ios/launch-c.in @@ -0,0 +1,3 @@ +#!/bin/sh +export CCACHE_CPP2=true +ccache clang "$@" diff --git a/ios/launch-cxx.in b/ios/launch-cxx.in new file mode 100644 index 000000000..2d52f768c --- /dev/null +++ b/ios/launch-cxx.in @@ -0,0 +1,3 @@ +#!/bin/sh +export CCACHE_CPP2=true +ccache clang++ "$@" From 65354da5f141e17632f95a1ca2be07023e152ab0 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 2 Oct 2023 00:09:25 +0200 Subject: [PATCH 0710/1248] Add CMake preset for iOS build without precompiled headers, which is required for ccache --- .github/workflows/github.yml | 2 +- CMakePresets.json | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 1fbc2a4b7..c5f18fe29 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -103,7 +103,7 @@ jobs: test: 0 pack: 1 extension: ipa - preset: ios-release-conan + preset: ios-release-conan-ccache conan_profile: ios-arm64 conan_options: --options with_apple_system_libs=True - platform: msvc diff --git a/CMakePresets.json b/CMakePresets.json index 34a93a199..37c321522 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -217,7 +217,19 @@ "inherits": [ "base-ios-release", "ios-device-conan" - ] + ], + "cacheVariables": { + "ENABLE_PCH" : "ON" + } + }, + { + "name": "ios-release-conan-ccache", + "displayName": "iOS+Conan release using ccache", + "description": "VCMI iOS release using Conan and ccache", + "inherits": "ios-release-conan", + "cacheVariables": { + "ENABLE_PCH" : "OFF" + } }, { "name": "ios-release-legacy", @@ -327,6 +339,11 @@ "CODE_SIGNING_ALLOWED_FOR_APPS=NO" ] }, + { + "name": "ios-release-conan-ccache", + "configurePreset": "ios-release-conan-ccache", + "inherits": "ios-release-conan" + }, { "name": "ios-release-legacy", "configurePreset": "ios-release-legacy", From c7523326a005b80b2d4ca16362e24e691aeb04d7 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 2 Oct 2023 01:12:32 +0200 Subject: [PATCH 0711/1248] Make ccache work with MSVC build --- CMakeLists.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 10ad46630..aa0a6f724 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -255,6 +255,23 @@ if(MINGW OR MSVC) set(CMAKE_SHARED_LIBRARY_PREFIX "") if(MSVC) + if(CMAKE_CXX_COMPILER_LAUNCHER) + # https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387 + find_program(ccache_exe ccache) + if (ccache_exe) + file(COPY_FILE + ${ccache_exe} ${CMAKE_BINARY_DIR}/cl.exe + ONLY_IF_DIFFERENT) + + set(CMAKE_VS_GLOBALS + "CLToolExe=cl.exe" + "CLToolPath=${CMAKE_BINARY_DIR}" + "TrackFileAccess=false" + "UseMultiToolTask=true" + ) + endif() + endif() + add_definitions(-DBOOST_ALL_NO_LIB) add_definitions(-DBOOST_ALL_DYN_LINK) set(Boost_USE_STATIC_LIBS OFF) From 7c3329a18417a3e7b9d829d901be7df84686b293 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 2 Oct 2023 20:46:20 +0200 Subject: [PATCH 0712/1248] Update translation --- Mods/vcmi/config/vcmi/russian.json | 96 +++++++++++++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 63d1a2d48..618ffd861 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -21,8 +21,40 @@ "vcmi.adventureMap.moveCostDetails" : "Очки движения - Стоимость: %TURNS ходов + %POINTS очков, Останется: %REMAINING очков", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки движения - Стоимость: %POINTS очков, Останется: %REMAINING очков", + "vcmi.capitalColors.0" : "Красный", + "vcmi.capitalColors.1" : "Синий", + "vcmi.capitalColors.2" : "Коричневый", + "vcmi.capitalColors.3" : "Зеленый", + "vcmi.capitalColors.4" : "Оранжевый", + "vcmi.capitalColors.5" : "Пурпурный", + "vcmi.capitalColors.6" : "Бирюзовый", + "vcmi.capitalColors.7" : "Розовый", + + "vcmi.heroOverview.startingArmy" : "Начальная армия", + "vcmi.heroOverview.warMachine" : "Боевые машины", + "vcmi.heroOverview.secondarySkills" : "Вторичные навыки", + "vcmi.heroOverview.spells" : "Заклинания", + + "vcmi.radialWheel.mergeSameUnit" : "Объединить одинковые существа", + "vcmi.radialWheel.fillSingleUnit" : "Заполнить свободные слоты единицами", + "vcmi.radialWheel.splitSingleUnit" : "Отделить единицу в отдельный слот", + "vcmi.radialWheel.splitUnitEqually" : "Разделить отряд поровну", + "vcmi.radialWheel.moveUnit" : "Перенести отряд в другую армию", + "vcmi.radialWheel.splitUnit" : "Разделить отряд в другой слот", + + "vcmi.mainMenu.serverConnecting" : "Подключение...", + "vcmi.mainMenu.serverAddressEnter" : "Введите адрес:", + "vcmi.mainMenu.serverClosing" : "Завершение...", + "vcmi.mainMenu.hostTCP" : "Создать игру по TCP/IP", + "vcmi.mainMenu.joinTCP" : "Присединиться к игре по TCP/IP", + "vcmi.mainMenu.playerName" : "Игрок", + + "vcmi.lobby.filename" : "Имя файла", + "vcmi.lobby.creationDate" : "Дата создания", + "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", "vcmi.server.errors.modsToEnable" : "{Требуемые моды для загрузки игры}", + "vcmi.server.errors.modsToDisable" : "{Необходимо отключить следующие моды}", "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", "vcmi.settingsMainWindow.generalTab.hover" : "Общее", @@ -37,12 +69,29 @@ "vcmi.systemOptions.otherGroup" : "Иное", // unused right now "vcmi.systemOptions.townsGroup" : "Экран города", + "vcmi.systemOptions.fullscreenBorderless.hover" : "Полноэкранный режим (в окне)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Полноэкранный режим (в окне)}\n\nЕсли выбрано, VCMI будет работать в режиме безрамочного окна, растянутого на весь экран. В данном режиме игра использует текущее разрешение экрана, не меняя его. ", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Полноэкранный режим (без окна)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Полноэкранный режим (без окна)}\n\nЕсли выбрано, VCMI будет работать в полноэкранном режиме. Теущее разрешение экрана будет изменено на выбранное.", "vcmi.systemOptions.resolutionButton.hover" : "Разрешение %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Разрешение экрана}\n\n Изменение разрешения экрана. Для применения нового разрешения требуется перезапуск игры.", + "vcmi.systemOptions.resolutionButton.help" : "{Разрешение экрана}\n\n Изменение разрешения экрана.", "vcmi.systemOptions.resolutionMenu.hover" : "Выбрать разрешения экрана", "vcmi.systemOptions.resolutionMenu.help" : "Изменение разрешения экрана в игре.", + "vcmi.systemOptions.scalingButton.hover" : "Масштаб интерфейса: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Масштаб интерфейса}\n\nИзменить масштаб игрового интерфеса.", + "vcmi.systemOptions.scalingMenu.hover" : "Выбрать масштаб интерфеса", + "vcmi.systemOptions.scalingMenu.help" : "Изменить масштаб игрового интерфеса.", + "vcmi.systemOptions.longTouchButton.hover" : "Интервал длительного касания: %d мс", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Интервал длительного касания}\n\nПри использовании сенсорного экрана, всплывающие окна будут показаны после длительного нажатия указанной продолжительности, в миллисекундах.", + "vcmi.systemOptions.longTouchMenu.hover" : "Выбрать интервал длительного касания:", + "vcmi.systemOptions.longTouchMenu.help" : "Изменить интервал длительного касания.", + "vcmi.systemOptions.longTouchMenu.entry" : "%d миллисекунд", "vcmi.systemOptions.framerateButton.hover" : "Показывать частоту кадров", - "vcmi.systemOptions.framerateButton.help" : "{Показывать частоту кадров}\n\n Включить счетчик частоты кадров в углу игрового клиента", + "vcmi.systemOptions.framerateButton.help" : "{Показывать частоту кадров}\n\nЕсли выбрано, частота кадров в секунду будет показана в верхнем левом углу игрового экрана.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильный отклик", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильный отклик}\n\nиспользовать вибрацию при использовании сенсорного экрана.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Расширенные функции интерфейса", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Расширенные функции интерфейса}\n\nРазличные улучшения и дополнительные функции интерфейса. Например, большая книга заклинаний, рюкзак и т.д. Отключите для классического интерфейса.", "vcmi.adventureOptions.infoBarPick.hover" : "Сообщения в информационной панели", "vcmi.adventureOptions.infoBarPick.help" : "{Сообщения в информационной панели}\n\n Если сообщения помещаются, то показывать их в информационной панели (только на интерфейсе карты).", @@ -52,6 +101,12 @@ "vcmi.adventureOptions.forceMovementInfo.help" : "{Всегда показывать стоимость перемещения}\n\n Заменить информацию в статусной строке на информацию о перемещении без необходимости нажатия {ALT}", "vcmi.adventureOptions.showGrid.hover" : "Сетка", "vcmi.adventureOptions.showGrid.help" : "{Сетка}\n\n Показывать сетку на видимой части карты.", + "vcmi.adventureOptions.borderScroll.hover" : "Прокрутка карты по краю", + "vcmi.adventureOptions.borderScroll.help" : "{Прокрутка карты по краю}\n\nПеремещение карты происходит при приближении курсора к краю окна. Отключается при зажатии клавиши CTRL.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Панель управления существами", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Панель управления существами}\n\nПозволяет управлять существами в информационной панели.", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Перемещение карты по нажатию", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Перемещение карты по нажатию}\n\nЕсли включено, зажатие и перемещение левой кнопки мыши перемещает карту.", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -74,10 +129,28 @@ "vcmi.battleOptions.animationsSpeed1.help": "Очень медленная скорость анимации.", "vcmi.battleOptions.animationsSpeed5.help": "Очень быстрая скорость анимации.", "vcmi.battleOptions.animationsSpeed6.help": "Мгновенная скорость анимации.", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Показывать область перемещения существ", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Показывать область перемещения существ}\n\nПодсвечивать область перемещения существ при наведении курсора.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Показывать область полного урона стрелков", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Показывать область полного урона стрелков}\n\nПоказывать область полного урона стреляющих существ при наведении курсора.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Показывать характеристики героев", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Показывать характеристики героев}\n\nПоказывать характеристики героев и очки магии все время.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускать вступительную музыку", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускать вступительную музыку}\n\n Пропускать музыку, которая проигрывается в начале каждой битвы. Также может быть пропущена по нажатию {ESC}", "vcmi.battleWindow.pressKeyToSkipIntro" : "Нажмите любую клавишу для пропуска вступительной музыки", + "vcmi.battleWindow.damageEstimation.melee" : "Атаковать %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Атаковать %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Стрелять в %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Стрелять в %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "Осталось %d выстрелов", + "vcmi.battleWindow.damageEstimation.shots.1" : "Остался %d выстрел", + "vcmi.battleWindow.damageEstimation.damage" : "%d единиц урона", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d единица урона", + "vcmi.battleWindow.damageEstimation.kills" : "%d погибнут", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d погибнет", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Принять результаты боя", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показывать доступных существ", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показывать доступных существ}\n\n Показывать число доступных существ вместо прироста на экране города (в левом нижнем углу).", @@ -106,6 +179,8 @@ "vcmi.heroWindow.openCommander.hover" : "Открыть экран командира", "vcmi.heroWindow.openCommander.help" : "Показать информацию о командире у данного героя", + "vcmi.heroWindow.openBackpack.hover" : "Открыть рюкзак артефактов", + "vcmi.heroWindow.openBackpack.help" : "Рюкзак артефактов упрощает управление артефактами", "vcmi.commanderWindow.artifactMessage" : "Вы хотите отдать артефакт назад герою?", @@ -125,6 +200,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Виды дорог", + "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Время игрока}\n\nОбратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", + "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Время на ход}\n\nОбратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", + "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Время на битву}\n\nОбратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", + "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Время на отряд}\n\nОбратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", + "vcmi.optionsTab.widgets.labelTimer" : "Таймер", + "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Классические часы", + "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Шахматные часы", + "mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов", "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", "mapObject.core.creatureBank.dwarvenTreasury.name" : "Сокровищница гномов", @@ -148,6 +231,15 @@ "spell.core.strongholdMoatTrigger.name" : "Стена шипов", "spell.core.fortressMoatTrigger.name" : "Кипящая смола", + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Врагу удалось выжить до сегодняшнего дня. Он одержал победу!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Поздравляем! Вам удалось остаться в живых. Победа за вами!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Враг одержал победу над всеми монстрами, заполонившими эту землю, и празднует победу!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Поздравляем! Вы одержали победу над всеми монстрами, заполонившими эту землю, и можете отпраздновать победу!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Получить три артефакта", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Поздравляем! Все ваши враги одержаны победой, и у вас есть Альянс Ангелов! Победа ваша!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Одержите победу над всеми врагами и создайте Альянс Ангелов", + // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» О п ы т с у щ е с т в «\n\nТип существа ................... : %s\nРанг опыта ................. : %s (%i)\nОчки опыта ............... : %i\nДо следующего .. : %i\nМаксимум за битву ... : %i%% (%i)\nЧисло в отряде .... : %i\nМаксимум новичков\n без потери ранга .... : %i\nМножитель опыта ........... : %.2f\nМножитель улучшения .......... : %.2f\nОпыт после 10 ранга ........ : %i\nМаксимум новичков для сохранения\n ранга 10 при максимальном опыте : %i", "vcmi.stackExperience.rank.0" : "Рекрут", From 614d000376f7e12e0ec1a7e785478a0f1ebb4b60 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 00:31:31 +0200 Subject: [PATCH 0713/1248] Sleep for 10 ms instead of 1000 ms while waiting for server connection --- client/CServerHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index aae41f2f0..dd034d36a 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -263,8 +263,8 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po } catch(std::runtime_error & error) { - logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + logNetwork->warn("\nCannot establish connection. %s Retrying in 10 ms", error.what()); + boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); } } From 9f9930a9dd17e6c632c0555ef7b2465af3fbef9d Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 15:55:15 +0200 Subject: [PATCH 0714/1248] CServerHandler::justConnectToServer(): Read settings only once --- client/CServerHandler.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index dd034d36a..0a88eafe8 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -251,14 +251,16 @@ void CServerHandler::startLocalServerAndConnect() void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) { state = EClientState::CONNECTING; + std::string hostAddress = getHostAddress(); + ui16 hostPort = getHostPort(); + logNetwork->info("Establishing connection..."); while(!c && state != EClientState::CONNECTION_CANCELLED) { try { - logNetwork->info("Establishing connection..."); c = std::make_shared( - addr.size() ? addr : getHostAddress(), - port ? port : getHostPort(), + addr.size() ? addr : hostAddress, + port ? port : hostPort, NAME, uuid); } catch(std::runtime_error & error) From 39427d6658f8e2fe98eb8cb66cced3a56167354a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 17:11:19 +0200 Subject: [PATCH 0715/1248] Use different delay and maximum number of connection attempts for local and remote servers --- client/CServerHandler.cpp | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 0a88eafe8..8dc99ae6f 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -251,11 +251,39 @@ void CServerHandler::startLocalServerAndConnect() void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) { state = EClientState::CONNECTING; + + logNetwork->info("Establishing connection..."); + std::string hostAddress = getHostAddress(); ui16 hostPort = getHostPort(); - logNetwork->info("Establishing connection..."); + + boost::chrono::duration> sleepDuration{}; + int maxConnectionAttempts; + + if(hostAddress == "127.0.0.1") + { + sleepDuration = boost::chrono::milliseconds(10); + maxConnectionAttempts = 100; + } + else + { + // remote server + sleepDuration = boost::chrono::seconds(2); + maxConnectionAttempts = 10; + } + + logNetwork->info("\nWaiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); + + uint connectionAttemptCount = 0; while(!c && state != EClientState::CONNECTION_CANCELLED) { + connectionAttemptCount++; + if(connectionAttemptCount > maxConnectionAttempts) + { + logNetwork->error("\nExceeded maximum of %d connection attempts", maxConnectionAttempts); + break; + } + try { c = std::make_shared( @@ -265,8 +293,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po } catch(std::runtime_error & error) { - logNetwork->warn("\nCannot establish connection. %s Retrying in 10 ms", error.what()); - boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); + boost::this_thread::sleep_for(sleepDuration); } } From e1401d26c53c6fb084ff7471922b36cca5b22449 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 17:55:39 +0200 Subject: [PATCH 0716/1248] hostAddress can be localhost, return from justConnectToServer() is maximum number of attempts is exceeded --- client/CServerHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 8dc99ae6f..2fb03be44 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -260,7 +260,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po boost::chrono::duration> sleepDuration{}; int maxConnectionAttempts; - if(hostAddress == "127.0.0.1") + if(hostAddress == "127.0.0.1" || hostAddress == "localhost") { sleepDuration = boost::chrono::milliseconds(10); maxConnectionAttempts = 100; @@ -281,7 +281,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po if(connectionAttemptCount > maxConnectionAttempts) { logNetwork->error("\nExceeded maximum of %d connection attempts", maxConnectionAttempts); - break; + return; } try From a66d8ecb8f2e8d04e1bb02989ce2e28a2641134e Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 19:51:29 +0200 Subject: [PATCH 0717/1248] Use ui16 instead of uint for connectionAttemptCount to fix MinGW build --- client/CServerHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 2fb03be44..5de41a39f 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -274,7 +274,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po logNetwork->info("\nWaiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); - uint connectionAttemptCount = 0; + ui16 connectionAttemptCount = 0; while(!c && state != EClientState::CONNECTION_CANCELLED) { connectionAttemptCount++; From b58cca77703beed78a6592fafc57b47344525af9 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 27 Sep 2023 19:59:56 +0200 Subject: [PATCH 0718/1248] 100 connection attempts is not always enough --- client/CServerHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 5de41a39f..a7423b9e9 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -263,7 +263,7 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po if(hostAddress == "127.0.0.1" || hostAddress == "localhost") { sleepDuration = boost::chrono::milliseconds(10); - maxConnectionAttempts = 100; + maxConnectionAttempts = 1000; } else { From 898733eed706ce7151e8c15d573c6337fd764ce2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 28 Sep 2023 01:17:05 +0300 Subject: [PATCH 0719/1248] Added Query to track visit duration for Taverns and Markets --- AI/Nullkiller/AIGateway.cpp | 15 +++++-- AI/Nullkiller/AIGateway.h | 6 +-- AI/VCAI/VCAI.cpp | 15 +++++-- AI/VCAI/VCAI.h | 6 +-- client/CPlayerInterface.cpp | 27 ++++++++---- client/CPlayerInterface.h | 6 +-- client/Client.h | 2 +- client/NetPacksClient.cpp | 43 +++++++++++-------- client/adventureMap/AdventureMapShortcuts.cpp | 2 +- client/windows/CCastleInterface.cpp | 16 +++---- client/windows/CTradeWindow.cpp | 25 ++++++++--- client/windows/CTradeWindow.h | 8 ++-- client/windows/GUIClasses.cpp | 36 ++++++++++++++-- client/windows/GUIClasses.h | 15 +++++-- lib/IGameCallback.h | 3 +- lib/IGameEventsReceiver.h | 6 +-- lib/NetPacks.h | 11 ++--- lib/mapObjects/CGDwelling.cpp | 9 +--- lib/mapObjects/CGMarket.cpp | 4 +- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/IObjectInterface.cpp | 10 ----- lib/mapObjects/IObjectInterface.h | 3 -- lib/mapObjects/MiscObjects.cpp | 9 ++-- server/CGameHandler.cpp | 19 +++++--- server/CGameHandler.h | 2 +- server/queries/MapQueries.cpp | 33 ++++++++++++++ server/queries/MapQueries.h | 10 ++++- 27 files changed, 230 insertions(+), 113 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 74f101883..6eeef4e08 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -154,10 +154,13 @@ void AIGateway::artifactAssembled(const ArtifactLocation & al) NET_EVENT_HANDLER; } -void AIGateway::showTavernWindow(const CGObjectInstance * townOrTavern) +void AIGateway::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "TavernWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::showThievesGuildWindow(const CGObjectInstance * obj) @@ -425,10 +428,13 @@ void AIGateway::receivedResource() NET_EVENT_HANDLER; } -void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) +void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "UniversityWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::heroManaPointsChanged(const CGHeroInstance * hero) @@ -498,10 +504,13 @@ void AIGateway::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonu NET_EVENT_HANDLER; } -void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) +void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "MarketWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 261a368b5..424739082 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -130,7 +130,7 @@ public: void tileHidden(const std::unordered_set & pos) override; void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; void artifactAssembled(const ArtifactLocation & al) override; - void showTavernWindow(const CGObjectInstance * townOrTavern) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; void showThievesGuildWindow(const CGObjectInstance * obj) override; void playerBlocked(int reason, bool start) override; void showPuzzleMap() override; @@ -157,7 +157,7 @@ public: void requestRealized(PackageApplied * pa) override; void receivedResource() override; void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void battleResultsApplied() override; @@ -165,7 +165,7 @@ public: void objectPropertyChanged(const SetObjectProperty * sop) override; void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index a4fea7d24..2098cfa70 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -165,10 +165,13 @@ void VCAI::artifactAssembled(const ArtifactLocation & al) NET_EVENT_HANDLER; } -void VCAI::showTavernWindow(const CGObjectInstance * townOrTavern) +void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "TavernWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) @@ -512,10 +515,13 @@ void VCAI::receivedResource() NET_EVENT_HANDLER; } -void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) +void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "UniversityWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) @@ -577,10 +583,13 @@ void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bo NET_EVENT_HANDLER; } -void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) +void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) { LOG_TRACE(logAi); NET_EVENT_HANDLER; + + status.addQuery(queryID, "MarketWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 4abdbcd62..f31a22e0d 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -163,7 +163,7 @@ public: void tileHidden(const std::unordered_set & pos) override; void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; void artifactAssembled(const ArtifactLocation & al) override; - void showTavernWindow(const CGObjectInstance * townOrTavern) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; void showThievesGuildWindow(const CGObjectInstance * obj) override; void playerBlocked(int reason, bool start) override; void showPuzzleMap() override; @@ -190,7 +190,7 @@ public: void requestRealized(PackageApplied * pa) override; void receivedResource() override; void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void battleResultsApplied() override; @@ -198,7 +198,7 @@ public: void objectPropertyChanged(const SetObjectProperty * sop) override; void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index be2ded812..d91621c32 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1621,24 +1621,30 @@ void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) battleInt->newRoundFirst(); } -void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) +void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) - GH.windows().createAndPushWindow(market, visitor, EMarketMode::ARTIFACT_EXP); + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP); else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) - GH.windows().createAndPushWindow(market, visitor, EMarketMode::CREATURE_EXP); + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP); else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) - GH.windows().createAndPushWindow(market, visitor); + GH.windows().createAndPushWindow(market, visitor, onWindowClosed); else if(!market->availableModes().empty()) - GH.windows().createAndPushWindow(market, visitor, market->availableModes().front()); + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, market->availableModes().front()); } -void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) +void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(visitor, market); + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(visitor, market, onWindowClosed); } void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) @@ -1654,10 +1660,13 @@ void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) cmw->artifactsChanged(false); } -void CPlayerInterface::showTavernWindow(const CGObjectInstance *townOrTavern) +void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(townOrTavern); + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(object, onWindowClosed); } void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index fdcad9ff2..175a109b5 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -122,8 +122,8 @@ protected: // Call-ins from server, should not be called directly, but only via void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override; - void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override; + void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; + void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war @@ -179,7 +179,7 @@ public: // public interface for use by client via LOCPLINT access void viewWorldMap() override; void showQuestLog() override; void showThievesGuildWindow (const CGObjectInstance * obj) override; - void showTavernWindow(const CGObjectInstance *townOrTavern) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); diff --git a/client/Client.h b/client/Client.h index 5952404a4..b9c98768b 100644 --- a/client/Client.h +++ b/client/Client.h @@ -170,7 +170,7 @@ public: void showBlockingDialog(BlockingDialog * iw) override {}; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; void showTeleportDialog(TeleportDialog * iw) override {}; - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {}; + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void giveResource(PlayerColor player, GameResID which, int val) override {}; virtual void giveResources(PlayerColor player, TResources resources) override {}; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index b26c07dbc..c0282a450 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -936,58 +936,67 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) case EOpenWindowMode::RECRUITMENT_FIRST: case EOpenWindowMode::RECRUITMENT_ALL: { - const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id1))); - const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id2))); + assert(pack.queryID == QueryID::NONE); + const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); + const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.visitor))); callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1); } break; case EOpenWindowMode::SHIPYARD_WINDOW: { - const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.id1))); + assert(pack.queryID == QueryID::NONE); + const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object))); callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); } break; case EOpenWindowMode::THIEVES_GUILD: { + assert(pack.queryID == QueryID::NONE); //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showThievesGuildWindow, obj); + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj); } break; case EOpenWindowMode::UNIVERSITY_WINDOW: { //displays University window (when hero enters University on adventure map) - const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.id1))); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero); + const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); } break; case EOpenWindowMode::MARKET_WINDOW: { //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); const IMarket *market = IMarket::castFrom(obj); - callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero); + callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); } break; case EOpenWindowMode::HILL_FORT_WINDOW: { + assert(pack.queryID == QueryID::NONE); //displays Hill fort window - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.id1)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.id2)); + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); } break; case EOpenWindowMode::PUZZLE_MAP: { - callInterfaceIfPresent(cl, PlayerColor(pack.id1), &IGameEventsReceiver::showPuzzleMap); + assert(pack.queryID == QueryID::NONE); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap); } break; case EOpenWindowMode::TAVERN_WINDOW: - const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.id1)), - *obj2 = cl.getObj(ObjectInstanceID(pack.id2)); - callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::showTavernWindow, obj2); + { + const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID); + } break; } } diff --git a/client/adventureMap/AdventureMapShortcuts.cpp b/client/adventureMap/AdventureMapShortcuts.cpp index 30edda987..abdb52cd1 100644 --- a/client/adventureMap/AdventureMapShortcuts.cpp +++ b/client/adventureMap/AdventureMapShortcuts.cpp @@ -342,7 +342,7 @@ void AdventureMapShortcuts::showMarketplace() } if(townWithMarket) //if any town has marketplace, open window - GH.windows().createAndPushWindow(townWithMarket); + GH.windows().createAndPushWindow(townWithMarket, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); else //if not - complain LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); } diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 471a79c2b..5b54b8fe5 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -674,7 +674,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil break; case BuildingID::TAVERN: - LOCPLINT->showTavernWindow(town); + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); break; case BuildingID::SHIPYARD: @@ -700,7 +700,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil case BuildingID::MARKETPLACE: // can't use allied marketplace if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero); + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); else enterBuilding(building); break; @@ -728,7 +728,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil case BuildingSubID::ARTIFACT_MERCHANT: if(town->visitingHero) - GH.windows().createAndPushWindow(town, town->visitingHero, EMarketMode::RESOURCE_ARTIFACT); + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT); else LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. break; @@ -739,21 +739,21 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil case BuildingSubID::FREELANCERS_GUILD: if(getHero()) - GH.windows().createAndPushWindow(town, getHero(), EMarketMode::CREATURE_RESOURCE); + GH.windows().createAndPushWindow(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE); else LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. break; case BuildingSubID::MAGIC_UNIVERSITY: if (getHero()) - GH.windows().createAndPushWindow(getHero(), town); + GH.windows().createAndPushWindow(getHero(), town, nullptr); else enterBuilding(building); break; case BuildingSubID::BROTHERHOOD_OF_SWORD: if(upgrades == BuildingID::TAVERN) - LOCPLINT->showTavernWindow(town); + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); else enterBuilding(building); break; @@ -763,7 +763,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil break; case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer - GH.windows().createAndPushWindow(town, getHero()); + GH.windows().createAndPushWindow(town, getHero(), nullptr); break; case BuildingSubID::PORTAL_OF_SUMMONING: @@ -1319,7 +1319,7 @@ void CCastleInterface::keyPressed(EShortcut key) break; case EShortcut::TOWN_OPEN_TAVERN: if(town->hasBuilt(BuildingID::TAVERN)) - LOCPLINT->showTavernWindow(town); + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); break; default: break; diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 17dc3d2bf..dfa72c231 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -317,9 +317,10 @@ void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) setID(-1); } -CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, EMarketMode Mode): +CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, const std::function & onWindowClosed, EMarketMode Mode): CWindowObject(PLAYER_COLORED, bgName), market(Market), + onWindowClosed(onWindowClosed), hero(Hero), readyToTrade(false) { @@ -561,6 +562,14 @@ void CTradeWindow::showAll(Canvas & to) } } +void CTradeWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CWindowObject::close(); +} + void CTradeWindow::removeItems(const std::set> & toRemove) { for(auto item : toRemove) @@ -589,17 +598,19 @@ void CTradeWindow::setMode(EMarketMode Mode) { const IMarket *m = market; const CGHeroInstance *h = hero; + const auto functor = onWindowClosed; + onWindowClosed = nullptr; // don't call on closing of this window - pass it to next window close(); switch(Mode) { case EMarketMode::CREATURE_EXP: case EMarketMode::ARTIFACT_EXP: - GH.windows().createAndPushWindow(m, h, Mode); + GH.windows().createAndPushWindow(m, h, functor, Mode); break; default: - GH.windows().createAndPushWindow(m, h, Mode); + GH.windows().createAndPushWindow(m, h, functor, Mode); break; } } @@ -635,8 +646,8 @@ ImagePath CMarketplaceWindow::getBackgroundForMode(EMarketMode mode) return {}; } -CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) - : CTradeWindow(getBackgroundForMode(Mode), Market, Hero, Mode) +CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode) + : CTradeWindow(getBackgroundForMode(Mode), Market, Hero, onWindowClosed, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1093,8 +1104,8 @@ void CMarketplaceWindow::updateTraderText() traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]); } -CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode) - : CTradeWindow(ImagePath::builtin(Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, Mode) +CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode) + : CTradeWindow(ImagePath::builtin(Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, onWindowClosed, Mode) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index bdf253b89..b097ebf76 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -83,9 +83,10 @@ public: std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; - CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); //c + CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); //c void showAll(Canvas & to) override; + void close(); void initSubs(bool Left); void initTypes(); @@ -106,6 +107,7 @@ public: virtual void garrisonChanged() = 0; virtual void artifactsChanged(bool left) = 0; protected: + std::function onWindowClosed; std::shared_ptr statusBar; std::vector> labels; std::vector> images; @@ -130,7 +132,7 @@ public: void sliderMoved(int to); void makeDeal(); void selectionChanged(bool side) override; //true == left - CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero = nullptr, EMarketMode Mode = EMarketMode::RESOURCE_RESOURCE); + CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); ~CMarketplaceWindow(); Point selectionOffset(bool Left) const override; @@ -157,7 +159,7 @@ public: std::shared_ptr expOnAltar; std::shared_ptr arts; - CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, EMarketMode Mode); + CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); ~CAltarWindow(); void getExpValues(); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 8bf9052c1..f21aef79d 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -430,8 +430,9 @@ CLevelWindow::~CLevelWindow() LOCPLINT->showingDialog->setn(false); } -CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj) +CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), + onWindowClosed(onWindowClosed), tavernObj(TavernObj) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -500,8 +501,9 @@ void CTavernWindow::recruitb() { const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; const CGObjectInstance *obj = tavernObj; - close(); + LOCPLINT->cb->recruitHero(obj, toBuy); + close(); } void CTavernWindow::thievesguildb() @@ -509,6 +511,14 @@ void CTavernWindow::thievesguildb() GH.windows().createAndPushWindow(tavernObj); } +void CTavernWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + CTavernWindow::~CTavernWindow() { CCS->videoh->close(); @@ -993,9 +1003,10 @@ void CTransformerWindow::updateGarrisons() item->update(); } -CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero) +CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), hero(_hero), + onWindowClosed(onWindowClosed), market(_market) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1024,6 +1035,14 @@ CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInst helpRight = std::make_shared(CGI->generaltexth->allTexts[488], Rect(320, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//creatures here will become skeletons } +void CTransformerWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int Y) : CIntObject(LCLICK | SHOW_POPUP | HOVER), ID(_ID), @@ -1083,9 +1102,10 @@ void CUniversityWindow::CItem::showAll(Canvas & to) CIntObject::showAll(to); } -CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market) +CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), hero(_hero), + onWindowClosed(onWindowClosed), market(_market) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -1130,6 +1150,14 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } +void CUniversityWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + void CUniversityWindow::makeDeal(int skill) { LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index a73669570..cc31ac83c 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -193,6 +193,8 @@ public: class CTavernWindow : public CStatusbarWindow { + std::function onWindowClosed; + public: class HeroPortrait : public CIntObject { @@ -233,9 +235,10 @@ public: std::shared_ptr rumor; - CTavernWindow(const CGObjectInstance * TavernObj); + CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); ~CTavernWindow(); + void close(); void recruitb(); void thievesguildb(); void show(Canvas & to) override; @@ -349,12 +352,15 @@ class CTransformerWindow : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr all; std::shared_ptr convert; std::shared_ptr cancel; + + std::function onWindowClosed; public: void makeDeal(); void addAll(); + void close(); void updateGarrisons() override; - CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero); + CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); }; class CUniversityWindow : public CStatusbarWindow @@ -390,10 +396,13 @@ class CUniversityWindow : public CStatusbarWindow std::shared_ptr title; std::shared_ptr clerkSpeech; + std::function onWindowClosed; + public: - CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market); + CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); void makeDeal(int skill); + void close(); }; /// Confirmation window for University diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index cdbb40209..03e96dbeb 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -25,6 +25,7 @@ struct ArtifactLocation; class CCreatureSet; class CStackBasicDescriptor; class CGCreature; +enum class EOpenWindowMode : uint8_t; namespace spells { @@ -96,7 +97,7 @@ public: virtual void showBlockingDialog(BlockingDialog *iw) =0; virtual void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) =0; //cb will be called when player closes garrison window virtual void showTeleportDialog(TeleportDialog *iw) =0; - virtual void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) =0; + virtual void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) = 0; virtual void giveResource(PlayerColor player, GameResID which, int val)=0; virtual void giveResources(PlayerColor player, TResources resources)=0; diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 065bd7f2a..fa72fc247 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -110,11 +110,11 @@ public: virtual void showPuzzleMap(){}; virtual void viewWorldMap(){}; - virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor){}; - virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor){}; + virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; + virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor){}; - virtual void showTavernWindow(const CGObjectInstance *townOrTavern){}; virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; + virtual void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) {}; virtual void showQuestLog(){}; virtual void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID){}; //called when a hero casts a spell virtual void tileHidden(const std::unordered_set &pos){}; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index f26733bb0..d34a0f6c2 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -786,19 +786,20 @@ struct DLL_LINKAGE GiveHero : public CPackForClient } }; -struct DLL_LINKAGE OpenWindow : public CPackForClient +struct DLL_LINKAGE OpenWindow : public Query { EOpenWindowMode window; - si32 id1 = -1; - si32 id2 = -1; + ObjectInstanceID object; + ObjectInstanceID visitor; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { + h & queryID; h & window; - h & id1; - h & id2; + h & object; + h & visitor; } }; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 8f4b2d982..becd3c336 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -393,13 +393,8 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const cb->sendAndApply(&sac); } - OpenWindow ow; - ow.id1 = id.getNum(); - ow.id2 = h->id.getNum(); - ow.window = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) - ? EOpenWindowMode::RECRUITMENT_FIRST - : EOpenWindowMode::RECRUITMENT_ALL; - cb->sendAndApply(&ow); + auto windowMode = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) ? EOpenWindowMode::RECRUITMENT_FIRST : EOpenWindowMode::RECRUITMENT_ALL; + cb->showObjectWindow(this, windowMode, h, false); } } diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index 747c925f4..595c130ba 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -30,7 +30,7 @@ void CGMarket::initObj(CRandomGenerator & rand) void CGMarket::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::MARKET_WINDOW, id.getNum(), h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::MARKET_WINDOW, h, true); } int CGMarket::getMarketEfficiency() const @@ -124,7 +124,7 @@ std::vector CGUniversity::availableItemsIds(EMarketMode mode) const void CGUniversity::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::UNIVERSITY_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::UNIVERSITY_WINDOW, h, true); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 6b5a0b274..1b2f0f92a 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -283,7 +283,7 @@ void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const break; case Obj::TAVERN: { - openWindow(EOpenWindowMode::TAVERN_WINDOW,h->id.getNum(),id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::TAVERN_WINDOW, h, true); } break; } diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index b76106232..867c02456 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -22,16 +22,6 @@ VCMI_LIB_NAMESPACE_BEGIN IGameCallback * IObjectInterface::cb = nullptr; -///helpers -void IObjectInterface::openWindow(const EOpenWindowMode type, const int id1, const int id2) -{ - OpenWindow ow; - ow.window = type; - ow.id1 = id1; - ow.id2 = id2; - IObjectInterface::cb->sendAndApply(&ow); -} - void IObjectInterface::showInfoDialog(const ui32 txtID, const ui16 soundID, EInfoWindowMode mode) const { InfoWindow iw; diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index b27a66f00..b0992a868 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -52,9 +52,6 @@ public: //unified helper to show info dialog for object owner virtual void showInfoDialog(const ui32 txtID, const ui16 soundID = 0, EInfoWindowMode mode = EInfoWindowMode::AUTO) const; - //unified helper to show a specific window - static void openWindow(const EOpenWindowMode type, const int id1, const int id2 = -1); - //unified interface, AI helpers virtual bool wasVisited (PlayerColor player) const; virtual bool wasVisited (const CGHeroInstance * h) const; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 818521339..771f84186 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1399,7 +1399,7 @@ void CGShipyard::onHeroVisit( const CGHeroInstance * h ) const } else { - openWindow(EOpenWindowMode::SHIPYARD_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::SHIPYARD_WINDOW, h, false); } } @@ -1488,7 +1488,7 @@ void CCartographer::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answ void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const { - cb->showThievesGuildWindow(h->tempOwner, id); + cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); } void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const @@ -1507,8 +1507,7 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const // increment general visited obelisks counter cb->setObjProperty(id, CGObelisk::OBJPROP_INC, team.getNum()); - - openWindow(EOpenWindowMode::PUZZLE_MAP, h->tempOwner.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::PUZZLE_MAP, h, false); // mark that particular obelisk as visited for all players in the team for(const auto & color : ts->players) @@ -1619,7 +1618,7 @@ void CGLighthouse::serializeJsonOptions(JsonSerializeFormat& handler) void HillFort::onHeroVisit(const CGHeroInstance * h) const { - openWindow(EOpenWindowMode::HILL_FORT_WINDOW,id.getNum(),h->id.getNum()); + cb->showObjectWindow(this, EOpenWindowMode::HILL_FORT_WINDOW, h, false); } void HillFort::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 6238d9057..273ea0514 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3364,13 +3364,20 @@ void CGameHandler::showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID h sendAndApply(&gd); } -void CGameHandler::showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) +void CGameHandler::showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) { - OpenWindow ow; - ow.window = EOpenWindowMode::THIEVES_GUILD; - ow.id1 = player.getNum(); - ow.id2 = requestingObjId.getNum(); - sendAndApply(&ow); + OpenWindow pack; + pack.window = window; + pack.object = object->id; + pack.visitor = visitor->id; + + if (addQuery) + { + auto windowQuery = std::make_shared(this, visitor, window); + pack.queryID = windowQuery->queryID; + queries->addQuery(windowQuery); + } + sendAndApply(&pack); } bool CGameHandler::isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2) diff --git a/server/CGameHandler.h b/server/CGameHandler.h index e17da3d3c..2784aa6a7 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -109,7 +109,7 @@ public: void showBlockingDialog(BlockingDialog *iw) override; void showTeleportDialog(TeleportDialog *iw) override; void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override; - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override; + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override; void giveResource(PlayerColor player, GameResID which, int val) override; void giveResources(PlayerColor player, TResources resources) override; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index c19d510fd..21b27910e 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -157,6 +157,39 @@ CBlockingDialogQuery::CBlockingDialogQuery(CGameHandler * owner, const BlockingD addPlayer(bd.player); } +OpenWindowQuery::OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode): + CDialogQuery(owner), + mode(mode) +{ + addPlayer(hero->getOwner()); +} + +bool OpenWindowQuery::blocksPack(const CPack *pack) const +{ + if (mode == EOpenWindowMode::TAVERN_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + if (mode == EOpenWindowMode::MARKET_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + if(dynamic_ptr_cast(pack)) + return false; + + if(dynamic_ptr_cast(pack)) + return false; + + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + + return CDialogQuery::blocksPack(pack); +} + void CTeleportDialogQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { // do not change to dynamic_ptr_cast - SIGSEGV! diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index e2b21cae0..5b7b5420a 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -61,7 +61,6 @@ public: virtual void onRemoval(PlayerColor color) override; }; - class CGarrisonDialogQuery : public CDialogQuery //used also for hero exchange dialogs { public: @@ -83,6 +82,15 @@ public: virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const override; }; +class OpenWindowQuery : public CDialogQuery +{ + EOpenWindowMode mode; +public: + OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode); + + virtual bool blocksPack(const CPack *pack) const override; +}; + class CTeleportDialogQuery : public CDialogQuery { public: From 3cb489e9bd2cabbcd853f46d33e29df2c790f9d4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 28 Sep 2023 12:16:42 +0300 Subject: [PATCH 0720/1248] Fix University and Altar --- server/queries/MapQueries.cpp | 11 +++++++++++ server/queries/MapQueries.h | 3 ++- test/mock/mock_IGameCallback.h | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 21b27910e..40752ef0e 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -164,6 +164,11 @@ OpenWindowQuery::OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *her addPlayer(hero->getOwner()); } +void OpenWindowQuery::onExposure(QueryPtr topQuery) +{ + //do nothing - wait for reply +} + bool OpenWindowQuery::blocksPack(const CPack *pack) const { if (mode == EOpenWindowMode::TAVERN_WINDOW) @@ -172,6 +177,12 @@ bool OpenWindowQuery::blocksPack(const CPack *pack) const return false; } + if (mode == EOpenWindowMode::UNIVERSITY_WINDOW) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + if (mode == EOpenWindowMode::MARKET_WINDOW) { if(dynamic_ptr_cast(pack) != nullptr) diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index 5b7b5420a..dfda9061d 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -88,7 +88,8 @@ class OpenWindowQuery : public CDialogQuery public: OpenWindowQuery(CGameHandler * owner, const CGHeroInstance *hero, EOpenWindowMode mode); - virtual bool blocksPack(const CPack *pack) const override; + bool blocksPack(const CPack *pack) const override; + void onExposure(QueryPtr topQuery) override; }; class CTeleportDialogQuery : public CDialogQuery diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 9a478c4f4..42dc8d3cc 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -48,7 +48,7 @@ public: void showBlockingDialog(BlockingDialog *iw) override {} void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {} //cb will be called when player closes garrison window void showTeleportDialog(TeleportDialog *iw) override {} - void showThievesGuildWindow(PlayerColor player, ObjectInstanceID requestingObjId) override {} + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; void giveResource(PlayerColor player, GameResID which, int val) override {} void giveResources(PlayerColor player, TResources resources) override {} From 39a92cdde339afbf1537c020fab83e27209e7f2a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 17:24:19 +0300 Subject: [PATCH 0721/1248] Add query for dwellings dialog --- AI/Nullkiller/AIGateway.cpp | 5 ++++- AI/Nullkiller/AIGateway.h | 2 +- AI/VCAI/VCAI.cpp | 5 ++++- AI/VCAI/VCAI.h | 2 +- client/CPlayerInterface.cpp | 10 +++++++--- client/CPlayerInterface.h | 2 +- client/NetPacksClient.cpp | 3 +-- client/windows/CCastleInterface.cpp | 4 ++-- client/windows/GUIClasses.cpp | 10 +++++++++- client/windows/GUIClasses.h | 4 +++- lib/IGameEventsReceiver.h | 2 +- lib/mapObjects/CGDwelling.cpp | 2 +- server/queries/MapQueries.cpp | 10 ++++++++++ 13 files changed, 45 insertions(+), 16 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 6eeef4e08..08ed86f6e 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -324,10 +324,13 @@ void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkil NET_EVENT_HANDLER; } -void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) +void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "level '%i'", level); NET_EVENT_HANDLER; + + status.addQuery(queryID, "RecruitmentDialog"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void AIGateway::heroMovePointsChanged(const CGHeroInstance * hero) diff --git a/AI/Nullkiller/AIGateway.h b/AI/Nullkiller/AIGateway.h index 424739082..63d7b1a44 100644 --- a/AI/Nullkiller/AIGateway.h +++ b/AI/Nullkiller/AIGateway.h @@ -145,7 +145,7 @@ public: void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; - void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void newObject(const CGObjectInstance * obj) override; diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 2098cfa70..bb6e0a1c9 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -360,10 +360,13 @@ void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill whi NET_EVENT_HANDLER; } -void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) +void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) { LOG_TRACE_PARAMS(logAi, "level '%i'", level); NET_EVENT_HANDLER; + + status.addQuery(queryID, "RecruitmentDialog"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); } void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index f31a22e0d..f1935eb3b 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -178,7 +178,7 @@ public: void tileRevealed(const std::unordered_set & pos) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; - void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; void heroMovePointsChanged(const CGHeroInstance * hero) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void newObject(const CGObjectInstance * obj) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index d91621c32..d5fb16329 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1318,15 +1318,19 @@ void CPlayerInterface::initializeHeroTownList() adventureInt->onHeroChanged(nullptr); } -void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) +void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; waitWhileDialog(); auto recruitCb = [=](CreatureID id, int count) { - LOCPLINT->cb->recruitCreatures(dwelling, dst, id, count, -1); + cb->recruitCreatures(dwelling, dst, id, count, -1); }; - GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb); + auto closeCb = [=]() + { + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb, closeCb); } void CPlayerInterface::waitWhileDialog() diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 175a109b5..62f38b026 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -117,7 +117,7 @@ protected: // Call-ins from server, should not be called directly, but only via void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; void receivedResource() override; void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; - void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override; + void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override; void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index c0282a450..497cce696 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -936,10 +936,9 @@ void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) case EOpenWindowMode::RECRUITMENT_FIRST: case EOpenWindowMode::RECRUITMENT_ALL: { - assert(pack.queryID == QueryID::NONE); const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.visitor))); - callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1); + callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID); } break; case EOpenWindowMode::SHIPYARD_WINDOW: diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 5b54b8fe5..39a58cf8d 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -864,7 +864,7 @@ void CCastleBuildings::enterDwelling(int level) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, -87); + GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, -87); } void CCastleBuildings::enterToTheQuickRecruitmentWindow() @@ -1073,7 +1073,7 @@ void CCreaInfo::clickPressed(const Point & cursorPosition) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, offset); + GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, offset); } std::string CCreaInfo::genGrowthText() diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index f21aef79d..4a9d9ca9f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -135,6 +135,13 @@ void CRecruitmentWindow::select(std::shared_ptr card) } } +void CRecruitmentWindow::close() +{ + if (onClose) + onClose(); + CStatusbarWindow::close(); +} + void CRecruitmentWindow::buy() { CreatureID crid = selected->creature->getId(); @@ -192,9 +199,10 @@ void CRecruitmentWindow::showAll(Canvas & to) to.drawBorder(Rect(289, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); } -CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, int y_offset): +CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset): CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPRCRT")), onRecruit(Recruit), + onClose(onClose), level(Level), dst(Dst), selected(nullptr), diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index cc31ac83c..6df706715 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -61,6 +61,7 @@ class CRecruitmentWindow : public CStatusbarWindow }; std::function onRecruit; //void (int ID, int amount) <-- call to recruit creatures + std::function onClose; int level; const CArmedInstance * dst; @@ -87,8 +88,9 @@ class CRecruitmentWindow : public CStatusbarWindow void showAll(Canvas & to) override; public: const CGDwelling * const dwelling; - CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, int y_offset = 0); + CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset = 0); void availableCreaturesChanged(); + void close(); }; /// Split window where creatures can be split up into two single unit stacks diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index fa72fc247..5c4acd5bd 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -105,7 +105,7 @@ public: virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){}; virtual void receivedResource(){}; virtual void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID){}; - virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level){} + virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID){} virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can buid, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water virtual void showPuzzleMap(){}; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index becd3c336..550be13fe 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -394,7 +394,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const } auto windowMode = (ID == Obj::CREATURE_GENERATOR1 || ID == Obj::REFUGEE_CAMP) ? EOpenWindowMode::RECRUITMENT_FIRST : EOpenWindowMode::RECRUITMENT_ALL; - cb->showObjectWindow(this, windowMode, h, false); + cb->showObjectWindow(this, windowMode, h, true); } } diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 40752ef0e..78d5809dc 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -171,6 +171,16 @@ void OpenWindowQuery::onExposure(QueryPtr topQuery) bool OpenWindowQuery::blocksPack(const CPack *pack) const { + if (mode == EOpenWindowMode::RECRUITMENT_FIRST || mode == EOpenWindowMode::RECRUITMENT_ALL) + { + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + // If hero has no free slots, he might get some stacks merged automatically + if(dynamic_ptr_cast(pack) != nullptr) + return false; + } + if (mode == EOpenWindowMode::TAVERN_WINDOW) { if(dynamic_ptr_cast(pack) != nullptr) From 46e4d862a51ea6cfe858c0f7d18729ffb6bbe19b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 17:38:38 +0300 Subject: [PATCH 0722/1248] Fix AI recruitment --- AI/Nullkiller/AIGateway.cpp | 9 +++++---- AI/VCAI/VCAI.cpp | 10 +++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 08ed86f6e..342592af9 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -330,7 +330,11 @@ void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedI NET_EVENT_HANDLER; status.addQuery(queryID, "RecruitmentDialog"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); + + requestActionASAP([=](){ + recruitCreatures(dwelling, dst); + answerQuery(queryID, 0); + }); } void AIGateway::heroMovePointsChanged(const CGHeroInstance * hero) @@ -845,9 +849,6 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); switch(obj->ID) { - case Obj::CREATURE_GENERATOR1: - recruitCreatures(dynamic_cast(obj), h.get()); - break; case Obj::TOWN: if(h->visitedTown) //we are inside, not just attacking { diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index bb6e0a1c9..2c9b4d897 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -366,7 +366,11 @@ void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstan NET_EVENT_HANDLER; status.addQuery(queryID, "RecruitmentDialog"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); + requestActionASAP([=](){ + recruitCreatures(dwelling, dst); + checkHeroArmy(dynamic_cast(dst)); + answerQuery(queryID, 0); + }); } void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) @@ -1056,10 +1060,6 @@ void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); switch(obj->ID) { - case Obj::CREATURE_GENERATOR1: - recruitCreatures(dynamic_cast(obj), h.get()); - checkHeroArmy(h); - break; case Obj::TOWN: moveCreaturesToHero(dynamic_cast(obj)); if(h->visitedTown) //we are inside, not just attacking From 037efdf5fcf650a222ed9ba6bdbd504a32219561 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 28 Sep 2023 19:43:04 +0300 Subject: [PATCH 0723/1248] Improvements to type safety of Identifier class - Constructor of Identifier from integer is now explicit - Lobby hero/town selection now uses Identifiers instead of int's - Removed serialization workaround for hero portraits - Added dummy objects for custom heroes portraits for ID resolver to use - HeroInstance now stores portrait ID only in case of custom portrait - Fixed loading of campaign heroes portraits on RoE maps --- Mods/vcmi/Sprites/PortraitsLarge.json | 9 +- Mods/vcmi/Sprites/PortraitsSmall.json | 15 +- client/ClientCommandManager.cpp | 2 +- client/NetPacksClient.cpp | 2 +- client/adventureMap/CList.cpp | 2 +- client/battle/BattleInterfaceClasses.cpp | 6 +- client/gui/InterfaceObjectConfigurable.cpp | 2 +- client/lobby/CBonusSelection.cpp | 2 +- client/lobby/CSelectionBase.cpp | 9 +- client/lobby/CSelectionBase.h | 2 +- client/lobby/OptionsTab.cpp | 278 ++++++++++----------- client/lobby/OptionsTab.h | 11 +- client/widgets/CComponent.cpp | 3 +- client/widgets/MiscWidgets.cpp | 8 +- client/windows/CCastleInterface.cpp | 2 +- client/windows/CHeroWindow.cpp | 4 +- client/windows/CKingdomInterface.cpp | 2 +- client/windows/GUIClasses.cpp | 10 +- config/gameConfig.json | 26 +- config/heroes/portraits.json | 206 +++++++++++++++ lib/CHeroHandler.cpp | 5 +- lib/StartInfo.cpp | 29 ++- lib/StartInfo.h | 23 +- lib/campaign/CampaignHandler.cpp | 4 +- lib/constants/EntityIdentifiers.cpp | 94 +++---- lib/constants/EntityIdentifiers.h | 42 +++- lib/constants/NumericConstants.h | 2 - lib/gameState/CGameState.cpp | 15 +- lib/gameState/CGameStateCampaign.cpp | 5 +- lib/gameState/InfoAboutArmy.cpp | 15 +- lib/gameState/InfoAboutArmy.h | 3 +- lib/mapObjects/CGHeroInstance.cpp | 53 ++-- lib/mapObjects/CGHeroInstance.h | 10 +- lib/mapObjects/CQuest.cpp | 3 +- lib/mapObjects/CQuest.h | 2 +- lib/mapping/CMapHeader.cpp | 14 +- lib/mapping/CMapHeader.h | 10 +- lib/mapping/MapFeaturesH3M.cpp | 4 +- lib/mapping/MapFormatH3M.cpp | 16 +- lib/mapping/MapFormatJson.cpp | 10 +- lib/mapping/MapIdentifiersH3M.cpp | 12 +- lib/mapping/MapIdentifiersH3M.h | 4 +- lib/mapping/MapReaderH3M.cpp | 8 +- lib/mapping/MapReaderH3M.h | 2 +- mapeditor/inspector/inspector.cpp | 4 +- mapeditor/mapcontroller.cpp | 3 - mapeditor/playerparams.cpp | 2 +- server/CVCMIServer.cpp | 128 +++++----- server/CVCMIServer.h | 12 +- server/NetPacksLobbyServer.cpp | 6 +- server/battles/BattleResultProcessor.cpp | 2 +- server/processors/HeroPoolProcessor.cpp | 10 +- test/game/CGameStateTest.cpp | 4 +- 53 files changed, 693 insertions(+), 464 deletions(-) create mode 100644 config/heroes/portraits.json diff --git a/Mods/vcmi/Sprites/PortraitsLarge.json b/Mods/vcmi/Sprites/PortraitsLarge.json index 9a74e7dc2..44dcc7ad7 100644 --- a/Mods/vcmi/Sprites/PortraitsLarge.json +++ b/Mods/vcmi/Sprites/PortraitsLarge.json @@ -156,13 +156,6 @@ { "frame" : 152, "file" : "HPL009SH.bmp"}, { "frame" : 153, "file" : "HPL008SH.bmp"}, { "frame" : 154, "file" : "HPL001SH.bmp"}, - { "frame" : 155, "file" : "HPL131DM.bmp"}, - { "frame" : 156, "file" : "HPL129MK.bmp"}, - { "frame" : 157, "file" : "HPL002SH.bmp"}, - { "frame" : 158, "file" : "HPL132Wl.bmp"}, - { "frame" : 159, "file" : "HPL133Nc.bmp"}, - { "frame" : 160, "file" : "HPL134Nc.bmp"}, - { "frame" : 161, "file" : "HPL135Wi.bmp"}, - { "frame" : 162, "file" : "HPL136Wi.bmp"} + { "frame" : 155, "file" : "HPL131DM.bmp"} ] } diff --git a/Mods/vcmi/Sprites/PortraitsSmall.json b/Mods/vcmi/Sprites/PortraitsSmall.json index 8064ecf25..701da953a 100644 --- a/Mods/vcmi/Sprites/PortraitsSmall.json +++ b/Mods/vcmi/Sprites/PortraitsSmall.json @@ -155,16 +155,9 @@ { "frame" : 151, "file" : "HPS007SH.bmp"}, { "frame" : 152, "file" : "HPS009SH.bmp"}, { "frame" : 153, "file" : "HPS008SH.bmp"}, - { "frame" : 154, "file" : "HPS001SH.bmp"}, - { "frame" : 155, "file" : "HPS131DM.bmp"}, - { "frame" : 156, "file" : "HPS129MK.bmp"}, - { "frame" : 157, "file" : "HPS002SH.bmp"}, - { "frame" : 158, "file" : "HPS132Wl.bmp"}, - { "frame" : 159, "file" : "HPS133Nc.bmp"}, - { "frame" : 160, "file" : "HPS134Nc.bmp"}, - { "frame" : 161, "file" : "HPS135Wi.bmp"}, - { "frame" : 162, "file" : "HPS136Wi.bmp"}, - { "frame" : 163, "file" : "HPSRAND1.bmp"}, //random hero - { "frame" : 164, "file" : "HPSRAND6.bmp"} //no hero + { "frame" : 154, "file" : "HPS001SH.bmp"}, + { "frame" : 155, "file" : "HPS131DM.bmp"}, + { "frame" : 156, "file" : "HPSRAND1.bmp"}, // random hero + { "frame" : 157, "file" : "HPSRAND6.bmp"} // no hero ] } diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index f08c9a755..1c36d983c 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -466,7 +466,7 @@ void ClientCommandManager::giveTurn(const PlayerColor &colorIdentifier) { PlayerStartsTurn yt; yt.player = colorIdentifier; - yt.queryID = -1; + yt.queryID = QueryID::NONE; ApplyClientNetPackVisitor visitor(*CSH->client, *CSH->client->gameState()); yt.visit(visitor); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index b26c07dbc..7c408f183 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -403,7 +403,7 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface if (cl.gameState()->isPlayerMakingTurn(player)) { callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); - callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, -1); + callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); } } }; diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index cd84d4348..3a9e18280 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -220,7 +220,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); - portrait = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->portrait, 0, movement->pos.w + 1); + portrait = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex(), 0, movement->pos.w + 1); mana = std::make_shared(AnimationPath::builtin("IMANA"), 0, 0, movement->pos.w + portrait->pos.w + 2, 1); pos.w = mana->pos.w + mana->pos.x - pos.x; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 79d306871..a9bc54bfe 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -407,7 +407,7 @@ void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) auto currentSpellPoints = hero.details->mana; auto maxSpellPoints = hero.details->manaLimit; - icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 10, 6)); + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6)); //primary stats labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); @@ -506,9 +506,9 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i); const int xs[] = {21, 392}; - if(heroInfo.portrait >= 0) //attacking hero + if(heroInfo.portraitSource.isValid()) //attacking hero { - icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.portrait, 0, xs[i], 38)); + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38)); sideNames[i] = heroInfo.name; } else diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 24d20d3fc..a840c9acb 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -240,7 +240,7 @@ PlayerColor InterfaceObjectConfigurable::readPlayerColor(const JsonNode & config { logGlobal->debug("Reading PlayerColor"); if(!config.isNull() && config.isString()) - return PlayerColor::decode(config.String()); + return PlayerColor(PlayerColor::decode(config.String())); logGlobal->debug("Unknown PlayerColor attribute"); return PlayerColor::CANNOT_DETERMINE; diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 4237cdb90..2566bf075 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -274,7 +274,7 @@ void CBonusSelection::createBonusesIcons() auto superhero = getCampaign()->strongestHero(static_cast(bonDescs[i].info2), PlayerColor(bonDescs[i].info1)); if(!superhero) logGlobal->warn("No superhero! How could it be transferred?"); - picNumber = superhero ? superhero->portrait : 0; + picNumber = superhero ? superhero->getIconIndex() : 0; desc.appendLocalString(EMetaText::GENERAL_TXT, 719); desc.replaceRawString(getCampaign()->scenario(static_cast(bonDescs[i].info2)).scenarioName.toString()); break; diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index dbc8793f4..e95ddf8de 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -68,9 +68,9 @@ int ISelectionScreenInfo::getCurrentDifficulty() return getStartInfo()->difficulty; } -PlayerInfo ISelectionScreenInfo::getPlayerInfo(int color) +PlayerInfo ISelectionScreenInfo::getPlayerInfo(PlayerColor color) { - return getMapInfo()->mapHeader->players[color]; + return getMapInfo()->mapHeader->players[color.getNum()]; } CSelectionBase::CSelectionBase(ESelectionScreen type) @@ -398,8 +398,9 @@ CFlagBox::CFlagBoxTooltipBox::CFlagBoxTooltipBox(std::shared_ptr ico labelTeamAlignment = std::make_shared(128, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[657]); labelGroupTeams = std::make_shared(FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - std::vector> teams(PlayerColor::PLAYER_LIMIT_I); - for(ui8 j = 0; j < PlayerColor::PLAYER_LIMIT_I; j++) + std::vector> teams(PlayerColor::PLAYER_LIMIT_I); + + for(PlayerColor j(0); j < PlayerColor::PLAYER_LIMIT; j++) { if(SEL->getPlayerInfo(j).canHumanPlay || SEL->getPlayerInfo(j).canComputerPlay) { diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 5436a959e..8de81ab50 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -44,7 +44,7 @@ public: virtual const StartInfo * getStartInfo() = 0; virtual int getCurrentDifficulty(); - virtual PlayerInfo getPlayerInfo(int color); + virtual PlayerInfo getPlayerInfo(PlayerColor color); }; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 16541525c..b7af0368c 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -291,48 +291,49 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) WOOD = 0, ORE = 0, MITHRIL = 10, // resources unavailable in bonuses file TOWN_RANDOM = 38, TOWN_NONE = 39, // Special frames in ITPA - HERO_RANDOM = 163, HERO_NONE = 164 // Special frames in PortraitsSmall + HERO_RANDOM = 156, HERO_NONE = 157 // Special frames in PortraitsSmall }; - auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); + auto factionIndex = playerSettings.getCastleValidated(); - switch(type) + switch(selectionType) { case TOWN: - switch(playerSettings.castle) - { - case PlayerSettings::NONE: + { + if (playerSettings.castle == FactionID::NONE) return TOWN_NONE; - case PlayerSettings::RANDOM: + + if (playerSettings.castle == FactionID::RANDOM) return TOWN_RANDOM; - default: - return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + (big ? 0 : 2); - } + + return (*CGI->townh)[factionIndex]->town->clientInfo.icons[true][false] + (big ? 0 : 2); + } + case HERO: - switch(playerSettings.hero) - { - case PlayerSettings::NONE: + { + if (playerSettings.hero == HeroTypeID::NONE) return HERO_NONE; - case PlayerSettings::RANDOM: + + if (playerSettings.hero == HeroTypeID::RANDOM) return HERO_RANDOM; - default: - { - if(playerSettings.heroPortrait != HeroTypeID::NONE) - return playerSettings.heroPortrait; - auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); - return (*CGI->heroh)[index]->imageIndex; - } - } + + if(playerSettings.heroPortrait != HeroTypeID::NONE) + return playerSettings.heroPortrait; + + auto index = playerSettings.getHeroValidated(); + return (*CGI->heroh)[index]->imageIndex; + } + case BONUS: { switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return RANDOM; - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return ARTIFACT; - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return GOLD; - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -364,7 +365,7 @@ size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) AnimationPath OptionsTab::CPlayerSettingsHelper::getImageName(bool big) { - switch(type) + switch(selectionType) { case OptionsTab::TOWN: return AnimationPath::builtin(big ? "ITPt": "ITPA"); @@ -378,74 +379,63 @@ AnimationPath OptionsTab::CPlayerSettingsHelper::getImageName(bool big) std::string OptionsTab::CPlayerSettingsHelper::getName() { - switch(type) + switch(selectionType) { - case TOWN: - { - switch(playerSettings.castle) + case TOWN: { - case PlayerSettings::NONE: - return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: - { - auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); + if (playerSettings.castle == FactionID::NONE) + return CGI->generaltexth->allTexts[523]; + + if (playerSettings.castle == FactionID::RANDOM) + return CGI->generaltexth->allTexts[522]; + + auto factionIndex = playerSettings.getCastleValidated(); return (*CGI->townh)[factionIndex]->getNameTranslated(); } - } - } - case HERO: - { - switch(playerSettings.hero) - { - case PlayerSettings::NONE: - return CGI->generaltexth->allTexts[523]; - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: + case HERO: { + if (playerSettings.hero == HeroTypeID::NONE) + return CGI->generaltexth->allTexts[523]; + + if (playerSettings.hero == HeroTypeID::RANDOM) + return CGI->generaltexth->allTexts[522]; + if(!playerSettings.heroNameTextId.empty()) return playerSettings.heroNameTextId; - auto index = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); + auto index = playerSettings.getHeroValidated(); return (*CGI->heroh)[index]->getNameTranslated(); } - } - } - case BONUS: - { - switch(playerSettings.bonus) + case BONUS: { - case PlayerSettings::RANDOM: - return CGI->generaltexth->allTexts[522]; - default: - return CGI->generaltexth->arraytxt[214 + playerSettings.bonus]; + if (playerSettings.bonus == PlayerStartingBonus::RANDOM) + return CGI->generaltexth->allTexts[522]; + + return CGI->generaltexth->arraytxt[214 + static_cast(playerSettings.bonus)]; } } - } return ""; } std::string OptionsTab::CPlayerSettingsHelper::getTitle() { - switch(type) + switch(selectionType) { case OptionsTab::TOWN: - return (playerSettings.castle.getNum() < 0) ? CGI->generaltexth->allTexts[103] : CGI->generaltexth->allTexts[80]; + return playerSettings.castle.isValid() ? CGI->generaltexth->allTexts[80] : CGI->generaltexth->allTexts[103]; case OptionsTab::HERO: - return (playerSettings.hero.getNum() < 0) ? CGI->generaltexth->allTexts[101] : CGI->generaltexth->allTexts[77]; + return playerSettings.hero.isValid() ? CGI->generaltexth->allTexts[77] : CGI->generaltexth->allTexts[101]; case OptionsTab::BONUS: { switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return CGI->generaltexth->allTexts[86]; //{Random Bonus} - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return CGI->generaltexth->allTexts[83]; //{Artifact Bonus} - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[84]; //{Gold Bonus} - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: return CGI->generaltexth->allTexts[85]; //{Resource Bonus} } } @@ -454,16 +444,16 @@ std::string OptionsTab::CPlayerSettingsHelper::getTitle() } std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { - auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); - auto heroIndex = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); + auto factionIndex = playerSettings.getCastleValidated(); + auto heroIndex = playerSettings.getHeroValidated(); - switch(type) + switch(selectionType) { case TOWN: return getName(); case HERO: { - if(playerSettings.hero.getNum() >= 0) + if(playerSettings.hero.isValid()) return getName() + " - " + (*CGI->heroh)[heroIndex]->heroClass->getNameTranslated(); return getName(); } @@ -472,9 +462,9 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() { switch(playerSettings.bonus) { - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[87]; //500-1000 - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -498,9 +488,9 @@ std::string OptionsTab::CPlayerSettingsHelper::getSubtitle() std::string OptionsTab::CPlayerSettingsHelper::getDescription() { - auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); + auto factionIndex = playerSettings.getCastleValidated(); - switch(type) + switch(selectionType) { case TOWN: return CGI->generaltexth->allTexts[104]; @@ -510,13 +500,13 @@ std::string OptionsTab::CPlayerSettingsHelper::getDescription() { switch(playerSettings.bonus) { - case PlayerSettings::RANDOM: + case PlayerStartingBonus::RANDOM: return CGI->generaltexth->allTexts[94]; //Gold, wood and ore, or an artifact is randomly chosen as your starting bonus - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: return CGI->generaltexth->allTexts[90]; //An artifact is randomly chosen and equipped to your starting hero - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: return CGI->generaltexth->allTexts[92]; //At the start of the game, 500-1000 gold is added to your Kingdom's resource pool - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { switch((*CGI->townh)[factionIndex]->town->primaryRes.toEnum()) { @@ -543,30 +533,19 @@ OptionsTab::CPlayerOptionTooltipBox::CPlayerOptionTooltipBox(CPlayerSettingsHelp { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - int value = PlayerSettings::NONE; - - switch(CPlayerSettingsHelper::type) + switch(selectionType) { - break; - case TOWN: - value = playerSettings.castle; - break; - case HERO: - value = playerSettings.hero; - break; - case BONUS: - value = playerSettings.bonus; + case TOWN: + genTownWindow(); + break; + case HERO: + genHeroWindow(); + break; + case BONUS: + genBonusWindow(); + break; } - if(value == PlayerSettings::RANDOM) - genBonusWindow(); - else if(CPlayerSettingsHelper::type == BONUS) - genBonusWindow(); - else if(CPlayerSettingsHelper::type == HERO) - genHeroWindow(); - else if(CPlayerSettingsHelper::type == TOWN) - genTownWindow(); - center(); } @@ -585,7 +564,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = playerSettings.castle.getNum() >= CGI->townh->size() ? 0 : playerSettings.castle.getNum(); + auto factionIndex = playerSettings.getCastleValidated(); std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; @@ -602,7 +581,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = playerSettings.hero.getNum() >= CGI->heroh->size() ? 0 : playerSettings.hero.getNum(); + auto heroIndex = playerSettings.getHeroValidated(); imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); @@ -630,7 +609,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) selectedFaction = initialFaction; selectedHero = initialHero; selectedBonus = initialBonus; - allowedFactions = SEL->getPlayerInfo(color.getNum()).allowedFactions; + allowedFactions = SEL->getPlayerInfo(color).allowedFactions; std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; for(int i = 0; i < allowedHeroesFlag.size(); i++) if(allowedHeroesFlag[i]) @@ -638,16 +617,19 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) for(auto & player : SEL->getStartInfo()->playerInfos) { - if(player.first != color && (int)player.second.hero > PlayerSettings::RANDOM) + if(player.first != color && (int)player.second.hero > HeroTypeID::RANDOM) unusableHeroes.insert(player.second.hero); } - allowedBonus.push_back(-1); // random - if(initialHero.getNum() >= -1 || SEL->getPlayerInfo(color.getNum()).heroesNames.size() > 0) - allowedBonus.push_back(0); // artifact - allowedBonus.push_back(1); // gold - if(initialFaction.getNum() >= 0) - allowedBonus.push_back(2); // resource + allowedBonus.push_back(PlayerStartingBonus::RANDOM); + + if(initialHero != HeroTypeID::NONE|| SEL->getPlayerInfo(color).heroesNames.size() > 0) + allowedBonus.push_back(PlayerStartingBonus::ARTIFACT); + + allowedBonus.push_back(PlayerStartingBonus::GOLD); + + if(initialFaction.isValid()) + allowedBonus.push_back(PlayerStartingBonus::RESOURCE); recreate(); } @@ -656,7 +638,7 @@ int OptionsTab::SelectionWindow::calcLines(FactionID faction) { double additionalItems = 1; // random - if(faction.getNum() < 0) + if(!faction.isValid()) return std::ceil(((double)allowedFactions.size() + additionalItems) / elementsPerLine); int count = 0; @@ -692,7 +674,7 @@ void OptionsTab::SelectionWindow::setSelection() CSH->setPlayerOption(LobbyChangePlayerOption::HERO_ID, selectedHero, color); if(selectedBonus != initialBonus) - CSH->setPlayerOption(LobbyChangePlayerOption::BONUS_ID, selectedBonus, color); + CSH->setPlayerOption(LobbyChangePlayerOption::BONUS_ID, static_cast(selectedBonus), color); } void OptionsTab::SelectionWindow::reopen() @@ -728,7 +710,7 @@ void OptionsTab::SelectionWindow::recreate() elementsPerLine = floor(sqrt(count)); } - amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : static_cast(PlayerSettings::RANDOM)); + amountLines = calcLines((type > SelType::TOWN) ? selectedFaction : FactionID::RANDOM); } int x = (elementsPerLine) * (ICON_BIG_WIDTH-1); @@ -777,11 +759,11 @@ void OptionsTab::SelectionWindow::genContentFactions() // random PlayerSettings set = PlayerSettings(); - set.castle = PlayerSettings::RANDOM; + set.castle = FactionID::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::TOWN); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); - drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedFaction.getNum() == PlayerSettings::RANDOM) + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedFaction == FactionID::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedFaction == FactionID::RANDOM) components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedFactions) @@ -809,11 +791,11 @@ void OptionsTab::SelectionWindow::genContentHeroes() // random PlayerSettings set = PlayerSettings(); - set.hero = PlayerSettings::RANDOM; + set.hero = HeroTypeID::RANDOM; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, 6, (ICON_SMALL_HEIGHT/2))); - drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero.getNum() == PlayerSettings::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); - if(selectedHero.getNum() == PlayerSettings::RANDOM) + drawOutlinedText(TEXT_POS_X, TEXT_POS_Y, (selectedHero == HeroTypeID::RANDOM) ? Colors::YELLOW : Colors::WHITE, helper.getName()); + if(selectedHero == HeroTypeID::RANDOM) components.push_back(std::make_shared(ImagePath::builtin("lobby/townBorderSmallActivated"), 6, (ICON_SMALL_HEIGHT/2))); for(auto & elem : allowedHeroes) @@ -856,7 +838,7 @@ void OptionsTab::SelectionWindow::genContentBonus() int x = i; int y = 0; - set.bonus = static_cast(elem); + set.bonus = elem; CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); components.push_back(std::make_shared(helper.getImageName(), helper.getImageIndex(), 0, x * (ICON_BIG_WIDTH-1) + 6, y * (ICON_BIG_HEIGHT-1) + (ICON_SMALL_HEIGHT/2))); drawOutlinedText(x * (ICON_BIG_WIDTH-1) + TEXT_POS_X, y * (ICON_BIG_HEIGHT-1) + TEXT_POS_Y, Colors::WHITE , helper.getName()); @@ -892,9 +874,9 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) } else { - set.castle = PlayerSettings::RANDOM; + set.castle = FactionID::RANDOM; } - if(set.castle.getNum() != PlayerSettings::NONE) + if(set.castle != FactionID::NONE) { if(!doApply) { @@ -916,18 +898,18 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) } else { - set.hero = PlayerSettings::RANDOM; + set.hero = HeroTypeID::RANDOM; } if(doApply && unusableHeroes.count(heroes[elem])) return; - if(set.hero.getNum() != PlayerSettings::NONE) + if(set.hero != HeroTypeID::NONE) { if(!doApply) { CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::HERO); - if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.getNum() > PlayerSettings::RANDOM && helper.playerSettings.heroNameTextId.empty()) + if(settings["general"]["enableUiEnhancements"].Bool() && helper.playerSettings.hero.isValid() && helper.playerSettings.heroNameTextId.empty()) GH.windows().createAndPushWindow(helper.playerSettings.hero); else GH.windows().createAndPushWindow(helper); @@ -940,17 +922,15 @@ void OptionsTab::SelectionWindow::setElement(int elem, bool doApply) { if(elem >= 4) return; - set.bonus = static_cast(allowedBonus[elem]); - if(set.bonus != PlayerSettings::NONE) + set.bonus = static_cast(allowedBonus[elem]); + + if(!doApply) { - if(!doApply) - { - CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); - GH.windows().createAndPushWindow(helper); - } - else - selectedBonus = set.bonus; + CPlayerSettingsHelper helper = CPlayerSettingsHelper(set, SelType::BONUS); + GH.windows().createAndPushWindow(helper); } + else + selectedBonus = set.bonus; } if(doApply) @@ -1008,12 +988,12 @@ void OptionsTab::SelectedBox::update() void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) { // cases when we do not need to display a message - if(playerSettings.castle.getNum() == PlayerSettings::NONE && CPlayerSettingsHelper::type == TOWN) + if(playerSettings.castle == FactionID::NONE && CPlayerSettingsHelper::selectionType == TOWN) return; - if(playerSettings.hero.getNum() == PlayerSettings::NONE && !SEL->getPlayerInfo(playerSettings.color.getNum()).hasCustomMainHero() && CPlayerSettingsHelper::type == HERO) + if(playerSettings.hero == HeroTypeID::NONE && !SEL->getPlayerInfo(playerSettings.color).hasCustomMainHero() && CPlayerSettingsHelper::selectionType == HERO) return; - if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::type == HERO && playerSettings.hero.getNum() > PlayerSettings::RANDOM && playerSettings.heroNameTextId.empty()) + if(settings["general"]["enableUiEnhancements"].Bool() && CPlayerSettingsHelper::selectionType == HERO && playerSettings.hero.isValid() && playerSettings.heroNameTextId.empty()) GH.windows().createAndPushWindow(playerSettings.hero); else GH.windows().createAndPushWindow(*this); @@ -1021,20 +1001,20 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { - PlayerInfo pi = SEL->getPlayerInfo(playerSettings.color.getNum()); + PlayerInfo pi = SEL->getPlayerInfo(playerSettings.color); const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(playerSettings.color); - if(type == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) + if(selectionType == SelType::TOWN && ((pi.allowedFactions.size() < 2 && !pi.isFactionRandom) || foreignPlayer)) return; - if(type == SelType::HERO && ((pi.defaultHero() != -1 || playerSettings.castle.getNum() < 0) || foreignPlayer)) + if(selectionType == SelType::HERO && ((pi.defaultHero() == HeroTypeID::NONE || !playerSettings.castle.isValid() || foreignPlayer))) return; - if(type == SelType::BONUS && foreignPlayer) + if(selectionType == SelType::BONUS && foreignPlayer) return; GH.input().hapticFeedback(); - GH.windows().createAndPushWindow(playerSettings.color, type); + GH.windows().createAndPushWindow(playerSettings.color, selectionType); } void OptionsTab::SelectedBox::scrollBy(int distance) @@ -1044,7 +1024,7 @@ void OptionsTab::SelectedBox::scrollBy(int distance) // so, currently, gesture will always move selection only by 1, and then wait for recreation from server info distance = std::clamp(distance, -1, 1); - switch(CPlayerSettingsHelper::type) + switch(CPlayerSettingsHelper::selectionType) { case TOWN: CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, playerSettings.color); @@ -1061,7 +1041,7 @@ void OptionsTab::SelectedBox::scrollBy(int distance) } OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parent) - : pi(std::make_unique(SEL->getPlayerInfo(S.color.getNum()))) + : pi(std::make_unique(SEL->getPlayerInfo(S.color))) , s(std::make_unique(S)) , parentTab(parent) { @@ -1069,7 +1049,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con defActions |= SHARE_POS; int serial = 0; - for(int g = 0; g < s->color.getNum(); ++g) + for(PlayerColor g = PlayerColor(0); g < s->color; ++g) { auto itred = SEL->getPlayerInfo(g); if(itred.canComputerPlay || itred.canHumanPlay) @@ -1080,7 +1060,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con pos.y += 128 + serial * 50; assert(CSH->mi && CSH->mi->mapHeader); - const PlayerInfo & p = SEL->getPlayerInfo(s->color.getNum()); + const PlayerInfo & p = SEL->getPlayerInfo(s->color); assert(p.canComputerPlay || p.canHumanPlay); //someone must be able to control this player if(p.canHumanPlay && p.canComputerPlay) whoCanPlay = HUMAN_OR_CPU; @@ -1100,7 +1080,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con "ADOPOPNL.bmp", "ADOPPPNL.bmp", "ADOPTPNL.bmp", "ADOPSPNL.bmp" }}; - background = std::make_shared(ImagePath::builtin(bgs[s->color.getNum()]), 0, 0); + background = std::make_shared(ImagePath::builtin(bgs[s->color]), 0, 0); labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); @@ -1116,7 +1096,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con hideUnavailableButtons(); - if(SEL->screenType != ESelectionScreen::scenarioInfo && SEL->getPlayerInfo(s->color.getNum()).canHumanPlay) + if(SEL->screenType != ESelectionScreen::scenarioInfo && SEL->getPlayerInfo(s->color).canHumanPlay) { flag = std::make_shared( Point(-43, 2), @@ -1159,7 +1139,7 @@ void OptionsTab::PlayerOptionsEntry::hideUnavailableButtons() buttonTownRight->enable(); } - if((pi->defaultHero() != -1 || s->castle.getNum() < 0) //fixed hero + if((pi->defaultHero() != HeroTypeID::RANDOM || !s->castle.isValid()) //fixed hero || foreignPlayer) //or not our player { buttonHeroLeft->disable(); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 34cd58dc5..0f8943d43 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct PlayerSettings; struct PlayerInfo; +enum class PlayerStartingBonus : int8_t; VCMI_LIB_NAMESPACE_END class CLabel; @@ -54,10 +55,10 @@ private: struct CPlayerSettingsHelper { const PlayerSettings & playerSettings; - const SelType type; + const SelType selectionType; CPlayerSettingsHelper(const PlayerSettings & playerSettings, SelType type) - : playerSettings(playerSettings), type(type) + : playerSettings(playerSettings), selectionType(type) {} /// visible image settings @@ -118,14 +119,14 @@ private: FactionID initialFaction; HeroTypeID initialHero; - int initialBonus; + PlayerStartingBonus initialBonus; FactionID selectedFaction; HeroTypeID selectedHero; - int selectedBonus; + PlayerStartingBonus selectedBonus; std::set allowedFactions; std::set allowedHeroes; - std::vector allowedBonus; + std::vector allowedBonus; void genContentGrid(int lines); void genContentFactions(); diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 4075ea084..626c37f52 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -30,6 +30,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CSkillHandler.h" @@ -153,7 +154,7 @@ size_t CComponent::getIndex() case morale: return val+3; case luck: return val+3; case building: return val; - case hero: return subtype; + case hero: return CGI->heroTypes()->getByIndex(subtype)->getIconIndex(); case flag: return subtype; } assert(0); diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index e3f11ebe2..7b22e5274 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -131,7 +131,7 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero) if(hero) { - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex()); clickFunctor = [hero]() -> void { LOCPLINT->openHeroWindow(hero); @@ -291,7 +291,7 @@ CArmyTooltip::CArmyTooltip(Point pos, const CArmedInstance * army): void CHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 3, 2); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 3, 2); if(hero.details) { @@ -329,7 +329,7 @@ CInteractableHeroTooltip::CInteractableHeroTooltip(Point pos, const CGHeroInstan void CInteractableHeroTooltip::init(const InfoAboutHero & hero) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.portrait, 0, 3, 2); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 3, 2); title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero.name); if(hero.details) @@ -607,4 +607,4 @@ SimpleLine::SimpleLine(Point pos1, Point pos2, ColorRGBA color) : void SimpleLine::showAll(Canvas & to) { to.drawLine(pos1 + pos.topLeft(), pos2 + pos.topLeft(), color, color); -} \ No newline at end of file +} diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 471a79c2b..d64446fad 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -450,7 +450,7 @@ void CHeroGSlot::set(const CGHeroInstance * newHero) if(newHero) { portrait->visible = true; - portrait->setFrame(newHero->portrait); + portrait->setFrame(newHero->getIconIndex()); } else if(!upg && owner->showEmpty) //up garrison { diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 5e80706e8..68eff233c 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -64,7 +64,7 @@ CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInsta OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); pos += pos_; - image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->portrait); + image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex()); pos.w = image->pos.w; pos.h = image->pos.h; } @@ -202,7 +202,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); portraitArea->text = curHero->getBiographyTranslated(); - portraitImage->setFrame(curHero->portrait); + portraitImage->setFrame(curHero->getIconIndex()); { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index a844b9ace..a1d1904c3 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -915,7 +915,7 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero) garr = std::make_shared(Point(6, 78), 4, Point(), hero, nullptr, true, true); - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait, 0, 5, 6); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 5, 6); heroArea = std::make_shared(5, 6, hero); name = std::make_shared(73, 7, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, hero->getNameTranslated()); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 8bf9052c1..36c8d30aa 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -401,7 +401,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); } - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->portrait, 0, 170, 66); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66); ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); //%s has gained a level. @@ -580,7 +580,7 @@ CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); - portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->portrait); + portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->getIconIndex()); } } @@ -1210,7 +1210,7 @@ CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); banner = std::make_shared(AnimationPath::builtin("CREST58"), up->getOwner().getNum(), 0, 28, 124); - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->portrait, 0, 29, 222); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->getIconIndex(), 0, 29, 222); } void CGarrisonWindow::updateGarrisons() @@ -1501,9 +1501,9 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): for(auto & iter : tgi.colorToBestHero) { banners.push_back(std::make_shared(ImagePath::builtin(colorToBox[iter.first.getNum()]), 253 + 66 * counter, 334)); - if(iter.second.portrait >= 0) + if(iter.second.portraitSource.isValid()) { - bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.portrait, 0, 260 + 66 * counter, 360)); + bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.getIconIndex(), 0, 260 + 66 * counter, 360)); //TODO: r-click info: // - r-click on hero // - r-click on primary skill label diff --git a/config/gameConfig.json b/config/gameConfig.json index c6ed7d8d8..ed4c0cd5e 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -42,7 +42,8 @@ "config/heroes/stronghold.json", "config/heroes/fortress.json", "config/heroes/conflux.json", - "config/heroes/special.json" + "config/heroes/special.json", + "config/heroes/portraits.json" ], "objects" : @@ -218,6 +219,11 @@ "special2" : 19, // bloodObelisk "special3" : 18 // glyphsOfFear } + }, + + "portraits" : { + "catherine" : 128, // In "RoE" Catherine only has portrait + "portraitGeneralKendal" : 129 } }, "armageddonsBlade" : { @@ -228,11 +234,27 @@ "special1" : 10, // artifactMerchants "special2" : 18 // magicUniversity } + }, + + "portraits" : { + "portraitGeneralKendal" : 156, + "portraitYoungCristian" : 157, + "portraitOrdwald" : 158 } }, "shadowOfDeath" : { "supported" : true, - "iconIndex" : 2 + "iconIndex" : 2, + + "portraits" : { + "portraitGeneralKendal" : 156, + "portraitYoungCristian" : 157, + "portraitOrdwald" : 158, + "portraitFinneas" : 159, + "portraitYoungGem" : 160, + "portraitYoungSandro" : 161, + "portraitYoungYog" : 162 + } }, "jsonVCMI" : { "supported" : true, diff --git a/config/heroes/portraits.json b/config/heroes/portraits.json new file mode 100644 index 000000000..d1fa147aa --- /dev/null +++ b/config/heroes/portraits.json @@ -0,0 +1,206 @@ +{ + // additional empty heroes for correct loading of hero portraits set in map editor + "portraitGeneralKendal" : + { + "class" : "knight", + "special" : true, + "images": { + "large" : "HPL129MK", + "small" : "HPS129MK", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "pikeman", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungCristian" : + { + "class" : "knight", + "special" : true, + "images": { + "large" : "HPL002SH", + "small" : "HPS002SH", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "pikeman", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitOrdwald" : + { + "class" : "druid", + "special" : true, + "images": { + "large" : "HPL132Wl", + "small" : "HPS132Wl", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "centaur", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitFinneas" : + { + "class" : "necromancer", + "special" : true, + "images": { + "large" : "HPL133Nc", + "small" : "HPS133Nc", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "skeleton", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungGem" : + { + "class" : "druid", + "special" : true, + "images": { + "large" : "HPL134Nc", + "small" : "HPS134Nc", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "centaur", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungSandro" : + { + "class" : "necromancer", + "special" : true, + "images": { + "large" : "HPL135Wi", + "small" : "HPS135Wi", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "skeleton", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + }, + "portraitYoungYog" : + { + "class" : "wizard", + "special" : true, + "images": { + "large" : "HPL136Wi", + "small" : "HPS136Wi", + "specialtySmall" : "default", + "specialtyLarge" : "default" + }, + "texts" : { + "name" : "", + "biography" : "", + "specialty" : { + "description" : "", + "tooltip" : "", + "name" : "" + } + }, + "army" : [ + { + "creature" : "gremlin", + "min" : 1, + "max" : 1 + } + ], + "skills" : [], + "specialty" : {} + } +} \ No newline at end of file diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index dceb58b47..e76241e2e 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -298,7 +298,7 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js VLC->identifiers()->requestIdentifier("faction", node["faction"], [=](si32 factionID) { - heroClass->faction = factionID; + heroClass->faction.setNum(factionID); }); VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) @@ -722,8 +722,9 @@ std::vector CHeroHandler::loadLegacyData() void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data) { size_t index = objects.size(); + static const int specialFramesCount = 2; // reserved for 2 special frames auto * object = loadFromJson(scope, data, name, index); - object->imageIndex = static_cast(index) + GameConstants::HERO_PORTRAIT_SHIFT; // 2 special frames + some extra portraits + object->imageIndex = static_cast(index) + specialFramesCount; objects.emplace_back(object); diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 7e14bb8bb..b9e9cfb4b 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -11,6 +11,8 @@ #include "StartInfo.h" #include "CGeneralTextHandler.h" +#include "CTownHandler.h" +#include "CHeroHandler.h" #include "VCMI_Lib.h" #include "rmg/CMapGenOptions.h" #include "mapping/CMapInfo.h" @@ -22,9 +24,28 @@ VCMI_LIB_NAMESPACE_BEGIN PlayerSettings::PlayerSettings() - : bonus(RANDOM), castle(NONE), hero(RANDOM), heroPortrait(RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false) + : bonus(PlayerStartingBonus::RANDOM), color(0), handicap(NO_HANDICAP), compOnly(false) { +} +FactionID PlayerSettings::getCastleValidated() const +{ + if (!castle.isValid()) + return FactionID(0); + if (castle.getNum() < VLC->townh->size()) + return castle; + + return FactionID(0); +} + +HeroTypeID PlayerSettings::getHeroValidated() const +{ + if (!hero.isValid()) + return HeroTypeID(0); + if (hero.getNum() < VLC->heroh->size()) + return hero; + + return HeroTypeID(0); } bool PlayerSettings::isControlledByAI() const @@ -196,15 +217,15 @@ ui8 LobbyInfo::clientFirstId(int clientId) const return 0; } -PlayerInfo & LobbyInfo::getPlayerInfo(int color) +PlayerInfo & LobbyInfo::getPlayerInfo(PlayerColor color) { - return mi->mapHeader->players[color]; + return mi->mapHeader->players[color.getNum()]; } TeamID LobbyInfo::getPlayerTeamId(const PlayerColor & color) { if(color.isValidPlayer()) - return getPlayerInfo(color.getNum()).team; + return getPlayerInfo(color).team; else return TeamID::NO_TEAM; } diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 127a2446a..dbf5b6a71 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -41,20 +41,20 @@ struct DLL_LINKAGE SimturnsInfo } }; +enum class PlayerStartingBonus : int8_t +{ + RANDOM = -1, + ARTIFACT = 0, + GOLD = 1, + RESOURCE = 2 +}; + /// Struct which describes the name, the color, the starting bonus of a player struct DLL_LINKAGE PlayerSettings { enum { PLAYER_AI = 0 }; // for use in playerID - enum Ebonus { - NONE = -2, - RANDOM = -1, - ARTIFACT = 0, - GOLD = 1, - RESOURCE = 2 - }; - - Ebonus bonus; + PlayerStartingBonus bonus; FactionID castle; HeroTypeID hero; HeroTypeID heroPortrait; //-1 if default, else ID @@ -85,6 +85,9 @@ struct DLL_LINKAGE PlayerSettings PlayerSettings(); bool isControlledByAI() const; bool isControlledByHuman() const; + + FactionID getCastleValidated() const; + HeroTypeID getHeroValidated() const; }; /// Struct which describes the difficulty, the turn time,.. of a heroes match. @@ -197,7 +200,7 @@ struct DLL_LINKAGE LobbyInfo : public LobbyState PlayerColor clientFirstColor(int clientId) const; bool isClientColor(int clientId, const PlayerColor & color) const; ui8 clientFirstId(int clientId) const; // Used by chat only! - PlayerInfo & getPlayerInfo(int color); + PlayerInfo & getPlayerInfo(PlayerColor color); TeamID getPlayerTeamId(const PlayerColor & color); }; diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 2efc2fac2..5d1dd9340 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -273,7 +273,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) break; case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose { - ret.playerColor = reader["playerColor"].Integer(); + ret.playerColor = PlayerColor(PlayerColor::decode(reader["playerColor"].String())); for(auto & bjson : reader["bonuses"].Vector()) { CampaignBonus bonus; @@ -472,7 +472,7 @@ CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & rea break; case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose { - ret.playerColor = reader.readUInt8(); + ret.playerColor.setNum(reader.readUInt8()); ui8 numOfBonuses = reader.readUInt8(); for (int g=0; g(*this); } + constexpr FinalClass & operator--() + { + --BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator--(int) + { + FinalClass ret(num); + --BaseClass::num; + return ret; + } + constexpr FinalClass operator++(int) { FinalClass ret(num); @@ -209,6 +234,12 @@ public: static std::string entityType(); DLL_LINKAGE static const HeroTypeID NONE; + DLL_LINKAGE static const HeroTypeID RANDOM; + + bool isValid() const + { + return getNum() >= 0; + } }; class SlotID : public Identifier @@ -336,6 +367,11 @@ public: static si32 decode(const std::string& identifier); static std::string encode(const si32 index); static std::string entityType(); + + bool isValid() const + { + return getNum() >= 0; + } }; class BuildingIDBase : public IdentifierBase diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index fdc2a67bd..0fd235c03 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -50,8 +50,6 @@ namespace GameConstants constexpr int CREATURES_COUNT = 197; constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement - - constexpr int HERO_PORTRAIT_SHIFT = 9;// 2 special frames + 7 extra portraits } VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index dd3c72a5e..ffdd990df 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -824,7 +824,7 @@ void CGameState::placeStartingHeroes() if (campaign && campaign->playerHasStartingHero(playerColor)) continue; - int heroTypeId = pickNextHeroType(playerColor); + HeroTypeID heroTypeId = pickNextHeroType(playerColor); if(playerSettingPair.second.hero == HeroTypeID::NONE) playerSettingPair.second.hero = heroTypeId; @@ -959,14 +959,15 @@ void CGameState::initStartingBonus() for(auto & elem : players) { //starting bonus - if(scenarioOps->playerInfos[elem.first].bonus==PlayerSettings::RANDOM) - scenarioOps->playerInfos[elem.first].bonus = static_cast(getRandomGenerator().nextInt(2)); + if(scenarioOps->playerInfos[elem.first].bonus == PlayerStartingBonus::RANDOM) + scenarioOps->playerInfos[elem.first].bonus = static_cast(getRandomGenerator().nextInt(2)); + switch(scenarioOps->playerInfos[elem.first].bonus) { - case PlayerSettings::GOLD: + case PlayerStartingBonus::GOLD: elem.second.resources[EGameResID::GOLD] += getRandomGenerator().nextInt(5, 10) * 100; break; - case PlayerSettings::RESOURCE: + case PlayerStartingBonus::RESOURCE: { auto res = (*VLC->townh)[scenarioOps->playerInfos[elem.first].castle]->town->primaryRes; if(res == EGameResID::WOOD_AND_ORE) @@ -981,7 +982,7 @@ void CGameState::initStartingBonus() } break; } - case PlayerSettings::ARTIFACT: + case PlayerStartingBonus::ARTIFACT: { if(elem.second.heroes.empty()) { @@ -2117,7 +2118,7 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow for(const auto & playerSettingPair : scenarioOps->playerInfos) //remove uninitialized yet heroes picked for start by other players { - if(playerSettingPair.second.hero.getNum() != PlayerSettings::RANDOM) + if(playerSettingPair.second.hero != HeroTypeID::RANDOM) ret -= HeroTypeID(playerSettingPair.second.hero); } diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index fbcd70011..f69f1f876 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -233,7 +233,7 @@ void CGameStateCampaign::placeCampaignHeroes() // now add removed heroes again with unused type ID for(auto * hero : removedHeroes) { - si32 heroTypeId = 0; + HeroTypeID heroTypeId; if(hero->ID == Obj::HERO) { heroTypeId = gameState->pickUnusedHeroTypeRandomly(hero->tempOwner); @@ -243,7 +243,7 @@ void CGameStateCampaign::placeCampaignHeroes() auto unusedHeroTypeIds = gameState->getUnusedAllowedHeroes(); if(!unusedHeroTypeIds.empty()) { - heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())).getNum(); + heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())); } else { @@ -257,7 +257,6 @@ void CGameStateCampaign::placeCampaignHeroes() } hero->subID = heroTypeId; - hero->portrait = hero->subID; gameState->map->getEditManager()->insertObject(hero); } } diff --git a/lib/gameState/InfoAboutArmy.cpp b/lib/gameState/InfoAboutArmy.cpp index 12d6fd81c..f5c807fe5 100644 --- a/lib/gameState/InfoAboutArmy.cpp +++ b/lib/gameState/InfoAboutArmy.cpp @@ -73,18 +73,18 @@ void InfoAboutHero::assign(const InfoAboutHero & iah) details = (iah.details ? new Details(*iah.details) : nullptr); hclass = iah.hclass; - portrait = iah.portrait; + portraitSource = iah.portraitSource; } -InfoAboutHero::InfoAboutHero(): portrait(-1) {} +InfoAboutHero::InfoAboutHero() +{} InfoAboutHero::InfoAboutHero(const InfoAboutHero & iah): InfoAboutArmy(iah) { assign(iah); } -InfoAboutHero::InfoAboutHero(const CGHeroInstance * h, InfoAboutHero::EInfoLevel infoLevel): - portrait(-1) +InfoAboutHero::InfoAboutHero(const CGHeroInstance * h, InfoAboutHero::EInfoLevel infoLevel) { initFromHero(h, infoLevel); } @@ -100,6 +100,11 @@ InfoAboutHero & InfoAboutHero::operator=(const InfoAboutHero & iah) return *this; } +int32_t InfoAboutHero::getIconIndex() const +{ + return VLC->heroTypes()->getById(portraitSource)->getIconIndex(); +} + void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLevel infoLevel) { vstd::clear_pointer(details); @@ -112,7 +117,7 @@ void InfoAboutHero::initFromHero(const CGHeroInstance *h, InfoAboutHero::EInfoLe hclass = h->type->heroClass; name = h->getNameTranslated(); - portrait = h->portrait; + portraitSource = h->getPortraitSource(); if(detailed) { diff --git a/lib/gameState/InfoAboutArmy.h b/lib/gameState/InfoAboutArmy.h index e62a2719e..569084c83 100644 --- a/lib/gameState/InfoAboutArmy.h +++ b/lib/gameState/InfoAboutArmy.h @@ -55,7 +55,7 @@ public: Details * details = nullptr; const CHeroClass *hclass; - int portrait; + HeroTypeID portraitSource; enum EInfoLevel { @@ -72,6 +72,7 @@ public: InfoAboutHero & operator=(const InfoAboutHero & iah); void initFromHero(const CGHeroInstance *h, EInfoLevel infoLevel); + int32_t getIconIndex() const; }; /// Struct which holds a int information about a town diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8325b712e..d4cbc8686 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -246,7 +246,6 @@ CGHeroInstance::CGHeroInstance(): moveDir(4), mana(UNINITIALIZED_MANA), movement(UNINITIALIZED_MOVEMENT), - portrait(UNINITIALIZED_PORTRAIT), level(1), exp(UNINITIALIZED_EXPERIENCE), gender(EHeroGender::DEFAULT), @@ -273,7 +272,7 @@ void CGHeroInstance::setType(si32 ID, si32 subID) { assert(ID == Obj::HERO); // just in case type = VLC->heroh->objects[subID]; - portrait = type->imageIndex; + CGObjectInstance::setType(ID, type->heroClass->getIndex()); // to find object handler we must use heroClass->id this->subID = subID; // after setType subID used to store unique hero identify id. Check issue 2277 for details randomizeArmy(type->heroClass->faction); @@ -309,8 +308,6 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if(!getArt(ArtifactPosition::MACH4)) putArtifact(ArtifactPosition::MACH4, ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT)); //everyone has a catapult - if(portrait < 0 || portrait == 255) - portrait = type->imageIndex; if(!hasBonus(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))) { for(int g=0; gheroTypes()->getById(getPortraitSource())->getIconIndex(); +} + std::string CGHeroInstance::getNameTranslated() const { return VLC->generaltexth->translate(getNameTextID()); @@ -1528,40 +1538,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) handler.serializeString("name", nameCustomTextId); handler.serializeInt("gender", gender, 0); - - { - const int legacyHeroes = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO); - const int moddedStart = legacyHeroes + GameConstants::HERO_PORTRAIT_SHIFT; - - if(handler.saving) - { - if(portrait >= 0) - { - if(portrait < legacyHeroes || portrait >= moddedStart) - { - int tempPortrait = portrait >= moddedStart - ? portrait - GameConstants::HERO_PORTRAIT_SHIFT - : portrait; - handler.serializeId("portrait", tempPortrait, -1); - } - else - handler.serializeInt("portrait", portrait, -1); - } - } - else - { - const JsonNode & portraitNode = handler.getCurrent()["portrait"]; - - if(portraitNode.getType() == JsonNode::JsonType::DATA_STRING) - { - handler.serializeId("portrait", portrait, -1); - if(portrait >= legacyHeroes) - portrait += GameConstants::HERO_PORTRAIT_SHIFT; - } - else - handler.serializeInt("portrait", portrait, -1); - } - } + handler.serializeId("portrait", customPortraitSource, HeroTypeID::NONE); //primary skills if(handler.saving) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 43a600e15..08335c71e 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -69,7 +69,9 @@ public: ConstTransitivePtr type; TExpType exp; //experience points ui32 level; //current level of hero - si32 portrait; //may be custom + + /// If not NONE - then hero should use portrait from referenced hero type + HeroTypeID customPortraitSource; si32 mana; // remaining spell points std::vector > secSkills; //first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert); if hero has ability (-1, -1) it meansthat it should have default secondary abilities EHeroGender gender; @@ -82,7 +84,6 @@ public: ConstTransitivePtr commander; const CGBoat * boat = nullptr; //set to CGBoat when sailing - static constexpr si32 UNINITIALIZED_PORTRAIT = -1; static constexpr si32 UNINITIALIZED_MANA = -1; static constexpr ui32 UNINITIALIZED_MOVEMENT = -1; static constexpr TExpType UNINITIALIZED_EXPERIENCE = std::numeric_limits::max(); @@ -144,6 +145,9 @@ public: std::string getBiographyTranslated() const; std::string getNameTranslated() const; + HeroTypeID getPortraitSource() const; + int32_t getIconIndex() const; + private: std::string getNameTextID() const; std::string getBiographyTextID() const; @@ -321,7 +325,7 @@ public: h & level; h & nameCustomTextId; h & biographyCustomTextId; - h & portrait; + h & customPortraitSource; h & mana; h & secSkills; h & movement; diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 1e6125841..b32d833f8 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -44,7 +44,6 @@ CQuest::CQuest(): textOption(0), completedOption(0), stackDirection(0), - heroPortrait(-1), isCustomFirst(false), isCustomNext(false), isCustomComplete(false) @@ -555,7 +554,7 @@ void CGSeerHut::setObjToKill() else if(quest->missionType == CQuest::MISSION_KILL_HERO) { quest->heroName = getHeroToKill(false)->getNameTranslated(); - quest->heroPortrait = getHeroToKill(false)->portrait; + quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); } quest->getCompletionText(configuration.onSelect); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3f4d6b793..82a6abd7e 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -69,7 +69,7 @@ public: CStackBasicDescriptor stackToKill; ui8 stackDirection; std::string heroName; //backup of hero name - si32 heroPortrait; + HeroTypeID heroPortrait; MetaString firstVisitText, nextVisitText, completedText; bool isCustomFirst; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index bdd6de2e4..44e7f9442 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -32,29 +32,29 @@ PlayerInfo::PlayerInfo(): canHumanPlay(false), canComputerPlay(false), allowedFactions = VLC->townh->getAllowedFactions(); } -si8 PlayerInfo::defaultCastle() const +FactionID PlayerInfo::defaultCastle() const { //if random allowed set it as default if(isFactionRandom) - return -1; + return FactionID::RANDOM; if(!allowedFactions.empty()) return *allowedFactions.begin(); // fall back to random - return -1; + return FactionID::RANDOM; } -si8 PlayerInfo::defaultHero() const +HeroTypeID PlayerInfo::defaultHero() const { // we will generate hero in front of main town if((generateHeroAtMainTown && hasMainTown) || hasRandomHero) { //random hero - return -1; + return HeroTypeID::RANDOM; } - return -2; + return HeroTypeID::NONE; } bool PlayerInfo::canAnyonePlay() const @@ -64,7 +64,7 @@ bool PlayerInfo::canAnyonePlay() const bool PlayerInfo::hasCustomMainHero() const { - return !mainCustomHeroNameTextId.empty() && mainCustomHeroPortrait != -1; + return mainCustomHeroId.isValid(); } EventCondition::EventCondition(EWinLoseType condition): diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8977a71d2..a64a08dfe 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -27,7 +27,7 @@ struct DLL_LINKAGE SHeroName { SHeroName(); - int heroId; + HeroTypeID heroId; std::string heroName; template @@ -45,9 +45,9 @@ struct DLL_LINKAGE PlayerInfo PlayerInfo(); /// Gets the default faction id or -1 for a random faction. - si8 defaultCastle() const; + FactionID defaultCastle() const; /// Gets the default hero id or -1 for a random hero. - si8 defaultHero() const; + HeroTypeID defaultHero() const; bool canAnyonePlay() const; bool hasCustomMainHero() const; @@ -63,10 +63,10 @@ struct DLL_LINKAGE PlayerInfo /// Player has a random main hero bool hasRandomHero; /// The default value is -1. - si32 mainCustomHeroPortrait; + HeroTypeID mainCustomHeroPortrait; std::string mainCustomHeroNameTextId; /// ID of custom hero (only if portrait and hero name are set, otherwise unpredicted value), -1 if none (not always -1) - si32 mainCustomHeroId; + HeroTypeID mainCustomHeroId; std::vector heroesNames; /// list of placed heroes on the map bool hasMainTown; /// The default value is false. diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index c0a7d5f1f..f1f63e12f 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -80,7 +80,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesAB() result.creaturesCount = 145; // + Conflux and new neutrals result.heroesCount = 156; // + Conflux and campaign heroes - result.heroesPortraitsCount = 163; + result.heroesPortraitsCount = 159; // +Kendal, +young Cristian, +Ordwald result.heroesBytes = 20; result.artifactsCount = 129; // + Armaggedon Blade and Vial of Dragon Blood @@ -100,6 +100,8 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() result.artifactsCount = 141; // + Combined artifacts result.artifactsBytes = 18; + result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog + result.artifactSlotsCount = 19; // + MISC_5 slot return result; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 400746b97..70646288f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -248,9 +248,9 @@ void CMapLoaderH3M::readPlayerInfo() } playerInfo.hasRandomHero = reader->readBool(); - playerInfo.mainCustomHeroId = reader->readHero().getNum(); + playerInfo.mainCustomHeroId = reader->readHero(); - if(playerInfo.mainCustomHeroId != -1) + if(playerInfo.mainCustomHeroId != HeroTypeID::NONE) { playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); @@ -263,7 +263,7 @@ void CMapLoaderH3M::readPlayerInfo() for(int pp = 0; pp < heroCount; ++pp) { SHeroName vv; - vv.heroId = reader->readUInt8(); + vv.heroId = reader->readHero(); vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); playerInfo.heroesNames.push_back(vv); @@ -547,7 +547,6 @@ void CMapLoaderH3M::readVictoryLossConditions() } ); - assert(playersOnMap > 1); if(playersOnMap == 1) { logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); @@ -703,8 +702,8 @@ void CMapLoaderH3M::readDisposedHeroes() map->disposedHeroes.resize(disp); for(int g = 0; g < disp; ++g) { - map->disposedHeroes[g].heroId = reader->readHero().getNum(); - map->disposedHeroes[g].portrait = reader->readHeroPortrait(); + map->disposedHeroes[g].heroId = reader->readHero(); + map->disposedHeroes[g].portrait.setNum(reader->readHeroPortrait()); map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); } @@ -1679,14 +1678,13 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec } setOwnerAndValidate(mapPosition, object, owner); - object->portrait = CGHeroInstance::UNINITIALIZED_PORTRAIT; for(auto & elem : map->disposedHeroes) { if(elem.heroId.getNum() == object->subID) { object->nameCustomTextId = elem.name; - object->portrait = elem.portrait; + object->customPortraitSource = elem.portrait; break; } } @@ -1712,7 +1710,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec bool hasPortrait = reader->readBool(); if(hasPortrait) - object->portrait = reader->readHeroPortrait(); + object->customPortraitSource = reader->readHeroPortrait(); bool hasSecSkills = reader->readBool(); if(hasSecSkills) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index beb31cf7a..a26c8b378 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -562,11 +562,11 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) const std::string instanceName = hero.first; SHeroName hname; - hname.heroId = -1; + hname.heroId = HeroTypeID::NONE; std::string rawId = data["type"].String(); if(!rawId.empty()) - hname.heroId = HeroTypeID::decode(rawId); + hname.heroId = HeroTypeID(HeroTypeID::decode(rawId)); hname.heroName = data["name"].String(); @@ -574,9 +574,9 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) { //this is main hero info.mainCustomHeroNameTextId = hname.heroName; - info.hasRandomHero = (hname.heroId == -1); + info.hasRandomHero = (hname.heroId == HeroTypeID::NONE); info.mainCustomHeroId = hname.heroId; - info.mainCustomHeroPortrait = -1; + info.mainCustomHeroPortrait = HeroTypeID::NONE; //todo:mainHeroPortrait } @@ -744,7 +744,7 @@ void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) { DisposedHero hero; - hero.heroId = type.getNum(); + hero.heroId = type; hero.players = mask; //name and portrait are not used diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index 9935519d8..afa50c338 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -87,15 +87,7 @@ void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) } } - for (auto entry : mapping["portraits"].Struct()) - { - int32_t sourceID = entry.second.Integer(); - int32_t targetID = *VLC->identifiers()->getIdentifier(entry.second.meta, "hero", entry.first); - int32_t iconID = VLC->heroTypes()->getByIndex(targetID)->getIconIndex(); - - mappingHeroPortrait[sourceID] = iconID; - } - + loadMapping(mappingHeroPortrait, mapping["portraits"], "hero"); loadMapping(mappingBuilding, mapping["buildingsCommon"], "building.core:random"); loadMapping(mappingFaction, mapping["factions"], "faction"); loadMapping(mappingCreature, mapping["creatures"], "creature"); @@ -168,7 +160,7 @@ HeroTypeID MapIdentifiersH3M::remap(HeroTypeID input) const return input; } -int32_t MapIdentifiersH3M::remapPortrrait(int32_t input) const +HeroTypeID MapIdentifiersH3M::remapPortrait(HeroTypeID input) const { if (mappingHeroPortrait.count(input)) return mappingHeroPortrait.at(input); diff --git a/lib/mapping/MapIdentifiersH3M.h b/lib/mapping/MapIdentifiersH3M.h index 581a80fda..9b694f158 100644 --- a/lib/mapping/MapIdentifiersH3M.h +++ b/lib/mapping/MapIdentifiersH3M.h @@ -38,7 +38,7 @@ class MapIdentifiersH3M std::map mappingFaction; std::map mappingCreature; std::map mappingHeroType; - std::map mappingHeroPortrait; + std::map mappingHeroPortrait; std::map mappingHeroClass; std::map mappingTerrain; std::map mappingArtifact; @@ -55,7 +55,7 @@ public: void remapTemplate(ObjectTemplate & objectTemplate); BuildingID remapBuilding(std::optional owner, BuildingID input) const; - int32_t remapPortrrait(int32_t input) const; + HeroTypeID remapPortrait(HeroTypeID input) const; FactionID remap(FactionID input) const; CreatureID remap(CreatureID input) const; HeroTypeID remap(HeroTypeID input) const; diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 306279104..0ec268575 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -106,20 +106,20 @@ HeroTypeID MapReaderH3M::readHero() return remapIdentifier(result); } -int32_t MapReaderH3M::readHeroPortrait() +HeroTypeID MapReaderH3M::readHeroPortrait() { HeroTypeID result(reader->readUInt8()); if(result.getNum() == features.heroIdentifierInvalid) - return int32_t(-1); + return HeroTypeID::NONE; if (result.getNum() >= features.heroesPortraitsCount) { logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); - return int32_t(-1); + return HeroTypeID::NONE; } - return remapper.remapPortrrait(result); + return remapper.remapPortrait(result); } CreatureID MapReaderH3M::readCreature() diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index ca2029722..c9e90fe80 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -35,7 +35,7 @@ public: ArtifactID readArtifact32(); CreatureID readCreature(); HeroTypeID readHero(); - int32_t readHeroPortrait(); + HeroTypeID readHeroPortrait(); TerrainId readTerrain(); RoadId readRoad(); RiverId readRiver(); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index eb2925953..386d6e05e 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -158,7 +158,6 @@ void Initializer::initialize(CGHeroInstance * o) // o->type = VLC->heroh->objects.at(o->subID); o->gender = o->type->gender; - o->portrait = o->type->imageIndex; o->randomizeArmy(o->type->heroClass->faction); } } @@ -279,7 +278,7 @@ void Inspector::updateProperties(CGHeroInstance * o) } addProperty("Name", o->getNameTranslated(), false); addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false); - addProperty("Portrait", o->portrait, false); + addProperty("Portrait", o->customPortraitSource, false); auto * delegate = new HeroSkillsDelegate(*o); addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); @@ -622,7 +621,6 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->type = t.get(); } o->gender = o->type->gender; - o->portrait = o->type->imageIndex; o->randomizeArmy(o->type->heroClass->faction); updateProperties(); //updating other properties after change } diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 2b4eb9a8f..ee0ba16fc 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -184,9 +184,6 @@ void MapController::repairMap(CMap * map) const nih->putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); } - //fix portrait - if(nih->portrait < 0 || nih->portrait == 255) - nih->portrait = type->imageIndex; } //fix town instance if(auto * tnh = dynamic_cast(obj.get())) diff --git a/mapeditor/playerparams.cpp b/mapeditor/playerparams.cpp index 192256d1a..97a869f39 100644 --- a/mapeditor/playerparams.cpp +++ b/mapeditor/playerparams.cpp @@ -172,7 +172,7 @@ void PlayerParams::on_mainTown_currentIndexChanged(int index) void PlayerParams::on_teamId_activated(int index) { - playerInfo.team = ui->teamId->currentData().toInt(); + playerInfo.team.setNum(ui->teamId->currentData().toInt()); } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index a5b6a9471..d155ead41 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -729,7 +729,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr mapInfo, pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; @@ -839,13 +839,13 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; FactionID & cur = s.castle; - auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; - const bool allowRandomTown = getPlayerInfo(player.getNum()).isFactionRandom; + auto & allowed = getPlayerInfo(player).allowedFactions; + const bool allowRandomTown = getPlayerInfo(player).isFactionRandom; - if(cur.getNum() == PlayerSettings::NONE) //no change + if(cur == FactionID::NONE) //no change return; - if(cur.getNum() == PlayerSettings::RANDOM) //first/last available + if(cur == FactionID::RANDOM) //first/last available { if(dir > 0) cur = *allowed.begin(); //id of first town @@ -859,7 +859,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { if(allowRandomTown) { - cur = PlayerSettings::RANDOM; + cur = FactionID::RANDOM; } else { @@ -878,34 +878,34 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir) } } - if(s.hero.getNum() >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { - s.hero = PlayerSettings::RANDOM; + s.hero = HeroTypeID::RANDOM; } - if(cur.getNum() < 0 && s.bonus == PlayerSettings::RESOURCE) - s.bonus = PlayerSettings::RANDOM; + if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE) + s.bonus = PlayerStartingBonus::RANDOM; } -void CVCMIServer::optionSetCastle(PlayerColor player, int id) +void CVCMIServer::optionSetCastle(PlayerColor player, FactionID id) { PlayerSettings & s = si->playerInfos[player]; FactionID & cur = s.castle; - auto & allowed = getPlayerInfo(player.getNum()).allowedFactions; + auto & allowed = getPlayerInfo(player).allowedFactions; - if(cur.getNum() == PlayerSettings::NONE) //no change + if(cur == FactionID::NONE) //no change return; - if(allowed.find(static_cast(id)) == allowed.end() && id != PlayerSettings::RANDOM) // valid id + if(allowed.find(id) == allowed.end() && id != FactionID::RANDOM) // valid id return; cur = static_cast(id); - if(s.hero.getNum() >= 0 && !getPlayerInfo(player.getNum()).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor + if(s.hero.isValid() && !getPlayerInfo(player).hasCustomMainHero()) // remove hero unless it set to fixed one in map editor { - s.hero = PlayerSettings::RANDOM; + s.hero = HeroTypeID::RANDOM; } - if(cur.getNum() < 0 && s.bonus == PlayerSettings::RESOURCE) - s.bonus = PlayerSettings::RANDOM; + if(!cur.isValid() && s.bonus == PlayerStartingBonus::RESOURCE) + s.bonus = PlayerStartingBonus::RANDOM; } void CVCMIServer::setCampaignMap(CampaignScenarioID mapId) @@ -937,105 +937,105 @@ void CVCMIServer::setCampaignBonus(int bonusId) void CVCMIServer::optionNextHero(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle.getNum() < 0 || s.hero.getNum() == PlayerSettings::NONE) + if(!s.castle.isValid() || s.hero == HeroTypeID::NONE) return; - if(s.hero.getNum() == PlayerSettings::RANDOM) // first/last available + if(s.hero == HeroTypeID::RANDOM) // first/last available { - int max = static_cast(VLC->heroh->size()), - min = 0; - s.hero = nextAllowedHero(player, min, max, 0, dir); + if (dir > 0) + s.hero = nextAllowedHero(player, HeroTypeID(-1), dir); + else + s.hero = nextAllowedHero(player, HeroTypeID(VLC->heroh->size()), dir); } else { - if(dir > 0) - s.hero = nextAllowedHero(player, s.hero, (int)VLC->heroh->size(), 1, dir); - else - s.hero = nextAllowedHero(player, -1, s.hero, 1, dir); // min needs to be -1 -- hero at index 0 would be skipped otherwise + s.hero = nextAllowedHero(player, s.hero, dir); } } -void CVCMIServer::optionSetHero(PlayerColor player, int id) +void CVCMIServer::optionSetHero(PlayerColor player, HeroTypeID id) { PlayerSettings & s = si->playerInfos[player]; - if(s.castle.getNum() < 0 || s.hero.getNum() == PlayerSettings::NONE) + if(!s.castle.isValid() || s.hero == HeroTypeID::NONE) return; - if(id == PlayerSettings::RANDOM) + if(id == HeroTypeID::RANDOM) { - s.hero = PlayerSettings::RANDOM; + s.hero = HeroTypeID::RANDOM; } if(canUseThisHero(player, id)) s.hero = static_cast(id); } -HeroTypeID CVCMIServer::nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir) +HeroTypeID CVCMIServer::nextAllowedHero(PlayerColor player, HeroTypeID initial, int direction) { - if(dir > 0) + HeroTypeID first(initial.getNum() + direction); + + if(direction > 0) { - for(int i = min + incl; i <= max - incl; i++) + for (auto i = first; i.getNum() < VLC->heroh->size(); ++i) if(canUseThisHero(player, i)) - return HeroTypeID(i); + return i; } else { - for(int i = max - incl; i >= min + incl; i--) + for (auto i = first; i.getNum() >= 0; --i) if(canUseThisHero(player, i)) - return HeroTypeID(i); + return i; } - return -1; + return HeroTypeID::RANDOM; } void CVCMIServer::optionNextBonus(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; - PlayerSettings::Ebonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); + PlayerStartingBonus & ret = s.bonus = static_cast(static_cast(s.bonus) + dir); - if(s.hero.getNum() == PlayerSettings::NONE && - !getPlayerInfo(player.getNum()).heroesNames.size() && - ret == PlayerSettings::ARTIFACT) //no hero - can't be artifact + if(s.hero == HeroTypeID::NONE && + !getPlayerInfo(player).heroesNames.size() && + ret == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact { if(dir < 0) - ret = PlayerSettings::RANDOM; + ret = PlayerStartingBonus::RANDOM; else - ret = PlayerSettings::GOLD; + ret = PlayerStartingBonus::GOLD; } - if(ret > PlayerSettings::RESOURCE) - ret = PlayerSettings::RANDOM; - if(ret < PlayerSettings::RANDOM) - ret = PlayerSettings::RESOURCE; + if(ret > PlayerStartingBonus::RESOURCE) + ret = PlayerStartingBonus::RANDOM; + if(ret < PlayerStartingBonus::RANDOM) + ret = PlayerStartingBonus::RESOURCE; - if(s.castle.getNum() == PlayerSettings::RANDOM && ret == PlayerSettings::RESOURCE) //random castle - can't be resource + if(s.castle == FactionID::RANDOM && ret == PlayerStartingBonus::RESOURCE) //random castle - can't be resource { if(dir < 0) - ret = PlayerSettings::GOLD; + ret = PlayerStartingBonus::GOLD; else - ret = PlayerSettings::RANDOM; + ret = PlayerStartingBonus::RANDOM; } } -void CVCMIServer::optionSetBonus(PlayerColor player, int id) +void CVCMIServer::optionSetBonus(PlayerColor player, PlayerStartingBonus id) { PlayerSettings & s = si->playerInfos[player]; - if(s.hero.getNum() == PlayerSettings::NONE && - !getPlayerInfo(player.getNum()).heroesNames.size() && - id == PlayerSettings::ARTIFACT) //no hero - can't be artifact + if(s.hero == HeroTypeID::NONE && + !getPlayerInfo(player).heroesNames.size() && + id == PlayerStartingBonus::ARTIFACT) //no hero - can't be artifact return; - if(id > PlayerSettings::RESOURCE) + if(id > PlayerStartingBonus::RESOURCE) return; - if(id < PlayerSettings::RANDOM) + if(id < PlayerStartingBonus::RANDOM) return; - if(s.castle.getNum() == PlayerSettings::RANDOM && id == PlayerSettings::RESOURCE) //random castle - can't be resource + if(s.castle == FactionID::RANDOM && id == PlayerStartingBonus::RESOURCE) //random castle - can't be resource return; - s.bonus = static_cast(static_cast(id)); + s.bonus = id;; } -bool CVCMIServer::canUseThisHero(PlayerColor player, int ID) +bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID) { return VLC->heroh->size() > ID && si->playerInfos[player].castle == VLC->heroh->objects[ID]->heroClass->faction @@ -1043,17 +1043,17 @@ bool CVCMIServer::canUseThisHero(PlayerColor player, int ID) && mi->mapHeader->allowedHeroes[ID]; } -std::vector CVCMIServer::getUsedHeroes() +std::vector CVCMIServer::getUsedHeroes() { - std::vector heroIds; + std::vector heroIds; for(auto & p : si->playerInfos) { - const auto & heroes = getPlayerInfo(p.first.getNum()).heroesNames; + const auto & heroes = getPlayerInfo(p.first).heroesNames; for(auto & hero : heroes) if(hero.heroId >= 0) //in VCMI map format heroId = -1 means random hero heroIds.push_back(hero.heroId); - if(p.second.hero.getNum() != PlayerSettings::RANDOM) + if(p.second.hero != HeroTypeID::RANDOM) heroIds.push_back(p.second.hero); } return heroIds; diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 4e3295aa0..8e26d2149 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -108,14 +108,14 @@ public: // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 - void optionSetHero(PlayerColor player, int id); - HeroTypeID nextAllowedHero(PlayerColor player, int min, int max, int incl, int dir); - bool canUseThisHero(PlayerColor player, int ID); - std::vector getUsedHeroes(); + void optionSetHero(PlayerColor player, HeroTypeID id); + HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction); + bool canUseThisHero(PlayerColor player, HeroTypeID ID); + std::vector getUsedHeroes(); void optionNextBonus(PlayerColor player, int dir); //dir == -1 or +1 - void optionSetBonus(PlayerColor player, int id); + void optionSetBonus(PlayerColor player, PlayerStartingBonus id); void optionNextCastle(PlayerColor player, int dir); //dir == -1 or + - void optionSetCastle(PlayerColor player, int id); + void optionSetCastle(PlayerColor player, FactionID id); // Campaigns void setCampaignMap(CampaignScenarioID mapId); diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 804874f94..865c26a50 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -369,19 +369,19 @@ void ApplyOnServerNetPackVisitor::visitLobbyChangePlayerOption(LobbyChangePlayer switch(pack.what) { case LobbyChangePlayerOption::TOWN_ID: - srv.optionSetCastle(pack.color, pack.value); + srv.optionSetCastle(pack.color, FactionID(pack.value)); break; case LobbyChangePlayerOption::TOWN: srv.optionNextCastle(pack.color, pack.value); break; case LobbyChangePlayerOption::HERO_ID: - srv.optionSetHero(pack.color, pack.value); + srv.optionSetHero(pack.color, HeroTypeID(pack.value)); break; case LobbyChangePlayerOption::HERO: srv.optionNextHero(pack.color, pack.value); break; case LobbyChangePlayerOption::BONUS_ID: - srv.optionSetBonus(pack.color, pack.value); + srv.optionSetBonus(pack.color, PlayerStartingBonus(pack.value)); break; case LobbyChangePlayerOption::BONUS: srv.optionNextBonus(pack.color, pack.value); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index cec5a5b5e..b8167a9e7 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -277,7 +277,7 @@ void BattleResultProcessor::endBattle(const CBattleInfoCallback & battle) gameHandler->queries->addQuery(battleDialogQuery); } else - battleResult->queryID = -1; + battleResult->queryID = QueryID::NONE; //set same battle result for all gameHandler->queries for(auto q : gameHandler->queries->allQueries()) diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index c2ad4cdd2..733d75806 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -72,7 +72,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->subID; + sah.hid.setNum(hero->subID); gameHandler->sendAndApply(&sah); } @@ -83,7 +83,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid = hero->subID; + sah.hid.setNum(hero->subID); sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); @@ -110,7 +110,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe if (newHero) { - sah.hid = newHero->subID; + sah.hid.setNum(newHero->subID); if (giveArmy) { @@ -126,7 +126,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe } else { - sah.hid = -1; + sah.hid = HeroTypeID::NONE; } gameHandler->sendAndApply(&sah); } @@ -205,7 +205,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy HeroRecruited hr; hr.tid = mapObject->id; - hr.hid = recruitedHero->subID; + hr.hid.setNum(recruitedHero->subID); hr.player = player; hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index e571d2d25..0141bc2a7 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -167,11 +167,11 @@ public: pset.castle = pinfo.defaultCastle(); pset.hero = pinfo.defaultHero(); - if(pset.hero.getNum() != PlayerSettings::RANDOM && pinfo.hasCustomMainHero()) + if(pset.hero != HeroTypeID::RANDOM && pinfo.hasCustomMainHero()) { pset.hero = pinfo.mainCustomHeroId; pset.heroNameTextId = pinfo.mainCustomHeroNameTextId; - pset.heroPortrait = pinfo.mainCustomHeroPortrait; + pset.heroPortrait = HeroTypeID(pinfo.mainCustomHeroPortrait); } pset.handicap = PlayerSettings::NO_HANDICAP; From 99221c2d56cdff76e9a4358e6c2f678ca98b0b4d Mon Sep 17 00:00:00 2001 From: Fabrice Tiercelin Date: Wed, 4 Oct 2023 20:44:20 +0200 Subject: [PATCH 0724/1248] Fix Chinese translation --- Mods/vcmi/config/vcmi/chinese.json | 145 +++++++++++++++++++++-------- launcher/translation/french.ts | 2 +- mapeditor/translation/french.ts | 6 +- mapeditor/translation/german.ts | 10 ++ mapeditor/translation/polish.ts | 10 ++ mapeditor/translation/spanish.ts | 8 +- mapeditor/translation/ukrainian.ts | 10 ++ 7 files changed, 145 insertions(+), 46 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index a62b3a606..70c7b3275 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -1,17 +1,17 @@ { "vcmi.adventureMap.monsterThreat.title" : "\n\n威胁度: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "不值一提", + "vcmi.adventureMap.monsterThreat.levels.0" : "极低", "vcmi.adventureMap.monsterThreat.levels.1" : "很低", "vcmi.adventureMap.monsterThreat.levels.2" : "低", "vcmi.adventureMap.monsterThreat.levels.3" : "较低", - "vcmi.adventureMap.monsterThreat.levels.4" : "势均力敌", + "vcmi.adventureMap.monsterThreat.levels.4" : "中等", "vcmi.adventureMap.monsterThreat.levels.5" : "较高", "vcmi.adventureMap.monsterThreat.levels.6" : "高", "vcmi.adventureMap.monsterThreat.levels.7" : "很高", - "vcmi.adventureMap.monsterThreat.levels.8" : "略有挑战", + "vcmi.adventureMap.monsterThreat.levels.8" : "挑战性的", "vcmi.adventureMap.monsterThreat.levels.9" : "压倒性的", - "vcmi.adventureMap.monsterThreat.levels.10" : "自寻死路", - "vcmi.adventureMap.monsterThreat.levels.11" : "天方夜谭", + "vcmi.adventureMap.monsterThreat.levels.10" : "致命的", + "vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜", "vcmi.adventureMap.confirmRestartGame" : "你想要重新开始游戏吗?", "vcmi.adventureMap.noTownWithMarket" : "没有足够的市场。", @@ -23,16 +23,39 @@ "vcmi.capitalColors.0" : "红色", "vcmi.capitalColors.1" : "蓝色", - "vcmi.capitalColors.2" : "青色", + "vcmi.capitalColors.2" : "褐色", "vcmi.capitalColors.3" : "绿色", "vcmi.capitalColors.4" : "橙色", "vcmi.capitalColors.5" : "紫色", - "vcmi.capitalColors.6" : "褐色", + "vcmi.capitalColors.6" : "青色", "vcmi.capitalColors.7" : "粉色", - "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", - "vcmi.server.errors.modsToEnable" : "{需要加载的MOD列表}", - "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", + "vcmi.heroOverview.startingArmy" : "初始部队", + "vcmi.heroOverview.warMachine" : "战争机器", + "vcmi.heroOverview.secondarySkills" : "初始技能", + "vcmi.heroOverview.spells" : "魔法", + + "vcmi.radialWheel.mergeSameUnit" : "合并相同生物", + "vcmi.radialWheel.showUnitInformation" : "显示生物信息", + "vcmi.radialWheel.splitSingleUnit" : "分割单个生物", + "vcmi.radialWheel.splitUnitEqually" : "平均分配生物", + "vcmi.radialWheel.moveUnit" : "将生物移动到部队", + "vcmi.radialWheel.splitUnit" : "分割生物到其他空位", + + "vcmi.mainMenu.serverConnecting" : "连接中...", + "vcmi.mainMenu.serverAddressEnter" : "使用地址:", + "vcmi.mainMenu.serverClosing" : "关闭中...", + "vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏", + "vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏", + "vcmi.mainMenu.playerName" : "Player", + + "vcmi.lobby.filename" : "文件名", + "vcmi.lobby.creationDate" : "创建时间", + + "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", + "vcmi.server.errors.modsToEnable" : "{需要启用的mod列表}", + "vcmi.server.errors.modsToDisable" : "{需要禁用的mod列表}", + "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", "vcmi.settingsMainWindow.generalTab.hover" : "常规", "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 设置游戏客户端呈现", @@ -46,15 +69,29 @@ "vcmi.systemOptions.otherGroup" : "其他设置", // unused right now "vcmi.systemOptions.townsGroup" : "城镇画面", - "vcmi.systemOptions.fullscreenButton.hover" : "全屏", - "vcmi.systemOptions.fullscreenButton.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", + "vcmi.systemOptions.fullscreenBorderless.hover" : "全屏 (无边框)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", + "vcmi.systemOptions.fullscreenExclusive.hover" : "全屏 (边框)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。", "vcmi.systemOptions.resolutionButton.hover" : "分辨率", "vcmi.systemOptions.resolutionButton.help" : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。", "vcmi.systemOptions.resolutionMenu.hover" : "分辨率选择", "vcmi.systemOptions.resolutionMenu.help" : "修改游戏运行时的分辨率。", - "vcmi.systemOptions.fullscreenFailed" : "{全屏}\n\n选择切换到全屏模式失败!当前显示器不支持该分辨率!", + "vcmi.systemOptions.scalingButton.hover" : "界面大小: %p%", + "vcmi.systemOptions.scalingButton.help" : "{界面大小}\n\n改变界面的大小", + "vcmi.systemOptions.scalingMenu.hover" : "选择界面大小", + "vcmi.systemOptions.scalingMenu.help" : "改变游戏界面大小。", + "vcmi.systemOptions.longTouchButton.hover" : "触控间距: %d 毫秒", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{触控间距}\n\n使用触摸屏时,触摸屏幕指定持续时间(以毫秒为单位)后将出现弹出窗口。", + "vcmi.systemOptions.longTouchMenu.hover" : "选择触控间距", + "vcmi.systemOptions.longTouchMenu.help" : "改变触控间距。", + "vcmi.systemOptions.longTouchMenu.entry" : "%d 毫秒", "vcmi.systemOptions.framerateButton.hover" : "显示FPS", "vcmi.systemOptions.framerateButton.help" : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "触觉反馈", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{触觉反馈}\n\n切换触摸输入的触觉反馈。", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "界面增强", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。", "vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息", "vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。", @@ -64,6 +101,12 @@ "vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。", "vcmi.adventureOptions.showGrid.hover" : "显示网格", "vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。", + "vcmi.adventureOptions.borderScroll.hover" : "滚动边界", + "vcmi.adventureOptions.borderScroll.help" : "{滚动边界}\n\n当光标靠近窗口边缘时滚动冒险地图。 可以通过按住 CTRL 键来禁用。", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "信息面板生物管理", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。", + "vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图", + "vcmi.adventureOptions.leftButtonDrag.help" : "{左键拖动地图}\n\n启用后,按住左键移动鼠标将拖动冒险地图视图。", "vcmi.adventureOptions.mapScrollSpeed1.hover": "", "vcmi.adventureOptions.mapScrollSpeed5.hover": "", "vcmi.adventureOptions.mapScrollSpeed6.hover": "", @@ -88,10 +131,16 @@ "vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为即刻", "vcmi.battleOptions.movementHighlightOnHover.hover": "鼠标悬停高亮单位移动范围", "vcmi.battleOptions.movementHighlightOnHover.help": "{鼠标悬停高亮单位移动范围}\n\n当你的鼠标悬停在单位上时高亮他的行动范围。", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "显示生物射程限制", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{显示生物射程限制}\n\n当您将鼠标悬停在其上时,显示射手的射程限制。", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "显示英雄统计数据窗口", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{显示英雄统计数据窗口}\n\n永久切换并显示主要统计数据和法术点的英雄统计数据窗口。", + "vcmi.battleOptions.enableAutocombatSpells.hover": "魔法", + "vcmi.battleOptions.enableAutocombatSpells.help": "{魔法}\n\n快速战斗时不会使用魔法。", "vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐", "vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。", - "vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗", + "vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗", "vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS).", "vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE).", @@ -122,16 +171,18 @@ "vcmi.townHall.greetingDefence" : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。", "vcmi.townHall.hasNotProduced" : "本周%s并没有产生什么资源。", "vcmi.townHall.hasProduced" : "本周%s产生了%d个%s。", - "vcmi.townHall.greetingCustomBonus" : "%s 赋予你 +%d %s%s", + "vcmi.townHall.greetingCustomBonus" : "当你的英雄访问%s 时,这个神奇的建筑使你的英雄 +%d %s%s。", "vcmi.townHall.greetingCustomUntil" : "直到下一场战斗。", "vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。", "vcmi.logicalExpressions.anyOf" : "以下任一前提:", "vcmi.logicalExpressions.allOf" : "以下所有前提:", - "vcmi.logicalExpressions.noneOf" : "无前提:", + "vcmi.logicalExpressions.noneOf" : "与此建筑冲突:", "vcmi.heroWindow.openCommander.hover" : "开启指挥官界面", "vcmi.heroWindow.openCommander.help" : "显示该英雄指挥官详细信息", + "vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面", + "vcmi.heroWindow.openBackpack.help" : "用更大的界面显示所有获得的宝物", "vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?", @@ -145,12 +196,30 @@ "vcmi.questLog.hideComplete.hover" : "隐藏完成任务", "vcmi.questLog.hideComplete.help" : "隐藏所有完成的任务", - "vcmi.randomMapTab.widgets.randomTemplate" : "(随机)", + "vcmi.randomMapTab.widgets.defaultTemplate" : "默认", "vcmi.randomMapTab.widgets.templateLabel" : "模板", "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设定...", "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", "vcmi.randomMapTab.widgets.roadTypesLabel" : "道路类型", - + + "vcmi.optionsTab.widgets.chessFieldBase.help" : "{额外计时器}\n\n当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", + "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{转动计时器}\n\n当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", + "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{战斗计时器}\n\n战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", + "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{堆栈计时器}\n\n当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", + "vcmi.optionsTab.widgets.labelTimer" : "计时器", + "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "经典计时器", + "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "国际象棋计时器", + + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "祝贺你,你依然存活,取得了胜利!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "敌人消灭了所有怪物,取得了胜利!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "祝贺你!你消灭了所有怪物,取得了胜利!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "获得所有三件宝物", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "祝贺你!你取得了天使联盟且消灭了所有敌人,取得了胜利!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "击败所有敌人并取得天使联盟", + // few strings from WoG used by vcmi "vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i", "vcmi.stackExperience.rank.0" : "新兵 1级", @@ -164,9 +233,9 @@ "vcmi.stackExperience.rank.8" : "少校 9级", "vcmi.stackExperience.rank.9" : "中校 10级", "vcmi.stackExperience.rank.10" : "上校 11级", - - "core.bonus.ADDITIONAL_ATTACK.name": "双重攻击", - "core.bonus.ADDITIONAL_ATTACK.description": "攻击两次", + + "core.bonus.ADDITIONAL_ATTACK.name": "双击", + "core.bonus.ADDITIONAL_ATTACK.description": "生物可以攻击两次", "core.bonus.ADDITIONAL_RETALIATION.name": "额外反击", "core.bonus.ADDITIONAL_RETALIATION.description": "每回合额外获得${val}次反击机会", "core.bonus.AIR_IMMUNITY.name": "气系免疫", @@ -185,11 +254,11 @@ "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "敌方施法消耗的魔法值增加${val}点", "core.bonus.CHARGE_IMMUNITY.name": "免疫冲锋", "core.bonus.CHARGE_IMMUNITY.description": "对冲锋特技的额外伤害免疫", - "core.bonus.DARKNESS.name": "阴影庇体", + "core.bonus.DARKNESS.name": "黑幕", "core.bonus.DARKNESS.description": "创建${val}半径黑幕", "core.bonus.DEATH_STARE.name": "死亡凝视 (${val}%)", "core.bonus.DEATH_STARE.description": "${val}%几率直接杀死一单位生物", - "core.bonus.DEFENSIVE_STANCE.name": "防御增效", + "core.bonus.DEFENSIVE_STANCE.name": "防御奖励", "core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val}防御力", "core.bonus.DESTRUCTION.name": "毁灭", "core.bonus.DESTRUCTION.description": "${val}%几率在攻击后追加消灭数量", @@ -199,9 +268,9 @@ "core.bonus.DRAGON_NATURE.description": "生物拥有龙的特性", "core.bonus.EARTH_IMMUNITY.name": "土系免疫", "core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法", - "core.bonus.ENCHANTER.name": "强化师", + "core.bonus.ENCHANTER.name": "施法者", "core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}", - "core.bonus.ENCHANTED.name": "魔法附身", + "core.bonus.ENCHANTED.name": "法术加持", "core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响", "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)", "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。", @@ -209,29 +278,29 @@ "core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法", "core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)", "core.bonus.FIRE_SHIELD.description": "反弹部分受到的近战伤害", - "core.bonus.FIRST_STRIKE.name": "抢先攻击", + "core.bonus.FIRST_STRIKE.name": "抢先反击", "core.bonus.FIRST_STRIKE.description": "该生物的反击将会在被攻击前进行", "core.bonus.FEAR.name": "恐惧", "core.bonus.FEAR.description": "使得敌方一只部队恐惧", "core.bonus.FEARLESS.name": "无惧", "core.bonus.FEARLESS.description": "免疫恐惧特质", - "core.bonus.FLYING.name": "飞行", + "core.bonus.FLYING.name": "飞行兵种", "core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)", "core.bonus.FREE_SHOOTING.name": "近身射击", "core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击", - "core.bonus.GARGOYLE.name": "石像鬼", + "core.bonus.GARGOYLE.name": "石像鬼属性", "core.bonus.GARGOYLE.description": "不能被复活或治疗", "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)", "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害", "core.bonus.HATE.name": "${subtype.creature}的死敌", "core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害", - "core.bonus.HEALER.name": "治疗者", + "core.bonus.HEALER.name": "治疗", "core.bonus.HEALER.description": "可以治疗友军单位", "core.bonus.HP_REGENERATION.name": "再生", - "core.bonus.HP_REGENERATION.description": "每回合恢复${SHval}点生命值", - "core.bonus.JOUSTING.name": "英勇冲锋", + "core.bonus.HP_REGENERATION.description": "每回合恢复${val}点生命值", + "core.bonus.JOUSTING.name": "冲锋", "core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害", - "core.bonus.KING.name": "王牌", + "core.bonus.KING.name": "顶级怪物", "core.bonus.KING.description": "受${val}级或更高级屠戮成性影响", "core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法", "core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法", @@ -239,7 +308,7 @@ "core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标", "core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)", "core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身", - "core.bonus.MANA_CHANNELING.name": "魔法通道${val}%", + "core.bonus.MANA_CHANNELING.name": "魔法虹吸${val}%", "core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值", "core.bonus.MANA_DRAIN.name": "吸取魔力", "core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值", @@ -256,14 +325,14 @@ "core.bonus.NO_MORALE.name": "无士气", "core.bonus.NO_MORALE.description": "生物不受士气影响", "core.bonus.NO_WALL_PENALTY.name": "无城墙影响", - "core.bonus.NO_WALL_PENALTY.description": "攻城战中被城墙阻挡造成全额伤害", + "core.bonus.NO_WALL_PENALTY.description": "攻城战中不被城墙阻挡造成全额伤害", "core.bonus.NON_LIVING.name": "无生命", "core.bonus.NON_LIVING.description": "免疫大多数的效果", "core.bonus.RANDOM_SPELLCASTER.name": "随机施法", "core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法", "core.bonus.RANGED_RETALIATION.name": "远程反击", "core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击", - "core.bonus.RECEPTIVE.name": "适应", + "core.bonus.RECEPTIVE.name": "接受", "core.bonus.RECEPTIVE.description": "不会免疫有益魔法", "core.bonus.REBIRTH.name": "复生 (${val}%)", "core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活", @@ -295,12 +364,12 @@ "core.bonus.SYNERGY_TARGET.description": "生物受到协助攻击的影响", "core.bonus.TWO_HEX_ATTACK_BREATH.name": "吐息", "core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击(2格范围)", - "core.bonus.THREE_HEADED_ATTACK.name": "三头攻击", + "core.bonus.THREE_HEADED_ATTACK.name": "半环攻击", "core.bonus.THREE_HEADED_ATTACK.description": "攻击三格邻接单位", "core.bonus.TRANSMUTATION.name": "变形术", "core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物", - "core.bonus.UNDEAD.name": "亡灵", - "core.bonus.UNDEAD.description": "该生物属于亡灵", + "core.bonus.UNDEAD.name": "不死生物", + "core.bonus.UNDEAD.description": "该生物属于丧尸", "core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击", "core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人", "core.bonus.WATER_IMMUNITY.name": "水系免疫", diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index e59b15e82..0bf2c7db8 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -175,7 +175,7 @@ Artifacts - Artéfacts + Artefacts diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 65c77b0fa..b2f21c8dd 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -292,13 +292,13 @@ Confirmation - + Confirmation Unsaved changes will be lost, are you sure? - - + Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? + Failed to open map diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 4da2aee66..8cbaaf904 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -304,6 +304,16 @@ Failed to open map Öffnen der Karte fehlgeschlagen + + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + Cannot open map from this folder diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index ef642f63b..86fa6daeb 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -304,6 +304,16 @@ Failed to open map Nie udało się otworzyć mapy + + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + Cannot open map from this folder diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 73a9b60ed..79f703770 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -292,22 +292,22 @@ Confirmation - + Confirmación Unsaved changes will be lost, are you sure? - + Los cambios no guardados se perderán. Está usted seguro ? Failed to open map - + No se pudo abrir el mapa Cannot open map from this folder - + No se puede abrir el mapa de esta carpeta diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index 8470bca90..b6d948bbd 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -309,6 +309,16 @@ Cannot open map from this folder + + + Confirmation + + + + + Unsaved changes will be lost, are you sure? + + Open map From f72a106e309488da5ed3ab2432b11534e2c7dfaf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 23:23:09 +0300 Subject: [PATCH 0725/1248] Overwrite Catherine / Kendal portraits in AB+ correctly --- config/gameConfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/gameConfig.json b/config/gameConfig.json index ed4c0cd5e..f80ff5a43 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -237,6 +237,8 @@ }, "portraits" : { + "pasis" : 128, + "thunar" : 129, "portraitGeneralKendal" : 156, "portraitYoungCristian" : 157, "portraitOrdwald" : 158 From 63453d4ad258ba3b0573f3e2f697334378f3c7eb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 5 Oct 2023 00:38:00 +0300 Subject: [PATCH 0726/1248] Reverted conan docs to older (but more correct) version --- docs/developers/Conan.md | 87 ++++++++++++++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 13 deletions(-) diff --git a/docs/developers/Conan.md b/docs/developers/Conan.md index d592ef26e..e4a87c932 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -10,10 +10,12 @@ The following platforms are supported and known to work, others might require ch - **macOS**: x86_64 (Intel) - target 10.13 (High Sierra), arm64 (Apple Silicon) - target 11.0 (Big Sur) - **iOS**: arm64 - target 12.0 +- **Windows**: x86_64 and x86 fully supported with building from Linux +- **Android**: armeabi-v7a (32-bit ARM) - target 4.4 (API level 19), aarch64-v8a (64-bit ARM) - target 5.0 (API level 21) ## Getting started -1. [Install Conan](https://docs.conan.io/en/latest/installation.html) +1. [Install Conan](https://docs.conan.io/en/latest/installation.html). Currently we support only Conan v1, you can install it with `pip` like this: `pip3 install 'conan<2.0'` 2. Execute in terminal: `conan profile new default --detect` ## Download dependencies @@ -22,36 +24,45 @@ The following platforms are supported and known to work, others might require ch 1. Check if your build environment can use the prebuilt binaries: basically, that your compiler version (or Xcode major version) matches the information below. If you're unsure, simply advance to the next step. - - macOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode and Xcode CLT 13.x - - iOS: libraries are built with Apple clang 13 (Xcode 13.4.1), should be consumable by Xcode 13.x + - **macOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode and Xcode CLT 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo) + - **iOS**: libraries are built with Apple clang 14 (Xcode 14.2), should be consumable by Xcode 14.x (older library versions are also available for Xcode 13, see Releases in the respective repo) + - **Windows**: libraries are built with x86_64-mingw-w64-gcc version 10 (which is available in repositories of Ubuntu 22.04) + - **Android**: libraries are built with NDK r25c (25.2.9519653) 2. Download the binaries archive and unpack it to `~/.conan` directory: - [macOS](https://github.com/vcmi/vcmi-deps-macos/releases/latest): pick **intel.txz** if you have Intel Mac, otherwise - **intel-cross-arm.txz** - [iOS](https://github.com/vcmi/vcmi-ios-deps/releases/latest) + - [Windows](https://github.com/vcmi/vcmi-deps-windows-conan/releases/latest): pick **vcmi-deps-windows-conan-w64.tgz** + if you want x86, otherwise pick **vcmi-deps-windows-conan.tgz** + - [Android](https://github.com/vcmi/vcmi-dependencies/releases) -3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders). +3. Only if you have Apple Silicon Mac and trying to build for macOS or iOS: + + 1. Open file `~/.conan/data/qt/5.15.x/_/_/export/conanfile.py` (`5.15.x` is a placeholder), search for string `Designer` (there should be only one match), comment this line and the one above it by inserting `#` in the beginning, and save the file. + 2. (optional) If you don't want to use Rosetta, follow [instructions how to build Qt host tools for Apple Silicon](https://github.com/vcmi/vcmi-ios-deps#note-for-arm-macs), on step 3 copy them to `~/.conan/data/qt/5.15.x/_/_/package/SOME_HASH/bin` (`5.15.x` and `SOME_HASH` are placeholders). Make sure **not** to copy `qt.conf`! ## Generate CMake integration Conan needs to generate CMake toolchain file to make dependencies available to CMake. See `CMakeDeps` and `CMakeToolchain` [in the official documentation](https://docs.conan.io/en/latest/reference/conanfile/tools/cmake.html) for details. -In terminal `cd` to the VCMI source directory and run the following command. If you want to download prebuilt binaries from ConanCenter, also read the [next section](#using-prebuilt-binaries-from-conancenter). +In terminal `cd` to the VCMI source directory and run the following command. Also check subsections for additional requirements on consuming prebuilt binaries. -```sh +
     conan install . \
       --install-folder=conan-generated \
       --no-imports \
       --build=never \
       --profile:build=default \
       --profile:host=CI/conan/PROFILE
    -```
    +
    The highlighted parts can be adjusted: - ***conan-generated***: directory (absolute or relative) where the generated files will appear. This value is used in CMake presets from VCMI, but you can actually use any directory and override it in your local CMake presets. - ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build recipes, that are not present in your local cache, from source. -- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile). +- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile). +- ***note for Windows x86***: use profile mingw32-linux.jinja for building instead of mingw64-linux.jinja If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead. @@ -59,6 +70,10 @@ VCMI "recipe" also has some options that you can specify. For example, if you do _Note_: you can find full reference of this command [in the official documentation](https://docs.conan.io/en/latest/reference/commands/consumer/install.html) or by executing `conan help install`. +### Using our prebuilt binaries for macOS/iOS + +We use custom recipes for some libraries that are provided by the OS. You must additionally pass `-o with_apple_system_libs=True` for `conan install` to use them. + ### Using prebuilt binaries from ConanCenter First, check if binaries for [your platform](https://github.com/conan-io/conan-center-index/blob/master/docs/supported_platforms_and_configurations.md) are produced. @@ -68,6 +83,25 @@ You must adjust the above `conan install` command: 1. Replace ***CI/conan/PROFILE*** with `default`. 2. Additionally pass `-o default_options_of_requirements=True`: this disables all custom options of our `conanfile.py` to match ConanCenter builds. +### Building dependencies from source + +This subsection describes platform specifics to build libraries from source properly. + +#### Building for macOS/iOS + +- To build Locale module of Boost in versions >= 1.81, you must use `compiler.cppstd=11` Conan setting (our profiles already contain it). To use it with another profile, either add this setting to your _host_ profile or pass `-s compiler.cppstd=11` on the command line. +- If you wish to build dependencies against system libraries (like our prebuilt ones do), follow [below instructions](#using-recipes-for-system-libraries) executing `conan create` for all directories. Don't forget to pass `-o with_apple_system_libs=True` to `conan install` afterwards. + +#### Building for Android + +Android has issues loading self-built shared Zlib library because binary name is identical to the system one, so we enforce using the OS-provided library. To achieve that, follow [below instructions](#using-recipes-for-system-libraries), you only need `zlib` directory. + +##### Using recipes for system libraries + +1. Clone/download https://github.com/kambala-decapitator/conan-system-libs +2. Execute `conan create PACKAGE vcmi/apple`, where `PACKAGE` is a directory path in that repository. Do it for each library you need. +3. Now you can execute `conan install` to build all dependencies. + ## Configure project for building You must pass the generated toolchain file to CMake invocation. @@ -81,13 +115,14 @@ In these examples only the minimum required amount of options is passed to `cmak ### Use our prebuilt binaries to build for macOS x86_64 with Xcode -```sh +``` conan install . \ --install-folder=conan-generated \ --no-imports \ --build=never \ --profile:build=default \ - --profile:host=CI/conan/macos-intel + --profile:host=CI/conan/macos-intel \ + -o with_apple_system_libs=True cmake -S . -B build -G Xcode \ --toolchain conan-generated/conan_toolchain.cmake @@ -97,7 +132,7 @@ cmake -S . -B build -G Xcode \ If you also want to build the missing binaries from source, use `--build=missing` instead of `--build=never`. -```sh +``` conan install . \ --install-folder=~/my-dir \ --no-imports \ @@ -112,13 +147,14 @@ cmake -S . -B build \ ### Use our prebuilt binaries to build for iOS arm64 device with custom preset -```sh +``` conan install . \ --install-folder=~/my-dir \ --no-imports \ --build=never \ --profile:build=default \ - --profile:host=CI/conan/ios-arm64 + --profile:host=CI/conan/ios-arm64 \ + -o with_apple_system_libs=True cmake --preset ios-conan ``` @@ -147,3 +183,28 @@ cmake --preset ios-conan ] } ``` + +### Build VCMI with all deps for 32-bit windows in Ubuntu 22.04 WSL +```powershell +wsl --install +wsl --install -d Ubuntu +ubuntu +``` +Next steps are identical both in WSL and in real Ubuntu 22.04 +```bash +sudo pip3 install conan +sudo apt install cmake build-essential +sed -i 's/x86_64-w64-mingw32/i686-w64-mingw32/g' CI/mingw-ubuntu/before-install.sh +sed -i 's/x86-64/i686/g' CI/mingw-ubuntu/before-install.sh +sudo ./CI/mingw-ubuntu/before-install.sh +conan install . \ + --install-folder=conan-generated \ + --no-imports \ + --build=missing \ + --profile:build=default \ + --profile:host=CI/conan/mingw32-linux \ + -c tools.cmake.cmaketoolchain.presets:max_schema_version=2 +cmake --preset windows-mingw-conan-linux +cmake --build --preset windows-mingw-conan-linux --target package +``` +After that, you will have functional VCMI installer for 32-bit windows. From bcce4c68e5f8e7b8d39a7edbd2a667fb459d90c5 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:37:02 +0300 Subject: [PATCH 0727/1248] Artifacts placement fixed --- lib/CArtHandler.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b1ee59bc1..b991d7dda 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -1136,10 +1136,13 @@ void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) { for(const ArtifactID & artifactID : backpackTemp) { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); - auto slot = ArtifactPosition::BACKPACK_START + (si32)artifactsInBackpack.size(); + auto * artifact = ArtifactUtils::createArtifact(map, artifactID); + auto slot = ArtifactPosition::BACKPACK_START + artifactsInBackpack.size(); if(artifact->artType->canBePutAt(this, slot)) - putArtifact(slot, artifact); + { + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); + } } } } @@ -1178,7 +1181,8 @@ void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const Artifa if(artifact->artType->canBePutAt(this, slot)) { - putArtifact(slot, artifact); + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); } else { From 0826b542e389ed2a5d95ccc3c11fd58b76ff646a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 5 Oct 2023 19:09:07 +0300 Subject: [PATCH 0728/1248] Apply suggestions from code review Co-authored-by: Andrey Filipenkov --- docs/developers/Conan.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developers/Conan.md b/docs/developers/Conan.md index e4a87c932..f4b9409d1 100644 --- a/docs/developers/Conan.md +++ b/docs/developers/Conan.md @@ -61,7 +61,7 @@ The highlighted parts can be adjusted: - ***conan-generated***: directory (absolute or relative) where the generated files will appear. This value is used in CMake presets from VCMI, but you can actually use any directory and override it in your local CMake presets. - ***never***: use this value to avoid building any dependency from source. You can also use `missing` to build recipes, that are not present in your local cache, from source. -- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile). +- ***CI/conan/PROFILE***: if you want to consume our prebuilt binaries, ***PROFILE*** must be replaced with one of filenames from our [Conan profiles directory](../../CI/conan) (determining the right file should be straight-forward). Otherwise, either select one of our profiles or replace ***CI/conan/PROFILE*** with `default` (your default profile). - ***note for Windows x86***: use profile mingw32-linux.jinja for building instead of mingw64-linux.jinja If you use `--build=never` and this command fails, then it means that you can't use prebuilt binaries out of the box. For example, try using `--build=missing` instead. @@ -99,7 +99,7 @@ Android has issues loading self-built shared Zlib library because binary name is ##### Using recipes for system libraries 1. Clone/download https://github.com/kambala-decapitator/conan-system-libs -2. Execute `conan create PACKAGE vcmi/apple`, where `PACKAGE` is a directory path in that repository. Do it for each library you need. +2. Execute `conan create PACKAGE vcmi/CHANNEL`, where `PACKAGE` is a directory path in that repository and `CHANNEL` is **apple** for macOS/iOS and **android** for Android.. Do it for each library you need. 3. Now you can execute `conan install` to build all dependencies. ## Configure project for building From 189f790b44d7d13ce8ccfc5a86098bed54156dc5 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 5 Oct 2023 22:54:28 +0200 Subject: [PATCH 0729/1248] Translations bugfixes --- mapeditor/mapsettings/loseconditions.cpp | 2 +- mapeditor/mapsettings/translations.cpp | 4 ++-- mapeditor/mapsettings/victoryconditions.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index c66c0f936..3e155060d 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -117,7 +117,7 @@ void LoseConditions::initialize(MapController & c) void LoseConditions::update() { //loss messages - controller->map()->defeatMessage = MetaString::createFromRawString(ui->defeatMessageEdit->text().toStdString()); + controller->map()->defeatMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "defeatMessage"), ui->defeatMessageEdit->text().toStdString())); //loss conditions EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index ab8a76bcc..5088bc4c3 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -17,7 +17,7 @@ void Translations::cleanupRemovedItems(CMap & map) { - std::set existingObjects; + std::set existingObjects{"core", "map", "header"}; for(auto object : map.objects) existingObjects.insert(object->instanceName); @@ -28,7 +28,7 @@ void Translations::cleanupRemovedItems(CMap & map) { for(auto part : QString::fromStdString(s.first).split('.')) { - if(part == "map" || existingObjects.count(part.toStdString())) + if(existingObjects.count(part.toStdString())) { updateTranslations.Struct()[s.first] = s.second; break; diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index 248b637e1..43131386b 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -179,7 +179,7 @@ void VictoryConditions::initialize(MapController & c) void VictoryConditions::update() { //victory messages - controller->map()->victoryMessage = MetaString::createFromRawString(ui->victoryMessageEdit->text().toStdString()); + controller->map()->victoryMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "victoryMessage"), ui->victoryMessageEdit->text().toStdString())); //victory conditions EventCondition victoryCondition(EventCondition::STANDARD_WIN); From 766f9cf19f4b5f509a8e049f38d1ac67f54f9ce4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 5 Oct 2023 22:55:24 +0200 Subject: [PATCH 0730/1248] Mistake fix --- mapeditor/mapsettings/translations.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/mapsettings/translations.cpp b/mapeditor/mapsettings/translations.cpp index 5088bc4c3..dc20d4db0 100644 --- a/mapeditor/mapsettings/translations.cpp +++ b/mapeditor/mapsettings/translations.cpp @@ -17,7 +17,7 @@ void Translations::cleanupRemovedItems(CMap & map) { - std::set existingObjects{"core", "map", "header"}; + std::set existingObjects{"map", "header"}; for(auto object : map.objects) existingObjects.insert(object->instanceName); From 043f54698b2044a97072215d573e6a07bd162973 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 5 Oct 2023 23:34:29 +0200 Subject: [PATCH 0731/1248] Fix translations in map editor / fix crash on game end --- client/CServerHandler.cpp | 7 +++++-- lib/mapping/CMapHeader.cpp | 2 +- mapeditor/mapsettings/loseconditions.cpp | 10 +++++++++- mapeditor/mapsettings/victoryconditions.cpp | 10 +++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ab647e2b6..91828e68d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -681,8 +681,11 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) } } - c->enterLobbyConnectionMode(); - c->disableStackSendingByID(); + if(c) + { + c->enterLobbyConnectionMode(); + c->disableStackSendingByID(); + } //reset settings Settings saveSession = settings.write["server"]["reconnect"]; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 44e7f9442..c86a923b8 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -157,7 +157,7 @@ void CMapHeader::registerMapStrings() mapLanguages.insert(translation.first); } - if(maxStrings == 0 || mapBaseLanguages.empty()) + if(maxStrings == 0 || mapLanguages.empty()) { logGlobal->info("Map %s doesn't have any supported translation", name.toString()); return; diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index 3e155060d..e19962734 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -117,7 +117,7 @@ void LoseConditions::initialize(MapController & c) void LoseConditions::update() { //loss messages - controller->map()->defeatMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "defeatMessage"), ui->defeatMessageEdit->text().toStdString())); + bool customMessage = true; //loss conditions EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); @@ -138,6 +138,7 @@ void LoseConditions::update() controller->map()->triggeredEvents.push_back(standardDefeat); controller->map()->defeatIconIndex = 3; controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.0"); + customMessage = false; } else { @@ -163,6 +164,7 @@ void LoseConditions::update() specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); specialDefeat.trigger = EventExpression(noneOf); controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.1"); + customMessage = false; break; } @@ -177,6 +179,7 @@ void LoseConditions::update() specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); specialDefeat.trigger = EventExpression(noneOf); controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.2"); + customMessage = false; break; } @@ -187,6 +190,7 @@ void LoseConditions::update() specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); specialDefeat.trigger = EventExpression(cond); controller->map()->defeatMessage = MetaString::createFromTextID("core.lcdesc.3"); + customMessage = false; break; } @@ -215,6 +219,10 @@ void LoseConditions::update() controller->map()->triggeredEvents.push_back(specialDefeat); } + if(customMessage) + { + controller->map()->defeatMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "defeatMessage"), ui->defeatMessageEdit->text().toStdString())); + } } void LoseConditions::on_loseComboBox_currentIndexChanged(int index) diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index 43131386b..f1db91643 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -179,7 +179,7 @@ void VictoryConditions::initialize(MapController & c) void VictoryConditions::update() { //victory messages - controller->map()->victoryMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "victoryMessage"), ui->victoryMessageEdit->text().toStdString())); + bool customMessage = true; //victory conditions EventCondition victoryCondition(EventCondition::STANDARD_WIN); @@ -199,6 +199,7 @@ void VictoryConditions::update() controller->map()->triggeredEvents.push_back(standardVictory); controller->map()->victoryIconIndex = 11; controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc.0"); + customMessage = false; } else { @@ -211,6 +212,7 @@ void VictoryConditions::update() controller->map()->victoryIconIndex = vicCondition; controller->map()->victoryMessage = MetaString::createFromTextID("core.vcdesc." + std::to_string(vicCondition + 1)); + customMessage = false; switch(vicCondition) { @@ -327,9 +329,15 @@ void VictoryConditions::update() controller->map()->victoryMessage.appendRawString(" / "); controller->map()->victoryMessage.appendTextID("core.vcdesc.0"); controller->map()->triggeredEvents.push_back(standardVictory); + customMessage = false; } controller->map()->triggeredEvents.push_back(specialVictory); } + + if(customMessage) + { + controller->map()->victoryMessage = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller->map(), TextIdentifier("header", "victoryMessage"), ui->victoryMessageEdit->text().toStdString())); + } } VictoryConditions::~VictoryConditions() From 510fb205e78a2ffc14fbf0179cbdc3f30fa9bcc9 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Thu, 5 Oct 2023 23:29:41 +0200 Subject: [PATCH 0732/1248] Add CMake option for enabling ccache for IOS and MSVC build --- CMakeLists.txt | 16 +++++++++++----- CMakePresets.json | 6 ++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index aa0a6f724..67e808017 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -79,6 +79,12 @@ if(NOT APPLE_IOS AND NOT ANDROID) option(ENABLE_MONOLITHIC_INSTALL "Install everything in single directory on Linux and Mac" OFF) endif() +# On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. +# The iOS and MSVC builds each require some configuration, which is enabled by the following option: +if(APPLE_IOS OR MSVC) + option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) +endif() + # Allow to pass package name from Travis CI set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name") set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename") @@ -223,7 +229,7 @@ if(APPLE_IOS) set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUNDLE_IDENTIFIER_PREFIX}.$(PRODUCT_NAME)") set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2") - if(CMAKE_CXX_COMPILER_LAUNCHER) + if(ENABLE_CCACHE) find_program(CCACHE "ccache") if(CCACHE) # https://stackoverflow.com/a/36515503/2278742 @@ -255,12 +261,12 @@ if(MINGW OR MSVC) set(CMAKE_SHARED_LIBRARY_PREFIX "") if(MSVC) - if(CMAKE_CXX_COMPILER_LAUNCHER) + if(ENABLE_CCACHE) # https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387 - find_program(ccache_exe ccache) - if (ccache_exe) + find_program(CCACHE ccache) + if (CCACHE) file(COPY_FILE - ${ccache_exe} ${CMAKE_BINARY_DIR}/cl.exe + ${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe ONLY_IF_DIFFERENT) set(CMAKE_VS_GLOBALS diff --git a/CMakePresets.json b/CMakePresets.json index 37c321522..f6a8802de 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -122,7 +122,8 @@ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "CMAKE_POLICY_DEFAULT_CMP0091": "NEW", - "FORCE_BUNDLED_MINIZIP": "ON" + "FORCE_BUNDLED_MINIZIP": "ON", + "ENABLE_CCACHE": "ON" } }, { @@ -228,7 +229,8 @@ "description": "VCMI iOS release using Conan and ccache", "inherits": "ios-release-conan", "cacheVariables": { - "ENABLE_PCH" : "OFF" + "ENABLE_PCH" : "OFF", + "ENABLE_CCACHE": "ON" } }, { From c5378f560ebedcc0d0e3a6a08a28f0256d530a8c Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 12:44:56 +0200 Subject: [PATCH 0733/1248] Mark ccache as required if ENABLE_CCACHE is ON, set ENABLE_CCACHE to ON by dafault instead of setting it in CMakePresets.json --- CMakeLists.txt | 6 +++--- CMakePresets.json | 6 ++---- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 67e808017..1d90820cc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ endif() # On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. # The iOS and MSVC builds each require some configuration, which is enabled by the following option: if(APPLE_IOS OR MSVC) - option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) + option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" ON) endif() # Allow to pass package name from Travis CI @@ -230,7 +230,7 @@ if(APPLE_IOS) set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2") if(ENABLE_CCACHE) - find_program(CCACHE "ccache") + find_program(CCACHE ccache REQUIRED) if(CCACHE) # https://stackoverflow.com/a/36515503/2278742 # Set up wrapper scripts @@ -263,7 +263,7 @@ if(MINGW OR MSVC) if(MSVC) if(ENABLE_CCACHE) # https://github.com/ccache/ccache/discussions/1154#discussioncomment-3611387 - find_program(CCACHE ccache) + find_program(CCACHE ccache REQUIRED) if (CCACHE) file(COPY_FILE ${CCACHE} ${CMAKE_BINARY_DIR}/cl.exe diff --git a/CMakePresets.json b/CMakePresets.json index f6a8802de..37c321522 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -122,8 +122,7 @@ "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "CMAKE_POLICY_DEFAULT_CMP0091": "NEW", - "FORCE_BUNDLED_MINIZIP": "ON", - "ENABLE_CCACHE": "ON" + "FORCE_BUNDLED_MINIZIP": "ON" } }, { @@ -229,8 +228,7 @@ "description": "VCMI iOS release using Conan and ccache", "inherits": "ios-release-conan", "cacheVariables": { - "ENABLE_PCH" : "OFF", - "ENABLE_CCACHE": "ON" + "ENABLE_PCH" : "OFF" } }, { From a09c595cf4edf2f441f22d8a995b83436ed3188f Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 6 Oct 2023 17:27:12 +0200 Subject: [PATCH 0734/1248] Support simturn timers --- client/CServerHandler.cpp | 2 +- client/adventureMap/TurnTimerWidget.cpp | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index ab647e2b6..282898446 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -300,7 +300,7 @@ void CServerHandler::applyPacksOnLobbyScreen() CPackForLobby * pack = packsForLobbyScreen.front(); packsForLobbyScreen.pop_front(); CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - apply->applyOnLobbyScreen(static_cast(SEL), this, pack); + apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); GH.windows().totalRedraw(); delete pack; } diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index af0840755..ecf3773bf 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -150,10 +150,15 @@ void TurnTimerWidget::tick(uint32_t msPassed) } else { - for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + if(LOCPLINT->makingTurn) + updateTimer(LOCPLINT->playerID, msPassed); + else { - if(LOCPLINT->cb->isPlayerMakingTurn(p)) - updateTimer(p, msPassed); + for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) + { + if(LOCPLINT->cb->isPlayerMakingTurn(p)) + updateTimer(p, msPassed); + } } } } From 9b54b25a6c861419e0a787906e2a3780bed10106 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 6 Oct 2023 17:56:46 +0200 Subject: [PATCH 0735/1248] Show timer only for one player --- client/adventureMap/TurnTimerWidget.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index ecf3773bf..0d3aaa571 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -157,7 +157,10 @@ void TurnTimerWidget::tick(uint32_t msPassed) for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p) { if(LOCPLINT->cb->isPlayerMakingTurn(p)) + { updateTimer(p, msPassed); + break; + } } } } From 4bed3c4726cc5c02ac0f6a86dd72b2b53f1a3464 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 19:39:32 +0200 Subject: [PATCH 0736/1248] Check CMAKE_GENERATOR, move iOS options to top level --- CMakeLists.txt | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1d90820cc..e1bacc055 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,10 +81,28 @@ endif() # On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. # The iOS and MSVC builds each require some configuration, which is enabled by the following option: -if(APPLE_IOS OR MSVC) +if(CMAKE_GENERATOR MATCHES "Xcode" OR "Visual Studio") option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" ON) endif() +if(ENABLE_CCACHE) + find_program(CCACHE ccache REQUIRED) + if(CCACHE) + # https://stackoverflow.com/a/36515503/2278742 + # Set up wrapper scripts + configure_file(ios/launch-c.in ios/launch-c) + configure_file(ios/launch-cxx.in ios/launch-cxx) + execute_process(COMMAND chmod a+rx + "${CMAKE_BINARY_DIR}/ios/launch-c" + "${CMAKE_BINARY_DIR}/ios/launch-cxx") + # Set Xcode project attributes to route compilation through our scripts + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/ios/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/ios/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/ios/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/ios/launch-cxx") + endif() +endif() + # Allow to pass package name from Travis CI set(PACKAGE_NAME_SUFFIX "" CACHE STRING "Suffix for CPack package name") set(PACKAGE_FILE_NAME "" CACHE STRING "Override for CPack package filename") @@ -228,24 +246,6 @@ if(APPLE_IOS) set(CMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED_FOR_APPS YES) set(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "${BUNDLE_IDENTIFIER_PREFIX}.$(PRODUCT_NAME)") set(CMAKE_XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2") - - if(ENABLE_CCACHE) - find_program(CCACHE ccache REQUIRED) - if(CCACHE) - # https://stackoverflow.com/a/36515503/2278742 - # Set up wrapper scripts - configure_file(ios/launch-c.in ios/launch-c) - configure_file(ios/launch-cxx.in ios/launch-cxx) - execute_process(COMMAND chmod a+rx - "${CMAKE_BINARY_DIR}/ios/launch-c" - "${CMAKE_BINARY_DIR}/ios/launch-cxx") - # Set Xcode project attributes to route compilation through our scripts - set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/ios/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/ios/launch-cxx") - set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/ios/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/ios/launch-cxx") - endif() - endif() endif() if(APPLE_MACOS) From 2bee8b5109d440a74f89a4445325ba53552141f1 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 19:55:40 +0200 Subject: [PATCH 0737/1248] Make suggested changes --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e1bacc055..ffb1df656 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,11 +81,11 @@ endif() # On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. # The iOS and MSVC builds each require some configuration, which is enabled by the following option: -if(CMAKE_GENERATOR MATCHES "Xcode" OR "Visual Studio") +if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode")) option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" ON) endif() -if(ENABLE_CCACHE) +if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) find_program(CCACHE ccache REQUIRED) if(CCACHE) # https://stackoverflow.com/a/36515503/2278742 From 19f8560456764155404a61238fa75bf03e8e459a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 19:58:32 +0200 Subject: [PATCH 0738/1248] Improve comment --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ffb1df656..f14185d99 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -80,7 +80,7 @@ if(NOT APPLE_IOS AND NOT ANDROID) endif() # On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. -# The iOS and MSVC builds each require some configuration, which is enabled by the following option: +# The XCode and MSVC builds each require some more configuration, which is enabled by the following option: if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode")) option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" ON) endif() From 3255b4232c3a0abedecd6154e91783c45b13d461 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 21:07:55 +0200 Subject: [PATCH 0739/1248] Move ccache wrappers for macOS and iOS builds to apple/ dir --- CMakeLists.txt | 16 ++++++++-------- {ios => xcode}/launch-c.in | 0 {ios => xcode}/launch-cxx.in | 0 3 files changed, 8 insertions(+), 8 deletions(-) rename {ios => xcode}/launch-c.in (100%) rename {ios => xcode}/launch-cxx.in (100%) diff --git a/CMakeLists.txt b/CMakeLists.txt index f14185d99..657b06c2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,16 +90,16 @@ if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) if(CCACHE) # https://stackoverflow.com/a/36515503/2278742 # Set up wrapper scripts - configure_file(ios/launch-c.in ios/launch-c) - configure_file(ios/launch-cxx.in ios/launch-cxx) + configure_file(xcode/launch-c.in xcode/launch-c) + configure_file(xcode/launch-cxx.in xcode/launch-cxx) execute_process(COMMAND chmod a+rx - "${CMAKE_BINARY_DIR}/ios/launch-c" - "${CMAKE_BINARY_DIR}/ios/launch-cxx") + "${CMAKE_BINARY_DIR}/xcode/launch-c" + "${CMAKE_BINARY_DIR}/xcode/launch-cxx") # Set Xcode project attributes to route compilation through our scripts - set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/ios/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/ios/launch-cxx") - set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/ios/launch-c") - set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/ios/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_CC "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_CXX "${CMAKE_BINARY_DIR}/xcode/launch-cxx") + set(CMAKE_XCODE_ATTRIBUTE_LD "${CMAKE_BINARY_DIR}/xcode/launch-c") + set(CMAKE_XCODE_ATTRIBUTE_LDPLUSPLUS "${CMAKE_BINARY_DIR}/xcode/launch-cxx") endif() endif() diff --git a/ios/launch-c.in b/xcode/launch-c.in similarity index 100% rename from ios/launch-c.in rename to xcode/launch-c.in diff --git a/ios/launch-cxx.in b/xcode/launch-cxx.in similarity index 100% rename from ios/launch-cxx.in rename to xcode/launch-cxx.in From c8f1512a3fcc0cace97faec4e3007bdbcfc69358 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 6 Oct 2023 23:27:19 +0200 Subject: [PATCH 0740/1248] Use correct address to decide whether we're connecting to a remote server or a local one --- client/CServerHandler.cpp | 32 +++++++++++++++++++------------- client/CServerHandler.h | 4 ++-- client/mainmenu/CMainMenu.cpp | 4 ++-- 3 files changed, 23 insertions(+), 17 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index a7423b9e9..8efdbfdd3 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -252,27 +252,33 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po { state = EClientState::CONNECTING; - logNetwork->info("Establishing connection..."); + logNetwork->info("justConnectToServer(%s, %d)", addr, port); + + std::string hostAddressFromSettings = getHostAddressFromSettings(); + ui16 hostPortFromSettings = getHostPortFromSettings(); + + logNetwork->info("Host settings %s:%d", hostAddressFromSettings, hostPortFromSettings); - std::string hostAddress = getHostAddress(); - ui16 hostPort = getHostPort(); + std::string connectionAddress = addr.size() ? addr : hostAddressFromSettings; + ui16 connectionPort = port ? port : hostPortFromSettings; boost::chrono::duration> sleepDuration{}; int maxConnectionAttempts; - if(hostAddress == "127.0.0.1" || hostAddress == "localhost") + if(connectionAddress == "127.0.0.1" || connectionAddress == "localhost") { + logNetwork->info("Local server"); sleepDuration = boost::chrono::milliseconds(10); maxConnectionAttempts = 1000; } else { - // remote server + logNetwork->info("Remote server"); sleepDuration = boost::chrono::seconds(2); maxConnectionAttempts = 10; } - logNetwork->info("\nWaiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); + logNetwork->info("Waiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); ui16 connectionAttemptCount = 0; while(!c && state != EClientState::CONNECTION_CANCELLED) @@ -287,8 +293,8 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po try { c = std::make_shared( - addr.size() ? addr : hostAddress, - port ? port : hostPort, + connectionAddress, + connectionPort, NAME, uuid); } catch(std::runtime_error & error) @@ -305,12 +311,12 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); - if(!addr.empty() && addr != getHostAddress()) + if(!addr.empty() && addr != getHostAddressFromSettings()) { Settings serverAddress = settings.write["server"]["server"]; serverAddress->String() = addr; } - if(port && port != getHostPort()) + if(port && port != getHostPortFromSettings()) { Settings serverPort = settings.write["server"]["port"]; serverPort->Integer() = port; @@ -394,7 +400,7 @@ std::string CServerHandler::getDefaultPortStr() return std::to_string(getDefaultPort()); } -std::string CServerHandler::getHostAddress() const +std::string CServerHandler::getHostAddressFromSettings() const { if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) return settings["server"]["server"].String(); @@ -405,7 +411,7 @@ std::string CServerHandler::getHostAddress() const return settings["session"]["address"].String(); } -ui16 CServerHandler::getHostPort() const +ui16 CServerHandler::getHostPortFromSettings() const { if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) return getDefaultPort(); @@ -995,7 +1001,7 @@ void CServerHandler::threadRunServer() setThreadName("runServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() - + " --port=" + std::to_string(getHostPort()) + + " --port=" + std::to_string(getHostPortFromSettings()) + " --run-by-client" + " --uuid=" + uuid; if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 4572d5fb8..1607e97cf 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -121,8 +121,8 @@ public: CServerHandler(); - std::string getHostAddress() const; - ui16 getHostPort() const; + std::string getHostAddressFromSettings() const; + ui16 getHostPortFromSettings() const; void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); void startLocalServerAndConnect(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 34bda33d8..a8d6ef363 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -534,8 +534,8 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->giveFocus(); } - inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); - inputPort->setText(std::to_string(CSH->getHostPort()), true); + inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddressFromSettings(), true); + inputPort->setText(std::to_string(CSH->getHostPortFromSettings()), true); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); From 732c39fcc6dc4e33971de74dc83eee8fbdbc7c38 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 7 Oct 2023 00:08:22 +0200 Subject: [PATCH 0741/1248] Fix build --- client/CServerHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 8efdbfdd3..6c65bec79 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -183,7 +183,7 @@ void CServerHandler::startLocalServerAndConnect() #if defined(SINGLE_PROCESS_APP) boost::condition_variable cond; - std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPort())}; + std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPortFromSettings())}; if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) { args.push_back("--lobby=" + settings["session"]["address"].String()); From 42bf5fdd5889679b13e5651c73db1b2f977cc419 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 14:31:49 +0200 Subject: [PATCH 0742/1248] video fix --- client/CVideoHandler.h | 4 +++ client/battle/BattleInterfaceClasses.cpp | 41 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 3 deletions(-) diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index d447d64e6..cda410c44 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -23,6 +23,7 @@ public: virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer + virtual VideoPath videoName()=0; virtual bool wait()=0; virtual int curFrame() const =0; virtual int frameCount() const =0; @@ -46,6 +47,7 @@ public: void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; bool nextFrame() override {return false;}; + VideoPath videoName() override {return VideoPath();}; void close() override {}; bool wait() override {return false;}; bool open(const VideoPath & name, bool scale = false) override {return false;}; @@ -106,6 +108,8 @@ public: // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; + VideoPath videoName() override {return fname;}; + //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index a9bc54bfe..d41dce25e 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -566,9 +566,16 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won { int text = 304; + AudioPath musicName = AudioPath::builtin("Music/Win Battle"); + VideoPath videoName = VideoPath::builtin("WIN3.BIK"); switch(br.result) { case EBattleResult::NORMAL: + if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) + { + musicName = AudioPath::builtin("Music/Defend Castle"); + videoName = VideoPath::builtin("DEFENDALL.BIK"); + } break; case EBattleResult::ESCAPE: text = 303; @@ -581,8 +588,8 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface break; } - CCS->musich->playMusic(AudioPath::builtin("Music/Win Battle"), false, true); - CCS->videoh->open(VideoPath::builtin("WIN3.BIK")); + CCS->musich->playMusic(musicName, false, true); + CCS->videoh->open(videoName); std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); @@ -603,6 +610,11 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface switch(br.result) { case EBattleResult::NORMAL: + if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) + { + musicName = AudioPath::builtin("Music/LoseCastle"); + videoName = VideoPath::builtin("LOSECSTL.BIK"); + } break; case EBattleResult::ESCAPE: musicName = AudioPath::builtin("Music/Retreat Battle"); @@ -634,7 +646,30 @@ void BattleResultWindow::activate() void BattleResultWindow::show(Canvas & to) { CIntObject::show(to); - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false); + CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, + [&]() + { + if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/LBSTART")) + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin("VIDEO/LBLOOP")); + } + if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/RTSTART")) + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin("VIDEO/RTLOOP")); + } + if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/LOSECSTL")) + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin("VIDEO/LOSECSLP")); + } + if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/DEFENDALL")) + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin("VIDEO/DEFENDLOOP")); + } + }); } void BattleResultWindow::buttonPressed(int button) From 63fbaa8380544ab2903340991dfbaa81e0c47ee7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 16:09:56 +0200 Subject: [PATCH 0743/1248] support raw audio --- client/CMusicHandler.cpp | 49 ++++++++++++++++++++++++++++++++++++++++ client/CMusicHandler.h | 3 +++ 2 files changed, 52 insertions(+) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 273404229..9d64e37ba 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -142,6 +142,30 @@ Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) } } +Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) +{ + try + { + std::vector startBytes = std::vector(data.first.get(), data.first.get() + 100); + + if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) + return soundChunksRaw[startBytes].first; + + SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); + Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if (cache) + soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); + + return chunk; + } + catch(std::exception &e) + { + logGlobal->warn("Cannot get sound chunk: %s", e.what()); + return nullptr; + } +} + int CSoundHandler::ambientDistToVolume(int distance) const { const auto & distancesVector = ambientConfig["distances"].Vector(); @@ -197,6 +221,31 @@ int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) return channel; } +int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) +{ + int channel; + Mix_Chunk *chunk = GetSoundChunk(data, cache); + + if (chunk) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if (channel == -1) + { + logGlobal->error("Unable to play sound, error %s", Mix_GetError()); + if (!cache) + Mix_FreeChunk(chunk); + } + else if (cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + else + channel = -1; + + return channel; +} + // Helper. Randomly select a sound from an array and play it int CSoundHandler::playSoundFromSet(std::vector &sound_vec) { diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 56e1e3d7e..28daaf97a 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -41,8 +41,10 @@ private: using CachedChunk = std::pair>; std::map soundChunks; + std::map, CachedChunk> soundChunksRaw; Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); /// have entry for every currently active channel /// vector will be empty if callback was not set @@ -76,6 +78,7 @@ public: // Sounds int playSound(soundBase::soundID soundID, int repeats=0); int playSound(const AudioPath & sound, int repeats=0, bool cache=false); + int playSound(std::pair, si64> & data, int repeats, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); From 628de69a63316c75e8d04c89f80d838f78243c07 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 17:19:10 +0200 Subject: [PATCH 0744/1248] audio --- client/CVideoHandler.cpp | 70 ++++++++++++++++++++++++++++++++++++++++ client/CVideoHandler.h | 9 ++++++ 2 files changed, 79 insertions(+) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 10bd54bf0..8ba0ddfc1 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -127,6 +127,17 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla } } + // Find the first audio stream + streamAudio = -1; + for(ui32 i=0; inb_streams; i++) + { + if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + { + streamAudio = i; + break; + } + } + if (stream < 0) // No video stream in that file return false; @@ -134,6 +145,12 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla // Find the decoder for the video stream codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); + if(streamAudio > -1) + { + // Find the decoder for the audio stream + codecAudio = avcodec_find_decoder(format->streams[streamAudio]->codecpar->codec_id); + } + if (codec == nullptr) { // Unsupported codec @@ -143,6 +160,10 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla codecContext = avcodec_alloc_context3(codec); if(!codecContext) return false; + + if (codecAudio != nullptr) + codecContextAudio = avcodec_alloc_context3(codecAudio); + // Get a pointer to the codec context for the video stream int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); if (ret < 0) @@ -152,6 +173,17 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla return false; } + // Get a pointer to the codec context for the audio stream + if (streamAudio > -1) + { + ret = avcodec_parameters_to_context(codecContextAudio, format->streams[streamAudio]->codecpar); + if (ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContextAudio); + } + } + // Open codec if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) { @@ -162,6 +194,18 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla // Allocate video frame frame = av_frame_alloc(); + // Open codec + if (codecAudio != nullptr) + { + if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) + { + // Could not open codec + codecAudio = nullptr; + } + // Allocate audio frame + frameAudio = av_frame_alloc(); + } + //setup scaling if(scale) { @@ -231,6 +275,8 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla if (sws == nullptr) return false; + playVideoAudio(); + return true; } @@ -435,6 +481,30 @@ void CVideoPlayer::close() } } +void CVideoPlayer::playVideoAudio() +{ + AVPacket packet; + std::pair, si64> data; + + std::vector samples; + while (av_read_frame(format, &packet) >= 0) + { + avcodec_send_packet(codecContextAudio, &packet); + avcodec_receive_frame(codecContextAudio, frameAudio); + + data.second = frameAudio->linesize[0]; + data.first = (new ui8[data.second]); + + for (int s = 0; s < frameAudio->linesize[0]; s+=sizeof(ui8)) + { + ui8 value; + memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); + samples.push_back(value); + data.first. + } + } +} + // Plays a video. Only works for overlays. bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) { diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index d447d64e6..155a1de57 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -23,6 +23,7 @@ public: virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer + virtual VideoPath videoName()=0; virtual bool wait()=0; virtual int curFrame() const =0; virtual int frameCount() const =0; @@ -46,6 +47,7 @@ public: void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; bool nextFrame() override {return false;}; + VideoPath videoName() override {return VideoPath();}; void close() override {}; bool wait() override {return false;}; bool open(const VideoPath & name, bool scale = false) override {return false;}; @@ -66,10 +68,14 @@ VCMI_LIB_NAMESPACE_END class CVideoPlayer : public IMainVideoPlayer { int stream; // stream index in video + int streamAudio; // stream index in audio AVFormatContext *format; AVCodecContext *codecContext; // codec context for stream + AVCodecContext *codecContextAudio; // codec context for stream const AVCodec *codec; + const AVCodec *codecAudio; AVFrame *frame; + AVFrame *frameAudio; struct SwsContext *sws; AVIOContext * context; @@ -90,6 +96,7 @@ class CVideoPlayer : public IMainVideoPlayer bool playVideo(int x, int y, bool stopOnKey); bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); + void playVideoAudio(); public: CVideoPlayer(); ~CVideoPlayer(); @@ -106,6 +113,8 @@ public: // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; + VideoPath videoName() override {return fname;}; + //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; From 8de4b88a1b6270547b8fde484f5deabef29fbd32 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 20:09:40 +0200 Subject: [PATCH 0745/1248] basically playing --- client/CMT.cpp | 2 + client/CMusicHandler.cpp | 3 +- client/CMusicHandler.h | 2 +- client/CVideoHandler.cpp | 182 +++++++++++++++++++++++++++------------ client/CVideoHandler.h | 10 +-- 5 files changed, 135 insertions(+), 64 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 84d808dc5..4f149bc9c 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -423,6 +423,8 @@ int main(int argc, char * argv[]) //plays intro, ends when intro is over or button has been pressed (handles events) void playIntro() { + auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); + CCS->soundh->playSound(audioData); if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) { if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 9d64e37ba..41a053df0 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -151,8 +151,7 @@ Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) return soundChunksRaw[startBytes].first; - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + Mix_Chunk *chunk = Mix_QuickLoad_RAW(data.first.get(), (int)data.second); if (cache) soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 28daaf97a..0719d5d9b 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -78,7 +78,7 @@ public: // Sounds int playSound(soundBase::soundID soundID, int repeats=0); int playSound(const AudioPath & sound, int repeats=0, bool cache=false); - int playSound(std::pair, si64> & data, int repeats, bool cache=false); + int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); int playSoundFromSet(std::vector &sound_vec); void stopSound(int handler); diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 8ba0ddfc1..3f08fb5a0 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -55,6 +55,24 @@ static si64 lodSeek(void * opaque, si64 pos, int whence) return video->data->seek(pos); } +// Define a set of functions to read data +static int lodReadAudio(void* opaque, uint8_t* buf, int size) +{ + auto video = reinterpret_cast(opaque); + + return static_cast(video->dataAudio->read(buf, size)); +} + +static si64 lodSeekAudio(void * opaque, si64 pos, int whence) +{ + auto video = reinterpret_cast(opaque); + + if (whence & AVSEEK_SIZE) + return video->dataAudio->getSize(); + + return video->dataAudio->seek(pos); +} + CVideoPlayer::CVideoPlayer() : stream(-1) , format (nullptr) @@ -127,17 +145,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla } } - // Find the first audio stream - streamAudio = -1; - for(ui32 i=0; inb_streams; i++) - { - if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - streamAudio = i; - break; - } - } - if (stream < 0) // No video stream in that file return false; @@ -145,12 +152,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla // Find the decoder for the video stream codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); - if(streamAudio > -1) - { - // Find the decoder for the audio stream - codecAudio = avcodec_find_decoder(format->streams[streamAudio]->codecpar->codec_id); - } - if (codec == nullptr) { // Unsupported codec @@ -160,10 +161,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla codecContext = avcodec_alloc_context3(codec); if(!codecContext) return false; - - if (codecAudio != nullptr) - codecContextAudio = avcodec_alloc_context3(codecAudio); - // Get a pointer to the codec context for the video stream int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); if (ret < 0) @@ -173,17 +170,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla return false; } - // Get a pointer to the codec context for the audio stream - if (streamAudio > -1) - { - ret = avcodec_parameters_to_context(codecContextAudio, format->streams[streamAudio]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContextAudio); - } - } - // Open codec if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) { @@ -194,18 +180,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla // Allocate video frame frame = av_frame_alloc(); - // Open codec - if (codecAudio != nullptr) - { - if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) - { - // Could not open codec - codecAudio = nullptr; - } - // Allocate audio frame - frameAudio = av_frame_alloc(); - } - //setup scaling if(scale) { @@ -275,8 +249,6 @@ bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverla if (sws == nullptr) return false; - playVideoAudio(); - return true; } @@ -481,28 +453,128 @@ void CVideoPlayer::close() } } -void CVideoPlayer::playVideoAudio() +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) { - AVPacket packet; - std::pair, si64> data; + std::pair, si64> dat(std::make_pair(std::make_unique(0), 0)); - std::vector samples; - while (av_read_frame(format, &packet) >= 0) + VideoPath fnameAudio; + + if (CResourceHandler::get()->existsResource(videoToOpen)) + fnameAudio = videoToOpen; + else + fnameAudio = videoToOpen.addPrefix("VIDEO/"); + + if (!CResourceHandler::get()->existsResource(fnameAudio)) + { + logGlobal->error("Error: video %s was not found", fnameAudio.getName()); + return dat; + } + + dataAudio = CResourceHandler::get()->load(fnameAudio); + + static const int BUFFER_SIZE = 4096; + + unsigned char * bufferAudio = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg + AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio); + + AVFormatContext * formatAudio = avformat_alloc_context(); + formatAudio->pb = contextAudio; + // filename is not needed - file was already open and stored in this->data; + int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr); + + if (avfopen != 0) + { + return dat; + } + // Retrieve stream information + if (avformat_find_stream_info(formatAudio, nullptr) < 0) + return dat; + + // Find the first audio stream + int streamAudio = -1; + for(ui32 i=0; inb_streams; i++) + { + if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + { + streamAudio = i; + break; + } + } + + if(streamAudio < 0) + return dat; + + const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id); + + AVCodecContext *codecContextAudio; + if (codecAudio != nullptr) + codecContextAudio = avcodec_alloc_context3(codecAudio); + + // Get a pointer to the codec context for the audio stream + if (streamAudio > -1) + { + int ret = 0; + ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); + if (ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContextAudio); + } + } + + // Open codec + AVFrame *frameAudio; + if (codecAudio != nullptr) + { + if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) + { + // Could not open codec + codecAudio = nullptr; + } + // Allocate audio frame + frameAudio = av_frame_alloc(); + } + + AVPacket packet; + + std::vector samples; + while (av_read_frame(formatAudio, &packet) >= 0) { avcodec_send_packet(codecContextAudio, &packet); avcodec_receive_frame(codecContextAudio, frameAudio); - data.second = frameAudio->linesize[0]; - data.first = (new ui8[data.second]); - for (int s = 0; s < frameAudio->linesize[0]; s+=sizeof(ui8)) { ui8 value; memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); samples.push_back(value); - data.first. } } + + dat = std::pair, si64>(std::make_pair(std::make_unique(samples.size()), samples.size())); + std::copy(samples.begin(), samples.end(), dat.first.get()); + + if (frameAudio) + av_frame_free(&frameAudio); + + if (codecAudio) + { + avcodec_close(codecContextAudio); + codecAudio = nullptr; + } + if (codecContextAudio) + avcodec_free_context(&codecContextAudio); + + if (formatAudio) + avformat_close_input(&formatAudio); + + if (contextAudio) + { + av_free(contextAudio); + contextAudio = nullptr; + } + + return dat; } // Plays a video. Only works for overlays. diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 155a1de57..c2f4471d4 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -37,6 +37,7 @@ public: { return false; } + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::pair, si64>(std::make_pair(std::make_unique(0), 0)); }; }; class CEmptyVideoPlayer : public IMainVideoPlayer @@ -68,14 +69,10 @@ VCMI_LIB_NAMESPACE_END class CVideoPlayer : public IMainVideoPlayer { int stream; // stream index in video - int streamAudio; // stream index in audio AVFormatContext *format; AVCodecContext *codecContext; // codec context for stream - AVCodecContext *codecContextAudio; // codec context for stream const AVCodec *codec; - const AVCodec *codecAudio; AVFrame *frame; - AVFrame *frameAudio; struct SwsContext *sws; AVIOContext * context; @@ -95,8 +92,6 @@ class CVideoPlayer : public IMainVideoPlayer bool playVideo(int x, int y, bool stopOnKey); bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); - - void playVideoAudio(); public: CVideoPlayer(); ~CVideoPlayer(); @@ -113,6 +108,8 @@ public: // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; + std::pair, si64> getAudio(const VideoPath & videoToOpen) override; + VideoPath videoName() override {return fname;}; //TODO: @@ -122,6 +119,7 @@ public: // public to allow access from ffmpeg IO functions std::unique_ptr data; + std::unique_ptr dataAudio; }; #endif From 2bbce0f38b3cc882523a3ee1fb11551e37b8585c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 22:20:39 +0200 Subject: [PATCH 0746/1248] decoding working --- client/CVideoHandler.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 3f08fb5a0..abe4dddb8 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -538,6 +538,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath AVPacket packet; std::vector samples; + while (av_read_frame(formatAudio, &packet) >= 0) { avcodec_send_packet(codecContextAudio, &packet); @@ -551,8 +552,30 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath } } - dat = std::pair, si64>(std::make_pair(std::make_unique(samples.size()), samples.size())); - std::copy(samples.begin(), samples.end(), dat.first.get()); + typedef struct WAV_HEADER { + ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; + ui32 ChunkSize; + ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; + ui8 fmt[4] = {'f', 'm', 't', ' '}; + ui32 Subchunk1Size = 16; + ui16 AudioFormat = 1; + ui16 NumOfChan = 2; + ui32 SamplesPerSec = 22050; + ui32 bytesPerSec = 22050 * 2; + ui16 blockAlign = 2; + ui16 bitsPerSample = 16; + ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; + ui32 Subchunk2Size; + } wav_hdr; + + wav_hdr wav; + wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; + wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; + auto wavPtr = reinterpret_cast(&wav); + + dat = std::pair, si64>(std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr))); + std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); + std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); if (frameAudio) av_frame_free(&frameAudio); From be2b3afe48ee20e1de5e37989e90e56b2c67afd6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 23:07:10 +0200 Subject: [PATCH 0747/1248] implements sounds --- client/CMT.cpp | 9 ++++++++- client/CMusicHandler.cpp | 3 ++- client/CVideoHandler.cpp | 2 ++ client/mainmenu/CHighScoreScreen.cpp | 5 +++++ client/mainmenu/CHighScoreScreen.h | 1 + client/mainmenu/CPrologEpilogVideo.cpp | 3 +++ client/mainmenu/CPrologEpilogVideo.h | 1 + 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 4f149bc9c..041cbaaa9 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -424,12 +424,19 @@ int main(int argc, char * argv[]) void playIntro() { auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); - CCS->soundh->playSound(audioData); + int sound = CCS->soundh->playSound(audioData); if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) { + audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); + sound = CCS->soundh->playSound(audioData); if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) + { + audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); + sound = CCS->soundh->playSound(audioData); CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); + } } + CCS->soundh->stopSound(sound); } static void mainLoop() diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 41a053df0..9d64e37ba 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -151,7 +151,8 @@ Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) return soundChunksRaw[startBytes].first; - Mix_Chunk *chunk = Mix_QuickLoad_RAW(data.first.get(), (int)data.second); + SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); + Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops if (cache) soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index abe4dddb8..45999480c 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -571,6 +571,8 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath wav_hdr wav; wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; + wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate; + wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; auto wavPtr = reinterpret_cast(&wav); dat = std::pair, si64>(std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr))); diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index 9d39343a5..e21f2c4f8 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -295,6 +295,8 @@ void CHighScoreInputScreen::show(Canvas & to) { CCS->videoh->close(); video = "HSLOOP.SMK"; + auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); + sound = CCS->soundh->playSound(audioData); CCS->videoh->open(VideoPath::builtin(video)); } else @@ -307,6 +309,8 @@ void CHighScoreInputScreen::show(Canvas & to) void CHighScoreInputScreen::activate() { + auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); + sound = CCS->soundh->playSound(audioData); if(!CCS->videoh->open(VideoPath::builtin(video))) { if(!won) @@ -320,6 +324,7 @@ void CHighScoreInputScreen::activate() void CHighScoreInputScreen::deactivate() { CCS->videoh->close(); + CCS->soundh->stopSound(sound); CIntObject::deactivate(); } diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 89e4bdf21..84083fcc2 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -95,6 +95,7 @@ class CHighScoreInputScreen : public CWindowObject std::shared_ptr background; std::string video; + int sound; bool won; HighScoreCalculation calc; public: diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 33231d4da..8db91c229 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -27,6 +27,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f pos = center(Rect(0, 0, 800, 600)); updateShadow(); + auto audioData = CCS->videoh->getAudio(spe.prologVideo); + sound = CCS->soundh->playSound(audioData); CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); @@ -62,6 +64,7 @@ void CPrologEpilogVideo::show(Canvas & to) void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) { close(); + CCS->soundh->stopSound(sound); CCS->soundh->stopSound(voiceSoundHandle); exitCb(); } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 756c2f5e7..2e863227f 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -23,6 +23,7 @@ class CPrologEpilogVideo : public CWindowObject std::shared_ptr text; + int sound = 0; bool voiceStopped = false; public: From 3c647334b08e003b844be3e58941cb3c81b83c62 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 7 Oct 2023 23:49:58 +0200 Subject: [PATCH 0748/1248] remove other pr --- client/CVideoHandler.h | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index c2f4471d4..f4b3ad0f0 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -23,7 +23,6 @@ public: virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer - virtual VideoPath videoName()=0; virtual bool wait()=0; virtual int curFrame() const =0; virtual int frameCount() const =0; @@ -48,7 +47,6 @@ public: void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; bool nextFrame() override {return false;}; - VideoPath videoName() override {return VideoPath();}; void close() override {}; bool wait() override {return false;}; bool open(const VideoPath & name, bool scale = false) override {return false;}; @@ -110,8 +108,6 @@ public: std::pair, si64> getAudio(const VideoPath & videoToOpen) override; - VideoPath videoName() override {return fname;}; - //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; From 39e06ea5e1eb70a41f43e3e55df77a59e2e4a900 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 02:37:54 +0200 Subject: [PATCH 0749/1248] fix error message --- client/CVideoHandler.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 45999480c..feb55a916 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -541,15 +541,22 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath while (av_read_frame(formatAudio, &packet) >= 0) { - avcodec_send_packet(codecContextAudio, &packet); - avcodec_receive_frame(codecContextAudio, frameAudio); - - for (int s = 0; s < frameAudio->linesize[0]; s+=sizeof(ui8)) + if(packet.stream_index == streamAudio) { - ui8 value; - memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); - samples.push_back(value); + int rc = avcodec_send_packet(codecContextAudio, &packet); + if (rc >= 0) + packet.size = 0; + rc = avcodec_receive_frame(codecContextAudio, frameAudio); + if (rc >= 0) + for (int s = 0; s < frameAudio->linesize[0]; s+=sizeof(ui8)) + { + ui8 value; + memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); + samples.push_back(value); + } } + + av_packet_unref(&packet); } typedef struct WAV_HEADER { From 66ff603456682792bbb12864ac2273032856515b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 03:31:49 +0200 Subject: [PATCH 0750/1248] remove crackle --- client/CVideoHandler.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index feb55a916..1624932a6 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -547,8 +547,9 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath if (rc >= 0) packet.size = 0; rc = avcodec_receive_frame(codecContextAudio, frameAudio); + int bytesToRead = (frameAudio->nb_samples * formatAudio->streams[streamAudio]->codecpar->ch_layout.nb_channels * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample/8)); if (rc >= 0) - for (int s = 0; s < frameAudio->linesize[0]; s+=sizeof(ui8)) + for (int s = 0; s < bytesToRead; s+=sizeof(ui8)) { ui8 value; memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); From 639b6c657732e4087ac1378aac76b80ea24baa5b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 03:54:40 +0200 Subject: [PATCH 0751/1248] version --- client/CVideoHandler.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 1624932a6..dbe9e1043 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -547,7 +547,11 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath if (rc >= 0) packet.size = 0; rc = avcodec_receive_frame(codecContextAudio, frameAudio); +#if (LIBAVUTIL_VERSION_MAJOR < 57) + int bytesToRead = (frameAudio->nb_samples * formatAudio->streams[streamAudio]->codecpar->channels * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample/8)); +#else int bytesToRead = (frameAudio->nb_samples * formatAudio->streams[streamAudio]->codecpar->ch_layout.nb_channels * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample/8)); +#endif if (rc >= 0) for (int s = 0; s < bytesToRead; s+=sizeof(ui8)) { From 7b7338d7eac25a41e00b2f43480eb1ef9110fcc2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:02:49 +0200 Subject: [PATCH 0752/1248] install firewall rules --- CMakeLists.txt | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index d711ceec9..ae364c2c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -713,12 +713,22 @@ if(WIN32) endif() if(ENABLE_LAUNCHER) set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\"") + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\" + ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE + ") else() set(CPACK_PACKAGE_EXECUTABLES "VCMI_client;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_client.exe\\\"") + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_client.exe\\\" + ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE + ") + endif() - set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ") + set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " + Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" + ExecShell '' 'netsh' 'advfirewall firewall delete rule name=vcmi_server' SW_HIDE + ") # Strip MinGW CPack target if build configuration without debug info if(MINGW) From a1f6e49f8761176dba2cdf0d36bd8a99e34e3b5f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 14:09:07 +0200 Subject: [PATCH 0753/1248] vcmi always uses only 2 channels --- client/CVideoHandler.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index dbe9e1043..0033e5507 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -547,11 +547,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath if (rc >= 0) packet.size = 0; rc = avcodec_receive_frame(codecContextAudio, frameAudio); -#if (LIBAVUTIL_VERSION_MAJOR < 57) - int bytesToRead = (frameAudio->nb_samples * formatAudio->streams[streamAudio]->codecpar->channels * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample/8)); -#else - int bytesToRead = (frameAudio->nb_samples * formatAudio->streams[streamAudio]->codecpar->ch_layout.nb_channels * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample/8)); -#endif + int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); if (rc >= 0) for (int s = 0; s < bytesToRead; s+=sizeof(ui8)) { From d7d435dcb7cd898cda1923fa93fb6937dba92d98 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 7 Oct 2023 01:44:37 +0200 Subject: [PATCH 0754/1248] Add game pause --- CCallback.cpp | 13 +++++++++++++ CCallback.h | 2 ++ client/CPlayerInterface.cpp | 5 +++++ client/CPlayerInterface.h | 1 + client/gui/CIntObject.h | 2 +- client/lobby/CSavingScreen.cpp | 8 ++++++++ client/lobby/CSavingScreen.h | 3 +++ client/windows/CCastleInterface.h | 2 +- client/windows/CTradeWindow.h | 2 +- client/windows/GUIClasses.h | 6 +++--- client/windows/InfoWindows.h | 4 ++-- client/windows/settings/SettingsMainWindow.cpp | 4 ++++ lib/NetPackVisitor.h | 1 + lib/NetPacks.h | 10 ++++++++++ lib/NetPacksLib.cpp | 5 +++++ lib/constants/EntityIdentifiers.cpp | 1 + lib/constants/EntityIdentifiers.h | 1 + lib/registerTypes/RegisterTypes.h | 1 + server/NetPacksServer.cpp | 9 +++++++++ server/ServerNetPackVisitors.h | 1 + server/processors/TurnOrderProcessor.cpp | 2 +- server/queries/MapQueries.cpp | 10 +++++----- server/queries/MapQueries.h | 8 ++++---- 23 files changed, 83 insertions(+), 18 deletions(-) diff --git a/CCallback.cpp b/CCallback.cpp index c63d22e2a..4a9348bec 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -274,6 +274,19 @@ void CCallback::save( const std::string &fname ) cl->save(fname); } +void CCallback::gamePause(bool pause) +{ + if(pause) + { + GamePause pack; + pack.player = *player; + sendRequest(&pack); + } + else + { + sendQueryReply(0, QueryID::CLIENT); + } +} void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) { diff --git a/CCallback.h b/CCallback.h index 31a183dd7..d31f5415e 100644 --- a/CCallback.h +++ b/CCallback.h @@ -98,6 +98,7 @@ public: virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; + virtual void gamePause(bool pause) = 0; virtual void buildBoat(const IShipyard *obj) = 0; // To implement high-level army management bulk actions @@ -186,6 +187,7 @@ public: void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; + void gamePause(bool pause) override; void buildBoat(const IShipyard *obj) override; void dig(const CGObjectInstance *hero) override; void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index d5fb16329..ea49a74fd 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -265,6 +265,11 @@ void CPlayerInterface::performAutosave() } } +void CPlayerInterface::gamePause(bool pause) +{ + cb->gamePause(pause); +} + void CPlayerInterface::yourTurn(QueryID queryID) { EVENT_HANDLER_CALLED_BY_CLIENT; diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 62f38b026..65041ae67 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -200,6 +200,7 @@ public: // public interface for use by client via LOCPLINT access void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; void proposeLoadingGame(); void performAutosave(); + void gamePause(bool pause); ///returns true if all events are processed internally bool capturedAllEvents(); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 916891b01..5833ddac3 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -144,7 +144,7 @@ class WindowBase : public CIntObject public: WindowBase(int used_ = 0, Point pos_ = Point()); protected: - void close(); + virtual void close(); }; class IGarrisonHolder diff --git a/client/lobby/CSavingScreen.cpp b/client/lobby/CSavingScreen.cpp index 5a43f2fc3..72c18b300 100644 --- a/client/lobby/CSavingScreen.cpp +++ b/client/lobby/CSavingScreen.cpp @@ -41,6 +41,8 @@ CSavingScreen::CSavingScreen() curTab = tabSel; buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRSAV.DEF"), CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME); + + LOCPLINT->gamePause(true); } const CMapInfo * CSavingScreen::getMapInfo() @@ -65,6 +67,12 @@ void CSavingScreen::changeSelection(std::shared_ptr to) card->redraw(); } +void CSavingScreen::close() +{ + LOCPLINT->gamePause(false); + CSelectionBase::close(); +} + void CSavingScreen::saveGame() { if(!(tabSel && tabSel->inputName && tabSel->inputName->getText().size())) diff --git a/client/lobby/CSavingScreen.h b/client/lobby/CSavingScreen.h index e99809407..17f637b74 100644 --- a/client/lobby/CSavingScreen.h +++ b/client/lobby/CSavingScreen.h @@ -32,4 +32,7 @@ public: const CMapInfo * getMapInfo() override; const StartInfo * getStartInfo() override; + +protected: + void close() override; }; diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 353d96542..a08e65361 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -250,7 +250,7 @@ public: void townChange(); void keyPressed(EShortcut key) override; - void close(); + void close() override; void addBuilding(BuildingID bid); void removeBuilding(BuildingID bid); void recreateIcons(); diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index b097ebf76..45a8cfee5 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -86,7 +86,7 @@ public: CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); //c void showAll(Canvas & to) override; - void close(); + void close() override; void initSubs(bool Left); void initTypes(); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 6df706715..22233740a 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -90,7 +90,7 @@ public: const CGDwelling * const dwelling; CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset = 0); void availableCreaturesChanged(); - void close(); + void close() override; }; /// Split window where creatures can be split up into two single unit stacks @@ -240,7 +240,7 @@ public: CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); ~CTavernWindow(); - void close(); + void close() override; void recruitb(); void thievesguildb(); void show(Canvas & to) override; @@ -360,7 +360,7 @@ public: void makeDeal(); void addAll(); - void close(); + void close() override; void updateGarrisons() override; CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); }; diff --git a/client/windows/InfoWindows.h b/client/windows/InfoWindows.h index ff6b880da..eea665326 100644 --- a/client/windows/InfoWindows.h +++ b/client/windows/InfoWindows.h @@ -56,7 +56,7 @@ public: std::vector> buttons; TCompsInfo components; - virtual void close(); + void close() override; void show(Canvas & to) override; void showAll(Canvas & to) override; @@ -79,7 +79,7 @@ public: class CRClickPopup : public WindowBase { public: - virtual void close(); + virtual void close() override; bool isPopupWindow() const override; static std::shared_ptr createCustomInfoWindow(Point position, const CGObjectInstance * specific); diff --git a/client/windows/settings/SettingsMainWindow.cpp b/client/windows/settings/SettingsMainWindow.cpp index 3ced217de..d122c8a02 100644 --- a/client/windows/settings/SettingsMainWindow.cpp +++ b/client/windows/settings/SettingsMainWindow.cpp @@ -75,6 +75,8 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter std::shared_ptr mainTabs = widget("settingsTabs"); mainTabs->setSelected(defaultTabIndex); + + LOCPLINT->gamePause(true); } std::shared_ptr SettingsMainWindow::createTab(size_t index) @@ -108,6 +110,8 @@ void SettingsMainWindow::close() { if(!GH.windows().isTopWindow(this)) logGlobal->error("Only top interface must be closed"); + + LOCPLINT->gamePause(false); GH.windows().popWindows(1); } diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index c32b24779..676e64698 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -29,6 +29,7 @@ public: virtual void visitPlayerStartsTurn(PlayerStartsTurn & pack) {} virtual void visitDaysWithoutTown(DaysWithoutTown & pack) {} virtual void visitTurnTimeUpdate(TurnTimeUpdate & pack) {} + virtual void visitGamePause(GamePause & pack) {} virtual void visitEntitiesChanged(EntitiesChanged & pack) {} virtual void visitSetResources(SetResources & pack) {} virtual void visitSetPrimSkill(SetPrimSkill & pack) {} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index d34a0f6c2..132e2a94e 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -2093,6 +2093,16 @@ protected: /***********************************************************************************************************/ +struct DLL_LINKAGE GamePause : public CPackForServer +{ + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + struct DLL_LINKAGE EndTurn : public CPackForServer { virtual void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index b31b44ab0..79d8ec0c9 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -520,6 +520,11 @@ void EndTurn::visitTyped(ICPackVisitor & visitor) visitor.visitEndTurn(*this); } +void GamePause::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGamePause(*this); +} + void DismissHero::visitTyped(ICPackVisitor & visitor) { visitor.visitDismissHero(*this); diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 26c5e630d..4fe197b5c 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -40,6 +40,7 @@ VCMI_LIB_NAMESPACE_BEGIN const BattleID BattleID::NONE(-1); const QueryID QueryID::NONE(-1); +const QueryID QueryID::CLIENT(-2); const HeroTypeID HeroTypeID::NONE(-1); const HeroTypeID HeroTypeID::RANDOM(-2); const ObjectInstanceID ObjectInstanceID::NONE(-1); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index cd4c1b35b..7545d5a2a 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -203,6 +203,7 @@ class QueryID : public Identifier public: using Identifier::Identifier; DLL_LINKAGE static const QueryID NONE; + DLL_LINKAGE static const QueryID CLIENT; }; class BattleID : public Identifier diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 3d2d1df56..28dcca012 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -367,6 +367,7 @@ void registerTypesServerPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); } template diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 32c5da7f7..3447272d7 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -16,6 +16,7 @@ #include "processors/PlayerMessageProcessor.h" #include "processors/TurnOrderProcessor.h" #include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" #include "../lib/IGameCallback.h" #include "../lib/mapObjects/CGTownInstance.h" @@ -35,6 +36,14 @@ void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) result = true; } +void ApplyGhNetPackVisitor::visitGamePause(GamePause & pack) +{ + auto turnQuery = std::make_shared(&gh, pack.player); + turnQuery->queryID = QueryID::CLIENT; + gh.queries->addQuery(turnQuery); + result = true; +} + void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) { gh.throwIfWrongPlayer(&pack); diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index e338cf00d..de1ec7ff6 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -29,6 +29,7 @@ public: } virtual void visitSaveGame(SaveGame & pack) override; + virtual void visitGamePause(GamePause & pack) override; virtual void visitEndTurn(EndTurn & pack) override; virtual void visitDismissHero(DismissHero & pack) override; virtual void visitMoveHero(MoveHero & pack) override; diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index d5eb1f0bd..b1359fadd 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -216,7 +216,7 @@ void TurnOrderProcessor::doStartPlayerTurn(PlayerColor which) awaitingPlayers.erase(which); gameHandler->onPlayerTurnStarted(which); - auto turnQuery = std::make_shared(gameHandler, which); + auto turnQuery = std::make_shared(gameHandler, which); gameHandler->queries->addQuery(turnQuery); PlayerStartsTurn pst; diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 78d5809dc..55acbe223 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -15,28 +15,28 @@ #include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/serializer/Cast.h" -PlayerStartsTurnQuery::PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player): +TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): CQuery(owner) { addPlayer(player); } -bool PlayerStartsTurnQuery::blocksPack(const CPack *pack) const +bool TimerPauseQuery::blocksPack(const CPack *pack) const { return blockAllButReply(pack); } -void PlayerStartsTurnQuery::onAdding(PlayerColor color) +void TimerPauseQuery::onAdding(PlayerColor color) { gh->turnTimerHandler.setTimerEnabled(color, false); } -void PlayerStartsTurnQuery::onRemoval(PlayerColor color) +void TimerPauseQuery::onRemoval(PlayerColor color) { gh->turnTimerHandler.setTimerEnabled(color, true); } -bool PlayerStartsTurnQuery::endsByPlayerAnswer() const +bool TimerPauseQuery::endsByPlayerAnswer() const { return true; } diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index dfda9061d..3db5889a3 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -15,12 +15,12 @@ class TurnTimerHandler; -//Created when player starts turn -//Removed when player accepts a turn -class PlayerStartsTurnQuery : public CQuery +//Created when player starts turn or when player puts game on [ause +//Removed when player accepts a turn or continur play +class TimerPauseQuery : public CQuery { public: - PlayerStartsTurnQuery(CGameHandler * owner, PlayerColor player); + TimerPauseQuery(CGameHandler * owner, PlayerColor player); bool blocksPack(const CPack *pack) const override; void onAdding(PlayerColor color) override; From 95f5b9c9b22a07ccfa801a496e15159c799fd1ac Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 16:28:23 +0200 Subject: [PATCH 0755/1248] suggestion --- CMakeLists.txt | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index ae364c2c5..c3964537c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -711,20 +711,17 @@ if(WIN32) else() set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES") endif() - if(ENABLE_LAUNCHER) - set(CPACK_PACKAGE_EXECUTABLES "VCMI_launcher;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " - CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_launcher.exe\\\" - ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE - ") - else() - set(CPACK_PACKAGE_EXECUTABLES "VCMI_client;VCMI") - set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " - CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\VCMI_client.exe\\\" - ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE - ") + if(ENABLE_LAUNCHER) + set(VCMI_MAIN_EXECUTABLE "VCMI_launcher") + else() + set(VCMI_MAIN_EXECUTABLE "VCMI_client") endif() + set(CPACK_PACKAGE_EXECUTABLES "${VCMI_MAIN_EXECUTABLE};VCMI") + set(CPACK_NSIS_EXTRA_INSTALL_COMMANDS " + CreateShortCut \\\"$DESKTOP\\\\VCMI.lnk\\\" \\\"$INSTDIR\\\\${VCMI_MAIN_EXECUTABLE}.exe\\\" + ExecShell '' 'netsh' 'advfirewall firewall add rule name=vcmi_server dir=in action=allow program=\\\"$INSTDIR\\\\vcmi_server.exe\\\" enable=yes profile=public,private' SW_HIDE + ") set(CPACK_NSIS_EXTRA_UNINSTALL_COMMANDS " Delete \\\"$DESKTOP\\\\VCMI.lnk\\\" ExecShell '' 'netsh' 'advfirewall firewall delete rule name=vcmi_server' SW_HIDE From fe7c01cd374c095b71c5eb1ca8945a7300fd3671 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 8 Oct 2023 17:46:39 +0200 Subject: [PATCH 0756/1248] Fix 3024 --- client/CServerHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 15c2d6299..3d73a22ef 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -924,7 +924,7 @@ void CServerHandler::threadHandleConnection() try { sendClientConnecting(); - while(c->connected) + while(c && c->connected) { while(state == EClientState::STARTING) boost::this_thread::sleep_for(boost::chrono::milliseconds(10)); From cfa1a6d880d5cc1c23ae1ab26ea392c59c2f596d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sun, 8 Oct 2023 20:25:59 +0200 Subject: [PATCH 0757/1248] Show coordinates on map --- mapeditor/mainwindow.cpp | 12 +++++++----- mapeditor/mainwindow.h | 1 + mapeditor/mapview.cpp | 3 +-- mapeditor/mapview.h | 1 + 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 6e7a72125..fac99c05b 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -213,6 +213,7 @@ MainWindow::MainWindow(QWidget* parent) : ui->mapView->setController(&controller); ui->mapView->setOptimizationFlags(QGraphicsView::DontSavePainterState | QGraphicsView::DontAdjustForAntialiasing); connect(ui->mapView, &MapView::openObjectProperties, this, &MainWindow::loadInspector); + connect(ui->mapView, &MapView::currentCoordinates, this, &MainWindow::currentCoordinatesChanged); ui->minimapView->setScene(controller.miniScene(0)); ui->minimapView->setController(&controller); @@ -296,12 +297,11 @@ void MainWindow::initializeMap(bool isNew) ui->minimapView->setScene(controller.miniScene(mapLevel)); ui->minimapView->dimensions(); - setStatusMessage(QString("Scene objects: %1").arg(ui->mapView->scene()->items().size())); - //enable settings ui->actionMapSettings->setEnabled(true); ui->actionPlayers_settings->setEnabled(true); ui->actionTranslations->setEnabled(true); + ui->actionLevel->setEnabled(controller.map()->twoLevel); //set minimal players count if(isNew) @@ -459,6 +459,11 @@ void MainWindow::on_actionSave_triggered() saveMap(); } +void MainWindow::currentCoordinatesChanged(int x, int y) +{ + setStatusMessage(QString("x: %1 y: %2").arg(x).arg(y)); +} + void MainWindow::terrainButtonClicked(TerrainId terrain) { controller.commitTerrainChange(mapLevel, terrain); @@ -1103,9 +1108,6 @@ void MainWindow::onSelectionMade(int level, bool anythingSelected) { if (level == mapLevel) { - auto info = QString::asprintf("Selection on layer %d: %s", level, anythingSelected ? "true" : "false"); - setStatusMessage(info); - ui->actionErase->setEnabled(anythingSelected); ui->toolErase->setEnabled(anythingSelected); } diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 6ff39d165..77c2c14f3 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -82,6 +82,7 @@ private slots: void terrainButtonClicked(TerrainId terrain); void roadOrRiverButtonClicked(ui8 type, bool isRoad); + void currentCoordinatesChanged(int x, int y); void on_toolErase_clicked(); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index dc2650884..3f0fa299b 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -98,8 +98,7 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) tilePrev = tile; - //TODO: cast parent->parent to MainWindow in order to show coordinates or another way to do it? - //main->setStatusMessage(QString("x: %1 y: %2").arg(tile.x, tile.y)); + emit currentCoordinates(tile.x, tile.y); switch(selectionTool) { diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index b5d8d0e1c..f9a83f133 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -111,6 +111,7 @@ public slots: signals: void openObjectProperties(CGObjectInstance *, bool switchTab); + void currentCoordinates(int, int); //void viewportChanged(const QRectF & rect); protected: From 89a39fcfc0834a9c01662999291c45ff8ef843b3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 20:27:56 +0200 Subject: [PATCH 0758/1248] code review --- client/CMusicHandler.cpp | 11 +++-------- client/CVideoHandler.cpp | 9 ++++----- client/CVideoHandler.h | 2 +- client/mainmenu/CHighScoreScreen.cpp | 8 ++++---- client/mainmenu/CHighScoreScreen.h | 2 +- client/mainmenu/CPrologEpilogVideo.cpp | 6 +++--- client/mainmenu/CPrologEpilogVideo.h | 2 +- 7 files changed, 17 insertions(+), 23 deletions(-) diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index 9d64e37ba..efbc41ce1 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -146,7 +146,7 @@ Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> { try { - std::vector startBytes = std::vector(data.first.get(), data.first.get() + 100); + std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) return soundChunksRaw[startBytes].first; @@ -223,10 +223,8 @@ int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) { - int channel; - Mix_Chunk *chunk = GetSoundChunk(data, cache); - - if (chunk) + int channel = -1; + if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) { channel = Mix_PlayChannel(-1, chunk, repeats); if (channel == -1) @@ -240,9 +238,6 @@ int CSoundHandler::playSound(std::pair, si64> & data, in else initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); } - else - channel = -1; - return channel; } diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 0033e5507..601864d49 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -492,7 +492,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath // Find the first audio stream int streamAudio = -1; - for(ui32 i=0; inb_streams; i++) + for(ui32 i = 0; i < formatAudio->nb_streams; i++) { if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { @@ -513,8 +513,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath // Get a pointer to the codec context for the audio stream if (streamAudio > -1) { - int ret = 0; - ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); + int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); if (ret < 0) { //We cannot get codec from parameters @@ -549,7 +548,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath rc = avcodec_receive_frame(codecContextAudio, frameAudio); int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); if (rc >= 0) - for (int s = 0; s < bytesToRead; s+=sizeof(ui8)) + for (int s = 0; s < bytesToRead; s += sizeof(ui8)) { ui8 value; memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); @@ -583,7 +582,7 @@ std::pair, si64> CVideoPlayer::getAudio(const VideoPath wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; auto wavPtr = reinterpret_cast(&wav); - dat = std::pair, si64>(std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr))); + dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index f4b3ad0f0..cb031a8e4 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -36,7 +36,7 @@ public: { return false; } - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::pair, si64>(std::make_pair(std::make_unique(0), 0)); }; + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(std::make_unique(0), 0); }; }; class CEmptyVideoPlayer : public IMainVideoPlayer diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index e21f2c4f8..d71e9148d 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -215,7 +215,7 @@ void CHighScoreScreen::buttonExitClick() } CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc) - : CWindowObject(BORDERED), won(won), calc(calc) + : CWindowObject(BORDERED), won(won), calc(calc), videoSoundHandle(-1) { addUsedEvents(LCLICK | KEYBOARD); @@ -296,7 +296,7 @@ void CHighScoreInputScreen::show(Canvas & to) CCS->videoh->close(); video = "HSLOOP.SMK"; auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - sound = CCS->soundh->playSound(audioData); + videoSoundHandle = CCS->soundh->playSound(audioData); CCS->videoh->open(VideoPath::builtin(video)); } else @@ -310,7 +310,7 @@ void CHighScoreInputScreen::show(Canvas & to) void CHighScoreInputScreen::activate() { auto audioData = CCS->videoh->getAudio(VideoPath::builtin(video)); - sound = CCS->soundh->playSound(audioData); + videoSoundHandle = CCS->soundh->playSound(audioData); if(!CCS->videoh->open(VideoPath::builtin(video))) { if(!won) @@ -324,7 +324,7 @@ void CHighScoreInputScreen::activate() void CHighScoreInputScreen::deactivate() { CCS->videoh->close(); - CCS->soundh->stopSound(sound); + CCS->soundh->stopSound(videoSoundHandle); CIntObject::deactivate(); } diff --git a/client/mainmenu/CHighScoreScreen.h b/client/mainmenu/CHighScoreScreen.h index 84083fcc2..2261dbe60 100644 --- a/client/mainmenu/CHighScoreScreen.h +++ b/client/mainmenu/CHighScoreScreen.h @@ -95,7 +95,7 @@ class CHighScoreInputScreen : public CWindowObject std::shared_ptr background; std::string video; - int sound; + int videoSoundHandle; bool won; HighScoreCalculation calc; public: diff --git a/client/mainmenu/CPrologEpilogVideo.cpp b/client/mainmenu/CPrologEpilogVideo.cpp index 8db91c229..6446d6d1b 100644 --- a/client/mainmenu/CPrologEpilogVideo.cpp +++ b/client/mainmenu/CPrologEpilogVideo.cpp @@ -20,7 +20,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function callback) - : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), exitCb(callback) + : CWindowObject(BORDERED), spe(_spe), positionCounter(0), voiceSoundHandle(-1), videoSoundHandle(-1), exitCb(callback) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; addUsedEvents(LCLICK); @@ -28,7 +28,7 @@ CPrologEpilogVideo::CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::f updateShadow(); auto audioData = CCS->videoh->getAudio(spe.prologVideo); - sound = CCS->soundh->playSound(audioData); + videoSoundHandle = CCS->soundh->playSound(audioData); CCS->videoh->open(spe.prologVideo); CCS->musich->playMusic(spe.prologMusic, true, true); voiceSoundHandle = CCS->soundh->playSound(spe.prologVoice); @@ -64,7 +64,7 @@ void CPrologEpilogVideo::show(Canvas & to) void CPrologEpilogVideo::clickPressed(const Point & cursorPosition) { close(); - CCS->soundh->stopSound(sound); CCS->soundh->stopSound(voiceSoundHandle); + CCS->soundh->stopSound(videoSoundHandle); exitCb(); } diff --git a/client/mainmenu/CPrologEpilogVideo.h b/client/mainmenu/CPrologEpilogVideo.h index 2e863227f..1666a87c1 100644 --- a/client/mainmenu/CPrologEpilogVideo.h +++ b/client/mainmenu/CPrologEpilogVideo.h @@ -19,11 +19,11 @@ class CPrologEpilogVideo : public CWindowObject CampaignScenarioPrologEpilog spe; int positionCounter; int voiceSoundHandle; + int videoSoundHandle; std::function exitCb; std::shared_ptr text; - int sound = 0; bool voiceStopped = false; public: From 7ef9e917412bcbd2bb742be5bea753f120cc7c65 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 8 Oct 2023 21:19:04 +0200 Subject: [PATCH 0759/1248] new approach --- client/CVideoHandler.h | 4 - client/battle/BattleInterfaceClasses.cpp | 116 +++++++++++++++-------- client/battle/BattleInterfaceClasses.h | 18 ++++ 3 files changed, 93 insertions(+), 45 deletions(-) diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index cda410c44..d447d64e6 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -23,7 +23,6 @@ public: virtual bool nextFrame()=0; virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer - virtual VideoPath videoName()=0; virtual bool wait()=0; virtual int curFrame() const =0; virtual int frameCount() const =0; @@ -47,7 +46,6 @@ public: void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; bool nextFrame() override {return false;}; - VideoPath videoName() override {return VideoPath();}; void close() override {}; bool wait() override {return false;}; bool open(const VideoPath & name, bool scale = false) override {return false;}; @@ -108,8 +106,6 @@ public: // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; - VideoPath videoName() override {return fname;}; - //TODO: bool wait() override {return false;}; int curFrame() const override {return -1;}; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index d41dce25e..feac0006b 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -459,7 +459,7 @@ HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) } BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) - : owner(_owner) + : owner(_owner), currentVideo(BattleResultVideo::NONE) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -566,16 +566,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won { int text = 304; - AudioPath musicName = AudioPath::builtin("Music/Win Battle"); - VideoPath videoName = VideoPath::builtin("WIN3.BIK"); + currentVideo = BattleResultVideo::WIN; switch(br.result) { case EBattleResult::NORMAL: if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - { - musicName = AudioPath::builtin("Music/Defend Castle"); - videoName = VideoPath::builtin("DEFENDALL.BIK"); - } + currentVideo = BattleResultVideo::WIN_SIEGE; break; case EBattleResult::ESCAPE: text = 303; @@ -587,9 +583,8 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); break; } + playVideo(); - CCS->musich->playMusic(musicName, false, true); - CCS->videoh->open(videoName); std::string str = CGI->generaltexth->allTexts[text]; const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); @@ -605,33 +600,26 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface else // we lose { int text = 311; - AudioPath musicName = AudioPath::builtin("Music/LoseCombat"); - VideoPath videoName = VideoPath::builtin("LBSTART.BIK"); + currentVideo = BattleResultVideo::DEFEAT; switch(br.result) { case EBattleResult::NORMAL: if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - { - musicName = AudioPath::builtin("Music/LoseCastle"); - videoName = VideoPath::builtin("LOSECSTL.BIK"); - } + currentVideo = BattleResultVideo::DEFEAT_SIEGE; break; case EBattleResult::ESCAPE: - musicName = AudioPath::builtin("Music/Retreat Battle"); - videoName = VideoPath::builtin("RTSTART.BIK"); + currentVideo = BattleResultVideo::RETREAT; text = 310; break; case EBattleResult::SURRENDER: - musicName = AudioPath::builtin("Music/Surrender Battle"); - videoName = VideoPath::builtin("SURRENDER.BIK"); + currentVideo = BattleResultVideo::SURRENDER; text = 309; break; default: logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); break; } - CCS->musich->playMusic(musicName, false, true); - CCS->videoh->open(videoName); + playVideo(); labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); } @@ -649,29 +637,75 @@ void BattleResultWindow::show(Canvas & to) CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, [&]() { - if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/LBSTART")) - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin("VIDEO/LBLOOP")); - } - if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/RTSTART")) - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin("VIDEO/RTLOOP")); - } - if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/LOSECSTL")) - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin("VIDEO/LOSECSLP")); - } - if(CCS->videoh->videoName() == VideoPath::builtin("VIDEO/DEFENDALL")) - { - CCS->videoh->close(); - CCS->videoh->open(VideoPath::builtin("VIDEO/DEFENDLOOP")); - } + playVideo(true); }); } +void BattleResultWindow::playVideo(bool startLoop) +{ + AudioPath musicName = AudioPath(); + VideoPath videoName = VideoPath(); + + if(!startLoop) + { + switch(currentVideo) + { + case BattleResultVideo::WIN: + musicName = AudioPath::builtin("Music/Win Battle"); + videoName = VideoPath::builtin("WIN3.BIK"); + break; + case BattleResultVideo::SURRENDER: + musicName = AudioPath::builtin("Music/Surrender Battle"); + videoName = VideoPath::builtin("SURRENDER.BIK"); + break; + case BattleResultVideo::RETREAT: + musicName = AudioPath::builtin("Music/Retreat Battle"); + videoName = VideoPath::builtin("RTSTART.BIK"); + break; + case BattleResultVideo::DEFEAT: + musicName = AudioPath::builtin("Music/LoseCombat"); + videoName = VideoPath::builtin("LBSTART.BIK"); + break; + case BattleResultVideo::DEFEAT_SIEGE: + musicName = AudioPath::builtin("Music/LoseCastle"); + videoName = VideoPath::builtin("LOSECSTL.BIK"); + break; + case BattleResultVideo::WIN_SIEGE: + musicName = AudioPath::builtin("Music/Defend Castle"); + videoName = VideoPath::builtin("DEFENDALL.BIK"); + break; + } + } + else + { + switch(currentVideo) + { + case BattleResultVideo::RETREAT: + currentVideo = BattleResultVideo::RETREAT_LOOP; + videoName = VideoPath::builtin("RTLOOP.BIK"); + break; + case BattleResultVideo::DEFEAT: + currentVideo = BattleResultVideo::DEFEAT_LOOP; + videoName = VideoPath::builtin("LBLOOP.BIK"); + break; + case BattleResultVideo::DEFEAT_SIEGE: + currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP; + videoName = VideoPath::builtin("LOSECSLP.BIK"); + break; + case BattleResultVideo::WIN_SIEGE: + currentVideo = BattleResultVideo::WIN_SIEGE_LOOP; + videoName = VideoPath::builtin("DEFENDLOOP.BIK"); + break; + } + } + + if(musicName != AudioPath()) + CCS->musich->playMusic(musicName, false, true); + + if(videoName != VideoPath()) + CCS->videoh->open(videoName); +} + void BattleResultWindow::buttonPressed(int button) { if (resultCallback) diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index c77f84782..2b395ed23 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -163,6 +163,24 @@ private: std::vector> icons; std::shared_ptr description; CPlayerInterface & owner; + + enum BattleResultVideo + { + NONE, + WIN, + SURRENDER, + RETREAT, + RETREAT_LOOP, + DEFEAT, + DEFEAT_LOOP, + DEFEAT_SIEGE, + DEFEAT_SIEGE_LOOP, + WIN_SIEGE, + WIN_SIEGE_LOOP, + }; + BattleResultVideo currentVideo; + + void playVideo(bool startLoop = false); void buttonPressed(int button); //internal function for button callbacks public: From b36767904acd3526be4bad5732162f6ae7225d6b Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 8 Oct 2023 21:18:44 +0200 Subject: [PATCH 0760/1248] Mention ccache in developer docs --- docs/developers/Building_Linux.md | 11 ++++++++--- docs/developers/Building_Windows.md | 9 ++++++++- docs/developers/Building_macOS.md | 4 +++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 0add5e91e..811f480d8 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -19,13 +19,15 @@ To compile, the following packages (and their development counterparts) are need - Boost C++ libraries v1.48+: program-options, filesystem, system, thread, locale - Recommended, if you want to build launcher or map editor: Qt 5, widget and network modules - Recommended, FFmpeg libraries, if you want to watch in-game videos: libavformat and libswscale. Their name could be libavformat-devel and libswscale-devel, or ffmpeg-libs-devel or similar names. -- Optional, if you want to build scripting modules: LuaJIT +- Optional: + - if you want to build scripting modules: LuaJIT + - to speed up recompilation: Ccache ## On Debian-based systems (e.g. Ubuntu) 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 qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev ninja-build` +`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 qtbase5-dev libtbb-dev libluajit-5.1-dev qttools5-dev ninja-build ccache` Alternatively if you have VCMI installed from repository or PPA you can use: @@ -33,7 +35,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 zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel tbb-devel luajit-devel fuzzylite-devel` +`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 zlib-devel ffmpeg-devel ffmpeg-libs qt5-qtbase-devel tbb-devel luajit-devel fuzzylite-devel ccache` NOTE: `fuzzylite-devel` package is no longer available in recent version of Fedora, for example Fedora 38. It's not a blocker because VCMI bundles fuzzylite lib in its source code. @@ -73,6 +75,9 @@ cmake ../vcmi **Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. +## To use ccache: +`cmake ../vcmi -DCMAKE_COMPILER_LAUNCHER=ccache` + ## Trigger build `cmake --build . -- -j2` diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index cab0722dc..e9e758581 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -10,7 +10,10 @@ Windows builds can be made in more than one way and with more than one tool. Thi - Git or git GUI, for example, SourceTree [download link](http://www.sourcetreeapp.com/download) - CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users". - To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html) -- Optionally, to create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) +- To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) +- Optional: + - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) + ## Choose an installation directory @@ -75,6 +78,10 @@ From command line use: For the list of the packages used you can also consult [vcmi-deps-windows readme](https://github.com/vcmi/vcmi-deps-windows) in case this article gets outdated a bit. +### CCache + +Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in. + # Build VCMI #### From GIT GUI diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index d6557e5f2..558e5338f 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -7,7 +7,9 @@ - Xcode IDE: - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) 2. CMake: `brew install --cask cmake` or get from -3. (optional) Ninja: `brew install ninja` or get from +3. CCache to speed up recompilation: `brew install ccache` +4. Optional: + * Ninja: `brew install ninja` or get it from # Obtaining source code From e2b8c2e9f8ded3f8369407edb9c134accf9ec219 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 01:24:00 +0200 Subject: [PATCH 0761/1248] PickObject delegate for random dwellings --- mapeditor/CMakeLists.txt | 2 + mapeditor/inspector/PickObjectDelegate.cpp | 59 ++++++++++++++++++++++ mapeditor/inspector/PickObjectDelegate.h | 45 +++++++++++++++++ mapeditor/inspector/inspector.cpp | 53 ++++++++++++------- mapeditor/inspector/inspector.h | 8 ++- mapeditor/mainwindow.cpp | 4 +- 6 files changed, 146 insertions(+), 25 deletions(-) create mode 100644 mapeditor/inspector/PickObjectDelegate.cpp create mode 100644 mapeditor/inspector/PickObjectDelegate.h diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 3a2c6191b..8cdf87e09 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -34,6 +34,7 @@ set(editor_SRCS inspector/rewardswidget.cpp inspector/questwidget.cpp inspector/heroskillswidget.cpp + inspector/PickObjectDelegate.cpp resourceExtractor/ResourceConverter.cpp ) @@ -72,6 +73,7 @@ set(editor_HEADERS inspector/rewardswidget.h inspector/questwidget.h inspector/heroskillswidget.h + inspector/PickObjectDelegate.h resourceExtractor/ResourceConverter.h ) diff --git a/mapeditor/inspector/PickObjectDelegate.cpp b/mapeditor/inspector/PickObjectDelegate.cpp new file mode 100644 index 000000000..369b5ee90 --- /dev/null +++ b/mapeditor/inspector/PickObjectDelegate.cpp @@ -0,0 +1,59 @@ +/* + * PickObjectDelegate.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 "PickObjectDelegate.h" + +#include "../mapcontroller.h" +#include "../../lib/mapObjects/CGObjectInstance.h" + +PickObjectDelegate::PickObjectDelegate(MapController & c): controller(c) +{ + filter = [](const CGObjectInstance *) + { + return true; + }; +} + +PickObjectDelegate::PickObjectDelegate(MapController & c, std::function f): controller(c), filter(f) +{ + +} + +void PickObjectDelegate::onObjectPicked(const CGObjectInstance * o) +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &PickObjectDelegate::onObjectPicked); + } + + QMap data; + data[Qt::DisplayRole] = QVariant("None"); + data[Qt::UserRole] = QVariant::fromValue(data_cast(o)); + if(o) + data[Qt::DisplayRole] = QVariant(QString::fromStdString(o->instanceName)); + const_cast(modelIndex.model())->setItemData(modelIndex, data); +} + +QWidget * PickObjectDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(filter); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &PickObjectDelegate::onObjectPicked); + } + + modelIndex = index; + return nullptr; +} diff --git a/mapeditor/inspector/PickObjectDelegate.h b/mapeditor/inspector/PickObjectDelegate.h new file mode 100644 index 000000000..f1a0089ac --- /dev/null +++ b/mapeditor/inspector/PickObjectDelegate.h @@ -0,0 +1,45 @@ +/* + * PickObjectDelegate.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 + +class MapController; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; + +VCMI_LIB_NAMESPACE_END + +class PickObjectDelegate : public QItemDelegate +{ + Q_OBJECT +public: + PickObjectDelegate(MapController &); + PickObjectDelegate(MapController &, std::function); + + template + static bool typedFilter(const CGObjectInstance * o) + { + return dynamic_cast(o) != nullptr; + } + + QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; + +public slots: + void onObjectPicked(const CGObjectInstance *); + +private: + MapController & controller; + std::function filter; + mutable QModelIndex modelIndex; +}; diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 386d6e05e..b77f78079 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -26,6 +26,8 @@ #include "rewardswidget.h" #include "questwidget.h" #include "heroskillswidget.h" +#include "PickObjectDelegate.h" +#include "../mapcontroller.h" static QList> MissionIdentifiers { @@ -239,6 +241,12 @@ void Inspector::updateProperties(CGDwelling * o) if(!o) return; addProperty("Owner", o->tempOwner, false); + + if(auto * info = dynamic_cast(o->info)) + { + auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); + addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); + } } void Inspector::updateProperties(CGLighthouse * o) @@ -288,7 +296,7 @@ void Inspector::updateProperties(CGHeroInstance * o) auto * delegate = new InspectorDelegate; for(int i = 0; i < VLC->heroh->objects.size(); ++i) { - if(map->allowedHeroes.at(i)) + if(controller.map()->allowedHeroes.at(i)) { if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) { @@ -325,7 +333,7 @@ void Inspector::updateProperties(CGArtifact * o) auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) { - if(map->allowedSpells.at(spell->id)) + if(controller.map()->allowedSpells.at(spell->id)) delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); @@ -379,7 +387,7 @@ void Inspector::updateProperties(CRewardableObject * o) { if(!o) return; - auto * delegate = new RewardsDelegate(*map, *o); + auto * delegate = new RewardsDelegate(*controller.map(), *o); addProperty("Reward", PropertyEditorPlaceholder(), delegate, false); } @@ -415,7 +423,7 @@ void Inspector::updateProperties(CGSeerHut * o) addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); { //Quest - auto * delegate = new QuestDelegate(*map, *o); + auto * delegate = new QuestDelegate(*controller.map(), *o); addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); } } @@ -441,8 +449,8 @@ void Inspector::updateProperties() auto * delegate = new InspectorDelegate(); delegate->options.push_back({QObject::tr("neutral"), QVariant::fromValue(PlayerColor::NEUTRAL.getNum())}); - for(int p = 0; p < map->players.size(); ++p) - if(map->players[p].canAnyonePlay()) + for(int p = 0; p < controller.map()->players.size(); ++p) + if(controller.map()->players[p].canAnyonePlay()) delegate->options.push_back({QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[p]), QVariant::fromValue(PlayerColor(p).getNum())}); addProperty("Owner", obj->tempOwner, delegate, true); @@ -530,7 +538,7 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -552,7 +560,7 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(mapRegisterLocalizedString("map", *map, TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -560,7 +568,7 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -576,7 +584,7 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -587,6 +595,16 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant & value) { if(!o) return; + + if(key == "Same as town") + { + if(auto * info = dynamic_cast(o->info)) + { + info->instanceId = ""; + if(CGTownInstance * town = data_cast(value.toLongLong())) + info->instanceId = town->instanceName; + } + } } void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant & value) @@ -605,10 +623,10 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); if(key == "Biography") - o->biographyCustomTextId = mapRegisterLocalizedString("map", *map, TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); + o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -644,7 +662,7 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -662,11 +680,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(key == "Mission type") o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") - o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *map, TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); } @@ -799,7 +817,7 @@ QTableWidgetItem * Inspector::addProperty(CQuest::Emission value) //======================================================================== -Inspector::Inspector(CMap * m, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), map(m) +Inspector::Inspector(MapController & c, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), controller(c) { } @@ -845,4 +863,3 @@ void InspectorDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, QStyledItemDelegate::setModelData(editor, model, index); } } - diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index c31f41818..69633a3db 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -32,6 +32,7 @@ void setProperty(x*, const QString &, const QVariant &); #define SET_PROPERTIES(x) setProperty(dynamic_cast(obj), key, value) +class MapController; class Initializer { public: @@ -101,7 +102,7 @@ protected: //===============END OF DECLARATION======================================= public: - Inspector(CMap *, CGObjectInstance *, QTableWidget *); + Inspector(MapController &, CGObjectInstance *, QTableWidget *); void setProperty(const QString & key, const QTableWidgetItem * item); @@ -154,12 +155,10 @@ protected: QTableWidget * table; CGObjectInstance * obj; QMap keyItems; - CMap * map; + MapController & controller; }; - - class InspectorDelegate : public QStyledItemDelegate { Q_OBJECT @@ -172,4 +171,3 @@ public: QList> options; }; - diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index fac99c05b..1dcb6b544 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -998,7 +998,7 @@ void MainWindow::loadInspector(CGObjectInstance * obj, bool switchTab) { if(switchTab) ui->tabWidget->setCurrentIndex(1); - Inspector inspector(controller.map(), obj, ui->inspectorWidget); + Inspector inspector(controller, obj, ui->inspectorWidget); inspector.updateProperties(); } @@ -1022,7 +1022,7 @@ void MainWindow::on_inspectorWidget_itemChanged(QTableWidgetItem *item) auto param = tableWidget->item(r, c - 1)->text(); //set parameter - Inspector inspector(controller.map(), obj, tableWidget); + Inspector inspector(controller, obj, tableWidget); inspector.setProperty(param, item); controller.commitObjectChange(mapLevel); } From 335585bd11c3f3917397128dbad4fd457ab440d4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 19:21:27 +0200 Subject: [PATCH 0762/1248] Remove unused variable --- mapeditor/inspector/inspector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b77f78079..d6fa5183b 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -242,7 +242,7 @@ void Inspector::updateProperties(CGDwelling * o) addProperty("Owner", o->tempOwner, false); - if(auto * info = dynamic_cast(o->info)) + if(dynamic_cast(o->info)) { auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); From 3f83d54dc1e94b0882e7b7f2562f4780cf418964 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 9 Oct 2023 21:22:11 +0200 Subject: [PATCH 0763/1248] Set ENABLE_CCACHE to OFF by default --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 657b06c2f..cd3fc33be 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -82,7 +82,7 @@ endif() # On Linux, use -DCMAKE_CXX_COMPILER_LAUNCHER=ccache instead. # The XCode and MSVC builds each require some more configuration, which is enabled by the following option: if(MSVC OR (CMAKE_GENERATOR STREQUAL "Xcode")) - option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" ON) + option(ENABLE_CCACHE "Speed up recompilation by caching previous compilations" OFF) endif() if(ENABLE_CCACHE AND (CMAKE_GENERATOR STREQUAL "Xcode")) From fa9d5e9971ae7e24a2ebd45face746fa7c7e1861 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 9 Oct 2023 21:43:43 +0200 Subject: [PATCH 0764/1248] show missing resources --- client/widgets/CComponent.cpp | 16 +++++++++++++--- client/widgets/CComponent.h | 4 +++- client/windows/CCastleInterface.cpp | 8 +++++++- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 626c37f52..dd96e48ac 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -42,7 +42,13 @@ CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font): perDay(false) { - init(Type, Subtype, Val, imageSize, font); + init(Type, Subtype, Val, imageSize, font, ""); +} + +CComponent::CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize, EFonts font): + perDay(false) +{ + init(Type, Subtype, 0, imageSize, font, Val); } CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) @@ -54,7 +60,7 @@ CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) init((Etype)c.id, c.subtype, c.val, imageSize, font); } -void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt) +void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt, std::string ValText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -63,6 +69,7 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts compType = Type; subtype = Subtype; val = Val; + valText = ValText; size = imageSize; font = fnt; @@ -87,6 +94,9 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts if (size < small) max = 30; + if(Type == Etype::resource && !valText.empty()) + max = 80; + std::vector textLines = CMessage::breakText(getSubtitle(), std::max(max, pos.w), font); for(auto & line : textLines) { @@ -209,7 +219,7 @@ std::string CComponent::getSubtitleInternal() { case primskill: return boost::str(boost::format("%+d %s") % val % (subtype < 4 ? CGI->generaltexth->primarySkillNames[subtype] : CGI->generaltexth->allTexts[387])); case secskill: return CGI->generaltexth->levels[val-1] + "\n" + CGI->skillh->getByIndex(subtype)->getNameTranslated(); - case resource: return std::to_string(val); + case resource: return valText.empty() ? std::to_string(val) : valText; case creature: { auto creature = CGI->creh->getByIndex(subtype); diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 7ef484fe8..3f94137ff 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -49,7 +49,7 @@ private: void setSurface(const AnimationPath & defName, int imgPos); std::string getSubtitleInternal(); - void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL); + void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText=""); public: std::shared_ptr image; @@ -59,12 +59,14 @@ public: EFonts font; //Font size of label int subtype; //type-dependant subtype. See getSomething methods for details int val; // value \ strength \ amount of component. See getSomething methods for details + std::string valText; // value instead of amount; currently only for resource bool perDay; // add "per day" text to subtitle std::string getDescription(); std::string getSubtitle(); CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL); CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL); void showPopupWindow(const Point & cursorPosition) override; //call-in diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 20f0aef08..b41adbe3d 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1473,7 +1473,13 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin for(int i = 0; iresources[i]) - components.push_back(std::make_shared(CComponent::resource, i, building->resources[i], CComponent::small)); + { + std::string text = std::to_string(building->resources[i]); + int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; + if(resAfterBuy < 0 && settings["general"]["enableUiEnhancements"].Bool()) + text += " {H3Red|(" + std::to_string(-resAfterBuy) + ")}"; + components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); + } } cost = std::make_shared(components, Rect(25, 300, pos.w - 50, 130)); From ec094006c8b4cb855a7889b12379a1c1ee067604 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 9 Oct 2023 21:48:49 +0200 Subject: [PATCH 0765/1248] Add separate CMake preset for MSVC build with ccache --- .github/workflows/github.yml | 2 +- CMakePresets.json | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index c5f18fe29..337067b31 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -111,7 +111,7 @@ jobs: test: 0 pack: 1 extension: exe - preset: windows-msvc-release + preset: windows-msvc-release-ccache - platform: mingw-ubuntu os: ubuntu-22.04 test: 0 diff --git a/CMakePresets.json b/CMakePresets.json index 37c321522..f43ac51ff 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -123,6 +123,17 @@ "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", "CMAKE_POLICY_DEFAULT_CMP0091": "NEW", "FORCE_BUNDLED_MINIZIP": "ON" + + } + }, + { + "name": "windows-msvc-release-ccache", + "displayName": "Windows x64 RelWithDebInfo with ccache", + "description": "VCMI RelWithDebInfo build with ccache", + "inherits": "windows-msvc-release", + "cacheVariables": { + "ENABLE_CCACHE": "ON" + } }, { @@ -228,7 +239,8 @@ "description": "VCMI iOS release using Conan and ccache", "inherits": "ios-release-conan", "cacheVariables": { - "ENABLE_PCH" : "OFF" + "ENABLE_PCH" : "OFF", + "ENABLE_CCACHE": "ON" } }, { @@ -318,6 +330,11 @@ "inherits": "default-release", "configuration": "Release" }, + { + "name": "windows-msvc-release-ccache", + "configurePreset": "windows-msvc-release-ccache", + "inherits": "windows-msvc-release" + }, { "name": "windows-msvc-relwithdebinfo", "configurePreset": "windows-msvc-release", From 19788c6399988101d62b8648e6b70678fa64260f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:22:10 +0200 Subject: [PATCH 0766/1248] suggestion --- client/CVideoHandler.cpp | 2 +- client/CVideoHandler.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 601864d49..20ece6787 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -455,7 +455,7 @@ void CVideoPlayer::close() std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) { - std::pair, si64> dat(std::make_pair(std::make_unique(0), 0)); + std::pair, si64> dat(std::make_pair(nullptr, 0)); VideoPath fnameAudio; diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index cb031a8e4..21fc21dfe 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -36,7 +36,7 @@ public: { return false; } - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(std::make_unique(0), 0); }; + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; }; class CEmptyVideoPlayer : public IMainVideoPlayer From 9bbd2a58bca4d33510cba9e20fee13c30a3d4ecc Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 8 Oct 2023 21:47:09 +0200 Subject: [PATCH 0767/1248] Improve docs --- docs/developers/Building_Android.md | 6 ++++-- docs/developers/Building_Linux.md | 4 ++-- docs/developers/Building_Windows.md | 11 +++++++---- docs/developers/Building_iOS.md | 6 +++++- docs/developers/Building_macOS.md | 5 +++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index 6800dc7e2..6f04b71a5 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -14,7 +14,9 @@ The following instructions apply to **v1.2 and later**. For earlier versions the - install with `sdkmanager` command line tool - download from https://developer.android.com/ndk/downloads - download with Conan, see [#NDK and Conan](#ndk-and-conan) -5. (optional) Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases +5. Optional: + - Ninja: download from your package manager or from https://github.com/ninja-build/ninja/releases + - Ccache: download from your package manager or from https://github.com/ccache/ccache/releases ## Obtaining source code @@ -60,7 +62,7 @@ Building for Android is a 2-step process. First, native C++ code is compiled to This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset. Example: ``` -cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug --toolchain ... +cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_COMPILER_LAUNCHER=ccache --toolchain ... cmake --build ../build ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index 811f480d8..a1371532c 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -71,12 +71,12 @@ cmake ../vcmi # Additional options that you may want to use: ## To enable debugging: -`cmake ../vcmi -DCMAKE_BUILD_TYPE=Debug` +`cmake ../vcmi -D CMAKE_BUILD_TYPE=Debug` **Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. ## To use ccache: -`cmake ../vcmi -DCMAKE_COMPILER_LAUNCHER=ccache` +`cmake ../vcmi -D CMAKE_COMPILER_LAUNCHER=ccache` ## Trigger build diff --git a/docs/developers/Building_Windows.md b/docs/developers/Building_Windows.md index e9e758581..1ba751662 100644 --- a/docs/developers/Building_Windows.md +++ b/docs/developers/Building_Windows.md @@ -10,10 +10,9 @@ Windows builds can be made in more than one way and with more than one tool. Thi - Git or git GUI, for example, SourceTree [download link](http://www.sourcetreeapp.com/download) - CMake [download link](https://cmake.org/download/). During install after accepting license agreement make sure to check "Add CMake to the system PATH for all users". - To unpack pre-build Vcpkg: [7-zip](http://www.7-zip.org/download.html) -- To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) - Optional: - To create installer: [NSIS](http://nsis.sourceforge.net/Main_Page) - + - To speed up recompilation: [CCache](https://github.com/ccache/ccache/releases) ## Choose an installation directory @@ -78,7 +77,7 @@ From command line use: For the list of the packages used you can also consult [vcmi-deps-windows readme](https://github.com/vcmi/vcmi-deps-windows) in case this article gets outdated a bit. -### CCache +# Install CCache Extract `ccache` to a folder of your choosing, add the folder to the `PATH` environment variable and log out and back in. @@ -104,7 +103,11 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi ## Compile VCMI with Visual Studio - Open `%VCMI_DIR%/build/VCMI.sln` in Visual Studio -- Select `Release` build type in combobox +- Select `Release` build type in the combobox +- If you want to use ccache: + - Select `Manage Configurations...` in the combobox + - Specify the following CMake variable: `ENABLE_CCACHE=ON` + - See the [Visual Studio documentation](https://learn.microsoft.com/en-us/cpp/build/customize-cmake-settings?view=msvc-170#cmake-variables-and-cache) for details - Right click on `BUILD_ALL` project. This `BUILD_ALL` project should be in `CMakePredefinedTargets` tree in Solution Explorer. - VCMI will be built in `%VCMI_DIR%/build/bin` folder! diff --git a/docs/developers/Building_iOS.md b/docs/developers/Building_iOS.md index 22a67d85a..e16bc1ff6 100644 --- a/docs/developers/Building_iOS.md +++ b/docs/developers/Building_iOS.md @@ -5,6 +5,8 @@ 1. **macOS** 2. Xcode: 3. CMake 3.21+: `brew install --cask cmake` or get from +4. Optional: + - CCache to speed up recompilation: `brew install ccache` ## Obtaining source code @@ -39,7 +41,9 @@ cmake --preset ios-device-conan \ -D BUNDLE_IDENTIFIER_PREFIX=com.MY-NAME ``` -By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. +By default build directory containing Xcode project will appear at `../build-ios-device-conan`, but you can change it with `-B` option. + +If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON` ### Building for device diff --git a/docs/developers/Building_macOS.md b/docs/developers/Building_macOS.md index 558e5338f..9052d2915 100644 --- a/docs/developers/Building_macOS.md +++ b/docs/developers/Building_macOS.md @@ -7,9 +7,9 @@ - Xcode IDE: - (not tested) other C++ compilers, e.g. gcc/clang from [Homebrew](https://brew.sh/) 2. CMake: `brew install --cask cmake` or get from -3. CCache to speed up recompilation: `brew install ccache` 4. Optional: * Ninja: `brew install ninja` or get it from + * CCache to speed up recompilation: `brew install ccache` # Obtaining source code @@ -71,7 +71,8 @@ Note that if you wish to use Qt Creator IDE, you should skip this step and confi - if you installed Qt 5 from Homebrew, insert:`$(brew --prefix qt@5)` - if you installed Qt from Online Installer, insert your path to Qt directory, for example: `/Users/kambala/dev/Qt-libs/5.15.2/Clang64` - example for FFmpeg + Qt 5: `-D "CMAKE_PREFIX_PATH=$(brew --prefix ffmpeg@4);$(brew --prefix qt@5)"` -8. now press Return +8. If you want to speed up the recompilation, add `-D ENABLE_CCACHE=ON` +9. Now press Return # Building project From 25f7bb40d0b069d8e786f512480017d5bca55240 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 9 Oct 2023 22:50:07 +0200 Subject: [PATCH 0768/1248] new layout --- client/windows/CCastleInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index b41adbe3d..b7bbf3f30 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1476,8 +1476,8 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin { std::string text = std::to_string(building->resources[i]); int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; - if(resAfterBuy < 0 && settings["general"]["enableUiEnhancements"].Bool()) - text += " {H3Red|(" + std::to_string(-resAfterBuy) + ")}"; + if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + text = "{H3Orange|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); } } From 7c627d8163ab77a6893a9248896cd3046d9b28ac Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 8 Oct 2023 23:07:25 +0200 Subject: [PATCH 0769/1248] Show info dialog when connection to multiplayer server fails --- Mods/vcmi/config/vcmi/english.json | 1 + Mods/vcmi/config/vcmi/german.json | 1 + client/CServerHandler.cpp | 15 +++++++-------- client/CServerHandler.h | 3 ++- client/mainmenu/CMainMenu.cpp | 9 +++++++++ 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index c5cba2fa1..f4c912f00 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -44,6 +44,7 @@ "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", + "vcmi.mainMenu.serverConnectionFailed" : "Failed to connect", "vcmi.mainMenu.serverClosing" : "Closing...", "vcmi.mainMenu.hostTCP" : "Host TCP/IP game", "vcmi.mainMenu.joinTCP" : "Join TCP/IP game", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 121deacb2..727df4ef9 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -43,6 +43,7 @@ "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", + "vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen", "vcmi.mainMenu.serverClosing" : "Trenne...", "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 15c2d6299..5f66af35e 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -251,25 +251,23 @@ void CServerHandler::startLocalServerAndConnect() void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) { state = EClientState::CONNECTING; - - logNetwork->info("justConnectToServer(%s, %d)", addr, port); std::string hostAddressFromSettings = getHostAddressFromSettings(); ui16 hostPortFromSettings = getHostPortFromSettings(); - logNetwork->info("Host settings %s:%d", hostAddressFromSettings, hostPortFromSettings); - std::string connectionAddress = addr.size() ? addr : hostAddressFromSettings; ui16 connectionPort = port ? port : hostPortFromSettings; + logNetwork->info("Connecting to %s:%d", connectionAddress, connectionPort); + boost::chrono::duration> sleepDuration{}; int maxConnectionAttempts; - + if(connectionAddress == "127.0.0.1" || connectionAddress == "localhost") { logNetwork->info("Local server"); sleepDuration = boost::chrono::milliseconds(10); - maxConnectionAttempts = 1000; + maxConnectionAttempts = 100; } else { @@ -279,14 +277,15 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po } logNetwork->info("Waiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); - + ui16 connectionAttemptCount = 0; while(!c && state != EClientState::CONNECTION_CANCELLED) { connectionAttemptCount++; if(connectionAttemptCount > maxConnectionAttempts) { - logNetwork->error("\nExceeded maximum of %d connection attempts", maxConnectionAttempts); + state = EClientState::CONNECTION_FAILED; + logNetwork->error("Exceeded maximum of %d connection attempts", maxConnectionAttempts); return; } diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 1607e97cf..83b458883 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -48,7 +48,8 @@ enum class EClientState : ui8 LOBBY_CAMPAIGN, // Client is on scenario bonus selection screen STARTING, // Gameplay interfaces being created, we pause netpacks retrieving GAMEPLAY, // In-game, used by some UI - DISCONNECTING // We disconnecting, drop all netpacks + DISCONNECTING, // We disconnecting, drop all netpacks + CONNECTION_FAILED // We could not connect to server }; class IServerAPI diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index d0ffcc14a..622898a74 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -589,6 +589,15 @@ void CSimpleJoinScreen::connectThread(const std::string & addr, ui16 port) // async call to prevent thread race GH.dispatchMainThread([this](){ + if(CSH->state == EClientState::CONNECTION_FAILED) + { + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.mainMenu.serverConnectionFailed"), {}); + + textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverAddressEnter")); + GH.startTextInput(inputAddress->pos); + buttonOk->block(false); + } + if(GH.windows().isTopWindow(this)) { close(); From 926bf718144d70957fb0e92d4b2a36d5bd678cf2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 10 Oct 2023 21:02:40 +0200 Subject: [PATCH 0770/1248] make townhall clickable --- client/windows/CCastleInterface.cpp | 4 ++++ client/windows/CCastleInterface.h | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 20f0aef08..7c6e945a7 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1286,6 +1286,10 @@ void CCastleInterface::recreateIcons() hall = std::make_shared(80, 413, town, true); fort = std::make_shared(122, 413, town, false); + fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setAnimateLonelyFrame(true); + fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); fastArmyPurchase->setAnimateLonelyFrame(true); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 353d96542..9ed063668 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -154,7 +154,6 @@ class CCastleBuildings : public CIntObject void enterCastleGate(); void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains void enterMagesGuild(); - void enterTownHall(); void openMagesGuild(); void openTownHall(); @@ -167,6 +166,7 @@ public: ~CCastleBuildings(); void enterDwelling(int level); + void enterTownHall(); void enterToTheQuickRecruitmentWindow(); void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); @@ -226,6 +226,7 @@ class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr exit; std::shared_ptr split; + std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; std::vector> creainfo;//small icons of creatures (bottom-left corner); From 890c4ac19dbe378c766f2c95c51e86c989392765 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:11:06 +0200 Subject: [PATCH 0771/1248] quick army managment click next --- client/adventureMap/CInfoBar.cpp | 5 ----- client/gui/EventDispatcher.cpp | 1 + 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index bdcf76766..7b0f7628f 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -295,12 +295,7 @@ void CInfoBar::clickReleased(const Point & cursorPosition) removeUsedEvents(TIME); //expiration trigger from just clicked element is not valid anymore if(state == HERO || state == TOWN) - { - if(settings["gameTweaks"]["infoBarCreatureManagement"].Bool()) - return; - showGameStatus(); - } else if(state == GAME) showDate(); else diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 3dc576f5e..08e4c6b44 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -218,6 +218,7 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc i->clickReleased(position); i->mouseClickedState = isPressed; + return; } else { From 53ee843e7e754e9511574dff69346f64d1eb0f8d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 10 Oct 2023 22:33:22 +0200 Subject: [PATCH 0772/1248] fixes problem with radial menu --- client/adventureMap/AdventureMapInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 7bd085cd5..376401437 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -24,6 +24,7 @@ #include "../mapView/mapHandler.h" #include "../mapView/MapView.h" #include "../windows/InfoWindows.h" +#include "../widgets/RadialMenu.h" #include "../CGameInfo.h" #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" @@ -169,8 +170,7 @@ void AdventureMapInterface::dim(Canvas & to) { for (auto window : GH.windows().findWindows()) { - std::shared_ptr casted = std::dynamic_pointer_cast(window); - if (!casted && !window->isPopupWindow()) + if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) { Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); From 8a3e58cd5e3e623550be8d458287e9653d17decb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:15:40 +0200 Subject: [PATCH 0773/1248] red is the most liked color --- client/windows/CCastleInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index b7bbf3f30..600ea3b3d 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1477,7 +1477,7 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin std::string text = std::to_string(building->resources[i]); int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) - text = "{H3Orange|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; + text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); } } From 0cf3205e150227cde2a58d921ed6a29e4180966b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 02:19:58 +0200 Subject: [PATCH 0774/1248] Fix quest regressions --- lib/mapObjects/CQuest.cpp | 57 +++++++++++++++++++++++++++++++++------ lib/mapObjects/CQuest.h | 4 +-- 2 files changed, 50 insertions(+), 11 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index b32d833f8..18ee673f5 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -178,6 +178,49 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const } } +void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const +{ + switch (missionType) + { + case CQuest::MISSION_ART: + for(auto & elem : m5arts) + { + if(h->hasArt(elem)) + { + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + } + else + { + const auto * assembly = h->getAssemblyByConstituent(elem); + assert(assembly); + auto parts = assembly->getPartsInfo(); + + // Remove the assembly + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + + // Disassemble this backpack artifact + for(const auto & ci : parts) + { + if(ci.art->getTypeId() != elem) + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); + } + } + } + break; + case CQuest::MISSION_ARMY: + cb->takeCreatures(h->id, m6creatures); + break; + case CQuest::MISSION_RESOURCES: + for (int i = 0; i < 7; ++i) + { + cb->giveResource(h->getOwner(), static_cast(i), -static_cast(m7resources[i])); + } + break; + default: + break; + } +} + void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const { MetaString text; @@ -590,9 +633,9 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomFirst) quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get()); if(!quest->isCustomNext) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); + quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); if(!quest->isCustomComplete) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); } else { @@ -758,11 +801,6 @@ int CGSeerHut::checkDirection() const } } -void CGSeerHut::completeQuest() const //reward -{ - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete -} - const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); @@ -785,7 +823,10 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) { CRewardableObject::blockingDialogAnswered(hero, answer); if(answer) - completeQuest(); + { + quest->completeQuest(cb, hero); + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + } } void CGSeerHut::afterAddToMap(CMap* map) diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 82a6abd7e..535691b98 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -83,7 +83,7 @@ public: virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry - virtual void completeQuest (const CGHeroInstance * h) const {}; + virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; virtual void addReplacements(MetaString &out, const std::string &base) const; void addArtifactID(const ArtifactID & id); @@ -156,8 +156,6 @@ public: const CGHeroInstance *getHeroToKill(bool allowNull = false) const; const CGCreature *getCreatureToKill(bool allowNull = false) const; void getRolloverText (MetaString &text, bool onHover) const; - void finishQuest (const CGHeroInstance * h, ui32 accept) const; //common for both objects - virtual void completeQuest() const; void afterAddToMap(CMap * map) override; From 937a8d63c72802ecf27ef06a38941e9eba463385 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 02:33:16 +0200 Subject: [PATCH 0775/1248] Check for artifacts copies in limiter --- lib/rewardable/Limiter.cpp | 19 ++++++++++++++++--- lib/rewardable/Limiter.h | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index bf3fb2911..9611f67aa 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -17,6 +17,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" #include "../CSkillHandler.h" +#include "../ArtifactUtils.h" VCMI_LIB_NAMESPACE_BEGIN @@ -93,12 +94,24 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } - for(const auto & art : artifacts) { - if (!hero->hasArt(art)) + std::unordered_map artifactsRequirements; // artifact ID -> required count + for(const auto & art : artifacts) + ++artifactsRequirements[art]; + + size_t reqSlots = 0; + for(const auto & elem : artifactsRequirements) + { + // check required amount of artifacts + if(hero->getArtPosCount(elem.first, false, true, true) < elem.second) + return false; + if(!hero->hasArt(elem.first)) + reqSlots += hero->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; + } + if(!ArtifactUtils::isBackpackFreeSlots(hero, reqSlots)) return false; } - + for(const auto & sublimiter : noneOf) { if (sublimiter->heroAllowed(hero)) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 68639bb68..1f92cf4f0 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -52,7 +52,7 @@ struct DLL_LINKAGE Limiter std::map secondary; /// artifacts that hero needs to have (equipped or in backpack) to trigger this - /// Note: does not checks for multiple copies of the same arts + /// checks for artifacts copies if same artifact id is included multiple times std::vector artifacts; /// Spells that hero must have in the spellbook From 6a7b23c007dd44b2cf6a778b637d0ce7607342c3 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 04:19:12 +0200 Subject: [PATCH 0776/1248] Additional properties for limiter --- docs/modders/Map_Objects/Rewardable.md | 36 +++++++++++++++++++--- lib/JsonRandom.cpp | 41 ++++++++++++++++++++++++++ lib/JsonRandom.h | 4 +++ lib/rewardable/Info.cpp | 4 +++ lib/rewardable/Limiter.cpp | 14 +++++++++ lib/rewardable/Limiter.h | 10 +++++++ 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 1694b7e7a..3aa316327 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -72,7 +72,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid if any of these conditions are true "anyOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] @@ -80,12 +80,12 @@ Rewardable object is defined similarly to other objects, with key difference bei // additional list of conditions. Limiter will be valid only if none of these conditions are true "noneOf" : [ { - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ] - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } @@ -95,7 +95,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // object will be disappeared after taking reward is set to true "removeObject": false - // See "Configurable Properties" section for additiona parameters + // See "Configurable Properties" section for additional parameters } ], @@ -450,4 +450,32 @@ Keep in mind, that all randomization is performed on map load and on object rese "spell" : "townPortal", "schoolLevel": 3 } +``` + +### Player color +- Can be used as limiter +- Can NOT be used as reward +- Only players with specific color can pass the limiter +- If not specified or empty all colors may pass the limiter + +```jsonc +"colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] +``` + +### Hero types +- Can be used as limiter +- Can NOT be used as reward +- Only specific heroes can pass the limiter + +```jsonc +"heroes" : [ "orrin" ] +``` + +### Hero classes +- Can be used as limiter +- Can NOT be used as reward +- Only heroes belonging to specific classes can pass the limiter + +```jsonc +"heroClasses" : [ "battlemage" ] ``` \ No newline at end of file diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 270c47b80..95ce1f4f4 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -22,6 +22,7 @@ #include "CCreatureSet.h" #include "spells/CSpellHandler.h" #include "CSkillHandler.h" +#include "CHeroHandler.h" #include "IGameCallback.h" #include "mapObjects/IObjectInterface.h" #include "modding/IdentifierStorage.h" @@ -282,6 +283,46 @@ namespace JsonRandom return ret; } + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + std::set def; + + for(auto & color : GameConstants::PLAYER_COLOR_NAMES) + def.insert(color); + + for(auto & entry : value.Vector()) + { + auto key = loadKey(entry, rng, def); + auto pos = vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, key); + if(pos < 0) + logMod->warn("Unable to determine player color %s", key); + else + ret.emplace_back(pos); + } + return ret; + } + + std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroTypes()->getByIndex(VLC->identifiers()->getIdentifier("hero", entry.String()).value())->getId()); + } + return ret; + } + + std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng) + { + std::vector ret; + for(auto & entry : value.Vector()) + { + ret.push_back(VLC->heroClasses()->getByIndex(VLC->identifiers()->getIdentifier("heroClass", entry.String()).value())->getId()); + } + return ret; + } + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) { CStackBasicDescriptor stack; diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 2ac9e1d65..ef066d14e 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -48,6 +48,10 @@ namespace JsonRandom DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value); + DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); //DLL_LINKAGE std::vector loadComponents(const JsonNode & value); } diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index c82fca352..b9a8f1b2b 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -122,6 +122,10 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); limiter.spells = JsonRandom::loadSpells(source["spells"], rng, spells); limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + + limiter.players = JsonRandom::loadColors(source["colors"], rng); + limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); + limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); limiter.allOf = configureSublimiters(object, rng, source["allOf"] ); limiter.anyOf = configureSublimiters(object, rng, source["anyOf"] ); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 9611f67aa..8167da026 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -16,6 +16,7 @@ #include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" +#include "../CHeroHandler.h" #include "../CSkillHandler.h" #include "../ArtifactUtils.h" @@ -112,6 +113,16 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } + if(!players.empty() && !vstd::contains(players, hero->getOwner())) + return false; + + if(!heroes.empty() && !vstd::contains(heroes, hero->type->getId())) + return false; + + if(!heroClasses.empty() && !vstd::contains(heroClasses, hero->type->heroClass->getId())) + return false; + + for(const auto & sublimiter : noneOf) { if (sublimiter->heroAllowed(hero)) @@ -143,6 +154,9 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); + handler.serializeIdArray("hero", heroes); + handler.serializeIdArray("heroClass", heroClasses); + handler.serializeIdArray("color", players); handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); handler.enterArray("creatures").serializeStruct(creatures); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 1f92cf4f0..300e572ae 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -60,6 +60,13 @@ struct DLL_LINKAGE Limiter /// creatures that hero needs to have std::vector creatures; + + /// only heroes/hero classes from list could pass limiter + std::vector heroes; + std::vector heroClasses; + + /// only player colors can pass limiter + std::vector players; /// sub-limiters, all must pass for this limiter to pass LimitersList allOf; @@ -91,6 +98,9 @@ struct DLL_LINKAGE Limiter h & allOf; h & anyOf; h & noneOf; + h & heroes; + h & heroClasses; + h & players; } void serializeJson(JsonSerializeFormat & handler); From b7568a160cd033125e6bf6d5af30e1f0180b64d1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 04:26:08 +0200 Subject: [PATCH 0777/1248] Fixes --- lib/constants/EntityIdentifiers.cpp | 21 +++++++++++++++++++++ lib/constants/EntityIdentifiers.h | 4 ++++ lib/rewardable/Limiter.cpp | 6 +++--- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 4fe197b5c..eaa790c9b 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -19,6 +19,8 @@ #include #include #include +#include +#include #include #include @@ -103,6 +105,25 @@ namespace GameConstants #endif } +si32 HeroClassID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string HeroClassID::encode(const si32 index) +{ + return VLC->heroClasses()->getByIndex(index)->getJsonKey(); +} + +std::string HeroClassID::entityType() +{ + return "heroClass"; +} + si32 HeroTypeID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7545d5a2a..7ca9a1333 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -223,6 +223,10 @@ class HeroClassID : public Identifier { public: using Identifier::Identifier; + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); }; class HeroTypeID : public Identifier diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 8167da026..9255fbbb1 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -154,9 +154,9 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); - handler.serializeIdArray("hero", heroes); - handler.serializeIdArray("heroClass", heroClasses); - handler.serializeIdArray("color", players); + handler.serializeIdArray("heroes", heroes); + handler.serializeIdArray("heroClasses", heroClasses); + handler.serializeIdArray("colors", players); handler.serializeInt("manaPoints", manaPoints); handler.serializeIdArray("artifacts", artifacts); handler.enterArray("creatures").serializeStruct(creatures); From 1460541ee53c77ef2696dce5fc2383bd8ef1cf3b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 05:24:40 +0200 Subject: [PATCH 0778/1248] New limiter based quests --- AI/Nullkiller/Goals/CompleteQuest.cpp | 7 +- AI/VCAI/Goals/CompleteQuest.cpp | 19 +- lib/mapObjects/CQuest.cpp | 300 ++++++++++-------------- lib/mapObjects/CQuest.h | 18 +- lib/mapping/MapFormatH3M.cpp | 34 ++- lib/rewardable/Limiter.cpp | 48 ++++ lib/rewardable/Limiter.h | 7 +- lib/rewardable/Reward.h | 2 +- lib/rmg/modificators/TreasurePlacer.cpp | 6 +- 9 files changed, 205 insertions(+), 236 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index e4d3fda47..00518d51b 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -63,9 +63,6 @@ TGoalVec CompleteQuest::decompose() const return missionLevel(); case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - break; case CQuest::MISSION_KEYMASTER: @@ -137,7 +134,7 @@ TGoalVec CompleteQuest::missionArt() const CaptureObjectsBehavior findArts; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->artifacts) { solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); } @@ -223,7 +220,7 @@ TGoalVec CompleteQuest::missionResources() const TGoalVec CompleteQuest::missionDestroyObj() const { - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return CaptureObjectsBehavior(q.obj).decompose(); diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index f724e308b..fb851bf04 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -54,9 +54,6 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() return missionLevel(); case CQuest::MISSION_PLAYER: - if(ai->playerID.getNum() != q.quest->m13489val) - logAi->debug("Can't be player of color %d", q.quest->m13489val); - break; case CQuest::MISSION_KEYMASTER: @@ -137,7 +134,7 @@ TGoalVec CompleteQuest::missionArt() const if(!solutions.empty()) return solutions; - for(auto art : q.quest->m5arts) + for(auto art : q.quest->artifacts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } @@ -165,7 +162,7 @@ TGoalVec CompleteQuest::missionArmy() const if(!solutions.empty()) return solutions; - for(auto creature : q.quest->m6creatures) + for(auto creature : q.quest->creatures) { solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); } @@ -179,7 +176,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const if(solutions.empty()) { - for(int i = 0; i < q.quest->m2stats.size(); ++i) + for(int i = 0; i < q.quest->primary.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); @@ -195,7 +192,7 @@ TGoalVec CompleteQuest::missionLevel() const if(solutions.empty()) { - logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); + logAi->debug("Don't know how to reach hero level %d", q.quest->heroLevel); } return solutions; @@ -227,10 +224,10 @@ TGoalVec CompleteQuest::missionResources() const } else { - for(int i = 0; i < q.quest->m7resources.size(); ++i) + for(int i = 0; i < q.quest->resources.size(); ++i) { - if(q.quest->m7resources[i]) - solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->m7resources[i]))); + if(q.quest->resources[i]) + solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->resources[i]))); } } } @@ -246,7 +243,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const { TGoalVec solutions; - auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); + auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget); if(!obj) return ai->ah->howToVisitObj(q.obj); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 18ee673f5..e744226b0 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -40,7 +40,7 @@ CQuest::CQuest(): missionType(MISSION_NONE), progress(NOT_ACTIVE), lastDay(-1), - m13489val(0), + killTarget(-1), textOption(0), completedOption(0), stackDirection(0), @@ -99,7 +99,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) ui32 count = 0; ui32 slotsCount = 0; bool hasExtraCreatures = false; - for(cre = q->m6creatures.begin(); cre != q->m6creatures.end(); ++cre) + for(cre = q->creatures.begin(); cre != q->creatures.end(); ++cre) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { @@ -121,110 +121,53 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) bool CQuest::checkQuest(const CGHeroInstance * h) const { - switch (missionType) + if(!heroAllowed(h)) + return false; + + if(killTarget >= 0) { - case MISSION_NONE: - return true; - case MISSION_LEVEL: - return m13489val <= h->level; - case MISSION_PRIMARY_STAT: - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - { - if(h->getPrimSkillLevel(static_cast(i)) < static_cast(m2stats[i])) - return false; - } - return true; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if(!CGHeroInstance::cb->getObjByQuestIdentifier(m13489val)) - return true; - return false; - case MISSION_ART: - { - // if the object was deserialized - if(artifactsRequirements.empty()) - for(const auto & id : m5arts) - ++artifactsRequirements[id]; - - size_t reqSlots = 0; - for(const auto & elem : artifactsRequirements) - { - // check required amount of artifacts - if(h->getArtPosCount(elem.first, false, true, true) < elem.second) - return false; - if(!h->hasArt(elem.first)) - reqSlots += h->getAssemblyByConstituent(elem.first)->getPartsInfo().size() - 2; - } - if(ArtifactUtils::isBackpackFreeSlots(h, reqSlots)) - return true; - else - return false; - } - case MISSION_ARMY: - return checkMissionArmy(this, h); - case MISSION_RESOURCES: - for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) //including Mithril ? - { //Quest has no direct access to callback - if(CGHeroInstance::cb->getResource(h->tempOwner, i) < static_cast(m7resources[i])) - return false; - } - return true; - case MISSION_HERO: - return m13489val == h->type->getIndex(); - case MISSION_PLAYER: - return m13489val == h->getOwner().getNum(); - default: + if(!CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; } + + return true; } void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { - switch (missionType) + for(auto & elem : artifacts) { - case CQuest::MISSION_ART: - for(auto & elem : m5arts) - { - if(h->hasArt(elem)) - { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); - } - else - { - const auto * assembly = h->getAssemblyByConstituent(elem); - assert(assembly); - auto parts = assembly->getPartsInfo(); + if(h->hasArt(elem)) + { + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + } + else + { + const auto * assembly = h->getAssemblyByConstituent(elem); + assert(assembly); + auto parts = assembly->getPartsInfo(); - // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + // Remove the assembly + cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); - // Disassemble this backpack artifact - for(const auto & ci : parts) - { - if(ci.art->getTypeId() != elem) - cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); - } - } - } - break; - case CQuest::MISSION_ARMY: - cb->takeCreatures(h->id, m6creatures); - break; - case CQuest::MISSION_RESOURCES: - for (int i = 0; i < 7; ++i) + // Disassemble this backpack artifact + for(const auto & ci : parts) { - cb->giveResource(h->getOwner(), static_cast(i), -static_cast(m7resources[i])); + if(ci.art->getTypeId() != elem) + cb->giveHeroNewArtifact(h, ci.art->artType, ArtifactPosition::BACKPACK_START); } - break; - default: - break; + } } + + cb->takeCreatures(h->id, creatures); + cb->giveResources(h->getOwner(), resources); } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const { MetaString text; bool failRequirements = (h ? !checkQuest(h) : true); + loadComponents(components, h); if(firstVisit) { @@ -241,20 +184,18 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components switch (missionType) { case MISSION_LEVEL: - components.emplace_back(Component::EComponentType::EXPERIENCE, 0, m13489val, 0); if(!isCustom) - iwText.replaceNumber(m13489val); + iwText.replaceNumber(heroLevel); //TODO: heroLevel break; case MISSION_PRIMARY_STAT: { MetaString loot; for(int i = 0; i < 4; ++i) { - if(m2stats[i]) + if(primary[i]) { - components.emplace_back(Component::EComponentType::PRIM_SKILL, i, m2stats[i], 0); loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -268,10 +209,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components addReplacements(iwText, text.toString()); break; case MISSION_HERO: - //FIXME: portrait may not match hero, if custom portrait was set in map editor - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroh->objects[m13489val]->imageIndex, 0, 0); - if(!isCustom) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + if(!isCustom && !heroes.empty()) + iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_KILL_CREATURE: { @@ -285,9 +224,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { - components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); } @@ -298,9 +236,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components case MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { - components.emplace_back(elem); loot.appendRawString("%s"); loot.replaceCreatureName(elem); } @@ -313,11 +250,10 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components MetaString loot; for(int i = 0; i < 7; ++i) { - if(m7resources[i]) + if(resources[i]) { - components.emplace_back(Component::EComponentType::RESOURCE, i, m7resources[i], 0); loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -326,9 +262,8 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components } break; case MISSION_PLAYER: - components.emplace_back(Component::EComponentType::FLAG, m13489val, 0, 0); - if(!isCustom) - iwText.replaceLocalString(EMetaText::COLOR, m13489val); + if(!isCustom && !players.empty()) + iwText.replaceLocalString(EMetaText::COLOR, players.front()); break; } } @@ -349,17 +284,17 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const switch(missionType) { case MISSION_LEVEL: - ms.replaceNumber(m13489val); + ms.replaceNumber(heroLevel); break; case MISSION_PRIMARY_STAT: { MetaString loot; for (int i = 0; i < 4; ++i) { - if (m2stats[i]) + if (primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -375,7 +310,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const case MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -386,7 +321,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const case MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -399,10 +334,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const MetaString loot; for (int i = 0; i < 7; ++i) { - if (m7resources[i]) + if (resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -410,10 +345,10 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const } break; case MISSION_HERO: - ms.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + ms.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_PLAYER: - ms.replaceRawString(VLC->generaltexth->colors[m13489val]); + ms.replaceRawString(VLC->generaltexth->colors[players.front()]); break; default: break; @@ -427,18 +362,18 @@ void CQuest::getCompletionText(MetaString &iwText) const { case CQuest::MISSION_LEVEL: if (!isCustomComplete) - iwText.replaceNumber(m13489val); + iwText.replaceNumber(heroLevel); break; case CQuest::MISSION_PRIMARY_STAT: { MetaString loot; - assert(m2stats.size() <= 4); - for (int i = 0; i < m2stats.size(); ++i) + assert(primary.size() <= 4); + for (int i = 0; i < primary.size(); ++i) { - if (m2stats[i]) + if (primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m2stats[i]); + loot.replaceNumber(primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } @@ -449,7 +384,7 @@ void CQuest::getCompletionText(MetaString &iwText) const case CQuest::MISSION_ART: { MetaString loot; - for(const auto & elem : m5arts) + for(const auto & elem : artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -461,7 +396,7 @@ void CQuest::getCompletionText(MetaString &iwText) const case CQuest::MISSION_ARMY: { MetaString loot; - for(const auto & elem : m6creatures) + for(const auto & elem : creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -475,10 +410,10 @@ void CQuest::getCompletionText(MetaString &iwText) const MetaString loot; for (int i = 0; i < 7; ++i) { - if (m7resources[i]) + if (resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(m7resources[i]); + loot.replaceNumber(resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } @@ -492,22 +427,16 @@ void CQuest::getCompletionText(MetaString &iwText) const addReplacements(iwText, completedText.toString()); break; case MISSION_HERO: - if (!isCustomComplete) - iwText.replaceRawString(VLC->heroh->objects[m13489val]->getNameTranslated()); + if (!isCustomComplete && !heroes.empty()) + iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); break; case MISSION_PLAYER: - if (!isCustomComplete) - iwText.replaceRawString(VLC->generaltexth->colors[m13489val]); + if (!isCustomComplete && !players.empty()) + iwText.replaceRawString(VLC->generaltexth->colors[players.front()]); break; } } -void CQuest::addArtifactID(const ArtifactID & id) -{ - m5arts.push_back(id); - ++artifactsRequirements[id]; -} - void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { auto q = handler.enterStruct(fieldName); @@ -522,6 +451,8 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi isCustomNext = !nextVisitText.empty(); isCustomComplete = !completedText.empty(); } + + Rewardable::Limiter::serializeJson(handler); static const std::vector MISSION_TYPE_JSON = { @@ -530,57 +461,64 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi handler.serializeEnum("missionType", missionType, Emission::MISSION_NONE, MISSION_TYPE_JSON); handler.serializeInt("timeLimit", lastDay, -1); + handler.serializeInstance("killTarget", killTarget, -1); - switch (missionType) + if(!handler.saving) { - case MISSION_NONE: - break; - case MISSION_LEVEL: - handler.serializeInt("heroLevel", m13489val, -1); - break; - case MISSION_PRIMARY_STAT: + switch (missionType) { - auto primarySkills = handler.enterStruct("primarySkills"); - if(!handler.saving) - m2stats.resize(GameConstants::PRIMARY_SKILLS); - - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], m2stats[i], 0); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - handler.serializeInstance("killTarget", m13489val, static_cast(-1)); - break; - case MISSION_ART: - //todo: ban artifacts - handler.serializeIdArray("artifacts", m5arts); - break; - case MISSION_ARMY: - { - auto a = handler.enterArray("creatures"); - a.serializeStruct(m6creatures); - } - break; - case MISSION_RESOURCES: - { - auto r = handler.enterStruct("resources"); - - for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) + case MISSION_NONE: + break; + case MISSION_LEVEL: + handler.serializeInt("heroLevel", heroLevel, -1); + break; + case MISSION_PRIMARY_STAT: + { + auto primarySkills = handler.enterStruct("primarySkills"); + for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + } + break; + case MISSION_KILL_HERO: + case MISSION_KILL_CREATURE: + break; + case MISSION_ART: + handler.serializeIdArray("artifacts", artifacts); + break; + case MISSION_ARMY: { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], m7resources[idx], 0); + auto a = handler.enterArray("creatures"); + a.serializeStruct(creatures); } + break; + case MISSION_RESOURCES: + { + auto r = handler.enterStruct("resources"); + + for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) + { + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); + } + } + break; + case MISSION_HERO: + { + ui32 temp; + handler.serializeId("hero", temp, 0); + heroes.emplace_back(temp); + } + break; + case MISSION_PLAYER: + { + ui32 temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); + players.emplace_back(temp); + } + break; + default: + logGlobal->error("Invalid quest mission type"); + break; } - break; - case MISSION_HERO: - handler.serializeId("hero", m13489val, 0); - break; - case MISSION_PLAYER: - handler.serializeId("player", m13489val, PlayerColor::NEUTRAL); - break; - default: - logGlobal->error("Invalid quest mission type"); - break; } } @@ -803,7 +741,7 @@ int CGSeerHut::checkDirection() const const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; assert(o && (o->ID == Obj::HERO || o->ID == Obj::PRISON)); @@ -812,7 +750,7 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const { - const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->m13489val); + const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; assert(o && o->ID == Obj::MONSTER); diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 535691b98..3485a51a1 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -17,10 +17,8 @@ VCMI_LIB_NAMESPACE_BEGIN class CGCreature; -class DLL_LINKAGE CQuest final +class DLL_LINKAGE CQuest: public Rewardable::Limiter { - mutable std::unordered_map artifactsRequirements; // artifact ID -> required count - public: enum Emission { MISSION_NONE = 0, @@ -54,12 +52,7 @@ public: Emission missionType; Eprogress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit - - ui32 m13489val; - std::vector m2stats; - std::vector m5arts; // artifact IDs. Add IDs through addArtifactID(), not directly to the field. - std::vector m6creatures; //pair[cre id, cre count], CreatureSet info irrelevant - TResources m7resources; + int killTarget; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info @@ -85,7 +78,6 @@ public: virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; virtual void addReplacements(MetaString &out, const std::string &base) const; - void addArtifactID(const ArtifactID & id); bool operator== (const CQuest & quest) const { @@ -98,11 +90,6 @@ public: h & missionType; h & progress; h & lastDay; - h & m13489val; - h & m2stats; - h & m5arts; - h & m6creatures; - h & m7resources; h & textOption; h & stackToKill; h & stackDirection; @@ -115,6 +102,7 @@ public: h & isCustomNext; h & isCustomComplete; h & completedOption; + h & static_cast(*this); } void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 70646288f..08b3e1545 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1871,7 +1871,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(artID != ArtifactID::NONE) { //not none quest - hut->quest->addArtifactID(artID); + hut->quest->artifacts.push_back(artID); hut->quest->missionType = CQuest::MISSION_ART; } else @@ -1986,10 +1986,9 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) return; case CQuest::MISSION_PRIMARY_STAT: { - guard->quest->m2stats.resize(4); for(int x = 0; x < 4; ++x) { - guard->quest->m2stats[x] = reader->readUInt8(); + guard->quest->primary[x] = reader->readUInt8(); } } break; @@ -1997,7 +1996,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case CQuest::MISSION_KILL_HERO: case CQuest::MISSION_KILL_CREATURE: { - guard->quest->m13489val = reader->readUInt32(); + guard->quest->killTarget = reader->readUInt32(); break; } case CQuest::MISSION_ART: @@ -2006,7 +2005,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) for(int yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); - guard->quest->addArtifactID(artid); + guard->quest->artifacts.push_back(artid); map->allowedArtifact[artid] = false; //these are unavailable for random generation } break; @@ -2014,29 +2013,29 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case CQuest::MISSION_ARMY: { int typeNumber = reader->readUInt8(); - guard->quest->m6creatures.resize(typeNumber); + guard->quest->creatures.resize(typeNumber); for(int hh = 0; hh < typeNumber; ++hh) { - guard->quest->m6creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->m6creatures[hh].count = reader->readUInt16(); + guard->quest->creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->creatures[hh].count = reader->readUInt16(); } break; } case CQuest::MISSION_RESOURCES: { for(int x = 0; x < 7; ++x) - guard->quest->m7resources[x] = reader->readUInt32(); + guard->quest->resources[x] = reader->readUInt32(); break; } case CQuest::MISSION_HERO: { - guard->quest->m13489val = reader->readHero().getNum(); + guard->quest->heroes.push_back(reader->readHero()); break; } case CQuest::MISSION_PLAYER: { - guard->quest->m13489val = reader->readPlayer().getNum(); + guard->quest->players.push_back(reader->readPlayer()); break; } case CQuest::MISSION_HOTA_MULTI: @@ -2045,22 +2044,19 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 0) { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_HERO_CLASS; + guard->quest->missionType = CQuest::MISSION_HOTA_HERO_CLASS; std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); - - logGlobal->warn("Map '%s': Quest at %s 'Belong to one of %d classes' is not implemented!", mapName, position.toString(), heroClasses.size()); + for(auto & hc : heroClasses) + guard->quest->heroClasses.push_back(hc); break; } if(missionSubID == 1) { - guard->quest->missionType = CQuest::MISSION_NONE; //TODO: CQuest::MISSION_HOTA_REACH_DATE; - uint32_t daysPassed = reader->readUInt32(); - - logGlobal->warn("Map '%s': Quest at %s 'Wait till %d days passed' is not implemented!", mapName, position.toString(), daysPassed); + guard->quest->missionType = CQuest::MISSION_HOTA_REACH_DATE; + guard->quest->daysPassed = reader->readUInt32(); break; } - assert(0); break; } default: diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 9255fbbb1..e32eeb778 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -146,6 +146,54 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } +void Rewardable::Limiter::loadComponents(std::vector & comps, + const CGHeroInstance * h) const +{ + if (heroExperience) + { + comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); + } + if (heroLevel) + comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + + if (manaPoints || manaPercentage > 0) + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, 0, 0); + + for (size_t i=0; i(i), primary[i], 0); + } + + for(const auto & entry : secondary) + comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + + for(const auto & entry : artifacts) + comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + + for(const auto & entry : spells) + comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + + for(const auto & entry : creatures) + comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + + for(const auto & entry : players) + comps.emplace_back(Component::EComponentType::FLAG, entry, 0, 0); + + //FIXME: portrait may not match hero, if custom portrait was set in map editor + for(const auto & entry : heroes) + comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroTypes()->getById(entry)->getIconIndex(), 0, 0); + + for(const auto & entry : heroClasses) + comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroClasses()->getById(entry)->getIconIndex(), 0, 0); + + for (size_t i=0; i(i), resources[i], 0); + } +} + void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("dayOfWeek", dayOfWeek); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 300e572ae..5a29b0eaa 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -17,6 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGHeroInstance; class CStackBasicDescriptor; +struct Component; namespace Rewardable { @@ -78,9 +79,13 @@ struct DLL_LINKAGE Limiter LimitersList noneOf; Limiter(); - ~Limiter(); + virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; + + /// Generates list of components that describes reward for a specific hero + virtual void loadComponents(std::vector & comps, + const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) { diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 3167a3546..59fd44bcc 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -86,7 +86,7 @@ struct DLL_LINKAGE Reward si32 calculateManaPoints(const CGHeroInstance * h) const; Reward(); - ~Reward(); + virtual ~Reward(); template void serialize(Handler &h, const int version) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index eff834612..f3d44ad8f 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -464,7 +464,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; @@ -515,7 +515,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; @@ -540,7 +540,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->addArtifactID(artid); + obj->quest->artifacts.push_back(artid); obj->quest->lastDay = -1; obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; From 3a17eeb3301ec760582a09cb9bfe71aade1dd8e4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 11:42:42 +0200 Subject: [PATCH 0779/1248] Fix typos in rewards widget --- mapeditor/inspector/rewardswidget.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index cfd900326..40ac8f392 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -347,7 +347,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) if(widget->value()) - vinfo.reward.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + vinfo.limiter.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } } @@ -445,10 +445,10 @@ void RewardsWidget::loadCurrentVisitInfo(int index) ui->lHeroExperience->setValue(vinfo.limiter.heroExperience); ui->lManaPoints->setValue(vinfo.limiter.manaPoints); ui->lManaPercentage->setValue(vinfo.limiter.manaPercentage); - ui->lAttack->setValue(vinfo.reward.primary[0]); - ui->lDefence->setValue(vinfo.reward.primary[1]); - ui->lPower->setValue(vinfo.reward.primary[2]); - ui->lKnowledge->setValue(vinfo.reward.primary[3]); + ui->lAttack->setValue(vinfo.limiter.primary[0]); + ui->lDefence->setValue(vinfo.limiter.primary[1]); + ui->lPower->setValue(vinfo.limiter.primary[2]); + ui->lKnowledge->setValue(vinfo.limiter.primary[3]); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) From 63bdfb8ff622f0e42d256301a098ff73c69e71df Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 15:54:14 +0200 Subject: [PATCH 0780/1248] New quests implemented --- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/CQuest.h | 1 + mapeditor/inspector/inspector.cpp | 5 +- mapeditor/inspector/questwidget.cpp | 490 ++++++++++++++------ mapeditor/inspector/questwidget.h | 30 +- mapeditor/inspector/questwidget.ui | 626 +++++++++++++++++++++++++- mapeditor/inspector/rewardswidget.cpp | 86 ++++ mapeditor/inspector/rewardswidget.ui | 116 ++++- 8 files changed, 1196 insertions(+), 162 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index e744226b0..98f235e6b 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -46,7 +46,8 @@ CQuest::CQuest(): stackDirection(0), isCustomFirst(false), isCustomNext(false), - isCustomComplete(false) + isCustomComplete(false), + repeatedQuest(false) { } @@ -444,6 +445,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi handler.serializeStruct("firstVisitText", firstVisitText); handler.serializeStruct("nextVisitText", nextVisitText); handler.serializeStruct("completedText", completedText); + handler.serializeBool("repeatedQuest", repeatedQuest, false); if(!handler.saving) { diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3485a51a1..6f3976c07 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -53,6 +53,7 @@ public: Eprogress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit int killTarget; + bool repeatedQuest; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index d6fa5183b..c19eda842 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -410,7 +410,7 @@ void Inspector::updateProperties(CGEvent * o) void Inspector::updateProperties(CGSeerHut * o) { - if(!o) return; + if(!o || !o->quest) return; { //Mission type auto * delegate = new InspectorDelegate; @@ -421,9 +421,10 @@ void Inspector::updateProperties(CGSeerHut * o) addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); + addProperty("Repeat quest", o->quest->repeatedQuest, false); { //Quest - auto * delegate = new QuestDelegate(*controller.map(), *o); + auto * delegate = new QuestDelegate(controller, *o->quest); addProperty("Quest", PropertyEditorPlaceholder(), delegate, false); } } diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index a21392983..0cf1a1315 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -10,21 +10,122 @@ #include "StdInc.h" #include "questwidget.h" #include "ui_questwidget.h" +#include "../mapcontroller.h" #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" +#include "../lib/spells/CSpellHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/CHeroHandler.h" #include "../lib/constants/StringConstants.h" #include "../lib/mapping/CMap.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGCreature.h" -QuestWidget::QuestWidget(const CMap & _map, CGSeerHut & _sh, QWidget *parent) : +QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *parent) : QDialog(parent), - map(_map), - seerhut(_sh), + controller(_controller), + quest(_sh), ui(new Ui::QuestWidget) { + setAttribute(Qt::WA_DeleteOnClose, true); ui->setupUi(this); + + ui->lDayOfWeek->addItem(tr("None")); + for(int i = 1; i <= 7; ++i) + ui->lDayOfWeek->addItem(tr("Day %1").arg(i)); + + //fill resources + ui->lResources->setRowCount(GameConstants::RESOURCE_QUANTITY - 1); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY - 1; ++i) + { + auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + ui->lResources->setItem(i, 0, item); + ui->lResources->setCellWidget(i, 1, new QSpinBox); + } + + //fill artifacts + for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->artifacts()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!controller.map()->allowedArtifact[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + ui->lArtifacts->addItem(item); + } + + //fill spells + for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + { + auto * item = new QListWidgetItem(QString::fromStdString(VLC->spells()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + if(!controller.map()->allowedSpells[i]) + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + ui->lSpells->addItem(item); + } + + //fill skills + ui->lSkills->setRowCount(controller.map()->allowedAbilities.size()); + for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + { + auto * item = new QTableWidgetItem(QString::fromStdString(VLC->skills()->getByIndex(i)->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(i)); + + auto * widget = new QComboBox; + for(auto & s : NSecondarySkill::levels) + widget->addItem(QString::fromStdString(s)); + + if(!controller.map()->allowedAbilities[i]) + { + item->setFlags(item->flags() & ~Qt::ItemIsEnabled); + widget->setEnabled(false); + } + + ui->lSkills->setItem(i, 0, item); + ui->lSkills->setCellWidget(i, 1, widget); + } + + //fill creatures + for(auto & creature : VLC->creh->objects) + { + ui->lCreatureId->addItem(QString::fromStdString(creature->getNameSingularTranslated())); + ui->lCreatureId->setItemData(ui->lCreatureId->count() - 1, creature->getIndex()); + } + + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } } QuestWidget::~QuestWidget() @@ -34,137 +135,259 @@ QuestWidget::~QuestWidget() void QuestWidget::obtainData() { - assert(seerhut.quest); - bool activeId = false; - bool activeAmount = false; - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - activeAmount = true; - ui->targetId->addItem("Reach level"); - ui->targetAmount->setText(QString::number(seerhut.quest->m13489val)); - break; - case CQuest::Emission::MISSION_PRIMARY_STAT: - activeId = true; - activeAmount = true; - for(auto s : NPrimarySkill::names) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m2stats.size(); ++i) - { - if(seerhut.quest->m2stats[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m2stats[i])); - break; //TODO: support multiple stats - } - } - break; - case CQuest::Emission::MISSION_KILL_HERO: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_KILL_CREATURE: - activeId = true; - //TODO: implement - break; - case CQuest::Emission::MISSION_ART: - activeId = true; - for(int i = 0; i < map.allowedArtifact.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->arth->objects.at(i)->getNameTranslated())); - if(!seerhut.quest->m5arts.empty()) - ui->targetId->setCurrentIndex(seerhut.quest->m5arts.front()); - //TODO: support multiple artifacts - break; - case CQuest::Emission::MISSION_ARMY: - activeId = true; - activeAmount = true; - break; - case CQuest::Emission::MISSION_RESOURCES: - activeId = true; - activeAmount = true; - for(auto s : GameConstants::RESOURCE_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - for(int i = 0; i < seerhut.quest->m7resources.size(); ++i) - { - if(seerhut.quest->m7resources[i] > 0) - { - ui->targetId->setCurrentIndex(i); - ui->targetAmount->setText(QString::number(seerhut.quest->m7resources[i])); - break; //TODO: support multiple resources - } - } - break; - case CQuest::Emission::MISSION_HERO: - activeId = true; - for(int i = 0; i < map.allowedHeroes.size(); ++i) - ui->targetId->addItem(QString::fromStdString(VLC->heroh->objects.at(i)->getNameTranslated())); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_PLAYER: - activeId = true; - for(auto s : GameConstants::PLAYER_COLOR_NAMES) - ui->targetId->addItem(QString::fromStdString(s)); - ui->targetId->setCurrentIndex(seerhut.quest->m13489val); - break; - case CQuest::Emission::MISSION_KEYMASTER: - break; - default: - break; + ui->lDayOfWeek->setCurrentIndex(quest.dayOfWeek); + ui->lDaysPassed->setValue(quest.daysPassed); + ui->lHeroLevel->setValue(quest.heroLevel); + ui->lHeroExperience->setValue(quest.heroExperience); + ui->lManaPoints->setValue(quest.manaPoints); + ui->lManaPercentage->setValue(quest.manaPercentage); + ui->lAttack->setValue(quest.primary[0]); + ui->lDefence->setValue(quest.primary[1]); + ui->lPower->setValue(quest.primary[2]); + ui->lKnowledge->setValue(quest.primary[3]); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + widget->setValue(quest.resources[i]); } - ui->targetId->setEnabled(activeId); - ui->targetAmount->setEnabled(activeAmount); -} - -QString QuestWidget::commitChanges() -{ - assert(seerhut.quest); - switch(seerhut.quest->missionType) { - case CQuest::Emission::MISSION_LEVEL: - seerhut.quest->m13489val = ui->targetAmount->text().toInt(); - return QString("Reach lvl ").append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_PRIMARY_STAT: - seerhut.quest->m2stats.resize(sizeof(NPrimarySkill::names), 0); - seerhut.quest->m2stats[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support multiple stats - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_KILL_HERO: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_KILL_CREATURE: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_ART: - seerhut.quest->m5arts.clear(); - seerhut.quest->m5arts.push_back(ArtifactID(ui->targetId->currentIndex())); - //TODO: support multiple artifacts - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_ARMY: - //TODO: implement - return QString("N/A"); - case CQuest::Emission::MISSION_RESOURCES: - seerhut.quest->m7resources[ui->targetId->currentIndex()] = ui->targetAmount->text().toInt(); - //TODO: support resources - return ui->targetId->currentText().append(ui->targetAmount->text()); - case CQuest::Emission::MISSION_HERO: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_PLAYER: - seerhut.quest->m13489val = ui->targetId->currentIndex(); - return ui->targetId->currentText(); - case CQuest::Emission::MISSION_KEYMASTER: - return QString("N/A"); - default: - return QString("N/A"); + for(auto i : quest.artifacts) + ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto i : quest.spells) + ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); + for(auto & i : quest.secondary) + { + int index = VLC->skills()->getById(i.first)->getIndex(); + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) + widget->setCurrentIndex(i.second); } + for(auto & i : quest.creatures) + { + int index = i.type->getIndex(); + ui->lCreatureId->setCurrentIndex(index); + ui->lCreatureAmount->setValue(i.count); + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); + } + for(auto & i : quest.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.heroClasses) + { + for(int e = 0; e < ui->lHeroClasses->count(); ++e) + { + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : quest.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + + if(quest.killTarget >= 0 && quest.killTarget < controller.map()->objects.size()) + ui->lKillTarget->setText(QString::fromStdString(controller.map()->objects[quest.killTarget]->instanceName)); + else + quest.killTarget = -1; } -QuestDelegate::QuestDelegate(const CMap & m, CGSeerHut & t): map(m), seerhut(t), QStyledItemDelegate() +bool QuestWidget::commitChanges() +{ + quest.dayOfWeek = ui->lDayOfWeek->currentIndex(); + quest.daysPassed = ui->lDaysPassed->value(); + quest.heroLevel = ui->lHeroLevel->value(); + quest.heroExperience = ui->lHeroExperience->value(); + quest.manaPoints = ui->lManaPoints->value(); + quest.manaPercentage = ui->lManaPercentage->value(); + quest.primary[0] = ui->lAttack->value(); + quest.primary[1] = ui->lDefence->value(); + quest.primary[2] = ui->lPower->value(); + quest.primary[3] = ui->lKnowledge->value(); + for(int i = 0; i < ui->lResources->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) + quest.resources[i] = widget->value(); + } + + quest.artifacts.clear(); + for(int i = 0; i < ui->lArtifacts->count(); ++i) + { + if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) + quest.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + } + quest.spells.clear(); + for(int i = 0; i < ui->lSpells->count(); ++i) + { + if(ui->lSpells->item(i)->checkState() == Qt::Checked) + quest.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + } + + quest.secondary.clear(); + for(int i = 0; i < ui->lSkills->rowCount(); ++i) + { + if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) + { + if(widget->currentIndex() > 0) + quest.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + } + } + + quest.creatures.clear(); + for(int i = 0; i < ui->lCreatures->rowCount(); ++i) + { + int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); + if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) + if(widget->value()) + quest.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + } + + quest.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + quest.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + quest.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + quest.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + quest.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + quest.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } + + //quest.killTarget is set directly in object picking + + return true; +} + +void QuestWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) +{ + QTableWidgetItem * item = nullptr; + QSpinBox * widget = nullptr; + for(int i = 0; i < listWidget->rowCount(); ++i) + { + if(auto * cname = listWidget->item(i, 0)) + { + if(cname->data(Qt::UserRole).toInt() == comboWidget->currentData().toInt()) + { + item = cname; + widget = qobject_cast(listWidget->cellWidget(i, 1)); + break; + } + } + } + + if(!item) + { + listWidget->setRowCount(listWidget->rowCount() + 1); + item = new QTableWidgetItem(comboWidget->currentText()); + listWidget->setItem(listWidget->rowCount() - 1, 0, item); + } + + item->setData(Qt::UserRole, comboWidget->currentData()); + + if(!widget) + { + widget = new QSpinBox; + widget->setRange(spinWidget->minimum(), spinWidget->maximum()); + listWidget->setCellWidget(listWidget->rowCount() - 1, 1, widget); + } + + widget->setValue(spinWidget->value()); +} + +void QuestWidget::on_lKillTargetSelect_clicked() +{ + auto pred = [](const CGObjectInstance * obj) -> bool + { + if(auto * o = dynamic_cast(obj)) + return o->ID != Obj::PRISON; + if(auto * o = dynamic_cast(obj)) + return true; + return false; + }; + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.highlight(pred); + l.update(); + QObject::connect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + hide(); +} + +void QuestWidget::onTargetPicked(const CGObjectInstance * obj) +{ + show(); + + for(int lvl : {0, 1}) + { + auto & l = controller.scene(lvl)->objectPickerView; + l.clear(); + l.update(); + QObject::disconnect(&l, &ObjectPickerLayer::selectionMade, this, &QuestWidget::onTargetPicked); + } + + if(!obj) //discarded + { + quest.killTarget = -1; + ui->lKillTarget->setText(""); + return; + } + + ui->lKillTarget->setText(QString::fromStdString(obj->instanceName)); + quest.killTarget = obj->id; +} + +void QuestWidget::on_lCreatureAdd_clicked() +{ + onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); +} + + +void QuestWidget::on_lCreatureRemove_clicked() +{ + std::set> rowsToRemove; + for(auto * i : ui->lCreatures->selectedItems()) + rowsToRemove.insert(i->row()); + + for(auto i : rowsToRemove) + ui->lCreatures->removeRow(i); +} + +QuestDelegate::QuestDelegate(MapController & c, CQuest & t): controller(c), quest(t), QStyledItemDelegate() { } QWidget * QuestDelegate::createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const { - return new QuestWidget(map, seerhut, parent); + return new QuestWidget(controller, quest, parent); } void QuestDelegate::setEditorData(QWidget * editor, const QModelIndex & index) const @@ -183,11 +406,26 @@ void QuestDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, c { if(auto *ed = qobject_cast(editor)) { - auto quest = ed->commitChanges(); - model->setData(index, quest); + ed->commitChanges(); } else { QStyledItemDelegate::setModelData(editor, model, index); } } + +bool QuestDelegate::eventFilter(QObject * object, QEvent * event) +{ + if(auto * ed = qobject_cast(object)) + { + if(event->type() == QEvent::Hide || event->type() == QEvent::FocusOut) + return false; + if(event->type() == QEvent::Close) + { + emit commitData(ed); + emit closeEditor(ed); + return true; + } + } + return QStyledItemDelegate::eventFilter(object, event); +} diff --git a/mapeditor/inspector/questwidget.h b/mapeditor/inspector/questwidget.h index cef1ad551..3237c0b65 100644 --- a/mapeditor/inspector/questwidget.h +++ b/mapeditor/inspector/questwidget.h @@ -16,20 +16,33 @@ namespace Ui { class QuestWidget; } +class MapController; + class QuestWidget : public QDialog { Q_OBJECT public: - explicit QuestWidget(const CMap &, CGSeerHut &, QWidget *parent = nullptr); + explicit QuestWidget(MapController &, CQuest &, QWidget *parent = nullptr); ~QuestWidget(); void obtainData(); - QString commitChanges(); + bool commitChanges(); + +private slots: + void onTargetPicked(const CGObjectInstance *); + + void on_lKillTargetSelect_clicked(); + + void on_lCreatureAdd_clicked(); + + void on_lCreatureRemove_clicked(); private: - CGSeerHut & seerhut; - const CMap & map; + void onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget); + + CQuest & quest; + MapController & controller; Ui::QuestWidget *ui; }; @@ -39,13 +52,16 @@ class QuestDelegate : public QStyledItemDelegate public: using QStyledItemDelegate::QStyledItemDelegate; - QuestDelegate(const CMap &, CGSeerHut &); + QuestDelegate(MapController &, CQuest &); QWidget * createEditor(QWidget * parent, const QStyleOptionViewItem & option, const QModelIndex & index) const override; void setEditorData(QWidget * editor, const QModelIndex & index) const override; void setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const override; +protected: + bool eventFilter(QObject * object, QEvent * event) override; + private: - CGSeerHut & seerhut; - const CMap & map; + CQuest & quest; + MapController & controller; }; diff --git a/mapeditor/inspector/questwidget.ui b/mapeditor/inspector/questwidget.ui index c5f2931b8..f08b3bf94 100644 --- a/mapeditor/inspector/questwidget.ui +++ b/mapeditor/inspector/questwidget.ui @@ -9,8 +9,8 @@ 0 0 - 429 - 89 + 531 + 495 @@ -19,34 +19,616 @@ true - + - - - - 0 - 0 - + + + + + Day of week + + + + + + + + 120 + 0 + + + + + + + + Days passed + + + + + + + + 20 + 0 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Hero level + + + + + + + + 40 + 0 + + + + + + + + Hero experience + + + + + + + + 80 + 0 + + + + 100000 + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Spell points + + + + + + + + 60 + 0 + + + + 999 + + + + + + + % + + + 100 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Kill hero/monster + + + + + + + true + + + + + + + ... + + + + + + + + + Primary skills + + + 12 + + + 3 + + + 3 + + + + + Attack + + + + + + + + + + Defence + + + + + + + + + + Spell power + + + + + + + + + + Knowledge + + + + + + + - - - - 0 - 0 - + + + Qt::LeftToRight - - - 60 - 16777215 - + + QTabWidget::North - - Qt::ImhDigitsOnly + + QTabWidget::Rounded + + 0 + + + Qt::ElideNone + + + true + + + false + + + true + + + + Resources + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + 2 + + + false + + + 180 + + + true + + + false + + + 24 + + + + + + + + + + Artifacts + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Spells + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Skills + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Creatures + + + + 0 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + 0 + 0 + + + + + + + + + 60 + 0 + + + + 9999 + + + QAbstractSpinBox::AdaptiveDecimalStepType + + + + + + + Add + + + + + + + Remove + + + + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + 2 + + + false + + + 180 + + + false + + + 24 + + + + + + + + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 40ac8f392..a509f4a24 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -13,6 +13,7 @@ #include "../lib/VCMI_Lib.h" #include "../lib/CSkillHandler.h" #include "../lib/spells/CSpellHandler.h" +#include "../lib/CHeroHandler.h" #include "../lib/CArtHandler.h" #include "../lib/CCreatureHandler.h" #include "../lib/constants/StringConstants.h" @@ -131,6 +132,36 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : } } + //fill heroes + VLC->heroTypes()->forEach([this](const HeroType * hero, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(hero->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(hero->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroes->addItem(item); + }); + + //fill hero classes + VLC->heroClasses()->forEach([this](const HeroClass * heroClass, bool &) + { + auto * item = new QListWidgetItem(QString::fromStdString(heroClass->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(heroClass->getId().getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lHeroClasses->addItem(item); + }); + + //fill players + for(auto color = PlayerColor(0); color < PlayerColor::PLAYER_LIMIT; ++color) + { + auto * item = new QListWidgetItem(QString::fromStdString(GameConstants::PLAYER_COLOR_NAMES[color.getNum()])); + item->setData(Qt::UserRole, QVariant::fromValue(color.getNum())); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->lPlayers->addItem(item); + } + //fill spell cast for(auto & s : NSecondarySkill::levels) ui->castLevel->addItem(QString::fromStdString(s)); @@ -349,6 +380,27 @@ void RewardsWidget::saveCurrentVisitInfo(int index) if(widget->value()) vinfo.limiter.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } + + vinfo.limiter.heroes.clear(); + for(int i = 0; i < ui->lHeroes->count(); ++i) + { + if(ui->lHeroes->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.heroClasses.clear(); + for(int i = 0; i < ui->lHeroClasses->count(); ++i) + { + if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) + vinfo.limiter.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + } + + vinfo.limiter.players.clear(); + for(int i = 0; i < ui->lPlayers->count(); ++i) + { + if(ui->lPlayers->item(i)->checkState() == Qt::Checked) + vinfo.limiter.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + } } void RewardsWidget::loadCurrentVisitInfo(int index) @@ -472,6 +524,40 @@ void RewardsWidget::loadCurrentVisitInfo(int index) ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); } + + for(auto & i : vinfo.limiter.heroes) + { + for(int e = 0; e < ui->lHeroes->count(); ++e) + { + if(ui->lHeroes->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroes->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : vinfo.limiter.heroClasses) + { + for(int e = 0; e < ui->lHeroClasses->count(); ++e) + { + if(ui->lHeroClasses->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lHeroClasses->item(e)->setCheckState(Qt::Checked); + break; + } + } + } + for(auto & i : vinfo.limiter.players) + { + for(int e = 0; e < ui->lPlayers->count(); ++e) + { + if(ui->lPlayers->item(e)->data(Qt::UserRole).toInt() == i.getNum()) + { + ui->lPlayers->item(e)->setCheckState(Qt::Checked); + break; + } + } + } } void RewardsWidget::onCreatureAdd(QTableWidget * listWidget, QComboBox * comboWidget, QSpinBox * spinWidget) diff --git a/mapeditor/inspector/rewardswidget.ui b/mapeditor/inspector/rewardswidget.ui index c3681e258..2ae3c6676 100644 --- a/mapeditor/inspector/rewardswidget.ui +++ b/mapeditor/inspector/rewardswidget.ui @@ -487,6 +487,12 @@ 0 + + Qt::ElideNone + + + true + true @@ -843,7 +849,7 @@ false - 21 + 24 @@ -1185,6 +1191,12 @@ 0 + + Qt::ElideNone + + + true + true @@ -1229,7 +1241,7 @@ false - 21 + 24 @@ -1333,7 +1345,7 @@ false - 21 + 24 @@ -1429,7 +1441,7 @@ false - 21 + 24 @@ -1437,6 +1449,102 @@
    + + + Heroes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Hero classes + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + + + + + Players + + + + 3 + + + 3 + + + 3 + + + 3 + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::NoSelection + + + true + + + + +
    From bb238f9b72cf7e39451d0c239dd21a39aeb31b9b Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 9 Oct 2023 19:15:34 +0200 Subject: [PATCH 0781/1248] New quests work --- AI/Nullkiller/Goals/CompleteQuest.cpp | 33 +- .../Rules/AIMovementAfterDestinationRule.cpp | 2 +- AI/VCAI/Goals/CompleteQuest.cpp | 33 +- client/windows/CQuestLog.cpp | 16 +- lib/CGeneralTextHandler.cpp | 2 +- lib/mapObjects/CQuest.cpp | 567 ++++++------------ lib/mapObjects/CQuest.h | 40 +- lib/mapping/MapFormatH3M.cpp | 67 ++- lib/mapping/MapFormatH3M.h | 2 +- lib/rewardable/Limiter.cpp | 2 +- lib/rmg/modificators/TreasurePlacer.cpp | 12 +- mapeditor/inspector/inspector.cpp | 40 -- mapeditor/inspector/inspector.h | 1 - 13 files changed, 294 insertions(+), 523 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 00518d51b..96b9d317a 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -37,39 +37,32 @@ TGoalVec CompleteQuest::decompose() const } logAi->debug("Trying to realize quest: %s", questToString()); - - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: + + if(!q.quest->artifacts.empty()) return missionArt(); - case CQuest::MISSION_HERO: + if(!q.quest->heroes.empty()) return missionHero(); - case CQuest::MISSION_ARMY: + if(!q.quest->creatures.empty()) return missionArmy(); - case CQuest::MISSION_RESOURCES: + if(q.quest->resources.nonZero()) return missionResources(); - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + if(q.quest->killTarget >= 0) return missionDestroyObj(); - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); + for(auto & s : q.quest->primary) + if(s) + return missionIncreasePrimaryStat(); - case CQuest::MISSION_LEVEL: + if(q.quest->heroLevel > 0) return missionLevel(); - - case CQuest::MISSION_PLAYER: - break; - - case CQuest::MISSION_KEYMASTER: + + if(q.quest->questName == CQuest::missionName(10)) return missionKeymaster(); - } //end of switch - return TGoalVec(); } @@ -104,7 +97,7 @@ std::string CompleteQuest::questToString() const return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent"; } - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index dafcb4758..68638efc5 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,7 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE) + if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->questName == CQuest::missionName(0)) { return false; } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index fb851bf04..d915efd62 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -25,41 +25,34 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; - if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) + if(q.quest->progress != CQuest::COMPLETE) { logAi->debug("Trying to realize quest: %s", questToString()); - switch(q.quest->missionType) - { - case CQuest::MISSION_ART: + if(!q.quest->artifacts.empty()) return missionArt(); - case CQuest::MISSION_HERO: + if(!q.quest->heroes.empty()) return missionHero(); - case CQuest::MISSION_ARMY: + if(!q.quest->creatures.empty()) return missionArmy(); - case CQuest::MISSION_RESOURCES: + if(q.quest->resources.nonZero()) return missionResources(); - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + if(q.quest->killTarget >= 0) return missionDestroyObj(); - case CQuest::MISSION_PRIMARY_STAT: - return missionIncreasePrimaryStat(); + for(auto & s : q.quest->primary) + if(s) + return missionIncreasePrimaryStat(); - case CQuest::MISSION_LEVEL: + if(q.quest->heroLevel > 0) return missionLevel(); - - case CQuest::MISSION_PLAYER: - break; - case CQuest::MISSION_KEYMASTER: + if(q.quest->questName == CQuest::missionName(10)) return missionKeymaster(); - - } //end of switch } return TGoalVec(); @@ -67,7 +60,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() TSubgoal CompleteQuest::whatToDoToAchieve() { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) { throw cannotFulfillGoalException("Can not complete inactive quest"); } @@ -101,7 +94,7 @@ std::string CompleteQuest::completeMessage() const std::string CompleteQuest::questToString() const { - if(q.quest->missionType == CQuest::MISSION_NONE) + if(q.quest->questName == CQuest::missionName(0)) return "inactive quest"; MetaString ms; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index de1f9878e..208a74744 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -149,7 +149,7 @@ void CQuestLog::recreateLabelList() for (int i = 0; i < quests.size(); ++i) { // Quests with MISSION_NONE type don't have text for them and can't be displayed - if (quests[i].quest->missionType == CQuest::MISSION_NONE) + if (quests[i].quest->questName == CQuest::missionName(0)) continue; if (quests[i].quest->progress == CQuest::COMPLETE) @@ -236,7 +236,7 @@ void CQuestLog::selectQuest(int which, int labelId) MetaString text; std::vector components; - currentQuest->quest->getVisitText (text, components, currentQuest->quest->isCustomFirst, true); + currentQuest->quest->getVisitText(text, components, true); if(description->slider) description->slider->scrollToMin(); // scroll text to start position description->setText(text.toString()); //TODO: use special log entry text @@ -247,9 +247,15 @@ void CQuestLog::selectQuest(int which, int labelId) int descriptionHeight = DESCRIPTION_HEIGHT_MAX; if(componentsSize) { - descriptionHeight -= 15; CComponent::ESize imageSize = CComponent::large; - switch (currentQuest->quest->missionType) + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 155; + } + else + descriptionHeight -= 130; + /*switch (currentQuest->quest->missionType) { case CQuest::MISSION_ARMY: { @@ -285,7 +291,7 @@ void CQuestLog::selectQuest(int which, int labelId) default: descriptionHeight -= 115; break; - } + }*/ OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 09d046ef7..657fb564e 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -547,7 +547,7 @@ CGeneralTextHandler::CGeneralTextHandler(): for (size_t i = 0; i < 9; ++i) //9 types of quests { - std::string questName = CQuest::missionName(static_cast(1+i)); + std::string questName = CQuest::missionName(1+i); for (size_t j = 0; j < 5; ++j) { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 98f235e6b..d704c8850 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -37,7 +37,6 @@ std::map > CGKeys::playerKeyMap; //TODO: Remove constructor CQuest::CQuest(): qid(-1), - missionType(MISSION_NONE), progress(NOT_ACTIVE), lastDay(-1), killTarget(-1), @@ -47,7 +46,8 @@ CQuest::CQuest(): isCustomFirst(false), isCustomNext(false), isCustomComplete(false), - repeatedQuest(false) + repeatedQuest(false), + questName(CQuest::missionName(0)) { } @@ -57,7 +57,7 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -const std::string & CQuest::missionName(CQuest::Emission mission) +const std::string & CQuest::missionName(int mission) { static const std::array names = { "empty", @@ -164,277 +164,137 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const cb->giveResources(h->getOwner(), resources); } -void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool isCustom, bool firstVisit, const CGHeroInstance * h) const +void CQuest::addTextReplacements(MetaString & text) const +{ + if(heroLevel > 0) + text.replaceNumber(heroLevel); + + { //primary skills + MetaString loot; + for(int i = 0; i < 4; ++i) + { + if(primary[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(primary[i]); + loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); + } + } + if(!loot.empty()) + text.replaceRawString(loot.buildList()); + } + + if(killTarget >= 0 && !heroName.empty()) + { + //components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + addKillTargetReplacements(text); + } + + if(killTarget >= 0 && stackToKill.type) + { + //components.emplace_back(stackToKill); + addKillTargetReplacements(text); + } + + if(!heroes.empty()) + text.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); + + if(!artifacts.empty()) + { + MetaString loot; + for(const auto & elem : artifacts) + { + loot.appendRawString("%s"); + loot.replaceLocalString(EMetaText::ART_NAMES, elem); + } + text.replaceRawString(loot.buildList()); + } + + if(!creatures.empty()) + { + MetaString loot; + for(const auto & elem : creatures) + { + loot.appendRawString("%s"); + loot.replaceCreatureName(elem); + } + text.replaceRawString(loot.buildList()); + } + + if(resources.nonZero()) + { + MetaString loot; + for(int i = 0; i < 7; ++i) + { + if(resources[i]) + { + loot.appendRawString("%d %s"); + loot.replaceNumber(resources[i]); + loot.replaceLocalString(EMetaText::RES_NAMES, i); + } + } + text.replaceRawString(loot.buildList()); + } + + if(!players.empty()) + text.replaceLocalString(EMetaText::COLOR, players.front()); +} + +void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { - MetaString text; bool failRequirements = (h ? !checkQuest(h) : true); loadComponents(components, h); if(firstVisit) - { - isCustom = isCustomFirst; - text = firstVisitText; - iwText.appendRawString(text.toString()); - } + iwText.appendRawString(firstVisitText.toString()); else if(failRequirements) - { - isCustom = isCustomNext; - text = nextVisitText; - iwText.appendRawString(text.toString()); - } - switch (missionType) - { - case MISSION_LEVEL: - if(!isCustom) - iwText.replaceNumber(heroLevel); //TODO: heroLevel - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for(int i = 0; i < 4; ++i) - { - if(primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); - if(!isCustom) - addReplacements(iwText, text.toString()); - break; - case MISSION_HERO: - if(!isCustom && !heroes.empty()) - iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_KILL_CREATURE: - { - components.emplace_back(stackToKill); - if(!isCustom) - { - addReplacements(iwText, text.toString()); - } - } - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for(int i = 0; i < 7; ++i) - { - if(resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if(!isCustom) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_PLAYER: - if(!isCustom && !players.empty()) - iwText.replaceLocalString(EMetaText::COLOR, players.front()); - break; - } + iwText.appendRawString(nextVisitText.toString()); + + addTextReplacements(iwText); } void CQuest::getRolloverText(MetaString &ms, bool onHover) const { - // Quests with MISSION_NONE type don't have a text for them - assert(missionType != MISSION_NONE); - if(onHover) ms.appendRawString("\n\n"); - std::string questName = missionName(missionType); std::string questState = missionState(onHover ? 3 : 4); - ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState,textOption)); + ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); - switch(missionType) - { - case MISSION_LEVEL: - ms.replaceNumber(heroLevel); - break; - case MISSION_PRIMARY_STAT: - { - MetaString loot; - for (int i = 0; i < 4; ++i) - { - if (primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - ms.replaceRawString(heroName); - break; - case MISSION_KILL_CREATURE: - ms.replaceCreatureName(stackToKill); - break; - case MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - ms.replaceRawString(loot.buildList()); - } - break; - case MISSION_HERO: - ms.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_PLAYER: - ms.replaceRawString(VLC->generaltexth->colors[players.front()]); - break; - default: - break; - } + addTextReplacements(ms); } void CQuest::getCompletionText(MetaString &iwText) const { iwText.appendRawString(completedText.toString()); - switch(missionType) + + addTextReplacements(iwText); +} + +void CQuest::defineQuestName() +{ + //standard quests + questName = CQuest::missionName(0); + if(heroLevel > 0) questName = CQuest::missionName(1); + for(auto & s : primary) if(s) questName = CQuest::missionName(2); + if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); + if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); + if(!artifacts.empty()) questName = CQuest::missionName(5); + if(!creatures.empty()) questName = CQuest::missionName(6); + if(resources.nonZero()) questName = CQuest::missionName(7); + if(!heroes.empty()) questName = CQuest::missionName(8); + if(!players.empty()) questName = CQuest::missionName(9); +} + +void CQuest::addKillTargetReplacements(MetaString &out) const +{ + if(!heroName.empty()) + out.replaceTextID(heroName); + if(stackToKill.type) { - case CQuest::MISSION_LEVEL: - if (!isCustomComplete) - iwText.replaceNumber(heroLevel); - break; - case CQuest::MISSION_PRIMARY_STAT: - { - MetaString loot; - assert(primary.size() <= 4); - for (int i = 0; i < primary.size(); ++i) - { - if (primary[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); - loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - break; - } - case CQuest::MISSION_ART: - { - MetaString loot; - for(const auto & elem : artifacts) - { - loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_ARMY: - { - MetaString loot; - for(const auto & elem : creatures) - { - loot.appendRawString("%s"); - loot.replaceCreatureName(elem); - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case CQuest::MISSION_RESOURCES: - { - MetaString loot; - for (int i = 0; i < 7; ++i) - { - if (resources[i]) - { - loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); - } - } - if (!isCustomComplete) - iwText.replaceRawString(loot.buildList()); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - if (!isCustomComplete) - addReplacements(iwText, completedText.toString()); - break; - case MISSION_HERO: - if (!isCustomComplete && !heroes.empty()) - iwText.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); - break; - case MISSION_PLAYER: - if (!isCustomComplete && !players.empty()) - iwText.replaceRawString(VLC->generaltexth->colors[players.front()]); - break; + out.replaceCreatureName(stackToKill); + out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); } } @@ -456,85 +316,87 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi Rewardable::Limiter::serializeJson(handler); - static const std::vector MISSION_TYPE_JSON = - { - "None", "Level", "PrimaryStat", "KillHero", "KillCreature", "Artifact", "Army", "Resources", "Hero", "Player" - }; - - handler.serializeEnum("missionType", missionType, Emission::MISSION_NONE, MISSION_TYPE_JSON); handler.serializeInt("timeLimit", lastDay, -1); handler.serializeInstance("killTarget", killTarget, -1); - if(!handler.saving) + if(!handler.saving) //compatibility with legacy vmaps { - switch (missionType) + std::string missionType = "None"; + handler.serializeString("missionType", missionType); + if(missionType == "None") + return; + + if(missionType == "Level") + handler.serializeInt("heroLevel", heroLevel, -1); + + if(missionType == "PrimaryStat") { - case MISSION_NONE: - break; - case MISSION_LEVEL: - handler.serializeInt("heroLevel", heroLevel, -1); - break; - case MISSION_PRIMARY_STAT: - { - auto primarySkills = handler.enterStruct("primarySkills"); - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); - } - break; - case MISSION_KILL_HERO: - case MISSION_KILL_CREATURE: - break; - case MISSION_ART: - handler.serializeIdArray("artifacts", artifacts); - break; - case MISSION_ARMY: - { - auto a = handler.enterArray("creatures"); - a.serializeStruct(creatures); - } - break; - case MISSION_RESOURCES: - { - auto r = handler.enterStruct("resources"); + auto primarySkills = handler.enterStruct("primarySkills"); + for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + } + + if(missionType == "Artifact") + handler.serializeIdArray("artifacts", artifacts); + + if(missionType == "Army") + { + auto a = handler.enterArray("creatures"); + a.serializeStruct(creatures); + } + + if(missionType == "Resources") + { + auto r = handler.enterStruct("resources"); - for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) - { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); - } - } - break; - case MISSION_HERO: + for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) { - ui32 temp; - handler.serializeId("hero", temp, 0); - heroes.emplace_back(temp); + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); } - break; - case MISSION_PLAYER: - { - ui32 temp; - handler.serializeId("player", temp, PlayerColor::NEUTRAL); - players.emplace_back(temp); - } - break; - default: - logGlobal->error("Invalid quest mission type"); - break; + } + + if(missionType == "Hero") + { + ui32 temp; + handler.serializeId("hero", temp, 0); + heroes.emplace_back(temp); + } + + if(missionType == "Player") + { + ui32 temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); + players.emplace_back(temp); } } } +bool IQuestObject::checkQuest(const CGHeroInstance* h) const +{ + return quest->checkQuest(h); +} + +void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const +{ + quest->getVisitText(text,components, FirstVisit, h); +} + +void IQuestObject::afterAddToMapCommon(CMap * map) const +{ + map->addNewQuestInstance(quest); +} + void CGSeerHut::setObjToKill() { - if(quest->missionType == CQuest::MISSION_KILL_CREATURE) + if(getCreatureToKill(true)) { quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? assert(quest->stackToKill.type); quest->stackToKill.count = 0; //no count in info window quest->stackDirection = checkDirection(); } - else if(quest->missionType == CQuest::MISSION_KILL_HERO) + else if(getHeroToKill(true)) { quest->heroName = getHeroToKill(false)->getNameTranslated(); quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); @@ -566,27 +428,28 @@ void CGSeerHut::initObj(CRandomGenerator & rand) CRewardableObject::initObj(rand); quest->progress = CQuest::NOT_ACTIVE; - if(quest->missionType) - { - std::string questName = quest->missionName(quest->missionType); - - if(!quest->isCustomFirst) - quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(0), quest->textOption).get()); - if(!quest->isCustomNext) - quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(1), quest->textOption).get()); - if(!quest->isCustomComplete) - quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, quest->missionState(2), quest->textOption).get()); - } - else + + quest->defineQuestName(); + + if(quest->questName == quest->missionName(0)) { quest->progress = CQuest::COMPLETE; quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } + else + { + if(!quest->isCustomFirst) + quest->firstVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(0), quest->textOption).get()); + if(!quest->isCustomNext) + quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(1), quest->textOption).get()); + if(!quest->isCustomComplete) + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(2), quest->textOption).get()); + } } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const { - quest->getRolloverText (text, onHover);//TODO: simplify? + quest->getRolloverText(text, onHover);//TODO: simplify? if(!onHover) text.replaceRawString(seerName); } @@ -600,7 +463,7 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const boost::algorithm::replace_first(hoverName, "%s", seerName); } - if(quest->progress & quest->missionType) //rollover when the quest is active + if(quest->progress/* & quest->missionType*/) //rollover when the quest is active { MetaString ms; getRolloverText (ms, true); @@ -609,47 +472,12 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CQuest::addReplacements(MetaString &out, const std::string &base) const -{ - switch(missionType) - { - case MISSION_KILL_CREATURE: - if(stackToKill.type) - { - out.replaceCreatureName(stackToKill); - if (std::count(base.begin(), base.end(), '%') == 2) //say where is placed monster - { - out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); - } - } - break; - case MISSION_KILL_HERO: - out.replaceTextID(heroName); - break; - } -} - -bool IQuestObject::checkQuest(const CGHeroInstance* h) const -{ - return quest->checkQuest(h); -} - -void IQuestObject::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const -{ - quest->getVisitText (text,components, isCustom, FirstVisit, h); -} - -void IQuestObject::afterAddToMapCommon(CMap * map) const -{ - map->addNewQuestInstance(quest); -} - void CGSeerHut::setPropertyDer (ui8 what, ui32 val) { switch(what) { case 10: - quest->progress = static_cast(val); + quest->progress = static_cast(val); break; } } @@ -671,11 +499,9 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const { bool firstVisit = !quest->progress; bool failRequirements = !checkQuest(h); - bool isCustom = false; if(firstVisit) { - isCustom = quest->isCustomFirst; cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::IN_PROGRESS); AddQuest aq; @@ -683,14 +509,10 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const aq.player = h->tempOwner; cb->sendAndApply(&aq); //TODO: merge with setObjProperty? } - else if(failRequirements) - { - isCustom = quest->isCustomNext; - } if(firstVisit || failRequirements) { - getVisitText (iw.text, iw.components, isCustom, firstVisit, h); + getVisitText (iw.text, iw.components, firstVisit, h); cb->showInfoDialog(&iw); } @@ -765,7 +587,10 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + if(quest && quest->repeatedQuest) + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::NOT_ACTIVE); + else + cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete } } @@ -919,12 +744,12 @@ void CGBorderGuard::initObj(CRandomGenerator & rand) blockVisit = true; } -void CGBorderGuard::getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h) const +void CGBorderGuard::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - text.appendLocalString(EMetaText::ADVOB_TXT,18); + text.appendLocalString(EMetaText::ADVOB_TXT, 18); } -void CGBorderGuard::getRolloverText (MetaString &text, bool onHover) const +void CGBorderGuard::getRolloverText(MetaString &text, bool onHover) const { if (!onHover) { diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 6f3976c07..b524c02fa 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -20,37 +20,21 @@ class CGCreature; class DLL_LINKAGE CQuest: public Rewardable::Limiter { public: - enum Emission { - MISSION_NONE = 0, - MISSION_LEVEL = 1, - MISSION_PRIMARY_STAT = 2, - MISSION_KILL_HERO = 3, - MISSION_KILL_CREATURE = 4, - MISSION_ART = 5, - MISSION_ARMY = 6, - MISSION_RESOURCES = 7, - MISSION_HERO = 8, - MISSION_PLAYER = 9, - MISSION_HOTA_MULTI = 10, - // end of H3 missions - MISSION_KEYMASTER = 100, - MISSION_HOTA_HERO_CLASS = 101, - MISSION_HOTA_REACH_DATE = 102 - }; - enum Eprogress { + enum EProgress { NOT_ACTIVE, IN_PROGRESS, COMPLETE }; - static const std::string & missionName(Emission mission); - static const std::string & missionState(int index); + static const std::string & missionName(int index); + static const std::string & missionState(int index); + + std::string questName; si32 qid; //unique quest id for serialization / identification - Emission missionType; - Eprogress progress; + EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit int killTarget; bool repeatedQuest; @@ -74,11 +58,13 @@ public: static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); virtual bool checkQuest (const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addReplacements(MetaString &out, const std::string &base) const; + virtual void addTextReplacements(MetaString &out) const; + virtual void addKillTargetReplacements(MetaString &out) const; + void defineQuestName(); bool operator== (const CQuest & quest) const { @@ -88,7 +74,6 @@ public: template void serialize(Handler &h, const int version) { h & qid; - h & missionType; h & progress; h & lastDay; h & textOption; @@ -103,6 +88,7 @@ public: h & isCustomNext; h & isCustomComplete; h & completedOption; + h & questName; h & static_cast(*this); } @@ -117,7 +103,7 @@ public: ///Information about quest should remain accessible even if IQuestObject removed from map ///All CQuest objects are freed in CMap destructor virtual ~IQuestObject() = default; - virtual void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual bool checkQuest (const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) @@ -215,7 +201,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - void getVisitText (MetaString &text, std::vector &components, bool isCustom, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; + void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const override; void getRolloverText (MetaString &text, bool onHover) const; bool checkQuest (const CGHeroInstance * h) const override; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 08b3e1545..97a2d6820 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1858,11 +1858,30 @@ enum class ESeerHutRewardType : uint8_t CREATURE = 10, }; +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) { + EQuestMission missionType = EQuestMission::NONE; if(features.levelAB) { - readQuest(hut, position); + missionType = static_cast(readQuest(hut, position)); } else { @@ -1872,11 +1891,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { //not none quest hut->quest->artifacts.push_back(artID); - hut->quest->missionType = CQuest::MISSION_ART; - } - else - { - hut->quest->missionType = CQuest::MISSION_NONE; + missionType = EQuestMission::ARTIFACT; } hut->quest->lastDay = -1; //no timeout hut->quest->isCustomFirst = false; @@ -1884,7 +1899,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con hut->quest->isCustomComplete = false; } - if(hut->quest->missionType) + if(missionType != EQuestMission::NONE) { auto rewardType = static_cast(reader->readUInt8()); Rewardable::VisitInfo vinfo; @@ -1976,15 +1991,15 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } } -void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { - guard->quest->missionType = static_cast(reader->readUInt8()); + auto missionId = reader->readUInt8(); - switch(guard->quest->missionType) + switch(static_cast(missionId)) { - case CQuest::MISSION_NONE: - return; - case CQuest::MISSION_PRIMARY_STAT: + case EQuestMission::NONE: + return missionId; + case EQuestMission::PRIMARY_SKILL: { for(int x = 0; x < 4; ++x) { @@ -1992,14 +2007,17 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } } break; - case CQuest::MISSION_LEVEL: - case CQuest::MISSION_KILL_HERO: - case CQuest::MISSION_KILL_CREATURE: + case EQuestMission::LEVEL: + { + guard->quest->heroLevel = reader->readUInt32(); + } + case EQuestMission::KILL_HERO: + case EQuestMission::KILL_CREATURE: { guard->quest->killTarget = reader->readUInt32(); break; } - case CQuest::MISSION_ART: + case EQuestMission::ARTIFACT: { int artNumber = reader->readUInt8(); for(int yy = 0; yy < artNumber; ++yy) @@ -2010,7 +2028,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } break; } - case CQuest::MISSION_ARMY: + case EQuestMission::ARMY: { int typeNumber = reader->readUInt8(); guard->quest->creatures.resize(typeNumber); @@ -2021,30 +2039,30 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } break; } - case CQuest::MISSION_RESOURCES: + case EQuestMission::RESOURCES: { for(int x = 0; x < 7; ++x) guard->quest->resources[x] = reader->readUInt32(); break; } - case CQuest::MISSION_HERO: + case EQuestMission::HERO: { guard->quest->heroes.push_back(reader->readHero()); break; } - case CQuest::MISSION_PLAYER: + case EQuestMission::PLAYER: { guard->quest->players.push_back(reader->readPlayer()); break; } - case CQuest::MISSION_HOTA_MULTI: + case EQuestMission::HOTA_MULTI: { uint32_t missionSubID = reader->readUInt32(); if(missionSubID == 0) { - guard->quest->missionType = CQuest::MISSION_HOTA_HERO_CLASS; + missionId = int(EQuestMission::HOTA_HERO_CLASS); std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) @@ -2053,7 +2071,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) } if(missionSubID == 1) { - guard->quest->missionType = CQuest::MISSION_HOTA_REACH_DATE; + missionId = int(EQuestMission::HOTA_REACH_DATE); guard->quest->daysPassed = reader->readUInt32(); break; } @@ -2072,6 +2090,7 @@ void CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); guard->quest->isCustomComplete = !guard->quest->completedText.empty(); + return missionId; } CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 6a0180b54..3eb7a3226 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -204,7 +204,7 @@ private: * * @param guard the quest guard where that quest should be applied to */ - void readQuest(IQuestObject * guard, const int3 & position); + int readQuest(IQuestObject * guard, const int3 & position); void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index e32eeb778..6c82eeb2e 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -26,7 +26,7 @@ Rewardable::Limiter::Limiter() : dayOfWeek(0) , daysPassed(0) , heroExperience(0) - , heroLevel(0) + , heroLevel(-1) , manaPercentage(0) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index f3d44ad8f..542c8294e 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -460,13 +460,9 @@ void TreasurePlacer::addAllPossibleObjects() reward.reward.creatures.emplace_back(creature->getId(), creaturesAmount); reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - - obj->quest->missionType = CQuest::MISSION_ART; - + ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -513,11 +509,8 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -538,11 +531,8 @@ void TreasurePlacer::addAllPossibleObjects() reward.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; obj->configuration.info.push_back(reward); - obj->quest->missionType = CQuest::MISSION_ART; ArtifactID artid = qap->drawRandomArtifact(); obj->quest->artifacts.push_back(artid); - obj->quest->lastDay = -1; - obj->quest->isCustomFirst = obj->quest->isCustomNext = obj->quest->isCustomComplete = false; generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index c19eda842..903cfaf22 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -29,20 +29,6 @@ #include "PickObjectDelegate.h" #include "../mapcontroller.h" -static QList> MissionIdentifiers -{ - {QObject::tr("None"), QVariant::fromValue(int(CQuest::Emission::MISSION_NONE))}, - {QObject::tr("Reach level"), QVariant::fromValue(int(CQuest::Emission::MISSION_LEVEL))}, - {QObject::tr("Stats"), QVariant::fromValue(int(CQuest::Emission::MISSION_PRIMARY_STAT))}, - {QObject::tr("Kill hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_HERO))}, - {QObject::tr("Kill monster"), QVariant::fromValue(int(CQuest::Emission::MISSION_KILL_CREATURE))}, - {QObject::tr("Artifact"), QVariant::fromValue(int(CQuest::Emission::MISSION_ART))}, - {QObject::tr("Army"), QVariant::fromValue(int(CQuest::Emission::MISSION_ARMY))}, - {QObject::tr("Resources"), QVariant::fromValue(int(CQuest::Emission::MISSION_RESOURCES))}, - {QObject::tr("Hero"), QVariant::fromValue(int(CQuest::Emission::MISSION_HERO))}, - {QObject::tr("Player"), QVariant::fromValue(int(CQuest::Emission::MISSION_PLAYER))}, -}; - static QList> CharacterIdentifiers { {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, @@ -411,12 +397,6 @@ void Inspector::updateProperties(CGEvent * o) void Inspector::updateProperties(CGSeerHut * o) { if(!o || !o->quest) return; - - { //Mission type - auto * delegate = new InspectorDelegate; - delegate->options = MissionIdentifiers; - addProperty("Mission type", o->quest->missionType, delegate, false); - } addProperty("First visit text", o->quest->firstVisitText, new MessageDelegate, false); addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); @@ -678,8 +658,6 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & { if(!o) return; - if(key == "Mission type") - o->quest->missionType = CQuest::Emission(value.toInt()); if(key == "First visit text") o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") @@ -798,24 +776,6 @@ QTableWidgetItem * Inspector::addProperty(CGCreature::Character value) return item; } -QTableWidgetItem * Inspector::addProperty(CQuest::Emission value) -{ - auto * item = new QTableWidgetItem; - item->setFlags(Qt::NoItemFlags); - item->setData(Qt::UserRole, QVariant::fromValue(int(value))); - - for(auto & i : MissionIdentifiers) - { - if(i.second.toInt() == value) - { - item->setText(i.first); - break; - } - } - - return item; -} - //======================================================================== Inspector::Inspector(MapController & c, CGObjectInstance * o, QTableWidget * t): obj(o), table(t), controller(c) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 69633a3db..5e2e1f96a 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -96,7 +96,6 @@ protected: QTableWidgetItem * addProperty(bool value); QTableWidgetItem * addProperty(CGObjectInstance * value); QTableWidgetItem * addProperty(CGCreature::Character value); - QTableWidgetItem * addProperty(CQuest::Emission value); QTableWidgetItem * addProperty(PropertyEditorPlaceholder value); //===============END OF DECLARATION======================================= From d2d64dbddda7c2f92d088932cf1abde49d0ae17d Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 01:20:55 +0200 Subject: [PATCH 0782/1248] Bugfixes --- lib/mapObjects/CQuest.cpp | 24 +++++++++-------- lib/mapObjects/CQuest.h | 6 ++--- lib/rewardable/Limiter.cpp | 39 ++++++++++++++++++++++++--- lib/rewardable/Limiter.h | 2 +- mapeditor/inspector/inspector.cpp | 32 +++++++++++++++------- mapeditor/inspector/questwidget.cpp | 4 ++- mapeditor/inspector/rewardswidget.cpp | 6 ++++- 7 files changed, 83 insertions(+), 30 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index d704c8850..1ab0e85cf 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -127,7 +127,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const if(killTarget >= 0) { - if(!CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) + if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; } @@ -164,7 +164,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const cb->giveResources(h->getOwner(), resources); } -void CQuest::addTextReplacements(MetaString & text) const +void CQuest::addTextReplacements(MetaString & text, std::vector & components) const { if(heroLevel > 0) text.replaceNumber(heroLevel); @@ -186,13 +186,13 @@ void CQuest::addTextReplacements(MetaString & text) const if(killTarget >= 0 && !heroName.empty()) { - //components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); addKillTargetReplacements(text); } if(killTarget >= 0 && stackToKill.type) { - //components.emplace_back(stackToKill); + components.emplace_back(stackToKill); addKillTargetReplacements(text); } @@ -250,7 +250,7 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components else if(failRequirements) iwText.appendRawString(nextVisitText.toString()); - addTextReplacements(iwText); + addTextReplacements(iwText, components); } void CQuest::getRolloverText(MetaString &ms, bool onHover) const @@ -262,14 +262,16 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); - addTextReplacements(ms); + std::vector components; + addTextReplacements(ms, components); } void CQuest::getCompletionText(MetaString &iwText) const { iwText.appendRawString(completedText.toString()); - addTextReplacements(iwText); + std::vector components; + addTextReplacements(iwText, components); } void CQuest::defineQuestName() @@ -379,7 +381,7 @@ bool IQuestObject::checkQuest(const CGHeroInstance* h) const void IQuestObject::getVisitText(MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h) const { - quest->getVisitText(text,components, FirstVisit, h); + quest->getVisitText(text, components, FirstVisit, h); } void IQuestObject::afterAddToMapCommon(CMap * map) const @@ -431,9 +433,11 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->defineQuestName(); + if(quest->empty() && quest->killTarget == -1) + quest->progress = CQuest::COMPLETE; + if(quest->questName == quest->missionName(0)) { - quest->progress = CQuest::COMPLETE; quest->firstVisitText.appendTextID(TextIdentifier("core", "seehut", "empty", quest->completedOption).get()); } else @@ -568,7 +572,6 @@ const CGHeroInstance * CGSeerHut::getHeroToKill(bool allowNull) const const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && (o->ID == Obj::HERO || o->ID == Obj::PRISON)); return dynamic_cast(o); } @@ -577,7 +580,6 @@ const CGCreature * CGSeerHut::getCreatureToKill(bool allowNull) const const CGObjectInstance *o = cb->getObjByQuestIdentifier(quest->killTarget); if(allowNull && !o) return nullptr; - assert(o && o->ID == Obj::MONSTER); return dynamic_cast(o); } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index b524c02fa..6a6383fe3 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -57,12 +57,12 @@ public: CQuest(); //TODO: Remove constructor static bool checkMissionArmy(const CQuest * q, const CCreatureSet * army); - virtual bool checkQuest (const CGHeroInstance * h) const; //determines whether the quest is complete or not - virtual void getVisitText (MetaString &text, std::vector &components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; + virtual bool checkQuest(const CGHeroInstance * h) const; //determines whether the quest is complete or not + virtual void getVisitText(MetaString &text, std::vector & components, bool FirstVisit, const CGHeroInstance * h = nullptr) const; virtual void getCompletionText(MetaString &text) const; virtual void getRolloverText (MetaString &text, bool onHover) const; //hover or quest log entry virtual void completeQuest(IGameCallback *, const CGHeroInstance * h) const; - virtual void addTextReplacements(MetaString &out) const; + virtual void addTextReplacements(MetaString &out, std::vector & components) const; virtual void addKillTargetReplacements(MetaString &out) const; void defineQuestName(); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 6c82eeb2e..8843d4e39 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -35,6 +35,37 @@ Rewardable::Limiter::Limiter() Rewardable::Limiter::~Limiter() = default; +bool Rewardable::Limiter::empty() const +{ + if(dayOfWeek != 0 + || daysPassed != 0 + || heroLevel > 0 + || heroExperience > 0 + || manaPoints > 0 + || manaPercentage > 0 + || !secondary.empty() + || !creatures.empty() + || !spells.empty() + || !artifacts.empty() + || !players.empty() + || !heroes.empty() + || !heroClasses.empty() + || resources.nonZero() + || std::find_if(primary.begin(), primary.end(), [](si32 i){return i != 0;}) != primary.end()) + return false; + + for(const auto & sub : {noneOf, allOf, anyOf}) + { + for(const auto & sublimiter : sub) + { + if(!sublimiter->empty()) + return false; + } + } + + return true; +} + bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) @@ -150,14 +181,16 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, const CGHeroInstance * h) const { if (heroExperience) - { comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - } + if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaPoints || manaPercentage > 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, 0, 0); + { + int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); + } for (size_t i=0; i>; /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures) -/// NOTE: in future should (partially) replace seer hut/quest guard quests checks struct DLL_LINKAGE Limiter { /// day of week, unused if 0, 1-7 will test for current day of week @@ -82,6 +81,7 @@ struct DLL_LINKAGE Limiter virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; + bool empty() const; /// Generates list of components that describes reward for a specific hero virtual void loadComponents(std::vector & comps, diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 903cfaf22..8b636a9dd 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -519,7 +519,8 @@ void Inspector::setProperty(CGPandoraBox * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGEvent * o, const QString & key, const QVariant & value) @@ -541,7 +542,8 @@ void Inspector::setProperty(CGTownInstance * o, const QString & key, const QVari if(!o) return; if(key == "Town name") - o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); + o->setNameTextId(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("town", o->instanceName, "name"), value.toString().toStdString())); } void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVariant & value) @@ -549,7 +551,8 @@ void Inspector::setProperty(CGSignBottle * o, const QString & key, const QVarian if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("sign", o->instanceName, "message"), value.toString().toStdString())); } void Inspector::setProperty(CGMine * o, const QString & key, const QVariant & value) @@ -565,7 +568,8 @@ void Inspector::setProperty(CGArtifact * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("guards", o->instanceName, "message"), value.toString().toStdString())); if(o->storedArtifact && key == "Spell") { @@ -604,10 +608,12 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari o->gender = EHeroGender(value.toInt()); if(key == "Name") - o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); + o->nameCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "name"), value.toString().toStdString()); if(key == "Biography") - o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); + o->biographyCustomTextId = mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("hero", o->instanceName, "biography"), value.toString().toStdString()); if(key == "Experience") o->exp = value.toString().toInt(); @@ -643,7 +649,8 @@ void Inspector::setProperty(CGCreature * o, const QString & key, const QVariant if(!o) return; if(key == "Message") - o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); + o->message = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("monster", o->instanceName, "message"), value.toString().toStdString())); if(key == "Character") o->character = CGCreature::Character(value.toInt()); if(key == "Never flees") @@ -659,11 +666,16 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & if(!o) return; if(key == "First visit text") - o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); + o->quest->firstVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "firstVisit"), value.toString().toStdString())); if(key == "Next visit text") - o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); + o->quest->nextVisitText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "nextVisit"), value.toString().toStdString())); if(key == "Completed text") - o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + o->quest->completedText = MetaString::createFromTextID(mapRegisterLocalizedString("map", *controller.map(), + TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); + if(key == "Repeat quest") + o->quest->repeatedQuest = value.toBool(); } diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index 0cf1a1315..bf611834e 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -42,7 +42,9 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); item->setData(Qt::UserRole, QVariant::fromValue(i)); ui->lResources->setItem(i, 0, item); - ui->lResources->setCellWidget(i, 1, new QSpinBox); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + ui->lResources->setCellWidget(i, 1, spinBox); } //fill artifacts diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index a509f4a24..92f773f01 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -56,7 +56,11 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : auto * item = new QTableWidgetItem(QString::fromStdString(GameConstants::RESOURCE_NAMES[i])); item->setData(Qt::UserRole, QVariant::fromValue(i)); w->setItem(i, 0, item); - w->setCellWidget(i, 1, new QSpinBox); + auto * spinBox = new QSpinBox; + spinBox->setMaximum(i == GameResID::GOLD ? 999999 : 999); + if(w == ui->rResources) + spinBox->setMinimum(i == GameResID::GOLD ? -999999 : -999); + w->setCellWidget(i, 1, spinBox); } } From 5eeda3cd25c0bacd24e645160087fe6ece965e58 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 01:53:25 +0200 Subject: [PATCH 0783/1248] Quests mostly work --- .../Pathfinding/Rules/AIMovementAfterDestinationRule.cpp | 2 +- lib/mapObjects/CQuest.cpp | 3 ++- mapeditor/inspector/inspector.cpp | 3 +++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 68638efc5..89c71820b 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,7 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->questName == CQuest::missionName(0)) + if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->empty() && questObj->quest->killTarget == -1) { return false; } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 1ab0e85cf..0469ca5be 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -431,6 +431,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) quest->progress = CQuest::NOT_ACTIVE; + setObjToKill(); quest->defineQuestName(); if(quest->empty() && quest->killTarget == -1) @@ -447,7 +448,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomNext) quest->nextVisitText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(1), quest->textOption).get()); if(!quest->isCustomComplete) - quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest->questName, quest->missionState(2), quest->textOption).get()); + quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 8b636a9dd..4a4590974 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -402,6 +402,7 @@ void Inspector::updateProperties(CGSeerHut * o) addProperty("Next visit text", o->quest->nextVisitText, new MessageDelegate, false); addProperty("Completed text", o->quest->completedText, new MessageDelegate, false); addProperty("Repeat quest", o->quest->repeatedQuest, false); + addProperty("Time limit", o->quest->lastDay, false); { //Quest auto * delegate = new QuestDelegate(controller, *o->quest); @@ -676,6 +677,8 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & TextIdentifier("quest", o->instanceName, "completed"), value.toString().toStdString())); if(key == "Repeat quest") o->quest->repeatedQuest = value.toBool(); + if(key == "Time limit") + o->quest->lastDay = value.toString().toInt(); } From 31d71ddd25f99db39e7e4eb1eea481c4176c9469 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 02:33:17 +0200 Subject: [PATCH 0784/1248] Minor fixes in text --- lib/mapObjects/CQuest.cpp | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 0469ca5be..5ddaef105 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -28,6 +28,7 @@ #include "../mapping/CMap.h" #include "../modding/ModScope.h" #include "../modding/ModUtility.h" +#include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -59,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -169,6 +170,9 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(heroLevel > 0) text.replaceNumber(heroLevel); + if(heroExperience > 0) + text.replaceNumber(heroExperience); + { //primary skills MetaString loot; for(int i = 0; i < 4; ++i) @@ -180,6 +184,17 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } + + for(auto & skill : secondary) + { + loot.appendTextID(VLC->skillh->getById(skill.first)->getNameTextID()); + } + + for(auto & spell : spells) + { + loot.appendTextID(VLC->spellh->getById(spell)->getNameTextID()); + } + if(!loot.empty()) text.replaceRawString(loot.buildList()); } @@ -237,7 +252,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com } if(!players.empty()) - text.replaceLocalString(EMetaText::COLOR, players.front()); + { + MetaString loot; + for(auto & p : players) + loot.appendLocalString(EMetaText::COLOR, p); + + text.replaceRawString(loot.buildList()); + } } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const @@ -280,6 +301,8 @@ void CQuest::defineQuestName() questName = CQuest::missionName(0); if(heroLevel > 0) questName = CQuest::missionName(1); for(auto & s : primary) if(s) questName = CQuest::missionName(2); + if(!spells.empty()) questName = CQuest::missionName(2); + if(!secondary.empty()) questName = CQuest::missionName(2); if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); if(!artifacts.empty()) questName = CQuest::missionName(5); From 62c4e8a98d344a988a34801d3f2804ea5e7072e9 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 10 Oct 2023 02:39:10 +0200 Subject: [PATCH 0785/1248] Fix compiling --- lib/mapping/MapFormatH3M.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 97a2d6820..9fc78927d 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2005,11 +2005,12 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { guard->quest->primary[x] = reader->readUInt8(); } + break; } - break; case EQuestMission::LEVEL: { guard->quest->heroLevel = reader->readUInt32(); + break; } case EQuestMission::KILL_HERO: case EQuestMission::KILL_CREATURE: From f9db3d131ffcb4ce7d90f34247949c9ebe1935fb Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 10 Oct 2023 23:51:05 +0200 Subject: [PATCH 0786/1248] lastActivated with overload --- client/adventureMap/CInfoBar.cpp | 7 +++++-- client/adventureMap/CInfoBar.h | 2 +- client/gui/EventDispatcher.cpp | 9 ++++++++- client/gui/EventsReceiver.h | 2 ++ 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 7b0f7628f..360dd4946 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -289,13 +289,16 @@ void CInfoBar::tick(uint32_t msPassed) } } -void CInfoBar::clickReleased(const Point & cursorPosition) +void CInfoBar::clickReleased(const Point & cursorPosition, bool lastActivated) { timerCounter = 0; removeUsedEvents(TIME); //expiration trigger from just clicked element is not valid anymore if(state == HERO || state == TOWN) - showGameStatus(); + { + if(lastActivated) + showGameStatus(); + } else if(state == GAME) showDate(); else diff --git a/client/adventureMap/CInfoBar.h b/client/adventureMap/CInfoBar.h index 8dc8c88cb..5800377b7 100644 --- a/client/adventureMap/CInfoBar.h +++ b/client/adventureMap/CInfoBar.h @@ -159,7 +159,7 @@ private: void tick(uint32_t msPassed) override; - void clickReleased(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition, bool lastActivated) override; void showPopupWindow(const Point & cursorPosition) override; void hover(bool on) override; diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 08e4c6b44..721954848 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -203,6 +203,7 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc // POSSIBLE SOLUTION: make EventReceivers inherit from create_shared_from this and store weak_ptr's in lists AEventsReceiver * nearestElement = findElementInToleranceRange(lclickable, position, AEventsReceiver::LCLICK, tolerance); auto hlp = lclickable; + bool lastActivated = true; for(auto & i : hlp) { @@ -212,13 +213,19 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement) { if(isPressed) + { i->clickPressed(position); + i->clickPressed(position, lastActivated); + } if (i->mouseClickedState && !isPressed) + { i->clickReleased(position); + i->clickReleased(position, lastActivated); + } i->mouseClickedState = isPressed; - return; + lastActivated = false; } else { diff --git a/client/gui/EventsReceiver.h b/client/gui/EventsReceiver.h index 2206c70bf..690c9b9a8 100644 --- a/client/gui/EventsReceiver.h +++ b/client/gui/EventsReceiver.h @@ -45,6 +45,8 @@ protected: public: virtual void clickPressed(const Point & cursorPosition) {} virtual void clickReleased(const Point & cursorPosition) {} + virtual void clickPressed(const Point & cursorPosition, bool lastActivated) {} + virtual void clickReleased(const Point & cursorPosition, bool lastActivated) {} virtual void clickCancel(const Point & cursorPosition) {} virtual void showPopupWindow(const Point & cursorPosition) {} virtual void clickDouble(const Point & cursorPosition) {} From 5b10b457cff0b2952d17743ce38c3917fc916320 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 00:47:19 +0200 Subject: [PATCH 0787/1248] Fix code review suggestions --- AI/Nullkiller/Goals/CompleteQuest.cpp | 16 +-- .../Rules/AIMovementAfterDestinationRule.cpp | 4 +- AI/VCAI/Goals/CompleteQuest.cpp | 28 ++--- docs/modders/Map_Objects/Rewardable.md | 1 - lib/CCreatureSet.cpp | 8 ++ lib/CCreatureSet.h | 2 + lib/mapObjects/CQuest.cpp | 103 +++++++++--------- lib/mapObjects/CQuest.h | 8 +- lib/mapping/MapFormatH3M.cpp | 26 ++--- lib/rewardable/Info.cpp | 2 +- lib/rewardable/Limiter.cpp | 57 ++++------ lib/rewardable/Limiter.h | 11 +- lib/rewardable/Reward.h | 4 +- lib/rmg/modificators/TreasurePlacer.cpp | 6 +- mapeditor/inspector/questwidget.cpp | 92 ++++++++-------- 15 files changed, 186 insertions(+), 182 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 96b9d317a..20ef080f2 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -38,26 +38,26 @@ TGoalVec CompleteQuest::decompose() const logAi->debug("Trying to realize quest: %s", questToString()); - if(!q.quest->artifacts.empty()) + if(!q.quest->mission.artifacts.empty()) return missionArt(); - if(!q.quest->heroes.empty()) + if(!q.quest->mission.heroes.empty()) return missionHero(); - if(!q.quest->creatures.empty()) + if(!q.quest->mission.creatures.empty()) return missionArmy(); - if(q.quest->resources.nonZero()) + if(q.quest->mission.resources.nonZero()) return missionResources(); - if(q.quest->killTarget >= 0) + if(q.quest->killTarget != ObjectInstanceID::NONE) return missionDestroyObj(); - for(auto & s : q.quest->primary) + for(auto & s : q.quest->mission.primary) if(s) return missionIncreasePrimaryStat(); - if(q.quest->heroLevel > 0) + if(q.quest->mission.heroLevel > 0) return missionLevel(); if(q.quest->questName == CQuest::missionName(10)) @@ -127,7 +127,7 @@ TGoalVec CompleteQuest::missionArt() const CaptureObjectsBehavior findArts; - for(auto art : q.quest->artifacts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); } diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 89c71820b..7f6664a22 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -130,7 +130,9 @@ namespace AIPathfinding auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord); QuestAction questAction(questInfo); - if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->empty() && questObj->quest->killTarget == -1) + if(destination.nodeObject->ID == Obj::QUEST_GUARD + && questObj->quest->mission == Rewardable::Limiter{} + && questObj->quest->killTarget == ObjectInstanceID::NONE) { return false; } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index d915efd62..66241ff8b 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -29,26 +29,26 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { logAi->debug("Trying to realize quest: %s", questToString()); - if(!q.quest->artifacts.empty()) + if(!q.quest->mission.artifacts.empty()) return missionArt(); - if(!q.quest->heroes.empty()) + if(!q.quest->mission.heroes.empty()) return missionHero(); - if(!q.quest->creatures.empty()) + if(!q.quest->mission.creatures.empty()) return missionArmy(); - if(q.quest->resources.nonZero()) + if(q.quest->mission.resources.nonZero()) return missionResources(); - if(q.quest->killTarget >= 0) + if(q.quest->killTarget != ObjectInstanceID::NONE) return missionDestroyObj(); - for(auto & s : q.quest->primary) + for(auto & s : q.quest->mission.primary) if(s) return missionIncreasePrimaryStat(); - if(q.quest->heroLevel > 0) + if(q.quest->mission.heroLevel > 0) return missionLevel(); if(q.quest->questName == CQuest::missionName(10)) @@ -127,7 +127,7 @@ TGoalVec CompleteQuest::missionArt() const if(!solutions.empty()) return solutions; - for(auto art : q.quest->artifacts) + for(auto art : q.quest->mission.artifacts) { solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? } @@ -155,7 +155,7 @@ TGoalVec CompleteQuest::missionArmy() const if(!solutions.empty()) return solutions; - for(auto creature : q.quest->creatures) + for(auto creature : q.quest->mission.creatures) { solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); } @@ -169,7 +169,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const if(solutions.empty()) { - for(int i = 0; i < q.quest->primary.size(); ++i) + for(int i = 0; i < q.quest->mission.primary.size(); ++i) { // TODO: library, school and other boost objects logAi->debug("Don't know how to increase primary stat %d", i); @@ -185,7 +185,7 @@ TGoalVec CompleteQuest::missionLevel() const if(solutions.empty()) { - logAi->debug("Don't know how to reach hero level %d", q.quest->heroLevel); + logAi->debug("Don't know how to reach hero level %d", q.quest->mission.heroLevel); } return solutions; @@ -217,10 +217,10 @@ TGoalVec CompleteQuest::missionResources() const } else { - for(int i = 0; i < q.quest->resources.size(); ++i) + for(int i = 0; i < q.quest->mission.resources.size(); ++i) { - if(q.quest->resources[i]) - solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->resources[i]))); + if(q.quest->mission.resources[i]) + solutions.push_back(sptr(CollectRes(static_cast(i), q.quest->mission.resources[i]))); } } } diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 3aa316327..9be9b6fcd 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -456,7 +456,6 @@ Keep in mind, that all randomization is performed on map load and on object rese - Can be used as limiter - Can NOT be used as reward - Only players with specific color can pass the limiter -- If not specified or empty all colors may pass the limiter ```jsonc "colors" : [ "red", "blue", "tan", "green", "orange", "purple", "teal", "pink" ] diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 317cabaf8..9f8a3b20f 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1029,6 +1029,14 @@ void CStackBasicDescriptor::setType(const CCreature * c) type = c; } +bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r) +{ + return (!l.type && !r.type) + || (l.type && r.type + && l.type->getId() == r.type->getId() + && l.count == r.count); +} + void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("amount", count); diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 6dff6b7fa..abc2d09d1 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -42,6 +42,8 @@ public: TQuantity getCount() const; virtual void setType(const CCreature * c); + + friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5ddaef105..f8de8ec4b 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -40,7 +40,7 @@ CQuest::CQuest(): qid(-1), progress(NOT_ACTIVE), lastDay(-1), - killTarget(-1), + killTarget(ObjectInstanceID::NONE), textOption(0), completedOption(0), stackDirection(0), @@ -101,7 +101,7 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) ui32 count = 0; ui32 slotsCount = 0; bool hasExtraCreatures = false; - for(cre = q->creatures.begin(); cre != q->creatures.end(); ++cre) + for(cre = q->mission.creatures.begin(); cre != q->mission.creatures.end(); ++cre) { for(count = 0, it = army->Slots().begin(); it != army->Slots().end(); ++it) { @@ -123,10 +123,10 @@ bool CQuest::checkMissionArmy(const CQuest * q, const CCreatureSet * army) bool CQuest::checkQuest(const CGHeroInstance * h) const { - if(!heroAllowed(h)) + if(!mission.heroAllowed(h)) return false; - if(killTarget >= 0) + if(killTarget != ObjectInstanceID::NONE) { if(CGHeroInstance::cb->getObjByQuestIdentifier(killTarget)) return false; @@ -137,7 +137,7 @@ bool CQuest::checkQuest(const CGHeroInstance * h) const void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { - for(auto & elem : artifacts) + for(auto & elem : mission.artifacts) { if(h->hasArt(elem)) { @@ -161,36 +161,36 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const } } - cb->takeCreatures(h->id, creatures); - cb->giveResources(h->getOwner(), resources); + cb->takeCreatures(h->id, mission.creatures); + cb->giveResources(h->getOwner(), mission.resources); } void CQuest::addTextReplacements(MetaString & text, std::vector & components) const { - if(heroLevel > 0) - text.replaceNumber(heroLevel); + if(mission.heroLevel > 0) + text.replaceNumber(mission.heroLevel); - if(heroExperience > 0) - text.replaceNumber(heroExperience); + if(mission.heroExperience > 0) + text.replaceNumber(mission.heroExperience); { //primary skills MetaString loot; for(int i = 0; i < 4; ++i) { - if(primary[i]) + if(mission.primary[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(primary[i]); + loot.replaceNumber(mission.primary[i]); loot.replaceRawString(VLC->generaltexth->primarySkillNames[i]); } } - for(auto & skill : secondary) + for(auto & skill : mission.secondary) { loot.appendTextID(VLC->skillh->getById(skill.first)->getNameTextID()); } - for(auto & spell : spells) + for(auto & spell : mission.spells) { loot.appendTextID(VLC->spellh->getById(spell)->getNameTextID()); } @@ -199,25 +199,25 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(killTarget >= 0 && !heroName.empty()) + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) { components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); addKillTargetReplacements(text); } - if(killTarget >= 0 && stackToKill.type) + if(killTarget != ObjectInstanceID::NONE && stackToKill.type) { components.emplace_back(stackToKill); addKillTargetReplacements(text); } - if(!heroes.empty()) - text.replaceRawString(VLC->heroh->getById(heroes.front())->getNameTranslated()); + if(!mission.heroes.empty()) + text.replaceRawString(VLC->heroh->getById(mission.heroes.front())->getNameTranslated()); - if(!artifacts.empty()) + if(!mission.artifacts.empty()) { MetaString loot; - for(const auto & elem : artifacts) + for(const auto & elem : mission.artifacts) { loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); @@ -225,10 +225,10 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(!creatures.empty()) + if(!mission.creatures.empty()) { MetaString loot; - for(const auto & elem : creatures) + for(const auto & elem : mission.creatures) { loot.appendRawString("%s"); loot.replaceCreatureName(elem); @@ -236,25 +236,25 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } - if(resources.nonZero()) + if(mission.resources.nonZero()) { MetaString loot; for(int i = 0; i < 7; ++i) { - if(resources[i]) + if(mission.resources[i]) { loot.appendRawString("%d %s"); - loot.replaceNumber(resources[i]); + loot.replaceNumber(mission.resources[i]); loot.replaceLocalString(EMetaText::RES_NAMES, i); } } text.replaceRawString(loot.buildList()); } - if(!players.empty()) + if(!mission.players.empty()) { MetaString loot; - for(auto & p : players) + for(auto & p : mission.players) loot.appendLocalString(EMetaText::COLOR, p); text.replaceRawString(loot.buildList()); @@ -264,7 +264,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const { bool failRequirements = (h ? !checkQuest(h) : true); - loadComponents(components, h); + mission.loadComponents(components, h); if(firstVisit) iwText.appendRawString(firstVisitText.toString()); @@ -299,17 +299,17 @@ void CQuest::defineQuestName() { //standard quests questName = CQuest::missionName(0); - if(heroLevel > 0) questName = CQuest::missionName(1); - for(auto & s : primary) if(s) questName = CQuest::missionName(2); - if(!spells.empty()) questName = CQuest::missionName(2); - if(!secondary.empty()) questName = CQuest::missionName(2); - if(killTarget >= 0 && !heroName.empty()) questName = CQuest::missionName(3); - if(killTarget >= 0 && stackToKill.getType()) questName = CQuest::missionName(4); - if(!artifacts.empty()) questName = CQuest::missionName(5); - if(!creatures.empty()) questName = CQuest::missionName(6); - if(resources.nonZero()) questName = CQuest::missionName(7); - if(!heroes.empty()) questName = CQuest::missionName(8); - if(!players.empty()) questName = CQuest::missionName(9); + if(mission.heroLevel > 0) questName = CQuest::missionName(1); + for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2); + if(!mission.spells.empty()) questName = CQuest::missionName(2); + if(!mission.secondary.empty()) questName = CQuest::missionName(2); + if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3); + if(killTarget != ObjectInstanceID::NONE && stackToKill.getType()) questName = CQuest::missionName(4); + if(!mission.artifacts.empty()) questName = CQuest::missionName(5); + if(!mission.creatures.empty()) questName = CQuest::missionName(6); + if(mission.resources.nonZero()) questName = CQuest::missionName(7); + if(!mission.heroes.empty()) questName = CQuest::missionName(8); + if(!mission.players.empty()) questName = CQuest::missionName(9); } void CQuest::addKillTargetReplacements(MetaString &out) const @@ -339,10 +339,9 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi isCustomComplete = !completedText.empty(); } - Rewardable::Limiter::serializeJson(handler); - handler.serializeInt("timeLimit", lastDay, -1); - handler.serializeInstance("killTarget", killTarget, -1); + handler.serializeStruct("limiter", mission); + handler.serializeInstance("killTarget", killTarget, ObjectInstanceID::NONE); if(!handler.saving) //compatibility with legacy vmaps { @@ -352,22 +351,22 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi return; if(missionType == "Level") - handler.serializeInt("heroLevel", heroLevel, -1); + handler.serializeInt("heroLevel", mission.heroLevel, -1); if(missionType == "PrimaryStat") { auto primarySkills = handler.enterStruct("primarySkills"); for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) - handler.serializeInt(NPrimarySkill::names[i], primary[i], 0); + handler.serializeInt(NPrimarySkill::names[i], mission.primary[i], 0); } if(missionType == "Artifact") - handler.serializeIdArray("artifacts", artifacts); + handler.serializeIdArray("artifacts", mission.artifacts); if(missionType == "Army") { auto a = handler.enterArray("creatures"); - a.serializeStruct(creatures); + a.serializeStruct(mission.creatures); } if(missionType == "Resources") @@ -376,7 +375,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi for(size_t idx = 0; idx < (GameConstants::RESOURCE_QUANTITY - 1); idx++) { - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], resources[idx], 0); + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], mission.resources[idx], 0); } } @@ -384,14 +383,14 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi { ui32 temp; handler.serializeId("hero", temp, 0); - heroes.emplace_back(temp); + mission.heroes.emplace_back(temp); } if(missionType == "Player") { ui32 temp; handler.serializeId("player", temp, PlayerColor::NEUTRAL); - players.emplace_back(temp); + mission.players.emplace_back(temp); } } @@ -457,7 +456,7 @@ void CGSeerHut::initObj(CRandomGenerator & rand) setObjToKill(); quest->defineQuestName(); - if(quest->empty() && quest->killTarget == -1) + if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE) quest->progress = CQuest::COMPLETE; if(quest->questName == quest->missionName(0)) @@ -504,7 +503,7 @@ void CGSeerHut::setPropertyDer (ui8 what, ui32 val) { switch(what) { - case 10: + case CGSeerHut::OBJPROP_VISITED: quest->progress = static_cast(val); break; } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 6a6383fe3..2fd8520da 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGCreature; -class DLL_LINKAGE CQuest: public Rewardable::Limiter +class DLL_LINKAGE CQuest final { public: @@ -36,7 +36,8 @@ public: EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit - int killTarget; + ObjectInstanceID killTarget; + Rewardable::Limiter mission; bool repeatedQuest; // following fields are used only for kill creature/hero missions, the original @@ -89,7 +90,8 @@ public: h & isCustomComplete; h & completedOption; h & questName; - h & static_cast(*this); + h & mission; + h & killTarget; } void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 9fc78927d..112511213 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1890,7 +1890,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con if(artID != ArtifactID::NONE) { //not none quest - hut->quest->artifacts.push_back(artID); + hut->quest->mission.artifacts.push_back(artID); missionType = EQuestMission::ARTIFACT; } hut->quest->lastDay = -1; //no timeout @@ -2003,19 +2003,19 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { for(int x = 0; x < 4; ++x) { - guard->quest->primary[x] = reader->readUInt8(); + guard->quest->mission.primary[x] = reader->readUInt8(); } break; } case EQuestMission::LEVEL: { - guard->quest->heroLevel = reader->readUInt32(); + guard->quest->mission.heroLevel = reader->readUInt32(); break; } case EQuestMission::KILL_HERO: case EQuestMission::KILL_CREATURE: { - guard->quest->killTarget = reader->readUInt32(); + guard->quest->killTarget = ObjectInstanceID(reader->readUInt32()); break; } case EQuestMission::ARTIFACT: @@ -2024,7 +2024,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) for(int yy = 0; yy < artNumber; ++yy) { auto artid = reader->readArtifact(); - guard->quest->artifacts.push_back(artid); + guard->quest->mission.artifacts.push_back(artid); map->allowedArtifact[artid] = false; //these are unavailable for random generation } break; @@ -2032,29 +2032,29 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) case EQuestMission::ARMY: { int typeNumber = reader->readUInt8(); - guard->quest->creatures.resize(typeNumber); + guard->quest->mission.creatures.resize(typeNumber); for(int hh = 0; hh < typeNumber; ++hh) { - guard->quest->creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->creatures[hh].count = reader->readUInt16(); + guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->mission.creatures[hh].count = reader->readUInt16(); } break; } case EQuestMission::RESOURCES: { for(int x = 0; x < 7; ++x) - guard->quest->resources[x] = reader->readUInt32(); + guard->quest->mission.resources[x] = reader->readUInt32(); break; } case EQuestMission::HERO: { - guard->quest->heroes.push_back(reader->readHero()); + guard->quest->mission.heroes.push_back(reader->readHero()); break; } case EQuestMission::PLAYER: { - guard->quest->players.push_back(reader->readPlayer()); + guard->quest->mission.players.push_back(reader->readPlayer()); break; } case EQuestMission::HOTA_MULTI: @@ -2067,13 +2067,13 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) std::set heroClasses; reader->readBitmaskHeroClassesSized(heroClasses, false); for(auto & hc : heroClasses) - guard->quest->heroClasses.push_back(hc); + guard->quest->mission.heroClasses.push_back(hc); break; } if(missionSubID == 1) { missionId = int(EQuestMission::HOTA_REACH_DATE); - guard->quest->daysPassed = reader->readUInt32(); + guard->quest->mission.daysPassed = reader->readUInt32(); break; } break; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b9a8f1b2b..6453b9175 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -112,7 +112,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); limiter.resources = JsonRandom::loadResources(source["resources"], rng); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 8843d4e39..d49ccd550 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -27,7 +27,7 @@ Rewardable::Limiter::Limiter() , daysPassed(0) , heroExperience(0) , heroLevel(-1) - , manaPercentage(0) + , manaPercentage(-1) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) { @@ -35,35 +35,26 @@ Rewardable::Limiter::Limiter() Rewardable::Limiter::~Limiter() = default; -bool Rewardable::Limiter::empty() const +bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) { - if(dayOfWeek != 0 - || daysPassed != 0 - || heroLevel > 0 - || heroExperience > 0 - || manaPoints > 0 - || manaPercentage > 0 - || !secondary.empty() - || !creatures.empty() - || !spells.empty() - || !artifacts.empty() - || !players.empty() - || !heroes.empty() - || !heroClasses.empty() - || resources.nonZero() - || std::find_if(primary.begin(), primary.end(), [](si32 i){return i != 0;}) != primary.end()) - return false; - - for(const auto & sub : {noneOf, allOf, anyOf}) - { - for(const auto & sublimiter : sub) - { - if(!sublimiter->empty()) - return false; - } - } - - return true; + return l.dayOfWeek == r.dayOfWeek + && l.daysPassed == r.daysPassed + && l.heroLevel == r.heroLevel + && l.heroExperience == r.heroExperience + && l.manaPoints == r.manaPoints + && l.manaPercentage == r.manaPercentage + && l.secondary == r.secondary + && l.creatures == r.creatures + && l.spells == r.spells + && l.artifacts == r.artifacts + && l.players == r.players + && l.heroes == r.heroes + && l.heroClasses == r.heroClasses + && l.resources == r.resources + && l.primary == r.primary + && l.noneOf == r.noneOf + && l.allOf == r.allOf + && l.anyOf == r.anyOf; } bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const @@ -127,7 +118,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const } { - std::unordered_map artifactsRequirements; // artifact ID -> required count + std::unordered_map artifactsRequirements; // artifact ID -> required count for(const auto & art : artifacts) ++artifactsRequirements[art]; @@ -186,9 +177,9 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); - if (manaPoints || manaPercentage > 0) + if (manaPoints || manaPercentage >= 0) { - int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + int absoluteMana = (h->manaLimit() && manaPercentage >= 0) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); } @@ -232,7 +223,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("dayOfWeek", dayOfWeek); handler.serializeInt("daysPassed", daysPassed); resources.serializeJson(handler, "resources"); - handler.serializeInt("manaPercentage", manaPercentage); + handler.serializeInt("manaPercentage", manaPercentage, -1); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); handler.serializeIdArray("heroes", heroes); diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index fd41dd031..a29843615 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -26,7 +26,7 @@ using LimitersList = std::vector>; /// Limiters of rewards. Rewards will be granted to hero only if he satisfies requirements /// Note: for this is only a test - it won't remove anything from hero (e.g. artifacts or creatures) -struct DLL_LINKAGE Limiter +struct DLL_LINKAGE Limiter final { /// day of week, unused if 0, 1-7 will test for current day of week si32 dayOfWeek; @@ -81,7 +81,6 @@ struct DLL_LINKAGE Limiter virtual ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; - bool empty() const; /// Generates list of components that describes reward for a specific hero virtual void loadComponents(std::vector & comps, @@ -100,12 +99,12 @@ struct DLL_LINKAGE Limiter h & secondary; h & artifacts; h & creatures; - h & allOf; - h & anyOf; - h & noneOf; h & heroes; h & heroClasses; h & players; + h & allOf; + h & anyOf; + h & noneOf; } void serializeJson(JsonSerializeFormat & handler); @@ -113,4 +112,6 @@ struct DLL_LINKAGE Limiter } +bool DLL_LINKAGE operator== (const Rewardable::Limiter & l, const Rewardable::Limiter & r); + VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 59fd44bcc..bed4d91b5 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -29,7 +29,7 @@ using RewardsList = std::vector>; /// Reward that can be granted to a hero /// NOTE: eventually should replace seer hut rewards and events/pandoras -struct DLL_LINKAGE Reward +struct DLL_LINKAGE Reward final { /// resources that will be given to player TResources resources; @@ -86,7 +86,7 @@ struct DLL_LINKAGE Reward si32 calculateManaPoints(const CGHeroInstance * h) const; Reward(); - virtual ~Reward(); + ~Reward(); template void serialize(Handler &h, const int version) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 542c8294e..20dc751b2 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -462,7 +462,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -510,7 +510,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); @@ -532,7 +532,7 @@ void TreasurePlacer::addAllPossibleObjects() obj->configuration.info.push_back(reward); ArtifactID artid = qap->drawRandomArtifact(); - obj->quest->artifacts.push_back(artid); + obj->quest->mission.artifacts.push_back(artid); generator.banQuestArt(artid); zone.getModificator()->addQuestArtifact(artid); diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index bf611834e..e50d91c86 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -137,40 +137,40 @@ QuestWidget::~QuestWidget() void QuestWidget::obtainData() { - ui->lDayOfWeek->setCurrentIndex(quest.dayOfWeek); - ui->lDaysPassed->setValue(quest.daysPassed); - ui->lHeroLevel->setValue(quest.heroLevel); - ui->lHeroExperience->setValue(quest.heroExperience); - ui->lManaPoints->setValue(quest.manaPoints); - ui->lManaPercentage->setValue(quest.manaPercentage); - ui->lAttack->setValue(quest.primary[0]); - ui->lDefence->setValue(quest.primary[1]); - ui->lPower->setValue(quest.primary[2]); - ui->lKnowledge->setValue(quest.primary[3]); + ui->lDayOfWeek->setCurrentIndex(quest.mission.dayOfWeek); + ui->lDaysPassed->setValue(quest.mission.daysPassed); + ui->lHeroLevel->setValue(quest.mission.heroLevel); + ui->lHeroExperience->setValue(quest.mission.heroExperience); + ui->lManaPoints->setValue(quest.mission.manaPoints); + ui->lManaPercentage->setValue(quest.mission.manaPercentage); + ui->lAttack->setValue(quest.mission.primary[0]); + ui->lDefence->setValue(quest.mission.primary[1]); + ui->lPower->setValue(quest.mission.primary[2]); + ui->lKnowledge->setValue(quest.mission.primary[3]); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) - widget->setValue(quest.resources[i]); + widget->setValue(quest.mission.resources[i]); } - for(auto i : quest.artifacts) + for(auto i : quest.mission.artifacts) ui->lArtifacts->item(VLC->artifacts()->getById(i)->getIndex())->setCheckState(Qt::Checked); - for(auto i : quest.spells) + for(auto i : quest.mission.spells) ui->lArtifacts->item(VLC->spells()->getById(i)->getIndex())->setCheckState(Qt::Checked); - for(auto & i : quest.secondary) + for(auto & i : quest.mission.secondary) { int index = VLC->skills()->getById(i.first)->getIndex(); if(auto * widget = qobject_cast(ui->lSkills->cellWidget(index, 1))) widget->setCurrentIndex(i.second); } - for(auto & i : quest.creatures) + for(auto & i : quest.mission.creatures) { int index = i.type->getIndex(); ui->lCreatureId->setCurrentIndex(index); ui->lCreatureAmount->setValue(i.count); onCreatureAdd(ui->lCreatures, ui->lCreatureId, ui->lCreatureAmount); } - for(auto & i : quest.heroes) + for(auto & i : quest.mission.heroes) { for(int e = 0; e < ui->lHeroes->count(); ++e) { @@ -181,7 +181,7 @@ void QuestWidget::obtainData() } } } - for(auto & i : quest.heroClasses) + for(auto & i : quest.mission.heroClasses) { for(int e = 0; e < ui->lHeroClasses->count(); ++e) { @@ -192,7 +192,7 @@ void QuestWidget::obtainData() } } } - for(auto & i : quest.players) + for(auto & i : quest.mission.players) { for(int e = 0; e < ui->lPlayers->count(); ++e) { @@ -204,81 +204,81 @@ void QuestWidget::obtainData() } } - if(quest.killTarget >= 0 && quest.killTarget < controller.map()->objects.size()) + if(quest.killTarget != ObjectInstanceID::NONE && quest.killTarget < controller.map()->objects.size()) ui->lKillTarget->setText(QString::fromStdString(controller.map()->objects[quest.killTarget]->instanceName)); else - quest.killTarget = -1; + quest.killTarget = ObjectInstanceID::NONE; } bool QuestWidget::commitChanges() { - quest.dayOfWeek = ui->lDayOfWeek->currentIndex(); - quest.daysPassed = ui->lDaysPassed->value(); - quest.heroLevel = ui->lHeroLevel->value(); - quest.heroExperience = ui->lHeroExperience->value(); - quest.manaPoints = ui->lManaPoints->value(); - quest.manaPercentage = ui->lManaPercentage->value(); - quest.primary[0] = ui->lAttack->value(); - quest.primary[1] = ui->lDefence->value(); - quest.primary[2] = ui->lPower->value(); - quest.primary[3] = ui->lKnowledge->value(); + quest.mission.dayOfWeek = ui->lDayOfWeek->currentIndex(); + quest.mission.daysPassed = ui->lDaysPassed->value(); + quest.mission.heroLevel = ui->lHeroLevel->value(); + quest.mission.heroExperience = ui->lHeroExperience->value(); + quest.mission.manaPoints = ui->lManaPoints->value(); + quest.mission.manaPercentage = ui->lManaPercentage->value(); + quest.mission.primary[0] = ui->lAttack->value(); + quest.mission.primary[1] = ui->lDefence->value(); + quest.mission.primary[2] = ui->lPower->value(); + quest.mission.primary[3] = ui->lKnowledge->value(); for(int i = 0; i < ui->lResources->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lResources->cellWidget(i, 1))) - quest.resources[i] = widget->value(); + quest.mission.resources[i] = widget->value(); } - quest.artifacts.clear(); + quest.mission.artifacts.clear(); for(int i = 0; i < ui->lArtifacts->count(); ++i) { if(ui->lArtifacts->item(i)->checkState() == Qt::Checked) - quest.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); + quest.mission.artifacts.push_back(VLC->artifacts()->getByIndex(i)->getId()); } - quest.spells.clear(); + quest.mission.spells.clear(); for(int i = 0; i < ui->lSpells->count(); ++i) { if(ui->lSpells->item(i)->checkState() == Qt::Checked) - quest.spells.push_back(VLC->spells()->getByIndex(i)->getId()); + quest.mission.spells.push_back(VLC->spells()->getByIndex(i)->getId()); } - quest.secondary.clear(); + quest.mission.secondary.clear(); for(int i = 0; i < ui->lSkills->rowCount(); ++i) { if(auto * widget = qobject_cast(ui->lSkills->cellWidget(i, 1))) { if(widget->currentIndex() > 0) - quest.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); + quest.mission.secondary[VLC->skills()->getByIndex(i)->getId()] = widget->currentIndex(); } } - quest.creatures.clear(); + quest.mission.creatures.clear(); for(int i = 0; i < ui->lCreatures->rowCount(); ++i) { int index = ui->lCreatures->item(i, 0)->data(Qt::UserRole).toInt(); if(auto * widget = qobject_cast(ui->lCreatures->cellWidget(i, 1))) if(widget->value()) - quest.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); + quest.mission.creatures.emplace_back(VLC->creatures()->getByIndex(index)->getId(), widget->value()); } - quest.heroes.clear(); + quest.mission.heroes.clear(); for(int i = 0; i < ui->lHeroes->count(); ++i) { if(ui->lHeroes->item(i)->checkState() == Qt::Checked) - quest.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); + quest.mission.heroes.emplace_back(ui->lHeroes->item(i)->data(Qt::UserRole).toInt()); } - quest.heroClasses.clear(); + quest.mission.heroClasses.clear(); for(int i = 0; i < ui->lHeroClasses->count(); ++i) { if(ui->lHeroClasses->item(i)->checkState() == Qt::Checked) - quest.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); + quest.mission.heroClasses.emplace_back(ui->lHeroClasses->item(i)->data(Qt::UserRole).toInt()); } - quest.players.clear(); + quest.mission.players.clear(); for(int i = 0; i < ui->lPlayers->count(); ++i) { if(ui->lPlayers->item(i)->checkState() == Qt::Checked) - quest.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); + quest.mission.players.emplace_back(ui->lPlayers->item(i)->data(Qt::UserRole).toInt()); } //quest.killTarget is set directly in object picking @@ -358,7 +358,7 @@ void QuestWidget::onTargetPicked(const CGObjectInstance * obj) if(!obj) //discarded { - quest.killTarget = -1; + quest.killTarget = ObjectInstanceID::NONE; ui->lKillTarget->setText(""); return; } From 8335fffaea5417af192d06584156f97648037bca Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 00:58:32 +0200 Subject: [PATCH 0788/1248] Fix rumors --- lib/CGameInfoCallback.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index bc3dfebe3..077f862c5 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -662,9 +662,9 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav case RumorState::TYPE_SPECIAL: text.replaceLocalString(EMetaText::GENERAL_TXT, rumor.first); if(rumor.first == RumorState::RUMOR_GRAIL) - text.replaceTextID(TextIdentifier("core", "genrltxt", "arraytxt", 158 + rumor.second).get()); + text.replaceTextID(TextIdentifier("core", "arraytxt", 158 + rumor.second).get()); else - text.replaceTextID(TextIdentifier("core", "genrltxt", "capitalColors", rumor.second).get()); + text.replaceTextID(TextIdentifier("core", "plcolors", rumor.second).get()); break; case RumorState::TYPE_MAP: @@ -672,7 +672,7 @@ std::string CGameInfoCallback::getTavernRumor(const CGObjectInstance * townOrTav break; case RumorState::TYPE_RAND: - text.replaceTextID(TextIdentifier("core", "genrltxt", "randtvrn", rumor.first).get()); + text.replaceTextID(TextIdentifier("core", "randtvrn", rumor.first).get()); break; } From 6093f042dd2a92080d583eb024b8eee52ccad395 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:26:07 +0200 Subject: [PATCH 0789/1248] Text container fix --- lib/CGeneralTextHandler.cpp | 10 +++++----- lib/CGeneralTextHandler.h | 2 +- lib/mapping/CMapHeader.cpp | 3 +++ lib/mapping/CMapInfo.cpp | 3 +++ 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 657fb564e..2a5e280d7 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -264,21 +264,21 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) { - subContainers.insert(&container); + subContainers.push_back(&container); } void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { - subContainers.erase(&container); + std::remove(subContainers.begin(), subContainers.end(), &container); } const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const { if(stringsLocalizations.count(identifier.get()) == 0) { - for(const auto * container : subContainers) - if(container->identifierExists(identifier)) - return container->deserialize(identifier); + for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) + if((*containerIter)->identifierExists(identifier)) + return (*containerIter)->deserialize(identifier); logGlobal->error("Unable to find localization for string '%s'", identifier.get()); return identifier.get(); diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 3807ddc7a..7bc7b3b94 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -146,7 +146,7 @@ protected: /// map identifier -> localization std::unordered_map stringsLocalizations; - std::set subContainers; + std::vector subContainers; /// add selected string to internal storage as high-priority strings void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index c86a923b8..2ca1ee8e0 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -144,6 +144,9 @@ ui8 CMapHeader::levels() const void CMapHeader::registerMapStrings() { + VLC->generaltexth->removeSubContainer(*this); + VLC->generaltexth->addSubContainer(*this); + //get supported languages. Assuming that translation containing most strings is the base language std::set mapLanguages, mapBaseLanguages; int maxStrings = 0; diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index f037ef7d4..8fecd28e9 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -105,7 +105,10 @@ std::string CMapInfo::getNameTranslated() const if(campaign && !campaign->getNameTranslated().empty()) return campaign->getNameTranslated(); else if(mapHeader && !mapHeader->name.empty()) + { + mapHeader->registerMapStrings(); return mapHeader->name.toString(); + } else return VLC->generaltexth->allTexts[508]; } From 2b88e6941c744eb3e4b0bd46630f6a3978f9bd63 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:43:24 +0200 Subject: [PATCH 0790/1248] Fix mana limiter --- lib/rewardable/Info.cpp | 2 +- lib/rewardable/Limiter.cpp | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 6453b9175..b9a8f1b2b 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -112,7 +112,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); limiter.resources = JsonRandom::loadResources(source["resources"], rng); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index d49ccd550..48328d7a1 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -27,7 +27,7 @@ Rewardable::Limiter::Limiter() , daysPassed(0) , heroExperience(0) , heroLevel(-1) - , manaPercentage(-1) + , manaPercentage(0) , manaPoints(0) , primary(GameConstants::PRIMARY_SKILLS, 0) { @@ -177,9 +177,9 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); - if (manaPoints || manaPercentage >= 0) + if (manaPoints || manaPercentage > 0) { - int absoluteMana = (h->manaLimit() && manaPercentage >= 0) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); } @@ -223,7 +223,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("dayOfWeek", dayOfWeek); handler.serializeInt("daysPassed", daysPassed); resources.serializeJson(handler, "resources"); - handler.serializeInt("manaPercentage", manaPercentage, -1); + handler.serializeInt("manaPercentage", manaPercentage); handler.serializeInt("heroExperience", heroExperience); handler.serializeInt("heroLevel", heroLevel); handler.serializeIdArray("heroes", heroes); From 7b7aaa36b2608c35756db6111957522fc7e109ea Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 01:44:58 +0200 Subject: [PATCH 0791/1248] fix compiling --- mapeditor/inspector/questwidget.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index e50d91c86..97e22f586 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -328,7 +328,7 @@ void QuestWidget::on_lKillTargetSelect_clicked() { if(auto * o = dynamic_cast(obj)) return o->ID != Obj::PRISON; - if(auto * o = dynamic_cast(obj)) + if(dynamic_cast(obj)) return true; return false; }; From 22c9eecd7d83702c7e5ef379d5b347b17165c672 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 08:54:54 +0200 Subject: [PATCH 0792/1248] Fix mistake --- lib/CGeneralTextHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index 2a5e280d7..f68d6b3d4 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -269,7 +269,7 @@ void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) { - std::remove(subContainers.begin(), subContainers.end(), &container); + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); } const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const From abab917ef9a9ea920dae0f390d8d167ff7674643 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Wed, 11 Oct 2023 11:23:38 +0200 Subject: [PATCH 0793/1248] fix crash with custom campaign preview --- client/windows/CMapOverview.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index ffa7aeb03..43e726707 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -149,7 +149,7 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): const JsonNode config(JsonPath::builtin("config/widgets/mapOverview.json")); - if(settings["lobby"]["mapPreview"].Bool()) + if(settings["lobby"]["mapPreview"].Bool() && p.tabType != ESelectionScreen::campaignList) { ResourcePath res = ResourcePath(p.resource.getName(), EResType::MAP); std::unique_ptr campaignMap = nullptr; @@ -192,7 +192,7 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent): { if(p.date.empty()) { - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), EResType::MAP))); + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), p.tabType == ESelectionScreen::campaignList ? EResType::CAMPAIGN : EResType::MAP))); w->setText(vstd::getFormattedDateTime(time)); } else From 8d1b5e24dd6793de53e9f47d75df01df3e2de158 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Wed, 11 Oct 2023 18:49:47 +0200 Subject: [PATCH 0794/1248] Add russian translation for connection error message --- Mods/vcmi/config/vcmi/russian.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 618ffd861..24b7d7931 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -44,6 +44,7 @@ "vcmi.mainMenu.serverConnecting" : "Подключение...", "vcmi.mainMenu.serverAddressEnter" : "Введите адрес:", + "vcmi.mainMenu.serverConnectionFailed" : "Ошибка соединения", "vcmi.mainMenu.serverClosing" : "Завершение...", "vcmi.mainMenu.hostTCP" : "Создать игру по TCP/IP", "vcmi.mainMenu.joinTCP" : "Присединиться к игре по TCP/IP", From 7ccd4cdcb270b5e1aea253b4bd5ef99c236423cc Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 21:10:42 +0200 Subject: [PATCH 0795/1248] Refactor quests progress --- .../Pathfinding/Actions/QuestAction.cpp | 2 +- AI/VCAI/Goals/CompleteQuest.cpp | 2 +- client/windows/CQuestLog.cpp | 4 +- lib/mapObjects/CQuest.cpp | 39 +++++++++++-------- lib/mapObjects/CQuest.h | 15 +++---- lib/rewardable/Limiter.cpp | 5 +++ lib/rewardable/Limiter.h | 1 + 7 files changed, 38 insertions(+), 30 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp index dae8c7bb2..1efebafca 100644 --- a/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp +++ b/AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp @@ -25,7 +25,7 @@ namespace AIPathfinding return dynamic_cast(questInfo.obj)->checkQuest(node->actor->hero); } - return questInfo.quest->progress == CQuest::NOT_ACTIVE + return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner()) || questInfo.quest->checkQuest(node->actor->hero); } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 66241ff8b..42743c9ab 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -25,7 +25,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; - if(q.quest->progress != CQuest::COMPLETE) + if(!q.quest->isCompleted) { logAi->debug("Trying to realize quest: %s", questToString()); diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index 208a74744..b4b3d80af 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -152,7 +152,7 @@ void CQuestLog::recreateLabelList() if (quests[i].quest->questName == CQuest::missionName(0)) continue; - if (quests[i].quest->progress == CQuest::COMPLETE) + if (quests[i].quest->isCompleted) { completeMissing = false; if (hideComplete) @@ -180,7 +180,7 @@ void CQuestLog::recreateLabelList() labels.push_back(label); // Select latest active quest - if (quests[i].quest->progress != CQuest::COMPLETE) + if(!quests[i].quest->isCompleted) selectQuest(i, currentLabel); currentLabel = static_cast(labels.size()); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index f8de8ec4b..ee45b3e01 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -38,7 +38,7 @@ std::map > CGKeys::playerKeyMap; //TODO: Remove constructor CQuest::CQuest(): qid(-1), - progress(NOT_ACTIVE), + isCompleted(false), lastDay(-1), killTarget(ObjectInstanceID::NONE), textOption(0), @@ -450,14 +450,12 @@ void CGSeerHut::initObj(CRandomGenerator & rand) init(rand); CRewardableObject::initObj(rand); - - quest->progress = CQuest::NOT_ACTIVE; setObjToKill(); quest->defineQuestName(); if(quest->mission == Rewardable::Limiter{} && quest->killTarget == ObjectInstanceID::NONE) - quest->progress = CQuest::COMPLETE; + quest->isCompleted = true; if(quest->questName == quest->missionName(0)) { @@ -484,13 +482,15 @@ void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const std::string CGSeerHut::getHoverText(PlayerColor player) const { std::string hoverName = getObjectName(); - if(ID == Obj::SEER_HUT && quest->progress != CQuest::NOT_ACTIVE) + if(ID == Obj::SEER_HUT && quest->activeForPlayers.count(player)) { hoverName = VLC->generaltexth->allTexts[347]; boost::algorithm::replace_first(hoverName, "%s", seerName); } - if(quest->progress/* & quest->missionType*/) //rollover when the quest is active + if(quest->activeForPlayers.count(player) + && (quest->mission != Rewardable::Limiter{} + || quest->killTarget != ObjectInstanceID::NONE)) //rollover when the quest is active { MetaString ms; getRolloverText (ms, true); @@ -499,13 +499,21 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } -void CGSeerHut::setPropertyDer (ui8 what, ui32 val) +void CGSeerHut::setPropertyDer(ui8 what, ui32 val) { switch(what) { - case CGSeerHut::OBJPROP_VISITED: - quest->progress = static_cast(val); + case CGSeerHut::SEERHUT_VISITED: + { + quest->activeForPlayers.emplace(val); break; + } + case CGSeerHut::SEERHUT_COMPLETE: + { + quest->isCompleted = val; + quest->activeForPlayers.clear(); + break; + } } } @@ -514,7 +522,7 @@ void CGSeerHut::newTurn(CRandomGenerator & rand) const CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { - cb->setObjProperty (id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); + cb->setObjProperty (id, CGSeerHut::SEERHUT_COMPLETE, true); } } @@ -522,14 +530,14 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const { InfoWindow iw; iw.player = h->getOwner(); - if(quest->progress < CQuest::COMPLETE) + if(!quest->isCompleted) { - bool firstVisit = !quest->progress; + bool firstVisit = !quest->activeForPlayers.count(h->getOwner()); bool failRequirements = !checkQuest(h); if(firstVisit) { - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::IN_PROGRESS); + cb->setObjProperty(id, CGSeerHut::SEERHUT_VISITED, h->getOwner()); AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); @@ -612,10 +620,7 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - if(quest && quest->repeatedQuest) - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::NOT_ACTIVE); - else - cb->setObjProperty(id, CGSeerHut::OBJPROP_VISITED, CQuest::COMPLETE); //mission complete + cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete } } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 2fd8520da..f12a8ae8a 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -21,12 +21,6 @@ class DLL_LINKAGE CQuest final { public: - enum EProgress { - NOT_ACTIVE, - IN_PROGRESS, - COMPLETE - }; - static const std::string & missionName(int index); static const std::string & missionState(int index); @@ -34,11 +28,12 @@ public: si32 qid; //unique quest id for serialization / identification - EProgress progress; si32 lastDay; //after this day (first day is 0) mission cannot be completed; if -1 - no limit ObjectInstanceID killTarget; Rewardable::Limiter mission; bool repeatedQuest; + bool isCompleted; + std::set activeForPlayers; // following fields are used only for kill creature/hero missions, the original // objects became inaccessible after their removal, so we need to store info @@ -75,7 +70,8 @@ public: template void serialize(Handler &h, const int version) { h & qid; - h & progress; + h & isCompleted; + h & activeForPlayers; h & lastDay; h & textOption; h & stackToKill; @@ -143,7 +139,8 @@ public: h & seerName; } protected: - static constexpr int OBJPROP_VISITED = 10; + static constexpr int SEERHUT_VISITED = 10; + static constexpr int SEERHUT_COMPLETE = 11; void setPropertyDer(ui8 what, ui32 val) override; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 48328d7a1..b0f8c74ac 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -57,6 +57,11 @@ bool operator==(const Rewardable::Limiter & l, const Rewardable::Limiter & r) && l.anyOf == r.anyOf; } +bool operator!=(const Rewardable::Limiter & l, const Rewardable::Limiter & r) +{ + return !(l == r); +} + bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const { if(dayOfWeek != 0) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index a29843615..35e1b8af9 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -113,5 +113,6 @@ struct DLL_LINKAGE Limiter final } bool DLL_LINKAGE operator== (const Rewardable::Limiter & l, const Rewardable::Limiter & r); +bool DLL_LINKAGE operator!= (const Rewardable::Limiter & l, const Rewardable::Limiter & r); VCMI_LIB_NAMESPACE_END From 14b030d2ebd9e9943191fbb8c8df0deb8df74fca Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 23:15:24 +0200 Subject: [PATCH 0796/1248] Support hota quest gates --- lib/mapObjects/CQuest.cpp | 15 ++++++++++++++- lib/mapObjects/CQuest.h | 3 +++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index ee45b3e01..10818b2aa 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -710,10 +710,23 @@ void CGQuestGuard::init(CRandomGenerator & rand) configuration.info.push_back({}); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - configuration.info.back().reward.removeObject = true; + configuration.info.back().reward.removeObject = subID == 0 ? true : false; configuration.canRefuse = true; } +void CGQuestGuard::onHeroVisit(const CGHeroInstance * h) const +{ + if(!quest->isCompleted) + CGSeerHut::onHeroVisit(h); + else + cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, false); +} + +bool CGQuestGuard::passableFor(PlayerColor color) const +{ + return quest->isCompleted; +} + void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) { //quest only, do not call base class diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index f12a8ae8a..3f76d3550 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -151,6 +151,9 @@ class DLL_LINKAGE CGQuestGuard : public CGSeerHut { public: void init(CRandomGenerator & rand) override; + + void onHeroVisit(const CGHeroInstance * h) const override; + bool passableFor(PlayerColor color) const override; template void serialize(Handler &h, const int version) { From 3199b7261f5fa01bb378a71ed4007ef0a274b7df Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 11 Oct 2023 23:28:10 +0200 Subject: [PATCH 0797/1248] Cosmetic fixes for map editor --- mapeditor/inspector/inspector.cpp | 15 +++++++++++++++ mapeditor/inspector/inspector.h | 1 + 2 files changed, 16 insertions(+) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 4a4590974..613aa2c94 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -410,6 +410,14 @@ void Inspector::updateProperties(CGSeerHut * o) } } +void Inspector::updateProperties(CGQuestGuard * o) +{ + if(!o || !o->quest) return; + + addProperty("Reward", PropertyEditorPlaceholder(), nullptr, true); + addProperty("Repeat quest", o->quest->repeatedQuest, true); +} + void Inspector::updateProperties() { if(!obj) @@ -452,6 +460,7 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGPandoraBox); UPDATE_OBJ_PROPERTIES(CGEvent); UPDATE_OBJ_PROPERTIES(CGSeerHut); + UPDATE_OBJ_PROPERTIES(CGQuestGuard); table->show(); } @@ -498,6 +507,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGPandoraBox); SET_PROPERTIES(CGEvent); SET_PROPERTIES(CGSeerHut); + SET_PROPERTIES(CGQuestGuard); } void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVariant & value) @@ -681,6 +691,11 @@ void Inspector::setProperty(CGSeerHut * o, const QString & key, const QVariant & o->quest->lastDay = value.toString().toInt(); } +void Inspector::setProperty(CGQuestGuard * o, const QString & key, const QVariant & value) +{ + if(!o) return; +} + //===============IMPLEMENT PROPERTY VALUE TYPE============================ QTableWidgetItem * Inspector::addProperty(CGObjectInstance * value) diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index 5e2e1f96a..e4a85715f 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -82,6 +82,7 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox); DECLARE_OBJ_PROPERTY_METHODS(CGEvent); DECLARE_OBJ_PROPERTY_METHODS(CGSeerHut); + DECLARE_OBJ_PROPERTY_METHODS(CGQuestGuard); //===============DECLARE PROPERTY VALUE TYPE============================== QTableWidgetItem * addProperty(unsigned int value); From 74a90cde5d078d4ad8d70d24e2dfdb3b048ddde1 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 12:52:01 +0200 Subject: [PATCH 0798/1248] Fix hero level limiter --- lib/mapObjects/CQuest.cpp | 4 ++-- lib/rewardable/Limiter.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 10818b2aa..51d0d1ead 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -60,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -351,7 +351,7 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi return; if(missionType == "Level") - handler.serializeInt("heroLevel", mission.heroLevel, -1); + handler.serializeInt("heroLevel", mission.heroLevel); if(missionType == "PrimaryStat") { diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b0f8c74ac..b23c50610 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -179,7 +179,7 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, if (heroExperience) comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - if (heroLevel) + if (heroLevel > 0) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaPoints || manaPercentage > 0) From fbf5492fd9c0f17beab94732e643da751995d72a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:09:10 +0200 Subject: [PATCH 0799/1248] Remove unnecessary virtuals --- lib/rewardable/Limiter.h | 4 ++-- lib/rewardable/Reward.h | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 35e1b8af9..6fa52925e 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -78,12 +78,12 @@ struct DLL_LINKAGE Limiter final LimitersList noneOf; Limiter(); - virtual ~Limiter(); + ~Limiter(); bool heroAllowed(const CGHeroInstance * hero) const; /// Generates list of components that describes reward for a specific hero - virtual void loadComponents(std::vector & comps, + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; template void serialize(Handler &h, const int version) diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index bed4d91b5..9493083f1 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -78,7 +78,7 @@ struct DLL_LINKAGE Reward final bool removeObject; /// Generates list of components that describes reward for a specific hero - virtual void loadComponents(std::vector & comps, + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; Component getDisplayedComponent(const CGHeroInstance * h) const; From fadf086e61db25f6c7be1c98f3eaa62635d43378 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:29:52 +0200 Subject: [PATCH 0800/1248] Fix review comments --- AI/Nullkiller/Goals/CompleteQuest.cpp | 3 --- AI/VCAI/Goals/CompleteQuest.cpp | 13 +++++++++---- client/windows/CQuestLog.cpp | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/AI/Nullkiller/Goals/CompleteQuest.cpp b/AI/Nullkiller/Goals/CompleteQuest.cpp index 20ef080f2..d0e981a78 100644 --- a/AI/Nullkiller/Goals/CompleteQuest.cpp +++ b/AI/Nullkiller/Goals/CompleteQuest.cpp @@ -59,9 +59,6 @@ TGoalVec CompleteQuest::decompose() const if(q.quest->mission.heroLevel > 0) return missionLevel(); - - if(q.quest->questName == CQuest::missionName(10)) - return missionKeymaster(); return TGoalVec(); } diff --git a/AI/VCAI/Goals/CompleteQuest.cpp b/AI/VCAI/Goals/CompleteQuest.cpp index 42743c9ab..cb78ee8db 100644 --- a/AI/VCAI/Goals/CompleteQuest.cpp +++ b/AI/VCAI/Goals/CompleteQuest.cpp @@ -21,6 +21,11 @@ bool CompleteQuest::operator==(const CompleteQuest & other) const return q.quest->qid == other.q.quest->qid; } +bool isKeyMaster(const QuestInfo & q) +{ + return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD); +} + TGoalVec CompleteQuest::getAllPossibleSubgoals() { TGoalVec solutions; @@ -28,6 +33,9 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() if(!q.quest->isCompleted) { logAi->debug("Trying to realize quest: %s", questToString()); + + if(isKeyMaster(q)) + return missionKeymaster(); if(!q.quest->mission.artifacts.empty()) return missionArt(); @@ -50,9 +58,6 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() if(q.quest->mission.heroLevel > 0) return missionLevel(); - - if(q.quest->questName == CQuest::missionName(10)) - return missionKeymaster(); } return TGoalVec(); @@ -60,7 +65,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals() TSubgoal CompleteQuest::whatToDoToAchieve() { - if(q.quest->questName == CQuest::missionName(0)) + if(q.quest->mission == Rewardable::Limiter{}) { throw cannotFulfillGoalException("Can not complete inactive quest"); } diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index b4b3d80af..c43870c9b 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -148,8 +148,8 @@ void CQuestLog::recreateLabelList() int currentLabel = 0; for (int i = 0; i < quests.size(); ++i) { - // Quests with MISSION_NONE type don't have text for them and can't be displayed - if (quests[i].quest->questName == CQuest::missionName(0)) + // Quests without mision don't have text for them and can't be displayed + if (quests[i].quest->mission == Rewardable::Limiter{}) continue; if (quests[i].quest->isCompleted) From 9108f7e3e52f7f5f86bac9530928d10377a13f5e Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 13:40:28 +0200 Subject: [PATCH 0801/1248] Remove assert triggered while flying --- server/CGameHandler.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 273ea0514..88eeda6d0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1095,8 +1095,6 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo if (isInTheMap(guardPos)) guardian = getTile(guardPos)->visitableObjects.back(); - assert(guardian == nullptr || dynamic_cast(guardian) != nullptr); - const bool embarking = !h->boat && objectToVisit && objectToVisit->ID == Obj::BOAT; const bool disembarking = h->boat && t.terType->isLand() From 006def7690ca96f99488ab38555342e9f25b57b2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 12 Oct 2023 15:33:09 +0200 Subject: [PATCH 0802/1248] ui improvements --- mapeditor/mainwindow.cpp | 2 ++ mapeditor/mainwindow.ui | 35 +++++++++++++---------------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 1dcb6b544..0dd36b686 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -939,6 +939,8 @@ void MainWindow::preparePreview(const QModelIndex &index) scenePreview->addPixmap(objPreview); } } + + ui->objectPreview->fitInView(scenePreview->itemsBoundingRect(), Qt::KeepAspectRatio); } diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 01aae4a05..0cd4f9318 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -163,6 +163,9 @@ 214 + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + Minimap @@ -203,12 +206,6 @@ 192 - - - 192 - 192 - - Qt::ScrollBarAlwaysOff @@ -239,6 +236,9 @@ 524287 + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + Map Objects View @@ -435,11 +435,8 @@ 192 - - - 524287 - 192 - + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable Tools @@ -762,6 +759,9 @@ 0 + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable + Painting @@ -920,11 +920,8 @@ - - - 524287 - 150 - + + QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable Preview @@ -954,12 +951,6 @@ 128 - - - 128 - 128 - - From d85c5189ba1c018abf950fae2698f9c4d58d60b2 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 05:21:09 +0200 Subject: [PATCH 0803/1248] Add object lock and zoom functinoality --- mapeditor/icons/zoom_base.png | Bin 0 -> 1490 bytes mapeditor/icons/zoom_minus.png | Bin 0 -> 1509 bytes mapeditor/icons/zoom_plus.png | Bin 0 -> 1528 bytes mapeditor/icons/zoom_zero.png | Bin 0 -> 1520 bytes mapeditor/mainwindow.cpp | 65 ++++++++++++++++++++++++++++ mapeditor/mainwindow.h | 11 +++++ mapeditor/mainwindow.ui | 76 ++++++++++++++++++++++++++++++--- mapeditor/maphandler.cpp | 16 +++++-- mapeditor/maphandler.h | 4 +- mapeditor/mapview.cpp | 13 ++---- mapeditor/scenelayer.cpp | 42 ++++++++++++++---- mapeditor/scenelayer.h | 8 ++++ 12 files changed, 207 insertions(+), 28 deletions(-) create mode 100644 mapeditor/icons/zoom_base.png create mode 100644 mapeditor/icons/zoom_minus.png create mode 100644 mapeditor/icons/zoom_plus.png create mode 100644 mapeditor/icons/zoom_zero.png diff --git a/mapeditor/icons/zoom_base.png b/mapeditor/icons/zoom_base.png new file mode 100644 index 0000000000000000000000000000000000000000..62a5dae81eb77de786020186df676dda4b842908 GIT binary patch literal 1490 zcmV;@1ugoCP)BE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096103e_P00aO4 z0096103ZMW0056pK*<0A0Xs=VK~z`??Uq3jf-nq4i#KqkGcGufC-yunoY9p>zzNQ0 zLP|oD0xqgI5Axr?x_|l&S(|hwA%_WE%K;FG(4o5)HH9#T|LkGO(fS?GFD8zw_ zMdmODh*u*6SZe*80U&74Tb}{U8I-O?y097m??Nds<*f!lViAZ{0SLl7{s37ArqTPz)iRSU$^GmNY?W`uA%-4v;tnt=S%q#2(@YlkWdrnOeN^100h4Tu{ppJ#4Z39 sg!&Rv(V9a(duZBE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096103e_P00aO4 z0096103ZMW0056pK*<0A0ZvIoK~z`?<(9z>!Y~X)17ZPAAc4eH~nLR(w1;O?~3660V$#3~6dxVnCR~8eTVrk{>-iejP=5tl0Wap`QoaO2t=a)3 zbcs!-5_C`if-gY~4p4&l3V;QnehI0l&qtm;w7&*Gh8hH5{xiM+4;*o!bZr4v00000 LNkvXXu0mjfxA49n literal 0 HcmV?d00001 diff --git a/mapeditor/icons/zoom_plus.png b/mapeditor/icons/zoom_plus.png new file mode 100644 index 0000000000000000000000000000000000000000..0d07f5456646c46c241ba20ea1066c8f0b49e657 GIT binary patch literal 1528 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 zB+>_>#+*D&01BXHU9Zykol*IJfRbeuAQobjectPreview->setScene(scenePreview); + initialScale = ui->mapView->viewport()->geometry(); + //loading objects loadObjectsTree(); @@ -296,6 +298,7 @@ void MainWindow::initializeMap(bool isNew) ui->mapView->setScene(controller.scene(mapLevel)); ui->minimapView->setScene(controller.miniScene(mapLevel)); ui->minimapView->dimensions(); + initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); //enable settings ui->actionMapSettings->setEnabled(true); @@ -1281,3 +1284,65 @@ void MainWindow::on_actionh3m_converter_triggered() } } + +void MainWindow::on_actionLock_triggered() +{ + if(controller.map()) + { + if(controller.scene(mapLevel)->selectionObjectsView.getSelection().empty()) + { + for(auto obj : controller.map()->objects) + { + controller.scene(mapLevel)->selectionObjectsView.setLockObject(obj, true); + controller.scene(mapLevel)->objectsView.setLockObject(obj, true); + } + } + else + { + for(auto * obj : controller.scene(mapLevel)->selectionObjectsView.getSelection()) + { + controller.scene(mapLevel)->selectionObjectsView.setLockObject(obj, true); + controller.scene(mapLevel)->objectsView.setLockObject(obj, true); + } + controller.scene(mapLevel)->selectionObjectsView.clear(); + } + controller.scene(mapLevel)->objectsView.update(); + controller.scene(mapLevel)->selectionObjectsView.update(); + } +} + + +void MainWindow::on_actionUnlock_triggered() +{ + if(controller.map()) + { + controller.scene(mapLevel)->selectionObjectsView.unlockAll(); + controller.scene(mapLevel)->objectsView.unlockAll(); + } + controller.scene(mapLevel)->objectsView.update(); +} + + +void MainWindow::on_actionZoom_in_triggered() +{ + auto rect = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); + rect -= QMargins{32 + 1, 32 + 1, 32 + 2, 32 + 2}; //compensate bounding box + ui->mapView->fitInView(rect, Qt::KeepAspectRatioByExpanding); +} + + +void MainWindow::on_actionZoom_out_triggered() +{ + auto rect = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); + rect += QMargins{32 - 1, 32 - 1, 32 - 2, 32 - 2}; //compensate bounding box + ui->mapView->fitInView(rect, Qt::KeepAspectRatioByExpanding); +} + + +void MainWindow::on_actionZoom_reset_triggered() +{ + auto center = ui->mapView->mapToScene(ui->mapView->viewport()->geometry().center()); + ui->mapView->fitInView(initialScale, Qt::KeepAspectRatioByExpanding); + ui->mapView->centerOn(center); +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 77c2c14f3..3d127b857 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -124,6 +124,16 @@ private slots: void on_actionh3m_converter_triggered(); + void on_actionLock_triggered(); + + void on_actionUnlock_triggered(); + + void on_actionZoom_in_triggered(); + + void on_actionZoom_out_triggered(); + + void on_actionZoom_reset_triggered(); + public slots: void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected); @@ -166,6 +176,7 @@ private: QStandardItemModel objectsModel; int mapLevel = 0; + QRectF initialScale; std::set catalog; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 0cd4f9318..b1942db80 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -86,6 +86,8 @@ + + @@ -94,6 +96,10 @@ + + + + @@ -143,6 +149,13 @@ + + + + + + + @@ -804,8 +817,8 @@ 0 0 - 128 - 192 + 256 + 90 @@ -847,8 +860,8 @@ 0 0 - 128 - 192 + 256 + 90 @@ -883,8 +896,8 @@ 0 0 - 128 - 192 + 256 + 90 @@ -1332,6 +1345,57 @@ h3m converter + + + + icons:lock-closed.pngicons:lock-closed.png + + + Lock + + + Lock objects on map to avoid unnecessary changes + + + + + + icons:lock-open.pngicons:lock-open.png + + + Unlock + + + Unlock all objects on the map + + + + + + icons:zoom_plus.pngicons:zoom_plus.png + + + Zoom in + + + + + + icons:zoom_minus.pngicons:zoom_minus.png + + + Zoom out + + + + + + icons:zoom_zero.pngicons:zoom_zero.png + + + Zoom reset + + diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 891d83901..922e38345 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -322,8 +322,11 @@ std::vector & MapHandler::getObjects(int x, int y, int z) return ttiles[index(x, y, z)]; } -void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) +void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked) { + painter.setRenderHint(QPainter::Antialiasing, false); + painter.setRenderHint(QPainter::SmoothPixmapTransform, false); + for(auto & object : getObjects(x, y, z)) { const CGObjectInstance * obj = object.obj; @@ -343,8 +346,15 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z) { auto pos = obj->getPosition(); - painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect); - + painter.drawImage(QPoint(x * tileSize, y * tileSize), *objData.objBitmap, object.rect, Qt::AutoColor | Qt::NoOpaqueDetection); + + if(locked.count(obj)) + { + painter.setCompositionMode(QPainter::CompositionMode_DestinationIn); + painter.fillRect(x * tileSize, y * tileSize, object.rect.width(), object.rect.height(), Qt::Dense4Pattern); + painter.setCompositionMode(QPainter::CompositionMode_SourceOver); + } + if(objData.flagBitmap) { if(x == pos.x && y == pos.y) diff --git a/mapeditor/maphandler.h b/mapeditor/maphandler.h index 2372f29f1..27b0518e8 100644 --- a/mapeditor/maphandler.h +++ b/mapeditor/maphandler.h @@ -88,6 +88,8 @@ private: void initObjectRects(); void initTerrainGraphics(); QRgb getTileColor(int x, int y, int z); + + QPolygon lockBitMask; public: MapHandler(); @@ -110,7 +112,7 @@ public: std::vector getTilesUnderObject(CGObjectInstance *) const; /// draws all objects on current tile (higher-level logic, unlike other draw*** methods) - void drawObjects(QPainter & painter, int x, int y, int z); + void drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked); void drawObject(QPainter & painter, const TileObject & object); void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y); std::vector & getObjects(int x, int y, int z); diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 3f0fa299b..5013d206c 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -44,15 +44,9 @@ void MinimapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(!sc) return; - int w = sc->viewport.viewportWidth(); - int h = sc->viewport.viewportHeight(); auto pos = mapToScene(mouseEvent->pos()); - pos.setX(pos.x() - w / 2); - pos.setY(pos.y() - h / 2); - - QPointF point = pos * 32; - - emit cameraPositionChanged(point); + pos *= 32; + emit cameraPositionChanged(pos); } void MinimapView::mousePressEvent(QMouseEvent* event) @@ -68,8 +62,7 @@ MapView::MapView(QWidget * parent): void MapView::cameraChanged(const QPointF & pos) { - horizontalScrollBar()->setValue(pos.x()); - verticalScrollBar()->setValue(pos.y()); + centerOn(pos); } void MapView::setController(MapController * ctrl) diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 6e05bfe09..6d49c3f25 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -377,8 +377,7 @@ void ObjectsLayer::draw(bool onlyDirty) return; QPainter painter(pixmap.get()); - std::set drawen; - + if(onlyDirty) { //objects could be modified @@ -392,7 +391,7 @@ void ObjectsLayer::draw(bool onlyDirty) painter.setCompositionMode(QPainter::CompositionMode_SourceOver); for(auto & p : dirty) - handler->drawObjects(painter, p.x, p.y, p.z); + handler->drawObjects(painter, p.x, p.y, p.z, lockedObjects); } else { @@ -401,7 +400,7 @@ void ObjectsLayer::draw(bool onlyDirty) { for(int i = 0; i < map->width; ++i) { - handler->drawObjects(painter, i, j, scene->level); + handler->drawObjects(painter, i, j, scene->level, lockedObjects); } } } @@ -430,6 +429,19 @@ void ObjectsLayer::setDirty(const CGObjectInstance * object) } } +void ObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) +{ + if(lock) + lockedObjects.insert(object); + else + lockedObjects.erase(object); +} + +void ObjectsLayer::unlockAll() +{ + lockedObjects.clear(); +} + SelectionObjectsLayer::SelectionObjectsLayer(MapSceneBase * s): AbstractLayer(s), newObject(nullptr) { } @@ -501,7 +513,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO //visitable is most important for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->visitableAt(x, y)) @@ -513,7 +525,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO //if not visitable tile - try to get blocked for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->blockingAt(x, y)) @@ -525,7 +537,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO //finally, we can take any object for(auto & object : objects) { - if(!object.obj || object.obj == ignore) + if(!object.obj || object.obj == ignore || lockedObjects.count(object.obj)) continue; if(object.obj->coveringAt(x, y)) @@ -555,7 +567,8 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) if(map->isInTheMap(int3(i, j, scene->level))) { for(auto & o : handler->getObjects(i, j, scene->level)) - selectObject(o.obj, false); //do not inform about each object added + if(!lockedObjects.count(o.obj)) + selectObject(o.obj, false); //do not inform about each object added } } } @@ -599,6 +612,19 @@ void SelectionObjectsLayer::onSelection() emit selectionMade(!selectedObjects.empty()); } +void SelectionObjectsLayer::setLockObject(const CGObjectInstance * object, bool lock) +{ + if(lock) + lockedObjects.insert(object); + else + lockedObjects.erase(object); +} + +void SelectionObjectsLayer::unlockAll() +{ + lockedObjects.clear(); +} + MinimapLayer::MinimapLayer(MapSceneBase * s): AbstractLayer(s) { diff --git a/mapeditor/scenelayer.h b/mapeditor/scenelayer.h index 2b0a127fd..92add8ede 100644 --- a/mapeditor/scenelayer.h +++ b/mapeditor/scenelayer.h @@ -120,9 +120,13 @@ public: void setDirty(int x, int y); void setDirty(const CGObjectInstance * object); + + void setLockObject(const CGObjectInstance * object, bool lock); + void unlockAll(); private: std::set objDirty; + std::set lockedObjects; std::set dirty; }; @@ -180,6 +184,9 @@ public: bool isSelected(const CGObjectInstance *) const; std::set getSelection() const; void clear(); + + void setLockObject(const CGObjectInstance * object, bool lock); + void unlockAll(); QPoint shift; CGObjectInstance * newObject; @@ -191,6 +198,7 @@ signals: private: std::set selectedObjects; + std::set lockedObjects; void onSelection(); }; From 051886c98ecd7a0a05d17f2e2a644fbde795efad Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 11:13:57 +0200 Subject: [PATCH 0804/1248] Add shurtcuts --- mapeditor/mainwindow.ui | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index b1942db80..518464262 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -1356,6 +1356,9 @@ Lock objects on map to avoid unnecessary changes + + Ctrl+L + @@ -1368,6 +1371,9 @@ Unlock all objects on the map + + Ctrl+Shift+L + @@ -1377,6 +1383,9 @@ Zoom in + + Ctrl+= + @@ -1386,6 +1395,9 @@ Zoom out + + Ctrl+- + @@ -1395,6 +1407,9 @@ Zoom reset + + Ctrl+Shift+= + From 2bf8cdc9f525099348439c700af0466955f01b30 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 12:52:45 +0200 Subject: [PATCH 0805/1248] Hota-related bugs were fixed --- lib/mapObjects/CQuest.cpp | 16 ++++++++++------ lib/mapping/MapFormatH3M.cpp | 1 + lib/rewardable/Limiter.cpp | 2 +- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 51d0d1ead..29f18e9da 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -60,7 +60,7 @@ static std::string visitedTxt(const bool visited) const std::string & CQuest::missionName(int mission) { - static const std::array names = { + static const std::array names = { "empty", "heroLevel", "primarySkill", @@ -71,7 +71,9 @@ const std::string & CQuest::missionName(int mission) "bringResources", "bringHero", "bringPlayer", - "keymaster" + "keymaster", + "hota", + "other" }; if(static_cast(mission) < names.size()) @@ -299,6 +301,7 @@ void CQuest::defineQuestName() { //standard quests questName = CQuest::missionName(0); + if(mission != Rewardable::Limiter{}) questName = CQuest::missionName(12); if(mission.heroLevel > 0) questName = CQuest::missionName(1); for(auto & s : mission.primary) if(s) questName = CQuest::missionName(2); if(!mission.spells.empty()) questName = CQuest::missionName(2); @@ -310,6 +313,7 @@ void CQuest::defineQuestName() if(mission.resources.nonZero()) questName = CQuest::missionName(7); if(!mission.heroes.empty()) questName = CQuest::missionName(8); if(!mission.players.empty()) questName = CQuest::missionName(9); + if(mission.daysPassed > 0 || !mission.heroClasses.empty()) questName = CQuest::missionName(11); } void CQuest::addKillTargetReplacements(MetaString &out) const @@ -425,10 +429,6 @@ void CGSeerHut::setObjToKill() quest->heroName = getHeroToKill(false)->getNameTranslated(); quest->heroPortrait = getHeroToKill(false)->getPortraitSource(); } - - quest->getCompletionText(configuration.onSelect); - for(auto & i : configuration.info) - quest->getCompletionText(i.message); } void CGSeerHut::init(CRandomGenerator & rand) @@ -470,6 +470,10 @@ void CGSeerHut::initObj(CRandomGenerator & rand) if(!quest->isCustomComplete) quest->completedText.appendTextID(TextIdentifier("core", "seerhut", "quest", quest-> questName, quest->missionState(2), quest->textOption).get()); } + + quest->getCompletionText(configuration.onSelect); + for(auto & i : configuration.info) + quest->getCompletionText(i.message); } void CGSeerHut::getRolloverText(MetaString &text, bool onHover) const diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 112511213..f1b15605f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1830,6 +1830,7 @@ CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const Objec if(features.levelHOTA3) { uint32_t repeateableQuestsCount = reader->readUInt32(); + hut->quest->repeatedQuest = repeateableQuestsCount != 0; if(repeateableQuestsCount != 0) logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b23c50610..b5c5f9384 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -72,7 +72,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(daysPassed != 0) { - if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed) + if (IObjectInterface::cb->getDate(Date::DAY) <= daysPassed) return false; } From 9e508fe2c2fd126058303484bcb2a8b585cf3a3a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 00:21:58 +0200 Subject: [PATCH 0806/1248] interface improvements --- client/widgets/MiscWidgets.cpp | 82 +++++++++++++++++++++++----- client/widgets/MiscWidgets.h | 18 +++++- client/windows/CCastleInterface.cpp | 15 +++++ client/windows/CCastleInterface.h | 5 +- client/windows/CKingdomInterface.cpp | 28 ++++++++++ client/windows/CKingdomInterface.h | 4 ++ 6 files changed, 136 insertions(+), 16 deletions(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 7b22e5274..0a3b79ea3 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -18,6 +18,8 @@ #include "../CPlayerInterface.h" #include "../CGameInfo.h" #include "../PlayerLocalState.h" +#include "../gui/WindowHandler.h" +#include "../windows/CTradeWindow.h" #include "../widgets/TextControls.h" #include "../widgets/CGarrisonInt.h" #include "../windows/CCastleInterface.h" @@ -175,6 +177,24 @@ LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownI { } +void LRClickableArea::clickPressed(const Point & cursorPosition) +{ + if(onClick) + onClick(); +} + +void LRClickableArea::showPopupWindow(const Point & cursorPosition) +{ + if(onPopup) + onPopup(); +} + +LRClickableArea::LRClickableArea(const Rect & Pos, std::function onClick, std::function onPopup) + : CIntObject(LCLICK | SHOW_POPUP), onClick(onClick), onPopup(onPopup) +{ + pos = Pos + pos.topLeft(); +} + void CMinorResDataBar::show(Canvas & to) { } @@ -401,50 +421,84 @@ CTownTooltip::CTownTooltip(Point pos, const CGTownInstance * town) CInteractableTownTooltip::CInteractableTownTooltip(Point pos, const CGTownInstance * town) { - init(InfoAboutTown(town, true)); + init(town); OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); garrison = std::make_shared(pos + Point(0, 73), 4, Point(0, 0), town->getUpperArmy(), nullptr, true, true, CGarrisonInt::ESlotsLayout::REVERSED_TWO_ROWS); } -void CInteractableTownTooltip::init(const InfoAboutTown & town) +void CInteractableTownTooltip::init(const CGTownInstance * town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + const InfoAboutTown townInfo = InfoAboutTown(town, true); + int townId = town->id; + //order of icons in def: fort, citadel, castle, no fort - size_t fortIndex = town.fortLevel ? town.fortLevel - 1 : 3; + size_t fortIndex = townInfo.fortLevel ? townInfo.fortLevel - 1 : 3; fort = std::make_shared(AnimationPath::builtin("ITMCLS"), fortIndex, 0, 105, 31); + fastArmyPurchase = std::make_shared(Rect(105, 31, 34, 34), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId) + std::make_shared(town)->enterToTheQuickRecruitmentWindow(); + } + }); + fastMarket = std::make_shared(Rect(143, 31, 30, 34), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); - assert(town.tType); + assert(townInfo.tType); - size_t iconIndex = town.tType->clientInfo.icons[town.fortLevel > 0][town.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + size_t iconIndex = townInfo.tType->clientInfo.icons[townInfo.fortLevel > 0][townInfo.built >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; build = std::make_shared(AnimationPath::builtin("itpt"), iconIndex, 0, 3, 2); - title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, town.name); + title = std::make_shared(66, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, townInfo.name); - if(town.details) + if(townInfo.details) { - hall = std::make_shared(AnimationPath::builtin("ITMTLS"), town.details->hallLevel, 0, 67, 31); + hall = std::make_shared(AnimationPath::builtin("ITMTLS"), townInfo.details->hallLevel, 0, 67, 31); + fastTownHall = std::make_shared(Rect(67, 31, 34, 34), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId) + std::make_shared(town)->enterTownHall(); + } + }); - if(town.details->goldIncome) + if(townInfo.details->goldIncome) { income = std::make_shared(157, 58, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, - std::to_string(town.details->goldIncome)); + std::to_string(townInfo.details->goldIncome)); } - if(town.details->garrisonedHero) //garrisoned hero icon + if(townInfo.details->garrisonedHero) //garrisoned hero icon garrisonedHero = std::make_shared(ImagePath::builtin("TOWNQKGH"), 149, 76); - if(town.details->customRes)//silo is built + if(townInfo.details->customRes)//silo is built { - if(town.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore + if(townInfo.tType->primaryRes == EGameResID::WOOD_AND_ORE )// wood & ore { res1 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::WOOD), 0, 7, 75); res2 = std::make_shared(AnimationPath::builtin("SMALRES"), GameResID(EGameResID::ORE), 0, 7, 88); } else { - res1 = std::make_shared(AnimationPath::builtin("SMALRES"), town.tType->primaryRes, 0, 7, 81); + res1 = std::make_shared(AnimationPath::builtin("SMALRES"), townInfo.tType->primaryRes, 0, 7, 81); } } } diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 53b866207..3e277280c 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -31,6 +31,7 @@ class CGarrisonInt; class CCreatureAnim; class CComponent; class CAnimImage; +class LRClickableArea; /// Shows a text by moving the mouse cursor over the object class CHoverableArea: public virtual CIntObject @@ -133,8 +134,12 @@ class CInteractableTownTooltip : public CIntObject std::shared_ptr res1; std::shared_ptr res2; std::shared_ptr garrison; + + std::shared_ptr fastMarket; + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; - void init(const InfoAboutTown & town); + void init(const CGTownInstance * town); public: CInteractableTownTooltip(Point pos, const CGTownInstance * town); }; @@ -215,6 +220,17 @@ public: LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town); }; +/// Can do action on click +class LRClickableArea: public CIntObject +{ + std::function onClick; + std::function onPopup; +public: + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + LRClickableArea(const Rect & Pos, std::function onClick = nullptr, std::function onPopup = nullptr); +}; + class MoraleLuckBox : public LRClickableAreaWTextComp { std::shared_ptr image; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 36b5a5207..82784f6da 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1294,6 +1294,21 @@ void CCastleInterface::recreateIcons() fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); fastArmyPurchase->setAnimateLonelyFrame(true); + fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + if (town->getOwner() == LOCPLINT->playerID) + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); + } + }); + + fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() + { + if(town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + }); + creainfo.clear(); bool compactCreatureInfo = useCompactCreatureBox(); diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index a5b7580ea..9e9e0b48d 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -36,6 +36,7 @@ class CTownList; class CGarrisonInt; class CComponent; class CComponentBox; +class LRClickableArea; /// Building "button" class CBuildingRect : public CShowableAnim @@ -154,7 +155,7 @@ class CCastleBuildings : public CIntObject void enterCastleGate(); void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains void enterMagesGuild(); - + void openMagesGuild(); void openTownHall(); @@ -228,6 +229,8 @@ class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr split; std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; std::vector> creainfo;//small icons of creatures (bottom-left corner); diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index a1d1904c3..13e3fcdd9 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -19,12 +19,14 @@ #include "../adventureMap/CResDataBar.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" #include "../widgets/CComponent.h" #include "../widgets/CGarrisonInt.h" #include "../widgets/TextControls.h" #include "../widgets/MiscWidgets.h" #include "../widgets/Buttons.h" #include "../widgets/ObjectLists.h" +#include "../windows/CTradeWindow.h" #include "../../CCallback.h" @@ -471,6 +473,8 @@ CKingdomInterface::CKingdomInterface() statusbar = CGStatusBar::create(std::make_shared(ImagePath::builtin("KSTATBAR"), 10,pos.h - 45)); resdatabar = std::make_shared(ImagePath::builtin("KRESBAR"), 7, 111+footerPos, 29, 5, 76, 81); + + activateTab(persistentStorage["gui"]["lastKindomInterface"].Integer()); } void CKingdomInterface::generateObjectsList(const std::vector &ownedObjects) @@ -595,6 +599,20 @@ void CKingdomInterface::generateMinesList(const std::vectorpos = Rect(pos.x+580, pos.y+31+footerPos, 136, 68); incomeArea->hoverText = CGI->generaltexth->allTexts[255]; incomeAmount = std::make_shared(628, footerPos + 70, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, std::to_string(totalIncome)); + + fastMarket = std::make_shared(Rect(20, 31 + footerPos, 716, 68), []() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); } void CKingdomInterface::generateButtons() @@ -628,6 +646,9 @@ void CKingdomInterface::generateButtons() void CKingdomInterface::activateTab(size_t which) { + Settings s = persistentStorage.write["gui"]["lastKindomInterface"]; + s->Integer() = which; + btnHeroes->block(which == 0); btnTowns->block(which == 1); tabArea->setActive(which); @@ -772,6 +793,13 @@ CTownItem::CTownItem(const CGTownInstance * Town) hall = std::make_shared( 69, 31, town, true); fort = std::make_shared(111, 31, town, false); + fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setAnimateLonelyFrame(true); + fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); + fastArmyPurchase->setAnimateLonelyFrame(true); + garr = std::make_shared(Point(313, 3), 4, Point(232,0), town->getUpperArmy(), town->visitingHero, true, true, CGarrisonInt::ESlotsLayout::TWO_ROWS); heroes = std::make_shared(town, Point(244,6), Point(475,6), garr, false); diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 52a5623ed..8cb5fe040 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -233,6 +233,7 @@ private: std::shared_ptr incomeArea; std::shared_ptr incomeAmount; + std::shared_ptr fastMarket; std::shared_ptr statusbar; std::shared_ptr resdatabar; @@ -277,6 +278,9 @@ class CTownItem : public CIntObject, public IGarrisonHolder std::shared_ptr openTown; + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; + public: const CGTownInstance * town; From 3c117da4eb99883b069524b0c1799747f8f19e8c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 01:38:33 +0200 Subject: [PATCH 0807/1248] fix tavern --- client/windows/CKingdomInterface.cpp | 46 +++++++++++++++------------- client/windows/CKingdomInterface.h | 3 +- client/windows/GUIClasses.cpp | 4 ++- 3 files changed, 30 insertions(+), 23 deletions(-) diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 13e3fcdd9..390ba1cb5 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -599,20 +599,6 @@ void CKingdomInterface::generateMinesList(const std::vectorpos = Rect(pos.x+580, pos.y+31+footerPos, 136, 68); incomeArea->hoverText = CGI->generaltexth->allTexts[255]; incomeAmount = std::make_shared(628, footerPos + 70, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, std::to_string(totalIncome)); - - fastMarket = std::make_shared(Rect(20, 31 + footerPos, 716, 68), []() - { - std::vector towns = LOCPLINT->cb->getTownsInfo(true); - for(auto & town : towns) - { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) - { - GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); - return; - } - } - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); - }); } void CKingdomInterface::generateButtons() @@ -793,13 +779,6 @@ CTownItem::CTownItem(const CGTownInstance * Town) hall = std::make_shared( 69, 31, town, true); fort = std::make_shared(111, 31, town, false); - fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); - fastTownHall->setAnimateLonelyFrame(true); - fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); - fastArmyPurchase->setAnimateLonelyFrame(true); - garr = std::make_shared(Point(313, 3), 4, Point(232,0), town->getUpperArmy(), town->visitingHero, true, true, CGarrisonInt::ESlotsLayout::TWO_ROWS); heroes = std::make_shared(town, Point(244,6), Point(475,6), garr, false); @@ -813,6 +792,31 @@ CTownItem::CTownItem(const CGTownInstance * Town) growth.push_back(std::make_shared(Point(401+37*(int)i, 78), town, (int)i, true, true)); available.push_back(std::make_shared(Point(48+37*(int)i, 78), town, (int)i, true, false)); } + + fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setAnimateLonelyFrame(true); + fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); + fastArmyPurchase->setAnimateLonelyFrame(true); + fastTavern = std::make_shared(Rect(5, 6, 58, 64), [&]() + { + if(town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + }); + fastMarket = std::make_shared(Rect(153, 6, 65, 64), []() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } + } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); + }); } void CTownItem::updateGarrisons() diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 8cb5fe040..a9a7d7cc4 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -233,7 +233,6 @@ private: std::shared_ptr incomeArea; std::shared_ptr incomeAmount; - std::shared_ptr fastMarket; std::shared_ptr statusbar; std::shared_ptr resdatabar; @@ -280,6 +279,8 @@ class CTownItem : public CIntObject, public IGarrisonHolder std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; public: const CGTownInstance * town; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 54d94bc60..403a0c406 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -489,7 +489,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); recruit->block(true); } - else if(LOCPLINT->castleInt && LOCPLINT->castleInt->town->visitingHero) + else if((LOCPLINT->castleInt && LOCPLINT->castleInt->town->visitingHero) || (dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero)) { recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. recruit->block(true); @@ -501,6 +501,8 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func } if(LOCPLINT->castleInt) CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + else if(dynamic_cast(TavernObj)) + CCS->videoh->open(dynamic_cast(TavernObj)->town->clientInfo.tavernVideo); else CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); } From 08832eeeb4389a0a1a5d5bf80c37dc28b5e8df1e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 01:49:02 +0200 Subject: [PATCH 0808/1248] fix --- client/widgets/MiscWidgets.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0a3b79ea3..0e6f7d68e 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -447,7 +447,7 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) std::make_shared(town)->enterToTheQuickRecruitmentWindow(); } }); - fastMarket = std::make_shared(Rect(143, 31, 30, 34), [townId]() + fastMarket = std::make_shared(Rect(143, 31, 30, 34), []() { std::vector towns = LOCPLINT->cb->getTownsInfo(true); for(auto & town : towns) From 5a84e7cd7f9d948439761f3d32660152d182c9e3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 11:36:18 +0200 Subject: [PATCH 0809/1248] Update client/windows/GUIClasses.cpp Co-authored-by: Nordsoft91 --- client/windows/GUIClasses.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 403a0c406..0271b2a7f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -501,8 +501,8 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func } if(LOCPLINT->castleInt) CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); - else if(dynamic_cast(TavernObj)) - CCS->videoh->open(dynamic_cast(TavernObj)->town->clientInfo.tavernVideo); + else if(const auto * townObj = dynamic_cast(TavernObj)) + CCS->videoh->open(townObj->town->clientInfo.tavernVideo); else CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); } From 55900ceb6673507582a1f7d8e0fd860cc8d0d764 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 20:15:11 +0200 Subject: [PATCH 0810/1248] Add portraits delegate --- lib/mapObjects/CGHeroInstance.cpp | 5 +- mapeditor/Animation.cpp | 5 +- mapeditor/CMakeLists.txt | 3 + mapeditor/graphics.h | 1 + mapeditor/inspector/inspector.cpp | 3 +- mapeditor/inspector/portraitwidget.cpp | 135 +++++++++++++++++++++++++ mapeditor/inspector/portraitwidget.h | 64 ++++++++++++ mapeditor/inspector/portraitwidget.ui | 96 ++++++++++++++++++ 8 files changed, 306 insertions(+), 6 deletions(-) create mode 100644 mapeditor/inspector/portraitwidget.cpp create mode 100644 mapeditor/inspector/portraitwidget.h create mode 100644 mapeditor/inspector/portraitwidget.ui diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index d4cbc8686..8941b9273 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1066,9 +1066,8 @@ std::string CGHeroInstance::getBiographyTextID() const return biographyCustomTextId; if (type) return type->getBiographyTextID(); - - assert(0); - return ""; + + return ""; //for random hero } CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) diff --git a/mapeditor/Animation.cpp b/mapeditor/Animation.cpp index 7bced089f..84e6a2be0 100644 --- a/mapeditor/Animation.cpp +++ b/mapeditor/Animation.cpp @@ -14,6 +14,7 @@ #include "Animation.h" #include "BitmapHandler.h" +#include "graphics.h" #include "../lib/vcmi_endian.h" #include "../lib/filesystem/Filesystem.h" @@ -585,8 +586,8 @@ void Animation::init() JsonPath resID = JsonPath::builtin("SPRITES/" + name); - //if(vstd::contains(graphics->imageLists, resID.getName())) - //initFromJson(graphics->imageLists[resID.getName()]); + if(vstd::contains(graphics->imageLists, resID.getName())) + initFromJson(graphics->imageLists[resID.getName()]); auto configList = CResourceHandler::get()->getResourcesWithName(resID); diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 8cdf87e09..14c409be9 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -35,6 +35,7 @@ set(editor_SRCS inspector/questwidget.cpp inspector/heroskillswidget.cpp inspector/PickObjectDelegate.cpp + inspector/portraitwidget.cpp resourceExtractor/ResourceConverter.cpp ) @@ -74,6 +75,7 @@ set(editor_HEADERS inspector/questwidget.h inspector/heroskillswidget.h inspector/PickObjectDelegate.h + inspector/portraitwidget.h resourceExtractor/ResourceConverter.h ) @@ -99,6 +101,7 @@ set(editor_FORMS inspector/rewardswidget.ui inspector/questwidget.ui inspector/heroskillswidget.ui + inspector/portraitwidget.ui ) set(editor_TS diff --git a/mapeditor/graphics.h b/mapeditor/graphics.h index bd04c2246..009cf4399 100644 --- a/mapeditor/graphics.h +++ b/mapeditor/graphics.h @@ -11,6 +11,7 @@ //code is copied from vcmiclient/Graphics.h with minimal changes #include "../lib/GameConstants.h" +#include "../lib/filesystem/ResourcePath.h" #include VCMI_LIB_NAMESPACE_BEGIN diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index d6fa5183b..0e3145dba 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -26,6 +26,7 @@ #include "rewardswidget.h" #include "questwidget.h" #include "heroskillswidget.h" +#include "portraitwidget.h" #include "PickObjectDelegate.h" #include "../mapcontroller.h" @@ -286,7 +287,7 @@ void Inspector::updateProperties(CGHeroInstance * o) } addProperty("Name", o->getNameTranslated(), false); addProperty("Biography", o->getBiographyTranslated(), new MessageDelegate, false); - addProperty("Portrait", o->customPortraitSource, false); + addProperty("Portrait", PropertyEditorPlaceholder(), new PortraitDelegate(*o), false); auto * delegate = new HeroSkillsDelegate(*o); addProperty("Skills", PropertyEditorPlaceholder(), delegate, false); diff --git a/mapeditor/inspector/portraitwidget.cpp b/mapeditor/inspector/portraitwidget.cpp new file mode 100644 index 000000000..3882ddf62 --- /dev/null +++ b/mapeditor/inspector/portraitwidget.cpp @@ -0,0 +1,135 @@ +/* + * portraitwidget.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 "portraitwidget.h" +#include "ui_portraitwidget.h" +#include "../../lib/CHeroHandler.h" +#include "../Animation.h" + +PortraitWidget::PortraitWidget(CGHeroInstance & h, QWidget *parent): + QDialog(parent), + ui(new Ui::PortraitWidget), + hero(h), + portraitIndex(0) +{ + ui->setupUi(this); + ui->portraitView->setScene(&scene); + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); +} + +PortraitWidget::~PortraitWidget() +{ + delete ui; +} + +void PortraitWidget::obtainData() +{ + portraitIndex = VLC->heroh->getById(hero.getPortraitSource())->getIndex(); + if(hero.customPortraitSource.isValid()) + { + ui->isDefault->setChecked(true); + } + + drawPortrait(); +} + +void PortraitWidget::commitChanges() +{ + if(portraitIndex == VLC->heroh->getById(HeroTypeID(hero.subID))->getIndex()) + hero.customPortraitSource = HeroTypeID::NONE; + else + hero.customPortraitSource = VLC->heroh->getByIndex(portraitIndex)->getId(); +} + +void PortraitWidget::drawPortrait() +{ + static Animation portraitAnimation(AnimationPath::builtin("PortraitsLarge").getOriginalName()); + portraitAnimation.preload(); + auto icon = VLC->heroTypes()->getByIndex(portraitIndex)->getIconIndex(); + pixmap = QPixmap::fromImage(*portraitAnimation.getImage(icon)); + scene.addPixmap(pixmap); + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); +} + +void PortraitWidget::resizeEvent(QResizeEvent *) +{ + ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); +} + +void PortraitWidget::on_isDefault_toggled(bool checked) +{ + if(checked) + { + ui->buttonNext->setEnabled(false); + ui->buttonPrev->setEnabled(false); + portraitIndex = VLC->heroh->getById(HeroTypeID(hero.subID))->getIndex(); + } + else + { + ui->buttonNext->setEnabled(true); + ui->buttonPrev->setEnabled(true); + } + drawPortrait(); +} + + +void PortraitWidget::on_buttonNext_clicked() +{ + if(portraitIndex < VLC->heroh->size() - 1) + ++portraitIndex; + else + portraitIndex = 0; + + drawPortrait(); +} + + +void PortraitWidget::on_buttonPrev_clicked() +{ + if(portraitIndex > 0) + --portraitIndex; + else + portraitIndex = VLC->heroh->size() - 1; + + drawPortrait(); +} + +PortraitDelegate::PortraitDelegate(CGHeroInstance & h): hero(h), QStyledItemDelegate() +{ +} + +QWidget * PortraitDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const +{ + return new PortraitWidget(hero, parent); +} + +void PortraitDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->obtainData(); + } + else + { + QStyledItemDelegate::setEditorData(editor, index); + } +} + +void PortraitDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + if(auto * ed = qobject_cast(editor)) + { + ed->commitChanges(); + } + else + { + QStyledItemDelegate::setModelData(editor, model, index); + } +} diff --git a/mapeditor/inspector/portraitwidget.h b/mapeditor/inspector/portraitwidget.h new file mode 100644 index 000000000..2ca79e801 --- /dev/null +++ b/mapeditor/inspector/portraitwidget.h @@ -0,0 +1,64 @@ +/* + * portraitwidget.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 +#include "../../lib/mapObjects/CGHeroInstance.h" + +namespace Ui { +class PortraitWidget; +} + +class PortraitWidget : public QDialog +{ + Q_OBJECT + +public: + explicit PortraitWidget(CGHeroInstance &, QWidget *parent = nullptr); + ~PortraitWidget(); + + void obtainData(); + void commitChanges(); + + void resizeEvent(QResizeEvent *) override; + +private slots: + void on_isDefault_toggled(bool checked); + + void on_buttonNext_clicked(); + + void on_buttonPrev_clicked(); + +private: + void drawPortrait(); + + Ui::PortraitWidget *ui; + QGraphicsScene scene; + QPixmap pixmap; + + CGHeroInstance & hero; + int portraitIndex; +}; + +class PortraitDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + using QStyledItemDelegate::QStyledItemDelegate; + + PortraitDelegate(CGHeroInstance &); + + QWidget * createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override; + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + +private: + CGHeroInstance & hero; +}; diff --git a/mapeditor/inspector/portraitwidget.ui b/mapeditor/inspector/portraitwidget.ui new file mode 100644 index 000000000..aa0b923ff --- /dev/null +++ b/mapeditor/inspector/portraitwidget.ui @@ -0,0 +1,96 @@ + + + PortraitWidget + + + + 0 + 0 + 286 + 305 + + + + Portrait + + + true + + + + + + 0 + + + + + + 116 + 128 + + + + + + + + 0 + + + + + false + + + + 0 + 0 + + + + ... + + + Qt::UpArrow + + + + + + + false + + + + 0 + 0 + + + + ... + + + Qt::DownArrow + + + + + + + + + + + Default + + + true + + + + + + + + From 8058efb3bdd2025dbc7f955c846f7cb82c0015c4 Mon Sep 17 00:00:00 2001 From: Warzyw647 Date: Fri, 13 Oct 2023 20:43:15 +0200 Subject: [PATCH 0811/1248] Fixed morale and luck chance lookup. --- server/battles/BattleActionProcessor.cpp | 4 ++-- server/battles/BattleFlowProcessor.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 34cd08213..fc491160c 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -884,7 +884,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if(attackerLuck > 0) { auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, attackerLuck); + size_t diceIndex = std::min(diceSize.size(), attackerLuck) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) bat.flags |= BattleAttack::LUCKY; @@ -893,7 +893,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const if(attackerLuck < 0) { auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE); - size_t diceIndex = std::min(diceSize.size() - 1, -attackerLuck); + size_t diceIndex = std::min(diceSize.size(), -attackerLuck) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) bat.flags |= BattleAttack::UNLUCKY; diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 85f214df0..8be9c9f0f 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -340,7 +340,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat if(!next->hadMorale && !next->waited() && nextStackMorale < 0) { auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, -nextStackMorale); + size_t diceIndex = std::min(diceSize.size(), -nextStackMorale) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) { @@ -492,7 +492,7 @@ bool BattleFlowProcessor::rollGoodMorale(const CBattleInfoCallback & battle, con && nextStackMorale > 0) { auto diceSize = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE); - size_t diceIndex = std::min(diceSize.size()-1, nextStackMorale); + size_t diceIndex = std::min(diceSize.size(), nextStackMorale) - 1; // array index, so 0-indexed if(diceSize.size() > 0 && gameHandler->getRandomGenerator().nextInt(1, diceSize[diceIndex]) == 1) { From 0c6b1ca3c8939bba716e3a85f4ecf13138249924 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 23:04:35 +0200 Subject: [PATCH 0812/1248] possibility to change name --- client/CServerHandler.cpp | 8 ++++++++ client/CServerHandler.h | 2 ++ client/lobby/OptionsTab.cpp | 29 +++++++++++++++++++++++++++-- client/lobby/OptionsTab.h | 3 +++ client/widgets/TextControls.cpp | 5 +++-- client/widgets/TextControls.h | 2 +- lib/NetPackVisitor.h | 1 + lib/NetPacksLib.cpp | 5 +++++ lib/NetPacksLobby.h | 14 ++++++++++++++ lib/registerTypes/RegisterTypes.h | 1 + server/CVCMIServer.cpp | 13 +++++++++++++ server/CVCMIServer.h | 1 + server/LobbyNetPackVisitors.h | 1 + server/NetPacksLobbyServer.cpp | 6 ++++++ 14 files changed, 86 insertions(+), 5 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 86eb43af7..a47a089fa 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -499,6 +499,14 @@ void CServerHandler::setPlayer(PlayerColor color) const sendLobbyPack(lsp); } +void CServerHandler::setPlayerName(PlayerColor color, std::string name) const +{ + LobbySetPlayerName lspn; + lspn.color = color; + lspn.name = name; + sendLobbyPack(lspn); +} + void CServerHandler::setPlayerOption(ui8 what, int32_t value, PlayerColor player) const { LobbyChangePlayerOption lcpo; diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 83b458883..207605a78 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -67,6 +67,7 @@ public: virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; + virtual void setPlayerName(PlayerColor color, std::string name) const = 0; virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; @@ -152,6 +153,7 @@ public: void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; + void setPlayerName(PlayerColor color, std::string name) const override; void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index b7af0368c..654653f90 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1041,7 +1041,8 @@ void OptionsTab::SelectedBox::scrollBy(int distance) } OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parent) - : pi(std::make_unique(SEL->getPlayerInfo(S.color))) + : CIntObject(KEYBOARD | TEXTINPUT) + , pi(std::make_unique(SEL->getPlayerInfo(S.color))) , s(std::make_unique(S)) , parentTab(parent) { @@ -1081,7 +1082,13 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con }}; background = std::make_shared(ImagePath::builtin(bgs[s->color]), 0, 0); - labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); + if(!CSH->isMyColor(s->color) || CSH->isGuest()) + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); + else + { + labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false); + labelPlayerNameEdit->setText(s->name); + } labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); if(SEL->screenType == ESelectionScreen::newGame) @@ -1115,6 +1122,24 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con bonus = std::make_shared(Point(271, 2), *s, BONUS); } +bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) +{ + return labelPlayerNameEdit && labelPlayerNameEdit->hasFocus() && key == EShortcut::GLOBAL_ACCEPT; +} + +void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) +{ + if(labelPlayerNameEdit) + { + if(key == EShortcut::GLOBAL_ACCEPT) + { + CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); + Settings name = settings.write["general"]["playerName"]; + name->String() = labelPlayerNameEdit->getText(); + } + } +} + void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const { if(ps.isControlledByAI() || humanPlayers > 0) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 0f8943d43..5c81e6479 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -170,6 +170,7 @@ private: std::unique_ptr pi; std::unique_ptr s; std::shared_ptr labelPlayerName; + std::shared_ptr labelPlayerNameEdit; std::shared_ptr labelWhoCanPlay; std::shared_ptr background; std::shared_ptr buttonTownLeft; @@ -186,6 +187,8 @@ private: PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parentTab); void hideUnavailableButtons(); + bool captureThisKey(EShortcut key) override; + void keyPressed(EShortcut key) override; private: const OptionsTab & parentTab; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index d534f4002..aca634272 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -532,7 +532,7 @@ Point CGStatusBar::getBorderSize() return Point(); } -CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB) +CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput) : CLabel(Pos.x, Pos.y, font, ETextAlignment::CENTER), cb(CB), CFocusable(std::make_shared(this)) @@ -544,7 +544,8 @@ CTextInput::CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB); + CTextInput(const Rect & Pos, EFonts font, const CFunctionList & CB, bool giveFocusToInput = true); CTextInput(const Rect & Pos, const Point & bgOffset, const ImagePath & bgName, const CFunctionList & CB); CTextInput(const Rect & Pos, std::shared_ptr srf); diff --git a/lib/NetPackVisitor.h b/lib/NetPackVisitor.h index 676e64698..40e81ab03 100644 --- a/lib/NetPackVisitor.h +++ b/lib/NetPackVisitor.h @@ -159,6 +159,7 @@ public: virtual void visitLobbySetCampaignBonus(LobbySetCampaignBonus & pack) {} virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) {} virtual void visitLobbySetPlayer(LobbySetPlayer & pack) {} + virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) {} virtual void visitLobbySetSimturns(LobbySetSimturns & pack) {} virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) {} virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) {} diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 79d8ec0c9..c37000719 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -755,6 +755,11 @@ void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) visitor.visitLobbySetPlayer(*this); } +void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayerName(*this); +} + void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) { visitor.visitLobbySetSimturns(*this); diff --git a/lib/NetPacksLobby.h b/lib/NetPacksLobby.h index e75929077..2f772cb3b 100644 --- a/lib/NetPacksLobby.h +++ b/lib/NetPacksLobby.h @@ -250,6 +250,20 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer } }; +struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer +{ + PlayerColor color = PlayerColor::CANNOT_DETERMINE; + std::string name = ""; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler &h, const int version) + { + h & color; + h & name; + } +}; + struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer { SimturnsInfo simturnsInfo; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 28dcca012..cde5b7c44 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -399,6 +399,7 @@ void registerTypesLobbyPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); + s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index d155ead41..95d78d942 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -835,6 +835,19 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor) } } +void CVCMIServer::setPlayerName(PlayerColor color, std::string name) +{ + PlayerSettings & player = si->playerInfos[color]; + + if(player.isControlledByHuman()) + { + int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + + playerNames[nameID].name = name; + setPlayerConnectedId(player, nameID); + } +} + void CVCMIServer::optionNextCastle(PlayerColor player, int dir) { PlayerSettings & s = si->playerInfos[player]; diff --git a/server/CVCMIServer.h b/server/CVCMIServer.h index 8e26d2149..22db4b93c 100644 --- a/server/CVCMIServer.h +++ b/server/CVCMIServer.h @@ -107,6 +107,7 @@ public: // Work with LobbyInfo void setPlayer(PlayerColor clickedColor); + void setPlayerName(PlayerColor player, std::string name); void optionNextHero(PlayerColor player, int dir); //dir == -1 or +1 void optionSetHero(PlayerColor player, HeroTypeID id); HeroTypeID nextAllowedHero(PlayerColor player, HeroTypeID id, int direction); diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index fba117a5f..e55c7040f 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -86,6 +86,7 @@ public: virtual void visitLobbyChangeHost(LobbyChangeHost & pack) override; virtual void visitLobbyChangePlayerOption(LobbyChangePlayerOption & pack) override; virtual void visitLobbySetPlayer(LobbySetPlayer & pack) override; + virtual void visitLobbySetPlayerName(LobbySetPlayerName & pack) override; virtual void visitLobbySetTurnTime(LobbySetTurnTime & pack) override; virtual void visitLobbySetSimturns(LobbySetSimturns & pack) override; virtual void visitLobbySetDifficulty(LobbySetDifficulty & pack) override; diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 865c26a50..69c66fcd8 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -397,6 +397,12 @@ void ApplyOnServerNetPackVisitor::visitLobbySetPlayer(LobbySetPlayer & pack) result = true; } +void ApplyOnServerNetPackVisitor::visitLobbySetPlayerName(LobbySetPlayerName & pack) +{ + srv.setPlayerName(pack.color, pack.name); + result = true; +} + void ApplyOnServerNetPackVisitor::visitLobbySetSimturns(LobbySetSimturns & pack) { srv.si->simturnsInfo = pack.simturnsInfo; From 5f52e6b35e5bdc9ee8a0c425f9ce7482ca5e8b62 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 13 Oct 2023 23:35:37 +0200 Subject: [PATCH 0813/1248] Revert "Auxiliary commit to revert individual files from 2bf8cdc9f525099348439c700af0466955f01b30" This reverts commit d9952495391f7ac77dd32dcf33f487e146b3655b. --- lib/rewardable/Limiter.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b5c5f9384..b23c50610 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -72,7 +72,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(daysPassed != 0) { - if (IObjectInterface::cb->getDate(Date::DAY) <= daysPassed) + if (IObjectInterface::cb->getDate(Date::DAY) < daysPassed) return false; } From b89cdda718e035f20a2c0fc6ac55c7cf3e3671c7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 23:36:51 +0200 Subject: [PATCH 0814/1248] Tavern also in infobox & haptics --- client/widgets/MiscWidgets.cpp | 13 +++++++++++++ client/widgets/MiscWidgets.h | 1 + 2 files changed, 14 insertions(+) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0e6f7d68e..4d5eff1d5 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -19,6 +19,7 @@ #include "../CGameInfo.h" #include "../PlayerLocalState.h" #include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" #include "../windows/CTradeWindow.h" #include "../widgets/TextControls.h" #include "../widgets/CGarrisonInt.h" @@ -180,7 +181,10 @@ LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownI void LRClickableArea::clickPressed(const Point & cursorPosition) { if(onClick) + { onClick(); + GH.input().hapticFeedback(); + } } void LRClickableArea::showPopupWindow(const Point & cursorPosition) @@ -447,6 +451,15 @@ void CInteractableTownTooltip::init(const CGTownInstance * town) std::make_shared(town)->enterToTheQuickRecruitmentWindow(); } }); + fastTavern = std::make_shared(Rect(3, 2, 58, 64), [townId]() + { + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) + { + if(town->id == townId && town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + } + }); fastMarket = std::make_shared(Rect(143, 31, 30, 34), []() { std::vector towns = LOCPLINT->cb->getTownsInfo(true); diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 3e277280c..021193e9a 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -135,6 +135,7 @@ class CInteractableTownTooltip : public CIntObject std::shared_ptr res2; std::shared_ptr garrison; + std::shared_ptr fastTavern; std::shared_ptr fastMarket; std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; From 785c6507a665b4064236575a2225f8eec8534a47 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 13 Oct 2023 23:50:01 +0200 Subject: [PATCH 0815/1248] code review --- server/CVCMIServer.cpp | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 95d78d942..088e222b8 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -837,15 +837,21 @@ void CVCMIServer::setPlayer(PlayerColor clickedColor) void CVCMIServer::setPlayerName(PlayerColor color, std::string name) { - PlayerSettings & player = si->playerInfos[color]; + if(color == PlayerColor::CANNOT_DETERMINE) + return; - if(player.isControlledByHuman()) - { - int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + PlayerSettings & player = si->playerInfos.at(color); - playerNames[nameID].name = name; - setPlayerConnectedId(player, nameID); - } + if(!player.isControlledByHuman()) + return; + + if(!player.connectedPlayerIDs.size()) + return; + + int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID + + playerNames[nameID].name = name; + setPlayerConnectedId(player, nameID); } void CVCMIServer::optionNextCastle(PlayerColor player, int dir) From 4651893b48c3842090ac83d745fd650fbae406e4 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 14 Oct 2023 00:15:15 +0200 Subject: [PATCH 0816/1248] Fix hota quests --- lib/mapObjects/CQuest.cpp | 8 +++++++- lib/mapping/MapFormatH3M.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 29f18e9da..85e154128 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -261,6 +261,9 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com text.replaceRawString(loot.buildList()); } + + if(lastDay >= 0) + text.replaceNumber(lastDay - IObjectInterface::cb->getDate(Date::DAY)); } void CQuest::getVisitText(MetaString &iwText, std::vector &components, bool firstVisit, const CGHeroInstance * h) const @@ -273,6 +276,9 @@ void CQuest::getVisitText(MetaString &iwText, std::vector &components else if(failRequirements) iwText.appendRawString(nextVisitText.toString()); + if(lastDay >= 0) + iwText.appendTextID(TextIdentifier("core", "seerhut", "time", textOption).get()); + addTextReplacements(iwText, components); } @@ -283,7 +289,7 @@ void CQuest::getRolloverText(MetaString &ms, bool onHover) const std::string questState = missionState(onHover ? 3 : 4); - ms.appendRawString(VLC->generaltexth->translate("core.seerhut.quest", questName, questState, textOption)); + ms.appendTextID(TextIdentifier("core", "seerhut", "quest", questName, questState, textOption).get()); std::vector components; addTextReplacements(ms, components); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index f1b15605f..b5c2ded6e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2074,7 +2074,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) if(missionSubID == 1) { missionId = int(EQuestMission::HOTA_REACH_DATE); - guard->quest->mission.daysPassed = reader->readUInt32(); + guard->quest->mission.daysPassed = reader->readUInt32() + 1; break; } break; From 788147a897252a4fe84e9b5c9f997ccfd91e08a0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 14 Oct 2023 02:58:13 +0200 Subject: [PATCH 0817/1248] Add line and fill brushes --- mapeditor/icons/brush-0.png | Bin 0 -> 1449 bytes mapeditor/icons/brush-6.png | Bin 0 -> 1478 bytes mapeditor/icons/brush-7.png | Bin 0 -> 1376 bytes mapeditor/mainwindow.cpp | 174 +++++----- mapeditor/mainwindow.h | 28 +- mapeditor/mainwindow.ui | 630 ++++++++++++++++++++---------------- mapeditor/mapview.cpp | 108 +++++++ mapeditor/mapview.h | 4 +- 8 files changed, 556 insertions(+), 388 deletions(-) create mode 100644 mapeditor/icons/brush-0.png create mode 100644 mapeditor/icons/brush-6.png create mode 100644 mapeditor/icons/brush-7.png diff --git a/mapeditor/icons/brush-0.png b/mapeditor/icons/brush-0.png new file mode 100644 index 0000000000000000000000000000000000000000..2aa81a4e78fcd2a950c981dfb3d66cb66df05fe4 GIT binary patch literal 1449 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 literal 0 HcmV?d00001 diff --git a/mapeditor/icons/brush-7.png b/mapeditor/icons/brush-7.png new file mode 100644 index 0000000000000000000000000000000000000000..0b075bd338516763d2dbdeab09504c703292b5a5 GIT binary patch literal 1376 zcmV-m1)utfP)BE1ZQLxAY_vg;$?V;oX(lt{{G@3qJ`_O^!KmPq;Qw2}Cn0L9?Tam?khm;cmfj@-EE=vY<#k5o0f^m&*sM6pBI0EL{uRZtU4R5Z z`^X8`V)zNfJ8YaF;u6EP09P2E5Ad?jnUOMxUqvh^s*L``+lU9}BlcPZ2XpY-jyyG} zttleQMaed+noc9ry@UIIwng1*#M^^UkFzj+6yF25_LnMhmf?pGpH<}CbcnAmX|u%; z|7E#p3GpKUUAw%6atiT2#A8)^yd3Cssj4^8z$>TJnP(8+gLrkpJj%xIMEtceUz`ec zeo~wa<_{hK;NmJRuzL3+9o0iVC08s$)vACv7|KExH6jmbd4gn8^0FkllzkRT6fd|P0n zrwSVAXrbt(XrcJk;OyVRDf#{Yw^VBW$m{6QFhZvPBk<6w!tZERBfJcLd1$M3&)TehHS09YRtkN(Z(GDi^F2;vKBX;HY+Q*UEnTqUvgh@A9J5aYSek}G;8j! zqp=F+X_rylpF&#LEj%b>g+0Q~5^H*5N-?MwSLAe5T{?GXqX;9WfX;*4pXw6&V*io?m|<6;W0GAb4f<-njQ>$>Q>9MPr@ zwU=n+0I+*9IKpp9{{0u*d8K7@p9VPmCp-6AwmA>rtOL;dXv;Q$XXCx^0IWWyc=l3w zC&Xgk0yx!!!@*Zi8qcdA>-AfBZYACT*nGWS|NH%Vee(m<-vGFv{{#Oecnig2*2@3@ z061k>NoGw=04e|g00;m9hiL!=000010000Q0000000N)_00aO40096103e_P00aO4 z0096103ZMW0056pK*<0A0Le*2K~z`??Uq>%fFKM+)p#^LwcbswS_2c7P+Ii^5|<+I zrc)B^y4IQl?>R6XsLi=f^7sOuz|@xO;ok>8RF9Hybs8*)mb><|Fqonj#^BWdFLHzc zZ&1A@nFXJM)NRo^~`yXUt1Zjw3f0Rm!+o@6*s66b!&OyMEH i=9Y(`t*<%osRKLCa#y+zeHT9f0000toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolBrush2_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - //ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush2; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - - -void MainWindow::on_toolBrush4_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - //ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Brush4; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolArea_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - //ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Area; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - -void MainWindow::on_toolLasso_clicked(bool checked) -{ - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - //ui->toolLasso->setChecked(false); - - if(checked) - ui->mapView->selectionTool = MapView::SelectionTool::Lasso; - else - ui->mapView->selectionTool = MapView::SelectionTool::None; - - ui->tabWidget->setCurrentIndex(0); -} - void MainWindow::on_actionErase_triggered() -{ - on_toolErase_clicked(); -} - -void MainWindow::on_toolErase_clicked() { if(controller.map()) { controller.commitObjectErase(mapLevel); } - ui->tabWidget->setCurrentIndex(0); } void MainWindow::preparePreview(const QModelIndex &index) @@ -949,11 +862,7 @@ void MainWindow::preparePreview(const QModelIndex &index) void MainWindow::treeViewSelected(const QModelIndex & index, const QModelIndex & deselected) { - ui->toolBrush->setChecked(false); - ui->toolBrush2->setChecked(false); - ui->toolBrush4->setChecked(false); - ui->toolArea->setChecked(false); - ui->toolLasso->setChecked(false); + ui->toolSelect->setChecked(true); ui->mapView->selectionTool = MapView::SelectionTool::None; preparePreview(index); @@ -1114,7 +1023,6 @@ void MainWindow::onSelectionMade(int level, bool anythingSelected) if (level == mapLevel) { ui->actionErase->setEnabled(anythingSelected); - ui->toolErase->setEnabled(anythingSelected); } } void MainWindow::displayStatus(const QString& message, int timeout /* = 2000 */) @@ -1346,3 +1254,83 @@ void MainWindow::on_actionZoom_reset_triggered() ui->mapView->centerOn(center); } + +void MainWindow::on_toolLine_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Line; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush2_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush2; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolBrush4_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Brush4; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolLasso_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Lasso; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolArea_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Area; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolFill_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::Fill; + ui->tabWidget->setCurrentIndex(0); + } +} + + +void MainWindow::on_toolSelect_toggled(bool checked) +{ + if(checked) + { + ui->mapView->selectionTool = MapView::SelectionTool::None; + ui->tabWidget->setCurrentIndex(0); + } +} + diff --git a/mapeditor/mainwindow.h b/mapeditor/mainwindow.h index 3d127b857..919267557 100644 --- a/mapeditor/mainwindow.h +++ b/mapeditor/mainwindow.h @@ -76,28 +76,16 @@ private slots: void on_actionGrid_triggered(bool checked); - void on_toolBrush_clicked(bool checked); - - void on_toolArea_clicked(bool checked); - void terrainButtonClicked(TerrainId terrain); void roadOrRiverButtonClicked(ui8 type, bool isRoad); void currentCoordinatesChanged(int x, int y); - void on_toolErase_clicked(); - void on_terrainFilterCombo_currentIndexChanged(int index); void on_filter_textChanged(const QString &arg1); void on_actionFill_triggered(); - void on_toolBrush2_clicked(bool checked); - - void on_toolBrush4_clicked(bool checked); - - void on_toolLasso_clicked(bool checked); - void on_inspectorWidget_itemChanged(QTableWidgetItem *item); void on_actionMapSettings_triggered(); @@ -134,6 +122,22 @@ private slots: void on_actionZoom_reset_triggered(); + void on_toolLine_toggled(bool checked); + + void on_toolBrush2_toggled(bool checked); + + void on_toolBrush_toggled(bool checked); + + void on_toolBrush4_toggled(bool checked); + + void on_toolLasso_toggled(bool checked); + + void on_toolArea_toggled(bool checked); + + void on_toolFill_toggled(bool checked); + + void on_toolSelect_toggled(bool checked); + public slots: void treeViewSelected(const QModelIndex &selected, const QModelIndex &deselected); diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 518464262..226dd3b39 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -442,12 +442,6 @@ 0 - - - 128 - 192 - - QDockWidget::DockWidgetFloatable|QDockWidget::DockWidgetMovable @@ -478,289 +472,361 @@ - 0 + 3 - 0 + 3 - 0 + 3 - 0 + 3 - - - - 0 - 0 - + + + 0 - - - 16777215 - 16777215 - + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-1.pngicons:brush-1.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-2.pngicons:brush-2.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-4.pngicons:brush-4.png + + + + 16 + 16 + + + + true + + + true + + + false + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-3.pngicons:brush-3.png + + + true + + + true + + + false + + + + + + + + + 0 - - Brush - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-1.pngicons:brush-1.png - - - - 16 - 16 - - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-2.pngicons:brush-2.png - - - - 16 - 16 - - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-4.pngicons:brush-4.png - - - - 16 - 16 - - - - true - - - false - - - - - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-5.pngicons:brush-5.png - - - true - - - false - - - - - - - true - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:brush-3.pngicons:brush-3.png - - - true - - - false - - - - - - - false - - - - 0 - 0 - - - - - 40 - 40 - - - - - 40 - 40 - - - - - - - - icons:edit-clear.pngicons:edit-clear.png - - - false - - - false - - - - - + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-7.pngicons:brush-7.png + + + true + + + true + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-5.pngicons:brush-5.png + + + true + + + true + + + false + + + + + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-6.pngicons:brush-6.png + + + true + + + true + + + + + + + true + + + + 0 + 0 + + + + + 40 + 40 + + + + + 40 + 40 + + + + + + + + icons:brush-0.pngicons:brush-0.png + + + true + + + true + + + true + + + false + + + + @@ -818,7 +884,7 @@ 0 0 256 - 90 + 120 @@ -861,7 +927,7 @@ 0 0 256 - 90 + 120 @@ -897,7 +963,7 @@ 0 0 256 - 90 + 120 diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 5013d206c..9171fd7e3 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -151,6 +151,60 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) sc->selectionTerrainView.draw(); break; + case MapView::SelectionTool::Line: + { + assert(tile.z == tileStart.z); + const auto diff = tile - tileStart; + if(diff == int3{}) + break; + + const int edge = std::max(abs(diff.x), abs(diff.y)); + int distMin = std::numeric_limits::max(); + int3 dir; + + for(auto & d : int3::getDirs()) + { + if(tile.dist2d(d * edge + tileStart) < distMin) + { + distMin = tile.dist2d(d * edge + tileStart); + dir = d; + } + } + + assert(dir != int3{}); + + if(mouseEvent->buttons() == Qt::LeftButton) + { + for(auto & ts : temporaryTiles) + sc->selectionTerrainView.erase(ts); + + for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) + { + if(!controller->map()->isInTheMap(ts)) + break; + if(!sc->selectionTerrainView.selection().count(ts)) + temporaryTiles.insert(ts); + sc->selectionTerrainView.select(ts); + } + } + if(mouseEvent->buttons() == Qt::RightButton) + { + for(auto & ts : temporaryTiles) + sc->selectionTerrainView.select(ts); + + for(auto ts = tileStart; ts.dist2d(tileStart) < edge; ts += dir) + { + if(!controller->map()->isInTheMap(ts)) + break; + if(sc->selectionTerrainView.selection().count(ts)) + temporaryTiles.insert(ts); + sc->selectionTerrainView.erase(ts); + } + } + sc->selectionTerrainView.draw(); + break; + } + case MapView::SelectionTool::Lasso: if(mouseEvent->buttons() == Qt::LeftButton) { @@ -205,6 +259,7 @@ void MapView::mousePressEvent(QMouseEvent *event) switch(selectionTool) { case MapView::SelectionTool::Brush: + case MapView::SelectionTool::Line: sc->selectionObjectsView.clear(); sc->selectionObjectsView.draw(); @@ -262,6 +317,55 @@ void MapView::mousePressEvent(QMouseEvent *event) sc->selectionObjectsView.clear(); sc->selectionObjectsView.draw(); break; + + case MapView::SelectionTool::Fill: + { + if(event->button() != Qt::RightButton && event->button() != Qt::LeftButton) + break; + + std::vector queue; + queue.push_back(tileStart); + + const std::array dirs{ int3{1, 0, 0}, int3{-1, 0, 0}, int3{0, 1, 0}, int3{0, -1, 0} }; + + while(!queue.empty()) + { + auto tile = queue.back(); + queue.pop_back(); + if(event->button() == Qt::LeftButton) + sc->selectionTerrainView.select(tile); + else + sc->selectionTerrainView.erase(tile); + for(auto & d : dirs) + { + auto tilen = tile + d; + if(!controller->map()->isInTheMap(tilen)) + continue; + if(event->button() == Qt::LeftButton) + { + if(controller->map()->getTile(tile).roadType + && controller->map()->getTile(tile).roadType != controller->map()->getTile(tilen).roadType) + continue; + else if(controller->map()->getTile(tile).riverType + && controller->map()->getTile(tile).riverType != controller->map()->getTile(tilen).riverType) + continue; + else if(controller->map()->getTile(tile).terType != controller->map()->getTile(tilen).terType) + continue; + } + if(event->button() == Qt::LeftButton && sc->selectionTerrainView.selection().count(tilen)) + continue; + if(event->button() == Qt::RightButton && !sc->selectionTerrainView.selection().count(tilen)) + continue; + queue.push_back(tilen); + } + } + + + sc->selectionTerrainView.draw(); + sc->selectionObjectsView.clear(); + sc->selectionObjectsView.draw(); + break; + } case MapView::SelectionTool::None: sc->selectionTerrainView.clear(); @@ -401,6 +505,10 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) break; } + case MapView::SelectionTool::Line: + temporaryTiles.clear(); + break; + case MapView::SelectionTool::None: if(event->button() == Qt::RightButton) break; diff --git a/mapeditor/mapview.h b/mapeditor/mapview.h index f9a83f133..c99785647 100644 --- a/mapeditor/mapview.h +++ b/mapeditor/mapview.h @@ -89,7 +89,7 @@ class MapView : public QGraphicsView public: enum class SelectionTool { - None, Brush, Brush2, Brush4, Area, Lasso + None, Brush, Brush2, Brush4, Area, Lasso, Line, Fill }; public: @@ -124,6 +124,8 @@ private: int3 tileStart; int3 tilePrev; bool pressedOnSelected; + + std::set temporaryTiles; }; class MinimapView : public QGraphicsView From 4ab203eaae7f77989452c58321dd66c0701b730a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 14 Oct 2023 03:16:17 +0200 Subject: [PATCH 0818/1248] Add more object search flexibility --- mapeditor/mainwindow.cpp | 16 +++++++++------- mapeditor/objectbrowser.cpp | 6 +++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 7d82c7714..fae6dc512 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -539,19 +539,21 @@ void MainWindow::addGroupIntoCatalog(const std::string & groupName, bool useCust painter.scale(scale, scale); painter.drawImage(QPoint(0, 0), *picture); } - + + //create object to extract name + std::unique_ptr temporaryObj(factory->create(templ)); + QString translated = useCustomName ? QString::fromStdString(temporaryObj->getObjectName().c_str()) : subGroupName; + itemType->setText(translated); + //add parameters QJsonObject data{{"id", QJsonValue(ID)}, {"subid", QJsonValue(secondaryID)}, {"template", QJsonValue(templateId)}, {"animationEditor", QString::fromStdString(templ->editorAnimationFile.getOriginalName())}, {"animation", QString::fromStdString(templ->animationFile.getOriginalName())}, - {"preview", jsonFromPixmap(preview)}}; - - //create object to extract name - std::unique_ptr temporaryObj(factory->create(templ)); - QString translated = useCustomName ? tr(temporaryObj->getObjectName().c_str()) : subGroupName; - itemType->setText(translated); + {"preview", jsonFromPixmap(preview)}, + {"typeName", QString::fromStdString(factory->getJsonKey())} + }; //do not have extra level if(singleTemplate) diff --git a/mapeditor/objectbrowser.cpp b/mapeditor/objectbrowser.cpp index c86b9aecc..beb9850be 100644 --- a/mapeditor/objectbrowser.cpp +++ b/mapeditor/objectbrowser.cpp @@ -76,8 +76,12 @@ bool ObjectBrowserProxyModel::filterAcceptsRowText(int source_row, const QModelI auto item = dynamic_cast(sourceModel())->itemFromIndex(index); if(!item) return false; + + auto data = item->data().toJsonObject(); - return (filter.isNull() || filter.isEmpty() || item->text().contains(filter, Qt::CaseInsensitive)); + return (filter.isNull() || filter.isEmpty() + || item->text().contains(filter, Qt::CaseInsensitive) + || data["typeName"].toString().contains(filter, Qt::CaseInsensitive)); } Qt::ItemFlags ObjectBrowserProxyModel::flags(const QModelIndex & index) const From 7217992898a8d14126983d393fdc4bd0894db500 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 14 Oct 2023 12:08:38 +0200 Subject: [PATCH 0819/1248] code review --- client/lobby/OptionsTab.cpp | 32 ++++++++++++++++++++++++++------ client/lobby/OptionsTab.h | 4 ++++ client/widgets/TextControls.cpp | 9 +++++++++ client/widgets/TextControls.h | 1 + 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 654653f90..bda9c43c5 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1041,7 +1041,7 @@ void OptionsTab::SelectedBox::scrollBy(int distance) } OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parent) - : CIntObject(KEYBOARD | TEXTINPUT) + : CIntObject(LCLICK | KEYBOARD | TEXTINPUT) , pi(std::make_unique(SEL->getPlayerInfo(S.color))) , s(std::make_unique(S)) , parentTab(parent) @@ -1132,14 +1132,34 @@ void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) if(labelPlayerNameEdit) { if(key == EShortcut::GLOBAL_ACCEPT) - { - CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); - Settings name = settings.write["general"]["playerName"]; - name->String() = labelPlayerNameEdit->getText(); - } + changeName(); } } +bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const +{ + return eventType == AEventsReceiver::LCLICK; // capture all left clicks (not only within control) +} + +void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) +{ + if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition) && labelPlayerNameEdit->hasFocus()) + { + changeName(); + } +} + +void OptionsTab::PlayerOptionsEntry::changeName() { + if(settings["general"]["playerName"].String() != labelPlayerNameEdit->getText()) + { + CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); + Settings set = settings.write["general"]["playerName"]; + set->String() = labelPlayerNameEdit->getText(); + } + + labelPlayerNameEdit->removeFocus(); +} + void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const { if(ps.isControlledByAI() || humanPlayers > 0) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 5c81e6479..230b74f40 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -189,8 +189,12 @@ private: void hideUnavailableButtons(); bool captureThisKey(EShortcut key) override; void keyPressed(EShortcut key) override; + void clickReleased(const Point & cursorPosition) override; + bool receiveEvent(const Point & position, int eventType) const override; private: const OptionsTab & parentTab; + + void changeName(); }; }; diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index aca634272..e86410638 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -806,3 +806,12 @@ void CFocusable::moveFocus() } } } + +void CFocusable::removeFocus() +{ + focus = false; + focusListener->focusLost(); + redraw(); + + inputWithFocus = nullptr; +} diff --git a/client/widgets/TextControls.h b/client/widgets/TextControls.h index 3f3823206..35e21245c 100644 --- a/client/widgets/TextControls.h +++ b/client/widgets/TextControls.h @@ -184,6 +184,7 @@ public: void giveFocus(); //captures focus void moveFocus(); //moves focus to next active control (may be used for tab switching) + void removeFocus(); //remove focus bool hasFocus() const; static std::list focusables; //all existing objs From b2396a61fa43da6be59d0f9f23b2ca1bd62446f1 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 14 Oct 2023 13:34:39 +0200 Subject: [PATCH 0820/1248] code review --- client/lobby/OptionsTab.cpp | 28 ++++++++++++++-------------- client/lobby/OptionsTab.h | 1 + 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index bda9c43c5..d41a8d716 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1045,6 +1045,7 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con , pi(std::make_unique(SEL->getPlayerInfo(S.color))) , s(std::make_unique(S)) , parentTab(parent) + , name(S.name) { OBJ_CONSTRUCTION; defActions |= SHARE_POS; @@ -1082,12 +1083,12 @@ OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, con }}; background = std::make_shared(ImagePath::builtin(bgs[s->color]), 0, 0); - if(!CSH->isMyColor(s->color) || CSH->isGuest()) - labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, s->name); + if(s->isControlledByAI() || CSH->isGuest()) + labelPlayerName = std::make_shared(55, 10, EFonts::FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, name); else { labelPlayerNameEdit = std::make_shared(Rect(6, 3, 95, 15), EFonts::FONT_SMALL, nullptr, false); - labelPlayerNameEdit->setText(s->name); + labelPlayerNameEdit->setText(name); } labelWhoCanPlay = std::make_shared(Rect(6, 23, 45, (int)graphics->fonts[EFonts::FONT_TINY]->getLineHeight()*2), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->arraytxt[206 + whoCanPlay]); @@ -1129,11 +1130,8 @@ bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) { - if(labelPlayerNameEdit) - { - if(key == EShortcut::GLOBAL_ACCEPT) - changeName(); - } + if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT) + changeName(); } bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const @@ -1143,21 +1141,23 @@ bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int ev void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) { - if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition) && labelPlayerNameEdit->hasFocus()) - { + if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition)) changeName(); - } } void OptionsTab::PlayerOptionsEntry::changeName() { - if(settings["general"]["playerName"].String() != labelPlayerNameEdit->getText()) + if(labelPlayerNameEdit->getText() != name) { CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); - Settings set = settings.write["general"]["playerName"]; - set->String() = labelPlayerNameEdit->getText(); + if(CSH->isMyColor(s->color)) + { + Settings set = settings.write["general"]["playerName"]; + set->String() = labelPlayerNameEdit->getText(); + } } labelPlayerNameEdit->removeFocus(); + name = labelPlayerNameEdit->getText(); } void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 230b74f40..0762f29cf 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -167,6 +167,7 @@ private: struct PlayerOptionsEntry : public CIntObject { + std::string name; std::unique_ptr pi; std::unique_ptr s; std::shared_ptr labelPlayerName; From 01aa288b72185d622e5b92a0572c0b5f6d2e49b2 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 14 Oct 2023 14:07:05 +0000 Subject: [PATCH 0821/1248] Link to Discord instead of Slack in metainfo --- launcher/eu.vcmi.VCMI.metainfo.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 326629a8b..adf4b037a 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -57,7 +57,7 @@ https://vcmi.eu/faq/ https://github.com/vcmi/vcmi/blob/master/docs/Readme.md https://github.com/vcmi/vcmi/blob/master/docs/modders/Translations.md - https://slack.vcmi.eu/ + https://discord.gg/chBT42V https://github.com/vcmi/vcmi keyboard From a8ff11ad74298a11d06e56fa0f90385b533bee39 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 14 Oct 2023 14:16:50 +0000 Subject: [PATCH 0822/1248] Remove redundant mention of GPL 2 or later from metainfo description --- launcher/eu.vcmi.VCMI.metainfo.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index adf4b037a..1ef4baf13 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -17,7 +17,6 @@
  1. Random map generator that supports objects added by mods
  2. Note: In order to play the game using VCMI you need to own data files for Heroes of Might and Magic III: The Shadow of Death.

    -

    VCMI is an open-source project released under the GNU General Public License version 2 or later.

    If you want help, please check our forum, bug tracker or GitHub page.

    From 5c0c3176911f494161c452fc9079cbfe8c1d28bd Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 14 Oct 2023 14:22:50 +0000 Subject: [PATCH 0823/1248] Move JSON validation script to CI folder --- .github/workflows/github.yml | 2 +- {.github => CI/linux-qt6}/validate_json.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename {.github => CI/linux-qt6}/validate_json.py (100%) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 337067b31..76b7bd3c6 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -151,7 +151,7 @@ jobs: if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | pip3 install json5 jstyleson - python3 .github/validate_json.py + python3 CI/linux-qt6/validate_json.py - name: Dependencies run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' diff --git a/.github/validate_json.py b/CI/linux-qt6/validate_json.py similarity index 100% rename from .github/validate_json.py rename to CI/linux-qt6/validate_json.py From c8452355ce98e7fd80696906a6e3b90397f37636 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 14 Oct 2023 22:52:24 +0200 Subject: [PATCH 0824/1248] Revert "Merge pull request #2959 from Alexander-Wilms/faster-server-connection" This reverts commit 43636af2e475a2b8d3ae0ee6eeacedc2cc2e290e, reversing changes made to aed2e360ba3235632158b9adc7eb5638c05b4e84. # Conflicts: # client/CServerHandler.cpp --- client/CServerHandler.cpp | 56 +++++++---------------------------- client/CServerHandler.h | 4 +-- client/mainmenu/CMainMenu.cpp | 4 +-- 3 files changed, 15 insertions(+), 49 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 86eb43af7..87988ad01 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -183,7 +183,7 @@ void CServerHandler::startLocalServerAndConnect() #if defined(SINGLE_PROCESS_APP) boost::condition_variable cond; - std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPortFromSettings())}; + std::vector args{"--uuid=" + uuid, "--port=" + std::to_string(getHostPort())}; if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) { args.push_back("--lobby=" + settings["session"]["address"].String()); @@ -251,54 +251,20 @@ void CServerHandler::startLocalServerAndConnect() void CServerHandler::justConnectToServer(const std::string & addr, const ui16 port) { state = EClientState::CONNECTING; - - std::string hostAddressFromSettings = getHostAddressFromSettings(); - ui16 hostPortFromSettings = getHostPortFromSettings(); - - std::string connectionAddress = addr.size() ? addr : hostAddressFromSettings; - ui16 connectionPort = port ? port : hostPortFromSettings; - - logNetwork->info("Connecting to %s:%d", connectionAddress, connectionPort); - - boost::chrono::duration> sleepDuration{}; - int maxConnectionAttempts; - - if(connectionAddress == "127.0.0.1" || connectionAddress == "localhost") - { - logNetwork->info("Local server"); - sleepDuration = boost::chrono::milliseconds(10); - maxConnectionAttempts = 100; - } - else - { - logNetwork->info("Remote server"); - sleepDuration = boost::chrono::seconds(2); - maxConnectionAttempts = 10; - } - - logNetwork->info("Waiting for %d ms between each of the %d attempts to connect", sleepDuration.count(), maxConnectionAttempts); - - ui16 connectionAttemptCount = 0; while(!c && state != EClientState::CONNECTION_CANCELLED) { - connectionAttemptCount++; - if(connectionAttemptCount > maxConnectionAttempts) - { - state = EClientState::CONNECTION_FAILED; - logNetwork->error("Exceeded maximum of %d connection attempts", maxConnectionAttempts); - return; - } - try { + logNetwork->info("Establishing connection..."); c = std::make_shared( - connectionAddress, - connectionPort, + addr.size() ? addr : getHostAddress(), + port ? port : getHostPort(), NAME, uuid); } catch(std::runtime_error & error) { - boost::this_thread::sleep_for(sleepDuration); + logNetwork->warn("\nCannot establish connection. %s Retrying in 1 second", error.what()); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); } } @@ -310,12 +276,12 @@ void CServerHandler::justConnectToServer(const std::string & addr, const ui16 po c->handler = std::make_shared(&CServerHandler::threadHandleConnection, this); - if(!addr.empty() && addr != getHostAddressFromSettings()) + if(!addr.empty() && addr != getHostAddress()) { Settings serverAddress = settings.write["server"]["server"]; serverAddress->String() = addr; } - if(port && port != getHostPortFromSettings()) + if(port && port != getHostPort()) { Settings serverPort = settings.write["server"]["port"]; serverPort->Integer() = port; @@ -399,7 +365,7 @@ std::string CServerHandler::getDefaultPortStr() return std::to_string(getDefaultPort()); } -std::string CServerHandler::getHostAddressFromSettings() const +std::string CServerHandler::getHostAddress() const { if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) return settings["server"]["server"].String(); @@ -410,7 +376,7 @@ std::string CServerHandler::getHostAddressFromSettings() const return settings["session"]["address"].String(); } -ui16 CServerHandler::getHostPortFromSettings() const +ui16 CServerHandler::getHostPort() const { if(settings["session"]["lobby"].isNull() || !settings["session"]["lobby"].Bool()) return getDefaultPort(); @@ -1001,7 +967,7 @@ void CServerHandler::threadRunServer() setThreadName("runServer"); const std::string logName = (VCMIDirs::get().userLogsPath() / "server_log.txt").string(); std::string comm = VCMIDirs::get().serverPath().string() - + " --port=" + std::to_string(getHostPortFromSettings()) + + " --port=" + std::to_string(getHostPort()) + " --run-by-client" + " --uuid=" + uuid; if(settings["session"]["lobby"].Bool() && settings["session"]["host"].Bool()) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 83b458883..98feee33e 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -122,8 +122,8 @@ public: CServerHandler(); - std::string getHostAddressFromSettings() const; - ui16 getHostPortFromSettings() const; + std::string getHostAddress() const; + ui16 getHostPort() const; void resetStateForLobby(const StartInfo::EMode mode, const std::vector * names = nullptr); void startLocalServerAndConnect(); diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 622898a74..825389069 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -532,8 +532,8 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->giveFocus(); } - inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddressFromSettings(), true); - inputPort->setText(std::to_string(CSH->getHostPortFromSettings()), true); + inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); + inputPort->setText(std::to_string(CSH->getHostPort()), true); buttonCancel = std::make_shared(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CSimpleJoinScreen::leaveScreen, this), EShortcut::GLOBAL_CANCEL); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(7, 186, 218, 18), 7, 186)); From c96cc405ed55ac6d0bd1d5e63ff368d312520c57 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 14 Oct 2023 23:41:12 +0200 Subject: [PATCH 0825/1248] town now updates --- client/CPlayerInterface.cpp | 3 +++ client/gui/CIntObject.h | 6 ++++++ client/windows/CKingdomInterface.cpp | 5 +++++ client/windows/CKingdomInterface.h | 3 ++- 4 files changed, 16 insertions(+), 1 deletion(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index ea49a74fd..f68d8d58d 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -591,6 +591,9 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build // Perform totalRedraw in order to force redraw of updated town list icon from adventure map GH.windows().totalRedraw(); } + + for (auto cgh : GH.windows().findWindows()) + cgh->buildChanged(town); } void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 5833ddac3..6ea72ed56 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -153,6 +153,12 @@ public: virtual void updateGarrisons() = 0; }; +class ITownHolder +{ +public: + virtual void buildChanged() = 0; +}; + class IStatusBar { public: diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 390ba1cb5..81426a045 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -640,6 +640,11 @@ void CKingdomInterface::activateTab(size_t which) tabArea->setActive(which); } +void CKingdomInterface::buildChanged() +{ + tabArea->reset(); +} + void CKingdomInterface::townChanged(const CGTownInstance *town) { if(auto townList = std::dynamic_pointer_cast(tabArea->getItem())) diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index a9a7d7cc4..31097a58c 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -200,7 +200,7 @@ public: }; /// Class which holds all parts of kingdom overview window -class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder +class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder, public ITownHolder { private: struct OwnedObjectInfo @@ -257,6 +257,7 @@ public: void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; void artifactDisassembled(const ArtifactLocation &artLoc) override; void artifactAssembled(const ArtifactLocation &artLoc) override; + void buildChanged() override; }; /// List item with town From f6071ed181cda38a84c9491630109cde053f290e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:09:32 +0200 Subject: [PATCH 0826/1248] fix --- client/CPlayerInterface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index f68d8d58d..31089089c 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -593,7 +593,7 @@ void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID build } for (auto cgh : GH.windows().findWindows()) - cgh->buildChanged(town); + cgh->buildChanged(); } void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) From 78026f40ebe982631d62db2d67450cf9d9434a4d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:41:17 +0200 Subject: [PATCH 0827/1248] name opens town itself --- client/windows/CKingdomInterface.cpp | 4 ++++ client/windows/CKingdomInterface.h | 1 + 2 files changed, 5 insertions(+) diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 81426a045..7e04aaef5 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -822,6 +822,10 @@ CTownItem::CTownItem(const CGTownInstance * Town) } LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); }); + fastTown = std::make_shared(Rect(67, 6, 165, 20), [&]() + { + GH.windows().createAndPushWindow(town); + }); } void CTownItem::updateGarrisons() diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 31097a58c..fa0e57687 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -282,6 +282,7 @@ class CTownItem : public CIntObject, public IGarrisonHolder std::shared_ptr fastArmyPurchase; std::shared_ptr fastMarket; std::shared_ptr fastTavern; + std::shared_ptr fastTown; public: const CGTownInstance * town; From 23e1b0b0d59ae4d96d971c68b844890093defe86 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 02:43:42 +0200 Subject: [PATCH 0828/1248] radial wheel for hero --- Mods/vcmi/Data/radialMenu/getArtifacts.png | Bin 0 -> 1055 bytes Mods/vcmi/Data/radialMenu/remove.png | Bin 0 -> 810 bytes Mods/vcmi/Data/radialMenu/swapArtifacts.png | Bin 0 -> 1096 bytes Mods/vcmi/Data/radialMenu/trade.png | Bin 0 -> 1159 bytes Mods/vcmi/config/vcmi/english.json | 7 ++++ client/windows/CCastleInterface.cpp | 39 +++++++++++++++++++- client/windows/CCastleInterface.h | 5 ++- 7 files changed, 48 insertions(+), 3 deletions(-) create mode 100644 Mods/vcmi/Data/radialMenu/getArtifacts.png create mode 100644 Mods/vcmi/Data/radialMenu/remove.png create mode 100644 Mods/vcmi/Data/radialMenu/swapArtifacts.png create mode 100644 Mods/vcmi/Data/radialMenu/trade.png diff --git a/Mods/vcmi/Data/radialMenu/getArtifacts.png b/Mods/vcmi/Data/radialMenu/getArtifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..072a95516fe5c011700ccecd9b1510e008c0e9f5 GIT binary patch literal 1055 zcmV+)1mOFLP)h0004lX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$i(@LdOI@m$PAw$*0E-K;tBaGOi^GTzbXb_5kdq&0OK+<>vNKnhUfUYhmWs!ah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G z=e$oGWo200qd;&MXg6*9``EVICxHJMxYE1+S__!{B)!?y zVn;w^8@RacYRVpPxdRM6)n!9=BtK1|SOnhB=$rDu;4KhZb9?LTxDFAAdN% z7f%72#-O7+{rN>3#Mo z6GO6Xl=hq@RbxoXrrPS)Tk}>W2C>S57+_h&z}cs%M(DrAz)Yz#&>m7>!b~BQ2A~{D zDKS$Bg;1J-a?r~J6h$>#o6py(!=B6f)L~Ec`MR|4AaWwsa62(%|6R445w(0RkXAcfJUAAOO@1h$$2;az#7+y3on6N^kQ_DRM<8 z`?}ChO>MQzJUy8K?=3(T=w!g_6c8;@b`=iliH7H&rohW2l1wPip zhI`GFSPvT}F_lX!hYiKkSn*e|IQw{R=GdnYN_>3@Glfu`LSgg9Od%9P;W7b5Q53b4 ZJ^&H4x1ATBf!F{5002ovPDHLkV1f?z-(>&* literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/radialMenu/remove.png b/Mods/vcmi/Data/radialMenu/remove.png new file mode 100644 index 0000000000000000000000000000000000000000..2a82a74e4c5fcc809bc614fbf112dcf94852b31a GIT binary patch literal 810 zcmV+_1J(SAP)h0004lX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$i(@LdOI@m$PAw$*0E-K;tBaGOi^GTzbXb_5kdq&0OK+<>vNKnhUfUYhmWs!ah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G z=e$oGWo200qd;&MXg6*9``EVICxHJMxYE1+S__!{B)!?y zVn;w^8@RacYRVpPxdRM6)n!9=BtK1|SOnhB=$rDu;4KhZb9?LTAL*VWDJHk$ck$jnZ$* z-gNIky00k;WNXSO!N_-?>cMoYQ>q5jy`rraOqsJ!m0-%9l6wbJwx7}~nDU)Bbubz2 zPR}%)9ZXjL*R!8wTE{&p5Hquz0iO>GEn9NoP$=Fg^EnXGI+!Vth+t?(MutM;S`f(4 zR~%f134)p8+BDMiR2S?Krl%amxMF(hp`A0Pml*GAF}>6v;1SbHj*R{?z4UPLC(|R2 op{FtO4+{VQ00000007X&8yQg%U4OYPhyVZp07*qoM6N<$f&ztSHvj+t literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/radialMenu/swapArtifacts.png b/Mods/vcmi/Data/radialMenu/swapArtifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..303b45ae0daf464238ad8ec70368b38b99352ca2 GIT binary patch literal 1096 zcmV-O1h@N%P)h0004lX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$i(@LdOI@m$PAw$*0E-K;tBaGOi^GTzbXb_5kdq&0OK+<>vNKnhUfUYhmWs!ah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G z=e$oGWo200qd;&MXg6*9``EVICxHJMxYE1+S__!{B)!?y zVn;w^8@RacYRVpPxdRM6)n!9=BtK1|SOnhB=$rDu;4KhZb9?LTKZf`5RZwWP2VEmRN`A%%-gL z7=)Q_Qq$O8;2CfocoYoY1@;3^e3@S{3dB^`)igH;+yWlBthhF~oPp;+ADD7ku^u@T zQ{AM7u>`mPECaLuB%KwDz%jZSp_uEA9ZIOu0?_qkeqw`5t^!|yS1}W5LN}@DaUS>p%!d~Fci*1`&V=^3 zX3&64b(5M^TLx|hBNv0QoxmO73-H*YC0pyJH9NtfF#H5O^ku#qI2+8)x~y0+dQUOc zbt9VD2fP7%nCb!tD=z;9EDg=Qa9MF_j1U^t9d)I2fDHH+gnxvG8sTm7a2S6C)+!8- zL!-K*hOz>j0+w7>d>*Ri4d7lloOW5U8ga)os+*4Zfa?P<|Kp+EF=)e$=_V1qAE_8y z6J0lEC^HpfYohDM48^zg-&X7!CPqekEd|uuGNJe~KMhO`Or2|Fi5Tg|S}AEuE5k%L zRw%=*aF;LhV-192q8lse^T4}-h0004lX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$i(@LdOI@m$PAw$*0E-K;tBaGOi^GTzbXb_5kdq&0OK+<>vNKnhUfUYhmWs!ah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G z=e$oGWo200qd;&MXg6*9``EVICxHJMxYE1+S__!{B)!?y zVn;w^8@RacYRVpPxdRM6)n!9=BtK1|SOnhB=$rDu;4KhZb9?LTleK~#9!?b%ODR8bfP@aK5v-i}l|nX45}glSyGiijYFM50w^ zIYbfIB10&MzCepUKoAjuWe^2za#3_wwhCMb2Gvl928EiGa55qgn%j)m)55`SO_G=s z^XK`kXEEoQ`=0lG?*K$ZL_|bHL_|bHL`03lq?5aiR{iyOwx)H2(POllz~-I|fYbFD zN^H$xXmtRV96_vQADXPXt^%=!Fb>0#w_wM+A%6S#QgIO`C|2F63STCL=l5;+lVY?w z3QOLE9q%j)W2N})7+}Y$E)?tAwhb*Gqt$U(@;dDJu2sXRbFoMmDOmClhOrWg zNhf=-sdWsF+r~kobSsG7@z)50D+m|n8AoJJ5uQIRI_|}h&kmrFn`0=^{_OepMb~H> z^4Sw8s)XQ!1-*0NqBbgRtan01NC@weng+t7|5cdTF#e^j}^b z)QBx`+`W+P6DE^-W3)O_u{-4tzsL}P{nfV+%04*mo}XXT0L4$!2!jj4^T#S1N+vZ0 z$L*+{x-vlV(|d%$CE@vx8?Ag+c?1ODxH};ek4z>tW3)QOP@?tWxI56CcwjQAIiuAX zh7#qtE}9c}&Gf*$(ds-y`Q>un+5$^n2GGh-i~$O{S>&@PMb~H>fVo&hhq2s=E(*_o zvhFdzj>4EK%`8vx0sxjm zVDa-SguyxC`I8&ICD)Xt(BS**b1Z%x6uskVHWaP;O*+}hIfICZh=_=Yh=_=Yh=_>( Z?gtkn;C42~%WMDu002ovPDHLkV1fvH9n$~+ literal 0 HcmV?d00001 diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index f4c912f00..32d874339 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -41,6 +41,13 @@ "vcmi.radialWheel.splitUnitEqually" : "Split creatures equally", "vcmi.radialWheel.moveUnit" : "Move creatures to another army", "vcmi.radialWheel.splitUnit" : "Split creature to another slot", + + "vcmi.radialWheel.heroGetArmy" : "Get army from other hero", + "vcmi.radialWheel.heroSwapArmy" : "Swap army with other hero", + "vcmi.radialWheel.heroExchange" : "Open hero exchange", + "vcmi.radialWheel.heroGetArtifacts" : "Get artifacts from other hero", + "vcmi.radialWheel.heroSwapArtifacts" : "Swap artifacts with other hero", + "vcmi.radialWheel.heroDismiss" : "Dismiss hero", "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 36b5a5207..1b19a1806 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -29,6 +29,8 @@ #include "../widgets/CGarrisonInt.h" #include "../widgets/Buttons.h" #include "../widgets/TextControls.h" +#include "../widgets/RadialMenu.h" +#include "../widgets/CExchangeController.h" #include "../render/Canvas.h" #include "../render/IImage.h" #include "../render/IRenderHandler.h" @@ -322,11 +324,46 @@ CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroS set(h); - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + addUsedEvents(LCLICK | SHOW_POPUP | GESTURE | HOVER); } CHeroGSlot::~CHeroGSlot() = default; +void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(!hero) + return; + + if (!settings["input"]["radialWheelGarrisonSwipe"].Bool()) + return; + + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + + bool twoHeroes = hero && other->hero; + + ObjectInstanceID heroId = hero->id; + ObjectInstanceID heroOtherId = twoHeroes ? other->hero->id : ObjectInstanceID::NONE; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_NW, twoHeroes, "stackMerge", "vcmi.radialWheel.heroGetArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArmy(false, std::nullopt);} }, + { RadialMenuConfig::ITEM_NE, twoHeroes, "stackSplitDialog", "vcmi.radialWheel.heroSwapArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArmy();} }, + { RadialMenuConfig::ITEM_EE, twoHeroes, "trade", "vcmi.radialWheel.heroExchange", [heroId, heroOtherId](){LOCPLINT->showHeroExchange(heroId, heroOtherId);} }, + { RadialMenuConfig::ITEM_SW, twoHeroes, "getArtifacts", "vcmi.radialWheel.heroGetArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArtifacts(false, true, true);} }, + { RadialMenuConfig::ITEM_SE, twoHeroes, "swapArtifacts", "vcmi.radialWheel.heroSwapArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArtifacts(true, true);} }, + { RadialMenuConfig::ITEM_WW, true, "remove", "vcmi.radialWheel.heroDismiss", [this]() + { + CFunctionList ony = [=](){ }; + ony += [=](){ LOCPLINT->cb->dismissHero(hero); }; + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); + } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements); +} + void CHeroGSlot::hover(bool on) { if(!on) diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index a5b7580ea..0601605f8 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -112,6 +112,7 @@ public: void set(const CGHeroInstance * newHero); void hover (bool on) override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; void deactivate() override; @@ -154,7 +155,7 @@ class CCastleBuildings : public CIntObject void enterCastleGate(); void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains void enterMagesGuild(); - + void openMagesGuild(); void openTownHall(); @@ -228,7 +229,7 @@ class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr split; std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; - + std::vector> creainfo;//small icons of creatures (bottom-left corner); public: From 5ae646bd157ecbee6091d1d1e3a0caeea41368bf Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:19:44 +0200 Subject: [PATCH 0829/1248] Update client/windows/GUIClasses.cpp Co-authored-by: Ivan Savenko --- client/windows/GUIClasses.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 0271b2a7f..80aa21f0e 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -489,7 +489,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); recruit->block(true); } - else if((LOCPLINT->castleInt && LOCPLINT->castleInt->town->visitingHero) || (dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero)) + else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero)) { recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. recruit->block(true); From dd0033296ff0331baa0fd8c64ef2b4d0dfd5b9c6 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:28:13 +0200 Subject: [PATCH 0830/1248] docs --- client/windows/GUIClasses.cpp | 2 +- docs/players/Game_Mechanics.md | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 80aa21f0e..5b946b28b 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -489,7 +489,7 @@ CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::func recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); recruit->block(true); } - else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero)) + else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero) { recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. recruit->block(true); diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index d360acb86..cb16df499 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -41,6 +41,14 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero - [Alt] + [LShift] + LClick - dismiss selected stack` +## Interface Shourtcuts + +It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitment and Marketplace (click on gold) from various places: + +- Town screen (left bottom) +- Kingdom overview for each town +- Infobox (only if info box army management is enabled) + ## Quick Recruitment Mouse click on castle icon in the town screen open quick recruitment window, where we can purhase in fast way units. From efcf6b71d77e7ccc73ff839c42d8a3244039ca9f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 12:58:07 +0200 Subject: [PATCH 0831/1248] formatting --- client/windows/CCastleInterface.cpp | 4 ++-- client/windows/CCastleInterface.h | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 1b19a1806..b11349b33 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -359,9 +359,9 @@ void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & f ony += [=](){ LOCPLINT->cb->dismissHero(hero); }; LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); } }, - }; + }; - GH.windows().createAndPushWindow(pos.center(), menuElements); + GH.windows().createAndPushWindow(pos.center(), menuElements); } void CHeroGSlot::hover(bool on) diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 0601605f8..4f1339249 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -155,7 +155,7 @@ class CCastleBuildings : public CIntObject void enterCastleGate(); void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains void enterMagesGuild(); - + void openMagesGuild(); void openTownHall(); @@ -229,7 +229,7 @@ class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder std::shared_ptr split; std::shared_ptr fastTownHall; std::shared_ptr fastArmyPurchase; - + std::vector> creainfo;//small icons of creatures (bottom-left corner); public: From 3b853bff0826dbd4e65dac171b216e8ab20119a8 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 14:31:33 +0200 Subject: [PATCH 0832/1248] sort town feature --- Mods/vcmi/Data/radialMenu/altDown.png | Bin 0 -> 5401 bytes Mods/vcmi/Data/radialMenu/altUp.png | Bin 0 -> 5373 bytes Mods/vcmi/Data/radialMenu/itemEmptyAlt.png | Bin 0 -> 5819 bytes Mods/vcmi/Data/radialMenu/itemInactiveAlt.png | Bin 0 -> 1499 bytes Mods/vcmi/config/vcmi/english.json | 3 ++ client/PlayerLocalState.cpp | 6 ++++ client/PlayerLocalState.h | 1 + client/adventureMap/CList.cpp | 32 +++++++++++++++++- client/adventureMap/CList.h | 2 ++ client/widgets/RadialMenu.cpp | 15 ++++---- client/widgets/RadialMenu.h | 13 +++++-- 11 files changed, 62 insertions(+), 10 deletions(-) create mode 100644 Mods/vcmi/Data/radialMenu/altDown.png create mode 100644 Mods/vcmi/Data/radialMenu/altUp.png create mode 100644 Mods/vcmi/Data/radialMenu/itemEmptyAlt.png create mode 100644 Mods/vcmi/Data/radialMenu/itemInactiveAlt.png diff --git a/Mods/vcmi/Data/radialMenu/altDown.png b/Mods/vcmi/Data/radialMenu/altDown.png new file mode 100644 index 0000000000000000000000000000000000000000..a3945a50072533aaff35366842388ea81a3db67c GIT binary patch literal 5401 zcmeHLX;4#H7Jj$@21P^^M3fMq5j86bSxgiNTaXAM%Hjf9UP8u%gd{+M+Rb7EHjao0 z3JnT^t*wBFv>T$bxiJb#E2AhNinxFXf{G3=4bym!z2&UerG?zyM( zvdwpe$25dF0)n7vEKgTIaPE)|HxFjA?O!AMvo z55hWt1@MQ3b z__^fnY|p5Hs-5arkY#?=x_|;B-{pmA9~O{a?vH$Du6mlCk&;Gd1`6{d-qokhh(W%% z(vRLAY&PR8#AzH}d&V)SBqZ=How?c0I`d3Vi~8m>iCddhZEkECVaBG3{9lbDXX=_( z>epr$V2*n3edaWB{h8}~h*TQn(Hmdt1VK7mMNFnIi^=?y4oD(rOB%zo#>t{CWYs~x zg>LtpYps}(WhuW-dvgTdU3Dfryb6Jgc2^=8QTRuC`~3O3muDTU0XU9EUI7FNA26{Y-;o4;IMN-_q(c(?a`+M5&yWw zM%c!Un6`|VO`K5SR3U0XwUjkpZjT#MaLX zefK+NZkhFv2vLW6^`m>+Zavf~C)xF$;uEP&ddTJL4@zSbn4#xf@4lGdJO61{KF9UV&_H8c!<0aQIR#j8REspza`uzDyn$R4NsVBVY*xG(eyg>To4Tg$`F(YbZuJTww)IE|Mul(r~1Plf#up zDj6sgm`8q!PaaX9EjQ(&9=}$ zGC)|4rcMEY)(&!^G378vDV6(6rD6t36B1J6`FWZRjuW4wnFSDFG_*P72r7zTpXIq0aZXG;dp2YpNc~Z z$V3vEh^OEM0>anq3aLOD&5^^-LZBni3Y4c-E93(0M%jOjjt+x0y8r}3<8kQEf)Q|I zf?>xJ#%g-Tr;O>?|I&o61&nJlz;DzBniuGW*w4*yOfyZ}`5Rwjd+|4}0I0t&@=f}F zm+QM+-=x4dfxlPRce%bvfo}qTude@_T!=4^Q*b!=6%-8~mW(@1hQNcCE_b4lGk-ojNK?0JEf@gims;CU>^1Hy!%3oV}@OpxVGZ-qQPCO5z5RM%@|V=jBwM^ze; z2WzJq^PBHfwn1H~r>sUcchsg&yvN1cn3onkY<*od`~bQ+qIeVm z5uspzuVf+j%?c#+FxDm(Dh%w3WT2OZ5#y!P!n^w?gFu=TmR zs(&~AXYEL1^aUf0ae=yJ-99MUv(@bf6*>^t-AL8zn4^6Jl`y&Uad8-dVFs8UJ`oA!2>KrohaM=nqlQ&RZ@G39wfd$Cw*6Z z!)hyhmR?`z@ZNz37De6j%X36V)#Uj>smz8y5-Klev+FmgF}?JrGPhUa;fbEjskMI| zZ8r!yTG<@gqeE~<1~~5XjtEJb**1;)E*&FTLz}q8!!Y*Wic0Yd^Pak=&BOus>lL?U zm)p9YPmzY_qwJkeNU+W2EDuXtD@!vTP3ZOs}J`E%GOF&=WI>hVHjPW@IJt)%uhCJ ziA~_pz^l-;UHOUoj!SGC%DV&aKeWazbZJa^mpMJpbP3Ljc7N;XGidVFoce8PjY3IY z9Cc?m+T_&o*o+lhk2&r)^}8WnuKo?aoHHpkzUMr`JXw0zF++E@Q%ZvjbE#?gQdn@R z(2r|RkR6%4Sf+Pxw#{_&_Pq0!%z*6kyDuVcc?Wl9W%k&t>thU5*N0}LG$=1xO%3f# zFGzEk=W;De+5O_2ubC|Ay-&`B!M4~;yZtNjbf4NzX*_Uc8UF22@5=VvD6qQ0jn0Nf zg~SXyGUdRsy7eA91yYdSjfuCM7wNZuMa)16IN%@xLyj#mK`-U zS&LcEvdm8pSNEtD9|`ttYfo5iK567BmY;oXTee|aVuGYNDfP{VOqO~MI`Ib!JmSfc z>CdAbBVXELA#p?XAD){kUi`B53T{qD^y%Y5+Wof8Iz^kO23I{gaquNj$dG1uA@g}~ zZdYgVK6)*-z&aP*xTT&|V?~7q8(ejLEW2D<9$X(f z65c%P6gWn^&n0CA=&Q-*BL`*?UzzH!wYxT9%JcpDZ%>u8wz;V$`6wFzsv}}#T40}k z*ib>IVoii)`Kykq#Fuk740YUe=!f!dl^Es7}(Ym4dEWPOTi;78mKeptbecf#I7&CZf7}Yq~N^fxeFLf$1 zU4JmxsboioMg}S2DwR#Dt)<#d z(?=pIWkVwQ6t&sXS14H>loBarJMcX-RJwMr>$|$HZ~tp%-qU^fJ@@^)@8`MRXEp`+ z%{Ml*GQ?mo#=Z+UOTpD29NuH~!P%&&yd7L>5`u!2OW`=IOfD4&MJQGoFGI1YN+`f! zR4w9wF!LJh*!OQHF;m>BD(f3FDd#*h!%eSm;?nP@IG^&oKDgHA3CwE0h-S5{x@su7 zQyh(J{@sB6s9JALn!aM?k6tW%mQUZYrpF#nduhFmv^f@b@f(VN^Sa-=d%E2#mTT6- zFw4_KoKok)fp@3m;*;O+FMb}sVZhhN`Q`1>cZXVAHxF(!jnl2K=RDKfX0xz~Yx#YN zSvqb0hca4Pw#$Qizxb(5sJlW~sSXd;V=y{tLN+_Vm(Bi|4oG5eQWkST4a>SVJn+a; zC+`-|i!<1$Lm-<^%OxTstn~X^(E~NpR89j7MEd)v)rB?vVVt~ z>Rxw?xn|9|EiavW6zjHno{k__IhZ>H-z`q3r$@RLLY+w+j&mBEUNByyd0Na|`=G3& zZ(8T}8r^FdR?S!3V|E5R_ZMHUDG)_nym67V?8fZyOJ@t@Z5;*6rWGjs2QLJ8hKD?Y zt}am*r`s*NNT zh=HRn(@xs`DKxCb^{(}~x6=Su%935}EIeDm$Wr)BM!7$M$h|_dUwK z!L^trm*v@_7>P!eSRNWFlrV8U z<>zr&A)kp`LE{p+GBz3|T#z6~0~7p$kc4Q2!N+;bGjvx$06>f?VXR6lk|-b*6Q{+6 zz`aIHz+tr}%4jAom>Ym)OXVn*>OyrP;=NVE)nwc}L#(@;FMyVEd`2L^6B8GuRLUR% zAucY?C63}El}8dt3 zd_0NHr{JjwU4VyC3Jp)>k*IDkl};lQ`J8h{{@@I)$=2vaDo6b8+06okKA2)Ys$jgCqK#RpI{I+;x4xiau5oe1Npd!gGnN~P^emsrYsOh z1`rEt`V;_YMCp8yLCqAr%Ij|B1phPkSA`u}HEr>*g z$Yh8}opooa1wsWFKVB;uo>75l%<$9jqe!ToN?5FRQa~^= zoPq*gjSklvU=6E~C|DASg4JWBT|df&f71#y8bag=1T;LIPb1^0bOgr3bX0)n3*1No zDw#y&Av`S$pU@Rjfie!3qn?p~N5B`wR#C+=E;FcV;pgoXDgkdnabUBQ-C>sqHd^|;Me{kBA8+00)jGvjbj?>d&6>1r;g8cVWG2V0y5gm0 z(I$Ixro!^R1yO&7SJ=YcE8lK7LSs@oGb{?nWmV$B{8~GAU%B~P_P&>MuT_{-5y#gM z$4?A)O#i7~ZDW*d%+hPT+%(xEy?>Ht!OopQr{A#ZRz%~LFC3fSZW+#L%0BpU%$n@!MwaZ(;UT zi_?CazL~?PH0;?pZFksfcBXZ57hZo&)vw8kk&4)7X^MW{jpe)OMk|H6YS);PlktL5=o_EwXgFB?{ps z&g#DHY?8dQqrdC%2}*BYVCqHD&oQjddSug%PW6HMWrU)4UV+{q6?R^wxOe`eqAj~k zJo_C)Vb3qC_Z?b>C-Aqd%6o72c(3u{&UIDqjq~22iM+zXUG`^+LgVjR>^Z}E+8Wy4 z7H=$&|6!6tO6h8~kw$sk{%Q5X<@+iYx0v<+XtVKPpYK4bdGC}N{yJR}vsZaYE4ecN z0eXGww(V`nZ+7dwdEG#4>o6i&$$BbnxFt48S--!*;jZOJyCoVzr4tA3azcHbr%dnF z_iALHpqV{AY(a~evdB7i^(FJ3>S^=F-VA&z>i9)enN$>1w>5XnlK5HO>67u9W9?5o zI7$ge|5(S+6MbZ%18*bWxG6QMvAOri;=oiQ+b||UJ+_W~?k>sDt)7y@5&xsA&EMo1nN+(kkQ|JIjY_H1j>J6_!K{zE7_zz86Ud{E|BUopKIIGxU2i`VB_y{B|ymK2HkHx2V zIHx}fzH!-?m7$l8`Porjk@7?7CACeMsTjNd8gW?k!{>F_YyK> zH{aY^2MrajpSu5W!;7SQ8CI#)Ug0+!AFIjPWgWVg{BjuvlebwL{BC`9@c@>T&m79+ zch!&4Wd&@t{Vw9-okc6z7-z%A7$KJM*4ZN1wgHKE&U{D$#|igcb;aQ5(+EctA+~*E zFgf#bgI$BGk8aMwOl4tS?U$}{U;IHZ)6uQa-m#DBctq)8TNA}x^3h8#(zlYeqW`2`a!qqVcRa)a)ailTkkRWFkd$2>MaPY PVi;dB#-_><}-+#?q@B2K@{oMEOx$ob7KhHbw zB)PjfDl5VjVKA67&B=}dwQkU5zkC^VSE@WW3^mQMo<0%=5RDLu1RQQSh>*kxK?Eq} za$qp&AkTeBxe2jyJ~%mbSTpFdJ{=jkCpBN*yh^!j*e>F&T=MLV#4(aSrxg{*n~5|g}F zI=|1vr|D$k>U1ToWw%=6`>FyQeF(L-3ny&+OJ8tjt`rYGzZP!){h*e1N_LCYg;C*% zWkUN+crU#EKISj20gV}fn?j!|sms^VrPt9O29rzV+S-8w)lE0c-yR+ zmy3vuU`P(Hncl33V(t*etQ$WsuAR2D4)Mz7JpT?xuSs#)xM6JO6m$4nn}o8FeVtdz zduR;!v4;=!EF15%NHbrYx4K^7htVEZOHwQ6`!wq`<8rs=&ss(#908B~nJ2weuqg=IpDa*R!VX3n zPb)OM`i42Qj%6LB8A9Z}dUfOM(E7MH+O>V>Ui&F!Ef}7^IZ)(iE!C;5oeJ&H6V@$V zX*B@aMj2WiaW`2q9Xv;m37XJYUQs80m^V5n*Vb3wN4#)R9}`gYDDq&m>Rk8ZJDc+V zT7YR|M3X;Vq-8>dC*?u~=0kU(umn6)fGuExrc#~|syrCX!b&OxSYe<9!32Z3d@6GK z-0w&PmrX@_o6|9Lp)DA~b&3^%9Ttk^IXnT@ovRJ4#%AOIdH0T5DNIA2VWQjv?e z6sRu~qmhV36-gKs=|gu%*a}1-0&j{p#h~n^+$b}or6R&Y#O6>Kb`I|#pb-@rB9RCw zXmoURv}rWXR3HjQW65MP8e@hwGebcdC~*v50!UGOv4ITYEruN^W{J2$30J^J$S?t> zAW}j_BB6Q2d;fSsI{gDYU;K^*h!3{!0_~$fzQo{D2sZ13y)aS~z8DUhg$Bbx9;7OUR>gj{q$7>){=q^3Kr$zxumH=PNMf^zI2`^HC>mcZ0r)IX1_gneav>ZJi%H;M zNJJC~hry!oOr|;1Gbf_RWDbYP1TY}hjQk0Nn}`clB@q6pS28Fz1O*UTWRe*HgCdbQ z5R^HGfC9`pBovd$B6G}e7#yBVT!dn?C=LP<4}j9ip+Lbv!~(K9g#Z>UpjarjA`p-WM4kdcI29>#2_dumFinTH6C02Kc7OzgKrv=G z3Km1bntNh#6by!fF*nATL7k8K0ydWu^WU^(n+IX>HswxSF|>ZnqUi08@&F^=j@}N# zxrGJ2T!#Y=Qn8Cl-qepByrf-CG%SctK|&`om%Pjx*V@^MCxj%f

    {R)bPUY4qnjseh% z)-vX1M?2UX*|*?)X*wiXA$0N;Lw^FO$u2oqPM#JdRFKf<_6p;Qs!A~VvY51`kRgj^ zXX7aye4H2N;t{;|^3NK^-U{(?1+MN51+3%?o+BJW(f7C!{5gcCO$9uEe{N2e$q;d& z=;@wxRH$Z&f0wm<<*7KSobr;p*W%@q_v|iKPd+QZ`p@YmlT;XE*nPX}E5qqh!uqcC zaca-@T|0X^mo_~(GF8~vD7WfjNb{&%Ja@-(jfD=@T)j(xi?4>Q?mzYBXB@R-1E1*} zi0PNtxS=_z*=(_e`}xCn?>yG zh0CkvMobl{UoIDG78^-#tQ^n2cKNV{Jq_PJRPE$qs-TmR<)0e-7;fZTG?|~{ovY(~ zmav4q|H0uOje-+n>Jmjz9|$wBrcd4LU@^w(>TdkNC~c+z8*ZFPzr=8QkRgn&-bri^ zYtXb=x-?-no7tAL-3L5YUZ*gQYtfomkH38oGv8Ny?)%+*Lmr_6Olll;=;QP`uW2i2 zPuH9-Qt?vlKH(YHMM-Du?yk_YFurZS4C`j1SonhD()!Fo+cYW1hj;tCEov3x?Lk&7 zN^_Ae{=UqO!ip`4qrvO;C{cDj$3B32sck$Mk?kG0%C+A_+E(kg-!D5;ZET>U3hFB< zS*kvhzw6q)W2wIgr1v8x`)!a%0GBG<|cjTwgpb-nZ)P07-Q?!&O|8> z-d;j?#8(oC8R5Ukw>5U@wY67?lUR##11 z6R%5MEn9!TH;D-D09W{_SGo)~XtWrg@XrhivUUsUkZ)VD)TUp*eN^!t+wZtdzs;=V z(5zp;(uex%`|AxuFI6}E1YFH{Q)2kY{Y{n9`tHoETW}kwDR|AjzAB_c;kI@6TpqmJ z+O@>XxF1O^<&f|L&TA%I2e*pJ6WFN}JY5sjMitR9n)%U$fcuC2c56O(Q?TLA&31TN zy~>HPwkzEeg@3HoHjLC2Mx9v}Sor9tnxh6k)JSvkdu{b|sl$)&FHbx(w4+C7x;DSz zeiv00<~M|YG8=L6;H#}U$De5|G~CfV<=yQNmdlw?$iIKuGtxLAQ#zQXM!59EH{6lg ztHSVVRtYq$Ia_vG*ht;AZx-hm{>|fqe)2sr&qEG}IlPWV$#m?vVvWQ-Z%)a zt_?L!K7l2ElU>&FBx2!3Pix6W7{WW`^tJ2C3PneA$2Z1Jvn;I(e%~~hF?bp{+1jcp z-C?N;1WKE3`3Ch}uG^%vsP?jn zZN9!a{AKgfyFY3QHAH+L;=2589&;PDjz%|)mxSvdAngi`YI@kwbf@)*`08A#VCU1` zL5z|v~KV2gzH=NHCA~@fkVnm13SY=Ad@CrkRmaX6Q{?UdEfvZnAA9!J~ zFG=jBWvqJEy8lWbF?9u5*^Cz%Ivu2dKcM8B@+@r3ruQb#-zr79G;30cten$e6H+hH zP?^Sd&ydbsS2aFbS72Ykd6YAju5+Np;zCvT$_340GqJ9JuXGH00>NnZu67ka1jYXw DCQkmu literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png b/Mods/vcmi/Data/radialMenu/itemInactiveAlt.png new file mode 100644 index 0000000000000000000000000000000000000000..631d8f2b514555df3aaef2c118a566abebe43c88 GIT binary patch literal 1499 zcmV<11tj{3P)EX>4Tx04R}tkv&MmP!xqvTcuJe9qb_DkfG{g9aO}zR-p(LLaorMgUL((ph-iL z;^HW{799LptU9+0Yt2!cN#t}afBE>hxsNufoI4=(THymt=w-3R!aRi>If2|(2> zBa=!Bx%{dSd_@QmstJrq%+%*ZF%8f4bq^ok@8Uem``(|cUn!Uj@Cn2TrW+RV2Jy_M zrE}gVjUPOWv){VBZWmQL4*JqbyQG=g*fdRDJC*>p7ih!Iew8`GPx>X zy{D4^000SaNLh0L z01FcU01FcV0GgZ_000B(Nkl+s6NXN`~H!DwR;G@_^%q82aEi(-roG}csVAH0~eNh!P2+39q5 zW_G?en|5YTrr$k1=lp-?Y)!V@gBNkR{4s-lr@W20WEhanO$g;W=|B??G&n zZ?@w8yvZBJ;~9S#tHByPf$Q=jukk=bios^=M_(S~ZO46z#l5(t+w#`onebX9)`iLK z%5KWrjU9@~t>O8*B5w#!hB2>*^o6C%=1$A2CcS}9$lHaT%EeuwE?-r7gJF>EFPXf{!ovBg)sk1kBN$OmwuEuKwi@!b z;Nf-}FBpx^aedbEQg!PBa&?#t`?HjHdzt~Ypxm8wLQEoWBc4q&l^2wO(EB!IA@2eC z4>_|YQE0lHyy2CUF9oKNQ4TM6Q{1)ZOSm9T%K8u@Xj+dkzFh@TXu2nVNu z;x1(`Zff;!TegrhYc_>SYi%=myKqP4D3+azYy5`1K~ZYbU-^lpKX@8!$g7EV!fvSC zBw_^nP^*i=zkSstk(gSj_ea*?1>9UYX^Sf`P{U#QJ&>4ZF4Sds%(TZiTE`pst8&p6 z6L_nRU-9wcvZErF1-y^3didc3{8D*I#8{)V)faa|(Q*5V*31M?6HCjpWB9uA6UzyF z(X6)p7q8=d<)|s=@M`dMi99%kL)GNblq2|Q@h_Ijo5y?jt#Wk{XM&gcIC<~~-Vr6I z70d=7XO~`RwE;AW?<;SJqW905U1%k53a{bs9v3$iDzJarkq6(kGTuXs;)hljLW}+PAz-Z-u3Ecm-d^-IV0?(_#GBW4-PaK3VRjB=XLOmGyLw zh?@?T)?^m);G4xYUCB8fYFJsx`!A#kWe-jN483k9OL=e_AC>SQ2?h z@N=3srj<7@+HpHmaB;tjwh`ykym=+(j;BHmc&B21$z>KV-O*-3eg(DZCU#Epjq@$5<)%Ib?MMF~wCA?NWd z8q0E)JS6|2X`+y`8}cT@AbcUG;$}i!KABa6{{zKIwjmPxWM=>X002ovPDHLkV1g(V B){_7L literal 0 HcmV?d00001 diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index f4c912f00..585a3477c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -42,6 +42,9 @@ "vcmi.radialWheel.moveUnit" : "Move creatures to another army", "vcmi.radialWheel.splitUnit" : "Split creature to another slot", + "vcmi.radialWheel.townUp" : "Move town up", + "vcmi.radialWheel.townDown" : "Move town down", + "vcmi.mainMenu.serverConnecting" : "Connecting...", "vcmi.mainMenu.serverAddressEnter" : "Enter address:", "vcmi.mainMenu.serverConnectionFailed" : "Failed to connect", diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 6ace13258..9630231e2 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -269,3 +269,9 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) if (currentSelection == nullptr && !ownedTowns.empty()) setSelection(ownedTowns.front()); } + +void PlayerLocalState::swapOwnedTowns(int pos1, int pos2) +{ + assert(ownedTowns[pos1] && ownedTowns[pos2]); + std::swap(ownedTowns[pos1], ownedTowns[pos2]); +} diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index e88ee6ca3..0ace8cf86 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -66,6 +66,7 @@ public: const CGTownInstance * getOwnedTown(size_t index); void addOwnedTown(const CGTownInstance * hero); void removeOwnedTown(const CGTownInstance * hero); + void swapOwnedTowns(int pos1, int pos2); const std::vector & getWanderingHeroes(); const CGHeroInstance * getWanderingHero(size_t index); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 3a9e18280..a78dee13f 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -16,11 +16,13 @@ #include "../widgets/Images.h" #include "../widgets/Buttons.h" #include "../widgets/ObjectLists.h" +#include "../widgets/RadialMenu.h" #include "../windows/InfoWindows.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../PlayerLocalState.h" #include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" #include "../render/Canvas.h" #include "../render/Colors.h" @@ -293,7 +295,7 @@ void CHeroList::updateWidget() for (size_t i = 0; i < heroes.size(); ++i) { - auto item = std::dynamic_pointer_cast(listBox->getItem(i)); + auto item = std::dynamic_pointer_cast(listBox->getItem(i)); if (!item) continue; @@ -324,12 +326,15 @@ std::shared_ptr CTownList::createItem(size_t index) CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): CListItem(parent), + parentList(parent), town(Town) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; update(); + + addUsedEvents(GESTURE); } std::shared_ptr CTownList::CTownItem::genSelection() @@ -361,6 +366,31 @@ void CTownList::CTownItem::showTooltip() CRClickPopup::createAndPush(town, GH.getCursorPosition()); } +void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(!town) + return; + + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + + if(towns.size() < 2) + return; + + int listPos = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), town)); + int townUpperPos = (listPos < 1) ? -1 : listPos - 1; + int townLowerPos = (listPos > towns.size() - 2) ? -1 : listPos + 1; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, listPos, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(listPos, townUpperPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, listPos, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(listPos, townLowerPos); parentList->updateWidget(); } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements, true); +} + std::string CTownList::CTownItem::getHoverText() { return town->getObjectName(); diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 0cefd814e..e56736f9f 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -150,6 +150,7 @@ class CTownList : public CList class CTownItem : public CListItem { std::shared_ptr picture; + CTownList *parentList; public: const CGTownInstance * const town; @@ -160,6 +161,7 @@ class CTownList : public CList void select(bool on) override; void open() override; void showTooltip() override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; std::string getHoverText() override; }; diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index b2e9d44fe..3ceef75e3 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -21,14 +21,14 @@ #include "../../lib/CGeneralTextHandler.h" -RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback) +RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool altLayout) : callback(callback) , hoverText(hoverText) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - inactiveImage = std::make_shared(ImagePath::builtin("radialMenu/itemInactive"), Point(0, 0)); - selectedImage = std::make_shared(ImagePath::builtin("radialMenu/itemEmpty"), Point(0, 0)); + inactiveImage = std::make_shared(ImagePath::builtin(altLayout ? "radialMenu/itemInactiveAlt" : "radialMenu/itemInactive"), Point(0, 0)); + selectedImage = std::make_shared(ImagePath::builtin(altLayout ? "radialMenu/itemEmptyAlt" : "radialMenu/itemEmpty"), Point(0, 0)); iconImage = std::make_shared(ImagePath::builtin("radialMenu/" + imageName), Point(0, 0)); @@ -42,13 +42,13 @@ void RadialMenuItem::setSelected(bool selected) inactiveImage->setEnabled(!selected); } -RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig): - centerPosition(positionToCenter) +RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool altLayout): + centerPosition(positionToCenter), altLayout(altLayout) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos += positionToCenter; - Point itemSize = Point(70, 80); + Point itemSize = altLayout ? Point(80, 70) : Point(70, 80); moveBy(-itemSize / 2); pos.w = itemSize.x; pos.h = itemSize.y; @@ -60,6 +60,7 @@ RadialMenu::RadialMenu(const Point & positionToCenter, const std::vectorpos); + pos = pos.include(statusBar->pos); fitToScreen(10); @@ -71,7 +72,7 @@ void RadialMenu::addItem(const Point & offset, bool enabled, const std::string & if (!enabled) return; - auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback); + auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback, altLayout); item->moveBy(offset); diff --git a/client/widgets/RadialMenu.h b/client/widgets/RadialMenu.h index 854999280..6129d84c3 100644 --- a/client/widgets/RadialMenu.h +++ b/client/widgets/RadialMenu.h @@ -26,6 +26,13 @@ struct RadialMenuConfig static constexpr Point ITEM_SW = Point(-40, +70); static constexpr Point ITEM_SE = Point(+40, +70); + static constexpr Point ITEM_ALT_NN = Point(0, -80); + static constexpr Point ITEM_ALT_SS = Point(0, +80); + static constexpr Point ITEM_ALT_NW = Point(-70, -40); + static constexpr Point ITEM_ALT_SW = Point(-70, +40); + static constexpr Point ITEM_ALT_NE = Point(+70, -40); + static constexpr Point ITEM_ALT_SE = Point(+70, +40); + Point itemPosition; bool enabled; std::string imageName; @@ -44,7 +51,7 @@ class RadialMenuItem : public CIntObject std::string hoverText; public: - RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback); + RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool altLayout); void setSelected(bool selected); }; @@ -60,8 +67,10 @@ class RadialMenu : public CIntObject void addItem(const Point & offset, bool enabled, const std::string & path, const std::string & hoverText, const std::function & callback); std::shared_ptr findNearestItem(const Point & cursorPosition) const; + + bool altLayout; public: - RadialMenu(const Point & positionToCenter, const std::vector & menuConfig); + RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool altLayout = false); void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; From 4528f42cdfa256d204418b0dc6f3a89916b76f33 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:53:19 +0200 Subject: [PATCH 0833/1248] fix segmentation fault; update townlist on adventuremap --- client/PlayerLocalState.cpp | 2 + client/adventureMap/AdventureMapInterface.cpp | 5 +++ client/adventureMap/AdventureMapInterface.h | 3 ++ client/adventureMap/CList.cpp | 38 ++++++++----------- client/adventureMap/CList.h | 2 +- 5 files changed, 27 insertions(+), 23 deletions(-) diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 9630231e2..3f9c62643 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -274,4 +274,6 @@ void PlayerLocalState::swapOwnedTowns(int pos1, int pos2) { assert(ownedTowns[pos1] && ownedTowns[pos2]); std::swap(ownedTowns[pos1], ownedTowns[pos2]); + + adventureInt->onTownOrderChanged(); } diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 376401437..afa9179f8 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -325,6 +325,11 @@ void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) widget->getTownList()->redraw(); } +void AdventureMapInterface::onTownOrderChanged() +{ + widget->getTownList()->updateWidget(); +} + void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) { if (positions) diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 8140d7c82..bd3ec828f 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -146,6 +146,9 @@ public: /// Called when currently selected object changes void onSelectionChanged(const CArmedInstance *sel); + /// Called when town order changes + void onTownOrderChanged(); + /// Called when map audio should be paused, e.g. on combat or town screen access void onAudioPaused(); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index a78dee13f..3216b6c1b 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -326,9 +326,11 @@ std::shared_ptr CTownList::createItem(size_t index) CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): CListItem(parent), - parentList(parent), - town(Town) + parentList(parent) { + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + townPos = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town)); + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); pos = picture->pos; @@ -344,6 +346,7 @@ std::shared_ptr CTownList::CTownItem::genSelection() void CTownList::CTownItem::update() { + const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townPos]; size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture->setFrame(iconIndex + 2); @@ -353,17 +356,17 @@ void CTownList::CTownItem::update() void CTownList::CTownItem::select(bool on) { if(on) - LOCPLINT->localState->setSelection(town); + LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townPos]); } void CTownList::CTownItem::open() { - LOCPLINT->openTownWindow(town); + LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townPos]); } void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(town, GH.getCursorPosition()); + CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townPos], GH.getCursorPosition()); } void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) @@ -371,7 +374,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const if(!on) return; - if(!town) + if(!LOCPLINT->localState->getOwnedTowns()[townPos]) return; const std::vector towns = LOCPLINT->localState->getOwnedTowns(); @@ -379,13 +382,12 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const if(towns.size() < 2) return; - int listPos = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), town)); - int townUpperPos = (listPos < 1) ? -1 : listPos - 1; - int townLowerPos = (listPos > towns.size() - 2) ? -1 : listPos + 1; + int townUpperPos = (townPos < 1) ? -1 : townPos - 1; + int townLowerPos = (townPos > towns.size() - 2) ? -1 : townPos + 1; std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, listPos, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(listPos, townUpperPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, listPos, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(listPos, townLowerPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townPos, townUpperPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townPos, townLowerPos); parentList->updateWidget(); } }, }; GH.windows().createAndPushWindow(pos.center(), menuElements, true); @@ -393,7 +395,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const std::string CTownList::CTownItem::getHoverText() { - return town->getObjectName(); + return LOCPLINT->localState->getOwnedTowns()[townPos]->getObjectName(); } CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount) @@ -420,20 +422,12 @@ void CTownList::updateWidget() for (size_t i = 0; i < towns.size(); ++i) { - auto item = std::dynamic_pointer_cast(listBox->getItem(i)); + auto item = std::dynamic_pointer_cast(listBox->getItem(i)); if (!item) continue; - if (item->town == towns[i]) - { - item->update(); - } - else - { - listBox->reset(); - break; - } + listBox->reset(); } if (LOCPLINT->localState->getCurrentTown()) diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index e56736f9f..6433d8c47 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -152,7 +152,7 @@ class CTownList : public CList std::shared_ptr picture; CTownList *parentList; public: - const CGTownInstance * const town; + int townPos; CTownItem(CTownList *parent, const CGTownInstance * town); From 0401b4a071a593ac80a1582507a0106c0f55aeb3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:34:58 +0200 Subject: [PATCH 0834/1248] adjusted icons --- Mods/vcmi/Data/radialMenu/remove.png | Bin 810 -> 1375 bytes Mods/vcmi/Data/radialMenu/trade.png | Bin 1159 -> 1252 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Mods/vcmi/Data/radialMenu/remove.png b/Mods/vcmi/Data/radialMenu/remove.png index 2a82a74e4c5fcc809bc614fbf112dcf94852b31a..e9cb18d889c7399c6b8fa610aaaaf08d430e5417 100644 GIT binary patch delta 1356 zcmV-S1+)692Hy&hB!7fyLqkwWLqi~Na&Km7Y-IodD3N`UJxIeq9K~N-MJbgI?I0qO zp*po7D&nYBC_;r$E41oha_JW|X-HCB90k{cgCC1k2N!2u9b5%L@B_rv#YxdcO8j3^ zXc6PVaX;SOd)&PP{LLy;&7N^U)hr{EN(#CBs?hg}Ai{`X1b=afnfjb4rr|lh?&0I> zU4mzMpZjwRC4rtTK|H-_>74h8qpU0`#OK6ugDyz?$aUG}H_j!8{X8>j zWHa-`QDULg#c~(3vY`@B5yuo&qkMnPWrgz=XSG^q?R)YUh6>usGS_JiBZWmQL4*Jq zbyQG=g#@h{DSsw1w4e0w4>^93Tr#;TVB}ap4JstZ5B>+gyETiG6K+x{3G}|$_QxpD zw+plyw*7r<+pQBI@C;mO9e=F}%zTpG=xDJcAiND+Tz52i54hX`2A_1vkQ^yM(_btC z?`QN)c_4BN1lQc&+WRQhI1~2{t-MVz>G!R|FVLfeK@bE%5ClOG1VIo4 zK@bE%5Xuf^GPvj3Nnj8dQI@k2K|BSt0@c8*vK-IEcP9)Ak^mk6&w$UKYd1v@wiB2K zCV@+)zCB?uQHFr$`tJr{%yaFQB1G;5-T;+A5`P%?Tsu`ZqIjM@B31xW%A zfu{!u0#FMq3-lB*+&a9mk&11dQpXnlImh4j|JH8i9#GQ_aORa7|gxuUIZ` zx+F${(;1rA%2?%?kmgLiuFdTo9HLqR82Au+)$RYQxIClun2T1 z%h@fPjaKg=UBI$|@N=dFF}>!aEN2b434bg_7&Z;0ObKHAlpL5!Q$T%@p=N;F%5t{L z);b6XQwMwljujYU6*wE!*gdrJ3F;C-jK8J$&{B$0Hkn;YQ5uWJH4%sZ0kr00RK=})+JKJ+1b@l+ zF^h#*n};Tf_EaZ}>>;_R1=1#Zj0x+Y7vE$K(?eDRQzp#jy95j<%UOw?D7uGq;$K6A zF^Fb>e!Z9}T^i$rGpg2sjwmkNq88=6_BSd+9`5c1&oa6sUgKMq`LYSLE6Z7rQ%|V| zj)Xk^-`($Ur!40y(5I(NnB}f2Zhwt-7U;phhsnhv(4#EpXAaw;75|+Fi>NLH3J3Ove^F=>2HbvH!3H6X?QMwJWP zic%2M8|`KMo4cfbG!5J-)OFWJ)d&2``b^1Oj9L)W8|@Tu1^@bD0smfRt3`+mT*1E| z+5~ohHWPxB1kbexJlC#_FkXY_+RX@pAP9mW2!bF8f*=TjAP9o+AM+1pQVH0ch5+yY O0000B!7cxLqkwWLqi~Na&Km7Y-IodD3N`UJxIeq9K~PLN~Kgf*g?c0 zL)FDDD&knHP=pGhR%q412R|084ld5RI=Bjg;0K7Si<6>@l=#1- z&?3fz<9@um_qclp2(2p9%s>Lrbj!%3l435uDh6K>LIgkn<9{+U>vNKnhUfUYhmWs! zah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G=e$oGWo20eDdehvkz)ZhXpkL0_#gc4)+|m&aJd5vJ=JAHb|gPdp;!do z&*+=-z~C(qT625r?BnzS$WmA78{ps&7%Nfsy2rcwdw+ZT_e`h1A4Ljslsn7EM*si- z24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0003yNklPsokzb6l4ONW5n2JYr;gAlSA0000000000KvlNaV;fb&v(5VbIh=+o zo?f)+Y<~?`z0Ogl6&kMmzOR~AXoQ8*vf!OHtG zEn9NoP$=Fg^EnXGI+!Vth+t?(MutM;S`f(4R~%f134)p8+BDMiR2S?Krl%amxMF(h zp`A0Pml*GAF}>6v;1SbHj*R{?z4UPLC(|R2p{FtO4+{VQ00000007X&8yQg%U4OYP RhztM#002ovPDHLkV1mX@XW#$; diff --git a/Mods/vcmi/Data/radialMenu/trade.png b/Mods/vcmi/Data/radialMenu/trade.png index b5a109f5b3981ca71145670d89a497acf12a1746..54df71506070548f6917a360585cd84dbfc4282e 100644 GIT binary patch delta 1232 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 Ef)%D8y#N3J From aa0b06415437d2f300adc6c8436652b7a779df55 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 29 Sep 2023 15:55:31 +0300 Subject: [PATCH 0835/1248] Fixed randomization of artifacts on some custom maps --- lib/CArtHandler.cpp | 140 +++++++++++++++------------------------- lib/CArtHandler.h | 16 ++--- lib/NetPacks.h | 7 +- lib/NetPacksLib.cpp | 5 +- server/CGameHandler.cpp | 5 +- 5 files changed, 60 insertions(+), 113 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b991d7dda..b297198f3 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -609,49 +609,60 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) { - auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) + std::set potentialPicks; + + // Select artifacts that satisfy provided criterias + for (auto const * artifact : allowedArtifacts) { - if (arts->empty()) //restock available arts - fillList(*arts, flag); + assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized - for (auto & arts_i : *arts) - { - if (accepts(arts_i->id)) - { - CArtifact *art = arts_i; - out.emplace_back(art); - } - } - }; + if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) + continue; - auto getAllowed = [&](std::vector > &out) + if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) + continue; + + if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) + continue; + + if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) + continue; + + if (!accepts(artifact->id)) + continue; + + potentialPicks.insert(artifact->id); + } + + return pickRandomArtifact(rand, potentialPicks); +} + +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +{ + // No allowed artifacts at all - give Grail - this can't be banned (hopefully) + // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior + if (potentialPicks.empty()) { - if (flags & CArtifact::ART_TREASURE) - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - if (flags & CArtifact::ART_MINOR) - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - if (flags & CArtifact::ART_MAJOR) - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - if (flags & CArtifact::ART_RELIC) - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - if(out.empty()) //no artifact of specified rarity, we need to take another one - { - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - } - if(out.empty()) //no arts are available at all - { - out.resize (64); - std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully) - } - }; + logGlobal->warn("Failed to find artifact that matches requested parameters!"); + return ArtifactID::GRAIL; + } - std::vector > out; - getAllowed(out); - ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; - erasePickedArt(artID); + // Find how many times least used artifacts were picked by randomizer + int leastUsedTimes = std::numeric_limits::max(); + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] < leastUsedTimes) + leastUsedTimes = allocatedArtifacts[artifact]; + + // Pick all artifacts that were used least number of times + std::set preferredPicks; + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] == leastUsedTimes) + preferredPicks.insert(artifact); + + assert(!preferredPicks.empty()); + + ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); + allocatedArtifacts[artID] += 1; // record +1 more usage return artID; } @@ -712,16 +723,13 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) { allowedArtifacts.clear(); - treasures.clear(); - minors.clear(); - majors.clear(); - relics.clear(); + allocatedArtifacts.clear(); for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) { if (allowed[i] && legalArtifact(ArtifactID(i))) allowedArtifacts.push_back(objects[i]); - //keep im mind that artifact can be worn by more than one type of bearer + //keep im mind that artifact can be worn by more than one type of bearer } } @@ -734,52 +742,6 @@ std::vector CArtHandler::getDefaultAllowed() const return allowedArtifacts; } -void CArtHandler::erasePickedArt(const ArtifactID & id) -{ - CArtifact *art = objects[id]; - - std::vector * artifactList = nullptr; - switch(art->aClass) - { - case CArtifact::ART_TREASURE: - artifactList = &treasures; - break; - case CArtifact::ART_MINOR: - artifactList = &minors; - break; - case CArtifact::ART_MAJOR: - artifactList = &majors; - break; - case CArtifact::ART_RELIC: - artifactList = &relics; - break; - default: - logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated()); - return; - } - - if(artifactList->empty()) - fillList(*artifactList, art->aClass); - - auto itr = vstd::find(*artifactList, art); - if(itr != artifactList->end()) - { - artifactList->erase(itr); - } - else - logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated()); -} - -void CArtHandler::fillList( std::vector &listToBeFilled, CArtifact::EartClass artifactClass ) -{ - assert(listToBeFilled.empty()); - for (auto & elem : allowedArtifacts) - { - if (elem->aClass == artifactClass) - listToBeFilled.push_back(elem); - } -} - void CArtHandler::afterLoadFinalization() { //All artifacts have their id, so we can properly update their bonuses' source ids. diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 9f592a5f3..b3846e304 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -172,21 +172,21 @@ public: class DLL_LINKAGE CArtHandler : public CHandlerBase { public: - std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! + /// Stores number of times each artifact was placed on map via randomization + std::map allocatedArtifacts; + /// List of artifacts allowed on the map std::vector allowedArtifacts; - std::set growingArtifacts; void addBonuses(CArtifact *art, const JsonNode &bonusList); - void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects - static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor /// Gets a artifact ID randomly and removes the selected artifact from this handler. ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); bool legalArtifact(const ArtifactID & id); void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed @@ -207,11 +207,7 @@ public: { h & objects; h & allowedArtifacts; - h & treasures; - h & minors; - h & majors; - h & relics; - h & growingArtifacts; + h & allocatedArtifacts; } protected: @@ -224,8 +220,6 @@ private: void loadClass(CArtifact * art, const JsonNode & node) const; void loadType(CArtifact * art, const JsonNode & node) const; void loadComponents(CArtifact * art, const JsonNode & node); - - void erasePickedArt(const ArtifactID & id); }; struct DLL_LINKAGE ArtSlotInfo diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 132e2a94e..2992e7be6 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -556,17 +556,14 @@ struct DLL_LINKAGE AddQuest : public CPackForClient struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient { - std::vector treasures, minors, majors, relics; + std::map allocatedArtifacts; void applyGs(CGameState * gs) const; virtual void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { - h & treasures; - h & minors; - h & majors; - h & relics; + h & allocatedArtifacts; } }; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 79d8ec0c9..c56928a3c 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -848,10 +848,7 @@ void AddQuest::applyGs(CGameState * gs) const void UpdateArtHandlerLists::applyGs(CGameState * gs) const { - VLC->arth->minors = minors; - VLC->arth->majors = majors; - VLC->arth->treasures = treasures; - VLC->arth->relics = relics; + VLC->arth->allocatedArtifacts = allocatedArtifacts; } void UpdateMapEvents::applyGs(CGameState * gs) const diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 88eeda6d0..27412a5e2 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4061,10 +4061,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; - uahl.treasures = VLC->arth->treasures; - uahl.minors = VLC->arth->minors; - uahl.majors = VLC->arth->majors; - uahl.relics = VLC->arth->relics; + uahl.allocatedArtifacts = VLC->arth->allocatedArtifacts; sendAndApply(&uahl); } From 79b7518b0e12b5fea98001036ba051af10e91061 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 29 Sep 2023 16:18:57 +0300 Subject: [PATCH 0836/1248] Unified Json randomization logic for all entity types --- lib/JsonRandom.cpp | 310 ++++++++++++++++++---------- lib/JsonRandom.h | 2 +- lib/constants/EntityIdentifiers.cpp | 10 + lib/constants/EntityIdentifiers.h | 3 + 4 files changed, 214 insertions(+), 111 deletions(-) diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 95ce1f4f4..f33bc85df 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -56,31 +56,156 @@ namespace JsonRandom return defaultValue; } - std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet) + template + IdentifierType decodeKey(const JsonNode & value) + { + return IdentifierType(*VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value)); + } + + template<> + PrimarySkill decodeKey(const JsonNode & value) + { + return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); + } + + /// Method that allows type-specific object filtering + /// Default implementation is to accept all input objects + template + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + { + return valuesSet; + } + + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + { + assert(value.isStruct()); + + std::set allowedClasses; + std::set allowedPositions; + ui32 minValue = 0; + ui32 maxValue = std::numeric_limits::max(); + + if (value["class"].getType() == JsonNode::JsonType::DATA_STRING) + allowedClasses.insert(CArtHandler::stringToClass(value["class"].String())); + else + for(const auto & entry : value["class"].Vector()) + allowedClasses.insert(CArtHandler::stringToClass(entry.String())); + + if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING) + allowedPositions.insert(ArtifactPosition::decode(value["class"].String())); + else + for(const auto & entry : value["slot"].Vector()) + allowedPositions.insert(ArtifactPosition::decode(entry.String())); + + if (!value["minValue"].isNull()) + minValue = static_cast(value["minValue"].Float()); + if (!value["maxValue"].isNull()) + maxValue = static_cast(value["maxValue"].Float()); + + std::set result; + + for (auto const & artID : valuesSet) + { + CArtifact * art = VLC->arth->objects[artID]; + + if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) + continue; + + if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) + continue; + + if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) + continue; + + if(!allowedPositions.empty()) + { + bool positionAllowed = false; + for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO)) + { + if(allowedPositions.count(pos)) + positionAllowed = true; + } + + if (!positionAllowed) + continue; + } + + result.insert(artID); + } + return result; + } + + template<> + std::set filterKeysTyped(const JsonNode & value, const std::set & valuesSet) + { + std::set result = valuesSet; + + if (!value["level"].isNull()) + { + int32_t spellLevel = value["level"].Float(); + + vstd::erase_if(result, [=](const SpellID & spell) + { + return VLC->spellh->getById(spell)->getLevel() != spellLevel; + }); + } + + if (!value["school"].isNull()) + { + int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value(); + + vstd::erase_if(result, [=](const SpellID & spell) + { + return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID)); + }); + } + return result; + } + + template + std::set filterKeys(const JsonNode & value, const std::set & valuesSet) { if(value.isString()) - return value.String(); - + return { decodeKey(value) }; + + assert(value.isStruct()); + if(value.isStruct()) { if(!value["type"].isNull()) - return value["type"].String(); + return filterKeys(value["type"], valuesSet); + + std::set filteredTypes = filterKeysTyped(value, valuesSet); if(!value["anyOf"].isNull()) - return RandomGeneratorUtil::nextItem(value["anyOf"].Vector(), rng)->String(); - + { + std::set filteredAnyOf; + for (auto const & entry : value["anyOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet); + filteredAnyOf.insert(subset.begin(), subset.end()); + } + + vstd::erase_if(filteredTypes, [&](const IdentifierType & value) + { + return filteredAnyOf.count(value) == 0; + }); + } + if(!value["noneOf"].isNull()) { - auto copyValuesSet = valuesSet; - for(auto & s : value["noneOf"].Vector()) - copyValuesSet.erase(s.String()); - - if(!copyValuesSet.empty()) - return *RandomGeneratorUtil::nextItem(copyValuesSet, rng); + for (auto const & entry : value["noneOf"].Vector()) + { + std::set subset = filterKeys(entry, valuesSet); + for (auto bannedEntry : subset ) + filteredTypes.erase(bannedEntry); + } } + + return filteredTypes; } - - return valuesSet.empty() ? "" : *RandomGeneratorUtil::nextItem(valuesSet, rng); + return valuesSet; } TResources loadResources(const JsonNode & value, CRandomGenerator & rng) @@ -103,11 +228,19 @@ namespace JsonRandom TResources loadResource(const JsonNode & value, CRandomGenerator & rng) { - std::set defaultResources(std::begin(GameConstants::RESOURCE_NAMES), std::end(GameConstants::RESOURCE_NAMES) - 1); //except mithril - - std::string resourceName = loadKey(value, rng, defaultResources); + std::set defaultResources{ + GameResID::WOOD, + GameResID::MERCURY, + GameResID::ORE, + GameResID::SULFUR, + GameResID::CRYSTAL, + GameResID::GEMS, + GameResID::GOLD + }; + + std::set potentialPicks = filterKeys(value, defaultResources); + GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); si32 resourceAmount = loadValue(value, rng, 0); - si32 resourceID(VLC->identifiers()->getIdentifier(value.meta, "resource", resourceName).value()); TResources ret; ret[resourceID] = resourceAmount; @@ -127,15 +260,22 @@ namespace JsonRandom } if(value.isVector()) { + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + ret.resize(GameConstants::PRIMARY_SKILLS, 0); - std::set defaultStats(std::begin(NPrimarySkill::names), std::end(NPrimarySkill::names)); + for(const auto & element : value.Vector()) { - auto key = loadKey(element, rng, defaultStats); - defaultStats.erase(key); - int id = vstd::find_pos(NPrimarySkill::names, key); - if(id != -1) - ret[id] += loadValue(element, rng); + std::set potentialPicks = filterKeys(element, defaultSkills); + PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); + ret[static_cast(skillID)] += loadValue(element, rng); } } return ret; @@ -154,26 +294,18 @@ namespace JsonRandom } if(value.isVector()) { - std::set defaultSkills; + std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) - { - IObjectInterface::cb->isAllowed(2, skill->getIndex()); - auto scopeAndName = vstd::splitStringToPair(skill->getJsonKey(), ':'); - if(scopeAndName.first == ModScope::scopeBuiltin() || scopeAndName.first == value.meta) - defaultSkills.insert(scopeAndName.second); - else - defaultSkills.insert(skill->getJsonKey()); - } - + if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + defaultSkills.insert(skill->getId()); + for(const auto & element : value.Vector()) { - auto key = loadKey(element, rng, defaultSkills); - defaultSkills.erase(key); //avoid dupicates - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "skill", key)) - { - SecondarySkill id(identifier.value()); - ret[id] = loadValue(element, rng); - } + std::set potentialPicks = filterKeys(element, defaultSkills); + SecondarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + + defaultSkills.erase(skillID); //avoid dupicates + ret[skillID] = loadValue(element, rng); } } return ret; @@ -181,53 +313,13 @@ namespace JsonRandom ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return ArtifactID(VLC->identifiers()->getIdentifier("artifact", value).value()); + std::set allowedArts; + for (auto const * artifact : VLC->arth->allowedArtifacts) + allowedArts.insert(artifact->getId()); - std::set allowedClasses; - std::set allowedPositions; - ui32 minValue = 0; - ui32 maxValue = std::numeric_limits::max(); + std::set potentialPicks = filterKeys(value, allowedArts); - if (value["class"].getType() == JsonNode::JsonType::DATA_STRING) - allowedClasses.insert(CArtHandler::stringToClass(value["class"].String())); - else - for(const auto & entry : value["class"].Vector()) - allowedClasses.insert(CArtHandler::stringToClass(entry.String())); - - if (value["slot"].getType() == JsonNode::JsonType::DATA_STRING) - allowedPositions.insert(ArtifactPosition::decode(value["class"].String())); - else - for(const auto & entry : value["slot"].Vector()) - allowedPositions.insert(ArtifactPosition::decode(entry.String())); - - if (!value["minValue"].isNull()) minValue = static_cast(value["minValue"].Float()); - if (!value["maxValue"].isNull()) maxValue = static_cast(value["maxValue"].Float()); - - return VLC->arth->pickRandomArtifact(rng, [=](const ArtifactID & artID) -> bool - { - CArtifact * art = VLC->arth->objects[artID]; - - if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) - return false; - - if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) - return false; - - if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) - return false; - - if(!allowedPositions.empty()) - { - for(const auto & pos : art->getPossibleSlots().at(ArtBearer::HERO)) - { - if(allowedPositions.count(pos)) - return true; - } - return false; - } - return true; - }); + return VLC->arth->pickRandomArtifact(rng, potentialPicks); } std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng) @@ -242,35 +334,19 @@ namespace JsonRandom SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells) { - if (value.getType() == JsonNode::JsonType::DATA_STRING) - return SpellID(VLC->identifiers()->getIdentifier("spell", value).value()); + std::set defaultSpells; + for(const auto & spell : VLC->spellh->objects) + if (IObjectInterface::cb->isAllowed(0, spell->getIndex())) + defaultSpells.insert(spell->getId()); - if (!value["level"].isNull()) - { - int32_t spellLevel = value["level"].Float(); + std::set potentialPicks = filterKeys(value, defaultSpells); - vstd::erase_if(spells, [=](const SpellID & spell) - { - return VLC->spellh->getById(spell)->getLevel() != spellLevel; - }); - } - - if (!value["school"].isNull()) - { - int32_t schoolID = VLC->identifiers()->getIdentifier("spellSchool", value["school"]).value(); - - vstd::erase_if(spells, [=](const SpellID & spell) - { - return !VLC->spellh->getById(spell)->hasSchool(SpellSchool(schoolID)); - }); - } - - if (spells.empty()) + if (potentialPicks.empty()) { logMod->warn("Failed to select suitable random spell!"); return SpellID::NONE; } - return SpellID(*RandomGeneratorUtil::nextItem(spells, rng)); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells) @@ -326,7 +402,21 @@ namespace JsonRandom CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) { CStackBasicDescriptor stack; - stack.type = VLC->creh->objects[VLC->identifiers()->getIdentifier("creature", value["type"]).value()]; + + std::set defaultCreatures; + for(const auto & creature : VLC->creh->objects) + if (!creature->special) + defaultCreatures.insert(creature->getId()); + + std::set potentialPicks = filterKeys(value, defaultCreatures); + CreatureID pickedCreature; + + if (!potentialPicks.empty()) + pickedCreature = *RandomGeneratorUtil::nextItem(potentialPicks, rng); + else + logMod->warn("Failed to select suitable random creature!"); + + stack.type = VLC->creh->objects[pickedCreature]; stack.count = loadValue(value, rng); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index ef066d14e..101aa4a2d 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -32,7 +32,7 @@ namespace JsonRandom }; DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0); - DLL_LINKAGE std::string loadKey(const JsonNode & value, CRandomGenerator & rng, const std::set & valuesSet = {}); + DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng); diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index eaa790c9b..4c8e86d15 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -323,4 +323,14 @@ const ObstacleInfo * Obstacle::getInfo() const return VLC->obstacles()->getById(*this); } +std::string GameResID::entityType() +{ + return "resource"; +} + +std::string SecondarySkill::entityType() +{ + return "secondarySkill"; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7ca9a1333..4b86f185a 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -347,6 +347,7 @@ class SecondarySkill : public IdentifierWithEnum::IdentifierWithEnum; + static std::string entityType(); }; class DLL_LINKAGE FactionID : public Identifier @@ -945,6 +946,8 @@ class GameResID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + static std::string entityType(); }; // Deprecated From fd01a2535286cc5ed745f430b3f4dbc52bb049b9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 30 Sep 2023 18:47:47 +0300 Subject: [PATCH 0837/1248] Implemented basic version of configurable Witch Hut --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 11 +- AI/Nullkiller/Engine/PriorityEvaluator.h | 4 +- AI/VCAI/ResourceManager.cpp | 4 +- config/gameConfig.json | 1 + config/objects/generic.json | 20 --- docs/modders/Map_Objects/Rewardable.md | 2 +- lib/CSkillHandler.h | 2 +- lib/JsonRandom.cpp | 137 +++++++++++------- lib/JsonRandom.h | 28 ++-- lib/constants/EntityIdentifiers.h | 5 +- .../CBankInstanceConstructor.cpp | 26 ++-- .../CObjectClassesHandler.cpp | 1 - .../CRewardableConstructor.h | 3 +- .../CommonConstructors.cpp | 4 +- .../DwellingInstanceConstructor.cpp | 3 +- .../ShrineInstanceConstructor.cpp | 9 +- lib/mapObjects/CGHeroInstance.cpp | 8 +- lib/mapObjects/CRewardableObject.h | 1 - lib/mapObjects/MiscObjects.cpp | 92 ------------ lib/mapObjects/MiscObjects.h | 20 --- lib/mapping/MapFormatH3M.cpp | 31 +++- lib/mapping/MapFormatH3M.h | 2 +- lib/registerTypes/RegisterTypes.h | 2 - lib/rewardable/Configuration.cpp | 37 +++++ lib/rewardable/Configuration.h | 28 +++- lib/rewardable/Info.cpp | 126 ++++++++++------ lib/rewardable/Info.h | 3 +- lib/rewardable/Interface.h | 2 - 28 files changed, 316 insertions(+), 296 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 1888c876a..eda7e7faa 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -528,13 +528,16 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons } } -float RewardEvaluator::evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const +float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const { + auto rewardable = dynamic_cast(hut); + assert(rewardable); + + auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill")); + if(!hut->wasVisited(hero->tempOwner)) return role == HeroRole::SCOUT ? 2 : 0; - auto skill = SecondarySkill(hut->ability); - if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) return 0; @@ -575,7 +578,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH case Obj::LIBRARY_OF_ENLIGHTENMENT: return 8; case Obj::WITCH_HUT: - return evaluateWitchHutSkillScore(dynamic_cast(target), hero, role); + return evaluateWitchHutSkillScore(target, hero, role); case Obj::PANDORAS_BOX: //Can contains experience, spells, or skills (only on custom maps) return 2.5f; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.h b/AI/Nullkiller/Engine/PriorityEvaluator.h index 4853e4aed..beccbef62 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.h +++ b/AI/Nullkiller/Engine/PriorityEvaluator.h @@ -18,8 +18,6 @@ VCMI_LIB_NAMESPACE_BEGIN -class CGWitchHut; - VCMI_LIB_NAMESPACE_END namespace NKAI @@ -43,7 +41,7 @@ public: float getResourceRequirementStrength(int resType) const; float getStrategicalValue(const CGObjectInstance * target) const; float getTotalResourceRequirementStrength(int resType) const; - float evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const; + float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const; float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const; int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const; uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const; diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index c8c3f3a33..8a97e430f 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -90,7 +90,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o { auto allResources = cb->getResourceAmount(); auto income = estimateIncome(); - GameResID resourceType = EGameResID::INVALID; + GameResID resourceType = EGameResID::NONE; TResource amountToCollect = 0; using resPair = std::pair; @@ -129,7 +129,7 @@ Goals::TSubgoal ResourceManager::collectResourcesForOurGoal(ResourceObjective &o break; } } - if (resourceType == EGameResID::INVALID) //no needed resources has 0 income, + if (resourceType == EGameResID::NONE) //no needed resources has 0 income, { //find the one which takes longest to collect using timePair = std::pair; diff --git a/config/gameConfig.json b/config/gameConfig.json index f80ff5a43..bed5db522 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -52,6 +52,7 @@ "config/objects/moddables.json", "config/objects/creatureBanks.json", "config/objects/dwellings.json", + "config/objects/rewardableGeneric.json", "config/objects/rewardableOncePerWeek.json", "config/objects/rewardablePickable.json", "config/objects/rewardableOnceVisitable.json", diff --git a/config/objects/generic.json b/config/objects/generic.json index a32cb0dda..0060d02e2 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -587,26 +587,6 @@ } } }, - "witchHut" : { - "index" :113, - "handler" : "witch", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "zoneLimit" : 3, - "value" : 1500, - "rarity" : 80 - } - } - } - }, "questGuard" : { "index" :215, "handler" : "questGuard", diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index 9be9b6fcd..c1030a738 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -220,7 +220,7 @@ Keep in mind, that all randomization is performed on map load and on object rese ```jsonc "resources": [ { - "list" : [ "wood", "ore" ], + "anyOf" : [ "wood", "ore" ], "amount" : 10 }, { diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index da5c8777b..72750fe8c 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -48,7 +48,7 @@ private: std::string identifier; public: - CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); + CSkill(const SecondarySkill & id = SecondarySkill::NONE, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); ~CSkill() = default; enum class Obligatory : ui8 diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index f33bc85df..724ba3378 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -32,38 +32,71 @@ VCMI_LIB_NAMESPACE_BEGIN namespace JsonRandom { - si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue) + si32 loadVariable(std::string variableGroup, const std::string & value, const Variables & variables, si32 defaultValue) + { + if (value.empty() || value[0] != '@') + { + logMod->warn("Invalid syntax in load value! Can not load value from '%s'", value); + return defaultValue; + } + + std::string variableID = variableGroup + value; + + if (variables.count(variableID) == 0) + { + logMod->warn("Invalid syntax in load value! Unknown variable '%s'", value); + return defaultValue; + } + return variables.at(variableID); + } + + si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue) { if(value.isNull()) return defaultValue; if(value.isNumber()) return static_cast(value.Float()); + if (value.isString()) + return loadVariable("number", value.String(), variables, defaultValue); + if(value.isVector()) { const auto & vector = value.Vector(); size_t index= rng.getIntRange(0, vector.size()-1)(); - return loadValue(vector[index], rng, 0); + return loadValue(vector[index], rng, variables, 0); } if(value.isStruct()) { if (!value["amount"].isNull()) - return static_cast(loadValue(value["amount"], rng, defaultValue)); - si32 min = static_cast(loadValue(value["min"], rng, 0)); - si32 max = static_cast(loadValue(value["max"], rng, 0)); + return static_cast(loadValue(value["amount"], rng, variables, defaultValue)); + si32 min = static_cast(loadValue(value["min"], rng, variables, 0)); + si32 max = static_cast(loadValue(value["max"], rng, variables, 0)); return rng.getIntRange(min, max)(); } return defaultValue; } template - IdentifierType decodeKey(const JsonNode & value) + IdentifierType decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) { - return IdentifierType(*VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value)); + if (value.empty() || value[0] != '@') + return IdentifierType(*VLC->identifiers()->getIdentifier(modScope, IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value, variables, IdentifierType::NONE); + } + + template + IdentifierType decodeKey(const JsonNode & value, const Variables & variables) + { + if (value.String().empty() || value.String()[0] != '@') + return IdentifierType(*VLC->identifiers()->getIdentifier(IdentifierType::entityType(), value)); + else + return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE); } template<> - PrimarySkill decodeKey(const JsonNode & value) + PrimarySkill decodeKey(const JsonNode & value, const Variables & variables) { return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); } @@ -143,7 +176,7 @@ namespace JsonRandom if (!value["level"].isNull()) { - int32_t spellLevel = value["level"].Float(); + int32_t spellLevel = value["level"].Integer(); vstd::erase_if(result, [=](const SpellID & spell) { @@ -164,17 +197,17 @@ namespace JsonRandom } template - std::set filterKeys(const JsonNode & value, const std::set & valuesSet) + std::set filterKeys(const JsonNode & value, const std::set & valuesSet, const Variables & variables) { if(value.isString()) - return { decodeKey(value) }; + return { decodeKey(value, variables) }; assert(value.isStruct()); if(value.isStruct()) { if(!value["type"].isNull()) - return filterKeys(value["type"], valuesSet); + return filterKeys(value["type"], valuesSet, variables); std::set filteredTypes = filterKeysTyped(value, valuesSet); @@ -183,7 +216,7 @@ namespace JsonRandom std::set filteredAnyOf; for (auto const & entry : value["anyOf"].Vector()) { - std::set subset = filterKeys(entry, valuesSet); + std::set subset = filterKeys(entry, valuesSet, variables); filteredAnyOf.insert(subset.begin(), subset.end()); } @@ -197,7 +230,7 @@ namespace JsonRandom { for (auto const & entry : value["noneOf"].Vector()) { - std::set subset = filterKeys(entry, valuesSet); + std::set subset = filterKeys(entry, valuesSet, variables); for (auto bannedEntry : subset ) filteredTypes.erase(bannedEntry); } @@ -208,25 +241,25 @@ namespace JsonRandom return valuesSet; } - TResources loadResources(const JsonNode & value, CRandomGenerator & rng) + TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { TResources ret; if (value.isVector()) { for (const auto & entry : value.Vector()) - ret += loadResource(entry, rng); + ret += loadResource(entry, rng, variables); return ret; } for (size_t i=0; i defaultResources{ GameResID::WOOD, @@ -238,9 +271,9 @@ namespace JsonRandom GameResID::GOLD }; - std::set potentialPicks = filterKeys(value, defaultResources); + std::set potentialPicks = filterKeys(value, defaultResources, variables); GameResID resourceID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); - si32 resourceAmount = loadValue(value, rng, 0); + si32 resourceAmount = loadValue(value, rng, variables, 0); TResources ret; ret[resourceID] = resourceAmount; @@ -248,14 +281,14 @@ namespace JsonRandom } - std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng) + std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; if(value.isStruct()) { for(const auto & name : NPrimarySkill::names) { - ret.push_back(loadValue(value[name], rng)); + ret.push_back(loadValue(value[name], rng, variables)); } } if(value.isVector()) @@ -271,25 +304,36 @@ namespace JsonRandom for(const auto & element : value.Vector()) { - std::set potentialPicks = filterKeys(element, defaultSkills); + std::set potentialPicks = filterKeys(element, defaultSkills, variables); PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); defaultSkills.erase(skillID); - ret[static_cast(skillID)] += loadValue(element, rng); + ret[static_cast(skillID)] += loadValue(element, rng, variables); } } return ret; } - std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng) + SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills; + for(const auto & skill : VLC->skillh->objects) + if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + defaultSkills.insert(skill->getId()); + + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } + + std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::map ret; if(value.isStruct()) { for(const auto & pair : value.Struct()) { - SecondarySkill id(VLC->identifiers()->getIdentifier(pair.second.meta, "skill", pair.first).value()); - ret[id] = loadValue(pair.second, rng); + SecondarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[id] = loadValue(pair.second, rng, variables); } } if(value.isVector()) @@ -301,45 +345,45 @@ namespace JsonRandom for(const auto & element : value.Vector()) { - std::set potentialPicks = filterKeys(element, defaultSkills); + std::set potentialPicks = filterKeys(element, defaultSkills, variables); SecondarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); defaultSkills.erase(skillID); //avoid dupicates - ret[skillID] = loadValue(element, rng); + ret[skillID] = loadValue(element, rng, variables); } } return ret; } - ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng) + ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set allowedArts; for (auto const * artifact : VLC->arth->allowedArtifacts) allowedArts.insert(artifact->getId()); - std::set potentialPicks = filterKeys(value, allowedArts); + std::set potentialPicks = filterKeys(value, allowedArts, variables); return VLC->arth->pickRandomArtifact(rng, potentialPicks); } - std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng) + std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) { - ret.push_back(loadArtifact(entry, rng)); + ret.push_back(loadArtifact(entry, rng, variables)); } return ret; } - SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells) + SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::set defaultSpells; for(const auto & spell : VLC->spellh->objects) if (IObjectInterface::cb->isAllowed(0, spell->getIndex())) defaultSpells.insert(spell->getId()); - std::set potentialPicks = filterKeys(value, defaultSpells); + std::set potentialPicks = filterKeys(value, defaultSpells, variables); if (potentialPicks.empty()) { @@ -349,12 +393,12 @@ namespace JsonRandom return *RandomGeneratorUtil::nextItem(potentialPicks, rng); } - std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells) + std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & entry : value.Vector()) { - ret.push_back(loadSpell(entry, rng, spells)); + ret.push_back(loadSpell(entry, rng, variables)); } return ret; } @@ -399,7 +443,7 @@ namespace JsonRandom return ret; } - CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng) + CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { CStackBasicDescriptor stack; @@ -408,7 +452,7 @@ namespace JsonRandom if (!creature->special) defaultCreatures.insert(creature->getId()); - std::set potentialPicks = filterKeys(value, defaultCreatures); + std::set potentialPicks = filterKeys(value, defaultCreatures, variables); CreatureID pickedCreature; if (!potentialPicks.empty()) @@ -417,7 +461,7 @@ namespace JsonRandom logMod->warn("Failed to select suitable random creature!"); stack.type = VLC->creh->objects[pickedCreature]; - stack.count = loadValue(value, rng); + stack.count = loadValue(value, rng, variables); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade @@ -428,17 +472,17 @@ namespace JsonRandom return stack; } - std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng) + std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) { - ret.push_back(loadCreature(node, rng)); + ret.push_back(loadCreature(node, rng, variables)); } return ret; } - std::vector evaluateCreatures(const JsonNode & value) + std::vector evaluateCreatures(const JsonNode & value, const Variables & variables) { std::vector ret; for (const JsonNode & node : value.Vector()) @@ -464,13 +508,6 @@ namespace JsonRandom return ret; } - //std::vector loadComponents(const JsonNode & value) - //{ - // std::vector ret; - // return ret; - // //TODO - //} - std::vector DLL_LINKAGE loadBonuses(const JsonNode & value) { std::vector ret; diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 101aa4a2d..6623ab86f 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -24,6 +24,8 @@ class CStackBasicDescriptor; namespace JsonRandom { + using Variables = std::map; + struct DLL_LINKAGE RandomStackInfo { std::vector allowedCreatures; @@ -31,29 +33,29 @@ namespace JsonRandom si32 maxAmount; }; - DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, si32 defaultValue = 0); + DLL_LINKAGE si32 loadValue(const JsonNode & value, CRandomGenerator & rng, const Variables & variables, si32 defaultValue = 0); - DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::map loadSecondary(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, std::vector spells = {}); - DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const std::vector & spells = {}); + DLL_LINKAGE SpellID loadSpell(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadSpells(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng); - DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value); + DLL_LINKAGE CStackBasicDescriptor loadCreature(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadBonuses(const JsonNode & value); - //DLL_LINKAGE std::vector loadComponents(const JsonNode & value); } VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 4b86f185a..f48be1217 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -308,8 +308,7 @@ class SecondarySkillBase : public IdentifierBase public: enum Type : int32_t { - WRONG = -2, - DEFAULT = -1, + NONE = -1, PATHFINDING = 0, ARCHERY, LOGISTICS, @@ -938,7 +937,7 @@ public: COUNT, WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 + NONE = -1 }; }; diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp index ed10802c3..f61641a85 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.cpp @@ -37,17 +37,15 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input) BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const { BankConfig bc; + JsonRandom::Variables emptyVariables; bc.chance = static_cast(level["chance"].Float()); - bc.guards = JsonRandom::loadCreatures(level["guards"], rng); - - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + bc.guards = JsonRandom::loadCreatures(level["guards"], rng, emptyVariables); bc.resources = ResourceSet(level["reward"]["resources"]); - bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng); - bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng); - bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, spells); + bc.creatures = JsonRandom::loadCreatures(level["reward"]["creatures"], rng, emptyVariables); + bc.artifacts = JsonRandom::loadArtifacts(level["reward"]["artifacts"], rng, emptyVariables); + bc.spells = JsonRandom::loadSpells(level["reward"]["spells"], rng, emptyVariables); return bc; } @@ -105,10 +103,12 @@ static void addStackToArmy(IObjectInfo::CArmyStructure & army, const CCreature * IObjectInfo::CArmyStructure CBankInfo::minGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -126,10 +126,12 @@ IObjectInfo::CArmyStructure CBankInfo::minGuards() const IObjectInfo::CArmyStructure CBankInfo::maxGuards() const { + JsonRandom::Variables emptyVariables; + std::vector armies; for(auto configEntry : config) { - auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"]); + auto stacks = JsonRandom::evaluateCreatures(configEntry["guards"], emptyVariables); IObjectInfo::CArmyStructure army; for(auto & stack : stacks) { @@ -147,12 +149,13 @@ IObjectInfo::CArmyStructure CBankInfo::maxGuards() const TPossibleGuards CBankInfo::getPossibleGuards() const { + JsonRandom::Variables emptyVariables; TPossibleGuards out; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["guards"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); IObjectInfo::CArmyStructure army; @@ -187,12 +190,13 @@ std::vector> CBankInfo::getPossibleResourcesReward() std::vector> CBankInfo::getPossibleCreaturesReward() const { + JsonRandom::Variables emptyVariables; std::vector> aproximateReward; for(const JsonNode & configEntry : config) { const JsonNode & guardsInfo = configEntry["reward"]["creatures"]; - auto stacks = JsonRandom::evaluateCreatures(guardsInfo); + auto stacks = JsonRandom::evaluateCreatures(guardsInfo, emptyVariables); for(auto stack : stacks) { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 7b95aee49..b157667d4 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -95,7 +95,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("monolith", CGMonolith); SET_HANDLER("subterraneanGate", CGSubterraneanGate); SET_HANDLER("whirlpool", CGWhirlpool); - SET_HANDLER("witch", CGWitchHut); SET_HANDLER("terrain", CGTerrainPatch); #undef SET_HANDLER_CLASS diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index aeb8d6b62..0461148fa 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -35,8 +35,7 @@ public: { AObjectTypeHandler::serialize(h, version); - if (version >= 816) - h & objectInfo; + h & objectInfo; } }; diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 7cea90465..800e930af 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -256,9 +256,11 @@ void MarketInstanceConstructor::initializeObject(CGMarket * market) const void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenerator & rng) const { + JsonRandom::Variables emptyVariables; + if(auto * university = dynamic_cast(object)) { - for(auto skill : JsonRandom::loadSecondary(predefinedOffer, rng)) + for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables)) university->skills.push_back(skill.first.getNum()); } } diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp index ccf3a7895..6a137b976 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.cpp @@ -93,7 +93,8 @@ void DwellingInstanceConstructor::randomizeObject(CGDwelling * object, CRandomGe } else if(guards.getType() == JsonNode::JsonType::DATA_VECTOR) //custom guards (eg. Elemental Conflux) { - for(auto & stack : JsonRandom::loadCreatures(guards, rng)) + JsonRandom::Variables emptyVariables; + for(auto & stack : JsonRandom::loadCreatures(guards, rng, emptyVariables)) { dwelling->putStack(SlotID(dwelling->stacksCount()), new CStackInstance(stack.type->getId(), stack.count)); } diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp b/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp index 84ef806e8..84df1eb94 100644 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp +++ b/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp @@ -23,6 +23,8 @@ void ShrineInstanceConstructor::initTypeData(const JsonNode & config) void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenerator & rng) const { + JsonRandom::Variables emptyVariables; + auto visitTextParameter = parameters["visitText"]; if (visitTextParameter.isNumber()) @@ -31,12 +33,7 @@ void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenera shrine->visitText.appendRawString(visitTextParameter.String()); if(shrine->spell == SpellID::NONE) // shrine has no predefined spell - { - std::vector possibilities; - shrine->cb->getAllowedSpells(possibilities); - - shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, possibilities); - } + shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, emptyVariables); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index d4cbc8686..ab047c30d 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -253,7 +253,7 @@ CGHeroInstance::CGHeroInstance(): { setNodeType(HERO); ID = Obj::HERO; - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); blockVisit = true; } @@ -315,7 +315,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) pushPrimSkill(static_cast(g), type->heroClass->primarySkillInitial[g]); } } - if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::DEFAULT, -1)) //set secondary skills to default + if(secSkills.size() == 1 && secSkills[0] == std::pair(SecondarySkill::NONE, -1)) //set secondary skills to default secSkills = type->secSkillsInit; if (gender == EHeroGender::DEFAULT) @@ -1580,7 +1580,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) bool normalSkills = false; for(const auto & p : secSkills) { - if(p.first == SecondarySkill(SecondarySkill::DEFAULT)) + if(p.first == SecondarySkill(SecondarySkill::NONE)) defaultSkills = true; else normalSkills = true; @@ -1618,7 +1618,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) secSkills.clear(); if(secondarySkills.getType() == JsonNode::JsonType::DATA_NULL) { - secSkills.emplace_back(SecondarySkill::DEFAULT, -1); + secSkills.emplace_back(SecondarySkill::NONE, -1); } else { diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 86040a89b..3e8bc6a7e 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -90,7 +90,6 @@ public: // POSSIBLE // class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles -// class DLL_LINKAGE CGWitchHut : public CPlayersVisited // class DLL_LINKAGE CGScholar : public CGObjectInstance VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 771f84186..bf370d858 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -842,98 +842,6 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) } } -void CGWitchHut::initObj(CRandomGenerator & rand) -{ - if (allowedAbilities.empty()) //this can happen for RMG and RoE maps. - { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - - // Necromancy and Leadership can't be learned by default - defaultAllowed[SecondarySkill::NECROMANCY] = false; - defaultAllowed[SecondarySkill::LEADERSHIP] = false; - - for(int i = 0; i < defaultAllowed.size(); i++) - if (defaultAllowed[i] && cb->isAllowed(2, i)) - allowedAbilities.insert(SecondarySkill(i)); - } - ability = *RandomGeneratorUtil::nextItem(allowedAbilities, rand); -} - -void CGWitchHut::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGWitchHut::OBJPROP_VISITED, h->tempOwner.getNum()); - ui32 txt_id; - if(h->getSecSkillLevel(SecondarySkill(ability))) //you already know this skill - { - txt_id =172; - } - else if(!h->canLearnSkill()) //already all skills slots used - { - txt_id = 173; - } - else //give sec skill - { - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, ability, 1, 0); - txt_id = 171; - cb->changeSecSkill(h, SecondarySkill(ability), 1, true); - } - - iw.text.appendLocalString(EMetaText::ADVOB_TXT,txt_id); - iw.text.replaceLocalString(EMetaText::SEC_SKILL_NAME, ability); - cb->showInfoDialog(&iw); -} - -std::string CGWitchHut::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[356]; // + (learn %s) - boost::algorithm::replace_first(hoverName, "%s", VLC->skillh->getByIndex(ability)->getNameTranslated()); - } - return hoverName; -} - -std::string CGWitchHut::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->getSecSkillLevel(SecondarySkill(ability))) //hero knows that ability - hoverName += "\n\n" + VLC->generaltexth->allTexts[357]; // (Already learned) - return hoverName; -} - -void CGWitchHut::serializeJsonOptions(JsonSerializeFormat & handler) -{ - //TODO: unify allowed abilities with others - make them std::vector - - std::vector temp; - size_t skillCount = VLC->skillh->size(); - temp.resize(skillCount, false); - - auto standard = VLC->skillh->getDefaultAllowed(); //todo: for WitchHut default is all except Leadership and Necromancy - - if(handler.saving) - { - for(SecondarySkill i(0); i < SecondarySkill(skillCount); ++i) - if(vstd::contains(allowedAbilities, i)) - temp[i] = true; - } - - handler.serializeLIC("allowedSkills", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, standard, temp); - - if(!handler.saving) - { - allowedAbilities.clear(); - for(si32 i = 0; i < skillCount; ++i) - if(temp[i]) - allowedAbilities.insert(SecondarySkill(i)); - } -} - void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 1b4397794..8b68c4dab 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -57,26 +57,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGWitchHut : public CTeamVisited -{ -public: - std::set allowedAbilities; - SecondarySkill ability; - - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & allowedAbilities; - h & ability; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGScholar : public CGObjectInstance { public: diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index b5c2ded6e..a15ac57d1 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1139,23 +1139,40 @@ CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) return object; } -CGObjectInstance * CMapLoaderH3M::readWitchHut() +CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGWitchHut(); + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); // AB and later maps have allowed abilities defined in H3M if(features.levelAB) { - reader->readBitmaskSkills(object->allowedAbilities, false); + std::set allowedAbilities; + reader->readBitmaskSkills(allowedAbilities, false); - if(object->allowedAbilities.size() != 1) + if(allowedAbilities.size() != 1) { auto defaultAllowed = VLC->skillh->getDefaultAllowed(); for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) if(defaultAllowed[skillID]) - object->allowedAbilities.insert(SecondarySkill(skillID)); + allowedAbilities.insert(SecondarySkill(skillID)); } + + JsonVector anyOfList; + + for (auto const & skill : allowedAbilities) + { + JsonNode entry; + entry.String() = VLC->skills()->getById(skill)->getJsonKey(); + anyOfList.push_back(entry); + } + JsonNode variable; + variable.Vector() = anyOfList; + + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } return object; } @@ -1458,7 +1475,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptrsecSkills.empty()) { - if(object->secSkills[0].first != SecondarySkill::DEFAULT) + if(object->secSkills[0].first != SecondarySkill::NONE) logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); object->secSkills.clear(); } diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 3eb7a3226..90ca6ee08 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -165,7 +165,7 @@ private: CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readSign(const int3 & position); - CGObjectInstance * readWitchHut(); + CGObjectInstance * readWitchHut(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readScholar(); CGObjectInstance * readGarrison(const int3 & mapPosition); CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 28dcca012..e431b9dd3 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -147,7 +147,6 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGWhirlpool); REGISTER_GENERIC_HANDLER(CGTownInstance); REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(CGWitchHut); REGISTER_GENERIC_HANDLER(HillFort); #undef REGISTER_GENERIC_HANDLER @@ -177,7 +176,6 @@ void registerTypesMapObjects2(Serializer &s) s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 5e2430e62..ba0791bf0 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -24,6 +24,38 @@ ui16 Rewardable::Configuration::getResetDuration() const return resetParameters.period; } +std::optional Rewardable::Configuration::getVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.values.count(variableID)) + return variables.values.at(variableID); + + return std::nullopt; +} + +JsonNode Rewardable::Configuration::getPresetVariable(const std::string & category, const std::string & name) const +{ + std::string variableID = category + '@' + name; + + if (variables.values.count(variableID)) + return variables.preset.at(variableID); + else + return JsonNode(); +} + +void Rewardable::Configuration::presetVariable(const std::string & category, const std::string & name, const JsonNode & value) +{ + std::string variableID = category + '@' + name; + variables.preset[variableID] = value; +} + +void Rewardable::Configuration::initVariable(const std::string & category, const std::string & name, int value) +{ + std::string variableID = category + '@' + name; + variables.values[variableID] = value; +} + void Rewardable::ResetInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("period", period); @@ -39,6 +71,11 @@ void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler) handler.serializeInt("visitType", visitType); } +void Rewardable::Variables::serializeJson(JsonSerializeFormat & handler) +{ + // TODO +} + void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) { handler.serializeStruct("onSelect", onSelect); diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 72377da04..36c34161a 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -62,7 +62,6 @@ struct DLL_LINKAGE ResetInfo /// if true - reset list of visitors (heroes & players) on reset bool visitors; - /// if true - re-randomize rewards on a new week bool rewards; @@ -86,7 +85,7 @@ struct DLL_LINKAGE VisitInfo /// Event to which this reward is assigned EEventType visitType; - + void serializeJson(JsonSerializeFormat & handler); template void serialize(Handler &h, const int version) @@ -98,6 +97,23 @@ struct DLL_LINKAGE VisitInfo } }; +struct DLL_LINKAGE Variables +{ + /// List of variables used by this object in their current values + std::map values; + + /// List of per-instance preconfigured variables, e.g. from map + std::map preset; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & values; + h & preset; + } +}; + /// Base class that can handle granting rewards to visiting heroes. struct DLL_LINKAGE Configuration { @@ -116,6 +132,9 @@ struct DLL_LINKAGE Configuration /// how and when should the object be reset Rewardable::ResetInfo resetParameters; + /// List of variables shoread between all limiters and rewards + Rewardable::Variables variables; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; @@ -124,6 +143,11 @@ struct DLL_LINKAGE Configuration EVisitMode getVisitMode() const; ui16 getResetDuration() const; + + std::optional getVariable(const std::string & category, const std::string & name) const; + JsonNode getPresetVariable(const std::string & category, const std::string & name) const; + void presetVariable(const std::string & category, const std::string & name, const JsonNode & value); + void initVariable(const std::string & category, const std::string & name, int value); void serializeJson(JsonSerializeFormat & handler); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b9a8f1b2b..b07805ce4 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -102,26 +102,23 @@ Rewardable::LimitersList Rewardable::Info::configureSublimiters(Rewardable::Conf void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const { - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); + auto const & variables = object.variables.values; + limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng, variables); + limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables); + limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); - limiter.dayOfWeek = JsonRandom::loadValue(source["dayOfWeek"], rng); - limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng); - limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng); - limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["minLevel"], rng); // VCMI 1.1 compatibilty + limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables); + limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables); - limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng); - limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng); + limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables); - limiter.resources = JsonRandom::loadResources(source["resources"], rng); - - limiter.primary = JsonRandom::loadPrimary(source["primary"], rng); - limiter.secondary = JsonRandom::loadSecondary(source["secondary"], rng); - limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - limiter.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + limiter.primary = JsonRandom::loadPrimary(source["primary"], rng, variables); + limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); + limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); limiter.players = JsonRandom::loadColors(source["colors"], rng); limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); @@ -134,36 +131,32 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Reward & reward, const JsonNode & source) const { - reward.resources = JsonRandom::loadResources(source["resources"], rng); + auto const & variables = object.variables.values; - reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng) - + JsonRandom::loadValue(source["gainedExp"], rng); // VCMI 1.1 compatibilty + reward.resources = JsonRandom::loadResources(source["resources"], rng, variables); - reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng) - + JsonRandom::loadValue(source["gainedLevels"], rng); // VCMI 1.1 compatibilty + reward.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); + reward.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); - reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng); - reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng); - reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, -1); + reward.manaDiff = JsonRandom::loadValue(source["manaPoints"], rng, variables); + reward.manaOverflowFactor = JsonRandom::loadValue(source["manaOverflowFactor"], rng, variables); + reward.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables, -1); - reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng); - reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, -1); + reward.movePoints = JsonRandom::loadValue(source["movePoints"], rng, variables); + reward.movePercentage = JsonRandom::loadValue(source["movePercentage"], rng, variables, -1); reward.removeObject = source["removeObject"].Bool(); reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]); - reward.primary = JsonRandom::loadPrimary(source["primary"], rng); - reward.secondary = JsonRandom::loadSecondary(source["secondary"], rng); + reward.primary = JsonRandom::loadPrimary(source["primary"], rng, variables); + reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); - std::vector spells; - IObjectInterface::cb->getAllowedSpells(spells); - - reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng); - reward.spells = JsonRandom::loadSpells(source["spells"], rng, spells); - reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng); + reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); + reward.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + reward.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); if(!source["spellCast"].isNull() && source["spellCast"].isStruct()) { - reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng); + reward.spellCast.first = JsonRandom::loadSpell(source["spellCast"]["spell"], rng, variables); reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer(); } @@ -185,11 +178,48 @@ void Rewardable::Info::configureResetInfo(Rewardable::Configuration & object, CR resetParameters.rewards = source["rewards"].Bool(); } +void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const +{ + for(const auto & category : source.Struct()) + { + for(const auto & entry : category.second.Struct()) + { + JsonNode preset = object.getPresetVariable(category.first, entry.first); + int32_t value = -1; + + if (category.first == "number") + value = JsonRandom::loadValue(entry.second, rng, object.variables.values); + + if (category.first == "artifact") + value = JsonRandom::loadArtifact(entry.second, rng, object.variables.values).getNum(); + + if (category.first == "spell") + value = JsonRandom::loadSpell(entry.second, rng, object.variables.values).getNum(); + + // TODO + // if (category.first == "creature") + // value = JsonRandom::loadCreature(entry.second, rng, object.variables.values).type->getId(); + + // TODO + // if (category.first == "resource") + // value = JsonRandom::loadResource(entry.second, rng, object.variables.values).getNum(); + + // TODO + // if (category.first == "primarySkill") + // value = JsonRandom::loadCreature(entry.second, rng, object.variables.values).getNum(); + + if (category.first == "secondarySkill") + value = JsonRandom::loadSecondary(entry.second, rng, object.variables.values).getNum(); + + object.initVariable(category.first, entry.first, value); + } + } +} + void Rewardable::Info::configureRewards( Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, - std::map & thrownDice, Rewardable::EEventType event, const std::string & modeName) const { @@ -200,21 +230,27 @@ void Rewardable::Info::configureRewards( if (!reward["appearChance"].isNull()) { JsonNode chance = reward["appearChance"]; - si32 diceID = static_cast(chance["dice"].Float()); + std::string diceID = std::to_string(chance["dice"].Integer()); - if (thrownDice.count(diceID) == 0) - thrownDice[diceID] = rng.getIntRange(0, 99)(); + auto diceValue = object.getVariable("dice", diceID); + + if (!diceValue.has_value()) + { + object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + diceValue = object.getVariable("dice", diceID); + } + assert(diceValue.has_value()); if (!chance["min"].isNull()) { int min = static_cast(chance["min"].Float()); - if (min > thrownDice[diceID]) + if (min > *diceValue) continue; } if (!chance["max"].isNull()) { int max = static_cast(chance["max"].Float()); - if (max <= thrownDice[diceID]) + if (max <= *diceValue) continue; } } @@ -240,11 +276,11 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand { object.info.clear(); - std::map thrownDice; + configureVariables(object, rng, parameters["variables"]); - configureRewards(object, rng, parameters["rewards"], thrownDice, Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); - configureRewards(object, rng, parameters["onVisited"], thrownDice, Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); - configureRewards(object, rng, parameters["onEmpty"], thrownDice, Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); + configureRewards(object, rng, parameters["rewards"], Rewardable::EEventType::EVENT_FIRST_VISIT, "rewards"); + configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); + configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index c9c826fae..1bf145c39 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -32,7 +32,8 @@ class DLL_LINKAGE Info : public IObjectInfo JsonNode parameters; std::string objectTextID; - void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, std::map & thrownDice, Rewardable::EEventType mode, const std::string & textPrefix) const; + void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; + void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; void configureLimiter(Rewardable::Configuration & object, CRandomGenerator & rng, Rewardable::Limiter & limiter, const JsonNode & source) const; Rewardable::LimitersList configureSublimiters(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; diff --git a/lib/rewardable/Interface.h b/lib/rewardable/Interface.h index c675f82bc..ac85e6b72 100644 --- a/lib/rewardable/Interface.h +++ b/lib/rewardable/Interface.h @@ -10,8 +10,6 @@ #pragma once -#include "../CCreatureSet.h" -#include "../ResourceSet.h" #include "../spells/ExternalCaster.h" #include "Configuration.h" From a3b23544813efb75cfeb887ff3390e5e2e9a298f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 30 Sep 2023 21:22:31 +0300 Subject: [PATCH 0838/1248] Implemented visit mode "limiter". Fixed h3m variable loading --- lib/mapObjects/CGTownBuilding.cpp | 2 ++ lib/mapObjects/CRewardableObject.cpp | 5 +++++ lib/mapping/MapFormatH3M.cpp | 6 ++++-- lib/rewardable/Configuration.cpp | 2 +- lib/rewardable/Configuration.h | 6 +++++- lib/rewardable/Info.cpp | 19 ++++++++++++------- lib/rewardable/Limiter.cpp | 3 +++ lib/rewardable/Limiter.h | 3 +++ scripting/lua/api/netpacks/SetResources.cpp | 4 ++-- 9 files changed, 37 insertions(+), 13 deletions(-) diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 0a8f952a6..72d998d7f 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -396,6 +396,8 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(town->town->faction->getIndex(), bID)); case Rewardable::VISIT_HERO: return visitors.find(contextHero->id) != visitors.end(); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); default: return false; } diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index ad3ebc5c6..4f31f362a 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -188,6 +188,8 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con return contextHero->hasBonusFrom(BonusSource::OBJECT, ID); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(contextHero); default: return false; } @@ -200,6 +202,7 @@ bool CRewardableObject::wasVisited(PlayerColor player) const case Rewardable::VISIT_UNLIMITED: case Rewardable::VISIT_BONUS: case Rewardable::VISIT_HERO: + case Rewardable::VISIT_LIMITER: return false; case Rewardable::VISIT_ONCE: case Rewardable::VISIT_PLAYER: @@ -217,6 +220,8 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const return h->hasBonusFrom(BonusSource::OBJECT, ID); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); + case Rewardable::VISIT_LIMITER: + return configuration.visitLimiter.heroAllowed(h); default: return wasVisited(h->tempOwner); } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index a15ac57d1..8d5ce2d69 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -34,6 +34,7 @@ #include "../mapObjects/CGCreature.h" #include "../mapObjects/MapObjects.h" #include "../mapObjects/ObjectTemplate.h" +#include "../modding/ModScope.h" #include "../spells/CSpellHandler.h" #include @@ -1156,7 +1157,7 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share { auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) + for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) if(defaultAllowed[skillID]) allowedAbilities.insert(SecondarySkill(skillID)); } @@ -1170,7 +1171,8 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share anyOfList.push_back(entry); } JsonNode variable; - variable.Vector() = anyOfList; + variable["anyOf"].Vector() = anyOfList; + variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index ba0791bf0..427c01401 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -38,7 +38,7 @@ JsonNode Rewardable::Configuration::getPresetVariable(const std::string & catego { std::string variableID = category + '@' + name; - if (variables.values.count(variableID)) + if (variables.preset.count(variableID)) return variables.preset.at(variableID); else return JsonNode(); diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 36c34161a..8dbfd0424 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -26,6 +26,7 @@ enum EVisitMode VISIT_ONCE, // only once, first to visit get all the rewards VISIT_HERO, // every hero can visit object once VISIT_BONUS, // can be visited by any hero that don't have bonus from this object + VISIT_LIMITER, // can be visited by heroes that don't fulfill provided limiter VISIT_PLAYER // every player can visit object once }; @@ -46,7 +47,7 @@ enum class EEventType }; const std::array SelectModeString{"selectFirst", "selectPlayer", "selectRandom"}; -const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "player"}; +const std::array VisitModeString{"unlimited", "once", "hero", "bonus", "limiter", "player"}; struct DLL_LINKAGE ResetInfo { @@ -135,6 +136,9 @@ struct DLL_LINKAGE Configuration /// List of variables shoread between all limiters and rewards Rewardable::Variables variables; + /// Limiter that will be used to determine that object is visited. Only if visit mode is set to "limiter" + Rewardable::Limiter visitLimiter; + /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b07805ce4..b842254ad 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -108,6 +108,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.daysPassed = JsonRandom::loadValue(source["daysPassed"], rng, variables); limiter.heroExperience = JsonRandom::loadValue(source["heroExperience"], rng, variables); limiter.heroLevel = JsonRandom::loadValue(source["heroLevel"], rng, variables); + limiter.canLearnSkills = source["canLearnSkills"].Bool(); limiter.manaPercentage = JsonRandom::loadValue(source["manaPercentage"], rng, variables); limiter.manaPoints = JsonRandom::loadValue(source["manaPoints"], rng, variables); @@ -185,31 +186,32 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR for(const auto & entry : category.second.Struct()) { JsonNode preset = object.getPresetVariable(category.first, entry.first); + const JsonNode & input = preset.isNull() ? entry.second : preset; int32_t value = -1; if (category.first == "number") - value = JsonRandom::loadValue(entry.second, rng, object.variables.values); + value = JsonRandom::loadValue(input, rng, object.variables.values); if (category.first == "artifact") - value = JsonRandom::loadArtifact(entry.second, rng, object.variables.values).getNum(); + value = JsonRandom::loadArtifact(input, rng, object.variables.values).getNum(); if (category.first == "spell") - value = JsonRandom::loadSpell(entry.second, rng, object.variables.values).getNum(); + value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); // TODO // if (category.first == "creature") - // value = JsonRandom::loadCreature(entry.second, rng, object.variables.values).type->getId(); + // value = JsonRandom::loadCreature(input, rng, object.variables.values).type->getId(); // TODO // if (category.first == "resource") - // value = JsonRandom::loadResource(entry.second, rng, object.variables.values).getNum(); + // value = JsonRandom::loadResource(input, rng, object.variables.values).getNum(); // TODO // if (category.first == "primarySkill") - // value = JsonRandom::loadCreature(entry.second, rng, object.variables.values).getNum(); + // value = JsonRandom::loadCreature(input, rng, object.variables.values).getNum(); if (category.first == "secondarySkill") - value = JsonRandom::loadSecondary(entry.second, rng, object.variables.values).getNum(); + value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); object.initVariable(category.first, entry.first, value); } @@ -328,6 +330,9 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand break; } } + + configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); + } bool Rewardable::Info::givesResources() const diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b23c50610..3c14dde6b 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -101,6 +101,9 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const if(manaPoints > hero->mana) return false; + if (canLearnSkills && !hero->canLearnSkill()) + return false; + if(manaPercentage > 100 * hero->mana / hero->manaLimit()) return false; diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index 6fa52925e..cc1762924 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -44,6 +44,9 @@ struct DLL_LINKAGE Limiter final /// percentage of mana points that hero needs to have si32 manaPercentage; + /// Number of free secondary slots that hero needs to have + bool canLearnSkills; + /// resources player needs to have in order to trigger reward TResources resources; diff --git a/scripting/lua/api/netpacks/SetResources.cpp b/scripting/lua/api/netpacks/SetResources.cpp index 2d8b5c569..d71e2aa34 100644 --- a/scripting/lua/api/netpacks/SetResources.cpp +++ b/scripting/lua/api/netpacks/SetResources.cpp @@ -102,7 +102,7 @@ int SetResourcesProxy::getAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); @@ -122,7 +122,7 @@ int SetResourcesProxy::setAmount(lua_State * L) if(!S.tryGet(1, object)) return S.retVoid(); - auto type = EGameResID::INVALID; + auto type = EGameResID::NONE; if(!S.tryGet(2, type)) return S.retVoid(); From c0a31b716a49e04b58b294667f5bdd6db6bd9168 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 30 Sep 2023 21:22:58 +0300 Subject: [PATCH 0839/1248] Added new version of witch hut config --- config/objects/rewardableGeneric.json | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 config/objects/rewardableGeneric.json diff --git a/config/objects/rewardableGeneric.json b/config/objects/rewardableGeneric.json new file mode 100644 index 000000000..85a3f1270 --- /dev/null +++ b/config/objects/rewardableGeneric.json @@ -0,0 +1,69 @@ +{ + "witchHut" : { + "index" :113, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "witchHut" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "zoneLimit" : 3, + "value" : 1500, + "rarity" : 80 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + + "variables" : { + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } + }, + "visitLimiter" : { + "secondary" : { + "@gainedSkill" : 1 + } + }, + "rewards" : [ + { + "limiter" : { + "canLearnSkills" : true, + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "secondary" : { + "@gainedSkill" : 1 + } + "message" : 171 // Witch teaches you skill + } + ], + "onVisited" : [ + { + "message" : 172 // You already known this skill + } + ], + "onEmpty" : [ + { + "message" : 173 // You know too much (no free slots) + } + ] + } + } + }, +} \ No newline at end of file From f3ed589e354bb02fa222fb69969ceb6df32df00d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 30 Sep 2023 22:01:39 +0300 Subject: [PATCH 0840/1248] Use same fow radius calculation as in H3 --- config/objects/rewardableGeneric.json | 2 +- lib/IGameCallback.cpp | 2 +- lib/int3.h | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/objects/rewardableGeneric.json b/config/objects/rewardableGeneric.json index 85a3f1270..5c48c25b4 100644 --- a/config/objects/rewardableGeneric.json +++ b/config/objects/rewardableGeneric.json @@ -49,7 +49,7 @@ }, "secondary" : { "@gainedSkill" : 1 - } + }, "message" : 171 // Witch teaches you skill } ], diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 4efa55cab..503d1a8d7 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -97,7 +97,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) { int3 tilePos(xd,yd,pos.z); - double distance = pos.dist(tilePos, distanceFormula); + int distance = pos.dist(tilePos, distanceFormula); if(distance <= radious) { diff --git a/lib/int3.h b/lib/int3.h index aa060703a..014c5c8fa 100644 --- a/lib/int3.h +++ b/lib/int3.h @@ -110,7 +110,7 @@ public: switch(formula) { case DIST_2D: - return static_cast(dist2d(o)); + return std::round(dist2d(o)); case DIST_MANHATTAN: return static_cast(mandist2d(o)); case DIST_CHEBYSHEV: From 98fd939ed6adc28990b6891f4ebfdfceb6d2cc23 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 14:11:13 +0300 Subject: [PATCH 0841/1248] Cartographer/Observatory is now configurable object --- Global.h | 13 ++ client/NetPacksClient.cpp | 2 +- config/objects/generic.json | 64 ------ config/objects/moddables.json | 17 -- config/objects/rewardableGeneric.json | 208 ++++++++++++++++++ lib/CGameInfoCallback.cpp | 2 +- lib/IGameCallback.cpp | 30 +-- lib/IGameCallback.h | 12 +- lib/NetPacks.h | 8 +- lib/NetPacksLib.cpp | 5 +- lib/TerrainHandler.cpp | 17 +- lib/TerrainHandler.h | 5 +- .../CObjectClassesHandler.cpp | 2 - lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/CRewardableObject.h | 1 - lib/mapObjects/MiscObjects.cpp | 109 +-------- lib/mapObjects/MiscObjects.h | 24 -- lib/registerTypes/RegisterTypes.h | 4 - lib/rewardable/Info.cpp | 14 ++ lib/rewardable/Interface.cpp | 47 ++++ lib/rewardable/Reward.cpp | 5 + lib/rewardable/Reward.h | 34 ++- server/CGameHandler.cpp | 6 +- server/processors/PlayerMessageProcessor.cpp | 4 +- 24 files changed, 353 insertions(+), 282 deletions(-) diff --git a/Global.h b/Global.h index 2eb54db45..1ba7846e4 100644 --- a/Global.h +++ b/Global.h @@ -475,6 +475,19 @@ namespace vstd } } + template + void erase_if(std::unordered_set &setContainer, Predicate pred) + { + auto itr = setContainer.begin(); + auto endItr = setContainer.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + setContainer.erase(tmpItr); + } + } + //works for map and std::map, maybe something else template void erase_if(std::map &container, Predicate pred) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 0af992ec6..affbdf49e 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -177,7 +177,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) } if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) { - if(pack.mode) + if(pack.mode == FoWChange::Mode::REVEAL) i.second->tileRevealed(pack.tiles); else i.second->tileHidden(pack.tiles); diff --git a/config/objects/generic.json b/config/objects/generic.json index 0060d02e2..a2c188ed1 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -156,70 +156,6 @@ } }, - "redwoodObservatory" : { - "index" :58, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "templates" : - { - "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, - "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } - }, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "pillarOfFire" : { - "index" :60, - "handler" : "observatory", - "base" : { - "sounds" : { - "ambient" : ["LOOPFIRE"], - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 750, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - } - } - } - }, - "coverOfDarkness" : { - "index" :15, - "handler" : "observatory", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - } - } - } - }, - "whirlpool" : { "index" :111, "handler" : "whirlpool", diff --git a/config/objects/moddables.json b/config/objects/moddables.json index da0b7e793..2a2b31371 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -255,23 +255,6 @@ } }, - // subtype: different revealed areas - "cartographer" : { - "index" :13, - "handler": "cartographer", - "lastReservedIndex" : 2, - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "water" : { "index" : 0, "aiValue" : 5000, "rmg" : { "zoneLimit" : 1, "value" : 5000, "rarity" : 20 } }, - "land" : { "index" : 1, "aiValue": 10000, "rmg" : { "zoneLimit" : 1, "value" : 10000, "rarity" : 20 } }, - "subterra" : { "index" : 2, "aiValue" : 7500, "rmg" : { "zoneLimit" : 1, "value" : 7500, "rarity" : 20 } } - } - }, - // subtype: resource ID "mine" : { "index" :53, diff --git a/config/objects/rewardableGeneric.json b/config/objects/rewardableGeneric.json index 5c48c25b4..cd36c3aac 100644 --- a/config/objects/rewardableGeneric.json +++ b/config/objects/rewardableGeneric.json @@ -66,4 +66,212 @@ } } }, + + "redwoodObservatory" : { + "index" :58, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "redwoodObservatory" : { + "index" : 0, + "aiValue" : 750, + "templates" : + { + "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, + "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } + }, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 98, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "pillarOfFire" : { + "index" :60, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFIRE"], + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "pillarOfFire" : { + "index" : 0, + "aiValue" : 750, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 99, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "coverOfDarkness" : { + "index" :15, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "coverOfDarkness" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 31, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true + } + } + ] + } + } + }, + + "cartographer" : { + "index" :13, + "handler": "configurable", + "lastReservedIndex" : 2, + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "cartographerWater" : { + "index" : 0, + "aiValue" : 5000, + "rmg" : { + "zoneLimit" : 1, + "value" : 5000, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "water" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 25, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "water" : 1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerLand" : { + "index" : 1, + "aiValue": 10000, + "rmg" : { + "zoneLimit" : 1, + "value" : 10000, + "rarity" : 2 + }, + "compatibilityIdentifiers" : [ "land" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 26, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "surface" : 1, + "water" : -1, + "rock" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerSubterranean" : { + "index" : 2, + "aiValue" : 7500, + "rmg" : { + "zoneLimit" : 1, + "value" : 7500, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "subterra" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 27, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "subterra" : 1, + "water" : -1, + "rock" : -1, + "surface" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + } + } + } } \ No newline at end of file diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 077f862c5..45b72a8b4 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -607,7 +607,7 @@ EBuildingState CGameInfoCallback::canBuildStructure( const CGTownInstance *t, Bu { const TerrainTile *tile = getTile(t->bestLocation(), false); - if(!tile || tile->terType->isLand()) + if(!tile || !tile->terType->isWater()) return EBuildingState::NO_WATER; //lack of water } diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 503d1a8d7..55ceaf9cd 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -88,7 +88,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, return; } if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map - getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE); + getAllTiles (tiles, player, -1, [](auto * tile){return true;}); else { const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); @@ -112,7 +112,7 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, } } -void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const +void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, std::function filter) const { if(!!Player && !Player->isValidPlayer()) { @@ -137,29 +137,9 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: { for(int yd = 0; yd < gs->map->height; yd++) { - bool isTileEligible = false; - - switch(tileFilterMode) - { - case MapTerrainFilterMode::NONE: - isTileEligible = true; - break; - case MapTerrainFilterMode::WATER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater(); - break; - case MapTerrainFilterMode::LAND: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand(); - break; - case MapTerrainFilterMode::LAND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible(); - break; - case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible(); - break; - } - - if(isTileEligible) - tiles.insert(int3(xd, yd, zd)); + int3 coordinates(xd, yd, zd); + if (filter(getTile(coordinates))) + tiles.insert(coordinates); } } } diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 03e96dbeb..e1f82f87c 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -43,15 +43,6 @@ namespace scripting class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback { public: - enum class MapTerrainFilterMode - { - NONE = 0, - LAND = 1, - WATER = 2, - LAND_CARTOGRAPHER = 3, - UNDERGROUND_CARTOGRAPHER = 4 - }; - CGameState *gameState(); //used for random spawns @@ -66,8 +57,7 @@ public: int3::EDistanceFormula formula = int3::DIST_2D) const; //returns all tiles on given level (-1 - both levels, otherwise number of level) - void getAllTiles(std::unordered_set &tiles, std::optional player = std::optional(), - int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const; + void getAllTiles(std::unordered_set &tiles, std::optional player, int level, std::function filter) const; //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 2992e7be6..017cc0118 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -344,11 +344,17 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient struct DLL_LINKAGE FoWChange : public CPackForClient { + enum class Mode : uint8_t + { + HIDE, + REVEAL + }; + void applyGs(CGameState * gs); std::unordered_set tiles; PlayerColor player; - ui8 mode = 0; //mode==0 - hide, mode==1 - reveal + Mode mode; bool waitForDialogs = false; virtual void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c56928a3c..65d4878af 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -924,8 +924,9 @@ void FoWChange::applyGs(CGameState *gs) TeamState * team = gs->getPlayerTeam(player); auto fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode; - if (mode == 0) //do not hide too much + (*fogOfWarMap)[t.z][t.x][t.y] = mode != Mode::HIDE; + + if (mode == Mode::HIDE) //do not hide too much { std::unordered_set tilesRevealed; for (auto & elem : gs->map->objects) diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index 2c33519d1..f7a3eb96f 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -155,9 +155,14 @@ bool TerrainType::isWater() const return passabilityType & PassabilityType::WATER; } +bool TerrainType::isRock() const +{ + return passabilityType & PassabilityType::ROCK; +} + bool TerrainType::isPassable() const { - return !(passabilityType & PassabilityType::ROCK); + return !isRock(); } bool TerrainType::isSurface() const @@ -170,16 +175,6 @@ bool TerrainType::isUnderground() const return passabilityType & PassabilityType::SUBTERRANEAN; } -bool TerrainType::isSurfaceCartographerCompatible() const -{ - return isSurface(); -} - -bool TerrainType::isUndergroundCartographerCompatible() const -{ - return isLand() && isPassable() && !isSurface(); -} - bool TerrainType::isTransitionRequired() const { return transitionRequired; diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 60d649d3c..b94e4427d 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -84,12 +84,13 @@ public: bool isLand() const; bool isWater() const; + bool isRock() const; + bool isPassable() const; + bool isSurface() const; bool isUnderground() const; bool isTransitionRequired() const; - bool isSurfaceCartographerCompatible() const; - bool isUndergroundCartographerCompatible() const; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index b157667d4..9b8b5ecff 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -71,7 +71,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("randomDwelling", CGDwelling); SET_HANDLER("generic", CGObjectInstance); - SET_HANDLER("cartographer", CCartographer); SET_HANDLER("artifact", CGArtifact); SET_HANDLER("borderGate", CGBorderGate); SET_HANDLER("borderGuard", CGBorderGuard); @@ -84,7 +83,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("magi", CGMagi); SET_HANDLER("mine", CGMine); SET_HANDLER("obelisk", CGObelisk); - SET_HANDLER("observatory", CGObservatory); SET_HANDLER("pandora", CGPandoraBox); SET_HANDLER("prison", CGHeroInstance); SET_HANDLER("questGuard", CGQuestGuard); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 7c9d97a7b..78c4399ba 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1068,7 +1068,7 @@ void CGTownInstance::onTownCaptured(const PlayerColor & winner) const setOwner(winner); FoWChange fw; fw.player = winner; - fw.mode = 1; + fw.mode = FoWChange::Mode::REVEAL; cb->getTilesInRange(fw.tiles, getSightCenter(), getSightRadius(), winner, 1); cb->sendAndApply(& fw); } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 3e8bc6a7e..fe41428c1 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -90,6 +90,5 @@ public: // POSSIBLE // class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles -// class DLL_LINKAGE CGScholar : public CGObjectInstance VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index bf370d858..36f7fd91a 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -842,40 +842,6 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) } } -void CGObservatory::onHeroVisit( const CGHeroInstance * h ) const -{ - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->tempOwner; - switch (ID) - { - case Obj::REDWOOD_OBSERVATORY: - case Obj::PILLAR_OF_FIRE: - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,98 + (ID==Obj::PILLAR_OF_FIRE)); - - FoWChange fw; - fw.player = h->tempOwner; - fw.mode = 1; - cb->getTilesInRange (fw.tiles, pos, 20, h->tempOwner, 1); - cb->sendAndApply (&fw); - break; - } - case Obj::COVER_OF_DARKNESS: - { - iw.text.appendLocalString (EMetaText::ADVOB_TXT, 31); - for (auto & player : cb->gameState()->players) - { - if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && - cb->getPlayerRelations(player.first, h->tempOwner) == PlayerRelations::ENEMIES) - cb->changeFogOfWar(visitablePos(), 20, player.first, true); - } - break; - } - } - cb->showInfoDialog(&iw); -} - void CGShrine::onHeroVisit( const CGHeroInstance * h ) const { if(spell == SpellID::NONE) @@ -1175,7 +1141,7 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const FoWChange fw; fw.player = h->tempOwner; - fw.mode = 1; + fw.mode = FoWChange::Mode::REVEAL; fw.waitForDialogs = true; for(const auto & it : eyelist[subID]) @@ -1321,79 +1287,6 @@ BoatId CGShipyard::getBoatType() const return createdBoat; } -void CCartographer::onHeroVisit( const CGHeroInstance * h ) const -{ - //if player has not bought map of this subtype yet and underground exist for stalagmite cartographer - if (!wasVisited(h->getOwner()) && (subID != 2 || cb->gameState()->map->twoLevel)) - { - if (cb->getResource(h->tempOwner, EGameResID::GOLD) >= 1000) //if he can afford a map - { - //ask if he wants to buy one - int text=0; - switch (subID) - { - case 0: - text = 25; - break; - case 1: - text = 26; - break; - case 2: - text = 27; - break; - default: - logGlobal->warn("Unrecognized subtype of cartographer"); - } - assert(text); - BlockingDialog bd (true, false); - bd.player = h->getOwner(); - bd.text.appendLocalString (EMetaText::ADVOB_TXT, text); - cb->showBlockingDialog (&bd); - } - else //if he cannot afford - { - h->showInfoDialog(28); - } - } - else //if he already visited carographer - { - h->showInfoDialog(24); - } -} - -void CCartographer::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const -{ - if(answer) //if hero wants to buy map - { - cb->giveResource(hero->tempOwner, EGameResID::GOLD, -1000); - FoWChange fw; - fw.mode = 1; - fw.player = hero->tempOwner; - - //subIDs of different types of cartographers: - //water = 0; land = 1; underground = 2; - - IGameCallback::MapTerrainFilterMode tileFilterMode = IGameCallback::MapTerrainFilterMode::NONE; - - switch(subID) - { - case 0: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::WATER; - break; - case 1: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::LAND_CARTOGRAPHER; - break; - case 2: - tileFilterMode = CPrivilegedInfoCallback::MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER; - break; - } - - cb->getAllTiles(fw.tiles, hero->tempOwner, -1, tileFilterMode); //reveal appropriate tiles - cb->sendAndApply(&fw); - cb->setObjProperty(id, CCartographer::OBJPROP_VISITED, hero->tempOwner.getNum()); - } -} - void CGDenOfthieves::onHeroVisit (const CGHeroInstance * h) const { cb->showObjectWindow(this, EOpenWindowMode::THIEVES_GUILD, h, false); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 8b68c4dab..ce7e7fcb2 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -314,17 +314,6 @@ public: } }; -class DLL_LINKAGE CGObservatory : public CGObjectInstance //Redwood observatory -{ -public: - void onHeroVisit(const CGHeroInstance * h) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGBoat : public CGObjectInstance, public CBonusSystemNode { public: @@ -396,19 +385,6 @@ public: } }; -class DLL_LINKAGE CCartographer : public CTeamVisited -{ -///behaviour varies depending on surface and floor -public: - void onHeroVisit(const CGHeroInstance * h) const override; - void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - } -}; - class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance { void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index e431b9dd3..e8ed5239e 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -56,7 +56,6 @@ void registerTypesMapObjects1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -112,7 +111,6 @@ void registerTypesMapObjectTypes(Serializer &s) #define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CCartographer); REGISTER_GENERIC_HANDLER(CGArtifact); REGISTER_GENERIC_HANDLER(CGBlackMarket); REGISTER_GENERIC_HANDLER(CGBoat); @@ -132,7 +130,6 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGMarket); REGISTER_GENERIC_HANDLER(CGMine); REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGObservatory); REGISTER_GENERIC_HANDLER(CGPandoraBox); REGISTER_GENERIC_HANDLER(CGQuestGuard); REGISTER_GENERIC_HANDLER(CGResource); @@ -177,7 +174,6 @@ void registerTypesMapObjects2(Serializer &s) s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); //s.template registerType(); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index b842254ad..a7d63a1b9 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -161,6 +161,20 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand reward.spellCast.second = source["spellCast"]["schoolLevel"].Integer(); } + if (!source["revealTiles"].isNull()) + { + auto const & entry = source["revealTiles"]; + + reward.revealTiles = RewardRevealTiles(); + reward.revealTiles->radius = JsonRandom::loadValue(entry["radius"], rng, variables); + reward.revealTiles->hide = entry["hide"].Bool(); + + reward.revealTiles->scoreSurface = JsonRandom::loadValue(entry["surface"], rng, variables); + reward.revealTiles->scoreSubterra = JsonRandom::loadValue(entry["subterra"], rng, variables); + reward.revealTiles->scoreWater = JsonRandom::loadValue(entry["water"], rng, variables); + reward.revealTiles->scoreRock = JsonRandom::loadValue(entry["rock"], rng, variables); + } + for ( auto node : source["changeCreatures"].Struct() ) { CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 84cfdc8e3..f13a4a5a5 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -12,6 +12,7 @@ #include "Interface.h" #include "../CHeroHandler.h" +#include "../TerrainHandler.h" #include "../CSoundBase.h" #include "../NetPacks.h" #include "../spells/CSpellHandler.h" @@ -46,6 +47,52 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R cb->giveResources(hero->tempOwner, info.reward.resources); + if (info.reward.revealTiles) + { + auto const & props = *info.reward.revealTiles; + FoWChange fw; + + if (props.hide) + fw.mode = FoWChange::Mode::HIDE; + else + fw.mode = FoWChange::Mode::REVEAL; + + fw.player = hero->tempOwner; + + auto const functor = [&props](const TerrainTile * tile) + { + int score = 0; + if (tile->terType->isSurface()) + score += props.scoreSurface; + + if (tile->terType->isUnderground()) + score += props.scoreSubterra; + + if (tile->terType->isWater()) + score += props.scoreWater; + + if (tile->terType->isRock()) + score += props.scoreRock; + + return score > 0; + }; + + if (props.radius > 0) + { + cb->getTilesInRange(fw.tiles, hero->getSightCenter(), props.radius, hero->tempOwner, 1); + vstd::erase_if(fw.tiles, [&](const int3 & coord){ + return functor(cb->getTile(coord)); + }); + } + else + { + cb->getAllTiles(fw.tiles, hero->tempOwner, -1, functor); + } + + cb->sendAndApply(&fw); + + } + for(const auto & entry : info.reward.secondary) { int current = hero->getSecSkillLevel(entry.first); diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index bbdbf3cdc..dad53105a 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -18,6 +18,11 @@ VCMI_LIB_NAMESPACE_BEGIN +void Rewardable::RewardRevealTiles::serializeJson(JsonSerializeFormat & handler) +{ + // TODO +} + Rewardable::Reward::Reward() : heroExperience(0) , heroLevel(0) diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 9493083f1..b842e5929 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -27,6 +27,34 @@ namespace Rewardable struct Reward; using RewardsList = std::vector>; +struct RewardRevealTiles +{ + /// Reveal distance, if not positive - reveal entire map + int radius; + /// Reveal score of terrains with "surface" flag set + int scoreSurface; + /// Reveal score of terrains with "subterra" flag set + int scoreSubterra; + /// Reveal score of terrains with "water" flag set + int scoreWater; + /// Reveal score of terrains with "rock" flag set + int scoreRock; + /// If set, then terrain will be instead hidden for all enemies (Cover of Darkness) + bool hide; + + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & radius; + h & scoreSurface; + h & scoreSubterra; + h & scoreWater; + h & scoreRock; + h & hide; + } +}; + /// Reward that can be granted to a hero /// NOTE: eventually should replace seer hut rewards and events/pandoras struct DLL_LINKAGE Reward final @@ -74,6 +102,8 @@ struct DLL_LINKAGE Reward final /// list of components that will be added to reward description. First entry in list will override displayed component std::vector extraComponents; + std::optional revealTiles; + /// if set to true, object will be removed after granting reward bool removeObject; @@ -107,8 +137,8 @@ struct DLL_LINKAGE Reward final h & spells; h & creatures; h & creaturesChange; - if(version >= 821) - h & spellCast; + h & revealTiles; + h & spellCast; } void serializeJson(JsonSerializeFormat & handler); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 27412a5e2..8653120b0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -858,7 +858,7 @@ void CGameHandler::onNewTurn() if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player { FoWChange fw; - fw.mode = 1; + fw.mode = FoWChange::Mode::REVEAL; fw.player = player; // find all hidden tiles const auto fow = getPlayerTeam(player)->fogOfWarMap; @@ -2389,7 +2389,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, // now when everything is built - reveal tiles for lookout tower FoWChange fw; fw.player = t->tempOwner; - fw.mode = 1; + fw.mode = FoWChange::Mode::REVEAL; getTilesInRange(fw.tiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, 1); sendAndApply(&fw); @@ -4135,7 +4135,7 @@ void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor p FoWChange fow; fow.tiles = tiles; fow.player = player; - fow.mode = hide? 0 : 1; + fow.mode = hide ? FoWChange::Mode::HIDE : FoWChange::Mode::REVEAL; sendAndApply(&fow); } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 7cadfda29..7df0a3cb9 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -346,7 +346,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player) void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) { FoWChange fc; - fc.mode = reveal; + fc.mode = reveal ? FoWChange::Mode::REVEAL : FoWChange::Mode::HIDE; fc.player = player; const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap; const auto & mapSize = gameHandler->gameState()->getMapSize(); @@ -356,7 +356,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) for(int z = 0; z < mapSize.z; z++) for(int x = 0; x < mapSize.x; x++) for(int y = 0; y < mapSize.y; y++) - if(!(*fowMap)[z][x][y] || !fc.mode) + if(!(*fowMap)[z][x][y] || fc.mode == FoWChange::Mode::HIDE) hlp_tab[lastUnc++] = int3(x, y, z); fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); From dd841bdaa7fa2ebaed4cada5959ece708bfc5ade Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 15:31:42 +0300 Subject: [PATCH 0842/1248] Use enum instead of mix of bool's and int's for tile reveal --- client/Client.h | 4 +-- client/NetPacksClient.cpp | 2 +- lib/CGameInfoCallback.cpp | 3 +- lib/IGameCallback.cpp | 6 ++-- lib/IGameCallback.h | 8 ++--- lib/NetPacks.h | 8 +---- lib/NetPacksLib.cpp | 6 ++-- lib/constants/Enumerations.h | 6 ++++ lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 6 +--- lib/mapObjects/MiscObjects.cpp | 4 +-- lib/pathfinder/CPathfinder.cpp | 2 +- lib/rewardable/Interface.cpp | 12 +++---- server/CGameHandler.cpp | 36 +++++++++++--------- server/CGameHandler.h | 4 +-- server/processors/PlayerMessageProcessor.cpp | 4 +-- 16 files changed, 55 insertions(+), 58 deletions(-) diff --git a/client/Client.h b/client/Client.h index b9c98768b..86538827c 100644 --- a/client/Client.h +++ b/client/Client.h @@ -209,8 +209,8 @@ public: void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index affbdf49e..483bb1126 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -177,7 +177,7 @@ void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) } if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) { - if(pack.mode == FoWChange::Mode::REVEAL) + if(pack.mode == ETileVisibility::REVEALED) i.second->tileRevealed(pack.tiles); else i.second->tileHidden(pack.tiles); diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 45b72a8b4..2be582779 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -942,7 +942,7 @@ bool CGameInfoCallback::isInTheMap(const int3 &pos) const void CGameInfoCallback::getVisibleTilesInRange(std::unordered_set &tiles, int3 pos, int radious, int3::EDistanceFormula distanceFormula) const { - gs->getTilesInRange(tiles, pos, radious, *getPlayerID(), -1, distanceFormula); + gs->getTilesInRange(tiles, pos, radious, ETileVisibility::REVEALED, *getPlayerID(), distanceFormula); } void CGameInfoCallback::calculatePaths(const std::shared_ptr & config) @@ -955,7 +955,6 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo & gs->calculatePaths(hero, out); } - const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const { return gs->map->artInstances[aid.num]; diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 55ceaf9cd..673f8cfa7 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -78,8 +78,8 @@ void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, const int3 & pos, int radious, + ETileVisibility mode, std::optional player, - int mode, int3::EDistanceFormula distanceFormula) const { if(!!player && !player->isValidPlayer()) @@ -102,8 +102,8 @@ void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, if(distance <= radious) { if(!player - || (mode == 1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) - || (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) + || (mode == ETileVisibility::HIDDEN && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) + || (mode == ETileVisibility::REVEALED && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) ) tiles.insert(int3(xd,yd,pos.z)); } diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index e1f82f87c..33d7737bf 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -51,9 +51,9 @@ public: //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed void getTilesInRange(std::unordered_set & tiles, const int3 & pos, - int radious, + int radius, + ETileVisibility mode, std::optional player = std::optional(), - int mode = 0, int3::EDistanceFormula formula = int3::DIST_2D) const; //returns all tiles on given level (-1 - both levels, otherwise number of level) @@ -125,8 +125,8 @@ public: virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; virtual void sendAndApply(CPackForClient * pack) = 0; virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map - virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; - virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; + virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) = 0; + virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) = 0; virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; }; diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 017cc0118..6bc6e3f9d 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -344,17 +344,11 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient struct DLL_LINKAGE FoWChange : public CPackForClient { - enum class Mode : uint8_t - { - HIDE, - REVEAL - }; - void applyGs(CGameState * gs); std::unordered_set tiles; PlayerColor player; - Mode mode; + ETileVisibility mode; bool waitForDialogs = false; virtual void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 65d4878af..5dddc2ff4 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -924,9 +924,9 @@ void FoWChange::applyGs(CGameState *gs) TeamState * team = gs->getPlayerTeam(player); auto fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode != Mode::HIDE; + (*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; - if (mode == Mode::HIDE) //do not hide too much + if (mode == ETileVisibility::HIDDEN) //do not hide too much { std::unordered_set tilesRevealed; for (auto & elem : gs->map->objects) @@ -941,7 +941,7 @@ void FoWChange::applyGs(CGameState *gs) case Obj::TOWN: case Obj::ABANDONED_MINE: if(vstd::contains(team->players, o->tempOwner)) //check owned observators - gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1); + gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), ETileVisibility::HIDDEN, o->tempOwner); break; } } diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 1c2cd7b06..c594cc271 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -250,4 +250,10 @@ enum class EBattleResult : int8_t SURRENDER = 2, }; +enum class ETileVisibility : int8_t // Fog of war change +{ + HIDDEN, + REVEALED +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index ffdd990df..87f8fac92 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -939,7 +939,7 @@ void CGameState::initFogOfWar() if(!obj || !vstd::contains(elem.second.players, obj->tempOwner)) continue; //not a flagged object std::unordered_set tiles; - getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), obj->tempOwner, 1); + getTilesInRange(tiles, obj->getSightCenter(), obj->getSightRadius(), ETileVisibility::HIDDEN, obj->tempOwner); for(const int3 & tile : tiles) { (*elem.second.fogOfWarMap)[tile.z][tile.x][tile.y] = 1; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 78c4399ba..36543e448 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1066,11 +1066,7 @@ void CGTownInstance::battleFinished(const CGHeroInstance * hero, const BattleRes void CGTownInstance::onTownCaptured(const PlayerColor & winner) const { setOwner(winner); - FoWChange fw; - fw.player = winner; - fw.mode = FoWChange::Mode::REVEAL; - cb->getTilesInRange(fw.tiles, getSightCenter(), getSightRadius(), winner, 1); - cb->sendAndApply(& fw); + cb->changeFogOfWar(getSightCenter(), getSightRadius(), winner, ETileVisibility::REVEALED); } void CGTownInstance::afterAddToMap(CMap * map) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 36f7fd91a..26c1d0e38 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1141,14 +1141,14 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const FoWChange fw; fw.player = h->tempOwner; - fw.mode = FoWChange::Mode::REVEAL; + fw.mode = ETileVisibility::REVEALED; fw.waitForDialogs = true; for(const auto & it : eyelist[subID]) { const CGObjectInstance *eye = cb->getObj(it); - cb->getTilesInRange (fw.tiles, eye->pos, 10, h->tempOwner, 1); + cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); cb->sendAndApply(&fw); cv.pos = eye->pos; diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 8044535fb..c5d8cf9a3 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -368,7 +368,7 @@ void CPathfinderHelper::initializePatrol() if(hero->patrol.patrolRadius) { state = PATROL_RADIUS; - gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, std::optional(), 0, int3::DIST_MANHATTAN); + gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, ETileVisibility::REVEALED, std::optional(), int3::DIST_MANHATTAN); } else state = PATROL_LOCKED; diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index f13a4a5a5..31b5a74e4 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -49,17 +49,17 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R if (info.reward.revealTiles) { - auto const & props = *info.reward.revealTiles; + const auto & props = *info.reward.revealTiles; FoWChange fw; if (props.hide) - fw.mode = FoWChange::Mode::HIDE; + fw.mode = ETileVisibility::HIDDEN; else - fw.mode = FoWChange::Mode::REVEAL; + fw.mode = ETileVisibility::REVEALED; fw.player = hero->tempOwner; - auto const functor = [&props](const TerrainTile * tile) + const auto functor = [&props](const TerrainTile * tile) { int score = 0; if (tile->terType->isSurface()) @@ -79,9 +79,9 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R if (props.radius > 0) { - cb->getTilesInRange(fw.tiles, hero->getSightCenter(), props.radius, hero->tempOwner, 1); + cb->getTilesInRange(fw.tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->tempOwner); vstd::erase_if(fw.tiles, [&](const int3 & coord){ - return functor(cb->getTile(coord)); + return !functor(cb->getTile(coord)); }); } else diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8653120b0..fec358bf8 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -858,7 +858,7 @@ void CGameHandler::onNewTurn() if (player != PlayerColor::NEUTRAL) //do not reveal fow for neutral player { FoWChange fw; - fw.mode = FoWChange::Mode::REVEAL; + fw.mode = ETileVisibility::REVEALED; fw.player = player; // find all hidden tiles const auto fow = getPlayerTeam(player)->fogOfWarMap; @@ -879,7 +879,7 @@ void CGameHandler::onNewTurn() { if (getPlayerStatus(player.first) == EPlayerStatus::INGAME && getPlayerRelations(player.first, t->tempOwner) == PlayerRelations::ENEMIES) - changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, true); + changeFogOfWar(t->visitablePos(), t->getBonusLocalFirst(Selector::type()(BonusType::DARKNESS))->val, player.first, ETileVisibility::HIDDEN); } } } @@ -1174,7 +1174,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo { obj->onHeroLeave(h); } - this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), h->tempOwner, 1); + this->getTilesInRange(tmh.fowRevealed, h->getSightCenter()+(tmh.end-tmh.start), h->getSightRadius(), ETileVisibility::HIDDEN, h->tempOwner); }; auto doMove = [&](TryMoveHero::EResult result, EGuardLook lookForGuards, @@ -1523,7 +1523,7 @@ void CGameHandler::giveHero(ObjectInstanceID id, PlayerColor player, ObjectInsta //Reveal fow around new hero, especially released from Prison auto h = getHero(id); - changeFogOfWar(h->pos, h->getSightRadius(), player, false); + changeFogOfWar(h->pos, h->getSightRadius(), player, ETileVisibility::REVEALED); } void CGameHandler::changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) @@ -2387,11 +2387,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, processAfterBuiltStructure(builtID); // now when everything is built - reveal tiles for lookout tower - FoWChange fw; - fw.player = t->tempOwner; - fw.mode = FoWChange::Mode::REVEAL; - getTilesInRange(fw.tiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, 1); - sendAndApply(&fw); + changeFogOfWar(t->getSightCenter(), t->getSightRadius(), t->getOwner(), ETileVisibility::REVEALED); if(t->visitingHero) visitCastleObjects(t, t->visitingHero); @@ -4108,34 +4104,40 @@ void CGameHandler::removeAfterVisit(const CGObjectInstance *object) assert("This function needs to be called during the object visit!"); } -void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) { std::unordered_set tiles; - getTilesInRange(tiles, center, radius, player, hide? -1 : 1); - if (hide) + + if (mode == ETileVisibility::HIDDEN) { + getTilesInRange(tiles, center, radius, ETileVisibility::REVEALED, player); + std::unordered_set observedTiles; //do not hide tiles observed by heroes. May lead to disastrous AI problems auto p = getPlayerState(player); for (auto h : p->heroes) { - getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), h->tempOwner, -1); + getTilesInRange(observedTiles, h->getSightCenter(), h->getSightRadius(), ETileVisibility::REVEALED, h->tempOwner); } for (auto t : p->towns) { - getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), t->tempOwner, -1); + getTilesInRange(observedTiles, t->getSightCenter(), t->getSightRadius(), ETileVisibility::REVEALED, t->tempOwner); } for (auto tile : observedTiles) vstd::erase_if_present (tiles, tile); } - changeFogOfWar(tiles, player, hide); + else + { + getTilesInRange(tiles, center, radius, ETileVisibility::HIDDEN, player); + } + changeFogOfWar(tiles, player, mode); } -void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) +void CGameHandler::changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) { FoWChange fow; fow.tiles = tiles; fow.player = player; - fow.mode = hide ? FoWChange::Mode::HIDE : FoWChange::Mode::REVEAL; + fow.mode = mode; sendAndApply(&fow); } diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 2784aa6a7..582eb0c2b 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -148,8 +148,8 @@ public: void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override; + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override; + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player,ETileVisibility mode) override; void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 7df0a3cb9..62b30a72e 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -346,7 +346,7 @@ void PlayerMessageProcessor::cheatDefeat(PlayerColor player) void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) { FoWChange fc; - fc.mode = reveal ? FoWChange::Mode::REVEAL : FoWChange::Mode::HIDE; + fc.mode = reveal ? ETileVisibility::REVEALED : ETileVisibility::HIDDEN; fc.player = player; const auto & fowMap = gameHandler->gameState()->getPlayerTeam(player)->fogOfWarMap; const auto & mapSize = gameHandler->gameState()->getMapSize(); @@ -356,7 +356,7 @@ void PlayerMessageProcessor::cheatMapReveal(PlayerColor player, bool reveal) for(int z = 0; z < mapSize.z; z++) for(int x = 0; x < mapSize.x; x++) for(int y = 0; y < mapSize.y; y++) - if(!(*fowMap)[z][x][y] || fc.mode == FoWChange::Mode::HIDE) + if(!(*fowMap)[z][x][y] || fc.mode == ETileVisibility::HIDDEN) hlp_tab[lastUnc++] = int3(x, y, z); fc.tiles.insert(hlp_tab, hlp_tab + lastUnc); From bb05c2dea561da4ec29bedbdc2056189faba6476 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 16:49:17 +0300 Subject: [PATCH 0843/1248] Implemented configurable shrine --- cmake_modules/VCMI_lib.cmake | 2 - config/gameConfig.json | 4 +- config/objects/generic.json | 72 ----- config/objects/rewardableGeneric.json | 277 ------------------ .../CObjectClassesHandler.cpp | 2 - .../ShrineInstanceConstructor.cpp | 39 --- .../ShrineInstanceConstructor.h | 34 --- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 71 ----- lib/mapObjects/MiscObjects.h | 21 -- lib/mapping/MapFormatH3M.cpp | 20 +- lib/mapping/MapFormatH3M.h | 2 +- lib/registerTypes/RegisterTypes.h | 4 - lib/rewardable/Info.cpp | 1 + lib/rewardable/Limiter.cpp | 6 + lib/rewardable/Limiter.h | 6 + test/mock/mock_IGameCallback.h | 4 +- 17 files changed, 36 insertions(+), 531 deletions(-) delete mode 100644 config/objects/rewardableGeneric.json delete mode 100644 lib/mapObjectConstructors/ShrineInstanceConstructor.cpp delete mode 100644 lib/mapObjectConstructors/ShrineInstanceConstructor.h diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 8c2a6a274..b00149829 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -81,7 +81,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/DwellingInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/HillFortInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.cpp - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.cpp ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.cpp ${MAIN_LIB_DIR}/mapObjects/CBank.cpp @@ -425,7 +424,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapObjectConstructors/IObjectInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/RandomMapInfo.h ${MAIN_LIB_DIR}/mapObjectConstructors/ShipyardInstanceConstructor.h - ${MAIN_LIB_DIR}/mapObjectConstructors/ShrineInstanceConstructor.h ${MAIN_LIB_DIR}/mapObjectConstructors/SObjectSounds.h ${MAIN_LIB_DIR}/mapObjects/CArmedInstance.h diff --git a/config/gameConfig.json b/config/gameConfig.json index bed5db522..0855ca7ba 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -52,7 +52,9 @@ "config/objects/moddables.json", "config/objects/creatureBanks.json", "config/objects/dwellings.json", - "config/objects/rewardableGeneric.json", + "config/objects/rewardableObservatories.json", + "config/objects/rewardableWitchHut.json", + "config/objects/rewardableShrine.json", "config/objects/rewardableOncePerWeek.json", "config/objects/rewardablePickable.json", "config/objects/rewardableOnceVisitable.json", diff --git a/config/objects/generic.json b/config/objects/generic.json index a2c188ed1..4b26bdafe 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -230,78 +230,6 @@ } } }, - "shrineOfMagicLevel1" : {//incantation - "index" :88, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 500, - "rmg" : { - "value" : 500, - "rarity" : 100 - }, - "visitText" : 127, - "spell" : { - "level" : 1 - } - } - } - }, - "shrineOfMagicLevel2" : {//gesture - "index" :89, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 2000, - "rmg" : { - "value" : 2000, - "rarity" : 100 - }, - "visitText" : 128, - "spell" : { - "level" : 2 - } - } - } - }, - "shrineOfMagicLevel3" : {//thinking - "index" :90, - "handler" : "shrine", - "base" : { - "sounds" : { - "ambient" : ["LOOPSHRIN"], - "visit" : ["TEMPLE"] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 3000, - "rmg" : { - "value" : 3000, - "rarity" : 100 - }, - "visitText" : 129, - "spell" : { - "level" : 3 - } - } - } - }, "eyeOfTheMagi" : { "index" :27, "handler" : "magi", diff --git a/config/objects/rewardableGeneric.json b/config/objects/rewardableGeneric.json deleted file mode 100644 index cd36c3aac..000000000 --- a/config/objects/rewardableGeneric.json +++ /dev/null @@ -1,277 +0,0 @@ -{ - "witchHut" : { - "index" :113, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"] - } - }, - "types" : { - "witchHut" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "zoneLimit" : 3, - "value" : 1500, - "rarity" : 80 - }, - "compatibilityIdentifiers" : [ "object" ], - - "visitMode" : "limiter", - - "variables" : { - "secondarySkill" : { - "gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI - "noneOf" : [ - "leadership", - "necromancy" - ] - } - } - }, - "visitLimiter" : { - "secondary" : { - "@gainedSkill" : 1 - } - }, - "rewards" : [ - { - "limiter" : { - "canLearnSkills" : true, - "noneOf" : [ - { - "secondary" : { - "@gainedSkill" : 1 - } - } - ] - }, - "secondary" : { - "@gainedSkill" : 1 - }, - "message" : 171 // Witch teaches you skill - } - ], - "onVisited" : [ - { - "message" : 172 // You already known this skill - } - ], - "onEmpty" : [ - { - "message" : 173 // You know too much (no free slots) - } - ] - } - } - }, - - "redwoodObservatory" : { - "index" :58, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "redwoodObservatory" : { - "index" : 0, - "aiValue" : 750, - "templates" : - { - "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, - "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } - }, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 98, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1 - } - } - ] - } - } - }, - - "pillarOfFire" : { - "index" :60, - "handler" : "configurable", - "base" : { - "sounds" : { - "ambient" : ["LOOPFIRE"], - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "pillarOfFire" : { - "index" : 0, - "aiValue" : 750, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 99, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1 - } - } - ] - } - } - }, - - "coverOfDarkness" : { - "index" :15, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "coverOfDarkness" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 31, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1, - "hide" : true - } - } - ] - } - } - }, - - "cartographer" : { - "index" :13, - "handler": "configurable", - "lastReservedIndex" : 2, - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "cartographerWater" : { - "index" : 0, - "aiValue" : 5000, - "rmg" : { - "zoneLimit" : 1, - "value" : 5000, - "rarity" : 20 - }, - "compatibilityIdentifiers" : [ "water" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 25, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "water" : 1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - }, - "cartographerLand" : { - "index" : 1, - "aiValue": 10000, - "rmg" : { - "zoneLimit" : 1, - "value" : 10000, - "rarity" : 2 - }, - "compatibilityIdentifiers" : [ "land" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 26, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "surface" : 1, - "water" : -1, - "rock" : -1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - }, - "cartographerSubterranean" : { - "index" : 2, - "aiValue" : 7500, - "rmg" : { - "zoneLimit" : 1, - "value" : 7500, - "rarity" : 20 - }, - "compatibilityIdentifiers" : [ "subterra" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 27, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "subterra" : 1, - "water" : -1, - "rock" : -1, - "surface" : -1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - } - } - } -} \ No newline at end of file diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 9b8b5ecff..4b98d0d1d 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -26,7 +26,6 @@ #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGPandoraBox.h" #include "../mapObjects/CQuest.h" @@ -54,7 +53,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER_CLASS("bank", CBankInstanceConstructor); SET_HANDLER_CLASS("boat", BoatInstanceConstructor); SET_HANDLER_CLASS("market", MarketInstanceConstructor); - SET_HANDLER_CLASS("shrine", ShrineInstanceConstructor); SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor); SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor); SET_HANDLER_CLASS("monster", CreatureInstanceConstructor); diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp b/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp deleted file mode 100644 index 84df1eb94..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.cpp +++ /dev/null @@ -1,39 +0,0 @@ -/* -* ShrineInstanceConstructor.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 "ShrineInstanceConstructor.h" - -#include "../mapObjects/MiscObjects.h" -#include "../JsonRandom.h" -#include "../IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void ShrineInstanceConstructor::initTypeData(const JsonNode & config) -{ - parameters = config; -} - -void ShrineInstanceConstructor::randomizeObject(CGShrine * shrine, CRandomGenerator & rng) const -{ - JsonRandom::Variables emptyVariables; - - auto visitTextParameter = parameters["visitText"]; - - if (visitTextParameter.isNumber()) - shrine->visitText.appendLocalString(EMetaText::ADVOB_TXT, static_cast(visitTextParameter.Float())); - else - shrine->visitText.appendRawString(visitTextParameter.String()); - - if(shrine->spell == SpellID::NONE) // shrine has no predefined spell - shrine->spell =JsonRandom::loadSpell(parameters["spell"], rng, emptyVariables); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShrineInstanceConstructor.h b/lib/mapObjectConstructors/ShrineInstanceConstructor.h deleted file mode 100644 index 0ceb22690..000000000 --- a/lib/mapObjectConstructors/ShrineInstanceConstructor.h +++ /dev/null @@ -1,34 +0,0 @@ -/* -* ShrineInstanceConstructor.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 "CDefaultObjectTypeHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGShrine; - -class ShrineInstanceConstructor final : public CDefaultObjectTypeHandler -{ - JsonNode parameters; - -protected: - void initTypeData(const JsonNode & config) override; - void randomizeObject(CGShrine * object, CRandomGenerator & rng) const override; - -public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ab047c30d..9e9396678 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -772,7 +772,7 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const { - if(!hasSpellbook()) + if(!hasSpellbook()) return false; if(spell->getLevel() > maxSpellLevel()) //not enough wisdom diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 26c1d0e38..e153b647c 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -842,77 +842,6 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) } } -void CGShrine::onHeroVisit( const CGHeroInstance * h ) const -{ - if(spell == SpellID::NONE) - { - logGlobal->error("Not initialized shrine visited!"); - return; - } - - if(!wasVisited(h->tempOwner)) - cb->setObjProperty(id, CGShrine::OBJPROP_VISITED, h->tempOwner.getNum()); - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text = visitText; - iw.text.appendLocalString(EMetaText::SPELL_NAME,spell); - iw.text.appendRawString("."); - - if(!h->getArt(ArtifactPosition::SPELLBOOK)) - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,131); - } - else if(h->spellbookContainsSpell(spell))//hero already knows the spell - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,174); - } - else if(spell.toSpell()->getLevel() > h->maxSpellLevel()) //it's third level spell and hero doesn't have wisdom - { - iw.text.appendLocalString(EMetaText::ADVOB_TXT,130); - } - else //give spell - { - std::set spells; - spells.insert(spell); - cb->changeSpells(h, true, spells); - - iw.components.emplace_back(Component::EComponentType::SPELL, spell, 0, 0); - } - - cb->showInfoDialog(&iw); -} - -void CGShrine::initObj(CRandomGenerator & rand) -{ - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); -} - -std::string CGShrine::getHoverText(PlayerColor player) const -{ - std::string hoverName = getObjectName(); - if(wasVisited(player)) - { - hoverName += "\n" + VLC->generaltexth->allTexts[355]; // + (learn %s) - boost::algorithm::replace_first(hoverName,"%s", spell.toSpell()->getNameTranslated()); - } - return hoverName; -} - -std::string CGShrine::getHoverText(const CGHeroInstance * hero) const -{ - std::string hoverName = getHoverText(hero->tempOwner); - if(wasVisited(hero->tempOwner) && hero->spellbookContainsSpell(spell)) //know what spell there is and hero knows that spell - hoverName += "\n\n" + VLC->generaltexth->allTexts[354]; // (Already learned) - return hoverName; -} - -void CGShrine::serializeJsonOptions(JsonSerializeFormat & handler) -{ - handler.serializeId("spell", spell, SpellID::NONE); -} - void CGSignBottle::initObj(CRandomGenerator & rand) { //if no text is set than we pick random from the predefined ones diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index ce7e7fcb2..8743b7354 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -149,27 +149,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGShrine : public CTeamVisited -{ -public: - MetaString visitText; - SpellID spell; //id of spell or NONE if random - - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - std::string getHoverText(PlayerColor player) const override; - std::string getHoverText(const CGHeroInstance * hero) const override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this);; - h & spell; - h & visitText; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGMine : public CArmedInstance { public: diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 8d5ce2d69..bd361bb77 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1325,10 +1325,22 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s return object; } -CGObjectInstance * CMapLoaderH3M::readShrine() +CGObjectInstance * CMapLoaderH3M::readShrine(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGShrine(); - object->spell = reader->readSpell32(); + auto * object = readGeneric(position, objectTemplate); + auto * rewardable = dynamic_cast(object); + + assert(rewardable); + + SpellID spell = reader->readSpell32(); + + if(spell != SpellID::NONE) + { + JsonNode variable; + variable.String() = VLC->spells()->getById(spell)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); // list may include spells from all mods + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + } return object; } @@ -1514,7 +1526,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objTempl); - CGObjectInstance * readShrine(); + CGObjectInstance * readShrine(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readHeroPlaceholder(const int3 & position); CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index e8ed5239e..0c543d3f5 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -23,7 +23,6 @@ #include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjectConstructors/HillFortInstanceConstructor.h" #include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" #include "../mapObjects/MapObjects.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGTownBuilding.h" @@ -102,7 +101,6 @@ void registerTypesMapObjectTypes(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -136,7 +134,6 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGScholar); REGISTER_GENERIC_HANDLER(CGSeerHut); REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGShrine); REGISTER_GENERIC_HANDLER(CGSignBottle); REGISTER_GENERIC_HANDLER(CGSirens); REGISTER_GENERIC_HANDLER(CGMonolith); @@ -173,7 +170,6 @@ void registerTypesMapObjects2(Serializer &s) s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); //s.template registerType(); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index a7d63a1b9..c89f617fc 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -119,6 +119,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); + limiter.canLearnSpells = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables); limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); limiter.players = JsonRandom::loadColors(source["colors"], rng); diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 3c14dde6b..b7c4352fd 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -125,6 +125,12 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const return false; } + for(const auto & spell : canLearnSpells) + { + if (!hero->canLearnSpell(spell.toSpell(VLC->spells()))) + return false; + } + { std::unordered_map artifactsRequirements; // artifact ID -> required count for(const auto & art : artifacts) diff --git a/lib/rewardable/Limiter.h b/lib/rewardable/Limiter.h index cc1762924..407db0f24 100644 --- a/lib/rewardable/Limiter.h +++ b/lib/rewardable/Limiter.h @@ -61,6 +61,9 @@ struct DLL_LINKAGE Limiter final /// Spells that hero must have in the spellbook std::vector spells; + /// Spells that hero must be able to learn + std::vector canLearnSpells; + /// creatures that hero needs to have std::vector creatures; @@ -97,10 +100,13 @@ struct DLL_LINKAGE Limiter final h & heroLevel; h & manaPoints; h & manaPercentage; + h & canLearnSkills; h & resources; h & primary; h & secondary; h & artifacts; + h & spells; + h & canLearnSpells; h & creatures; h & heroes; h & heroClasses; diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index 42dc8d3cc..f92c1690c 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -85,8 +85,8 @@ public: void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {} void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {} void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {} //when two heroes meet on adventure map - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override {} + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, ETileVisibility mode) override {} void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {} ///useful callback methods From ca368f606f948307ac9956598631d80e72c263a6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 18:36:14 +0300 Subject: [PATCH 0844/1248] Added new files --- config/gameConfig.json | 1 + config/objects/generic.json | 20 -- config/objects/rewardableObservatories.json | 209 ++++++++++++++++++++ config/objects/rewardableScholar.json | 22 +++ config/objects/rewardableShrine.json | 197 ++++++++++++++++++ config/objects/rewardableWitchHut.json | 61 ++++++ 6 files changed, 490 insertions(+), 20 deletions(-) create mode 100644 config/objects/rewardableObservatories.json create mode 100644 config/objects/rewardableScholar.json create mode 100644 config/objects/rewardableShrine.json create mode 100644 config/objects/rewardableWitchHut.json diff --git a/config/gameConfig.json b/config/gameConfig.json index 0855ca7ba..27842d3f9 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -55,6 +55,7 @@ "config/objects/rewardableObservatories.json", "config/objects/rewardableWitchHut.json", "config/objects/rewardableShrine.json", + "config/objects/rewardableScholar.json", "config/objects/rewardableOncePerWeek.json", "config/objects/rewardablePickable.json", "config/objects/rewardableOnceVisitable.json", diff --git a/config/objects/generic.json b/config/objects/generic.json index 4b26bdafe..396ddc374 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -318,26 +318,6 @@ } } }, - "scholar" : { - "index" :81, - "handler" : "scholar", - "base" : { - "sounds" : { - "visit" : ["GAZEBO"], - "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] - } - }, - "types" : { - "object" : { - "index" : 0, - "aiValue" : 1500, - "rmg" : { - "value" : 1500, - "rarity" : 100 - } - } - } - }, "shipyard" : { "index" :87, "handler" : "shipyard", diff --git a/config/objects/rewardableObservatories.json b/config/objects/rewardableObservatories.json new file mode 100644 index 000000000..7e6d60fdf --- /dev/null +++ b/config/objects/rewardableObservatories.json @@ -0,0 +1,209 @@ +{ + "redwoodObservatory" : { + "index" :58, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "redwoodObservatory" : { + "index" : 0, + "aiValue" : 750, + "templates" : + { + "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, + "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } + }, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 98, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "pillarOfFire" : { + "index" :60, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFIRE"], + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "pillarOfFire" : { + "index" : 0, + "aiValue" : 750, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 99, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "coverOfDarkness" : { + "index" :15, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "coverOfDarkness" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 31, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true + } + } + ] + } + } + }, + + "cartographer" : { + "index" :13, + "handler": "configurable", + "lastReservedIndex" : 2, + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "cartographerWater" : { + "index" : 0, + "aiValue" : 5000, + "rmg" : { + "zoneLimit" : 1, + "value" : 5000, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "water" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 25, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "water" : 1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerLand" : { + "index" : 1, + "aiValue": 10000, + "rmg" : { + "zoneLimit" : 1, + "value" : 10000, + "rarity" : 2 + }, + "compatibilityIdentifiers" : [ "land" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 26, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "surface" : 1, + "water" : -1, + "rock" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerSubterranean" : { + "index" : 2, + "aiValue" : 7500, + "rmg" : { + "zoneLimit" : 1, + "value" : 7500, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "subterra" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 27, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "subterra" : 1, + "water" : -1, + "rock" : -1, + "surface" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableScholar.json b/config/objects/rewardableScholar.json new file mode 100644 index 000000000..ab4453d8c --- /dev/null +++ b/config/objects/rewardableScholar.json @@ -0,0 +1,22 @@ +{ + "scholar" : { + "index" :81, + "handler" : "scholar", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"], + "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] + } + }, + "types" : { + "object" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "value" : 1500, + "rarity" : 100 + } + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableShrine.json b/config/objects/rewardableShrine.json new file mode 100644 index 000000000..3f7e3b36f --- /dev/null +++ b/config/objects/rewardableShrine.json @@ -0,0 +1,197 @@ +{ + "shrineOfMagicLevel1" : {//incantation + "index" :88, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel1" : { + "index" : 0, + "aiValue" : 500, + "rmg" : { + "value" : 500, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 1 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "message" : 127 // You learn new spell + } + ], + "onVisitedMessage" : 174, // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellbook" + } + ] + }, + "message" : 130 // No Wisdom + }, + { + "message" : 131 // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel2" : {//gesture + "index" :89, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel2" : { + "index" : 0, + "aiValue" : 2000, + "rmg" : { + "value" : 2000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 2 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "message" : 128 // You learn new spell + } + ], + "onVisitedMessage" : 174, // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellbook" + } + ] + }, + "message" : 130 // No Wisdom + }, + { + "message" : 131 // No spellbook + } + ] + } + } + }, + "shrineOfMagicLevel3" : {//thinking + "index" :90, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPSHRIN"], + "visit" : ["TEMPLE"] + } + }, + "types" : { + "shrineOfMagicLevel3" : { + "index" : 0, + "aiValue" : 3000, + "rmg" : { + "value" : 3000, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + "level": 3 + } + } + }, + "visitLimiter" : { + "spells" : [ + "@gainedSpell" + ] + }, + "rewards" : [ + { + "limiter" : { + "canLearnSpells" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ], + "message" : 129 // You learn new spell + } + ], + "onVisitedMessage" : 174, // You already known this spell + "onEmpty" : [ + { + "limiter" : { + "artifacts" : [ + { + "type" : "spellbook" + } + ] + }, + "message" : 130 // No Wisdom + }, + { + "message" : 131 // No spellbook + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableWitchHut.json b/config/objects/rewardableWitchHut.json new file mode 100644 index 000000000..d63d5a883 --- /dev/null +++ b/config/objects/rewardableWitchHut.json @@ -0,0 +1,61 @@ +{ + "witchHut" : { + "index" :113, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["GAZEBO"] + } + }, + "types" : { + "witchHut" : { + "index" : 0, + "aiValue" : 1500, + "rmg" : { + "zoneLimit" : 3, + "value" : 1500, + "rarity" : 80 + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "limiter", + + "variables" : { + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading and by AI + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } + }, + "visitLimiter" : { + "secondary" : { + "@gainedSkill" : 1 + } + }, + "rewards" : [ + { + "limiter" : { + "canLearnSkills" : true, + "noneOf" : [ + { + "secondary" : { + "@gainedSkill" : 1 + } + } + ] + }, + "secondary" : { + "@gainedSkill" : 1 + }, + "message" : 171 // Witch teaches you skill + } + ], + "onVisitedMessage" : 172, // You already known this skill + "onEmptyMessage" : 173 // You know too much (no free slots) + } + } + } +} \ No newline at end of file From e10de0594e7c38bce56c418d19375fe1daa538c1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 4 Oct 2023 19:32:16 +0300 Subject: [PATCH 0845/1248] Scholar is now configurable object (partial) --- config/objects/rewardableScholar.json | 82 +++++++++++- lib/JsonRandom.cpp | 13 +- lib/JsonRandom.h | 3 +- .../CObjectClassesHandler.cpp | 1 - lib/mapObjects/MiscObjects.cpp | 126 ------------------ lib/mapObjects/MiscObjects.h | 20 --- lib/mapping/MapFormatH3M.cpp | 20 ++- lib/mapping/MapFormatH3M.h | 2 +- lib/registerTypes/RegisterTypes.h | 2 - lib/rewardable/Info.cpp | 9 +- 10 files changed, 113 insertions(+), 165 deletions(-) diff --git a/config/objects/rewardableScholar.json b/config/objects/rewardableScholar.json index ab4453d8c..ebb73d090 100644 --- a/config/objects/rewardableScholar.json +++ b/config/objects/rewardableScholar.json @@ -1,7 +1,7 @@ { "scholar" : { "index" :81, - "handler" : "scholar", + "handler" : "configurable", "base" : { "sounds" : { "visit" : ["GAZEBO"], @@ -9,13 +9,89 @@ } }, "types" : { - "object" : { + "scholar" : { "index" : 0, "aiValue" : 1500, "rmg" : { "value" : 1500, "rarity" : 100 - } + }, + "compatibilityIdentifiers" : [ "object" ], + + "visitMode" : "unlimited", + "blockedVisitable" : true, + + "variables" : { + "spell" : { + "gainedSpell" : { // Note: this variable name is used by engine for H3M loading + } + }, + "secondarySkill" : { + "gainedSkill" : { // Note: this variable name is used by engine for H3M loading + } + }, + "primarySkill" : { + "gainedStat" : { // Note: this variable name is used by engine for H3M loading + } + } + }, + "selectMode" : "selectFirst", + "rewards" : [ + { + "appearChance" : { "min" : 0, "max" : 33 }, + "message" : 115, + "limiter" : { + "canLearnSpell" : [ + "@gainedSpell" + ] + }, + "spells" : [ + "@gainedSpell" + ] + "removeObject" : true + }, + { + "appearChance" : { "min" : 33, "max" : 66 }, + "message" : 115, + "limiter" : { + // Hero does not have this skill at expert + "noneOf" : [ + { + "secondarySkill" : { + "@gainedSkill" : 3 + } + } + ], + // And have either free skill slot or this skill + "anyOf" : [ + { + "canLearnSkills" : true + }, + { + "noneOf" : [ + { + "secondarySkill" : { + "@gainedSkill" : 1 + } + } + ] + } + ] + }, + "secondary" : { + "@gainedSkill" : 1 + }, + "removeObject" : true + }, + { + // Always present - fallback if hero can't learn secondary / spell + "message" : 115, + "primary" : { + "@gainedStat" : 1 + }, + "removeObject" : true + } + ] } } } diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 724ba3378..4e5434fee 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -280,8 +280,19 @@ namespace JsonRandom return ret; } + PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + { + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + std::set potentialPicks = filterKeys(value, defaultSkills, variables); + return *RandomGeneratorUtil::nextItem(potentialPicks, rng); + } - std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) + std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; if(value.isStruct()) diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 6623ab86f..0a6e34079 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -37,7 +37,8 @@ namespace JsonRandom DLL_LINKAGE TResources loadResources(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE TResources loadResource(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); - DLL_LINKAGE std::vector loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE PrimarySkill loadPrimary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); + DLL_LINKAGE std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE SecondarySkill loadSecondary(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE std::map loadSecondaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 4b98d0d1d..e7fd552b4 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -84,7 +84,6 @@ CObjectClassesHandler::CObjectClassesHandler() SET_HANDLER("pandora", CGPandoraBox); SET_HANDLER("prison", CGHeroInstance); SET_HANDLER("questGuard", CGQuestGuard); - SET_HANDLER("scholar", CGScholar); SET_HANDLER("seerHut", CGSeerHut); SET_HANDLER("sign", CGSignBottle); SET_HANDLER("siren", CGSirens); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index e153b647c..86b8dd933 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -874,132 +874,6 @@ void CGSignBottle::serializeJsonOptions(JsonSerializeFormat& handler) handler.serializeStruct("text", message); } -void CGScholar::onHeroVisit( const CGHeroInstance * h ) const -{ - EBonusType type = bonusType; - int bid = bonusID; - //check if the bonus if applicable, if not - give primary skill (always possible) - int ssl = h->getSecSkillLevel(SecondarySkill(bid)); //current sec skill level, used if bonusType == 1 - if((type == SECONDARY_SKILL && ((ssl == 3) || (!ssl && !h->canLearnSkill()))) ////hero already has expert level in the skill or (don't know skill and doesn't have free slot) - || (type == SPELL && !h->canLearnSpell(SpellID(bid).toSpell()))) - { - type = PRIM_SKILL; - bid = CRandomGenerator::getDefault().nextInt(GameConstants::PRIMARY_SKILLS - 1); - } - - InfoWindow iw; - iw.type = EInfoWindowMode::AUTO; - iw.player = h->getOwner(); - iw.text.appendLocalString(EMetaText::ADVOB_TXT,115); - - switch (type) - { - case PRIM_SKILL: - cb->changePrimSkill(h,static_cast(bid),+1); - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, bid, +1, 0); - break; - case SECONDARY_SKILL: - cb->changeSecSkill(h,SecondarySkill(bid),+1); - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, bid, ssl + 1, 0); - break; - case SPELL: - { - std::set hlp; - hlp.insert(SpellID(bid)); - cb->changeSpells(h,true,hlp); - iw.components.emplace_back(Component::EComponentType::SPELL, bid, 0, 0); - } - break; - default: - logGlobal->error("Error: wrong bonus type (%d) for Scholar!\n", static_cast(type)); - return; - } - - cb->showInfoDialog(&iw); - cb->removeObject(this, h->getOwner()); -} - -void CGScholar::initObj(CRandomGenerator & rand) -{ - blockVisit = true; - if(bonusType == RANDOM) - { - bonusType = static_cast(rand.nextInt(2)); - switch(bonusType) - { - case PRIM_SKILL: - bonusID = rand.nextInt(GameConstants::PRIMARY_SKILLS -1); - break; - case SECONDARY_SKILL: - bonusID = rand.nextInt(static_cast(VLC->skillh->size()) - 1); - break; - case SPELL: - std::vector possibilities; - cb->getAllowedSpells (possibilities); - bonusID = *RandomGeneratorUtil::nextItem(possibilities, rand); - break; - } - } -} - -void CGScholar::serializeJsonOptions(JsonSerializeFormat & handler) -{ - if(handler.saving) - { - std::string value; - switch(bonusType) - { - case PRIM_SKILL: - value = NPrimarySkill::names[bonusID]; - handler.serializeString("rewardPrimSkill", value); - break; - case SECONDARY_SKILL: - value = CSkillHandler::encodeSkill(bonusID); - handler.serializeString("rewardSkill", value); - break; - case SPELL: - value = SpellID::encode(bonusID); - handler.serializeString("rewardSpell", value); - break; - case RANDOM: - break; - } - } - else - { - //TODO: unify - const JsonNode & json = handler.getCurrent(); - bonusType = RANDOM; - if(!json["rewardPrimSkill"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "primSkill", json["rewardPrimSkill"].String()); - if(raw) - { - bonusType = PRIM_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSkill"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "skill", json["rewardSkill"].String()); - if(raw) - { - bonusType = SECONDARY_SKILL; - bonusID = raw.value(); - } - } - else if(!json["rewardSpell"].String().empty()) - { - auto raw = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", json["rewardSpell"].String()); - if(raw) - { - bonusType = SPELL; - bonusID = raw.value(); - } - } - } -} - void CGGarrison::onHeroVisit (const CGHeroInstance *h) const { auto relations = cb->gameState()->getPlayerRelations(h->tempOwner, tempOwner); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 8743b7354..53021a4a9 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -57,26 +57,6 @@ protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; }; -class DLL_LINKAGE CGScholar : public CGObjectInstance -{ -public: - enum EBonusType {PRIM_SKILL, SECONDARY_SKILL, SPELL, RANDOM = 255}; - EBonusType bonusType; - ui16 bonusID; //ID of skill/spell - - CGScholar() : bonusType(EBonusType::RANDOM),bonusID(0){}; - void onHeroVisit(const CGHeroInstance * h) const override; - void initObj(CRandomGenerator & rand) override; - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & bonusType; - h & bonusID; - } -protected: - void serializeJsonOptions(JsonSerializeFormat & handler) override; -}; - class DLL_LINKAGE CGGarrison : public CArmedInstance { public: diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index bd361bb77..cc50fae43 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1179,11 +1179,21 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share return object; } -CGObjectInstance * CMapLoaderH3M::readScholar() +CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared_ptr objectTemplate) { - auto * object = new CGScholar(); - object->bonusType = static_cast(reader->readUInt8()); - object->bonusID = reader->readUInt8(); + enum class ScholarBonusType : uint8_t { + PRIM_SKILL = 0, + SECONDARY_SKILL = 1, + SPELL = 2, + RANDOM = 255 + }; + + auto * object = readGeneric(position, objectTemplate); + //auto * rewardable = dynamic_cast(object); + + /*uint8_t bonusTypeRaw =*/ reader->readUInt8(); + /*auto bonusType = static_cast(bonusTypeRaw);*/ + /*auto bonusID =*/ reader->readUInt8(); reader->skipZero(6); return object; } @@ -1491,7 +1501,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objTempl); CGObjectInstance * readSign(const int3 & position); CGObjectInstance * readWitchHut(const int3 & position, std::shared_ptr objectTemplate); - CGObjectInstance * readScholar(); + CGObjectInstance * readScholar(const int3 & position, std::shared_ptr objectTemplate); CGObjectInstance * readGarrison(const int3 & mapPosition); CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 0c543d3f5..245422ae6 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -54,7 +54,6 @@ void registerTypesMapObjects1(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); @@ -131,7 +130,6 @@ void registerTypesMapObjectTypes(Serializer &s) REGISTER_GENERIC_HANDLER(CGPandoraBox); REGISTER_GENERIC_HANDLER(CGQuestGuard); REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGScholar); REGISTER_GENERIC_HANDLER(CGSeerHut); REGISTER_GENERIC_HANDLER(CGShipyard); REGISTER_GENERIC_HANDLER(CGSignBottle); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index c89f617fc..5a4e0e52b 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -115,7 +115,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.resources = JsonRandom::loadResources(source["resources"], rng, variables); - limiter.primary = JsonRandom::loadPrimary(source["primary"], rng, variables); + limiter.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); limiter.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); limiter.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); limiter.spells = JsonRandom::loadSpells(source["spells"], rng, variables); @@ -150,7 +150,7 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand reward.removeObject = source["removeObject"].Bool(); reward.bonuses = JsonRandom::loadBonuses(source["bonuses"]); - reward.primary = JsonRandom::loadPrimary(source["primary"], rng, variables); + reward.primary = JsonRandom::loadPrimaries(source["primary"], rng, variables); reward.secondary = JsonRandom::loadSecondaries(source["secondary"], rng, variables); reward.artifacts = JsonRandom::loadArtifacts(source["artifacts"], rng, variables); @@ -221,9 +221,8 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR // if (category.first == "resource") // value = JsonRandom::loadResource(input, rng, object.variables.values).getNum(); - // TODO - // if (category.first == "primarySkill") - // value = JsonRandom::loadCreature(input, rng, object.variables.values).getNum(); + if (category.first == "primarySkill") + value = static_cast(JsonRandom::loadPrimary(input, rng, object.variables.values)); if (category.first == "secondarySkill") value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); From 0ea44520fd26de9dfd63053cabc94f9c498405c8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 15 Oct 2023 21:59:16 +0300 Subject: [PATCH 0846/1248] Fix build --- config/objects/rewardableScholar.json | 2 +- lib/JsonRandom.cpp | 26 ++++++++++++++------------ lib/JsonRandom.h | 2 +- lib/rewardable/Info.cpp | 2 +- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/config/objects/rewardableScholar.json b/config/objects/rewardableScholar.json index ebb73d090..f7c45a874 100644 --- a/config/objects/rewardableScholar.json +++ b/config/objects/rewardableScholar.json @@ -47,7 +47,7 @@ }, "spells" : [ "@gainedSpell" - ] + ], "removeObject" : true }, { diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 4e5434fee..f8043c7c2 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -95,6 +95,12 @@ namespace JsonRandom return loadVariable(IdentifierType::entityType(), value.String(), variables, IdentifierType::NONE); } + template<> + PlayerColor decodeKey(const JsonNode & value, const Variables & variables) + { + return PlayerColor(*VLC->identifiers()->getIdentifier("playerColor", value)); + } + template<> PrimarySkill decodeKey(const JsonNode & value, const Variables & variables) { @@ -414,23 +420,19 @@ namespace JsonRandom return ret; } - std::vector loadColors(const JsonNode & value, CRandomGenerator & rng) + std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { std::vector ret; - std::set def; - - for(auto & color : GameConstants::PLAYER_COLOR_NAMES) - def.insert(color); - + std::set defaultPlayers; + for(size_t i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + defaultPlayers.insert(PlayerColor(i)); + for(auto & entry : value.Vector()) { - auto key = loadKey(entry, rng, def); - auto pos = vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, key); - if(pos < 0) - logMod->warn("Unable to determine player color %s", key); - else - ret.emplace_back(pos); + std::set potentialPicks = filterKeys(entry, defaultPlayers, variables); + ret.push_back(*RandomGeneratorUtil::nextItem(potentialPicks, rng)); } + return ret; } diff --git a/lib/JsonRandom.h b/lib/JsonRandom.h index 0a6e34079..804c87e3e 100644 --- a/lib/JsonRandom.h +++ b/lib/JsonRandom.h @@ -52,7 +52,7 @@ namespace JsonRandom DLL_LINKAGE std::vector loadCreatures(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE std::vector evaluateCreatures(const JsonNode & value, const Variables & variables); - DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng); + DLL_LINKAGE std::vector loadColors(const JsonNode & value, CRandomGenerator & rng, const Variables & variables); DLL_LINKAGE std::vector loadHeroes(const JsonNode & value, CRandomGenerator & rng); DLL_LINKAGE std::vector loadHeroClasses(const JsonNode & value, CRandomGenerator & rng); diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 5a4e0e52b..6fcbd96b9 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -122,7 +122,7 @@ void Rewardable::Info::configureLimiter(Rewardable::Configuration & object, CRan limiter.canLearnSpells = JsonRandom::loadSpells(source["canLearnSpells"], rng, variables); limiter.creatures = JsonRandom::loadCreatures(source["creatures"], rng, variables); - limiter.players = JsonRandom::loadColors(source["colors"], rng); + limiter.players = JsonRandom::loadColors(source["colors"], rng, variables); limiter.heroes = JsonRandom::loadHeroes(source["heroes"], rng); limiter.heroClasses = JsonRandom::loadHeroClasses(source["heroClasses"], rng); From aeb559efb89065f3a37b3ee46e0d3a6d8ad9e0f7 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:04:29 +0200 Subject: [PATCH 0847/1248] code review --- client/widgets/RadialMenu.cpp | 14 +++++++------- client/widgets/RadialMenu.h | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/widgets/RadialMenu.cpp b/client/widgets/RadialMenu.cpp index 3ceef75e3..ea8743b91 100644 --- a/client/widgets/RadialMenu.cpp +++ b/client/widgets/RadialMenu.cpp @@ -21,14 +21,14 @@ #include "../../lib/CGeneralTextHandler.h" -RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool altLayout) +RadialMenuItem::RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool alternativeLayout) : callback(callback) , hoverText(hoverText) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - inactiveImage = std::make_shared(ImagePath::builtin(altLayout ? "radialMenu/itemInactiveAlt" : "radialMenu/itemInactive"), Point(0, 0)); - selectedImage = std::make_shared(ImagePath::builtin(altLayout ? "radialMenu/itemEmptyAlt" : "radialMenu/itemEmpty"), Point(0, 0)); + inactiveImage = std::make_shared(ImagePath::builtin(alternativeLayout ? "radialMenu/itemInactiveAlt" : "radialMenu/itemInactive"), Point(0, 0)); + selectedImage = std::make_shared(ImagePath::builtin(alternativeLayout ? "radialMenu/itemEmptyAlt" : "radialMenu/itemEmpty"), Point(0, 0)); iconImage = std::make_shared(ImagePath::builtin("radialMenu/" + imageName), Point(0, 0)); @@ -42,13 +42,13 @@ void RadialMenuItem::setSelected(bool selected) inactiveImage->setEnabled(!selected); } -RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool altLayout): - centerPosition(positionToCenter), altLayout(altLayout) +RadialMenu::RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool alternativeLayout): + centerPosition(positionToCenter), alternativeLayout(alternativeLayout) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos += positionToCenter; - Point itemSize = altLayout ? Point(80, 70) : Point(70, 80); + Point itemSize = alternativeLayout ? Point(80, 70) : Point(70, 80); moveBy(-itemSize / 2); pos.w = itemSize.x; pos.h = itemSize.y; @@ -72,7 +72,7 @@ void RadialMenu::addItem(const Point & offset, bool enabled, const std::string & if (!enabled) return; - auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback, altLayout); + auto item = std::make_shared(path, CGI->generaltexth->translate(hoverText), callback, alternativeLayout); item->moveBy(offset); diff --git a/client/widgets/RadialMenu.h b/client/widgets/RadialMenu.h index 6129d84c3..1b8462fd9 100644 --- a/client/widgets/RadialMenu.h +++ b/client/widgets/RadialMenu.h @@ -51,7 +51,7 @@ class RadialMenuItem : public CIntObject std::string hoverText; public: - RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool altLayout); + RadialMenuItem(const std::string & imageName, const std::string & hoverText, const std::function & callback, bool alternativeLayout); void setSelected(bool selected); }; @@ -68,9 +68,9 @@ class RadialMenu : public CIntObject std::shared_ptr findNearestItem(const Point & cursorPosition) const; - bool altLayout; + bool alternativeLayout; public: - RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool altLayout = false); + RadialMenu(const Point & positionToCenter, const std::vector & menuConfig, bool alternativeLayout = false); void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; From f6843c05748bf1abc8ab4151124b0a016fc0c69b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:39:53 +0200 Subject: [PATCH 0848/1248] fix focus problem --- client/widgets/TextControls.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index e86410638..7785fca2a 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -809,9 +809,12 @@ void CFocusable::moveFocus() void CFocusable::removeFocus() { - focus = false; - focusListener->focusLost(); - redraw(); + if(this == inputWithFocus) + { + focus = false; + focusListener->focusLost(); + redraw(); - inputWithFocus = nullptr; + inputWithFocus = nullptr; + } } From f3262a47c02334ffcd8f78ce54dac22aa85ad149 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:42:01 +0200 Subject: [PATCH 0849/1248] Update client/adventureMap/CList.cpp Co-authored-by: Nordsoft91 --- client/adventureMap/CList.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 3216b6c1b..5beb69402 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -374,10 +374,10 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const if(!on) return; - if(!LOCPLINT->localState->getOwnedTowns()[townPos]) - return; - const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + + if(townPos < 0 || townPos > towns.size() - 1 || !towns[townPos]) + return; if(towns.size() < 2) return; From d5b16ac96cc2a06812d94727dd5cad1bfa71618e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 01:43:50 +0200 Subject: [PATCH 0850/1248] rename --- client/adventureMap/CList.cpp | 22 +++++++++++----------- client/adventureMap/CList.h | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 5beb69402..c451b9cf1 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -329,7 +329,7 @@ CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town): parentList(parent) { const std::vector towns = LOCPLINT->localState->getOwnedTowns(); - townPos = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town)); + townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town)); OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); picture = std::make_shared(AnimationPath::builtin("ITPA"), 0); @@ -346,7 +346,7 @@ std::shared_ptr CTownList::CTownItem::genSelection() void CTownList::CTownItem::update() { - const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townPos]; + const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townIndex]; size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; picture->setFrame(iconIndex + 2); @@ -356,17 +356,17 @@ void CTownList::CTownItem::update() void CTownList::CTownItem::select(bool on) { if(on) - LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townPos]); + LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townIndex]); } void CTownList::CTownItem::open() { - LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townPos]); + LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townIndex]); } void CTownList::CTownItem::showTooltip() { - CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townPos], GH.getCursorPosition()); + CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townIndex], GH.getCursorPosition()); } void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) @@ -376,18 +376,18 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const const std::vector towns = LOCPLINT->localState->getOwnedTowns(); - if(townPos < 0 || townPos > towns.size() - 1 || !towns[townPos]) + if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex]) return; if(towns.size() < 2) return; - int townUpperPos = (townPos < 1) ? -1 : townPos - 1; - int townLowerPos = (townPos > towns.size() - 2) ? -1 : townPos + 1; + int townUpperPos = (townIndex < 1) ? -1 : townIndex - 1; + int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townPos, townUpperPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townPos, townLowerPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, }; GH.windows().createAndPushWindow(pos.center(), menuElements, true); @@ -395,7 +395,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const std::string CTownList::CTownItem::getHoverText() { - return LOCPLINT->localState->getOwnedTowns()[townPos]->getObjectName(); + return LOCPLINT->localState->getOwnedTowns()[townIndex]->getObjectName(); } CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount) diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index 6433d8c47..d8ee860d9 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -152,7 +152,7 @@ class CTownList : public CList std::shared_ptr picture; CTownList *parentList; public: - int townPos; + int townIndex; CTownItem(CTownList *parent, const CGTownInstance * town); From 850d0ff8ebb5b593a9614d75cd50728c13885f1a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 16 Oct 2023 17:55:31 +0300 Subject: [PATCH 0851/1248] Show (mostly) correct messages in Shrines --- config/objects/rewardableShrine.json | 33 +++++++++++++-------- config/objects/rewardableWitchHut.json | 1 + lib/JsonNode.cpp | 3 -- lib/mapObjects/CRewardableObject.cpp | 40 +++++++++++++++++--------- lib/mapObjects/CRewardableObject.h | 9 +++--- lib/rewardable/Configuration.h | 18 ++++++++++++ lib/rewardable/Info.cpp | 32 +++++++++++++++++++-- 7 files changed, 100 insertions(+), 36 deletions(-) diff --git a/config/objects/rewardableShrine.json b/config/objects/rewardableShrine.json index 3f7e3b36f..1cec9655a 100644 --- a/config/objects/rewardableShrine.json +++ b/config/objects/rewardableShrine.json @@ -19,6 +19,8 @@ "compatibilityIdentifiers" : [ "object" ], "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "(Learn 1st level spell)", "variables" : { "spell" : { @@ -42,10 +44,11 @@ "spells" : [ "@gainedSpell" ], - "message" : 127 // You learn new spell + "description" : 355, + "message" : [ 127, "%s." ] // You learn new spell } ], - "onVisitedMessage" : 174, // You already known this spell + "onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -55,10 +58,10 @@ } ] }, - "message" : 130 // No Wisdom + "message" : [ 127, "%s.", 130 ] // No Wisdom }, { - "message" : 131 // No spellbook + "message" : [ 127, "%s.", 131 ] // No spellbook } ] } @@ -84,6 +87,8 @@ "compatibilityIdentifiers" : [ "object" ], "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "(Learn 2nd level spell)", "variables" : { "spell" : { @@ -107,10 +112,11 @@ "spells" : [ "@gainedSpell" ], - "message" : 128 // You learn new spell + "description" : 355, + "message" : [ 128, "%s." ] // You learn new spell } ], - "onVisitedMessage" : 174, // You already known this spell + "onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -120,10 +126,10 @@ } ] }, - "message" : 130 // No Wisdom + "message" : [ 128, "%s.", 130 ] // No Wisdom }, { - "message" : 131 // No spellbook + "message" : [ 128, "%s.", 131 ] // No spellbook } ] } @@ -149,6 +155,8 @@ "compatibilityIdentifiers" : [ "object" ], "visitMode" : "limiter", + "visitedTooltip" : 354, + "description" : "(Learn 3rd level spell)", "variables" : { "spell" : { @@ -172,10 +180,11 @@ "spells" : [ "@gainedSpell" ], - "message" : 129 // You learn new spell + "description" : 355, + "message" : [ 129, "%s." ] // You learn new spell } ], - "onVisitedMessage" : 174, // You already known this spell + "onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell "onEmpty" : [ { "limiter" : { @@ -185,10 +194,10 @@ } ] }, - "message" : 130 // No Wisdom + "message" : [ 129, "%s.", 130 ] // No Wisdom }, { - "message" : 131 // No spellbook + "message" : [ 129, "%s.", 131 ] // No spellbook } ] } diff --git a/config/objects/rewardableWitchHut.json b/config/objects/rewardableWitchHut.json index d63d5a883..dc0b7c9a8 100644 --- a/config/objects/rewardableWitchHut.json +++ b/config/objects/rewardableWitchHut.json @@ -19,6 +19,7 @@ "compatibilityIdentifiers" : [ "object" ], "visitMode" : "limiter", + "visitedTooltip" : 354, "variables" : { "secondarySkill" : { diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 5b6b27957..afd091984 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -822,10 +822,7 @@ std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) { // caller code can not handle this case and presumes that returned bonus is always valid logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); - b->type = BonusType::NONE; - assert(0); // or throw? Game *should* work with dummy bonus - return b; } return b; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 4f31f362a..3d75e6097 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -21,13 +21,6 @@ VCMI_LIB_NAMESPACE_BEGIN -// FIXME: copy-pasted from CObjectHandler -static std::string visitedTxt(const bool visited) -{ - int id = visited ? 352 : 353; - return VLC->generaltexth->allTexts[id]; -} - void CRewardableObject::grantRewardWithMessage(const CGHeroInstance * contextHero, int index, bool markAsVisit) const { auto vi = configuration.info.at(index); @@ -124,6 +117,12 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const { logGlobal->debug("Revisiting already visited object"); + if (!wasVisited(h->getOwner())) + { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + } + auto visitedRewards = getAvailableRewards(h, Rewardable::EEventType::EVENT_ALREADY_VISITED); if (!visitedRewards.empty()) grantRewardWithMessage(h, visitedRewards[0], false); @@ -212,6 +211,11 @@ bool CRewardableObject::wasVisited(PlayerColor player) const } } +bool CRewardableObject::wasScouted(PlayerColor player) const +{ + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); +} + bool CRewardableObject::wasVisited(const CGHeroInstance * h) const { switch (configuration.visitMode) @@ -221,24 +225,34 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: - return configuration.visitLimiter.heroAllowed(h); + return wasScouted(h->getOwner()) && configuration.visitLimiter.heroAllowed(h); default: - return wasVisited(h->tempOwner); + return wasVisited(h->getOwner()); } } std::string CRewardableObject::getHoverText(PlayerColor player) const { if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) - return getObjectName() + " " + visitedTxt(wasVisited(player)); - return getObjectName(); + { + if (wasVisited(player)) + return getObjectName() + "\n" + configuration.visitedTooltip.toString() + "\n\n" + configuration.description.toString(); + else + return getObjectName() + "\n" + configuration.notVisitedTooltip.toString() + "\n\n" + configuration.description.toString(); + } + return getObjectName() + "\n\n" + configuration.description.toString(); } std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) - return getObjectName() + " " + visitedTxt(wasVisited(hero)); - return getObjectName(); + { + if (wasVisited(hero)) + return getObjectName() + "\n" + configuration.visitedTooltip.toString() + "\n\n" + configuration.description.toString(); + else + return getObjectName() + "\n" + configuration.notVisitedTooltip.toString() + "\n\n" + configuration.description.toString(); + } + return getObjectName() + "\n\n" + configuration.description.toString(); } void CRewardableObject::setPropertyDer(ui8 what, ui32 val) diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index fe41428c1..f6199bdda 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CRewardableObject : public CArmedInstance, public Rewardable::Interface { protected: - + bool onceVisitableObjectCleared = false; /// reward selected by player, no serialize @@ -41,6 +41,9 @@ public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; bool wasVisited(const CGHeroInstance * h) const override; + + /// Returns true if object was scouted by player and he is aware of its internal state + bool wasScouted(PlayerColor player) const; /// gives reward to player or ask for choice in case of multiple rewards void onHeroVisit(const CGHeroInstance *h) const override; @@ -74,10 +77,6 @@ public: //TODO: // MAX -// class DLL_LINKAGE CGPandoraBox : public CArmedInstance -// class DLL_LINKAGE CGEvent : public CGPandoraBox //event objects -// class DLL_LINKAGE CGSeerHut : public CArmedInstance, public IQuestObject //army is used when giving reward -// class DLL_LINKAGE CGQuestGuard : public CGSeerHut // class DLL_LINKAGE CBank : public CArmedInstance // class DLL_LINKAGE CGPyramid : public CBank diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 8dbfd0424..3d80591ba 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -84,6 +84,10 @@ struct DLL_LINKAGE VisitInfo /// Message that will be displayed on granting of this reward, if not empty MetaString message; + /// Object description that will be shown on right-click, after object name + /// Used only after player have "scouted" object and knows internal state of an object + MetaString description; + /// Event to which this reward is assigned EEventType visitType; @@ -94,6 +98,7 @@ struct DLL_LINKAGE VisitInfo h & limiter; h & reward; h & message; + h & description; h & visitType; } }; @@ -121,6 +126,16 @@ struct DLL_LINKAGE Configuration /// Message that will be shown if player needs to select one of multiple rewards MetaString onSelect; + /// Object description that will be shown on right-click, after object name + /// Used only if player is not aware of object internal state, e.g. have never visited it + MetaString description; + + /// Text that will be shown if hero has not visited this object + MetaString notVisitedTooltip; + + /// Text that will be shown after hero has visited this object + MetaString visitedTooltip; + /// Rewards that can be applied by an object std::vector info; @@ -161,6 +176,9 @@ struct DLL_LINKAGE Configuration h & canRefuse; h & resetParameters; h & onSelect; + h & description; + h & notVisitedTooltip; + h & visitedTooltip; h & visitMode; h & selectMode; h & infoWindowType; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 6fcbd96b9..979c89405 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -24,12 +24,25 @@ VCMI_LIB_NAMESPACE_BEGIN namespace { - MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier ) + MetaString loadMessage(const JsonNode & value, const TextIdentifier & textIdentifier, EMetaText textSource = EMetaText::ADVOB_TXT ) { MetaString ret; + + if (value.isVector()) + { + for(const auto & entry : value.Vector()) + { + if (entry.isNumber()) + ret.appendLocalString(textSource, static_cast(entry.Float())); + if (entry.isString()) + ret.appendRawString(entry.String()); + } + return ret; + } + if (value.isNumber()) { - ret.appendLocalString(EMetaText::ADVOB_TXT, static_cast(value.Float())); + ret.appendLocalString(textSource, static_cast(value.Float())); return ret; } @@ -81,6 +94,9 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o } loadString(parameters["onSelectMessage"], TextIdentifier(objectName, "onSelect")); + loadString(parameters["description"], TextIdentifier(objectName, "description")); + loadString(parameters["notVisitedTooltip"], TextIdentifier(objectName, "notVisitedText")); + loadString(parameters["visitedTooltip"], TextIdentifier(objectName, "visitedTooltip")); loadString(parameters["onVisitedMessage"], TextIdentifier(objectName, "onVisited")); loadString(parameters["onEmptyMessage"], TextIdentifier(objectName, "onEmpty")); } @@ -277,6 +293,7 @@ void Rewardable::Info::configureRewards( info.visitType = event; info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); + info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i)); for (const auto & artifact : info.reward.artifacts ) info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); @@ -298,7 +315,16 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand configureRewards(object, rng, parameters["onVisited"], Rewardable::EEventType::EVENT_ALREADY_VISITED, "onVisited"); configureRewards(object, rng, parameters["onEmpty"], Rewardable::EEventType::EVENT_NOT_AVAILABLE, "onEmpty"); - object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.onSelect = loadMessage(parameters["onSelectMessage"], TextIdentifier(objectTextID, "onSelect")); + object.description = loadMessage(parameters["description"], TextIdentifier(objectTextID, "description")); + object.notVisitedTooltip = loadMessage(parameters["notVisitedTooltip"], TextIdentifier(objectTextID, "notVisitedTooltip"), EMetaText::GENERAL_TXT); + object.visitedTooltip = loadMessage(parameters["visitedTooltip"], TextIdentifier(objectTextID, "visitedTooltip"), EMetaText::GENERAL_TXT); + + if (object.notVisitedTooltip.empty()) + object.notVisitedTooltip.appendTextID("core.genrltxt.353"); + + if (object.visitedTooltip.empty()) + object.visitedTooltip.appendTextID("core.genrltxt.352"); if (!parameters["onVisitedMessage"].isNull()) { From 0a1578b797382de1e35e015275c4bf5b48adb82e Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:24:59 +0200 Subject: [PATCH 0852/1248] Apply suggestions from code review Co-authored-by: Nordsoft91 --- client/CServerHandler.h | 4 ++-- client/lobby/OptionsTab.cpp | 2 +- client/lobby/OptionsTab.h | 2 +- server/CVCMIServer.cpp | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/CServerHandler.h b/client/CServerHandler.h index 207605a78..62a7e3b4c 100644 --- a/client/CServerHandler.h +++ b/client/CServerHandler.h @@ -67,7 +67,7 @@ public: virtual void setCampaignBonus(int bonusId) const = 0; virtual void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const = 0; virtual void setPlayer(PlayerColor color) const = 0; - virtual void setPlayerName(PlayerColor color, std::string name) const = 0; + virtual void setPlayerName(PlayerColor color, const std::string & name) const = 0; virtual void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const = 0; virtual void setDifficulty(int to) const = 0; virtual void setTurnTimerInfo(const TurnTimerInfo &) const = 0; @@ -153,7 +153,7 @@ public: void setCampaignBonus(int bonusId) const override; void setMapInfo(std::shared_ptr to, std::shared_ptr mapGenOpts = {}) const override; void setPlayer(PlayerColor color) const override; - void setPlayerName(PlayerColor color, std::string name) const override; + void setPlayerName(PlayerColor color, const std::string & name) const override; void setPlayerOption(ui8 what, int32_t value, PlayerColor player) const override; void setDifficulty(int to) const override; void setTurnTimerInfo(const TurnTimerInfo &) const override; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index d41a8d716..06a65e1bc 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1145,7 +1145,7 @@ void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) changeName(); } -void OptionsTab::PlayerOptionsEntry::changeName() { +void OptionsTab::PlayerOptionsEntry::updateName() { if(labelPlayerNameEdit->getText() != name) { CSH->setPlayerName(s->color, labelPlayerNameEdit->getText()); diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 0762f29cf..23eff9c96 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -196,6 +196,6 @@ private: private: const OptionsTab & parentTab; - void changeName(); + void updateName(); }; }; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 088e222b8..271ece218 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -845,7 +845,7 @@ void CVCMIServer::setPlayerName(PlayerColor color, std::string name) if(!player.isControlledByHuman()) return; - if(!player.connectedPlayerIDs.size()) + if(player.connectedPlayerIDs.empty()) return; int nameID = *(player.connectedPlayerIDs.begin()); //if not AI - set appropiate ID From ecd093cae45aa8140cf3a8d37f1f46e644e36159 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:28:44 +0200 Subject: [PATCH 0853/1248] fix --- client/lobby/OptionsTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 06a65e1bc..042410270 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1131,7 +1131,7 @@ bool OptionsTab::PlayerOptionsEntry::captureThisKey(EShortcut key) void OptionsTab::PlayerOptionsEntry::keyPressed(EShortcut key) { if(labelPlayerNameEdit && key == EShortcut::GLOBAL_ACCEPT) - changeName(); + updateName(); } bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int eventType) const @@ -1142,7 +1142,7 @@ bool OptionsTab::PlayerOptionsEntry::receiveEvent(const Point & position, int ev void OptionsTab::PlayerOptionsEntry::clickReleased(const Point & cursorPosition) { if(labelPlayerNameEdit && !labelPlayerNameEdit->pos.isInside(cursorPosition)) - changeName(); + updateName(); } void OptionsTab::PlayerOptionsEntry::updateName() { From be6ed364226cf3232685407c43df93b00dfafe59 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Mon, 16 Oct 2023 21:35:29 +0200 Subject: [PATCH 0854/1248] fix --- client/CServerHandler.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index a47a089fa..1212e394d 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -499,7 +499,7 @@ void CServerHandler::setPlayer(PlayerColor color) const sendLobbyPack(lsp); } -void CServerHandler::setPlayerName(PlayerColor color, std::string name) const +void CServerHandler::setPlayerName(PlayerColor color, const std::string & name) const { LobbySetPlayerName lspn; lspn.color = color; From f6d7755c6ab4045dc44e08363c54b27e13afe8cf Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 16 Oct 2023 22:24:12 +0200 Subject: [PATCH 0855/1248] Add hero placeholder properties --- lib/mapObjects/CGHeroInstance.cpp | 19 +++++++ lib/mapObjects/CGHeroInstance.h | 5 +- mapeditor/inspector/inspector.cpp | 78 +++++++++++++++++++++++++- mapeditor/inspector/inspector.h | 2 + mapeditor/inspector/portraitwidget.cpp | 1 + mapeditor/mainwindow.cpp | 4 +- 6 files changed, 105 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8941b9273..aaa2a180f 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -40,6 +40,25 @@ VCMI_LIB_NAMESPACE_BEGIN +void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) +{ + bool isHeroType = heroType.has_value(); + handler.serializeBool("placeholderType", isHeroType, false); + + if(!handler.saving) + { + if(isHeroType) + heroType = HeroTypeID::NONE; + else + powerRank = 0; + } + + if(isHeroType) + handler.serializeId("heroType", heroType.value(), HeroTypeID::NONE); + else + handler.serializeInt("powerRank", powerRank.value()); +} + static int lowestSpeed(const CGHeroInstance * chi) { static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 08335c71e..ab19d02f9 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -25,7 +25,7 @@ struct TerrainTile; struct TurnInfo; enum class EHeroGender : uint8_t; -class CGHeroPlaceholder : public CGObjectInstance +class DLL_LINKAGE CGHeroPlaceholder : public CGObjectInstance { public: /// if this is placeholder by power, then power rank of desired hero @@ -40,6 +40,9 @@ public: h & powerRank; h & heroType; } + +protected: + void serializeJsonOptions(JsonSerializeFormat & handler) override; }; diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 9f188860a..46740e09f 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -53,6 +53,7 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default INIT_OBJ_TYPE(CGDwelling); INIT_OBJ_TYPE(CGTownInstance); INIT_OBJ_TYPE(CGCreature); + INIT_OBJ_TYPE(CGHeroPlaceholder); INIT_OBJ_TYPE(CGHeroInstance); INIT_OBJ_TYPE(CGSignBottle); INIT_OBJ_TYPE(CGLighthouse); @@ -118,6 +119,17 @@ void Initializer::initialize(CGLighthouse * o) o->tempOwner = defaultPlayer; } +void Initializer::initialize(CGHeroPlaceholder * o) +{ + if(!o) return; + + if(!o->powerRank.has_value() && !o->heroType.has_value()) + o->powerRank = 0; + + if(o->powerRank.has_value() && o->heroType.has_value()) + o->powerRank.reset(); +} + void Initializer::initialize(CGHeroInstance * o) { if(!o) @@ -258,6 +270,34 @@ void Inspector::updateProperties(CGShipyard * o) addProperty("Owner", o->tempOwner, false); } +void Inspector::updateProperties(CGHeroPlaceholder * o) +{ + if(!o) return; + + bool type = false; + if(o->heroType.has_value()) + type = true; + else if(!o->powerRank.has_value()) + assert(0); //one of values must be initialized + + { + auto * delegate = new InspectorDelegate; + delegate->options = {{"POWER RANK", QVariant::fromValue(false)}, {"HERO TYPE", QVariant::fromValue(true)}}; + addProperty("Placeholder type", delegate->options[type].first, delegate, false); + } + + addProperty("Power rank", o->powerRank.has_value() ? o->powerRank.value() : 0, type); + + { + auto * delegate = new InspectorDelegate; + for(int i = 0; i < VLC->heroh->objects.size(); ++i) + { + delegate->options.push_back({QObject::tr(VLC->heroh->objects[i]->getNameTranslated().c_str()), QVariant::fromValue(VLC->heroh->objects[i]->getId().getNum())}); + } + addProperty("Hero type", o->heroType.has_value() ? VLC->heroh->getById(o->heroType.value())->getNameTranslated() : "", delegate, !type); + } +} + void Inspector::updateProperties(CGHeroInstance * o) { if(!o) return; @@ -454,6 +494,7 @@ void Inspector::updateProperties() UPDATE_OBJ_PROPERTIES(CGDwelling); UPDATE_OBJ_PROPERTIES(CGTownInstance); UPDATE_OBJ_PROPERTIES(CGCreature); + UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder); UPDATE_OBJ_PROPERTIES(CGHeroInstance); UPDATE_OBJ_PROPERTIES(CGSignBottle); UPDATE_OBJ_PROPERTIES(CGLighthouse); @@ -500,6 +541,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value) SET_PROPERTIES(CGDwelling); SET_PROPERTIES(CGGarrison); SET_PROPERTIES(CGCreature); + SET_PROPERTIES(CGHeroPlaceholder); SET_PROPERTIES(CGHeroInstance); SET_PROPERTIES(CGShipyard); SET_PROPERTIES(CGSignBottle); @@ -612,6 +654,37 @@ void Inspector::setProperty(CGGarrison * o, const QString & key, const QVariant o->removableUnits = value.toBool(); } +void Inspector::setProperty(CGHeroPlaceholder * o, const QString & key, const QVariant & value) +{ + if(!o) return; + + if(key == "Placeholder type") + { + if(value.toBool()) + { + if(!o->heroType.has_value()) + o->heroType = HeroTypeID(0); + o->powerRank.reset(); + } + else + { + if(!o->powerRank.has_value()) + o->powerRank = 0; + o->heroType.reset(); + } + + updateProperties(); + } + + if(key == "Power rank") + o->powerRank = value.toInt(); + + if(key == "Hero type") + { + o->heroType.value() = HeroTypeID(value.toInt()); + } +} + void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVariant & value) { if(!o) return; @@ -634,8 +707,11 @@ void Inspector::setProperty(CGHeroInstance * o, const QString & key, const QVari { for(auto t : VLC->heroh->objects) { - if(t->getNameTranslated() == value.toString().toStdString()) + if(t->getId() == value.toInt()) + { + o->subID = value.toInt(); o->type = t.get(); + } } o->gender = o->type->gender; o->randomizeArmy(o->type->heroClass->faction); diff --git a/mapeditor/inspector/inspector.h b/mapeditor/inspector/inspector.h index e4a85715f..432e40ab0 100644 --- a/mapeditor/inspector/inspector.h +++ b/mapeditor/inspector/inspector.h @@ -45,6 +45,7 @@ public: DECLARE_OBJ_TYPE(CGResource); DECLARE_OBJ_TYPE(CGDwelling); DECLARE_OBJ_TYPE(CGGarrison); + DECLARE_OBJ_TYPE(CGHeroPlaceholder); DECLARE_OBJ_TYPE(CGHeroInstance); DECLARE_OBJ_TYPE(CGCreature); DECLARE_OBJ_TYPE(CGSignBottle); @@ -74,6 +75,7 @@ protected: DECLARE_OBJ_PROPERTY_METHODS(CGResource); DECLARE_OBJ_PROPERTY_METHODS(CGDwelling); DECLARE_OBJ_PROPERTY_METHODS(CGGarrison); + DECLARE_OBJ_PROPERTY_METHODS(CGHeroPlaceholder); DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance); DECLARE_OBJ_PROPERTY_METHODS(CGCreature); DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle); diff --git a/mapeditor/inspector/portraitwidget.cpp b/mapeditor/inspector/portraitwidget.cpp index 3882ddf62..669bb880c 100644 --- a/mapeditor/inspector/portraitwidget.cpp +++ b/mapeditor/inspector/portraitwidget.cpp @@ -22,6 +22,7 @@ PortraitWidget::PortraitWidget(CGHeroInstance & h, QWidget *parent): ui->setupUi(this); ui->portraitView->setScene(&scene); ui->portraitView->fitInView(scene.itemsBoundingRect(), Qt::KeepAspectRatio); + show(); } PortraitWidget::~PortraitWidget() diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index fae6dc512..9971f113c 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -222,8 +222,6 @@ MainWindow::MainWindow(QWidget* parent) : scenePreview = new QGraphicsScene(this); ui->objectPreview->setScene(scenePreview); - initialScale = ui->mapView->viewport()->geometry(); - //loading objects loadObjectsTree(); @@ -298,6 +296,8 @@ void MainWindow::initializeMap(bool isNew) ui->mapView->setScene(controller.scene(mapLevel)); ui->minimapView->setScene(controller.miniScene(mapLevel)); ui->minimapView->dimensions(); + if(initialScale.isValid()) + on_actionZoom_reset_triggered(); initialScale = ui->mapView->mapToScene(ui->mapView->viewport()->geometry()).boundingRect(); //enable settings From 927ce4e60e625d9eee813559082a9267198ef8eb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 16 Oct 2023 23:55:37 +0300 Subject: [PATCH 0856/1248] Improvements to rewardable objects popups --- client/windows/InfoWindows.cpp | 14 +++++- lib/mapObjects/CGObjectInstance.cpp | 10 +++++ lib/mapObjects/CGObjectInstance.h | 3 ++ lib/mapObjects/CRewardableObject.cpp | 67 ++++++++++++++++++++++++---- lib/mapObjects/CRewardableObject.h | 5 +++ lib/rewardable/Reward.cpp | 10 ++--- lib/rewardable/Reward.h | 4 +- 7 files changed, 94 insertions(+), 19 deletions(-) diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index f015c779b..8a298cdc8 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -351,10 +351,20 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, } else { + std::vector components; if(LOCPLINT->localState->getCurrentHero()) - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero())); + components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); else - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID)); + components = obj->getPopupComponents(LOCPLINT->playerID); + + std::vector> guiComponents; + for (auto & component : components) + guiComponents.push_back(std::make_shared(component)); + + if(LOCPLINT->localState->getCurrentHero()) + CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero()), guiComponents); + else + CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID), guiComponents); } } diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 1b2f0f92a..e4ed862c9 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -271,6 +271,16 @@ std::string CGObjectInstance::getHoverText(const CGHeroInstance * hero) const return getHoverText(hero->tempOwner); } +std::vector CGObjectInstance::getPopupComponents(PlayerColor player) const +{ + return {}; +} + +std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance * hero) const +{ + return {}; +} + void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const { switch(ID) diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index fdbefa545..fb093bbd1 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -112,6 +112,9 @@ public: /// Returns hero-specific hover name, including visited/not visited info. Default = player-specific name virtual std::string getHoverText(const CGHeroInstance * hero) const; + virtual std::vector getPopupComponents(PlayerColor player) const; + virtual std::vector getPopupComponents(const CGHeroInstance * hero) const; + /** OVERRIDES OF IObjectInterface **/ void initObj(CRandomGenerator & rand) override; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 3d75e6097..f59a0ac68 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -47,15 +47,22 @@ void CRewardableObject::selectRewardWthMessage(const CGHeroInstance * contextHer BlockingDialog sd(configuration.canRefuse, rewardIndices.size() > 1); sd.player = contextHero->tempOwner; sd.text = dialog; + sd.components = loadComponents(contextHero, rewardIndices); + cb->showBlockingDialog(&sd); +} + +std::vector CRewardableObject::loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const +{ + std::vector result; if (rewardIndices.size() > 1) for (auto index : rewardIndices) - sd.components.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); + result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); if (rewardIndices.size() == 1) - configuration.info.at(rewardIndices.front()).reward.loadComponents(sd.components, contextHero); + configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); - cb->showBlockingDialog(&sd); + return result; } void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const @@ -233,26 +240,68 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const std::string CRewardableObject::getHoverText(PlayerColor player) const { + std::string result = getObjectName(); + + if (!configuration.description.empty()) + result += "\n" + configuration.description.toString(); + if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) { if (wasVisited(player)) - return getObjectName() + "\n" + configuration.visitedTooltip.toString() + "\n\n" + configuration.description.toString(); + result += "\n\n" + configuration.visitedTooltip.toString(); else - return getObjectName() + "\n" + configuration.notVisitedTooltip.toString() + "\n\n" + configuration.description.toString(); + result += "\n\n" + configuration.notVisitedTooltip.toString(); } - return getObjectName() + "\n\n" + configuration.description.toString(); + return result; } std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { + std::string result = getObjectName(); + + if (!configuration.description.empty()) + result += "\n" + configuration.description.toString(); + if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) { if (wasVisited(hero)) - return getObjectName() + "\n" + configuration.visitedTooltip.toString() + "\n\n" + configuration.description.toString(); + result += "\n\n" + configuration.visitedTooltip.toString(); else - return getObjectName() + "\n" + configuration.notVisitedTooltip.toString() + "\n\n" + configuration.description.toString(); + result += "\n\n" + configuration.notVisitedTooltip.toString(); } - return getObjectName() + "\n\n" + configuration.description.toString(); + return result; +} + +std::vector CRewardableObject::getPopupComponents(PlayerColor player) const +{ + if (!wasScouted(player)) + return {}; + + auto rewardIndices = getAvailableRewards(nullptr, Rewardable::EEventType::EVENT_FIRST_VISIT); + + if (rewardIndices.empty() && !configuration.info.empty()) + rewardIndices.push_back(0); + + if (rewardIndices.empty()) + return {}; + + return loadComponents(nullptr, rewardIndices); +} + +std::vector CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const +{ + if (!wasScouted(hero->getOwner())) + return {}; + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + + if (rewardIndices.empty() && !configuration.info.empty()) + rewardIndices.push_back(0); + + if (rewardIndices.empty()) + return {}; + + return loadComponents(nullptr, rewardIndices); } void CRewardableObject::setPropertyDer(ui8 what, ui32 val) diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index f6199bdda..725b869ef 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -37,6 +37,8 @@ protected: virtual void grantRewardWithMessage(const CGHeroInstance * contextHero, int rewardIndex, bool markAsVisit) const; virtual void selectRewardWthMessage(const CGHeroInstance * contextHero, const std::vector & rewardIndices, const MetaString & dialog) const; + std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; + public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; @@ -66,6 +68,9 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; + std::vector getPopupComponents(const CGHeroInstance * hero) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index dad53105a..71480aae1 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -61,8 +61,7 @@ Component Rewardable::Reward::getDisplayedComponent(const CGHeroInstance * h) co return comps.front(); } -void Rewardable::Reward::loadComponents(std::vector & comps, - const CGHeroInstance * h) const +void Rewardable::Reward::loadComponents(std::vector & comps, const CGHeroInstance * h) const { for (auto comp : extraComponents) comps.push_back(comp); @@ -76,14 +75,13 @@ void Rewardable::Reward::loadComponents(std::vector & comps, } if (heroExperience) - { - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); - } + comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h ? h->calculateXp(heroExperience) : heroExperience), 0); + if (heroLevel) comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); if (manaDiff || manaPercentage >= 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, calculateManaPoints(h) - h->mana, 0); + comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, h ? (calculateManaPoints(h) - h->mana) : manaDiff, 0); for (size_t i=0; i & comps, - const CGHeroInstance * h) const; + /// If hero is nullptr, then rewards will be generated without accounting for hero + void loadComponents(std::vector & comps, const CGHeroInstance * h) const; Component getDisplayedComponent(const CGHeroInstance * h) const; From eedaa63f5f6936b79f80908e3b1c604b3cd6bb75 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 17 Oct 2023 16:35:34 +0300 Subject: [PATCH 0857/1248] Hnadling of Shrine messages now matches H3 --- config/objects/rewardableShrine.json | 9 +++----- lib/mapObjects/CRewardableObject.cpp | 34 +++++++++++++++++++++++----- lib/mapObjects/CRewardableObject.h | 3 +++ lib/rewardable/Configuration.h | 10 ++++---- lib/rewardable/Info.cpp | 28 +++++++++++++++++++++-- 5 files changed, 66 insertions(+), 18 deletions(-) diff --git a/config/objects/rewardableShrine.json b/config/objects/rewardableShrine.json index 1cec9655a..5bfc9de26 100644 --- a/config/objects/rewardableShrine.json +++ b/config/objects/rewardableShrine.json @@ -20,7 +20,6 @@ "visitMode" : "limiter", "visitedTooltip" : 354, - "description" : "(Learn 1st level spell)", "variables" : { "spell" : { @@ -54,7 +53,7 @@ "limiter" : { "artifacts" : [ { - "type" : "spellbook" + "type" : "spellBook" } ] }, @@ -88,7 +87,6 @@ "visitMode" : "limiter", "visitedTooltip" : 354, - "description" : "(Learn 2nd level spell)", "variables" : { "spell" : { @@ -122,7 +120,7 @@ "limiter" : { "artifacts" : [ { - "type" : "spellbook" + "type" : "spellBook" } ] }, @@ -156,7 +154,6 @@ "visitMode" : "limiter", "visitedTooltip" : 354, - "description" : "(Learn 3rd level spell)", "variables" : { "spell" : { @@ -190,7 +187,7 @@ "limiter" : { "artifacts" : [ { - "type" : "spellbook" + "type" : "spellBook" } ] }, diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index f59a0ac68..2780e0d2d 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -242,8 +242,8 @@ std::string CRewardableObject::getHoverText(PlayerColor player) const { std::string result = getObjectName(); - if (!configuration.description.empty()) - result += "\n" + configuration.description.toString(); + if (!getDescriptionMessage(player).empty()) + result += "\n" + getDescriptionMessage(player); if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) { @@ -259,8 +259,8 @@ std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { std::string result = getObjectName(); - if (!configuration.description.empty()) - result += "\n" + configuration.description.toString(); + if (!getDescriptionMessage(hero).empty()) + result += "\n" + getDescriptionMessage(hero); if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) { @@ -272,13 +272,36 @@ std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const return result; } +std::string CRewardableObject::getDescriptionMessage(PlayerColor player) const +{ + if (!wasScouted(player) || configuration.info.empty()) + return configuration.description.toString(); + + auto rewardIndices = getAvailableRewards(nullptr, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty()) + return configuration.info[0].description.toString(); + + return configuration.info[rewardIndices.front()].description.toString(); +} + +std::string CRewardableObject::getDescriptionMessage(const CGHeroInstance * hero) const +{ + if (!wasScouted(hero->getOwner()) || configuration.info.empty()) + return configuration.description.toString(); + + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); + if (rewardIndices.empty()) + return configuration.info[0].description.toString(); + + return configuration.info[rewardIndices.front()].description.toString(); +} + std::vector CRewardableObject::getPopupComponents(PlayerColor player) const { if (!wasScouted(player)) return {}; auto rewardIndices = getAvailableRewards(nullptr, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info.empty()) rewardIndices.push_back(0); @@ -294,7 +317,6 @@ std::vector CRewardableObject::getPopupComponents(const CGHeroInstanc return {}; auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info.empty()) rewardIndices.push_back(0); diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 725b869ef..45134a125 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -68,6 +68,9 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getDescriptionMessage(PlayerColor player) const; + std::string getDescriptionMessage(const CGHeroInstance * hero) const; + std::vector getPopupComponents(PlayerColor player) const override; std::vector getPopupComponents(const CGHeroInstance * hero) const override; diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 3d80591ba..2fb901651 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -172,15 +172,17 @@ struct DLL_LINKAGE Configuration template void serialize(Handler &h, const int version) { - h & info; - h & canRefuse; - h & resetParameters; h & onSelect; h & description; h & notVisitedTooltip; h & visitedTooltip; - h & visitMode; + h & info; h & selectMode; + h & visitMode; + h & resetParameters; + h & variables; + h & visitLimiter; + h & canRefuse; h & infoWindowType; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 979c89405..bd1b73de2 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -293,13 +293,28 @@ void Rewardable::Info::configureRewards( info.visitType = event; info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); - info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i)); + info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i), EMetaText::GENERAL_TXT); for (const auto & artifact : info.reward.artifacts ) + { info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + info.description.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + } for (const auto & artifact : info.reward.spells ) + { info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + info.description.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + } + + for (const auto & variable : object.variables.values ) + { + if( boost::algorithm::starts_with(variable.first, "spell")) + { + info.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + info.description.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + } + } object.info.push_back(info); } @@ -331,6 +346,10 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onVisited; onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED; onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited")); + for (const auto & variable : object.variables.values ) + if( boost::algorithm::starts_with(variable.first, "spell")) + onVisited.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + object.info.push_back(onVisited); } @@ -339,6 +358,10 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onEmpty; onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE; onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty")); + for (const auto & variable : object.variables.values ) + if( boost::algorithm::starts_with(variable.first, "spell")) + onEmpty.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + object.info.push_back(onEmpty); } @@ -371,7 +394,8 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand } } - configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); + if (object.visitMode == Rewardable::VISIT_LIMITER) + configureLimiter(object, rng, object.visitLimiter, parameters["visitLimiter"]); } From 48eba6c36226a10840dd9576daeee1be2c6276b4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 17 Oct 2023 16:52:57 +0300 Subject: [PATCH 0858/1248] Fix Witch Hut messages to match H3 --- config/objects/rewardableWitchHut.json | 1 + lib/rewardable/Info.cpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/config/objects/rewardableWitchHut.json b/config/objects/rewardableWitchHut.json index dc0b7c9a8..e3db3b726 100644 --- a/config/objects/rewardableWitchHut.json +++ b/config/objects/rewardableWitchHut.json @@ -48,6 +48,7 @@ } ] }, + "description" : 355, "secondary" : { "@gainedSkill" : 1 }, diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index bd1b73de2..311948fcb 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -307,6 +307,12 @@ void Rewardable::Info::configureRewards( info.description.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); } + for (const auto & secondary : info.reward.secondary ) + { + info.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + info.description.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + } + for (const auto & variable : object.variables.values ) { if( boost::algorithm::starts_with(variable.first, "spell")) @@ -314,6 +320,11 @@ void Rewardable::Info::configureRewards( info.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); info.description.replaceLocalString(EMetaText::SPELL_NAME, variable.second); } + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + { + info.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + info.description.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + } } object.info.push_back(info); @@ -347,8 +358,12 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED; onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited")); for (const auto & variable : object.variables.values ) + { if( boost::algorithm::starts_with(variable.first, "spell")) onVisited.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + onVisited.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + } object.info.push_back(onVisited); } @@ -359,8 +374,12 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE; onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty")); for (const auto & variable : object.variables.values ) + { if( boost::algorithm::starts_with(variable.first, "spell")) onEmpty.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + onEmpty.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + } object.info.push_back(onEmpty); } From 464577be282f636d14fdb45e1a96fbb6dd17f5f2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:06:35 +0200 Subject: [PATCH 0859/1248] new icons --- Mods/vcmi/Data/radialMenu/dismissHero.png | Bin 0 -> 352 bytes Mods/vcmi/Data/radialMenu/getArtifacts.png | Bin 1055 -> 0 bytes Mods/vcmi/Data/radialMenu/moveArtifacts.png | Bin 0 -> 1827 bytes Mods/vcmi/Data/radialMenu/moveTroops.png | Bin 0 -> 518 bytes Mods/vcmi/Data/radialMenu/remove.png | Bin 1375 -> 0 bytes Mods/vcmi/Data/radialMenu/swapArtifacts.png | Bin 1096 -> 1823 bytes Mods/vcmi/Data/radialMenu/trade.png | Bin 1252 -> 0 bytes Mods/vcmi/Data/radialMenu/tradeHeroes.png | Bin 0 -> 378 bytes client/windows/CCastleInterface.cpp | 8 ++++---- 9 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 Mods/vcmi/Data/radialMenu/dismissHero.png delete mode 100644 Mods/vcmi/Data/radialMenu/getArtifacts.png create mode 100644 Mods/vcmi/Data/radialMenu/moveArtifacts.png create mode 100644 Mods/vcmi/Data/radialMenu/moveTroops.png delete mode 100644 Mods/vcmi/Data/radialMenu/remove.png delete mode 100644 Mods/vcmi/Data/radialMenu/trade.png create mode 100644 Mods/vcmi/Data/radialMenu/tradeHeroes.png diff --git a/Mods/vcmi/Data/radialMenu/dismissHero.png b/Mods/vcmi/Data/radialMenu/dismissHero.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb2b1115392e81094660ca70dbec53108fc6641 GIT binary patch literal 352 zcmV-m0iXVfP)6s5M z4Cm`49yAq;MVvXm8z`B;a;0Z|{yNOEaW29ZW;IJPhY(P)oIOFtm{%=y)d}p17LY_B z^|BCi-f5Xh0004lX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$i(@LdOI@m$PAw$*0E-K;tBaGOi^GTzbXb_5kdq&0OK+<>vNKnhUfUYhmWs!ah~OU?$0rx7EA{CMB)Ui8y4{f@yw=G z=e$oGWo200qd;&MXg6*9``EVICxHJMxYE1+S__!{B)!?y zVn;w^8@RacYRVpPxdRM6)n!9=BtK1|SOnhB=$rDu;4KhZb9?LTxDFAAdN% z7f%72#-O7+{rN>3#Mo z6GO6Xl=hq@RbxoXrrPS)Tk}>W2C>S57+_h&z}cs%M(DrAz)Yz#&>m7>!b~BQ2A~{D zDKS$Bg;1J-a?r~J6h$>#o6py(!=B6f)L~Ec`MR|4AaWwsa62(%|6R445w(0RkXAcfJUAAOO@1h$$2;az#7+y3on6N^kQ_DRM<8 z`?}ChO>MQzJUy8K?=3(T=w!g_6c8;@b`=iliH7H&rohW2l1wPip zhI`GFSPvT}F_lX!hYiKkSn*e|IQw{R=GdnYN_>3@Glfu`LSgg9Od%9P;W7b5Q53b4 ZJ^&H4x1ATBf!F{5002ovPDHLkV1f?z-(>&* diff --git a/Mods/vcmi/Data/radialMenu/moveArtifacts.png b/Mods/vcmi/Data/radialMenu/moveArtifacts.png new file mode 100644 index 0000000000000000000000000000000000000000..3f2a9b2f4747499abe528bae65aa3aa3c3dad598 GIT binary patch literal 1827 zcmV+;2i*9HP)h000K+NklYNdu`jcZQIt^wz0NtTX9+cx3Bfk-&Ng1ySp+b5)t{&j1krv z-p;BfE2lZO`9HymfKPH#?p!$DMn|KIor|qfaBQ$nS1S`aryEUsl@6@(I^8o`ziG~B z+y3p^wKk!zN{Re;iiJ;dQVy^6?q+Q&|H0l;@lXE*Bg`FR`!@GLa9-~I;HoVP+>T5i zZvAa^U$e(khgvONFy5}q@SdiMt7kYVO&ny=PJBwl!^cZXWy;_VRE8HR6QF=8rKftF zs7M}wvQs^AVw3ON^>bWqA6e&vk`qbD*&qDr;A-#2d1GzwZe8g1Aa!TJF)CddF$ob9 zA1^7jnPY;3y)-^+sdfI8zAG?l&S;xVS6gM2oa~0$tZ_)&8G!2a5$sl(JP;h9s??z< zIvR_*>J)Fk61YK#M|kl}?yA$P)K1 zoyNtARk%{K2{)Sd;BxhP_WDxQT3je!j_VD(aQ{lm!>e^$rNtseM11_Dl%MId!%#;W z4wj0~xYv7@dk*n;Q~O0v(Nr)C&+e7u#e-_RdfbHP_p9*WS~_mE9ArS63+He_Ki+KK z?;|J^JopH|p!mQ~O5g2&Gx2us3QvQbOWdcp*eF3xraj_Abn)|+_BGUbAQxpnSvJnjquV@C(Fo<#@*Zi1E~NGt?@>|;m8l9 z1U>?}Wzq0SL`vrFKp8SbPdL`c_A{@gK+o&5CxUbyWRRHjc{-|~GG!o}^g*8L$U6|i zraY$t#m5s-mo@$%?v4_I1JUqFL`w4Z0QGVG&7WwfbpEw)g58CLP~Fc1lZ>!`l@}WG zXMjo%Yi2rO@kDzz;Y&|;r$7m~TDJ{km~ecfFB)YNd>s8zofG4)i>VoC+!!A3eZNc7z2d)YNG6PFDlDGjO}lZ zmZJGA7!5Bbd2}OBu!fnas?;G3^*NISbhqN;<0ZwzQKb`kfS2P3Smcp&q@L7ZrI@`h zm@7MwPXKuX__(NoOb`Xy$V#;6XbkIDa*nK*X|Pq-Wset-6!G!#lA^58v9r0M>`Pjz zs-3Kr;N_$OTE{_Uiu^-iD5Q&%41#(ZnR|jzlQD`wcp!&Bk?m#yY5M=H%^W?ssbFSX z;ph+_A1^5z=DM`&7NJ){6TB)ZT<@2=y)p~fJJx_rbdog01Zgt~r1u#6g-4>nEnyqy zxnfK|a}*zozeYQXk+5`#kB^s>C6gUmQF$(x>f8KzRRdS1v|UnfP6VAwql z6p}V6lC~ivd2#fzOPG&ljhB-uVuN&8fNBcL4J|TF49IHo1Jut1B!aE!RcS++7oY)F zLK~_O?ISim$uC>w82&;{4bbmpcz>tT{c^occi9qF%iIXXbQA=pzR z&)i6EJ;|ZRzLlP>L?A^3d=ipEQ@%~0hkC0no!WMxmv(epOZ{6zmO4d2uER2V9Yr@Q zx+&95if$d~CaDDf7ef9K8sER|+yMr_Fc3u1`=7d11j<6He(=Hq727X1$jtQbJ~NcG z7b3nHO0WOe`2Kq8cqktS<`$GyJ(Rh@ym}~3O01U)W{MR+aZ=0_x6iy$P;VgXUtGyQ z?7Ke$>X|}hT)?2s=i*>-VTP)D=ON@apTDe002ovPDHLkV1hiHc`5(^ literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/radialMenu/moveTroops.png b/Mods/vcmi/Data/radialMenu/moveTroops.png new file mode 100644 index 0000000000000000000000000000000000000000..710657656c83486a2ad529b34fc1a905c0064803 GIT binary patch literal 518 zcmeAS@N?(olHy`uVBq!ia0vp^u0ZV1!3-q3PW-3^QceLrAt1s3_p|?B^8u0fGymVq z{C_v&|J}6z_ki52|94aW---QyC+`29=>PYU{y)h6e=qC*C7b{ElK1xBZ?K0|R5cr;B4q1>@W4iQb1D1YFYREPJ*kYW1)G z|5ttG2%Aw-yum%F>wUIdzpef>ooR}nCMbO7WjM(<`S>Qz6D5nC)TBE3tnXOe{HGx{ zS@xU3jpAD_?Y4On|K4A5=l0we8;;l8ZvV-;iEm>2|Eb$fHJ5P|p44jAsy_30Qn;L9 z)}jQl>1&j19la%|uUYiGoAJ2n`YV^^;!Hkr9ami~Q~x^O%J>jagTq7<2d5I1>2H>K z*LKYPU%B2ZY~@SG&`X?s-f=s7BxFvg&UbC{s<^CIx%9zayUBJ5EJBX*6%7ezyu&UY zdG09j-GJ$&Hmgw1jEGIA#JgCOC#)%A@w>>nGiBz<&Av$Rzt_vrhI>TbTKGj+kHr{>N{)1r4OyS?Ch0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iTSY0A4(%W!lA$`aAS&XhRVYG*P%E_RU~=gfG-*gu zTpR`0f`cE6RRU<7fAnfjb4rr|lh?&0I>U4mzMpZjwRC4rtTK|H-_ z>74h8qpU0`#OK6ugDyz?$aUG}H_j!8{X8>jWHa-`QDULg#c~(3vY`@B5yuo&qkMnP zWrgz=XSG^q?R)YUh6>usGS_JiBZWmQL4*JqbyQG=g#@h{DJC+spY-q#Iew8`GPx>X z#CInQ3X%XG0MCHWo@+Nn5VjMT2PT0_roKI4Fj0np=lbskV9ayvmLf#%1>OLa zKoS`DTsu`ZqIjM@B31xW%Afu{!u0#FMq7VXAQUsEJYYL z4Wvv7V*HdGm`YPXeUYJNfZNJ)w#(K!2nbULd;^XZ7-AJT8`ao7wDJk+5m2aHlNiE6}H>Oqk`aDsGK-7U;phhsnhv z(4#EpXAaw;75|+Fi>NLH3J z3Ove^F=>2HbvH!3H6X?QMwJWPic%2M8|`KMo4cfbG!5J-)OFWJ)d&2``b^1Oj9L)W z8|@Tu1^@bD0smfRtB4F-!M`8c1a^Qn6M~ck&$S0U*RG5(UW4b_%?N@Z2!bF8f*=Tj hAP9mW2!ik*^ABcH3D}&50Pp|+002ovPDHLkV1fqIVtxPs diff --git a/Mods/vcmi/Data/radialMenu/swapArtifacts.png b/Mods/vcmi/Data/radialMenu/swapArtifacts.png index 303b45ae0daf464238ad8ec70368b38b99352ca2..6e513e94e4d1f70005af332fc92145e56d676f3e 100644 GIT binary patch delta 1809 zcmV+s2k!XD2%ip+BYy_wNklpT*pR0Q*rS^3x z6&MeY>~3N#U+5A5eB?N82nDVSbqO3rGvjE4F0yGQS{*-iYH;9M_YaH>-Zqv zg#lTF0oh#e!JMvRB(pF8PIgpy2t_CdZ`KqgSr!C$thR02wtwxhZQHhOTW#CM*tTuO z=DTNp>!Z%r-H&qB%jif%|AV> zf@6bqx>}jYIo)X5t8`$M*Xf?o`b~32+xBnQuC)n$Rf_+g$`hX$q#R!B-Obum{)4@x z;-CHrMwmOs_J3{ef#AH{{lQgR7PuXmKHU1-=)Pu;rw+ARx?sFrm*G846<5!2Qkpo( zq8&df_~R2vN@dF64pfF0DHEW8DW#`+oTx}1fU;9PablD2+x2r?ZXa3agOU?T$k`wK z>ELSb#(86H?`~b__8@g)yP!t`FMP2rU-z`P+<~HO`ZN-Zej`&2AQk^z@ zje)i_tW4yeHxYTZ_8UZiluCz_PPW-EQt_Gade3stA^vV^zvwBN3TENiy>h&G zP>olQoACU86&_qm$E}uw3`ld~93kk(o6Y-uB+7&ce1u=3_#m2;zT5w1;_coQo(4OY zxKDAhQG%RId&Gz6;^FlyJi3uhK^yVr*#$hgQ-4fZ*~cjv9EB{LXelXFO!LS}C zuzxU?14$qC=T4T98;!e#0R~b599rXzg2Ry?NC|v+xn)V=6N{A0-GMS>h@J?nkL_n( zOM#x(XHNv_Jjft1>GO0{L1oH7HtB;r)sc4~giU!t1&WU+qAqLvLEIh1g%nBQ6N{AO z?E&iJ`kOz|Q0e?@;RL%238A{52PPR||9>hkH0I9$l^)j2bi(3^_H4qJp6pJ65^%L{ z8^|!>_(oqee7x@Q6S!Q-G^T4j99%#s$O7DdrYi|$*o${J|!#pxj?IleLM|`45A@?U0;D4_E zlFG7I-+05Hv|k`9KpVYc3=rn4iL%qZs4N39w!b-AisrLmG`yJP(TzO88fK!ZQin9u z=S-5gyXA;aG$|gADxJs!yc|EkB9ELS^`r(X#q52-LfL_Q0>~S{$3+!nf+)~NR-#2m zV_3hEb7Z|tgRQzQd%VO+;fPN(DSyfe9Xp#F%D$wfs@lm~30_VrpmiKnrpP}OhC;eH z$snkwk+~-bH5sD_ga>j66xnVTkf#67+RV|Dn+j&Ol{6_F@rfp7!(5kk-6Hf#Xo6QI zh3oxtw^wEXd&e5EiB6J+m>_K?f%G0@zwk&jge7d_JXehAXO7}y@z-caF@KUYA)N7v zCS}QFhgMXc%cVNELxi6e{9M%_lqqeO)SDAQXBH4LWNW4XTFH?Zrc0$8L_>_SbT|qG z6ts=rtT@}xlm{uC@QEhH)?7hKQNF{jHf>scpozV4SWna2&Sar|De$SyeyB+w$ubci zqQe4{46@$cvBaGXI2vB)7JsRa^0R$^RHqLQk~AS4@rfpdq)m#XZ3szT9KGxk=A&8T z<)n((ARQKW_hAQ#);fGI*Gg>l6L!Xnl<8W5$5Y8fimW=q-R@#%Sqehr4tTq$JM&TYvuEMC0L)PppG$ z1NBV1Bjt&-GdGf3Pjcw7Z>47|4y16v zCnhO0<=X^$sJH6Uscjc}X-BuU)W0=ksZ$i>IxM5tQFODSn=;*`=+=R5;!5y;A>^Nb z+qnY_fMF;G!}mY+rhgy@sA%ww2e8Jkd$1(;`bPiz1j>dMCcZk9y!boQgpxC+p+sm) zaO123ln7jz0VrCmrWYXTH=j{i?!iX%OK_hGc015yANkvXXu0mjfKp%y4 delta 1076 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 : ObjectInstanceID::NONE; std::vector menuElements = { - { RadialMenuConfig::ITEM_NW, twoHeroes, "stackMerge", "vcmi.radialWheel.heroGetArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArmy(false, std::nullopt);} }, + { RadialMenuConfig::ITEM_NW, twoHeroes, "moveTroops", "vcmi.radialWheel.heroGetArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArmy(false, std::nullopt);} }, { RadialMenuConfig::ITEM_NE, twoHeroes, "stackSplitDialog", "vcmi.radialWheel.heroSwapArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArmy();} }, - { RadialMenuConfig::ITEM_EE, twoHeroes, "trade", "vcmi.radialWheel.heroExchange", [heroId, heroOtherId](){LOCPLINT->showHeroExchange(heroId, heroOtherId);} }, - { RadialMenuConfig::ITEM_SW, twoHeroes, "getArtifacts", "vcmi.radialWheel.heroGetArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArtifacts(false, true, true);} }, + { RadialMenuConfig::ITEM_EE, twoHeroes, "tradeHeroes", "vcmi.radialWheel.heroExchange", [heroId, heroOtherId](){LOCPLINT->showHeroExchange(heroId, heroOtherId);} }, + { RadialMenuConfig::ITEM_SW, twoHeroes, "moveArtifacts", "vcmi.radialWheel.heroGetArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArtifacts(false, true, true);} }, { RadialMenuConfig::ITEM_SE, twoHeroes, "swapArtifacts", "vcmi.radialWheel.heroSwapArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArtifacts(true, true);} }, - { RadialMenuConfig::ITEM_WW, true, "remove", "vcmi.radialWheel.heroDismiss", [this]() + { RadialMenuConfig::ITEM_WW, true, "dismissHero", "vcmi.radialWheel.heroDismiss", [this]() { CFunctionList ony = [=](){ }; ony += [=](){ LOCPLINT->cb->dismissHero(hero); }; From e05102192ba87394d3ed1967b533da03c3f75c5b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 17 Oct 2023 21:13:08 +0200 Subject: [PATCH 0860/1248] heroes and more buttons --- Mods/vcmi/Data/radialMenu/altDownBottom.png | Bin 0 -> 1919 bytes Mods/vcmi/Data/radialMenu/altUpTop.png | Bin 0 -> 1852 bytes Mods/vcmi/config/vcmi/english.json | 6 +- client/PlayerLocalState.cpp | 6 ++ client/PlayerLocalState.h | 1 + client/adventureMap/AdventureMapInterface.cpp | 1 + client/adventureMap/CList.cpp | 62 ++++++++++++++++-- client/adventureMap/CList.h | 4 +- 8 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 Mods/vcmi/Data/radialMenu/altDownBottom.png create mode 100644 Mods/vcmi/Data/radialMenu/altUpTop.png diff --git a/Mods/vcmi/Data/radialMenu/altDownBottom.png b/Mods/vcmi/Data/radialMenu/altDownBottom.png new file mode 100644 index 0000000000000000000000000000000000000000..f157471382d82e2016d9702654450107f76187c2 GIT binary patch literal 1919 zcmV-_2Y~pAP)EX>4Tx04R}tkv&MmKpe$iQ$;B)qIM8*$WWc^q9Wo{t5Adrp;l;tBaGOiZLMN=c5B95q13P5N=!|DsicUzoJTVo_6#aIA>1EG(>>rZ!sbg%;Yz`au=(g97fJb7rO& zU=tMH3X<&)xz= zDL~Y-|4<~>yZ=tY7EvM8wLs8vv*G21SjKyYVA00siDrSE1fX6_v7CG51-w^Q8lHxy;c0jpo|cBE;c0jpo|cBE;c0jpo`$ET z;c0jpo`$EDdw3EcGWiWLQ2<1iMxnMRg z7|R5B%{|bBMv~0(f7h#m01+N=R zE9Hm*x^V67@kts@--`ph_Es!%q(v$4O2cw`<=aF#l!HVwGCZ6fmkCa9{7m z*ms|&?^Wf1l}Db$-D6(A;O!~AK+uvxsNY}|SukJw8zhBtgD}8c`D-+M_?VFDvKL+h zJx#~!gVy-GKavUrEgA0Ia|+zo4O$`oSG`6!z*kO!l~}ed`n>eYM9FPr!nFD1^+$nq z6Qp+4hIJ74%K+ykyRt-~;LT`XsOjL=UG0B*?KSU5-cvcYjDh=3fcfp0+V_%$0AK$2 zLZrFbb>-DedF2JXtqtwNeO-+w!E3L9md?Gx89oQRy2rM~h3C)Ly!6TocvCwM`Fm`j z^#BatMAD{6A;A12k)FeQg;X~(?Ufhsl)_j%Av(dUeg<1U`((vyCcyn|t}Cwwf|i|W zue^X42wGA~bvxSk%|WoryEAU~X{iG95A}5&Xi^Ge+3?B>cu#j8iG=t8ykY>X_QfM_ zzTyE`+3C$Y+b+*l#CqwK7w`a}K4^{i_Z)5puf9HY>N0&wZY-7H{vUfg_a4oKS6;va zfQ=uVJ_lCX0ls2t!Y3wRuDTtId~I#n)jpUjue^W<0DWDJr(o7ZK?@aVnE>;hiN{4- zuDwz|tjT6$t}8ErE;$49^|hdj?ho`FJioP}eK^-%c>ymFwA^qj>Dv76r^Oe-`|dcd z{AoUT0gV283%sn7a9Nn002ovPDHLk FV1kT!lxY9} literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Data/radialMenu/altUpTop.png b/Mods/vcmi/Data/radialMenu/altUpTop.png new file mode 100644 index 0000000000000000000000000000000000000000..ae34d577595f37affca5ff3de0a39afc99316ff6 GIT binary patch literal 1852 zcmV-C2gCS@P)EX>4Tx04R}tkv&MmKpe$iQ$;B)qIM8*$WWc^q9Wo{t5Adrp;l;tBaGOiZLMN=c5B95qQE~&&K#E3#PA*I{h zUMV2cHkfdU3l$RzXr;zK!o+_7A57GT0Rleu$+#^SCA|7zjDkR0N|#->7c4F8%$Yep zEDuE3Ep2Car{;W<$!@Z&{e79d@cS_PD6out`d2!QVjM2LK=*K4^60btq z&+p_;5=62#+IFgk2$jWPEeUx*i~a&io-#0M?zi!_7U1IHPSIu?`ku^m#qC z+5ddw0?t?fVmwm2x$$dh-}i)77SaxRJ#l6pjygS{vG?Ij?1ep4xv^H;s@U;PtTY?}A7Ky*V@q*mnST+8dTgR6(SVa?Un+b@_TCk8`RGH+|9rkyw>o z>F_F!ho}b(_GoQ%SMLEvwAvT);-(P_>O2f~}YfoKGBCmIUc}Y?FjD zwJFlt+Y&e3Ul;nad_nT!roo741xQOGu=WoG*5*D?DK$9b8)1)DK~BC{Fk9&$Q1b+= zj6js9(@a7-+@1X`#4weIwIKyokpNMi+cnPsYz9CGz=Ea-3$9af)6f~wDq^V0S7eB2 zMYQ?YSIFFGfEa%93b6jxBf)Eaymsl^ehK+FM>%nc=#{$g;nTVt*^!ThVC@fAem?V^ zgq+D!nq1eFQ&J*b)R6}Ot+9R!K;r3^_of2kGjoh6R|^F13CI1ZHP%0ykEI7Ijv$kt z1!7aeasSH4^6h%w^KD3$zurp%pwZmN<0WfJ$ot<$$y4%_JS9)bQ&IAiJS9)bQ&IAi zJS9)bQ}R@lJS9)bQ}R?wFOLDB-0yWu##(f=;xr>UKoVd8{pWm12ie~w2ffV6^pfR& zc>)w1g0?0FR<@}Dk>@}1<1ekF3}CZ<#Q1L0000 & PlayerLocalState::getOwnedTowns() { return ownedTowns; diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index 0ace8cf86..98c30e854 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -73,6 +73,7 @@ public: const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); void addWanderingHero(const CGHeroInstance * hero); void removeWanderingHero(const CGHeroInstance * hero); + void swapWanderingHero(int pos1, int pos2); void setPath(const CGHeroInstance * h, const CGPath & path); bool setPath(const CGHeroInstance * h, const int3 & destination); diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index afa9179f8..7ef3a59ed 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -304,6 +304,7 @@ void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) auto town = dynamic_cast(sel); widget->getInfoBar()->showTownSelection(town); + widget->getTownList()->updateWidget();; widget->getTownList()->select(town); widget->getHeroList()->select(nullptr); onHeroChanged(nullptr); diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index c451b9cf1..2257399c6 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -218,7 +218,8 @@ CHeroList::CEmptyHeroItem::CEmptyHeroItem() CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) : CListItem(parent), - hero(Hero) + hero(Hero), + parentList(parent) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); movement = std::make_shared(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1); @@ -229,6 +230,8 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero) pos.h = std::max(std::max(movement->pos.h + 1, mana->pos.h + 1), portrait->pos.h); update(); + + addUsedEvents(GESTURE); } void CHeroList::CHeroItem::update() @@ -264,6 +267,43 @@ std::string CHeroList::CHeroItem::getHoverText() return boost::str(boost::format(CGI->generaltexth->allTexts[15]) % hero->getNameTranslated() % hero->type->heroClass->getNameTranslated()); } +void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if(!on) + return; + + if(!hero) + return; + + auto & heroes = LOCPLINT->localState->getWanderingHeroes(); + + if(heroes.size() < 2) + return; + + int heroPos = vstd::find_pos(heroes, hero); + const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes[heroPos - 1]; + const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1]; + + std::vector menuElements = { + { RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [this, heroPos]() + { + for (int i = heroPos; i > 0; i--) + LOCPLINT->localState->swapWanderingHero(i, i - 1); + parentList->updateWidget(); + } }, + { RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [this, heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [this, heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, heroPos, heroes]() + { + for (int i = heroPos; i < heroes.size() - 1; i++) + LOCPLINT->localState->swapWanderingHero(i, i + 1); + parentList->updateWidget(); + } }, + }; + + GH.windows().createAndPushWindow(pos.center(), menuElements, true); +} + std::shared_ptr CHeroList::createItem(size_t index) { if (LOCPLINT->localState->getWanderingHeroes().size() > index) @@ -374,8 +414,8 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const if(!on) return; - const std::vector towns = LOCPLINT->localState->getOwnedTowns(); - + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex]) return; @@ -386,8 +426,20 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.townUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.townDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this, townUpperPos]() + { + for (int i = townIndex; i > 0; i--) + LOCPLINT->localState->swapOwnedTowns(i, i - 1); + parentList->updateWidget(); + } }, + { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, + { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, townLowerPos, towns]() + { + for (int i = townIndex; i < towns.size() - 1; i++) + LOCPLINT->localState->swapOwnedTowns(i, i + 1); + parentList->updateWidget(); + } }, }; GH.windows().createAndPushWindow(pos.center(), menuElements, true); diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index d8ee860d9..e7fe944c4 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -117,6 +117,7 @@ class CHeroList : public CList std::shared_ptr movement; std::shared_ptr mana; std::shared_ptr portrait; + CHeroList *parentList; public: const CGHeroInstance * const hero; @@ -127,6 +128,7 @@ class CHeroList : public CList void select(bool on) override; void open() override; void showTooltip() override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; std::string getHoverText() override; }; @@ -152,7 +154,7 @@ class CTownList : public CList std::shared_ptr picture; CTownList *parentList; public: - int townIndex; + int townPos; CTownItem(CTownList *parent, const CGTownInstance * town); From 464c65a595d3af6200555fedf4e0bc755fdc8373 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 17 Oct 2023 21:17:57 +0200 Subject: [PATCH 0861/1248] fix --- client/adventureMap/CList.cpp | 2 +- client/adventureMap/CList.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 2257399c6..815a4cff9 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -414,7 +414,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const if(!on) return; - const std::vector towns = LOCPLINT->localState->getOwnedTowns(); + const std::vector towns = LOCPLINT->localState->getOwnedTowns(); if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex]) return; diff --git a/client/adventureMap/CList.h b/client/adventureMap/CList.h index e7fe944c4..01312e725 100644 --- a/client/adventureMap/CList.h +++ b/client/adventureMap/CList.h @@ -154,7 +154,7 @@ class CTownList : public CList std::shared_ptr picture; CTownList *parentList; public: - int townPos; + int townIndex; CTownItem(CTownList *parent, const CGTownInstance * town); From 2fcbf78e4e7f50714722dcb0e76ef563d81409f2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 17 Oct 2023 21:22:27 +0200 Subject: [PATCH 0862/1248] unused var --- client/adventureMap/CList.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/adventureMap/CList.cpp b/client/adventureMap/CList.cpp index 815a4cff9..49f3a383f 100644 --- a/client/adventureMap/CList.cpp +++ b/client/adventureMap/CList.cpp @@ -426,7 +426,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1; std::vector menuElements = { - { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this, townUpperPos]() + { RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this]() { for (int i = townIndex; i > 0; i--) LOCPLINT->localState->swapOwnedTowns(i, i - 1); @@ -434,7 +434,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const } }, { RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } }, { RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } }, - { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, townLowerPos, towns]() + { RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, towns]() { for (int i = townIndex; i < towns.size() - 1; i++) LOCPLINT->localState->swapOwnedTowns(i, i + 1); From 15e45f966c3f5fdc85c2ffe5765b6fd0c5b8b64a Mon Sep 17 00:00:00 2001 From: Alexandre Detiste Date: Tue, 17 Oct 2023 22:06:08 +0200 Subject: [PATCH 0863/1248] typos found by lintian --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 2 +- client/gui/InterfaceObjectConfigurable.cpp | 6 +++--- docs/developers/Logging_API.md | 4 ++-- lib/JsonNode.cpp | 2 +- lib/battle/BattleInfo.cpp | 4 ++-- lib/mapObjects/CGCreature.h | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- mapeditor/inspector/inspector.cpp | 2 +- mapeditor/mainwindow.cpp | 4 ++-- mapeditor/translation/english.ts | 2 +- mapeditor/translation/french.ts | 2 +- mapeditor/translation/german.ts | 2 +- mapeditor/translation/polish.ts | 2 +- mapeditor/translation/russian.ts | 2 +- mapeditor/translation/spanish.ts | 2 +- mapeditor/translation/ukrainian.ts | 2 +- mapeditor/translation/vietnamese.ts | 2 +- 17 files changed, 22 insertions(+), 22 deletions(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index ded687924..494682d3b 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -111,7 +111,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", + "Hero %s in garrison of town %s is supposed to defend the town", town->garrisonHero->getNameTranslated(), town->getNameTranslated()); diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index a840c9acb..7ad244622 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -198,7 +198,7 @@ ETextAlignment InterfaceObjectConfigurable::readTextAlignment(const JsonNode & c if(config.String() == "right") return ETextAlignment::BOTTOMRIGHT; } - logGlobal->debug("Uknown text alignment attribute"); + logGlobal->debug("Unknown text alignment attribute"); return ETextAlignment::CENTER; } @@ -231,7 +231,7 @@ ColorRGBA InterfaceObjectConfigurable::readColor(const JsonNode & config) const return ColorRGBA(asVector[0].Integer(), asVector[1].Integer(), asVector[2].Integer()); } } - logGlobal->debug("Uknown color attribute"); + logGlobal->debug("Unknown color attribute"); return Colors::DEFAULT_KEY_COLOR; } @@ -262,7 +262,7 @@ EFonts InterfaceObjectConfigurable::readFont(const JsonNode & config) const if(config.String() == "calisto") return EFonts::FONT_CALLI; } - logGlobal->debug("Uknown font attribute"); + logGlobal->debug("Unknown font attribute"); return EFonts::FONT_TIMES; } diff --git a/docs/developers/Logging_API.md b/docs/developers/Logging_API.md index 65ea11ea0..6de4f7cc6 100644 --- a/docs/developers/Logging_API.md +++ b/docs/developers/Logging_API.md @@ -104,7 +104,7 @@ Don't include a '\n' or std::endl at the end of your log message, a new line wil The following list shows several log levels from the highest one to the lowest one: -- error -\> for errors, e.g. if resource is not available, if a initialization fault has occured, if a exception has been thrown (can result in program termination) +- error -\> for errors, e.g. if resource is not available, if a initialization fault has occurred, if a exception has been thrown (can result in program termination) - warn -\> for warnings, e.g. if sth. is wrong, but the program can continue execution "normally" - info -\> informational messages, e.g. Filesystem initialized, Map loaded, Server started, etc... - debug -\> for debugging, e.g. hero moved to (12,3,0), direction 3', 'following artifacts influence X: .. or pattern detected at pos (10,15,0), p-nr. 30, flip 1, repl. 'D' @@ -160,4 +160,4 @@ global, level=debug ai, level=not set, effective level=debug ai.battle, level=trace, effective level=trace -The same technique is applied to the console colors. If you want to have another debug color for the domain ai, you can explicitely set a color for that domain and level. \ No newline at end of file +The same technique is applied to the console colors. If you want to have another debug color for the domain ai, you can explicitely set a color for that domain and level. diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 5b6b27957..44999cfb0 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -860,7 +860,7 @@ static BonusParams convertDeprecatedBonus(const JsonNode &ability) params.targetType = BonusSource::SECONDARY_SKILL; } - logMod->warn("Please, use this bonus:\n%s\nConverted sucessfully!", params.toJson().toJson()); + logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); return params; } else diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 27b2d9b05..89cdf6cbe 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -253,7 +253,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const catch(RangeGenerator::ExhaustedPossibilities &) { //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place absolute obstacle"); + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place absolute obstacle"); } } @@ -306,7 +306,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } catch(RangeGenerator::ExhaustedPossibilities &) { - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place usual obstacle"); + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place usual obstacle"); } } diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index ac599465a..385b96cb5 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -23,7 +23,7 @@ public: }; enum Character { - COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 + COMPLIANT = 0, FRIENDLY = 1, AGGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 }; ui32 identifier; //unique code for this monster (used in missions) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index b5c2ded6e..3df10a799 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1117,7 +1117,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob if(agressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) logGlobal->warn( - "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemeted!", + "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", mapName, mapPosition.toString(), agressionExact, diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 9f188860a..381d8ff2f 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -34,7 +34,7 @@ static QList> CharacterIdentifiers { {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, {QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY))}, - {QObject::tr("Agressive"), QVariant::fromValue(int(CGCreature::Character::AGRESSIVE))}, + {QObject::tr("Aggressive"), QVariant::fromValue(int(CGCreature::Character::AGGRESSIVE))}, {QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE))}, {QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE))}, }; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index fae6dc512..ebab1ba96 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -358,7 +358,7 @@ bool MainWindow::openMap(const QString & filenameSelect) catch(const ModIncompatibility & e) { assert(e.whatExcessive().empty()); - QMessageBox::warning(this, "Mods are requiered", QString::fromStdString(e.whatMissing())); + QMessageBox::warning(this, "Mods are required", QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) @@ -1108,7 +1108,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() if(errors) - QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occured. %1 objects were not updated")).arg(errors)); + QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occurred. %1 objects were not updated")).arg(errors)); } diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 1c026f635..da1c573c9 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index b2f21c8dd..f9517019f 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 8cbaaf904..e3108fca8 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index 86fa6daeb..84322017a 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 6c5641347..d311dc48b 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 79f703770..bbc80f230 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index b6d948bbd..ee623286b 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index fc5e130ac..55015581b 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Xảy ra lỗi. %1 mục tiêu không được cập nhật From 966ffe4377ae4297737f384fc86253fa46608ee0 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Wed, 18 Oct 2023 00:13:32 +0200 Subject: [PATCH 0864/1248] Fix warnings about empty kill target --- lib/mapObjects/CQuest.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 85e154128..69eefc237 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -423,6 +423,9 @@ void IQuestObject::afterAddToMapCommon(CMap * map) const void CGSeerHut::setObjToKill() { + if(quest->killTarget == ObjectInstanceID::NONE) + return; + if(getCreatureToKill(true)) { quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? From f9e1b302c720c255dc4b3befeb79859e4b24fde1 Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Wed, 18 Oct 2023 01:41:02 +0200 Subject: [PATCH 0865/1248] some spelling fixes --- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 2 +- AI/VCAI/Pathfinding/PathfindingManager.cpp | 2 +- client/CMT.cpp | 2 +- client/gui/InterfaceObjectConfigurable.cpp | 4 ++-- lib/JsonNode.cpp | 2 +- lib/battle/BattleInfo.cpp | 4 ++-- lib/mapObjects/CGCreature.h | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- mapeditor/inspector/inspector.cpp | 2 +- mapeditor/mainwindow.cpp | 6 +++--- mapeditor/translation/english.ts | 4 ++-- mapeditor/translation/french.ts | 4 ++-- mapeditor/translation/german.ts | 4 ++-- mapeditor/translation/polish.ts | 4 ++-- mapeditor/translation/russian.ts | 4 ++-- mapeditor/translation/spanish.ts | 4 ++-- mapeditor/translation/ukrainian.ts | 4 ++-- mapeditor/translation/vietnamese.ts | 4 ++-- 18 files changed, 30 insertions(+), 30 deletions(-) diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index ded687924..494682d3b 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -111,7 +111,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) { logAi->trace( - "Hero %s in garrison of town %s is suposed to defend the town", + "Hero %s in garrison of town %s is supposed to defend the town", town->garrisonHero->getNameTranslated(), town->getNameTranslated()); diff --git a/AI/VCAI/Pathfinding/PathfindingManager.cpp b/AI/VCAI/Pathfinding/PathfindingManager.cpp index e418a6dd3..3b56951ed 100644 --- a/AI/VCAI/Pathfinding/PathfindingManager.cpp +++ b/AI/VCAI/Pathfinding/PathfindingManager.cpp @@ -238,6 +238,6 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet void PathfindingManager::updatePaths(std::vector heroes) { - logAi->debug("AIPathfinder has been reseted."); + logAi->debug("AIPathfinder has been reset."); pathfinder->updatePaths(heroes); } diff --git a/client/CMT.cpp b/client/CMT.cpp index 041cbaaa9..27014dc23 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -126,7 +126,7 @@ int main(int argc, char * argv[]) ("spectate-battle-speed", po::value(), "battle animation speed for spectator") ("spectate-skip-battle", "skip battles in spectator view") ("spectate-skip-battle-result", "skip battle result window") - ("onlyAI", "allow to run without human player, all players will be default AI") + ("onlyAI", "allow one to run without human player, all players will be default AI") ("headless", "runs without GUI, implies --onlyAI") ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index a840c9acb..0bcf0a611 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -198,7 +198,7 @@ ETextAlignment InterfaceObjectConfigurable::readTextAlignment(const JsonNode & c if(config.String() == "right") return ETextAlignment::BOTTOMRIGHT; } - logGlobal->debug("Uknown text alignment attribute"); + logGlobal->debug("Unknown text alignment attribute"); return ETextAlignment::CENTER; } @@ -720,7 +720,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildWidget(JsonNode co //overrides from variables for(auto & item : config["overrides"].Struct()) { - logGlobal->debug("Config attribute %s was overriden by variable %s", item.first, item.second.String()); + logGlobal->debug("Config attribute %s was overridden by variable %s", item.first, item.second.String()); config[item.first] = variables[item.second.String()]; } diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 5b6b27957..44999cfb0 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -860,7 +860,7 @@ static BonusParams convertDeprecatedBonus(const JsonNode &ability) params.targetType = BonusSource::SECONDARY_SKILL; } - logMod->warn("Please, use this bonus:\n%s\nConverted sucessfully!", params.toJson().toJson()); + logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); return params; } else diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 27b2d9b05..89cdf6cbe 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -253,7 +253,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const catch(RangeGenerator::ExhaustedPossibilities &) { //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place absolute obstacle"); + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place absolute obstacle"); } } @@ -306,7 +306,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const } catch(RangeGenerator::ExhaustedPossibilities &) { - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occured - cannot place usual obstacle"); + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place usual obstacle"); } } diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index ac599465a..385b96cb5 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -23,7 +23,7 @@ public: }; enum Character { - COMPLIANT = 0, FRIENDLY = 1, AGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 + COMPLIANT = 0, FRIENDLY = 1, AGGRESSIVE = 2, HOSTILE = 3, SAVAGE = 4 }; ui32 identifier; //unique code for this monster (used in missions) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index b5c2ded6e..3df10a799 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1117,7 +1117,7 @@ CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const Ob if(agressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) logGlobal->warn( - "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemeted!", + "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", mapName, mapPosition.toString(), agressionExact, diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 9f188860a..381d8ff2f 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -34,7 +34,7 @@ static QList> CharacterIdentifiers { {QObject::tr("Compliant"), QVariant::fromValue(int(CGCreature::Character::COMPLIANT))}, {QObject::tr("Friendly"), QVariant::fromValue(int(CGCreature::Character::FRIENDLY))}, - {QObject::tr("Agressive"), QVariant::fromValue(int(CGCreature::Character::AGRESSIVE))}, + {QObject::tr("Aggressive"), QVariant::fromValue(int(CGCreature::Character::AGGRESSIVE))}, {QObject::tr("Hostile"), QVariant::fromValue(int(CGCreature::Character::HOSTILE))}, {QObject::tr("Savage"), QVariant::fromValue(int(CGCreature::Character::SAVAGE))}, }; diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index fae6dc512..71b0e6f55 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -105,7 +105,7 @@ void MainWindow::parseCommandLine(ExtractionOptions & extractionOptions) {"e", QCoreApplication::translate("main", "Extract original H3 archives into a separate folder.")}, {"s", QCoreApplication::translate("main", "From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's.")}, {"c", QCoreApplication::translate("main", "From an extracted archive, Converts single Images (found in Images folder) from .pcx to png.")}, - {"d", QCoreApplication::translate("main", "Delete original files, for the ones splitted / converted.")}, + {"d", QCoreApplication::translate("main", "Delete original files, for the ones split / converted.")}, }); parser.process(qApp->arguments()); @@ -358,7 +358,7 @@ bool MainWindow::openMap(const QString & filenameSelect) catch(const ModIncompatibility & e) { assert(e.whatExcessive().empty()); - QMessageBox::warning(this, "Mods are requiered", QString::fromStdString(e.whatMissing())); + QMessageBox::warning(this, "Mods are required", QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) @@ -1108,7 +1108,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() if(errors) - QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occured. %1 objects were not updated")).arg(errors)); + QMessageBox::warning(this, tr("Update appearance"), QString(tr("Errors occurred. %1 objects were not updated")).arg(errors)); } diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 1c026f635..3e5d669d1 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated @@ -1011,7 +1011,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index b2f21c8dd..91cd9b88a 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated @@ -1015,7 +1015,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 8cbaaf904..07b7bac38 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden @@ -1025,7 +1025,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index 86fa6daeb..df426e4e4 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych @@ -1025,7 +1025,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 6c5641347..25e4ae21b 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated @@ -1015,7 +1015,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 79f703770..8018b8000 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated @@ -1015,7 +1015,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Eliminar archivos originales, por los que se han separado / convertido. diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index b6d948bbd..2233f658e 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -363,7 +363,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated @@ -1021,7 +1021,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index fc5e130ac..0d37f5242 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -353,7 +353,7 @@ - Errors occured. %1 objects were not updated + Errors occurred. %1 objects were not updated Xảy ra lỗi. %1 mục tiêu không được cập nhật @@ -1011,7 +1011,7 @@ - Delete original files, for the ones splitted / converted. + Delete original files, for the ones split / converted. Xóa các tập tin gốc đã được phân chia / chuyển đổi. From ce480c8c84bf7f8acbb18ed2d15e778a84a03810 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 18 Oct 2023 17:14:22 +0300 Subject: [PATCH 0866/1248] Fixed Scholar handling --- config/objects/rewardableScholar.json | 14 +++----- lib/JsonRandom.cpp | 32 ++++++++++------- lib/mapObjects/CRewardableObject.cpp | 12 +++++-- lib/mapping/MapFormatH3M.cpp | 51 ++++++++++++++++++++++++--- lib/rewardable/Info.cpp | 7 +++- 5 files changed, 87 insertions(+), 29 deletions(-) diff --git a/config/objects/rewardableScholar.json b/config/objects/rewardableScholar.json index f7c45a874..c6d1ed139 100644 --- a/config/objects/rewardableScholar.json +++ b/config/objects/rewardableScholar.json @@ -41,7 +41,7 @@ "appearChance" : { "min" : 0, "max" : 33 }, "message" : 115, "limiter" : { - "canLearnSpell" : [ + "canLearnSpells" : [ "@gainedSpell" ] }, @@ -57,7 +57,7 @@ // Hero does not have this skill at expert "noneOf" : [ { - "secondarySkill" : { + "secondary" : { "@gainedSkill" : 3 } } @@ -68,13 +68,9 @@ "canLearnSkills" : true }, { - "noneOf" : [ - { - "secondarySkill" : { - "@gainedSkill" : 1 - } - } - ] + "secondary" : { + "@gainedSkill" : 1 + } } ] }, diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index f8043c7c2..2e78df61f 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -107,6 +107,15 @@ namespace JsonRandom return PrimarySkill(*VLC->identifiers()->getIdentifier("primarySkill", value)); } + template<> + PrimarySkill decodeKey(const std::string & modScope, const std::string & value, const Variables & variables) + { + if (value.empty() || value[0] != '@') + return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value)); + else + return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast(PrimarySkill::NONE))); + } + /// Method that allows type-specific object filtering /// Default implementation is to accept all input objects template @@ -300,25 +309,24 @@ namespace JsonRandom std::vector loadPrimaries(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) { - std::vector ret; + std::vector ret(GameConstants::PRIMARY_SKILLS, 0); + std::set defaultSkills{ + PrimarySkill::ATTACK, + PrimarySkill::DEFENSE, + PrimarySkill::SPELL_POWER, + PrimarySkill::KNOWLEDGE + }; + if(value.isStruct()) { - for(const auto & name : NPrimarySkill::names) + for(const auto & pair : value.Struct()) { - ret.push_back(loadValue(value[name], rng, variables)); + PrimarySkill id = decodeKey(pair.second.meta, pair.first, variables); + ret[static_cast(id)] += loadValue(pair.second, rng, variables); } } if(value.isVector()) { - std::set defaultSkills{ - PrimarySkill::ATTACK, - PrimarySkill::DEFENSE, - PrimarySkill::SPELL_POWER, - PrimarySkill::KNOWLEDGE - }; - - ret.resize(GameConstants::PRIMARY_SKILLS, 0); - for(const auto & element : value.Vector()) { std::set potentialPicks = filterKeys(element, defaultSkills, variables); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 2780e0d2d..da74db7f3 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -55,12 +55,18 @@ std::vector CRewardableObject::loadComponents(const CGHeroInstance * { std::vector result; - if (rewardIndices.size() > 1) + if (rewardIndices.empty()) + return result; + + if (configuration.selectMode != Rewardable::SELECT_FIRST) + { for (auto index : rewardIndices) result.push_back(configuration.info.at(index).reward.getDisplayedComponent(contextHero)); - - if (rewardIndices.size() == 1) + } + else + { configuration.info.at(rewardIndices.front()).reward.loadComponents(result, contextHero); + } return result; } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index cc50fae43..71df92137 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -27,6 +27,7 @@ #include "../TerrainHandler.h" #include "../TextOperations.h" #include "../VCMI_Lib.h" +#include "../constants/StringConstants.h" #include "../filesystem/CBinaryReader.h" #include "../filesystem/Filesystem.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" @@ -1189,11 +1190,53 @@ CGObjectInstance * CMapLoaderH3M::readScholar(const int3 & position, std::shared }; auto * object = readGeneric(position, objectTemplate); - //auto * rewardable = dynamic_cast(object); + auto * rewardable = dynamic_cast(object); + + uint8_t bonusTypeRaw = reader->readUInt8(); + auto bonusType = static_cast(bonusTypeRaw); + auto bonusID = reader->readUInt8(); + + switch (bonusType) + { + case ScholarBonusType::PRIM_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = NPrimarySkill::names[bonusID]; + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 80; + rewardable->configuration.presetVariable("primarySkill", "gainedStat", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SECONDARY_SKILL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->skills()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 50; + rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::SPELL: + { + JsonNode variable; + JsonNode dice; + variable.String() = VLC->spells()->getByIndex(bonusID)->getJsonKey(); + variable.setMeta(ModScope::scopeGame()); + dice.Integer() = 20; + rewardable->configuration.presetVariable("spell", "gainedSpell", variable); + rewardable->configuration.presetVariable("dice", "0", dice); + break; + } + case ScholarBonusType::RANDOM: + break;// No-op + default: + logGlobal->warn("Map '%s': Invalid Scholar settings! Ignoring...", mapName); + } - /*uint8_t bonusTypeRaw =*/ reader->readUInt8(); - /*auto bonusType = static_cast(bonusTypeRaw);*/ - /*auto bonusID =*/ reader->readUInt8(); reader->skipZero(6); return object; } diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 311948fcb..04a217b5d 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -268,7 +268,12 @@ void Rewardable::Info::configureRewards( if (!diceValue.has_value()) { - object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + const JsonNode & preset = object.getPresetVariable("dice", diceID); + if (preset.isNull()) + object.initVariable("dice", diceID, rng.getIntRange(0, 99)()); + else + object.initVariable("dice", diceID, preset.Integer()); + diceValue = object.getVariable("dice", diceID); } assert(diceValue.has_value()); From 8f33fdcd8300d81c022a6fe37ed1a6efbd7251fa Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 18 Oct 2023 17:38:40 +0300 Subject: [PATCH 0867/1248] Allow learning banned spell from Scholars (H3 logic) --- lib/mapObjects/CGHeroInstance.cpp | 4 ++-- lib/mapObjects/CGHeroInstance.h | 2 +- lib/rewardable/Limiter.cpp | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 9e9396678..c362ee48f 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -770,7 +770,7 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const } } -bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const +bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned) const { if(!hasSpellbook()) return false; @@ -793,7 +793,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell) const return false;//creature abilities can not be learned } - if(!IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if(!allowBanned && !IObjectInterface::cb->isAllowed(0, spell->getIndex())) { logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated()); return false;//banned spells should not be learned diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 08335c71e..b0316dd7d 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -172,7 +172,7 @@ public: int getCurrentLuck(int stack=-1, bool town=false) const; int32_t getSpellCost(const spells::Spell * sp) const; //do not use during battles -> bonuses from army would be ignored - bool canLearnSpell(const spells::Spell * spell) const; + bool canLearnSpell(const spells::Spell * spell, bool allowBanned = false) const; bool canCastThisSpell(const spells::Spell * spell) const; //determines if this hero can cast given spell; takes into account existing spell in spellbook, existing spellbook and artifact bonuses /// convert given position between map position (CGObjectInstance::pos) and visitable position used for hero interactions diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index b7c4352fd..d0db694fb 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -127,7 +127,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & spell : canLearnSpells) { - if (!hero->canLearnSpell(spell.toSpell(VLC->spells()))) + if (!hero->canLearnSpell(spell.toSpell(VLC->spells()), true)) return false; } From 901fd77699d2a357c9cc58fd534e1ffdc478c4a0 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 18 Oct 2023 18:04:01 +0300 Subject: [PATCH 0868/1248] Fix Cover of Darkness --- lib/rewardable/Interface.cpp | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 31b5a74e4..18fd6cc9b 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -13,8 +13,10 @@ #include "../CHeroHandler.h" #include "../TerrainHandler.h" +#include "../CPlayerState.h" #include "../CSoundBase.h" #include "../NetPacks.h" +#include "../gameState/CGameState.h" #include "../spells/CSpellHandler.h" #include "../spells/ISpellMechanics.h" #include "../mapObjects/MiscObjects.h" @@ -50,14 +52,6 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R if (info.reward.revealTiles) { const auto & props = *info.reward.revealTiles; - FoWChange fw; - - if (props.hide) - fw.mode = ETileVisibility::HIDDEN; - else - fw.mode = ETileVisibility::REVEALED; - - fw.player = hero->tempOwner; const auto functor = [&props](const TerrainTile * tile) { @@ -77,20 +71,34 @@ void Rewardable::Interface::grantRewardBeforeLevelup(IGameCallback * cb, const R return score > 0; }; + std::unordered_set tiles; if (props.radius > 0) { - cb->getTilesInRange(fw.tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->tempOwner); - vstd::erase_if(fw.tiles, [&](const int3 & coord){ + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::HIDDEN, hero->getOwner()); + if (props.hide) + cb->getTilesInRange(tiles, hero->getSightCenter(), props.radius, ETileVisibility::REVEALED, hero->getOwner()); + + vstd::erase_if(tiles, [&](const int3 & coord){ return !functor(cb->getTile(coord)); }); } else { - cb->getAllTiles(fw.tiles, hero->tempOwner, -1, functor); + cb->getAllTiles(tiles, hero->tempOwner, -1, functor); } - cb->sendAndApply(&fw); - + if (props.hide) + { + for (auto & player : cb->gameState()->players) + { + if (cb->getPlayerStatus(player.first) == EPlayerStatus::INGAME && cb->getPlayerRelations(player.first, hero->getOwner()) == PlayerRelations::ENEMIES) + cb->changeFogOfWar(tiles, player.first, ETileVisibility::HIDDEN); + } + } + else + { + cb->changeFogOfWar(tiles, hero->getOwner(), ETileVisibility::REVEALED); + } } for(const auto & entry : info.reward.secondary) From 646c0db7f36a8f994f12308c2e073c1a2e3ce735 Mon Sep 17 00:00:00 2001 From: Waters Date: Wed, 18 Oct 2023 16:27:25 -0400 Subject: [PATCH 0869/1248] Fixed anchor links `Stack_Queue` was not working on GitHub for me. `stack-queue` does. --- docs/players/Manual.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/players/Manual.md b/docs/players/Manual.md index e5f613c8f..fb7c3d682 100644 --- a/docs/players/Manual.md +++ b/docs/players/Manual.md @@ -24,7 +24,7 @@ A number of enchancements had been introduced thorough new versions of VCMI. In ## High resolutions VCMI supports resolutions higher than original 800x600. -Switching resolution may not only change visible area of map, but also alters some interface features such as [Stack Queue.](#Stack_Queue) +Switching resolution may not only change visible area of map, but also alters some interface features such as [Stack Queue.](#stack-queue) To change resolution or full screen mode use System Options menu when in game. Fullscreen mode can be toggled anytime using F4 hotkey. ## Stack Experience @@ -89,7 +89,7 @@ interface. - Numbers for components in selection window - for example Treasure Chest, skill choice dialog and more yet to come. - Type numbers in the Split Stack screen (for example 25 will split the stacks as such that there are 25 creatures in the second stack). -- 'Q' - Toggles the [Stack Queue](#Stack_Queue) display (so it can be enabled/disabled with single key press). +- 'Q' - Toggles the [Stack Queue](#stack-queue) display (so it can be enabled/disabled with single key press). - During Tactics phase, click on any of your stack to instantly activate it. No need to scroll trough entire army anymore. ## Cheat codes From 0f2922d064ed0931dc63a4a0ff40b4e290fd094a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 13:33:49 +0300 Subject: [PATCH 0870/1248] Only show components if UI enhancements is on --- client/windows/InfoWindows.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 8a298cdc8..a3be7a9b6 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -352,10 +352,13 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, else { std::vector components; - if(LOCPLINT->localState->getCurrentHero()) - components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); - else - components = obj->getPopupComponents(LOCPLINT->playerID); + if (settings["general"]["enableUiEnhancements"].Bool()) + { + if(LOCPLINT->localState->getCurrentHero()) + components = obj->getPopupComponents(LOCPLINT->localState->getCurrentHero()); + else + components = obj->getPopupComponents(LOCPLINT->playerID); + } std::vector> guiComponents; for (auto & component : components) From 530c70c282e619b8b7da3f55062a99a089a5436d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 13:34:15 +0300 Subject: [PATCH 0871/1248] Added object descriptions present in H3 --- config/objects/rewardableBonusing.json | 11 +++++++++++ config/objects/rewardableOncePerHero.json | 12 ++++++++++++ config/objects/rewardableOncePerWeek.json | 2 ++ config/objects/rewardableShrine.json | 9 ++++++--- config/objects/rewardableWitchHut.json | 1 + lib/CGeneralTextHandler.cpp | 1 + lib/rewardable/Info.cpp | 8 ++++++-- 7 files changed, 39 insertions(+), 5 deletions(-) diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index 64f1855c0..b7ee79698 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -23,6 +23,7 @@ "blockedVisitable" : true, "onVisitedMessage" : 22, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -54,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 30, + "description" : "@core.xtrainfo.1", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -87,6 +89,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 50, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -119,6 +122,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 56, + "description" : "@core.xtrainfo.3", "visitMode" : "bonus", "selectMode" : "selectFirst", "resetParameters" : { @@ -172,6 +176,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 58, + "description" : "@core.xtrainfo.0", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -204,6 +209,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 63, + "description" : "@core.xtrainfo.22", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -261,6 +267,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 82, + "description" : "@core.xtrainfo.2", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -292,6 +299,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 95, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -383,6 +391,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 141, + "description" : "@core.xtrainfo.23", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -420,6 +429,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 111, + "description" : "@core.xtrainfo.17", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -455,6 +465,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 167, + "description" : "@core.xtrainfo.13", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index e6bac178a..c582b2f0f 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -55,6 +55,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 40, + "description" : "@core.xtrainfo.7", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -86,6 +87,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 60, + "description" : "@core.xtrainfo.4", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -117,6 +119,7 @@ "onVisitedMessage" : 67, "onEmptyMessage" : 68, + "description" : "@core.xtrainfo.6", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -161,6 +164,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 81, + "description" : "@core.xtrainfo.8", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -192,6 +196,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 101, + "description" : "@core.xtrainfo.11", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ @@ -233,16 +238,19 @@ } ], "onVisitedMessage" : 147, + "description" : "@core.xtrainfo.18", "visitMode" : "hero", "selectMode" : "selectFirst", "canRefuse" : true, "rewards" : [ { + "description" : "@core.arraytxt.202", "message" : 148, "appearChance" : { "max" : 34 }, "gainedLevels" : 1 }, { + "description" : "@core.arraytxt.203", "message" : 149, "appearChance" : { "min" : 34, "max" : 67 }, "limiter" : { "resources" : { "gold" : 2000 } }, @@ -250,6 +258,7 @@ "gainedLevels" : 1 }, { + "description" : "@core.arraytxt.204", "message" : 151, "appearChance" : { "min" : 67 }, "limiter" : { "resources" : { "gems" : 10 } }, @@ -282,6 +291,7 @@ "onSelectMessage" : 71, "onVisitedMessage" : 72, "onEmptyMessage" : 73, + "description" : "@core.xtrainfo.9", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -322,6 +332,7 @@ "onSelectMessage" : 158, "onVisitedMessage" : 159, "onEmptyMessage" : 160, + "description" : "@core.xtrainfo.10", "visitMode" : "hero", "selectMode" : "selectPlayer", "canRefuse" : true, @@ -360,6 +371,7 @@ "compatibilityIdentifiers" : [ "object" ], "onVisitedMessage" : 144, + "description" : "@core.xtrainfo.5", "visitMode" : "hero", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index e83375b8c..110943c32 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -21,6 +21,7 @@ "onEmptyMessage" : 79, "onVisitedMessage" : 78, + "description" : "@core.xtrainfo.25", "visitMode" : "bonus", "selectMode" : "selectFirst", "rewards" : [ @@ -59,6 +60,7 @@ "onEmptyMessage" : 76, "onVisitedMessage" : 75, + "description" : "@core.xtrainfo.15", "resetParameters" : { "period" : 7, "visitors" : true diff --git a/config/objects/rewardableShrine.json b/config/objects/rewardableShrine.json index 5bfc9de26..50a78d5a8 100644 --- a/config/objects/rewardableShrine.json +++ b/config/objects/rewardableShrine.json @@ -20,6 +20,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, + "description" : "@core.xtrainfo.19", "variables" : { "spell" : { @@ -43,7 +44,7 @@ "spells" : [ "@gainedSpell" ], - "description" : 355, + "description" : "@core.genrltxt.355", "message" : [ 127, "%s." ] // You learn new spell } ], @@ -87,6 +88,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, + "description" : "@core.xtrainfo.20", "variables" : { "spell" : { @@ -110,7 +112,7 @@ "spells" : [ "@gainedSpell" ], - "description" : 355, + "description" : "@core.genrltxt.355", "message" : [ 128, "%s." ] // You learn new spell } ], @@ -154,6 +156,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, + "description" : "@core.xtrainfo.21", "variables" : { "spell" : { @@ -177,7 +180,7 @@ "spells" : [ "@gainedSpell" ], - "description" : 355, + "description" : "@core.genrltxt.355", "message" : [ 129, "%s." ] // You learn new spell } ], diff --git a/config/objects/rewardableWitchHut.json b/config/objects/rewardableWitchHut.json index e3db3b726..006aff7dc 100644 --- a/config/objects/rewardableWitchHut.json +++ b/config/objects/rewardableWitchHut.json @@ -20,6 +20,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, + "description" : "@core.xtrainfo.12", "variables" : { "secondarySkill" : { diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index f68d6b3d4..ea0faea4a 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -475,6 +475,7 @@ CGeneralTextHandler::CGeneralTextHandler(): readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); readToVector("core.minename", "DATA/MINENAME.TXT" ); readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); + readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" ); static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 04a217b5d..69ed5dadd 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -49,7 +49,11 @@ namespace { if (value.String().empty()) return ret; - ret.appendTextID(textIdentifier.get()); + if (value.String()[0] == '@') + ret.appendTextID(value.String().substr(1)); + else + ret.appendTextID(textIdentifier.get()); + return ret; } @@ -69,7 +73,7 @@ void Rewardable::Info::init(const JsonNode & objectConfig, const std::string & o objectTextID = objectName; auto loadString = [&](const JsonNode & entry, const TextIdentifier & textID){ - if (entry.isString() && !entry.String().empty()) + if (entry.isString() && !entry.String().empty() && entry.String()[0] != '@') VLC->generaltexth->registerString(entry.meta, textID, entry.String()); }; From e0f6b582f5351f2fb6f4ba0ccabee7284cf57cc7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 14:36:11 +0300 Subject: [PATCH 0872/1248] Display object description only on right-click --- client/windows/InfoWindows.cpp | 4 +- lib/mapObjects/CGObjectInstance.cpp | 9 +++ lib/mapObjects/CGObjectInstance.h | 3 + lib/mapObjects/CRewardableObject.cpp | 101 +++++++++++++-------------- lib/mapObjects/CRewardableObject.h | 8 ++- 5 files changed, 70 insertions(+), 55 deletions(-) diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index a3be7a9b6..320aabef8 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -365,9 +365,9 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, guiComponents.push_back(std::make_shared(component)); if(LOCPLINT->localState->getCurrentHero()) - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->localState->getCurrentHero()), guiComponents); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->localState->getCurrentHero()), guiComponents); else - CRClickPopup::createAndPush(obj->getHoverText(LOCPLINT->playerID), guiComponents); + CRClickPopup::createAndPush(obj->getPopupText(LOCPLINT->playerID), guiComponents); } } diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index e4ed862c9..b03ae97a7 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -271,6 +271,15 @@ std::string CGObjectInstance::getHoverText(const CGHeroInstance * hero) const return getHoverText(hero->tempOwner); } +std::string CGObjectInstance::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} +std::string CGObjectInstance::getPopupText(const CGHeroInstance * hero) const +{ + return getHoverText(hero); +} + std::vector CGObjectInstance::getPopupComponents(PlayerColor player) const { return {}; diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index fb093bbd1..98f6dceba 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -112,6 +112,9 @@ public: /// Returns hero-specific hover name, including visited/not visited info. Default = player-specific name virtual std::string getHoverText(const CGHeroInstance * hero) const; + virtual std::string getPopupText(PlayerColor player) const; + virtual std::string getPopupText(const CGHeroInstance * hero) const; + virtual std::vector getPopupComponents(PlayerColor player) const; virtual std::vector getPopupComponents(const CGHeroInstance * hero) const; diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index da74db7f3..0599f689c 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -244,57 +244,61 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const } } -std::string CRewardableObject::getHoverText(PlayerColor player) const +std::string CRewardableObject::getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const { std::string result = getObjectName(); - if (!getDescriptionMessage(player).empty()) - result += "\n" + getDescriptionMessage(player); + if (includeDescription && !getDescriptionMessage(player, hero).empty()) + result += "\n" + getDescriptionMessage(player, hero); - if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) + if (hero) { - if (wasVisited(player)) - result += "\n\n" + configuration.visitedTooltip.toString(); - else - result += "\n\n" + configuration.notVisitedTooltip.toString(); + if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) + { + if (wasVisited(hero)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n " + configuration.notVisitedTooltip.toString(); + } + } + else + { + if(configuration.visitMode == Rewardable::VISIT_PLAYER || configuration.visitMode == Rewardable::VISIT_ONCE) + { + if (wasVisited(player)) + result += "\n" + configuration.visitedTooltip.toString(); + else + result += "\n" + configuration.notVisitedTooltip.toString(); + } } return result; } +std::string CRewardableObject::getHoverText(PlayerColor player) const +{ + return getDisplayTextImpl(player, nullptr, false); +} + std::string CRewardableObject::getHoverText(const CGHeroInstance * hero) const { - std::string result = getObjectName(); - - if (!getDescriptionMessage(hero).empty()) - result += "\n" + getDescriptionMessage(hero); - - if(configuration.visitMode != Rewardable::VISIT_UNLIMITED) - { - if (wasVisited(hero)) - result += "\n\n" + configuration.visitedTooltip.toString(); - else - result += "\n\n" + configuration.notVisitedTooltip.toString(); - } - return result; + return getDisplayTextImpl(hero->getOwner(), hero, false); } -std::string CRewardableObject::getDescriptionMessage(PlayerColor player) const +std::string CRewardableObject::getPopupText(PlayerColor player) const +{ + return getDisplayTextImpl(player, nullptr, true); +} + +std::string CRewardableObject::getPopupText(const CGHeroInstance * hero) const +{ + return getDisplayTextImpl(hero->getOwner(), hero, true); +} + +std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const { if (!wasScouted(player) || configuration.info.empty()) return configuration.description.toString(); - auto rewardIndices = getAvailableRewards(nullptr, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty()) - return configuration.info[0].description.toString(); - - return configuration.info[rewardIndices.front()].description.toString(); -} - -std::string CRewardableObject::getDescriptionMessage(const CGHeroInstance * hero) const -{ - if (!wasScouted(hero->getOwner()) || configuration.info.empty()) - return configuration.description.toString(); - auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); if (rewardIndices.empty()) return configuration.info[0].description.toString(); @@ -302,26 +306,11 @@ std::string CRewardableObject::getDescriptionMessage(const CGHeroInstance * hero return configuration.info[rewardIndices.front()].description.toString(); } -std::vector CRewardableObject::getPopupComponents(PlayerColor player) const +std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const { if (!wasScouted(player)) return {}; - auto rewardIndices = getAvailableRewards(nullptr, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info.empty()) - rewardIndices.push_back(0); - - if (rewardIndices.empty()) - return {}; - - return loadComponents(nullptr, rewardIndices); -} - -std::vector CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const -{ - if (!wasScouted(hero->getOwner())) - return {}; - auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); if (rewardIndices.empty() && !configuration.info.empty()) rewardIndices.push_back(0); @@ -329,7 +318,17 @@ std::vector CRewardableObject::getPopupComponents(const CGHeroInstanc if (rewardIndices.empty()) return {}; - return loadComponents(nullptr, rewardIndices); + return loadComponents(hero, rewardIndices); +} + +std::vector CRewardableObject::getPopupComponents(PlayerColor player) const +{ + return getPopupComponentsImpl(player, nullptr); +} + +std::vector CRewardableObject::getPopupComponents(const CGHeroInstance * hero) const +{ + return getPopupComponentsImpl(hero->getOwner(), hero); } void CRewardableObject::setPropertyDer(ui8 what, ui32 val) diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 45134a125..3d9484dca 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -39,6 +39,10 @@ protected: std::vector loadComponents(const CGHeroInstance * contextHero, const std::vector & rewardIndices) const; + std::string getDisplayTextImpl(PlayerColor player, const CGHeroInstance * hero, bool includeDescription) const; + std::string getDescriptionMessage(PlayerColor player, const CGHeroInstance * hero) const; + std::vector getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const; + public: /// Visitability checks. Note that hero check includes check for hero owner (returns true if object was visited by player) bool wasVisited(PlayerColor player) const override; @@ -68,8 +72,8 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; - std::string getDescriptionMessage(PlayerColor player) const; - std::string getDescriptionMessage(const CGHeroInstance * hero) const; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; std::vector getPopupComponents(PlayerColor player) const override; std::vector getPopupComponents(const CGHeroInstance * hero) const override; From 8f4791914ed567717a14ce0a32104fcefe8a935b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 15:17:58 +0300 Subject: [PATCH 0873/1248] Show content preview only for Witch Huts / Shrines / Tree --- config/objects/rewardableOncePerHero.json | 2 ++ config/objects/rewardableShrine.json | 3 +++ config/objects/rewardableWitchHut.json | 1 + lib/mapObjects/CRewardableObject.cpp | 10 ++++++++-- lib/rewardable/Configuration.cpp | 1 + lib/rewardable/Configuration.h | 4 ++++ lib/rewardable/Info.cpp | 1 + 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index c582b2f0f..43065ed8d 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -242,6 +242,8 @@ "visitMode" : "hero", "selectMode" : "selectFirst", "canRefuse" : true, + "showScoutedPreview" : true, + "rewards" : [ { "description" : "@core.arraytxt.202", diff --git a/config/objects/rewardableShrine.json b/config/objects/rewardableShrine.json index 50a78d5a8..44bd2a952 100644 --- a/config/objects/rewardableShrine.json +++ b/config/objects/rewardableShrine.json @@ -21,6 +21,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, "description" : "@core.xtrainfo.19", + "showScoutedPreview" : true, "variables" : { "spell" : { @@ -89,6 +90,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, "description" : "@core.xtrainfo.20", + "showScoutedPreview" : true, "variables" : { "spell" : { @@ -157,6 +159,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, "description" : "@core.xtrainfo.21", + "showScoutedPreview" : true, "variables" : { "spell" : { diff --git a/config/objects/rewardableWitchHut.json b/config/objects/rewardableWitchHut.json index 006aff7dc..ad5d036df 100644 --- a/config/objects/rewardableWitchHut.json +++ b/config/objects/rewardableWitchHut.json @@ -21,6 +21,7 @@ "visitMode" : "limiter", "visitedTooltip" : 354, "description" : "@core.xtrainfo.12", + "showScoutedPreview" : true, "variables" : { "secondarySkill" : { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 0599f689c..75437c925 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -58,6 +58,9 @@ std::vector CRewardableObject::loadComponents(const CGHeroInstance * if (rewardIndices.empty()) return result; + if (!configuration.showScoutedPreview) + return result; + if (configuration.selectMode != Rewardable::SELECT_FIRST) { for (auto index : rewardIndices) @@ -300,10 +303,13 @@ std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const C return configuration.description.toString(); auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty()) + if (rewardIndices.empty() && !configuration.info[0].description.empty()) return configuration.info[0].description.toString(); - return configuration.info[rewardIndices.front()].description.toString(); + if (!configuration.info[rewardIndices.front()].description.empty()) + return configuration.info[rewardIndices.front()].description.toString(); + + return configuration.description.toString(); } std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor player, const CGHeroInstance * hero) const diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 427c01401..18788473f 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -84,6 +84,7 @@ void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) handler.serializeEnum("visitMode", visitMode, std::vector{VisitModeString.begin(), VisitModeString.end()}); handler.serializeStruct("resetParameters", resetParameters); handler.serializeBool("canRefuse", canRefuse); + handler.serializeBool("showScoutedPreview", showScoutedPreview); handler.serializeInt("infoWindowType", infoWindowType); } diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index 2fb901651..c31290506 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -157,6 +157,9 @@ struct DLL_LINKAGE Configuration /// if true - player can refuse visiting an object (e.g. Tomb) bool canRefuse = false; + /// if true - right-clicking object will show preview of object rewards + bool showScoutedPreview = false; + /// if true - object info will shown in infobox (like resource pickup) EInfoWindowMode infoWindowType = EInfoWindowMode::AUTO; @@ -183,6 +186,7 @@ struct DLL_LINKAGE Configuration h & variables; h & visitLimiter; h & canRefuse; + h & showScoutedPreview; h & infoWindowType; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 69ed5dadd..61d12282f 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -396,6 +396,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand configureResetInfo(object, rng, object.resetParameters, parameters["resetParameters"]); object.canRefuse = parameters["canRefuse"].Bool(); + object.showScoutedPreview = parameters["showScoutedPreview"].Bool(); if(parameters["showInInfobox"].isNull()) object.infoWindowType = EInfoWindowMode::AUTO; From 01920bb74e9439ae4a7aef720649ec4c589b6686 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 15:41:41 +0300 Subject: [PATCH 0874/1248] Fix treasure chests --- config/objects/rewardablePickable.json | 6 +++--- lib/mapObjects/CRewardableObject.cpp | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index b5756a528..6b01cf3d6 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -235,7 +235,7 @@ }, { "appearChance" : { "max" : 33 }, - "gainedExp" : 1500, + "heroExperience" : 1500, "removeObject" : true, }, { @@ -245,7 +245,7 @@ }, { "appearChance" : { "min" : 33, "max" : 65 }, - "gainedExp" : 1000, + "heroExperience" : 1000, "removeObject" : true, }, { @@ -255,7 +255,7 @@ }, { "appearChance" : { "min" : 65, "max" : 95 }, - "gainedExp" : 500, + "heroExperience" : 500, "removeObject" : true, }, { diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 75437c925..f2e1a6953 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -58,9 +58,6 @@ std::vector CRewardableObject::loadComponents(const CGHeroInstance * if (rewardIndices.empty()) return result; - if (!configuration.showScoutedPreview) - return result; - if (configuration.selectMode != Rewardable::SELECT_FIRST) { for (auto index : rewardIndices) @@ -317,6 +314,9 @@ std::vector CRewardableObject::getPopupComponentsImpl(PlayerColor pla if (!wasScouted(player)) return {}; + if (!configuration.showScoutedPreview) + return {}; + auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); if (rewardIndices.empty() && !configuration.info.empty()) rewardIndices.push_back(0); From 4c0eabf20cc54170f466197912c742f1c538e3af Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 19 Oct 2023 15:41:59 +0300 Subject: [PATCH 0875/1248] Show guards preview for visited banks on right click --- lib/mapObjects/CBank.cpp | 59 ++++++++++++++++++----------- lib/mapObjects/CBank.h | 2 + lib/mapObjects/CGObjectInstance.cpp | 2 +- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index b2c1d3261..7ff668945 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -18,6 +18,7 @@ #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../GameSettings.h" +#include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" #include "../IGameCallback.h" @@ -51,8 +52,37 @@ bool CBank::isCoastVisitable() const std::string CBank::getHoverText(PlayerColor player) const { - // TODO: record visited players - return getObjectName() + " " + visitedTxt(bc == nullptr); + if (!wasVisited(player)) + return getObjectName(); + + return getObjectName() + "\n" + visitedTxt(bc == nullptr); +} + +std::vector CBank::getPopupComponents(PlayerColor player) const +{ + if (!wasVisited(player)) + return {}; + + if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) + return {}; + + std::map guardsAmounts; + std::vector result; + + for (auto const & slot : Slots()) + if (slot.second) + guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); + + for (auto const & guard : guardsAmounts) + { + Component comp; + comp.id = Component::EComponentType::CREATURE; + comp.subtype = guard.first.getNum(); + comp.val = guard.second; + + result.push_back(comp); + } + return result; } void CBank::setConfig(const BankConfig & config) @@ -98,11 +128,14 @@ void CBank::newTurn(CRandomGenerator & rand) const bool CBank::wasVisited (PlayerColor player) const { - return !bc; //FIXME: player A should not know about visit done by player B + return vstd::contains(cb->getPlayerState(player)->visitedObjects, ObjectInstanceID(id)); } void CBank::onHeroVisit(const CGHeroInstance * h) const { + ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD_TEAM, id, h->id); + cb->sendAndApply(&cov); + int banktext = 0; switch (ID) { @@ -130,28 +163,10 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const bd.player = h->getOwner(); bd.soundID = soundBase::invalid; // Sound is handled in json files, else two sounds are played bd.text.appendLocalString(EMetaText::ADVOB_TXT, banktext); + bd.components = getPopupComponents(h->getOwner()); if (banktext == 32) bd.text.replaceRawString(getObjectName()); - if (VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) - { - std::map guardsAmounts; - - for (auto const & slot : Slots()) - if (slot.second) - guardsAmounts[slot.second->getCreatureID()] += slot.second->getCount(); - - for (auto const & guard : guardsAmounts) - { - Component comp; - comp.id = Component::EComponentType::CREATURE; - comp.subtype = guard.first.getNum(); - comp.val = guard.second; - - bd.components.push_back(comp); - } - } - cb->showBlockingDialog(&bd); } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 7d62f30a2..8f7105689 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -41,6 +41,8 @@ public: void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + std::vector getPopupComponents(PlayerColor player) const override; + template void serialize(Handler &h, const int version) { h & static_cast(*this); diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index b03ae97a7..e162e64c1 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -287,7 +287,7 @@ std::vector CGObjectInstance::getPopupComponents(PlayerColor player) std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance * hero) const { - return {}; + return getPopupComponents(hero->getOwner()); } void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const From a1a5bc28c206d945f74dd354b483d1501dea32c2 Mon Sep 17 00:00:00 2001 From: Johannes Schauer Marin Rodrigues Date: Thu, 19 Oct 2023 16:19:09 +0200 Subject: [PATCH 0876/1248] convert line endings from CRLF (Windows) to LF (Linux/Unix) Mixed line endings cause problems when exporting patches with git-format-patch and then trying to "git am" a patch with mixed and non-matching line endings. In such a situation git will fail to apply the patch. This commit runs the dos2unix tools on the remaining files with CRLF (\r\n) line endings to convert them to line-feeds (\n) only. Files that are Windows specific like *.vcxproj and *.props files were not converted. Closes: #3073 --- AI/BattleAI/BattleAI.cpp | 584 +- AI/BattleAI/StdInc.cpp | 22 +- AI/BattleAI/StdInc.h | 34 +- AI/BattleAI/main.cpp | 66 +- AI/EmptyAI/CEmptyAI.cpp | 164 +- AI/EmptyAI/CEmptyAI.h | 76 +- AI/EmptyAI/StdInc.cpp | 2 +- AI/EmptyAI/StdInc.h | 18 +- AI/EmptyAI/main.cpp | 56 +- AI/GeniusAI.brain | 1472 ++--- AI/StupidAI/StdInc.cpp | 2 +- AI/StupidAI/StdInc.h | 18 +- AI/StupidAI/StupidAI.cpp | 666 +- AI/StupidAI/StupidAI.h | 112 +- AI/StupidAI/main.cpp | 68 +- AI/VCAI/AIUtility.cpp | 518 +- AI/VCAI/VCAI.cpp | 5824 ++++++++--------- AI/VCAI/VCAI.h | 814 +-- AUTHORS.h | 106 +- CCallback.cpp | 846 +-- CCallback.h | 394 +- ChangeLog.md | 4320 ++++++------ Global.h | 1358 ++-- Mods/vcmi/Data/s/std.verm | 78 +- Mods/vcmi/Data/s/testy.erm | 26 +- Mods/vcmi/config/vcmi/german.json | 738 +-- .../config/vcmi/rmg/hdmod/blockbuster.JSON | 744 +-- Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON | 724 +- Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON | 620 +- .../vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON | 166 +- Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON | 280 +- .../config/vcmi/rmg/hdmod/roadrunner.JSON | 278 +- .../vcmi/config/vcmi/rmg/hdmod/superslam.JSON | 222 +- .../config/vcmi/rmg/heroes3/readyOrNot.JSON | 292 +- .../config/vcmi/rmg/heroes3/worldsAtWar.JSON | 464 +- .../vcmi/rmg/heroes3unused/gauntlet.JSON | 722 +- client/CGameInfo.cpp | 230 +- client/CGameInfo.h | 202 +- client/CMT.cpp | 1058 +-- client/CMusicHandler.cpp | 1436 ++-- client/CMusicHandler.h | 334 +- client/CPlayerInterface.cpp | 3782 +++++------ client/CPlayerInterface.h | 468 +- client/CVideoHandler.cpp | 1330 ++-- client/CVideoHandler.h | 242 +- client/Client.cpp | 1480 ++--- client/Client.h | 480 +- client/NetPacksClient.cpp | 2084 +++--- client/PlayerLocalState.cpp | 542 +- client/PlayerLocalState.h | 222 +- client/StdInc.cpp | 2 +- client/StdInc.h | 18 +- client/adventureMap/AdventureMapInterface.cpp | 1778 ++--- client/adventureMap/AdventureMapInterface.h | 382 +- client/adventureMap/AdventureMapWidget.cpp | 910 +-- client/adventureMap/AdventureMapWidget.h | 220 +- client/adventureMap/AdventureOptions.cpp | 122 +- client/adventureMap/AdventureOptions.h | 62 +- client/adventureMap/CResDataBar.cpp | 180 +- client/adventureMap/CResDataBar.h | 80 +- client/adventureMap/MapAudioPlayer.cpp | 504 +- client/adventureMap/MapAudioPlayer.h | 154 +- client/battle/BattleActionsController.cpp | 2108 +++--- client/battle/BattleActionsController.h | 250 +- client/battle/BattleAnimationClasses.cpp | 2274 +++---- client/battle/BattleAnimationClasses.h | 744 +-- client/battle/BattleConstants.h | 200 +- client/battle/BattleEffectsController.cpp | 320 +- client/battle/BattleEffectsController.h | 150 +- client/battle/BattleFieldController.cpp | 1838 +++--- client/battle/BattleFieldController.h | 288 +- client/battle/BattleInterface.cpp | 1656 ++--- client/battle/BattleInterface.h | 460 +- client/battle/BattleInterfaceClasses.cpp | 1842 +++--- client/battle/BattleInterfaceClasses.h | 476 +- client/battle/BattleObstacleController.cpp | 456 +- client/battle/BattleObstacleController.h | 136 +- client/battle/BattleProjectileController.cpp | 772 +-- client/battle/BattleProjectileController.h | 250 +- client/battle/BattleRenderer.cpp | 152 +- client/battle/BattleRenderer.h | 106 +- client/battle/BattleSiegeController.cpp | 734 +-- client/battle/BattleSiegeController.h | 222 +- client/battle/BattleStacksController.cpp | 1788 ++--- client/battle/BattleStacksController.h | 294 +- client/battle/BattleWindow.cpp | 1374 ++-- client/battle/BattleWindow.h | 236 +- client/battle/CreatureAnimation.cpp | 866 +-- client/battle/CreatureAnimation.h | 316 +- client/gui/CGuiHandler.cpp | 496 +- client/gui/CGuiHandler.h | 260 +- client/gui/CIntObject.cpp | 692 +- client/gui/CIntObject.h | 382 +- client/gui/CursorHandler.cpp | 584 +- client/gui/CursorHandler.h | 364 +- client/gui/MouseButton.h | 38 +- client/gui/TextAlignment.h | 24 +- client/mapView/IMapRendererContext.h | 186 +- client/mapView/IMapRendererObserver.h | 106 +- client/mapView/MapRenderer.cpp | 1578 ++--- client/mapView/MapRenderer.h | 326 +- client/mapView/MapRendererContext.cpp | 1060 +-- client/mapView/MapRendererContext.h | 332 +- client/mapView/MapRendererContextState.cpp | 186 +- client/mapView/MapRendererContextState.h | 124 +- client/mapView/MapView.cpp | 430 +- client/mapView/MapView.h | 202 +- client/mapView/MapViewActions.cpp | 298 +- client/mapView/MapViewActions.h | 96 +- client/mapView/MapViewCache.cpp | 394 +- client/mapView/MapViewCache.h | 156 +- client/mapView/MapViewController.cpp | 1278 ++-- client/mapView/MapViewController.h | 212 +- client/mapView/MapViewModel.cpp | 258 +- client/mapView/MapViewModel.h | 126 +- client/mapView/mapHandler.cpp | 472 +- client/mapView/mapHandler.h | 152 +- client/render/CAnimation.cpp | 734 +-- client/render/CAnimation.h | 190 +- client/render/CBitmapHandler.cpp | 420 +- client/render/CBitmapHandler.h | 40 +- client/render/CDefFile.cpp | 676 +- client/render/CDefFile.h | 104 +- client/render/Canvas.cpp | 376 +- client/render/Canvas.h | 208 +- client/render/ColorFilter.cpp | 322 +- client/render/ColorFilter.h | 130 +- client/render/Colors.h | 110 +- client/render/Graphics.cpp | 588 +- client/render/Graphics.h | 154 +- client/render/ICursor.h | 56 +- client/render/IImage.h | 176 +- client/render/IImageLoader.h | 68 +- client/renderSDL/CursorHardware.cpp | 154 +- client/renderSDL/CursorHardware.h | 72 +- client/renderSDL/CursorSoftware.cpp | 204 +- client/renderSDL/CursorSoftware.h | 88 +- client/renderSDL/SDLImage.cpp | 712 +- client/renderSDL/SDLImage.h | 166 +- client/renderSDL/SDLImageLoader.cpp | 148 +- client/renderSDL/SDLImageLoader.h | 64 +- client/renderSDL/SDL_Extensions.cpp | 1738 ++--- client/renderSDL/SDL_Extensions.h | 264 +- client/resource.h | 32 +- client/windows/CCastleInterface.cpp | 3830 +++++------ client/windows/CCastleInterface.h | 818 +-- client/windows/CCreatureWindow.cpp | 1966 +++--- client/windows/CCreatureWindow.h | 412 +- client/windows/CHeroWindow.cpp | 726 +- client/windows/CHeroWindow.h | 230 +- client/windows/CKingdomInterface.h | 722 +- client/windows/CMessage.cpp | 1098 ++-- client/windows/CMessage.h | 100 +- client/windows/CQuestLog.cpp | 650 +- client/windows/CQuestLog.h | 222 +- client/windows/CSpellWindow.cpp | 1370 ++-- client/windows/CSpellWindow.h | 256 +- client/windows/GUIClasses.cpp | 3510 +++++----- client/windows/GUIClasses.h | 1016 +-- config/artifacts.json | 4616 ++++++------- config/bonuses.json | 1144 ++-- config/campaignMedia.json | 478 +- config/commanders.json | 82 +- config/difficulty.json | 140 +- config/obstacles.json | 2398 +++---- config/schemas/skill.json | 196 +- config/schemas/spell.json | 636 +- docs/Readme.md | 162 +- launcher/StdInc.cpp | 2 +- launcher/StdInc.h | 62 +- launcher/main.cpp | 192 +- launcher/main.h | 38 +- lib/AI_Base.h | 28 +- lib/CArtHandler.cpp | 2410 +++---- lib/CArtHandler.h | 628 +- lib/CBonusTypeHandler.cpp | 498 +- lib/CBonusTypeHandler.h | 150 +- lib/CBuildingHandler.cpp | 170 +- lib/CBuildingHandler.h | 44 +- lib/CConfigHandler.cpp | 390 +- lib/CConfigHandler.h | 242 +- lib/CConsoleHandler.cpp | 592 +- lib/CConsoleHandler.h | 196 +- lib/CCreatureHandler.cpp | 2772 ++++---- lib/CCreatureHandler.h | 640 +- lib/CCreatureSet.cpp | 2156 +++--- lib/CCreatureSet.h | 606 +- lib/CGameInterface.cpp | 544 +- lib/CGameInterface.h | 338 +- lib/CGeneralTextHandler.cpp | 1410 ++-- lib/CGeneralTextHandler.h | 576 +- lib/CHeroHandler.cpp | 1560 ++--- lib/CHeroHandler.h | 566 +- lib/CScriptingModule.h | 108 +- lib/CSkillHandler.h | 258 +- lib/CSoundBase.h | 2102 +++--- lib/CStack.cpp | 812 +-- lib/CStack.h | 300 +- lib/CStopWatch.h | 136 +- lib/CThreadHelper.cpp | 202 +- lib/CThreadHelper.h | 174 +- lib/CTownHandler.cpp | 2532 +++---- lib/CTownHandler.h | 916 +-- lib/Color.h | 124 +- lib/CondSh.h | 142 +- lib/ConstTransitivePtr.h | 160 +- lib/GameConstants.h | 44 +- lib/GameSettings.cpp | 256 +- lib/GameSettings.h | 208 +- lib/IBonusTypeHandler.h | 60 +- lib/IGameCallback.cpp | 590 +- lib/IGameCallback.h | 378 +- lib/IGameEventsReceiver.h | 286 +- lib/JsonDetail.cpp | 2536 +++---- lib/JsonNode.cpp | 3008 ++++----- lib/JsonNode.h | 742 +-- lib/Languages.h | 224 +- lib/NetPacks.h | 5562 ++++++++-------- lib/NetPacksBase.h | 572 +- lib/NetPacksLib.cpp | 5226 +++++++-------- lib/Point.h | 264 +- lib/Rect.cpp | 294 +- lib/Rect.h | 350 +- lib/ResourceSet.cpp | 338 +- lib/ResourceSet.h | 454 +- lib/StartInfo.h | 416 +- lib/StdInc.cpp | 2 +- lib/StdInc.h | 14 +- lib/TextOperations.cpp | 426 +- lib/TextOperations.h | 166 +- lib/UnlockGuard.h | 240 +- lib/VCMI_Lib.cpp | 650 +- lib/VCMI_Lib.h | 352 +- lib/battle/AccessibilityInfo.cpp | 106 +- lib/battle/AccessibilityInfo.h | 90 +- lib/battle/BattleAction.cpp | 488 +- lib/battle/BattleAction.h | 170 +- lib/battle/BattleAttackInfo.cpp | 68 +- lib/battle/BattleAttackInfo.h | 82 +- lib/battle/BattleHex.cpp | 490 +- lib/battle/BattleHex.h | 252 +- lib/battle/BattleInfo.cpp | 2108 +++--- lib/battle/BattleInfo.h | 360 +- lib/battle/CBattleInfoCallback.cpp | 3760 +++++------ lib/battle/CBattleInfoCallback.h | 312 +- lib/battle/CBattleInfoEssentials.cpp | 882 +-- lib/battle/CBattleInfoEssentials.h | 240 +- lib/battle/CObstacleInstance.cpp | 538 +- lib/battle/CObstacleInstance.h | 278 +- lib/battle/CPlayerBattleCallback.cpp | 166 +- lib/battle/CPlayerBattleCallback.h | 84 +- lib/battle/ReachabilityInfo.cpp | 150 +- lib/battle/ReachabilityInfo.h | 120 +- lib/battle/SideInBattle.cpp | 74 +- lib/battle/SideInBattle.h | 84 +- lib/battle/SiegeInfo.cpp | 90 +- lib/battle/SiegeInfo.h | 66 +- lib/bonuses/Bonus.cpp | 604 +- lib/bonuses/Bonus.h | 382 +- lib/campaign/CampaignHandler.cpp | 1244 ++-- lib/campaign/CampaignHandler.h | 92 +- lib/constants/EntityIdentifiers.cpp | 652 +- lib/constants/EntityIdentifiers.h | 1918 +++--- lib/constants/Enumerations.h | 506 +- lib/constants/NumericConstants.h | 110 +- lib/gameState/CGameState.h | 470 +- lib/int3.h | 434 +- lib/mapping/CMap.cpp | 1408 ++-- lib/mapping/CMap.h | 410 +- lib/mapping/CMapDefines.h | 262 +- lib/mapping/CMapInfo.cpp | 416 +- lib/mapping/CMapInfo.h | 148 +- lib/mapping/MapFeaturesH3M.cpp | 316 +- lib/mapping/MapFeaturesH3M.h | 146 +- lib/mapping/MapFormatH3M.cpp | 4650 ++++++------- lib/mapping/MapFormatH3M.h | 508 +- lib/mapping/MapFormatJson.cpp | 2960 ++++----- lib/mapping/MapFormatJson.h | 584 +- lib/mapping/MapReaderH3M.cpp | 886 +-- lib/mapping/MapReaderH3M.h | 200 +- lib/modding/CModHandler.cpp | 1114 ++-- lib/modding/CModHandler.h | 220 +- lib/registerTypes/RegisterTypes.cpp | 104 +- lib/registerTypes/RegisterTypes.h | 860 +-- lib/registerTypes/TypesClientPacks1.cpp | 68 +- lib/registerTypes/TypesClientPacks2.cpp | 76 +- lib/registerTypes/TypesMapObjects1.cpp | 70 +- lib/registerTypes/TypesMapObjects2.cpp | 74 +- lib/registerTypes/TypesServerPacks.cpp | 66 +- lib/vcmi_endian.h | 162 +- license.txt | 558 +- mapeditor/StdInc.cpp | 2 +- mapeditor/StdInc.h | 110 +- scripting/erm/ERMInterpreter.cpp | 3548 +++++----- scripting/erm/ERMInterpreter.h | 636 +- scripting/erm/ERMParser.cpp | 1058 +-- scripting/erm/ERMParser.h | 556 +- scripting/erm/ERMScriptModule.cpp | 136 +- scripting/erm/ERMScriptModule.h | 50 +- scripting/erm/StdInc.cpp | 2 +- scripting/erm/StdInc.h | 18 +- server/CGameHandler.h | 596 +- server/NetPacksServer.cpp | 688 +- server/StdInc.cpp | 2 +- server/StdInc.h | 28 +- server/queries/CQuery.cpp | 392 +- server/queries/CQuery.h | 182 +- test/StdInc.cpp | 24 +- test/StdInc.h | 26 +- 309 files changed, 98656 insertions(+), 98656 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 73a133467..45e1d626e 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -1,292 +1,292 @@ -/* - * BattleAI.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 "BattleAI.h" -#include "BattleEvaluator.h" -#include "BattleExchangeVariant.h" - -#include "StackWithBonuses.h" -#include "EnemyInfo.h" -#include "tbb/parallel_for.h" -#include "../../lib/CStopWatch.h" -#include "../../lib/CThreadHelper.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/battle/BattleStateInfoForRetreat.h" -#include "../../lib/battle/CObstacleInstance.h" -#include "../../lib/CStack.h" // TODO: remove - // Eventually only IBattleInfoCallback and battle::Unit should be used, - // CUnitState should be private and CStack should be removed completely - -#define LOGL(text) print(text) -#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) - -CBattleAI::CBattleAI() - : side(-1), - wasWaitingForRealize(false), - wasUnlockingGs(false) -{ -} - -CBattleAI::~CBattleAI() -{ - if(cb) - { - //Restore previous state of CB - it may be shared with the main AI (like VCAI) - cb->waitTillRealize = wasWaitingForRealize; - cb->unlockGsWhenWaiting = wasUnlockingGs; - } -} - -void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - setCbc(CB); - env = ENV; - cb = CB; - playerID = *CB->getPlayerID(); - wasWaitingForRealize = CB->waitTillRealize; - wasUnlockingGs = CB->unlockGsWhenWaiting; - CB->waitTillRealize = false; - CB->unlockGsWhenWaiting = false; - movesSkippedByDefense = 0; -} - -void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) -{ - initBattleInterface(ENV, CB); - autobattlePreferences = autocombatPreferences; -} - -BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack) -{ - auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); - std::map woundHpToStack; - for(const auto * stack : healingTargets) - { - if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) - woundHpToStack[woundHp] = stack; - } - - if(woundHpToStack.empty()) - return BattleAction::makeDefend(stack); - else - return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack -} - -void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance) -{ - cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); -} - -static float getStrengthRatio(std::shared_ptr cb, int side) -{ - auto stacks = cb->battleGetAllStacks(); - auto our = 0, enemy = 0; - - for(auto stack : stacks) - { - auto creature = stack->creatureId().toCreature(); - - if(!creature) - continue; - - if(stack->unitSide() == side) - our += stack->getCount() * creature->getAIValue(); - else - enemy += stack->getCount() * creature->getAIValue(); - } - - return enemy == 0 ? 1.0f : static_cast(our) / enemy; -} - -void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) -{ - LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); - - auto timeElapsed = [](std::chrono::time_point start) -> uint64_t - { - auto end = std::chrono::high_resolution_clock::now(); - - return std::chrono::duration_cast(end - start).count(); - }; - - BattleAction result = BattleAction::makeDefend(stack); - setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) - - auto start = std::chrono::high_resolution_clock::now(); - - try - { - if(stack->creatureId() == CreatureID::CATAPULT) - { - cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack)); - return; - } - if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) - { - cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack)); - return; - } - -#if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Build evaluator and targets"); -#endif - - BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); - - result = evaluator.selectStackAction(stack); - - if(!skipCastUntilNextBattle && evaluator.canCastSpell()) - { - auto spelCasted = evaluator.attemptCastingSpell(stack); - - if(spelCasted) - return; - - skipCastUntilNextBattle = true; - } - - logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); - - if(auto action = considerFleeingOrSurrendering(battleID)) - { - cb->battleMakeUnitAction(battleID, *action); - return; - } - } - catch(boost::thread_interrupted &) - { - throw; - } - catch(std::exception &e) - { - logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what()); - } - - if(result.actionType == EActionType::DEFEND) - { - movesSkippedByDefense++; - } - else if(result.actionType != EActionType::WAIT) - { - movesSkippedByDefense = 0; - } - - logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); - - cb->battleMakeUnitAction(battleID, result); -} - -BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack) -{ - BattleAction attack; - BattleHex targetHex = BattleHex::INVALID; - - if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED) - { - targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE); - } - else - { - EWallPart wallParts[] = { - EWallPart::KEEP, - EWallPart::BOTTOM_TOWER, - EWallPart::UPPER_TOWER, - EWallPart::BELOW_GATE, - EWallPart::OVER_GATE, - EWallPart::BOTTOM_WALL, - EWallPart::UPPER_WALL - }; - - for(auto wallPart : wallParts) - { - auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); - - if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) - { - targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); - break; - } - } - } - - if(!targetHex.isValid()) - { - return BattleAction::makeDefend(stack); - } - - attack.aimToHex(targetHex); - attack.actionType = EActionType::CATAPULT; - attack.side = side; - attack.stackNumber = stack->unitId(); - - movesSkippedByDefense = 0; - - return attack; -} - -void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) -{ - LOG_TRACE(logAi); - side = Side; - - skipCastUntilNextBattle = false; -} - -void CBattleAI::print(const std::string &text) const -{ - logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); -} - -std::optional CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID) -{ - BattleStateInfoForRetreat bs; - - bs.canFlee = cb->getBattle(battleID)->battleCanFlee(); - bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID); - bs.ourSide = cb->getBattle(battleID)->battleGetMySide(); - bs.ourHero = cb->getBattle(battleID)->battleGetMyHero(); - bs.enemyHero = nullptr; - - for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false)) - { - if(stack->alive()) - { - if(stack->unitSide() == bs.ourSide) - bs.ourStacks.push_back(stack); - else - { - bs.enemyStacks.push_back(stack); - bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack); - } - } - } - - bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); - - if(!bs.canFlee && !bs.canSurrender) - { - return std::nullopt; - } - - auto result = cb->makeSurrenderRetreatDecision(battleID, bs); - - if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) - { - return BattleAction::makeRetreat(bs.ourSide); - } - - return result; -} - - - +/* + * BattleAI.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 "BattleAI.h" +#include "BattleEvaluator.h" +#include "BattleExchangeVariant.h" + +#include "StackWithBonuses.h" +#include "EnemyInfo.h" +#include "tbb/parallel_for.h" +#include "../../lib/CStopWatch.h" +#include "../../lib/CThreadHelper.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleStateInfoForRetreat.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/CStack.h" // TODO: remove + // Eventually only IBattleInfoCallback and battle::Unit should be used, + // CUnitState should be private and CStack should be removed completely + +#define LOGL(text) print(text) +#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) + +CBattleAI::CBattleAI() + : side(-1), + wasWaitingForRealize(false), + wasUnlockingGs(false) +{ +} + +CBattleAI::~CBattleAI() +{ + if(cb) + { + //Restore previous state of CB - it may be shared with the main AI (like VCAI) + cb->waitTillRealize = wasWaitingForRealize; + cb->unlockGsWhenWaiting = wasUnlockingGs; + } +} + +void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + setCbc(CB); + env = ENV; + cb = CB; + playerID = *CB->getPlayerID(); + wasWaitingForRealize = CB->waitTillRealize; + wasUnlockingGs = CB->unlockGsWhenWaiting; + CB->waitTillRealize = false; + CB->unlockGsWhenWaiting = false; + movesSkippedByDefense = 0; +} + +void CBattleAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) +{ + initBattleInterface(ENV, CB); + autobattlePreferences = autocombatPreferences; +} + +BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack) +{ + auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); + std::map woundHpToStack; + for(const auto * stack : healingTargets) + { + if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft()) + woundHpToStack[woundHp] = stack; + } + + if(woundHpToStack.empty()) + return BattleAction::makeDefend(stack); + else + return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack +} + +void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); +} + +static float getStrengthRatio(std::shared_ptr cb, int side) +{ + auto stacks = cb->battleGetAllStacks(); + auto our = 0, enemy = 0; + + for(auto stack : stacks) + { + auto creature = stack->creatureId().toCreature(); + + if(!creature) + continue; + + if(stack->unitSide() == side) + our += stack->getCount() * creature->getAIValue(); + else + enemy += stack->getCount() * creature->getAIValue(); + } + + return enemy == 0 ? 1.0f : static_cast(our) / enemy; +} + +void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) +{ + LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); + + auto timeElapsed = [](std::chrono::time_point start) -> uint64_t + { + auto end = std::chrono::high_resolution_clock::now(); + + return std::chrono::duration_cast(end - start).count(); + }; + + BattleAction result = BattleAction::makeDefend(stack); + setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) + + auto start = std::chrono::high_resolution_clock::now(); + + try + { + if(stack->creatureId() == CreatureID::CATAPULT) + { + cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack)); + return; + } + if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) + { + cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack)); + return; + } + +#if BATTLE_TRACE_LEVEL>=1 + logAi->trace("Build evaluator and targets"); +#endif + + BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side)); + + result = evaluator.selectStackAction(stack); + + if(!skipCastUntilNextBattle && evaluator.canCastSpell()) + { + auto spelCasted = evaluator.attemptCastingSpell(stack); + + if(spelCasted) + return; + + skipCastUntilNextBattle = true; + } + + logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); + + if(auto action = considerFleeingOrSurrendering(battleID)) + { + cb->battleMakeUnitAction(battleID, *action); + return; + } + } + catch(boost::thread_interrupted &) + { + throw; + } + catch(std::exception &e) + { + logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what()); + } + + if(result.actionType == EActionType::DEFEND) + { + movesSkippedByDefense++; + } + else if(result.actionType != EActionType::WAIT) + { + movesSkippedByDefense = 0; + } + + logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); + + cb->battleMakeUnitAction(battleID, result); +} + +BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack) +{ + BattleAction attack; + BattleHex targetHex = BattleHex::INVALID; + + if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED) + { + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE); + } + else + { + EWallPart wallParts[] = { + EWallPart::KEEP, + EWallPart::BOTTOM_TOWER, + EWallPart::UPPER_TOWER, + EWallPart::BELOW_GATE, + EWallPart::OVER_GATE, + EWallPart::BOTTOM_WALL, + EWallPart::UPPER_WALL + }; + + for(auto wallPart : wallParts) + { + auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); + + if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) + { + targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); + break; + } + } + } + + if(!targetHex.isValid()) + { + return BattleAction::makeDefend(stack); + } + + attack.aimToHex(targetHex); + attack.actionType = EActionType::CATAPULT; + attack.side = side; + attack.stackNumber = stack->unitId(); + + movesSkippedByDefense = 0; + + return attack; +} + +void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +{ + LOG_TRACE(logAi); + side = Side; + + skipCastUntilNextBattle = false; +} + +void CBattleAI::print(const std::string &text) const +{ + logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text); +} + +std::optional CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID) +{ + BattleStateInfoForRetreat bs; + + bs.canFlee = cb->getBattle(battleID)->battleCanFlee(); + bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID); + bs.ourSide = cb->getBattle(battleID)->battleGetMySide(); + bs.ourHero = cb->getBattle(battleID)->battleGetMyHero(); + bs.enemyHero = nullptr; + + for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false)) + { + if(stack->alive()) + { + if(stack->unitSide() == bs.ourSide) + bs.ourStacks.push_back(stack); + else + { + bs.enemyStacks.push_back(stack); + bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack); + } + } + } + + bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size(); + + if(!bs.canFlee && !bs.canSurrender) + { + return std::nullopt; + } + + auto result = cb->makeSurrenderRetreatDecision(battleID, bs); + + if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) + { + return BattleAction::makeRetreat(bs.ourSide); + } + + return result; +} + + + diff --git a/AI/BattleAI/StdInc.cpp b/AI/BattleAI/StdInc.cpp index a284091d2..277dd9af0 100644 --- a/AI/BattleAI/StdInc.cpp +++ b/AI/BattleAI/StdInc.cpp @@ -1,11 +1,11 @@ -/* - * StdInc.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 - * - */ -// Creates the precompiled header -#include "StdInc.h" +/* + * StdInc.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 + * + */ +// Creates the precompiled header +#include "StdInc.h" diff --git a/AI/BattleAI/StdInc.h b/AI/BattleAI/StdInc.h index ba1e8b9ee..310e3d172 100644 --- a/AI/BattleAI/StdInc.h +++ b/AI/BattleAI/StdInc.h @@ -1,17 +1,17 @@ -/* - * StdInc.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 "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +/* + * StdInc.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 "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/BattleAI/main.cpp b/AI/BattleAI/main.cpp index 349a205a6..101491d93 100644 --- a/AI/BattleAI/main.cpp +++ b/AI/BattleAI/main.cpp @@ -1,33 +1,33 @@ -/* - * main.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 "../../lib/AI_Base.h" -#include "BattleAI.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -static const char *g_cszAiName = "Battle AI"; - -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "../../lib/AI_Base.h" +#include "BattleAI.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +static const char *g_cszAiName = "Battle AI"; + +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/EmptyAI/CEmptyAI.cpp b/AI/EmptyAI/CEmptyAI.cpp index 1263bf61b..2812b7140 100644 --- a/AI/EmptyAI/CEmptyAI.cpp +++ b/AI/EmptyAI/CEmptyAI.cpp @@ -1,82 +1,82 @@ -/* - * CEmptyAI.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 "CEmptyAI.h" - -#include "../../lib/CRandomGenerator.h" -#include "../../lib/CStack.h" -#include "../../lib/battle/BattleAction.h" - -void CEmptyAI::saveGame(BinarySerializer & h, const int version) -{ -} - -void CEmptyAI::loadGame(BinaryDeserializer & h, const int version) -{ -} - -void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - cb = CB; - env = ENV; - human=false; - playerID = *cb->getPlayerID(); -} - -void CEmptyAI::yourTurn(QueryID queryID) -{ - cb->selectionMade(0, queryID); - cb->endTurn(); -} - -void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack) -{ - cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); -} - -void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance) -{ - cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); -} - -void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) -{ - cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); -} - -void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); -} - -void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) -{ - cb->selectionMade(0, askID); -} - -void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ - cb->selectionMade(0, askID); -} - -void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) -{ - cb->selectionMade(0, queryID); -} - -void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - cb->selectionMade(0, askID); -} - -std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) -{ - return std::nullopt; -} +/* + * CEmptyAI.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 "CEmptyAI.h" + +#include "../../lib/CRandomGenerator.h" +#include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" + +void CEmptyAI::saveGame(BinarySerializer & h, const int version) +{ +} + +void CEmptyAI::loadGame(BinaryDeserializer & h, const int version) +{ +} + +void CEmptyAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + cb = CB; + env = ENV; + human=false; + playerID = *cb->getPlayerID(); +} + +void CEmptyAI::yourTurn(QueryID queryID) +{ + cb->selectionMade(0, queryID); + cb->endTurn(); +} + +void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); +} + +void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); +} + +void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) +{ + cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); +} + +void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); +} + +void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) +{ + cb->selectionMade(0, askID); +} + +void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + cb->selectionMade(0, askID); +} + +void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) +{ + cb->selectionMade(0, queryID); +} + +void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + cb->selectionMade(0, askID); +} + +std::optional CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/EmptyAI/CEmptyAI.h b/AI/EmptyAI/CEmptyAI.h index 2170e1661..a2f125fbe 100644 --- a/AI/EmptyAI/CEmptyAI.h +++ b/AI/EmptyAI/CEmptyAI.h @@ -1,38 +1,38 @@ -/* - * CEmptyAI.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/AI_Base.h" -#include "../../CCallback.h" - -struct HeroMoveDetails; - -class CEmptyAI : public CGlobalAI -{ - std::shared_ptr cb; - -public: - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; - - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn(QueryID queryID) override; - void yourTacticPhase(const BattleID & battleID, int distance) override; - void activeStack(const BattleID & battleID, const CStack * stack) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; - void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; - void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; - void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; - void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; - void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; -}; - -#define NAME "EmptyAI 0.1" +/* + * CEmptyAI.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/AI_Base.h" +#include "../../CCallback.h" + +struct HeroMoveDetails; + +class CEmptyAI : public CGlobalAI +{ + std::shared_ptr cb; + +public: + virtual void saveGame(BinarySerializer & h, const int version) override; + virtual void loadGame(BinaryDeserializer & h, const int version) override; + + void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + void yourTurn(QueryID queryID) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; + void activeStack(const BattleID & battleID, const CStack * stack) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; + void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; + void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; +}; + +#define NAME "EmptyAI 0.1" diff --git a/AI/EmptyAI/StdInc.cpp b/AI/EmptyAI/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/AI/EmptyAI/StdInc.cpp +++ b/AI/EmptyAI/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/AI/EmptyAI/StdInc.h b/AI/EmptyAI/StdInc.h index 02b2c08f3..020481377 100644 --- a/AI/EmptyAI/StdInc.h +++ b/AI/EmptyAI/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/EmptyAI/main.cpp b/AI/EmptyAI/main.cpp index 931033075..e6ae9483c 100644 --- a/AI/EmptyAI/main.cpp +++ b/AI/EmptyAI/main.cpp @@ -1,28 +1,28 @@ -/* - * main.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 "CEmptyAI.h" - -std::set ais; -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy(name,NAME); -} - -extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "CEmptyAI.h" + +std::set ais; +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy(name,NAME); +} + +extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/GeniusAI.brain b/AI/GeniusAI.brain index 5dc5d3d7b..8377ade3e 100644 --- a/AI/GeniusAI.brain +++ b/AI/GeniusAI.brain @@ -1,736 +1,736 @@ -o 34 16 17 -R -o 34 16 17 -R -o 47 16 -R -o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24 -R -o 98 16 17 -R -o 98 16 17 -R -o 100 16 -R -o 38 16 -R -o 61 16 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 -R -o 28 16 -R -o 81 16 -R -o 83 25 -R -o 31 16 -R -o 57 24 -R -o 23 16 -R -o 102 16 -R -o 37 24 -R -o 51 16 -R -t 0 0 25 -R -t 0 1 25 -R -t 0 2 25 -R -t 0 3 25 -R -t 0 4 25 -R -t 0 5 25 -R -t 0 6 25 -R -t 0 7 25 -R -t 0 8 25 -R -t 0 9 25 -R -t 0 10 25 -R -t 0 11 25 -R -t 0 12 25 -R -t 0 13 25 -R -t 0 14 25 -R -t 0 15 25 -R -t 0 16 25 -R -t 0 17 25 -R -t 0 18 25 -R -t 0 19 25 -R -t 0 20 25 -R -t 0 21 25 -R -t 0 22 25 -R -t 0 23 25 -R -t 0 30 25 -R -t 0 31 25 -R -t 0 32 25 -R -t 0 33 25 -R -t 0 34 25 -R -t 0 35 25 -R -t 0 36 25 -R -t 0 37 25 -R -t 0 38 25 -R -t 0 39 25 -R -t 0 40 25 -R -t 0 41 25 -R -t 0 42 25 -R -t 0 43 25 -R -t 1 0 25 -R -t 1 1 25 -R -t 1 2 25 -R -t 1 3 25 -R -t 1 4 25 -R -t 1 5 25 -R -t 1 6 25 -R -t 1 7 25 -R -t 1 8 25 -R -t 1 9 25 -R -t 1 10 25 -R -t 1 11 25 -R -t 1 12 25 -R -t 1 13 25 -R -t 1 14 25 -R -t 1 15 25 -R -t 1 16 25 -R -t 1 17 25 -R -t 1 18 25 -R -t 1 19 25 -R -t 1 20 25 -R -t 1 21 25 -R -t 1 22 25 -R -t 1 23 25 -R -t 1 30 25 -R -t 1 31 25 -R -t 1 32 25 -R -t 1 33 25 -R -t 1 34 25 -R -t 1 35 25 -R -t 1 36 25 -R -t 1 37 25 -R -t 1 38 25 -R -t 1 39 25 -R -t 1 40 25 -R -t 1 41 25 -R -t 1 42 25 -R -t 1 43 25 -R -t 2 0 25 -R -t 2 1 25 -R -t 2 2 25 -R -t 2 3 25 -R -t 2 4 25 -R -t 2 5 25 -R -t 2 6 25 -R -t 2 7 25 -R -t 2 8 25 -R -t 2 9 25 -R -t 2 10 25 -R -t 2 11 25 -R -t 2 12 25 -R -t 2 13 25 -R -t 2 14 25 -R -t 2 15 25 -R -t 2 16 25 -R -t 2 17 25 -R -t 2 18 25 -R -t 2 19 25 -R -t 2 20 25 -R -t 2 21 25 -R -t 2 22 25 -R -t 2 23 25 -R -t 2 30 25 -R -t 2 31 25 -R -t 2 32 25 -R -t 2 33 25 -R -t 2 34 25 -R -t 2 35 25 -R -t 2 36 25 -R -t 2 37 25 -R -t 2 38 25 -R -t 2 39 25 -R -t 2 40 25 -R -t 2 41 25 -R -t 2 42 25 -R -t 2 43 25 -R -t 3 0 25 -R -t 3 1 25 -R -t 3 2 25 -R -t 3 3 25 -R -t 3 4 25 -R -t 3 5 25 -R -t 3 6 25 -R -t 3 7 25 -R -t 3 8 25 -R -t 3 9 25 -R -t 3 10 25 -R -t 3 11 25 -R -t 3 12 25 -R -t 3 13 25 -R -t 3 14 25 -R -t 3 15 25 -R -t 3 16 25 -R -t 3 17 25 -R -t 3 18 25 -R -t 3 19 25 -R -t 3 20 25 -R -t 3 21 25 -R -t 3 22 25 -R -t 3 23 25 -R -t 3 30 25 -R -t 3 31 25 -R -t 3 32 25 -R -t 3 33 25 -R -t 3 34 25 -R -t 3 35 25 -R -t 3 36 25 -R -t 3 37 25 -R -t 3 38 25 -R -t 3 39 25 -R -t 3 40 25 -R -t 3 41 25 -R -t 3 42 25 -R -t 3 43 25 -R -t 4 0 25 -R -t 4 1 25 -R -t 4 2 25 -R -t 4 3 25 -R -t 4 4 25 -R -t 4 5 25 -R -t 4 6 25 -R -t 4 7 25 -R -t 4 8 25 -R -t 4 9 25 -R -t 4 10 25 -R -t 4 11 25 -R -t 4 12 25 -R -t 4 13 25 -R -t 4 14 25 -R -t 4 15 25 -R -t 4 16 25 -R -t 4 17 25 -R -t 4 18 25 -R -t 4 19 25 -R -t 4 20 25 -R -t 4 21 25 -R -t 4 22 25 -R -t 4 23 25 -R -t 4 30 25 -R -t 4 31 25 -R -t 4 32 25 -R -t 4 33 25 -R -t 4 34 25 -R -t 4 35 25 -R -t 4 36 25 -R -t 4 37 25 -R -t 4 38 25 -R -t 4 39 25 -R -t 4 40 25 -R -t 4 41 25 -R -t 4 42 25 -R -t 4 43 25 -R -t 5 0 25 -R -t 5 1 25 -R -t 5 2 25 -R -t 5 3 25 -R -t 5 4 25 -R -t 5 5 25 -R -t 5 6 25 -R -t 5 7 25 -R -t 5 8 25 -R -t 5 9 25 -R -t 5 10 25 -R -t 5 11 25 -R -t 5 12 25 -R -t 5 13 25 -R -t 5 14 25 -R -t 5 15 25 -R -t 5 16 25 -R -t 5 17 25 -R -t 5 18 25 -R -t 5 19 25 -R -t 5 20 25 -R -t 5 21 25 -R -t 5 22 25 -R -t 5 23 25 -R -t 5 30 25 -R -t 5 31 25 -R -t 5 32 25 -R -t 5 33 25 -R -t 5 34 25 -R -t 5 35 25 -R -t 5 36 25 -R -t 5 37 25 -R -t 5 38 25 -R -t 5 39 25 -R -t 5 40 25 -R -t 5 41 25 -R -t 5 42 25 -R -t 5 43 25 -R -t 6 0 25 -R -t 6 1 25 -R -t 6 2 25 -R -t 6 3 25 -R -t 6 4 25 -R -t 6 5 25 -R -t 6 6 25 -R -t 6 7 25 -R -t 6 8 25 -R -t 6 9 25 -R -t 6 10 25 -R -t 6 11 25 -R -t 6 12 25 -R -t 6 13 25 -R -t 6 14 25 -R -t 6 15 25 -R -t 6 16 25 -R -t 6 17 25 -R -t 6 18 25 -R -t 6 19 25 -R -t 6 20 25 -R -t 6 21 25 -R -t 6 22 25 -R -t 6 23 25 -R -t 6 30 25 -R -t 6 31 25 -R -t 6 32 25 -R -t 6 33 25 -R -t 6 34 25 -R -t 6 35 25 -R -t 6 36 25 -R -t 6 37 25 -R -t 6 38 25 -R -t 6 39 25 -R -t 6 40 25 -R -t 6 41 25 -R -t 6 42 25 -R -t 6 43 25 -R -t 7 0 25 -R -t 7 1 25 -R -t 7 2 25 -R -t 7 3 25 -R -t 7 4 25 -R -t 7 5 25 -R -t 7 6 25 -R -t 7 7 25 -R -t 7 8 25 -R -t 7 9 25 -R -t 7 10 25 -R -t 7 11 25 -R -t 7 12 25 -R -t 7 13 25 -R -t 7 14 25 -R -t 7 15 25 -R -t 7 16 25 -R -t 7 17 25 -R -t 7 18 25 -R -t 7 19 25 -R -t 7 20 25 -R -t 7 21 25 -R -t 7 22 25 -R -t 7 23 25 -R -t 7 30 25 -R -t 7 31 25 -R -t 7 32 25 -R -t 7 33 25 -R -t 7 34 25 -R -t 7 35 25 -R -t 7 36 25 -R -t 7 37 25 -R -t 7 38 25 -R -t 7 39 25 -R -t 7 40 25 -R -t 7 41 25 -R -t 7 42 25 -R -t 7 43 25 -R -t 8 0 25 -R -t 8 1 25 -R -t 8 2 25 -R -t 8 3 25 -R -t 8 4 25 -R -t 8 5 25 -R -t 8 6 25 -R -t 8 7 25 -R -t 8 8 25 -R -t 8 9 25 -R -t 8 10 25 -R -t 8 11 25 -R -t 8 12 25 -R -t 8 13 25 -R -t 8 14 25 -R -t 8 15 25 -R -t 8 16 25 -R -t 8 17 25 -R -t 8 18 25 -R -t 8 19 25 -R -t 8 20 25 -R -t 8 21 25 -R -t 8 22 25 -R -t 8 23 25 -R -t 8 30 25 -R -t 8 31 25 -R -t 8 32 25 -R -t 8 33 25 -R -t 8 34 25 -R -t 8 35 25 -R -t 8 36 25 -R -t 8 37 25 -R -t 8 38 25 -R -t 8 39 25 -R -t 8 40 25 -R -t 8 41 25 -R -t 8 42 25 -R -t 8 43 25 -R +o 34 16 17 +R +o 34 16 17 +R +o 47 16 +R +o 101 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 24 +R +o 98 16 17 +R +o 98 16 17 +R +o 100 16 +R +o 38 16 +R +o 61 16 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 53 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +R +o 28 16 +R +o 81 16 +R +o 83 25 +R +o 31 16 +R +o 57 24 +R +o 23 16 +R +o 102 16 +R +o 37 24 +R +o 51 16 +R +t 0 0 25 +R +t 0 1 25 +R +t 0 2 25 +R +t 0 3 25 +R +t 0 4 25 +R +t 0 5 25 +R +t 0 6 25 +R +t 0 7 25 +R +t 0 8 25 +R +t 0 9 25 +R +t 0 10 25 +R +t 0 11 25 +R +t 0 12 25 +R +t 0 13 25 +R +t 0 14 25 +R +t 0 15 25 +R +t 0 16 25 +R +t 0 17 25 +R +t 0 18 25 +R +t 0 19 25 +R +t 0 20 25 +R +t 0 21 25 +R +t 0 22 25 +R +t 0 23 25 +R +t 0 30 25 +R +t 0 31 25 +R +t 0 32 25 +R +t 0 33 25 +R +t 0 34 25 +R +t 0 35 25 +R +t 0 36 25 +R +t 0 37 25 +R +t 0 38 25 +R +t 0 39 25 +R +t 0 40 25 +R +t 0 41 25 +R +t 0 42 25 +R +t 0 43 25 +R +t 1 0 25 +R +t 1 1 25 +R +t 1 2 25 +R +t 1 3 25 +R +t 1 4 25 +R +t 1 5 25 +R +t 1 6 25 +R +t 1 7 25 +R +t 1 8 25 +R +t 1 9 25 +R +t 1 10 25 +R +t 1 11 25 +R +t 1 12 25 +R +t 1 13 25 +R +t 1 14 25 +R +t 1 15 25 +R +t 1 16 25 +R +t 1 17 25 +R +t 1 18 25 +R +t 1 19 25 +R +t 1 20 25 +R +t 1 21 25 +R +t 1 22 25 +R +t 1 23 25 +R +t 1 30 25 +R +t 1 31 25 +R +t 1 32 25 +R +t 1 33 25 +R +t 1 34 25 +R +t 1 35 25 +R +t 1 36 25 +R +t 1 37 25 +R +t 1 38 25 +R +t 1 39 25 +R +t 1 40 25 +R +t 1 41 25 +R +t 1 42 25 +R +t 1 43 25 +R +t 2 0 25 +R +t 2 1 25 +R +t 2 2 25 +R +t 2 3 25 +R +t 2 4 25 +R +t 2 5 25 +R +t 2 6 25 +R +t 2 7 25 +R +t 2 8 25 +R +t 2 9 25 +R +t 2 10 25 +R +t 2 11 25 +R +t 2 12 25 +R +t 2 13 25 +R +t 2 14 25 +R +t 2 15 25 +R +t 2 16 25 +R +t 2 17 25 +R +t 2 18 25 +R +t 2 19 25 +R +t 2 20 25 +R +t 2 21 25 +R +t 2 22 25 +R +t 2 23 25 +R +t 2 30 25 +R +t 2 31 25 +R +t 2 32 25 +R +t 2 33 25 +R +t 2 34 25 +R +t 2 35 25 +R +t 2 36 25 +R +t 2 37 25 +R +t 2 38 25 +R +t 2 39 25 +R +t 2 40 25 +R +t 2 41 25 +R +t 2 42 25 +R +t 2 43 25 +R +t 3 0 25 +R +t 3 1 25 +R +t 3 2 25 +R +t 3 3 25 +R +t 3 4 25 +R +t 3 5 25 +R +t 3 6 25 +R +t 3 7 25 +R +t 3 8 25 +R +t 3 9 25 +R +t 3 10 25 +R +t 3 11 25 +R +t 3 12 25 +R +t 3 13 25 +R +t 3 14 25 +R +t 3 15 25 +R +t 3 16 25 +R +t 3 17 25 +R +t 3 18 25 +R +t 3 19 25 +R +t 3 20 25 +R +t 3 21 25 +R +t 3 22 25 +R +t 3 23 25 +R +t 3 30 25 +R +t 3 31 25 +R +t 3 32 25 +R +t 3 33 25 +R +t 3 34 25 +R +t 3 35 25 +R +t 3 36 25 +R +t 3 37 25 +R +t 3 38 25 +R +t 3 39 25 +R +t 3 40 25 +R +t 3 41 25 +R +t 3 42 25 +R +t 3 43 25 +R +t 4 0 25 +R +t 4 1 25 +R +t 4 2 25 +R +t 4 3 25 +R +t 4 4 25 +R +t 4 5 25 +R +t 4 6 25 +R +t 4 7 25 +R +t 4 8 25 +R +t 4 9 25 +R +t 4 10 25 +R +t 4 11 25 +R +t 4 12 25 +R +t 4 13 25 +R +t 4 14 25 +R +t 4 15 25 +R +t 4 16 25 +R +t 4 17 25 +R +t 4 18 25 +R +t 4 19 25 +R +t 4 20 25 +R +t 4 21 25 +R +t 4 22 25 +R +t 4 23 25 +R +t 4 30 25 +R +t 4 31 25 +R +t 4 32 25 +R +t 4 33 25 +R +t 4 34 25 +R +t 4 35 25 +R +t 4 36 25 +R +t 4 37 25 +R +t 4 38 25 +R +t 4 39 25 +R +t 4 40 25 +R +t 4 41 25 +R +t 4 42 25 +R +t 4 43 25 +R +t 5 0 25 +R +t 5 1 25 +R +t 5 2 25 +R +t 5 3 25 +R +t 5 4 25 +R +t 5 5 25 +R +t 5 6 25 +R +t 5 7 25 +R +t 5 8 25 +R +t 5 9 25 +R +t 5 10 25 +R +t 5 11 25 +R +t 5 12 25 +R +t 5 13 25 +R +t 5 14 25 +R +t 5 15 25 +R +t 5 16 25 +R +t 5 17 25 +R +t 5 18 25 +R +t 5 19 25 +R +t 5 20 25 +R +t 5 21 25 +R +t 5 22 25 +R +t 5 23 25 +R +t 5 30 25 +R +t 5 31 25 +R +t 5 32 25 +R +t 5 33 25 +R +t 5 34 25 +R +t 5 35 25 +R +t 5 36 25 +R +t 5 37 25 +R +t 5 38 25 +R +t 5 39 25 +R +t 5 40 25 +R +t 5 41 25 +R +t 5 42 25 +R +t 5 43 25 +R +t 6 0 25 +R +t 6 1 25 +R +t 6 2 25 +R +t 6 3 25 +R +t 6 4 25 +R +t 6 5 25 +R +t 6 6 25 +R +t 6 7 25 +R +t 6 8 25 +R +t 6 9 25 +R +t 6 10 25 +R +t 6 11 25 +R +t 6 12 25 +R +t 6 13 25 +R +t 6 14 25 +R +t 6 15 25 +R +t 6 16 25 +R +t 6 17 25 +R +t 6 18 25 +R +t 6 19 25 +R +t 6 20 25 +R +t 6 21 25 +R +t 6 22 25 +R +t 6 23 25 +R +t 6 30 25 +R +t 6 31 25 +R +t 6 32 25 +R +t 6 33 25 +R +t 6 34 25 +R +t 6 35 25 +R +t 6 36 25 +R +t 6 37 25 +R +t 6 38 25 +R +t 6 39 25 +R +t 6 40 25 +R +t 6 41 25 +R +t 6 42 25 +R +t 6 43 25 +R +t 7 0 25 +R +t 7 1 25 +R +t 7 2 25 +R +t 7 3 25 +R +t 7 4 25 +R +t 7 5 25 +R +t 7 6 25 +R +t 7 7 25 +R +t 7 8 25 +R +t 7 9 25 +R +t 7 10 25 +R +t 7 11 25 +R +t 7 12 25 +R +t 7 13 25 +R +t 7 14 25 +R +t 7 15 25 +R +t 7 16 25 +R +t 7 17 25 +R +t 7 18 25 +R +t 7 19 25 +R +t 7 20 25 +R +t 7 21 25 +R +t 7 22 25 +R +t 7 23 25 +R +t 7 30 25 +R +t 7 31 25 +R +t 7 32 25 +R +t 7 33 25 +R +t 7 34 25 +R +t 7 35 25 +R +t 7 36 25 +R +t 7 37 25 +R +t 7 38 25 +R +t 7 39 25 +R +t 7 40 25 +R +t 7 41 25 +R +t 7 42 25 +R +t 7 43 25 +R +t 8 0 25 +R +t 8 1 25 +R +t 8 2 25 +R +t 8 3 25 +R +t 8 4 25 +R +t 8 5 25 +R +t 8 6 25 +R +t 8 7 25 +R +t 8 8 25 +R +t 8 9 25 +R +t 8 10 25 +R +t 8 11 25 +R +t 8 12 25 +R +t 8 13 25 +R +t 8 14 25 +R +t 8 15 25 +R +t 8 16 25 +R +t 8 17 25 +R +t 8 18 25 +R +t 8 19 25 +R +t 8 20 25 +R +t 8 21 25 +R +t 8 22 25 +R +t 8 23 25 +R +t 8 30 25 +R +t 8 31 25 +R +t 8 32 25 +R +t 8 33 25 +R +t 8 34 25 +R +t 8 35 25 +R +t 8 36 25 +R +t 8 37 25 +R +t 8 38 25 +R +t 8 39 25 +R +t 8 40 25 +R +t 8 41 25 +R +t 8 42 25 +R +t 8 43 25 +R diff --git a/AI/StupidAI/StdInc.cpp b/AI/StupidAI/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/AI/StupidAI/StdInc.cpp +++ b/AI/StupidAI/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/AI/StupidAI/StdInc.h b/AI/StupidAI/StdInc.h index 02b2c08f3..020481377 100644 --- a/AI/StupidAI/StdInc.h +++ b/AI/StupidAI/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/AI/StupidAI/StupidAI.cpp b/AI/StupidAI/StupidAI.cpp index 5836b2b7a..3888e5a6b 100644 --- a/AI/StupidAI/StupidAI.cpp +++ b/AI/StupidAI/StupidAI.cpp @@ -1,333 +1,333 @@ -/* - * StupidAI.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 "../../lib/AI_Base.h" -#include "StupidAI.h" -#include "../../lib/CStack.h" -#include "../../CCallback.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/battle/BattleInfo.h" - -static std::shared_ptr cbc; - -CStupidAI::CStupidAI() - : side(-1) - , wasWaitingForRealize(false) - , wasUnlockingGs(false) -{ - print("created"); -} - -CStupidAI::~CStupidAI() -{ - print("destroyed"); - if(cb) - { - //Restore previous state of CB - it may be shared with the main AI (like VCAI) - cb->waitTillRealize = wasWaitingForRealize; - cb->unlockGsWhenWaiting = wasUnlockingGs; - } -} - -void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - print("init called, saving ptr to IBattleCallback"); - env = ENV; - cbc = cb = CB; - - wasWaitingForRealize = CB->waitTillRealize; - wasUnlockingGs = CB->unlockGsWhenWaiting; - CB->waitTillRealize = false; - CB->unlockGsWhenWaiting = false; -} - -void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) -{ - initBattleInterface(ENV, CB); -} - -void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action) -{ - print("actionFinished called"); -} - -void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action) -{ - print("actionStarted called"); -} - -class EnemyInfo -{ -public: - const CStack * s; - int adi, adr; - std::vector attackFrom; //for melee fight - EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) - {} - void calcDmg(const BattleID & battleID, const CStack * ourStack) - { - // FIXME: provide distance info for Jousting bonus - DamageEstimation retal; - DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); - adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); - adr = static_cast((retal.damage.min + retal.damage.max) / 2); - } - - bool operator==(const EnemyInfo& ei) const - { - return s == ei.s; - } -}; - -bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) -{ - return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); -} - -static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) -{ - int shooters[2] = {0}; //count of shooters on hexes - - for(int i = 0; i < 2; i++) - { - for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) - if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour)) - if(s->isShooter()) - shooters[i]++; - } - - return shooters[0] < shooters[1]; -} - -void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance) -{ - cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); -} - -void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) -{ - //boost::this_thread::sleep_for(boost::chrono::seconds(2)); - print("activeStack called for " + stack->nodeName()); - ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack); - std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; - - if(stack->creatureId() == CreatureID::CATAPULT) - { - BattleAction attack; - static const std::vector wallHexes = {50, 183, 182, 130, 78, 29, 12, 95}; - auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); - attack.aimToHex(seletectedHex); - attack.actionType = EActionType::CATAPULT; - attack.side = side; - attack.stackNumber = stack->unitId(); - - cb->battleMakeUnitAction(battleID, attack); - return; - } - else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) - { - cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); - return; - } - - for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY)) - { - if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition())) - { - enemiesShootable.push_back(s); - } - else - { - std::vector avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false); - - for (BattleHex hex : avHexes) - { - if(CStack::isMeleeAttackPossible(stack, s, hex)) - { - std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); - if(i == enemiesReachable.end()) - { - enemiesReachable.push_back(s); - i = enemiesReachable.begin() + (enemiesReachable.size() - 1); - } - - i->attackFrom.push_back(hex); - } - } - - if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid()) - enemiesUnreachable.push_back(s); - } - } - - for ( auto & enemy : enemiesReachable ) - enemy.calcDmg(battleID, stack); - - for ( auto & enemy : enemiesShootable ) - enemy.calcDmg(battleID, stack); - - if(enemiesShootable.size()) - { - const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); - cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s)); - return; - } - else if(enemiesReachable.size()) - { - const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); - BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);}); - - cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex)); - return; - } - else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies - { - auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int - { - return dists.distToNearestNeighbour(stack, ei.s); - }); - - if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) - { - cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack))); - return; - } - } - - cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); - return; -} - -void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba) -{ - print("battleAttack called"); -} - -void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) -{ - print("battleStacksAttacked called"); -} - -void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) -{ - print("battleEnd called"); -} - -// void CStupidAI::battleResultsApplied() -// { -// print("battleResultsApplied called"); -// } - -void CStupidAI::battleNewRoundFirst(const BattleID & battleID) -{ - print("battleNewRoundFirst called"); -} - -void CStupidAI::battleNewRound(const BattleID & battleID) -{ - print("battleNewRound called"); -} - -void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) -{ - print("battleStackMoved called"); -} - -void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) -{ - print("battleSpellCast called"); -} - -void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) -{ - print("battleStacksEffectsSet called"); -} - -void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) -{ - print("battleStart called"); - side = Side; -} - -void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) -{ - print("battleCatapultAttacked called"); -} - -void CStupidAI::print(const std::string &text) const -{ - logAi->trace("CStupidAI [%p]: %s", this, text); -} - -BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const -{ - auto reachability = cb->getBattle(battleID)->getReachability(stack); - auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); - - if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked - { - return BattleAction::makeDefend(stack); - } - - std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool - { - return reachability.distances[h1] < reachability.distances[h2]; - }); - - for(auto hex : hexes) - { - if(vstd::contains(avHexes, hex)) - return BattleAction::makeMove(stack, hex); - - if(stack->coversPos(hex)) - { - logAi->warn("Warning: already standing on neighbouring tile!"); - //We shouldn't even be here... - return BattleAction::makeDefend(stack); - } - } - - BattleHex bestNeighbor = hexes.front(); - - if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) - { - return BattleAction::makeDefend(stack); - } - - if(stack->hasBonusOfType(BonusType::FLYING)) - { - // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. - // We just check all available hexes and pick the one closest to the target. - auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int - { - return BattleHex::getDistance(bestNeighbor, hex); - }); - - return BattleAction::makeMove(stack, *nearestAvailableHex); - } - else - { - BattleHex currentDest = bestNeighbor; - while(1) - { - if(!currentDest.isValid()) - { - logAi->error("CBattleAI::goTowards: internal error"); - return BattleAction::makeDefend(stack); - } - - if(vstd::contains(avHexes, currentDest)) - return BattleAction::makeMove(stack, currentDest); - - currentDest = reachability.predecessors[currentDest]; - } - } -} +/* + * StupidAI.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 "../../lib/AI_Base.h" +#include "StupidAI.h" +#include "../../lib/CStack.h" +#include "../../CCallback.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleInfo.h" + +static std::shared_ptr cbc; + +CStupidAI::CStupidAI() + : side(-1) + , wasWaitingForRealize(false) + , wasUnlockingGs(false) +{ + print("created"); +} + +CStupidAI::~CStupidAI() +{ + print("destroyed"); + if(cb) + { + //Restore previous state of CB - it may be shared with the main AI (like VCAI) + cb->waitTillRealize = wasWaitingForRealize; + cb->unlockGsWhenWaiting = wasUnlockingGs; + } +} + +void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + print("init called, saving ptr to IBattleCallback"); + env = ENV; + cbc = cb = CB; + + wasWaitingForRealize = CB->waitTillRealize; + wasUnlockingGs = CB->unlockGsWhenWaiting; + CB->waitTillRealize = false; + CB->unlockGsWhenWaiting = false; +} + +void CStupidAI::initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) +{ + initBattleInterface(ENV, CB); +} + +void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action) +{ + print("actionFinished called"); +} + +void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action) +{ + print("actionStarted called"); +} + +class EnemyInfo +{ +public: + const CStack * s; + int adi, adr; + std::vector attackFrom; //for melee fight + EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) + {} + void calcDmg(const BattleID & battleID, const CStack * ourStack) + { + // FIXME: provide distance info for Jousting bonus + DamageEstimation retal; + DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal); + adi = static_cast((dmg.damage.min + dmg.damage.max) / 2); + adr = static_cast((retal.damage.min + retal.damage.max) / 2); + } + + bool operator==(const EnemyInfo& ei) const + { + return s == ei.s; + } +}; + +bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2) +{ + return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); +} + +static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2) +{ + int shooters[2] = {0}; //count of shooters on hexes + + for(int i = 0; i < 2; i++) + { + for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) + if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour)) + if(s->isShooter()) + shooters[i]++; + } + + return shooters[0] < shooters[1]; +} + +void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide())); +} + +void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + //boost::this_thread::sleep_for(boost::chrono::seconds(2)); + print("activeStack called for " + stack->nodeName()); + ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack); + std::vector enemiesShootable, enemiesReachable, enemiesUnreachable; + + if(stack->creatureId() == CreatureID::CATAPULT) + { + BattleAction attack; + static const std::vector wallHexes = {50, 183, 182, 130, 78, 29, 12, 95}; + auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault()); + attack.aimToHex(seletectedHex); + attack.actionType = EActionType::CATAPULT; + attack.side = side; + attack.stackNumber = stack->unitId(); + + cb->battleMakeUnitAction(battleID, attack); + return; + } + else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) + { + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return; + } + + for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY)) + { + if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition())) + { + enemiesShootable.push_back(s); + } + else + { + std::vector avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false); + + for (BattleHex hex : avHexes) + { + if(CStack::isMeleeAttackPossible(stack, s, hex)) + { + std::vector::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s); + if(i == enemiesReachable.end()) + { + enemiesReachable.push_back(s); + i = enemiesReachable.begin() + (enemiesReachable.size() - 1); + } + + i->attackFrom.push_back(hex); + } + } + + if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid()) + enemiesUnreachable.push_back(s); + } + } + + for ( auto & enemy : enemiesReachable ) + enemy.calcDmg(battleID, stack); + + for ( auto & enemy : enemiesShootable ) + enemy.calcDmg(battleID, stack); + + if(enemiesShootable.size()) + { + const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); + cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s)); + return; + } + else if(enemiesReachable.size()) + { + const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); + BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);}); + + cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex)); + return; + } + else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies + { + auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int + { + return dists.distToNearestNeighbour(stack, ei.s); + }); + + if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) + { + cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack))); + return; + } + } + + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return; +} + +void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba) +{ + print("battleAttack called"); +} + +void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + print("battleStacksAttacked called"); +} + +void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) +{ + print("battleEnd called"); +} + +// void CStupidAI::battleResultsApplied() +// { +// print("battleResultsApplied called"); +// } + +void CStupidAI::battleNewRoundFirst(const BattleID & battleID) +{ + print("battleNewRoundFirst called"); +} + +void CStupidAI::battleNewRound(const BattleID & battleID) +{ + print("battleNewRound called"); +} + +void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + print("battleStackMoved called"); +} + +void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) +{ + print("battleSpellCast called"); +} + +void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + print("battleStacksEffectsSet called"); +} + +void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) +{ + print("battleStart called"); + side = Side; +} + +void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + print("battleCatapultAttacked called"); +} + +void CStupidAI::print(const std::string &text) const +{ + logAi->trace("CStupidAI [%p]: %s", this, text); +} + +BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const +{ + auto reachability = cb->getBattle(battleID)->getReachability(stack); + auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); + + if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked + { + return BattleAction::makeDefend(stack); + } + + std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool + { + return reachability.distances[h1] < reachability.distances[h2]; + }); + + for(auto hex : hexes) + { + if(vstd::contains(avHexes, hex)) + return BattleAction::makeMove(stack, hex); + + if(stack->coversPos(hex)) + { + logAi->warn("Warning: already standing on neighbouring tile!"); + //We shouldn't even be here... + return BattleAction::makeDefend(stack); + } + } + + BattleHex bestNeighbor = hexes.front(); + + if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE) + { + return BattleAction::makeDefend(stack); + } + + if(stack->hasBonusOfType(BonusType::FLYING)) + { + // Flying stack doesn't go hex by hex, so we can't backtrack using predecessors. + // We just check all available hexes and pick the one closest to the target. + auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int + { + return BattleHex::getDistance(bestNeighbor, hex); + }); + + return BattleAction::makeMove(stack, *nearestAvailableHex); + } + else + { + BattleHex currentDest = bestNeighbor; + while(1) + { + if(!currentDest.isValid()) + { + logAi->error("CBattleAI::goTowards: internal error"); + return BattleAction::makeDefend(stack); + } + + if(vstd::contains(avHexes, currentDest)) + return BattleAction::makeMove(stack, currentDest); + + currentDest = reachability.predecessors[currentDest]; + } + } +} diff --git a/AI/StupidAI/StupidAI.h b/AI/StupidAI/StupidAI.h index 744214643..7c81a864c 100644 --- a/AI/StupidAI/StupidAI.h +++ b/AI/StupidAI/StupidAI.h @@ -1,56 +1,56 @@ -/* - * StupidAI.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/battle/BattleHex.h" -#include "../../lib/battle/ReachabilityInfo.h" -#include "../../lib/CGameInterface.h" - -class EnemyInfo; - -class CStupidAI : public CBattleGameInterface -{ - int side; - std::shared_ptr cb; - std::shared_ptr env; - - bool wasWaitingForRealize; - bool wasUnlockingGs; - - void print(const std::string &text) const; -public: - CStupidAI(); - ~CStupidAI(); - - void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; - - void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero - void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero - void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack - void yourTacticPhase(const BattleID & battleID, int distance) override; - - void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack - void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) - void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; - //void battleResultsApplied() override; //called when all effects of last battle are applied - void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; - void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; - void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; - void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks - //void battleTriggerEffect(const BattleTriggerEffect & bte) override; - void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right - void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack - -private: - BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const; -}; - +/* + * StupidAI.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/battle/BattleHex.h" +#include "../../lib/battle/ReachabilityInfo.h" +#include "../../lib/CGameInterface.h" + +class EnemyInfo; + +class CStupidAI : public CBattleGameInterface +{ + int side; + std::shared_ptr cb; + std::shared_ptr env; + + bool wasWaitingForRealize; + bool wasUnlockingGs; + + void print(const std::string &text) const; +public: + CStupidAI(); + ~CStupidAI(); + + void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences) override; + + void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void yourTacticPhase(const BattleID & battleID, int distance) override; + + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + //void battleResultsApplied() override; //called when all effects of last battle are applied + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks + //void battleTriggerEffect(const BattleTriggerEffect & bte) override; + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack + +private: + BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector hexes) const; +}; + diff --git a/AI/StupidAI/main.cpp b/AI/StupidAI/main.cpp index 55e2d1f01..93f2ff517 100644 --- a/AI/StupidAI/main.cpp +++ b/AI/StupidAI/main.cpp @@ -1,34 +1,34 @@ -/* - * main.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 "../../lib/AI_Base.h" -#include "StupidAI.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -static const char *g_cszAiName = "Stupid AI 0.1"; - -extern "C" DLL_EXPORT int GetGlobalAiVersion() -{ - return AI_INTERFACE_VER; -} - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) -{ - out = std::make_shared(); -} +/* + * main.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 "../../lib/AI_Base.h" +#include "StupidAI.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +static const char *g_cszAiName = "Stupid AI 0.1"; + +extern "C" DLL_EXPORT int GetGlobalAiVersion() +{ + return AI_INTERFACE_VER; +} + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr &out) +{ + out = std::make_shared(); +} diff --git a/AI/VCAI/AIUtility.cpp b/AI/VCAI/AIUtility.cpp index 694803f19..ac68fa71b 100644 --- a/AI/VCAI/AIUtility.cpp +++ b/AI/VCAI/AIUtility.cpp @@ -1,259 +1,259 @@ -/* - * AIUtility.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 "AIUtility.h" -#include "VCAI.h" -#include "FuzzyHelper.h" -#include "Goals/Goals.h" - -#include "../../lib/UnlockGuard.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/mapObjects/CBank.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapObjects/CQuest.h" -#include "../../lib/mapping/CMapDefines.h" - -extern FuzzyHelper * fh; - -const CGObjectInstance * ObjectIdRef::operator->() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::operator const CGObjectInstance *() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::operator bool() const -{ - return cb->getObj(id, false); -} - -ObjectIdRef::ObjectIdRef(ObjectInstanceID _id) - : id(_id) -{ - -} - -ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj) - : id(obj->id) -{ - -} - -bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const -{ - return id < rhs.id; -} - -HeroPtr::HeroPtr(const CGHeroInstance * H) -{ - if(!H) - { - //init from nullptr should equal to default init - *this = HeroPtr(); - return; - } - - h = H; - name = h->getNameTranslated(); - hid = H->id; -// infosCount[ai->playerID][hid]++; -} - -HeroPtr::HeroPtr() -{ - h = nullptr; - hid = ObjectInstanceID(); -} - -HeroPtr::~HeroPtr() -{ -// if(hid >= 0) -// infosCount[ai->playerID][hid]--; -} - -bool HeroPtr::operator<(const HeroPtr & rhs) const -{ - return hid < rhs.hid; -} - -const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const -{ - //TODO? check if these all assertions every time we get info about hero affect efficiency - // - //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) - assert(doWeExpectNull || h); - - if(h) - { - auto obj = cb->getObj(hid); - const bool owned = obj && obj->tempOwner == ai->playerID; - - if(doWeExpectNull && !owned) - { - return nullptr; - } - else - { - assert(obj); - assert(owned); - } - } - - return h; -} - -const CGHeroInstance * HeroPtr::operator->() const -{ - return get(); -} - -bool HeroPtr::validAndSet() const -{ - return get(true); -} - -const CGHeroInstance * HeroPtr::operator*() const -{ - return get(); -} - -bool HeroPtr::operator==(const HeroPtr & rhs) const -{ - return h == rhs.get(true); -} - -bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const -{ - const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()); - const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); - - return ln->getCost() < rn->getCost(); -} - -bool isSafeToVisit(HeroPtr h, crint3 tile) -{ - return isSafeToVisit(h, fh->evaluateDanger(tile, h.get())); -} - -bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength) -{ - const ui64 heroStrength = h->getTotalStrength(); - - if(dangerStrength) - { - return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; - } - - return true; //there's no danger -} - -bool isObjectRemovable(const CGObjectInstance * obj) -{ - //FIXME: move logic to object property! - switch (obj->ID) - { - case Obj::MONSTER: - case Obj::RESOURCE: - case Obj::CAMPFIRE: - case Obj::TREASURE_CHEST: - case Obj::ARTIFACT: - case Obj::BORDERGUARD: - case Obj::FLOTSAM: - case Obj::PANDORAS_BOX: - case Obj::OCEAN_BOTTLE: - case Obj::SEA_CHEST: - case Obj::SHIPWRECK_SURVIVOR: - case Obj::SPELL_SCROLL: - return true; - break; - default: - return false; - break; - } - -} - -bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater) -{ - // TODO: Such information should be provided by pathfinder - // Tile must be free or with unoccupied boat - if(!t->blocked) - { - return true; - } - else if(!fromWater) // do not try to board when in water sector - { - if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) - return true; - } - return false; -} - -bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder -{ - if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE) - return false; - auto gate = dynamic_cast(cb->getTile(tileToHit)->topVisitableObj()); - return !gate->passableFor(ai->playerID); -} - -bool isBlockVisitObj(const int3 & pos) -{ - if(auto obj = cb->getTopObj(pos)) - { - if(obj->isBlockedVisitable()) //we can't stand on that object - return true; - } - - return false; -} - -creInfo infoFromDC(const dwellingContent & dc) -{ - creInfo ci; - ci.count = dc.first; - ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed - if (ci.creID != CreatureID::NONE) - { - ci.cre = VLC->creatures()->getById(ci.creID); - ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. - } - else - { - ci.cre = nullptr; - ci.level = 0; - } - return ci; -} - -bool compareHeroStrength(HeroPtr h1, HeroPtr h2) -{ - return h1->getTotalStrength() < h2->getTotalStrength(); -} - -bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) -{ - return a1->getArmyStrength() < a2->getArmyStrength(); -} - -bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) -{ - auto art1 = a1->artType; - auto art2 = a2->artType; - - if(art1->getPrice() == art2->getPrice()) - return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); - else - return art1->getPrice() > art2->getPrice(); -} +/* + * AIUtility.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 "AIUtility.h" +#include "VCAI.h" +#include "FuzzyHelper.h" +#include "Goals/Goals.h" + +#include "../../lib/UnlockGuard.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CBank.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CQuest.h" +#include "../../lib/mapping/CMapDefines.h" + +extern FuzzyHelper * fh; + +const CGObjectInstance * ObjectIdRef::operator->() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::operator const CGObjectInstance *() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::operator bool() const +{ + return cb->getObj(id, false); +} + +ObjectIdRef::ObjectIdRef(ObjectInstanceID _id) + : id(_id) +{ + +} + +ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj) + : id(obj->id) +{ + +} + +bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const +{ + return id < rhs.id; +} + +HeroPtr::HeroPtr(const CGHeroInstance * H) +{ + if(!H) + { + //init from nullptr should equal to default init + *this = HeroPtr(); + return; + } + + h = H; + name = h->getNameTranslated(); + hid = H->id; +// infosCount[ai->playerID][hid]++; +} + +HeroPtr::HeroPtr() +{ + h = nullptr; + hid = ObjectInstanceID(); +} + +HeroPtr::~HeroPtr() +{ +// if(hid >= 0) +// infosCount[ai->playerID][hid]--; +} + +bool HeroPtr::operator<(const HeroPtr & rhs) const +{ + return hid < rhs.hid; +} + +const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const +{ + //TODO? check if these all assertions every time we get info about hero affect efficiency + // + //behave terribly when attempting unauthorized access to hero that is not ours (or was lost) + assert(doWeExpectNull || h); + + if(h) + { + auto obj = cb->getObj(hid); + const bool owned = obj && obj->tempOwner == ai->playerID; + + if(doWeExpectNull && !owned) + { + return nullptr; + } + else + { + assert(obj); + assert(owned); + } + } + + return h; +} + +const CGHeroInstance * HeroPtr::operator->() const +{ + return get(); +} + +bool HeroPtr::validAndSet() const +{ + return get(true); +} + +const CGHeroInstance * HeroPtr::operator*() const +{ + return get(); +} + +bool HeroPtr::operator==(const HeroPtr & rhs) const +{ + return h == rhs.get(true); +} + +bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const +{ + const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos()); + const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos()); + + return ln->getCost() < rn->getCost(); +} + +bool isSafeToVisit(HeroPtr h, crint3 tile) +{ + return isSafeToVisit(h, fh->evaluateDanger(tile, h.get())); +} + +bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength) +{ + const ui64 heroStrength = h->getTotalStrength(); + + if(dangerStrength) + { + return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; + } + + return true; //there's no danger +} + +bool isObjectRemovable(const CGObjectInstance * obj) +{ + //FIXME: move logic to object property! + switch (obj->ID) + { + case Obj::MONSTER: + case Obj::RESOURCE: + case Obj::CAMPFIRE: + case Obj::TREASURE_CHEST: + case Obj::ARTIFACT: + case Obj::BORDERGUARD: + case Obj::FLOTSAM: + case Obj::PANDORAS_BOX: + case Obj::OCEAN_BOTTLE: + case Obj::SEA_CHEST: + case Obj::SHIPWRECK_SURVIVOR: + case Obj::SPELL_SCROLL: + return true; + break; + default: + return false; + break; + } + +} + +bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater) +{ + // TODO: Such information should be provided by pathfinder + // Tile must be free or with unoccupied boat + if(!t->blocked) + { + return true; + } + else if(!fromWater) // do not try to board when in water sector + { + if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT) + return true; + } + return false; +} + +bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder +{ + if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE) + return false; + auto gate = dynamic_cast(cb->getTile(tileToHit)->topVisitableObj()); + return !gate->passableFor(ai->playerID); +} + +bool isBlockVisitObj(const int3 & pos) +{ + if(auto obj = cb->getTopObj(pos)) + { + if(obj->isBlockedVisitable()) //we can't stand on that object + return true; + } + + return false; +} + +creInfo infoFromDC(const dwellingContent & dc) +{ + creInfo ci; + ci.count = dc.first; + ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed + if (ci.creID != CreatureID::NONE) + { + ci.cre = VLC->creatures()->getById(ci.creID); + ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. + } + else + { + ci.cre = nullptr; + ci.level = 0; + } + return ci; +} + +bool compareHeroStrength(HeroPtr h1, HeroPtr h2) +{ + return h1->getTotalStrength() < h2->getTotalStrength(); +} + +bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2) +{ + return a1->getArmyStrength() < a2->getArmyStrength(); +} + +bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) +{ + auto art1 = a1->artType; + auto art2 = a2->artType; + + if(art1->getPrice() == art2->getPrice()) + return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); + else + return art1->getPrice() > art2->getPrice(); +} diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 2c9b4d897..8f79eaa43 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1,2912 +1,2912 @@ -/* - * VCAI.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 "VCAI.h" -#include "FuzzyHelper.h" -#include "ResourceManager.h" -#include "BuildingManager.h" -#include "Goals/Goals.h" - -#include "../../lib/UnlockGuard.h" -#include "../../lib/mapObjects/MapObjects.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacksBase.h" -#include "../../lib/NetPacks.h" -#include "../../lib/bonuses/CBonusSystemNode.h" -#include "../../lib/bonuses/Limiters.h" -#include "../../lib/bonuses/Updaters.h" -#include "../../lib/bonuses/Propagators.h" -#include "../../lib/serializer/CTypeList.h" -#include "../../lib/serializer/BinarySerializer.h" -#include "../../lib/serializer/BinaryDeserializer.h" - -#include "AIhelper.h" - -extern FuzzyHelper * fh; - -const double SAFE_ATTACK_CONSTANT = 1.5; - -//one thread may be turn of AI and another will be handling a side effect for AI2 -thread_local CCallback * cb = nullptr; -thread_local VCAI * ai = nullptr; - -//std::map > HeroView::infosCount; - -//helper RAII to manage global ai/cb ptrs -struct SetGlobalState -{ - SetGlobalState(VCAI * AI) - { - assert(!ai); - assert(!cb); - - ai = AI; - cb = AI->myCb.get(); - } - ~SetGlobalState() - { - //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully - //TODO: to ensure that, make rm unique_ptr - ai = nullptr; - cb = nullptr; - } -}; - - -#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); - -#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) -#define MAKING_TURN SET_GLOBAL_STATE(this) - -VCAI::VCAI() -{ - LOG_TRACE(logAi); - makingTurn = nullptr; - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - - ah = new AIhelper(); - ah->setAI(this); -} - -VCAI::~VCAI() -{ - delete ah; - LOG_TRACE(logAi); - finish(); -} - -void VCAI::availableCreaturesChanged(const CGDwelling * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroMoved(const TryMoveHero & details, bool verbose) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - //enemy hero may have left visible area - validateObject(details.id); - auto hero = cb->getHero(details.id); - - const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; - const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); - - const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); - const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose)); - - if(details.result == TryMoveHero::TELEPORTATION) - { - auto t1 = dynamic_cast(o1); - auto t2 = dynamic_cast(o2); - if(t1 && t2) - { - if(cb->isTeleportChannelBidirectional(t1->channel)) - { - if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels - { - knownSubterraneanGates[o1] = o2; - knownSubterraneanGates[o2] = o1; - logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString()); - } - } - } - //FIXME: teleports are not correctly visited - unreserveObject(hero, t1); - unreserveObject(hero, t2); - } - else if(details.result == TryMoveHero::EMBARK && hero) - { - //make sure AI not attempt to visit used boat - validateObject(hero->boat); - } - else if(details.result == TryMoveHero::DISEMBARK && o1) - { - auto boat = dynamic_cast(o1); - if(boat) - addVisitableObj(boat); - } -} - -void VCAI::heroInGarrisonChange(const CGTownInstance * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::centerView(int3 pos, int focusTime) -{ - LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime); - NET_EVENT_HANDLER; -} - -void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactAssembled(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - status.addQuery(queryID, "TavernWindow"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::playerBlocked(int reason, bool start) -{ - LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start); - NET_EVENT_HANDLER; - if(start && reason == PlayerBlocked::UPCOMING_BATTLE) - status.setBattle(UPCOMING_BATTLE); - - if(reason == PlayerBlocked::ONGOING_MOVEMENT) - status.setMove(start); -} - -void VCAI::showPuzzleMap() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showShipyardDialog(const IShipyard * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) -{ - LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); - NET_EVENT_HANDLER; - logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); - if(player == playerID) - { - if(victoryLossCheckResult.victory()) - { - logAi->debug("VCAI: I won! Incredible!"); - logAi->debug("Turn nr %d", myCb->getDate()); - } - else - { - logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); - } - - finish(); - } -} - -void VCAI::artifactPut(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactRemoved(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::artifactDisassembled(const ArtifactLocation & al) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) -{ - LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a"))); - NET_EVENT_HANDLER; - - if(start && visitedObj) //we can end visit with null object, anyway - { - markObjectVisited(visitedObj); - unreserveObject(visitor, visitedObj); - completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore - //TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..) - if (visitedObj->ID == Obj::HERO) - { - visitedHeroes[visitor].insert(HeroPtr(dynamic_cast(visitedObj))); - } - } - - status.heroVisit(visitedObj, start); -} - -void VCAI::availableArtifactsChanged(const CGBlackMarket * bm) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - //buildArmyIn(town); - //moveCreaturesToHero(town); -} - -void VCAI::tileHidden(const std::unordered_set & pos) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - validateVisitableObjs(); - clearPathsInfo(); -} - -void VCAI::tileRevealed(const std::unordered_set & pos) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - for(int3 tile : pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) - addVisitableObj(obj); - } - - clearPathsInfo(); -} - -void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - auto firstHero = cb->getHero(hero1); - auto secondHero = cb->getHero(hero2); - - status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner)); - - requestActionASAP([=]() - { - float goalpriority1 = 0, goalpriority2 = 0; - - auto firstGoal = getGoal(firstHero); - if(firstGoal->goalType == Goals::GATHER_ARMY) - goalpriority1 = firstGoal->priority; - auto secondGoal = getGoal(secondHero); - if(secondGoal->goalType == Goals::GATHER_ARMY) - goalpriority2 = secondGoal->priority; - - auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void - { - this->pickBestCreatures(h1, h2); - this->pickBestArtifacts(h1, h2); - }; - - //Do not attempt army or artifacts exchange if we visited ally player - //Visits can still be useful if hero have skills like Scholar - if(firstHero->tempOwner != secondHero->tempOwner) - { - logAi->debug("Heroes owned by different players. Do not exchange army or artifacts."); - } - else if(goalpriority1 > goalpriority2) - { - transferFrom2to1(firstHero, secondHero); - } - else if(goalpriority1 < goalpriority2) - { - transferFrom2to1(secondHero, firstHero); - } - else //regular criteria - { - if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero)) - transferFrom2to1(firstHero, secondHero); - else if(ah->canGetArmy(secondHero, firstHero)) - transferFrom2to1(secondHero, firstHero); - } - - completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime? - completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum()))); - - answerQuery(query, 0); - }); -} - -void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) -{ - LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); - NET_EVENT_HANDLER; -} - -void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "level '%i'", level); - NET_EVENT_HANDLER; - - status.addQuery(queryID, "RecruitmentDialog"); - requestActionASAP([=](){ - recruitCreatures(dwelling, dst); - checkHeroArmy(dynamic_cast(dst)); - answerQuery(queryID, 0); - }); -} - -void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::newObject(const CGObjectInstance * obj) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(obj->isVisitable()) - addVisitableObj(obj); -} - -//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight -//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes -void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - vstd::erase_if_present(visitableObjs, obj); - vstd::erase_if_present(alreadyVisited, obj); - - for(auto h : cb->getHeroesInfo()) - unreserveObject(h, obj); - - std::function checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool - { - if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum())) - return true; - else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal - return true; - else - return false; - }; - - //clear VCAI / main loop caches - vstd::erase_if(lockedHeroes, [&](const std::pair & x) -> bool - { - return checkRemovalValidity(x.second); - }); - - vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool - { - return checkRemovalValidity(x.first); - }); - - vstd::erase_if(basicGoals, checkRemovalValidity); - vstd::erase_if(goalsToAdd, checkRemovalValidity); - vstd::erase_if(goalsToRemove, checkRemovalValidity); - - for(auto goal : ultimateGoalsFromBasic) - vstd::erase_if(goal.second, checkRemovalValidity); - - //clear resource manager goal cache - ah->removeOutdatedObjectives(checkRemovalValidity); - - //TODO: Find better way to handle hero boat removal - if(auto hero = dynamic_cast(obj)) - { - if(hero->boat) - { - vstd::erase_if_present(visitableObjs, hero->boat); - vstd::erase_if_present(alreadyVisited, hero->boat); - - for(auto h : cb->getHeroesInfo()) - unreserveObject(h, hero->boat); - } - } - - //TODO - //there are other places where CGObjectinstance ptrs are stored... - // - - if(obj->ID == Obj::HERO && obj->tempOwner == playerID) - { - lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion - } -} - -void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - requestActionASAP([=]() - { - makePossibleUpgrades(visitor); - }); -} - -void VCAI::playerBonusChanged(const Bonus & bonus, bool gain) -{ - LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); - NET_EVENT_HANDLER; -} - -void VCAI::heroCreated(const CGHeroInstance * h) -{ - LOG_TRACE(logAi); - if(h->visitedTown) - townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown); - NET_EVENT_HANDLER; -} - -void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) -{ - LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); - NET_EVENT_HANDLER; -} - -void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) -{ - LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID); - NET_EVENT_HANDLER; -} - -void VCAI::requestRealized(PackageApplied * pa) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(status.haveTurn()) - { - if(pa->packType == typeList.getTypeID()) - { - if(pa->result) - status.madeTurn(); - } - } - - if(pa->packType == typeList.getTypeID()) - { - status.receivedAnswerConfirmation(pa->requestID, pa->result); - } -} - -void VCAI::receivedResource() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - status.addQuery(queryID, "UniversityWindow"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) -{ - LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val); - NET_EVENT_HANDLER; -} - -void VCAI::battleResultsApplied() -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - assert(status.getBattle() == ENDING_BATTLE); - status.setBattle(NO_BATTLE); -} - -void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop) -{ - -} - -void VCAI::objectPropertyChanged(const SetObjectProperty * sop) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - if(sop->what == ObjProperty::OWNER) - { - if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) - { - //we want to visit objects owned by oppponents - auto obj = myCb->getObj(sop->id, false); - if(obj) - { - addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set - vstd::erase_if_present(alreadyVisited, obj); - } - } - } -} - -void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) -{ - LOG_TRACE_PARAMS(logAi, "what '%i'", what); - NET_EVENT_HANDLER; - - if(town->getOwner() == playerID && what == 1) //built - completeGoal(sptr(Goals::BuildThis(buildingID, town))); -} - -void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) -{ - LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); - NET_EVENT_HANDLER; -} - -void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) -{ - LOG_TRACE(logAi); - NET_EVENT_HANDLER; - - status.addQuery(queryID, "MarketWindow"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) -{ - //TODO: AI support for ViewXXX spell - LOG_TRACE(logAi); - NET_EVENT_HANDLER; -} - -void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - LOG_TRACE(logAi); - env = ENV; - myCb = CB; - cbc = CB; - - ah->init(CB.get()); - - NET_EVENT_HANDLER; //sets ah->rm->cb - playerID = *myCb->getPlayerID(); - myCb->waitTillRealize = true; - myCb->unlockGsWhenWaiting = true; - - if(!fh) - fh = new FuzzyHelper(); - - retrieveVisitableObjs(); -} - -void VCAI::yourTurn(QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); - NET_EVENT_HANDLER; - status.addQuery(queryID, "YourTurn"); - requestActionASAP([=](){ answerQuery(queryID, 0); }); - status.startedTurn(); - makingTurn = std::make_unique(&VCAI::makeTurn, this); -} - -void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); - NET_EVENT_HANDLER; - status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level)); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); - NET_EVENT_HANDLER; - status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level)); - requestActionASAP([=](){ answerQuery(queryID, 0); }); -} - -void VCAI::showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) -{ - LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel); - NET_EVENT_HANDLER; - int sel = 0; - status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s") - % components.size() % text)); - - if(selection) //select from multiple components -> take the last one (they're indexed [1-size]) - sel = static_cast(components.size()); - - if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :) - sel = 1; - - requestActionASAP([=]() - { - answerQuery(askID, sel); - }); -} - -void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ -// LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits); - NET_EVENT_HANDLER; - status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") - % exits.size())); - - int choosenExit = -1; - if(impassable) - { - knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; - } - else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid()) - { - auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); - if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) - choosenExit = vstd::find_pos(exits, neededExit); - } - - for(auto exit : exits) - { - if(status.channelProbing() && exit.first == destinationTeleport) - { - choosenExit = vstd::find_pos(exits, exit); - break; - } - else - { - // TODO: Implement checking if visiting that teleport will uncovert any FoW - // So far this is the best option to handle decision about probing - auto obj = cb->getObj(exit.first, false); - if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first)) - { - if(exit.first != destinationTeleport) - teleportChannelProbingList.push_back(exit.first); - } - } - } - - requestActionASAP([=]() - { - answerQuery(askID, choosenExit); - }); -} - -void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) -{ - LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); - NET_EVENT_HANDLER; - - std::string s1 = up ? up->nodeName() : "NONE"; - std::string s2 = down ? down->nodeName() : "NONE"; - - status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); - - //you can't request action from action-response thread - requestActionASAP([=]() - { - if(removableUnits) - pickBestCreatures(down, up); - - answerQuery(queryID, 0); - }); -} - -void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - NET_EVENT_HANDLER; - status.addQuery(askID, "Map object select query"); - requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); -} - -void VCAI::saveGame(BinarySerializer & h, const int version) -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - NET_EVENT_HANDLER; - validateVisitableObjs(); - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::saveGame(h, version); - serializeInternal(h, version); -} - -void VCAI::loadGame(BinaryDeserializer & h, const int version) -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - //NET_EVENT_HANDLER; - - #if 0 - //disabled due to issue 2890 - registerGoals(h); - #endif // 0 - CAdventureAI::loadGame(h, version); - serializeInternal(h, version); -} - -void makePossibleUpgrades(const CArmedInstance * obj) -{ - if(!obj) - return; - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) - { - UpgradeInfo ui; - cb->fillUpgradeInfo(obj, SlotID(i), ui); - if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) - { - cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); - } - } - } -} - -void VCAI::makeTurn() -{ - MAKING_TURN; - - auto day = cb->getDate(Date::DAY); - logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); - - boost::shared_lock gsLock(CGameState::mutex); - setThreadName("VCAI::makeTurn"); - - switch(cb->getDate(Date::DAY_OF_WEEK)) - { - case 1: - { - townVisitsThisWeek.clear(); - std::vector objs; - retrieveVisitableObjs(objs, true); - for(const CGObjectInstance * obj : objs) - { - if(isWeeklyRevisitable(obj)) - { - addVisitableObj(obj); - vstd::erase_if_present(alreadyVisited, obj); - } - } - break; - } - } - markHeroAbleToExplore(primaryHero()); - visitedHeroes.clear(); - - try - { - //it looks messy here, but it's better to have armed heroes before attempting realizing goals - for (const CGTownInstance * t : cb->getTownsInfo()) - moveCreaturesToHero(t); - - mainLoop(); - - /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. - Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/ - performTypicalActions(); - - //for debug purpose - for (auto h : cb->getHeroesInfo()) - { - if (h->movementPointsRemaining()) - logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); - } - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); - return; - } - catch (std::exception & e) - { - logAi->debug("Making turn thread has caught an exception: %s", e.what()); - } - - endTurn(); -} - -std::vector VCAI::getMyHeroes() const -{ - std::vector ret; - - for(auto h : cb->getHeroesInfo()) - { - ret.push_back(h); - } - - return ret; -} - -void VCAI::mainLoop() -{ - std::vector elementarGoals; //no duplicates allowed (operator ==) - basicGoals.clear(); - - validateVisitableObjs(); - - //get all potential and saved goals - //TODO: not lose - basicGoals.push_back(sptr(Goals::Win())); - for (auto goalPair : lockedHeroes) - { - fh->setPriority(goalPair.second); //re-evaluate, as heroes moved in the meantime - basicGoals.push_back(goalPair.second); - } - if (ah->hasTasksLeft()) - basicGoals.push_back(ah->whatToDo()); - for (auto quest : myCb->getMyQuests()) - { - basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); - } - basicGoals.push_back(sptr(Goals::Build())); - - invalidPathHeroes.clear(); - - for (int pass = 0; pass< 30 && basicGoals.size(); pass++) - { - vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice - goalsToAdd.clear(); - goalsToRemove.clear(); - elementarGoals.clear(); - ultimateGoalsFromBasic.clear(); - - ah->updatePaths(getMyHeroes()); - - logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size()); - - for (auto basicGoal : basicGoals) - { - logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name()); - - auto goalToDecompose = basicGoal; - Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); - int maxAbstractGoals = 10; - while (!elementarGoal->isElementar && maxAbstractGoals) - { - try - { - elementarGoal = decomposeGoal(goalToDecompose); - } - catch (goalFulfilledException & e) - { - //it is impossible to continue some goals (like exploration, for example) - //complete abstract goal for now, but maybe main goal finds another path - logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); - completeGoal(e.goal); //put in goalsToRemove - break; - } - catch(cannotFulfillGoalException & e) - { - //it is impossible to continue some goals (like exploration, for example) - //complete abstract goal for now, but maybe main goal finds another path - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what()); - break; - } - catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree - { - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); - break; - } - if (elementarGoal->isAbstract) //we can decompose it further - { - goalsToAdd.push_back(elementarGoal); - //decompose further now - this is necesssary if we can't add over 10 goals in the pool - goalToDecompose = elementarGoal; - //there is a risk of infinite abstract goal loop, though it indicates failed logic - maxAbstractGoals--; - } - else if (elementarGoal->isElementar) //should be - { - logAi->debug("Found elementar goal %s", elementarGoal->name()); - elementarGoals.push_back(elementarGoal); - ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? - break; - } - else //should never be here - throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); - } - } - - logAi->trace("Main loop: selecting best elementar goal"); - - //now choose one elementar goal to realize - Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector - Goals::TSubgoal goalToRealize = sptr(Goals::Invalid()); - while (possibleGoals.size()) - { - //allow assign goals to heroes with 0 movement, but don't realize them - //maybe there are beter ones left - - auto bestGoal = fh->chooseSolution(possibleGoals); - if (bestGoal->hero) //lock this hero to fulfill goal - { - setGoal(bestGoal->hero, bestGoal); - if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero)) - { - if (!vstd::erase_if_present(possibleGoals, bestGoal)) - { - logAi->error("erase_if_preset failed? Something very wrong!"); - break; - } - continue; //chose next from the list - } - } - goalToRealize = bestGoal; //we found our goal to execute - break; - } - - //realize best goal - if (!goalToRealize->invalid()) - { - logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority); - - try - { - boost::this_thread::interruption_point(); - goalToRealize->accept(this); //visitor pattern - boost::this_thread::interruption_point(); - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Player %d: Making turn thread received an interruption!", playerID); - throw; //rethrow, we want to truly end this thread - } - catch (goalFulfilledException & e) - { - //the sub-goal was completed successfully - completeGoal(e.goal); - //local goal was also completed? - completeGoal(goalToRealize); - - // remove abstract visit tile if we completed the elementar one - vstd::erase_if_present(goalsToAdd, goalToRealize); - } - catch (std::exception & e) - { - logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name()); - logAi->debug("The error message was: %s", e.what()); - - //erase base goal if we failed to execute decomposed goal - for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize]) - goalsToRemove.push_back(basicGoal); - - // sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn. - ai->ah->notifyGoalCompleted(goalToRealize); - - //we failed to realize best goal, but maybe others are still possible? - } - - //remove goals we couldn't decompose - for (auto goal : goalsToRemove) - vstd::erase_if_present(basicGoals, goal); - - //add abstract goals - boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool - { - return lhs->priority > rhs->priority; //highest priority at the beginning - }); - - //max number of goals = 10 - int i = 0; - while (basicGoals.size() < 10 && goalsToAdd.size() > i) - { - if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates - basicGoals.push_back(goalsToAdd[i]); - i++; - } - } - else //no elementar goals possible - { - logAi->debug("Goal decomposition exhausted"); - break; - } - } -} - -void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) -{ - LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); - switch(obj->ID) - { - case Obj::TOWN: - moveCreaturesToHero(dynamic_cast(obj)); - if(h->visitedTown) //we are inside, not just attacking - { - townVisitsThisWeek[h].insert(h->visitedTown); - if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) - { - if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) - cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); - } - } - break; - } - completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h))); -} - -void VCAI::moveCreaturesToHero(const CGTownInstance * t) -{ - if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner) - { - pickBestCreatures(t->visitingHero, t); - } -} - -void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source) -{ - const CArmedInstance * armies[] = {destinationArmy, source}; - - auto bestArmy = ah->getSortedSlots(destinationArmy, source); - - //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs - for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot - { - const CCreature * targetCreature = bestArmy[i.getNum()].creature; - - for(auto armyPtr : armies) - { - for(SlotID j = SlotID(0); j.validSlot(); j.advance(1)) - { - if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT - { - //can't take away last creature without split. generate a new stack with 1 creature which is weak but fast - if(armyPtr == source - && source->needsLastStack() - && source->stacksCount() == 1 - && (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature)) - { - auto weakest = ah->getWeakestCreature(bestArmy); - - if(weakest->creature == targetCreature) - { - if(1 == source->getStackCount(j)) - break; - - // move all except 1 of weakest creature from source to destination - cb->splitStack( - source, - destinationArmy, - j, - destinationArmy->getSlotFor(targetCreature), - destinationArmy->getStackCount(i) + source->getStackCount(j) - 1); - - break; - } - else - { - // Source last stack is not weakest. Move 1 of weakest creature from destination to source - cb->splitStack( - destinationArmy, - source, - destinationArmy->getSlotFor(weakest->creature), - source->getFreeSlot(), - 1); - } - } - - cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i); - } - } - } - } - - //TODO - having now strongest possible army, we may want to think about arranging stacks - - auto hero = dynamic_cast(destinationArmy); - if(hero) - checkHeroArmy(hero); -} - -void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other) -{ - auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void - { - bool changeMade = false; - do - { - changeMade = false; - - //we collect gear always in same order - std::vector allArtifacts; - if(giveStuffToFirstHero) - { - for(auto p : h->artifactsWorn) - { - if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); - } - } - for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); - - if(otherh) - { - for(auto p : otherh->artifactsWorn) - { - if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); - } - for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); - } - //we give stuff to one hero or another, depending on giveStuffToFirstHero - - const CGHeroInstance * target = nullptr; - if(giveStuffToFirstHero || !otherh) - target = h; - else - target = otherh; - - for(auto location : allArtifacts) - { - if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) - continue; // don't attempt to move catapult and spellbook - - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) - continue; //don't reequip artifact we already wear - - auto s = location.getSlot(); - if(!s || s->locked) //we can't move locks - continue; - auto artifact = s->artifact; - if(!artifact) - continue; - //FIXME: why are the above possible to be null? - - bool emptySlotFound = false; - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) - { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move - { - cb->swapArtifacts(location, destLocation); //just put into empty slot - emptySlotFound = true; - changeMade = true; - break; - } - } - if(!emptySlotFound) //try to put that atifact in already occupied slot - { - for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) - { - auto otherSlot = target->getSlot(slot); - if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one - { - ArtifactLocation destLocation(target, slot); - //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move - { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); - changeMade = true; - break; - } - } - } - } - if(changeMade) - break; //start evaluating artifacts from scratch - } - } - while(changeMade); - }; - - equipBest(h, other, true); - - if(other) - equipBest(h, other, false); -} - -void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) -{ - //now used only for visited dwellings / towns, not BuyArmy goal - for(int i = 0; i < d->creatures.size(); i++) - { - if(!d->creatures[i].second.size()) - continue; - - int count = d->creatures[i].first; - CreatureID creID = d->creatures[i].second.back(); - - vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost()); - if(count > 0) - cb->recruitCreatures(d, recruiter, creID, count, i); - } -} - -bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit) -{ - int3 op = obj->visitablePos(); - auto paths = ah->getPathsToTile(h, op); - - for(const auto & path : paths) - { - if(movementCostLimit && movementCostLimit.value() < path.movementCost()) - return false; - - if(isGoodForVisit(obj, h, path)) - return true; - } - - return false; -} - -bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const -{ - const int3 pos = obj->visitablePos(); - const int3 targetPos = path.firstTileToGet(); - if (!targetPos.valid()) - return false; - if (!isTileNotReserved(h.get(), targetPos)) - return false; - if (obj->wasVisited(playerID)) - return false; - if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj)) - return false; // Otherwise we flag or get weekly resources / creatures - if (!isSafeToVisit(h, pos)) - return false; - if (!shouldVisit(h, obj)) - return false; - if (vstd::contains(alreadyVisited, obj)) - return false; - if (vstd::contains(reservedObjs, obj)) - return false; - - // TODO: looks extra if we already have AIPath - //if (!isAccessibleForHero(targetPos, h)) - // return false; - - const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj - //we don't try visiting object on which allied or owned hero stands - // -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited - return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met -} - -bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const -{ - if(t.valid()) - { - auto obj = cb->getTopObj(t); - if(obj && vstd::contains(ai->reservedObjs, obj) - && vstd::contains(reservedHeroesMap, h) - && !vstd::contains(reservedHeroesMap.at(h), obj)) - return false; //do not capture object reserved by another hero - else - return true; - } - else - { - return false; - } -} - -bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const -{ - //TODO: make gathering gold, building tavern or conquering town (?) possible subgoals - if(!t) - t = findTownWithTavern(); - if(!t) - return false; - if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager - return false; - if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) - return false; - if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) - return false; - if(!cb->getAvailableHeroes(t).size()) - return false; - - return true; -} - -void VCAI::wander(HeroPtr h) -{ - auto visitTownIfAny = [this](HeroPtr h) -> bool - { - if (h->visitedTown) - { - townVisitsThisWeek[h].insert(h->visitedTown); - buildArmyIn(h->visitedTown); - return true; - } - return false; - }; - - //unclaim objects that are now dangerous for us - auto reservedObjsSetCopy = reservedHeroesMap[h]; - for(auto obj : reservedObjsSetCopy) - { - if(!isSafeToVisit(h, obj->visitablePos())) - unreserveObject(h, obj); - } - - TimeCheck tc("looking for wander destination"); - - for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) - { - validateVisitableObjs(); - ah->updatePaths(getMyHeroes()); - - std::vector dests; - - //also visit our reserved objects - but they are not prioritized to avoid running back and forth - vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool - { - return ah->isTileAccessible(h, obj->visitablePos()); - }); - - int pass = 0; - std::vector> distanceLimits = {1.0, 2.0, std::nullopt}; - - while(!dests.size() && pass < distanceLimits.size()) - { - auto & distanceLimit = distanceLimits[pass]; - - logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0)); - - vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool - { - return isGoodForVisit(obj, h, distanceLimit); - }); - - pass++; - } - - if(!dests.size()) - { - logAi->debug("Looking for town destination"); - - if(cb->getVisitableObjs(h->visitablePos()).size() > 1) - moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate - - auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool - { - const CGHeroInstance * hptr = h.get(); - auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs), - r2 = ah->howManyReinforcementsCanGet(hptr, rhs); - if (r1 != r2) - return r1 < r2; - else - return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs); - }; - - std::vector townsReachable; - std::vector townsNotReachable; - for(const CGTownInstance * t : cb->getTownsInfo()) - { - if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t)) - { - if(isAccessibleForHero(t->visitablePos(), h)) - townsReachable.push_back(t); - else - townsNotReachable.push_back(t); - } - } - if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing - { - dests.push_back(*boost::max_element(townsReachable, compareReinforcements)); - } - else if(townsNotReachable.size()) - { - //TODO pick the truly best - const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements); - logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString()); - int3 pos1 = h->pos; - striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop - //if out hero is stuck, we may need to request another hero to clear the way we see - - if(pos1 == h->pos && h == primaryHero()) //hero can't move - { - if(canRecruitAnyHero(t)) - recruitHero(t); - } - break; - } - else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST) - { - std::vector towns = cb->getTownsInfo(); - vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool - { - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t)) - return true; - } - return false; - }); - if (towns.size()) - { - recruitHero(*boost::max_element(towns, compareArmyStrength)); - } - break; - } - else - { - logAi->debug("Nowhere more to go..."); - break; - } - } - //end of objs empty - - if(dests.size()) //performance improvement - { - Goals::TGoalVec targetObjectGoals; - for(auto destination : dests) - { - vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false)); - } - - if(targetObjectGoals.size()) - { - auto bestObjectGoal = fh->chooseSolution(targetObjectGoals); - - //wander should not cause heroes to be reserved - they are always considered free - if(bestObjectGoal->goalType == Goals::VISIT_OBJ) - { - auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid)); - if(chosenObject != nullptr) - logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString()); - } - else - logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); - - try - { - decomposeGoal(bestObjectGoal)->accept(this); - } - catch(const goalFulfilledException & e) - { - if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) - continue; - - throw e; - } - } - else - { - logAi->debug("Nowhere more to go..."); - break; - } - - visitTownIfAny(h); - } - } - - visitTownIfAny(h); //in case hero is just sitting in town -} - -void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) -{ - if(goal->invalid()) - { - vstd::erase_if_present(lockedHeroes, h); - } - else - { - lockedHeroes[h] = goal; - goal->setisElementar(false); //Force always evaluate goals before realizing - } -} -void VCAI::evaluateGoal(HeroPtr h) -{ - if(vstd::contains(lockedHeroes, h)) - fh->setPriority(lockedHeroes[h]); -} - -void VCAI::completeGoal(Goals::TSubgoal goal) -{ - if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won - return; - - logAi->debug("Completing goal: %s", goal->name()); - - //notify Managers - ah->notifyGoalCompleted(goal); - //notify mainLoop() - goalsToRemove.push_back(goal); //will be removed from mainLoop() goals - for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals - { - if (basicGoal->fulfillsMe(goal)) - goalsToRemove.push_back(basicGoal); - } - - //unreserve heroes - if(const CGHeroInstance * h = goal->hero.get(true)) - { - auto it = lockedHeroes.find(h); - if(it != lockedHeroes.end()) - { - if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete - { - logAi->debug(goal->completeMessage()); - lockedHeroes.erase(it); //goal fulfilled, free hero - } - } - } - else //complete goal for all heroes maybe? - { - vstd::erase_if(lockedHeroes, [goal](std::pair p) - { - if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance - { - logAi->debug(p.second->completeMessage()); - return true; - } - return false; - }); - } - -} - -void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) -{ - NET_EVENT_HANDLER; - assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); - status.setBattle(ONGOING_BATTLE); - const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit - battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); - CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); -} - -void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) -{ - NET_EVENT_HANDLER; - assert(status.getBattle() == ONGOING_BATTLE); - status.setBattle(ENDING_BATTLE); - bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); - logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); - battlename.clear(); - - if (queryID != QueryID::NONE) - { - status.addQuery(queryID, "Combat result dialog"); - const int confirmAction = 0; - requestActionASAP([=]() - { - answerQuery(queryID, confirmAction); - }); - } - CAdventureAI::battleEnd(battleID, br, queryID); -} - -void VCAI::waitTillFree() -{ - auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex); - status.waitTillFree(); -} - -void VCAI::markObjectVisited(const CGObjectInstance * obj) -{ - if(!obj) - return; - - if(const auto * rewardable = dynamic_cast(obj)) //we may want to visit it with another hero - { - if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero - return; - - if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time - return; - } - - if(obj->ID == Obj::MONSTER) - return; - - alreadyVisited.insert(obj); -} - -void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj) -{ - reservedObjs.insert(obj); - reservedHeroesMap[h].insert(obj); - logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName()); -} - -void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj) -{ - vstd::erase_if_present(reservedObjs, obj); //unreserve objects - vstd::erase_if_present(reservedHeroesMap[h], obj); -} - -void VCAI::markHeroUnableToExplore(HeroPtr h) -{ - heroesUnableToExplore.insert(h); -} -void VCAI::markHeroAbleToExplore(HeroPtr h) -{ - vstd::erase_if_present(heroesUnableToExplore, h); -} -bool VCAI::isAbleToExplore(HeroPtr h) -{ - return !vstd::contains(heroesUnableToExplore, h); -} -void VCAI::clearPathsInfo() -{ - heroesUnableToExplore.clear(); -} - -void VCAI::validateVisitableObjs() -{ - std::string errorMsg; - auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool - { - if(obj) - return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility - else - return true; - }; - - //errorMsg is captured by ref so lambda will take the new text - errorMsg = " shouldn't be on the visitable objects list!"; - vstd::erase_if(visitableObjs, shouldBeErased); - - //FIXME: how comes our own heroes become inaccessible? - vstd::erase_if(reservedHeroesMap, [](std::pair> hp) -> bool - { - return !hp.first.get(true); - }); - for(auto & p : reservedHeroesMap) - { - errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!"; - vstd::erase_if(p.second, shouldBeErased); - } - - errorMsg = " shouldn't be on the reserved objs list!"; - vstd::erase_if(reservedObjs, shouldBeErased); - - //TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game. - errorMsg = " shouldn't be on the already visited objs list!"; - vstd::erase_if(alreadyVisited, shouldBeErased); -} - -void VCAI::retrieveVisitableObjs(std::vector & out, bool includeOwned) const -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(includeOwned || obj->tempOwner != playerID) - out.push_back(obj); - } - }); -} - -void VCAI::retrieveVisitableObjs() -{ - foreach_tile_pos([&](const int3 & pos) - { - for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) - { - if(obj->tempOwner != playerID) - addVisitableObj(obj); - } - }); -} - -std::vector VCAI::getFlaggedObjects() const -{ - std::vector ret; - for(const CGObjectInstance * obj : visitableObjs) - { - if(obj->tempOwner == playerID) - ret.push_back(obj); - } - return ret; -} - -void VCAI::addVisitableObj(const CGObjectInstance * obj) -{ - if(obj->ID == Obj::EVENT) - return; - - visitableObjs.insert(obj); - - // All teleport objects seen automatically assigned to appropriate channels - auto teleportObj = dynamic_cast(obj); - if(teleportObj) - CGTeleport::addToChannel(knownTeleportChannels, teleportObj); -} - -const CGObjectInstance * VCAI::lookForArt(int aid) const -{ - for(const CGObjectInstance * obj : ai->visitableObjs) - { - if(obj->ID == Obj::ARTIFACT && obj->subID == aid) - return obj; - } - - return nullptr; - - //TODO what if more than one artifact is available? return them all or some slection criteria -} - -bool VCAI::isAccessible(const int3 & pos) const -{ - //TODO precalculate for speed - - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(isAccessibleForHero(pos, h)) - return true; - } - - return false; -} - -HeroPtr VCAI::getHeroWithGrail() const -{ - for(const CGHeroInstance * h : cb->getHeroesInfo()) - { - if(h->hasArt(ArtifactID::GRAIL)) - return h; - } - return nullptr; -} - -const CGObjectInstance * VCAI::getUnvisitedObj(const std::function & predicate) -{ - //TODO smarter definition of unvisited - for(const CGObjectInstance * obj : visitableObjs) - { - if(predicate(obj) && !vstd::contains(alreadyVisited, obj)) - return obj; - } - return nullptr; -} - -bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const -{ - // Don't visit tile occupied by allied hero - if(!includeAllies) - { - for(auto obj : cb->getVisitableObjs(pos)) - { - if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES) - { - if(obj != h.get()) - return false; - } - } - } - return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); -} - -bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) -{ - //TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj() - - auto afterMovementCheck = [&]() -> void - { - waitTillFree(); //movement may cause battle or blocking dialog - if(!h) - { - lostHero(h); - teleportChannelProbingList.clear(); - if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off - status.setChannelProbing(false); - throw cannotFulfillGoalException("Hero was lost!"); - } - }; - - logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString()); - int3 startHpos = h->visitablePos(); - bool ret = false; - if(startHpos == dst) - { - //FIXME: this assertion fails also if AI moves onto defeated guarded object - assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object - cb->moveHero(*h, h->convertFromVisitablePos(dst)); - afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? - // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared - teleportChannelProbingList.clear(); - // not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information. - ret = true; - } - else - { - CGPath path; - cb->getPathsInfo(h.get())->getPath(path, dst); - if(path.nodes.empty()) - { - logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString()); - throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h))); - } - int i = (int)path.nodes.size() - 1; - - auto getObj = [&](int3 coord, bool ignoreHero) - { - auto tile = cb->getTile(coord, false); - assert(tile); - return tile->topVisitableObj(ignoreHero); - //return cb->getTile(coord,false)->topVisitableObj(ignoreHero); - }; - - auto isTeleportAction = [&](EPathNodeAction action) -> bool - { - if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT) - { - if(action != EPathNodeAction::TELEPORT_BATTLE) - { - return false; - } - } - - return true; - }; - - auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * - { - if(CGTeleport::isConnected(currentObject, nextObjectTop)) - return nextObjectTop; - if(nextObjectTop && nextObjectTop->ID == Obj::HERO) - { - if(CGTeleport::isConnected(currentObject, nextObject)) - return nextObject; - } - - return nullptr; - }; - - auto doMovement = [&](int3 dst, bool transit) - { - cb->moveHero(*h, h->convertFromVisitablePos(dst), transit); - }; - - auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) - { - destinationTeleport = exitId; - if(exitPos.valid()) - destinationTeleportPos = h->convertFromVisitablePos(exitPos); - cb->moveHero(*h, h->pos); - destinationTeleport = ObjectInstanceID(); - destinationTeleportPos = int3(-1); - afterMovementCheck(); - }; - - auto doChannelProbing = [&]() -> void - { - auto currentPos = h->visitablePos(); - auto currentExit = getObj(currentPos, true)->id; - - status.setChannelProbing(true); - for(auto exit : teleportChannelProbingList) - doTeleportMovement(exit, int3(-1)); - teleportChannelProbingList.clear(); - status.setChannelProbing(false); - - doTeleportMovement(currentExit, currentPos); - }; - - for(; i > 0; i--) - { - int3 currentCoord = path.nodes[i].coord; - int3 nextCoord = path.nodes[i - 1].coord; - - auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos()); - auto nextObjectTop = getObj(nextCoord, false); - auto nextObject = getObj(nextCoord, true); - auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); - if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr) - { - //we use special login if hero standing on teleporter it's mean we need - doTeleportMovement(destTeleportObj->id, nextCoord); - if(teleportChannelProbingList.size()) - doChannelProbing(); - markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited - - continue; - } - - //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) - if(path.nodes[i - 1].turns) - { - //blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs - break; - } - - int3 endpos = path.nodes[i - 1].coord; - if(endpos == h->visitablePos()) - continue; - - bool isConnected = false; - bool isNextObjectTeleport = false; - // Check there is node after next one; otherwise transit is pointless - if(i - 2 >= 0) - { - isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false)); - isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop); - } - if(isConnected || isNextObjectTeleport) - { - // Hero should be able to go through object if it's allow transit - doMovement(endpos, true); - } - else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR) - { - doMovement(endpos, true); - } - else - { - doMovement(endpos, false); - } - - afterMovementCheck(); - - if(teleportChannelProbingList.size()) - doChannelProbing(); - } - - if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT) - { - ret = h && i == 0; // when we take resource we do not reach its position. We even might not move - } - } - if(h) - { - if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting - { - if(visitedObject != *h) - performObjectInteraction(visitedObject, h); - } - } - if(h) //we could have lost hero after last move - { - completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway - completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h))); - - ret = ret || (dst == h->visitablePos()); - - if(!ret) //reserve object we are heading towards - { - auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst)); - if(obj && obj != *h) - reserveObject(h, obj); - } - - if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target - { - vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move - invalidPathHeroes.insert(h); - throw cannotFulfillGoalException("Invalid path found!"); - } - evaluateGoal(h); //new hero position means new game situation - logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret); - } - return ret; -} - -void VCAI::buildStructure(const CGTownInstance * t, BuildingID building) -{ - auto name = t->town->buildings.at(building)->getNameTranslated(); - logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString()); - cb->buildBuilding(t, building); //just do this; -} - -void VCAI::tryRealize(Goals::Explore & g) -{ - throw cannotFulfillGoalException("EXPLORE is not an elementar goal!"); -} - -void VCAI::tryRealize(Goals::RecruitHero & g) -{ - if(const CGTownInstance * t = findTownWithTavern()) - { - recruitHero(t, true); - //TODO try to free way to blocked town - //TODO: adventure map tavern or prison? - } - else - { - throw cannotFulfillGoalException("No town to recruit hero!"); - } -} - -void VCAI::tryRealize(Goals::VisitTile & g) -{ - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!"); - if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) - { - logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); - throw goalFulfilledException(sptr(g)); - } - if(ai->moveHeroToTile(g.tile, g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } -} - -void VCAI::tryRealize(Goals::VisitObj & g) -{ - auto position = g.tile; - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!"); - if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) - { - logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); - throw goalFulfilledException(sptr(g)); - } - if(ai->moveHeroToTile(position, g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } -} - -void VCAI::tryRealize(Goals::VisitHero & g) -{ - if(!g.hero->movementPointsRemaining()) - throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!"); - - const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid)); - if(obj) - { - if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get())) - { - throw goalFulfilledException(sptr(g)); - } - } - else - { - throw cannotFulfillGoalException("Cannot visit hero: object not found!"); - } -} - -void VCAI::tryRealize(Goals::BuildThis & g) -{ - auto b = BuildingID(g.bid); - auto t = g.town; - - if (t) - { - if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED) - { - logAi->debug("Player %d will build %s in town of %s at %s", - playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString()); - cb->buildBuilding(t, b); - throw goalFulfilledException(sptr(g)); - } - } - throw cannotFulfillGoalException("Cannot build a given structure!"); -} - -void VCAI::tryRealize(Goals::DigAtTile & g) -{ - assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? - if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) - { - cb->dig(g.hero.get()); - completeGoal(sptr(g)); // finished digging - } - else - { - ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else - throw cannotFulfillGoalException("A hero can't dig!\n"); - } -} - -void VCAI::tryRealize(Goals::Trade & g) //trade -{ - if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway? - throw goalFulfilledException(sptr(g)); - - int accquiredResources = 0; - if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) - { - if(const IMarket * m = IMarket::castFrom(obj, false)) - { - auto freeRes = ah->freeResources(); //trade only resources which are not reserved - for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) - { - auto res = it->resType; - if(res.getNum() == g.resID) //sell any other resource - continue; - - int toGive, toGet; - m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); - toGive = static_cast(toGive * (it->resVal / toGive)); //round down - //TODO trade only as much as needed - if (toGive) //don't try to sell 0 resources - { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); - accquiredResources = static_cast(toGet * (it->resVal / toGive)); - logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); - } - if (ah->freeResources()[g.resID] >= g.value) - throw goalFulfilledException(sptr(g)); //we traded all we needed - } - - throw cannotFulfillGoalException("I cannot get needed resources by trade!"); - } - else - { - throw cannotFulfillGoalException("I don't know how to use this object to raise resources!"); - } - } - else - { - throw cannotFulfillGoalException("No object that could be used to raise resources!"); - } -} - -void VCAI::tryRealize(Goals::BuyArmy & g) -{ - auto t = g.town; - - ui64 valueBought = 0; - //buy the stacks with largest AI value - - makePossibleUpgrades(t); - - while (valueBought < g.value) - { - auto res = ah->allResources(); - std::vector creaturesInDwellings; - - for (int i = 0; i < t->creatures.size(); i++) - { - auto ci = infoFromDC(t->creatures[i]); - - if(!ci.count - || ci.creID == CreatureID::NONE - || (g.objid != -1 && ci.creID.getNum() != g.objid) - || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) - continue; - - vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford - - if(!ci.count) - continue; - - ci.level = i; //this is important for Dungeon Summoning Portal - creaturesInDwellings.push_back(ci); - } - - if (creaturesInDwellings.empty()) - throw cannotFulfillGoalException("Can't buy any more creatures!"); - - creInfo ci = - *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) - { - //max value of creatures we can buy with our res - int value1 = lhs.cre->getAIValue() * lhs.count, - value2 = rhs.cre->getAIValue() * rhs.count; - - return value1 < value2; - }); - - - cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); - valueBought += ci.count * ci.cre->getAIValue(); - } - - throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted -} - -void VCAI::tryRealize(Goals::Invalid & g) -{ - throw cannotFulfillGoalException("I don't know how to fulfill this!"); -} - -void VCAI::tryRealize(Goals::AbstractGoal & g) -{ - logAi->debug("Attempting realizing goal with code %s", g.name()); - throw cannotFulfillGoalException("Unknown type of goal !"); -} - -const CGTownInstance * VCAI::findTownWithTavern() const -{ - for(const CGTownInstance * t : cb->getTownsInfo()) - if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero) - return t; - - return nullptr; -} - -Goals::TSubgoal VCAI::getGoal(HeroPtr h) const -{ - auto it = lockedHeroes.find(h); - if(it != lockedHeroes.end()) - return it->second; - else - return sptr(Goals::Invalid()); -} - - -std::vector VCAI::getUnblockedHeroes() const -{ - std::vector ret; - for(auto h : cb->getHeroesInfo()) - { - //&& !vstd::contains(lockedHeroes, h) - //at this point we assume heroes exhausted their locked goals - if(canAct(h)) - ret.push_back(h); - } - return ret; -} - -bool VCAI::canAct(HeroPtr h) const -{ - auto mission = lockedHeroes.find(h); - if(mission != lockedHeroes.end()) - { - //FIXME: I'm afraid there can be other conditions when heroes can act but not move :? - if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar) - return false; - } - - return h->movementPointsRemaining(); -} - -HeroPtr VCAI::primaryHero() const -{ - auto hs = cb->getHeroesInfo(); - if (hs.empty()) - return nullptr; - else - return *boost::max_element(hs, compareHeroStrength); -} - -void VCAI::endTurn() -{ - logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); - if(!status.haveTurn()) - { - logAi->error("Not having turn at the end of turn???"); - } - logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); - do - { - cb->endTurn(); - } - while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over - - logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); -} - -void VCAI::striveToGoal(Goals::TSubgoal basicGoal) -{ - //TODO: this function is deprecated and should be dropped altogether - - auto goalToDecompose = basicGoal; - Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); - int maxAbstractGoals = 10; - while (!elementarGoal->isElementar && maxAbstractGoals) - { - try - { - elementarGoal = decomposeGoal(goalToDecompose); - } - catch (goalFulfilledException & e) - { - //it is impossible to continue some goals (like exploration, for example) - completeGoal(e.goal); //put in goalsToRemove - logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); - return; - } - catch (std::exception & e) - { - goalsToRemove.push_back(basicGoal); - logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); - return; - } - if (elementarGoal->isAbstract) //we can decompose it further - { - goalsToAdd.push_back(elementarGoal); - //decompose further now - this is necesssary if we can't add over 10 goals in the pool - goalToDecompose = elementarGoal; - //there is a risk of infinite abstract goal loop, though it indicates failed logic - maxAbstractGoals--; - } - else if (elementarGoal->isElementar) //should be - { - logAi->debug("Found elementar goal %s", elementarGoal->name()); - ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? - break; - } - else //should never be here - throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); - } - - //realize best goal - if (!elementarGoal->invalid()) - { - logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority); - - try - { - boost::this_thread::interruption_point(); - elementarGoal->accept(this); //visitor pattern - boost::this_thread::interruption_point(); - } - catch (boost::thread_interrupted & e) - { - (void)e; - logAi->debug("Player %d: Making turn thread received an interruption!", playerID); - throw; //rethrow, we want to truly end this thread - } - catch (goalFulfilledException & e) - { - //the sub-goal was completed successfully - completeGoal(e.goal); - //local goal was also completed - completeGoal(elementarGoal); - } - catch (std::exception & e) - { - logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name()); - logAi->debug("The error message was: %s", e.what()); - - //erase base goal if we failed to execute decomposed goal - for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal]) - goalsToRemove.push_back(basicGoalToRemove); - } - } -} - -Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) -{ - if(ultimateGoal->isElementar) - { - logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name()); - - return ultimateGoal; - } - - const int searchDepth = 30; - - Goals::TSubgoal goal = ultimateGoal; - logAi->debug("Decomposing goal %s", ultimateGoal->name()); - int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals - while (maxGoals) - { - boost::this_thread::interruption_point(); - - goal = goal->whatToDoToAchieve(); //may throw if decomposition fails - --maxGoals; - if (goal == ultimateGoal) //compare objects by value - if (goal->isElementar == ultimateGoal->isElementar) - throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!") - % ultimateGoal->name()).str()); - if (goal->isAbstract || goal->isElementar) - return goal; - else - logAi->debug("Considering: %s", goal->name()); - } - - throw cannotFulfillGoalException("Too many subgoals, don't know what to do"); -} - -void VCAI::performTypicalActions() -{ - for(auto h : getUnblockedHeroes()) - { - if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn - continue; - - logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining()); - makePossibleUpgrades(*h); - pickBestArtifacts(*h); - try - { - wander(h); - } - catch(std::exception & e) - { - logAi->debug("Cannot use this hero anymore, received exception: %s", e.what()); - continue; - } - } -} - -void VCAI::buildArmyIn(const CGTownInstance * t) -{ - makePossibleUpgrades(t->visitingHero); - makePossibleUpgrades(t); - recruitCreatures(t, t->getUpperArmy()); - moveCreaturesToHero(t); -} - -void VCAI::checkHeroArmy(HeroPtr h) -{ - auto it = lockedHeroes.find(h); - if(it != lockedHeroes.end()) - { - if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength()) - completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h))); - } -} - -void VCAI::recruitHero(const CGTownInstance * t, bool throwing) -{ - logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString()); - - auto heroes = cb->getAvailableHeroes(t); - if(heroes.size()) - { - auto hero = heroes[0]; - if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week - { - if(heroes[1]->getTotalStrength() > hero->getTotalStrength()) - hero = heroes[1]; - } - cb->recruitHero(t, hero); - throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); - } - else if(throwing) - { - throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); - } -} - -void VCAI::finish() -{ - //we want to lock to avoid multiple threads from calling makingTurn->join() at same time - boost::lock_guard multipleCleanupGuard(turnInterruptionMutex); - if(makingTurn) - { - makingTurn->interrupt(); - makingTurn->join(); - makingTurn.reset(); - } -} - -void VCAI::requestActionASAP(std::function whatToDo) -{ - boost::thread newThread([this, whatToDo]() - { - setThreadName("VCAI::requestActionASAP::whatToDo"); - SET_GLOBAL_STATE(this); - boost::shared_lock gsLock(CGameState::mutex); - whatToDo(); - }); - - newThread.detach(); -} - -void VCAI::lostHero(HeroPtr h) -{ - logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name); - - vstd::erase_if_present(lockedHeroes, h); - for(auto obj : reservedHeroesMap[h]) - { - vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero - } - vstd::erase_if_present(reservedHeroesMap, h); - vstd::erase_if_present(visitedHeroes, h); - for (auto heroVec : visitedHeroes) - { - vstd::erase_if_present(heroVec.second, h); - } - - //remove goals with removed hero assigned from main loop - vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool - { - if(x.first->hero == h) - return true; - else - return false; - }); - - auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool - { - if(x->hero == h) - return true; - else - return false; - }; - - vstd::erase_if(basicGoals, removedHeroGoalPredicate); - vstd::erase_if(goalsToAdd, removedHeroGoalPredicate); - vstd::erase_if(goalsToRemove, removedHeroGoalPredicate); - - for(auto goal : ultimateGoalsFromBasic) - vstd::erase_if(goal.second, removedHeroGoalPredicate); -} - -void VCAI::answerQuery(QueryID queryID, int selection) -{ - logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection); - if(queryID != QueryID(-1)) - { - cb->selectionMade(selection, queryID); - } - else - { - logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID); - //do nothing - } -} - -void VCAI::requestSent(const CPackForServer * pack, int requestID) -{ - //BNLOG("I have sent request of type %s", typeid(*pack).name()); - if(auto reply = dynamic_cast(pack)) - { - status.attemptedAnsweringQuery(reply->qid, requestID); - } -} - -std::string VCAI::getBattleAIName() const -{ - if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING) - return settings["server"]["enemyAI"].String(); - else - return "BattleAI"; -} - -void VCAI::validateObject(const CGObjectInstance * obj) -{ - validateObject(obj->id); -} - -void VCAI::validateObject(ObjectIdRef obj) -{ - auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool - { - return hlpObj->id == obj.id; - }; - if(!obj) - { - vstd::erase_if(visitableObjs, matchesId); - - for(auto & p : reservedHeroesMap) - vstd::erase_if(p.second, matchesId); - - vstd::erase_if(reservedObjs, matchesId); - } -} - -AIStatus::AIStatus() -{ - battle = NO_BATTLE; - havingTurn = false; - ongoingHeroMovement = false; - ongoingChannelProbing = false; -} - -AIStatus::~AIStatus() -{ - -} - -void AIStatus::setBattle(BattleState BS) -{ - boost::unique_lock lock(mx); - LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS); - battle = BS; - cv.notify_all(); -} - -BattleState AIStatus::getBattle() -{ - boost::unique_lock lock(mx); - return battle; -} - -void AIStatus::addQuery(QueryID ID, std::string description) -{ - if(ID == QueryID(-1)) - { - logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description); - return; - } - - assert(ID.getNum() >= 0); - boost::unique_lock lock(mx); - - assert(!vstd::contains(remainingQueries, ID)); - - remainingQueries[ID] = description; - - cv.notify_all(); - logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); -} - -void AIStatus::removeQuery(QueryID ID) -{ - boost::unique_lock lock(mx); - assert(vstd::contains(remainingQueries, ID)); - - std::string description = remainingQueries[ID]; - remainingQueries.erase(ID); - - cv.notify_all(); - logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); -} - -int AIStatus::getQueriesCount() -{ - boost::unique_lock lock(mx); - return static_cast(remainingQueries.size()); -} - -void AIStatus::startedTurn() -{ - boost::unique_lock lock(mx); - havingTurn = true; - cv.notify_all(); -} - -void AIStatus::madeTurn() -{ - boost::unique_lock lock(mx); - havingTurn = false; - cv.notify_all(); -} - -void AIStatus::waitTillFree() -{ - boost::unique_lock lock(mx); - while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) - cv.wait_for(lock, boost::chrono::milliseconds(100)); -} - -bool AIStatus::haveTurn() -{ - boost::unique_lock lock(mx); - return havingTurn; -} - -void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID) -{ - boost::unique_lock lock(mx); - assert(vstd::contains(remainingQueries, queryID)); - std::string description = remainingQueries[queryID]; - logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID); - requestToQueryID[answerRequestID] = queryID; -} - -void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result) -{ - QueryID query; - - { - boost::unique_lock lock(mx); - - assert(vstd::contains(requestToQueryID, answerRequestID)); - query = requestToQueryID[answerRequestID]; - assert(vstd::contains(remainingQueries, query)); - requestToQueryID.erase(answerRequestID); - } - - if(result) - { - removeQuery(query); - } - else - { - logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]); - //TODO safely retry - } -} - -void AIStatus::heroVisit(const CGObjectInstance * obj, bool started) -{ - boost::unique_lock lock(mx); - if(started) - { - objectsBeingVisited.push_back(obj); - } - else - { - // There can be more than one object visited at the time (eg. hero visits Subterranean Gate - // causing visit to hero on the other side. - // However, we are guaranteed that start/end visit notification maintain stack order. - assert(!objectsBeingVisited.empty()); - objectsBeingVisited.pop_back(); - } - cv.notify_all(); -} - -void AIStatus::setMove(bool ongoing) -{ - boost::unique_lock lock(mx); - ongoingHeroMovement = ongoing; - cv.notify_all(); -} - -void AIStatus::setChannelProbing(bool ongoing) -{ - boost::unique_lock lock(mx); - ongoingChannelProbing = ongoing; - cv.notify_all(); -} - -bool AIStatus::channelProbing() -{ - return ongoingChannelProbing; -} - - - -bool isWeeklyRevisitable(const CGObjectInstance * obj) -{ - //TODO: allow polling of remaining creatures in dwelling - if(const auto * rewardable = dynamic_cast(obj)) - return rewardable->configuration.getResetDuration() == 7; - - if(dynamic_cast(obj)) - return true; - if(dynamic_cast(obj)) //banks tend to respawn often in mods - return true; - - switch(obj->ID) - { - case Obj::STABLES: - case Obj::MAGIC_WELL: - case Obj::HILL_FORT: - return true; - case Obj::BORDER_GATE: - case Obj::BORDERGUARD: - return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week - } - return false; -} - -bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) -{ - switch(obj->ID) - { - case Obj::TOWN: - case Obj::HERO: //never visit our heroes at random - return obj->tempOwner != h->tempOwner; //do not visit our towns at random - case Obj::BORDER_GATE: - { - for(auto q : ai->myCb->getMyQuests()) - { - if(q.obj == obj) - { - return false; // do not visit guards or gates when wandering - } - } - return true; //we don't have this quest yet - } - case Obj::BORDERGUARD: //open borderguard if possible - return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); - case Obj::SEER_HUT: - case Obj::QUEST_GUARD: - { - for(auto q : ai->myCb->getMyQuests()) - { - if(q.obj == obj) - { - if(q.quest->checkQuest(h.h)) - return true; //we completed the quest - else - return false; //we can't complete this quest - } - } - return true; //we don't have this quest yet - } - case Obj::CREATURE_GENERATOR1: - { - if(obj->tempOwner != h->tempOwner) - return true; //flag just in case - bool canRecruitCreatures = false; - const CGDwelling * d = dynamic_cast(obj); - for(auto level : d->creatures) - { - for(auto c : level.second) - { - if(h->getSlotFor(CreatureID(c)) != SlotID()) - canRecruitCreatures = true; - } - } - return canRecruitCreatures; - } - case Obj::HILL_FORT: - { - for(auto slot : h->Slots()) - { - if(slot.second->type->hasUpgrades()) - return true; //TODO: check price? - } - return false; - } - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_ONE_WAY_EXIT: - case Obj::MONOLITH_TWO_WAY: - case Obj::WHIRLPOOL: - return false; - case Obj::SCHOOL_OF_MAGIC: - case Obj::SCHOOL_OF_WAR: - { - if (ai->ah->freeGold() < 1000) - return false; - break; - } - case Obj::LIBRARY_OF_ENLIGHTENMENT: - if(h->level < 12) - return false; - break; - case Obj::TREE_OF_KNOWLEDGE: - { - TResources myRes = ai->ah->freeResources(); - if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10) - return false; - break; - } - case Obj::MAGIC_WELL: - return h->mana < h->manaLimit(); - case Obj::PRISON: - return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); - case Obj::TAVERN: - { - //TODO: make AI actually recruit heroes - //TODO: only on request - if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) - return false; - else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) - return false; - break; - } - case Obj::BOAT: - return false; - //Boats are handled by pathfinder - case Obj::EYE_OF_MAGI: - return false; //this object is useless to visit, but could be visited indefinitely - } - - if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); - return false; - - return true; -} - -std::optional VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) -{ - return std::nullopt; -} +/* + * VCAI.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 "VCAI.h" +#include "FuzzyHelper.h" +#include "ResourceManager.h" +#include "BuildingManager.h" +#include "Goals/Goals.h" + +#include "../../lib/UnlockGuard.h" +#include "../../lib/mapObjects/MapObjects.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/GameSettings.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/NetPacksBase.h" +#include "../../lib/NetPacks.h" +#include "../../lib/bonuses/CBonusSystemNode.h" +#include "../../lib/bonuses/Limiters.h" +#include "../../lib/bonuses/Updaters.h" +#include "../../lib/bonuses/Propagators.h" +#include "../../lib/serializer/CTypeList.h" +#include "../../lib/serializer/BinarySerializer.h" +#include "../../lib/serializer/BinaryDeserializer.h" + +#include "AIhelper.h" + +extern FuzzyHelper * fh; + +const double SAFE_ATTACK_CONSTANT = 1.5; + +//one thread may be turn of AI and another will be handling a side effect for AI2 +thread_local CCallback * cb = nullptr; +thread_local VCAI * ai = nullptr; + +//std::map > HeroView::infosCount; + +//helper RAII to manage global ai/cb ptrs +struct SetGlobalState +{ + SetGlobalState(VCAI * AI) + { + assert(!ai); + assert(!cb); + + ai = AI; + cb = AI->myCb.get(); + } + ~SetGlobalState() + { + //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully + //TODO: to ensure that, make rm unique_ptr + ai = nullptr; + cb = nullptr; + } +}; + + +#define SET_GLOBAL_STATE(ai) SetGlobalState _hlpSetState(ai); + +#define NET_EVENT_HANDLER SET_GLOBAL_STATE(this) +#define MAKING_TURN SET_GLOBAL_STATE(this) + +VCAI::VCAI() +{ + LOG_TRACE(logAi); + makingTurn = nullptr; + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + + ah = new AIhelper(); + ah->setAI(this); +} + +VCAI::~VCAI() +{ + delete ah; + LOG_TRACE(logAi); + finish(); +} + +void VCAI::availableCreaturesChanged(const CGDwelling * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroMoved(const TryMoveHero & details, bool verbose) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + //enemy hero may have left visible area + validateObject(details.id); + auto hero = cb->getHero(details.id); + + const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));; + const int3 to = hero ? hero->convertToVisitablePos(details.end) : (details.end - int3(0,1,0)); + + const CGObjectInstance * o1 = vstd::frontOrNull(cb->getVisitableObjs(from, verbose)); + const CGObjectInstance * o2 = vstd::frontOrNull(cb->getVisitableObjs(to, verbose)); + + if(details.result == TryMoveHero::TELEPORTATION) + { + auto t1 = dynamic_cast(o1); + auto t2 = dynamic_cast(o2); + if(t1 && t2) + { + if(cb->isTeleportChannelBidirectional(t1->channel)) + { + if(o1->ID == Obj::SUBTERRANEAN_GATE && o1->ID == o2->ID) // We need to only add subterranean gates in knownSubterraneanGates. Used for features not yet ported to use teleport channels + { + knownSubterraneanGates[o1] = o2; + knownSubterraneanGates[o2] = o1; + logAi->debug("Found a pair of subterranean gates between %s and %s!", from.toString(), to.toString()); + } + } + } + //FIXME: teleports are not correctly visited + unreserveObject(hero, t1); + unreserveObject(hero, t2); + } + else if(details.result == TryMoveHero::EMBARK && hero) + { + //make sure AI not attempt to visit used boat + validateObject(hero->boat); + } + else if(details.result == TryMoveHero::DISEMBARK && o1) + { + auto boat = dynamic_cast(o1); + if(boat) + addVisitableObj(boat); + } +} + +void VCAI::heroInGarrisonChange(const CGTownInstance * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::centerView(int3 pos, int focusTime) +{ + LOG_TRACE_PARAMS(logAi, "focusTime '%i'", focusTime); + NET_EVENT_HANDLER; +} + +void VCAI::artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactAssembled(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "TavernWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showThievesGuildWindow(const CGObjectInstance * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::playerBlocked(int reason, bool start) +{ + LOG_TRACE_PARAMS(logAi, "reason '%i', start '%i'", reason % start); + NET_EVENT_HANDLER; + if(start && reason == PlayerBlocked::UPCOMING_BATTLE) + status.setBattle(UPCOMING_BATTLE); + + if(reason == PlayerBlocked::ONGOING_MOVEMENT) + status.setMove(start); +} + +void VCAI::showPuzzleMap() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showShipyardDialog(const IShipyard * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) +{ + LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString()); + NET_EVENT_HANDLER; + logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost")); + if(player == playerID) + { + if(victoryLossCheckResult.victory()) + { + logAi->debug("VCAI: I won! Incredible!"); + logAi->debug("Turn nr %d", myCb->getDate()); + } + else + { + logAi->debug("VCAI: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString()); + } + + finish(); + } +} + +void VCAI::artifactPut(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactRemoved(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::artifactDisassembled(const ArtifactLocation & al) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) +{ + LOG_TRACE_PARAMS(logAi, "start '%i'; obj '%s'", start % (visitedObj ? visitedObj->getObjectName() : std::string("n/a"))); + NET_EVENT_HANDLER; + + if(start && visitedObj) //we can end visit with null object, anyway + { + markObjectVisited(visitedObj); + unreserveObject(visitor, visitedObj); + completeGoal(sptr(Goals::VisitObj(visitedObj->id.getNum()).sethero(visitor))); //we don't need to visit it anymore + //TODO: what if we visited one-time visitable object that was reserved by another hero (shouldn't, but..) + if (visitedObj->ID == Obj::HERO) + { + visitedHeroes[visitor].insert(HeroPtr(dynamic_cast(visitedObj))); + } + } + + status.heroVisit(visitedObj, start); +} + +void VCAI::availableArtifactsChanged(const CGBlackMarket * bm) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + //buildArmyIn(town); + //moveCreaturesToHero(town); +} + +void VCAI::tileHidden(const std::unordered_set & pos) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + validateVisitableObjs(); + clearPathsInfo(); +} + +void VCAI::tileRevealed(const std::unordered_set & pos) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + for(int3 tile : pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile)) + addVisitableObj(obj); + } + + clearPathsInfo(); +} + +void VCAI::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + auto firstHero = cb->getHero(hero1); + auto secondHero = cb->getHero(hero2); + + status.addQuery(query, boost::str(boost::format("Exchange between heroes %s (%d) and %s (%d)") % firstHero->getNameTranslated() % firstHero->tempOwner % secondHero->getNameTranslated() % secondHero->tempOwner)); + + requestActionASAP([=]() + { + float goalpriority1 = 0, goalpriority2 = 0; + + auto firstGoal = getGoal(firstHero); + if(firstGoal->goalType == Goals::GATHER_ARMY) + goalpriority1 = firstGoal->priority; + auto secondGoal = getGoal(secondHero); + if(secondGoal->goalType == Goals::GATHER_ARMY) + goalpriority2 = secondGoal->priority; + + auto transferFrom2to1 = [this](const CGHeroInstance * h1, const CGHeroInstance * h2) -> void + { + this->pickBestCreatures(h1, h2); + this->pickBestArtifacts(h1, h2); + }; + + //Do not attempt army or artifacts exchange if we visited ally player + //Visits can still be useful if hero have skills like Scholar + if(firstHero->tempOwner != secondHero->tempOwner) + { + logAi->debug("Heroes owned by different players. Do not exchange army or artifacts."); + } + else if(goalpriority1 > goalpriority2) + { + transferFrom2to1(firstHero, secondHero); + } + else if(goalpriority1 < goalpriority2) + { + transferFrom2to1(secondHero, firstHero); + } + else //regular criteria + { + if(firstHero->getFightingStrength() > secondHero->getFightingStrength() && ah->canGetArmy(firstHero, secondHero)) + transferFrom2to1(firstHero, secondHero); + else if(ah->canGetArmy(secondHero, firstHero)) + transferFrom2to1(secondHero, firstHero); + } + + completeGoal(sptr(Goals::VisitHero(firstHero->id.getNum()))); //TODO: what if we were visited by other hero in the meantime? + completeGoal(sptr(Goals::VisitHero(secondHero->id.getNum()))); + + answerQuery(query, 0); + }); +} + +void VCAI::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) +{ + LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast(which) % val); + NET_EVENT_HANDLER; +} + +void VCAI::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "level '%i'", level); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "RecruitmentDialog"); + requestActionASAP([=](){ + recruitCreatures(dwelling, dst); + checkHeroArmy(dynamic_cast(dst)); + answerQuery(queryID, 0); + }); +} + +void VCAI::heroMovePointsChanged(const CGHeroInstance * hero) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::newObject(const CGObjectInstance * obj) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(obj->isVisitable()) + addVisitableObj(obj); +} + +//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight +//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes +void VCAI::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + vstd::erase_if_present(visitableObjs, obj); + vstd::erase_if_present(alreadyVisited, obj); + + for(auto h : cb->getHeroesInfo()) + unreserveObject(h, obj); + + std::function checkRemovalValidity = [&](const Goals::TSubgoal & x) -> bool + { + if((x->goalType == Goals::VISIT_OBJ) && (x->objid == obj->id.getNum())) + return true; + else if(x->parent && checkRemovalValidity(x->parent)) //repeat this lambda check recursively on parent goal + return true; + else + return false; + }; + + //clear VCAI / main loop caches + vstd::erase_if(lockedHeroes, [&](const std::pair & x) -> bool + { + return checkRemovalValidity(x.second); + }); + + vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool + { + return checkRemovalValidity(x.first); + }); + + vstd::erase_if(basicGoals, checkRemovalValidity); + vstd::erase_if(goalsToAdd, checkRemovalValidity); + vstd::erase_if(goalsToRemove, checkRemovalValidity); + + for(auto goal : ultimateGoalsFromBasic) + vstd::erase_if(goal.second, checkRemovalValidity); + + //clear resource manager goal cache + ah->removeOutdatedObjectives(checkRemovalValidity); + + //TODO: Find better way to handle hero boat removal + if(auto hero = dynamic_cast(obj)) + { + if(hero->boat) + { + vstd::erase_if_present(visitableObjs, hero->boat); + vstd::erase_if_present(alreadyVisited, hero->boat); + + for(auto h : cb->getHeroesInfo()) + unreserveObject(h, hero->boat); + } + } + + //TODO + //there are other places where CGObjectinstance ptrs are stored... + // + + if(obj->ID == Obj::HERO && obj->tempOwner == playerID) + { + lostHero(cb->getHero(obj->id)); //we can promote, since objectRemoved is called just before actual deletion + } +} + +void VCAI::showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + requestActionASAP([=]() + { + makePossibleUpgrades(visitor); + }); +} + +void VCAI::playerBonusChanged(const Bonus & bonus, bool gain) +{ + LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); + NET_EVENT_HANDLER; +} + +void VCAI::heroCreated(const CGHeroInstance * h) +{ + LOG_TRACE(logAi); + if(h->visitedTown) + townVisitsThisWeek[HeroPtr(h)].insert(h->visitedTown); + NET_EVENT_HANDLER; +} + +void VCAI::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) +{ + LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID); + NET_EVENT_HANDLER; +} + +void VCAI::showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) +{ + LOG_TRACE_PARAMS(logAi, "soundID '%i'", soundID); + NET_EVENT_HANDLER; +} + +void VCAI::requestRealized(PackageApplied * pa) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(status.haveTurn()) + { + if(pa->packType == typeList.getTypeID()) + { + if(pa->result) + status.madeTurn(); + } + } + + if(pa->packType == typeList.getTypeID()) + { + status.receivedAnswerConfirmation(pa->requestID, pa->result); + } +} + +void VCAI::receivedResource() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "UniversityWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::heroManaPointsChanged(const CGHeroInstance * hero) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) +{ + LOG_TRACE_PARAMS(logAi, "which '%d', val '%d'", which % val); + NET_EVENT_HANDLER; +} + +void VCAI::battleResultsApplied() +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + assert(status.getBattle() == ENDING_BATTLE); + status.setBattle(NO_BATTLE); +} + +void VCAI::beforeObjectPropertyChanged(const SetObjectProperty * sop) +{ + +} + +void VCAI::objectPropertyChanged(const SetObjectProperty * sop) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + if(sop->what == ObjProperty::OWNER) + { + if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) + { + //we want to visit objects owned by oppponents + auto obj = myCb->getObj(sop->id, false); + if(obj) + { + addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set + vstd::erase_if_present(alreadyVisited, obj); + } + } + } +} + +void VCAI::buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) +{ + LOG_TRACE_PARAMS(logAi, "what '%i'", what); + NET_EVENT_HANDLER; + + if(town->getOwner() == playerID && what == 1) //built + completeGoal(sptr(Goals::BuildThis(buildingID, town))); +} + +void VCAI::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) +{ + LOG_TRACE_PARAMS(logAi, "gain '%i'", gain); + NET_EVENT_HANDLER; +} + +void VCAI::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) +{ + LOG_TRACE(logAi); + NET_EVENT_HANDLER; + + status.addQuery(queryID, "MarketWindow"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showWorldViewEx(const std::vector & objectPositions, bool showTerrain) +{ + //TODO: AI support for ViewXXX spell + LOG_TRACE(logAi); + NET_EVENT_HANDLER; +} + +void VCAI::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + LOG_TRACE(logAi); + env = ENV; + myCb = CB; + cbc = CB; + + ah->init(CB.get()); + + NET_EVENT_HANDLER; //sets ah->rm->cb + playerID = *myCb->getPlayerID(); + myCb->waitTillRealize = true; + myCb->unlockGsWhenWaiting = true; + + if(!fh) + fh = new FuzzyHelper(); + + retrieveVisitableObjs(); +} + +void VCAI::yourTurn(QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, "YourTurn"); + requestActionASAP([=](){ answerQuery(queryID, 0); }); + status.startedTurn(); + makingTurn = std::make_unique(&VCAI::makeTurn, this); +} + +void VCAI::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, boost::str(boost::format("Hero %s got level %d") % hero->getNameTranslated() % hero->level)); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID); + NET_EVENT_HANDLER; + status.addQuery(queryID, boost::str(boost::format("Commander %s of %s got level %d") % commander->name % commander->armyObj->nodeName() % (int)commander->level)); + requestActionASAP([=](){ answerQuery(queryID, 0); }); +} + +void VCAI::showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) +{ + LOG_TRACE_PARAMS(logAi, "text '%s', askID '%i', soundID '%i', selection '%i', cancel '%i'", text % askID % soundID % selection % cancel); + NET_EVENT_HANDLER; + int sel = 0; + status.addQuery(askID, boost::str(boost::format("Blocking dialog query with %d components - %s") + % components.size() % text)); + + if(selection) //select from multiple components -> take the last one (they're indexed [1-size]) + sel = static_cast(components.size()); + + if(!selection && cancel) //yes&no -> always answer yes, we are a brave AI :) + sel = 1; + + requestActionASAP([=]() + { + answerQuery(askID, sel); + }); +} + +void VCAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ +// LOG_TRACE_PARAMS(logAi, "askID '%i', exits '%s'", askID % exits); + NET_EVENT_HANDLER; + status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") + % exits.size())); + + int choosenExit = -1; + if(impassable) + { + knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE; + } + else if(destinationTeleport != ObjectInstanceID() && destinationTeleportPos.valid()) + { + auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos); + if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit)) + choosenExit = vstd::find_pos(exits, neededExit); + } + + for(auto exit : exits) + { + if(status.channelProbing() && exit.first == destinationTeleport) + { + choosenExit = vstd::find_pos(exits, exit); + break; + } + else + { + // TODO: Implement checking if visiting that teleport will uncovert any FoW + // So far this is the best option to handle decision about probing + auto obj = cb->getObj(exit.first, false); + if(obj == nullptr && !vstd::contains(teleportChannelProbingList, exit.first)) + { + if(exit.first != destinationTeleport) + teleportChannelProbingList.push_back(exit.first); + } + } + } + + requestActionASAP([=]() + { + answerQuery(askID, choosenExit); + }); +} + +void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) +{ + LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID); + NET_EVENT_HANDLER; + + std::string s1 = up ? up->nodeName() : "NONE"; + std::string s2 = down ? down->nodeName() : "NONE"; + + status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); + + //you can't request action from action-response thread + requestActionASAP([=]() + { + if(removableUnits) + pickBestCreatures(down, up); + + answerQuery(queryID, 0); + }); +} + +void VCAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + NET_EVENT_HANDLER; + status.addQuery(askID, "Map object select query"); + requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); }); +} + +void VCAI::saveGame(BinarySerializer & h, const int version) +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + NET_EVENT_HANDLER; + validateVisitableObjs(); + + #if 0 + //disabled due to issue 2890 + registerGoals(h); + #endif // 0 + CAdventureAI::saveGame(h, version); + serializeInternal(h, version); +} + +void VCAI::loadGame(BinaryDeserializer & h, const int version) +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + //NET_EVENT_HANDLER; + + #if 0 + //disabled due to issue 2890 + registerGoals(h); + #endif // 0 + CAdventureAI::loadGame(h, version); + serializeInternal(h, version); +} + +void makePossibleUpgrades(const CArmedInstance * obj) +{ + if(!obj) + return; + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + if(const CStackInstance * s = obj->getStackPtr(SlotID(i))) + { + UpgradeInfo ui; + cb->fillUpgradeInfo(obj, SlotID(i), ui); + if(ui.oldID != CreatureID::NONE && cb->getResourceAmount().canAfford(ui.cost[0] * s->count)) + { + cb->upgradeCreature(obj, SlotID(i), ui.newID[0]); + } + } + } +} + +void VCAI::makeTurn() +{ + MAKING_TURN; + + auto day = cb->getDate(Date::DAY); + logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day); + + boost::shared_lock gsLock(CGameState::mutex); + setThreadName("VCAI::makeTurn"); + + switch(cb->getDate(Date::DAY_OF_WEEK)) + { + case 1: + { + townVisitsThisWeek.clear(); + std::vector objs; + retrieveVisitableObjs(objs, true); + for(const CGObjectInstance * obj : objs) + { + if(isWeeklyRevisitable(obj)) + { + addVisitableObj(obj); + vstd::erase_if_present(alreadyVisited, obj); + } + } + break; + } + } + markHeroAbleToExplore(primaryHero()); + visitedHeroes.clear(); + + try + { + //it looks messy here, but it's better to have armed heroes before attempting realizing goals + for (const CGTownInstance * t : cb->getTownsInfo()) + moveCreaturesToHero(t); + + mainLoop(); + + /*Below function is also responsible for hero movement via internal wander function. By design it is separate logic for heroes that have nothing to do. + Heroes that were not picked by striveToGoal(sptr(Goals::Win())); recently (so they do not have new goals and cannot continue/reevaluate previously locked goals) will do logic in wander().*/ + performTypicalActions(); + + //for debug purpose + for (auto h : cb->getHeroesInfo()) + { + if (h->movementPointsRemaining()) + logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining()); + } + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Making turn thread has been interrupted. We'll end without calling endTurn."); + return; + } + catch (std::exception & e) + { + logAi->debug("Making turn thread has caught an exception: %s", e.what()); + } + + endTurn(); +} + +std::vector VCAI::getMyHeroes() const +{ + std::vector ret; + + for(auto h : cb->getHeroesInfo()) + { + ret.push_back(h); + } + + return ret; +} + +void VCAI::mainLoop() +{ + std::vector elementarGoals; //no duplicates allowed (operator ==) + basicGoals.clear(); + + validateVisitableObjs(); + + //get all potential and saved goals + //TODO: not lose + basicGoals.push_back(sptr(Goals::Win())); + for (auto goalPair : lockedHeroes) + { + fh->setPriority(goalPair.second); //re-evaluate, as heroes moved in the meantime + basicGoals.push_back(goalPair.second); + } + if (ah->hasTasksLeft()) + basicGoals.push_back(ah->whatToDo()); + for (auto quest : myCb->getMyQuests()) + { + basicGoals.push_back(sptr(Goals::CompleteQuest(quest))); + } + basicGoals.push_back(sptr(Goals::Build())); + + invalidPathHeroes.clear(); + + for (int pass = 0; pass< 30 && basicGoals.size(); pass++) + { + vstd::removeDuplicates(basicGoals); //TODO: container which does this automagically without has would be nice + goalsToAdd.clear(); + goalsToRemove.clear(); + elementarGoals.clear(); + ultimateGoalsFromBasic.clear(); + + ah->updatePaths(getMyHeroes()); + + logAi->debug("Main loop: decomposing %i basic goals", basicGoals.size()); + + for (auto basicGoal : basicGoals) + { + logAi->debug("Main loop: decomposing basic goal %s", basicGoal->name()); + + auto goalToDecompose = basicGoal; + Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); + int maxAbstractGoals = 10; + while (!elementarGoal->isElementar && maxAbstractGoals) + { + try + { + elementarGoal = decomposeGoal(goalToDecompose); + } + catch (goalFulfilledException & e) + { + //it is impossible to continue some goals (like exploration, for example) + //complete abstract goal for now, but maybe main goal finds another path + logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); + completeGoal(e.goal); //put in goalsToRemove + break; + } + catch(cannotFulfillGoalException & e) + { + //it is impossible to continue some goals (like exploration, for example) + //complete abstract goal for now, but maybe main goal finds another path + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", goalToDecompose->name(), e.what()); + break; + } + catch (std::exception & e) //decomposition failed, which means we can't decompose entire tree + { + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); + break; + } + if (elementarGoal->isAbstract) //we can decompose it further + { + goalsToAdd.push_back(elementarGoal); + //decompose further now - this is necesssary if we can't add over 10 goals in the pool + goalToDecompose = elementarGoal; + //there is a risk of infinite abstract goal loop, though it indicates failed logic + maxAbstractGoals--; + } + else if (elementarGoal->isElementar) //should be + { + logAi->debug("Found elementar goal %s", elementarGoal->name()); + elementarGoals.push_back(elementarGoal); + ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? + break; + } + else //should never be here + throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); + } + } + + logAi->trace("Main loop: selecting best elementar goal"); + + //now choose one elementar goal to realize + Goals::TGoalVec possibleGoals(elementarGoals.begin(), elementarGoals.end()); //copy to vector + Goals::TSubgoal goalToRealize = sptr(Goals::Invalid()); + while (possibleGoals.size()) + { + //allow assign goals to heroes with 0 movement, but don't realize them + //maybe there are beter ones left + + auto bestGoal = fh->chooseSolution(possibleGoals); + if (bestGoal->hero) //lock this hero to fulfill goal + { + setGoal(bestGoal->hero, bestGoal); + if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero)) + { + if (!vstd::erase_if_present(possibleGoals, bestGoal)) + { + logAi->error("erase_if_preset failed? Something very wrong!"); + break; + } + continue; //chose next from the list + } + } + goalToRealize = bestGoal; //we found our goal to execute + break; + } + + //realize best goal + if (!goalToRealize->invalid()) + { + logAi->debug("Trying to realize %s (value %2.3f)", goalToRealize->name(), goalToRealize->priority); + + try + { + boost::this_thread::interruption_point(); + goalToRealize->accept(this); //visitor pattern + boost::this_thread::interruption_point(); + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Player %d: Making turn thread received an interruption!", playerID); + throw; //rethrow, we want to truly end this thread + } + catch (goalFulfilledException & e) + { + //the sub-goal was completed successfully + completeGoal(e.goal); + //local goal was also completed? + completeGoal(goalToRealize); + + // remove abstract visit tile if we completed the elementar one + vstd::erase_if_present(goalsToAdd, goalToRealize); + } + catch (std::exception & e) + { + logAi->debug("Failed to realize subgoal of type %s, I will stop.", goalToRealize->name()); + logAi->debug("The error message was: %s", e.what()); + + //erase base goal if we failed to execute decomposed goal + for (auto basicGoal : ultimateGoalsFromBasic[goalToRealize]) + goalsToRemove.push_back(basicGoal); + + // sometimes resource manager contains an elementar goal which is not able to execute anymore and just fails each turn. + ai->ah->notifyGoalCompleted(goalToRealize); + + //we failed to realize best goal, but maybe others are still possible? + } + + //remove goals we couldn't decompose + for (auto goal : goalsToRemove) + vstd::erase_if_present(basicGoals, goal); + + //add abstract goals + boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool + { + return lhs->priority > rhs->priority; //highest priority at the beginning + }); + + //max number of goals = 10 + int i = 0; + while (basicGoals.size() < 10 && goalsToAdd.size() > i) + { + if (!vstd::contains(basicGoals, goalsToAdd[i])) //don't add duplicates + basicGoals.push_back(goalsToAdd[i]); + i++; + } + } + else //no elementar goals possible + { + logAi->debug("Goal decomposition exhausted"); + break; + } + } +} + +void VCAI::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h) +{ + LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString()); + switch(obj->ID) + { + case Obj::TOWN: + moveCreaturesToHero(dynamic_cast(obj)); + if(h->visitedTown) //we are inside, not just attacking + { + townVisitsThisWeek[h].insert(h->visitedTown); + if(!h->hasSpellbook() && ah->freeGold() >= GameConstants::SPELLBOOK_GOLD_COST) + { + if(h->visitedTown->hasBuilt(BuildingID::MAGES_GUILD_1)) + cb->buyArtifact(h.get(), ArtifactID::SPELLBOOK); + } + } + break; + } + completeGoal(sptr(Goals::VisitObj(obj->id.getNum()).sethero(h))); +} + +void VCAI::moveCreaturesToHero(const CGTownInstance * t) +{ + if(t->visitingHero && t->armedGarrison() && t->visitingHero->tempOwner == t->tempOwner) + { + pickBestCreatures(t->visitingHero, t); + } +} + +void VCAI::pickBestCreatures(const CArmedInstance * destinationArmy, const CArmedInstance * source) +{ + const CArmedInstance * armies[] = {destinationArmy, source}; + + auto bestArmy = ah->getSortedSlots(destinationArmy, source); + + //foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs + for(SlotID i = SlotID(0); i.getNum() < bestArmy.size() && i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot + { + const CCreature * targetCreature = bestArmy[i.getNum()].creature; + + for(auto armyPtr : armies) + { + for(SlotID j = SlotID(0); j.validSlot(); j.advance(1)) + { + if(armyPtr->getCreature(j) == targetCreature && (i != j || armyPtr != destinationArmy)) //it's a searched creature not in dst SLOT + { + //can't take away last creature without split. generate a new stack with 1 creature which is weak but fast + if(armyPtr == source + && source->needsLastStack() + && source->stacksCount() == 1 + && (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature)) + { + auto weakest = ah->getWeakestCreature(bestArmy); + + if(weakest->creature == targetCreature) + { + if(1 == source->getStackCount(j)) + break; + + // move all except 1 of weakest creature from source to destination + cb->splitStack( + source, + destinationArmy, + j, + destinationArmy->getSlotFor(targetCreature), + destinationArmy->getStackCount(i) + source->getStackCount(j) - 1); + + break; + } + else + { + // Source last stack is not weakest. Move 1 of weakest creature from destination to source + cb->splitStack( + destinationArmy, + source, + destinationArmy->getSlotFor(weakest->creature), + source->getFreeSlot(), + 1); + } + } + + cb->mergeOrSwapStacks(armyPtr, destinationArmy, j, i); + } + } + } + } + + //TODO - having now strongest possible army, we may want to think about arranging stacks + + auto hero = dynamic_cast(destinationArmy); + if(hero) + checkHeroArmy(hero); +} + +void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other) +{ + auto equipBest = [](const CGHeroInstance * h, const CGHeroInstance * otherh, bool giveStuffToFirstHero) -> void + { + bool changeMade = false; + do + { + changeMade = false; + + //we collect gear always in same order + std::vector allArtifacts; + if(giveStuffToFirstHero) + { + for(auto p : h->artifactsWorn) + { + if(p.second.artifact) + allArtifacts.push_back(ArtifactLocation(h, p.first)); + } + } + for(auto slot : h->artifactsInBackpack) + allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + + if(otherh) + { + for(auto p : otherh->artifactsWorn) + { + if(p.second.artifact) + allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + } + for(auto slot : otherh->artifactsInBackpack) + allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + } + //we give stuff to one hero or another, depending on giveStuffToFirstHero + + const CGHeroInstance * target = nullptr; + if(giveStuffToFirstHero || !otherh) + target = h; + else + target = otherh; + + for(auto location : allArtifacts) + { + if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) + continue; // don't attempt to move catapult and spellbook + + if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + continue; //don't reequip artifact we already wear + + auto s = location.getSlot(); + if(!s || s->locked) //we can't move locks + continue; + auto artifact = s->artifact; + if(!artifact) + continue; + //FIXME: why are the above possible to be null? + + bool emptySlotFound = false; + for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + { + ArtifactLocation destLocation(target, slot); + if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + { + cb->swapArtifacts(location, destLocation); //just put into empty slot + emptySlotFound = true; + changeMade = true; + break; + } + } + if(!emptySlotFound) //try to put that atifact in already occupied slot + { + for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) + { + auto otherSlot = target->getSlot(slot); + if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one + { + ArtifactLocation destLocation(target, slot); + //if that artifact is better than what we have, pick it + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + { + cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + changeMade = true; + break; + } + } + } + } + if(changeMade) + break; //start evaluating artifacts from scratch + } + } + while(changeMade); + }; + + equipBest(h, other, true); + + if(other) + equipBest(h, other, false); +} + +void VCAI::recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter) +{ + //now used only for visited dwellings / towns, not BuyArmy goal + for(int i = 0; i < d->creatures.size(); i++) + { + if(!d->creatures[i].second.size()) + continue; + + int count = d->creatures[i].first; + CreatureID creID = d->creatures[i].second.back(); + + vstd::amin(count, ah->freeResources() / VLC->creatures()->getById(creID)->getFullRecruitCost()); + if(count > 0) + cb->recruitCreatures(d, recruiter, creID, count, i); + } +} + +bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit) +{ + int3 op = obj->visitablePos(); + auto paths = ah->getPathsToTile(h, op); + + for(const auto & path : paths) + { + if(movementCostLimit && movementCostLimit.value() < path.movementCost()) + return false; + + if(isGoodForVisit(obj, h, path)) + return true; + } + + return false; +} + +bool VCAI::isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const +{ + const int3 pos = obj->visitablePos(); + const int3 targetPos = path.firstTileToGet(); + if (!targetPos.valid()) + return false; + if (!isTileNotReserved(h.get(), targetPos)) + return false; + if (obj->wasVisited(playerID)) + return false; + if (cb->getPlayerRelations(playerID, obj->tempOwner) != PlayerRelations::ENEMIES && !isWeeklyRevisitable(obj)) + return false; // Otherwise we flag or get weekly resources / creatures + if (!isSafeToVisit(h, pos)) + return false; + if (!shouldVisit(h, obj)) + return false; + if (vstd::contains(alreadyVisited, obj)) + return false; + if (vstd::contains(reservedObjs, obj)) + return false; + + // TODO: looks extra if we already have AIPath + //if (!isAccessibleForHero(targetPos, h)) + // return false; + + const CGObjectInstance * topObj = cb->getVisitableObjs(obj->visitablePos()).back(); //it may be hero visiting this obj + //we don't try visiting object on which allied or owned hero stands + // -> it will just trigger exchange windows and AI will be confused that obj behind doesn't get visited + return !(topObj->ID == Obj::HERO && cb->getPlayerRelations(h->tempOwner, topObj->tempOwner) != PlayerRelations::ENEMIES); //all of the following is met +} + +bool VCAI::isTileNotReserved(const CGHeroInstance * h, int3 t) const +{ + if(t.valid()) + { + auto obj = cb->getTopObj(t); + if(obj && vstd::contains(ai->reservedObjs, obj) + && vstd::contains(reservedHeroesMap, h) + && !vstd::contains(reservedHeroesMap.at(h), obj)) + return false; //do not capture object reserved by another hero + else + return true; + } + else + { + return false; + } +} + +bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const +{ + //TODO: make gathering gold, building tavern or conquering town (?) possible subgoals + if(!t) + t = findTownWithTavern(); + if(!t) + return false; + if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager + return false; + if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES) + return false; + if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + return false; + if(!cb->getAvailableHeroes(t).size()) + return false; + + return true; +} + +void VCAI::wander(HeroPtr h) +{ + auto visitTownIfAny = [this](HeroPtr h) -> bool + { + if (h->visitedTown) + { + townVisitsThisWeek[h].insert(h->visitedTown); + buildArmyIn(h->visitedTown); + return true; + } + return false; + }; + + //unclaim objects that are now dangerous for us + auto reservedObjsSetCopy = reservedHeroesMap[h]; + for(auto obj : reservedObjsSetCopy) + { + if(!isSafeToVisit(h, obj->visitablePos())) + unreserveObject(h, obj); + } + + TimeCheck tc("looking for wander destination"); + + for(int k = 0; k < 10 && h->movementPointsRemaining(); k++) + { + validateVisitableObjs(); + ah->updatePaths(getMyHeroes()); + + std::vector dests; + + //also visit our reserved objects - but they are not prioritized to avoid running back and forth + vstd::copy_if(reservedHeroesMap[h], std::back_inserter(dests), [&](ObjectIdRef obj) -> bool + { + return ah->isTileAccessible(h, obj->visitablePos()); + }); + + int pass = 0; + std::vector> distanceLimits = {1.0, 2.0, std::nullopt}; + + while(!dests.size() && pass < distanceLimits.size()) + { + auto & distanceLimit = distanceLimits[pass]; + + logAi->debug("Looking for wander destination pass=%i, cost limit=%f", pass, distanceLimit.value_or(-1.0)); + + vstd::copy_if(visitableObjs, std::back_inserter(dests), [&](ObjectIdRef obj) -> bool + { + return isGoodForVisit(obj, h, distanceLimit); + }); + + pass++; + } + + if(!dests.size()) + { + logAi->debug("Looking for town destination"); + + if(cb->getVisitableObjs(h->visitablePos()).size() > 1) + moveHeroToTile(h->visitablePos(), h); //just in case we're standing on blocked subterranean gate + + auto compareReinforcements = [&](const CGTownInstance * lhs, const CGTownInstance * rhs) -> bool + { + const CGHeroInstance * hptr = h.get(); + auto r1 = ah->howManyReinforcementsCanGet(hptr, lhs), + r2 = ah->howManyReinforcementsCanGet(hptr, rhs); + if (r1 != r2) + return r1 < r2; + else + return ah->howManyReinforcementsCanBuy(hptr, lhs) < ah->howManyReinforcementsCanBuy(hptr, rhs); + }; + + std::vector townsReachable; + std::vector townsNotReachable; + for(const CGTownInstance * t : cb->getTownsInfo()) + { + if(!t->visitingHero && !vstd::contains(townVisitsThisWeek[h], t)) + { + if(isAccessibleForHero(t->visitablePos(), h)) + townsReachable.push_back(t); + else + townsNotReachable.push_back(t); + } + } + if(townsReachable.size()) //travel to town with largest garrison, or empty - better than nothing + { + dests.push_back(*boost::max_element(townsReachable, compareReinforcements)); + } + else if(townsNotReachable.size()) + { + //TODO pick the truly best + const CGTownInstance * t = *boost::max_element(townsNotReachable, compareReinforcements); + logAi->debug("%s can't reach any town, we'll try to make our way to %s at %s", h->getNameTranslated(), t->getNameTranslated(), t->visitablePos().toString()); + int3 pos1 = h->pos; + striveToGoal(sptr(Goals::ClearWayTo(t->visitablePos()).sethero(h))); //TODO: drop "strive", add to mainLoop + //if out hero is stuck, we may need to request another hero to clear the way we see + + if(pos1 == h->pos && h == primaryHero()) //hero can't move + { + if(canRecruitAnyHero(t)) + recruitHero(t); + } + break; + } + else if(cb->getResourceAmount(EGameResID::GOLD) >= GameConstants::HERO_GOLD_COST) + { + std::vector towns = cb->getTownsInfo(); + vstd::erase_if(towns, [&](const CGTownInstance * t) -> bool + { + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(!t->getArmyStrength() || ah->howManyReinforcementsCanGet(h, t)) + return true; + } + return false; + }); + if (towns.size()) + { + recruitHero(*boost::max_element(towns, compareArmyStrength)); + } + break; + } + else + { + logAi->debug("Nowhere more to go..."); + break; + } + } + //end of objs empty + + if(dests.size()) //performance improvement + { + Goals::TGoalVec targetObjectGoals; + for(auto destination : dests) + { + vstd::concatenate(targetObjectGoals, ah->howToVisitObj(h, destination, false)); + } + + if(targetObjectGoals.size()) + { + auto bestObjectGoal = fh->chooseSolution(targetObjectGoals); + + //wander should not cause heroes to be reserved - they are always considered free + if(bestObjectGoal->goalType == Goals::VISIT_OBJ) + { + auto chosenObject = cb->getObjInstance(ObjectInstanceID(bestObjectGoal->objid)); + if(chosenObject != nullptr) + logAi->debug("Of all %d destinations, object %s at pos=%s seems nice", dests.size(), chosenObject->getObjectName(), chosenObject->pos.toString()); + } + else + logAi->debug("Trying to realize goal of type %s as part of wandering.", bestObjectGoal->name()); + + try + { + decomposeGoal(bestObjectGoal)->accept(this); + } + catch(const goalFulfilledException & e) + { + if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) + continue; + + throw e; + } + } + else + { + logAi->debug("Nowhere more to go..."); + break; + } + + visitTownIfAny(h); + } + } + + visitTownIfAny(h); //in case hero is just sitting in town +} + +void VCAI::setGoal(HeroPtr h, Goals::TSubgoal goal) +{ + if(goal->invalid()) + { + vstd::erase_if_present(lockedHeroes, h); + } + else + { + lockedHeroes[h] = goal; + goal->setisElementar(false); //Force always evaluate goals before realizing + } +} +void VCAI::evaluateGoal(HeroPtr h) +{ + if(vstd::contains(lockedHeroes, h)) + fh->setPriority(lockedHeroes[h]); +} + +void VCAI::completeGoal(Goals::TSubgoal goal) +{ + if (goal->goalType == Goals::WIN) //we can never complete this goal - unless we already won + return; + + logAi->debug("Completing goal: %s", goal->name()); + + //notify Managers + ah->notifyGoalCompleted(goal); + //notify mainLoop() + goalsToRemove.push_back(goal); //will be removed from mainLoop() goals + for (auto basicGoal : basicGoals) //we could luckily fulfill any of our goals + { + if (basicGoal->fulfillsMe(goal)) + goalsToRemove.push_back(basicGoal); + } + + //unreserve heroes + if(const CGHeroInstance * h = goal->hero.get(true)) + { + auto it = lockedHeroes.find(h); + if(it != lockedHeroes.end()) + { + if(it->second == goal || it->second->fulfillsMe(goal)) //FIXME this is overspecified, fulfillsMe shoudl be complete + { + logAi->debug(goal->completeMessage()); + lockedHeroes.erase(it); //goal fulfilled, free hero + } + } + } + else //complete goal for all heroes maybe? + { + vstd::erase_if(lockedHeroes, [goal](std::pair p) + { + if(p.second == goal || p.second->fulfillsMe(goal)) //we could have fulfilled goals of other heroes by chance + { + logAi->debug(p.second->completeMessage()); + return true; + } + return false; + }); + } + +} + +void VCAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +{ + NET_EVENT_HANDLER; + assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE); + status.setBattle(ONGOING_BATTLE); + const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit + battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString()); + CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); +} + +void VCAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) +{ + NET_EVENT_HANDLER; + assert(status.getBattle() == ONGOING_BATTLE); + status.setBattle(ENDING_BATTLE); + bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide(); + logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename); + battlename.clear(); + + if (queryID != QueryID::NONE) + { + status.addQuery(queryID, "Combat result dialog"); + const int confirmAction = 0; + requestActionASAP([=]() + { + answerQuery(queryID, confirmAction); + }); + } + CAdventureAI::battleEnd(battleID, br, queryID); +} + +void VCAI::waitTillFree() +{ + auto unlock = vstd::makeUnlockSharedGuard(CGameState::mutex); + status.waitTillFree(); +} + +void VCAI::markObjectVisited(const CGObjectInstance * obj) +{ + if(!obj) + return; + + if(const auto * rewardable = dynamic_cast(obj)) //we may want to visit it with another hero + { + if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero + return; + + if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time + return; + } + + if(obj->ID == Obj::MONSTER) + return; + + alreadyVisited.insert(obj); +} + +void VCAI::reserveObject(HeroPtr h, const CGObjectInstance * obj) +{ + reservedObjs.insert(obj); + reservedHeroesMap[h].insert(obj); + logAi->debug("reserved object id=%d; address=%p; name=%s", obj->id, obj, obj->getObjectName()); +} + +void VCAI::unreserveObject(HeroPtr h, const CGObjectInstance * obj) +{ + vstd::erase_if_present(reservedObjs, obj); //unreserve objects + vstd::erase_if_present(reservedHeroesMap[h], obj); +} + +void VCAI::markHeroUnableToExplore(HeroPtr h) +{ + heroesUnableToExplore.insert(h); +} +void VCAI::markHeroAbleToExplore(HeroPtr h) +{ + vstd::erase_if_present(heroesUnableToExplore, h); +} +bool VCAI::isAbleToExplore(HeroPtr h) +{ + return !vstd::contains(heroesUnableToExplore, h); +} +void VCAI::clearPathsInfo() +{ + heroesUnableToExplore.clear(); +} + +void VCAI::validateVisitableObjs() +{ + std::string errorMsg; + auto shouldBeErased = [&](const CGObjectInstance * obj) -> bool + { + if(obj) + return !cb->getObj(obj->id, false); // no verbose output needed as we check object visibility + else + return true; + }; + + //errorMsg is captured by ref so lambda will take the new text + errorMsg = " shouldn't be on the visitable objects list!"; + vstd::erase_if(visitableObjs, shouldBeErased); + + //FIXME: how comes our own heroes become inaccessible? + vstd::erase_if(reservedHeroesMap, [](std::pair> hp) -> bool + { + return !hp.first.get(true); + }); + for(auto & p : reservedHeroesMap) + { + errorMsg = " shouldn't be on list for hero " + p.first->getNameTranslated() + "!"; + vstd::erase_if(p.second, shouldBeErased); + } + + errorMsg = " shouldn't be on the reserved objs list!"; + vstd::erase_if(reservedObjs, shouldBeErased); + + //TODO overkill, hidden object should not be removed. However, we can't know if hidden object is erased from game. + errorMsg = " shouldn't be on the already visited objs list!"; + vstd::erase_if(alreadyVisited, shouldBeErased); +} + +void VCAI::retrieveVisitableObjs(std::vector & out, bool includeOwned) const +{ + foreach_tile_pos([&](const int3 & pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) + { + if(includeOwned || obj->tempOwner != playerID) + out.push_back(obj); + } + }); +} + +void VCAI::retrieveVisitableObjs() +{ + foreach_tile_pos([&](const int3 & pos) + { + for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false)) + { + if(obj->tempOwner != playerID) + addVisitableObj(obj); + } + }); +} + +std::vector VCAI::getFlaggedObjects() const +{ + std::vector ret; + for(const CGObjectInstance * obj : visitableObjs) + { + if(obj->tempOwner == playerID) + ret.push_back(obj); + } + return ret; +} + +void VCAI::addVisitableObj(const CGObjectInstance * obj) +{ + if(obj->ID == Obj::EVENT) + return; + + visitableObjs.insert(obj); + + // All teleport objects seen automatically assigned to appropriate channels + auto teleportObj = dynamic_cast(obj); + if(teleportObj) + CGTeleport::addToChannel(knownTeleportChannels, teleportObj); +} + +const CGObjectInstance * VCAI::lookForArt(int aid) const +{ + for(const CGObjectInstance * obj : ai->visitableObjs) + { + if(obj->ID == Obj::ARTIFACT && obj->subID == aid) + return obj; + } + + return nullptr; + + //TODO what if more than one artifact is available? return them all or some slection criteria +} + +bool VCAI::isAccessible(const int3 & pos) const +{ + //TODO precalculate for speed + + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(isAccessibleForHero(pos, h)) + return true; + } + + return false; +} + +HeroPtr VCAI::getHeroWithGrail() const +{ + for(const CGHeroInstance * h : cb->getHeroesInfo()) + { + if(h->hasArt(ArtifactID::GRAIL)) + return h; + } + return nullptr; +} + +const CGObjectInstance * VCAI::getUnvisitedObj(const std::function & predicate) +{ + //TODO smarter definition of unvisited + for(const CGObjectInstance * obj : visitableObjs) + { + if(predicate(obj) && !vstd::contains(alreadyVisited, obj)) + return obj; + } + return nullptr; +} + +bool VCAI::isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies) const +{ + // Don't visit tile occupied by allied hero + if(!includeAllies) + { + for(auto obj : cb->getVisitableObjs(pos)) + { + if(obj->ID == Obj::HERO && cb->getPlayerRelations(ai->playerID, obj->tempOwner) != PlayerRelations::ENEMIES) + { + if(obj != h.get()) + return false; + } + } + } + return cb->getPathsInfo(h.get())->getPathInfo(pos)->reachable(); +} + +bool VCAI::moveHeroToTile(int3 dst, HeroPtr h) +{ + //TODO: consider if blockVisit objects change something in our checks: AIUtility::isBlockVisitObj() + + auto afterMovementCheck = [&]() -> void + { + waitTillFree(); //movement may cause battle or blocking dialog + if(!h) + { + lostHero(h); + teleportChannelProbingList.clear(); + if(status.channelProbing()) // if hero lost during channel probing we need to switch this mode off + status.setChannelProbing(false); + throw cannotFulfillGoalException("Hero was lost!"); + } + }; + + logAi->debug("Moving hero %s to tile %s", h->getNameTranslated(), dst.toString()); + int3 startHpos = h->visitablePos(); + bool ret = false; + if(startHpos == dst) + { + //FIXME: this assertion fails also if AI moves onto defeated guarded object + assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object + cb->moveHero(*h, h->convertFromVisitablePos(dst)); + afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly? + // If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared + teleportChannelProbingList.clear(); + // not sure if AI can currently reconsider to attack bank while staying on it. Check issue 2084 on mantis for more information. + ret = true; + } + else + { + CGPath path; + cb->getPathsInfo(h.get())->getPath(path, dst); + if(path.nodes.empty()) + { + logAi->error("Hero %s cannot reach %s.", h->getNameTranslated(), dst.toString()); + throw goalFulfilledException(sptr(Goals::VisitTile(dst).sethero(h))); + } + int i = (int)path.nodes.size() - 1; + + auto getObj = [&](int3 coord, bool ignoreHero) + { + auto tile = cb->getTile(coord, false); + assert(tile); + return tile->topVisitableObj(ignoreHero); + //return cb->getTile(coord,false)->topVisitableObj(ignoreHero); + }; + + auto isTeleportAction = [&](EPathNodeAction action) -> bool + { + if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT) + { + if(action != EPathNodeAction::TELEPORT_BATTLE) + { + return false; + } + } + + return true; + }; + + auto getDestTeleportObj = [&](const CGObjectInstance * currentObject, const CGObjectInstance * nextObjectTop, const CGObjectInstance * nextObject) -> const CGObjectInstance * + { + if(CGTeleport::isConnected(currentObject, nextObjectTop)) + return nextObjectTop; + if(nextObjectTop && nextObjectTop->ID == Obj::HERO) + { + if(CGTeleport::isConnected(currentObject, nextObject)) + return nextObject; + } + + return nullptr; + }; + + auto doMovement = [&](int3 dst, bool transit) + { + cb->moveHero(*h, h->convertFromVisitablePos(dst), transit); + }; + + auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) + { + destinationTeleport = exitId; + if(exitPos.valid()) + destinationTeleportPos = h->convertFromVisitablePos(exitPos); + cb->moveHero(*h, h->pos); + destinationTeleport = ObjectInstanceID(); + destinationTeleportPos = int3(-1); + afterMovementCheck(); + }; + + auto doChannelProbing = [&]() -> void + { + auto currentPos = h->visitablePos(); + auto currentExit = getObj(currentPos, true)->id; + + status.setChannelProbing(true); + for(auto exit : teleportChannelProbingList) + doTeleportMovement(exit, int3(-1)); + teleportChannelProbingList.clear(); + status.setChannelProbing(false); + + doTeleportMovement(currentExit, currentPos); + }; + + for(; i > 0; i--) + { + int3 currentCoord = path.nodes[i].coord; + int3 nextCoord = path.nodes[i - 1].coord; + + auto currentObject = getObj(currentCoord, currentCoord == h->visitablePos()); + auto nextObjectTop = getObj(nextCoord, false); + auto nextObject = getObj(nextCoord, true); + auto destTeleportObj = getDestTeleportObj(currentObject, nextObjectTop, nextObject); + if(isTeleportAction(path.nodes[i - 1].action) && destTeleportObj != nullptr) + { + //we use special login if hero standing on teleporter it's mean we need + doTeleportMovement(destTeleportObj->id, nextCoord); + if(teleportChannelProbingList.size()) + doChannelProbing(); + markObjectVisited(destTeleportObj); //FIXME: Monoliths are not correctly visited + + continue; + } + + //stop sending move requests if the next node can't be reached at the current turn (hero exhausted his move points) + if(path.nodes[i - 1].turns) + { + //blockedHeroes.insert(h); //to avoid attempts of moving heroes with very little MPs + break; + } + + int3 endpos = path.nodes[i - 1].coord; + if(endpos == h->visitablePos()) + continue; + + bool isConnected = false; + bool isNextObjectTeleport = false; + // Check there is node after next one; otherwise transit is pointless + if(i - 2 >= 0) + { + isConnected = CGTeleport::isConnected(nextObjectTop, getObj(path.nodes[i - 2].coord, false)); + isNextObjectTeleport = CGTeleport::isTeleport(nextObjectTop); + } + if(isConnected || isNextObjectTeleport) + { + // Hero should be able to go through object if it's allow transit + doMovement(endpos, true); + } + else if(path.nodes[i - 1].layer == EPathfindingLayer::AIR) + { + doMovement(endpos, true); + } + else + { + doMovement(endpos, false); + } + + afterMovementCheck(); + + if(teleportChannelProbingList.size()) + doChannelProbing(); + } + + if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT) + { + ret = h && i == 0; // when we take resource we do not reach its position. We even might not move + } + } + if(h) + { + if(auto visitedObject = vstd::frontOrNull(cb->getVisitableObjs(h->visitablePos()))) //we stand on something interesting + { + if(visitedObject != *h) + performObjectInteraction(visitedObject, h); + } + } + if(h) //we could have lost hero after last move + { + completeGoal(sptr(Goals::VisitTile(dst).sethero(h))); //we stepped on some tile, anyway + completeGoal(sptr(Goals::ClearWayTo(dst).sethero(h))); + + ret = ret || (dst == h->visitablePos()); + + if(!ret) //reserve object we are heading towards + { + auto obj = vstd::frontOrNull(cb->getVisitableObjs(dst)); + if(obj && obj != *h) + reserveObject(h, obj); + } + + if(startHpos == h->visitablePos() && !ret) //we didn't move and didn't reach the target + { + vstd::erase_if_present(lockedHeroes, h); //hero seemingly is confused or has only 95mp which is not enough to move + invalidPathHeroes.insert(h); + throw cannotFulfillGoalException("Invalid path found!"); + } + evaluateGoal(h); //new hero position means new game situation + logAi->debug("Hero %s moved from %s to %s. Returning %d.", h->getNameTranslated(), startHpos.toString(), h->visitablePos().toString(), ret); + } + return ret; +} + +void VCAI::buildStructure(const CGTownInstance * t, BuildingID building) +{ + auto name = t->town->buildings.at(building)->getNameTranslated(); + logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString()); + cb->buildBuilding(t, building); //just do this; +} + +void VCAI::tryRealize(Goals::Explore & g) +{ + throw cannotFulfillGoalException("EXPLORE is not an elementar goal!"); +} + +void VCAI::tryRealize(Goals::RecruitHero & g) +{ + if(const CGTownInstance * t = findTownWithTavern()) + { + recruitHero(t, true); + //TODO try to free way to blocked town + //TODO: adventure map tavern or prison? + } + else + { + throw cannotFulfillGoalException("No town to recruit hero!"); + } +} + +void VCAI::tryRealize(Goals::VisitTile & g) +{ + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!"); + if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) + { + logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); + throw goalFulfilledException(sptr(g)); + } + if(ai->moveHeroToTile(g.tile, g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } +} + +void VCAI::tryRealize(Goals::VisitObj & g) +{ + auto position = g.tile; + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!"); + if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2) + { + logAi->warn("Why do I want to move hero %s to tile %s? Already standing on that tile! ", g.hero->getNameTranslated(), g.tile.toString()); + throw goalFulfilledException(sptr(g)); + } + if(ai->moveHeroToTile(position, g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } +} + +void VCAI::tryRealize(Goals::VisitHero & g) +{ + if(!g.hero->movementPointsRemaining()) + throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!"); + + const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid)); + if(obj) + { + if(ai->moveHeroToTile(obj->visitablePos(), g.hero.get())) + { + throw goalFulfilledException(sptr(g)); + } + } + else + { + throw cannotFulfillGoalException("Cannot visit hero: object not found!"); + } +} + +void VCAI::tryRealize(Goals::BuildThis & g) +{ + auto b = BuildingID(g.bid); + auto t = g.town; + + if (t) + { + if (cb->canBuildStructure(t, b) == EBuildingState::ALLOWED) + { + logAi->debug("Player %d will build %s in town of %s at %s", + playerID, t->town->buildings.at(b)->getNameTranslated(), t->getNameTranslated(), t->pos.toString()); + cb->buildBuilding(t, b); + throw goalFulfilledException(sptr(g)); + } + } + throw cannotFulfillGoalException("Cannot build a given structure!"); +} + +void VCAI::tryRealize(Goals::DigAtTile & g) +{ + assert(g.hero->visitablePos() == g.tile); //surely we want to crash here? + if(g.hero->diggingStatus() == EDiggingStatus::CAN_DIG) + { + cb->dig(g.hero.get()); + completeGoal(sptr(g)); // finished digging + } + else + { + ai->lockedHeroes[g.hero] = sptr(g); //hero who tries to dig shouldn't do anything else + throw cannotFulfillGoalException("A hero can't dig!\n"); + } +} + +void VCAI::tryRealize(Goals::Trade & g) //trade +{ + if(ah->freeResources()[g.resID] >= g.value) //goal is already fulfilled. Why we need this check, anyway? + throw goalFulfilledException(sptr(g)); + + int accquiredResources = 0; + if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false)) + { + if(const IMarket * m = IMarket::castFrom(obj, false)) + { + auto freeRes = ah->freeResources(); //trade only resources which are not reserved + for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++) + { + auto res = it->resType; + if(res.getNum() == g.resID) //sell any other resource + continue; + + int toGive, toGet; + m->getOffer(res, g.resID, toGive, toGet, EMarketMode::RESOURCE_RESOURCE); + toGive = static_cast(toGive * (it->resVal / toGive)); //round down + //TODO trade only as much as needed + if (toGive) //don't try to sell 0 resources + { + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); + accquiredResources = static_cast(toGet * (it->resVal / toGive)); + logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); + } + if (ah->freeResources()[g.resID] >= g.value) + throw goalFulfilledException(sptr(g)); //we traded all we needed + } + + throw cannotFulfillGoalException("I cannot get needed resources by trade!"); + } + else + { + throw cannotFulfillGoalException("I don't know how to use this object to raise resources!"); + } + } + else + { + throw cannotFulfillGoalException("No object that could be used to raise resources!"); + } +} + +void VCAI::tryRealize(Goals::BuyArmy & g) +{ + auto t = g.town; + + ui64 valueBought = 0; + //buy the stacks with largest AI value + + makePossibleUpgrades(t); + + while (valueBought < g.value) + { + auto res = ah->allResources(); + std::vector creaturesInDwellings; + + for (int i = 0; i < t->creatures.size(); i++) + { + auto ci = infoFromDC(t->creatures[i]); + + if(!ci.count + || ci.creID == CreatureID::NONE + || (g.objid != -1 && ci.creID.getNum() != g.objid) + || t->getUpperArmy()->getSlotFor(ci.creID) == SlotID()) + continue; + + vstd::amin(ci.count, res / ci.cre->getFullRecruitCost()); //max count we can afford + + if(!ci.count) + continue; + + ci.level = i; //this is important for Dungeon Summoning Portal + creaturesInDwellings.push_back(ci); + } + + if (creaturesInDwellings.empty()) + throw cannotFulfillGoalException("Can't buy any more creatures!"); + + creInfo ci = + *boost::max_element(creaturesInDwellings, [](const creInfo & lhs, const creInfo & rhs) + { + //max value of creatures we can buy with our res + int value1 = lhs.cre->getAIValue() * lhs.count, + value2 = rhs.cre->getAIValue() * rhs.count; + + return value1 < value2; + }); + + + cb->recruitCreatures(t, t->getUpperArmy(), ci.creID, ci.count, ci.level); + valueBought += ci.count * ci.cre->getAIValue(); + } + + throw goalFulfilledException(sptr(g)); //we bought as many creatures as we wanted +} + +void VCAI::tryRealize(Goals::Invalid & g) +{ + throw cannotFulfillGoalException("I don't know how to fulfill this!"); +} + +void VCAI::tryRealize(Goals::AbstractGoal & g) +{ + logAi->debug("Attempting realizing goal with code %s", g.name()); + throw cannotFulfillGoalException("Unknown type of goal !"); +} + +const CGTownInstance * VCAI::findTownWithTavern() const +{ + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->hasBuilt(BuildingID::TAVERN) && !t->visitingHero) + return t; + + return nullptr; +} + +Goals::TSubgoal VCAI::getGoal(HeroPtr h) const +{ + auto it = lockedHeroes.find(h); + if(it != lockedHeroes.end()) + return it->second; + else + return sptr(Goals::Invalid()); +} + + +std::vector VCAI::getUnblockedHeroes() const +{ + std::vector ret; + for(auto h : cb->getHeroesInfo()) + { + //&& !vstd::contains(lockedHeroes, h) + //at this point we assume heroes exhausted their locked goals + if(canAct(h)) + ret.push_back(h); + } + return ret; +} + +bool VCAI::canAct(HeroPtr h) const +{ + auto mission = lockedHeroes.find(h); + if(mission != lockedHeroes.end()) + { + //FIXME: I'm afraid there can be other conditions when heroes can act but not move :? + if(mission->second->goalType == Goals::DIG_AT_TILE && !mission->second->isElementar) + return false; + } + + return h->movementPointsRemaining(); +} + +HeroPtr VCAI::primaryHero() const +{ + auto hs = cb->getHeroesInfo(); + if (hs.empty()) + return nullptr; + else + return *boost::max_element(hs, compareHeroStrength); +} + +void VCAI::endTurn() +{ + logAi->info("Player %d (%s) ends turn", playerID, playerID.toString()); + if(!status.haveTurn()) + { + logAi->error("Not having turn at the end of turn???"); + } + logAi->debug("Resources at the end of turn: %s", cb->getResourceAmount().toString()); + do + { + cb->endTurn(); + } + while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over + + logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString()); +} + +void VCAI::striveToGoal(Goals::TSubgoal basicGoal) +{ + //TODO: this function is deprecated and should be dropped altogether + + auto goalToDecompose = basicGoal; + Goals::TSubgoal elementarGoal = sptr(Goals::Invalid()); + int maxAbstractGoals = 10; + while (!elementarGoal->isElementar && maxAbstractGoals) + { + try + { + elementarGoal = decomposeGoal(goalToDecompose); + } + catch (goalFulfilledException & e) + { + //it is impossible to continue some goals (like exploration, for example) + completeGoal(e.goal); //put in goalsToRemove + logAi->debug("Goal %s decomposition failed: goal was completed as much as possible", e.goal->name()); + return; + } + catch (std::exception & e) + { + goalsToRemove.push_back(basicGoal); + logAi->debug("Goal %s decomposition failed: %s", basicGoal->name(), e.what()); + return; + } + if (elementarGoal->isAbstract) //we can decompose it further + { + goalsToAdd.push_back(elementarGoal); + //decompose further now - this is necesssary if we can't add over 10 goals in the pool + goalToDecompose = elementarGoal; + //there is a risk of infinite abstract goal loop, though it indicates failed logic + maxAbstractGoals--; + } + else if (elementarGoal->isElementar) //should be + { + logAi->debug("Found elementar goal %s", elementarGoal->name()); + ultimateGoalsFromBasic[elementarGoal].push_back(goalToDecompose); //TODO: how about indirect basicGoal? + break; + } + else //should never be here + throw cannotFulfillGoalException("Goal %s is neither abstract nor elementar!" + basicGoal->name()); + } + + //realize best goal + if (!elementarGoal->invalid()) + { + logAi->debug("Trying to realize %s (value %2.3f)", elementarGoal->name(), elementarGoal->priority); + + try + { + boost::this_thread::interruption_point(); + elementarGoal->accept(this); //visitor pattern + boost::this_thread::interruption_point(); + } + catch (boost::thread_interrupted & e) + { + (void)e; + logAi->debug("Player %d: Making turn thread received an interruption!", playerID); + throw; //rethrow, we want to truly end this thread + } + catch (goalFulfilledException & e) + { + //the sub-goal was completed successfully + completeGoal(e.goal); + //local goal was also completed + completeGoal(elementarGoal); + } + catch (std::exception & e) + { + logAi->debug("Failed to realize subgoal of type %s, I will stop.", elementarGoal->name()); + logAi->debug("The error message was: %s", e.what()); + + //erase base goal if we failed to execute decomposed goal + for (auto basicGoalToRemove : ultimateGoalsFromBasic[elementarGoal]) + goalsToRemove.push_back(basicGoalToRemove); + } + } +} + +Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal) +{ + if(ultimateGoal->isElementar) + { + logAi->warn("Trying to decompose elementar goal %s", ultimateGoal->name()); + + return ultimateGoal; + } + + const int searchDepth = 30; + + Goals::TSubgoal goal = ultimateGoal; + logAi->debug("Decomposing goal %s", ultimateGoal->name()); + int maxGoals = searchDepth; //preventing deadlock for mutually dependent goals + while (maxGoals) + { + boost::this_thread::interruption_point(); + + goal = goal->whatToDoToAchieve(); //may throw if decomposition fails + --maxGoals; + if (goal == ultimateGoal) //compare objects by value + if (goal->isElementar == ultimateGoal->isElementar) + throw cannotFulfillGoalException((boost::format("Goal dependency loop detected for %s!") + % ultimateGoal->name()).str()); + if (goal->isAbstract || goal->isElementar) + return goal; + else + logAi->debug("Considering: %s", goal->name()); + } + + throw cannotFulfillGoalException("Too many subgoals, don't know what to do"); +} + +void VCAI::performTypicalActions() +{ + for(auto h : getUnblockedHeroes()) + { + if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn + continue; + + logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining()); + makePossibleUpgrades(*h); + pickBestArtifacts(*h); + try + { + wander(h); + } + catch(std::exception & e) + { + logAi->debug("Cannot use this hero anymore, received exception: %s", e.what()); + continue; + } + } +} + +void VCAI::buildArmyIn(const CGTownInstance * t) +{ + makePossibleUpgrades(t->visitingHero); + makePossibleUpgrades(t); + recruitCreatures(t, t->getUpperArmy()); + moveCreaturesToHero(t); +} + +void VCAI::checkHeroArmy(HeroPtr h) +{ + auto it = lockedHeroes.find(h); + if(it != lockedHeroes.end()) + { + if(it->second->goalType == Goals::GATHER_ARMY && it->second->value <= h->getArmyStrength()) + completeGoal(sptr(Goals::GatherArmy(it->second->value).sethero(h))); + } +} + +void VCAI::recruitHero(const CGTownInstance * t, bool throwing) +{ + logAi->debug("Trying to recruit a hero in %s at %s", t->getNameTranslated(), t->visitablePos().toString()); + + auto heroes = cb->getAvailableHeroes(t); + if(heroes.size()) + { + auto hero = heroes[0]; + if(heroes.size() >= 2) //makes sense to recruit two heroes with starting amries in first week + { + if(heroes[1]->getTotalStrength() > hero->getTotalStrength()) + hero = heroes[1]; + } + cb->recruitHero(t, hero); + throw goalFulfilledException(sptr(Goals::RecruitHero().settown(t))); + } + else if(throwing) + { + throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName()); + } +} + +void VCAI::finish() +{ + //we want to lock to avoid multiple threads from calling makingTurn->join() at same time + boost::lock_guard multipleCleanupGuard(turnInterruptionMutex); + if(makingTurn) + { + makingTurn->interrupt(); + makingTurn->join(); + makingTurn.reset(); + } +} + +void VCAI::requestActionASAP(std::function whatToDo) +{ + boost::thread newThread([this, whatToDo]() + { + setThreadName("VCAI::requestActionASAP::whatToDo"); + SET_GLOBAL_STATE(this); + boost::shared_lock gsLock(CGameState::mutex); + whatToDo(); + }); + + newThread.detach(); +} + +void VCAI::lostHero(HeroPtr h) +{ + logAi->debug("I lost my hero %s. It's best to forget and move on.", h.name); + + vstd::erase_if_present(lockedHeroes, h); + for(auto obj : reservedHeroesMap[h]) + { + vstd::erase_if_present(reservedObjs, obj); //unreserve all objects for that hero + } + vstd::erase_if_present(reservedHeroesMap, h); + vstd::erase_if_present(visitedHeroes, h); + for (auto heroVec : visitedHeroes) + { + vstd::erase_if_present(heroVec.second, h); + } + + //remove goals with removed hero assigned from main loop + vstd::erase_if(ultimateGoalsFromBasic, [&](const std::pair & x) -> bool + { + if(x.first->hero == h) + return true; + else + return false; + }); + + auto removedHeroGoalPredicate = [&](const Goals::TSubgoal & x) ->bool + { + if(x->hero == h) + return true; + else + return false; + }; + + vstd::erase_if(basicGoals, removedHeroGoalPredicate); + vstd::erase_if(goalsToAdd, removedHeroGoalPredicate); + vstd::erase_if(goalsToRemove, removedHeroGoalPredicate); + + for(auto goal : ultimateGoalsFromBasic) + vstd::erase_if(goal.second, removedHeroGoalPredicate); +} + +void VCAI::answerQuery(QueryID queryID, int selection) +{ + logAi->debug("I'll answer the query %d giving the choice %d", queryID, selection); + if(queryID != QueryID(-1)) + { + cb->selectionMade(selection, queryID); + } + else + { + logAi->debug("Since the query ID is %d, the answer won't be sent. This is not a real query!", queryID); + //do nothing + } +} + +void VCAI::requestSent(const CPackForServer * pack, int requestID) +{ + //BNLOG("I have sent request of type %s", typeid(*pack).name()); + if(auto reply = dynamic_cast(pack)) + { + status.attemptedAnsweringQuery(reply->qid, requestID); + } +} + +std::string VCAI::getBattleAIName() const +{ + if(settings["server"]["enemyAI"].getType() == JsonNode::JsonType::DATA_STRING) + return settings["server"]["enemyAI"].String(); + else + return "BattleAI"; +} + +void VCAI::validateObject(const CGObjectInstance * obj) +{ + validateObject(obj->id); +} + +void VCAI::validateObject(ObjectIdRef obj) +{ + auto matchesId = [&](const CGObjectInstance * hlpObj) -> bool + { + return hlpObj->id == obj.id; + }; + if(!obj) + { + vstd::erase_if(visitableObjs, matchesId); + + for(auto & p : reservedHeroesMap) + vstd::erase_if(p.second, matchesId); + + vstd::erase_if(reservedObjs, matchesId); + } +} + +AIStatus::AIStatus() +{ + battle = NO_BATTLE; + havingTurn = false; + ongoingHeroMovement = false; + ongoingChannelProbing = false; +} + +AIStatus::~AIStatus() +{ + +} + +void AIStatus::setBattle(BattleState BS) +{ + boost::unique_lock lock(mx); + LOG_TRACE_PARAMS(logAi, "battle state=%d", (int)BS); + battle = BS; + cv.notify_all(); +} + +BattleState AIStatus::getBattle() +{ + boost::unique_lock lock(mx); + return battle; +} + +void AIStatus::addQuery(QueryID ID, std::string description) +{ + if(ID == QueryID(-1)) + { + logAi->debug("The \"query\" has an id %d, it'll be ignored as non-query. Description: %s", ID, description); + return; + } + + assert(ID.getNum() >= 0); + boost::unique_lock lock(mx); + + assert(!vstd::contains(remainingQueries, ID)); + + remainingQueries[ID] = description; + + cv.notify_all(); + logAi->debug("Adding query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); +} + +void AIStatus::removeQuery(QueryID ID) +{ + boost::unique_lock lock(mx); + assert(vstd::contains(remainingQueries, ID)); + + std::string description = remainingQueries[ID]; + remainingQueries.erase(ID); + + cv.notify_all(); + logAi->debug("Removing query %d - %s. Total queries count: %d", ID, description, remainingQueries.size()); +} + +int AIStatus::getQueriesCount() +{ + boost::unique_lock lock(mx); + return static_cast(remainingQueries.size()); +} + +void AIStatus::startedTurn() +{ + boost::unique_lock lock(mx); + havingTurn = true; + cv.notify_all(); +} + +void AIStatus::madeTurn() +{ + boost::unique_lock lock(mx); + havingTurn = false; + cv.notify_all(); +} + +void AIStatus::waitTillFree() +{ + boost::unique_lock lock(mx); + while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) + cv.wait_for(lock, boost::chrono::milliseconds(100)); +} + +bool AIStatus::haveTurn() +{ + boost::unique_lock lock(mx); + return havingTurn; +} + +void AIStatus::attemptedAnsweringQuery(QueryID queryID, int answerRequestID) +{ + boost::unique_lock lock(mx); + assert(vstd::contains(remainingQueries, queryID)); + std::string description = remainingQueries[queryID]; + logAi->debug("Attempted answering query %d - %s. Request id=%d. Waiting for results...", queryID, description, answerRequestID); + requestToQueryID[answerRequestID] = queryID; +} + +void AIStatus::receivedAnswerConfirmation(int answerRequestID, int result) +{ + QueryID query; + + { + boost::unique_lock lock(mx); + + assert(vstd::contains(requestToQueryID, answerRequestID)); + query = requestToQueryID[answerRequestID]; + assert(vstd::contains(remainingQueries, query)); + requestToQueryID.erase(answerRequestID); + } + + if(result) + { + removeQuery(query); + } + else + { + logAi->error("Something went really wrong, failed to answer query %d : %s", query.getNum(), remainingQueries[query]); + //TODO safely retry + } +} + +void AIStatus::heroVisit(const CGObjectInstance * obj, bool started) +{ + boost::unique_lock lock(mx); + if(started) + { + objectsBeingVisited.push_back(obj); + } + else + { + // There can be more than one object visited at the time (eg. hero visits Subterranean Gate + // causing visit to hero on the other side. + // However, we are guaranteed that start/end visit notification maintain stack order. + assert(!objectsBeingVisited.empty()); + objectsBeingVisited.pop_back(); + } + cv.notify_all(); +} + +void AIStatus::setMove(bool ongoing) +{ + boost::unique_lock lock(mx); + ongoingHeroMovement = ongoing; + cv.notify_all(); +} + +void AIStatus::setChannelProbing(bool ongoing) +{ + boost::unique_lock lock(mx); + ongoingChannelProbing = ongoing; + cv.notify_all(); +} + +bool AIStatus::channelProbing() +{ + return ongoingChannelProbing; +} + + + +bool isWeeklyRevisitable(const CGObjectInstance * obj) +{ + //TODO: allow polling of remaining creatures in dwelling + if(const auto * rewardable = dynamic_cast(obj)) + return rewardable->configuration.getResetDuration() == 7; + + if(dynamic_cast(obj)) + return true; + if(dynamic_cast(obj)) //banks tend to respawn often in mods + return true; + + switch(obj->ID) + { + case Obj::STABLES: + case Obj::MAGIC_WELL: + case Obj::HILL_FORT: + return true; + case Obj::BORDER_GATE: + case Obj::BORDERGUARD: + return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); //FIXME: they could be revisited sooner than in a week + } + return false; +} + +bool shouldVisit(HeroPtr h, const CGObjectInstance * obj) +{ + switch(obj->ID) + { + case Obj::TOWN: + case Obj::HERO: //never visit our heroes at random + return obj->tempOwner != h->tempOwner; //do not visit our towns at random + case Obj::BORDER_GATE: + { + for(auto q : ai->myCb->getMyQuests()) + { + if(q.obj == obj) + { + return false; // do not visit guards or gates when wandering + } + } + return true; //we don't have this quest yet + } + case Obj::BORDERGUARD: //open borderguard if possible + return (dynamic_cast(obj))->wasMyColorVisited(ai->playerID); + case Obj::SEER_HUT: + case Obj::QUEST_GUARD: + { + for(auto q : ai->myCb->getMyQuests()) + { + if(q.obj == obj) + { + if(q.quest->checkQuest(h.h)) + return true; //we completed the quest + else + return false; //we can't complete this quest + } + } + return true; //we don't have this quest yet + } + case Obj::CREATURE_GENERATOR1: + { + if(obj->tempOwner != h->tempOwner) + return true; //flag just in case + bool canRecruitCreatures = false; + const CGDwelling * d = dynamic_cast(obj); + for(auto level : d->creatures) + { + for(auto c : level.second) + { + if(h->getSlotFor(CreatureID(c)) != SlotID()) + canRecruitCreatures = true; + } + } + return canRecruitCreatures; + } + case Obj::HILL_FORT: + { + for(auto slot : h->Slots()) + { + if(slot.second->type->hasUpgrades()) + return true; //TODO: check price? + } + return false; + } + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_ONE_WAY_EXIT: + case Obj::MONOLITH_TWO_WAY: + case Obj::WHIRLPOOL: + return false; + case Obj::SCHOOL_OF_MAGIC: + case Obj::SCHOOL_OF_WAR: + { + if (ai->ah->freeGold() < 1000) + return false; + break; + } + case Obj::LIBRARY_OF_ENLIGHTENMENT: + if(h->level < 12) + return false; + break; + case Obj::TREE_OF_KNOWLEDGE: + { + TResources myRes = ai->ah->freeResources(); + if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10) + return false; + break; + } + case Obj::MAGIC_WELL: + return h->mana < h->manaLimit(); + case Obj::PRISON: + return ai->myCb->getHeroesInfo().size() < VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + case Obj::TAVERN: + { + //TODO: make AI actually recruit heroes + //TODO: only on request + if(ai->myCb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + return false; + else if(ai->ah->freeGold() < GameConstants::HERO_GOLD_COST) + return false; + break; + } + case Obj::BOAT: + return false; + //Boats are handled by pathfinder + case Obj::EYE_OF_MAGI: + return false; //this object is useless to visit, but could be visited indefinitely + } + + if(obj->wasVisited(*h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); + return false; + + return true; +} + +std::optional VCAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index f1935eb3b..bbf754210 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -1,407 +1,407 @@ -/* - * VCAI.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 "AIUtility.h" -#include "Goals/AbstractGoal.h" -#include "../../lib/AI_Base.h" -#include "../../CCallback.h" - -#include "../../lib/CThreadHelper.h" - -#include "../../lib/GameConstants.h" -#include "../../lib/VCMI_Lib.h" -#include "../../lib/CBuildingHandler.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/CondSh.h" -#include "Pathfinding/AIPathfinder.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class AIhelper; - -class AIStatus -{ - boost::mutex mx; - boost::condition_variable cv; - - BattleState battle; - std::map remainingQueries; - std::map requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query) - std::vector objectsBeingVisited; - bool ongoingHeroMovement; - bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits - - bool havingTurn; - -public: - AIStatus(); - ~AIStatus(); - void setBattle(BattleState BS); - void setMove(bool ongoing); - void setChannelProbing(bool ongoing); - bool channelProbing(); - BattleState getBattle(); - void addQuery(QueryID ID, std::string description); - void removeQuery(QueryID ID); - int getQueriesCount(); - void startedTurn(); - void madeTurn(); - void waitTillFree(); - bool haveTurn(); - void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); - void receivedAnswerConfirmation(int answerRequestID, int result); - void heroVisit(const CGObjectInstance * obj, bool started); - - - template void serialize(Handler & h, const int version) - { - h & battle; - h & remainingQueries; - h & requestToQueryID; - h & havingTurn; - } -}; - -class DLL_EXPORT VCAI : public CAdventureAI -{ -public: - - friend class FuzzyHelper; - friend class ResourceManager; - friend class BuildingManager; - - std::map> knownTeleportChannels; - std::map knownSubterraneanGates; - ObjectInstanceID destinationTeleport; - int3 destinationTeleportPos; - std::vector teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored - //std::vector visitedThisWeek; //only OPWs - std::map> townVisitsThisWeek; - - //part of mainLoop, but accessible from outisde - std::vector basicGoals; - Goals::TGoalVec goalsToRemove; - Goals::TGoalVec goalsToAdd; - std::map ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals - - std::set invalidPathHeroes; //FIXME, just a workaround - std::map lockedHeroes; //TODO: allow non-elementar objectives - std::map> reservedHeroesMap; //objects reserved by specific heroes - std::set heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game - - //sets are faster to search, also do not contain duplicates - std::set visitableObjs; - std::set alreadyVisited; - std::set reservedObjs; //to be visited by specific hero - std::map> visitedHeroes; //visited this turn //FIXME: this is just bug workaround - - AIStatus status; - std::string battlename; - - std::shared_ptr myCb; - - std::unique_ptr makingTurn; -private: - boost::mutex turnInterruptionMutex; -public: - ObjectInstanceID selectedObject; - - AIhelper * ah; - - VCAI(); - virtual ~VCAI(); - - //TODO: use only smart pointers? - void tryRealize(Goals::Explore & g); - void tryRealize(Goals::RecruitHero & g); - void tryRealize(Goals::VisitTile & g); - void tryRealize(Goals::VisitObj & g); - void tryRealize(Goals::VisitHero & g); - void tryRealize(Goals::BuildThis & g); - void tryRealize(Goals::DigAtTile & g); - void tryRealize(Goals::Trade & g); - void tryRealize(Goals::BuyArmy & g); - void tryRealize(Goals::Invalid & g); - void tryRealize(Goals::AbstractGoal & g); - - bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved - - std::string getBattleAIName() const override; - - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - void yourTurn(QueryID queryID) override; - - void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO - void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done - void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; - void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading - void finish() override; - - void availableCreaturesChanged(const CGDwelling * town) override; - void heroMoved(const TryMoveHero & details, bool verbose = true) override; - void heroInGarrisonChange(const CGTownInstance * town) override; - void centerView(int3 pos, int focusTime) override; - void tileHidden(const std::unordered_set & pos) override; - void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; - void artifactAssembled(const ArtifactLocation & al) override; - void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; - void showThievesGuildWindow(const CGObjectInstance * obj) override; - void playerBlocked(int reason, bool start) override; - void showPuzzleMap() override; - void showShipyardDialog(const IShipyard * obj) override; - void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; - void artifactPut(const ArtifactLocation & al) override; - void artifactRemoved(const ArtifactLocation & al) override; - void artifactDisassembled(const ArtifactLocation & al) override; - void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; - void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; - void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; - void tileRevealed(const std::unordered_set & pos) override; - void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; - void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; - void heroMovePointsChanged(const CGHeroInstance * hero) override; - void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; - void newObject(const CGObjectInstance * obj) override; - void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; - void playerBonusChanged(const Bonus & bonus, bool gain) override; - void heroCreated(const CGHeroInstance *) override; - void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; - void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; - void requestRealized(PackageApplied * pa) override; - void receivedResource() override; - void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; - void heroManaPointsChanged(const CGHeroInstance * hero) override; - void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; - void battleResultsApplied() override; - void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; - void objectPropertyChanged(const SetObjectProperty * sop) override; - void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; - void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; - void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; - void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - - void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; - void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; - std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - - void makeTurn(); - void mainLoop(); - void performTypicalActions(); - - void buildArmyIn(const CGTownInstance * t); - void striveToGoal(Goals::TSubgoal ultimateGoal); - Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal); - void endTurn(); - void wander(HeroPtr h); - void setGoal(HeroPtr h, Goals::TSubgoal goal); - void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any - void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero - - void recruitHero(const CGTownInstance * t, bool throwing = false); - bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit = std::nullopt); - bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const; - //void recruitCreatures(const CGTownInstance * t); - void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); - void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack - void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr); - void moveCreaturesToHero(const CGTownInstance * t); - void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); - - bool moveHeroToTile(int3 dst, HeroPtr h); - void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager - - void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on) - void waitTillFree(); - - void addVisitableObj(const CGObjectInstance * obj); - void markObjectVisited(const CGObjectInstance * obj); - void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit - void unreserveObject(HeroPtr h, const CGObjectInstance * obj); - - void markHeroUnableToExplore(HeroPtr h); - void markHeroAbleToExplore(HeroPtr h); - bool isAbleToExplore(HeroPtr h); - void clearPathsInfo(); - - void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it - void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it - void validateVisitableObjs(); - void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; - void retrieveVisitableObjs(); - virtual std::vector getFlaggedObjects() const; - - const CGObjectInstance * lookForArt(int aid) const; - bool isAccessible(const int3 & pos) const; - HeroPtr getHeroWithGrail() const; - - const CGObjectInstance * getUnvisitedObj(const std::function & predicate); - bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; - //optimization - use one SM for every hero call - - const CGTownInstance * findTownWithTavern() const; - bool canRecruitAnyHero(const CGTownInstance * t = NULL) const; - - Goals::TSubgoal getGoal(HeroPtr h) const; - bool canAct(HeroPtr h) const; - std::vector getUnblockedHeroes() const; - std::vector getMyHeroes() const; - HeroPtr primaryHero() const; - void checkHeroArmy(HeroPtr h); - - void requestSent(const CPackForServer * pack, int requestID) override; - void answerQuery(QueryID queryID, int selection); - //special function that can be called ONLY from game events handling thread and will send request ASAP - void requestActionASAP(std::function whatToDo); - - #if 0 - //disabled due to issue 2890 - template void registerGoals(Handler & h) - { - //h.template registerType(); - h.template registerType(); - h.template registerType(); - //h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - //h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - h.template registerType(); - } - #endif - - template void serializeInternal(Handler & h, const int version) - { - h & knownTeleportChannels; - h & knownSubterraneanGates; - h & destinationTeleport; - h & townVisitsThisWeek; - - #if 0 - //disabled due to issue 2890 - h & lockedHeroes; - #else - { - ui32 length = 0; - h & length; - if(!h.saving) - { - std::set loadedPointers; - lockedHeroes.clear(); - for(ui32 index = 0; index < length; index++) - { - HeroPtr ignored1; - h & ignored1; - - ui8 flag = 0; - h & flag; - - if(flag) - { - ui32 pid = 0xffffffff; - h & pid; - - if(!vstd::contains(loadedPointers, pid)) - { - loadedPointers.insert(pid); - - ui16 typeId = 0; - //this is the problem requires such hack - //we have to explicitly ignore invalid goal class type id - h & typeId; - Goals::AbstractGoal ignored2; - ignored2.serialize(h, version); - } - } - } - } - } - #endif - - h & reservedHeroesMap; //FIXME: cannot instantiate abstract class - h & visitableObjs; - h & alreadyVisited; - h & reservedObjs; - h & status; - h & battlename; - h & heroesUnableToExplore; - - //myCB is restored after load by init call - } -}; - -class cannotFulfillGoalException : public std::exception -{ - std::string msg; - -public: - explicit cannotFulfillGoalException(crstring _Message) - : msg(_Message) - { - } - - virtual ~cannotFulfillGoalException() throw () - { - }; - - const char * what() const throw () override - { - return msg.c_str(); - } -}; - -class goalFulfilledException : public std::exception -{ - std::string msg; - -public: - Goals::TSubgoal goal; - - explicit goalFulfilledException(Goals::TSubgoal Goal) - : goal(Goal) - { - msg = goal->name(); - } - - virtual ~goalFulfilledException() throw () - { - }; - - const char * what() const throw () override - { - return msg.c_str(); - } -}; - -void makePossibleUpgrades(const CArmedInstance * obj); +/* + * VCAI.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 "AIUtility.h" +#include "Goals/AbstractGoal.h" +#include "../../lib/AI_Base.h" +#include "../../CCallback.h" + +#include "../../lib/CThreadHelper.h" + +#include "../../lib/GameConstants.h" +#include "../../lib/VCMI_Lib.h" +#include "../../lib/CBuildingHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/CondSh.h" +#include "Pathfinding/AIPathfinder.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class AIhelper; + +class AIStatus +{ + boost::mutex mx; + boost::condition_variable cv; + + BattleState battle; + std::map remainingQueries; + std::map requestToQueryID; //IDs of answer-requests sent to server => query ids (so we can match answer confirmation from server to the query) + std::vector objectsBeingVisited; + bool ongoingHeroMovement; + bool ongoingChannelProbing; // true if AI currently explore bidirectional teleport channel exits + + bool havingTurn; + +public: + AIStatus(); + ~AIStatus(); + void setBattle(BattleState BS); + void setMove(bool ongoing); + void setChannelProbing(bool ongoing); + bool channelProbing(); + BattleState getBattle(); + void addQuery(QueryID ID, std::string description); + void removeQuery(QueryID ID); + int getQueriesCount(); + void startedTurn(); + void madeTurn(); + void waitTillFree(); + bool haveTurn(); + void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); + void receivedAnswerConfirmation(int answerRequestID, int result); + void heroVisit(const CGObjectInstance * obj, bool started); + + + template void serialize(Handler & h, const int version) + { + h & battle; + h & remainingQueries; + h & requestToQueryID; + h & havingTurn; + } +}; + +class DLL_EXPORT VCAI : public CAdventureAI +{ +public: + + friend class FuzzyHelper; + friend class ResourceManager; + friend class BuildingManager; + + std::map> knownTeleportChannels; + std::map knownSubterraneanGates; + ObjectInstanceID destinationTeleport; + int3 destinationTeleportPos; + std::vector teleportChannelProbingList; //list of teleport channel exits that not visible and need to be (re-)explored + //std::vector visitedThisWeek; //only OPWs + std::map> townVisitsThisWeek; + + //part of mainLoop, but accessible from outisde + std::vector basicGoals; + Goals::TGoalVec goalsToRemove; + Goals::TGoalVec goalsToAdd; + std::map ultimateGoalsFromBasic; //theoreticlaly same goal can fulfill multiple basic goals + + std::set invalidPathHeroes; //FIXME, just a workaround + std::map lockedHeroes; //TODO: allow non-elementar objectives + std::map> reservedHeroesMap; //objects reserved by specific heroes + std::set heroesUnableToExplore; //these heroes will not be polled for exploration in current state of game + + //sets are faster to search, also do not contain duplicates + std::set visitableObjs; + std::set alreadyVisited; + std::set reservedObjs; //to be visited by specific hero + std::map> visitedHeroes; //visited this turn //FIXME: this is just bug workaround + + AIStatus status; + std::string battlename; + + std::shared_ptr myCb; + + std::unique_ptr makingTurn; +private: + boost::mutex turnInterruptionMutex; +public: + ObjectInstanceID selectedObject; + + AIhelper * ah; + + VCAI(); + virtual ~VCAI(); + + //TODO: use only smart pointers? + void tryRealize(Goals::Explore & g); + void tryRealize(Goals::RecruitHero & g); + void tryRealize(Goals::VisitTile & g); + void tryRealize(Goals::VisitObj & g); + void tryRealize(Goals::VisitHero & g); + void tryRealize(Goals::BuildThis & g); + void tryRealize(Goals::DigAtTile & g); + void tryRealize(Goals::Trade & g); + void tryRealize(Goals::BuyArmy & g); + void tryRealize(Goals::Invalid & g); + void tryRealize(Goals::AbstractGoal & g); + + bool isTileNotReserved(const CGHeroInstance * h, int3 t) const; //the tile is not occupied by allied hero and the object is not reserved + + std::string getBattleAIName() const override; + + void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + void yourTurn(QueryID queryID) override; + + void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + void commanderGotLevel(const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; //TODO + void showBlockingDialog(const std::string & text, const std::vector & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; + void saveGame(BinarySerializer & h, const int version) override; //saving + void loadGame(BinaryDeserializer & h, const int version) override; //loading + void finish() override; + + void availableCreaturesChanged(const CGDwelling * town) override; + void heroMoved(const TryMoveHero & details, bool verbose = true) override; + void heroInGarrisonChange(const CGTownInstance * town) override; + void centerView(int3 pos, int focusTime) override; + void tileHidden(const std::unordered_set & pos) override; + void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; + void artifactAssembled(const ArtifactLocation & al) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; + void showThievesGuildWindow(const CGObjectInstance * obj) override; + void playerBlocked(int reason, bool start) override; + void showPuzzleMap() override; + void showShipyardDialog(const IShipyard * obj) override; + void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; + void artifactPut(const ArtifactLocation & al) override; + void artifactRemoved(const ArtifactLocation & al) override; + void artifactDisassembled(const ArtifactLocation & al) override; + void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; + void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override; + void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; + void tileRevealed(const std::unordered_set & pos) override; + void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; + void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override; + void heroMovePointsChanged(const CGHeroInstance * hero) override; + void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; + void newObject(const CGObjectInstance * obj) override; + void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; + void playerBonusChanged(const Bonus & bonus, bool gain) override; + void heroCreated(const CGHeroInstance *) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; + void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; + void requestRealized(PackageApplied * pa) override; + void receivedResource() override; + void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; + void heroManaPointsChanged(const CGHeroInstance * hero) override; + void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; + void battleResultsApplied() override; + void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; + void objectPropertyChanged(const SetObjectProperty * sop) override; + void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; + void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override; + void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override; + void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; + + void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override; + void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + + void makeTurn(); + void mainLoop(); + void performTypicalActions(); + + void buildArmyIn(const CGTownInstance * t); + void striveToGoal(Goals::TSubgoal ultimateGoal); + Goals::TSubgoal decomposeGoal(Goals::TSubgoal ultimateGoal); + void endTurn(); + void wander(HeroPtr h); + void setGoal(HeroPtr h, Goals::TSubgoal goal); + void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any + void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero + + void recruitHero(const CGTownInstance * t, bool throwing = false); + bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, std::optional movementCostLimit = std::nullopt); + bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, const AIPath & path) const; + //void recruitCreatures(const CGTownInstance * t); + void recruitCreatures(const CGDwelling * d, const CArmedInstance * recruiter); + void pickBestCreatures(const CArmedInstance * army, const CArmedInstance * source); //called when we can't find a slot for new stack + void pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * other = nullptr); + void moveCreaturesToHero(const CGTownInstance * t); + void performObjectInteraction(const CGObjectInstance * obj, HeroPtr h); + + bool moveHeroToTile(int3 dst, HeroPtr h); + void buildStructure(const CGTownInstance * t, BuildingID building); //TODO: move to BuildingManager + + void lostHero(HeroPtr h); //should remove all references to hero (assigned tasks and so on) + void waitTillFree(); + + void addVisitableObj(const CGObjectInstance * obj); + void markObjectVisited(const CGObjectInstance * obj); + void reserveObject(HeroPtr h, const CGObjectInstance * obj); //TODO: reserve all objects that heroes attempt to visit + void unreserveObject(HeroPtr h, const CGObjectInstance * obj); + + void markHeroUnableToExplore(HeroPtr h); + void markHeroAbleToExplore(HeroPtr h); + bool isAbleToExplore(HeroPtr h); + void clearPathsInfo(); + + void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it + void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it + void validateVisitableObjs(); + void retrieveVisitableObjs(std::vector & out, bool includeOwned = false) const; + void retrieveVisitableObjs(); + virtual std::vector getFlaggedObjects() const; + + const CGObjectInstance * lookForArt(int aid) const; + bool isAccessible(const int3 & pos) const; + HeroPtr getHeroWithGrail() const; + + const CGObjectInstance * getUnvisitedObj(const std::function & predicate); + bool isAccessibleForHero(const int3 & pos, HeroPtr h, bool includeAllies = false) const; + //optimization - use one SM for every hero call + + const CGTownInstance * findTownWithTavern() const; + bool canRecruitAnyHero(const CGTownInstance * t = NULL) const; + + Goals::TSubgoal getGoal(HeroPtr h) const; + bool canAct(HeroPtr h) const; + std::vector getUnblockedHeroes() const; + std::vector getMyHeroes() const; + HeroPtr primaryHero() const; + void checkHeroArmy(HeroPtr h); + + void requestSent(const CPackForServer * pack, int requestID) override; + void answerQuery(QueryID queryID, int selection); + //special function that can be called ONLY from game events handling thread and will send request ASAP + void requestActionASAP(std::function whatToDo); + + #if 0 + //disabled due to issue 2890 + template void registerGoals(Handler & h) + { + //h.template registerType(); + h.template registerType(); + h.template registerType(); + //h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + //h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + h.template registerType(); + } + #endif + + template void serializeInternal(Handler & h, const int version) + { + h & knownTeleportChannels; + h & knownSubterraneanGates; + h & destinationTeleport; + h & townVisitsThisWeek; + + #if 0 + //disabled due to issue 2890 + h & lockedHeroes; + #else + { + ui32 length = 0; + h & length; + if(!h.saving) + { + std::set loadedPointers; + lockedHeroes.clear(); + for(ui32 index = 0; index < length; index++) + { + HeroPtr ignored1; + h & ignored1; + + ui8 flag = 0; + h & flag; + + if(flag) + { + ui32 pid = 0xffffffff; + h & pid; + + if(!vstd::contains(loadedPointers, pid)) + { + loadedPointers.insert(pid); + + ui16 typeId = 0; + //this is the problem requires such hack + //we have to explicitly ignore invalid goal class type id + h & typeId; + Goals::AbstractGoal ignored2; + ignored2.serialize(h, version); + } + } + } + } + } + #endif + + h & reservedHeroesMap; //FIXME: cannot instantiate abstract class + h & visitableObjs; + h & alreadyVisited; + h & reservedObjs; + h & status; + h & battlename; + h & heroesUnableToExplore; + + //myCB is restored after load by init call + } +}; + +class cannotFulfillGoalException : public std::exception +{ + std::string msg; + +public: + explicit cannotFulfillGoalException(crstring _Message) + : msg(_Message) + { + } + + virtual ~cannotFulfillGoalException() throw () + { + }; + + const char * what() const throw () override + { + return msg.c_str(); + } +}; + +class goalFulfilledException : public std::exception +{ + std::string msg; + +public: + Goals::TSubgoal goal; + + explicit goalFulfilledException(Goals::TSubgoal Goal) + : goal(Goal) + { + msg = goal->name(); + } + + virtual ~goalFulfilledException() throw () + { + }; + + const char * what() const throw () override + { + return msg.c_str(); + } +}; + +void makePossibleUpgrades(const CArmedInstance * obj); diff --git a/AUTHORS.h b/AUTHORS.h index 97a716d04..483d475af 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -1,53 +1,53 @@ -/* - * AUTHORS.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 - -//VCMI PROJECT CODE CONTRIBUTORS: -std::vector> contributors = { -// Task Name Aka E-Mail - { "Idea", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, - { "Idea", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, - - { "Developing", "Andrea Palmate", "afxgroup", "andrea@amigasoft.net" }, - { "Developing", "Alexander Shishkin", "alexvins", "" }, - { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, - { "Developing", "Andrii Danylchenko", "", "" }, - { "Developing", "Benjamin Gentner", "beegee", "" }, - { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, - { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, - { "Developing", "", "Dydzio", "blood990@gmail.com" }, - { "Developing", "Andrzej Żak", "godric3", "" }, - { "Developing", "Henning Koehler", "henningkoehlernz", "henning.koehler.nz@gmail.com" }, - { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, - { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, - { "Developing", "", "krs0", "" }, - { "Developing", "", "Laserlicht", "" }, - { "Developing", "Alexey", "Macron1Robot", "" }, - { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, - { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, - { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, - { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, - { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, - { "Developing", "", "rilian-la-te", "" }, - { "Developing", "", "SoundSSGood", "" }, - { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, - { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, - { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, - { "Developing", "Trevor Standley", "tstandley", "" }, - { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, - { "Developing", "Frank Zago", "ubuntux", "" }, - { "Developing", "", "vmarkovtsev", "" }, - { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, - { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, - - { "Testing", "Ben Yan", "by003", "benyan9110@gmail.com," }, - { "Testing", "", "Misiokles", "" }, - { "Testing", "", "Povelitel", "" }, -}; +/* + * AUTHORS.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 + +//VCMI PROJECT CODE CONTRIBUTORS: +std::vector> contributors = { +// Task Name Aka E-Mail + { "Idea", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, + { "Idea", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, + + { "Developing", "Andrea Palmate", "afxgroup", "andrea@amigasoft.net" }, + { "Developing", "Alexander Shishkin", "alexvins", "" }, + { "Developing", "Rafal R.", "ambtrip", "ambtrip@wp.pl" }, + { "Developing", "Andrii Danylchenko", "", "" }, + { "Developing", "Benjamin Gentner", "beegee", "" }, + { "Developing", "Piotr Wójcik", "Chocimier", "chocimier@tlen.pl" }, + { "Developing", "Dmitry Orlov", "", "shubus.corporation@gmail.com" }, + { "Developing", "", "Dydzio", "blood990@gmail.com" }, + { "Developing", "Andrzej Żak", "godric3", "" }, + { "Developing", "Henning Koehler", "henningkoehlernz", "henning.koehler.nz@gmail.com" }, + { "Developing", "Ivan Savenko", "", "saven.ivan@gmail.com" }, + { "Developing", "", "kambala-decapitator", "decapitator@ukr.net" }, + { "Developing", "", "krs0", "" }, + { "Developing", "", "Laserlicht", "" }, + { "Developing", "Alexey", "Macron1Robot", "" }, + { "Developing", "Michał Kalinowski", "", "feniks_fire@o2.pl" }, + { "Developing", "Vadim Glazunov", "neweagle", "neweagle@gmail.com" }, + { "Developing", "Andrey Cherkas", "nordsoft", "nordsoft@yahoo.com" }, + { "Developing", "Rickard Westerlund", "Onion Knight", "onionknigh@gmail.com" }, + { "Developing", "Yifeng Sun", "phoebus118", "pkusunyifeng@gmail.com" }, + { "Developing", "", "rilian-la-te", "" }, + { "Developing", "", "SoundSSGood", "" }, + { "Developing", "Stefan Pavlov", "Ste", "mailste@gmail.com" }, + { "Developing", "Arseniy Shestakov", "SXX", "me@arseniyshestakov.com" }, + { "Developing", "Lukasz Wychrystenko", "tezeriusz", "t0@czlug.icis.pcz.pl" }, + { "Developing", "Trevor Standley", "tstandley", "" }, + { "Developing", "Vadim Markovtsev", "", "gmarkhor@gmail.com" }, + { "Developing", "Frank Zago", "ubuntux", "" }, + { "Developing", "", "vmarkovtsev", "" }, + { "Developing", "Tom Zielinski", "Warmonger", "Warmonger@vp.pl" }, + { "Developing", "Xiaomin Ding", "", "dingding303@gmail.com" }, + + { "Testing", "Ben Yan", "by003", "benyan9110@gmail.com," }, + { "Testing", "", "Misiokles", "" }, + { "Testing", "", "Povelitel", "" }, +}; diff --git a/CCallback.cpp b/CCallback.cpp index 4a9348bec..0758967b9 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -1,423 +1,423 @@ -/* - * CCallback.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 "CCallback.h" - -#include "lib/CCreatureHandler.h" -#include "lib/gameState/CGameState.h" -#include "client/CPlayerInterface.h" -#include "client/Client.h" -#include "lib/mapping/CMap.h" -#include "lib/CBuildingHandler.h" -#include "lib/CGeneralTextHandler.h" -#include "lib/CHeroHandler.h" -#include "lib/NetPacks.h" -#include "lib/CArtHandler.h" -#include "lib/GameConstants.h" -#include "lib/CPlayerState.h" -#include "lib/UnlockGuard.h" -#include "lib/battle/BattleInfo.h" - -bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) -{ - CastleTeleportHero pack(who->id, where->id, 1); - sendRequest(&pack); - return true; -} - -bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) -{ - MoveHero pack(dst,h->id,transit); - sendRequest(&pack); - return true; -} - -int CCallback::selectionMade(int selection, QueryID queryID) -{ - return sendQueryReply(selection, queryID); -} - -int CCallback::sendQueryReply(std::optional reply, QueryID queryID) -{ - ASSERT_IF_CALLED_WITH_PLAYER - if(queryID == QueryID(-1)) - { - logGlobal->error("Cannot answer the query -1!"); - return -1; - } - - QueryReply pack(queryID, reply); - pack.player = *player; - return sendRequest(&pack); -} - -void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) -{ - // TODO exception for neutral dwellings shouldn't be hardcoded - if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP) - return; - - RecruitCreatures pack(obj->id, dst->id, ID, amount, level); - sendRequest(&pack); -} - -bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) -{ - if((player && obj->tempOwner != player) || (obj->stacksCount()<2 && obj->needsLastStack())) - return false; - - DisbandCreature pack(stackPos,obj->id); - sendRequest(&pack); - return true; -} - -bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) -{ - UpgradeCreature pack(stackPos,obj->id,newID); - sendRequest(&pack); - return false; -} - -void CCallback::endTurn() -{ - logGlobal->trace("Player %d ended his turn.", player->getNum()); - EndTurn pack; - sendRequest(&pack); -} -int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) -{ - ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0); - sendRequest(&pack); - return 0; -} - -int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) -{ - ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0); - sendRequest(&pack); - return 0; -} - -int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) -{ - ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) -{ - BulkMoveArmy pack(srcArmy, destArmy, srcSlot); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) -{ - BulkSplitStack pack(armyId, srcSlot, howMany); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) -{ - BulkSmartSplitStack pack(armyId, srcSlot); - sendRequest(&pack); - return 0; -} - -int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) -{ - BulkMergeStacks pack(armyId, srcSlot); - sendRequest(&pack); - return 0; -} - -bool CCallback::dismissHero(const CGHeroInstance *hero) -{ - if(player!=hero->tempOwner) return false; - - DismissHero pack(hero->id); - sendRequest(&pack); - return true; -} - -bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) -{ - ExchangeArtifacts ea; - ea.src = l1; - ea.dst = l2; - sendRequest(&ea); - return true; -} - -/** - * Assembles or disassembles a combination artifact. - * @param hero Hero holding the artifact(s). - * @param artifactSlot The worn slot ID of the combination- or constituent artifact. - * @param assemble True for assembly operation, false for disassembly. - * @param assembleTo If assemble is true, this represents the artifact ID of the combination - * artifact to assemble to. Otherwise it's not used. - */ -void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) -{ - AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); - sendRequest(&aa); -} - -void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) -{ - BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack); - sendRequest(&bma); -} - -void CCallback::eraseArtifactByClient(const ArtifactLocation & al) -{ - EraseArtifactByClient ea(al); - sendRequest(&ea); -} - -bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) -{ - if(town->tempOwner!=player) - return false; - - if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED) - return false; - - BuildStructure pack(town->id,buildingID); - sendRequest(&pack); - return true; -} - -void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) -{ - assert(action.actionType == EActionType::HERO_SPELL); - MakeAction mca(action); - mca.battleID = battleID; - sendRequest(&mca); -} - -int CBattleCallback::sendRequest(const CPackForServer * request) -{ - int requestID = cl->sendRequest(request, *getPlayerID()); - if(waitTillRealize) - { - logGlobal->trace("We'll wait till request %d is answered.\n", requestID); - auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting); - CClient::waitingRequest.waitWhileContains(requestID); - } - - boost::this_thread::interruption_point(); - return requestID; -} - -void CCallback::swapGarrisonHero( const CGTownInstance *town ) -{ - if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) - { - GarrisonHeroSwap pack(town->id); - sendRequest(&pack); - } -} - -void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) -{ - if(hero->tempOwner != *player) return; - - BuyArtifact pack(hero->id,aid); - sendRequest(&pack); -} - -void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) -{ - trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); -} - -void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) -{ - TradeOnMarketplace pack; - pack.marketId = dynamic_cast(market)->id; - pack.heroId = hero ? hero->id : ObjectInstanceID(); - pack.mode = mode; - pack.r1 = id1; - pack.r2 = id2; - pack.val = val1; - sendRequest(&pack); -} - -void CCallback::setFormation(const CGHeroInstance * hero, bool tight) -{ - SetFormation pack(hero->id,tight); - sendRequest(&pack); -} - -void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) -{ - assert(townOrTavern); - assert(hero); - - HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); - pack.player = *player; - sendRequest(&pack); -} - -void CCallback::save( const std::string &fname ) -{ - cl->save(fname); -} - -void CCallback::gamePause(bool pause) -{ - if(pause) - { - GamePause pack; - pack.player = *player; - sendRequest(&pack); - } - else - { - sendQueryReply(0, QueryID::CLIENT); - } -} - -void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) -{ - ASSERT_IF_CALLED_WITH_PLAYER - PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); - if(player) - pm.player = *player; - sendRequest(&pm); -} - -void CCallback::buildBoat( const IShipyard *obj ) -{ - BuildBoat bb; - bb.objid = dynamic_cast(obj)->id; - sendRequest(&bb); -} - -CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C) - : CBattleCallback(Player, C) -{ - gs = GS; - - waitTillRealize = false; - unlockGsWhenWaiting = false; -} - -CCallback::~CCallback() = default; - -bool CCallback::canMoveBetween(const int3 &a, const int3 &b) -{ - //bidirectional - return gs->map->canMoveBetween(a, b); -} - -std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * h) -{ - return cl->getPathsInfo(h); -} - -std::optional CCallback::getPlayerID() const -{ - return CBattleCallback::getPlayerID(); -} - -int3 CCallback::getGuardingCreaturePosition(int3 tile) -{ - if (!gs->map->isInTheMap(tile)) - return int3(-1,-1,-1); - - return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y]; -} - -void CCallback::dig( const CGObjectInstance *hero ) -{ - DigWithHero dwh; - dwh.id = hero->id; - sendRequest(&dwh); -} - -void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) -{ - CastAdvSpell cas; - cas.hid = hero->id; - cas.sid = spellID; - cas.pos = pos; - sendRequest(&cas); -} - -int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) -{ - if(s1->getCreature(p1) == s2->getCreature(p2)) - return mergeStacks(s1, s2, p1, p2); - else - return swapCreatures(s1, s2, p1, p2); -} - -void CCallback::registerBattleInterface(std::shared_ptr battleEvents) -{ - cl->additionalBattleInts[*player].push_back(battleEvents); -} - -void CCallback::unregisterBattleInterface(std::shared_ptr battleEvents) -{ - cl->additionalBattleInts[*player] -= battleEvents; -} - -CBattleCallback::CBattleCallback(std::optional player, CClient * C): - cl(C), - player(player) -{ -} - -void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) -{ - assert(!cl->gs->getBattle(battleID)->tacticDistance); - MakeAction ma; - ma.ba = action; - ma.battleID = battleID; - sendRequest(&ma); -} - -void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action ) -{ - assert(cl->gs->getBattle(battleID)->tacticDistance); - MakeAction ma; - ma.ba = action; - ma.battleID = battleID; - sendRequest(&ma); -} - -std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) -{ - return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState); -} - -std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) -{ - return activeBattles.at(battleID); -} - -std::optional CBattleCallback::getPlayerID() const -{ - return player; -} - -void CBattleCallback::onBattleStarted(const IBattleInfo * info) -{ - activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); -} - -void CBattleCallback::onBattleEnded(const BattleID & battleID) -{ - activeBattles.erase(battleID); -} +/* + * CCallback.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 "CCallback.h" + +#include "lib/CCreatureHandler.h" +#include "lib/gameState/CGameState.h" +#include "client/CPlayerInterface.h" +#include "client/Client.h" +#include "lib/mapping/CMap.h" +#include "lib/CBuildingHandler.h" +#include "lib/CGeneralTextHandler.h" +#include "lib/CHeroHandler.h" +#include "lib/NetPacks.h" +#include "lib/CArtHandler.h" +#include "lib/GameConstants.h" +#include "lib/CPlayerState.h" +#include "lib/UnlockGuard.h" +#include "lib/battle/BattleInfo.h" + +bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) +{ + CastleTeleportHero pack(who->id, where->id, 1); + sendRequest(&pack); + return true; +} + +bool CCallback::moveHero(const CGHeroInstance *h, int3 dst, bool transit) +{ + MoveHero pack(dst,h->id,transit); + sendRequest(&pack); + return true; +} + +int CCallback::selectionMade(int selection, QueryID queryID) +{ + return sendQueryReply(selection, queryID); +} + +int CCallback::sendQueryReply(std::optional reply, QueryID queryID) +{ + ASSERT_IF_CALLED_WITH_PLAYER + if(queryID == QueryID(-1)) + { + logGlobal->error("Cannot answer the query -1!"); + return -1; + } + + QueryReply pack(queryID, reply); + pack.player = *player; + return sendRequest(&pack); +} + +void CCallback::recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level) +{ + // TODO exception for neutral dwellings shouldn't be hardcoded + if(player != obj->tempOwner && obj->ID != Obj::WAR_MACHINE_FACTORY && obj->ID != Obj::REFUGEE_CAMP) + return; + + RecruitCreatures pack(obj->id, dst->id, ID, amount, level); + sendRequest(&pack); +} + +bool CCallback::dismissCreature(const CArmedInstance *obj, SlotID stackPos) +{ + if((player && obj->tempOwner != player) || (obj->stacksCount()<2 && obj->needsLastStack())) + return false; + + DisbandCreature pack(stackPos,obj->id); + sendRequest(&pack); + return true; +} + +bool CCallback::upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID) +{ + UpgradeCreature pack(stackPos,obj->id,newID); + sendRequest(&pack); + return false; +} + +void CCallback::endTurn() +{ + logGlobal->trace("Player %d ended his turn.", player->getNum()); + EndTurn pack; + sendRequest(&pack); +} +int CCallback::swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) +{ + ArrangeStacks pack(1,p1,p2,s1->id,s2->id,0); + sendRequest(&pack); + return 0; +} + +int CCallback::mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) +{ + ArrangeStacks pack(2,p1,p2,s1->id,s2->id,0); + sendRequest(&pack); + return 0; +} + +int CCallback::splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) +{ + ArrangeStacks pack(3,p1,p2,s1->id,s2->id,val); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) +{ + BulkMoveArmy pack(srcArmy, destArmy, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany) +{ + BulkSplitStack pack(armyId, srcSlot, howMany); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkSmartSplitStack pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + +int CCallback::bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) +{ + BulkMergeStacks pack(armyId, srcSlot); + sendRequest(&pack); + return 0; +} + +bool CCallback::dismissHero(const CGHeroInstance *hero) +{ + if(player!=hero->tempOwner) return false; + + DismissHero pack(hero->id); + sendRequest(&pack); + return true; +} + +bool CCallback::swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) +{ + ExchangeArtifacts ea; + ea.src = l1; + ea.dst = l2; + sendRequest(&ea); + return true; +} + +/** + * Assembles or disassembles a combination artifact. + * @param hero Hero holding the artifact(s). + * @param artifactSlot The worn slot ID of the combination- or constituent artifact. + * @param assemble True for assembly operation, false for disassembly. + * @param assembleTo If assemble is true, this represents the artifact ID of the combination + * artifact to assemble to. Otherwise it's not used. + */ +void CCallback::assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) +{ + AssembleArtifacts aa(hero->id, artifactSlot, assemble, assembleTo); + sendRequest(&aa); +} + +void CCallback::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) +{ + BulkExchangeArtifacts bma(srcHero, dstHero, swap, equipped, backpack); + sendRequest(&bma); +} + +void CCallback::eraseArtifactByClient(const ArtifactLocation & al) +{ + EraseArtifactByClient ea(al); + sendRequest(&ea); +} + +bool CCallback::buildBuilding(const CGTownInstance *town, BuildingID buildingID) +{ + if(town->tempOwner!=player) + return false; + + if(canBuildStructure(town, buildingID) != EBuildingState::ALLOWED) + return false; + + BuildStructure pack(town->id,buildingID); + sendRequest(&pack); + return true; +} + +void CBattleCallback::battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) +{ + assert(action.actionType == EActionType::HERO_SPELL); + MakeAction mca(action); + mca.battleID = battleID; + sendRequest(&mca); +} + +int CBattleCallback::sendRequest(const CPackForServer * request) +{ + int requestID = cl->sendRequest(request, *getPlayerID()); + if(waitTillRealize) + { + logGlobal->trace("We'll wait till request %d is answered.\n", requestID); + auto gsUnlocker = vstd::makeUnlockSharedGuardIf(CGameState::mutex, unlockGsWhenWaiting); + CClient::waitingRequest.waitWhileContains(requestID); + } + + boost::this_thread::interruption_point(); + return requestID; +} + +void CCallback::swapGarrisonHero( const CGTownInstance *town ) +{ + if(town->tempOwner == *player || (town->garrisonHero && town->garrisonHero->tempOwner == *player )) + { + GarrisonHeroSwap pack(town->id); + sendRequest(&pack); + } +} + +void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) +{ + if(hero->tempOwner != *player) return; + + BuyArtifact pack(hero->id,aid); + sendRequest(&pack); +} + +void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) +{ + trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); +} + +void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) +{ + TradeOnMarketplace pack; + pack.marketId = dynamic_cast(market)->id; + pack.heroId = hero ? hero->id : ObjectInstanceID(); + pack.mode = mode; + pack.r1 = id1; + pack.r2 = id2; + pack.val = val1; + sendRequest(&pack); +} + +void CCallback::setFormation(const CGHeroInstance * hero, bool tight) +{ + SetFormation pack(hero->id,tight); + sendRequest(&pack); +} + +void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) +{ + assert(townOrTavern); + assert(hero); + + HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); + pack.player = *player; + sendRequest(&pack); +} + +void CCallback::save( const std::string &fname ) +{ + cl->save(fname); +} + +void CCallback::gamePause(bool pause) +{ + if(pause) + { + GamePause pack; + pack.player = *player; + sendRequest(&pack); + } + else + { + sendQueryReply(0, QueryID::CLIENT); + } +} + +void CCallback::sendMessage(const std::string &mess, const CGObjectInstance * currentObject) +{ + ASSERT_IF_CALLED_WITH_PLAYER + PlayerMessage pm(mess, currentObject? currentObject->id : ObjectInstanceID(-1)); + if(player) + pm.player = *player; + sendRequest(&pm); +} + +void CCallback::buildBoat( const IShipyard *obj ) +{ + BuildBoat bb; + bb.objid = dynamic_cast(obj)->id; + sendRequest(&bb); +} + +CCallback::CCallback(CGameState * GS, std::optional Player, CClient * C) + : CBattleCallback(Player, C) +{ + gs = GS; + + waitTillRealize = false; + unlockGsWhenWaiting = false; +} + +CCallback::~CCallback() = default; + +bool CCallback::canMoveBetween(const int3 &a, const int3 &b) +{ + //bidirectional + return gs->map->canMoveBetween(a, b); +} + +std::shared_ptr CCallback::getPathsInfo(const CGHeroInstance * h) +{ + return cl->getPathsInfo(h); +} + +std::optional CCallback::getPlayerID() const +{ + return CBattleCallback::getPlayerID(); +} + +int3 CCallback::getGuardingCreaturePosition(int3 tile) +{ + if (!gs->map->isInTheMap(tile)) + return int3(-1,-1,-1); + + return gs->map->guardingCreaturePositions[tile.z][tile.x][tile.y]; +} + +void CCallback::dig( const CGObjectInstance *hero ) +{ + DigWithHero dwh; + dwh.id = hero->id; + sendRequest(&dwh); +} + +void CCallback::castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos) +{ + CastAdvSpell cas; + cas.hid = hero->id; + cas.sid = spellID; + cas.pos = pos; + sendRequest(&cas); +} + +int CCallback::mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) +{ + if(s1->getCreature(p1) == s2->getCreature(p2)) + return mergeStacks(s1, s2, p1, p2); + else + return swapCreatures(s1, s2, p1, p2); +} + +void CCallback::registerBattleInterface(std::shared_ptr battleEvents) +{ + cl->additionalBattleInts[*player].push_back(battleEvents); +} + +void CCallback::unregisterBattleInterface(std::shared_ptr battleEvents) +{ + cl->additionalBattleInts[*player] -= battleEvents; +} + +CBattleCallback::CBattleCallback(std::optional player, CClient * C): + cl(C), + player(player) +{ +} + +void CBattleCallback::battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) +{ + assert(!cl->gs->getBattle(battleID)->tacticDistance); + MakeAction ma; + ma.ba = action; + ma.battleID = battleID; + sendRequest(&ma); +} + +void CBattleCallback::battleMakeTacticAction(const BattleID & battleID, const BattleAction & action ) +{ + assert(cl->gs->getBattle(battleID)->tacticDistance); + MakeAction ma; + ma.ba = action; + ma.battleID = battleID; + sendRequest(&ma); +} + +std::optional CBattleCallback::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return cl->playerint[getPlayerID().value()]->makeSurrenderRetreatDecision(battleID, battleState); +} + +std::shared_ptr CBattleCallback::getBattle(const BattleID & battleID) +{ + return activeBattles.at(battleID); +} + +std::optional CBattleCallback::getPlayerID() const +{ + return player; +} + +void CBattleCallback::onBattleStarted(const IBattleInfo * info) +{ + activeBattles[info->getBattleID()] = std::make_shared(info, *getPlayerID()); +} + +void CBattleCallback::onBattleEnded(const BattleID & battleID) +{ + activeBattles.erase(battleID); +} diff --git a/CCallback.h b/CCallback.h index d31f5415e..d7af2e38c 100644 --- a/CCallback.h +++ b/CCallback.h @@ -1,197 +1,197 @@ -/* - * CCallback.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/CGameInfoCallback.h" -#include "lib/battle/CPlayerBattleCallback.h" -#include "lib/int3.h" // for int3 - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CGameState; -struct CPath; -class CGObjectInstance; -class CArmedInstance; -class BattleAction; -class CGTownInstance; -class IShipyard; -struct CGPathNode; -struct CGPath; -struct CPathsInfo; -class PathfinderConfig; -struct CPack; -struct CPackForServer; -class IBattleEventsReceiver; -class IGameEventsReceiver; -struct ArtifactLocation; -class BattleStateInfoForRetreat; -class IMarket; - -VCMI_LIB_NAMESPACE_END - -// in static AI build this file gets included into libvcmi -#ifdef STATIC_AI -VCMI_LIB_USING_NAMESPACE -#endif - -class CClient; -struct lua_State; - -class IBattleCallback -{ -public: - virtual ~IBattleCallback() = default; - - bool waitTillRealize = false; //if true, request functions will return after they are realized by server - bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! - //battle - virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0; - virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0; - virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0; - virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; - - virtual std::shared_ptr getBattle(const BattleID & battleID) = 0; - virtual std::optional getPlayerID() const = 0; -}; - -class IGameActionCallback -{ -public: - //hero - virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile) - virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly - virtual void dig(const CGObjectInstance *hero)=0; - virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell - - //town - virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0; - virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; - virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; - virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made - virtual void swapGarrisonHero(const CGTownInstance *town)=0; - - virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce - virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; - - virtual int selectionMade(int selection, QueryID queryID) =0; - virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; - virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! - virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) - virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second - virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack - //virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes - virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0; - virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; - virtual void eraseArtifactByClient(const ArtifactLocation & al)=0; - virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; - virtual void endTurn()=0; - virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) - virtual void setFormation(const CGHeroInstance * hero, bool tight)=0; - - virtual void save(const std::string &fname) = 0; - virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; - virtual void gamePause(bool pause) = 0; - virtual void buildBoat(const IShipyard *obj) = 0; - - // To implement high-level army management bulk actions - virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0; - virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0; - virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0; - virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0; - - - // Moves all artifacts from one hero to another - virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0; -}; - -class CBattleCallback : public IBattleCallback -{ - std::map> activeBattles; - - std::optional player; - -protected: - int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) - CClient *cl; - -public: - CBattleCallback(std::optional player, CClient * C); - void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack - void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override; - void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions - std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - - std::shared_ptr getBattle(const BattleID & battleID) override; - std::optional getPlayerID() const override; - - void onBattleStarted(const IBattleInfo * info); - void onBattleEnded(const BattleID & battleID); - - friend class CCallback; - friend class CClient; -}; - -class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback -{ -public: - CCallback(CGameState * GS, std::optional Player, CClient * C); - virtual ~CCallback(); - - //client-specific functionalities (pathfinding) - virtual bool canMoveBetween(const int3 &a, const int3 &b); - virtual int3 getGuardingCreaturePosition(int3 tile); - virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); - - std::optional getPlayerID() const override; - - //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. - void registerBattleInterface(std::shared_ptr battleEvents); - void unregisterBattleInterface(std::shared_ptr battleEvents); - -//commands - bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) - bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); - int selectionMade(int selection, QueryID queryID) override; - int sendQueryReply(std::optional reply, QueryID queryID) override; - int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; - int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second - int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second - int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; - int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override; - int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override; - int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override; - int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; - bool dismissHero(const CGHeroInstance * hero) override; - bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; - void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; - void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override; - void eraseArtifactByClient(const ArtifactLocation & al) override; - bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; - void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; - bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override; - bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; - void endTurn() override; - void swapGarrisonHero(const CGTownInstance *town) override; - void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; - void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; - void setFormation(const CGHeroInstance * hero, bool tight) override; - void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; - void save(const std::string &fname) override; - void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; - void gamePause(bool pause) override; - void buildBoat(const IShipyard *obj) override; - void dig(const CGObjectInstance *hero) override; - void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; - -//friends - friend class CClient; -}; +/* + * CCallback.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/CGameInfoCallback.h" +#include "lib/battle/CPlayerBattleCallback.h" +#include "lib/int3.h" // for int3 + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CGameState; +struct CPath; +class CGObjectInstance; +class CArmedInstance; +class BattleAction; +class CGTownInstance; +class IShipyard; +struct CGPathNode; +struct CGPath; +struct CPathsInfo; +class PathfinderConfig; +struct CPack; +struct CPackForServer; +class IBattleEventsReceiver; +class IGameEventsReceiver; +struct ArtifactLocation; +class BattleStateInfoForRetreat; +class IMarket; + +VCMI_LIB_NAMESPACE_END + +// in static AI build this file gets included into libvcmi +#ifdef STATIC_AI +VCMI_LIB_USING_NAMESPACE +#endif + +class CClient; +struct lua_State; + +class IBattleCallback +{ +public: + virtual ~IBattleCallback() = default; + + bool waitTillRealize = false; //if true, request functions will return after they are realized by server + bool unlockGsWhenWaiting = false;//if true after sending each request, gs mutex will be unlocked so the changes can be applied; NOTICE caller must have gs mx locked prior to any call to actiob callback! + //battle + virtual void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) = 0; + virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; + + virtual std::shared_ptr getBattle(const BattleID & battleID) = 0; + virtual std::optional getPlayerID() const = 0; +}; + +class IGameActionCallback +{ +public: + //hero + virtual bool moveHero(const CGHeroInstance *h, int3 dst, bool transit) =0; //dst must be free, neighbouring tile (this function can move hero only by one tile) + virtual bool dismissHero(const CGHeroInstance * hero)=0; //dismisses given hero; true - successfuly, false - not successfuly + virtual void dig(const CGObjectInstance *hero)=0; + virtual void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1))=0; //cast adventure map spell + + //town + virtual void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero)=0; + virtual bool buildBuilding(const CGTownInstance *town, BuildingID buildingID)=0; + virtual void recruitCreatures(const CGDwelling *obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1)=0; + virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made + virtual void swapGarrisonHero(const CGTownInstance *town)=0; + + virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce + virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; + + virtual int selectionMade(int selection, QueryID queryID) =0; + virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; + virtual int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//swaps creatures between two possibly different garrisons // TODO: AI-unsafe code - fix it! + virtual int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2)=0;//joins first stack to the second (creatures must be same type) + virtual int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) =0; //first goes to the second + virtual int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val)=0;//split creatures from the first stack + //virtual bool swapArtifacts(const CGHeroInstance * hero1, ui16 pos1, const CGHeroInstance * hero2, ui16 pos2)=0; //swaps artifacts between two given heroes + virtual bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2)=0; + virtual void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo)=0; + virtual void eraseArtifactByClient(const ArtifactLocation & al)=0; + virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; + virtual void endTurn()=0; + virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) + virtual void setFormation(const CGHeroInstance * hero, bool tight)=0; + + virtual void save(const std::string &fname) = 0; + virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; + virtual void gamePause(bool pause) = 0; + virtual void buildBoat(const IShipyard *obj) = 0; + + // To implement high-level army management bulk actions + virtual int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) = 0; + virtual int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) = 0; + virtual int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) = 0; + virtual int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) = 0; + + + // Moves all artifacts from one hero to another + virtual void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack) = 0; +}; + +class CBattleCallback : public IBattleCallback +{ + std::map> activeBattles; + + std::optional player; + +protected: + int sendRequest(const CPackForServer * request); //returns requestID (that'll be matched to requestID in PackageApplied) + CClient *cl; + +public: + CBattleCallback(std::optional player, CClient * C); + void battleMakeSpellAction(const BattleID & battleID, const BattleAction & action) override;//for casting spells by hero - DO NOT use it for moving active stack + void battleMakeUnitAction(const BattleID & battleID, const BattleAction & action) override; + void battleMakeTacticAction(const BattleID & battleID, const BattleAction & action) override; // performs tactic phase actions + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + + std::shared_ptr getBattle(const BattleID & battleID) override; + std::optional getPlayerID() const override; + + void onBattleStarted(const IBattleInfo * info); + void onBattleEnded(const BattleID & battleID); + + friend class CCallback; + friend class CClient; +}; + +class CCallback : public CPlayerSpecificInfoCallback, public CBattleCallback, public IGameActionCallback +{ +public: + CCallback(CGameState * GS, std::optional Player, CClient * C); + virtual ~CCallback(); + + //client-specific functionalities (pathfinding) + virtual bool canMoveBetween(const int3 &a, const int3 &b); + virtual int3 getGuardingCreaturePosition(int3 tile); + virtual std::shared_ptr getPathsInfo(const CGHeroInstance * h); + + std::optional getPlayerID() const override; + + //Set of metrhods that allows adding more interfaces for this player that'll receive game event call-ins. + void registerBattleInterface(std::shared_ptr battleEvents); + void unregisterBattleInterface(std::shared_ptr battleEvents); + +//commands + bool moveHero(const CGHeroInstance *h, int3 dst, bool transit = false) override; //dst must be free, neighbouring tile (this function can move hero only by one tile) + bool teleportHero(const CGHeroInstance *who, const CGTownInstance *where); + int selectionMade(int selection, QueryID queryID) override; + int sendQueryReply(std::optional reply, QueryID queryID) override; + int swapCreatures(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; + int mergeOrSwapStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second + int mergeStacks(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2) override; //first goes to the second + int splitStack(const CArmedInstance *s1, const CArmedInstance *s2, SlotID p1, SlotID p2, int val) override; + int bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot) override; + int bulkSplitStack(ObjectInstanceID armyId, SlotID srcSlot, int howMany = 1) override; + int bulkSmartSplitStack(ObjectInstanceID armyId, SlotID srcSlot) override; + int bulkMergeStacks(ObjectInstanceID armyId, SlotID srcSlot) override; + bool dismissHero(const CGHeroInstance * hero) override; + bool swapArtifacts(const ArtifactLocation &l1, const ArtifactLocation &l2) override; + void assembleArtifacts(const CGHeroInstance * hero, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo) override; + void bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped = true, bool backpack = true) override; + void eraseArtifactByClient(const ArtifactLocation & al) override; + bool buildBuilding(const CGTownInstance *town, BuildingID buildingID) override; + void recruitCreatures(const CGDwelling * obj, const CArmedInstance * dst, CreatureID ID, ui32 amount, si32 level=-1) override; + bool dismissCreature(const CArmedInstance *obj, SlotID stackPos) override; + bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE) override; + void endTurn() override; + void swapGarrisonHero(const CGTownInstance *town) override; + void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; + void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; + void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; + void setFormation(const CGHeroInstance * hero, bool tight) override; + void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; + void save(const std::string &fname) override; + void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; + void gamePause(bool pause) override; + void buildBoat(const IShipyard *obj) override; + void dig(const CGObjectInstance *hero) override; + void castSpell(const CGHeroInstance *hero, SpellID spellID, const int3 &pos = int3(-1, -1, -1)) override; + +//friends + friend class CClient; +}; diff --git a/ChangeLog.md b/ChangeLog.md index 873ce4147..82f214f5e 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,2160 +1,2160 @@ -# 1.3.1 -> 1.3.2 - -### GENERAL -* VCMI now uses new application icon -* Added initial version of Czech translation -* Game will now use tile hero is moving from for movement cost calculations, in line with H3 -* Added option to open hero backpack window in hero screen -* Added detection of misclicks for touch inputs to make hitting small UI elements easier -* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities -* It is no longer possible to stop movement while moving over water with Water Walk -* Game will now automatically update hero path if it was blocked by another hero -* Added "vcmiartifacts angelWings" form to "give artifacts" cheat - -### STABILITY -* Fixed freeze in Launcher on repository checkout and on mod install -* Fixed crash on loading VCMI map with placed Abandoned Mine -* Fixed crash on loading VCMI map with neutral towns -* Fixed crash on attempting to visit unknown object, such as Market of Time -* Fixed crash on attempting to teleport unit that is immune to a spell -* Fixed crash on switching fullscreen mode during AI turn - -### CAMPAIGNS -* Fixed reorderging of hero primary skills after moving to next scenario in campaigns - -### BATTLES -* Conquering a town will now correctly award additional 500 experience points -* Quick combat is now enabled by default -* Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses -* Added option to toggle spell usage by AI in quick combat -* Fixed updating of spell point of enemy hero in game interface after spell cast -* Fixed wrong creature spellcasting shortcut (now set to "F") -* It is now possible to perform melee attack by creatures with spells, especially area spells -* Right-click will now properly end spellcast mode -* Fixed cursor preview when casting spell using touchscreen -* Long tap during spell casting will now properly abort the spell - -### INTERFACE -* Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows -* Context popup for adventure map monsters will now show creature icon -* Game will now show correct victory message for gather troops victory condition -* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window -* Fixed incorrect color of resource bar in hotseat mode -* Fixed broken toggle map level button in world view mode -* Fixed corrupted interface after opening puzzle window from world view mode -* Fixed blocked interface after attempt to start invalid map -* Add yellow border to selected commander grandmaster ability -* Always use bonus description for commander abilities instead of not provided wog-specific translation -* Fix scrolling when commander has large number of grandmaster abilities -* Fixed corrupted message on another player defeat -* Fixed unavailable Quest Log button on maps with quests -* Fixed incorrect values on a difficulty selector in save load screen -* Removed invalid error message on attempting to move non-existing unit in exchange window - -### RANDOM MAP GENERATOR -* Fixed bug leading to unreachable resources around mines - -### MAP EDITOR -* Fixed crash on maps containing abandoned mines -* Fixed crash on maps containing neutral objects -* Fixed problem with random map initialized in map editor -* Fixed problem with initialization of random dwellings - -# 1.3.0 -> 1.3.1 - -### GENERAL: -* Fixed framerate drops on hero movement with active hota mod -* Fade-out animations will now be skipped when instant hero movement speed is used -* Restarting loaded campaing scenario will now correctly reapply starting bonus -* Reverted FPS limit on mobile systems back to 60 fps -* Fixed loading of translations for maps and campaigns -* Fixed loading of preconfigured starting army for heroes with preconfigured spells -* Background battlefield obstacles will now appear below creatures -* it is now possible to load save game located inside mod -* Added option to configure reserved screen area in Launcher on iOS -* Fixed border scrolling when game window is maximized - -### AI PLAYER: -* BattleAI: Improved performance of AI spell selection -* NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero -* NKAI: Fixed town threat calculation -* NKAI: Fixed recruitment of new heroes -* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location -* VCAI: Fixed spellcasting by Archangels - -### RANDOM MAP GENERATOR: -* Fixed placement of roads inside rock in underground -* Fixed placement of shifted creature animations from HotA -* Fixed placement of treasures at the boundary of wide connections -* Added more potential locations for quest artifacts in zone - -### STABILITY: -* When starting client without H3 data game will now show message instead of silently crashing -* When starting invalid map in campaign, game will now show message instead of silently crashing -* Blocked loading of saves made with different set of mods to prevent crashes -* Fixed crash on starting game with outdated mods -* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice -* Fixed crash on leveling up after winning battle as defender -* Fixed possible crash on end of battle opening sound -* Fixed crash on accepting battle result after winning battle as defender -* Fixed possible crash on casting spell in battle by AI -* Fixed multiple possible crashes on managing mods on Android -* Fixed multiple possible crashes on importing data on Android -* Fixed crash on refusing rewards from town building -* Fixed possible crash on threat evaluation by NKAI -* Fixed crash on using haptic feedback on some Android systems -* Fixed crash on right-clicking flags area in RMG setup mode -* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations -* Fixed possible crash on displaying animated main menu -* Fixed crash on recruiting hero in town located on the border of map - -# 1.2.1 -> 1.3.0 - -### GENERAL: -* Implemented automatic interface scaling to any resolution supported by monitor -* Implemented UI scaling option to scale game interface -* Game resolution and UI scaling can now be changed without game restart -* Fixed multiple issues with borderless fullscreen mode -* On mobile systems game will now always run at native resolution with configurable UI scaling -* Implemented support for Horn of the Abyss map format -* Implemented option to replay results of quick combat -* Added translations to French and Chinese -* All in-game cheats are now case-insensitive -* Added high-definition icon for Windows -* Fix crash on connecting to server on FreeBSD and Flatpak builds -* Save games now consist of a single file -* Added H3:SOD cheat codes as alternative to vcmi cheats -* Fixed several possible crashes caused by autocombat activation -* Fixed artifact lock icon in localized versions of the game -* Fixed possible crash on changing hardware cursor - -### TOUCHSCREEN SUPPORT: -* VCMI will now properly recognizes touch screen input -* Implemented long tap gesture that shows popup window. Tap once more to close popup -* Long tap gesture duration can now be configured in settings -* Implemented radial menu for army management, activated via swiping creature icon -* Implemented swipe gesture for scrolling through lists -* All windows that have sliders in UI can now be scrolled using swipe gesture -* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from -* Implemented pinch gesture for zooming adventure map -* Implemented haptic feedback (vibration) for long press gesture - -### LAUNCHER: -* Launcher will now attempt to automatically detect language of OS on first launch -* Added "About" tab with information about project and environment -* Added separate options for Allied AI and Enemy AI for adventure map -* Patially fixed displaying of download progress for mods -* Fixed potential crash on opening mod information for mods with a changelog -* Added option to configure number of autosaves - -### MAP EDITOR: -* Fixed crash on cutting random town -* Added option to export entire map as an image -* Added validation for placing multiple heroes into starting town -* It is now possible to have single player on a map -* It is now possible to configure teams in editor - -### AI PLAYER: -* Fixed potential crash on accessing market (VCAI) -* Fixed potentially infinite turns (VCAI) -* Reworked object prioritizing -* Improved town defense against enemy heroes -* Improved town building (mage guild and horde) -* Various behavior fixes - -### GAME MECHANICS -* Hero retreating after end of 7th turn will now correctly appear in tavern -* Implemented hero backpack limit (disabled by default) -* Fixed Admiral's Hat movement points calculation -* It is now possible to access Shipwrecks from coast -* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings -* It is no longer possible to abort movement while hero is flying over water -* Fixed digging for Grail -* Implemented "Survive beyond a time limit" victory condition -* Implemented "Defeat all monsters" victory condition -* 100% damage resistance or damage reduction will make unit immune to a spell -* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic -* Fixed duration of bonuses from visitable object such as Idol of Fortune -* Rescued hero from prison will now correctly reveal map around him -* Lighthouses will no longer give movement bonus on land - -### CAMPAIGNS: -* Fixed transfer of artifacts into next scenario -* Fixed crash on advancing to next scenario with heroes from mods -* Fixed handling of "Start with building" campaign bonus -* Fixed incorrect starting level of heroes in campaigns -* Game will now play correct music track on scenario selection window -* Dracon woll now correctly start without spellbook in Dragon Slayer campaign -* Fixed frequent crash on moving to next scenario during campaign -* Fixed inability to dismiss heroes on maps with "capture town" victory condition - -### RANDOM MAP GENERATOR: -* Improved zone placement, shape and connections -* Improved zone passability for better gameplay -* Improved treasure distribution and treasure values to match SoD closely -* Navigation and water-specific spells are now banned on maps without water -* RMG will now respect road settings set in menu -* Tweaked many original templates so they allow new terrains and factions -* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties -* Added "road" property to connections -* Added monster strength "none" -* Support for "wide" connections -* Support for new "fictive" and "repulsive" connections -* RMG will now run faster, utilizing many CPU cores -* Removed random seed number from random map description - -### INTERFACE: -* Adventure map is now scalable and can be used with any resolution without mods -* Adventure map interface is now correctly blocked during enemy turn -* Visiting creature banks will now show amount of guards in bank -* It is now possible to arrange army using status window -* It is now possible to zoom in or out using mouse wheel or pinch gesture -* It is now possible to reset zoom via Backspace hotkey -* Receiving a message in chat will now play sound -* Map grid will now correctly display on map start -* Fixed multiple issues with incorrect updates of save/load game screen -* Fixed missing fortifications level icon in town tooltip -* Fixed positioning of resource label in Blacksmith window -* Status bar on inactive windows will no longer show any tooltip from active window -* Fixed highlighting of possible artifact placements when exchanging with allied hero -* Implemented sound of flying movement (for Fly spell or Angel Wings) -* Last symbol of entered cheat/chat message will no longer trigger hotkey -* Right-clicking map name in scenario selection will now show file name -* Right-clicking save game in save/load screen will now show file name and creation date -* Right-clicking in town fort window will now show creature information popup -* Implemented pasting from clipboard (Ctrl+V) for text input - -### BATTLES: -* Implemented Tower moat (Land Mines) -* Implemented defence reduction for units in moat -* Added option to always show hero status window -* Battle opening sound can now be skipped with mouse click -* Fixed movement through moat of double-hexed units -* Fixed removal of Land Mines and Fire Walls -* Obstacles will now corectly show up either below or above unit -* It is now possible to teleport a unit through destroyed walls -* Added distinct overlay image for showing movement range of highlighted unit -* Added overlay for displaying shooting range penalties of units - -### MODDING: -* Implemented initial version of VCMI campaign format -* Implemented spell cast as possible reward for configurable object -* Implemented support for configurable buildings in towns -* Implemented support for placing prison, tavern and heroes on water -* Implemented support for new boat types -* It is now possible for boats to use other movement layers, such as "air" -* It is now possible to use growing artifacts on artifacts that can be used by hero -* It is now possible to configure town moat -* Palette-cycling animation of terrains and rivers can now be configured in json -* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless') -* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades -* It is now possible to configure spells for Shrines -* It is now possible to configure upgrade costs per level for Hill Forts -* It is now possible to configure boat type for Shipyards on adventure map and in town -* Implemented support for HotA-style adventure map images for monsters, with offset -* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype). -* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL -* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance -* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses -* Configurable objects can now be translated -* Fixed loading of custom battlefield identifiers for map objects - -# 1.2.0 -> 1.2.1 - -### GENERAL: -* Implemented spell range overlay for Dimension Door and Scuttle Boat -* Fixed movement cost penalty from terrain -* Fixed empty Black Market on game start -* Fixed bad morale happening after waiting -* Fixed good morale happening after defeating last enemy unit -* Fixed death animation of Efreeti killed by petrification attack -* Fixed crash on leaving to main menu from battle in hotseat mode -* Fixed music playback on switching between towns -* Special months (double growth and plague) will now appear correctly -* Adventure map spells are no longer visible on units in battle -* Attempt to cast spell with no valid targets in hotseat will show appropriate error message -* RMG settings will now show all existing in game templates and not just those suitable for current settings -* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked -* Fixed centering of scenario information window -* Fixed crash on empty save game list after filtering -* Fixed blocked progress in Launcher on language detection failure -* Launcher will now correctly handle selection of Ddata directory in H3 install -* Map editor will now correctly save message property for events and pandoras -* Fixed incorrect saving of heroes portraits in editor - -# 1.1.1 -> 1.2.0 - -### GENERAL: -* Adventure map rendering was entirely rewritten with better, more functional code -* Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches -* Client mechanics are now framerate-independent, rather than speeding up with higher framerate -* Implemented hardware cursor support -* Heroes III language can now be detected automatically -* Increased targeted framerate from 48 to 60 -* Increased performance of UI updates -* Fixed bonus values of heroes who specialize in secondary skills -* Fixed bonus values of heroes who specialize in creatures -* Fixed damage increase from Adela's Bless specialty -* Fixed missing obstacles in battles on subterranean terrain -* Video files now play at correct speed -* Fixed crash on switching to second mission in campaigns -* New cheat code: vcmiazure - give 5000 azure dragons in every empty slot -* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot -* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp -* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000 -* Fixed oversided message window from Scholar skill that had confirmation button outside game window -* Fixed loading of prebuilt creature hordes from h3m maps -* Fixed volume of ambient sounds when changing game sounds volume -* Fixed might&magic affinities of Dungeon heroes -* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins -* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally -* Default game difficulty is now set to "normal" instead of "easy" -* Fixed crash on missing music files - -### MAP EDITOR: -* Added translations to German, Polish, Russian, Spanish, Ukrainian -* Implemented cut/copy/paste operations -* Implemented lasso brush for terrain editing -* Toolbar actions now have names -* Added basic victory and lose conditions - -### LAUNCHER: -* Added initial Welcome/Setup screen for new players -* Added option to install translation mod if such mod exists and player's H3 version has different language -* Icons now have higher resolution, to prevent upscaling artifacts -* Added translations to German, Polish, Russian, Spanish, Ukrainian -* Mods tab layout has been adjusted based on feedback from players -* Settings tab layout has been redesigned to support longer texts -* Added button to start map editor directly from Launcher -* Simplified game starting flow from online lobby -* Mod description will now show list of languages supported by mod -* Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions -* Size of mod list and mod details sub-windows can now be adjusted by player - -### AI PLAYER: -* Nullkiller AI is now used by default -* AI should now be more active in destroying heroes causing treat on AI towns -* AI now has higher priority for resource-producing mines -* Increased AI priority of town dwelling upgrades -* AI will now de-prioritize town hall upgrades when low on resources -* Messages from cheats used by AI are now hidden -* Improved army gathering from towns -* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army. -* Improved Pandora handling -* AI takes into account fort level now when evaluating enemy town capturing priority. -* AI can not use allied shipyard now to avoid freeze -* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed. -* AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist) - -### RANDOM MAP GENERATOR -* Random map generator can now be used without vcmi-extras mod -* RMG will no longer place shipyards or boats at very small lakes -* Fixed placement of shipyards in invalid locations -* Fixed potential game hang on generation of random map -* RMG will now generate addditional monolith pairs to create required number of zone connections -* RMG will try to place Subterranean Gates as far away from other objects (including each other) as possible -* RMG will now try to place objects as far as possible in both zones sharing a guard, not only the first one. -* Use only one template for an object in zone -* Objects with limited per-map count will be distributed evenly among zones with suitable terrain -* Objects above zone treasure value will not be considered for placement -* RMG will prefer terrain-specific templates for objects placement -* RMG will place Towns and Monoliths first in order to generate long roads across the zone. -* Adjust the position of center town in the zone for better look & feel on S maps. -* Description of random map will correctly show number of levels -* Fixed amount of creatures found in Pandora Boxes to match H3 -* Visitable objects will no longer be placed on top of the map, obscured by map border - -### ADVENTURE MAP: -* Added option to replace popup messages on object visiting with messages in status window -* Implemented different hero movement sounds for offroad movement -* Cartographers now reveal terrain in the same way as in H3 -* Status bar will now show movement points information on pressing ALT or after enabling option in settings -* It is now not possible to receive rewards from School of War without required gold amount -* Owned objects, like Mines and Dwellings will always show their owner in status bar -* It is now possible to interact with on-map Shipyard when no hero is selected -* Added option to show amount of creatures as numeric range rather than adjective -* Added option to show map grid -* Map swipe is no longer exclusive for phones and can be enabled on desktop platforms -* Added more graduated settigns for hero movement speed -* Map scrolling is now more graduated and scrolls with pixel-level precision -* Hero movement speed now matches H3 -* Improved performance of adventure map rendering -* Fixed embarking and disembarking sounds -* Fixed selection of "new week" animation for status window -* Object render order now mostly matches H3 -* Fixed movement cost calculation when using "Fly" spell or "Angel Wings" -* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy -* Fixed invalid ambient sound of Whirlpool -* Hero path will now be correctly removed on defeating monsters that are at the end of hero path -* Seer Hut tooltips will now show messages for correct quest type - -### INTERFACE -* Implemented new settings window -* Added framerate display option -* Fixed white status bar on server connection screen -* Buttons in battle window now correctly show tooltip in status bar -* Fixed cursor image during enemy turn in combat -* Game will no longer promt to assemble artifacts if they fall into backpack -* It is now possible to use in-game console for vcmi commands -* Stacks sized 1000-9999 units will not be displayed as "1k" -* It is now possible to select destination town for Town Portal via double-click -* Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads - -### HERO SCREEN -* Fixed cases of incorrect artifact slot highlighting -* Improved performance of artifact exchange operation -* Picking up composite artifact will immediately unlock slots -* It is now possible to swap two composite artifacts - -### TOWN SCREEN -* Fixed gradual fade-in of a newly built building -* Fixed duration of building fade-in to match H3 -* Fixed rendering of Shipyard in Castle -* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine -* Added option to show number of available creatures in place of growth -* Fixed possible interaction with hero / town list from adventure map while in town screen -* Fixed missing left-click message popup for some town buildings -* Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes" - -### BATTLES: -* Added settings for even faster animation speed than in H3 -* Added display of potential kills numbers into attack tooltip in status bar -* Added option to skip battle opening music entirely -* All effects will now wait for battle opening sound before playing -* Hex highlighting will now be disabled during enemy turn -* Fixed incorrect log message when casting spell that kills zero units -* Implemented animated cursor for spellcasting -* Fixed multiple issues related to ordering of creature animations -* Fixed missing flags from hero animations when opening menus -* Fixed rendering order of moat and grid shadow -* Jousting bonus from Champions will now be correctly accounted for in damage estimation -* Building Castle building will now provide walls with additional health point -* Speed of all battle animations should now match H3 -* Fixed missing obstacles on subterranean terrain -* Ballistics mechanics now matches H3 logic -* Arrow Tower base damage should now match H3 -* Destruction of wall segments will now remove ranged attack penalty -* Force Field cast in front of drawbridge will now block it as in H3 -* Fixed computations for Behemoth defense reduction ability -* Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics -* Fixed highlighting of movement range for creatures standing on a corpse -* All battle animations now have same duration/speed as in H3 -* Added missing combat log message on resurrecting creatures -* Fixed visibility of blue border around targeted creature when spellcaster is making turn -* Fixed selection highlight when in targeted creature spellcasting mode -* Hovering over hero now correctly shows hero cursor -* Creature currently making turn is now highlighted in the Battle Queue -* Hovering over creature icon in Battle Queue will highlight this creature in the battlefield -* New battle UI extension allows control over creatures' special abilities -* Fixed crash on activating auto-combat in battle -* Fixed visibility of unit creature amount labels and timing of their updates -* Firewall will no longer hit double-wide units twice when passing through -* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance -* Orb of Vulnerability will now negate Resistance skill - -### SPELLS: -* Hero casting animation will play before spell effect -* Fire Shield: added sound effect -* Fire Shield: effect now correctly plays on defending creature -* Earthquake: added sound effect -* Earthquake: spell will not select sections that were already destroyed before cast -* Remove Obstacles: fixed error message when casting on maps without obstacles -* All area-effect spells (e.g. Fireball) will play their effect animation on top -* Summoning spells: added fade-in effect for summoned creatures -* Fixed timing of hit animation for damage-dealing spells -* Obstacle-creating spells: UI is now locked during effect animation -* Obstacle-creating spells: added sound effect -* Added reverse death animation for spells that bring stack back to life -* Bloodlust: implemented visual effect -* Teleport: implemented visual fade-out and fade-in effect for teleporting -* Berserk: Fixed duration of effect -* Frost Ring: Fixed spell effect range -* Fixed several cases where multiple different effects could play at the same time -* All spells that can affecte multiple targets will now highlight affected stacks -* Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels - -### ABILITIES: -* Rebirth (Phoenix): Sound will now play in the same time as animation effect -* Master Genie spellcasting: Sound will now play in the same time as animation effect -* Power Lich, Magogs: Sound will now play in the same time as attack animation effect -* Dragon Breath attack now correctly uses different attack animation if multiple targets are hit -* Petrification: implemented visual effect -* Paralyze: added visual effect -* Blind: Stacks will no longer retailate on attack that blinds them -* Demon Summon: Added animation effect for summoning -* Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath -* Weakness now has correct visual effect -* Added damage bonus for opposite elements for Elementals -* Added damage reduction for Magic Elemental attacks against creatures immune to magic -* Added incoming damage reduction to Petrify -* Added counter-attack damage reduction for Paralyze - -### MODDING: -* All configurable objects from H3 now have their configuration in json -* Improvements to functionality of configurable objects -* Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill. -* Removed multiple bonuses that can be replaced with another bonus. -* It is now possible to define new hero movement sounds in terrains -* Implemented translation support for mods -* Implemented translation support for .h3m maps and .h3c campaigns -* Translation mods are now automatically disabled if player uses different language -* Files with new Terrains, Roads and Rivers are now validated by game -* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json -* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range -* Battle window and Random Map Tab now have their layout defined in json file -* Implemented code support for alternative actions mod -* Implemented code support for improved random map dialog -* It is now possible to configure number of creature stacks in heroes' starting armies -* It is now possible to configure number of constructed dwellings in towns on map start -* Game settings previously located in defaultMods.json are now loaded directly from mod.json -* It is now possible for spellcaster units to have multiple spells (but only for targeting different units) -* Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions - -# 1.1.0 -> 1.1.1 - -### GENERAL: -* Fixed missing sound in Polish version from gog.com -* Fixed positioning of main menu buttons in localized versions of H3 -* Fixed crash on transferring artifact to commander -* Fixed game freeze on receiving multiple artifact assembly dialogs after combat -* Fixed potential game freeze on end of music playback -* macOS/iOS: fixed sound glitches -* Android: upgraded version of SDL library -* Android: reworked right click gesture and relative pointer mode -* Improved map loading speed -* Ubuntu PPA: game will no longer crash on assertion failure - -### ADVENTURE MAP: -* Fixed hero movement lag in single-player games -* Fixed number of drowned troops on visiting Sirens to match H3 -* iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console - -### TOWNS: -* Fixed displaying growth bonus from Statue of Legion -* Growth bonus tooltip ordering now matches H3 -* Buy All Units dialog will now buy units starting from the highest level - -### LAUNCHER: -* Local mods can be disabled or uninstalled -* Fixed styling of Launcher interface - -### MAP EDITOR: -* Fixed saving of roads and rivers -* Fixed placement of heroes on map - -# 1.0.0 -> 1.1.0 - -### GENERAL: -* iOS is supported -* Mods and their versions and serialized into save files. Game checks mod compatibility before loading -* Logs are stored in system default logs directory -* LUA/ERM libs are not compiled by default -* FFMpeg dependency is optional now -* Conan package manager is supported for MacOS and iOS - -### MULTIPLAYER: -* Map is passed over network, so different platforms are compatible with each other -* Server self-killing is more robust -* Unlock in-game console while opponent's turn -* Host can control game session by using console commands -* Control over player is transferred to AI if client escaped the game -* Reconnection mode for crashed client processes -* Playing online is available using proxy server - -### ADVENTURE MAP: -* Fix for digging while opponent's turn -* Supported right click for quick recruit window -* Fixed problem with quests are requiring identical artefacts -* Bulk move and swap artifacts -* Pause & resume for towns and terrains music themes -* Feature to assemble/disassemble artefacts in backpack -* Clickable status bar to send messages -* Heroes no longer have chance to receive forbidden skill on leveling up -* Fixed visibility of newly recruited heroes near town -* Fixed missing artifact slot in Artifact Merchant window - -### BATTLES: -* Fix healing/regeneration behaviour and effect -* Fix crashes related to auto battle -* Implemented ray projectiles for shooters -* Introduced default tower shooter icons -* Towers destroyed during battle will no longer be listed as casualties - -### AI: -* BattleAI: Target prioritizing is now based on damage difference instead of health difference -* Nullkiller AI can retreat and surrender -* Nullkiller AI doesn't visit allied dwellings anymore -* Fixed a few freezes in Nullkiller AI - -### RANDOM MAP GENERATOR: -* Speedup generation of random maps -* Necromancy cannot be learned in Witch Hut on random maps - -### MODS: -* Supported rewardable objects customization -* Battleground obstacles are extendable now with VLC mechanism -* Introduced "compatibility" section into mods settings -* Fixed bonus system for custom advmap spells -* Supported customisable town entrance placement -* Fixed validation of mods with new adventure map objects - -### LAUNCHER: -* Fixed problem with duplicated mods in the list -* Launcher shows compatible mods only -* Uninstall button was moved to the left of layout -* Unsupported resolutions are not shown -* Lobby for online gameplay is implemented - -### MAP EDITOR: -* Basic version of Qt-based map editor - -# 0.99 -> 1.0.0 - -### GENERAL: -* Spectator mode was implemented through command-line options -* Some main menu settings get saved after returning to main menu - last selected map, save etc. -* Restart scenario button should work correctly now -* Skyship Grail works now immediately after capturing without battle -* Lodestar Grail implemented -* Fixed Gargoyles immunity -* New bonuses: -* * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 -* * TRANSMUTATION - "WoG werewolf"-like ability -* * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension -* * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so -* * RANGED_RETALIATION - allows ranged counterattack -* * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack -* * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills -* * MANUAL_CONTROL - grant manual control over war machine -* * WIDE_BREATH - melee creature attacks affect many nearby hexes -* * FIRST_STRIKE - creature counterattacks before attack if possible -* * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) -* * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes -* * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this -* * DESTRUCTION - creature ability for killing extra units after hit, configurable - -### MULTIPLAYER: -* Loading support. Save from single client could be used to load all clients. -* Restart support. All clients will restart together on same server. -* Hotseat mixed with network game. Multiple colors can be controlled by each client. - -### SPELLS: -* Implemented cumulative effects for spells - -### MODS: -* Improve support for WoG commander artifacts and skill descriptions -* Added support for modding of original secondary skills and creation of new ones. -* Map object sounds can now be configured via json -* Added bonus updaters for hero specialties -* Added allOf, anyOf and noneOf qualifiers for bonus limiters -* Added bonus limiters: alignment, faction and terrain -* Supported new terrains, new battlefields, custom water and rock terrains -* Following special buildings becomes available in the fan towns: -* * attackVisitingBonus -* * defenceVisitingBonus -* * spellPowerVisitingBonus -* * knowledgeVisitingBonus -* * experienceVisitingBonus -* * lighthouse -* * treasury - -### SOUND: -* Fixed many mising or wrong pickup and visit sounds for map objects -* All map objects now have ambient sounds identical to OH3 - -### RANDOM MAP GENERATOR: -* Random map generator supports water modes (normal, islands) -* Added config randomMap.json with settings for map generator -* Added parameter for template allowedWaterContent -* Extra resource packs appear nearby mines -* Underground can be player starting place for factions allowed to be placed underground -* Improved obstacles placement aesthetics -* Rivers are generated on the random maps -* RMG works more stable, various crashes have been fixed -* Treasures requiring guards are guaranteed to be protected - -### VCAI: -* Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster. -* AI will now use universal pathfinding globally -* AI can use Summon Boat and Town Portal -* AI can gather and save resources on purpose -* AI will only buy army on demand instead of every turn -* AI can distinguish the value of all map objects -* General speed optimizations - -### BATTLES: -* Towers should block ranged retaliation -* AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed -* Towers do not attack war machines automatically -* Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose) - -### ADVENTURE MAP: -* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes -* Fix: Captured town should not be duplicated on the UI - -### LAUNCHER: -* Implemented notifications about updates -* Supported redirection links for downloading mods - -# 0.98 -> 0.99 - -### GENERAL: -* New Bonus NO_TERRAIN_PENALTY -* Nomads will remove Sand movement penalty from army -* Flying and water walking is now supported in pathfinder -* New artifacts supported -* * Angel Wings -* * Boots of Levitation -* Implemented rumors in tavern window -* New cheat codes: -* * vcmiglaurung - gives 5000 crystal dragons into each slot -* * vcmiungoliant - conceal fog of war for current player -* New console commands: -* * gosolo - AI take control over human players and vice versa -* * controlai - give control of one or all AIs to player -* * set hideSystemMessages on/off - supress server messages in chat - -### BATTLES: -* Drawbridge mechanics implemented (animation still missing) -* Merging of town and visiting hero armies on siege implemented -* Hero info tooltip for skills and mana implemented - -### ADVENTURE AI: -* Fixed AI trying to go through underground rock -* Fixed several cases causing AI wandering aimlessly -* AI can again pick best artifacts and exchange artifacts between heroes -* AI heroes with patrol enabled won't leave patrol area anymore - -### RANDOM MAP GENERATOR: -* Changed fractalization algorithm so it can create cycles -* Zones will not have straight paths anymore, they are totally random -* Generated zones will have different size depending on template setting -* Added Thieves Guild random object (1 per zone) -* Added Seer Huts with quests that match OH3 -* RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs - -# 0.97 -> 0.98 - -### GENERAL: -* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) - -### ADVENTURE AI: -* AI will try to use Monolith entrances for exploration -* AI will now always revisit each exit of two way monolith if exit no longer visible -* AI will eagerly pick guarded and blocked treasures - -### ADVENTURE MAP: -* Implemented world view -* Added graphical fading effects - -### SPELLS: -* New spells handled: -* * Earthquake -* * View Air -* * View Earth -* * Visions -* * Disguise -* Implemented CURE spell negative dispell effect -* Added LOCATION target for spells castable on any hex with new target modifiers - -### BATTLES: -* Implemented OH3 stack split / upgrade formulas according to AlexSpl - -### RANDOM MAP GENERATOR: -* Underground tunnels are working now -* Implemented "junction" zone type -* Improved zone placing algorithm -* More balanced distribution of treasure piles -* More obstacles within zones - -# 0.96 -> 0.97 (Nov 01 2014) - -### GENERAL: -* (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi' -* (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves' -* (linux) -* Changes in used librries: -* * VCMI can now be compiled with SDL2 -* * Movies will use ffmpeg library -* * change boost::bind to std::bind -* * removed boost::asign -* * Updated FuzzyLite to 5.0 -* Multiplayer load support was implemented through command-line options - -### ADVENTURE AI: -* Significantly optimized execution time, AI should be much faster now. - -### ADVENTURE MAP: -* Non-latin characters can now be entered in chat window or used for save names. -* Implemented separate speed for owned heroes and heroes owned by other players - -### GRAPHICS: -* Better upscaling when running in fullscreen mode. -* New creature/commader window -* New resolutions and bonus icons are now part of a separate mod -* Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn) - -### RANDOM MAP GENERATOR: -* Random map generator now creates complete and playable maps, should match original RMG -* All important features from original map templates are implemented -* Fixed major crash on removing objects -* Undeground zones will look just like surface zones - -### LAUNCHER: -* Implemented switch to disable intro movies in game - -# 0.95 -> 0.96 (Jul 01 2014) - -### GENERAL: -* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 - -### ADVENTURE AI: -* Optimized speed and removed various bottlenecks. - -### ADVENTURE MAP: -* Heroes auto-level primary and secondary skill levels according to experience - -### BATTLES: -* Wall hit/miss sound will be played when using catapult during siege - -### SPELLS: -* New configuration format - -### RANDOM MAP GENERATOR: -* Towns from mods can be used -* Reading connections, terrains, towns and mines from template -* Zone placement -* Zone borders and connections, fractalized paths inside zones -* Guard generation -* Treasue piles generation (so far only few removable objects) - -### MODS: -* Support for submods - mod may have their own "submods" located in /Mods directory -* Mods may provide their own changelogs and screenshots that will be visible in Launcher -* Mods can now add new (offensive, buffs, debuffs) spells and change existing -* Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings - -### GENERAL: -* Added configuring of heroes quantity per player allowed in game - -# 0.94 -> 0.95 (Mar 01 2014) - -### GENERAL: -* Components of combined artifacts will now display info about entire set. -* Implements level limit -* Added WoG creature abilities by Kuririn -* Implemented a confirmation dialog when pressing Alt + F4 to quit the game -* Added precompiled header compilation for CMake (can be enabled per flag) -* VCMI will detect changes in text files using crc-32 checksum -* Basic support for unicode. Internally vcmi always uses utf-8 -* (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher -* (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience) - -### ADVENTURE AI: -* AI will use fuzzy logic to compare and choose multiple possible subgoals. -* AI will now use SectorMap to find a way to guarded / covered objects. -* Significantly improved exploration algorithm. -* Locked heroes now try to decompose their goals exhaustively. -* Fixed (common) issue when AI found neutral stacks infinitely strong. -* Improvements for army exchange criteria. -* GatherArmy may include building dwellings in town (experimental). -* AI should now conquer map more aggressively and much faster -* Fuzzy rules will be printed out at map launch (if AI log is enabled) - -### CAMPAIGNS: -* Implemented move heroes to next scenario -* Support for non-standard victory conditions for H3 campaigns -* Campaigns use window with bonus & scenario selection than scenario information window from normal maps -* Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign) -* Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3 -* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 - -### TOWNS: -* Extended building dependencies support - -### MODS: -* Custom victory/loss conditions for maps or campaigns -* 7 days without towns loss condition is no longer hardcoded -* Only changed mods will be validated - -# 0.93 -> 0.94 (Oct 01 2013) - -### GENERAL: -* New Launcher application, see -* Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory. -* fixed "get txt" console command -* command "extract" to extract file by name -* command "def2bmp" to convert def into set of frames. -* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus) -* fixed duels, added no-GUI mode for automatic AI testing -* Sir Mullich is available at the start of the game -* Upgrade cost will never be negative. -* support for Chinese fonts (GBK 2-byte encoding) - -### ADVENTURE MAP: -* if Quick Combat option is turned on, battles will be resolved by AI -* first hero is awakened on new turn -* fixed 3000 gems reward in shipwreck - -### BATTLES: -* autofight implemented -* most of the animations is time-based -* simplified postioning of units in battle, should fix remaining issues with unit positioning -* synchronized attack/defence animation -* spell animation speed uses game settings -* fixed disrupting ray duration -* added logging domain for battle animations -* Fixed crashes on Land Mines / Fire Wall casting. -* UI will be correctly greyed-out during opponent turn -* fixed remaining issues with blit order -* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3) -* Fixed Remove Obstacle. -* defeating hero will yield 500 XP -* Added lots of missing spell immunities from Strategija -* Added stone gaze immunity for Troglodytes (did you know about it?) -* damage done by turrets is properly increased by built buldings -* Wyverns will cast Poison instead of Stone Gaze. - -### TOWN: -* Fixed issue that allowed to build multiple boats in town. -* fix for lookout tower - -# 0.92 -> 0.93 (Jun 01 2013) - -### GENERAL: -* Support for SoD-only installations, WoG becomes optional addition -* New logging framework -* Negative luck support, disabled by default -* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack) -* Fixed stack artifact (and related buttons) not displaying in creature window. -* Fixed crash at month of double population. - -### MODS: -* Improved json validation. Now it support most of features from latest json schema draft. -* Icons use path to icon instead of image indexes. -* It is possible to edit data of another mod or H3 data via mods. -* Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility) -* Removed no longer needed field "projectile spins" -* Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json. - -### BATTLES: -* Fixed Death Stare of Commanders -* Projectile blitting should be closer to original H3. But still not perfect. -* Fixed missing Mirth effects -* Stack affected by Berserk should not try to attack itself -* Fixed several cases of incorrect positioning of creatures in battles -* Fixed abilities of Efreet. -* Fixed broken again palette in some battle backgrounds - -### TOWN: -* VCMI will not crash if building selection area is smaller than def -* Detection of transparency on selection area is closer to H3 -* Improved handling buildings with mode "auto": -* * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) -* * transitive dependencies are handled (A makes B build, and B makes C and D) - -### SOUND: -* Added missing WoG creature sounds (from Kuririn). -* The Windows package comes with DLLs needed to play .ogg files -* (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's -* some missing sounds for battle effects - -### ARTIFACTS: -* Several fixes to combined artifacts added via mods. -* Fixed Spellbinder's Hat giving level 1 spells instead of 5. -* Fixed incorrect components of Cornucopia. -* Cheat code with grant all artifacts, including the ones added by mods - -# 0.91 -> 0.92 (Mar 01 2013) - -### GENERAL: -* hero crossover between missions in campaigns -* introduction before missions in campaigns - -### MODS: -* Added CREATURE_SPELL_POWER for commanders -* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine -* Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION -* Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges -* Double growth creatures are configurable now -* Drain Life now has % effect depending on bonus value -* Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT". -* Moat damage configurable -* More config options for spells: -* * mind immunity handled by config -* * direct damage immunity handled by config -* * immunity icon configurable -* * removed mind_spell flag -* creature config use string ids now. -* support for string subtype id in short bonus format -* primary skill identifiers for bonuses - -# 0.9 -> 0.91 (Feb 01 2013) - -### GENERAL: -* VCMI build on OS X is now supported -* Completely removed autotools -* Added RMG interace and ability to generate simplest working maps -* Added loading screen - -### MODS: -* Simplified mod structure. Mods from 0.9 will not be compatible. -* Mods can be turned on and off in config/modSettings.json file -* Support for new factions, including: -* * New towns -* * New hero classes -* * New heroes -* * New town-related external dwellings -* Support for new artifact, including combined, commander and stack artifacts -* Extended configuration options -* * All game objects are referenced by string identifiers -* * Subtype resolution for bonuses - -### BATTLES: -* Support for "enchanted" WoG ability - -### ADVENTURE AI: -* AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration -* Improved exploration algorithm -* AI will prioritize dwellings and mines when there are no opponents visible - -# 0.89 -> 0.9 (Oct 01 2012) - -### GENERAL: -* Provisional support creature-adding mods -* New filesystem allowing easier resource adding/replacing -* Reorganized package for better compatibility with HotA and not affecting the original game -* Moved many hard-coded settings into text config files -* Commander level-up dialog -* New Quest Log window -* Fixed a number of bugs in campaigns, support for starting hero selection bonus. - -### BATTLES: -* New graphics for Stack Queue -* Death Stare works identically to H3 -* No explosion when catapult fails to damage the wall -* Fixed crash when attacking stack dies before counterattack -* Fixed crash when attacking stack dies in the Moat just before the attack -* Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented) -* Fleeing hero won't lose artifacts. -* Spellbook won't be captured. - -### ADVENTURE AI: -* support for quests (Seer Huts, Quest Guardians, and so) -* AI will now wander with all the heroes that have spare movement points. It should prevent stalling. -* AI will now understand threat of Abandoned Mine. -* AI can now exchange armies between heroes. By default, it will pass army to main hero. -* Fixed strange case when AI found allied town extremely dangerous -* Fixed crash when AI tried to "revisit" a Boat -* Fixed crash when hero assigned to goal was lost when attempting realizing it -* Fixed a possible freeze when exchanging resources at marketplace - -### BATTLE AI: -* It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of avaialble modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases. -* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells - -# 0.88 -> 0.89 (Jun 01 2012) - -### GENERAL: -* Mostly implemented Commanders feature (missing level-up dialog) -* Support for stack artifacts -* New creature window graphics contributed by fishkebab -* Config file may have multiple upgrades for creatures -* CTRL+T will open marketplace window -* G will open thieves guild window if player owns at least one town with tavern -* Implemented restart functionality. CTRL+R will trigger a quick restart -* Save game screen and returning to main menu will work if game was started with --start option -* Simple mechanism for detecting game desynchronization after init -* 1280x800 resolution graphics, contributed by Topas - -### ADVENTURE MAP: -* Fixed monsters regenerating casualties from battle at the start of new week. -* T in adventure map will switch to next town - -### BATTLES: -* It's possible to switch active creature during tacts phase by clicking on stack -* After battle artifacts of the defeated hero (and his army) will be taken by winner -* Rewritten handling of battle obstacles. They will be now placed following H3 algorithm. -* Fixed crash when death stare or acid breath activated on stack that was just killed -* First aid tent can heal only creatures that suffered damage -* War machines can't be healed by tent -* Creatures casting spells won't try to cast them during tactic phase -* Console tooltips for first aid tent -* Console tooltips for teleport spell -* Cursor is reset to pointer when action is requested -* Fixed a few other missing or wrong tooltips/cursors -* Implemented opening creature window by l-clicking on stack -* Fixed crash on attacking walls with Cyclop Kings -* Fixed and simplified Teleport casting -* Fixed Remove Obstacle spell -* New spells supported: -* * Chain Lightning -* * Fire Wall -* * Force Field -* * Land Mine -* * Quicksands -* * Sacrifice - -### TOWNS: -* T in castle window will open a tavern window (if available) - -### PREGAME: -* Pregame will use same resolution as main game -* Support for scaling background image -* Customization of graphics with config file. - -### ADVENTURE AI: -* basic rule system for threat evaluation -* new town development logic -* AI can now use external dwellings -* AI will weekly revisit dwellings & mills -* AI will now always pick best stacks from towns -* AI will recruit multiple heroes for exploration -* AI won't try attacking its own heroes - -# 0.87 -> 0.88 (Mar 01 2012) - -* added an initial version of new adventure AI: VCAI -* system settings window allows to change default resolution -* introduced unified JSON-based settings system -* fixed all known localization issues -* Creature Window can handle descriptions of spellcasting abilities -* Support for the clone spell - -# 0.86 -> 0.87 (Dec 01 2011) - -### GENERAL: -* Pathfinder can find way using ships and subterranean gates -* Hero reminder & sleep button - -### PREGAME: -* Credits are implemented - -### BATTLES: -* All attacked hexes will be highlighted -* New combat abilities supported: -* * Spell Resistance aura -* * Random spellcaster (Genies) -* * Mana channeling -* * Daemon summoning -* * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) -* * Fear -* * Fearless -* * No wall penalty -* * Enchanter -* * Bind -* * Dispell helpful spells - -# 0.85 -> 0.86 (Sep 01 2011) - -### GENERAL: -* Reinstated music support -* Bonus system optimizations (caching) -* converted many config files to JSON -* .tga file support -* New artifacts supported -* * Admiral's Hat -* * Statue of Legion -* * Titan's Thunder - -### BATTLES: -* Correct handling of siege obstacles -* Catapult animation -* New combat abilities supported -* * Dragon Breath -* * Three-headed Attack -* * Attack all around -* * Death Cloud / Fireball area attack -* * Death Blow -* * Lightning Strike -* * Rebirth -* New WoG abilities supported -* * Defense Bonus -* * Cast before attack -* * Immunity to direct damage spells -* New spells supported -* * Magic Mirror -* * Titan's Lightning Bolt - -# 0.84 -> 0.85 (Jun 01 2011) - -### GENERAL: -* Support for stack experience -* Implemented original campaign selection screens -* New artifacts supported: -* * Statesman's Medal -* * Diplomat's Ring -* * Ambassador's Sash - -### TOWNS: -* Implemented animation for new town buildings -* It's possible to sell artifacts at Artifact Merchants - -### BATTLES: -* Neutral monsters will be split into multiple stacks -* Hero can surrender battle to keep army -* Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness -* Partial support for Stone Gaze, Paralyze, Mana drain - -# 0.83 -> 0.84 (Mar 01 2011) - -### GENERAL: -* Bonus system has been rewritten -* Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles) -* New artifacts supported: -* * Angellic Alliance -* * Bird of Perception -* * Emblem of Cognizance -* * Spell Scroll -* * Stoic Watchman - -### BATTLES: -* Better animations handling -* Defensive stance is supported - -### HERO: -* New secondary skills supported: -* * Artillery -* * Eagle Eye -* * Tactics - -### AI PLAYER: -* new AI leading neutral creatures in combat, slightly better then previous - -# 0.82 -> 0.83 (Nov 01 2010) - -### GENERAL: -* Alliances support -* Week of / Month of events -* Mostly done pregame for MP games (temporarily only for local clients) -* Support for 16bpp displays -* Campaigns: -* * support for building bonus -* * moving to next map after victory -* Town Portal supported -* Vial of Dragon Blood and Statue of Legion supported - -### HERO: -* remaining specialities have been implemented - -### TOWNS: -* town events supported -* Support for new town structures: Deiety of Fire and Escape Tunnel - -### BATTLES: -* blocked retreating from castle - -# 0.81 -> 0.82 (Aug 01 2010) - -### GENERAL: -* Some of the starting bonuses in campaigns are supported -* It's possible to select difficulty level of mission in campaign -* new cheat codes: -* * vcmisilmaril - player wins -* * vcmimelkor - player loses - -### ADVENTURE MAP: -* Neutral armies growth implemented (10% weekly) -* Power rating of neutral stacks -* Favourable Winds reduce sailing cost - -### HERO: -* Learning secondary skill supported. -* Most of hero specialities are supported, including: -* * Creature specialities (progressive, fixed, Sir Mullich) -* * Spell damage specialities (Deemer), fixed bonus (Ciele) -* * Secondary skill bonuses -* * Creature Upgrades (Gelu) -* * Resorce generation -* * Starting Skill (Adrienne) - -### TOWNS: -* Support for new town structures: -* * Artifact Merchant -* * Aurora Borealis -* * Castle Gates -* * Magic University -* * Portal of Summoning -* * Skeleton transformer -* * Veil of Darkness - -### OBJECTS: -* Stables will now upgrade Cavaliers to Champions. -* New object supported: -* * Abandoned Mine -* * Altar of Sacrifice -* * Black Market -* * Cover of Darkness -* * Hill Fort -* * Refugee Camp -* * Sanctuary -* * Tavern -* * University -* * Whirlpool - -# 0.8 -> 0.81 (Jun 01 2010) - -### GENERAL: -* It's possible to start campaign -* Support for build grail victory condition -* New artifacts supported: -* * Angel's Wings -* * Boots of levitation -* * Orb of Vulnerability -* * Ammo cart -* * Golden Bow -* * Hourglass of Evil Hour -* * Bow of Sharpshooter -* * Armor of the Damned - -### ADVENTURE MAP: -* Creatures now guard surrounding tiles -* New adventura map spells supported: -* * Summon Boat -* * Scuttle Boat -* * Dimension Door -* * Fly -* * Water walk - -### BATTLES: -* A number of new creature abilities supported -* First Aid Tent is functional -* Support for distance/wall/melee penalties & no * penalty abilities -* Reworked damage calculation to fit OH3 formula better -* Luck support -* Teleportation spell - -### HERO: -* First Aid secondary skill -* Improved formula for necromancy to match better OH3 - -### TOWNS: -* Sending resources to other players by marketplace -* Support for new town structures: -* * Lighthouse -* * Colossus -* * Freelancer's Guild -* * Guardian Spirit -* * Necromancy Amplifier -* * Soul Prison - -### OBJECTS: -* New object supported: -* * Freelancer's Guild -* * Trading Post -* * War Machine Factory - -# 0.75 -> 0.8 (Mar 01 2010) - -### GENERAL: -* Victory and loss conditions are supported. It's now possible to win or lose the game. -* Implemented assembling and disassembling of combination artifacts. -* Kingdom Overview screen is now available. -* Implemented Grail (puzzle map, digging, constructing ultimate building) -* Replaced TTF fonts with original ones. - -### ADVENTURE MAP: -* Implemented rivers animations (thx to GrayFace). - -### BATTLES: -* Fire Shield spell (and creature ability) supported -* affecting morale/luck and casting spell after attack creature abilities supported - -### HERO: -* Implementation of Scholar secondary skill - -### TOWN: -* New left-bottom info panel functionalities. - -### TOWNS: -* new town structures supported: -* * Ballista Yard -* * Blood Obelisk -* * Brimstone Clouds -* * Dwarven Treasury -* * Fountain of Fortune -* * Glyphs of Fear -* * Mystic Pond -* * Thieves Guild -* * Special Grail functionalities for Dungeon, Stronghold and Fortress - -### OBJECTS: -* New objects supported: -* * Border gate -* * Den of Thieves -* * Lighthouse -* * Obelisk -* * Quest Guard -* * Seer hut - -A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=14 - -# 0.74 -> 0.75 (Dec 01 2009) - -### GENERAL: -* Implemented "main menu" in-game option. -* Hide the mouse cursor while displaying a popup window. -* Better handling of huge and empty message boxes (still needs more changes) -* Fixed several crashes when exiting. - -### ADVENTURE INTERFACE: -* Movement cursor shown for unguarded enemy towns. -* Battle cursor shown for guarded enemy garrisons. -* Clicking on the border no longer opens an empty info windows - -### HERO WINDOW: -* Improved artifact moving. Available slots are higlighted. Moved artifact is bound to mouse cursor. - -### TOWNS: -* new special town structures supported: -* * Academy of Battle Scholars -* * Cage of Warlords -* * Mana Vortex -* * Stables -* * Skyship (revealing entire map only) - -### OBJECTS: -* External dwellings increase town growth -* Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none -* Scholar won't give unavaliable spells anymore. - -A lot of of various bugfixes and improvements: -http://bugs.vcmi.eu/changelog_page.php?version_id=2 - -# 0.73 -> 0.74 (Oct 01 2009) - -### GENERAL: -* Scenario Information window -* Save Game window -* VCMI window should start centered -* support for Necromancy and Ballistics secondary skills -* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining) -* VCMI client has its own icon (thx for graphic to Dikamilo) -* Ellipsis won't be split when breaking text on several lines -* split button will be grayed out when no creature is selected -* fixed issue when splitting stack to the hero with only one creatures -* a few fixes for shipyard window - -### ADVENTURE INTERFACE: -* Cursor shows if tile is accesible and how many turns away -* moving hero with arrow keys / numpad -* fixed Next Hero button behaviour -* fixed Surface/Underground switch button in higher resolutions - -### BATTLES: -* partial siege support -* new stack queue for higher resolutions (graphics made by Dru, thx!) -* 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press) -* more creatures special abilities supported -* battle settings will be stored -* fixed crashes occurring on attacking two hex creatures from back -* fixed crash when clicking on enemy stack without moving mouse just after receiving action -* even large stack numbers will fit the boxes -* when active stack is killed by spell, game behaves properly -* shooters attacking twice (like Grand Elves) won't attack twice in melee -* ballista can shoot even if there's an enemy creature next to it -* improved obstacles placement, so they'll better fit hexes (thx to Ivan!) -* selecting attack directions works as in H3 -* estimating damage that will be dealt while choosing stack to be attacked -* modified the positioning of battle effects, they should look about right now. -* after selecting a spell during combat, l-click is locked for any action other than casting. -* flying creatures will be blitted over all other creatures, obstacles and wall -* obstacles and units should be printed in better order (not tested) -* fixed armageddon animation -* new spells supported: -* * Anti-Magic -* * Cure -* * Resurrection -* * Animate Dead -* * Counterstrike -* * Berserk -* * Hypnotize -* * Blind -* * Fire Elemental -* * Earth Elemental -* * Water Elemental -* * Air Elemental -* * Remove obstacle - -### TOWNS: -* enemy castle can be taken over -* only one capitol per player allowed (additional ones will be lost) -* garrisoned hero can buy a spellbook -* heroes available in tavern should be always different -* ship bought in town will be correctly placed -* new special town structures supported: -* * Lookout Tower -* * Temple of Valhalla -* * Wall of Knowledge -* * Order of Fire - -### HERO WINDOW: -* war machines cannot be unequiped - -### PREGAME: -* sorting: a second click on the column header sorts in descending order. -* advanced options tab: r-click popups for selected town, hero and bonus -* starting scenario / game by double click -* arrows in options tab are hidden when not available -* subtitles for chosen hero/town/bonus in pregame - -### OBJECTS: -* fixed pairing Subterranean Gates -* New objects supported: -* * Borderguard & Keymaster Tent -* * Cartographer -* * Creature banks -* * Eye of the Magi & Hut of the Magi -* * Garrison -* * Stables -* * Pandora Box -* * Pyramid - -# 0.72 -> 0.73 (Aug 01 2009) - -### GENERAL: -* infowindow popup will be completely on screen -* fixed possible crash with in game console -* fixed crash when gaining artifact after r-click on hero in tavern -* Estates / hero bonuses won't give resources on first day. -* video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window) -* hero meeting window allowing exchanging armies and artifacts between heroes on adventure map -* 'T' hotkey opens marketplace window -* giving starting spells for heroes -* pressing enter or escape close spellbook -* removed redundant quotation marks from skills description and artifact events texts -* disabled autosaving on first turn -* bonuses from bonus artifacts -* increased char per line limit for subtitles under components -* corrected some exp/level values -* primary skills cannot be negative -* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity -* fixed timed events reappearing -* saving system options -* saving hero direction -* r-click popups on enemy heroes and towns -* hero leveling formula matches the H3 - -### ADVENTURE INTERFACE: -* Garrisoning, then removing hero from garrison move him at the end of the heroes list -* The size of the frame around the map depends on the screen size. -* spellbook shows adventure spells when opened on adventure map -* erasing path after picking objects with last movement point - -### BATTLES: -* spell resistance supported (secondary skill, artifacts, creature skill) -* corrected damage inflicted by spells and ballista -* added some missing projectile infos -* added native terrain bonuses in battles -* number of units in stack in battle should better fit the box -* non-living and undead creatures have now always 0 morale -* displaying luck effect animation -* support for battleground overlays: -* * cursed ground -* * magic plains -* * fiery fields -* * rock lands -* * magic clouds -* * lucid pools -* * holy ground -* * clover field -* * evil fog - -### TOWNS: -* fixes for horde buildings -* garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero -* capitol bar in town hall is grey (not red) if already one exists -* fixed crash on entering hall when town was near map edge - -### HERO WINDOW: -* garrisoned heroes won't be shown on the list -* artifacts will be present on morale/luck bonuses list - -### PREGAME: -* saves are sorted primary by map format, secondary by name -* fixed displaying date of saved game (uses local time, removed square character) - -### OBJECTS: -* Fixed primary/secondary skill levels given by a scholar. -* fixed problems with 3-tiles monoliths -* fixed crash with flaggable building next to map edge -* fixed some descriptions for events -* New objects supported: -* * Buoy -* * Creature Generators -* * Flotsam -* * Mermaid -* * Ocean bottle -* * Sea Chest -* * Shipwreck Survivor -* * Shipyard -* * Sirens - -# 0.71 -> 0.72 (Jun 1 2009) - -### GENERAL: -* many sound effects and music -* autosave (to 5 subsequent files) -* artifacts support (most of them) -* added internal game console (activated on TAB) -* fixed 8 hero limit to check only for wandering heroes (not garrisoned) -* improved randomization -* fixed crash on closing application -* VCMI won't always give all three stacks in the starting armies -* fix for drawing starting army creatures count -* Diplomacy secondary skill support -* timed events won't cause resources amount to be negative -* support for sorcery secondary skill -* reduntant quotation marks from artifact descriptions are removed -* no income at the first day - -### ADVENTURE INTERFACE: -* fixed crasbug occurring on revisiting objects (by pressing space) -* always restoring default cursor when movng mouse out of the terrain -* fixed map scrolling with ctrl+arrows when some windows are opened -* clicking scrolling arrows in town/hero list won't open town/hero window -* pathfinder will now look for a path going via printed positions of roads when it's possible -* enter can be used to open window with selected hero/town - -### BATTLES: -* many creatures special skills implemented -* battle will end when one side has only war machines -* fixed some problems with handling obstacles info -* fixed bug with defending / waiting while no stack is active -* spellbook button is inactive when hero cannot cast any spell -* obstacles will be placed more properly when resolution is different than 800x600 -* canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell) -* spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible -* new spells: -* * frost ring -* * fireball -* * inferno -* * meteor shower -* * death ripple -* * destroy undead -* * dispel -* * armageddon -* * disrupting ray -* * protection from air -* * protection from fire -* * protection from water -* * protection from earth -* * precision -* * slayer - -### TOWNS: -* resting in town with mage guild will replenih all the mana points -* fixed Blacksmith -* the number of creatures at the beginning of game is their base growth -* it's possible to enter Tavern via Brotherhood of Sword - -### HERO WINDOW: -* fixed mana limit info in the hero window -* war machines can't be removed -* fixed problems with removing artifacts when all visible slots in backpack are full - -### PREGAME: -* clicking on "advanced options" a second time now closes the tab instead of refreshing it. -* Fix position of maps names. -* Made the slider cursor much more responsive. Speedup the map select screen. -* Try to behave when no maps/saves are present. -* Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list - -### OBJECTS: -* Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved) -* leaving guardians in flagged mines. -* support for Scholar object -* support for School of Magic -* support for School of War -* support for Pillar of Fire -* support for Corpse -* support for Lean To -* support for Wagon -* support for Warrior's Tomb -* support for Event -* Corpse (Skeleton) will be accessible from all directions - -# 0.7 -> 0.71 (Apr 01 2009) - -### GENERAL: -* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) -* morale/luck system and corresponding sec. skills supported -* fixed crash when hero get level and has less than two sec. skills to choose between -* added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key -* proper handling of custom portraits of heroes -* fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor) -* fixed crash when there was no hero available to hire for some player -* fixed problems with 1024x600 screen resolution -* updating blockmap/visitmap of randomized objects -* fixed crashes on loading maps with flag all mines/dwelling victory condition -* further fixes for leveling-up (stability and identical offered skills bug) -* splitting window allows to rebalance two stack with the same creatures -* support for numpad keyboard -* support for timed events - -### ADVENTURE INTERFACE: -* added "Next hero" button functionality -* added missing path arrows -* corrected centering on hero's position -* recalculating hero path after reselecting hero -* further changes in pathfinder making it more like original one -* orientation of hero can't be change if movement points are exhausted -* campfire, borderguard, bordergate, questguard will be accessible from the top -* new movement cost calculation algorithm -* fixed sight radious calculation -* it's possible to stop hero movement -* faster minimap refreshing -* provisional support for "Save" button in System Options Window -* it's possible to revisit object under hero by pressing Space - -### BATTLES: -* partial support for battle obstacles -* only one spell can be casted per turn -* blocked opening sepllbook if hero doesn't have a one -* spells not known by hero can't be casted -* spell books won't be placed in War Machine slots after battle -* attack is now possible when hex under cursor is not displayed -* glowing effect of yellow border around creatures -* blue glowing border around hovered creature -* made animation on battlefield more smooth -* standing stacks have more static animation -* probably fixed problem with displaying corpses on battlefield -* fixes for two-hex creatures actions -* fixed hero casting spell animation -* corrected stack death animation -* battle settings will be remembered between battles -* improved damage calculation formula -* correct handling of flying creatures in battles -* a few tweaks in battle path/available hexes calculation (more of them is needed) -* amounts of units taking actions / being an object of actions won't be shown until action ends -* fixed positions of stack queue and battle result window when resolution is != 800x600 -* corrected duration of frenzy spell which was incorrect in certain cases -* corrected hero spell casting animation -* better support for battle backgrounds -* blocked "save" command during battle -* spellbook displays only spells known by Hero -* New spells supported: -* * Mirth -* * Sorrow -* * Fortune -* * Misfortune - -### TOWN INTERFACE: -* cannot build more than one capitol -* cannot build shipyard if town is not near water -* Rampart's Treasury requires Miner's Guild -* minor improvements in Recruitment Window -* fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window -* proper updating resdatabar after building structure in town or buying creatures (non 800x600 res) -* fixed blinking resdatabar in town screen when buying (800x600) -* fixed horde buildings displaying in town hall -* forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled - -### PREGAME: -* added scrolling scenario list with mouse wheel -* fixed mouse slow downs -* cannot select heroes for computer player (pregame) -* no crash if uses gives wrong resolution ID number -* minor fixes - -### OBJECTS: -* windmill gives 500 gold only during first week ever (not every month) -* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. -* New objects supported: -* * Prison -* * Magic Well -* * Faerie Ring -* * Swan Pond -* * Idol of Fortune -* * Fountain of Fortune -* * Rally Flag -* * Oasis -* * Temple -* * Watering Hole -* * Fountain of Youth -* * support for Redwood Observatory -* * support for Shrine of Magic Incantation / Gesture / Thought -* * support for Sign / Ocean Bottle - -### AI PLAYER: -* Minor improvements and fixes. - -# 0.64 -> 0.7 (Feb 01 2009) - -### GENERAL: -* move some settings to the config/settings.txt file -* partial support for new screen resolutions -* it's possible to set game resolution in pregame (type 'resolution' in the console) -* /Data and /Sprites subfolders can be used for adding files not present in .lod archives -* fixed crashbug occurring when hero levelled above 15 level -* support for non-standard screen resolutions -* F4 toggles between full-screen and windowed mode -* minor improvements in creature card window -* splitting stacks with the shift+click -* creature card window contains info about modified speed - -### ADVENTURE INTERFACE: -* added water animation -* speed of scrolling map and hero movement can be adjusted in the System Options Window -* partial handling r-clicks on adventure map - -### TOWN INTERFACE: -* the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar -* fixed cloning creatures bug in garrisons (and related issues) - -### BATTLES: -* support for the Wait command -* magic arrow *really* works -* war machines support partially added -* queue of stacks narrowed -* spell effect animation displaying improvements -* positive/negative spells cannot be cast on hostile/our stacks -* showing spell effects affecting stack in creature info window -* more appropriate coloring of stack amount box when stack is affected by a spell -* battle console displays notifications about wait/defend commands -* several reported bugs fixed -* new spells supported: -* * Haste -* * lightning bolt -* * ice bolt -* * slow -* * implosion -* * forgetfulness -* * shield -* * air shield -* * bless -* * curse -* * bloodlust -* * weakness -* * stone skin -* * prayer -* * frenzy - -### AI PLAYER: -* Genius AI (first VCMI AI) will control computer creatures during the combat. - -### OBJECTS: -* Guardians property for resources is handled -* support for Witch Hut -* support for Arena -* support for Library of Enlightenment - -And a lot of minor fixes - -# 0.63 -> 0.64 (Nov 01 2008) - -### GENERAL: -* sprites from /Sprites folder are handled correctly -* several fixes for pathfinder and path arrows -* better handling disposed/predefined heroes -* heroes regain 1 mana point each turn -* support for mistycisim and intelligence skills -* hero hiring possible -* added support for a number of hotkeys -* it's not possible anymore to leave hero level-up window without selecting secondary skill -* many minor improvements - -* Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents: -* * woggaladriel -> vcmiainur -* * wogoliphaunt -> vcminoldor -* * wogshadowfax -> vcminahar -* * wogeyeofsauron -> vcmieagles -* * wogisengard -> vcmiformenos -* * wogsaruman -> vcmiistari -* * wogpathofthedead -> vcmiangband -* * woggandalfwhite -> vcmiglorfindel - -### ADVENTURE INTERFACE: -* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one -* slowed map scrolling -* blocked scrolling adventure map with mouse when left ctrl is pressed -* blocked map scrolling when dialog window is opened -* scholar will be accessible from the top - -### TOWN INTERFACE: -* partially done tavern window (only hero hiring functionality) - -### BATTLES: -* water elemental will really be treated as 2 hex creature -* potential infinite loop in reverseCreature removed -* better handling of battle cursor -* fixed blocked shooter behavior -* it's possible in battles to check remeaining HP of neutral stacks -* partial support for Magic Arrow spell -* fixed bug with dying unit -* stack queue hotkey is now 'Q' -* added shots limit - -# 0.62 -> 0.63 (Oct 01 2008) - -### GENERAL: -* coloured console output, logging all info to txt files -* it's possible to use other port than 3030 by passing it as an additional argument -* removed some redundant warnings -* partially done spellbook -* Alt+F4 quits the game -* some crashbugs was fixed -* added handling of navigation, logistics, pathfinding, scouting end estates secondary skill -* magical hero are given spellbook at the beginning -* added initial secondary skills for heroes - -### BATTLES: -* very significant optimization of battles -* battle summary window -* fixed crashbug occurring sometimes on exiting battle -* confirm window is shown before retreat -* graphic stack queue in battle (shows when 'c' key is pressed) -* it's possible to attack enemy hero -* neutral monster army disappears when defeated -* casualties among hero army and neutral creatures are saved -* better animation handling in battles -* directional attack in battles -* mostly done battle options (although they're not saved) -* added receiving exp (and leveling-up) after a won battle -* added support for archery, offence and armourer secondary abilities -* hero's primary skills accounted for damage dealt by creatures in battle - -### TOWNS: -* mostly done marketplace -* fixed crashbug with battles on swamps and rough terrain -* counterattacks -* heroes can learn new spells in towns -* working resource silo -* fixed bug with the mage guild when no spells available -* it's possible to build lighthouse - -### HERO WINDOW: -* setting army formation -* tooltips for artifacts in backpack - -### ADVENTURE INTERFACE: -* fixed bug with disappearing head of a hero in adventure map -* some objects are no longer accessible from the top -* no tooltips for objects under FoW -* events won't be shown -* working Subterranean Gates, Monoliths -* minimap shows all flaggable objects (towns, mines, etc.) -* artifacts we pick up go to the appropriate slot (if free) - -# 0.61 -> 0.62 (Sep 01 2008) - -### GENERAL: -* restructured to the server-client model -* support for heroes placed in towns -* upgrading creatures -* working gaining levels for heroes (including dialog with skill selection) -* added graphical cursor -* showing creature amount in the creature info window -* giving starting bonus - -### CASTLES: -* icon in infobox showing that there is hero in town garrison -* fort/citadel/castle screen -* taking last stack from the heroes army should be impossible (or at least harder) -* fixed reading forbidden structures -* randomizing spells in towns -* viewing hero window in the town screen -* possibility of moving hero into the garrison -* mage guild screen -* support for blacksmith -* if hero doesn't have a spell book, he can buy one in a mage guild -* it's possible to build glyph of fear in fortress -* creatures placeholders work properly - -### ADVENTURE INTERFACE: -* hopefully fixed problems with wrong town defs (village/fort/capitol) - -### HERO WINDOW: -* bugfix: splitting stacks works in hero window -* removed bug causing significant increase of CPU consumption - -### BATTLES: -* shooting -* removed some displaying problems -* showing last group of frames in creature animation won't crash -* added start moving and end moving animations -* fixed moving two-hex creatures -* showing/hiding graphic cursor -* a part of using graphic cursor -* slightly optimized showing of battle interface -* animation of getting hit / death by shooting is displayed when it should be -* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. -* minor optimizations - -### PREGAME: -* updates settings when selecting new map after changing sorting criteria -* if sorting not by name, name will be used as a secondary criteria -* when filter is applied a first available map is selected automatically -* slider position updated after sorting in pregame - -### OBJECTS: -* support for the Tree of knowledge -* support for Campfires -* added event message when picking artifact - -# 0.6 -> 0.61 (Jun 15 2008) - -### IMPROVEMENTS: -* improved attacking in the battles -* it's possible to kill hostile stack -* animations won't go in the same phase -* Better pathfinder -* "%s" substitutions in Right-click information in town hall -* windmill won't give wood -* hover text for heroes -* support for ZSoft-style PCX files in /Data -* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved -* in the townlist in castle selected town will by placed on the 2nd place (not 3rd) -* stack at the limit of unit's range can now be attacked -* range of unit is now properly displayed -* battle log is scrolled down when new event occurs -* console is closed when application exits - -### BUGFIXES: -* stack at the limit of unit's range can now be attacked -* good background for the town hall screen in Stronghold -* fixed typo in hall.txt -* VCMI won't crash when r-click neutral stack during the battle -* water won't blink behind shipyard in the Castle -* fixed several memory leaks -* properly displaying two-hex creatures in recruit/split/info window -* corrupted map file won't cause crash on initializing main menu - -# 0.59 -> 0.6 (Jun 1 2008) - -* partially done attacking in battles -* screen isn't now refreshed while blitting creature info window -* r-click creature info windows in battles -* no more division by 0 in slider -* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working) -* fixed estate problems -* fixed blinking mana vortex -* grail increases creature growths -* new pathfinder -* several minor improvements - -# 0.58 -> 0.59 (May 24 2008 - closed, test release) - -* fixed memory leak in battles -* blitting creature animations to rects in the recruitment window -* fixed wrong creatures def names -* better battle pathfinder and unit reversing -* improved slider ( #58 ) -* fixed problems with horde buildings (won't block original dwellings) -* giving primary skill when hero get level (but there is still no dialog) -* if an upgraded creature is available it'll be shown as the first in a recruitment window -* creature levels not messed in Fortress -* war machines are added to the hero's inventory, not to the garrison -* support for H3-style PCX graphics in Data/ -* VCMI won't crash when is unable to initialize audio system -* fixed displaying wrong town defs -* improvements in recruitment window (slider won't allow to select more creatures than we can afford) -* creature info window (only r-click) -* callback for buttons/lists based on boost::function -* a lot of minor improvements - -# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) - -### TOWNS: -* recruiting creatures -* working creature growths (including castle and horde building influences) -* towns give income -* town hall screen -* building buildings (requirements and cost are handled) -* hints for structures -* updating town infobox - -### GARRISONS: -* merging stacks -* splitting stacks - -### BATTLES: -* starting battles -* displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks -* leaving battle by pressing flee button -* moving units in battles and displaying their ranges -* defend command for units - -### GENERAL: -* a number of minor fixes and improvements - -# 0.54 -> 0.55 (Feb 29 2008) - -* Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental) -* randomization quantity of creatures on the map -* fix of Pandora's Box handling -* reading disposed/predefined heroes -* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder. -* more detailed logs -* reported problems with hero flags resolved -* heroes cannot occupy the same tile -* hints for most of creature generators -* some minor stuff - -# 0.53b -> 0.54 (Feb 23 2008 - first public release) -* given hero is placed in the town entrance -* some objects such as river delta won't be blitted "on" hero -* tiles under FoW are inaccessible -* giving random hero on RoE maps -* improved protection against hero duplication -* fixed starting values of primary abilities of random heroes on RoE/AB maps -* right click popups with infoboxes for heroes/towns lists -* new interface coloring (many thanks to GrayFace ;]) -* fixed bug in object flag's coloring -* added hints in town lists -* eliminated square from city hints - -# 0.53 - 0.53b (Feb 20 2008) - -* added giving default buildings in towns -* town infobox won't crash on empty town - -# 0.52 - 0.53 (Feb 18 2008): - -* hopefully the last bugfix of Pandora's Box -* fixed blockmaps of generated heroes -* disposed hero cannot be chosen in scenario settings (unless he is in prison) -* fixed town randomization -* fixed hero randomization -* fixed displaying heroes in preGame -* fixed selecting/deselecting artifact slots in hero window -* much faster pathfinder -* memory usage and load time significantly decreased -* it's impossible to select empty artifact slot in hero window -* fixed problem with FoW displaying on minimap on L-sized maps -* fixed crashbug in hero list connected with heroes dismissing -* mostly done town infobox -* town daily income is properly calculated - -# 0.51 - 0.52 (Feb 7 2008): - -* [feature] giving starting hero -* [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod -* [feature] picked artifacts are added to hero's backpack -* [feature] possibility of choosing player to play -* [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english -* [bugfix] fixed crashbug in reading defs with negativ left/right margins -* [bugfix] improved randomization -* [bugfix] pathfinder can't be cheated (what caused errors) - -# 0.5 - 0.51 (Feb 3 2008): - -* close button properly closes (same does 'q' key) -* two players can't have selected same hero -* double click on "Show Available Scenarios" won't reset options -* fixed possible crashbug in town/hero lists -* fixed crashbug in initializing game caused by wrong prisons handling -* fixed crashbug on reading hero's custom artifacts in RoE maps -* fixed crashbug on reading custom Pandora's Box in RoE maps -* fixed crashbug on reading blank Quest Guards -* better console messages -* map reading speed up (though it's still slow, especially on bigger maps) - -# 0.0 -> 0.5 (Feb 2 2008 - first closed release): - -* Main menu and New game screens -* Scenario selection, part of advanced options support -* Partially done adventure map, town and hero interfaces -* Moving hero -* Interactions with several objects (mines, resources, mills, and others) +# 1.3.1 -> 1.3.2 + +### GENERAL +* VCMI now uses new application icon +* Added initial version of Czech translation +* Game will now use tile hero is moving from for movement cost calculations, in line with H3 +* Added option to open hero backpack window in hero screen +* Added detection of misclicks for touch inputs to make hitting small UI elements easier +* Hero commander will now gain option to learn perks on reaching master level in corresponding abilities +* It is no longer possible to stop movement while moving over water with Water Walk +* Game will now automatically update hero path if it was blocked by another hero +* Added "vcmiartifacts angelWings" form to "give artifacts" cheat + +### STABILITY +* Fixed freeze in Launcher on repository checkout and on mod install +* Fixed crash on loading VCMI map with placed Abandoned Mine +* Fixed crash on loading VCMI map with neutral towns +* Fixed crash on attempting to visit unknown object, such as Market of Time +* Fixed crash on attempting to teleport unit that is immune to a spell +* Fixed crash on switching fullscreen mode during AI turn + +### CAMPAIGNS +* Fixed reorderging of hero primary skills after moving to next scenario in campaigns + +### BATTLES +* Conquering a town will now correctly award additional 500 experience points +* Quick combat is now enabled by default +* Fixed invisible creatures from SUMMON_GUARDIANS and TRANSMUTATION bonuses +* Added option to toggle spell usage by AI in quick combat +* Fixed updating of spell point of enemy hero in game interface after spell cast +* Fixed wrong creature spellcasting shortcut (now set to "F") +* It is now possible to perform melee attack by creatures with spells, especially area spells +* Right-click will now properly end spellcast mode +* Fixed cursor preview when casting spell using touchscreen +* Long tap during spell casting will now properly abort the spell + +### INTERFACE +* Added "Fill all empty slots with 1 creature" option to radial wheel in garrison windows +* Context popup for adventure map monsters will now show creature icon +* Game will now show correct victory message for gather troops victory condition +* Fixed incorrect display of number of owned Sawmills in Kingdom Overview window +* Fixed incorrect color of resource bar in hotseat mode +* Fixed broken toggle map level button in world view mode +* Fixed corrupted interface after opening puzzle window from world view mode +* Fixed blocked interface after attempt to start invalid map +* Add yellow border to selected commander grandmaster ability +* Always use bonus description for commander abilities instead of not provided wog-specific translation +* Fix scrolling when commander has large number of grandmaster abilities +* Fixed corrupted message on another player defeat +* Fixed unavailable Quest Log button on maps with quests +* Fixed incorrect values on a difficulty selector in save load screen +* Removed invalid error message on attempting to move non-existing unit in exchange window + +### RANDOM MAP GENERATOR +* Fixed bug leading to unreachable resources around mines + +### MAP EDITOR +* Fixed crash on maps containing abandoned mines +* Fixed crash on maps containing neutral objects +* Fixed problem with random map initialized in map editor +* Fixed problem with initialization of random dwellings + +# 1.3.0 -> 1.3.1 + +### GENERAL: +* Fixed framerate drops on hero movement with active hota mod +* Fade-out animations will now be skipped when instant hero movement speed is used +* Restarting loaded campaing scenario will now correctly reapply starting bonus +* Reverted FPS limit on mobile systems back to 60 fps +* Fixed loading of translations for maps and campaigns +* Fixed loading of preconfigured starting army for heroes with preconfigured spells +* Background battlefield obstacles will now appear below creatures +* it is now possible to load save game located inside mod +* Added option to configure reserved screen area in Launcher on iOS +* Fixed border scrolling when game window is maximized + +### AI PLAYER: +* BattleAI: Improved performance of AI spell selection +* NKAI: Fixed freeze on attempt to exchange army between garrisoned and visiting hero +* NKAI: Fixed town threat calculation +* NKAI: Fixed recruitment of new heroes +* VCAI: Added workaround to avoid freeze on attempting to reach unreachable location +* VCAI: Fixed spellcasting by Archangels + +### RANDOM MAP GENERATOR: +* Fixed placement of roads inside rock in underground +* Fixed placement of shifted creature animations from HotA +* Fixed placement of treasures at the boundary of wide connections +* Added more potential locations for quest artifacts in zone + +### STABILITY: +* When starting client without H3 data game will now show message instead of silently crashing +* When starting invalid map in campaign, game will now show message instead of silently crashing +* Blocked loading of saves made with different set of mods to prevent crashes +* Fixed crash on starting game with outdated mods +* Fixed crash on attempt to sacrifice all your artifacts in Altar of Sacrifice +* Fixed crash on leveling up after winning battle as defender +* Fixed possible crash on end of battle opening sound +* Fixed crash on accepting battle result after winning battle as defender +* Fixed possible crash on casting spell in battle by AI +* Fixed multiple possible crashes on managing mods on Android +* Fixed multiple possible crashes on importing data on Android +* Fixed crash on refusing rewards from town building +* Fixed possible crash on threat evaluation by NKAI +* Fixed crash on using haptic feedback on some Android systems +* Fixed crash on right-clicking flags area in RMG setup mode +* Fixed crash on opening Blacksmith window and Build Structure dialogs in some localizations +* Fixed possible crash on displaying animated main menu +* Fixed crash on recruiting hero in town located on the border of map + +# 1.2.1 -> 1.3.0 + +### GENERAL: +* Implemented automatic interface scaling to any resolution supported by monitor +* Implemented UI scaling option to scale game interface +* Game resolution and UI scaling can now be changed without game restart +* Fixed multiple issues with borderless fullscreen mode +* On mobile systems game will now always run at native resolution with configurable UI scaling +* Implemented support for Horn of the Abyss map format +* Implemented option to replay results of quick combat +* Added translations to French and Chinese +* All in-game cheats are now case-insensitive +* Added high-definition icon for Windows +* Fix crash on connecting to server on FreeBSD and Flatpak builds +* Save games now consist of a single file +* Added H3:SOD cheat codes as alternative to vcmi cheats +* Fixed several possible crashes caused by autocombat activation +* Fixed artifact lock icon in localized versions of the game +* Fixed possible crash on changing hardware cursor + +### TOUCHSCREEN SUPPORT: +* VCMI will now properly recognizes touch screen input +* Implemented long tap gesture that shows popup window. Tap once more to close popup +* Long tap gesture duration can now be configured in settings +* Implemented radial menu for army management, activated via swiping creature icon +* Implemented swipe gesture for scrolling through lists +* All windows that have sliders in UI can now be scrolled using swipe gesture +* Implemented swipe gesture for attack direction selection: swipe from enemy position to position you want to attack from +* Implemented pinch gesture for zooming adventure map +* Implemented haptic feedback (vibration) for long press gesture + +### LAUNCHER: +* Launcher will now attempt to automatically detect language of OS on first launch +* Added "About" tab with information about project and environment +* Added separate options for Allied AI and Enemy AI for adventure map +* Patially fixed displaying of download progress for mods +* Fixed potential crash on opening mod information for mods with a changelog +* Added option to configure number of autosaves + +### MAP EDITOR: +* Fixed crash on cutting random town +* Added option to export entire map as an image +* Added validation for placing multiple heroes into starting town +* It is now possible to have single player on a map +* It is now possible to configure teams in editor + +### AI PLAYER: +* Fixed potential crash on accessing market (VCAI) +* Fixed potentially infinite turns (VCAI) +* Reworked object prioritizing +* Improved town defense against enemy heroes +* Improved town building (mage guild and horde) +* Various behavior fixes + +### GAME MECHANICS +* Hero retreating after end of 7th turn will now correctly appear in tavern +* Implemented hero backpack limit (disabled by default) +* Fixed Admiral's Hat movement points calculation +* It is now possible to access Shipwrecks from coast +* Hero path will now be correctly updated on equipping/unequipping Levitation Boots or Angel Wings +* It is no longer possible to abort movement while hero is flying over water +* Fixed digging for Grail +* Implemented "Survive beyond a time limit" victory condition +* Implemented "Defeat all monsters" victory condition +* 100% damage resistance or damage reduction will make unit immune to a spell +* Game will now randomly select obligatory skill for hero on levelup instead of always picking Fire Magic +* Fixed duration of bonuses from visitable object such as Idol of Fortune +* Rescued hero from prison will now correctly reveal map around him +* Lighthouses will no longer give movement bonus on land + +### CAMPAIGNS: +* Fixed transfer of artifacts into next scenario +* Fixed crash on advancing to next scenario with heroes from mods +* Fixed handling of "Start with building" campaign bonus +* Fixed incorrect starting level of heroes in campaigns +* Game will now play correct music track on scenario selection window +* Dracon woll now correctly start without spellbook in Dragon Slayer campaign +* Fixed frequent crash on moving to next scenario during campaign +* Fixed inability to dismiss heroes on maps with "capture town" victory condition + +### RANDOM MAP GENERATOR: +* Improved zone placement, shape and connections +* Improved zone passability for better gameplay +* Improved treasure distribution and treasure values to match SoD closely +* Navigation and water-specific spells are now banned on maps without water +* RMG will now respect road settings set in menu +* Tweaked many original templates so they allow new terrains and factions +* Added "bannedTowns", "bannedTerrains", "bannedMonsters" zone properties +* Added "road" property to connections +* Added monster strength "none" +* Support for "wide" connections +* Support for new "fictive" and "repulsive" connections +* RMG will now run faster, utilizing many CPU cores +* Removed random seed number from random map description + +### INTERFACE: +* Adventure map is now scalable and can be used with any resolution without mods +* Adventure map interface is now correctly blocked during enemy turn +* Visiting creature banks will now show amount of guards in bank +* It is now possible to arrange army using status window +* It is now possible to zoom in or out using mouse wheel or pinch gesture +* It is now possible to reset zoom via Backspace hotkey +* Receiving a message in chat will now play sound +* Map grid will now correctly display on map start +* Fixed multiple issues with incorrect updates of save/load game screen +* Fixed missing fortifications level icon in town tooltip +* Fixed positioning of resource label in Blacksmith window +* Status bar on inactive windows will no longer show any tooltip from active window +* Fixed highlighting of possible artifact placements when exchanging with allied hero +* Implemented sound of flying movement (for Fly spell or Angel Wings) +* Last symbol of entered cheat/chat message will no longer trigger hotkey +* Right-clicking map name in scenario selection will now show file name +* Right-clicking save game in save/load screen will now show file name and creation date +* Right-clicking in town fort window will now show creature information popup +* Implemented pasting from clipboard (Ctrl+V) for text input + +### BATTLES: +* Implemented Tower moat (Land Mines) +* Implemented defence reduction for units in moat +* Added option to always show hero status window +* Battle opening sound can now be skipped with mouse click +* Fixed movement through moat of double-hexed units +* Fixed removal of Land Mines and Fire Walls +* Obstacles will now corectly show up either below or above unit +* It is now possible to teleport a unit through destroyed walls +* Added distinct overlay image for showing movement range of highlighted unit +* Added overlay for displaying shooting range penalties of units + +### MODDING: +* Implemented initial version of VCMI campaign format +* Implemented spell cast as possible reward for configurable object +* Implemented support for configurable buildings in towns +* Implemented support for placing prison, tavern and heroes on water +* Implemented support for new boat types +* It is now possible for boats to use other movement layers, such as "air" +* It is now possible to use growing artifacts on artifacts that can be used by hero +* It is now possible to configure town moat +* Palette-cycling animation of terrains and rivers can now be configured in json +* Game will now correctly resolve identifier in unexpected form (e.g. 'bless' vs 'spell.bless' vs 'core:bless') +* Creature specialties that use short form ( "creature" : "pikeman" ) will now correctly affect all creature upgrades +* It is now possible to configure spells for Shrines +* It is now possible to configure upgrade costs per level for Hill Forts +* It is now possible to configure boat type for Shipyards on adventure map and in town +* Implemented support for HotA-style adventure map images for monsters, with offset +* Replaced (SCHOOL)_SPELL_DMG_PREMY with SPELL_DAMAGE bonus (uses school as subtype). +* Removed bonuses (SCHOOL)_SPELLS - replaced with SPELLS_OF_SCHOOL +* Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance +* MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses +* Configurable objects can now be translated +* Fixed loading of custom battlefield identifiers for map objects + +# 1.2.0 -> 1.2.1 + +### GENERAL: +* Implemented spell range overlay for Dimension Door and Scuttle Boat +* Fixed movement cost penalty from terrain +* Fixed empty Black Market on game start +* Fixed bad morale happening after waiting +* Fixed good morale happening after defeating last enemy unit +* Fixed death animation of Efreeti killed by petrification attack +* Fixed crash on leaving to main menu from battle in hotseat mode +* Fixed music playback on switching between towns +* Special months (double growth and plague) will now appear correctly +* Adventure map spells are no longer visible on units in battle +* Attempt to cast spell with no valid targets in hotseat will show appropriate error message +* RMG settings will now show all existing in game templates and not just those suitable for current settings +* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked +* Fixed centering of scenario information window +* Fixed crash on empty save game list after filtering +* Fixed blocked progress in Launcher on language detection failure +* Launcher will now correctly handle selection of Ddata directory in H3 install +* Map editor will now correctly save message property for events and pandoras +* Fixed incorrect saving of heroes portraits in editor + +# 1.1.1 -> 1.2.0 + +### GENERAL: +* Adventure map rendering was entirely rewritten with better, more functional code +* Client battle code was heavily reworked, leading to better visual look & feel and fixing multiple minor battle bugs / glitches +* Client mechanics are now framerate-independent, rather than speeding up with higher framerate +* Implemented hardware cursor support +* Heroes III language can now be detected automatically +* Increased targeted framerate from 48 to 60 +* Increased performance of UI updates +* Fixed bonus values of heroes who specialize in secondary skills +* Fixed bonus values of heroes who specialize in creatures +* Fixed damage increase from Adela's Bless specialty +* Fixed missing obstacles in battles on subterranean terrain +* Video files now play at correct speed +* Fixed crash on switching to second mission in campaigns +* New cheat code: vcmiazure - give 5000 azure dragons in every empty slot +* New cheat code: vcmifaerie - give 5000 faerie dragons in every empty slot +* New cheat code: vcmiarmy or vcminissi - give specified creatures in every empty slot. EG: vcmiarmy imp +* New cheat code: vcmiexp or vcmiolorin - give specified amount of experience to current hero. EG: vcmiexp 10000 +* Fixed oversided message window from Scholar skill that had confirmation button outside game window +* Fixed loading of prebuilt creature hordes from h3m maps +* Fixed volume of ambient sounds when changing game sounds volume +* Fixed might&magic affinities of Dungeon heroes +* Fixed Roland's specialty to affect Swordsmen/Crusaders instead of Griffins +* Buying boat in town of an ally now correctly uses own resources instead of stealing them from ally +* Default game difficulty is now set to "normal" instead of "easy" +* Fixed crash on missing music files + +### MAP EDITOR: +* Added translations to German, Polish, Russian, Spanish, Ukrainian +* Implemented cut/copy/paste operations +* Implemented lasso brush for terrain editing +* Toolbar actions now have names +* Added basic victory and lose conditions + +### LAUNCHER: +* Added initial Welcome/Setup screen for new players +* Added option to install translation mod if such mod exists and player's H3 version has different language +* Icons now have higher resolution, to prevent upscaling artifacts +* Added translations to German, Polish, Russian, Spanish, Ukrainian +* Mods tab layout has been adjusted based on feedback from players +* Settings tab layout has been redesigned to support longer texts +* Added button to start map editor directly from Launcher +* Simplified game starting flow from online lobby +* Mod description will now show list of languages supported by mod +* Launcher now uses separate mod repository from vcmi-1.1 version to prevent mod updates to unsupported versions +* Size of mod list and mod details sub-windows can now be adjusted by player + +### AI PLAYER: +* Nullkiller AI is now used by default +* AI should now be more active in destroying heroes causing treat on AI towns +* AI now has higher priority for resource-producing mines +* Increased AI priority of town dwelling upgrades +* AI will now de-prioritize town hall upgrades when low on resources +* Messages from cheats used by AI are now hidden +* Improved army gathering from towns +* AI will now attempt to exchange armies between main heroes to get the strongest hero with the strongest army. +* Improved Pandora handling +* AI takes into account fort level now when evaluating enemy town capturing priority. +* AI can not use allied shipyard now to avoid freeze +* AI will avoid attacking creatures standing on draw-bridge tile during siege if the bridge is closed. +* AI will consider retreat during siege if it can not do anything (catapult is destroyed, no destroyed walls exist) + +### RANDOM MAP GENERATOR +* Random map generator can now be used without vcmi-extras mod +* RMG will no longer place shipyards or boats at very small lakes +* Fixed placement of shipyards in invalid locations +* Fixed potential game hang on generation of random map +* RMG will now generate addditional monolith pairs to create required number of zone connections +* RMG will try to place Subterranean Gates as far away from other objects (including each other) as possible +* RMG will now try to place objects as far as possible in both zones sharing a guard, not only the first one. +* Use only one template for an object in zone +* Objects with limited per-map count will be distributed evenly among zones with suitable terrain +* Objects above zone treasure value will not be considered for placement +* RMG will prefer terrain-specific templates for objects placement +* RMG will place Towns and Monoliths first in order to generate long roads across the zone. +* Adjust the position of center town in the zone for better look & feel on S maps. +* Description of random map will correctly show number of levels +* Fixed amount of creatures found in Pandora Boxes to match H3 +* Visitable objects will no longer be placed on top of the map, obscured by map border + +### ADVENTURE MAP: +* Added option to replace popup messages on object visiting with messages in status window +* Implemented different hero movement sounds for offroad movement +* Cartographers now reveal terrain in the same way as in H3 +* Status bar will now show movement points information on pressing ALT or after enabling option in settings +* It is now not possible to receive rewards from School of War without required gold amount +* Owned objects, like Mines and Dwellings will always show their owner in status bar +* It is now possible to interact with on-map Shipyard when no hero is selected +* Added option to show amount of creatures as numeric range rather than adjective +* Added option to show map grid +* Map swipe is no longer exclusive for phones and can be enabled on desktop platforms +* Added more graduated settigns for hero movement speed +* Map scrolling is now more graduated and scrolls with pixel-level precision +* Hero movement speed now matches H3 +* Improved performance of adventure map rendering +* Fixed embarking and disembarking sounds +* Fixed selection of "new week" animation for status window +* Object render order now mostly matches H3 +* Fixed movement cost calculation when using "Fly" spell or "Angel Wings" +* Fixed game freeze on using Town Portal to teleport into town with unvisited Battle Scholar Academy +* Fixed invalid ambient sound of Whirlpool +* Hero path will now be correctly removed on defeating monsters that are at the end of hero path +* Seer Hut tooltips will now show messages for correct quest type + +### INTERFACE +* Implemented new settings window +* Added framerate display option +* Fixed white status bar on server connection screen +* Buttons in battle window now correctly show tooltip in status bar +* Fixed cursor image during enemy turn in combat +* Game will no longer promt to assemble artifacts if they fall into backpack +* It is now possible to use in-game console for vcmi commands +* Stacks sized 1000-9999 units will not be displayed as "1k" +* It is now possible to select destination town for Town Portal via double-click +* Implemented extended options for random map tab: generate G+U size, select RMG template, manage teams and roads + +### HERO SCREEN +* Fixed cases of incorrect artifact slot highlighting +* Improved performance of artifact exchange operation +* Picking up composite artifact will immediately unlock slots +* It is now possible to swap two composite artifacts + +### TOWN SCREEN +* Fixed gradual fade-in of a newly built building +* Fixed duration of building fade-in to match H3 +* Fixed rendering of Shipyard in Castle +* Blacksmith purchase button is now properly locked if artifact slot is occupied by another warmachine +* Added option to show number of available creatures in place of growth +* Fixed possible interaction with hero / town list from adventure map while in town screen +* Fixed missing left-click message popup for some town buildings +* Moving hero from garrison by pressing space will now correctly show message "Cannot have more than 8 adventuring heroes" + +### BATTLES: +* Added settings for even faster animation speed than in H3 +* Added display of potential kills numbers into attack tooltip in status bar +* Added option to skip battle opening music entirely +* All effects will now wait for battle opening sound before playing +* Hex highlighting will now be disabled during enemy turn +* Fixed incorrect log message when casting spell that kills zero units +* Implemented animated cursor for spellcasting +* Fixed multiple issues related to ordering of creature animations +* Fixed missing flags from hero animations when opening menus +* Fixed rendering order of moat and grid shadow +* Jousting bonus from Champions will now be correctly accounted for in damage estimation +* Building Castle building will now provide walls with additional health point +* Speed of all battle animations should now match H3 +* Fixed missing obstacles on subterranean terrain +* Ballistics mechanics now matches H3 logic +* Arrow Tower base damage should now match H3 +* Destruction of wall segments will now remove ranged attack penalty +* Force Field cast in front of drawbridge will now block it as in H3 +* Fixed computations for Behemoth defense reduction ability +* Bad luck (if enabled) will now multiple all damage by 50%, in line with other damage reducing mechanics +* Fixed highlighting of movement range for creatures standing on a corpse +* All battle animations now have same duration/speed as in H3 +* Added missing combat log message on resurrecting creatures +* Fixed visibility of blue border around targeted creature when spellcaster is making turn +* Fixed selection highlight when in targeted creature spellcasting mode +* Hovering over hero now correctly shows hero cursor +* Creature currently making turn is now highlighted in the Battle Queue +* Hovering over creature icon in Battle Queue will highlight this creature in the battlefield +* New battle UI extension allows control over creatures' special abilities +* Fixed crash on activating auto-combat in battle +* Fixed visibility of unit creature amount labels and timing of their updates +* Firewall will no longer hit double-wide units twice when passing through +* Unicorn Magic Damper Aura ability now works multiplicatively with Resistance +* Orb of Vulnerability will now negate Resistance skill + +### SPELLS: +* Hero casting animation will play before spell effect +* Fire Shield: added sound effect +* Fire Shield: effect now correctly plays on defending creature +* Earthquake: added sound effect +* Earthquake: spell will not select sections that were already destroyed before cast +* Remove Obstacles: fixed error message when casting on maps without obstacles +* All area-effect spells (e.g. Fireball) will play their effect animation on top +* Summoning spells: added fade-in effect for summoned creatures +* Fixed timing of hit animation for damage-dealing spells +* Obstacle-creating spells: UI is now locked during effect animation +* Obstacle-creating spells: added sound effect +* Added reverse death animation for spells that bring stack back to life +* Bloodlust: implemented visual effect +* Teleport: implemented visual fade-out and fade-in effect for teleporting +* Berserk: Fixed duration of effect +* Frost Ring: Fixed spell effect range +* Fixed several cases where multiple different effects could play at the same time +* All spells that can affecte multiple targets will now highlight affected stacks +* Bless and Curse now provide +1 or -1 to base damage on Advanced & Expert levels + +### ABILITIES: +* Rebirth (Phoenix): Sound will now play in the same time as animation effect +* Master Genie spellcasting: Sound will now play in the same time as animation effect +* Power Lich, Magogs: Sound will now play in the same time as attack animation effect +* Dragon Breath attack now correctly uses different attack animation if multiple targets are hit +* Petrification: implemented visual effect +* Paralyze: added visual effect +* Blind: Stacks will no longer retailate on attack that blinds them +* Demon Summon: Added animation effect for summoning +* Fire shield will no longer trigger on non-adjacent attacks, e.g. from Dragon Breath +* Weakness now has correct visual effect +* Added damage bonus for opposite elements for Elementals +* Added damage reduction for Magic Elemental attacks against creatures immune to magic +* Added incoming damage reduction to Petrify +* Added counter-attack damage reduction for Paralyze + +### MODDING: +* All configurable objects from H3 now have their configuration in json +* Improvements to functionality of configurable objects +* Replaced `SECONDARY_SKILL_PREMY` bonus with separate bonuses for each skill. +* Removed multiple bonuses that can be replaced with another bonus. +* It is now possible to define new hero movement sounds in terrains +* Implemented translation support for mods +* Implemented translation support for .h3m maps and .h3c campaigns +* Translation mods are now automatically disabled if player uses different language +* Files with new Terrains, Roads and Rivers are now validated by game +* Parameters controlling effect of attack and defences stats on damage are now configurable in defaultMods.json +* New bonus: `LIMITED_SHOOTING_RANGE`. Creatures with this bonus can only use ranged attack within specified range +* Battle window and Random Map Tab now have their layout defined in json file +* Implemented code support for alternative actions mod +* Implemented code support for improved random map dialog +* It is now possible to configure number of creature stacks in heroes' starting armies +* It is now possible to configure number of constructed dwellings in towns on map start +* Game settings previously located in defaultMods.json are now loaded directly from mod.json +* It is now possible for spellcaster units to have multiple spells (but only for targeting different units) +* Fixed incorrect resolving of identifiers in commander abilities and stack experience definitions + +# 1.1.0 -> 1.1.1 + +### GENERAL: +* Fixed missing sound in Polish version from gog.com +* Fixed positioning of main menu buttons in localized versions of H3 +* Fixed crash on transferring artifact to commander +* Fixed game freeze on receiving multiple artifact assembly dialogs after combat +* Fixed potential game freeze on end of music playback +* macOS/iOS: fixed sound glitches +* Android: upgraded version of SDL library +* Android: reworked right click gesture and relative pointer mode +* Improved map loading speed +* Ubuntu PPA: game will no longer crash on assertion failure + +### ADVENTURE MAP: +* Fixed hero movement lag in single-player games +* Fixed number of drowned troops on visiting Sirens to match H3 +* iOS: pinch gesture visits current object (Spacebar behavior) instead of activating in-game console + +### TOWNS: +* Fixed displaying growth bonus from Statue of Legion +* Growth bonus tooltip ordering now matches H3 +* Buy All Units dialog will now buy units starting from the highest level + +### LAUNCHER: +* Local mods can be disabled or uninstalled +* Fixed styling of Launcher interface + +### MAP EDITOR: +* Fixed saving of roads and rivers +* Fixed placement of heroes on map + +# 1.0.0 -> 1.1.0 + +### GENERAL: +* iOS is supported +* Mods and their versions and serialized into save files. Game checks mod compatibility before loading +* Logs are stored in system default logs directory +* LUA/ERM libs are not compiled by default +* FFMpeg dependency is optional now +* Conan package manager is supported for MacOS and iOS + +### MULTIPLAYER: +* Map is passed over network, so different platforms are compatible with each other +* Server self-killing is more robust +* Unlock in-game console while opponent's turn +* Host can control game session by using console commands +* Control over player is transferred to AI if client escaped the game +* Reconnection mode for crashed client processes +* Playing online is available using proxy server + +### ADVENTURE MAP: +* Fix for digging while opponent's turn +* Supported right click for quick recruit window +* Fixed problem with quests are requiring identical artefacts +* Bulk move and swap artifacts +* Pause & resume for towns and terrains music themes +* Feature to assemble/disassemble artefacts in backpack +* Clickable status bar to send messages +* Heroes no longer have chance to receive forbidden skill on leveling up +* Fixed visibility of newly recruited heroes near town +* Fixed missing artifact slot in Artifact Merchant window + +### BATTLES: +* Fix healing/regeneration behaviour and effect +* Fix crashes related to auto battle +* Implemented ray projectiles for shooters +* Introduced default tower shooter icons +* Towers destroyed during battle will no longer be listed as casualties + +### AI: +* BattleAI: Target prioritizing is now based on damage difference instead of health difference +* Nullkiller AI can retreat and surrender +* Nullkiller AI doesn't visit allied dwellings anymore +* Fixed a few freezes in Nullkiller AI + +### RANDOM MAP GENERATOR: +* Speedup generation of random maps +* Necromancy cannot be learned in Witch Hut on random maps + +### MODS: +* Supported rewardable objects customization +* Battleground obstacles are extendable now with VLC mechanism +* Introduced "compatibility" section into mods settings +* Fixed bonus system for custom advmap spells +* Supported customisable town entrance placement +* Fixed validation of mods with new adventure map objects + +### LAUNCHER: +* Fixed problem with duplicated mods in the list +* Launcher shows compatible mods only +* Uninstall button was moved to the left of layout +* Unsupported resolutions are not shown +* Lobby for online gameplay is implemented + +### MAP EDITOR: +* Basic version of Qt-based map editor + +# 0.99 -> 1.0.0 + +### GENERAL: +* Spectator mode was implemented through command-line options +* Some main menu settings get saved after returning to main menu - last selected map, save etc. +* Restart scenario button should work correctly now +* Skyship Grail works now immediately after capturing without battle +* Lodestar Grail implemented +* Fixed Gargoyles immunity +* New bonuses: +* * SOUL_STEAL - "WoG ghost" ability, should work somewhat same as in H3 +* * TRANSMUTATION - "WoG werewolf"-like ability +* * SUMMON_GUARDIANS - "WoG santa gremlin"-like ability + two-hex unit extension +* * CATAPULT_EXTRA_SHOTS - defines number of extra wall attacks for units that can do so +* * RANGED_RETALIATION - allows ranged counterattack +* * BLOCKS_RANGED_RETALIATION - disallow enemy ranged counterattack +* * SECONDARY_SKILL_VAL2 - set additional parameter for certain secondary skills +* * MANUAL_CONTROL - grant manual control over war machine +* * WIDE_BREATH - melee creature attacks affect many nearby hexes +* * FIRST_STRIKE - creature counterattacks before attack if possible +* * SYNERGY_TARGET - placeholder bonus for Mod Design Team (subject to removal in future) +* * SHOOTS_ALL_ADJACENT - makes creature shots affect all neighbouring hexes +* * BLOCK_MAGIC_BELOW - allows blocking spells below particular spell level. HotA cape artifact can be implemented with this +* * DESTRUCTION - creature ability for killing extra units after hit, configurable + +### MULTIPLAYER: +* Loading support. Save from single client could be used to load all clients. +* Restart support. All clients will restart together on same server. +* Hotseat mixed with network game. Multiple colors can be controlled by each client. + +### SPELLS: +* Implemented cumulative effects for spells + +### MODS: +* Improve support for WoG commander artifacts and skill descriptions +* Added support for modding of original secondary skills and creation of new ones. +* Map object sounds can now be configured via json +* Added bonus updaters for hero specialties +* Added allOf, anyOf and noneOf qualifiers for bonus limiters +* Added bonus limiters: alignment, faction and terrain +* Supported new terrains, new battlefields, custom water and rock terrains +* Following special buildings becomes available in the fan towns: +* * attackVisitingBonus +* * defenceVisitingBonus +* * spellPowerVisitingBonus +* * knowledgeVisitingBonus +* * experienceVisitingBonus +* * lighthouse +* * treasury + +### SOUND: +* Fixed many mising or wrong pickup and visit sounds for map objects +* All map objects now have ambient sounds identical to OH3 + +### RANDOM MAP GENERATOR: +* Random map generator supports water modes (normal, islands) +* Added config randomMap.json with settings for map generator +* Added parameter for template allowedWaterContent +* Extra resource packs appear nearby mines +* Underground can be player starting place for factions allowed to be placed underground +* Improved obstacles placement aesthetics +* Rivers are generated on the random maps +* RMG works more stable, various crashes have been fixed +* Treasures requiring guards are guaranteed to be protected + +### VCAI: +* Reworked goal decomposition engine, fixing many loopholes. AI will now pick correct goals faster. +* AI will now use universal pathfinding globally +* AI can use Summon Boat and Town Portal +* AI can gather and save resources on purpose +* AI will only buy army on demand instead of every turn +* AI can distinguish the value of all map objects +* General speed optimizations + +### BATTLES: +* Towers should block ranged retaliation +* AI can bypass broken wall with moat instead of standing and waiting until gate is destroyed +* Towers do not attack war machines automatically +* Draw is possible now as battle outcome in case the battle ends with only summoned creatures (both sides loose) + +### ADVENTURE MAP: +* Added buttons and keyboard shortcuts to quickly exchange army and artifacts between heroes +* Fix: Captured town should not be duplicated on the UI + +### LAUNCHER: +* Implemented notifications about updates +* Supported redirection links for downloading mods + +# 0.98 -> 0.99 + +### GENERAL: +* New Bonus NO_TERRAIN_PENALTY +* Nomads will remove Sand movement penalty from army +* Flying and water walking is now supported in pathfinder +* New artifacts supported +* * Angel Wings +* * Boots of Levitation +* Implemented rumors in tavern window +* New cheat codes: +* * vcmiglaurung - gives 5000 crystal dragons into each slot +* * vcmiungoliant - conceal fog of war for current player +* New console commands: +* * gosolo - AI take control over human players and vice versa +* * controlai - give control of one or all AIs to player +* * set hideSystemMessages on/off - supress server messages in chat + +### BATTLES: +* Drawbridge mechanics implemented (animation still missing) +* Merging of town and visiting hero armies on siege implemented +* Hero info tooltip for skills and mana implemented + +### ADVENTURE AI: +* Fixed AI trying to go through underground rock +* Fixed several cases causing AI wandering aimlessly +* AI can again pick best artifacts and exchange artifacts between heroes +* AI heroes with patrol enabled won't leave patrol area anymore + +### RANDOM MAP GENERATOR: +* Changed fractalization algorithm so it can create cycles +* Zones will not have straight paths anymore, they are totally random +* Generated zones will have different size depending on template setting +* Added Thieves Guild random object (1 per zone) +* Added Seer Huts with quests that match OH3 +* RMG will guarantee at least 100 pairs of Monoliths are available even if there are not enough different defs + +# 0.97 -> 0.98 + +### GENERAL: +* Pathfinder can now find way using Monoliths and Whirlpools (only used if hero has protection) + +### ADVENTURE AI: +* AI will try to use Monolith entrances for exploration +* AI will now always revisit each exit of two way monolith if exit no longer visible +* AI will eagerly pick guarded and blocked treasures + +### ADVENTURE MAP: +* Implemented world view +* Added graphical fading effects + +### SPELLS: +* New spells handled: +* * Earthquake +* * View Air +* * View Earth +* * Visions +* * Disguise +* Implemented CURE spell negative dispell effect +* Added LOCATION target for spells castable on any hex with new target modifiers + +### BATTLES: +* Implemented OH3 stack split / upgrade formulas according to AlexSpl + +### RANDOM MAP GENERATOR: +* Underground tunnels are working now +* Implemented "junction" zone type +* Improved zone placing algorithm +* More balanced distribution of treasure piles +* More obstacles within zones + +# 0.96 -> 0.97 (Nov 01 2014) + +### GENERAL: +* (windows) Moved VCMI data directory from '%userprofile%\vcmi' to '%userprofile%\Documents\My Games\vcmi' +* (windows) (OSX) Moved VCMI save directory from 'VCMI_DATA\Games' to 'VCMI_DATA\Saves' +* (linux) +* Changes in used librries: +* * VCMI can now be compiled with SDL2 +* * Movies will use ffmpeg library +* * change boost::bind to std::bind +* * removed boost::asign +* * Updated FuzzyLite to 5.0 +* Multiplayer load support was implemented through command-line options + +### ADVENTURE AI: +* Significantly optimized execution time, AI should be much faster now. + +### ADVENTURE MAP: +* Non-latin characters can now be entered in chat window or used for save names. +* Implemented separate speed for owned heroes and heroes owned by other players + +### GRAPHICS: +* Better upscaling when running in fullscreen mode. +* New creature/commader window +* New resolutions and bonus icons are now part of a separate mod +* Added graphics for GENERAL_DAMAGE_REDUCTION bonus (Kuririn) + +### RANDOM MAP GENERATOR: +* Random map generator now creates complete and playable maps, should match original RMG +* All important features from original map templates are implemented +* Fixed major crash on removing objects +* Undeground zones will look just like surface zones + +### LAUNCHER: +* Implemented switch to disable intro movies in game + +# 0.95 -> 0.96 (Jul 01 2014) + +### GENERAL: +* (linux) now VCMI follows XDG specifications. See http://forum.vcmi.eu/viewtopic.php?t=858 + +### ADVENTURE AI: +* Optimized speed and removed various bottlenecks. + +### ADVENTURE MAP: +* Heroes auto-level primary and secondary skill levels according to experience + +### BATTLES: +* Wall hit/miss sound will be played when using catapult during siege + +### SPELLS: +* New configuration format + +### RANDOM MAP GENERATOR: +* Towns from mods can be used +* Reading connections, terrains, towns and mines from template +* Zone placement +* Zone borders and connections, fractalized paths inside zones +* Guard generation +* Treasue piles generation (so far only few removable objects) + +### MODS: +* Support for submods - mod may have their own "submods" located in /Mods directory +* Mods may provide their own changelogs and screenshots that will be visible in Launcher +* Mods can now add new (offensive, buffs, debuffs) spells and change existing +* Mods can use custom mage guild background pictures and videos for taverns, setting of resources daily income for buildings + +### GENERAL: +* Added configuring of heroes quantity per player allowed in game + +# 0.94 -> 0.95 (Mar 01 2014) + +### GENERAL: +* Components of combined artifacts will now display info about entire set. +* Implements level limit +* Added WoG creature abilities by Kuririn +* Implemented a confirmation dialog when pressing Alt + F4 to quit the game +* Added precompiled header compilation for CMake (can be enabled per flag) +* VCMI will detect changes in text files using crc-32 checksum +* Basic support for unicode. Internally vcmi always uses utf-8 +* (linux) Launcher will be available as "VCMI" menu entry from system menu/launcher +* (linux) Added a SIGSEV violation handler to vcmiserver executable for logging stacktrace (for convenience) + +### ADVENTURE AI: +* AI will use fuzzy logic to compare and choose multiple possible subgoals. +* AI will now use SectorMap to find a way to guarded / covered objects. +* Significantly improved exploration algorithm. +* Locked heroes now try to decompose their goals exhaustively. +* Fixed (common) issue when AI found neutral stacks infinitely strong. +* Improvements for army exchange criteria. +* GatherArmy may include building dwellings in town (experimental). +* AI should now conquer map more aggressively and much faster +* Fuzzy rules will be printed out at map launch (if AI log is enabled) + +### CAMPAIGNS: +* Implemented move heroes to next scenario +* Support for non-standard victory conditions for H3 campaigns +* Campaigns use window with bonus & scenario selection than scenario information window from normal maps +* Implemented hero recreate handling (e.g. Xeron will be recreated on AB campaign) +* Moved place bonus hero before normal random hero and starting hero placement -> same behaviour as in OH3 +* Moved placing campaign heroes before random object generation -> same behaviour as in OH3 + +### TOWNS: +* Extended building dependencies support + +### MODS: +* Custom victory/loss conditions for maps or campaigns +* 7 days without towns loss condition is no longer hardcoded +* Only changed mods will be validated + +# 0.93 -> 0.94 (Oct 01 2013) + +### GENERAL: +* New Launcher application, see +* Filesystem now supports zip archives. They can be loaded similarly to other archives in filesystem.json. Mods can use Content.zip instead of Content/ directory. +* fixed "get txt" console command +* command "extract" to extract file by name +* command "def2bmp" to convert def into set of frames. +* fixed crash related to cammander's SPELL_AFTER_ATTACK spell id not initialized properly (text id was resolved on copy of bonus) +* fixed duels, added no-GUI mode for automatic AI testing +* Sir Mullich is available at the start of the game +* Upgrade cost will never be negative. +* support for Chinese fonts (GBK 2-byte encoding) + +### ADVENTURE MAP: +* if Quick Combat option is turned on, battles will be resolved by AI +* first hero is awakened on new turn +* fixed 3000 gems reward in shipwreck + +### BATTLES: +* autofight implemented +* most of the animations is time-based +* simplified postioning of units in battle, should fix remaining issues with unit positioning +* synchronized attack/defence animation +* spell animation speed uses game settings +* fixed disrupting ray duration +* added logging domain for battle animations +* Fixed crashes on Land Mines / Fire Wall casting. +* UI will be correctly greyed-out during opponent turn +* fixed remaining issues with blit order +* Catapult attacks should be identical to H3. Catapult may miss and attack another part of wall instead (this is how it works in H3) +* Fixed Remove Obstacle. +* defeating hero will yield 500 XP +* Added lots of missing spell immunities from Strategija +* Added stone gaze immunity for Troglodytes (did you know about it?) +* damage done by turrets is properly increased by built buldings +* Wyverns will cast Poison instead of Stone Gaze. + +### TOWN: +* Fixed issue that allowed to build multiple boats in town. +* fix for lookout tower + +# 0.92 -> 0.93 (Jun 01 2013) + +### GENERAL: +* Support for SoD-only installations, WoG becomes optional addition +* New logging framework +* Negative luck support, disabled by default +* Several new icons for creature abilities (Fire Shield, Non-living, Magic Mirror, Spell-like Attack) +* Fixed stack artifact (and related buttons) not displaying in creature window. +* Fixed crash at month of double population. + +### MODS: +* Improved json validation. Now it support most of features from latest json schema draft. +* Icons use path to icon instead of image indexes. +* It is possible to edit data of another mod or H3 data via mods. +* Mods can access only ID's from dependenies, virtual "core" mod and itself (optional for some mods compatibility) +* Removed no longer needed field "projectile spins" +* Heroes: split heroes.json in manner similar to creatures\factions; string ID's for H3 heroes; h3 hero classes and artifacts can be modified via json. + +### BATTLES: +* Fixed Death Stare of Commanders +* Projectile blitting should be closer to original H3. But still not perfect. +* Fixed missing Mirth effects +* Stack affected by Berserk should not try to attack itself +* Fixed several cases of incorrect positioning of creatures in battles +* Fixed abilities of Efreet. +* Fixed broken again palette in some battle backgrounds + +### TOWN: +* VCMI will not crash if building selection area is smaller than def +* Detection of transparency on selection area is closer to H3 +* Improved handling buildings with mode "auto": +* * they will be properly processed (new creatures will be added if dwelling, spells learned if mage guild, and so on) +* * transitive dependencies are handled (A makes B build, and B makes C and D) + +### SOUND: +* Added missing WoG creature sounds (from Kuririn). +* The Windows package comes with DLLs needed to play .ogg files +* (linux) convertMP3 option for vcmibuilder for systems where SDL_Mixer can't play mp3's +* some missing sounds for battle effects + +### ARTIFACTS: +* Several fixes to combined artifacts added via mods. +* Fixed Spellbinder's Hat giving level 1 spells instead of 5. +* Fixed incorrect components of Cornucopia. +* Cheat code with grant all artifacts, including the ones added by mods + +# 0.91 -> 0.92 (Mar 01 2013) + +### GENERAL: +* hero crossover between missions in campaigns +* introduction before missions in campaigns + +### MODS: +* Added CREATURE_SPELL_POWER for commanders +* Added spell modifiers to various spells: Hypnotize (Astral), Firewall (Luna), Landmine +* Fixed ENEMY_DEFENCE_REDUCTION, GENERAL_ATTACK_REDUCTION +* Extended usefulness of ONLY_DISTANCE_FIGHT, ONLY_MELEE_FIGHT ranges +* Double growth creatures are configurable now +* Drain Life now has % effect depending on bonus value +* Stack can use more than 2 attacks. Additional attacks can now be separated as "ONLY_MELEE_FIGHT and "ONLY_DISTANCE_FIGHT". +* Moat damage configurable +* More config options for spells: +* * mind immunity handled by config +* * direct damage immunity handled by config +* * immunity icon configurable +* * removed mind_spell flag +* creature config use string ids now. +* support for string subtype id in short bonus format +* primary skill identifiers for bonuses + +# 0.9 -> 0.91 (Feb 01 2013) + +### GENERAL: +* VCMI build on OS X is now supported +* Completely removed autotools +* Added RMG interace and ability to generate simplest working maps +* Added loading screen + +### MODS: +* Simplified mod structure. Mods from 0.9 will not be compatible. +* Mods can be turned on and off in config/modSettings.json file +* Support for new factions, including: +* * New towns +* * New hero classes +* * New heroes +* * New town-related external dwellings +* Support for new artifact, including combined, commander and stack artifacts +* Extended configuration options +* * All game objects are referenced by string identifiers +* * Subtype resolution for bonuses + +### BATTLES: +* Support for "enchanted" WoG ability + +### ADVENTURE AI: +* AI will try to use Subterranean Gate, Redwood Observatory and Cartographer for exploration +* Improved exploration algorithm +* AI will prioritize dwellings and mines when there are no opponents visible + +# 0.89 -> 0.9 (Oct 01 2012) + +### GENERAL: +* Provisional support creature-adding mods +* New filesystem allowing easier resource adding/replacing +* Reorganized package for better compatibility with HotA and not affecting the original game +* Moved many hard-coded settings into text config files +* Commander level-up dialog +* New Quest Log window +* Fixed a number of bugs in campaigns, support for starting hero selection bonus. + +### BATTLES: +* New graphics for Stack Queue +* Death Stare works identically to H3 +* No explosion when catapult fails to damage the wall +* Fixed crash when attacking stack dies before counterattack +* Fixed crash when attacking stack dies in the Moat just before the attack +* Fixed Orb of Inhibition and Recanter's Cloak (they were incorrectly implemented) +* Fleeing hero won't lose artifacts. +* Spellbook won't be captured. + +### ADVENTURE AI: +* support for quests (Seer Huts, Quest Guardians, and so) +* AI will now wander with all the heroes that have spare movement points. It should prevent stalling. +* AI will now understand threat of Abandoned Mine. +* AI can now exchange armies between heroes. By default, it will pass army to main hero. +* Fixed strange case when AI found allied town extremely dangerous +* Fixed crash when AI tried to "revisit" a Boat +* Fixed crash when hero assigned to goal was lost when attempting realizing it +* Fixed a possible freeze when exchanging resources at marketplace + +### BATTLE AI: +* It is possible to select a battle AI module used by VCMI by typing into the console "setBattleAI ". The names of avaialble modules are "StupidAI" and "BattleAI". BattleAI may be a little smarter but less stable. By the default, StupidAI will be used, as in previous releases. +* New battle AI module: "BattleAI" that is smarter and capable of casting some offensive and enchantment spells + +# 0.88 -> 0.89 (Jun 01 2012) + +### GENERAL: +* Mostly implemented Commanders feature (missing level-up dialog) +* Support for stack artifacts +* New creature window graphics contributed by fishkebab +* Config file may have multiple upgrades for creatures +* CTRL+T will open marketplace window +* G will open thieves guild window if player owns at least one town with tavern +* Implemented restart functionality. CTRL+R will trigger a quick restart +* Save game screen and returning to main menu will work if game was started with --start option +* Simple mechanism for detecting game desynchronization after init +* 1280x800 resolution graphics, contributed by Topas + +### ADVENTURE MAP: +* Fixed monsters regenerating casualties from battle at the start of new week. +* T in adventure map will switch to next town + +### BATTLES: +* It's possible to switch active creature during tacts phase by clicking on stack +* After battle artifacts of the defeated hero (and his army) will be taken by winner +* Rewritten handling of battle obstacles. They will be now placed following H3 algorithm. +* Fixed crash when death stare or acid breath activated on stack that was just killed +* First aid tent can heal only creatures that suffered damage +* War machines can't be healed by tent +* Creatures casting spells won't try to cast them during tactic phase +* Console tooltips for first aid tent +* Console tooltips for teleport spell +* Cursor is reset to pointer when action is requested +* Fixed a few other missing or wrong tooltips/cursors +* Implemented opening creature window by l-clicking on stack +* Fixed crash on attacking walls with Cyclop Kings +* Fixed and simplified Teleport casting +* Fixed Remove Obstacle spell +* New spells supported: +* * Chain Lightning +* * Fire Wall +* * Force Field +* * Land Mine +* * Quicksands +* * Sacrifice + +### TOWNS: +* T in castle window will open a tavern window (if available) + +### PREGAME: +* Pregame will use same resolution as main game +* Support for scaling background image +* Customization of graphics with config file. + +### ADVENTURE AI: +* basic rule system for threat evaluation +* new town development logic +* AI can now use external dwellings +* AI will weekly revisit dwellings & mills +* AI will now always pick best stacks from towns +* AI will recruit multiple heroes for exploration +* AI won't try attacking its own heroes + +# 0.87 -> 0.88 (Mar 01 2012) + +* added an initial version of new adventure AI: VCAI +* system settings window allows to change default resolution +* introduced unified JSON-based settings system +* fixed all known localization issues +* Creature Window can handle descriptions of spellcasting abilities +* Support for the clone spell + +# 0.86 -> 0.87 (Dec 01 2011) + +### GENERAL: +* Pathfinder can find way using ships and subterranean gates +* Hero reminder & sleep button + +### PREGAME: +* Credits are implemented + +### BATTLES: +* All attacked hexes will be highlighted +* New combat abilities supported: +* * Spell Resistance aura +* * Random spellcaster (Genies) +* * Mana channeling +* * Daemon summoning +* * Spellcaster (Archangel Ogre Mage, Elementals, Faerie Dragon) +* * Fear +* * Fearless +* * No wall penalty +* * Enchanter +* * Bind +* * Dispell helpful spells + +# 0.85 -> 0.86 (Sep 01 2011) + +### GENERAL: +* Reinstated music support +* Bonus system optimizations (caching) +* converted many config files to JSON +* .tga file support +* New artifacts supported +* * Admiral's Hat +* * Statue of Legion +* * Titan's Thunder + +### BATTLES: +* Correct handling of siege obstacles +* Catapult animation +* New combat abilities supported +* * Dragon Breath +* * Three-headed Attack +* * Attack all around +* * Death Cloud / Fireball area attack +* * Death Blow +* * Lightning Strike +* * Rebirth +* New WoG abilities supported +* * Defense Bonus +* * Cast before attack +* * Immunity to direct damage spells +* New spells supported +* * Magic Mirror +* * Titan's Lightning Bolt + +# 0.84 -> 0.85 (Jun 01 2011) + +### GENERAL: +* Support for stack experience +* Implemented original campaign selection screens +* New artifacts supported: +* * Statesman's Medal +* * Diplomat's Ring +* * Ambassador's Sash + +### TOWNS: +* Implemented animation for new town buildings +* It's possible to sell artifacts at Artifact Merchants + +### BATTLES: +* Neutral monsters will be split into multiple stacks +* Hero can surrender battle to keep army +* Support for Death Stare, Support for Poison, Age, Disease, Acid Breath, Fire / Water / Earth / Air immunities and Receptiveness +* Partial support for Stone Gaze, Paralyze, Mana drain + +# 0.83 -> 0.84 (Mar 01 2011) + +### GENERAL: +* Bonus system has been rewritten +* Partial support for running VCMI in duel mode (no adventure map, only one battle, ATM only AI-AI battles) +* New artifacts supported: +* * Angellic Alliance +* * Bird of Perception +* * Emblem of Cognizance +* * Spell Scroll +* * Stoic Watchman + +### BATTLES: +* Better animations handling +* Defensive stance is supported + +### HERO: +* New secondary skills supported: +* * Artillery +* * Eagle Eye +* * Tactics + +### AI PLAYER: +* new AI leading neutral creatures in combat, slightly better then previous + +# 0.82 -> 0.83 (Nov 01 2010) + +### GENERAL: +* Alliances support +* Week of / Month of events +* Mostly done pregame for MP games (temporarily only for local clients) +* Support for 16bpp displays +* Campaigns: +* * support for building bonus +* * moving to next map after victory +* Town Portal supported +* Vial of Dragon Blood and Statue of Legion supported + +### HERO: +* remaining specialities have been implemented + +### TOWNS: +* town events supported +* Support for new town structures: Deiety of Fire and Escape Tunnel + +### BATTLES: +* blocked retreating from castle + +# 0.81 -> 0.82 (Aug 01 2010) + +### GENERAL: +* Some of the starting bonuses in campaigns are supported +* It's possible to select difficulty level of mission in campaign +* new cheat codes: +* * vcmisilmaril - player wins +* * vcmimelkor - player loses + +### ADVENTURE MAP: +* Neutral armies growth implemented (10% weekly) +* Power rating of neutral stacks +* Favourable Winds reduce sailing cost + +### HERO: +* Learning secondary skill supported. +* Most of hero specialities are supported, including: +* * Creature specialities (progressive, fixed, Sir Mullich) +* * Spell damage specialities (Deemer), fixed bonus (Ciele) +* * Secondary skill bonuses +* * Creature Upgrades (Gelu) +* * Resorce generation +* * Starting Skill (Adrienne) + +### TOWNS: +* Support for new town structures: +* * Artifact Merchant +* * Aurora Borealis +* * Castle Gates +* * Magic University +* * Portal of Summoning +* * Skeleton transformer +* * Veil of Darkness + +### OBJECTS: +* Stables will now upgrade Cavaliers to Champions. +* New object supported: +* * Abandoned Mine +* * Altar of Sacrifice +* * Black Market +* * Cover of Darkness +* * Hill Fort +* * Refugee Camp +* * Sanctuary +* * Tavern +* * University +* * Whirlpool + +# 0.8 -> 0.81 (Jun 01 2010) + +### GENERAL: +* It's possible to start campaign +* Support for build grail victory condition +* New artifacts supported: +* * Angel's Wings +* * Boots of levitation +* * Orb of Vulnerability +* * Ammo cart +* * Golden Bow +* * Hourglass of Evil Hour +* * Bow of Sharpshooter +* * Armor of the Damned + +### ADVENTURE MAP: +* Creatures now guard surrounding tiles +* New adventura map spells supported: +* * Summon Boat +* * Scuttle Boat +* * Dimension Door +* * Fly +* * Water walk + +### BATTLES: +* A number of new creature abilities supported +* First Aid Tent is functional +* Support for distance/wall/melee penalties & no * penalty abilities +* Reworked damage calculation to fit OH3 formula better +* Luck support +* Teleportation spell + +### HERO: +* First Aid secondary skill +* Improved formula for necromancy to match better OH3 + +### TOWNS: +* Sending resources to other players by marketplace +* Support for new town structures: +* * Lighthouse +* * Colossus +* * Freelancer's Guild +* * Guardian Spirit +* * Necromancy Amplifier +* * Soul Prison + +### OBJECTS: +* New object supported: +* * Freelancer's Guild +* * Trading Post +* * War Machine Factory + +# 0.75 -> 0.8 (Mar 01 2010) + +### GENERAL: +* Victory and loss conditions are supported. It's now possible to win or lose the game. +* Implemented assembling and disassembling of combination artifacts. +* Kingdom Overview screen is now available. +* Implemented Grail (puzzle map, digging, constructing ultimate building) +* Replaced TTF fonts with original ones. + +### ADVENTURE MAP: +* Implemented rivers animations (thx to GrayFace). + +### BATTLES: +* Fire Shield spell (and creature ability) supported +* affecting morale/luck and casting spell after attack creature abilities supported + +### HERO: +* Implementation of Scholar secondary skill + +### TOWN: +* New left-bottom info panel functionalities. + +### TOWNS: +* new town structures supported: +* * Ballista Yard +* * Blood Obelisk +* * Brimstone Clouds +* * Dwarven Treasury +* * Fountain of Fortune +* * Glyphs of Fear +* * Mystic Pond +* * Thieves Guild +* * Special Grail functionalities for Dungeon, Stronghold and Fortress + +### OBJECTS: +* New objects supported: +* * Border gate +* * Den of Thieves +* * Lighthouse +* * Obelisk +* * Quest Guard +* * Seer hut + +A lot of of various bugfixes and improvements: +http://bugs.vcmi.eu/changelog_page.php?version_id=14 + +# 0.74 -> 0.75 (Dec 01 2009) + +### GENERAL: +* Implemented "main menu" in-game option. +* Hide the mouse cursor while displaying a popup window. +* Better handling of huge and empty message boxes (still needs more changes) +* Fixed several crashes when exiting. + +### ADVENTURE INTERFACE: +* Movement cursor shown for unguarded enemy towns. +* Battle cursor shown for guarded enemy garrisons. +* Clicking on the border no longer opens an empty info windows + +### HERO WINDOW: +* Improved artifact moving. Available slots are higlighted. Moved artifact is bound to mouse cursor. + +### TOWNS: +* new special town structures supported: +* * Academy of Battle Scholars +* * Cage of Warlords +* * Mana Vortex +* * Stables +* * Skyship (revealing entire map only) + +### OBJECTS: +* External dwellings increase town growth +* Right-click info window for castles and garrisons you do not own shows a rough amount of creatures instead of none +* Scholar won't give unavaliable spells anymore. + +A lot of of various bugfixes and improvements: +http://bugs.vcmi.eu/changelog_page.php?version_id=2 + +# 0.73 -> 0.74 (Oct 01 2009) + +### GENERAL: +* Scenario Information window +* Save Game window +* VCMI window should start centered +* support for Necromancy and Ballistics secondary skills +* new artifacts supported, including those improving Necromancy, Legion Statue parts, Shackles of War and most of combination artifacts (but not combining) +* VCMI client has its own icon (thx for graphic to Dikamilo) +* Ellipsis won't be split when breaking text on several lines +* split button will be grayed out when no creature is selected +* fixed issue when splitting stack to the hero with only one creatures +* a few fixes for shipyard window + +### ADVENTURE INTERFACE: +* Cursor shows if tile is accesible and how many turns away +* moving hero with arrow keys / numpad +* fixed Next Hero button behaviour +* fixed Surface/Underground switch button in higher resolutions + +### BATTLES: +* partial siege support +* new stack queue for higher resolutions (graphics made by Dru, thx!) +* 'Q' pressing toggles the stack queue displaying (so it can be enabled/disabled it with single key press) +* more creatures special abilities supported +* battle settings will be stored +* fixed crashes occurring on attacking two hex creatures from back +* fixed crash when clicking on enemy stack without moving mouse just after receiving action +* even large stack numbers will fit the boxes +* when active stack is killed by spell, game behaves properly +* shooters attacking twice (like Grand Elves) won't attack twice in melee +* ballista can shoot even if there's an enemy creature next to it +* improved obstacles placement, so they'll better fit hexes (thx to Ivan!) +* selecting attack directions works as in H3 +* estimating damage that will be dealt while choosing stack to be attacked +* modified the positioning of battle effects, they should look about right now. +* after selecting a spell during combat, l-click is locked for any action other than casting. +* flying creatures will be blitted over all other creatures, obstacles and wall +* obstacles and units should be printed in better order (not tested) +* fixed armageddon animation +* new spells supported: +* * Anti-Magic +* * Cure +* * Resurrection +* * Animate Dead +* * Counterstrike +* * Berserk +* * Hypnotize +* * Blind +* * Fire Elemental +* * Earth Elemental +* * Water Elemental +* * Air Elemental +* * Remove obstacle + +### TOWNS: +* enemy castle can be taken over +* only one capitol per player allowed (additional ones will be lost) +* garrisoned hero can buy a spellbook +* heroes available in tavern should be always different +* ship bought in town will be correctly placed +* new special town structures supported: +* * Lookout Tower +* * Temple of Valhalla +* * Wall of Knowledge +* * Order of Fire + +### HERO WINDOW: +* war machines cannot be unequiped + +### PREGAME: +* sorting: a second click on the column header sorts in descending order. +* advanced options tab: r-click popups for selected town, hero and bonus +* starting scenario / game by double click +* arrows in options tab are hidden when not available +* subtitles for chosen hero/town/bonus in pregame + +### OBJECTS: +* fixed pairing Subterranean Gates +* New objects supported: +* * Borderguard & Keymaster Tent +* * Cartographer +* * Creature banks +* * Eye of the Magi & Hut of the Magi +* * Garrison +* * Stables +* * Pandora Box +* * Pyramid + +# 0.72 -> 0.73 (Aug 01 2009) + +### GENERAL: +* infowindow popup will be completely on screen +* fixed possible crash with in game console +* fixed crash when gaining artifact after r-click on hero in tavern +* Estates / hero bonuses won't give resources on first day. +* video handling (intro, main menu animation, tavern animation, spellbook animation, battle result window) +* hero meeting window allowing exchanging armies and artifacts between heroes on adventure map +* 'T' hotkey opens marketplace window +* giving starting spells for heroes +* pressing enter or escape close spellbook +* removed redundant quotation marks from skills description and artifact events texts +* disabled autosaving on first turn +* bonuses from bonus artifacts +* increased char per line limit for subtitles under components +* corrected some exp/level values +* primary skills cannot be negative +* support for new artifacts: Ring of Vitality, Ring of Life, Vial of Lifeblood, Garniture of Interference, Surcoat of Counterpoise, Boots of Polarity +* fixed timed events reappearing +* saving system options +* saving hero direction +* r-click popups on enemy heroes and towns +* hero leveling formula matches the H3 + +### ADVENTURE INTERFACE: +* Garrisoning, then removing hero from garrison move him at the end of the heroes list +* The size of the frame around the map depends on the screen size. +* spellbook shows adventure spells when opened on adventure map +* erasing path after picking objects with last movement point + +### BATTLES: +* spell resistance supported (secondary skill, artifacts, creature skill) +* corrected damage inflicted by spells and ballista +* added some missing projectile infos +* added native terrain bonuses in battles +* number of units in stack in battle should better fit the box +* non-living and undead creatures have now always 0 morale +* displaying luck effect animation +* support for battleground overlays: +* * cursed ground +* * magic plains +* * fiery fields +* * rock lands +* * magic clouds +* * lucid pools +* * holy ground +* * clover field +* * evil fog + +### TOWNS: +* fixes for horde buildings +* garrisoned hero can buy a spellbook if he is selected or if there is no visiting hero +* capitol bar in town hall is grey (not red) if already one exists +* fixed crash on entering hall when town was near map edge + +### HERO WINDOW: +* garrisoned heroes won't be shown on the list +* artifacts will be present on morale/luck bonuses list + +### PREGAME: +* saves are sorted primary by map format, secondary by name +* fixed displaying date of saved game (uses local time, removed square character) + +### OBJECTS: +* Fixed primary/secondary skill levels given by a scholar. +* fixed problems with 3-tiles monoliths +* fixed crash with flaggable building next to map edge +* fixed some descriptions for events +* New objects supported: +* * Buoy +* * Creature Generators +* * Flotsam +* * Mermaid +* * Ocean bottle +* * Sea Chest +* * Shipwreck Survivor +* * Shipyard +* * Sirens + +# 0.71 -> 0.72 (Jun 1 2009) + +### GENERAL: +* many sound effects and music +* autosave (to 5 subsequent files) +* artifacts support (most of them) +* added internal game console (activated on TAB) +* fixed 8 hero limit to check only for wandering heroes (not garrisoned) +* improved randomization +* fixed crash on closing application +* VCMI won't always give all three stacks in the starting armies +* fix for drawing starting army creatures count +* Diplomacy secondary skill support +* timed events won't cause resources amount to be negative +* support for sorcery secondary skill +* reduntant quotation marks from artifact descriptions are removed +* no income at the first day + +### ADVENTURE INTERFACE: +* fixed crasbug occurring on revisiting objects (by pressing space) +* always restoring default cursor when movng mouse out of the terrain +* fixed map scrolling with ctrl+arrows when some windows are opened +* clicking scrolling arrows in town/hero list won't open town/hero window +* pathfinder will now look for a path going via printed positions of roads when it's possible +* enter can be used to open window with selected hero/town + +### BATTLES: +* many creatures special skills implemented +* battle will end when one side has only war machines +* fixed some problems with handling obstacles info +* fixed bug with defending / waiting while no stack is active +* spellbook button is inactive when hero cannot cast any spell +* obstacles will be placed more properly when resolution is different than 800x600 +* canceling of casting a spell by pressing Escape or R-click (R-click on a creatures does not cancel a spell) +* spellbook cannot be opened by L-click on hero in battle when it shouldn't be possible +* new spells: +* * frost ring +* * fireball +* * inferno +* * meteor shower +* * death ripple +* * destroy undead +* * dispel +* * armageddon +* * disrupting ray +* * protection from air +* * protection from fire +* * protection from water +* * protection from earth +* * precision +* * slayer + +### TOWNS: +* resting in town with mage guild will replenih all the mana points +* fixed Blacksmith +* the number of creatures at the beginning of game is their base growth +* it's possible to enter Tavern via Brotherhood of Sword + +### HERO WINDOW: +* fixed mana limit info in the hero window +* war machines can't be removed +* fixed problems with removing artifacts when all visible slots in backpack are full + +### PREGAME: +* clicking on "advanced options" a second time now closes the tab instead of refreshing it. +* Fix position of maps names. +* Made the slider cursor much more responsive. Speedup the map select screen. +* Try to behave when no maps/saves are present. +* Page Up / Page Down / Home / End hotkeys for scrolling through scenarios / games list + +### OBJECTS: +* Neutral creatures can join or escape depending on hero strength (escape formula needs to be improved) +* leaving guardians in flagged mines. +* support for Scholar object +* support for School of Magic +* support for School of War +* support for Pillar of Fire +* support for Corpse +* support for Lean To +* support for Wagon +* support for Warrior's Tomb +* support for Event +* Corpse (Skeleton) will be accessible from all directions + +# 0.7 -> 0.71 (Apr 01 2009) + +### GENERAL: +* fixed scrolling behind window problem (now it's possible to scroll with CTRL + arrows) +* morale/luck system and corresponding sec. skills supported +* fixed crash when hero get level and has less than two sec. skills to choose between +* added keybindings for components in selection window (eg. for treasure chest dialog): 1, 2, and so on. Selection dialog can be closed with Enter key +* proper handling of custom portraits of heroes +* fixed problems with non-hero/town defs not present in def list but present on map (occurring probably only in case of def substitution in map editor) +* fixed crash when there was no hero available to hire for some player +* fixed problems with 1024x600 screen resolution +* updating blockmap/visitmap of randomized objects +* fixed crashes on loading maps with flag all mines/dwelling victory condition +* further fixes for leveling-up (stability and identical offered skills bug) +* splitting window allows to rebalance two stack with the same creatures +* support for numpad keyboard +* support for timed events + +### ADVENTURE INTERFACE: +* added "Next hero" button functionality +* added missing path arrows +* corrected centering on hero's position +* recalculating hero path after reselecting hero +* further changes in pathfinder making it more like original one +* orientation of hero can't be change if movement points are exhausted +* campfire, borderguard, bordergate, questguard will be accessible from the top +* new movement cost calculation algorithm +* fixed sight radious calculation +* it's possible to stop hero movement +* faster minimap refreshing +* provisional support for "Save" button in System Options Window +* it's possible to revisit object under hero by pressing Space + +### BATTLES: +* partial support for battle obstacles +* only one spell can be casted per turn +* blocked opening sepllbook if hero doesn't have a one +* spells not known by hero can't be casted +* spell books won't be placed in War Machine slots after battle +* attack is now possible when hex under cursor is not displayed +* glowing effect of yellow border around creatures +* blue glowing border around hovered creature +* made animation on battlefield more smooth +* standing stacks have more static animation +* probably fixed problem with displaying corpses on battlefield +* fixes for two-hex creatures actions +* fixed hero casting spell animation +* corrected stack death animation +* battle settings will be remembered between battles +* improved damage calculation formula +* correct handling of flying creatures in battles +* a few tweaks in battle path/available hexes calculation (more of them is needed) +* amounts of units taking actions / being an object of actions won't be shown until action ends +* fixed positions of stack queue and battle result window when resolution is != 800x600 +* corrected duration of frenzy spell which was incorrect in certain cases +* corrected hero spell casting animation +* better support for battle backgrounds +* blocked "save" command during battle +* spellbook displays only spells known by Hero +* New spells supported: +* * Mirth +* * Sorrow +* * Fortune +* * Misfortune + +### TOWN INTERFACE: +* cannot build more than one capitol +* cannot build shipyard if town is not near water +* Rampart's Treasury requires Miner's Guild +* minor improvements in Recruitment Window +* fixed crash occurring when clicking on hero portrait in Tavern Window, minor improvements for Tavern Window +* proper updating resdatabar after building structure in town or buying creatures (non 800x600 res) +* fixed blinking resdatabar in town screen when buying (800x600) +* fixed horde buildings displaying in town hall +* forbidden buildings will be shown as forbidden, even if there are no res / other conditions are not fulfilled + +### PREGAME: +* added scrolling scenario list with mouse wheel +* fixed mouse slow downs +* cannot select heroes for computer player (pregame) +* no crash if uses gives wrong resolution ID number +* minor fixes + +### OBJECTS: +* windmill gives 500 gold only during first week ever (not every month) +* After the first visit to the Witch Hut, right-click/hover tip mentions the skill available. +* New objects supported: +* * Prison +* * Magic Well +* * Faerie Ring +* * Swan Pond +* * Idol of Fortune +* * Fountain of Fortune +* * Rally Flag +* * Oasis +* * Temple +* * Watering Hole +* * Fountain of Youth +* * support for Redwood Observatory +* * support for Shrine of Magic Incantation / Gesture / Thought +* * support for Sign / Ocean Bottle + +### AI PLAYER: +* Minor improvements and fixes. + +# 0.64 -> 0.7 (Feb 01 2009) + +### GENERAL: +* move some settings to the config/settings.txt file +* partial support for new screen resolutions +* it's possible to set game resolution in pregame (type 'resolution' in the console) +* /Data and /Sprites subfolders can be used for adding files not present in .lod archives +* fixed crashbug occurring when hero levelled above 15 level +* support for non-standard screen resolutions +* F4 toggles between full-screen and windowed mode +* minor improvements in creature card window +* splitting stacks with the shift+click +* creature card window contains info about modified speed + +### ADVENTURE INTERFACE: +* added water animation +* speed of scrolling map and hero movement can be adjusted in the System Options Window +* partial handling r-clicks on adventure map + +### TOWN INTERFACE: +* the scroll tab won't remain hanged to our mouse position if we move the mouse is away from the scroll bar +* fixed cloning creatures bug in garrisons (and related issues) + +### BATTLES: +* support for the Wait command +* magic arrow *really* works +* war machines support partially added +* queue of stacks narrowed +* spell effect animation displaying improvements +* positive/negative spells cannot be cast on hostile/our stacks +* showing spell effects affecting stack in creature info window +* more appropriate coloring of stack amount box when stack is affected by a spell +* battle console displays notifications about wait/defend commands +* several reported bugs fixed +* new spells supported: +* * Haste +* * lightning bolt +* * ice bolt +* * slow +* * implosion +* * forgetfulness +* * shield +* * air shield +* * bless +* * curse +* * bloodlust +* * weakness +* * stone skin +* * prayer +* * frenzy + +### AI PLAYER: +* Genius AI (first VCMI AI) will control computer creatures during the combat. + +### OBJECTS: +* Guardians property for resources is handled +* support for Witch Hut +* support for Arena +* support for Library of Enlightenment + +And a lot of minor fixes + +# 0.63 -> 0.64 (Nov 01 2008) + +### GENERAL: +* sprites from /Sprites folder are handled correctly +* several fixes for pathfinder and path arrows +* better handling disposed/predefined heroes +* heroes regain 1 mana point each turn +* support for mistycisim and intelligence skills +* hero hiring possible +* added support for a number of hotkeys +* it's not possible anymore to leave hero level-up window without selecting secondary skill +* many minor improvements + +* Added some kind of simple chatting functionality through console. Implemented several WoG cheats equivalents: +* * woggaladriel -> vcmiainur +* * wogoliphaunt -> vcminoldor +* * wogshadowfax -> vcminahar +* * wogeyeofsauron -> vcmieagles +* * wogisengard -> vcmiformenos +* * wogsaruman -> vcmiistari +* * wogpathofthedead -> vcmiangband +* * woggandalfwhite -> vcmiglorfindel + +### ADVENTURE INTERFACE: +* clicking on a tile in advmap view when a path is shown will not only hide it but also calculate a new one +* slowed map scrolling +* blocked scrolling adventure map with mouse when left ctrl is pressed +* blocked map scrolling when dialog window is opened +* scholar will be accessible from the top + +### TOWN INTERFACE: +* partially done tavern window (only hero hiring functionality) + +### BATTLES: +* water elemental will really be treated as 2 hex creature +* potential infinite loop in reverseCreature removed +* better handling of battle cursor +* fixed blocked shooter behavior +* it's possible in battles to check remeaining HP of neutral stacks +* partial support for Magic Arrow spell +* fixed bug with dying unit +* stack queue hotkey is now 'Q' +* added shots limit + +# 0.62 -> 0.63 (Oct 01 2008) + +### GENERAL: +* coloured console output, logging all info to txt files +* it's possible to use other port than 3030 by passing it as an additional argument +* removed some redundant warnings +* partially done spellbook +* Alt+F4 quits the game +* some crashbugs was fixed +* added handling of navigation, logistics, pathfinding, scouting end estates secondary skill +* magical hero are given spellbook at the beginning +* added initial secondary skills for heroes + +### BATTLES: +* very significant optimization of battles +* battle summary window +* fixed crashbug occurring sometimes on exiting battle +* confirm window is shown before retreat +* graphic stack queue in battle (shows when 'c' key is pressed) +* it's possible to attack enemy hero +* neutral monster army disappears when defeated +* casualties among hero army and neutral creatures are saved +* better animation handling in battles +* directional attack in battles +* mostly done battle options (although they're not saved) +* added receiving exp (and leveling-up) after a won battle +* added support for archery, offence and armourer secondary abilities +* hero's primary skills accounted for damage dealt by creatures in battle + +### TOWNS: +* mostly done marketplace +* fixed crashbug with battles on swamps and rough terrain +* counterattacks +* heroes can learn new spells in towns +* working resource silo +* fixed bug with the mage guild when no spells available +* it's possible to build lighthouse + +### HERO WINDOW: +* setting army formation +* tooltips for artifacts in backpack + +### ADVENTURE INTERFACE: +* fixed bug with disappearing head of a hero in adventure map +* some objects are no longer accessible from the top +* no tooltips for objects under FoW +* events won't be shown +* working Subterranean Gates, Monoliths +* minimap shows all flaggable objects (towns, mines, etc.) +* artifacts we pick up go to the appropriate slot (if free) + +# 0.61 -> 0.62 (Sep 01 2008) + +### GENERAL: +* restructured to the server-client model +* support for heroes placed in towns +* upgrading creatures +* working gaining levels for heroes (including dialog with skill selection) +* added graphical cursor +* showing creature amount in the creature info window +* giving starting bonus + +### CASTLES: +* icon in infobox showing that there is hero in town garrison +* fort/citadel/castle screen +* taking last stack from the heroes army should be impossible (or at least harder) +* fixed reading forbidden structures +* randomizing spells in towns +* viewing hero window in the town screen +* possibility of moving hero into the garrison +* mage guild screen +* support for blacksmith +* if hero doesn't have a spell book, he can buy one in a mage guild +* it's possible to build glyph of fear in fortress +* creatures placeholders work properly + +### ADVENTURE INTERFACE: +* hopefully fixed problems with wrong town defs (village/fort/capitol) + +### HERO WINDOW: +* bugfix: splitting stacks works in hero window +* removed bug causing significant increase of CPU consumption + +### BATTLES: +* shooting +* removed some displaying problems +* showing last group of frames in creature animation won't crash +* added start moving and end moving animations +* fixed moving two-hex creatures +* showing/hiding graphic cursor +* a part of using graphic cursor +* slightly optimized showing of battle interface +* animation of getting hit / death by shooting is displayed when it should be +* improved pathfinding in battles, removed problems with displaying movement, adventure map interface won't be called during battles. +* minor optimizations + +### PREGAME: +* updates settings when selecting new map after changing sorting criteria +* if sorting not by name, name will be used as a secondary criteria +* when filter is applied a first available map is selected automatically +* slider position updated after sorting in pregame + +### OBJECTS: +* support for the Tree of knowledge +* support for Campfires +* added event message when picking artifact + +# 0.6 -> 0.61 (Jun 15 2008) + +### IMPROVEMENTS: +* improved attacking in the battles +* it's possible to kill hostile stack +* animations won't go in the same phase +* Better pathfinder +* "%s" substitutions in Right-click information in town hall +* windmill won't give wood +* hover text for heroes +* support for ZSoft-style PCX files in /Data +* Splitting: when moving slider to the right so that 0 is left in old slot the army is moved +* in the townlist in castle selected town will by placed on the 2nd place (not 3rd) +* stack at the limit of unit's range can now be attacked +* range of unit is now properly displayed +* battle log is scrolled down when new event occurs +* console is closed when application exits + +### BUGFIXES: +* stack at the limit of unit's range can now be attacked +* good background for the town hall screen in Stronghold +* fixed typo in hall.txt +* VCMI won't crash when r-click neutral stack during the battle +* water won't blink behind shipyard in the Castle +* fixed several memory leaks +* properly displaying two-hex creatures in recruit/split/info window +* corrupted map file won't cause crash on initializing main menu + +# 0.59 -> 0.6 (Jun 1 2008) + +* partially done attacking in battles +* screen isn't now refreshed while blitting creature info window +* r-click creature info windows in battles +* no more division by 0 in slider +* "plural" reference names for Conflux creatures (starting armies of Conflux heroes should now be working) +* fixed estate problems +* fixed blinking mana vortex +* grail increases creature growths +* new pathfinder +* several minor improvements + +# 0.58 -> 0.59 (May 24 2008 - closed, test release) + +* fixed memory leak in battles +* blitting creature animations to rects in the recruitment window +* fixed wrong creatures def names +* better battle pathfinder and unit reversing +* improved slider ( #58 ) +* fixed problems with horde buildings (won't block original dwellings) +* giving primary skill when hero get level (but there is still no dialog) +* if an upgraded creature is available it'll be shown as the first in a recruitment window +* creature levels not messed in Fortress +* war machines are added to the hero's inventory, not to the garrison +* support for H3-style PCX graphics in Data/ +* VCMI won't crash when is unable to initialize audio system +* fixed displaying wrong town defs +* improvements in recruitment window (slider won't allow to select more creatures than we can afford) +* creature info window (only r-click) +* callback for buttons/lists based on boost::function +* a lot of minor improvements + +# 0.55 -> 0.58 (Apr 20 2008 - closed, test release) + +### TOWNS: +* recruiting creatures +* working creature growths (including castle and horde building influences) +* towns give income +* town hall screen +* building buildings (requirements and cost are handled) +* hints for structures +* updating town infobox + +### GARRISONS: +* merging stacks +* splitting stacks + +### BATTLES: +* starting battles +* displaying terrain, animations of heroes, units, grid, range of units, battle menu with console, amounts of units in stacks +* leaving battle by pressing flee button +* moving units in battles and displaying their ranges +* defend command for units + +### GENERAL: +* a number of minor fixes and improvements + +# 0.54 -> 0.55 (Feb 29 2008) + +* Sprites/ folder works for h3sprite.lod same as Data/ for h3bitmap.lod (but it's still experimental) +* randomization quantity of creatures on the map +* fix of Pandora's Box handling +* reading disposed/predefined heroes +* new command - "get txt" - VCMI will extract all .txt files from h3bitmap.lod to the Extracted_txts/ folder. +* more detailed logs +* reported problems with hero flags resolved +* heroes cannot occupy the same tile +* hints for most of creature generators +* some minor stuff + +# 0.53b -> 0.54 (Feb 23 2008 - first public release) +* given hero is placed in the town entrance +* some objects such as river delta won't be blitted "on" hero +* tiles under FoW are inaccessible +* giving random hero on RoE maps +* improved protection against hero duplication +* fixed starting values of primary abilities of random heroes on RoE/AB maps +* right click popups with infoboxes for heroes/towns lists +* new interface coloring (many thanks to GrayFace ;]) +* fixed bug in object flag's coloring +* added hints in town lists +* eliminated square from city hints + +# 0.53 - 0.53b (Feb 20 2008) + +* added giving default buildings in towns +* town infobox won't crash on empty town + +# 0.52 - 0.53 (Feb 18 2008): + +* hopefully the last bugfix of Pandora's Box +* fixed blockmaps of generated heroes +* disposed hero cannot be chosen in scenario settings (unless he is in prison) +* fixed town randomization +* fixed hero randomization +* fixed displaying heroes in preGame +* fixed selecting/deselecting artifact slots in hero window +* much faster pathfinder +* memory usage and load time significantly decreased +* it's impossible to select empty artifact slot in hero window +* fixed problem with FoW displaying on minimap on L-sized maps +* fixed crashbug in hero list connected with heroes dismissing +* mostly done town infobox +* town daily income is properly calculated + +# 0.51 - 0.52 (Feb 7 2008): + +* [feature] giving starting hero +* [feature] VCMI will try to use files from /Data folder instead of those from h3bitmap.lod +* [feature] picked artifacts are added to hero's backpack +* [feature] possibility of choosing player to play +* [bugfix] ZELP.TXT file *should* be handled correctly even it is non-english +* [bugfix] fixed crashbug in reading defs with negativ left/right margins +* [bugfix] improved randomization +* [bugfix] pathfinder can't be cheated (what caused errors) + +# 0.5 - 0.51 (Feb 3 2008): + +* close button properly closes (same does 'q' key) +* two players can't have selected same hero +* double click on "Show Available Scenarios" won't reset options +* fixed possible crashbug in town/hero lists +* fixed crashbug in initializing game caused by wrong prisons handling +* fixed crashbug on reading hero's custom artifacts in RoE maps +* fixed crashbug on reading custom Pandora's Box in RoE maps +* fixed crashbug on reading blank Quest Guards +* better console messages +* map reading speed up (though it's still slow, especially on bigger maps) + +# 0.0 -> 0.5 (Feb 2 2008 - first closed release): + +* Main menu and New game screens +* Scenario selection, part of advanced options support +* Partially done adventure map, town and hero interfaces +* Moving hero +* Interactions with several objects (mines, resources, mills, and others) diff --git a/Global.h b/Global.h index 2eb54db45..c18b5a5e8 100644 --- a/Global.h +++ b/Global.h @@ -1,679 +1,679 @@ -/* - * Global.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 - -/* ---------------------------------------------------------------------------- */ -/* Compiler detection */ -/* ---------------------------------------------------------------------------- */ -// Fixed width bool data type is important for serialization -static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); - -/* ---------------------------------------------------------------------------- */ -/* System detection. */ -/* ---------------------------------------------------------------------------- */ -// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/ -// and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor -// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h) -#ifdef _WIN16 // Defined for 16-bit environments -# error "16-bit Windows isn't supported" -#elif defined(_WIN64) // Defined for 64-bit environments -# define VCMI_WINDOWS -# define VCMI_WINDOWS_64 -#elif defined(_WIN32) // Defined for both 32-bit and 64-bit environments -# define VCMI_WINDOWS -# define VCMI_WINDOWS_32 -#elif defined(_WIN32_WCE) -# error "Windows CE isn't supported" -#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux) -# define VCMI_UNIX -# define VCMI_XDG -# if defined(__ANDROID__) || defined(ANDROID) -# define VCMI_ANDROID -# endif -#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_FREEBSD -#elif defined(__HAIKU__) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_HAIKU -#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__)) -# define VCMI_UNIX -# define VCMI_XDG -# define VCMI_HURD -#elif defined(__APPLE__) && defined(__MACH__) -# define VCMI_UNIX -# define VCMI_APPLE -# include "TargetConditionals.h" -# if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR -# define VCMI_IOS -# define VCMI_IOS_SIM -# elif TARGET_OS_IPHONE -# define VCMI_IOS -# elif TARGET_OS_MAC -# define VCMI_MAC -# else -//# warning "Unknown Apple target."? -# endif -#else -# error "This platform isn't supported" -#endif - -#if defined(VCMI_ANDROID) || defined(VCMI_IOS) -#define VCMI_MOBILE -#endif - -/* ---------------------------------------------------------------------------- */ -/* Commonly used C++, Boost headers */ -/* ---------------------------------------------------------------------------- */ -#ifdef VCMI_WINDOWS -# ifndef WIN32_LEAN_AND_MEAN -# define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers - delete this line if something is missing. -# endif -# ifndef NOMINMAX -# define NOMINMAX // Exclude min/max macros from . Use std::[min/max] from instead. -# endif -# ifndef _NO_W32_PSEUDO_MODIFIERS -# define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux. -# endif -#endif - -/* ---------------------------------------------------------------------------- */ -/* A macro to force inlining some of our functions */ -/* ---------------------------------------------------------------------------- */ -// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower -#ifdef _MSC_VER -# define STRONG_INLINE __forceinline -#elif __GNUC__ -# define STRONG_INLINE inline __attribute__((always_inline)) -#else -# define STRONG_INLINE inline -#endif - -#define _USE_MATH_DEFINES - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//The only available version is 3, as of Boost 1.50 -#include - -#define BOOST_FILESYSTEM_VERSION 3 -#if BOOST_VERSION > 105000 -# define BOOST_THREAD_VERSION 3 -#endif -#define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1 -//need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary -#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically -#define BOOST_BIND_NO_PLACEHOLDERS - -#if BOOST_VERSION >= 106600 -#define BOOST_ASIO_ENABLE_OLD_SERVICES -#endif - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#ifdef VCMI_WINDOWS -#include -#endif -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#ifndef M_PI -# define M_PI 3.14159265358979323846 -#endif - -/* ---------------------------------------------------------------------------- */ -/* Usings */ -/* ---------------------------------------------------------------------------- */ -using namespace std::placeholders; -namespace range = boost::range; - -/* ---------------------------------------------------------------------------- */ -/* Typedefs */ -/* ---------------------------------------------------------------------------- */ -// Integral data types -typedef uint64_t ui64; //unsigned int 64 bits (8 bytes) -typedef uint32_t ui32; //unsigned int 32 bits (4 bytes) -typedef uint16_t ui16; //unsigned int 16 bits (2 bytes) -typedef uint8_t ui8; //unsigned int 8 bits (1 byte) -typedef int64_t si64; //signed int 64 bits (8 bytes) -typedef int32_t si32; //signed int 32 bits (4 bytes) -typedef int16_t si16; //signed int 16 bits (2 bytes) -typedef int8_t si8; //signed int 8 bits (1 byte) - -// Lock typedefs -using TLockGuard = std::lock_guard; -using TLockGuardRec = std::lock_guard; - -/* ---------------------------------------------------------------------------- */ -/* Macros */ -/* ---------------------------------------------------------------------------- */ -// Import + Export macro declarations -#ifdef VCMI_WINDOWS -#ifdef VCMI_DLL_STATIC -# define DLL_IMPORT -# define DLL_EXPORT -#elif defined(__GNUC__) -# define DLL_IMPORT __attribute__((dllimport)) -# define DLL_EXPORT __attribute__((dllexport)) -# else -# define DLL_IMPORT __declspec(dllimport) -# define DLL_EXPORT __declspec(dllexport) -# endif -# define ELF_VISIBILITY -#else -# ifdef __GNUC__ -# define DLL_IMPORT __attribute__ ((visibility("default"))) -# define DLL_EXPORT __attribute__ ((visibility("default"))) -# define ELF_VISIBILITY __attribute__ ((visibility("default"))) -# endif -#endif - -#ifdef VCMI_DLL -# define DLL_LINKAGE DLL_EXPORT -#else -# define DLL_LINKAGE DLL_IMPORT -#endif - -#define THROW_FORMAT(message, formatting_elems) throw std::runtime_error(boost::str(boost::format(message) % formatting_elems)) - -// old iOS SDKs compatibility -#ifdef VCMI_IOS -#include - -#ifndef __IPHONE_13_0 -#define __IPHONE_13_0 130000 -#endif -#endif // VCMI_IOS - -// single-process build makes 2 copies of the main lib by wrapping it in a namespace -#ifdef VCMI_LIB_NAMESPACE -#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE { -#define VCMI_LIB_NAMESPACE_END } -#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE; -#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x -#else -#define VCMI_LIB_NAMESPACE_BEGIN -#define VCMI_LIB_NAMESPACE_END -#define VCMI_LIB_USING_NAMESPACE -#define VCMI_LIB_WRAP_NAMESPACE(x) ::x -#endif - -/* ---------------------------------------------------------------------------- */ -/* VCMI standard library */ -/* ---------------------------------------------------------------------------- */ -#include - -VCMI_LIB_NAMESPACE_BEGIN - -namespace vstd -{ - // combine hashes. Present in boost but not in std - template - inline void hash_combine(std::size_t& seed, const T& v) - { - std::hash hasher; - seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); - } - - //returns true if container c contains item i - template - bool contains(const Container & c, const Item &i) - { - return std::find(std::begin(c), std::end(c),i) != std::end(c); - } - - //returns true if container c contains item i - template - bool contains_if(const Container & c, Pred p) - { - return std::find_if(std::begin(c), std::end(c), p) != std::end(c); - } - - //returns true if map c contains item i - template - bool contains(const std::map & c, const Item2 &i) - { - return c.find(i)!=c.end(); - } - - //returns true if unordered set c contains item i - template - bool contains(const std::unordered_set & c, const Item &i) - { - return c.find(i)!=c.end(); - } - - template - bool contains(const std::unordered_map & c, const Item2 &i) - { - return c.find(i)!=c.end(); - } - - //returns position of first element in vector c equal to s, if there is no such element, -1 is returned - template - int find_pos(const Container & c, const T2 &s) - { - int i=0; - for (auto iter = std::begin(c); iter != std::end(c); iter++, i++) - if(*iter == s) - return i; - return -1; - } - - //Func f tells if element matches - template - int find_pos_if(const Container & c, const Func &f) - { - auto ret = boost::range::find_if(c, f); - if(ret != std::end(c)) - return std::distance(std::begin(c), ret); - - return -1; - } - - //returns iterator to the given element if present in container, end() if not - template - typename Container::iterator find(Container & c, const Item &i) - { - return std::find(c.begin(),c.end(),i); - } - - //returns const iterator to the given element if present in container, end() if not - template - typename Container::const_iterator find(const Container & c, const Item &i) - { - return std::find(c.begin(),c.end(),i); - } - - //returns first key that maps to given value if present, returns success via found if provided - template - Key findKey(const std::map & map, const T & value, bool * found = nullptr) - { - for(auto iter = map.cbegin(); iter != map.cend(); iter++) - { - if(iter->second == value) - { - if(found) - *found = true; - return iter->first; - } - } - if(found) - *found = false; - return Key(); - } - - //removes element i from container c, returns false if c does not contain i - template - typename Container::size_type operator-=(Container &c, const Item &i) - { - typename Container::iterator itr = find(c,i); - if(itr == c.end()) - return false; - c.erase(itr); - return true; - } - - //assigns greater of (a, b) to a and returns maximum of (a, b) - template - t1 &amax(t1 &a, const t2 &b) - { - if(a >= (t1)b) - return a; - else - { - a = t1(b); - return a; - } - } - - //assigns smaller of (a, b) to a and returns minimum of (a, b) - template - t1 &amin(t1 &a, const t2 &b) - { - if(a <= (t1)b) - return a; - else - { - a = t1(b); - return a; - } - } - - //makes a to fit the range - template - void abetween(T &value, const T &min, const T &max) - { - value = std::clamp(value, min, max); - } - - //checks if a is between b and c - template - bool isbetween(const t1 &value, const t2 &min, const t3 &max) - { - return value > (t1)min && value < (t1)max; - } - - //checks if a is within b and c - template - bool iswithin(const t1 &value, const t2 &min, const t3 &max) - { - return value >= (t1)min && value <= (t1)max; - } - - template - struct assigner - { - public: - t1 &op1; - t2 op2; - assigner(t1 &a1, const t2 & a2) - :op1(a1), op2(a2) - {} - void operator()() - { - op1 = op2; - } - }; - - //deleted pointer and sets it to nullptr - template - void clear_pointer(T* &ptr) - { - delete ptr; - ptr = nullptr; - } - - template - typename Container::const_reference circularAt(const Container &r, size_t index) - { - assert(r.size()); - index %= r.size(); - auto itr = std::begin(r); - std::advance(itr, index); - return *itr; - } - - template - void erase(Container &c, const Item &item) - { - c.erase(boost::remove(c, item), c.end()); - } - - template - void erase_if(Range &vec, Predicate pred) - { - vec.erase(boost::remove_if(vec, pred),vec.end()); - } - - template - void erase_if(std::set &setContainer, Predicate pred) - { - auto itr = setContainer.begin(); - auto endItr = setContainer.end(); - while(itr != endItr) - { - auto tmpItr = itr++; - if(pred(*tmpItr)) - setContainer.erase(tmpItr); - } - } - - //works for map and std::map, maybe something else - template - void erase_if(std::map &container, Predicate pred) - { - auto itr = container.begin(); - auto endItr = container.end(); - while(itr != endItr) - { - auto tmpItr = itr++; - if(pred(*tmpItr)) - container.erase(tmpItr); - } - } - - template - OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred) - { - return std::copy_if(std::cbegin(input), std::end(input), result, pred); - } - - template - std::insert_iterator set_inserter(Container &c) - { - return std::inserter(c, c.end()); - } - - //Returns iterator to the element for which the value of ValueFunction is minimal - template - auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) - { - /* Clang crashes when instantiating this function template and having PCH compilation enabled. - * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 - * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype - * directly for both function parameters. - */ - return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool - { - return vf(lhs) < vf(rhs); - }); - } - - //Returns iterator to the element for which the value of ValueFunction is maximal - template - auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) - { - /* Clang crashes when instantiating this function template and having PCH compilation enabled. - * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 - * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype - * directly for both function parameters. - */ - return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool - { - return vf(lhs) < vf(rhs); - }); - } - - /// Increments value by specific delta - /// similar to std::next but works with other types, e.g. enum class - template - T next(const T &obj, int change) - { - return static_cast(static_cast(obj) + change); - } - - template - typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers) - { - if(c.size()) - return c.back(); - else - return typename Container::value_type(); - } - - template - typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers) - { - if(c.size()) - return c.front(); - else - return nullptr; - } - - template - bool isValidIndex(const Container &c, Index i) - { - return i >= 0 && i < c.size(); - } - - template - typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue) - { - if(index < r.size()) - return r[index]; - - return defaultValue; - } - - template - bool erase_if_present(Container &c, const Item &item) - { - auto i = std::find(c.begin(), c.end(), item); - if (i != c.end()) - { - c.erase(i); - return true; - } - - return false; - } - - template - bool erase_if_present(std::map & c, const Item2 &item) - { - auto i = c.find(item); - if (i != c.end()) - { - c.erase(i); - return true; - } - return false; - } - - template - void removeDuplicates(std::vector &vec) - { - std::sort(vec.begin(), vec.end()); - vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); - } - - template - void concatenate(std::vector &dest, const std::vector &src) - { - dest.reserve(dest.size() + src.size()); - dest.insert(dest.end(), src.begin(), src.end()); - } - - template - std::vector intersection(std::vector &v1, std::vector &v2) - { - std::vector v3; - std::sort(v1.begin(), v1.end()); - std::sort(v2.begin(), v2.end()); - std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3)); - return v3; - } - - template - std::set difference(const std::set &s1, const std::set s2) - { - std::set s3; - std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end())); - return s3; - } - - template - bool containsMapping(const std::multimap & map, const std::pair & mapping) - { - auto range = map.equal_range(mapping.first); - for(auto contained = range.first; contained != range.second; contained++) - { - if(mapping.second == contained->second) - return true; - } - return false; - } - - template - typename M::mapped_type & getOrCompute(M & m, const Key & k, F f) - { - typedef typename M::mapped_type V; - - std::pair r = m.insert(typename M::value_type(k, V())); - V & v = r.first->second; - - if(r.second) - f(v); - - return v; - } - - //c++20 feature - template - Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f) - { - return a + (b - a) * f; - } - - ///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr - static constexpr int abs(int i) { - if(i < 0) return -i; - return i; - } - - ///C++23 - template< class Enum > constexpr std::underlying_type_t to_underlying( Enum e ) noexcept - { - return static_cast>(e); - } -} -using vstd::operator-=; - -VCMI_LIB_NAMESPACE_END +/* + * Global.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 + +/* ---------------------------------------------------------------------------- */ +/* Compiler detection */ +/* ---------------------------------------------------------------------------- */ +// Fixed width bool data type is important for serialization +static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size."); + +/* ---------------------------------------------------------------------------- */ +/* System detection. */ +/* ---------------------------------------------------------------------------- */ +// Based on: http://sourceforge.net/p/predef/wiki/OperatingSystems/ +// and on: http://stackoverflow.com/questions/5919996/how-to-detect-reliably-mac-os-x-ios-linux-windows-in-c-preprocessor +// TODO?: Should be moved to vstd\os_detect.h (and then included by Global.h) +#ifdef _WIN16 // Defined for 16-bit environments +# error "16-bit Windows isn't supported" +#elif defined(_WIN64) // Defined for 64-bit environments +# define VCMI_WINDOWS +# define VCMI_WINDOWS_64 +#elif defined(_WIN32) // Defined for both 32-bit and 64-bit environments +# define VCMI_WINDOWS +# define VCMI_WINDOWS_32 +#elif defined(_WIN32_WCE) +# error "Windows CE isn't supported" +#elif defined(__linux__) || defined(__gnu_linux__) || defined(linux) || defined(__linux) +# define VCMI_UNIX +# define VCMI_XDG +# if defined(__ANDROID__) || defined(ANDROID) +# define VCMI_ANDROID +# endif +#elif defined(__FreeBSD_kernel__) || defined(__FreeBSD__) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_FREEBSD +#elif defined(__HAIKU__) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_HAIKU +#elif defined(__GNU__) || defined(__gnu_hurd__) || (defined(__MACH__) && !defined(__APPLE__)) +# define VCMI_UNIX +# define VCMI_XDG +# define VCMI_HURD +#elif defined(__APPLE__) && defined(__MACH__) +# define VCMI_UNIX +# define VCMI_APPLE +# include "TargetConditionals.h" +# if TARGET_OS_SIMULATOR || TARGET_IPHONE_SIMULATOR +# define VCMI_IOS +# define VCMI_IOS_SIM +# elif TARGET_OS_IPHONE +# define VCMI_IOS +# elif TARGET_OS_MAC +# define VCMI_MAC +# else +//# warning "Unknown Apple target."? +# endif +#else +# error "This platform isn't supported" +#endif + +#if defined(VCMI_ANDROID) || defined(VCMI_IOS) +#define VCMI_MOBILE +#endif + +/* ---------------------------------------------------------------------------- */ +/* Commonly used C++, Boost headers */ +/* ---------------------------------------------------------------------------- */ +#ifdef VCMI_WINDOWS +# ifndef WIN32_LEAN_AND_MEAN +# define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers - delete this line if something is missing. +# endif +# ifndef NOMINMAX +# define NOMINMAX // Exclude min/max macros from . Use std::[min/max] from instead. +# endif +# ifndef _NO_W32_PSEUDO_MODIFIERS +# define _NO_W32_PSEUDO_MODIFIERS // Exclude more macros for compiling with MinGW on Linux. +# endif +#endif + +/* ---------------------------------------------------------------------------- */ +/* A macro to force inlining some of our functions */ +/* ---------------------------------------------------------------------------- */ +// Compiler (at least MSVC) is not so smart here-> without that displaying is MUCH slower +#ifdef _MSC_VER +# define STRONG_INLINE __forceinline +#elif __GNUC__ +# define STRONG_INLINE inline __attribute__((always_inline)) +#else +# define STRONG_INLINE inline +#endif + +#define _USE_MATH_DEFINES + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +//The only available version is 3, as of Boost 1.50 +#include + +#define BOOST_FILESYSTEM_VERSION 3 +#if BOOST_VERSION > 105000 +# define BOOST_THREAD_VERSION 3 +#endif +#define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1 +//need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary +#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically +#define BOOST_BIND_NO_PLACEHOLDERS + +#if BOOST_VERSION >= 106600 +#define BOOST_ASIO_ENABLE_OLD_SERVICES +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef VCMI_WINDOWS +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef M_PI +# define M_PI 3.14159265358979323846 +#endif + +/* ---------------------------------------------------------------------------- */ +/* Usings */ +/* ---------------------------------------------------------------------------- */ +using namespace std::placeholders; +namespace range = boost::range; + +/* ---------------------------------------------------------------------------- */ +/* Typedefs */ +/* ---------------------------------------------------------------------------- */ +// Integral data types +typedef uint64_t ui64; //unsigned int 64 bits (8 bytes) +typedef uint32_t ui32; //unsigned int 32 bits (4 bytes) +typedef uint16_t ui16; //unsigned int 16 bits (2 bytes) +typedef uint8_t ui8; //unsigned int 8 bits (1 byte) +typedef int64_t si64; //signed int 64 bits (8 bytes) +typedef int32_t si32; //signed int 32 bits (4 bytes) +typedef int16_t si16; //signed int 16 bits (2 bytes) +typedef int8_t si8; //signed int 8 bits (1 byte) + +// Lock typedefs +using TLockGuard = std::lock_guard; +using TLockGuardRec = std::lock_guard; + +/* ---------------------------------------------------------------------------- */ +/* Macros */ +/* ---------------------------------------------------------------------------- */ +// Import + Export macro declarations +#ifdef VCMI_WINDOWS +#ifdef VCMI_DLL_STATIC +# define DLL_IMPORT +# define DLL_EXPORT +#elif defined(__GNUC__) +# define DLL_IMPORT __attribute__((dllimport)) +# define DLL_EXPORT __attribute__((dllexport)) +# else +# define DLL_IMPORT __declspec(dllimport) +# define DLL_EXPORT __declspec(dllexport) +# endif +# define ELF_VISIBILITY +#else +# ifdef __GNUC__ +# define DLL_IMPORT __attribute__ ((visibility("default"))) +# define DLL_EXPORT __attribute__ ((visibility("default"))) +# define ELF_VISIBILITY __attribute__ ((visibility("default"))) +# endif +#endif + +#ifdef VCMI_DLL +# define DLL_LINKAGE DLL_EXPORT +#else +# define DLL_LINKAGE DLL_IMPORT +#endif + +#define THROW_FORMAT(message, formatting_elems) throw std::runtime_error(boost::str(boost::format(message) % formatting_elems)) + +// old iOS SDKs compatibility +#ifdef VCMI_IOS +#include + +#ifndef __IPHONE_13_0 +#define __IPHONE_13_0 130000 +#endif +#endif // VCMI_IOS + +// single-process build makes 2 copies of the main lib by wrapping it in a namespace +#ifdef VCMI_LIB_NAMESPACE +#define VCMI_LIB_NAMESPACE_BEGIN namespace VCMI_LIB_NAMESPACE { +#define VCMI_LIB_NAMESPACE_END } +#define VCMI_LIB_USING_NAMESPACE using namespace VCMI_LIB_NAMESPACE; +#define VCMI_LIB_WRAP_NAMESPACE(x) VCMI_LIB_NAMESPACE::x +#else +#define VCMI_LIB_NAMESPACE_BEGIN +#define VCMI_LIB_NAMESPACE_END +#define VCMI_LIB_USING_NAMESPACE +#define VCMI_LIB_WRAP_NAMESPACE(x) ::x +#endif + +/* ---------------------------------------------------------------------------- */ +/* VCMI standard library */ +/* ---------------------------------------------------------------------------- */ +#include + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + // combine hashes. Present in boost but not in std + template + inline void hash_combine(std::size_t& seed, const T& v) + { + std::hash hasher; + seed ^= hasher(v) + 0x9e3779b9 + (seed<<6) + (seed>>2); + } + + //returns true if container c contains item i + template + bool contains(const Container & c, const Item &i) + { + return std::find(std::begin(c), std::end(c),i) != std::end(c); + } + + //returns true if container c contains item i + template + bool contains_if(const Container & c, Pred p) + { + return std::find_if(std::begin(c), std::end(c), p) != std::end(c); + } + + //returns true if map c contains item i + template + bool contains(const std::map & c, const Item2 &i) + { + return c.find(i)!=c.end(); + } + + //returns true if unordered set c contains item i + template + bool contains(const std::unordered_set & c, const Item &i) + { + return c.find(i)!=c.end(); + } + + template + bool contains(const std::unordered_map & c, const Item2 &i) + { + return c.find(i)!=c.end(); + } + + //returns position of first element in vector c equal to s, if there is no such element, -1 is returned + template + int find_pos(const Container & c, const T2 &s) + { + int i=0; + for (auto iter = std::begin(c); iter != std::end(c); iter++, i++) + if(*iter == s) + return i; + return -1; + } + + //Func f tells if element matches + template + int find_pos_if(const Container & c, const Func &f) + { + auto ret = boost::range::find_if(c, f); + if(ret != std::end(c)) + return std::distance(std::begin(c), ret); + + return -1; + } + + //returns iterator to the given element if present in container, end() if not + template + typename Container::iterator find(Container & c, const Item &i) + { + return std::find(c.begin(),c.end(),i); + } + + //returns const iterator to the given element if present in container, end() if not + template + typename Container::const_iterator find(const Container & c, const Item &i) + { + return std::find(c.begin(),c.end(),i); + } + + //returns first key that maps to given value if present, returns success via found if provided + template + Key findKey(const std::map & map, const T & value, bool * found = nullptr) + { + for(auto iter = map.cbegin(); iter != map.cend(); iter++) + { + if(iter->second == value) + { + if(found) + *found = true; + return iter->first; + } + } + if(found) + *found = false; + return Key(); + } + + //removes element i from container c, returns false if c does not contain i + template + typename Container::size_type operator-=(Container &c, const Item &i) + { + typename Container::iterator itr = find(c,i); + if(itr == c.end()) + return false; + c.erase(itr); + return true; + } + + //assigns greater of (a, b) to a and returns maximum of (a, b) + template + t1 &amax(t1 &a, const t2 &b) + { + if(a >= (t1)b) + return a; + else + { + a = t1(b); + return a; + } + } + + //assigns smaller of (a, b) to a and returns minimum of (a, b) + template + t1 &amin(t1 &a, const t2 &b) + { + if(a <= (t1)b) + return a; + else + { + a = t1(b); + return a; + } + } + + //makes a to fit the range + template + void abetween(T &value, const T &min, const T &max) + { + value = std::clamp(value, min, max); + } + + //checks if a is between b and c + template + bool isbetween(const t1 &value, const t2 &min, const t3 &max) + { + return value > (t1)min && value < (t1)max; + } + + //checks if a is within b and c + template + bool iswithin(const t1 &value, const t2 &min, const t3 &max) + { + return value >= (t1)min && value <= (t1)max; + } + + template + struct assigner + { + public: + t1 &op1; + t2 op2; + assigner(t1 &a1, const t2 & a2) + :op1(a1), op2(a2) + {} + void operator()() + { + op1 = op2; + } + }; + + //deleted pointer and sets it to nullptr + template + void clear_pointer(T* &ptr) + { + delete ptr; + ptr = nullptr; + } + + template + typename Container::const_reference circularAt(const Container &r, size_t index) + { + assert(r.size()); + index %= r.size(); + auto itr = std::begin(r); + std::advance(itr, index); + return *itr; + } + + template + void erase(Container &c, const Item &item) + { + c.erase(boost::remove(c, item), c.end()); + } + + template + void erase_if(Range &vec, Predicate pred) + { + vec.erase(boost::remove_if(vec, pred),vec.end()); + } + + template + void erase_if(std::set &setContainer, Predicate pred) + { + auto itr = setContainer.begin(); + auto endItr = setContainer.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + setContainer.erase(tmpItr); + } + } + + //works for map and std::map, maybe something else + template + void erase_if(std::map &container, Predicate pred) + { + auto itr = container.begin(); + auto endItr = container.end(); + while(itr != endItr) + { + auto tmpItr = itr++; + if(pred(*tmpItr)) + container.erase(tmpItr); + } + } + + template + OutputIterator copy_if(const InputRange &input, OutputIterator result, Predicate pred) + { + return std::copy_if(std::cbegin(input), std::end(input), result, pred); + } + + template + std::insert_iterator set_inserter(Container &c) + { + return std::inserter(c, c.end()); + } + + //Returns iterator to the element for which the value of ValueFunction is minimal + template + auto minElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) + { + /* Clang crashes when instantiating this function template and having PCH compilation enabled. + * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 + * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype + * directly for both function parameters. + */ + return boost::min_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool + { + return vf(lhs) < vf(rhs); + }); + } + + //Returns iterator to the element for which the value of ValueFunction is maximal + template + auto maxElementByFun(const ForwardRange& rng, ValueFunction vf) -> decltype(std::begin(rng)) + { + /* Clang crashes when instantiating this function template and having PCH compilation enabled. + * There is a bug report here: http://llvm.org/bugs/show_bug.cgi?id=18744 + * Current bugfix is to don't use a typedef for decltype(*std::begin(rng)) and to use decltype + * directly for both function parameters. + */ + return boost::max_element(rng, [&] (decltype(*std::begin(rng)) lhs, decltype(*std::begin(rng)) rhs) -> bool + { + return vf(lhs) < vf(rhs); + }); + } + + /// Increments value by specific delta + /// similar to std::next but works with other types, e.g. enum class + template + T next(const T &obj, int change) + { + return static_cast(static_cast(obj) + change); + } + + template + typename Container::value_type backOrNull(const Container &c) //returns last element of container or nullptr if it is empty (to be used with containers of pointers) + { + if(c.size()) + return c.back(); + else + return typename Container::value_type(); + } + + template + typename Container::value_type frontOrNull(const Container &c) //returns first element of container or nullptr if it is empty (to be used with containers of pointers) + { + if(c.size()) + return c.front(); + else + return nullptr; + } + + template + bool isValidIndex(const Container &c, Index i) + { + return i >= 0 && i < c.size(); + } + + template + typename Container::const_reference atOrDefault(const Container &r, size_t index, const typename Container::const_reference &defaultValue) + { + if(index < r.size()) + return r[index]; + + return defaultValue; + } + + template + bool erase_if_present(Container &c, const Item &item) + { + auto i = std::find(c.begin(), c.end(), item); + if (i != c.end()) + { + c.erase(i); + return true; + } + + return false; + } + + template + bool erase_if_present(std::map & c, const Item2 &item) + { + auto i = c.find(item); + if (i != c.end()) + { + c.erase(i); + return true; + } + return false; + } + + template + void removeDuplicates(std::vector &vec) + { + std::sort(vec.begin(), vec.end()); + vec.erase(std::unique(vec.begin(), vec.end()), vec.end()); + } + + template + void concatenate(std::vector &dest, const std::vector &src) + { + dest.reserve(dest.size() + src.size()); + dest.insert(dest.end(), src.begin(), src.end()); + } + + template + std::vector intersection(std::vector &v1, std::vector &v2) + { + std::vector v3; + std::sort(v1.begin(), v1.end()); + std::sort(v2.begin(), v2.end()); + std::set_intersection(v1.begin(), v1.end(), v2.begin(), v2.end(), std::back_inserter(v3)); + return v3; + } + + template + std::set difference(const std::set &s1, const std::set s2) + { + std::set s3; + std::set_difference(s1.begin(), s1.end(), s2.begin(), s2.end(), std::inserter(s3, s3.end())); + return s3; + } + + template + bool containsMapping(const std::multimap & map, const std::pair & mapping) + { + auto range = map.equal_range(mapping.first); + for(auto contained = range.first; contained != range.second; contained++) + { + if(mapping.second == contained->second) + return true; + } + return false; + } + + template + typename M::mapped_type & getOrCompute(M & m, const Key & k, F f) + { + typedef typename M::mapped_type V; + + std::pair r = m.insert(typename M::value_type(k, V())); + V & v = r.first->second; + + if(r.second) + f(v); + + return v; + } + + //c++20 feature + template + Arithmetic lerp(const Arithmetic & a, const Arithmetic & b, const Floating & f) + { + return a + (b - a) * f; + } + + ///compile-time version of std::abs for ints for int3, in clang++15 std::abs is constexpr + static constexpr int abs(int i) { + if(i < 0) return -i; + return i; + } + + ///C++23 + template< class Enum > constexpr std::underlying_type_t to_underlying( Enum e ) noexcept + { + return static_cast>(e); + } +} +using vstd::operator-=; + +VCMI_LIB_NAMESPACE_END diff --git a/Mods/vcmi/Data/s/std.verm b/Mods/vcmi/Data/s/std.verm index 865997ce7..54c08dc57 100755 --- a/Mods/vcmi/Data/s/std.verm +++ b/Mods/vcmi/Data/s/std.verm @@ -1,40 +1,40 @@ -VERM -; standard verm file, global engine things should be put here - -!?PI; -; example 1 --- Hello World -![print ^Hello world!^] - -; example 2 --- simple arithmetics -![defun add [x y] [+ x y]] -![print [add 2 3]] - -; example 3 --- semantic macros -![defmacro do-n-times [times body] - `[progn - [setq do-counter 0] - [setq do-max ,times] - [do [< do-counter do-max] - [progn - [setq do-counter [+ do-counter 1]] - ,body - ] - ] - ] -] -![do-n-times 4 [print ^tekst\n^]] - - -; example 4 --- conditional expression -![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]] - -; example 5 --- lambda expressions -![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3] - -; example 6 --- resursion -![defun factorial [n] - [if [= n 0] 1 - [* n [factorial [- n 1]]] - ] -] +VERM +; standard verm file, global engine things should be put here + +!?PI; +; example 1 --- Hello World +![print ^Hello world!^] + +; example 2 --- simple arithmetics +![defun add [x y] [+ x y]] +![print [add 2 3]] + +; example 3 --- semantic macros +![defmacro do-n-times [times body] + `[progn + [setq do-counter 0] + [setq do-max ,times] + [do [< do-counter do-max] + [progn + [setq do-counter [+ do-counter 1]] + ,body + ] + ] + ] +] +![do-n-times 4 [print ^tekst\n^]] + + +; example 4 --- conditional expression +![if [> 2 1] [print ^Wieksze^] [print ^Mniejsze^]] + +; example 5 --- lambda expressions +![[lambda [x y] [if [> x y] [print ^wieksze^] [print ^mniejsze^]]] 2 3] + +; example 6 --- resursion +![defun factorial [n] + [if [= n 0] 1 + [* n [factorial [- n 1]]] + ] +] ![print [factorial 8]] \ No newline at end of file diff --git a/Mods/vcmi/Data/s/testy.erm b/Mods/vcmi/Data/s/testy.erm index 082c1891e..051c27a19 100755 --- a/Mods/vcmi/Data/s/testy.erm +++ b/Mods/vcmi/Data/s/testy.erm @@ -1,14 +1,14 @@ -ZVSE -!?PI; - !!VRv2777:S4; - !!DO1/0/5/1&v2777<>1:P0; - -!?FU1; - !!VRv2778:Sx16%2; - !!IF&x16>3:M^Hello world number %X16! To duza liczba^; - !!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^; - !!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^; - -!?PI; - !!VRz10:S^Composed hello ^; +ZVSE +!?PI; + !!VRv2777:S4; + !!DO1/0/5/1&v2777<>1:P0; + +!?FU1; + !!VRv2778:Sx16%2; + !!IF&x16>3:M^Hello world number %X16! To duza liczba^; + !!IF&v2778==0&x16<=3:M^Hello world number %X16! To mala parzysta liczba^; + !!IF&v2778==1&x16<=3:M^Hello world number %X16! To mala nieparzysta liczba^; + +!?PI; + !!VRz10:S^Composed hello ^; !!IF:M^%Z10%%world%%, v2777=%V2777, v2778=%V2778!^; \ No newline at end of file diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 727df4ef9..4c1cbdb79 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -1,369 +1,369 @@ -{ - "vcmi.adventureMap.monsterThreat.title" : "\n\n Bedrohung: ", - "vcmi.adventureMap.monsterThreat.levels.0" : "Mühelos", - "vcmi.adventureMap.monsterThreat.levels.1" : "Sehr schwach", - "vcmi.adventureMap.monsterThreat.levels.2" : "Schwach", - "vcmi.adventureMap.monsterThreat.levels.3" : "Ein bisschen schwächer", - "vcmi.adventureMap.monsterThreat.levels.4" : "Gleichauf", - "vcmi.adventureMap.monsterThreat.levels.5" : "Ein bisschen stärker", - "vcmi.adventureMap.monsterThreat.levels.6" : "Stark", - "vcmi.adventureMap.monsterThreat.levels.7" : "Sehr Stark", - "vcmi.adventureMap.monsterThreat.levels.8" : "Herausfordernd", - "vcmi.adventureMap.monsterThreat.levels.9" : "Überwältigend", - "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", - "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", - - "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", - "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", - "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", - "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", - "vcmi.adventureMap.playerAttacked" : "Spieler wurde attackiert: %s", - "vcmi.adventureMap.moveCostDetails" : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING", - "vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING", - - "vcmi.capitalColors.0" : "Rot", - "vcmi.capitalColors.1" : "Blau", - "vcmi.capitalColors.2" : "Braun", - "vcmi.capitalColors.3" : "Grün", - "vcmi.capitalColors.4" : "Orange", - "vcmi.capitalColors.5" : "Violett", - "vcmi.capitalColors.6" : "Türkis", - "vcmi.capitalColors.7" : "Rosa", - - "vcmi.heroOverview.startingArmy" : "Starteinheiten", - "vcmi.heroOverview.warMachine" : "Kriegsmaschinen", - "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", - "vcmi.heroOverview.spells" : "Zaubersprüche", - - "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", - "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", - "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", - "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", - "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", - - "vcmi.mainMenu.serverConnecting" : "Verbinde...", - "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", - "vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen", - "vcmi.mainMenu.serverClosing" : "Trenne...", - "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", - "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", - "vcmi.mainMenu.playerName" : "Spieler", - - "vcmi.lobby.filepath" : "Dateipfad", - "vcmi.lobby.creationDate" : "Erstellungsdatum", - "vcmi.lobby.scenarioName" : "Szenario-Name", - "vcmi.lobby.mapPreview" : "Kartenvorschau", - "vcmi.lobby.noPreview" : "Keine Vorschau", - "vcmi.lobby.noUnderground" : "Kein Untergrund", - - "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", - "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", - "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", - - "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", - "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", - "vcmi.settingsMainWindow.battleTab.hover" : "Kampf", - "vcmi.settingsMainWindow.battleTab.help" : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.", - "vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte", - "vcmi.settingsMainWindow.adventureTab.help" : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.", - - "vcmi.systemOptions.videoGroup" : "Video-Einstellungen", - "vcmi.systemOptions.audioGroup" : "Audio-Einstellungen", - "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now - "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", - - "vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)", - "vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.", - "vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)", - "vcmi.systemOptions.fullscreenExclusive.help" : "{Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im exklusiven Vollbildmodus ausgeführt. In diesem Modus ändert das Spiel die Auflösung des Monitors auf die ausgewählte Auflösung.", - "vcmi.systemOptions.resolutionButton.hover" : "Auflösung: %wx%h", - "vcmi.systemOptions.resolutionButton.help" : "{Wähle Auflösung}\n\n Ändert die Spielauflösung.", - "vcmi.systemOptions.resolutionMenu.hover" : "Wähle Auflösung", - "vcmi.systemOptions.resolutionMenu.help" : "Ändere die Spielauflösung.", - "vcmi.systemOptions.scalingButton.hover" : "Interface-Skalierung: %p%", - "vcmi.systemOptions.scalingButton.help" : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel", - "vcmi.systemOptions.scalingMenu.hover" : "Skalierung des Interfaces auswählen", - "vcmi.systemOptions.scalingMenu.help" : "Ändern der Skalierung des Interfaces im Spiel.", - "vcmi.systemOptions.longTouchButton.hover" : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" - "vcmi.systemOptions.longTouchButton.help" : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", - "vcmi.systemOptions.longTouchMenu.hover" : "Wähle Berührungsdauer für langer Touch", - "vcmi.systemOptions.longTouchMenu.help" : "Ändere die Berührungsdauer für den langen Touch", - "vcmi.systemOptions.longTouchMenu.entry" : "%d Millisekunden", - "vcmi.systemOptions.framerateButton.hover" : "FPS anzeigen", - "vcmi.systemOptions.framerateButton.help" : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.", - "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", - "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", - "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", - - "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", - "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", - "vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen", - "vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.", - "vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen", - "vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.", - "vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen", - "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", - "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", - "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", - "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", - "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", - "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", - "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", - "vcmi.adventureOptions.mapScrollSpeed1.hover": "", - "vcmi.adventureOptions.mapScrollSpeed5.hover": "", - "vcmi.adventureOptions.mapScrollSpeed6.hover": "", - "vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen", - "vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen", - "vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen", - - "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", - "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", - "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", - "vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN", - "vcmi.battleOptions.queueSizeBigButton.hover": "GROß", - "vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf", - "vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)", - "vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein", - "vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)", - "vcmi.battleOptions.animationsSpeed1.hover": "", - "vcmi.battleOptions.animationsSpeed5.hover": "", - "vcmi.battleOptions.animationsSpeed6.hover": "", - "vcmi.battleOptions.animationsSpeed1.help": "Setzt die Animationsgeschwindigkeit auf sehr langsam", - "vcmi.battleOptions.animationsSpeed5.help": "Setzt die Animationsgeschwindigkeit auf sehr schnell", - "vcmi.battleOptions.animationsSpeed6.help": "Setzt die Animationsgeschwindigkeit auf sofort", - "vcmi.battleOptions.movementHighlightOnHover.hover": "Hervorhebung der Bewegung bei Hover", - "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", - "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", - "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", - "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen", - "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", - "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", - "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", - - "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", - "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", - "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", - "vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).", - "vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend", - "vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend", - "vcmi.battleWindow.damageEstimation.damage" : "%d Schaden", - "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", - "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", - "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", - - "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", - - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", - "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", - "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Wöchentliches Wachstum der Kreaturen anzeigen}\n\n Zeigt das wöchentliche Wachstum der Kreaturen anstelle der verfügbaren Menge in der Stadtübersicht (unten links).", - "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakte Kreatur-Infos", - "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakte Kreatur-Infos}\n\n Kleinere Stadt-Kreaturen Informationen in der Stadtübersicht.", - - "vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden", - "vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!", - "vcmi.townHall.greetingManaVortex" : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.", - "vcmi.townHall.greetingKnowledge" : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).", - "vcmi.townHall.greetingSpellPower" : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).", - "vcmi.townHall.greetingExperience" : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).", - "vcmi.townHall.greetingAttack" : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).", - "vcmi.townHall.greetingDefence" : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).", - "vcmi.townHall.hasNotProduced" : "Die %s hat noch nichts produziert.", - "vcmi.townHall.hasProduced" : "Die %s hat diese Woche %d %s produziert.", - "vcmi.townHall.greetingCustomBonus" : "%s gibt Ihnen +%d %s%s", - "vcmi.townHall.greetingCustomUntil" : " bis zur nächsten Schlacht.", - "vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.", - - "vcmi.logicalExpressions.anyOf" : "Eines der folgenden:", - "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", - "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", - - "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", - "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", - - "vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", - - "vcmi.creatureWindow.showBonuses.hover" : "Wechsle zur Bonus-Ansicht", - "vcmi.creatureWindow.showBonuses.help" : "Zeige alle aktiven Boni des Kommandanten", - "vcmi.creatureWindow.showSkills.hover" : "Wechsle zur Fertigkeits-Ansicht", - "vcmi.creatureWindow.showSkills.help" : "Zeige alle erlernten Fertigkeiten des Kommandanten", - "vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben", - "vcmi.creatureWindow.returnArtifact.help" : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben", - - "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", - "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", - - "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)", - "vcmi.randomMapTab.widgets.templateLabel" : "Template", - "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", - "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", - "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", - - // Custom victory conditions for H3 campaigns and HotA maps - "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", - "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", - "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!", - "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!", - "vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte", - "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!", - "vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz", - - // few strings from WoG used by vcmi - "vcmi.stackExperience.description" : "» D e t a i l s z u r S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i", - "vcmi.stackExperience.rank.0" : "Grundlagen", - "vcmi.stackExperience.rank.1" : "Neuling", - "vcmi.stackExperience.rank.2" : "Ausgebildet", - "vcmi.stackExperience.rank.3" : "Kompetent", - "vcmi.stackExperience.rank.4" : "Bewährt", - "vcmi.stackExperience.rank.5" : "Veteran", - "vcmi.stackExperience.rank.6" : "Gekonnt", - "vcmi.stackExperience.rank.7" : "Experte", - "vcmi.stackExperience.rank.8" : "Elite", - "vcmi.stackExperience.rank.9" : "Meister", - "vcmi.stackExperience.rank.10" : "Ass", - - "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", - "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", - "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", - "core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten", - "core.bonus.AIR_IMMUNITY.name": "Luftimmunität", - "core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber", - "core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen", - "core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an", - "core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung", - "core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten", - "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung", - "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten", - "core.bonus.CATAPULT.name": "Katapult", - "core.bonus.CATAPULT.description": "Greift Belagerungsmauern an", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})", - "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern", - "core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung", - "core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung", - "core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit", - "core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu", - "core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)", - "core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten", - "core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus", - "core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen", - "core.bonus.DESTRUCTION.name": "Zerstörung", - "core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten", - "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß", - "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% Chance auf doppelten Schaden", - "core.bonus.DRAGON_NATURE.name": "Drache", - "core.bonus.DRAGON_NATURE.description": "Kreatur hat eine Drachennatur", - "core.bonus.EARTH_IMMUNITY.name": "Erdimmunität", - "core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule", - "core.bonus.ENCHANTER.name": "Verzauberer", - "core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern", - "core.bonus.ENCHANTED.name": "Verzaubert", - "core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}", - "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)", - "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff", - "core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität", - "core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers", - "core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)", - "core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens", - "core.bonus.FIRST_STRIKE.name": "Erstschlag", - "core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten", - "core.bonus.FEAR.name": "Furcht", - "core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel", - "core.bonus.FEARLESS.name": "Furchtlos", - "core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht", - "core.bonus.FLYING.name": "Fliegen", - "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", - "core.bonus.FREE_SHOOTING.name": "Nah schießen", - "core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen", - "core.bonus.GARGOYLE.name": "Gargoyle", - "core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden", - "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)", - "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf", - "core.bonus.HATE.name": "Hasst ${subtype.creature}", - "core.bonus.HATE.description": "Macht ${val}% mehr Schaden", - "core.bonus.HEALER.name": "Heiler", - "core.bonus.HEALER.description": "Heilt verbündete Einheiten", - "core.bonus.HP_REGENERATION.name": "Regeneration", - "core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde", - "core.bonus.JOUSTING.name": "Champion Charge", - "core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld", - "core.bonus.KING.name": "König", - "core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher", - "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}", - "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}", - "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite", - "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind", - "core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)", - "core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens", - "core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%", - "core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird", - "core.bonus.MANA_DRAIN.name": "Mana-Entzug", - "core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde", - "core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)", - "core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken", - "core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)", - "core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen", - "core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität", - "core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist", - "core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe", - "core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung", - "core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe", - "core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus", - "core.bonus.NO_MORALE.name": "Neutrale Moral", - "core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte", - "core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe", - "core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung", - "core.bonus.NON_LIVING.name": "Nicht lebend", - "core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte", - "core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker", - "core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken", - "core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung", - "core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen", - "core.bonus.RECEPTIVE.name": "Empfänglich", - "core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber", - "core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)", - "core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen", - "core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr", - "core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück", - "core.bonus.SHOOTER.name": "Fernkämpfer", - "core.bonus.SHOOTER.description": "Kreatur kann schießen", - "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", - "core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich", - "core.bonus.SOUL_STEAL.name": "Seelenraub", - "core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner", - "core.bonus.SPELLCASTER.name": "Zauberer", - "core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern", - "core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern", - "core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken", - "core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff", - "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken", - "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand", - "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.", - "core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität", - "core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}", - "core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff", - "core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}", - "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands", - "core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand", - "core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören", - "core.bonus.SUMMON_GUARDIANS.description": "Beschwört bei Kampfbeginn ${subtype.creature} (${val}%)", - "core.bonus.SYNERGY_TARGET.name": "Synergierbar", - "core.bonus.SYNERGY_TARGET.description": "Diese Kreatur ist anfällig für Synergieeffekte", - "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Atem", - "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Atem-Angriff (2-Hex-Bereich)", - "core.bonus.THREE_HEADED_ATTACK.name": "Dreiköpfiger Angriff", - "core.bonus.THREE_HEADED_ATTACK.description": "Greift drei benachbarte Einheiten an", - "core.bonus.TRANSMUTATION.name": "Transmutation", - "core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln", - "core.bonus.UNDEAD.name": "Untot", - "core.bonus.UNDEAD.description": "Kreatur ist untot", - "core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen", - "core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen", - "core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität", - "core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule", - "core.bonus.WIDE_BREATH.name": "Breiter Atem", - "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)" -} +{ + "vcmi.adventureMap.monsterThreat.title" : "\n\n Bedrohung: ", + "vcmi.adventureMap.monsterThreat.levels.0" : "Mühelos", + "vcmi.adventureMap.monsterThreat.levels.1" : "Sehr schwach", + "vcmi.adventureMap.monsterThreat.levels.2" : "Schwach", + "vcmi.adventureMap.monsterThreat.levels.3" : "Ein bisschen schwächer", + "vcmi.adventureMap.monsterThreat.levels.4" : "Gleichauf", + "vcmi.adventureMap.monsterThreat.levels.5" : "Ein bisschen stärker", + "vcmi.adventureMap.monsterThreat.levels.6" : "Stark", + "vcmi.adventureMap.monsterThreat.levels.7" : "Sehr Stark", + "vcmi.adventureMap.monsterThreat.levels.8" : "Herausfordernd", + "vcmi.adventureMap.monsterThreat.levels.9" : "Überwältigend", + "vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich", + "vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich", + + "vcmi.adventureMap.confirmRestartGame" : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?", + "vcmi.adventureMap.noTownWithMarket" : "Kein Marktplatz verfügbar!", + "vcmi.adventureMap.noTownWithTavern" : "Keine Stadt mit Taverne verfügbar!", + "vcmi.adventureMap.spellUnknownProblem" : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.", + "vcmi.adventureMap.playerAttacked" : "Spieler wurde attackiert: %s", + "vcmi.adventureMap.moveCostDetails" : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING", + "vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING", + + "vcmi.capitalColors.0" : "Rot", + "vcmi.capitalColors.1" : "Blau", + "vcmi.capitalColors.2" : "Braun", + "vcmi.capitalColors.3" : "Grün", + "vcmi.capitalColors.4" : "Orange", + "vcmi.capitalColors.5" : "Violett", + "vcmi.capitalColors.6" : "Türkis", + "vcmi.capitalColors.7" : "Rosa", + + "vcmi.heroOverview.startingArmy" : "Starteinheiten", + "vcmi.heroOverview.warMachine" : "Kriegsmaschinen", + "vcmi.heroOverview.secondarySkills" : "Sekundäre Skills", + "vcmi.heroOverview.spells" : "Zaubersprüche", + + "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", + "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", + "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", + "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", + "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", + + "vcmi.mainMenu.serverConnecting" : "Verbinde...", + "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", + "vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen", + "vcmi.mainMenu.serverClosing" : "Trenne...", + "vcmi.mainMenu.hostTCP" : "Hoste TCP/IP Spiel", + "vcmi.mainMenu.joinTCP" : "Trete TCP/IP Spiel bei", + "vcmi.mainMenu.playerName" : "Spieler", + + "vcmi.lobby.filepath" : "Dateipfad", + "vcmi.lobby.creationDate" : "Erstellungsdatum", + "vcmi.lobby.scenarioName" : "Szenario-Name", + "vcmi.lobby.mapPreview" : "Kartenvorschau", + "vcmi.lobby.noPreview" : "Keine Vorschau", + "vcmi.lobby.noUnderground" : "Kein Untergrund", + + "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", + "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", + "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + + "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", + "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", + "vcmi.settingsMainWindow.battleTab.hover" : "Kampf", + "vcmi.settingsMainWindow.battleTab.help" : "Wechselt zur Registerkarte Kampfoptionen, auf der das Spielverhalten während eines Kampfes konfiguriert werden kann.", + "vcmi.settingsMainWindow.adventureTab.hover" : "Abenteuer-Karte", + "vcmi.settingsMainWindow.adventureTab.help" : "Wechselt zur Registerkarte Abenteuerkartenoptionen - die Abenteuerkarte ist der Teil des Spiels, in dem du deine Helden bewegen kannst.", + + "vcmi.systemOptions.videoGroup" : "Video-Einstellungen", + "vcmi.systemOptions.audioGroup" : "Audio-Einstellungen", + "vcmi.systemOptions.otherGroup" : "Andere Einstellungen", // unused right now + "vcmi.systemOptions.townsGroup" : "Stadt-Bildschirm", + + "vcmi.systemOptions.fullscreenBorderless.hover" : "Vollbild (randlos)", + "vcmi.systemOptions.fullscreenBorderless.help" : "{Randloses Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im randlosen Vollbildmodus ausgeführt. In diesem Modus wird das Spiel immer dieselbe Auflösung wie der Desktop verwenden und die gewählte Auflösung ignorieren.", + "vcmi.systemOptions.fullscreenExclusive.hover" : "Vollbild (exklusiv)", + "vcmi.systemOptions.fullscreenExclusive.help" : "{Vollbild}\n\nWenn diese Option ausgewählt ist, wird VCMI im exklusiven Vollbildmodus ausgeführt. In diesem Modus ändert das Spiel die Auflösung des Monitors auf die ausgewählte Auflösung.", + "vcmi.systemOptions.resolutionButton.hover" : "Auflösung: %wx%h", + "vcmi.systemOptions.resolutionButton.help" : "{Wähle Auflösung}\n\n Ändert die Spielauflösung.", + "vcmi.systemOptions.resolutionMenu.hover" : "Wähle Auflösung", + "vcmi.systemOptions.resolutionMenu.help" : "Ändere die Spielauflösung.", + "vcmi.systemOptions.scalingButton.hover" : "Interface-Skalierung: %p%", + "vcmi.systemOptions.scalingButton.help" : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel", + "vcmi.systemOptions.scalingMenu.hover" : "Skalierung des Interfaces auswählen", + "vcmi.systemOptions.scalingMenu.help" : "Ändern der Skalierung des Interfaces im Spiel.", + "vcmi.systemOptions.longTouchButton.hover" : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds" + "vcmi.systemOptions.longTouchButton.help" : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)", + "vcmi.systemOptions.longTouchMenu.hover" : "Wähle Berührungsdauer für langer Touch", + "vcmi.systemOptions.longTouchMenu.help" : "Ändere die Berührungsdauer für den langen Touch", + "vcmi.systemOptions.longTouchMenu.entry" : "%d Millisekunden", + "vcmi.systemOptions.framerateButton.hover" : "FPS anzeigen", + "vcmi.systemOptions.framerateButton.help" : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.", + "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", + "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + + "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", + "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", + "vcmi.adventureOptions.numericQuantities.hover" : "Numerische Kreaturenmengen", + "vcmi.adventureOptions.numericQuantities.help" : "{Numerische Kreaturenmengen}\n\n Zeigt die ungefähre Menge der feindlichen Kreaturen im numerischen Format A-B an.", + "vcmi.adventureOptions.forceMovementInfo.hover" : "Bewegungskosten immer anzeigen", + "vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.", + "vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen", + "vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.", + "vcmi.adventureOptions.borderScroll.hover" : "Scrollen am Rand", + "vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.", + "vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement", + "vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln", + "vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links", + "vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen", + "vcmi.adventureOptions.mapScrollSpeed1.hover": "", + "vcmi.adventureOptions.mapScrollSpeed5.hover": "", + "vcmi.adventureOptions.mapScrollSpeed6.hover": "", + "vcmi.adventureOptions.mapScrollSpeed1.help": "Geschwindigkeit des Kartenbildlaufs auf sehr langsam einstellen", + "vcmi.adventureOptions.mapScrollSpeed5.help": "Geschwindigkeit des Kartenbildlaufs auf sehr schnell einstellen", + "vcmi.adventureOptions.mapScrollSpeed6.help": "Geschwindigkeit des Kartenbildlaufs auf sofort einstellen", + + "vcmi.battleOptions.queueSizeLabel.hover": "Reihenfolge der Kreaturen anzeigen", + "vcmi.battleOptions.queueSizeNoneButton.hover": "AUS", + "vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO", + "vcmi.battleOptions.queueSizeSmallButton.hover": "KLEIN", + "vcmi.battleOptions.queueSizeBigButton.hover": "GROß", + "vcmi.battleOptions.queueSizeNoneButton.help": "Vollständige Deaktivierung der Sichtbarkeit der Reihenfolge der Kreaturen im Kampf", + "vcmi.battleOptions.queueSizeAutoButton.help": "Stellt die Größe der Zugreihenfolge abhängig von der Spielauflösung ein (klein, wenn mit einer Bildschirmauflösung unter 700 Pixeln gespielt wird, ansonsten groß)", + "vcmi.battleOptions.queueSizeSmallButton.help": "Setzt die Zugreihenfolge auf klein", + "vcmi.battleOptions.queueSizeBigButton.help": "Setzt die Größe der Zugreihenfolge auf groß (nicht unterstützt, wenn die Spielauflösung weniger als 700 Pixel hoch ist)", + "vcmi.battleOptions.animationsSpeed1.hover": "", + "vcmi.battleOptions.animationsSpeed5.hover": "", + "vcmi.battleOptions.animationsSpeed6.hover": "", + "vcmi.battleOptions.animationsSpeed1.help": "Setzt die Animationsgeschwindigkeit auf sehr langsam", + "vcmi.battleOptions.animationsSpeed5.help": "Setzt die Animationsgeschwindigkeit auf sehr schnell", + "vcmi.battleOptions.animationsSpeed6.help": "Setzt die Animationsgeschwindigkeit auf sofort", + "vcmi.battleOptions.movementHighlightOnHover.hover": "Hervorhebung der Bewegung bei Hover", + "vcmi.battleOptions.movementHighlightOnHover.help": "{Hervorhebung der Bewegung bei Hover}\n\nHebt die Bewegungsreichweite der Einheit hervor, wenn man mit dem Mauszeiger über sie fährt.", + "vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Bereichsgrenzen für Schützen anzeigen", + "vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Bereichsgrenzen für Schützen anzeigen}\n\nZeigt die Entfernungsgrenzen des Schützen an, wenn man mit dem Mauszeiger über ihn fährt.", + "vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Statistikfenster für Helden anzeigen", + "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", + "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", + "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", + + "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", + "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", + "vcmi.battleWindow.damageEstimation.meleeKills" : "Angriff auf %CREATURE (%DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.ranged" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE).", + "vcmi.battleWindow.damageEstimation.rangedKills" : "Schuss auf %CREATURE (%SHOTS, %DAMAGE, %KILLS).", + "vcmi.battleWindow.damageEstimation.shots" : "%d Schüsse verbleibend", + "vcmi.battleWindow.damageEstimation.shots.1" : "%d Schüsse verbleibend", + "vcmi.battleWindow.damageEstimation.damage" : "%d Schaden", + "vcmi.battleWindow.damageEstimation.damage.1" : "%d Schaden", + "vcmi.battleWindow.damageEstimation.kills" : "%d werden verenden", + "vcmi.battleWindow.damageEstimation.kills.1" : "%d werden verenden", + + "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", + + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", + "vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Wöchentliches Wachstum der Kreaturen anzeigen}\n\n Zeigt das wöchentliche Wachstum der Kreaturen anstelle der verfügbaren Menge in der Stadtübersicht (unten links).", + "vcmi.otherOptions.compactTownCreatureInfo.hover": "Kompakte Kreatur-Infos", + "vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompakte Kreatur-Infos}\n\n Kleinere Stadt-Kreaturen Informationen in der Stadtübersicht.", + + "vcmi.townHall.missingBase" : "Basis Gebäude %s muss als erstes gebaut werden", + "vcmi.townHall.noCreaturesToRecruit" : "Es gibt keine Kreaturen zu rekrutieren!", + "vcmi.townHall.greetingManaVortex" : "Wenn Ihr Euch den %s nähert, wird Euer Körper mit neuer Energie gefüllt. Ihr habt Eure normalen Zauberpunkte verdoppelt.", + "vcmi.townHall.greetingKnowledge" : "Ihr studiert die Glyphen auf dem %s und erhaltet Einblick in die Funktionsweise verschiedener Magie (+1 Wissen).", + "vcmi.townHall.greetingSpellPower" : "Der %s lehrt Euch neue Wege, Eure magischen Kräfte zu bündeln (+1 Kraft).", + "vcmi.townHall.greetingExperience" : "Ein Besuch bei den %s bringt Euch viele neue Fähigkeiten bei (+1000 Erfahrung).", + "vcmi.townHall.greetingAttack" : "Nach einiger Zeit im %s könnt Ihr effizientere Kampffertigkeiten erlernen (+1 Angriffsfertigkeit).", + "vcmi.townHall.greetingDefence" : "Wenn Ihr Zeit im %s verbringt, bringen Euch die erfahrenen Krieger dort zusätzliche Verteidigungsfähigkeiten bei (+1 Verteidigung).", + "vcmi.townHall.hasNotProduced" : "Die %s hat noch nichts produziert.", + "vcmi.townHall.hasProduced" : "Die %s hat diese Woche %d %s produziert.", + "vcmi.townHall.greetingCustomBonus" : "%s gibt Ihnen +%d %s%s", + "vcmi.townHall.greetingCustomUntil" : " bis zur nächsten Schlacht.", + "vcmi.townHall.greetingInTownMagicWell" : "%s hat Eure Zauberpunkte wieder auf das Maximum erhöht.", + + "vcmi.logicalExpressions.anyOf" : "Eines der folgenden:", + "vcmi.logicalExpressions.allOf" : "Alles der folgenden:", + "vcmi.logicalExpressions.noneOf" : "Keines der folgenden:", + + "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", + "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", + + "vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", + + "vcmi.creatureWindow.showBonuses.hover" : "Wechsle zur Bonus-Ansicht", + "vcmi.creatureWindow.showBonuses.help" : "Zeige alle aktiven Boni des Kommandanten", + "vcmi.creatureWindow.showSkills.hover" : "Wechsle zur Fertigkeits-Ansicht", + "vcmi.creatureWindow.showSkills.help" : "Zeige alle erlernten Fertigkeiten des Kommandanten", + "vcmi.creatureWindow.returnArtifact.hover" : "Artefekt zurückgeben", + "vcmi.creatureWindow.returnArtifact.help" : "Nutze diese Schaltfläche, um Stapel-Artefakt in den Rucksack des Helden zurückzugeben", + + "vcmi.questLog.hideComplete.hover" : "Verstecke abgeschlossene Quests", + "vcmi.questLog.hideComplete.help" : "Verstecke alle Quests die bereits abgeschlossen sind", + + "vcmi.randomMapTab.widgets.randomTemplate" : "(Zufällig)", + "vcmi.randomMapTab.widgets.templateLabel" : "Template", + "vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Einrichtung...", + "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", + "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", + + // Custom victory conditions for H3 campaigns and HotA maps + "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", + "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", + "vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Der Feind hat alle Monster besiegt, die das Land heimsuchen, und fordert den Sieg!", + "vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Herzlichen Glückwunsch! Ihr habt alle Monster besiegt, die dieses Land plagen, und könnt den Sieg für euch beanspruchen!", + "vcmi.map.victoryCondition.collectArtifacts.message" : "Sammelt drei Artefakte", + "vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Herzlichen Glückwunsch! Alle eure Feinde wurden besiegt und ihr habt die Engelsallianz! Der Sieg ist euer!", + "vcmi.map.victoryCondition.angelicAlliance.message" : "Besiege alle Feinde und gründe eine Engelsallianz", + + // few strings from WoG used by vcmi + "vcmi.stackExperience.description" : "» D e t a i l s z u r S t a p e l e r f a h r u n g «\n\nKreatur-Typ ................... : %s\nErfahrungsrang ................. : %s (%i)\nErfahrungspunkte ............... : %i\nErfahrungspunkte für den nächsten Rang .. : %i\nMaximale Erfahrung pro Kampf ... : %i%% (%i)\nAnzahl der Kreaturen im Stapel .... : %i\nMaximale Anzahl neuer Rekruten\n ohne Verlust von aktuellem Rang .... : %i\nErfahrungs-Multiplikator ........... : %.2f\nUpgrade-Multiplikator .............. : %.2f\nErfahrung nach Rang 10 ........ : %i\nMaximale Anzahl der neuen Rekruten, die bei\n Rang 10 bei maximaler Erfahrung übrig sind : %i", + "vcmi.stackExperience.rank.0" : "Grundlagen", + "vcmi.stackExperience.rank.1" : "Neuling", + "vcmi.stackExperience.rank.2" : "Ausgebildet", + "vcmi.stackExperience.rank.3" : "Kompetent", + "vcmi.stackExperience.rank.4" : "Bewährt", + "vcmi.stackExperience.rank.5" : "Veteran", + "vcmi.stackExperience.rank.6" : "Gekonnt", + "vcmi.stackExperience.rank.7" : "Experte", + "vcmi.stackExperience.rank.8" : "Elite", + "vcmi.stackExperience.rank.9" : "Meister", + "vcmi.stackExperience.rank.10" : "Ass", + + "core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag", + "core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an", + "core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen", + "core.bonus.ADDITIONAL_RETALIATION.description": "Kann ${val} zusätzliche Male vergelten", + "core.bonus.AIR_IMMUNITY.name": "Luftimmunität", + "core.bonus.AIR_IMMUNITY.description": "Immun gegen alle Luftschulzauber", + "core.bonus.ATTACKS_ALL_ADJACENT.name": "Rundum angreifen", + "core.bonus.ATTACKS_ALL_ADJACENT.description": "Greift alle benachbarten Gegner an", + "core.bonus.BLOCKS_RETALIATION.name": "Keine Vergeltung", + "core.bonus.BLOCKS_RETALIATION.description": "Feind kann nicht vergelten", + "core.bonus.BLOCKS_RANGED_RETALIATION.name": "Keine Reichweitenverschiebung", + "core.bonus.BLOCKS_RANGED_RETALIATION.description": "Feind kann nicht durch Schießen vergelten", + "core.bonus.CATAPULT.name": "Katapult", + "core.bonus.CATAPULT.description": "Greift Belagerungsmauern an", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduziere Zauberkosten (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduziert die Zauberkosten für den Helden", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Zauberdämpfer (${val})", + "core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Erhöht die Kosten von gegnerischen Zaubern", + "core.bonus.CHARGE_IMMUNITY.name": "Immun gegen Aufladung", + "core.bonus.CHARGE_IMMUNITY.description": "Immun gegen Aufladung", + "core.bonus.DARKNESS.name": "Abdeckung der Dunkelheit", + "core.bonus.DARKNESS.description": "Fügt ${val} Dunkelheitsradius hinzu", + "core.bonus.DEATH_STARE.name": "Todesstarren (${val}%)", + "core.bonus.DEATH_STARE.description": "${val}% Chance, eine einzelne Kreatur zu töten", + "core.bonus.DEFENSIVE_STANCE.name": "Verteidigungsbonus", + "core.bonus.DEFENSIVE_STANCE.description": "+${val} Verteidigung beim Verteidigen", + "core.bonus.DESTRUCTION.name": "Zerstörung", + "core.bonus.DESTRUCTION.description": "Hat ${val}% Chance, zusätzliche Einheiten nach dem Angriff zu töten", + "core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Todesstoß", + "core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% Chance auf doppelten Schaden", + "core.bonus.DRAGON_NATURE.name": "Drache", + "core.bonus.DRAGON_NATURE.description": "Kreatur hat eine Drachennatur", + "core.bonus.EARTH_IMMUNITY.name": "Erdimmunität", + "core.bonus.EARTH_IMMUNITY.description": "Immun gegen alle Zauber der Erdschule", + "core.bonus.ENCHANTER.name": "Verzauberer", + "core.bonus.ENCHANTER.description": "Kann jede Runde eine Masse von ${subtype.spell} zaubern", + "core.bonus.ENCHANTED.name": "Verzaubert", + "core.bonus.ENCHANTED.description": "Beeinflusst von permanentem ${subtype.spell}", + "core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignoriere Verteidigung (${val}%)", + "core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Ignoriert einen Teil der Verteidigung für den Angriff", + "core.bonus.FIRE_IMMUNITY.name": "Feuerimmunität", + "core.bonus.FIRE_IMMUNITY.description": "Immun gegen alle Zauber der Schule des Feuers", + "core.bonus.FIRE_SHIELD.name": "Feuerschild (${val}%)", + "core.bonus.FIRE_SHIELD.description": "Reflektiert einen Teil des Nahkampfschadens", + "core.bonus.FIRST_STRIKE.name": "Erstschlag", + "core.bonus.FIRST_STRIKE.description": "Diese Kreatur greift zuerst an, anstatt zu vergelten", + "core.bonus.FEAR.name": "Furcht", + "core.bonus.FEAR.description": "Verursacht Furcht bei einem gegnerischen Stapel", + "core.bonus.FEARLESS.name": "Furchtlos", + "core.bonus.FEARLESS.description": "immun gegen die Fähigkeit Furcht", + "core.bonus.FLYING.name": "Fliegen", + "core.bonus.FLYING.description": "Kann fliegen (ignoriert Hindernisse)", + "core.bonus.FREE_SHOOTING.name": "Nah schießen", + "core.bonus.FREE_SHOOTING.description": "Kann im Nahkampf schießen", + "core.bonus.GARGOYLE.name": "Gargoyle", + "core.bonus.GARGOYLE.description": "Kann nicht aufgerichtet oder geheilt werden", + "core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Schaden vermindern (${val}%)", + "core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduziert physischen Schaden aus dem Fern- oder Nahkampf", + "core.bonus.HATE.name": "Hasst ${subtype.creature}", + "core.bonus.HATE.description": "Macht ${val}% mehr Schaden", + "core.bonus.HEALER.name": "Heiler", + "core.bonus.HEALER.description": "Heilt verbündete Einheiten", + "core.bonus.HP_REGENERATION.name": "Regeneration", + "core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde", + "core.bonus.JOUSTING.name": "Champion Charge", + "core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld", + "core.bonus.KING.name": "König", + "core.bonus.KING.description": "Anfällig für SLAYER Level ${val} oder höher", + "core.bonus.LEVEL_SPELL_IMMUNITY.name": "Zauberimmunität 1-${val}", + "core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immun gegen Zaubersprüche der Stufen 1-${val}", + "core.bonus.LIMITED_SHOOTING_RANGE.name" : "Begrenzte Schussweite", + "core.bonus.LIMITED_SHOOTING_RANGE.description" : "Kann nicht auf Ziele schießen, die weiter als ${val} Felder entfernt sind", + "core.bonus.LIFE_DRAIN.name": "Leben entziehen (${val}%)", + "core.bonus.LIFE_DRAIN.description": "Drainiert ${val}% des zugefügten Schadens", + "core.bonus.MANA_CHANNELING.name": "Magiekanal ${val}%", + "core.bonus.MANA_CHANNELING.description": "Gibt Ihrem Helden Mana, das vom Gegner ausgegeben wird", + "core.bonus.MANA_DRAIN.name": "Mana-Entzug", + "core.bonus.MANA_DRAIN.description": "Entzieht ${val} Mana jede Runde", + "core.bonus.MAGIC_MIRROR.name": "Zauberspiegel (${val}%)", + "core.bonus.MAGIC_MIRROR.description": "${val}% Chance, einen Angriffszauber auf den Gegner umzulenken", + "core.bonus.MAGIC_RESISTANCE.name": "Magie-Widerstand(${val}%)", + "core.bonus.MAGIC_RESISTANCE.description": "${val}% Chance, gegnerischem Zauber zu widerstehen", + "core.bonus.MIND_IMMUNITY.name": "Geist-Zauber-Immunität", + "core.bonus.MIND_IMMUNITY.description": "Immun gegen Zauber vom Typ Geist", + "core.bonus.NO_DISTANCE_PENALTY.name": "Keine Entfernungsstrafe", + "core.bonus.NO_DISTANCE_PENALTY.description": "Voller Schaden aus beliebiger Entfernung", + "core.bonus.NO_MELEE_PENALTY.name": "Keine Nahkampf-Strafe", + "core.bonus.NO_MELEE_PENALTY.description": "Kreatur hat keinen Nahkampf-Malus", + "core.bonus.NO_MORALE.name": "Neutrale Moral", + "core.bonus.NO_MORALE.description": "Kreatur ist immun gegen Moral-Effekte", + "core.bonus.NO_WALL_PENALTY.name": "Keine Wand-Strafe", + "core.bonus.NO_WALL_PENALTY.description": "Voller Schaden bei Belagerung", + "core.bonus.NON_LIVING.name": "Nicht lebend", + "core.bonus.NON_LIVING.description": "Immunität gegen viele Effekte", + "core.bonus.RANDOM_SPELLCASTER.name": "Zufälliger Zauberwirker", + "core.bonus.RANDOM_SPELLCASTER.description": "Kann einen zufälligen Zauberspruch wirken", + "core.bonus.RANGED_RETALIATION.name": "Fernkampf-Vergeltung", + "core.bonus.RANGED_RETALIATION.description": "Kann einen Fernkampf-Gegenangriff durchführen", + "core.bonus.RECEPTIVE.name": "Empfänglich", + "core.bonus.RECEPTIVE.description": "Keine Immunität gegen Freundschaftszauber", + "core.bonus.REBIRTH.name": "Wiedergeburt (${val}%)", + "core.bonus.REBIRTH.description": "${val}% des Stacks wird nach dem Tod auferstehen", + "core.bonus.RETURN_AFTER_STRIKE.name": "Angriff und Rückkehr", + "core.bonus.RETURN_AFTER_STRIKE.description": "Kehrt nach Nahkampfangriff zurück", + "core.bonus.SHOOTER.name": "Fernkämpfer", + "core.bonus.SHOOTER.description": "Kreatur kann schießen", + "core.bonus.SHOOTS_ALL_ADJACENT.name": "Schießt rundherum", + "core.bonus.SHOOTS_ALL_ADJACENT.description": "Die Fernkampfangriffe dieser Kreatur treffen alle Ziele in einem kleinen Bereich", + "core.bonus.SOUL_STEAL.name": "Seelenraub", + "core.bonus.SOUL_STEAL.description": "Gewinnt ${val} neue Kreaturen für jeden getöteten Gegner", + "core.bonus.SPELLCASTER.name": "Zauberer", + "core.bonus.SPELLCASTER.description": "Kann ${subtype.spell} zaubern", + "core.bonus.SPELL_AFTER_ATTACK.name": "Nach Angriff zaubern", + "core.bonus.SPELL_AFTER_ATTACK.description": "${val}%, um ${subtype.spell} nach dem Angriff zu wirken", + "core.bonus.SPELL_BEFORE_ATTACK.name": "Zauber vor Angriff", + "core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% um ${subtype.spell} vor dem Angriff zu wirken", + "core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand", + "core.bonus.SPELL_DAMAGE_REDUCTION.description": "Schaden von Zaubern reduziert ${val}%.", + "core.bonus.SPELL_IMMUNITY.name": "Zauberimmunität", + "core.bonus.SPELL_IMMUNITY.description": "Immun gegen ${subtype.spell}", + "core.bonus.SPELL_LIKE_ATTACK.name": "zauberähnlicher Angriff", + "core.bonus.SPELL_LIKE_ATTACK.description": "Angriffe mit ${subtype.spell}", + "core.bonus.SPELL_RESISTANCE_AURA.name": "Aura des Widerstands", + "core.bonus.SPELL_RESISTANCE_AURA.description": "Stapel in der Nähe erhalten ${val}% Widerstand", + "core.bonus.SUMMON_GUARDIANS.name": "Wächter beschwören", + "core.bonus.SUMMON_GUARDIANS.description": "Beschwört bei Kampfbeginn ${subtype.creature} (${val}%)", + "core.bonus.SYNERGY_TARGET.name": "Synergierbar", + "core.bonus.SYNERGY_TARGET.description": "Diese Kreatur ist anfällig für Synergieeffekte", + "core.bonus.TWO_HEX_ATTACK_BREATH.name": "Atem", + "core.bonus.TWO_HEX_ATTACK_BREATH.description": "Atem-Angriff (2-Hex-Bereich)", + "core.bonus.THREE_HEADED_ATTACK.name": "Dreiköpfiger Angriff", + "core.bonus.THREE_HEADED_ATTACK.description": "Greift drei benachbarte Einheiten an", + "core.bonus.TRANSMUTATION.name": "Transmutation", + "core.bonus.TRANSMUTATION.description": "${val}% Chance, angegriffene Einheit in einen anderen Typ zu verwandeln", + "core.bonus.UNDEAD.name": "Untot", + "core.bonus.UNDEAD.description": "Kreatur ist untot", + "core.bonus.UNLIMITED_RETALIATIONS.name": "Unbegrenzte Vergeltungsmaßnahmen", + "core.bonus.UNLIMITED_RETALIATIONS.description": "Vergeltungen für eine beliebige Anzahl von Angriffen", + "core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität", + "core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule", + "core.bonus.WIDE_BREATH.name": "Breiter Atem", + "core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)" +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON index ff5c7d684..0cccafbb0 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/blockbuster.JSON @@ -1,372 +1,372 @@ -{ - "Blockbuster M" : - //(ban fly/DD, 2 player, 15-Jun-03, midnight design) - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 6000, "density" : 4 }, - { "min" : 800, "max" : 2000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9800, "density" : 3 }, - { "min" : 3500, "max" : 8000, "density" : 10 }, - { "min" : 800, "max" : 1200, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 9000, "max" : 9800, "density" : 2 }, - { "min" : 3500, "max" : 8999, "density" : 8 }, - { "min" : 800, "max" : 1200, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 18000, "max" : 25000, "density" : 2 }, - { "min" : 9000, "max" : 9800, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 50000 }, - { "a" : "1", "b" : "3", "guard" : 4000 }, - { "a" : "1", "b" : "5", "guard" : 8000 }, - { "a" : "2", "b" : "4", "guard" : 4000 }, - { "a" : "2", "b" : "6", "guard" : 8000 }, - { "a" : "5", "b" : "7", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 } - ] - }, - "Blockbuster L" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 5500, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 320, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9100, "density" : 4 }, - { "min" : 3500, "max" : 8000, "density" : 5 }, - { "min" : 800, "max" : 2000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 6000, "max" : 9300, "density" : 8 }, - { "min" : 800, "max" : 1200, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 18000, "max" : 25000, "density" : 2 }, - { "min" : 0, "max" : 45000, "density" : 6 }, - { "min" : 8000, "max" : 9300, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 7, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 110000 }, - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "1", "b" : "5", "guard" : 15000 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "2", "b" : "6", "guard" : 15000 }, - { "a" : "5", "b" : "7", "guard" : 24000 }, - { "a" : "6", "b" : "8", "guard" : 24000 }, - { "a" : "7", "b" : "8", "guard" : 40000 } - ] - }, - "Blockbuster XL" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 5500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 3 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9100, "density" : 3 }, - { "min" : 3500, "max" : 8000, "density" : 4 }, - { "min" : 800, "max" : 2000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 6000, "max" : 9200, "density" : 6 }, - { "min" : 800, "max" : 2000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 28000, "max" : 29000, "density" : 1 }, - { "min" : 0, "max" : 50000, "density" : 5 }, - { "min" : 7500, "max" : 9200, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 7, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 140000 }, - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 17000 }, - { "a" : "2", "b" : "4", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 17000 }, - { "a" : "5", "b" : "7", "guard" : 30000 }, - { "a" : "6", "b" : "8", "guard" : 30000 }, - { "a" : "7", "b" : "8", "guard" : 50000 } - ] - } -} +{ + "Blockbuster M" : + //(ban fly/DD, 2 player, 15-Jun-03, midnight design) + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 6000, "density" : 4 }, + { "min" : 800, "max" : 2000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9800, "density" : 3 }, + { "min" : 3500, "max" : 8000, "density" : 10 }, + { "min" : 800, "max" : 1200, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 9000, "max" : 9800, "density" : 2 }, + { "min" : 3500, "max" : 8999, "density" : 8 }, + { "min" : 800, "max" : 1200, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 18000, "max" : 25000, "density" : 2 }, + { "min" : 9000, "max" : 9800, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 50000 }, + { "a" : "1", "b" : "3", "guard" : 4000 }, + { "a" : "1", "b" : "5", "guard" : 8000 }, + { "a" : "2", "b" : "4", "guard" : 4000 }, + { "a" : "2", "b" : "6", "guard" : 8000 }, + { "a" : "5", "b" : "7", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 } + ] + }, + "Blockbuster L" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 5500, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 320, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9100, "density" : 4 }, + { "min" : 3500, "max" : 8000, "density" : 5 }, + { "min" : 800, "max" : 2000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 6000, "max" : 9300, "density" : 8 }, + { "min" : 800, "max" : 1200, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 18000, "max" : 25000, "density" : 2 }, + { "min" : 0, "max" : 45000, "density" : 6 }, + { "min" : 8000, "max" : 9300, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 7, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 110000 }, + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "1", "b" : "5", "guard" : 15000 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "2", "b" : "6", "guard" : 15000 }, + { "a" : "5", "b" : "7", "guard" : 24000 }, + { "a" : "6", "b" : "8", "guard" : 24000 }, + { "a" : "7", "b" : "8", "guard" : 40000 } + ] + }, + "Blockbuster XL" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 5500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 3 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9100, "density" : 3 }, + { "min" : 3500, "max" : 8000, "density" : 4 }, + { "min" : 800, "max" : 2000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 6000, "max" : 9200, "density" : 6 }, + { "min" : 800, "max" : 2000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 28000, "max" : 29000, "density" : 1 }, + { "min" : 0, "max" : 50000, "density" : 5 }, + { "min" : 7500, "max" : 9200, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 7, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 140000 }, + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 17000 }, + { "a" : "2", "b" : "4", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 17000 }, + { "a" : "5", "b" : "7", "guard" : 30000 }, + { "a" : "6", "b" : "8", "guard" : 30000 }, + { "a" : "7", "b" : "8", "guard" : 50000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON index 37fc0f1ea..bac5bc7bc 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme.JSON @@ -1,362 +1,362 @@ -{ - "Extreme L" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3400, "max" : 3500, "density" : 3 }, - { "min" : 1000, "max" : 2000, "density" : 10 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3500, "max" : 9600, "density" : 8 }, - { "min" : 300, "max" : 1000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1 }, - "treasure" : - [ - { "min" : 40000, "max" : 42000, "density" : 1 }, - { "min" : 25000, "max" : 27000, "density" : 2 }, - { "min" : 6000, "max" : 15000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 29000, "density" : 2 }, - { "min" : 3500, "max" : 20000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 1 }, - { "min" : 50000, "max" : 70000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 15000 }, - { "a" : "3", "b" : "7", "guard" : 20000 }, - { "a" : "4", "b" : "6", "guard" : 15000 }, - { "a" : "4", "b" : "8", "guard" : 20000 }, - { "a" : "3", "b" : "11", "guard" : 50000 }, - { "a" : "4", "b" : "12", "guard" : 50000 }, - { "a" : "5", "b" : "9", "guard" : 40000 }, - { "a" : "7", "b" : "11", "guard" : 40000 }, - { "a" : "6", "b" : "10", "guard" : 40000 }, - { "a" : "8", "b" : "12", "guard" : 40000 }, - { "a" : "1", "b" : "9", "guard" : 50000 }, - { "a" : "2", "b" : "10", "guard" : 50000 }, - { "a" : "9", "b" : "12", "guard" : 100000 }, - { "a" : "10", "b" : "11", "guard" : 100000 } - ] - }, - "Extreme XL" : - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xh", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3400, "max" : 3500, "density" : 3 }, - { "min" : 1000, "max" : 2000, "density" : 6 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3500, "max" : 9500, "density" : 5 }, - { "min" : 300, "max" : 1000, "density" : 2 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1 }, - "treasure" : - [ - { "min" : 40000, "max" : 42000, "density" : 1 }, - { "min" : 25000, "max" : 27000, "density" : 1 }, - { "min" : 6000, "max" : 15000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 29000, "density" : 1 }, - { "min" : 3500, "max" : 20000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 1 }, - { "min" : 50000, "max" : 80000, "density" : 6 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 17, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 18000 }, - { "a" : "3", "b" : "7", "guard" : 22000 }, - { "a" : "4", "b" : "6", "guard" : 18000 }, - { "a" : "4", "b" : "8", "guard" : 22000 }, - { "a" : "3", "b" : "11", "guard" : 60000 }, - { "a" : "4", "b" : "12", "guard" : 60000 }, - { "a" : "5", "b" : "9", "guard" : 50000 }, - { "a" : "7", "b" : "11", "guard" : 50000 }, - { "a" : "6", "b" : "10", "guard" : 50000 }, - { "a" : "8", "b" : "12", "guard" : 50000 }, - { "a" : "1", "b" : "9", "guard" : 60000 }, - { "a" : "2", "b" : "10", "guard" : 60000 }, - { "a" : "9", "b" : "12", "guard" : 140000 }, - { "a" : "10", "b" : "11", "guard" : 140000 } - ] - } -} +{ + "Extreme L" : + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3400, "max" : 3500, "density" : 3 }, + { "min" : 1000, "max" : 2000, "density" : 10 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3500, "max" : 9600, "density" : 8 }, + { "min" : 300, "max" : 1000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1 }, + "treasure" : + [ + { "min" : 40000, "max" : 42000, "density" : 1 }, + { "min" : 25000, "max" : 27000, "density" : 2 }, + { "min" : 6000, "max" : 15000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 29000, "density" : 2 }, + { "min" : 3500, "max" : 20000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 1 }, + { "min" : 50000, "max" : 70000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 15000 }, + { "a" : "3", "b" : "7", "guard" : 20000 }, + { "a" : "4", "b" : "6", "guard" : 15000 }, + { "a" : "4", "b" : "8", "guard" : 20000 }, + { "a" : "3", "b" : "11", "guard" : 50000 }, + { "a" : "4", "b" : "12", "guard" : 50000 }, + { "a" : "5", "b" : "9", "guard" : 40000 }, + { "a" : "7", "b" : "11", "guard" : 40000 }, + { "a" : "6", "b" : "10", "guard" : 40000 }, + { "a" : "8", "b" : "12", "guard" : 40000 }, + { "a" : "1", "b" : "9", "guard" : 50000 }, + { "a" : "2", "b" : "10", "guard" : 50000 }, + { "a" : "9", "b" : "12", "guard" : 100000 }, + { "a" : "10", "b" : "11", "guard" : 100000 } + ] + }, + "Extreme XL" : + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xh", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3400, "max" : 3500, "density" : 3 }, + { "min" : 1000, "max" : 2000, "density" : 6 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3500, "max" : 9500, "density" : 5 }, + { "min" : 300, "max" : 1000, "density" : 2 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1 }, + "treasure" : + [ + { "min" : 40000, "max" : 42000, "density" : 1 }, + { "min" : 25000, "max" : 27000, "density" : 1 }, + { "min" : 6000, "max" : 15000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 29000, "density" : 1 }, + { "min" : 3500, "max" : 20000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 1 }, + { "min" : 50000, "max" : 80000, "density" : 6 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 17, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 18000 }, + { "a" : "3", "b" : "7", "guard" : 22000 }, + { "a" : "4", "b" : "6", "guard" : 18000 }, + { "a" : "4", "b" : "8", "guard" : 22000 }, + { "a" : "3", "b" : "11", "guard" : 60000 }, + { "a" : "4", "b" : "12", "guard" : 60000 }, + { "a" : "5", "b" : "9", "guard" : 50000 }, + { "a" : "7", "b" : "11", "guard" : 50000 }, + { "a" : "6", "b" : "10", "guard" : 50000 }, + { "a" : "8", "b" : "12", "guard" : 50000 }, + { "a" : "1", "b" : "9", "guard" : 60000 }, + { "a" : "2", "b" : "10", "guard" : 60000 }, + { "a" : "9", "b" : "12", "guard" : 140000 }, + { "a" : "10", "b" : "11", "guard" : 140000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON index 965cba95c..62006c85e 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/extreme2.JSON @@ -1,310 +1,310 @@ -{ - "Extreme II L": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)" - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 120, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 16000, "max" : 90000, "density" : 1 }, - { "min" : 300, "max" : 16000, "density" : 2 }, - { "min" : 370, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 120, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 22, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 300, "max" : 1000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 22, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 70000, "max" : 90000, "density" : 6 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasure" : - [ - { "min" : 90000, "max" : 120000, "density" : 6 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 65000 }, - { "a" : "3", "b" : "7", "guard" : 65000 }, - { "a" : "4", "b" : "6", "guard" : 65000 }, - { "a" : "4", "b" : "8", "guard" : 65000 }, - { "a" : "5", "b" : "9", "guard" : 135000 }, - { "a" : "6", "b" : "9", "guard" : 135000 }, - { "a" : "7", "b" : "10", "guard" : 135000 }, - { "a" : "8", "b" : "10", "guard" : 135000 }, - { "a" : "3", "b" : "5", "guard" : 60000 }, - { "a" : "3", "b" : "7", "guard" : 60000 }, - { "a" : "4", "b" : "6", "guard" : 60000 }, - { "a" : "4", "b" : "8", "guard" : 60000 } - ] - }, - "Extreme II XL": - //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xh", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 90, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, - "treasure" : - [ - { "min" : 16000, "max" : 120000, "density" : 1 }, - { "min" : 300, "max" : 16000, "density" : 2 }, - { "min" : 370, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 90, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 300, "max" : 1000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 85000, "max" : 100000, "density" : 4 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasure" : - [ - { "min" : 115000, "max" : 120000, "density" : 4 }, - { "min" : 20000, "max" : 20000, "density" : 2 }, - { "min" : 300, "max" : 400, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 5500 }, - { "a" : "2", "b" : "4", "guard" : 6500 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 5500 }, - { "a" : "3", "b" : "5", "guard" : 80000 }, - { "a" : "3", "b" : "7", "guard" : 80000 }, - { "a" : "4", "b" : "6", "guard" : 80000 }, - { "a" : "4", "b" : "8", "guard" : 80000 }, - { "a" : "5", "b" : "9", "guard" : 160000 }, - { "a" : "6", "b" : "9", "guard" : 160000 }, - { "a" : "7", "b" : "10", "guard" : 160000 }, - { "a" : "8", "b" : "10", "guard" : 160000 }, - { "a" : "3", "b" : "5", "guard" : 70000 }, - { "a" : "3", "b" : "7", "guard" : 70000 }, - { "a" : "4", "b" : "6", "guard" : 70000 }, - { "a" : "4", "b" : "8", "guard" : 70000 } - ] - } -} +{ + "Extreme II L": + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design)" + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 120, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 16000, "max" : 90000, "density" : 1 }, + { "min" : 300, "max" : 16000, "density" : 2 }, + { "min" : 370, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 120, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 22, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 300, "max" : 1000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 22, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 70000, "max" : 90000, "density" : 6 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasure" : + [ + { "min" : 90000, "max" : 120000, "density" : 6 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 65000 }, + { "a" : "3", "b" : "7", "guard" : 65000 }, + { "a" : "4", "b" : "6", "guard" : 65000 }, + { "a" : "4", "b" : "8", "guard" : 65000 }, + { "a" : "5", "b" : "9", "guard" : 135000 }, + { "a" : "6", "b" : "9", "guard" : 135000 }, + { "a" : "7", "b" : "10", "guard" : 135000 }, + { "a" : "8", "b" : "10", "guard" : 135000 }, + { "a" : "3", "b" : "5", "guard" : 60000 }, + { "a" : "3", "b" : "7", "guard" : 60000 }, + { "a" : "4", "b" : "6", "guard" : 60000 }, + { "a" : "4", "b" : "8", "guard" : 60000 } + ] + }, + "Extreme II XL": + //(ban fly/DD/orb inhibition/Castle-Necro-Conflux towns, 2 player, 3-Aug-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xh", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 90, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1, "gold" : 3 }, + "treasure" : + [ + { "min" : 16000, "max" : 120000, "density" : 1 }, + { "min" : 300, "max" : 16000, "density" : 2 }, + { "min" : 370, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 90, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 300, "max" : 1000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 85000, "max" : 100000, "density" : 4 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasure" : + [ + { "min" : 115000, "max" : 120000, "density" : 4 }, + { "min" : 20000, "max" : 20000, "density" : 2 }, + { "min" : 300, "max" : 400, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 5500 }, + { "a" : "2", "b" : "4", "guard" : 6500 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 5500 }, + { "a" : "3", "b" : "5", "guard" : 80000 }, + { "a" : "3", "b" : "7", "guard" : 80000 }, + { "a" : "4", "b" : "6", "guard" : 80000 }, + { "a" : "4", "b" : "8", "guard" : 80000 }, + { "a" : "5", "b" : "9", "guard" : 160000 }, + { "a" : "6", "b" : "9", "guard" : 160000 }, + { "a" : "7", "b" : "10", "guard" : 160000 }, + { "a" : "8", "b" : "10", "guard" : 160000 }, + { "a" : "3", "b" : "5", "guard" : 70000 }, + { "a" : "3", "b" : "7", "guard" : 70000 }, + { "a" : "4", "b" : "6", "guard" : 70000 }, + { "a" : "4", "b" : "8", "guard" : 70000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON index d04595322..3113a91dd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/poorJebus.JSON @@ -1,83 +1,83 @@ -{ - "Poor Jebus" : - //(made by Bjorn190, modified by Maretti and Angelito) - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 12000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 16000, "density" : 6 }, - { "min" : 400, "max" : 3000, "density" : 4 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "castles" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 45000 }, - { "a" : "2", "b" : "5", "guard" : 45000 }, - { "a" : "3", "b" : "5", "guard" : 45000 }, - { "a" : "4", "b" : "5", "guard" : 45000 } - ] - } -} +{ + "Poor Jebus" : + //(made by Bjorn190, modified by Maretti and Angelito) + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "mines" : { "wood" : 4, "mercury" : 1, "ore" : 4, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 12000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 16000, "density" : 6 }, + { "min" : 400, "max" : 3000, "density" : 4 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "castles" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 45000 }, + { "a" : "2", "b" : "5", "guard" : 45000 }, + { "a" : "3", "b" : "5", "guard" : 45000 }, + { "a" : "4", "b" : "5", "guard" : 45000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON index 82911eafd..f41c68cd3 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/reckless.JSON @@ -1,140 +1,140 @@ -{ - "Reckless" : - //(2 player, 6-Jan-03, midnight design) - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 9800, "density" : 1 }, - { "min" : 3500, "max" : 4500, "density" : 5 }, - { "min" : 1500, "max" : 2000, "density" : 7 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 25000, "density" : 1 }, - { "min" : 3500, "max" : 9800, "density" : 8 }, - { "min" : 800, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "castles" : 4 }, - "bannedTowns" : ["necropolis", "conflux"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 2, "ore" : 2, "gold" : 3 }, - "treasure" : - [ - { "min" : 40000, "max" : 60000, "density" : 1 }, - { "min" : 500, "max" : 29000, "density" : 12 }, - { "min" : 800, "max" : 2000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "2", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 11000 }, - { "a" : "4", "b" : "9", "guard" : 11000 }, - { "a" : "5", "b" : "9", "guard" : 11000 }, - { "a" : "6", "b" : "9", "guard" : 11000 }, - { "a" : "7", "b" : "9", "guard" : 11000 }, - { "a" : "8", "b" : "9", "guard" : 11000 } - ] - } -} +{ + "Reckless" : + //(2 player, 6-Jan-03, midnight design) + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 9800, "density" : 1 }, + { "min" : 3500, "max" : 4500, "density" : 5 }, + { "min" : 1500, "max" : 2000, "density" : 7 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 25000, "density" : 1 }, + { "min" : 3500, "max" : 9800, "density" : 8 }, + { "min" : 800, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "castles" : 4 }, + "bannedTowns" : ["necropolis", "conflux"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 2, "ore" : 2, "gold" : 3 }, + "treasure" : + [ + { "min" : 40000, "max" : 60000, "density" : 1 }, + { "min" : 500, "max" : 29000, "density" : 12 }, + { "min" : 800, "max" : 2000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "2", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 11000 }, + { "a" : "4", "b" : "9", "guard" : 11000 }, + { "a" : "5", "b" : "9", "guard" : 11000 }, + { "a" : "6", "b" : "9", "guard" : 11000 }, + { "a" : "7", "b" : "9", "guard" : 11000 }, + { "a" : "8", "b" : "9", "guard" : 11000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON index 4167424c0..063ec0b1f 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/roadrunner.JSON @@ -1,139 +1,139 @@ -{ - "Roadrunner" : - //(ban fly/DD, 2 player, 31-May-03, midnight design) - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 6000, "density" : 4 }, - { "min" : 300, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9500, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 45000, "max" : 50000, "density" : 1 }, - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 8000, "max" : 9200, "density" : 12 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 100000, "max" : 120000, "density" : 1 }, - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 8000, "max" : 9200, "density" : 12 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 } - ] - } -} +{ + "Roadrunner" : + //(ban fly/DD, 2 player, 31-May-03, midnight design) + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 6000, "density" : 4 }, + { "min" : 300, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9500, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 45000, "max" : 50000, "density" : 1 }, + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 8000, "max" : 9200, "density" : 12 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 100000, "max" : 120000, "density" : 1 }, + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 8000, "max" : 9200, "density" : 12 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON index 19d3922e9..0bad27b1e 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/superslam.JSON @@ -1,111 +1,111 @@ -{ - "SuperSlam" : - //(2 player, Large or XL no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules - { - "minSize" : "l", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 400, "max" : 2000, "density" : 6 }, - { "min" : 3500, "max" : 5000, "density" : 5 }, - { "min" : 2100, "max" : 3000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9500, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "mercury" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 5 }, - { "min" : 10000, "max" : 22000, "density" : 3 }, - { "min" : 1000, "max" : 1700, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "minesLikeZone" : 5, - "treasure" : - [ - { "min" : 10000, "max" : 22000, "density" : 3 }, - { "min" : 25000, "max" : 29000, "density" : 5 }, - { "min" : 1000, "max" : 1700, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 10 }, - { "a" : "2", "b" : "4", "guard" : 10 }, - { "a" : "3", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 120000 }, - { "a" : "3", "b" : "4", "guard" : 120000 } - ] - } -} +{ + "SuperSlam" : + //(2 player, Large or XL no under) For powermonger players, meet by SLAMMING thru super zones. Your chances are not over until the fat lady sings! Should ban spec log along with normal random rules + { + "minSize" : "l", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 400, "max" : 2000, "density" : 6 }, + { "min" : 3500, "max" : 5000, "density" : 5 }, + { "min" : 2100, "max" : 3000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9500, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "mercury" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 5 }, + { "min" : 10000, "max" : 22000, "density" : 3 }, + { "min" : 1000, "max" : 1700, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "dungeon", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "minesLikeZone" : 5, + "treasure" : + [ + { "min" : 10000, "max" : 22000, "density" : 3 }, + { "min" : 25000, "max" : 29000, "density" : 5 }, + { "min" : 1000, "max" : 1700, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 10 }, + { "a" : "2", "b" : "4", "guard" : 10 }, + { "a" : "3", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 120000 }, + { "a" : "3", "b" : "4", "guard" : 120000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON index 07a3d8391..e7bef4fe1 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/readyOrNot.JSON @@ -1,146 +1,146 @@ -{ - "Ready or Not" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "inferno" ], - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "rampart" ], - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "rampart" ], - "allowedMonsters" : [ "inferno", "fortress", "neutral" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "fortress" ], - "allowedMonsters" : [ "rampart", "inferno", "neutral" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "inferno" ], - "allowedMonsters" : [ "rampart", "fortress", "neutral" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "terrainTypeLikeZone" : 8, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "terrainTypeLikeZone" : 8, - "treasureLikeZone" : 8 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "6", "b" : "5", "guard" : 0 }, - { "a" : "7", "b" : "5", "guard" : 0 }, - { "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster - { "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster - { "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster - ] - } -} +{ + "Ready or Not" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "inferno" ], + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "rampart" ], + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "rampart" ], + "allowedMonsters" : [ "inferno", "fortress", "neutral" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "rough", "subterra" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "fortress" ], + "allowedMonsters" : [ "rampart", "inferno", "neutral" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "inferno" ], + "allowedMonsters" : [ "rampart", "fortress", "neutral" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "terrainTypeLikeZone" : 8, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "terrainTypeLikeZone" : 8, + "treasureLikeZone" : 8 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "6", "b" : "5", "guard" : 0 }, + { "a" : "7", "b" : "5", "guard" : 0 }, + { "a" : "8", "b" : "1", "guard" : 12500 }, // Border guard replaced by monster + { "a" : "9", "b" : "2", "guard" : 12500 }, // Border guard replaced by monster + { "a" : "10", "b" : "3", "guard" : 12500 } // Border guard replaced by monster + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON index 1e2ebb553..e3d640f71 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON @@ -1,232 +1,232 @@ -{ - "Worlds at War" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", "cpu" : "3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "cpuStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 8000, "density" : 8 }, - { "min" : 3000, "max" : 15000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 15000, "max" : 20000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 3 }, - { "min" : 10000, "max" : 20000, "density" : 2 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 10000, "max" : 20000, "density" : 1 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 2 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 10 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 6000, "density" : 4 }, - { "min" : 10000, "max" : 20000, "density" : 3 } - ] - }, - "13" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 12 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 4 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "12", "guard" : 4500 }, - { "a" : "4", "b" : "14", "guard" : 4500 }, - { "a" : "5", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "15", "guard" : 3000 }, - { "a" : "5", "b" : "16", "guard" : 3000 }, - { "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "6", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "10", "guard" : 4500 }, - { "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "8", "b" : "11", "guard" : 4500 }, - { "a" : "9", "b" : "13", "guard" : 6000 }, - { "a" : "12", "b" : "13", "guard" : 4500 }, - { "a" : "13", "b" : "14", "guard" : 4500 }, - { "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 } - ] - } -} +{ + "Worlds at War" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2", "cpu" : "3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "cpuStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 8000, "density" : 8 }, + { "min" : 3000, "max" : 15000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 15000, "max" : 20000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 3 }, + { "min" : 10000, "max" : 20000, "density" : 2 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 10000, "max" : 20000, "density" : 1 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 2 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 10 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 6000, "density" : 4 }, + { "min" : 10000, "max" : 20000, "density" : 3 } + ] + }, + "13" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 12 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 4 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "12", "guard" : 4500 }, + { "a" : "4", "b" : "14", "guard" : 4500 }, + { "a" : "5", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "15", "guard" : 3000 }, + { "a" : "5", "b" : "16", "guard" : 3000 }, + { "a" : "6", "b" : "7", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "6", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "10", "guard" : 4500 }, + { "a" : "7", "b" : "8", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "8", "b" : "11", "guard" : 4500 }, + { "a" : "9", "b" : "13", "guard" : 6000 }, + { "a" : "12", "b" : "13", "guard" : 4500 }, + { "a" : "13", "b" : "14", "guard" : 4500 }, + { "a" : "13", "b" : "17", "guard" : 12500 }, // Border guard replaced by 12.5k guard + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON index d5478386c..399bc52ed 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON @@ -1,361 +1,361 @@ -{ - "Gauntlet" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "1", "cpu" : "5", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "treasure" : - [ - { "min" : 5000, "max" : 8000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 2 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "treasure" : - [ - { "min" : 10000, "max" : 14000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 1, - "mines" : { "wood" : 1 }, - "treasureLikeZone" : 2 - }, - "5" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "rough" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 5, - "mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "treasure" : - [ - { "min" : 12000, "max" : 18000, "density" : 1 }, - { "min" : 5000, "max" : 10000, "density" : 5 }, - { "min" : 1000, "max" : 4000, "density" : 7 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 7, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 6 }, - { "min" : 1000, "max" : 4000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 8000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "11" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 4 }, - { "min" : 1000, "max" : 4000, "density" : 8 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "14" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "minesLikeZone" : 6, - "treasure" : - [ - { "min" : 4000, "max" : 8000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "15" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 11 - }, - "16" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasure" : - [ - { "min" : 10000, "max" : 14000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 7 } - ] - }, - "17" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 14 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 4 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "19" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "20" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "normal", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 18 - }, - "21" : - { - "type" : "cpuStart", - "size" : 8, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "minesLikeZone" : 10, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "22" : - { - "type" : "junction", - "size" : 6, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "treasureLikeZone" : 5 - }, - "23" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "weak", - "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], - "terrainTypeLikeZone" : 10, - "minesLikeZone" : 18, - "treasure" : - [ - { "min" : 8000, "max" : 12000, "density" : 2 }, - { "min" : 6000, "max" : 10000, "density" : 4 }, - { "min" : 1000, "max" : 5000, "density" : 8 } - ] - }, - "24" : - { - "type" : "cpuStart", - "size" : 8, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "stronghold" ], - "minesLikeZone" : 17, - "treasure" : - [ - { "min" : 5000, "max" : 10000, "density" : 1 }, - { "min" : 4000, "max" : 8000, "density" : 3 }, - { "min" : 1000, "max" : 4000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster - { "a" : "1", "b" : "15", "guard" : 0 }, - { "a" : "2", "b" : "3", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 }, - { "a" : "8", "b" : "9", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "13", "b" : "14", "guard" : 0 }, - { "a" : "14", "b" : "15", "guard" : 0 }, - { "a" : "15", "b" : "16", "guard" : 0 }, - { "a" : "16", "b" : "17", "guard" : 0 }, - { "a" : "17", "b" : "18", "guard" : 0 }, - { "a" : "18", "b" : "19", "guard" : 0 }, - { "a" : "18", "b" : "22", "guard" : 0 }, - { "a" : "19", "b" : "20", "guard" : 0 }, - { "a" : "20", "b" : "21", "guard" : 0 }, - { "a" : "22", "b" : "23", "guard" : 0 }, - { "a" : "23", "b" : "24", "guard" : 0 } - ] - } -} +{ + "Gauntlet" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "1", "cpu" : "5", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "treasure" : + [ + { "min" : 5000, "max" : 8000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 2 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "treasure" : + [ + { "min" : 10000, "max" : 14000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 1, + "mines" : { "wood" : 1 }, + "treasureLikeZone" : 2 + }, + "5" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "rough" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 5, + "mines" : { "mercury" : 1, "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "treasure" : + [ + { "min" : 12000, "max" : 18000, "density" : 1 }, + { "min" : 5000, "max" : 10000, "density" : 5 }, + { "min" : 1000, "max" : 4000, "density" : 7 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 7, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 6 }, + { "min" : 1000, "max" : 4000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 8000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "11" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 4 }, + { "min" : 1000, "max" : 4000, "density" : 8 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "14" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "minesLikeZone" : 6, + "treasure" : + [ + { "min" : 4000, "max" : 8000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "15" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 11 + }, + "16" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasure" : + [ + { "min" : 10000, "max" : 14000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 7 } + ] + }, + "17" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 14 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 4 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "19" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "20" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "normal", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 18 + }, + "21" : + { + "type" : "cpuStart", + "size" : 8, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "minesLikeZone" : 10, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "22" : + { + "type" : "junction", + "size" : 6, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "treasureLikeZone" : 5 + }, + "23" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "weak", + "allowedMonsters" : [ "castle", "rampart", "tower", "stronghold", "fortress", "neutral" ], + "terrainTypeLikeZone" : 10, + "minesLikeZone" : 18, + "treasure" : + [ + { "min" : 8000, "max" : 12000, "density" : 2 }, + { "min" : 6000, "max" : 10000, "density" : 4 }, + { "min" : 1000, "max" : 5000, "density" : 8 } + ] + }, + "24" : + { + "type" : "cpuStart", + "size" : 8, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "stronghold" ], + "minesLikeZone" : 17, + "treasure" : + [ + { "min" : 5000, "max" : 10000, "density" : 1 }, + { "min" : 4000, "max" : 8000, "density" : 3 }, + { "min" : 1000, "max" : 4000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 12500 }, // Border guard replaced with monster + { "a" : "1", "b" : "15", "guard" : 0 }, + { "a" : "2", "b" : "3", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "21", "guard" : 12500 }, // Border guard replaced with monster + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 }, + { "a" : "8", "b" : "9", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "13", "b" : "14", "guard" : 0 }, + { "a" : "14", "b" : "15", "guard" : 0 }, + { "a" : "15", "b" : "16", "guard" : 0 }, + { "a" : "16", "b" : "17", "guard" : 0 }, + { "a" : "17", "b" : "18", "guard" : 0 }, + { "a" : "18", "b" : "19", "guard" : 0 }, + { "a" : "18", "b" : "22", "guard" : 0 }, + { "a" : "19", "b" : "20", "guard" : 0 }, + { "a" : "20", "b" : "21", "guard" : 0 }, + { "a" : "22", "b" : "23", "guard" : 0 }, + { "a" : "23", "b" : "24", "guard" : 0 } + ] + } +} diff --git a/client/CGameInfo.cpp b/client/CGameInfo.cpp index 59a523e2b..df2e1372d 100644 --- a/client/CGameInfo.cpp +++ b/client/CGameInfo.cpp @@ -1,115 +1,115 @@ -/* - * CGameInfo.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 "CGameInfo.h" - -#include "../lib/VCMI_Lib.h" - -const CGameInfo * CGI; -CClientState * CCS = nullptr; -CServerHandler * CSH; - - -CGameInfo::CGameInfo() -{ - generaltexth = nullptr; - mh = nullptr; - townh = nullptr; - globalServices = nullptr; -} - -void CGameInfo::setFromLib() -{ - globalServices = VLC; - modh = VLC->modh; - generaltexth = VLC->generaltexth; - creh = VLC->creh; - townh = VLC->townh; - heroh = VLC->heroh; - objh = VLC->objh; - spellh = VLC->spellh; - skillh = VLC->skillh; - objtypeh = VLC->objtypeh; - terrainTypeHandler = VLC->terrainTypeHandler; - battleFieldHandler = VLC->battlefieldsHandler; - obstacleHandler = VLC->obstacleHandler; -} - -const ArtifactService * CGameInfo::artifacts() const -{ - return globalServices->artifacts(); -} - -const BattleFieldService * CGameInfo::battlefields() const -{ - return globalServices->battlefields(); -} - -const CreatureService * CGameInfo::creatures() const -{ - return globalServices->creatures(); -} - -const FactionService * CGameInfo::factions() const -{ - return globalServices->factions(); -} - -const HeroClassService * CGameInfo::heroClasses() const -{ - return globalServices->heroClasses(); -} - -const HeroTypeService * CGameInfo::heroTypes() const -{ - return globalServices->heroTypes(); -} - -#if SCRIPTING_ENABLED -const scripting::Service * CGameInfo::scripts() const -{ - return globalServices->scripts(); -} -#endif - -const spells::Service * CGameInfo::spells() const -{ - return globalServices->spells(); -} - -const SkillService * CGameInfo::skills() const -{ - return globalServices->skills(); -} - -const ObstacleService * CGameInfo::obstacles() const -{ - return globalServices->obstacles(); -} - -const IGameSettings * CGameInfo::settings() const -{ - return globalServices->settings(); -} - -void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) -{ - logGlobal->error("CGameInfo::updateEntity call is not expected."); -} - -spells::effects::Registry * CGameInfo::spellEffects() -{ - return nullptr; -} - -const spells::effects::Registry * CGameInfo::spellEffects() const -{ - return globalServices->spellEffects(); -} +/* + * CGameInfo.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 "CGameInfo.h" + +#include "../lib/VCMI_Lib.h" + +const CGameInfo * CGI; +CClientState * CCS = nullptr; +CServerHandler * CSH; + + +CGameInfo::CGameInfo() +{ + generaltexth = nullptr; + mh = nullptr; + townh = nullptr; + globalServices = nullptr; +} + +void CGameInfo::setFromLib() +{ + globalServices = VLC; + modh = VLC->modh; + generaltexth = VLC->generaltexth; + creh = VLC->creh; + townh = VLC->townh; + heroh = VLC->heroh; + objh = VLC->objh; + spellh = VLC->spellh; + skillh = VLC->skillh; + objtypeh = VLC->objtypeh; + terrainTypeHandler = VLC->terrainTypeHandler; + battleFieldHandler = VLC->battlefieldsHandler; + obstacleHandler = VLC->obstacleHandler; +} + +const ArtifactService * CGameInfo::artifacts() const +{ + return globalServices->artifacts(); +} + +const BattleFieldService * CGameInfo::battlefields() const +{ + return globalServices->battlefields(); +} + +const CreatureService * CGameInfo::creatures() const +{ + return globalServices->creatures(); +} + +const FactionService * CGameInfo::factions() const +{ + return globalServices->factions(); +} + +const HeroClassService * CGameInfo::heroClasses() const +{ + return globalServices->heroClasses(); +} + +const HeroTypeService * CGameInfo::heroTypes() const +{ + return globalServices->heroTypes(); +} + +#if SCRIPTING_ENABLED +const scripting::Service * CGameInfo::scripts() const +{ + return globalServices->scripts(); +} +#endif + +const spells::Service * CGameInfo::spells() const +{ + return globalServices->spells(); +} + +const SkillService * CGameInfo::skills() const +{ + return globalServices->skills(); +} + +const ObstacleService * CGameInfo::obstacles() const +{ + return globalServices->obstacles(); +} + +const IGameSettings * CGameInfo::settings() const +{ + return globalServices->settings(); +} + +void CGameInfo::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) +{ + logGlobal->error("CGameInfo::updateEntity call is not expected."); +} + +spells::effects::Registry * CGameInfo::spellEffects() +{ + return nullptr; +} + +const spells::effects::Registry * CGameInfo::spellEffects() const +{ + return globalServices->spellEffects(); +} diff --git a/client/CGameInfo.h b/client/CGameInfo.h index 76b58a610..332068395 100644 --- a/client/CGameInfo.h +++ b/client/CGameInfo.h @@ -1,101 +1,101 @@ -/* - * CGameInfo.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 - -#include "../lib/ConstTransitivePtr.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CModHandler; -class CHeroHandler; -class CCreatureHandler; -class CSpellHandler; -class CSkillHandler; -class CBuildingHandler; -class CObjectHandler; -class CObjectClassesHandler; -class CTownHandler; -class CGeneralTextHandler; -class CConsoleHandler; -class CGameState; -class BattleFieldHandler; -class ObstacleHandler; -class TerrainTypeHandler; - -class CMap; - -VCMI_LIB_NAMESPACE_END - -class CMapHandler; -class CSoundHandler; -class CMusicHandler; -class CursorHandler; -class IMainVideoPlayer; -class CServerHandler; - -//a class for non-mechanical client GUI classes -class CClientState -{ -public: - CSoundHandler * soundh; - CMusicHandler * musich; - CConsoleHandler * consoleh; - CursorHandler * curh; - IMainVideoPlayer * videoh; -}; -extern CClientState * CCS; - -/// CGameInfo class -/// for allowing different functions for accessing game informations -class CGameInfo : public Services -{ -public: - const ArtifactService * artifacts() const override; - const CreatureService * creatures() const override; - const FactionService * factions() const override; - const HeroClassService * heroClasses() const override; - const HeroTypeService * heroTypes() const override; -#if SCRIPTING_ENABLED - const scripting::Service * scripts() const override; -#endif - const spells::Service * spells() const override; - const SkillService * skills() const override; - const BattleFieldService * battlefields() const override; - const ObstacleService * obstacles() const override; - const IGameSettings * settings() const override; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; - - const spells::effects::Registry * spellEffects() const override; - spells::effects::Registry * spellEffects() override; - - ConstTransitivePtr modh; //public? - ConstTransitivePtr battleFieldHandler; - ConstTransitivePtr heroh; - ConstTransitivePtr creh; - ConstTransitivePtr spellh; - ConstTransitivePtr skillh; - ConstTransitivePtr objh; - ConstTransitivePtr terrainTypeHandler; - ConstTransitivePtr objtypeh; - ConstTransitivePtr obstacleHandler; - CGeneralTextHandler * generaltexth; - CMapHandler * mh; - CTownHandler * townh; - - void setFromLib(); - - CGameInfo(); -private: - const Services * globalServices; -}; -extern const CGameInfo* CGI; +/* + * CGameInfo.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 + +#include "../lib/ConstTransitivePtr.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CModHandler; +class CHeroHandler; +class CCreatureHandler; +class CSpellHandler; +class CSkillHandler; +class CBuildingHandler; +class CObjectHandler; +class CObjectClassesHandler; +class CTownHandler; +class CGeneralTextHandler; +class CConsoleHandler; +class CGameState; +class BattleFieldHandler; +class ObstacleHandler; +class TerrainTypeHandler; + +class CMap; + +VCMI_LIB_NAMESPACE_END + +class CMapHandler; +class CSoundHandler; +class CMusicHandler; +class CursorHandler; +class IMainVideoPlayer; +class CServerHandler; + +//a class for non-mechanical client GUI classes +class CClientState +{ +public: + CSoundHandler * soundh; + CMusicHandler * musich; + CConsoleHandler * consoleh; + CursorHandler * curh; + IMainVideoPlayer * videoh; +}; +extern CClientState * CCS; + +/// CGameInfo class +/// for allowing different functions for accessing game informations +class CGameInfo : public Services +{ +public: + const ArtifactService * artifacts() const override; + const CreatureService * creatures() const override; + const FactionService * factions() const override; + const HeroClassService * heroClasses() const override; + const HeroTypeService * heroTypes() const override; +#if SCRIPTING_ENABLED + const scripting::Service * scripts() const override; +#endif + const spells::Service * spells() const override; + const SkillService * skills() const override; + const BattleFieldService * battlefields() const override; + const ObstacleService * obstacles() const override; + const IGameSettings * settings() const override; + + void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + + const spells::effects::Registry * spellEffects() const override; + spells::effects::Registry * spellEffects() override; + + ConstTransitivePtr modh; //public? + ConstTransitivePtr battleFieldHandler; + ConstTransitivePtr heroh; + ConstTransitivePtr creh; + ConstTransitivePtr spellh; + ConstTransitivePtr skillh; + ConstTransitivePtr objh; + ConstTransitivePtr terrainTypeHandler; + ConstTransitivePtr objtypeh; + ConstTransitivePtr obstacleHandler; + CGeneralTextHandler * generaltexth; + CMapHandler * mh; + CTownHandler * townh; + + void setFromLib(); + + CGameInfo(); +private: + const Services * globalServices; +}; +extern const CGameInfo* CGI; diff --git a/client/CMT.cpp b/client/CMT.cpp index 27014dc23..2a1c40a8a 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -1,529 +1,529 @@ -/* - * CMT.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 - * - */ - -// CMT.cpp : Defines the entry point for the console application. -#include "StdInc.h" -#include "CMT.h" - -#include "CGameInfo.h" -#include "mainmenu/CMainMenu.h" -#include "gui/CursorHandler.h" -#include "eventsSDL/InputHandler.h" -#include "CPlayerInterface.h" -#include "CVideoHandler.h" -#include "CMusicHandler.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "CServerHandler.h" -#include "ClientCommandManager.h" -#include "windows/CMessage.h" -#include "render/IScreenHandler.h" -#include "render/Graphics.h" - -#include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CThreadHelper.h" -#include "../lib/VCMIDirs.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/filesystem/Filesystem.h" - -#include "../lib/logging/CBasicLogConfigurator.h" - -#include -#include - -#include -#include - -#ifdef VCMI_ANDROID -#include "../lib/CAndroidVMHelper.h" -#include -#endif - -#if __MINGW32__ -#undef main -#endif - -namespace po = boost::program_options; -namespace po_style = boost::program_options::command_line_style; - -static po::variables_map vm; - -#ifndef VCMI_IOS -void processCommand(const std::string &message); -#endif -void playIntro(); -static void mainLoop(); - -static CBasicLogConfigurator *logConfig; - -void init() -{ - CStopWatch tmh; - - loadDLLClasses(); - const_cast(CGI)->setFromLib(); - - logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); - - // Debug code to load all maps on start - //ClientCommandManager commandController; - //commandController.processCommand("convert txt", false); -} - -static void prog_version() -{ - printf("%s\n", GameConstants::VCMI_VERSION.c_str()); - std::cout << VCMIDirs::get().genHelpString(); -} - -static void prog_help(const po::options_description &opts) -{ - auto time = std::time(0); - printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); - printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); - printf("This is free software; see the source for copying conditions. There is NO\n"); - printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); - printf("\n"); - std::cout << opts; -} - -#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE) -int wmain(int argc, wchar_t* argv[]) -#elif defined(VCMI_MOBILE) -int SDL_main(int argc, char *argv[]) -#else -int main(int argc, char * argv[]) -#endif -{ -#ifdef VCMI_ANDROID - CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv()); - // boost will crash without this - setenv("LANG", "C", 1); -#endif - -#if !defined(VCMI_MOBILE) - // Correct working dir executable folder (not bundle folder) so we can use executable relative paths - boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); -#endif - std::cout << "Starting... " << std::endl; - po::options_description opts("Allowed options"); - opts.add_options() - ("help,h", "display help and exit") - ("version,v", "display version information and exit") - ("testmap", po::value(), "") - ("testsave", po::value(), "") - ("spectate,s", "enable spectator interface for AI-only games") - ("spectate-ignore-hero", "wont follow heroes on adventure map") - ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") - ("spectate-battle-speed", po::value(), "battle animation speed for spectator") - ("spectate-skip-battle", "skip battles in spectator view") - ("spectate-skip-battle-result", "skip battle result window") - ("onlyAI", "allow one to run without human player, all players will be default AI") - ("headless", "runs without GUI, implies --onlyAI") - ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") - ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") - ("autoSkip", "automatically skip turns in GUI") - ("disable-video", "disable video player") - ("nointro,i", "skips intro movies") - ("donotstartserver,d","do not attempt to start server and just connect to it instead server") - ("serverport", po::value(), "override port specified in config file") - ("savefrequency", po::value(), "limit auto save creation to each N days") - ("lobby", "parameters address, port, uuid to connect ro remote lobby session") - ("lobby-address", po::value(), "address to remote lobby") - ("lobby-port", po::value(), "port to remote lobby") - ("lobby-host", "if this client hosts session") - ("lobby-uuid", po::value(), "uuid to the server") - ("lobby-connections", po::value(), "connections of server") - ("lobby-username", po::value(), "player name") - ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") - ("uuid", po::value(), "uuid for the client"); - - if(argc > 1) - { - try - { - po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); - } - catch(boost::program_options::error &e) - { - std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; - } - } - - po::notify(vm); - if(vm.count("help")) - { - prog_help(opts); -#ifdef VCMI_IOS - exit(0); -#else - return 0; -#endif - } - if(vm.count("version")) - { - prog_version(); -#ifdef VCMI_IOS - exit(0); -#else - return 0; -#endif - } - - // Init old logging system and new (temporary) logging system - CStopWatch total, pomtime; - std::cout.flags(std::ios::unitbuf); -#ifndef VCMI_IOS - console = new CConsoleHandler(); - - auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole) - { - ClientCommandManager commandController; - commandController.processCommand(buffer, calledFromIngameConsole); - }; - - *console->cb = callbackFunction; - console->start(); -#endif - - const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; - logConfig = new CBasicLogConfigurator(logPath, console); - logConfig->configureDefault(); - logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION); - logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); - logGlobal->info("The log file will be saved to %s", logPath); - - // Init filesystem and settings - preinitDLL(::console); - - Settings session = settings.write["session"]; - auto setSettingBool = [](std::string key, std::string arg) { - Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) - s->Bool() = true; - else if(s->isNull()) - s->Bool() = false; - }; - auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { - Settings s = settings.write(vstd::split(key, "/")); - if(::vm.count(arg)) - s->Integer() = ::vm[arg].as(); - else if(s->isNull()) - s->Integer() = defaultValue; - }; - - setSettingBool("session/onlyai", "onlyAI"); - if(vm.count("headless")) - { - session["headless"].Bool() = true; - session["onlyai"].Bool() = true; - } - else if(vm.count("spectate")) - { - session["spectate"].Bool() = true; - session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); - session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); - session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); - if(vm.count("spectate-hero-speed")) - session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); - if(vm.count("spectate-battle-speed")) - session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); - } - // Server settings - setSettingBool("session/donotstartserver", "donotstartserver"); - - // Init special testing settings - setSettingInteger("session/serverport", "serverport", 0); - setSettingInteger("general/saveFrequency", "savefrequency", 1); - - // Initialize logging based on settings - logConfig->configure(); - logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); - - // Some basic data validation to produce better error messages in cases of incorrect install - auto testFile = [](std::string filename, std::string message) - { - if (!CResourceHandler::get()->existsResource(ResourcePath(filename))) - handleFatalError(message, false); - }; - - testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!"); - testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); - testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them."); - testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them."); - testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); - - srand ( (unsigned int)time(nullptr) ); - - if(!settings["session"]["headless"].Bool()) - GH.init(); - - CCS = new CClientState(); - CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) - CSH = new CServerHandler(); - - // Initialize video -#ifdef DISABLE_VIDEO - CCS->videoh = new CEmptyVideoPlayer(); -#else - if (!settings["session"]["headless"].Bool() && !vm.count("disable-video")) - CCS->videoh = new CVideoPlayer(); - else - CCS->videoh = new CEmptyVideoPlayer(); -#endif - - logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); - - if(!settings["session"]["headless"].Bool()) - { - //initializing audio - CCS->soundh = new CSoundHandler(); - CCS->soundh->init(); - CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float()); - CCS->musich = new CMusicHandler(); - CCS->musich->init(); - CCS->musich->setVolume((ui32)settings["general"]["music"].Float()); - logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); - } - -#ifndef VCMI_NO_THREADED_LOAD - //we can properly play intro only in the main thread, so we have to move loading to the separate thread - boost::thread loading([]() - { - setThreadName("initialize"); - init(); - }); -#else - init(); -#endif - - if(!settings["session"]["headless"].Bool()) - { - if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool()) - playIntro(); - GH.screenHandler().clearScreen(); - } - - -#ifndef VCMI_NO_THREADED_LOAD - #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds - { - CAndroidVMHelper vmHelper; - vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress"); - #endif // ANDROID - loading.join(); - #ifdef VCMI_ANDROID - vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress"); - } - #endif // ANDROID -#endif // THREADED - - if(!settings["session"]["headless"].Bool()) - { - pomtime.getDiff(); - graphics = new Graphics(); // should be before curh - - CCS->curh = new CursorHandler(); - logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); - - CMessage::init(); - logGlobal->info("Message handler: %d ms", pomtime.getDiff()); - - CCS->curh->show(); - } - - logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff()); - - session["autoSkip"].Bool() = vm.count("autoSkip"); - session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); - session["aiSolo"].Bool() = false; - std::shared_ptr mmenu; - - if(vm.count("testmap")) - { - session["testmap"].String() = vm["testmap"].as(); - session["onlyai"].Bool() = true; - boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); - } - else if(vm.count("testsave")) - { - session["testsave"].String() = vm["testsave"].as(); - session["onlyai"].Bool() = true; - boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); - } - else - { - mmenu = CMainMenu::create(); - GH.curInt = mmenu.get(); - } - - std::vector names; - session["lobby"].Bool() = false; - if(vm.count("lobby")) - { - session["lobby"].Bool() = true; - session["host"].Bool() = false; - session["address"].String() = vm["lobby-address"].as(); - if(vm.count("lobby-username")) - session["username"].String() = vm["lobby-username"].as(); - else - session["username"].String() = settings["launcher"]["lobbyUsername"].String(); - if(vm.count("lobby-gamemode")) - session["gamemode"].Integer() = vm["lobby-gamemode"].as(); - else - session["gamemode"].Integer() = 0; - CSH->uuid = vm["uuid"].as(); - session["port"].Integer() = vm["lobby-port"].as(); - logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); - if(vm.count("lobby-host")) - { - session["host"].Bool() = true; - session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); - session["hostUuid"].String() = vm["lobby-uuid"].as(); - logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); - } - - //we should not reconnect to previous game in online mode - Settings saveSession = settings.write["server"]["reconnect"]; - saveSession->Bool() = false; - - //start lobby immediately - names.push_back(session["username"].String()); - ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; - mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); - } - - // Restore remote session - start game immediately - if(settings["server"]["reconnect"].Bool()) - { - CSH->restoreLastSession(); - } - - if(!settings["session"]["headless"].Bool()) - { - mainLoop(); - } - else - { - while(true) - boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); - } - - return 0; -} - -//plays intro, ends when intro is over or button has been pressed (handles events) -void playIntro() -{ - auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); - int sound = CCS->soundh->playSound(audioData); - if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); - sound = CCS->soundh->playSound(audioData); - if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) - { - audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); - sound = CCS->soundh->playSound(audioData); - CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); - } - } - CCS->soundh->stopSound(sound); -} - -static void mainLoop() -{ - setThreadName("MainGUI"); - - while(1) //main SDL events loop - { - GH.input().fetchEvents(); - CSH->applyPacksOnLobbyScreen(); - GH.renderFrame(); - } -} - -static void quitApplication() -{ - if(!settings["session"]["headless"].Bool()) - { - if(CSH->client) - CSH->endGameplay(); - } - - GH.windows().clear(); - - CMM.reset(); - - if(!settings["session"]["headless"].Bool()) - { - // cleanup, mostly to remove false leaks from analyzer - if(CCS) - { - CCS->musich->release(); - CCS->soundh->release(); - - vstd::clear_pointer(CCS); - } - CMessage::dispose(); - - vstd::clear_pointer(graphics); - } - - vstd::clear_pointer(VLC); - - vstd::clear_pointer(console);// should be removed after everything else since used by logging - - if(!settings["session"]["headless"].Bool()) - GH.screenHandler().close(); - - if(logConfig != nullptr) - { - logConfig->deconfigure(); - delete logConfig; - logConfig = nullptr; - } - - std::cout << "Ending...\n"; - - // this method is always called from event/network threads, which keep interface mutex locked - // unlock it here to avoid assertion failure on GH destruction in exit() - GH.interfaceMutex.unlock(); - exit(0); -} - -void handleQuit(bool ask) -{ - if(CSH->client && LOCPLINT && ask) - { - CCS->curh->set(Cursor::Map::POINTER); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); - } - else - { - quitApplication(); - } -} - -void handleFatalError(const std::string & message, bool terminate) -{ - logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); - logGlobal->error("Reason: %s", message); - - std::string messageToShow = "Fatal error! " + message; - - SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr); - - if (terminate) - throw std::runtime_error(message); - else - exit(1); -} +/* + * CMT.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 + * + */ + +// CMT.cpp : Defines the entry point for the console application. +#include "StdInc.h" +#include "CMT.h" + +#include "CGameInfo.h" +#include "mainmenu/CMainMenu.h" +#include "gui/CursorHandler.h" +#include "eventsSDL/InputHandler.h" +#include "CPlayerInterface.h" +#include "CVideoHandler.h" +#include "CMusicHandler.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "CServerHandler.h" +#include "ClientCommandManager.h" +#include "windows/CMessage.h" +#include "render/IScreenHandler.h" +#include "render/Graphics.h" + +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/filesystem/Filesystem.h" + +#include "../lib/logging/CBasicLogConfigurator.h" + +#include +#include + +#include +#include + +#ifdef VCMI_ANDROID +#include "../lib/CAndroidVMHelper.h" +#include +#endif + +#if __MINGW32__ +#undef main +#endif + +namespace po = boost::program_options; +namespace po_style = boost::program_options::command_line_style; + +static po::variables_map vm; + +#ifndef VCMI_IOS +void processCommand(const std::string &message); +#endif +void playIntro(); +static void mainLoop(); + +static CBasicLogConfigurator *logConfig; + +void init() +{ + CStopWatch tmh; + + loadDLLClasses(); + const_cast(CGI)->setFromLib(); + + logGlobal->info("Initializing VCMI_Lib: %d ms", tmh.getDiff()); + + // Debug code to load all maps on start + //ClientCommandManager commandController; + //commandController.processCommand("convert txt", false); +} + +static void prog_version() +{ + printf("%s\n", GameConstants::VCMI_VERSION.c_str()); + std::cout << VCMIDirs::get().genHelpString(); +} + +static void prog_help(const po::options_description &opts) +{ + auto time = std::time(0); + printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); + printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); + printf("This is free software; see the source for copying conditions. There is NO\n"); + printf("warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.\n"); + printf("\n"); + std::cout << opts; +} + +#if defined(VCMI_WINDOWS) && !defined(__GNUC__) && defined(VCMI_WITH_DEBUG_CONSOLE) +int wmain(int argc, wchar_t* argv[]) +#elif defined(VCMI_MOBILE) +int SDL_main(int argc, char *argv[]) +#else +int main(int argc, char * argv[]) +#endif +{ +#ifdef VCMI_ANDROID + CAndroidVMHelper::initClassloader(SDL_AndroidGetJNIEnv()); + // boost will crash without this + setenv("LANG", "C", 1); +#endif + +#if !defined(VCMI_MOBILE) + // Correct working dir executable folder (not bundle folder) so we can use executable relative paths + boost::filesystem::current_path(boost::filesystem::system_complete(argv[0]).parent_path()); +#endif + std::cout << "Starting... " << std::endl; + po::options_description opts("Allowed options"); + opts.add_options() + ("help,h", "display help and exit") + ("version,v", "display version information and exit") + ("testmap", po::value(), "") + ("testsave", po::value(), "") + ("spectate,s", "enable spectator interface for AI-only games") + ("spectate-ignore-hero", "wont follow heroes on adventure map") + ("spectate-hero-speed", po::value(), "hero movement speed on adventure map") + ("spectate-battle-speed", po::value(), "battle animation speed for spectator") + ("spectate-skip-battle", "skip battles in spectator view") + ("spectate-skip-battle-result", "skip battle result window") + ("onlyAI", "allow one to run without human player, all players will be default AI") + ("headless", "runs without GUI, implies --onlyAI") + ("ai", po::value>(), "AI to be used for the player, can be specified several times for the consecutive players") + ("oneGoodAI", "puts one default AI and the rest will be EmptyAI") + ("autoSkip", "automatically skip turns in GUI") + ("disable-video", "disable video player") + ("nointro,i", "skips intro movies") + ("donotstartserver,d","do not attempt to start server and just connect to it instead server") + ("serverport", po::value(), "override port specified in config file") + ("savefrequency", po::value(), "limit auto save creation to each N days") + ("lobby", "parameters address, port, uuid to connect ro remote lobby session") + ("lobby-address", po::value(), "address to remote lobby") + ("lobby-port", po::value(), "port to remote lobby") + ("lobby-host", "if this client hosts session") + ("lobby-uuid", po::value(), "uuid to the server") + ("lobby-connections", po::value(), "connections of server") + ("lobby-username", po::value(), "player name") + ("lobby-gamemode", po::value(), "use 0 for new game and 1 for load game") + ("uuid", po::value(), "uuid for the client"); + + if(argc > 1) + { + try + { + po::store(po::parse_command_line(argc, argv, opts, po_style::unix_style|po_style::case_insensitive), vm); + } + catch(boost::program_options::error &e) + { + std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl; + } + } + + po::notify(vm); + if(vm.count("help")) + { + prog_help(opts); +#ifdef VCMI_IOS + exit(0); +#else + return 0; +#endif + } + if(vm.count("version")) + { + prog_version(); +#ifdef VCMI_IOS + exit(0); +#else + return 0; +#endif + } + + // Init old logging system and new (temporary) logging system + CStopWatch total, pomtime; + std::cout.flags(std::ios::unitbuf); +#ifndef VCMI_IOS + console = new CConsoleHandler(); + + auto callbackFunction = [](std::string buffer, bool calledFromIngameConsole) + { + ClientCommandManager commandController; + commandController.processCommand(buffer, calledFromIngameConsole); + }; + + *console->cb = callbackFunction; + console->start(); +#endif + + const boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "VCMI_Client_log.txt"; + logConfig = new CBasicLogConfigurator(logPath, console); + logConfig->configureDefault(); + logGlobal->info("Starting client of '%s'", GameConstants::VCMI_VERSION); + logGlobal->info("Creating console and configuring logger: %d ms", pomtime.getDiff()); + logGlobal->info("The log file will be saved to %s", logPath); + + // Init filesystem and settings + preinitDLL(::console); + + Settings session = settings.write["session"]; + auto setSettingBool = [](std::string key, std::string arg) { + Settings s = settings.write(vstd::split(key, "/")); + if(::vm.count(arg)) + s->Bool() = true; + else if(s->isNull()) + s->Bool() = false; + }; + auto setSettingInteger = [](std::string key, std::string arg, si64 defaultValue) { + Settings s = settings.write(vstd::split(key, "/")); + if(::vm.count(arg)) + s->Integer() = ::vm[arg].as(); + else if(s->isNull()) + s->Integer() = defaultValue; + }; + + setSettingBool("session/onlyai", "onlyAI"); + if(vm.count("headless")) + { + session["headless"].Bool() = true; + session["onlyai"].Bool() = true; + } + else if(vm.count("spectate")) + { + session["spectate"].Bool() = true; + session["spectate-ignore-hero"].Bool() = vm.count("spectate-ignore-hero"); + session["spectate-skip-battle"].Bool() = vm.count("spectate-skip-battle"); + session["spectate-skip-battle-result"].Bool() = vm.count("spectate-skip-battle-result"); + if(vm.count("spectate-hero-speed")) + session["spectate-hero-speed"].Integer() = vm["spectate-hero-speed"].as(); + if(vm.count("spectate-battle-speed")) + session["spectate-battle-speed"].Float() = vm["spectate-battle-speed"].as(); + } + // Server settings + setSettingBool("session/donotstartserver", "donotstartserver"); + + // Init special testing settings + setSettingInteger("session/serverport", "serverport", 0); + setSettingInteger("general/saveFrequency", "savefrequency", 1); + + // Initialize logging based on settings + logConfig->configure(); + logGlobal->debug("settings = %s", settings.toJsonNode().toJson()); + + // Some basic data validation to produce better error messages in cases of incorrect install + auto testFile = [](std::string filename, std::string message) + { + if (!CResourceHandler::get()->existsResource(ResourcePath(filename))) + handleFatalError(message, false); + }; + + testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!"); + testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!"); + testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them."); + testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them."); + testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!"); + + srand ( (unsigned int)time(nullptr) ); + + if(!settings["session"]["headless"].Bool()) + GH.init(); + + CCS = new CClientState(); + CGI = new CGameInfo(); //contains all global informations about game (texts, lodHandlers, map handler etc.) + CSH = new CServerHandler(); + + // Initialize video +#ifdef DISABLE_VIDEO + CCS->videoh = new CEmptyVideoPlayer(); +#else + if (!settings["session"]["headless"].Bool() && !vm.count("disable-video")) + CCS->videoh = new CVideoPlayer(); + else + CCS->videoh = new CEmptyVideoPlayer(); +#endif + + logGlobal->info("\tInitializing video: %d ms", pomtime.getDiff()); + + if(!settings["session"]["headless"].Bool()) + { + //initializing audio + CCS->soundh = new CSoundHandler(); + CCS->soundh->init(); + CCS->soundh->setVolume((ui32)settings["general"]["sound"].Float()); + CCS->musich = new CMusicHandler(); + CCS->musich->init(); + CCS->musich->setVolume((ui32)settings["general"]["music"].Float()); + logGlobal->info("Initializing screen and sound handling: %d ms", pomtime.getDiff()); + } + +#ifndef VCMI_NO_THREADED_LOAD + //we can properly play intro only in the main thread, so we have to move loading to the separate thread + boost::thread loading([]() + { + setThreadName("initialize"); + init(); + }); +#else + init(); +#endif + + if(!settings["session"]["headless"].Bool()) + { + if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool()) + playIntro(); + GH.screenHandler().clearScreen(); + } + + +#ifndef VCMI_NO_THREADED_LOAD + #ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds + { + CAndroidVMHelper vmHelper; + vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "showProgress"); + #endif // ANDROID + loading.join(); + #ifdef VCMI_ANDROID + vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hideProgress"); + } + #endif // ANDROID +#endif // THREADED + + if(!settings["session"]["headless"].Bool()) + { + pomtime.getDiff(); + graphics = new Graphics(); // should be before curh + + CCS->curh = new CursorHandler(); + logGlobal->info("Screen handler: %d ms", pomtime.getDiff()); + + CMessage::init(); + logGlobal->info("Message handler: %d ms", pomtime.getDiff()); + + CCS->curh->show(); + } + + logGlobal->info("Initialization of VCMI (together): %d ms", total.getDiff()); + + session["autoSkip"].Bool() = vm.count("autoSkip"); + session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); + session["aiSolo"].Bool() = false; + std::shared_ptr mmenu; + + if(vm.count("testmap")) + { + session["testmap"].String() = vm["testmap"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testmap"].String(), false); + } + else if(vm.count("testsave")) + { + session["testsave"].String() = vm["testsave"].as(); + session["onlyai"].Bool() = true; + boost::thread(&CServerHandler::debugStartTest, CSH, session["testsave"].String(), true); + } + else + { + mmenu = CMainMenu::create(); + GH.curInt = mmenu.get(); + } + + std::vector names; + session["lobby"].Bool() = false; + if(vm.count("lobby")) + { + session["lobby"].Bool() = true; + session["host"].Bool() = false; + session["address"].String() = vm["lobby-address"].as(); + if(vm.count("lobby-username")) + session["username"].String() = vm["lobby-username"].as(); + else + session["username"].String() = settings["launcher"]["lobbyUsername"].String(); + if(vm.count("lobby-gamemode")) + session["gamemode"].Integer() = vm["lobby-gamemode"].as(); + else + session["gamemode"].Integer() = 0; + CSH->uuid = vm["uuid"].as(); + session["port"].Integer() = vm["lobby-port"].as(); + logGlobal->info("Remote lobby mode at %s:%d, uuid is %s", session["address"].String(), session["port"].Integer(), CSH->uuid); + if(vm.count("lobby-host")) + { + session["host"].Bool() = true; + session["hostConnections"].String() = std::to_string(vm["lobby-connections"].as()); + session["hostUuid"].String() = vm["lobby-uuid"].as(); + logGlobal->info("This client will host session, server uuid is %s", session["hostUuid"].String()); + } + + //we should not reconnect to previous game in online mode + Settings saveSession = settings.write["server"]["reconnect"]; + saveSession->Bool() = false; + + //start lobby immediately + names.push_back(session["username"].String()); + ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; + mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); + } + + // Restore remote session - start game immediately + if(settings["server"]["reconnect"].Bool()) + { + CSH->restoreLastSession(); + } + + if(!settings["session"]["headless"].Bool()) + { + mainLoop(); + } + else + { + while(true) + boost::this_thread::sleep_for(boost::chrono::milliseconds(1000)); + } + + return 0; +} + +//plays intro, ends when intro is over or button has been pressed (handles events) +void playIntro() +{ + auto audioData = CCS->videoh->getAudio(VideoPath::builtin("3DOLOGO.SMK")); + int sound = CCS->soundh->playSound(audioData); + if(CCS->videoh->openAndPlayVideo(VideoPath::builtin("3DOLOGO.SMK"), 0, 1, true, true)) + { + audioData = CCS->videoh->getAudio(VideoPath::builtin("NWCLOGO.SMK")); + sound = CCS->soundh->playSound(audioData); + if (CCS->videoh->openAndPlayVideo(VideoPath::builtin("NWCLOGO.SMK"), 0, 1, true, true)) + { + audioData = CCS->videoh->getAudio(VideoPath::builtin("H3INTRO.SMK")); + sound = CCS->soundh->playSound(audioData); + CCS->videoh->openAndPlayVideo(VideoPath::builtin("H3INTRO.SMK"), 0, 1, true, true); + } + } + CCS->soundh->stopSound(sound); +} + +static void mainLoop() +{ + setThreadName("MainGUI"); + + while(1) //main SDL events loop + { + GH.input().fetchEvents(); + CSH->applyPacksOnLobbyScreen(); + GH.renderFrame(); + } +} + +static void quitApplication() +{ + if(!settings["session"]["headless"].Bool()) + { + if(CSH->client) + CSH->endGameplay(); + } + + GH.windows().clear(); + + CMM.reset(); + + if(!settings["session"]["headless"].Bool()) + { + // cleanup, mostly to remove false leaks from analyzer + if(CCS) + { + CCS->musich->release(); + CCS->soundh->release(); + + vstd::clear_pointer(CCS); + } + CMessage::dispose(); + + vstd::clear_pointer(graphics); + } + + vstd::clear_pointer(VLC); + + vstd::clear_pointer(console);// should be removed after everything else since used by logging + + if(!settings["session"]["headless"].Bool()) + GH.screenHandler().close(); + + if(logConfig != nullptr) + { + logConfig->deconfigure(); + delete logConfig; + logConfig = nullptr; + } + + std::cout << "Ending...\n"; + + // this method is always called from event/network threads, which keep interface mutex locked + // unlock it here to avoid assertion failure on GH destruction in exit() + GH.interfaceMutex.unlock(); + exit(0); +} + +void handleQuit(bool ask) +{ + if(CSH->client && LOCPLINT && ask) + { + CCS->curh->set(Cursor::Map::POINTER); + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + } + else + { + quitApplication(); + } +} + +void handleFatalError(const std::string & message, bool terminate) +{ + logGlobal->error("FATAL ERROR ENCOUTERED, VCMI WILL NOW TERMINATE"); + logGlobal->error("Reason: %s", message); + + std::string messageToShow = "Fatal error! " + message; + + SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "Fatal error!", messageToShow.c_str(), nullptr); + + if (terminate) + throw std::runtime_error(message); + else + exit(1); +} diff --git a/client/CMusicHandler.cpp b/client/CMusicHandler.cpp index efbc41ce1..b6101c7cb 100644 --- a/client/CMusicHandler.cpp +++ b/client/CMusicHandler.cpp @@ -1,718 +1,718 @@ -/* - * CMusicHandler.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 -#include - -#include "CMusicHandler.h" -#include "CGameInfo.h" -#include "renderSDL/SDLRWwrapper.h" -#include "eventsSDL/InputHandler.h" -#include "gui/CGuiHandler.h" - -#include "../lib/JsonNode.h" -#include "../lib/GameConstants.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/constants/StringConstants.h" -#include "../lib/CRandomGenerator.h" -#include "../lib/VCMIDirs.h" -#include "../lib/TerrainHandler.h" - - -#define VCMI_SOUND_NAME(x) -#define VCMI_SOUND_FILE(y) #y, - -// sounds mapped to soundBase enum -static std::string sounds[] = { - "", // invalid - "", // todo - VCMI_SOUND_LIST -}; -#undef VCMI_SOUND_NAME -#undef VCMI_SOUND_FILE - -void CAudioBase::init() -{ - if (initialized) - return; - - if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) - { - logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); - return; - } - - initialized = true; -} - -void CAudioBase::release() -{ - if(!(CCS->soundh->initialized && CCS->musich->initialized)) - Mix_CloseAudio(); - - initialized = false; -} - -void CAudioBase::setVolume(ui32 percent) -{ - if (percent > 100) - percent = 100; - - volume = percent; -} - -void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CSoundHandler::CSoundHandler(): - listener(settings.listen["general"]["sound"]), - ambientConfig(JsonPath::builtin("config/ambientSounds.json")) -{ - listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); - - battleIntroSounds = - { - soundBase::battle00, soundBase::battle01, - soundBase::battle02, soundBase::battle03, soundBase::battle04, - soundBase::battle05, soundBase::battle06, soundBase::battle07 - }; -} - -void CSoundHandler::init() -{ - CAudioBase::init(); - if(ambientConfig["allocateChannels"].isNumber()) - Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer()); - - if (initialized) - { - Mix_ChannelFinished([](int channel) - { - CCS->soundh->soundFinishedCallback(channel); - }); - } -} - -void CSoundHandler::release() -{ - if (initialized) - { - Mix_HaltChannel(-1); - - for (auto &chunk : soundChunks) - { - if (chunk.second.first) - Mix_FreeChunk(chunk.second.first); - } - } - - CAudioBase::release(); -} - -// Allocate an SDL chunk and cache it. -Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) -{ - try - { - if (cache && soundChunks.find(sound) != soundChunks.end()) - return soundChunks[sound].first; - - auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); - return nullptr; - } -} - -Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) -{ - try - { - std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); - - if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) - return soundChunksRaw[startBytes].first; - - SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); - Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops - - if (cache) - soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); - - return chunk; - } - catch(std::exception &e) - { - logGlobal->warn("Cannot get sound chunk: %s", e.what()); - return nullptr; - } -} - -int CSoundHandler::ambientDistToVolume(int distance) const -{ - const auto & distancesVector = ambientConfig["distances"].Vector(); - - if(distance >= distancesVector.size()) - return 0; - - int volume = static_cast(distancesVector[distance].Integer()); - return volume * (int)ambientConfig["volume"].Integer() / 100; -} - -void CSoundHandler::ambientStopSound(const AudioPath & soundId) -{ - stopSound(ambientChannels[soundId]); - setChannelVolume(ambientChannels[soundId], volume); -} - -// Plays a sound, and return its channel so we can fade it out later -int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) -{ - assert(soundID < soundBase::sound_after_last); - auto sound = AudioPath::builtin(sounds[soundID]); - logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); - - return playSound(sound, repeats, true); -} - -int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) -{ - if (!initialized || sound.empty()) - return -1; - - int channel; - Mix_Chunk *chunk = GetSoundChunk(sound, cache); - - if (chunk) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - else - channel = -1; - - return channel; -} - -int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) -{ - int channel = -1; - if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) - { - channel = Mix_PlayChannel(-1, chunk, repeats); - if (channel == -1) - { - logGlobal->error("Unable to play sound, error %s", Mix_GetError()); - if (!cache) - Mix_FreeChunk(chunk); - } - else if (cache) - initCallback(channel); - else - initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); - } - return channel; -} - -// Helper. Randomly select a sound from an array and play it -int CSoundHandler::playSoundFromSet(std::vector &sound_vec) -{ - return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); -} - -void CSoundHandler::stopSound(int handler) -{ - if (initialized && handler != -1) - Mix_HaltChannel(handler); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - { - setChannelVolume(-1, volume); - - for (auto const & channel : channelVolumes) - updateChannelVolume(channel.first); - } -} - -void CSoundHandler::updateChannelVolume(int channel) -{ - if (channelVolumes.count(channel)) - setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); - else - setChannelVolume(channel, getVolume()); -} - -// Sets the sound volume, from 0 (mute) to 100 -void CSoundHandler::setChannelVolume(int channel, ui32 percent) -{ - Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); -} - -void CSoundHandler::setCallback(int channel, std::function function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - auto iter = callbacks.find(channel); - - //channel not found. It may have finished so fire callback now - if(iter == callbacks.end()) - function(); - else - iter->second.push_back(function); -} - -void CSoundHandler::soundFinishedCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - - if (callbacks.count(channel) == 0) - return; - - // store callbacks from container locally - SDL might reuse this channel for another sound - // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own - auto callback = callbacks.at(channel); - callbacks.erase(channel); - - if (!callback.empty()) - { - GH.dispatchMainThread([callback](){ - for (auto entry : callback) - entry(); - }); - } -} - -void CSoundHandler::initCallback(int channel) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel] = {}; -} - -void CSoundHandler::initCallback(int channel, const std::function & function) -{ - boost::mutex::scoped_lock lockGuard(mutexCallbacks); - assert(callbacks.count(channel) == 0); - callbacks[channel].push_back(function); -} - -int CSoundHandler::ambientGetRange() const -{ - return static_cast(ambientConfig["range"].Integer()); -} - -void CSoundHandler::ambientUpdateChannels(std::map soundsArg) -{ - boost::mutex::scoped_lock guard(mutex); - - std::vector stoppedSounds; - for(auto & pair : ambientChannels) - { - const auto & soundId = pair.first; - const int channel = pair.second; - - if(!vstd::contains(soundsArg, soundId)) - { - ambientStopSound(soundId); - stoppedSounds.push_back(soundId); - } - else - { - int volume = ambientDistToVolume(soundsArg[soundId]); - channelVolumes[channel] = volume; - updateChannelVolume(channel); - } - } - for(auto soundId : stoppedSounds) - { - channelVolumes.erase(ambientChannels[soundId]); - ambientChannels.erase(soundId); - } - - for(auto & pair : soundsArg) - { - const auto & soundId = pair.first; - const int distance = pair.second; - - if(!vstd::contains(ambientChannels, soundId)) - { - int channel = playSound(soundId, -1); - int volume = ambientDistToVolume(distance); - channelVolumes[channel] = volume; - - updateChannelVolume(channel); - ambientChannels[soundId] = channel; - } - } -} - -void CSoundHandler::ambientStopAllChannels() -{ - boost::mutex::scoped_lock guard(mutex); - - for(auto ch : ambientChannels) - { - ambientStopSound(ch.first); - } - channelVolumes.clear(); - ambientChannels.clear(); -} - -void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) -{ - setVolume((ui32)volumeNode.Float()); -} - -CMusicHandler::CMusicHandler(): - listener(settings.listen["general"]["music"]) -{ - listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); - - auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool - { - if(id.getType() != EResType::SOUND) - return false; - - if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) - return false; - - logGlobal->trace("Found music file %s", id.getName()); - return true; - }); - - for(const ResourcePath & file : mp3files) - { - if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) - addEntryToSet("battle", AudioPath::fromResource(file)); - else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) - addEntryToSet("enemy-turn", AudioPath::fromResource(file)); - } - -} - -void CMusicHandler::loadTerrainMusicThemes() -{ - for (const auto & terrain : CGI->terrainTypeHandler->objects) - { - addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); - } -} - -void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) -{ - musicsSet[set].push_back(musicURI); -} - -void CMusicHandler::init() -{ - CAudioBase::init(); - - if (initialized) - { - Mix_HookMusicFinished([]() - { - CCS->musich->musicFinishedCallback(); - }); - } -} - -void CMusicHandler::release() -{ - if (initialized) - { - boost::mutex::scoped_lock guard(mutex); - - Mix_HookMusicFinished(nullptr); - current->stop(); - - current.reset(); - next.reset(); - } - - CAudioBase::release(); -} - -void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - if (current && current->isPlaying() && current->isTrack(musicURI)) - return; - - queueNext(this, "", musicURI, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) -{ - playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); -} - -void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) -{ - boost::mutex::scoped_lock guard(mutex); - - auto selectedSet = musicsSet.find(whichSet); - if (selectedSet == musicsSet.end()) - { - logGlobal->error("Error: playing music from non-existing set: %s", whichSet); - return; - } - - if (current && current->isPlaying() && current->isSet(whichSet)) - return; - - // in this mode - play random track from set - queueNext(this, whichSet, AudioPath(), loop, fromStart); -} - -void CMusicHandler::queueNext(std::unique_ptr queued) -{ - if (!initialized) - return; - - next = std::move(queued); - - if (current.get() == nullptr || !current->stop(1000)) - { - current.reset(next.release()); - current->play(); - } -} - -void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) -{ - queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); -} - -void CMusicHandler::stopMusic(int fade_ms) -{ - if (!initialized) - return; - - boost::mutex::scoped_lock guard(mutex); - - if (current.get() != nullptr) - current->stop(fade_ms); - next.reset(); -} - -void CMusicHandler::setVolume(ui32 percent) -{ - CAudioBase::setVolume(percent); - - if (initialized) - Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); -} - -void CMusicHandler::musicFinishedCallback() -{ - // call music restart in separate thread to avoid deadlock in some cases - // It is possible for: - // 1) SDL thread to call this method on end of playback - // 2) VCMI code to call queueNext() method to queue new file - // this leads to: - // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) - // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) - - GH.dispatchMainThread([this]() - { - boost::unique_lock lockGuard(mutex); - if (current.get() != nullptr) - { - // if music is looped, play it again - if (current->play()) - return; - else - current.reset(); - } - - if (current.get() == nullptr && next.get() != nullptr) - { - current.reset(next.release()); - current->play(); - } - }); -} - -MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart): - owner(owner), - music(nullptr), - playing(false), - startTime(uint32_t(-1)), - startPosition(0), - loop(looped ? -1 : 1), - fromStart(fromStart), - setName(std::move(setName)) -{ - if (!musicURI.empty()) - load(std::move(musicURI)); -} -MusicEntry::~MusicEntry() -{ - if (playing && loop > 0) - { - assert(0); - logGlobal->error("Attempt to delete music while playing!"); - Mix_HaltMusic(); - } - - if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) - { - assert(0); - logGlobal->error("Attempt to delete music while fading out!"); - Mix_HaltMusic(); - } - - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - if (music) - Mix_FreeMusic(music); -} - -void MusicEntry::load(const AudioPath & musicURI) -{ - if (music) - { - logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); - Mix_FreeMusic(music); - music = nullptr; - } - - if (CResourceHandler::get()->existsResource(musicURI)) - currentName = musicURI; - else - currentName = musicURI.addPrefix("MUSIC/"); - - music = nullptr; - - logGlobal->trace("Loading music file %s", currentName.getOriginalName()); - - try - { - auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); - music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); - } - catch(std::exception &e) - { - logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); - logGlobal->error("Exception: %s", e.what()); - } - - if(!music) - { - logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); - return; - } -} - -bool MusicEntry::play() -{ - if (!(loop--) && music) //already played once - return - return false; - - if (!setName.empty()) - { - const auto & set = owner->musicsSet[setName]; - const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); - load(*iter); - } - - logGlobal->trace("Playing music file %s", currentName.getOriginalName()); - - if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) - { - float timeToStart = owner->trackPositions[currentName]; - startPosition = std::round(timeToStart * 1000); - - // erase stored position: - // if music track will be interrupted again - new position will be written in stop() method - // if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should - owner->trackPositions.erase(owner->trackPositions.find(currentName)); - - if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - else - { - startPosition = 0; - - if(Mix_PlayMusic(music, 1) == -1) - { - logGlobal->error("Unable to play music (%s)", Mix_GetError()); - return false; - } - } - - startTime = GH.input().getTicks(); - - playing = true; - return true; -} - -bool MusicEntry::stop(int fade_ms) -{ - if (Mix_PlayingMusic()) - { - playing = false; - loop = 0; - uint32_t endTime = GH.input().getTicks(); - assert(startTime != uint32_t(-1)); - float playDuration = (endTime - startTime + startPosition) / 1000.f; - owner->trackPositions[currentName] = playDuration; - logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); - - Mix_FadeOutMusic(fade_ms); - return true; - } - return false; -} - -bool MusicEntry::isPlaying() -{ - return playing; -} - -bool MusicEntry::isSet(std::string set) -{ - return !setName.empty() && set == setName; -} - -bool MusicEntry::isTrack(const AudioPath & track) -{ - return setName.empty() && track == currentName; -} +/* + * CMusicHandler.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 +#include + +#include "CMusicHandler.h" +#include "CGameInfo.h" +#include "renderSDL/SDLRWwrapper.h" +#include "eventsSDL/InputHandler.h" +#include "gui/CGuiHandler.h" + +#include "../lib/JsonNode.h" +#include "../lib/GameConstants.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/constants/StringConstants.h" +#include "../lib/CRandomGenerator.h" +#include "../lib/VCMIDirs.h" +#include "../lib/TerrainHandler.h" + + +#define VCMI_SOUND_NAME(x) +#define VCMI_SOUND_FILE(y) #y, + +// sounds mapped to soundBase enum +static std::string sounds[] = { + "", // invalid + "", // todo + VCMI_SOUND_LIST +}; +#undef VCMI_SOUND_NAME +#undef VCMI_SOUND_FILE + +void CAudioBase::init() +{ + if (initialized) + return; + + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 1024)==-1) + { + logGlobal->error("Mix_OpenAudio error: %s", Mix_GetError()); + return; + } + + initialized = true; +} + +void CAudioBase::release() +{ + if(!(CCS->soundh->initialized && CCS->musich->initialized)) + Mix_CloseAudio(); + + initialized = false; +} + +void CAudioBase::setVolume(ui32 percent) +{ + if (percent > 100) + percent = 100; + + volume = percent; +} + +void CSoundHandler::onVolumeChange(const JsonNode &volumeNode) +{ + setVolume((ui32)volumeNode.Float()); +} + +CSoundHandler::CSoundHandler(): + listener(settings.listen["general"]["sound"]), + ambientConfig(JsonPath::builtin("config/ambientSounds.json")) +{ + listener(std::bind(&CSoundHandler::onVolumeChange, this, _1)); + + battleIntroSounds = + { + soundBase::battle00, soundBase::battle01, + soundBase::battle02, soundBase::battle03, soundBase::battle04, + soundBase::battle05, soundBase::battle06, soundBase::battle07 + }; +} + +void CSoundHandler::init() +{ + CAudioBase::init(); + if(ambientConfig["allocateChannels"].isNumber()) + Mix_AllocateChannels((int)ambientConfig["allocateChannels"].Integer()); + + if (initialized) + { + Mix_ChannelFinished([](int channel) + { + CCS->soundh->soundFinishedCallback(channel); + }); + } +} + +void CSoundHandler::release() +{ + if (initialized) + { + Mix_HaltChannel(-1); + + for (auto &chunk : soundChunks) + { + if (chunk.second.first) + Mix_FreeChunk(chunk.second.first); + } + } + + CAudioBase::release(); +} + +// Allocate an SDL chunk and cache it. +Mix_Chunk *CSoundHandler::GetSoundChunk(const AudioPath & sound, bool cache) +{ + try + { + if (cache && soundChunks.find(sound) != soundChunks.end()) + return soundChunks[sound].first; + + auto data = CResourceHandler::get()->load(sound.addPrefix("SOUNDS/"))->readAll(); + SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); + Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if (cache) + soundChunks.insert({sound, std::make_pair (chunk, std::move (data.first))}); + + return chunk; + } + catch(std::exception &e) + { + logGlobal->warn("Cannot get sound %s chunk: %s", sound.getOriginalName(), e.what()); + return nullptr; + } +} + +Mix_Chunk *CSoundHandler::GetSoundChunk(std::pair, si64> & data, bool cache) +{ + try + { + std::vector startBytes = std::vector(data.first.get(), data.first.get() + std::min((si64)100, data.second)); + + if (cache && soundChunksRaw.find(startBytes) != soundChunksRaw.end()) + return soundChunksRaw[startBytes].first; + + SDL_RWops *ops = SDL_RWFromMem(data.first.get(), (int)data.second); + Mix_Chunk *chunk = Mix_LoadWAV_RW(ops, 1); // will free ops + + if (cache) + soundChunksRaw.insert({startBytes, std::make_pair (chunk, std::move (data.first))}); + + return chunk; + } + catch(std::exception &e) + { + logGlobal->warn("Cannot get sound chunk: %s", e.what()); + return nullptr; + } +} + +int CSoundHandler::ambientDistToVolume(int distance) const +{ + const auto & distancesVector = ambientConfig["distances"].Vector(); + + if(distance >= distancesVector.size()) + return 0; + + int volume = static_cast(distancesVector[distance].Integer()); + return volume * (int)ambientConfig["volume"].Integer() / 100; +} + +void CSoundHandler::ambientStopSound(const AudioPath & soundId) +{ + stopSound(ambientChannels[soundId]); + setChannelVolume(ambientChannels[soundId], volume); +} + +// Plays a sound, and return its channel so we can fade it out later +int CSoundHandler::playSound(soundBase::soundID soundID, int repeats) +{ + assert(soundID < soundBase::sound_after_last); + auto sound = AudioPath::builtin(sounds[soundID]); + logGlobal->trace("Attempt to play sound %d with file name %s with cache", soundID, sound.getOriginalName()); + + return playSound(sound, repeats, true); +} + +int CSoundHandler::playSound(const AudioPath & sound, int repeats, bool cache) +{ + if (!initialized || sound.empty()) + return -1; + + int channel; + Mix_Chunk *chunk = GetSoundChunk(sound, cache); + + if (chunk) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if (channel == -1) + { + logGlobal->error("Unable to play sound file %s , error %s", sound.getOriginalName(), Mix_GetError()); + if (!cache) + Mix_FreeChunk(chunk); + } + else if (cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + else + channel = -1; + + return channel; +} + +int CSoundHandler::playSound(std::pair, si64> & data, int repeats, bool cache) +{ + int channel = -1; + if (Mix_Chunk *chunk = GetSoundChunk(data, cache)) + { + channel = Mix_PlayChannel(-1, chunk, repeats); + if (channel == -1) + { + logGlobal->error("Unable to play sound, error %s", Mix_GetError()); + if (!cache) + Mix_FreeChunk(chunk); + } + else if (cache) + initCallback(channel); + else + initCallback(channel, [chunk](){ Mix_FreeChunk(chunk);}); + } + return channel; +} + +// Helper. Randomly select a sound from an array and play it +int CSoundHandler::playSoundFromSet(std::vector &sound_vec) +{ + return playSound(*RandomGeneratorUtil::nextItem(sound_vec, CRandomGenerator::getDefault())); +} + +void CSoundHandler::stopSound(int handler) +{ + if (initialized && handler != -1) + Mix_HaltChannel(handler); +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setVolume(ui32 percent) +{ + CAudioBase::setVolume(percent); + + if (initialized) + { + setChannelVolume(-1, volume); + + for (auto const & channel : channelVolumes) + updateChannelVolume(channel.first); + } +} + +void CSoundHandler::updateChannelVolume(int channel) +{ + if (channelVolumes.count(channel)) + setChannelVolume(channel, getVolume() * channelVolumes[channel] / 100); + else + setChannelVolume(channel, getVolume()); +} + +// Sets the sound volume, from 0 (mute) to 100 +void CSoundHandler::setChannelVolume(int channel, ui32 percent) +{ + Mix_Volume(channel, (MIX_MAX_VOLUME * percent)/100); +} + +void CSoundHandler::setCallback(int channel, std::function function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + auto iter = callbacks.find(channel); + + //channel not found. It may have finished so fire callback now + if(iter == callbacks.end()) + function(); + else + iter->second.push_back(function); +} + +void CSoundHandler::soundFinishedCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + + if (callbacks.count(channel) == 0) + return; + + // store callbacks from container locally - SDL might reuse this channel for another sound + // but do actualy execution in separate thread, to avoid potential deadlocks in case if callback requires locks of its own + auto callback = callbacks.at(channel); + callbacks.erase(channel); + + if (!callback.empty()) + { + GH.dispatchMainThread([callback](){ + for (auto entry : callback) + entry(); + }); + } +} + +void CSoundHandler::initCallback(int channel) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel] = {}; +} + +void CSoundHandler::initCallback(int channel, const std::function & function) +{ + boost::mutex::scoped_lock lockGuard(mutexCallbacks); + assert(callbacks.count(channel) == 0); + callbacks[channel].push_back(function); +} + +int CSoundHandler::ambientGetRange() const +{ + return static_cast(ambientConfig["range"].Integer()); +} + +void CSoundHandler::ambientUpdateChannels(std::map soundsArg) +{ + boost::mutex::scoped_lock guard(mutex); + + std::vector stoppedSounds; + for(auto & pair : ambientChannels) + { + const auto & soundId = pair.first; + const int channel = pair.second; + + if(!vstd::contains(soundsArg, soundId)) + { + ambientStopSound(soundId); + stoppedSounds.push_back(soundId); + } + else + { + int volume = ambientDistToVolume(soundsArg[soundId]); + channelVolumes[channel] = volume; + updateChannelVolume(channel); + } + } + for(auto soundId : stoppedSounds) + { + channelVolumes.erase(ambientChannels[soundId]); + ambientChannels.erase(soundId); + } + + for(auto & pair : soundsArg) + { + const auto & soundId = pair.first; + const int distance = pair.second; + + if(!vstd::contains(ambientChannels, soundId)) + { + int channel = playSound(soundId, -1); + int volume = ambientDistToVolume(distance); + channelVolumes[channel] = volume; + + updateChannelVolume(channel); + ambientChannels[soundId] = channel; + } + } +} + +void CSoundHandler::ambientStopAllChannels() +{ + boost::mutex::scoped_lock guard(mutex); + + for(auto ch : ambientChannels) + { + ambientStopSound(ch.first); + } + channelVolumes.clear(); + ambientChannels.clear(); +} + +void CMusicHandler::onVolumeChange(const JsonNode &volumeNode) +{ + setVolume((ui32)volumeNode.Float()); +} + +CMusicHandler::CMusicHandler(): + listener(settings.listen["general"]["music"]) +{ + listener(std::bind(&CMusicHandler::onVolumeChange, this, _1)); + + auto mp3files = CResourceHandler::get()->getFilteredFiles([](const ResourcePath & id) -> bool + { + if(id.getType() != EResType::SOUND) + return false; + + if(!boost::algorithm::istarts_with(id.getName(), "MUSIC/")) + return false; + + logGlobal->trace("Found music file %s", id.getName()); + return true; + }); + + for(const ResourcePath & file : mp3files) + { + if(boost::algorithm::istarts_with(file.getName(), "MUSIC/Combat")) + addEntryToSet("battle", AudioPath::fromResource(file)); + else if(boost::algorithm::istarts_with(file.getName(), "MUSIC/AITheme")) + addEntryToSet("enemy-turn", AudioPath::fromResource(file)); + } + +} + +void CMusicHandler::loadTerrainMusicThemes() +{ + for (const auto & terrain : CGI->terrainTypeHandler->objects) + { + addEntryToSet("terrain_" + terrain->getJsonKey(), terrain->musicFilename); + } +} + +void CMusicHandler::addEntryToSet(const std::string & set, const AudioPath & musicURI) +{ + musicsSet[set].push_back(musicURI); +} + +void CMusicHandler::init() +{ + CAudioBase::init(); + + if (initialized) + { + Mix_HookMusicFinished([]() + { + CCS->musich->musicFinishedCallback(); + }); + } +} + +void CMusicHandler::release() +{ + if (initialized) + { + boost::mutex::scoped_lock guard(mutex); + + Mix_HookMusicFinished(nullptr); + current->stop(); + + current.reset(); + next.reset(); + } + + CAudioBase::release(); +} + +void CMusicHandler::playMusic(const AudioPath & musicURI, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + if (current && current->isPlaying() && current->isTrack(musicURI)) + return; + + queueNext(this, "", musicURI, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart) +{ + playMusicFromSet(musicSet + "_" + entryID, loop, fromStart); +} + +void CMusicHandler::playMusicFromSet(const std::string & whichSet, bool loop, bool fromStart) +{ + boost::mutex::scoped_lock guard(mutex); + + auto selectedSet = musicsSet.find(whichSet); + if (selectedSet == musicsSet.end()) + { + logGlobal->error("Error: playing music from non-existing set: %s", whichSet); + return; + } + + if (current && current->isPlaying() && current->isSet(whichSet)) + return; + + // in this mode - play random track from set + queueNext(this, whichSet, AudioPath(), loop, fromStart); +} + +void CMusicHandler::queueNext(std::unique_ptr queued) +{ + if (!initialized) + return; + + next = std::move(queued); + + if (current.get() == nullptr || !current->stop(1000)) + { + current.reset(next.release()); + current->play(); + } +} + +void CMusicHandler::queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart) +{ + queueNext(std::make_unique(owner, setName, musicURI, looped, fromStart)); +} + +void CMusicHandler::stopMusic(int fade_ms) +{ + if (!initialized) + return; + + boost::mutex::scoped_lock guard(mutex); + + if (current.get() != nullptr) + current->stop(fade_ms); + next.reset(); +} + +void CMusicHandler::setVolume(ui32 percent) +{ + CAudioBase::setVolume(percent); + + if (initialized) + Mix_VolumeMusic((MIX_MAX_VOLUME * volume)/100); +} + +void CMusicHandler::musicFinishedCallback() +{ + // call music restart in separate thread to avoid deadlock in some cases + // It is possible for: + // 1) SDL thread to call this method on end of playback + // 2) VCMI code to call queueNext() method to queue new file + // this leads to: + // 1) SDL thread waiting to acquire music lock in this method (while keeping internal SDL mutex locked) + // 2) VCMI thread waiting to acquire internal SDL mutex (while keeping music mutex locked) + + GH.dispatchMainThread([this]() + { + boost::unique_lock lockGuard(mutex); + if (current.get() != nullptr) + { + // if music is looped, play it again + if (current->play()) + return; + else + current.reset(); + } + + if (current.get() == nullptr && next.get() != nullptr) + { + current.reset(next.release()); + current->play(); + } + }); +} + +MusicEntry::MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart): + owner(owner), + music(nullptr), + playing(false), + startTime(uint32_t(-1)), + startPosition(0), + loop(looped ? -1 : 1), + fromStart(fromStart), + setName(std::move(setName)) +{ + if (!musicURI.empty()) + load(std::move(musicURI)); +} +MusicEntry::~MusicEntry() +{ + if (playing && loop > 0) + { + assert(0); + logGlobal->error("Attempt to delete music while playing!"); + Mix_HaltMusic(); + } + + if (loop == 0 && Mix_FadingMusic() != MIX_NO_FADING) + { + assert(0); + logGlobal->error("Attempt to delete music while fading out!"); + Mix_HaltMusic(); + } + + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + if (music) + Mix_FreeMusic(music); +} + +void MusicEntry::load(const AudioPath & musicURI) +{ + if (music) + { + logGlobal->trace("Del-ing music file %s", currentName.getOriginalName()); + Mix_FreeMusic(music); + music = nullptr; + } + + if (CResourceHandler::get()->existsResource(musicURI)) + currentName = musicURI; + else + currentName = musicURI.addPrefix("MUSIC/"); + + music = nullptr; + + logGlobal->trace("Loading music file %s", currentName.getOriginalName()); + + try + { + auto musicFile = MakeSDLRWops(CResourceHandler::get()->load(currentName)); + music = Mix_LoadMUS_RW(musicFile, SDL_TRUE); + } + catch(std::exception &e) + { + logGlobal->error("Failed to load music. setName=%s\tmusicURI=%s", setName, currentName.getOriginalName()); + logGlobal->error("Exception: %s", e.what()); + } + + if(!music) + { + logGlobal->warn("Warning: Cannot open %s: %s", currentName.getOriginalName(), Mix_GetError()); + return; + } +} + +bool MusicEntry::play() +{ + if (!(loop--) && music) //already played once - return + return false; + + if (!setName.empty()) + { + const auto & set = owner->musicsSet[setName]; + const auto & iter = RandomGeneratorUtil::nextItem(set, CRandomGenerator::getDefault()); + load(*iter); + } + + logGlobal->trace("Playing music file %s", currentName.getOriginalName()); + + if (!fromStart && owner->trackPositions.count(currentName) > 0 && owner->trackPositions[currentName] > 0) + { + float timeToStart = owner->trackPositions[currentName]; + startPosition = std::round(timeToStart * 1000); + + // erase stored position: + // if music track will be interrupted again - new position will be written in stop() method + // if music track is not interrupted and will finish by timeout/end of file - it will restart from begginning as it should + owner->trackPositions.erase(owner->trackPositions.find(currentName)); + + if (Mix_FadeInMusicPos(music, 1, 1000, timeToStart) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + else + { + startPosition = 0; + + if(Mix_PlayMusic(music, 1) == -1) + { + logGlobal->error("Unable to play music (%s)", Mix_GetError()); + return false; + } + } + + startTime = GH.input().getTicks(); + + playing = true; + return true; +} + +bool MusicEntry::stop(int fade_ms) +{ + if (Mix_PlayingMusic()) + { + playing = false; + loop = 0; + uint32_t endTime = GH.input().getTicks(); + assert(startTime != uint32_t(-1)); + float playDuration = (endTime - startTime + startPosition) / 1000.f; + owner->trackPositions[currentName] = playDuration; + logGlobal->trace("Stopping music file %s at %f", currentName.getOriginalName(), playDuration); + + Mix_FadeOutMusic(fade_ms); + return true; + } + return false; +} + +bool MusicEntry::isPlaying() +{ + return playing; +} + +bool MusicEntry::isSet(std::string set) +{ + return !setName.empty() && set == setName; +} + +bool MusicEntry::isTrack(const AudioPath & track) +{ + return setName.empty() && track == currentName; +} diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 0719d5d9b..44f39673c 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -1,167 +1,167 @@ -/* - * CMusicHandler.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/CConfigHandler.h" -#include "../lib/CSoundBase.h" - -struct _Mix_Music; -struct SDL_RWops; -using Mix_Music = struct _Mix_Music; -struct Mix_Chunk; - -class CAudioBase { -protected: - boost::mutex mutex; - bool initialized; - int volume; // from 0 (mute) to 100 - -public: - CAudioBase(): initialized(false), volume(0) {}; - virtual void init() = 0; - virtual void release() = 0; - - virtual void setVolume(ui32 percent); - ui32 getVolume() const { return volume; }; -}; - -class CSoundHandler: public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - using CachedChunk = std::pair>; - std::map soundChunks; - std::map, CachedChunk> soundChunksRaw; - - Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); - Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); - - /// have entry for every currently active channel - /// vector will be empty if callback was not set - std::map> > callbacks; - - /// Protects access to callbacks member to avoid data races: - /// SDL calls sound finished callbacks from audio thread - boost::mutex mutexCallbacks; - - int ambientDistToVolume(int distance) const; - void ambientStopSound(const AudioPath & soundId); - void updateChannelVolume(int channel); - - const JsonNode ambientConfig; - - std::map ambientChannels; - std::map channelVolumes; - - void initCallback(int channel, const std::function & function); - void initCallback(int channel); - -public: - CSoundHandler(); - - void init() override; - void release() override; - - void setVolume(ui32 percent) override; - void setChannelVolume(int channel, ui32 percent); - - // Sounds - int playSound(soundBase::soundID soundID, int repeats=0); - int playSound(const AudioPath & sound, int repeats=0, bool cache=false); - int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); - int playSoundFromSet(std::vector &sound_vec); - void stopSound(int handler); - - void setCallback(int channel, std::function function); - void soundFinishedCallback(int channel); - - int ambientGetRange() const; - void ambientUpdateChannels(std::map currentSounds); - void ambientStopAllChannels(); - - // Sets - std::vector battleIntroSounds; -}; - -class CMusicHandler; - -//Class for handling one music file -class MusicEntry -{ - CMusicHandler *owner; - Mix_Music *music; - - int loop; // -1 = indefinite - bool fromStart; - bool playing; - uint32_t startTime; - uint32_t startPosition; - //if not null - set from which music will be randomly selected - std::string setName; - AudioPath currentName; - - void load(const AudioPath & musicURI); - -public: - MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); - ~MusicEntry(); - - bool isSet(std::string setName); - bool isTrack(const AudioPath & trackName); - bool isPlaying(); - - bool play(); - bool stop(int fade_ms=0); -}; - -class CMusicHandler: public CAudioBase -{ -private: - //update volume on configuration change - SettingsListener listener; - void onVolumeChange(const JsonNode &volumeNode); - - std::unique_ptr current; - std::unique_ptr next; - - void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); - void queueNext(std::unique_ptr queued); - void musicFinishedCallback(); - - /// map -> - std::map> musicsSet; - /// stored position, in seconds at which music player should resume playing this track - std::map trackPositions; - -public: - CMusicHandler(); - - /// add entry with URI musicURI in set. Track will have ID musicID - void addEntryToSet(const std::string & set, const AudioPath & musicURI); - - void init() override; - void loadTerrainMusicThemes(); - void release() override; - void setVolume(ui32 percent) override; - - /// play track by URI, if loop = true music will be looped - void playMusic(const AudioPath & musicURI, bool loop, bool fromStart); - /// play random track from this set - void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart); - /// play random track from set (musicSet, entryID) - void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart); - /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any - void stopMusic(int fade_ms=1000); - - friend class MusicEntry; -}; +/* + * CMusicHandler.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/CConfigHandler.h" +#include "../lib/CSoundBase.h" + +struct _Mix_Music; +struct SDL_RWops; +using Mix_Music = struct _Mix_Music; +struct Mix_Chunk; + +class CAudioBase { +protected: + boost::mutex mutex; + bool initialized; + int volume; // from 0 (mute) to 100 + +public: + CAudioBase(): initialized(false), volume(0) {}; + virtual void init() = 0; + virtual void release() = 0; + + virtual void setVolume(ui32 percent); + ui32 getVolume() const { return volume; }; +}; + +class CSoundHandler: public CAudioBase +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode &volumeNode); + + using CachedChunk = std::pair>; + std::map soundChunks; + std::map, CachedChunk> soundChunksRaw; + + Mix_Chunk *GetSoundChunk(const AudioPath & sound, bool cache); + Mix_Chunk *GetSoundChunk(std::pair, si64> & data, bool cache); + + /// have entry for every currently active channel + /// vector will be empty if callback was not set + std::map> > callbacks; + + /// Protects access to callbacks member to avoid data races: + /// SDL calls sound finished callbacks from audio thread + boost::mutex mutexCallbacks; + + int ambientDistToVolume(int distance) const; + void ambientStopSound(const AudioPath & soundId); + void updateChannelVolume(int channel); + + const JsonNode ambientConfig; + + std::map ambientChannels; + std::map channelVolumes; + + void initCallback(int channel, const std::function & function); + void initCallback(int channel); + +public: + CSoundHandler(); + + void init() override; + void release() override; + + void setVolume(ui32 percent) override; + void setChannelVolume(int channel, ui32 percent); + + // Sounds + int playSound(soundBase::soundID soundID, int repeats=0); + int playSound(const AudioPath & sound, int repeats=0, bool cache=false); + int playSound(std::pair, si64> & data, int repeats=0, bool cache=false); + int playSoundFromSet(std::vector &sound_vec); + void stopSound(int handler); + + void setCallback(int channel, std::function function); + void soundFinishedCallback(int channel); + + int ambientGetRange() const; + void ambientUpdateChannels(std::map currentSounds); + void ambientStopAllChannels(); + + // Sets + std::vector battleIntroSounds; +}; + +class CMusicHandler; + +//Class for handling one music file +class MusicEntry +{ + CMusicHandler *owner; + Mix_Music *music; + + int loop; // -1 = indefinite + bool fromStart; + bool playing; + uint32_t startTime; + uint32_t startPosition; + //if not null - set from which music will be randomly selected + std::string setName; + AudioPath currentName; + + void load(const AudioPath & musicURI); + +public: + MusicEntry(CMusicHandler *owner, std::string setName, const AudioPath & musicURI, bool looped, bool fromStart); + ~MusicEntry(); + + bool isSet(std::string setName); + bool isTrack(const AudioPath & trackName); + bool isPlaying(); + + bool play(); + bool stop(int fade_ms=0); +}; + +class CMusicHandler: public CAudioBase +{ +private: + //update volume on configuration change + SettingsListener listener; + void onVolumeChange(const JsonNode &volumeNode); + + std::unique_ptr current; + std::unique_ptr next; + + void queueNext(CMusicHandler *owner, const std::string & setName, const AudioPath & musicURI, bool looped, bool fromStart); + void queueNext(std::unique_ptr queued); + void musicFinishedCallback(); + + /// map -> + std::map> musicsSet; + /// stored position, in seconds at which music player should resume playing this track + std::map trackPositions; + +public: + CMusicHandler(); + + /// add entry with URI musicURI in set. Track will have ID musicID + void addEntryToSet(const std::string & set, const AudioPath & musicURI); + + void init() override; + void loadTerrainMusicThemes(); + void release() override; + void setVolume(ui32 percent) override; + + /// play track by URI, if loop = true music will be looped + void playMusic(const AudioPath & musicURI, bool loop, bool fromStart); + /// play random track from this set + void playMusicFromSet(const std::string & musicSet, bool loop, bool fromStart); + /// play random track from set (musicSet, entryID) + void playMusicFromSet(const std::string & musicSet, const std::string & entryID, bool loop, bool fromStart); + /// stops currently playing music by fading out it over fade_ms and starts next scheduled track, if any + void stopMusic(int fade_ms=1000); + + friend class MusicEntry; +}; diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 31089089c..4be7b8c2a 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1,1891 +1,1891 @@ -/* - * CPlayerInterface.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 "CPlayerInterface.h" - -#include - -#include "CGameInfo.h" -#include "CMT.h" -#include "CMusicHandler.h" -#include "CServerHandler.h" -#include "HeroMovementController.h" -#include "PlayerLocalState.h" - -#include "adventureMap/AdventureMapInterface.h" -#include "adventureMap/CInGameConsole.h" -#include "adventureMap/CList.h" - -#include "battle/BattleEffectsController.h" -#include "battle/BattleFieldController.h" -#include "battle/BattleInterface.h" -#include "battle/BattleInterfaceClasses.h" -#include "battle/BattleWindow.h" - -#include "eventsSDL/InputHandler.h" -#include "eventsSDL/NotificationHandler.h" - -#include "gui/CGuiHandler.h" -#include "gui/CursorHandler.h" -#include "gui/WindowHandler.h" - -#include "mainmenu/CMainMenu.h" -#include "mainmenu/CHighScoreScreen.h" - -#include "mapView/mapHandler.h" - -#include "render/CAnimation.h" -#include "render/IImage.h" - -#include "widgets/Buttons.h" -#include "widgets/CComponent.h" -#include "widgets/CGarrisonInt.h" - -#include "windows/CCastleInterface.h" -#include "windows/CCreatureWindow.h" -#include "windows/CHeroWindow.h" -#include "windows/CKingdomInterface.h" -#include "windows/CPuzzleWindow.h" -#include "windows/CQuestLog.h" -#include "windows/CSpellWindow.h" -#include "windows/CTradeWindow.h" -#include "windows/GUIClasses.h" -#include "windows/InfoWindows.h" - -#include "../CCallback.h" - -#include "../lib/CArtHandler.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CPlayerState.h" -#include "../lib/CStack.h" -#include "../lib/CStopWatch.h" -#include "../lib/CThreadHelper.h" -#include "../lib/CTownHandler.h" -#include "../lib/CondSh.h" -#include "../lib/GameConstants.h" -#include "../lib/JsonNode.h" -#include "../lib/NetPacks.h" //todo: remove -#include "../lib/NetPacksBase.h" -#include "../lib/RoadHandler.h" -#include "../lib/StartInfo.h" -#include "../lib/TerrainHandler.h" -#include "../lib/TextOperations.h" -#include "../lib/UnlockGuard.h" -#include "../lib/VCMIDirs.h" - -#include "../lib/bonuses/CBonusSystemNode.h" -#include "../lib/bonuses/Limiters.h" -#include "../lib/bonuses/Propagators.h" -#include "../lib/bonuses/Updaters.h" - -#include "../lib/gameState/CGameState.h" - -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/mapObjects/MiscObjects.h" -#include "../lib/mapObjects/ObjectTemplate.h" - -#include "../lib/mapping/CMapHeader.h" - -#include "../lib/pathfinder/CGPathNode.h" - -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/serializer/BinarySerializer.h" -#include "../lib/serializer/CTypeList.h" - -#include "../lib/spells/CSpellHandler.h" - -// The macro below is used to mark functions that are called by client when game state changes. -// They all assume that interface mutex is locked. -#define EVENT_HANDLER_CALLED_BY_CLIENT - -#define BATTLE_EVENT_POSSIBLE_RETURN \ - if (LOCPLINT != this) \ - return; \ - if (isAutoFightOn && !battleInt) \ - return; - -CPlayerInterface * LOCPLINT; - -std::shared_ptr CPlayerInterface::battleInt; - -struct HeroObjectRetriever -{ - const CGHeroInstance * operator()(const ConstTransitivePtr &h) const - { - return h; - } - const CGHeroInstance * operator()(const ConstTransitivePtr &s) const - { - return nullptr; - } -}; - -CPlayerInterface::CPlayerInterface(PlayerColor Player): - localState(std::make_unique(*this)), - movementController(std::make_unique()) -{ - logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); - GH.defActionsDef = 0; - LOCPLINT = this; - playerID=Player; - human=true; - battleInt = nullptr; - castleInt = nullptr; - makingTurn = false; - showingDialog = new CondSh(false); - cingconsole = new CInGameConsole(); - firstCall = 1; //if loading will be overwritten in serialize - autosaveCount = 0; - isAutoFightOn = false; - ignoreEvents = false; - numOfMovedArts = 0; -} - -CPlayerInterface::~CPlayerInterface() -{ - logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString()); - delete showingDialog; - delete cingconsole; - if (LOCPLINT == this) - LOCPLINT = nullptr; -} -void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) -{ - cb = CB; - env = ENV; - - CCS->musich->loadTerrainMusicThemes(); - initializeHeroTownList(); - - adventureInt.reset(new AdventureMapInterface()); -} - -void CPlayerInterface::playerEndsTurn(PlayerColor player) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (player == playerID) - { - makingTurn = false; - - // remove all active dialogs that do not expect query answer - for (;;) - { - auto adventureWindow = GH.windows().topWindow(); - auto infoWindow = GH.windows().topWindow(); - - if(adventureWindow != nullptr) - break; - - if(infoWindow && infoWindow->ID != QueryID::NONE) - break; - - if (infoWindow) - infoWindow->close(); - else - GH.windows().popWindows(1); - } - - // remove all pending dialogs that do not expect query answer - vstd::erase_if(dialogs, [](const std::shared_ptr & window){ - return window->ID == QueryID::NONE; - }); - } -} - -void CPlayerInterface::playerStartsTurn(PlayerColor player) -{ - if(GH.windows().findWindows().empty()) - { - // after map load - remove all active windows and replace them with adventure map - GH.windows().clear(); - GH.windows().pushWindow(adventureInt); - } - - EVENT_HANDLER_CALLED_BY_CLIENT; - if (player != playerID && LOCPLINT == this) - { - waitWhileDialog(); - - bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman(); - - if (makingTurn == false) - adventureInt->onEnemyTurnStarted(player, isHuman); - } -} - -void CPlayerInterface::performAutosave() -{ - int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); - if(frequency > 0 && cb->getDate() % frequency == 0) - { - bool usePrefix = settings["general"]["useSavePrefix"].Bool(); - std::string prefix = std::string(); - - if(usePrefix) - { - prefix = settings["general"]["savePrefix"].String(); - if(prefix.empty()) - { - std::string name = cb->getMapHeader()->name.toString(); - int txtlen = TextOperations::getUnicodeCharactersCount(name); - - TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); - std::string forbiddenChars("\\/:?\"<>| "); - std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' ); - - prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; - } - } - - autosaveCount++; - - int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); - if(autosaveCountLimit > 0) - { - cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount)); - autosaveCount %= autosaveCountLimit; - } - else - { - std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH)) - + std::to_string(cb->getDate(Date::WEEK)) - + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); - - cb->save("Saves/Autosave/" + prefix + stringifiedDate); - } - } -} - -void CPlayerInterface::gamePause(bool pause) -{ - cb->gamePause(pause); -} - -void CPlayerInterface::yourTurn(QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - { - LOCPLINT = this; - GH.curInt = this; - - NotificationHandler::notify("Your turn"); - if(settings["general"]["startTurnAutosave"].Bool()) - { - performAutosave(); - } - - if (CSH->howManyPlayerInterfaces() > 1) //hot seat message - { - adventureInt->onHotseatWaitStarted(playerID); - - makingTurn = true; - std::string msg = CGI->generaltexth->allTexts[13]; - boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); - std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); - showInfoDialog(msg, cmp); - } - else - { - makingTurn = true; - adventureInt->onPlayerTurnStarted(playerID); - } - } - acceptTurn(queryID); -} - -void CPlayerInterface::acceptTurn(QueryID queryID) -{ - if (settings["session"]["autoSkip"].Bool()) - { - while(auto iw = GH.windows().topWindow()) - iw->close(); - } - - if(CSH->howManyPlayerInterfaces() > 1) - { - waitWhileDialog(); // wait for player to accept turn in hot-seat mode - - adventureInt->onPlayerTurnStarted(playerID); - } - - // warn player if he has no town - if (cb->howManyTowns() == 0) - { - auto playerColor = *cb->getPlayerID(); - - std::vector components; - components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0); - MetaString text; - - const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; - - if(optDaysWithoutCastle) - { - auto daysWithoutCastle = optDaysWithoutCastle.value(); - if (daysWithoutCastle < 6) - { - text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); - text.replaceNumber(7 - daysWithoutCastle); - } - else if (daysWithoutCastle == 6) - { - text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); - } - - showInfoDialogAndWait(components, text); - } - else - logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); - } - - cb->selectionMade(0, queryID); - movementController->onPlayerTurnStarted(); -} - -void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - if(LOCPLINT != this) - return; - - //FIXME: read once and store - if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) - return; - - const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero - - if (!hero) - return; - - movementController->onTryMoveHero(hero, details); -} - -void CPlayerInterface::heroKilled(const CGHeroInstance* hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID); - - // if hero is not in town garrison - if (vstd::contains(localState->getWanderingHeroes(), hero)) - localState->removeWanderingHero(hero); - - adventureInt->onHeroChanged(hero); - localState->erasePath(hero); -} - -void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if(start && visitedObj) - { - if(visitedObj->getVisitSound()) - CCS->soundh->playSound(visitedObj->getVisitSound().value()); - } -} - -void CPlayerInterface::heroCreated(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->addWanderingHero(hero); - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::openTownWindow(const CGTownInstance * town) -{ - if(castleInt) - castleInt->close(); - castleInt = nullptr; - - auto newCastleInt = std::make_shared(town); - - GH.windows().pushWindow(newCastleInt); -} - -void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (which == PrimarySkill::EXPERIENCE) - { - for (auto ctw : GH.windows().findWindows()) - ctw->setExpToLevel(); - } - else - adventureInt->onHeroChanged(hero); -} - -void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto cuw : GH.windows().findWindows()) - cuw->redraw(); -} - -void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onHeroChanged(hero); - if (makingTurn && hero->tempOwner == playerID) - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (makingTurn && hero->tempOwner == playerID) - adventureInt->onHeroChanged(hero); -} -void CPlayerInterface::receivedResource() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto mw : GH.windows().findWindows()) - mw->resourceChanged(); - - GH.windows().totalRedraw(); -} - -void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector& skills, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->soundh->playSound(soundBase::heroNewLevel); - GH.windows().createAndPushWindow(hero, pskill, skills, [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }); -} - -void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->soundh->playSound(soundBase::heroNewLevel); - GH.windows().createAndPushWindow(commander, skills, [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }); -} - -void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(town->garrisonHero) //wandering hero moved to the garrison - { - // This method also gets called on hero recruitment -> garrisoned hero is already in garrison - if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero)) - localState->removeWanderingHero(town->garrisonHero); - } - - if(town->visitingHero) //hero leaves garrison - { - // This method also gets called on hero recruitment -> wandering heroes already contains new hero - if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero)) - localState->addWanderingHero(town->visitingHero); - } - adventureInt->onHeroChanged(nullptr); - adventureInt->onTownChanged(town); - - for (auto gh : GH.windows().findWindows()) - gh->updateGarrisons(); - - for (auto ki : GH.windows().findWindows()) - ki->townChanged(town); - - // Perform totalRedraw to update hero list on adventure map, if any dialogs are open - GH.windows().totalRedraw(); -} - -void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (hero->tempOwner != playerID ) - return; - - waitWhileDialog(); - openTownWindow(town); -} - -void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) -{ - std::vector instances; - - if(auto obj = cb->getObj(id1)) - instances.push_back(obj); - - - if(id2 != ObjectInstanceID() && id2 != id1) - { - if(auto obj = cb->getObj(id2)) - instances.push_back(obj); - } - - garrisonsChanged(instances); -} - -void CPlayerInterface::garrisonsChanged(std::vector objs) -{ - for (auto object : objs) - { - auto * hero = dynamic_cast(object); - auto * town = dynamic_cast(object); - - if (hero) - { - adventureInt->onHeroChanged(hero); - - if(hero->inTownGarrison) - { - adventureInt->onTownChanged(hero->visitedTown); - } - } - if (town) - adventureInt->onTownChanged(town); - } - - for (auto cgh : GH.windows().findWindows()) - cgh->updateGarrisons(); - - for (auto cmw : GH.windows().findWindows()) - { - if (vstd::contains(objs, cmw->hero)) - cmw->garrisonChanged(); - } - - GH.windows().totalRedraw(); -} - -void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onTownChanged(town); - - if (castleInt) - { - castleInt->townlist->updateElement(town); - - if (castleInt->town == town) - { - switch(what) - { - case 1: - CCS->soundh->playSound(soundBase::newBuilding); - castleInt->addBuilding(buildingID); - break; - case 2: - castleInt->removeBuilding(buildingID); - break; - } - } - - // Perform totalRedraw in order to force redraw of updated town list icon from adventure map - GH.windows().totalRedraw(); - } - - for (auto cgh : GH.windows().findWindows()) - cgh->buildChanged(); -} - -void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) -{ - movementController->onBattleStarted(); - - //Don't wait for dialogs when we are non-active hot-seat player - if (LOCPLINT == this) - waitForAllDialogs(); -} - -void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); - bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); - - if ((replayAllowed && useQuickCombat) || forceQuickCombat) - { - autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); - - AutocombatPreferences autocombatPreferences = AutocombatPreferences(); - autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); - - autofightingAI->initBattleInterface(env, cb, autocombatPreferences); - autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false); - isAutoFightOn = true; - cb->registerBattleInterface(autofightingAI); - } - - //Don't wait for dialogs when we are non-active hot-seat player - if (LOCPLINT == this) - waitForAllDialogs(); - - BATTLE_EVENT_POSSIBLE_RETURN; -} - -void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector & units) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - for(auto & info : units) - { - switch(info.operation) - { - case UnitChanges::EOperation::RESET_STATE: - { - const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id ); - - if(!stack) - { - logGlobal->error("Invalid unit ID %d", info.id); - continue; - } - battleInt->stackReset(stack); - } - break; - case UnitChanges::EOperation::REMOVE: - battleInt->stackRemoved(info.id); - break; - case UnitChanges::EOperation::ADD: - { - const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id); - if(!unit) - { - logGlobal->error("Invalid unit ID %d", info.id); - continue; - } - battleInt->stackAdded(unit); - } - break; - default: - logGlobal->error("Unknown unit operation %d", (int)info.operation); - break; - } - } -} - -void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - std::vector> newObstacles; - std::vector removedObstacles; - - for(auto & change : obstacles) - { - if(change.operation == BattleChanges::EOperation::ADD) - { - auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id); - if(instance) - newObstacles.push_back(instance); - else - logNetwork->error("Invalid obstacle instance %d", change.id); - } - if(change.operation == BattleChanges::EOperation::REMOVE) - removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct - } - - if (!newObstacles.empty()) - battleInt->obstaclePlaced(newObstacles); - - if (!removedObstacles.empty()) - battleInt->obstacleRemoved(removedObstacles); - - battleInt->fieldController->redrawBackgroundWithHexes(); -} - -void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->stackIsCatapulting(ca); -} - -void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->newRound(); -} - -void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->startAction(action); -} - -void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->endAction(action); -} - -void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - logGlobal->trace("Awaiting command for %s", stack->nodeName()); - - assert(!cb->getBattle(battleID)->battleIsFinished()); - if (cb->getBattle(battleID)->battleIsFinished()) - { - logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); - - cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); - return ; - } - - if (autofightingAI) - { - if (isAutoFightOn) - { - //FIXME: we want client rendering to proceed while AI is making actions - // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - autofightingAI->activeStack(battleID, stack); - return; - } - - cb->unregisterBattleInterface(autofightingAI); - autofightingAI.reset(); - } - - assert(battleInt); - if(!battleInt) - { - // probably battle is finished already - cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); - } - - battleInt->stackActivated(stack); -} - -void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if(isAutoFightOn || autofightingAI) - { - isAutoFightOn = false; - cb->unregisterBattleInterface(autofightingAI); - autofightingAI.reset(); - - if(!battleInt) - { - bool allowManualReplay = queryID != QueryID::NONE; - - auto wnd = std::make_shared(*br, *this, allowManualReplay); - - if (allowManualReplay) - { - wnd->resultCallback = [=](ui32 selection) - { - cb->selectionMade(selection, queryID); - }; - } - GH.windows().pushWindow(wnd); - // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. - // Otherwise NewTurn causes freeze. - waitWhileDialog(); - return; - } - } - - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->battleFinished(*br, queryID); -} - -void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector & lines) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->displayBattleLog(lines); -} - -void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->stackMoved(stack, dest, distance, teleport); -} -void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->spellCast(sc); -} -void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->battleStacksEffectsSet(sse); -} -void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->effectsController->battleTriggerEffect(bte); - - if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN)) - { - const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo)); - battleInt->windowObject->heroManaPointsChanged(manaDrainedHero); - } -} -void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - std::vector arg; - for(auto & elem : bsa) - { - const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false); - const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false); - - assert(defender); - - StackAttackedInfo info; - info.defender = defender; - info.attacker = attacker; - info.damageDealt = elem.damageAmount; - info.amountKilled = elem.killedAmount; - info.spellEffect = SpellID::NONE; - info.indirectAttack = ranged; - info.killed = elem.killed(); - info.rebirth = elem.willRebirth(); - info.cloneKilled = elem.cloneKilled(); - info.fireShield = elem.fireShield(); - - if (elem.isSpell()) - info.spellEffect = elem.spellID; - - arg.push_back(info); - } - battleInt->stacksAreAttacked(arg); -} -void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - StackAttackInfo info; - info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking); - info.defender = nullptr; - info.indirectAttack = ba->shot(); - info.lucky = ba->lucky(); - info.unlucky = ba->unlucky(); - info.deathBlow = ba->deathBlow(); - info.lifeDrain = ba->lifeDrain(); - info.tile = ba->tile; - info.spellEffect = SpellID::NONE; - - if (ba->spellLike()) - info.spellEffect = ba->spellID; - - for(auto & elem : ba->bsa) - { - if(!elem.isSecondary()) - { - assert(info.defender == nullptr); - info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked); - } - else - { - info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked)); - } - } - assert(info.defender != nullptr); - assert(info.attacker != nullptr); - - battleInt->stackAttacking(info); -} - -void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->gateStateChanged(state); -} - -void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; -} - -void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector & components, int soundID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO; - auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod - - if(autoTryHover || type == EInfoWindowMode::INFO) - { - waitWhileDialog(); //Fix for mantis #98 - adventureInt->showInfoBoxMessage(components, text, timer); - - // abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on - movementController->requestMovementAbort(); - - if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) - CCS->soundh->playSound(static_cast(soundID)); - return; - } - - if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - return; - } - std::vector vect = components; //I do not know currently how to avoid copy here - do - { - std::vector sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))}; - std::vector> intComps; - for (auto & component : sender) - intComps.push_back(std::make_shared(component)); - showInfoDialog(text,intComps,soundID); - vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))); - } - while(!vect.empty()); -} - -void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr component) -{ - std::vector> intComps; - intComps.push_back(component); - - showInfoDialog(text, intComps, soundBase::sound_todo); -} - -void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector> & components, int soundID) -{ - LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); - waitWhileDialog(); - - if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - return; - } - std::shared_ptr temp = CInfoWindow::create(text, playerID, components); - - if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) - { - CCS->soundh->playSound(static_cast(soundID)); - showingDialog->set(true); - movementController->requestMovementAbort(); // interrupt movement to show dialog - GH.windows().pushWindow(temp); - } - else - { - dialogs.push_back(temp); - } -} - -void CPlayerInterface::showInfoDialogAndWait(std::vector & components, const MetaString & text) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - std::string str = text.toString(); - - showInfoDialog(EInfoWindowMode::MODAL, str, components, 0); - waitWhileDialog(); -} - -void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) -{ - movementController->requestMovementAbort(); - LOCPLINT->showingDialog->setn(true); - CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); -} - -void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - - movementController->requestMovementAbort(); - CCS->soundh->playSound(static_cast(soundID)); - - if (!selection && cancel) //simple yes/no dialog - { - std::vector> intComps; - for (auto & component : components) - intComps.push_back(std::make_shared(component)); //will be deleted by close in window - - showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps); - } - else if (selection) - { - std::vector> intComps; - for (auto & component : components) - intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close - - std::vector > > pom; - pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0}); - if (cancel) - { - pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0}); - } - - int charperline = 35; - if (pom.size() > 1) - charperline = 50; - GH.windows().createAndPushWindow(text, playerID, charperline, intComps, pom, askID); - intComps[0]->clickPressed(GH.getCursorPosition()); - intComps[0]->clickReleased(GH.getCursorPosition()); - } -} - -void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - movementController->showTeleportDialog(hero, channel, exits, impassable, askID); -} - -void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - auto selectCallback = [=](int selection) - { - cb->sendQueryReply(selection, askID); - }; - - auto cancelCallback = [=]() - { - cb->sendQueryReply(std::nullopt, askID); - }; - - const std::string localTitle = title.toString(); - const std::string localDescription = description.toString(); - - std::vector tempList; - tempList.reserve(objects.size()); - - for(auto item : objects) - tempList.push_back(item.getNum()); - - CComponent localIconC(icon); - - std::shared_ptr localIcon = localIconC.image; - localIconC.removeChild(localIcon.get(), false); - - std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); - wnd->onExit = cancelCallback; - GH.windows().pushWindow(wnd); -} - -void CPlayerInterface::tileRevealed(const std::unordered_set &pos) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - //FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas - adventureInt->onMapTilesChanged(pos); -} - -void CPlayerInterface::tileHidden(const std::unordered_set &pos) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onMapTilesChanged(pos); -} - -void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) -{ - GH.windows().createAndPushWindow(hero); -} - -void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (const CGTownInstance * townObj = dynamic_cast(town)) - { - for (auto fortScreen : GH.windows().findWindows()) - fortScreen->creaturesChangedEventHandler(); - - for (auto castleInterface : GH.windows().findWindows()) - if(castleInterface->town == town) - castleInterface->creaturesChangedEventHandler(); - - if (townObj) - for (auto ki : GH.windows().findWindows()) - ki->townChanged(townObj); - } - else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1 - || town->ID == Obj::CREATURE_GENERATOR4 || town->ID == Obj::WAR_MACHINE_FACTORY)) - { - for (auto crw : GH.windows().findWindows()) - if (crw->dwelling == town) - crw->availableCreaturesChanged(); - } -} - -void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if (bonus.type == BonusType::NONE) - return; - - adventureInt->onHeroChanged(hero); - if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain) - { - //recalculate paths because hero has lost bonus influencing pathfinding - localState->erasePath(hero); - } -} - -void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); -} - -void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - localState->serialize(h, version); - firstCall = -1; -} - -void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) -{ - assert(LOCPLINT->makingTurn); - assert(h); - assert(!showingDialog->get()); - assert(dialogs.empty()); - - LOG_TRACE(logGlobal); - if (!LOCPLINT->makingTurn) - return; - if (!h) - return; //can't find hero - - //It shouldn't be possible to move hero with open dialog (or dialog waiting in bg) - if (showingDialog->get() || !dialogs.empty()) - return; - - if (localState->isHeroSleeping(h)) - localState->setHeroAwaken(h); - - movementController->requestMovementStart(h, path); -} - -void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto onEnd = [=](){ cb->selectionMade(0, queryID); }; - - if (movementController->isHeroMovingThroughGarrison(down, up)) - { - onEnd(); - return; - } - - waitForAllDialogs(); - - auto cgw = std::make_shared(up, down, removableUnits); - cgw->quit->addCallback(onEnd); - GH.windows().pushWindow(cgw); -} - -/** - * Shows the dialog that appears when right-clicking an artifact that can be assembled - * into a combinational one on an artifact screen. Does not require the combination of - * artifacts to be legal. - */ -void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes) -{ - std::string text = artifact->getDescriptionTranslated(); - text += "\n\n"; - std::vector> scs; - - if(assembledArtifact) - { - // You possess all of the components to... - text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated()); - - // Picture of assembled artifact at bottom. - auto sc = std::make_shared(CComponent::artifact, assembledArtifact->getIndex(), 0); - scs.push_back(sc); - } - else - { - // Do you wish to disassemble this artifact? - text += CGI->generaltexth->allTexts[733]; - } - - showYesNoDialog(text, onYes, nullptr, scs); -} - -void CPlayerInterface::requestRealized( PackageApplied *pa ) -{ - if(pa->packType == typeList.getTypeID()) - movementController->onMoveHeroApplied(); - - if(pa->packType == typeList.getTypeID()) - movementController->onQueryReplyApplied(); -} - -void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) -{ - heroExchangeStarted(hero1, hero2, QueryID(-1)); -} - -void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(hero1, hero2, query); -} - -void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop) -{ - if (sop->what == ObjProperty::OWNER) - { - const CGObjectInstance * obj = cb->getObj(sop->id); - - if(obj->ID == Obj::TOWN) - { - auto town = static_cast(obj); - - if(obj->tempOwner == playerID) - { - localState->removeOwnedTown(town); - adventureInt->onTownChanged(town); - } - } - } -} - -void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if (sop->what == ObjProperty::OWNER) - { - const CGObjectInstance * obj = cb->getObj(sop->id); - - if(obj->ID == Obj::TOWN) - { - auto town = static_cast(obj); - - if(obj->tempOwner == playerID) - { - localState->addOwnedTown(town); - adventureInt->onTownChanged(town); - } - } - - //redraw minimap if owner changed - std::set pos = obj->getBlockedPos(); - std::unordered_set upos(pos.begin(), pos.end()); - adventureInt->onMapTilesChanged(upos); - - assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size()); - } -} - -void CPlayerInterface::initializeHeroTownList() -{ - if(localState->getWanderingHeroes().empty()) - { - for(auto & hero : cb->getHeroesInfo()) - { - if(!hero->inTownGarrison) - localState->addWanderingHero(hero); - } - } - - if(localState->getOwnedTowns().empty()) - { - for(auto & town : cb->getTownsInfo()) - localState->addOwnedTown(town); - } - - if(adventureInt) - adventureInt->onHeroChanged(nullptr); -} - -void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - auto recruitCb = [=](CreatureID id, int count) - { - cb->recruitCreatures(dwelling, dst, id, count, -1); - }; - auto closeCb = [=]() - { - cb->selectionMade(0, queryID); - }; - GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb, closeCb); -} - -void CPlayerInterface::waitWhileDialog() -{ - if (GH.amIGuiThread()) - { - logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!"); - return; - } - - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - boost::unique_lock un(showingDialog->mx); - while(showingDialog->data) - showingDialog->cond.wait(un); -} - -void CPlayerInterface::showShipyardDialog(const IShipyard *obj) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto state = obj->shipyardStatus(); - TResources cost; - obj->getBoatCost(cost); - GH.windows().createAndPushWindow(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); }); -} - -void CPlayerInterface::newObject( const CGObjectInstance * obj ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - //we might have built a boat in shipyard in opened town screen - if (obj->ID == Obj::BOAT - && LOCPLINT->castleInt - && obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation()) - { - CCS->soundh->playSound(soundBase::newBuilding); - LOCPLINT->castleInt->addBuilding(BuildingID::SHIP); - } -} - -void CPlayerInterface::centerView (int3 pos, int focusTime) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - CCS->curh->hide(); - adventureInt->centerOnTile(pos); - if (focusTime) - { - GH.windows().totalRedraw(); - { - IgnoreEvents ignore(*this); - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); - } - } - CCS->curh->show(); -} - -void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - if(playerID == initiator && obj->getRemovalSound()) - { - waitWhileDialog(); - CCS->soundh->playSound(obj->getRemovalSound().value()); - } - CGI->mh->waitForOngoingAnimations(); - - if(obj->ID == Obj::HERO && obj->tempOwner == playerID) - { - const CGHeroInstance * h = static_cast(obj); - heroKilled(h); - } - GH.fakeMouseMove(); -} - -void CPlayerInterface::objectRemovedAfter() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->onMapTilesChanged(boost::none); - - // visiting or garrisoned hero removed - update window - if (castleInt) - castleInt->updateGarrisons(); - - for (auto ki : GH.windows().findWindows()) - ki->heroRemoved(); -} - -void CPlayerInterface::playerBlocked(int reason, bool start) -{ - if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE) - { - if(CSH->howManyPlayerInterfaces() > 1 && LOCPLINT != this && LOCPLINT->makingTurn == false) - { - //one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode) - LOCPLINT = this; - GH.curInt = this; - adventureInt->onCurrentPlayerChanged(playerID); - std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); - boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); - std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); - makingTurn = true; //workaround for stiff showInfoDialog implementation - showInfoDialog(msg, cmp); - makingTurn = false; - } - } -} - -void CPlayerInterface::update() -{ - // Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request - boost::shared_lock gsLock(CGameState::mutex); - - // While mutexes were locked away we may be have stopped being the active interface - if (LOCPLINT != this) - return; - - //if there are any waiting dialogs, show them - if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) - { - showingDialog->set(true); - GH.windows().pushWindow(dialogs.front()); - dialogs.pop_front(); - } - - assert(adventureInt); - - // Handles mouse and key input - GH.handleEvents(); - GH.windows().simpleRedraw(); -} - -int CPlayerInterface::getLastIndex( std::string namePrefix) -{ - using namespace boost::filesystem; - using namespace boost::algorithm; - - path gamesDir = VCMIDirs::get().userSavePath(); - std::map dates; //save number => datestamp - - const directory_iterator enddir; - if (!exists(gamesDir)) - create_directory(gamesDir); - else - for (directory_iterator dir(gamesDir); dir != enddir; ++dir) - { - if (is_regular_file(dir->status())) - { - std::string name = dir->path().filename().string(); - if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1")) - { - char nr = name[namePrefix.size()]; - if (std::isdigit(nr)) - dates[last_write_time(dir->path())] = boost::lexical_cast(nr); - } - } - } - - if (!dates.empty()) - return (--dates.end())->second; //return latest file number - return 0; -} - -void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if (player == playerID) - { - if (victoryLossCheckResult.loss()) - showInfoDialog(CGI->generaltexth->allTexts[95]); - - assert(GH.curInt == LOCPLINT); - auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily - - LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday - GH.curInt = this; //waiting for dialogs requires this to get events - - if(!makingTurn) - { - makingTurn = true; //also needed for dialog to show with current implementation - waitForAllDialogs(); - makingTurn = false; - } - else - waitForAllDialogs(); - - GH.curInt = previousInterface; - LOCPLINT = previousInterface; - - if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated - { - if(adventureInt) - { - GH.windows().popWindows(GH.windows().count()); - adventureInt.reset(); - } - } - - if (victoryLossCheckResult.victory() && LOCPLINT == this) - { - // end game if current human player has won - CSH->sendClientDisconnecting(); - requestReturningToMainMenu(true); - } - else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) - { - //all human players eliminated - CSH->sendClientDisconnecting(); - requestReturningToMainMenu(false); - } - - if (GH.curInt == this) - GH.curInt = nullptr; - } -} - -void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; -} - -void CPlayerInterface::showPuzzleMap() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - waitWhileDialog(); - - //TODO: interface should not know the real position of Grail... - double ratio = 0; - int3 grailPos = cb->getGrailPos(&ratio); - - GH.windows().createAndPushWindow(grailPos, ratio); -} - -void CPlayerInterface::viewWorldMap() -{ - adventureInt->openWorldView(); -} - -void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - - if(GH.windows().topWindow()) - GH.windows().popWindows(1); - - if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) - localState->erasePath(caster); - - auto castSoundPath = spellID.toSpell()->getCastSound(); - if(!castSoundPath.empty()) - CCS->soundh->playSound(castSoundPath); -} - -void CPlayerInterface::tryDigging(const CGHeroInstance * h) -{ - int msgToShow = -1; - - const auto diggingStatus = h->diggingStatus(); - - switch(diggingStatus) - { - case EDiggingStatus::CAN_DIG: - break; - case EDiggingStatus::LACK_OF_MOVEMENT: - msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." - break; - case EDiggingStatus::TILE_OCCUPIED: - msgToShow = 97; //Try searching on clear ground. - break; - case EDiggingStatus::WRONG_TERRAIN: - msgToShow = 60; ////Try looking on land! - break; - case EDiggingStatus::BACKPACK_IS_FULL: - msgToShow = 247; //Searching for the Grail is fruitless... - break; - default: - assert(0); - } - - if(msgToShow < 0) - cb->dig(h); - else - showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); -} - -void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - BATTLE_EVENT_POSSIBLE_RETURN; - - battleInt->newRoundFirst(); -} - -void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto onWindowClosed = [this, queryID](){ - cb->selectionMade(0, queryID); - }; - - if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) - GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP); - else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) - GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP); - else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) - GH.windows().createAndPushWindow(market, visitor, onWindowClosed); - else if(!market->availableModes().empty()) - GH.windows().createAndPushWindow(market, visitor, onWindowClosed, market->availableModes().front()); -} - -void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto onWindowClosed = [this, queryID](){ - cb->selectionMade(0, queryID); - }; - GH.windows().createAndPushWindow(visitor, market, onWindowClosed); -} - -void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(visitor, object); -} - -void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - for (auto cmw : GH.windows().findWindows()) - cmw->artifactsChanged(false); -} - -void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto onWindowClosed = [this, queryID](){ - cb->selectionMade(0, queryID); - }; - GH.windows().createAndPushWindow(object, onWindowClosed); -} - -void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(obj); -} - -void CPlayerInterface::showQuestLog() -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - GH.windows().createAndPushWindow(LOCPLINT->cb->getMyQuests()); -} - -void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) -{ - if (obj->shipyardStatus() != IBoatGenerator::GOOD) - { - MetaString txt; - obj->getProblemText(txt); - showInfoDialog(txt.toString()); - } - else - showShipyardDialog(obj); -} - -void CPlayerInterface::requestReturningToMainMenu(bool won) -{ - HighScoreParameter param; - param.difficulty = cb->getStartInfo()->difficulty; - param.day = cb->getDate(); - param.townAmount = cb->howManyTowns(); - param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated; - param.hasGrail = false; - for(const CGHeroInstance * h : cb->getHeroesInfo()) - if(h->hasArt(ArtifactID::GRAIL)) - param.hasGrail = true; - for(const CGTownInstance * t : cb->getTownsInfo()) - if(t->builtBuildings.count(BuildingID::GRAIL)) - param.hasGrail = true; - param.allDefeated = true; - for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) - { - auto ps = cb->getPlayerState(player, false); - if(ps && player != *cb->getPlayerID()) - if(!ps->checkVanquished()) - param.allDefeated = false; - } - param.scenarioName = cb->getMapHeader()->name.toString(); - param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; - HighScoreCalculation highScoreCalc; - highScoreCalc.parameters.push_back(param); - highScoreCalc.isCampaign = false; - - if(won && cb->getStartInfo()->campState) - CSH->startCampaignScenario(param, cb->getStartInfo()->campState); - else - { - GH.dispatchMainThread( - [won, highScoreCalc]() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("main"); - GH.windows().createAndPushWindow(won, highScoreCalc); - } - ); - } -} - -void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) -{ - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - if(hero) - { - auto art = hero->getArt(al.slot); - if(art == nullptr) - { - logGlobal->error("artifact location %d points to nothing", - al.slot.num); - return; - } - ArtifactUtilsClient::askToAssemble(hero, al.slot); - } -} - -void CPlayerInterface::artifactPut(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); -} - -void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactRemoved(al); - - waitWhileDialog(); -} - -void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), dst.artHolder); - adventureInt->onHeroChanged(hero); - - bool redraw = true; - // If a bulk transfer has arrived, then redrawing only the last art movement. - if(numOfMovedArts != 0) - { - numOfMovedArts--; - if(numOfMovedArts != 0) - redraw = false; - } - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactMoved(src, dst, redraw); - - waitWhileDialog(); -} - -void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) -{ - numOfMovedArts = numOfArts; -} - -void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactAssembled(al); -} - -void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); - - for(auto artWin : GH.windows().findWindows()) - artWin->artifactDisassembled(al); -} - -void CPlayerInterface::waitForAllDialogs() -{ - while(!dialogs.empty()) - { - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); - } - waitWhileDialog(); -} - -void CPlayerInterface::proposeLoadingGame() -{ - showYesNoDialog( - CGI->generaltexth->allTexts[68], - []() - { - GH.dispatchMainThread( - []() - { - CSH->endGameplay(); - GH.defActionsDef = 63; - CMM->menu->switchToTab("load"); - } - ); - }, - nullptr - ); -} - -bool CPlayerInterface::capturedAllEvents() -{ - if(movementController->isHeroMoving()) - { - //just inform that we are capturing events. they will be processed by heroMoved() in client thread. - return true; - } - - bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); - bool quickCombatOngoing = isAutoFightOn && !battleInt; - - if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing ) - { - GH.input().ignoreEventsUntilInput(); - return true; - } - - return false; -} - -void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions, bool showTerrain) -{ - EVENT_HANDLER_CALLED_BY_CLIENT; - adventureInt->openWorldView(objectPositions, showTerrain ); -} - -std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) -{ - return std::nullopt; -} +/* + * CPlayerInterface.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 "CPlayerInterface.h" + +#include + +#include "CGameInfo.h" +#include "CMT.h" +#include "CMusicHandler.h" +#include "CServerHandler.h" +#include "HeroMovementController.h" +#include "PlayerLocalState.h" + +#include "adventureMap/AdventureMapInterface.h" +#include "adventureMap/CInGameConsole.h" +#include "adventureMap/CList.h" + +#include "battle/BattleEffectsController.h" +#include "battle/BattleFieldController.h" +#include "battle/BattleInterface.h" +#include "battle/BattleInterfaceClasses.h" +#include "battle/BattleWindow.h" + +#include "eventsSDL/InputHandler.h" +#include "eventsSDL/NotificationHandler.h" + +#include "gui/CGuiHandler.h" +#include "gui/CursorHandler.h" +#include "gui/WindowHandler.h" + +#include "mainmenu/CMainMenu.h" +#include "mainmenu/CHighScoreScreen.h" + +#include "mapView/mapHandler.h" + +#include "render/CAnimation.h" +#include "render/IImage.h" + +#include "widgets/Buttons.h" +#include "widgets/CComponent.h" +#include "widgets/CGarrisonInt.h" + +#include "windows/CCastleInterface.h" +#include "windows/CCreatureWindow.h" +#include "windows/CHeroWindow.h" +#include "windows/CKingdomInterface.h" +#include "windows/CPuzzleWindow.h" +#include "windows/CQuestLog.h" +#include "windows/CSpellWindow.h" +#include "windows/CTradeWindow.h" +#include "windows/GUIClasses.h" +#include "windows/InfoWindows.h" + +#include "../CCallback.h" + +#include "../lib/CArtHandler.h" +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" +#include "../lib/CStack.h" +#include "../lib/CStopWatch.h" +#include "../lib/CThreadHelper.h" +#include "../lib/CTownHandler.h" +#include "../lib/CondSh.h" +#include "../lib/GameConstants.h" +#include "../lib/JsonNode.h" +#include "../lib/NetPacks.h" //todo: remove +#include "../lib/NetPacksBase.h" +#include "../lib/RoadHandler.h" +#include "../lib/StartInfo.h" +#include "../lib/TerrainHandler.h" +#include "../lib/TextOperations.h" +#include "../lib/UnlockGuard.h" +#include "../lib/VCMIDirs.h" + +#include "../lib/bonuses/CBonusSystemNode.h" +#include "../lib/bonuses/Limiters.h" +#include "../lib/bonuses/Propagators.h" +#include "../lib/bonuses/Updaters.h" + +#include "../lib/gameState/CGameState.h" + +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/MiscObjects.h" +#include "../lib/mapObjects/ObjectTemplate.h" + +#include "../lib/mapping/CMapHeader.h" + +#include "../lib/pathfinder/CGPathNode.h" + +#include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/serializer/CTypeList.h" + +#include "../lib/spells/CSpellHandler.h" + +// The macro below is used to mark functions that are called by client when game state changes. +// They all assume that interface mutex is locked. +#define EVENT_HANDLER_CALLED_BY_CLIENT + +#define BATTLE_EVENT_POSSIBLE_RETURN \ + if (LOCPLINT != this) \ + return; \ + if (isAutoFightOn && !battleInt) \ + return; + +CPlayerInterface * LOCPLINT; + +std::shared_ptr CPlayerInterface::battleInt; + +struct HeroObjectRetriever +{ + const CGHeroInstance * operator()(const ConstTransitivePtr &h) const + { + return h; + } + const CGHeroInstance * operator()(const ConstTransitivePtr &s) const + { + return nullptr; + } +}; + +CPlayerInterface::CPlayerInterface(PlayerColor Player): + localState(std::make_unique(*this)), + movementController(std::make_unique()) +{ + logGlobal->trace("\tHuman player interface for player %s being constructed", Player.toString()); + GH.defActionsDef = 0; + LOCPLINT = this; + playerID=Player; + human=true; + battleInt = nullptr; + castleInt = nullptr; + makingTurn = false; + showingDialog = new CondSh(false); + cingconsole = new CInGameConsole(); + firstCall = 1; //if loading will be overwritten in serialize + autosaveCount = 0; + isAutoFightOn = false; + ignoreEvents = false; + numOfMovedArts = 0; +} + +CPlayerInterface::~CPlayerInterface() +{ + logGlobal->trace("\tHuman player interface for player %s being destructed", playerID.toString()); + delete showingDialog; + delete cingconsole; + if (LOCPLINT == this) + LOCPLINT = nullptr; +} +void CPlayerInterface::initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) +{ + cb = CB; + env = ENV; + + CCS->musich->loadTerrainMusicThemes(); + initializeHeroTownList(); + + adventureInt.reset(new AdventureMapInterface()); +} + +void CPlayerInterface::playerEndsTurn(PlayerColor player) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (player == playerID) + { + makingTurn = false; + + // remove all active dialogs that do not expect query answer + for (;;) + { + auto adventureWindow = GH.windows().topWindow(); + auto infoWindow = GH.windows().topWindow(); + + if(adventureWindow != nullptr) + break; + + if(infoWindow && infoWindow->ID != QueryID::NONE) + break; + + if (infoWindow) + infoWindow->close(); + else + GH.windows().popWindows(1); + } + + // remove all pending dialogs that do not expect query answer + vstd::erase_if(dialogs, [](const std::shared_ptr & window){ + return window->ID == QueryID::NONE; + }); + } +} + +void CPlayerInterface::playerStartsTurn(PlayerColor player) +{ + if(GH.windows().findWindows().empty()) + { + // after map load - remove all active windows and replace them with adventure map + GH.windows().clear(); + GH.windows().pushWindow(adventureInt); + } + + EVENT_HANDLER_CALLED_BY_CLIENT; + if (player != playerID && LOCPLINT == this) + { + waitWhileDialog(); + + bool isHuman = cb->getStartInfo()->playerInfos.count(player) && cb->getStartInfo()->playerInfos.at(player).isControlledByHuman(); + + if (makingTurn == false) + adventureInt->onEnemyTurnStarted(player, isHuman); + } +} + +void CPlayerInterface::performAutosave() +{ + int frequency = static_cast(settings["general"]["saveFrequency"].Integer()); + if(frequency > 0 && cb->getDate() % frequency == 0) + { + bool usePrefix = settings["general"]["useSavePrefix"].Bool(); + std::string prefix = std::string(); + + if(usePrefix) + { + prefix = settings["general"]["savePrefix"].String(); + if(prefix.empty()) + { + std::string name = cb->getMapHeader()->name.toString(); + int txtlen = TextOperations::getUnicodeCharactersCount(name); + + TextOperations::trimRightUnicode(name, std::max(0, txtlen - 15)); + std::string forbiddenChars("\\/:?\"<>| "); + std::replace_if(name.begin(), name.end(), [&](char c) { return std::string::npos != forbiddenChars.find(c); }, '_' ); + + prefix = name + "_" + cb->getStartInfo()->startTimeIso8601 + "/"; + } + } + + autosaveCount++; + + int autosaveCountLimit = settings["general"]["autosaveCountLimit"].Integer(); + if(autosaveCountLimit > 0) + { + cb->save("Saves/Autosave/" + prefix + std::to_string(autosaveCount)); + autosaveCount %= autosaveCountLimit; + } + else + { + std::string stringifiedDate = std::to_string(cb->getDate(Date::MONTH)) + + std::to_string(cb->getDate(Date::WEEK)) + + std::to_string(cb->getDate(Date::DAY_OF_WEEK)); + + cb->save("Saves/Autosave/" + prefix + stringifiedDate); + } + } +} + +void CPlayerInterface::gamePause(bool pause) +{ + cb->gamePause(pause); +} + +void CPlayerInterface::yourTurn(QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + { + LOCPLINT = this; + GH.curInt = this; + + NotificationHandler::notify("Your turn"); + if(settings["general"]["startTurnAutosave"].Bool()) + { + performAutosave(); + } + + if (CSH->howManyPlayerInterfaces() > 1) //hot seat message + { + adventureInt->onHotseatWaitStarted(playerID); + + makingTurn = true; + std::string msg = CGI->generaltexth->allTexts[13]; + boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); + std::vector> cmp; + cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + showInfoDialog(msg, cmp); + } + else + { + makingTurn = true; + adventureInt->onPlayerTurnStarted(playerID); + } + } + acceptTurn(queryID); +} + +void CPlayerInterface::acceptTurn(QueryID queryID) +{ + if (settings["session"]["autoSkip"].Bool()) + { + while(auto iw = GH.windows().topWindow()) + iw->close(); + } + + if(CSH->howManyPlayerInterfaces() > 1) + { + waitWhileDialog(); // wait for player to accept turn in hot-seat mode + + adventureInt->onPlayerTurnStarted(playerID); + } + + // warn player if he has no town + if (cb->howManyTowns() == 0) + { + auto playerColor = *cb->getPlayerID(); + + std::vector components; + components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0); + MetaString text; + + const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; + + if(optDaysWithoutCastle) + { + auto daysWithoutCastle = optDaysWithoutCastle.value(); + if (daysWithoutCastle < 6) + { + text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. + text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + text.replaceNumber(7 - daysWithoutCastle); + } + else if (daysWithoutCastle == 6) + { + text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. + text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + } + + showInfoDialogAndWait(components, text); + } + else + logGlobal->warn("Player has no towns, but daysWithoutCastle is not set"); + } + + cb->selectionMade(0, queryID); + movementController->onPlayerTurnStarted(); +} + +void CPlayerInterface::heroMoved(const TryMoveHero & details, bool verbose) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + if(LOCPLINT != this) + return; + + //FIXME: read once and store + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-ignore-hero"].Bool()) + return; + + const CGHeroInstance * hero = cb->getHero(details.id); //object representing this hero + + if (!hero) + return; + + movementController->onTryMoveHero(hero, details); +} + +void CPlayerInterface::heroKilled(const CGHeroInstance* hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + LOG_TRACE_PARAMS(logGlobal, "Hero %s killed handler for player %s", hero->getNameTranslated() % playerID); + + // if hero is not in town garrison + if (vstd::contains(localState->getWanderingHeroes(), hero)) + localState->removeWanderingHero(hero); + + adventureInt->onHeroChanged(hero); + localState->erasePath(hero); +} + +void CPlayerInterface::heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if(start && visitedObj) + { + if(visitedObj->getVisitSound()) + CCS->soundh->playSound(visitedObj->getVisitSound().value()); + } +} + +void CPlayerInterface::heroCreated(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->addWanderingHero(hero); + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::openTownWindow(const CGTownInstance * town) +{ + if(castleInt) + castleInt->close(); + castleInt = nullptr; + + auto newCastleInt = std::make_shared(town); + + GH.windows().pushWindow(newCastleInt); +} + +void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (which == PrimarySkill::EXPERIENCE) + { + for (auto ctw : GH.windows().findWindows()) + ctw->setExpToLevel(); + } + else + adventureInt->onHeroChanged(hero); +} + +void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + for (auto cuw : GH.windows().findWindows()) + cuw->redraw(); +} + +void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onHeroChanged(hero); + if (makingTurn && hero->tempOwner == playerID) + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (makingTurn && hero->tempOwner == playerID) + adventureInt->onHeroChanged(hero); +} +void CPlayerInterface::receivedResource() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + for (auto mw : GH.windows().findWindows()) + mw->resourceChanged(); + + GH.windows().totalRedraw(); +} + +void CPlayerInterface::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector& skills, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->soundh->playSound(soundBase::heroNewLevel); + GH.windows().createAndPushWindow(hero, pskill, skills, [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }); +} + +void CPlayerInterface::commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->soundh->playSound(soundBase::heroNewLevel); + GH.windows().createAndPushWindow(commander, skills, [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }); +} + +void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(town->garrisonHero) //wandering hero moved to the garrison + { + // This method also gets called on hero recruitment -> garrisoned hero is already in garrison + if(town->garrisonHero->tempOwner == playerID && vstd::contains(localState->getWanderingHeroes(), town->garrisonHero)) + localState->removeWanderingHero(town->garrisonHero); + } + + if(town->visitingHero) //hero leaves garrison + { + // This method also gets called on hero recruitment -> wandering heroes already contains new hero + if(town->visitingHero->tempOwner == playerID && !vstd::contains(localState->getWanderingHeroes(), town->visitingHero)) + localState->addWanderingHero(town->visitingHero); + } + adventureInt->onHeroChanged(nullptr); + adventureInt->onTownChanged(town); + + for (auto gh : GH.windows().findWindows()) + gh->updateGarrisons(); + + for (auto ki : GH.windows().findWindows()) + ki->townChanged(town); + + // Perform totalRedraw to update hero list on adventure map, if any dialogs are open + GH.windows().totalRedraw(); +} + +void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (hero->tempOwner != playerID ) + return; + + waitWhileDialog(); + openTownWindow(town); +} + +void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) +{ + std::vector instances; + + if(auto obj = cb->getObj(id1)) + instances.push_back(obj); + + + if(id2 != ObjectInstanceID() && id2 != id1) + { + if(auto obj = cb->getObj(id2)) + instances.push_back(obj); + } + + garrisonsChanged(instances); +} + +void CPlayerInterface::garrisonsChanged(std::vector objs) +{ + for (auto object : objs) + { + auto * hero = dynamic_cast(object); + auto * town = dynamic_cast(object); + + if (hero) + { + adventureInt->onHeroChanged(hero); + + if(hero->inTownGarrison) + { + adventureInt->onTownChanged(hero->visitedTown); + } + } + if (town) + adventureInt->onTownChanged(town); + } + + for (auto cgh : GH.windows().findWindows()) + cgh->updateGarrisons(); + + for (auto cmw : GH.windows().findWindows()) + { + if (vstd::contains(objs, cmw->hero)) + cmw->garrisonChanged(); + } + + GH.windows().totalRedraw(); +} + +void CPlayerInterface::buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) //what: 1 - built, 2 - demolished +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onTownChanged(town); + + if (castleInt) + { + castleInt->townlist->updateElement(town); + + if (castleInt->town == town) + { + switch(what) + { + case 1: + CCS->soundh->playSound(soundBase::newBuilding); + castleInt->addBuilding(buildingID); + break; + case 2: + castleInt->removeBuilding(buildingID); + break; + } + } + + // Perform totalRedraw in order to force redraw of updated town list icon from adventure map + GH.windows().totalRedraw(); + } + + for (auto cgh : GH.windows().findWindows()) + cgh->buildChanged(); +} + +void CPlayerInterface::battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) +{ + movementController->onBattleStarted(); + + //Don't wait for dialogs when we are non-active hot-seat player + if (LOCPLINT == this) + waitForAllDialogs(); +} + +void CPlayerInterface::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + bool useQuickCombat = settings["adventure"]["quickCombat"].Bool(); + bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool(); + + if ((replayAllowed && useQuickCombat) || forceQuickCombat) + { + autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); + + AutocombatPreferences autocombatPreferences = AutocombatPreferences(); + autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); + + autofightingAI->initBattleInterface(env, cb, autocombatPreferences); + autofightingAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, false); + isAutoFightOn = true; + cb->registerBattleInterface(autofightingAI); + } + + //Don't wait for dialogs when we are non-active hot-seat player + if (LOCPLINT == this) + waitForAllDialogs(); + + BATTLE_EVENT_POSSIBLE_RETURN; +} + +void CPlayerInterface::battleUnitsChanged(const BattleID & battleID, const std::vector & units) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + for(auto & info : units) + { + switch(info.operation) + { + case UnitChanges::EOperation::RESET_STATE: + { + const CStack * stack = cb->getBattle(battleID)->battleGetStackByID(info.id ); + + if(!stack) + { + logGlobal->error("Invalid unit ID %d", info.id); + continue; + } + battleInt->stackReset(stack); + } + break; + case UnitChanges::EOperation::REMOVE: + battleInt->stackRemoved(info.id); + break; + case UnitChanges::EOperation::ADD: + { + const CStack * unit = cb->getBattle(battleID)->battleGetStackByID(info.id); + if(!unit) + { + logGlobal->error("Invalid unit ID %d", info.id); + continue; + } + battleInt->stackAdded(unit); + } + break; + default: + logGlobal->error("Unknown unit operation %d", (int)info.operation); + break; + } + } +} + +void CPlayerInterface::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + std::vector> newObstacles; + std::vector removedObstacles; + + for(auto & change : obstacles) + { + if(change.operation == BattleChanges::EOperation::ADD) + { + auto instance = cb->getBattle(battleID)->battleGetObstacleByID(change.id); + if(instance) + newObstacles.push_back(instance); + else + logNetwork->error("Invalid obstacle instance %d", change.id); + } + if(change.operation == BattleChanges::EOperation::REMOVE) + removedObstacles.push_back(change); //Obstacles are already removed, so, show animation based on json struct + } + + if (!newObstacles.empty()) + battleInt->obstaclePlaced(newObstacles); + + if (!removedObstacles.empty()) + battleInt->obstacleRemoved(removedObstacles); + + battleInt->fieldController->redrawBackgroundWithHexes(); +} + +void CPlayerInterface::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->stackIsCatapulting(ca); +} + +void CPlayerInterface::battleNewRound(const BattleID & battleID) //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->newRound(); +} + +void CPlayerInterface::actionStarted(const BattleID & battleID, const BattleAction &action) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->startAction(action); +} + +void CPlayerInterface::actionFinished(const BattleID & battleID, const BattleAction &action) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->endAction(action); +} + +void CPlayerInterface::activeStack(const BattleID & battleID, const CStack * stack) //called when it's turn of that stack +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + logGlobal->trace("Awaiting command for %s", stack->nodeName()); + + assert(!cb->getBattle(battleID)->battleIsFinished()); + if (cb->getBattle(battleID)->battleIsFinished()) + { + logGlobal->error("Received CPlayerInterface::activeStack after battle is finished!"); + + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + return ; + } + + if (autofightingAI) + { + if (isAutoFightOn) + { + //FIXME: we want client rendering to proceed while AI is making actions + // so unlock mutex while AI is busy since this might take quite a while, especially if hero has many spells + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + autofightingAI->activeStack(battleID, stack); + return; + } + + cb->unregisterBattleInterface(autofightingAI); + autofightingAI.reset(); + } + + assert(battleInt); + if(!battleInt) + { + // probably battle is finished already + cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack)); + } + + battleInt->stackActivated(stack); +} + +void CPlayerInterface::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if(isAutoFightOn || autofightingAI) + { + isAutoFightOn = false; + cb->unregisterBattleInterface(autofightingAI); + autofightingAI.reset(); + + if(!battleInt) + { + bool allowManualReplay = queryID != QueryID::NONE; + + auto wnd = std::make_shared(*br, *this, allowManualReplay); + + if (allowManualReplay) + { + wnd->resultCallback = [=](ui32 selection) + { + cb->selectionMade(selection, queryID); + }; + } + GH.windows().pushWindow(wnd); + // #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it. + // Otherwise NewTurn causes freeze. + waitWhileDialog(); + return; + } + } + + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->battleFinished(*br, queryID); +} + +void CPlayerInterface::battleLogMessage(const BattleID & battleID, const std::vector & lines) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->displayBattleLog(lines); +} + +void CPlayerInterface::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->stackMoved(stack, dest, distance, teleport); +} +void CPlayerInterface::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->spellCast(sc); +} +void CPlayerInterface::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->battleStacksEffectsSet(sse); +} +void CPlayerInterface::battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->effectsController->battleTriggerEffect(bte); + + if(bte.effect == vstd::to_underlying(BonusType::MANA_DRAIN)) + { + const CGHeroInstance * manaDrainedHero = LOCPLINT->cb->getHero(ObjectInstanceID(bte.additionalInfo)); + battleInt->windowObject->heroManaPointsChanged(manaDrainedHero); + } +} +void CPlayerInterface::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + std::vector arg; + for(auto & elem : bsa) + { + const CStack * defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked, false); + const CStack * attacker = cb->getBattle(battleID)->battleGetStackByID(elem.attackerID, false); + + assert(defender); + + StackAttackedInfo info; + info.defender = defender; + info.attacker = attacker; + info.damageDealt = elem.damageAmount; + info.amountKilled = elem.killedAmount; + info.spellEffect = SpellID::NONE; + info.indirectAttack = ranged; + info.killed = elem.killed(); + info.rebirth = elem.willRebirth(); + info.cloneKilled = elem.cloneKilled(); + info.fireShield = elem.fireShield(); + + if (elem.isSpell()) + info.spellEffect = elem.spellID; + + arg.push_back(info); + } + battleInt->stacksAreAttacked(arg); +} +void CPlayerInterface::battleAttack(const BattleID & battleID, const BattleAttack * ba) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + StackAttackInfo info; + info.attacker = cb->getBattle(battleID)->battleGetStackByID(ba->stackAttacking); + info.defender = nullptr; + info.indirectAttack = ba->shot(); + info.lucky = ba->lucky(); + info.unlucky = ba->unlucky(); + info.deathBlow = ba->deathBlow(); + info.lifeDrain = ba->lifeDrain(); + info.tile = ba->tile; + info.spellEffect = SpellID::NONE; + + if (ba->spellLike()) + info.spellEffect = ba->spellID; + + for(auto & elem : ba->bsa) + { + if(!elem.isSecondary()) + { + assert(info.defender == nullptr); + info.defender = cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked); + } + else + { + info.secondaryDefender.push_back(cb->getBattle(battleID)->battleGetStackByID(elem.stackAttacked)); + } + } + assert(info.defender != nullptr); + assert(info.attacker != nullptr); + + battleInt->stackAttacking(info); +} + +void CPlayerInterface::battleGateStateChanged(const BattleID & battleID, const EGateState state) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->gateStateChanged(state); +} + +void CPlayerInterface::yourTacticPhase(const BattleID & battleID, int distance) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; +} + +void CPlayerInterface::showInfoDialog(EInfoWindowMode type, const std::string &text, const std::vector & components, int soundID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + bool autoTryHover = settings["gameTweaks"]["infoBarPick"].Bool() && type == EInfoWindowMode::AUTO; + auto timer = type == EInfoWindowMode::INFO ? 3000 : 4500; //Implement long info windows like in HD mod + + if(autoTryHover || type == EInfoWindowMode::INFO) + { + waitWhileDialog(); //Fix for mantis #98 + adventureInt->showInfoBoxMessage(components, text, timer); + + // abort movement, if any. Strictly speaking unnecessary, but prevents some edge cases, like movement sound on visiting Magi Hut with "show messages in status window" on + movementController->requestMovementAbort(); + + if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) + CCS->soundh->playSound(static_cast(soundID)); + return; + } + + if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + return; + } + std::vector vect = components; //I do not know currently how to avoid copy here + do + { + std::vector sender = {vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))}; + std::vector> intComps; + for (auto & component : sender) + intComps.push_back(std::make_shared(component)); + showInfoDialog(text,intComps,soundID); + vect.erase(vect.begin(), vect.begin() + std::min(vect.size(), static_cast(8))); + } + while(!vect.empty()); +} + +void CPlayerInterface::showInfoDialog(const std::string & text, std::shared_ptr component) +{ + std::vector> intComps; + intComps.push_back(component); + + showInfoDialog(text, intComps, soundBase::sound_todo); +} + +void CPlayerInterface::showInfoDialog(const std::string &text, const std::vector> & components, int soundID) +{ + LOG_TRACE_PARAMS(logGlobal, "player=%s, text=%s, is LOCPLINT=%d", playerID % text % (this==LOCPLINT)); + waitWhileDialog(); + + if (settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + return; + } + std::shared_ptr temp = CInfoWindow::create(text, playerID, components); + + if (makingTurn && GH.windows().count() > 0 && LOCPLINT == this) + { + CCS->soundh->playSound(static_cast(soundID)); + showingDialog->set(true); + movementController->requestMovementAbort(); // interrupt movement to show dialog + GH.windows().pushWindow(temp); + } + else + { + dialogs.push_back(temp); + } +} + +void CPlayerInterface::showInfoDialogAndWait(std::vector & components, const MetaString & text) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + std::string str = text.toString(); + + showInfoDialog(EInfoWindowMode::MODAL, str, components, 0); + waitWhileDialog(); +} + +void CPlayerInterface::showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components) +{ + movementController->requestMovementAbort(); + LOCPLINT->showingDialog->setn(true); + CInfoWindow::showYesNoDialog(text, components, onYes, onNo, playerID); +} + +void CPlayerInterface::showBlockingDialog( const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + + movementController->requestMovementAbort(); + CCS->soundh->playSound(static_cast(soundID)); + + if (!selection && cancel) //simple yes/no dialog + { + std::vector> intComps; + for (auto & component : components) + intComps.push_back(std::make_shared(component)); //will be deleted by close in window + + showYesNoDialog(text, [=](){ cb->selectionMade(1, askID); }, [=](){ cb->selectionMade(0, askID); }, intComps); + } + else if (selection) + { + std::vector> intComps; + for (auto & component : components) + intComps.push_back(std::make_shared(component)); //will be deleted by CSelWindow::close + + std::vector > > pom; + pom.push_back({ AnimationPath::builtin("IOKAY.DEF"),0}); + if (cancel) + { + pom.push_back({AnimationPath::builtin("ICANCEL.DEF"),0}); + } + + int charperline = 35; + if (pom.size() > 1) + charperline = 50; + GH.windows().createAndPushWindow(text, playerID, charperline, intComps, pom, askID); + intComps[0]->clickPressed(GH.getCursorPosition()); + intComps[0]->clickReleased(GH.getCursorPosition()); + } +} + +void CPlayerInterface::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + movementController->showTeleportDialog(hero, channel, exits, impassable, askID); +} + +void CPlayerInterface::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + auto selectCallback = [=](int selection) + { + cb->sendQueryReply(selection, askID); + }; + + auto cancelCallback = [=]() + { + cb->sendQueryReply(std::nullopt, askID); + }; + + const std::string localTitle = title.toString(); + const std::string localDescription = description.toString(); + + std::vector tempList; + tempList.reserve(objects.size()); + + for(auto item : objects) + tempList.push_back(item.getNum()); + + CComponent localIconC(icon); + + std::shared_ptr localIcon = localIconC.image; + localIconC.removeChild(localIcon.get(), false); + + std::shared_ptr wnd = std::make_shared(tempList, localIcon, localTitle, localDescription, selectCallback); + wnd->onExit = cancelCallback; + GH.windows().pushWindow(wnd); +} + +void CPlayerInterface::tileRevealed(const std::unordered_set &pos) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + //FIXME: wait for dialog? Magi hut/eye would benefit from this but may break other areas + adventureInt->onMapTilesChanged(pos); +} + +void CPlayerInterface::tileHidden(const std::unordered_set &pos) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onMapTilesChanged(pos); +} + +void CPlayerInterface::openHeroWindow(const CGHeroInstance *hero) +{ + GH.windows().createAndPushWindow(hero); +} + +void CPlayerInterface::availableCreaturesChanged( const CGDwelling *town ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (const CGTownInstance * townObj = dynamic_cast(town)) + { + for (auto fortScreen : GH.windows().findWindows()) + fortScreen->creaturesChangedEventHandler(); + + for (auto castleInterface : GH.windows().findWindows()) + if(castleInterface->town == town) + castleInterface->creaturesChangedEventHandler(); + + if (townObj) + for (auto ki : GH.windows().findWindows()) + ki->townChanged(townObj); + } + else if(town && GH.windows().count() > 0 && (town->ID == Obj::CREATURE_GENERATOR1 + || town->ID == Obj::CREATURE_GENERATOR4 || town->ID == Obj::WAR_MACHINE_FACTORY)) + { + for (auto crw : GH.windows().findWindows()) + if (crw->dwelling == town) + crw->availableCreaturesChanged(); + } +} + +void CPlayerInterface::heroBonusChanged( const CGHeroInstance *hero, const Bonus &bonus, bool gain ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if (bonus.type == BonusType::NONE) + return; + + adventureInt->onHeroChanged(hero); + if ((bonus.type == BonusType::FLYING_MOVEMENT || bonus.type == BonusType::WATER_WALKING) && !gain) + { + //recalculate paths because hero has lost bonus influencing pathfinding + localState->erasePath(hero); + } +} + +void CPlayerInterface::saveGame( BinarySerializer & h, const int version ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->serialize(h, version); +} + +void CPlayerInterface::loadGame( BinaryDeserializer & h, const int version ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + localState->serialize(h, version); + firstCall = -1; +} + +void CPlayerInterface::moveHero( const CGHeroInstance *h, const CGPath& path ) +{ + assert(LOCPLINT->makingTurn); + assert(h); + assert(!showingDialog->get()); + assert(dialogs.empty()); + + LOG_TRACE(logGlobal); + if (!LOCPLINT->makingTurn) + return; + if (!h) + return; //can't find hero + + //It shouldn't be possible to move hero with open dialog (or dialog waiting in bg) + if (showingDialog->get() || !dialogs.empty()) + return; + + if (localState->isHeroSleeping(h)) + localState->setHeroAwaken(h); + + movementController->requestMovementStart(h, path); +} + +void CPlayerInterface::showGarrisonDialog( const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onEnd = [=](){ cb->selectionMade(0, queryID); }; + + if (movementController->isHeroMovingThroughGarrison(down, up)) + { + onEnd(); + return; + } + + waitForAllDialogs(); + + auto cgw = std::make_shared(up, down, removableUnits); + cgw->quit->addCallback(onEnd); + GH.windows().pushWindow(cgw); +} + +/** + * Shows the dialog that appears when right-clicking an artifact that can be assembled + * into a combinational one on an artifact screen. Does not require the combination of + * artifacts to be legal. + */ +void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes) +{ + std::string text = artifact->getDescriptionTranslated(); + text += "\n\n"; + std::vector> scs; + + if(assembledArtifact) + { + // You possess all of the components to... + text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated()); + + // Picture of assembled artifact at bottom. + auto sc = std::make_shared(CComponent::artifact, assembledArtifact->getIndex(), 0); + scs.push_back(sc); + } + else + { + // Do you wish to disassemble this artifact? + text += CGI->generaltexth->allTexts[733]; + } + + showYesNoDialog(text, onYes, nullptr, scs); +} + +void CPlayerInterface::requestRealized( PackageApplied *pa ) +{ + if(pa->packType == typeList.getTypeID()) + movementController->onMoveHeroApplied(); + + if(pa->packType == typeList.getTypeID()) + movementController->onQueryReplyApplied(); +} + +void CPlayerInterface::showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) +{ + heroExchangeStarted(hero1, hero2, QueryID(-1)); +} + +void CPlayerInterface::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(hero1, hero2, query); +} + +void CPlayerInterface::beforeObjectPropertyChanged(const SetObjectProperty * sop) +{ + if (sop->what == ObjProperty::OWNER) + { + const CGObjectInstance * obj = cb->getObj(sop->id); + + if(obj->ID == Obj::TOWN) + { + auto town = static_cast(obj); + + if(obj->tempOwner == playerID) + { + localState->removeOwnedTown(town); + adventureInt->onTownChanged(town); + } + } + } +} + +void CPlayerInterface::objectPropertyChanged(const SetObjectProperty * sop) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if (sop->what == ObjProperty::OWNER) + { + const CGObjectInstance * obj = cb->getObj(sop->id); + + if(obj->ID == Obj::TOWN) + { + auto town = static_cast(obj); + + if(obj->tempOwner == playerID) + { + localState->addOwnedTown(town); + adventureInt->onTownChanged(town); + } + } + + //redraw minimap if owner changed + std::set pos = obj->getBlockedPos(); + std::unordered_set upos(pos.begin(), pos.end()); + adventureInt->onMapTilesChanged(upos); + + assert(cb->getTownsInfo().size() == localState->getOwnedTowns().size()); + } +} + +void CPlayerInterface::initializeHeroTownList() +{ + if(localState->getWanderingHeroes().empty()) + { + for(auto & hero : cb->getHeroesInfo()) + { + if(!hero->inTownGarrison) + localState->addWanderingHero(hero); + } + } + + if(localState->getOwnedTowns().empty()) + { + for(auto & town : cb->getTownsInfo()) + localState->addOwnedTown(town); + } + + if(adventureInt) + adventureInt->onHeroChanged(nullptr); +} + +void CPlayerInterface::showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + auto recruitCb = [=](CreatureID id, int count) + { + cb->recruitCreatures(dwelling, dst, id, count, -1); + }; + auto closeCb = [=]() + { + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(dwelling, level, dst, recruitCb, closeCb); +} + +void CPlayerInterface::waitWhileDialog() +{ + if (GH.amIGuiThread()) + { + logGlobal->warn("Cannot wait for dialogs in gui thread (deadlock risk)!"); + return; + } + + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::unique_lock un(showingDialog->mx); + while(showingDialog->data) + showingDialog->cond.wait(un); +} + +void CPlayerInterface::showShipyardDialog(const IShipyard *obj) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto state = obj->shipyardStatus(); + TResources cost; + obj->getBoatCost(cost); + GH.windows().createAndPushWindow(cost, state, obj->getBoatType(), [=](){ cb->buildBoat(obj); }); +} + +void CPlayerInterface::newObject( const CGObjectInstance * obj ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + //we might have built a boat in shipyard in opened town screen + if (obj->ID == Obj::BOAT + && LOCPLINT->castleInt + && obj->visitablePos() == LOCPLINT->castleInt->town->bestLocation()) + { + CCS->soundh->playSound(soundBase::newBuilding); + LOCPLINT->castleInt->addBuilding(BuildingID::SHIP); + } +} + +void CPlayerInterface::centerView (int3 pos, int focusTime) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + CCS->curh->hide(); + adventureInt->centerOnTile(pos); + if (focusTime) + { + GH.windows().totalRedraw(); + { + IgnoreEvents ignore(*this); + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(focusTime)); + } + } + CCS->curh->show(); +} + +void CPlayerInterface::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + if(playerID == initiator && obj->getRemovalSound()) + { + waitWhileDialog(); + CCS->soundh->playSound(obj->getRemovalSound().value()); + } + CGI->mh->waitForOngoingAnimations(); + + if(obj->ID == Obj::HERO && obj->tempOwner == playerID) + { + const CGHeroInstance * h = static_cast(obj); + heroKilled(h); + } + GH.fakeMouseMove(); +} + +void CPlayerInterface::objectRemovedAfter() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->onMapTilesChanged(boost::none); + + // visiting or garrisoned hero removed - update window + if (castleInt) + castleInt->updateGarrisons(); + + for (auto ki : GH.windows().findWindows()) + ki->heroRemoved(); +} + +void CPlayerInterface::playerBlocked(int reason, bool start) +{ + if(reason == PlayerBlocked::EReason::UPCOMING_BATTLE) + { + if(CSH->howManyPlayerInterfaces() > 1 && LOCPLINT != this && LOCPLINT->makingTurn == false) + { + //one of our players who isn't last in order got attacked not by our another player (happens for example in hotseat mode) + LOCPLINT = this; + GH.curInt = this; + adventureInt->onCurrentPlayerChanged(playerID); + std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); + boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); + std::vector> cmp; + cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + makingTurn = true; //workaround for stiff showInfoDialog implementation + showInfoDialog(msg, cmp); + makingTurn = false; + } + } +} + +void CPlayerInterface::update() +{ + // Make sure that gamestate won't change when GUI objects may obtain its parts on event processing or drawing request + boost::shared_lock gsLock(CGameState::mutex); + + // While mutexes were locked away we may be have stopped being the active interface + if (LOCPLINT != this) + return; + + //if there are any waiting dialogs, show them + if ((CSH->howManyPlayerInterfaces() <= 1 || makingTurn) && !dialogs.empty() && !showingDialog->get()) + { + showingDialog->set(true); + GH.windows().pushWindow(dialogs.front()); + dialogs.pop_front(); + } + + assert(adventureInt); + + // Handles mouse and key input + GH.handleEvents(); + GH.windows().simpleRedraw(); +} + +int CPlayerInterface::getLastIndex( std::string namePrefix) +{ + using namespace boost::filesystem; + using namespace boost::algorithm; + + path gamesDir = VCMIDirs::get().userSavePath(); + std::map dates; //save number => datestamp + + const directory_iterator enddir; + if (!exists(gamesDir)) + create_directory(gamesDir); + else + for (directory_iterator dir(gamesDir); dir != enddir; ++dir) + { + if (is_regular_file(dir->status())) + { + std::string name = dir->path().filename().string(); + if (starts_with(name, namePrefix) && ends_with(name, ".vcgm1")) + { + char nr = name[namePrefix.size()]; + if (std::isdigit(nr)) + dates[last_write_time(dir->path())] = boost::lexical_cast(nr); + } + } + } + + if (!dates.empty()) + return (--dates.end())->second; //return latest file number + return 0; +} + +void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if (player == playerID) + { + if (victoryLossCheckResult.loss()) + showInfoDialog(CGI->generaltexth->allTexts[95]); + + assert(GH.curInt == LOCPLINT); + auto previousInterface = LOCPLINT; //without multiple player interfaces some of lines below are useless, but for hotseat we wanna swap player interface temporarily + + LOCPLINT = this; //this is needed for dialog to show and avoid freeze, dialog showing logic should be reworked someday + GH.curInt = this; //waiting for dialogs requires this to get events + + if(!makingTurn) + { + makingTurn = true; //also needed for dialog to show with current implementation + waitForAllDialogs(); + makingTurn = false; + } + else + waitForAllDialogs(); + + GH.curInt = previousInterface; + LOCPLINT = previousInterface; + + if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated + { + if(adventureInt) + { + GH.windows().popWindows(GH.windows().count()); + adventureInt.reset(); + } + } + + if (victoryLossCheckResult.victory() && LOCPLINT == this) + { + // end game if current human player has won + CSH->sendClientDisconnecting(); + requestReturningToMainMenu(true); + } + else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) + { + //all human players eliminated + CSH->sendClientDisconnecting(); + requestReturningToMainMenu(false); + } + + if (GH.curInt == this) + GH.curInt = nullptr; + } +} + +void CPlayerInterface::playerBonusChanged( const Bonus &bonus, bool gain ) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; +} + +void CPlayerInterface::showPuzzleMap() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + waitWhileDialog(); + + //TODO: interface should not know the real position of Grail... + double ratio = 0; + int3 grailPos = cb->getGrailPos(&ratio); + + GH.windows().createAndPushWindow(grailPos, ratio); +} + +void CPlayerInterface::viewWorldMap() +{ + adventureInt->openWorldView(); +} + +void CPlayerInterface::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + + if(GH.windows().topWindow()) + GH.windows().popWindows(1); + + if(spellID == SpellID::FLY || spellID == SpellID::WATER_WALK) + localState->erasePath(caster); + + auto castSoundPath = spellID.toSpell()->getCastSound(); + if(!castSoundPath.empty()) + CCS->soundh->playSound(castSoundPath); +} + +void CPlayerInterface::tryDigging(const CGHeroInstance * h) +{ + int msgToShow = -1; + + const auto diggingStatus = h->diggingStatus(); + + switch(diggingStatus) + { + case EDiggingStatus::CAN_DIG: + break; + case EDiggingStatus::LACK_OF_MOVEMENT: + msgToShow = 56; //"Digging for artifacts requires a whole day, try again tomorrow." + break; + case EDiggingStatus::TILE_OCCUPIED: + msgToShow = 97; //Try searching on clear ground. + break; + case EDiggingStatus::WRONG_TERRAIN: + msgToShow = 60; ////Try looking on land! + break; + case EDiggingStatus::BACKPACK_IS_FULL: + msgToShow = 247; //Searching for the Grail is fruitless... + break; + default: + assert(0); + } + + if(msgToShow < 0) + cb->dig(h); + else + showInfoDialog(CGI->generaltexth->allTexts[msgToShow]); +} + +void CPlayerInterface::battleNewRoundFirst(const BattleID & battleID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + BATTLE_EVENT_POSSIBLE_RETURN; + + battleInt->newRoundFirst(); +} + +void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + + if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL) + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP); + else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD) + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP); + else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD)) + GH.windows().createAndPushWindow(market, visitor, onWindowClosed); + else if(!market->availableModes().empty()) + GH.windows().createAndPushWindow(market, visitor, onWindowClosed, market->availableModes().front()); +} + +void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(visitor, market, onWindowClosed); +} + +void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(visitor, object); +} + +void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + for (auto cmw : GH.windows().findWindows()) + cmw->artifactsChanged(false); +} + +void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto onWindowClosed = [this, queryID](){ + cb->selectionMade(0, queryID); + }; + GH.windows().createAndPushWindow(object, onWindowClosed); +} + +void CPlayerInterface::showThievesGuildWindow (const CGObjectInstance * obj) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(obj); +} + +void CPlayerInterface::showQuestLog() +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + GH.windows().createAndPushWindow(LOCPLINT->cb->getMyQuests()); +} + +void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj) +{ + if (obj->shipyardStatus() != IBoatGenerator::GOOD) + { + MetaString txt; + obj->getProblemText(txt); + showInfoDialog(txt.toString()); + } + else + showShipyardDialog(obj); +} + +void CPlayerInterface::requestReturningToMainMenu(bool won) +{ + HighScoreParameter param; + param.difficulty = cb->getStartInfo()->difficulty; + param.day = cb->getDate(); + param.townAmount = cb->howManyTowns(); + param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated; + param.hasGrail = false; + for(const CGHeroInstance * h : cb->getHeroesInfo()) + if(h->hasArt(ArtifactID::GRAIL)) + param.hasGrail = true; + for(const CGTownInstance * t : cb->getTownsInfo()) + if(t->builtBuildings.count(BuildingID::GRAIL)) + param.hasGrail = true; + param.allDefeated = true; + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + auto ps = cb->getPlayerState(player, false); + if(ps && player != *cb->getPlayerID()) + if(!ps->checkVanquished()) + param.allDefeated = false; + } + param.scenarioName = cb->getMapHeader()->name.toString(); + param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name; + HighScoreCalculation highScoreCalc; + highScoreCalc.parameters.push_back(param); + highScoreCalc.isCampaign = false; + + if(won && cb->getStartInfo()->campState) + CSH->startCampaignScenario(param, cb->getStartInfo()->campState); + else + { + GH.dispatchMainThread( + [won, highScoreCalc]() + { + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("main"); + GH.windows().createAndPushWindow(won, highScoreCalc); + } + ); + } +} + +void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) +{ + auto hero = std::visit(HeroObjectRetriever(), al.artHolder); + if(hero) + { + auto art = hero->getArt(al.slot); + if(art == nullptr) + { + logGlobal->error("artifact location %d points to nothing", + al.slot.num); + return; + } + ArtifactUtilsClient::askToAssemble(hero, al.slot); + } +} + +void CPlayerInterface::artifactPut(const ArtifactLocation &al) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto hero = std::visit(HeroObjectRetriever(), al.artHolder); + adventureInt->onHeroChanged(hero); +} + +void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto hero = std::visit(HeroObjectRetriever(), al.artHolder); + adventureInt->onHeroChanged(hero); + + for(auto artWin : GH.windows().findWindows()) + artWin->artifactRemoved(al); + + waitWhileDialog(); +} + +void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto hero = std::visit(HeroObjectRetriever(), dst.artHolder); + adventureInt->onHeroChanged(hero); + + bool redraw = true; + // If a bulk transfer has arrived, then redrawing only the last art movement. + if(numOfMovedArts != 0) + { + numOfMovedArts--; + if(numOfMovedArts != 0) + redraw = false; + } + + for(auto artWin : GH.windows().findWindows()) + artWin->artifactMoved(src, dst, redraw); + + waitWhileDialog(); +} + +void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) +{ + numOfMovedArts = numOfArts; +} + +void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto hero = std::visit(HeroObjectRetriever(), al.artHolder); + adventureInt->onHeroChanged(hero); + + for(auto artWin : GH.windows().findWindows()) + artWin->artifactAssembled(al); +} + +void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + auto hero = std::visit(HeroObjectRetriever(), al.artHolder); + adventureInt->onHeroChanged(hero); + + for(auto artWin : GH.windows().findWindows()) + artWin->artifactDisassembled(al); +} + +void CPlayerInterface::waitForAllDialogs() +{ + while(!dialogs.empty()) + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(5)); + } + waitWhileDialog(); +} + +void CPlayerInterface::proposeLoadingGame() +{ + showYesNoDialog( + CGI->generaltexth->allTexts[68], + []() + { + GH.dispatchMainThread( + []() + { + CSH->endGameplay(); + GH.defActionsDef = 63; + CMM->menu->switchToTab("load"); + } + ); + }, + nullptr + ); +} + +bool CPlayerInterface::capturedAllEvents() +{ + if(movementController->isHeroMoving()) + { + //just inform that we are capturing events. they will be processed by heroMoved() in client thread. + return true; + } + + bool needToLockAdventureMap = adventureInt && adventureInt->isActive() && CGI->mh->hasOngoingAnimations(); + bool quickCombatOngoing = isAutoFightOn && !battleInt; + + if (ignoreEvents || needToLockAdventureMap || quickCombatOngoing ) + { + GH.input().ignoreEventsUntilInput(); + return true; + } + + return false; +} + +void CPlayerInterface::showWorldViewEx(const std::vector& objectPositions, bool showTerrain) +{ + EVENT_HANDLER_CALLED_BY_CLIENT; + adventureInt->openWorldView(objectPositions, showTerrain ); +} + +std::optional CPlayerInterface::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) +{ + return std::nullopt; +} diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 65041ae67..41245ee5f 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -1,234 +1,234 @@ -/* - * CPlayerInterface.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/FunctionList.h" -#include "../lib/CGameInterface.h" -#include "gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Artifact; - -struct TryMoveHero; -class CGHeroInstance; -class CStack; -class CCreature; -struct CGPath; -class CCreatureSet; -class CGObjectInstance; -struct UpgradeInfo; -template struct CondSh; -struct CPathsInfo; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class AdventureMapInterface; -class CCastleInterface; -class BattleInterface; -class CComponent; -class CSelectableComponent; -class CSlider; -class CInGameConsole; -class CInfoWindow; -class IShowActivatable; -class ClickableL; -class ClickableR; -class Hoverable; -class KeyInterested; -class MotionInterested; -class PlayerLocalState; -class TimeInterested; -class HeroMovementController; - -namespace boost -{ - class mutex; - class recursive_mutex; -} - -/// Central class for managing user interface logic -class CPlayerInterface : public CGameInterface, public IUpdateable -{ - bool ignoreEvents; - size_t numOfMovedArts; - - // -1 - just loaded game; 1 - just started game; 0 otherwise - int firstCall; - int autosaveCount; - - std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) - - std::unique_ptr movementController; -public: // TODO: make private - std::shared_ptr env; - - std::unique_ptr localState; - - //minor interfaces - CondSh *showingDialog; //indicates if dialog box is displayed - - bool makingTurn; //if player is already making his turn - - CCastleInterface * castleInt; //nullptr if castle window isn't opened - static std::shared_ptr battleInt; //nullptr if no battle - CInGameConsole * cingconsole; - - std::shared_ptr cb; //to communicate with engine - - //During battle is quick combat mode is used - std::shared_ptr autofightingAI; //AI that makes decisions - bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. - -protected: // Call-ins from server, should not be called directly, but only via GameInterface - - void update() override; - void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; - - void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; - void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished - - void artifactPut(const ArtifactLocation &al) override; - void artifactRemoved(const ArtifactLocation &al) override; - void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override; - void bulkArtMovementStart(size_t numOfArts) override; - void artifactAssembled(const ArtifactLocation &al) override; - void askToAssembleArtifact(const ArtifactLocation & dst) override; - void artifactDisassembled(const ArtifactLocation &al) override; - - void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; - void heroCreated(const CGHeroInstance* hero) override; - void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; - void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; - void heroInGarrisonChange(const CGTownInstance *town) override; - void heroMoved(const TryMoveHero & details, bool verbose = true) override; - void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; - void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; - void heroManaPointsChanged(const CGHeroInstance * hero) override; - void heroMovePointsChanged(const CGHeroInstance * hero) override; - void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; - void receivedResource() override; - void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; - void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override; - void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; - void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; - void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; - void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; - void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; - void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; - void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell - void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war - void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles - void newObject(const CGObjectInstance * obj) override; - void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) - void yourTurn(QueryID queryID) override; - void availableCreaturesChanged(const CGDwelling *town) override; - void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it - void playerBonusChanged(const Bonus &bonus, bool gain) override; - void requestRealized(PackageApplied *pa) override; - void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; - void centerView (int3 pos, int focusTime) override; - void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; - void objectPropertyChanged(const SetObjectProperty * sop) override; - void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override; - void objectRemovedAfter() override; - void playerBlocked(int reason, bool start) override; - void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; - void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface - void playerEndsTurn(PlayerColor player) override; - void saveGame(BinarySerializer & h, const int version) override; //saving - void loadGame(BinaryDeserializer & h, const int version) override; //loading - void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; - - //for battles - void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero - void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero - void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack - void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack - void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle - void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling - void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - void battleLogMessage(const BattleID & battleID, const std::vector & lines) override; - void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; - void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; - void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks - void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect - void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; - void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right - void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right - void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; - void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; - void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack - void battleGateStateChanged(const BattleID & battleID, const EGateState state) override; - void yourTacticPhase(const BattleID & battleID, int distance) override; - std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; - -public: // public interface for use by client via LOCPLINT access - - // part of GameInterface that is also used by client code - void showPuzzleMap() override; - void viewWorldMap() override; - void showQuestLog() override; - void showThievesGuildWindow (const CGObjectInstance * obj) override; - void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; - void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; - - void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); - void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); - void waitWhileDialog(); - void waitForAllDialogs(); - void openTownWindow(const CGTownInstance * town); //shows townscreen - void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero - - void showInfoDialog(const std::string &text, std::shared_ptr component); - void showInfoDialog(const std::string &text, const std::vector> & components = std::vector>(), int soundID = 0); - void showInfoDialogAndWait(std::vector & components, const MetaString & text); - void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); - - void moveHero(const CGHeroInstance *h, const CGPath& path); - - void tryDigging(const CGHeroInstance *h); - void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; - void proposeLoadingGame(); - void performAutosave(); - void gamePause(bool pause); - - ///returns true if all events are processed internally - bool capturedAllEvents(); - - CPlayerInterface(PlayerColor Player); - ~CPlayerInterface(); - -private: - struct IgnoreEvents - { - CPlayerInterface & owner; - IgnoreEvents(CPlayerInterface & Owner):owner(Owner) - { - owner.ignoreEvents = true; - }; - ~IgnoreEvents() - { - owner.ignoreEvents = false; - }; - }; - - void heroKilled(const CGHeroInstance* hero); - void garrisonsChanged(std::vector objs); - void requestReturningToMainMenu(bool won); - void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close - void initializeHeroTownList(); - int getLastIndex(std::string namePrefix); -}; - -/// Provides global access to instance of interface of currently active player -extern CPlayerInterface * LOCPLINT; +/* + * CPlayerInterface.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/FunctionList.h" +#include "../lib/CGameInterface.h" +#include "gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Artifact; + +struct TryMoveHero; +class CGHeroInstance; +class CStack; +class CCreature; +struct CGPath; +class CCreatureSet; +class CGObjectInstance; +struct UpgradeInfo; +template struct CondSh; +struct CPathsInfo; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class AdventureMapInterface; +class CCastleInterface; +class BattleInterface; +class CComponent; +class CSelectableComponent; +class CSlider; +class CInGameConsole; +class CInfoWindow; +class IShowActivatable; +class ClickableL; +class ClickableR; +class Hoverable; +class KeyInterested; +class MotionInterested; +class PlayerLocalState; +class TimeInterested; +class HeroMovementController; + +namespace boost +{ + class mutex; + class recursive_mutex; +} + +/// Central class for managing user interface logic +class CPlayerInterface : public CGameInterface, public IUpdateable +{ + bool ignoreEvents; + size_t numOfMovedArts; + + // -1 - just loaded game; 1 - just started game; 0 otherwise + int firstCall; + int autosaveCount; + + std::list> dialogs; //queue of dialogs awaiting to be shown (not currently shown!) + + std::unique_ptr movementController; +public: // TODO: make private + std::shared_ptr env; + + std::unique_ptr localState; + + //minor interfaces + CondSh *showingDialog; //indicates if dialog box is displayed + + bool makingTurn; //if player is already making his turn + + CCastleInterface * castleInt; //nullptr if castle window isn't opened + static std::shared_ptr battleInt; //nullptr if no battle + CInGameConsole * cingconsole; + + std::shared_ptr cb; //to communicate with engine + + //During battle is quick combat mode is used + std::shared_ptr autofightingAI; //AI that makes decisions + bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface. + +protected: // Call-ins from server, should not be called directly, but only via GameInterface + + void update() override; + void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB) override; + + void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; + void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished + + void artifactPut(const ArtifactLocation &al) override; + void artifactRemoved(const ArtifactLocation &al) override; + void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) override; + void bulkArtMovementStart(size_t numOfArts) override; + void artifactAssembled(const ArtifactLocation &al) override; + void askToAssembleArtifact(const ArtifactLocation & dst) override; + void artifactDisassembled(const ArtifactLocation &al) override; + + void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override; + void heroCreated(const CGHeroInstance* hero) override; + void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID) override; + void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID) override; + void heroInGarrisonChange(const CGTownInstance *town) override; + void heroMoved(const TryMoveHero & details, bool verbose = true) override; + void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override; + void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; + void heroManaPointsChanged(const CGHeroInstance * hero) override; + void heroMovePointsChanged(const CGHeroInstance * hero) override; + void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town) override; + void receivedResource() override; + void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID) override; + void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID) override; + void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override; + void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override; + void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) override; + void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; + void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID) override; + void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override; + void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override; //called when a hero casts a spell + void tileHidden(const std::unordered_set &pos) override; //called when given tiles become hidden under fog of war + void tileRevealed(const std::unordered_set &pos) override; //called when fog of war disappears from given tiles + void newObject(const CGObjectInstance * obj) override; + void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) + void yourTurn(QueryID queryID) override; + void availableCreaturesChanged(const CGDwelling *town) override; + void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain) override;//if gain hero received bonus, else he lost it + void playerBonusChanged(const Bonus &bonus, bool gain) override; + void requestRealized(PackageApplied *pa) override; + void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; + void centerView (int3 pos, int focusTime) override; + void beforeObjectPropertyChanged(const SetObjectProperty * sop) override; + void objectPropertyChanged(const SetObjectProperty * sop) override; + void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator) override; + void objectRemovedAfter() override; + void playerBlocked(int reason, bool start) override; + void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) override; + void playerStartsTurn(PlayerColor player) override; //called before yourTurn on active itnerface + void playerEndsTurn(PlayerColor player) override; + void saveGame(BinarySerializer & h, const int version) override; //saving + void loadGame(BinaryDeserializer & h, const int version) override; //loading + void showWorldViewEx(const std::vector & objectPositions, bool showTerrain) override; + + //for battles + void actionFinished(const BattleID & battleID, const BattleAction& action) override;//occurs AFTER action taken by active stack or by the hero + void actionStarted(const BattleID & battleID, const BattleAction& action) override;//occurs BEFORE action taken by active stack or by the hero + void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack + void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //stack performs attack + void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; //end of battle + void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied; used for HP regen handling + void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + void battleLogMessage(const BattleID & battleID, const std::vector & lines) override; + void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; //called when a specific effect is set to stacks + void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte) override; //various one-shot effect + void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right + void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right + void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; + void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack + void battleGateStateChanged(const BattleID & battleID, const EGateState state) override; + void yourTacticPhase(const BattleID & battleID, int distance) override; + std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override; + +public: // public interface for use by client via LOCPLINT access + + // part of GameInterface that is also used by client code + void showPuzzleMap() override; + void viewWorldMap() override; + void showQuestLog() override; + void showThievesGuildWindow (const CGObjectInstance * obj) override; + void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override; + void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard; + + void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2); + void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList onYes); + void waitWhileDialog(); + void waitForAllDialogs(); + void openTownWindow(const CGTownInstance * town); //shows townscreen + void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero + + void showInfoDialog(const std::string &text, std::shared_ptr component); + void showInfoDialog(const std::string &text, const std::vector> & components = std::vector>(), int soundID = 0); + void showInfoDialogAndWait(std::vector & components, const MetaString & text); + void showYesNoDialog(const std::string &text, CFunctionList onYes, CFunctionList onNo, const std::vector> & components = std::vector>()); + + void moveHero(const CGHeroInstance *h, const CGPath& path); + + void tryDigging(const CGHeroInstance *h); + void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard; + void proposeLoadingGame(); + void performAutosave(); + void gamePause(bool pause); + + ///returns true if all events are processed internally + bool capturedAllEvents(); + + CPlayerInterface(PlayerColor Player); + ~CPlayerInterface(); + +private: + struct IgnoreEvents + { + CPlayerInterface & owner; + IgnoreEvents(CPlayerInterface & Owner):owner(Owner) + { + owner.ignoreEvents = true; + }; + ~IgnoreEvents() + { + owner.ignoreEvents = false; + }; + }; + + void heroKilled(const CGHeroInstance* hero); + void garrisonsChanged(std::vector objs); + void requestReturningToMainMenu(bool won); + void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close + void initializeHeroTownList(); + int getLastIndex(std::string namePrefix); +}; + +/// Provides global access to instance of interface of currently active player +extern CPlayerInterface * LOCPLINT; diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 20ece6787..fae06b68a 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -1,665 +1,665 @@ -/* - * CVideoHandler.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 "CVideoHandler.h" - -#include "CMT.h" -#include "gui/CGuiHandler.h" -#include "eventsSDL/InputHandler.h" -#include "gui/FramerateManager.h" -#include "renderSDL/SDL_Extensions.h" -#include "CPlayerInterface.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CInputStream.h" - -#include - -#ifndef DISABLE_VIDEO - -extern "C" { -#include -#include -#include -#include -} - -#ifdef _MSC_VER -#pragma comment(lib, "avcodec.lib") -#pragma comment(lib, "avutil.lib") -#pragma comment(lib, "avformat.lib") -#pragma comment(lib, "swscale.lib") -#endif // _MSC_VER - -// Define a set of functions to read data -static int lodRead(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - - return static_cast(video->data->read(buf, size)); -} - -static si64 lodSeek(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->data->getSize(); - - return video->data->seek(pos); -} - -// Define a set of functions to read data -static int lodReadAudio(void* opaque, uint8_t* buf, int size) -{ - auto video = reinterpret_cast(opaque); - - return static_cast(video->dataAudio->read(buf, size)); -} - -static si64 lodSeekAudio(void * opaque, si64 pos, int whence) -{ - auto video = reinterpret_cast(opaque); - - if (whence & AVSEEK_SIZE) - return video->dataAudio->getSize(); - - return video->dataAudio->seek(pos); -} - -CVideoPlayer::CVideoPlayer() - : stream(-1) - , format (nullptr) - , codecContext(nullptr) - , codec(nullptr) - , frame(nullptr) - , sws(nullptr) - , context(nullptr) - , texture(nullptr) - , dest(nullptr) - , destRect(0,0,0,0) - , pos(0,0,0,0) - , frameTime(0) - , doLoop(false) -{} - -bool CVideoPlayer::open(const VideoPath & fname, bool scale) -{ - return open(fname, true, false); -} - -// loop = to loop through the video -// useOverlay = directly write to the screen. -bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale) -{ - close(); - - doLoop = loop; - frameTime = 0; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fname = videoToOpen; - else - fname = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fname)) - { - logGlobal->error("Error: video %s was not found", fname.getName()); - return false; - } - - data = CResourceHandler::get()->load(fname); - - static const int BUFFER_SIZE = 4096; - - unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek); - - format = avformat_alloc_context(); - format->pb = context; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return false; - } - // Retrieve stream information - if (avformat_find_stream_info(format, nullptr) < 0) - return false; - - // Find the first video stream - stream = -1; - for(ui32 i=0; inb_streams; i++) - { - if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) - { - stream = i; - break; - } - } - - if (stream < 0) - // No video stream in that file - return false; - - // Find the decoder for the video stream - codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); - - if (codec == nullptr) - { - // Unsupported codec - return false; - } - - codecContext = avcodec_alloc_context3(codec); - if(!codecContext) - return false; - // Get a pointer to the codec context for the video stream - int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContext); - return false; - } - - // Open codec - if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) - { - // Could not open codec - codec = nullptr; - return false; - } - // Allocate video frame - frame = av_frame_alloc(); - - //setup scaling - if(scale) - { - pos.w = screen->w; - pos.h = screen->h; - } - else - { - pos.w = codecContext->width; - pos.h = codecContext->height; - } - - // Allocate a place to put our YUV image on that screen - if (useOverlay) - { - texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); - } - else - { - dest = CSDL_Ext::newSurface(pos.w, pos.h); - destRect.x = destRect.y = 0; - destRect.w = pos.w; - destRect.h = pos.h; - } - - if (texture == nullptr && dest == nullptr) - return false; - - if (texture) - { // Convert the image into YUV format that SDL uses - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, - AV_PIX_FMT_YUV420P, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - else - { - AVPixelFormat screenFormat = AV_PIX_FMT_NONE; - if (screen->format->Bshift > screen->format->Rshift) - { - // this a BGR surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_BGR565; break; - case 3: screenFormat = AV_PIX_FMT_BGR24; break; - case 4: screenFormat = AV_PIX_FMT_BGR32; break; - default: return false; - } - } - else - { - // this a RGB surface - switch (screen->format->BytesPerPixel) - { - case 2: screenFormat = AV_PIX_FMT_RGB565; break; - case 3: screenFormat = AV_PIX_FMT_RGB24; break; - case 4: screenFormat = AV_PIX_FMT_RGB32; break; - default: return false; - } - } - - sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, - pos.w, pos.h, screenFormat, - SWS_BICUBIC, nullptr, nullptr, nullptr); - } - - if (sws == nullptr) - return false; - - return true; -} - -// Read the next frame. Return false on error/end of file. -bool CVideoPlayer::nextFrame() -{ - AVPacket packet; - int frameFinished = 0; - bool gotError = false; - - if (sws == nullptr) - return false; - - while(!frameFinished) - { - int ret = av_read_frame(format, &packet); - if (ret < 0) - { - // Error. It's probably an end of file. - if (doLoop && !gotError) - { - // Rewind - frameTime = 0; - if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) - break; - gotError = true; - } - else - { - break; - } - } - else - { - // Is this a packet from the video stream? - if (packet.stream_index == stream) - { - // Decode video frame - int rc = avcodec_send_packet(codecContext, &packet); - if (rc >=0) - packet.size = 0; - rc = avcodec_receive_frame(codecContext, frame); - if (rc >= 0) - frameFinished = 1; - // Did we get a video frame? - if (frameFinished) - { - uint8_t *data[4]; - int linesize[4]; - - if (texture) { - av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1); - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - - SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0], - data[1], linesize[1], - data[2], linesize[2]); - av_freep(&data[0]); - } - else - { - /* Avoid buffer overflow caused by sws_scale(): - * http://trac.ffmpeg.org/ticket/9254 - * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() - * has a few requirements for target data buffers on rescaling: - * 1. buffer has to be aligned to be usable for SIMD instructions - * 2. buffer has to be padded to allow small overflow by SIMD instructions - * Unfortunately SDL_Surface does not provide these guarantees. - * This means that atempt to rescale directly into SDL surface causes - * memory corruption. Usually it happens on campaign selection screen - * where short video moves start spinning on mouse hover. - * - * To fix [1.] we use av_malloc() for memory allocation. - * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. - * We have to use intermdiate buffer and then use memcpy() to land it - * to SDL_Surface. - */ - size_t pic_bytes = dest->pitch * dest->h; - size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ - void * for_sws = av_malloc (pic_bytes + ffmped_pad); - data[0] = (ui8 *)for_sws; - linesize[0] = dest->pitch; - - sws_scale(sws, frame->data, frame->linesize, - 0, codecContext->height, data, linesize); - memcpy(dest->pixels, for_sws, pic_bytes); - av_free(for_sws); - } - } - } - - av_packet_unref(&packet); - } - } - - return frameFinished != 0; -} - -void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) -{ - if (sws == nullptr) - return; - - pos.x = x; - pos.y = y; - CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft()); - - if (update) - CSDL_Ext::updateRect(dst, pos); -} - -void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) -{ - show(x, y, dst, update); -} - -void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) -{ - if (sws == nullptr) - return; - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base); - frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0; - - if (frameTime >= frameEndTime ) - { - if (nextFrame()) - show(x,y,dst,update); - else - { - if(onVideoRestart) - onVideoRestart(); - VideoPath filenameToReopen = fname; // create copy to backup this->fname - open(filenameToReopen); - nextFrame(); - - // The y position is wrong at the first frame. - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - show(x, y--, dst, update); - } - } - else - { - redraw(x, y, dst, update); - } -} - -void CVideoPlayer::close() -{ - fname = VideoPath(); - - if (sws) - { - sws_freeContext(sws); - sws = nullptr; - } - - if (texture) - { - SDL_DestroyTexture(texture); - texture = nullptr; - } - - if (dest) - { - SDL_FreeSurface(dest); - dest = nullptr; - } - - if (frame) - { - av_frame_free(&frame);//will be set to null - } - - if (codec) - { - avcodec_close(codecContext); - codec = nullptr; - } - if (codecContext) - { - avcodec_free_context(&codecContext); - } - - if (format) - { - avformat_close_input(&format); - } - - if (context) - { - av_free(context); - context = nullptr; - } -} - -std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) -{ - std::pair, si64> dat(std::make_pair(nullptr, 0)); - - VideoPath fnameAudio; - - if (CResourceHandler::get()->existsResource(videoToOpen)) - fnameAudio = videoToOpen; - else - fnameAudio = videoToOpen.addPrefix("VIDEO/"); - - if (!CResourceHandler::get()->existsResource(fnameAudio)) - { - logGlobal->error("Error: video %s was not found", fnameAudio.getName()); - return dat; - } - - dataAudio = CResourceHandler::get()->load(fnameAudio); - - static const int BUFFER_SIZE = 4096; - - unsigned char * bufferAudio = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg - AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio); - - AVFormatContext * formatAudio = avformat_alloc_context(); - formatAudio->pb = contextAudio; - // filename is not needed - file was already open and stored in this->data; - int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr); - - if (avfopen != 0) - { - return dat; - } - // Retrieve stream information - if (avformat_find_stream_info(formatAudio, nullptr) < 0) - return dat; - - // Find the first audio stream - int streamAudio = -1; - for(ui32 i = 0; i < formatAudio->nb_streams; i++) - { - if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) - { - streamAudio = i; - break; - } - } - - if(streamAudio < 0) - return dat; - - const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id); - - AVCodecContext *codecContextAudio; - if (codecAudio != nullptr) - codecContextAudio = avcodec_alloc_context3(codecAudio); - - // Get a pointer to the codec context for the audio stream - if (streamAudio > -1) - { - int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); - if (ret < 0) - { - //We cannot get codec from parameters - avcodec_free_context(&codecContextAudio); - } - } - - // Open codec - AVFrame *frameAudio; - if (codecAudio != nullptr) - { - if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) - { - // Could not open codec - codecAudio = nullptr; - } - // Allocate audio frame - frameAudio = av_frame_alloc(); - } - - AVPacket packet; - - std::vector samples; - - while (av_read_frame(formatAudio, &packet) >= 0) - { - if(packet.stream_index == streamAudio) - { - int rc = avcodec_send_packet(codecContextAudio, &packet); - if (rc >= 0) - packet.size = 0; - rc = avcodec_receive_frame(codecContextAudio, frameAudio); - int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); - if (rc >= 0) - for (int s = 0; s < bytesToRead; s += sizeof(ui8)) - { - ui8 value; - memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); - samples.push_back(value); - } - } - - av_packet_unref(&packet); - } - - typedef struct WAV_HEADER { - ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; - ui32 ChunkSize; - ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; - ui8 fmt[4] = {'f', 'm', 't', ' '}; - ui32 Subchunk1Size = 16; - ui16 AudioFormat = 1; - ui16 NumOfChan = 2; - ui32 SamplesPerSec = 22050; - ui32 bytesPerSec = 22050 * 2; - ui16 blockAlign = 2; - ui16 bitsPerSample = 16; - ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; - ui32 Subchunk2Size; - } wav_hdr; - - wav_hdr wav; - wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; - wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; - wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate; - wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; - auto wavPtr = reinterpret_cast(&wav); - - dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); - std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); - std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); - - if (frameAudio) - av_frame_free(&frameAudio); - - if (codecAudio) - { - avcodec_close(codecContextAudio); - codecAudio = nullptr; - } - if (codecContextAudio) - avcodec_free_context(&codecContextAudio); - - if (formatAudio) - avformat_close_input(&formatAudio); - - if (contextAudio) - { - av_free(contextAudio); - contextAudio = nullptr; - } - - return dat; -} - -// Plays a video. Only works for overlays. -bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) -{ - // Note: either the windows player or the linux player is - // broken. Compensate here until the bug is found. - y--; - - pos.x = x; - pos.y = y; - frameTime = 0.0; - - while(nextFrame()) - { - if(stopOnKey) - { - GH.input().fetchEvents(); - if(GH.input().ignoreEventsUntilInput()) - return false; - } - - SDL_Rect rect = CSDL_Ext::toSDL(pos); - - SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); - SDL_RenderPresent(mainRenderer); - -#if (LIBAVUTIL_VERSION_MAJOR < 58) - auto packet_duration = frame->pkt_duration; -#else - auto packet_duration = frame->duration; -#endif - double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); - uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); - - boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); - } - - return true; -} - -bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale) -{ - open(name, false, true, scale); - bool ret = playVideo(x, y, stopOnKey); - close(); - return ret; -} - -CVideoPlayer::~CVideoPlayer() -{ - close(); -} - -#endif - +/* + * CVideoHandler.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 "CVideoHandler.h" + +#include "CMT.h" +#include "gui/CGuiHandler.h" +#include "eventsSDL/InputHandler.h" +#include "gui/FramerateManager.h" +#include "renderSDL/SDL_Extensions.h" +#include "CPlayerInterface.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CInputStream.h" + +#include + +#ifndef DISABLE_VIDEO + +extern "C" { +#include +#include +#include +#include +} + +#ifdef _MSC_VER +#pragma comment(lib, "avcodec.lib") +#pragma comment(lib, "avutil.lib") +#pragma comment(lib, "avformat.lib") +#pragma comment(lib, "swscale.lib") +#endif // _MSC_VER + +// Define a set of functions to read data +static int lodRead(void* opaque, uint8_t* buf, int size) +{ + auto video = reinterpret_cast(opaque); + + return static_cast(video->data->read(buf, size)); +} + +static si64 lodSeek(void * opaque, si64 pos, int whence) +{ + auto video = reinterpret_cast(opaque); + + if (whence & AVSEEK_SIZE) + return video->data->getSize(); + + return video->data->seek(pos); +} + +// Define a set of functions to read data +static int lodReadAudio(void* opaque, uint8_t* buf, int size) +{ + auto video = reinterpret_cast(opaque); + + return static_cast(video->dataAudio->read(buf, size)); +} + +static si64 lodSeekAudio(void * opaque, si64 pos, int whence) +{ + auto video = reinterpret_cast(opaque); + + if (whence & AVSEEK_SIZE) + return video->dataAudio->getSize(); + + return video->dataAudio->seek(pos); +} + +CVideoPlayer::CVideoPlayer() + : stream(-1) + , format (nullptr) + , codecContext(nullptr) + , codec(nullptr) + , frame(nullptr) + , sws(nullptr) + , context(nullptr) + , texture(nullptr) + , dest(nullptr) + , destRect(0,0,0,0) + , pos(0,0,0,0) + , frameTime(0) + , doLoop(false) +{} + +bool CVideoPlayer::open(const VideoPath & fname, bool scale) +{ + return open(fname, true, false); +} + +// loop = to loop through the video +// useOverlay = directly write to the screen. +bool CVideoPlayer::open(const VideoPath & videoToOpen, bool loop, bool useOverlay, bool scale) +{ + close(); + + doLoop = loop; + frameTime = 0; + + if (CResourceHandler::get()->existsResource(videoToOpen)) + fname = videoToOpen; + else + fname = videoToOpen.addPrefix("VIDEO/"); + + if (!CResourceHandler::get()->existsResource(fname)) + { + logGlobal->error("Error: video %s was not found", fname.getName()); + return false; + } + + data = CResourceHandler::get()->load(fname); + + static const int BUFFER_SIZE = 4096; + + unsigned char * buffer = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg + context = avio_alloc_context( buffer, BUFFER_SIZE, 0, (void *)this, lodRead, nullptr, lodSeek); + + format = avformat_alloc_context(); + format->pb = context; + // filename is not needed - file was already open and stored in this->data; + int avfopen = avformat_open_input(&format, "dummyFilename", nullptr, nullptr); + + if (avfopen != 0) + { + return false; + } + // Retrieve stream information + if (avformat_find_stream_info(format, nullptr) < 0) + return false; + + // Find the first video stream + stream = -1; + for(ui32 i=0; inb_streams; i++) + { + if (format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + { + stream = i; + break; + } + } + + if (stream < 0) + // No video stream in that file + return false; + + // Find the decoder for the video stream + codec = avcodec_find_decoder(format->streams[stream]->codecpar->codec_id); + + if (codec == nullptr) + { + // Unsupported codec + return false; + } + + codecContext = avcodec_alloc_context3(codec); + if(!codecContext) + return false; + // Get a pointer to the codec context for the video stream + int ret = avcodec_parameters_to_context(codecContext, format->streams[stream]->codecpar); + if (ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContext); + return false; + } + + // Open codec + if ( avcodec_open2(codecContext, codec, nullptr) < 0 ) + { + // Could not open codec + codec = nullptr; + return false; + } + // Allocate video frame + frame = av_frame_alloc(); + + //setup scaling + if(scale) + { + pos.w = screen->w; + pos.h = screen->h; + } + else + { + pos.w = codecContext->width; + pos.h = codecContext->height; + } + + // Allocate a place to put our YUV image on that screen + if (useOverlay) + { + texture = SDL_CreateTexture( mainRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STATIC, pos.w, pos.h); + } + else + { + dest = CSDL_Ext::newSurface(pos.w, pos.h); + destRect.x = destRect.y = 0; + destRect.w = pos.w; + destRect.h = pos.h; + } + + if (texture == nullptr && dest == nullptr) + return false; + + if (texture) + { // Convert the image into YUV format that SDL uses + sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, + pos.w, pos.h, + AV_PIX_FMT_YUV420P, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + else + { + AVPixelFormat screenFormat = AV_PIX_FMT_NONE; + if (screen->format->Bshift > screen->format->Rshift) + { + // this a BGR surface + switch (screen->format->BytesPerPixel) + { + case 2: screenFormat = AV_PIX_FMT_BGR565; break; + case 3: screenFormat = AV_PIX_FMT_BGR24; break; + case 4: screenFormat = AV_PIX_FMT_BGR32; break; + default: return false; + } + } + else + { + // this a RGB surface + switch (screen->format->BytesPerPixel) + { + case 2: screenFormat = AV_PIX_FMT_RGB565; break; + case 3: screenFormat = AV_PIX_FMT_RGB24; break; + case 4: screenFormat = AV_PIX_FMT_RGB32; break; + default: return false; + } + } + + sws = sws_getContext(codecContext->width, codecContext->height, codecContext->pix_fmt, + pos.w, pos.h, screenFormat, + SWS_BICUBIC, nullptr, nullptr, nullptr); + } + + if (sws == nullptr) + return false; + + return true; +} + +// Read the next frame. Return false on error/end of file. +bool CVideoPlayer::nextFrame() +{ + AVPacket packet; + int frameFinished = 0; + bool gotError = false; + + if (sws == nullptr) + return false; + + while(!frameFinished) + { + int ret = av_read_frame(format, &packet); + if (ret < 0) + { + // Error. It's probably an end of file. + if (doLoop && !gotError) + { + // Rewind + frameTime = 0; + if (av_seek_frame(format, stream, 0, AVSEEK_FLAG_BYTE) < 0) + break; + gotError = true; + } + else + { + break; + } + } + else + { + // Is this a packet from the video stream? + if (packet.stream_index == stream) + { + // Decode video frame + int rc = avcodec_send_packet(codecContext, &packet); + if (rc >=0) + packet.size = 0; + rc = avcodec_receive_frame(codecContext, frame); + if (rc >= 0) + frameFinished = 1; + // Did we get a video frame? + if (frameFinished) + { + uint8_t *data[4]; + int linesize[4]; + + if (texture) { + av_image_alloc(data, linesize, pos.w, pos.h, AV_PIX_FMT_YUV420P, 1); + + sws_scale(sws, frame->data, frame->linesize, + 0, codecContext->height, data, linesize); + + SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0], + data[1], linesize[1], + data[2], linesize[2]); + av_freep(&data[0]); + } + else + { + /* Avoid buffer overflow caused by sws_scale(): + * http://trac.ffmpeg.org/ticket/9254 + * Currently (ffmpeg-4.4 with SSE3 enabled) sws_scale() + * has a few requirements for target data buffers on rescaling: + * 1. buffer has to be aligned to be usable for SIMD instructions + * 2. buffer has to be padded to allow small overflow by SIMD instructions + * Unfortunately SDL_Surface does not provide these guarantees. + * This means that atempt to rescale directly into SDL surface causes + * memory corruption. Usually it happens on campaign selection screen + * where short video moves start spinning on mouse hover. + * + * To fix [1.] we use av_malloc() for memory allocation. + * To fix [2.] we add an `ffmpeg_pad` that provides plenty of space. + * We have to use intermdiate buffer and then use memcpy() to land it + * to SDL_Surface. + */ + size_t pic_bytes = dest->pitch * dest->h; + size_t ffmped_pad = 1024; /* a few bytes of overflow will go here */ + void * for_sws = av_malloc (pic_bytes + ffmped_pad); + data[0] = (ui8 *)for_sws; + linesize[0] = dest->pitch; + + sws_scale(sws, frame->data, frame->linesize, + 0, codecContext->height, data, linesize); + memcpy(dest->pixels, for_sws, pic_bytes); + av_free(for_sws); + } + } + } + + av_packet_unref(&packet); + } + } + + return frameFinished != 0; +} + +void CVideoPlayer::show( int x, int y, SDL_Surface *dst, bool update ) +{ + if (sws == nullptr) + return; + + pos.x = x; + pos.y = y; + CSDL_Ext::blitSurface(dest, destRect, dst, pos.topLeft()); + + if (update) + CSDL_Ext::updateRect(dst, pos); +} + +void CVideoPlayer::redraw( int x, int y, SDL_Surface *dst, bool update ) +{ + show(x, y, dst, update); +} + +void CVideoPlayer::update( int x, int y, SDL_Surface *dst, bool forceRedraw, bool update, std::function onVideoRestart) +{ + if (sws == nullptr) + return; + +#if (LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + double frameEndTime = (frame->pts + packet_duration) * av_q2d(format->streams[stream]->time_base); + frameTime += GH.framerate().getElapsedMilliseconds() / 1000.0; + + if (frameTime >= frameEndTime ) + { + if (nextFrame()) + show(x,y,dst,update); + else + { + if(onVideoRestart) + onVideoRestart(); + VideoPath filenameToReopen = fname; // create copy to backup this->fname + open(filenameToReopen); + nextFrame(); + + // The y position is wrong at the first frame. + // Note: either the windows player or the linux player is + // broken. Compensate here until the bug is found. + show(x, y--, dst, update); + } + } + else + { + redraw(x, y, dst, update); + } +} + +void CVideoPlayer::close() +{ + fname = VideoPath(); + + if (sws) + { + sws_freeContext(sws); + sws = nullptr; + } + + if (texture) + { + SDL_DestroyTexture(texture); + texture = nullptr; + } + + if (dest) + { + SDL_FreeSurface(dest); + dest = nullptr; + } + + if (frame) + { + av_frame_free(&frame);//will be set to null + } + + if (codec) + { + avcodec_close(codecContext); + codec = nullptr; + } + if (codecContext) + { + avcodec_free_context(&codecContext); + } + + if (format) + { + avformat_close_input(&format); + } + + if (context) + { + av_free(context); + context = nullptr; + } +} + +std::pair, si64> CVideoPlayer::getAudio(const VideoPath & videoToOpen) +{ + std::pair, si64> dat(std::make_pair(nullptr, 0)); + + VideoPath fnameAudio; + + if (CResourceHandler::get()->existsResource(videoToOpen)) + fnameAudio = videoToOpen; + else + fnameAudio = videoToOpen.addPrefix("VIDEO/"); + + if (!CResourceHandler::get()->existsResource(fnameAudio)) + { + logGlobal->error("Error: video %s was not found", fnameAudio.getName()); + return dat; + } + + dataAudio = CResourceHandler::get()->load(fnameAudio); + + static const int BUFFER_SIZE = 4096; + + unsigned char * bufferAudio = (unsigned char *)av_malloc(BUFFER_SIZE);// will be freed by ffmpeg + AVIOContext * contextAudio = avio_alloc_context( bufferAudio, BUFFER_SIZE, 0, (void *)this, lodReadAudio, nullptr, lodSeekAudio); + + AVFormatContext * formatAudio = avformat_alloc_context(); + formatAudio->pb = contextAudio; + // filename is not needed - file was already open and stored in this->data; + int avfopen = avformat_open_input(&formatAudio, "dummyFilename", nullptr, nullptr); + + if (avfopen != 0) + { + return dat; + } + // Retrieve stream information + if (avformat_find_stream_info(formatAudio, nullptr) < 0) + return dat; + + // Find the first audio stream + int streamAudio = -1; + for(ui32 i = 0; i < formatAudio->nb_streams; i++) + { + if (formatAudio->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) + { + streamAudio = i; + break; + } + } + + if(streamAudio < 0) + return dat; + + const AVCodec *codecAudio = avcodec_find_decoder(formatAudio->streams[streamAudio]->codecpar->codec_id); + + AVCodecContext *codecContextAudio; + if (codecAudio != nullptr) + codecContextAudio = avcodec_alloc_context3(codecAudio); + + // Get a pointer to the codec context for the audio stream + if (streamAudio > -1) + { + int ret = avcodec_parameters_to_context(codecContextAudio, formatAudio->streams[streamAudio]->codecpar); + if (ret < 0) + { + //We cannot get codec from parameters + avcodec_free_context(&codecContextAudio); + } + } + + // Open codec + AVFrame *frameAudio; + if (codecAudio != nullptr) + { + if ( avcodec_open2(codecContextAudio, codecAudio, nullptr) < 0 ) + { + // Could not open codec + codecAudio = nullptr; + } + // Allocate audio frame + frameAudio = av_frame_alloc(); + } + + AVPacket packet; + + std::vector samples; + + while (av_read_frame(formatAudio, &packet) >= 0) + { + if(packet.stream_index == streamAudio) + { + int rc = avcodec_send_packet(codecContextAudio, &packet); + if (rc >= 0) + packet.size = 0; + rc = avcodec_receive_frame(codecContextAudio, frameAudio); + int bytesToRead = (frameAudio->nb_samples * 2 * (formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample / 8)); + if (rc >= 0) + for (int s = 0; s < bytesToRead; s += sizeof(ui8)) + { + ui8 value; + memcpy(&value, &frameAudio->data[0][s], sizeof(ui8)); + samples.push_back(value); + } + } + + av_packet_unref(&packet); + } + + typedef struct WAV_HEADER { + ui8 RIFF[4] = {'R', 'I', 'F', 'F'}; + ui32 ChunkSize; + ui8 WAVE[4] = {'W', 'A', 'V', 'E'}; + ui8 fmt[4] = {'f', 'm', 't', ' '}; + ui32 Subchunk1Size = 16; + ui16 AudioFormat = 1; + ui16 NumOfChan = 2; + ui32 SamplesPerSec = 22050; + ui32 bytesPerSec = 22050 * 2; + ui16 blockAlign = 2; + ui16 bitsPerSample = 16; + ui8 Subchunk2ID[4] = {'d', 'a', 't', 'a'}; + ui32 Subchunk2Size; + } wav_hdr; + + wav_hdr wav; + wav.ChunkSize = samples.size() + sizeof(wav_hdr) - 8; + wav.Subchunk2Size = samples.size() + sizeof(wav_hdr) - 44; + wav.SamplesPerSec = formatAudio->streams[streamAudio]->codecpar->sample_rate; + wav.bitsPerSample = formatAudio->streams[streamAudio]->codecpar->bits_per_coded_sample; + auto wavPtr = reinterpret_cast(&wav); + + dat = std::make_pair(std::make_unique(samples.size() + sizeof(wav_hdr)), samples.size() + sizeof(wav_hdr)); + std::copy(wavPtr, wavPtr + sizeof(wav_hdr), dat.first.get()); + std::copy(samples.begin(), samples.end(), dat.first.get() + sizeof(wav_hdr)); + + if (frameAudio) + av_frame_free(&frameAudio); + + if (codecAudio) + { + avcodec_close(codecContextAudio); + codecAudio = nullptr; + } + if (codecContextAudio) + avcodec_free_context(&codecContextAudio); + + if (formatAudio) + avformat_close_input(&formatAudio); + + if (contextAudio) + { + av_free(contextAudio); + contextAudio = nullptr; + } + + return dat; +} + +// Plays a video. Only works for overlays. +bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) +{ + // Note: either the windows player or the linux player is + // broken. Compensate here until the bug is found. + y--; + + pos.x = x; + pos.y = y; + frameTime = 0.0; + + while(nextFrame()) + { + if(stopOnKey) + { + GH.input().fetchEvents(); + if(GH.input().ignoreEventsUntilInput()) + return false; + } + + SDL_Rect rect = CSDL_Ext::toSDL(pos); + + SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); + SDL_RenderPresent(mainRenderer); + +#if (LIBAVUTIL_VERSION_MAJOR < 58) + auto packet_duration = frame->pkt_duration; +#else + auto packet_duration = frame->duration; +#endif + double frameDurationSec = packet_duration * av_q2d(format->streams[stream]->time_base); + uint32_t timeToSleepMillisec = 1000 * (frameDurationSec); + + boost::this_thread::sleep_for(boost::chrono::milliseconds(timeToSleepMillisec)); + } + + return true; +} + +bool CVideoPlayer::openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey, bool scale) +{ + open(name, false, true, scale); + bool ret = playVideo(x, y, stopOnKey); + close(); + return ret; +} + +CVideoPlayer::~CVideoPlayer() +{ + close(); +} + +#endif + diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index 21fc21dfe..ca4d3b293 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -1,121 +1,121 @@ -/* - * CVideoHandler.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" -#include "../lib/filesystem/ResourcePath.h" - -struct SDL_Surface; -struct SDL_Texture; - -class IVideoPlayer : boost::noncopyable -{ -public: - virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes - virtual void close()=0; - virtual bool nextFrame()=0; - virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; - virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer - virtual bool wait()=0; - virtual int curFrame() const =0; - virtual int frameCount() const =0; -}; - -class IMainVideoPlayer : public IVideoPlayer -{ -public: - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0){} - virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) - { - return false; - } - virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; -}; - -class CEmptyVideoPlayer : public IMainVideoPlayer -{ -public: - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; - bool nextFrame() override {return false;}; - void close() override {}; - bool wait() override {return false;}; - bool open(const VideoPath & name, bool scale = false) override {return false;}; -}; - -#ifndef DISABLE_VIDEO - -struct AVFormatContext; -struct AVCodecContext; -struct AVCodec; -struct AVFrame; -struct AVIOContext; - -VCMI_LIB_NAMESPACE_BEGIN -class CInputStream; -VCMI_LIB_NAMESPACE_END - -class CVideoPlayer : public IMainVideoPlayer -{ - int stream; // stream index in video - AVFormatContext *format; - AVCodecContext *codecContext; // codec context for stream - const AVCodec *codec; - AVFrame *frame; - struct SwsContext *sws; - - AVIOContext * context; - - VideoPath fname; //name of current video file (empty if idle) - - // Destination. Either overlay or dest. - - SDL_Texture *texture; - SDL_Surface *dest; - Rect destRect; // valid when dest is used - Rect pos; // destination on screen - - /// video playback currnet progress, in seconds - double frameTime; - bool doLoop; // loop through video - - bool playVideo(int x, int y, bool stopOnKey); - bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); -public: - CVideoPlayer(); - ~CVideoPlayer(); - - bool init(); - bool open(const VideoPath & fname, bool scale = false) override; - void close() override; - bool nextFrame() override; // display next frame - - void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame - void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer - void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true - - // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) - bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; - - std::pair, si64> getAudio(const VideoPath & videoToOpen) override; - - //TODO: - bool wait() override {return false;}; - int curFrame() const override {return -1;}; - int frameCount() const override {return -1;}; - - // public to allow access from ffmpeg IO functions - std::unique_ptr data; - std::unique_ptr dataAudio; -}; - -#endif +/* + * CVideoHandler.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" +#include "../lib/filesystem/ResourcePath.h" + +struct SDL_Surface; +struct SDL_Texture; + +class IVideoPlayer : boost::noncopyable +{ +public: + virtual bool open(const VideoPath & name, bool scale = false)=0; //true - succes + virtual void close()=0; + virtual bool nextFrame()=0; + virtual void show(int x, int y, SDL_Surface *dst, bool update = true)=0; + virtual void redraw(int x, int y, SDL_Surface *dst, bool update = true)=0; //reblits buffer + virtual bool wait()=0; + virtual int curFrame() const =0; + virtual int frameCount() const =0; +}; + +class IMainVideoPlayer : public IVideoPlayer +{ +public: + virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0){} + virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) + { + return false; + } + virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; +}; + +class CEmptyVideoPlayer : public IMainVideoPlayer +{ +public: + int curFrame() const override {return -1;}; + int frameCount() const override {return -1;}; + void redraw( int x, int y, SDL_Surface *dst, bool update = true ) override {}; + void show( int x, int y, SDL_Surface *dst, bool update = true ) override {}; + bool nextFrame() override {return false;}; + void close() override {}; + bool wait() override {return false;}; + bool open(const VideoPath & name, bool scale = false) override {return false;}; +}; + +#ifndef DISABLE_VIDEO + +struct AVFormatContext; +struct AVCodecContext; +struct AVCodec; +struct AVFrame; +struct AVIOContext; + +VCMI_LIB_NAMESPACE_BEGIN +class CInputStream; +VCMI_LIB_NAMESPACE_END + +class CVideoPlayer : public IMainVideoPlayer +{ + int stream; // stream index in video + AVFormatContext *format; + AVCodecContext *codecContext; // codec context for stream + const AVCodec *codec; + AVFrame *frame; + struct SwsContext *sws; + + AVIOContext * context; + + VideoPath fname; //name of current video file (empty if idle) + + // Destination. Either overlay or dest. + + SDL_Texture *texture; + SDL_Surface *dest; + Rect destRect; // valid when dest is used + Rect pos; // destination on screen + + /// video playback currnet progress, in seconds + double frameTime; + bool doLoop; // loop through video + + bool playVideo(int x, int y, bool stopOnKey); + bool open(const VideoPath & fname, bool loop, bool useOverlay = false, bool scale = false); +public: + CVideoPlayer(); + ~CVideoPlayer(); + + bool init(); + bool open(const VideoPath & fname, bool scale = false) override; + void close() override; + bool nextFrame() override; // display next frame + + void show(int x, int y, SDL_Surface *dst, bool update = true) override; //blit current frame + void redraw(int x, int y, SDL_Surface *dst, bool update = true) override; //reblits buffer + void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function onVideoRestart = nullptr) override; //moves to next frame if appropriate, and blits it or blits only if redraw parameter is set true + + // Opens video, calls playVideo, closes video; returns playVideo result (if whole video has been played) + bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) override; + + std::pair, si64> getAudio(const VideoPath & videoToOpen) override; + + //TODO: + bool wait() override {return false;}; + int curFrame() const override {return -1;}; + int frameCount() const override {return -1;}; + + // public to allow access from ffmpeg IO functions + std::unique_ptr data; + std::unique_ptr dataAudio; +}; + +#endif diff --git a/client/Client.cpp b/client/Client.cpp index cfc036fed..d01828b97 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -1,740 +1,740 @@ -/* - * Client.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 "Global.h" -#include "StdInc.h" -#include "Client.h" - -#include "CGameInfo.h" -#include "CPlayerInterface.h" -#include "CServerHandler.h" -#include "ClientNetPackVisitors.h" -#include "adventureMap/AdventureMapInterface.h" -#include "battle/BattleInterface.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "mapView/mapHandler.h" - -#include "../CCallback.h" -#include "../lib/CConfigHandler.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CThreadHelper.h" -#include "../lib/VCMIDirs.h" -#include "../lib/UnlockGuard.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/serializer/BinaryDeserializer.h" -#include "../lib/mapping/CMapService.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" -#include "../lib/serializer/Connection.h" - -#include -#include - -#if SCRIPTING_ENABLED -#include "../lib/ScriptHandler.h" -#endif - -#ifdef VCMI_ANDROID -#include "lib/CAndroidVMHelper.h" - -#ifndef SINGLE_PROCESS_APP -std::atomic_bool androidTestServerReadyFlag; -#endif -#endif - -ThreadSafeVector CClient::waitingRequest; - -template class CApplyOnCL; - -class CBaseForCLApply -{ -public: - virtual void applyOnClAfter(CClient * cl, void * pack) const =0; - virtual void applyOnClBefore(CClient * cl, void * pack) const =0; - virtual ~CBaseForCLApply(){} - - template static CBaseForCLApply * getApplier(const U * t = nullptr) - { - return new CApplyOnCL(); - } -}; - -template class CApplyOnCL : public CBaseForCLApply -{ -public: - void applyOnClAfter(CClient * cl, void * pack) const override - { - T * ptr = static_cast(pack); - ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); - ptr->visit(visitor); - } - void applyOnClBefore(CClient * cl, void * pack) const override - { - T * ptr = static_cast(pack); - ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState()); - ptr->visit(visitor); - } -}; - -template<> class CApplyOnCL: public CBaseForCLApply -{ -public: - void applyOnClAfter(CClient * cl, void * pack) const override - { - logGlobal->error("Cannot apply on CL plain CPack!"); - assert(0); - } - void applyOnClBefore(CClient * cl, void * pack) const override - { - logGlobal->error("Cannot apply on CL plain CPack!"); - assert(0); - } -}; - -CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) - : player(player_), - cl(cl_), - mainCallback(mainCallback_) -{ - -} - -const Services * CPlayerEnvironment::services() const -{ - return VLC; -} - -vstd::CLoggerBase * CPlayerEnvironment::logger() const -{ - return logGlobal; -} - -events::EventBus * CPlayerEnvironment::eventBus() const -{ - return cl->eventBus();//always get actual value -} - -const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const -{ - return mainCallback->getBattle(battleID).get(); -} - -const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const -{ - return mainCallback.get(); -} - - -CClient::CClient() -{ - waitingRequest.clear(); - applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); - IObjectInterface::cb = this; - gs = nullptr; -} - -CClient::~CClient() -{ - IObjectInterface::cb = nullptr; -} - -const Services * CClient::services() const -{ - return VLC; //todo: this should be CGI -} - -const CClient::BattleCb * CClient::battle(const BattleID & battleID) const -{ - return nullptr; //todo? -} - -const CClient::GameCb * CClient::game() const -{ - return this; -} - -vstd::CLoggerBase * CClient::logger() const -{ - return logGlobal; -} - -events::EventBus * CClient::eventBus() const -{ - return clientEventBus.get(); -} - -void CClient::newGame(CGameState * initializedGameState) -{ - CSH->th->update(); - CMapService mapService; - gs = initializedGameState ? initializedGameState : new CGameState(); - gs->preInit(VLC); - logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); - if(!initializedGameState) - { - Load::ProgressAccumulator progressTracking; - gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); - } - logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); - - initMapHandler(); - reinitScripting(); - initPlayerEnvironments(); - initPlayerInterfaces(); -} - -void CClient::loadGame(CGameState * initializedGameState) -{ - logNetwork->info("Loading procedure started!"); - - logNetwork->info("Game state was transferred over network, loading."); - gs = initializedGameState; - - gs->preInit(VLC); - gs->updateOnLoad(CSH->si.get()); - logNetwork->info("Game loaded, initialize interfaces."); - - initMapHandler(); - - reinitScripting(); - - initPlayerEnvironments(); - - // Loading of client state - disabled for now - // Since client no longer writes or loads its own state and instead receives it from server - // client state serializer will serialize its own copies of all pointers, e.g. heroes/towns/objects - // and on deserialize will create its own copies (instead of using copies from state received from server) - // Potential solutions: - // 1) Use server gamestate to deserialize pointers, so any pointer to same object will point to server instance and not our copy - // 2) Remove all serialization of pointers with instance ID's and restore them on load (including AI deserializer code) - // 3) Completely remove client savegame and send all information, like hero paths and sleeping status to server (either in form of hero properties or as some generic "client options" message -#ifdef BROKEN_CLIENT_STATE_SERIALIZATION_HAS_BEEN_FIXED - // try to deserialize client data including sleepingHeroes - try - { - boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); - - if(clientSaveName.empty()) - throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); - - std::unique_ptr loader (new CLoadFile(clientSaveName)); - serialize(loader->serializer, loader->serializer.fileVersion); - - logNetwork->info("Client data loaded."); - } - catch(std::exception & e) - { - logGlobal->info("Cannot load client data for game %s. Error: %s", CSH->si->mapname, e.what()); - } -#endif - - initPlayerInterfaces(); -} - -void CClient::serialize(BinarySerializer & h, const int version) -{ - assert(h.saving); - ui8 players = static_cast(playerint.size()); - h & players; - - for(auto i = playerint.begin(); i != playerint.end(); i++) - { - logGlobal->trace("Saving player %s interface", i->first); - assert(i->first == i->second->playerID); - h & i->first; - h & i->second->dllName; - h & i->second->human; - i->second->saveGame(h, version); - } - -#if SCRIPTING_ENABLED - if(version >= 800) - { - JsonNode scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - h & scriptsState; - } -#endif -} - -void CClient::serialize(BinaryDeserializer & h, const int version) -{ - assert(!h.saving); - ui8 players = 0; - h & players; - - for(int i = 0; i < players; i++) - { - std::string dllname; - PlayerColor pid; - bool isHuman = false; - auto prevInt = LOCPLINT; - - h & pid; - h & dllname; - h & isHuman; - assert(dllname.length() == 0 || !isHuman); - if(pid == PlayerColor::NEUTRAL) - { - logGlobal->trace("Neutral battle interfaces are not serialized."); - continue; - } - - logGlobal->trace("Loading player %s interface", pid); - std::shared_ptr nInt; - if(dllname.length()) - nInt = CDynLibHandler::getNewAI(dllname); - else - nInt = std::make_shared(pid); - - nInt->dllName = dllname; - nInt->human = isHuman; - nInt->playerID = pid; - - bool shouldResetInterface = true; - // Client no longer handle this player at all - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) - { - logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); - } - else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid)) - { - logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid); - } - else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid)) - { - logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid); - } - else - { - installNewPlayerInterface(nInt, pid); - shouldResetInterface = false; - } - - // loadGame needs to be called after initGameInterface to load paths correctly - // initGameInterface is called in installNewPlayerInterface - nInt->loadGame(h, version); - - if (shouldResetInterface) - { - nInt.reset(); - LOCPLINT = prevInt; - } - } - -#if SCRIPTING_ENABLED - { - JsonNode scriptsState; - h & scriptsState; - clientScripts->serializeState(h.saving, scriptsState); - } -#endif - - logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff()); -} - -void CClient::save(const std::string & fname) -{ - if(!gs->currentBattles.empty()) - { - logNetwork->error("Game cannot be saved during battle!"); - return; - } - - SaveGame save_game(fname); - sendRequest(&save_game, PlayerColor::NEUTRAL); -} - -void CClient::endGame() -{ -#if SCRIPTING_ENABLED - clientScripts.reset(); -#endif - - //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) - for(auto & i : playerint) - i.second->finish(); - - GH.curInt = nullptr; - { - logNetwork->info("Ending current game!"); - removeGUI(); - - vstd::clear_pointer(const_cast(CGI)->mh); - vstd::clear_pointer(gs); - - logNetwork->info("Deleted mapHandler and gameState."); - } - - CPlayerInterface::battleInt.reset(); - playerint.clear(); - battleints.clear(); - battleCallbacks.clear(); - playerEnvironments.clear(); - logNetwork->info("Deleted playerInts."); - logNetwork->info("Client stopped."); -} - -void CClient::initMapHandler() -{ - // TODO: CMapHandler initialization can probably go somewhere else - // It's can't be before initialization of interfaces - // During loading CPlayerInterface from serialized state it's depend on MH - if(!settings["session"]["headless"].Bool()) - { - const_cast(CGI)->mh = new CMapHandler(gs->map); - logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); - } - - pathCache.clear(); -} - -void CClient::initPlayerEnvironments() -{ - playerEnvironments.clear(); - - auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); - bool hasHumanPlayer = false; - for(auto & color : allPlayers) - { - logNetwork->info("Preparing environment for player %s", color.toString()); - playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); - - if(!hasHumanPlayer && gs->players[color].isHuman()) - hasHumanPlayer = true; - } - - if(!hasHumanPlayer) - { - Settings session = settings.write["session"]; - session["spectate"].Bool() = true; - session["spectate-skip-battle-result"].Bool() = true; - session["spectate-ignore-hero"].Bool() = true; - } - - if(settings["session"]["spectate"].Bool()) - { - playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); - } -} - -void CClient::initPlayerInterfaces() -{ - for(auto & playerInfo : gs->scenarioOps->playerInfos) - { - PlayerColor color = playerInfo.first; - if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) - continue; - - if(!vstd::contains(playerint, color)) - { - logNetwork->info("Preparing interface for player %s", color.toString()); - if(playerInfo.second.isControlledByAI()) - { - bool alliedToHuman = false; - for(auto & allyInfo : gs->scenarioOps->playerInfos) - if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) - alliedToHuman = true; - - auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); - logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); - installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); - } - else - { - logNetwork->info("Player %s will be lead by human", color.toString()); - installNewPlayerInterface(std::make_shared(color), color); - } - } - } - - if(settings["session"]["spectate"].Bool()) - { - installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); - } - - if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) - installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); - - logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); -} - -std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) -{ - if(ps.name.size()) - { - const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); - if(boost::filesystem::exists(aiPath)) - return ps.name; - } - - return aiNameForPlayer(battleAI, alliedToHuman); -} - -std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) -{ - const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; - std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); - std::string goodBattleAI = settings["server"]["neutralAI"].String(); - std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; - std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; - - //TODO what about human players - if(battleints.size() >= sensibleAILimit) - return badAI; - - return goodAI; -} - -void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) -{ - playerint[color] = gameInterface; - - logGlobal->trace("\tInitializing the interface for player %s", color.toString()); - auto cb = std::make_shared(gs, color, this); - battleCallbacks[color] = cb; - gameInterface->initGameInterface(playerEnvironments.at(color), cb); - - installNewBattleInterface(gameInterface, color, battlecb); -} - -void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) -{ - battleints[color] = battleInterface; - - if(needCallback) - { - logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); - auto cbc = std::make_shared(color, this); - battleCallbacks[color] = cbc; - battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); - } -} - -void CClient::handlePack(CPack * pack) -{ - CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier - if(apply) - { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - apply->applyOnClBefore(this, pack); - logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); - gs->apply(pack); - logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); - apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); - } - else - { - logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); - } - delete pack; -} - -int CClient::sendRequest(const CPackForServer * request, PlayerColor player) -{ - static ui32 requestCounter = 0; - - ui32 requestID = requestCounter++; - logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); - - waitingRequest.pushBack(requestID); - request->requestID = requestID; - request->player = player; - CSH->c->sendPack(request); - if(vstd::contains(playerint, player)) - playerint[player]->requestSent(request, requestID); - - return requestID; -} - -void CClient::battleStarted(const BattleInfo * info) -{ - for(auto & battleCb : battleCallbacks) - { - if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) - || !battleCb.first.isValidPlayer()) - { - battleCb.second->onBattleStarted(info); - } - } - - std::shared_ptr att, def; - auto & leftSide = info->sides[0], & rightSide = info->sides[1]; - - //If quick combat is not, do not prepare interfaces for battleint - auto callBattleStart = [&](PlayerColor color, ui8 side) - { - if(vstd::contains(battleints, color)) - battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); - }; - - callBattleStart(leftSide.color, 0); - callBattleStart(rightSide.color, 1); - callBattleStart(PlayerColor::UNFLAGGABLE, 1); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - callBattleStart(PlayerColor::SPECTATOR, 1); - - if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) - att = std::dynamic_pointer_cast(playerint[leftSide.color]); - - if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) - def = std::dynamic_pointer_cast(playerint[rightSide.color]); - - //Remove player interfaces for auto battle (quickCombat option) - if(att && att->isAutoFightOn) - { - if (att->cb->getBattle(info->battleID)->battleGetTacticDist()) - { - auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); - att->cb->battleMakeTacticAction(info->battleID, action); - } - - att.reset(); - def.reset(); - } - - if(!settings["session"]["headless"].Bool()) - { - if(att || def) - { - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); - } - else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - { - //TODO: This certainly need improvement - auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); - spectratorInt->cb->onBattleStarted(info); - CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); - } - } - - if(info->tacticDistance) - { - auto tacticianColor = info->sides[info->tacticsSide].color; - - if (vstd::contains(battleints, tacticianColor)) - battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); - } -} - -void CClient::battleFinished(const BattleID & battleID) -{ - for(auto & side : gs->getBattle(battleID)->sides) - if(battleCallbacks.count(side.color)) - battleCallbacks[side.color]->onBattleEnded(battleID); - - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) - battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); -} - -void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) -{ - auto battleint = battleints.at(color); - - if (!battleint->human) - { - // we want to avoid locking gamestate and causing UI to freeze while AI is making turn - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } - else - { - battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); - } -} - -void CClient::invalidatePaths() -{ - boost::unique_lock pathLock(pathCacheMutex); - pathCache.clear(); -} - -std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) -{ - assert(h); - boost::unique_lock pathLock(pathCacheMutex); - - auto iter = pathCache.find(h); - - if(iter == std::end(pathCache)) - { - std::shared_ptr paths = std::make_shared(getMapSize(), h); - - gs->calculatePaths(h, *paths.get()); - - pathCache[h] = paths; - return paths; - } - else - { - return iter->second; - } -} - -#if SCRIPTING_ENABLED -scripting::Pool * CClient::getGlobalContextPool() const -{ - return clientScripts.get(); -} -#endif - -void CClient::reinitScripting() -{ - clientEventBus = std::make_unique(); -#if SCRIPTING_ENABLED - clientScripts.reset(new scripting::PoolImpl(this)); -#endif -} - -void CClient::removeGUI() -{ - // CClient::endGame - GH.curInt = nullptr; - GH.windows().clear(); - adventureInt.reset(); - logGlobal->info("Removed GUI."); - - LOCPLINT = nullptr; -} - -#ifdef VCMI_ANDROID -#ifndef SINGLE_PROCESS_APP -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server closed signal"); - if (CSH) { - CSH->campaignServerRestartLock.setn(false); - } -} - -extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls) -{ - logNetwork->info("Received server ready signal"); - androidTestServerReadyFlag.store(true); -} -#endif - -extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) -{ - logGlobal->info("Received emergency save game request"); - if(!LOCPLINT || !LOCPLINT->cb) - { - return false; - } - - LOCPLINT->cb->save("Saves/_Android_Autosave"); - return true; -} -#endif +/* + * Client.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 "Global.h" +#include "StdInc.h" +#include "Client.h" + +#include "CGameInfo.h" +#include "CPlayerInterface.h" +#include "CServerHandler.h" +#include "ClientNetPackVisitors.h" +#include "adventureMap/AdventureMapInterface.h" +#include "battle/BattleInterface.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "mapView/mapHandler.h" + +#include "../CCallback.h" +#include "../lib/CConfigHandler.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CThreadHelper.h" +#include "../lib/VCMIDirs.h" +#include "../lib/UnlockGuard.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/serializer/BinaryDeserializer.h" +#include "../lib/mapping/CMapService.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/serializer/Connection.h" + +#include +#include + +#if SCRIPTING_ENABLED +#include "../lib/ScriptHandler.h" +#endif + +#ifdef VCMI_ANDROID +#include "lib/CAndroidVMHelper.h" + +#ifndef SINGLE_PROCESS_APP +std::atomic_bool androidTestServerReadyFlag; +#endif +#endif + +ThreadSafeVector CClient::waitingRequest; + +template class CApplyOnCL; + +class CBaseForCLApply +{ +public: + virtual void applyOnClAfter(CClient * cl, void * pack) const =0; + virtual void applyOnClBefore(CClient * cl, void * pack) const =0; + virtual ~CBaseForCLApply(){} + + template static CBaseForCLApply * getApplier(const U * t = nullptr) + { + return new CApplyOnCL(); + } +}; + +template class CApplyOnCL : public CBaseForCLApply +{ +public: + void applyOnClAfter(CClient * cl, void * pack) const override + { + T * ptr = static_cast(pack); + ApplyClientNetPackVisitor visitor(*cl, *cl->gameState()); + ptr->visit(visitor); + } + void applyOnClBefore(CClient * cl, void * pack) const override + { + T * ptr = static_cast(pack); + ApplyFirstClientNetPackVisitor visitor(*cl, *cl->gameState()); + ptr->visit(visitor); + } +}; + +template<> class CApplyOnCL: public CBaseForCLApply +{ +public: + void applyOnClAfter(CClient * cl, void * pack) const override + { + logGlobal->error("Cannot apply on CL plain CPack!"); + assert(0); + } + void applyOnClBefore(CClient * cl, void * pack) const override + { + logGlobal->error("Cannot apply on CL plain CPack!"); + assert(0); + } +}; + +CPlayerEnvironment::CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_) + : player(player_), + cl(cl_), + mainCallback(mainCallback_) +{ + +} + +const Services * CPlayerEnvironment::services() const +{ + return VLC; +} + +vstd::CLoggerBase * CPlayerEnvironment::logger() const +{ + return logGlobal; +} + +events::EventBus * CPlayerEnvironment::eventBus() const +{ + return cl->eventBus();//always get actual value +} + +const CPlayerEnvironment::BattleCb * CPlayerEnvironment::battle(const BattleID & battleID) const +{ + return mainCallback->getBattle(battleID).get(); +} + +const CPlayerEnvironment::GameCb * CPlayerEnvironment::game() const +{ + return mainCallback.get(); +} + + +CClient::CClient() +{ + waitingRequest.clear(); + applier = std::make_shared>(); + registerTypesClientPacks1(*applier); + registerTypesClientPacks2(*applier); + IObjectInterface::cb = this; + gs = nullptr; +} + +CClient::~CClient() +{ + IObjectInterface::cb = nullptr; +} + +const Services * CClient::services() const +{ + return VLC; //todo: this should be CGI +} + +const CClient::BattleCb * CClient::battle(const BattleID & battleID) const +{ + return nullptr; //todo? +} + +const CClient::GameCb * CClient::game() const +{ + return this; +} + +vstd::CLoggerBase * CClient::logger() const +{ + return logGlobal; +} + +events::EventBus * CClient::eventBus() const +{ + return clientEventBus.get(); +} + +void CClient::newGame(CGameState * initializedGameState) +{ + CSH->th->update(); + CMapService mapService; + gs = initializedGameState ? initializedGameState : new CGameState(); + gs->preInit(VLC); + logNetwork->trace("\tCreating gamestate: %i", CSH->th->getDiff()); + if(!initializedGameState) + { + Load::ProgressAccumulator progressTracking; + gs->init(&mapService, CSH->si.get(), progressTracking, settings["general"]["saveRandomMaps"].Bool()); + } + logNetwork->trace("Initializing GameState (together): %d ms", CSH->th->getDiff()); + + initMapHandler(); + reinitScripting(); + initPlayerEnvironments(); + initPlayerInterfaces(); +} + +void CClient::loadGame(CGameState * initializedGameState) +{ + logNetwork->info("Loading procedure started!"); + + logNetwork->info("Game state was transferred over network, loading."); + gs = initializedGameState; + + gs->preInit(VLC); + gs->updateOnLoad(CSH->si.get()); + logNetwork->info("Game loaded, initialize interfaces."); + + initMapHandler(); + + reinitScripting(); + + initPlayerEnvironments(); + + // Loading of client state - disabled for now + // Since client no longer writes or loads its own state and instead receives it from server + // client state serializer will serialize its own copies of all pointers, e.g. heroes/towns/objects + // and on deserialize will create its own copies (instead of using copies from state received from server) + // Potential solutions: + // 1) Use server gamestate to deserialize pointers, so any pointer to same object will point to server instance and not our copy + // 2) Remove all serialization of pointers with instance ID's and restore them on load (including AI deserializer code) + // 3) Completely remove client savegame and send all information, like hero paths and sleeping status to server (either in form of hero properties or as some generic "client options" message +#ifdef BROKEN_CLIENT_STATE_SERIALIZATION_HAS_BEEN_FIXED + // try to deserialize client data including sleepingHeroes + try + { + boost::filesystem::path clientSaveName = *CResourceHandler::get()->getResourceName(ResourcePath(CSH->si->mapname, EResType::CLIENT_SAVEGAME)); + + if(clientSaveName.empty()) + throw std::runtime_error("Cannot open client part of " + CSH->si->mapname); + + std::unique_ptr loader (new CLoadFile(clientSaveName)); + serialize(loader->serializer, loader->serializer.fileVersion); + + logNetwork->info("Client data loaded."); + } + catch(std::exception & e) + { + logGlobal->info("Cannot load client data for game %s. Error: %s", CSH->si->mapname, e.what()); + } +#endif + + initPlayerInterfaces(); +} + +void CClient::serialize(BinarySerializer & h, const int version) +{ + assert(h.saving); + ui8 players = static_cast(playerint.size()); + h & players; + + for(auto i = playerint.begin(); i != playerint.end(); i++) + { + logGlobal->trace("Saving player %s interface", i->first); + assert(i->first == i->second->playerID); + h & i->first; + h & i->second->dllName; + h & i->second->human; + i->second->saveGame(h, version); + } + +#if SCRIPTING_ENABLED + if(version >= 800) + { + JsonNode scriptsState; + clientScripts->serializeState(h.saving, scriptsState); + h & scriptsState; + } +#endif +} + +void CClient::serialize(BinaryDeserializer & h, const int version) +{ + assert(!h.saving); + ui8 players = 0; + h & players; + + for(int i = 0; i < players; i++) + { + std::string dllname; + PlayerColor pid; + bool isHuman = false; + auto prevInt = LOCPLINT; + + h & pid; + h & dllname; + h & isHuman; + assert(dllname.length() == 0 || !isHuman); + if(pid == PlayerColor::NEUTRAL) + { + logGlobal->trace("Neutral battle interfaces are not serialized."); + continue; + } + + logGlobal->trace("Loading player %s interface", pid); + std::shared_ptr nInt; + if(dllname.length()) + nInt = CDynLibHandler::getNewAI(dllname); + else + nInt = std::make_shared(pid); + + nInt->dllName = dllname; + nInt->human = isHuman; + nInt->playerID = pid; + + bool shouldResetInterface = true; + // Client no longer handle this player at all + if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), pid)) + { + logGlobal->trace("Player %s is not belong to this client. Destroying interface", pid); + } + else if(isHuman && !vstd::contains(CSH->getHumanColors(), pid)) + { + logGlobal->trace("Player %s is no longer controlled by human. Destroying interface", pid); + } + else if(!isHuman && vstd::contains(CSH->getHumanColors(), pid)) + { + logGlobal->trace("Player %s is no longer controlled by AI. Destroying interface", pid); + } + else + { + installNewPlayerInterface(nInt, pid); + shouldResetInterface = false; + } + + // loadGame needs to be called after initGameInterface to load paths correctly + // initGameInterface is called in installNewPlayerInterface + nInt->loadGame(h, version); + + if (shouldResetInterface) + { + nInt.reset(); + LOCPLINT = prevInt; + } + } + +#if SCRIPTING_ENABLED + { + JsonNode scriptsState; + h & scriptsState; + clientScripts->serializeState(h.saving, scriptsState); + } +#endif + + logNetwork->trace("Loaded client part of save %d ms", CSH->th->getDiff()); +} + +void CClient::save(const std::string & fname) +{ + if(!gs->currentBattles.empty()) + { + logNetwork->error("Game cannot be saved during battle!"); + return; + } + + SaveGame save_game(fname); + sendRequest(&save_game, PlayerColor::NEUTRAL); +} + +void CClient::endGame() +{ +#if SCRIPTING_ENABLED + clientScripts.reset(); +#endif + + //suggest interfaces to finish their stuff (AI should interrupt any bg working threads) + for(auto & i : playerint) + i.second->finish(); + + GH.curInt = nullptr; + { + logNetwork->info("Ending current game!"); + removeGUI(); + + vstd::clear_pointer(const_cast(CGI)->mh); + vstd::clear_pointer(gs); + + logNetwork->info("Deleted mapHandler and gameState."); + } + + CPlayerInterface::battleInt.reset(); + playerint.clear(); + battleints.clear(); + battleCallbacks.clear(); + playerEnvironments.clear(); + logNetwork->info("Deleted playerInts."); + logNetwork->info("Client stopped."); +} + +void CClient::initMapHandler() +{ + // TODO: CMapHandler initialization can probably go somewhere else + // It's can't be before initialization of interfaces + // During loading CPlayerInterface from serialized state it's depend on MH + if(!settings["session"]["headless"].Bool()) + { + const_cast(CGI)->mh = new CMapHandler(gs->map); + logNetwork->trace("Creating mapHandler: %d ms", CSH->th->getDiff()); + } + + pathCache.clear(); +} + +void CClient::initPlayerEnvironments() +{ + playerEnvironments.clear(); + + auto allPlayers = CSH->getAllClientPlayers(CSH->c->connectionID); + bool hasHumanPlayer = false; + for(auto & color : allPlayers) + { + logNetwork->info("Preparing environment for player %s", color.toString()); + playerEnvironments[color] = std::make_shared(color, this, std::make_shared(gs, color, this)); + + if(!hasHumanPlayer && gs->players[color].isHuman()) + hasHumanPlayer = true; + } + + if(!hasHumanPlayer) + { + Settings session = settings.write["session"]; + session["spectate"].Bool() = true; + session["spectate-skip-battle-result"].Bool() = true; + session["spectate-ignore-hero"].Bool() = true; + } + + if(settings["session"]["spectate"].Bool()) + { + playerEnvironments[PlayerColor::SPECTATOR] = std::make_shared(PlayerColor::SPECTATOR, this, std::make_shared(gs, std::nullopt, this)); + } +} + +void CClient::initPlayerInterfaces() +{ + for(auto & playerInfo : gs->scenarioOps->playerInfos) + { + PlayerColor color = playerInfo.first; + if(!vstd::contains(CSH->getAllClientPlayers(CSH->c->connectionID), color)) + continue; + + if(!vstd::contains(playerint, color)) + { + logNetwork->info("Preparing interface for player %s", color.toString()); + if(playerInfo.second.isControlledByAI()) + { + bool alliedToHuman = false; + for(auto & allyInfo : gs->scenarioOps->playerInfos) + if (gs->getPlayerTeam(allyInfo.first) == gs->getPlayerTeam(playerInfo.first) && allyInfo.second.isControlledByHuman()) + alliedToHuman = true; + + auto AiToGive = aiNameForPlayer(playerInfo.second, false, alliedToHuman); + logNetwork->info("Player %s will be lead by %s", color.toString(), AiToGive); + installNewPlayerInterface(CDynLibHandler::getNewAI(AiToGive), color); + } + else + { + logNetwork->info("Player %s will be lead by human", color.toString()); + installNewPlayerInterface(std::make_shared(color), color); + } + } + } + + if(settings["session"]["spectate"].Bool()) + { + installNewPlayerInterface(std::make_shared(PlayerColor::SPECTATOR), PlayerColor::SPECTATOR, true); + } + + if(CSH->getAllClientPlayers(CSH->c->connectionID).count(PlayerColor::NEUTRAL)) + installNewBattleInterface(CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String()), PlayerColor::NEUTRAL); + + logNetwork->trace("Initialized player interfaces %d ms", CSH->th->getDiff()); +} + +std::string CClient::aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman) +{ + if(ps.name.size()) + { + const boost::filesystem::path aiPath = VCMIDirs::get().fullLibraryPath("AI", ps.name); + if(boost::filesystem::exists(aiPath)) + return ps.name; + } + + return aiNameForPlayer(battleAI, alliedToHuman); +} + +std::string CClient::aiNameForPlayer(bool battleAI, bool alliedToHuman) +{ + const int sensibleAILimit = settings["session"]["oneGoodAI"].Bool() ? 1 : PlayerColor::PLAYER_LIMIT_I; + std::string goodAdventureAI = alliedToHuman ? settings["server"]["alliedAI"].String() : settings["server"]["playerAI"].String(); + std::string goodBattleAI = settings["server"]["neutralAI"].String(); + std::string goodAI = battleAI ? goodBattleAI : goodAdventureAI; + std::string badAI = battleAI ? "StupidAI" : "EmptyAI"; + + //TODO what about human players + if(battleints.size() >= sensibleAILimit) + return badAI; + + return goodAI; +} + +void CClient::installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb) +{ + playerint[color] = gameInterface; + + logGlobal->trace("\tInitializing the interface for player %s", color.toString()); + auto cb = std::make_shared(gs, color, this); + battleCallbacks[color] = cb; + gameInterface->initGameInterface(playerEnvironments.at(color), cb); + + installNewBattleInterface(gameInterface, color, battlecb); +} + +void CClient::installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback) +{ + battleints[color] = battleInterface; + + if(needCallback) + { + logGlobal->trace("\tInitializing the battle interface for player %s", color.toString()); + auto cbc = std::make_shared(color, this); + battleCallbacks[color] = cbc; + battleInterface->initBattleInterface(playerEnvironments.at(color), cbc); + } +} + +void CClient::handlePack(CPack * pack) +{ + CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + if(apply) + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + apply->applyOnClBefore(this, pack); + logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); + gs->apply(pack); + logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); + apply->applyOnClAfter(this, pack); + logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); + } + else + { + logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); + } + delete pack; +} + +int CClient::sendRequest(const CPackForServer * request, PlayerColor player) +{ + static ui32 requestCounter = 0; + + ui32 requestID = requestCounter++; + logNetwork->trace("Sending a request \"%s\". It'll have an ID=%d.", typeid(*request).name(), requestID); + + waitingRequest.pushBack(requestID); + request->requestID = requestID; + request->player = player; + CSH->c->sendPack(request); + if(vstd::contains(playerint, player)) + playerint[player]->requestSent(request, requestID); + + return requestID; +} + +void CClient::battleStarted(const BattleInfo * info) +{ + for(auto & battleCb : battleCallbacks) + { + if(vstd::contains_if(info->sides, [&](const SideInBattle& side) {return side.color == battleCb.first; }) + || !battleCb.first.isValidPlayer()) + { + battleCb.second->onBattleStarted(info); + } + } + + std::shared_ptr att, def; + auto & leftSide = info->sides[0], & rightSide = info->sides[1]; + + //If quick combat is not, do not prepare interfaces for battleint + auto callBattleStart = [&](PlayerColor color, ui8 side) + { + if(vstd::contains(battleints, color)) + battleints[color]->battleStart(info->battleID, leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed); + }; + + callBattleStart(leftSide.color, 0); + callBattleStart(rightSide.color, 1); + callBattleStart(PlayerColor::UNFLAGGABLE, 1); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + callBattleStart(PlayerColor::SPECTATOR, 1); + + if(vstd::contains(playerint, leftSide.color) && playerint[leftSide.color]->human) + att = std::dynamic_pointer_cast(playerint[leftSide.color]); + + if(vstd::contains(playerint, rightSide.color) && playerint[rightSide.color]->human) + def = std::dynamic_pointer_cast(playerint[rightSide.color]); + + //Remove player interfaces for auto battle (quickCombat option) + if(att && att->isAutoFightOn) + { + if (att->cb->getBattle(info->battleID)->battleGetTacticDist()) + { + auto side = att->cb->getBattle(info->battleID)->playerToSide(att->playerID); + auto action = BattleAction::makeEndOFTacticPhase(*side); + att->cb->battleMakeTacticAction(info->battleID, action); + } + + att.reset(); + def.reset(); + } + + if(!settings["session"]["headless"].Bool()) + { + if(att || def) + { + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def); + } + else if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + { + //TODO: This certainly need improvement + auto spectratorInt = std::dynamic_pointer_cast(playerint[PlayerColor::SPECTATOR]); + spectratorInt->cb->onBattleStarted(info); + CPlayerInterface::battleInt = std::make_shared(info->getBattleID(), leftSide.armyObject, rightSide.armyObject, leftSide.hero, rightSide.hero, att, def, spectratorInt); + } + } + + if(info->tacticDistance) + { + auto tacticianColor = info->sides[info->tacticsSide].color; + + if (vstd::contains(battleints, tacticianColor)) + battleints[tacticianColor]->yourTacticPhase(info->battleID, info->tacticDistance); + } +} + +void CClient::battleFinished(const BattleID & battleID) +{ + for(auto & side : gs->getBattle(battleID)->sides) + if(battleCallbacks.count(side.color)) + battleCallbacks[side.color]->onBattleEnded(battleID); + + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool()) + battleCallbacks[PlayerColor::SPECTATOR]->onBattleEnded(battleID); +} + +void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) +{ + auto battleint = battleints.at(color); + + if (!battleint->human) + { + // we want to avoid locking gamestate and causing UI to freeze while AI is making turn + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } + else + { + battleint->activeStack(battleID, gs->getBattle(battleID)->battleGetStackByID(gs->getBattle(battleID)->activeStack, false)); + } +} + +void CClient::invalidatePaths() +{ + boost::unique_lock pathLock(pathCacheMutex); + pathCache.clear(); +} + +std::shared_ptr CClient::getPathsInfo(const CGHeroInstance * h) +{ + assert(h); + boost::unique_lock pathLock(pathCacheMutex); + + auto iter = pathCache.find(h); + + if(iter == std::end(pathCache)) + { + std::shared_ptr paths = std::make_shared(getMapSize(), h); + + gs->calculatePaths(h, *paths.get()); + + pathCache[h] = paths; + return paths; + } + else + { + return iter->second; + } +} + +#if SCRIPTING_ENABLED +scripting::Pool * CClient::getGlobalContextPool() const +{ + return clientScripts.get(); +} +#endif + +void CClient::reinitScripting() +{ + clientEventBus = std::make_unique(); +#if SCRIPTING_ENABLED + clientScripts.reset(new scripting::PoolImpl(this)); +#endif +} + +void CClient::removeGUI() +{ + // CClient::endGame + GH.curInt = nullptr; + GH.windows().clear(); + adventureInt.reset(); + logGlobal->info("Removed GUI."); + + LOCPLINT = nullptr; +} + +#ifdef VCMI_ANDROID +#ifndef SINGLE_PROCESS_APP +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerClosed(JNIEnv * env, jclass cls) +{ + logNetwork->info("Received server closed signal"); + if (CSH) { + CSH->campaignServerRestartLock.setn(false); + } +} + +extern "C" JNIEXPORT void JNICALL Java_eu_vcmi_vcmi_NativeMethods_notifyServerReady(JNIEnv * env, jclass cls) +{ + logNetwork->info("Received server ready signal"); + androidTestServerReadyFlag.store(true); +} +#endif + +extern "C" JNIEXPORT jboolean JNICALL Java_eu_vcmi_vcmi_NativeMethods_tryToSaveTheGame(JNIEnv * env, jclass cls) +{ + logGlobal->info("Received emergency save game request"); + if(!LOCPLINT || !LOCPLINT->cb) + { + return false; + } + + LOCPLINT->cb->save("Saves/_Android_Autosave"); + return true; +} +#endif diff --git a/client/Client.h b/client/Client.h index b9c98768b..4d89d3784 100644 --- a/client/Client.h +++ b/client/Client.h @@ -1,240 +1,240 @@ -/* - * Client.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 -#include - -#include "../lib/IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct CPack; -struct CPackForServer; -class IBattleEventsReceiver; -class CBattleGameInterface; -class CGameInterface; -class BinaryDeserializer; -class BinarySerializer; -class BattleAction; -class BattleInfo; - -template class CApplier; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class PoolImpl; -} -#endif - -namespace events -{ - class EventBus; -} - -VCMI_LIB_NAMESPACE_END - -class CBattleCallback; -class CCallback; -class CClient; -class CBaseForCLApply; - -namespace boost { class thread; } - -template -class ThreadSafeVector -{ - using TLock = boost::unique_lock; - std::vector items; - boost::mutex mx; - boost::condition_variable cond; - -public: - void clear() - { - TLock lock(mx); - items.clear(); - cond.notify_all(); - } - - void pushBack(const T & item) - { - TLock lock(mx); - items.push_back(item); - cond.notify_all(); - } - - void waitWhileContains(const T & item) - { - TLock lock(mx); - while(vstd::contains(items, item)) - cond.wait(lock); - } - - bool tryRemovingElement(const T & item) //returns false if element was not present - { - TLock lock(mx); - auto itr = vstd::find(items, item); - if(itr == items.end()) //not in container - { - return false; - } - - items.erase(itr); - cond.notify_all(); - return true; - } -}; - -class CPlayerEnvironment : public Environment -{ -public: - PlayerColor player; - CClient * cl; - std::shared_ptr mainCallback; - - CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_); - const Services * services() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - const BattleCb * battle(const BattleID & battle) const override; - const GameCb * game() const override; -}; - -/// Class which handles client - server logic -class CClient : public IGameCallback, public Environment -{ -public: - std::map> playerint; - std::map> battleints; - - std::map>> additionalBattleInts; - - std::unique_ptr currentBattleAction; - - CClient(); - ~CClient(); - - const Services * services() const override; - const BattleCb * battle(const BattleID & battle) const override; - const GameCb * game() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - - void newGame(CGameState * gameState); - void loadGame(CGameState * gameState); - void serialize(BinarySerializer & h, const int version); - void serialize(BinaryDeserializer & h, const int version); - - void save(const std::string & fname); - void endGame(); - - void initMapHandler(); - void initPlayerEnvironments(); - void initPlayerInterfaces(); - std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman); //empty means no AI -> human - std::string aiNameForPlayer(bool battleAI, bool alliedToHuman); - void installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb = false); - void installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback = true); - - static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) - - void handlePack(CPack * pack); //applies the given pack and deletes it - int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request - - void battleStarted(const BattleInfo * info); - void battleFinished(const BattleID & battleID); - void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); - - void invalidatePaths(); - std::shared_ptr getPathsInfo(const CGHeroInstance * h); - - friend class CCallback; //handling players actions - friend class CBattleCallback; //handling players actions - - void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; - bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {}; - void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; - void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; - - void showBlockingDialog(BlockingDialog * iw) override {}; - void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; - void showTeleportDialog(TeleportDialog * iw) override {}; - void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; - void giveResource(PlayerColor player, GameResID which, int val) override {}; - virtual void giveResources(PlayerColor player, TResources resources) override {}; - - void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; - void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; - bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; - bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; - bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; - bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; - bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} - bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} - void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} - bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} - - void removeAfterVisit(const CGObjectInstance * object) override {}; - bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; - void removeArtifact(const ArtifactLocation & al) override {}; - bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; - - void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; - void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle - bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; - void giveHeroBonus(GiveBonus * bonus) override {}; - void setMovePoints(SetMovePoints * smp) override {}; - void setManaPoints(ObjectInstanceID hid, int val) override {}; - void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; - void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}; - void sendAndApply(CPackForClient * pack) override {}; - void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; - void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; - - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} - void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} - - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} - - void showInfoDialog(InfoWindow * iw) override {}; - void showInfoDialog(const std::string & msg, PlayerColor player) override {}; - void removeGUI(); - -#if SCRIPTING_ENABLED - scripting::Pool * getGlobalContextPool() const override; -#endif - -private: - std::map> battleCallbacks; //callbacks given to player interfaces - std::map> playerEnvironments; - -#if SCRIPTING_ENABLED - std::shared_ptr clientScripts; -#endif - std::unique_ptr clientEventBus; - - std::shared_ptr> applier; - - mutable boost::mutex pathCacheMutex; - std::map> pathCache; - - void reinitScripting(); -}; +/* + * Client.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 +#include + +#include "../lib/IGameCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CPack; +struct CPackForServer; +class IBattleEventsReceiver; +class CBattleGameInterface; +class CGameInterface; +class BinaryDeserializer; +class BinarySerializer; +class BattleAction; +class BattleInfo; + +template class CApplier; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class PoolImpl; +} +#endif + +namespace events +{ + class EventBus; +} + +VCMI_LIB_NAMESPACE_END + +class CBattleCallback; +class CCallback; +class CClient; +class CBaseForCLApply; + +namespace boost { class thread; } + +template +class ThreadSafeVector +{ + using TLock = boost::unique_lock; + std::vector items; + boost::mutex mx; + boost::condition_variable cond; + +public: + void clear() + { + TLock lock(mx); + items.clear(); + cond.notify_all(); + } + + void pushBack(const T & item) + { + TLock lock(mx); + items.push_back(item); + cond.notify_all(); + } + + void waitWhileContains(const T & item) + { + TLock lock(mx); + while(vstd::contains(items, item)) + cond.wait(lock); + } + + bool tryRemovingElement(const T & item) //returns false if element was not present + { + TLock lock(mx); + auto itr = vstd::find(items, item); + if(itr == items.end()) //not in container + { + return false; + } + + items.erase(itr); + cond.notify_all(); + return true; + } +}; + +class CPlayerEnvironment : public Environment +{ +public: + PlayerColor player; + CClient * cl; + std::shared_ptr mainCallback; + + CPlayerEnvironment(PlayerColor player_, CClient * cl_, std::shared_ptr mainCallback_); + const Services * services() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + const BattleCb * battle(const BattleID & battle) const override; + const GameCb * game() const override; +}; + +/// Class which handles client - server logic +class CClient : public IGameCallback, public Environment +{ +public: + std::map> playerint; + std::map> battleints; + + std::map>> additionalBattleInts; + + std::unique_ptr currentBattleAction; + + CClient(); + ~CClient(); + + const Services * services() const override; + const BattleCb * battle(const BattleID & battle) const override; + const GameCb * game() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + + void newGame(CGameState * gameState); + void loadGame(CGameState * gameState); + void serialize(BinarySerializer & h, const int version); + void serialize(BinaryDeserializer & h, const int version); + + void save(const std::string & fname); + void endGame(); + + void initMapHandler(); + void initPlayerEnvironments(); + void initPlayerInterfaces(); + std::string aiNameForPlayer(const PlayerSettings & ps, bool battleAI, bool alliedToHuman); //empty means no AI -> human + std::string aiNameForPlayer(bool battleAI, bool alliedToHuman); + void installNewPlayerInterface(std::shared_ptr gameInterface, PlayerColor color, bool battlecb = false); + void installNewBattleInterface(std::shared_ptr battleInterface, PlayerColor color, bool needCallback = true); + + static ThreadSafeVector waitingRequest; //FIXME: make this normal field (need to join all threads before client destruction) + + void handlePack(CPack * pack); //applies the given pack and deletes it + int sendRequest(const CPackForServer * request, PlayerColor player); //returns ID given to that request + + void battleStarted(const BattleInfo * info); + void battleFinished(const BattleID & battleID); + void startPlayerBattleAction(const BattleID & battleID, PlayerColor color); + + void invalidatePaths(); + std::shared_ptr getPathsInfo(const CGHeroInstance * h); + + friend class CCallback; //handling players actions + friend class CBattleCallback; //handling players actions + + void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; + bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {}; + void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; + void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; + + void showBlockingDialog(BlockingDialog * iw) override {}; + void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override {}; + void showTeleportDialog(TeleportDialog * iw) override {}; + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override {}; + void giveResource(PlayerColor player, GameResID which, int val) override {}; + virtual void giveResources(PlayerColor player, TResources resources) override {}; + + void giveCreatures(const CArmedInstance * objid, const CGHeroInstance * h, const CCreatureSet & creatures, bool remove) override {}; + void takeCreatures(ObjectInstanceID objid, const std::vector & creatures) override {}; + bool changeStackType(const StackLocation & sl, const CCreature * c) override {return false;}; + bool changeStackCount(const StackLocation & sl, TQuantity count, bool absoluteValue = false) override {return false;}; + bool insertNewStack(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;}; + bool eraseStack(const StackLocation & sl, bool forceRemoval = false) override {return false;}; + bool swapStacks(const StackLocation & sl1, const StackLocation & sl2) override {return false;} + bool addToSlot(const StackLocation & sl, const CCreature * c, TQuantity count) override {return false;} + void tryJoiningArmy(const CArmedInstance * src, const CArmedInstance * dst, bool removeObjWhenFinished, bool allowMerging) override {} + bool moveStack(const StackLocation & src, const StackLocation & dst, TQuantity count = -1) override {return false;} + + void removeAfterVisit(const CGObjectInstance * object) override {}; + bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; + bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} + bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} + void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; + void removeArtifact(const ArtifactLocation & al) override {}; + bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; + + void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; + void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; + void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override {}; + void startBattlePrimary(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool creatureBank = false, const CGTownInstance * town = nullptr) override {}; //use hero=nullptr for no hero + void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, int3 tile, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used + void startBattleI(const CArmedInstance * army1, const CArmedInstance * army2, bool creatureBank = false) override {}; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override {return false;}; + void giveHeroBonus(GiveBonus * bonus) override {}; + void setMovePoints(SetMovePoints * smp) override {}; + void setManaPoints(ObjectInstanceID hid, int val) override {}; + void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override {}; + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override {}; + void sendAndApply(CPackForClient * pack) override {}; + void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override {}; + void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {}; + + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {} + void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, bool hide) override {} + + void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + + void showInfoDialog(InfoWindow * iw) override {}; + void showInfoDialog(const std::string & msg, PlayerColor player) override {}; + void removeGUI(); + +#if SCRIPTING_ENABLED + scripting::Pool * getGlobalContextPool() const override; +#endif + +private: + std::map> battleCallbacks; //callbacks given to player interfaces + std::map> playerEnvironments; + +#if SCRIPTING_ENABLED + std::shared_ptr clientScripts; +#endif + std::unique_ptr clientEventBus; + + std::shared_ptr> applier; + + mutable boost::mutex pathCacheMutex; + std::map> pathCache; + + void reinitScripting(); +}; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 0af992ec6..cd8f12f27 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -1,1042 +1,1042 @@ -/* - * NetPacksClient.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 "ClientNetPackVisitors.h" - -#include "Client.h" -#include "CPlayerInterface.h" -#include "CGameInfo.h" -#include "windows/GUIClasses.h" -#include "mapView/mapHandler.h" -#include "adventureMap/CInGameConsole.h" -#include "battle/BattleInterface.h" -#include "battle/BattleWindow.h" -#include "gui/CGuiHandler.h" -#include "gui/WindowHandler.h" -#include "widgets/MiscWidgets.h" -#include "CMT.h" -#include "CServerHandler.h" - -#include "../CCallback.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/FileInfo.h" -#include "../lib/serializer/Connection.h" -#include "../lib/serializer/BinarySerializer.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/VCMI_Lib.h" -#include "../lib/mapping/CMap.h" -#include "../lib/VCMIDirs.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/CSoundBase.h" -#include "../lib/StartInfo.h" -#include "../lib/CConfigHandler.h" -#include "../lib/mapObjects/CGMarket.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/CStack.h" -#include "../lib/battle/BattleInfo.h" -#include "../lib/GameConstants.h" -#include "../lib/CPlayerState.h" - -// TODO: as Tow suggested these template should all be part of CClient -// This will require rework spectator interface properly though - -template -bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - if(vstd::contains(cl.playerint, player)) - { - ((*cl.playerint[player]).*ptr)(std::forward(args)...); - return true; - } - return false; -} - -template -bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - bool called = callOnlyThatInterface(cl, player, ptr, std::forward(args)...); - return called; -} - -template -void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - if(vstd::contains(cl.battleints,player)) - ((*cl.battleints[player]).*ptr)(std::forward(args)...); - - if(cl.additionalBattleInts.count(player)) - { - for(auto bInt : cl.additionalBattleInts[player]) - ((*bInt).*ptr)(std::forward(args)...); - } -} - -template -void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) -{ - callOnlyThatInterface(cl, player, ptr, std::forward(args)...); -} - -//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy -template -void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) -{ - for(auto pInt : cl.playerint) - ((*pInt.second).*ptr)(std::forward(args)...); -} - -//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy -template -void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args) -{ - assert(cl.gameState()->getBattle(battleID)); - - if (!cl.gameState()->getBattle(battleID)) - { - logGlobal->error("Attempt to call battle interface without ongoing battle!"); - return; - } - - callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward(args)...); - callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward(args)...); - if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) - { - callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); - } -} - -void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack) -{ - //todo: inform on actual resource set transfered - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource); -} - -void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack) -{ - const CGHeroInstance * h = cl.getHero(pack.id); - if(!h) - { - logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); - return; - } - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val); -} - -void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.id); - if(!h) - { - logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); - return; - } - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val); -} - -void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - - if(pack.start()) - { - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid)); - } -} - -void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); - - for (auto window : GH.windows().findWindows()) - window->heroManaPointsChanged(h); -} - -void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - cl.invalidatePaths(); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); -} - -void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) -{ - for(auto &i : cl.playerint) - { - if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get()) - { - LOCPLINT->waitWhileDialog(); - } - if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) - { - if(pack.mode) - i.second->tileRevealed(pack.tiles); - else - i.second->tileHidden(pack.tiles); - } - } - cl.invalidatePaths(); -} - -static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2) -{ - auto obj1 = cl.getObj(army1); - if(!obj1) - { - logNetwork->error("Cannot find army with pack.id %d", army1.getNum()); - return; - } - - callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); - - if(army2 != ObjectInstanceID() && army2 != army1) - { - auto obj2 = cl.getObj(army2); - if(!obj2) - { - logNetwork->error("Cannot find army with pack.id %d", army2.getNum()); - return; - } - - if(obj1->tempOwner != obj2->tempOwner) - callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); - } -} - -void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) -{ - dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); -} - -void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) -{ - dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); -} - -void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) -{ - dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); -} - -void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) -{ - if(!pack.moves.empty()) - { - auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy - ? ObjectInstanceID() - : pack.moves[0].dstArmy; - dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); - } -} - -void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) -{ - if(!pack.moves.empty()) - { - assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy); - dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID()); - } - else if(!pack.changes.empty()) - { - dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID()); - } -} - -void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al); - if(pack.askAssemble) - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al); -} - -void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al); -} - -void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) -{ - auto moveArtifact = [this, &pack](PlayerColor player) -> void - { - callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, pack.src, pack.dst); - if(pack.askAssemble) - callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst); - }; - - moveArtifact(pack.src.owningPlayer()); - if(pack.src.owningPlayer() != pack.dst.owningPlayer()) - moveArtifact(pack.dst.owningPlayer()); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) -{ - auto applyMove = [this, &pack](std::vector & artsPack) -> void - { - for(auto & slotToMove : artsPack) - { - auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); - auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); - MoveArtifact ma(&srcLoc, &dstLoc, false); - visitMoveArtifact(ma); - } - }; - - auto srcOwner = std::get>(pack.srcArtHolder)->tempOwner; - auto dstOwner = std::get>(pack.dstArtHolder)->tempOwner; - - // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. - callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); - if(srcOwner != dstOwner) - callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); - - applyMove(pack.artsPack0); - if(pack.swap) - applyMove(pack.artsPack1); -} - -void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) -{ - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al); - - cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings -} - -void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) -{ - auto hero = cl.getHero(pack.heroId); - auto obj = cl.getObj(pack.objId, false); - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting); -} - -void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) -{ - cl.invalidatePaths(); -} - -void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) -{ - cl.invalidatePaths(); - switch(pack.who) - { - case GiveBonus::ETarget::HERO: - { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); - } - break; - case GiveBonus::ETarget::PLAYER: - { - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); - } - break; - } -} - -void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) -{ - CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeOut(obj, pack.initiator); - - CGI->mh->waitForOngoingAnimations(); -} - -void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) -{ - CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeIn(obj, pack.initiator); - - CGI->mh->waitForOngoingAnimations(); - cl.invalidatePaths(); -} - -void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) -{ - callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult); - - // In auto testing pack.mode we always close client if red pack.player won or lose - if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0)) - handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not -} - -void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack) -{ - auto initInterfaces = [this]() - { - cl.initPlayerInterfaces(); - - for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) - { - if (cl.gameState()->isPlayerMakingTurn(player)) - { - callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); - callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); - } - } - }; - - for(auto player : pack.players) - { - auto & plSettings = CSH->si->getIthPlayersSettings(player); - if(pack.playerConnectionId == PlayerSettings::PLAYER_AI) - { - plSettings.connectedPlayerIDs.clear(); - cl.initPlayerEnvironments(); - initInterfaces(); - } - else if(pack.playerConnectionId == CSH->c->connectionID) - { - plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); - cl.playerint.clear(); - initInterfaces(); - } - } -} - -void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) -{ - cl.invalidatePaths(); - switch(pack.who) - { - case GiveBonus::ETarget::HERO: - { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); - } - break; - case GiveBonus::ETarget::PLAYER: - { - //const PlayerState *p = gs.getPlayerState(pack.id); - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); - } - break; - } -} - -void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) -{ - const CGObjectInstance *o = cl.getObj(pack.objectID); - - if(CGI->mh) - CGI->mh->onObjectFadeOut(o, pack.initiator); - - //notify interfaces about removal - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW - //TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this - if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) - i->second->objectRemoved(o, pack.initiator); - } - - CGI->mh->waitForOngoingAnimations(); -} - -void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) -{ - cl.invalidatePaths(); - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - i->second->objectRemovedAfter(); -} - -void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) -{ - CGHeroInstance *h = gs.getHero(pack.id); - - if(CGI->mh) - { - switch (pack.result) - { - case TryMoveHero::EMBARK: - CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end); - break; - case TryMoveHero::TELEPORTATION: - CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end); - break; - case TryMoveHero::DISEMBARK: - CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end); - break; - } - CGI->mh->waitForOngoingAnimations(); - } -} - -void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.id); - cl.invalidatePaths(); - - if(CGI->mh) - { - switch(pack.result) - { - case TryMoveHero::SUCCESS: - CGI->mh->onHeroMoved(h, pack.start, pack.end); - break; - case TryMoveHero::EMBARK: - CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end); - break; - case TryMoveHero::TELEPORTATION: - CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end); - break; - case TryMoveHero::DISEMBARK: - CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end); - break; - } - } - - PlayerColor player = h->tempOwner; - - for(auto &i : cl.playerint) - if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) - i.second->tileRevealed(pack.fowRevealed); - - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface - continue; - - if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first) - || gs.isVisible(h->convertToVisitablePos(pack.end), i->first)) - { - // pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false - const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES; - i->second->heroMoved(pack, verbose); - } - } -} - -void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) -{ - CGTownInstance *town = gs.getTown(pack.tid); - for(const auto & id : pack.bid) - { - callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1); - } - - // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town, town->getOwner()); - CGI->mh->onObjectInstantAdd(town, town->getOwner()); - -} -void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) -{ - CGTownInstance * town = gs.getTown(pack.tid); - for(const auto & id : pack.bid) - { - callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2); - } - - // invalidate section of map view with our object and force an update - CGI->mh->onObjectInstantRemove(town, town->getOwner()); - CGI->mh->onObjectInstantAdd(town, town->getOwner()); -} - -void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) -{ - const CGDwelling * dw = static_cast(cl.getObj(pack.tid)); - - PlayerColor p; - if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor - p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner; - else - p = dw->tempOwner; - - callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw); -} - -void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) -{ - CGTownInstance * t = gs.getTown(pack.tid); - CGHeroInstance * hGarr = gs.getHero(pack.garrison); - CGHeroInstance * hVisit = gs.getHero(pack.visiting); - - //inform all players that see this object - for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i) - { - if(!i->first.isValidPlayer()) - continue; - - if(gs.isVisible(t, i->first) || - (hGarr && gs.isVisible(hGarr, i->first)) || - (hVisit && gs.isVisible(hVisit, i->first))) - { - cl.playerint[i->first]->heroInGarrisonChange(t); - } - } -} - -void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) -{ - CGHeroInstance *h = gs.map->heroesOnMap.back(); - if(h->subID != pack.hid) - { - logNetwork->error("Something wrong with hero recruited!"); - } - - if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) - { - if(const CGTownInstance *t = gs.getTown(pack.tid)) - callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t); - } - if(CGI->mh) - CGI->mh->onObjectInstantAdd(h, h->getOwner()); -} - -void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack) -{ - CGHeroInstance *h = gs.getHero(pack.id); - if(CGI->mh) - CGI->mh->onObjectInstantAdd(h, h->getOwner()); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); -} - -void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack) -{ -} - -void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack) -{ - std::string str = pack.text.toString(); - - if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID)) - logNetwork->warn("We received InfoWindow for not our player..."); -} - -void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) -{ - //inform all players that see this object - for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) - { - if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) - callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack); - } - - // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) - { - auto object = gs.getObjInstance(pack.id); - CGI->mh->onObjectInstantRemove(object, object->getOwner()); - } -} - -void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) -{ - //inform all players that see this object - for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) - { - if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) - callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack); - } - - // invalidate section of map view with our object and force an update with new flag color - if (pack.what == ObjProperty::OWNER) - { - auto object = gs.getObjInstance(pack.id); - CGI->mh->onObjectInstantAdd(object, object->getOwner()); - } -} - -void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack) -{ - const CGHeroInstance * hero = cl.getHero(pack.heroId); - assert(hero); - callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack) -{ - const CGHeroInstance * hero = cl.getHero(pack.heroId); - assert(hero); - const CCommanderInstance * commander = hero->commander; - assert(commander); - assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance? - callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack) -{ - std::string str = pack.text.toString(); - - if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel())) - logNetwork->warn("We received YesNoDialog for not our player..."); -} - -void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hid); - const CArmedInstance *obj = static_cast(cl.getObj(pack.objid)); - - callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack) -{ - const CGHeroInstance *h = cl.getHero(pack.hero); - callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack) -{ - callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects); -} - -void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack) -{ - // Cannot use the usual code because curB is not set yet - callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); - callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, - pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); -} - -void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) -{ - cl.battleStarted(pack.info); -} - -void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID); -} - -void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID); -} - -void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) -{ - if(!pack.askPlayerInterface) - return; - - const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); - PlayerColor playerToCall; //pack.player that will move activated stack - if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) - { - playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner() - ? gs.getBattle(pack.battleID)->sides[1].color - : gs.getBattle(pack.battleID)->sides[0].color); - } - else - { - playerToCall = activated->unitOwner(); - } - - cl.startPlayerBattleAction(pack.battleID, playerToCall); -} - -void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines); -} - -void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack); -} - -void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state); -} - -void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID); - cl.battleFinished(pack.battleID); -} - -void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack) -{ - const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); -} - -void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack); - - // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit - // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot()); -} - -void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) -{ -} - -void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) -{ - cl.currentBattleAction = std::make_unique(pack.ba); - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba); -} - -void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack); -} - -void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack) -{ - //informing about effects - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack); -} - -void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false); -} - -void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) -{ - callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied); - callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied); - callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); -} - -void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks); -} - -void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack) -{ - //inform interfaces about removed obstacles - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes); -} - -void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) -{ - //inform interfaces about catapult attack - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack); -} - -void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) -{ - callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction); - cl.currentBattleAction.reset(); -} - -void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack); - if(!CClient::waitingRequest.tryRemovingElement(pack.requestID)) - logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); -} - -void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack) -{ - std::ostringstream str; - str << "System message: " << pack.text; - - logNetwork->error(str.str()); // usually used to receive error messages from server - if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool()) - LOCPLINT->cingconsole->print(str.str()); -} - -void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED); -} - -void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack) -{ - logNetwork->debug("Server gives turn to %s", pack.player.toString()); - - callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); - callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); -} - -void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) -{ - logNetwork->debug("Server ends turn of %s", pack.player.toString()); - - callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player); -} - -void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) -{ - logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); -} - -void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) -{ - logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text); - - std::ostringstream str; - if(pack.player.isSpectator()) - str << "Spectator: " << pack.text; - else - str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text; - if(LOCPLINT) - LOCPLINT->cingconsole->print(str.str()); -} - -void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack) -{ - cl.invalidatePaths(); - auto caster = cl.getHero(pack.casterID); - if(caster) - //consider notifying other interfaces that see hero? - callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID); - else - logNetwork->error("Invalid hero instance"); -} - -void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack) -{ - callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain); -} - -void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) -{ - switch(pack.window) - { - case EOpenWindowMode::RECRUITMENT_FIRST: - case EOpenWindowMode::RECRUITMENT_ALL: - { - const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); - const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.visitor))); - callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID); - } - break; - case EOpenWindowMode::SHIPYARD_WINDOW: - { - assert(pack.queryID == QueryID::NONE); - const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object))); - callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); - } - break; - case EOpenWindowMode::THIEVES_GUILD: - { - assert(pack.queryID == QueryID::NONE); - //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj); - } - break; - case EOpenWindowMode::UNIVERSITY_WINDOW: - { - //displays University window (when hero enters University on adventure map) - const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object))); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); - } - break; - case EOpenWindowMode::MARKET_WINDOW: - { - //displays Thieves' Guild window (when hero enters Den of Thieves) - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - const IMarket *market = IMarket::castFrom(obj); - callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); - } - break; - case EOpenWindowMode::HILL_FORT_WINDOW: - { - assert(pack.queryID == QueryID::NONE); - //displays Hill fort window - const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); - } - break; - case EOpenWindowMode::PUZZLE_MAP: - { - assert(pack.queryID == QueryID::NONE); - const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); - callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap); - } - break; - case EOpenWindowMode::TAVERN_WINDOW: - { - const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object)); - const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor)); - callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID); - } - break; - } -} - -void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack) -{ - callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime); -} - -void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) -{ - cl.invalidatePaths(); - - const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); - if(CGI->mh) - CGI->mh->onObjectFadeIn(obj, pack.initiator); - - for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) - { - if(gs.isVisible(obj, i->first)) - i->second->newObject(obj); - } - CGI->mh->waitForOngoingAnimations(); -} - -void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack) -{ - if(pack.id < 0) //artifact merchants globally - { - callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr); - } - else - { - const CGBlackMarket *bm = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id))); - assert(bm); - callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm); - } -} - - -void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack) -{ - cl.invalidatePaths(); -} +/* + * NetPacksClient.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 "ClientNetPackVisitors.h" + +#include "Client.h" +#include "CPlayerInterface.h" +#include "CGameInfo.h" +#include "windows/GUIClasses.h" +#include "mapView/mapHandler.h" +#include "adventureMap/CInGameConsole.h" +#include "battle/BattleInterface.h" +#include "battle/BattleWindow.h" +#include "gui/CGuiHandler.h" +#include "gui/WindowHandler.h" +#include "widgets/MiscWidgets.h" +#include "CMT.h" +#include "CServerHandler.h" + +#include "../CCallback.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/FileInfo.h" +#include "../lib/serializer/Connection.h" +#include "../lib/serializer/BinarySerializer.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/VCMI_Lib.h" +#include "../lib/mapping/CMap.h" +#include "../lib/VCMIDirs.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/CSoundBase.h" +#include "../lib/StartInfo.h" +#include "../lib/CConfigHandler.h" +#include "../lib/mapObjects/CGMarket.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/CStack.h" +#include "../lib/battle/BattleInfo.h" +#include "../lib/GameConstants.h" +#include "../lib/CPlayerState.h" + +// TODO: as Tow suggested these template should all be part of CClient +// This will require rework spectator interface properly though + +template +bool callOnlyThatInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + if(vstd::contains(cl.playerint, player)) + { + ((*cl.playerint[player]).*ptr)(std::forward(args)...); + return true; + } + return false; +} + +template +bool callInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + bool called = callOnlyThatInterface(cl, player, ptr, std::forward(args)...); + return called; +} + +template +void callOnlyThatBattleInterface(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + if(vstd::contains(cl.battleints,player)) + ((*cl.battleints[player]).*ptr)(std::forward(args)...); + + if(cl.additionalBattleInts.count(player)) + { + for(auto bInt : cl.additionalBattleInts[player]) + ((*bInt).*ptr)(std::forward(args)...); + } +} + +template +void callBattleInterfaceIfPresent(CClient & cl, PlayerColor player, void (T::*ptr)(Args...), Args2 && ...args) +{ + callOnlyThatInterface(cl, player, ptr, std::forward(args)...); +} + +//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy +template +void callAllInterfaces(CClient & cl, void (T::*ptr)(Args...), Args2 && ...args) +{ + for(auto pInt : cl.playerint) + ((*pInt.second).*ptr)(std::forward(args)...); +} + +//calls all normal interfaces and privileged ones, playerints may be updated when iterating over it, so we need a copy +template +void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & battleID, void (T::*ptr)(Args...), Args2 && ...args) +{ + assert(cl.gameState()->getBattle(battleID)); + + if (!cl.gameState()->getBattle(battleID)) + { + logGlobal->error("Attempt to call battle interface without ongoing battle!"); + return; + } + + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[0].color, ptr, std::forward(args)...); + callOnlyThatBattleInterface(cl, cl.gameState()->getBattle(battleID)->sides[1].color, ptr, std::forward(args)...); + if(settings["session"]["spectate"].Bool() && !settings["session"]["spectate-skip-battle"].Bool() && LOCPLINT->battleInt) + { + callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, ptr, std::forward(args)...); + } +} + +void ApplyClientNetPackVisitor::visitSetResources(SetResources & pack) +{ + //todo: inform on actual resource set transfered + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::receivedResource); +} + +void ApplyClientNetPackVisitor::visitSetPrimSkill(SetPrimSkill & pack) +{ + const CGHeroInstance * h = cl.getHero(pack.id); + if(!h) + { + logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); + return; + } + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroPrimarySkillChanged, h, pack.which, pack.val); +} + +void ApplyClientNetPackVisitor::visitSetSecSkill(SetSecSkill & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.id); + if(!h) + { + logNetwork->error("Cannot find hero with pack.id %d", pack.id.getNum()); + return; + } + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroSecondarySkillChanged, h, pack.which, pack.val); +} + +void ApplyClientNetPackVisitor::visitHeroVisitCastle(HeroVisitCastle & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + + if(pack.start()) + { + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroVisitsTown, h, gs.getTown(pack.tid)); + } +} + +void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroManaPointsChanged, h); + + for (auto window : GH.windows().findWindows()) + window->heroManaPointsChanged(h); +} + +void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + cl.invalidatePaths(); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h); +} + +void ApplyClientNetPackVisitor::visitFoWChange(FoWChange & pack) +{ + for(auto &i : cl.playerint) + { + if(cl.getPlayerRelations(i.first, pack.player) == PlayerRelations::SAME_PLAYER && pack.waitForDialogs && LOCPLINT == i.second.get()) + { + LOCPLINT->waitWhileDialog(); + } + if(cl.getPlayerRelations(i.first, pack.player) != PlayerRelations::ENEMIES) + { + if(pack.mode) + i.second->tileRevealed(pack.tiles); + else + i.second->tileHidden(pack.tiles); + } + } + cl.invalidatePaths(); +} + +static void dispatchGarrisonChange(CClient & cl, ObjectInstanceID army1, ObjectInstanceID army2) +{ + auto obj1 = cl.getObj(army1); + if(!obj1) + { + logNetwork->error("Cannot find army with pack.id %d", army1.getNum()); + return; + } + + callInterfaceIfPresent(cl, obj1->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); + + if(army2 != ObjectInstanceID() && army2 != army1) + { + auto obj2 = cl.getObj(army2); + if(!obj2) + { + logNetwork->error("Cannot find army with pack.id %d", army2.getNum()); + return; + } + + if(obj1->tempOwner != obj2->tempOwner) + callInterfaceIfPresent(cl, obj2->tempOwner, &IGameEventsReceiver::garrisonsChanged, army1, army2); + } +} + +void ApplyClientNetPackVisitor::visitChangeStackCount(ChangeStackCount & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack) +{ + dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); +} + +void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack) +{ + dispatchGarrisonChange(cl, pack.army, ObjectInstanceID()); +} + +void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack) +{ + dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy); +} + +void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack) +{ + if(!pack.moves.empty()) + { + auto destArmy = pack.moves[0].srcArmy == pack.moves[0].dstArmy + ? ObjectInstanceID() + : pack.moves[0].dstArmy; + dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy); + } +} + +void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalanceStacks & pack) +{ + if(!pack.moves.empty()) + { + assert(pack.moves[0].srcArmy == pack.moves[0].dstArmy); + dispatchGarrisonChange(cl, pack.moves[0].srcArmy, ObjectInstanceID()); + } + else if(!pack.changes.empty()) + { + dispatchGarrisonChange(cl, pack.changes[0].army, ObjectInstanceID()); + } +} + +void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) +{ + callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al); + if(pack.askAssemble) + callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al); +} + +void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) +{ + callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al); +} + +void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) +{ + auto moveArtifact = [this, &pack](PlayerColor player) -> void + { + callInterfaceIfPresent(cl, player, &IGameEventsReceiver::artifactMoved, pack.src, pack.dst); + if(pack.askAssemble) + callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst); + }; + + moveArtifact(pack.src.owningPlayer()); + if(pack.src.owningPlayer() != pack.dst.owningPlayer()) + moveArtifact(pack.dst.owningPlayer()); + + cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings +} + +void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) +{ + auto applyMove = [this, &pack](std::vector & artsPack) -> void + { + for(auto & slotToMove : artsPack) + { + auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); + auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); + MoveArtifact ma(&srcLoc, &dstLoc, false); + visitMoveArtifact(ma); + } + }; + + auto srcOwner = std::get>(pack.srcArtHolder)->tempOwner; + auto dstOwner = std::get>(pack.dstArtHolder)->tempOwner; + + // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. + callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + if(srcOwner != dstOwner) + callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); + + applyMove(pack.artsPack0); + if(pack.swap) + applyMove(pack.artsPack1); +} + +void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) +{ + callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al); + + cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings +} + +void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) +{ + callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al); + + cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings +} + +void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack) +{ + auto hero = cl.getHero(pack.heroId); + auto obj = cl.getObj(pack.objId, false); + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroVisit, hero, obj, pack.starting); +} + +void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack) +{ + cl.invalidatePaths(); +} + +void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) +{ + cl.invalidatePaths(); + switch(pack.who) + { + case GiveBonus::ETarget::HERO: + { + const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); + } + break; + case GiveBonus::ETarget::PLAYER: + { + callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); + } + break; + } +} + +void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) +{ + CGObjectInstance *obj = gs.getObjInstance(pack.objid); + if(CGI->mh) + CGI->mh->onObjectFadeOut(obj, pack.initiator); + + CGI->mh->waitForOngoingAnimations(); +} + +void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) +{ + CGObjectInstance *obj = gs.getObjInstance(pack.objid); + if(CGI->mh) + CGI->mh->onObjectFadeIn(obj, pack.initiator); + + CGI->mh->waitForOngoingAnimations(); + cl.invalidatePaths(); +} + +void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack) +{ + callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult); + + // In auto testing pack.mode we always close client if red pack.player won or lose + if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0)) + handleQuit(settings["session"]["spectate"].Bool()); // if spectator is active ask to close client or not +} + +void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface & pack) +{ + auto initInterfaces = [this]() + { + cl.initPlayerInterfaces(); + + for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player) + { + if (cl.gameState()->isPlayerMakingTurn(player)) + { + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player); + callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE); + } + } + }; + + for(auto player : pack.players) + { + auto & plSettings = CSH->si->getIthPlayersSettings(player); + if(pack.playerConnectionId == PlayerSettings::PLAYER_AI) + { + plSettings.connectedPlayerIDs.clear(); + cl.initPlayerEnvironments(); + initInterfaces(); + } + else if(pack.playerConnectionId == CSH->c->connectionID) + { + plSettings.connectedPlayerIDs.insert(pack.playerConnectionId); + cl.playerint.clear(); + initInterfaces(); + } + } +} + +void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) +{ + cl.invalidatePaths(); + switch(pack.who) + { + case GiveBonus::ETarget::HERO: + { + const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); + } + break; + case GiveBonus::ETarget::PLAYER: + { + //const PlayerState *p = gs.getPlayerState(pack.id); + callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); + } + break; + } +} + +void ApplyFirstClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) +{ + const CGObjectInstance *o = cl.getObj(pack.objectID); + + if(CGI->mh) + CGI->mh->onObjectFadeOut(o, pack.initiator); + + //notify interfaces about removal + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + //below line contains little cheat for AI so it will be aware of deletion of enemy heroes that moved or got re-covered by FoW + //TODO: loose requirements as next AI related crashes appear, for example another pack.player collects object that got re-covered by FoW, unsure if AI code workarounds this + if(gs.isVisible(o, i->first) || (!cl.getPlayerState(i->first)->human && o->ID == Obj::HERO && o->tempOwner != i->first)) + i->second->objectRemoved(o, pack.initiator); + } + + CGI->mh->waitForOngoingAnimations(); +} + +void ApplyClientNetPackVisitor::visitRemoveObject(RemoveObject & pack) +{ + cl.invalidatePaths(); + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + i->second->objectRemovedAfter(); +} + +void ApplyFirstClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) +{ + CGHeroInstance *h = gs.getHero(pack.id); + + if(CGI->mh) + { + switch (pack.result) + { + case TryMoveHero::EMBARK: + CGI->mh->onBeforeHeroEmbark(h, pack.start, pack.end); + break; + case TryMoveHero::TELEPORTATION: + CGI->mh->onBeforeHeroTeleported(h, pack.start, pack.end); + break; + case TryMoveHero::DISEMBARK: + CGI->mh->onBeforeHeroDisembark(h, pack.start, pack.end); + break; + } + CGI->mh->waitForOngoingAnimations(); + } +} + +void ApplyClientNetPackVisitor::visitTryMoveHero(TryMoveHero & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.id); + cl.invalidatePaths(); + + if(CGI->mh) + { + switch(pack.result) + { + case TryMoveHero::SUCCESS: + CGI->mh->onHeroMoved(h, pack.start, pack.end); + break; + case TryMoveHero::EMBARK: + CGI->mh->onAfterHeroEmbark(h, pack.start, pack.end); + break; + case TryMoveHero::TELEPORTATION: + CGI->mh->onAfterHeroTeleported(h, pack.start, pack.end); + break; + case TryMoveHero::DISEMBARK: + CGI->mh->onAfterHeroDisembark(h, pack.start, pack.end); + break; + } + } + + PlayerColor player = h->tempOwner; + + for(auto &i : cl.playerint) + if(cl.getPlayerRelations(i.first, player) != PlayerRelations::ENEMIES) + i.second->tileRevealed(pack.fowRevealed); + + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + if(i->first != PlayerColor::SPECTATOR && gs.checkForStandardLoss(i->first)) // Do not notify vanquished pack.player's interface + continue; + + if(gs.isVisible(h->convertToVisitablePos(pack.start), i->first) + || gs.isVisible(h->convertToVisitablePos(pack.end), i->first)) + { + // pack.src and pack.dst of enemy hero move may be not visible => 'verbose' should be false + const bool verbose = cl.getPlayerRelations(i->first, player) != PlayerRelations::ENEMIES; + i->second->heroMoved(pack, verbose); + } + } +} + +void ApplyClientNetPackVisitor::visitNewStructures(NewStructures & pack) +{ + CGTownInstance *town = gs.getTown(pack.tid); + for(const auto & id : pack.bid) + { + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 1); + } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); + +} +void ApplyClientNetPackVisitor::visitRazeStructures(RazeStructures & pack) +{ + CGTownInstance * town = gs.getTown(pack.tid); + for(const auto & id : pack.bid) + { + callInterfaceIfPresent(cl, town->getOwner(), &IGameEventsReceiver::buildChanged, town, id, 2); + } + + // invalidate section of map view with our object and force an update + CGI->mh->onObjectInstantRemove(town, town->getOwner()); + CGI->mh->onObjectInstantAdd(town, town->getOwner()); +} + +void ApplyClientNetPackVisitor::visitSetAvailableCreatures(SetAvailableCreatures & pack) +{ + const CGDwelling * dw = static_cast(cl.getObj(pack.tid)); + + PlayerColor p; + if(dw->ID == Obj::WAR_MACHINE_FACTORY) //War Machines Factory is not flaggable, it's "owned" by visitor + p = cl.getTile(dw->visitablePos())->visitableObjects.back()->tempOwner; + else + p = dw->tempOwner; + + callInterfaceIfPresent(cl, p, &IGameEventsReceiver::availableCreaturesChanged, dw); +} + +void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) +{ + CGTownInstance * t = gs.getTown(pack.tid); + CGHeroInstance * hGarr = gs.getHero(pack.garrison); + CGHeroInstance * hVisit = gs.getHero(pack.visiting); + + //inform all players that see this object + for(auto i = cl.playerint.cbegin(); i != cl.playerint.cend(); ++i) + { + if(!i->first.isValidPlayer()) + continue; + + if(gs.isVisible(t, i->first) || + (hGarr && gs.isVisible(hGarr, i->first)) || + (hVisit && gs.isVisible(hVisit, i->first))) + { + cl.playerint[i->first]->heroInGarrisonChange(t); + } + } +} + +void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) +{ + CGHeroInstance *h = gs.map->heroesOnMap.back(); + if(h->subID != pack.hid) + { + logNetwork->error("Something wrong with hero recruited!"); + } + + if(callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h)) + { + if(const CGTownInstance *t = gs.getTown(pack.tid)) + callInterfaceIfPresent(cl, h->getOwner(), &IGameEventsReceiver::heroInGarrisonChange, t); + } + if(CGI->mh) + CGI->mh->onObjectInstantAdd(h, h->getOwner()); +} + +void ApplyClientNetPackVisitor::visitGiveHero(GiveHero & pack) +{ + CGHeroInstance *h = gs.getHero(pack.id); + if(CGI->mh) + CGI->mh->onObjectInstantAdd(h, h->getOwner()); + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroCreated, h); +} + +void ApplyFirstClientNetPackVisitor::visitGiveHero(GiveHero & pack) +{ +} + +void ApplyClientNetPackVisitor::visitInfoWindow(InfoWindow & pack) +{ + std::string str = pack.text.toString(); + + if(!callInterfaceIfPresent(cl, pack.player, &CGameInterface::showInfoDialog, pack.type, str, pack.components,(soundBase::soundID)pack.soundID)) + logNetwork->warn("We received InfoWindow for not our player..."); +} + +void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) +{ + //inform all players that see this object + for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) + { + if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) + callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::beforeObjectPropertyChanged, &pack); + } + + // invalidate section of map view with our object and force an update with new flag color + if (pack.what == ObjProperty::OWNER) + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantRemove(object, object->getOwner()); + } +} + +void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack) +{ + //inform all players that see this object + for(auto it = cl.playerint.cbegin(); it != cl.playerint.cend(); ++it) + { + if(gs.isVisible(gs.getObjInstance(pack.id), it->first)) + callInterfaceIfPresent(cl, it->first, &IGameEventsReceiver::objectPropertyChanged, &pack); + } + + // invalidate section of map view with our object and force an update with new flag color + if (pack.what == ObjProperty::OWNER) + { + auto object = gs.getObjInstance(pack.id); + CGI->mh->onObjectInstantAdd(object, object->getOwner()); + } +} + +void ApplyClientNetPackVisitor::visitHeroLevelUp(HeroLevelUp & pack) +{ + const CGHeroInstance * hero = cl.getHero(pack.heroId); + assert(hero); + callOnlyThatInterface(cl, pack.player, &CGameInterface::heroGotLevel, hero, pack.primskill, pack.skills, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitCommanderLevelUp(CommanderLevelUp & pack) +{ + const CGHeroInstance * hero = cl.getHero(pack.heroId); + assert(hero); + const CCommanderInstance * commander = hero->commander; + assert(commander); + assert(commander->armyObj); //is it possible for Commander to exist beyond armed instance? + callOnlyThatInterface(cl, pack.player, &CGameInterface::commanderGotLevel, commander, pack.skills, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitBlockingDialog(BlockingDialog & pack) +{ + std::string str = pack.text.toString(); + + if(!callOnlyThatInterface(cl, pack.player, &CGameInterface::showBlockingDialog, str, pack.components, pack.queryID, (soundBase::soundID)pack.soundID, pack.selection(), pack.cancel())) + logNetwork->warn("We received YesNoDialog for not our player..."); +} + +void ApplyClientNetPackVisitor::visitGarrisonDialog(GarrisonDialog & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hid); + const CArmedInstance *obj = static_cast(cl.getObj(pack.objid)); + + callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showGarrisonDialog, obj, h, pack.removableUnits, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitExchangeDialog(ExchangeDialog & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::heroExchangeStarted, pack.hero1, pack.hero2, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitTeleportDialog(TeleportDialog & pack) +{ + const CGHeroInstance *h = cl.getHero(pack.hero); + callOnlyThatInterface(cl, h->getOwner(), &CGameInterface::showTeleportDialog, h, pack.channel, pack.exits, pack.impassable, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitMapObjectSelectDialog(MapObjectSelectDialog & pack) +{ + callOnlyThatInterface(cl, pack.player, &CGameInterface::showMapObjectSelectDialog, pack.queryID, pack.icon, pack.title, pack.description, pack.objects); +} + +void ApplyFirstClientNetPackVisitor::visitBattleStart(BattleStart & pack) +{ + // Cannot use the usual code because curB is not set yet + callOnlyThatBattleInterface(cl, pack.info->sides[0].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); + callOnlyThatBattleInterface(cl, pack.info->sides[1].color, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); + callOnlyThatBattleInterface(cl, PlayerColor::SPECTATOR, &IBattleEventsReceiver::battleStartBefore, pack.battleID, pack.info->sides[0].armyObject, pack.info->sides[1].armyObject, + pack.info->tile, pack.info->sides[0].hero, pack.info->sides[1].hero); +} + +void ApplyClientNetPackVisitor::visitBattleStart(BattleStart & pack) +{ + cl.battleStarted(pack.info); +} + +void ApplyFirstClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRoundFirst, pack.battleID); +} + +void ApplyClientNetPackVisitor::visitBattleNextRound(BattleNextRound & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleNewRound, pack.battleID); +} + +void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack & pack) +{ + if(!pack.askPlayerInterface) + return; + + const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); + PlayerColor playerToCall; //pack.player that will move activated stack + if (activated->hasBonusOfType(BonusType::HYPNOTIZED)) + { + playerToCall = (gs.getBattle(pack.battleID)->sides[0].color == activated->unitOwner() + ? gs.getBattle(pack.battleID)->sides[1].color + : gs.getBattle(pack.battleID)->sides[0].color); + } + else + { + playerToCall = activated->unitOwner(); + } + + cl.startPlayerBattleAction(pack.battleID, playerToCall); +} + +void ApplyClientNetPackVisitor::visitBattleLogMessage(BattleLogMessage & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleLogMessage, pack.battleID, pack.lines); +} + +void ApplyClientNetPackVisitor::visitBattleTriggerEffect(BattleTriggerEffect & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleTriggerEffect, pack.battleID, pack); +} + +void ApplyFirstClientNetPackVisitor::visitBattleUpdateGateState(BattleUpdateGateState & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleGateStateChanged, pack.battleID, pack.state); +} + +void ApplyFirstClientNetPackVisitor::visitBattleResult(BattleResult & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleEnd, pack.battleID, &pack, pack.queryID); + cl.battleFinished(pack.battleID); +} + +void ApplyFirstClientNetPackVisitor::visitBattleStackMoved(BattleStackMoved & pack) +{ + const CStack * movedStack = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStackMoved, pack.battleID, movedStack, pack.tilesToMove, pack.distance, pack.teleporting); +} + +void ApplyFirstClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleAttack, pack.battleID, &pack); + + // battleStacksAttacked should be excuted before BattleAttack.applyGs() to play animation before damaging unit + // so this has to be here instead of ApplyClientNetPackVisitor::visitBattleAttack() + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.bsa, pack.shot()); +} + +void ApplyClientNetPackVisitor::visitBattleAttack(BattleAttack & pack) +{ +} + +void ApplyFirstClientNetPackVisitor::visitStartAction(StartAction & pack) +{ + cl.currentBattleAction = std::make_unique(pack.ba); + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionStarted, pack.battleID, pack.ba); +} + +void ApplyClientNetPackVisitor::visitBattleSpellCast(BattleSpellCast & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleSpellCast, pack.battleID, &pack); +} + +void ApplyClientNetPackVisitor::visitSetStackEffect(SetStackEffect & pack) +{ + //informing about effects + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksEffectsSet, pack.battleID, pack); +} + +void ApplyClientNetPackVisitor::visitStacksInjured(StacksInjured & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleStacksAttacked, pack.battleID, pack.stacks, false); +} + +void ApplyClientNetPackVisitor::visitBattleResultsApplied(BattleResultsApplied & pack) +{ + callInterfaceIfPresent(cl, pack.player1, &IGameEventsReceiver::battleResultsApplied); + callInterfaceIfPresent(cl, pack.player2, &IGameEventsReceiver::battleResultsApplied); + callInterfaceIfPresent(cl, PlayerColor::SPECTATOR, &IGameEventsReceiver::battleResultsApplied); +} + +void ApplyClientNetPackVisitor::visitBattleUnitsChanged(BattleUnitsChanged & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleUnitsChanged, pack.battleID, pack.changedStacks); +} + +void ApplyClientNetPackVisitor::visitBattleObstaclesChanged(BattleObstaclesChanged & pack) +{ + //inform interfaces about removed obstacles + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleObstaclesChanged, pack.battleID, pack.changes); +} + +void ApplyClientNetPackVisitor::visitCatapultAttack(CatapultAttack & pack) +{ + //inform interfaces about catapult attack + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::battleCatapultAttacked, pack.battleID, pack); +} + +void ApplyClientNetPackVisitor::visitEndAction(EndAction & pack) +{ + callBattleInterfaceIfPresentForBothSides(cl, pack.battleID, &IBattleEventsReceiver::actionFinished, pack.battleID, *cl.currentBattleAction); + cl.currentBattleAction.reset(); +} + +void ApplyClientNetPackVisitor::visitPackageApplied(PackageApplied & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::requestRealized, &pack); + if(!CClient::waitingRequest.tryRemovingElement(pack.requestID)) + logNetwork->warn("Surprising server message! PackageApplied for unknown requestID!"); +} + +void ApplyClientNetPackVisitor::visitSystemMessage(SystemMessage & pack) +{ + std::ostringstream str; + str << "System message: " << pack.text; + + logNetwork->error(str.str()); // usually used to receive error messages from server + if(LOCPLINT && !settings["session"]["hideSystemMessages"].Bool()) + LOCPLINT->cingconsole->print(str.str()); +} + +void ApplyClientNetPackVisitor::visitPlayerBlocked(PlayerBlocked & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::playerBlocked, pack.reason, pack.startOrEnd == PlayerBlocked::BLOCKADE_STARTED); +} + +void ApplyClientNetPackVisitor::visitPlayerStartsTurn(PlayerStartsTurn & pack) +{ + logNetwork->debug("Server gives turn to %s", pack.player.toString()); + + callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, pack.player); + callOnlyThatInterface(cl, pack.player, &CGameInterface::yourTurn, pack.queryID); +} + +void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) +{ + logNetwork->debug("Server ends turn of %s", pack.player.toString()); + + callAllInterfaces(cl, &IGameEventsReceiver::playerEndsTurn, pack.player); +} + +void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) +{ + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); +} + +void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) +{ + logNetwork->debug("pack.player %s sends a message: %s", pack.player.toString(), pack.text); + + std::ostringstream str; + if(pack.player.isSpectator()) + str << "Spectator: " << pack.text; + else + str << cl.getPlayerState(pack.player)->nodeName() <<": " << pack.text; + if(LOCPLINT) + LOCPLINT->cingconsole->print(str.str()); +} + +void ApplyClientNetPackVisitor::visitAdvmapSpellCast(AdvmapSpellCast & pack) +{ + cl.invalidatePaths(); + auto caster = cl.getHero(pack.casterID); + if(caster) + //consider notifying other interfaces that see hero? + callInterfaceIfPresent(cl, caster->getOwner(), &IGameEventsReceiver::advmapSpellCast, caster, pack.spellID); + else + logNetwork->error("Invalid hero instance"); +} + +void ApplyClientNetPackVisitor::visitShowWorldViewEx(ShowWorldViewEx & pack) +{ + callOnlyThatInterface(cl, pack.player, &CGameInterface::showWorldViewEx, pack.objectPositions, pack.showTerrain); +} + +void ApplyClientNetPackVisitor::visitOpenWindow(OpenWindow & pack) +{ + switch(pack.window) + { + case EOpenWindowMode::RECRUITMENT_FIRST: + case EOpenWindowMode::RECRUITMENT_ALL: + { + const CGDwelling *dw = dynamic_cast(cl.getObj(ObjectInstanceID(pack.object))); + const CArmedInstance *dst = dynamic_cast(cl.getObj(ObjectInstanceID(pack.visitor))); + callInterfaceIfPresent(cl, dst->tempOwner, &IGameEventsReceiver::showRecruitmentDialog, dw, dst, pack.window == EOpenWindowMode::RECRUITMENT_FIRST ? 0 : -1, pack.queryID); + } + break; + case EOpenWindowMode::SHIPYARD_WINDOW: + { + assert(pack.queryID == QueryID::NONE); + const IShipyard *sy = IShipyard::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + callInterfaceIfPresent(cl, sy->getObject()->getOwner(), &IGameEventsReceiver::showShipyardDialog, sy); + } + break; + case EOpenWindowMode::THIEVES_GUILD: + { + assert(pack.queryID == QueryID::NONE); + //displays Thieves' Guild window (when hero enters Den of Thieves) + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showThievesGuildWindow, obj); + } + break; + case EOpenWindowMode::UNIVERSITY_WINDOW: + { + //displays University window (when hero enters University on adventure map) + const IMarket *market = IMarket::castFrom(cl.getObj(ObjectInstanceID(pack.object))); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showUniversityWindow, market, hero, pack.queryID); + } + break; + case EOpenWindowMode::MARKET_WINDOW: + { + //displays Thieves' Guild window (when hero enters Den of Thieves) + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + const IMarket *market = IMarket::castFrom(obj); + callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showMarketWindow, market, hero, pack.queryID); + } + break; + case EOpenWindowMode::HILL_FORT_WINDOW: + { + assert(pack.queryID == QueryID::NONE); + //displays Hill fort window + const CGObjectInstance *obj = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, cl.getTile(obj->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::showHillFortWindow, obj, hero); + } + break; + case EOpenWindowMode::PUZZLE_MAP: + { + assert(pack.queryID == QueryID::NONE); + const CGHeroInstance *hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->getOwner(), &IGameEventsReceiver::showPuzzleMap); + } + break; + case EOpenWindowMode::TAVERN_WINDOW: + { + const CGObjectInstance *obj1 = cl.getObj(ObjectInstanceID(pack.object)); + const CGHeroInstance * hero = cl.getHero(ObjectInstanceID(pack.visitor)); + callInterfaceIfPresent(cl, hero->tempOwner, &IGameEventsReceiver::showTavernWindow, obj1, hero, pack.queryID); + } + break; + } +} + +void ApplyClientNetPackVisitor::visitCenterView(CenterView & pack) +{ + callInterfaceIfPresent(cl, pack.player, &IGameEventsReceiver::centerView, pack.pos, pack.focusTime); +} + +void ApplyClientNetPackVisitor::visitNewObject(NewObject & pack) +{ + cl.invalidatePaths(); + + const CGObjectInstance *obj = cl.getObj(pack.createdObjectID); + if(CGI->mh) + CGI->mh->onObjectFadeIn(obj, pack.initiator); + + for(auto i=cl.playerint.begin(); i!=cl.playerint.end(); i++) + { + if(gs.isVisible(obj, i->first)) + i->second->newObject(obj); + } + CGI->mh->waitForOngoingAnimations(); +} + +void ApplyClientNetPackVisitor::visitSetAvailableArtifacts(SetAvailableArtifacts & pack) +{ + if(pack.id < 0) //artifact merchants globally + { + callAllInterfaces(cl, &IGameEventsReceiver::availableArtifactsChanged, nullptr); + } + else + { + const CGBlackMarket *bm = dynamic_cast(cl.getObj(ObjectInstanceID(pack.id))); + assert(bm); + callInterfaceIfPresent(cl, cl.getTile(bm->visitablePos())->visitableObjects.back()->tempOwner, &IGameEventsReceiver::availableArtifactsChanged, bm); + } +} + + +void ApplyClientNetPackVisitor::visitEntitiesChanged(EntitiesChanged & pack) +{ + cl.invalidatePaths(); +} diff --git a/client/PlayerLocalState.cpp b/client/PlayerLocalState.cpp index 6ace13258..3d89db499 100644 --- a/client/PlayerLocalState.cpp +++ b/client/PlayerLocalState.cpp @@ -1,271 +1,271 @@ -/* - * PlayerLocalState.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 "PlayerLocalState.h" - -#include "../CCallback.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/pathfinder/CGPathNode.h" -#include "CPlayerInterface.h" -#include "adventureMap/AdventureMapInterface.h" - -PlayerLocalState::PlayerLocalState(CPlayerInterface & owner) - : owner(owner) - , currentSelection(nullptr) -{ -} - -void PlayerLocalState::saveHeroPaths(std::map & pathsMap) -{ - for(auto & p : paths) - { - if(p.second.nodes.size()) - pathsMap[p.first] = p.second.endPos(); - else - logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated()); - } -} - -void PlayerLocalState::loadHeroPaths(std::map & pathsMap) -{ - if(owner.cb) - { - for(auto & p : pathsMap) - { - CGPath path; - owner.cb->getPathsInfo(p.first)->getPath(path, p.second); - paths[p.first] = path; - logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size()); - } - } -} - -void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path) -{ - paths[h] = path; -} - -const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const -{ - assert(hasPath(h)); - return paths.at(h); -} - -bool PlayerLocalState::hasPath(const CGHeroInstance * h) const -{ - return paths.count(h) > 0; -} - -bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination) -{ - CGPath path; - if(!owner.cb->getPathsInfo(h)->getPath(path, destination)) - { - paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired) - return false; - } - - setPath(h, path); - return true; -} - -void PlayerLocalState::removeLastNode(const CGHeroInstance * h) -{ - assert(hasPath(h)); - if(!hasPath(h)) - return; - - auto & path = paths[h]; - path.nodes.pop_back(); - if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path - erasePath(h); -} - -void PlayerLocalState::erasePath(const CGHeroInstance * h) -{ - paths.erase(h); - adventureInt->onHeroChanged(h); -} - -void PlayerLocalState::verifyPath(const CGHeroInstance * h) -{ - if(!hasPath(h)) - return; - setPath(h, getPath(h).endPos()); -} - -const CGHeroInstance * PlayerLocalState::getCurrentHero() const -{ - if(currentSelection && currentSelection->ID == Obj::HERO) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero) -{ - bool currentHeroFound = false; - const CGHeroInstance * firstSuitable = nullptr; - const CGHeroInstance * nextSuitable = nullptr; - - for(const auto * hero : getWanderingHeroes()) - { - if (hero == currentHero) - { - currentHeroFound = true; - continue; - } - - if (isHeroSleeping(hero)) - continue; - - if (hero->movementPointsRemaining() == 0) - continue; - - if (!firstSuitable) - firstSuitable = hero; - - if (!nextSuitable && currentHeroFound) - nextSuitable = hero; - } - - // if we found suitable hero after currently selected hero -> return this hero - if (nextSuitable) - return nextSuitable; - - // othervice -> loop over and return first suitable hero in the list (or null if none) - return firstSuitable; -} - -const CGTownInstance * PlayerLocalState::getCurrentTown() const -{ - if(currentSelection && currentSelection->ID == Obj::TOWN) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -const CArmedInstance * PlayerLocalState::getCurrentArmy() const -{ - if(currentSelection) - return dynamic_cast(currentSelection); - else - return nullptr; -} - -void PlayerLocalState::setSelection(const CArmedInstance * selection) -{ - if (currentSelection == selection) - return; - - currentSelection = selection; - - if (selection) - adventureInt->onSelectionChanged(selection); -} - -bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const -{ - return vstd::contains(sleepingHeroes, hero); -} - -void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - assert(!vstd::contains(sleepingHeroes, hero)); - - sleepingHeroes.push_back(hero); -} - -void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - assert(vstd::contains(sleepingHeroes, hero)); - - vstd::erase(sleepingHeroes, hero); -} - -const std::vector & PlayerLocalState::getWanderingHeroes() -{ - return wanderingHeroes; -} - -const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index) -{ - if(index < wanderingHeroes.size()) - return wanderingHeroes[index]; - return nullptr; -} - -void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) -{ - assert(hero); - assert(!vstd::contains(wanderingHeroes, hero)); - wanderingHeroes.push_back(hero); -} - -void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) -{ - assert(hero); - assert(vstd::contains(wanderingHeroes, hero)); - - if (hero == currentSelection) - { - auto const * nextHero = getNextWanderingHero(hero); - setSelection(nextHero); - } - - vstd::erase(wanderingHeroes, hero); - vstd::erase(sleepingHeroes, hero); - - if (currentSelection == nullptr && !wanderingHeroes.empty()) - setSelection(wanderingHeroes.front()); - - if (currentSelection == nullptr && !ownedTowns.empty()) - setSelection(ownedTowns.front()); -} - -const std::vector & PlayerLocalState::getOwnedTowns() -{ - return ownedTowns; -} - -const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index) -{ - if(index < ownedTowns.size()) - return ownedTowns[index]; - return nullptr; -} - -void PlayerLocalState::addOwnedTown(const CGTownInstance * town) -{ - assert(town); - assert(!vstd::contains(ownedTowns, town)); - ownedTowns.push_back(town); -} - -void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) -{ - assert(town); - assert(vstd::contains(ownedTowns, town)); - vstd::erase(ownedTowns, town); - - if (town == currentSelection) - setSelection(nullptr); - - if (currentSelection == nullptr && !wanderingHeroes.empty()) - setSelection(wanderingHeroes.front()); - - if (currentSelection == nullptr && !ownedTowns.empty()) - setSelection(ownedTowns.front()); -} +/* + * PlayerLocalState.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 "PlayerLocalState.h" + +#include "../CCallback.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/pathfinder/CGPathNode.h" +#include "CPlayerInterface.h" +#include "adventureMap/AdventureMapInterface.h" + +PlayerLocalState::PlayerLocalState(CPlayerInterface & owner) + : owner(owner) + , currentSelection(nullptr) +{ +} + +void PlayerLocalState::saveHeroPaths(std::map & pathsMap) +{ + for(auto & p : paths) + { + if(p.second.nodes.size()) + pathsMap[p.first] = p.second.endPos(); + else + logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated()); + } +} + +void PlayerLocalState::loadHeroPaths(std::map & pathsMap) +{ + if(owner.cb) + { + for(auto & p : pathsMap) + { + CGPath path; + owner.cb->getPathsInfo(p.first)->getPath(path, p.second); + paths[p.first] = path; + logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size()); + } + } +} + +void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path) +{ + paths[h] = path; +} + +const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const +{ + assert(hasPath(h)); + return paths.at(h); +} + +bool PlayerLocalState::hasPath(const CGHeroInstance * h) const +{ + return paths.count(h) > 0; +} + +bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination) +{ + CGPath path; + if(!owner.cb->getPathsInfo(h)->getPath(path, destination)) + { + paths.erase(h); //invalidate previously possible path if selected (before other hero blocked only path / fly spell expired) + return false; + } + + setPath(h, path); + return true; +} + +void PlayerLocalState::removeLastNode(const CGHeroInstance * h) +{ + assert(hasPath(h)); + if(!hasPath(h)) + return; + + auto & path = paths[h]; + path.nodes.pop_back(); + if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path + erasePath(h); +} + +void PlayerLocalState::erasePath(const CGHeroInstance * h) +{ + paths.erase(h); + adventureInt->onHeroChanged(h); +} + +void PlayerLocalState::verifyPath(const CGHeroInstance * h) +{ + if(!hasPath(h)) + return; + setPath(h, getPath(h).endPos()); +} + +const CGHeroInstance * PlayerLocalState::getCurrentHero() const +{ + if(currentSelection && currentSelection->ID == Obj::HERO) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero) +{ + bool currentHeroFound = false; + const CGHeroInstance * firstSuitable = nullptr; + const CGHeroInstance * nextSuitable = nullptr; + + for(const auto * hero : getWanderingHeroes()) + { + if (hero == currentHero) + { + currentHeroFound = true; + continue; + } + + if (isHeroSleeping(hero)) + continue; + + if (hero->movementPointsRemaining() == 0) + continue; + + if (!firstSuitable) + firstSuitable = hero; + + if (!nextSuitable && currentHeroFound) + nextSuitable = hero; + } + + // if we found suitable hero after currently selected hero -> return this hero + if (nextSuitable) + return nextSuitable; + + // othervice -> loop over and return first suitable hero in the list (or null if none) + return firstSuitable; +} + +const CGTownInstance * PlayerLocalState::getCurrentTown() const +{ + if(currentSelection && currentSelection->ID == Obj::TOWN) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +const CArmedInstance * PlayerLocalState::getCurrentArmy() const +{ + if(currentSelection) + return dynamic_cast(currentSelection); + else + return nullptr; +} + +void PlayerLocalState::setSelection(const CArmedInstance * selection) +{ + if (currentSelection == selection) + return; + + currentSelection = selection; + + if (selection) + adventureInt->onSelectionChanged(selection); +} + +bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const +{ + return vstd::contains(sleepingHeroes, hero); +} + +void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + assert(!vstd::contains(sleepingHeroes, hero)); + + sleepingHeroes.push_back(hero); +} + +void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + assert(vstd::contains(sleepingHeroes, hero)); + + vstd::erase(sleepingHeroes, hero); +} + +const std::vector & PlayerLocalState::getWanderingHeroes() +{ + return wanderingHeroes; +} + +const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index) +{ + if(index < wanderingHeroes.size()) + return wanderingHeroes[index]; + return nullptr; +} + +void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero) +{ + assert(hero); + assert(!vstd::contains(wanderingHeroes, hero)); + wanderingHeroes.push_back(hero); +} + +void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero) +{ + assert(hero); + assert(vstd::contains(wanderingHeroes, hero)); + + if (hero == currentSelection) + { + auto const * nextHero = getNextWanderingHero(hero); + setSelection(nextHero); + } + + vstd::erase(wanderingHeroes, hero); + vstd::erase(sleepingHeroes, hero); + + if (currentSelection == nullptr && !wanderingHeroes.empty()) + setSelection(wanderingHeroes.front()); + + if (currentSelection == nullptr && !ownedTowns.empty()) + setSelection(ownedTowns.front()); +} + +const std::vector & PlayerLocalState::getOwnedTowns() +{ + return ownedTowns; +} + +const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index) +{ + if(index < ownedTowns.size()) + return ownedTowns[index]; + return nullptr; +} + +void PlayerLocalState::addOwnedTown(const CGTownInstance * town) +{ + assert(town); + assert(!vstd::contains(ownedTowns, town)); + ownedTowns.push_back(town); +} + +void PlayerLocalState::removeOwnedTown(const CGTownInstance * town) +{ + assert(town); + assert(vstd::contains(ownedTowns, town)); + vstd::erase(ownedTowns, town); + + if (town == currentSelection) + setSelection(nullptr); + + if (currentSelection == nullptr && !wanderingHeroes.empty()) + setSelection(wanderingHeroes.front()); + + if (currentSelection == nullptr && !ownedTowns.empty()) + setSelection(ownedTowns.front()); +} diff --git a/client/PlayerLocalState.h b/client/PlayerLocalState.h index e88ee6ca3..57e4551a4 100644 --- a/client/PlayerLocalState.h +++ b/client/PlayerLocalState.h @@ -1,111 +1,111 @@ -/* - * PlayerLocalState.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CGTownInstance; -class CArmedInstance; -struct CGPath; -class int3; - -VCMI_LIB_NAMESPACE_END - -class CPlayerInterface; - -/// Class that contains potentially serializeable state of a local player -class PlayerLocalState -{ - CPlayerInterface & owner; - - /// Currently selected object, can be town, hero or null - const CArmedInstance * currentSelection; - - std::map paths; //maps hero => selected path in adventure map - std::vector sleepingHeroes; //if hero is in here, he's sleeping - std::vector wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones) - std::vector ownedTowns; //our towns on the adventure map - - void saveHeroPaths(std::map & paths); - void loadHeroPaths(std::map & paths); - -public: - struct SpellbookLastSetting - { - //on which page we left spellbook - int spellbookLastPageBattle = 0; - int spellbokLastPageAdvmap = 0; - int spellbookLastTabBattle = 4; - int spellbookLastTabAdvmap = 4; - - template - void serialize(Handler & h, const int version) - { - h & spellbookLastPageBattle; - h & spellbokLastPageAdvmap; - h & spellbookLastTabBattle; - h & spellbookLastTabAdvmap; - } - } spellbookSettings; - - explicit PlayerLocalState(CPlayerInterface & owner); - - bool isHeroSleeping(const CGHeroInstance * hero) const; - void setHeroAsleep(const CGHeroInstance * hero); - void setHeroAwaken(const CGHeroInstance * hero); - - const std::vector & getOwnedTowns(); - const CGTownInstance * getOwnedTown(size_t index); - void addOwnedTown(const CGTownInstance * hero); - void removeOwnedTown(const CGTownInstance * hero); - - const std::vector & getWanderingHeroes(); - const CGHeroInstance * getWanderingHero(size_t index); - const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); - void addWanderingHero(const CGHeroInstance * hero); - void removeWanderingHero(const CGHeroInstance * hero); - - void setPath(const CGHeroInstance * h, const CGPath & path); - bool setPath(const CGHeroInstance * h, const int3 & destination); - - const CGPath & getPath(const CGHeroInstance * h) const; - bool hasPath(const CGHeroInstance * h) const; - - void removeLastNode(const CGHeroInstance * h); - void erasePath(const CGHeroInstance * h); - void verifyPath(const CGHeroInstance * h); - - /// Returns currently selected object - const CGHeroInstance * getCurrentHero() const; - const CGTownInstance * getCurrentTown() const; - const CArmedInstance * getCurrentArmy() const; - - /// Changes currently selected object - void setSelection(const CArmedInstance *sel); - - template - void serialize(Handler & h, int version) - { - //WARNING: this code is broken and not used. See CClient::loadGame - std::map pathsMap; //hero -> dest - if(h.saving) - saveHeroPaths(pathsMap); - - h & pathsMap; - - if(!h.saving) - loadHeroPaths(pathsMap); - - h & ownedTowns; - h & wanderingHeroes; - h & sleepingHeroes; - } -}; +/* + * PlayerLocalState.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +struct CGPath; +class int3; + +VCMI_LIB_NAMESPACE_END + +class CPlayerInterface; + +/// Class that contains potentially serializeable state of a local player +class PlayerLocalState +{ + CPlayerInterface & owner; + + /// Currently selected object, can be town, hero or null + const CArmedInstance * currentSelection; + + std::map paths; //maps hero => selected path in adventure map + std::vector sleepingHeroes; //if hero is in here, he's sleeping + std::vector wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones) + std::vector ownedTowns; //our towns on the adventure map + + void saveHeroPaths(std::map & paths); + void loadHeroPaths(std::map & paths); + +public: + struct SpellbookLastSetting + { + //on which page we left spellbook + int spellbookLastPageBattle = 0; + int spellbokLastPageAdvmap = 0; + int spellbookLastTabBattle = 4; + int spellbookLastTabAdvmap = 4; + + template + void serialize(Handler & h, const int version) + { + h & spellbookLastPageBattle; + h & spellbokLastPageAdvmap; + h & spellbookLastTabBattle; + h & spellbookLastTabAdvmap; + } + } spellbookSettings; + + explicit PlayerLocalState(CPlayerInterface & owner); + + bool isHeroSleeping(const CGHeroInstance * hero) const; + void setHeroAsleep(const CGHeroInstance * hero); + void setHeroAwaken(const CGHeroInstance * hero); + + const std::vector & getOwnedTowns(); + const CGTownInstance * getOwnedTown(size_t index); + void addOwnedTown(const CGTownInstance * hero); + void removeOwnedTown(const CGTownInstance * hero); + + const std::vector & getWanderingHeroes(); + const CGHeroInstance * getWanderingHero(size_t index); + const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero); + void addWanderingHero(const CGHeroInstance * hero); + void removeWanderingHero(const CGHeroInstance * hero); + + void setPath(const CGHeroInstance * h, const CGPath & path); + bool setPath(const CGHeroInstance * h, const int3 & destination); + + const CGPath & getPath(const CGHeroInstance * h) const; + bool hasPath(const CGHeroInstance * h) const; + + void removeLastNode(const CGHeroInstance * h); + void erasePath(const CGHeroInstance * h); + void verifyPath(const CGHeroInstance * h); + + /// Returns currently selected object + const CGHeroInstance * getCurrentHero() const; + const CGTownInstance * getCurrentTown() const; + const CArmedInstance * getCurrentArmy() const; + + /// Changes currently selected object + void setSelection(const CArmedInstance *sel); + + template + void serialize(Handler & h, int version) + { + //WARNING: this code is broken and not used. See CClient::loadGame + std::map pathsMap; //hero -> dest + if(h.saving) + saveHeroPaths(pathsMap); + + h & pathsMap; + + if(!h.saving) + loadHeroPaths(pathsMap); + + h & ownedTowns; + h & wanderingHeroes; + h & sleepingHeroes; + } +}; diff --git a/client/StdInc.cpp b/client/StdInc.cpp index c8f4ddf05..c17377322 100644 --- a/client/StdInc.cpp +++ b/client/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" \ No newline at end of file diff --git a/client/StdInc.h b/client/StdInc.h index 1f4e8bafc..90fdbd301 100644 --- a/client/StdInc.h +++ b/client/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 376401437..f35ff81bf 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -1,889 +1,889 @@ -/* - * AdventureMapInterface.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 "AdventureMapInterface.h" - -#include "AdventureOptions.h" -#include "AdventureState.h" -#include "CInGameConsole.h" -#include "CMinimap.h" -#include "CList.h" -#include "CInfoBar.h" -#include "MapAudioPlayer.h" -#include "TurnTimerWidget.h" -#include "AdventureMapWidget.h" -#include "AdventureMapShortcuts.h" - -#include "../mapView/mapHandler.h" -#include "../mapView/MapView.h" -#include "../windows/InfoWindows.h" -#include "../widgets/RadialMenu.h" -#include "../CGameInfo.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../render/Canvas.h" -#include "../CMT.h" -#include "../PlayerLocalState.h" -#include "../CPlayerInterface.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/StartInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/mapping/CMapDefines.h" -#include "../../lib/pathfinder/CGPathNode.h" - -std::shared_ptr adventureInt; - -AdventureMapInterface::AdventureMapInterface(): - mapAudio(new MapAudioPlayer()), - spellBeingCasted(nullptr), - scrollingWasActive(false), - scrollingWasBlocked(false), - backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - shortcuts = std::make_shared(*this); - - widget = std::make_shared(shortcuts); - shortcuts->setState(EAdventureState::MAKING_TURN); - widget->getMapView()->onViewMapActivated(); - - if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled()) - watches = std::make_shared(); - - addUsedEvents(KEYBOARD | TIME); -} - -void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel) -{ - shortcuts->onMapViewMoved(visibleArea, mapLevel); - widget->getMinimap()->onMapViewMoved(visibleArea, mapLevel); - widget->onMapViewMoved(visibleArea, mapLevel); -} - -void AdventureMapInterface::onAudioResumed() -{ - mapAudio->onAudioResumed(); -} - -void AdventureMapInterface::onAudioPaused() -{ - mapAudio->onAudioPaused(); -} - -void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero) -{ - if (shortcuts->optionMapViewActive()) - { - widget->getInfoBar()->popAll(); - widget->getInfoBar()->showSelection(); - } -} - -void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) -{ - widget->getHeroList()->updateElement(h); - - if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents()) - widget->getInfoBar()->showSelection(); - - widget->updateActiveState(); -} - -void AdventureMapInterface::onTownChanged(const CGTownInstance * town) -{ - widget->getTownList()->updateElement(town); - - if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents()) - widget->getInfoBar()->showSelection(); -} - -void AdventureMapInterface::showInfoBoxMessage(const std::vector & components, std::string message, int timer) -{ - widget->getInfoBar()->pushComponents(components, message, timer); -} - -void AdventureMapInterface::activate() -{ - CIntObject::activate(); - - adjustActiveness(); - - screenBuf = screen; - - if(LOCPLINT) - { - LOCPLINT->cingconsole->activate(); - LOCPLINT->cingconsole->pos = this->pos; - } - - GH.fakeMouseMove(); //to restore the cursor - - // workaround for an edge case: - // if player unequips Angel Wings / Boots of Levitation of currently active hero - // game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero - if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero()) - LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero()); -} - -void AdventureMapInterface::deactivate() -{ - CIntObject::deactivate(); - CCS->curh->set(Cursor::Map::POINTER); - - if(LOCPLINT) - LOCPLINT->cingconsole->deactivate(); -} - -void AdventureMapInterface::showAll(Canvas & to) -{ - CIntObject::showAll(to); - dim(to); - LOCPLINT->cingconsole->show(to); -} - -void AdventureMapInterface::show(Canvas & to) -{ - CIntObject::show(to); - dim(to); - LOCPLINT->cingconsole->show(to); -} - -void AdventureMapInterface::dim(Canvas & to) -{ - for (auto window : GH.windows().findWindows()) - { - if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) - { - Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); - ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); - if(backgroundDimLevel > 0) - to.drawColorBlended(targetRect, colorToFill); - return; - } - } -} - -void AdventureMapInterface::tick(uint32_t msPassed) -{ - handleMapScrollingUpdate(msPassed); - - // we want animations to be active during enemy turn but map itself to be non-interactive - // so call timer update directly on inactive element - widget->getMapView()->tick(msPassed); -} - -void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) -{ - /// Width of window border, in pixels, that triggers map scrolling - static constexpr int32_t borderScrollWidth = 15; - - int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float(); - int32_t scrollDistance = scrollSpeedPixels * timePassed / 1000; - - Point cursorPosition = GH.getCursorPosition(); - Point scrollDirection; - - if (cursorPosition.x < borderScrollWidth) - scrollDirection.x = -1; - - if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth) - scrollDirection.x = +1; - - if (cursorPosition.y < borderScrollWidth) - scrollDirection.y = -1; - - if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth) - scrollDirection.y = +1; - - Point scrollDelta = scrollDirection * scrollDistance; - - bool cursorInScrollArea = scrollDelta != Point(0,0); - bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; - bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); - - if (!scrollingWasActive && scrollingBlocked) - { - scrollingWasBlocked = true; - return; - } - - if (!cursorInScrollArea && scrollingWasBlocked) - { - scrollingWasBlocked = false; - return; - } - - if (scrollingActive) - widget->getMapView()->onMapScrolled(scrollDelta); - - if (!scrollingActive && !scrollingWasActive) - return; - - if(scrollDelta.x > 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::SCROLL_EAST); - } - if(scrollDelta.x < 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::SCROLL_WEST); - } - - if (scrollDelta.x == 0) - { - if(scrollDelta.y < 0) - CCS->curh->set(Cursor::Map::SCROLL_NORTH); - if(scrollDelta.y > 0) - CCS->curh->set(Cursor::Map::SCROLL_SOUTH); - if(scrollDelta.y == 0) - CCS->curh->set(Cursor::Map::POINTER); - } - - scrollingWasActive = scrollingActive; -} - -void AdventureMapInterface::centerOnTile(int3 on) -{ - widget->getMapView()->onCenteredTile(on); -} - -void AdventureMapInterface::centerOnObject(const CGObjectInstance * obj) -{ - widget->getMapView()->onCenteredObject(obj); -} - -void AdventureMapInterface::keyPressed(EShortcut key) -{ - if (key == EShortcut::GLOBAL_CANCEL && spellBeingCasted) - hotkeyAbortCastingMode(); - - //fake mouse use to trigger onTileHovered() - GH.fakeMouseMove(); -} - -void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) -{ - assert(sel); - - widget->getInfoBar()->popAll(); - mapAudio->onSelectionChanged(sel); - bool centerView = !settings["session"]["autoSkip"].Bool(); - - if (centerView) - centerOnObject(sel); - - if(sel->ID==Obj::TOWN) - { - auto town = dynamic_cast(sel); - - widget->getInfoBar()->showTownSelection(town); - widget->getTownList()->select(town); - widget->getHeroList()->select(nullptr); - onHeroChanged(nullptr); - } - else //hero selected - { - auto hero = dynamic_cast(sel); - - widget->getInfoBar()->showHeroSelection(hero); - widget->getHeroList()->select(hero); - widget->getTownList()->select(nullptr); - - LOCPLINT->localState->verifyPath(hero); - onHeroChanged(hero); - } - - widget->updateActiveState(); - widget->getHeroList()->redraw(); - widget->getTownList()->redraw(); -} - -void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) -{ - if (positions) - widget->getMinimap()->updateTiles(*positions); - else - widget->getMinimap()->update(); -} - -void AdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID) -{ - backgroundDimLevel = 255; - - onCurrentPlayerChanged(playerID); - setState(EAdventureState::HOTSEAT_WAIT); -} - -void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman) -{ - if(settings["session"]["spectate"].Bool()) - return; - - mapAudio->onEnemyTurnStarted(); - widget->getMinimap()->setAIRadar(!isHuman); - widget->getInfoBar()->startEnemyTurn(playerID); - setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); -} - -void AdventureMapInterface::setState(EAdventureState state) -{ - shortcuts->setState(state); - adjustActiveness(); - widget->updateActiveState(); -} - -void AdventureMapInterface::adjustActiveness() -{ - bool widgetMustBeActive = isActive() && shortcuts->optionSidePanelActive(); - bool mapViewMustBeActive = isActive() && (shortcuts->optionMapViewActive()); - - widget->setInputEnabled(widgetMustBeActive); - widget->getMapView()->setInputEnabled(mapViewMustBeActive); -} - -void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID) -{ - LOCPLINT->localState->setSelection(nullptr); - - if (playerID == currentPlayerID) - return; - - currentPlayerID = playerID; - widget->setPlayer(playerID); -} - -void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) -{ - backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); - - onCurrentPlayerChanged(playerID); - - setState(EAdventureState::MAKING_TURN); - if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool()) - { - widget->getMinimap()->setAIRadar(false); - widget->getInfoBar()->showSelection(); - } - - widget->getHeroList()->updateWidget(); - widget->getTownList()->updateWidget(); - - const CGHeroInstance * heroToSelect = nullptr; - - // find first non-sleeping hero - for (auto hero : LOCPLINT->localState->getWanderingHeroes()) - { - if (!LOCPLINT->localState->isHeroSleeping(hero)) - { - heroToSelect = hero; - break; - } - } - - //select first hero if available. - if (heroToSelect != nullptr) - { - LOCPLINT->localState->setSelection(heroToSelect); - } - else if (LOCPLINT->localState->getOwnedTowns().size()) - { - LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0)); - } - else - { - LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0)); - } - - //show new day animation and sound on infobar, except for 1st day of the game - if (LOCPLINT->cb->getDate(Date::DAY) != 1) - widget->getInfoBar()->showDate(); - - onHeroChanged(nullptr); - Canvas canvas = Canvas::createFromSurface(screen); - showAll(canvas); - mapAudio->onPlayerTurnStarted(); - - if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) - { - if(auto iw = GH.windows().topWindow()) - iw->close(); - - hotkeyEndingTurn(); - } -} - -void AdventureMapInterface::hotkeyEndingTurn() -{ - if(settings["session"]["spectate"].Bool()) - return; - - if(!settings["general"]["startTurnAutosave"].Bool()) - { - LOCPLINT->performAutosave(); - } - - LOCPLINT->makingTurn = false; - LOCPLINT->cb->endTurn(); - - mapAudio->onPlayerTurnEnded(); -} - -const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) -{ - std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos); //blocking objects at tile - - if (bobjs.empty()) - return nullptr; - - return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder); -} - -void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - //FIXME: this line breaks H3 behavior for Dimension Door - if(!LOCPLINT->cb->isVisible(mapPos)) - return; - if(!LOCPLINT->makingTurn) - return; - - const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos); - - const CGObjectInstance *topBlocking = getActiveObject(mapPos); - - int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - if(spellBeingCasted) - { - assert(shortcuts->optionSpellcasting()); - - if (!isInScreenRange(selPos, mapPos)) - return; - - const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos); - - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: //Scuttle Boat - if(topBlocking && topBlocking->ID == Obj::BOAT) - performSpellcasting(mapPos); - break; - case SpellID::DIMENSION_DOOR: - if(!tile || tile->isClear(heroTile)) - performSpellcasting(mapPos); - break; - } - return; - } - //check if we can select this object - bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; - canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES; - - bool isHero = false; - if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town) - { - if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked - LOCPLINT->openTownWindow(static_cast(topBlocking)); - else if(canSelect) - LOCPLINT->localState->setSelection(static_cast(topBlocking)); - } - else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected - { - isHero = true; - - const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos); - if(currentHero == topBlocking) //clicked selected hero - { - LOCPLINT->openHeroWindow(currentHero); - return; - } - else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile - { - LOCPLINT->localState->setSelection(static_cast(topBlocking)); - return; - } - else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise - { - if(LOCPLINT->localState->hasPath(currentHero) && - LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving - { - assert(!CGI->mh->hasOngoingAnimations()); - if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0) - LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); - return; - } - else - { - if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected) - { - if(canSelect) - LOCPLINT->localState->setSelection(static_cast(topBlocking)); - } - else //remove old path and find a new one if we clicked on accessible tile - { - LOCPLINT->localState->setPath(currentHero, mapPos); - onHeroChanged(currentHero); - } - } - } - } //end of hero is selected "case" - else - { - throw std::runtime_error("Nothing is selected..."); - } - - const auto shipyard = ourInaccessibleShipyard(topBlocking); - if(isHero && shipyard != nullptr) - { - LOCPLINT->showShipyardDialogOrProblemPopup(shipyard); - } -} - -void AdventureMapInterface::onTileHovered(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - //may occur just at the start of game (fake move before full intiialization) - if(!LOCPLINT->localState->getCurrentArmy()) - return; - - if(!LOCPLINT->cb->isVisible(mapPos)) - { - CCS->curh->set(Cursor::Map::POINTER); - GH.statusbar()->clear(); - return; - } - auto objRelations = PlayerRelations::ALLIES; - const CGObjectInstance *objAtTile = getActiveObject(mapPos); - if(objAtTile) - { - objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner); - std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID); - boost::replace_all(text,"\n"," "); - GH.statusbar()->write(text); - } - else - { - std::string hlp = CGI->mh->getTerrainDescr(mapPos, false); - GH.statusbar()->write(hlp); - } - - if(spellBeingCasted) - { - switch(spellBeingCasted->id) - { - case SpellID::SCUTTLE_BOAT: - { - int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - - if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos)) - CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); - else - CCS->curh->set(Cursor::Map::POINTER); - return; - } - case SpellID::DIMENSION_DOOR: - { - const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false); - int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); - if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos)) - CCS->curh->set(Cursor::Map::TELEPORT); - else - CCS->curh->set(Cursor::Map::POINTER); - return; - } - } - } - - if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown()) - { - if(objAtTile) - { - if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES) - CCS->curh->set(Cursor::Map::TOWN); - else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(Cursor::Map::POINTER); - } - else - CCS->curh->set(Cursor::Map::POINTER); - } - else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero()) - { - std::array cursorMove = { Cursor::Map::T1_MOVE, Cursor::Map::T2_MOVE, Cursor::Map::T3_MOVE, Cursor::Map::T4_MOVE, }; - std::array cursorAttack = { Cursor::Map::T1_ATTACK, Cursor::Map::T2_ATTACK, Cursor::Map::T3_ATTACK, Cursor::Map::T4_ATTACK, }; - std::array cursorSail = { Cursor::Map::T1_SAIL, Cursor::Map::T2_SAIL, Cursor::Map::T3_SAIL, Cursor::Map::T4_SAIL, }; - std::array cursorDisembark = { Cursor::Map::T1_DISEMBARK, Cursor::Map::T2_DISEMBARK, Cursor::Map::T3_DISEMBARK, Cursor::Map::T4_DISEMBARK, }; - std::array cursorExchange = { Cursor::Map::T1_EXCHANGE, Cursor::Map::T2_EXCHANGE, Cursor::Map::T3_EXCHANGE, Cursor::Map::T4_EXCHANGE, }; - std::array cursorVisit = { Cursor::Map::T1_VISIT, Cursor::Map::T2_VISIT, Cursor::Map::T3_VISIT, Cursor::Map::T4_VISIT, }; - std::array cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, }; - - const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos); - assert(pathNode); - - if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info - { - showMoveDetailsInStatusbar(*hero, *pathNode); - } - - int turns = pathNode->turns; - vstd::amin(turns, 3); - switch(pathNode->action) - { - case EPathNodeAction::NORMAL: - case EPathNodeAction::TELEPORT_NORMAL: - if(pathNode->layer == EPathfindingLayer::LAND) - CCS->curh->set(cursorMove[turns]); - else - CCS->curh->set(cursorSailVisit[turns]); - break; - - case EPathNodeAction::VISIT: - case EPathNodeAction::BLOCKING_VISIT: - case EPathNodeAction::TELEPORT_BLOCKING_VISIT: - if(objAtTile && objAtTile->ID == Obj::HERO) - { - if(LOCPLINT->localState->getCurrentArmy() == objAtTile) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(cursorExchange[turns]); - } - else if(pathNode->layer == EPathfindingLayer::LAND) - CCS->curh->set(cursorVisit[turns]); - else - CCS->curh->set(cursorSailVisit[turns]); - break; - - case EPathNodeAction::BATTLE: - case EPathNodeAction::TELEPORT_BATTLE: - CCS->curh->set(cursorAttack[turns]); - break; - - case EPathNodeAction::EMBARK: - CCS->curh->set(cursorSail[turns]); - break; - - case EPathNodeAction::DISEMBARK: - CCS->curh->set(cursorDisembark[turns]); - break; - - default: - if(objAtTile && objRelations != PlayerRelations::ENEMIES) - { - if(objAtTile->ID == Obj::TOWN) - CCS->curh->set(Cursor::Map::TOWN); - else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) - CCS->curh->set(Cursor::Map::HERO); - else - CCS->curh->set(Cursor::Map::POINTER); - } - else - CCS->curh->set(Cursor::Map::POINTER); - break; - } - } - - if(ourInaccessibleShipyard(objAtTile)) - { - CCS->curh->set(Cursor::Map::T1_SAIL); - } -} - -void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode) -{ - const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining(); - const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains; - const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0; - - std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns"); - - boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns)); - boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost)); - boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove)); - - GH.statusbar()->write(result); -} - -void AdventureMapInterface::onTileRightClicked(const int3 &mapPos) -{ - if(!shortcuts->optionMapViewActive()) - return; - - if(spellBeingCasted) - { - hotkeyAbortCastingMode(); - return; - } - - if(!LOCPLINT->cb->isVisible(mapPos)) - { - CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory - return; - } - - const CGObjectInstance * obj = getActiveObject(mapPos); - if(!obj) - { - // Bare or undiscovered terrain - const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos); - if(tile) - { - std::string hlp = CGI->mh->getTerrainDescr(mapPos, true); - CRClickPopup::createAndPush(hlp); - } - return; - } - - CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER); -} - -void AdventureMapInterface::enterCastingMode(const CSpell * sp) -{ - assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR); - spellBeingCasted = sp; - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = true; - - setState(EAdventureState::CASTING_SPELL); -} - -void AdventureMapInterface::exitCastingMode() -{ - assert(spellBeingCasted); - spellBeingCasted = nullptr; - setState(EAdventureState::MAKING_TURN); - - Settings config = settings.write["session"]["showSpellRange"]; - config->Bool() = false; -} - -void AdventureMapInterface::hotkeyAbortCastingMode() -{ - exitCastingMode(); - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled -} - -void AdventureMapInterface::performSpellcasting(const int3 & dest) -{ - SpellID id = spellBeingCasted->id; - exitCastingMode(); - LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest); -} - -Rect AdventureMapInterface::terrainAreaPixels() const -{ - return widget->getMapView()->pos; -} - -const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const -{ - const IShipyard *ret = IShipyard::castFrom(obj); - - if(!ret || - obj->tempOwner != currentPlayerID || - (CCS->curh->get() != Cursor::Map::T1_SAIL && CCS->curh->get() != Cursor::Map::POINTER)) - return nullptr; - - return ret; -} - -void AdventureMapInterface::hotkeyExitWorldView() -{ - setState(EAdventureState::MAKING_TURN); - widget->getMapView()->onViewMapActivated(); -} - -void AdventureMapInterface::openWorldView(int tileSize) -{ - setState(EAdventureState::WORLD_VIEW); - widget->getMapView()->onViewWorldActivated(tileSize); -} - -void AdventureMapInterface::openWorldView() -{ - openWorldView(11); -} - -void AdventureMapInterface::openWorldView(const std::vector& objectPositions, bool showTerrain) -{ - openWorldView(11); - widget->getMapView()->onViewSpellActivated(11, objectPositions, showTerrain); -} - -void AdventureMapInterface::hotkeyNextTown() -{ - widget->getTownList()->selectNext(); -} - -void AdventureMapInterface::hotkeySwitchMapLevel() -{ - widget->getMapView()->onMapLevelSwitched(); -} - -void AdventureMapInterface::hotkeyZoom(int delta) -{ - widget->getMapView()->onMapZoomLevelChanged(delta); -} - -void AdventureMapInterface::onScreenResize() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - // remember our activation state and reactive after reconstruction - // since othervice activate() calls for created elements will bypass virtual dispatch - // and will call directly CIntObject::activate() instead of dispatching virtual function call - bool widgetActive = isActive(); - - if (widgetActive) - deactivate(); - - widget.reset(); - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - widget = std::make_shared(shortcuts); - widget->getMapView()->onViewMapActivated(); - widget->setPlayer(currentPlayerID); - widget->updateActiveState(); - widget->getMinimap()->update(); - widget->getInfoBar()->showSelection(); - - if (LOCPLINT && LOCPLINT->localState->getCurrentArmy()) - widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); - - adjustActiveness(); - - if (widgetActive) - activate(); -} +/* + * AdventureMapInterface.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 "AdventureMapInterface.h" + +#include "AdventureOptions.h" +#include "AdventureState.h" +#include "CInGameConsole.h" +#include "CMinimap.h" +#include "CList.h" +#include "CInfoBar.h" +#include "MapAudioPlayer.h" +#include "TurnTimerWidget.h" +#include "AdventureMapWidget.h" +#include "AdventureMapShortcuts.h" + +#include "../mapView/mapHandler.h" +#include "../mapView/MapView.h" +#include "../windows/InfoWindows.h" +#include "../widgets/RadialMenu.h" +#include "../CGameInfo.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../render/Canvas.h" +#include "../CMT.h" +#include "../PlayerLocalState.h" +#include "../CPlayerInterface.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/StartInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/pathfinder/CGPathNode.h" + +std::shared_ptr adventureInt; + +AdventureMapInterface::AdventureMapInterface(): + mapAudio(new MapAudioPlayer()), + spellBeingCasted(nullptr), + scrollingWasActive(false), + scrollingWasBlocked(false), + backgroundDimLevel(settings["adventure"]["backgroundDimLevel"].Integer()) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + shortcuts = std::make_shared(*this); + + widget = std::make_shared(shortcuts); + shortcuts->setState(EAdventureState::MAKING_TURN); + widget->getMapView()->onViewMapActivated(); + + if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled()) + watches = std::make_shared(); + + addUsedEvents(KEYBOARD | TIME); +} + +void AdventureMapInterface::onMapViewMoved(const Rect & visibleArea, int mapLevel) +{ + shortcuts->onMapViewMoved(visibleArea, mapLevel); + widget->getMinimap()->onMapViewMoved(visibleArea, mapLevel); + widget->onMapViewMoved(visibleArea, mapLevel); +} + +void AdventureMapInterface::onAudioResumed() +{ + mapAudio->onAudioResumed(); +} + +void AdventureMapInterface::onAudioPaused() +{ + mapAudio->onAudioPaused(); +} + +void AdventureMapInterface::onHeroMovementStarted(const CGHeroInstance * hero) +{ + if (shortcuts->optionMapViewActive()) + { + widget->getInfoBar()->popAll(); + widget->getInfoBar()->showSelection(); + } +} + +void AdventureMapInterface::onHeroChanged(const CGHeroInstance *h) +{ + widget->getHeroList()->updateElement(h); + + if (h && h == LOCPLINT->localState->getCurrentHero() && !widget->getInfoBar()->showingComponents()) + widget->getInfoBar()->showSelection(); + + widget->updateActiveState(); +} + +void AdventureMapInterface::onTownChanged(const CGTownInstance * town) +{ + widget->getTownList()->updateElement(town); + + if (town && town == LOCPLINT->localState->getCurrentTown() && !widget->getInfoBar()->showingComponents()) + widget->getInfoBar()->showSelection(); +} + +void AdventureMapInterface::showInfoBoxMessage(const std::vector & components, std::string message, int timer) +{ + widget->getInfoBar()->pushComponents(components, message, timer); +} + +void AdventureMapInterface::activate() +{ + CIntObject::activate(); + + adjustActiveness(); + + screenBuf = screen; + + if(LOCPLINT) + { + LOCPLINT->cingconsole->activate(); + LOCPLINT->cingconsole->pos = this->pos; + } + + GH.fakeMouseMove(); //to restore the cursor + + // workaround for an edge case: + // if player unequips Angel Wings / Boots of Levitation of currently active hero + // game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero + if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero()) + LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero()); +} + +void AdventureMapInterface::deactivate() +{ + CIntObject::deactivate(); + CCS->curh->set(Cursor::Map::POINTER); + + if(LOCPLINT) + LOCPLINT->cingconsole->deactivate(); +} + +void AdventureMapInterface::showAll(Canvas & to) +{ + CIntObject::showAll(to); + dim(to); + LOCPLINT->cingconsole->show(to); +} + +void AdventureMapInterface::show(Canvas & to) +{ + CIntObject::show(to); + dim(to); + LOCPLINT->cingconsole->show(to); +} + +void AdventureMapInterface::dim(Canvas & to) +{ + for (auto window : GH.windows().findWindows()) + { + if (!std::dynamic_pointer_cast(window) && !std::dynamic_pointer_cast(window) && !window->isPopupWindow()) + { + Rect targetRect(0, 0, GH.screenDimensions().x, GH.screenDimensions().y); + ColorRGBA colorToFill(0, 0, 0, std::clamp(backgroundDimLevel, 0, 255)); + if(backgroundDimLevel > 0) + to.drawColorBlended(targetRect, colorToFill); + return; + } + } +} + +void AdventureMapInterface::tick(uint32_t msPassed) +{ + handleMapScrollingUpdate(msPassed); + + // we want animations to be active during enemy turn but map itself to be non-interactive + // so call timer update directly on inactive element + widget->getMapView()->tick(msPassed); +} + +void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed) +{ + /// Width of window border, in pixels, that triggers map scrolling + static constexpr int32_t borderScrollWidth = 15; + + int32_t scrollSpeedPixels = settings["adventure"]["scrollSpeedPixels"].Float(); + int32_t scrollDistance = scrollSpeedPixels * timePassed / 1000; + + Point cursorPosition = GH.getCursorPosition(); + Point scrollDirection; + + if (cursorPosition.x < borderScrollWidth) + scrollDirection.x = -1; + + if (cursorPosition.x > GH.screenDimensions().x - borderScrollWidth) + scrollDirection.x = +1; + + if (cursorPosition.y < borderScrollWidth) + scrollDirection.y = -1; + + if (cursorPosition.y > GH.screenDimensions().y - borderScrollWidth) + scrollDirection.y = +1; + + Point scrollDelta = scrollDirection * scrollDistance; + + bool cursorInScrollArea = scrollDelta != Point(0,0); + bool scrollingActive = cursorInScrollArea && shortcuts->optionMapScrollingActive() && !scrollingWasBlocked; + bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool(); + + if (!scrollingWasActive && scrollingBlocked) + { + scrollingWasBlocked = true; + return; + } + + if (!cursorInScrollArea && scrollingWasBlocked) + { + scrollingWasBlocked = false; + return; + } + + if (scrollingActive) + widget->getMapView()->onMapScrolled(scrollDelta); + + if (!scrollingActive && !scrollingWasActive) + return; + + if(scrollDelta.x > 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTHEAST); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTHEAST); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::SCROLL_EAST); + } + if(scrollDelta.x < 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTHWEST); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTHWEST); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::SCROLL_WEST); + } + + if (scrollDelta.x == 0) + { + if(scrollDelta.y < 0) + CCS->curh->set(Cursor::Map::SCROLL_NORTH); + if(scrollDelta.y > 0) + CCS->curh->set(Cursor::Map::SCROLL_SOUTH); + if(scrollDelta.y == 0) + CCS->curh->set(Cursor::Map::POINTER); + } + + scrollingWasActive = scrollingActive; +} + +void AdventureMapInterface::centerOnTile(int3 on) +{ + widget->getMapView()->onCenteredTile(on); +} + +void AdventureMapInterface::centerOnObject(const CGObjectInstance * obj) +{ + widget->getMapView()->onCenteredObject(obj); +} + +void AdventureMapInterface::keyPressed(EShortcut key) +{ + if (key == EShortcut::GLOBAL_CANCEL && spellBeingCasted) + hotkeyAbortCastingMode(); + + //fake mouse use to trigger onTileHovered() + GH.fakeMouseMove(); +} + +void AdventureMapInterface::onSelectionChanged(const CArmedInstance *sel) +{ + assert(sel); + + widget->getInfoBar()->popAll(); + mapAudio->onSelectionChanged(sel); + bool centerView = !settings["session"]["autoSkip"].Bool(); + + if (centerView) + centerOnObject(sel); + + if(sel->ID==Obj::TOWN) + { + auto town = dynamic_cast(sel); + + widget->getInfoBar()->showTownSelection(town); + widget->getTownList()->select(town); + widget->getHeroList()->select(nullptr); + onHeroChanged(nullptr); + } + else //hero selected + { + auto hero = dynamic_cast(sel); + + widget->getInfoBar()->showHeroSelection(hero); + widget->getHeroList()->select(hero); + widget->getTownList()->select(nullptr); + + LOCPLINT->localState->verifyPath(hero); + onHeroChanged(hero); + } + + widget->updateActiveState(); + widget->getHeroList()->redraw(); + widget->getTownList()->redraw(); +} + +void AdventureMapInterface::onMapTilesChanged(boost::optional> positions) +{ + if (positions) + widget->getMinimap()->updateTiles(*positions); + else + widget->getMinimap()->update(); +} + +void AdventureMapInterface::onHotseatWaitStarted(PlayerColor playerID) +{ + backgroundDimLevel = 255; + + onCurrentPlayerChanged(playerID); + setState(EAdventureState::HOTSEAT_WAIT); +} + +void AdventureMapInterface::onEnemyTurnStarted(PlayerColor playerID, bool isHuman) +{ + if(settings["session"]["spectate"].Bool()) + return; + + mapAudio->onEnemyTurnStarted(); + widget->getMinimap()->setAIRadar(!isHuman); + widget->getInfoBar()->startEnemyTurn(playerID); + setState(isHuman ? EAdventureState::OTHER_HUMAN_PLAYER_TURN : EAdventureState::AI_PLAYER_TURN); +} + +void AdventureMapInterface::setState(EAdventureState state) +{ + shortcuts->setState(state); + adjustActiveness(); + widget->updateActiveState(); +} + +void AdventureMapInterface::adjustActiveness() +{ + bool widgetMustBeActive = isActive() && shortcuts->optionSidePanelActive(); + bool mapViewMustBeActive = isActive() && (shortcuts->optionMapViewActive()); + + widget->setInputEnabled(widgetMustBeActive); + widget->getMapView()->setInputEnabled(mapViewMustBeActive); +} + +void AdventureMapInterface::onCurrentPlayerChanged(PlayerColor playerID) +{ + LOCPLINT->localState->setSelection(nullptr); + + if (playerID == currentPlayerID) + return; + + currentPlayerID = playerID; + widget->setPlayer(playerID); +} + +void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) +{ + backgroundDimLevel = settings["adventure"]["backgroundDimLevel"].Integer(); + + onCurrentPlayerChanged(playerID); + + setState(EAdventureState::MAKING_TURN); + if(playerID == LOCPLINT->playerID || settings["session"]["spectate"].Bool()) + { + widget->getMinimap()->setAIRadar(false); + widget->getInfoBar()->showSelection(); + } + + widget->getHeroList()->updateWidget(); + widget->getTownList()->updateWidget(); + + const CGHeroInstance * heroToSelect = nullptr; + + // find first non-sleeping hero + for (auto hero : LOCPLINT->localState->getWanderingHeroes()) + { + if (!LOCPLINT->localState->isHeroSleeping(hero)) + { + heroToSelect = hero; + break; + } + } + + //select first hero if available. + if (heroToSelect != nullptr) + { + LOCPLINT->localState->setSelection(heroToSelect); + } + else if (LOCPLINT->localState->getOwnedTowns().size()) + { + LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTown(0)); + } + else + { + LOCPLINT->localState->setSelection(LOCPLINT->localState->getWanderingHero(0)); + } + + //show new day animation and sound on infobar, except for 1st day of the game + if (LOCPLINT->cb->getDate(Date::DAY) != 1) + widget->getInfoBar()->showDate(); + + onHeroChanged(nullptr); + Canvas canvas = Canvas::createFromSurface(screen); + showAll(canvas); + mapAudio->onPlayerTurnStarted(); + + if(settings["session"]["autoSkip"].Bool() && !GH.isKeyboardShiftDown()) + { + if(auto iw = GH.windows().topWindow()) + iw->close(); + + hotkeyEndingTurn(); + } +} + +void AdventureMapInterface::hotkeyEndingTurn() +{ + if(settings["session"]["spectate"].Bool()) + return; + + if(!settings["general"]["startTurnAutosave"].Bool()) + { + LOCPLINT->performAutosave(); + } + + LOCPLINT->makingTurn = false; + LOCPLINT->cb->endTurn(); + + mapAudio->onPlayerTurnEnded(); +} + +const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPos) +{ + std::vector < const CGObjectInstance * > bobjs = LOCPLINT->cb->getBlockingObjs(mapPos); //blocking objects at tile + + if (bobjs.empty()) + return nullptr; + + return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder); +} + +void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + //FIXME: this line breaks H3 behavior for Dimension Door + if(!LOCPLINT->cb->isVisible(mapPos)) + return; + if(!LOCPLINT->makingTurn) + return; + + const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos); + + const CGObjectInstance *topBlocking = getActiveObject(mapPos); + + int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + if(spellBeingCasted) + { + assert(shortcuts->optionSpellcasting()); + + if (!isInScreenRange(selPos, mapPos)) + return; + + const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos); + + switch(spellBeingCasted->id) + { + case SpellID::SCUTTLE_BOAT: //Scuttle Boat + if(topBlocking && topBlocking->ID == Obj::BOAT) + performSpellcasting(mapPos); + break; + case SpellID::DIMENSION_DOOR: + if(!tile || tile->isClear(heroTile)) + performSpellcasting(mapPos); + break; + } + return; + } + //check if we can select this object + bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID; + canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES; + + bool isHero = false; + if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town) + { + if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked + LOCPLINT->openTownWindow(static_cast(topBlocking)); + else if(canSelect) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + } + else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected + { + isHero = true; + + const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos); + if(currentHero == topBlocking) //clicked selected hero + { + LOCPLINT->openHeroWindow(currentHero); + return; + } + else if(canSelect && pn->turns == 255 ) //selectable object at inaccessible tile + { + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + return; + } + else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise + { + if(LOCPLINT->localState->hasPath(currentHero) && + LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving + { + assert(!CGI->mh->hasOngoingAnimations()); + if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0) + LOCPLINT->moveHero(currentHero, LOCPLINT->localState->getPath(currentHero)); + return; + } + else + { + if(GH.isKeyboardCtrlDown()) //normal click behaviour (as no hero selected) + { + if(canSelect) + LOCPLINT->localState->setSelection(static_cast(topBlocking)); + } + else //remove old path and find a new one if we clicked on accessible tile + { + LOCPLINT->localState->setPath(currentHero, mapPos); + onHeroChanged(currentHero); + } + } + } + } //end of hero is selected "case" + else + { + throw std::runtime_error("Nothing is selected..."); + } + + const auto shipyard = ourInaccessibleShipyard(topBlocking); + if(isHero && shipyard != nullptr) + { + LOCPLINT->showShipyardDialogOrProblemPopup(shipyard); + } +} + +void AdventureMapInterface::onTileHovered(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + //may occur just at the start of game (fake move before full intiialization) + if(!LOCPLINT->localState->getCurrentArmy()) + return; + + if(!LOCPLINT->cb->isVisible(mapPos)) + { + CCS->curh->set(Cursor::Map::POINTER); + GH.statusbar()->clear(); + return; + } + auto objRelations = PlayerRelations::ALLIES; + const CGObjectInstance *objAtTile = getActiveObject(mapPos); + if(objAtTile) + { + objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner); + std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID); + boost::replace_all(text,"\n"," "); + GH.statusbar()->write(text); + } + else + { + std::string hlp = CGI->mh->getTerrainDescr(mapPos, false); + GH.statusbar()->write(hlp); + } + + if(spellBeingCasted) + { + switch(spellBeingCasted->id) + { + case SpellID::SCUTTLE_BOAT: + { + int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + + if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos)) + CCS->curh->set(Cursor::Map::SCUTTLE_BOAT); + else + CCS->curh->set(Cursor::Map::POINTER); + return; + } + case SpellID::DIMENSION_DOOR: + { + const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false); + int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter(); + if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos)) + CCS->curh->set(Cursor::Map::TELEPORT); + else + CCS->curh->set(Cursor::Map::POINTER); + return; + } + } + } + + if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown()) + { + if(objAtTile) + { + if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES) + CCS->curh->set(Cursor::Map::TOWN); + else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(Cursor::Map::POINTER); + } + else + CCS->curh->set(Cursor::Map::POINTER); + } + else if(const CGHeroInstance * hero = LOCPLINT->localState->getCurrentHero()) + { + std::array cursorMove = { Cursor::Map::T1_MOVE, Cursor::Map::T2_MOVE, Cursor::Map::T3_MOVE, Cursor::Map::T4_MOVE, }; + std::array cursorAttack = { Cursor::Map::T1_ATTACK, Cursor::Map::T2_ATTACK, Cursor::Map::T3_ATTACK, Cursor::Map::T4_ATTACK, }; + std::array cursorSail = { Cursor::Map::T1_SAIL, Cursor::Map::T2_SAIL, Cursor::Map::T3_SAIL, Cursor::Map::T4_SAIL, }; + std::array cursorDisembark = { Cursor::Map::T1_DISEMBARK, Cursor::Map::T2_DISEMBARK, Cursor::Map::T3_DISEMBARK, Cursor::Map::T4_DISEMBARK, }; + std::array cursorExchange = { Cursor::Map::T1_EXCHANGE, Cursor::Map::T2_EXCHANGE, Cursor::Map::T3_EXCHANGE, Cursor::Map::T4_EXCHANGE, }; + std::array cursorVisit = { Cursor::Map::T1_VISIT, Cursor::Map::T2_VISIT, Cursor::Map::T3_VISIT, Cursor::Map::T4_VISIT, }; + std::array cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, }; + + const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos); + assert(pathNode); + + if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info + { + showMoveDetailsInStatusbar(*hero, *pathNode); + } + + int turns = pathNode->turns; + vstd::amin(turns, 3); + switch(pathNode->action) + { + case EPathNodeAction::NORMAL: + case EPathNodeAction::TELEPORT_NORMAL: + if(pathNode->layer == EPathfindingLayer::LAND) + CCS->curh->set(cursorMove[turns]); + else + CCS->curh->set(cursorSailVisit[turns]); + break; + + case EPathNodeAction::VISIT: + case EPathNodeAction::BLOCKING_VISIT: + case EPathNodeAction::TELEPORT_BLOCKING_VISIT: + if(objAtTile && objAtTile->ID == Obj::HERO) + { + if(LOCPLINT->localState->getCurrentArmy() == objAtTile) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(cursorExchange[turns]); + } + else if(pathNode->layer == EPathfindingLayer::LAND) + CCS->curh->set(cursorVisit[turns]); + else + CCS->curh->set(cursorSailVisit[turns]); + break; + + case EPathNodeAction::BATTLE: + case EPathNodeAction::TELEPORT_BATTLE: + CCS->curh->set(cursorAttack[turns]); + break; + + case EPathNodeAction::EMBARK: + CCS->curh->set(cursorSail[turns]); + break; + + case EPathNodeAction::DISEMBARK: + CCS->curh->set(cursorDisembark[turns]); + break; + + default: + if(objAtTile && objRelations != PlayerRelations::ENEMIES) + { + if(objAtTile->ID == Obj::TOWN) + CCS->curh->set(Cursor::Map::TOWN); + else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER) + CCS->curh->set(Cursor::Map::HERO); + else + CCS->curh->set(Cursor::Map::POINTER); + } + else + CCS->curh->set(Cursor::Map::POINTER); + break; + } + } + + if(ourInaccessibleShipyard(objAtTile)) + { + CCS->curh->set(Cursor::Map::T1_SAIL); + } +} + +void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode) +{ + const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining(); + const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains; + const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0; + + std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns"); + + boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns)); + boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost)); + boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove)); + + GH.statusbar()->write(result); +} + +void AdventureMapInterface::onTileRightClicked(const int3 &mapPos) +{ + if(!shortcuts->optionMapViewActive()) + return; + + if(spellBeingCasted) + { + hotkeyAbortCastingMode(); + return; + } + + if(!LOCPLINT->cb->isVisible(mapPos)) + { + CRClickPopup::createAndPush(VLC->generaltexth->allTexts[61]); //Uncharted Territory + return; + } + + const CGObjectInstance * obj = getActiveObject(mapPos); + if(!obj) + { + // Bare or undiscovered terrain + const TerrainTile * tile = LOCPLINT->cb->getTile(mapPos); + if(tile) + { + std::string hlp = CGI->mh->getTerrainDescr(mapPos, true); + CRClickPopup::createAndPush(hlp); + } + return; + } + + CRClickPopup::createAndPush(obj, GH.getCursorPosition(), ETextAlignment::CENTER); +} + +void AdventureMapInterface::enterCastingMode(const CSpell * sp) +{ + assert(sp->id == SpellID::SCUTTLE_BOAT || sp->id == SpellID::DIMENSION_DOOR); + spellBeingCasted = sp; + Settings config = settings.write["session"]["showSpellRange"]; + config->Bool() = true; + + setState(EAdventureState::CASTING_SPELL); +} + +void AdventureMapInterface::exitCastingMode() +{ + assert(spellBeingCasted); + spellBeingCasted = nullptr; + setState(EAdventureState::MAKING_TURN); + + Settings config = settings.write["session"]["showSpellRange"]; + config->Bool() = false; +} + +void AdventureMapInterface::hotkeyAbortCastingMode() +{ + exitCastingMode(); + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[731]); //Spell cancelled +} + +void AdventureMapInterface::performSpellcasting(const int3 & dest) +{ + SpellID id = spellBeingCasted->id; + exitCastingMode(); + LOCPLINT->cb->castSpell(LOCPLINT->localState->getCurrentHero(), id, dest); +} + +Rect AdventureMapInterface::terrainAreaPixels() const +{ + return widget->getMapView()->pos; +} + +const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const +{ + const IShipyard *ret = IShipyard::castFrom(obj); + + if(!ret || + obj->tempOwner != currentPlayerID || + (CCS->curh->get() != Cursor::Map::T1_SAIL && CCS->curh->get() != Cursor::Map::POINTER)) + return nullptr; + + return ret; +} + +void AdventureMapInterface::hotkeyExitWorldView() +{ + setState(EAdventureState::MAKING_TURN); + widget->getMapView()->onViewMapActivated(); +} + +void AdventureMapInterface::openWorldView(int tileSize) +{ + setState(EAdventureState::WORLD_VIEW); + widget->getMapView()->onViewWorldActivated(tileSize); +} + +void AdventureMapInterface::openWorldView() +{ + openWorldView(11); +} + +void AdventureMapInterface::openWorldView(const std::vector& objectPositions, bool showTerrain) +{ + openWorldView(11); + widget->getMapView()->onViewSpellActivated(11, objectPositions, showTerrain); +} + +void AdventureMapInterface::hotkeyNextTown() +{ + widget->getTownList()->selectNext(); +} + +void AdventureMapInterface::hotkeySwitchMapLevel() +{ + widget->getMapView()->onMapLevelSwitched(); +} + +void AdventureMapInterface::hotkeyZoom(int delta) +{ + widget->getMapView()->onMapZoomLevelChanged(delta); +} + +void AdventureMapInterface::onScreenResize() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // remember our activation state and reactive after reconstruction + // since othervice activate() calls for created elements will bypass virtual dispatch + // and will call directly CIntObject::activate() instead of dispatching virtual function call + bool widgetActive = isActive(); + + if (widgetActive) + deactivate(); + + widget.reset(); + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + widget = std::make_shared(shortcuts); + widget->getMapView()->onViewMapActivated(); + widget->setPlayer(currentPlayerID); + widget->updateActiveState(); + widget->getMinimap()->update(); + widget->getInfoBar()->showSelection(); + + if (LOCPLINT && LOCPLINT->localState->getCurrentArmy()) + widget->getMapView()->onCenteredObject(LOCPLINT->localState->getCurrentArmy()); + + adjustActiveness(); + + if (widgetActive) + activate(); +} diff --git a/client/adventureMap/AdventureMapInterface.h b/client/adventureMap/AdventureMapInterface.h index 8140d7c82..1318162c7 100644 --- a/client/adventureMap/AdventureMapInterface.h +++ b/client/adventureMap/AdventureMapInterface.h @@ -1,191 +1,191 @@ -/* - * AdventureMapInterface.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 "../gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CGTownInstance; -class CArmedInstance; -class IShipyard; -struct CGPathNode; -struct ObjectPosInfo; -struct Component; -class int3; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class IImage; -class CAnimImage; -class CGStatusBar; -class AdventureMapWidget; -class AdventureMapShortcuts; -class CAnimation; -class MapView; -class CResDataBar; -class CHeroList; -class CTownList; -class CInfoBar; -class CMinimap; -class MapAudioPlayer; -class TurnTimerWidget; -enum class EAdventureState; - -struct MapDrawingInfo; - -/// That's a huge class which handles general adventure map actions and -/// shows the right menu(questlog, spellbook, end turn,..) from where you -/// can get to the towns and heroes. -class AdventureMapInterface : public CIntObject -{ -private: - /// currently acting player - PlayerColor currentPlayerID; - - /// if true, cursor was changed to scrolling and must be reset back once scroll is over - bool scrollingWasActive; - - /// if true, then scrolling was blocked via ctrl and should not restart until player move cursor outside scrolling area - bool scrollingWasBlocked; - - /// how much should the background dimmed, when windows are on the top - int backgroundDimLevel; - - /// spell for which player is selecting target, or nullptr if none - const CSpell *spellBeingCasted; - - std::shared_ptr mapAudio; - std::shared_ptr widget; - std::shared_ptr shortcuts; - std::shared_ptr watches; - -private: - void setState(EAdventureState state); - - /// updates active state of game window whenever game state changes - void adjustActiveness(); - - /// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else - const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; - - /// check and if necessary reacts on scrolling by moving cursor to screen edge - void handleMapScrollingUpdate(uint32_t msPassed); - - void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode); - - const CGObjectInstance *getActiveObject(const int3 &tile); - - /// exits currently opened world view mode and returns to normal map - void exitCastingMode(); - - /// casts current spell at specified location - void performSpellcasting(const int3 & castTarget); - - /// dim interface if some windows opened - void dim(Canvas & to); - -protected: - /// CIntObject interface implementation - - void activate() override; - void deactivate() override; - - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - void keyPressed(EShortcut key) override; - - void onScreenResize() override; - -public: - AdventureMapInterface(); - - void hotkeyAbortCastingMode(); - void hotkeyExitWorldView(); - void hotkeyEndingTurn(); - void hotkeyNextTown(); - void hotkeySwitchMapLevel(); - void hotkeyZoom(int delta); - - /// Called by PlayerInterface when specified player is ready to start his turn - void onHotseatWaitStarted(PlayerColor playerID); - - /// Called by PlayerInterface when AI or remote human player starts his turn - void onEnemyTurnStarted(PlayerColor playerID, bool isHuman); - - /// Called by PlayerInterface when local human player starts his turn - void onPlayerTurnStarted(PlayerColor playerID); - - /// Called by PlayerInterface when interface should be switched to specified player without starting turn - void onCurrentPlayerChanged(PlayerColor playerID); - - /// Called by PlayerInterface when specific map tile changed and must be updated on minimap - void onMapTilesChanged(boost::optional> positions); - - /// Called by PlayerInterface when hero starts movement - void onHeroMovementStarted(const CGHeroInstance * hero); - - /// Called by PlayerInterface when hero state changed and hero list must be updated - void onHeroChanged(const CGHeroInstance * hero); - - /// Called by PlayerInterface when town state changed and town list must be updated - void onTownChanged(const CGTownInstance * town); - - /// Called when currently selected object changes - void onSelectionChanged(const CArmedInstance *sel); - - /// Called when map audio should be paused, e.g. on combat or town screen access - void onAudioPaused(); - - /// Called when map audio should be resume, opposite to onPaused - void onAudioResumed(); - - /// Requests to display provided information inside infobox - void showInfoBoxMessage(const std::vector & components, std::string message, int timer); - - /// Changes position on map to center selected location - void centerOnTile(int3 on); - void centerOnObject(const CGObjectInstance *obj); - - /// called by MapView whenever currently visible area changes - /// visibleArea describes now visible map section measured in tiles - void onMapViewMoved(const Rect & visibleArea, int mapLevel); - - /// called by MapView whenever tile is clicked - void onTileLeftClicked(const int3 & mapPos); - - /// called by MapView whenever tile is hovered - void onTileHovered(const int3 & mapPos); - - /// called by MapView whenever tile is clicked - void onTileRightClicked(const int3 & mapPos); - - /// called by spell window when spell to cast has been selected - void enterCastingMode(const CSpell * sp); - - /// returns area of screen covered by terrain (main game area) - Rect terrainAreaPixels() const; - - /// opens world view at default scale - void openWorldView(); - - /// opens world view at specific scale - void openWorldView(int tileSize); - - /// opens world view with specific info, e.g. after View Earth/Air is shown - void openWorldView(const std::vector& objectPositions, bool showTerrain); -}; - -extern std::shared_ptr adventureInt; +/* + * AdventureMapInterface.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 "../gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGHeroInstance; +class CGTownInstance; +class CArmedInstance; +class IShipyard; +struct CGPathNode; +struct ObjectPosInfo; +struct Component; +class int3; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class IImage; +class CAnimImage; +class CGStatusBar; +class AdventureMapWidget; +class AdventureMapShortcuts; +class CAnimation; +class MapView; +class CResDataBar; +class CHeroList; +class CTownList; +class CInfoBar; +class CMinimap; +class MapAudioPlayer; +class TurnTimerWidget; +enum class EAdventureState; + +struct MapDrawingInfo; + +/// That's a huge class which handles general adventure map actions and +/// shows the right menu(questlog, spellbook, end turn,..) from where you +/// can get to the towns and heroes. +class AdventureMapInterface : public CIntObject +{ +private: + /// currently acting player + PlayerColor currentPlayerID; + + /// if true, cursor was changed to scrolling and must be reset back once scroll is over + bool scrollingWasActive; + + /// if true, then scrolling was blocked via ctrl and should not restart until player move cursor outside scrolling area + bool scrollingWasBlocked; + + /// how much should the background dimmed, when windows are on the top + int backgroundDimLevel; + + /// spell for which player is selecting target, or nullptr if none + const CSpell *spellBeingCasted; + + std::shared_ptr mapAudio; + std::shared_ptr widget; + std::shared_ptr shortcuts; + std::shared_ptr watches; + +private: + void setState(EAdventureState state); + + /// updates active state of game window whenever game state changes + void adjustActiveness(); + + /// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else + const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; + + /// check and if necessary reacts on scrolling by moving cursor to screen edge + void handleMapScrollingUpdate(uint32_t msPassed); + + void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode); + + const CGObjectInstance *getActiveObject(const int3 &tile); + + /// exits currently opened world view mode and returns to normal map + void exitCastingMode(); + + /// casts current spell at specified location + void performSpellcasting(const int3 & castTarget); + + /// dim interface if some windows opened + void dim(Canvas & to); + +protected: + /// CIntObject interface implementation + + void activate() override; + void deactivate() override; + + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + void keyPressed(EShortcut key) override; + + void onScreenResize() override; + +public: + AdventureMapInterface(); + + void hotkeyAbortCastingMode(); + void hotkeyExitWorldView(); + void hotkeyEndingTurn(); + void hotkeyNextTown(); + void hotkeySwitchMapLevel(); + void hotkeyZoom(int delta); + + /// Called by PlayerInterface when specified player is ready to start his turn + void onHotseatWaitStarted(PlayerColor playerID); + + /// Called by PlayerInterface when AI or remote human player starts his turn + void onEnemyTurnStarted(PlayerColor playerID, bool isHuman); + + /// Called by PlayerInterface when local human player starts his turn + void onPlayerTurnStarted(PlayerColor playerID); + + /// Called by PlayerInterface when interface should be switched to specified player without starting turn + void onCurrentPlayerChanged(PlayerColor playerID); + + /// Called by PlayerInterface when specific map tile changed and must be updated on minimap + void onMapTilesChanged(boost::optional> positions); + + /// Called by PlayerInterface when hero starts movement + void onHeroMovementStarted(const CGHeroInstance * hero); + + /// Called by PlayerInterface when hero state changed and hero list must be updated + void onHeroChanged(const CGHeroInstance * hero); + + /// Called by PlayerInterface when town state changed and town list must be updated + void onTownChanged(const CGTownInstance * town); + + /// Called when currently selected object changes + void onSelectionChanged(const CArmedInstance *sel); + + /// Called when map audio should be paused, e.g. on combat or town screen access + void onAudioPaused(); + + /// Called when map audio should be resume, opposite to onPaused + void onAudioResumed(); + + /// Requests to display provided information inside infobox + void showInfoBoxMessage(const std::vector & components, std::string message, int timer); + + /// Changes position on map to center selected location + void centerOnTile(int3 on); + void centerOnObject(const CGObjectInstance *obj); + + /// called by MapView whenever currently visible area changes + /// visibleArea describes now visible map section measured in tiles + void onMapViewMoved(const Rect & visibleArea, int mapLevel); + + /// called by MapView whenever tile is clicked + void onTileLeftClicked(const int3 & mapPos); + + /// called by MapView whenever tile is hovered + void onTileHovered(const int3 & mapPos); + + /// called by MapView whenever tile is clicked + void onTileRightClicked(const int3 & mapPos); + + /// called by spell window when spell to cast has been selected + void enterCastingMode(const CSpell * sp); + + /// returns area of screen covered by terrain (main game area) + Rect terrainAreaPixels() const; + + /// opens world view at default scale + void openWorldView(); + + /// opens world view at specific scale + void openWorldView(int tileSize); + + /// opens world view with specific info, e.g. after View Earth/Air is shown + void openWorldView(const std::vector& objectPositions, bool showTerrain); +}; + +extern std::shared_ptr adventureInt; diff --git a/client/adventureMap/AdventureMapWidget.cpp b/client/adventureMap/AdventureMapWidget.cpp index 5654b2b72..7333f1d37 100644 --- a/client/adventureMap/AdventureMapWidget.cpp +++ b/client/adventureMap/AdventureMapWidget.cpp @@ -1,455 +1,455 @@ -/* - * CAdventureMapWidget.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 "AdventureMapWidget.h" - -#include "AdventureMapShortcuts.h" -#include "CInfoBar.h" -#include "CList.h" -#include "CMinimap.h" -#include "CResDataBar.h" -#include "AdventureState.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../mapView/MapView.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" - -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" - -#include "../../lib/constants/StringConstants.h" -#include "../../lib/filesystem/ResourcePath.h" - -AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) - : shortcuts(shortcuts) - , mapLevel(0) -{ - pos.x = pos.y = 0; - pos.w = GH.screenDimensions().x; - pos.h = GH.screenDimensions().y; - - REGISTER_BUILDER("adventureInfobar", &AdventureMapWidget::buildInfobox ); - REGISTER_BUILDER("adventureMapImage", &AdventureMapWidget::buildMapImage ); - REGISTER_BUILDER("adventureMapButton", &AdventureMapWidget::buildMapButton ); - REGISTER_BUILDER("adventureMapContainer", &AdventureMapWidget::buildMapContainer ); - REGISTER_BUILDER("adventureMapGameArea", &AdventureMapWidget::buildMapGameArea ); - REGISTER_BUILDER("adventureMapHeroList", &AdventureMapWidget::buildMapHeroList ); - REGISTER_BUILDER("adventureMapIcon", &AdventureMapWidget::buildMapIcon ); - REGISTER_BUILDER("adventureMapTownList", &AdventureMapWidget::buildMapTownList ); - REGISTER_BUILDER("adventureMinimap", &AdventureMapWidget::buildMinimap ); - REGISTER_BUILDER("adventureResourceDateBar", &AdventureMapWidget::buildResourceDateBar ); - REGISTER_BUILDER("adventureStatusBar", &AdventureMapWidget::buildStatusBar ); - REGISTER_BUILDER("adventurePlayerTexture", &AdventureMapWidget::buildTexturePlayerColored); - - for (const auto & entry : shortcuts->getShortcuts()) - addShortcut(entry.shortcut, entry.callback); - - const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); - - for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) - playerColorerImages.push_back(ImagePath::fromJson(entry)); - - build(config); - addUsedEvents(KEYBOARD); -} - -void AdventureMapWidget::onMapViewMoved(const Rect & visibleArea, int newMapLevel) -{ - if(mapLevel == newMapLevel) - return; - - mapLevel = newMapLevel; - updateActiveState(); -} - -Rect AdventureMapWidget::readSourceArea(const JsonNode & source, const JsonNode & sourceCommon) -{ - const auto & input = source.isNull() ? sourceCommon : source; - - return readArea(input, Rect(Point(0, 0), Point(800, 600))); -} - -Rect AdventureMapWidget::readTargetArea(const JsonNode & source) -{ - if(subwidgetSizes.empty()) - return readArea(source, pos); - return readArea(source, subwidgetSizes.back()); -} - -Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & boundingBox) -{ - const auto & object = source.Struct(); - - if(object.count("left") + object.count("width") + object.count("right") != 2) - logGlobal->error("Invalid area definition in widget! Unable to load width!"); - - if(object.count("top") + object.count("height") + object.count("bottom") != 2) - logGlobal->error("Invalid area definition in widget! Unable to load height!"); - - int left = source["left"].Integer(); - int width = source["width"].Integer(); - int right = source["right"].Integer(); - - int top = source["top"].Integer(); - int height = source["height"].Integer(); - int bottom = source["bottom"].Integer(); - - Point topLeft(left, top); - Point dimensions(width, height); - - if(source["left"].isNull()) - topLeft.x = boundingBox.w - right - width; - - if(source["width"].isNull()) - dimensions.x = boundingBox.w - right - left; - - if(source["top"].isNull()) - topLeft.y = boundingBox.h - bottom - height; - - if(source["height"].isNull()) - dimensions.y = boundingBox.h - bottom - top; - - return Rect(topLeft + boundingBox.topLeft(), dimensions); -} - -std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) -{ - ImagePath resource = ImagePath::fromJson(name); - - if(images.count(resource) == 0) - images[resource] = GH.renderHandler().loadImage(resource); - - return images[resource]; -} - -std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & name) -{ - AnimationPath resource = AnimationPath::fromJson(name); - - if(animations.count(resource) == 0) - animations[resource] = GH.renderHandler().loadAnimation(resource); - - return animations[resource]; -} - -std::shared_ptr AdventureMapWidget::buildInfobox(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - infoBar = std::make_shared(area); - return infoBar; -} - -std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & input) -{ - Rect targetArea = readTargetArea(input["area"]); - Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); - - return std::make_shared(loadImage(input["image"]), targetArea, sourceArea); -} - -std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) -{ - auto position = readTargetArea(input["area"]); - auto image = AnimationPath::fromJson(input["image"]); - auto help = readHintText(input["help"]); - bool playerColored = input["playerColored"].Bool(); - - auto button = std::make_shared(position.topLeft(), image, help, 0, EShortcut::NONE, playerColored); - - loadButtonBorderColor(button, input["borderColor"]); - loadButtonHotkey(button, input["hotkey"]); - - return button; -} - -std::shared_ptr AdventureMapWidget::buildMapContainer(const JsonNode & input) -{ - auto position = readTargetArea(input["area"]); - std::shared_ptr result; - - if (!input["exists"].isNull()) - { - if (!input["exists"]["heightMin"].isNull() && input["exists"]["heightMin"].Integer() >= pos.h) - return nullptr; - if (!input["exists"]["heightMax"].isNull() && input["exists"]["heightMax"].Integer() < pos.h) - return nullptr; - if (!input["exists"]["widthMin"].isNull() && input["exists"]["widthMin"].Integer() >= pos.w) - return nullptr; - if (!input["exists"]["widthMax"].isNull() && input["exists"]["widthMax"].Integer() < pos.w) - return nullptr; - } - - if (input["overlay"].Bool()) - result = std::make_shared(); - else - result = std::make_shared(); - - result->disableCondition = input["hideWhen"].String(); - - result->moveBy(position.topLeft()); - subwidgetSizes.push_back(position); - for(const auto & entry : input["items"].Vector()) - { - auto widget = buildWidget(entry); - - addWidget(entry["name"].String(), widget); - result->ownedChildren.push_back(widget); - - // FIXME: remove cast and replace it with better check - if (std::dynamic_pointer_cast(widget) || std::dynamic_pointer_cast(widget)) - result->addChild(widget.get(), true); - else - result->addChild(widget.get(), false); - } - subwidgetSizes.pop_back(); - - return result; -} - -std::shared_ptr AdventureMapWidget::buildMapGameArea(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - mapView = std::make_shared(area.topLeft(), area.dimensions()); - return mapView; -} - -std::shared_ptr AdventureMapWidget::buildMapHeroList(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - subwidgetSizes.push_back(area); - - Rect item = readTargetArea(input["item"]); - - Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); - int itemsCount = input["itemsCount"].Integer(); - - auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getWanderingHeroes().size()); - - - if(!input["scrollUp"].isNull()) - result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); - - if(!input["scrollDown"].isNull()) - result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); - - subwidgetSizes.pop_back(); - - heroList = result; - return result; -} - -std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - size_t index = input["index"].Integer(); - size_t perPlayer = input["perPlayer"].Integer(); - - return std::make_shared(area.topLeft(), loadAnimation(input["image"]), index, perPlayer); -} - -std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - subwidgetSizes.push_back(area); - - Rect item = readTargetArea(input["item"]); - Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); - int itemsCount = input["itemsCount"].Integer(); - - auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getOwnedTowns().size()); - - - if(!input["scrollUp"].isNull()) - result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); - - if(!input["scrollDown"].isNull()) - result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); - - subwidgetSizes.pop_back(); - - townList = result; - return result; -} - -std::shared_ptr AdventureMapWidget::buildMinimap(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - minimap = std::make_shared(area); - return minimap; -} - -std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - auto image = ImagePath::fromJson(input["image"]); - - auto result = std::make_shared(image, area.topLeft()); - - for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - { - const auto & node = input[GameConstants::RESOURCE_NAMES[i]]; - - if(node.isNull()) - continue; - - result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer())); - } - - result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer())); - - return result; -} - -std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & input) -{ - Rect area = readTargetArea(input["area"]); - auto image = ImagePath::fromJson(input["image"]); - - auto background = std::make_shared(image, area); - - return CGStatusBar::create(background); -} - -std::shared_ptr AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input) -{ - logGlobal->debug("Building widget CFilledTexture"); - auto image = ImagePath::fromJson(input["image"]); - Rect area = readTargetArea(input["area"]); - return std::make_shared(image, area); -} - -std::shared_ptr AdventureMapWidget::getHeroList() -{ - return heroList; -} - -std::shared_ptr AdventureMapWidget::getTownList() -{ - return townList; -} - -std::shared_ptr AdventureMapWidget::getMinimap() -{ - return minimap; -} - -std::shared_ptr AdventureMapWidget::getMapView() -{ - return mapView; -} - -std::shared_ptr AdventureMapWidget::getInfoBar() -{ - return infoBar; -} - -void AdventureMapWidget::setPlayer(const PlayerColor & player) -{ - setPlayerChildren(this, player); -} - -void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColor & player) -{ - for(auto & entry : widget->children) - { - auto container = dynamic_cast(entry); - auto icon = dynamic_cast(entry); - auto button = dynamic_cast(entry); - auto resDataBar = dynamic_cast(entry); - auto texture = dynamic_cast(entry); - - if(button) - button->setPlayerColor(player); - - if(resDataBar) - resDataBar->colorize(player); - - if(icon) - icon->setPlayer(player); - - if(container) - setPlayerChildren(container, player); - - if (texture) - texture->playerColored(player); - } - - for(const auto & entry : playerColorerImages) - { - if(images.count(entry)) - images[entry]->playerColored(player); - } - - redraw(); -} - -CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr animation, size_t index, size_t iconsPerPlayer) - : index(index) - , iconsPerPlayer(iconsPerPlayer) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += position; - image = std::make_shared(animation, index); -} - -void CAdventureMapIcon::setPlayer(const PlayerColor & player) -{ - image->setFrame(index + player.getNum() * iconsPerPlayer); -} - -void CAdventureMapOverlayWidget::show(Canvas & to) -{ - CIntObject::showAll(to); -} - -void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget) -{ - for(auto & entry : widget->children) - { - auto container = dynamic_cast(entry); - - if (container) - { - if (container->disableCondition == "heroAwake") - container->setEnabled(!shortcuts->optionHeroSleeping()); - - if (container->disableCondition == "heroSleeping") - container->setEnabled(shortcuts->optionHeroSleeping()); - - if (container->disableCondition == "mapLayerSurface") - container->setEnabled(shortcuts->optionMapLevelSurface()); - - if (container->disableCondition == "mapLayerUnderground") - container->setEnabled(!shortcuts->optionMapLevelSurface()); - - if (container->disableCondition == "mapViewMode") - container->setEnabled(shortcuts->optionInWorldView()); - - if (container->disableCondition == "worldViewMode") - container->setEnabled(!shortcuts->optionInWorldView()); - - updateActiveStateChildden(container); - } - } -} - -void AdventureMapWidget::updateActiveState() -{ - updateActiveStateChildden(this); - - for (auto entry: shortcuts->getShortcuts()) - setShortcutBlocked(entry.shortcut, !entry.isEnabled); -} +/* + * CAdventureMapWidget.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 "AdventureMapWidget.h" + +#include "AdventureMapShortcuts.h" +#include "CInfoBar.h" +#include "CList.h" +#include "CMinimap.h" +#include "CResDataBar.h" +#include "AdventureState.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../mapView/MapView.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" + +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" + +#include "../../lib/constants/StringConstants.h" +#include "../../lib/filesystem/ResourcePath.h" + +AdventureMapWidget::AdventureMapWidget( std::shared_ptr shortcuts ) + : shortcuts(shortcuts) + , mapLevel(0) +{ + pos.x = pos.y = 0; + pos.w = GH.screenDimensions().x; + pos.h = GH.screenDimensions().y; + + REGISTER_BUILDER("adventureInfobar", &AdventureMapWidget::buildInfobox ); + REGISTER_BUILDER("adventureMapImage", &AdventureMapWidget::buildMapImage ); + REGISTER_BUILDER("adventureMapButton", &AdventureMapWidget::buildMapButton ); + REGISTER_BUILDER("adventureMapContainer", &AdventureMapWidget::buildMapContainer ); + REGISTER_BUILDER("adventureMapGameArea", &AdventureMapWidget::buildMapGameArea ); + REGISTER_BUILDER("adventureMapHeroList", &AdventureMapWidget::buildMapHeroList ); + REGISTER_BUILDER("adventureMapIcon", &AdventureMapWidget::buildMapIcon ); + REGISTER_BUILDER("adventureMapTownList", &AdventureMapWidget::buildMapTownList ); + REGISTER_BUILDER("adventureMinimap", &AdventureMapWidget::buildMinimap ); + REGISTER_BUILDER("adventureResourceDateBar", &AdventureMapWidget::buildResourceDateBar ); + REGISTER_BUILDER("adventureStatusBar", &AdventureMapWidget::buildStatusBar ); + REGISTER_BUILDER("adventurePlayerTexture", &AdventureMapWidget::buildTexturePlayerColored); + + for (const auto & entry : shortcuts->getShortcuts()) + addShortcut(entry.shortcut, entry.callback); + + const JsonNode config(JsonPath::builtin("config/widgets/adventureMap.json")); + + for(const auto & entry : config["options"]["imagesPlayerColored"].Vector()) + playerColorerImages.push_back(ImagePath::fromJson(entry)); + + build(config); + addUsedEvents(KEYBOARD); +} + +void AdventureMapWidget::onMapViewMoved(const Rect & visibleArea, int newMapLevel) +{ + if(mapLevel == newMapLevel) + return; + + mapLevel = newMapLevel; + updateActiveState(); +} + +Rect AdventureMapWidget::readSourceArea(const JsonNode & source, const JsonNode & sourceCommon) +{ + const auto & input = source.isNull() ? sourceCommon : source; + + return readArea(input, Rect(Point(0, 0), Point(800, 600))); +} + +Rect AdventureMapWidget::readTargetArea(const JsonNode & source) +{ + if(subwidgetSizes.empty()) + return readArea(source, pos); + return readArea(source, subwidgetSizes.back()); +} + +Rect AdventureMapWidget::readArea(const JsonNode & source, const Rect & boundingBox) +{ + const auto & object = source.Struct(); + + if(object.count("left") + object.count("width") + object.count("right") != 2) + logGlobal->error("Invalid area definition in widget! Unable to load width!"); + + if(object.count("top") + object.count("height") + object.count("bottom") != 2) + logGlobal->error("Invalid area definition in widget! Unable to load height!"); + + int left = source["left"].Integer(); + int width = source["width"].Integer(); + int right = source["right"].Integer(); + + int top = source["top"].Integer(); + int height = source["height"].Integer(); + int bottom = source["bottom"].Integer(); + + Point topLeft(left, top); + Point dimensions(width, height); + + if(source["left"].isNull()) + topLeft.x = boundingBox.w - right - width; + + if(source["width"].isNull()) + dimensions.x = boundingBox.w - right - left; + + if(source["top"].isNull()) + topLeft.y = boundingBox.h - bottom - height; + + if(source["height"].isNull()) + dimensions.y = boundingBox.h - bottom - top; + + return Rect(topLeft + boundingBox.topLeft(), dimensions); +} + +std::shared_ptr AdventureMapWidget::loadImage(const JsonNode & name) +{ + ImagePath resource = ImagePath::fromJson(name); + + if(images.count(resource) == 0) + images[resource] = GH.renderHandler().loadImage(resource); + + return images[resource]; +} + +std::shared_ptr AdventureMapWidget::loadAnimation(const JsonNode & name) +{ + AnimationPath resource = AnimationPath::fromJson(name); + + if(animations.count(resource) == 0) + animations[resource] = GH.renderHandler().loadAnimation(resource); + + return animations[resource]; +} + +std::shared_ptr AdventureMapWidget::buildInfobox(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + infoBar = std::make_shared(area); + return infoBar; +} + +std::shared_ptr AdventureMapWidget::buildMapImage(const JsonNode & input) +{ + Rect targetArea = readTargetArea(input["area"]); + Rect sourceArea = readSourceArea(input["sourceArea"], input["area"]); + + return std::make_shared(loadImage(input["image"]), targetArea, sourceArea); +} + +std::shared_ptr AdventureMapWidget::buildMapButton(const JsonNode & input) +{ + auto position = readTargetArea(input["area"]); + auto image = AnimationPath::fromJson(input["image"]); + auto help = readHintText(input["help"]); + bool playerColored = input["playerColored"].Bool(); + + auto button = std::make_shared(position.topLeft(), image, help, 0, EShortcut::NONE, playerColored); + + loadButtonBorderColor(button, input["borderColor"]); + loadButtonHotkey(button, input["hotkey"]); + + return button; +} + +std::shared_ptr AdventureMapWidget::buildMapContainer(const JsonNode & input) +{ + auto position = readTargetArea(input["area"]); + std::shared_ptr result; + + if (!input["exists"].isNull()) + { + if (!input["exists"]["heightMin"].isNull() && input["exists"]["heightMin"].Integer() >= pos.h) + return nullptr; + if (!input["exists"]["heightMax"].isNull() && input["exists"]["heightMax"].Integer() < pos.h) + return nullptr; + if (!input["exists"]["widthMin"].isNull() && input["exists"]["widthMin"].Integer() >= pos.w) + return nullptr; + if (!input["exists"]["widthMax"].isNull() && input["exists"]["widthMax"].Integer() < pos.w) + return nullptr; + } + + if (input["overlay"].Bool()) + result = std::make_shared(); + else + result = std::make_shared(); + + result->disableCondition = input["hideWhen"].String(); + + result->moveBy(position.topLeft()); + subwidgetSizes.push_back(position); + for(const auto & entry : input["items"].Vector()) + { + auto widget = buildWidget(entry); + + addWidget(entry["name"].String(), widget); + result->ownedChildren.push_back(widget); + + // FIXME: remove cast and replace it with better check + if (std::dynamic_pointer_cast(widget) || std::dynamic_pointer_cast(widget)) + result->addChild(widget.get(), true); + else + result->addChild(widget.get(), false); + } + subwidgetSizes.pop_back(); + + return result; +} + +std::shared_ptr AdventureMapWidget::buildMapGameArea(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + mapView = std::make_shared(area.topLeft(), area.dimensions()); + return mapView; +} + +std::shared_ptr AdventureMapWidget::buildMapHeroList(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + subwidgetSizes.push_back(area); + + Rect item = readTargetArea(input["item"]); + + Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); + int itemsCount = input["itemsCount"].Integer(); + + auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getWanderingHeroes().size()); + + + if(!input["scrollUp"].isNull()) + result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); + + if(!input["scrollDown"].isNull()) + result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); + + subwidgetSizes.pop_back(); + + heroList = result; + return result; +} + +std::shared_ptr AdventureMapWidget::buildMapIcon(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + size_t index = input["index"].Integer(); + size_t perPlayer = input["perPlayer"].Integer(); + + return std::make_shared(area.topLeft(), loadAnimation(input["image"]), index, perPlayer); +} + +std::shared_ptr AdventureMapWidget::buildMapTownList(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + subwidgetSizes.push_back(area); + + Rect item = readTargetArea(input["item"]); + Point itemOffset(input["itemsOffset"]["x"].Integer(), input["itemsOffset"]["y"].Integer()); + int itemsCount = input["itemsCount"].Integer(); + + auto result = std::make_shared(itemsCount, area, item.topLeft() - area.topLeft(), itemOffset, LOCPLINT->localState->getOwnedTowns().size()); + + + if(!input["scrollUp"].isNull()) + result->setScrollUpButton(std::dynamic_pointer_cast(buildMapButton(input["scrollUp"]))); + + if(!input["scrollDown"].isNull()) + result->setScrollDownButton(std::dynamic_pointer_cast(buildMapButton(input["scrollDown"]))); + + subwidgetSizes.pop_back(); + + townList = result; + return result; +} + +std::shared_ptr AdventureMapWidget::buildMinimap(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + minimap = std::make_shared(area); + return minimap; +} + +std::shared_ptr AdventureMapWidget::buildResourceDateBar(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + auto image = ImagePath::fromJson(input["image"]); + + auto result = std::make_shared(image, area.topLeft()); + + for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + { + const auto & node = input[GameConstants::RESOURCE_NAMES[i]]; + + if(node.isNull()) + continue; + + result->setResourcePosition(GameResID(i), Point(node["x"].Integer(), node["y"].Integer())); + } + + result->setDatePosition(Point(input["date"]["x"].Integer(), input["date"]["y"].Integer())); + + return result; +} + +std::shared_ptr AdventureMapWidget::buildStatusBar(const JsonNode & input) +{ + Rect area = readTargetArea(input["area"]); + auto image = ImagePath::fromJson(input["image"]); + + auto background = std::make_shared(image, area); + + return CGStatusBar::create(background); +} + +std::shared_ptr AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input) +{ + logGlobal->debug("Building widget CFilledTexture"); + auto image = ImagePath::fromJson(input["image"]); + Rect area = readTargetArea(input["area"]); + return std::make_shared(image, area); +} + +std::shared_ptr AdventureMapWidget::getHeroList() +{ + return heroList; +} + +std::shared_ptr AdventureMapWidget::getTownList() +{ + return townList; +} + +std::shared_ptr AdventureMapWidget::getMinimap() +{ + return minimap; +} + +std::shared_ptr AdventureMapWidget::getMapView() +{ + return mapView; +} + +std::shared_ptr AdventureMapWidget::getInfoBar() +{ + return infoBar; +} + +void AdventureMapWidget::setPlayer(const PlayerColor & player) +{ + setPlayerChildren(this, player); +} + +void AdventureMapWidget::setPlayerChildren(CIntObject * widget, const PlayerColor & player) +{ + for(auto & entry : widget->children) + { + auto container = dynamic_cast(entry); + auto icon = dynamic_cast(entry); + auto button = dynamic_cast(entry); + auto resDataBar = dynamic_cast(entry); + auto texture = dynamic_cast(entry); + + if(button) + button->setPlayerColor(player); + + if(resDataBar) + resDataBar->colorize(player); + + if(icon) + icon->setPlayer(player); + + if(container) + setPlayerChildren(container, player); + + if (texture) + texture->playerColored(player); + } + + for(const auto & entry : playerColorerImages) + { + if(images.count(entry)) + images[entry]->playerColored(player); + } + + redraw(); +} + +CAdventureMapIcon::CAdventureMapIcon(const Point & position, std::shared_ptr animation, size_t index, size_t iconsPerPlayer) + : index(index) + , iconsPerPlayer(iconsPerPlayer) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += position; + image = std::make_shared(animation, index); +} + +void CAdventureMapIcon::setPlayer(const PlayerColor & player) +{ + image->setFrame(index + player.getNum() * iconsPerPlayer); +} + +void CAdventureMapOverlayWidget::show(Canvas & to) +{ + CIntObject::showAll(to); +} + +void AdventureMapWidget::updateActiveStateChildden(CIntObject * widget) +{ + for(auto & entry : widget->children) + { + auto container = dynamic_cast(entry); + + if (container) + { + if (container->disableCondition == "heroAwake") + container->setEnabled(!shortcuts->optionHeroSleeping()); + + if (container->disableCondition == "heroSleeping") + container->setEnabled(shortcuts->optionHeroSleeping()); + + if (container->disableCondition == "mapLayerSurface") + container->setEnabled(shortcuts->optionMapLevelSurface()); + + if (container->disableCondition == "mapLayerUnderground") + container->setEnabled(!shortcuts->optionMapLevelSurface()); + + if (container->disableCondition == "mapViewMode") + container->setEnabled(shortcuts->optionInWorldView()); + + if (container->disableCondition == "worldViewMode") + container->setEnabled(!shortcuts->optionInWorldView()); + + updateActiveStateChildden(container); + } + } +} + +void AdventureMapWidget::updateActiveState() +{ + updateActiveStateChildden(this); + + for (auto entry: shortcuts->getShortcuts()) + setShortcutBlocked(entry.shortcut, !entry.isEnabled); +} diff --git a/client/adventureMap/AdventureMapWidget.h b/client/adventureMap/AdventureMapWidget.h index fca239ffc..93a19eeb6 100644 --- a/client/adventureMap/AdventureMapWidget.h +++ b/client/adventureMap/AdventureMapWidget.h @@ -1,110 +1,110 @@ -/* - * CAdventureMapWidget.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 "../gui/InterfaceObjectConfigurable.h" - -class CAnimation; -class CHeroList; -class CTownList; -class CMinimap; -class MapView; -class CInfoBar; -class IImage; -class AdventureMapShortcuts; -enum class EAdventureState; - -/// Internal class of AdventureMapInterface that contains actual UI elements -class AdventureMapWidget : public InterfaceObjectConfigurable -{ - int mapLevel; - /// temporary stack of sizes of currently building widgets - std::vector subwidgetSizes; - - /// list of images on which player-colored palette will be applied - std::vector playerColorerImages; - - /// list of named images shared between widgets - std::map> images; - std::map> animations; - - /// Widgets that require access from adventure map - std::shared_ptr heroList; - std::shared_ptr townList; - std::shared_ptr minimap; - std::shared_ptr mapView; - std::shared_ptr infoBar; - - std::shared_ptr shortcuts; - - Rect readTargetArea(const JsonNode & source); - Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon); - Rect readArea(const JsonNode & source, const Rect & boundingBox); - - std::shared_ptr loadImage(const JsonNode & name); - std::shared_ptr loadAnimation(const JsonNode & name); - - std::shared_ptr buildInfobox(const JsonNode & input); - std::shared_ptr buildMapImage(const JsonNode & input); - std::shared_ptr buildMapButton(const JsonNode & input); - std::shared_ptr buildMapContainer(const JsonNode & input); - std::shared_ptr buildMapGameArea(const JsonNode & input); - std::shared_ptr buildMapHeroList(const JsonNode & input); - std::shared_ptr buildMapIcon(const JsonNode & input); - std::shared_ptr buildMapTownList(const JsonNode & input); - std::shared_ptr buildMinimap(const JsonNode & input); - std::shared_ptr buildResourceDateBar(const JsonNode & input); - std::shared_ptr buildStatusBar(const JsonNode & input); - std::shared_ptr buildTexturePlayerColored(const JsonNode &); - - - void setPlayerChildren(CIntObject * widget, const PlayerColor & player); - void updateActiveStateChildden(CIntObject * widget); -public: - explicit AdventureMapWidget( std::shared_ptr shortcuts ); - - std::shared_ptr getHeroList(); - std::shared_ptr getTownList(); - std::shared_ptr getMinimap(); - std::shared_ptr getMapView(); - std::shared_ptr getInfoBar(); - - void setPlayer(const PlayerColor & player); - - void onMapViewMoved(const Rect & visibleArea, int mapLevel); - void updateActiveState(); -}; - -/// Small helper class that provides ownership for shared_ptr's of child elements -class CAdventureMapContainerWidget : public CIntObject -{ - friend class AdventureMapWidget; - std::vector> ownedChildren; - std::string disableCondition; -}; - -class CAdventureMapOverlayWidget : public CAdventureMapContainerWidget -{ -public: - void show(Canvas & to) override; -}; - -/// Small helper class that provides player-colorable icon using animation file -class CAdventureMapIcon : public CIntObject -{ - std::shared_ptr image; - - size_t index; - size_t iconsPerPlayer; -public: - CAdventureMapIcon(const Point & position, std::shared_ptr image, size_t index, size_t iconsPerPlayer); - - void setPlayer(const PlayerColor & player); -}; +/* + * CAdventureMapWidget.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 "../gui/InterfaceObjectConfigurable.h" + +class CAnimation; +class CHeroList; +class CTownList; +class CMinimap; +class MapView; +class CInfoBar; +class IImage; +class AdventureMapShortcuts; +enum class EAdventureState; + +/// Internal class of AdventureMapInterface that contains actual UI elements +class AdventureMapWidget : public InterfaceObjectConfigurable +{ + int mapLevel; + /// temporary stack of sizes of currently building widgets + std::vector subwidgetSizes; + + /// list of images on which player-colored palette will be applied + std::vector playerColorerImages; + + /// list of named images shared between widgets + std::map> images; + std::map> animations; + + /// Widgets that require access from adventure map + std::shared_ptr heroList; + std::shared_ptr townList; + std::shared_ptr minimap; + std::shared_ptr mapView; + std::shared_ptr infoBar; + + std::shared_ptr shortcuts; + + Rect readTargetArea(const JsonNode & source); + Rect readSourceArea(const JsonNode & source, const JsonNode & sourceCommon); + Rect readArea(const JsonNode & source, const Rect & boundingBox); + + std::shared_ptr loadImage(const JsonNode & name); + std::shared_ptr loadAnimation(const JsonNode & name); + + std::shared_ptr buildInfobox(const JsonNode & input); + std::shared_ptr buildMapImage(const JsonNode & input); + std::shared_ptr buildMapButton(const JsonNode & input); + std::shared_ptr buildMapContainer(const JsonNode & input); + std::shared_ptr buildMapGameArea(const JsonNode & input); + std::shared_ptr buildMapHeroList(const JsonNode & input); + std::shared_ptr buildMapIcon(const JsonNode & input); + std::shared_ptr buildMapTownList(const JsonNode & input); + std::shared_ptr buildMinimap(const JsonNode & input); + std::shared_ptr buildResourceDateBar(const JsonNode & input); + std::shared_ptr buildStatusBar(const JsonNode & input); + std::shared_ptr buildTexturePlayerColored(const JsonNode &); + + + void setPlayerChildren(CIntObject * widget, const PlayerColor & player); + void updateActiveStateChildden(CIntObject * widget); +public: + explicit AdventureMapWidget( std::shared_ptr shortcuts ); + + std::shared_ptr getHeroList(); + std::shared_ptr getTownList(); + std::shared_ptr getMinimap(); + std::shared_ptr getMapView(); + std::shared_ptr getInfoBar(); + + void setPlayer(const PlayerColor & player); + + void onMapViewMoved(const Rect & visibleArea, int mapLevel); + void updateActiveState(); +}; + +/// Small helper class that provides ownership for shared_ptr's of child elements +class CAdventureMapContainerWidget : public CIntObject +{ + friend class AdventureMapWidget; + std::vector> ownedChildren; + std::string disableCondition; +}; + +class CAdventureMapOverlayWidget : public CAdventureMapContainerWidget +{ +public: + void show(Canvas & to) override; +}; + +/// Small helper class that provides player-colorable icon using animation file +class CAdventureMapIcon : public CIntObject +{ + std::shared_ptr image; + + size_t index; + size_t iconsPerPlayer; +public: + CAdventureMapIcon(const Point & position, std::shared_ptr image, size_t index, size_t iconsPerPlayer); + + void setPlayer(const PlayerColor & player); +}; diff --git a/client/adventureMap/AdventureOptions.cpp b/client/adventureMap/AdventureOptions.cpp index 328baedeb..265f8e5a0 100644 --- a/client/adventureMap/AdventureOptions.cpp +++ b/client/adventureMap/AdventureOptions.cpp @@ -1,61 +1,61 @@ -/* - * CAdventureOptions.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 "AdventureOptions.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" -#include "../lobby/CCampaignInfoScreen.h" -#include "../lobby/CScenarioInfoScreen.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" -#include "../gui/Shortcut.h" -#include "../widgets/Buttons.h" - -#include "../../CCallback.h" -#include "../../lib/StartInfo.h" - -AdventureOptions::AdventureOptions() - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); - viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); - - exit = std::make_shared(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); - - scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); - scenInfo->addCallback(AdventureOptions::showScenarioInfo); - - puzzle = std::make_shared(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); - puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); - - dig = std::make_shared(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); - if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero()) - dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); - else - dig->block(true); -} - -void AdventureOptions::showScenarioInfo() -{ - if(LOCPLINT->cb->getStartInfo()->campState) - { - GH.windows().createAndPushWindow(); - } - else - { - GH.windows().createAndPushWindow(); - } -} - +/* + * CAdventureOptions.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 "AdventureOptions.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" +#include "../lobby/CCampaignInfoScreen.h" +#include "../lobby/CScenarioInfoScreen.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" + +#include "../../CCallback.h" +#include "../../lib/StartInfo.h" + +AdventureOptions::AdventureOptions() + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("ADVOPTS")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + viewWorld = std::make_shared(Point(24, 23), AnimationPath::builtin("ADVVIEW.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD); + viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); }); + + exit = std::make_shared(Point(204, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN); + + scenInfo = std::make_shared(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO); + scenInfo->addCallback(AdventureOptions::showScenarioInfo); + + puzzle = std::make_shared(Point(24, 81), AnimationPath::builtin("ADVPUZ.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE); + puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT)); + + dig = std::make_shared(Point(24, 139), AnimationPath::builtin("ADVDIG.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL); + if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero()) + dig->addCallback(std::bind(&CPlayerInterface::tryDigging, LOCPLINT, h)); + else + dig->block(true); +} + +void AdventureOptions::showScenarioInfo() +{ + if(LOCPLINT->cb->getStartInfo()->campState) + { + GH.windows().createAndPushWindow(); + } + else + { + GH.windows().createAndPushWindow(); + } +} + diff --git a/client/adventureMap/AdventureOptions.h b/client/adventureMap/AdventureOptions.h index 2c1c8ddd3..41b47c56e 100644 --- a/client/adventureMap/AdventureOptions.h +++ b/client/adventureMap/AdventureOptions.h @@ -1,31 +1,31 @@ -/* - * CAdventureOptions.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 "../windows/CWindowObject.h" - -class CButton; - -/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,... -class AdventureOptions : public CWindowObject -{ - std::shared_ptr exit; - std::shared_ptr viewWorld; - std::shared_ptr puzzle; - std::shared_ptr dig; - std::shared_ptr scenInfo; - /*std::shared_ptr replay*/ - -public: - AdventureOptions(); - - static void showScenarioInfo(); -}; - +/* + * CAdventureOptions.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 "../windows/CWindowObject.h" + +class CButton; + +/// Adventure options dialog where you can view the world, dig, play the replay of the last turn,... +class AdventureOptions : public CWindowObject +{ + std::shared_ptr exit; + std::shared_ptr viewWorld; + std::shared_ptr puzzle; + std::shared_ptr dig; + std::shared_ptr scenInfo; + /*std::shared_ptr replay*/ + +public: + AdventureOptions(); + + static void showScenarioInfo(); +}; + diff --git a/client/adventureMap/CResDataBar.cpp b/client/adventureMap/CResDataBar.cpp index ea7914ecf..7096b47bc 100644 --- a/client/adventureMap/CResDataBar.cpp +++ b/client/adventureMap/CResDataBar.cpp @@ -1,90 +1,90 @@ -/* - * CResDataBar.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 "CResDataBar.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/Colors.h" -#include "../render/EFont.h" -#include "../gui/CGuiHandler.h" -#include "../gui/TextAlignment.h" -#include "../widgets/Images.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/ResourceSet.h" - -CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) -{ - pos.x += position.x; - pos.y += position.y; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared(imageName, 0, 0); - background->colorize(LOCPLINT->playerID); - - pos.w = background->pos.w; - pos.h = background->pos.h; -} - -CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist): - CResDataBar(defname, Point(x,y)) -{ - for (int i = 0; i < 7 ; i++) - resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy ); - - datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0); -} - -void CResDataBar::setDatePosition(const Point & position) -{ - datePosition = position; -} - -void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position) -{ - resourcePositions[resource] = position; -} - -std::string CResDataBar::buildDateString() -{ - std::string pattern = "%s: %d, %s: %d, %s: %d"; - - auto formatted = boost::format(pattern) - % CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH) - % CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK) - % CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK); - - return boost::str(formatted); -} - -void CResDataBar::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - //TODO: all this should be labels, but they require proper text update on change - for (auto & entry : resourcePositions) - { - std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first)); - - to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text); - } - - if (datePosition) - to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString()); -} - -void CResDataBar::colorize(PlayerColor player) -{ - background->colorize(player); -} +/* + * CResDataBar.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 "CResDataBar.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../render/Colors.h" +#include "../render/EFont.h" +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../widgets/Images.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/ResourceSet.h" + +CResDataBar::CResDataBar(const ImagePath & imageName, const Point & position) +{ + pos.x += position.x; + pos.y += position.y; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + background = std::make_shared(imageName, 0, 0); + background->colorize(LOCPLINT->playerID); + + pos.w = background->pos.w; + pos.h = background->pos.h; +} + +CResDataBar::CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist): + CResDataBar(defname, Point(x,y)) +{ + for (int i = 0; i < 7 ; i++) + resourcePositions[GameResID(i)] = Point( offx + resdist*i, offy ); + + datePosition = resourcePositions[EGameResID::GOLD] + Point(datedist, 0); +} + +void CResDataBar::setDatePosition(const Point & position) +{ + datePosition = position; +} + +void CResDataBar::setResourcePosition(const GameResID & resource, const Point & position) +{ + resourcePositions[resource] = position; +} + +std::string CResDataBar::buildDateString() +{ + std::string pattern = "%s: %d, %s: %d, %s: %d"; + + auto formatted = boost::format(pattern) + % CGI->generaltexth->translate("core.genrltxt.62") % LOCPLINT->cb->getDate(Date::MONTH) + % CGI->generaltexth->translate("core.genrltxt.63") % LOCPLINT->cb->getDate(Date::WEEK) + % CGI->generaltexth->translate("core.genrltxt.64") % LOCPLINT->cb->getDate(Date::DAY_OF_WEEK); + + return boost::str(formatted); +} + +void CResDataBar::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + //TODO: all this should be labels, but they require proper text update on change + for (auto & entry : resourcePositions) + { + std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(entry.first)); + + to.drawText(pos.topLeft() + entry.second, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, text); + } + + if (datePosition) + to.drawText(pos.topLeft() + *datePosition, FONT_SMALL, Colors::WHITE, ETextAlignment::TOPLEFT, buildDateString()); +} + +void CResDataBar::colorize(PlayerColor player) +{ + background->colorize(player); +} diff --git a/client/adventureMap/CResDataBar.h b/client/adventureMap/CResDataBar.h index b37abeacf..3bf294ad4 100644 --- a/client/adventureMap/CResDataBar.h +++ b/client/adventureMap/CResDataBar.h @@ -1,40 +1,40 @@ -/* - * CResDataBar.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 "../gui/CIntObject.h" -#include "../../lib/filesystem/ResourcePath.h" - -/// Resources bar which shows information about how many gold, crystals,... you have -/// Current date is displayed too -class CResDataBar : public CIntObject -{ - std::string buildDateString(); - - std::shared_ptr background; - - std::map resourcePositions; - std::optional datePosition; - -public: - - /// For dynamically-sized UI windows, e.g. adventure map interface - CResDataBar(const ImagePath & imageName, const Point & position); - - /// For fixed-size UI windows, e.g. CastleInterface - CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist); - - void setDatePosition(const Point & position); - void setResourcePosition(const GameResID & resource, const Point & position); - - void colorize(PlayerColor player); - void showAll(Canvas & to) override; -}; - +/* + * CResDataBar.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 "../gui/CIntObject.h" +#include "../../lib/filesystem/ResourcePath.h" + +/// Resources bar which shows information about how many gold, crystals,... you have +/// Current date is displayed too +class CResDataBar : public CIntObject +{ + std::string buildDateString(); + + std::shared_ptr background; + + std::map resourcePositions; + std::optional datePosition; + +public: + + /// For dynamically-sized UI windows, e.g. adventure map interface + CResDataBar(const ImagePath & imageName, const Point & position); + + /// For fixed-size UI windows, e.g. CastleInterface + CResDataBar(const ImagePath & defname, int x, int y, int offx, int offy, int resdist, int datedist); + + void setDatePosition(const Point & position); + void setResourcePosition(const GameResID & resource, const Point & position); + + void colorize(PlayerColor player); + void showAll(Canvas & to) override; +}; + diff --git a/client/adventureMap/MapAudioPlayer.cpp b/client/adventureMap/MapAudioPlayer.cpp index 4787655f8..64218498a 100644 --- a/client/adventureMap/MapAudioPlayer.cpp +++ b/client/adventureMap/MapAudioPlayer.cpp @@ -1,252 +1,252 @@ -/* - * MapAudioPlayer.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 "MapAudioPlayer.h" - -#include "../CCallback.h" -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../mapView/mapHandler.h" - -#include "../../lib/TerrainHandler.h" -#include "../../lib/mapObjects/CArmedInstance.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapping/CMap.h" - -bool MapAudioPlayer::hasOngoingAnimations() -{ - return false; -} - -void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(obj == currentSelection) - update(); -} - -void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - addObject(obj); -} - -void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - removeObject(obj); -} - -void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - addObject(obj); -} - -void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - removeObject(obj); -} - -void MapAudioPlayer::addObject(const CGObjectInstance * obj) -{ - if(obj->isTile2Terrain()) - { - // terrain overlay - all covering tiles act as sound source - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - } - } - return; - } - - if(obj->isVisitable()) - { - // visitable object - visitable tile acts as sound source - int3 currTile = obj->visitablePos(); - - if(LOCPLINT->cb->isInTheMap(currTile)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - - return; - } - - if(!obj->isVisitable()) - { - // static object - blocking tiles act as sound source - auto tiles = obj->getBlockedOffsets(); - - for(const auto & tile : tiles) - { - int3 currTile = obj->pos + tile; - - if(LOCPLINT->cb->isInTheMap(currTile)) - objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); - } - return; - } -} - -void MapAudioPlayer::removeObject(const CGObjectInstance * obj) -{ - for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) - for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) - for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) - vstd::erase(objects[z][x][y], obj->id); -} - -std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) -{ - std::vector result; - - for(auto & objectID : objects[tile.z][tile.x][tile.y]) - { - const auto & object = CGI->mh->getMap()->objects[objectID.getNum()]; - - assert(object); - if (!object) - logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z); - - if(object && object->getAmbientSound()) - result.push_back(object->getAmbientSound().value()); - } - - if(CGI->mh->getMap()->isCoastalTile(tile)) - result.emplace_back(AudioPath::builtin("LOOPOCEA")); - - return result; -} - -void MapAudioPlayer::updateAmbientSounds() -{ - std::map currentSounds; - auto updateSounds = [&](const AudioPath& soundId, int distance) -> void - { - if(vstd::contains(currentSounds, soundId)) - currentSounds[soundId] = std::min(currentSounds[soundId], distance); - else - currentSounds.insert(std::make_pair(soundId, distance)); - }; - - int3 pos = currentSelection->getSightCenter(); - std::unordered_set tiles; - LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV); - for(int3 tile : tiles) - { - int dist = pos.dist(tile, int3::DIST_CHEBYSHEV); - - for(auto & soundName : getAmbientSounds(tile)) - updateSounds(soundName, dist); - } - CCS->soundh->ambientUpdateChannels(currentSounds); -} - -void MapAudioPlayer::updateMusic() -{ - if(audioPlaying && playerMakingTurn && currentSelection) - { - const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; - CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); - } - - if(audioPlaying && enemyMakingTurn) - { - CCS->musich->playMusicFromSet("enemy-turn", true, false); - } -} - -void MapAudioPlayer::update() -{ - updateMusic(); - - if(audioPlaying && playerMakingTurn && currentSelection) - updateAmbientSounds(); -} - -MapAudioPlayer::MapAudioPlayer() -{ - auto mapSize = LOCPLINT->cb->getMapSize(); - - objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); - - for(const auto & obj : CGI->mh->getMap()->objects) - { - if (obj) - addObject(obj); - } -} - -MapAudioPlayer::~MapAudioPlayer() -{ - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} - -void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection) -{ - currentSelection = newSelection; - update(); -} - -void MapAudioPlayer::onAudioPaused() -{ - audioPlaying = false; - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} - -void MapAudioPlayer::onAudioResumed() -{ - audioPlaying = true; - update(); -} - -void MapAudioPlayer::onPlayerTurnStarted() -{ - enemyMakingTurn = false; - playerMakingTurn = true; - update(); -} - -void MapAudioPlayer::onEnemyTurnStarted() -{ - playerMakingTurn = false; - enemyMakingTurn = true; - update(); -} - -void MapAudioPlayer::onPlayerTurnEnded() -{ - playerMakingTurn = false; - enemyMakingTurn = false; - CCS->soundh->ambientStopAllChannels(); - CCS->musich->stopMusic(1000); -} +/* + * MapAudioPlayer.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 "MapAudioPlayer.h" + +#include "../CCallback.h" +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../mapView/mapHandler.h" + +#include "../../lib/TerrainHandler.h" +#include "../../lib/mapObjects/CArmedInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapping/CMap.h" + +bool MapAudioPlayer::hasOngoingAnimations() +{ + return false; +} + +void MapAudioPlayer::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(obj == currentSelection) + update(); +} + +void MapAudioPlayer::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +} + +void MapAudioPlayer::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +} + +void MapAudioPlayer::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +} + +void MapAudioPlayer::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +} + +void MapAudioPlayer::addObject(const CGObjectInstance * obj) +{ + if(obj->isTile2Terrain()) + { + // terrain overlay - all covering tiles act as sound source + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + } + } + return; + } + + if(obj->isVisitable()) + { + // visitable object - visitable tile acts as sound source + int3 currTile = obj->visitablePos(); + + if(LOCPLINT->cb->isInTheMap(currTile)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + + return; + } + + if(!obj->isVisitable()) + { + // static object - blocking tiles act as sound source + auto tiles = obj->getBlockedOffsets(); + + for(const auto & tile : tiles) + { + int3 currTile = obj->pos + tile; + + if(LOCPLINT->cb->isInTheMap(currTile)) + objects[currTile.z][currTile.x][currTile.y].push_back(obj->id); + } + return; + } +} + +void MapAudioPlayer::removeObject(const CGObjectInstance * obj) +{ + for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) + for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) + for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) + vstd::erase(objects[z][x][y], obj->id); +} + +std::vector MapAudioPlayer::getAmbientSounds(const int3 & tile) +{ + std::vector result; + + for(auto & objectID : objects[tile.z][tile.x][tile.y]) + { + const auto & object = CGI->mh->getMap()->objects[objectID.getNum()]; + + assert(object); + if (!object) + logGlobal->warn("Already removed object %d found on tile! (%d %d %d)", objectID.getNum(), tile.x, tile.y, tile.z); + + if(object && object->getAmbientSound()) + result.push_back(object->getAmbientSound().value()); + } + + if(CGI->mh->getMap()->isCoastalTile(tile)) + result.emplace_back(AudioPath::builtin("LOOPOCEA")); + + return result; +} + +void MapAudioPlayer::updateAmbientSounds() +{ + std::map currentSounds; + auto updateSounds = [&](const AudioPath& soundId, int distance) -> void + { + if(vstd::contains(currentSounds, soundId)) + currentSounds[soundId] = std::min(currentSounds[soundId], distance); + else + currentSounds.insert(std::make_pair(soundId, distance)); + }; + + int3 pos = currentSelection->getSightCenter(); + std::unordered_set tiles; + LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV); + for(int3 tile : tiles) + { + int dist = pos.dist(tile, int3::DIST_CHEBYSHEV); + + for(auto & soundName : getAmbientSounds(tile)) + updateSounds(soundName, dist); + } + CCS->soundh->ambientUpdateChannels(currentSounds); +} + +void MapAudioPlayer::updateMusic() +{ + if(audioPlaying && playerMakingTurn && currentSelection) + { + const auto * terrain = LOCPLINT->cb->getTile(currentSelection->visitablePos())->terType; + CCS->musich->playMusicFromSet("terrain", terrain->getJsonKey(), true, false); + } + + if(audioPlaying && enemyMakingTurn) + { + CCS->musich->playMusicFromSet("enemy-turn", true, false); + } +} + +void MapAudioPlayer::update() +{ + updateMusic(); + + if(audioPlaying && playerMakingTurn && currentSelection) + updateAmbientSounds(); +} + +MapAudioPlayer::MapAudioPlayer() +{ + auto mapSize = LOCPLINT->cb->getMapSize(); + + objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + for(const auto & obj : CGI->mh->getMap()->objects) + { + if (obj) + addObject(obj); + } +} + +MapAudioPlayer::~MapAudioPlayer() +{ + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} + +void MapAudioPlayer::onSelectionChanged(const CArmedInstance * newSelection) +{ + currentSelection = newSelection; + update(); +} + +void MapAudioPlayer::onAudioPaused() +{ + audioPlaying = false; + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} + +void MapAudioPlayer::onAudioResumed() +{ + audioPlaying = true; + update(); +} + +void MapAudioPlayer::onPlayerTurnStarted() +{ + enemyMakingTurn = false; + playerMakingTurn = true; + update(); +} + +void MapAudioPlayer::onEnemyTurnStarted() +{ + playerMakingTurn = false; + enemyMakingTurn = true; + update(); +} + +void MapAudioPlayer::onPlayerTurnEnded() +{ + playerMakingTurn = false; + enemyMakingTurn = false; + CCS->soundh->ambientStopAllChannels(); + CCS->musich->stopMusic(1000); +} diff --git a/client/adventureMap/MapAudioPlayer.h b/client/adventureMap/MapAudioPlayer.h index edb83b051..938bf1abe 100644 --- a/client/adventureMap/MapAudioPlayer.h +++ b/client/adventureMap/MapAudioPlayer.h @@ -1,77 +1,77 @@ -/* - * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h" -#include "../../lib/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN -class ObjectInstanceID; -class CArmedInstance; -class PlayerColor; -VCMI_LIB_NAMESPACE_END - -class MapAudioPlayer : public IMapObjectObserver -{ - using MapObjectsList = std::vector; - - boost::multi_array objects; - const CArmedInstance * currentSelection = nullptr; - bool playerMakingTurn = false; - bool enemyMakingTurn = false; - bool audioPlaying = true; - - void addObject(const CGObjectInstance * obj); - void removeObject(const CGObjectInstance * obj); - - std::vector getAmbientSounds(const int3 & tile); - void updateAmbientSounds(); - void updateMusic(); - void update(); - -protected: - // IMapObjectObserver impl - bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; - - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} - -public: - MapAudioPlayer(); - ~MapAudioPlayer() override; - - /// Called whenever current adventure map selection changes - void onSelectionChanged(const CArmedInstance * newSelection); - - /// Called when local player starts his turn - void onPlayerTurnStarted(); - - /// Called when AI or non-local player start his turn - void onEnemyTurnStarted(); - - /// Called when local player ends his turn - void onPlayerTurnEnded(); - - /// Called when map audio should be paused, e.g. on combat or town scren access - void onAudioPaused(); - - /// Called when map audio should be resume, opposite to onPaused - void onAudioResumed(); -}; +/* + * MapAudioPlayer.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 "../mapView/IMapRendererObserver.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN +class ObjectInstanceID; +class CArmedInstance; +class PlayerColor; +VCMI_LIB_NAMESPACE_END + +class MapAudioPlayer : public IMapObjectObserver +{ + using MapObjectsList = std::vector; + + boost::multi_array objects; + const CArmedInstance * currentSelection = nullptr; + bool playerMakingTurn = false; + bool enemyMakingTurn = false; + bool audioPlaying = true; + + void addObject(const CGObjectInstance * obj); + void removeObject(const CGObjectInstance * obj); + + std::vector getAmbientSounds(const int3 & tile); + void updateAmbientSounds(); + void updateMusic(); + void update(); + +protected: + // IMapObjectObserver impl + bool hasOngoingAnimations() override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; + + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override {} + +public: + MapAudioPlayer(); + ~MapAudioPlayer() override; + + /// Called whenever current adventure map selection changes + void onSelectionChanged(const CArmedInstance * newSelection); + + /// Called when local player starts his turn + void onPlayerTurnStarted(); + + /// Called when AI or non-local player start his turn + void onEnemyTurnStarted(); + + /// Called when local player ends his turn + void onPlayerTurnEnded(); + + /// Called when map audio should be paused, e.g. on combat or town scren access + void onAudioPaused(); + + /// Called when map audio should be resume, opposite to onPaused + void onAudioResumed(); +}; diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 745bb325b..abf2fb632 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -1,1054 +1,1054 @@ -/* - * BattleActionsController.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 "BattleActionsController.h" - -#include "BattleWindow.h" -#include "BattleStacksController.h" -#include "BattleInterface.h" -#include "BattleFieldController.h" -#include "BattleSiegeController.h" -#include "BattleInterfaceClasses.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CIntObject.h" -#include "../gui/WindowHandler.h" -#include "../windows/CCreatureWindow.h" -#include "../windows/InfoWindows.h" - -#include "../../CCallback.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CStack.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/spells/Problem.h" - -struct TextReplacement -{ - std::string placeholder; - std::string replacement; -}; - -using TextReplacementList = std::vector; - -static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) -{ - for(const auto & entry : format) - boost::replace_all(input, entry.placeholder, entry.replacement); - - return input; -} - -static std::string translatePlural(int amount, const std::string& baseTextID) -{ - if(amount == 1) - return CGI->generaltexth->translate(baseTextID + ".1"); - return CGI->generaltexth->translate(baseTextID); -} - -static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) -{ - std::string baseString = translatePlural(amount, baseTextID); - TextReplacementList replacements { - { "%d", amountString } - }; - - return replacePlaceholders(baseString, replacements); -} - -static std::string formatPlural(int amount, const std::string & baseTextID) -{ - return formatPluralImpl(amount, std::to_string(amount), baseTextID); -} - -static std::string formatPlural(DamageRange range, const std::string & baseTextID) -{ - if (range.min == range.max) - return formatPlural(range.min, baseTextID); - - std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); - - return formatPluralImpl(range.max, rangeString, baseTextID); -} - -static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) -{ - TextReplacementList replacements = { - { "%CREATURE", creatureName }, - { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, - { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, - { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, - }; - - return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); -} - -static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) -{ - std::string baseTextID = estimation.kills.max == 0 ? - "vcmi.battleWindow.damageEstimation.melee" : - "vcmi.battleWindow.damageEstimation.meleeKills"; - - return formatAttack(estimation, creatureName, baseTextID, 0); -} - -static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) -{ - std::string baseTextID = estimation.kills.max == 0 ? - "vcmi.battleWindow.damageEstimation.ranged" : - "vcmi.battleWindow.damageEstimation.rangedKills"; - - return formatAttack(estimation, creatureName, baseTextID, shotsLeft); -} - -BattleActionsController::BattleActionsController(BattleInterface & owner): - owner(owner), - selectedStack(nullptr), - heroSpellToCast(nullptr) -{ -} - -void BattleActionsController::endCastingSpell() -{ - if(heroSpellToCast) - { - heroSpellToCast.reset(); - owner.windowObject->blockUI(false); - } - - if(owner.stacksController->getActiveStack()) - possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared - - selectedStack = nullptr; - GH.fakeMouseMove(); -} - -bool BattleActionsController::isActiveStackSpellcaster() const -{ - const CStack * casterStack = owner.stacksController->getActiveStack(); - if (!casterStack) - return false; - - bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); - return (spellcaster && casterStack->canCast()); -} - -void BattleActionsController::enterCreatureCastingMode() -{ - //silently check for possible errors - if (owner.tacticsMode) - return; - - //hero is casting a spell - if (heroSpellToCast) - return; - - if (!owner.stacksController->getActiveStack()) - return; - - if (!isActiveStackSpellcaster()) - return; - - for(const auto & action : possibleActions) - { - if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) - continue; - - const spells::Caster * caster = owner.stacksController->getActiveStack(); - const CSpell * spell = action.spell().toSpell(); - - spells::Target target; - target.emplace_back(); - - spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell); - - auto m = spell->battleMechanics(&cast); - spells::detail::ProblemImpl ignored; - - const bool isCastingPossible = m->canBeCastAt(target, ignored); - - if (isCastingPossible) - { - owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId()); - selectedStack = nullptr; - - CCS->curh->set(Cursor::Combat::POINTER); - } - return; - } - - possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); - - auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) - { - return !x.spellcast(); - }; - - vstd::erase_if(possibleActions, actionFilterPredicate); - GH.fakeMouseMove(); -} - -std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const -{ - BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass - - for(const auto & spell : creatureSpells) - data.creatureSpellsToCast.push_back(spell->id); - - data.tacticsMode = owner.tacticsMode; - auto allActions = owner.getBattle()->getClientActionsForStack(stack, data); - - allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); - allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); - - return std::vector(allActions); -} - -void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack) -{ - if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack - - auto assignPriority = [&](const PossiblePlayerBattleAction & item - ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it - { - switch(item.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::NO_LOCATION: - case PossiblePlayerBattleAction::FREE_LOCATION: - case PossiblePlayerBattleAction::OBSTACLE: - if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) - { - PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack); - bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; - bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; - - if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast) - return 1; - } - return 100; //bottom priority - - break; - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - return 2; - break; - case PossiblePlayerBattleAction::SHOOT: - return 4; - break; - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - return 5; - break; - case PossiblePlayerBattleAction::ATTACK: - return 6; - break; - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - return 7; - break; - case PossiblePlayerBattleAction::MOVE_STACK: - return 8; - break; - case PossiblePlayerBattleAction::CATAPULT: - return 9; - break; - case PossiblePlayerBattleAction::HEAL: - return 10; - break; - case PossiblePlayerBattleAction::CREATURE_INFO: - return 11; - break; - case PossiblePlayerBattleAction::HERO_INFO: - return 12; - break; - case PossiblePlayerBattleAction::TELEPORT: - return 13; - break; - default: - assert(0); - return 200; - break; - } - }; - - auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs) - { - return assignPriority(lhs) < assignPriority(rhs); - }; - - std::sort(possibleActions.begin(), possibleActions.end(), comparer); -} - -void BattleActionsController::castThisSpell(SpellID spellID) -{ - heroSpellToCast = std::make_shared(); - heroSpellToCast->actionType = EActionType::HERO_SPELL; - heroSpellToCast->spell = spellID; - heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; - heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; - - //choosing possible targets - const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; - assert(castingHero); // code below assumes non-null hero - PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); - - if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location - { - heroSpellToCast->aimToHex(BattleHex::INVALID); - owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); - endCastingSpell(); - } - else - { - possibleActions.clear(); - possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment - GH.fakeMouseMove();//update cursor - } - - owner.windowObject->blockUI(true); -} - -const CSpell * BattleActionsController::getHeroSpellToCast( ) const -{ - if (heroSpellToCast) - return heroSpellToCast->spell.toSpell(); - return nullptr; -} - -const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) -{ - if (heroSpellToCast) - return nullptr; - - if (!owner.stacksController->getActiveStack()) - return nullptr; - - if (!hoveredHex.isValid()) - return nullptr; - - auto action = selectAction(hoveredHex); - - if (action.spell() == SpellID::NONE) - return nullptr; - - return action.spell().toSpell(); -} - -const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) -{ - if (getHeroSpellToCast()) - return getHeroSpellToCast(); - return getStackSpellToCast(hoveredHex); -} - -const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) -{ - const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true); - if(shere) - return shere; - return owner.getBattle()->battleGetStackByPos(hoveredHex, false); -} - -void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - CCS->curh->set(Cursor::Combat::POINTER); - return; - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) - CCS->curh->set(Cursor::Combat::FLY); - else - CCS->curh->set(Cursor::Combat::MOVE); - return; - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - { - static const std::map sectorCursor = { - {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST}, - {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST}, - {BattleHex::RIGHT, Cursor::Combat::HIT_WEST }, - {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST}, - {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST}, - {BattleHex::LEFT, Cursor::Combat::HIT_EAST }, - {BattleHex::TOP, Cursor::Combat::HIT_SOUTH }, - {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH } - }; - - auto direction = owner.fieldController->selectAttackDirection(targetHex); - - assert(sectorCursor.count(direction) > 0); - if (sectorCursor.count(direction)) - CCS->curh->set(sectorCursor.at(direction)); - - return; - } - - case PossiblePlayerBattleAction::SHOOT: - if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) - CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); - else - CCS->curh->set(Cursor::Combat::SHOOT); - return; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - case PossiblePlayerBattleAction::FREE_LOCATION: - case PossiblePlayerBattleAction::OBSTACLE: - CCS->curh->set(Cursor::Spellcast::SPELL); - return; - - case PossiblePlayerBattleAction::TELEPORT: - CCS->curh->set(Cursor::Combat::TELEPORT); - return; - - case PossiblePlayerBattleAction::SACRIFICE: - CCS->curh->set(Cursor::Combat::SACRIFICE); - return; - - case PossiblePlayerBattleAction::HEAL: - CCS->curh->set(Cursor::Combat::HEAL); - return; - - case PossiblePlayerBattleAction::CATAPULT: - CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT); - return; - - case PossiblePlayerBattleAction::CREATURE_INFO: - CCS->curh->set(Cursor::Combat::QUERY); - return; - case PossiblePlayerBattleAction::HERO_INFO: - CCS->curh->set(Cursor::Combat::HERO); - return; - } - assert(0); -} - -void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - case PossiblePlayerBattleAction::TELEPORT: - case PossiblePlayerBattleAction::SACRIFICE: - case PossiblePlayerBattleAction::FREE_LOCATION: - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - default: - if (targetHex == -1) - CCS->curh->set(Cursor::Combat::POINTER); - else - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - } - assert(0); -} - -std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - - switch (action.get()) //display console message, realize selected action - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) - return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here - else - return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return - { - BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); - estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); - estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); - - return formatMeleeAttack(estimation, targetStack->getName()); - } - - case PossiblePlayerBattleAction::SHOOT: - { - const auto * shooter = owner.stacksController->getActiveStack(); - - DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); - estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); - estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); - - return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); - } - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s - - case PossiblePlayerBattleAction::ANY_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s - - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell - return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % - - case PossiblePlayerBattleAction::TELEPORT: - return CGI->generaltexth->allTexts[25]; //Teleport Here - - case PossiblePlayerBattleAction::OBSTACLE: - return CGI->generaltexth->allTexts[550]; - - case PossiblePlayerBattleAction::SACRIFICE: - return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s - - case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s - - case PossiblePlayerBattleAction::HEAL: - return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s - - case PossiblePlayerBattleAction::CATAPULT: - return ""; // TODO - - case PossiblePlayerBattleAction::CREATURE_INFO: - return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str(); - - case PossiblePlayerBattleAction::HERO_INFO: - return CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats" - } - assert(0); - return ""; -} - -std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - switch (action.get()) - { - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - return CGI->generaltexth->allTexts[23]; - break; - case PossiblePlayerBattleAction::TELEPORT: - return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination - break; - case PossiblePlayerBattleAction::SACRIFICE: - return CGI->generaltexth->allTexts[543]; //choose army to sacrifice - break; - case PossiblePlayerBattleAction::FREE_LOCATION: - return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here - break; - default: - return ""; - } -} - -bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID; - - switch (action.get()) - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - return (targetStack && targetStackOwned && targetStack->speed() > 0); - - case PossiblePlayerBattleAction::CREATURE_INFO: - return (targetStack && targetStackOwned && targetStack->alive()); - - case PossiblePlayerBattleAction::HERO_INFO: - if (targetHex == BattleHex::HERO_ATTACKER) - return owner.attackingHero != nullptr; - - if (targetHex == BattleHex::HERO_DEFENDER) - return owner.defendingHero != nullptr; - - return false; - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - if (!(targetStack && targetStack->alive())) //we can walk on dead stacks - { - if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex)) - return true; - } - return false; - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) - { - if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? - return true; - } - return false; - - case PossiblePlayerBattleAction::SHOOT: - return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); - - case PossiblePlayerBattleAction::NO_LOCATION: - return false; - - case PossiblePlayerBattleAction::ANY_LOCATION: - return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: - if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures - { - int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); - return spellID > -1; - } - return false; - - case PossiblePlayerBattleAction::TELEPORT: - return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex); - - case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice - return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive(); - - case PossiblePlayerBattleAction::OBSTACLE: - case PossiblePlayerBattleAction::FREE_LOCATION: - return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); - - case PossiblePlayerBattleAction::CATAPULT: - return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); - - case PossiblePlayerBattleAction::HEAL: - return targetStack && targetStackOwned && targetStack->canBeHealed(); - } - - assert(0); - return false; -} - -void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex) -{ - const CStack * targetStack = getStackForHex(targetHex); - - switch (action.get()) //display console message, realize selected action - { - case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: - { - owner.stackActivated(targetStack); - return; - } - - case PossiblePlayerBattleAction::MOVE_TACTICS: - case PossiblePlayerBattleAction::MOVE_STACK: - { - if(owner.stacksController->getActiveStack()->doubleWide()) - { - std::vector acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); - BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); - if(vstd::contains(acc, targetHex)) - owner.giveCommand(EActionType::WALK, targetHex); - else if(vstd::contains(acc, shiftedDest)) - owner.giveCommand(EActionType::WALK, shiftedDest); - } - else - { - owner.giveCommand(EActionType::WALK, targetHex); - } - return; - } - - case PossiblePlayerBattleAction::ATTACK: - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return - { - bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; - BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); - if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) - { - BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack); - owner.sendCommand(command, owner.stacksController->getActiveStack()); - } - return; - } - - case PossiblePlayerBattleAction::SHOOT: - { - owner.giveCommand(EActionType::SHOOT, targetHex); - return; - } - - case PossiblePlayerBattleAction::HEAL: - { - owner.giveCommand(EActionType::STACK_HEAL, targetHex); - return; - }; - - case PossiblePlayerBattleAction::CATAPULT: - { - owner.giveCommand(EActionType::CATAPULT, targetHex); - return; - } - - case PossiblePlayerBattleAction::CREATURE_INFO: - { - GH.windows().createAndPushWindow(targetStack, false); - return; - } - - case PossiblePlayerBattleAction::HERO_INFO: - { - if (targetHex == BattleHex::HERO_ATTACKER) - owner.attackingHero->heroLeftClicked(); - - if (targetHex == BattleHex::HERO_DEFENDER) - owner.defendingHero->heroLeftClicked(); - - return; - } - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - case PossiblePlayerBattleAction::ANY_LOCATION: - case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell - case PossiblePlayerBattleAction::TELEPORT: - case PossiblePlayerBattleAction::OBSTACLE: - case PossiblePlayerBattleAction::SACRIFICE: - case PossiblePlayerBattleAction::FREE_LOCATION: - { - if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) - { - if (action.spell() == SpellID::SACRIFICE) - { - heroSpellToCast->aimToHex(targetHex); - possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()}); - selectedStack = targetStack; - return; - } - if (action.spell() == SpellID::TELEPORT) - { - heroSpellToCast->aimToUnit(targetStack); - possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()}); - selectedStack = targetStack; - return; - } - } - - if (!spellcastingModeActive()) - { - if (action.spell().toSpell()) - { - owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); - } - else //unknown random spell - { - owner.giveCommand(EActionType::MONSTER_SPELL, targetHex); - } - } - else - { - assert(getHeroSpellToCast()); - switch (getHeroSpellToCast()->id.toEnum()) - { - case SpellID::SACRIFICE: - heroSpellToCast->aimToUnit(targetStack);//victim - break; - default: - heroSpellToCast->aimToHex(targetHex); - break; - } - owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); - endCastingSpell(); - } - selectedStack = nullptr; - return; - } - } - assert(0); - return; -} - -PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex) -{ - assert(owner.stacksController->getActiveStack() != nullptr); - assert(!possibleActions.empty()); - assert(targetHex.isValid()); - - if (owner.stacksController->getActiveStack() == nullptr) - return PossiblePlayerBattleAction::INVALID; - - if (possibleActions.empty()) - return PossiblePlayerBattleAction::INVALID; - - const CStack * targetStack = getStackForHex(targetHex); - - reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack); - - for (PossiblePlayerBattleAction action : possibleActions) - { - if (actionIsLegal(action, targetHex)) - return action; - } - return possibleActions.front(); -} - -void BattleActionsController::onHexHovered(BattleHex hoveredHex) -{ - if (owner.openingPlaying()) - { - currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro"); - GH.statusbar()->write(currentConsoleMsg); - return; - } - - if (owner.stacksController->getActiveStack() == nullptr) - return; - - if (hoveredHex == BattleHex::INVALID) - { - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - currentConsoleMsg.clear(); - CCS->curh->set(Cursor::Combat::BLOCKED); - return; - } - - auto action = selectAction(hoveredHex); - - std::string newConsoleMsg; - - if (actionIsLegal(action, hoveredHex)) - { - actionSetCursor(action, hoveredHex); - newConsoleMsg = actionGetStatusMessage(action, hoveredHex); - } - else - { - actionSetCursorBlocked(action, hoveredHex); - newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex); - } - - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - if (!newConsoleMsg.empty()) - GH.statusbar()->write(newConsoleMsg); - - currentConsoleMsg = newConsoleMsg; -} - -void BattleActionsController::onHoverEnded() -{ - CCS->curh->set(Cursor::Combat::POINTER); - - if (!currentConsoleMsg.empty()) - GH.statusbar()->clearIfMatching(currentConsoleMsg); - - currentConsoleMsg.clear(); -} - -void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) -{ - if (owner.stacksController->getActiveStack() == nullptr) - return; - - auto action = selectAction(clickedHex); - - std::string newConsoleMsg; - - if (!actionIsLegal(action, clickedHex)) - return; - - actionRealize(action, clickedHex); - GH.statusbar()->clear(); -} - -void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) -{ - creatureSpells.clear(); - - bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); - if(casterStack->canCast() && spellcaster) - { - // faerie dragon can cast only one, randomly selected spell until their next move - //TODO: faerie dragon type spell should be selected by server - const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); - - if (spellToCast) - creatureSpells.push_back(spellToCast); - } - - TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); - - for(const auto & bonus : *bl) - { - if (bonus->additionalInfo[0] <= 0) - creatureSpells.push_back(SpellID(bonus->subtype).toSpell()); - } -} - -const spells::Caster * BattleActionsController::getCurrentSpellcaster() const -{ - if (heroSpellToCast) - return owner.getActiveHero(); - else - return owner.stacksController->getActiveStack(); -} - -spells::Mode BattleActionsController::getCurrentCastMode() const -{ - if (heroSpellToCast) - return spells::Mode::HERO; - else - return spells::Mode::CREATURE_ACTIVE; - -} - -bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex) -{ - assert(currentSpell); - if (!currentSpell) - return false; - - auto caster = getCurrentSpellcaster(); - - const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE; - - spells::Target target; - if(targetStack) - target.emplace_back(targetStack); - target.emplace_back(targetHex); - - spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell); - - auto m = currentSpell->battleMechanics(&cast); - spells::detail::ProblemImpl problem; //todo: display problem in status bar - - return m->canBeCastAt(target, problem); -} - -bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const -{ - std::vector acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false); - BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); - - if (vstd::contains(acc, myNumber)) - return true; - else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest)) - return true; - else - return false; -} - -void BattleActionsController::activateStack() -{ - const CStack * s = owner.stacksController->getActiveStack(); - if(s) - { - tryActivateStackSpellcasting(s); - - possibleActions = getPossibleActionsForStack(s); - std::list actionsToSelect; - if(!possibleActions.empty()) - { - switch(possibleActions.front().get()) - { - case PossiblePlayerBattleAction::SHOOT: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); - break; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - case PossiblePlayerBattleAction::ANY_LOCATION: - actionsToSelect.push_back(possibleActions.front()); - actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); - break; - } - } - owner.windowObject->setAlternativeActions(actionsToSelect); - } -} - -void BattleActionsController::onHexRightClicked(BattleHex clickedHex) -{ - auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) - { - return action.spellcast(); - }; - - bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); - - if (spellcastingModeActive() || isCurrentStackInSpellcastMode) - { - endCastingSpell(); - CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled - return; - } - - auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true); - - if (selectedStack != nullptr) - GH.windows().createAndPushWindow(selectedStack, true); - - if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero) - owner.attackingHero->heroRightClicked(); - - if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero) - owner.defendingHero->heroRightClicked(); -} - -bool BattleActionsController::spellcastingModeActive() const -{ - return heroSpellToCast != nullptr; -} - -bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) -{ - if (heroSpellToCast) - return true; - - if (!owner.stacksController->getActiveStack()) - return false; - - auto action = selectAction(hoveredHex); - - return action.spellcast(); -} - -const std::vector & BattleActionsController::getPossibleActions() const -{ - return possibleActions; -} - -void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action) -{ - vstd::erase(possibleActions, action); -} - -void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action) -{ - possibleActions.insert(possibleActions.begin(), action); -} +/* + * BattleActionsController.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 "BattleActionsController.h" + +#include "BattleWindow.h" +#include "BattleStacksController.h" +#include "BattleInterface.h" +#include "BattleFieldController.h" +#include "BattleSiegeController.h" +#include "BattleInterfaceClasses.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CIntObject.h" +#include "../gui/WindowHandler.h" +#include "../windows/CCreatureWindow.h" +#include "../windows/InfoWindows.h" + +#include "../../CCallback.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/spells/Problem.h" + +struct TextReplacement +{ + std::string placeholder; + std::string replacement; +}; + +using TextReplacementList = std::vector; + +static std::string replacePlaceholders(std::string input, const TextReplacementList & format ) +{ + for(const auto & entry : format) + boost::replace_all(input, entry.placeholder, entry.replacement); + + return input; +} + +static std::string translatePlural(int amount, const std::string& baseTextID) +{ + if(amount == 1) + return CGI->generaltexth->translate(baseTextID + ".1"); + return CGI->generaltexth->translate(baseTextID); +} + +static std::string formatPluralImpl(int amount, const std::string & amountString, const std::string & baseTextID) +{ + std::string baseString = translatePlural(amount, baseTextID); + TextReplacementList replacements { + { "%d", amountString } + }; + + return replacePlaceholders(baseString, replacements); +} + +static std::string formatPlural(int amount, const std::string & baseTextID) +{ + return formatPluralImpl(amount, std::to_string(amount), baseTextID); +} + +static std::string formatPlural(DamageRange range, const std::string & baseTextID) +{ + if (range.min == range.max) + return formatPlural(range.min, baseTextID); + + std::string rangeString = std::to_string(range.min) + " - " + std::to_string(range.max); + + return formatPluralImpl(range.max, rangeString, baseTextID); +} + +static std::string formatAttack(const DamageEstimation & estimation, const std::string & creatureName, const std::string & baseTextID, int shotsLeft) +{ + TextReplacementList replacements = { + { "%CREATURE", creatureName }, + { "%DAMAGE", formatPlural(estimation.damage, "vcmi.battleWindow.damageEstimation.damage") }, + { "%SHOTS", formatPlural(shotsLeft, "vcmi.battleWindow.damageEstimation.shots") }, + { "%KILLS", formatPlural(estimation.kills, "vcmi.battleWindow.damageEstimation.kills") }, + }; + + return replacePlaceholders(CGI->generaltexth->translate(baseTextID), replacements); +} + +static std::string formatMeleeAttack(const DamageEstimation & estimation, const std::string & creatureName) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.melee" : + "vcmi.battleWindow.damageEstimation.meleeKills"; + + return formatAttack(estimation, creatureName, baseTextID, 0); +} + +static std::string formatRangedAttack(const DamageEstimation & estimation, const std::string & creatureName, int shotsLeft) +{ + std::string baseTextID = estimation.kills.max == 0 ? + "vcmi.battleWindow.damageEstimation.ranged" : + "vcmi.battleWindow.damageEstimation.rangedKills"; + + return formatAttack(estimation, creatureName, baseTextID, shotsLeft); +} + +BattleActionsController::BattleActionsController(BattleInterface & owner): + owner(owner), + selectedStack(nullptr), + heroSpellToCast(nullptr) +{ +} + +void BattleActionsController::endCastingSpell() +{ + if(heroSpellToCast) + { + heroSpellToCast.reset(); + owner.windowObject->blockUI(false); + } + + if(owner.stacksController->getActiveStack()) + possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared + + selectedStack = nullptr; + GH.fakeMouseMove(); +} + +bool BattleActionsController::isActiveStackSpellcaster() const +{ + const CStack * casterStack = owner.stacksController->getActiveStack(); + if (!casterStack) + return false; + + bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); + return (spellcaster && casterStack->canCast()); +} + +void BattleActionsController::enterCreatureCastingMode() +{ + //silently check for possible errors + if (owner.tacticsMode) + return; + + //hero is casting a spell + if (heroSpellToCast) + return; + + if (!owner.stacksController->getActiveStack()) + return; + + if (!isActiveStackSpellcaster()) + return; + + for(const auto & action : possibleActions) + { + if (action.get() != PossiblePlayerBattleAction::NO_LOCATION) + continue; + + const spells::Caster * caster = owner.stacksController->getActiveStack(); + const CSpell * spell = action.spell().toSpell(); + + spells::Target target; + target.emplace_back(); + + spells::BattleCast cast(owner.getBattle().get(), caster, spells::Mode::CREATURE_ACTIVE, spell); + + auto m = spell->battleMechanics(&cast); + spells::detail::ProblemImpl ignored; + + const bool isCastingPossible = m->canBeCastAt(target, ignored); + + if (isCastingPossible) + { + owner.giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, spell->getId()); + selectedStack = nullptr; + + CCS->curh->set(Cursor::Combat::POINTER); + } + return; + } + + possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); + + auto actionFilterPredicate = [](const PossiblePlayerBattleAction x) + { + return !x.spellcast(); + }; + + vstd::erase_if(possibleActions, actionFilterPredicate); + GH.fakeMouseMove(); +} + +std::vector BattleActionsController::getPossibleActionsForStack(const CStack *stack) const +{ + BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass + + for(const auto & spell : creatureSpells) + data.creatureSpellsToCast.push_back(spell->id); + + data.tacticsMode = owner.tacticsMode; + auto allActions = owner.getBattle()->getClientActionsForStack(stack, data); + + allActions.push_back(PossiblePlayerBattleAction::HERO_INFO); + allActions.push_back(PossiblePlayerBattleAction::CREATURE_INFO); + + return std::vector(allActions); +} + +void BattleActionsController::reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack) +{ + if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack + + auto assignPriority = [&](const PossiblePlayerBattleAction & item + ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it + { + switch(item.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::NO_LOCATION: + case PossiblePlayerBattleAction::FREE_LOCATION: + case PossiblePlayerBattleAction::OBSTACLE: + if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && targetStack != nullptr) + { + PlayerColor stackOwner = owner.getBattle()->battleGetOwner(targetStack); + bool enemyTargetingPositiveSpellcast = item.spell().toSpell()->isPositive() && stackOwner != LOCPLINT->playerID; + bool friendTargetingNegativeSpellcast = item.spell().toSpell()->isNegative() && stackOwner == LOCPLINT->playerID; + + if(!enemyTargetingPositiveSpellcast && !friendTargetingNegativeSpellcast) + return 1; + } + return 100; //bottom priority + + break; + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + return 2; + break; + case PossiblePlayerBattleAction::SHOOT: + return 4; + break; + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + return 5; + break; + case PossiblePlayerBattleAction::ATTACK: + return 6; + break; + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + return 7; + break; + case PossiblePlayerBattleAction::MOVE_STACK: + return 8; + break; + case PossiblePlayerBattleAction::CATAPULT: + return 9; + break; + case PossiblePlayerBattleAction::HEAL: + return 10; + break; + case PossiblePlayerBattleAction::CREATURE_INFO: + return 11; + break; + case PossiblePlayerBattleAction::HERO_INFO: + return 12; + break; + case PossiblePlayerBattleAction::TELEPORT: + return 13; + break; + default: + assert(0); + return 200; + break; + } + }; + + auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs) + { + return assignPriority(lhs) < assignPriority(rhs); + }; + + std::sort(possibleActions.begin(), possibleActions.end(), comparer); +} + +void BattleActionsController::castThisSpell(SpellID spellID) +{ + heroSpellToCast = std::make_shared(); + heroSpellToCast->actionType = EActionType::HERO_SPELL; + heroSpellToCast->spell = spellID; + heroSpellToCast->stackNumber = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? -1 : -2; + heroSpellToCast->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false; + + //choosing possible targets + const CGHeroInstance *castingHero = (owner.attackingHeroInstance->tempOwner == owner.curInt->playerID) ? owner.attackingHeroInstance : owner.defendingHeroInstance; + assert(castingHero); // code below assumes non-null hero + PossiblePlayerBattleAction spellSelMode = owner.getBattle()->getCasterAction(spellID.toSpell(), castingHero, spells::Mode::HERO); + + if (spellSelMode.get() == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location + { + heroSpellToCast->aimToHex(BattleHex::INVALID); + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); + endCastingSpell(); + } + else + { + possibleActions.clear(); + possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment + GH.fakeMouseMove();//update cursor + } + + owner.windowObject->blockUI(true); +} + +const CSpell * BattleActionsController::getHeroSpellToCast( ) const +{ + if (heroSpellToCast) + return heroSpellToCast->spell.toSpell(); + return nullptr; +} + +const CSpell * BattleActionsController::getStackSpellToCast(BattleHex hoveredHex) +{ + if (heroSpellToCast) + return nullptr; + + if (!owner.stacksController->getActiveStack()) + return nullptr; + + if (!hoveredHex.isValid()) + return nullptr; + + auto action = selectAction(hoveredHex); + + if (action.spell() == SpellID::NONE) + return nullptr; + + return action.spell().toSpell(); +} + +const CSpell * BattleActionsController::getCurrentSpell(BattleHex hoveredHex) +{ + if (getHeroSpellToCast()) + return getHeroSpellToCast(); + return getStackSpellToCast(hoveredHex); +} + +const CStack * BattleActionsController::getStackForHex(BattleHex hoveredHex) +{ + const CStack * shere = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(shere) + return shere; + return owner.getBattle()->battleGetStackByPos(hoveredHex, false); +} + +void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + CCS->curh->set(Cursor::Combat::POINTER); + return; + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) + CCS->curh->set(Cursor::Combat::FLY); + else + CCS->curh->set(Cursor::Combat::MOVE); + return; + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + { + static const std::map sectorCursor = { + {BattleHex::TOP_LEFT, Cursor::Combat::HIT_SOUTHEAST}, + {BattleHex::TOP_RIGHT, Cursor::Combat::HIT_SOUTHWEST}, + {BattleHex::RIGHT, Cursor::Combat::HIT_WEST }, + {BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST}, + {BattleHex::BOTTOM_LEFT, Cursor::Combat::HIT_NORTHEAST}, + {BattleHex::LEFT, Cursor::Combat::HIT_EAST }, + {BattleHex::TOP, Cursor::Combat::HIT_SOUTH }, + {BattleHex::BOTTOM, Cursor::Combat::HIT_NORTH } + }; + + auto direction = owner.fieldController->selectAttackDirection(targetHex); + + assert(sectorCursor.count(direction) > 0); + if (sectorCursor.count(direction)) + CCS->curh->set(sectorCursor.at(direction)); + + return; + } + + case PossiblePlayerBattleAction::SHOOT: + if (owner.getBattle()->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex)) + CCS->curh->set(Cursor::Combat::SHOOT_PENALTY); + else + CCS->curh->set(Cursor::Combat::SHOOT); + return; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + case PossiblePlayerBattleAction::FREE_LOCATION: + case PossiblePlayerBattleAction::OBSTACLE: + CCS->curh->set(Cursor::Spellcast::SPELL); + return; + + case PossiblePlayerBattleAction::TELEPORT: + CCS->curh->set(Cursor::Combat::TELEPORT); + return; + + case PossiblePlayerBattleAction::SACRIFICE: + CCS->curh->set(Cursor::Combat::SACRIFICE); + return; + + case PossiblePlayerBattleAction::HEAL: + CCS->curh->set(Cursor::Combat::HEAL); + return; + + case PossiblePlayerBattleAction::CATAPULT: + CCS->curh->set(Cursor::Combat::SHOOT_CATAPULT); + return; + + case PossiblePlayerBattleAction::CREATURE_INFO: + CCS->curh->set(Cursor::Combat::QUERY); + return; + case PossiblePlayerBattleAction::HERO_INFO: + CCS->curh->set(Cursor::Combat::HERO); + return; + } + assert(0); +} + +void BattleActionsController::actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + case PossiblePlayerBattleAction::TELEPORT: + case PossiblePlayerBattleAction::SACRIFICE: + case PossiblePlayerBattleAction::FREE_LOCATION: + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + default: + if (targetHex == -1) + CCS->curh->set(Cursor::Combat::POINTER); + else + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + } + assert(0); +} + +std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + + switch (action.get()) //display console message, realize selected action + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + return (boost::format(CGI->generaltexth->allTexts[481]) % targetStack->getName()).str(); //Select %s + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING)) + return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here + else + return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return + { + BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(owner.stacksController->getActiveStack(), targetStack, attackFromHex); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatMeleeAttack(estimation, targetStack->getName()); + } + + case PossiblePlayerBattleAction::SHOOT: + { + const auto * shooter = owner.stacksController->getActiveStack(); + + DamageEstimation estimation = owner.getBattle()->battleEstimateDamage(shooter, targetStack, shooter->getPosition()); + estimation.kills.max = std::min(estimation.kills.max, targetStack->getCount()); + estimation.kills.min = std::min(estimation.kills.min, targetStack->getCount()); + + return formatRangedAttack(estimation, targetStack->getName(), shooter->shots.available()); + } + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + return boost::str(boost::format(CGI->generaltexth->allTexts[27]) % action.spell().toSpell()->getNameTranslated() % targetStack->getName()); //Cast %s on %s + + case PossiblePlayerBattleAction::ANY_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s + + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell + return boost::str(boost::format(CGI->generaltexth->allTexts[301]) % targetStack->getName()); //Cast a spell on % + + case PossiblePlayerBattleAction::TELEPORT: + return CGI->generaltexth->allTexts[25]; //Teleport Here + + case PossiblePlayerBattleAction::OBSTACLE: + return CGI->generaltexth->allTexts[550]; + + case PossiblePlayerBattleAction::SACRIFICE: + return (boost::format(CGI->generaltexth->allTexts[549]) % targetStack->getName()).str(); //sacrifice the %s + + case PossiblePlayerBattleAction::FREE_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[26]) % action.spell().toSpell()->getNameTranslated()); //Cast %s + + case PossiblePlayerBattleAction::HEAL: + return (boost::format(CGI->generaltexth->allTexts[419]) % targetStack->getName()).str(); //Apply first aid to the %s + + case PossiblePlayerBattleAction::CATAPULT: + return ""; // TODO + + case PossiblePlayerBattleAction::CREATURE_INFO: + return (boost::format(CGI->generaltexth->allTexts[297]) % targetStack->getName()).str(); + + case PossiblePlayerBattleAction::HERO_INFO: + return CGI->generaltexth->translate("core.genrltxt.417"); // "View Hero Stats" + } + assert(0); + return ""; +} + +std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + switch (action.get()) + { + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + return CGI->generaltexth->allTexts[23]; + break; + case PossiblePlayerBattleAction::TELEPORT: + return CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination + break; + case PossiblePlayerBattleAction::SACRIFICE: + return CGI->generaltexth->allTexts[543]; //choose army to sacrifice + break; + case PossiblePlayerBattleAction::FREE_LOCATION: + return boost::str(boost::format(CGI->generaltexth->allTexts[181]) % action.spell().toSpell()->getNameTranslated()); //No room to place %s here + break; + default: + return ""; + } +} + +bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID; + + switch (action.get()) + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + return (targetStack && targetStackOwned && targetStack->speed() > 0); + + case PossiblePlayerBattleAction::CREATURE_INFO: + return (targetStack && targetStackOwned && targetStack->alive()); + + case PossiblePlayerBattleAction::HERO_INFO: + if (targetHex == BattleHex::HERO_ATTACKER) + return owner.attackingHero != nullptr; + + if (targetHex == BattleHex::HERO_DEFENDER) + return owner.defendingHero != nullptr; + + return false; + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + if (!(targetStack && targetStack->alive())) //we can walk on dead stacks + { + if(canStackMoveHere(owner.stacksController->getActiveStack(), targetHex)) + return true; + } + return false; + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + if(owner.getBattle()->battleCanAttack(owner.stacksController->getActiveStack(), targetStack, targetHex)) + { + if (owner.fieldController->isTileAttackable(targetHex)) // move isTileAttackable to be part of battleCanAttack? + return true; + } + return false; + + case PossiblePlayerBattleAction::SHOOT: + return owner.getBattle()->battleCanShoot(owner.stacksController->getActiveStack(), targetHex); + + case PossiblePlayerBattleAction::NO_LOCATION: + return false; + + case PossiblePlayerBattleAction::ANY_LOCATION: + return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: + if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures + { + int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE); + return spellID > -1; + } + return false; + + case PossiblePlayerBattleAction::TELEPORT: + return selectedStack && isCastingPossibleHere(action.spell().toSpell(), selectedStack, targetHex); + + case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice + return targetStack && targetStack != selectedStack && targetStackOwned && targetStack->alive(); + + case PossiblePlayerBattleAction::OBSTACLE: + case PossiblePlayerBattleAction::FREE_LOCATION: + return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex); + + case PossiblePlayerBattleAction::CATAPULT: + return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex); + + case PossiblePlayerBattleAction::HEAL: + return targetStack && targetStackOwned && targetStack->canBeHealed(); + } + + assert(0); + return false; +} + +void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, BattleHex targetHex) +{ + const CStack * targetStack = getStackForHex(targetHex); + + switch (action.get()) //display console message, realize selected action + { + case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK: + { + owner.stackActivated(targetStack); + return; + } + + case PossiblePlayerBattleAction::MOVE_TACTICS: + case PossiblePlayerBattleAction::MOVE_STACK: + { + if(owner.stacksController->getActiveStack()->doubleWide()) + { + std::vector acc = owner.getBattle()->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false); + BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false); + if(vstd::contains(acc, targetHex)) + owner.giveCommand(EActionType::WALK, targetHex); + else if(vstd::contains(acc, shiftedDest)) + owner.giveCommand(EActionType::WALK, shiftedDest); + } + else + { + owner.giveCommand(EActionType::WALK, targetHex); + } + return; + } + + case PossiblePlayerBattleAction::ATTACK: + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return + { + bool returnAfterAttack = action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN; + BattleHex attackFromHex = owner.fieldController->fromWhichHexAttack(targetHex); + if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308) + { + BattleAction command = BattleAction::makeMeleeAttack(owner.stacksController->getActiveStack(), targetHex, attackFromHex, returnAfterAttack); + owner.sendCommand(command, owner.stacksController->getActiveStack()); + } + return; + } + + case PossiblePlayerBattleAction::SHOOT: + { + owner.giveCommand(EActionType::SHOOT, targetHex); + return; + } + + case PossiblePlayerBattleAction::HEAL: + { + owner.giveCommand(EActionType::STACK_HEAL, targetHex); + return; + }; + + case PossiblePlayerBattleAction::CATAPULT: + { + owner.giveCommand(EActionType::CATAPULT, targetHex); + return; + } + + case PossiblePlayerBattleAction::CREATURE_INFO: + { + GH.windows().createAndPushWindow(targetStack, false); + return; + } + + case PossiblePlayerBattleAction::HERO_INFO: + { + if (targetHex == BattleHex::HERO_ATTACKER) + owner.attackingHero->heroLeftClicked(); + + if (targetHex == BattleHex::HERO_DEFENDER) + owner.defendingHero->heroLeftClicked(); + + return; + } + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + case PossiblePlayerBattleAction::ANY_LOCATION: + case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell + case PossiblePlayerBattleAction::TELEPORT: + case PossiblePlayerBattleAction::OBSTACLE: + case PossiblePlayerBattleAction::SACRIFICE: + case PossiblePlayerBattleAction::FREE_LOCATION: + { + if (action.get() == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE ) + { + if (action.spell() == SpellID::SACRIFICE) + { + heroSpellToCast->aimToHex(targetHex); + possibleActions.push_back({PossiblePlayerBattleAction::SACRIFICE, action.spell()}); + selectedStack = targetStack; + return; + } + if (action.spell() == SpellID::TELEPORT) + { + heroSpellToCast->aimToUnit(targetStack); + possibleActions.push_back({PossiblePlayerBattleAction::TELEPORT, action.spell()}); + selectedStack = targetStack; + return; + } + } + + if (!spellcastingModeActive()) + { + if (action.spell().toSpell()) + { + owner.giveCommand(EActionType::MONSTER_SPELL, targetHex, action.spell()); + } + else //unknown random spell + { + owner.giveCommand(EActionType::MONSTER_SPELL, targetHex); + } + } + else + { + assert(getHeroSpellToCast()); + switch (getHeroSpellToCast()->id.toEnum()) + { + case SpellID::SACRIFICE: + heroSpellToCast->aimToUnit(targetStack);//victim + break; + default: + heroSpellToCast->aimToHex(targetHex); + break; + } + owner.curInt->cb->battleMakeSpellAction(owner.getBattleID(), *heroSpellToCast); + endCastingSpell(); + } + selectedStack = nullptr; + return; + } + } + assert(0); + return; +} + +PossiblePlayerBattleAction BattleActionsController::selectAction(BattleHex targetHex) +{ + assert(owner.stacksController->getActiveStack() != nullptr); + assert(!possibleActions.empty()); + assert(targetHex.isValid()); + + if (owner.stacksController->getActiveStack() == nullptr) + return PossiblePlayerBattleAction::INVALID; + + if (possibleActions.empty()) + return PossiblePlayerBattleAction::INVALID; + + const CStack * targetStack = getStackForHex(targetHex); + + reorderPossibleActionsPriority(owner.stacksController->getActiveStack(), targetStack); + + for (PossiblePlayerBattleAction action : possibleActions) + { + if (actionIsLegal(action, targetHex)) + return action; + } + return possibleActions.front(); +} + +void BattleActionsController::onHexHovered(BattleHex hoveredHex) +{ + if (owner.openingPlaying()) + { + currentConsoleMsg = VLC->generaltexth->translate("vcmi.battleWindow.pressKeyToSkipIntro"); + GH.statusbar()->write(currentConsoleMsg); + return; + } + + if (owner.stacksController->getActiveStack() == nullptr) + return; + + if (hoveredHex == BattleHex::INVALID) + { + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + currentConsoleMsg.clear(); + CCS->curh->set(Cursor::Combat::BLOCKED); + return; + } + + auto action = selectAction(hoveredHex); + + std::string newConsoleMsg; + + if (actionIsLegal(action, hoveredHex)) + { + actionSetCursor(action, hoveredHex); + newConsoleMsg = actionGetStatusMessage(action, hoveredHex); + } + else + { + actionSetCursorBlocked(action, hoveredHex); + newConsoleMsg = actionGetStatusMessageBlocked(action, hoveredHex); + } + + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + if (!newConsoleMsg.empty()) + GH.statusbar()->write(newConsoleMsg); + + currentConsoleMsg = newConsoleMsg; +} + +void BattleActionsController::onHoverEnded() +{ + CCS->curh->set(Cursor::Combat::POINTER); + + if (!currentConsoleMsg.empty()) + GH.statusbar()->clearIfMatching(currentConsoleMsg); + + currentConsoleMsg.clear(); +} + +void BattleActionsController::onHexLeftClicked(BattleHex clickedHex) +{ + if (owner.stacksController->getActiveStack() == nullptr) + return; + + auto action = selectAction(clickedHex); + + std::string newConsoleMsg; + + if (!actionIsLegal(action, clickedHex)) + return; + + actionRealize(action, clickedHex); + GH.statusbar()->clear(); +} + +void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack) +{ + creatureSpells.clear(); + + bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER); + if(casterStack->canCast() && spellcaster) + { + // faerie dragon can cast only one, randomly selected spell until their next move + //TODO: faerie dragon type spell should be selected by server + const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell(); + + if (spellToCast) + creatureSpells.push_back(spellToCast); + } + + TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER)); + + for(const auto & bonus : *bl) + { + if (bonus->additionalInfo[0] <= 0) + creatureSpells.push_back(SpellID(bonus->subtype).toSpell()); + } +} + +const spells::Caster * BattleActionsController::getCurrentSpellcaster() const +{ + if (heroSpellToCast) + return owner.getActiveHero(); + else + return owner.stacksController->getActiveStack(); +} + +spells::Mode BattleActionsController::getCurrentCastMode() const +{ + if (heroSpellToCast) + return spells::Mode::HERO; + else + return spells::Mode::CREATURE_ACTIVE; + +} + +bool BattleActionsController::isCastingPossibleHere(const CSpell * currentSpell, const CStack *targetStack, BattleHex targetHex) +{ + assert(currentSpell); + if (!currentSpell) + return false; + + auto caster = getCurrentSpellcaster(); + + const spells::Mode mode = heroSpellToCast ? spells::Mode::HERO : spells::Mode::CREATURE_ACTIVE; + + spells::Target target; + if(targetStack) + target.emplace_back(targetStack); + target.emplace_back(targetHex); + + spells::BattleCast cast(owner.getBattle().get(), caster, mode, currentSpell); + + auto m = currentSpell->battleMechanics(&cast); + spells::detail::ProblemImpl problem; //todo: display problem in status bar + + return m->canBeCastAt(target, problem); +} + +bool BattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber) const +{ + std::vector acc = owner.getBattle()->battleGetAvailableHexes(stackToMove, false); + BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false); + + if (vstd::contains(acc, myNumber)) + return true; + else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest)) + return true; + else + return false; +} + +void BattleActionsController::activateStack() +{ + const CStack * s = owner.stacksController->getActiveStack(); + if(s) + { + tryActivateStackSpellcasting(s); + + possibleActions = getPossibleActionsForStack(s); + std::list actionsToSelect; + if(!possibleActions.empty()) + { + switch(possibleActions.front().get()) + { + case PossiblePlayerBattleAction::SHOOT: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); + break; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + case PossiblePlayerBattleAction::ANY_LOCATION: + actionsToSelect.push_back(possibleActions.front()); + actionsToSelect.push_back(PossiblePlayerBattleAction::ATTACK); + break; + } + } + owner.windowObject->setAlternativeActions(actionsToSelect); + } +} + +void BattleActionsController::onHexRightClicked(BattleHex clickedHex) +{ + auto spellcastActionPredicate = [](PossiblePlayerBattleAction & action) + { + return action.spellcast(); + }; + + bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); + + if (spellcastingModeActive() || isCurrentStackInSpellcastMode) + { + endCastingSpell(); + CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled + return; + } + + auto selectedStack = owner.getBattle()->battleGetStackByPos(clickedHex, true); + + if (selectedStack != nullptr) + GH.windows().createAndPushWindow(selectedStack, true); + + if (clickedHex == BattleHex::HERO_ATTACKER && owner.attackingHero) + owner.attackingHero->heroRightClicked(); + + if (clickedHex == BattleHex::HERO_DEFENDER && owner.defendingHero) + owner.defendingHero->heroRightClicked(); +} + +bool BattleActionsController::spellcastingModeActive() const +{ + return heroSpellToCast != nullptr; +} + +bool BattleActionsController::currentActionSpellcasting(BattleHex hoveredHex) +{ + if (heroSpellToCast) + return true; + + if (!owner.stacksController->getActiveStack()) + return false; + + auto action = selectAction(hoveredHex); + + return action.spellcast(); +} + +const std::vector & BattleActionsController::getPossibleActions() const +{ + return possibleActions; +} + +void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action) +{ + vstd::erase(possibleActions, action); +} + +void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action) +{ + possibleActions.insert(possibleActions.begin(), action); +} diff --git a/client/battle/BattleActionsController.h b/client/battle/BattleActionsController.h index 86acc8e44..3c9b35660 100644 --- a/client/battle/BattleActionsController.h +++ b/client/battle/BattleActionsController.h @@ -1,125 +1,125 @@ -/* - * BattleActionsController.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/battle/CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BattleAction; -namespace spells { -class Caster; -enum class Mode; -} - -VCMI_LIB_NAMESPACE_END - -class BattleInterface; - -/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc -/// As well as all relevant feedback for these actions in user interface -class BattleActionsController -{ - BattleInterface & owner; - - /// all actions possible to call at the moment by player - std::vector possibleActions; - - /// spell for which player's hero is choosing destination - std::shared_ptr heroSpellToCast; - - /// cached message that was set by this class in status bar - std::string currentConsoleMsg; - - /// if true, active stack could possibly cast some target spell - std::vector creatureSpells; - - /// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice) - const CStack * selectedStack; - - bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber); - bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback - std::vector getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn - void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack); - - bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex); - void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex); - std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex); - - PossiblePlayerBattleAction selectAction(BattleHex myNumber); - - const CStack * getStackForHex(BattleHex myNumber) ; - - /// attempts to initialize spellcasting action for stack - /// will silently return if stack is not a spellcaster - void tryActivateStackSpellcasting(const CStack *casterStack); - - /// returns spell that is currently being cast by hero or nullptr if none - const CSpell * getHeroSpellToCast() const; - - /// if current stack is spellcaster, returns spell being cast, or null othervice - const CSpell * getStackSpellToCast(BattleHex hoveredHex); - - /// returns true if current stack is a spellcaster - bool isActiveStackSpellcaster() const; - -public: - BattleActionsController(BattleInterface & owner); - - /// initialize list of potential actions for new active stack - void activateStack(); - - /// returns true if UI is currently in target selection mode - bool spellcastingModeActive() const; - - /// returns true if one of the following is true: - /// - we are casting spell by hero - /// - we are casting spell by creature in targeted mode (F hotkey) - /// - current creature is spellcaster and preferred action for current hex is spellcast - bool currentActionSpellcasting(BattleHex hoveredHex); - - /// enter targeted spellcasting mode for creature, e.g. via "F" hotkey - void enterCreatureCastingMode(); - - /// initialize hero spellcasting mode, e.g. on selecting spell in spellbook - void castThisSpell(SpellID spellID); - - /// ends casting spell (eg. when spell has been cast or canceled) - void endCastingSpell(); - - /// update cursor and status bar according to new active hex - void onHexHovered(BattleHex hoveredHex); - - /// called when cursor is no longer over battlefield and cursor/battle log should be reset - void onHoverEnded(); - - /// performs action according to selected hex - void onHexLeftClicked(BattleHex clickedHex); - - /// performs action according to selected hex - void onHexRightClicked(BattleHex clickedHex); - - const spells::Caster * getCurrentSpellcaster() const; - const CSpell * getCurrentSpell(BattleHex hoveredHex); - spells::Mode getCurrentCastMode() const; - - /// methods to work with array of possible actions, needed to control special creatures abilities - const std::vector & getPossibleActions() const; - void removePossibleAction(PossiblePlayerBattleAction); - - /// inserts possible action in the beggining in order to prioritize it - void pushFrontPossibleAction(PossiblePlayerBattleAction); -}; +/* + * BattleActionsController.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/battle/CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleAction; +namespace spells { +class Caster; +enum class Mode; +} + +VCMI_LIB_NAMESPACE_END + +class BattleInterface; + +/// Class that controls actions that can be performed by player, e.g. moving stacks, attacking, etc +/// As well as all relevant feedback for these actions in user interface +class BattleActionsController +{ + BattleInterface & owner; + + /// all actions possible to call at the moment by player + std::vector possibleActions; + + /// spell for which player's hero is choosing destination + std::shared_ptr heroSpellToCast; + + /// cached message that was set by this class in status bar + std::string currentConsoleMsg; + + /// if true, active stack could possibly cast some target spell + std::vector creatureSpells; + + /// stack that has been selected as first target for multi-target spells (Teleport & Sacrifice) + const CStack * selectedStack; + + bool isCastingPossibleHere (const CSpell * spell, const CStack *shere, BattleHex myNumber); + bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber) const; //TODO: move to BattleState / callback + std::vector getPossibleActionsForStack (const CStack *stack) const; //called when stack gets its turn + void reorderPossibleActionsPriority(const CStack * stack, const CStack * targetStack); + + bool actionIsLegal(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + void actionSetCursor(PossiblePlayerBattleAction action, BattleHex hoveredHex); + void actionSetCursorBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + std::string actionGetStatusMessage(PossiblePlayerBattleAction action, BattleHex hoveredHex); + std::string actionGetStatusMessageBlocked(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + void actionRealize(PossiblePlayerBattleAction action, BattleHex hoveredHex); + + PossiblePlayerBattleAction selectAction(BattleHex myNumber); + + const CStack * getStackForHex(BattleHex myNumber) ; + + /// attempts to initialize spellcasting action for stack + /// will silently return if stack is not a spellcaster + void tryActivateStackSpellcasting(const CStack *casterStack); + + /// returns spell that is currently being cast by hero or nullptr if none + const CSpell * getHeroSpellToCast() const; + + /// if current stack is spellcaster, returns spell being cast, or null othervice + const CSpell * getStackSpellToCast(BattleHex hoveredHex); + + /// returns true if current stack is a spellcaster + bool isActiveStackSpellcaster() const; + +public: + BattleActionsController(BattleInterface & owner); + + /// initialize list of potential actions for new active stack + void activateStack(); + + /// returns true if UI is currently in target selection mode + bool spellcastingModeActive() const; + + /// returns true if one of the following is true: + /// - we are casting spell by hero + /// - we are casting spell by creature in targeted mode (F hotkey) + /// - current creature is spellcaster and preferred action for current hex is spellcast + bool currentActionSpellcasting(BattleHex hoveredHex); + + /// enter targeted spellcasting mode for creature, e.g. via "F" hotkey + void enterCreatureCastingMode(); + + /// initialize hero spellcasting mode, e.g. on selecting spell in spellbook + void castThisSpell(SpellID spellID); + + /// ends casting spell (eg. when spell has been cast or canceled) + void endCastingSpell(); + + /// update cursor and status bar according to new active hex + void onHexHovered(BattleHex hoveredHex); + + /// called when cursor is no longer over battlefield and cursor/battle log should be reset + void onHoverEnded(); + + /// performs action according to selected hex + void onHexLeftClicked(BattleHex clickedHex); + + /// performs action according to selected hex + void onHexRightClicked(BattleHex clickedHex); + + const spells::Caster * getCurrentSpellcaster() const; + const CSpell * getCurrentSpell(BattleHex hoveredHex); + spells::Mode getCurrentCastMode() const; + + /// methods to work with array of possible actions, needed to control special creatures abilities + const std::vector & getPossibleActions() const; + void removePossibleAction(PossiblePlayerBattleAction); + + /// inserts possible action in the beggining in order to prioritize it + void pushFrontPossibleAction(PossiblePlayerBattleAction); +}; diff --git a/client/battle/BattleAnimationClasses.cpp b/client/battle/BattleAnimationClasses.cpp index 5ce919da6..f56298930 100644 --- a/client/battle/BattleAnimationClasses.cpp +++ b/client/battle/BattleAnimationClasses.cpp @@ -1,1137 +1,1137 @@ -/* - * BattleAnimationClasses.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 "BattleAnimationClasses.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleProjectileController.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleEffectsController.h" -#include "BattleStacksController.h" -#include "CreatureAnimation.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" - -BattleAnimation::BattleAnimation(BattleInterface & owner) - : owner(owner), - ID(owner.stacksController->animIDhelper++), - initialized(false) -{ - logAnim->trace("Animation #%d created", ID); -} - -bool BattleAnimation::tryInitialize() -{ - assert(!initialized); - - if ( init() ) - { - initialized = true; - return true; - } - return false; -} - -bool BattleAnimation::isInitialized() -{ - return initialized; -} - -BattleAnimation::~BattleAnimation() -{ - logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); - for(auto & elem : pendingAnimations()) - { - if(elem == this) - elem = nullptr; - } - logAnim->trace("Animation #%d deleted", ID); -} - -std::vector & BattleAnimation::pendingAnimations() -{ - return owner.stacksController->currentAnimations; -} - -std::shared_ptr BattleAnimation::stackAnimation(const CStack * stack) const -{ - return owner.stacksController->stackAnimation[stack->unitId()]; -} - -bool BattleAnimation::stackFacingRight(const CStack * stack) -{ - return owner.stacksController->stackFacingRight[stack->unitId()]; -} - -void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) -{ - owner.stacksController->stackFacingRight[stack->unitId()] = facingRight; -} - -BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack) - : BattleAnimation(owner), - myAnim(stackAnimation(stack)), - stack(stack) -{ - assert(myAnim); -} - -StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack) - : BattleStackAnimation(owner, stack) - , nextGroup(ECreatureAnimType::HOLDING) - , currGroup(ECreatureAnimType::HOLDING) -{ -} - -ECreatureAnimType StackActionAnimation::getGroup() const -{ - return currGroup; -} - -void StackActionAnimation::setNextGroup( ECreatureAnimType group ) -{ - nextGroup = group; -} - -void StackActionAnimation::setGroup( ECreatureAnimType group ) -{ - currGroup = group; -} - -void StackActionAnimation::setSound( const AudioPath & sound ) -{ - this->sound = sound; -} - -bool StackActionAnimation::init() -{ - if (!sound.empty()) - CCS->soundh->playSound(sound); - - if (myAnim->framesInGroup(currGroup) > 0) - { - myAnim->playOnce(currGroup); - myAnim->onAnimationReset += [&](){ delete this; }; - } - else - delete this; - - return true; -} - -StackActionAnimation::~StackActionAnimation() -{ - if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED) - myAnim->setType(ECreatureAnimType::HOLDING); - else - myAnim->setType(nextGroup); - -} - -ECreatureAnimType AttackAnimation::findValidGroup( const std::vector candidates ) const -{ - for ( auto group : candidates) - { - if(myAnim->framesInGroup(group) > 0) - return group; - } - - assert(0); - return ECreatureAnimType::HOLDING; -} - -const CCreature * AttackAnimation::getCreature() const -{ - if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS) - return owner.siegeController->getTurretCreature(); - else - return attackingStack->unitType(); -} - - -AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) - : StackActionAnimation(owner, attacker), - dest(_dest), - defendingStack(defender), - attackingStack(attacker) -{ - assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); - attackingStackPosBeforeReturn = attackingStack->getPosition(); -} - -HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) - : StackActionAnimation(owner, stack) -{ - setGroup(ECreatureAnimType::HITTED); - setSound(stack->unitType()->sounds.wince); - logAnim->debug("Created HittedAnimation for %s", stack->getName()); -} - -DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack) - : StackActionAnimation(owner, stack) -{ - setGroup(ECreatureAnimType::DEFENCE); - setSound(stack->unitType()->sounds.defend); - logAnim->debug("Created DefenceAnimation for %s", stack->getName()); -} - -DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): - StackActionAnimation(owner, stack) -{ - setSound(stack->unitType()->sounds.killed); - - if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) - setGroup(ECreatureAnimType::DEATH_RANGED); - else - setGroup(ECreatureAnimType::DEATH); - - if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0) - setNextGroup(ECreatureAnimType::DEAD_RANGED); - else - setNextGroup(ECreatureAnimType::DEAD); - - logAnim->debug("Created DeathAnimation for %s", stack->getName()); -} - -DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames) - : BattleAnimation(owner), - counter(0), - howMany(howManyFrames) -{ - logAnim->debug("Created dummy animation for %d frames", howManyFrames); -} - -bool DummyAnimation::init() -{ - return true; -} - -void DummyAnimation::tick(uint32_t msPassed) -{ - counter++; - if(counter > howMany) - delete this; -} - -ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_UP; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_UP, - ECreatureAnimType::SPECIAL_UP, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::ATTACK_UP - }); -} - -ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_FRONT; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_FRONT, - ECreatureAnimType::SPECIAL_FRONT, - ECreatureAnimType::ATTACK_FRONT - }); -} - -ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const -{ - if (!multiAttack) - return ECreatureAnimType::ATTACK_DOWN; - - return findValidGroup({ - ECreatureAnimType::GROUP_ATTACK_DOWN, - ECreatureAnimType::SPECIAL_DOWN, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::ATTACK_DOWN - }); -} - -ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack) -{ - const ECreatureAnimType mutPosToGroup[] = - { - getUpwardsGroup (multiAttack), - getUpwardsGroup (multiAttack), - getForwardGroup (multiAttack), - getDownwardsGroup(multiAttack), - getDownwardsGroup(multiAttack), - getForwardGroup (multiAttack) - }; - - int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1); - - int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); - if(mutPos == -1 && attackingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition()); - } - if (mutPos == -1 && defendingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex()); - } - if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide()) - { - mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex()); - } - - assert(mutPos >= 0 && mutPos <=5); - - return mutPosToGroup[mutPos]; -} - -void MeleeAttackAnimation::tick(uint32_t msPassed) -{ - size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame(); - size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - - if ( currentFrame * 2 >= totalFrames ) - owner.executeAnimationStage(EAnimationEvents::HIT); - - AttackAnimation::tick(msPassed); -} - -MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack) - : AttackAnimation(owner, attacker, _dest, _attacked) -{ - logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); - setSound(getCreature()->sounds.attack); - setGroup(selectGroup(multiAttack)); -} - -StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex): - BattleStackAnimation(owner, _stack), - prevHex(prevHex), - nextHex(nextHex) -{ -} - -bool MovementAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0); - - if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0) - { - //no movement, end immediately - delete this; - return false; - } - - logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex); - - //reverse unit if necessary - if(owner.stacksController->shouldRotate(stack, prevHex, nextHex)) - { - // it seems that H3 does NOT plays full rotation animation during movement - // Logical since it takes quite a lot of time - rotateStack(prevHex); - } - - if(myAnim->getType() != ECreatureAnimType::MOVING) - { - myAnim->setType(ECreatureAnimType::MOVING); - } - - if (moveSoundHander == -1) - { - moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); - } - - Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); - Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); - - progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); - - begX = begPosition.x; - begY = begPosition.y; - //progress = 0; - distanceX = endPosition.x - begPosition.x; - distanceY = endPosition.y - begPosition.y; - - if (stack->hasBonus(Selector::type()(BonusType::FLYING))) - { - float distance = static_cast(sqrt(distanceX * distanceX + distanceY * distanceY)); - progressPerSecond = AnimationControls::getFlightDistance(stack->unitType()) / distance; - } - - return true; -} - -void MovementAnimation::tick(uint32_t msPassed) -{ - progress += float(msPassed) / 1000 * progressPerSecond; - - //moving instructions - myAnim->pos.x = begX + distanceX * progress; - myAnim->pos.y = begY + distanceY * progress; - - BattleAnimation::tick(msPassed); - - if(progress >= 1.0) - { - progress -= 1.0; - // Sets the position of the creature animation sprites - Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack); - myAnim->pos.moveTo(coords); - - // true if creature haven't reached the final destination hex - if ((curentMoveIndex + 1) < destTiles.size()) - { - // update the next hex field which has to be reached by the stack - curentMoveIndex++; - prevHex = nextHex; - nextHex = destTiles[curentMoveIndex]; - - // request re-initialization - initialized = false; - } - else - delete this; - } -} - -MovementAnimation::~MovementAnimation() -{ - assert(stack); - - if(moveSoundHander != -1) - CCS->soundh->stopSound(moveSoundHander); -} - -MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) - : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), - destTiles(_destTiles), - curentMoveIndex(0), - begX(0), begY(0), - distanceX(0), distanceY(0), - progressPerSecond(0.0), - moveSoundHander(-1), - progress(0.0) -{ - logAnim->debug("Created MovementAnimation for %s", stack->getName()); -} - -MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) -: StackMoveAnimation(owner, _stack, destTile, destTile) -{ - logAnim->debug("Created MovementEndAnimation for %s", stack->getName()); -} - -bool MovementEndAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - - if(!stack || myAnim->isDeadOrDying()) - { - delete this; - return false; - } - - logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); - myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); - - CCS->soundh->playSound(stack->unitType()->sounds.endMoving); - - if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) - { - delete this; - return false; - } - - - myAnim->setType(ECreatureAnimType::MOVE_END); - myAnim->onAnimationReset += [&](){ delete this; }; - - return true; -} - -MovementEndAnimation::~MovementEndAnimation() -{ - if(myAnim->getType() != ECreatureAnimType::DEAD) - myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default - - CCS->curh->show(); -} - -MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack) - : StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition()) -{ - logAnim->debug("Created MovementStartAnimation for %s", stack->getName()); -} - -bool MovementStartAnimation::init() -{ - assert(stack); - assert(!myAnim->isDeadOrDying()); - - if(!stack || myAnim->isDeadOrDying()) - { - delete this; - return false; - } - - logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); - CCS->soundh->playSound(stack->unitType()->sounds.startMoving); - - if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) - { - delete this; - return false; - } - - myAnim->setType(ECreatureAnimType::MOVE_START); - myAnim->onAnimationReset += [&](){ delete this; }; - return true; -} - -ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest) - : StackMoveAnimation(owner, stack, dest, dest) -{ - logAnim->debug("Created ReverseAnimation for %s", stack->getName()); -} - -bool ReverseAnimation::init() -{ - assert(myAnim); - assert(!myAnim->isDeadOrDying()); - - if(myAnim == nullptr || myAnim->isDeadOrDying()) - { - delete this; - return false; //there is no such creature - } - - logAnim->debug("CReverseAnimation::init: stack %s", stack->getName()); - if(myAnim->framesInGroup(ECreatureAnimType::TURN_L)) - { - myAnim->playOnce(ECreatureAnimType::TURN_L); - myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this); - } - else - { - setupSecondPart(); - } - return true; -} - -void BattleStackAnimation::rotateStack(BattleHex hex) -{ - setStackFacingRight(stack, !stackFacingRight(stack)); - - stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack)); -} - -void ReverseAnimation::setupSecondPart() -{ - assert(stack); - - if(!stack) - { - delete this; - return; - } - - rotateStack(nextHex); - - if(myAnim->framesInGroup(ECreatureAnimType::TURN_R)) - { - myAnim->playOnce(ECreatureAnimType::TURN_R); - myAnim->onAnimationReset += [&](){ delete this; }; - } - else - delete this; -} - -ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack): - StackActionAnimation(owner, _stack) -{ - setGroup(ECreatureAnimType::RESURRECTION); - logAnim->debug("Created ResurrectionAnimation for %s", stack->getName()); -} - -bool ColorTransformAnimation::init() -{ - return true; -} - -void ColorTransformAnimation::tick(uint32_t msPassed) -{ - float elapsed = msPassed / 1000.f; - float fullTime = AnimationControls::getFadeInDuration(); - float delta = elapsed / fullTime; - totalProgress += delta; - - size_t index = 0; - - while (index < timePoints.size() && timePoints[index] < totalProgress ) - ++index; - - if (index == timePoints.size()) - { - //end of animation. Apply ColorShifter using final values and die - const auto & shifter = steps[index - 1]; - owner.stacksController->setStackColorFilter(shifter, stack, spell, false); - delete this; - return; - } - - assert(index != 0); - - const auto & prevShifter = steps[index - 1]; - const auto & nextShifter = steps[index]; - - float prevPoint = timePoints[index-1]; - float nextPoint = timePoints[index]; - float localProgress = totalProgress - prevPoint; - float stepDuration = (nextPoint - prevPoint); - float factor = localProgress / stepDuration; - - auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor); - - owner.stacksController->setStackColorFilter(shifter, stack, spell, true); -} - -ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell): - BattleStackAnimation(owner, _stack), - spell(spell), - totalProgress(0.f) -{ - auto effect = owner.effectsController->getMuxerEffect(colorFilterName); - steps = effect.filters; - timePoints = effect.timePoints; - - assert(!steps.empty() && steps.size() == timePoints.size()); - - logAnim->debug("Created ColorTransformAnimation for %s", stack->getName()); -} - -RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) - : AttackAnimation(owner_, attacker, dest_, defender), - projectileEmitted(false) -{ - setSound(getCreature()->sounds.shoot); -} - -bool RangedAttackAnimation::init() -{ - setAnimationGroup(); - initializeProjectile(); - - return AttackAnimation::init(); -} - -void RangedAttackAnimation::setAnimationGroup() -{ - Point shooterPos = stackAnimation(attackingStack)->pos.topLeft(); - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack); - - //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) - static const double straightAngle = 0.2; - - double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); - - // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. - if (projectileAngle > straightAngle) - setGroup(getUpwardsGroup()); - else if (projectileAngle < -straightAngle) - setGroup(getDownwardsGroup()); - else - setGroup(getForwardGroup()); -} - -void RangedAttackAnimation::initializeProjectile() -{ - const CCreature *shooterInfo = getCreature(); - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225); - Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); - int multiplier = stackFacingRight(attackingStack) ? 1 : -1; - - if (getGroup() == getUpwardsGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; - } - else if (getGroup() == getDownwardsGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; - } - else if (getGroup() == getForwardGroup()) - { - shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; - shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; - } - else - { - assert(0); - } - - createProjectile(shotOrigin, shotTarget); -} - -void RangedAttackAnimation::emitProjectile() -{ - logAnim->debug("Ranged attack projectile emitted"); - owner.projectilesController->emitStackProjectile(attackingStack); - projectileEmitted = true; -} - -void RangedAttackAnimation::tick(uint32_t msPassed) -{ - // animation should be paused if there is an active projectile - if (projectileEmitted) - { - if (!owner.projectilesController->hasActiveProjectile(attackingStack, false)) - owner.executeAnimationStage(EAnimationEvents::HIT); - - } - - bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true); - - if (!projectileEmitted || stackHasProjectile) - stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame()); - else - stackAnimation(attackingStack)->playUntil(static_cast(-1)); - - AttackAnimation::tick(msPassed); - - if (!projectileEmitted) - { - // emit projectile once animation playback reached "climax" frame - if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() ) - { - emitProjectile(); - return; - } - } -} - -RangedAttackAnimation::~RangedAttackAnimation() -{ - assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false)); - assert(projectileEmitted); - - // FIXME: is this possible? Animation is over but we're yet to fire projectile? - if (!projectileEmitted) - { - logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now..."); - emitProjectile(); - } -} - -ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) - : RangedAttackAnimation(owner, attacker, _dest, _attacked) -{ - logAnim->debug("Created ShootingAnimation for %s", stack->getName()); -} - -void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const -{ - owner.projectilesController->createProjectile(attackingStack, from, dest); -} - -uint32_t ShootingAnimation::getAttackClimaxFrame() const -{ - const CCreature *shooterInfo = getCreature(); - - uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame; - uint32_t selectedFrame = std::clamp(shooterInfo->animation.attackClimaxFrame, 1, maxFrames); - - if (climaxFrame != selectedFrame) - logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames); - - return selectedFrame - 1; // H3 counts frames from 1 -} - -ECreatureAnimType ShootingAnimation::getUpwardsGroup() const -{ - return ECreatureAnimType::SHOOT_UP; -} - -ECreatureAnimType ShootingAnimation::getForwardGroup() const -{ - return ECreatureAnimType::SHOOT_FRONT; -} - -ECreatureAnimType ShootingAnimation::getDownwardsGroup() const -{ - return ECreatureAnimType::SHOOT_DOWN; -} - -CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) - : ShootingAnimation(owner, attacker, _dest, _attacked), - catapultDamage(_catapultDmg), - explosionEmitted(false) -{ - logAnim->debug("Created shooting anim for %s", stack->getName()); -} - -void CatapultAnimation::tick(uint32_t msPassed) -{ - ShootingAnimation::tick(msPassed); - - if ( explosionEmitted) - return; - - if ( !projectileEmitted) - return; - - if (owner.projectilesController->hasActiveProjectile(attackingStack, false)) - return; - - explosionEmitted = true; - Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); - - auto soundFilename = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS"); - AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK"); - - CCS->soundh->playSound( soundFilename ); - owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); -} - -void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const -{ - owner.projectilesController->createCatapultProjectile(attackingStack, from, dest); -} - -CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell) - : RangedAttackAnimation(owner_, attacker, dest, defender), - spell(spell) -{ - if(!dest.isValid()) - { - assert(spell->animationInfo.projectile.empty()); - - if (defender) - dest = defender->getPosition(); - else - dest = attacker->getPosition(); - } -} - -ECreatureAnimType CastAnimation::getUpwardsGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_UP, - ECreatureAnimType::SPECIAL_UP, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::SHOOT_UP, - ECreatureAnimType::ATTACK_UP - }); -} - -ECreatureAnimType CastAnimation::getForwardGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_FRONT, - ECreatureAnimType::SPECIAL_FRONT, - ECreatureAnimType::SHOOT_FRONT, - ECreatureAnimType::ATTACK_FRONT - }); -} - -ECreatureAnimType CastAnimation::getDownwardsGroup() const -{ - return findValidGroup({ - ECreatureAnimType::CAST_DOWN, - ECreatureAnimType::SPECIAL_DOWN, - ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 - ECreatureAnimType::SHOOT_DOWN, - ECreatureAnimType::ATTACK_DOWN - }); -} - -void CastAnimation::createProjectile(const Point & from, const Point & dest) const -{ - if (!spell->animationInfo.projectile.empty()) - owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell); -} - -uint32_t CastAnimation::getAttackClimaxFrame() const -{ - //TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks - uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); - - return maxFrames / 2; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): - BattleAnimation(owner), - animation(GH.renderHandler().loadAnimation(animationName)), - effectFlags(effects), - effectFinished(false), - reversed(reversed) -{ - logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName()); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - battlehexes = hex; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - assert(hex.isValid()); - battlehexes.push_back(hex); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - positions = pos; -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - positions.push_back(pos); -} - -EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed): - EffectAnimation(owner, animationName, effects, reversed) -{ - assert(hex.isValid()); - battlehexes.push_back(hex); - positions.push_back(pos); -} - -bool EffectAnimation::init() -{ - animation->preload(); - - auto first = animation->getImage(0, 0, true); - if(!first) - { - delete this; - return false; - } - - for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i) - { - size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i; - - animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE)); - } - - if (screenFill()) - { - for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i) - for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j) - positions.push_back(Point( i * first->width(), j * first->height())); - } - - BattleEffect be; - be.effectID = ID; - be.animation = animation; - be.currentFrame = 0; - be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT; - - for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i) - { - bool hasTile = i < battlehexes.size(); - bool hasPosition = i < positions.size(); - - if (hasTile && !forceOnTop()) - be.tile = battlehexes[i]; - else - be.tile = BattleHex::INVALID; - - if (hasPosition) - { - be.pos.x = positions[i].x; - be.pos.y = positions[i].y; - } - else - { - const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false); - Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); - - be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; - - if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. - be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; - - if (alignToBottom()) - be.pos.y = tilePos.y + tilePos.h - first->height(); - else - be.pos.y = tilePos.y - first->height()/2; - } - owner.effectsController->battleEffects.push_back(be); - } - return true; -} - -void EffectAnimation::tick(uint32_t msPassed) -{ - playEffect(msPassed); - - if (effectFinished) - { - //remove visual effect itself only if sound has finished as well - necessary for obstacles like force field - clearEffect(); - delete this; - } -} - -bool EffectAnimation::alignToBottom() const -{ - return effectFlags & ALIGN_TO_BOTTOM; -} - -bool EffectAnimation::forceOnTop() const -{ - return effectFlags & FORCE_ON_TOP; -} - -bool EffectAnimation::screenFill() const -{ - return effectFlags & SCREEN_FILL; -} - -void EffectAnimation::onEffectFinished() -{ - effectFinished = true; -} - -void EffectAnimation::playEffect(uint32_t msPassed) -{ - if ( effectFinished ) - return; - - for(auto & elem : owner.effectsController->battleEffects) - { - if(elem.effectID == ID) - { - elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - - if(elem.currentFrame >= elem.animation->size()) - { - elem.currentFrame = elem.animation->size() - 1; - onEffectFinished(); - break; - } - } - } -} - -void EffectAnimation::clearEffect() -{ - auto & effects = owner.effectsController->battleEffects; - - vstd::erase_if(effects, [&](const BattleEffect & effect){ - return effect.effectID == ID; - }); -} - -EffectAnimation::~EffectAnimation() -{ - assert(effectFinished); -} - -HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell): - BattleAnimation(owner), - projectileEmitted(false), - hero(hero), - target(defender), - tile(dest), - spell(spell) -{ -} - -bool HeroCastAnimation::init() -{ - hero->setPhase(EHeroAnimType::CAST_SPELL); - - hero->onPhaseFinished([&](){ - delete this; - }); - - initializeProjectile(); - - return true; -} - -void HeroCastAnimation::initializeProjectile() -{ - // spell has no projectile to play, ignore this step - if (spell->animationInfo.projectile.empty()) - return; - - // targeted spells should have well, target - assert(tile.isValid()); - - Point srccoord = hero->pos.center() - hero->parent->pos.topLeft(); - Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile - - destcoord += Point(222, 265); // FIXME: what are these constants? - owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); -} - -void HeroCastAnimation::emitProjectile() -{ - if (projectileEmitted) - return; - - //spell has no projectile to play, skip this step and immediately play hit animations - if (spell->animationInfo.projectile.empty()) - emitAnimationEvent(); - else - owner.projectilesController->emitStackProjectile( nullptr ); - - projectileEmitted = true; -} - -void HeroCastAnimation::emitAnimationEvent() -{ - owner.executeAnimationStage(EAnimationEvents::HIT); -} - -void HeroCastAnimation::tick(uint32_t msPassed) -{ - float frame = hero->getFrame(); - - if (frame < 4.0f) // middle point of animation //TODO: un-hardcode - return; - - if (!projectileEmitted) - { - emitProjectile(); - hero->pause(); - return; - } - - if (!owner.projectilesController->hasActiveProjectile(nullptr, false)) - { - emitAnimationEvent(); - //TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile - hero->play(); - } -} +/* + * BattleAnimationClasses.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 "BattleAnimationClasses.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleProjectileController.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleEffectsController.h" +#include "BattleStacksController.h" +#include "CreatureAnimation.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" + +BattleAnimation::BattleAnimation(BattleInterface & owner) + : owner(owner), + ID(owner.stacksController->animIDhelper++), + initialized(false) +{ + logAnim->trace("Animation #%d created", ID); +} + +bool BattleAnimation::tryInitialize() +{ + assert(!initialized); + + if ( init() ) + { + initialized = true; + return true; + } + return false; +} + +bool BattleAnimation::isInitialized() +{ + return initialized; +} + +BattleAnimation::~BattleAnimation() +{ + logAnim->trace("Animation #%d ended, type is %s", ID, typeid(this).name()); + for(auto & elem : pendingAnimations()) + { + if(elem == this) + elem = nullptr; + } + logAnim->trace("Animation #%d deleted", ID); +} + +std::vector & BattleAnimation::pendingAnimations() +{ + return owner.stacksController->currentAnimations; +} + +std::shared_ptr BattleAnimation::stackAnimation(const CStack * stack) const +{ + return owner.stacksController->stackAnimation[stack->unitId()]; +} + +bool BattleAnimation::stackFacingRight(const CStack * stack) +{ + return owner.stacksController->stackFacingRight[stack->unitId()]; +} + +void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight) +{ + owner.stacksController->stackFacingRight[stack->unitId()] = facingRight; +} + +BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack) + : BattleAnimation(owner), + myAnim(stackAnimation(stack)), + stack(stack) +{ + assert(myAnim); +} + +StackActionAnimation::StackActionAnimation(BattleInterface & owner, const CStack * stack) + : BattleStackAnimation(owner, stack) + , nextGroup(ECreatureAnimType::HOLDING) + , currGroup(ECreatureAnimType::HOLDING) +{ +} + +ECreatureAnimType StackActionAnimation::getGroup() const +{ + return currGroup; +} + +void StackActionAnimation::setNextGroup( ECreatureAnimType group ) +{ + nextGroup = group; +} + +void StackActionAnimation::setGroup( ECreatureAnimType group ) +{ + currGroup = group; +} + +void StackActionAnimation::setSound( const AudioPath & sound ) +{ + this->sound = sound; +} + +bool StackActionAnimation::init() +{ + if (!sound.empty()) + CCS->soundh->playSound(sound); + + if (myAnim->framesInGroup(currGroup) > 0) + { + myAnim->playOnce(currGroup); + myAnim->onAnimationReset += [&](){ delete this; }; + } + else + delete this; + + return true; +} + +StackActionAnimation::~StackActionAnimation() +{ + if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED) + myAnim->setType(ECreatureAnimType::HOLDING); + else + myAnim->setType(nextGroup); + +} + +ECreatureAnimType AttackAnimation::findValidGroup( const std::vector candidates ) const +{ + for ( auto group : candidates) + { + if(myAnim->framesInGroup(group) > 0) + return group; + } + + assert(0); + return ECreatureAnimType::HOLDING; +} + +const CCreature * AttackAnimation::getCreature() const +{ + if (attackingStack->unitType()->getId() == CreatureID::ARROW_TOWERS) + return owner.siegeController->getTurretCreature(); + else + return attackingStack->unitType(); +} + + +AttackAnimation::AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender) + : StackActionAnimation(owner, attacker), + dest(_dest), + defendingStack(defender), + attackingStack(attacker) +{ + assert(attackingStack && "attackingStack is nullptr in CBattleAttack::CBattleAttack !\n"); + attackingStackPosBeforeReturn = attackingStack->getPosition(); +} + +HittedAnimation::HittedAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) +{ + setGroup(ECreatureAnimType::HITTED); + setSound(stack->unitType()->sounds.wince); + logAnim->debug("Created HittedAnimation for %s", stack->getName()); +} + +DefenceAnimation::DefenceAnimation(BattleInterface & owner, const CStack * stack) + : StackActionAnimation(owner, stack) +{ + setGroup(ECreatureAnimType::DEFENCE); + setSound(stack->unitType()->sounds.defend); + logAnim->debug("Created DefenceAnimation for %s", stack->getName()); +} + +DeathAnimation::DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged): + StackActionAnimation(owner, stack) +{ + setSound(stack->unitType()->sounds.killed); + + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEATH_RANGED) > 0) + setGroup(ECreatureAnimType::DEATH_RANGED); + else + setGroup(ECreatureAnimType::DEATH); + + if(ranged && myAnim->framesInGroup(ECreatureAnimType::DEAD_RANGED) > 0) + setNextGroup(ECreatureAnimType::DEAD_RANGED); + else + setNextGroup(ECreatureAnimType::DEAD); + + logAnim->debug("Created DeathAnimation for %s", stack->getName()); +} + +DummyAnimation::DummyAnimation(BattleInterface & owner, int howManyFrames) + : BattleAnimation(owner), + counter(0), + howMany(howManyFrames) +{ + logAnim->debug("Created dummy animation for %d frames", howManyFrames); +} + +bool DummyAnimation::init() +{ + return true; +} + +void DummyAnimation::tick(uint32_t msPassed) +{ + counter++; + if(counter > howMany) + delete this; +} + +ECreatureAnimType MeleeAttackAnimation::getUpwardsGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_UP; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_UP + }); +} + +ECreatureAnimType MeleeAttackAnimation::getForwardGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_FRONT; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::ATTACK_FRONT + }); +} + +ECreatureAnimType MeleeAttackAnimation::getDownwardsGroup(bool multiAttack) const +{ + if (!multiAttack) + return ECreatureAnimType::ATTACK_DOWN; + + return findValidGroup({ + ECreatureAnimType::GROUP_ATTACK_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::ATTACK_DOWN + }); +} + +ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack) +{ + const ECreatureAnimType mutPosToGroup[] = + { + getUpwardsGroup (multiAttack), + getUpwardsGroup (multiAttack), + getForwardGroup (multiAttack), + getDownwardsGroup(multiAttack), + getDownwardsGroup(multiAttack), + getForwardGroup (multiAttack) + }; + + int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1); + + int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest); + if(mutPos == -1 && attackingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->getPosition()); + } + if (mutPos == -1 && defendingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, defendingStack->occupiedHex()); + } + if (mutPos == -1 && defendingStack->doubleWide() && attackingStack->doubleWide()) + { + mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn + revShiftattacker, defendingStack->occupiedHex()); + } + + assert(mutPos >= 0 && mutPos <=5); + + return mutPosToGroup[mutPos]; +} + +void MeleeAttackAnimation::tick(uint32_t msPassed) +{ + size_t currentFrame = stackAnimation(attackingStack)->getCurrentFrame(); + size_t totalFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + + if ( currentFrame * 2 >= totalFrames ) + owner.executeAnimationStage(EAnimationEvents::HIT); + + AttackAnimation::tick(msPassed); +} + +MeleeAttackAnimation::MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack) + : AttackAnimation(owner, attacker, _dest, _attacked) +{ + logAnim->debug("Created MeleeAttackAnimation for %s", attacker->getName()); + setSound(getCreature()->sounds.attack); + setGroup(selectGroup(multiAttack)); +} + +StackMoveAnimation::StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex): + BattleStackAnimation(owner, _stack), + prevHex(prevHex), + nextHex(nextHex) +{ +} + +bool MovementAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + assert(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) > 0); + + if(stackAnimation(stack)->framesInGroup(ECreatureAnimType::MOVING) == 0) + { + //no movement, end immediately + delete this; + return false; + } + + logAnim->debug("CMovementAnimation::init: stack %s moves %d -> %d", stack->getName(), prevHex, nextHex); + + //reverse unit if necessary + if(owner.stacksController->shouldRotate(stack, prevHex, nextHex)) + { + // it seems that H3 does NOT plays full rotation animation during movement + // Logical since it takes quite a lot of time + rotateStack(prevHex); + } + + if(myAnim->getType() != ECreatureAnimType::MOVING) + { + myAnim->setType(ECreatureAnimType::MOVING); + } + + if (moveSoundHander == -1) + { + moveSoundHander = CCS->soundh->playSound(stack->unitType()->sounds.move, -1); + } + + Point begPosition = owner.stacksController->getStackPositionAtHex(prevHex, stack); + Point endPosition = owner.stacksController->getStackPositionAtHex(nextHex, stack); + + progressPerSecond = AnimationControls::getMovementDistance(stack->unitType()); + + begX = begPosition.x; + begY = begPosition.y; + //progress = 0; + distanceX = endPosition.x - begPosition.x; + distanceY = endPosition.y - begPosition.y; + + if (stack->hasBonus(Selector::type()(BonusType::FLYING))) + { + float distance = static_cast(sqrt(distanceX * distanceX + distanceY * distanceY)); + progressPerSecond = AnimationControls::getFlightDistance(stack->unitType()) / distance; + } + + return true; +} + +void MovementAnimation::tick(uint32_t msPassed) +{ + progress += float(msPassed) / 1000 * progressPerSecond; + + //moving instructions + myAnim->pos.x = begX + distanceX * progress; + myAnim->pos.y = begY + distanceY * progress; + + BattleAnimation::tick(msPassed); + + if(progress >= 1.0) + { + progress -= 1.0; + // Sets the position of the creature animation sprites + Point coords = owner.stacksController->getStackPositionAtHex(nextHex, stack); + myAnim->pos.moveTo(coords); + + // true if creature haven't reached the final destination hex + if ((curentMoveIndex + 1) < destTiles.size()) + { + // update the next hex field which has to be reached by the stack + curentMoveIndex++; + prevHex = nextHex; + nextHex = destTiles[curentMoveIndex]; + + // request re-initialization + initialized = false; + } + else + delete this; + } +} + +MovementAnimation::~MovementAnimation() +{ + assert(stack); + + if(moveSoundHander != -1) + CCS->soundh->stopSound(moveSoundHander); +} + +MovementAnimation::MovementAnimation(BattleInterface & owner, const CStack *stack, std::vector _destTiles, int _distance) + : StackMoveAnimation(owner, stack, stack->getPosition(), _destTiles.front()), + destTiles(_destTiles), + curentMoveIndex(0), + begX(0), begY(0), + distanceX(0), distanceY(0), + progressPerSecond(0.0), + moveSoundHander(-1), + progress(0.0) +{ + logAnim->debug("Created MovementAnimation for %s", stack->getName()); +} + +MovementEndAnimation::MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile) +: StackMoveAnimation(owner, _stack, destTile, destTile) +{ + logAnim->debug("Created MovementEndAnimation for %s", stack->getName()); +} + +bool MovementEndAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + + if(!stack || myAnim->isDeadOrDying()) + { + delete this; + return false; + } + + logAnim->debug("CMovementEndAnimation::init: stack %s", stack->getName()); + myAnim->pos.moveTo(owner.stacksController->getStackPositionAtHex(nextHex, stack)); + + CCS->soundh->playSound(stack->unitType()->sounds.endMoving); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_END)) + { + delete this; + return false; + } + + + myAnim->setType(ECreatureAnimType::MOVE_END); + myAnim->onAnimationReset += [&](){ delete this; }; + + return true; +} + +MovementEndAnimation::~MovementEndAnimation() +{ + if(myAnim->getType() != ECreatureAnimType::DEAD) + myAnim->setType(ECreatureAnimType::HOLDING); //resetting to default + + CCS->curh->show(); +} + +MovementStartAnimation::MovementStartAnimation(BattleInterface & owner, const CStack * _stack) + : StackMoveAnimation(owner, _stack, _stack->getPosition(), _stack->getPosition()) +{ + logAnim->debug("Created MovementStartAnimation for %s", stack->getName()); +} + +bool MovementStartAnimation::init() +{ + assert(stack); + assert(!myAnim->isDeadOrDying()); + + if(!stack || myAnim->isDeadOrDying()) + { + delete this; + return false; + } + + logAnim->debug("CMovementStartAnimation::init: stack %s", stack->getName()); + CCS->soundh->playSound(stack->unitType()->sounds.startMoving); + + if(!myAnim->framesInGroup(ECreatureAnimType::MOVE_START)) + { + delete this; + return false; + } + + myAnim->setType(ECreatureAnimType::MOVE_START); + myAnim->onAnimationReset += [&](){ delete this; }; + return true; +} + +ReverseAnimation::ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest) + : StackMoveAnimation(owner, stack, dest, dest) +{ + logAnim->debug("Created ReverseAnimation for %s", stack->getName()); +} + +bool ReverseAnimation::init() +{ + assert(myAnim); + assert(!myAnim->isDeadOrDying()); + + if(myAnim == nullptr || myAnim->isDeadOrDying()) + { + delete this; + return false; //there is no such creature + } + + logAnim->debug("CReverseAnimation::init: stack %s", stack->getName()); + if(myAnim->framesInGroup(ECreatureAnimType::TURN_L)) + { + myAnim->playOnce(ECreatureAnimType::TURN_L); + myAnim->onAnimationReset += std::bind(&ReverseAnimation::setupSecondPart, this); + } + else + { + setupSecondPart(); + } + return true; +} + +void BattleStackAnimation::rotateStack(BattleHex hex) +{ + setStackFacingRight(stack, !stackFacingRight(stack)); + + stackAnimation(stack)->pos.moveTo(owner.stacksController->getStackPositionAtHex(hex, stack)); +} + +void ReverseAnimation::setupSecondPart() +{ + assert(stack); + + if(!stack) + { + delete this; + return; + } + + rotateStack(nextHex); + + if(myAnim->framesInGroup(ECreatureAnimType::TURN_R)) + { + myAnim->playOnce(ECreatureAnimType::TURN_R); + myAnim->onAnimationReset += [&](){ delete this; }; + } + else + delete this; +} + +ResurrectionAnimation::ResurrectionAnimation(BattleInterface & owner, const CStack * _stack): + StackActionAnimation(owner, _stack) +{ + setGroup(ECreatureAnimType::RESURRECTION); + logAnim->debug("Created ResurrectionAnimation for %s", stack->getName()); +} + +bool ColorTransformAnimation::init() +{ + return true; +} + +void ColorTransformAnimation::tick(uint32_t msPassed) +{ + float elapsed = msPassed / 1000.f; + float fullTime = AnimationControls::getFadeInDuration(); + float delta = elapsed / fullTime; + totalProgress += delta; + + size_t index = 0; + + while (index < timePoints.size() && timePoints[index] < totalProgress ) + ++index; + + if (index == timePoints.size()) + { + //end of animation. Apply ColorShifter using final values and die + const auto & shifter = steps[index - 1]; + owner.stacksController->setStackColorFilter(shifter, stack, spell, false); + delete this; + return; + } + + assert(index != 0); + + const auto & prevShifter = steps[index - 1]; + const auto & nextShifter = steps[index]; + + float prevPoint = timePoints[index-1]; + float nextPoint = timePoints[index]; + float localProgress = totalProgress - prevPoint; + float stepDuration = (nextPoint - prevPoint); + float factor = localProgress / stepDuration; + + auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor); + + owner.stacksController->setStackColorFilter(shifter, stack, spell, true); +} + +ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell): + BattleStackAnimation(owner, _stack), + spell(spell), + totalProgress(0.f) +{ + auto effect = owner.effectsController->getMuxerEffect(colorFilterName); + steps = effect.filters; + timePoints = effect.timePoints; + + assert(!steps.empty() && steps.size() == timePoints.size()); + + logAnim->debug("Created ColorTransformAnimation for %s", stack->getName()); +} + +RangedAttackAnimation::RangedAttackAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest_, const CStack * defender) + : AttackAnimation(owner_, attacker, dest_, defender), + projectileEmitted(false) +{ + setSound(getCreature()->sounds.shoot); +} + +bool RangedAttackAnimation::init() +{ + setAnimationGroup(); + initializeProjectile(); + + return AttackAnimation::init(); +} + +void RangedAttackAnimation::setAnimationGroup() +{ + Point shooterPos = stackAnimation(attackingStack)->pos.topLeft(); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack); + + //maximal angle in radians between straight horizontal line and shooting line for which shot is considered to be straight (absoulte value) + static const double straightAngle = 0.2; + + double projectileAngle = -atan2(shotTarget.y - shooterPos.y, std::abs(shotTarget.x - shooterPos.x)); + + // Calculate projectile start position. Offsets are read out of the CRANIM.TXT. + if (projectileAngle > straightAngle) + setGroup(getUpwardsGroup()); + else if (projectileAngle < -straightAngle) + setGroup(getDownwardsGroup()); + else + setGroup(getForwardGroup()); +} + +void RangedAttackAnimation::initializeProjectile() +{ + const CCreature *shooterInfo = getCreature(); + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225); + Point shotOrigin = stackAnimation(attackingStack)->pos.topLeft() + Point(222, 265); + int multiplier = stackFacingRight(attackingStack) ? 1 : -1; + + if (getGroup() == getUpwardsGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.upperRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.upperRightMissleOffsetY; + } + else if (getGroup() == getDownwardsGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.lowerRightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.lowerRightMissleOffsetY; + } + else if (getGroup() == getForwardGroup()) + { + shotOrigin.x += ( -25 + shooterInfo->animation.rightMissleOffsetX ) * multiplier; + shotOrigin.y += shooterInfo->animation.rightMissleOffsetY; + } + else + { + assert(0); + } + + createProjectile(shotOrigin, shotTarget); +} + +void RangedAttackAnimation::emitProjectile() +{ + logAnim->debug("Ranged attack projectile emitted"); + owner.projectilesController->emitStackProjectile(attackingStack); + projectileEmitted = true; +} + +void RangedAttackAnimation::tick(uint32_t msPassed) +{ + // animation should be paused if there is an active projectile + if (projectileEmitted) + { + if (!owner.projectilesController->hasActiveProjectile(attackingStack, false)) + owner.executeAnimationStage(EAnimationEvents::HIT); + + } + + bool stackHasProjectile = owner.projectilesController->hasActiveProjectile(stack, true); + + if (!projectileEmitted || stackHasProjectile) + stackAnimation(attackingStack)->playUntil(getAttackClimaxFrame()); + else + stackAnimation(attackingStack)->playUntil(static_cast(-1)); + + AttackAnimation::tick(msPassed); + + if (!projectileEmitted) + { + // emit projectile once animation playback reached "climax" frame + if ( stackAnimation(attackingStack)->getCurrentFrame() >= getAttackClimaxFrame() ) + { + emitProjectile(); + return; + } + } +} + +RangedAttackAnimation::~RangedAttackAnimation() +{ + assert(!owner.projectilesController->hasActiveProjectile(attackingStack, false)); + assert(projectileEmitted); + + // FIXME: is this possible? Animation is over but we're yet to fire projectile? + if (!projectileEmitted) + { + logAnim->warn("Shooting animation has finished but projectile was not emitted! Emitting it now..."); + emitProjectile(); + } +} + +ShootingAnimation::ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked) + : RangedAttackAnimation(owner, attacker, _dest, _attacked) +{ + logAnim->debug("Created ShootingAnimation for %s", stack->getName()); +} + +void ShootingAnimation::createProjectile(const Point & from, const Point & dest) const +{ + owner.projectilesController->createProjectile(attackingStack, from, dest); +} + +uint32_t ShootingAnimation::getAttackClimaxFrame() const +{ + const CCreature *shooterInfo = getCreature(); + + uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + uint32_t climaxFrame = shooterInfo->animation.attackClimaxFrame; + uint32_t selectedFrame = std::clamp(shooterInfo->animation.attackClimaxFrame, 1, maxFrames); + + if (climaxFrame != selectedFrame) + logGlobal->warn("Shooter %s has ranged attack climax frame set to %d, but only %d available!", shooterInfo->getNamePluralTranslated(), climaxFrame, maxFrames); + + return selectedFrame - 1; // H3 counts frames from 1 +} + +ECreatureAnimType ShootingAnimation::getUpwardsGroup() const +{ + return ECreatureAnimType::SHOOT_UP; +} + +ECreatureAnimType ShootingAnimation::getForwardGroup() const +{ + return ECreatureAnimType::SHOOT_FRONT; +} + +ECreatureAnimType ShootingAnimation::getDownwardsGroup() const +{ + return ECreatureAnimType::SHOOT_DOWN; +} + +CatapultAnimation::CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, int _catapultDmg) + : ShootingAnimation(owner, attacker, _dest, _attacked), + catapultDamage(_catapultDmg), + explosionEmitted(false) +{ + logAnim->debug("Created shooting anim for %s", stack->getName()); +} + +void CatapultAnimation::tick(uint32_t msPassed) +{ + ShootingAnimation::tick(msPassed); + + if ( explosionEmitted) + return; + + if ( !projectileEmitted) + return; + + if (owner.projectilesController->hasActiveProjectile(attackingStack, false)) + return; + + explosionEmitted = true; + Point shotTarget = owner.stacksController->getStackPositionAtHex(dest, defendingStack) + Point(225, 225) - Point(126, 105); + + auto soundFilename = AudioPath::builtin((catapultDamage > 0) ? "WALLHIT" : "WALLMISS"); + AnimationPath effectFilename = AnimationPath::builtin((catapultDamage > 0) ? "SGEXPL" : "CSGRCK"); + + CCS->soundh->playSound( soundFilename ); + owner.stacksController->addNewAnim( new EffectAnimation(owner, effectFilename, shotTarget)); +} + +void CatapultAnimation::createProjectile(const Point & from, const Point & dest) const +{ + owner.projectilesController->createCatapultProjectile(attackingStack, from, dest); +} + +CastAnimation::CastAnimation(BattleInterface & owner_, const CStack * attacker, BattleHex dest, const CStack * defender, const CSpell * spell) + : RangedAttackAnimation(owner_, attacker, dest, defender), + spell(spell) +{ + if(!dest.isValid()) + { + assert(spell->animationInfo.projectile.empty()); + + if (defender) + dest = defender->getPosition(); + else + dest = attacker->getPosition(); + } +} + +ECreatureAnimType CastAnimation::getUpwardsGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_UP, + ECreatureAnimType::SPECIAL_UP, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_UP, + ECreatureAnimType::ATTACK_UP + }); +} + +ECreatureAnimType CastAnimation::getForwardGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_FRONT, + ECreatureAnimType::SPECIAL_FRONT, + ECreatureAnimType::SHOOT_FRONT, + ECreatureAnimType::ATTACK_FRONT + }); +} + +ECreatureAnimType CastAnimation::getDownwardsGroup() const +{ + return findValidGroup({ + ECreatureAnimType::CAST_DOWN, + ECreatureAnimType::SPECIAL_DOWN, + ECreatureAnimType::SPECIAL_FRONT, // weird, but required for H3 + ECreatureAnimType::SHOOT_DOWN, + ECreatureAnimType::ATTACK_DOWN + }); +} + +void CastAnimation::createProjectile(const Point & from, const Point & dest) const +{ + if (!spell->animationInfo.projectile.empty()) + owner.projectilesController->createSpellProjectile(attackingStack, from, dest, spell); +} + +uint32_t CastAnimation::getAttackClimaxFrame() const +{ + //TODO: allow defining this parameter in config file, separately from attackClimaxFrame of missile attacks + uint32_t maxFrames = stackAnimation(attackingStack)->framesInGroup(getGroup()); + + return maxFrames / 2; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed): + BattleAnimation(owner), + animation(GH.renderHandler().loadAnimation(animationName)), + effectFlags(effects), + effectFinished(false), + reversed(reversed) +{ + logAnim->debug("CPointEffectAnimation::init: effect %s", animationName.getName()); +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + battlehexes = hex; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + assert(hex.isValid()); + battlehexes.push_back(hex); +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + positions = pos; +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + positions.push_back(pos); +} + +EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed): + EffectAnimation(owner, animationName, effects, reversed) +{ + assert(hex.isValid()); + battlehexes.push_back(hex); + positions.push_back(pos); +} + +bool EffectAnimation::init() +{ + animation->preload(); + + auto first = animation->getImage(0, 0, true); + if(!first) + { + delete this; + return false; + } + + for (size_t i = 0; i < animation->size(size_t(BattleEffect::AnimType::DEFAULT)); ++i) + { + size_t current = animation->size(size_t(BattleEffect::AnimType::DEFAULT)) - 1 - i; + + animation->duplicateImage(size_t(BattleEffect::AnimType::DEFAULT), current, size_t(BattleEffect::AnimType::REVERSE)); + } + + if (screenFill()) + { + for(int i=0; i * first->width() < owner.fieldController->pos.w ; ++i) + for(int j=0; j * first->height() < owner.fieldController->pos.h ; ++j) + positions.push_back(Point( i * first->width(), j * first->height())); + } + + BattleEffect be; + be.effectID = ID; + be.animation = animation; + be.currentFrame = 0; + be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT; + + for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i) + { + bool hasTile = i < battlehexes.size(); + bool hasPosition = i < positions.size(); + + if (hasTile && !forceOnTop()) + be.tile = battlehexes[i]; + else + be.tile = BattleHex::INVALID; + + if (hasPosition) + { + be.pos.x = positions[i].x; + be.pos.y = positions[i].y; + } + else + { + const auto * destStack = owner.getBattle()->battleGetUnitByPos(battlehexes[i], false); + Rect tilePos = owner.fieldController->hexPositionLocal(battlehexes[i]); + + be.pos.x = tilePos.x + tilePos.w/2 - first->width()/2; + + if(destStack && destStack->doubleWide()) // Correction for 2-hex creatures. + be.pos.x += (destStack->unitSide() == BattleSide::ATTACKER ? -1 : 1)*tilePos.w/2; + + if (alignToBottom()) + be.pos.y = tilePos.y + tilePos.h - first->height(); + else + be.pos.y = tilePos.y - first->height()/2; + } + owner.effectsController->battleEffects.push_back(be); + } + return true; +} + +void EffectAnimation::tick(uint32_t msPassed) +{ + playEffect(msPassed); + + if (effectFinished) + { + //remove visual effect itself only if sound has finished as well - necessary for obstacles like force field + clearEffect(); + delete this; + } +} + +bool EffectAnimation::alignToBottom() const +{ + return effectFlags & ALIGN_TO_BOTTOM; +} + +bool EffectAnimation::forceOnTop() const +{ + return effectFlags & FORCE_ON_TOP; +} + +bool EffectAnimation::screenFill() const +{ + return effectFlags & SCREEN_FILL; +} + +void EffectAnimation::onEffectFinished() +{ + effectFinished = true; +} + +void EffectAnimation::playEffect(uint32_t msPassed) +{ + if ( effectFinished ) + return; + + for(auto & elem : owner.effectsController->battleEffects) + { + if(elem.effectID == ID) + { + elem.currentFrame += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + + if(elem.currentFrame >= elem.animation->size()) + { + elem.currentFrame = elem.animation->size() - 1; + onEffectFinished(); + break; + } + } + } +} + +void EffectAnimation::clearEffect() +{ + auto & effects = owner.effectsController->battleEffects; + + vstd::erase_if(effects, [&](const BattleEffect & effect){ + return effect.effectID == ID; + }); +} + +EffectAnimation::~EffectAnimation() +{ + assert(effectFinished); +} + +HeroCastAnimation::HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell): + BattleAnimation(owner), + projectileEmitted(false), + hero(hero), + target(defender), + tile(dest), + spell(spell) +{ +} + +bool HeroCastAnimation::init() +{ + hero->setPhase(EHeroAnimType::CAST_SPELL); + + hero->onPhaseFinished([&](){ + delete this; + }); + + initializeProjectile(); + + return true; +} + +void HeroCastAnimation::initializeProjectile() +{ + // spell has no projectile to play, ignore this step + if (spell->animationInfo.projectile.empty()) + return; + + // targeted spells should have well, target + assert(tile.isValid()); + + Point srccoord = hero->pos.center() - hero->parent->pos.topLeft(); + Point destcoord = owner.stacksController->getStackPositionAtHex(tile, target); //position attacked by projectile + + destcoord += Point(222, 265); // FIXME: what are these constants? + owner.projectilesController->createSpellProjectile( nullptr, srccoord, destcoord, spell); +} + +void HeroCastAnimation::emitProjectile() +{ + if (projectileEmitted) + return; + + //spell has no projectile to play, skip this step and immediately play hit animations + if (spell->animationInfo.projectile.empty()) + emitAnimationEvent(); + else + owner.projectilesController->emitStackProjectile( nullptr ); + + projectileEmitted = true; +} + +void HeroCastAnimation::emitAnimationEvent() +{ + owner.executeAnimationStage(EAnimationEvents::HIT); +} + +void HeroCastAnimation::tick(uint32_t msPassed) +{ + float frame = hero->getFrame(); + + if (frame < 4.0f) // middle point of animation //TODO: un-hardcode + return; + + if (!projectileEmitted) + { + emitProjectile(); + hero->pause(); + return; + } + + if (!owner.projectilesController->hasActiveProjectile(nullptr, false)) + { + emitAnimationEvent(); + //TODO: check H3 - it is possible that hero animation should be paused until hit effect is over, not just projectile + hero->play(); + } +} diff --git a/client/battle/BattleAnimationClasses.h b/client/battle/BattleAnimationClasses.h index 11fd827a8..00b68bf7a 100644 --- a/client/battle/BattleAnimationClasses.h +++ b/client/battle/BattleAnimationClasses.h @@ -1,372 +1,372 @@ -/* - * BattleAnimations.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/battle/BattleHex.h" -#include "../../lib/filesystem/ResourcePath.h" -#include "BattleConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CCreature; -class CSpell; -class Point; - -VCMI_LIB_NAMESPACE_END - -class ColorFilter; -class BattleHero; -class CAnimation; -class BattleInterface; -class CreatureAnimation; -struct StackAttackedInfo; - -/// Base class of battle animations -class BattleAnimation -{ -protected: - BattleInterface & owner; - bool initialized; - - std::vector & pendingAnimations(); - std::shared_ptr stackAnimation(const CStack * stack) const; - bool stackFacingRight(const CStack * stack); - void setStackFacingRight(const CStack * stack, bool facingRight); - - virtual bool init() = 0; //to be called - if returned false, call again until returns true - -public: - ui32 ID; //unique identifier - - bool isInitialized(); - bool tryInitialize(); - virtual void tick(uint32_t msPassed) {} //call every new frame - virtual ~BattleAnimation(); - - BattleAnimation(BattleInterface & owner); -}; - -/// Sub-class which is responsible for managing the battle stack animation. -class BattleStackAnimation : public BattleAnimation -{ -public: - std::shared_ptr myAnim; //animation for our stack, managed by BattleInterface - const CStack * stack; //id of stack whose animation it is - - BattleStackAnimation(BattleInterface & owner, const CStack * _stack); - void rotateStack(BattleHex hex); -}; - -class StackActionAnimation : public BattleStackAnimation -{ - ECreatureAnimType nextGroup; - ECreatureAnimType currGroup; - AudioPath sound; -public: - void setNextGroup( ECreatureAnimType group ); - void setGroup( ECreatureAnimType group ); - void setSound( const AudioPath & sound ); - - ECreatureAnimType getGroup() const; - - StackActionAnimation(BattleInterface & owner, const CStack * _stack); - ~StackActionAnimation(); - - bool init() override; -}; - -/// Animation of a defending unit -class DefenceAnimation : public StackActionAnimation -{ -public: - DefenceAnimation(BattleInterface & owner, const CStack * stack); -}; - -/// Animation of a hit unit -class HittedAnimation : public StackActionAnimation -{ -public: - HittedAnimation(BattleInterface & owner, const CStack * stack); -}; - -/// Animation of a dying unit -class DeathAnimation : public StackActionAnimation -{ -public: - DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged); -}; - -/// Resurrects stack from dead state -class ResurrectionAnimation : public StackActionAnimation -{ -public: - ResurrectionAnimation(BattleInterface & owner, const CStack * _stack); -}; - -class ColorTransformAnimation : public BattleStackAnimation -{ - std::vector steps; - std::vector timePoints; - const CSpell * spell; - - float totalProgress; - - bool init() override; - void tick(uint32_t msPassed) override; - -public: - ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell); -}; - -/// Base class for all animations that play during stack movement -class StackMoveAnimation : public BattleStackAnimation -{ -public: - BattleHex nextHex; - BattleHex prevHex; - -protected: - StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex); -}; - -/// Move animation of a creature -class MovementAnimation : public StackMoveAnimation -{ -private: - int moveSoundHander; // sound handler used when moving a unit - - std::vector destTiles; //full path, includes already passed hexes - ui32 curentMoveIndex; // index of nextHex in destTiles - - double begX, begY; // starting position - double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft - - /// progress gain per second - double progressPerSecond; - - /// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends - double progress; - -public: - bool init() override; - void tick(uint32_t msPassed) override; - - MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); - ~MovementAnimation(); -}; - -/// Move end animation of a creature -class MovementEndAnimation : public StackMoveAnimation -{ -public: - bool init() override; - - MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); - ~MovementEndAnimation(); -}; - -/// Move start animation of a creature -class MovementStartAnimation : public StackMoveAnimation -{ -public: - bool init() override; - - MovementStartAnimation(BattleInterface & owner, const CStack * _stack); -}; - -/// Class responsible for animation of stack chaning direction (left <-> right) -class ReverseAnimation : public StackMoveAnimation -{ - void setupSecondPart(); -public: - bool init() override; - - ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest); -}; - -/// This class is responsible for managing the battle attack animation -class AttackAnimation : public StackActionAnimation -{ -protected: - BattleHex dest; //attacked hex - const CStack *defendingStack; - const CStack *attackingStack; - int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature - - const CCreature * getCreature() const; - ECreatureAnimType findValidGroup( const std::vector candidates ) const; - -public: - AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); -}; - -/// Hand-to-hand attack -class MeleeAttackAnimation : public AttackAnimation -{ - ECreatureAnimType getUpwardsGroup(bool multiAttack) const; - ECreatureAnimType getForwardGroup(bool multiAttack) const; - ECreatureAnimType getDownwardsGroup(bool multiAttack) const; - - ECreatureAnimType selectGroup(bool multiAttack); - -public: - MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack); - - void tick(uint32_t msPassed) override; -}; - - -class RangedAttackAnimation : public AttackAnimation -{ - void setAnimationGroup(); - void initializeProjectile(); - void emitProjectile(); - void emitExplosion(); - -protected: - bool projectileEmitted; - - virtual ECreatureAnimType getUpwardsGroup() const = 0; - virtual ECreatureAnimType getForwardGroup() const = 0; - virtual ECreatureAnimType getDownwardsGroup() const = 0; - - virtual void createProjectile(const Point & from, const Point & dest) const = 0; - virtual uint32_t getAttackClimaxFrame() const = 0; - -public: - RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); - ~RangedAttackAnimation(); - - bool init() override; - void tick(uint32_t msPassed) override; -}; - -/// Shooting attack -class ShootingAnimation : public RangedAttackAnimation -{ - ECreatureAnimType getUpwardsGroup() const override; - ECreatureAnimType getForwardGroup() const override; - ECreatureAnimType getDownwardsGroup() const override; - - void createProjectile(const Point & from, const Point & dest) const override; - uint32_t getAttackClimaxFrame() const override; - -public: - ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); - -}; - -/// Catapult attack -class CatapultAnimation : public ShootingAnimation -{ -private: - bool explosionEmitted; - int catapultDamage; - -public: - CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); - - void createProjectile(const Point & from, const Point & dest) const override; - void tick(uint32_t msPassed) override; -}; - -class CastAnimation : public RangedAttackAnimation -{ - const CSpell * spell; - - ECreatureAnimType getUpwardsGroup() const override; - ECreatureAnimType getForwardGroup() const override; - ECreatureAnimType getDownwardsGroup() const override; - - void createProjectile(const Point & from, const Point & dest) const override; - uint32_t getAttackClimaxFrame() const override; - -public: - CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); -}; - -class DummyAnimation : public BattleAnimation -{ -private: - int counter; - int howMany; -public: - bool init() override; - void tick(uint32_t msPassed) override; - - DummyAnimation(BattleInterface & owner, int howManyFrames); -}; - -/// Class that plays effect at one or more positions along with (single) sound effect -class EffectAnimation : public BattleAnimation -{ - std::string soundName; - bool effectFinished; - bool reversed; - int effectFlags; - - std::shared_ptr animation; - std::vector positions; - std::vector battlehexes; - - bool alignToBottom() const; - bool waitForSound() const; - bool forceOnTop() const; - bool screenFill() const; - - void onEffectFinished(); - void clearEffect(); - void playEffect(uint32_t msPassed); - -public: - enum EEffectFlags - { - ALIGN_TO_BOTTOM = 1, - FORCE_ON_TOP = 2, - SCREEN_FILL = 4, - }; - - /// Create animation with screen-wide effect - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false); - - /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos , int effects = 0, bool reversed = false); - - /// Create animation positioned at certain hex(es) - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex , int effects = 0, bool reversed = false); - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects = 0, bool reversed = false); - - EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); - ~EffectAnimation(); - - bool init() override; - void tick(uint32_t msPassed) override; -}; - -class HeroCastAnimation : public BattleAnimation -{ - std::shared_ptr hero; - const CStack * target; - const CSpell * spell; - BattleHex tile; - bool projectileEmitted; - - void initializeProjectile(); - void emitProjectile(); - void emitAnimationEvent(); - -public: - HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell); - - void tick(uint32_t msPassed) override; - bool init() override; -}; +/* + * BattleAnimations.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/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "BattleConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CCreature; +class CSpell; +class Point; + +VCMI_LIB_NAMESPACE_END + +class ColorFilter; +class BattleHero; +class CAnimation; +class BattleInterface; +class CreatureAnimation; +struct StackAttackedInfo; + +/// Base class of battle animations +class BattleAnimation +{ +protected: + BattleInterface & owner; + bool initialized; + + std::vector & pendingAnimations(); + std::shared_ptr stackAnimation(const CStack * stack) const; + bool stackFacingRight(const CStack * stack); + void setStackFacingRight(const CStack * stack, bool facingRight); + + virtual bool init() = 0; //to be called - if returned false, call again until returns true + +public: + ui32 ID; //unique identifier + + bool isInitialized(); + bool tryInitialize(); + virtual void tick(uint32_t msPassed) {} //call every new frame + virtual ~BattleAnimation(); + + BattleAnimation(BattleInterface & owner); +}; + +/// Sub-class which is responsible for managing the battle stack animation. +class BattleStackAnimation : public BattleAnimation +{ +public: + std::shared_ptr myAnim; //animation for our stack, managed by BattleInterface + const CStack * stack; //id of stack whose animation it is + + BattleStackAnimation(BattleInterface & owner, const CStack * _stack); + void rotateStack(BattleHex hex); +}; + +class StackActionAnimation : public BattleStackAnimation +{ + ECreatureAnimType nextGroup; + ECreatureAnimType currGroup; + AudioPath sound; +public: + void setNextGroup( ECreatureAnimType group ); + void setGroup( ECreatureAnimType group ); + void setSound( const AudioPath & sound ); + + ECreatureAnimType getGroup() const; + + StackActionAnimation(BattleInterface & owner, const CStack * _stack); + ~StackActionAnimation(); + + bool init() override; +}; + +/// Animation of a defending unit +class DefenceAnimation : public StackActionAnimation +{ +public: + DefenceAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a hit unit +class HittedAnimation : public StackActionAnimation +{ +public: + HittedAnimation(BattleInterface & owner, const CStack * stack); +}; + +/// Animation of a dying unit +class DeathAnimation : public StackActionAnimation +{ +public: + DeathAnimation(BattleInterface & owner, const CStack * stack, bool ranged); +}; + +/// Resurrects stack from dead state +class ResurrectionAnimation : public StackActionAnimation +{ +public: + ResurrectionAnimation(BattleInterface & owner, const CStack * _stack); +}; + +class ColorTransformAnimation : public BattleStackAnimation +{ + std::vector steps; + std::vector timePoints; + const CSpell * spell; + + float totalProgress; + + bool init() override; + void tick(uint32_t msPassed) override; + +public: + ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell); +}; + +/// Base class for all animations that play during stack movement +class StackMoveAnimation : public BattleStackAnimation +{ +public: + BattleHex nextHex; + BattleHex prevHex; + +protected: + StackMoveAnimation(BattleInterface & owner, const CStack * _stack, BattleHex prevHex, BattleHex nextHex); +}; + +/// Move animation of a creature +class MovementAnimation : public StackMoveAnimation +{ +private: + int moveSoundHander; // sound handler used when moving a unit + + std::vector destTiles; //full path, includes already passed hexes + ui32 curentMoveIndex; // index of nextHex in destTiles + + double begX, begY; // starting position + double distanceX, distanceY; // full movement distance, may be negative if creture moves topleft + + /// progress gain per second + double progressPerSecond; + + /// range 0 -> 1, indicates move progrees. 0 = movement starts, 1 = move ends + double progress; + +public: + bool init() override; + void tick(uint32_t msPassed) override; + + MovementAnimation(BattleInterface & owner, const CStack *_stack, std::vector _destTiles, int _distance); + ~MovementAnimation(); +}; + +/// Move end animation of a creature +class MovementEndAnimation : public StackMoveAnimation +{ +public: + bool init() override; + + MovementEndAnimation(BattleInterface & owner, const CStack * _stack, BattleHex destTile); + ~MovementEndAnimation(); +}; + +/// Move start animation of a creature +class MovementStartAnimation : public StackMoveAnimation +{ +public: + bool init() override; + + MovementStartAnimation(BattleInterface & owner, const CStack * _stack); +}; + +/// Class responsible for animation of stack chaning direction (left <-> right) +class ReverseAnimation : public StackMoveAnimation +{ + void setupSecondPart(); +public: + bool init() override; + + ReverseAnimation(BattleInterface & owner, const CStack * stack, BattleHex dest); +}; + +/// This class is responsible for managing the battle attack animation +class AttackAnimation : public StackActionAnimation +{ +protected: + BattleHex dest; //attacked hex + const CStack *defendingStack; + const CStack *attackingStack; + int attackingStackPosBeforeReturn; //for stacks with return_after_strike feature + + const CCreature * getCreature() const; + ECreatureAnimType findValidGroup( const std::vector candidates ) const; + +public: + AttackAnimation(BattleInterface & owner, const CStack *attacker, BattleHex _dest, const CStack *defender); +}; + +/// Hand-to-hand attack +class MeleeAttackAnimation : public AttackAnimation +{ + ECreatureAnimType getUpwardsGroup(bool multiAttack) const; + ECreatureAnimType getForwardGroup(bool multiAttack) const; + ECreatureAnimType getDownwardsGroup(bool multiAttack) const; + + ECreatureAnimType selectGroup(bool multiAttack); + +public: + MeleeAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex _dest, const CStack * _attacked, bool multiAttack); + + void tick(uint32_t msPassed) override; +}; + + +class RangedAttackAnimation : public AttackAnimation +{ + void setAnimationGroup(); + void initializeProjectile(); + void emitProjectile(); + void emitExplosion(); + +protected: + bool projectileEmitted; + + virtual ECreatureAnimType getUpwardsGroup() const = 0; + virtual ECreatureAnimType getForwardGroup() const = 0; + virtual ECreatureAnimType getDownwardsGroup() const = 0; + + virtual void createProjectile(const Point & from, const Point & dest) const = 0; + virtual uint32_t getAttackClimaxFrame() const = 0; + +public: + RangedAttackAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + ~RangedAttackAnimation(); + + bool init() override; + void tick(uint32_t msPassed) override; +}; + +/// Shooting attack +class ShootingAnimation : public RangedAttackAnimation +{ + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; + + void createProjectile(const Point & from, const Point & dest) const override; + uint32_t getAttackClimaxFrame() const override; + +public: + ShootingAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender); + +}; + +/// Catapult attack +class CatapultAnimation : public ShootingAnimation +{ +private: + bool explosionEmitted; + int catapultDamage; + +public: + CatapultAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest, const CStack * defender, int _catapultDmg = 0); + + void createProjectile(const Point & from, const Point & dest) const override; + void tick(uint32_t msPassed) override; +}; + +class CastAnimation : public RangedAttackAnimation +{ + const CSpell * spell; + + ECreatureAnimType getUpwardsGroup() const override; + ECreatureAnimType getForwardGroup() const override; + ECreatureAnimType getDownwardsGroup() const override; + + void createProjectile(const Point & from, const Point & dest) const override; + uint32_t getAttackClimaxFrame() const override; + +public: + CastAnimation(BattleInterface & owner, const CStack * attacker, BattleHex dest_, const CStack * defender, const CSpell * spell); +}; + +class DummyAnimation : public BattleAnimation +{ +private: + int counter; + int howMany; +public: + bool init() override; + void tick(uint32_t msPassed) override; + + DummyAnimation(BattleInterface & owner, int howManyFrames); +}; + +/// Class that plays effect at one or more positions along with (single) sound effect +class EffectAnimation : public BattleAnimation +{ + std::string soundName; + bool effectFinished; + bool reversed; + int effectFlags; + + std::shared_ptr animation; + std::vector positions; + std::vector battlehexes; + + bool alignToBottom() const; + bool waitForSound() const; + bool forceOnTop() const; + bool screenFill() const; + + void onEffectFinished(); + void clearEffect(); + void playEffect(uint32_t msPassed); + +public: + enum EEffectFlags + { + ALIGN_TO_BOTTOM = 1, + FORCE_ON_TOP = 2, + SCREEN_FILL = 4, + }; + + /// Create animation with screen-wide effect + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false); + + /// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector pos , int effects = 0, bool reversed = false); + + /// Create animation positioned at certain hex(es) + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex , int effects = 0, bool reversed = false); + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector hex, int effects = 0, bool reversed = false); + + EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects = 0, bool reversed = false); + ~EffectAnimation(); + + bool init() override; + void tick(uint32_t msPassed) override; +}; + +class HeroCastAnimation : public BattleAnimation +{ + std::shared_ptr hero; + const CStack * target; + const CSpell * spell; + BattleHex tile; + bool projectileEmitted; + + void initializeProjectile(); + void emitProjectile(); + void emitAnimationEvent(); + +public: + HeroCastAnimation(BattleInterface & owner, std::shared_ptr hero, BattleHex dest, const CStack * defender, const CSpell * spell); + + void tick(uint32_t msPassed) override; + bool init() override; +}; diff --git a/client/battle/BattleConstants.h b/client/battle/BattleConstants.h index fb6bb4baa..580e864bd 100644 --- a/client/battle/BattleConstants.h +++ b/client/battle/BattleConstants.h @@ -1,100 +1,100 @@ -/* - * BattleConstants.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 - -enum class EBattleEffect -{ - // list of battle effects that have hardcoded triggers - MAGIC_MIRROR = 3, - FIRE_SHIELD = 11, - FEAR = 15, - GOOD_LUCK = 18, - GOOD_MORALE = 20, - BAD_MORALE = 30, - BAD_LUCK = 48, - RESURRECT = 50, - DRAIN_LIFE = 52, - POISON = 67, - DEATH_BLOW = 73, - REGENERATION = 74, - MANA_DRAIN = 77, - RESISTANCE = 78, - - INVALID = -1, -}; - -enum class EAnimationEvents -{ - // any action - ROTATE, // stacks rotate before action - - // movement action - MOVE_START, // stack starts movement - MOVEMENT, // movement animation loop starts - MOVE_END, // stack end movement - - // attack/spellcast action - BEFORE_HIT, // attack and defence effects play, e.g. luck/death blow - ATTACK, // attack and defence animations are playing - HIT, // hit & death animations are playing - AFTER_HIT, // post-attack effect, e.g. phoenix rebirth - - COUNT -}; - -enum class EHeroAnimType -{ - HOLDING = 0, - IDLE = 1, // idling movement that happens from time to time - DEFEAT = 2, // played when army loses stack or on friendly fire - VICTORY = 3, // when enemy stack killed or huge damage is dealt - CAST_SPELL = 4 // spellcasting -}; - -enum class ECreatureAnimType -{ - INVALID = -1, - - MOVING = 0, - MOUSEON = 1, - HOLDING = 2, // base idling animation - HITTED = 3, // base animation for when stack is taking damage - DEFENCE = 4, // alternative animation for defending in melee if stack spent its action on defending - DEATH = 5, - DEATH_RANGED = 6, // Optional, alternative animation for when stack is killed by ranged attack - TURN_L = 7, - TURN_R = 8, - //TURN_L2 = 9, //unused - identical to TURN_L - //TURN_R2 = 10, //unused - identical to TURN_R - ATTACK_UP = 11, - ATTACK_FRONT = 12, - ATTACK_DOWN = 13, - SHOOT_UP = 14, // Shooters only - SHOOT_FRONT = 15, // Shooters only - SHOOT_DOWN = 16, // Shooters only - SPECIAL_UP = 17, // If empty, fallback to SPECIAL_FRONT - SPECIAL_FRONT = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability - SPECIAL_DOWN = 19, // If empty, fallback to SPECIAL_FRONT - MOVE_START = 20, // small animation to be played before MOVING - MOVE_END = 21, // small animation to be played after MOVING - - DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here - DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here - RESURRECTION = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here - FROZEN = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation - - CAST_UP = 30, - CAST_FRONT = 31, - CAST_DOWN = 32, - - GROUP_ATTACK_UP = 40, - GROUP_ATTACK_FRONT = 41, - GROUP_ATTACK_DOWN = 42 -}; +/* + * BattleConstants.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 + +enum class EBattleEffect +{ + // list of battle effects that have hardcoded triggers + MAGIC_MIRROR = 3, + FIRE_SHIELD = 11, + FEAR = 15, + GOOD_LUCK = 18, + GOOD_MORALE = 20, + BAD_MORALE = 30, + BAD_LUCK = 48, + RESURRECT = 50, + DRAIN_LIFE = 52, + POISON = 67, + DEATH_BLOW = 73, + REGENERATION = 74, + MANA_DRAIN = 77, + RESISTANCE = 78, + + INVALID = -1, +}; + +enum class EAnimationEvents +{ + // any action + ROTATE, // stacks rotate before action + + // movement action + MOVE_START, // stack starts movement + MOVEMENT, // movement animation loop starts + MOVE_END, // stack end movement + + // attack/spellcast action + BEFORE_HIT, // attack and defence effects play, e.g. luck/death blow + ATTACK, // attack and defence animations are playing + HIT, // hit & death animations are playing + AFTER_HIT, // post-attack effect, e.g. phoenix rebirth + + COUNT +}; + +enum class EHeroAnimType +{ + HOLDING = 0, + IDLE = 1, // idling movement that happens from time to time + DEFEAT = 2, // played when army loses stack or on friendly fire + VICTORY = 3, // when enemy stack killed or huge damage is dealt + CAST_SPELL = 4 // spellcasting +}; + +enum class ECreatureAnimType +{ + INVALID = -1, + + MOVING = 0, + MOUSEON = 1, + HOLDING = 2, // base idling animation + HITTED = 3, // base animation for when stack is taking damage + DEFENCE = 4, // alternative animation for defending in melee if stack spent its action on defending + DEATH = 5, + DEATH_RANGED = 6, // Optional, alternative animation for when stack is killed by ranged attack + TURN_L = 7, + TURN_R = 8, + //TURN_L2 = 9, //unused - identical to TURN_L + //TURN_R2 = 10, //unused - identical to TURN_R + ATTACK_UP = 11, + ATTACK_FRONT = 12, + ATTACK_DOWN = 13, + SHOOT_UP = 14, // Shooters only + SHOOT_FRONT = 15, // Shooters only + SHOOT_DOWN = 16, // Shooters only + SPECIAL_UP = 17, // If empty, fallback to SPECIAL_FRONT + SPECIAL_FRONT = 18, // Used for any special moves - dragon breath, spellcasting, Pit Lord/Ogre Mage ability + SPECIAL_DOWN = 19, // If empty, fallback to SPECIAL_FRONT + MOVE_START = 20, // small animation to be played before MOVING + MOVE_END = 21, // small animation to be played after MOVING + + DEAD = 22, // new group, used to show dead stacks. If empty - last frame from "DEATH" will be copied here + DEAD_RANGED = 23, // new group, used to show dead stacks (if DEATH_RANGED was used). If empty - last frame from "DEATH_RANGED" will be copied here + RESURRECTION = 24, // new group, used for animating resurrection, if empty - reversed "DEATH" animation will be copied here + FROZEN = 25, // new group, used when stack animation is paused (e.g. petrified). If empty - consist of first frame from HOLDING animation + + CAST_UP = 30, + CAST_FRONT = 31, + CAST_DOWN = 32, + + GROUP_ATTACK_UP = 40, + GROUP_ATTACK_FRONT = 41, + GROUP_ATTACK_DOWN = 42 +}; diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index f724a1777..74b219f5f 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -1,160 +1,160 @@ -/* - * BattleEffectsController.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 "BattleEffectsController.h" - -#include "BattleAnimationClasses.h" -#include "BattleWindow.h" -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#include "BattleRenderer.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../render/CAnimation.h" -#include "../render/Graphics.h" - -#include "../../CCallback.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/filesystem/ResourcePath.h" -#include "../../lib/NetPacks.h" -#include "../../lib/CStack.h" -#include "../../lib/IGameEventsReceiver.h" -#include "../../lib/CGeneralTextHandler.h" - -BattleEffectsController::BattleEffectsController(BattleInterface & owner): - owner(owner) -{ - loadColorMuxers(); -} - -void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) -{ - displayEffect(effect, AudioPath(), destTile); -} - -void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile) -{ - size_t effectID = static_cast(effect); - - AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]); - - CCS->soundh->playSound( soundFile ); - - owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); -} - -void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) -{ - owner.checkForAnimations(); - - const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID); - if(!stack) - { - logGlobal->error("Invalid stack ID %d", bte.stackID); - return; - } - //don't show animation when no HP is regenerated - switch(static_cast(bte.effect)) - { - case BonusType::HP_REGENERATION: - displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition()); - break; - case BonusType::MANA_DRAIN: - displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition()); - break; - case BonusType::POISON: - displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition()); - break; - case BonusType::FEAR: - displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition()); - break; - case BonusType::MORALE: - { - std::string hlp = CGI->generaltexth->allTexts[33]; - boost::algorithm::replace_first(hlp,"%s",(stack->getName())); - displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition()); - owner.appendBattleLog(hlp); - break; - } - default: - return; - } - owner.waitForAnimations(); -} - -void BattleEffectsController::startAction(const BattleAction & action) -{ - owner.checkForAnimations(); - - const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber); - - switch(action.actionType) - { - case EActionType::WAIT: - owner.appendBattleLog(stack->formatGeneralMessage(136)); - break; - case EActionType::BAD_MORALE: - owner.appendBattleLog(stack->formatGeneralMessage(-34)); - displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition()); - break; - } - - owner.waitForAnimations(); -} - -void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (auto & elem : battleEffects) - { - renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas) - { - int currentFrame = static_cast(floor(elem.currentFrame)); - currentFrame %= elem.animation->size(); - - auto img = elem.animation->getImage(currentFrame, static_cast(elem.type)); - - canvas.draw(img, elem.pos); - }); - } -} - -void BattleEffectsController::loadColorMuxers() -{ - const JsonNode config(JsonPath::builtin("config/battleEffects.json")); - - for(auto & muxer : config["colorMuxers"].Struct()) - { - ColorMuxerEffect effect; - std::string identifier = muxer.first; - - for (const JsonNode & entry : muxer.second.Vector() ) - { - effect.timePoints.push_back(entry["time"].Float()); - effect.filters.push_back(ColorFilter::genFromJson(entry)); - } - colorMuxerEffects[identifier] = effect; - } -} - -const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name) -{ - static const ColorMuxerEffect emptyEffect; - - if (colorMuxerEffects.count(name)) - return colorMuxerEffects[name]; - - logAnim->error("Failed to find color muxer effect named '%s'!", name); - return emptyEffect; -} +/* + * BattleEffectsController.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 "BattleEffectsController.h" + +#include "BattleAnimationClasses.h" +#include "BattleWindow.h" +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#include "BattleRenderer.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../render/CAnimation.h" +#include "../render/Graphics.h" + +#include "../../CCallback.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/NetPacks.h" +#include "../../lib/CStack.h" +#include "../../lib/IGameEventsReceiver.h" +#include "../../lib/CGeneralTextHandler.h" + +BattleEffectsController::BattleEffectsController(BattleInterface & owner): + owner(owner) +{ + loadColorMuxers(); +} + +void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHex & destTile) +{ + displayEffect(effect, AudioPath(), destTile); +} + +void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile) +{ + size_t effectID = static_cast(effect); + + AnimationPath customAnim = AnimationPath::builtinTODO(graphics->battleACToDef[effectID][0]); + + CCS->soundh->playSound( soundFile ); + + owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile)); +} + +void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte) +{ + owner.checkForAnimations(); + + const CStack * stack = owner.getBattle()->battleGetStackByID(bte.stackID); + if(!stack) + { + logGlobal->error("Invalid stack ID %d", bte.stackID); + return; + } + //don't show animation when no HP is regenerated + switch(static_cast(bte.effect)) + { + case BonusType::HP_REGENERATION: + displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition()); + break; + case BonusType::MANA_DRAIN: + displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition()); + break; + case BonusType::POISON: + displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition()); + break; + case BonusType::FEAR: + displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition()); + break; + case BonusType::MORALE: + { + std::string hlp = CGI->generaltexth->allTexts[33]; + boost::algorithm::replace_first(hlp,"%s",(stack->getName())); + displayEffect(EBattleEffect::GOOD_MORALE, AudioPath::builtin("GOODMRLE"), stack->getPosition()); + owner.appendBattleLog(hlp); + break; + } + default: + return; + } + owner.waitForAnimations(); +} + +void BattleEffectsController::startAction(const BattleAction & action) +{ + owner.checkForAnimations(); + + const CStack *stack = owner.getBattle()->battleGetStackByID(action.stackNumber); + + switch(action.actionType) + { + case EActionType::WAIT: + owner.appendBattleLog(stack->formatGeneralMessage(136)); + break; + case EActionType::BAD_MORALE: + owner.appendBattleLog(stack->formatGeneralMessage(-34)); + displayEffect(EBattleEffect::BAD_MORALE, AudioPath::builtin("BADMRLE"), stack->getPosition()); + break; + } + + owner.waitForAnimations(); +} + +void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (auto & elem : battleEffects) + { + renderer.insert( EBattleFieldLayer::EFFECTS, elem.tile, [&elem](BattleRenderer::RendererRef canvas) + { + int currentFrame = static_cast(floor(elem.currentFrame)); + currentFrame %= elem.animation->size(); + + auto img = elem.animation->getImage(currentFrame, static_cast(elem.type)); + + canvas.draw(img, elem.pos); + }); + } +} + +void BattleEffectsController::loadColorMuxers() +{ + const JsonNode config(JsonPath::builtin("config/battleEffects.json")); + + for(auto & muxer : config["colorMuxers"].Struct()) + { + ColorMuxerEffect effect; + std::string identifier = muxer.first; + + for (const JsonNode & entry : muxer.second.Vector() ) + { + effect.timePoints.push_back(entry["time"].Float()); + effect.filters.push_back(ColorFilter::genFromJson(entry)); + } + colorMuxerEffects[identifier] = effect; + } +} + +const ColorMuxerEffect & BattleEffectsController::getMuxerEffect(const std::string & name) +{ + static const ColorMuxerEffect emptyEffect; + + if (colorMuxerEffects.count(name)) + return colorMuxerEffects[name]; + + logAnim->error("Failed to find color muxer effect named '%s'!", name); + return emptyEffect; +} diff --git a/client/battle/BattleEffectsController.h b/client/battle/BattleEffectsController.h index 203a121fd..5e551901a 100644 --- a/client/battle/BattleEffectsController.h +++ b/client/battle/BattleEffectsController.h @@ -1,75 +1,75 @@ -/* - * BattleEffectsController.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/battle/BattleHex.h" -#include "../../lib/Point.h" -#include "../../lib/filesystem/ResourcePath.h" -#include "BattleConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BattleAction; -struct BattleTriggerEffect; - -VCMI_LIB_NAMESPACE_END - -struct ColorMuxerEffect; -class CAnimation; -class Canvas; -class BattleInterface; -class BattleRenderer; -class EffectAnimation; - -/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... -struct BattleEffect -{ - enum class AnimType : ui8 - { - DEFAULT = 0, //If we have such animation - REVERSE = 1 //Reverse DEFAULT will be used - }; - - AnimType type; - Point pos; //position on the screen - float currentFrame; - std::shared_ptr animation; - int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim - BattleHex tile; //Indicates if effect which hex the effect is drawn on -}; - -/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale -class BattleEffectsController -{ - BattleInterface & owner; - - /// list of current effects that are being displayed on screen (spells & creature abilities) - std::vector battleEffects; - - std::map colorMuxerEffects; - - void loadColorMuxers(); -public: - const ColorMuxerEffect &getMuxerEffect(const std::string & name); - - BattleEffectsController(BattleInterface & owner); - - void startAction(const BattleAction & action); - - //displays custom effect on the battlefield - void displayEffect(EBattleEffect effect, const BattleHex & destTile); - void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile); - - void battleTriggerEffect(const BattleTriggerEffect & bte); - - void collectRenderableObjects(BattleRenderer & renderer); - - friend class EffectAnimation; -}; +/* + * BattleEffectsController.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/battle/BattleHex.h" +#include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "BattleConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleAction; +struct BattleTriggerEffect; + +VCMI_LIB_NAMESPACE_END + +struct ColorMuxerEffect; +class CAnimation; +class Canvas; +class BattleInterface; +class BattleRenderer; +class EffectAnimation; + +/// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,... +struct BattleEffect +{ + enum class AnimType : ui8 + { + DEFAULT = 0, //If we have such animation + REVERSE = 1 //Reverse DEFAULT will be used + }; + + AnimType type; + Point pos; //position on the screen + float currentFrame; + std::shared_ptr animation; + int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim + BattleHex tile; //Indicates if effect which hex the effect is drawn on +}; + +/// Controls rendering of effects in battle, e.g. from spells, abilities and various other actions like morale +class BattleEffectsController +{ + BattleInterface & owner; + + /// list of current effects that are being displayed on screen (spells & creature abilities) + std::vector battleEffects; + + std::map colorMuxerEffects; + + void loadColorMuxers(); +public: + const ColorMuxerEffect &getMuxerEffect(const std::string & name); + + BattleEffectsController(BattleInterface & owner); + + void startAction(const BattleAction & action); + + //displays custom effect on the battlefield + void displayEffect(EBattleEffect effect, const BattleHex & destTile); + void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile); + + void battleTriggerEffect(const BattleTriggerEffect & bte); + + void collectRenderableObjects(BattleRenderer & renderer); + + friend class EffectAnimation; +}; diff --git a/client/battle/BattleFieldController.cpp b/client/battle/BattleFieldController.cpp index fb20c72bb..b0ee095b5 100644 --- a/client/battle/BattleFieldController.cpp +++ b/client/battle/BattleFieldController.cpp @@ -1,919 +1,919 @@ -/* - * BattleFieldController.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 "BattleFieldController.h" - -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleInterfaceClasses.h" -#include "BattleEffectsController.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "BattleObstacleController.h" -#include "BattleProjectileController.h" -#include "BattleRenderer.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" -#include "../render/IRenderHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../adventureMap/CInGameConsole.h" -#include "../client/render/CAnimation.h" - -#include "../../CCallback.h" -#include "../../lib/BattleFieldHandler.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CStack.h" -#include "../../lib/spells/ISpellMechanics.h" - -namespace HexMasks -{ - // mask definitions that has set to 1 the edges present in the hex edges highlight image - /* - /\ - 0 1 - / \ - | | - 5 2 - | | - \ / - 4 3 - \/ - */ - enum HexEdgeMasks { - empty = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed - topLeft = 0b000001, - topRight = 0b000010, - right = 0b000100, - bottomRight = 0b001000, - bottomLeft = 0b010000, - left = 0b100000, - - top = 0b000011, - bottom = 0b011000, - topRightHalfCorner = 0b000110, - bottomRightHalfCorner = 0b001100, - bottomLeftHalfCorner = 0b110000, - topLeftHalfCorner = 0b100001, - - rightTopAndBottom = 0b001010, // special case, right half can be drawn instead of only top and bottom - leftTopAndBottom = 0b010001, // special case, left half can be drawn instead of only top and bottom - - rightHalf = 0b001110, - leftHalf = 0b110001, - - topRightCorner = 0b000111, - bottomRightCorner = 0b011100, - bottomLeftCorner = 0b111000, - topLeftCorner = 0b100011 - }; -} - -std::map hexEdgeMaskToFrameIndex; - -// Maps HexEdgesMask to "Frame" indexes for range highligt images -void initializeHexEdgeMaskToFrameIndex() -{ - hexEdgeMaskToFrameIndex[HexMasks::empty] = 0; - - hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1; - hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2; - hexEdgeMaskToFrameIndex[HexMasks::right] = 3; - hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5; - hexEdgeMaskToFrameIndex[HexMasks::left] = 6; - - hexEdgeMaskToFrameIndex[HexMasks::top] = 7; - hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8; - - hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9; - hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11; - hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12; - - hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13; - hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14; - - hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13; - hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14; - - hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15; - hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16; - hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17; - hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18; -} - -BattleFieldController::BattleFieldController(BattleInterface & owner): - owner(owner) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - //preparing cells and hexes - cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); - cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP")); - cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); - - attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")); - attackCursors->preload(); - - spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")); - spellCursors->preload(); - - initializeHexEdgeMaskToFrameIndex(); - - rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); - rangedFullDamageLimitImages->preload(); - - shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); - shootingRangeLimitImages->preload(); - - flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); - flipRangeLimitImagesIntoPositions(shootingRangeLimitImages); - - if(!owner.siegeController) - { - auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); - - if(bfieldType == BattleField::NONE) - logGlobal->error("Invalid battlefield returned for current battle"); - else - background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); - } - else - { - auto backgroundName = owner.siegeController->getBattleBackgroundName(); - background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE); - } - - pos.w = background->width(); - pos.h = background->height(); - - backgroundWithHexes = std::make_unique(Point(background->width(), background->height())); - - updateAccessibleHexes(); - addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); -} - -void BattleFieldController::activate() -{ - LOCPLINT->cingconsole->pos = this->pos; - CIntObject::activate(); -} - -void BattleFieldController::createHeroes() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - // create heroes as part of our constructor for correct positioning inside battlefield - if(owner.attackingHeroInstance) - owner.attackingHero = std::make_shared(owner, owner.attackingHeroInstance, false); - - if(owner.defendingHeroInstance) - owner.defendingHero = std::make_shared(owner, owner.defendingHeroInstance, true); -} - -void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition) -{ - if (!on && pos.isInside(finalPosition)) - clickPressed(finalPosition); -} - -void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) -{ - Point distance = currentPosition - initialPosition; - - if (distance.length() < settings["battle"]["swipeAttackDistance"].Float()) - hoveredHex = getHexAtPosition(initialPosition); - else - hoveredHex = BattleHex::INVALID; - - currentAttackOriginPoint = currentPosition; - - if (pos.isInside(initialPosition)) - owner.actionsController->onHexHovered(getHoveredHex()); -} - -void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - hoveredHex = getHexAtPosition(cursorPosition); - currentAttackOriginPoint = cursorPosition; - - if (pos.isInside(cursorPosition)) - owner.actionsController->onHexHovered(getHoveredHex()); - else - owner.actionsController->onHoverEnded(); -} - -void BattleFieldController::clickPressed(const Point & cursorPosition) -{ - BattleHex selectedHex = getHoveredHex(); - - if (selectedHex != BattleHex::INVALID) - owner.actionsController->onHexLeftClicked(selectedHex); -} - -void BattleFieldController::showPopupWindow(const Point & cursorPosition) -{ - BattleHex selectedHex = getHoveredHex(); - - if (selectedHex != BattleHex::INVALID) - owner.actionsController->onHexRightClicked(selectedHex); -} - -void BattleFieldController::renderBattlefield(Canvas & canvas) -{ - Canvas clippedCanvas(canvas, pos); - - showBackground(clippedCanvas); - - BattleRenderer renderer(owner); - - renderer.execute(clippedCanvas); - - owner.projectilesController->render(clippedCanvas); -} - -void BattleFieldController::showBackground(Canvas & canvas) -{ - if (owner.stacksController->getActiveStack() != nullptr ) - showBackgroundImageWithHexes(canvas); - else - showBackgroundImage(canvas); - - showHighlightedHexes(canvas); -} - -void BattleFieldController::showBackgroundImage(Canvas & canvas) -{ - canvas.draw(background, Point(0, 0)); - - owner.obstacleController->showAbsoluteObstacles(canvas); - if ( owner.siegeController ) - owner.siegeController->showAbsoluteObstacles(canvas); - - if (settings["battle"]["cellBorders"].Bool()) - { - for (int i=0; igetActiveStack(); - std::vector attackableHexes; - if(activeStack) - occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); - - // prepare background graphic with hexes and shaded hexes - backgroundWithHexes->draw(background, Point(0,0)); - owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes); - if(owner.siegeController) - owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes); - - // show shaded hexes for active's stack valid movement and the hexes that it can attack - if(settings["battle"]["stackRange"].Bool()) - { - std::vector hexesToShade = occupiableHexes; - hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end()); - for(BattleHex hex : hexesToShade) - { - showHighlightedHex(*backgroundWithHexes, cellShade, hex, false); - } - } - - // draw cell borders - if(settings["battle"]["cellBorders"].Bool()) - { - for(int i=0; idraw(cellBorder, hexPositionLocal(i).topLeft()); - } - } -} - -void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr highlight, BattleHex hex, bool darkBorder) -{ - Point hexPos = hexPositionLocal(hex).topLeft(); - - canvas.draw(highlight, hexPos); - if(!darkBorder && settings["battle"]["cellBorders"].Bool()) - canvas.draw(cellBorder, hexPos); -} - -std::set BattleFieldController::getHighlightedHexesForActiveStack() -{ - std::set result; - - if(!owner.stacksController->getActiveStack()) - return result; - - if(!settings["battle"]["stackRange"].Bool()) - return result; - - auto hoveredHex = getHoveredHex(); - - std::set set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); - for(BattleHex hex : set) - result.insert(hex); - - return result; -} - -std::set BattleFieldController::getMovementRangeForHoveredStack() -{ - std::set result; - - if (!owner.stacksController->getActiveStack()) - return result; - - if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) - return result; - - auto hoveredHex = getHoveredHex(); - - // add possible movement hexes for stack under mouse - const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); - if(hoveredStack) - { - std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); - for(BattleHex hex : v) - result.insert(hex); - } - return result; -} - -std::set BattleFieldController::getHighlightedHexesForSpellRange() -{ - std::set result; - auto hoveredHex = getHoveredHex(); - - if(!settings["battle"]["mouseShadow"].Bool()) - return result; - - const spells::Caster *caster = nullptr; - const CSpell *spell = nullptr; - - spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(hoveredHex); - caster = owner.actionsController->getCurrentSpellcaster(); - - if(caster && spell) //when casting spell - { - // printing shaded hex(es) - spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); - auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex); - - for(BattleHex shadedHex : shadedHexes) - { - if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1)) - result.insert(shadedHex); - } - } - return result; -} - -std::set BattleFieldController::getHighlightedHexesForMovementTarget() -{ - const CStack * stack = owner.stacksController->getActiveStack(); - auto hoveredHex = getHoveredHex(); - - if(!stack) - return {}; - - std::vector availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr); - - auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); - if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex)) - { - if(isTileAttackable(hoveredHex)) - { - BattleHex attackFromHex = fromWhichHexAttack(hoveredHex); - - if(stack->doubleWide()) - return {attackFromHex, stack->occupiedHex(attackFromHex)}; - else - return {attackFromHex}; - } - } - - if(vstd::contains(availableHexes, hoveredHex)) - { - if(stack->doubleWide()) - return {hoveredHex, stack->occupiedHex(hoveredHex)}; - else - return {hoveredHex}; - } - - if(stack->doubleWide()) - { - for(auto const & hex : availableHexes) - { - if(stack->occupiedHex(hex) == hoveredHex) - return {hoveredHex, hex}; - } - } - - return {}; -} - -// Range limit highlight helpers - -std::vector BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance) -{ - std::vector rangeHexes; - - if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) - return rangeHexes; - - // get only battlefield hexes that are within the given distance - for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++) - { - BattleHex hex(i); - if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance) - rangeHexes.push_back(hex); - } - - return rangeHexes; -} - -std::vector BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector rangeHexes, uint8_t distanceToLimit) -{ - std::vector rangeLimitHexes; - - // from range hexes get only the ones at the limit - for(auto & hex : rangeHexes) - { - if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit) - rangeLimitHexes.push_back(hex); - } - - return rangeLimitHexes; -} - -bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit) -{ - bool hexInRangeLimit = false; - - if(!rangeLimitHexes.empty()) - { - auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex); - *hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos); - hexInRangeLimit = pos != rangeLimitHexes.end(); - } - - return hexInRangeLimit; -} - -std::vector> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector wholeRangeHexes, std::vector rangeLimitHexes) -{ - std::vector> output; - - if(wholeRangeHexes.empty()) - return output; - - for(auto & hex : rangeLimitHexes) - { - // get all neighbours and their directions - - auto neighbouringTiles = hex.allNeighbouringTiles(); - - std::vector outsideNeighbourDirections; - - // for each neighbour add to output only the valid ones and only that are not found in range Hexes - for(auto direction = 0; direction < 6; direction++) - { - if(!neighbouringTiles[direction].isAvailable()) - continue; - - auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]); - - if(it == wholeRangeHexes.end()) - outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction - } - - output.push_back(outsideNeighbourDirections); - } - - return output; -} - -std::vector> BattleFieldController::calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages) -{ - std::vector> output; // if no image is to be shown an empty image is still added to help with traverssing the range - - if(hexesNeighbourDirections.empty()) - return output; - - for(auto & directions : hexesNeighbourDirections) - { - std::bitset<6> mask; - - // convert directions to mask - for(auto direction : directions) - mask.set(direction); - - uint8_t imageKey = static_cast(mask.to_ulong()); - output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey])); - } - - return output; -} - -void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts) -{ - std::vector rangeHexes = getRangeHexes(hoveredHex, distance); - rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance); - std::vector> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes); - rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages); -} - -void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr images) -{ - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip(); - - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip(); - images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip(); -} - -void BattleFieldController::showHighlightedHexes(Canvas & canvas) -{ - std::vector rangedFullDamageLimitHexes; - std::vector shootingRangeLimitHexes; - - std::vector> rangedFullDamageLimitHexesHighligts; - std::vector> shootingRangeLimitHexesHighligts; - - std::set hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack(); - std::set hoveredSpellHexes = getHighlightedHexesForSpellRange(); - std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); - - BattleHex hoveredHex = getHoveredHex(); - if(hoveredHex == BattleHex::INVALID) - return; - - const CStack * hoveredStack = getHoveredStack(); - - // skip range limit calculations if unit hovered is not a shooter - if(hoveredStack && hoveredStack->isShooter()) - { - // calculate array with highlight images for ranged full damage limit - auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance(); - calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts); - - // calculate array with highlight images for shooting range limit - auto shootingRangeDistance = hoveredStack->getShootingRangeDistance(); - calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); - } - - auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; - - for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) - { - bool stackMovement = hoveredStackMovementRangeHexes.count(hex); - bool mouse = hoveredMouseHexes.count(hex); - - // calculate if hex is Ranged Full Damage Limit and its position in highlight array - int hexIndexInRangedFullDamageLimit = 0; - bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit); - - // calculate if hex is Shooting Range Limit and its position in highlight array - int hexIndexInShootingRangeLimit = 0; - bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit); - - if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well - { - showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); - showHighlightedHex(canvas, cellShade, hex, true); - } - if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded - { - showHighlightedHex(canvas, cellShade, hex, true); - } - if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight - { - showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); - } - if(hexInRangedFullDamageLimit) - { - showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false); - } - if(hexInShootingRangeLimit) - { - showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false); - } - } -} - -Rect BattleFieldController::hexPositionLocal(BattleHex hex) const -{ - int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX(); - int y = 86 + 42 *hex.getY(); - int w = cellShade->width(); - int h = cellShade->height(); - return Rect(x, y, w, h); -} - -Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const -{ - return hexPositionLocal(hex) + pos.topLeft(); -} - -bool BattleFieldController::isPixelInHex(Point const & position) -{ - return !cellShade->isTransparent(position); -} - -BattleHex BattleFieldController::getHoveredHex() -{ - return hoveredHex; -} - -const CStack* BattleFieldController::getHoveredStack() -{ - auto hoveredHex = getHoveredHex(); - const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); - - return hoveredStack; -} - -BattleHex BattleFieldController::getHexAtPosition(Point hoverPos) -{ - if (owner.attackingHero) - { - if (owner.attackingHero->pos.isInside(hoverPos)) - return BattleHex::HERO_ATTACKER; - } - - if (owner.defendingHero) - { - if (owner.attackingHero->pos.isInside(hoverPos)) - return BattleHex::HERO_DEFENDER; - } - - for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h) - { - Rect hexPosition = hexPositionAbsolute(h); - - if (!hexPosition.isInside(hoverPos)) - continue; - - if (isPixelInHex(hoverPos - hexPosition.topLeft())) - return h; - } - - return BattleHex::INVALID; -} - -BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber) -{ - const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); - auto neighbours = myNumber.allNeighbouringTiles(); - // 0 1 - // 5 x 2 - // 4 3 - - // if true - our current stack can move into this hex (and attack) - std::array attackAvailability; - - if (doubleWide) - { - // For double-hexes we need to ensure that both hexes needed for this direction are occupyable: - // | -0- | -1- | -2- | -3- | -4- | -5- | -6- | -7- - // | o o - | - o o | - - | - - | - - | - - | o o | - - - // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - - // | - - | - - | - - | - o o | o o - | - - | - - | o o - - for (size_t i : { 1, 2, 3}) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false)); - - for (size_t i : { 4, 5, 0}) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false)); - - attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]); - attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]); - } - else - { - for (size_t i = 0; i < 6; ++i) - attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]); - - attackAvailability[6] = false; - attackAvailability[7] = false; - } - - // Zero available tiles to attack from - if ( vstd::find(attackAvailability, true) == attackAvailability.end()) - { - logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber); - return BattleHex::NONE; - } - - // For each valid direction, select position to test against - std::array testPoint; - - for (size_t i = 0; i < 6; ++i) - if (attackAvailability[i]) - testPoint[i] = hexPositionAbsolute(neighbours[i]).center(); - - // For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them - if (attackAvailability[6]) - testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5); - - if (attackAvailability[7]) - testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0, 5); - - // Compute distance between tested position & cursor position and pick nearest - std::array distance2; - - for (size_t i = 0; i < 8; ++i) - if (attackAvailability[i]) - distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x); - - size_t nearest = -1; - for (size_t i = 0; i < 8; ++i) - if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) ) - nearest = i; - - assert(nearest != -1); - return BattleHex::EDir(nearest); -} - -BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget) -{ - BattleHex::EDir direction = selectAttackDirection(getHoveredHex()); - - const CStack * attacker = owner.stacksController->getActiveStack(); - - assert(direction != BattleHex::NONE); - assert(attacker); - - if (!attacker->doubleWide()) - { - assert(direction != BattleHex::BOTTOM); - assert(direction != BattleHex::TOP); - return attackTarget.cloneInDirection(direction); - } - else - { - // We need to find position of right hex of double-hex creature (or left for defending side) - // | TOP_LEFT |TOP_RIGHT | RIGHT |BOTTOM_RIGHT|BOTTOM_LEFT| LEFT | TOP |BOTTOM - // | o o - | - o o | - - | - - | - - | - - | o o | - - - // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - - // | - - | - - | - - | - o o | o o - | - - | - - | o o - - switch (direction) - { - case BattleHex::TOP_LEFT: - case BattleHex::LEFT: - case BattleHex::BOTTOM_LEFT: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(direction); - else - return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT); - } - - case BattleHex::TOP_RIGHT: - case BattleHex::RIGHT: - case BattleHex::BOTTOM_RIGHT: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT); - else - return attackTarget.cloneInDirection(direction); - } - - case BattleHex::TOP: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT); - else - return attackTarget.cloneInDirection(BattleHex::TOP_LEFT); - } - - case BattleHex::BOTTOM: - { - if ( attacker->unitSide() == BattleSide::ATTACKER ) - return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT); - else - return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT); - } - default: - assert(0); - return BattleHex::INVALID; - } - } -} - -bool BattleFieldController::isTileAttackable(const BattleHex & number) const -{ - for (auto & elem : occupiableHexes) - { - if (BattleHex::mutualPosition(elem, number) != -1 || elem == number) - return true; - } - return false; -} - -void BattleFieldController::updateAccessibleHexes() -{ - auto accessibility = owner.getBattle()->getAccesibility(); - - for(int i = 0; i < accessibility.size(); i++) - stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); -} - -bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const -{ - return stackCountOutsideHexes[number]; -} - -void BattleFieldController::showAll(Canvas & to) -{ - show(to); -} - -void BattleFieldController::tick(uint32_t msPassed) -{ - updateAccessibleHexes(); - owner.stacksController->tick(msPassed); - owner.obstacleController->tick(msPassed); - owner.projectilesController->tick(msPassed); -} - -void BattleFieldController::show(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - - renderBattlefield(to); - - if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID) - { - auto combatCursorIndex = CCS->curh->get(); - if (combatCursorIndex) - { - auto combatImageIndex = static_cast(*combatCursorIndex); - to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex)); - return; - } - - auto spellCursorIndex = CCS->curh->get(); - if (spellCursorIndex) - { - auto spellImageIndex = static_cast(*spellCursorIndex); - to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast()); - return; - } - - } -} - -bool BattleFieldController::receiveEvent(const Point & position, int eventType) const -{ - if (eventType == HOVER) - return true; - return CIntObject::receiveEvent(position, eventType); -} +/* + * BattleFieldController.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 "BattleFieldController.h" + +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleInterfaceClasses.h" +#include "BattleEffectsController.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "BattleObstacleController.h" +#include "BattleProjectileController.h" +#include "BattleRenderer.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../renderSDL/SDL_Extensions.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../adventureMap/CInGameConsole.h" +#include "../client/render/CAnimation.h" + +#include "../../CCallback.h" +#include "../../lib/BattleFieldHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CStack.h" +#include "../../lib/spells/ISpellMechanics.h" + +namespace HexMasks +{ + // mask definitions that has set to 1 the edges present in the hex edges highlight image + /* + /\ + 0 1 + / \ + | | + 5 2 + | | + \ / + 4 3 + \/ + */ + enum HexEdgeMasks { + empty = 0b000000, // empty used when wanting to keep indexes the same but no highlight should be displayed + topLeft = 0b000001, + topRight = 0b000010, + right = 0b000100, + bottomRight = 0b001000, + bottomLeft = 0b010000, + left = 0b100000, + + top = 0b000011, + bottom = 0b011000, + topRightHalfCorner = 0b000110, + bottomRightHalfCorner = 0b001100, + bottomLeftHalfCorner = 0b110000, + topLeftHalfCorner = 0b100001, + + rightTopAndBottom = 0b001010, // special case, right half can be drawn instead of only top and bottom + leftTopAndBottom = 0b010001, // special case, left half can be drawn instead of only top and bottom + + rightHalf = 0b001110, + leftHalf = 0b110001, + + topRightCorner = 0b000111, + bottomRightCorner = 0b011100, + bottomLeftCorner = 0b111000, + topLeftCorner = 0b100011 + }; +} + +std::map hexEdgeMaskToFrameIndex; + +// Maps HexEdgesMask to "Frame" indexes for range highligt images +void initializeHexEdgeMaskToFrameIndex() +{ + hexEdgeMaskToFrameIndex[HexMasks::empty] = 0; + + hexEdgeMaskToFrameIndex[HexMasks::topLeft] = 1; + hexEdgeMaskToFrameIndex[HexMasks::topRight] = 2; + hexEdgeMaskToFrameIndex[HexMasks::right] = 3; + hexEdgeMaskToFrameIndex[HexMasks::bottomRight] = 4; + hexEdgeMaskToFrameIndex[HexMasks::bottomLeft] = 5; + hexEdgeMaskToFrameIndex[HexMasks::left] = 6; + + hexEdgeMaskToFrameIndex[HexMasks::top] = 7; + hexEdgeMaskToFrameIndex[HexMasks::bottom] = 8; + + hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner] = 9; + hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner] = 10; + hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner] = 11; + hexEdgeMaskToFrameIndex[HexMasks::topLeftHalfCorner] = 12; + + hexEdgeMaskToFrameIndex[HexMasks::rightTopAndBottom] = 13; + hexEdgeMaskToFrameIndex[HexMasks::leftTopAndBottom] = 14; + + hexEdgeMaskToFrameIndex[HexMasks::rightHalf] = 13; + hexEdgeMaskToFrameIndex[HexMasks::leftHalf] = 14; + + hexEdgeMaskToFrameIndex[HexMasks::topRightCorner] = 15; + hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner] = 16; + hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner] = 17; + hexEdgeMaskToFrameIndex[HexMasks::topLeftCorner] = 18; +} + +BattleFieldController::BattleFieldController(BattleInterface & owner): + owner(owner) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + //preparing cells and hexes + cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY); + cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP")); + cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY); + cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY); + + attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")); + attackCursors->preload(); + + spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")); + spellCursors->preload(); + + initializeHexEdgeMaskToFrameIndex(); + + rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json")); + rangedFullDamageLimitImages->preload(); + + shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json")); + shootingRangeLimitImages->preload(); + + flipRangeLimitImagesIntoPositions(rangedFullDamageLimitImages); + flipRangeLimitImagesIntoPositions(shootingRangeLimitImages); + + if(!owner.siegeController) + { + auto bfieldType = owner.getBattle()->battleGetBattlefieldType(); + + if(bfieldType == BattleField::NONE) + logGlobal->error("Invalid battlefield returned for current battle"); + else + background = GH.renderHandler().loadImage(bfieldType.getInfo()->graphics, EImageBlitMode::OPAQUE); + } + else + { + auto backgroundName = owner.siegeController->getBattleBackgroundName(); + background = GH.renderHandler().loadImage(backgroundName, EImageBlitMode::OPAQUE); + } + + pos.w = background->width(); + pos.h = background->height(); + + backgroundWithHexes = std::make_unique(Point(background->width(), background->height())); + + updateAccessibleHexes(); + addUsedEvents(LCLICK | SHOW_POPUP | MOVE | TIME | GESTURE); +} + +void BattleFieldController::activate() +{ + LOCPLINT->cingconsole->pos = this->pos; + CIntObject::activate(); +} + +void BattleFieldController::createHeroes() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + // create heroes as part of our constructor for correct positioning inside battlefield + if(owner.attackingHeroInstance) + owner.attackingHero = std::make_shared(owner, owner.attackingHeroInstance, false); + + if(owner.defendingHeroInstance) + owner.defendingHero = std::make_shared(owner, owner.defendingHeroInstance, true); +} + +void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + if (!on && pos.isInside(finalPosition)) + clickPressed(finalPosition); +} + +void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + Point distance = currentPosition - initialPosition; + + if (distance.length() < settings["battle"]["swipeAttackDistance"].Float()) + hoveredHex = getHexAtPosition(initialPosition); + else + hoveredHex = BattleHex::INVALID; + + currentAttackOriginPoint = currentPosition; + + if (pos.isInside(initialPosition)) + owner.actionsController->onHexHovered(getHoveredHex()); +} + +void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + hoveredHex = getHexAtPosition(cursorPosition); + currentAttackOriginPoint = cursorPosition; + + if (pos.isInside(cursorPosition)) + owner.actionsController->onHexHovered(getHoveredHex()); + else + owner.actionsController->onHoverEnded(); +} + +void BattleFieldController::clickPressed(const Point & cursorPosition) +{ + BattleHex selectedHex = getHoveredHex(); + + if (selectedHex != BattleHex::INVALID) + owner.actionsController->onHexLeftClicked(selectedHex); +} + +void BattleFieldController::showPopupWindow(const Point & cursorPosition) +{ + BattleHex selectedHex = getHoveredHex(); + + if (selectedHex != BattleHex::INVALID) + owner.actionsController->onHexRightClicked(selectedHex); +} + +void BattleFieldController::renderBattlefield(Canvas & canvas) +{ + Canvas clippedCanvas(canvas, pos); + + showBackground(clippedCanvas); + + BattleRenderer renderer(owner); + + renderer.execute(clippedCanvas); + + owner.projectilesController->render(clippedCanvas); +} + +void BattleFieldController::showBackground(Canvas & canvas) +{ + if (owner.stacksController->getActiveStack() != nullptr ) + showBackgroundImageWithHexes(canvas); + else + showBackgroundImage(canvas); + + showHighlightedHexes(canvas); +} + +void BattleFieldController::showBackgroundImage(Canvas & canvas) +{ + canvas.draw(background, Point(0, 0)); + + owner.obstacleController->showAbsoluteObstacles(canvas); + if ( owner.siegeController ) + owner.siegeController->showAbsoluteObstacles(canvas); + + if (settings["battle"]["cellBorders"].Bool()) + { + for (int i=0; igetActiveStack(); + std::vector attackableHexes; + if(activeStack) + occupiableHexes = owner.getBattle()->battleGetAvailableHexes(activeStack, false, true, &attackableHexes); + + // prepare background graphic with hexes and shaded hexes + backgroundWithHexes->draw(background, Point(0,0)); + owner.obstacleController->showAbsoluteObstacles(*backgroundWithHexes); + if(owner.siegeController) + owner.siegeController->showAbsoluteObstacles(*backgroundWithHexes); + + // show shaded hexes for active's stack valid movement and the hexes that it can attack + if(settings["battle"]["stackRange"].Bool()) + { + std::vector hexesToShade = occupiableHexes; + hexesToShade.insert(hexesToShade.end(), attackableHexes.begin(), attackableHexes.end()); + for(BattleHex hex : hexesToShade) + { + showHighlightedHex(*backgroundWithHexes, cellShade, hex, false); + } + } + + // draw cell borders + if(settings["battle"]["cellBorders"].Bool()) + { + for(int i=0; idraw(cellBorder, hexPositionLocal(i).topLeft()); + } + } +} + +void BattleFieldController::showHighlightedHex(Canvas & canvas, std::shared_ptr highlight, BattleHex hex, bool darkBorder) +{ + Point hexPos = hexPositionLocal(hex).topLeft(); + + canvas.draw(highlight, hexPos); + if(!darkBorder && settings["battle"]["cellBorders"].Bool()) + canvas.draw(cellBorder, hexPos); +} + +std::set BattleFieldController::getHighlightedHexesForActiveStack() +{ + std::set result; + + if(!owner.stacksController->getActiveStack()) + return result; + + if(!settings["battle"]["stackRange"].Bool()) + return result; + + auto hoveredHex = getHoveredHex(); + + std::set set = owner.getBattle()->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex); + for(BattleHex hex : set) + result.insert(hex); + + return result; +} + +std::set BattleFieldController::getMovementRangeForHoveredStack() +{ + std::set result; + + if (!owner.stacksController->getActiveStack()) + return result; + + if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) + return result; + + auto hoveredHex = getHoveredHex(); + + // add possible movement hexes for stack under mouse + const CStack * const hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(hoveredStack) + { + std::vector v = owner.getBattle()->battleGetAvailableHexes(hoveredStack, true, true, nullptr); + for(BattleHex hex : v) + result.insert(hex); + } + return result; +} + +std::set BattleFieldController::getHighlightedHexesForSpellRange() +{ + std::set result; + auto hoveredHex = getHoveredHex(); + + if(!settings["battle"]["mouseShadow"].Bool()) + return result; + + const spells::Caster *caster = nullptr; + const CSpell *spell = nullptr; + + spells::Mode mode = owner.actionsController->getCurrentCastMode(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); + caster = owner.actionsController->getCurrentSpellcaster(); + + if(caster && spell) //when casting spell + { + // printing shaded hex(es) + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); + auto shadedHexes = spell->battleMechanics(&event)->rangeInHexes(hoveredHex); + + for(BattleHex shadedHex : shadedHexes) + { + if((shadedHex.getX() != 0) && (shadedHex.getX() != GameConstants::BFIELD_WIDTH - 1)) + result.insert(shadedHex); + } + } + return result; +} + +std::set BattleFieldController::getHighlightedHexesForMovementTarget() +{ + const CStack * stack = owner.stacksController->getActiveStack(); + auto hoveredHex = getHoveredHex(); + + if(!stack) + return {}; + + std::vector availableHexes = owner.getBattle()->battleGetAvailableHexes(stack, false, false, nullptr); + + auto hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + if(owner.getBattle()->battleCanAttack(stack, hoveredStack, hoveredHex)) + { + if(isTileAttackable(hoveredHex)) + { + BattleHex attackFromHex = fromWhichHexAttack(hoveredHex); + + if(stack->doubleWide()) + return {attackFromHex, stack->occupiedHex(attackFromHex)}; + else + return {attackFromHex}; + } + } + + if(vstd::contains(availableHexes, hoveredHex)) + { + if(stack->doubleWide()) + return {hoveredHex, stack->occupiedHex(hoveredHex)}; + else + return {hoveredHex}; + } + + if(stack->doubleWide()) + { + for(auto const & hex : availableHexes) + { + if(stack->occupiedHex(hex) == hoveredHex) + return {hoveredHex, hex}; + } + } + + return {}; +} + +// Range limit highlight helpers + +std::vector BattleFieldController::getRangeHexes(BattleHex sourceHex, uint8_t distance) +{ + std::vector rangeHexes; + + if (!settings["battle"]["rangeLimitHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown()) + return rangeHexes; + + // get only battlefield hexes that are within the given distance + for(auto i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + BattleHex hex(i); + if(hex.isAvailable() && BattleHex::getDistance(sourceHex, hex) <= distance) + rangeHexes.push_back(hex); + } + + return rangeHexes; +} + +std::vector BattleFieldController::getRangeLimitHexes(BattleHex hoveredHex, std::vector rangeHexes, uint8_t distanceToLimit) +{ + std::vector rangeLimitHexes; + + // from range hexes get only the ones at the limit + for(auto & hex : rangeHexes) + { + if(BattleHex::getDistance(hoveredHex, hex) == distanceToLimit) + rangeLimitHexes.push_back(hex); + } + + return rangeLimitHexes; +} + +bool BattleFieldController::IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit) +{ + bool hexInRangeLimit = false; + + if(!rangeLimitHexes.empty()) + { + auto pos = std::find(rangeLimitHexes.begin(), rangeLimitHexes.end(), hex); + *hexIndexInRangeLimit = std::distance(rangeLimitHexes.begin(), pos); + hexInRangeLimit = pos != rangeLimitHexes.end(); + } + + return hexInRangeLimit; +} + +std::vector> BattleFieldController::getOutsideNeighbourDirectionsForLimitHexes(std::vector wholeRangeHexes, std::vector rangeLimitHexes) +{ + std::vector> output; + + if(wholeRangeHexes.empty()) + return output; + + for(auto & hex : rangeLimitHexes) + { + // get all neighbours and their directions + + auto neighbouringTiles = hex.allNeighbouringTiles(); + + std::vector outsideNeighbourDirections; + + // for each neighbour add to output only the valid ones and only that are not found in range Hexes + for(auto direction = 0; direction < 6; direction++) + { + if(!neighbouringTiles[direction].isAvailable()) + continue; + + auto it = std::find(wholeRangeHexes.begin(), wholeRangeHexes.end(), neighbouringTiles[direction]); + + if(it == wholeRangeHexes.end()) + outsideNeighbourDirections.push_back(BattleHex::EDir(direction)); // push direction + } + + output.push_back(outsideNeighbourDirections); + } + + return output; +} + +std::vector> BattleFieldController::calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages) +{ + std::vector> output; // if no image is to be shown an empty image is still added to help with traverssing the range + + if(hexesNeighbourDirections.empty()) + return output; + + for(auto & directions : hexesNeighbourDirections) + { + std::bitset<6> mask; + + // convert directions to mask + for(auto direction : directions) + mask.set(direction); + + uint8_t imageKey = static_cast(mask.to_ulong()); + output.push_back(limitImages->getImage(hexEdgeMaskToFrameIndex[imageKey])); + } + + return output; +} + +void BattleFieldController::calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts) +{ + std::vector rangeHexes = getRangeHexes(hoveredHex, distance); + rangeLimitHexes = getRangeLimitHexes(hoveredHex, rangeHexes, distance); + std::vector> rangeLimitNeighbourDirections = getOutsideNeighbourDirectionsForLimitHexes(rangeHexes, rangeLimitHexes); + rangeLimitHexesHighligts = calculateRangeLimitHighlightImages(rangeLimitNeighbourDirections, rangeLimitImages); +} + +void BattleFieldController::flipRangeLimitImagesIntoPositions(std::shared_ptr images) +{ + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRight])->verticalFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::right])->verticalFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRight])->doubleFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeft])->horizontalFlip(); + + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottom])->horizontalFlip(); + + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightHalfCorner])->verticalFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightHalfCorner])->doubleFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftHalfCorner])->horizontalFlip(); + + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::rightHalf])->verticalFlip(); + + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::topRightCorner])->verticalFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomRightCorner])->doubleFlip(); + images->getImage(hexEdgeMaskToFrameIndex[HexMasks::bottomLeftCorner])->horizontalFlip(); +} + +void BattleFieldController::showHighlightedHexes(Canvas & canvas) +{ + std::vector rangedFullDamageLimitHexes; + std::vector shootingRangeLimitHexes; + + std::vector> rangedFullDamageLimitHexesHighligts; + std::vector> shootingRangeLimitHexesHighligts; + + std::set hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack(); + std::set hoveredSpellHexes = getHighlightedHexesForSpellRange(); + std::set hoveredMoveHexes = getHighlightedHexesForMovementTarget(); + + BattleHex hoveredHex = getHoveredHex(); + if(hoveredHex == BattleHex::INVALID) + return; + + const CStack * hoveredStack = getHoveredStack(); + + // skip range limit calculations if unit hovered is not a shooter + if(hoveredStack && hoveredStack->isShooter()) + { + // calculate array with highlight images for ranged full damage limit + auto rangedFullDamageDistance = hoveredStack->getRangedFullDamageDistance(); + calculateRangeLimitAndHighlightImages(rangedFullDamageDistance, rangedFullDamageLimitImages, rangedFullDamageLimitHexes, rangedFullDamageLimitHexesHighligts); + + // calculate array with highlight images for shooting range limit + auto shootingRangeDistance = hoveredStack->getShootingRangeDistance(); + calculateRangeLimitAndHighlightImages(shootingRangeDistance, shootingRangeLimitImages, shootingRangeLimitHexes, shootingRangeLimitHexesHighligts); + } + + auto const & hoveredMouseHexes = owner.actionsController->currentActionSpellcasting(getHoveredHex()) ? hoveredSpellHexes : hoveredMoveHexes; + + for(int hex = 0; hex < GameConstants::BFIELD_SIZE; ++hex) + { + bool stackMovement = hoveredStackMovementRangeHexes.count(hex); + bool mouse = hoveredMouseHexes.count(hex); + + // calculate if hex is Ranged Full Damage Limit and its position in highlight array + int hexIndexInRangedFullDamageLimit = 0; + bool hexInRangedFullDamageLimit = IsHexInRangeLimit(hex, rangedFullDamageLimitHexes, &hexIndexInRangedFullDamageLimit); + + // calculate if hex is Shooting Range Limit and its position in highlight array + int hexIndexInShootingRangeLimit = 0; + bool hexInShootingRangeLimit = IsHexInRangeLimit(hex, shootingRangeLimitHexes, &hexIndexInShootingRangeLimit); + + if(stackMovement && mouse) // area where hovered stackMovement can move shown with highlight. Because also affected by mouse cursor, shade as well + { + showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); + showHighlightedHex(canvas, cellShade, hex, true); + } + if(!stackMovement && mouse) // hexes affected only at mouse cursor shown as shaded + { + showHighlightedHex(canvas, cellShade, hex, true); + } + if(stackMovement && !mouse) // hexes where hovered stackMovement can move shown with highlight + { + showHighlightedHex(canvas, cellUnitMovementHighlight, hex, false); + } + if(hexInRangedFullDamageLimit) + { + showHighlightedHex(canvas, rangedFullDamageLimitHexesHighligts[hexIndexInRangedFullDamageLimit], hex, false); + } + if(hexInShootingRangeLimit) + { + showHighlightedHex(canvas, shootingRangeLimitHexesHighligts[hexIndexInShootingRangeLimit], hex, false); + } + } +} + +Rect BattleFieldController::hexPositionLocal(BattleHex hex) const +{ + int x = 14 + ((hex.getY())%2==0 ? 22 : 0) + 44*hex.getX(); + int y = 86 + 42 *hex.getY(); + int w = cellShade->width(); + int h = cellShade->height(); + return Rect(x, y, w, h); +} + +Rect BattleFieldController::hexPositionAbsolute(BattleHex hex) const +{ + return hexPositionLocal(hex) + pos.topLeft(); +} + +bool BattleFieldController::isPixelInHex(Point const & position) +{ + return !cellShade->isTransparent(position); +} + +BattleHex BattleFieldController::getHoveredHex() +{ + return hoveredHex; +} + +const CStack* BattleFieldController::getHoveredStack() +{ + auto hoveredHex = getHoveredHex(); + const CStack* hoveredStack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + + return hoveredStack; +} + +BattleHex BattleFieldController::getHexAtPosition(Point hoverPos) +{ + if (owner.attackingHero) + { + if (owner.attackingHero->pos.isInside(hoverPos)) + return BattleHex::HERO_ATTACKER; + } + + if (owner.defendingHero) + { + if (owner.attackingHero->pos.isInside(hoverPos)) + return BattleHex::HERO_DEFENDER; + } + + for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h) + { + Rect hexPosition = hexPositionAbsolute(h); + + if (!hexPosition.isInside(hoverPos)) + continue; + + if (isPixelInHex(hoverPos - hexPosition.topLeft())) + return h; + } + + return BattleHex::INVALID; +} + +BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber) +{ + const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide(); + auto neighbours = myNumber.allNeighbouringTiles(); + // 0 1 + // 5 x 2 + // 4 3 + + // if true - our current stack can move into this hex (and attack) + std::array attackAvailability; + + if (doubleWide) + { + // For double-hexes we need to ensure that both hexes needed for this direction are occupyable: + // | -0- | -1- | -2- | -3- | -4- | -5- | -6- | -7- + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + for (size_t i : { 1, 2, 3}) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::RIGHT, false)); + + for (size_t i : { 4, 5, 0}) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]) && vstd::contains(occupiableHexes, neighbours[i].cloneInDirection(BattleHex::LEFT, false)); + + attackAvailability[6] = vstd::contains(occupiableHexes, neighbours[0]) && vstd::contains(occupiableHexes, neighbours[1]); + attackAvailability[7] = vstd::contains(occupiableHexes, neighbours[3]) && vstd::contains(occupiableHexes, neighbours[4]); + } + else + { + for (size_t i = 0; i < 6; ++i) + attackAvailability[i] = vstd::contains(occupiableHexes, neighbours[i]); + + attackAvailability[6] = false; + attackAvailability[7] = false; + } + + // Zero available tiles to attack from + if ( vstd::find(attackAvailability, true) == attackAvailability.end()) + { + logGlobal->error("Error: cannot find a hex to attack hex %d from!", myNumber); + return BattleHex::NONE; + } + + // For each valid direction, select position to test against + std::array testPoint; + + for (size_t i = 0; i < 6; ++i) + if (attackAvailability[i]) + testPoint[i] = hexPositionAbsolute(neighbours[i]).center(); + + // For bottom/top directions select central point, but move it a bit away from true center to reduce zones allocated to them + if (attackAvailability[6]) + testPoint[6] = (hexPositionAbsolute(neighbours[0]).center() + hexPositionAbsolute(neighbours[1]).center()) / 2 + Point(0, -5); + + if (attackAvailability[7]) + testPoint[7] = (hexPositionAbsolute(neighbours[3]).center() + hexPositionAbsolute(neighbours[4]).center()) / 2 + Point(0, 5); + + // Compute distance between tested position & cursor position and pick nearest + std::array distance2; + + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i]) + distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x); + + size_t nearest = -1; + for (size_t i = 0; i < 8; ++i) + if (attackAvailability[i] && (nearest == -1 || distance2[i] < distance2[nearest]) ) + nearest = i; + + assert(nearest != -1); + return BattleHex::EDir(nearest); +} + +BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget) +{ + BattleHex::EDir direction = selectAttackDirection(getHoveredHex()); + + const CStack * attacker = owner.stacksController->getActiveStack(); + + assert(direction != BattleHex::NONE); + assert(attacker); + + if (!attacker->doubleWide()) + { + assert(direction != BattleHex::BOTTOM); + assert(direction != BattleHex::TOP); + return attackTarget.cloneInDirection(direction); + } + else + { + // We need to find position of right hex of double-hex creature (or left for defending side) + // | TOP_LEFT |TOP_RIGHT | RIGHT |BOTTOM_RIGHT|BOTTOM_LEFT| LEFT | TOP |BOTTOM + // | o o - | - o o | - - | - - | - - | - - | o o | - - + // | - x - | - x - | - x o o| - x - | - x - |o o x - | - x - | - x - + // | - - | - - | - - | - o o | o o - | - - | - - | o o + + switch (direction) + { + case BattleHex::TOP_LEFT: + case BattleHex::LEFT: + case BattleHex::BOTTOM_LEFT: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction); + else + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT); + } + + case BattleHex::TOP_RIGHT: + case BattleHex::RIGHT: + case BattleHex::BOTTOM_RIGHT: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT); + else + return attackTarget.cloneInDirection(direction); + } + + case BattleHex::TOP: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::TOP_LEFT); + } + + case BattleHex::BOTTOM: + { + if ( attacker->unitSide() == BattleSide::ATTACKER ) + return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT); + else + return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT); + } + default: + assert(0); + return BattleHex::INVALID; + } + } +} + +bool BattleFieldController::isTileAttackable(const BattleHex & number) const +{ + for (auto & elem : occupiableHexes) + { + if (BattleHex::mutualPosition(elem, number) != -1 || elem == number) + return true; + } + return false; +} + +void BattleFieldController::updateAccessibleHexes() +{ + auto accessibility = owner.getBattle()->getAccesibility(); + + for(int i = 0; i < accessibility.size(); i++) + stackCountOutsideHexes[i] = (accessibility[i] == EAccessibility::ACCESSIBLE || (accessibility[i] == EAccessibility::SIDE_COLUMN)); +} + +bool BattleFieldController::stackCountOutsideHex(const BattleHex & number) const +{ + return stackCountOutsideHexes[number]; +} + +void BattleFieldController::showAll(Canvas & to) +{ + show(to); +} + +void BattleFieldController::tick(uint32_t msPassed) +{ + updateAccessibleHexes(); + owner.stacksController->tick(msPassed); + owner.obstacleController->tick(msPassed); + owner.projectilesController->tick(msPassed); +} + +void BattleFieldController::show(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + + renderBattlefield(to); + + if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID) + { + auto combatCursorIndex = CCS->curh->get(); + if (combatCursorIndex) + { + auto combatImageIndex = static_cast(*combatCursorIndex); + to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex)); + return; + } + + auto spellCursorIndex = CCS->curh->get(); + if (spellCursorIndex) + { + auto spellImageIndex = static_cast(*spellCursorIndex); + to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast()); + return; + } + + } +} + +bool BattleFieldController::receiveEvent(const Point & position, int eventType) const +{ + if (eventType == HOVER) + return true; + return CIntObject::receiveEvent(position, eventType); +} diff --git a/client/battle/BattleFieldController.h b/client/battle/BattleFieldController.h index 088b1ef7d..4a713a5de 100644 --- a/client/battle/BattleFieldController.h +++ b/client/battle/BattleFieldController.h @@ -1,144 +1,144 @@ -/* - * BattleFieldController.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/battle/BattleHex.h" -#include "../../lib/Point.h" -#include "../gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN -class CStack; -class Rect; -VCMI_LIB_NAMESPACE_END - -class BattleHero; -class CAnimation; -class Canvas; -class IImage; -class BattleInterface; - -/// Handles battlefield grid as well as rendering of background layer of battle interface -class BattleFieldController : public CIntObject -{ - BattleInterface & owner; - - std::shared_ptr background; - std::shared_ptr cellBorder; - std::shared_ptr cellUnitMovementHighlight; - std::shared_ptr cellUnitMaxMovementHighlight; - std::shared_ptr cellShade; - std::shared_ptr rangedFullDamageLimitImages; - std::shared_ptr shootingRangeLimitImages; - - std::shared_ptr attackCursors; - std::shared_ptr spellCursors; - - /// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack - std::unique_ptr backgroundWithHexes; - - /// direction which will be used to perform attack with current cursor position - Point currentAttackOriginPoint; - - /// hex currently under mouse hover - BattleHex hoveredHex; - - /// hexes to which currently active stack can move - std::vector occupiableHexes; - - /// hexes that when in front of a unit cause it's amount box to move back - std::array stackCountOutsideHexes; - - void showHighlightedHex(Canvas & to, std::shared_ptr highlight, BattleHex hex, bool darkBorder); - - std::set getHighlightedHexesForActiveStack(); - std::set getMovementRangeForHoveredStack(); - std::set getHighlightedHexesForSpellRange(); - std::set getHighlightedHexesForMovementTarget(); - - // Range limit highlight helpers - - /// get all hexes within a certain distance of given hex - std::vector getRangeHexes(BattleHex sourceHex, uint8_t distance); - - /// get only hexes at the limit of a range - std::vector getRangeLimitHexes(BattleHex hoveredHex, std::vector hexRange, uint8_t distanceToLimit); - - /// calculate if a hex is in range limit and return its index in range - bool IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit); - - /// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists - std::vector> getOutsideNeighbourDirectionsForLimitHexes(std::vector rangeHexes, std::vector rangeLimitHexes); - - /// calculates what image to use as range limit, depending on the direction of neighbors - /// a mask is used internally to mark the directions of all neighbours - /// based on this mask the corresponding image is selected - std::vector> calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages); - - /// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes - void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts); - - /// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones - void flipRangeLimitImagesIntoPositions(std::shared_ptr images); - - void showBackground(Canvas & canvas); - void showBackgroundImage(Canvas & canvas); - void showBackgroundImageWithHexes(Canvas & canvas); - void showHighlightedHexes(Canvas & canvas); - void updateAccessibleHexes(); - - BattleHex getHexAtPosition(Point hoverPosition); - - /// Checks whether selected pixel is transparent, uses local coordinates of a hex - bool isPixelInHex(Point const & position); - size_t selectBattleCursor(BattleHex myNumber); - - void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; - void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; - void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void activate() override; - - void showAll(Canvas & to) override; - void show(Canvas & to) override; - void tick(uint32_t msPassed) override; - - bool receiveEvent(const Point & position, int eventType) const override; - -public: - BattleFieldController(BattleInterface & owner); - - void createHeroes(); - - void redrawBackgroundWithHexes(); - void renderBattlefield(Canvas & canvas); - - /// Returns position of hex relative to owner (BattleInterface) - Rect hexPositionLocal(BattleHex hex) const; - - /// Returns position of hex relative to game window - Rect hexPositionAbsolute(BattleHex hex) const; - - /// Returns ID of currently hovered hex or BattleHex::INVALID if none - BattleHex getHoveredHex(); - - /// Returns the currently hovered stack - const CStack* getHoveredStack(); - - /// returns true if selected tile can be attacked in melee by current stack - bool isTileAttackable(const BattleHex & number) const; - - /// returns true if stack should render its stack count image in default position - outside own hex - bool stackCountOutsideHex(const BattleHex & number) const; - - BattleHex::EDir selectAttackDirection(BattleHex myNumber); - - BattleHex fromWhichHexAttack(BattleHex myNumber); -}; +/* + * BattleFieldController.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/battle/BattleHex.h" +#include "../../lib/Point.h" +#include "../gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CStack; +class Rect; +VCMI_LIB_NAMESPACE_END + +class BattleHero; +class CAnimation; +class Canvas; +class IImage; +class BattleInterface; + +/// Handles battlefield grid as well as rendering of background layer of battle interface +class BattleFieldController : public CIntObject +{ + BattleInterface & owner; + + std::shared_ptr background; + std::shared_ptr cellBorder; + std::shared_ptr cellUnitMovementHighlight; + std::shared_ptr cellUnitMaxMovementHighlight; + std::shared_ptr cellShade; + std::shared_ptr rangedFullDamageLimitImages; + std::shared_ptr shootingRangeLimitImages; + + std::shared_ptr attackCursors; + std::shared_ptr spellCursors; + + /// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack + std::unique_ptr backgroundWithHexes; + + /// direction which will be used to perform attack with current cursor position + Point currentAttackOriginPoint; + + /// hex currently under mouse hover + BattleHex hoveredHex; + + /// hexes to which currently active stack can move + std::vector occupiableHexes; + + /// hexes that when in front of a unit cause it's amount box to move back + std::array stackCountOutsideHexes; + + void showHighlightedHex(Canvas & to, std::shared_ptr highlight, BattleHex hex, bool darkBorder); + + std::set getHighlightedHexesForActiveStack(); + std::set getMovementRangeForHoveredStack(); + std::set getHighlightedHexesForSpellRange(); + std::set getHighlightedHexesForMovementTarget(); + + // Range limit highlight helpers + + /// get all hexes within a certain distance of given hex + std::vector getRangeHexes(BattleHex sourceHex, uint8_t distance); + + /// get only hexes at the limit of a range + std::vector getRangeLimitHexes(BattleHex hoveredHex, std::vector hexRange, uint8_t distanceToLimit); + + /// calculate if a hex is in range limit and return its index in range + bool IsHexInRangeLimit(BattleHex hex, std::vector & rangeLimitHexes, int * hexIndexInRangeLimit); + + /// get an array that has for each hex in range, an aray with all directions where an ouside neighbour hex exists + std::vector> getOutsideNeighbourDirectionsForLimitHexes(std::vector rangeHexes, std::vector rangeLimitHexes); + + /// calculates what image to use as range limit, depending on the direction of neighbors + /// a mask is used internally to mark the directions of all neighbours + /// based on this mask the corresponding image is selected + std::vector> calculateRangeLimitHighlightImages(std::vector> hexesNeighbourDirections, std::shared_ptr limitImages); + + /// calculates all hexes for a range limit and what images to be shown as highlight for each of the hexes + void calculateRangeLimitAndHighlightImages(uint8_t distance, std::shared_ptr rangeLimitImages, std::vector & rangeLimitHexes, std::vector> & rangeLimitHexesHighligts); + + /// to reduce the number of source images used, some images will be used as flipped versions of preloaded ones + void flipRangeLimitImagesIntoPositions(std::shared_ptr images); + + void showBackground(Canvas & canvas); + void showBackgroundImage(Canvas & canvas); + void showBackgroundImageWithHexes(Canvas & canvas); + void showHighlightedHexes(Canvas & canvas); + void updateAccessibleHexes(); + + BattleHex getHexAtPosition(Point hoverPosition); + + /// Checks whether selected pixel is transparent, uses local coordinates of a hex + bool isPixelInHex(Point const & position); + size_t selectBattleCursor(BattleHex myNumber); + + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void activate() override; + + void showAll(Canvas & to) override; + void show(Canvas & to) override; + void tick(uint32_t msPassed) override; + + bool receiveEvent(const Point & position, int eventType) const override; + +public: + BattleFieldController(BattleInterface & owner); + + void createHeroes(); + + void redrawBackgroundWithHexes(); + void renderBattlefield(Canvas & canvas); + + /// Returns position of hex relative to owner (BattleInterface) + Rect hexPositionLocal(BattleHex hex) const; + + /// Returns position of hex relative to game window + Rect hexPositionAbsolute(BattleHex hex) const; + + /// Returns ID of currently hovered hex or BattleHex::INVALID if none + BattleHex getHoveredHex(); + + /// Returns the currently hovered stack + const CStack* getHoveredStack(); + + /// returns true if selected tile can be attacked in melee by current stack + bool isTileAttackable(const BattleHex & number) const; + + /// returns true if stack should render its stack count image in default position - outside own hex + bool stackCountOutsideHex(const BattleHex & number) const; + + BattleHex::EDir selectAttackDirection(BattleHex myNumber); + + BattleHex fromWhichHexAttack(BattleHex myNumber); +}; diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index b0e383a66..9542c2245 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -1,828 +1,828 @@ -/* - * BattleInterface.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 "BattleInterface.h" - -#include "BattleAnimationClasses.h" -#include "BattleActionsController.h" -#include "BattleInterfaceClasses.h" -#include "CreatureAnimation.h" -#include "BattleProjectileController.h" -#include "BattleEffectsController.h" -#include "BattleObstacleController.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleWindow.h" -#include "BattleStacksController.h" -#include "BattleRenderer.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" -#include "../render/Canvas.h" -#include "../adventureMap/AdventureMapInterface.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/CondSh.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/NetPacks.h" -#include "../../lib/UnlockGuard.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/CThreadHelper.h" - -BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, - const CGHeroInstance *hero1, const CGHeroInstance *hero2, - std::shared_ptr att, - std::shared_ptr defen, - std::shared_ptr spectatorInt) - : attackingHeroInstance(hero1) - , defendingHeroInstance(hero2) - , attackerInt(att) - , defenderInt(defen) - , curInt(att) - , battleID(battleID) - , battleOpeningDelayActive(true) -{ - if(spectatorInt) - { - curInt = spectatorInt; - } - else if(!curInt) - { - //May happen when we are defending during network MP game -> attacker interface is just not present - curInt = defenderInt; - } - - //hot-seat -> check tactics for both players (defender may be local human) - if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist()) - tacticianInterface = attackerInt; - else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist()) - tacticianInterface = defenderInt; - - //if we found interface of player with tactics, then enter tactics mode - tacticsMode = static_cast(tacticianInterface); - - //initializing armies - this->army1 = army1; - this->army2 = army2; - - const CGTownInstance *town = getBattle()->battleGetDefendedTown(); - if(town && town->hasFort()) - siegeController.reset(new BattleSiegeController(*this, town)); - - windowObject = std::make_shared(*this); - projectilesController.reset(new BattleProjectileController(*this)); - stacksController.reset( new BattleStacksController(*this)); - actionsController.reset( new BattleActionsController(*this)); - effectsController.reset(new BattleEffectsController(*this)); - obstacleController.reset(new BattleObstacleController(*this)); - - adventureInt->onAudioPaused(); - ongoingAnimationsState.set(true); - - GH.windows().pushWindow(windowObject); - windowObject->blockUI(true); - windowObject->updateQueue(); - - playIntroSoundAndUnlockInterface(); -} - -void BattleInterface::playIntroSoundAndUnlockInterface() -{ - auto onIntroPlayed = [this]() - { - if(LOCPLINT->battleInt) - onIntroSoundPlayed(); - }; - - int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); - if (battleIntroSoundChannel != -1) - { - CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); - - if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool()) - openingEnd(); - } - else // failed to play sound - { - onIntroSoundPlayed(); - } -} - -bool BattleInterface::openingPlaying() -{ - return battleOpeningDelayActive; -} - -void BattleInterface::onIntroSoundPlayed() -{ - if (openingPlaying()) - openingEnd(); - - CCS->musich->playMusicFromSet("battle", true, true); -} - -void BattleInterface::openingEnd() -{ - assert(openingPlaying()); - if (!openingPlaying()) - return; - - onAnimationsFinished(); - if(tacticsMode) - tacticNextStack(nullptr); - activateStack(); - battleOpeningDelayActive = false; -} - -BattleInterface::~BattleInterface() -{ - CPlayerInterface::battleInt = nullptr; - - if (adventureInt) - adventureInt->onAudioResumed(); - - awaitingEvents.clear(); - onAnimationsFinished(); -} - -void BattleInterface::redrawBattlefield() -{ - fieldController->redrawBackgroundWithHexes(); - GH.windows().totalRedraw(); -} - -void BattleInterface::stackReset(const CStack * stack) -{ - stacksController->stackReset(stack); -} - -void BattleInterface::stackAdded(const CStack * stack) -{ - stacksController->stackAdded(stack, false); -} - -void BattleInterface::stackRemoved(uint32_t stackID) -{ - stacksController->stackRemoved(stackID); - fieldController->redrawBackgroundWithHexes(); - windowObject->updateQueue(); -} - -void BattleInterface::stackActivated(const CStack *stack) -{ - stacksController->stackActivated(stack); -} - -void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport) -{ - if (teleport) - stacksController->stackTeleported(stack, destHex, distance); - else - stacksController->stackMoved(stack, destHex, distance); -} - -void BattleInterface::stacksAreAttacked(std::vector attackedInfos) -{ - stacksController->stacksAreAttacked(attackedInfos); - - std::array killedBySide = {0, 0}; - - for(const StackAttackedInfo & attackedInfo : attackedInfos) - { - ui8 side = attackedInfo.defender->unitSide(); - killedBySide.at(side) += attackedInfo.amountKilled; - } - - for(ui8 side = 0; side < 2; side++) - { - if(killedBySide.at(side) > killedBySide.at(1-side)) - setHeroAnimation(side, EHeroAnimType::DEFEAT); - else if(killedBySide.at(side) < killedBySide.at(1-side)) - setHeroAnimation(side, EHeroAnimType::VICTORY); - } -} - -void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) -{ - stacksController->stackAttacking(attackInfo); -} - -void BattleInterface::newRoundFirst() -{ - waitForAnimations(); -} - -void BattleInterface::newRound() -{ - console->addText(CGI->generaltexth->allTexts[412]); -} - -void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) -{ - const CStack * actor = nullptr; - if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) - { - actor = stacksController->getActiveStack(); - } - - auto side = getBattle()->playerToSide(curInt->playerID); - if(!side) - { - logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); - return; - } - - BattleAction ba; - ba.side = side.value(); - ba.actionType = action; - ba.aimToHex(tile); - ba.spell = spell; - - sendCommand(ba, actor); -} - -void BattleInterface::sendCommand(BattleAction command, const CStack * actor) -{ - command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2); - - if(!tacticsMode) - { - logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); - stacksController->setActiveStack(nullptr); - curInt->cb->battleMakeUnitAction(battleID, command); - } - else - { - curInt->cb->battleMakeTacticAction(battleID, command); - stacksController->setActiveStack(nullptr); - //next stack will be activated when action ends - } - CCS->curh->set(Cursor::Combat::POINTER); -} - -const CGHeroInstance * BattleInterface::getActiveHero() -{ - const CStack *attacker = stacksController->getActiveStack(); - if(!attacker) - { - return nullptr; - } - - if(attacker->unitSide() == BattleSide::ATTACKER) - { - return attackingHeroInstance; - } - - return defendingHeroInstance; -} - -void BattleInterface::stackIsCatapulting(const CatapultAttack & ca) -{ - if (siegeController) - siegeController->stackIsCatapulting(ca); -} - -void BattleInterface::gateStateChanged(const EGateState state) -{ - if (siegeController) - siegeController->gateStateChanged(state); -} - -void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) -{ - checkForAnimations(); - stacksController->setActiveStack(nullptr); - - CCS->curh->set(Cursor::Map::POINTER); - curInt->waitWhileDialog(); - - if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) - { - curInt->cb->selectionMade(0, queryID); - windowObject->close(); - return; - } - - auto wnd = std::make_shared(br, *(this->curInt)); - wnd->resultCallback = [=](ui32 selection) - { - curInt->cb->selectionMade(selection, queryID); - }; - GH.windows().pushWindow(wnd); - - curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 - CPlayerInterface::battleInt = nullptr; -} - -void BattleInterface::spellCast(const BattleSpellCast * sc) -{ - // Do not deactivate anything in tactics mode - // This is battlefield setup spells - if(!tacticsMode) - { - windowObject->blockUI(true); - - // Disable current active stack duing the cast - // Store the current activeStack to stackToActivate - stacksController->deactivateStack(); - } - - CCS->curh->set(Cursor::Combat::BLOCKED); - - const SpellID spellID = sc->spellID; - const CSpell * spell = spellID.toSpell(); - auto targetedTile = sc->tile; - - assert(spell); - if(!spell) - return; - - const AudioPath & castSoundPath = spell->getCastSound(); - - if (!castSoundPath.empty()) - { - auto group = spell->animationInfo.projectile.empty() ? - EAnimationEvents::HIT: - EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning - - addToAnimationStage(group, [=]() { - CCS->soundh->playSound(castSoundPath); - }); - } - - if ( sc->activeCast ) - { - const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack); - - if(casterStack != nullptr ) - { - addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() - { - stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); - displaySpellCast(spell, casterStack->getPosition()); - }); - } - else - { - auto hero = sc->side ? defendingHero : attackingHero; - assert(hero); - - addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() - { - stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); - }); - } - } - - addToAnimationStage(EAnimationEvents::HIT, [=](){ - displaySpellHit(spell, targetedTile); - }); - - //queuing affect animation - for(auto & elem : sc->affectedCres) - { - auto stack = getBattle()->battleGetStackByID(elem, false); - assert(stack); - if(stack) - { - addToAnimationStage(EAnimationEvents::HIT, [=](){ - displaySpellEffect(spell, stack->getPosition()); - }); - } - } - - for(auto & elem : sc->reflectedCres) - { - auto stack = getBattle()->battleGetStackByID(elem, false); - assert(stack); - addToAnimationStage(EAnimationEvents::HIT, [=](){ - effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); - }); - } - - if (!sc->resistedCres.empty()) - { - addToAnimationStage(EAnimationEvents::HIT, [=](){ - CCS->soundh->playSound(AudioPath::builtin("MAGICRES")); - }); - } - - for(auto & elem : sc->resistedCres) - { - auto stack = getBattle()->battleGetStackByID(elem, false); - assert(stack); - addToAnimationStage(EAnimationEvents::HIT, [=](){ - effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); - }); - } - - //mana absorption - if (sc->manaGained > 0) - { - Point leftHero = Point(15, 30); - Point rightHero = Point(755, 30); - bool side = sc->side; - - addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); - stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); - }); - } - - // animations will be executed by spell effects -} - -void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) -{ - if(stacksController->getActiveStack() != nullptr) - fieldController->redrawBackgroundWithHexes(); -} - -void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) -{ - if(side == BattleSide::ATTACKER) - { - if(attackingHero) - attackingHero->setPhase(phase); - } - else - { - if(defendingHero) - defendingHero->setPhase(phase); - } -} - -void BattleInterface::displayBattleLog(const std::vector & battleLog) -{ - for(const auto & line : battleLog) - { - std::string formatted = line.toString(); - boost::algorithm::trim(formatted); - appendBattleLog(formatted); - } -} - -void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) -{ - for(const CSpell::TAnimation & animation : q) - { - if(animation.pause > 0) - stacksController->addNewAnim(new DummyAnimation(*this, animation.pause)); - - if (!animation.effectName.empty()) - { - const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false); - - if (destStack) - stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); - } - - if(!animation.resourceName.empty()) - { - int flags = 0; - - if (isHit) - flags |= EffectAnimation::FORCE_ON_TOP; - - if (animation.verticalPosition == VerticalPosition::BOTTOM) - flags |= EffectAnimation::ALIGN_TO_BOTTOM; - - if (!destinationTile.isValid()) - flags |= EffectAnimation::SCREEN_FILL; - - if (!destinationTile.isValid()) - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); - else - stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); - } - } -} - -void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false); -} - -void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false); -} - -void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile) -{ - if(spell) - displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true); -} - -CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const -{ - return curInt.get(); -} - -void BattleInterface::trySetActivePlayer( PlayerColor player ) -{ - if ( attackerInt && attackerInt->playerID == player ) - curInt = attackerInt; - - if ( defenderInt && defenderInt->playerID == player ) - curInt = defenderInt; -} - -void BattleInterface::activateStack() -{ - stacksController->activateStack(); - - const CStack * s = stacksController->getActiveStack(); - if(!s) - return; - - windowObject->updateQueue(); - windowObject->blockUI(false); - fieldController->redrawBackgroundWithHexes(); - actionsController->activateStack(); - GH.fakeMouseMove(); -} - -bool BattleInterface::makingTurn() const -{ - return stacksController->getActiveStack() != nullptr; -} - -BattleID BattleInterface::getBattleID() const -{ - return battleID; -} - -std::shared_ptr BattleInterface::getBattle() const -{ - return curInt->cb->getBattle(battleID); -} - -void BattleInterface::endAction(const BattleAction &action) -{ - // it is possible that tactics mode ended while opening music is still playing - waitForAnimations(); - - const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber); - - // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast - activateStack(); - - stacksController->endAction(action); - windowObject->updateQueue(); - - //stack ended movement in tactics phase -> select the next one - if (tacticsMode) - tacticNextStack(stack); - - //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed - if(action.actionType == EActionType::HERO_SPELL) - fieldController->redrawBackgroundWithHexes(); -} - -void BattleInterface::appendBattleLog(const std::string & newEntry) -{ - console->addText(newEntry); -} - -void BattleInterface::startAction(const BattleAction & action) -{ - if(action.actionType == EActionType::END_TACTIC_PHASE) - { - windowObject->tacticPhaseEnded(); - return; - } - - stacksController->startAction(action); - - if (!action.isUnitAction()) - return; - - assert(getBattle()->battleGetStackByID(action.stackNumber)); - windowObject->updateQueue(); - effectsController->startAction(action); -} - -void BattleInterface::tacticPhaseEnd() -{ - stacksController->setActiveStack(nullptr); - tacticsMode = false; - - auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); - auto action = BattleAction::makeEndOFTacticPhase(*side); - - tacticianInterface->cb->battleMakeTacticAction(battleID, action); -} - -static bool immobile(const CStack *s) -{ - return !s->speed(0, true); //should bound stacks be immobile? -} - -void BattleInterface::tacticNextStack(const CStack * current) -{ - if (!current) - current = stacksController->getActiveStack(); - - //no switching stacks when the current one is moving - checkForAnimations(); - - TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE); - vstd::erase_if (stacksOfMine, &immobile); - if (stacksOfMine.empty()) - { - tacticPhaseEnd(); - return; - } - - auto it = vstd::find(stacksOfMine, current); - if (it != stacksOfMine.end() && ++it != stacksOfMine.end()) - stackActivated(*it); - else - stackActivated(stacksOfMine.front()); - -} - -void BattleInterface::obstaclePlaced(const std::vector> oi) -{ - obstacleController->obstaclePlaced(oi); -} - -void BattleInterface::obstacleRemoved(const std::vector & obstacles) -{ - obstacleController->obstacleRemoved(obstacles); -} - -const CGHeroInstance *BattleInterface::currentHero() const -{ - if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID) - return attackingHeroInstance; - - if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID) - return defendingHeroInstance; - - return nullptr; -} - -InfoAboutHero BattleInterface::enemyHero() const -{ - InfoAboutHero ret; - if (attackingHeroInstance->tempOwner == curInt->playerID) - curInt->cb->getHeroInfo(defendingHeroInstance, ret); - else - curInt->cb->getHeroInfo(attackingHeroInstance, ret); - - return ret; -} - -void BattleInterface::requestAutofightingAIToTakeAction() -{ - assert(curInt->isAutoFightOn); - - if(getBattle()->battleIsFinished()) - { - return; // battle finished with spellcast - } - - if (tacticsMode) - { - // Always end tactics mode. Player interface is blocked currently, so it's not possible that - // the AI can take any action except end tactics phase (AI actions won't be triggered) - //TODO implement the possibility that the AI will be triggered for further actions - //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? - tacticPhaseEnd(); - stacksController->setActiveStack(nullptr); - } - else - { - const CStack* activeStack = stacksController->getActiveStack(); - - // If enemy is moving, activeStack can be null - if (activeStack) - { - stacksController->setActiveStack(nullptr); - - // FIXME: unsafe - // Run task in separate thread to avoid UI lock while AI is making turn (which might take some time) - // HOWEVER this thread won't atttempt to lock game state, potentially leading to races - boost::thread aiThread([this, activeStack]() - { - setThreadName("autofightingAI"); - curInt->autofightingAI->activeStack(battleID, activeStack); - }); - aiThread.detach(); - } - } -} - -void BattleInterface::castThisSpell(SpellID spellID) -{ - actionsController->castThisSpell(spellID); -} - -void BattleInterface::executeStagedAnimations() -{ - EAnimationEvents earliestStage = EAnimationEvents::COUNT; - - for(const auto & event : awaitingEvents) - earliestStage = std::min(earliestStage, event.event); - - if(earliestStage != EAnimationEvents::COUNT) - executeAnimationStage(earliestStage); -} - -void BattleInterface::executeAnimationStage(EAnimationEvents event) -{ - decltype(awaitingEvents) executingEvents; - - for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();) - { - if(it->event == event) - { - executingEvents.push_back(*it); - it = awaitingEvents.erase(it); - } - else - ++it; - } - for(const auto & event : executingEvents) - event.action(); -} - -void BattleInterface::onAnimationsStarted() -{ - ongoingAnimationsState.setn(true); -} - -void BattleInterface::onAnimationsFinished() -{ - ongoingAnimationsState.setn(false); -} - -void BattleInterface::waitForAnimations() -{ - { - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - ongoingAnimationsState.waitUntil(false); - } - - assert(!hasAnimations()); - assert(awaitingEvents.empty()); - - if (!awaitingEvents.empty()) - { - logGlobal->error("Wait for animations finished but we still have awaiting events!"); - awaitingEvents.clear(); - } -} - -bool BattleInterface::hasAnimations() -{ - return ongoingAnimationsState.get(); -} - -void BattleInterface::checkForAnimations() -{ - assert(!hasAnimations()); - if(hasAnimations()) - logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!"); - - waitForAnimations(); -} - -void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action) -{ - awaitingEvents.push_back({action, event}); -} - -void BattleInterface::setBattleQueueVisibility(bool visible) -{ - windowObject->hideQueue(); - if(visible) - windowObject->showQueue(); -} - -void BattleInterface::setStickyHeroWindowsVisibility(bool visible) -{ - windowObject->hideStickyHeroWindows(); - if(visible) - windowObject->showStickyHeroWindows(); -} +/* + * BattleInterface.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 "BattleInterface.h" + +#include "BattleAnimationClasses.h" +#include "BattleActionsController.h" +#include "BattleInterfaceClasses.h" +#include "CreatureAnimation.h" +#include "BattleProjectileController.h" +#include "BattleEffectsController.h" +#include "BattleObstacleController.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleWindow.h" +#include "BattleStacksController.h" +#include "BattleRenderer.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../render/Canvas.h" +#include "../adventureMap/AdventureMapInterface.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/CondSh.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/NetPacks.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/CThreadHelper.h" + +BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, + const CGHeroInstance *hero1, const CGHeroInstance *hero2, + std::shared_ptr att, + std::shared_ptr defen, + std::shared_ptr spectatorInt) + : attackingHeroInstance(hero1) + , defendingHeroInstance(hero2) + , attackerInt(att) + , defenderInt(defen) + , curInt(att) + , battleID(battleID) + , battleOpeningDelayActive(true) +{ + if(spectatorInt) + { + curInt = spectatorInt; + } + else if(!curInt) + { + //May happen when we are defending during network MP game -> attacker interface is just not present + curInt = defenderInt; + } + + //hot-seat -> check tactics for both players (defender may be local human) + if(attackerInt && attackerInt->cb->getBattle(getBattleID())->battleGetTacticDist()) + tacticianInterface = attackerInt; + else if(defenderInt && defenderInt->cb->getBattle(getBattleID())->battleGetTacticDist()) + tacticianInterface = defenderInt; + + //if we found interface of player with tactics, then enter tactics mode + tacticsMode = static_cast(tacticianInterface); + + //initializing armies + this->army1 = army1; + this->army2 = army2; + + const CGTownInstance *town = getBattle()->battleGetDefendedTown(); + if(town && town->hasFort()) + siegeController.reset(new BattleSiegeController(*this, town)); + + windowObject = std::make_shared(*this); + projectilesController.reset(new BattleProjectileController(*this)); + stacksController.reset( new BattleStacksController(*this)); + actionsController.reset( new BattleActionsController(*this)); + effectsController.reset(new BattleEffectsController(*this)); + obstacleController.reset(new BattleObstacleController(*this)); + + adventureInt->onAudioPaused(); + ongoingAnimationsState.set(true); + + GH.windows().pushWindow(windowObject); + windowObject->blockUI(true); + windowObject->updateQueue(); + + playIntroSoundAndUnlockInterface(); +} + +void BattleInterface::playIntroSoundAndUnlockInterface() +{ + auto onIntroPlayed = [this]() + { + if(LOCPLINT->battleInt) + onIntroSoundPlayed(); + }; + + int battleIntroSoundChannel = CCS->soundh->playSoundFromSet(CCS->soundh->battleIntroSounds); + if (battleIntroSoundChannel != -1) + { + CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed); + + if (settings["gameTweaks"]["skipBattleIntroMusic"].Bool()) + openingEnd(); + } + else // failed to play sound + { + onIntroSoundPlayed(); + } +} + +bool BattleInterface::openingPlaying() +{ + return battleOpeningDelayActive; +} + +void BattleInterface::onIntroSoundPlayed() +{ + if (openingPlaying()) + openingEnd(); + + CCS->musich->playMusicFromSet("battle", true, true); +} + +void BattleInterface::openingEnd() +{ + assert(openingPlaying()); + if (!openingPlaying()) + return; + + onAnimationsFinished(); + if(tacticsMode) + tacticNextStack(nullptr); + activateStack(); + battleOpeningDelayActive = false; +} + +BattleInterface::~BattleInterface() +{ + CPlayerInterface::battleInt = nullptr; + + if (adventureInt) + adventureInt->onAudioResumed(); + + awaitingEvents.clear(); + onAnimationsFinished(); +} + +void BattleInterface::redrawBattlefield() +{ + fieldController->redrawBackgroundWithHexes(); + GH.windows().totalRedraw(); +} + +void BattleInterface::stackReset(const CStack * stack) +{ + stacksController->stackReset(stack); +} + +void BattleInterface::stackAdded(const CStack * stack) +{ + stacksController->stackAdded(stack, false); +} + +void BattleInterface::stackRemoved(uint32_t stackID) +{ + stacksController->stackRemoved(stackID); + fieldController->redrawBackgroundWithHexes(); + windowObject->updateQueue(); +} + +void BattleInterface::stackActivated(const CStack *stack) +{ + stacksController->stackActivated(stack); +} + +void BattleInterface::stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport) +{ + if (teleport) + stacksController->stackTeleported(stack, destHex, distance); + else + stacksController->stackMoved(stack, destHex, distance); +} + +void BattleInterface::stacksAreAttacked(std::vector attackedInfos) +{ + stacksController->stacksAreAttacked(attackedInfos); + + std::array killedBySide = {0, 0}; + + for(const StackAttackedInfo & attackedInfo : attackedInfos) + { + ui8 side = attackedInfo.defender->unitSide(); + killedBySide.at(side) += attackedInfo.amountKilled; + } + + for(ui8 side = 0; side < 2; side++) + { + if(killedBySide.at(side) > killedBySide.at(1-side)) + setHeroAnimation(side, EHeroAnimType::DEFEAT); + else if(killedBySide.at(side) < killedBySide.at(1-side)) + setHeroAnimation(side, EHeroAnimType::VICTORY); + } +} + +void BattleInterface::stackAttacking( const StackAttackInfo & attackInfo ) +{ + stacksController->stackAttacking(attackInfo); +} + +void BattleInterface::newRoundFirst() +{ + waitForAnimations(); +} + +void BattleInterface::newRound() +{ + console->addText(CGI->generaltexth->allTexts[412]); +} + +void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell) +{ + const CStack * actor = nullptr; + if(action != EActionType::HERO_SPELL && action != EActionType::RETREAT && action != EActionType::SURRENDER) + { + actor = stacksController->getActiveStack(); + } + + auto side = getBattle()->playerToSide(curInt->playerID); + if(!side) + { + logGlobal->error("Player %s is not in battle", curInt->playerID.toString()); + return; + } + + BattleAction ba; + ba.side = side.value(); + ba.actionType = action; + ba.aimToHex(tile); + ba.spell = spell; + + sendCommand(ba, actor); +} + +void BattleInterface::sendCommand(BattleAction command, const CStack * actor) +{ + command.stackNumber = actor ? actor->unitId() : ((command.side == BattleSide::ATTACKER) ? -1 : -2); + + if(!tacticsMode) + { + logGlobal->trace("Setting command for %s", (actor ? actor->nodeName() : "hero")); + stacksController->setActiveStack(nullptr); + curInt->cb->battleMakeUnitAction(battleID, command); + } + else + { + curInt->cb->battleMakeTacticAction(battleID, command); + stacksController->setActiveStack(nullptr); + //next stack will be activated when action ends + } + CCS->curh->set(Cursor::Combat::POINTER); +} + +const CGHeroInstance * BattleInterface::getActiveHero() +{ + const CStack *attacker = stacksController->getActiveStack(); + if(!attacker) + { + return nullptr; + } + + if(attacker->unitSide() == BattleSide::ATTACKER) + { + return attackingHeroInstance; + } + + return defendingHeroInstance; +} + +void BattleInterface::stackIsCatapulting(const CatapultAttack & ca) +{ + if (siegeController) + siegeController->stackIsCatapulting(ca); +} + +void BattleInterface::gateStateChanged(const EGateState state) +{ + if (siegeController) + siegeController->gateStateChanged(state); +} + +void BattleInterface::battleFinished(const BattleResult& br, QueryID queryID) +{ + checkForAnimations(); + stacksController->setActiveStack(nullptr); + + CCS->curh->set(Cursor::Map::POINTER); + curInt->waitWhileDialog(); + + if(settings["session"]["spectate"].Bool() && settings["session"]["spectate-skip-battle-result"].Bool()) + { + curInt->cb->selectionMade(0, queryID); + windowObject->close(); + return; + } + + auto wnd = std::make_shared(br, *(this->curInt)); + wnd->resultCallback = [=](ui32 selection) + { + curInt->cb->selectionMade(selection, queryID); + }; + GH.windows().pushWindow(wnd); + + curInt->waitWhileDialog(); // Avoid freeze when AI end turn after battle. Check bug #1897 + CPlayerInterface::battleInt = nullptr; +} + +void BattleInterface::spellCast(const BattleSpellCast * sc) +{ + // Do not deactivate anything in tactics mode + // This is battlefield setup spells + if(!tacticsMode) + { + windowObject->blockUI(true); + + // Disable current active stack duing the cast + // Store the current activeStack to stackToActivate + stacksController->deactivateStack(); + } + + CCS->curh->set(Cursor::Combat::BLOCKED); + + const SpellID spellID = sc->spellID; + const CSpell * spell = spellID.toSpell(); + auto targetedTile = sc->tile; + + assert(spell); + if(!spell) + return; + + const AudioPath & castSoundPath = spell->getCastSound(); + + if (!castSoundPath.empty()) + { + auto group = spell->animationInfo.projectile.empty() ? + EAnimationEvents::HIT: + EAnimationEvents::BEFORE_HIT;//FIXME: recheck whether this should be on projectile spawning + + addToAnimationStage(group, [=]() { + CCS->soundh->playSound(castSoundPath); + }); + } + + if ( sc->activeCast ) + { + const CStack * casterStack = getBattle()->battleGetStackByID(sc->casterStack); + + if(casterStack != nullptr ) + { + addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() + { + stacksController->addNewAnim(new CastAnimation(*this, casterStack, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); + displaySpellCast(spell, casterStack->getPosition()); + }); + } + else + { + auto hero = sc->side ? defendingHero : attackingHero; + assert(hero); + + addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() + { + stacksController->addNewAnim(new HeroCastAnimation(*this, hero, targetedTile, getBattle()->battleGetStackByPos(targetedTile), spell)); + }); + } + } + + addToAnimationStage(EAnimationEvents::HIT, [=](){ + displaySpellHit(spell, targetedTile); + }); + + //queuing affect animation + for(auto & elem : sc->affectedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + if(stack) + { + addToAnimationStage(EAnimationEvents::HIT, [=](){ + displaySpellEffect(spell, stack->getPosition()); + }); + } + } + + for(auto & elem : sc->reflectedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + addToAnimationStage(EAnimationEvents::HIT, [=](){ + effectsController->displayEffect(EBattleEffect::MAGIC_MIRROR, stack->getPosition()); + }); + } + + if (!sc->resistedCres.empty()) + { + addToAnimationStage(EAnimationEvents::HIT, [=](){ + CCS->soundh->playSound(AudioPath::builtin("MAGICRES")); + }); + } + + for(auto & elem : sc->resistedCres) + { + auto stack = getBattle()->battleGetStackByID(elem, false); + assert(stack); + addToAnimationStage(EAnimationEvents::HIT, [=](){ + effectsController->displayEffect(EBattleEffect::RESISTANCE, stack->getPosition()); + }); + } + + //mana absorption + if (sc->manaGained > 0) + { + Point leftHero = Point(15, 30); + Point rightHero = Point(755, 30); + bool side = sc->side; + + addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_A.DEF" : "SP07_B.DEF"), leftHero)); + stacksController->addNewAnim(new EffectAnimation(*this, AnimationPath::builtin(side ? "SP07_B.DEF" : "SP07_A.DEF"), rightHero)); + }); + } + + // animations will be executed by spell effects +} + +void BattleInterface::battleStacksEffectsSet(const SetStackEffect & sse) +{ + if(stacksController->getActiveStack() != nullptr) + fieldController->redrawBackgroundWithHexes(); +} + +void BattleInterface::setHeroAnimation(ui8 side, EHeroAnimType phase) +{ + if(side == BattleSide::ATTACKER) + { + if(attackingHero) + attackingHero->setPhase(phase); + } + else + { + if(defendingHero) + defendingHero->setPhase(phase); + } +} + +void BattleInterface::displayBattleLog(const std::vector & battleLog) +{ + for(const auto & line : battleLog) + { + std::string formatted = line.toString(); + boost::algorithm::trim(formatted); + appendBattleLog(formatted); + } +} + +void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit) +{ + for(const CSpell::TAnimation & animation : q) + { + if(animation.pause > 0) + stacksController->addNewAnim(new DummyAnimation(*this, animation.pause)); + + if (!animation.effectName.empty()) + { + const CStack * destStack = getBattle()->battleGetStackByPos(destinationTile, false); + + if (destStack) + stacksController->addNewAnim(new ColorTransformAnimation(*this, destStack, animation.effectName, spell )); + } + + if(!animation.resourceName.empty()) + { + int flags = 0; + + if (isHit) + flags |= EffectAnimation::FORCE_ON_TOP; + + if (animation.verticalPosition == VerticalPosition::BOTTOM) + flags |= EffectAnimation::ALIGN_TO_BOTTOM; + + if (!destinationTile.isValid()) + flags |= EffectAnimation::SCREEN_FILL; + + if (!destinationTile.isValid()) + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags)); + else + stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags)); + } + } +} + +void BattleInterface::displaySpellCast(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.cast, destinationTile, false); +} + +void BattleInterface::displaySpellEffect(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.affect, destinationTile, false); +} + +void BattleInterface::displaySpellHit(const CSpell * spell, BattleHex destinationTile) +{ + if(spell) + displaySpellAnimationQueue(spell, spell->animationInfo.hit, destinationTile, true); +} + +CPlayerInterface *BattleInterface::getCurrentPlayerInterface() const +{ + return curInt.get(); +} + +void BattleInterface::trySetActivePlayer( PlayerColor player ) +{ + if ( attackerInt && attackerInt->playerID == player ) + curInt = attackerInt; + + if ( defenderInt && defenderInt->playerID == player ) + curInt = defenderInt; +} + +void BattleInterface::activateStack() +{ + stacksController->activateStack(); + + const CStack * s = stacksController->getActiveStack(); + if(!s) + return; + + windowObject->updateQueue(); + windowObject->blockUI(false); + fieldController->redrawBackgroundWithHexes(); + actionsController->activateStack(); + GH.fakeMouseMove(); +} + +bool BattleInterface::makingTurn() const +{ + return stacksController->getActiveStack() != nullptr; +} + +BattleID BattleInterface::getBattleID() const +{ + return battleID; +} + +std::shared_ptr BattleInterface::getBattle() const +{ + return curInt->cb->getBattle(battleID); +} + +void BattleInterface::endAction(const BattleAction &action) +{ + // it is possible that tactics mode ended while opening music is still playing + waitForAnimations(); + + const CStack *stack = getBattle()->battleGetStackByID(action.stackNumber); + + // Activate stack from stackToActivate because this might have been temporary disabled, e.g., during spell cast + activateStack(); + + stacksController->endAction(action); + windowObject->updateQueue(); + + //stack ended movement in tactics phase -> select the next one + if (tacticsMode) + tacticNextStack(stack); + + //we have activated next stack after sending request that has been just realized -> blockmap due to movement has changed + if(action.actionType == EActionType::HERO_SPELL) + fieldController->redrawBackgroundWithHexes(); +} + +void BattleInterface::appendBattleLog(const std::string & newEntry) +{ + console->addText(newEntry); +} + +void BattleInterface::startAction(const BattleAction & action) +{ + if(action.actionType == EActionType::END_TACTIC_PHASE) + { + windowObject->tacticPhaseEnded(); + return; + } + + stacksController->startAction(action); + + if (!action.isUnitAction()) + return; + + assert(getBattle()->battleGetStackByID(action.stackNumber)); + windowObject->updateQueue(); + effectsController->startAction(action); +} + +void BattleInterface::tacticPhaseEnd() +{ + stacksController->setActiveStack(nullptr); + tacticsMode = false; + + auto side = tacticianInterface->cb->getBattle(battleID)->playerToSide(tacticianInterface->playerID); + auto action = BattleAction::makeEndOFTacticPhase(*side); + + tacticianInterface->cb->battleMakeTacticAction(battleID, action); +} + +static bool immobile(const CStack *s) +{ + return !s->speed(0, true); //should bound stacks be immobile? +} + +void BattleInterface::tacticNextStack(const CStack * current) +{ + if (!current) + current = stacksController->getActiveStack(); + + //no switching stacks when the current one is moving + checkForAnimations(); + + TStacks stacksOfMine = tacticianInterface->cb->getBattle(battleID)->battleGetStacks(CPlayerBattleCallback::ONLY_MINE); + vstd::erase_if (stacksOfMine, &immobile); + if (stacksOfMine.empty()) + { + tacticPhaseEnd(); + return; + } + + auto it = vstd::find(stacksOfMine, current); + if (it != stacksOfMine.end() && ++it != stacksOfMine.end()) + stackActivated(*it); + else + stackActivated(stacksOfMine.front()); + +} + +void BattleInterface::obstaclePlaced(const std::vector> oi) +{ + obstacleController->obstaclePlaced(oi); +} + +void BattleInterface::obstacleRemoved(const std::vector & obstacles) +{ + obstacleController->obstacleRemoved(obstacles); +} + +const CGHeroInstance *BattleInterface::currentHero() const +{ + if (attackingHeroInstance && attackingHeroInstance->tempOwner == curInt->playerID) + return attackingHeroInstance; + + if (defendingHeroInstance && defendingHeroInstance->tempOwner == curInt->playerID) + return defendingHeroInstance; + + return nullptr; +} + +InfoAboutHero BattleInterface::enemyHero() const +{ + InfoAboutHero ret; + if (attackingHeroInstance->tempOwner == curInt->playerID) + curInt->cb->getHeroInfo(defendingHeroInstance, ret); + else + curInt->cb->getHeroInfo(attackingHeroInstance, ret); + + return ret; +} + +void BattleInterface::requestAutofightingAIToTakeAction() +{ + assert(curInt->isAutoFightOn); + + if(getBattle()->battleIsFinished()) + { + return; // battle finished with spellcast + } + + if (tacticsMode) + { + // Always end tactics mode. Player interface is blocked currently, so it's not possible that + // the AI can take any action except end tactics phase (AI actions won't be triggered) + //TODO implement the possibility that the AI will be triggered for further actions + //TODO any solution to merge tactics phase & normal phase in the way it is handled by the player and battle interface? + tacticPhaseEnd(); + stacksController->setActiveStack(nullptr); + } + else + { + const CStack* activeStack = stacksController->getActiveStack(); + + // If enemy is moving, activeStack can be null + if (activeStack) + { + stacksController->setActiveStack(nullptr); + + // FIXME: unsafe + // Run task in separate thread to avoid UI lock while AI is making turn (which might take some time) + // HOWEVER this thread won't atttempt to lock game state, potentially leading to races + boost::thread aiThread([this, activeStack]() + { + setThreadName("autofightingAI"); + curInt->autofightingAI->activeStack(battleID, activeStack); + }); + aiThread.detach(); + } + } +} + +void BattleInterface::castThisSpell(SpellID spellID) +{ + actionsController->castThisSpell(spellID); +} + +void BattleInterface::executeStagedAnimations() +{ + EAnimationEvents earliestStage = EAnimationEvents::COUNT; + + for(const auto & event : awaitingEvents) + earliestStage = std::min(earliestStage, event.event); + + if(earliestStage != EAnimationEvents::COUNT) + executeAnimationStage(earliestStage); +} + +void BattleInterface::executeAnimationStage(EAnimationEvents event) +{ + decltype(awaitingEvents) executingEvents; + + for(auto it = awaitingEvents.begin(); it != awaitingEvents.end();) + { + if(it->event == event) + { + executingEvents.push_back(*it); + it = awaitingEvents.erase(it); + } + else + ++it; + } + for(const auto & event : executingEvents) + event.action(); +} + +void BattleInterface::onAnimationsStarted() +{ + ongoingAnimationsState.setn(true); +} + +void BattleInterface::onAnimationsFinished() +{ + ongoingAnimationsState.setn(false); +} + +void BattleInterface::waitForAnimations() +{ + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + ongoingAnimationsState.waitUntil(false); + } + + assert(!hasAnimations()); + assert(awaitingEvents.empty()); + + if (!awaitingEvents.empty()) + { + logGlobal->error("Wait for animations finished but we still have awaiting events!"); + awaitingEvents.clear(); + } +} + +bool BattleInterface::hasAnimations() +{ + return ongoingAnimationsState.get(); +} + +void BattleInterface::checkForAnimations() +{ + assert(!hasAnimations()); + if(hasAnimations()) + logGlobal->error("Unexpected animations state: expected all animations to be over, but some are still ongoing!"); + + waitForAnimations(); +} + +void BattleInterface::addToAnimationStage(EAnimationEvents event, const AwaitingAnimationAction & action) +{ + awaitingEvents.push_back({action, event}); +} + +void BattleInterface::setBattleQueueVisibility(bool visible) +{ + windowObject->hideQueue(); + if(visible) + windowObject->showQueue(); +} + +void BattleInterface::setStickyHeroWindowsVisibility(bool visible) +{ + windowObject->hideStickyHeroWindows(); + if(visible) + windowObject->showStickyHeroWindows(); +} diff --git a/client/battle/BattleInterface.h b/client/battle/BattleInterface.h index a4a1a54e7..31e64ef29 100644 --- a/client/battle/BattleInterface.h +++ b/client/battle/BattleInterface.h @@ -1,230 +1,230 @@ -/* - * BattleInterface.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 "BattleConstants.h" -#include "../gui/CIntObject.h" -#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation -#include "../../lib/CondSh.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCreatureSet; -class CGHeroInstance; -class CStack; -struct BattleResult; -struct BattleSpellCast; -struct CObstacleInstance; -struct SetStackEffect; -class BattleAction; -class CGTownInstance; -struct CatapultAttack; -struct BattleTriggerEffect; -struct BattleHex; -struct InfoAboutHero; -class ObstacleChanges; -class CPlayerBattleCallback; - -VCMI_LIB_NAMESPACE_END - -class BattleHero; -class Canvas; -class BattleResultWindow; -class StackQueue; -class CPlayerInterface; -class CAnimation; -struct BattleEffect; -class IImage; -class StackQueue; - -class BattleProjectileController; -class BattleSiegeController; -class BattleObstacleController; -class BattleFieldController; -class BattleRenderer; -class BattleWindow; -class BattleStacksController; -class BattleActionsController; -class BattleEffectsController; -class BattleConsole; - -/// Small struct which contains information about the id of the attacked stack, the damage dealt,... -struct StackAttackedInfo -{ - const CStack *defender; - const CStack *attacker; - - int64_t damageDealt; - uint32_t amountKilled; - SpellID spellEffect; - - bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack - bool killed; //if true, stack has been killed - bool rebirth; //if true, play rebirth animation after all - bool cloneKilled; - bool fireShield; -}; - -struct StackAttackInfo -{ - const CStack *attacker; - const CStack *defender; - std::vector< const CStack *> secondaryDefender; - - SpellID spellEffect; - BattleHex tile; - - bool indirectAttack; - bool lucky; - bool unlucky; - bool deathBlow; - bool lifeDrain; -}; - -/// Main class for battles, responsible for relaying information from server to various battle entities -class BattleInterface -{ - using AwaitingAnimationAction = std::function; - - struct AwaitingAnimationEvents { - AwaitingAnimationAction action; - EAnimationEvents event; - }; - - /// Conditional variables that are set depending on ongoing animations on the battlefield - CondSh ongoingAnimationsState; - - /// List of events that are waiting to be triggered - std::vector awaitingEvents; - - /// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players - std::shared_ptr tacticianInterface; - - /// attacker interface, not null if attacker is human in our vcmiclient - std::shared_ptr attackerInt; - - /// defender interface, not null if attacker is human in our vcmiclient - std::shared_ptr defenderInt; - - /// if set to true, battle is still starting and waiting for intro sound to end / key press from player - bool battleOpeningDelayActive; - - /// ID of ongoing battle - BattleID battleID; - - void playIntroSoundAndUnlockInterface(); - void onIntroSoundPlayed(); -public: - /// copy of initial armies (for result window) - const CCreatureSet *army1; - const CCreatureSet *army2; - - std::shared_ptr windowObject; - std::shared_ptr console; - - /// currently active player interface - std::shared_ptr curInt; - - const CGHeroInstance *attackingHeroInstance; - const CGHeroInstance *defendingHeroInstance; - - bool tacticsMode; - - std::unique_ptr projectilesController; - std::unique_ptr siegeController; - std::unique_ptr obstacleController; - std::unique_ptr fieldController; - std::unique_ptr stacksController; - std::unique_ptr actionsController; - std::unique_ptr effectsController; - - std::shared_ptr attackingHero; - std::shared_ptr defendingHero; - - bool openingPlaying(); - void openingEnd(); - - bool makingTurn() const; - - BattleID getBattleID() const; - std::shared_ptr getBattle() const; - - BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); - ~BattleInterface(); - - void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player - void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all - void requestAutofightingAIToTakeAction(); - - void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE); - void sendCommand(BattleAction command, const CStack * actor = nullptr); - - const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell - - void showInterface(Canvas & to); - - void setHeroAnimation(ui8 side, EHeroAnimType phase); - - void executeSpellCast(); //called when a hero casts a spell - - void appendBattleLog(const std::string & newEntry); - - void redrawBattlefield(); //refresh GUI after changing stack range / grid settings - CPlayerInterface *getCurrentPlayerInterface() const; - - void tacticNextStack(const CStack *current); - void tacticPhaseEnd(); - - void setBattleQueueVisibility(bool visible); - void setStickyHeroWindowsVisibility(bool visible); - - void executeStagedAnimations(); - void executeAnimationStage( EAnimationEvents event); - void onAnimationsStarted(); - void onAnimationsFinished(); - void waitForAnimations(); - bool hasAnimations(); - void checkForAnimations(); - void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); - - //call-ins - void startAction(const BattleAction & action); - void stackReset(const CStack * stack); - void stackAdded(const CStack * stack); //new stack appeared on battlefield - void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled - void stackActivated(const CStack *stack); //active stack has been changed - void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex - void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked - void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest - void newRoundFirst(); - void newRound(); //caled when round is ended; - void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls - void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed - void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell - void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks - void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook - - void displayBattleLog(const std::vector & battleLog); - - void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); - void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation - void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation - - void endAction(const BattleAction & action); - - void obstaclePlaced(const std::vector> oi); - void obstacleRemoved(const std::vector & obstacles); - - void gateStateChanged(const EGateState state); - - const CGHeroInstance *currentHero() const; - InfoAboutHero enemyHero() const; -}; +/* + * BattleInterface.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 "BattleConstants.h" +#include "../gui/CIntObject.h" +#include "../../lib/spells/CSpellHandler.h" //CSpell::TAnimation +#include "../../lib/CondSh.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCreatureSet; +class CGHeroInstance; +class CStack; +struct BattleResult; +struct BattleSpellCast; +struct CObstacleInstance; +struct SetStackEffect; +class BattleAction; +class CGTownInstance; +struct CatapultAttack; +struct BattleTriggerEffect; +struct BattleHex; +struct InfoAboutHero; +class ObstacleChanges; +class CPlayerBattleCallback; + +VCMI_LIB_NAMESPACE_END + +class BattleHero; +class Canvas; +class BattleResultWindow; +class StackQueue; +class CPlayerInterface; +class CAnimation; +struct BattleEffect; +class IImage; +class StackQueue; + +class BattleProjectileController; +class BattleSiegeController; +class BattleObstacleController; +class BattleFieldController; +class BattleRenderer; +class BattleWindow; +class BattleStacksController; +class BattleActionsController; +class BattleEffectsController; +class BattleConsole; + +/// Small struct which contains information about the id of the attacked stack, the damage dealt,... +struct StackAttackedInfo +{ + const CStack *defender; + const CStack *attacker; + + int64_t damageDealt; + uint32_t amountKilled; + SpellID spellEffect; + + bool indirectAttack; //if true, stack was attacked indirectly - spell or ranged attack + bool killed; //if true, stack has been killed + bool rebirth; //if true, play rebirth animation after all + bool cloneKilled; + bool fireShield; +}; + +struct StackAttackInfo +{ + const CStack *attacker; + const CStack *defender; + std::vector< const CStack *> secondaryDefender; + + SpellID spellEffect; + BattleHex tile; + + bool indirectAttack; + bool lucky; + bool unlucky; + bool deathBlow; + bool lifeDrain; +}; + +/// Main class for battles, responsible for relaying information from server to various battle entities +class BattleInterface +{ + using AwaitingAnimationAction = std::function; + + struct AwaitingAnimationEvents { + AwaitingAnimationAction action; + EAnimationEvents event; + }; + + /// Conditional variables that are set depending on ongoing animations on the battlefield + CondSh ongoingAnimationsState; + + /// List of events that are waiting to be triggered + std::vector awaitingEvents; + + /// used during tactics mode, points to the interface of player with higher tactics (can be either attacker or defender in hot-seat), valid onloy for human players + std::shared_ptr tacticianInterface; + + /// attacker interface, not null if attacker is human in our vcmiclient + std::shared_ptr attackerInt; + + /// defender interface, not null if attacker is human in our vcmiclient + std::shared_ptr defenderInt; + + /// if set to true, battle is still starting and waiting for intro sound to end / key press from player + bool battleOpeningDelayActive; + + /// ID of ongoing battle + BattleID battleID; + + void playIntroSoundAndUnlockInterface(); + void onIntroSoundPlayed(); +public: + /// copy of initial armies (for result window) + const CCreatureSet *army1; + const CCreatureSet *army2; + + std::shared_ptr windowObject; + std::shared_ptr console; + + /// currently active player interface + std::shared_ptr curInt; + + const CGHeroInstance *attackingHeroInstance; + const CGHeroInstance *defendingHeroInstance; + + bool tacticsMode; + + std::unique_ptr projectilesController; + std::unique_ptr siegeController; + std::unique_ptr obstacleController; + std::unique_ptr fieldController; + std::unique_ptr stacksController; + std::unique_ptr actionsController; + std::unique_ptr effectsController; + + std::shared_ptr attackingHero; + std::shared_ptr defendingHero; + + bool openingPlaying(); + void openingEnd(); + + bool makingTurn() const; + + BattleID getBattleID() const; + std::shared_ptr getBattle() const; + + BattleInterface(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, const CGHeroInstance *hero1, const CGHeroInstance *hero2, std::shared_ptr att, std::shared_ptr defen, std::shared_ptr spectatorInt = nullptr); + ~BattleInterface(); + + void trySetActivePlayer( PlayerColor player ); // if in hotseat, will activate interface of chosen player + void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all + void requestAutofightingAIToTakeAction(); + + void giveCommand(EActionType action, BattleHex tile = BattleHex(), SpellID spell = SpellID::NONE); + void sendCommand(BattleAction command, const CStack * actor = nullptr); + + const CGHeroInstance *getActiveHero(); //returns hero that can currently cast a spell + + void showInterface(Canvas & to); + + void setHeroAnimation(ui8 side, EHeroAnimType phase); + + void executeSpellCast(); //called when a hero casts a spell + + void appendBattleLog(const std::string & newEntry); + + void redrawBattlefield(); //refresh GUI after changing stack range / grid settings + CPlayerInterface *getCurrentPlayerInterface() const; + + void tacticNextStack(const CStack *current); + void tacticPhaseEnd(); + + void setBattleQueueVisibility(bool visible); + void setStickyHeroWindowsVisibility(bool visible); + + void executeStagedAnimations(); + void executeAnimationStage( EAnimationEvents event); + void onAnimationsStarted(); + void onAnimationsFinished(); + void waitForAnimations(); + bool hasAnimations(); + void checkForAnimations(); + void addToAnimationStage( EAnimationEvents event, const AwaitingAnimationAction & action); + + //call-ins + void startAction(const BattleAction & action); + void stackReset(const CStack * stack); + void stackAdded(const CStack * stack); //new stack appeared on battlefield + void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled + void stackActivated(const CStack *stack); //active stack has been changed + void stackMoved(const CStack *stack, std::vector destHex, int distance, bool teleport); //stack with id number moved to destHex + void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked + void stackAttacking(const StackAttackInfo & attackInfo); //called when stack with id ID is attacking something on hex dest + void newRoundFirst(); + void newRound(); //caled when round is ended; + void stackIsCatapulting(const CatapultAttack & ca); //called when a stack is attacking walls + void battleFinished(const BattleResult& br, QueryID queryID); //called when battle is finished - battleresult window should be printed + void spellCast(const BattleSpellCast *sc); //called when a hero casts a spell + void battleStacksEffectsSet(const SetStackEffect & sse); //called when a specific effect is set to stacks + void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook + + void displayBattleLog(const std::vector & battleLog); + + void displaySpellAnimationQueue(const CSpell * spell, const CSpell::TAnimationQueue & q, BattleHex destinationTile, bool isHit); + void displaySpellCast(const CSpell * spell, BattleHex destinationTile); //displays spell`s cast animation + void displaySpellEffect(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation + void displaySpellHit(const CSpell * spell, BattleHex destinationTile); //displays spell`s affected animation + + void endAction(const BattleAction & action); + + void obstaclePlaced(const std::vector> oi); + void obstacleRemoved(const std::vector & obstacles); + + void gateStateChanged(const EGateState state); + + const CGHeroInstance *currentHero() const; + InfoAboutHero enemyHero() const; +}; diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index feac0006b..483a947b2 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -1,921 +1,921 @@ -/* - * BattleInterfaceClasses.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 "BattleInterfaceClasses.h" - -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleRenderer.h" -#include "BattleSiegeController.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#include "BattleWindow.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../CVideoHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/MouseButton.h" -#include "../gui/WindowHandler.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/IFont.h" -#include "../render/Graphics.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" -#include "../windows/CMessage.h" -#include "../windows/CSpellWindow.h" -#include "../render/CAnimation.h" -#include "../render/IRenderHandler.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../CCallback.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/NetPacks.h" -#include "../../lib/StartInfo.h" -#include "../../lib/CondSh.h" -#include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/TextOperations.h" - -void BattleConsole::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - Point line1 (pos.x + pos.w/2, pos.y + 8); - Point line2 (pos.x + pos.w/2, pos.y + 24); - - auto visibleText = getVisibleText(); - - if(visibleText.size() > 0) - to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]); - - if(visibleText.size() > 1) - to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]); -} - -std::vector BattleConsole::getVisibleText() -{ - // high priority texts that hide battle log entries - for(const auto & text : {consoleText, hoverText}) - { - if (text.empty()) - continue; - - auto result = CMessage::breakText(text, pos.w, FONT_SMALL); - - if(result.size() > 2) - result.resize(2); - return result; - } - - // log is small enough to fit entirely - display it as such - if (logEntries.size() < 3) - return logEntries; - - return { logEntries[scrollPosition - 1], logEntries[scrollPosition] }; -} - -std::vector BattleConsole::splitText(const std::string &text) -{ - std::vector lines; - std::vector output; - - boost::split(lines, text, boost::is_any_of("\n")); - - for(const auto & line : lines) - { - if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) - { - output.push_back(line); - } - else - { - std::vector substrings = CMessage::breakText(line, pos.w, FONT_SMALL); - output.insert(output.end(), substrings.begin(), substrings.end()); - } - } - return output; -} - -bool BattleConsole::addText(const std::string & text) -{ - logGlobal->trace("CBattleConsole message: %s", text); - - auto newLines = splitText(text); - - logEntries.insert(logEntries.end(), newLines.begin(), newLines.end()); - scrollPosition = (int)logEntries.size()-1; - redraw(); - return true; -} -void BattleConsole::scrollUp(ui32 by) -{ - if(scrollPosition > static_cast(by)) - scrollPosition -= by; - redraw(); -} - -void BattleConsole::scrollDown(ui32 by) -{ - if(scrollPosition + by < logEntries.size()) - scrollPosition += by; - redraw(); -} - -BattleConsole::BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size) - : scrollPosition(-1) - , enteringText(false) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += objectPos; - pos.w = size.x; - pos.h = size.y; - - background = std::make_shared(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 ); -} - -void BattleConsole::deactivate() -{ - if (enteringText) - LOCPLINT->cingconsole->endEnteringText(false); - - CIntObject::deactivate(); -} - -void BattleConsole::setEnteringMode(bool on) -{ - consoleText.clear(); - - if (on) - { - assert(enteringText == false); - GH.startTextInput(pos); - } - else - { - assert(enteringText == true); - GH.stopTextInput(); - } - enteringText = on; - redraw(); -} - -void BattleConsole::setEnteredText(const std::string & text) -{ - assert(enteringText == true); - consoleText = text; - redraw(); -} - -void BattleConsole::write(const std::string & Text) -{ - hoverText = Text; - redraw(); -} - -void BattleConsole::clearIfMatching(const std::string & Text) -{ - if (hoverText == Text) - clear(); -} - -void BattleConsole::clear() -{ - write({}); -} - -const CGHeroInstance * BattleHero::instance() -{ - return hero; -} - -void BattleHero::tick(uint32_t msPassed) -{ - size_t groupIndex = static_cast(phase); - - float timePassed = msPassed / 1000.f; - - flagCurrentFrame += currentSpeed * timePassed; - currentFrame += currentSpeed * timePassed; - - if(flagCurrentFrame >= flagAnimation->size(0)) - flagCurrentFrame -= flagAnimation->size(0); - - if(currentFrame >= animation->size(groupIndex)) - { - currentFrame -= animation->size(groupIndex); - switchToNextPhase(); - } -} - -void BattleHero::render(Canvas & canvas) -{ - size_t groupIndex = static_cast(phase); - - auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true); - auto heroFrame = animation->getImage(currentFrame, groupIndex, true); - - Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2; - Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2; - - if(defender) - flagPosition += Point(-4, -41); - else - flagPosition += Point(4, -41); - - canvas.draw(flagFrame, flagPosition); - canvas.draw(heroFrame, heroPosition); -} - -void BattleHero::pause() -{ - currentSpeed = 0.f; -} - -void BattleHero::play() -{ - //H3 speed: 10 fps ( 100 ms per frame) - currentSpeed = 10.f; -} - -float BattleHero::getFrame() const -{ - return currentFrame; -} - -void BattleHero::collectRenderableObjects(BattleRenderer & renderer) -{ - auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0); - - renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas) - { - render(canvas); - }); -} - -void BattleHero::onPhaseFinished(const std::function & callback) -{ - phaseFinishedCallback = callback; -} - -void BattleHero::setPhase(EHeroAnimType newPhase) -{ - nextPhase = newPhase; - switchToNextPhase(); //immediately switch to next phase and then restore idling phase - nextPhase = EHeroAnimType::HOLDING; -} - -void BattleHero::heroLeftClicked() -{ - if(owner.actionsController->spellcastingModeActive()) //we are casting a spell - return; - - if(!hero || !owner.makingTurn()) - return; - - if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions - { - CCS->curh->set(Cursor::Map::POINTER); - GH.windows().createAndPushWindow(hero, owner.getCurrentPlayerInterface()); - } -} - -void BattleHero::heroRightClicked() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool()) - return; - - Point windowPosition; - if(GH.screenDimensions().x < 1000) - { - windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79; - windowPosition.y = owner.fieldController->pos.y + 135; - } - else - { - windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15; - windowPosition.y = owner.fieldController->pos.y; - } - - InfoAboutHero targetHero; - if(owner.makingTurn() || settings["session"]["spectate"].Bool()) - { - auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance; - targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); - GH.windows().createAndPushWindow(targetHero, &windowPosition); - } -} - -void BattleHero::switchToNextPhase() -{ - phase = nextPhase; - currentFrame = 0.f; - - auto copy = phaseFinishedCallback; - phaseFinishedCallback.clear(); - copy(); -} - -BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender): - defender(defender), - hero(hero), - owner(owner), - phase(EHeroAnimType::HOLDING), - nextPhase(EHeroAnimType::HOLDING), - currentSpeed(0.f), - currentFrame(0.f), - flagCurrentFrame(0.f) -{ - AnimationPath animationPath; - - if(!hero->type->battleImage.empty()) - animationPath = hero->type->battleImage; - else - if(hero->gender == EHeroGender::FEMALE) - animationPath = hero->type->heroClass->imageBattleFemale; - else - animationPath = hero->type->heroClass->imageBattleMale; - - animation = GH.renderHandler().loadAnimation(animationPath); - animation->preload(); - - pos.w = 64; - pos.h = 136; - pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0); - pos.y = owner.fieldController->pos.y; - - if(defender) - animation->verticalFlip(); - - if(defender) - flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR")); - else - flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL")); - - flagAnimation->preload(); - flagAnimation->playerColored(hero->tempOwner); - - switchToNextPhase(); - play(); - - addUsedEvents(TIME); -} - -HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground) - : CIntObject(0) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if (position != nullptr) - moveTo(*position); - - if(initializeBackground) - { - background = std::make_shared(ImagePath::builtin("CHRPOP")); - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - background->colorize(hero.owner); - } - - initializeData(hero); -} - -void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto attack = hero.details->primskills[0]; - auto defense = hero.details->primskills[1]; - auto power = hero.details->primskills[2]; - auto knowledge = hero.details->primskills[3]; - auto morale = hero.details->morale; - auto luck = hero.details->luck; - auto currentSpellPoints = hero.details->mana; - auto maxSpellPoints = hero.details->manaLimit; - - icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6)); - - //primary stats - labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); - labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); - labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":")); - labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":")); - - labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack))); - labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense))); - labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power))); - labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge))); - - //morale+luck - labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); - labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); - - icons.push_back(std::make_shared(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131)); - icons.push_back(std::make_shared(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143)); - - //spell points - labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); - labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); -} - -void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo) -{ - icons.clear(); - labels.clear(); - - initializeData(updatedInfo); -} - -void HeroInfoBasicPanel::show(Canvas & to) -{ - showAll(to); - CIntObject::show(to); -} - -HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) - : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if (position != nullptr) - moveTo(*position); - - background->colorize(hero.owner); //maybe add this functionality to base class? - - content = std::make_shared(hero, nullptr, false); -} - -BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) - : owner(_owner), currentVideo(BattleResultVideo::NONE) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - background = std::make_shared(ImagePath::builtin("CPRESULT")); - background->colorize(owner.playerID); - pos = center(background->pos); - - exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); - exit->setBorderColor(Colors::METALLIC_GOLD); - - if(allowReplay) - { - repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); - repeat->setBorderColor(Colors::METALLIC_GOLD); - labels.push_back(std::make_shared(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel"))); - } - - if(br.winner == 0) //attacker won - { - labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); - } - else - { - labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); - } - - if(br.winner == 1) - { - labels.push_back(std::make_shared(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); - } - else - { - labels.push_back(std::make_shared(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); - } - - labels.push_back(std::make_shared(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407])); - labels.push_back(std::make_shared(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408])); - labels.push_back(std::make_shared(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409])); - - std::string sideNames[2] = {"N/A", "N/A"}; - - for(int i = 0; i < 2; i++) - { - auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i); - const int xs[] = {21, 392}; - - if(heroInfo.portraitSource.isValid()) //attacking hero - { - icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38)); - sideNames[i] = heroInfo.name; - } - else - { - auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks(); - vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison - { - return stack->unitSide() != i || !stack->base; - }); - - auto best = vstd::maxElementByFun(stacks, [](const CStack * stack) - { - return stack->unitType()->getAIValue(); - }); - - if(best != stacks.end()) //should be always but to be safe... - { - icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); - sideNames[i] = (*best)->unitType()->getNamePluralTranslated(); - } - } - } - - //printing attacker and defender's names - labels.push_back(std::make_shared(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0])); - labels.push_back(std::make_shared(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1])); - - //printing casualties - for(int step = 0; step < 2; ++step) - { - if(br.casualties[step].size()==0) - { - labels.push_back(std::make_shared(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523])); - } - else - { - int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture - int yPos = 344 + step * 97; - for(auto & elem : br.casualties[step]) - { - auto creature = CGI->creatures()->getByIndex(elem.first); - if (creature->getId() == CreatureID::ARROW_TOWERS ) - continue; // do not show destroyed towers in battle results - - icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos)); - std::ostringstream amount; - amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); - xPos += 42; - } - } - } - //printing result description - bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); - if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won - { - int text = 304; - currentVideo = BattleResultVideo::WIN; - switch(br.result) - { - case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::WIN_SIEGE; - break; - case EBattleResult::ESCAPE: - text = 303; - break; - case EBattleResult::SURRENDER: - text = 302; - break; - default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; - } - playVideo(); - - std::string str = CGI->generaltexth->allTexts[text]; - - const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); - if (ourHero) - { - str += CGI->generaltexth->allTexts[305]; - boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated()); - boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1])); - } - - description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - else // we lose - { - int text = 311; - currentVideo = BattleResultVideo::DEFEAT; - switch(br.result) - { - case EBattleResult::NORMAL: - if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) - currentVideo = BattleResultVideo::DEFEAT_SIEGE; - break; - case EBattleResult::ESCAPE: - currentVideo = BattleResultVideo::RETREAT; - text = 310; - break; - case EBattleResult::SURRENDER: - currentVideo = BattleResultVideo::SURRENDER; - text = 309; - break; - default: - logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); - break; - } - playVideo(); - - labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); - } -} - -void BattleResultWindow::activate() -{ - owner.showingDialog->set(true); - CIntObject::activate(); -} - -void BattleResultWindow::show(Canvas & to) -{ - CIntObject::show(to); - CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, - [&]() - { - playVideo(true); - }); -} - -void BattleResultWindow::playVideo(bool startLoop) -{ - AudioPath musicName = AudioPath(); - VideoPath videoName = VideoPath(); - - if(!startLoop) - { - switch(currentVideo) - { - case BattleResultVideo::WIN: - musicName = AudioPath::builtin("Music/Win Battle"); - videoName = VideoPath::builtin("WIN3.BIK"); - break; - case BattleResultVideo::SURRENDER: - musicName = AudioPath::builtin("Music/Surrender Battle"); - videoName = VideoPath::builtin("SURRENDER.BIK"); - break; - case BattleResultVideo::RETREAT: - musicName = AudioPath::builtin("Music/Retreat Battle"); - videoName = VideoPath::builtin("RTSTART.BIK"); - break; - case BattleResultVideo::DEFEAT: - musicName = AudioPath::builtin("Music/LoseCombat"); - videoName = VideoPath::builtin("LBSTART.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - musicName = AudioPath::builtin("Music/LoseCastle"); - videoName = VideoPath::builtin("LOSECSTL.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - musicName = AudioPath::builtin("Music/Defend Castle"); - videoName = VideoPath::builtin("DEFENDALL.BIK"); - break; - } - } - else - { - switch(currentVideo) - { - case BattleResultVideo::RETREAT: - currentVideo = BattleResultVideo::RETREAT_LOOP; - videoName = VideoPath::builtin("RTLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT: - currentVideo = BattleResultVideo::DEFEAT_LOOP; - videoName = VideoPath::builtin("LBLOOP.BIK"); - break; - case BattleResultVideo::DEFEAT_SIEGE: - currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP; - videoName = VideoPath::builtin("LOSECSLP.BIK"); - break; - case BattleResultVideo::WIN_SIEGE: - currentVideo = BattleResultVideo::WIN_SIEGE_LOOP; - videoName = VideoPath::builtin("DEFENDLOOP.BIK"); - break; - } - } - - if(musicName != AudioPath()) - CCS->musich->playMusic(musicName, false, true); - - if(videoName != VideoPath()) - CCS->videoh->open(videoName); -} - -void BattleResultWindow::buttonPressed(int button) -{ - if (resultCallback) - resultCallback(button); - - CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon - - close(); - - if(GH.windows().topWindow()) - GH.windows().popWindows(1); //pop battle interface if present - - //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, - //so we can be sure that there is no dialogs left on GUI stack. - intTmp.showingDialog->setn(false); - CCS->videoh->close(); -} - -void BattleResultWindow::bExitf() -{ - buttonPressed(0); -} - -void BattleResultWindow::bRepeatf() -{ - buttonPressed(1); -} - -StackQueue::StackQueue(bool Embedded, BattleInterface & owner) - : embedded(Embedded), - owner(owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(embedded) - { - pos.w = QUEUE_SIZE * 41; - pos.h = 49; - pos.x += parent->pos.w/2 - pos.w/2; - pos.y += 10; - - icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); - stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); - } - else - { - pos.w = 800; - pos.h = 85; - pos.x += 0; - pos.y -= pos.h; - - background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); - - icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT")); - stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); - //TODO: where use big icons? - //stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG"); - } - stateIcons->preload(); - - stackBoxes.resize(QUEUE_SIZE); - for (int i = 0; i < stackBoxes.size(); i++) - { - stackBoxes[i] = std::make_shared(this); - stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0)); - } -} - -void StackQueue::show(Canvas & to) -{ - if (embedded) - showAll(to); - CIntObject::show(to); -} - -void StackQueue::update() -{ - std::vector queueData; - - owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0); - - size_t boxIndex = 0; - - for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) - { - for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) - stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); - } - - for(; boxIndex < stackBoxes.size(); boxIndex++) - stackBoxes[boxIndex]->setUnit(nullptr); -} - -int32_t StackQueue::getSiegeShooterIconID() -{ - return owner.siegeController->getSiegedTown()->town->faction->getIndex(); -} - -std::optional StackQueue::getHoveredUnitIdIfAny() const -{ - for(const auto & stackBox : stackBoxes) - { - if(stackBox->isHovered()) - { - return stackBox->getBoundUnitID(); - } - } - - return std::nullopt; -} - -StackQueue::StackBox::StackBox(StackQueue * owner): - CIntObject(SHOW_POPUP | HOVER), owner(owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background = std::make_shared(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge")); - - pos.w = background->pos.w; - pos.h = background->pos.h; - - if(owner->embedded) - { - icon = std::make_shared(owner->icons, 0, 0, 5, 2); - amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - else - { - icon = std::make_shared(owner->icons, 0, 0, 9, 1); - amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - - int icon_x = pos.w - 17; - int icon_y = pos.h - 18; - - stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); - stateIcon->visible = false; - } -} - -void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) -{ - if(unit) - { - boundUnitID = unit->unitId(); - background->colorize(unit->unitOwner()); - icon->visible = true; - - // temporary code for mod compatibility: - // first, set icon that should definitely exist (arrow tower icon in base vcmi mod) - // second, try to switch to icon that should be provided by mod - // if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image - // for 1.2 release & later next line should be moved into 'else' block - icon->setFrame(unit->creatureIconIndex(), 0); - if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) - icon->setFrame(owner->getSiegeShooterIconID(), 1); - - amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); - - if(stateIcon) - { - if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1))) - { - stateIcon->setFrame(0, 0); - stateIcon->visible = true; - } - else if(unit->waited((int)turn)) - { - stateIcon->setFrame(1, 0); - stateIcon->visible = true; - } - else - { - stateIcon->visible = false; - } - } - } - else - { - boundUnitID = std::nullopt; - background->colorize(PlayerColor::NEUTRAL); - icon->visible = false; - icon->setFrame(0); - amount->setText(""); - - if(stateIcon) - stateIcon->visible = false; - } -} - -std::optional StackQueue::StackBox::getBoundUnitID() const -{ - return boundUnitID; -} - -bool StackQueue::StackBox::isBoundUnitHighlighted() const -{ - auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds(); - return vstd::contains(unitIdsToHighlight, getBoundUnitID()); -} - -void StackQueue::StackBox::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - if(isBoundUnitHighlighted()) - to.drawBorder(background->pos, Colors::CYAN, 2); -} - -void StackQueue::StackBox::show(Canvas & to) -{ - CIntObject::show(to); - - if(isBoundUnitHighlighted()) - to.drawBorder(background->pos, Colors::CYAN, 2); -} +/* + * BattleInterfaceClasses.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 "BattleInterfaceClasses.h" + +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleRenderer.h" +#include "BattleSiegeController.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#include "BattleWindow.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../CVideoHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/MouseButton.h" +#include "../gui/WindowHandler.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IFont.h" +#include "../render/Graphics.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../windows/CMessage.h" +#include "../windows/CSpellWindow.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../CCallback.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/NetPacks.h" +#include "../../lib/StartInfo.h" +#include "../../lib/CondSh.h" +#include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/TextOperations.h" + +void BattleConsole::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + Point line1 (pos.x + pos.w/2, pos.y + 8); + Point line2 (pos.x + pos.w/2, pos.y + 24); + + auto visibleText = getVisibleText(); + + if(visibleText.size() > 0) + to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]); + + if(visibleText.size() > 1) + to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]); +} + +std::vector BattleConsole::getVisibleText() +{ + // high priority texts that hide battle log entries + for(const auto & text : {consoleText, hoverText}) + { + if (text.empty()) + continue; + + auto result = CMessage::breakText(text, pos.w, FONT_SMALL); + + if(result.size() > 2) + result.resize(2); + return result; + } + + // log is small enough to fit entirely - display it as such + if (logEntries.size() < 3) + return logEntries; + + return { logEntries[scrollPosition - 1], logEntries[scrollPosition] }; +} + +std::vector BattleConsole::splitText(const std::string &text) +{ + std::vector lines; + std::vector output; + + boost::split(lines, text, boost::is_any_of("\n")); + + for(const auto & line : lines) + { + if (graphics->fonts[FONT_SMALL]->getStringWidth(text) < pos.w) + { + output.push_back(line); + } + else + { + std::vector substrings = CMessage::breakText(line, pos.w, FONT_SMALL); + output.insert(output.end(), substrings.begin(), substrings.end()); + } + } + return output; +} + +bool BattleConsole::addText(const std::string & text) +{ + logGlobal->trace("CBattleConsole message: %s", text); + + auto newLines = splitText(text); + + logEntries.insert(logEntries.end(), newLines.begin(), newLines.end()); + scrollPosition = (int)logEntries.size()-1; + redraw(); + return true; +} +void BattleConsole::scrollUp(ui32 by) +{ + if(scrollPosition > static_cast(by)) + scrollPosition -= by; + redraw(); +} + +void BattleConsole::scrollDown(ui32 by) +{ + if(scrollPosition + by < logEntries.size()) + scrollPosition += by; + redraw(); +} + +BattleConsole::BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size) + : scrollPosition(-1) + , enteringText(false) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += objectPos; + pos.w = size.x; + pos.h = size.y; + + background = std::make_shared(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 ); +} + +void BattleConsole::deactivate() +{ + if (enteringText) + LOCPLINT->cingconsole->endEnteringText(false); + + CIntObject::deactivate(); +} + +void BattleConsole::setEnteringMode(bool on) +{ + consoleText.clear(); + + if (on) + { + assert(enteringText == false); + GH.startTextInput(pos); + } + else + { + assert(enteringText == true); + GH.stopTextInput(); + } + enteringText = on; + redraw(); +} + +void BattleConsole::setEnteredText(const std::string & text) +{ + assert(enteringText == true); + consoleText = text; + redraw(); +} + +void BattleConsole::write(const std::string & Text) +{ + hoverText = Text; + redraw(); +} + +void BattleConsole::clearIfMatching(const std::string & Text) +{ + if (hoverText == Text) + clear(); +} + +void BattleConsole::clear() +{ + write({}); +} + +const CGHeroInstance * BattleHero::instance() +{ + return hero; +} + +void BattleHero::tick(uint32_t msPassed) +{ + size_t groupIndex = static_cast(phase); + + float timePassed = msPassed / 1000.f; + + flagCurrentFrame += currentSpeed * timePassed; + currentFrame += currentSpeed * timePassed; + + if(flagCurrentFrame >= flagAnimation->size(0)) + flagCurrentFrame -= flagAnimation->size(0); + + if(currentFrame >= animation->size(groupIndex)) + { + currentFrame -= animation->size(groupIndex); + switchToNextPhase(); + } +} + +void BattleHero::render(Canvas & canvas) +{ + size_t groupIndex = static_cast(phase); + + auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true); + auto heroFrame = animation->getImage(currentFrame, groupIndex, true); + + Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2; + Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2; + + if(defender) + flagPosition += Point(-4, -41); + else + flagPosition += Point(4, -41); + + canvas.draw(flagFrame, flagPosition); + canvas.draw(heroFrame, heroPosition); +} + +void BattleHero::pause() +{ + currentSpeed = 0.f; +} + +void BattleHero::play() +{ + //H3 speed: 10 fps ( 100 ms per frame) + currentSpeed = 10.f; +} + +float BattleHero::getFrame() const +{ + return currentFrame; +} + +void BattleHero::collectRenderableObjects(BattleRenderer & renderer) +{ + auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0); + + renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas) + { + render(canvas); + }); +} + +void BattleHero::onPhaseFinished(const std::function & callback) +{ + phaseFinishedCallback = callback; +} + +void BattleHero::setPhase(EHeroAnimType newPhase) +{ + nextPhase = newPhase; + switchToNextPhase(); //immediately switch to next phase and then restore idling phase + nextPhase = EHeroAnimType::HOLDING; +} + +void BattleHero::heroLeftClicked() +{ + if(owner.actionsController->spellcastingModeActive()) //we are casting a spell + return; + + if(!hero || !owner.makingTurn()) + return; + + if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions + { + CCS->curh->set(Cursor::Map::POINTER); + GH.windows().createAndPushWindow(hero, owner.getCurrentPlayerInterface()); + } +} + +void BattleHero::heroRightClicked() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool()) + return; + + Point windowPosition; + if(GH.screenDimensions().x < 1000) + { + windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79; + windowPosition.y = owner.fieldController->pos.y + 135; + } + else + { + windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15; + windowPosition.y = owner.fieldController->pos.y; + } + + InfoAboutHero targetHero; + if(owner.makingTurn() || settings["session"]["spectate"].Bool()) + { + auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance; + targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE); + GH.windows().createAndPushWindow(targetHero, &windowPosition); + } +} + +void BattleHero::switchToNextPhase() +{ + phase = nextPhase; + currentFrame = 0.f; + + auto copy = phaseFinishedCallback; + phaseFinishedCallback.clear(); + copy(); +} + +BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender): + defender(defender), + hero(hero), + owner(owner), + phase(EHeroAnimType::HOLDING), + nextPhase(EHeroAnimType::HOLDING), + currentSpeed(0.f), + currentFrame(0.f), + flagCurrentFrame(0.f) +{ + AnimationPath animationPath; + + if(!hero->type->battleImage.empty()) + animationPath = hero->type->battleImage; + else + if(hero->gender == EHeroGender::FEMALE) + animationPath = hero->type->heroClass->imageBattleFemale; + else + animationPath = hero->type->heroClass->imageBattleMale; + + animation = GH.renderHandler().loadAnimation(animationPath); + animation->preload(); + + pos.w = 64; + pos.h = 136; + pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0); + pos.y = owner.fieldController->pos.y; + + if(defender) + animation->verticalFlip(); + + if(defender) + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR")); + else + flagAnimation = GH.renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL")); + + flagAnimation->preload(); + flagAnimation->playerColored(hero->tempOwner); + + switchToNextPhase(); + play(); + + addUsedEvents(TIME); +} + +HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground) + : CIntObject(0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if (position != nullptr) + moveTo(*position); + + if(initializeBackground) + { + background = std::make_shared(ImagePath::builtin("CHRPOP")); + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + background->colorize(hero.owner); + } + + initializeData(hero); +} + +void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + auto attack = hero.details->primskills[0]; + auto defense = hero.details->primskills[1]; + auto power = hero.details->primskills[2]; + auto knowledge = hero.details->primskills[3]; + auto morale = hero.details->morale; + auto luck = hero.details->luck; + auto currentSpellPoints = hero.details->mana; + auto maxSpellPoints = hero.details->manaLimit; + + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6)); + + //primary stats + labels.push_back(std::make_shared(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[380] + ":")); + labels.push_back(std::make_shared(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[381] + ":")); + labels.push_back(std::make_shared(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[382] + ":")); + labels.push_back(std::make_shared(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[383] + ":")); + + labels.push_back(std::make_shared(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack))); + labels.push_back(std::make_shared(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense))); + labels.push_back(std::make_shared(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power))); + labels.push_back(std::make_shared(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge))); + + //morale+luck + labels.push_back(std::make_shared(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[384] + ":")); + labels.push_back(std::make_shared(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->allTexts[385] + ":")); + + icons.push_back(std::make_shared(AnimationPath::builtin("IMRL22"), morale + 3, 0, 47, 131)); + icons.push_back(std::make_shared(AnimationPath::builtin("ILCK22"), luck + 3, 0, 47, 143)); + + //spell points + labels.push_back(std::make_shared(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[387])); + labels.push_back(std::make_shared(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints))); +} + +void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo) +{ + icons.clear(); + labels.clear(); + + initializeData(updatedInfo); +} + +void HeroInfoBasicPanel::show(Canvas & to) +{ + showAll(to); + CIntObject::show(to); +} + +HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position) + : CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if (position != nullptr) + moveTo(*position); + + background->colorize(hero.owner); //maybe add this functionality to base class? + + content = std::make_shared(hero, nullptr, false); +} + +BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay) + : owner(_owner), currentVideo(BattleResultVideo::NONE) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + background = std::make_shared(ImagePath::builtin("CPRESULT")); + background->colorize(owner.playerID); + pos = center(background->pos); + + exit = std::make_shared(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT); + exit->setBorderColor(Colors::METALLIC_GOLD); + + if(allowReplay) + { + repeat = std::make_shared(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL); + repeat->setBorderColor(Colors::METALLIC_GOLD); + labels.push_back(std::make_shared(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel"))); + } + + if(br.winner == 0) //attacker won + { + labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); + } + else + { + labels.push_back(std::make_shared(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); + } + + if(br.winner == 1) + { + labels.push_back(std::make_shared(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[410])); + } + else + { + labels.push_back(std::make_shared(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[411])); + } + + labels.push_back(std::make_shared(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[407])); + labels.push_back(std::make_shared(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[408])); + labels.push_back(std::make_shared(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[409])); + + std::string sideNames[2] = {"N/A", "N/A"}; + + for(int i = 0; i < 2; i++) + { + auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i); + const int xs[] = {21, 392}; + + if(heroInfo.portraitSource.isValid()) //attacking hero + { + icons.push_back(std::make_shared(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[i], 38)); + sideNames[i] = heroInfo.name; + } + else + { + auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks(); + vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison + { + return stack->unitSide() != i || !stack->base; + }); + + auto best = vstd::maxElementByFun(stacks, [](const CStack * stack) + { + return stack->unitType()->getAIValue(); + }); + + if(best != stacks.end()) //should be always but to be safe... + { + icons.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[i], 38)); + sideNames[i] = (*best)->unitType()->getNamePluralTranslated(); + } + } + } + + //printing attacker and defender's names + labels.push_back(std::make_shared(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0])); + labels.push_back(std::make_shared(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1])); + + //printing casualties + for(int step = 0; step < 2; ++step) + { + if(br.casualties[step].size()==0) + { + labels.push_back(std::make_shared(235, 360 + 97 * step, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[523])); + } + else + { + int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture + int yPos = 344 + step * 97; + for(auto & elem : br.casualties[step]) + { + auto creature = CGI->creatures()->getByIndex(elem.first); + if (creature->getId() == CreatureID::ARROW_TOWERS ) + continue; // do not show destroyed towers in battle results + + icons.push_back(std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos)); + std::ostringstream amount; + amount<(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str())); + xPos += 42; + } + } + } + //printing result description + bool weAreAttacker = !(owner.cb->getBattle(br.battleID)->battleGetMySide()); + if((br.winner == 0 && weAreAttacker) || (br.winner == 1 && !weAreAttacker)) //we've won + { + int text = 304; + currentVideo = BattleResultVideo::WIN; + switch(br.result) + { + case EBattleResult::NORMAL: + if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) + currentVideo = BattleResultVideo::WIN_SIEGE; + break; + case EBattleResult::ESCAPE: + text = 303; + break; + case EBattleResult::SURRENDER: + text = 302; + break; + default: + logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); + break; + } + playVideo(); + + std::string str = CGI->generaltexth->allTexts[text]; + + const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero(); + if (ourHero) + { + str += CGI->generaltexth->allTexts[305]; + boost::algorithm::replace_first(str, "%s", ourHero->getNameTranslated()); + boost::algorithm::replace_first(str, "%d", std::to_string(br.exp[weAreAttacker ? 0 : 1])); + } + + description = std::make_shared(str, Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + } + else // we lose + { + int text = 311; + currentVideo = BattleResultVideo::DEFEAT; + switch(br.result) + { + case EBattleResult::NORMAL: + if(owner.cb->getBattle(br.battleID)->battleGetDefendedTown() && !weAreAttacker) + currentVideo = BattleResultVideo::DEFEAT_SIEGE; + break; + case EBattleResult::ESCAPE: + currentVideo = BattleResultVideo::RETREAT; + text = 310; + break; + case EBattleResult::SURRENDER: + currentVideo = BattleResultVideo::SURRENDER; + text = 309; + break; + default: + logGlobal->error("Invalid battle result code %d. Assumed normal.", static_cast(br.result)); + break; + } + playVideo(); + + labels.push_back(std::make_shared(235, 235, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[text])); + } +} + +void BattleResultWindow::activate() +{ + owner.showingDialog->set(true); + CIntObject::activate(); +} + +void BattleResultWindow::show(Canvas & to) +{ + CIntObject::show(to); + CCS->videoh->update(pos.x + 107, pos.y + 70, to.getInternalSurface(), true, false, + [&]() + { + playVideo(true); + }); +} + +void BattleResultWindow::playVideo(bool startLoop) +{ + AudioPath musicName = AudioPath(); + VideoPath videoName = VideoPath(); + + if(!startLoop) + { + switch(currentVideo) + { + case BattleResultVideo::WIN: + musicName = AudioPath::builtin("Music/Win Battle"); + videoName = VideoPath::builtin("WIN3.BIK"); + break; + case BattleResultVideo::SURRENDER: + musicName = AudioPath::builtin("Music/Surrender Battle"); + videoName = VideoPath::builtin("SURRENDER.BIK"); + break; + case BattleResultVideo::RETREAT: + musicName = AudioPath::builtin("Music/Retreat Battle"); + videoName = VideoPath::builtin("RTSTART.BIK"); + break; + case BattleResultVideo::DEFEAT: + musicName = AudioPath::builtin("Music/LoseCombat"); + videoName = VideoPath::builtin("LBSTART.BIK"); + break; + case BattleResultVideo::DEFEAT_SIEGE: + musicName = AudioPath::builtin("Music/LoseCastle"); + videoName = VideoPath::builtin("LOSECSTL.BIK"); + break; + case BattleResultVideo::WIN_SIEGE: + musicName = AudioPath::builtin("Music/Defend Castle"); + videoName = VideoPath::builtin("DEFENDALL.BIK"); + break; + } + } + else + { + switch(currentVideo) + { + case BattleResultVideo::RETREAT: + currentVideo = BattleResultVideo::RETREAT_LOOP; + videoName = VideoPath::builtin("RTLOOP.BIK"); + break; + case BattleResultVideo::DEFEAT: + currentVideo = BattleResultVideo::DEFEAT_LOOP; + videoName = VideoPath::builtin("LBLOOP.BIK"); + break; + case BattleResultVideo::DEFEAT_SIEGE: + currentVideo = BattleResultVideo::DEFEAT_SIEGE_LOOP; + videoName = VideoPath::builtin("LOSECSLP.BIK"); + break; + case BattleResultVideo::WIN_SIEGE: + currentVideo = BattleResultVideo::WIN_SIEGE_LOOP; + videoName = VideoPath::builtin("DEFENDLOOP.BIK"); + break; + } + } + + if(musicName != AudioPath()) + CCS->musich->playMusic(musicName, false, true); + + if(videoName != VideoPath()) + CCS->videoh->open(videoName); +} + +void BattleResultWindow::buttonPressed(int button) +{ + if (resultCallback) + resultCallback(button); + + CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon + + close(); + + if(GH.windows().topWindow()) + GH.windows().popWindows(1); //pop battle interface if present + + //Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle, + //so we can be sure that there is no dialogs left on GUI stack. + intTmp.showingDialog->setn(false); + CCS->videoh->close(); +} + +void BattleResultWindow::bExitf() +{ + buttonPressed(0); +} + +void BattleResultWindow::bRepeatf() +{ + buttonPressed(1); +} + +StackQueue::StackQueue(bool Embedded, BattleInterface & owner) + : embedded(Embedded), + owner(owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(embedded) + { + pos.w = QUEUE_SIZE * 41; + pos.h = 49; + pos.x += parent->pos.w/2 - pos.w/2; + pos.y += 10; + + icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("CPRSMALL")); + stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); + } + else + { + pos.w = 800; + pos.h = 85; + pos.x += 0; + pos.y -= pos.h; + + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + + icons = GH.renderHandler().loadAnimation(AnimationPath::builtin("TWCRPORT")); + stateIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("VCMI/BATTLEQUEUE/STATESSMALL")); + //TODO: where use big icons? + //stateIcons = GH.renderHandler().loadAnimation("VCMI/BATTLEQUEUE/STATESBIG"); + } + stateIcons->preload(); + + stackBoxes.resize(QUEUE_SIZE); + for (int i = 0; i < stackBoxes.size(); i++) + { + stackBoxes[i] = std::make_shared(this); + stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0)); + } +} + +void StackQueue::show(Canvas & to) +{ + if (embedded) + showAll(to); + CIntObject::show(to); +} + +void StackQueue::update() +{ + std::vector queueData; + + owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0); + + size_t boxIndex = 0; + + for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++) + { + for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++) + stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn); + } + + for(; boxIndex < stackBoxes.size(); boxIndex++) + stackBoxes[boxIndex]->setUnit(nullptr); +} + +int32_t StackQueue::getSiegeShooterIconID() +{ + return owner.siegeController->getSiegedTown()->town->faction->getIndex(); +} + +std::optional StackQueue::getHoveredUnitIdIfAny() const +{ + for(const auto & stackBox : stackBoxes) + { + if(stackBox->isHovered()) + { + return stackBox->getBoundUnitID(); + } + } + + return std::nullopt; +} + +StackQueue::StackBox::StackBox(StackQueue * owner): + CIntObject(SHOW_POPUP | HOVER), owner(owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + background = std::make_shared(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge")); + + pos.w = background->pos.w; + pos.h = background->pos.h; + + if(owner->embedded) + { + icon = std::make_shared(owner->icons, 0, 0, 5, 2); + amount = std::make_shared(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + } + else + { + icon = std::make_shared(owner->icons, 0, 0, 9, 1); + amount = std::make_shared(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + + int icon_x = pos.w - 17; + int icon_y = pos.h - 18; + + stateIcon = std::make_shared(owner->stateIcons, 0, 0, icon_x, icon_y); + stateIcon->visible = false; + } +} + +void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn) +{ + if(unit) + { + boundUnitID = unit->unitId(); + background->colorize(unit->unitOwner()); + icon->visible = true; + + // temporary code for mod compatibility: + // first, set icon that should definitely exist (arrow tower icon in base vcmi mod) + // second, try to switch to icon that should be provided by mod + // if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image + // for 1.2 release & later next line should be moved into 'else' block + icon->setFrame(unit->creatureIconIndex(), 0); + if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS) + icon->setFrame(owner->getSiegeShooterIconID(), 1); + + amount->setText(TextOperations::formatMetric(unit->getCount(), 4)); + + if(stateIcon) + { + if(unit->defended((int)turn) || (turn > 0 && unit->defended((int)turn - 1))) + { + stateIcon->setFrame(0, 0); + stateIcon->visible = true; + } + else if(unit->waited((int)turn)) + { + stateIcon->setFrame(1, 0); + stateIcon->visible = true; + } + else + { + stateIcon->visible = false; + } + } + } + else + { + boundUnitID = std::nullopt; + background->colorize(PlayerColor::NEUTRAL); + icon->visible = false; + icon->setFrame(0); + amount->setText(""); + + if(stateIcon) + stateIcon->visible = false; + } +} + +std::optional StackQueue::StackBox::getBoundUnitID() const +{ + return boundUnitID; +} + +bool StackQueue::StackBox::isBoundUnitHighlighted() const +{ + auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds(); + return vstd::contains(unitIdsToHighlight, getBoundUnitID()); +} + +void StackQueue::StackBox::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + if(isBoundUnitHighlighted()) + to.drawBorder(background->pos, Colors::CYAN, 2); +} + +void StackQueue::StackBox::show(Canvas & to) +{ + CIntObject::show(to); + + if(isBoundUnitHighlighted()) + to.drawBorder(background->pos, Colors::CYAN, 2); +} diff --git a/client/battle/BattleInterfaceClasses.h b/client/battle/BattleInterfaceClasses.h index 2b395ed23..ac2431e94 100644 --- a/client/battle/BattleInterfaceClasses.h +++ b/client/battle/BattleInterfaceClasses.h @@ -1,238 +1,238 @@ -/* - * BattleInterfaceClasses.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 "BattleConstants.h" -#include "../gui/CIntObject.h" -#include "../../lib/FunctionList.h" -#include "../../lib/battle/BattleHex.h" -#include "../windows/CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -struct BattleResult; -struct InfoAboutHero; -class CStack; - -namespace battle -{ -class Unit; -} - -VCMI_LIB_NAMESPACE_END - -class CAnimation; -class Canvas; -class BattleInterface; -class CPicture; -class CFilledTexture; -class CButton; -class CToggleButton; -class CLabel; -class CTextBox; -class CAnimImage; -class CPlayerInterface; -class BattleRenderer; - -/// Class which shows the console at the bottom of the battle screen and manages the text of the console -class BattleConsole : public CIntObject, public IStatusBar -{ -private: - std::shared_ptr background; - - /// List of all texts added during battle, essentially - log of entire battle - std::vector< std::string > logEntries; - - /// Current scrolling position, to allow showing older entries via scroll buttons - int scrollPosition; - - /// current hover text set on mouse move, takes priority over log entries - std::string hoverText; - - /// current text entered via in-game console, takes priority over both log entries and hover text - std::string consoleText; - - /// if true then we are currently entering console text - bool enteringText; - - /// splits text into individual strings for battle log - std::vector splitText(const std::string &text); - - /// select line(s) that will be visible in UI - std::vector getVisibleText(); -public: - BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); - - void showAll(Canvas & to) override; - void deactivate() override; - - bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters) - void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions - void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions - - // IStatusBar interface - void write(const std::string & Text) override; - void clearIfMatching(const std::string & Text) override; - void clear() override; - void setEnteringMode(bool on) override; - void setEnteredText(const std::string & text) override; -}; - -/// Hero battle animation -class BattleHero : public CIntObject -{ - bool defender; - - CFunctionList phaseFinishedCallback; - - std::shared_ptr animation; - std::shared_ptr flagAnimation; - - const CGHeroInstance * hero; //this animation's hero instance - const BattleInterface & owner; //battle interface to which this animation is assigned - - EHeroAnimType phase; //stage of animation - EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed - - float currentSpeed; - float currentFrame; //frame of animation - float flagCurrentFrame; - - void switchToNextPhase(); - - void render(Canvas & canvas); //prints next frame of animation to to -public: - const CGHeroInstance * instance(); - - void setPhase(EHeroAnimType newPhase); //sets phase of hero animation - - void collectRenderableObjects(BattleRenderer & renderer); - void tick(uint32_t msPassed) override; - - float getFrame() const; - void onPhaseFinished(const std::function &); - - void pause(); - void play(); - - void heroLeftClicked(); - void heroRightClicked(); - - BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); -}; - -class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element -{ -private: - std::shared_ptr background; - std::vector> labels; - std::vector> icons; -public: - HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); - - void show(Canvas & to) override; - - void initializeData(const InfoAboutHero & hero); - void update(const InfoAboutHero & updatedInfo); -}; - -class HeroInfoWindow : public CWindowObject -{ -private: - std::shared_ptr content; -public: - HeroInfoWindow(const InfoAboutHero & hero, Point * position); -}; - -/// Class which is responsible for showing the battle result window -class BattleResultWindow : public WindowBase -{ -private: - std::shared_ptr background; - std::vector> labels; - std::shared_ptr exit; - std::shared_ptr repeat; - std::vector> icons; - std::shared_ptr description; - CPlayerInterface & owner; - - enum BattleResultVideo - { - NONE, - WIN, - SURRENDER, - RETREAT, - RETREAT_LOOP, - DEFEAT, - DEFEAT_LOOP, - DEFEAT_SIEGE, - DEFEAT_SIEGE_LOOP, - WIN_SIEGE, - WIN_SIEGE_LOOP, - }; - BattleResultVideo currentVideo; - - void playVideo(bool startLoop = false); - - void buttonPressed(int button); //internal function for button callbacks -public: - BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); - - void bExitf(); //exit button callback - void bRepeatf(); //repeat button callback - std::function resultCallback; //callback receiving which button was pressed - - void activate() override; - void show(Canvas & to) override; -}; - -/// Shows the stack queue -class StackQueue : public CIntObject -{ - class StackBox : public CIntObject - { - StackQueue * owner; - std::optional boundUnitID; - - std::shared_ptr background; - std::shared_ptr icon; - std::shared_ptr amount; - std::shared_ptr stateIcon; - - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - bool isBoundUnitHighlighted() const; - public: - StackBox(StackQueue * owner); - void setUnit(const battle::Unit * unit, size_t turn = 0); - std::optional getBoundUnitID() const; - - }; - - static const int QUEUE_SIZE = 10; - std::shared_ptr background; - std::vector> stackBoxes; - BattleInterface & owner; - - std::shared_ptr icons; - std::shared_ptr stateIcons; - - int32_t getSiegeShooterIconID(); -public: - const bool embedded; - - StackQueue(bool Embedded, BattleInterface & owner); - void update(); - std::optional getHoveredUnitIdIfAny() const; - - void show(Canvas & to) override; -}; +/* + * BattleInterfaceClasses.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 "BattleConstants.h" +#include "../gui/CIntObject.h" +#include "../../lib/FunctionList.h" +#include "../../lib/battle/BattleHex.h" +#include "../windows/CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +struct BattleResult; +struct InfoAboutHero; +class CStack; + +namespace battle +{ +class Unit; +} + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class Canvas; +class BattleInterface; +class CPicture; +class CFilledTexture; +class CButton; +class CToggleButton; +class CLabel; +class CTextBox; +class CAnimImage; +class CPlayerInterface; +class BattleRenderer; + +/// Class which shows the console at the bottom of the battle screen and manages the text of the console +class BattleConsole : public CIntObject, public IStatusBar +{ +private: + std::shared_ptr background; + + /// List of all texts added during battle, essentially - log of entire battle + std::vector< std::string > logEntries; + + /// Current scrolling position, to allow showing older entries via scroll buttons + int scrollPosition; + + /// current hover text set on mouse move, takes priority over log entries + std::string hoverText; + + /// current text entered via in-game console, takes priority over both log entries and hover text + std::string consoleText; + + /// if true then we are currently entering console text + bool enteringText; + + /// splits text into individual strings for battle log + std::vector splitText(const std::string &text); + + /// select line(s) that will be visible in UI + std::vector getVisibleText(); +public: + BattleConsole(std::shared_ptr backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size); + + void showAll(Canvas & to) override; + void deactivate() override; + + bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters) + void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions + void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions + + // IStatusBar interface + void write(const std::string & Text) override; + void clearIfMatching(const std::string & Text) override; + void clear() override; + void setEnteringMode(bool on) override; + void setEnteredText(const std::string & text) override; +}; + +/// Hero battle animation +class BattleHero : public CIntObject +{ + bool defender; + + CFunctionList phaseFinishedCallback; + + std::shared_ptr animation; + std::shared_ptr flagAnimation; + + const CGHeroInstance * hero; //this animation's hero instance + const BattleInterface & owner; //battle interface to which this animation is assigned + + EHeroAnimType phase; //stage of animation + EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed + + float currentSpeed; + float currentFrame; //frame of animation + float flagCurrentFrame; + + void switchToNextPhase(); + + void render(Canvas & canvas); //prints next frame of animation to to +public: + const CGHeroInstance * instance(); + + void setPhase(EHeroAnimType newPhase); //sets phase of hero animation + + void collectRenderableObjects(BattleRenderer & renderer); + void tick(uint32_t msPassed) override; + + float getFrame() const; + void onPhaseFinished(const std::function &); + + void pause(); + void play(); + + void heroLeftClicked(); + void heroRightClicked(); + + BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender); +}; + +class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element +{ +private: + std::shared_ptr background; + std::vector> labels; + std::vector> icons; +public: + HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true); + + void show(Canvas & to) override; + + void initializeData(const InfoAboutHero & hero); + void update(const InfoAboutHero & updatedInfo); +}; + +class HeroInfoWindow : public CWindowObject +{ +private: + std::shared_ptr content; +public: + HeroInfoWindow(const InfoAboutHero & hero, Point * position); +}; + +/// Class which is responsible for showing the battle result window +class BattleResultWindow : public WindowBase +{ +private: + std::shared_ptr background; + std::vector> labels; + std::shared_ptr exit; + std::shared_ptr repeat; + std::vector> icons; + std::shared_ptr description; + CPlayerInterface & owner; + + enum BattleResultVideo + { + NONE, + WIN, + SURRENDER, + RETREAT, + RETREAT_LOOP, + DEFEAT, + DEFEAT_LOOP, + DEFEAT_SIEGE, + DEFEAT_SIEGE_LOOP, + WIN_SIEGE, + WIN_SIEGE_LOOP, + }; + BattleResultVideo currentVideo; + + void playVideo(bool startLoop = false); + + void buttonPressed(int button); //internal function for button callbacks +public: + BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false); + + void bExitf(); //exit button callback + void bRepeatf(); //repeat button callback + std::function resultCallback; //callback receiving which button was pressed + + void activate() override; + void show(Canvas & to) override; +}; + +/// Shows the stack queue +class StackQueue : public CIntObject +{ + class StackBox : public CIntObject + { + StackQueue * owner; + std::optional boundUnitID; + + std::shared_ptr background; + std::shared_ptr icon; + std::shared_ptr amount; + std::shared_ptr stateIcon; + + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + bool isBoundUnitHighlighted() const; + public: + StackBox(StackQueue * owner); + void setUnit(const battle::Unit * unit, size_t turn = 0); + std::optional getBoundUnitID() const; + + }; + + static const int QUEUE_SIZE = 10; + std::shared_ptr background; + std::vector> stackBoxes; + BattleInterface & owner; + + std::shared_ptr icons; + std::shared_ptr stateIcons; + + int32_t getSiegeShooterIconID(); +public: + const bool embedded; + + StackQueue(bool Embedded, BattleInterface & owner); + void update(); + std::optional getHoveredUnitIdIfAny() const; + + void show(Canvas & to) override; +}; diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 9a409d496..0bb6e0bd2 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -1,228 +1,228 @@ -/* - * BattleObstacleController.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 "BattleObstacleController.h" - -#include "BattleInterface.h" -#include "BattleFieldController.h" -#include "BattleAnimationClasses.h" -#include "BattleStacksController.h" -#include "BattleRenderer.h" -#include "CreatureAnimation.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CGuiHandler.h" -#include "../render/Canvas.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" -#include "../../lib/battle/CObstacleInstance.h" -#include "../../lib/ObstacleHandler.h" - -BattleObstacleController::BattleObstacleController(BattleInterface & owner): - owner(owner), - timePassed(0.f) -{ - auto obst = owner.getBattle()->battleGetAllObstacles(); - for(auto & elem : obst) - { - if ( elem->obstacleType == CObstacleInstance::MOAT ) - continue; // handled by siege controller; - loadObstacleImage(*elem); - } -} - -void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) -{ - AnimationPath animationName = oi.getAnimation(); - - if (animationsCache.count(animationName) == 0) - { - if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - { - // obstacle uses single bitmap image for animations - auto animation = GH.renderHandler().createAnimation(); - animation->setCustom(animationName.getName(), 0, 0); - animationsCache[animationName] = animation; - animation->preload(); - } - else - { - auto animation = GH.renderHandler().loadAnimation(animationName); - animationsCache[animationName] = animation; - animation->preload(); - } - } - obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; -} - -void BattleObstacleController::obstacleRemoved(const std::vector & obstacles) -{ - for(const auto & oi : obstacles) - { - auto & obstacle = oi.data["obstacle"]; - - if (!obstacle.isStruct()) - { - logGlobal->error("I don't know how to animate removal of this obstacle"); - continue; - } - - auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"])); - animation->preload(); - - auto first = animation->getImage(0, 0); - if(!first) - continue; - - //we assume here that effect graphics have the same size as the usual obstacle image - // -> if we know how to blit obstacle, let's blit the effect in the same place - Point whereTo = getObstaclePosition(first, obstacle); - //AFAIK, in H3 there is no sound of obstacle removal - owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true)); - - obstacleAnimations.erase(oi.id); - //so when multiple obstacles are removed, they show up one after another - owner.waitForAnimations(); - } -} - -void BattleObstacleController::obstaclePlaced(const std::vector> & obstacles) -{ - for(const auto & oi : obstacles) - { - auto side = owner.getBattle()->playerToSide(owner.curInt->playerID); - - if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value()))) - continue; - - auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation()); - animation->preload(); - - auto first = animation->getImage(0, 0); - if(!first) - continue; - - //we assume here that effect graphics have the same size as the usual obstacle image - // -> if we know how to blit obstacle, let's blit the effect in the same place - Point whereTo = getObstaclePosition(first, *oi); - CCS->soundh->playSound( oi->getAppearSound() ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos)); - - //so when multiple obstacles are added, they show up one after another - owner.waitForAnimations(); - - loadObstacleImage(*oi); - } -} - -void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) -{ - //Blit absolute obstacles - for(auto & obstacle : owner.getBattle()->battleGetAllObstacles()) - { - if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - { - auto img = getObstacleImage(*obstacle); - if(img) - canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height)); - } - - if (obstacle->obstacleType == CObstacleInstance::USUAL) - { - if (obstacle->getInfo().isForegroundObstacle) - continue; - - auto img = getObstacleImage(*obstacle); - if(img) - { - Point p = getObstaclePosition(img, *obstacle); - canvas.draw(img, p); - } - } - } -} - -void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (auto obstacle : owner.getBattle()->battleGetAllObstacles()) - { - if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) - continue; - - if (obstacle->obstacleType == CObstacleInstance::MOAT) - continue; - - if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle) - continue; - - renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ - auto img = getObstacleImage(*obstacle); - if(img) - { - Point p = getObstaclePosition(img, *obstacle); - canvas.draw(img, p); - } - }); - } -} - -void BattleObstacleController::tick(uint32_t msPassed) -{ - timePassed += msPassed / 1000.f; -} - -std::shared_ptr BattleObstacleController::getObstacleImage(const CObstacleInstance & oi) -{ - int framesCount = timePassed * AnimationControls::getObstaclesSpeed(); - std::shared_ptr animation; - - // obstacle is not loaded yet, don't show anything - if (obstacleAnimations.count(oi.uniqueID) == 0) - return nullptr; - - animation = obstacleAnimations[oi.uniqueID]; - assert(animation); - - if(animation) - { - int frameIndex = framesCount % animation->size(0); - return animation->getImage(frameIndex, 0); - } - return nullptr; -} - -Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) -{ - int offset = obstacle.getAnimationYOffset(image->height()); - - Rect r = owner.fieldController->hexPositionLocal(obstacle.pos); - r.y += 42 - image->height() + offset; - - return r.topLeft(); -} - -Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle) -{ - auto animationYOffset = obstacle["animationYOffset"].Integer(); - auto offset = image->height() % 42; - - if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37) - animationYOffset -= 42; - - offset += animationYOffset; - - Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer()); - r.y += 42 - image->height() + offset; - - return r.topLeft(); -} +/* + * BattleObstacleController.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 "BattleObstacleController.h" + +#include "BattleInterface.h" +#include "BattleFieldController.h" +#include "BattleAnimationClasses.h" +#include "BattleStacksController.h" +#include "BattleRenderer.h" +#include "CreatureAnimation.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/battle/CObstacleInstance.h" +#include "../../lib/ObstacleHandler.h" + +BattleObstacleController::BattleObstacleController(BattleInterface & owner): + owner(owner), + timePassed(0.f) +{ + auto obst = owner.getBattle()->battleGetAllObstacles(); + for(auto & elem : obst) + { + if ( elem->obstacleType == CObstacleInstance::MOAT ) + continue; // handled by siege controller; + loadObstacleImage(*elem); + } +} + +void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi) +{ + AnimationPath animationName = oi.getAnimation(); + + if (animationsCache.count(animationName) == 0) + { + if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + { + // obstacle uses single bitmap image for animations + auto animation = GH.renderHandler().createAnimation(); + animation->setCustom(animationName.getName(), 0, 0); + animationsCache[animationName] = animation; + animation->preload(); + } + else + { + auto animation = GH.renderHandler().loadAnimation(animationName); + animationsCache[animationName] = animation; + animation->preload(); + } + } + obstacleAnimations[oi.uniqueID] = animationsCache[animationName]; +} + +void BattleObstacleController::obstacleRemoved(const std::vector & obstacles) +{ + for(const auto & oi : obstacles) + { + auto & obstacle = oi.data["obstacle"]; + + if (!obstacle.isStruct()) + { + logGlobal->error("I don't know how to animate removal of this obstacle"); + continue; + } + + auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"])); + animation->preload(); + + auto first = animation->getImage(0, 0); + if(!first) + continue; + + //we assume here that effect graphics have the same size as the usual obstacle image + // -> if we know how to blit obstacle, let's blit the effect in the same place + Point whereTo = getObstaclePosition(first, obstacle); + //AFAIK, in H3 there is no sound of obstacle removal + owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true)); + + obstacleAnimations.erase(oi.id); + //so when multiple obstacles are removed, they show up one after another + owner.waitForAnimations(); + } +} + +void BattleObstacleController::obstaclePlaced(const std::vector> & obstacles) +{ + for(const auto & oi : obstacles) + { + auto side = owner.getBattle()->playerToSide(owner.curInt->playerID); + + if(!oi->visibleForSide(side.value(), owner.getBattle()->battleHasNativeStack(side.value()))) + continue; + + auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation()); + animation->preload(); + + auto first = animation->getImage(0, 0); + if(!first) + continue; + + //we assume here that effect graphics have the same size as the usual obstacle image + // -> if we know how to blit obstacle, let's blit the effect in the same place + Point whereTo = getObstaclePosition(first, *oi); + CCS->soundh->playSound( oi->getAppearSound() ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, oi->getAppearAnimation(), whereTo, oi->pos)); + + //so when multiple obstacles are added, they show up one after another + owner.waitForAnimations(); + + loadObstacleImage(*oi); + } +} + +void BattleObstacleController::showAbsoluteObstacles(Canvas & canvas) +{ + //Blit absolute obstacles + for(auto & obstacle : owner.getBattle()->battleGetAllObstacles()) + { + if(obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + { + auto img = getObstacleImage(*obstacle); + if(img) + canvas.draw(img, Point(obstacle->getInfo().width, obstacle->getInfo().height)); + } + + if (obstacle->obstacleType == CObstacleInstance::USUAL) + { + if (obstacle->getInfo().isForegroundObstacle) + continue; + + auto img = getObstacleImage(*obstacle); + if(img) + { + Point p = getObstaclePosition(img, *obstacle); + canvas.draw(img, p); + } + } + } +} + +void BattleObstacleController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (auto obstacle : owner.getBattle()->battleGetAllObstacles()) + { + if (obstacle->obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE) + continue; + + if (obstacle->obstacleType == CObstacleInstance::MOAT) + continue; + + if (obstacle->obstacleType == CObstacleInstance::USUAL && !obstacle->getInfo().isForegroundObstacle) + continue; + + renderer.insert(EBattleFieldLayer::OBSTACLES, obstacle->pos, [this, obstacle]( BattleRenderer::RendererRef canvas ){ + auto img = getObstacleImage(*obstacle); + if(img) + { + Point p = getObstaclePosition(img, *obstacle); + canvas.draw(img, p); + } + }); + } +} + +void BattleObstacleController::tick(uint32_t msPassed) +{ + timePassed += msPassed / 1000.f; +} + +std::shared_ptr BattleObstacleController::getObstacleImage(const CObstacleInstance & oi) +{ + int framesCount = timePassed * AnimationControls::getObstaclesSpeed(); + std::shared_ptr animation; + + // obstacle is not loaded yet, don't show anything + if (obstacleAnimations.count(oi.uniqueID) == 0) + return nullptr; + + animation = obstacleAnimations[oi.uniqueID]; + assert(animation); + + if(animation) + { + int frameIndex = framesCount % animation->size(0); + return animation->getImage(frameIndex, 0); + } + return nullptr; +} + +Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle) +{ + int offset = obstacle.getAnimationYOffset(image->height()); + + Rect r = owner.fieldController->hexPositionLocal(obstacle.pos); + r.y += 42 - image->height() + offset; + + return r.topLeft(); +} + +Point BattleObstacleController::getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle) +{ + auto animationYOffset = obstacle["animationYOffset"].Integer(); + auto offset = image->height() % 42; + + if(obstacle["needAnimationOffsetFix"].Bool() && offset > 37) + animationYOffset -= 42; + + offset += animationYOffset; + + Rect r = owner.fieldController->hexPositionLocal(obstacle["position"].Integer()); + r.y += 42 - image->height() + offset; + + return r.topLeft(); +} diff --git a/client/battle/BattleObstacleController.h b/client/battle/BattleObstacleController.h index b05ded9a6..c4a7467a4 100644 --- a/client/battle/BattleObstacleController.h +++ b/client/battle/BattleObstacleController.h @@ -1,68 +1,68 @@ -/* - * BattleObstacleController.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/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct BattleHex; -struct CObstacleInstance; -class JsonNode; -class ObstacleChanges; -class Point; - -VCMI_LIB_NAMESPACE_END - -class IImage; -class Canvas; -class CAnimation; -class BattleInterface; -class BattleRenderer; - -/// Controls all currently active projectiles on the battlefield -/// (with exception of moat, which is apparently handled by siege controller) -class BattleObstacleController -{ - BattleInterface & owner; - - /// total time, in seconds, since start of battle. Used for animating obstacles - float timePassed; - - /// cached animations of all obstacles in current battle - std::map> animationsCache; - - /// list of all obstacles that are currently being rendered - std::map> obstacleAnimations; - - void loadObstacleImage(const CObstacleInstance & oi); - - std::shared_ptr getObstacleImage(const CObstacleInstance & oi); - Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); - Point getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle); - -public: - BattleObstacleController(BattleInterface & owner); - - /// called every frame - void tick(uint32_t msPassed); - - /// call-in from network pack, add newly placed obstacles with any required animations - void obstaclePlaced(const std::vector> & oi); - - /// call-in from network pack, remove required obstacles with any required animations - void obstacleRemoved(const std::vector & obstacles); - - /// renders all "absolute" obstacles - void showAbsoluteObstacles(Canvas & canvas); - - /// adds all non-"absolute" visible obstacles for rendering - void collectRenderableObjects(BattleRenderer & renderer); -}; +/* + * BattleObstacleController.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/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleHex; +struct CObstacleInstance; +class JsonNode; +class ObstacleChanges; +class Point; + +VCMI_LIB_NAMESPACE_END + +class IImage; +class Canvas; +class CAnimation; +class BattleInterface; +class BattleRenderer; + +/// Controls all currently active projectiles on the battlefield +/// (with exception of moat, which is apparently handled by siege controller) +class BattleObstacleController +{ + BattleInterface & owner; + + /// total time, in seconds, since start of battle. Used for animating obstacles + float timePassed; + + /// cached animations of all obstacles in current battle + std::map> animationsCache; + + /// list of all obstacles that are currently being rendered + std::map> obstacleAnimations; + + void loadObstacleImage(const CObstacleInstance & oi); + + std::shared_ptr getObstacleImage(const CObstacleInstance & oi); + Point getObstaclePosition(std::shared_ptr image, const CObstacleInstance & obstacle); + Point getObstaclePosition(std::shared_ptr image, const JsonNode & obstacle); + +public: + BattleObstacleController(BattleInterface & owner); + + /// called every frame + void tick(uint32_t msPassed); + + /// call-in from network pack, add newly placed obstacles with any required animations + void obstaclePlaced(const std::vector> & oi); + + /// call-in from network pack, remove required obstacles with any required animations + void obstacleRemoved(const std::vector & obstacles); + + /// renders all "absolute" obstacles + void showAbsoluteObstacles(Canvas & canvas); + + /// adds all non-"absolute" visible obstacles for rendering + void collectRenderableObjects(BattleRenderer & renderer); +}; diff --git a/client/battle/BattleProjectileController.cpp b/client/battle/BattleProjectileController.cpp index f4b861c27..404540c82 100644 --- a/client/battle/BattleProjectileController.cpp +++ b/client/battle/BattleProjectileController.cpp @@ -1,386 +1,386 @@ -/* - * BattleProjectileController.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 "BattleProjectileController.h" - -#include "BattleInterface.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "CreatureAnimation.h" - -#include "../render/Canvas.h" -#include "../render/IRenderHandler.h" -#include "../gui/CGuiHandler.h" -#include "../CGameInfo.h" - -#include "../../lib/CStack.h" -#include "../../lib/mapObjects/CGTownInstance.h" - -static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x) -{ - double facA = 0.005; // seems to be constant - - // system of 2 linear equations, solutions of which are missing coefficients - // for quadratic equation a*x*x + b*x + c - double eq[2][3] = { - { static_cast(from.x), 1.0, from.y - facA*from.x*from.x }, - { static_cast(dest.x), 1.0, dest.y - facA*dest.x*dest.x } - }; - - // solve system via determinants - double det = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1]; - double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; - double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; - - double facB = detB / det; - double facC = detC / det; - - return facA *pow(x, 2.0) + facB *x + facC; -} - -void ProjectileMissile::show(Canvas & canvas) -{ - size_t group = reverse ? 1 : 0; - auto image = animation->getImage(frameNum, group, true); - - if(image) - { - Point pos { - vstd::lerp(from.x, dest.x, progress) - image->width() / 2, - vstd::lerp(from.y, dest.y, progress) - image->height() / 2, - }; - - canvas.draw(image, pos); - } -} - -void ProjectileMissile::tick(uint32_t msPassed) -{ - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -void ProjectileAnimatedMissile::tick(uint32_t msPassed) -{ - ProjectileMissile::tick(msPassed); - frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - size_t animationSize = animation->size(reverse ? 1 : 0); - while (frameProgress > animationSize) - frameProgress -= animationSize; - - frameNum = std::floor(frameProgress); -} - -void ProjectileCatapult::tick(uint32_t msPassed) -{ - frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -void ProjectileCatapult::show(Canvas & canvas) -{ - int frameCounter = std::floor(frameProgress); - int frameIndex = (frameCounter + 1) % animation->size(0); - - auto image = animation->getImage(frameIndex, 0, true); - - if(image) - { - int posX = vstd::lerp(from.x, dest.x, progress); - int posY = calculateCatapultParabolaY(from, dest, posX); - Point pos(posX, posY); - - canvas.draw(image, pos); - } -} - -void ProjectileRay::show(Canvas & canvas) -{ - Point curr { - vstd::lerp(from.x, dest.x, progress), - vstd::lerp(from.y, dest.y, progress), - }; - - Point length = curr - from; - - //select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other - - if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis - { - int y1 = from.y - rayConfig.size() / 2; - int y2 = curr.y - rayConfig.size() / 2; - - int x1 = from.x; - int x2 = curr.x; - - for (size_t i = 0; i < rayConfig.size(); ++i) - { - auto ray = rayConfig[i]; - canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end); - } - } - else // draw in vertical axis - { - int x1 = from.x - rayConfig.size() / 2; - int x2 = curr.x - rayConfig.size() / 2; - - int y1 = from.y; - int y2 = curr.y; - - for (size_t i = 0; i < rayConfig.size(); ++i) - { - auto ray = rayConfig[i]; - - canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end); - } - } -} - -void ProjectileRay::tick(uint32_t msPassed) -{ - float timePassed = msPassed / 1000.f; - progress += timePassed * speed; -} - -BattleProjectileController::BattleProjectileController(BattleInterface & owner): - owner(owner) -{} - -const CCreature & BattleProjectileController::getShooter(const CStack * stack) const -{ - const CCreature * creature = stack->unitType(); - - if(creature->getId() == CreatureID::ARROW_TOWERS) - creature = owner.siegeController->getTurretCreature(); - - if(creature->animation.missleFrameAngles.empty()) - { - logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated()); - creature = CGI->creh->objects[CreatureID::ARCHER]; - } - - return *creature; -} - -bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const -{ - return !getShooter(stack).animation.projectileRay.empty(); -} - -bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const -{ - return !getShooter(stack).animation.projectileImageName.empty(); -} - -void BattleProjectileController::initStackProjectile(const CStack * stack) -{ - if (!stackUsesMissileProjectile(stack)) - return; - - const CCreature & creature = getShooter(stack); - projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName); -} - -std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) -{ - std::shared_ptr projectile = GH.renderHandler().loadAnimation(path); - projectile->preload(); - - if(projectile->size(1) != 0) - logAnim->error("Expected empty group 1 in stack projectile"); - else - projectile->createFlippedGroup(0, 1); - - return projectile; -} - -std::shared_ptr BattleProjectileController::getProjectileImage(const CStack * stack) -{ - const CCreature & creature = getShooter(stack); - AnimationPath imageName = creature.animation.projectileImageName; - - if (!projectilesCache.count(imageName)) - initStackProjectile(stack); - - return projectilesCache[imageName]; -} - -void BattleProjectileController::emitStackProjectile(const CStack * stack) -{ - int stackID = stack ? stack->unitId() : -1; - - for (auto projectile : projectiles) - { - if ( !projectile->playing && projectile->shooterID == stackID) - { - projectile->playing = true; - return; - } - } -} - -void BattleProjectileController::render(Canvas & canvas) -{ - for ( auto projectile: projectiles) - { - if ( projectile->playing ) - projectile->show(canvas); - } -} - -void BattleProjectileController::tick(uint32_t msPassed) -{ - for ( auto projectile: projectiles) - { - if ( projectile->playing ) - projectile->tick(msPassed); - } - - vstd::erase_if(projectiles, [&](const std::shared_ptr & projectile){ - return projectile->progress > 1.0f; - }); -} - -bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const -{ - int stackID = stack ? stack->unitId() : -1; - - for(auto const & instance : projectiles) - { - if(instance->shooterID == stackID && (instance->playing || !emittedOnly)) - { - return true; - } - } - return false; -} - -float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) -{ - float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); - float distance = sqrt(distanceSquared); - - assert(distance > 1.f); - - return animSpeed / std::max( 1.f, distance); -} - -int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack) -{ - const CCreature & creature = getShooter(stack); - - auto & angles = creature.animation.missleFrameAngles; - auto animation = getProjectileImage(stack); - - // only frames below maxFrame are usable: anything higher is either no present or we don't know when it should be used - size_t maxFrame = std::min(angles.size(), animation->size(0)); - - assert(maxFrame > 0); - double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x)); - - // values in angles array indicate position from which this frame was rendered, in degrees. - // possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots - // find frame that has closest angle to one that we need for this shot - int bestID = 0; - double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle ); - - for (int i=1; ianimation = getProjectileImage(shooter); - catapultProjectile->progress = 0; - catapultProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); - catapultProjectile->from = from; - catapultProjectile->dest = dest; - catapultProjectile->shooterID = shooter->unitId(); - catapultProjectile->playing = false; - catapultProjectile->frameProgress = 0.f; - - projectiles.push_back(std::shared_ptr(catapultProjectile)); -} - -void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest) -{ - const CCreature & shooterInfo = getShooter(shooter); - - std::shared_ptr projectile; - if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter)) - { - logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated()); - } - - if (stackUsesRayProjectile(shooter)) - { - auto rayProjectile = new ProjectileRay(); - projectile.reset(rayProjectile); - - rayProjectile->rayConfig = shooterInfo.animation.projectileRay; - rayProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed()); - } - else if (stackUsesMissileProjectile(shooter)) - { - auto missileProjectile = new ProjectileMissile(); - projectile.reset(missileProjectile); - - missileProjectile->animation = getProjectileImage(shooter); - missileProjectile->reverse = !owner.stacksController->facingRight(shooter); - missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter); - missileProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); - } - - - projectile->from = from; - projectile->dest = dest; - projectile->shooterID = shooter->unitId(); - projectile->progress = 0; - projectile->playing = false; - - projectiles.push_back(projectile); -} - -void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell) -{ - double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y)); - AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); - - assert(!animToDisplay.empty()); - - if(!animToDisplay.empty()) - { - auto projectile = new ProjectileAnimatedMissile(); - - projectile->animation = createProjectileImage(animToDisplay); - projectile->frameProgress = 0; - projectile->frameNum = 0; - projectile->reverse = from.x > dest.x; - projectile->from = from; - projectile->dest = dest; - projectile->shooterID = shooter ? shooter->unitId() : -1; - projectile->progress = 0; - projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); - projectile->playing = false; - - projectiles.push_back(std::shared_ptr(projectile)); - } -} +/* + * BattleProjectileController.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 "BattleProjectileController.h" + +#include "BattleInterface.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "CreatureAnimation.h" + +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" +#include "../CGameInfo.h" + +#include "../../lib/CStack.h" +#include "../../lib/mapObjects/CGTownInstance.h" + +static double calculateCatapultParabolaY(const Point & from, const Point & dest, int x) +{ + double facA = 0.005; // seems to be constant + + // system of 2 linear equations, solutions of which are missing coefficients + // for quadratic equation a*x*x + b*x + c + double eq[2][3] = { + { static_cast(from.x), 1.0, from.y - facA*from.x*from.x }, + { static_cast(dest.x), 1.0, dest.y - facA*dest.x*dest.x } + }; + + // solve system via determinants + double det = eq[0][0] *eq[1][1] - eq[1][0] *eq[0][1]; + double detB = eq[0][2] *eq[1][1] - eq[1][2] *eq[0][1]; + double detC = eq[0][0] *eq[1][2] - eq[1][0] *eq[0][2]; + + double facB = detB / det; + double facC = detC / det; + + return facA *pow(x, 2.0) + facB *x + facC; +} + +void ProjectileMissile::show(Canvas & canvas) +{ + size_t group = reverse ? 1 : 0; + auto image = animation->getImage(frameNum, group, true); + + if(image) + { + Point pos { + vstd::lerp(from.x, dest.x, progress) - image->width() / 2, + vstd::lerp(from.y, dest.y, progress) - image->height() / 2, + }; + + canvas.draw(image, pos); + } +} + +void ProjectileMissile::tick(uint32_t msPassed) +{ + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +void ProjectileAnimatedMissile::tick(uint32_t msPassed) +{ + ProjectileMissile::tick(msPassed); + frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + size_t animationSize = animation->size(reverse ? 1 : 0); + while (frameProgress > animationSize) + frameProgress -= animationSize; + + frameNum = std::floor(frameProgress); +} + +void ProjectileCatapult::tick(uint32_t msPassed) +{ + frameProgress += AnimationControls::getSpellEffectSpeed() * msPassed / 1000; + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +void ProjectileCatapult::show(Canvas & canvas) +{ + int frameCounter = std::floor(frameProgress); + int frameIndex = (frameCounter + 1) % animation->size(0); + + auto image = animation->getImage(frameIndex, 0, true); + + if(image) + { + int posX = vstd::lerp(from.x, dest.x, progress); + int posY = calculateCatapultParabolaY(from, dest, posX); + Point pos(posX, posY); + + canvas.draw(image, pos); + } +} + +void ProjectileRay::show(Canvas & canvas) +{ + Point curr { + vstd::lerp(from.x, dest.x, progress), + vstd::lerp(from.y, dest.y, progress), + }; + + Point length = curr - from; + + //select axis to draw ray on, we want angle to be less than 45 degrees so individual sub-rays won't overlap each other + + if (std::abs(length.x) > std::abs(length.y)) // draw in horizontal axis + { + int y1 = from.y - rayConfig.size() / 2; + int y2 = curr.y - rayConfig.size() / 2; + + int x1 = from.x; + int x2 = curr.x; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + canvas.drawLine(Point(x1, y1 + i), Point(x2, y2+i), ray.start, ray.end); + } + } + else // draw in vertical axis + { + int x1 = from.x - rayConfig.size() / 2; + int x2 = curr.x - rayConfig.size() / 2; + + int y1 = from.y; + int y2 = curr.y; + + for (size_t i = 0; i < rayConfig.size(); ++i) + { + auto ray = rayConfig[i]; + + canvas.drawLine(Point(x1 + i, y1), Point(x2 + i, y2), ray.start, ray.end); + } + } +} + +void ProjectileRay::tick(uint32_t msPassed) +{ + float timePassed = msPassed / 1000.f; + progress += timePassed * speed; +} + +BattleProjectileController::BattleProjectileController(BattleInterface & owner): + owner(owner) +{} + +const CCreature & BattleProjectileController::getShooter(const CStack * stack) const +{ + const CCreature * creature = stack->unitType(); + + if(creature->getId() == CreatureID::ARROW_TOWERS) + creature = owner.siegeController->getTurretCreature(); + + if(creature->animation.missleFrameAngles.empty()) + { + logAnim->error("Mod error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Trying to use archer's data instead...", creature->getNameSingularTranslated()); + creature = CGI->creh->objects[CreatureID::ARCHER]; + } + + return *creature; +} + +bool BattleProjectileController::stackUsesRayProjectile(const CStack * stack) const +{ + return !getShooter(stack).animation.projectileRay.empty(); +} + +bool BattleProjectileController::stackUsesMissileProjectile(const CStack * stack) const +{ + return !getShooter(stack).animation.projectileImageName.empty(); +} + +void BattleProjectileController::initStackProjectile(const CStack * stack) +{ + if (!stackUsesMissileProjectile(stack)) + return; + + const CCreature & creature = getShooter(stack); + projectilesCache[creature.animation.projectileImageName] = createProjectileImage(creature.animation.projectileImageName); +} + +std::shared_ptr BattleProjectileController::createProjectileImage(const AnimationPath & path ) +{ + std::shared_ptr projectile = GH.renderHandler().loadAnimation(path); + projectile->preload(); + + if(projectile->size(1) != 0) + logAnim->error("Expected empty group 1 in stack projectile"); + else + projectile->createFlippedGroup(0, 1); + + return projectile; +} + +std::shared_ptr BattleProjectileController::getProjectileImage(const CStack * stack) +{ + const CCreature & creature = getShooter(stack); + AnimationPath imageName = creature.animation.projectileImageName; + + if (!projectilesCache.count(imageName)) + initStackProjectile(stack); + + return projectilesCache[imageName]; +} + +void BattleProjectileController::emitStackProjectile(const CStack * stack) +{ + int stackID = stack ? stack->unitId() : -1; + + for (auto projectile : projectiles) + { + if ( !projectile->playing && projectile->shooterID == stackID) + { + projectile->playing = true; + return; + } + } +} + +void BattleProjectileController::render(Canvas & canvas) +{ + for ( auto projectile: projectiles) + { + if ( projectile->playing ) + projectile->show(canvas); + } +} + +void BattleProjectileController::tick(uint32_t msPassed) +{ + for ( auto projectile: projectiles) + { + if ( projectile->playing ) + projectile->tick(msPassed); + } + + vstd::erase_if(projectiles, [&](const std::shared_ptr & projectile){ + return projectile->progress > 1.0f; + }); +} + +bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const +{ + int stackID = stack ? stack->unitId() : -1; + + for(auto const & instance : projectiles) + { + if(instance->shooterID == stackID && (instance->playing || !emittedOnly)) + { + return true; + } + } + return false; +} + +float BattleProjectileController::computeProjectileFlightTime( Point from, Point dest, double animSpeed) +{ + float distanceSquared = (dest.x - from.x) * (dest.x - from.x) + (dest.y - from.y) * (dest.y - from.y); + float distance = sqrt(distanceSquared); + + assert(distance > 1.f); + + return animSpeed / std::max( 1.f, distance); +} + +int BattleProjectileController::computeProjectileFrameID( Point from, Point dest, const CStack * stack) +{ + const CCreature & creature = getShooter(stack); + + auto & angles = creature.animation.missleFrameAngles; + auto animation = getProjectileImage(stack); + + // only frames below maxFrame are usable: anything higher is either no present or we don't know when it should be used + size_t maxFrame = std::min(angles.size(), animation->size(0)); + + assert(maxFrame > 0); + double projectileAngle = -atan2(dest.y - from.y, std::abs(dest.x - from.x)); + + // values in angles array indicate position from which this frame was rendered, in degrees. + // possible range is 90 ... -90, where projectile for +90 will be used for shooting upwards, +0 for shots towards right and -90 for downwards shots + // find frame that has closest angle to one that we need for this shot + int bestID = 0; + double bestDiff = fabs( angles[0] / 180 * M_PI - projectileAngle ); + + for (int i=1; ianimation = getProjectileImage(shooter); + catapultProjectile->progress = 0; + catapultProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed()); + catapultProjectile->from = from; + catapultProjectile->dest = dest; + catapultProjectile->shooterID = shooter->unitId(); + catapultProjectile->playing = false; + catapultProjectile->frameProgress = 0.f; + + projectiles.push_back(std::shared_ptr(catapultProjectile)); +} + +void BattleProjectileController::createProjectile(const CStack * shooter, Point from, Point dest) +{ + const CCreature & shooterInfo = getShooter(shooter); + + std::shared_ptr projectile; + if (stackUsesRayProjectile(shooter) && stackUsesMissileProjectile(shooter)) + { + logAnim->error("Mod error: Creature '%s' has both missile and ray projectiles configured. Mod should be fixed. Using ray projectile configuration...", shooterInfo.getNameSingularTranslated()); + } + + if (stackUsesRayProjectile(shooter)) + { + auto rayProjectile = new ProjectileRay(); + projectile.reset(rayProjectile); + + rayProjectile->rayConfig = shooterInfo.animation.projectileRay; + rayProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getRayProjectileSpeed()); + } + else if (stackUsesMissileProjectile(shooter)) + { + auto missileProjectile = new ProjectileMissile(); + projectile.reset(missileProjectile); + + missileProjectile->animation = getProjectileImage(shooter); + missileProjectile->reverse = !owner.stacksController->facingRight(shooter); + missileProjectile->frameNum = computeProjectileFrameID(from, dest, shooter); + missileProjectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); + } + + + projectile->from = from; + projectile->dest = dest; + projectile->shooterID = shooter->unitId(); + projectile->progress = 0; + projectile->playing = false; + + projectiles.push_back(projectile); +} + +void BattleProjectileController::createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell) +{ + double projectileAngle = std::abs(atan2(dest.x - from.x, dest.y - from.y)); + AnimationPath animToDisplay = spell->animationInfo.selectProjectile(projectileAngle); + + assert(!animToDisplay.empty()); + + if(!animToDisplay.empty()) + { + auto projectile = new ProjectileAnimatedMissile(); + + projectile->animation = createProjectileImage(animToDisplay); + projectile->frameProgress = 0; + projectile->frameNum = 0; + projectile->reverse = from.x > dest.x; + projectile->from = from; + projectile->dest = dest; + projectile->shooterID = shooter ? shooter->unitId() : -1; + projectile->progress = 0; + projectile->speed = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed()); + projectile->playing = false; + + projectiles.push_back(std::shared_ptr(projectile)); + } +} diff --git a/client/battle/BattleProjectileController.h b/client/battle/BattleProjectileController.h index b54a5bca1..d2b3a04e1 100644 --- a/client/battle/BattleProjectileController.h +++ b/client/battle/BattleProjectileController.h @@ -1,125 +1,125 @@ -/* - * BattleSiegeController.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/CCreatureHandler.h" -#include "../../lib/Point.h" -#include "../../lib/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CSpell; - -VCMI_LIB_NAMESPACE_END - -class CAnimation; -class Canvas; -class BattleInterface; - -/// Base class for projectiles -struct ProjectileBase -{ - virtual ~ProjectileBase() = default; - virtual void show(Canvas & canvas) = 0; - virtual void tick(uint32_t msPassed) = 0; - - Point from; // initial position on the screen - Point dest; // target position on the screen - - float progress; // current position of projectile on from->dest line - float speed; // how much progress is gained per second - int shooterID; // ID of shooter stack - bool playing; // if set to true, projectile animation is playing, e.g. flying to target -}; - -/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination -struct ProjectileMissile : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::shared_ptr animation; - int frameNum; // frame to display from projectile animation - bool reverse; // if true, projectile will be flipped by vertical axis -}; - -/// Projectile for spell - render animation moving in straight line from origin to destination -struct ProjectileAnimatedMissile : ProjectileMissile -{ - void tick(uint32_t msPassed) override; - float frameProgress; -}; - -/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination -struct ProjectileCatapult : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::shared_ptr animation; - float frameProgress; -}; - -/// Projectile for mages/evil eye - render ray expanding from origin position to destination -struct ProjectileRay : ProjectileBase -{ - void show(Canvas & canvas) override; - void tick(uint32_t msPassed) override; - - std::vector rayConfig; -}; - -/// Class that manages all ongoing projectiles in the game -/// ... even though in H3 only 1 projectile can be on screen at any point of time -class BattleProjectileController -{ - BattleInterface & owner; - - /// all projectiles loaded during current battle - std::map> projectilesCache; - - /// projectiles currently flying on battlefield - std::vector> projectiles; - - std::shared_ptr getProjectileImage(const CStack * stack); - std::shared_ptr createProjectileImage(const AnimationPath & path ); - void initStackProjectile(const CStack * stack); - - bool stackUsesRayProjectile(const CStack * stack) const; - bool stackUsesMissileProjectile(const CStack * stack) const; - - void showProjectile(Canvas & canvas, std::shared_ptr projectile); - - const CCreature & getShooter(const CStack * stack) const; - - int computeProjectileFrameID( Point from, Point dest, const CStack * stack); - float computeProjectileFlightTime( Point from, Point dest, double speed); - -public: - BattleProjectileController(BattleInterface & owner); - - /// renders all currently active projectiles - void render(Canvas & canvas); - - /// updates positioning / animations of all projectiles - void tick(uint32_t msPassed); - - /// returns true if stack has projectile that is yet to hit target - bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const; - - /// starts rendering previously created projectile - void emitStackProjectile(const CStack * stack); - - /// creates (but not emits!) projectile and initializes it based on parameters - void createProjectile(const CStack * shooter, Point from, Point dest); - void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell); - void createCatapultProjectile(const CStack * shooter, Point from, Point dest); -}; +/* + * BattleSiegeController.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/CCreatureHandler.h" +#include "../../lib/Point.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class Canvas; +class BattleInterface; + +/// Base class for projectiles +struct ProjectileBase +{ + virtual ~ProjectileBase() = default; + virtual void show(Canvas & canvas) = 0; + virtual void tick(uint32_t msPassed) = 0; + + Point from; // initial position on the screen + Point dest; // target position on the screen + + float progress; // current position of projectile on from->dest line + float speed; // how much progress is gained per second + int shooterID; // ID of shooter stack + bool playing; // if set to true, projectile animation is playing, e.g. flying to target +}; + +/// Projectile for most shooters - render pre-selected frame moving in straight line from origin to destination +struct ProjectileMissile : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::shared_ptr animation; + int frameNum; // frame to display from projectile animation + bool reverse; // if true, projectile will be flipped by vertical axis +}; + +/// Projectile for spell - render animation moving in straight line from origin to destination +struct ProjectileAnimatedMissile : ProjectileMissile +{ + void tick(uint32_t msPassed) override; + float frameProgress; +}; + +/// Projectile for catapult - render spinning projectile moving at parabolic trajectory to its destination +struct ProjectileCatapult : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::shared_ptr animation; + float frameProgress; +}; + +/// Projectile for mages/evil eye - render ray expanding from origin position to destination +struct ProjectileRay : ProjectileBase +{ + void show(Canvas & canvas) override; + void tick(uint32_t msPassed) override; + + std::vector rayConfig; +}; + +/// Class that manages all ongoing projectiles in the game +/// ... even though in H3 only 1 projectile can be on screen at any point of time +class BattleProjectileController +{ + BattleInterface & owner; + + /// all projectiles loaded during current battle + std::map> projectilesCache; + + /// projectiles currently flying on battlefield + std::vector> projectiles; + + std::shared_ptr getProjectileImage(const CStack * stack); + std::shared_ptr createProjectileImage(const AnimationPath & path ); + void initStackProjectile(const CStack * stack); + + bool stackUsesRayProjectile(const CStack * stack) const; + bool stackUsesMissileProjectile(const CStack * stack) const; + + void showProjectile(Canvas & canvas, std::shared_ptr projectile); + + const CCreature & getShooter(const CStack * stack) const; + + int computeProjectileFrameID( Point from, Point dest, const CStack * stack); + float computeProjectileFlightTime( Point from, Point dest, double speed); + +public: + BattleProjectileController(BattleInterface & owner); + + /// renders all currently active projectiles + void render(Canvas & canvas); + + /// updates positioning / animations of all projectiles + void tick(uint32_t msPassed); + + /// returns true if stack has projectile that is yet to hit target + bool hasActiveProjectile(const CStack * stack, bool emittedOnly) const; + + /// starts rendering previously created projectile + void emitStackProjectile(const CStack * stack); + + /// creates (but not emits!) projectile and initializes it based on parameters + void createProjectile(const CStack * shooter, Point from, Point dest); + void createSpellProjectile(const CStack * shooter, Point from, Point dest, const CSpell * spell); + void createCatapultProjectile(const CStack * shooter, Point from, Point dest); +}; diff --git a/client/battle/BattleRenderer.cpp b/client/battle/BattleRenderer.cpp index 9e5971749..fbbe3e943 100644 --- a/client/battle/BattleRenderer.cpp +++ b/client/battle/BattleRenderer.cpp @@ -1,76 +1,76 @@ -/* - * BattleFieldController.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 "BattleRenderer.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleEffectsController.h" -#include "BattleWindow.h" -#include "BattleSiegeController.h" -#include "BattleStacksController.h" -#include "BattleObstacleController.h" - -void BattleRenderer::collectObjects() -{ - owner.effectsController->collectRenderableObjects(*this); - owner.obstacleController->collectRenderableObjects(*this); - owner.stacksController->collectRenderableObjects(*this); - if (owner.siegeController) - owner.siegeController->collectRenderableObjects(*this); - if (owner.defendingHero) - owner.defendingHero->collectRenderableObjects(*this); - if (owner.attackingHero) - owner.attackingHero->collectRenderableObjects(*this); -} - -void BattleRenderer::sortObjects() -{ - auto getRow = [](const RenderableInstance & object) -> int - { - if (object.tile.isValid()) - return object.tile.getY(); - - if ( object.tile == BattleHex::HEX_BEFORE_ALL ) - return -1; - - assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID); - return GameConstants::BFIELD_HEIGHT; - }; - - std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){ - if ( getRow(left) != getRow(right)) - return getRow(left) < getRow(right); - return left.layer < right.layer; - }); -} - -void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas) -{ - for (auto const & object : objects) - object.functor(targetCanvas); -} - -BattleRenderer::BattleRenderer(BattleInterface & owner): - owner(owner) -{ -} - -void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor) -{ - objects.push_back({functor, layer, tile}); -} - -void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas) -{ - collectObjects(); - sortObjects(); - renderObjects(targetCanvas); -} +/* + * BattleFieldController.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 "BattleRenderer.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleEffectsController.h" +#include "BattleWindow.h" +#include "BattleSiegeController.h" +#include "BattleStacksController.h" +#include "BattleObstacleController.h" + +void BattleRenderer::collectObjects() +{ + owner.effectsController->collectRenderableObjects(*this); + owner.obstacleController->collectRenderableObjects(*this); + owner.stacksController->collectRenderableObjects(*this); + if (owner.siegeController) + owner.siegeController->collectRenderableObjects(*this); + if (owner.defendingHero) + owner.defendingHero->collectRenderableObjects(*this); + if (owner.attackingHero) + owner.attackingHero->collectRenderableObjects(*this); +} + +void BattleRenderer::sortObjects() +{ + auto getRow = [](const RenderableInstance & object) -> int + { + if (object.tile.isValid()) + return object.tile.getY(); + + if ( object.tile == BattleHex::HEX_BEFORE_ALL ) + return -1; + + assert( object.tile == BattleHex::HEX_AFTER_ALL || object.tile == BattleHex::INVALID); + return GameConstants::BFIELD_HEIGHT; + }; + + std::stable_sort(objects.begin(), objects.end(), [&](const RenderableInstance & left, const RenderableInstance & right){ + if ( getRow(left) != getRow(right)) + return getRow(left) < getRow(right); + return left.layer < right.layer; + }); +} + +void BattleRenderer::renderObjects(BattleRenderer::RendererRef targetCanvas) +{ + for (auto const & object : objects) + object.functor(targetCanvas); +} + +BattleRenderer::BattleRenderer(BattleInterface & owner): + owner(owner) +{ +} + +void BattleRenderer::insert(EBattleFieldLayer layer, BattleHex tile, BattleRenderer::RenderFunctor functor) +{ + objects.push_back({functor, layer, tile}); +} + +void BattleRenderer::execute(BattleRenderer::RendererRef targetCanvas) +{ + collectObjects(); + sortObjects(); + renderObjects(targetCanvas); +} diff --git a/client/battle/BattleRenderer.h b/client/battle/BattleRenderer.h index 0dbab110b..9a5f20dd8 100644 --- a/client/battle/BattleRenderer.h +++ b/client/battle/BattleRenderer.h @@ -1,53 +1,53 @@ -/* - * BattleFieldController.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/battle/BattleHex.h" - -class Canvas; -class BattleInterface; - -enum class EBattleFieldLayer { - // confirmed ordering requirements: - CORPSES = 0, - WALLS = 1, - HEROES = 2, - STACKS = 2, // after corpses, obstacles, walls - OBSTACLES = 3, // after stacks - STACK_AMOUNTS = 3, // after stacks, obstacles, corpses - EFFECTS = 4, // after obstacles, battlements -}; - -class BattleRenderer -{ -public: - using RendererRef = Canvas &; - using RenderFunctor = std::function; - -private: - BattleInterface & owner; - - struct RenderableInstance - { - RenderFunctor functor; - EBattleFieldLayer layer; - BattleHex tile; - }; - std::vector objects; - - void collectObjects(); - void sortObjects(); - void renderObjects(RendererRef targetCanvas); -public: - BattleRenderer(BattleInterface & owner); - - void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor); - void execute(RendererRef targetCanvas); -}; +/* + * BattleFieldController.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/battle/BattleHex.h" + +class Canvas; +class BattleInterface; + +enum class EBattleFieldLayer { + // confirmed ordering requirements: + CORPSES = 0, + WALLS = 1, + HEROES = 2, + STACKS = 2, // after corpses, obstacles, walls + OBSTACLES = 3, // after stacks + STACK_AMOUNTS = 3, // after stacks, obstacles, corpses + EFFECTS = 4, // after obstacles, battlements +}; + +class BattleRenderer +{ +public: + using RendererRef = Canvas &; + using RenderFunctor = std::function; + +private: + BattleInterface & owner; + + struct RenderableInstance + { + RenderFunctor functor; + EBattleFieldLayer layer; + BattleHex tile; + }; + std::vector objects; + + void collectObjects(); + void sortObjects(); + void renderObjects(RendererRef targetCanvas); +public: + BattleRenderer(BattleInterface & owner); + + void insert(EBattleFieldLayer layer, BattleHex tile, RenderFunctor functor); + void execute(RendererRef targetCanvas); +}; diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index 433a793c1..c041f3743 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -1,367 +1,367 @@ -/* - * BattleSiegeController.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 "BattleSiegeController.h" - -#include "BattleAnimationClasses.h" -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleStacksController.h" -#include "BattleFieldController.h" -#include "BattleRenderer.h" - -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CGuiHandler.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" -#include "../../lib/NetPacks.h" -#include "../../lib/CStack.h" -#include "../../lib/mapObjects/CGTownInstance.h" - -ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const -{ - auto getImageIndex = [&]() -> int - { - bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER); - - switch (state) - { - case EWallState::REINFORCED : - return 1; - case EWallState::INTACT : - if (town->hasBuilt(BuildingID::CASTLE)) - return 2; // reinforced walls were damaged - else - return 1; - case EWallState::DAMAGED : - // towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2 - if (isTower) - return 1; - else - return 2; - case EWallState::DESTROYED : - if (isTower) - return 2; - else - return 3; - } - return 1; - }; - - const std::string & prefix = town->town->clientInfo.siegePrefix; - std::string addit = std::to_string(getImageIndex()); - - switch(what) - { - case EWallVisual::BACKGROUND_WALL: - { - auto faction = town->town->faction->getIndex(); - - if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) - return ImagePath::builtinTODO(prefix + "TPW1.BMP"); - else - return ImagePath::builtinTODO(prefix + "TPWL.BMP"); - } - case EWallVisual::KEEP: - return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP"); - case EWallVisual::BOTTOM_TOWER: - return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP"); - case EWallVisual::BOTTOM_WALL: - return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP"); - case EWallVisual::WALL_BELLOW_GATE: - return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP"); - case EWallVisual::WALL_OVER_GATE: - return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP"); - case EWallVisual::UPPER_WALL: - return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP"); - case EWallVisual::UPPER_TOWER: - return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP"); - case EWallVisual::GATE: - return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP"); - case EWallVisual::GATE_ARCH: - return ImagePath::builtinTODO(prefix + "ARCH.BMP"); - case EWallVisual::BOTTOM_STATIC_WALL: - return ImagePath::builtinTODO(prefix + "WA2.BMP"); - case EWallVisual::UPPER_STATIC_WALL: - return ImagePath::builtinTODO(prefix + "WA5.BMP"); - case EWallVisual::MOAT: - return ImagePath::builtinTODO(prefix + "MOAT.BMP"); - case EWallVisual::MOAT_BANK: - return ImagePath::builtinTODO(prefix + "MLIP.BMP"); - case EWallVisual::KEEP_BATTLEMENT: - return ImagePath::builtinTODO(prefix + "MANC.BMP"); - case EWallVisual::BOTTOM_BATTLEMENT: - return ImagePath::builtinTODO(prefix + "TW1C.BMP"); - case EWallVisual::UPPER_BATTLEMENT: - return ImagePath::builtinTODO(prefix + "TW2C.BMP"); - default: - return ImagePath(); - } -} - -void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) -{ - auto & ci = town->town->clientInfo; - auto const & pos = ci.siegePositions[what]; - - if ( wallPieceImages[what] && pos.isValid()) - canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); -} - -ImagePath BattleSiegeController::getBattleBackgroundName() const -{ - const std::string & prefix = town->town->clientInfo.siegePrefix; - return ImagePath::builtinTODO(prefix + "BACK.BMP"); -} - -bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const -{ - //FIXME: use this instead of buildings test? - //ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel(); - - switch (what) - { - case EWallVisual::MOAT: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); - case EWallVisual::MOAT_BANK: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); - case EWallVisual::KEEP_BATTLEMENT: return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; - case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; - case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; - default: return true; - } -} - -BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const -{ - static const std::array wallsPositions = { - BattleHex::INVALID, // BACKGROUND, // handled separately - BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL, - 135, // KEEP, - BattleHex::HEX_AFTER_ALL, // BOTTOM_TOWER, - 182, // BOTTOM_WALL, - 130, // WALL_BELLOW_GATE, - 62, // WALL_OVER_GATE, - 12, // UPPER_WALL, - BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER, - BattleHex::HEX_BEFORE_ALL, // GATE, // 94 - 112, // GATE_ARCH, - 165, // BOTTOM_STATIC_WALL, - 45, // UPPER_STATIC_WALL, - BattleHex::INVALID, // MOAT, // printed as absolute obstacle - BattleHex::INVALID, // MOAT_BANK, // printed as absolute obstacle - 135, // KEEP_BATTLEMENT, - BattleHex::HEX_AFTER_ALL, // BOTTOM_BATTLEMENT, - BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT, - }; - - return wallsPositions[what]; -} - -BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown): - owner(owner), - town(siegeTown) -{ - assert(owner.fieldController.get() == nullptr); // must be created after this - - for (int g = 0; g < wallPieceImages.size(); ++g) - { - if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state - continue; - - if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) - continue; - - wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); - } -} - -const CCreature *BattleSiegeController::getTurretCreature() const -{ - return CGI->creh->objects[town->town->clientInfo.siegeShooter]; -} - -Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const -{ - // Turret positions are read out of the config/wall_pos.txt - int posID = 0; - switch (position) - { - case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature - posID = EWallVisual::CREATURE_KEEP; - break; - case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature - posID = EWallVisual::CREATURE_BOTTOM_TOWER; - break; - case BattleHex::CASTLE_UPPER_TOWER: // upper creature - posID = EWallVisual::CREATURE_UPPER_TOWER; - break; - } - - if (posID != 0) - { - return { - town->town->clientInfo.siegePositions[posID].x, - town->town->clientInfo.siegePositions[posID].y - }; - } - - assert(0); - return Point(0,0); -} - -void BattleSiegeController::gateStateChanged(const EGateState state) -{ - auto oldState = owner.getBattle()->battleGetGateState(); - bool playSound = false; - auto stateId = EWallState::NONE; - switch(state) - { - case EGateState::CLOSED: - if (oldState != EGateState::BLOCKED) - playSound = true; - break; - case EGateState::BLOCKED: - if (oldState != EGateState::CLOSED) - playSound = true; - break; - case EGateState::OPENED: - playSound = true; - stateId = EWallState::DAMAGED; - break; - case EGateState::DESTROYED: - stateId = EWallState::DESTROYED; - break; - } - - if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED) - wallPieceImages[EWallVisual::GATE] = nullptr; - - if (stateId != EWallState::NONE) - wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId)); - - if (playSound) - CCS->soundh->playSound(soundBase::DRAWBRG); -} - -void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) -{ - if (getWallPieceExistance(EWallVisual::MOAT)) - showWallPiece(canvas, EWallVisual::MOAT); - - if (getWallPieceExistance(EWallVisual::MOAT_BANK)) - showWallPiece(canvas, EWallVisual::MOAT_BANK); -} - -BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const -{ - switch(wallPiece) - { - case EWallVisual::KEEP_BATTLEMENT: return BattleHex::CASTLE_CENTRAL_TOWER; - case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER; - case EWallVisual::UPPER_BATTLEMENT: return BattleHex::CASTLE_UPPER_TOWER; - } - assert(0); - return BattleHex::INVALID; -} - -const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const -{ - for (auto & stack : owner.getBattle()->battleGetAllStacks(true)) - { - if ( stack->initialPosition == getTurretBattleHex(wallPiece)) - return stack; - } - assert(0); - return nullptr; -} - -void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) -{ - for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i) - { - auto wallPiece = EWallVisual::EWallVisual(i); - - if ( !getWallPieceExistance(wallPiece)) - continue; - - if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID) - continue; - - if (wallPiece == EWallVisual::KEEP_BATTLEMENT || - wallPiece == EWallVisual::BOTTOM_BATTLEMENT || - wallPiece == EWallVisual::UPPER_BATTLEMENT) - { - renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); - }); - renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece); - }); - } - renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ - showWallPiece(canvas, wallPiece); - }); - } -} - -bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const -{ - if (owner.tacticsMode) - return false; - - auto wallPart = owner.getBattle()->battleHexToWallPart(hex); - return owner.getBattle()->isWallPartAttackable(wallPart); -} - -void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) -{ - if (ca.attacker != -1) - { - const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker); - for (auto attackInfo : ca.attackedParts) - { - owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); - } - } - else - { - std::vector positions; - - //no attacker stack, assume spell-related (earthquake) - only hit animation - for (auto attackInfo : ca.attackedParts) - positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); - - CCS->soundh->playSound( AudioPath::builtin("WALLHIT") ); - owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions)); - } - - owner.waitForAnimations(); - - for (auto attackInfo : ca.attackedParts) - { - int wallId = static_cast(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST; - //gate state changing handled separately - if (wallId == EWallVisual::GATE) - continue; - - auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart)); - - wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); - } -} - -const CGTownInstance *BattleSiegeController::getSiegedTown() const -{ - return town; -} +/* + * BattleSiegeController.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 "BattleSiegeController.h" + +#include "BattleAnimationClasses.h" +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleStacksController.h" +#include "BattleFieldController.h" +#include "BattleRenderer.h" + +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/NetPacks.h" +#include "../../lib/CStack.h" +#include "../../lib/mapObjects/CGTownInstance.h" + +ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const +{ + auto getImageIndex = [&]() -> int + { + bool isTower = (what == EWallVisual::KEEP || what == EWallVisual::BOTTOM_TOWER || what == EWallVisual::UPPER_TOWER); + + switch (state) + { + case EWallState::REINFORCED : + return 1; + case EWallState::INTACT : + if (town->hasBuilt(BuildingID::CASTLE)) + return 2; // reinforced walls were damaged + else + return 1; + case EWallState::DAMAGED : + // towers don't have separate image here - INTACT and DAMAGED is 1, DESTROYED is 2 + if (isTower) + return 1; + else + return 2; + case EWallState::DESTROYED : + if (isTower) + return 2; + else + return 3; + } + return 1; + }; + + const std::string & prefix = town->town->clientInfo.siegePrefix; + std::string addit = std::to_string(getImageIndex()); + + switch(what) + { + case EWallVisual::BACKGROUND_WALL: + { + auto faction = town->town->faction->getIndex(); + + if (faction == ETownType::RAMPART || faction == ETownType::NECROPOLIS || faction == ETownType::DUNGEON || faction == ETownType::STRONGHOLD) + return ImagePath::builtinTODO(prefix + "TPW1.BMP"); + else + return ImagePath::builtinTODO(prefix + "TPWL.BMP"); + } + case EWallVisual::KEEP: + return ImagePath::builtinTODO(prefix + "MAN" + addit + ".BMP"); + case EWallVisual::BOTTOM_TOWER: + return ImagePath::builtinTODO(prefix + "TW1" + addit + ".BMP"); + case EWallVisual::BOTTOM_WALL: + return ImagePath::builtinTODO(prefix + "WA1" + addit + ".BMP"); + case EWallVisual::WALL_BELLOW_GATE: + return ImagePath::builtinTODO(prefix + "WA3" + addit + ".BMP"); + case EWallVisual::WALL_OVER_GATE: + return ImagePath::builtinTODO(prefix + "WA4" + addit + ".BMP"); + case EWallVisual::UPPER_WALL: + return ImagePath::builtinTODO(prefix + "WA6" + addit + ".BMP"); + case EWallVisual::UPPER_TOWER: + return ImagePath::builtinTODO(prefix + "TW2" + addit + ".BMP"); + case EWallVisual::GATE: + return ImagePath::builtinTODO(prefix + "DRW" + addit + ".BMP"); + case EWallVisual::GATE_ARCH: + return ImagePath::builtinTODO(prefix + "ARCH.BMP"); + case EWallVisual::BOTTOM_STATIC_WALL: + return ImagePath::builtinTODO(prefix + "WA2.BMP"); + case EWallVisual::UPPER_STATIC_WALL: + return ImagePath::builtinTODO(prefix + "WA5.BMP"); + case EWallVisual::MOAT: + return ImagePath::builtinTODO(prefix + "MOAT.BMP"); + case EWallVisual::MOAT_BANK: + return ImagePath::builtinTODO(prefix + "MLIP.BMP"); + case EWallVisual::KEEP_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "MANC.BMP"); + case EWallVisual::BOTTOM_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "TW1C.BMP"); + case EWallVisual::UPPER_BATTLEMENT: + return ImagePath::builtinTODO(prefix + "TW2C.BMP"); + default: + return ImagePath(); + } +} + +void BattleSiegeController::showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what) +{ + auto & ci = town->town->clientInfo; + auto const & pos = ci.siegePositions[what]; + + if ( wallPieceImages[what] && pos.isValid()) + canvas.draw(wallPieceImages[what], Point(pos.x, pos.y)); +} + +ImagePath BattleSiegeController::getBattleBackgroundName() const +{ + const std::string & prefix = town->town->clientInfo.siegePrefix; + return ImagePath::builtinTODO(prefix + "BACK.BMP"); +} + +bool BattleSiegeController::getWallPieceExistance(EWallVisual::EWallVisual what) const +{ + //FIXME: use this instead of buildings test? + //ui8 siegeLevel = owner.curInt->cb->battleGetSiegeLevel(); + + switch (what) + { + case EWallVisual::MOAT: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT).isValid(); + case EWallVisual::MOAT_BANK: return town->hasBuilt(BuildingID::CITADEL) && town->town->clientInfo.siegePositions.at(EWallVisual::MOAT_BANK).isValid(); + case EWallVisual::KEEP_BATTLEMENT: return town->hasBuilt(BuildingID::CITADEL) && owner.getBattle()->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED; + case EWallVisual::UPPER_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED; + case EWallVisual::BOTTOM_BATTLEMENT: return town->hasBuilt(BuildingID::CASTLE) && owner.getBattle()->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED; + default: return true; + } +} + +BattleHex BattleSiegeController::getWallPiecePosition(EWallVisual::EWallVisual what) const +{ + static const std::array wallsPositions = { + BattleHex::INVALID, // BACKGROUND, // handled separately + BattleHex::HEX_BEFORE_ALL, // BACKGROUND_WALL, + 135, // KEEP, + BattleHex::HEX_AFTER_ALL, // BOTTOM_TOWER, + 182, // BOTTOM_WALL, + 130, // WALL_BELLOW_GATE, + 62, // WALL_OVER_GATE, + 12, // UPPER_WALL, + BattleHex::HEX_BEFORE_ALL, // UPPER_TOWER, + BattleHex::HEX_BEFORE_ALL, // GATE, // 94 + 112, // GATE_ARCH, + 165, // BOTTOM_STATIC_WALL, + 45, // UPPER_STATIC_WALL, + BattleHex::INVALID, // MOAT, // printed as absolute obstacle + BattleHex::INVALID, // MOAT_BANK, // printed as absolute obstacle + 135, // KEEP_BATTLEMENT, + BattleHex::HEX_AFTER_ALL, // BOTTOM_BATTLEMENT, + BattleHex::HEX_BEFORE_ALL, // UPPER_BATTLEMENT, + }; + + return wallsPositions[what]; +} + +BattleSiegeController::BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown): + owner(owner), + town(siegeTown) +{ + assert(owner.fieldController.get() == nullptr); // must be created after this + + for (int g = 0; g < wallPieceImages.size(); ++g) + { + if ( g == EWallVisual::GATE ) // gate is initially closed and has no image to display in this state + continue; + + if ( !getWallPieceExistance(EWallVisual::EWallVisual(g)) ) + continue; + + wallPieceImages[g] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(g), EWallState::REINFORCED)); + } +} + +const CCreature *BattleSiegeController::getTurretCreature() const +{ + return CGI->creh->objects[town->town->clientInfo.siegeShooter]; +} + +Point BattleSiegeController::getTurretCreaturePosition( BattleHex position ) const +{ + // Turret positions are read out of the config/wall_pos.txt + int posID = 0; + switch (position) + { + case BattleHex::CASTLE_CENTRAL_TOWER: // keep creature + posID = EWallVisual::CREATURE_KEEP; + break; + case BattleHex::CASTLE_BOTTOM_TOWER: // bottom creature + posID = EWallVisual::CREATURE_BOTTOM_TOWER; + break; + case BattleHex::CASTLE_UPPER_TOWER: // upper creature + posID = EWallVisual::CREATURE_UPPER_TOWER; + break; + } + + if (posID != 0) + { + return { + town->town->clientInfo.siegePositions[posID].x, + town->town->clientInfo.siegePositions[posID].y + }; + } + + assert(0); + return Point(0,0); +} + +void BattleSiegeController::gateStateChanged(const EGateState state) +{ + auto oldState = owner.getBattle()->battleGetGateState(); + bool playSound = false; + auto stateId = EWallState::NONE; + switch(state) + { + case EGateState::CLOSED: + if (oldState != EGateState::BLOCKED) + playSound = true; + break; + case EGateState::BLOCKED: + if (oldState != EGateState::CLOSED) + playSound = true; + break; + case EGateState::OPENED: + playSound = true; + stateId = EWallState::DAMAGED; + break; + case EGateState::DESTROYED: + stateId = EWallState::DESTROYED; + break; + } + + if (oldState != EGateState::NONE && oldState != EGateState::CLOSED && oldState != EGateState::BLOCKED) + wallPieceImages[EWallVisual::GATE] = nullptr; + + if (stateId != EWallState::NONE) + wallPieceImages[EWallVisual::GATE] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::GATE, stateId)); + + if (playSound) + CCS->soundh->playSound(soundBase::DRAWBRG); +} + +void BattleSiegeController::showAbsoluteObstacles(Canvas & canvas) +{ + if (getWallPieceExistance(EWallVisual::MOAT)) + showWallPiece(canvas, EWallVisual::MOAT); + + if (getWallPieceExistance(EWallVisual::MOAT_BANK)) + showWallPiece(canvas, EWallVisual::MOAT_BANK); +} + +BattleHex BattleSiegeController::getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const +{ + switch(wallPiece) + { + case EWallVisual::KEEP_BATTLEMENT: return BattleHex::CASTLE_CENTRAL_TOWER; + case EWallVisual::BOTTOM_BATTLEMENT: return BattleHex::CASTLE_BOTTOM_TOWER; + case EWallVisual::UPPER_BATTLEMENT: return BattleHex::CASTLE_UPPER_TOWER; + } + assert(0); + return BattleHex::INVALID; +} + +const CStack * BattleSiegeController::getTurretStack(EWallVisual::EWallVisual wallPiece) const +{ + for (auto & stack : owner.getBattle()->battleGetAllStacks(true)) + { + if ( stack->initialPosition == getTurretBattleHex(wallPiece)) + return stack; + } + assert(0); + return nullptr; +} + +void BattleSiegeController::collectRenderableObjects(BattleRenderer & renderer) +{ + for (int i = EWallVisual::WALL_FIRST; i <= EWallVisual::WALL_LAST; ++i) + { + auto wallPiece = EWallVisual::EWallVisual(i); + + if ( !getWallPieceExistance(wallPiece)) + continue; + + if ( getWallPiecePosition(wallPiece) == BattleHex::INVALID) + continue; + + if (wallPiece == EWallVisual::KEEP_BATTLEMENT || + wallPiece == EWallVisual::BOTTOM_BATTLEMENT || + wallPiece == EWallVisual::UPPER_BATTLEMENT) + { + renderer.insert( EBattleFieldLayer::STACKS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + owner.stacksController->showStack(canvas, getTurretStack(wallPiece)); + }); + renderer.insert( EBattleFieldLayer::OBSTACLES, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + showWallPiece(canvas, wallPiece); + }); + } + renderer.insert( EBattleFieldLayer::WALLS, getWallPiecePosition(wallPiece), [this, wallPiece](BattleRenderer::RendererRef canvas){ + showWallPiece(canvas, wallPiece); + }); + } +} + +bool BattleSiegeController::isAttackableByCatapult(BattleHex hex) const +{ + if (owner.tacticsMode) + return false; + + auto wallPart = owner.getBattle()->battleHexToWallPart(hex); + return owner.getBattle()->isWallPartAttackable(wallPart); +} + +void BattleSiegeController::stackIsCatapulting(const CatapultAttack & ca) +{ + if (ca.attacker != -1) + { + const CStack *stack = owner.getBattle()->battleGetStackByID(ca.attacker); + for (auto attackInfo : ca.attackedParts) + { + owner.stacksController->addNewAnim(new CatapultAnimation(owner, stack, attackInfo.destinationTile, nullptr, attackInfo.damageDealt)); + } + } + else + { + std::vector positions; + + //no attacker stack, assume spell-related (earthquake) - only hit animation + for (auto attackInfo : ca.attackedParts) + positions.push_back(owner.stacksController->getStackPositionAtHex(attackInfo.destinationTile, nullptr) + Point(99, 120)); + + CCS->soundh->playSound( AudioPath::builtin("WALLHIT") ); + owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::builtin("SGEXPL.DEF"), positions)); + } + + owner.waitForAnimations(); + + for (auto attackInfo : ca.attackedParts) + { + int wallId = static_cast(attackInfo.attackedPart) + EWallVisual::DESTRUCTIBLE_FIRST; + //gate state changing handled separately + if (wallId == EWallVisual::GATE) + continue; + + auto wallState = EWallState(owner.getBattle()->battleGetWallState(attackInfo.attackedPart)); + + wallPieceImages[wallId] = GH.renderHandler().loadImage(getWallPieceImageName(EWallVisual::EWallVisual(wallId), wallState)); + } +} + +const CGTownInstance *BattleSiegeController::getSiegedTown() const +{ + return town; +} diff --git a/client/battle/BattleSiegeController.h b/client/battle/BattleSiegeController.h index 7acb38f13..196967296 100644 --- a/client/battle/BattleSiegeController.h +++ b/client/battle/BattleSiegeController.h @@ -1,111 +1,111 @@ -/* - * BattleObstacleController.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/GameConstants.h" -#include "../../lib/battle/BattleHex.h" -#include "../../lib/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct CatapultAttack; -class CCreature; -class CStack; -class CGTownInstance; -class Point; - -VCMI_LIB_NAMESPACE_END - -class Canvas; -class BattleInterface; -class BattleRenderer; -class IImage; - -namespace EWallVisual -{ - enum EWallVisual - { - BACKGROUND, - BACKGROUND_WALL, - - KEEP, - BOTTOM_TOWER, - BOTTOM_WALL, - WALL_BELLOW_GATE, - WALL_OVER_GATE, - UPPER_WALL, - UPPER_TOWER, - GATE, - - GATE_ARCH, - BOTTOM_STATIC_WALL, - UPPER_STATIC_WALL, - MOAT, - MOAT_BANK, - KEEP_BATTLEMENT, - BOTTOM_BATTLEMENT, - UPPER_BATTLEMENT, - - CREATURE_KEEP, - CREATURE_BOTTOM_TOWER, - CREATURE_UPPER_TOWER, - - WALL_FIRST = BACKGROUND_WALL, - WALL_LAST = UPPER_BATTLEMENT, - - // these entries are mapped to EWallPart enum - DESTRUCTIBLE_FIRST = KEEP, - DESTRUCTIBLE_LAST = GATE, - }; -} - -class BattleSiegeController -{ - BattleInterface & owner; - - /// besieged town - const CGTownInstance *town; - - /// sections of castle walls, in their currently visible state - std::array, EWallVisual::WALL_LAST + 1> wallPieceImages; - - /// return URI for image for a wall piece - ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; - - /// returns BattleHex to which chosen wall piece is bound - BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; - - /// returns true if chosen wall piece should be present in current battle - bool getWallPieceExistance(EWallVisual::EWallVisual what) const; - - void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); - - BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const; - const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const; - -public: - BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown); - - /// call-ins from server - void gateStateChanged(const EGateState state); - void stackIsCatapulting(const CatapultAttack & ca); - - /// call-ins from other battle controllers - void showAbsoluteObstacles(Canvas & canvas); - void collectRenderableObjects(BattleRenderer & renderer); - - /// queries from other battle controllers - bool isAttackableByCatapult(BattleHex hex) const; - ImagePath getBattleBackgroundName() const; - const CCreature *getTurretCreature() const; - Point getTurretCreaturePosition( BattleHex position ) const; - - const CGTownInstance *getSiegedTown() const; -}; +/* + * BattleObstacleController.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/GameConstants.h" +#include "../../lib/battle/BattleHex.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CatapultAttack; +class CCreature; +class CStack; +class CGTownInstance; +class Point; + +VCMI_LIB_NAMESPACE_END + +class Canvas; +class BattleInterface; +class BattleRenderer; +class IImage; + +namespace EWallVisual +{ + enum EWallVisual + { + BACKGROUND, + BACKGROUND_WALL, + + KEEP, + BOTTOM_TOWER, + BOTTOM_WALL, + WALL_BELLOW_GATE, + WALL_OVER_GATE, + UPPER_WALL, + UPPER_TOWER, + GATE, + + GATE_ARCH, + BOTTOM_STATIC_WALL, + UPPER_STATIC_WALL, + MOAT, + MOAT_BANK, + KEEP_BATTLEMENT, + BOTTOM_BATTLEMENT, + UPPER_BATTLEMENT, + + CREATURE_KEEP, + CREATURE_BOTTOM_TOWER, + CREATURE_UPPER_TOWER, + + WALL_FIRST = BACKGROUND_WALL, + WALL_LAST = UPPER_BATTLEMENT, + + // these entries are mapped to EWallPart enum + DESTRUCTIBLE_FIRST = KEEP, + DESTRUCTIBLE_LAST = GATE, + }; +} + +class BattleSiegeController +{ + BattleInterface & owner; + + /// besieged town + const CGTownInstance *town; + + /// sections of castle walls, in their currently visible state + std::array, EWallVisual::WALL_LAST + 1> wallPieceImages; + + /// return URI for image for a wall piece + ImagePath getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const; + + /// returns BattleHex to which chosen wall piece is bound + BattleHex getWallPiecePosition(EWallVisual::EWallVisual what) const; + + /// returns true if chosen wall piece should be present in current battle + bool getWallPieceExistance(EWallVisual::EWallVisual what) const; + + void showWallPiece(Canvas & canvas, EWallVisual::EWallVisual what); + + BattleHex getTurretBattleHex(EWallVisual::EWallVisual wallPiece) const; + const CStack * getTurretStack(EWallVisual::EWallVisual wallPiece) const; + +public: + BattleSiegeController(BattleInterface & owner, const CGTownInstance *siegeTown); + + /// call-ins from server + void gateStateChanged(const EGateState state); + void stackIsCatapulting(const CatapultAttack & ca); + + /// call-ins from other battle controllers + void showAbsoluteObstacles(Canvas & canvas); + void collectRenderableObjects(BattleRenderer & renderer); + + /// queries from other battle controllers + bool isAttackableByCatapult(BattleHex hex) const; + ImagePath getBattleBackgroundName() const; + const CCreature *getTurretCreature() const; + Point getTurretCreaturePosition( BattleHex position ) const; + + const CGTownInstance *getSiegedTown() const; +}; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 9f0a86c3e..2a9531d44 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -1,894 +1,894 @@ -/* - * BattleStacksController.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 "BattleStacksController.h" - -#include "BattleSiegeController.h" -#include "BattleInterfaceClasses.h" -#include "BattleInterface.h" -#include "BattleActionsController.h" -#include "BattleAnimationClasses.h" -#include "BattleFieldController.h" -#include "BattleEffectsController.h" -#include "BattleProjectileController.h" -#include "BattleWindow.h" -#include "BattleRenderer.h" -#include "CreatureAnimation.h" - -#include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../CGameInfo.h" -#include "../gui/CGuiHandler.h" -#include "../render/Colors.h" -#include "../render/Canvas.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" -#include "../../lib/spells/ISpellMechanics.h" -#include "../../lib/battle/BattleAction.h" -#include "../../lib/battle/BattleHex.h" -#include "../../lib/CStack.h" -#include "../../lib/CondSh.h" -#include "../../lib/TextOperations.h" - -static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) -{ - std::shared_ptr animation = anim.lock(); - if(!animation) - return; - - if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN) - animation->setType(ECreatureAnimType::HOLDING); - - if (animation->isIdle()) - { - const CCreature *creature = stack->unitType(); - - if (stack->isFrozen()) - animation->setType(ECreatureAnimType::FROZEN); - else - if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0) - { - if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) - animation->playOnce(ECreatureAnimType::MOUSEON); - else - animation->setType(ECreatureAnimType::HOLDING); - } - else - { - animation->setType(ECreatureAnimType::HOLDING); - } - } - // always reset callback - animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim); -} - -BattleStacksController::BattleStacksController(BattleInterface & owner): - owner(owner), - activeStack(nullptr), - stackToActivate(nullptr), - animIDhelper(0) -{ - //preparing graphics for displaying amounts of creatures - amountNormal = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountPositive = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountNegative = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); - - static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); - static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); - static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f ); - static const auto shifterNeutral = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f ); - - // do not change border color - static const int32_t ignoredMask = 1 << 26; - - amountNormal->adjustPalette(shifterNormal, ignoredMask); - amountPositive->adjustPalette(shifterPositive, ignoredMask); - amountNegative->adjustPalette(shifterNegative, ignoredMask); - amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask); - - std::vector stacks = owner.getBattle()->battleGetAllStacks(true); - for(const CStack * s : stacks) - { - stackAdded(s, true); - } -} - -BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const -{ - if ( !stackAnimation.at(stack->unitId())->isMoving()) - return stack->getPosition(); - - if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING ) - return BattleHex::HEX_AFTER_ALL; - - for (auto & anim : currentAnimations) - { - // certainly ugly workaround but fixes quite annoying bug - // stack position will be updated only *after* movement is finished - // before this - stack is always at its initial position. Thus we need to find - // its current position. Which can be found only in this class - if (StackMoveAnimation *move = dynamic_cast(anim)) - { - if (move->stack == stack) - return std::max(move->prevHex, move->nextHex); - } - } - return stack->getPosition(); -} - -void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) -{ - auto stacks = owner.getBattle()->battleGetAllStacks(false); - - for (auto stack : stacks) - { - if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks - continue; - - //FIXME: hack to ignore ghost stacks - if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost()) - continue; - - auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS; - auto location = getStackCurrentPosition(stack); - - renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){ - showStack(renderer, stack); - }); - - if (stackNeedsAmountBox(stack)) - { - renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){ - showStackAmountBox(renderer, stack); - }); - } - } -} - -void BattleStacksController::stackReset(const CStack * stack) -{ - auto iter = stackAnimation.find(stack->unitId()); - - if(iter == stackAnimation.end()) - { - logGlobal->error("Unit %d have no animation", stack->unitId()); - return; - } - - auto animation = iter->second; - - if(stack->alive() && animation->isDeadOrDying()) - { - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - addNewAnim(new ResurrectionAnimation(owner, stack)); - }); - } -} - -void BattleStacksController::stackAdded(const CStack * stack, bool instant) -{ - // Tower shooters have only their upper half visible - static const int turretCreatureAnimationHeight = 232; - - stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position - - Point coords = getStackPositionAtHex(stack->getPosition(), stack); - - if(stack->initialPosition < 0) //turret - { - assert(owner.siegeController); - - const CCreature *turretCreature = owner.siegeController->getTurretCreature(); - - stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature); - stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight; - stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); - - // FIXME: workaround for visible animation of Medusa tails (animation disabled in H3) - if (turretCreature->getId() == CreatureID::MEDUSA ) - stackAnimation[stack->unitId()]->pos.w = 250; - - coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition); - } - else - { - stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType()); - stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]); - stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight(); - stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); - } - stackAnimation[stack->unitId()]->pos.x = coords.x; - stackAnimation[stack->unitId()]->pos.y = coords.y; - stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING); - - if (!instant) - { - // immediately make stack transparent, giving correct shifter time to start - auto shifterFade = ColorFilter::genAlphaShifter(0); - setStackColorFilter(shifterFade, stack, nullptr, true); - - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr)); - if (stack->isClone()) - addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() )); - }); - } -} - -void BattleStacksController::setActiveStack(const CStack *stack) -{ - if (activeStack) // update UI - stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); - - activeStack = stack; - - if (activeStack) // update UI - stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); - - owner.windowObject->blockUI(activeStack == nullptr); - - if (activeStack) - stackAmountBoxHidden.clear(); -} - -bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const -{ - //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature - if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1) - return false; - - if(!stack->alive()) - return false; - - //hide box when target is going to die anyway - do not display "0 creatures" - if(stack->getCount() == 0) - return false; - - // if stack has any ongoing animation - hide the box - if (stackAmountBoxHidden.count(stack->unitId())) - return false; - - return true; -} - -std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * stack) -{ - std::vector activeSpells = stack->activeSpells(); - - if ( activeSpells.empty()) - return amountNormal; - - int effectsPositivness = 0; - - for(const auto & spellID : activeSpells) - { - auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness(); - if(!boost::logic::indeterminate(positiveness)) - { - if(positiveness) - effectsPositivness++; - else - effectsPositivness--; - } - } - - if (effectsPositivness > 0) - return amountPositive; - - if (effectsPositivness < 0) - return amountNegative; - - return amountEffNeutral; -} - -void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack) -{ - auto amountBG = getStackAmountBox(stack); - - bool doubleWide = stack->doubleWide(); - bool turnedRight = facingRight(stack); - bool attacker = stack->unitSide() == BattleSide::ATTACKER; - - BattleHex stackPos = stack->getPosition(); - - // double-wide unit turned around - use opposite hex for stack label - if (doubleWide && turnedRight != attacker) - stackPos = stack->occupiedHex(); - - BattleHex frontPos = turnedRight ? - stackPos.cloneInDirection(BattleHex::RIGHT) : - stackPos.cloneInDirection(BattleHex::LEFT); - - bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos); - - Point boxPosition; - - if (moveInside) - { - boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1); - } - else - { - if (turnedRight) - boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1); - else - boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); - } - - Point textPosition = amountBG->dimensions()/2 + boxPosition; - - canvas.draw(amountBG, boxPosition); - canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); -} - -void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) -{ - ColorFilter fullFilter = ColorFilter::genEmptyShifter(); - for(const auto & filter : stackFilterEffects) - { - if (filter.target == stack) - fullFilter = ColorFilter::genCombined(fullFilter, filter.effect); - } - - stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit -} - -void BattleStacksController::tick(uint32_t msPassed) -{ - updateHoveredStacks(); - updateBattleAnimations(msPassed); -} - -void BattleStacksController::initializeBattleAnimations() -{ - auto copiedVector = currentAnimations; - for (auto & elem : copiedVector) - if (elem && !elem->isInitialized()) - elem->tryInitialize(); -} - -void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed) -{ - for (auto stack : owner.getBattle()->battleGetAllStacks(true)) - { - if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks - continue; - - stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f); - } - - // operate on copy - to prevent potential iterator invalidation due to push_back's - // FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing) - - auto copiedVector = currentAnimations; - for (auto & elem : copiedVector) - if (elem && elem->isInitialized()) - elem->tick(msPassed); -} - -void BattleStacksController::updateBattleAnimations(uint32_t msPassed) -{ - bool hadAnimations = !currentAnimations.empty(); - initializeBattleAnimations(); - tickFrameBattleAnimations(msPassed); - vstd::erase(currentAnimations, nullptr); - - if (currentAnimations.empty()) - owner.executeStagedAnimations(); - - if (hadAnimations && currentAnimations.empty()) - owner.onAnimationsFinished(); - - initializeBattleAnimations(); -} - -void BattleStacksController::addNewAnim(BattleAnimation *anim) -{ - if (currentAnimations.empty()) - stackAmountBoxHidden.clear(); - - owner.onAnimationsStarted(); - currentAnimations.push_back(anim); - - auto stackAnimation = dynamic_cast(anim); - if(stackAnimation) - stackAmountBoxHidden.insert(stackAnimation->stack->unitId()); -} - -void BattleStacksController::stackRemoved(uint32_t stackID) -{ - if (getActiveStack() && getActiveStack()->unitId() == stackID) - setActiveStack(nullptr); -} - -void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) -{ - owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ - // remove any potentially erased petrification effect - removeExpiredColorFilters(); - }); - - for(auto & attackedInfo : attackedInfos) - { - if (!attackedInfo.attacker) - continue; - - // In H3, attacked stack will not reverse on ranged attack - if (attackedInfo.indirectAttack) - continue; - - // Another type of indirect attack - dragon breath - if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender)) - continue; - - // defender need to face in direction opposited to out attacker - bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender); - - // FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed - // if (needsReverse && !attackedInfo.defender->isFrozen()) - if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() - { - addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition())); - }); - } - } - - for(auto & attackedInfo : attackedInfos) - { - bool useDeathAnim = attackedInfo.killed; - bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed; - - EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT; - - owner.addToAnimationStage(usedEvent, [=]() - { - if (useDeathAnim) - addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack)); - else if(useDefenceAnim) - addNewAnim(new DefenceAnimation(owner, attackedInfo.defender)); - else - addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); - - if (attackedInfo.fireShield) - owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition()); - - if (attackedInfo.spellEffect != SpellID::NONE) - { - auto spell = attackedInfo.spellEffect.toSpell(); - if (!spell->getCastSound().empty()) - CCS->soundh->playSound(spell->getCastSound()); - - - owner.displaySpellEffect(spell, attackedInfo.defender->getPosition()); - } - }); - } - - for (auto & attackedInfo : attackedInfos) - { - if (attackedInfo.rebirth) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition()); - addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); - }); - } - - if (attackedInfo.killed && attackedInfo.defender->summoned) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr)); - stackRemoved(attackedInfo.defender->unitId()); - }); - } - } - owner.executeStagedAnimations(); - owner.waitForAnimations(); -} - -void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) -{ - assert(destHex.size() > 0); - //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed - - owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ - addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); - }); - - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ - stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack)); - addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) ); - }); - - // animations will be executed by spell -} - -void BattleStacksController::stackMoved(const CStack *stack, std::vector destHex, int distance) -{ - assert(destHex.size() > 0); - owner.checkForAnimations(); - - if(shouldRotate(stack, stack->getPosition(), destHex[0])) - { - owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]() - { - addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition())); - }); - } - - owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]() - { - addNewAnim(new MovementStartAnimation(owner, stack)); - }); - - if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, 1))) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() - { - addNewAnim(new MovementAnimation(owner, stack, destHex, distance)); - }); - } - - owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]() - { - addNewAnim(new MovementEndAnimation(owner, stack, destHex.back())); - }); - - owner.executeStagedAnimations(); - owner.waitForAnimations(); -} - -bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender) -{ - bool mustReverse = owner.getBattle()->isToReverse(attacker, defender); - - if (attacker->unitSide() == BattleSide::ATTACKER) - return !mustReverse; - else - return mustReverse; -} - -void BattleStacksController::stackAttacking( const StackAttackInfo & info ) -{ - owner.checkForAnimations(); - - auto attacker = info.attacker; - auto defender = info.defender; - auto tile = info.tile; - auto spellEffect = info.spellEffect; - auto multiAttack = !info.secondaryDefender.empty(); - bool needsReverse = false; - - if (info.indirectAttack) - { - needsReverse = shouldRotate(attacker, attacker->position, info.tile); - } - else - { - needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker); - } - - if (needsReverse) - { - owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() - { - addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition())); - }); - } - - if(info.lucky) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); - owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition()); - }); - } - - if(info.unlucky) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); - owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition()); - }); - } - - if(info.deathBlow) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); - owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition()); - }); - - for(auto elem : info.secondaryDefender) - { - owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { - owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition()); - }); - } - } - - owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]() - { - if (info.indirectAttack) - { - addNewAnim(new ShootingAnimation(owner, attacker, tile, defender)); - } - else - { - addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack)); - } - }); - - if (info.spellEffect != SpellID::NONE) - { - owner.addToAnimationStage(EAnimationEvents::HIT, [=]() - { - owner.displaySpellHit(spellEffect.toSpell(), tile); - }); - } - - if (info.lifeDrain) - { - owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() - { - owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition()); - }); - } - - //return, animation playback will be handled by stacksAreAttacked -} - -bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const -{ - Point begPosition = getStackPositionAtHex(oldPos,stack); - Point endPosition = getStackPositionAtHex(nextHex, stack); - - if((begPosition.x > endPosition.x) && facingRight(stack)) - return true; - else if((begPosition.x < endPosition.x) && !facingRight(stack)) - return true; - - return false; -} - -void BattleStacksController::endAction(const BattleAction & action) -{ - owner.checkForAnimations(); - - //check if we should reverse stacks - TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY); - - for (const CStack *s : stacks) - { - bool shouldFaceRight = s && s->unitSide() == BattleSide::ATTACKER; - - if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle()) - { - addNewAnim(new ReverseAnimation(owner, s, s->getPosition())); - } - } - owner.executeStagedAnimations(); - owner.waitForAnimations(); - - stackAmountBoxHidden.clear(); - - owner.windowObject->blockUI(activeStack == nullptr); - removeExpiredColorFilters(); -} - -void BattleStacksController::startAction(const BattleAction & action) -{ - removeExpiredColorFilters(); -} - -void BattleStacksController::stackActivated(const CStack *stack) -{ - stackToActivate = stack; - owner.waitForAnimations(); - logAnim->debug("Activating next stack"); - owner.activateStack(); -} - -void BattleStacksController::deactivateStack() -{ - if (!activeStack) { - return; - } - stackToActivate = activeStack; - setActiveStack(nullptr); -} - -void BattleStacksController::activateStack() -{ - if ( !currentAnimations.empty()) - return; - - if ( !stackToActivate) - return; - - owner.trySetActivePlayer(stackToActivate->unitOwner()); - - setActiveStack(stackToActivate); - stackToActivate = nullptr; - - const CStack * s = getActiveStack(); - if(!s) - return; -} - -const CStack* BattleStacksController::getActiveStack() const -{ - return activeStack; -} - -bool BattleStacksController::facingRight(const CStack * stack) const -{ - return stackFacingRight.at(stack->unitId()); -} - -Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const -{ - Point ret(-500, -500); //returned value - if(stack && stack->initialPosition < 0) //creatures in turrets - return owner.siegeController->getTurretCreaturePosition(stack->initialPosition); - - static const Point basePos(-189, -139); // position of creature in topleft corner - static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left - - ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX(); - ret.y = basePos.y + 42 * hexNum.getY(); - - if (stack) - { - if(facingRight(stack)) - ret.x += imageShiftX; - else - ret.x -= imageShiftX; - - //shifting position for double - hex creatures - if(stack->doubleWide()) - { - if(stack->unitSide() == BattleSide::ATTACKER) - { - if(facingRight(stack)) - ret.x -= 44; - } - else - { - if(!facingRight(stack)) - ret.x += 44; - } - } - } - //returning - return ret; -} - -void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent) -{ - for (auto & filter : stackFilterEffects) - { - if (filter.target == target && filter.source == source) - { - filter.effect = effect; - filter.persistent = persistent; - return; - } - } - stackFilterEffects.push_back({ effect, target, source, persistent }); -} - -void BattleStacksController::removeExpiredColorFilters() -{ - vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter) - { - if (!filter.persistent) - { - if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, filter.source->id), Selector::all)) - return true; - if (filter.effect == ColorFilter::genEmptyShifter()) - return true; - } - return false; - }); -} - -void BattleStacksController::updateHoveredStacks() -{ - auto newStacks = selectHoveredStacks(); - - for(const auto * stack : mouseHoveredStacks) - { - if (vstd::contains(newStacks, stack)) - continue; - - if (stack == activeStack) - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); - else - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); - } - - for(const auto * stack : newStacks) - { - if (vstd::contains(mouseHoveredStacks, stack)) - continue; - - stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); - if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) - stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); - } - - mouseHoveredStacks = newStacks; -} - -std::vector BattleStacksController::selectHoveredStacks() -{ - // only allow during our turn - do not try to highlight creatures while they are in the middle of actions - if (!activeStack) - return {}; - - if(owner.hasAnimations()) - return {}; - - auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId(); - if(hoveredQueueUnitId.has_value()) - { - return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) }; - } - - auto hoveredHex = owner.fieldController->getHoveredHex(); - - if (!hoveredHex.isValid()) - return {}; - - const spells::Caster *caster = nullptr; - const CSpell *spell = nullptr; - - spells::Mode mode = owner.actionsController->getCurrentCastMode(); - spell = owner.actionsController->getCurrentSpell(hoveredHex); - caster = owner.actionsController->getCurrentSpellcaster(); - - if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell - { - spells::Target target; - target.emplace_back(hoveredHex); - - spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); - auto mechanics = spell->battleMechanics(&event); - return mechanics->getAffectedStacks(target); - } - - if(hoveredHex.isValid()) - { - const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); - - if (stack) - return {stack}; - } - - return {}; -} - -const std::vector BattleStacksController::getHoveredStacksUnitIds() const -{ - auto result = std::vector(); - for(const auto * stack : mouseHoveredStacks) - { - result.push_back(stack->unitId()); - } - - return result; -} +/* + * BattleStacksController.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 "BattleStacksController.h" + +#include "BattleSiegeController.h" +#include "BattleInterfaceClasses.h" +#include "BattleInterface.h" +#include "BattleActionsController.h" +#include "BattleAnimationClasses.h" +#include "BattleFieldController.h" +#include "BattleEffectsController.h" +#include "BattleProjectileController.h" +#include "BattleWindow.h" +#include "BattleRenderer.h" +#include "CreatureAnimation.h" + +#include "../CPlayerInterface.h" +#include "../CMusicHandler.h" +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../render/Colors.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" +#include "../../lib/spells/ISpellMechanics.h" +#include "../../lib/battle/BattleAction.h" +#include "../../lib/battle/BattleHex.h" +#include "../../lib/CStack.h" +#include "../../lib/CondSh.h" +#include "../../lib/TextOperations.h" + +static void onAnimationFinished(const CStack *stack, std::weak_ptr anim) +{ + std::shared_ptr animation = anim.lock(); + if(!animation) + return; + + if (!stack->isFrozen() && animation->getType() == ECreatureAnimType::FROZEN) + animation->setType(ECreatureAnimType::HOLDING); + + if (animation->isIdle()) + { + const CCreature *creature = stack->unitType(); + + if (stack->isFrozen()) + animation->setType(ECreatureAnimType::FROZEN); + else + if (animation->framesInGroup(ECreatureAnimType::MOUSEON) > 0) + { + if (CRandomGenerator::getDefault().nextDouble(99.0) < creature->animation.timeBetweenFidgets *10) + animation->playOnce(ECreatureAnimType::MOUSEON); + else + animation->setType(ECreatureAnimType::HOLDING); + } + else + { + animation->setType(ECreatureAnimType::HOLDING); + } + } + // always reset callback + animation->onAnimationReset += std::bind(&onAnimationFinished, stack, anim); +} + +BattleStacksController::BattleStacksController(BattleInterface & owner): + owner(owner), + activeStack(nullptr), + stackToActivate(nullptr), + animIDhelper(0) +{ + //preparing graphics for displaying amounts of creatures + amountNormal = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountPositive = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountNegative = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY); + + static const auto shifterNormal = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f ); + static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f ); + static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f ); + static const auto shifterNeutral = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f ); + + // do not change border color + static const int32_t ignoredMask = 1 << 26; + + amountNormal->adjustPalette(shifterNormal, ignoredMask); + amountPositive->adjustPalette(shifterPositive, ignoredMask); + amountNegative->adjustPalette(shifterNegative, ignoredMask); + amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask); + + std::vector stacks = owner.getBattle()->battleGetAllStacks(true); + for(const CStack * s : stacks) + { + stackAdded(s, true); + } +} + +BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const +{ + if ( !stackAnimation.at(stack->unitId())->isMoving()) + return stack->getPosition(); + + if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING ) + return BattleHex::HEX_AFTER_ALL; + + for (auto & anim : currentAnimations) + { + // certainly ugly workaround but fixes quite annoying bug + // stack position will be updated only *after* movement is finished + // before this - stack is always at its initial position. Thus we need to find + // its current position. Which can be found only in this class + if (StackMoveAnimation *move = dynamic_cast(anim)) + { + if (move->stack == stack) + return std::max(move->prevHex, move->nextHex); + } + } + return stack->getPosition(); +} + +void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer) +{ + auto stacks = owner.getBattle()->battleGetAllStacks(false); + + for (auto stack : stacks) + { + if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks + continue; + + //FIXME: hack to ignore ghost stacks + if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost()) + continue; + + auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS; + auto location = getStackCurrentPosition(stack); + + renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){ + showStack(renderer, stack); + }); + + if (stackNeedsAmountBox(stack)) + { + renderer.insert(EBattleFieldLayer::STACK_AMOUNTS, location, [this, stack]( BattleRenderer::RendererRef renderer ){ + showStackAmountBox(renderer, stack); + }); + } + } +} + +void BattleStacksController::stackReset(const CStack * stack) +{ + auto iter = stackAnimation.find(stack->unitId()); + + if(iter == stackAnimation.end()) + { + logGlobal->error("Unit %d have no animation", stack->unitId()); + return; + } + + auto animation = iter->second; + + if(stack->alive() && animation->isDeadOrDying()) + { + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + addNewAnim(new ResurrectionAnimation(owner, stack)); + }); + } +} + +void BattleStacksController::stackAdded(const CStack * stack, bool instant) +{ + // Tower shooters have only their upper half visible + static const int turretCreatureAnimationHeight = 232; + + stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position + + Point coords = getStackPositionAtHex(stack->getPosition(), stack); + + if(stack->initialPosition < 0) //turret + { + assert(owner.siegeController); + + const CCreature *turretCreature = owner.siegeController->getTurretCreature(); + + stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature); + stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight; + stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); + + // FIXME: workaround for visible animation of Medusa tails (animation disabled in H3) + if (turretCreature->getId() == CreatureID::MEDUSA ) + stackAnimation[stack->unitId()]->pos.w = 250; + + coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition); + } + else + { + stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType()); + stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]); + stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight(); + stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth(); + } + stackAnimation[stack->unitId()]->pos.x = coords.x; + stackAnimation[stack->unitId()]->pos.y = coords.y; + stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING); + + if (!instant) + { + // immediately make stack transparent, giving correct shifter time to start + auto shifterFade = ColorFilter::genAlphaShifter(0); + setStackColorFilter(shifterFade, stack, nullptr, true); + + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + addNewAnim(new ColorTransformAnimation(owner, stack, "summonFadeIn", nullptr)); + if (stack->isClone()) + addNewAnim(new ColorTransformAnimation(owner, stack, "cloning", SpellID(SpellID::CLONE).toSpell() )); + }); + } +} + +void BattleStacksController::setActiveStack(const CStack *stack) +{ + if (activeStack) // update UI + stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); + + activeStack = stack; + + if (activeStack) // update UI + stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); + + owner.windowObject->blockUI(activeStack == nullptr); + + if (activeStack) + stackAmountBoxHidden.clear(); +} + +bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const +{ + //do not show box for singular war machines, stacked war machines with box shown are supported as extension feature + if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1) + return false; + + if(!stack->alive()) + return false; + + //hide box when target is going to die anyway - do not display "0 creatures" + if(stack->getCount() == 0) + return false; + + // if stack has any ongoing animation - hide the box + if (stackAmountBoxHidden.count(stack->unitId())) + return false; + + return true; +} + +std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * stack) +{ + std::vector activeSpells = stack->activeSpells(); + + if ( activeSpells.empty()) + return amountNormal; + + int effectsPositivness = 0; + + for(const auto & spellID : activeSpells) + { + auto positiveness = CGI->spells()->getByIndex(spellID)->getPositiveness(); + if(!boost::logic::indeterminate(positiveness)) + { + if(positiveness) + effectsPositivness++; + else + effectsPositivness--; + } + } + + if (effectsPositivness > 0) + return amountPositive; + + if (effectsPositivness < 0) + return amountNegative; + + return amountEffNeutral; +} + +void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack * stack) +{ + auto amountBG = getStackAmountBox(stack); + + bool doubleWide = stack->doubleWide(); + bool turnedRight = facingRight(stack); + bool attacker = stack->unitSide() == BattleSide::ATTACKER; + + BattleHex stackPos = stack->getPosition(); + + // double-wide unit turned around - use opposite hex for stack label + if (doubleWide && turnedRight != attacker) + stackPos = stack->occupiedHex(); + + BattleHex frontPos = turnedRight ? + stackPos.cloneInDirection(BattleHex::RIGHT) : + stackPos.cloneInDirection(BattleHex::LEFT); + + bool moveInside = !owner.fieldController->stackCountOutsideHex(frontPos); + + Point boxPosition; + + if (moveInside) + { + boxPosition = owner.fieldController->hexPositionLocal(stackPos).center() + Point(-15, 1); + } + else + { + if (turnedRight) + boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point (-22, 1); + else + boxPosition = owner.fieldController->hexPositionLocal(frontPos).center() + Point(-8, -14); + } + + Point textPosition = amountBG->dimensions()/2 + boxPosition; + + canvas.draw(amountBG, boxPosition); + canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4)); +} + +void BattleStacksController::showStack(Canvas & canvas, const CStack * stack) +{ + ColorFilter fullFilter = ColorFilter::genEmptyShifter(); + for(const auto & filter : stackFilterEffects) + { + if (filter.target == stack) + fullFilter = ColorFilter::genCombined(fullFilter, filter.effect); + } + + stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit +} + +void BattleStacksController::tick(uint32_t msPassed) +{ + updateHoveredStacks(); + updateBattleAnimations(msPassed); +} + +void BattleStacksController::initializeBattleAnimations() +{ + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && !elem->isInitialized()) + elem->tryInitialize(); +} + +void BattleStacksController::tickFrameBattleAnimations(uint32_t msPassed) +{ + for (auto stack : owner.getBattle()->battleGetAllStacks(true)) + { + if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks + continue; + + stackAnimation[stack->unitId()]->incrementFrame(msPassed / 1000.f); + } + + // operate on copy - to prevent potential iterator invalidation due to push_back's + // FIXME? : remove remaining calls to addNewAnim from BattleAnimation::nextFrame (only Catapult explosion at the time of writing) + + auto copiedVector = currentAnimations; + for (auto & elem : copiedVector) + if (elem && elem->isInitialized()) + elem->tick(msPassed); +} + +void BattleStacksController::updateBattleAnimations(uint32_t msPassed) +{ + bool hadAnimations = !currentAnimations.empty(); + initializeBattleAnimations(); + tickFrameBattleAnimations(msPassed); + vstd::erase(currentAnimations, nullptr); + + if (currentAnimations.empty()) + owner.executeStagedAnimations(); + + if (hadAnimations && currentAnimations.empty()) + owner.onAnimationsFinished(); + + initializeBattleAnimations(); +} + +void BattleStacksController::addNewAnim(BattleAnimation *anim) +{ + if (currentAnimations.empty()) + stackAmountBoxHidden.clear(); + + owner.onAnimationsStarted(); + currentAnimations.push_back(anim); + + auto stackAnimation = dynamic_cast(anim); + if(stackAnimation) + stackAmountBoxHidden.insert(stackAnimation->stack->unitId()); +} + +void BattleStacksController::stackRemoved(uint32_t stackID) +{ + if (getActiveStack() && getActiveStack()->unitId() == stackID) + setActiveStack(nullptr); +} + +void BattleStacksController::stacksAreAttacked(std::vector attackedInfos) +{ + owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ + // remove any potentially erased petrification effect + removeExpiredColorFilters(); + }); + + for(auto & attackedInfo : attackedInfos) + { + if (!attackedInfo.attacker) + continue; + + // In H3, attacked stack will not reverse on ranged attack + if (attackedInfo.indirectAttack) + continue; + + // Another type of indirect attack - dragon breath + if (!CStack::isMeleeAttackPossible(attackedInfo.attacker, attackedInfo.defender)) + continue; + + // defender need to face in direction opposited to out attacker + bool needsReverse = shouldAttackFacingRight(attackedInfo.attacker, attackedInfo.defender) == facingRight(attackedInfo.defender); + + // FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed + // if (needsReverse && !attackedInfo.defender->isFrozen()) + if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() + { + addNewAnim(new ReverseAnimation(owner, attackedInfo.defender, attackedInfo.defender->getPosition())); + }); + } + } + + for(auto & attackedInfo : attackedInfos) + { + bool useDeathAnim = attackedInfo.killed; + bool useDefenceAnim = attackedInfo.defender->defendingAnim && !attackedInfo.indirectAttack && !attackedInfo.killed; + + EAnimationEvents usedEvent = useDefenceAnim ? EAnimationEvents::ATTACK : EAnimationEvents::HIT; + + owner.addToAnimationStage(usedEvent, [=]() + { + if (useDeathAnim) + addNewAnim(new DeathAnimation(owner, attackedInfo.defender, attackedInfo.indirectAttack)); + else if(useDefenceAnim) + addNewAnim(new DefenceAnimation(owner, attackedInfo.defender)); + else + addNewAnim(new HittedAnimation(owner, attackedInfo.defender)); + + if (attackedInfo.fireShield) + owner.effectsController->displayEffect(EBattleEffect::FIRE_SHIELD, AudioPath::builtin("FIRESHIE"), attackedInfo.attacker->getPosition()); + + if (attackedInfo.spellEffect != SpellID::NONE) + { + auto spell = attackedInfo.spellEffect.toSpell(); + if (!spell->getCastSound().empty()) + CCS->soundh->playSound(spell->getCastSound()); + + + owner.displaySpellEffect(spell, attackedInfo.defender->getPosition()); + } + }); + } + + for (auto & attackedInfo : attackedInfos) + { + if (attackedInfo.rebirth) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + owner.effectsController->displayEffect(EBattleEffect::RESURRECT, AudioPath::builtin("RESURECT"), attackedInfo.defender->getPosition()); + addNewAnim(new ResurrectionAnimation(owner, attackedInfo.defender)); + }); + } + + if (attackedInfo.killed && attackedInfo.defender->summoned) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr)); + stackRemoved(attackedInfo.defender->unitId()); + }); + } + } + owner.executeStagedAnimations(); + owner.waitForAnimations(); +} + +void BattleStacksController::stackTeleported(const CStack *stack, std::vector destHex, int distance) +{ + assert(destHex.size() > 0); + //owner.checkForAnimations(); // NOTE: at this point spellcast animations were added, but not executed + + owner.addToAnimationStage(EAnimationEvents::HIT, [=](){ + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeOut", nullptr) ); + }); + + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){ + stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack)); + addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) ); + }); + + // animations will be executed by spell +} + +void BattleStacksController::stackMoved(const CStack *stack, std::vector destHex, int distance) +{ + assert(destHex.size() > 0); + owner.checkForAnimations(); + + if(shouldRotate(stack, stack->getPosition(), destHex[0])) + { + owner.addToAnimationStage(EAnimationEvents::ROTATE, [&]() + { + addNewAnim(new ReverseAnimation(owner, stack, stack->getPosition())); + }); + } + + owner.addToAnimationStage(EAnimationEvents::MOVE_START, [&]() + { + addNewAnim(new MovementStartAnimation(owner, stack)); + }); + + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, 1))) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() + { + addNewAnim(new MovementAnimation(owner, stack, destHex, distance)); + }); + } + + owner.addToAnimationStage(EAnimationEvents::MOVE_END, [&]() + { + addNewAnim(new MovementEndAnimation(owner, stack, destHex.back())); + }); + + owner.executeStagedAnimations(); + owner.waitForAnimations(); +} + +bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, const CStack * defender) +{ + bool mustReverse = owner.getBattle()->isToReverse(attacker, defender); + + if (attacker->unitSide() == BattleSide::ATTACKER) + return !mustReverse; + else + return mustReverse; +} + +void BattleStacksController::stackAttacking( const StackAttackInfo & info ) +{ + owner.checkForAnimations(); + + auto attacker = info.attacker; + auto defender = info.defender; + auto tile = info.tile; + auto spellEffect = info.spellEffect; + auto multiAttack = !info.secondaryDefender.empty(); + bool needsReverse = false; + + if (info.indirectAttack) + { + needsReverse = shouldRotate(attacker, attacker->position, info.tile); + } + else + { + needsReverse = shouldAttackFacingRight(attacker, defender) != facingRight(attacker); + } + + if (needsReverse) + { + owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]() + { + addNewAnim(new ReverseAnimation(owner, attacker, attacker->getPosition())); + }); + } + + if(info.lucky) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-45)); + owner.effectsController->displayEffect(EBattleEffect::GOOD_LUCK, AudioPath::builtin("GOODLUCK"), attacker->getPosition()); + }); + } + + if(info.unlucky) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(-44)); + owner.effectsController->displayEffect(EBattleEffect::BAD_LUCK, AudioPath::builtin("BADLUCK"), attacker->getPosition()); + }); + } + + if(info.deathBlow) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.appendBattleLog(info.attacker->formatGeneralMessage(365)); + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, AudioPath::builtin("DEATHBLO"), defender->getPosition()); + }); + + for(auto elem : info.secondaryDefender) + { + owner.addToAnimationStage(EAnimationEvents::BEFORE_HIT, [=]() { + owner.effectsController->displayEffect(EBattleEffect::DEATH_BLOW, elem->getPosition()); + }); + } + } + + owner.addToAnimationStage(EAnimationEvents::ATTACK, [=]() + { + if (info.indirectAttack) + { + addNewAnim(new ShootingAnimation(owner, attacker, tile, defender)); + } + else + { + addNewAnim(new MeleeAttackAnimation(owner, attacker, tile, defender, multiAttack)); + } + }); + + if (info.spellEffect != SpellID::NONE) + { + owner.addToAnimationStage(EAnimationEvents::HIT, [=]() + { + owner.displaySpellHit(spellEffect.toSpell(), tile); + }); + } + + if (info.lifeDrain) + { + owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]() + { + owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition()); + }); + } + + //return, animation playback will be handled by stacksAreAttacked +} + +bool BattleStacksController::shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const +{ + Point begPosition = getStackPositionAtHex(oldPos,stack); + Point endPosition = getStackPositionAtHex(nextHex, stack); + + if((begPosition.x > endPosition.x) && facingRight(stack)) + return true; + else if((begPosition.x < endPosition.x) && !facingRight(stack)) + return true; + + return false; +} + +void BattleStacksController::endAction(const BattleAction & action) +{ + owner.checkForAnimations(); + + //check if we should reverse stacks + TStacks stacks = owner.getBattle()->battleGetStacks(CPlayerBattleCallback::MINE_AND_ENEMY); + + for (const CStack *s : stacks) + { + bool shouldFaceRight = s && s->unitSide() == BattleSide::ATTACKER; + + if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle()) + { + addNewAnim(new ReverseAnimation(owner, s, s->getPosition())); + } + } + owner.executeStagedAnimations(); + owner.waitForAnimations(); + + stackAmountBoxHidden.clear(); + + owner.windowObject->blockUI(activeStack == nullptr); + removeExpiredColorFilters(); +} + +void BattleStacksController::startAction(const BattleAction & action) +{ + removeExpiredColorFilters(); +} + +void BattleStacksController::stackActivated(const CStack *stack) +{ + stackToActivate = stack; + owner.waitForAnimations(); + logAnim->debug("Activating next stack"); + owner.activateStack(); +} + +void BattleStacksController::deactivateStack() +{ + if (!activeStack) { + return; + } + stackToActivate = activeStack; + setActiveStack(nullptr); +} + +void BattleStacksController::activateStack() +{ + if ( !currentAnimations.empty()) + return; + + if ( !stackToActivate) + return; + + owner.trySetActivePlayer(stackToActivate->unitOwner()); + + setActiveStack(stackToActivate); + stackToActivate = nullptr; + + const CStack * s = getActiveStack(); + if(!s) + return; +} + +const CStack* BattleStacksController::getActiveStack() const +{ + return activeStack; +} + +bool BattleStacksController::facingRight(const CStack * stack) const +{ + return stackFacingRight.at(stack->unitId()); +} + +Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const +{ + Point ret(-500, -500); //returned value + if(stack && stack->initialPosition < 0) //creatures in turrets + return owner.siegeController->getTurretCreaturePosition(stack->initialPosition); + + static const Point basePos(-189, -139); // position of creature in topleft corner + static const int imageShiftX = 29; // X offset to base pos for facing right stacks, negative for facing left + + ret.x = basePos.x + 22 * ( (hexNum.getY() + 1)%2 ) + 44 * hexNum.getX(); + ret.y = basePos.y + 42 * hexNum.getY(); + + if (stack) + { + if(facingRight(stack)) + ret.x += imageShiftX; + else + ret.x -= imageShiftX; + + //shifting position for double - hex creatures + if(stack->doubleWide()) + { + if(stack->unitSide() == BattleSide::ATTACKER) + { + if(facingRight(stack)) + ret.x -= 44; + } + else + { + if(!facingRight(stack)) + ret.x += 44; + } + } + } + //returning + return ret; +} + +void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent) +{ + for (auto & filter : stackFilterEffects) + { + if (filter.target == target && filter.source == source) + { + filter.effect = effect; + filter.persistent = persistent; + return; + } + } + stackFilterEffects.push_back({ effect, target, source, persistent }); +} + +void BattleStacksController::removeExpiredColorFilters() +{ + vstd::erase_if(stackFilterEffects, [&](const BattleStackFilterEffect & filter) + { + if (!filter.persistent) + { + if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, filter.source->id), Selector::all)) + return true; + if (filter.effect == ColorFilter::genEmptyShifter()) + return true; + } + return false; + }); +} + +void BattleStacksController::updateHoveredStacks() +{ + auto newStacks = selectHoveredStacks(); + + for(const auto * stack : mouseHoveredStacks) + { + if (vstd::contains(newStacks, stack)) + continue; + + if (stack == activeStack) + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder()); + else + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder()); + } + + for(const auto * stack : newStacks) + { + if (vstd::contains(mouseHoveredStacks, stack)) + continue; + + stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder()); + if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen()) + stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON); + } + + mouseHoveredStacks = newStacks; +} + +std::vector BattleStacksController::selectHoveredStacks() +{ + // only allow during our turn - do not try to highlight creatures while they are in the middle of actions + if (!activeStack) + return {}; + + if(owner.hasAnimations()) + return {}; + + auto hoveredQueueUnitId = owner.windowObject->getQueueHoveredUnitId(); + if(hoveredQueueUnitId.has_value()) + { + return { owner.getBattle()->battleGetStackByID(hoveredQueueUnitId.value(), true) }; + } + + auto hoveredHex = owner.fieldController->getHoveredHex(); + + if (!hoveredHex.isValid()) + return {}; + + const spells::Caster *caster = nullptr; + const CSpell *spell = nullptr; + + spells::Mode mode = owner.actionsController->getCurrentCastMode(); + spell = owner.actionsController->getCurrentSpell(hoveredHex); + caster = owner.actionsController->getCurrentSpellcaster(); + + if(caster && spell && owner.actionsController->currentActionSpellcasting(hoveredHex) ) //when casting spell + { + spells::Target target; + target.emplace_back(hoveredHex); + + spells::BattleCast event(owner.getBattle().get(), caster, mode, spell); + auto mechanics = spell->battleMechanics(&event); + return mechanics->getAffectedStacks(target); + } + + if(hoveredHex.isValid()) + { + const CStack * const stack = owner.getBattle()->battleGetStackByPos(hoveredHex, true); + + if (stack) + return {stack}; + } + + return {}; +} + +const std::vector BattleStacksController::getHoveredStacksUnitIds() const +{ + auto result = std::vector(); + for(const auto * stack : mouseHoveredStacks) + { + result.push_back(stack->unitId()); + } + + return result; +} diff --git a/client/battle/BattleStacksController.h b/client/battle/BattleStacksController.h index 30352a3db..ccf66c5a9 100644 --- a/client/battle/BattleStacksController.h +++ b/client/battle/BattleStacksController.h @@ -1,147 +1,147 @@ -/* - * BattleStacksController.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 "../render/ColorFilter.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct BattleHex; -class BattleAction; -class CStack; -class CSpell; -class SpellID; -class Point; - -VCMI_LIB_NAMESPACE_END - -struct StackAttackedInfo; -struct StackAttackInfo; - -class ColorFilter; -class Canvas; -class BattleInterface; -class BattleAnimation; -class CreatureAnimation; -class BattleAnimation; -class BattleRenderer; -class IImage; - -struct BattleStackFilterEffect -{ - ColorFilter effect; - const CStack * target; - const CSpell * source; - bool persistent; -}; - -/// Class responsible for handling stacks in battle -/// Handles ordering of stacks animation -/// As well as rendering of stacks, their amount boxes -/// And any other effect applied to stacks -class BattleStacksController -{ - BattleInterface & owner; - - std::shared_ptr amountNormal; - std::shared_ptr amountNegative; - std::shared_ptr amountPositive; - std::shared_ptr amountEffNeutral; - - /// currently displayed animations - std::vector currentAnimations; - - /// currently active color effects on stacks, in order of their addition (which corresponds to their apply order) - std::vector stackFilterEffects; - - /// animations of creatures from fighting armies (order by BattleInfo's stacks' ID) - std::map> stackAnimation; - - /// //TODO: move it to battle callback - std::map stackFacingRight; - - /// Stacks have amount box hidden due to ongoing animations - std::set stackAmountBoxHidden; - - /// currently active stack; nullptr - no one - const CStack *activeStack; - - /// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation - std::vector mouseHoveredStacks; - - ///when animation is playing, we should wait till the end to make the next stack active; nullptr of none - const CStack *stackToActivate; - - /// for giving IDs for animations - ui32 animIDhelper; - - bool stackNeedsAmountBox(const CStack * stack) const; - void showStackAmountBox(Canvas & canvas, const CStack * stack); - BattleHex getStackCurrentPosition(const CStack * stack) const; - - std::shared_ptr getStackAmountBox(const CStack * stack); - - void removeExpiredColorFilters(); - - void initializeBattleAnimations(); - void tickFrameBattleAnimations(uint32_t msPassed); - - void updateBattleAnimations(uint32_t msPassed); - void updateHoveredStacks(); - - std::vector selectHoveredStacks(); - - bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender); - -public: - BattleStacksController(BattleInterface & owner); - - bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const; - bool facingRight(const CStack * stack) const; - - void stackReset(const CStack * stack); - void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield - void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled - void stackActivated(const CStack *stack); //active stack has been changed - void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex - void stackTeleported(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex - void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked - void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest - - void startAction(const BattleAction & action); - void endAction(const BattleAction & action); - - void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack - - void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack - - void setActiveStack(const CStack *stack); - - void showAliveStack(Canvas & canvas, const CStack * stack); - void showStack(Canvas & canvas, const CStack * stack); - - void collectRenderableObjects(BattleRenderer & renderer); - - /// Adds new color filter effect targeting stack - /// Effect will last as long as stack is affected by specified spell (unless effect is persistent) - /// If effect from same (target, source) already exists, it will be updated - void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent); - void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims - - const CStack* getActiveStack() const; - const std::vector getHoveredStacksUnitIds() const; - - void tick(uint32_t msPassed); - - /// returns position of animation needed to place stack in specific hex - Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const; - - friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations -}; +/* + * BattleStacksController.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 "../render/ColorFilter.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleHex; +class BattleAction; +class CStack; +class CSpell; +class SpellID; +class Point; + +VCMI_LIB_NAMESPACE_END + +struct StackAttackedInfo; +struct StackAttackInfo; + +class ColorFilter; +class Canvas; +class BattleInterface; +class BattleAnimation; +class CreatureAnimation; +class BattleAnimation; +class BattleRenderer; +class IImage; + +struct BattleStackFilterEffect +{ + ColorFilter effect; + const CStack * target; + const CSpell * source; + bool persistent; +}; + +/// Class responsible for handling stacks in battle +/// Handles ordering of stacks animation +/// As well as rendering of stacks, their amount boxes +/// And any other effect applied to stacks +class BattleStacksController +{ + BattleInterface & owner; + + std::shared_ptr amountNormal; + std::shared_ptr amountNegative; + std::shared_ptr amountPositive; + std::shared_ptr amountEffNeutral; + + /// currently displayed animations + std::vector currentAnimations; + + /// currently active color effects on stacks, in order of their addition (which corresponds to their apply order) + std::vector stackFilterEffects; + + /// animations of creatures from fighting armies (order by BattleInfo's stacks' ID) + std::map> stackAnimation; + + /// //TODO: move it to battle callback + std::map stackFacingRight; + + /// Stacks have amount box hidden due to ongoing animations + std::set stackAmountBoxHidden; + + /// currently active stack; nullptr - no one + const CStack *activeStack; + + /// stacks or their battle queue images below mouse pointer (multiple stacks possible while spellcasting), used for border animation + std::vector mouseHoveredStacks; + + ///when animation is playing, we should wait till the end to make the next stack active; nullptr of none + const CStack *stackToActivate; + + /// for giving IDs for animations + ui32 animIDhelper; + + bool stackNeedsAmountBox(const CStack * stack) const; + void showStackAmountBox(Canvas & canvas, const CStack * stack); + BattleHex getStackCurrentPosition(const CStack * stack) const; + + std::shared_ptr getStackAmountBox(const CStack * stack); + + void removeExpiredColorFilters(); + + void initializeBattleAnimations(); + void tickFrameBattleAnimations(uint32_t msPassed); + + void updateBattleAnimations(uint32_t msPassed); + void updateHoveredStacks(); + + std::vector selectHoveredStacks(); + + bool shouldAttackFacingRight(const CStack * attacker, const CStack * defender); + +public: + BattleStacksController(BattleInterface & owner); + + bool shouldRotate(const CStack * stack, const BattleHex & oldPos, const BattleHex & nextHex) const; + bool facingRight(const CStack * stack) const; + + void stackReset(const CStack * stack); + void stackAdded(const CStack * stack, bool instant); //new stack appeared on battlefield + void stackRemoved(uint32_t stackID); //stack disappeared from batlefiled + void stackActivated(const CStack *stack); //active stack has been changed + void stackMoved(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stackTeleported(const CStack *stack, std::vector destHex, int distance); //stack with id number moved to destHex + void stacksAreAttacked(std::vector attackedInfos); //called when a certain amount of stacks has been attacked + void stackAttacking(const StackAttackInfo & info); //called when stack with id ID is attacking something on hex dest + + void startAction(const BattleAction & action); + void endAction(const BattleAction & action); + + void deactivateStack(); //copy activeStack to stackToActivate, then set activeStack to nullptr to temporary disable current stack + + void activateStack(); //copy stackToActivate to activeStack to enable controls of the stack + + void setActiveStack(const CStack *stack); + + void showAliveStack(Canvas & canvas, const CStack * stack); + void showStack(Canvas & canvas, const CStack * stack); + + void collectRenderableObjects(BattleRenderer & renderer); + + /// Adds new color filter effect targeting stack + /// Effect will last as long as stack is affected by specified spell (unless effect is persistent) + /// If effect from same (target, source) already exists, it will be updated + void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent); + void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims + + const CStack* getActiveStack() const; + const std::vector getHoveredStacksUnitIds() const; + + void tick(uint32_t msPassed); + + /// returns position of animation needed to place stack in specific hex + Point getStackPositionAtHex(BattleHex hexNum, const CStack * creature) const; + + friend class BattleAnimation; // for exposing pendingAnims/creAnims/creDir to animations +}; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 81c5b12f4..0910db5cd 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -1,687 +1,687 @@ -/* - * BattleWindow.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 "BattleWindow.h" - -#include "BattleInterface.h" -#include "BattleInterfaceClasses.h" -#include "BattleFieldController.h" -#include "BattleStacksController.h" -#include "BattleActionsController.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../CMusicHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../windows/CSpellWindow.h" -#include "../widgets/Buttons.h" -#include "../widgets/Images.h" -#include "../windows/CMessage.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IRenderHandler.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../CCallback.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/gameState/InfoAboutArmy.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/CStack.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/filesystem/ResourcePath.h" -#include "../windows/settings/SettingsMainWindow.h" - -BattleWindow::BattleWindow(BattleInterface & owner): - owner(owner), - defaultAction(PossiblePlayerBattleAction::INVALID) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos.w = 800; - pos.h = 600; - pos = center(); - - REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); - - const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); - - addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); - addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); - addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this)); - addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this)); - addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this)); - addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this)); - addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this)); - addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this)); - addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this)); - addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this)); - addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this)); - addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this)); - - addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();}); - addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();}); - addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); }); - addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); }); - - build(config); - - console = widget("console"); - - owner.console = console; - - owner.fieldController.reset( new BattleFieldController(owner)); - owner.fieldController->createHeroes(); - - createQueue(); - createStickyHeroInfoWindows(); - - if ( owner.tacticsMode ) - tacticPhaseStarted(); - else - tacticPhaseEnded(); - - addUsedEvents(LCLICK | KEYBOARD); -} - -void BattleWindow::createQueue() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - //create stack queue and adjust our own position - bool embedQueue; - bool showQueue = settings["battle"]["showQueue"].Bool(); - std::string queueSize = settings["battle"]["queueSize"].String(); - - if(queueSize == "auto") - embedQueue = GH.screenDimensions().y < 700; - else - embedQueue = GH.screenDimensions().y < 700 || queueSize == "small"; - - queue = std::make_shared(embedQueue, owner); - if(!embedQueue && showQueue) - { - //re-center, taking into account stack queue position - pos.y -= queue->pos.h; - pos.h += queue->pos.h; - pos = center(); - } - - if (!showQueue) - queue->disable(); -} - -void BattleWindow::createStickyHeroInfoWindows() -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - - if(owner.defendingHeroInstance) - { - InfoAboutHero info; - info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x + pos.w + 15, pos.y) - : Point(pos.x + pos.w -79, pos.y + 135); - defenderHeroWindow = std::make_shared(info, &position); - } - if(owner.attackingHeroInstance) - { - InfoAboutHero info; - info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); - Point position = (GH.screenDimensions().x >= 1000) - ? Point(pos.x - 93, pos.y) - : Point(pos.x + 1, pos.y + 135); - attackerHeroWindow = std::make_shared(info, &position); - } - - bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); - - if(!showInfoWindows) - { - if(attackerHeroWindow) - attackerHeroWindow->disable(); - - if(defenderHeroWindow) - defenderHeroWindow->disable(); - } -} - -BattleWindow::~BattleWindow() -{ - CPlayerInterface::battleInt = nullptr; -} - -std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & config) const -{ - auto rect = readRect(config["rect"]); - auto offset = readPosition(config["imagePosition"]); - auto background = widget("menuBattle"); - return std::make_shared(background, rect.topLeft(), offset, rect.dimensions() ); -} - -void BattleWindow::toggleQueueVisibility() -{ - if(settings["battle"]["showQueue"].Bool()) - hideQueue(); - else - showQueue(); -} - -void BattleWindow::hideQueue() -{ - if(settings["battle"]["showQueue"].Bool() == false) - return; - - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = false; - - queue->disable(); - - if (!queue->embedded) - { - //re-center, taking into account stack queue position - pos.y += queue->pos.h; - pos.h -= queue->pos.h; - pos = center(); - } - GH.windows().totalRedraw(); -} - -void BattleWindow::showQueue() -{ - if(settings["battle"]["showQueue"].Bool() == true) - return; - - Settings showQueue = settings.write["battle"]["showQueue"]; - showQueue->Bool() = true; - - createQueue(); - updateQueue(); - GH.windows().totalRedraw(); -} - -void BattleWindow::toggleStickyHeroWindowsVisibility() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool()) - hideStickyHeroWindows(); - else - showStickyHeroWindows(); -} - -void BattleWindow::hideStickyHeroWindows() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false) - return; - - Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; - showStickyHeroInfoWindows->Bool() = false; - - if(attackerHeroWindow) - attackerHeroWindow->disable(); - - if(defenderHeroWindow) - defenderHeroWindow->disable(); - - GH.windows().totalRedraw(); -} - -void BattleWindow::showStickyHeroWindows() -{ - if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) - return; - - Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; - showStickyHeroInfoWindows->Bool() = true; - - - createStickyHeroInfoWindows(); - GH.windows().totalRedraw(); -} - -void BattleWindow::updateQueue() -{ - queue->update(); -} - -void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) -{ - std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; - panelToUpdate->update(hero); -} - -void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) -{ - if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) - { - InfoAboutHero heroInfo = InfoAboutHero(); - heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE); - - updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo); - } - else - { - logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window"); - } -} - -void BattleWindow::activate() -{ - GH.setStatusbar(console); - CIntObject::activate(); - LOCPLINT->cingconsole->activate(); -} - -void BattleWindow::deactivate() -{ - GH.setStatusbar(nullptr); - CIntObject::deactivate(); - LOCPLINT->cingconsole->deactivate(); -} - -bool BattleWindow::captureThisKey(EShortcut key) -{ - return owner.openingPlaying(); -} - -void BattleWindow::keyPressed(EShortcut key) -{ - if (owner.openingPlaying()) - { - owner.openingEnd(); - return; - } - InterfaceObjectConfigurable::keyPressed(key); -} - -void BattleWindow::clickPressed(const Point & cursorPosition) -{ - if (owner.openingPlaying()) - { - owner.openingEnd(); - return; - } - InterfaceObjectConfigurable::clickPressed(cursorPosition); -} - -void BattleWindow::tacticPhaseStarted() -{ - auto menuBattle = widget("menuBattle"); - auto console = widget("console"); - auto menuTactics = widget("menuTactics"); - auto tacticNext = widget("tacticNext"); - auto tacticEnd = widget("tacticEnd"); - auto alternativeAction = widget("alternativeAction"); - - menuBattle->disable(); - console->disable(); - if (alternativeAction) - alternativeAction->disable(); - - menuTactics->enable(); - tacticNext->enable(); - tacticEnd->enable(); - - redraw(); -} - -void BattleWindow::tacticPhaseEnded() -{ - auto menuBattle = widget("menuBattle"); - auto console = widget("console"); - auto menuTactics = widget("menuTactics"); - auto tacticNext = widget("tacticNext"); - auto tacticEnd = widget("tacticEnd"); - auto alternativeAction = widget("alternativeAction"); - - menuBattle->enable(); - console->enable(); - if (alternativeAction) - alternativeAction->enable(); - - menuTactics->disable(); - tacticNext->disable(); - tacticEnd->disable(); - - redraw(); -} - -void BattleWindow::bOptionsf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - CCS->curh->set(Cursor::Map::POINTER); - - GH.windows().createAndPushWindow(&owner); -} - -void BattleWindow::bSurrenderf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - int cost = owner.getBattle()->battleGetSurrenderCost(); - if(cost >= 0) - { - std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name; - if(enemyHeroName.empty()) - { - logGlobal->warn("Surrender performed without enemy hero, should not happen!"); - enemyHeroName = "#ENEMY#"; - } - - std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold." - owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr); - } -} - -void BattleWindow::bFleef() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if ( owner.getBattle()->battleCanFlee() ) - { - CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); - owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? - } - else - { - std::vector> comps; - std::string heroName; - //calculating fleeing hero's name - if (owner.attackingHeroInstance) - if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) - heroName = owner.attackingHeroInstance->getNameTranslated(); - if (owner.defendingHeroInstance) - if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) - heroName = owner.defendingHeroInstance->getNameTranslated(); - //calculating text - auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! - - //printing message - owner.curInt->showInfoDialog(boost::str(txt), comps); - } -} - -void BattleWindow::reallyFlee() -{ - owner.giveCommand(EActionType::RETREAT); - CCS->curh->set(Cursor::Map::POINTER); -} - -void BattleWindow::reallySurrender() -{ - if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost()) - { - owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! - } - else - { - owner.giveCommand(EActionType::SURRENDER); - CCS->curh->set(Cursor::Map::POINTER); - } -} - -void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) -{ - auto w = widget("alternativeAction"); - if(!w) - return; - - AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]); - switch(action.get()) - { - case PossiblePlayerBattleAction::ATTACK: - iconName = AnimationPath::fromJson(variables["actionIconAttack"]); - break; - - case PossiblePlayerBattleAction::SHOOT: - iconName = AnimationPath::fromJson(variables["actionIconShoot"]); - break; - - case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: - iconName = AnimationPath::fromJson(variables["actionIconSpell"]); - break; - - case PossiblePlayerBattleAction::ANY_LOCATION: - iconName = AnimationPath::fromJson(variables["actionIconSpell"]); - break; - - //TODO: figure out purpose of this icon - //case PossiblePlayerBattleAction::???: - //iconName = variables["actionIconWalk"].String(); - //break; - - case PossiblePlayerBattleAction::ATTACK_AND_RETURN: - iconName = AnimationPath::fromJson(variables["actionIconReturn"]); - break; - - case PossiblePlayerBattleAction::WALK_AND_ATTACK: - iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]); - break; - } - - auto anim = GH.renderHandler().loadAnimation(iconName); - w->setImage(anim); - w->redraw(); -} - -void BattleWindow::setAlternativeActions(const std::list & actions) -{ - alternativeActions = actions; - defaultAction = PossiblePlayerBattleAction::INVALID; - if(alternativeActions.size() > 1) - defaultAction = alternativeActions.back(); - if(!alternativeActions.empty()) - showAlternativeActionIcon(alternativeActions.front()); - else - showAlternativeActionIcon(defaultAction); -} - -void BattleWindow::bAutofightf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - //Stop auto-fight mode - if(owner.curInt->isAutoFightOn) - { - assert(owner.curInt->autofightingAI); - owner.curInt->isAutoFightOn = false; - logGlobal->trace("Stopping the autofight..."); - } - else if(!owner.curInt->autofightingAI) - { - owner.curInt->isAutoFightOn = true; - blockUI(true); - - auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); - - AutocombatPreferences autocombatPreferences = AutocombatPreferences(); - autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); - - ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); - ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false); - owner.curInt->autofightingAI = ai; - owner.curInt->cb->registerBattleInterface(ai); - - owner.requestAutofightingAIToTakeAction(); - } -} - -void BattleWindow::bSpellf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (!owner.makingTurn()) - return; - - auto myHero = owner.currentHero(); - if(!myHero) - return; - - CCS->curh->set(Cursor::Map::POINTER); - - ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO); - - if(spellCastProblem == ESpellCastProblem::OK) - { - GH.windows().createAndPushWindow(myHero, owner.curInt.get()); - } - else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) - { - //TODO: move to spell mechanics, add more information to spell cast problem - //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible - auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); - if (!blockingBonus) - return; - - if (blockingBonus->source == BonusSource::ARTIFACT) - { - const auto artID = ArtifactID(blockingBonus->sid); - //If we have artifact, put name of our hero. Otherwise assume it's the enemy. - //TODO check who *really* is source of bonus - std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name; - - //%s wields the %s, an ancient artifact which creates a p dead to all magic. - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683]) - % heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated())); - } - } -} - -void BattleWindow::bSwitchActionf() -{ - if(alternativeActions.empty()) - return; - - if(alternativeActions.front() == defaultAction) - { - alternativeActions.push_back(alternativeActions.front()); - alternativeActions.pop_front(); - } - - auto actions = owner.actionsController->getPossibleActions(); - if(!actions.empty() && actions.front() == alternativeActions.front()) - { - owner.actionsController->removePossibleAction(alternativeActions.front()); - showAlternativeActionIcon(defaultAction); - } - else - { - owner.actionsController->pushFrontPossibleAction(alternativeActions.front()); - showAlternativeActionIcon(alternativeActions.front()); - } - - alternativeActions.push_back(alternativeActions.front()); - alternativeActions.pop_front(); -} - -void BattleWindow::bWaitf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (owner.stacksController->getActiveStack() != nullptr) - owner.giveCommand(EActionType::WAIT); -} - -void BattleWindow::bDefencef() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - if (owner.stacksController->getActiveStack() != nullptr) - owner.giveCommand(EActionType::DEFEND); -} - -void BattleWindow::bConsoleUpf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - console->scrollUp(); -} - -void BattleWindow::bConsoleDownf() -{ - if (owner.actionsController->spellcastingModeActive()) - return; - - console->scrollDown(); -} - -void BattleWindow::bTacticNextStack() -{ - owner.tacticNextStack(nullptr); -} - -void BattleWindow::bTacticPhaseEnd() -{ - owner.tacticPhaseEnd(); -} - -void BattleWindow::blockUI(bool on) -{ - bool canCastSpells = false; - auto hero = owner.getBattle()->battleGetMyHero(); - - if(hero) - { - ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO); - - //if magic is blocked, we leave button active, so the message can be displayed after button click - canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; - } - - bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false; - - setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on); - setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee()); - setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0); - setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells); - setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait); - setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive()); - setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode); - setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode); -} - -std::optional BattleWindow::getQueueHoveredUnitId() -{ - return queue->getHoveredUnitIdIfAny(); -} - -void BattleWindow::showAll(Canvas & to) -{ - CIntObject::showAll(to); - - if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600) - CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); -} - -void BattleWindow::show(Canvas & to) -{ - CIntObject::show(to); - LOCPLINT->cingconsole->show(to); -} - -void BattleWindow::close() -{ - if(!GH.windows().isTopWindow(this)) - logGlobal->error("Only top interface must be closed"); - GH.windows().popWindows(1); -} +/* + * BattleWindow.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 "BattleWindow.h" + +#include "BattleInterface.h" +#include "BattleInterfaceClasses.h" +#include "BattleFieldController.h" +#include "BattleStacksController.h" +#include "BattleActionsController.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../CMusicHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../windows/CSpellWindow.h" +#include "../widgets/Buttons.h" +#include "../widgets/Images.h" +#include "../windows/CMessage.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IRenderHandler.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../CCallback.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/gameState/InfoAboutArmy.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/CStack.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../windows/settings/SettingsMainWindow.h" + +BattleWindow::BattleWindow(BattleInterface & owner): + owner(owner), + defaultAction(PossiblePlayerBattleAction::INVALID) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos.w = 800; + pos.h = 600; + pos = center(); + + REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole); + + const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json")); + + addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this)); + addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this)); + addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this)); + addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this)); + addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this)); + addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this)); + addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this)); + addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this)); + addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this)); + addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this)); + addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this)); + addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this)); + + addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();}); + addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();}); + addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); }); + addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); }); + + build(config); + + console = widget("console"); + + owner.console = console; + + owner.fieldController.reset( new BattleFieldController(owner)); + owner.fieldController->createHeroes(); + + createQueue(); + createStickyHeroInfoWindows(); + + if ( owner.tacticsMode ) + tacticPhaseStarted(); + else + tacticPhaseEnded(); + + addUsedEvents(LCLICK | KEYBOARD); +} + +void BattleWindow::createQueue() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + //create stack queue and adjust our own position + bool embedQueue; + bool showQueue = settings["battle"]["showQueue"].Bool(); + std::string queueSize = settings["battle"]["queueSize"].String(); + + if(queueSize == "auto") + embedQueue = GH.screenDimensions().y < 700; + else + embedQueue = GH.screenDimensions().y < 700 || queueSize == "small"; + + queue = std::make_shared(embedQueue, owner); + if(!embedQueue && showQueue) + { + //re-center, taking into account stack queue position + pos.y -= queue->pos.h; + pos.h += queue->pos.h; + pos = center(); + } + + if (!showQueue) + queue->disable(); +} + +void BattleWindow::createStickyHeroInfoWindows() +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + if(owner.defendingHeroInstance) + { + InfoAboutHero info; + info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x + pos.w + 15, pos.y) + : Point(pos.x + pos.w -79, pos.y + 135); + defenderHeroWindow = std::make_shared(info, &position); + } + if(owner.attackingHeroInstance) + { + InfoAboutHero info; + info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE); + Point position = (GH.screenDimensions().x >= 1000) + ? Point(pos.x - 93, pos.y) + : Point(pos.x + 1, pos.y + 135); + attackerHeroWindow = std::make_shared(info, &position); + } + + bool showInfoWindows = settings["battle"]["stickyHeroInfoWindows"].Bool(); + + if(!showInfoWindows) + { + if(attackerHeroWindow) + attackerHeroWindow->disable(); + + if(defenderHeroWindow) + defenderHeroWindow->disable(); + } +} + +BattleWindow::~BattleWindow() +{ + CPlayerInterface::battleInt = nullptr; +} + +std::shared_ptr BattleWindow::buildBattleConsole(const JsonNode & config) const +{ + auto rect = readRect(config["rect"]); + auto offset = readPosition(config["imagePosition"]); + auto background = widget("menuBattle"); + return std::make_shared(background, rect.topLeft(), offset, rect.dimensions() ); +} + +void BattleWindow::toggleQueueVisibility() +{ + if(settings["battle"]["showQueue"].Bool()) + hideQueue(); + else + showQueue(); +} + +void BattleWindow::hideQueue() +{ + if(settings["battle"]["showQueue"].Bool() == false) + return; + + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = false; + + queue->disable(); + + if (!queue->embedded) + { + //re-center, taking into account stack queue position + pos.y += queue->pos.h; + pos.h -= queue->pos.h; + pos = center(); + } + GH.windows().totalRedraw(); +} + +void BattleWindow::showQueue() +{ + if(settings["battle"]["showQueue"].Bool() == true) + return; + + Settings showQueue = settings.write["battle"]["showQueue"]; + showQueue->Bool() = true; + + createQueue(); + updateQueue(); + GH.windows().totalRedraw(); +} + +void BattleWindow::toggleStickyHeroWindowsVisibility() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool()) + hideStickyHeroWindows(); + else + showStickyHeroWindows(); +} + +void BattleWindow::hideStickyHeroWindows() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool() == false) + return; + + Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; + showStickyHeroInfoWindows->Bool() = false; + + if(attackerHeroWindow) + attackerHeroWindow->disable(); + + if(defenderHeroWindow) + defenderHeroWindow->disable(); + + GH.windows().totalRedraw(); +} + +void BattleWindow::showStickyHeroWindows() +{ + if(settings["battle"]["stickyHeroInfoWindows"].Bool() == true) + return; + + Settings showStickyHeroInfoWindows = settings.write["battle"]["stickyHeroInfoWindows"]; + showStickyHeroInfoWindows->Bool() = true; + + + createStickyHeroInfoWindows(); + GH.windows().totalRedraw(); +} + +void BattleWindow::updateQueue() +{ + queue->update(); +} + +void BattleWindow::updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero) +{ + std::shared_ptr panelToUpdate = side == 0 ? attackerHeroWindow : defenderHeroWindow; + panelToUpdate->update(hero); +} + +void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero) +{ + if(hero == owner.attackingHeroInstance || hero == owner.defendingHeroInstance) + { + InfoAboutHero heroInfo = InfoAboutHero(); + heroInfo.initFromHero(hero, InfoAboutHero::INBATTLE); + + updateHeroInfoWindow(hero == owner.attackingHeroInstance ? 0 : 1, heroInfo); + } + else + { + logGlobal->error("BattleWindow::heroManaPointsChanged: 'Mana points changed' called for hero not belonging to current battle window"); + } +} + +void BattleWindow::activate() +{ + GH.setStatusbar(console); + CIntObject::activate(); + LOCPLINT->cingconsole->activate(); +} + +void BattleWindow::deactivate() +{ + GH.setStatusbar(nullptr); + CIntObject::deactivate(); + LOCPLINT->cingconsole->deactivate(); +} + +bool BattleWindow::captureThisKey(EShortcut key) +{ + return owner.openingPlaying(); +} + +void BattleWindow::keyPressed(EShortcut key) +{ + if (owner.openingPlaying()) + { + owner.openingEnd(); + return; + } + InterfaceObjectConfigurable::keyPressed(key); +} + +void BattleWindow::clickPressed(const Point & cursorPosition) +{ + if (owner.openingPlaying()) + { + owner.openingEnd(); + return; + } + InterfaceObjectConfigurable::clickPressed(cursorPosition); +} + +void BattleWindow::tacticPhaseStarted() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + auto alternativeAction = widget("alternativeAction"); + + menuBattle->disable(); + console->disable(); + if (alternativeAction) + alternativeAction->disable(); + + menuTactics->enable(); + tacticNext->enable(); + tacticEnd->enable(); + + redraw(); +} + +void BattleWindow::tacticPhaseEnded() +{ + auto menuBattle = widget("menuBattle"); + auto console = widget("console"); + auto menuTactics = widget("menuTactics"); + auto tacticNext = widget("tacticNext"); + auto tacticEnd = widget("tacticEnd"); + auto alternativeAction = widget("alternativeAction"); + + menuBattle->enable(); + console->enable(); + if (alternativeAction) + alternativeAction->enable(); + + menuTactics->disable(); + tacticNext->disable(); + tacticEnd->disable(); + + redraw(); +} + +void BattleWindow::bOptionsf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + CCS->curh->set(Cursor::Map::POINTER); + + GH.windows().createAndPushWindow(&owner); +} + +void BattleWindow::bSurrenderf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + int cost = owner.getBattle()->battleGetSurrenderCost(); + if(cost >= 0) + { + std::string enemyHeroName = owner.getBattle()->battleGetEnemyHero().name; + if(enemyHeroName.empty()) + { + logGlobal->warn("Surrender performed without enemy hero, should not happen!"); + enemyHeroName = "#ENEMY#"; + } + + std::string surrenderMessage = boost::str(boost::format(CGI->generaltexth->allTexts[32]) % enemyHeroName % cost); //%s states: "I will accept your surrender and grant you and your troops safe passage for the price of %d gold." + owner.curInt->showYesNoDialog(surrenderMessage, [this](){ reallySurrender(); }, nullptr); + } +} + +void BattleWindow::bFleef() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if ( owner.getBattle()->battleCanFlee() ) + { + CFunctionList ony = std::bind(&BattleWindow::reallyFlee,this); + owner.curInt->showYesNoDialog(CGI->generaltexth->allTexts[28], ony, nullptr); //Are you sure you want to retreat? + } + else + { + std::vector> comps; + std::string heroName; + //calculating fleeing hero's name + if (owner.attackingHeroInstance) + if (owner.attackingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) + heroName = owner.attackingHeroInstance->getNameTranslated(); + if (owner.defendingHeroInstance) + if (owner.defendingHeroInstance->tempOwner == owner.curInt->cb->getPlayerID()) + heroName = owner.defendingHeroInstance->getNameTranslated(); + //calculating text + auto txt = boost::format(CGI->generaltexth->allTexts[340]) % heroName; //The Shackles of War are present. %s can not retreat! + + //printing message + owner.curInt->showInfoDialog(boost::str(txt), comps); + } +} + +void BattleWindow::reallyFlee() +{ + owner.giveCommand(EActionType::RETREAT); + CCS->curh->set(Cursor::Map::POINTER); +} + +void BattleWindow::reallySurrender() +{ + if (owner.curInt->cb->getResourceAmount(EGameResID::GOLD) < owner.getBattle()->battleGetSurrenderCost()) + { + owner.curInt->showInfoDialog(CGI->generaltexth->allTexts[29]); //You don't have enough gold! + } + else + { + owner.giveCommand(EActionType::SURRENDER); + CCS->curh->set(Cursor::Map::POINTER); + } +} + +void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action) +{ + auto w = widget("alternativeAction"); + if(!w) + return; + + AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]); + switch(action.get()) + { + case PossiblePlayerBattleAction::ATTACK: + iconName = AnimationPath::fromJson(variables["actionIconAttack"]); + break; + + case PossiblePlayerBattleAction::SHOOT: + iconName = AnimationPath::fromJson(variables["actionIconShoot"]); + break; + + case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE: + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); + break; + + case PossiblePlayerBattleAction::ANY_LOCATION: + iconName = AnimationPath::fromJson(variables["actionIconSpell"]); + break; + + //TODO: figure out purpose of this icon + //case PossiblePlayerBattleAction::???: + //iconName = variables["actionIconWalk"].String(); + //break; + + case PossiblePlayerBattleAction::ATTACK_AND_RETURN: + iconName = AnimationPath::fromJson(variables["actionIconReturn"]); + break; + + case PossiblePlayerBattleAction::WALK_AND_ATTACK: + iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]); + break; + } + + auto anim = GH.renderHandler().loadAnimation(iconName); + w->setImage(anim); + w->redraw(); +} + +void BattleWindow::setAlternativeActions(const std::list & actions) +{ + alternativeActions = actions; + defaultAction = PossiblePlayerBattleAction::INVALID; + if(alternativeActions.size() > 1) + defaultAction = alternativeActions.back(); + if(!alternativeActions.empty()) + showAlternativeActionIcon(alternativeActions.front()); + else + showAlternativeActionIcon(defaultAction); +} + +void BattleWindow::bAutofightf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + //Stop auto-fight mode + if(owner.curInt->isAutoFightOn) + { + assert(owner.curInt->autofightingAI); + owner.curInt->isAutoFightOn = false; + logGlobal->trace("Stopping the autofight..."); + } + else if(!owner.curInt->autofightingAI) + { + owner.curInt->isAutoFightOn = true; + blockUI(true); + + auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String()); + + AutocombatPreferences autocombatPreferences = AutocombatPreferences(); + autocombatPreferences.enableSpellsUsage = settings["battle"]["enableAutocombatSpells"].Bool(); + + ai->initBattleInterface(owner.curInt->env, owner.curInt->cb, autocombatPreferences); + ai->battleStart(owner.getBattleID(), owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.getBattle()->battleGetMySide(), false); + owner.curInt->autofightingAI = ai; + owner.curInt->cb->registerBattleInterface(ai); + + owner.requestAutofightingAIToTakeAction(); + } +} + +void BattleWindow::bSpellf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (!owner.makingTurn()) + return; + + auto myHero = owner.currentHero(); + if(!myHero) + return; + + CCS->curh->set(Cursor::Map::POINTER); + + ESpellCastProblem spellCastProblem = owner.getBattle()->battleCanCastSpell(myHero, spells::Mode::HERO); + + if(spellCastProblem == ESpellCastProblem::OK) + { + GH.windows().createAndPushWindow(myHero, owner.curInt.get()); + } + else if (spellCastProblem == ESpellCastProblem::MAGIC_IS_BLOCKED) + { + //TODO: move to spell mechanics, add more information to spell cast problem + //Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible + auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC)); + if (!blockingBonus) + return; + + if (blockingBonus->source == BonusSource::ARTIFACT) + { + const auto artID = ArtifactID(blockingBonus->sid); + //If we have artifact, put name of our hero. Otherwise assume it's the enemy. + //TODO check who *really* is source of bonus + std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name; + + //%s wields the %s, an ancient artifact which creates a p dead to all magic. + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683]) + % heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated())); + } + } +} + +void BattleWindow::bSwitchActionf() +{ + if(alternativeActions.empty()) + return; + + if(alternativeActions.front() == defaultAction) + { + alternativeActions.push_back(alternativeActions.front()); + alternativeActions.pop_front(); + } + + auto actions = owner.actionsController->getPossibleActions(); + if(!actions.empty() && actions.front() == alternativeActions.front()) + { + owner.actionsController->removePossibleAction(alternativeActions.front()); + showAlternativeActionIcon(defaultAction); + } + else + { + owner.actionsController->pushFrontPossibleAction(alternativeActions.front()); + showAlternativeActionIcon(alternativeActions.front()); + } + + alternativeActions.push_back(alternativeActions.front()); + alternativeActions.pop_front(); +} + +void BattleWindow::bWaitf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (owner.stacksController->getActiveStack() != nullptr) + owner.giveCommand(EActionType::WAIT); +} + +void BattleWindow::bDefencef() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + if (owner.stacksController->getActiveStack() != nullptr) + owner.giveCommand(EActionType::DEFEND); +} + +void BattleWindow::bConsoleUpf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + console->scrollUp(); +} + +void BattleWindow::bConsoleDownf() +{ + if (owner.actionsController->spellcastingModeActive()) + return; + + console->scrollDown(); +} + +void BattleWindow::bTacticNextStack() +{ + owner.tacticNextStack(nullptr); +} + +void BattleWindow::bTacticPhaseEnd() +{ + owner.tacticPhaseEnd(); +} + +void BattleWindow::blockUI(bool on) +{ + bool canCastSpells = false; + auto hero = owner.getBattle()->battleGetMyHero(); + + if(hero) + { + ESpellCastProblem spellcastingProblem = owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO); + + //if magic is blocked, we leave button active, so the message can be displayed after button click + canCastSpells = spellcastingProblem == ESpellCastProblem::OK || spellcastingProblem == ESpellCastProblem::MAGIC_IS_BLOCKED; + } + + bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false; + + setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on); + setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee()); + setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0); + setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells); + setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait); + setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode); + setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode); + setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive()); + setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode); + setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode); + setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode); + setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode); +} + +std::optional BattleWindow::getQueueHoveredUnitId() +{ + return queue->getHoveredUnitIdIfAny(); +} + +void BattleWindow::showAll(Canvas & to) +{ + CIntObject::showAll(to); + + if (GH.screenDimensions().x != 800 || GH.screenDimensions().y !=600) + CMessage::drawBorder(owner.curInt->playerID, to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15); +} + +void BattleWindow::show(Canvas & to) +{ + CIntObject::show(to); + LOCPLINT->cingconsole->show(to); +} + +void BattleWindow::close() +{ + if(!GH.windows().isTopWindow(this)) + logGlobal->error("Only top interface must be closed"); + GH.windows().popWindows(1); +} diff --git a/client/battle/BattleWindow.h b/client/battle/BattleWindow.h index 6241e3f57..761d5183b 100644 --- a/client/battle/BattleWindow.h +++ b/client/battle/BattleWindow.h @@ -1,118 +1,118 @@ -/* - * BattleWindow.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 "../gui/CIntObject.h" -#include "../gui/InterfaceObjectConfigurable.h" -#include "../../lib/battle/CBattleInfoCallback.h" -#include "../../lib/battle/PossiblePlayerBattleAction.h" - -VCMI_LIB_NAMESPACE_BEGIN -class CStack; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class BattleInterface; -class BattleConsole; -class BattleRenderer; -class StackQueue; -class HeroInfoBasicPanel; - -/// GUI object that handles functionality of panel at the bottom of combat screen -class BattleWindow : public InterfaceObjectConfigurable -{ - BattleInterface & owner; - - std::shared_ptr queue; - std::shared_ptr console; - std::shared_ptr attackerHeroWindow; - std::shared_ptr defenderHeroWindow; - - /// button press handling functions - void bOptionsf(); - void bSurrenderf(); - void bFleef(); - void bAutofightf(); - void bSpellf(); - void bWaitf(); - void bSwitchActionf(); - void bDefencef(); - void bConsoleUpf(); - void bConsoleDownf(); - void bTacticNextStack(); - void bTacticPhaseEnd(); - - /// functions for handling actions after they were confirmed by popup window - void reallyFlee(); - void reallySurrender(); - - /// management of alternative actions - std::list alternativeActions; - PossiblePlayerBattleAction defaultAction; - void showAlternativeActionIcon(PossiblePlayerBattleAction); - - /// flip battle queue visibility to opposite - void toggleQueueVisibility(); - void createQueue(); - - void toggleStickyHeroWindowsVisibility(); - void createStickyHeroInfoWindows(); - - std::shared_ptr buildBattleConsole(const JsonNode &) const; - -public: - BattleWindow(BattleInterface & owner ); - ~BattleWindow(); - - /// Closes window once battle finished - void close(); - - /// Toggle StackQueue visibility - void hideQueue(); - void showQueue(); - - /// Toggle permanent hero info windows visibility (HD mod feature) - void hideStickyHeroWindows(); - void showStickyHeroWindows(); - - /// Event handler for netpack changing hero mana points - void heroManaPointsChanged(const CGHeroInstance * hero); - - /// block all UI elements when player is not allowed to act, e.g. during enemy turn - void blockUI(bool on); - - /// Refresh queue after turn order changes - void updateQueue(); - - /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side - void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); - - /// Get mouse-hovered battle queue unit ID if any found - std::optional getQueueHoveredUnitId(); - - void activate() override; - void deactivate() override; - void keyPressed(EShortcut key) override; - bool captureThisKey(EShortcut key) override; - void clickPressed(const Point & cursorPosition) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; - - /// Toggle UI to displaying tactics phase - void tacticPhaseStarted(); - - /// Toggle UI to displaying battle log in place of tactics UI - void tacticPhaseEnded(); - - /// Set possible alternative options. If more than 1 - the last will be considered as default option - void setAlternativeActions(const std::list &); -}; - +/* + * BattleWindow.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 "../gui/CIntObject.h" +#include "../gui/InterfaceObjectConfigurable.h" +#include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/PossiblePlayerBattleAction.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CStack; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class BattleInterface; +class BattleConsole; +class BattleRenderer; +class StackQueue; +class HeroInfoBasicPanel; + +/// GUI object that handles functionality of panel at the bottom of combat screen +class BattleWindow : public InterfaceObjectConfigurable +{ + BattleInterface & owner; + + std::shared_ptr queue; + std::shared_ptr console; + std::shared_ptr attackerHeroWindow; + std::shared_ptr defenderHeroWindow; + + /// button press handling functions + void bOptionsf(); + void bSurrenderf(); + void bFleef(); + void bAutofightf(); + void bSpellf(); + void bWaitf(); + void bSwitchActionf(); + void bDefencef(); + void bConsoleUpf(); + void bConsoleDownf(); + void bTacticNextStack(); + void bTacticPhaseEnd(); + + /// functions for handling actions after they were confirmed by popup window + void reallyFlee(); + void reallySurrender(); + + /// management of alternative actions + std::list alternativeActions; + PossiblePlayerBattleAction defaultAction; + void showAlternativeActionIcon(PossiblePlayerBattleAction); + + /// flip battle queue visibility to opposite + void toggleQueueVisibility(); + void createQueue(); + + void toggleStickyHeroWindowsVisibility(); + void createStickyHeroInfoWindows(); + + std::shared_ptr buildBattleConsole(const JsonNode &) const; + +public: + BattleWindow(BattleInterface & owner ); + ~BattleWindow(); + + /// Closes window once battle finished + void close(); + + /// Toggle StackQueue visibility + void hideQueue(); + void showQueue(); + + /// Toggle permanent hero info windows visibility (HD mod feature) + void hideStickyHeroWindows(); + void showStickyHeroWindows(); + + /// Event handler for netpack changing hero mana points + void heroManaPointsChanged(const CGHeroInstance * hero); + + /// block all UI elements when player is not allowed to act, e.g. during enemy turn + void blockUI(bool on); + + /// Refresh queue after turn order changes + void updateQueue(); + + /// Refresh sticky variant of hero info window after spellcast, side same as in BattleSpellCast::side + void updateHeroInfoWindow(uint8_t side, const InfoAboutHero & hero); + + /// Get mouse-hovered battle queue unit ID if any found + std::optional getQueueHoveredUnitId(); + + void activate() override; + void deactivate() override; + void keyPressed(EShortcut key) override; + bool captureThisKey(EShortcut key) override; + void clickPressed(const Point & cursorPosition) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; + + /// Toggle UI to displaying tactics phase + void tacticPhaseStarted(); + + /// Toggle UI to displaying battle log in place of tactics UI + void tacticPhaseEnded(); + + /// Set possible alternative options. If more than 1 - the last will be considered as default option + void setAlternativeActions(const std::list &); +}; + diff --git a/client/battle/CreatureAnimation.cpp b/client/battle/CreatureAnimation.cpp index b22718129..d3c6f7632 100644 --- a/client/battle/CreatureAnimation.cpp +++ b/client/battle/CreatureAnimation.cpp @@ -1,433 +1,433 @@ -/* - * CCreatureAnimation.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 "CreatureAnimation.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/CCreatureHandler.h" - -#include "../gui/CGuiHandler.h" -#include "../render/Canvas.h" -#include "../render/ColorFilter.h" -#include "../render/IRenderHandler.h" - -static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; -static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 }; -static const ColorRGBA creatureNoBorder = { 0, 0, 0, 0 }; - -static ColorRGBA genShadow(ui8 alpha) -{ - return ColorRGBA(0, 0, 0, alpha); -} - -ColorRGBA AnimationControls::getBlueBorder() -{ - return creatureBlueBorder; -} - -ColorRGBA AnimationControls::getGoldBorder() -{ - return creatureGoldBorder; -} - -ColorRGBA AnimationControls::getNoBorder() -{ - return creatureNoBorder; -} - -std::shared_ptr AnimationControls::getAnimation(const CCreature * creature) -{ - auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); - return std::make_shared(creature->animDefName, func); -} - -float AnimationControls::getAnimationSpeedFactor() -{ - // according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666) - // exact value is hard to tell due to large rounding errors - // however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays: - // with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames - return settings["battle"]["speedFactor"].Float(); -} - -float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type) -{ - assert(creature->animation.walkAnimationTime != 0); - assert(creature->animation.attackAnimationTime != 0); - assert(anim->framesInGroup(type) != 0); - - // possible new fields for creature format: - //split "Attack time" into "Shoot Time" and "Cast Time" - - // base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame) - const float baseSpeed = 10.f; - const float speed = baseSpeed * getAnimationSpeedFactor(); - - switch (type) - { - case ECreatureAnimType::MOVING: - return speed / creature->animation.walkAnimationTime; - - case ECreatureAnimType::MOUSEON: - return baseSpeed; - - case ECreatureAnimType::HOLDING: - return creature->animation.idleAnimationTime; - - case ECreatureAnimType::SHOOT_UP: - case ECreatureAnimType::SHOOT_FRONT: - case ECreatureAnimType::SHOOT_DOWN: - case ECreatureAnimType::SPECIAL_UP: - case ECreatureAnimType::SPECIAL_FRONT: - case ECreatureAnimType::SPECIAL_DOWN: - case ECreatureAnimType::CAST_DOWN: - case ECreatureAnimType::CAST_FRONT: - case ECreatureAnimType::CAST_UP: - return speed / creature->animation.attackAnimationTime; - - // as strange as it looks like "attackAnimationTime" does not affects melee attacks - // necessary because length of these animations must be same for all creatures for synchronization - case ECreatureAnimType::ATTACK_UP: - case ECreatureAnimType::ATTACK_FRONT: - case ECreatureAnimType::ATTACK_DOWN: - case ECreatureAnimType::HITTED: - case ECreatureAnimType::DEFENCE: - case ECreatureAnimType::DEATH: - case ECreatureAnimType::DEATH_RANGED: - case ECreatureAnimType::RESURRECTION: - case ECreatureAnimType::GROUP_ATTACK_DOWN: - case ECreatureAnimType::GROUP_ATTACK_FRONT: - case ECreatureAnimType::GROUP_ATTACK_UP: - return speed; - - case ECreatureAnimType::TURN_L: - case ECreatureAnimType::TURN_R: - return speed; - - case ECreatureAnimType::MOVE_START: - case ECreatureAnimType::MOVE_END: - return speed; - - case ECreatureAnimType::DEAD: - case ECreatureAnimType::DEAD_RANGED: - return speed; - - default: - return speed; - } -} - -float AnimationControls::getProjectileSpeed() -{ - // H3 speed: 1250/2500/3750 pixels per second - return static_cast(getAnimationSpeedFactor() * 1250); -} - -float AnimationControls::getRayProjectileSpeed() -{ - // H3 speed: 4000/8000/12000 pixels per second - return static_cast(getAnimationSpeedFactor() * 4000); -} - -float AnimationControls::getCatapultSpeed() -{ - // H3 speed: 200/400/600 pixels per second - return static_cast(getAnimationSpeedFactor() * 200); -} - -float AnimationControls::getSpellEffectSpeed() -{ - // H3 speed: 10/20/30 frames per second - return static_cast(getAnimationSpeedFactor() * 10); -} - -float AnimationControls::getMovementDistance(const CCreature * creature) -{ - // H3 speed: 2/4/6 tiles per second - return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); -} - -float AnimationControls::getFlightDistance(const CCreature * creature) -{ - // Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists - // H3 speed: 250/500/750 pixels per second - return static_cast( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); -} - -float AnimationControls::getFadeInDuration() -{ - // H3 speed: 500/250/166 ms - return 0.5f / getAnimationSpeedFactor(); -} - -float AnimationControls::getObstaclesSpeed() -{ - // H3 speed: 20 frames per second, irregardless of speed setting. - return 20.f; -} - -ECreatureAnimType CreatureAnimation::getType() const -{ - return type; -} - -void CreatureAnimation::setType(ECreatureAnimType type) -{ - this->type = type; - currentFrame = 0; - once = false; - - speed = speedController(this, type); -} - -CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller) - : name(name_), - speed(0.1f), - shadowAlpha(128), - currentFrame(0), - animationEnd(-1), - elapsedTime(0), - type(ECreatureAnimType::HOLDING), - speedController(controller), - once(false) -{ - forward = GH.renderHandler().loadAnimation(name_); - reverse = GH.renderHandler().loadAnimation(name_); - - //todo: optimize - forward->preload(); - reverse->preload(); - - // if necessary, add one frame into vcmi-only group DEAD - if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); - } - - if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); - } - - if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0) - { - forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); - reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); - } - - if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0) - { - for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i) - { - size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i; - - forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); - reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); - } - } - - //TODO: get dimensions form CAnimation - auto first = forward->getImage(0, size_t(type), true); - - if(!first) - { - fullWidth = 0; - fullHeight = 0; - return; - } - fullWidth = first->width(); - fullHeight = first->height(); - - reverse->verticalFlip(); - - speed = speedController(this, type); -} - -void CreatureAnimation::endAnimation() -{ - once = false; - auto copy = onAnimationReset; - onAnimationReset.clear(); - copy(); -} - -bool CreatureAnimation::incrementFrame(float timePassed) -{ - elapsedTime += timePassed; - currentFrame += timePassed * speed; - if (animationEnd >= 0) - currentFrame = std::min(currentFrame, animationEnd); - - const auto framesNumber = framesInGroup(type); - - if(framesNumber <= 0) - { - endAnimation(); - } - else if(currentFrame >= float(framesNumber)) - { - // just in case of extremely low fps (or insanely high speed) - while(currentFrame >= float(framesNumber)) - currentFrame -= framesNumber; - - if(once) - setType(ECreatureAnimType::HOLDING); - - endAnimation(); - return true; - } - return false; -} - -void CreatureAnimation::setBorderColor(ColorRGBA palette) -{ - border = palette; -} - -int CreatureAnimation::getWidth() const -{ - return fullWidth; -} - -int CreatureAnimation::getHeight() const -{ - return fullHeight; -} - -float CreatureAnimation::getCurrentFrame() const -{ - return currentFrame; -} - -void CreatureAnimation::playOnce( ECreatureAnimType type ) -{ - setType(type); - once = true; -} - -inline int getBorderStrength(float time) -{ - float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1 - - return static_cast(borderStrength * 155 + 100); // scale to 0-255 -} - -static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base) -{ - return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256)); -} - -static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) -{ - return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; -} - -static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) -{ - return ColorRGBA( - mixChannels(over.r, base.r, over.a, base.a), - mixChannels(over.g, base.g, over.a, base.a), - mixChannels(over.b, base.b, over.a, base.a), - ui8(over.a + base.a * (255 - over.a) / 256) - ); -} - -void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) -{ - target.resize(8); - target[0] = genShadow(0); - target[1] = genShadow(shadowAlpha / 2); - // colors 2 & 3 are not used in creatures - target[4] = genShadow(shadowAlpha); - target[5] = genBorderColor(getBorderStrength(elapsedTime), border); - target[6] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); - target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); -} - -void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) -{ - ColorRGBA shadowTest = shifter.shiftColor(genShadow(128)); - shadowAlpha = shadowTest.a; - - size_t frame = static_cast(floor(currentFrame)); - - std::shared_ptr image; - - if(facingRight) - image = forward->getImage(frame, size_t(type)); - else - image = reverse->getImage(frame, size_t(type)); - - if(image) - { - IImage::SpecialPalette SpecialPalette; - genSpecialPalette(SpecialPalette); - - image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); - image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); - - canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); - - } -} - -void CreatureAnimation::playUntil(size_t frameIndex) -{ - animationEnd = frameIndex; -} - -int CreatureAnimation::framesInGroup(ECreatureAnimType group) const -{ - return static_cast(forward->size(size_t(group))); -} - -bool CreatureAnimation::isDead() const -{ - return getType() == ECreatureAnimType::DEAD - || getType() == ECreatureAnimType::DEAD_RANGED; -} - -bool CreatureAnimation::isDying() const -{ - return getType() == ECreatureAnimType::DEATH - || getType() == ECreatureAnimType::DEATH_RANGED; -} - -bool CreatureAnimation::isDeadOrDying() const -{ - return getType() == ECreatureAnimType::DEAD - || getType() == ECreatureAnimType::DEATH - || getType() == ECreatureAnimType::DEAD_RANGED - || getType() == ECreatureAnimType::DEATH_RANGED; -} - -bool CreatureAnimation::isIdle() const -{ - return getType() == ECreatureAnimType::HOLDING - || getType() == ECreatureAnimType::MOUSEON; -} - -bool CreatureAnimation::isMoving() const -{ - return getType() == ECreatureAnimType::MOVE_START - || getType() == ECreatureAnimType::MOVING - || getType() == ECreatureAnimType::MOVE_END - || getType() == ECreatureAnimType::TURN_L - || getType() == ECreatureAnimType::TURN_R; -} - -bool CreatureAnimation::isShooting() const -{ - return getType() == ECreatureAnimType::SHOOT_UP - || getType() == ECreatureAnimType::SHOOT_FRONT - || getType() == ECreatureAnimType::SHOOT_DOWN; -} +/* + * CCreatureAnimation.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 "CreatureAnimation.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CCreatureHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../render/ColorFilter.h" +#include "../render/IRenderHandler.h" + +static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 }; +static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 }; +static const ColorRGBA creatureNoBorder = { 0, 0, 0, 0 }; + +static ColorRGBA genShadow(ui8 alpha) +{ + return ColorRGBA(0, 0, 0, alpha); +} + +ColorRGBA AnimationControls::getBlueBorder() +{ + return creatureBlueBorder; +} + +ColorRGBA AnimationControls::getGoldBorder() +{ + return creatureGoldBorder; +} + +ColorRGBA AnimationControls::getNoBorder() +{ + return creatureNoBorder; +} + +std::shared_ptr AnimationControls::getAnimation(const CCreature * creature) +{ + auto func = std::bind(&AnimationControls::getCreatureAnimationSpeed, creature, _1, _2); + return std::make_shared(creature->animDefName, func); +} + +float AnimationControls::getAnimationSpeedFactor() +{ + // according to testing, H3 ratios between slow/medium/fast might actually be 36/60/100 (x1.666) + // exact value is hard to tell due to large rounding errors + // however we will assume them to be 33/66/100 since these values are better for standard 60 fps displays: + // with these numbers, base frame display duration will be 100/66/33 ms - exactly 6/4/2 frames + return settings["battle"]["speedFactor"].Float(); +} + +float AnimationControls::getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType type) +{ + assert(creature->animation.walkAnimationTime != 0); + assert(creature->animation.attackAnimationTime != 0); + assert(anim->framesInGroup(type) != 0); + + // possible new fields for creature format: + //split "Attack time" into "Shoot Time" and "Cast Time" + + // base speed for all H3 animations on slow speed is 10 frames per second (or 100ms per frame) + const float baseSpeed = 10.f; + const float speed = baseSpeed * getAnimationSpeedFactor(); + + switch (type) + { + case ECreatureAnimType::MOVING: + return speed / creature->animation.walkAnimationTime; + + case ECreatureAnimType::MOUSEON: + return baseSpeed; + + case ECreatureAnimType::HOLDING: + return creature->animation.idleAnimationTime; + + case ECreatureAnimType::SHOOT_UP: + case ECreatureAnimType::SHOOT_FRONT: + case ECreatureAnimType::SHOOT_DOWN: + case ECreatureAnimType::SPECIAL_UP: + case ECreatureAnimType::SPECIAL_FRONT: + case ECreatureAnimType::SPECIAL_DOWN: + case ECreatureAnimType::CAST_DOWN: + case ECreatureAnimType::CAST_FRONT: + case ECreatureAnimType::CAST_UP: + return speed / creature->animation.attackAnimationTime; + + // as strange as it looks like "attackAnimationTime" does not affects melee attacks + // necessary because length of these animations must be same for all creatures for synchronization + case ECreatureAnimType::ATTACK_UP: + case ECreatureAnimType::ATTACK_FRONT: + case ECreatureAnimType::ATTACK_DOWN: + case ECreatureAnimType::HITTED: + case ECreatureAnimType::DEFENCE: + case ECreatureAnimType::DEATH: + case ECreatureAnimType::DEATH_RANGED: + case ECreatureAnimType::RESURRECTION: + case ECreatureAnimType::GROUP_ATTACK_DOWN: + case ECreatureAnimType::GROUP_ATTACK_FRONT: + case ECreatureAnimType::GROUP_ATTACK_UP: + return speed; + + case ECreatureAnimType::TURN_L: + case ECreatureAnimType::TURN_R: + return speed; + + case ECreatureAnimType::MOVE_START: + case ECreatureAnimType::MOVE_END: + return speed; + + case ECreatureAnimType::DEAD: + case ECreatureAnimType::DEAD_RANGED: + return speed; + + default: + return speed; + } +} + +float AnimationControls::getProjectileSpeed() +{ + // H3 speed: 1250/2500/3750 pixels per second + return static_cast(getAnimationSpeedFactor() * 1250); +} + +float AnimationControls::getRayProjectileSpeed() +{ + // H3 speed: 4000/8000/12000 pixels per second + return static_cast(getAnimationSpeedFactor() * 4000); +} + +float AnimationControls::getCatapultSpeed() +{ + // H3 speed: 200/400/600 pixels per second + return static_cast(getAnimationSpeedFactor() * 200); +} + +float AnimationControls::getSpellEffectSpeed() +{ + // H3 speed: 10/20/30 frames per second + return static_cast(getAnimationSpeedFactor() * 10); +} + +float AnimationControls::getMovementDistance(const CCreature * creature) +{ + // H3 speed: 2/4/6 tiles per second + return static_cast( 2.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); +} + +float AnimationControls::getFlightDistance(const CCreature * creature) +{ + // Note: for whatever reason, H3 uses "Walk Animation Time" here, even though "Flight Animation Distance" also exists + // H3 speed: 250/500/750 pixels per second + return static_cast( 250.0 * getAnimationSpeedFactor() / creature->animation.walkAnimationTime); +} + +float AnimationControls::getFadeInDuration() +{ + // H3 speed: 500/250/166 ms + return 0.5f / getAnimationSpeedFactor(); +} + +float AnimationControls::getObstaclesSpeed() +{ + // H3 speed: 20 frames per second, irregardless of speed setting. + return 20.f; +} + +ECreatureAnimType CreatureAnimation::getType() const +{ + return type; +} + +void CreatureAnimation::setType(ECreatureAnimType type) +{ + this->type = type; + currentFrame = 0; + once = false; + + speed = speedController(this, type); +} + +CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller) + : name(name_), + speed(0.1f), + shadowAlpha(128), + currentFrame(0), + animationEnd(-1), + elapsedTime(0), + type(ECreatureAnimType::HOLDING), + speedController(controller), + once(false) +{ + forward = GH.renderHandler().loadAnimation(name_); + reverse = GH.renderHandler().loadAnimation(name_); + + //todo: optimize + forward->preload(); + reverse->preload(); + + // if necessary, add one frame into vcmi-only group DEAD + if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), forward->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), reverse->size(size_t(ECreatureAnimType::DEATH))-1, size_t(ECreatureAnimType::DEAD)); + } + + if(forward->size(size_t(ECreatureAnimType::DEAD_RANGED)) == 0 && forward->size(size_t(ECreatureAnimType::DEATH_RANGED)) != 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), forward->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH_RANGED), reverse->size(size_t(ECreatureAnimType::DEATH_RANGED))-1, size_t(ECreatureAnimType::DEAD_RANGED)); + } + + if(forward->size(size_t(ECreatureAnimType::FROZEN)) == 0) + { + forward->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + reverse->duplicateImage(size_t(ECreatureAnimType::HOLDING), 0, size_t(ECreatureAnimType::FROZEN)); + } + + if(forward->size(size_t(ECreatureAnimType::RESURRECTION)) == 0) + { + for (size_t i = 0; i < forward->size(size_t(ECreatureAnimType::DEATH)); ++i) + { + size_t current = forward->size(size_t(ECreatureAnimType::DEATH)) - 1 - i; + + forward->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + reverse->duplicateImage(size_t(ECreatureAnimType::DEATH), current, size_t(ECreatureAnimType::RESURRECTION)); + } + } + + //TODO: get dimensions form CAnimation + auto first = forward->getImage(0, size_t(type), true); + + if(!first) + { + fullWidth = 0; + fullHeight = 0; + return; + } + fullWidth = first->width(); + fullHeight = first->height(); + + reverse->verticalFlip(); + + speed = speedController(this, type); +} + +void CreatureAnimation::endAnimation() +{ + once = false; + auto copy = onAnimationReset; + onAnimationReset.clear(); + copy(); +} + +bool CreatureAnimation::incrementFrame(float timePassed) +{ + elapsedTime += timePassed; + currentFrame += timePassed * speed; + if (animationEnd >= 0) + currentFrame = std::min(currentFrame, animationEnd); + + const auto framesNumber = framesInGroup(type); + + if(framesNumber <= 0) + { + endAnimation(); + } + else if(currentFrame >= float(framesNumber)) + { + // just in case of extremely low fps (or insanely high speed) + while(currentFrame >= float(framesNumber)) + currentFrame -= framesNumber; + + if(once) + setType(ECreatureAnimType::HOLDING); + + endAnimation(); + return true; + } + return false; +} + +void CreatureAnimation::setBorderColor(ColorRGBA palette) +{ + border = palette; +} + +int CreatureAnimation::getWidth() const +{ + return fullWidth; +} + +int CreatureAnimation::getHeight() const +{ + return fullHeight; +} + +float CreatureAnimation::getCurrentFrame() const +{ + return currentFrame; +} + +void CreatureAnimation::playOnce( ECreatureAnimType type ) +{ + setType(type); + once = true; +} + +inline int getBorderStrength(float time) +{ + float borderStrength = fabs(std::round(time) - time) * 2; // generate value in range 0-1 + + return static_cast(borderStrength * 155 + 100); // scale to 0-255 +} + +static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base) +{ + return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256)); +} + +static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2) +{ + return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256; +} + +static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over) +{ + return ColorRGBA( + mixChannels(over.r, base.r, over.a, base.a), + mixChannels(over.g, base.g, over.a, base.a), + mixChannels(over.b, base.b, over.a, base.a), + ui8(over.a + base.a * (255 - over.a) / 256) + ); +} + +void CreatureAnimation::genSpecialPalette(IImage::SpecialPalette & target) +{ + target.resize(8); + target[0] = genShadow(0); + target[1] = genShadow(shadowAlpha / 2); + // colors 2 & 3 are not used in creatures + target[4] = genShadow(shadowAlpha); + target[5] = genBorderColor(getBorderStrength(elapsedTime), border); + target[6] = addColors(genShadow(shadowAlpha), genBorderColor(getBorderStrength(elapsedTime), border)); + target[7] = addColors(genShadow(shadowAlpha / 2), genBorderColor(getBorderStrength(elapsedTime), border)); +} + +void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight) +{ + ColorRGBA shadowTest = shifter.shiftColor(genShadow(128)); + shadowAlpha = shadowTest.a; + + size_t frame = static_cast(floor(currentFrame)); + + std::shared_ptr image; + + if(facingRight) + image = forward->getImage(frame, size_t(type)); + else + image = reverse->getImage(frame, size_t(type)); + + if(image) + { + IImage::SpecialPalette SpecialPalette; + genSpecialPalette(SpecialPalette); + + image->setSpecialPallete(SpecialPalette, IImage::SPECIAL_PALETTE_MASK_CREATURES); + image->adjustPalette(shifter, IImage::SPECIAL_PALETTE_MASK_CREATURES); + + canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h)); + + } +} + +void CreatureAnimation::playUntil(size_t frameIndex) +{ + animationEnd = frameIndex; +} + +int CreatureAnimation::framesInGroup(ECreatureAnimType group) const +{ + return static_cast(forward->size(size_t(group))); +} + +bool CreatureAnimation::isDead() const +{ + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEAD_RANGED; +} + +bool CreatureAnimation::isDying() const +{ + return getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEATH_RANGED; +} + +bool CreatureAnimation::isDeadOrDying() const +{ + return getType() == ECreatureAnimType::DEAD + || getType() == ECreatureAnimType::DEATH + || getType() == ECreatureAnimType::DEAD_RANGED + || getType() == ECreatureAnimType::DEATH_RANGED; +} + +bool CreatureAnimation::isIdle() const +{ + return getType() == ECreatureAnimType::HOLDING + || getType() == ECreatureAnimType::MOUSEON; +} + +bool CreatureAnimation::isMoving() const +{ + return getType() == ECreatureAnimType::MOVE_START + || getType() == ECreatureAnimType::MOVING + || getType() == ECreatureAnimType::MOVE_END + || getType() == ECreatureAnimType::TURN_L + || getType() == ECreatureAnimType::TURN_R; +} + +bool CreatureAnimation::isShooting() const +{ + return getType() == ECreatureAnimType::SHOOT_UP + || getType() == ECreatureAnimType::SHOOT_FRONT + || getType() == ECreatureAnimType::SHOOT_DOWN; +} diff --git a/client/battle/CreatureAnimation.h b/client/battle/CreatureAnimation.h index 66ad3f285..9029f7437 100644 --- a/client/battle/CreatureAnimation.h +++ b/client/battle/CreatureAnimation.h @@ -1,158 +1,158 @@ -/* - * CCreatureAnimation.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/FunctionList.h" -#include "../../lib/Color.h" -#include "../widgets/Images.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" - -class CIntObject; -class CreatureAnimation; -class Canvas; - -/// Namespace for some common controls of animations -namespace AnimationControls -{ - /// get color for creature selection borders - ColorRGBA getBlueBorder(); - ColorRGBA getGoldBorder(); - ColorRGBA getNoBorder(); - - /// returns animation speed factor according to game settings, - /// slow speed is considered to be "base speed" and will return 1.0 - float getAnimationSpeedFactor(); - - /// creates animation object with preset speed control - std::shared_ptr getAnimation(const CCreature * creature); - - /// returns animation speed of specific group, taking in mind game setting (in frames per second) - float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID); - - /// returns how far projectile should move per second, in pixels per second - float getProjectileSpeed(); - - /// returns how far projectile should move per second, in pixels per second - float getRayProjectileSpeed(); - - /// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction - float getCatapultSpeed(); - - /// returns speed of any spell effects, including any special effects like morale (in frames per second) - float getSpellEffectSpeed(); - - /// returns speed of movement animation across the screen, in tiles per second - float getMovementDistance(const CCreature * creature); - - /// returns speed of movement animation across the screen, in pixels per seconds - float getFlightDistance(const CCreature * creature); - - /// Returns total time for full fade-in effect on newly summoned creatures, in seconds - float getFadeInDuration(); - - /// Returns animation speed for obstacles, in frames per second - float getObstaclesSpeed(); -} - -/// Class which manages animations of creatures/units inside battles -/// TODO: split into constant image container and class that does *control* of animation -class CreatureAnimation : public CIntObject -{ -public: - using TSpeedController = std::function; - -private: - AnimationPath name; - - /// animation for rendering stack in default orientation - facing right - std::shared_ptr forward; - - /// animation that has all its frames flipped for rendering stack facing left - std::shared_ptr reverse; - - int fullWidth; - int fullHeight; - - /// speed of animation, measure in frames per second - float speed; - - /// currently displayed frame. Float to allow H3-style animations where frames - /// don't display for integer number of frames - float currentFrame; - float animationEnd; - - /// cumulative, real-time duration of animation. Used for effects like selection border - float elapsedTime; - - ///type of animation being displayed - ECreatureAnimType type; - - /// current value of shadow transparency - uint8_t shadowAlpha; - - /// border color, disabled if alpha = 0 - ColorRGBA border; - - TSpeedController speedController; - - /// animation will be played once and the reset to idling - bool once; - - void endAnimation(); - - void genSpecialPalette(IImage::SpecialPalette & target); -public: - - /// function(s) that will be called when animation ends, after reset to 1st frame - /// NOTE that these functions will be fired only once - CFunctionList onAnimationReset; - - int getWidth() const; - int getHeight() const; - - /// Constructor - /// name - path to .def file, relative to SPRITES/ directory - /// controller - function that will return for how long *each* frame - /// in specified group of animation should be played, measured in seconds - CreatureAnimation(const AnimationPath & name_, TSpeedController speedController); - - /// sets type of animation and resets framecount - void setType(ECreatureAnimType type); - - /// returns currently rendered type of animation - ECreatureAnimType getType() const; - - void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight); - - /// should be called every frame, return true when animation was reset to beginning - bool incrementFrame(float timePassed); - - void setBorderColor(ColorRGBA palette); - - /// Gets the current frame ID within current group. - float getCurrentFrame() const; - - /// plays once given type of animation, then resets to idle - void playOnce(ECreatureAnimType type); - - /// returns number of frames in selected animation type - int framesInGroup(ECreatureAnimType group) const; - - void playUntil(size_t frameIndex); - - /// helpers to classify current type of animation - bool isDead() const; - bool isDying() const; - bool isDeadOrDying() const; - bool isIdle() const; - bool isMoving() const; - bool isShooting() const; -}; +/* + * CCreatureAnimation.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/FunctionList.h" +#include "../../lib/Color.h" +#include "../widgets/Images.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" + +class CIntObject; +class CreatureAnimation; +class Canvas; + +/// Namespace for some common controls of animations +namespace AnimationControls +{ + /// get color for creature selection borders + ColorRGBA getBlueBorder(); + ColorRGBA getGoldBorder(); + ColorRGBA getNoBorder(); + + /// returns animation speed factor according to game settings, + /// slow speed is considered to be "base speed" and will return 1.0 + float getAnimationSpeedFactor(); + + /// creates animation object with preset speed control + std::shared_ptr getAnimation(const CCreature * creature); + + /// returns animation speed of specific group, taking in mind game setting (in frames per second) + float getCreatureAnimationSpeed(const CCreature * creature, const CreatureAnimation * anim, ECreatureAnimType groupID); + + /// returns how far projectile should move per second, in pixels per second + float getProjectileSpeed(); + + /// returns how far projectile should move per second, in pixels per second + float getRayProjectileSpeed(); + + /// returns speed of catapult projectile, in pixels per second, on a straight line, without parabola correction + float getCatapultSpeed(); + + /// returns speed of any spell effects, including any special effects like morale (in frames per second) + float getSpellEffectSpeed(); + + /// returns speed of movement animation across the screen, in tiles per second + float getMovementDistance(const CCreature * creature); + + /// returns speed of movement animation across the screen, in pixels per seconds + float getFlightDistance(const CCreature * creature); + + /// Returns total time for full fade-in effect on newly summoned creatures, in seconds + float getFadeInDuration(); + + /// Returns animation speed for obstacles, in frames per second + float getObstaclesSpeed(); +} + +/// Class which manages animations of creatures/units inside battles +/// TODO: split into constant image container and class that does *control* of animation +class CreatureAnimation : public CIntObject +{ +public: + using TSpeedController = std::function; + +private: + AnimationPath name; + + /// animation for rendering stack in default orientation - facing right + std::shared_ptr forward; + + /// animation that has all its frames flipped for rendering stack facing left + std::shared_ptr reverse; + + int fullWidth; + int fullHeight; + + /// speed of animation, measure in frames per second + float speed; + + /// currently displayed frame. Float to allow H3-style animations where frames + /// don't display for integer number of frames + float currentFrame; + float animationEnd; + + /// cumulative, real-time duration of animation. Used for effects like selection border + float elapsedTime; + + ///type of animation being displayed + ECreatureAnimType type; + + /// current value of shadow transparency + uint8_t shadowAlpha; + + /// border color, disabled if alpha = 0 + ColorRGBA border; + + TSpeedController speedController; + + /// animation will be played once and the reset to idling + bool once; + + void endAnimation(); + + void genSpecialPalette(IImage::SpecialPalette & target); +public: + + /// function(s) that will be called when animation ends, after reset to 1st frame + /// NOTE that these functions will be fired only once + CFunctionList onAnimationReset; + + int getWidth() const; + int getHeight() const; + + /// Constructor + /// name - path to .def file, relative to SPRITES/ directory + /// controller - function that will return for how long *each* frame + /// in specified group of animation should be played, measured in seconds + CreatureAnimation(const AnimationPath & name_, TSpeedController speedController); + + /// sets type of animation and resets framecount + void setType(ECreatureAnimType type); + + /// returns currently rendered type of animation + ECreatureAnimType getType() const; + + void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight); + + /// should be called every frame, return true when animation was reset to beginning + bool incrementFrame(float timePassed); + + void setBorderColor(ColorRGBA palette); + + /// Gets the current frame ID within current group. + float getCurrentFrame() const; + + /// plays once given type of animation, then resets to idle + void playOnce(ECreatureAnimType type); + + /// returns number of frames in selected animation type + int framesInGroup(ECreatureAnimType group) const; + + void playUntil(size_t frameIndex); + + /// helpers to classify current type of animation + bool isDead() const; + bool isDying() const; + bool isDeadOrDying() const; + bool isIdle() const; + bool isMoving() const; + bool isShooting() const; +}; diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index e3ad8d960..db3838b61 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -1,248 +1,248 @@ -/* - * CGuiHandler.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 "CGuiHandler.h" -#include "../lib/CondSh.h" - -#include "CIntObject.h" -#include "CursorHandler.h" -#include "ShortcutHandler.h" -#include "FramerateManager.h" -#include "WindowHandler.h" -#include "EventDispatcher.h" -#include "../eventsSDL/InputHandler.h" - -#include "../CGameInfo.h" -#include "../render/Colors.h" -#include "../render/Graphics.h" -#include "../render/IFont.h" -#include "../render/EFont.h" -#include "../renderSDL/ScreenHandler.h" -#include "../renderSDL/RenderHandler.h" -#include "../CMT.h" -#include "../CPlayerInterface.h" -#include "../battle/BattleInterface.h" - -#include "../../lib/CThreadHelper.h" -#include "../../lib/CConfigHandler.h" - -#include - -CGuiHandler GH; - -static thread_local bool inGuiThread = false; - -SObjectConstruction::SObjectConstruction(CIntObject *obj) -:myObj(obj) -{ - GH.createdObj.push_front(obj); - GH.captureChildren = true; -} - -SObjectConstruction::~SObjectConstruction() -{ - assert(GH.createdObj.size()); - assert(GH.createdObj.front() == myObj); - GH.createdObj.pop_front(); - GH.captureChildren = GH.createdObj.size(); -} - -SSetCaptureState::SSetCaptureState(bool allow, ui8 actions) -{ - previousCapture = GH.captureChildren; - GH.captureChildren = false; - prevActions = GH.defActionsDef; - GH.defActionsDef = actions; -} - -SSetCaptureState::~SSetCaptureState() -{ - GH.captureChildren = previousCapture; - GH.defActionsDef = prevActions; -} - -void CGuiHandler::init() -{ - inGuiThread = true; - - inputHandlerInstance = std::make_unique(); - eventDispatcherInstance = std::make_unique(); - windowHandlerInstance = std::make_unique(); - screenHandlerInstance = std::make_unique(); - renderHandlerInstance = std::make_unique(); - shortcutsHandlerInstance = std::make_unique(); - framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); -} - -void CGuiHandler::handleEvents() -{ - events().dispatchTimer(framerate().getElapsedMilliseconds()); - - //player interface may want special event handling - if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents()) - return; - - input().processEvents(); -} - -void CGuiHandler::fakeMouseMove() -{ - dispatchMainThread([](){ - GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); - }); -} - -void CGuiHandler::startTextInput(const Rect & whereInput) -{ - input().startTextInput(whereInput); -} - -void CGuiHandler::stopTextInput() -{ - input().stopTextInput(); -} - -void CGuiHandler::renderFrame() -{ - { - boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - - if(nullptr != curInt) - curInt->update(); - - if(settings["video"]["showfps"].Bool()) - drawFPSCounter(); - - SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); - - SDL_RenderClear(mainRenderer); - SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); - - CCS->curh->render(); - - windows().onFrameRendered(); - } - - SDL_RenderPresent(mainRenderer); - framerate().framerateDelay(); // holds a constant FPS -} - -CGuiHandler::CGuiHandler() - : defActionsDef(0) - , captureChildren(false) - , curInt(nullptr) - , fakeStatusBar(std::make_shared()) -{ -} - -CGuiHandler::~CGuiHandler() = default; - -ShortcutHandler & CGuiHandler::shortcuts() -{ - assert(shortcutsHandlerInstance); - return *shortcutsHandlerInstance; -} - -FramerateManager & CGuiHandler::framerate() -{ - assert(framerateManagerInstance); - return *framerateManagerInstance; -} - -bool CGuiHandler::isKeyboardCtrlDown() const -{ - return inputHandlerInstance->isKeyboardCtrlDown(); -} - -bool CGuiHandler::isKeyboardAltDown() const -{ - return inputHandlerInstance->isKeyboardAltDown(); -} - -bool CGuiHandler::isKeyboardShiftDown() const -{ - return inputHandlerInstance->isKeyboardShiftDown(); -} - -const Point & CGuiHandler::getCursorPosition() const -{ - return inputHandlerInstance->getCursorPosition(); -} - -Point CGuiHandler::screenDimensions() const -{ - return Point(screen->w, screen->h); -} - -void CGuiHandler::drawFPSCounter() -{ - static SDL_Rect overlay = { 0, 0, 24, 24}; - uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); - SDL_FillRect(screen, &overlay, black); - std::string fps = std::to_string(framerate().getFramerate()); - graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2)); -} - -bool CGuiHandler::amIGuiThread() -{ - return inGuiThread; -} - -void CGuiHandler::dispatchMainThread(const std::function & functor) -{ - inputHandlerInstance->dispatchMainThread(functor); -} - -IScreenHandler & CGuiHandler::screenHandler() -{ - return *screenHandlerInstance; -} - -IRenderHandler & CGuiHandler::renderHandler() -{ - return *renderHandlerInstance; -} - -EventDispatcher & CGuiHandler::events() -{ - return *eventDispatcherInstance; -} - -InputHandler & CGuiHandler::input() -{ - return *inputHandlerInstance; -} - -WindowHandler & CGuiHandler::windows() -{ - assert(windowHandlerInstance); - return *windowHandlerInstance; -} - -std::shared_ptr CGuiHandler::statusbar() -{ - auto locked = currentStatusBar.lock(); - - if (!locked) - return fakeStatusBar; - - return locked; -} - -void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) -{ - currentStatusBar = newStatusBar; -} - -void CGuiHandler::onScreenResize() -{ - screenHandler().onScreenResize(); - windows().onScreenResize(); -} +/* + * CGuiHandler.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 "CGuiHandler.h" +#include "../lib/CondSh.h" + +#include "CIntObject.h" +#include "CursorHandler.h" +#include "ShortcutHandler.h" +#include "FramerateManager.h" +#include "WindowHandler.h" +#include "EventDispatcher.h" +#include "../eventsSDL/InputHandler.h" + +#include "../CGameInfo.h" +#include "../render/Colors.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../render/EFont.h" +#include "../renderSDL/ScreenHandler.h" +#include "../renderSDL/RenderHandler.h" +#include "../CMT.h" +#include "../CPlayerInterface.h" +#include "../battle/BattleInterface.h" + +#include "../../lib/CThreadHelper.h" +#include "../../lib/CConfigHandler.h" + +#include + +CGuiHandler GH; + +static thread_local bool inGuiThread = false; + +SObjectConstruction::SObjectConstruction(CIntObject *obj) +:myObj(obj) +{ + GH.createdObj.push_front(obj); + GH.captureChildren = true; +} + +SObjectConstruction::~SObjectConstruction() +{ + assert(GH.createdObj.size()); + assert(GH.createdObj.front() == myObj); + GH.createdObj.pop_front(); + GH.captureChildren = GH.createdObj.size(); +} + +SSetCaptureState::SSetCaptureState(bool allow, ui8 actions) +{ + previousCapture = GH.captureChildren; + GH.captureChildren = false; + prevActions = GH.defActionsDef; + GH.defActionsDef = actions; +} + +SSetCaptureState::~SSetCaptureState() +{ + GH.captureChildren = previousCapture; + GH.defActionsDef = prevActions; +} + +void CGuiHandler::init() +{ + inGuiThread = true; + + inputHandlerInstance = std::make_unique(); + eventDispatcherInstance = std::make_unique(); + windowHandlerInstance = std::make_unique(); + screenHandlerInstance = std::make_unique(); + renderHandlerInstance = std::make_unique(); + shortcutsHandlerInstance = std::make_unique(); + framerateManagerInstance = std::make_unique(settings["video"]["targetfps"].Integer()); +} + +void CGuiHandler::handleEvents() +{ + events().dispatchTimer(framerate().getElapsedMilliseconds()); + + //player interface may want special event handling + if(nullptr != LOCPLINT && LOCPLINT->capturedAllEvents()) + return; + + input().processEvents(); +} + +void CGuiHandler::fakeMouseMove() +{ + dispatchMainThread([](){ + GH.events().dispatchMouseMoved(Point(0, 0), GH.getCursorPosition()); + }); +} + +void CGuiHandler::startTextInput(const Rect & whereInput) +{ + input().startTextInput(whereInput); +} + +void CGuiHandler::stopTextInput() +{ + input().stopTextInput(); +} + +void CGuiHandler::renderFrame() +{ + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); + + if(nullptr != curInt) + curInt->update(); + + if(settings["video"]["showfps"].Bool()) + drawFPSCounter(); + + SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); + + SDL_RenderClear(mainRenderer); + SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + + CCS->curh->render(); + + windows().onFrameRendered(); + } + + SDL_RenderPresent(mainRenderer); + framerate().framerateDelay(); // holds a constant FPS +} + +CGuiHandler::CGuiHandler() + : defActionsDef(0) + , captureChildren(false) + , curInt(nullptr) + , fakeStatusBar(std::make_shared()) +{ +} + +CGuiHandler::~CGuiHandler() = default; + +ShortcutHandler & CGuiHandler::shortcuts() +{ + assert(shortcutsHandlerInstance); + return *shortcutsHandlerInstance; +} + +FramerateManager & CGuiHandler::framerate() +{ + assert(framerateManagerInstance); + return *framerateManagerInstance; +} + +bool CGuiHandler::isKeyboardCtrlDown() const +{ + return inputHandlerInstance->isKeyboardCtrlDown(); +} + +bool CGuiHandler::isKeyboardAltDown() const +{ + return inputHandlerInstance->isKeyboardAltDown(); +} + +bool CGuiHandler::isKeyboardShiftDown() const +{ + return inputHandlerInstance->isKeyboardShiftDown(); +} + +const Point & CGuiHandler::getCursorPosition() const +{ + return inputHandlerInstance->getCursorPosition(); +} + +Point CGuiHandler::screenDimensions() const +{ + return Point(screen->w, screen->h); +} + +void CGuiHandler::drawFPSCounter() +{ + static SDL_Rect overlay = { 0, 0, 24, 24}; + uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); + SDL_FillRect(screen, &overlay, black); + std::string fps = std::to_string(framerate().getFramerate()); + graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2)); +} + +bool CGuiHandler::amIGuiThread() +{ + return inGuiThread; +} + +void CGuiHandler::dispatchMainThread(const std::function & functor) +{ + inputHandlerInstance->dispatchMainThread(functor); +} + +IScreenHandler & CGuiHandler::screenHandler() +{ + return *screenHandlerInstance; +} + +IRenderHandler & CGuiHandler::renderHandler() +{ + return *renderHandlerInstance; +} + +EventDispatcher & CGuiHandler::events() +{ + return *eventDispatcherInstance; +} + +InputHandler & CGuiHandler::input() +{ + return *inputHandlerInstance; +} + +WindowHandler & CGuiHandler::windows() +{ + assert(windowHandlerInstance); + return *windowHandlerInstance; +} + +std::shared_ptr CGuiHandler::statusbar() +{ + auto locked = currentStatusBar.lock(); + + if (!locked) + return fakeStatusBar; + + return locked; +} + +void CGuiHandler::setStatusbar(std::shared_ptr newStatusBar) +{ + currentStatusBar = newStatusBar; +} + +void CGuiHandler::onScreenResize() +{ + screenHandler().onScreenResize(); + windows().onScreenResize(); +} diff --git a/client/gui/CGuiHandler.h b/client/gui/CGuiHandler.h index 20d52f74d..4f651c159 100644 --- a/client/gui/CGuiHandler.h +++ b/client/gui/CGuiHandler.h @@ -1,130 +1,130 @@ -/* - * CGuiHandler.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 - -VCMI_LIB_NAMESPACE_BEGIN -template struct CondSh; -class Point; -class Rect; -VCMI_LIB_NAMESPACE_END - -enum class MouseButton; -class ShortcutHandler; -class FramerateManager; -class IStatusBar; -class CIntObject; -class IUpdateable; -class IShowActivatable; -class IRenderHandler; -class IScreenHandler; -class WindowHandler; -class EventDispatcher; -class InputHandler; - -// Handles GUI logic and drawing -class CGuiHandler -{ -private: - /// Fake no-op version status bar, for use in windows that have no status bar - std::shared_ptr fakeStatusBar; - - /// Status bar of current window, if any. Uses weak_ptr to allow potential hanging reference after owned window has been deleted - std::weak_ptr currentStatusBar; - - std::unique_ptr shortcutsHandlerInstance; - std::unique_ptr windowHandlerInstance; - - std::unique_ptr screenHandlerInstance; - std::unique_ptr renderHandlerInstance; - std::unique_ptr framerateManagerInstance; - std::unique_ptr eventDispatcherInstance; - std::unique_ptr inputHandlerInstance; - -public: - boost::mutex interfaceMutex; - - /// returns current position of mouse cursor, relative to vcmi window - const Point & getCursorPosition() const; - - ShortcutHandler & shortcuts(); - FramerateManager & framerate(); - EventDispatcher & events(); - InputHandler & input(); - - /// Returns current logical screen dimensions - /// May not match size of window if user has UI scaling different from 100% - Point screenDimensions() const; - - /// returns true if chosen keyboard key is currently pressed down - bool isKeyboardAltDown() const; - bool isKeyboardCtrlDown() const; - bool isKeyboardShiftDown() const; - - void startTextInput(const Rect & where); - void stopTextInput(); - - IScreenHandler & screenHandler(); - IRenderHandler & renderHandler(); - WindowHandler & windows(); - - /// Returns currently active status bar. Guaranteed to be non-null - std::shared_ptr statusbar(); - - /// Set currently active status bar - void setStatusbar(std::shared_ptr); - - IUpdateable *curInt; - - ui8 defActionsDef; //default auto actions - bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list - std::list createdObj; //stack of objs being created - - CGuiHandler(); - ~CGuiHandler(); - - void init(); - void renderFrame(); - - /// called whenever user selects different resolution, requiring to center/resize all windows - void onScreenResize(); - - void handleEvents(); //takes events from queue and calls interested objects - void fakeMouseMove(); - void drawFPSCounter(); // draws the FPS to the upper left corner of the screen - - bool amIGuiThread(); - - /// Calls provided functor in main thread on next execution frame - void dispatchMainThread(const std::function & functor); -}; - -extern CGuiHandler GH; //global gui handler - -struct SObjectConstruction -{ - CIntObject *myObj; - SObjectConstruction(CIntObject *obj); - ~SObjectConstruction(); -}; - -struct SSetCaptureState -{ - bool previousCapture; - ui8 prevActions; - SSetCaptureState(bool allow, ui8 actions); - ~SSetCaptureState(); -}; - -#define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) -#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj) -#define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) -#define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) - -#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this) +/* + * CGuiHandler.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 + +VCMI_LIB_NAMESPACE_BEGIN +template struct CondSh; +class Point; +class Rect; +VCMI_LIB_NAMESPACE_END + +enum class MouseButton; +class ShortcutHandler; +class FramerateManager; +class IStatusBar; +class CIntObject; +class IUpdateable; +class IShowActivatable; +class IRenderHandler; +class IScreenHandler; +class WindowHandler; +class EventDispatcher; +class InputHandler; + +// Handles GUI logic and drawing +class CGuiHandler +{ +private: + /// Fake no-op version status bar, for use in windows that have no status bar + std::shared_ptr fakeStatusBar; + + /// Status bar of current window, if any. Uses weak_ptr to allow potential hanging reference after owned window has been deleted + std::weak_ptr currentStatusBar; + + std::unique_ptr shortcutsHandlerInstance; + std::unique_ptr windowHandlerInstance; + + std::unique_ptr screenHandlerInstance; + std::unique_ptr renderHandlerInstance; + std::unique_ptr framerateManagerInstance; + std::unique_ptr eventDispatcherInstance; + std::unique_ptr inputHandlerInstance; + +public: + boost::mutex interfaceMutex; + + /// returns current position of mouse cursor, relative to vcmi window + const Point & getCursorPosition() const; + + ShortcutHandler & shortcuts(); + FramerateManager & framerate(); + EventDispatcher & events(); + InputHandler & input(); + + /// Returns current logical screen dimensions + /// May not match size of window if user has UI scaling different from 100% + Point screenDimensions() const; + + /// returns true if chosen keyboard key is currently pressed down + bool isKeyboardAltDown() const; + bool isKeyboardCtrlDown() const; + bool isKeyboardShiftDown() const; + + void startTextInput(const Rect & where); + void stopTextInput(); + + IScreenHandler & screenHandler(); + IRenderHandler & renderHandler(); + WindowHandler & windows(); + + /// Returns currently active status bar. Guaranteed to be non-null + std::shared_ptr statusbar(); + + /// Set currently active status bar + void setStatusbar(std::shared_ptr); + + IUpdateable *curInt; + + ui8 defActionsDef; //default auto actions + bool captureChildren; //all newly created objects will get their parents from stack and will be added to parents children list + std::list createdObj; //stack of objs being created + + CGuiHandler(); + ~CGuiHandler(); + + void init(); + void renderFrame(); + + /// called whenever user selects different resolution, requiring to center/resize all windows + void onScreenResize(); + + void handleEvents(); //takes events from queue and calls interested objects + void fakeMouseMove(); + void drawFPSCounter(); // draws the FPS to the upper left corner of the screen + + bool amIGuiThread(); + + /// Calls provided functor in main thread on next execution frame + void dispatchMainThread(const std::function & functor); +}; + +extern CGuiHandler GH; //global gui handler + +struct SObjectConstruction +{ + CIntObject *myObj; + SObjectConstruction(CIntObject *obj); + ~SObjectConstruction(); +}; + +struct SSetCaptureState +{ + bool previousCapture; + ui8 prevActions; + SSetCaptureState(bool allow, ui8 actions); + ~SSetCaptureState(); +}; + +#define OBJ_CONSTRUCTION SObjectConstruction obj__i(this) +#define OBJ_CONSTRUCTION_TARGETED(obj) SObjectConstruction obj__i(obj) +#define OBJECT_CONSTRUCTION_CAPTURING(actions) defActions = actions; SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) +#define OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(actions) SSetCaptureState obj__i1(true, actions); SObjectConstruction obj__i(this) + +#define OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE defActions = 255 - DISPOSE; SSetCaptureState obj__i1(true, 255 - DISPOSE); SObjectConstruction obj__i(this) diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 74c946d16..3f136151e 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -1,346 +1,346 @@ -/* - * CIntObject.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 "CIntObject.h" - -#include "CGuiHandler.h" -#include "WindowHandler.h" -#include "EventDispatcher.h" -#include "Shortcut.h" -#include "../render/Canvas.h" -#include "../windows/CMessage.h" -#include "../CMT.h" - -CIntObject::CIntObject(int used_, Point pos_): - parent_m(nullptr), - parent(parent_m), - redrawParent(false), - inputEnabled(true), - used(used_), - recActions(GH.defActionsDef), - defActions(GH.defActionsDef), - pos(pos_, Point()) -{ - if(GH.captureChildren) - GH.createdObj.front()->addChild(this, true); -} - -CIntObject::~CIntObject() -{ - if(isActive()) - deactivate(); - - while(!children.empty()) - { - if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) - delete children.front(); - else - removeChild(children.front()); - } - - if(parent_m) - parent_m->removeChild(this); -} - -void CIntObject::show(Canvas & to) -{ - if(defActions & UPDATE) - for(auto & elem : children) - if(elem->recActions & UPDATE) - elem->show(to); -} - -void CIntObject::showAll(Canvas & to) -{ - if(defActions & SHOWALL) - { - for(auto & elem : children) - if(elem->recActions & SHOWALL) - elem->showAll(to); - } -} - -void CIntObject::activate() -{ - if (isActive()) - return; - - if (inputEnabled) - activateEvents(used | GENERAL); - else - activateEvents(GENERAL); - - assert(isActive()); - - if(defActions & ACTIVATE) - for(auto & elem : children) - if(elem->recActions & ACTIVATE) - elem->activate(); -} - -void CIntObject::deactivate() -{ - if (!isActive()) - return; - - deactivateEvents(used | GENERAL); - - assert(!isActive()); - - if(defActions & DEACTIVATE) - for(auto & elem : children) - if(elem->recActions & DEACTIVATE) - elem->deactivate(); -} - -void CIntObject::addUsedEvents(ui16 newActions) -{ - if (isActive() && inputEnabled) - activateEvents(~used & newActions); - used |= newActions; -} - -void CIntObject::removeUsedEvents(ui16 newActions) -{ - if (isActive()) - deactivateEvents(used & newActions); - used &= ~newActions; -} - -void CIntObject::disable() -{ - if(isActive()) - deactivate(); - - recActions = DISPOSE; -} - -void CIntObject::enable() -{ - if(!isActive() && (!parent_m || parent_m->isActive())) - { - activate(); - redraw(); - } - - recActions = 255; -} - -void CIntObject::setEnabled(bool on) -{ - if (on) - enable(); - else - disable(); -} - -void CIntObject::setInputEnabled(bool on) -{ - if (inputEnabled == on) - return; - - inputEnabled = on; - - if (isActive()) - { - assert((used & GENERAL) == 0); - - if (on) - activateEvents(used); - else - deactivateEvents(used); - } - - for(auto & elem : children) - elem->setInputEnabled(on); -} - -void CIntObject::setRedrawParent(bool on) -{ - redrawParent = on; -} - -void CIntObject::fitToScreen(int borderWidth, bool propagate) -{ - Point newPos = pos.topLeft(); - vstd::amax(newPos.x, borderWidth); - vstd::amax(newPos.y, borderWidth); - vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w); - vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h); - if (newPos != pos.topLeft()) - moveTo(newPos, propagate); -} - -void CIntObject::moveBy(const Point & p, bool propagate) -{ - pos.x += p.x; - pos.y += p.y; - if(propagate) - for(auto & elem : children) - elem->moveBy(p, propagate); -} - -void CIntObject::moveTo(const Point & p, bool propagate) -{ - moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); -} - -void CIntObject::addChild(CIntObject * child, bool adjustPosition) -{ - if (vstd::contains(children, child)) - { - return; - } - if (child->parent_m) - { - child->parent_m->removeChild(child, adjustPosition); - } - children.push_back(child); - child->parent_m = this; - if(adjustPosition) - child->moveBy(pos.topLeft(), adjustPosition); - - if (inputEnabled != child->inputEnabled) - child->setInputEnabled(inputEnabled); - - if (!isActive() && child->isActive()) - child->deactivate(); - if (isActive()&& !child->isActive()) - child->activate(); -} - -void CIntObject::removeChild(CIntObject * child, bool adjustPosition) -{ - if (!child) - return; - - if(!vstd::contains(children, child)) - throw std::runtime_error("Wrong child object"); - - if(child->parent_m != this) - throw std::runtime_error("Wrong child object"); - - children -= child; - child->parent_m = nullptr; - if(adjustPosition) - child->pos -= pos.topLeft(); -} - -void CIntObject::redraw() -{ - //currently most of calls come from active objects so this check won't affect them - //it should fix glitches when called by inactive elements located below active window - if (isActive()) - { - if (parent_m && redrawParent) - { - parent_m->redraw(); - } - else - { - Canvas buffer = Canvas::createFromSurface(screenBuf); - - showAll(buffer); - if(screenBuf != screen) - { - Canvas screenBuffer = Canvas::createFromSurface(screen); - - showAll(screenBuffer); - } - } - } -} - -bool CIntObject::receiveEvent(const Point & position, int eventType) const -{ - return pos.isInside(position); -} - -const Rect & CIntObject::getPosition() const -{ - return pos; -} - -void CIntObject::onScreenResize() -{ - center(pos, true); -} - -bool CIntObject::isPopupWindow() const -{ - return false; -} - -const Rect & CIntObject::center( const Rect &r, bool propagate ) -{ - pos.w = r.w; - pos.h = r.h; - return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate); -} - -const Rect & CIntObject::center( bool propagate ) -{ - return center(pos, propagate); -} - -const Rect & CIntObject::center(const Point & p, bool propagate) -{ - moveBy(Point(p.x - pos.w/2 - pos.x, - p.y - pos.h/2 - pos.y), - propagate); - return pos; -} - -bool CIntObject::captureThisKey(EShortcut key) -{ - return false; -} - -CKeyShortcut::CKeyShortcut() - : assignedKey(EShortcut::NONE) - , shortcutPressed(false) -{} - -CKeyShortcut::CKeyShortcut(EShortcut key) - : assignedKey(key) - , shortcutPressed(false) -{ -} - -void CKeyShortcut::keyPressed(EShortcut key) -{ - if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed) - { - shortcutPressed = true; - clickPressed(GH.getCursorPosition()); - } -} - -void CKeyShortcut::keyReleased(EShortcut key) -{ - if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed) - { - shortcutPressed = false; - clickReleased(GH.getCursorPosition()); - } -} - -WindowBase::WindowBase(int used_, Point pos_) - : CIntObject(used_, pos_) -{ - -} - -void WindowBase::close() -{ - if(!GH.windows().isTopWindow(this)) - logGlobal->error("Only top interface must be closed"); - GH.windows().popWindows(1); -} +/* + * CIntObject.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 "CIntObject.h" + +#include "CGuiHandler.h" +#include "WindowHandler.h" +#include "EventDispatcher.h" +#include "Shortcut.h" +#include "../render/Canvas.h" +#include "../windows/CMessage.h" +#include "../CMT.h" + +CIntObject::CIntObject(int used_, Point pos_): + parent_m(nullptr), + parent(parent_m), + redrawParent(false), + inputEnabled(true), + used(used_), + recActions(GH.defActionsDef), + defActions(GH.defActionsDef), + pos(pos_, Point()) +{ + if(GH.captureChildren) + GH.createdObj.front()->addChild(this, true); +} + +CIntObject::~CIntObject() +{ + if(isActive()) + deactivate(); + + while(!children.empty()) + { + if((defActions & DISPOSE) && (children.front()->recActions & DISPOSE)) + delete children.front(); + else + removeChild(children.front()); + } + + if(parent_m) + parent_m->removeChild(this); +} + +void CIntObject::show(Canvas & to) +{ + if(defActions & UPDATE) + for(auto & elem : children) + if(elem->recActions & UPDATE) + elem->show(to); +} + +void CIntObject::showAll(Canvas & to) +{ + if(defActions & SHOWALL) + { + for(auto & elem : children) + if(elem->recActions & SHOWALL) + elem->showAll(to); + } +} + +void CIntObject::activate() +{ + if (isActive()) + return; + + if (inputEnabled) + activateEvents(used | GENERAL); + else + activateEvents(GENERAL); + + assert(isActive()); + + if(defActions & ACTIVATE) + for(auto & elem : children) + if(elem->recActions & ACTIVATE) + elem->activate(); +} + +void CIntObject::deactivate() +{ + if (!isActive()) + return; + + deactivateEvents(used | GENERAL); + + assert(!isActive()); + + if(defActions & DEACTIVATE) + for(auto & elem : children) + if(elem->recActions & DEACTIVATE) + elem->deactivate(); +} + +void CIntObject::addUsedEvents(ui16 newActions) +{ + if (isActive() && inputEnabled) + activateEvents(~used & newActions); + used |= newActions; +} + +void CIntObject::removeUsedEvents(ui16 newActions) +{ + if (isActive()) + deactivateEvents(used & newActions); + used &= ~newActions; +} + +void CIntObject::disable() +{ + if(isActive()) + deactivate(); + + recActions = DISPOSE; +} + +void CIntObject::enable() +{ + if(!isActive() && (!parent_m || parent_m->isActive())) + { + activate(); + redraw(); + } + + recActions = 255; +} + +void CIntObject::setEnabled(bool on) +{ + if (on) + enable(); + else + disable(); +} + +void CIntObject::setInputEnabled(bool on) +{ + if (inputEnabled == on) + return; + + inputEnabled = on; + + if (isActive()) + { + assert((used & GENERAL) == 0); + + if (on) + activateEvents(used); + else + deactivateEvents(used); + } + + for(auto & elem : children) + elem->setInputEnabled(on); +} + +void CIntObject::setRedrawParent(bool on) +{ + redrawParent = on; +} + +void CIntObject::fitToScreen(int borderWidth, bool propagate) +{ + Point newPos = pos.topLeft(); + vstd::amax(newPos.x, borderWidth); + vstd::amax(newPos.y, borderWidth); + vstd::amin(newPos.x, GH.screenDimensions().x - borderWidth - pos.w); + vstd::amin(newPos.y, GH.screenDimensions().y - borderWidth - pos.h); + if (newPos != pos.topLeft()) + moveTo(newPos, propagate); +} + +void CIntObject::moveBy(const Point & p, bool propagate) +{ + pos.x += p.x; + pos.y += p.y; + if(propagate) + for(auto & elem : children) + elem->moveBy(p, propagate); +} + +void CIntObject::moveTo(const Point & p, bool propagate) +{ + moveBy(Point(p.x - pos.x, p.y - pos.y), propagate); +} + +void CIntObject::addChild(CIntObject * child, bool adjustPosition) +{ + if (vstd::contains(children, child)) + { + return; + } + if (child->parent_m) + { + child->parent_m->removeChild(child, adjustPosition); + } + children.push_back(child); + child->parent_m = this; + if(adjustPosition) + child->moveBy(pos.topLeft(), adjustPosition); + + if (inputEnabled != child->inputEnabled) + child->setInputEnabled(inputEnabled); + + if (!isActive() && child->isActive()) + child->deactivate(); + if (isActive()&& !child->isActive()) + child->activate(); +} + +void CIntObject::removeChild(CIntObject * child, bool adjustPosition) +{ + if (!child) + return; + + if(!vstd::contains(children, child)) + throw std::runtime_error("Wrong child object"); + + if(child->parent_m != this) + throw std::runtime_error("Wrong child object"); + + children -= child; + child->parent_m = nullptr; + if(adjustPosition) + child->pos -= pos.topLeft(); +} + +void CIntObject::redraw() +{ + //currently most of calls come from active objects so this check won't affect them + //it should fix glitches when called by inactive elements located below active window + if (isActive()) + { + if (parent_m && redrawParent) + { + parent_m->redraw(); + } + else + { + Canvas buffer = Canvas::createFromSurface(screenBuf); + + showAll(buffer); + if(screenBuf != screen) + { + Canvas screenBuffer = Canvas::createFromSurface(screen); + + showAll(screenBuffer); + } + } + } +} + +bool CIntObject::receiveEvent(const Point & position, int eventType) const +{ + return pos.isInside(position); +} + +const Rect & CIntObject::getPosition() const +{ + return pos; +} + +void CIntObject::onScreenResize() +{ + center(pos, true); +} + +bool CIntObject::isPopupWindow() const +{ + return false; +} + +const Rect & CIntObject::center( const Rect &r, bool propagate ) +{ + pos.w = r.w; + pos.h = r.h; + return center(Point(GH.screenDimensions().x/2, GH.screenDimensions().y/2), propagate); +} + +const Rect & CIntObject::center( bool propagate ) +{ + return center(pos, propagate); +} + +const Rect & CIntObject::center(const Point & p, bool propagate) +{ + moveBy(Point(p.x - pos.w/2 - pos.x, + p.y - pos.h/2 - pos.y), + propagate); + return pos; +} + +bool CIntObject::captureThisKey(EShortcut key) +{ + return false; +} + +CKeyShortcut::CKeyShortcut() + : assignedKey(EShortcut::NONE) + , shortcutPressed(false) +{} + +CKeyShortcut::CKeyShortcut(EShortcut key) + : assignedKey(key) + , shortcutPressed(false) +{ +} + +void CKeyShortcut::keyPressed(EShortcut key) +{ + if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed) + { + shortcutPressed = true; + clickPressed(GH.getCursorPosition()); + } +} + +void CKeyShortcut::keyReleased(EShortcut key) +{ + if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed) + { + shortcutPressed = false; + clickReleased(GH.getCursorPosition()); + } +} + +WindowBase::WindowBase(int used_, Point pos_) + : CIntObject(used_, pos_) +{ + +} + +void WindowBase::close() +{ + if(!GH.windows().isTopWindow(this)) + logGlobal->error("Only top interface must be closed"); + GH.windows().popWindows(1); +} diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 6ea72ed56..150e55b8a 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -1,191 +1,191 @@ -/* - * CIntObject.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 "EventsReceiver.h" - -#include "../../lib/Rect.h" -#include "../../lib/Color.h" -#include "../../lib/GameConstants.h" - -class CGuiHandler; -class CPicture; -class Canvas; - -class IUpdateable -{ -public: - virtual void update()=0; - virtual ~IUpdateable() = default; -}; - -class IShowActivatable -{ -public: - virtual void activate()=0; - virtual void deactivate()=0; - - virtual void redraw()=0; - virtual void show(Canvas & to) = 0; - virtual void showAll(Canvas & to) = 0; - - virtual bool isPopupWindow() const = 0; - virtual void onScreenResize() = 0; - virtual ~IShowActivatable() = default; -}; - -// Base UI element -class CIntObject : public IShowActivatable, public AEventsReceiver //interface object -{ - ui16 used; - - //non-const versions of fields to allow changing them in CIntObject - CIntObject *parent_m; //parent object - - bool inputEnabled; - bool redrawParent; - -public: - std::vector children; - - /// read-only parent access. May not be a "clean" solution but allows some compatibility - CIntObject * const & parent; - - /// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead - /*const*/ Rect pos; - - CIntObject(int used=0, Point offset=Point()); - virtual ~CIntObject(); - - bool captureThisKey(EShortcut key) override; - - void addUsedEvents(ui16 newActions); - void removeUsedEvents(ui16 newActions); - - enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32}; - ui8 defActions; //which calls will be tried to be redirected to children - ui8 recActions; //which calls we allow to receive from parent - - /// deactivates if needed, blocks all automatic activity, allows only disposal - void disable(); - /// activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!) - void enable(); - /// deactivates or activates UI element based on flag - void setEnabled(bool on); - - /// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element - void setInputEnabled(bool on); - - /// Mark this input as one that requires parent redraw on update, - /// for example if current control might have semi-transparent elements and requires redrawing of background - void setRedrawParent(bool on); - - // activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse) - // usually used automatically by parent - void activate() override; - void deactivate() override; - - //called each frame to update screen - void show(Canvas & to) override; - //called on complete redraw only - void showAll(Canvas & to) override; - //request complete redraw of this object - void redraw() override; - - /// returns true if this element is a popup window - /// called only for windows - bool isPopupWindow() const override; - - /// called only for windows whenever screen size changes - /// default behavior is to re-center, can be overriden - void onScreenResize() override; - - /// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...) - /// by default, usedEvents inside UI elements are always handled - bool receiveEvent(const Point & position, int eventType) const override; - - const Rect & getPosition() const override; - - const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position - const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center - const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position - void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen - void moveBy(const Point &p, bool propagate = true); - void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner) - - void addChild(CIntObject *child, bool adjustPosition = false); - void removeChild(CIntObject *child, bool adjustPosition = false); - -}; - -/// Class for binding keys to left mouse button clicks -/// Classes wanting use it should have it as one of their base classes -class CKeyShortcut : public virtual CIntObject -{ - bool shortcutPressed; -public: - EShortcut assignedKey; - CKeyShortcut(); - CKeyShortcut(EShortcut key); - void keyPressed(EShortcut key) override; - void keyReleased(EShortcut key) override; - -}; - -class WindowBase : public CIntObject -{ -public: - WindowBase(int used_ = 0, Point pos_ = Point()); -protected: - virtual void close(); -}; - -class IGarrisonHolder -{ -public: - virtual void updateGarrisons() = 0; -}; - -class ITownHolder -{ -public: - virtual void buildChanged() = 0; -}; - -class IStatusBar -{ -public: - virtual ~IStatusBar() = default; - - /// set current text for the status bar - virtual void write(const std::string & text) = 0; - - /// remove any current text from the status bar - virtual void clear() = 0; - - /// remove text from status bar if current text matches tested text - virtual void clearIfMatching(const std::string & testedText) = 0; - - /// enables mode for entering text instead of showing hover text - virtual void setEnteringMode(bool on) = 0; - - /// overrides hover text from controls with text entered into in-game console (for chat/cheats) - virtual void setEnteredText(const std::string & text) = 0; - -}; - -class EmptyStatusBar : public IStatusBar -{ - virtual void write(const std::string & text){}; - virtual void clear(){}; - virtual void clearIfMatching(const std::string & testedText){}; - virtual void setEnteringMode(bool on){}; - virtual void setEnteredText(const std::string & text){}; -}; +/* + * CIntObject.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 "EventsReceiver.h" + +#include "../../lib/Rect.h" +#include "../../lib/Color.h" +#include "../../lib/GameConstants.h" + +class CGuiHandler; +class CPicture; +class Canvas; + +class IUpdateable +{ +public: + virtual void update()=0; + virtual ~IUpdateable() = default; +}; + +class IShowActivatable +{ +public: + virtual void activate()=0; + virtual void deactivate()=0; + + virtual void redraw()=0; + virtual void show(Canvas & to) = 0; + virtual void showAll(Canvas & to) = 0; + + virtual bool isPopupWindow() const = 0; + virtual void onScreenResize() = 0; + virtual ~IShowActivatable() = default; +}; + +// Base UI element +class CIntObject : public IShowActivatable, public AEventsReceiver //interface object +{ + ui16 used; + + //non-const versions of fields to allow changing them in CIntObject + CIntObject *parent_m; //parent object + + bool inputEnabled; + bool redrawParent; + +public: + std::vector children; + + /// read-only parent access. May not be a "clean" solution but allows some compatibility + CIntObject * const & parent; + + /// position of object on the screen. Please do not modify this anywhere but in constructor - use moveBy\moveTo instead + /*const*/ Rect pos; + + CIntObject(int used=0, Point offset=Point()); + virtual ~CIntObject(); + + bool captureThisKey(EShortcut key) override; + + void addUsedEvents(ui16 newActions); + void removeUsedEvents(ui16 newActions); + + enum {ACTIVATE=1, DEACTIVATE=2, UPDATE=4, SHOWALL=8, DISPOSE=16, SHARE_POS=32}; + ui8 defActions; //which calls will be tried to be redirected to children + ui8 recActions; //which calls we allow to receive from parent + + /// deactivates if needed, blocks all automatic activity, allows only disposal + void disable(); + /// activates if needed, all activity enabled (Warning: may not be symetric with disable if recActions was limited!) + void enable(); + /// deactivates or activates UI element based on flag + void setEnabled(bool on); + + /// Block (or allow) all user input, e.g. mouse/keyboard/touch without hiding element + void setInputEnabled(bool on); + + /// Mark this input as one that requires parent redraw on update, + /// for example if current control might have semi-transparent elements and requires redrawing of background + void setRedrawParent(bool on); + + // activate or deactivate object. Inactive object won't receive any input events (keyboard\mouse) + // usually used automatically by parent + void activate() override; + void deactivate() override; + + //called each frame to update screen + void show(Canvas & to) override; + //called on complete redraw only + void showAll(Canvas & to) override; + //request complete redraw of this object + void redraw() override; + + /// returns true if this element is a popup window + /// called only for windows + bool isPopupWindow() const override; + + /// called only for windows whenever screen size changes + /// default behavior is to re-center, can be overriden + void onScreenResize() override; + + /// returns true if UI elements wants to handle event of specific type (LCLICK, SHOW_POPUP ...) + /// by default, usedEvents inside UI elements are always handled + bool receiveEvent(const Point & position, int eventType) const override; + + const Rect & getPosition() const override; + + const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position + const Rect & center(const Point &p, bool propagate = true); //moves object so that point p will be in its center + const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position + void fitToScreen(int borderWidth, bool propagate = true); //moves window to fit into screen + void moveBy(const Point &p, bool propagate = true); + void moveTo(const Point &p, bool propagate = true);//move this to new position, coordinates are absolute (0,0 is topleft screen corner) + + void addChild(CIntObject *child, bool adjustPosition = false); + void removeChild(CIntObject *child, bool adjustPosition = false); + +}; + +/// Class for binding keys to left mouse button clicks +/// Classes wanting use it should have it as one of their base classes +class CKeyShortcut : public virtual CIntObject +{ + bool shortcutPressed; +public: + EShortcut assignedKey; + CKeyShortcut(); + CKeyShortcut(EShortcut key); + void keyPressed(EShortcut key) override; + void keyReleased(EShortcut key) override; + +}; + +class WindowBase : public CIntObject +{ +public: + WindowBase(int used_ = 0, Point pos_ = Point()); +protected: + virtual void close(); +}; + +class IGarrisonHolder +{ +public: + virtual void updateGarrisons() = 0; +}; + +class ITownHolder +{ +public: + virtual void buildChanged() = 0; +}; + +class IStatusBar +{ +public: + virtual ~IStatusBar() = default; + + /// set current text for the status bar + virtual void write(const std::string & text) = 0; + + /// remove any current text from the status bar + virtual void clear() = 0; + + /// remove text from status bar if current text matches tested text + virtual void clearIfMatching(const std::string & testedText) = 0; + + /// enables mode for entering text instead of showing hover text + virtual void setEnteringMode(bool on) = 0; + + /// overrides hover text from controls with text entered into in-game console (for chat/cheats) + virtual void setEnteredText(const std::string & text) = 0; + +}; + +class EmptyStatusBar : public IStatusBar +{ + virtual void write(const std::string & text){}; + virtual void clear(){}; + virtual void clearIfMatching(const std::string & testedText){}; + virtual void setEnteringMode(bool on){}; + virtual void setEnteredText(const std::string & text){}; +}; diff --git a/client/gui/CursorHandler.cpp b/client/gui/CursorHandler.cpp index d5c7a91cd..a9fa812e2 100644 --- a/client/gui/CursorHandler.cpp +++ b/client/gui/CursorHandler.cpp @@ -1,292 +1,292 @@ -/* - * CCursorHandler.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 "CursorHandler.h" - -#include "CGuiHandler.h" -#include "FramerateManager.h" -#include "../renderSDL/CursorSoftware.h" -#include "../renderSDL/CursorHardware.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" - -#include "../../lib/CConfigHandler.h" - -std::unique_ptr CursorHandler::createCursor() -{ -#if defined(VCMI_MOBILE) - if (settings["general"]["userRelativePointer"].Bool()) - return std::make_unique(); -#endif - - if (settings["video"]["cursor"].String() == "hardware") - return std::make_unique(); - - assert(settings["video"]["cursor"].String() == "software"); - return std::make_unique(); -} - -CursorHandler::CursorHandler() - : cursor(createCursor()) - , frameTime(0.f) - , showing(false) - , pos(0,0) -{ - - type = Cursor::Type::DEFAULT; - dndObject = nullptr; - - cursors = - { - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")), - GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")) - }; - - for (auto & cursor : cursors) - cursor->preload(); - - set(Cursor::Map::POINTER); -} - -CursorHandler::~CursorHandler() = default; - -void CursorHandler::changeGraphic(Cursor::Type type, size_t index) -{ - assert(dndObject == nullptr); - - if (type == this->type && index == this->frame) - return; - - this->type = type; - this->frame = index; - - cursor->setImage(getCurrentImage(), getPivotOffset()); -} - -void CursorHandler::set(Cursor::Default index) -{ - changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); -} - -void CursorHandler::set(Cursor::Map index) -{ - changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); -} - -void CursorHandler::set(Cursor::Combat index) -{ - changeGraphic(Cursor::Type::COMBAT, static_cast(index)); -} - -void CursorHandler::set(Cursor::Spellcast index) -{ - //Note: this is animated cursor, ignore specified frame and only change type - changeGraphic(Cursor::Type::SPELLBOOK, frame); -} - -void CursorHandler::dragAndDropCursor(std::shared_ptr image) -{ - dndObject = image; - cursor->setImage(getCurrentImage(), getPivotOffset()); -} - -void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) -{ - auto anim = GH.renderHandler().loadAnimation(path); - anim->load(index); - dragAndDropCursor(anim->getImage(index)); -} - -void CursorHandler::cursorMove(const int & x, const int & y) -{ - pos.x = x; - pos.y = y; - - cursor->setCursorPosition(pos); -} - -Point CursorHandler::getPivotOffsetDefault(size_t index) -{ - return {0, 0}; -} - -Point CursorHandler::getPivotOffsetMap(size_t index) -{ - static const std::array offsets = {{ - { 0, 0}, // POINTER = 0, - { 0, 0}, // HOURGLASS = 1, - { 12, 10}, // HERO = 2, - { 12, 12}, // TOWN = 3, - - { 15, 13}, // T1_MOVE = 4, - { 13, 13}, // T1_ATTACK = 5, - { 16, 32}, // T1_SAIL = 6, - { 13, 20}, // T1_DISEMBARK = 7, - { 8, 9}, // T1_EXCHANGE = 8, - { 14, 16}, // T1_VISIT = 9, - - { 15, 13}, // T2_MOVE = 10, - { 13, 13}, // T2_ATTACK = 11, - { 16, 32}, // T2_SAIL = 12, - { 13, 20}, // T2_DISEMBARK = 13, - { 8, 9}, // T2_EXCHANGE = 14, - { 14, 16}, // T2_VISIT = 15, - - { 15, 13}, // T3_MOVE = 16, - { 13, 13}, // T3_ATTACK = 17, - { 16, 32}, // T3_SAIL = 18, - { 13, 20}, // T3_DISEMBARK = 19, - { 8, 9}, // T3_EXCHANGE = 20, - { 14, 16}, // T3_VISIT = 21, - - { 15, 13}, // T4_MOVE = 22, - { 13, 13}, // T4_ATTACK = 23, - { 16, 32}, // T4_SAIL = 24, - { 13, 20}, // T4_DISEMBARK = 25, - { 8, 9}, // T4_EXCHANGE = 26, - { 14, 16}, // T4_VISIT = 27, - - { 16, 32}, // T1_SAIL_VISIT = 28, - { 16, 32}, // T2_SAIL_VISIT = 29, - { 16, 32}, // T3_SAIL_VISIT = 30, - { 16, 32}, // T4_SAIL_VISIT = 31, - - { 6, 1}, // SCROLL_NORTH = 32, - { 16, 2}, // SCROLL_NORTHEAST = 33, - { 21, 6}, // SCROLL_EAST = 34, - { 16, 16}, // SCROLL_SOUTHEAST = 35, - { 6, 21}, // SCROLL_SOUTH = 36, - { 1, 16}, // SCROLL_SOUTHWEST = 37, - { 1, 5}, // SCROLL_WEST = 38, - { 2, 1}, // SCROLL_NORTHWEST = 39, - - { 0, 0}, // POINTER_COPY = 40, - { 14, 16}, // TELEPORT = 41, - { 20, 20}, // SCUTTLE_BOAT = 42 - }}; - - assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index]; -} - -Point CursorHandler::getPivotOffsetCombat(size_t index) -{ - static const std::array offsets = {{ - { 12, 12 }, // BLOCKED = 0, - { 10, 14 }, // MOVE = 1, - { 14, 14 }, // FLY = 2, - { 12, 12 }, // SHOOT = 3, - { 12, 12 }, // HERO = 4, - { 8, 12 }, // QUERY = 5, - { 0, 0 }, // POINTER = 6, - { 21, 0 }, // HIT_NORTHEAST = 7, - { 31, 5 }, // HIT_EAST = 8, - { 21, 21 }, // HIT_SOUTHEAST = 9, - { 0, 21 }, // HIT_SOUTHWEST = 10, - { 0, 5 }, // HIT_WEST = 11, - { 0, 0 }, // HIT_NORTHWEST = 12, - { 6, 0 }, // HIT_NORTH = 13, - { 6, 31 }, // HIT_SOUTH = 14, - { 14, 0 }, // SHOOT_PENALTY = 15, - { 12, 12 }, // SHOOT_CATAPULT = 16, - { 12, 12 }, // HEAL = 17, - { 12, 12 }, // SACRIFICE = 18, - { 14, 20 }, // TELEPORT = 19 - }}; - - assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor - assert(index < offsets.size()); - return offsets[index]; -} - -Point CursorHandler::getPivotOffsetSpellcast() -{ - return { 18, 28}; -} - -Point CursorHandler::getPivotOffset() -{ - if (dndObject) - return dndObject->dimensions() / 2; - - switch (type) { - case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame); - case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame); - case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame); - case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast(); - }; - - assert(0); - return {0, 0}; -} - -std::shared_ptr CursorHandler::getCurrentImage() -{ - if (dndObject) - return dndObject; - - return cursors[static_cast(type)]->getImage(frame); -} - -void CursorHandler::updateSpellcastCursor() -{ - static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame - - frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f; - size_t newFrame = frame; - - while (frameTime >= frameDisplayDuration) - { - frameTime -= frameDisplayDuration; - newFrame++; - } - - auto & animation = cursors.at(static_cast(type)); - - while (newFrame >= animation->size()) - newFrame -= animation->size(); - - changeGraphic(Cursor::Type::SPELLBOOK, newFrame); -} - -void CursorHandler::render() -{ - if(!showing) - return; - - if (type == Cursor::Type::SPELLBOOK) - updateSpellcastCursor(); - - cursor->render(); -} - -void CursorHandler::hide() -{ - if (!showing) - return; - - showing = false; - cursor->setVisible(false); -} - -void CursorHandler::show() -{ - if (showing) - return; - - showing = true; - cursor->setVisible(true); -} - +/* + * CCursorHandler.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 "CursorHandler.h" + +#include "CGuiHandler.h" +#include "FramerateManager.h" +#include "../renderSDL/CursorSoftware.h" +#include "../renderSDL/CursorHardware.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../../lib/CConfigHandler.h" + +std::unique_ptr CursorHandler::createCursor() +{ +#if defined(VCMI_MOBILE) + if (settings["general"]["userRelativePointer"].Bool()) + return std::make_unique(); +#endif + + if (settings["video"]["cursor"].String() == "hardware") + return std::make_unique(); + + assert(settings["video"]["cursor"].String() == "software"); + return std::make_unique(); +} + +CursorHandler::CursorHandler() + : cursor(createCursor()) + , frameTime(0.f) + , showing(false) + , pos(0,0) +{ + + type = Cursor::Type::DEFAULT; + dndObject = nullptr; + + cursors = + { + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRADVNTR")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRDEFLT")), + GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL")) + }; + + for (auto & cursor : cursors) + cursor->preload(); + + set(Cursor::Map::POINTER); +} + +CursorHandler::~CursorHandler() = default; + +void CursorHandler::changeGraphic(Cursor::Type type, size_t index) +{ + assert(dndObject == nullptr); + + if (type == this->type && index == this->frame) + return; + + this->type = type; + this->frame = index; + + cursor->setImage(getCurrentImage(), getPivotOffset()); +} + +void CursorHandler::set(Cursor::Default index) +{ + changeGraphic(Cursor::Type::DEFAULT, static_cast(index)); +} + +void CursorHandler::set(Cursor::Map index) +{ + changeGraphic(Cursor::Type::ADVENTURE, static_cast(index)); +} + +void CursorHandler::set(Cursor::Combat index) +{ + changeGraphic(Cursor::Type::COMBAT, static_cast(index)); +} + +void CursorHandler::set(Cursor::Spellcast index) +{ + //Note: this is animated cursor, ignore specified frame and only change type + changeGraphic(Cursor::Type::SPELLBOOK, frame); +} + +void CursorHandler::dragAndDropCursor(std::shared_ptr image) +{ + dndObject = image; + cursor->setImage(getCurrentImage(), getPivotOffset()); +} + +void CursorHandler::dragAndDropCursor (const AnimationPath & path, size_t index) +{ + auto anim = GH.renderHandler().loadAnimation(path); + anim->load(index); + dragAndDropCursor(anim->getImage(index)); +} + +void CursorHandler::cursorMove(const int & x, const int & y) +{ + pos.x = x; + pos.y = y; + + cursor->setCursorPosition(pos); +} + +Point CursorHandler::getPivotOffsetDefault(size_t index) +{ + return {0, 0}; +} + +Point CursorHandler::getPivotOffsetMap(size_t index) +{ + static const std::array offsets = {{ + { 0, 0}, // POINTER = 0, + { 0, 0}, // HOURGLASS = 1, + { 12, 10}, // HERO = 2, + { 12, 12}, // TOWN = 3, + + { 15, 13}, // T1_MOVE = 4, + { 13, 13}, // T1_ATTACK = 5, + { 16, 32}, // T1_SAIL = 6, + { 13, 20}, // T1_DISEMBARK = 7, + { 8, 9}, // T1_EXCHANGE = 8, + { 14, 16}, // T1_VISIT = 9, + + { 15, 13}, // T2_MOVE = 10, + { 13, 13}, // T2_ATTACK = 11, + { 16, 32}, // T2_SAIL = 12, + { 13, 20}, // T2_DISEMBARK = 13, + { 8, 9}, // T2_EXCHANGE = 14, + { 14, 16}, // T2_VISIT = 15, + + { 15, 13}, // T3_MOVE = 16, + { 13, 13}, // T3_ATTACK = 17, + { 16, 32}, // T3_SAIL = 18, + { 13, 20}, // T3_DISEMBARK = 19, + { 8, 9}, // T3_EXCHANGE = 20, + { 14, 16}, // T3_VISIT = 21, + + { 15, 13}, // T4_MOVE = 22, + { 13, 13}, // T4_ATTACK = 23, + { 16, 32}, // T4_SAIL = 24, + { 13, 20}, // T4_DISEMBARK = 25, + { 8, 9}, // T4_EXCHANGE = 26, + { 14, 16}, // T4_VISIT = 27, + + { 16, 32}, // T1_SAIL_VISIT = 28, + { 16, 32}, // T2_SAIL_VISIT = 29, + { 16, 32}, // T3_SAIL_VISIT = 30, + { 16, 32}, // T4_SAIL_VISIT = 31, + + { 6, 1}, // SCROLL_NORTH = 32, + { 16, 2}, // SCROLL_NORTHEAST = 33, + { 21, 6}, // SCROLL_EAST = 34, + { 16, 16}, // SCROLL_SOUTHEAST = 35, + { 6, 21}, // SCROLL_SOUTH = 36, + { 1, 16}, // SCROLL_SOUTHWEST = 37, + { 1, 5}, // SCROLL_WEST = 38, + { 2, 1}, // SCROLL_NORTHWEST = 39, + + { 0, 0}, // POINTER_COPY = 40, + { 14, 16}, // TELEPORT = 41, + { 20, 20}, // SCUTTLE_BOAT = 42 + }}; + + assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor + assert(index < offsets.size()); + return offsets[index]; +} + +Point CursorHandler::getPivotOffsetCombat(size_t index) +{ + static const std::array offsets = {{ + { 12, 12 }, // BLOCKED = 0, + { 10, 14 }, // MOVE = 1, + { 14, 14 }, // FLY = 2, + { 12, 12 }, // SHOOT = 3, + { 12, 12 }, // HERO = 4, + { 8, 12 }, // QUERY = 5, + { 0, 0 }, // POINTER = 6, + { 21, 0 }, // HIT_NORTHEAST = 7, + { 31, 5 }, // HIT_EAST = 8, + { 21, 21 }, // HIT_SOUTHEAST = 9, + { 0, 21 }, // HIT_SOUTHWEST = 10, + { 0, 5 }, // HIT_WEST = 11, + { 0, 0 }, // HIT_NORTHWEST = 12, + { 6, 0 }, // HIT_NORTH = 13, + { 6, 31 }, // HIT_SOUTH = 14, + { 14, 0 }, // SHOOT_PENALTY = 15, + { 12, 12 }, // SHOOT_CATAPULT = 16, + { 12, 12 }, // HEAL = 17, + { 12, 12 }, // SACRIFICE = 18, + { 14, 20 }, // TELEPORT = 19 + }}; + + assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor + assert(index < offsets.size()); + return offsets[index]; +} + +Point CursorHandler::getPivotOffsetSpellcast() +{ + return { 18, 28}; +} + +Point CursorHandler::getPivotOffset() +{ + if (dndObject) + return dndObject->dimensions() / 2; + + switch (type) { + case Cursor::Type::ADVENTURE: return getPivotOffsetMap(frame); + case Cursor::Type::COMBAT: return getPivotOffsetCombat(frame); + case Cursor::Type::DEFAULT: return getPivotOffsetDefault(frame); + case Cursor::Type::SPELLBOOK: return getPivotOffsetSpellcast(); + }; + + assert(0); + return {0, 0}; +} + +std::shared_ptr CursorHandler::getCurrentImage() +{ + if (dndObject) + return dndObject; + + return cursors[static_cast(type)]->getImage(frame); +} + +void CursorHandler::updateSpellcastCursor() +{ + static const float frameDisplayDuration = 0.1f; // H3 uses 100 ms per frame + + frameTime += GH.framerate().getElapsedMilliseconds() / 1000.f; + size_t newFrame = frame; + + while (frameTime >= frameDisplayDuration) + { + frameTime -= frameDisplayDuration; + newFrame++; + } + + auto & animation = cursors.at(static_cast(type)); + + while (newFrame >= animation->size()) + newFrame -= animation->size(); + + changeGraphic(Cursor::Type::SPELLBOOK, newFrame); +} + +void CursorHandler::render() +{ + if(!showing) + return; + + if (type == Cursor::Type::SPELLBOOK) + updateSpellcastCursor(); + + cursor->render(); +} + +void CursorHandler::hide() +{ + if (!showing) + return; + + showing = false; + cursor->setVisible(false); +} + +void CursorHandler::show() +{ + if (showing) + return; + + showing = true; + cursor->setVisible(true); +} + diff --git a/client/gui/CursorHandler.h b/client/gui/CursorHandler.h index 73c40339d..b1f3d4a27 100644 --- a/client/gui/CursorHandler.h +++ b/client/gui/CursorHandler.h @@ -1,182 +1,182 @@ -/* - * CCursorHandler.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/Point.h" -#include "../../lib/filesystem/ResourcePath.h" - -class ICursor; -class IImage; -class CAnimation; - -namespace Cursor -{ - enum class Type { - ADVENTURE, // set of various cursors for adventure map - COMBAT, // set of various cursors for combat - DEFAULT, // default arrow and hourglass cursors - SPELLBOOK // animated cursor for spellcasting - }; - - enum class Default { - POINTER = 0, - //ARROW_COPY = 1, // probably unused - HOURGLASS = 2, - }; - - enum class Combat { - BLOCKED = 0, - MOVE = 1, - FLY = 2, - SHOOT = 3, - HERO = 4, - QUERY = 5, - POINTER = 6, - HIT_NORTHEAST = 7, - HIT_EAST = 8, - HIT_SOUTHEAST = 9, - HIT_SOUTHWEST = 10, - HIT_WEST = 11, - HIT_NORTHWEST = 12, - HIT_NORTH = 13, - HIT_SOUTH = 14, - SHOOT_PENALTY = 15, - SHOOT_CATAPULT = 16, - HEAL = 17, - SACRIFICE = 18, - TELEPORT = 19, - - COUNT - }; - - enum class Map { - POINTER = 0, - HOURGLASS = 1, - HERO = 2, - TOWN = 3, - T1_MOVE = 4, - T1_ATTACK = 5, - T1_SAIL = 6, - T1_DISEMBARK = 7, - T1_EXCHANGE = 8, - T1_VISIT = 9, - T2_MOVE = 10, - T2_ATTACK = 11, - T2_SAIL = 12, - T2_DISEMBARK = 13, - T2_EXCHANGE = 14, - T2_VISIT = 15, - T3_MOVE = 16, - T3_ATTACK = 17, - T3_SAIL = 18, - T3_DISEMBARK = 19, - T3_EXCHANGE = 20, - T3_VISIT = 21, - T4_MOVE = 22, - T4_ATTACK = 23, - T4_SAIL = 24, - T4_DISEMBARK = 25, - T4_EXCHANGE = 26, - T4_VISIT = 27, - T1_SAIL_VISIT = 28, - T2_SAIL_VISIT = 29, - T3_SAIL_VISIT = 30, - T4_SAIL_VISIT = 31, - SCROLL_NORTH = 32, - SCROLL_NORTHEAST = 33, - SCROLL_EAST = 34, - SCROLL_SOUTHEAST = 35, - SCROLL_SOUTH = 36, - SCROLL_SOUTHWEST = 37, - SCROLL_WEST = 38, - SCROLL_NORTHWEST = 39, - //POINTER_COPY = 40, // probably unused - TELEPORT = 41, - SCUTTLE_BOAT = 42, - - COUNT - }; - - enum class Spellcast { - SPELL = 0, - }; -} - -/// handles mouse cursor -class CursorHandler final -{ - std::shared_ptr dndObject; //if set, overrides currentCursor - - std::array, 4> cursors; - - bool showing; - - /// Current cursor - Cursor::Type type; - size_t frame; - float frameTime; - Point pos; - - void changeGraphic(Cursor::Type type, size_t index); - - Point getPivotOffset(); - - void updateSpellcastCursor(); - - std::shared_ptr getCurrentImage(); - - std::unique_ptr cursor; - - static std::unique_ptr createCursor(); -public: - CursorHandler(); - ~CursorHandler(); - - /// Replaces the cursor with a custom image. - /// @param image Image to replace cursor with or nullptr to use the normal cursor. - void dragAndDropCursor(std::shared_ptr image); - - void dragAndDropCursor(const AnimationPath & path, size_t index); - - /// Changes cursor to specified index - void set(Cursor::Default index); - void set(Cursor::Map index); - void set(Cursor::Combat index); - void set(Cursor::Spellcast index); - - /// Returns current index of cursor - template - std::optional get() - { - bool typeValid = true; - - typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; - typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; - typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; - - if (typeValid) - return static_cast(frame); - return std::nullopt; - } - - Point getPivotOffsetSpellcast(); - Point getPivotOffsetDefault(size_t index); - Point getPivotOffsetMap(size_t index); - Point getPivotOffsetCombat(size_t index); - - void render(); - - void hide(); - void show(); - - /// change cursor's positions to (x, y) - void cursorMove(const int & x, const int & y); -}; +/* + * CCursorHandler.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/Point.h" +#include "../../lib/filesystem/ResourcePath.h" + +class ICursor; +class IImage; +class CAnimation; + +namespace Cursor +{ + enum class Type { + ADVENTURE, // set of various cursors for adventure map + COMBAT, // set of various cursors for combat + DEFAULT, // default arrow and hourglass cursors + SPELLBOOK // animated cursor for spellcasting + }; + + enum class Default { + POINTER = 0, + //ARROW_COPY = 1, // probably unused + HOURGLASS = 2, + }; + + enum class Combat { + BLOCKED = 0, + MOVE = 1, + FLY = 2, + SHOOT = 3, + HERO = 4, + QUERY = 5, + POINTER = 6, + HIT_NORTHEAST = 7, + HIT_EAST = 8, + HIT_SOUTHEAST = 9, + HIT_SOUTHWEST = 10, + HIT_WEST = 11, + HIT_NORTHWEST = 12, + HIT_NORTH = 13, + HIT_SOUTH = 14, + SHOOT_PENALTY = 15, + SHOOT_CATAPULT = 16, + HEAL = 17, + SACRIFICE = 18, + TELEPORT = 19, + + COUNT + }; + + enum class Map { + POINTER = 0, + HOURGLASS = 1, + HERO = 2, + TOWN = 3, + T1_MOVE = 4, + T1_ATTACK = 5, + T1_SAIL = 6, + T1_DISEMBARK = 7, + T1_EXCHANGE = 8, + T1_VISIT = 9, + T2_MOVE = 10, + T2_ATTACK = 11, + T2_SAIL = 12, + T2_DISEMBARK = 13, + T2_EXCHANGE = 14, + T2_VISIT = 15, + T3_MOVE = 16, + T3_ATTACK = 17, + T3_SAIL = 18, + T3_DISEMBARK = 19, + T3_EXCHANGE = 20, + T3_VISIT = 21, + T4_MOVE = 22, + T4_ATTACK = 23, + T4_SAIL = 24, + T4_DISEMBARK = 25, + T4_EXCHANGE = 26, + T4_VISIT = 27, + T1_SAIL_VISIT = 28, + T2_SAIL_VISIT = 29, + T3_SAIL_VISIT = 30, + T4_SAIL_VISIT = 31, + SCROLL_NORTH = 32, + SCROLL_NORTHEAST = 33, + SCROLL_EAST = 34, + SCROLL_SOUTHEAST = 35, + SCROLL_SOUTH = 36, + SCROLL_SOUTHWEST = 37, + SCROLL_WEST = 38, + SCROLL_NORTHWEST = 39, + //POINTER_COPY = 40, // probably unused + TELEPORT = 41, + SCUTTLE_BOAT = 42, + + COUNT + }; + + enum class Spellcast { + SPELL = 0, + }; +} + +/// handles mouse cursor +class CursorHandler final +{ + std::shared_ptr dndObject; //if set, overrides currentCursor + + std::array, 4> cursors; + + bool showing; + + /// Current cursor + Cursor::Type type; + size_t frame; + float frameTime; + Point pos; + + void changeGraphic(Cursor::Type type, size_t index); + + Point getPivotOffset(); + + void updateSpellcastCursor(); + + std::shared_ptr getCurrentImage(); + + std::unique_ptr cursor; + + static std::unique_ptr createCursor(); +public: + CursorHandler(); + ~CursorHandler(); + + /// Replaces the cursor with a custom image. + /// @param image Image to replace cursor with or nullptr to use the normal cursor. + void dragAndDropCursor(std::shared_ptr image); + + void dragAndDropCursor(const AnimationPath & path, size_t index); + + /// Changes cursor to specified index + void set(Cursor::Default index); + void set(Cursor::Map index); + void set(Cursor::Combat index); + void set(Cursor::Spellcast index); + + /// Returns current index of cursor + template + std::optional get() + { + bool typeValid = true; + + typeValid &= (std::is_same::value )|| type != Cursor::Type::DEFAULT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::ADVENTURE; + typeValid &= (std::is_same::value )|| type != Cursor::Type::COMBAT; + typeValid &= (std::is_same::value )|| type != Cursor::Type::SPELLBOOK; + + if (typeValid) + return static_cast(frame); + return std::nullopt; + } + + Point getPivotOffsetSpellcast(); + Point getPivotOffsetDefault(size_t index); + Point getPivotOffsetMap(size_t index); + Point getPivotOffsetCombat(size_t index); + + void render(); + + void hide(); + void show(); + + /// change cursor's positions to (x, y) + void cursorMove(const int & x, const int & y); +}; diff --git a/client/gui/MouseButton.h b/client/gui/MouseButton.h index 74ca41807..84c8ea188 100644 --- a/client/gui/MouseButton.h +++ b/client/gui/MouseButton.h @@ -1,19 +1,19 @@ -/* - * MouseButton.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 - -enum class MouseButton -{ - LEFT = 1, - MIDDLE = 2, - RIGHT = 3, - EXTRA1 = 4, - EXTRA2 = 5 -}; +/* + * MouseButton.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 + +enum class MouseButton +{ + LEFT = 1, + MIDDLE = 2, + RIGHT = 3, + EXTRA1 = 4, + EXTRA2 = 5 +}; diff --git a/client/gui/TextAlignment.h b/client/gui/TextAlignment.h index 5c716ba31..ec74cc5a6 100644 --- a/client/gui/TextAlignment.h +++ b/client/gui/TextAlignment.h @@ -1,12 +1,12 @@ -/* - * TextAlignment.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 - -enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT}; +/* + * TextAlignment.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 + +enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT}; diff --git a/client/mapView/IMapRendererContext.h b/client/mapView/IMapRendererContext.h index 049204c08..1fab9394d 100644 --- a/client/mapView/IMapRendererContext.h +++ b/client/mapView/IMapRendererContext.h @@ -1,93 +1,93 @@ -/* - * IMapRendererContext.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class int3; -class Point; -class CGObjectInstance; -class ObjectInstanceID; -struct TerrainTile; -struct CGPath; - -VCMI_LIB_NAMESPACE_END - -class IMapRendererContext -{ -public: - using MapObject = ObjectInstanceID; - using MapObjectsList = std::vector; - - virtual ~IMapRendererContext() = default; - - /// returns dimensions of current map - virtual int3 getMapSize() const = 0; - - /// returns true if chosen coordinates exist on map - virtual bool isInMap(const int3 & coordinates) const = 0; - - /// returns true if selected tile has animation and should not be cached - virtual bool tileAnimated(const int3 & coordinates) const = 0; - - /// returns tile by selected coordinates. Coordinates MUST be valid - virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0; - - /// returns all objects visible on specified tile - virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0; - - /// returns specific object by ID, or nullptr if not found - virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0; - - /// returns path of currently active hero, or nullptr if none - virtual const CGPath * currentPath() const = 0; - - /// returns true if specified tile is visible in current context - virtual bool isVisible(const int3 & coordinates) const = 0; - - /// returns true if specified object is the currently active hero - virtual bool isActiveHero(const CGObjectInstance* obj) const = 0; - - virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0; - virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0; - - /// returns object animation transparency. IF set to 0, object will not be visible - virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0; - - /// returns animation frame for selected object - virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0; - - /// returns index of image for overlay on specific tile, or numeric_limits::max if none - virtual size_t overlayImageIndex(const int3 & coordinates) const = 0; - - /// returns animation frame for terrain - virtual size_t terrainImageIndex(size_t groupSize) const = 0; - - virtual double viewTransitionProgress() const = 0; - - /// if true, rendered images will be converted to grayscale - virtual bool filterGrayscale() const = 0; - - virtual bool showRoads() const = 0; - virtual bool showRivers() const = 0; - virtual bool showBorder() const = 0; - - /// if true, world view overlay will be shown - virtual bool showOverlay() const = 0; - - /// if true, map grid should be visible on map - virtual bool showGrid() const = 0; - virtual bool showVisitable() const = 0; - virtual bool showBlocked() const = 0; - - /// if true, spell range for teleport / scuttle boat will be visible - virtual bool showSpellRange(const int3 & position) const = 0; - -}; +/* + * IMapRendererContext.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class int3; +class Point; +class CGObjectInstance; +class ObjectInstanceID; +struct TerrainTile; +struct CGPath; + +VCMI_LIB_NAMESPACE_END + +class IMapRendererContext +{ +public: + using MapObject = ObjectInstanceID; + using MapObjectsList = std::vector; + + virtual ~IMapRendererContext() = default; + + /// returns dimensions of current map + virtual int3 getMapSize() const = 0; + + /// returns true if chosen coordinates exist on map + virtual bool isInMap(const int3 & coordinates) const = 0; + + /// returns true if selected tile has animation and should not be cached + virtual bool tileAnimated(const int3 & coordinates) const = 0; + + /// returns tile by selected coordinates. Coordinates MUST be valid + virtual const TerrainTile & getMapTile(const int3 & coordinates) const = 0; + + /// returns all objects visible on specified tile + virtual const MapObjectsList & getObjects(const int3 & coordinates) const = 0; + + /// returns specific object by ID, or nullptr if not found + virtual const CGObjectInstance * getObject(ObjectInstanceID objectID) const = 0; + + /// returns path of currently active hero, or nullptr if none + virtual const CGPath * currentPath() const = 0; + + /// returns true if specified tile is visible in current context + virtual bool isVisible(const int3 & coordinates) const = 0; + + /// returns true if specified object is the currently active hero + virtual bool isActiveHero(const CGObjectInstance* obj) const = 0; + + virtual size_t objectGroupIndex(ObjectInstanceID objectID) const = 0; + virtual Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const = 0; + + /// returns object animation transparency. IF set to 0, object will not be visible + virtual double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const = 0; + + /// returns animation frame for selected object + virtual size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const = 0; + + /// returns index of image for overlay on specific tile, or numeric_limits::max if none + virtual size_t overlayImageIndex(const int3 & coordinates) const = 0; + + /// returns animation frame for terrain + virtual size_t terrainImageIndex(size_t groupSize) const = 0; + + virtual double viewTransitionProgress() const = 0; + + /// if true, rendered images will be converted to grayscale + virtual bool filterGrayscale() const = 0; + + virtual bool showRoads() const = 0; + virtual bool showRivers() const = 0; + virtual bool showBorder() const = 0; + + /// if true, world view overlay will be shown + virtual bool showOverlay() const = 0; + + /// if true, map grid should be visible on map + virtual bool showGrid() const = 0; + virtual bool showVisitable() const = 0; + virtual bool showBlocked() const = 0; + + /// if true, spell range for teleport / scuttle boat will be visible + virtual bool showSpellRange(const int3 & position) const = 0; + +}; diff --git a/client/mapView/IMapRendererObserver.h b/client/mapView/IMapRendererObserver.h index f7ba8d5f3..d1df5d9f1 100644 --- a/client/mapView/IMapRendererObserver.h +++ b/client/mapView/IMapRendererObserver.h @@ -1,53 +1,53 @@ -/* - * IMapRendererObserver.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class int3; -class CGObjectInstance; -class CGHeroInstance; -class PlayerColor; - -VCMI_LIB_NAMESPACE_END - -class IMapObjectObserver -{ -public: - IMapObjectObserver(); - virtual ~IMapObjectObserver(); - - virtual bool hasOngoingAnimations() = 0; - - /// Plays fade-in animation and adds object to map - virtual void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - - /// Plays fade-out animation and removed object from map - virtual void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - - /// Adds object to map instantly, with no animation - virtual void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - - /// Removes object from map instantly, with no animation - virtual void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - - /// Perform hero movement animation, moving hero across terrain - virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - /// Perform initialization of hero teleportation animation with terrain fade animation - virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - - virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; - virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; -}; +/* + * IMapRendererObserver.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class int3; +class CGObjectInstance; +class CGHeroInstance; +class PlayerColor; + +VCMI_LIB_NAMESPACE_END + +class IMapObjectObserver +{ +public: + IMapObjectObserver(); + virtual ~IMapObjectObserver(); + + virtual bool hasOngoingAnimations() = 0; + + /// Plays fade-in animation and adds object to map + virtual void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Plays fade-out animation and removed object from map + virtual void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Adds object to map instantly, with no animation + virtual void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Removes object from map instantly, with no animation + virtual void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + + /// Perform hero movement animation, moving hero across terrain + virtual void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + /// Perform initialization of hero teleportation animation with terrain fade animation + virtual void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + virtual void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + + virtual void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; + virtual void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) = 0; +}; diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index a97c03c85..6a3947ce9 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -1,789 +1,789 @@ -/* - * MapRenderer.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 "MapRenderer.h" - -#include "IMapRendererContext.h" -#include "mapHandler.h" - -#include "../CGameInfo.h" -#include "../gui/CGuiHandler.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../render/Colors.h" - -#include "../../CCallback.h" - -#include "../../lib/RiverHandler.h" -#include "../../lib/RoadHandler.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/mapping/CMapDefines.h" -#include "../../lib/pathfinder/CGPathNode.h" - -struct NeighborTilesInfo -{ - //567 - //3 4 - //012 - std::bitset<8> d; - - NeighborTilesInfo(IMapRendererContext & context, const int3 & pos) - { - auto checkTile = [&](int dx, int dy) - { - return context.isVisible(pos + int3(dx, dy, 0)); - }; - - // sides - d[1] = checkTile(0, +1); - d[3] = checkTile(-1, 0); - d[4] = checkTile(+1, 0); - d[6] = checkTile(0, -1); - - // corners - select visible image if either corner or adjacent sides are visible - d[0] = d[1] || d[3] || checkTile(-1, +1); - d[2] = d[1] || d[4] || checkTile(+1, +1); - d[5] = d[3] || d[6] || checkTile(-1, -1); - d[7] = d[4] || d[6] || checkTile(+1, -1); - } - - bool areAllHidden() const - { - return d.none(); - } - - int getBitmapID() const - { - //NOTE: some images have unused in VCMI pair (same blockmap but a bit different look) - // 0-1, 2-3, 4-5, 11-13, 12-14 - static const int visBitmaps[256] = { - -1, 34, 4, 4, 22, 23, 4, 4, 36, 36, 38, 38, 47, 47, 38, 38, //16 - 3, 25, 12, 12, 3, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //32 - 35, 39, 48, 48, 41, 43, 48, 48, 36, 36, 38, 38, 47, 47, 38, 38, //48 - 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //64 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //80 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //96 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //112 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //128 - 15, 17, 30, 30, 16, 19, 30, 30, 46, 46, 40, 40, 32, 32, 40, 40, //144 - 2, 25, 12, 12, 2, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //160 - 18, 42, 31, 31, 20, 21, 31, 31, 46, 46, 40, 40, 32, 32, 40, 40, //176 - 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //192 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //208 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //224 - 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //240 - 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10 //256 - }; - - return visBitmaps[d.to_ulong()]; // >=0 -> partial hide, <0 - full hide - } -}; - -MapTileStorage::MapTileStorage(size_t capacity) - : animations(capacity) -{ -} - -void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode) -{ - auto & terrainAnimations = animations[index]; - - for(auto & entry : terrainAnimations) - { - if (!filename.empty()) - { - entry = GH.renderHandler().loadAnimation(filename); - entry->preload(); - } - else - entry = GH.renderHandler().createAnimation(); - - for(size_t i = 0; i < entry->size(); ++i) - entry->getImage(i)->setBlitMode(blitMode); - } - - for(size_t i = 0; i < terrainAnimations[0]->size(); ++i) - { - terrainAnimations[1]->getImage(i)->verticalFlip(); - terrainAnimations[3]->getImage(i)->verticalFlip(); - - terrainAnimations[2]->getImage(i)->horizontalFlip(); - terrainAnimations[3]->getImage(i)->horizontalFlip(); - } -} - -std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) -{ - const auto & animation = animations[fileIndex][rotationIndex]; - return animation->getImage(imageIndex); -} - -MapRendererTerrain::MapRendererTerrain() - : storage(VLC->terrainTypeHandler->objects.size()) -{ - for(const auto & terrain : VLC->terrainTypeHandler->objects) - storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); -} - -void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - int32_t terrainIndex = mapTile.terType->getIndex(); - int32_t imageIndex = mapTile.terView; - int32_t rotationIndex = mapTile.extTileFlags % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - - assert(image); - if (!image) - { - logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.terType->getNameTranslated(), coordinates.toString()); - return; - } - - for( auto const & element : mapTile.terType->paletteAnimation) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); - - target.draw(image, Point(0, 0)); -} - -uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(!mapTile.terType->paletteAnimation.empty()) - return context.terrainImageIndex(250); - return 0xff - 1; -} - -MapRendererRiver::MapRendererRiver() - : storage(VLC->riverTypeHandler->objects.size()) -{ - for(const auto & river : VLC->riverTypeHandler->objects) - storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); -} - -void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(mapTile.riverType->getId() == River::NO_RIVER) - return; - - int32_t terrainIndex = mapTile.riverType->getIndex(); - int32_t imageIndex = mapTile.riverDir; - int32_t rotationIndex = (mapTile.extTileFlags >> 2) % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - - for( auto const & element : mapTile.riverType->paletteAnimation) - image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); - - target.draw(image, Point(0, 0)); -} - -uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const TerrainTile & mapTile = context.getMapTile(coordinates); - - if(!mapTile.riverType->paletteAnimation.empty()) - return context.terrainImageIndex(250); - return 0xff-1; -} - -MapRendererRoad::MapRendererRoad() - : storage(VLC->roadTypeHandler->objects.size()) -{ - for(const auto & road : VLC->roadTypeHandler->objects) - storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); -} - -void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - const int3 coordinatesAbove = coordinates - int3(0, 1, 0); - - if(context.isInMap(coordinatesAbove)) - { - const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove); - if(mapTileAbove.roadType->getId() != Road::NO_ROAD) - { - int32_t terrainIndex = mapTileAbove.roadType->getIndex(); - int32_t imageIndex = mapTileAbove.roadDir; - int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - target.draw(image, Point(0, 0), Rect(0, 16, 32, 16)); - } - } - - const TerrainTile & mapTile = context.getMapTile(coordinates); - if(mapTile.roadType->getId() != Road::NO_ROAD) - { - int32_t terrainIndex = mapTile.roadType->getIndex(); - int32_t imageIndex = mapTile.roadDir; - int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4; - - const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); - target.draw(image, Point(0, 16), Rect(0, 0, 32, 16)); - } -} - -uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return 0; -} - -MapRendererBorder::MapRendererBorder() -{ - animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG")); - animation->preload(); -} - -size_t MapRendererBorder::getIndexForTile(IMapRendererContext & context, const int3 & tile) -{ - assert(!context.isInMap(tile)); - - int3 size = context.getMapSize(); - - if(tile.x < -1 || tile.x > size.x || tile.y < -1 || tile.y > size.y) - return std::abs(tile.x) % 4 + 4 * (std::abs(tile.y) % 4); - - if(tile.x == -1 && tile.y == -1) - return 16; - - if(tile.x == size.x && tile.y == -1) - return 17; - - if(tile.x == size.x && tile.y == size.y) - return 18; - - if(tile.x == -1 && tile.y == size.y) - return 19; - - if(tile.y == -1) - return 20 + (tile.x % 4); - - if(tile.x == size.x) - return 24 + (tile.y % 4); - - if(tile.y == size.y) - return 28 + (tile.x % 4); - - if(tile.x == -1) - return 32 + (tile.y % 4); - - //else - visible area, no renderable border - assert(0); - return 0; -} - -void MapRendererBorder::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if (context.showBorder()) - { - const auto & image = animation->getImage(getIndexForTile(context, coordinates)); - target.draw(image, Point(0, 0)); - } - else - { - target.drawColor(Rect(0,0,32,32), Colors::BLACK); - } -} - -uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return 0; -} - -MapRendererFow::MapRendererFow() -{ - fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC")); - fogOfWarFullHide->preload(); - fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE")); - fogOfWarPartialHide->preload(); - - for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) - fogOfWarFullHide->getImage(i)->setBlitMode(EImageBlitMode::OPAQUE); - - static const std::vector rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; - - size_t size = fogOfWarPartialHide->size(0); //group size after next rotation - - for(const int rotation : rotations) - { - fogOfWarPartialHide->duplicateImage(0, rotation, 0); - auto image = fogOfWarPartialHide->getImage(size, 0); - image->verticalFlip(); - size++; - } -} - -void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - assert(!context.isVisible(coordinates)); - - const NeighborTilesInfo neighborInfo(context, coordinates); - - int retBitmapID = neighborInfo.getBitmapID(); // >=0 -> partial hide, <0 - full hide - if(retBitmapID < 0) - { - // generate a number that is predefined for each tile, - // but appears random to break visible pattern in large areas of fow - // current approach (use primes as magic numbers for formula) looks to be suitable - size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101; - size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size(); - - target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0)); - } - else - { - target.draw(fogOfWarPartialHide->getImage(retBitmapID), Point(0, 0)); - } -} - -uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - const NeighborTilesInfo neighborInfo(context, coordinates); - int retBitmapID = neighborInfo.getBitmapID(); - if(retBitmapID < 0) - return 0xff - 1; - return retBitmapID; -} - -std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectInstance* obj) -{ - const auto & info = obj->appearance; - - //the only(?) invisible object - if(info->id == Obj::EVENT) - return std::shared_ptr(); - - if(info->animationFile.empty()) - { - logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid); - return std::shared_ptr(); - } - - bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO); - - // Boat appearance files only contain single, unanimated image - // proper boat animations are actually in different file - if (info->id == Obj::BOAT) - if(auto boat = dynamic_cast(obj); boat && !boat->actualAnimation.empty()) - return getAnimation(boat->actualAnimation, generateMovementGroups); - - return getAnimation(info->animationFile, generateMovementGroups); -} - -std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups) -{ - auto it = animations.find(filename); - - if(it != animations.end()) - return it->second; - - auto ret = GH.renderHandler().loadAnimation(filename); - animations[filename] = ret; - ret->preload(); - - if(generateMovementGroups) - { - ret->createFlippedGroup(1, 13); - ret->createFlippedGroup(2, 14); - ret->createFlippedGroup(3, 15); - - ret->createFlippedGroup(6, 10); - ret->createFlippedGroup(7, 11); - ret->createFlippedGroup(8, 12); - } - return ret; -} - -std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectInstance* obj) -{ - //TODO: relocate to config file? - static const std::vector heroFlags = { - "AF00", "AF01", "AF02", "AF03", "AF04", "AF05", "AF06", "AF07" - }; - - if(obj->ID == Obj::HERO) - { - assert(dynamic_cast(obj) != nullptr); - assert(obj->tempOwner.isValidPlayer()); - return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true); - } - - if(obj->ID == Obj::BOAT) - { - const auto * boat = dynamic_cast(obj); - if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty()) - return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true); - } - - return nullptr; -} - -std::shared_ptr MapRendererObjects::getOverlayAnimation(const CGObjectInstance * obj) -{ - if(obj->ID == Obj::BOAT) - { - // Boats have additional animation with waves around boat - const auto * boat = dynamic_cast(obj); - if(boat && boat->hero && !boat->overlayAnimation.empty()) - return getAnimation(boat->overlayAnimation, true); - } - return nullptr; -} - -std::shared_ptr MapRendererObjects::getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr& animation) const -{ - if(!animation) - return nullptr; - - size_t groupIndex = context.objectGroupIndex(obj->id); - - if(animation->size(groupIndex) == 0) - return nullptr; - - size_t frameIndex = context.objectImageIndex(obj->id, animation->size(groupIndex)); - - return animation->getImage(frameIndex, groupIndex); -} - -void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr& image) -{ - if(!image) - return; - - auto transparency = static_cast(std::round(255 * context.objectTransparency(object->id, coordinates))); - - if (transparency == 0) - return; - - image->setAlpha(transparency); - image->setFlagColor(object->tempOwner); - - Point offsetPixels = context.objectImageOffset(object->id, coordinates); - - if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) - { - Point imagePos = image->dimensions() - offsetPixels - Point(32, 32); - - //if (transparency == 255) - //{ - // Canvas intermediate(Point(32,32)); - // intermediate.enableTransparency(true); - // image->setBlitMode(EImageBlitMode::OPAQUE); - // intermediate.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); - // target.draw(intermediate, Point(0,0)); - // return; - //} - target.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); - } -} - -void MapRendererObjects::renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * instance) -{ - renderImage(context, target, coordinates, instance, getImage(context, instance, getBaseAnimation(instance))); - renderImage(context, target, coordinates, instance, getImage(context, instance, getFlagAnimation(instance))); - renderImage(context, target, coordinates, instance, getImage(context, instance, getOverlayAnimation(instance))); -} - -void MapRendererObjects::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * objectInstance = context.getObject(objectID); - - assert(objectInstance); - if(!objectInstance) - { - logGlobal->error("Stray map object that isn't fading"); - continue; - } - - renderObject(context, target, coordinates, objectInstance); - } -} - -uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * objectInstance = context.getObject(objectID); - - assert(objectInstance); - if(!objectInstance) - { - logGlobal->error("Stray map object that isn't fading"); - continue; - } - - size_t groupIndex = context.objectGroupIndex(objectInstance->id); - Point offsetPixels = context.objectImageOffset(objectInstance->id, coordinates); - - auto base = getBaseAnimation(objectInstance); - auto flag = getFlagAnimation(objectInstance); - - if (base && base->size(groupIndex) > 1) - { - auto imageIndex = context.objectImageIndex(objectID, base->size(groupIndex)); - auto image = base->getImage(imageIndex, groupIndex); - if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) - return context.objectImageIndex(objectID, 250); - } - - if (flag && flag->size(groupIndex) > 1) - { - auto imageIndex = context.objectImageIndex(objectID, flag->size(groupIndex)); - auto image = flag->getImage(imageIndex, groupIndex); - if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) - return context.objectImageIndex(objectID, 250); - } - } - return 0xff-1; -} - -MapRendererOverlay::MapRendererOverlay() - : imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) - , imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) - , imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) - , imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) -{ - -} - -void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if(context.showGrid()) - target.draw(imageGrid, Point(0,0)); - - if(context.showVisitable() || context.showBlocked()) - { - bool blocking = false; - bool visitable = false; - - for(const auto & objectID : context.getObjects(coordinates)) - { - const auto * object = context.getObject(objectID); - - if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) - { - visitable |= object->visitableAt(coordinates.x, coordinates.y); - blocking |= object->blockingAt(coordinates.x, coordinates.y); - } - } - - if (context.showBlocked() && blocking) - target.draw(imageBlocked, Point(0,0)); - if (context.showVisitable() && visitable) - target.draw(imageVisitable, Point(0,0)); - } - - if (context.showSpellRange(coordinates)) - target.draw(imageSpellRange, Point(0,0)); -} - -uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - uint8_t result = 0; - - if (context.showVisitable()) - result += 1; - - if (context.showBlocked()) - result += 2; - - if (context.showGrid()) - result += 4; - - if (context.showSpellRange(coordinates)) - result += 8; - - return result; -} - -MapRendererPath::MapRendererPath() - : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"))) -{ - pathNodes->preload(); -} - -size_t MapRendererPath::selectImageReachability(bool reachableToday, size_t imageIndex) -{ - const static size_t unreachableTodayOffset = 25; - - if(!reachableToday) - return unreachableTodayOffset + imageIndex; - - return imageIndex; -} - -size_t MapRendererPath::selectImageCross(bool reachableToday, const int3 & curr) -{ - return selectImageReachability(reachableToday, 0); -} - -size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next) -{ - // Vector directions - // 0 1 2 - // | - // 3 - 4 - 5 - // | - // 6 7 8 - //For example: - // | - // +-> - // is (directionToArrowIndex[7][5]) - // - const static size_t directionToArrowIndex[9][9] = { - {16, 17, 18, 7, 0, 19, 6, 5, 0 }, - {8, 9, 18, 7, 0, 19, 6, 0, 20}, - {8, 1, 10, 7, 0, 19, 0, 21, 20}, - {24, 17, 18, 15, 0, 0, 6, 5, 4 }, - {0, 0, 0, 0, 0, 0, 0, 0, 0 }, - {8, 1, 2, 0, 0, 11, 22, 21, 20}, - {24, 17, 0, 23, 0, 3, 14, 5, 4 }, - {24, 0, 2, 23, 0, 3, 22, 13, 4 }, - {0, 1, 2, 23, 0, 3, 22, 21, 12} - }; - - size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1); - size_t leaveDirection = (prev.x - curr.x + 1) + 3 * (prev.y - curr.y + 1); - size_t imageIndex = directionToArrowIndex[enterDirection][leaveDirection]; - - return selectImageReachability(reachableToday, imageIndex); -} - -void MapRendererPath::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - size_t imageID = selectImage(context, coordinates); - - if (imageID < pathNodes->size()) - target.draw(pathNodes->getImage(imageID), Point(0,0)); -} - -size_t MapRendererPath::selectImage(IMapRendererContext & context, const int3 & coordinates) -{ - const auto & functor = [&](const CGPathNode & node) - { - return node.coord == coordinates; - }; - - const auto * path = context.currentPath(); - if(!path) - return std::numeric_limits::max(); - - const auto & iter = boost::range::find_if(path->nodes, functor); - - if(iter == path->nodes.end()) - return std::numeric_limits::max(); - - bool reachableToday = iter->turns == 0; - if(iter == path->nodes.begin()) - return selectImageCross(reachableToday, iter->coord); - - auto next = iter + 1; - auto prev = iter - 1; - - // start of path - current hero location - if(next == path->nodes.end()) - return std::numeric_limits::max(); - - bool pathContinuous = iter->coord.areNeighbours(next->coord) && iter->coord.areNeighbours(prev->coord); - bool embarking = iter->action == EPathNodeAction::EMBARK || iter->action == EPathNodeAction::DISEMBARK; - - if(pathContinuous && !embarking) - return selectImageArrow(reachableToday, iter->coord, prev->coord, next->coord); - - return selectImageCross(reachableToday, iter->coord); -} - -uint8_t MapRendererPath::checksum(IMapRendererContext & context, const int3 & coordinates) -{ - return selectImage(context, coordinates) & 0xff; -} - -MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & context, const int3 & coordinates) -{ - // computes basic checksum to determine whether tile needs an update - // if any component gives different value, tile will be updated - TileChecksum result; - boost::range::fill(result, std::numeric_limits::max()); - - if(!context.isInMap(coordinates)) - { - result[0] = rendererBorder.checksum(context, coordinates); - return result; - } - - const NeighborTilesInfo neighborInfo(context, coordinates); - - if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) - { - result[7] = rendererFow.checksum(context, coordinates); - } - else - { - result[1] = rendererTerrain.checksum(context, coordinates); - if (context.showRivers()) - result[2] = rendererRiver.checksum(context, coordinates); - if (context.showRoads()) - result[3] = rendererRoad.checksum(context, coordinates); - result[4] = rendererObjects.checksum(context, coordinates); - result[5] = rendererPath.checksum(context, coordinates); - result[6] = rendererOverlay.checksum(context, coordinates); - - if(!context.isVisible(coordinates)) - result[7] = rendererFow.checksum(context, coordinates); - } - return result; -} - -void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) -{ - if(!context.isInMap(coordinates)) - { - rendererBorder.renderTile(context, target, coordinates); - return; - } - - const NeighborTilesInfo neighborInfo(context, coordinates); - - if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) - { - rendererFow.renderTile(context, target, coordinates); - } - else - { - rendererTerrain.renderTile(context, target, coordinates); - - if (context.showRivers()) - rendererRiver.renderTile(context, target, coordinates); - - if (context.showRoads()) - rendererRoad.renderTile(context, target, coordinates); - - rendererObjects.renderTile(context, target, coordinates); - rendererPath.renderTile(context, target, coordinates); - rendererOverlay.renderTile(context, target, coordinates); - - if(!context.isVisible(coordinates)) - rendererFow.renderTile(context, target, coordinates); - } -} +/* + * MapRenderer.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 "MapRenderer.h" + +#include "IMapRendererContext.h" +#include "mapHandler.h" + +#include "../CGameInfo.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/Colors.h" + +#include "../../CCallback.h" + +#include "../../lib/RiverHandler.h" +#include "../../lib/RoadHandler.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMapDefines.h" +#include "../../lib/pathfinder/CGPathNode.h" + +struct NeighborTilesInfo +{ + //567 + //3 4 + //012 + std::bitset<8> d; + + NeighborTilesInfo(IMapRendererContext & context, const int3 & pos) + { + auto checkTile = [&](int dx, int dy) + { + return context.isVisible(pos + int3(dx, dy, 0)); + }; + + // sides + d[1] = checkTile(0, +1); + d[3] = checkTile(-1, 0); + d[4] = checkTile(+1, 0); + d[6] = checkTile(0, -1); + + // corners - select visible image if either corner or adjacent sides are visible + d[0] = d[1] || d[3] || checkTile(-1, +1); + d[2] = d[1] || d[4] || checkTile(+1, +1); + d[5] = d[3] || d[6] || checkTile(-1, -1); + d[7] = d[4] || d[6] || checkTile(+1, -1); + } + + bool areAllHidden() const + { + return d.none(); + } + + int getBitmapID() const + { + //NOTE: some images have unused in VCMI pair (same blockmap but a bit different look) + // 0-1, 2-3, 4-5, 11-13, 12-14 + static const int visBitmaps[256] = { + -1, 34, 4, 4, 22, 23, 4, 4, 36, 36, 38, 38, 47, 47, 38, 38, //16 + 3, 25, 12, 12, 3, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //32 + 35, 39, 48, 48, 41, 43, 48, 48, 36, 36, 38, 38, 47, 47, 38, 38, //48 + 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //64 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //80 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //96 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //112 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //128 + 15, 17, 30, 30, 16, 19, 30, 30, 46, 46, 40, 40, 32, 32, 40, 40, //144 + 2, 25, 12, 12, 2, 25, 12, 12, 9, 9, 6, 6, 9, 9, 6, 6, //160 + 18, 42, 31, 31, 20, 21, 31, 31, 46, 46, 40, 40, 32, 32, 40, 40, //176 + 26, 49, 28, 28, 26, 49, 28, 28, 9, 9, 6, 6, 9, 9, 6, 6, //192 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //208 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10, //224 + 0, 45, 29, 29, 24, 33, 29, 29, 37, 37, 7, 7, 50, 50, 7, 7, //240 + 13, 27, 44, 44, 13, 27, 44, 44, 8, 8, 10, 10, 8, 8, 10, 10 //256 + }; + + return visBitmaps[d.to_ulong()]; // >=0 -> partial hide, <0 - full hide + } +}; + +MapTileStorage::MapTileStorage(size_t capacity) + : animations(capacity) +{ +} + +void MapTileStorage::load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode) +{ + auto & terrainAnimations = animations[index]; + + for(auto & entry : terrainAnimations) + { + if (!filename.empty()) + { + entry = GH.renderHandler().loadAnimation(filename); + entry->preload(); + } + else + entry = GH.renderHandler().createAnimation(); + + for(size_t i = 0; i < entry->size(); ++i) + entry->getImage(i)->setBlitMode(blitMode); + } + + for(size_t i = 0; i < terrainAnimations[0]->size(); ++i) + { + terrainAnimations[1]->getImage(i)->verticalFlip(); + terrainAnimations[3]->getImage(i)->verticalFlip(); + + terrainAnimations[2]->getImage(i)->horizontalFlip(); + terrainAnimations[3]->getImage(i)->horizontalFlip(); + } +} + +std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIndex, size_t imageIndex) +{ + const auto & animation = animations[fileIndex][rotationIndex]; + return animation->getImage(imageIndex); +} + +MapRendererTerrain::MapRendererTerrain() + : storage(VLC->terrainTypeHandler->objects.size()) +{ + for(const auto & terrain : VLC->terrainTypeHandler->objects) + storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); +} + +void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + int32_t terrainIndex = mapTile.terType->getIndex(); + int32_t imageIndex = mapTile.terView; + int32_t rotationIndex = mapTile.extTileFlags % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + + assert(image); + if (!image) + { + logGlobal->error("Failed to find image %d for terrain %s on tile %s", imageIndex, mapTile.terType->getNameTranslated(), coordinates.toString()); + return; + } + + for( auto const & element : mapTile.terType->paletteAnimation) + image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); + + target.draw(image, Point(0, 0)); +} + +uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(!mapTile.terType->paletteAnimation.empty()) + return context.terrainImageIndex(250); + return 0xff - 1; +} + +MapRendererRiver::MapRendererRiver() + : storage(VLC->riverTypeHandler->objects.size()) +{ + for(const auto & river : VLC->riverTypeHandler->objects) + storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); +} + +void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(mapTile.riverType->getId() == River::NO_RIVER) + return; + + int32_t terrainIndex = mapTile.riverType->getIndex(); + int32_t imageIndex = mapTile.riverDir; + int32_t rotationIndex = (mapTile.extTileFlags >> 2) % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + + for( auto const & element : mapTile.riverType->paletteAnimation) + image->shiftPalette(element.start, element.length, context.terrainImageIndex(element.length)); + + target.draw(image, Point(0, 0)); +} + +uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const TerrainTile & mapTile = context.getMapTile(coordinates); + + if(!mapTile.riverType->paletteAnimation.empty()) + return context.terrainImageIndex(250); + return 0xff-1; +} + +MapRendererRoad::MapRendererRoad() + : storage(VLC->roadTypeHandler->objects.size()) +{ + for(const auto & road : VLC->roadTypeHandler->objects) + storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); +} + +void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + const int3 coordinatesAbove = coordinates - int3(0, 1, 0); + + if(context.isInMap(coordinatesAbove)) + { + const TerrainTile & mapTileAbove = context.getMapTile(coordinatesAbove); + if(mapTileAbove.roadType->getId() != Road::NO_ROAD) + { + int32_t terrainIndex = mapTileAbove.roadType->getIndex(); + int32_t imageIndex = mapTileAbove.roadDir; + int32_t rotationIndex = (mapTileAbove.extTileFlags >> 4) % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + target.draw(image, Point(0, 0), Rect(0, 16, 32, 16)); + } + } + + const TerrainTile & mapTile = context.getMapTile(coordinates); + if(mapTile.roadType->getId() != Road::NO_ROAD) + { + int32_t terrainIndex = mapTile.roadType->getIndex(); + int32_t imageIndex = mapTile.roadDir; + int32_t rotationIndex = (mapTile.extTileFlags >> 4) % 4; + + const auto & image = storage.find(terrainIndex, rotationIndex, imageIndex); + target.draw(image, Point(0, 16), Rect(0, 0, 32, 16)); + } +} + +uint8_t MapRendererRoad::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return 0; +} + +MapRendererBorder::MapRendererBorder() +{ + animation = GH.renderHandler().loadAnimation(AnimationPath::builtin("EDG")); + animation->preload(); +} + +size_t MapRendererBorder::getIndexForTile(IMapRendererContext & context, const int3 & tile) +{ + assert(!context.isInMap(tile)); + + int3 size = context.getMapSize(); + + if(tile.x < -1 || tile.x > size.x || tile.y < -1 || tile.y > size.y) + return std::abs(tile.x) % 4 + 4 * (std::abs(tile.y) % 4); + + if(tile.x == -1 && tile.y == -1) + return 16; + + if(tile.x == size.x && tile.y == -1) + return 17; + + if(tile.x == size.x && tile.y == size.y) + return 18; + + if(tile.x == -1 && tile.y == size.y) + return 19; + + if(tile.y == -1) + return 20 + (tile.x % 4); + + if(tile.x == size.x) + return 24 + (tile.y % 4); + + if(tile.y == size.y) + return 28 + (tile.x % 4); + + if(tile.x == -1) + return 32 + (tile.y % 4); + + //else - visible area, no renderable border + assert(0); + return 0; +} + +void MapRendererBorder::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if (context.showBorder()) + { + const auto & image = animation->getImage(getIndexForTile(context, coordinates)); + target.draw(image, Point(0, 0)); + } + else + { + target.drawColor(Rect(0,0,32,32), Colors::BLACK); + } +} + +uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return 0; +} + +MapRendererFow::MapRendererFow() +{ + fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC")); + fogOfWarFullHide->preload(); + fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE")); + fogOfWarPartialHide->preload(); + + for(size_t i = 0; i < fogOfWarFullHide->size(); ++i) + fogOfWarFullHide->getImage(i)->setBlitMode(EImageBlitMode::OPAQUE); + + static const std::vector rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27}; + + size_t size = fogOfWarPartialHide->size(0); //group size after next rotation + + for(const int rotation : rotations) + { + fogOfWarPartialHide->duplicateImage(0, rotation, 0); + auto image = fogOfWarPartialHide->getImage(size, 0); + image->verticalFlip(); + size++; + } +} + +void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + assert(!context.isVisible(coordinates)); + + const NeighborTilesInfo neighborInfo(context, coordinates); + + int retBitmapID = neighborInfo.getBitmapID(); // >=0 -> partial hide, <0 - full hide + if(retBitmapID < 0) + { + // generate a number that is predefined for each tile, + // but appears random to break visible pattern in large areas of fow + // current approach (use primes as magic numbers for formula) looks to be suitable + size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101; + size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size(); + + target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0)); + } + else + { + target.draw(fogOfWarPartialHide->getImage(retBitmapID), Point(0, 0)); + } +} + +uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + const NeighborTilesInfo neighborInfo(context, coordinates); + int retBitmapID = neighborInfo.getBitmapID(); + if(retBitmapID < 0) + return 0xff - 1; + return retBitmapID; +} + +std::shared_ptr MapRendererObjects::getBaseAnimation(const CGObjectInstance* obj) +{ + const auto & info = obj->appearance; + + //the only(?) invisible object + if(info->id == Obj::EVENT) + return std::shared_ptr(); + + if(info->animationFile.empty()) + { + logGlobal->warn("Def name for obj (%d,%d) is empty!", info->id, info->subid); + return std::shared_ptr(); + } + + bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO); + + // Boat appearance files only contain single, unanimated image + // proper boat animations are actually in different file + if (info->id == Obj::BOAT) + if(auto boat = dynamic_cast(obj); boat && !boat->actualAnimation.empty()) + return getAnimation(boat->actualAnimation, generateMovementGroups); + + return getAnimation(info->animationFile, generateMovementGroups); +} + +std::shared_ptr MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups) +{ + auto it = animations.find(filename); + + if(it != animations.end()) + return it->second; + + auto ret = GH.renderHandler().loadAnimation(filename); + animations[filename] = ret; + ret->preload(); + + if(generateMovementGroups) + { + ret->createFlippedGroup(1, 13); + ret->createFlippedGroup(2, 14); + ret->createFlippedGroup(3, 15); + + ret->createFlippedGroup(6, 10); + ret->createFlippedGroup(7, 11); + ret->createFlippedGroup(8, 12); + } + return ret; +} + +std::shared_ptr MapRendererObjects::getFlagAnimation(const CGObjectInstance* obj) +{ + //TODO: relocate to config file? + static const std::vector heroFlags = { + "AF00", "AF01", "AF02", "AF03", "AF04", "AF05", "AF06", "AF07" + }; + + if(obj->ID == Obj::HERO) + { + assert(dynamic_cast(obj) != nullptr); + assert(obj->tempOwner.isValidPlayer()); + return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true); + } + + if(obj->ID == Obj::BOAT) + { + const auto * boat = dynamic_cast(obj); + if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty()) + return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true); + } + + return nullptr; +} + +std::shared_ptr MapRendererObjects::getOverlayAnimation(const CGObjectInstance * obj) +{ + if(obj->ID == Obj::BOAT) + { + // Boats have additional animation with waves around boat + const auto * boat = dynamic_cast(obj); + if(boat && boat->hero && !boat->overlayAnimation.empty()) + return getAnimation(boat->overlayAnimation, true); + } + return nullptr; +} + +std::shared_ptr MapRendererObjects::getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr& animation) const +{ + if(!animation) + return nullptr; + + size_t groupIndex = context.objectGroupIndex(obj->id); + + if(animation->size(groupIndex) == 0) + return nullptr; + + size_t frameIndex = context.objectImageIndex(obj->id, animation->size(groupIndex)); + + return animation->getImage(frameIndex, groupIndex); +} + +void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr& image) +{ + if(!image) + return; + + auto transparency = static_cast(std::round(255 * context.objectTransparency(object->id, coordinates))); + + if (transparency == 0) + return; + + image->setAlpha(transparency); + image->setFlagColor(object->tempOwner); + + Point offsetPixels = context.objectImageOffset(object->id, coordinates); + + if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) + { + Point imagePos = image->dimensions() - offsetPixels - Point(32, 32); + + //if (transparency == 255) + //{ + // Canvas intermediate(Point(32,32)); + // intermediate.enableTransparency(true); + // image->setBlitMode(EImageBlitMode::OPAQUE); + // intermediate.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); + // target.draw(intermediate, Point(0,0)); + // return; + //} + target.draw(image, Point(0, 0), Rect(imagePos, Point(32,32))); + } +} + +void MapRendererObjects::renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * instance) +{ + renderImage(context, target, coordinates, instance, getImage(context, instance, getBaseAnimation(instance))); + renderImage(context, target, coordinates, instance, getImage(context, instance, getFlagAnimation(instance))); + renderImage(context, target, coordinates, instance, getImage(context, instance, getOverlayAnimation(instance))); +} + +void MapRendererObjects::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * objectInstance = context.getObject(objectID); + + assert(objectInstance); + if(!objectInstance) + { + logGlobal->error("Stray map object that isn't fading"); + continue; + } + + renderObject(context, target, coordinates, objectInstance); + } +} + +uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * objectInstance = context.getObject(objectID); + + assert(objectInstance); + if(!objectInstance) + { + logGlobal->error("Stray map object that isn't fading"); + continue; + } + + size_t groupIndex = context.objectGroupIndex(objectInstance->id); + Point offsetPixels = context.objectImageOffset(objectInstance->id, coordinates); + + auto base = getBaseAnimation(objectInstance); + auto flag = getFlagAnimation(objectInstance); + + if (base && base->size(groupIndex) > 1) + { + auto imageIndex = context.objectImageIndex(objectID, base->size(groupIndex)); + auto image = base->getImage(imageIndex, groupIndex); + if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) + return context.objectImageIndex(objectID, 250); + } + + if (flag && flag->size(groupIndex) > 1) + { + auto imageIndex = context.objectImageIndex(objectID, flag->size(groupIndex)); + auto image = flag->getImage(imageIndex, groupIndex); + if ( offsetPixels.x < image->dimensions().x && offsetPixels.y < image->dimensions().y) + return context.objectImageIndex(objectID, 250); + } + } + return 0xff-1; +} + +MapRendererOverlay::MapRendererOverlay() + : imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA)) + , imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA)) + , imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA)) + , imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA)) +{ + +} + +void MapRendererOverlay::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if(context.showGrid()) + target.draw(imageGrid, Point(0,0)); + + if(context.showVisitable() || context.showBlocked()) + { + bool blocking = false; + bool visitable = false; + + for(const auto & objectID : context.getObjects(coordinates)) + { + const auto * object = context.getObject(objectID); + + if(context.objectTransparency(objectID, coordinates) > 0 && !context.isActiveHero(object)) + { + visitable |= object->visitableAt(coordinates.x, coordinates.y); + blocking |= object->blockingAt(coordinates.x, coordinates.y); + } + } + + if (context.showBlocked() && blocking) + target.draw(imageBlocked, Point(0,0)); + if (context.showVisitable() && visitable) + target.draw(imageVisitable, Point(0,0)); + } + + if (context.showSpellRange(coordinates)) + target.draw(imageSpellRange, Point(0,0)); +} + +uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + uint8_t result = 0; + + if (context.showVisitable()) + result += 1; + + if (context.showBlocked()) + result += 2; + + if (context.showGrid()) + result += 4; + + if (context.showSpellRange(coordinates)) + result += 8; + + return result; +} + +MapRendererPath::MapRendererPath() + : pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"))) +{ + pathNodes->preload(); +} + +size_t MapRendererPath::selectImageReachability(bool reachableToday, size_t imageIndex) +{ + const static size_t unreachableTodayOffset = 25; + + if(!reachableToday) + return unreachableTodayOffset + imageIndex; + + return imageIndex; +} + +size_t MapRendererPath::selectImageCross(bool reachableToday, const int3 & curr) +{ + return selectImageReachability(reachableToday, 0); +} + +size_t MapRendererPath::selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next) +{ + // Vector directions + // 0 1 2 + // | + // 3 - 4 - 5 + // | + // 6 7 8 + //For example: + // | + // +-> + // is (directionToArrowIndex[7][5]) + // + const static size_t directionToArrowIndex[9][9] = { + {16, 17, 18, 7, 0, 19, 6, 5, 0 }, + {8, 9, 18, 7, 0, 19, 6, 0, 20}, + {8, 1, 10, 7, 0, 19, 0, 21, 20}, + {24, 17, 18, 15, 0, 0, 6, 5, 4 }, + {0, 0, 0, 0, 0, 0, 0, 0, 0 }, + {8, 1, 2, 0, 0, 11, 22, 21, 20}, + {24, 17, 0, 23, 0, 3, 14, 5, 4 }, + {24, 0, 2, 23, 0, 3, 22, 13, 4 }, + {0, 1, 2, 23, 0, 3, 22, 21, 12} + }; + + size_t enterDirection = (curr.x - next.x + 1) + 3 * (curr.y - next.y + 1); + size_t leaveDirection = (prev.x - curr.x + 1) + 3 * (prev.y - curr.y + 1); + size_t imageIndex = directionToArrowIndex[enterDirection][leaveDirection]; + + return selectImageReachability(reachableToday, imageIndex); +} + +void MapRendererPath::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + size_t imageID = selectImage(context, coordinates); + + if (imageID < pathNodes->size()) + target.draw(pathNodes->getImage(imageID), Point(0,0)); +} + +size_t MapRendererPath::selectImage(IMapRendererContext & context, const int3 & coordinates) +{ + const auto & functor = [&](const CGPathNode & node) + { + return node.coord == coordinates; + }; + + const auto * path = context.currentPath(); + if(!path) + return std::numeric_limits::max(); + + const auto & iter = boost::range::find_if(path->nodes, functor); + + if(iter == path->nodes.end()) + return std::numeric_limits::max(); + + bool reachableToday = iter->turns == 0; + if(iter == path->nodes.begin()) + return selectImageCross(reachableToday, iter->coord); + + auto next = iter + 1; + auto prev = iter - 1; + + // start of path - current hero location + if(next == path->nodes.end()) + return std::numeric_limits::max(); + + bool pathContinuous = iter->coord.areNeighbours(next->coord) && iter->coord.areNeighbours(prev->coord); + bool embarking = iter->action == EPathNodeAction::EMBARK || iter->action == EPathNodeAction::DISEMBARK; + + if(pathContinuous && !embarking) + return selectImageArrow(reachableToday, iter->coord, prev->coord, next->coord); + + return selectImageCross(reachableToday, iter->coord); +} + +uint8_t MapRendererPath::checksum(IMapRendererContext & context, const int3 & coordinates) +{ + return selectImage(context, coordinates) & 0xff; +} + +MapRenderer::TileChecksum MapRenderer::getTileChecksum(IMapRendererContext & context, const int3 & coordinates) +{ + // computes basic checksum to determine whether tile needs an update + // if any component gives different value, tile will be updated + TileChecksum result; + boost::range::fill(result, std::numeric_limits::max()); + + if(!context.isInMap(coordinates)) + { + result[0] = rendererBorder.checksum(context, coordinates); + return result; + } + + const NeighborTilesInfo neighborInfo(context, coordinates); + + if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) + { + result[7] = rendererFow.checksum(context, coordinates); + } + else + { + result[1] = rendererTerrain.checksum(context, coordinates); + if (context.showRivers()) + result[2] = rendererRiver.checksum(context, coordinates); + if (context.showRoads()) + result[3] = rendererRoad.checksum(context, coordinates); + result[4] = rendererObjects.checksum(context, coordinates); + result[5] = rendererPath.checksum(context, coordinates); + result[6] = rendererOverlay.checksum(context, coordinates); + + if(!context.isVisible(coordinates)) + result[7] = rendererFow.checksum(context, coordinates); + } + return result; +} + +void MapRenderer::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) +{ + if(!context.isInMap(coordinates)) + { + rendererBorder.renderTile(context, target, coordinates); + return; + } + + const NeighborTilesInfo neighborInfo(context, coordinates); + + if(!context.isVisible(coordinates) && neighborInfo.areAllHidden()) + { + rendererFow.renderTile(context, target, coordinates); + } + else + { + rendererTerrain.renderTile(context, target, coordinates); + + if (context.showRivers()) + rendererRiver.renderTile(context, target, coordinates); + + if (context.showRoads()) + rendererRoad.renderTile(context, target, coordinates); + + rendererObjects.renderTile(context, target, coordinates); + rendererPath.renderTile(context, target, coordinates); + rendererOverlay.renderTile(context, target, coordinates); + + if(!context.isVisible(coordinates)) + rendererFow.renderTile(context, target, coordinates); + } +} diff --git a/client/mapView/MapRenderer.h b/client/mapView/MapRenderer.h index 9bff917b1..46bbca888 100644 --- a/client/mapView/MapRenderer.h +++ b/client/mapView/MapRenderer.h @@ -1,163 +1,163 @@ -/* - * MapRenderer.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/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class int3; -class ObjectInstanceID; -class CGObjectInstance; - -VCMI_LIB_NAMESPACE_END - -class CAnimation; -class IImage; -class Canvas; -class IMapRendererContext; -enum class EImageBlitMode; - -class MapTileStorage -{ - using TerrainAnimation = std::array, 4>; - std::vector animations; - -public: - explicit MapTileStorage(size_t capacity); - void load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode); - std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); -}; - -class MapRendererTerrain -{ - MapTileStorage storage; - -public: - MapRendererTerrain(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererRiver -{ - MapTileStorage storage; - -public: - MapRendererRiver(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererRoad -{ - MapTileStorage storage; - -public: - MapRendererRoad(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererObjects -{ - std::map> animations; - - std::shared_ptr getBaseAnimation(const CGObjectInstance * obj); - std::shared_ptr getFlagAnimation(const CGObjectInstance * obj); - std::shared_ptr getOverlayAnimation(const CGObjectInstance * obj); - - std::shared_ptr getAnimation(const AnimationPath & filename, bool generateMovementGroups); - - std::shared_ptr getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; - - void renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr & image); - void renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * obj); - -public: - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererBorder -{ - std::shared_ptr animation; - - size_t getIndexForTile(IMapRendererContext & context, const int3 & coordinates); - -public: - MapRendererBorder(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererFow -{ - std::shared_ptr fogOfWarFullHide; - std::shared_ptr fogOfWarPartialHide; - -public: - MapRendererFow(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererPath -{ - std::shared_ptr pathNodes; - - size_t selectImageReachability(bool reachableToday, size_t imageIndex); - size_t selectImageCross(bool reachableToday, const int3 & curr); - size_t selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next); - size_t selectImage(IMapRendererContext & context, const int3 & coordinates); - -public: - MapRendererPath(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRendererOverlay -{ - std::shared_ptr imageGrid; - std::shared_ptr imageVisitable; - std::shared_ptr imageBlocked; - std::shared_ptr imageSpellRange; -public: - MapRendererOverlay(); - - uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; - -class MapRenderer -{ - MapRendererTerrain rendererTerrain; - MapRendererRiver rendererRiver; - MapRendererRoad rendererRoad; - MapRendererBorder rendererBorder; - MapRendererFow rendererFow; - MapRendererObjects rendererObjects; - MapRendererPath rendererPath; - MapRendererOverlay rendererOverlay; - -public: - using TileChecksum = std::array; - - TileChecksum getTileChecksum(IMapRendererContext & context, const int3 & coordinates); - - void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); -}; +/* + * MapRenderer.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/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class int3; +class ObjectInstanceID; +class CGObjectInstance; + +VCMI_LIB_NAMESPACE_END + +class CAnimation; +class IImage; +class Canvas; +class IMapRendererContext; +enum class EImageBlitMode; + +class MapTileStorage +{ + using TerrainAnimation = std::array, 4>; + std::vector animations; + +public: + explicit MapTileStorage(size_t capacity); + void load(size_t index, const AnimationPath & filename, EImageBlitMode blitMode); + std::shared_ptr find(size_t fileIndex, size_t rotationIndex, size_t imageIndex); +}; + +class MapRendererTerrain +{ + MapTileStorage storage; + +public: + MapRendererTerrain(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererRiver +{ + MapTileStorage storage; + +public: + MapRendererRiver(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererRoad +{ + MapTileStorage storage; + +public: + MapRendererRoad(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererObjects +{ + std::map> animations; + + std::shared_ptr getBaseAnimation(const CGObjectInstance * obj); + std::shared_ptr getFlagAnimation(const CGObjectInstance * obj); + std::shared_ptr getOverlayAnimation(const CGObjectInstance * obj); + + std::shared_ptr getAnimation(const AnimationPath & filename, bool generateMovementGroups); + + std::shared_ptr getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr & animation) const; + + void renderImage(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * object, const std::shared_ptr & image); + void renderObject(IMapRendererContext & context, Canvas & target, const int3 & coordinates, const CGObjectInstance * obj); + +public: + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererBorder +{ + std::shared_ptr animation; + + size_t getIndexForTile(IMapRendererContext & context, const int3 & coordinates); + +public: + MapRendererBorder(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererFow +{ + std::shared_ptr fogOfWarFullHide; + std::shared_ptr fogOfWarPartialHide; + +public: + MapRendererFow(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererPath +{ + std::shared_ptr pathNodes; + + size_t selectImageReachability(bool reachableToday, size_t imageIndex); + size_t selectImageCross(bool reachableToday, const int3 & curr); + size_t selectImageArrow(bool reachableToday, const int3 & curr, const int3 & prev, const int3 & next); + size_t selectImage(IMapRendererContext & context, const int3 & coordinates); + +public: + MapRendererPath(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRendererOverlay +{ + std::shared_ptr imageGrid; + std::shared_ptr imageVisitable; + std::shared_ptr imageBlocked; + std::shared_ptr imageSpellRange; +public: + MapRendererOverlay(); + + uint8_t checksum(IMapRendererContext & context, const int3 & coordinates); + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; + +class MapRenderer +{ + MapRendererTerrain rendererTerrain; + MapRendererRiver rendererRiver; + MapRendererRoad rendererRoad; + MapRendererBorder rendererBorder; + MapRendererFow rendererFow; + MapRendererObjects rendererObjects; + MapRendererPath rendererPath; + MapRendererOverlay rendererOverlay; + +public: + using TileChecksum = std::array; + + TileChecksum getTileChecksum(IMapRendererContext & context, const int3 & coordinates); + + void renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates); +}; diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index caa355673..d3c896029 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -1,530 +1,530 @@ -/* - * MapRendererContextState.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 "MapRendererContext.h" - -#include "MapRendererContextState.h" -#include "mapHandler.h" - -#include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" - -#include "../../lib/Point.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/mapping/CMap.h" -#include "../../lib/pathfinder/CGPathNode.h" - -MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState) - : viewState(viewState) -{ -} - -uint32_t MapRendererBaseContext::getObjectRotation(ObjectInstanceID objectID) const -{ - const CGObjectInstance * obj = getObject(objectID); - - if(obj->ID == Obj::HERO) - { - const auto * hero = dynamic_cast(obj); - return hero->moveDir; - } - - if(obj->ID == Obj::BOAT) - { - const auto * boat = dynamic_cast(obj); - - if(boat->hero) - return boat->hero->moveDir; - return boat->direction; - } - return 0; -} - -int3 MapRendererBaseContext::getMapSize() const -{ - return LOCPLINT->cb->getMapSize(); -} - -bool MapRendererBaseContext::isInMap(const int3 & coordinates) const -{ - return LOCPLINT->cb->isInTheMap(coordinates); -} - -bool MapRendererBaseContext::isVisible(const int3 & coordinates) const -{ - if(settingsSessionSpectate) - return LOCPLINT->cb->isInTheMap(coordinates); - else - return LOCPLINT->cb->isVisible(coordinates); -} - -bool MapRendererBaseContext::isActiveHero(const CGObjectInstance * obj) const -{ - if(obj->ID == Obj::HERO) - { - assert(dynamic_cast(obj) != nullptr); - if(LOCPLINT->localState->getCurrentHero() != nullptr) - { - if(obj->id == LOCPLINT->localState->getCurrentHero()->id) - return true; - } - } - - return false; -} - -bool MapRendererBaseContext::tileAnimated(const int3 & coordinates) const -{ - return false; -} - -const TerrainTile & MapRendererBaseContext::getMapTile(const int3 & coordinates) const -{ - return CGI->mh->getMap()->getTile(coordinates); -} - -const MapRendererBaseContext::MapObjectsList & MapRendererBaseContext::getObjects(const int3 & coordinates) const -{ - assert(isInMap(coordinates)); - return viewState.objects[coordinates.z][coordinates.x][coordinates.y]; -} - -const CGObjectInstance * MapRendererBaseContext::getObject(ObjectInstanceID objectID) const -{ - return CGI->mh->getMap()->objects.at(objectID.getNum()); -} - -const CGPath * MapRendererBaseContext::currentPath() const -{ - return nullptr; -} - -size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const -{ - static const std::vector idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14}; - return idleGroups[getObjectRotation(objectID)]; -} - -Point MapRendererBaseContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const -{ - const CGObjectInstance * object = getObject(objectID); - int3 offsetTiles(object->getPosition() - coordinates); - return Point(offsetTiles) * Point(32, 32); -} - -double MapRendererBaseContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - const CGObjectInstance * object = getObject(objectID); - - if(object->ID == Obj::HERO) - { - const auto * hero = dynamic_cast(object); - - if(hero->inTownGarrison) - return 0; - - if(hero->boat) - return 0; - } - return 1; -} - -size_t MapRendererBaseContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - return 0; -} - -size_t MapRendererBaseContext::terrainImageIndex(size_t groupSize) const -{ - return 0; -} - -size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const -{ - return std::numeric_limits::max(); -} - -double MapRendererBaseContext::viewTransitionProgress() const -{ - return 0; -} - -bool MapRendererBaseContext::filterGrayscale() const -{ - return false; -} - -bool MapRendererBaseContext::showRoads() const -{ - return true; -} - -bool MapRendererBaseContext::showRivers() const -{ - return true; -} - -bool MapRendererBaseContext::showBorder() const -{ - return false; -} - -bool MapRendererBaseContext::showOverlay() const -{ - return false; -} - -bool MapRendererBaseContext::showGrid() const -{ - return false; -} - -bool MapRendererBaseContext::showVisitable() const -{ - return false; -} - -bool MapRendererBaseContext::showBlocked() const -{ - return false; -} - -bool MapRendererBaseContext::showSpellRange(const int3 & position) const -{ - return false; -} - -MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -const CGPath * MapRendererAdventureContext::currentPath() const -{ - const auto * hero = LOCPLINT->localState->getCurrentHero(); - - if(!hero) - return nullptr; - - if(!LOCPLINT->localState->hasPath(hero)) - return nullptr; - - return &LOCPLINT->localState->getPath(hero); -} - -size_t MapRendererAdventureContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - assert(groupSize > 0); - - if(!settingsAdventureObjectAnimation) - return 0; - - if(groupSize == 0) - return 0; - - // usign objectID for frameCounter to add pseudo-random element per-object. - // Without it, animation of multiple visible objects of the same type will always be in sync - size_t baseFrameTime = 180; - size_t frameCounter = animationTime / baseFrameTime + objectID.getNum(); - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const -{ - if(!settingsAdventureTerrainAnimation) - return 0; - - size_t baseFrameTime = 180; - size_t frameCounter = animationTime / baseFrameTime; - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -bool MapRendererAdventureContext::showBorder() const -{ - return true; -} - -bool MapRendererAdventureContext::showGrid() const -{ - return settingShowGrid; -} - -bool MapRendererAdventureContext::showVisitable() const -{ - return settingShowVisitable; -} - -bool MapRendererAdventureContext::showBlocked() const -{ - return settingShowBlocked; -} - -bool MapRendererAdventureContext::showSpellRange(const int3 & position) const -{ - if (!settingSpellRange) - return false; - - auto hero = LOCPLINT->localState->getCurrentHero(); - - if (!hero) - return false; - - return !isInScreenRange(hero->getSightCenter(), position); -} - -MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -double MapRendererAdventureTransitionContext::viewTransitionProgress() const -{ - return progress; -} - -MapRendererAdventureFadingContext::MapRendererAdventureFadingContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -bool MapRendererAdventureFadingContext::tileAnimated(const int3 & coordinates) const -{ - if(!isInMap(coordinates)) - return false; - - auto objects = getObjects(coordinates); - if(vstd::contains(objects, target)) - return true; - - return false; -} - -double MapRendererAdventureFadingContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(objectID == target) - return progress; - - return MapRendererAdventureContext::objectTransparency(objectID, coordinates); -} - -MapRendererAdventureMovingContext::MapRendererAdventureMovingContext(const MapRendererContextState & viewState) - : MapRendererAdventureContext(viewState) -{ -} - -size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID objectID) const -{ - if(target == objectID) - { - static const std::vector moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; - return moveGroups[getObjectRotation(objectID)]; - } - return MapRendererAdventureContext::objectGroupIndex(objectID); -} - -bool MapRendererAdventureMovingContext::tileAnimated(const int3 & coordinates) const -{ - if(!isInMap(coordinates)) - return false; - - auto objects = getObjects(coordinates); - if(vstd::contains(objects, target)) - return true; - - return false; -} - -Point MapRendererAdventureMovingContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(target == objectID) - { - int3 offsetTilesFrom = tileFrom - coordinates; - int3 offsetTilesDest = tileDest - coordinates; - - Point offsetPixelsFrom = Point(offsetTilesFrom) * Point(32, 32); - Point offsetPixelsDest = Point(offsetTilesDest) * Point(32, 32); - - Point result = vstd::lerp(offsetPixelsFrom, offsetPixelsDest, progress); - - return result; - } - - return MapRendererAdventureContext::objectImageOffset(objectID, coordinates); -} - -size_t MapRendererAdventureMovingContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const -{ - if(target != objectID) - return MapRendererAdventureContext::objectImageIndex(objectID, groupSize); - - int32_t baseFrameTime = 50; - size_t frameCounter = animationTime / baseFrameTime; - size_t frameIndex = frameCounter % groupSize; - return frameIndex; -} - -size_t MapRendererWorldViewContext::selectOverlayImageForObject(const ObjectPosInfo & object) const -{ - size_t ownerIndex = PlayerColor::PLAYER_LIMIT.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); - - if(object.owner.isValidPlayer()) - ownerIndex = object.owner.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); - - switch(object.id) - { - case Obj::MONOLITH_ONE_WAY_ENTRANCE: - case Obj::MONOLITH_ONE_WAY_EXIT: - case Obj::MONOLITH_TWO_WAY: - return ownerIndex + static_cast(EWorldViewIcon::TELEPORT); - case Obj::SUBTERRANEAN_GATE: - return ownerIndex + static_cast(EWorldViewIcon::GATE); - case Obj::ARTIFACT: - return ownerIndex + static_cast(EWorldViewIcon::ARTIFACT); - case Obj::TOWN: - return ownerIndex + static_cast(EWorldViewIcon::TOWN); - case Obj::HERO: - return ownerIndex + static_cast(EWorldViewIcon::HERO); - case Obj::MINE: - return ownerIndex + static_cast(EWorldViewIcon::MINE_WOOD) + object.subId; - case Obj::RESOURCE: - return ownerIndex + static_cast(EWorldViewIcon::RES_WOOD) + object.subId; - } - return std::numeric_limits::max(); -} - -MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -bool MapRendererWorldViewContext::showOverlay() const -{ - return true; -} - -size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) const -{ - if(!isVisible(coordinates)) - return std::numeric_limits::max(); - - for(const auto & objectID : getObjects(coordinates)) - { - const auto * object = getObject(objectID); - - if(!object->visitableAt(coordinates.x, coordinates.y)) - continue; - - ObjectPosInfo info; - info.pos = coordinates; - info.id = object->ID; - info.subId = object->subID; - info.owner = object->tempOwner; - - size_t iconIndex = selectOverlayImageForObject(info); - - if(iconIndex != std::numeric_limits::max()) - return iconIndex; - } - - return std::numeric_limits::max(); -} - -MapRendererSpellViewContext::MapRendererSpellViewContext(const MapRendererContextState & viewState) - : MapRendererWorldViewContext(viewState) -{ -} - -double MapRendererSpellViewContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - if(showAllTerrain) - { - if(getObject(objectID)->isVisitable() && !MapRendererWorldViewContext::isVisible(coordinates)) - return 0; - } - - return MapRendererWorldViewContext::objectTransparency(objectID, coordinates); -} - -bool MapRendererSpellViewContext::isVisible(const int3 & coordinates) const -{ - if(showAllTerrain) - return isInMap(coordinates); - return MapRendererBaseContext::isVisible(coordinates); -} - -size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) const -{ - for(const auto & entry : additionalOverlayIcons) - { - if(entry.pos != coordinates) - continue; - - size_t iconIndex = selectOverlayImageForObject(entry); - - if(iconIndex != std::numeric_limits::max()) - return iconIndex; - } - - return MapRendererWorldViewContext::overlayImageIndex(coordinates); -} - -MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) - : MapRendererBaseContext(viewState) -{ -} - -MapRendererPuzzleMapContext::~MapRendererPuzzleMapContext() = default; - -const CGPath * MapRendererPuzzleMapContext::currentPath() const -{ - return grailPos.get(); -} - -double MapRendererPuzzleMapContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const -{ - const auto * object = getObject(objectID); - - if(!object) - return 0; - - if(object->isVisitable()) - return 0; - - if(object->ID == Obj::HOLE) - return 0; - - return MapRendererBaseContext::objectTransparency(objectID, coordinates); -} - -bool MapRendererPuzzleMapContext::isVisible(const int3 & coordinates) const -{ - return LOCPLINT->cb->isInTheMap(coordinates); -} - -bool MapRendererPuzzleMapContext::filterGrayscale() const -{ - return true; -} - -bool MapRendererPuzzleMapContext::showRoads() const -{ - return false; -} - -bool MapRendererPuzzleMapContext::showRivers() const -{ - return false; -} +/* + * MapRendererContextState.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 "MapRendererContext.h" + +#include "MapRendererContextState.h" +#include "mapHandler.h" + +#include "../../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" + +#include "../../lib/Point.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/mapping/CMap.h" +#include "../../lib/pathfinder/CGPathNode.h" + +MapRendererBaseContext::MapRendererBaseContext(const MapRendererContextState & viewState) + : viewState(viewState) +{ +} + +uint32_t MapRendererBaseContext::getObjectRotation(ObjectInstanceID objectID) const +{ + const CGObjectInstance * obj = getObject(objectID); + + if(obj->ID == Obj::HERO) + { + const auto * hero = dynamic_cast(obj); + return hero->moveDir; + } + + if(obj->ID == Obj::BOAT) + { + const auto * boat = dynamic_cast(obj); + + if(boat->hero) + return boat->hero->moveDir; + return boat->direction; + } + return 0; +} + +int3 MapRendererBaseContext::getMapSize() const +{ + return LOCPLINT->cb->getMapSize(); +} + +bool MapRendererBaseContext::isInMap(const int3 & coordinates) const +{ + return LOCPLINT->cb->isInTheMap(coordinates); +} + +bool MapRendererBaseContext::isVisible(const int3 & coordinates) const +{ + if(settingsSessionSpectate) + return LOCPLINT->cb->isInTheMap(coordinates); + else + return LOCPLINT->cb->isVisible(coordinates); +} + +bool MapRendererBaseContext::isActiveHero(const CGObjectInstance * obj) const +{ + if(obj->ID == Obj::HERO) + { + assert(dynamic_cast(obj) != nullptr); + if(LOCPLINT->localState->getCurrentHero() != nullptr) + { + if(obj->id == LOCPLINT->localState->getCurrentHero()->id) + return true; + } + } + + return false; +} + +bool MapRendererBaseContext::tileAnimated(const int3 & coordinates) const +{ + return false; +} + +const TerrainTile & MapRendererBaseContext::getMapTile(const int3 & coordinates) const +{ + return CGI->mh->getMap()->getTile(coordinates); +} + +const MapRendererBaseContext::MapObjectsList & MapRendererBaseContext::getObjects(const int3 & coordinates) const +{ + assert(isInMap(coordinates)); + return viewState.objects[coordinates.z][coordinates.x][coordinates.y]; +} + +const CGObjectInstance * MapRendererBaseContext::getObject(ObjectInstanceID objectID) const +{ + return CGI->mh->getMap()->objects.at(objectID.getNum()); +} + +const CGPath * MapRendererBaseContext::currentPath() const +{ + return nullptr; +} + +size_t MapRendererBaseContext::objectGroupIndex(ObjectInstanceID objectID) const +{ + static const std::vector idleGroups = {0, 13, 0, 1, 2, 3, 4, 15, 14}; + return idleGroups[getObjectRotation(objectID)]; +} + +Point MapRendererBaseContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const +{ + const CGObjectInstance * object = getObject(objectID); + int3 offsetTiles(object->getPosition() - coordinates); + return Point(offsetTiles) * Point(32, 32); +} + +double MapRendererBaseContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + const CGObjectInstance * object = getObject(objectID); + + if(object->ID == Obj::HERO) + { + const auto * hero = dynamic_cast(object); + + if(hero->inTownGarrison) + return 0; + + if(hero->boat) + return 0; + } + return 1; +} + +size_t MapRendererBaseContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + return 0; +} + +size_t MapRendererBaseContext::terrainImageIndex(size_t groupSize) const +{ + return 0; +} + +size_t MapRendererBaseContext::overlayImageIndex(const int3 & coordinates) const +{ + return std::numeric_limits::max(); +} + +double MapRendererBaseContext::viewTransitionProgress() const +{ + return 0; +} + +bool MapRendererBaseContext::filterGrayscale() const +{ + return false; +} + +bool MapRendererBaseContext::showRoads() const +{ + return true; +} + +bool MapRendererBaseContext::showRivers() const +{ + return true; +} + +bool MapRendererBaseContext::showBorder() const +{ + return false; +} + +bool MapRendererBaseContext::showOverlay() const +{ + return false; +} + +bool MapRendererBaseContext::showGrid() const +{ + return false; +} + +bool MapRendererBaseContext::showVisitable() const +{ + return false; +} + +bool MapRendererBaseContext::showBlocked() const +{ + return false; +} + +bool MapRendererBaseContext::showSpellRange(const int3 & position) const +{ + return false; +} + +MapRendererAdventureContext::MapRendererAdventureContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +const CGPath * MapRendererAdventureContext::currentPath() const +{ + const auto * hero = LOCPLINT->localState->getCurrentHero(); + + if(!hero) + return nullptr; + + if(!LOCPLINT->localState->hasPath(hero)) + return nullptr; + + return &LOCPLINT->localState->getPath(hero); +} + +size_t MapRendererAdventureContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + assert(groupSize > 0); + + if(!settingsAdventureObjectAnimation) + return 0; + + if(groupSize == 0) + return 0; + + // usign objectID for frameCounter to add pseudo-random element per-object. + // Without it, animation of multiple visible objects of the same type will always be in sync + size_t baseFrameTime = 180; + size_t frameCounter = animationTime / baseFrameTime + objectID.getNum(); + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +size_t MapRendererAdventureContext::terrainImageIndex(size_t groupSize) const +{ + if(!settingsAdventureTerrainAnimation) + return 0; + + size_t baseFrameTime = 180; + size_t frameCounter = animationTime / baseFrameTime; + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +bool MapRendererAdventureContext::showBorder() const +{ + return true; +} + +bool MapRendererAdventureContext::showGrid() const +{ + return settingShowGrid; +} + +bool MapRendererAdventureContext::showVisitable() const +{ + return settingShowVisitable; +} + +bool MapRendererAdventureContext::showBlocked() const +{ + return settingShowBlocked; +} + +bool MapRendererAdventureContext::showSpellRange(const int3 & position) const +{ + if (!settingSpellRange) + return false; + + auto hero = LOCPLINT->localState->getCurrentHero(); + + if (!hero) + return false; + + return !isInScreenRange(hero->getSightCenter(), position); +} + +MapRendererAdventureTransitionContext::MapRendererAdventureTransitionContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +double MapRendererAdventureTransitionContext::viewTransitionProgress() const +{ + return progress; +} + +MapRendererAdventureFadingContext::MapRendererAdventureFadingContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +bool MapRendererAdventureFadingContext::tileAnimated(const int3 & coordinates) const +{ + if(!isInMap(coordinates)) + return false; + + auto objects = getObjects(coordinates); + if(vstd::contains(objects, target)) + return true; + + return false; +} + +double MapRendererAdventureFadingContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(objectID == target) + return progress; + + return MapRendererAdventureContext::objectTransparency(objectID, coordinates); +} + +MapRendererAdventureMovingContext::MapRendererAdventureMovingContext(const MapRendererContextState & viewState) + : MapRendererAdventureContext(viewState) +{ +} + +size_t MapRendererAdventureMovingContext::objectGroupIndex(ObjectInstanceID objectID) const +{ + if(target == objectID) + { + static const std::vector moveGroups = {0, 10, 5, 6, 7, 8, 9, 12, 11}; + return moveGroups[getObjectRotation(objectID)]; + } + return MapRendererAdventureContext::objectGroupIndex(objectID); +} + +bool MapRendererAdventureMovingContext::tileAnimated(const int3 & coordinates) const +{ + if(!isInMap(coordinates)) + return false; + + auto objects = getObjects(coordinates); + if(vstd::contains(objects, target)) + return true; + + return false; +} + +Point MapRendererAdventureMovingContext::objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(target == objectID) + { + int3 offsetTilesFrom = tileFrom - coordinates; + int3 offsetTilesDest = tileDest - coordinates; + + Point offsetPixelsFrom = Point(offsetTilesFrom) * Point(32, 32); + Point offsetPixelsDest = Point(offsetTilesDest) * Point(32, 32); + + Point result = vstd::lerp(offsetPixelsFrom, offsetPixelsDest, progress); + + return result; + } + + return MapRendererAdventureContext::objectImageOffset(objectID, coordinates); +} + +size_t MapRendererAdventureMovingContext::objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const +{ + if(target != objectID) + return MapRendererAdventureContext::objectImageIndex(objectID, groupSize); + + int32_t baseFrameTime = 50; + size_t frameCounter = animationTime / baseFrameTime; + size_t frameIndex = frameCounter % groupSize; + return frameIndex; +} + +size_t MapRendererWorldViewContext::selectOverlayImageForObject(const ObjectPosInfo & object) const +{ + size_t ownerIndex = PlayerColor::PLAYER_LIMIT.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); + + if(object.owner.isValidPlayer()) + ownerIndex = object.owner.getNum() * static_cast(EWorldViewIcon::ICONS_PER_PLAYER); + + switch(object.id) + { + case Obj::MONOLITH_ONE_WAY_ENTRANCE: + case Obj::MONOLITH_ONE_WAY_EXIT: + case Obj::MONOLITH_TWO_WAY: + return ownerIndex + static_cast(EWorldViewIcon::TELEPORT); + case Obj::SUBTERRANEAN_GATE: + return ownerIndex + static_cast(EWorldViewIcon::GATE); + case Obj::ARTIFACT: + return ownerIndex + static_cast(EWorldViewIcon::ARTIFACT); + case Obj::TOWN: + return ownerIndex + static_cast(EWorldViewIcon::TOWN); + case Obj::HERO: + return ownerIndex + static_cast(EWorldViewIcon::HERO); + case Obj::MINE: + return ownerIndex + static_cast(EWorldViewIcon::MINE_WOOD) + object.subId; + case Obj::RESOURCE: + return ownerIndex + static_cast(EWorldViewIcon::RES_WOOD) + object.subId; + } + return std::numeric_limits::max(); +} + +MapRendererWorldViewContext::MapRendererWorldViewContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +bool MapRendererWorldViewContext::showOverlay() const +{ + return true; +} + +size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) const +{ + if(!isVisible(coordinates)) + return std::numeric_limits::max(); + + for(const auto & objectID : getObjects(coordinates)) + { + const auto * object = getObject(objectID); + + if(!object->visitableAt(coordinates.x, coordinates.y)) + continue; + + ObjectPosInfo info; + info.pos = coordinates; + info.id = object->ID; + info.subId = object->subID; + info.owner = object->tempOwner; + + size_t iconIndex = selectOverlayImageForObject(info); + + if(iconIndex != std::numeric_limits::max()) + return iconIndex; + } + + return std::numeric_limits::max(); +} + +MapRendererSpellViewContext::MapRendererSpellViewContext(const MapRendererContextState & viewState) + : MapRendererWorldViewContext(viewState) +{ +} + +double MapRendererSpellViewContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + if(showAllTerrain) + { + if(getObject(objectID)->isVisitable() && !MapRendererWorldViewContext::isVisible(coordinates)) + return 0; + } + + return MapRendererWorldViewContext::objectTransparency(objectID, coordinates); +} + +bool MapRendererSpellViewContext::isVisible(const int3 & coordinates) const +{ + if(showAllTerrain) + return isInMap(coordinates); + return MapRendererBaseContext::isVisible(coordinates); +} + +size_t MapRendererSpellViewContext::overlayImageIndex(const int3 & coordinates) const +{ + for(const auto & entry : additionalOverlayIcons) + { + if(entry.pos != coordinates) + continue; + + size_t iconIndex = selectOverlayImageForObject(entry); + + if(iconIndex != std::numeric_limits::max()) + return iconIndex; + } + + return MapRendererWorldViewContext::overlayImageIndex(coordinates); +} + +MapRendererPuzzleMapContext::MapRendererPuzzleMapContext(const MapRendererContextState & viewState) + : MapRendererBaseContext(viewState) +{ +} + +MapRendererPuzzleMapContext::~MapRendererPuzzleMapContext() = default; + +const CGPath * MapRendererPuzzleMapContext::currentPath() const +{ + return grailPos.get(); +} + +double MapRendererPuzzleMapContext::objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const +{ + const auto * object = getObject(objectID); + + if(!object) + return 0; + + if(object->isVisitable()) + return 0; + + if(object->ID == Obj::HOLE) + return 0; + + return MapRendererBaseContext::objectTransparency(objectID, coordinates); +} + +bool MapRendererPuzzleMapContext::isVisible(const int3 & coordinates) const +{ + return LOCPLINT->cb->isInTheMap(coordinates); +} + +bool MapRendererPuzzleMapContext::filterGrayscale() const +{ + return true; +} + +bool MapRendererPuzzleMapContext::showRoads() const +{ + return false; +} + +bool MapRendererPuzzleMapContext::showRivers() const +{ + return false; +} diff --git a/client/mapView/MapRendererContext.h b/client/mapView/MapRendererContext.h index ee8061535..742053fd1 100644 --- a/client/mapView/MapRendererContext.h +++ b/client/mapView/MapRendererContext.h @@ -1,166 +1,166 @@ -/* - * MapRendererContext.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 "IMapRendererContext.h" - -#include "../lib/GameConstants.h" -#include "../lib/int3.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -VCMI_LIB_NAMESPACE_END - -struct MapRendererContextState; - -class MapRendererBaseContext : public IMapRendererContext -{ -public: - const MapRendererContextState & viewState; - bool settingsSessionSpectate = false; - - explicit MapRendererBaseContext(const MapRendererContextState & viewState); - - uint32_t getObjectRotation(ObjectInstanceID objectID) const; - - int3 getMapSize() const override; - bool isInMap(const int3 & coordinates) const override; - bool isVisible(const int3 & coordinates) const override; - bool tileAnimated(const int3 & coordinates) const override; - - bool isActiveHero(const CGObjectInstance* obj) const override; - - const TerrainTile & getMapTile(const int3 & coordinates) const override; - const MapObjectsList & getObjects(const int3 & coordinates) const override; - const CGObjectInstance * getObject(ObjectInstanceID objectID) const override; - const CGPath * currentPath() const override; - - size_t objectGroupIndex(ObjectInstanceID objectID) const override; - Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; - size_t terrainImageIndex(size_t groupSize) const override; - size_t overlayImageIndex(const int3 & coordinates) const override; - - double viewTransitionProgress() const override; - bool filterGrayscale() const override; - bool showRoads() const override; - bool showRivers() const override; - bool showBorder() const override; - bool showOverlay() const override; - bool showGrid() const override; - bool showVisitable() const override; - bool showBlocked() const override; - bool showSpellRange(const int3 & position) const override; -}; - -class MapRendererAdventureContext : public MapRendererBaseContext -{ -public: - uint32_t animationTime = 0; - bool settingShowGrid = false; - bool settingShowVisitable = false; - bool settingShowBlocked = false; - bool settingSpellRange= false; - bool settingsAdventureObjectAnimation = true; - bool settingsAdventureTerrainAnimation = true; - - explicit MapRendererAdventureContext(const MapRendererContextState & viewState); - - const CGPath * currentPath() const override; - size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; - size_t terrainImageIndex(size_t groupSize) const override; - - bool showBorder() const override; - bool showGrid() const override; - bool showVisitable() const override; - bool showBlocked() const override; - - bool showSpellRange(const int3 & position) const override; -}; - -class MapRendererAdventureTransitionContext : public MapRendererAdventureContext -{ -public: - double progress = 0; - - explicit MapRendererAdventureTransitionContext(const MapRendererContextState & viewState); - - double viewTransitionProgress() const override; -}; - -class MapRendererAdventureFadingContext : public MapRendererAdventureContext -{ -public: - ObjectInstanceID target; - double progress; - - explicit MapRendererAdventureFadingContext(const MapRendererContextState & viewState); - - bool tileAnimated(const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; -}; - -class MapRendererAdventureMovingContext : public MapRendererAdventureContext -{ -public: - ObjectInstanceID target; - int3 tileFrom; - int3 tileDest; - double progress; - - explicit MapRendererAdventureMovingContext(const MapRendererContextState & viewState); - - bool tileAnimated(const int3 & coordinates) const override; - size_t objectGroupIndex(ObjectInstanceID objectID) const override; - Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; - size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; -}; - -class MapRendererWorldViewContext : public MapRendererBaseContext -{ -protected: - size_t selectOverlayImageForObject(const ObjectPosInfo & object) const; - -public: - explicit MapRendererWorldViewContext(const MapRendererContextState & viewState); - - size_t overlayImageIndex(const int3 & coordinates) const override; - bool showOverlay() const override; -}; - -class MapRendererSpellViewContext : public MapRendererWorldViewContext -{ -public: - std::vector additionalOverlayIcons; - bool showAllTerrain = false; - - explicit MapRendererSpellViewContext(const MapRendererContextState & viewState); - - bool isVisible(const int3 & coordinates) const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - size_t overlayImageIndex(const int3 & coordinates) const override; -}; - -class MapRendererPuzzleMapContext : public MapRendererBaseContext -{ -public: - std::unique_ptr grailPos; - - explicit MapRendererPuzzleMapContext(const MapRendererContextState & viewState); - ~MapRendererPuzzleMapContext(); - - const CGPath * currentPath() const override; - double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; - bool isVisible(const int3 & coordinates) const override; - bool filterGrayscale() const override; - bool showRoads() const override; - bool showRivers() const override; -}; +/* + * MapRendererContext.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 "IMapRendererContext.h" + +#include "../lib/GameConstants.h" +#include "../lib/int3.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +VCMI_LIB_NAMESPACE_END + +struct MapRendererContextState; + +class MapRendererBaseContext : public IMapRendererContext +{ +public: + const MapRendererContextState & viewState; + bool settingsSessionSpectate = false; + + explicit MapRendererBaseContext(const MapRendererContextState & viewState); + + uint32_t getObjectRotation(ObjectInstanceID objectID) const; + + int3 getMapSize() const override; + bool isInMap(const int3 & coordinates) const override; + bool isVisible(const int3 & coordinates) const override; + bool tileAnimated(const int3 & coordinates) const override; + + bool isActiveHero(const CGObjectInstance* obj) const override; + + const TerrainTile & getMapTile(const int3 & coordinates) const override; + const MapObjectsList & getObjects(const int3 & coordinates) const override; + const CGObjectInstance * getObject(ObjectInstanceID objectID) const override; + const CGPath * currentPath() const override; + + size_t objectGroupIndex(ObjectInstanceID objectID) const override; + Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; + size_t terrainImageIndex(size_t groupSize) const override; + size_t overlayImageIndex(const int3 & coordinates) const override; + + double viewTransitionProgress() const override; + bool filterGrayscale() const override; + bool showRoads() const override; + bool showRivers() const override; + bool showBorder() const override; + bool showOverlay() const override; + bool showGrid() const override; + bool showVisitable() const override; + bool showBlocked() const override; + bool showSpellRange(const int3 & position) const override; +}; + +class MapRendererAdventureContext : public MapRendererBaseContext +{ +public: + uint32_t animationTime = 0; + bool settingShowGrid = false; + bool settingShowVisitable = false; + bool settingShowBlocked = false; + bool settingSpellRange= false; + bool settingsAdventureObjectAnimation = true; + bool settingsAdventureTerrainAnimation = true; + + explicit MapRendererAdventureContext(const MapRendererContextState & viewState); + + const CGPath * currentPath() const override; + size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; + size_t terrainImageIndex(size_t groupSize) const override; + + bool showBorder() const override; + bool showGrid() const override; + bool showVisitable() const override; + bool showBlocked() const override; + + bool showSpellRange(const int3 & position) const override; +}; + +class MapRendererAdventureTransitionContext : public MapRendererAdventureContext +{ +public: + double progress = 0; + + explicit MapRendererAdventureTransitionContext(const MapRendererContextState & viewState); + + double viewTransitionProgress() const override; +}; + +class MapRendererAdventureFadingContext : public MapRendererAdventureContext +{ +public: + ObjectInstanceID target; + double progress; + + explicit MapRendererAdventureFadingContext(const MapRendererContextState & viewState); + + bool tileAnimated(const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; +}; + +class MapRendererAdventureMovingContext : public MapRendererAdventureContext +{ +public: + ObjectInstanceID target; + int3 tileFrom; + int3 tileDest; + double progress; + + explicit MapRendererAdventureMovingContext(const MapRendererContextState & viewState); + + bool tileAnimated(const int3 & coordinates) const override; + size_t objectGroupIndex(ObjectInstanceID objectID) const override; + Point objectImageOffset(ObjectInstanceID objectID, const int3 & coordinates) const override; + size_t objectImageIndex(ObjectInstanceID objectID, size_t groupSize) const override; +}; + +class MapRendererWorldViewContext : public MapRendererBaseContext +{ +protected: + size_t selectOverlayImageForObject(const ObjectPosInfo & object) const; + +public: + explicit MapRendererWorldViewContext(const MapRendererContextState & viewState); + + size_t overlayImageIndex(const int3 & coordinates) const override; + bool showOverlay() const override; +}; + +class MapRendererSpellViewContext : public MapRendererWorldViewContext +{ +public: + std::vector additionalOverlayIcons; + bool showAllTerrain = false; + + explicit MapRendererSpellViewContext(const MapRendererContextState & viewState); + + bool isVisible(const int3 & coordinates) const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + size_t overlayImageIndex(const int3 & coordinates) const override; +}; + +class MapRendererPuzzleMapContext : public MapRendererBaseContext +{ +public: + std::unique_ptr grailPos; + + explicit MapRendererPuzzleMapContext(const MapRendererContextState & viewState); + ~MapRendererPuzzleMapContext(); + + const CGPath * currentPath() const override; + double objectTransparency(ObjectInstanceID objectID, const int3 & coordinates) const override; + bool isVisible(const int3 & coordinates) const override; + bool filterGrayscale() const override; + bool showRoads() const override; + bool showRivers() const override; +}; diff --git a/client/mapView/MapRendererContextState.cpp b/client/mapView/MapRendererContextState.cpp index 1571cc459..196ff9671 100644 --- a/client/mapView/MapRendererContextState.cpp +++ b/client/mapView/MapRendererContextState.cpp @@ -1,93 +1,93 @@ -/* - * MapRendererContext.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 "MapRendererContextState.h" - -#include "IMapRendererContext.h" -#include "mapHandler.h" - -#include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" - -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapping/CMap.h" - -static bool compareObjectBlitOrder(ObjectInstanceID left, ObjectInstanceID right) -{ - //FIXME: remove mh access - return CGI->mh->compareObjectBlitOrder(CGI->mh->getMap()->objects[left.getNum()], CGI->mh->getMap()->objects[right.getNum()]); -} - -MapRendererContextState::MapRendererContextState() -{ - auto mapSize = LOCPLINT->cb->getMapSize(); - - objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); - - for(const auto & obj : CGI->mh->getMap()->objects) - addObject(obj); -} - -void MapRendererContextState::addObject(const CGObjectInstance * obj) -{ - if(!obj) - return; - - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) - { - auto & container = objects[currTile.z][currTile.x][currTile.y]; - - container.push_back(obj->id); - boost::range::sort(container, compareObjectBlitOrder); - } - } - } -} - -void MapRendererContextState::addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest) -{ - int xFrom = std::min(tileFrom.x, tileDest.x) - object->getWidth(); - int xDest = std::max(tileFrom.x, tileDest.x); - int yFrom = std::min(tileFrom.y, tileDest.y) - object->getHeight(); - int yDest = std::max(tileFrom.y, tileDest.y); - - for(int x = xFrom; x <= xDest; ++x) - { - for(int y = yFrom; y <= yDest; ++y) - { - int3 currTile(x, y, object->pos.z); - - if(LOCPLINT->cb->isInTheMap(currTile)) - { - auto & container = objects[currTile.z][currTile.x][currTile.y]; - - container.push_back(object->id); - boost::range::sort(container, compareObjectBlitOrder); - } - } - } -} - -void MapRendererContextState::removeObject(const CGObjectInstance * object) -{ - for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) - for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) - for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) - vstd::erase(objects[z][x][y], object->id); -} +/* + * MapRendererContext.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 "MapRendererContextState.h" + +#include "IMapRendererContext.h" +#include "mapHandler.h" + +#include "../../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapping/CMap.h" + +static bool compareObjectBlitOrder(ObjectInstanceID left, ObjectInstanceID right) +{ + //FIXME: remove mh access + return CGI->mh->compareObjectBlitOrder(CGI->mh->getMap()->objects[left.getNum()], CGI->mh->getMap()->objects[right.getNum()]); +} + +MapRendererContextState::MapRendererContextState() +{ + auto mapSize = LOCPLINT->cb->getMapSize(); + + objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + + for(const auto & obj : CGI->mh->getMap()->objects) + addObject(obj); +} + +void MapRendererContextState::addObject(const CGObjectInstance * obj) +{ + if(!obj) + return; + + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile.x, currTile.y)) + { + auto & container = objects[currTile.z][currTile.x][currTile.y]; + + container.push_back(obj->id); + boost::range::sort(container, compareObjectBlitOrder); + } + } + } +} + +void MapRendererContextState::addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest) +{ + int xFrom = std::min(tileFrom.x, tileDest.x) - object->getWidth(); + int xDest = std::max(tileFrom.x, tileDest.x); + int yFrom = std::min(tileFrom.y, tileDest.y) - object->getHeight(); + int yDest = std::max(tileFrom.y, tileDest.y); + + for(int x = xFrom; x <= xDest; ++x) + { + for(int y = yFrom; y <= yDest; ++y) + { + int3 currTile(x, y, object->pos.z); + + if(LOCPLINT->cb->isInTheMap(currTile)) + { + auto & container = objects[currTile.z][currTile.x][currTile.y]; + + container.push_back(object->id); + boost::range::sort(container, compareObjectBlitOrder); + } + } + } +} + +void MapRendererContextState::removeObject(const CGObjectInstance * object) +{ + for(int z = 0; z < LOCPLINT->cb->getMapSize().z; z++) + for(int x = 0; x < LOCPLINT->cb->getMapSize().x; x++) + for(int y = 0; y < LOCPLINT->cb->getMapSize().y; y++) + vstd::erase(objects[z][x][y], object->id); +} diff --git a/client/mapView/MapRendererContextState.h b/client/mapView/MapRendererContextState.h index 9adee457f..cf0442b72 100644 --- a/client/mapView/MapRendererContextState.h +++ b/client/mapView/MapRendererContextState.h @@ -1,62 +1,62 @@ -/* - * MapRendererContext.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/GameConstants.h" -#include "../lib/int3.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -class CGObjectInstance; -VCMI_LIB_NAMESPACE_END - -class IMapRendererContext; - -// from VwSymbol.def -enum class EWorldViewIcon -{ - TOWN = 0, - HERO = 1, - ARTIFACT = 2, - TELEPORT = 3, - GATE = 4, - MINE_WOOD = 5, - MINE_MERCURY = 6, - MINE_STONE = 7, - MINE_SULFUR = 8, - MINE_CRYSTAL = 9, - MINE_GEM = 10, - MINE_GOLD = 11, - RES_WOOD = 12, - RES_MERCURY = 13, - RES_STONE = 14, - RES_SULFUR = 15, - RES_CRYSTAL = 16, - RES_GEM = 17, - RES_GOLD = 18, - - ICONS_PER_PLAYER = 19, - ICONS_TOTAL = 19 * 9 // 8 players + neutral set at the end -}; - -struct MapRendererContextState -{ -public: - MapRendererContextState(); - - using MapObject = ObjectInstanceID; - using MapObjectsList = std::vector; - - boost::multi_array objects; - - void addObject(const CGObjectInstance * object); - void addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest); - void removeObject(const CGObjectInstance * object); -}; +/* + * MapRendererContext.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/GameConstants.h" +#include "../lib/int3.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class IMapRendererContext; + +// from VwSymbol.def +enum class EWorldViewIcon +{ + TOWN = 0, + HERO = 1, + ARTIFACT = 2, + TELEPORT = 3, + GATE = 4, + MINE_WOOD = 5, + MINE_MERCURY = 6, + MINE_STONE = 7, + MINE_SULFUR = 8, + MINE_CRYSTAL = 9, + MINE_GEM = 10, + MINE_GOLD = 11, + RES_WOOD = 12, + RES_MERCURY = 13, + RES_STONE = 14, + RES_SULFUR = 15, + RES_CRYSTAL = 16, + RES_GEM = 17, + RES_GOLD = 18, + + ICONS_PER_PLAYER = 19, + ICONS_TOTAL = 19 * 9 // 8 players + neutral set at the end +}; + +struct MapRendererContextState +{ +public: + MapRendererContextState(); + + using MapObject = ObjectInstanceID; + using MapObjectsList = std::vector; + + boost::multi_array objects; + + void addObject(const CGObjectInstance * object); + void addMovingObject(const CGObjectInstance * object, const int3 & tileFrom, const int3 & tileDest); + void removeObject(const CGObjectInstance * object); +}; diff --git a/client/mapView/MapView.cpp b/client/mapView/MapView.cpp index cc7e0dbee..b39c8a6f5 100644 --- a/client/mapView/MapView.cpp +++ b/client/mapView/MapView.cpp @@ -1,215 +1,215 @@ -/* - * MapView.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 "MapView.h" - -#include "MapViewActions.h" -#include "MapViewCache.h" -#include "MapViewController.h" -#include "MapViewModel.h" -#include "mapHandler.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../renderSDL/SDL_Extensions.h" -#include "../eventsSDL/InputHandler.h" - -#include "../../CCallback.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" - -BasicMapView::~BasicMapView() = default; - -std::shared_ptr BasicMapView::createModel(const Point & dimensions) const -{ - auto result = std::make_shared(); - - result->setLevel(0); - result->setTileSize(Point(32, 32)); - result->setViewCenter(Point(0, 0)); - result->setViewDimensions(dimensions); - - return result; -} - -BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) - : model(createModel(dimensions)) - , tilesCache(new MapViewCache(model)) - , controller(new MapViewController(model, tilesCache)) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos += offset; - pos.w = dimensions.x; - pos.h = dimensions.y; -} - -void BasicMapView::render(Canvas & target, bool fullUpdate) -{ - Canvas targetClipped(target, pos); - tilesCache->update(controller->getContext()); - tilesCache->render(controller->getContext(), targetClipped, fullUpdate); -} - -void BasicMapView::tick(uint32_t msPassed) -{ - controller->tick(msPassed); -} - -void BasicMapView::show(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - render(to, false); - - controller->afterRender(); -} - -void BasicMapView::showAll(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); - render(to, true); -} - -void MapView::tick(uint32_t msPassed) -{ - if(settings["adventure"]["smoothDragging"].Bool()) - postSwipe(msPassed); - - BasicMapView::tick(msPassed); -} - -void MapView::show(Canvas & to) -{ - actions->setContext(controller->getContext()); - BasicMapView::show(to); -} - -MapView::MapView(const Point & offset, const Point & dimensions) - : BasicMapView(offset, dimensions) -{ - OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - actions = std::make_shared(*this, model); - actions->setContext(controller->getContext()); - - // catch min 6 frames - postSwipeCatchIntervalMs = std::max(100, static_cast(6.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float()))); -} - -void MapView::onMapLevelSwitched() -{ - if(LOCPLINT->cb->getMapSize().z > 1) - { - if(model->getLevel() == 0) - controller->setViewCenter(model->getMapViewCenter(), 1); - else - controller->setViewCenter(model->getMapViewCenter(), 0); - } -} - -void MapView::onMapScrolled(const Point & distance) -{ - if(!isGesturing()) - controller->setViewCenter(model->getMapViewCenter() + distance, model->getLevel()); -} - -void MapView::onMapSwiped(const Point & viewPosition) -{ - if(settings["adventure"]["smoothDragging"].Bool()) - swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); - - controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); -} - -void MapView::postSwipe(uint32_t msPassed) -{ - if(!actions->dragActive) - { - if(swipeHistory.size() > 1) - { - Point diff = Point(0, 0); - std::pair firstAccepted; - uint32_t now = GH.input().getTicks(); - for (auto & x : swipeHistory) { - if(now - x.first < postSwipeCatchIntervalMs) { // only the last x ms are catched - if(firstAccepted.first == 0) - firstAccepted = x; - diff += x.second; - } - } - - uint32_t timediff = swipeHistory.back().first - firstAccepted.first; - - if(diff.length() > 0 && timediff > 0) - { - postSwipeAngle = diff.angle(); - postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond - } - } - swipeHistory.clear(); - } else - postSwipeSpeed = 0.0; - if(postSwipeSpeed > postSwipeMinimalSpeed) { - double len = postSwipeSpeed * static_cast(msPassed); - Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); - - controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); - - postSwipeSpeed /= 1 + msPassed * postSwipeSlowdownSpeed; - } -} - -void MapView::onCenteredTile(const int3 & tile) -{ - controller->setViewCenter(tile); -} - -void MapView::onCenteredObject(const CGObjectInstance * target) -{ - controller->setViewCenter(target->getSightCenter()); -} - -void MapView::onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain) -{ - controller->activateSpellViewContext(); - controller->setTileSize(Point(tileSize, tileSize)); - controller->setOverlayVisibility(objectPositions); - controller->setTerrainVisibility(showTerrain); -} - -void MapView::onViewWorldActivated(uint32_t tileSize) -{ - controller->activateWorldViewContext(); - controller->setTileSize(Point(tileSize, tileSize)); -} - -void MapView::onMapZoomLevelChanged(int stepsChange) -{ - controller->modifyTileSize(stepsChange); -} - -void MapView::onViewMapActivated() -{ - controller->activateAdventureContext(); - controller->setTileSize(Point(32, 32)); -} - -PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter) - : BasicMapView(offset, dimensions) -{ - controller->activatePuzzleMapContext(tileToCenter); - controller->setViewCenter(tileToCenter); - -} +/* + * MapView.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 "MapView.h" + +#include "MapViewActions.h" +#include "MapViewCache.h" +#include "MapViewController.h" +#include "MapViewModel.h" +#include "mapHandler.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../renderSDL/SDL_Extensions.h" +#include "../eventsSDL/InputHandler.h" + +#include "../../CCallback.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +BasicMapView::~BasicMapView() = default; + +std::shared_ptr BasicMapView::createModel(const Point & dimensions) const +{ + auto result = std::make_shared(); + + result->setLevel(0); + result->setTileSize(Point(32, 32)); + result->setViewCenter(Point(0, 0)); + result->setViewDimensions(dimensions); + + return result; +} + +BasicMapView::BasicMapView(const Point & offset, const Point & dimensions) + : model(createModel(dimensions)) + , tilesCache(new MapViewCache(model)) + , controller(new MapViewController(model, tilesCache)) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + pos += offset; + pos.w = dimensions.x; + pos.h = dimensions.y; +} + +void BasicMapView::render(Canvas & target, bool fullUpdate) +{ + Canvas targetClipped(target, pos); + tilesCache->update(controller->getContext()); + tilesCache->render(controller->getContext(), targetClipped, fullUpdate); +} + +void BasicMapView::tick(uint32_t msPassed) +{ + controller->tick(msPassed); +} + +void BasicMapView::show(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + render(to, false); + + controller->afterRender(); +} + +void BasicMapView::showAll(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + render(to, true); +} + +void MapView::tick(uint32_t msPassed) +{ + if(settings["adventure"]["smoothDragging"].Bool()) + postSwipe(msPassed); + + BasicMapView::tick(msPassed); +} + +void MapView::show(Canvas & to) +{ + actions->setContext(controller->getContext()); + BasicMapView::show(to); +} + +MapView::MapView(const Point & offset, const Point & dimensions) + : BasicMapView(offset, dimensions) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + actions = std::make_shared(*this, model); + actions->setContext(controller->getContext()); + + // catch min 6 frames + postSwipeCatchIntervalMs = std::max(100, static_cast(6.0 * 1000.0 * (1.0 / settings["video"]["targetfps"].Float()))); +} + +void MapView::onMapLevelSwitched() +{ + if(LOCPLINT->cb->getMapSize().z > 1) + { + if(model->getLevel() == 0) + controller->setViewCenter(model->getMapViewCenter(), 1); + else + controller->setViewCenter(model->getMapViewCenter(), 0); + } +} + +void MapView::onMapScrolled(const Point & distance) +{ + if(!isGesturing()) + controller->setViewCenter(model->getMapViewCenter() + distance, model->getLevel()); +} + +void MapView::onMapSwiped(const Point & viewPosition) +{ + if(settings["adventure"]["smoothDragging"].Bool()) + swipeHistory.push_back(std::pair(GH.input().getTicks(), viewPosition)); + + controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel()); +} + +void MapView::postSwipe(uint32_t msPassed) +{ + if(!actions->dragActive) + { + if(swipeHistory.size() > 1) + { + Point diff = Point(0, 0); + std::pair firstAccepted; + uint32_t now = GH.input().getTicks(); + for (auto & x : swipeHistory) { + if(now - x.first < postSwipeCatchIntervalMs) { // only the last x ms are catched + if(firstAccepted.first == 0) + firstAccepted = x; + diff += x.second; + } + } + + uint32_t timediff = swipeHistory.back().first - firstAccepted.first; + + if(diff.length() > 0 && timediff > 0) + { + postSwipeAngle = diff.angle(); + postSwipeSpeed = static_cast(diff.length()) / static_cast(timediff); // unit: pixel/millisecond + } + } + swipeHistory.clear(); + } else + postSwipeSpeed = 0.0; + if(postSwipeSpeed > postSwipeMinimalSpeed) { + double len = postSwipeSpeed * static_cast(msPassed); + Point delta = Point(len * cos(postSwipeAngle), len * sin(postSwipeAngle)); + + controller->setViewCenter(model->getMapViewCenter() + delta, model->getLevel()); + + postSwipeSpeed /= 1 + msPassed * postSwipeSlowdownSpeed; + } +} + +void MapView::onCenteredTile(const int3 & tile) +{ + controller->setViewCenter(tile); +} + +void MapView::onCenteredObject(const CGObjectInstance * target) +{ + controller->setViewCenter(target->getSightCenter()); +} + +void MapView::onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain) +{ + controller->activateSpellViewContext(); + controller->setTileSize(Point(tileSize, tileSize)); + controller->setOverlayVisibility(objectPositions); + controller->setTerrainVisibility(showTerrain); +} + +void MapView::onViewWorldActivated(uint32_t tileSize) +{ + controller->activateWorldViewContext(); + controller->setTileSize(Point(tileSize, tileSize)); +} + +void MapView::onMapZoomLevelChanged(int stepsChange) +{ + controller->modifyTileSize(stepsChange); +} + +void MapView::onViewMapActivated() +{ + controller->activateAdventureContext(); + controller->setTileSize(Point(32, 32)); +} + +PuzzleMapView::PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter) + : BasicMapView(offset, dimensions) +{ + controller->activatePuzzleMapContext(tileToCenter); + controller->setViewCenter(tileToCenter); + +} diff --git a/client/mapView/MapView.h b/client/mapView/MapView.h index c95839638..f83b9d319 100644 --- a/client/mapView/MapView.h +++ b/client/mapView/MapView.h @@ -1,101 +1,101 @@ -/* - * MapView.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 "../gui/CIntObject.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -class CGObjectInstance; -VCMI_LIB_NAMESPACE_END - -class Canvas; -class MapViewActions; -class MapViewController; -class MapViewModel; -class MapViewCache; - -/// Internal class that contains logic shared between all map views -class BasicMapView : public CIntObject -{ -protected: - std::shared_ptr model; - std::shared_ptr tilesCache; - std::shared_ptr controller; - - std::shared_ptr createModel(const Point & dimensions) const; - - void render(Canvas & target, bool fullUpdate); - -public: - BasicMapView(const Point & offset, const Point & dimensions); - ~BasicMapView() override; - - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; -}; - -/// Main class that represents visible section of adventure map -/// Contains all public interface of view and translates calls to internal model -class MapView : public BasicMapView -{ - std::shared_ptr actions; - - std::vector> swipeHistory; - double postSwipeAngle = 0.0; - double postSwipeSpeed = 0.0; - - int postSwipeCatchIntervalMs; - const double postSwipeSlowdownSpeed = 0.006; - const double postSwipeMinimalSpeed = 0.1; - - void postSwipe(uint32_t msPassed); - -public: - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - - MapView(const Point & offset, const Point & dimensions); - - /// Moves current view to another level, preserving position - void onMapLevelSwitched(); - - /// Moves current view by specified distance in pixels - void onMapScrolled(const Point & distance); - - /// Moves current view to specified position, in pixels - void onMapSwiped(const Point & viewPosition); - - /// Moves current view to specified tile - void onCenteredTile(const int3 & tile); - - /// Moves current view to specified object - void onCenteredObject(const CGObjectInstance * target); - - /// Switches view to "View Earth" / "View Air" mode, displaying downscaled map with overlay - void onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain); - - /// Switches view to downscaled View World - void onViewWorldActivated(uint32_t tileSize); - - /// Changes zoom level / tile size of current view by specified factor - void onMapZoomLevelChanged(int stepsChange); - - /// Switches view from View World mode back to standard view - void onViewMapActivated(); -}; - -/// Main class that represents map view for puzzle map -class PuzzleMapView : public BasicMapView -{ -public: - PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter); -}; +/* + * MapView.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 "../gui/CIntObject.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class Canvas; +class MapViewActions; +class MapViewController; +class MapViewModel; +class MapViewCache; + +/// Internal class that contains logic shared between all map views +class BasicMapView : public CIntObject +{ +protected: + std::shared_ptr model; + std::shared_ptr tilesCache; + std::shared_ptr controller; + + std::shared_ptr createModel(const Point & dimensions) const; + + void render(Canvas & target, bool fullUpdate); + +public: + BasicMapView(const Point & offset, const Point & dimensions); + ~BasicMapView() override; + + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; +}; + +/// Main class that represents visible section of adventure map +/// Contains all public interface of view and translates calls to internal model +class MapView : public BasicMapView +{ + std::shared_ptr actions; + + std::vector> swipeHistory; + double postSwipeAngle = 0.0; + double postSwipeSpeed = 0.0; + + int postSwipeCatchIntervalMs; + const double postSwipeSlowdownSpeed = 0.006; + const double postSwipeMinimalSpeed = 0.1; + + void postSwipe(uint32_t msPassed); + +public: + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + + MapView(const Point & offset, const Point & dimensions); + + /// Moves current view to another level, preserving position + void onMapLevelSwitched(); + + /// Moves current view by specified distance in pixels + void onMapScrolled(const Point & distance); + + /// Moves current view to specified position, in pixels + void onMapSwiped(const Point & viewPosition); + + /// Moves current view to specified tile + void onCenteredTile(const int3 & tile); + + /// Moves current view to specified object + void onCenteredObject(const CGObjectInstance * target); + + /// Switches view to "View Earth" / "View Air" mode, displaying downscaled map with overlay + void onViewSpellActivated(uint32_t tileSize, const std::vector & objectPositions, bool showTerrain); + + /// Switches view to downscaled View World + void onViewWorldActivated(uint32_t tileSize); + + /// Changes zoom level / tile size of current view by specified factor + void onMapZoomLevelChanged(int stepsChange); + + /// Switches view from View World mode back to standard view + void onViewMapActivated(); +}; + +/// Main class that represents map view for puzzle map +class PuzzleMapView : public BasicMapView +{ +public: + PuzzleMapView(const Point & offset, const Point & dimensions, const int3 & tileToCenter); +}; diff --git a/client/mapView/MapViewActions.cpp b/client/mapView/MapViewActions.cpp index 6f71182f1..68cb692fd 100644 --- a/client/mapView/MapViewActions.cpp +++ b/client/mapView/MapViewActions.cpp @@ -1,149 +1,149 @@ -/* - * MapViewActions.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 "MapViewActions.h" - -#include "IMapRendererContext.h" -#include "MapView.h" -#include "MapViewModel.h" - -#include "../CGameInfo.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/MouseButton.h" - -#include "../CPlayerInterface.h" -#include "../adventureMap/CInGameConsole.h" - -#include "../../lib/CConfigHandler.h" - -MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr & model) - : model(model) - , owner(owner) - , pinchZoomFactor(1.0) - , dragActive(false) -{ - pos.w = model->getPixelsVisibleDimensions().x; - pos.h = model->getPixelsVisibleDimensions().y; - - addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); -} - -void MapViewActions::setContext(const std::shared_ptr & context) -{ - this->context = context; -} - -void MapViewActions::clickPressed(const Point & cursorPosition) -{ - if (!settings["adventure"]["leftButtonDrag"].Bool()) - { - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileLeftClicked(tile); - } -} - -void MapViewActions::clickReleased(const Point & cursorPosition) -{ - if (!dragActive && settings["adventure"]["leftButtonDrag"].Bool()) - { - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileLeftClicked(tile); - } - dragActive = false; - dragDistance = Point(0,0); -} - -void MapViewActions::clickCancel(const Point & cursorPosition) -{ - dragActive = false; - dragDistance = Point(0,0); -} - -void MapViewActions::showPopupWindow(const Point & cursorPosition) -{ - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(context->isInMap(tile)) - adventureInt->onTileRightClicked(tile); -} - -void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - handleHover(cursorPosition); -} - -void MapViewActions::wheelScrolled(int distance) -{ - adventureInt->hotkeyZoom(distance * 4); -} - -void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) -{ - dragDistance += lastUpdateDistance; - - if (dragDistance.length() > 16) - dragActive = true; - - if (dragActive && settings["adventure"]["leftButtonDrag"].Bool()) - owner.onMapSwiped(lastUpdateDistance); -} - -void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) -{ - owner.onMapSwiped(lastUpdateDistance); -} - -void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdateFactor) -{ - double newZoom = pinchZoomFactor * lastUpdateFactor; - - int newZoomSteps = std::round(std::log(newZoom) / std::log(1.01)); - int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01)); - - if (newZoomSteps != oldZoomSteps) - adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps); - - pinchZoomFactor = newZoom; -} - -void MapViewActions::gesture(bool on, const Point & initialPosition, const Point & finalPosition) -{ - dragActive = on; - - pinchZoomFactor = 1.0; -} - -void MapViewActions::handleHover(const Point & cursorPosition) -{ - int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); - - if(!context->isInMap(tile)) - { - CCS->curh->set(Cursor::Map::POINTER); - return; - } - - adventureInt->onTileHovered(tile); -} - -void MapViewActions::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - CCS->curh->set(Cursor::Map::POINTER); - } -} +/* + * MapViewActions.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 "MapViewActions.h" + +#include "IMapRendererContext.h" +#include "MapView.h" +#include "MapViewModel.h" + +#include "../CGameInfo.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/MouseButton.h" + +#include "../CPlayerInterface.h" +#include "../adventureMap/CInGameConsole.h" + +#include "../../lib/CConfigHandler.h" + +MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr & model) + : model(model) + , owner(owner) + , pinchZoomFactor(1.0) + , dragActive(false) +{ + pos.w = model->getPixelsVisibleDimensions().x; + pos.h = model->getPixelsVisibleDimensions().y; + + addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL); +} + +void MapViewActions::setContext(const std::shared_ptr & context) +{ + this->context = context; +} + +void MapViewActions::clickPressed(const Point & cursorPosition) +{ + if (!settings["adventure"]["leftButtonDrag"].Bool()) + { + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileLeftClicked(tile); + } +} + +void MapViewActions::clickReleased(const Point & cursorPosition) +{ + if (!dragActive && settings["adventure"]["leftButtonDrag"].Bool()) + { + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileLeftClicked(tile); + } + dragActive = false; + dragDistance = Point(0,0); +} + +void MapViewActions::clickCancel(const Point & cursorPosition) +{ + dragActive = false; + dragDistance = Point(0,0); +} + +void MapViewActions::showPopupWindow(const Point & cursorPosition) +{ + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(context->isInMap(tile)) + adventureInt->onTileRightClicked(tile); +} + +void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + handleHover(cursorPosition); +} + +void MapViewActions::wheelScrolled(int distance) +{ + adventureInt->hotkeyZoom(distance * 4); +} + +void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) +{ + dragDistance += lastUpdateDistance; + + if (dragDistance.length() > 16) + dragActive = true; + + if (dragActive && settings["adventure"]["leftButtonDrag"].Bool()) + owner.onMapSwiped(lastUpdateDistance); +} + +void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) +{ + owner.onMapSwiped(lastUpdateDistance); +} + +void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdateFactor) +{ + double newZoom = pinchZoomFactor * lastUpdateFactor; + + int newZoomSteps = std::round(std::log(newZoom) / std::log(1.01)); + int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01)); + + if (newZoomSteps != oldZoomSteps) + adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps); + + pinchZoomFactor = newZoom; +} + +void MapViewActions::gesture(bool on, const Point & initialPosition, const Point & finalPosition) +{ + dragActive = on; + + pinchZoomFactor = 1.0; +} + +void MapViewActions::handleHover(const Point & cursorPosition) +{ + int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft()); + + if(!context->isInMap(tile)) + { + CCS->curh->set(Cursor::Map::POINTER); + return; + } + + adventureInt->onTileHovered(tile); +} + +void MapViewActions::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + CCS->curh->set(Cursor::Map::POINTER); + } +} diff --git a/client/mapView/MapViewActions.h b/client/mapView/MapViewActions.h index c78e8de40..02dd877aa 100644 --- a/client/mapView/MapViewActions.h +++ b/client/mapView/MapViewActions.h @@ -1,48 +1,48 @@ -/* - * MapViewActions.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/int3.h" -#include "../gui/CIntObject.h" - -class IMapRendererContext; -class MapViewModel; -class MapView; - -class MapViewActions : public CIntObject -{ - MapView & owner; - std::shared_ptr model; - std::shared_ptr context; - - Point dragDistance; - double pinchZoomFactor; - - void handleHover(const Point & cursorPosition); - -public: - MapViewActions(MapView & owner, const std::shared_ptr & model); - - void setContext(const std::shared_ptr & context); - - void clickPressed(const Point & cursorPosition) override; - void clickReleased(const Point & cursorPosition) override; - void clickCancel(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; - void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override; - void hover(bool on) override; - void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; - void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; - void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override; - void wheelScrolled(int distance) override; - - bool dragActive; -}; +/* + * MapViewActions.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/int3.h" +#include "../gui/CIntObject.h" + +class IMapRendererContext; +class MapViewModel; +class MapView; + +class MapViewActions : public CIntObject +{ + MapView & owner; + std::shared_ptr model; + std::shared_ptr context; + + Point dragDistance; + double pinchZoomFactor; + + void handleHover(const Point & cursorPosition); + +public: + MapViewActions(MapView & owner, const std::shared_ptr & model); + + void setContext(const std::shared_ptr & context); + + void clickPressed(const Point & cursorPosition) override; + void clickReleased(const Point & cursorPosition) override; + void clickCancel(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override; + void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override; + void hover(bool on) override; + void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override; + void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override; + void wheelScrolled(int distance) override; + + bool dragActive; +}; diff --git a/client/mapView/MapViewCache.cpp b/client/mapView/MapViewCache.cpp index cb69c9fdf..ad851ef11 100644 --- a/client/mapView/MapViewCache.cpp +++ b/client/mapView/MapViewCache.cpp @@ -1,197 +1,197 @@ -/* - * MapViewCache.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 "MapViewCache.h" - -#include "IMapRendererContext.h" -#include "MapRenderer.h" -#include "MapViewModel.h" - -#include "../render/CAnimation.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" - -#include "../gui/CGuiHandler.h" - -#include "../../lib/mapObjects/CObjectHandler.h" -#include "../../lib/int3.h" - -MapViewCache::~MapViewCache() = default; - -MapViewCache::MapViewCache(const std::shared_ptr & model) - : model(model) - , cachedLevel(0) - , mapRenderer(new MapRenderer()) - , iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"))) - , intermediate(new Canvas(Point(32, 32))) - , terrain(new Canvas(model->getCacheDimensionsPixels())) - , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) -{ - iconsStorage->preload(); - for(size_t i = 0; i < iconsStorage->size(); ++i) - iconsStorage->getImage(i)->setBlitMode(EImageBlitMode::COLORKEY); - - Point visibleSize = model->getTilesVisibleDimensions(); - terrainChecksum.resize(boost::extents[visibleSize.x][visibleSize.y]); - tilesUpToDate.resize(boost::extents[visibleSize.x][visibleSize.y]); -} - -Canvas MapViewCache::getTile(const int3 & coordinates) -{ - return Canvas(*terrain, model->getCacheTileArea(coordinates)); -} - -std::shared_ptr MapViewCache::getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates) -{ - size_t imageIndex = context->overlayImageIndex(coordinates); - - if(imageIndex < iconsStorage->size()) - return iconsStorage->getImage(imageIndex); - return nullptr; -} - -void MapViewCache::invalidate(const std::shared_ptr & context, const ObjectInstanceID & object) -{ - for(size_t cacheY = 0; cacheY < terrainChecksum.shape()[1]; ++cacheY) - { - for(size_t cacheX = 0; cacheX < terrainChecksum.shape()[0]; ++cacheX) - { - auto & entry = terrainChecksum[cacheX][cacheY]; - - int3 tile(entry.tileX, entry.tileY, cachedLevel); - - if(context->isInMap(tile) && vstd::contains(context->getObjects(tile), object)) - entry = TileChecksum{}; - } - } -} - -void MapViewCache::updateTile(const std::shared_ptr & context, const int3 & coordinates) -{ - int cacheX = (terrainChecksum.shape()[0] + coordinates.x) % terrainChecksum.shape()[0]; - int cacheY = (terrainChecksum.shape()[1] + coordinates.y) % terrainChecksum.shape()[1]; - - auto & oldCacheEntry = terrainChecksum[cacheX][cacheY]; - TileChecksum newCacheEntry; - - newCacheEntry.tileX = coordinates.x; - newCacheEntry.tileY = coordinates.y; - newCacheEntry.checksum = mapRenderer->getTileChecksum(*context, coordinates); - - if(cachedLevel == coordinates.z && oldCacheEntry == newCacheEntry && !context->tileAnimated(coordinates)) - return; - - Canvas target = getTile(coordinates); - - if(model->getSingleTileSize() == Point(32, 32)) - { - mapRenderer->renderTile(*context, target, coordinates); - } - else - { - mapRenderer->renderTile(*context, *intermediate, coordinates); - target.drawScaled(*intermediate, Point(0, 0), model->getSingleTileSize()); - } - - if(context->filterGrayscale()) - target.applyGrayscale(); - - oldCacheEntry = newCacheEntry; - tilesUpToDate[cacheX][cacheY] = false; -} - -void MapViewCache::update(const std::shared_ptr & context) -{ - Rect dimensions = model->getTilesTotalRect(); - bool mapResized = cachedSize != model->getSingleTileSize(); - - if(mapResized || dimensions.w != terrainChecksum.shape()[0] || dimensions.h != terrainChecksum.shape()[1]) - { - boost::multi_array newCache; - newCache.resize(boost::extents[dimensions.w][dimensions.h]); - terrainChecksum.resize(boost::extents[dimensions.w][dimensions.h]); - terrainChecksum = newCache; - } - - if(mapResized || dimensions.w != tilesUpToDate.shape()[0] || dimensions.h != tilesUpToDate.shape()[1]) - { - boost::multi_array newCache; - newCache.resize(boost::extents[dimensions.w][dimensions.h]); - tilesUpToDate.resize(boost::extents[dimensions.w][dimensions.h]); - tilesUpToDate = newCache; - } - - for(int y = dimensions.top(); y < dimensions.bottom(); ++y) - for(int x = dimensions.left(); x < dimensions.right(); ++x) - updateTile(context, {x, y, model->getLevel()}); - - cachedSize = model->getSingleTileSize(); - cachedLevel = model->getLevel(); -} - -void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) -{ - bool mapMoved = (cachedPosition != model->getMapViewCenter()); - bool lazyUpdate = !mapMoved && !fullRedraw && context->viewTransitionProgress() == 0; - - Rect dimensions = model->getTilesTotalRect(); - - for(int y = dimensions.top(); y < dimensions.bottom(); ++y) - { - for(int x = dimensions.left(); x < dimensions.right(); ++x) - { - int cacheX = (terrainChecksum.shape()[0] + x) % terrainChecksum.shape()[0]; - int cacheY = (terrainChecksum.shape()[1] + y) % terrainChecksum.shape()[1]; - int3 tile(x, y, model->getLevel()); - - if(lazyUpdate && tilesUpToDate[cacheX][cacheY]) - continue; - - Canvas source = getTile(tile); - Rect targetRect = model->getTargetTileArea(tile); - target.draw(source, targetRect.topLeft()); - - if (!fullRedraw) - tilesUpToDate[cacheX][cacheY] = true; - } - } - - if(context->showOverlay()) - { - for(int y = dimensions.top(); y < dimensions.bottom(); ++y) - { - for(int x = dimensions.left(); x < dimensions.right(); ++x) - { - int3 tile(x, y, model->getLevel()); - Rect targetRect = model->getTargetTileArea(tile); - auto overlay = getOverlayImageForTile(context, tile); - - if(overlay) - { - Point position = targetRect.center() - overlay->dimensions() / 2; - target.draw(overlay, position); - } - } - } - } - - if(context->viewTransitionProgress() != 0) - target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); - - cachedPosition = model->getMapViewCenter(); -} - -void MapViewCache::createTransitionSnapshot(const std::shared_ptr & context) -{ - update(context); - render(context, *terrainTransition, true); -} +/* + * MapViewCache.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 "MapViewCache.h" + +#include "IMapRendererContext.h" +#include "MapRenderer.h" +#include "MapViewModel.h" + +#include "../render/CAnimation.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" + +#include "../gui/CGuiHandler.h" + +#include "../../lib/mapObjects/CObjectHandler.h" +#include "../../lib/int3.h" + +MapViewCache::~MapViewCache() = default; + +MapViewCache::MapViewCache(const std::shared_ptr & model) + : model(model) + , cachedLevel(0) + , mapRenderer(new MapRenderer()) + , iconsStorage(GH.renderHandler().loadAnimation(AnimationPath::builtin("VwSymbol"))) + , intermediate(new Canvas(Point(32, 32))) + , terrain(new Canvas(model->getCacheDimensionsPixels())) + , terrainTransition(new Canvas(model->getPixelsVisibleDimensions())) +{ + iconsStorage->preload(); + for(size_t i = 0; i < iconsStorage->size(); ++i) + iconsStorage->getImage(i)->setBlitMode(EImageBlitMode::COLORKEY); + + Point visibleSize = model->getTilesVisibleDimensions(); + terrainChecksum.resize(boost::extents[visibleSize.x][visibleSize.y]); + tilesUpToDate.resize(boost::extents[visibleSize.x][visibleSize.y]); +} + +Canvas MapViewCache::getTile(const int3 & coordinates) +{ + return Canvas(*terrain, model->getCacheTileArea(coordinates)); +} + +std::shared_ptr MapViewCache::getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates) +{ + size_t imageIndex = context->overlayImageIndex(coordinates); + + if(imageIndex < iconsStorage->size()) + return iconsStorage->getImage(imageIndex); + return nullptr; +} + +void MapViewCache::invalidate(const std::shared_ptr & context, const ObjectInstanceID & object) +{ + for(size_t cacheY = 0; cacheY < terrainChecksum.shape()[1]; ++cacheY) + { + for(size_t cacheX = 0; cacheX < terrainChecksum.shape()[0]; ++cacheX) + { + auto & entry = terrainChecksum[cacheX][cacheY]; + + int3 tile(entry.tileX, entry.tileY, cachedLevel); + + if(context->isInMap(tile) && vstd::contains(context->getObjects(tile), object)) + entry = TileChecksum{}; + } + } +} + +void MapViewCache::updateTile(const std::shared_ptr & context, const int3 & coordinates) +{ + int cacheX = (terrainChecksum.shape()[0] + coordinates.x) % terrainChecksum.shape()[0]; + int cacheY = (terrainChecksum.shape()[1] + coordinates.y) % terrainChecksum.shape()[1]; + + auto & oldCacheEntry = terrainChecksum[cacheX][cacheY]; + TileChecksum newCacheEntry; + + newCacheEntry.tileX = coordinates.x; + newCacheEntry.tileY = coordinates.y; + newCacheEntry.checksum = mapRenderer->getTileChecksum(*context, coordinates); + + if(cachedLevel == coordinates.z && oldCacheEntry == newCacheEntry && !context->tileAnimated(coordinates)) + return; + + Canvas target = getTile(coordinates); + + if(model->getSingleTileSize() == Point(32, 32)) + { + mapRenderer->renderTile(*context, target, coordinates); + } + else + { + mapRenderer->renderTile(*context, *intermediate, coordinates); + target.drawScaled(*intermediate, Point(0, 0), model->getSingleTileSize()); + } + + if(context->filterGrayscale()) + target.applyGrayscale(); + + oldCacheEntry = newCacheEntry; + tilesUpToDate[cacheX][cacheY] = false; +} + +void MapViewCache::update(const std::shared_ptr & context) +{ + Rect dimensions = model->getTilesTotalRect(); + bool mapResized = cachedSize != model->getSingleTileSize(); + + if(mapResized || dimensions.w != terrainChecksum.shape()[0] || dimensions.h != terrainChecksum.shape()[1]) + { + boost::multi_array newCache; + newCache.resize(boost::extents[dimensions.w][dimensions.h]); + terrainChecksum.resize(boost::extents[dimensions.w][dimensions.h]); + terrainChecksum = newCache; + } + + if(mapResized || dimensions.w != tilesUpToDate.shape()[0] || dimensions.h != tilesUpToDate.shape()[1]) + { + boost::multi_array newCache; + newCache.resize(boost::extents[dimensions.w][dimensions.h]); + tilesUpToDate.resize(boost::extents[dimensions.w][dimensions.h]); + tilesUpToDate = newCache; + } + + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + for(int x = dimensions.left(); x < dimensions.right(); ++x) + updateTile(context, {x, y, model->getLevel()}); + + cachedSize = model->getSingleTileSize(); + cachedLevel = model->getLevel(); +} + +void MapViewCache::render(const std::shared_ptr & context, Canvas & target, bool fullRedraw) +{ + bool mapMoved = (cachedPosition != model->getMapViewCenter()); + bool lazyUpdate = !mapMoved && !fullRedraw && context->viewTransitionProgress() == 0; + + Rect dimensions = model->getTilesTotalRect(); + + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + { + for(int x = dimensions.left(); x < dimensions.right(); ++x) + { + int cacheX = (terrainChecksum.shape()[0] + x) % terrainChecksum.shape()[0]; + int cacheY = (terrainChecksum.shape()[1] + y) % terrainChecksum.shape()[1]; + int3 tile(x, y, model->getLevel()); + + if(lazyUpdate && tilesUpToDate[cacheX][cacheY]) + continue; + + Canvas source = getTile(tile); + Rect targetRect = model->getTargetTileArea(tile); + target.draw(source, targetRect.topLeft()); + + if (!fullRedraw) + tilesUpToDate[cacheX][cacheY] = true; + } + } + + if(context->showOverlay()) + { + for(int y = dimensions.top(); y < dimensions.bottom(); ++y) + { + for(int x = dimensions.left(); x < dimensions.right(); ++x) + { + int3 tile(x, y, model->getLevel()); + Rect targetRect = model->getTargetTileArea(tile); + auto overlay = getOverlayImageForTile(context, tile); + + if(overlay) + { + Point position = targetRect.center() - overlay->dimensions() / 2; + target.draw(overlay, position); + } + } + } + } + + if(context->viewTransitionProgress() != 0) + target.drawTransparent(*terrainTransition, Point(0, 0), 1.0 - context->viewTransitionProgress()); + + cachedPosition = model->getMapViewCenter(); +} + +void MapViewCache::createTransitionSnapshot(const std::shared_ptr & context) +{ + update(context); + render(context, *terrainTransition, true); +} diff --git a/client/mapView/MapViewCache.h b/client/mapView/MapViewCache.h index 01b09a32e..0435d3584 100644 --- a/client/mapView/MapViewCache.h +++ b/client/mapView/MapViewCache.h @@ -1,78 +1,78 @@ -/* - * MapViewCache.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/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN -class ObjectInstanceID; -VCMI_LIB_NAMESPACE_END - -class IImage; -class CAnimation; -class Canvas; -class MapRenderer; -class IMapRendererContext; -class MapViewModel; - -/// Class responsible for rendering of entire map view -/// uses rendering parameters provided by owner class -class MapViewCache -{ - struct TileChecksum - { - int tileX = std::numeric_limits::min(); - int tileY = std::numeric_limits::min(); - std::array checksum{}; - - bool operator==(const TileChecksum & other) const - { - return tileX == other.tileX && tileY == other.tileY && checksum == other.checksum; - } - }; - - boost::multi_array terrainChecksum; - boost::multi_array tilesUpToDate; - - Point cachedSize; - Point cachedPosition; - int cachedLevel; - - std::shared_ptr model; - - std::unique_ptr terrain; - std::unique_ptr terrainTransition; - std::unique_ptr intermediate; - std::unique_ptr mapRenderer; - - std::shared_ptr iconsStorage; - - Canvas getTile(const int3 & coordinates); - void updateTile(const std::shared_ptr & context, const int3 & coordinates); - - std::shared_ptr getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates); - -public: - explicit MapViewCache(const std::shared_ptr & model); - ~MapViewCache(); - - /// invalidates cache of specified object - void invalidate(const std::shared_ptr & context, const ObjectInstanceID & object); - - /// updates internal terrain cache according to provided time delta - void update(const std::shared_ptr & context); - - /// renders updated terrain cache onto provided canvas - void render(const std::shared_ptr & context, Canvas & target, bool fullRedraw); - - /// creates snapshot of current view and stores it into internal canvas - /// used for view transition, e.g. Dimension Door spell or teleporters (Subterra gates / Monolith) - void createTransitionSnapshot(const std::shared_ptr & context); -}; +/* + * MapViewCache.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/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +class ObjectInstanceID; +VCMI_LIB_NAMESPACE_END + +class IImage; +class CAnimation; +class Canvas; +class MapRenderer; +class IMapRendererContext; +class MapViewModel; + +/// Class responsible for rendering of entire map view +/// uses rendering parameters provided by owner class +class MapViewCache +{ + struct TileChecksum + { + int tileX = std::numeric_limits::min(); + int tileY = std::numeric_limits::min(); + std::array checksum{}; + + bool operator==(const TileChecksum & other) const + { + return tileX == other.tileX && tileY == other.tileY && checksum == other.checksum; + } + }; + + boost::multi_array terrainChecksum; + boost::multi_array tilesUpToDate; + + Point cachedSize; + Point cachedPosition; + int cachedLevel; + + std::shared_ptr model; + + std::unique_ptr terrain; + std::unique_ptr terrainTransition; + std::unique_ptr intermediate; + std::unique_ptr mapRenderer; + + std::shared_ptr iconsStorage; + + Canvas getTile(const int3 & coordinates); + void updateTile(const std::shared_ptr & context, const int3 & coordinates); + + std::shared_ptr getOverlayImageForTile(const std::shared_ptr & context, const int3 & coordinates); + +public: + explicit MapViewCache(const std::shared_ptr & model); + ~MapViewCache(); + + /// invalidates cache of specified object + void invalidate(const std::shared_ptr & context, const ObjectInstanceID & object); + + /// updates internal terrain cache according to provided time delta + void update(const std::shared_ptr & context); + + /// renders updated terrain cache onto provided canvas + void render(const std::shared_ptr & context, Canvas & target, bool fullRedraw); + + /// creates snapshot of current view and stores it into internal canvas + /// used for view transition, e.g. Dimension Door spell or teleporters (Subterra gates / Monolith) + void createTransitionSnapshot(const std::shared_ptr & context); +}; diff --git a/client/mapView/MapViewController.cpp b/client/mapView/MapViewController.cpp index 424c31518..d710dc6d2 100644 --- a/client/mapView/MapViewController.cpp +++ b/client/mapView/MapViewController.cpp @@ -1,639 +1,639 @@ -/* - * MapViewController.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 "MapViewController.h" - -#include "MapRendererContext.h" -#include "MapRendererContextState.h" -#include "MapViewCache.h" -#include "MapViewModel.h" - -#include "../CPlayerInterface.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../gui/CGuiHandler.h" -#include "../gui/WindowHandler.h" -#include "../eventsSDL/InputHandler.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/MiscObjects.h" -#include "../../lib/pathfinder/CGPathNode.h" -#include "../../lib/spells/ViewSpellInt.h" - -void MapViewController::setViewCenter(const int3 & position) -{ - setViewCenter(Point(position) * model->getSingleTileSize() + model->getSingleTileSize() / 2, position.z); -} - -void MapViewController::setViewCenter(const Point & position, int level) -{ - Point upperLimit = Point(context->getMapSize()) * model->getSingleTileSize(); - Point lowerLimit = Point(0, 0); - - if(worldViewContext) - { - Point area = model->getPixelsVisibleDimensions(); - Point mapCenter = upperLimit / 2; - - Point desiredLowerLimit = lowerLimit + area / 2; - Point desiredUpperLimit = upperLimit - area / 2; - - Point actualLowerLimit{ - std::min(desiredLowerLimit.x, mapCenter.x), - std::min(desiredLowerLimit.y, mapCenter.y) - }; - - Point actualUpperLimit{ - std::max(desiredUpperLimit.x, mapCenter.x), - std::max(desiredUpperLimit.y, mapCenter.y) - }; - - upperLimit = actualUpperLimit; - lowerLimit = actualLowerLimit; - } - - Point betterPosition = {std::clamp(position.x, lowerLimit.x, upperLimit.x), std::clamp(position.y, lowerLimit.y, upperLimit.y)}; - - model->setViewCenter(betterPosition); - model->setLevel(std::clamp(level, 0, context->getMapSize().z)); - - if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized - adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel()); -} - -void MapViewController::setTileSize(const Point & tileSize) -{ - Point oldSize = model->getSingleTileSize(); - model->setTileSize(tileSize); - - double scaleChangeX = 1.0 * tileSize.x / oldSize.x; - double scaleChangeY = 1.0 * tileSize.y / oldSize.y; - - Point newViewCenter { - static_cast(std::round(model->getMapViewCenter().x * scaleChangeX)), - static_cast(std::round(model->getMapViewCenter().y * scaleChangeY)) - }; - - // force update of view center since changing tile size may invalidated it - setViewCenter(newViewCenter, model->getLevel()); -} - -void MapViewController::modifyTileSize(int stepsChange) -{ - // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling - // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale - // try to determine current zooming level and change it by requested number of steps - double currentZoomFactor = targetTileSize.x / static_cast(defaultTileSize); - double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); - double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; - double newZoomFactor = std::pow(1.01, newZoomSteps); - - Point currentZoom = targetTileSize; - Point desiredZoom = Point(defaultTileSize,defaultTileSize) * newZoomFactor; - - if (desiredZoom == currentZoom && stepsChange < 0) - desiredZoom -= Point(1,1); - - if (desiredZoom == currentZoom && stepsChange > 0) - desiredZoom += Point(1,1); - - Point minimal = model->getSingleTileSizeLowerLimit(); - Point maximal = model->getSingleTileSizeUpperLimit(); - Point actualZoom = { - std::clamp(desiredZoom.x, minimal.x, maximal.x), - std::clamp(desiredZoom.y, minimal.y, maximal.y) - }; - - if (actualZoom != currentZoom) - { - targetTileSize = actualZoom; - if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea) - actualZoom.x = defaultTileSize; - if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) - actualZoom.y = defaultTileSize; - - bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize); - - if(!wasInDeadZone && isInDeadZone) - GH.input().hapticFeedback(); - - wasInDeadZone = isInDeadZone; - - setTileSize(actualZoom); - } -} - -MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) - : state(new MapRendererContextState()) - , model(std::move(model)) - , view(view) -{ - adventureContext = std::make_shared(*state); - context = adventureContext; -} - -std::shared_ptr MapViewController::getContext() const -{ - return context; -} - -void MapViewController::tick(uint32_t timeDelta) -{ - // confirmed to match H3 for - // - hero embarking on boat (500 ms) - // - hero disembarking from boat (500 ms) - // - TODO: picking up resources - // - TODO: killing mosters - // - teleporting ( 250 ms) - static const double fadeOutDuration = 500; - static const double fadeInDuration = 500; - static const double heroTeleportDuration = 250; - - if(movementContext) - { - const auto * object = context->getObject(movementContext->target); - const auto * hero = dynamic_cast(object); - const auto * boat = dynamic_cast(object); - - assert(boat || hero); - - if(!hero) - hero = boat->hero; - - double heroMoveTime = LOCPLINT->playerID == hero->getOwner() ? - settings["adventure"]["heroMoveTime"].Float() : - settings["adventure"]["enemyMoveTime"].Float(); - - movementContext->progress += timeDelta / heroMoveTime; - movementContext->progress = std::min( 1.0, movementContext->progress); - - Point positionFrom = Point(hero->convertToVisitablePos(movementContext->tileFrom)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; - Point positionDest = Point(hero->convertToVisitablePos(movementContext->tileDest)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; - - Point positionCurr = vstd::lerp(positionFrom, positionDest, movementContext->progress); - - setViewCenter(positionCurr, movementContext->tileDest.z); - } - - if(teleportContext) - { - teleportContext->progress += timeDelta / heroTeleportDuration; - teleportContext->progress = std::min( 1.0, teleportContext->progress); - } - - if(fadingOutContext) - { - fadingOutContext->progress -= timeDelta / fadeOutDuration; - fadingOutContext->progress = std::max( 0.0, fadingOutContext->progress); - } - - if(fadingInContext) - { - fadingInContext->progress += timeDelta / fadeInDuration; - fadingInContext->progress = std::min( 1.0, fadingInContext->progress); - } - - if (adventureContext) - adventureContext->animationTime += timeDelta; - - updateState(); -} - -void MapViewController::updateState() -{ - if(adventureContext) - { - adventureContext->settingsSessionSpectate = settings["session"]["spectate"].Bool(); - adventureContext->settingsAdventureObjectAnimation = settings["adventure"]["objectAnimation"].Bool(); - adventureContext->settingsAdventureTerrainAnimation = settings["adventure"]["terrainAnimation"].Bool(); - adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); - adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); - adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); - adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); - } -} - -void MapViewController::afterRender() -{ - if(movementContext) - { - const auto * object = context->getObject(movementContext->target); - const auto * hero = dynamic_cast(object); - const auto * boat = dynamic_cast(object); - - assert(boat || hero); - - if(!hero) - hero = boat->hero; - - if(movementContext->progress >= 0.999) - { - logGlobal->debug("Ending movement animation"); - setViewCenter(hero->getSightCenter()); - - removeObject(context->getObject(movementContext->target)); - addObject(context->getObject(movementContext->target)); - - activateAdventureContext(movementContext->animationTime); - } - } - - if(teleportContext && teleportContext->progress >= 0.999) - { - logGlobal->debug("Ending teleport animation"); - activateAdventureContext(teleportContext->animationTime); - } - - if(fadingOutContext && fadingOutContext->progress <= 0.001) - { - logGlobal->debug("Ending fade out animation"); - removeObject(context->getObject(fadingOutContext->target)); - - activateAdventureContext(fadingOutContext->animationTime); - } - - if(fadingInContext && fadingInContext->progress >= 0.999) - { - logGlobal->debug("Ending fade in animation"); - activateAdventureContext(fadingInContext->animationTime); - } -} - -bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - if (!isEventVisible(obj, initiator)) - return true; - - if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() <= 0) - return true; // instant movement speed - - if(initiator == LOCPLINT->playerID && settings["adventure"]["heroMoveTime"].Float() <= 0) - return true; // instant movement speed - - return false; -} - -bool MapViewController::isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - if(adventureContext == nullptr) - return false; - - if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) - return false; // enemy move speed set to "hidden/none" - - if(!GH.windows().isTopWindow(adventureInt)) - return false; - - // do not focus on actions of other players during our turn (e.g. simturns) - if (LOCPLINT->makingTurn && initiator != LOCPLINT->playerID) - return false; - - if(obj->isVisitable()) - return context->isVisible(obj->visitablePos()); - else - return context->isVisible(obj->pos); -} - -bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(adventureContext == nullptr) - return false; - - if(obj->getOwner() != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) - return false; // enemy move speed set to "hidden/none" - - if(!GH.windows().isTopWindow(adventureInt)) - return false; - - // do not focus on actions of other players during our turn (e.g. simturns) - if (LOCPLINT->makingTurn && obj->getOwner() != LOCPLINT->playerID) - return false; - - if(context->isVisible(obj->convertToVisitablePos(from))) - return true; - - if(context->isVisible(obj->convertToVisitablePos(dest))) - return true; - - return false; -} - -void MapViewController::fadeOutObject(const CGObjectInstance * obj) -{ - logGlobal->debug("Starting fade out animation"); - fadingOutContext = std::make_shared(*state); - fadingOutContext->animationTime = adventureContext->animationTime; - adventureContext = fadingOutContext; - context = fadingOutContext; - - const CGObjectInstance * movingObject = obj; - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - movingObject = hero->boat; - } - - fadingOutContext->target = movingObject->id; - fadingOutContext->progress = 1.0; -} - -void MapViewController::fadeInObject(const CGObjectInstance * obj) -{ - logGlobal->debug("Starting fade in animation"); - fadingInContext = std::make_shared(*state); - fadingInContext->animationTime = adventureContext->animationTime; - adventureContext = fadingInContext; - context = fadingInContext; - - const CGObjectInstance * movingObject = obj; - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - movingObject = hero->boat; - } - - fadingInContext->target = movingObject->id; - fadingInContext->progress = 0.0; -} - -void MapViewController::removeObject(const CGObjectInstance * obj) -{ - if (obj->ID == Obj::BOAT) - { - auto * boat = dynamic_cast(obj); - if (boat->hero) - { - view->invalidate(context, boat->hero->id); - state->removeObject(boat->hero); - } - } - - if (obj->ID == Obj::HERO) - { - auto * hero = dynamic_cast(obj); - if (hero->boat) - { - view->invalidate(context, hero->boat->id); - state->removeObject(hero->boat); - } - } - - view->invalidate(context, obj->id); - state->removeObject(obj); -} - -void MapViewController::addObject(const CGObjectInstance * obj) -{ - state->addObject(obj); - view->invalidate(context, obj->id); -} - -void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - { - if (!isEventInstant(obj, obj->getOwner())) - fadeOutObject(obj); - setViewCenter(obj->getSightCenter()); - } - else - removeObject(obj); -} - -void MapViewController::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - setViewCenter(obj->getSightCenter()); -} - -void MapViewController::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - setViewCenter(obj->getSightCenter()); -} - -void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - if(isEventVisible(obj, from, dest)) - { - if (!isEventInstant(obj, obj->getOwner())) - fadeInObject(obj); - setViewCenter(obj->getSightCenter()); - } - addObject(obj); -} - -void MapViewController::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) - fadeInObject(obj); - - addObject(obj); -} - -void MapViewController::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) - fadeOutObject(obj); - else - removeObject(obj); -} - -void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - addObject(obj); -}; - -void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - removeObject(obj); -}; - -void MapViewController::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - if(isEventVisible(obj, from, dest)) - { - setViewCenter(obj->getSightCenter()); - view->createTransitionSnapshot(context); - } -} - -void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - const CGObjectInstance * movingObject = obj; - if(obj->boat) - movingObject = obj->boat; - - removeObject(movingObject); - addObject(movingObject); - - if(isEventVisible(obj, from, dest)) - { - logGlobal->debug("Starting teleport animation"); - teleportContext = std::make_shared(*state); - teleportContext->animationTime = adventureContext->animationTime; - adventureContext = teleportContext; - context = teleportContext; - setViewCenter(movingObject->getSightCenter()); - } -} - -void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(!hasOngoingAnimations()); - - // revisiting via spacebar, no need to animate - if(from == dest) - return; - - const CGObjectInstance * movingObject = obj; - if(obj->boat) - movingObject = obj->boat; - - removeObject(movingObject); - - if(!isEventVisible(obj, from, dest)) - { - addObject(movingObject); - return; - } - - double movementTime = LOCPLINT->playerID == obj->tempOwner ? - settings["adventure"]["heroMoveTime"].Float() : - settings["adventure"]["enemyMoveTime"].Float(); - - if(movementTime > 1) - { - logGlobal->debug("Starting movement animation"); - movementContext = std::make_shared(*state); - movementContext->animationTime = adventureContext->animationTime; - adventureContext = movementContext; - context = movementContext; - - state->addMovingObject(movingObject, from, dest); - - movementContext->target = movingObject->id; - movementContext->tileFrom = from; - movementContext->tileDest = dest; - movementContext->progress = 0.0; - } - else // instant movement - { - addObject(movingObject); - setViewCenter(movingObject->visitablePos()); - } -} - -bool MapViewController::hasOngoingAnimations() -{ - if(movementContext) - return true; - - if(fadingOutContext) - return true; - - if(fadingInContext) - return true; - - if(teleportContext) - return true; - - return false; -} - -void MapViewController::activateAdventureContext(uint32_t animationTime) -{ - resetContext(); - - adventureContext = std::make_shared(*state); - adventureContext->animationTime = animationTime; - context = adventureContext; - updateState(); -} - -void MapViewController::activateAdventureContext() -{ - activateAdventureContext(0); -} - -void MapViewController::activateWorldViewContext() -{ - if(worldViewContext) - return; - - resetContext(); - - worldViewContext = std::make_shared(*state); - context = worldViewContext; -} - -void MapViewController::activateSpellViewContext() -{ - if(spellViewContext) - return; - - resetContext(); - - spellViewContext = std::make_shared(*state); - worldViewContext = spellViewContext; - context = spellViewContext; -} - -void MapViewController::activatePuzzleMapContext(const int3 & grailPosition) -{ - resetContext(); - - puzzleMapContext = std::make_shared(*state); - context = puzzleMapContext; - - CGPathNode fakeNode; - fakeNode.coord = grailPosition; - - puzzleMapContext->grailPos = std::make_unique(); - - // create two nodes since 1st one is normally not visible - puzzleMapContext->grailPos->nodes.push_back(fakeNode); - puzzleMapContext->grailPos->nodes.push_back(fakeNode); -} - -void MapViewController::resetContext() -{ - adventureContext.reset(); - movementContext.reset(); - fadingOutContext.reset(); - fadingInContext.reset(); - teleportContext.reset(); - worldViewContext.reset(); - spellViewContext.reset(); - puzzleMapContext.reset(); -} - -void MapViewController::setTerrainVisibility(bool showAllTerrain) -{ - assert(spellViewContext); - spellViewContext->showAllTerrain = showAllTerrain; -} - -void MapViewController::setOverlayVisibility(const std::vector & objectPositions) -{ - assert(spellViewContext); - spellViewContext->additionalOverlayIcons = objectPositions; -} +/* + * MapViewController.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 "MapViewController.h" + +#include "MapRendererContext.h" +#include "MapRendererContextState.h" +#include "MapViewCache.h" +#include "MapViewModel.h" + +#include "../CPlayerInterface.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../eventsSDL/InputHandler.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/pathfinder/CGPathNode.h" +#include "../../lib/spells/ViewSpellInt.h" + +void MapViewController::setViewCenter(const int3 & position) +{ + setViewCenter(Point(position) * model->getSingleTileSize() + model->getSingleTileSize() / 2, position.z); +} + +void MapViewController::setViewCenter(const Point & position, int level) +{ + Point upperLimit = Point(context->getMapSize()) * model->getSingleTileSize(); + Point lowerLimit = Point(0, 0); + + if(worldViewContext) + { + Point area = model->getPixelsVisibleDimensions(); + Point mapCenter = upperLimit / 2; + + Point desiredLowerLimit = lowerLimit + area / 2; + Point desiredUpperLimit = upperLimit - area / 2; + + Point actualLowerLimit{ + std::min(desiredLowerLimit.x, mapCenter.x), + std::min(desiredLowerLimit.y, mapCenter.y) + }; + + Point actualUpperLimit{ + std::max(desiredUpperLimit.x, mapCenter.x), + std::max(desiredUpperLimit.y, mapCenter.y) + }; + + upperLimit = actualUpperLimit; + lowerLimit = actualLowerLimit; + } + + Point betterPosition = {std::clamp(position.x, lowerLimit.x, upperLimit.x), std::clamp(position.y, lowerLimit.y, upperLimit.y)}; + + model->setViewCenter(betterPosition); + model->setLevel(std::clamp(level, 0, context->getMapSize().z)); + + if(adventureInt && !puzzleMapContext) // may be called before adventureInt is initialized + adventureInt->onMapViewMoved(model->getTilesTotalRect(), model->getLevel()); +} + +void MapViewController::setTileSize(const Point & tileSize) +{ + Point oldSize = model->getSingleTileSize(); + model->setTileSize(tileSize); + + double scaleChangeX = 1.0 * tileSize.x / oldSize.x; + double scaleChangeY = 1.0 * tileSize.y / oldSize.y; + + Point newViewCenter { + static_cast(std::round(model->getMapViewCenter().x * scaleChangeX)), + static_cast(std::round(model->getMapViewCenter().y * scaleChangeY)) + }; + + // force update of view center since changing tile size may invalidated it + setViewCenter(newViewCenter, model->getLevel()); +} + +void MapViewController::modifyTileSize(int stepsChange) +{ + // we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling + // so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale + // try to determine current zooming level and change it by requested number of steps + double currentZoomFactor = targetTileSize.x / static_cast(defaultTileSize); + double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01)); + double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange; + double newZoomFactor = std::pow(1.01, newZoomSteps); + + Point currentZoom = targetTileSize; + Point desiredZoom = Point(defaultTileSize,defaultTileSize) * newZoomFactor; + + if (desiredZoom == currentZoom && stepsChange < 0) + desiredZoom -= Point(1,1); + + if (desiredZoom == currentZoom && stepsChange > 0) + desiredZoom += Point(1,1); + + Point minimal = model->getSingleTileSizeLowerLimit(); + Point maximal = model->getSingleTileSizeUpperLimit(); + Point actualZoom = { + std::clamp(desiredZoom.x, minimal.x, maximal.x), + std::clamp(desiredZoom.y, minimal.y, maximal.y) + }; + + if (actualZoom != currentZoom) + { + targetTileSize = actualZoom; + if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea) + actualZoom.x = defaultTileSize; + if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea) + actualZoom.y = defaultTileSize; + + bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize); + + if(!wasInDeadZone && isInDeadZone) + GH.input().hapticFeedback(); + + wasInDeadZone = isInDeadZone; + + setTileSize(actualZoom); + } +} + +MapViewController::MapViewController(std::shared_ptr model, std::shared_ptr view) + : state(new MapRendererContextState()) + , model(std::move(model)) + , view(view) +{ + adventureContext = std::make_shared(*state); + context = adventureContext; +} + +std::shared_ptr MapViewController::getContext() const +{ + return context; +} + +void MapViewController::tick(uint32_t timeDelta) +{ + // confirmed to match H3 for + // - hero embarking on boat (500 ms) + // - hero disembarking from boat (500 ms) + // - TODO: picking up resources + // - TODO: killing mosters + // - teleporting ( 250 ms) + static const double fadeOutDuration = 500; + static const double fadeInDuration = 500; + static const double heroTeleportDuration = 250; + + if(movementContext) + { + const auto * object = context->getObject(movementContext->target); + const auto * hero = dynamic_cast(object); + const auto * boat = dynamic_cast(object); + + assert(boat || hero); + + if(!hero) + hero = boat->hero; + + double heroMoveTime = LOCPLINT->playerID == hero->getOwner() ? + settings["adventure"]["heroMoveTime"].Float() : + settings["adventure"]["enemyMoveTime"].Float(); + + movementContext->progress += timeDelta / heroMoveTime; + movementContext->progress = std::min( 1.0, movementContext->progress); + + Point positionFrom = Point(hero->convertToVisitablePos(movementContext->tileFrom)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; + Point positionDest = Point(hero->convertToVisitablePos(movementContext->tileDest)) * model->getSingleTileSize() + model->getSingleTileSize() / 2; + + Point positionCurr = vstd::lerp(positionFrom, positionDest, movementContext->progress); + + setViewCenter(positionCurr, movementContext->tileDest.z); + } + + if(teleportContext) + { + teleportContext->progress += timeDelta / heroTeleportDuration; + teleportContext->progress = std::min( 1.0, teleportContext->progress); + } + + if(fadingOutContext) + { + fadingOutContext->progress -= timeDelta / fadeOutDuration; + fadingOutContext->progress = std::max( 0.0, fadingOutContext->progress); + } + + if(fadingInContext) + { + fadingInContext->progress += timeDelta / fadeInDuration; + fadingInContext->progress = std::min( 1.0, fadingInContext->progress); + } + + if (adventureContext) + adventureContext->animationTime += timeDelta; + + updateState(); +} + +void MapViewController::updateState() +{ + if(adventureContext) + { + adventureContext->settingsSessionSpectate = settings["session"]["spectate"].Bool(); + adventureContext->settingsAdventureObjectAnimation = settings["adventure"]["objectAnimation"].Bool(); + adventureContext->settingsAdventureTerrainAnimation = settings["adventure"]["terrainAnimation"].Bool(); + adventureContext->settingShowGrid = settings["gameTweaks"]["showGrid"].Bool(); + adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool(); + adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool(); + adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool(); + } +} + +void MapViewController::afterRender() +{ + if(movementContext) + { + const auto * object = context->getObject(movementContext->target); + const auto * hero = dynamic_cast(object); + const auto * boat = dynamic_cast(object); + + assert(boat || hero); + + if(!hero) + hero = boat->hero; + + if(movementContext->progress >= 0.999) + { + logGlobal->debug("Ending movement animation"); + setViewCenter(hero->getSightCenter()); + + removeObject(context->getObject(movementContext->target)); + addObject(context->getObject(movementContext->target)); + + activateAdventureContext(movementContext->animationTime); + } + } + + if(teleportContext && teleportContext->progress >= 0.999) + { + logGlobal->debug("Ending teleport animation"); + activateAdventureContext(teleportContext->animationTime); + } + + if(fadingOutContext && fadingOutContext->progress <= 0.001) + { + logGlobal->debug("Ending fade out animation"); + removeObject(context->getObject(fadingOutContext->target)); + + activateAdventureContext(fadingOutContext->animationTime); + } + + if(fadingInContext && fadingInContext->progress >= 0.999) + { + logGlobal->debug("Ending fade in animation"); + activateAdventureContext(fadingInContext->animationTime); + } +} + +bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + if (!isEventVisible(obj, initiator)) + return true; + + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() <= 0) + return true; // instant movement speed + + if(initiator == LOCPLINT->playerID && settings["adventure"]["heroMoveTime"].Float() <= 0) + return true; // instant movement speed + + return false; +} + +bool MapViewController::isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + if(adventureContext == nullptr) + return false; + + if(initiator != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) + return false; // enemy move speed set to "hidden/none" + + if(!GH.windows().isTopWindow(adventureInt)) + return false; + + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && initiator != LOCPLINT->playerID) + return false; + + if(obj->isVisitable()) + return context->isVisible(obj->visitablePos()); + else + return context->isVisible(obj->pos); +} + +bool MapViewController::isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(adventureContext == nullptr) + return false; + + if(obj->getOwner() != LOCPLINT->playerID && settings["adventure"]["enemyMoveTime"].Float() < 0) + return false; // enemy move speed set to "hidden/none" + + if(!GH.windows().isTopWindow(adventureInt)) + return false; + + // do not focus on actions of other players during our turn (e.g. simturns) + if (LOCPLINT->makingTurn && obj->getOwner() != LOCPLINT->playerID) + return false; + + if(context->isVisible(obj->convertToVisitablePos(from))) + return true; + + if(context->isVisible(obj->convertToVisitablePos(dest))) + return true; + + return false; +} + +void MapViewController::fadeOutObject(const CGObjectInstance * obj) +{ + logGlobal->debug("Starting fade out animation"); + fadingOutContext = std::make_shared(*state); + fadingOutContext->animationTime = adventureContext->animationTime; + adventureContext = fadingOutContext; + context = fadingOutContext; + + const CGObjectInstance * movingObject = obj; + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + movingObject = hero->boat; + } + + fadingOutContext->target = movingObject->id; + fadingOutContext->progress = 1.0; +} + +void MapViewController::fadeInObject(const CGObjectInstance * obj) +{ + logGlobal->debug("Starting fade in animation"); + fadingInContext = std::make_shared(*state); + fadingInContext->animationTime = adventureContext->animationTime; + adventureContext = fadingInContext; + context = fadingInContext; + + const CGObjectInstance * movingObject = obj; + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + movingObject = hero->boat; + } + + fadingInContext->target = movingObject->id; + fadingInContext->progress = 0.0; +} + +void MapViewController::removeObject(const CGObjectInstance * obj) +{ + if (obj->ID == Obj::BOAT) + { + auto * boat = dynamic_cast(obj); + if (boat->hero) + { + view->invalidate(context, boat->hero->id); + state->removeObject(boat->hero); + } + } + + if (obj->ID == Obj::HERO) + { + auto * hero = dynamic_cast(obj); + if (hero->boat) + { + view->invalidate(context, hero->boat->id); + state->removeObject(hero->boat); + } + } + + view->invalidate(context, obj->id); + state->removeObject(obj); +} + +void MapViewController::addObject(const CGObjectInstance * obj) +{ + state->addObject(obj); + view->invalidate(context, obj->id); +} + +void MapViewController::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + { + if (!isEventInstant(obj, obj->getOwner())) + fadeOutObject(obj); + setViewCenter(obj->getSightCenter()); + } + else + removeObject(obj); +} + +void MapViewController::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + setViewCenter(obj->getSightCenter()); +} + +void MapViewController::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + setViewCenter(obj->getSightCenter()); +} + +void MapViewController::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + if(isEventVisible(obj, from, dest)) + { + if (!isEventInstant(obj, obj->getOwner())) + fadeInObject(obj); + setViewCenter(obj->getSightCenter()); + } + addObject(obj); +} + +void MapViewController::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) + fadeInObject(obj); + + addObject(obj); +} + +void MapViewController::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, initiator) && !isEventInstant(obj, initiator) ) + fadeOutObject(obj); + else + removeObject(obj); +} + +void MapViewController::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + addObject(obj); +}; + +void MapViewController::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + removeObject(obj); +}; + +void MapViewController::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + if(isEventVisible(obj, from, dest)) + { + setViewCenter(obj->getSightCenter()); + view->createTransitionSnapshot(context); + } +} + +void MapViewController::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + const CGObjectInstance * movingObject = obj; + if(obj->boat) + movingObject = obj->boat; + + removeObject(movingObject); + addObject(movingObject); + + if(isEventVisible(obj, from, dest)) + { + logGlobal->debug("Starting teleport animation"); + teleportContext = std::make_shared(*state); + teleportContext->animationTime = adventureContext->animationTime; + adventureContext = teleportContext; + context = teleportContext; + setViewCenter(movingObject->getSightCenter()); + } +} + +void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(!hasOngoingAnimations()); + + // revisiting via spacebar, no need to animate + if(from == dest) + return; + + const CGObjectInstance * movingObject = obj; + if(obj->boat) + movingObject = obj->boat; + + removeObject(movingObject); + + if(!isEventVisible(obj, from, dest)) + { + addObject(movingObject); + return; + } + + double movementTime = LOCPLINT->playerID == obj->tempOwner ? + settings["adventure"]["heroMoveTime"].Float() : + settings["adventure"]["enemyMoveTime"].Float(); + + if(movementTime > 1) + { + logGlobal->debug("Starting movement animation"); + movementContext = std::make_shared(*state); + movementContext->animationTime = adventureContext->animationTime; + adventureContext = movementContext; + context = movementContext; + + state->addMovingObject(movingObject, from, dest); + + movementContext->target = movingObject->id; + movementContext->tileFrom = from; + movementContext->tileDest = dest; + movementContext->progress = 0.0; + } + else // instant movement + { + addObject(movingObject); + setViewCenter(movingObject->visitablePos()); + } +} + +bool MapViewController::hasOngoingAnimations() +{ + if(movementContext) + return true; + + if(fadingOutContext) + return true; + + if(fadingInContext) + return true; + + if(teleportContext) + return true; + + return false; +} + +void MapViewController::activateAdventureContext(uint32_t animationTime) +{ + resetContext(); + + adventureContext = std::make_shared(*state); + adventureContext->animationTime = animationTime; + context = adventureContext; + updateState(); +} + +void MapViewController::activateAdventureContext() +{ + activateAdventureContext(0); +} + +void MapViewController::activateWorldViewContext() +{ + if(worldViewContext) + return; + + resetContext(); + + worldViewContext = std::make_shared(*state); + context = worldViewContext; +} + +void MapViewController::activateSpellViewContext() +{ + if(spellViewContext) + return; + + resetContext(); + + spellViewContext = std::make_shared(*state); + worldViewContext = spellViewContext; + context = spellViewContext; +} + +void MapViewController::activatePuzzleMapContext(const int3 & grailPosition) +{ + resetContext(); + + puzzleMapContext = std::make_shared(*state); + context = puzzleMapContext; + + CGPathNode fakeNode; + fakeNode.coord = grailPosition; + + puzzleMapContext->grailPos = std::make_unique(); + + // create two nodes since 1st one is normally not visible + puzzleMapContext->grailPos->nodes.push_back(fakeNode); + puzzleMapContext->grailPos->nodes.push_back(fakeNode); +} + +void MapViewController::resetContext() +{ + adventureContext.reset(); + movementContext.reset(); + fadingOutContext.reset(); + fadingInContext.reset(); + teleportContext.reset(); + worldViewContext.reset(); + spellViewContext.reset(); + puzzleMapContext.reset(); +} + +void MapViewController::setTerrainVisibility(bool showAllTerrain) +{ + assert(spellViewContext); + spellViewContext->showAllTerrain = showAllTerrain; +} + +void MapViewController::setOverlayVisibility(const std::vector & objectPositions) +{ + assert(spellViewContext); + spellViewContext->additionalOverlayIcons = objectPositions; +} diff --git a/client/mapView/MapViewController.h b/client/mapView/MapViewController.h index f7f4d079e..151ef6a93 100644 --- a/client/mapView/MapViewController.h +++ b/client/mapView/MapViewController.h @@ -1,106 +1,106 @@ -/* - * MapViewController.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 "IMapRendererObserver.h" -#include "../../lib/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN -struct ObjectPosInfo; -class PlayerColor; -VCMI_LIB_NAMESPACE_END - -struct MapRendererContextState; - -class MapViewCache; -class MapViewModel; -class IMapRendererContext; -class MapRendererAdventureContext; -class MapRendererAdventureFadingContext; -class MapRendererAdventureMovingContext; -class MapRendererAdventureTransitionContext; -class MapRendererWorldViewContext; -class MapRendererSpellViewContext; -class MapRendererPuzzleMapContext; - -/// Class responsible for updating view state, -/// such as its position and any animations -class MapViewController : public IMapObjectObserver -{ - std::shared_ptr context; - std::shared_ptr state; - std::shared_ptr model; - std::shared_ptr view; - - // all potential contexts for view - // only some are present at any given moment, rest are nullptr's - std::shared_ptr adventureContext; - std::shared_ptr movementContext; - std::shared_ptr teleportContext; - std::shared_ptr fadingOutContext; - std::shared_ptr fadingInContext; - std::shared_ptr worldViewContext; - std::shared_ptr spellViewContext; - std::shared_ptr puzzleMapContext; - -private: - const int defaultTileSize = 32; - const int zoomTileDeadArea = 5; - Point targetTileSize = Point(32, 32); - bool wasInDeadZone = true; - - bool isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator); - bool isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator); - bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - - void fadeOutObject(const CGObjectInstance * obj); - void fadeInObject(const CGObjectInstance * obj); - - void removeObject(const CGObjectInstance * obj); - void addObject(const CGObjectInstance * obj); - - // IMapObjectObserver impl - bool hasOngoingAnimations() override; - void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; - - void resetContext(); - void updateState(); - -public: - MapViewController(std::shared_ptr model, std::shared_ptr view); - - std::shared_ptr getContext() const; - - void setViewCenter(const int3 & position); - void setViewCenter(const Point & position, int level); - void setTileSize(const Point & tileSize); - void modifyTileSize(int stepsChange); - void tick(uint32_t timePassed); - void afterRender(); - - void activateAdventureContext(uint32_t animationTime); - void activateAdventureContext(); - void activateWorldViewContext(); - void activateSpellViewContext(); - void activatePuzzleMapContext(const int3 & grailPosition); - - void setTerrainVisibility(bool showAllTerrain); - void setOverlayVisibility(const std::vector & objectPositions); -}; +/* + * MapViewController.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 "IMapRendererObserver.h" +#include "../../lib/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +struct ObjectPosInfo; +class PlayerColor; +VCMI_LIB_NAMESPACE_END + +struct MapRendererContextState; + +class MapViewCache; +class MapViewModel; +class IMapRendererContext; +class MapRendererAdventureContext; +class MapRendererAdventureFadingContext; +class MapRendererAdventureMovingContext; +class MapRendererAdventureTransitionContext; +class MapRendererWorldViewContext; +class MapRendererSpellViewContext; +class MapRendererPuzzleMapContext; + +/// Class responsible for updating view state, +/// such as its position and any animations +class MapViewController : public IMapObjectObserver +{ + std::shared_ptr context; + std::shared_ptr state; + std::shared_ptr model; + std::shared_ptr view; + + // all potential contexts for view + // only some are present at any given moment, rest are nullptr's + std::shared_ptr adventureContext; + std::shared_ptr movementContext; + std::shared_ptr teleportContext; + std::shared_ptr fadingOutContext; + std::shared_ptr fadingInContext; + std::shared_ptr worldViewContext; + std::shared_ptr spellViewContext; + std::shared_ptr puzzleMapContext; + +private: + const int defaultTileSize = 32; + const int zoomTileDeadArea = 5; + Point targetTileSize = Point(32, 32); + bool wasInDeadZone = true; + + bool isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator); + bool isEventVisible(const CGObjectInstance * obj, const PlayerColor & initiator); + bool isEventVisible(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + + void fadeOutObject(const CGObjectInstance * obj); + void fadeInObject(const CGObjectInstance * obj); + + void removeObject(const CGObjectInstance * obj); + void addObject(const CGObjectInstance * obj); + + // IMapObjectObserver impl + bool hasOngoingAnimations() override; + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) override; + + void resetContext(); + void updateState(); + +public: + MapViewController(std::shared_ptr model, std::shared_ptr view); + + std::shared_ptr getContext() const; + + void setViewCenter(const int3 & position); + void setViewCenter(const Point & position, int level); + void setTileSize(const Point & tileSize); + void modifyTileSize(int stepsChange); + void tick(uint32_t timePassed); + void afterRender(); + + void activateAdventureContext(uint32_t animationTime); + void activateAdventureContext(); + void activateWorldViewContext(); + void activateSpellViewContext(); + void activatePuzzleMapContext(const int3 & grailPosition); + + void setTerrainVisibility(bool showAllTerrain); + void setOverlayVisibility(const std::vector & objectPositions); +}; diff --git a/client/mapView/MapViewModel.cpp b/client/mapView/MapViewModel.cpp index 9651c92c9..38b1271a7 100644 --- a/client/mapView/MapViewModel.cpp +++ b/client/mapView/MapViewModel.cpp @@ -1,129 +1,129 @@ -/* - * MapViewModel.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 "MapViewModel.h" - -#include "../../lib/int3.h" - -void MapViewModel::setTileSize(const Point & newValue) -{ - tileSize = newValue; -} - -void MapViewModel::setViewCenter(const Point & newValue) -{ - viewCenter = newValue; -} - -void MapViewModel::setViewDimensions(const Point & newValue) -{ - viewDimensions = newValue; -} - -void MapViewModel::setLevel(int newLevel) -{ - mapLevel = newLevel; -} - -Point MapViewModel::getSingleTileSizeUpperLimit() const -{ - // arbitrary-seleted upscaling limit - return Point(256, 256); -} - -Point MapViewModel::getSingleTileSizeLowerLimit() const -{ - // arbitrary-seleted downscaling limit - return Point(4, 4); -} - -Point MapViewModel::getSingleTileSize() const -{ - return tileSize; -} - -Point MapViewModel::getMapViewCenter() const -{ - return viewCenter; -} - -Point MapViewModel::getPixelsVisibleDimensions() const -{ - return viewDimensions; -} - -int MapViewModel::getLevel() const -{ - return mapLevel; -} - -Point MapViewModel::getTilesVisibleDimensions() const -{ - // total number of potentially visible tiles is: - // 1) number of completely visible tiles - // 2) additional tile that might be partially visible from left/top size - // 3) additional tile that might be partially visible from right/bottom size - return { - getPixelsVisibleDimensions().x / getSingleTileSize().x + 2, - getPixelsVisibleDimensions().y / getSingleTileSize().y + 2, - }; -} - -Rect MapViewModel::getTilesTotalRect() const -{ - return Rect( - Point(getTileAtPoint(Point(0,0))), - getTilesVisibleDimensions() - ); -} - -int3 MapViewModel::getTileAtPoint(const Point & position) const -{ - Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; - - Point absolutePosition = position + topLeftOffset; - - // NOTE: using division via double in order to use std::floor - // which rounds to negative infinity and not towards zero (like integer division) - return { - static_cast(std::floor(static_cast(absolutePosition.x) / getSingleTileSize().x)), - static_cast(std::floor(static_cast(absolutePosition.y) / getSingleTileSize().y)), - getLevel() - }; -} - -Point MapViewModel::getCacheDimensionsPixels() const -{ - return getTilesVisibleDimensions() * getSingleTileSizeUpperLimit(); -} - -Rect MapViewModel::getCacheTileArea(const int3 & coordinates) const -{ - assert(mapLevel == coordinates.z); - assert(getTilesVisibleDimensions().x + coordinates.x >= 0); - assert(getTilesVisibleDimensions().y + coordinates.y >= 0); - - Point tileIndex{ - (getTilesVisibleDimensions().x + coordinates.x) % getTilesVisibleDimensions().x, - (getTilesVisibleDimensions().y + coordinates.y) % getTilesVisibleDimensions().y - }; - - return Rect(tileIndex * getSingleTileSize(), getSingleTileSize()); -} - -Rect MapViewModel::getTargetTileArea(const int3 & coordinates) const -{ - Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; - Point tilePosAbsolute = Point(coordinates) * getSingleTileSize(); - Point tilePosRelative = tilePosAbsolute - topLeftOffset; - - return Rect(tilePosRelative, getSingleTileSize()); -} +/* + * MapViewModel.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 "MapViewModel.h" + +#include "../../lib/int3.h" + +void MapViewModel::setTileSize(const Point & newValue) +{ + tileSize = newValue; +} + +void MapViewModel::setViewCenter(const Point & newValue) +{ + viewCenter = newValue; +} + +void MapViewModel::setViewDimensions(const Point & newValue) +{ + viewDimensions = newValue; +} + +void MapViewModel::setLevel(int newLevel) +{ + mapLevel = newLevel; +} + +Point MapViewModel::getSingleTileSizeUpperLimit() const +{ + // arbitrary-seleted upscaling limit + return Point(256, 256); +} + +Point MapViewModel::getSingleTileSizeLowerLimit() const +{ + // arbitrary-seleted downscaling limit + return Point(4, 4); +} + +Point MapViewModel::getSingleTileSize() const +{ + return tileSize; +} + +Point MapViewModel::getMapViewCenter() const +{ + return viewCenter; +} + +Point MapViewModel::getPixelsVisibleDimensions() const +{ + return viewDimensions; +} + +int MapViewModel::getLevel() const +{ + return mapLevel; +} + +Point MapViewModel::getTilesVisibleDimensions() const +{ + // total number of potentially visible tiles is: + // 1) number of completely visible tiles + // 2) additional tile that might be partially visible from left/top size + // 3) additional tile that might be partially visible from right/bottom size + return { + getPixelsVisibleDimensions().x / getSingleTileSize().x + 2, + getPixelsVisibleDimensions().y / getSingleTileSize().y + 2, + }; +} + +Rect MapViewModel::getTilesTotalRect() const +{ + return Rect( + Point(getTileAtPoint(Point(0,0))), + getTilesVisibleDimensions() + ); +} + +int3 MapViewModel::getTileAtPoint(const Point & position) const +{ + Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; + + Point absolutePosition = position + topLeftOffset; + + // NOTE: using division via double in order to use std::floor + // which rounds to negative infinity and not towards zero (like integer division) + return { + static_cast(std::floor(static_cast(absolutePosition.x) / getSingleTileSize().x)), + static_cast(std::floor(static_cast(absolutePosition.y) / getSingleTileSize().y)), + getLevel() + }; +} + +Point MapViewModel::getCacheDimensionsPixels() const +{ + return getTilesVisibleDimensions() * getSingleTileSizeUpperLimit(); +} + +Rect MapViewModel::getCacheTileArea(const int3 & coordinates) const +{ + assert(mapLevel == coordinates.z); + assert(getTilesVisibleDimensions().x + coordinates.x >= 0); + assert(getTilesVisibleDimensions().y + coordinates.y >= 0); + + Point tileIndex{ + (getTilesVisibleDimensions().x + coordinates.x) % getTilesVisibleDimensions().x, + (getTilesVisibleDimensions().y + coordinates.y) % getTilesVisibleDimensions().y + }; + + return Rect(tileIndex * getSingleTileSize(), getSingleTileSize()); +} + +Rect MapViewModel::getTargetTileArea(const int3 & coordinates) const +{ + Point topLeftOffset = getMapViewCenter() - getPixelsVisibleDimensions() / 2; + Point tilePosAbsolute = Point(coordinates) * getSingleTileSize(); + Point tilePosRelative = tilePosAbsolute - topLeftOffset; + + return Rect(tilePosRelative, getSingleTileSize()); +} diff --git a/client/mapView/MapViewModel.h b/client/mapView/MapViewModel.h index c5902f16d..b8e3a9236 100644 --- a/client/mapView/MapViewModel.h +++ b/client/mapView/MapViewModel.h @@ -1,63 +1,63 @@ -/* - * MapViewModel.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" - -class MapViewModel -{ - Point tileSize; - Point viewCenter; - Point viewDimensions; - - int mapLevel = 0; - -public: - void setTileSize(const Point & newValue); - void setViewCenter(const Point & newValue); - void setViewDimensions(const Point & newValue); - void setLevel(int newLevel); - - /// returns maximal possible size for a single tile - Point getSingleTileSizeUpperLimit() const; - - /// returns minimal possible size for a single tile - Point getSingleTileSizeLowerLimit() const; - - /// returns current size of map tile in pixels - Point getSingleTileSize() const; - - /// returns center point of map view, in Map coordinates - Point getMapViewCenter() const; - - /// returns total number of visible tiles - Point getTilesVisibleDimensions() const; - - /// returns rect encompassing all visible tiles - Rect getTilesTotalRect() const; - - /// returns required area in pixels of cache canvas - Point getCacheDimensionsPixels() const; - - /// returns actual player-visible area - Point getPixelsVisibleDimensions() const; - - /// returns area covered by specified tile in map cache - Rect getCacheTileArea(const int3 & coordinates) const; - - /// returns area covered by specified tile in target view - Rect getTargetTileArea(const int3 & coordinates) const; - - /// returns tile under specified position in target view - int3 getTileAtPoint(const Point & position) const; - - /// returns currently visible map level - int getLevel() const; -}; +/* + * MapViewModel.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" + +class MapViewModel +{ + Point tileSize; + Point viewCenter; + Point viewDimensions; + + int mapLevel = 0; + +public: + void setTileSize(const Point & newValue); + void setViewCenter(const Point & newValue); + void setViewDimensions(const Point & newValue); + void setLevel(int newLevel); + + /// returns maximal possible size for a single tile + Point getSingleTileSizeUpperLimit() const; + + /// returns minimal possible size for a single tile + Point getSingleTileSizeLowerLimit() const; + + /// returns current size of map tile in pixels + Point getSingleTileSize() const; + + /// returns center point of map view, in Map coordinates + Point getMapViewCenter() const; + + /// returns total number of visible tiles + Point getTilesVisibleDimensions() const; + + /// returns rect encompassing all visible tiles + Rect getTilesTotalRect() const; + + /// returns required area in pixels of cache canvas + Point getCacheDimensionsPixels() const; + + /// returns actual player-visible area + Point getPixelsVisibleDimensions() const; + + /// returns area covered by specified tile in map cache + Rect getCacheTileArea(const int3 & coordinates) const; + + /// returns area covered by specified tile in target view + Rect getTargetTileArea(const int3 & coordinates) const; + + /// returns tile under specified position in target view + int3 getTileAtPoint(const Point & position) const; + + /// returns currently visible map level + int getLevel() const; +}; diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index 2b83b2fba..fdd4fae86 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -1,236 +1,236 @@ -/* - * mapHandler.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 "IMapRendererObserver.h" -#include "mapHandler.h" - -#include "../CCallback.h" -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../gui/CGuiHandler.h" - -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/TerrainHandler.h" -#include "../../lib/UnlockGuard.h" -#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/ObjectTemplate.h" -#include "../../lib/mapping/CMap.h" - -bool CMapHandler::hasOngoingAnimations() -{ - for(auto * observer : observers) - if(observer->hasOngoingAnimations()) - return true; - - return false; -} - -void CMapHandler::waitForOngoingAnimations() -{ - while(CGI->mh->hasOngoingAnimations()) - { - auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); - boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); - } -} - -std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) const -{ - const TerrainTile & t = map->getTile(pos); - - if(t.hasFavorableWinds()) - return CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0); - - std::string result = t.terType->getNameTranslated(); - - for(const auto & object : map->objects) - { - if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain()) - { - result = object->getObjectName(); - break; - } - } - - if(LOCPLINT->cb->getTileDigStatus(pos, false) == EDiggingStatus::CAN_DIG) - { - return boost::str( - boost::format(rightClick ? "%s\r\n%s" : "%s %s") // New line for the Message Box, space for the Status Bar - % result % CGI->generaltexth->allTexts[330] - ); // 'digging ok' - } - - return result; -} - -bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) -{ - if(!a) - return true; - if(!b) - return false; - - // Background objects will always be placed below foreground objects - if(a->appearance->printPriority != 0 || b->appearance->printPriority != 0) - { - if(a->appearance->printPriority != b->appearance->printPriority) - return a->appearance->printPriority > b->appearance->printPriority; - - //Two background objects will be placed based on their placement order on map - return a->id < b->id; - } - - int aBlocksB = 0; - int bBlocksA = 0; - - for(const auto & aOffset : a->getBlockedOffsets()) - { - int3 testTarget = a->pos + aOffset + int3(0, 1, 0); - if(b->blockingAt(testTarget.x, testTarget.y)) - bBlocksA += 1; - } - - for(const auto & bOffset : b->getBlockedOffsets()) - { - int3 testTarget = b->pos + bOffset + int3(0, 1, 0); - if(a->blockingAt(testTarget.x, testTarget.y)) - aBlocksB += 1; - } - - // Discovered by experimenting with H3 maps - object priority depends on how many tiles of object A are "blocked" by object B - // For example if blockmap of two objects looks like this: - // ABB - // AAB - // Here, in middle column object A has blocked tile that is immediately below tile blocked by object B - // Meaning, object A blocks 1 tile of object B and object B blocks 0 tiles of object A - // In this scenario in H3 object A will always appear above object B, irregardless of H3M order - if(aBlocksB != bBlocksA) - return aBlocksB < bBlocksA; - - // object that don't have clear priority via tile blocking will appear based on their row - if(a->pos.y != b->pos.y) - return a->pos.y < b->pos.y; - - // heroes should appear on top of objects on the same tile - if(b->ID==Obj::HERO && a->ID!=Obj::HERO) - return true; - if(b->ID!=Obj::HERO && a->ID==Obj::HERO) - return false; - - // or, if all other tests fail to determine priority - simply based on H3M order - return a->id < b->id; -} - -CMapHandler::CMapHandler(const CMap * map) - : map(map) -{ -} - -const CMap * CMapHandler::getMap() -{ - return map; -} - -bool CMapHandler::isInMap(const int3 & tile) -{ - return map->isInTheMap(tile); -} - -void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - for(auto * observer : observers) - observer->onObjectFadeIn(obj, initiator); -} - -void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - for(auto * observer : observers) - observer->onObjectFadeOut(obj, initiator); -} - -void CMapHandler::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onBeforeHeroEmbark(obj, from, dest); -} - -void CMapHandler::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onAfterHeroEmbark(obj, from, dest); -} - -void CMapHandler::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onBeforeHeroDisembark(obj, from, dest); -} - -void CMapHandler::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - for(auto * observer : observers) - observer->onAfterHeroDisembark(obj, from, dest); -} - -void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - for(auto * observer : observers) - observer->onObjectInstantAdd(obj, initiator); -} - -void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) -{ - for(auto * observer : observers) - observer->onObjectInstantRemove(obj, initiator); -} - -void CMapHandler::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == dest); - for(auto * observer : observers) - observer->onAfterHeroTeleported(obj, from, dest); -} - -void CMapHandler::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == from); - for(auto * observer : observers) - observer->onBeforeHeroTeleported(obj, from, dest); -} - -void CMapHandler::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) -{ - assert(obj->pos == dest); - for(auto * observer : observers) - observer->onHeroMoved(obj, from, dest); -} - -void CMapHandler::addMapObserver(IMapObjectObserver * object) -{ - observers.push_back(object); -} - -void CMapHandler::removeMapObserver(IMapObjectObserver * object) -{ - vstd::erase(observers, object); -} - -IMapObjectObserver::IMapObjectObserver() -{ - CGI->mh->addMapObserver(this); -} - -IMapObjectObserver::~IMapObjectObserver() -{ - if (CGI && CGI->mh) - CGI->mh->removeMapObserver(this); -} +/* + * mapHandler.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 "IMapRendererObserver.h" +#include "mapHandler.h" + +#include "../CCallback.h" +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../gui/CGuiHandler.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/TerrainHandler.h" +#include "../../lib/UnlockGuard.h" +#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapping/CMap.h" + +bool CMapHandler::hasOngoingAnimations() +{ + for(auto * observer : observers) + if(observer->hasOngoingAnimations()) + return true; + + return false; +} + +void CMapHandler::waitForOngoingAnimations() +{ + while(CGI->mh->hasOngoingAnimations()) + { + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + boost::this_thread::sleep_for(boost::chrono::milliseconds(1)); + } +} + +std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) const +{ + const TerrainTile & t = map->getTile(pos); + + if(t.hasFavorableWinds()) + return CGI->objtypeh->getObjectName(Obj::FAVORABLE_WINDS, 0); + + std::string result = t.terType->getNameTranslated(); + + for(const auto & object : map->objects) + { + if(object && object->coveringAt(pos.x, pos.y) && object->pos.z == pos.z && object->isTile2Terrain()) + { + result = object->getObjectName(); + break; + } + } + + if(LOCPLINT->cb->getTileDigStatus(pos, false) == EDiggingStatus::CAN_DIG) + { + return boost::str( + boost::format(rightClick ? "%s\r\n%s" : "%s %s") // New line for the Message Box, space for the Status Bar + % result % CGI->generaltexth->allTexts[330] + ); // 'digging ok' + } + + return result; +} + +bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) +{ + if(!a) + return true; + if(!b) + return false; + + // Background objects will always be placed below foreground objects + if(a->appearance->printPriority != 0 || b->appearance->printPriority != 0) + { + if(a->appearance->printPriority != b->appearance->printPriority) + return a->appearance->printPriority > b->appearance->printPriority; + + //Two background objects will be placed based on their placement order on map + return a->id < b->id; + } + + int aBlocksB = 0; + int bBlocksA = 0; + + for(const auto & aOffset : a->getBlockedOffsets()) + { + int3 testTarget = a->pos + aOffset + int3(0, 1, 0); + if(b->blockingAt(testTarget.x, testTarget.y)) + bBlocksA += 1; + } + + for(const auto & bOffset : b->getBlockedOffsets()) + { + int3 testTarget = b->pos + bOffset + int3(0, 1, 0); + if(a->blockingAt(testTarget.x, testTarget.y)) + aBlocksB += 1; + } + + // Discovered by experimenting with H3 maps - object priority depends on how many tiles of object A are "blocked" by object B + // For example if blockmap of two objects looks like this: + // ABB + // AAB + // Here, in middle column object A has blocked tile that is immediately below tile blocked by object B + // Meaning, object A blocks 1 tile of object B and object B blocks 0 tiles of object A + // In this scenario in H3 object A will always appear above object B, irregardless of H3M order + if(aBlocksB != bBlocksA) + return aBlocksB < bBlocksA; + + // object that don't have clear priority via tile blocking will appear based on their row + if(a->pos.y != b->pos.y) + return a->pos.y < b->pos.y; + + // heroes should appear on top of objects on the same tile + if(b->ID==Obj::HERO && a->ID!=Obj::HERO) + return true; + if(b->ID!=Obj::HERO && a->ID==Obj::HERO) + return false; + + // or, if all other tests fail to determine priority - simply based on H3M order + return a->id < b->id; +} + +CMapHandler::CMapHandler(const CMap * map) + : map(map) +{ +} + +const CMap * CMapHandler::getMap() +{ + return map; +} + +bool CMapHandler::isInMap(const int3 & tile) +{ + return map->isInTheMap(tile); +} + +void CMapHandler::onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectFadeIn(obj, initiator); +} + +void CMapHandler::onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectFadeOut(obj, initiator); +} + +void CMapHandler::onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onBeforeHeroEmbark(obj, from, dest); +} + +void CMapHandler::onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onAfterHeroEmbark(obj, from, dest); +} + +void CMapHandler::onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onBeforeHeroDisembark(obj, from, dest); +} + +void CMapHandler::onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + for(auto * observer : observers) + observer->onAfterHeroDisembark(obj, from, dest); +} + +void CMapHandler::onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectInstantAdd(obj, initiator); +} + +void CMapHandler::onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator) +{ + for(auto * observer : observers) + observer->onObjectInstantRemove(obj, initiator); +} + +void CMapHandler::onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == dest); + for(auto * observer : observers) + observer->onAfterHeroTeleported(obj, from, dest); +} + +void CMapHandler::onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == from); + for(auto * observer : observers) + observer->onBeforeHeroTeleported(obj, from, dest); +} + +void CMapHandler::onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest) +{ + assert(obj->pos == dest); + for(auto * observer : observers) + observer->onHeroMoved(obj, from, dest); +} + +void CMapHandler::addMapObserver(IMapObjectObserver * object) +{ + observers.push_back(object); +} + +void CMapHandler::removeMapObserver(IMapObjectObserver * object) +{ + vstd::erase(observers, object); +} + +IMapObjectObserver::IMapObjectObserver() +{ + CGI->mh->addMapObserver(this); +} + +IMapObjectObserver::~IMapObjectObserver() +{ + if (CGI && CGI->mh) + CGI->mh->removeMapObserver(this); +} diff --git a/client/mapView/mapHandler.h b/client/mapView/mapHandler.h index e18f58e91..4ca5e6cc7 100644 --- a/client/mapView/mapHandler.h +++ b/client/mapView/mapHandler.h @@ -1,76 +1,76 @@ -/* - * mapHandler.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 "../gui/CIntObject.h" - -#include "../../lib/Rect.h" -#include "../../lib/int3.h" -#include "../../lib/spells/ViewSpellInt.h" - -#ifdef IN -# undef IN -#endif - -#ifdef OUT -# undef OUT -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGHeroInstance; -class CMap; - -VCMI_LIB_NAMESPACE_END - -class IMapObjectObserver; - -class CMapHandler -{ - const CMap * map; - std::vector observers; - -public: - explicit CMapHandler(const CMap * map); - - const CMap * getMap(); - - /// returns true if tile is within map bounds - bool isInMap(const int3 & tile); - - /// see MapObjectObserver interface - void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator); - void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator); - void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator); - void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator); - void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest); - - /// Add object to receive notifications on any changes in visible map state - void addMapObserver(IMapObjectObserver * observer); - void removeMapObserver(IMapObjectObserver * observer); - - /// returns string description for terrain interaction - std::string getTerrainDescr(const int3 & pos, bool rightClick) const; - - /// determines if the map is ready to handle new hero movement (not available during fading animations) - bool hasOngoingAnimations(); - - /// blocking wait until all ongoing animatins are over - void waitForOngoingAnimations(); - - static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b); -}; +/* + * mapHandler.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 "../gui/CIntObject.h" + +#include "../../lib/Rect.h" +#include "../../lib/int3.h" +#include "../../lib/spells/ViewSpellInt.h" + +#ifdef IN +# undef IN +#endif + +#ifdef OUT +# undef OUT +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGHeroInstance; +class CMap; + +VCMI_LIB_NAMESPACE_END + +class IMapObjectObserver; + +class CMapHandler +{ + const CMap * map; + std::vector observers; + +public: + explicit CMapHandler(const CMap * map); + + const CMap * getMap(); + + /// returns true if tile is within map bounds + bool isInMap(const int3 & tile); + + /// see MapObjectObserver interface + void onObjectFadeIn(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectFadeOut(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantAdd(const CGObjectInstance * obj, const PlayerColor & initiator); + void onObjectInstantRemove(const CGObjectInstance * obj, const PlayerColor & initiator); + void onBeforeHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroTeleported(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onBeforeHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroEmbark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onBeforeHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onAfterHeroDisembark(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + void onHeroMoved(const CGHeroInstance * obj, const int3 & from, const int3 & dest); + + /// Add object to receive notifications on any changes in visible map state + void addMapObserver(IMapObjectObserver * observer); + void removeMapObserver(IMapObjectObserver * observer); + + /// returns string description for terrain interaction + std::string getTerrainDescr(const int3 & pos, bool rightClick) const; + + /// determines if the map is ready to handle new hero movement (not available during fading animations) + bool hasOngoingAnimations(); + + /// blocking wait until all ongoing animatins are over + void waitForOngoingAnimations(); + + static bool compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b); +}; diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 1f81c5e34..8077c3438 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -1,367 +1,367 @@ -/* - * CAnimation.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 "CAnimation.h" - -#include "CDefFile.h" - -#include "Graphics.h" -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/JsonNode.h" -#include "../renderSDL/SDLImage.h" - -std::shared_ptr CAnimation::getFromExtraDef(std::string filename) -{ - size_t pos = filename.find(':'); - if (pos == -1) - return nullptr; - CAnimation anim(AnimationPath::builtinTODO(filename.substr(0, pos))); - pos++; - size_t frame = atoi(filename.c_str()+pos); - size_t group = 0; - pos = filename.find(':', pos); - if (pos != -1) - { - pos++; - group = frame; - frame = atoi(filename.c_str()+pos); - } - anim.load(frame ,group); - auto ret = anim.images[group][frame]; - anim.images.clear(); - return ret; -} - -bool CAnimation::loadFrame(size_t frame, size_t group) -{ - if(size(group) <= frame) - { - printError(frame, group, "LoadFrame"); - return false; - } - - auto image = getImage(frame, group, false); - if(image) - { - return true; - } - - //try to get image from def - if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) - { - if(defFile) - { - auto frameList = defFile->getEntries(); - - if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present - { - images[group][frame] = std::make_shared(defFile.get(), frame, group); - return true; - } - } - // still here? image is missing - - printError(frame, group, "LoadFrame"); - images[group][frame] = std::make_shared(ImagePath::builtin("DEFAULT"), EImageBlitMode::ALPHA); - } - else //load from separate file - { - auto img = getFromExtraDef(source[group][frame]["file"].String()); - if(!img) - img = std::make_shared(source[group][frame], EImageBlitMode::ALPHA); - - images[group][frame] = img; - return true; - } - return false; -} - -bool CAnimation::unloadFrame(size_t frame, size_t group) -{ - auto image = getImage(frame, group, false); - if(image) - { - images[group].erase(frame); - - if(images[group].empty()) - images.erase(group); - return true; - } - return false; -} - -void CAnimation::initFromJson(const JsonNode & config) -{ - std::string basepath; - basepath = config["basepath"].String(); - - JsonNode base(JsonNode::JsonType::DATA_STRUCT); - base["margins"] = config["margins"]; - base["width"] = config["width"]; - base["height"] = config["height"]; - - for(const JsonNode & group : config["sequences"].Vector()) - { - size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING) - source[groupID].clear(); - - for(const JsonNode & frame : group["frames"].Vector()) - { - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); - JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + frame.String(); - source[groupID].push_back(toAdd); - } - } - - for(const JsonNode & node : config["images"].Vector()) - { - size_t group = node["group"].Integer(); - size_t frame = node["frame"].Integer(); - - if (source[group].size() <= frame) - source[group].resize(frame+1); - - JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); - JsonUtils::inherit(toAdd, base); - toAdd["file"].String() = basepath + node["file"].String(); - source[group][frame] = toAdd; - } -} - -void CAnimation::exportBitmaps(const boost::filesystem::path& path) const -{ - if(images.empty()) - { - logGlobal->error("Nothing to export, animation is empty"); - return; - } - - boost::filesystem::path actualPath = path / "SPRITES" / name.getName(); - boost::filesystem::create_directories(actualPath); - - size_t counter = 0; - - for(const auto & groupPair : images) - { - size_t group = groupPair.first; - - for(const auto & imagePair : groupPair.second) - { - size_t frame = imagePair.first; - const auto img = imagePair.second; - - boost::format fmt("%d_%d.bmp"); - fmt % group % frame; - - img->exportBitmap(actualPath / fmt.str()); - counter++; - } - } - - logGlobal->info("Exported %d frames to %s", counter, actualPath.string()); -} - -void CAnimation::init() -{ - if(defFile) - { - const std::map defEntries = defFile->getEntries(); - - for (auto & defEntry : defEntries) - source[defEntry.first].resize(defEntry.second); - } - - if (vstd::contains(graphics->imageLists, name.getName())) - initFromJson(graphics->imageLists[name.getName()]); - - auto jsonResource = name.toType(); - auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); - - for(auto & loader : configList) - { - auto stream = loader->load(jsonResource); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - const JsonNode config((char*)textData.get(), stream->getSize()); - - initFromJson(config); - } -} - -void CAnimation::printError(size_t frame, size_t group, std::string type) const -{ - logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name.getOriginalName(), group, frame); -} - -CAnimation::CAnimation(const AnimationPath & Name): - name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), - preloaded(false), - defFile() -{ - if(CResourceHandler::get()->existsResource(name)) - defFile = std::make_shared(name); - - init(); - - if(source.empty()) - logAnim->error("Animation %s failed to load", Name.getOriginalName()); -} - -CAnimation::CAnimation(): - preloaded(false) -{ - init(); -} - -CAnimation::~CAnimation() = default; - -void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup) -{ - if(!source.count(sourceGroup)) - { - logAnim->error("Group %d missing in %s", sourceGroup, name.getName()); - return; - } - - if(source[sourceGroup].size() <= sourceFrame) - { - logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name.getName()); - return; - } - - //todo: clone actual loaded Image object - JsonNode clone(source[sourceGroup][sourceFrame]); - - if(clone.getType() == JsonNode::JsonType::DATA_NULL) - { - std::string temp = name.getName()+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); - clone["file"].String() = temp; - } - - source[targetGroup].push_back(clone); - - size_t index = source[targetGroup].size() - 1; - - if(preloaded) - load(index, targetGroup); -} - -void CAnimation::setCustom(std::string filename, size_t frame, size_t group) -{ - if (source[group].size() <= frame) - source[group].resize(frame+1); - source[group][frame]["file"].String() = filename; - //FIXME: update image if already loaded -} - -std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) const -{ - auto groupIter = images.find(group); - if (groupIter != images.end()) - { - auto imageIter = groupIter->second.find(frame); - if (imageIter != groupIter->second.end()) - return imageIter->second; - } - if (verbose) - printError(frame, group, "GetImage"); - return nullptr; -} - -void CAnimation::load() -{ - for (auto & elem : source) - for (size_t image=0; image < elem.second.size(); image++) - loadFrame(image, elem.first); -} - -void CAnimation::unload() -{ - for (auto & elem : source) - for (size_t image=0; image < elem.second.size(); image++) - unloadFrame(image, elem.first); - -} - -void CAnimation::preload() -{ - if(!preloaded) - { - preloaded = true; - load(); - } -} - -void CAnimation::loadGroup(size_t group) -{ - if (vstd::contains(source, group)) - for (size_t image=0; image < source[group].size(); image++) - loadFrame(image, group); -} - -void CAnimation::unloadGroup(size_t group) -{ - if (vstd::contains(source, group)) - for (size_t image=0; image < source[group].size(); image++) - unloadFrame(image, group); -} - -void CAnimation::load(size_t frame, size_t group) -{ - loadFrame(frame, group); -} - -void CAnimation::unload(size_t frame, size_t group) -{ - unloadFrame(frame, group); -} - -size_t CAnimation::size(size_t group) const -{ - auto iter = source.find(group); - if (iter != source.end()) - return iter->second.size(); - return 0; -} - -void CAnimation::horizontalFlip() -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->horizontalFlip(); -} - -void CAnimation::verticalFlip() -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->verticalFlip(); -} - -void CAnimation::playerColored(PlayerColor player) -{ - for(auto & group : images) - for(auto & image : group.second) - image.second->playerColored(player); -} - -void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) -{ - for(size_t frame = 0; frame < size(sourceGroup); ++frame) - { - duplicateImage(sourceGroup, frame, targetGroup); - - auto image = getImage(frame, targetGroup); - image->verticalFlip(); - } -} - +/* + * CAnimation.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 "CAnimation.h" + +#include "CDefFile.h" + +#include "Graphics.h" +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/JsonNode.h" +#include "../renderSDL/SDLImage.h" + +std::shared_ptr CAnimation::getFromExtraDef(std::string filename) +{ + size_t pos = filename.find(':'); + if (pos == -1) + return nullptr; + CAnimation anim(AnimationPath::builtinTODO(filename.substr(0, pos))); + pos++; + size_t frame = atoi(filename.c_str()+pos); + size_t group = 0; + pos = filename.find(':', pos); + if (pos != -1) + { + pos++; + group = frame; + frame = atoi(filename.c_str()+pos); + } + anim.load(frame ,group); + auto ret = anim.images[group][frame]; + anim.images.clear(); + return ret; +} + +bool CAnimation::loadFrame(size_t frame, size_t group) +{ + if(size(group) <= frame) + { + printError(frame, group, "LoadFrame"); + return false; + } + + auto image = getImage(frame, group, false); + if(image) + { + return true; + } + + //try to get image from def + if(source[group][frame].getType() == JsonNode::JsonType::DATA_NULL) + { + if(defFile) + { + auto frameList = defFile->getEntries(); + + if(vstd::contains(frameList, group) && frameList.at(group) > frame) // frame is present + { + images[group][frame] = std::make_shared(defFile.get(), frame, group); + return true; + } + } + // still here? image is missing + + printError(frame, group, "LoadFrame"); + images[group][frame] = std::make_shared(ImagePath::builtin("DEFAULT"), EImageBlitMode::ALPHA); + } + else //load from separate file + { + auto img = getFromExtraDef(source[group][frame]["file"].String()); + if(!img) + img = std::make_shared(source[group][frame], EImageBlitMode::ALPHA); + + images[group][frame] = img; + return true; + } + return false; +} + +bool CAnimation::unloadFrame(size_t frame, size_t group) +{ + auto image = getImage(frame, group, false); + if(image) + { + images[group].erase(frame); + + if(images[group].empty()) + images.erase(group); + return true; + } + return false; +} + +void CAnimation::initFromJson(const JsonNode & config) +{ + std::string basepath; + basepath = config["basepath"].String(); + + JsonNode base(JsonNode::JsonType::DATA_STRUCT); + base["margins"] = config["margins"]; + base["width"] = config["width"]; + base["height"] = config["height"]; + + for(const JsonNode & group : config["sequences"].Vector()) + { + size_t groupID = group["group"].Integer();//TODO: string-to-value conversion("moving" -> MOVING) + source[groupID].clear(); + + for(const JsonNode & frame : group["frames"].Vector()) + { + JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonUtils::inherit(toAdd, base); + toAdd["file"].String() = basepath + frame.String(); + source[groupID].push_back(toAdd); + } + } + + for(const JsonNode & node : config["images"].Vector()) + { + size_t group = node["group"].Integer(); + size_t frame = node["frame"].Integer(); + + if (source[group].size() <= frame) + source[group].resize(frame+1); + + JsonNode toAdd(JsonNode::JsonType::DATA_STRUCT); + JsonUtils::inherit(toAdd, base); + toAdd["file"].String() = basepath + node["file"].String(); + source[group][frame] = toAdd; + } +} + +void CAnimation::exportBitmaps(const boost::filesystem::path& path) const +{ + if(images.empty()) + { + logGlobal->error("Nothing to export, animation is empty"); + return; + } + + boost::filesystem::path actualPath = path / "SPRITES" / name.getName(); + boost::filesystem::create_directories(actualPath); + + size_t counter = 0; + + for(const auto & groupPair : images) + { + size_t group = groupPair.first; + + for(const auto & imagePair : groupPair.second) + { + size_t frame = imagePair.first; + const auto img = imagePair.second; + + boost::format fmt("%d_%d.bmp"); + fmt % group % frame; + + img->exportBitmap(actualPath / fmt.str()); + counter++; + } + } + + logGlobal->info("Exported %d frames to %s", counter, actualPath.string()); +} + +void CAnimation::init() +{ + if(defFile) + { + const std::map defEntries = defFile->getEntries(); + + for (auto & defEntry : defEntries) + source[defEntry.first].resize(defEntry.second); + } + + if (vstd::contains(graphics->imageLists, name.getName())) + initFromJson(graphics->imageLists[name.getName()]); + + auto jsonResource = name.toType(); + auto configList = CResourceHandler::get()->getResourcesWithName(jsonResource); + + for(auto & loader : configList) + { + auto stream = loader->load(jsonResource); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + + const JsonNode config((char*)textData.get(), stream->getSize()); + + initFromJson(config); + } +} + +void CAnimation::printError(size_t frame, size_t group, std::string type) const +{ + logGlobal->error("%s error: Request for frame not present in CAnimation! File name: %s, Group: %d, Frame: %d", type, name.getOriginalName(), group, frame); +} + +CAnimation::CAnimation(const AnimationPath & Name): + name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), + preloaded(false), + defFile() +{ + if(CResourceHandler::get()->existsResource(name)) + defFile = std::make_shared(name); + + init(); + + if(source.empty()) + logAnim->error("Animation %s failed to load", Name.getOriginalName()); +} + +CAnimation::CAnimation(): + preloaded(false) +{ + init(); +} + +CAnimation::~CAnimation() = default; + +void CAnimation::duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup) +{ + if(!source.count(sourceGroup)) + { + logAnim->error("Group %d missing in %s", sourceGroup, name.getName()); + return; + } + + if(source[sourceGroup].size() <= sourceFrame) + { + logAnim->error("Frame [%d %d] missing in %s", sourceGroup, sourceFrame, name.getName()); + return; + } + + //todo: clone actual loaded Image object + JsonNode clone(source[sourceGroup][sourceFrame]); + + if(clone.getType() == JsonNode::JsonType::DATA_NULL) + { + std::string temp = name.getName()+":"+std::to_string(sourceGroup)+":"+std::to_string(sourceFrame); + clone["file"].String() = temp; + } + + source[targetGroup].push_back(clone); + + size_t index = source[targetGroup].size() - 1; + + if(preloaded) + load(index, targetGroup); +} + +void CAnimation::setCustom(std::string filename, size_t frame, size_t group) +{ + if (source[group].size() <= frame) + source[group].resize(frame+1); + source[group][frame]["file"].String() = filename; + //FIXME: update image if already loaded +} + +std::shared_ptr CAnimation::getImage(size_t frame, size_t group, bool verbose) const +{ + auto groupIter = images.find(group); + if (groupIter != images.end()) + { + auto imageIter = groupIter->second.find(frame); + if (imageIter != groupIter->second.end()) + return imageIter->second; + } + if (verbose) + printError(frame, group, "GetImage"); + return nullptr; +} + +void CAnimation::load() +{ + for (auto & elem : source) + for (size_t image=0; image < elem.second.size(); image++) + loadFrame(image, elem.first); +} + +void CAnimation::unload() +{ + for (auto & elem : source) + for (size_t image=0; image < elem.second.size(); image++) + unloadFrame(image, elem.first); + +} + +void CAnimation::preload() +{ + if(!preloaded) + { + preloaded = true; + load(); + } +} + +void CAnimation::loadGroup(size_t group) +{ + if (vstd::contains(source, group)) + for (size_t image=0; image < source[group].size(); image++) + loadFrame(image, group); +} + +void CAnimation::unloadGroup(size_t group) +{ + if (vstd::contains(source, group)) + for (size_t image=0; image < source[group].size(); image++) + unloadFrame(image, group); +} + +void CAnimation::load(size_t frame, size_t group) +{ + loadFrame(frame, group); +} + +void CAnimation::unload(size_t frame, size_t group) +{ + unloadFrame(frame, group); +} + +size_t CAnimation::size(size_t group) const +{ + auto iter = source.find(group); + if (iter != source.end()) + return iter->second.size(); + return 0; +} + +void CAnimation::horizontalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->horizontalFlip(); +} + +void CAnimation::verticalFlip() +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->verticalFlip(); +} + +void CAnimation::playerColored(PlayerColor player) +{ + for(auto & group : images) + for(auto & image : group.second) + image.second->playerColored(player); +} + +void CAnimation::createFlippedGroup(const size_t sourceGroup, const size_t targetGroup) +{ + for(size_t frame = 0; frame < size(sourceGroup); ++frame) + { + duplicateImage(sourceGroup, frame, targetGroup); + + auto image = getImage(frame, targetGroup); + image->verticalFlip(); + } +} + diff --git a/client/render/CAnimation.h b/client/render/CAnimation.h index 7332051c8..efb66f602 100644 --- a/client/render/CAnimation.h +++ b/client/render/CAnimation.h @@ -1,95 +1,95 @@ -/* - * CAnimation.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/GameConstants.h" -#include "../../lib/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -class CDefFile; -class IImage; -class RenderHandler; - -/// Class for handling animation -class CAnimation -{ -private: - //source[group][position] - file with this frame, if string is empty - image located in def file - std::map > source; - - //bitmap[group][position], store objects with loaded bitmaps - std::map > > images; - - //animation file name - AnimationPath name; - - bool preloaded; - - std::shared_ptr defFile; - - //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded - bool loadFrame(size_t frame, size_t group); - - //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) - bool unloadFrame(size_t frame, size_t group); - - //initialize animation from file - void initFromJson(const JsonNode & input); - void init(); - - //to get rid of copy-pasting error message :] - void printError(size_t frame, size_t group, std::string type) const; - - //not a very nice method to get image from another def file - //TODO: remove after implementing resource manager - std::shared_ptr getFromExtraDef(std::string filename); - -public: - CAnimation(const AnimationPath & Name); - CAnimation(); - ~CAnimation(); - - //duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup - //and loads it if animation is preloaded - void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); - - //add custom surface to the selected position. - void setCustom(std::string filename, size_t frame, size_t group=0); - - std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true) const; - - void exportBitmaps(const boost::filesystem::path & path) const; - - //all available frames - void load (); - void unload(); - void preload(); - - //all frames from group - void loadGroup (size_t group); - void unloadGroup(size_t group); - - //single image - void load (size_t frame, size_t group=0); - void unload(size_t frame, size_t group=0); - - //total count of frames in group (including not loaded) - size_t size(size_t group=0) const; - - void horizontalFlip(); - void verticalFlip(); - void playerColored(PlayerColor player); - - void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); -}; - +/* + * CAnimation.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/GameConstants.h" +#include "../../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class CDefFile; +class IImage; +class RenderHandler; + +/// Class for handling animation +class CAnimation +{ +private: + //source[group][position] - file with this frame, if string is empty - image located in def file + std::map > source; + + //bitmap[group][position], store objects with loaded bitmaps + std::map > > images; + + //animation file name + AnimationPath name; + + bool preloaded; + + std::shared_ptr defFile; + + //loader, will be called by load(), require opened def file for loading from it. Returns true if image is loaded + bool loadFrame(size_t frame, size_t group); + + //unloadFrame, returns true if image has been unloaded ( either deleted or decreased refCount) + bool unloadFrame(size_t frame, size_t group); + + //initialize animation from file + void initFromJson(const JsonNode & input); + void init(); + + //to get rid of copy-pasting error message :] + void printError(size_t frame, size_t group, std::string type) const; + + //not a very nice method to get image from another def file + //TODO: remove after implementing resource manager + std::shared_ptr getFromExtraDef(std::string filename); + +public: + CAnimation(const AnimationPath & Name); + CAnimation(); + ~CAnimation(); + + //duplicates frame at [sourceGroup, sourceFrame] as last frame in targetGroup + //and loads it if animation is preloaded + void duplicateImage(const size_t sourceGroup, const size_t sourceFrame, const size_t targetGroup); + + //add custom surface to the selected position. + void setCustom(std::string filename, size_t frame, size_t group=0); + + std::shared_ptr getImage(size_t frame, size_t group=0, bool verbose=true) const; + + void exportBitmaps(const boost::filesystem::path & path) const; + + //all available frames + void load (); + void unload(); + void preload(); + + //all frames from group + void loadGroup (size_t group); + void unloadGroup(size_t group); + + //single image + void load (size_t frame, size_t group=0); + void unload(size_t frame, size_t group=0); + + //total count of frames in group (including not loaded) + size_t size(size_t group=0) const; + + void horizontalFlip(); + void verticalFlip(); + void playerColored(PlayerColor player); + + void createFlippedGroup(const size_t sourceGroup, const size_t targetGroup); +}; + diff --git a/client/render/CBitmapHandler.cpp b/client/render/CBitmapHandler.cpp index 626d3d31f..79b597480 100644 --- a/client/render/CBitmapHandler.cpp +++ b/client/render/CBitmapHandler.cpp @@ -1,210 +1,210 @@ -/* - * CBitmapHandler.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 "CBitmapHandler.h" - -#include "../renderSDL/SDL_Extensions.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/vcmi_endian.h" - -#include - -namespace BitmapHandler -{ - SDL_Surface * loadH3PCX(ui8 * data, size_t size); - - SDL_Surface * loadBitmapFromDir(const ImagePath & path); -} - -bool isPCX(const ui8 *header)//check whether file can be PCX according to header -{ - ui32 fSize = read_le_u32(header + 0); - ui32 width = read_le_u32(header + 4); - ui32 height = read_le_u32(header + 8); - return fSize == width*height || fSize == width*height*3; -} - -enum Epcxformat -{ - PCX8B, - PCX24B -}; - -SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) -{ - SDL_Surface * ret; - - Epcxformat format; - int it=0; - - ui32 fSize = read_le_u32(pcx + it); it+=4; - ui32 width = read_le_u32(pcx + it); it+=4; - ui32 height = read_le_u32(pcx + it); it+=4; - - if (fSize==width*height*3) - format=PCX24B; - else if (fSize==width*height) - format=PCX8B; - else - return nullptr; - - if (format==PCX8B) - { - ret = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); - - it = 0xC; - for (int i=0; i<(int)height; i++) - { - memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width); - it+= width; - } - - //palette - last 256*3 bytes - it = (int)size-256*3; - for (int i=0;i<256;i++) - { - SDL_Color tp; - tp.r = pcx[it++]; - tp.g = pcx[it++]; - tp.b = pcx[it++]; - tp.a = SDL_ALPHA_OPAQUE; - ret->format->palette->colors[i] = tp; - } - } - else - { -#ifdef VCMI_ENDIAN_BIG - int bmask = 0xff0000; - int gmask = 0x00ff00; - int rmask = 0x0000ff; -#else - int bmask = 0x0000ff; - int gmask = 0x00ff00; - int rmask = 0xff0000; -#endif - ret = SDL_CreateRGBSurface(0, width, height, 24, rmask, gmask, bmask, 0); - - //it == 0xC; - for (int i=0; i<(int)height; i++) - { - memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width*3); - it+= width*3; - } - - } - return ret; -} - -SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) -{ - if (!CResourceHandler::get()->existsResource(path)) - { - return nullptr; - } - - SDL_Surface * ret=nullptr; - - auto readFile = CResourceHandler::get()->load(path)->readAll(); - - if (isPCX(readFile.first.get())) - {//H3-style PCX - ret = loadH3PCX(readFile.first.get(), readFile.second); - if (!ret) - { - logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName()); - return nullptr; - } - } - else - { //loading via SDL_Image - ret = IMG_Load_RW( - //create SDL_RW with our data (will be deleted by SDL) - SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second), - 1); // mark it for auto-deleting - if (ret) - { - if (ret->format->palette) - { - // set correct value for alpha\unused channel - // NOTE: might be unnecessary with SDL2 - for (int i=0; i < ret->format->palette->ncolors; i++) - ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; - } - } - else - { - logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName()); - logGlobal->error("Reason: %s", IMG_GetError()); - return nullptr; - } - } - - // When modifying anything here please check two use cases: - // 1) Vampire mansion in Necropolis (not 1st color is transparent) - // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) - // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) - // 4) special case - there are 2 .bmp images that have semi-transparency (CCELLGRD.BMP & CCELLSHD.BMP) - if (ret->format->palette && - ret->format->palette->colors[0].r == 255 && - ret->format->palette->colors[0].g == 0 && - ret->format->palette->colors[0].b == 255 ) - { - static SDL_Color shadow[3] = - { - { 0, 0, 0, 0},// 100% - transparency - { 0, 0, 0, 32},// 75% - shadow border, - { 0, 0, 0, 128},// 50% - shadow body - }; - - CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]); - - ret->format->palette->colors[0] = shadow[0]; - ret->format->palette->colors[1] = shadow[1]; - ret->format->palette->colors[4] = shadow[2]; - } - else if (ret->format->palette) - { - CSDL_Ext::setDefaultColorKeyPresize(ret); - } - else if (ret->format->Amask) - { - SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); - } - else // always set - { - CSDL_Ext::setDefaultColorKey(ret); - } - return ret; -} - -SDL_Surface * BitmapHandler::loadBitmap(const ImagePath & fname) -{ - if(fname.empty()) - { - logGlobal->warn("Call to loadBitmap with void fname!"); - return nullptr; - } - - SDL_Surface * bitmap = loadBitmapFromDir(fname); - if (bitmap != nullptr) - return bitmap; - - SDL_Surface * bitmapData = loadBitmapFromDir(fname.addPrefix("DATA/")); - if (bitmapData != nullptr) - return bitmapData; - - SDL_Surface * bitmapSprites = loadBitmapFromDir(fname.addPrefix("SPRITES/")); - if (bitmapSprites != nullptr) - return bitmapSprites; - - logGlobal->error("Error: Failed to find file %s", fname.getOriginalName()); - return nullptr; -} +/* + * CBitmapHandler.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 "CBitmapHandler.h" + +#include "../renderSDL/SDL_Extensions.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/vcmi_endian.h" + +#include + +namespace BitmapHandler +{ + SDL_Surface * loadH3PCX(ui8 * data, size_t size); + + SDL_Surface * loadBitmapFromDir(const ImagePath & path); +} + +bool isPCX(const ui8 *header)//check whether file can be PCX according to header +{ + ui32 fSize = read_le_u32(header + 0); + ui32 width = read_le_u32(header + 4); + ui32 height = read_le_u32(header + 8); + return fSize == width*height || fSize == width*height*3; +} + +enum Epcxformat +{ + PCX8B, + PCX24B +}; + +SDL_Surface * BitmapHandler::loadH3PCX(ui8 * pcx, size_t size) +{ + SDL_Surface * ret; + + Epcxformat format; + int it=0; + + ui32 fSize = read_le_u32(pcx + it); it+=4; + ui32 width = read_le_u32(pcx + it); it+=4; + ui32 height = read_le_u32(pcx + it); it+=4; + + if (fSize==width*height*3) + format=PCX24B; + else if (fSize==width*height) + format=PCX8B; + else + return nullptr; + + if (format==PCX8B) + { + ret = SDL_CreateRGBSurface(0, width, height, 8, 0, 0, 0, 0); + + it = 0xC; + for (int i=0; i<(int)height; i++) + { + memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width); + it+= width; + } + + //palette - last 256*3 bytes + it = (int)size-256*3; + for (int i=0;i<256;i++) + { + SDL_Color tp; + tp.r = pcx[it++]; + tp.g = pcx[it++]; + tp.b = pcx[it++]; + tp.a = SDL_ALPHA_OPAQUE; + ret->format->palette->colors[i] = tp; + } + } + else + { +#ifdef VCMI_ENDIAN_BIG + int bmask = 0xff0000; + int gmask = 0x00ff00; + int rmask = 0x0000ff; +#else + int bmask = 0x0000ff; + int gmask = 0x00ff00; + int rmask = 0xff0000; +#endif + ret = SDL_CreateRGBSurface(0, width, height, 24, rmask, gmask, bmask, 0); + + //it == 0xC; + for (int i=0; i<(int)height; i++) + { + memcpy((char*)ret->pixels + ret->pitch * i, pcx + it, width*3); + it+= width*3; + } + + } + return ret; +} + +SDL_Surface * BitmapHandler::loadBitmapFromDir(const ImagePath & path) +{ + if (!CResourceHandler::get()->existsResource(path)) + { + return nullptr; + } + + SDL_Surface * ret=nullptr; + + auto readFile = CResourceHandler::get()->load(path)->readAll(); + + if (isPCX(readFile.first.get())) + {//H3-style PCX + ret = loadH3PCX(readFile.first.get(), readFile.second); + if (!ret) + { + logGlobal->error("Failed to open %s as H3 PCX!", path.getOriginalName()); + return nullptr; + } + } + else + { //loading via SDL_Image + ret = IMG_Load_RW( + //create SDL_RW with our data (will be deleted by SDL) + SDL_RWFromConstMem((void*)readFile.first.get(), (int)readFile.second), + 1); // mark it for auto-deleting + if (ret) + { + if (ret->format->palette) + { + // set correct value for alpha\unused channel + // NOTE: might be unnecessary with SDL2 + for (int i=0; i < ret->format->palette->ncolors; i++) + ret->format->palette->colors[i].a = SDL_ALPHA_OPAQUE; + } + } + else + { + logGlobal->error("Failed to open %s via SDL_Image", path.getOriginalName()); + logGlobal->error("Reason: %s", IMG_GetError()); + return nullptr; + } + } + + // When modifying anything here please check two use cases: + // 1) Vampire mansion in Necropolis (not 1st color is transparent) + // 2) Battle background when fighting on grass/dirt, topmost sky part (NO transparent color) + // 3) New objects that may use 24-bit images for icons (e.g. witchking arts) + // 4) special case - there are 2 .bmp images that have semi-transparency (CCELLGRD.BMP & CCELLSHD.BMP) + if (ret->format->palette && + ret->format->palette->colors[0].r == 255 && + ret->format->palette->colors[0].g == 0 && + ret->format->palette->colors[0].b == 255 ) + { + static SDL_Color shadow[3] = + { + { 0, 0, 0, 0},// 100% - transparency + { 0, 0, 0, 32},// 75% - shadow border, + { 0, 0, 0, 128},// 50% - shadow body + }; + + CSDL_Ext::setColorKey(ret, ret->format->palette->colors[0]); + + ret->format->palette->colors[0] = shadow[0]; + ret->format->palette->colors[1] = shadow[1]; + ret->format->palette->colors[4] = shadow[2]; + } + else if (ret->format->palette) + { + CSDL_Ext::setDefaultColorKeyPresize(ret); + } + else if (ret->format->Amask) + { + SDL_SetSurfaceBlendMode(ret, SDL_BLENDMODE_BLEND); + } + else // always set + { + CSDL_Ext::setDefaultColorKey(ret); + } + return ret; +} + +SDL_Surface * BitmapHandler::loadBitmap(const ImagePath & fname) +{ + if(fname.empty()) + { + logGlobal->warn("Call to loadBitmap with void fname!"); + return nullptr; + } + + SDL_Surface * bitmap = loadBitmapFromDir(fname); + if (bitmap != nullptr) + return bitmap; + + SDL_Surface * bitmapData = loadBitmapFromDir(fname.addPrefix("DATA/")); + if (bitmapData != nullptr) + return bitmapData; + + SDL_Surface * bitmapSprites = loadBitmapFromDir(fname.addPrefix("SPRITES/")); + if (bitmapSprites != nullptr) + return bitmapSprites; + + logGlobal->error("Error: Failed to find file %s", fname.getOriginalName()); + return nullptr; +} diff --git a/client/render/CBitmapHandler.h b/client/render/CBitmapHandler.h index 3bd8a7e82..ac3ae5f35 100644 --- a/client/render/CBitmapHandler.h +++ b/client/render/CBitmapHandler.h @@ -1,20 +1,20 @@ -/* - * CBitmapHandler.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/filesystem/ResourcePath.h" - -struct SDL_Surface; - -namespace BitmapHandler -{ - //Load file from /DATA or /SPRITES - SDL_Surface * loadBitmap(const ImagePath & fname); -} +/* + * CBitmapHandler.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/filesystem/ResourcePath.h" + +struct SDL_Surface; + +namespace BitmapHandler +{ + //Load file from /DATA or /SPRITES + SDL_Surface * loadBitmap(const ImagePath & fname); +} diff --git a/client/render/CDefFile.cpp b/client/render/CDefFile.cpp index bf4a79b06..23377c4e5 100644 --- a/client/render/CDefFile.cpp +++ b/client/render/CDefFile.cpp @@ -1,338 +1,338 @@ -/* - * CDefFile.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 "CDefFile.h" - -#include "IImageLoader.h" - -#include "../../lib/filesystem/Filesystem.h" -#include "../../lib/Point.h" - -#include - -// Extremely simple file cache. TODO: smarter, more general solution -class CFileCache -{ - static const int cacheSize = 50; //Max number of cached files - struct FileData - { - AnimationPath name; - size_t size; - std::unique_ptr data; - - std::unique_ptr getCopy() - { - auto ret = std::unique_ptr(new ui8[size]); - std::copy(data.get(), data.get() + size, ret.get()); - return ret; - } - FileData(AnimationPath name_, size_t size_, std::unique_ptr data_): - name{std::move(name_)}, - size{size_}, - data{std::move(data_)} - {} - }; - - std::deque cache; -public: - std::unique_ptr getCachedFile(AnimationPath rid) - { - for(auto & file : cache) - { - if (file.name == rid) - return file.getCopy(); - } - // Still here? Cache miss - if (cache.size() > cacheSize) - cache.pop_front(); - - auto data = CResourceHandler::get()->load(rid)->readAll(); - - cache.emplace_back(std::move(rid), data.second, std::move(data.first)); - - return cache.back().getCopy(); - } -}; - -enum class DefType : uint32_t -{ - SPELL = 0x40, - SPRITE = 0x41, - CREATURE = 0x42, - MAP = 0x43, - MAP_HERO = 0x44, - TERRAIN = 0x45, - CURSOR = 0x46, - INTERFACE = 0x47, - SPRITE_FRAME = 0x48, - BATTLE_HERO = 0x49 -}; - -static CFileCache animationCache; - -/************************************************************************* - * DefFile, class used for def loading * - *************************************************************************/ - -static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs) -{ - // it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow - // exact logic is not clear and requires extensive testing with image editing - // potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component - static const int threshold = 8; - - int diffR = static_cast(lhs.r) - rhs.r; - int diffG = static_cast(lhs.g) - rhs.g; - int diffB = static_cast(lhs.b) - rhs.b; - int diffA = static_cast(lhs.a) - rhs.a; - - return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; -} - -CDefFile::CDefFile(const AnimationPath & Name): - data(nullptr), - palette(nullptr) -{ - //First 8 colors in def palette used for transparency - static const SDL_Color sourcePalette[8] = { - {0, 255, 255, SDL_ALPHA_OPAQUE}, - {255, 150, 255, SDL_ALPHA_OPAQUE}, - {255, 100, 255, SDL_ALPHA_OPAQUE}, - {255, 50, 255, SDL_ALPHA_OPAQUE}, - {255, 0, 255, SDL_ALPHA_OPAQUE}, - {255, 255, 0, SDL_ALPHA_OPAQUE}, - {180, 0, 255, SDL_ALPHA_OPAQUE}, - {0, 255, 0, SDL_ALPHA_OPAQUE} - }; - - static const SDL_Color targetPalette[8] = { - {0, 0, 0, 0 }, // transparency ( used in most images ) - {0, 0, 0, 64 }, // shadow border ( used in battle, adventure map def's ) - {0, 0, 0, 64 }, // shadow border ( used in fog-of-war def's ) - {0, 0, 0, 128}, // shadow body ( used in fog-of-war def's ) - {0, 0, 0, 128}, // shadow body ( used in battle, adventure map def's ) - {0, 0, 0, 0 }, // selection / owner flag ( used in battle, adventure map def's ) - {0, 0, 0, 128}, // shadow body below selection ( used in battle def's ) - {0, 0, 0, 64 } // shadow border below selection ( used in battle def's ) - }; - - data = animationCache.getCachedFile(Name); - - palette = std::unique_ptr(new SDL_Color[256]); - int it = 0; - - //ui32 type = read_le_u32(data.get() + it); - it+=4; - //int width = read_le_u32(data + it); it+=4;//not used - //int height = read_le_u32(data + it); it+=4; - it+=8; - ui32 totalBlocks = read_le_u32(data.get() + it); - it+=4; - - for (ui32 i= 0; i<256; i++) - { - palette[i].r = data[it++]; - palette[i].g = data[it++]; - palette[i].b = data[it++]; - palette[i].a = SDL_ALPHA_OPAQUE; - } - - // these colors seems to be used unconditionally - palette[0] = targetPalette[0]; - palette[1] = targetPalette[1]; - palette[4] = targetPalette[4]; - - // rest of special colors are used only if their RGB values are close to H3 - for (uint32_t i = 0; i < 8; ++i) - { - if (colorsSimilar(sourcePalette[i], palette[i])) - palette[i] = targetPalette[i]; - } - - for (ui32 i=0; i >::const_iterator it; - it = offset.find(group); - assert (it != offset.end()); - - const ui8 * FDef = data.get()+it->second[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); - - ui32 currentOffset = sizeof(SSpriteDef); - - //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) - - if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) - { - sprite.leftMargin = 0; - sprite.topMargin = 0; - sprite.width = sprite.fullWidth; - sprite.height = sprite.fullHeight; - - currentOffset -= 16; - } - - const ui32 BaseOffset = currentOffset; - - loader.init(Point(sprite.width, sprite.height), - Point(sprite.leftMargin, sprite.topMargin), - Point(sprite.fullWidth, sprite.fullHeight), palette.get()); - - switch(sprite.format) - { - case 0: - { - //pixel data is not compressed, copy data to surface - for(ui32 i=0; i(FDef+currentOffset); - currentOffset += sizeof(ui32) * sprite.height; - - for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); - break; - } -} - -CDefFile::~CDefFile() = default; - -const std::map CDefFile::getEntries() const -{ - std::map ret; - - for (auto & elem : offset) - ret[elem.first] = elem.second.size(); - return ret; -} - +/* + * CDefFile.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 "CDefFile.h" + +#include "IImageLoader.h" + +#include "../../lib/filesystem/Filesystem.h" +#include "../../lib/Point.h" + +#include + +// Extremely simple file cache. TODO: smarter, more general solution +class CFileCache +{ + static const int cacheSize = 50; //Max number of cached files + struct FileData + { + AnimationPath name; + size_t size; + std::unique_ptr data; + + std::unique_ptr getCopy() + { + auto ret = std::unique_ptr(new ui8[size]); + std::copy(data.get(), data.get() + size, ret.get()); + return ret; + } + FileData(AnimationPath name_, size_t size_, std::unique_ptr data_): + name{std::move(name_)}, + size{size_}, + data{std::move(data_)} + {} + }; + + std::deque cache; +public: + std::unique_ptr getCachedFile(AnimationPath rid) + { + for(auto & file : cache) + { + if (file.name == rid) + return file.getCopy(); + } + // Still here? Cache miss + if (cache.size() > cacheSize) + cache.pop_front(); + + auto data = CResourceHandler::get()->load(rid)->readAll(); + + cache.emplace_back(std::move(rid), data.second, std::move(data.first)); + + return cache.back().getCopy(); + } +}; + +enum class DefType : uint32_t +{ + SPELL = 0x40, + SPRITE = 0x41, + CREATURE = 0x42, + MAP = 0x43, + MAP_HERO = 0x44, + TERRAIN = 0x45, + CURSOR = 0x46, + INTERFACE = 0x47, + SPRITE_FRAME = 0x48, + BATTLE_HERO = 0x49 +}; + +static CFileCache animationCache; + +/************************************************************************* + * DefFile, class used for def loading * + *************************************************************************/ + +static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs) +{ + // it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow + // exact logic is not clear and requires extensive testing with image editing + // potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component + static const int threshold = 8; + + int diffR = static_cast(lhs.r) - rhs.r; + int diffG = static_cast(lhs.g) - rhs.g; + int diffB = static_cast(lhs.b) - rhs.b; + int diffA = static_cast(lhs.a) - rhs.a; + + return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold; +} + +CDefFile::CDefFile(const AnimationPath & Name): + data(nullptr), + palette(nullptr) +{ + //First 8 colors in def palette used for transparency + static const SDL_Color sourcePalette[8] = { + {0, 255, 255, SDL_ALPHA_OPAQUE}, + {255, 150, 255, SDL_ALPHA_OPAQUE}, + {255, 100, 255, SDL_ALPHA_OPAQUE}, + {255, 50, 255, SDL_ALPHA_OPAQUE}, + {255, 0, 255, SDL_ALPHA_OPAQUE}, + {255, 255, 0, SDL_ALPHA_OPAQUE}, + {180, 0, 255, SDL_ALPHA_OPAQUE}, + {0, 255, 0, SDL_ALPHA_OPAQUE} + }; + + static const SDL_Color targetPalette[8] = { + {0, 0, 0, 0 }, // transparency ( used in most images ) + {0, 0, 0, 64 }, // shadow border ( used in battle, adventure map def's ) + {0, 0, 0, 64 }, // shadow border ( used in fog-of-war def's ) + {0, 0, 0, 128}, // shadow body ( used in fog-of-war def's ) + {0, 0, 0, 128}, // shadow body ( used in battle, adventure map def's ) + {0, 0, 0, 0 }, // selection / owner flag ( used in battle, adventure map def's ) + {0, 0, 0, 128}, // shadow body below selection ( used in battle def's ) + {0, 0, 0, 64 } // shadow border below selection ( used in battle def's ) + }; + + data = animationCache.getCachedFile(Name); + + palette = std::unique_ptr(new SDL_Color[256]); + int it = 0; + + //ui32 type = read_le_u32(data.get() + it); + it+=4; + //int width = read_le_u32(data + it); it+=4;//not used + //int height = read_le_u32(data + it); it+=4; + it+=8; + ui32 totalBlocks = read_le_u32(data.get() + it); + it+=4; + + for (ui32 i= 0; i<256; i++) + { + palette[i].r = data[it++]; + palette[i].g = data[it++]; + palette[i].b = data[it++]; + palette[i].a = SDL_ALPHA_OPAQUE; + } + + // these colors seems to be used unconditionally + palette[0] = targetPalette[0]; + palette[1] = targetPalette[1]; + palette[4] = targetPalette[4]; + + // rest of special colors are used only if their RGB values are close to H3 + for (uint32_t i = 0; i < 8; ++i) + { + if (colorsSimilar(sourcePalette[i], palette[i])) + palette[i] = targetPalette[i]; + } + + for (ui32 i=0; i >::const_iterator it; + it = offset.find(group); + assert (it != offset.end()); + + const ui8 * FDef = data.get()+it->second[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); + + ui32 currentOffset = sizeof(SSpriteDef); + + //special case for some "old" format defs (SGTWMTA.DEF and SGTWMTB.DEF) + + if(sprite.format == 1 && sprite.width > sprite.fullWidth && sprite.height > sprite.fullHeight) + { + sprite.leftMargin = 0; + sprite.topMargin = 0; + sprite.width = sprite.fullWidth; + sprite.height = sprite.fullHeight; + + currentOffset -= 16; + } + + const ui32 BaseOffset = currentOffset; + + loader.init(Point(sprite.width, sprite.height), + Point(sprite.leftMargin, sprite.topMargin), + Point(sprite.fullWidth, sprite.fullHeight), palette.get()); + + switch(sprite.format) + { + case 0: + { + //pixel data is not compressed, copy data to surface + for(ui32 i=0; i(FDef+currentOffset); + currentOffset += sizeof(ui32) * sprite.height; + + for(ui32 i=0; ierror("Error: unsupported format of def file: %d", sprite.format); + break; + } +} + +CDefFile::~CDefFile() = default; + +const std::map CDefFile::getEntries() const +{ + std::map ret; + + for (auto & elem : offset) + ret[elem.first] = elem.second.size(); + return ret; +} + diff --git a/client/render/CDefFile.h b/client/render/CDefFile.h index ad6de846e..72f3996db 100644 --- a/client/render/CDefFile.h +++ b/client/render/CDefFile.h @@ -1,52 +1,52 @@ -/* - * CDefFile.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/vcmi_endian.h" -#include "../../lib/filesystem/ResourcePath.h" - -class IImageLoader; -struct SDL_Color; - -/// Class for def loading -/// After loading will store general info (palette and frame offsets) and pointer to file itself -class CDefFile -{ -private: - - PACKED_STRUCT_BEGIN - struct SSpriteDef - { - ui32 size; - ui32 format; /// format in which pixel data is stored - ui32 fullWidth; /// full width and height of frame, including borders - ui32 fullHeight; - ui32 width; /// width and height of pixel data, borders excluded - ui32 height; - si32 leftMargin; - si32 topMargin; - } PACKED_STRUCT_END; - //offset[group][frame] - offset of frame data in file - std::map > offset; - - std::unique_ptr data; - std::unique_ptr palette; - -public: - CDefFile(const AnimationPath & Name); - ~CDefFile(); - - //load frame as SDL_Surface - void loadFrame(size_t frame, size_t group, IImageLoader &loader) const; - - const std::map getEntries() const; -}; - - +/* + * CDefFile.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/vcmi_endian.h" +#include "../../lib/filesystem/ResourcePath.h" + +class IImageLoader; +struct SDL_Color; + +/// Class for def loading +/// After loading will store general info (palette and frame offsets) and pointer to file itself +class CDefFile +{ +private: + + PACKED_STRUCT_BEGIN + struct SSpriteDef + { + ui32 size; + ui32 format; /// format in which pixel data is stored + ui32 fullWidth; /// full width and height of frame, including borders + ui32 fullHeight; + ui32 width; /// width and height of pixel data, borders excluded + ui32 height; + si32 leftMargin; + si32 topMargin; + } PACKED_STRUCT_END; + //offset[group][frame] - offset of frame data in file + std::map > offset; + + std::unique_ptr data; + std::unique_ptr palette; + +public: + CDefFile(const AnimationPath & Name); + ~CDefFile(); + + //load frame as SDL_Surface + void loadFrame(size_t frame, size_t group, IImageLoader &loader) const; + + const std::map getEntries() const; +}; + + diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index d468250e5..10e983c7c 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -1,188 +1,188 @@ -/* - * Canvas.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 "Canvas.h" - -#include "../renderSDL/SDL_Extensions.h" -#include "Colors.h" -#include "IImage.h" -#include "Graphics.h" -#include "IFont.h" - -#include -#include - -Canvas::Canvas(SDL_Surface * surface): - surface(surface), - renderArea(0,0, surface->w, surface->h) -{ - surface->refcount++; -} - -Canvas::Canvas(const Canvas & other): - surface(other.surface), - renderArea(other.renderArea) -{ - surface->refcount++; -} - -Canvas::Canvas(Canvas && other): - surface(other.surface), - renderArea(other.renderArea) -{ - surface->refcount++; -} - -Canvas::Canvas(const Canvas & other, const Rect & newClipRect): - Canvas(other) -{ - renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft()); -} - -Canvas::Canvas(const Point & size): - renderArea(Point(0,0), size), - surface(CSDL_Ext::newSurface(size.x, size.y)) -{ - CSDL_Ext::fillSurface(surface, CSDL_Ext::toSDL(Colors::TRANSPARENCY) ); - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); -} - -Canvas Canvas::createFromSurface(SDL_Surface * surface) -{ - return Canvas(surface); -} - -void Canvas::applyTransparency(bool on) -{ - if (on) - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND); - else - SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); -} - -void Canvas::applyGrayscale() -{ - CSDL_Ext::convertToGrayscale(surface, renderArea); -} - -Canvas::~Canvas() -{ - SDL_FreeSurface(surface); -} - -void Canvas::draw(const std::shared_ptr& image, const Point & pos) -{ - assert(image); - if (image) - image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y); -} - -void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) -{ - assert(image); - if (image) - image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect); -} - -void Canvas::draw(const Canvas & image, const Point & pos) -{ - CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); -} - -void Canvas::drawTransparent(const Canvas & image, const Point & pos, double transparency) -{ - SDL_BlendMode oldMode; - - SDL_GetSurfaceBlendMode(image.surface, &oldMode); - SDL_SetSurfaceBlendMode(image.surface, SDL_BLENDMODE_BLEND); - SDL_SetSurfaceAlphaMod(image.surface, 255 * transparency); - CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); - SDL_SetSurfaceAlphaMod(image.surface, 255); - SDL_SetSurfaceBlendMode(image.surface, oldMode); -} - -void Canvas::drawScaled(const Canvas & image, const Point & pos, const Point & targetSize) -{ - SDL_Rect targetRect = CSDL_Ext::toSDL(Rect(pos + renderArea.topLeft(), targetSize)); - SDL_BlitScaled(image.surface, nullptr, surface, &targetRect); -} - -void Canvas::drawPoint(const Point & dest, const ColorRGBA & color) -{ - CSDL_Ext::putPixelWithoutRefreshIfInSurf(surface, dest.x, dest.y, color.r, color.g, color.b, color.a); -} - -void Canvas::drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest) -{ - CSDL_Ext::drawLine(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest)); -} - -void Canvas::drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color) -{ - CSDL_Ext::drawLineDashed(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(color)); -} - -void Canvas::drawBorder(const Rect & target, const ColorRGBA & color, int width) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width); -} - -void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.topRight(), CSDL_Ext::toSDL(color)); - CSDL_Ext::drawLineDashed(surface, realTarget.bottomLeft(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); - CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.bottomLeft(), CSDL_Ext::toSDL(color)); - CSDL_Ext::drawLineDashed(surface, realTarget.topRight(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); -} - -void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ) -{ - switch (alignment) - { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLeft (surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderArea.topLeft() + position); - } -} - -void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ) -{ - switch (alignment) - { - case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLinesLeft (surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); - case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderArea.topLeft() + position); - } -} - -void Canvas::drawColor(const Rect & target, const ColorRGBA & color) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); -} - -void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) -{ - Rect realTarget = target + renderArea.topLeft(); - - CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); -} - -SDL_Surface * Canvas::getInternalSurface() -{ - return surface; -} +/* + * Canvas.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 "Canvas.h" + +#include "../renderSDL/SDL_Extensions.h" +#include "Colors.h" +#include "IImage.h" +#include "Graphics.h" +#include "IFont.h" + +#include +#include + +Canvas::Canvas(SDL_Surface * surface): + surface(surface), + renderArea(0,0, surface->w, surface->h) +{ + surface->refcount++; +} + +Canvas::Canvas(const Canvas & other): + surface(other.surface), + renderArea(other.renderArea) +{ + surface->refcount++; +} + +Canvas::Canvas(Canvas && other): + surface(other.surface), + renderArea(other.renderArea) +{ + surface->refcount++; +} + +Canvas::Canvas(const Canvas & other, const Rect & newClipRect): + Canvas(other) +{ + renderArea = other.renderArea.intersect(newClipRect + other.renderArea.topLeft()); +} + +Canvas::Canvas(const Point & size): + renderArea(Point(0,0), size), + surface(CSDL_Ext::newSurface(size.x, size.y)) +{ + CSDL_Ext::fillSurface(surface, CSDL_Ext::toSDL(Colors::TRANSPARENCY) ); + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); +} + +Canvas Canvas::createFromSurface(SDL_Surface * surface) +{ + return Canvas(surface); +} + +void Canvas::applyTransparency(bool on) +{ + if (on) + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_BLEND); + else + SDL_SetSurfaceBlendMode(surface, SDL_BLENDMODE_NONE); +} + +void Canvas::applyGrayscale() +{ + CSDL_Ext::convertToGrayscale(surface, renderArea); +} + +Canvas::~Canvas() +{ + SDL_FreeSurface(surface); +} + +void Canvas::draw(const std::shared_ptr& image, const Point & pos) +{ + assert(image); + if (image) + image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y); +} + +void Canvas::draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect) +{ + assert(image); + if (image) + image->draw(surface, renderArea.x + pos.x, renderArea.y + pos.y, &sourceRect); +} + +void Canvas::draw(const Canvas & image, const Point & pos) +{ + CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); +} + +void Canvas::drawTransparent(const Canvas & image, const Point & pos, double transparency) +{ + SDL_BlendMode oldMode; + + SDL_GetSurfaceBlendMode(image.surface, &oldMode); + SDL_SetSurfaceBlendMode(image.surface, SDL_BLENDMODE_BLEND); + SDL_SetSurfaceAlphaMod(image.surface, 255 * transparency); + CSDL_Ext::blitSurface(image.surface, image.renderArea, surface, renderArea.topLeft() + pos); + SDL_SetSurfaceAlphaMod(image.surface, 255); + SDL_SetSurfaceBlendMode(image.surface, oldMode); +} + +void Canvas::drawScaled(const Canvas & image, const Point & pos, const Point & targetSize) +{ + SDL_Rect targetRect = CSDL_Ext::toSDL(Rect(pos + renderArea.topLeft(), targetSize)); + SDL_BlitScaled(image.surface, nullptr, surface, &targetRect); +} + +void Canvas::drawPoint(const Point & dest, const ColorRGBA & color) +{ + CSDL_Ext::putPixelWithoutRefreshIfInSurf(surface, dest.x, dest.y, color.r, color.g, color.b, color.a); +} + +void Canvas::drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest) +{ + CSDL_Ext::drawLine(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(colorFrom), CSDL_Ext::toSDL(colorDest)); +} + +void Canvas::drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color) +{ + CSDL_Ext::drawLineDashed(surface, renderArea.topLeft() + from, renderArea.topLeft() + dest, CSDL_Ext::toSDL(color)); +} + +void Canvas::drawBorder(const Rect & target, const ColorRGBA & color, int width) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::drawBorder(surface, realTarget.x, realTarget.y, realTarget.w, realTarget.h, CSDL_Ext::toSDL(color), width); +} + +void Canvas::drawBorderDashed(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.topRight(), CSDL_Ext::toSDL(color)); + CSDL_Ext::drawLineDashed(surface, realTarget.bottomLeft(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); + CSDL_Ext::drawLineDashed(surface, realTarget.topLeft(), realTarget.bottomLeft(), CSDL_Ext::toSDL(color)); + CSDL_Ext::drawLineDashed(surface, realTarget.topRight(), realTarget.bottomRight(), CSDL_Ext::toSDL(color)); +} + +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ) +{ + switch (alignment) + { + case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLeft (surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderArea.topLeft() + position); + } +} + +void Canvas::drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ) +{ + switch (alignment) + { + case ETextAlignment::TOPLEFT: return graphics->fonts[font]->renderTextLinesLeft (surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::TOPCENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::CENTER: return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position); + case ETextAlignment::BOTTOMRIGHT: return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderArea.topLeft() + position); + } +} + +void Canvas::drawColor(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color)); +} + +void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color) +{ + Rect realTarget = target + renderArea.topLeft(); + + CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color)); +} + +SDL_Surface * Canvas::getInternalSurface() +{ + return surface; +} diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 591e8fc07..b9d3389db 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -1,104 +1,104 @@ -/* - * Canvas.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 "../gui/TextAlignment.h" -#include "../../lib/Rect.h" -#include "../../lib/Color.h" - -struct SDL_Surface; -class IImage; -enum EFonts : int; - -/// Class that represents surface for drawing on -class Canvas -{ - /// Target surface - SDL_Surface * surface; - - /// Current rendering area, all rendering operations will be moved into selected area - Rect renderArea; - - /// constructs canvas using existing surface. Caller maintains ownership on the surface - explicit Canvas(SDL_Surface * surface); - - /// copy contructor - Canvas(const Canvas & other); - -public: - Canvas & operator = (const Canvas & other) = delete; - Canvas & operator = (Canvas && other) = delete; - - /// move contructor - Canvas(Canvas && other); - - /// creates canvas that only covers specified subsection of a surface - Canvas(const Canvas & other, const Rect & clipRect); - - /// constructs canvas of specified size - explicit Canvas(const Point & size); - - /// constructs canvas using existing surface. Caller maintains ownership on the surface - /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. - static Canvas createFromSurface(SDL_Surface * surface); - - ~Canvas(); - - /// if set to true, drawing this canvas onto another canvas will use alpha channel information - void applyTransparency(bool on); - - /// applies grayscale filter onto current image - void applyGrayscale(); - - /// renders image onto this canvas at specified position - void draw(const std::shared_ptr& image, const Point & pos); - - /// renders section of image bounded by sourceRect at specified position - void draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect); - - /// renders another canvas onto this canvas - void draw(const Canvas &image, const Point & pos); - - /// renders another canvas onto this canvas with transparency - void drawTransparent(const Canvas & image, const Point & pos, double transparency); - - /// renders another canvas onto this canvas with scaling - void drawScaled(const Canvas &image, const Point & pos, const Point & targetSize); - - /// renders single pixels with specified color - void drawPoint(const Point & dest, const ColorRGBA & color); - - /// renders continuous, 1-pixel wide line with color gradient - void drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest); - - /// renders dashed, 1-pixel wide line with specified color - void drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color); - - /// renders rectangular, solid-color border in specified location - void drawBorder(const Rect & target, const ColorRGBA & color, int width = 1); - - /// renders rectangular, dashed border in specified location - void drawBorderDashed(const Rect & target, const ColorRGBA & color); - - /// renders single line of text with specified parameters - void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ); - - /// renders multiple lines of text with specified parameters - void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); - - /// fills selected area with solid color - void drawColor(const Rect & target, const ColorRGBA & color); - - /// fills selected area with blended color - void drawColorBlended(const Rect & target, const ColorRGBA & color); - - /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. - SDL_Surface * getInternalSurface(); -}; +/* + * Canvas.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 "../gui/TextAlignment.h" +#include "../../lib/Rect.h" +#include "../../lib/Color.h" + +struct SDL_Surface; +class IImage; +enum EFonts : int; + +/// Class that represents surface for drawing on +class Canvas +{ + /// Target surface + SDL_Surface * surface; + + /// Current rendering area, all rendering operations will be moved into selected area + Rect renderArea; + + /// constructs canvas using existing surface. Caller maintains ownership on the surface + explicit Canvas(SDL_Surface * surface); + + /// copy contructor + Canvas(const Canvas & other); + +public: + Canvas & operator = (const Canvas & other) = delete; + Canvas & operator = (Canvas && other) = delete; + + /// move contructor + Canvas(Canvas && other); + + /// creates canvas that only covers specified subsection of a surface + Canvas(const Canvas & other, const Rect & clipRect); + + /// constructs canvas of specified size + explicit Canvas(const Point & size); + + /// constructs canvas using existing surface. Caller maintains ownership on the surface + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. + static Canvas createFromSurface(SDL_Surface * surface); + + ~Canvas(); + + /// if set to true, drawing this canvas onto another canvas will use alpha channel information + void applyTransparency(bool on); + + /// applies grayscale filter onto current image + void applyGrayscale(); + + /// renders image onto this canvas at specified position + void draw(const std::shared_ptr& image, const Point & pos); + + /// renders section of image bounded by sourceRect at specified position + void draw(const std::shared_ptr& image, const Point & pos, const Rect & sourceRect); + + /// renders another canvas onto this canvas + void draw(const Canvas &image, const Point & pos); + + /// renders another canvas onto this canvas with transparency + void drawTransparent(const Canvas & image, const Point & pos, double transparency); + + /// renders another canvas onto this canvas with scaling + void drawScaled(const Canvas &image, const Point & pos, const Point & targetSize); + + /// renders single pixels with specified color + void drawPoint(const Point & dest, const ColorRGBA & color); + + /// renders continuous, 1-pixel wide line with color gradient + void drawLine(const Point & from, const Point & dest, const ColorRGBA & colorFrom, const ColorRGBA & colorDest); + + /// renders dashed, 1-pixel wide line with specified color + void drawLineDashed(const Point & from, const Point & dest, const ColorRGBA & color); + + /// renders rectangular, solid-color border in specified location + void drawBorder(const Rect & target, const ColorRGBA & color, int width = 1); + + /// renders rectangular, dashed border in specified location + void drawBorderDashed(const Rect & target, const ColorRGBA & color); + + /// renders single line of text with specified parameters + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::string & text ); + + /// renders multiple lines of text with specified parameters + void drawText(const Point & position, const EFonts & font, const ColorRGBA & colorDest, ETextAlignment alignment, const std::vector & text ); + + /// fills selected area with solid color + void drawColor(const Rect & target, const ColorRGBA & color); + + /// fills selected area with blended color + void drawColorBlended(const Rect & target, const ColorRGBA & color); + + /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. + SDL_Surface * getInternalSurface(); +}; diff --git a/client/render/ColorFilter.cpp b/client/render/ColorFilter.cpp index 3f13db8a2..d6234116f 100644 --- a/client/render/ColorFilter.cpp +++ b/client/render/ColorFilter.cpp @@ -1,161 +1,161 @@ -/* - * Canvas.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 "ColorFilter.h" - -#include "../../lib/JsonNode.h" -#include "../../lib/Color.h" - -ColorRGBA ColorFilter::shiftColor(const ColorRGBA & in) const -{ - int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; - int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; - int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a; - int a_out = in.a * a; - - vstd::abetween(r_out, 0, 255); - vstd::abetween(g_out, 0, 255); - vstd::abetween(b_out, 0, 255); - vstd::abetween(a_out, 0, 255); - - return { - static_cast(r_out), - static_cast(g_out), - static_cast(b_out), - static_cast(a_out) - }; -} - -bool ColorFilter::operator != (const ColorFilter & other) const -{ - return !(this->operator==(other)); -} - -bool ColorFilter::operator == (const ColorFilter & other) const -{ - return - r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && - g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && - b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && - a == other.a; -} - -ColorFilter ColorFilter::genEmptyShifter( ) -{ - return genAlphaShifter( 1.f); -} - -ColorFilter ColorFilter::genAlphaShifter( float alpha ) -{ - return genMuxerShifter( - { 1.f, 0.f, 0.f, 0.f }, - { 0.f, 1.f, 0.f, 0.f }, - { 0.f, 0.f, 1.f, 0.f }, - alpha); -} - -ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ) -{ - return genMuxerShifter( - { maxR - minR, 0.f, 0.f, minR }, - { 0.f, maxG - minG, 0.f, minG }, - { 0.f, 0.f, maxB - minB, minB }, - 1.f); -} - -ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) -{ - return ColorFilter(r, g, b, a); -} - -ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power) -{ - auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer - { - return { - vstd::lerp(left.r, right.r, power), - vstd::lerp(left.g, right.g, power), - vstd::lerp(left.b, right.b, power), - vstd::lerp(left.a, right.a, power) - }; - }; - - return genMuxerShifter( - lerpMuxer(left.r, right.r), - lerpMuxer(left.g, right.g), - lerpMuxer(left.b, right.b), - vstd::lerp(left.a, right.a, power) - ); -} - -ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right) -{ - // matrix multiplication - ChannelMuxer r{ - left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b, - left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b, - left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b, - left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a, - }; - - ChannelMuxer g{ - left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b, - left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b, - left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b, - left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a, - }; - - ChannelMuxer b{ - left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b, - left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b, - left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b, - left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a, - }; - - float a = left.a * right.a; - return genMuxerShifter(r,g,b,a); -} - -ColorFilter ColorFilter::genFromJson(const JsonNode & entry) -{ - ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f }; - ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f }; - ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f }; - float a{ 1.0}; - - if (!entry["red"].isNull()) - { - r.r = entry["red"].Vector()[0].Float(); - r.g = entry["red"].Vector()[1].Float(); - r.b = entry["red"].Vector()[2].Float(); - r.a = entry["red"].Vector()[3].Float(); - } - - if (!entry["green"].isNull()) - { - g.r = entry["green"].Vector()[0].Float(); - g.g = entry["green"].Vector()[1].Float(); - g.b = entry["green"].Vector()[2].Float(); - g.a = entry["green"].Vector()[3].Float(); - } - - if (!entry["blue"].isNull()) - { - b.r = entry["blue"].Vector()[0].Float(); - b.g = entry["blue"].Vector()[1].Float(); - b.b = entry["blue"].Vector()[2].Float(); - b.a = entry["blue"].Vector()[3].Float(); - } - - if (!entry["alpha"].isNull()) - a = entry["alpha"].Float(); - - return genMuxerShifter(r,g,b,a); -} +/* + * Canvas.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 "ColorFilter.h" + +#include "../../lib/JsonNode.h" +#include "../../lib/Color.h" + +ColorRGBA ColorFilter::shiftColor(const ColorRGBA & in) const +{ + int r_out = in.r * r.r + in.g * r.g + in.b * r.b + 255 * r.a; + int g_out = in.r * g.r + in.g * g.g + in.b * g.b + 255 * g.a; + int b_out = in.r * b.r + in.g * b.g + in.b * b.b + 255 * b.a; + int a_out = in.a * a; + + vstd::abetween(r_out, 0, 255); + vstd::abetween(g_out, 0, 255); + vstd::abetween(b_out, 0, 255); + vstd::abetween(a_out, 0, 255); + + return { + static_cast(r_out), + static_cast(g_out), + static_cast(b_out), + static_cast(a_out) + }; +} + +bool ColorFilter::operator != (const ColorFilter & other) const +{ + return !(this->operator==(other)); +} + +bool ColorFilter::operator == (const ColorFilter & other) const +{ + return + r.r == other.r.r && r.g && other.r.g && r.b == other.r.b && r.a == other.r.a && + g.r == other.g.r && g.g && other.g.g && g.b == other.g.b && g.a == other.g.a && + b.r == other.b.r && b.g && other.b.g && b.b == other.b.b && b.a == other.b.a && + a == other.a; +} + +ColorFilter ColorFilter::genEmptyShifter( ) +{ + return genAlphaShifter( 1.f); +} + +ColorFilter ColorFilter::genAlphaShifter( float alpha ) +{ + return genMuxerShifter( + { 1.f, 0.f, 0.f, 0.f }, + { 0.f, 1.f, 0.f, 0.f }, + { 0.f, 0.f, 1.f, 0.f }, + alpha); +} + +ColorFilter ColorFilter::genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ) +{ + return genMuxerShifter( + { maxR - minR, 0.f, 0.f, minR }, + { 0.f, maxG - minG, 0.f, minG }, + { 0.f, 0.f, maxB - minB, minB }, + 1.f); +} + +ColorFilter ColorFilter::genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ) +{ + return ColorFilter(r, g, b, a); +} + +ColorFilter ColorFilter::genInterpolated(const ColorFilter & left, const ColorFilter & right, float power) +{ + auto lerpMuxer = [=]( const ChannelMuxer & left, const ChannelMuxer & right ) -> ChannelMuxer + { + return { + vstd::lerp(left.r, right.r, power), + vstd::lerp(left.g, right.g, power), + vstd::lerp(left.b, right.b, power), + vstd::lerp(left.a, right.a, power) + }; + }; + + return genMuxerShifter( + lerpMuxer(left.r, right.r), + lerpMuxer(left.g, right.g), + lerpMuxer(left.b, right.b), + vstd::lerp(left.a, right.a, power) + ); +} + +ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter & right) +{ + // matrix multiplication + ChannelMuxer r{ + left.r.r * right.r.r + left.g.r * right.r.g + left.b.r * right.r.b, + left.r.g * right.r.r + left.g.g * right.r.g + left.b.g * right.r.b, + left.r.b * right.r.r + left.g.b * right.r.g + left.b.b * right.r.b, + left.r.a * right.r.r + left.g.a * right.r.g + left.b.a * right.r.b + 1.f * right.r.a, + }; + + ChannelMuxer g{ + left.r.r * right.g.r + left.g.r * right.g.g + left.b.r * right.g.b, + left.r.g * right.g.r + left.g.g * right.g.g + left.b.g * right.g.b, + left.r.b * right.g.r + left.g.b * right.g.g + left.b.b * right.g.b, + left.r.a * right.g.r + left.g.a * right.g.g + left.b.a * right.g.b + 1.f * right.g.a, + }; + + ChannelMuxer b{ + left.r.r * right.b.r + left.g.r * right.b.g + left.b.r * right.b.b, + left.r.g * right.b.r + left.g.g * right.b.g + left.b.g * right.b.b, + left.r.b * right.b.r + left.g.b * right.b.g + left.b.b * right.b.b, + left.r.a * right.b.r + left.g.a * right.b.g + left.b.a * right.b.b + 1.f * right.b.a, + }; + + float a = left.a * right.a; + return genMuxerShifter(r,g,b,a); +} + +ColorFilter ColorFilter::genFromJson(const JsonNode & entry) +{ + ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f }; + ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f }; + ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f }; + float a{ 1.0}; + + if (!entry["red"].isNull()) + { + r.r = entry["red"].Vector()[0].Float(); + r.g = entry["red"].Vector()[1].Float(); + r.b = entry["red"].Vector()[2].Float(); + r.a = entry["red"].Vector()[3].Float(); + } + + if (!entry["green"].isNull()) + { + g.r = entry["green"].Vector()[0].Float(); + g.g = entry["green"].Vector()[1].Float(); + g.b = entry["green"].Vector()[2].Float(); + g.a = entry["green"].Vector()[3].Float(); + } + + if (!entry["blue"].isNull()) + { + b.r = entry["blue"].Vector()[0].Float(); + b.g = entry["blue"].Vector()[1].Float(); + b.b = entry["blue"].Vector()[2].Float(); + b.a = entry["blue"].Vector()[3].Float(); + } + + if (!entry["alpha"].isNull()) + a = entry["alpha"].Float(); + + return genMuxerShifter(r,g,b,a); +} diff --git a/client/render/ColorFilter.h b/client/render/ColorFilter.h index 989fab7bf..5df63ec67 100644 --- a/client/render/ColorFilter.h +++ b/client/render/ColorFilter.h @@ -1,65 +1,65 @@ -/* - * ColorFilter.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 - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -class ColorRGBA; -VCMI_LIB_NAMESPACE_END - -/// Base class for applying palette transformation on images -class ColorFilter -{ - struct ChannelMuxer { - float r, g, b, a; - }; - - ChannelMuxer r; - ChannelMuxer g; - ChannelMuxer b; - float a; - - ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a): - r(r), g(g), b(b), a(a) - {} -public: - ColorRGBA shiftColor(const ColorRGBA & in) const; - - bool operator == (const ColorFilter & other) const; - bool operator != (const ColorFilter & other) const; - - /// Generates empty object that has no effect on image - static ColorFilter genEmptyShifter(); - - /// Generates object that changes alpha (transparency) of the image - static ColorFilter genAlphaShifter( float alpha ); - - /// Generates object that transforms each channel independently - static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); - - /// Generates object that performs arbitrary mixing between any channels - static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); - - /// Combines 2 mixers into a single object - static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right); - - /// Scales down strength of a shifter to a specified factor - static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power); - - /// Generates object using supplied Json config - static ColorFilter genFromJson(const JsonNode & entry); -}; - -struct ColorMuxerEffect -{ - std::vector filters; - std::vector timePoints; -}; +/* + * ColorFilter.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 + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +class ColorRGBA; +VCMI_LIB_NAMESPACE_END + +/// Base class for applying palette transformation on images +class ColorFilter +{ + struct ChannelMuxer { + float r, g, b, a; + }; + + ChannelMuxer r; + ChannelMuxer g; + ChannelMuxer b; + float a; + + ColorFilter(ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a): + r(r), g(g), b(b), a(a) + {} +public: + ColorRGBA shiftColor(const ColorRGBA & in) const; + + bool operator == (const ColorFilter & other) const; + bool operator != (const ColorFilter & other) const; + + /// Generates empty object that has no effect on image + static ColorFilter genEmptyShifter(); + + /// Generates object that changes alpha (transparency) of the image + static ColorFilter genAlphaShifter( float alpha ); + + /// Generates object that transforms each channel independently + static ColorFilter genRangeShifter( float minR, float minG, float minB, float maxR, float maxG, float maxB ); + + /// Generates object that performs arbitrary mixing between any channels + static ColorFilter genMuxerShifter( ChannelMuxer r, ChannelMuxer g, ChannelMuxer b, float a ); + + /// Combines 2 mixers into a single object + static ColorFilter genCombined(const ColorFilter & left, const ColorFilter & right); + + /// Scales down strength of a shifter to a specified factor + static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power); + + /// Generates object using supplied Json config + static ColorFilter genFromJson(const JsonNode & entry); +}; + +struct ColorMuxerEffect +{ + std::vector filters; + std::vector timePoints; +}; diff --git a/client/render/Colors.h b/client/render/Colors.h index a73b5a856..7d890bc7e 100644 --- a/client/render/Colors.h +++ b/client/render/Colors.h @@ -1,55 +1,55 @@ -/* - * ICursor.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/Color.h" - -/** - * The colors class defines color constants of type ColorRGBA. - */ -class Colors -{ -public: - /** the h3 yellow color, typically used for headlines */ - static const ColorRGBA YELLOW; - - /** the standard h3 white color */ - static const ColorRGBA WHITE; - - /** the metallic gold color used mostly as a border around buttons */ - static const ColorRGBA METALLIC_GOLD; - - /** green color used for in-game console */ - static const ColorRGBA GREEN; - - /** the h3 orange color, used for blocked buttons */ - static const ColorRGBA ORANGE; - - /** the h3 bright yellow color, used for selection border */ - static const ColorRGBA BRIGHT_YELLOW; - - /** default key color for all 8 & 24 bit graphics */ - static const ColorRGBA DEFAULT_KEY_COLOR; - - /// Selected creature card - static const ColorRGBA RED; - - /// Minimap border - static const ColorRGBA PURPLE; - - static const ColorRGBA CYAN; - - static const ColorRGBA BLACK; - - static const ColorRGBA TRANSPARENCY; - - /// parse color - static std::optional parseColor(std::string text); -}; +/* + * ICursor.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/Color.h" + +/** + * The colors class defines color constants of type ColorRGBA. + */ +class Colors +{ +public: + /** the h3 yellow color, typically used for headlines */ + static const ColorRGBA YELLOW; + + /** the standard h3 white color */ + static const ColorRGBA WHITE; + + /** the metallic gold color used mostly as a border around buttons */ + static const ColorRGBA METALLIC_GOLD; + + /** green color used for in-game console */ + static const ColorRGBA GREEN; + + /** the h3 orange color, used for blocked buttons */ + static const ColorRGBA ORANGE; + + /** the h3 bright yellow color, used for selection border */ + static const ColorRGBA BRIGHT_YELLOW; + + /** default key color for all 8 & 24 bit graphics */ + static const ColorRGBA DEFAULT_KEY_COLOR; + + /// Selected creature card + static const ColorRGBA RED; + + /// Minimap border + static const ColorRGBA PURPLE; + + static const ColorRGBA CYAN; + + static const ColorRGBA BLACK; + + static const ColorRGBA TRANSPARENCY; + + /// parse color + static std::optional parseColor(std::string text); +}; diff --git a/client/render/Graphics.cpp b/client/render/Graphics.cpp index fd98ca13d..603972feb 100644 --- a/client/render/Graphics.cpp +++ b/client/render/Graphics.cpp @@ -1,294 +1,294 @@ -/* - * Graphics.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 "Graphics.h" - -#include -#include -#include -#include -#include -#include -#include - -#include "../renderSDL/SDL_Extensions.h" -#include "../renderSDL/CBitmapFont.h" -#include "../renderSDL/CBitmapHanFont.h" -#include "../renderSDL/CTrueTypeFont.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../gui/CGuiHandler.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/filesystem/CBinaryReader.h" -#include "../lib/modding/CModHandler.h" -#include "../lib/modding/ModScope.h" -#include "CGameInfo.h" -#include "../lib/VCMI_Lib.h" -#include "../CCallback.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/JsonNode.h" -#include "../lib/vcmi_endian.h" -#include "../lib/CStopWatch.h" -#include "../lib/CHeroHandler.h" - -#include - -Graphics * graphics = nullptr; - -void Graphics::loadPaletteAndColors() -{ - auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); - std::string pals((char*)textFile.first.get(), textFile.second); - - int startPoint = 24; //beginning byte; used to read - for(int i=0; i<8; ++i) - { - for(int j=0; j<32; ++j) - { - ColorRGBA col; - col.r = pals[startPoint++]; - col.g = pals[startPoint++]; - col.b = pals[startPoint++]; - col.a = SDL_ALPHA_OPAQUE; - startPoint++; - playerColorPalette[i][j] = col; - } - } - - auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); - CBinaryReader reader(stream.get()); - - for(int i=0; i<32; ++i) - { - neutralColorPalette[i].r = reader.readUInt8(); - neutralColorPalette[i].g = reader.readUInt8(); - neutralColorPalette[i].b = reader.readUInt8(); - reader.readUInt8(); // this is "flags" entry, not alpha - neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; - } - //colors initialization - ColorRGBA colors[] = { - {0xff,0, 0, SDL_ALPHA_OPAQUE}, - {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, - {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, - {0x42,0x94,0x29,SDL_ALPHA_OPAQUE}, - - {0xff,0x84,0, SDL_ALPHA_OPAQUE}, - {0x8c,0x29,0xa5,SDL_ALPHA_OPAQUE}, - {0x09,0x9c,0xa5,SDL_ALPHA_OPAQUE}, - {0xc6,0x7b,0x8c,SDL_ALPHA_OPAQUE}}; - - for(int i=0;i<8;i++) - { - playerColors[i] = colors[i]; - } - //gray - neutralColor.r = 0x84; - neutralColor.g = 0x84; - neutralColor.b = 0x84; - neutralColor.a = SDL_ALPHA_OPAQUE; -} - -void Graphics::initializeBattleGraphics() -{ - auto allConfigs = VLC->modh->getActiveMods(); - allConfigs.insert(allConfigs.begin(), ModScope::scopeBuiltin()); - for(auto & mod : allConfigs) - { - if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) - continue; - - const JsonNode config(mod, JsonPath::builtin("config/battles_graphics.json")); - - //initialization of AC->def name mapping - if(!config["ac_mapping"].isNull()) - for(const JsonNode &ac : config["ac_mapping"].Vector()) - { - int ACid = static_cast(ac["id"].Float()); - std::vector< std::string > toAdd; - - for(const JsonNode &defname : ac["defnames"].Vector()) - { - toAdd.push_back(defname.String()); - } - - battleACToDef[ACid] = toAdd; - } - } -} -Graphics::Graphics() -{ - loadFonts(); - loadPaletteAndColors(); - initializeBattleGraphics(); - loadErmuToPicture(); - initializeImageLists(); - - //(!) do not load any CAnimation here -} - -void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) -{ - if(sur->format->palette) - { - SDL_Color palette[32]; - if(player.isValidPlayer()) - { - for(int i=0; i<32; ++i) - palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); - } - else if(player == PlayerColor::NEUTRAL) - { - for(int i=0; i<32; ++i) - palette[i] = CSDL_Ext::toSDL(neutralColorPalette[i]); - } - else - { - logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); - return; - } -//FIXME: not all player colored images have player palette at last 32 indexes -//NOTE: following code is much more correct but still not perfect (bugged with status bar) - CSDL_Ext::setColors(sur, palette, 224, 32); - - -#if 0 - - SDL_Color * bluePalette = playerColorPalette + 32; - - SDL_Palette * oldPalette = sur->format->palette; - - SDL_Palette * newPalette = SDL_AllocPalette(256); - - for(size_t destIndex = 0; destIndex < 256; destIndex++) - { - SDL_Color old = oldPalette->colors[destIndex]; - - bool found = false; - - for(size_t srcIndex = 0; srcIndex < 32; srcIndex++) - { - if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r) - { - found = true; - newPalette->colors[destIndex] = palette[srcIndex]; - break; - } - } - if(!found) - newPalette->colors[destIndex] = old; - } - - SDL_SetSurfacePalette(sur, newPalette); - - SDL_FreePalette(newPalette); - -#endif // 0 - } - else - { - //TODO: implement. H3 method works only for images with palettes. - // Add some kind of player-colored overlay? - // Or keep palette approach here and replace only colors of specific value(s) - // Or just wait for OpenGL support? - logGlobal->warn("Image must have palette to be player-colored!"); - } -} - -void Graphics::loadFonts() -{ - const JsonNode config(JsonPath::builtin("config/fonts.json")); - - const JsonVector & bmpConf = config["bitmap"].Vector(); - const JsonNode & ttfConf = config["trueType"]; - const JsonNode & hanConf = config["bitmapHan"]; - - assert(bmpConf.size() == FONTS_NUMBER); - - for (size_t i=0; i(hanConf[filename]); - else if (!ttfConf[filename].isNull()) // no ttf override - fonts[i] = std::make_shared(ttfConf[filename]); - else - fonts[i] = std::make_shared(filename); - } -} - -void Graphics::loadErmuToPicture() -{ - //loading ERMU to picture - const JsonNode config(JsonPath::builtin("config/ERMU_to_picture.json")); - int etp_idx = 0; - for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { - int idx = 0; - for(const JsonNode &n : etp.Vector()) { - ERMUtoPicture[idx][etp_idx] = n.String(); - idx ++; - } - assert (idx == std::size(ERMUtoPicture)); - - etp_idx ++; - } - assert (etp_idx == 44); -} - -void Graphics::addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName) -{ - if (!imageName.empty()) - { - JsonNode entry; - if (group != 0) - entry["group"].Integer() = group; - entry["frame"].Integer() = index; - entry["file"].String() = imageName; - - imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry); - } -} - -void Graphics::addImageListEntries(const EntityService * service) -{ - auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3, _4); - - auto loopCb = [&](const Entity * entity, bool & stop) - { - entity->registerIcons(cb); - }; - - service->forEachBase(loopCb); -} - -void Graphics::initializeImageLists() -{ - addImageListEntries(CGI->creatures()); - addImageListEntries(CGI->heroTypes()); - addImageListEntries(CGI->artifacts()); - addImageListEntries(CGI->factions()); - addImageListEntries(CGI->spells()); - addImageListEntries(CGI->skills()); -} - -std::shared_ptr Graphics::getAnimation(const AnimationPath & path) -{ - if (cachedAnimations.count(path) != 0) - return cachedAnimations.at(path); - - auto newAnimation = GH.renderHandler().loadAnimation(path); - - newAnimation->preload(); - cachedAnimations[path] = newAnimation; - return newAnimation; -} +/* + * Graphics.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 "Graphics.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "../renderSDL/SDL_Extensions.h" +#include "../renderSDL/CBitmapFont.h" +#include "../renderSDL/CBitmapHanFont.h" +#include "../renderSDL/CTrueTypeFont.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../gui/CGuiHandler.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/filesystem/CBinaryReader.h" +#include "../lib/modding/CModHandler.h" +#include "../lib/modding/ModScope.h" +#include "CGameInfo.h" +#include "../lib/VCMI_Lib.h" +#include "../CCallback.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/JsonNode.h" +#include "../lib/vcmi_endian.h" +#include "../lib/CStopWatch.h" +#include "../lib/CHeroHandler.h" + +#include + +Graphics * graphics = nullptr; + +void Graphics::loadPaletteAndColors() +{ + auto textFile = CResourceHandler::get()->load(ResourcePath("DATA/PLAYERS.PAL"))->readAll(); + std::string pals((char*)textFile.first.get(), textFile.second); + + int startPoint = 24; //beginning byte; used to read + for(int i=0; i<8; ++i) + { + for(int j=0; j<32; ++j) + { + ColorRGBA col; + col.r = pals[startPoint++]; + col.g = pals[startPoint++]; + col.b = pals[startPoint++]; + col.a = SDL_ALPHA_OPAQUE; + startPoint++; + playerColorPalette[i][j] = col; + } + } + + auto stream = CResourceHandler::get()->load(ResourcePath("config/NEUTRAL.PAL")); + CBinaryReader reader(stream.get()); + + for(int i=0; i<32; ++i) + { + neutralColorPalette[i].r = reader.readUInt8(); + neutralColorPalette[i].g = reader.readUInt8(); + neutralColorPalette[i].b = reader.readUInt8(); + reader.readUInt8(); // this is "flags" entry, not alpha + neutralColorPalette[i].a = SDL_ALPHA_OPAQUE; + } + //colors initialization + ColorRGBA colors[] = { + {0xff,0, 0, SDL_ALPHA_OPAQUE}, + {0x31,0x52,0xff,SDL_ALPHA_OPAQUE}, + {0x9c,0x73,0x52,SDL_ALPHA_OPAQUE}, + {0x42,0x94,0x29,SDL_ALPHA_OPAQUE}, + + {0xff,0x84,0, SDL_ALPHA_OPAQUE}, + {0x8c,0x29,0xa5,SDL_ALPHA_OPAQUE}, + {0x09,0x9c,0xa5,SDL_ALPHA_OPAQUE}, + {0xc6,0x7b,0x8c,SDL_ALPHA_OPAQUE}}; + + for(int i=0;i<8;i++) + { + playerColors[i] = colors[i]; + } + //gray + neutralColor.r = 0x84; + neutralColor.g = 0x84; + neutralColor.b = 0x84; + neutralColor.a = SDL_ALPHA_OPAQUE; +} + +void Graphics::initializeBattleGraphics() +{ + auto allConfigs = VLC->modh->getActiveMods(); + allConfigs.insert(allConfigs.begin(), ModScope::scopeBuiltin()); + for(auto & mod : allConfigs) + { + if(!CResourceHandler::get(mod)->existsResource(ResourcePath("config/battles_graphics.json"))) + continue; + + const JsonNode config(mod, JsonPath::builtin("config/battles_graphics.json")); + + //initialization of AC->def name mapping + if(!config["ac_mapping"].isNull()) + for(const JsonNode &ac : config["ac_mapping"].Vector()) + { + int ACid = static_cast(ac["id"].Float()); + std::vector< std::string > toAdd; + + for(const JsonNode &defname : ac["defnames"].Vector()) + { + toAdd.push_back(defname.String()); + } + + battleACToDef[ACid] = toAdd; + } + } +} +Graphics::Graphics() +{ + loadFonts(); + loadPaletteAndColors(); + initializeBattleGraphics(); + loadErmuToPicture(); + initializeImageLists(); + + //(!) do not load any CAnimation here +} + +void Graphics::blueToPlayersAdv(SDL_Surface * sur, PlayerColor player) +{ + if(sur->format->palette) + { + SDL_Color palette[32]; + if(player.isValidPlayer()) + { + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(playerColorPalette[player][i]); + } + else if(player == PlayerColor::NEUTRAL) + { + for(int i=0; i<32; ++i) + palette[i] = CSDL_Ext::toSDL(neutralColorPalette[i]); + } + else + { + logGlobal->error("Wrong player id in blueToPlayersAdv (%s)!", player.toString()); + return; + } +//FIXME: not all player colored images have player palette at last 32 indexes +//NOTE: following code is much more correct but still not perfect (bugged with status bar) + CSDL_Ext::setColors(sur, palette, 224, 32); + + +#if 0 + + SDL_Color * bluePalette = playerColorPalette + 32; + + SDL_Palette * oldPalette = sur->format->palette; + + SDL_Palette * newPalette = SDL_AllocPalette(256); + + for(size_t destIndex = 0; destIndex < 256; destIndex++) + { + SDL_Color old = oldPalette->colors[destIndex]; + + bool found = false; + + for(size_t srcIndex = 0; srcIndex < 32; srcIndex++) + { + if(old.b == bluePalette[srcIndex].b && old.g == bluePalette[srcIndex].g && old.r == bluePalette[srcIndex].r) + { + found = true; + newPalette->colors[destIndex] = palette[srcIndex]; + break; + } + } + if(!found) + newPalette->colors[destIndex] = old; + } + + SDL_SetSurfacePalette(sur, newPalette); + + SDL_FreePalette(newPalette); + +#endif // 0 + } + else + { + //TODO: implement. H3 method works only for images with palettes. + // Add some kind of player-colored overlay? + // Or keep palette approach here and replace only colors of specific value(s) + // Or just wait for OpenGL support? + logGlobal->warn("Image must have palette to be player-colored!"); + } +} + +void Graphics::loadFonts() +{ + const JsonNode config(JsonPath::builtin("config/fonts.json")); + + const JsonVector & bmpConf = config["bitmap"].Vector(); + const JsonNode & ttfConf = config["trueType"]; + const JsonNode & hanConf = config["bitmapHan"]; + + assert(bmpConf.size() == FONTS_NUMBER); + + for (size_t i=0; i(hanConf[filename]); + else if (!ttfConf[filename].isNull()) // no ttf override + fonts[i] = std::make_shared(ttfConf[filename]); + else + fonts[i] = std::make_shared(filename); + } +} + +void Graphics::loadErmuToPicture() +{ + //loading ERMU to picture + const JsonNode config(JsonPath::builtin("config/ERMU_to_picture.json")); + int etp_idx = 0; + for(const JsonNode &etp : config["ERMU_to_picture"].Vector()) { + int idx = 0; + for(const JsonNode &n : etp.Vector()) { + ERMUtoPicture[idx][etp_idx] = n.String(); + idx ++; + } + assert (idx == std::size(ERMUtoPicture)); + + etp_idx ++; + } + assert (etp_idx == 44); +} + +void Graphics::addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName) +{ + if (!imageName.empty()) + { + JsonNode entry; + if (group != 0) + entry["group"].Integer() = group; + entry["frame"].Integer() = index; + entry["file"].String() = imageName; + + imageLists["SPRITES/" + listName]["images"].Vector().push_back(entry); + } +} + +void Graphics::addImageListEntries(const EntityService * service) +{ + auto cb = std::bind(&Graphics::addImageListEntry, this, _1, _2, _3, _4); + + auto loopCb = [&](const Entity * entity, bool & stop) + { + entity->registerIcons(cb); + }; + + service->forEachBase(loopCb); +} + +void Graphics::initializeImageLists() +{ + addImageListEntries(CGI->creatures()); + addImageListEntries(CGI->heroTypes()); + addImageListEntries(CGI->artifacts()); + addImageListEntries(CGI->factions()); + addImageListEntries(CGI->spells()); + addImageListEntries(CGI->skills()); +} + +std::shared_ptr Graphics::getAnimation(const AnimationPath & path) +{ + if (cachedAnimations.count(path) != 0) + return cachedAnimations.at(path); + + auto newAnimation = GH.renderHandler().loadAnimation(path); + + newAnimation->preload(); + cachedAnimations[path] = newAnimation; + return newAnimation; +} diff --git a/client/render/Graphics.h b/client/render/Graphics.h index dfa0e1123..4170f8e1a 100644 --- a/client/render/Graphics.h +++ b/client/render/Graphics.h @@ -1,77 +1,77 @@ -/* - * Graphics.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/GameConstants.h" -#include "../lib/Color.h" -#include "../lib/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CGTownInstance; -class CHeroClass; -struct InfoAboutHero; -struct InfoAboutTown; -class CGObjectInstance; -class ObjectTemplate; -class EntityService; -class JsonNode; - -VCMI_LIB_NAMESPACE_END - -struct SDL_Surface; -class CAnimation; -class IFont; - -/// Handles fonts, hero images, town images, various graphics -class Graphics -{ - void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName); - void addImageListEntries(const EntityService * service); - - void initializeBattleGraphics(); - void loadPaletteAndColors(); - void loadErmuToPicture(); - void loadFonts(); - void initializeImageLists(); - - std::map> cachedAnimations; - -public: - std::shared_ptr getAnimation(const AnimationPath & path); - - //Fonts - static const int FONTS_NUMBER = 9; - std::array< std::shared_ptr, FONTS_NUMBER> fonts; - - using PlayerPalette = std::array; - - //various graphics - std::array playerColors; - std::array playerColorPalette; //palette to make interface colors good - array of size [256] - - PlayerPalette neutralColorPalette; - ColorRGBA neutralColor; - - std::map imageLists; - - //towns - std::map ERMUtoPicture[GameConstants::F_NUMBER]; //maps building ID to it's picture's name for each town type - //for battles - std::map< int, std::vector < std::string > > battleACToDef; //maps AC format to vector of appropriate def names - - //functions - Graphics(); - - void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player -}; - -extern Graphics * graphics; +/* + * Graphics.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/GameConstants.h" +#include "../lib/Color.h" +#include "../lib/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CGTownInstance; +class CHeroClass; +struct InfoAboutHero; +struct InfoAboutTown; +class CGObjectInstance; +class ObjectTemplate; +class EntityService; +class JsonNode; + +VCMI_LIB_NAMESPACE_END + +struct SDL_Surface; +class CAnimation; +class IFont; + +/// Handles fonts, hero images, town images, various graphics +class Graphics +{ + void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName); + void addImageListEntries(const EntityService * service); + + void initializeBattleGraphics(); + void loadPaletteAndColors(); + void loadErmuToPicture(); + void loadFonts(); + void initializeImageLists(); + + std::map> cachedAnimations; + +public: + std::shared_ptr getAnimation(const AnimationPath & path); + + //Fonts + static const int FONTS_NUMBER = 9; + std::array< std::shared_ptr, FONTS_NUMBER> fonts; + + using PlayerPalette = std::array; + + //various graphics + std::array playerColors; + std::array playerColorPalette; //palette to make interface colors good - array of size [256] + + PlayerPalette neutralColorPalette; + ColorRGBA neutralColor; + + std::map imageLists; + + //towns + std::map ERMUtoPicture[GameConstants::F_NUMBER]; //maps building ID to it's picture's name for each town type + //for battles + std::map< int, std::vector < std::string > > battleACToDef; //maps AC format to vector of appropriate def names + + //functions + Graphics(); + + void blueToPlayersAdv(SDL_Surface * sur, PlayerColor player); //replaces blue interface colour with a color of player +}; + +extern Graphics * graphics; diff --git a/client/render/ICursor.h b/client/render/ICursor.h index 6d37b3e37..af9e2f1c6 100644 --- a/client/render/ICursor.h +++ b/client/render/ICursor.h @@ -1,28 +1,28 @@ -/* - * ICursor.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 - -VCMI_LIB_NAMESPACE_BEGIN -class Point; -VCMI_LIB_NAMESPACE_END - -class IImage; - -class ICursor -{ -public: - virtual ~ICursor() = default; - - virtual void setImage(std::shared_ptr image, const Point & pivotOffset) = 0; - virtual void setCursorPosition( const Point & newPos ) = 0; - virtual void render() = 0; - virtual void setVisible( bool on) = 0; -}; - +/* + * ICursor.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 + +VCMI_LIB_NAMESPACE_BEGIN +class Point; +VCMI_LIB_NAMESPACE_END + +class IImage; + +class ICursor +{ +public: + virtual ~ICursor() = default; + + virtual void setImage(std::shared_ptr image, const Point & pivotOffset) = 0; + virtual void setCursorPosition( const Point & newPos ) = 0; + virtual void render() = 0; + virtual void setVisible( bool on) = 0; +}; + diff --git a/client/render/IImage.h b/client/render/IImage.h index 4308069c9..adf5126cd 100644 --- a/client/render/IImage.h +++ b/client/render/IImage.h @@ -1,88 +1,88 @@ -/* - * IImage.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/filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class PlayerColor; -class Rect; -class Point; -class ColorRGBA; - -VCMI_LIB_NAMESPACE_END - -struct SDL_Surface; -class ColorFilter; - -/// Defines which blit method will be selected when image is used for rendering -enum class EImageBlitMode -{ - /// Image can have no transparency and can be only used as background - OPAQUE, - - /// Image can have only a single color as transparency and has no semi-transparent areas - COLORKEY, - - /// Image might have full alpha transparency range, e.g. shadows - ALPHA -}; - -/* - * Base class for images, can be used for non-animation pictures as well - */ -class IImage -{ -public: - using SpecialPalette = std::vector; - static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011; - - //draws image on surface "where" at position - virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0; - virtual void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const = 0; - - virtual std::shared_ptr scaleFast(const Point & size) const = 0; - - virtual void exportBitmap(const boost::filesystem::path & path) const = 0; - - //Change palette to specific player - virtual void playerColored(PlayerColor player)=0; - - //set special color for flag - virtual void setFlagColor(PlayerColor player)=0; - - //test transparency of specific pixel - virtual bool isTransparent(const Point & coords) const = 0; - - virtual Point dimensions() const = 0; - int width() const; - int height() const; - - //only indexed bitmaps, 16 colors maximum - virtual void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) = 0; - virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0; - virtual void resetPalette(int colorID) = 0; - virtual void resetPalette() = 0; - - virtual void setAlpha(uint8_t value) = 0; - virtual void setBlitMode(EImageBlitMode mode) = 0; - - //only indexed bitmaps with 7 special colors - virtual void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) = 0; - - virtual void horizontalFlip() = 0; - virtual void verticalFlip() = 0; - virtual void doubleFlip() = 0; - - IImage() = default; - virtual ~IImage() = default; -}; - +/* + * IImage.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/filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; +class Rect; +class Point; +class ColorRGBA; + +VCMI_LIB_NAMESPACE_END + +struct SDL_Surface; +class ColorFilter; + +/// Defines which blit method will be selected when image is used for rendering +enum class EImageBlitMode +{ + /// Image can have no transparency and can be only used as background + OPAQUE, + + /// Image can have only a single color as transparency and has no semi-transparent areas + COLORKEY, + + /// Image might have full alpha transparency range, e.g. shadows + ALPHA +}; + +/* + * Base class for images, can be used for non-animation pictures as well + */ +class IImage +{ +public: + using SpecialPalette = std::vector; + static constexpr int32_t SPECIAL_PALETTE_MASK_CREATURES = 0b11110011; + + //draws image on surface "where" at position + virtual void draw(SDL_Surface * where, int posX = 0, int posY = 0, const Rect * src = nullptr) const = 0; + virtual void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const = 0; + + virtual std::shared_ptr scaleFast(const Point & size) const = 0; + + virtual void exportBitmap(const boost::filesystem::path & path) const = 0; + + //Change palette to specific player + virtual void playerColored(PlayerColor player)=0; + + //set special color for flag + virtual void setFlagColor(PlayerColor player)=0; + + //test transparency of specific pixel + virtual bool isTransparent(const Point & coords) const = 0; + + virtual Point dimensions() const = 0; + int width() const; + int height() const; + + //only indexed bitmaps, 16 colors maximum + virtual void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) = 0; + virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0; + virtual void resetPalette(int colorID) = 0; + virtual void resetPalette() = 0; + + virtual void setAlpha(uint8_t value) = 0; + virtual void setBlitMode(EImageBlitMode mode) = 0; + + //only indexed bitmaps with 7 special colors + virtual void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) = 0; + + virtual void horizontalFlip() = 0; + virtual void verticalFlip() = 0; + virtual void doubleFlip() = 0; + + IImage() = default; + virtual ~IImage() = default; +}; + diff --git a/client/render/IImageLoader.h b/client/render/IImageLoader.h index 966a5be26..7cd950091 100644 --- a/client/render/IImageLoader.h +++ b/client/render/IImageLoader.h @@ -1,34 +1,34 @@ -/* - * IImageLoader.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 - -VCMI_LIB_NAMESPACE_BEGIN -class Point; -VCMI_LIB_NAMESPACE_END - -class SDLImage; - -struct SDL_Color; - -class IImageLoader -{ -public: - //load size raw pixels from data - virtual void load(size_t size, const ui8 * data) = 0; - //set size pixels to color - virtual void load(size_t size, ui8 color=0) = 0; - - virtual void endLine() = 0; - //init image with these sizes and palette - virtual void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) = 0; - - virtual ~IImageLoader() = default; -}; +/* + * IImageLoader.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 + +VCMI_LIB_NAMESPACE_BEGIN +class Point; +VCMI_LIB_NAMESPACE_END + +class SDLImage; + +struct SDL_Color; + +class IImageLoader +{ +public: + //load size raw pixels from data + virtual void load(size_t size, const ui8 * data) = 0; + //set size pixels to color + virtual void load(size_t size, ui8 color=0) = 0; + + virtual void endLine() = 0; + //init image with these sizes and palette + virtual void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) = 0; + + virtual ~IImageLoader() = default; +}; diff --git a/client/renderSDL/CursorHardware.cpp b/client/renderSDL/CursorHardware.cpp index 7c117a001..aed082adc 100644 --- a/client/renderSDL/CursorHardware.cpp +++ b/client/renderSDL/CursorHardware.cpp @@ -1,77 +1,77 @@ -/* - * CursorHardware.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 "CursorHardware.h" - -#include "../gui/CGuiHandler.h" -#include "../render/Colors.h" -#include "../render/IImage.h" -#include "SDL_Extensions.h" - -#include -#include - -CursorHardware::CursorHardware(): - cursor(nullptr) -{ - SDL_ShowCursor(SDL_DISABLE); -} - -CursorHardware::~CursorHardware() -{ - if(cursor) - SDL_FreeCursor(cursor); -} - -void CursorHardware::setVisible(bool on) -{ - GH.dispatchMainThread([on]() - { - if (on) - SDL_ShowCursor(SDL_ENABLE); - else - SDL_ShowCursor(SDL_DISABLE); - }); -} - -void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) -{ - auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y); - - CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); - - image->draw(cursorSurface); - - auto oldCursor = cursor; - cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y); - - if (!cursor) - logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); - - SDL_FreeSurface(cursorSurface); - - GH.dispatchMainThread([this, oldCursor](){ - SDL_SetCursor(cursor); - - if (oldCursor) - SDL_FreeCursor(oldCursor); - }); -} - -void CursorHardware::setCursorPosition( const Point & newPos ) -{ - //no-op -} - -void CursorHardware::render() -{ - //no-op -} +/* + * CursorHardware.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 "CursorHardware.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Colors.h" +#include "../render/IImage.h" +#include "SDL_Extensions.h" + +#include +#include + +CursorHardware::CursorHardware(): + cursor(nullptr) +{ + SDL_ShowCursor(SDL_DISABLE); +} + +CursorHardware::~CursorHardware() +{ + if(cursor) + SDL_FreeCursor(cursor); +} + +void CursorHardware::setVisible(bool on) +{ + GH.dispatchMainThread([on]() + { + if (on) + SDL_ShowCursor(SDL_ENABLE); + else + SDL_ShowCursor(SDL_DISABLE); + }); +} + +void CursorHardware::setImage(std::shared_ptr image, const Point & pivotOffset) +{ + auto cursorSurface = CSDL_Ext::newSurface(image->dimensions().x, image->dimensions().y); + + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); + + image->draw(cursorSurface); + + auto oldCursor = cursor; + cursor = SDL_CreateColorCursor(cursorSurface, pivotOffset.x, pivotOffset.y); + + if (!cursor) + logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError()); + + SDL_FreeSurface(cursorSurface); + + GH.dispatchMainThread([this, oldCursor](){ + SDL_SetCursor(cursor); + + if (oldCursor) + SDL_FreeCursor(oldCursor); + }); +} + +void CursorHardware::setCursorPosition( const Point & newPos ) +{ + //no-op +} + +void CursorHardware::render() +{ + //no-op +} diff --git a/client/renderSDL/CursorHardware.h b/client/renderSDL/CursorHardware.h index 8f0aa2f19..c4e311778 100644 --- a/client/renderSDL/CursorHardware.h +++ b/client/renderSDL/CursorHardware.h @@ -1,36 +1,36 @@ -/* - * CursorHardware.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -class CAnimation; -class IImage; -struct SDL_Surface; -struct SDL_Texture; -struct SDL_Cursor; - -#include "../../lib/Point.h" -#include "../render/ICursor.h" - -class CursorHardware : public ICursor -{ - std::shared_ptr cursorImage; - - SDL_Cursor * cursor; - -public: - CursorHardware(); - ~CursorHardware(); - - void setImage(std::shared_ptr image, const Point & pivotOffset) override; - void setCursorPosition( const Point & newPos ) override; - void render() override; - void setVisible( bool on) override; -}; - +/* + * CursorHardware.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CAnimation; +class IImage; +struct SDL_Surface; +struct SDL_Texture; +struct SDL_Cursor; + +#include "../../lib/Point.h" +#include "../render/ICursor.h" + +class CursorHardware : public ICursor +{ + std::shared_ptr cursorImage; + + SDL_Cursor * cursor; + +public: + CursorHardware(); + ~CursorHardware(); + + void setImage(std::shared_ptr image, const Point & pivotOffset) override; + void setCursorPosition( const Point & newPos ) override; + void render() override; + void setVisible( bool on) override; +}; + diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index d7e184c75..1437d12ce 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -1,102 +1,102 @@ -/* - * CursorSoftware.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 "CursorSoftware.h" - -#include "../render/Colors.h" -#include "../render/IImage.h" -#include "../CMT.h" -#include "SDL_Extensions.h" - -#include -#include - -void CursorSoftware::render() -{ - //texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads - if (needUpdate) - updateTexture(); - - Point renderPos = pos - pivot; - - SDL_Rect destRect; - destRect.x = renderPos.x; - destRect.y = renderPos.y; - destRect.w = 40; - destRect.h = 40; - - SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect); -} - -void CursorSoftware::createTexture(const Point & dimensions) -{ - if(cursorTexture) - SDL_DestroyTexture(cursorTexture); - - if (cursorSurface) - SDL_FreeSurface(cursorSurface); - - cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); - cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); - - SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE); - SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND); -} - -void CursorSoftware::updateTexture() -{ - if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) - createTexture(cursorImage->dimensions()); - - CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); - - cursorImage->draw(cursorSurface); - SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch); - needUpdate = false; -} - -void CursorSoftware::setImage(std::shared_ptr image, const Point & pivotOffset) -{ - assert(image != nullptr); - cursorImage = image; - pivot = pivotOffset; - needUpdate = true; -} - -void CursorSoftware::setCursorPosition( const Point & newPos ) -{ - pos = newPos; -} - -void CursorSoftware::setVisible(bool on) -{ - visible = on; -} - -CursorSoftware::CursorSoftware(): - cursorTexture(nullptr), - cursorSurface(nullptr), - needUpdate(false), - visible(false), - pivot(0,0) -{ - SDL_ShowCursor(SDL_DISABLE); -} - -CursorSoftware::~CursorSoftware() -{ - if(cursorTexture) - SDL_DestroyTexture(cursorTexture); - - if (cursorSurface) - SDL_FreeSurface(cursorSurface); -} - +/* + * CursorSoftware.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 "CursorSoftware.h" + +#include "../render/Colors.h" +#include "../render/IImage.h" +#include "../CMT.h" +#include "SDL_Extensions.h" + +#include +#include + +void CursorSoftware::render() +{ + //texture must be updated in the main (renderer) thread, but changes to cursor type may come from other threads + if (needUpdate) + updateTexture(); + + Point renderPos = pos - pivot; + + SDL_Rect destRect; + destRect.x = renderPos.x; + destRect.y = renderPos.y; + destRect.w = 40; + destRect.h = 40; + + SDL_RenderCopy(mainRenderer, cursorTexture, nullptr, &destRect); +} + +void CursorSoftware::createTexture(const Point & dimensions) +{ + if(cursorTexture) + SDL_DestroyTexture(cursorTexture); + + if (cursorSurface) + SDL_FreeSurface(cursorSurface); + + cursorSurface = CSDL_Ext::newSurface(dimensions.x, dimensions.y); + cursorTexture = SDL_CreateTexture(mainRenderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, dimensions.x, dimensions.y); + + SDL_SetSurfaceBlendMode(cursorSurface, SDL_BLENDMODE_NONE); + SDL_SetTextureBlendMode(cursorTexture, SDL_BLENDMODE_BLEND); +} + +void CursorSoftware::updateTexture() +{ + if (!cursorSurface || Point(cursorSurface->w, cursorSurface->h) != cursorImage->dimensions()) + createTexture(cursorImage->dimensions()); + + CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); + + cursorImage->draw(cursorSurface); + SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch); + needUpdate = false; +} + +void CursorSoftware::setImage(std::shared_ptr image, const Point & pivotOffset) +{ + assert(image != nullptr); + cursorImage = image; + pivot = pivotOffset; + needUpdate = true; +} + +void CursorSoftware::setCursorPosition( const Point & newPos ) +{ + pos = newPos; +} + +void CursorSoftware::setVisible(bool on) +{ + visible = on; +} + +CursorSoftware::CursorSoftware(): + cursorTexture(nullptr), + cursorSurface(nullptr), + needUpdate(false), + visible(false), + pivot(0,0) +{ + SDL_ShowCursor(SDL_DISABLE); +} + +CursorSoftware::~CursorSoftware() +{ + if(cursorTexture) + SDL_DestroyTexture(cursorTexture); + + if (cursorSurface) + SDL_FreeSurface(cursorSurface); +} + diff --git a/client/renderSDL/CursorSoftware.h b/client/renderSDL/CursorSoftware.h index f33add2f6..44080e3e2 100644 --- a/client/renderSDL/CursorSoftware.h +++ b/client/renderSDL/CursorSoftware.h @@ -1,44 +1,44 @@ -/* - * CursorSoftware.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -class CAnimation; -class IImage; -struct SDL_Surface; -struct SDL_Texture; -struct SDL_Cursor; - -#include "../../lib/Point.h" -#include "../render/ICursor.h" - -class CursorSoftware : public ICursor -{ - std::shared_ptr cursorImage; - - SDL_Texture * cursorTexture; - SDL_Surface * cursorSurface; - - Point pos; - Point pivot; - bool needUpdate; - bool visible; - - void createTexture(const Point & dimensions); - void updateTexture(); -public: - CursorSoftware(); - ~CursorSoftware(); - - void setImage(std::shared_ptr image, const Point & pivotOffset) override; - void setCursorPosition( const Point & newPos ) override; - void render() override; - void setVisible( bool on) override; -}; - +/* + * CursorSoftware.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CAnimation; +class IImage; +struct SDL_Surface; +struct SDL_Texture; +struct SDL_Cursor; + +#include "../../lib/Point.h" +#include "../render/ICursor.h" + +class CursorSoftware : public ICursor +{ + std::shared_ptr cursorImage; + + SDL_Texture * cursorTexture; + SDL_Surface * cursorSurface; + + Point pos; + Point pivot; + bool needUpdate; + bool visible; + + void createTexture(const Point & dimensions); + void updateTexture(); +public: + CursorSoftware(); + ~CursorSoftware(); + + void setImage(std::shared_ptr image, const Point & pivotOffset) override; + void setCursorPosition( const Point & newPos ) override; + void render() override; + void setVisible( bool on) override; +}; + diff --git a/client/renderSDL/SDLImage.cpp b/client/renderSDL/SDLImage.cpp index 891b2e975..e8930abd3 100644 --- a/client/renderSDL/SDLImage.cpp +++ b/client/renderSDL/SDLImage.cpp @@ -1,356 +1,356 @@ -/* - * SDLImage.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 "SDLImage.h" - -#include "SDLImageLoader.h" -#include "SDL_Extensions.h" - -#include "../render/ColorFilter.h" -#include "../render/CBitmapHandler.h" -#include "../render/CDefFile.h" -#include "../render/Graphics.h" - -#include "../../lib/JsonNode.h" - -#include - -class SDLImageLoader; - -int IImage::width() const -{ - return dimensions().x; -} - -int IImage::height() const -{ - return dimensions().y; -} - -SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - SDLImageLoader loader(this); - data->loadFrame(frame, group, loader); - - savePalette(); - setBlitMode(EImageBlitMode::ALPHA); -} - -SDLImage::SDLImage(SDL_Surface * from, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - surf = from; - if (surf == nullptr) - return; - - savePalette(); - setBlitMode(mode); - - surf->refcount++; - fullSize.x = surf->w; - fullSize.y = surf->h; -} - -SDLImage::SDLImage(const JsonNode & conf, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - surf = BitmapHandler::loadBitmap(ImagePath::fromJson(conf["file"])); - - if(surf == nullptr) - return; - - savePalette(); - setBlitMode(mode); - - const JsonNode & jsonMargins = conf["margins"]; - - margins.x = static_cast(jsonMargins["left"].Integer()); - margins.y = static_cast(jsonMargins["top"].Integer()); - - fullSize.x = static_cast(conf["width"].Integer()); - fullSize.y = static_cast(conf["height"].Integer()); - - if(fullSize.x == 0) - { - fullSize.x = margins.x + surf->w + (int)jsonMargins["right"].Integer(); - } - - if(fullSize.y == 0) - { - fullSize.y = margins.y + surf->h + (int)jsonMargins["bottom"].Integer(); - } -} - -SDLImage::SDLImage(const ImagePath & filename, EImageBlitMode mode) - : surf(nullptr), - margins(0, 0), - fullSize(0, 0), - originalPalette(nullptr) -{ - surf = BitmapHandler::loadBitmap(filename); - - if(surf == nullptr) - { - logGlobal->error("Error: failed to load image %s", filename.getOriginalName()); - return; - } - else - { - savePalette(); - setBlitMode(mode); - fullSize.x = surf->w; - fullSize.y = surf->h; - } -} - -void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src) const -{ - if(!surf) - return; - - Rect destRect(posX, posY, surf->w, surf->h); - draw(where, &destRect, src); -} - -void SDLImage::draw(SDL_Surface* where, const Rect * dest, const Rect* src) const -{ - if (!surf) - return; - - Rect sourceRect(0, 0, surf->w, surf->h); - - Point destShift(0, 0); - - if(src) - { - if(src->x < margins.x) - destShift.x += margins.x - src->x; - - if(src->y < margins.y) - destShift.y += margins.y - src->y; - - sourceRect = Rect(*src).intersect(Rect(margins.x, margins.y, surf->w, surf->h)); - - sourceRect -= margins; - } - else - destShift = margins; - - if(dest) - destShift += dest->topLeft(); - - uint8_t perSurfaceAlpha; - if (SDL_GetSurfaceAlphaMod(surf, &perSurfaceAlpha) != 0) - logGlobal->error("SDL_GetSurfaceAlphaMod faied! %s", SDL_GetError()); - - if(surf->format->BitsPerPixel == 8 && perSurfaceAlpha == SDL_ALPHA_OPAQUE && blitMode == EImageBlitMode::ALPHA) - { - CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift); - } - else - { - CSDL_Ext::blitSurface(surf, sourceRect, where, destShift); - } -} - -std::shared_ptr SDLImage::scaleFast(const Point & size) const -{ - float scaleX = float(size.x) / width(); - float scaleY = float(size.y) / height(); - - auto scaled = CSDL_Ext::scaleSurfaceFast(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY)); - - if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point - CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]); - else if(scaled->format && scaled->format->Amask) - SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND);//just in case - else - CSDL_Ext::setDefaultColorKey(scaled);//just in case - - SDLImage * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); - - ret->fullSize.x = (int) round((float)fullSize.x * scaleX); - ret->fullSize.y = (int) round((float)fullSize.y * scaleY); - - ret->margins.x = (int) round((float)margins.x * scaleX); - ret->margins.y = (int) round((float)margins.y * scaleY); - - // erase our own reference - SDL_FreeSurface(scaled); - - return std::shared_ptr(ret); -} - -void SDLImage::exportBitmap(const boost::filesystem::path& path) const -{ - SDL_SaveBMP(surf, path.string().c_str()); -} - -void SDLImage::playerColored(PlayerColor player) -{ - graphics->blueToPlayersAdv(surf, player); -} - -void SDLImage::setAlpha(uint8_t value) -{ - CSDL_Ext::setAlpha (surf, value); - if (value != 255) - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); -} - -void SDLImage::setBlitMode(EImageBlitMode mode) -{ - blitMode = mode; - - if (blitMode != EImageBlitMode::OPAQUE && surf->format->Amask != 0) - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); - else - SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); -} - -void SDLImage::setFlagColor(PlayerColor player) -{ - if(player.isValidPlayer() || player==PlayerColor::NEUTRAL) - CSDL_Ext::setPlayerColor(surf, player); -} - -bool SDLImage::isTransparent(const Point & coords) const -{ - return CSDL_Ext::isTransparent(surf, coords.x, coords.y); -} - -Point SDLImage::dimensions() const -{ - return fullSize; -} - -void SDLImage::horizontalFlip() -{ - margins.y = fullSize.y - surf->h - margins.y; - - //todo: modify in-place - SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); - SDL_FreeSurface(surf); - surf = flipped; -} - -void SDLImage::verticalFlip() -{ - margins.x = fullSize.x - surf->w - margins.x; - - //todo: modify in-place - SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); - SDL_FreeSurface(surf); - surf = flipped; -} - -void SDLImage::doubleFlip() -{ - horizontalFlip(); - verticalFlip(); -} - -// Keep the original palette, in order to do color switching operation -void SDLImage::savePalette() -{ - // For some images that don't have palette, skip this - if(surf->format->palette == nullptr) - return; - - if(originalPalette == nullptr) - originalPalette = SDL_AllocPalette(DEFAULT_PALETTE_COLORS); - - SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, DEFAULT_PALETTE_COLORS); -} - -void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) -{ - if(surf->format->palette) - { - std::vector shifterColors(colorsToMove); - - for(uint32_t i=0; icolors[firstColorID + i]; - } - CSDL_Ext::setColors(surf, shifterColors.data(), firstColorID, colorsToMove); - } -} - -void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) -{ - if(originalPalette == nullptr) - return; - - SDL_Palette* palette = surf->format->palette; - - // Note: here we skip first colors in the palette that are predefined in H3 images - for(int i = 0; i < palette->ncolors; i++) - { - if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - continue; - - palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); - } -} - -void SDLImage::resetPalette() -{ - if(originalPalette == nullptr) - return; - - // Always keept the original palette not changed, copy a new palette to assign to surface - SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors); -} - -void SDLImage::resetPalette( int colorID ) -{ - if(originalPalette == nullptr) - return; - - // Always keept the original palette not changed, copy a new palette to assign to surface - SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1); -} - -void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) -{ - if(surf->format->palette) - { - size_t last = std::min(specialPalette.size(), surf->format->palette->ncolors); - - for (size_t i = 0; i < last; ++i) - { - if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) - surf->format->palette->colors[i] = CSDL_Ext::toSDL(specialPalette[i]); - } - } -} - -SDLImage::~SDLImage() -{ - SDL_FreeSurface(surf); - - if(originalPalette != nullptr) - { - SDL_FreePalette(originalPalette); - originalPalette = nullptr; - } -} - +/* + * SDLImage.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 "SDLImage.h" + +#include "SDLImageLoader.h" +#include "SDL_Extensions.h" + +#include "../render/ColorFilter.h" +#include "../render/CBitmapHandler.h" +#include "../render/CDefFile.h" +#include "../render/Graphics.h" + +#include "../../lib/JsonNode.h" + +#include + +class SDLImageLoader; + +int IImage::width() const +{ + return dimensions().x; +} + +int IImage::height() const +{ + return dimensions().y; +} + +SDLImage::SDLImage(CDefFile * data, size_t frame, size_t group) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + SDLImageLoader loader(this); + data->loadFrame(frame, group, loader); + + savePalette(); + setBlitMode(EImageBlitMode::ALPHA); +} + +SDLImage::SDLImage(SDL_Surface * from, EImageBlitMode mode) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + surf = from; + if (surf == nullptr) + return; + + savePalette(); + setBlitMode(mode); + + surf->refcount++; + fullSize.x = surf->w; + fullSize.y = surf->h; +} + +SDLImage::SDLImage(const JsonNode & conf, EImageBlitMode mode) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + surf = BitmapHandler::loadBitmap(ImagePath::fromJson(conf["file"])); + + if(surf == nullptr) + return; + + savePalette(); + setBlitMode(mode); + + const JsonNode & jsonMargins = conf["margins"]; + + margins.x = static_cast(jsonMargins["left"].Integer()); + margins.y = static_cast(jsonMargins["top"].Integer()); + + fullSize.x = static_cast(conf["width"].Integer()); + fullSize.y = static_cast(conf["height"].Integer()); + + if(fullSize.x == 0) + { + fullSize.x = margins.x + surf->w + (int)jsonMargins["right"].Integer(); + } + + if(fullSize.y == 0) + { + fullSize.y = margins.y + surf->h + (int)jsonMargins["bottom"].Integer(); + } +} + +SDLImage::SDLImage(const ImagePath & filename, EImageBlitMode mode) + : surf(nullptr), + margins(0, 0), + fullSize(0, 0), + originalPalette(nullptr) +{ + surf = BitmapHandler::loadBitmap(filename); + + if(surf == nullptr) + { + logGlobal->error("Error: failed to load image %s", filename.getOriginalName()); + return; + } + else + { + savePalette(); + setBlitMode(mode); + fullSize.x = surf->w; + fullSize.y = surf->h; + } +} + +void SDLImage::draw(SDL_Surface *where, int posX, int posY, const Rect *src) const +{ + if(!surf) + return; + + Rect destRect(posX, posY, surf->w, surf->h); + draw(where, &destRect, src); +} + +void SDLImage::draw(SDL_Surface* where, const Rect * dest, const Rect* src) const +{ + if (!surf) + return; + + Rect sourceRect(0, 0, surf->w, surf->h); + + Point destShift(0, 0); + + if(src) + { + if(src->x < margins.x) + destShift.x += margins.x - src->x; + + if(src->y < margins.y) + destShift.y += margins.y - src->y; + + sourceRect = Rect(*src).intersect(Rect(margins.x, margins.y, surf->w, surf->h)); + + sourceRect -= margins; + } + else + destShift = margins; + + if(dest) + destShift += dest->topLeft(); + + uint8_t perSurfaceAlpha; + if (SDL_GetSurfaceAlphaMod(surf, &perSurfaceAlpha) != 0) + logGlobal->error("SDL_GetSurfaceAlphaMod faied! %s", SDL_GetError()); + + if(surf->format->BitsPerPixel == 8 && perSurfaceAlpha == SDL_ALPHA_OPAQUE && blitMode == EImageBlitMode::ALPHA) + { + CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift); + } + else + { + CSDL_Ext::blitSurface(surf, sourceRect, where, destShift); + } +} + +std::shared_ptr SDLImage::scaleFast(const Point & size) const +{ + float scaleX = float(size.x) / width(); + float scaleY = float(size.y) / height(); + + auto scaled = CSDL_Ext::scaleSurfaceFast(surf, (int)(surf->w * scaleX), (int)(surf->h * scaleY)); + + if (scaled->format && scaled->format->palette) // fix color keying, because SDL loses it at this point + CSDL_Ext::setColorKey(scaled, scaled->format->palette->colors[0]); + else if(scaled->format && scaled->format->Amask) + SDL_SetSurfaceBlendMode(scaled, SDL_BLENDMODE_BLEND);//just in case + else + CSDL_Ext::setDefaultColorKey(scaled);//just in case + + SDLImage * ret = new SDLImage(scaled, EImageBlitMode::ALPHA); + + ret->fullSize.x = (int) round((float)fullSize.x * scaleX); + ret->fullSize.y = (int) round((float)fullSize.y * scaleY); + + ret->margins.x = (int) round((float)margins.x * scaleX); + ret->margins.y = (int) round((float)margins.y * scaleY); + + // erase our own reference + SDL_FreeSurface(scaled); + + return std::shared_ptr(ret); +} + +void SDLImage::exportBitmap(const boost::filesystem::path& path) const +{ + SDL_SaveBMP(surf, path.string().c_str()); +} + +void SDLImage::playerColored(PlayerColor player) +{ + graphics->blueToPlayersAdv(surf, player); +} + +void SDLImage::setAlpha(uint8_t value) +{ + CSDL_Ext::setAlpha (surf, value); + if (value != 255) + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); +} + +void SDLImage::setBlitMode(EImageBlitMode mode) +{ + blitMode = mode; + + if (blitMode != EImageBlitMode::OPAQUE && surf->format->Amask != 0) + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_BLEND); + else + SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); +} + +void SDLImage::setFlagColor(PlayerColor player) +{ + if(player.isValidPlayer() || player==PlayerColor::NEUTRAL) + CSDL_Ext::setPlayerColor(surf, player); +} + +bool SDLImage::isTransparent(const Point & coords) const +{ + return CSDL_Ext::isTransparent(surf, coords.x, coords.y); +} + +Point SDLImage::dimensions() const +{ + return fullSize; +} + +void SDLImage::horizontalFlip() +{ + margins.y = fullSize.y - surf->h - margins.y; + + //todo: modify in-place + SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf); + SDL_FreeSurface(surf); + surf = flipped; +} + +void SDLImage::verticalFlip() +{ + margins.x = fullSize.x - surf->w - margins.x; + + //todo: modify in-place + SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf); + SDL_FreeSurface(surf); + surf = flipped; +} + +void SDLImage::doubleFlip() +{ + horizontalFlip(); + verticalFlip(); +} + +// Keep the original palette, in order to do color switching operation +void SDLImage::savePalette() +{ + // For some images that don't have palette, skip this + if(surf->format->palette == nullptr) + return; + + if(originalPalette == nullptr) + originalPalette = SDL_AllocPalette(DEFAULT_PALETTE_COLORS); + + SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, DEFAULT_PALETTE_COLORS); +} + +void SDLImage::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) +{ + if(surf->format->palette) + { + std::vector shifterColors(colorsToMove); + + for(uint32_t i=0; icolors[firstColorID + i]; + } + CSDL_Ext::setColors(surf, shifterColors.data(), firstColorID, colorsToMove); + } +} + +void SDLImage::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) +{ + if(originalPalette == nullptr) + return; + + SDL_Palette* palette = surf->format->palette; + + // Note: here we skip first colors in the palette that are predefined in H3 images + for(int i = 0; i < palette->ncolors; i++) + { + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) + continue; + + palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i]))); + } +} + +void SDLImage::resetPalette() +{ + if(originalPalette == nullptr) + return; + + // Always keept the original palette not changed, copy a new palette to assign to surface + SDL_SetPaletteColors(surf->format->palette, originalPalette->colors, 0, originalPalette->ncolors); +} + +void SDLImage::resetPalette( int colorID ) +{ + if(originalPalette == nullptr) + return; + + // Always keept the original palette not changed, copy a new palette to assign to surface + SDL_SetPaletteColors(surf->format->palette, originalPalette->colors + colorID, colorID, 1); +} + +void SDLImage::setSpecialPallete(const IImage::SpecialPalette & specialPalette, uint32_t colorsToSkipMask) +{ + if(surf->format->palette) + { + size_t last = std::min(specialPalette.size(), surf->format->palette->ncolors); + + for (size_t i = 0; i < last; ++i) + { + if(i < std::numeric_limits::digits && ((colorsToSkipMask >> i) & 1) == 1) + surf->format->palette->colors[i] = CSDL_Ext::toSDL(specialPalette[i]); + } + } +} + +SDLImage::~SDLImage() +{ + SDL_FreeSurface(surf); + + if(originalPalette != nullptr) + { + SDL_FreePalette(originalPalette); + originalPalette = nullptr; + } +} + diff --git a/client/renderSDL/SDLImage.h b/client/renderSDL/SDLImage.h index 10aecd70d..05c516fa2 100644 --- a/client/renderSDL/SDLImage.h +++ b/client/renderSDL/SDLImage.h @@ -1,83 +1,83 @@ -/* - * SDLImage.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 "../render/IImage.h" -#include "../../lib/Point.h" - -VCMI_LIB_NAMESPACE_BEGIN -class JsonNode; -VCMI_LIB_NAMESPACE_END - -class CDefFile; - -struct SDL_Surface; -struct SDL_Palette; - -/* - * Wrapper around SDL_Surface - */ -class SDLImage : public IImage -{ -public: - - const static int DEFAULT_PALETTE_COLORS = 256; - - //Surface without empty borders - SDL_Surface * surf; - //size of left and top borders - Point margins; - //total size including borders - Point fullSize; - - EImageBlitMode blitMode; - -public: - //Load image from def file - SDLImage(CDefFile *data, size_t frame, size_t group=0); - //Load from bitmap file - SDLImage(const ImagePath & filename, EImageBlitMode blitMode); - - SDLImage(const JsonNode & conf, EImageBlitMode blitMode); - //Create using existing surface, extraRef will increase refcount on SDL_Surface - SDLImage(SDL_Surface * from, EImageBlitMode blitMode); - ~SDLImage(); - - // Keep the original palette, in order to do color switching operation - void savePalette(); - - void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr) const override; - void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const override; - std::shared_ptr scaleFast(const Point & size) const override; - void exportBitmap(const boost::filesystem::path & path) const override; - void playerColored(PlayerColor player) override; - void setFlagColor(PlayerColor player) override; - bool isTransparent(const Point & coords) const override; - Point dimensions() const override; - - void horizontalFlip() override; - void verticalFlip() override; - void doubleFlip() override; - - void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; - void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; - void resetPalette(int colorID) override; - void resetPalette() override; - - void setAlpha(uint8_t value) override; - void setBlitMode(EImageBlitMode mode) override; - - void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) override; - - friend class SDLImageLoader; - -private: - SDL_Palette * originalPalette; -}; +/* + * SDLImage.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 "../render/IImage.h" +#include "../../lib/Point.h" + +VCMI_LIB_NAMESPACE_BEGIN +class JsonNode; +VCMI_LIB_NAMESPACE_END + +class CDefFile; + +struct SDL_Surface; +struct SDL_Palette; + +/* + * Wrapper around SDL_Surface + */ +class SDLImage : public IImage +{ +public: + + const static int DEFAULT_PALETTE_COLORS = 256; + + //Surface without empty borders + SDL_Surface * surf; + //size of left and top borders + Point margins; + //total size including borders + Point fullSize; + + EImageBlitMode blitMode; + +public: + //Load image from def file + SDLImage(CDefFile *data, size_t frame, size_t group=0); + //Load from bitmap file + SDLImage(const ImagePath & filename, EImageBlitMode blitMode); + + SDLImage(const JsonNode & conf, EImageBlitMode blitMode); + //Create using existing surface, extraRef will increase refcount on SDL_Surface + SDLImage(SDL_Surface * from, EImageBlitMode blitMode); + ~SDLImage(); + + // Keep the original palette, in order to do color switching operation + void savePalette(); + + void draw(SDL_Surface * where, int posX=0, int posY=0, const Rect *src=nullptr) const override; + void draw(SDL_Surface * where, const Rect * dest, const Rect * src) const override; + std::shared_ptr scaleFast(const Point & size) const override; + void exportBitmap(const boost::filesystem::path & path) const override; + void playerColored(PlayerColor player) override; + void setFlagColor(PlayerColor player) override; + bool isTransparent(const Point & coords) const override; + Point dimensions() const override; + + void horizontalFlip() override; + void verticalFlip() override; + void doubleFlip() override; + + void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override; + void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override; + void resetPalette(int colorID) override; + void resetPalette() override; + + void setAlpha(uint8_t value) override; + void setBlitMode(EImageBlitMode mode) override; + + void setSpecialPallete(const SpecialPalette & SpecialPalette, uint32_t colorsToSkipMask) override; + + friend class SDLImageLoader; + +private: + SDL_Palette * originalPalette; +}; diff --git a/client/renderSDL/SDLImageLoader.cpp b/client/renderSDL/SDLImageLoader.cpp index c951dcc2d..30e288f38 100644 --- a/client/renderSDL/SDLImageLoader.cpp +++ b/client/renderSDL/SDLImageLoader.cpp @@ -1,74 +1,74 @@ -/* - * SDLImageLoader.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 "SDLImageLoader.h" - -#include "SDLImage.h" - -#include "../../lib/Point.h" - -#include - -SDLImageLoader::SDLImageLoader(SDLImage * Img): - image(Img), - lineStart(nullptr), - position(nullptr) -{ -} - -void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) -{ - //Init image - image->surf = SDL_CreateRGBSurface(0, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); - image->margins = Margins; - image->fullSize = FullSize; - - //Prepare surface - SDL_Palette * p = SDL_AllocPalette(SDLImage::DEFAULT_PALETTE_COLORS); - SDL_SetPaletteColors(p, pal, 0, SDLImage::DEFAULT_PALETTE_COLORS); - SDL_SetSurfacePalette(image->surf, p); - SDL_FreePalette(p); - - SDL_LockSurface(image->surf); - lineStart = position = (ui8*)image->surf->pixels; -} - -inline void SDLImageLoader::load(size_t size, const ui8 * data) -{ - if (size) - { - memcpy((void *)position, data, size); - position += size; - } -} - -inline void SDLImageLoader::load(size_t size, ui8 color) -{ - if (size) - { - memset((void *)position, color, size); - position += size; - } -} - -inline void SDLImageLoader::endLine() -{ - lineStart += image->surf->pitch; - position = lineStart; -} - -SDLImageLoader::~SDLImageLoader() -{ - SDL_UnlockSurface(image->surf); - SDL_SetColorKey(image->surf, SDL_TRUE, 0); - //TODO: RLE if compressed and bpp>1 -} - +/* + * SDLImageLoader.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 "SDLImageLoader.h" + +#include "SDLImage.h" + +#include "../../lib/Point.h" + +#include + +SDLImageLoader::SDLImageLoader(SDLImage * Img): + image(Img), + lineStart(nullptr), + position(nullptr) +{ +} + +void SDLImageLoader::init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal) +{ + //Init image + image->surf = SDL_CreateRGBSurface(0, SpriteSize.x, SpriteSize.y, 8, 0, 0, 0, 0); + image->margins = Margins; + image->fullSize = FullSize; + + //Prepare surface + SDL_Palette * p = SDL_AllocPalette(SDLImage::DEFAULT_PALETTE_COLORS); + SDL_SetPaletteColors(p, pal, 0, SDLImage::DEFAULT_PALETTE_COLORS); + SDL_SetSurfacePalette(image->surf, p); + SDL_FreePalette(p); + + SDL_LockSurface(image->surf); + lineStart = position = (ui8*)image->surf->pixels; +} + +inline void SDLImageLoader::load(size_t size, const ui8 * data) +{ + if (size) + { + memcpy((void *)position, data, size); + position += size; + } +} + +inline void SDLImageLoader::load(size_t size, ui8 color) +{ + if (size) + { + memset((void *)position, color, size); + position += size; + } +} + +inline void SDLImageLoader::endLine() +{ + lineStart += image->surf->pitch; + position = lineStart; +} + +SDLImageLoader::~SDLImageLoader() +{ + SDL_UnlockSurface(image->surf); + SDL_SetColorKey(image->surf, SDL_TRUE, 0); + //TODO: RLE if compressed and bpp>1 +} + diff --git a/client/renderSDL/SDLImageLoader.h b/client/renderSDL/SDLImageLoader.h index 5a5b6e97f..6e4cca11b 100644 --- a/client/renderSDL/SDLImageLoader.h +++ b/client/renderSDL/SDLImageLoader.h @@ -1,32 +1,32 @@ -/* - * SDLImageLoader.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 "../render/IImageLoader.h" - -class SDLImageLoader : public IImageLoader -{ - SDLImage * image; - ui8 * lineStart; - ui8 * position; -public: - //load size raw pixels from data - void load(size_t size, const ui8 * data); - //set size pixels to color - void load(size_t size, ui8 color=0); - void endLine(); - //init image with these sizes and palette - void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); - - SDLImageLoader(SDLImage * Img); - ~SDLImageLoader(); -}; - - +/* + * SDLImageLoader.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 "../render/IImageLoader.h" + +class SDLImageLoader : public IImageLoader +{ + SDLImage * image; + ui8 * lineStart; + ui8 * position; +public: + //load size raw pixels from data + void load(size_t size, const ui8 * data); + //set size pixels to color + void load(size_t size, ui8 color=0); + void endLine(); + //init image with these sizes and palette + void init(Point SpriteSize, Point Margins, Point FullSize, SDL_Color *pal); + + SDLImageLoader(SDLImage * Img); + ~SDLImageLoader(); +}; + + diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index f1717644e..04254db36 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -1,869 +1,869 @@ -/* - * SDL_Extensions.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 "SDL_Extensions.h" - -#include "SDL_PixelAccess.h" - -#include "../render/Graphics.h" -#include "../render/Colors.h" -#include "../CMT.h" - -#include "../../lib/GameConstants.h" - -#include - -Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) -{ - return Rect(Point(rect.x, rect.y), Point(rect.w, rect.h)); -} - -SDL_Rect CSDL_Ext::toSDL(const Rect & rect) -{ - SDL_Rect result; - result.x = rect.x; - result.y = rect.y; - result.w = rect.w; - result.h = rect.h; - return result; -} - -ColorRGBA CSDL_Ext::fromSDL(const SDL_Color & color) -{ - return { color.r, color.g, color.b, color.a }; -} - -SDL_Color CSDL_Ext::toSDL(const ColorRGBA & color) -{ - SDL_Color result; - result.r = color.r; - result.g = color.g; - result.b = color.b; - result.a = color.a; - - return result; -} - -void CSDL_Ext::setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors) -{ - SDL_SetPaletteColors(surface->format->palette,colors,firstcolor,ncolors); -} - -void CSDL_Ext::setAlpha(SDL_Surface * bg, int value) -{ - SDL_SetSurfaceAlphaMod(bg, value); -} - -void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) -{ - SDL_Rect rectSDL = CSDL_Ext::toSDL(rect); - if(0 !=SDL_UpdateTexture(screenTexture, &rectSDL, surface->pixels, surface->pitch)) - logGlobal->error("%sSDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); - - SDL_RenderClear(mainRenderer); - if(0 != SDL_RenderCopy(mainRenderer, screenTexture, NULL, NULL)) - logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); - SDL_RenderPresent(mainRenderer); - -} - -SDL_Surface * CSDL_Ext::newSurface(int w, int h) -{ - return newSurface(w, h, screen); -} - -SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given -{ - SDL_Surface * ret = SDL_CreateRGBSurface(0,w,h,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); - - if(ret == nullptr) - { - const char * error = SDL_GetError(); - - std::string messagePattern = "Failed to create SDL Surface of size %d x %d, %d bpp. Reason: %s"; - std::string message = boost::str(boost::format(messagePattern) % w % h % mod->format->BitsPerPixel % error); - - handleFatalError(message, true); - } - - if (mod->format->palette) - { - assert(ret->format->palette); - assert(ret->format->palette->ncolors == mod->format->palette->ncolors); - memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color)); - } - return ret; -} - -SDL_Surface * CSDL_Ext::copySurface(SDL_Surface * mod) //returns copy of given surface -{ - //return SDL_DisplayFormat(mod); - return SDL_ConvertSurface(mod, mod->format, mod->flags); -} - -template -SDL_Surface * CSDL_Ext::createSurfaceWithBpp(int width, int height) -{ - uint32_t rMask = 0, gMask = 0, bMask = 0, aMask = 0; - - Channels::px::r.set((uint8_t*)&rMask, 255); - Channels::px::g.set((uint8_t*)&gMask, 255); - Channels::px::b.set((uint8_t*)&bMask, 255); - Channels::px::a.set((uint8_t*)&aMask, 255); - - return SDL_CreateRGBSurface(0, width, height, bpp * 8, rMask, gMask, bMask, aMask); -} - -void CSDL_Ext::blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst) -{ - CSDL_Ext::blitSurface(src, dst, Point(x, y)); -} - -void CSDL_Ext::blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst) -{ - if (src) - blitAt(src,pos.x,pos.y,dst); -} - -// Vertical flip -SDL_Surface * CSDL_Ext::verticalFlip(SDL_Surface * toRot) -{ - SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); - - SDL_LockSurface(ret); - SDL_LockSurface(toRot); - - const int bpp = ret->format->BytesPerPixel; - - char * src = reinterpret_cast(toRot->pixels); - char * dst = reinterpret_cast(ret->pixels); - - for(int i=0; ih; i++) - { - //FIXME: optimization bugged -// if (bpp == 1) -// { -// // much faster for 8-bit surfaces (majority of our data) -// std::reverse_copy(src, src + toRot->pitch, dst); -// } -// else -// { - char * srcPxl = src; - char * dstPxl = dst + ret->w * bpp; - - for(int j=0; jw; j++) - { - dstPxl -= bpp; - std::copy(srcPxl, srcPxl + bpp, dstPxl); - srcPxl += bpp; - } -// } - src += toRot->pitch; - dst += ret->pitch; - } - SDL_UnlockSurface(ret); - SDL_UnlockSurface(toRot); - return ret; -} - -// Horizontal flip -SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) -{ - SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); - SDL_LockSurface(ret); - SDL_LockSurface(toRot); - char * src = reinterpret_cast(toRot->pixels); - char * dst = reinterpret_cast(ret->pixels) + ret->h * ret->pitch; - - for(int i=0; ih; i++) - { - dst -= ret->pitch; - std::copy(src, src + toRot->pitch, dst); - src += toRot->pitch; - } - SDL_UnlockSurface(ret); - SDL_UnlockSurface(toRot); - return ret; -} - -uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte) -{ - int bpp = surface->format->BytesPerPixel; - /* Here p is the address to the pixel we want to retrieve */ - uint8_t *p = (uint8_t *)surface->pixels + y * surface->pitch + x * bpp; - - switch(bpp) - { - case 1: - if(colorByte) - return colorTouint32_t(surface->format->palette->colors+(*p)); - else - return *p; - - case 2: - return *(uint16_t *)p; - - case 3: - return p[0] | p[1] << 8 | p[2] << 16; - - case 4: - return *(uint32_t *)p; - - default: - return 0; // shouldn't happen, but avoids warnings - } -} - -template -int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput) -{ - SDL_Rect srcRectInstance = CSDL_Ext::toSDL(srcRectInput); - SDL_Rect dstRectInstance = CSDL_Ext::toSDL(Rect(dstPointInput, srcRectInput.dimensions())); - - SDL_Rect * srcRect =&srcRectInstance; - SDL_Rect * dstRect =&dstRectInstance; - - /* Make sure the surfaces aren't locked */ - if ( ! src || ! dst ) - { - SDL_SetError("SDL_UpperBlit: passed a nullptr surface"); - return -1; - } - - if ( src->locked || dst->locked ) - { - SDL_SetError("Surfaces must not be locked during blit"); - return -1; - } - - if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok - { - SDL_Rect fulldst; - int srcx, srcy, w, h; - - /* If the destination rectangle is nullptr, use the entire dest surface */ - if ( dstRect == nullptr ) - { - fulldst.x = fulldst.y = 0; - dstRect = &fulldst; - } - - /* clip the source rectangle to the source surface */ - if(srcRect) - { - int maxw, maxh; - - srcx = srcRect->x; - w = srcRect->w; - if(srcx < 0) - { - w += srcx; - dstRect->x -= srcx; - srcx = 0; - } - maxw = src->w - srcx; - if(maxw < w) - w = maxw; - - srcy = srcRect->y; - h = srcRect->h; - if(srcy < 0) - { - h += srcy; - dstRect->y -= srcy; - srcy = 0; - } - maxh = src->h - srcy; - if(maxh < h) - h = maxh; - - } - else - { - srcx = srcy = 0; - w = src->w; - h = src->h; - } - - /* clip the destination rectangle against the clip rectangle */ - { - SDL_Rect *clip = &dst->clip_rect; - int dx, dy; - - dx = clip->x - dstRect->x; - if(dx > 0) - { - w -= dx; - dstRect->x += dx; - srcx += dx; - } - dx = dstRect->x + w - clip->x - clip->w; - if(dx > 0) - w -= dx; - - dy = clip->y - dstRect->y; - if(dy > 0) - { - h -= dy; - dstRect->y += dy; - srcy += dy; - } - dy = dstRect->y + h - clip->y - clip->h; - if(dy > 0) - h -= dy; - } - - if(w > 0 && h > 0) - { - dstRect->w = w; - dstRect->h = h; - - if(SDL_LockSurface(dst)) - return -1; //if we cannot lock the surface - - const SDL_Color *colors = src->format->palette->colors; - uint8_t *colory = (uint8_t*)src->pixels + srcy*src->pitch + srcx; - uint8_t *py = (uint8_t*)dst->pixels + dstRect->y*dst->pitch + dstRect->x*bpp; - - for(int y=h; y; y--, colory+=src->pitch, py+=dst->pitch) - { - uint8_t *color = colory; - uint8_t *p = py; - - for(int x = w; x; x--) - { - const SDL_Color &tbc = colors[*color++]; //color to blit - ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); - } - } - SDL_UnlockSurface(dst); - } - } - return 0; -} - -int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint) -{ - switch(dst->format->BytesPerPixel) - { - case 2: return blit8bppAlphaTo24bppT<2>(src, srcRect, dst, dstPoint); - case 3: return blit8bppAlphaTo24bppT<3>(src, srcRect, dst, dstPoint); - case 4: return blit8bppAlphaTo24bppT<4>(src, srcRect, dst, dstPoint); - default: - logGlobal->error("%d bpp is not supported!", (int)dst->format->BitsPerPixel); - return -1; - } -} - -uint32_t CSDL_Ext::colorTouint32_t(const SDL_Color * color) -{ - uint32_t ret = 0; - ret+=color->a; - ret<<=8; //*=256 - ret+=color->b; - ret<<=8; //*=256 - ret+=color->g; - ret<<=8; //*=256 - ret+=color->r; - return ret; -} - -static void drawLineXDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) -{ - double length(x2 - x1); - - for(int x = x1; x <= x2; x++) - { - double f = (x - x1) / length; - int y = vstd::lerp(y1, y2, f); - - if (std::abs(x - x1) % 5 != 4) - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); - } -} - -static void drawLineYDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) -{ - double length(y2 - y1); - - for(int y = y1; y <= y2; y++) - { - double f = (y - y1) / length; - int x = vstd::lerp(x1, x2, f); - - if (std::abs(y - y1) % 5 != 4) - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); - } -} - -static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) -{ - double length(x2 - x1); - for(int x = x1; x <= x2; x++) - { - double f = (x - x1) / length; - int y = vstd::lerp(y1, y2, f); - - uint8_t r = vstd::lerp(color1.r, color2.r, f); - uint8_t g = vstd::lerp(color1.g, color2.g, f); - uint8_t b = vstd::lerp(color1.b, color2.b, f); - uint8_t a = vstd::lerp(color1.a, color2.a, f); - - uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); - ColorPutter<4, 0>::PutColor(p, r,g,b,a); - } -} - -static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) -{ - double length(y2 - y1); - for(int y = y1; y <= y2; y++) - { - double f = (y - y1) / length; - int x = vstd::lerp(x1, x2, f); - - uint8_t r = vstd::lerp(color1.r, color2.r, f); - uint8_t g = vstd::lerp(color1.g, color2.g, f); - uint8_t b = vstd::lerp(color1.b, color2.b, f); - uint8_t a = vstd::lerp(color1.a, color2.a, f); - - uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); - ColorPutter<4, 0>::PutColor(p, r,g,b,a); - } -} - -void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2) -{ - //FIXME: duplicated code with drawLineDashed - int width = std::abs(from.x - dest.x); - int height = std::abs(from.y - dest.y); - - if ( width == 0 && height == 0) - { - uint8_t *p = CSDL_Ext::getPxPtr(sur, from.x, from.y); - ColorPutter<4, 0>::PutColorAlpha(p, color1); - return; - } - - if (width > height) - { - if ( from.x < dest.x) - drawLineX(sur, from.x, from.y, dest.x, dest.y, color1, color2); - else - drawLineX(sur, dest.x, dest.y, from.x, from.y, color2, color1); - } - else - { - if ( from.y < dest.y) - drawLineY(sur, from.x, from.y, dest.x, dest.y, color1, color2); - else - drawLineY(sur, dest.x, dest.y, from.x, from.y, color2, color1); - } -} - -void CSDL_Ext::drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color) -{ - //FIXME: duplicated code with drawLine - int width = std::abs(from.x - dest.x); - int height = std::abs(from.y - dest.y); - - if ( width == 0 && height == 0) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, from.x, from.y, color.r, color.g, color.b); - return; - } - - if (width > height) - { - if ( from.x < dest.x) - drawLineXDashed(sur, from.x, from.y, dest.x, dest.y, color); - else - drawLineXDashed(sur, dest.x, dest.y, from.x, from.y, color); - } - else - { - if ( from.y < dest.y) - drawLineYDashed(sur, from.x, from.y, dest.x, dest.y, color); - else - drawLineYDashed(sur, dest.x, dest.y, from.x, from.y, color); - } -} - -void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color &color, int depth) -{ - depth = std::max(1, depth); - for(int depthIterator = 0; depthIterator < depth; depthIterator++) - { - for(int i = 0; i < w; i++) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+depthIterator,color.r,color.g,color.b); - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+h-1-depthIterator,color.r,color.g,color.b); - } - for(int i = 0; i < h; i++) - { - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+depthIterator,y+i,color.r,color.g,color.b); - CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+w-1-depthIterator,y+i,color.r,color.g,color.b); - } - } -} - -void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &color, int depth) -{ - drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); -} - -void CSDL_Ext::setPlayerColor(SDL_Surface * sur, const PlayerColor & player) -{ - if(player==PlayerColor::UNFLAGGABLE) - return; - if(sur->format->BitsPerPixel==8) - { - ColorRGBA color = (player == PlayerColor::NEUTRAL - ? graphics->neutralColor - : graphics->playerColors[player.getNum()]); - - SDL_Color colorSDL = toSDL(color); - CSDL_Ext::setColors(sur, &colorSDL, 5, 1); - } - else - logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); -} - -CSDL_Ext::TColorPutter CSDL_Ext::getPutterFor(SDL_Surface * const &dest, int incrementing) -{ -#define CASE_BPP(BytesPerPixel) \ -case BytesPerPixel: \ - if(incrementing > 0) \ - return ColorPutter::PutColor; \ - else if(incrementing == 0) \ - return ColorPutter::PutColor; \ - else \ - return ColorPutter::PutColor;\ - break; - - switch(dest->format->BytesPerPixel) - { - CASE_BPP(2) - CASE_BPP(3) - CASE_BPP(4) - default: - logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); - return nullptr; - } - -} - -CSDL_Ext::TColorPutterAlpha CSDL_Ext::getPutterAlphaFor(SDL_Surface * const &dest, int incrementing) -{ - switch(dest->format->BytesPerPixel) - { - CASE_BPP(2) - CASE_BPP(3) - CASE_BPP(4) - default: - logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); - return nullptr; - } -#undef CASE_BPP -} - -uint8_t * CSDL_Ext::getPxPtr(const SDL_Surface * const &srf, const int x, const int y) -{ - return (uint8_t *)srf->pixels + y * srf->pitch + x * srf->format->BytesPerPixel; -} - -bool CSDL_Ext::isTransparent( SDL_Surface * srf, const Point & position ) -{ - return isTransparent(srf, position.x, position.y); -} - -bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) -{ - if (x < 0 || y < 0 || x >= srf->w || y >= srf->h) - return true; - - SDL_Color color; - - SDL_GetRGBA(CSDL_Ext::getPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); - - bool pixelTransparent = color.a < 128; - bool pixelCyan = (color.r == 0 && color.g == 255 && color.b == 255); - - return pixelTransparent || pixelCyan; -} - -void CSDL_Ext::putPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) -{ - uint8_t *p = getPxPtr(ekran, x, y); - getPutterFor(ekran, false)(p, R, G, B); - - switch(ekran->format->BytesPerPixel) - { - case 2: Channels::px<2>::a.set(p, A); break; - case 3: Channels::px<3>::a.set(p, A); break; - case 4: Channels::px<4>::a.set(p, A); break; - } -} - -void CSDL_Ext::putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) -{ - const SDL_Rect & rect = ekran->clip_rect; - - if(x >= rect.x && x < rect.w + rect.x - && y >= rect.y && y < rect.h + rect.y) - CSDL_Ext::putPixelWithoutRefresh(ekran, x, y, R, G, B, A); -} - -template -void CSDL_Ext::convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect ) -{ - uint8_t * pixels = static_cast(surf->pixels); - - for(int yp = rect.top(); yp < rect.bottom(); ++yp) - { - uint8_t * pixel_from = pixels + yp * surf->pitch + rect.left() * surf->format->BytesPerPixel; - uint8_t * pixel_dest = pixels + yp * surf->pitch + rect.right() * surf->format->BytesPerPixel; - - for (uint8_t * pixel = pixel_from; pixel < pixel_dest; pixel += surf->format->BytesPerPixel) - { - int r = Channels::px::r.get(pixel); - int g = Channels::px::g.get(pixel); - int b = Channels::px::b.get(pixel); - - int gray = static_cast(0.299 * r + 0.587 * g + 0.114 *b); - - Channels::px::r.set(pixel, gray); - Channels::px::g.set(pixel, gray); - Channels::px::b.set(pixel, gray); - } - } -} - -void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect ) -{ - switch(surf->format->BytesPerPixel) - { - case 2: convertToGrayscaleBpp<2>(surf, rect); break; - case 3: convertToGrayscaleBpp<3>(surf, rect); break; - case 4: convertToGrayscaleBpp<4>(surf, rect); break; - } -} - -template -void scaleSurfaceFastInternal(SDL_Surface *surf, SDL_Surface *ret) -{ - const float factorX = float(surf->w) / float(ret->w), - factorY = float(surf->h) / float(ret->h); - - for(int y = 0; y < ret->h; y++) - { - for(int x = 0; x < ret->w; x++) - { - //coordinates we want to calculate - int origX = static_cast(floor(factorX * x)), - origY = static_cast(floor(factorY * y)); - - // Get pointers to source pixels - uint8_t *srcPtr = (uint8_t*)surf->pixels + origY * surf->pitch + origX * bpp; - uint8_t *destPtr = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; - - memcpy(destPtr, srcPtr, bpp); - } - } -} - -SDL_Surface * CSDL_Ext::scaleSurfaceFast(SDL_Surface *surf, int width, int height) -{ - if (!surf || !width || !height) - return nullptr; - - //Same size? return copy - this should more be faster - if (width == surf->w && height == surf->h) - return copySurface(surf); - - SDL_Surface *ret = newSurface(width, height, surf); - - switch(surf->format->BytesPerPixel) - { - case 1: scaleSurfaceFastInternal<1>(surf, ret); break; - case 2: scaleSurfaceFastInternal<2>(surf, ret); break; - case 3: scaleSurfaceFastInternal<3>(surf, ret); break; - case 4: scaleSurfaceFastInternal<4>(surf, ret); break; - } - return ret; -} - -template -void scaleSurfaceInternal(SDL_Surface *surf, SDL_Surface *ret) -{ - const float factorX = float(surf->w - 1) / float(ret->w), - factorY = float(surf->h - 1) / float(ret->h); - - for(int y = 0; y < ret->h; y++) - { - for(int x = 0; x < ret->w; x++) - { - //coordinates we want to interpolate - float origX = factorX * x, - origY = factorY * y; - - float x1 = floor(origX), x2 = floor(origX+1), - y1 = floor(origY), y2 = floor(origY+1); - //assert( x1 >= 0 && y1 >= 0 && x2 < surf->w && y2 < surf->h);//All pixels are in range - - // Calculate weights of each source pixel - float w11 = ((origX - x1) * (origY - y1)); - float w12 = ((origX - x1) * (y2 - origY)); - float w21 = ((x2 - origX) * (origY - y1)); - float w22 = ((x2 - origX) * (y2 - origY)); - //assert( w11 + w12 + w21 + w22 > 0.99 && w11 + w12 + w21 + w22 < 1.01);//total weight is ~1.0 - - // Get pointers to source pixels - uint8_t *p11 = (uint8_t*)surf->pixels + int(y1) * surf->pitch + int(x1) * bpp; - uint8_t *p12 = p11 + bpp; - uint8_t *p21 = p11 + surf->pitch; - uint8_t *p22 = p21 + bpp; - // Calculate resulting channels -#define PX(X, PTR) Channels::px::X.get(PTR) - int resR = static_cast(PX(r, p11) * w11 + PX(r, p12) * w12 + PX(r, p21) * w21 + PX(r, p22) * w22); - int resG = static_cast(PX(g, p11) * w11 + PX(g, p12) * w12 + PX(g, p21) * w21 + PX(g, p22) * w22); - int resB = static_cast(PX(b, p11) * w11 + PX(b, p12) * w12 + PX(b, p21) * w21 + PX(b, p22) * w22); - int resA = static_cast(PX(a, p11) * w11 + PX(a, p12) * w12 + PX(a, p21) * w21 + PX(a, p22) * w22); - //assert(resR < 256 && resG < 256 && resB < 256 && resA < 256); -#undef PX - uint8_t *dest = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; - Channels::px::r.set(dest, resR); - Channels::px::g.set(dest, resG); - Channels::px::b.set(dest, resB); - Channels::px::a.set(dest, resA); - } - } -} - -// scaling via bilinear interpolation algorithm. -// NOTE: best results are for scaling in range 50%...200%. -// And upscaling looks awful right now - should be fixed somehow -SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface *surf, int width, int height) -{ - if (!surf || !width || !height) - return nullptr; - - if (surf->format->palette) - return scaleSurfaceFast(surf, width, height); - - //Same size? return copy - this should more be faster - if (width == surf->w && height == surf->h) - return copySurface(surf); - - SDL_Surface *ret = newSurface(width, height, surf); - - switch(surf->format->BytesPerPixel) - { - case 2: scaleSurfaceInternal<2>(surf, ret); break; - case 3: scaleSurfaceInternal<3>(surf, ret); break; - case 4: scaleSurfaceInternal<4>(surf, ret); break; - } - - return ret; -} - -void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint) -{ - SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput); - SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(dstPoint, srcRectInput.dimensions())); - - int result = SDL_UpperBlit(src, &srcRect, dst, &dstRect); - - if (result != 0) - logGlobal->error("SDL_UpperBlit failed! %s", SDL_GetError()); -} - -void CSDL_Ext::blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest) -{ - Rect allSurface( Point(0,0), Point(src->w, src->h)); - - blitSurface(src, allSurface, dst, dest); -} - -void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) -{ - Rect allSurface( Point(0,0), Point(dst->w, dst->h)); - - fillRect(dst, allSurface, color); -} - -void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) -{ - SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); - - uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - SDL_FillRect(dst, &newRect, sdlColor); -} - -void CSDL_Ext::fillRectBlended( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) -{ - SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); - uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); - - SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); - SDL_FillRect(tmp, NULL, sdlColor); - SDL_BlitSurface(tmp, NULL, dst, &newRect); - SDL_FreeSurface(tmp); -} - -STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) -{ - return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); -} - -void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) -{ - uint32_t key = mapColor(surface,color); - SDL_SetColorKey(surface, SDL_TRUE, key); -} - -void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) -{ - setColorKey(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); -} - -void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) -{ - uint32_t key = mapColor(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); - auto & color = surface->format->palette->colors[key]; - - // set color key only if exactly such color was found - if (color.r == Colors::DEFAULT_KEY_COLOR.r && color.g == Colors::DEFAULT_KEY_COLOR.g && color.b == Colors::DEFAULT_KEY_COLOR.b) - { - SDL_SetColorKey(surface, SDL_TRUE, key); - color.a = SDL_ALPHA_TRANSPARENT; - } -} - -void CSDL_Ext::setClipRect(SDL_Surface * src, const Rect & other) -{ - SDL_Rect rect = CSDL_Ext::toSDL(other); - - SDL_SetClipRect(src, &rect); -} - -void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) -{ - SDL_Rect rect; - - SDL_GetClipRect(src, &rect); - - other = CSDL_Ext::fromSDL(rect); -} - -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); -template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); - +/* + * SDL_Extensions.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 "SDL_Extensions.h" + +#include "SDL_PixelAccess.h" + +#include "../render/Graphics.h" +#include "../render/Colors.h" +#include "../CMT.h" + +#include "../../lib/GameConstants.h" + +#include + +Rect CSDL_Ext::fromSDL(const SDL_Rect & rect) +{ + return Rect(Point(rect.x, rect.y), Point(rect.w, rect.h)); +} + +SDL_Rect CSDL_Ext::toSDL(const Rect & rect) +{ + SDL_Rect result; + result.x = rect.x; + result.y = rect.y; + result.w = rect.w; + result.h = rect.h; + return result; +} + +ColorRGBA CSDL_Ext::fromSDL(const SDL_Color & color) +{ + return { color.r, color.g, color.b, color.a }; +} + +SDL_Color CSDL_Ext::toSDL(const ColorRGBA & color) +{ + SDL_Color result; + result.r = color.r; + result.g = color.g; + result.b = color.b; + result.a = color.a; + + return result; +} + +void CSDL_Ext::setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors) +{ + SDL_SetPaletteColors(surface->format->palette,colors,firstcolor,ncolors); +} + +void CSDL_Ext::setAlpha(SDL_Surface * bg, int value) +{ + SDL_SetSurfaceAlphaMod(bg, value); +} + +void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) +{ + SDL_Rect rectSDL = CSDL_Ext::toSDL(rect); + if(0 !=SDL_UpdateTexture(screenTexture, &rectSDL, surface->pixels, surface->pitch)) + logGlobal->error("%sSDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); + + SDL_RenderClear(mainRenderer); + if(0 != SDL_RenderCopy(mainRenderer, screenTexture, NULL, NULL)) + logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); + SDL_RenderPresent(mainRenderer); + +} + +SDL_Surface * CSDL_Ext::newSurface(int w, int h) +{ + return newSurface(w, h, screen); +} + +SDL_Surface * CSDL_Ext::newSurface(int w, int h, SDL_Surface * mod) //creates new surface, with flags/format same as in surface given +{ + SDL_Surface * ret = SDL_CreateRGBSurface(0,w,h,mod->format->BitsPerPixel,mod->format->Rmask,mod->format->Gmask,mod->format->Bmask,mod->format->Amask); + + if(ret == nullptr) + { + const char * error = SDL_GetError(); + + std::string messagePattern = "Failed to create SDL Surface of size %d x %d, %d bpp. Reason: %s"; + std::string message = boost::str(boost::format(messagePattern) % w % h % mod->format->BitsPerPixel % error); + + handleFatalError(message, true); + } + + if (mod->format->palette) + { + assert(ret->format->palette); + assert(ret->format->palette->ncolors == mod->format->palette->ncolors); + memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color)); + } + return ret; +} + +SDL_Surface * CSDL_Ext::copySurface(SDL_Surface * mod) //returns copy of given surface +{ + //return SDL_DisplayFormat(mod); + return SDL_ConvertSurface(mod, mod->format, mod->flags); +} + +template +SDL_Surface * CSDL_Ext::createSurfaceWithBpp(int width, int height) +{ + uint32_t rMask = 0, gMask = 0, bMask = 0, aMask = 0; + + Channels::px::r.set((uint8_t*)&rMask, 255); + Channels::px::g.set((uint8_t*)&gMask, 255); + Channels::px::b.set((uint8_t*)&bMask, 255); + Channels::px::a.set((uint8_t*)&aMask, 255); + + return SDL_CreateRGBSurface(0, width, height, bpp * 8, rMask, gMask, bMask, aMask); +} + +void CSDL_Ext::blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst) +{ + CSDL_Ext::blitSurface(src, dst, Point(x, y)); +} + +void CSDL_Ext::blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst) +{ + if (src) + blitAt(src,pos.x,pos.y,dst); +} + +// Vertical flip +SDL_Surface * CSDL_Ext::verticalFlip(SDL_Surface * toRot) +{ + SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); + + SDL_LockSurface(ret); + SDL_LockSurface(toRot); + + const int bpp = ret->format->BytesPerPixel; + + char * src = reinterpret_cast(toRot->pixels); + char * dst = reinterpret_cast(ret->pixels); + + for(int i=0; ih; i++) + { + //FIXME: optimization bugged +// if (bpp == 1) +// { +// // much faster for 8-bit surfaces (majority of our data) +// std::reverse_copy(src, src + toRot->pitch, dst); +// } +// else +// { + char * srcPxl = src; + char * dstPxl = dst + ret->w * bpp; + + for(int j=0; jw; j++) + { + dstPxl -= bpp; + std::copy(srcPxl, srcPxl + bpp, dstPxl); + srcPxl += bpp; + } +// } + src += toRot->pitch; + dst += ret->pitch; + } + SDL_UnlockSurface(ret); + SDL_UnlockSurface(toRot); + return ret; +} + +// Horizontal flip +SDL_Surface * CSDL_Ext::horizontalFlip(SDL_Surface * toRot) +{ + SDL_Surface * ret = SDL_ConvertSurface(toRot, toRot->format, toRot->flags); + SDL_LockSurface(ret); + SDL_LockSurface(toRot); + char * src = reinterpret_cast(toRot->pixels); + char * dst = reinterpret_cast(ret->pixels) + ret->h * ret->pitch; + + for(int i=0; ih; i++) + { + dst -= ret->pitch; + std::copy(src, src + toRot->pitch, dst); + src += toRot->pitch; + } + SDL_UnlockSurface(ret); + SDL_UnlockSurface(toRot); + return ret; +} + +uint32_t CSDL_Ext::getPixel(SDL_Surface *surface, const int & x, const int & y, bool colorByte) +{ + int bpp = surface->format->BytesPerPixel; + /* Here p is the address to the pixel we want to retrieve */ + uint8_t *p = (uint8_t *)surface->pixels + y * surface->pitch + x * bpp; + + switch(bpp) + { + case 1: + if(colorByte) + return colorTouint32_t(surface->format->palette->colors+(*p)); + else + return *p; + + case 2: + return *(uint16_t *)p; + + case 3: + return p[0] | p[1] << 8 | p[2] << 16; + + case 4: + return *(uint32_t *)p; + + default: + return 0; // shouldn't happen, but avoids warnings + } +} + +template +int CSDL_Ext::blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPointInput) +{ + SDL_Rect srcRectInstance = CSDL_Ext::toSDL(srcRectInput); + SDL_Rect dstRectInstance = CSDL_Ext::toSDL(Rect(dstPointInput, srcRectInput.dimensions())); + + SDL_Rect * srcRect =&srcRectInstance; + SDL_Rect * dstRect =&dstRectInstance; + + /* Make sure the surfaces aren't locked */ + if ( ! src || ! dst ) + { + SDL_SetError("SDL_UpperBlit: passed a nullptr surface"); + return -1; + } + + if ( src->locked || dst->locked ) + { + SDL_SetError("Surfaces must not be locked during blit"); + return -1; + } + + if (src->format->BytesPerPixel==1 && (bpp==3 || bpp==4 || bpp==2)) //everything's ok + { + SDL_Rect fulldst; + int srcx, srcy, w, h; + + /* If the destination rectangle is nullptr, use the entire dest surface */ + if ( dstRect == nullptr ) + { + fulldst.x = fulldst.y = 0; + dstRect = &fulldst; + } + + /* clip the source rectangle to the source surface */ + if(srcRect) + { + int maxw, maxh; + + srcx = srcRect->x; + w = srcRect->w; + if(srcx < 0) + { + w += srcx; + dstRect->x -= srcx; + srcx = 0; + } + maxw = src->w - srcx; + if(maxw < w) + w = maxw; + + srcy = srcRect->y; + h = srcRect->h; + if(srcy < 0) + { + h += srcy; + dstRect->y -= srcy; + srcy = 0; + } + maxh = src->h - srcy; + if(maxh < h) + h = maxh; + + } + else + { + srcx = srcy = 0; + w = src->w; + h = src->h; + } + + /* clip the destination rectangle against the clip rectangle */ + { + SDL_Rect *clip = &dst->clip_rect; + int dx, dy; + + dx = clip->x - dstRect->x; + if(dx > 0) + { + w -= dx; + dstRect->x += dx; + srcx += dx; + } + dx = dstRect->x + w - clip->x - clip->w; + if(dx > 0) + w -= dx; + + dy = clip->y - dstRect->y; + if(dy > 0) + { + h -= dy; + dstRect->y += dy; + srcy += dy; + } + dy = dstRect->y + h - clip->y - clip->h; + if(dy > 0) + h -= dy; + } + + if(w > 0 && h > 0) + { + dstRect->w = w; + dstRect->h = h; + + if(SDL_LockSurface(dst)) + return -1; //if we cannot lock the surface + + const SDL_Color *colors = src->format->palette->colors; + uint8_t *colory = (uint8_t*)src->pixels + srcy*src->pitch + srcx; + uint8_t *py = (uint8_t*)dst->pixels + dstRect->y*dst->pitch + dstRect->x*bpp; + + for(int y=h; y; y--, colory+=src->pitch, py+=dst->pitch) + { + uint8_t *color = colory; + uint8_t *p = py; + + for(int x = w; x; x--) + { + const SDL_Color &tbc = colors[*color++]; //color to blit + ColorPutter::PutColorAlphaSwitch(p, tbc.r, tbc.g, tbc.b, tbc.a); + } + } + SDL_UnlockSurface(dst); + } + } + return 0; +} + +int CSDL_Ext::blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint) +{ + switch(dst->format->BytesPerPixel) + { + case 2: return blit8bppAlphaTo24bppT<2>(src, srcRect, dst, dstPoint); + case 3: return blit8bppAlphaTo24bppT<3>(src, srcRect, dst, dstPoint); + case 4: return blit8bppAlphaTo24bppT<4>(src, srcRect, dst, dstPoint); + default: + logGlobal->error("%d bpp is not supported!", (int)dst->format->BitsPerPixel); + return -1; + } +} + +uint32_t CSDL_Ext::colorTouint32_t(const SDL_Color * color) +{ + uint32_t ret = 0; + ret+=color->a; + ret<<=8; //*=256 + ret+=color->b; + ret<<=8; //*=256 + ret+=color->g; + ret<<=8; //*=256 + ret+=color->r; + return ret; +} + +static void drawLineXDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) +{ + double length(x2 - x1); + + for(int x = x1; x <= x2; x++) + { + double f = (x - x1) / length; + int y = vstd::lerp(y1, y2, f); + + if (std::abs(x - x1) % 5 != 4) + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); + } +} + +static void drawLineYDashed(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color) +{ + double length(y2 - y1); + + for(int y = y1; y <= y2; y++) + { + double f = (y - y1) / length; + int x = vstd::lerp(x1, x2, f); + + if (std::abs(y - y1) % 5 != 4) + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, x, y, color.r, color.g, color.b); + } +} + +static void drawLineX(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) +{ + double length(x2 - x1); + for(int x = x1; x <= x2; x++) + { + double f = (x - x1) / length; + int y = vstd::lerp(y1, y2, f); + + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + uint8_t a = vstd::lerp(color1.a, color2.a, f); + + uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); + ColorPutter<4, 0>::PutColor(p, r,g,b,a); + } +} + +static void drawLineY(SDL_Surface * sur, int x1, int y1, int x2, int y2, const SDL_Color & color1, const SDL_Color & color2) +{ + double length(y2 - y1); + for(int y = y1; y <= y2; y++) + { + double f = (y - y1) / length; + int x = vstd::lerp(x1, x2, f); + + uint8_t r = vstd::lerp(color1.r, color2.r, f); + uint8_t g = vstd::lerp(color1.g, color2.g, f); + uint8_t b = vstd::lerp(color1.b, color2.b, f); + uint8_t a = vstd::lerp(color1.a, color2.a, f); + + uint8_t *p = CSDL_Ext::getPxPtr(sur, x, y); + ColorPutter<4, 0>::PutColor(p, r,g,b,a); + } +} + +void CSDL_Ext::drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2) +{ + //FIXME: duplicated code with drawLineDashed + int width = std::abs(from.x - dest.x); + int height = std::abs(from.y - dest.y); + + if ( width == 0 && height == 0) + { + uint8_t *p = CSDL_Ext::getPxPtr(sur, from.x, from.y); + ColorPutter<4, 0>::PutColorAlpha(p, color1); + return; + } + + if (width > height) + { + if ( from.x < dest.x) + drawLineX(sur, from.x, from.y, dest.x, dest.y, color1, color2); + else + drawLineX(sur, dest.x, dest.y, from.x, from.y, color2, color1); + } + else + { + if ( from.y < dest.y) + drawLineY(sur, from.x, from.y, dest.x, dest.y, color1, color2); + else + drawLineY(sur, dest.x, dest.y, from.x, from.y, color2, color1); + } +} + +void CSDL_Ext::drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color) +{ + //FIXME: duplicated code with drawLine + int width = std::abs(from.x - dest.x); + int height = std::abs(from.y - dest.y); + + if ( width == 0 && height == 0) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur, from.x, from.y, color.r, color.g, color.b); + return; + } + + if (width > height) + { + if ( from.x < dest.x) + drawLineXDashed(sur, from.x, from.y, dest.x, dest.y, color); + else + drawLineXDashed(sur, dest.x, dest.y, from.x, from.y, color); + } + else + { + if ( from.y < dest.y) + drawLineYDashed(sur, from.x, from.y, dest.x, dest.y, color); + else + drawLineYDashed(sur, dest.x, dest.y, from.x, from.y, color); + } +} + +void CSDL_Ext::drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color &color, int depth) +{ + depth = std::max(1, depth); + for(int depthIterator = 0; depthIterator < depth; depthIterator++) + { + for(int i = 0; i < w; i++) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+depthIterator,color.r,color.g,color.b); + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+i,y+h-1-depthIterator,color.r,color.g,color.b); + } + for(int i = 0; i < h; i++) + { + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+depthIterator,y+i,color.r,color.g,color.b); + CSDL_Ext::putPixelWithoutRefreshIfInSurf(sur,x+w-1-depthIterator,y+i,color.r,color.g,color.b); + } + } +} + +void CSDL_Ext::drawBorder( SDL_Surface * sur, const Rect &r, const SDL_Color &color, int depth) +{ + drawBorder(sur, r.x, r.y, r.w, r.h, color, depth); +} + +void CSDL_Ext::setPlayerColor(SDL_Surface * sur, const PlayerColor & player) +{ + if(player==PlayerColor::UNFLAGGABLE) + return; + if(sur->format->BitsPerPixel==8) + { + ColorRGBA color = (player == PlayerColor::NEUTRAL + ? graphics->neutralColor + : graphics->playerColors[player.getNum()]); + + SDL_Color colorSDL = toSDL(color); + CSDL_Ext::setColors(sur, &colorSDL, 5, 1); + } + else + logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); +} + +CSDL_Ext::TColorPutter CSDL_Ext::getPutterFor(SDL_Surface * const &dest, int incrementing) +{ +#define CASE_BPP(BytesPerPixel) \ +case BytesPerPixel: \ + if(incrementing > 0) \ + return ColorPutter::PutColor; \ + else if(incrementing == 0) \ + return ColorPutter::PutColor; \ + else \ + return ColorPutter::PutColor;\ + break; + + switch(dest->format->BytesPerPixel) + { + CASE_BPP(2) + CASE_BPP(3) + CASE_BPP(4) + default: + logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); + return nullptr; + } + +} + +CSDL_Ext::TColorPutterAlpha CSDL_Ext::getPutterAlphaFor(SDL_Surface * const &dest, int incrementing) +{ + switch(dest->format->BytesPerPixel) + { + CASE_BPP(2) + CASE_BPP(3) + CASE_BPP(4) + default: + logGlobal->error("%d bpp is not supported!", (int)dest->format->BitsPerPixel); + return nullptr; + } +#undef CASE_BPP +} + +uint8_t * CSDL_Ext::getPxPtr(const SDL_Surface * const &srf, const int x, const int y) +{ + return (uint8_t *)srf->pixels + y * srf->pitch + x * srf->format->BytesPerPixel; +} + +bool CSDL_Ext::isTransparent( SDL_Surface * srf, const Point & position ) +{ + return isTransparent(srf, position.x, position.y); +} + +bool CSDL_Ext::isTransparent( SDL_Surface * srf, int x, int y ) +{ + if (x < 0 || y < 0 || x >= srf->w || y >= srf->h) + return true; + + SDL_Color color; + + SDL_GetRGBA(CSDL_Ext::getPixel(srf, x, y), srf->format, &color.r, &color.g, &color.b, &color.a); + + bool pixelTransparent = color.a < 128; + bool pixelCyan = (color.r == 0 && color.g == 255 && color.b == 255); + + return pixelTransparent || pixelCyan; +} + +void CSDL_Ext::putPixelWithoutRefresh(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) +{ + uint8_t *p = getPxPtr(ekran, x, y); + getPutterFor(ekran, false)(p, R, G, B); + + switch(ekran->format->BytesPerPixel) + { + case 2: Channels::px<2>::a.set(p, A); break; + case 3: Channels::px<3>::a.set(p, A); break; + case 4: Channels::px<4>::a.set(p, A); break; + } +} + +void CSDL_Ext::putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A) +{ + const SDL_Rect & rect = ekran->clip_rect; + + if(x >= rect.x && x < rect.w + rect.x + && y >= rect.y && y < rect.h + rect.y) + CSDL_Ext::putPixelWithoutRefresh(ekran, x, y, R, G, B, A); +} + +template +void CSDL_Ext::convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect ) +{ + uint8_t * pixels = static_cast(surf->pixels); + + for(int yp = rect.top(); yp < rect.bottom(); ++yp) + { + uint8_t * pixel_from = pixels + yp * surf->pitch + rect.left() * surf->format->BytesPerPixel; + uint8_t * pixel_dest = pixels + yp * surf->pitch + rect.right() * surf->format->BytesPerPixel; + + for (uint8_t * pixel = pixel_from; pixel < pixel_dest; pixel += surf->format->BytesPerPixel) + { + int r = Channels::px::r.get(pixel); + int g = Channels::px::g.get(pixel); + int b = Channels::px::b.get(pixel); + + int gray = static_cast(0.299 * r + 0.587 * g + 0.114 *b); + + Channels::px::r.set(pixel, gray); + Channels::px::g.set(pixel, gray); + Channels::px::b.set(pixel, gray); + } + } +} + +void CSDL_Ext::convertToGrayscale( SDL_Surface * surf, const Rect & rect ) +{ + switch(surf->format->BytesPerPixel) + { + case 2: convertToGrayscaleBpp<2>(surf, rect); break; + case 3: convertToGrayscaleBpp<3>(surf, rect); break; + case 4: convertToGrayscaleBpp<4>(surf, rect); break; + } +} + +template +void scaleSurfaceFastInternal(SDL_Surface *surf, SDL_Surface *ret) +{ + const float factorX = float(surf->w) / float(ret->w), + factorY = float(surf->h) / float(ret->h); + + for(int y = 0; y < ret->h; y++) + { + for(int x = 0; x < ret->w; x++) + { + //coordinates we want to calculate + int origX = static_cast(floor(factorX * x)), + origY = static_cast(floor(factorY * y)); + + // Get pointers to source pixels + uint8_t *srcPtr = (uint8_t*)surf->pixels + origY * surf->pitch + origX * bpp; + uint8_t *destPtr = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; + + memcpy(destPtr, srcPtr, bpp); + } + } +} + +SDL_Surface * CSDL_Ext::scaleSurfaceFast(SDL_Surface *surf, int width, int height) +{ + if (!surf || !width || !height) + return nullptr; + + //Same size? return copy - this should more be faster + if (width == surf->w && height == surf->h) + return copySurface(surf); + + SDL_Surface *ret = newSurface(width, height, surf); + + switch(surf->format->BytesPerPixel) + { + case 1: scaleSurfaceFastInternal<1>(surf, ret); break; + case 2: scaleSurfaceFastInternal<2>(surf, ret); break; + case 3: scaleSurfaceFastInternal<3>(surf, ret); break; + case 4: scaleSurfaceFastInternal<4>(surf, ret); break; + } + return ret; +} + +template +void scaleSurfaceInternal(SDL_Surface *surf, SDL_Surface *ret) +{ + const float factorX = float(surf->w - 1) / float(ret->w), + factorY = float(surf->h - 1) / float(ret->h); + + for(int y = 0; y < ret->h; y++) + { + for(int x = 0; x < ret->w; x++) + { + //coordinates we want to interpolate + float origX = factorX * x, + origY = factorY * y; + + float x1 = floor(origX), x2 = floor(origX+1), + y1 = floor(origY), y2 = floor(origY+1); + //assert( x1 >= 0 && y1 >= 0 && x2 < surf->w && y2 < surf->h);//All pixels are in range + + // Calculate weights of each source pixel + float w11 = ((origX - x1) * (origY - y1)); + float w12 = ((origX - x1) * (y2 - origY)); + float w21 = ((x2 - origX) * (origY - y1)); + float w22 = ((x2 - origX) * (y2 - origY)); + //assert( w11 + w12 + w21 + w22 > 0.99 && w11 + w12 + w21 + w22 < 1.01);//total weight is ~1.0 + + // Get pointers to source pixels + uint8_t *p11 = (uint8_t*)surf->pixels + int(y1) * surf->pitch + int(x1) * bpp; + uint8_t *p12 = p11 + bpp; + uint8_t *p21 = p11 + surf->pitch; + uint8_t *p22 = p21 + bpp; + // Calculate resulting channels +#define PX(X, PTR) Channels::px::X.get(PTR) + int resR = static_cast(PX(r, p11) * w11 + PX(r, p12) * w12 + PX(r, p21) * w21 + PX(r, p22) * w22); + int resG = static_cast(PX(g, p11) * w11 + PX(g, p12) * w12 + PX(g, p21) * w21 + PX(g, p22) * w22); + int resB = static_cast(PX(b, p11) * w11 + PX(b, p12) * w12 + PX(b, p21) * w21 + PX(b, p22) * w22); + int resA = static_cast(PX(a, p11) * w11 + PX(a, p12) * w12 + PX(a, p21) * w21 + PX(a, p22) * w22); + //assert(resR < 256 && resG < 256 && resB < 256 && resA < 256); +#undef PX + uint8_t *dest = (uint8_t*)ret->pixels + y * ret->pitch + x * bpp; + Channels::px::r.set(dest, resR); + Channels::px::g.set(dest, resG); + Channels::px::b.set(dest, resB); + Channels::px::a.set(dest, resA); + } + } +} + +// scaling via bilinear interpolation algorithm. +// NOTE: best results are for scaling in range 50%...200%. +// And upscaling looks awful right now - should be fixed somehow +SDL_Surface * CSDL_Ext::scaleSurface(SDL_Surface *surf, int width, int height) +{ + if (!surf || !width || !height) + return nullptr; + + if (surf->format->palette) + return scaleSurfaceFast(surf, width, height); + + //Same size? return copy - this should more be faster + if (width == surf->w && height == surf->h) + return copySurface(surf); + + SDL_Surface *ret = newSurface(width, height, surf); + + switch(surf->format->BytesPerPixel) + { + case 2: scaleSurfaceInternal<2>(surf, ret); break; + case 3: scaleSurfaceInternal<3>(surf, ret); break; + case 4: scaleSurfaceInternal<4>(surf, ret); break; + } + + return ret; +} + +void CSDL_Ext::blitSurface(SDL_Surface * src, const Rect & srcRectInput, SDL_Surface * dst, const Point & dstPoint) +{ + SDL_Rect srcRect = CSDL_Ext::toSDL(srcRectInput); + SDL_Rect dstRect = CSDL_Ext::toSDL(Rect(dstPoint, srcRectInput.dimensions())); + + int result = SDL_UpperBlit(src, &srcRect, dst, &dstRect); + + if (result != 0) + logGlobal->error("SDL_UpperBlit failed! %s", SDL_GetError()); +} + +void CSDL_Ext::blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest) +{ + Rect allSurface( Point(0,0), Point(src->w, src->h)); + + blitSurface(src, allSurface, dst, dest); +} + +void CSDL_Ext::fillSurface( SDL_Surface *dst, const SDL_Color & color ) +{ + Rect allSurface( Point(0,0), Point(dst->w, dst->h)); + + fillRect(dst, allSurface, color); +} + +void CSDL_Ext::fillRect( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) +{ + SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); + + uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); + SDL_FillRect(dst, &newRect, sdlColor); +} + +void CSDL_Ext::fillRectBlended( SDL_Surface *dst, const Rect & dstrect, const SDL_Color & color) +{ + SDL_Rect newRect = CSDL_Ext::toSDL(dstrect); + uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); + + SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); + SDL_FillRect(tmp, NULL, sdlColor); + SDL_BlitSurface(tmp, NULL, dst, &newRect); + SDL_FreeSurface(tmp); +} + +STRONG_INLINE static uint32_t mapColor(SDL_Surface * surface, SDL_Color color) +{ + return SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); +} + +void CSDL_Ext::setColorKey(SDL_Surface * surface, SDL_Color color) +{ + uint32_t key = mapColor(surface,color); + SDL_SetColorKey(surface, SDL_TRUE, key); +} + +void CSDL_Ext::setDefaultColorKey(SDL_Surface * surface) +{ + setColorKey(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); +} + +void CSDL_Ext::setDefaultColorKeyPresize(SDL_Surface * surface) +{ + uint32_t key = mapColor(surface, toSDL(Colors::DEFAULT_KEY_COLOR)); + auto & color = surface->format->palette->colors[key]; + + // set color key only if exactly such color was found + if (color.r == Colors::DEFAULT_KEY_COLOR.r && color.g == Colors::DEFAULT_KEY_COLOR.g && color.b == Colors::DEFAULT_KEY_COLOR.b) + { + SDL_SetColorKey(surface, SDL_TRUE, key); + color.a = SDL_ALPHA_TRANSPARENT; + } +} + +void CSDL_Ext::setClipRect(SDL_Surface * src, const Rect & other) +{ + SDL_Rect rect = CSDL_Ext::toSDL(other); + + SDL_SetClipRect(src, &rect); +} + +void CSDL_Ext::getClipRect(SDL_Surface * src, Rect & other) +{ + SDL_Rect rect; + + SDL_GetClipRect(src, &rect); + + other = CSDL_Ext::fromSDL(rect); +} + +template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<2>(int, int); +template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<3>(int, int); +template SDL_Surface * CSDL_Ext::createSurfaceWithBpp<4>(int, int); + diff --git a/client/renderSDL/SDL_Extensions.h b/client/renderSDL/SDL_Extensions.h index 712ff5c79..c85458c09 100644 --- a/client/renderSDL/SDL_Extensions.h +++ b/client/renderSDL/SDL_Extensions.h @@ -1,132 +1,132 @@ -/* - * SDL_Extensions.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" -#include "../../lib/Color.h" - -struct SDL_Rect; -struct SDL_Window; -struct SDL_Renderer; -struct SDL_Texture; -struct SDL_Surface; -struct SDL_Color; - -VCMI_LIB_NAMESPACE_BEGIN - -class PlayerColor; -class Rect; -class Point; - -VCMI_LIB_NAMESPACE_END - -namespace CSDL_Ext -{ - -/// creates Rect using provided rect -Rect fromSDL(const SDL_Rect & rect); - -/// creates SDL_Rect using provided rect -SDL_Rect toSDL(const Rect & rect); - -/// creates Color using provided SDL_Color -ColorRGBA fromSDL(const SDL_Color & color); - -/// creates SDL_Color using provided Color -SDL_Color toSDL(const ColorRGBA & color); - -void setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors); -void setAlpha(SDL_Surface * bg, int value); - -using TColorPutter = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &); -using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &, const uint8_t &); - - void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst); - void blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst); - - void setClipRect(SDL_Surface * src, const Rect & other); - void getClipRect(SDL_Surface * src, Rect & other); - - void blitSurface(SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dest); - void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); - - void fillSurface(SDL_Surface * dst, const SDL_Color & color); - void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); - void fillRectBlended(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); - - void updateRect(SDL_Surface * surface, const Rect & rect); - - void putPixelWithoutRefresh(SDL_Surface * ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); - void putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); - - SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip - SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip - uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false); - bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position - bool isTransparent(SDL_Surface * srf, const Point & position); //checks if surface is transparent at given position - - uint8_t * getPxPtr(const SDL_Surface * const & srf, const int x, const int y); - TColorPutter getPutterFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 - TColorPutterAlpha getPutterAlphaFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 - - template - int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface - int blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface - uint32_t colorTouint32_t(const SDL_Color * color); //little endian only - - void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2); - void drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color); - - void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color & color, int depth = 1); - void drawBorder(SDL_Surface * sur, const Rect & r, const SDL_Color & color, int depth = 1); - void setPlayerColor(SDL_Surface * sur, const PlayerColor & player); //sets correct color of flags; -1 for neutral - - SDL_Surface * newSurface(int w, int h, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given - SDL_Surface * newSurface(int w, int h); //creates new surface, with flags/format same as in screen surface - SDL_Surface * copySurface(SDL_Surface * mod); //returns copy of given surface - template - SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value - - //scale surface to required size. - //nearest neighbour algorithm - SDL_Surface * scaleSurfaceFast(SDL_Surface * surf, int width, int height); - // bilinear filtering. Uses fallback to scaleSurfaceFast in case of indexed surfaces - SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); - - template - void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); - void convertToGrayscale(SDL_Surface * surf, const Rect & rect); - - void setColorKey(SDL_Surface * surface, SDL_Color color); - - ///set key-color to 0,255,255 - void setDefaultColorKey(SDL_Surface * surface); - ///set key-color to 0,255,255 only if it exactly mapped - void setDefaultColorKeyPresize(SDL_Surface * surface); - - /// helper that will safely set and un-set ClipRect for SDL_Surface - class CClipRectGuard: boost::noncopyable - { - SDL_Surface * surf; - Rect oldRect; - - public: - CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface) - { - CSDL_Ext::getClipRect(surf, oldRect); - CSDL_Ext::setClipRect(surf, rect); - } - - ~CClipRectGuard() - { - CSDL_Ext::setClipRect(surf, oldRect); - } - }; -} +/* + * SDL_Extensions.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" +#include "../../lib/Color.h" + +struct SDL_Rect; +struct SDL_Window; +struct SDL_Renderer; +struct SDL_Texture; +struct SDL_Surface; +struct SDL_Color; + +VCMI_LIB_NAMESPACE_BEGIN + +class PlayerColor; +class Rect; +class Point; + +VCMI_LIB_NAMESPACE_END + +namespace CSDL_Ext +{ + +/// creates Rect using provided rect +Rect fromSDL(const SDL_Rect & rect); + +/// creates SDL_Rect using provided rect +SDL_Rect toSDL(const Rect & rect); + +/// creates Color using provided SDL_Color +ColorRGBA fromSDL(const SDL_Color & color); + +/// creates SDL_Color using provided Color +SDL_Color toSDL(const ColorRGBA & color); + +void setColors(SDL_Surface *surface, SDL_Color *colors, int firstcolor, int ncolors); +void setAlpha(SDL_Surface * bg, int value); + +using TColorPutter = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &); +using TColorPutterAlpha = void (*)(uint8_t *&, const uint8_t &, const uint8_t &, const uint8_t &, const uint8_t &); + + void blitAt(SDL_Surface * src, int x, int y, SDL_Surface * dst); + void blitAt(SDL_Surface * src, const Rect & pos, SDL_Surface * dst); + + void setClipRect(SDL_Surface * src, const Rect & other); + void getClipRect(SDL_Surface * src, Rect & other); + + void blitSurface(SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dest); + void blitSurface(SDL_Surface * src, SDL_Surface * dst, const Point & dest); + + void fillSurface(SDL_Surface * dst, const SDL_Color & color); + void fillRect(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); + void fillRectBlended(SDL_Surface * dst, const Rect & dstrect, const SDL_Color & color); + + void updateRect(SDL_Surface * surface, const Rect & rect); + + void putPixelWithoutRefresh(SDL_Surface * ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); + void putPixelWithoutRefreshIfInSurf(SDL_Surface *ekran, const int & x, const int & y, const uint8_t & R, const uint8_t & G, const uint8_t & B, uint8_t A = 255); + + SDL_Surface * verticalFlip(SDL_Surface * toRot); //vertical flip + SDL_Surface * horizontalFlip(SDL_Surface * toRot); //horizontal flip + uint32_t getPixel(SDL_Surface * surface, const int & x, const int & y, bool colorByte = false); + bool isTransparent(SDL_Surface * srf, int x, int y); //checks if surface is transparent at given position + bool isTransparent(SDL_Surface * srf, const Point & position); //checks if surface is transparent at given position + + uint8_t * getPxPtr(const SDL_Surface * const & srf, const int x, const int y); + TColorPutter getPutterFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 + TColorPutterAlpha getPutterAlphaFor(SDL_Surface * const & dest, int incrementing); //incrementing: -1, 0, 1 + + template + int blit8bppAlphaTo24bppT(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface + int blit8bppAlphaTo24bpp(const SDL_Surface * src, const Rect & srcRect, SDL_Surface * dst, const Point & dstPoint); //blits 8 bpp surface with alpha channel to 24 bpp surface + uint32_t colorTouint32_t(const SDL_Color * color); //little endian only + + void drawLine(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color1, const SDL_Color & color2); + void drawLineDashed(SDL_Surface * sur, const Point & from, const Point & dest, const SDL_Color & color); + + void drawBorder(SDL_Surface * sur, int x, int y, int w, int h, const SDL_Color & color, int depth = 1); + void drawBorder(SDL_Surface * sur, const Rect & r, const SDL_Color & color, int depth = 1); + void setPlayerColor(SDL_Surface * sur, const PlayerColor & player); //sets correct color of flags; -1 for neutral + + SDL_Surface * newSurface(int w, int h, SDL_Surface * mod); //creates new surface, with flags/format same as in surface given + SDL_Surface * newSurface(int w, int h); //creates new surface, with flags/format same as in screen surface + SDL_Surface * copySurface(SDL_Surface * mod); //returns copy of given surface + template + SDL_Surface * createSurfaceWithBpp(int width, int height); //create surface with give bits per pixels value + + //scale surface to required size. + //nearest neighbour algorithm + SDL_Surface * scaleSurfaceFast(SDL_Surface * surf, int width, int height); + // bilinear filtering. Uses fallback to scaleSurfaceFast in case of indexed surfaces + SDL_Surface * scaleSurface(SDL_Surface * surf, int width, int height); + + template + void convertToGrayscaleBpp(SDL_Surface * surf, const Rect & rect); + void convertToGrayscale(SDL_Surface * surf, const Rect & rect); + + void setColorKey(SDL_Surface * surface, SDL_Color color); + + ///set key-color to 0,255,255 + void setDefaultColorKey(SDL_Surface * surface); + ///set key-color to 0,255,255 only if it exactly mapped + void setDefaultColorKeyPresize(SDL_Surface * surface); + + /// helper that will safely set and un-set ClipRect for SDL_Surface + class CClipRectGuard: boost::noncopyable + { + SDL_Surface * surf; + Rect oldRect; + + public: + CClipRectGuard(SDL_Surface * surface, const Rect & rect): surf(surface) + { + CSDL_Ext::getClipRect(surf, oldRect); + CSDL_Ext::setClipRect(surf, rect); + } + + ~CClipRectGuard() + { + CSDL_Ext::setClipRect(surf, oldRect); + } + }; +} diff --git a/client/resource.h b/client/resource.h index caafaed7d..b5eedceac 100644 --- a/client/resource.h +++ b/client/resource.h @@ -1,16 +1,16 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by VCMI_client.rc -// -#define IDI_ICON1 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by VCMI_client.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 82784f6da..07ee15b5a 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1,1915 +1,1915 @@ -/* - * CCastleInterface.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 "CCastleInterface.h" - -#include "CHeroWindow.h" -#include "CTradeWindow.h" -#include "InfoWindows.h" -#include "GUIClasses.h" -#include "QuickRecruitmentWindow.h" -#include "CCreatureWindow.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/Buttons.h" -#include "../widgets/TextControls.h" -#include "../render/Canvas.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../render/ColorFilter.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../adventureMap/CList.h" -#include "../adventureMap/CResDataBar.h" - -#include "../../CCallback.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CBuildingHandler.h" -#include "../../lib/CCreatureHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/CTownHandler.h" -#include "../../lib/GameConstants.h" -#include "../../lib/StartInfo.h" -#include "../../lib/campaign/CampaignState.h" -#include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/mapObjects/CGTownInstance.h" - - -static bool useCompactCreatureBox() -{ - return settings["gameTweaks"]["compactTownCreatureInfo"].Bool(); -} - -static bool useAvailableAmountAsCreatureLabel() -{ - return settings["gameTweaks"]["availableCreaturesAsDwellingLabel"].Bool(); -} - -CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town, const CStructure * Str) - : CShowableAnim(0, 0, Str->defName, CShowableAnim::BASE, BUILDING_FRAME_TIME), - parent(Par), - town(Town), - str(Str), - border(nullptr), - area(nullptr), - stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT) -{ - addUsedEvents(LCLICK | SHOW_POPUP | MOVE | HOVER | TIME); - pos.x += str->pos.x; - pos.y += str->pos.y; - - // special animation frame manipulation for castle shipyard with and without ship - // done due to .def used in special way, not to animate building - first image is for shipyard without citadel moat, 2nd image is for including moat - if(Town->town->faction->getId() == FactionID::CASTLE && Str->building && - (Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP)) - { - if(Town->hasBuilt(BuildingID::CITADEL)) - { - this->first = 1; - this->frame = 1; - } - else - this->last = 0; - } - - if(!str->borderName.empty()) - border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::ALPHA); - - if(!str->areaName.empty()) - area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA); -} - -const CBuilding * CBuildingRect::getBuilding() -{ - if (!str->building) - return nullptr; - - if (str->hiddenUpgrade) // hidden upgrades, e.g. hordes - return base (dwelling for hordes) - return town->town->buildings.at(str->building->getBase()); - - return str->building; -} - -bool CBuildingRect::operator<(const CBuildingRect & p2) const -{ - return (str->pos.z) < (p2.str->pos.z); -} - -void CBuildingRect::hover(bool on) -{ - if (!area) - return; - - if(on) - { - if(! parent->selectedBuilding //no building hovered - || (*parent->selectedBuilding)<(*this)) //or we are on top - { - parent->selectedBuilding = this; - GH.statusbar()->write(getSubtitle()); - } - } - else - { - if(parent->selectedBuilding == this) - { - parent->selectedBuilding = nullptr; - GH.statusbar()->clear(); - } - } -} - -void CBuildingRect::clickPressed(const Point & cursorPosition) -{ - if(getBuilding() && area && (parent->selectedBuilding==this)) - { - auto building = getBuilding(); - parent->buildingClicked(building->bid, building->subId, building->upgrade); - } -} - -void CBuildingRect::showPopupWindow(const Point & cursorPosition) -{ - if((!area) || (this!=parent->selectedBuilding) || getBuilding() == nullptr) - return; - - BuildingID bid = getBuilding()->bid; - const CBuilding *bld = town->town->buildings.at(bid); - if (bid < BuildingID::DWELL_FIRST) - { - CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), - std::make_shared(CComponent::building, bld->town->faction->getIndex(), bld->bid)); - } - else - { - int level = ( bid - BuildingID::DWELL_FIRST ) % GameConstants::CREATURES_PER_TOWN; - GH.windows().createAndPushWindow(parent->pos.x+parent->pos.w / 2, parent->pos.y+parent->pos.h /2, town, level); - } -} - -void CBuildingRect::show(Canvas & to) -{ - uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT; - - if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT) - { - setAlpha(255 * stateTimeCounter / stageDelay); - CShowableAnim::show(to); - } - else - { - setAlpha(255); - CShowableAnim::show(to); - } - - if(border && stateTimeCounter > BUILDING_APPEAR_TIMEPOINT) - { - if(stateTimeCounter >= BUILD_ANIMATION_FINISHED_TIMEPOINT) - { - if(parent->selectedBuilding == this) - to.draw(border, pos.topLeft()); - return; - } - - auto darkBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 0.5f, 0.5f, 0.5f ); - auto lightBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 2.0f, 2.0f, 2.0f ); - auto baseBorder = ColorFilter::genEmptyShifter(); - - float progress = float(stateTimeCounter % stageDelay) / stageDelay; - - if (stateTimeCounter < BUILDING_WHITE_BORDER_TIMEPOINT) - border->adjustPalette(ColorFilter::genInterpolated(lightBorder, darkBorder, progress), 0); - else - if (stateTimeCounter < BUILDING_YELLOW_BORDER_TIMEPOINT) - border->adjustPalette(ColorFilter::genInterpolated(darkBorder, baseBorder, progress), 0); - else - border->adjustPalette(baseBorder, 0); - - to.draw(border, pos.topLeft()); - } -} - -void CBuildingRect::tick(uint32_t msPassed) -{ - CShowableAnim::tick(msPassed); - stateTimeCounter += msPassed; -} - -void CBuildingRect::showAll(Canvas & to) -{ - if (stateTimeCounter == 0) - return; - - CShowableAnim::showAll(to); - if(!isActive() && parent->selectedBuilding == this && border) - to.draw(border, pos.topLeft()); -} - -std::string CBuildingRect::getSubtitle()//hover text for building -{ - if (!getBuilding()) - return ""; - - int bid = getBuilding()->bid; - - if (bid<30)//non-dwellings - only buiding name - return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); - else//dwellings - recruit %creature% - { - auto & availableCreatures = town->creatures[(bid-30)%GameConstants::CREATURES_PER_TOWN].second; - if(availableCreatures.size()) - { - int creaID = availableCreatures.back();//taking last of available creatures - return CGI->generaltexth->allTexts[16] + " " + CGI->creh->objects.at(creaID)->getNamePluralTranslated(); - } - else - { - logGlobal->warn("Dwelling with id %d offers no creatures!", bid); - return "#ERROR#"; - } - } -} - -void CBuildingRect::mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) -{ - hover(true); -} - -bool CBuildingRect::receiveEvent(const Point & position, int eventType) const -{ - if (!pos.isInside(position.x, position.y)) - return false; - - if(area && area->isTransparent(position - pos.topLeft())) - return false; - - return CIntObject::receiveEvent(position, eventType); -} - -CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level) - : CWindowObject(RCLICK_POPUP, ImagePath::builtin("CRTOINFO"), Point(centerX, centerY)) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - background->colorize(Town->tempOwner); - - const CCreature * creature = CGI->creh->objects.at(Town->creatures.at(level).second.back()); - - title = std::make_shared(80, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getNamePluralTranslated()); - animation = std::make_shared(30, 44, creature, true, true); - - std::string text = std::to_string(Town->creatures.at(level).first); - available = std::make_shared(80,190, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[217] + text); - costPerTroop = std::make_shared(80, 227, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[346]); - - for(int i = 0; i(i); - if(creature->getRecruitCost(res)) - { - resPicture.push_back(std::make_shared(AnimationPath::builtin("RESOURCE"), i, 0, 0, 0)); - resAmount.push_back(std::make_shared(0,0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(creature->getRecruitCost(res)))); - } - } - - int posY = 238; - int posX = pos.w/2 - (int)resAmount.size() * 25 + 5; - for (size_t i=0; imoveBy(Point(posX, posY)); - resAmount[i]->moveBy(Point(posX+16, posY+43)); - posX += 50; - } -} - -CDwellingInfoBox::~CDwellingInfoBox() = default; - -CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroSlots * Owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - owner = Owner; - pos.x += x; - pos.y += y; - pos.w = 58; - pos.h = 64; - upg = updown; - - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 0, 0); - portrait->visible = false; - - flag = std::make_shared(AnimationPath::builtin("CREST58"), 0, 0, 0, 0); - flag->visible = false; - - selection = std::make_shared(AnimationPath::builtin("TWCRPORT"), 1, 0); - selection->visible = false; - - set(h); - - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); -} - -CHeroGSlot::~CHeroGSlot() = default; - -void CHeroGSlot::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - return; - } - std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; - std::string temp; - if(hero) - { - if(isSelected())//view NNN - { - temp = CGI->generaltexth->tcommands[4]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - else if(other->hero && other->isSelected())//exchange - { - temp = CGI->generaltexth->tcommands[7]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); - } - else// select NNN (in ZZZ) - { - if(upg)//down - visiting - { - temp = CGI->generaltexth->tcommands[32]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - else //up - garrison - { - temp = CGI->generaltexth->tcommands[12]; - boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); - } - } - } - else //we are empty slot - { - if(other->isSelected() && other->hero) //move NNNN - { - temp = CGI->generaltexth->tcommands[6]; - boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); - } - else //empty - { - temp = CGI->generaltexth->allTexts[507]; - } - } - if(temp.size()) - GH.statusbar()->write(temp); -} - -void CHeroGSlot::clickPressed(const Point & cursorPosition) -{ - std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; - - owner->garr->setSplittingMode(false); - owner->garr->selectSlot(nullptr); - - if(hero && isSelected()) - { - setHighlight(false); - LOCPLINT->openHeroWindow(hero); - } - else if(other->hero && other->isSelected()) - { - owner->swapArmies(); - } - else if(hero) - { - setHighlight(true); - owner->garr->selectSlot(nullptr); - redraw(); - } - - //refresh statusbar - hover(false); - hover(true); -} - -void CHeroGSlot::showPopupWindow(const Point & cursorPosition) -{ - if(hero) - { - GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); - } -} - -void CHeroGSlot::deactivate() -{ - selection->visible = false; - CIntObject::deactivate(); -} - -bool CHeroGSlot::isSelected() const -{ - return selection->visible; -} - -void CHeroGSlot::setHighlight(bool on) -{ - selection->visible = on; - - if(owner->garrisonedHero->hero && owner->visitingHero->hero) //two heroes in town - { - for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot higlighted - elem->block(!on); - } -} - -void CHeroGSlot::set(const CGHeroInstance * newHero) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - hero = newHero; - - selection->visible = false; - portrait->visible = false; - flag->visible = false; - - if(newHero) - { - portrait->visible = true; - portrait->setFrame(newHero->getIconIndex()); - } - else if(!upg && owner->showEmpty) //up garrison - { - flag->visible = true; - flag->setFrame(LOCPLINT->castleInt->town->getOwner().getNum()); - } -} - -HeroSlots::HeroSlots(const CGTownInstance * Town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty): - showEmpty(ShowEmpty), - town(Town), - garr(Garrison) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - garrisonedHero = std::make_shared(garrPos.x, garrPos.y, 0, town->garrisonHero, this); - visitingHero = std::make_shared(visitPos.x, visitPos.y, 1, town->visitingHero, this); -} - -HeroSlots::~HeroSlots() = default; - -void HeroSlots::update() -{ - garrisonedHero->set(town->garrisonHero); - visitingHero->set(town->visitingHero); -} - -void HeroSlots::splitClicked() -{ - if(!!town->visitingHero && town->garrisonHero && (visitingHero->isSelected() || garrisonedHero->isSelected())) - { - LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id); - } -} - -void HeroSlots::swapArmies() -{ - bool allow = true; - - //moving hero out of town - check if it is allowed - if (town->garrisonHero) - { - if (!town->visitingHero && LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) - { - std::string text = CGI->generaltexth->translate("core.genrltxt.18"); //You already have %d adventuring heroes under your command. - boost::algorithm::replace_first(text,"%d",std::to_string(LOCPLINT->cb->howManyHeroes(false))); - - LOCPLINT->showInfoDialog(text, std::vector>(), soundBase::sound_todo); - allow = false; - } - else if (town->garrisonHero->stacksCount() == 0) - { - //This hero has no creatures. A hero must have creatures before he can brave the dangers of the countryside. - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.19"), {}, soundBase::sound_todo); - allow = false; - } - } - - if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army - { - if(!town->visitingHero->canBeMergedWith(*town)) - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[275], std::vector>(), soundBase::sound_todo); - allow = false; - } - } - - garrisonedHero->setHighlight(false); - visitingHero->setHighlight(false); - - if (allow) - LOCPLINT->cb->swapGarrisonHero(town); -} - -class SORTHELP -{ -public: - bool operator() (const CIntObject * a, const CIntObject * b) - { - auto b1 = dynamic_cast(a); - auto b2 = dynamic_cast(b); - - if(!b1 && !b2) - return intptr_t(a) < intptr_t(b); - if(b1 && !b2) - return false; - if(!b1 && b2) - return true; - - return (*b1)<(*b2); - } -}; - -SORTHELP buildSorter; - -CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): - town(Town), - selectedBuilding(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - background = std::make_shared(town->town->clientInfo.townBackground); - background->needRefresh = true; - background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); - pos.w = background->pos.w; - pos.h = background->pos.h; - - recreate(); -} - -CCastleBuildings::~CCastleBuildings() = default; - -void CCastleBuildings::recreate() -{ - selectedBuilding = nullptr; - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - buildings.clear(); - groups.clear(); - - //Generate buildings list - - auto buildingsCopy = town->builtBuildings;// a bit modified copy of built buildings - - if(vstd::contains(town->builtBuildings, BuildingID::SHIPYARD)) - { - auto bayPos = town->bestLocation(); - if(!bayPos.valid()) - logGlobal->warn("Shipyard in non-coastal town!"); - std::vector vobjs = LOCPLINT->cb->getVisitableObjs(bayPos, false); - //there is visitable obj at shipyard output tile and it's a boat or hero (on boat) - if(!vobjs.empty() && (vobjs.front()->ID == Obj::BOAT || vobjs.front()->ID == Obj::HERO)) - { - buildingsCopy.insert(BuildingID::SHIP); - } - } - - for(const CStructure * structure : town->town->clientInfo.structures) - { - if(!structure->building) - { - buildings.push_back(std::make_shared(this, town, structure)); - continue; - } - if(vstd::contains(buildingsCopy, structure->building->bid)) - { - groups[structure->building->getBase()].push_back(structure); - } - } - - for(auto & entry : groups) - { - const CBuilding * build = town->town->buildings.at(entry.first); - - const CStructure * toAdd = *boost::max_element(entry.second, [=](const CStructure * a, const CStructure * b) - { - return build->getDistance(a->building->bid) < build->getDistance(b->building->bid); - }); - - buildings.push_back(std::make_shared(this, town, toAdd)); - } - - boost::sort(children, buildSorter); //TODO: create building in blit order -} - -void CCastleBuildings::addBuilding(BuildingID building) -{ - //FIXME: implement faster method without complete recreation of town - BuildingID base = town->town->buildings.at(building)->getBase(); - - recreate(); - - auto & structures = groups.at(base); - - for(auto buildingRect : buildings) - { - if(vstd::contains(structures, buildingRect->str)) - { - //reset animation - if(structures.size() == 1) - buildingRect->stateTimeCounter = 0; // transparency -> fully visible stage - else - buildingRect->stateTimeCounter = CBuildingRect::BUILDING_APPEAR_TIMEPOINT; // already in fully visible stage - break; - } - } -} - -void CCastleBuildings::removeBuilding(BuildingID building) -{ - //FIXME: implement faster method without complete recreation of town - recreate(); -} - -const CGHeroInstance * CCastleBuildings::getHero() -{ - if(town->visitingHero) - return town->visitingHero; - else - return town->garrisonHero; -} - -void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) -{ - logGlobal->trace("You've clicked on %d", (int)building.toEnum()); - const CBuilding *b = town->town->buildings.find(building)->second; - - if(building >= BuildingID::DWELL_FIRST) - { - enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN); - } - else - { - switch(building) - { - case BuildingID::MAGES_GUILD_1: - case BuildingID::MAGES_GUILD_2: - case BuildingID::MAGES_GUILD_3: - case BuildingID::MAGES_GUILD_4: - case BuildingID::MAGES_GUILD_5: - enterMagesGuild(); - break; - - case BuildingID::TAVERN: - LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - break; - - case BuildingID::SHIPYARD: - if(town->shipyardStatus() == IBoatGenerator::GOOD) - LOCPLINT->showShipyardDialog(town); - else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT) - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); - break; - - case BuildingID::FORT: - case BuildingID::CITADEL: - case BuildingID::CASTLE: - GH.windows().createAndPushWindow(town); - break; - - case BuildingID::VILLAGE_HALL: - case BuildingID::CITY_HALL: - case BuildingID::TOWN_HALL: - case BuildingID::CAPITOL: - enterTownHall(); - break; - - case BuildingID::MARKETPLACE: - // can't use allied marketplace - if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); - else - enterBuilding(building); - break; - - case BuildingID::BLACKSMITH: - enterBlacksmith(town->town->warMachine); - break; - - case BuildingID::SHIP: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat - break; - - case BuildingID::SPECIAL_1: - case BuildingID::SPECIAL_2: - case BuildingID::SPECIAL_3: - switch(subID) - { - case BuildingSubID::NONE: - enterBuilding(building); - break; - - case BuildingSubID::MYSTIC_POND: - enterFountain(building, subID, upgrades); - break; - - case BuildingSubID::ARTIFACT_MERCHANT: - if(town->visitingHero) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT); - else - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. - break; - - case BuildingSubID::FOUNTAIN_OF_FORTUNE: - enterFountain(building, subID, upgrades); - break; - - case BuildingSubID::FREELANCERS_GUILD: - if(getHero()) - GH.windows().createAndPushWindow(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE); - else - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. - break; - - case BuildingSubID::MAGIC_UNIVERSITY: - if (getHero()) - GH.windows().createAndPushWindow(getHero(), town, nullptr); - else - enterBuilding(building); - break; - - case BuildingSubID::BROTHERHOOD_OF_SWORD: - if(upgrades == BuildingID::TAVERN) - LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - else - enterBuilding(building); - break; - - case BuildingSubID::CASTLE_GATE: - enterCastleGate(); - break; - - case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer - GH.windows().createAndPushWindow(town, getHero(), nullptr); - break; - - case BuildingSubID::PORTAL_OF_SUMMONING: - if (town->creatures[GameConstants::CREATURES_PER_TOWN].second.empty())//No creatures - LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); - else - enterDwelling(GameConstants::CREATURES_PER_TOWN); - break; - - case BuildingSubID::BALLISTA_YARD: - enterBlacksmith(ArtifactID::BALLISTA); - break; - - default: - enterBuilding(building); - break; - } - break; - - default: - enterBuilding(building); - break; - } - } -} - -void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) -{ - const CGHeroInstance *hero = town->visitingHero; - if(!hero) - { - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated())); - return; - } - auto art = artifactID.toArtifact(); - - int price = art->getPrice(); - bool possible = LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= price; - if(possible) - { - for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) - { - if(hero->getArt(slot) == nullptr) - { - possible = true; - break; - } - else - { - possible = false; - } - } - } - CreatureID cre = art->getWarMachine(); - GH.windows().createAndPushWindow(possible, cre, artifactID, hero->id); -} - -void CCastleBuildings::enterBuilding(BuildingID building) -{ - std::vector> comps(1, std::make_shared(CComponent::building, town->subID, building)); - LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); -} - -void CCastleBuildings::enterCastleGate() -{ - if (!town->visitingHero) - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[126]); - return;//only visiting hero can use castle gates - } - std::vector availableTowns; - std::vector Towns = LOCPLINT->cb->getTownsInfo(true); - for(auto & Town : Towns) - { - const CGTownInstance *t = Town; - if (t->id != this->town->id && t->visitingHero == nullptr && //another town, empty and this is - t->town->faction->getId() == town->town->faction->getId() && //the town of the same faction - t->hasBuilt(BuildingSubID::CASTLE_GATE)) //and the town has a castle gate - { - availableTowns.push_back(t->id.getNum());//add to the list - } - } - auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window - GH.windows().createAndPushWindow(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], - CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1)); -} - -void CCastleBuildings::enterDwelling(int level) -{ - if (level < 0 || level >= town->creatures.size() || town->creatures[level].second.empty()) - { - assert(0); - logGlobal->error("Attempt to enter into invalid dwelling of level %d in town %s (%s)", level, town->getNameTranslated(), town->town->faction->getNameTranslated()); - return; - } - - auto recruitCb = [=](CreatureID id, int count) - { - LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); - }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, -87); -} - -void CCastleBuildings::enterToTheQuickRecruitmentWindow() -{ - const auto beginIt = town->creatures.cbegin(); - const auto afterLastIt = town->creatures.size() > GameConstants::CREATURES_PER_TOWN - ? std::next(beginIt, GameConstants::CREATURES_PER_TOWN) - : town->creatures.cend(); - const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt, - [](const auto & creatureInfo) { return creatureInfo.first > 0; }); - if(hasSomeoneToRecruit) - GH.windows().createAndPushWindow(town, pos); - else - CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); -} - -void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) -{ - std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); - std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); - std::string hasNotProduced; - std::string hasProduced; - - if(this->town->town->faction->getIndex() == ETownType::RAMPART) - { - hasNotProduced = CGI->generaltexth->allTexts[677]; - hasProduced = CGI->generaltexth->allTexts[678]; - } - else - { - auto buildingName = town->town->getSpecialBuilding(subID)->getNameTranslated(); - - hasNotProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasNotProduced")); - hasProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasProduced")); - boost::algorithm::replace_first(hasNotProduced, "%s", buildingName); - boost::algorithm::replace_first(hasProduced, "%s", buildingName); - } - - bool isMysticPondOrItsUpgrade = subID == BuildingSubID::MYSTIC_POND - || (upgrades != BuildingID::NONE - && town->town->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); - - if(upgrades != BuildingID::NONE) - descr += "\n\n"+town->town->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); - - if(isMysticPondOrItsUpgrade) //for vanila Rampart like towns - { - if(town->bonusValue.first == 0) //Mystic Pond produced nothing; - descr += "\n\n" + hasNotProduced; - else //Mystic Pond produced something; - { - descr += "\n\n" + hasProduced; - boost::algorithm::replace_first(descr,"%s",CGI->generaltexth->restypes[town->bonusValue.first]); - boost::algorithm::replace_first(descr,"%d",std::to_string(town->bonusValue.second)); - } - } - LOCPLINT->showInfoDialog(descr, comps); -} - -void CCastleBuildings::enterMagesGuild() -{ - const CGHeroInstance *hero = getHero(); - - if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok - { - const StartInfo *si = LOCPLINT->cb->getStartInfo(); - // it would be nice to find a way to move this hack to config/mapOverrides.json - if(si && si->campState && si->campState && // We're in campaign, - (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", - (hero->subID == 45)) // and the hero is Yog (based on Solmyr) - { - // "Yog has given up magic in all its forms..." - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); - } - else if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < 500) //not enough gold to buy spellbook - { - openMagesGuild(); - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[213]); - } - else - { - CFunctionList onYes = [this](){ openMagesGuild(); }; - CFunctionList onNo = onYes; - onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; - std::vector> components(1, std::make_shared(CComponent::artifact,ArtifactID::SPELLBOOK,0)); - - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); - } - } - else - { - openMagesGuild(); - } -} - -void CCastleBuildings::enterTownHall() -{ - if(town->visitingHero && town->visitingHero->hasArt(ArtifactID::GRAIL) && - !vstd::contains(town->builtBuildings, BuildingID::GRAIL)) //hero has grail, but town does not have it - { - if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL)) - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[597], //Do you wish this to be the permanent home of the Grail? - [&](){ LOCPLINT->cb->buildBuilding(town, BuildingID::GRAIL); }, - [&](){ openTownHall(); }); - } - else - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[673]); - assert(GH.windows().topWindow() != nullptr); - GH.windows().topWindow()->buttons[0]->addCallback(std::bind(&CCastleBuildings::openTownHall, this)); - } - } - else - { - openTownHall(); - } -} - -void CCastleBuildings::openMagesGuild() -{ - auto mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; - GH.windows().createAndPushWindow(LOCPLINT->castleInt, mageGuildBackground); -} - -void CCastleBuildings::openTownHall() -{ - GH.windows().createAndPushWindow(town); -} - -CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable): - town(Town), - level(Level), - showAvailable(_showAvailable) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos += position; - - if(town->creatures.size() <= level || town->creatures[level].second.empty()) - { - level = -1; - return;//No creature - } - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - - ui32 creatureID = town->creatures[level].second.back(); - creature = CGI->creh->objects[creatureID]; - - picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, 8, 0); - - std::string value; - if(showAvailable) - value = std::to_string(town->creatures[level].first); - else - value = std::string("+") + std::to_string(town->creatureGrowth(level)); - - if(compact) - { - label = std::make_shared(40, 32, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, value); - pos.x += 8; - pos.w = 32; - pos.h = 32; - } - else - { - label = std::make_shared(24, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, value); - pos.w = 48; - pos.h = 48; - } -} - -void CCreaInfo::update() -{ - if(label) - { - std::string value; - if(showAvailable) - value = std::to_string(town->creatures[level].first); - else - value = std::string("+") + std::to_string(town->creatureGrowth(level)); - - if(value != label->getText()) - label->setText(value); - } -} - -void CCreaInfo::hover(bool on) -{ - std::string message = CGI->generaltexth->allTexts[588]; - boost::algorithm::replace_first(message, "%s", creature->getNamePluralTranslated()); - - if(on) - { - GH.statusbar()->write(message); - } - else - { - GH.statusbar()->clearIfMatching(message); - } -} - -void CCreaInfo::clickPressed(const Point & cursorPosition) -{ - int offset = LOCPLINT->castleInt? (-87) : 0; - auto recruitCb = [=](CreatureID id, int count) - { - LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); - }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, offset); -} - -std::string CCreaInfo::genGrowthText() -{ - GrowthInfo gi = town->getGrowthInfo(level); - std::string descr = boost::str(boost::format(CGI->generaltexth->allTexts[589]) % creature->getNameSingularTranslated() % gi.totalGrowth()); - - for(const GrowthInfo::Entry & entry : gi.entries) - descr +="\n" + entry.description; - - return descr; -} - -void CCreaInfo::showPopupWindow(const Point & cursorPosition) -{ - if (showAvailable) - GH.windows().createAndPushWindow(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level); - else - CRClickPopup::createAndPush(genGrowthText(), std::make_shared(CComponent::creature, creature->getId())); -} - -bool CCreaInfo::getShowAvailable() -{ - return showAvailable; -} - -CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townHall) - : town(Town), - building(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - addUsedEvents(SHOW_POPUP | HOVER); - pos.x += posX; - pos.y += posY; - int buildID; - - if(townHall) - { - buildID = 10 + town->hallLevel(); - picture = std::make_shared(AnimationPath::builtin("ITMTL.DEF"), town->hallLevel()); - } - else - { - buildID = 6 + town->fortLevel(); - if(buildID == 6) - return;//FIXME: suspicious statement, fix or comment - picture = std::make_shared(AnimationPath::builtin("ITMCL.DEF"), town->fortLevel()-1); - } - building = town->town->buildings.at(BuildingID(buildID)); - pos = picture->pos; -} - -void CTownInfo::hover(bool on) -{ - if(on) - { - if(building ) - GH.statusbar()->write(building->getNameTranslated()); - } - else - { - GH.statusbar()->clear(); - } -} - -void CTownInfo::showPopupWindow(const Point & cursorPosition) -{ - if(building) - { - auto c = std::make_shared(CComponent::building, building->town->faction->getIndex(), building->bid); - CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); - } -} - -CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from): - CStatusbarWindow(PLAYER_COLORED | BORDERED), - town(Town) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - LOCPLINT->castleInt = this; - addUsedEvents(KEYBOARD); - - builds = std::make_shared(town); - panel = std::make_shared(ImagePath::builtin("TOWNSCRN"), 0, builds->pos.h); - panel->colorize(LOCPLINT->playerID); - pos.w = panel->pos.w; - pos.h = builds->pos.h + panel->pos.h; - center(); - updateShadow(); - - garr = std::make_shared(Point(305, 387), 4, Point(0,96), town->getUpperArmy(), town->visitingHero); - garr->setRedrawParent(true); - - heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); - title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); - income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); - icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); - - exit = std::make_shared(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); - exit->setImageOrder(4, 5, 6, 7); - - auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() - { - garr->splitClick(); - heroes->splitClicked(); - }); - garr->addSplitBtn(split); - - Rect barRect(9, 182, 732, 18); - auto statusbarBackground = std::make_shared(panel->getSurface(), barRect, 9, 555); - statusbar = CGStatusBar::create(statusbarBackground); - resdatabar = std::make_shared(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78); - - townlist = std::make_shared(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() ); - townlist->setScrollUpButton( std::make_shared( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"))); - townlist->setScrollDownButton( std::make_shared( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"))); - - if(from) - townlist->select(from); - - townlist->select(town); //this will scroll list to select current town - townlist->onSelect = std::bind(&CCastleInterface::townChange, this); - - recreateIcons(); - if (!from) - adventureInt->onAudioPaused(); - CCS->musich->playMusic(town->town->clientInfo.musicTheme, true, false); -} - -CCastleInterface::~CCastleInterface() -{ - // resume map audio if: - // adventureInt exists (may happen on exiting client with open castle interface) - // castleInt has not been replaced (happens on switching between towns inside castle interface) - if (adventureInt && LOCPLINT->castleInt == this) - adventureInt->onAudioResumed(); - if(LOCPLINT->castleInt == this) - LOCPLINT->castleInt = nullptr; -} - -void CCastleInterface::updateGarrisons() -{ - garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); - garr->setArmy(town->visitingHero, EGarrisonType::LOWER); - garr->recreateSlots(); - heroes->update(); - - redraw(); -} - -void CCastleInterface::close() -{ - if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town - { - if(town->visitingHero && town->visitingHero->tempOwner == LOCPLINT->playerID) - LOCPLINT->localState->setSelection(town->visitingHero); - else - LOCPLINT->localState->setSelection(town); - } - CWindowObject::close(); -} - -void CCastleInterface::castleTeleport(int where) -{ - const CGTownInstance * dest = LOCPLINT->cb->getTown(ObjectInstanceID(where)); - LOCPLINT->localState->setSelection(town->visitingHero);//according to assert(ho == adventureInt->selection) in the eraseCurrentPathOf - LOCPLINT->cb->teleportHero(town->visitingHero, dest); - LOCPLINT->localState->erasePath(town->visitingHero); -} - -void CCastleInterface::townChange() -{ - //TODO: do not recreate window - const CGTownInstance * dest = LOCPLINT->localState->getOwnedTown(townlist->getSelectedIndex()); - const CGTownInstance * town = this->town;// "this" is going to be deleted - if ( dest == town ) - return; - close(); - GH.windows().createAndPushWindow(dest, town); -} - -void CCastleInterface::addBuilding(BuildingID bid) -{ - deactivate(); - builds->addBuilding(bid); - recreateIcons(); - activate(); - redraw(); -} - -void CCastleInterface::removeBuilding(BuildingID bid) -{ - deactivate(); - builds->removeBuilding(bid); - recreateIcons(); - activate(); - redraw(); -} - -void CCastleInterface::recreateIcons() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; - - icon->setFrame(iconIndex); - TResources townIncome = town->dailyIncome(); - income->setText(std::to_string(townIncome[EGameResID::GOLD])); - - hall = std::make_shared(80, 413, town, true); - fort = std::make_shared(122, 413, town, false); - - fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); - fastTownHall->setAnimateLonelyFrame(true); - - fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); - fastArmyPurchase->setAnimateLonelyFrame(true); - - fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() - { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) - { - if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); - } - }); - - fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() - { - if(town->builtBuildings.count(BuildingID::TAVERN)) - LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - }); - - creainfo.clear(); - - bool compactCreatureInfo = useCompactCreatureBox(); - bool useAvailableCreaturesForLabel = useAvailableAmountAsCreatureLabel(); - - for(size_t i=0; i<4; i++) - creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 459), town, (int)i, compactCreatureInfo, useAvailableCreaturesForLabel)); - - - for(size_t i=0; i<4; i++) - creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel)); - -} - -void CCastleInterface::keyPressed(EShortcut key) -{ - switch(key) - { - case EShortcut::MOVE_UP: - townlist->selectPrev(); - break; - case EShortcut::MOVE_DOWN: - townlist->selectNext(); - break; - case EShortcut::TOWN_SWAP_ARMIES: - heroes->swapArmies(); - break; - case EShortcut::TOWN_OPEN_TAVERN: - if(town->hasBuilt(BuildingID::TAVERN)) - LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); - break; - default: - break; - } -} - -void CCastleInterface::creaturesChangedEventHandler() -{ - for(auto creatureInfoBox : creainfo) - { - if(creatureInfoBox->getShowAvailable()) - { - creatureInfoBox->update(); - } - } -} - -CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building): - town(Town), - building(Building) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos.x += x; - pos.y += y; - pos.w = 154; - pos.h = 92; - - state = LOCPLINT->cb->canBuildStructure(town, building->bid); - - static int panelIndex[12] = - { - 3, 3, 3, 0, 0, 2, 2, 1, 2, 2, 3, 3 - }; - static int iconIndex[12] = - { - -1, -1, -1, 0, 0, 1, 2, -1, 1, 1, -1, -1 - }; - - icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 2, 2); - header = std::make_shared(AnimationPath::builtin("TPTHBAR"), panelIndex[static_cast(state)], 0, 1, 73); - if(iconIndex[static_cast(state)] >=0) - mark = std::make_shared(AnimationPath::builtin("TPTHCHK"), iconIndex[static_cast(state)], 0, 136, 56); - name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); - - //todo: add support for all possible states - if(state >= EBuildingState::BUILDING_ERROR) - state = EBuildingState::FORBIDDEN; -} - -void CHallInterface::CBuildingBox::hover(bool on) -{ - if(on) - { - std::string toPrint; - if(state==EBuildingState::PREREQUIRES || state == EBuildingState::MISSING_BASE) - toPrint = CGI->generaltexth->hcommands[5]; - else if(state==EBuildingState::CANT_BUILD_TODAY) - toPrint = CGI->generaltexth->allTexts[223]; - else - toPrint = CGI->generaltexth->hcommands[static_cast(state)]; - boost::algorithm::replace_first(toPrint,"%s",building->getNameTranslated()); - GH.statusbar()->write(toPrint); - } - else - { - GH.statusbar()->clear(); - } -} - -void CHallInterface::CBuildingBox::clickPressed(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(town,building,state,0); -} - -void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(town,building,state,1); -} - -CHallInterface::CHallInterface(const CGTownInstance * Town): - CStatusbarWindow(PLAYER_COLORED | BORDERED, Town->town->clientInfo.hallBackground), - town(Town) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - Rect barRect(5, 556, 740, 18); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 5, 556); - statusbar = CGStatusBar::create(statusbarBackground); - - title = std::make_shared(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->town->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated()); - exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); - - auto & boxList = town->town->clientInfo.hallSlots; - boxes.resize(boxList.size()); - for(size_t row=0; rowtown->buildings.at(buildingID); - if(vstd::contains(town->builtBuildings, buildingID)) - { - building = current; - } - else - { - if(current->mode == CBuilding::BUILD_NORMAL) - { - building = current; - break; - } - } - } - int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col, - posY = 35 + 104*(int)row; - - if(building) - boxes[row].push_back(std::make_shared(posX, posY, town, building)); - } - } -} - -CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, EBuildingState state, bool rightClick): - CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), ImagePath::builtin("TPUBUILD")), - town(Town), - building(Building) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 125, 50); - auto statusbarBackground = std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26); - statusbar = CGStatusBar::create(statusbarBackground); - - MetaString nameString; - nameString.appendTextID("core.hallinfo.7"); - nameString.replaceTextID(building->getNameTextID()); - - name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, nameString.toString()); - description = std::make_shared(building->getDescriptionTranslated(), Rect(33, 135, 329, 67), 0, FONT_MEDIUM, ETextAlignment::CENTER); - stateText = std::make_shared(getTextForState(state), Rect(33, 216, 329, 67), 0, FONT_SMALL, ETextAlignment::CENTER); - - //Create components for all required resources - std::vector> components; - - for(int i = 0; iresources[i]) - { - std::string text = std::to_string(building->resources[i]); - int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; - if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) - text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; - components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); - } - } - - cost = std::make_shared(components, Rect(25, 300, pos.w - 50, 130)); - - if(!rightClick) - { //normal window - - MetaString tooltipYes; - tooltipYes.appendTextID("core.genrltxt.595"); - tooltipYes.replaceTextID(building->getNameTextID()); - - MetaString tooltipNo; - tooltipNo.appendTextID("core.genrltxt.596"); - tooltipNo.replaceTextID(building->getNameTextID()); - - buy = std::make_shared(Point(45, 446), AnimationPath::builtin("IBUY30"), CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); - buy->setBorderColor(Colors::METALLIC_GOLD); - buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner); - - cancel = std::make_shared(Point(290, 445), AnimationPath::builtin("ICANCEL"), CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); - cancel->setBorderColor(Colors::METALLIC_GOLD); - } -} - -void CBuildWindow::buyFunc() -{ - LOCPLINT->cb->buildBuilding(town,building->bid); - GH.windows().popWindows(2); //we - build window and hall screen -} - -std::string CBuildWindow::getTextForState(EBuildingState state) -{ - std::string ret; - if(state < EBuildingState::ALLOWED) - ret = CGI->generaltexth->hcommands[static_cast(state)]; - switch (state) - { - case EBuildingState::ALREADY_PRESENT: - case EBuildingState::CANT_BUILD_TODAY: - case EBuildingState::NO_RESOURCES: - ret.replace(ret.find_first_of("%s"), 2, building->getNameTranslated()); - break; - case EBuildingState::ALLOWED: - return CGI->generaltexth->allTexts[219]; //all prereq. are met - case EBuildingState::PREREQUIRES: - { - auto toStr = [&](const BuildingID build) -> std::string - { - return town->town->buildings.at(build)->getNameTranslated(); - }; - - ret = CGI->generaltexth->allTexts[52]; - ret += "\n" + town->genBuildingRequirements(building->bid).toString(toStr); - break; - } - case EBuildingState::MISSING_BASE: - { - std::string msg = CGI->generaltexth->translate("vcmi.townHall.missingBase"); - ret = boost::str(boost::format(msg) % town->town->buildings.at(building->upgrade)->getNameTranslated()); - break; - } - } - return ret; -} - -LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int min, int max) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x+=size.x; - pos.y+=size.y; - pos.w = size.w; - pos.h = size.h; - init(name, descr, min, max); -} - -LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int val) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x+=size.x; - pos.y+=size.y; - pos.w = size.w; - pos.h = size.h; - init(name, descr, val, val); -} - -void LabeledValue::init(std::string nameText, std::string descr, int min, int max) -{ - addUsedEvents(HOVER); - hoverText = descr; - std::string valueText; - if(min && max) - { - valueText = std::to_string(min); - if(min != max) - valueText += '-' + std::to_string(max); - } - name = std::make_shared(3, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, nameText); - value = std::make_shared(pos.w-3, pos.h-2, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, valueText); -} - -void LabeledValue::hover(bool on) -{ - if(on) - { - GH.statusbar()->write(hoverText); - } - else - { - GH.statusbar()->clear(); - } -} - -CFortScreen::CFortScreen(const CGTownInstance * town): - CStatusbarWindow(PLAYER_COLORED | BORDERED, getBgName(town)) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) - fortSize--; - - const CBuilding * fortBuilding = town->town->buildings.at(BuildingID(town->fortLevel()+6)); - title = std::make_shared(400, 12, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE, fortBuilding->getNameTranslated()); - - std::string text = boost::str(boost::format(CGI->generaltexth->fcommands[6]) % fortBuilding->getNameTranslated()); - exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); - - std::vector positions = - { - Point(10, 22), Point(404, 22), - Point(10, 155), Point(404,155), - Point(10, 288), Point(404,288) - }; - - if(fortSize == GameConstants::CREATURES_PER_TOWN) - { - positions.push_back(Point(206,421)); - } - else - { - positions.push_back(Point(10, 421)); - positions.push_back(Point(404,421)); - } - - for(ui32 i=0; ibuiltBuildings, dwelling)) - buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); - else - buildingID = BuildingID(BuildingID::DWELL_FIRST+i); - } - else - { - buildingID = BuildingID::SPECIAL_3; - } - - recAreas.push_back(std::make_shared(positions[i].x, positions[i].y, town, i)); - } - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - Rect barRect(4, 554, 740, 18); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 4, 554); - statusbar = CGStatusBar::create(statusbarBackground); -} - -ImagePath CFortScreen::getBgName(const CGTownInstance * town) -{ - ui32 fortSize = static_cast(town->creatures.size()); - if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) - fortSize--; - - if(fortSize == GameConstants::CREATURES_PER_TOWN) - return ImagePath::builtin("TPCASTL7"); - else - return ImagePath::builtin("TPCASTL8"); -} - -void CFortScreen::creaturesChangedEventHandler() -{ - for(auto & elem : recAreas) - elem->creaturesChangedEventHandler(); - - LOCPLINT->castleInt->creaturesChangedEventHandler(); -} - -CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * Town, int Level): - town(Town), - level(Level), - availableCount(nullptr) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x +=posX; - pos.y +=posY; - pos.w = 386; - pos.h = 126; - - if(!town->creatures[level].second.empty()) - addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present - - addUsedEvents(SHOW_POPUP); - - icons = std::make_shared(ImagePath::builtin("TPCAINFO"), 261, 3); - - if(getMyBuilding() != nullptr) - { - buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); - buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); - - if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) - { - ui32 available = town->creatures[level].first; - std::string availableText = CGI->generaltexth->allTexts[217]+ std::to_string(available); - availableCount = std::make_shared(78, 119, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, availableText); - } - } - - if(getMyCreature() != nullptr) - { - hoverText = boost::str(boost::format(CGI->generaltexth->tcommands[21]) % getMyCreature()->getNamePluralTranslated()); - new CCreaturePic(159, 4, getMyCreature(), false); - new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated()); - - Rect sizes(287, 4, 96, 18); - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[191], CGI->generaltexth->fcommands[1], getMyCreature()->getDefense(false))); - sizes.y+=21; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[199], CGI->generaltexth->fcommands[2], getMyCreature()->getMinDamage(false), getMyCreature()->getMaxDamage(false))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[388], CGI->generaltexth->fcommands[3], getMyCreature()->getMaxHealth())); - sizes.y+=21; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[193], CGI->generaltexth->fcommands[4], getMyCreature()->valOfBonuses(BonusType::STACKS_SPEED))); - sizes.y+=20; - values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[194], CGI->generaltexth->fcommands[5], town->creatureGrowth(level))); - } -} - -const CCreature * CFortScreen::RecruitArea::getMyCreature() -{ - if(!town->creatures.at(level).second.empty()) // built - return VLC->creh->objects[town->creatures.at(level).second.back()]; - if(!town->town->creatures.at(level).empty()) // there are creatures on this level - return VLC->creh->objects[town->town->creatures.at(level).front()]; - return nullptr; -} - -const CBuilding * CFortScreen::RecruitArea::getMyBuilding() -{ - BuildingID myID = BuildingID(BuildingID::DWELL_FIRST + level); - - if (level == GameConstants::CREATURES_PER_TOWN) - return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); - - if (!town->town->buildings.count(myID)) - return nullptr; - - const CBuilding * build = town->town->buildings.at(myID); - while (town->town->buildings.count(myID)) - { - if (town->hasBuilt(myID)) - build = town->town->buildings.at(myID); - myID.advance(GameConstants::CREATURES_PER_TOWN); - } - return build; -} - -void CFortScreen::RecruitArea::hover(bool on) -{ - if(on) - GH.statusbar()->write(hoverText); - else - GH.statusbar()->clear(); -} - -void CFortScreen::RecruitArea::creaturesChangedEventHandler() -{ - if(availableCount) - { - std::string availableText = CGI->generaltexth->allTexts[217] + std::to_string(town->creatures[level].first); - availableCount->setText(availableText); - } -} - -void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition) -{ - LOCPLINT->castleInt->builds->enterDwelling(level); -} - -void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) -{ - if (getMyCreature() != nullptr) - GH.windows().createAndPushWindow(getMyCreature(), true); -} - -CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename) - : CStatusbarWindow(BORDERED, imagename) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - window = std::make_shared(owner->town->town->clientInfo.guildWindow, 332, 76); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - Rect barRect(7, 556, 737, 18); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 7, 556); - statusbar = CGStatusBar::create(statusbarBackground); - - exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); - - static const std::vector > positions = - { - {Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)}, - {Point(48,53), Point(48,147), Point(48,241), Point(48,335), Point(48,429)}, - {Point(570,82), Point(672,82), Point(570,157), Point(672,157)}, - {Point(183,42), Point(183,148), Point(183,253)}, - {Point(491,325), Point(591,325)} - }; - - for(size_t i=0; itown->town->mageLevel; i++) - { - size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm? - for(size_t j=0; jtown->mageGuildLevel() && owner->town->spells[i].size()>j) - spells.push_back(std::make_shared(positions[i][j], CGI->spellh->objects[owner->town->spells[i][j]])); - else - emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); - } - } -} - -CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) - : spell(Spell) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos += position; - image = std::make_shared(AnimationPath::builtin("SPELLSCR"), spell->id); - pos = image->pos; -} - -void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) -{ - LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); -} - -void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); -} - -void CMageGuildScreen::Scroll::hover(bool on) -{ - if(on) - GH.statusbar()->write(spell->getNameTranslated()); - else - GH.statusbar()->clear(); - -} - -CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): - CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSMITH")) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - Rect barRect(8, pos.h - 26, pos.w - 16, 19); - - auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 8, pos.h - 26); - statusbar = CGStatusBar::create(statusbarBackground); - - animBG = std::make_shared(ImagePath::builtin("TPSMITBK"), 64, 50); - animBG->needRefresh = true; - - const CCreature * creature = CGI->creh->objects[creMachineID]; - anim = std::make_shared(64, 50, creature->animDefName); - anim->clipRect(113,125,200,150); - - MetaString titleString; - titleString.appendTextID("core.genrltxt.274"); - titleString.replaceTextID(creature->getNameSingularTextID()); - - MetaString buyText; - buyText.appendTextID("core.genrltxt.595"); - buyText.replaceTextID(creature->getNameSingularTextID()); - - MetaString cancelText; - cancelText.appendTextID("core.genrltxt.596"); - cancelText.replaceTextID(creature->getNameSingularTextID()); - - std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); - - title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); - costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); - costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); - buy = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30.DEF"), CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); - - if(possible) - buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); - else - buy->block(true); - - costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 148, 244); -} +/* + * CCastleInterface.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 "CCastleInterface.h" + +#include "CHeroWindow.h" +#include "CTradeWindow.h" +#include "InfoWindows.h" +#include "GUIClasses.h" +#include "QuickRecruitmentWindow.h" +#include "CCreatureWindow.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" +#include "../render/Canvas.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/ColorFilter.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../adventureMap/CList.h" +#include "../adventureMap/CResDataBar.h" + +#include "../../CCallback.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CBuildingHandler.h" +#include "../../lib/CCreatureHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/GameSettings.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/CTownHandler.h" +#include "../../lib/GameConstants.h" +#include "../../lib/StartInfo.h" +#include "../../lib/campaign/CampaignState.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGTownInstance.h" + + +static bool useCompactCreatureBox() +{ + return settings["gameTweaks"]["compactTownCreatureInfo"].Bool(); +} + +static bool useAvailableAmountAsCreatureLabel() +{ + return settings["gameTweaks"]["availableCreaturesAsDwellingLabel"].Bool(); +} + +CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town, const CStructure * Str) + : CShowableAnim(0, 0, Str->defName, CShowableAnim::BASE, BUILDING_FRAME_TIME), + parent(Par), + town(Town), + str(Str), + border(nullptr), + area(nullptr), + stateTimeCounter(BUILD_ANIMATION_FINISHED_TIMEPOINT) +{ + addUsedEvents(LCLICK | SHOW_POPUP | MOVE | HOVER | TIME); + pos.x += str->pos.x; + pos.y += str->pos.y; + + // special animation frame manipulation for castle shipyard with and without ship + // done due to .def used in special way, not to animate building - first image is for shipyard without citadel moat, 2nd image is for including moat + if(Town->town->faction->getId() == FactionID::CASTLE && Str->building && + (Str->building->bid == BuildingID::SHIPYARD || Str->building->bid == BuildingID::SHIP)) + { + if(Town->hasBuilt(BuildingID::CITADEL)) + { + this->first = 1; + this->frame = 1; + } + else + this->last = 0; + } + + if(!str->borderName.empty()) + border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::ALPHA); + + if(!str->areaName.empty()) + area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA); +} + +const CBuilding * CBuildingRect::getBuilding() +{ + if (!str->building) + return nullptr; + + if (str->hiddenUpgrade) // hidden upgrades, e.g. hordes - return base (dwelling for hordes) + return town->town->buildings.at(str->building->getBase()); + + return str->building; +} + +bool CBuildingRect::operator<(const CBuildingRect & p2) const +{ + return (str->pos.z) < (p2.str->pos.z); +} + +void CBuildingRect::hover(bool on) +{ + if (!area) + return; + + if(on) + { + if(! parent->selectedBuilding //no building hovered + || (*parent->selectedBuilding)<(*this)) //or we are on top + { + parent->selectedBuilding = this; + GH.statusbar()->write(getSubtitle()); + } + } + else + { + if(parent->selectedBuilding == this) + { + parent->selectedBuilding = nullptr; + GH.statusbar()->clear(); + } + } +} + +void CBuildingRect::clickPressed(const Point & cursorPosition) +{ + if(getBuilding() && area && (parent->selectedBuilding==this)) + { + auto building = getBuilding(); + parent->buildingClicked(building->bid, building->subId, building->upgrade); + } +} + +void CBuildingRect::showPopupWindow(const Point & cursorPosition) +{ + if((!area) || (this!=parent->selectedBuilding) || getBuilding() == nullptr) + return; + + BuildingID bid = getBuilding()->bid; + const CBuilding *bld = town->town->buildings.at(bid); + if (bid < BuildingID::DWELL_FIRST) + { + CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), + std::make_shared(CComponent::building, bld->town->faction->getIndex(), bld->bid)); + } + else + { + int level = ( bid - BuildingID::DWELL_FIRST ) % GameConstants::CREATURES_PER_TOWN; + GH.windows().createAndPushWindow(parent->pos.x+parent->pos.w / 2, parent->pos.y+parent->pos.h /2, town, level); + } +} + +void CBuildingRect::show(Canvas & to) +{ + uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT; + + if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT) + { + setAlpha(255 * stateTimeCounter / stageDelay); + CShowableAnim::show(to); + } + else + { + setAlpha(255); + CShowableAnim::show(to); + } + + if(border && stateTimeCounter > BUILDING_APPEAR_TIMEPOINT) + { + if(stateTimeCounter >= BUILD_ANIMATION_FINISHED_TIMEPOINT) + { + if(parent->selectedBuilding == this) + to.draw(border, pos.topLeft()); + return; + } + + auto darkBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 0.5f, 0.5f, 0.5f ); + auto lightBorder = ColorFilter::genRangeShifter(0.f, 0.f, 0.f, 2.0f, 2.0f, 2.0f ); + auto baseBorder = ColorFilter::genEmptyShifter(); + + float progress = float(stateTimeCounter % stageDelay) / stageDelay; + + if (stateTimeCounter < BUILDING_WHITE_BORDER_TIMEPOINT) + border->adjustPalette(ColorFilter::genInterpolated(lightBorder, darkBorder, progress), 0); + else + if (stateTimeCounter < BUILDING_YELLOW_BORDER_TIMEPOINT) + border->adjustPalette(ColorFilter::genInterpolated(darkBorder, baseBorder, progress), 0); + else + border->adjustPalette(baseBorder, 0); + + to.draw(border, pos.topLeft()); + } +} + +void CBuildingRect::tick(uint32_t msPassed) +{ + CShowableAnim::tick(msPassed); + stateTimeCounter += msPassed; +} + +void CBuildingRect::showAll(Canvas & to) +{ + if (stateTimeCounter == 0) + return; + + CShowableAnim::showAll(to); + if(!isActive() && parent->selectedBuilding == this && border) + to.draw(border, pos.topLeft()); +} + +std::string CBuildingRect::getSubtitle()//hover text for building +{ + if (!getBuilding()) + return ""; + + int bid = getBuilding()->bid; + + if (bid<30)//non-dwellings - only buiding name + return town->town->buildings.at(getBuilding()->bid)->getNameTranslated(); + else//dwellings - recruit %creature% + { + auto & availableCreatures = town->creatures[(bid-30)%GameConstants::CREATURES_PER_TOWN].second; + if(availableCreatures.size()) + { + int creaID = availableCreatures.back();//taking last of available creatures + return CGI->generaltexth->allTexts[16] + " " + CGI->creh->objects.at(creaID)->getNamePluralTranslated(); + } + else + { + logGlobal->warn("Dwelling with id %d offers no creatures!", bid); + return "#ERROR#"; + } + } +} + +void CBuildingRect::mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) +{ + hover(true); +} + +bool CBuildingRect::receiveEvent(const Point & position, int eventType) const +{ + if (!pos.isInside(position.x, position.y)) + return false; + + if(area && area->isTransparent(position - pos.topLeft())) + return false; + + return CIntObject::receiveEvent(position, eventType); +} + +CDwellingInfoBox::CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level) + : CWindowObject(RCLICK_POPUP, ImagePath::builtin("CRTOINFO"), Point(centerX, centerY)) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + background->colorize(Town->tempOwner); + + const CCreature * creature = CGI->creh->objects.at(Town->creatures.at(level).second.back()); + + title = std::make_shared(80, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getNamePluralTranslated()); + animation = std::make_shared(30, 44, creature, true, true); + + std::string text = std::to_string(Town->creatures.at(level).first); + available = std::make_shared(80,190, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[217] + text); + costPerTroop = std::make_shared(80, 227, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[346]); + + for(int i = 0; i(i); + if(creature->getRecruitCost(res)) + { + resPicture.push_back(std::make_shared(AnimationPath::builtin("RESOURCE"), i, 0, 0, 0)); + resAmount.push_back(std::make_shared(0,0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(creature->getRecruitCost(res)))); + } + } + + int posY = 238; + int posX = pos.w/2 - (int)resAmount.size() * 25 + 5; + for (size_t i=0; imoveBy(Point(posX, posY)); + resAmount[i]->moveBy(Point(posX+16, posY+43)); + posX += 50; + } +} + +CDwellingInfoBox::~CDwellingInfoBox() = default; + +CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroSlots * Owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + owner = Owner; + pos.x += x; + pos.y += y; + pos.w = 58; + pos.h = 64; + upg = updown; + + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 0, 0); + portrait->visible = false; + + flag = std::make_shared(AnimationPath::builtin("CREST58"), 0, 0, 0, 0); + flag->visible = false; + + selection = std::make_shared(AnimationPath::builtin("TWCRPORT"), 1, 0); + selection->visible = false; + + set(h); + + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); +} + +CHeroGSlot::~CHeroGSlot() = default; + +void CHeroGSlot::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + return; + } + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + std::string temp; + if(hero) + { + if(isSelected())//view NNN + { + temp = CGI->generaltexth->tcommands[4]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + else if(other->hero && other->isSelected())//exchange + { + temp = CGI->generaltexth->tcommands[7]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); + } + else// select NNN (in ZZZ) + { + if(upg)//down - visiting + { + temp = CGI->generaltexth->tcommands[32]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + else //up - garrison + { + temp = CGI->generaltexth->tcommands[12]; + boost::algorithm::replace_first(temp,"%s",hero->getNameTranslated()); + } + } + } + else //we are empty slot + { + if(other->isSelected() && other->hero) //move NNNN + { + temp = CGI->generaltexth->tcommands[6]; + boost::algorithm::replace_first(temp,"%s",other->hero->getNameTranslated()); + } + else //empty + { + temp = CGI->generaltexth->allTexts[507]; + } + } + if(temp.size()) + GH.statusbar()->write(temp); +} + +void CHeroGSlot::clickPressed(const Point & cursorPosition) +{ + std::shared_ptr other = upg ? owner->garrisonedHero : owner->visitingHero; + + owner->garr->setSplittingMode(false); + owner->garr->selectSlot(nullptr); + + if(hero && isSelected()) + { + setHighlight(false); + LOCPLINT->openHeroWindow(hero); + } + else if(other->hero && other->isSelected()) + { + owner->swapArmies(); + } + else if(hero) + { + setHighlight(true); + owner->garr->selectSlot(nullptr); + redraw(); + } + + //refresh statusbar + hover(false); + hover(true); +} + +void CHeroGSlot::showPopupWindow(const Point & cursorPosition) +{ + if(hero) + { + GH.windows().createAndPushWindow(Point(pos.x + 175, pos.y + 100), hero); + } +} + +void CHeroGSlot::deactivate() +{ + selection->visible = false; + CIntObject::deactivate(); +} + +bool CHeroGSlot::isSelected() const +{ + return selection->visible; +} + +void CHeroGSlot::setHighlight(bool on) +{ + selection->visible = on; + + if(owner->garrisonedHero->hero && owner->visitingHero->hero) //two heroes in town + { + for(auto & elem : owner->garr->splitButtons) //splitting enabled when slot higlighted + elem->block(!on); + } +} + +void CHeroGSlot::set(const CGHeroInstance * newHero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + hero = newHero; + + selection->visible = false; + portrait->visible = false; + flag->visible = false; + + if(newHero) + { + portrait->visible = true; + portrait->setFrame(newHero->getIconIndex()); + } + else if(!upg && owner->showEmpty) //up garrison + { + flag->visible = true; + flag->setFrame(LOCPLINT->castleInt->town->getOwner().getNum()); + } +} + +HeroSlots::HeroSlots(const CGTownInstance * Town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty): + showEmpty(ShowEmpty), + town(Town), + garr(Garrison) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + garrisonedHero = std::make_shared(garrPos.x, garrPos.y, 0, town->garrisonHero, this); + visitingHero = std::make_shared(visitPos.x, visitPos.y, 1, town->visitingHero, this); +} + +HeroSlots::~HeroSlots() = default; + +void HeroSlots::update() +{ + garrisonedHero->set(town->garrisonHero); + visitingHero->set(town->visitingHero); +} + +void HeroSlots::splitClicked() +{ + if(!!town->visitingHero && town->garrisonHero && (visitingHero->isSelected() || garrisonedHero->isSelected())) + { + LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id); + } +} + +void HeroSlots::swapArmies() +{ + bool allow = true; + + //moving hero out of town - check if it is allowed + if (town->garrisonHero) + { + if (!town->visitingHero && LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + { + std::string text = CGI->generaltexth->translate("core.genrltxt.18"); //You already have %d adventuring heroes under your command. + boost::algorithm::replace_first(text,"%d",std::to_string(LOCPLINT->cb->howManyHeroes(false))); + + LOCPLINT->showInfoDialog(text, std::vector>(), soundBase::sound_todo); + allow = false; + } + else if (town->garrisonHero->stacksCount() == 0) + { + //This hero has no creatures. A hero must have creatures before he can brave the dangers of the countryside. + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.19"), {}, soundBase::sound_todo); + allow = false; + } + } + + if(!town->garrisonHero && town->visitingHero) //visiting => garrison, merge armies: town army => hero army + { + if(!town->visitingHero->canBeMergedWith(*town)) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[275], std::vector>(), soundBase::sound_todo); + allow = false; + } + } + + garrisonedHero->setHighlight(false); + visitingHero->setHighlight(false); + + if (allow) + LOCPLINT->cb->swapGarrisonHero(town); +} + +class SORTHELP +{ +public: + bool operator() (const CIntObject * a, const CIntObject * b) + { + auto b1 = dynamic_cast(a); + auto b2 = dynamic_cast(b); + + if(!b1 && !b2) + return intptr_t(a) < intptr_t(b); + if(b1 && !b2) + return false; + if(!b1 && b2) + return true; + + return (*b1)<(*b2); + } +}; + +SORTHELP buildSorter; + +CCastleBuildings::CCastleBuildings(const CGTownInstance* Town): + town(Town), + selectedBuilding(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + background = std::make_shared(town->town->clientInfo.townBackground); + background->needRefresh = true; + background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE); + pos.w = background->pos.w; + pos.h = background->pos.h; + + recreate(); +} + +CCastleBuildings::~CCastleBuildings() = default; + +void CCastleBuildings::recreate() +{ + selectedBuilding = nullptr; + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + buildings.clear(); + groups.clear(); + + //Generate buildings list + + auto buildingsCopy = town->builtBuildings;// a bit modified copy of built buildings + + if(vstd::contains(town->builtBuildings, BuildingID::SHIPYARD)) + { + auto bayPos = town->bestLocation(); + if(!bayPos.valid()) + logGlobal->warn("Shipyard in non-coastal town!"); + std::vector vobjs = LOCPLINT->cb->getVisitableObjs(bayPos, false); + //there is visitable obj at shipyard output tile and it's a boat or hero (on boat) + if(!vobjs.empty() && (vobjs.front()->ID == Obj::BOAT || vobjs.front()->ID == Obj::HERO)) + { + buildingsCopy.insert(BuildingID::SHIP); + } + } + + for(const CStructure * structure : town->town->clientInfo.structures) + { + if(!structure->building) + { + buildings.push_back(std::make_shared(this, town, structure)); + continue; + } + if(vstd::contains(buildingsCopy, structure->building->bid)) + { + groups[structure->building->getBase()].push_back(structure); + } + } + + for(auto & entry : groups) + { + const CBuilding * build = town->town->buildings.at(entry.first); + + const CStructure * toAdd = *boost::max_element(entry.second, [=](const CStructure * a, const CStructure * b) + { + return build->getDistance(a->building->bid) < build->getDistance(b->building->bid); + }); + + buildings.push_back(std::make_shared(this, town, toAdd)); + } + + boost::sort(children, buildSorter); //TODO: create building in blit order +} + +void CCastleBuildings::addBuilding(BuildingID building) +{ + //FIXME: implement faster method without complete recreation of town + BuildingID base = town->town->buildings.at(building)->getBase(); + + recreate(); + + auto & structures = groups.at(base); + + for(auto buildingRect : buildings) + { + if(vstd::contains(structures, buildingRect->str)) + { + //reset animation + if(structures.size() == 1) + buildingRect->stateTimeCounter = 0; // transparency -> fully visible stage + else + buildingRect->stateTimeCounter = CBuildingRect::BUILDING_APPEAR_TIMEPOINT; // already in fully visible stage + break; + } + } +} + +void CCastleBuildings::removeBuilding(BuildingID building) +{ + //FIXME: implement faster method without complete recreation of town + recreate(); +} + +const CGHeroInstance * CCastleBuildings::getHero() +{ + if(town->visitingHero) + return town->visitingHero; + else + return town->garrisonHero; +} + +void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) +{ + logGlobal->trace("You've clicked on %d", (int)building.toEnum()); + const CBuilding *b = town->town->buildings.find(building)->second; + + if(building >= BuildingID::DWELL_FIRST) + { + enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN); + } + else + { + switch(building) + { + case BuildingID::MAGES_GUILD_1: + case BuildingID::MAGES_GUILD_2: + case BuildingID::MAGES_GUILD_3: + case BuildingID::MAGES_GUILD_4: + case BuildingID::MAGES_GUILD_5: + enterMagesGuild(); + break; + + case BuildingID::TAVERN: + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + break; + + case BuildingID::SHIPYARD: + if(town->shipyardStatus() == IBoatGenerator::GOOD) + LOCPLINT->showShipyardDialog(town); + else if(town->shipyardStatus() == IBoatGenerator::BOAT_ALREADY_BUILT) + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); + break; + + case BuildingID::FORT: + case BuildingID::CITADEL: + case BuildingID::CASTLE: + GH.windows().createAndPushWindow(town); + break; + + case BuildingID::VILLAGE_HALL: + case BuildingID::CITY_HALL: + case BuildingID::TOWN_HALL: + case BuildingID::CAPITOL: + enterTownHall(); + break; + + case BuildingID::MARKETPLACE: + // can't use allied marketplace + if (town->getOwner() == LOCPLINT->playerID) + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); + else + enterBuilding(building); + break; + + case BuildingID::BLACKSMITH: + enterBlacksmith(town->town->warMachine); + break; + + case BuildingID::SHIP: + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[51]); //Cannot build another boat + break; + + case BuildingID::SPECIAL_1: + case BuildingID::SPECIAL_2: + case BuildingID::SPECIAL_3: + switch(subID) + { + case BuildingSubID::NONE: + enterBuilding(building); + break; + + case BuildingSubID::MYSTIC_POND: + enterFountain(building, subID, upgrades); + break; + + case BuildingSubID::ARTIFACT_MERCHANT: + if(town->visitingHero) + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT); + else + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. + break; + + case BuildingSubID::FOUNTAIN_OF_FORTUNE: + enterFountain(building, subID, upgrades); + break; + + case BuildingSubID::FREELANCERS_GUILD: + if(getHero()) + GH.windows().createAndPushWindow(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE); + else + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s. + break; + + case BuildingSubID::MAGIC_UNIVERSITY: + if (getHero()) + GH.windows().createAndPushWindow(getHero(), town, nullptr); + else + enterBuilding(building); + break; + + case BuildingSubID::BROTHERHOOD_OF_SWORD: + if(upgrades == BuildingID::TAVERN) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + else + enterBuilding(building); + break; + + case BuildingSubID::CASTLE_GATE: + enterCastleGate(); + break; + + case BuildingSubID::CREATURE_TRANSFORMER: //Skeleton Transformer + GH.windows().createAndPushWindow(town, getHero(), nullptr); + break; + + case BuildingSubID::PORTAL_OF_SUMMONING: + if (town->creatures[GameConstants::CREATURES_PER_TOWN].second.empty())//No creatures + LOCPLINT->showInfoDialog(CGI->generaltexth->tcommands[30]); + else + enterDwelling(GameConstants::CREATURES_PER_TOWN); + break; + + case BuildingSubID::BALLISTA_YARD: + enterBlacksmith(ArtifactID::BALLISTA); + break; + + default: + enterBuilding(building); + break; + } + break; + + default: + enterBuilding(building); + break; + } + } +} + +void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) +{ + const CGHeroInstance *hero = town->visitingHero; + if(!hero) + { + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % town->town->buildings.find(BuildingID::BLACKSMITH)->second->getNameTranslated())); + return; + } + auto art = artifactID.toArtifact(); + + int price = art->getPrice(); + bool possible = LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= price; + if(possible) + { + for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO)) + { + if(hero->getArt(slot) == nullptr) + { + possible = true; + break; + } + else + { + possible = false; + } + } + } + CreatureID cre = art->getWarMachine(); + GH.windows().createAndPushWindow(possible, cre, artifactID, hero->id); +} + +void CCastleBuildings::enterBuilding(BuildingID building) +{ + std::vector> comps(1, std::make_shared(CComponent::building, town->subID, building)); + LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); +} + +void CCastleBuildings::enterCastleGate() +{ + if (!town->visitingHero) + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[126]); + return;//only visiting hero can use castle gates + } + std::vector availableTowns; + std::vector Towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & Town : Towns) + { + const CGTownInstance *t = Town; + if (t->id != this->town->id && t->visitingHero == nullptr && //another town, empty and this is + t->town->faction->getId() == town->town->faction->getId() && //the town of the same faction + t->hasBuilt(BuildingSubID::CASTLE_GATE)) //and the town has a castle gate + { + availableTowns.push_back(t->id.getNum());//add to the list + } + } + auto gateIcon = std::make_shared(town->town->clientInfo.buildingsIcons, BuildingID::CASTLE_GATE);//will be deleted by selection window + GH.windows().createAndPushWindow(availableTowns, gateIcon, CGI->generaltexth->jktexts[40], + CGI->generaltexth->jktexts[41], std::bind (&CCastleInterface::castleTeleport, LOCPLINT->castleInt, _1)); +} + +void CCastleBuildings::enterDwelling(int level) +{ + if (level < 0 || level >= town->creatures.size() || town->creatures[level].second.empty()) + { + assert(0); + logGlobal->error("Attempt to enter into invalid dwelling of level %d in town %s (%s)", level, town->getNameTranslated(), town->town->faction->getNameTranslated()); + return; + } + + auto recruitCb = [=](CreatureID id, int count) + { + LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); + }; + GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, -87); +} + +void CCastleBuildings::enterToTheQuickRecruitmentWindow() +{ + const auto beginIt = town->creatures.cbegin(); + const auto afterLastIt = town->creatures.size() > GameConstants::CREATURES_PER_TOWN + ? std::next(beginIt, GameConstants::CREATURES_PER_TOWN) + : town->creatures.cend(); + const auto hasSomeoneToRecruit = std::any_of(beginIt, afterLastIt, + [](const auto & creatureInfo) { return creatureInfo.first > 0; }); + if(hasSomeoneToRecruit) + GH.windows().createAndPushWindow(town, pos); + else + CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.townHall.noCreaturesToRecruit"), {}); +} + +void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) +{ + std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); + std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); + std::string hasNotProduced; + std::string hasProduced; + + if(this->town->town->faction->getIndex() == ETownType::RAMPART) + { + hasNotProduced = CGI->generaltexth->allTexts[677]; + hasProduced = CGI->generaltexth->allTexts[678]; + } + else + { + auto buildingName = town->town->getSpecialBuilding(subID)->getNameTranslated(); + + hasNotProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasNotProduced")); + hasProduced = std::string(CGI->generaltexth->translate("vcmi.townHall.hasProduced")); + boost::algorithm::replace_first(hasNotProduced, "%s", buildingName); + boost::algorithm::replace_first(hasProduced, "%s", buildingName); + } + + bool isMysticPondOrItsUpgrade = subID == BuildingSubID::MYSTIC_POND + || (upgrades != BuildingID::NONE + && town->town->buildings.find(BuildingID(upgrades))->second->subId == BuildingSubID::MYSTIC_POND); + + if(upgrades != BuildingID::NONE) + descr += "\n\n"+town->town->buildings.find(BuildingID(upgrades))->second->getDescriptionTranslated(); + + if(isMysticPondOrItsUpgrade) //for vanila Rampart like towns + { + if(town->bonusValue.first == 0) //Mystic Pond produced nothing; + descr += "\n\n" + hasNotProduced; + else //Mystic Pond produced something; + { + descr += "\n\n" + hasProduced; + boost::algorithm::replace_first(descr,"%s",CGI->generaltexth->restypes[town->bonusValue.first]); + boost::algorithm::replace_first(descr,"%d",std::to_string(town->bonusValue.second)); + } + } + LOCPLINT->showInfoDialog(descr, comps); +} + +void CCastleBuildings::enterMagesGuild() +{ + const CGHeroInstance *hero = getHero(); + + if(hero && !hero->hasSpellbook()) //hero doesn't have spellbok + { + const StartInfo *si = LOCPLINT->cb->getStartInfo(); + // it would be nice to find a way to move this hack to config/mapOverrides.json + if(si && si->campState && si->campState && // We're in campaign, + (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", + (hero->subID == 45)) // and the hero is Yog (based on Solmyr) + { + // "Yog has given up magic in all its forms..." + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); + } + else if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < 500) //not enough gold to buy spellbook + { + openMagesGuild(); + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[213]); + } + else + { + CFunctionList onYes = [this](){ openMagesGuild(); }; + CFunctionList onNo = onYes; + onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; + std::vector> components(1, std::make_shared(CComponent::artifact,ArtifactID::SPELLBOOK,0)); + + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); + } + } + else + { + openMagesGuild(); + } +} + +void CCastleBuildings::enterTownHall() +{ + if(town->visitingHero && town->visitingHero->hasArt(ArtifactID::GRAIL) && + !vstd::contains(town->builtBuildings, BuildingID::GRAIL)) //hero has grail, but town does not have it + { + if(!vstd::contains(town->forbiddenBuildings, BuildingID::GRAIL)) + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[597], //Do you wish this to be the permanent home of the Grail? + [&](){ LOCPLINT->cb->buildBuilding(town, BuildingID::GRAIL); }, + [&](){ openTownHall(); }); + } + else + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[673]); + assert(GH.windows().topWindow() != nullptr); + GH.windows().topWindow()->buttons[0]->addCallback(std::bind(&CCastleBuildings::openTownHall, this)); + } + } + else + { + openTownHall(); + } +} + +void CCastleBuildings::openMagesGuild() +{ + auto mageGuildBackground = LOCPLINT->castleInt->town->town->clientInfo.guildBackground; + GH.windows().createAndPushWindow(LOCPLINT->castleInt, mageGuildBackground); +} + +void CCastleBuildings::openTownHall() +{ + GH.windows().createAndPushWindow(town); +} + +CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable): + town(Town), + level(Level), + showAvailable(_showAvailable) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos += position; + + if(town->creatures.size() <= level || town->creatures[level].second.empty()) + { + level = -1; + return;//No creature + } + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + + ui32 creatureID = town->creatures[level].second.back(); + creature = CGI->creh->objects[creatureID]; + + picture = std::make_shared(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, 8, 0); + + std::string value; + if(showAvailable) + value = std::to_string(town->creatures[level].first); + else + value = std::string("+") + std::to_string(town->creatureGrowth(level)); + + if(compact) + { + label = std::make_shared(40, 32, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, value); + pos.x += 8; + pos.w = 32; + pos.h = 32; + } + else + { + label = std::make_shared(24, 40, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, value); + pos.w = 48; + pos.h = 48; + } +} + +void CCreaInfo::update() +{ + if(label) + { + std::string value; + if(showAvailable) + value = std::to_string(town->creatures[level].first); + else + value = std::string("+") + std::to_string(town->creatureGrowth(level)); + + if(value != label->getText()) + label->setText(value); + } +} + +void CCreaInfo::hover(bool on) +{ + std::string message = CGI->generaltexth->allTexts[588]; + boost::algorithm::replace_first(message, "%s", creature->getNamePluralTranslated()); + + if(on) + { + GH.statusbar()->write(message); + } + else + { + GH.statusbar()->clearIfMatching(message); + } +} + +void CCreaInfo::clickPressed(const Point & cursorPosition) +{ + int offset = LOCPLINT->castleInt? (-87) : 0; + auto recruitCb = [=](CreatureID id, int count) + { + LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); + }; + GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, offset); +} + +std::string CCreaInfo::genGrowthText() +{ + GrowthInfo gi = town->getGrowthInfo(level); + std::string descr = boost::str(boost::format(CGI->generaltexth->allTexts[589]) % creature->getNameSingularTranslated() % gi.totalGrowth()); + + for(const GrowthInfo::Entry & entry : gi.entries) + descr +="\n" + entry.description; + + return descr; +} + +void CCreaInfo::showPopupWindow(const Point & cursorPosition) +{ + if (showAvailable) + GH.windows().createAndPushWindow(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level); + else + CRClickPopup::createAndPush(genGrowthText(), std::make_shared(CComponent::creature, creature->getId())); +} + +bool CCreaInfo::getShowAvailable() +{ + return showAvailable; +} + +CTownInfo::CTownInfo(int posX, int posY, const CGTownInstance * Town, bool townHall) + : town(Town), + building(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + addUsedEvents(SHOW_POPUP | HOVER); + pos.x += posX; + pos.y += posY; + int buildID; + + if(townHall) + { + buildID = 10 + town->hallLevel(); + picture = std::make_shared(AnimationPath::builtin("ITMTL.DEF"), town->hallLevel()); + } + else + { + buildID = 6 + town->fortLevel(); + if(buildID == 6) + return;//FIXME: suspicious statement, fix or comment + picture = std::make_shared(AnimationPath::builtin("ITMCL.DEF"), town->fortLevel()-1); + } + building = town->town->buildings.at(BuildingID(buildID)); + pos = picture->pos; +} + +void CTownInfo::hover(bool on) +{ + if(on) + { + if(building ) + GH.statusbar()->write(building->getNameTranslated()); + } + else + { + GH.statusbar()->clear(); + } +} + +void CTownInfo::showPopupWindow(const Point & cursorPosition) +{ + if(building) + { + auto c = std::make_shared(CComponent::building, building->town->faction->getIndex(), building->bid); + CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); + } +} + +CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from): + CStatusbarWindow(PLAYER_COLORED | BORDERED), + town(Town) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + LOCPLINT->castleInt = this; + addUsedEvents(KEYBOARD); + + builds = std::make_shared(town); + panel = std::make_shared(ImagePath::builtin("TOWNSCRN"), 0, builds->pos.h); + panel->colorize(LOCPLINT->playerID); + pos.w = panel->pos.w; + pos.h = builds->pos.h + panel->pos.h; + center(); + updateShadow(); + + garr = std::make_shared(Point(305, 387), 4, Point(0,96), town->getUpperArmy(), town->visitingHero); + garr->setRedrawParent(true); + + heroes = std::make_shared(town, Point(241, 387), Point(241, 483), garr, true); + title = std::make_shared(85, 387, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, town->getNameTranslated()); + income = std::make_shared(195, 443, FONT_SMALL, ETextAlignment::CENTER); + icon = std::make_shared(AnimationPath::builtin("ITPT"), 0, 0, 15, 387); + + exit = std::make_shared(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); + exit->setImageOrder(4, 5, 6, 7); + + auto split = std::make_shared(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [&]() + { + garr->splitClick(); + heroes->splitClicked(); + }); + garr->addSplitBtn(split); + + Rect barRect(9, 182, 732, 18); + auto statusbarBackground = std::make_shared(panel->getSurface(), barRect, 9, 555); + statusbar = CGStatusBar::create(statusbarBackground); + resdatabar = std::make_shared(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78); + + townlist = std::make_shared(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() ); + townlist->setScrollUpButton( std::make_shared( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"))); + townlist->setScrollDownButton( std::make_shared( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"))); + + if(from) + townlist->select(from); + + townlist->select(town); //this will scroll list to select current town + townlist->onSelect = std::bind(&CCastleInterface::townChange, this); + + recreateIcons(); + if (!from) + adventureInt->onAudioPaused(); + CCS->musich->playMusic(town->town->clientInfo.musicTheme, true, false); +} + +CCastleInterface::~CCastleInterface() +{ + // resume map audio if: + // adventureInt exists (may happen on exiting client with open castle interface) + // castleInt has not been replaced (happens on switching between towns inside castle interface) + if (adventureInt && LOCPLINT->castleInt == this) + adventureInt->onAudioResumed(); + if(LOCPLINT->castleInt == this) + LOCPLINT->castleInt = nullptr; +} + +void CCastleInterface::updateGarrisons() +{ + garr->setArmy(town->getUpperArmy(), EGarrisonType::UPPER); + garr->setArmy(town->visitingHero, EGarrisonType::LOWER); + garr->recreateSlots(); + heroes->update(); + + redraw(); +} + +void CCastleInterface::close() +{ + if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town + { + if(town->visitingHero && town->visitingHero->tempOwner == LOCPLINT->playerID) + LOCPLINT->localState->setSelection(town->visitingHero); + else + LOCPLINT->localState->setSelection(town); + } + CWindowObject::close(); +} + +void CCastleInterface::castleTeleport(int where) +{ + const CGTownInstance * dest = LOCPLINT->cb->getTown(ObjectInstanceID(where)); + LOCPLINT->localState->setSelection(town->visitingHero);//according to assert(ho == adventureInt->selection) in the eraseCurrentPathOf + LOCPLINT->cb->teleportHero(town->visitingHero, dest); + LOCPLINT->localState->erasePath(town->visitingHero); +} + +void CCastleInterface::townChange() +{ + //TODO: do not recreate window + const CGTownInstance * dest = LOCPLINT->localState->getOwnedTown(townlist->getSelectedIndex()); + const CGTownInstance * town = this->town;// "this" is going to be deleted + if ( dest == town ) + return; + close(); + GH.windows().createAndPushWindow(dest, town); +} + +void CCastleInterface::addBuilding(BuildingID bid) +{ + deactivate(); + builds->addBuilding(bid); + recreateIcons(); + activate(); + redraw(); +} + +void CCastleInterface::removeBuilding(BuildingID bid) +{ + deactivate(); + builds->removeBuilding(bid); + recreateIcons(); + activate(); + redraw(); +} + +void CCastleInterface::recreateIcons() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)]; + + icon->setFrame(iconIndex); + TResources townIncome = town->dailyIncome(); + income->setText(std::to_string(townIncome[EGameResID::GOLD])); + + hall = std::make_shared(80, 413, town, true); + fort = std::make_shared(122, 413, town, false); + + fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); + fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setAnimateLonelyFrame(true); + + fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); + fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); + fastArmyPurchase->setAnimateLonelyFrame(true); + + fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() + { + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + if (town->getOwner() == LOCPLINT->playerID) + GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); + } + }); + + fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() + { + if(town->builtBuildings.count(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + }); + + creainfo.clear(); + + bool compactCreatureInfo = useCompactCreatureBox(); + bool useAvailableCreaturesForLabel = useAvailableAmountAsCreatureLabel(); + + for(size_t i=0; i<4; i++) + creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 459), town, (int)i, compactCreatureInfo, useAvailableCreaturesForLabel)); + + + for(size_t i=0; i<4; i++) + creainfo.push_back(std::make_shared(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel)); + +} + +void CCastleInterface::keyPressed(EShortcut key) +{ + switch(key) + { + case EShortcut::MOVE_UP: + townlist->selectPrev(); + break; + case EShortcut::MOVE_DOWN: + townlist->selectNext(); + break; + case EShortcut::TOWN_SWAP_ARMIES: + heroes->swapArmies(); + break; + case EShortcut::TOWN_OPEN_TAVERN: + if(town->hasBuilt(BuildingID::TAVERN)) + LOCPLINT->showTavernWindow(town, nullptr, QueryID::NONE); + break; + default: + break; + } +} + +void CCastleInterface::creaturesChangedEventHandler() +{ + for(auto creatureInfoBox : creainfo) + { + if(creatureInfoBox->getShowAvailable()) + { + creatureInfoBox->update(); + } + } +} + +CHallInterface::CBuildingBox::CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building): + town(Town), + building(Building) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos.x += x; + pos.y += y; + pos.w = 154; + pos.h = 92; + + state = LOCPLINT->cb->canBuildStructure(town, building->bid); + + static int panelIndex[12] = + { + 3, 3, 3, 0, 0, 2, 2, 1, 2, 2, 3, 3 + }; + static int iconIndex[12] = + { + -1, -1, -1, 0, 0, 1, 2, -1, 1, 1, -1, -1 + }; + + icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 2, 2); + header = std::make_shared(AnimationPath::builtin("TPTHBAR"), panelIndex[static_cast(state)], 0, 1, 73); + if(iconIndex[static_cast(state)] >=0) + mark = std::make_shared(AnimationPath::builtin("TPTHCHK"), iconIndex[static_cast(state)], 0, 136, 56); + name = std::make_shared(75, 81, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, building->getNameTranslated()); + + //todo: add support for all possible states + if(state >= EBuildingState::BUILDING_ERROR) + state = EBuildingState::FORBIDDEN; +} + +void CHallInterface::CBuildingBox::hover(bool on) +{ + if(on) + { + std::string toPrint; + if(state==EBuildingState::PREREQUIRES || state == EBuildingState::MISSING_BASE) + toPrint = CGI->generaltexth->hcommands[5]; + else if(state==EBuildingState::CANT_BUILD_TODAY) + toPrint = CGI->generaltexth->allTexts[223]; + else + toPrint = CGI->generaltexth->hcommands[static_cast(state)]; + boost::algorithm::replace_first(toPrint,"%s",building->getNameTranslated()); + GH.statusbar()->write(toPrint); + } + else + { + GH.statusbar()->clear(); + } +} + +void CHallInterface::CBuildingBox::clickPressed(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(town,building,state,0); +} + +void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(town,building,state,1); +} + +CHallInterface::CHallInterface(const CGTownInstance * Town): + CStatusbarWindow(PLAYER_COLORED | BORDERED, Town->town->clientInfo.hallBackground), + town(Town) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + Rect barRect(5, 556, 740, 18); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 5, 556); + statusbar = CGStatusBar::create(statusbarBackground); + + title = std::make_shared(399, 12, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, town->town->buildings.at(BuildingID(town->hallLevel()+BuildingID::VILLAGE_HALL))->getNameTranslated()); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->hcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN); + + auto & boxList = town->town->clientInfo.hallSlots; + boxes.resize(boxList.size()); + for(size_t row=0; rowtown->buildings.at(buildingID); + if(vstd::contains(town->builtBuildings, buildingID)) + { + building = current; + } + else + { + if(current->mode == CBuilding::BUILD_NORMAL) + { + building = current; + break; + } + } + } + int posX = pos.w/2 - (int)boxList[row].size()*154/2 - ((int)boxList[row].size()-1)*20 + 194*(int)col, + posY = 35 + 104*(int)row; + + if(building) + boxes[row].push_back(std::make_shared(posX, posY, town, building)); + } + } +} + +CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Building, EBuildingState state, bool rightClick): + CStatusbarWindow(PLAYER_COLORED | (rightClick ? RCLICK_POPUP : 0), ImagePath::builtin("TPUBUILD")), + town(Town), + building(Building) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + icon = std::make_shared(town->town->clientInfo.buildingsIcons, building->bid, 0, 125, 50); + auto statusbarBackground = std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26); + statusbar = CGStatusBar::create(statusbarBackground); + + MetaString nameString; + nameString.appendTextID("core.hallinfo.7"); + nameString.replaceTextID(building->getNameTextID()); + + name = std::make_shared(197, 30, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, nameString.toString()); + description = std::make_shared(building->getDescriptionTranslated(), Rect(33, 135, 329, 67), 0, FONT_MEDIUM, ETextAlignment::CENTER); + stateText = std::make_shared(getTextForState(state), Rect(33, 216, 329, 67), 0, FONT_SMALL, ETextAlignment::CENTER); + + //Create components for all required resources + std::vector> components; + + for(int i = 0; iresources[i]) + { + std::string text = std::to_string(building->resources[i]); + int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; + if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; + components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); + } + } + + cost = std::make_shared(components, Rect(25, 300, pos.w - 50, 130)); + + if(!rightClick) + { //normal window + + MetaString tooltipYes; + tooltipYes.appendTextID("core.genrltxt.595"); + tooltipYes.replaceTextID(building->getNameTextID()); + + MetaString tooltipNo; + tooltipNo.appendTextID("core.genrltxt.596"); + tooltipNo.replaceTextID(building->getNameTextID()); + + buy = std::make_shared(Point(45, 446), AnimationPath::builtin("IBUY30"), CButton::tooltip(tooltipYes.toString()), [&](){ buyFunc(); }, EShortcut::GLOBAL_ACCEPT); + buy->setBorderColor(Colors::METALLIC_GOLD); + buy->block(state!=EBuildingState::ALLOWED || LOCPLINT->playerID != town->tempOwner); + + cancel = std::make_shared(Point(290, 445), AnimationPath::builtin("ICANCEL"), CButton::tooltip(tooltipNo.toString()), [&](){ close();}, EShortcut::GLOBAL_CANCEL); + cancel->setBorderColor(Colors::METALLIC_GOLD); + } +} + +void CBuildWindow::buyFunc() +{ + LOCPLINT->cb->buildBuilding(town,building->bid); + GH.windows().popWindows(2); //we - build window and hall screen +} + +std::string CBuildWindow::getTextForState(EBuildingState state) +{ + std::string ret; + if(state < EBuildingState::ALLOWED) + ret = CGI->generaltexth->hcommands[static_cast(state)]; + switch (state) + { + case EBuildingState::ALREADY_PRESENT: + case EBuildingState::CANT_BUILD_TODAY: + case EBuildingState::NO_RESOURCES: + ret.replace(ret.find_first_of("%s"), 2, building->getNameTranslated()); + break; + case EBuildingState::ALLOWED: + return CGI->generaltexth->allTexts[219]; //all prereq. are met + case EBuildingState::PREREQUIRES: + { + auto toStr = [&](const BuildingID build) -> std::string + { + return town->town->buildings.at(build)->getNameTranslated(); + }; + + ret = CGI->generaltexth->allTexts[52]; + ret += "\n" + town->genBuildingRequirements(building->bid).toString(toStr); + break; + } + case EBuildingState::MISSING_BASE: + { + std::string msg = CGI->generaltexth->translate("vcmi.townHall.missingBase"); + ret = boost::str(boost::format(msg) % town->town->buildings.at(building->upgrade)->getNameTranslated()); + break; + } + } + return ret; +} + +LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int min, int max) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x+=size.x; + pos.y+=size.y; + pos.w = size.w; + pos.h = size.h; + init(name, descr, min, max); +} + +LabeledValue::LabeledValue(Rect size, std::string name, std::string descr, int val) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x+=size.x; + pos.y+=size.y; + pos.w = size.w; + pos.h = size.h; + init(name, descr, val, val); +} + +void LabeledValue::init(std::string nameText, std::string descr, int min, int max) +{ + addUsedEvents(HOVER); + hoverText = descr; + std::string valueText; + if(min && max) + { + valueText = std::to_string(min); + if(min != max) + valueText += '-' + std::to_string(max); + } + name = std::make_shared(3, 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, nameText); + value = std::make_shared(pos.w-3, pos.h-2, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, valueText); +} + +void LabeledValue::hover(bool on) +{ + if(on) + { + GH.statusbar()->write(hoverText); + } + else + { + GH.statusbar()->clear(); + } +} + +CFortScreen::CFortScreen(const CGTownInstance * town): + CStatusbarWindow(PLAYER_COLORED | BORDERED, getBgName(town)) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + ui32 fortSize = static_cast(town->creatures.size()); + if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) + fortSize--; + + const CBuilding * fortBuilding = town->town->buildings.at(BuildingID(town->fortLevel()+6)); + title = std::make_shared(400, 12, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE, fortBuilding->getNameTranslated()); + + std::string text = boost::str(boost::format(CGI->generaltexth->fcommands[6]) % fortBuilding->getNameTranslated()); + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(text), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + + std::vector positions = + { + Point(10, 22), Point(404, 22), + Point(10, 155), Point(404,155), + Point(10, 288), Point(404,288) + }; + + if(fortSize == GameConstants::CREATURES_PER_TOWN) + { + positions.push_back(Point(206,421)); + } + else + { + positions.push_back(Point(10, 421)); + positions.push_back(Point(404,421)); + } + + for(ui32 i=0; ibuiltBuildings, dwelling)) + buildingID = BuildingID(BuildingID::DWELL_UP_FIRST+i); + else + buildingID = BuildingID(BuildingID::DWELL_FIRST+i); + } + else + { + buildingID = BuildingID::SPECIAL_3; + } + + recAreas.push_back(std::make_shared(positions[i].x, positions[i].y, town, i)); + } + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + Rect barRect(4, 554, 740, 18); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 4, 554); + statusbar = CGStatusBar::create(statusbarBackground); +} + +ImagePath CFortScreen::getBgName(const CGTownInstance * town) +{ + ui32 fortSize = static_cast(town->creatures.size()); + if(fortSize > GameConstants::CREATURES_PER_TOWN && town->creatures.back().second.empty()) + fortSize--; + + if(fortSize == GameConstants::CREATURES_PER_TOWN) + return ImagePath::builtin("TPCASTL7"); + else + return ImagePath::builtin("TPCASTL8"); +} + +void CFortScreen::creaturesChangedEventHandler() +{ + for(auto & elem : recAreas) + elem->creaturesChangedEventHandler(); + + LOCPLINT->castleInt->creaturesChangedEventHandler(); +} + +CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance * Town, int Level): + town(Town), + level(Level), + availableCount(nullptr) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x +=posX; + pos.y +=posY; + pos.w = 386; + pos.h = 126; + + if(!town->creatures[level].second.empty()) + addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present + + addUsedEvents(SHOW_POPUP); + + icons = std::make_shared(ImagePath::builtin("TPCAINFO"), 261, 3); + + if(getMyBuilding() != nullptr) + { + buildingIcon = std::make_shared(town->town->clientInfo.buildingsIcons, getMyBuilding()->bid, 0, 4, 21); + buildingName = std::make_shared(78, 101, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyBuilding()->getNameTranslated()); + + if(vstd::contains(town->builtBuildings, getMyBuilding()->bid)) + { + ui32 available = town->creatures[level].first; + std::string availableText = CGI->generaltexth->allTexts[217]+ std::to_string(available); + availableCount = std::make_shared(78, 119, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, availableText); + } + } + + if(getMyCreature() != nullptr) + { + hoverText = boost::str(boost::format(CGI->generaltexth->tcommands[21]) % getMyCreature()->getNamePluralTranslated()); + new CCreaturePic(159, 4, getMyCreature(), false); + new CLabel(78, 11, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, getMyCreature()->getNamePluralTranslated()); + + Rect sizes(287, 4, 96, 18); + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[190], CGI->generaltexth->fcommands[0], getMyCreature()->getAttack(false))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[191], CGI->generaltexth->fcommands[1], getMyCreature()->getDefense(false))); + sizes.y+=21; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[199], CGI->generaltexth->fcommands[2], getMyCreature()->getMinDamage(false), getMyCreature()->getMaxDamage(false))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[388], CGI->generaltexth->fcommands[3], getMyCreature()->getMaxHealth())); + sizes.y+=21; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[193], CGI->generaltexth->fcommands[4], getMyCreature()->valOfBonuses(BonusType::STACKS_SPEED))); + sizes.y+=20; + values.push_back(std::make_shared(sizes, CGI->generaltexth->allTexts[194], CGI->generaltexth->fcommands[5], town->creatureGrowth(level))); + } +} + +const CCreature * CFortScreen::RecruitArea::getMyCreature() +{ + if(!town->creatures.at(level).second.empty()) // built + return VLC->creh->objects[town->creatures.at(level).second.back()]; + if(!town->town->creatures.at(level).empty()) // there are creatures on this level + return VLC->creh->objects[town->town->creatures.at(level).front()]; + return nullptr; +} + +const CBuilding * CFortScreen::RecruitArea::getMyBuilding() +{ + BuildingID myID = BuildingID(BuildingID::DWELL_FIRST + level); + + if (level == GameConstants::CREATURES_PER_TOWN) + return town->town->getSpecialBuilding(BuildingSubID::PORTAL_OF_SUMMONING); + + if (!town->town->buildings.count(myID)) + return nullptr; + + const CBuilding * build = town->town->buildings.at(myID); + while (town->town->buildings.count(myID)) + { + if (town->hasBuilt(myID)) + build = town->town->buildings.at(myID); + myID.advance(GameConstants::CREATURES_PER_TOWN); + } + return build; +} + +void CFortScreen::RecruitArea::hover(bool on) +{ + if(on) + GH.statusbar()->write(hoverText); + else + GH.statusbar()->clear(); +} + +void CFortScreen::RecruitArea::creaturesChangedEventHandler() +{ + if(availableCount) + { + std::string availableText = CGI->generaltexth->allTexts[217] + std::to_string(town->creatures[level].first); + availableCount->setText(availableText); + } +} + +void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition) +{ + LOCPLINT->castleInt->builds->enterDwelling(level); +} + +void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition) +{ + if (getMyCreature() != nullptr) + GH.windows().createAndPushWindow(getMyCreature(), true); +} + +CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner, const ImagePath & imagename) + : CStatusbarWindow(BORDERED, imagename) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + window = std::make_shared(owner->town->town->clientInfo.guildWindow, 332, 76); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + Rect barRect(7, 556, 737, 18); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 7, 556); + statusbar = CGStatusBar::create(statusbarBackground); + + exit = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1.DEF"), CButton::tooltip(CGI->generaltexth->allTexts[593]), [&](){ close(); }, EShortcut::GLOBAL_RETURN); + + static const std::vector > positions = + { + {Point(222,445), Point(312,445), Point(402,445), Point(520,445), Point(610,445), Point(700,445)}, + {Point(48,53), Point(48,147), Point(48,241), Point(48,335), Point(48,429)}, + {Point(570,82), Point(672,82), Point(570,157), Point(672,157)}, + {Point(183,42), Point(183,148), Point(183,253)}, + {Point(491,325), Point(591,325)} + }; + + for(size_t i=0; itown->town->mageLevel; i++) + { + size_t spellCount = owner->town->spellsAtLevel((int)i+1,false); //spell at level with -1 hmmm? + for(size_t j=0; jtown->mageGuildLevel() && owner->town->spells[i].size()>j) + spells.push_back(std::make_shared(positions[i][j], CGI->spellh->objects[owner->town->spells[i][j]])); + else + emptyScrolls.push_back(std::make_shared(AnimationPath::builtin("TPMAGES.DEF"), 1, 0, positions[i][j].x, positions[i][j].y)); + } + } +} + +CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) + : spell(Spell) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos += position; + image = std::make_shared(AnimationPath::builtin("SPELLSCR"), spell->id); + pos = image->pos; +} + +void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) +{ + LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); +} + +void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); +} + +void CMageGuildScreen::Scroll::hover(bool on) +{ + if(on) + GH.statusbar()->write(spell->getNameTranslated()); + else + GH.statusbar()->clear(); + +} + +CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid): + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSMITH")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + Rect barRect(8, pos.h - 26, pos.w - 16, 19); + + auto statusbarBackground = std::make_shared(background->getSurface(), barRect, 8, pos.h - 26); + statusbar = CGStatusBar::create(statusbarBackground); + + animBG = std::make_shared(ImagePath::builtin("TPSMITBK"), 64, 50); + animBG->needRefresh = true; + + const CCreature * creature = CGI->creh->objects[creMachineID]; + anim = std::make_shared(64, 50, creature->animDefName); + anim->clipRect(113,125,200,150); + + MetaString titleString; + titleString.appendTextID("core.genrltxt.274"); + titleString.replaceTextID(creature->getNameSingularTextID()); + + MetaString buyText; + buyText.appendTextID("core.genrltxt.595"); + buyText.replaceTextID(creature->getNameSingularTextID()); + + MetaString cancelText; + cancelText.appendTextID("core.genrltxt.596"); + cancelText.replaceTextID(creature->getNameSingularTextID()); + + std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); + + title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); + costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); + costValue = std::make_shared(165, 292, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, costString); + buy = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30.DEF"), CButton::tooltip(buyText.toString()), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(cancelText.toString()), [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + + if(possible) + buy->addCallback([=](){ LOCPLINT->cb->buyArtifact(LOCPLINT->cb->getHero(hid),aid); }); + else + buy->block(true); + + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 148, 244); +} diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 9e9e0b48d..dde0bffa1 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -1,409 +1,409 @@ -/* - * CCastleInterface.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 "../windows/CWindowObject.h" -#include "../widgets/Images.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBuilding; -class CGTownInstance; -class CSpell; -struct CStructure; -class CGHeroInstance; -class CCreature; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CCastleBuildings; -class CCreaturePic; -class CGStatusBar; -class CLabel; -class CMinorResDataBar; -class CPicture; -class CResDataBar; -class CTextBox; -class CTownList; -class CGarrisonInt; -class CComponent; -class CComponentBox; -class LRClickableArea; - -/// Building "button" -class CBuildingRect : public CShowableAnim -{ - std::string getSubtitle(); -public: - enum EBuildingCreationAnimationPhases : uint32_t - { - BUILDING_APPEAR_TIMEPOINT = 500, //500 msec building appears: 0->100% transparency - BUILDING_WHITE_BORDER_TIMEPOINT = 900, //400 msec border glows from white to yellow - BUILDING_YELLOW_BORDER_TIMEPOINT = 1100, //200 msec border glows from yellow to normal (dark orange) - BUILD_ANIMATION_FINISHED_TIMEPOINT = 2100, // 1000msec once border is back to yellow nothing happens (this stage is basically removed by HD Mod) - - BUILDING_FRAME_TIME = 150 // confirmed H3 timing: 150 ms for each building animation frame - }; - - /// returns building associated with this structure - const CBuilding * getBuilding(); - - CCastleBuildings * parent; - const CGTownInstance * town; - const CStructure* str; - std::shared_ptr border; - std::shared_ptr area; - - ui32 stateTimeCounter;//For building construction - current stage in animation - - CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str); - bool operator<(const CBuildingRect & p2) const; - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) override; - bool receiveEvent(const Point & position, int eventType) const override; - void tick(uint32_t msPassed) override; - void show(Canvas & to) override; - void showAll(Canvas & to) override; -}; - -/// Dwelling info box - right-click screen for dwellings -class CDwellingInfoBox : public CWindowObject -{ - std::shared_ptr title; - std::shared_ptr animation; - std::shared_ptr available; - std::shared_ptr costPerTroop; - - std::vector> resPicture; - std::vector> resAmount; -public: - CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level); - ~CDwellingInfoBox(); -}; - -class HeroSlots; -/// Hero icon slot -class CHeroGSlot : public CIntObject -{ - std::shared_ptr portrait; - std::shared_ptr flag; - std::shared_ptr selection; //selection border. nullptr if not selected - - HeroSlots * owner; - const CGHeroInstance * hero; - int upg; //0 - up garrison, 1 - down garrison - -public: - CHeroGSlot(int x, int y, int updown, const CGHeroInstance *h, HeroSlots * Owner); - ~CHeroGSlot(); - - bool isSelected() const; - - void setHighlight(bool on); - void set(const CGHeroInstance * newHero); - - void hover (bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void deactivate() override; -}; - -/// Two hero slots that can interact with each other -class HeroSlots : public CIntObject -{ -public: - bool showEmpty; - const CGTownInstance * town; - - std::shared_ptr garr; - std::shared_ptr garrisonedHero; - std::shared_ptr visitingHero; - - HeroSlots(const CGTownInstance * town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty); - ~HeroSlots(); - - void splitClicked(); //for hero meeting only (splitting stacks is handled by garrison int) - void update(); - void swapArmies(); //exchange garrisoned and visiting heroes or move hero to\from garrison -}; - -/// Class for town screen management (town background and structures) -class CCastleBuildings : public CIntObject -{ - std::shared_ptr background; - //List of buildings and structures that can represent them - std::map > groups; - // actual IntObject's visible on screen - std::vector> buildings; - - const CGTownInstance * town; - - const CGHeroInstance* getHero();//Select hero for buildings usage - - void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard - void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages - void enterCastleGate(); - void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains - void enterMagesGuild(); - - void openMagesGuild(); - void openTownHall(); - - void recreate(); -public: - CBuildingRect * selectedBuilding; - - CCastleBuildings(const CGTownInstance * town); - ~CCastleBuildings(); - - void enterDwelling(int level); - void enterTownHall(); - void enterToTheQuickRecruitmentWindow(); - - void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); - void addBuilding(BuildingID building); - void removeBuilding(BuildingID building);//FIXME: not tested!!! -}; - -/// Creature info window -class CCreaInfo : public CIntObject -{ - const CGTownInstance * town; - const CCreature * creature; - int level; - bool showAvailable; - - std::shared_ptr picture; - std::shared_ptr label; - - std::string genGrowthText(); - -public: - CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact=false, bool _showAvailable=false); - - void update(); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - bool getShowAvailable(); -}; - -/// Town hall and fort icons for town screen -class CTownInfo : public CIntObject -{ - const CGTownInstance * town; - const CBuilding * building; -public: - std::shared_ptr picture; - //if (townHall) hall-capital else fort - castle - CTownInfo(int posX, int posY, const CGTownInstance * town, bool townHall); - - void hover(bool on) override; - void showPopupWindow(const Point & cursorPosition) override; -}; - -/// Class which manages the castle window -class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder -{ - std::shared_ptr title; - std::shared_ptr income; - std::shared_ptr icon; - - std::shared_ptr panel; - std::shared_ptr resdatabar; - - std::shared_ptr hall; - std::shared_ptr fort; - - std::shared_ptr exit; - std::shared_ptr split; - std::shared_ptr fastTownHall; - std::shared_ptr fastArmyPurchase; - std::shared_ptr fastMarket; - std::shared_ptr fastTavern; - - std::vector> creainfo;//small icons of creatures (bottom-left corner); - -public: - std::shared_ptr townlist; - - //TODO: move to private - const CGTownInstance * town; - std::shared_ptr heroes; - std::shared_ptr builds; - - std::shared_ptr garr; - - //from - previously selected castle (if any) - CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); - ~CCastleInterface(); - - virtual void updateGarrisons() override; - - void castleTeleport(int where); - void townChange(); - void keyPressed(EShortcut key) override; - - void close() override; - void addBuilding(BuildingID bid); - void removeBuilding(BuildingID bid); - void recreateIcons(); - void creaturesChangedEventHandler(); -}; - -/// Hall window where you can build things -class CHallInterface : public CStatusbarWindow -{ - class CBuildingBox : public CIntObject - { - const CGTownInstance * town; - const CBuilding * building; - - EBuildingState state; - - std::shared_ptr header; - std::shared_ptr icon; - std::shared_ptr mark; - std::shared_ptr name; - public: - CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - }; - const CGTownInstance * town; - - std::vector>> boxes; - std::shared_ptr title; - std::shared_ptr resdatabar; - std::shared_ptr exit; - -public: - CHallInterface(const CGTownInstance * Town); -}; - -/// Window where you can decide to buy a building or not -class CBuildWindow: public CStatusbarWindow -{ - const CGTownInstance * town; - const CBuilding * building; - - std::shared_ptr icon; - std::shared_ptr name; - std::shared_ptr description; - std::shared_ptr stateText; - std::shared_ptr cost; - - std::shared_ptr buy; - std::shared_ptr cancel; - - std::string getTextForState(EBuildingState state); - void buyFunc(); -public: - CBuildWindow(const CGTownInstance *Town, const CBuilding * building, EBuildingState State, bool rightClick); -}; - -//Small class to display -class LabeledValue : public CIntObject -{ - std::string hoverText; - std::shared_ptr name; - std::shared_ptr value; - void init(std::string name, std::string descr, int min, int max); - -public: - LabeledValue(Rect size, std::string name, std::string descr, int min, int max); - LabeledValue(Rect size, std::string name, std::string descr, int val); - void hover(bool on) override; -}; - -/// The fort screen where you can afford units -class CFortScreen : public CStatusbarWindow -{ - class RecruitArea : public CIntObject - { - const CGTownInstance * town; - int level; - - std::string hoverText; - std::shared_ptr availableCount; - - std::vector> values; - std::shared_ptr icons; - std::shared_ptr buildingIcon; - std::shared_ptr buildingName; - - const CCreature * getMyCreature(); - const CBuilding * getMyBuilding(); - public: - RecruitArea(int posX, int posY, const CGTownInstance *town, int level); - - void creaturesChangedEventHandler(); - void hover(bool on) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - - }; - std::shared_ptr title; - std::vector> recAreas; - std::shared_ptr resdatabar; - std::shared_ptr exit; - - ImagePath getBgName(const CGTownInstance * town); - -public: - CFortScreen(const CGTownInstance * town); - - void creaturesChangedEventHandler(); -}; - -/// The mage guild screen where you can see which spells you have -class CMageGuildScreen : public CStatusbarWindow -{ - class Scroll : public CIntObject - { - const CSpell * spell; - std::shared_ptr image; - - public: - Scroll(Point position, const CSpell *Spell); - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - }; - std::shared_ptr window; - std::shared_ptr exit; - std::vector> spells; - std::vector> emptyScrolls; - - std::shared_ptr resdatabar; - -public: - CMageGuildScreen(CCastleInterface * owner, const ImagePath & image); -}; - -/// The blacksmith window where you can buy available in town war machine -class CBlacksmithDialog : public CStatusbarWindow -{ - std::shared_ptr buy; - std::shared_ptr cancel; - std::shared_ptr animBG; - std::shared_ptr anim; - std::shared_ptr title; - std::shared_ptr costIcon; - std::shared_ptr costText; - std::shared_ptr costValue; - -public: - CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid); -}; +/* + * CCastleInterface.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 "../windows/CWindowObject.h" +#include "../widgets/Images.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBuilding; +class CGTownInstance; +class CSpell; +struct CStructure; +class CGHeroInstance; +class CCreature; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CCastleBuildings; +class CCreaturePic; +class CGStatusBar; +class CLabel; +class CMinorResDataBar; +class CPicture; +class CResDataBar; +class CTextBox; +class CTownList; +class CGarrisonInt; +class CComponent; +class CComponentBox; +class LRClickableArea; + +/// Building "button" +class CBuildingRect : public CShowableAnim +{ + std::string getSubtitle(); +public: + enum EBuildingCreationAnimationPhases : uint32_t + { + BUILDING_APPEAR_TIMEPOINT = 500, //500 msec building appears: 0->100% transparency + BUILDING_WHITE_BORDER_TIMEPOINT = 900, //400 msec border glows from white to yellow + BUILDING_YELLOW_BORDER_TIMEPOINT = 1100, //200 msec border glows from yellow to normal (dark orange) + BUILD_ANIMATION_FINISHED_TIMEPOINT = 2100, // 1000msec once border is back to yellow nothing happens (this stage is basically removed by HD Mod) + + BUILDING_FRAME_TIME = 150 // confirmed H3 timing: 150 ms for each building animation frame + }; + + /// returns building associated with this structure + const CBuilding * getBuilding(); + + CCastleBuildings * parent; + const CGTownInstance * town; + const CStructure* str; + std::shared_ptr border; + std::shared_ptr area; + + ui32 stateTimeCounter;//For building construction - current stage in animation + + CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str); + bool operator<(const CBuildingRect & p2) const; + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) override; + bool receiveEvent(const Point & position, int eventType) const override; + void tick(uint32_t msPassed) override; + void show(Canvas & to) override; + void showAll(Canvas & to) override; +}; + +/// Dwelling info box - right-click screen for dwellings +class CDwellingInfoBox : public CWindowObject +{ + std::shared_ptr title; + std::shared_ptr animation; + std::shared_ptr available; + std::shared_ptr costPerTroop; + + std::vector> resPicture; + std::vector> resAmount; +public: + CDwellingInfoBox(int centerX, int centerY, const CGTownInstance * Town, int level); + ~CDwellingInfoBox(); +}; + +class HeroSlots; +/// Hero icon slot +class CHeroGSlot : public CIntObject +{ + std::shared_ptr portrait; + std::shared_ptr flag; + std::shared_ptr selection; //selection border. nullptr if not selected + + HeroSlots * owner; + const CGHeroInstance * hero; + int upg; //0 - up garrison, 1 - down garrison + +public: + CHeroGSlot(int x, int y, int updown, const CGHeroInstance *h, HeroSlots * Owner); + ~CHeroGSlot(); + + bool isSelected() const; + + void setHighlight(bool on); + void set(const CGHeroInstance * newHero); + + void hover (bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void deactivate() override; +}; + +/// Two hero slots that can interact with each other +class HeroSlots : public CIntObject +{ +public: + bool showEmpty; + const CGTownInstance * town; + + std::shared_ptr garr; + std::shared_ptr garrisonedHero; + std::shared_ptr visitingHero; + + HeroSlots(const CGTownInstance * town, Point garrPos, Point visitPos, std::shared_ptr Garrison, bool ShowEmpty); + ~HeroSlots(); + + void splitClicked(); //for hero meeting only (splitting stacks is handled by garrison int) + void update(); + void swapArmies(); //exchange garrisoned and visiting heroes or move hero to\from garrison +}; + +/// Class for town screen management (town background and structures) +class CCastleBuildings : public CIntObject +{ + std::shared_ptr background; + //List of buildings and structures that can represent them + std::map > groups; + // actual IntObject's visible on screen + std::vector> buildings; + + const CGTownInstance * town; + + const CGHeroInstance* getHero();//Select hero for buildings usage + + void enterBlacksmith(ArtifactID artifactID);//support for blacksmith + ballista yard + void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages + void enterCastleGate(); + void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains + void enterMagesGuild(); + + void openMagesGuild(); + void openTownHall(); + + void recreate(); +public: + CBuildingRect * selectedBuilding; + + CCastleBuildings(const CGTownInstance * town); + ~CCastleBuildings(); + + void enterDwelling(int level); + void enterTownHall(); + void enterToTheQuickRecruitmentWindow(); + + void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE); + void addBuilding(BuildingID building); + void removeBuilding(BuildingID building);//FIXME: not tested!!! +}; + +/// Creature info window +class CCreaInfo : public CIntObject +{ + const CGTownInstance * town; + const CCreature * creature; + int level; + bool showAvailable; + + std::shared_ptr picture; + std::shared_ptr label; + + std::string genGrowthText(); + +public: + CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact=false, bool _showAvailable=false); + + void update(); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + bool getShowAvailable(); +}; + +/// Town hall and fort icons for town screen +class CTownInfo : public CIntObject +{ + const CGTownInstance * town; + const CBuilding * building; +public: + std::shared_ptr picture; + //if (townHall) hall-capital else fort - castle + CTownInfo(int posX, int posY, const CGTownInstance * town, bool townHall); + + void hover(bool on) override; + void showPopupWindow(const Point & cursorPosition) override; +}; + +/// Class which manages the castle window +class CCastleInterface : public CStatusbarWindow, public IGarrisonHolder +{ + std::shared_ptr title; + std::shared_ptr income; + std::shared_ptr icon; + + std::shared_ptr panel; + std::shared_ptr resdatabar; + + std::shared_ptr hall; + std::shared_ptr fort; + + std::shared_ptr exit; + std::shared_ptr split; + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; + + std::vector> creainfo;//small icons of creatures (bottom-left corner); + +public: + std::shared_ptr townlist; + + //TODO: move to private + const CGTownInstance * town; + std::shared_ptr heroes; + std::shared_ptr builds; + + std::shared_ptr garr; + + //from - previously selected castle (if any) + CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); + ~CCastleInterface(); + + virtual void updateGarrisons() override; + + void castleTeleport(int where); + void townChange(); + void keyPressed(EShortcut key) override; + + void close() override; + void addBuilding(BuildingID bid); + void removeBuilding(BuildingID bid); + void recreateIcons(); + void creaturesChangedEventHandler(); +}; + +/// Hall window where you can build things +class CHallInterface : public CStatusbarWindow +{ + class CBuildingBox : public CIntObject + { + const CGTownInstance * town; + const CBuilding * building; + + EBuildingState state; + + std::shared_ptr header; + std::shared_ptr icon; + std::shared_ptr mark; + std::shared_ptr name; + public: + CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + }; + const CGTownInstance * town; + + std::vector>> boxes; + std::shared_ptr title; + std::shared_ptr resdatabar; + std::shared_ptr exit; + +public: + CHallInterface(const CGTownInstance * Town); +}; + +/// Window where you can decide to buy a building or not +class CBuildWindow: public CStatusbarWindow +{ + const CGTownInstance * town; + const CBuilding * building; + + std::shared_ptr icon; + std::shared_ptr name; + std::shared_ptr description; + std::shared_ptr stateText; + std::shared_ptr cost; + + std::shared_ptr buy; + std::shared_ptr cancel; + + std::string getTextForState(EBuildingState state); + void buyFunc(); +public: + CBuildWindow(const CGTownInstance *Town, const CBuilding * building, EBuildingState State, bool rightClick); +}; + +//Small class to display +class LabeledValue : public CIntObject +{ + std::string hoverText; + std::shared_ptr name; + std::shared_ptr value; + void init(std::string name, std::string descr, int min, int max); + +public: + LabeledValue(Rect size, std::string name, std::string descr, int min, int max); + LabeledValue(Rect size, std::string name, std::string descr, int val); + void hover(bool on) override; +}; + +/// The fort screen where you can afford units +class CFortScreen : public CStatusbarWindow +{ + class RecruitArea : public CIntObject + { + const CGTownInstance * town; + int level; + + std::string hoverText; + std::shared_ptr availableCount; + + std::vector> values; + std::shared_ptr icons; + std::shared_ptr buildingIcon; + std::shared_ptr buildingName; + + const CCreature * getMyCreature(); + const CBuilding * getMyBuilding(); + public: + RecruitArea(int posX, int posY, const CGTownInstance *town, int level); + + void creaturesChangedEventHandler(); + void hover(bool on) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + + }; + std::shared_ptr title; + std::vector> recAreas; + std::shared_ptr resdatabar; + std::shared_ptr exit; + + ImagePath getBgName(const CGTownInstance * town); + +public: + CFortScreen(const CGTownInstance * town); + + void creaturesChangedEventHandler(); +}; + +/// The mage guild screen where you can see which spells you have +class CMageGuildScreen : public CStatusbarWindow +{ + class Scroll : public CIntObject + { + const CSpell * spell; + std::shared_ptr image; + + public: + Scroll(Point position, const CSpell *Spell); + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + }; + std::shared_ptr window; + std::shared_ptr exit; + std::vector> spells; + std::vector> emptyScrolls; + + std::shared_ptr resdatabar; + +public: + CMageGuildScreen(CCastleInterface * owner, const ImagePath & image); +}; + +/// The blacksmith window where you can buy available in town war machine +class CBlacksmithDialog : public CStatusbarWindow +{ + std::shared_ptr buy; + std::shared_ptr cancel; + std::shared_ptr animBG; + std::shared_ptr anim; + std::shared_ptr title; + std::shared_ptr costIcon; + std::shared_ptr costText; + std::shared_ptr costValue; + +public: + CBlacksmithDialog(bool possible, CreatureID creMachineID, ArtifactID aid, ObjectInstanceID hid); +}; diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index c83d04d38..59e7e8413 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -1,983 +1,983 @@ -/* - * CCreatureWindow.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 "CCreatureWindow.h" - -#include -#include - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../render/Canvas.h" -#include "../widgets/Buttons.h" -#include "../widgets/CArtifactHolder.h" -#include "../widgets/CComponent.h" -#include "../widgets/Images.h" -#include "../widgets/TextControls.h" -#include "../widgets/ObjectLists.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" - -#include "../../CCallback.h" -#include "../../lib/ArtifactUtils.h" -#include "../../lib/CStack.h" -#include "../../lib/CBonusTypeHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/GameSettings.h" -#include "../../lib/CHeroHandler.h" -#include "../../lib/gameState/CGameState.h" -#include "../../lib/TextOperations.h" - -class CCreatureArtifactInstance; -class CSelectableSkill; - -class UnitView -{ -public: - // helper structs - struct CommanderLevelInfo - { - std::vector skills; - std::function callback; - }; - struct StackDismissInfo - { - std::function callback; - }; - struct StackUpgradeInfo - { - UpgradeInfo info; - std::function callback; - }; - - // pointers to permament objects in game state - const CCreature * creature; - const CCommanderInstance * commander; - const CStackInstance * stackNode; - const CStack * stack; - const CGHeroInstance * owner; - - // temporary objects which should be kept as copy if needed - std::optional levelupInfo; - std::optional dismissInfo; - std::optional upgradeInfo; - - // misc fields - unsigned int creatureCount; - bool popupWindow; - - UnitView() - : creature(nullptr), - commander(nullptr), - stackNode(nullptr), - stack(nullptr), - owner(nullptr), - creatureCount(0), - popupWindow(false) - { - } - - std::string getName() const - { - if(commander) - return commander->type->getNameSingularTranslated(); - else - return creature->getNamePluralTranslated(); - } -private: - -}; - -CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback) - : object(), - isMasterAbility(isMasterAbility_), - isSelected(false), - callback(callback) -{ - pos = object_->pos; - this->isMasterAbility = isMasterAbility_; - setObject(object_); -} - -void CCommanderSkillIcon::setObject(std::shared_ptr newObject) -{ - if(object) - removeChild(object.get()); - object = newObject; - addChild(object.get()); - object->moveTo(pos.topLeft()); - redraw(); -} - -void CCommanderSkillIcon::clickPressed(const Point & cursorPosition) -{ - callback(); - isSelected = true; -} - -void CCommanderSkillIcon::deselect() -{ - isSelected = false; -} - -bool CCommanderSkillIcon::getIsMasterAbility() -{ - return isMasterAbility; -} - -void CCommanderSkillIcon::show(Canvas &to) -{ - CIntObject::show(to); - - if(isMasterAbility && isSelected) - to.drawBorder(pos, Colors::YELLOW, 2); -} - -static ImagePath skillToFile(int skill, int level, bool selected) -{ - // FIXME: is this a correct hadling? - // level 0 = skill not present, use image with "no" suffix - // level 1-5 = skill available, mapped to images indexed as 0-4 - // selecting skill means that it will appear one level higher (as if alredy upgraded) - std::string file = "zvs/Lib1.res/_"; - switch (skill) - { - case ECommander::ATTACK: - file += "AT"; - break; - case ECommander::DEFENSE: - file += "DF"; - break; - case ECommander::HEALTH: - file += "HP"; - break; - case ECommander::DAMAGE: - file += "DM"; - break; - case ECommander::SPEED: - file += "SP"; - break; - case ECommander::SPELL_POWER: - file += "MP"; - break; - } - std::string sufix; - if (selected) - level++; // UI will display resulting level - if (level == 0) - sufix = "no"; //not avaliable - no number - else - sufix = std::to_string(level-1); - if (selected) - sufix += "="; //level-up highlight - - return ImagePath::builtin(file + sufix + ".bmp"); -} - -CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset) - : parent(parent) -{ - pos.y += yOffset; - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(!backgroundPath.empty()) - { - background = std::make_shared(backgroundPath); - pos.w = background->pos.w; - pos.h = background->pos.h; - } -} - -CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, ImagePath::builtin("stackWindow/spell-effects"), yOffset) -{ - static const Point firstPos(6, 2); // position of 1st spell box - static const Point offset(54, 0); // offset of each spell box from previous - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - const CStack * battleStack = parent->info->stack; - - assert(battleStack); // Section should be created only for battles - - //spell effects - int printed=0; //how many effect pics have been printed - std::vector spells = battleStack->activeSpells(); - for(si32 effect : spells) - { - const spells::Spell * spell = CGI->spells()->getByIndex(effect); - - std::string spellText; - - //not all effects have graphics (for eg. Acid Breath) - //for modded spells iconEffect is added to SpellInt.def - const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST); - - if (hasGraphics) - { - spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." - boost::replace_first(spellText, "%s", spell->getNameTranslated()); - //FIXME: support permanent duration - int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT,effect))->turnsRemain; - boost::replace_first(spellText, "%d", std::to_string(duration)); - - spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); - clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); - if(++printed >= 8) // interface limit reached - break; - } - } -} - -CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) - : CWindowSection(owner, ImagePath::builtin("stackWindow/bonus-effects"), 0) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - static const std::array offset = - { - Point(6, 4), - Point(214, 4) - }; - - for(size_t leftRight : {0, 1}) - { - auto position = offset[leftRight]; - size_t bonusIndex = lineIndex * 2 + leftRight; - - if(parent->activeBonuses.size() > bonusIndex) - { - BonusInfo & bi = parent->activeBonuses[bonusIndex]; - icon[leftRight] = std::make_shared(bi.imagePath, position.x, position.y); - name[leftRight] = std::make_shared(position.x + 60, position.y + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name); - description[leftRight] = std::make_shared(Rect(position.x + 60, position.y + 17, 137, 30), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description); - } - } -} - -CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): - CWindowSection(owner, {}, yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - // size of single image for an item - static const int itemHeight = 59; - - size_t totalSize = (owner->activeBonuses.size() + 1) / 2; - size_t visibleSize = preferredSize.value_or(std::min(3, totalSize)); - - pos.w = owner->pos.w; - pos.h = itemHeight * (int)visibleSize; - - auto onCreate = [=](size_t index) -> std::shared_ptr - { - return std::make_shared(owner, index); - }; - - lines = std::make_shared(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, 1, Rect(pos.w - 15, 0, pos.h, pos.h)); -} - -CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, ImagePath::builtin("stackWindow/button-panel"), yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(parent->info->dismissInfo && parent->info->dismissInfo->callback) - { - auto onDismiss = [=]() - { - parent->info->dismissInfo->callback(); - parent->close(); - }; - auto onClick = [=] () - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr); - }; - dismiss = std::make_shared(Point(5, 5),AnimationPath::builtin("IVIEWCR2.DEF"), CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); - } - - if(parent->info->upgradeInfo && !parent->info->commander) - { - // used space overlaps with commander switch button - // besides - should commander really be upgradeable? - - auto & upgradeInfo = parent->info->upgradeInfo.value(); - const size_t buttonsToCreate = std::min(upgradeInfo.info.newID.size(), upgrade.size()); - - for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++) - { - TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount; - - auto onUpgrade = [=]() - { - upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]); - parent->close(); - }; - auto onClick = [=]() - { - std::vector> resComps; - for(TResources::nziterator i(totalCost); i.valid(); i++) - { - resComps.push_back(std::make_shared(CComponent::resource, i->resType, (int)i->resVal)); - } - - if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) - { - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[207], onUpgrade, nullptr, resComps); - } - else - { - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps); - } - }; - auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); - - upgradeBtn->addOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); - - if(buttonsToCreate == 1) // single upgrade avaialbe - upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; - - upgrade[buttonIndex] = upgradeBtn; - } - } - - if(parent->info->commander) - { - for(size_t buttonIndex = 0; buttonIndex < 2; buttonIndex++) - { - std::string btnIDs[2] = { "showSkills", "showBonuses" }; - auto onSwitch = [buttonIndex, this]() - { - logAnim->debug("Switch %d->%d", parent->activeTab, buttonIndex); - - parent->switchButtons[parent->activeTab]->enable(); - parent->commanderTab->setActive(buttonIndex); - parent->switchButtons[buttonIndex]->disable(); - parent->redraw(); // FIXME: enable/disable don't redraw screen themselves - }; - - std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; - parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CButton::tooltipLocalized(tooltipText), onSwitch); - parent->switchButtons[buttonIndex]->addOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); - } - parent->switchButtons[parent->activeTab]->disable(); - } - - exit = std::make_shared(Point(382, 5), AnimationPath::builtin("hsbtns.def"), CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); -} - -CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) - : CWindowSection(owner, ImagePath::builtin("stackWindow/commander-bg"), yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - auto getSkillPos = [](int index) - { - return Point(10 + 80 * (index%3), 20 + 80 * (index/3)); - }; - - auto getSkillImage = [this](int skillIndex) - { - bool selected = ((parent->selectedSkill == skillIndex) && parent->info->levelupInfo ); - return skillToFile(skillIndex, parent->info->commander->secondarySkills[skillIndex], selected); - }; - - auto getSkillDescription = [this](int skillIndex) -> std::string - { - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; - }; - - for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) - { - Point skillPos = getSkillPos(index); - - auto icon = std::make_shared(std::make_shared(getSkillImage(index), skillPos.x, skillPos.y), false, [=]() - { - LOCPLINT->showInfoDialog(getSkillDescription(index)); - }); - - icon->text = getSkillDescription(index); //used to handle right click description via LRClickableAreaWText::ClickRight() - - if(parent->selectedSkill == index) - parent->selectedIcon = icon; - - if(parent->info->levelupInfo && vstd::contains(parent->info->levelupInfo->skills, index)) // can be upgraded - enable selection switch - { - if(parent->selectedSkill == index) - parent->setSelection(index, icon); - - icon->callback = [=]() - { - parent->setSelection(index, icon); - }; - } - - skillIcons.push_back(icon); - } - - auto getArtifactPos = [](int index) - { - return Point(269 + 47 * (index % 3), 22 + 47 * (index / 3)); - }; - - for(auto equippedArtifact : parent->info->commander->artifactsWorn) - { - Point artPos = getArtifactPos(equippedArtifact.first); - auto artPlace = std::make_shared(artPos, parent->info->owner, equippedArtifact.first, equippedArtifact.second.artifact); - artifacts.push_back(artPlace); - } - - if(parent->info->levelupInfo) - { - abilitiesBackground = std::make_shared(ImagePath::builtin("stackWindow/commander-abilities.png")); - abilitiesBackground->moveBy(Point(0, pos.h)); - - size_t abilitiesCount = boost::range::count_if(parent->info->levelupInfo->skills, [](ui32 skillID) - { - return skillID >= 100; - }); - - auto onCreate = [=](size_t index)->std::shared_ptr - { - for(auto skillID : parent->info->levelupInfo->skills) - { - if(index == 0 && skillID >= 100) - { - const auto bonus = CGI->creh->skillRequirements[skillID-100].first; - const CStackInstance * stack = parent->info->commander; - auto icon = std::make_shared(std::make_shared(stack->bonusToGraphics(bonus)), true, [](){}); - icon->callback = [=]() - { - parent->setSelection(skillID, icon); - }; - icon->text = stack->bonusToString(bonus, true); - icon->hoverText = stack->bonusToString(bonus, false); - - return icon; - } - if(skillID >= 100) - index--; - } - return nullptr; - }; - - abilities = std::make_shared(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount); - abilities->setRedrawParent(true); - - leftBtn = std::make_shared(Point(10, pos.h + 6), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); - rightBtn = std::make_shared(Point(411, pos.h + 6), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); - - if(abilitiesCount <= 6) - { - leftBtn->block(true); - rightBtn->block(true); - } - - pos.h += abilitiesBackground->pos.h; - } -} - -CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt) - : CWindowSection(owner, getBackgroundName(showExp, showArt), yOffset) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - statNames = - { - CGI->generaltexth->primarySkillNames[0], //ATTACK - CGI->generaltexth->primarySkillNames[1],//DEFENCE - CGI->generaltexth->allTexts[198],//SHOTS - CGI->generaltexth->allTexts[199],//DAMAGE - - CGI->generaltexth->allTexts[388],//HEALTH - CGI->generaltexth->allTexts[200],//HEALTH_LEFT - CGI->generaltexth->zelp[441].first,//SPEED - CGI->generaltexth->allTexts[399]//MANA - }; - - statFormats = - { - "%d (%d)", - "%d (%d)", - "%d (%d)", - "%d - %d", - - "%d (%d)", - "%d (%d)", - "%d (%d)", - "%d (%d)" - }; - - animation = std::make_shared(5, 41, parent->info->creature); - - if(parent->info->stackNode != nullptr && parent->info->commander == nullptr) - { - //normal stack, not a commander and not non-existing stack (e.g. recruitment dialog) - animation->setAmount(parent->info->creatureCount); - } - - name = std::make_shared(215, 12, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName()); - - int dmgMultiply = 1; - if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) - dmgMultiply += parent->info->owner->getPrimSkillLevel(PrimarySkill::ATTACK); - - icons = std::make_shared(ImagePath::builtin("stackWindow/icons"), 117, 32); - - const CStack * battleStack = parent->info->stack; - - morale = std::make_shared(true, Rect(Point(321, 110), Point(42, 42) )); - luck = std::make_shared(false, Rect(Point(375, 110), Point(42, 42) )); - - if(battleStack != nullptr) // in battle - { - addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(battleStack->isShooter()), battleStack->getAttack(battleStack->isShooter())); - addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter())); - addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply); - addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed()); - - if(battleStack->isShooter()) - addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); - if(battleStack->isCaster()) - addStatLabel(EStat::MANA, battleStack->casts.total(), battleStack->casts.available()); - addStatLabel(EStat::HEALTH_LEFT, battleStack->getFirstHPleft()); - - morale->set(battleStack); - luck->set(battleStack); - } - else - { - const bool shooter = parent->info->stackNode->hasBonusOfType(BonusType::SHOOTER) && parent->info->stackNode->valOfBonuses(BonusType::SHOTS); - const bool caster = parent->info->stackNode->valOfBonuses(BonusType::CASTS); - - addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(shooter), parent->info->stackNode->getAttack(shooter)); - addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(shooter), parent->info->stackNode->getDefense(shooter)); - addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter) * dmgMultiply, parent->info->stackNode->getMaxDamage(shooter) * dmgMultiply); - addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); - addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed()); - - if(shooter) - addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); - if(caster) - addStatLabel(EStat::MANA, parent->info->stackNode->valOfBonuses(BonusType::CASTS)); - - morale->set(parent->info->stackNode); - luck->set(parent->info->stackNode); - } - - if(showExp) - { - const CStackInstance * stack = parent->info->stackNode; - Point pos = showArt ? Point(321, 32) : Point(347, 32); - if(parent->info->commander) - { - const CCommanderInstance * commander = parent->info->commander; - expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); - - auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); - expArea = area; - area->text = CGI->generaltexth->allTexts[2]; - area->bonusValue = commander->getExpRank(); - boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); - boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); - boost::replace_first(area->text, "%d", std::to_string(commander->experience)); - } - else - { - expRankIcon = std::make_shared(AnimationPath::builtin("stackWindow/levels"), stack->getExpRank(), 0, pos.x, pos.y); - expArea = std::make_shared(Rect(pos.x, pos.y, 44, 44)); - expArea->text = parent->generateStackExpDescription(); - } - expLabel = std::make_shared( - pos.x + 21, pos.y + 52, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, - TextOperations::formatMetric(stack->experience, 6)); - } - - if(showArt) - { - Point pos = showExp ? Point(375, 32) : Point(347, 32); - // ALARMA: do not refactor this into a separate function - // otherwise, artifact icon is drawn near the hero's portrait - // this is really strange - auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); - if(art) - { - parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); - parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); - parent->stackArtifactHelp->type = art->artType->getId(); - - if(parent->info->owner) - { - parent->stackArtifactButton = std::make_shared( - Point(pos.x - 2 , pos.y + 46), AnimationPath::builtin("stackWindow/cancelButton"), - CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() - { - parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); - }); - } - } - } -} - - -ImagePath CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) -{ - if(showExp && showArt) - return ImagePath::builtin("stackWindow/info-panel-2"); - else if(showExp || showArt) - return ImagePath::builtin("stackWindow/info-panel-1"); - else - return ImagePath::builtin("stackWindow/info-panel-0"); -} - -void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value1, int64_t value2) -{ - const auto title = statNames.at(static_cast(index)); - stats.push_back(std::make_shared(145, 32 + (int)index*19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, title)); - - const bool useRange = value1 != value2; - std::string formatStr = useRange ? statFormats.at(static_cast(index)) : "%d"; - - boost::format fmt(formatStr); - fmt % value1; - if(useRange) - fmt % value2; - - stats.push_back(std::make_shared(307, 48 + (int)index*19, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, fmt.str())); -} - -void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value) -{ - addStatLabel(index, value, value); -} - -CStackWindow::CStackWindow(const CStack * stack, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stack = stack; - info->stackNode = stack->base; - info->creature = stack->unitType(); - info->creatureCount = stack->getCount(); - info->popupWindow = popup; - init(); -} - -CStackWindow::CStackWindow(const CCreature * creature, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->creature = creature; - info->popupWindow = popup; - init(); -} - -CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stackNode = stack; - info->creature = stack->type; - info->creatureCount = stack->count; - info->popupWindow = popup; - info->owner = dynamic_cast (stack->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & upgradeInfo, std::function callback) - : CWindowObject(BORDERED), - info(new UnitView()) -{ - info->stackNode = stack; - info->creature = stack->type; - info->creatureCount = stack->count; - - info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); - info->dismissInfo = std::make_optional(UnitView::StackDismissInfo()); - info->upgradeInfo->info = upgradeInfo; - info->upgradeInfo->callback = callback; - info->dismissInfo->callback = dismiss; - info->owner = dynamic_cast (stack->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CCommanderInstance * commander, bool popup) - : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), - info(new UnitView()) -{ - info->stackNode = commander; - info->creature = commander->type; - info->commander = commander; - info->creatureCount = 1; - info->popupWindow = popup; - info->owner = dynamic_cast (commander->armyObj); - init(); -} - -CStackWindow::CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback) - : CWindowObject(BORDERED), - info(new UnitView()) -{ - info->stackNode = commander; - info->creature = commander->type; - info->commander = commander; - info->creatureCount = 1; - info->levelupInfo = std::make_optional(UnitView::CommanderLevelInfo()); - info->levelupInfo->skills = skills; - info->levelupInfo->callback = callback; - info->owner = dynamic_cast (commander->armyObj); - init(); -} - -CStackWindow::~CStackWindow() -{ - if(info->levelupInfo && !info->levelupInfo->skills.empty()) - info->levelupInfo->callback(vstd::find_pos(info->levelupInfo->skills, selectedSkill)); -} - -void CStackWindow::init() -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(!info->stackNode) - info->stackNode = new CStackInstance(info->creature, 1, true);// FIXME: free data - - selectedIcon = nullptr; - selectedSkill = -1; - if(info->levelupInfo && !info->levelupInfo->skills.empty()) - selectedSkill = info->levelupInfo->skills.front(); - - activeTab = 0; - - initBonusesList(); - initSections(); -} - -void CStackWindow::initBonusesList() -{ - BonusList output, input; - input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); - - while(!input.empty()) - { - auto b = input.front(); - output.push_back(std::make_shared(*b)); - output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one - input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses - } - - BonusInfo bonusInfo; - for(auto b : output) - { - bonusInfo.name = info->stackNode->bonusToString(b, false); - bonusInfo.description = info->stackNode->bonusToString(b, true); - bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); - - //if it's possible to give any description or image for this kind of bonus - //TODO: figure out why half of bonuses don't have proper description - if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty()) - activeBonuses.push_back(bonusInfo); - } -} - -void CStackWindow::initSections() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - bool showArt = CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; - bool showExp = (CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode; - - mainSection = std::make_shared(this, pos.h, showExp, showArt); - - pos.w = mainSection->pos.w; - pos.h += mainSection->pos.h; - - if(info->stack) // in battle - { - activeSpellsSection = std::make_shared(this, pos.h); - pos.h += activeSpellsSection->pos.h; - } - - if(info->commander) - { - auto onCreate = [=](size_t index) -> std::shared_ptr - { - auto obj = switchTab(index); - - if(obj) - { - obj->activate(); - obj->recActions |= (UPDATE | SHOWALL); - } - return obj; - }; - - auto deactivateObj = [=](std::shared_ptr obj) - { - obj->deactivate(); - obj->recActions &= ~(UPDATE | SHOWALL); - }; - - commanderMainSection = std::make_shared(this, 0); - - auto size = std::make_optional((info->levelupInfo) ? 4 : 3); - commanderBonusesSection = std::make_shared(this, 0, size); - deactivateObj(commanderBonusesSection); - - commanderTab = std::make_shared(onCreate, Point(0, pos.h), 0); - - pos.h += commanderMainSection->pos.h; - } - - if(!info->commander && !activeBonuses.empty()) - { - bonusesSection = std::make_shared(this, pos.h); - pos.h += bonusesSection->pos.h; - } - - if(!info->popupWindow) - { - buttonsSection = std::make_shared(this, pos.h); - pos.h += buttonsSection->pos.h; - //FIXME: add status bar to image? - } - updateShadow(); - pos = center(pos); -} - -std::string CStackWindow::generateStackExpDescription() -{ - const CStackInstance * stack = info->stackNode; - const CCreature * creature = info->creature; - - int tier = stack->type->getLevel(); - int rank = stack->getExpRank(); - if (!vstd::iswithin(tier, 1, 7)) - tier = 0; - int number; - std::string expText = CGI->generaltexth->translate("vcmi.stackExperience.description"); - boost::replace_first(expText, "%s", creature->getNamePluralTranslated()); - boost::replace_first(expText, "%s", CGI->generaltexth->translate("vcmi.stackExperience.rank", rank)); - boost::replace_first(expText, "%i", std::to_string(rank)); - boost::replace_first(expText, "%i", std::to_string(stack->experience)); - number = static_cast(CGI->creh->expRanks[tier][rank] - stack->experience); - boost::replace_first(expText, "%i", std::to_string(number)); - - number = CGI->creh->maxExpPerBattle[tier]; //percent - boost::replace_first(expText, "%i%", std::to_string(number)); - number *= CGI->creh->expRanks[tier].back() / 100; //actual amount - boost::replace_first(expText, "%i", std::to_string(number)); - - boost::replace_first(expText, "%i", std::to_string(stack->count)); //Number of Creatures in stack - - int expmin = std::max(CGI->creh->expRanks[tier][std::max(rank-1, 0)], (ui32)1); - number = static_cast((stack->count * (stack->experience - expmin)) / expmin); //Maximum New Recruits without losing current Rank - boost::replace_first(expText, "%i", std::to_string(number)); //TODO - - boost::replace_first(expText, "%.2f", std::to_string(1)); //TODO Experience Multiplier - number = CGI->creh->expAfterUpgrade; - boost::replace_first(expText, "%.2f", std::to_string(number) + "%"); //Upgrade Multiplier - - expmin = CGI->creh->expRanks[tier][9]; - int expmax = CGI->creh->expRanks[tier][10]; - number = expmax - expmin; - boost::replace_first(expText, "%i", std::to_string(number)); //Experience after Rank 10 - number = (stack->count * (expmax - expmin)) / expmin; - boost::replace_first(expText, "%i", std::to_string(number)); //Maximum New Recruits to remain at Rank 10 if at Maximum Experience - - return expText; -} - -void CStackWindow::setSelection(si32 newSkill, std::shared_ptr newIcon) -{ - auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string - { - if(selected) - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description - else - return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; - }; - - auto getSkillImage = [this](int skillIndex) - { - bool selected = ((selectedSkill == skillIndex) && info->levelupInfo ); - return skillToFile(skillIndex, info->commander->secondarySkills[skillIndex], selected); - }; - - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - int oldSelection = selectedSkill; // update selection - selectedSkill = newSkill; - - if(selectedIcon && oldSelection < 100) // recreate image on old selection, only for skills - selectedIcon->setObject(std::make_shared(getSkillImage(oldSelection))); - - if(selectedIcon) - { - if(!selectedIcon->getIsMasterAbility()) //unlike WoG, in VCMI master skill descriptions are taken from bonus descriptions - { - selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level - } - selectedIcon->deselect(); - } - - selectedIcon = newIcon; // update new selection - if(newSkill < 100) - { - newIcon->setObject(std::make_shared(getSkillImage(newSkill))); - if(!newIcon->getIsMasterAbility()) - { - newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description - } - } -} - -std::shared_ptr CStackWindow::switchTab(size_t index) -{ - std::shared_ptr ret; - switch(index) - { - case 0: - { - activeTab = 0; - ret = commanderMainSection; - } - break; - case 1: - { - activeTab = 1; - ret = commanderBonusesSection; - } - break; - default: - break; - } - - return ret; -} - -void CStackWindow::removeStackArtifact(ArtifactPosition pos) -{ - auto art = info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); - if(!art) - { - logGlobal->error("Attempt to remove missing artifact"); - return; - } - const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); - if(slot != ArtifactPosition::PRE_FIRST) - { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot)); - stackArtifactButton.reset(); - stackArtifactHelp.reset(); - stackArtifactIcon.reset(); - redraw(); - } -} - +/* + * CCreatureWindow.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 "CCreatureWindow.h" + +#include +#include + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../render/Canvas.h" +#include "../widgets/Buttons.h" +#include "../widgets/CArtifactHolder.h" +#include "../widgets/CComponent.h" +#include "../widgets/Images.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" + +#include "../../CCallback.h" +#include "../../lib/ArtifactUtils.h" +#include "../../lib/CStack.h" +#include "../../lib/CBonusTypeHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/GameSettings.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/gameState/CGameState.h" +#include "../../lib/TextOperations.h" + +class CCreatureArtifactInstance; +class CSelectableSkill; + +class UnitView +{ +public: + // helper structs + struct CommanderLevelInfo + { + std::vector skills; + std::function callback; + }; + struct StackDismissInfo + { + std::function callback; + }; + struct StackUpgradeInfo + { + UpgradeInfo info; + std::function callback; + }; + + // pointers to permament objects in game state + const CCreature * creature; + const CCommanderInstance * commander; + const CStackInstance * stackNode; + const CStack * stack; + const CGHeroInstance * owner; + + // temporary objects which should be kept as copy if needed + std::optional levelupInfo; + std::optional dismissInfo; + std::optional upgradeInfo; + + // misc fields + unsigned int creatureCount; + bool popupWindow; + + UnitView() + : creature(nullptr), + commander(nullptr), + stackNode(nullptr), + stack(nullptr), + owner(nullptr), + creatureCount(0), + popupWindow(false) + { + } + + std::string getName() const + { + if(commander) + return commander->type->getNameSingularTranslated(); + else + return creature->getNamePluralTranslated(); + } +private: + +}; + +CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback) + : object(), + isMasterAbility(isMasterAbility_), + isSelected(false), + callback(callback) +{ + pos = object_->pos; + this->isMasterAbility = isMasterAbility_; + setObject(object_); +} + +void CCommanderSkillIcon::setObject(std::shared_ptr newObject) +{ + if(object) + removeChild(object.get()); + object = newObject; + addChild(object.get()); + object->moveTo(pos.topLeft()); + redraw(); +} + +void CCommanderSkillIcon::clickPressed(const Point & cursorPosition) +{ + callback(); + isSelected = true; +} + +void CCommanderSkillIcon::deselect() +{ + isSelected = false; +} + +bool CCommanderSkillIcon::getIsMasterAbility() +{ + return isMasterAbility; +} + +void CCommanderSkillIcon::show(Canvas &to) +{ + CIntObject::show(to); + + if(isMasterAbility && isSelected) + to.drawBorder(pos, Colors::YELLOW, 2); +} + +static ImagePath skillToFile(int skill, int level, bool selected) +{ + // FIXME: is this a correct hadling? + // level 0 = skill not present, use image with "no" suffix + // level 1-5 = skill available, mapped to images indexed as 0-4 + // selecting skill means that it will appear one level higher (as if alredy upgraded) + std::string file = "zvs/Lib1.res/_"; + switch (skill) + { + case ECommander::ATTACK: + file += "AT"; + break; + case ECommander::DEFENSE: + file += "DF"; + break; + case ECommander::HEALTH: + file += "HP"; + break; + case ECommander::DAMAGE: + file += "DM"; + break; + case ECommander::SPEED: + file += "SP"; + break; + case ECommander::SPELL_POWER: + file += "MP"; + break; + } + std::string sufix; + if (selected) + level++; // UI will display resulting level + if (level == 0) + sufix = "no"; //not avaliable - no number + else + sufix = std::to_string(level-1); + if (selected) + sufix += "="; //level-up highlight + + return ImagePath::builtin(file + sufix + ".bmp"); +} + +CStackWindow::CWindowSection::CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset) + : parent(parent) +{ + pos.y += yOffset; + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(!backgroundPath.empty()) + { + background = std::make_shared(backgroundPath); + pos.w = background->pos.w; + pos.h = background->pos.h; + } +} + +CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/spell-effects"), yOffset) +{ + static const Point firstPos(6, 2); // position of 1st spell box + static const Point offset(54, 0); // offset of each spell box from previous + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + const CStack * battleStack = parent->info->stack; + + assert(battleStack); // Section should be created only for battles + + //spell effects + int printed=0; //how many effect pics have been printed + std::vector spells = battleStack->activeSpells(); + for(si32 effect : spells) + { + const spells::Spell * spell = CGI->spells()->getByIndex(effect); + + std::string spellText; + + //not all effects have graphics (for eg. Acid Breath) + //for modded spells iconEffect is added to SpellInt.def + const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST); + + if (hasGraphics) + { + spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." + boost::replace_first(spellText, "%s", spell->getNameTranslated()); + //FIXME: support permanent duration + int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT,effect))->turnsRemain; + boost::replace_first(spellText, "%d", std::to_string(duration)); + + spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); + clickableAreas.push_back(std::make_shared(Rect(firstPos + offset * printed, Point(50, 38)), spellText, spellText)); + if(++printed >= 8) // interface limit reached + break; + } + } +} + +CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t lineIndex) + : CWindowSection(owner, ImagePath::builtin("stackWindow/bonus-effects"), 0) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + static const std::array offset = + { + Point(6, 4), + Point(214, 4) + }; + + for(size_t leftRight : {0, 1}) + { + auto position = offset[leftRight]; + size_t bonusIndex = lineIndex * 2 + leftRight; + + if(parent->activeBonuses.size() > bonusIndex) + { + BonusInfo & bi = parent->activeBonuses[bonusIndex]; + icon[leftRight] = std::make_shared(bi.imagePath, position.x, position.y); + name[leftRight] = std::make_shared(position.x + 60, position.y + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name); + description[leftRight] = std::make_shared(Rect(position.x + 60, position.y + 17, 137, 30), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description); + } + } +} + +CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize): + CWindowSection(owner, {}, yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + // size of single image for an item + static const int itemHeight = 59; + + size_t totalSize = (owner->activeBonuses.size() + 1) / 2; + size_t visibleSize = preferredSize.value_or(std::min(3, totalSize)); + + pos.w = owner->pos.w; + pos.h = itemHeight * (int)visibleSize; + + auto onCreate = [=](size_t index) -> std::shared_ptr + { + return std::make_shared(owner, index); + }; + + lines = std::make_shared(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, 1, Rect(pos.w - 15, 0, pos.h, pos.h)); +} + +CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/button-panel"), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(parent->info->dismissInfo && parent->info->dismissInfo->callback) + { + auto onDismiss = [=]() + { + parent->info->dismissInfo->callback(); + parent->close(); + }; + auto onClick = [=] () + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[12], onDismiss, nullptr); + }; + dismiss = std::make_shared(Point(5, 5),AnimationPath::builtin("IVIEWCR2.DEF"), CGI->generaltexth->zelp[445], onClick, EShortcut::HERO_DISMISS); + } + + if(parent->info->upgradeInfo && !parent->info->commander) + { + // used space overlaps with commander switch button + // besides - should commander really be upgradeable? + + auto & upgradeInfo = parent->info->upgradeInfo.value(); + const size_t buttonsToCreate = std::min(upgradeInfo.info.newID.size(), upgrade.size()); + + for(size_t buttonIndex = 0; buttonIndex < buttonsToCreate; buttonIndex++) + { + TResources totalCost = upgradeInfo.info.cost[buttonIndex] * parent->info->creatureCount; + + auto onUpgrade = [=]() + { + upgradeInfo.callback(upgradeInfo.info.newID[buttonIndex]); + parent->close(); + }; + auto onClick = [=]() + { + std::vector> resComps; + for(TResources::nziterator i(totalCost); i.valid(); i++) + { + resComps.push_back(std::make_shared(CComponent::resource, i->resType, (int)i->resVal)); + } + + if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) + { + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[207], onUpgrade, nullptr, resComps); + } + else + { + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314], resComps); + } + }; + auto upgradeBtn = std::make_shared(Point(221 + (int)buttonIndex * 40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CGI->generaltexth->zelp[446], onClick); + + upgradeBtn->addOverlay(std::make_shared(AnimationPath::builtin("CPRSMALL"), VLC->creh->objects[upgradeInfo.info.newID[buttonIndex]]->getIconIndex())); + + if(buttonsToCreate == 1) // single upgrade avaialbe + upgradeBtn->assignedKey = EShortcut::RECRUITMENT_UPGRADE; + + upgrade[buttonIndex] = upgradeBtn; + } + } + + if(parent->info->commander) + { + for(size_t buttonIndex = 0; buttonIndex < 2; buttonIndex++) + { + std::string btnIDs[2] = { "showSkills", "showBonuses" }; + auto onSwitch = [buttonIndex, this]() + { + logAnim->debug("Switch %d->%d", parent->activeTab, buttonIndex); + + parent->switchButtons[parent->activeTab]->enable(); + parent->commanderTab->setActive(buttonIndex); + parent->switchButtons[buttonIndex]->disable(); + parent->redraw(); // FIXME: enable/disable don't redraw screen themselves + }; + + std::string tooltipText = "vcmi.creatureWindow." + btnIDs[buttonIndex]; + parent->switchButtons[buttonIndex] = std::make_shared(Point(302 + (int)buttonIndex*40, 5), AnimationPath::builtin("stackWindow/upgradeButton"), CButton::tooltipLocalized(tooltipText), onSwitch); + parent->switchButtons[buttonIndex]->addOverlay(std::make_shared(AnimationPath::builtin("stackWindow/switchModeIcons"), buttonIndex)); + } + parent->switchButtons[parent->activeTab]->disable(); + } + + exit = std::make_shared(Point(382, 5), AnimationPath::builtin("hsbtns.def"), CGI->generaltexth->zelp[447], [=](){ parent->close(); }, EShortcut::GLOBAL_RETURN); +} + +CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, int yOffset) + : CWindowSection(owner, ImagePath::builtin("stackWindow/commander-bg"), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + auto getSkillPos = [](int index) + { + return Point(10 + 80 * (index%3), 20 + 80 * (index/3)); + }; + + auto getSkillImage = [this](int skillIndex) + { + bool selected = ((parent->selectedSkill == skillIndex) && parent->info->levelupInfo ); + return skillToFile(skillIndex, parent->info->commander->secondarySkills[skillIndex], selected); + }; + + auto getSkillDescription = [this](int skillIndex) -> std::string + { + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)]; + }; + + for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index) + { + Point skillPos = getSkillPos(index); + + auto icon = std::make_shared(std::make_shared(getSkillImage(index), skillPos.x, skillPos.y), false, [=]() + { + LOCPLINT->showInfoDialog(getSkillDescription(index)); + }); + + icon->text = getSkillDescription(index); //used to handle right click description via LRClickableAreaWText::ClickRight() + + if(parent->selectedSkill == index) + parent->selectedIcon = icon; + + if(parent->info->levelupInfo && vstd::contains(parent->info->levelupInfo->skills, index)) // can be upgraded - enable selection switch + { + if(parent->selectedSkill == index) + parent->setSelection(index, icon); + + icon->callback = [=]() + { + parent->setSelection(index, icon); + }; + } + + skillIcons.push_back(icon); + } + + auto getArtifactPos = [](int index) + { + return Point(269 + 47 * (index % 3), 22 + 47 * (index / 3)); + }; + + for(auto equippedArtifact : parent->info->commander->artifactsWorn) + { + Point artPos = getArtifactPos(equippedArtifact.first); + auto artPlace = std::make_shared(artPos, parent->info->owner, equippedArtifact.first, equippedArtifact.second.artifact); + artifacts.push_back(artPlace); + } + + if(parent->info->levelupInfo) + { + abilitiesBackground = std::make_shared(ImagePath::builtin("stackWindow/commander-abilities.png")); + abilitiesBackground->moveBy(Point(0, pos.h)); + + size_t abilitiesCount = boost::range::count_if(parent->info->levelupInfo->skills, [](ui32 skillID) + { + return skillID >= 100; + }); + + auto onCreate = [=](size_t index)->std::shared_ptr + { + for(auto skillID : parent->info->levelupInfo->skills) + { + if(index == 0 && skillID >= 100) + { + const auto bonus = CGI->creh->skillRequirements[skillID-100].first; + const CStackInstance * stack = parent->info->commander; + auto icon = std::make_shared(std::make_shared(stack->bonusToGraphics(bonus)), true, [](){}); + icon->callback = [=]() + { + parent->setSelection(skillID, icon); + }; + icon->text = stack->bonusToString(bonus, true); + icon->hoverText = stack->bonusToString(bonus, false); + + return icon; + } + if(skillID >= 100) + index--; + } + return nullptr; + }; + + abilities = std::make_shared(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount); + abilities->setRedrawParent(true); + + leftBtn = std::make_shared(Point(10, pos.h + 6), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT); + rightBtn = std::make_shared(Point(411, pos.h + 6), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT); + + if(abilitiesCount <= 6) + { + leftBtn->block(true); + rightBtn->block(true); + } + + pos.h += abilitiesBackground->pos.h; + } +} + +CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt) + : CWindowSection(owner, getBackgroundName(showExp, showArt), yOffset) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + statNames = + { + CGI->generaltexth->primarySkillNames[0], //ATTACK + CGI->generaltexth->primarySkillNames[1],//DEFENCE + CGI->generaltexth->allTexts[198],//SHOTS + CGI->generaltexth->allTexts[199],//DAMAGE + + CGI->generaltexth->allTexts[388],//HEALTH + CGI->generaltexth->allTexts[200],//HEALTH_LEFT + CGI->generaltexth->zelp[441].first,//SPEED + CGI->generaltexth->allTexts[399]//MANA + }; + + statFormats = + { + "%d (%d)", + "%d (%d)", + "%d (%d)", + "%d - %d", + + "%d (%d)", + "%d (%d)", + "%d (%d)", + "%d (%d)" + }; + + animation = std::make_shared(5, 41, parent->info->creature); + + if(parent->info->stackNode != nullptr && parent->info->commander == nullptr) + { + //normal stack, not a commander and not non-existing stack (e.g. recruitment dialog) + animation->setAmount(parent->info->creatureCount); + } + + name = std::make_shared(215, 12, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName()); + + int dmgMultiply = 1; + if(parent->info->owner && parent->info->stackNode->hasBonusOfType(BonusType::SIEGE_WEAPON)) + dmgMultiply += parent->info->owner->getPrimSkillLevel(PrimarySkill::ATTACK); + + icons = std::make_shared(ImagePath::builtin("stackWindow/icons"), 117, 32); + + const CStack * battleStack = parent->info->stack; + + morale = std::make_shared(true, Rect(Point(321, 110), Point(42, 42) )); + luck = std::make_shared(false, Rect(Point(375, 110), Point(42, 42) )); + + if(battleStack != nullptr) // in battle + { + addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(battleStack->isShooter()), battleStack->getAttack(battleStack->isShooter())); + addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(battleStack->isShooter()), battleStack->getDefense(battleStack->isShooter())); + addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(battleStack->isShooter()) * dmgMultiply, battleStack->getMaxDamage(battleStack->isShooter()) * dmgMultiply); + addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), battleStack->getMaxHealth()); + addStatLabel(EStat::SPEED, parent->info->creature->speed(), battleStack->speed()); + + if(battleStack->isShooter()) + addStatLabel(EStat::SHOTS, battleStack->shots.total(), battleStack->shots.available()); + if(battleStack->isCaster()) + addStatLabel(EStat::MANA, battleStack->casts.total(), battleStack->casts.available()); + addStatLabel(EStat::HEALTH_LEFT, battleStack->getFirstHPleft()); + + morale->set(battleStack); + luck->set(battleStack); + } + else + { + const bool shooter = parent->info->stackNode->hasBonusOfType(BonusType::SHOOTER) && parent->info->stackNode->valOfBonuses(BonusType::SHOTS); + const bool caster = parent->info->stackNode->valOfBonuses(BonusType::CASTS); + + addStatLabel(EStat::ATTACK, parent->info->creature->getAttack(shooter), parent->info->stackNode->getAttack(shooter)); + addStatLabel(EStat::DEFENCE, parent->info->creature->getDefense(shooter), parent->info->stackNode->getDefense(shooter)); + addStatLabel(EStat::DAMAGE, parent->info->stackNode->getMinDamage(shooter) * dmgMultiply, parent->info->stackNode->getMaxDamage(shooter) * dmgMultiply); + addStatLabel(EStat::HEALTH, parent->info->creature->getMaxHealth(), parent->info->stackNode->getMaxHealth()); + addStatLabel(EStat::SPEED, parent->info->creature->speed(), parent->info->stackNode->speed()); + + if(shooter) + addStatLabel(EStat::SHOTS, parent->info->stackNode->valOfBonuses(BonusType::SHOTS)); + if(caster) + addStatLabel(EStat::MANA, parent->info->stackNode->valOfBonuses(BonusType::CASTS)); + + morale->set(parent->info->stackNode); + luck->set(parent->info->stackNode); + } + + if(showExp) + { + const CStackInstance * stack = parent->info->stackNode; + Point pos = showArt ? Point(321, 32) : Point(347, 32); + if(parent->info->commander) + { + const CCommanderInstance * commander = parent->info->commander; + expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); + + auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); + expArea = area; + area->text = CGI->generaltexth->allTexts[2]; + area->bonusValue = commander->getExpRank(); + boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); + boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); + boost::replace_first(area->text, "%d", std::to_string(commander->experience)); + } + else + { + expRankIcon = std::make_shared(AnimationPath::builtin("stackWindow/levels"), stack->getExpRank(), 0, pos.x, pos.y); + expArea = std::make_shared(Rect(pos.x, pos.y, 44, 44)); + expArea->text = parent->generateStackExpDescription(); + } + expLabel = std::make_shared( + pos.x + 21, pos.y + 52, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, + TextOperations::formatMetric(stack->experience, 6)); + } + + if(showArt) + { + Point pos = showExp ? Point(375, 32) : Point(347, 32); + // ALARMA: do not refactor this into a separate function + // otherwise, artifact icon is drawn near the hero's portrait + // this is really strange + auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); + if(art) + { + parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); + parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); + parent->stackArtifactHelp->type = art->artType->getId(); + + if(parent->info->owner) + { + parent->stackArtifactButton = std::make_shared( + Point(pos.x - 2 , pos.y + 46), AnimationPath::builtin("stackWindow/cancelButton"), + CButton::tooltipLocalized("vcmi.creatureWindow.returnArtifact"), [=]() + { + parent->removeStackArtifact(ArtifactPosition::CREATURE_SLOT); + }); + } + } + } +} + + +ImagePath CStackWindow::MainSection::getBackgroundName(bool showExp, bool showArt) +{ + if(showExp && showArt) + return ImagePath::builtin("stackWindow/info-panel-2"); + else if(showExp || showArt) + return ImagePath::builtin("stackWindow/info-panel-1"); + else + return ImagePath::builtin("stackWindow/info-panel-0"); +} + +void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value1, int64_t value2) +{ + const auto title = statNames.at(static_cast(index)); + stats.push_back(std::make_shared(145, 32 + (int)index*19, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, title)); + + const bool useRange = value1 != value2; + std::string formatStr = useRange ? statFormats.at(static_cast(index)) : "%d"; + + boost::format fmt(formatStr); + fmt % value1; + if(useRange) + fmt % value2; + + stats.push_back(std::make_shared(307, 48 + (int)index*19, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, fmt.str())); +} + +void CStackWindow::MainSection::addStatLabel(EStat index, int64_t value) +{ + addStatLabel(index, value, value); +} + +CStackWindow::CStackWindow(const CStack * stack, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stack = stack; + info->stackNode = stack->base; + info->creature = stack->unitType(); + info->creatureCount = stack->getCount(); + info->popupWindow = popup; + init(); +} + +CStackWindow::CStackWindow(const CCreature * creature, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->creature = creature; + info->popupWindow = popup; + init(); +} + +CStackWindow::CStackWindow(const CStackInstance * stack, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stackNode = stack; + info->creature = stack->type; + info->creatureCount = stack->count; + info->popupWindow = popup; + info->owner = dynamic_cast (stack->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & upgradeInfo, std::function callback) + : CWindowObject(BORDERED), + info(new UnitView()) +{ + info->stackNode = stack; + info->creature = stack->type; + info->creatureCount = stack->count; + + info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo()); + info->dismissInfo = std::make_optional(UnitView::StackDismissInfo()); + info->upgradeInfo->info = upgradeInfo; + info->upgradeInfo->callback = callback; + info->dismissInfo->callback = dismiss; + info->owner = dynamic_cast (stack->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CCommanderInstance * commander, bool popup) + : CWindowObject(BORDERED | (popup ? RCLICK_POPUP : 0)), + info(new UnitView()) +{ + info->stackNode = commander; + info->creature = commander->type; + info->commander = commander; + info->creatureCount = 1; + info->popupWindow = popup; + info->owner = dynamic_cast (commander->armyObj); + init(); +} + +CStackWindow::CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback) + : CWindowObject(BORDERED), + info(new UnitView()) +{ + info->stackNode = commander; + info->creature = commander->type; + info->commander = commander; + info->creatureCount = 1; + info->levelupInfo = std::make_optional(UnitView::CommanderLevelInfo()); + info->levelupInfo->skills = skills; + info->levelupInfo->callback = callback; + info->owner = dynamic_cast (commander->armyObj); + init(); +} + +CStackWindow::~CStackWindow() +{ + if(info->levelupInfo && !info->levelupInfo->skills.empty()) + info->levelupInfo->callback(vstd::find_pos(info->levelupInfo->skills, selectedSkill)); +} + +void CStackWindow::init() +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(!info->stackNode) + info->stackNode = new CStackInstance(info->creature, 1, true);// FIXME: free data + + selectedIcon = nullptr; + selectedSkill = -1; + if(info->levelupInfo && !info->levelupInfo->skills.empty()) + selectedSkill = info->levelupInfo->skills.front(); + + activeTab = 0; + + initBonusesList(); + initSections(); +} + +void CStackWindow::initBonusesList() +{ + BonusList output, input; + input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all)); + + while(!input.empty()) + { + auto b = input.front(); + output.push_back(std::make_shared(*b)); + output.back()->val = input.valOfBonuses(Selector::typeSubtype(b->type, b->subtype)); //merge multiple bonuses into one + input.remove_if (Selector::typeSubtype(b->type, b->subtype)); //remove used bonuses + } + + BonusInfo bonusInfo; + for(auto b : output) + { + bonusInfo.name = info->stackNode->bonusToString(b, false); + bonusInfo.description = info->stackNode->bonusToString(b, true); + bonusInfo.imagePath = info->stackNode->bonusToGraphics(b); + + //if it's possible to give any description or image for this kind of bonus + //TODO: figure out why half of bonuses don't have proper description + if(!bonusInfo.name.empty() || !bonusInfo.imagePath.empty()) + activeBonuses.push_back(bonusInfo); + } +} + +void CStackWindow::initSections() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + bool showArt = CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT) && info->commander == nullptr && info->stackNode; + bool showExp = (CGI->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) || info->commander != nullptr) && info->stackNode; + + mainSection = std::make_shared(this, pos.h, showExp, showArt); + + pos.w = mainSection->pos.w; + pos.h += mainSection->pos.h; + + if(info->stack) // in battle + { + activeSpellsSection = std::make_shared(this, pos.h); + pos.h += activeSpellsSection->pos.h; + } + + if(info->commander) + { + auto onCreate = [=](size_t index) -> std::shared_ptr + { + auto obj = switchTab(index); + + if(obj) + { + obj->activate(); + obj->recActions |= (UPDATE | SHOWALL); + } + return obj; + }; + + auto deactivateObj = [=](std::shared_ptr obj) + { + obj->deactivate(); + obj->recActions &= ~(UPDATE | SHOWALL); + }; + + commanderMainSection = std::make_shared(this, 0); + + auto size = std::make_optional((info->levelupInfo) ? 4 : 3); + commanderBonusesSection = std::make_shared(this, 0, size); + deactivateObj(commanderBonusesSection); + + commanderTab = std::make_shared(onCreate, Point(0, pos.h), 0); + + pos.h += commanderMainSection->pos.h; + } + + if(!info->commander && !activeBonuses.empty()) + { + bonusesSection = std::make_shared(this, pos.h); + pos.h += bonusesSection->pos.h; + } + + if(!info->popupWindow) + { + buttonsSection = std::make_shared(this, pos.h); + pos.h += buttonsSection->pos.h; + //FIXME: add status bar to image? + } + updateShadow(); + pos = center(pos); +} + +std::string CStackWindow::generateStackExpDescription() +{ + const CStackInstance * stack = info->stackNode; + const CCreature * creature = info->creature; + + int tier = stack->type->getLevel(); + int rank = stack->getExpRank(); + if (!vstd::iswithin(tier, 1, 7)) + tier = 0; + int number; + std::string expText = CGI->generaltexth->translate("vcmi.stackExperience.description"); + boost::replace_first(expText, "%s", creature->getNamePluralTranslated()); + boost::replace_first(expText, "%s", CGI->generaltexth->translate("vcmi.stackExperience.rank", rank)); + boost::replace_first(expText, "%i", std::to_string(rank)); + boost::replace_first(expText, "%i", std::to_string(stack->experience)); + number = static_cast(CGI->creh->expRanks[tier][rank] - stack->experience); + boost::replace_first(expText, "%i", std::to_string(number)); + + number = CGI->creh->maxExpPerBattle[tier]; //percent + boost::replace_first(expText, "%i%", std::to_string(number)); + number *= CGI->creh->expRanks[tier].back() / 100; //actual amount + boost::replace_first(expText, "%i", std::to_string(number)); + + boost::replace_first(expText, "%i", std::to_string(stack->count)); //Number of Creatures in stack + + int expmin = std::max(CGI->creh->expRanks[tier][std::max(rank-1, 0)], (ui32)1); + number = static_cast((stack->count * (stack->experience - expmin)) / expmin); //Maximum New Recruits without losing current Rank + boost::replace_first(expText, "%i", std::to_string(number)); //TODO + + boost::replace_first(expText, "%.2f", std::to_string(1)); //TODO Experience Multiplier + number = CGI->creh->expAfterUpgrade; + boost::replace_first(expText, "%.2f", std::to_string(number) + "%"); //Upgrade Multiplier + + expmin = CGI->creh->expRanks[tier][9]; + int expmax = CGI->creh->expRanks[tier][10]; + number = expmax - expmin; + boost::replace_first(expText, "%i", std::to_string(number)); //Experience after Rank 10 + number = (stack->count * (expmax - expmin)) / expmin; + boost::replace_first(expText, "%i", std::to_string(number)); //Maximum New Recruits to remain at Rank 10 if at Maximum Experience + + return expText; +} + +void CStackWindow::setSelection(si32 newSkill, std::shared_ptr newIcon) +{ + auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string + { + if(selected) + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description + else + return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)]; + }; + + auto getSkillImage = [this](int skillIndex) + { + bool selected = ((selectedSkill == skillIndex) && info->levelupInfo ); + return skillToFile(skillIndex, info->commander->secondarySkills[skillIndex], selected); + }; + + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + int oldSelection = selectedSkill; // update selection + selectedSkill = newSkill; + + if(selectedIcon && oldSelection < 100) // recreate image on old selection, only for skills + selectedIcon->setObject(std::make_shared(getSkillImage(oldSelection))); + + if(selectedIcon) + { + if(!selectedIcon->getIsMasterAbility()) //unlike WoG, in VCMI master skill descriptions are taken from bonus descriptions + { + selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level + } + selectedIcon->deselect(); + } + + selectedIcon = newIcon; // update new selection + if(newSkill < 100) + { + newIcon->setObject(std::make_shared(getSkillImage(newSkill))); + if(!newIcon->getIsMasterAbility()) + { + newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description + } + } +} + +std::shared_ptr CStackWindow::switchTab(size_t index) +{ + std::shared_ptr ret; + switch(index) + { + case 0: + { + activeTab = 0; + ret = commanderMainSection; + } + break; + case 1: + { + activeTab = 1; + ret = commanderBonusesSection; + } + break; + default: + break; + } + + return ret; +} + +void CStackWindow::removeStackArtifact(ArtifactPosition pos) +{ + auto art = info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT); + if(!art) + { + logGlobal->error("Attempt to remove missing artifact"); + return; + } + const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); + if(slot != ArtifactPosition::PRE_FIRST) + { + LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot)); + stackArtifactButton.reset(); + stackArtifactHelp.reset(); + stackArtifactIcon.reset(); + redraw(); + } +} + diff --git a/client/windows/CCreatureWindow.h b/client/windows/CCreatureWindow.h index 3c9241fd3..b20a1ef39 100644 --- a/client/windows/CCreatureWindow.h +++ b/client/windows/CCreatureWindow.h @@ -1,206 +1,206 @@ -/* - * CCreatureWindow.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/bonuses/Bonus.h" -#include "../../lib/filesystem/ResourcePath.h" -#include "../widgets/MiscWidgets.h" -#include "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCommanderInstance; -class CStackInstance; -class CStack; -struct UpgradeInfo; - -VCMI_LIB_NAMESPACE_END - -class UnitView; -class CTabbedInt; -class CButton; -class CMultiLineLabel; -class CListBox; -class CCommanderArtPlace; - -class CCommanderSkillIcon : public LRClickableAreaWText //TODO: maybe bring commander skill button initialization logic inside? -{ - std::shared_ptr object; // passive object that will be used to determine clickable area - bool isMasterAbility; // refers to WoG abilities obtainable via combining master skills (for example attack + speed unlocks shoot) - bool isSelected; // used only for programatically created border around selected "master abilities" -public: - CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback); - - std::function callback; - - void clickPressed(const Point & cursorPosition) override; - - void setObject(std::shared_ptr object); - void deselect(); //TODO: consider using observer pattern instead? - bool getIsMasterAbility(); - - void show(Canvas &to) override; -}; - -class CStackWindow : public CWindowObject -{ - struct BonusInfo - { - std::string name; - std::string description; - ImagePath imagePath; - }; - - class CWindowSection : public CIntObject - { - private: - std::shared_ptr background; - protected: - CStackWindow * parent; - public: - CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset); - }; - - class ActiveSpellsSection : public CWindowSection - { - std::vector> spellIcons; - std::vector> clickableAreas; - public: - ActiveSpellsSection(CStackWindow * owner, int yOffset); - }; - - class BonusLineSection : public CWindowSection - { - std::array, 2> icon; - std::array, 2> name; - std::array, 2> description; - public: - BonusLineSection(CStackWindow * owner, size_t lineIndex); - }; - - class BonusesSection : public CWindowSection - { - std::shared_ptr lines; - public: - BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize = std::optional()); - }; - - class ButtonsSection : public CWindowSection - { - std::shared_ptr dismiss; - std::array, 3> upgrade;// no more than 3 buttons - space limit - std::shared_ptr exit; - public: - ButtonsSection(CStackWindow * owner, int yOffset); - }; - - class CommanderMainSection : public CWindowSection - { - std::vector> skillIcons; - std::vector> artifacts; - - std::shared_ptr abilitiesBackground; - std::shared_ptr abilities; - - std::shared_ptr leftBtn; - std::shared_ptr rightBtn; - public: - CommanderMainSection(CStackWindow * owner, int yOffset); - }; - - class MainSection : public CWindowSection - { - enum class EStat : size_t - { - ATTACK, - DEFENCE, - SHOTS, - DAMAGE, - HEALTH, - HEALTH_LEFT, - SPEED, - MANA, - AFTER_LAST - }; - - std::shared_ptr animation; - std::shared_ptr name; - std::shared_ptr icons; - std::shared_ptr morale; - std::shared_ptr luck; - - std::vector> stats; - - std::shared_ptr expRankIcon; - std::shared_ptr expArea; - std::shared_ptr expLabel; - - void addStatLabel(EStat index, int64_t value1, int64_t value2); - void addStatLabel(EStat index, int64_t value); - - static ImagePath getBackgroundName(bool showExp, bool showArt); - - std::array statNames; - std::array statFormats; - public: - MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt); - }; - - std::shared_ptr stackArtifactIcon; - std::shared_ptr stackArtifactHelp; - std::shared_ptr stackArtifactButton; - - - std::shared_ptr info; - std::vector activeBonuses; - size_t activeTab; - std::shared_ptr commanderTab; - - std::map> switchButtons; - - std::shared_ptr mainSection; - std::shared_ptr activeSpellsSection; - std::shared_ptr commanderMainSection; - std::shared_ptr commanderBonusesSection; - std::shared_ptr bonusesSection; - std::shared_ptr buttonsSection; - - std::shared_ptr selectedIcon; - si32 selectedSkill; - - void setSelection(si32 newSkill, std::shared_ptr newIcon); - std::shared_ptr switchTab(size_t index); - - void removeStackArtifact(ArtifactPosition pos); - - void initSections(); - void initBonusesList(); - - void init(); - - std::string generateStackExpDescription(); - -public: - // for battles - CStackWindow(const CStack * stack, bool popup); - - // for non-existing stacks, e.g. recruit screen - CStackWindow(const CCreature * creature, bool popup); - - // for normal stacks in armies - CStackWindow(const CStackInstance * stack, bool popup); - CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & info, std::function callback); - - // for commanders & commander level-up dialog - CStackWindow(const CCommanderInstance * commander, bool popup); - CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback); - - ~CStackWindow(); -}; +/* + * CCreatureWindow.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/bonuses/Bonus.h" +#include "../../lib/filesystem/ResourcePath.h" +#include "../widgets/MiscWidgets.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCommanderInstance; +class CStackInstance; +class CStack; +struct UpgradeInfo; + +VCMI_LIB_NAMESPACE_END + +class UnitView; +class CTabbedInt; +class CButton; +class CMultiLineLabel; +class CListBox; +class CCommanderArtPlace; + +class CCommanderSkillIcon : public LRClickableAreaWText //TODO: maybe bring commander skill button initialization logic inside? +{ + std::shared_ptr object; // passive object that will be used to determine clickable area + bool isMasterAbility; // refers to WoG abilities obtainable via combining master skills (for example attack + speed unlocks shoot) + bool isSelected; // used only for programatically created border around selected "master abilities" +public: + CCommanderSkillIcon(std::shared_ptr object_, bool isMasterAbility_, std::function callback); + + std::function callback; + + void clickPressed(const Point & cursorPosition) override; + + void setObject(std::shared_ptr object); + void deselect(); //TODO: consider using observer pattern instead? + bool getIsMasterAbility(); + + void show(Canvas &to) override; +}; + +class CStackWindow : public CWindowObject +{ + struct BonusInfo + { + std::string name; + std::string description; + ImagePath imagePath; + }; + + class CWindowSection : public CIntObject + { + private: + std::shared_ptr background; + protected: + CStackWindow * parent; + public: + CWindowSection(CStackWindow * parent, const ImagePath & backgroundPath, int yOffset); + }; + + class ActiveSpellsSection : public CWindowSection + { + std::vector> spellIcons; + std::vector> clickableAreas; + public: + ActiveSpellsSection(CStackWindow * owner, int yOffset); + }; + + class BonusLineSection : public CWindowSection + { + std::array, 2> icon; + std::array, 2> name; + std::array, 2> description; + public: + BonusLineSection(CStackWindow * owner, size_t lineIndex); + }; + + class BonusesSection : public CWindowSection + { + std::shared_ptr lines; + public: + BonusesSection(CStackWindow * owner, int yOffset, std::optional preferredSize = std::optional()); + }; + + class ButtonsSection : public CWindowSection + { + std::shared_ptr dismiss; + std::array, 3> upgrade;// no more than 3 buttons - space limit + std::shared_ptr exit; + public: + ButtonsSection(CStackWindow * owner, int yOffset); + }; + + class CommanderMainSection : public CWindowSection + { + std::vector> skillIcons; + std::vector> artifacts; + + std::shared_ptr abilitiesBackground; + std::shared_ptr abilities; + + std::shared_ptr leftBtn; + std::shared_ptr rightBtn; + public: + CommanderMainSection(CStackWindow * owner, int yOffset); + }; + + class MainSection : public CWindowSection + { + enum class EStat : size_t + { + ATTACK, + DEFENCE, + SHOTS, + DAMAGE, + HEALTH, + HEALTH_LEFT, + SPEED, + MANA, + AFTER_LAST + }; + + std::shared_ptr animation; + std::shared_ptr name; + std::shared_ptr icons; + std::shared_ptr morale; + std::shared_ptr luck; + + std::vector> stats; + + std::shared_ptr expRankIcon; + std::shared_ptr expArea; + std::shared_ptr expLabel; + + void addStatLabel(EStat index, int64_t value1, int64_t value2); + void addStatLabel(EStat index, int64_t value); + + static ImagePath getBackgroundName(bool showExp, bool showArt); + + std::array statNames; + std::array statFormats; + public: + MainSection(CStackWindow * owner, int yOffset, bool showExp, bool showArt); + }; + + std::shared_ptr stackArtifactIcon; + std::shared_ptr stackArtifactHelp; + std::shared_ptr stackArtifactButton; + + + std::shared_ptr info; + std::vector activeBonuses; + size_t activeTab; + std::shared_ptr commanderTab; + + std::map> switchButtons; + + std::shared_ptr mainSection; + std::shared_ptr activeSpellsSection; + std::shared_ptr commanderMainSection; + std::shared_ptr commanderBonusesSection; + std::shared_ptr bonusesSection; + std::shared_ptr buttonsSection; + + std::shared_ptr selectedIcon; + si32 selectedSkill; + + void setSelection(si32 newSkill, std::shared_ptr newIcon); + std::shared_ptr switchTab(size_t index); + + void removeStackArtifact(ArtifactPosition pos); + + void initSections(); + void initBonusesList(); + + void init(); + + std::string generateStackExpDescription(); + +public: + // for battles + CStackWindow(const CStack * stack, bool popup); + + // for non-existing stacks, e.g. recruit screen + CStackWindow(const CCreature * creature, bool popup); + + // for normal stacks in armies + CStackWindow(const CStackInstance * stack, bool popup); + CStackWindow(const CStackInstance * stack, std::function dismiss, const UpgradeInfo & info, std::function callback); + + // for commanders & commander level-up dialog + CStackWindow(const CCommanderInstance * commander, bool popup); + CStackWindow(const CCommanderInstance * commander, std::vector &skills, std::function callback); + + ~CStackWindow(); +}; diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 68eff233c..ec98bc1d8 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -1,363 +1,363 @@ -/* - * CHeroWindow.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 "CHeroWindow.h" - -#include "CCreatureWindow.h" -#include "CHeroBackpackWindow.h" -#include "CKingdomInterface.h" -#include "GUIClasses.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/TextAlignment.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/TextControls.h" -#include "../widgets/Buttons.h" -#include "../render/CAnimation.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" - -#include "../lib/ArtifactUtils.h" -#include "../lib/CArtHandler.h" -#include "../lib/CConfigHandler.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/CSkillHandler.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/NetPacksBase.h" - -void CHeroSwitcher::clickPressed(const Point & cursorPosition) -{ - //TODO: do not recreate window - if (false) - { - owner->update(hero, true); - } - else - { - const CGHeroInstance * buf = hero; - GH.windows().popWindows(1); - GH.windows().createAndPushWindow(buf); - } -} - -CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_) - : CIntObject(LCLICK), - owner(owner_), - hero(hero_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos += pos_; - - image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex()); - pos.w = image->pos.w; - pos.h = image->pos.h; -} - -CHeroWindow::CHeroWindow(const CGHeroInstance * hero) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("HeroScr4")) -{ - auto & heroscrn = CGI->generaltexth->heroscrn; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - curHero = hero; - - banner = std::make_shared(AnimationPath::builtin("CREST58"), LOCPLINT->playerID.getNum(), 0, 606, 8); - name = std::make_shared(190, 38, EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); - title = std::make_shared(190, 65, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); - - statusbar = CGStatusBar::create(7, 559, ImagePath::builtin("ADROLLVR.bmp"), 660); - - quitButton = std::make_shared(Point(609, 516), AnimationPath::builtin("hsbtns.def"), CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); - - if(settings["general"]["enableUiEnhancements"].Bool()) - { - questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); - backpackButton->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); - dismissButton = std::make_shared(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); - } - else - { - dismissLabel = std::make_shared(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - questlogLabel = std::make_shared(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); - dismissButton = std::make_shared(Point(454, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); - questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); - } - - formations = std::make_shared(0); - formations->addToggle(0, std::make_shared(Point(481, 483), AnimationPath::builtin("hsbtns6.def"), std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); - formations->addToggle(1, std::make_shared(Point(481, 519), AnimationPath::builtin("hsbtns7.def"), std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); - - if(hero->commander) - { - commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("buttons/commander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); - } - - //right list of heroes - for(int i=0; i < std::min(LOCPLINT->cb->howManyHeroes(false), 8); i++) - heroList.push_back(std::make_shared(this, Point(612, 87 + i * 54), LOCPLINT->cb->getHeroBySerial(i, false))); - - //areas - portraitArea = std::make_shared(Rect(18, 18, 58, 64)); - portraitImage = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 19, 19); - - for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) - { - auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill); - area->text = CGI->generaltexth->arraytxt[2+v]; - area->type = v; - area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); - primSkillAreas.push_back(area); - - auto value = std::make_shared(53 + 70 * v, 166, FONT_SMALL, ETextAlignment::CENTER); - primSkillValues.push_back(value); - } - - auto primSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL42")); - primSkills->preload(); - primSkillImages.push_back(std::make_shared(primSkills, 0, 0, 32, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 1, 0, 102, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 2, 0, 172, 111)); - primSkillImages.push_back(std::make_shared(primSkills, 3, 0, 162, 230)); - primSkillImages.push_back(std::make_shared(primSkills, 4, 0, 20, 230)); - primSkillImages.push_back(std::make_shared(primSkills, 5, 0, 242, 111)); - - specImage = std::make_shared(AnimationPath::builtin("UN44"), 0, 0, 18, 180); - specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); - specName = std::make_shared(69, 205); - - expArea = std::make_shared(Rect(18, 228, 136, 42), CGI->generaltexth->heroscrn[9]); - morale = std::make_shared(true, Rect(175, 179, 53, 45)); - luck = std::make_shared(false, Rect(233, 179, 53, 45)); - spellPointsArea = std::make_shared(Rect(162,228, 136, 42), CGI->generaltexth->heroscrn[22]); - - expValue = std::make_shared(68, 252); - manaValue = std::make_shared(211, 252); - - auto secSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSKILL")); - for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) - { - Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); - secSkillAreas.push_back(std::make_shared(r, CComponent::secskill)); - secSkillImages.push_back(std::make_shared(secSkills, 0, 0, r.x, r.y)); - - int x = (i % 2) ? 212 : 68; - int y = 280 + 48 * (i/2); - - secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT)); - secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT)); - } - - // various texts - labels.push_back(std::make_shared(52, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[1])); - labels.push_back(std::make_shared(123, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[2])); - labels.push_back(std::make_shared(193, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[3])); - labels.push_back(std::make_shared(262, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[4])); - - labels.push_back(std::make_shared(69, 183, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[5])); - labels.push_back(std::make_shared(69, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[6])); - labels.push_back(std::make_shared(213, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[7])); - - update(hero); -} - -void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) -{ - auto & heroscrn = CGI->generaltexth->heroscrn; - - if(!hero) //something strange... no hero? it shouldn't happen - { - logGlobal->error("Set nullptr hero? no way..."); - return; - } - - assert(hero == curHero); - - name->setText(curHero->getNameTranslated()); - title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str()); - - specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); - specImage->setFrame(curHero->type->imageIndex); - specName->setText(curHero->type->getSpecialtyNameTranslated()); - - tacticsButton = std::make_shared(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); - tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); - - dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); - portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); - portraitArea->text = curHero->getBiographyTranslated(); - portraitImage->setFrame(curHero->getIconIndex()); - - { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - if(!garr) - { - std::string helpBox = heroscrn[32]; - boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]); - - garr = std::make_shared(Point(15, 485), 8, Point(), curHero); - auto split = std::make_shared(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }); - garr->addSplitBtn(split); - } - if(!arts) - { - arts = std::make_shared(Point(-65, -8)); - arts->setHero(curHero); - addSetAndCallbacks(arts); - } - - int serial = LOCPLINT->cb->getHeroSerial(curHero, false); - - listSelection.reset(); - if(serial >= 0) - listSelection = std::make_shared(ImagePath::builtin("HPSYYY"), 612, 33 + serial * 54); - } - - //primary skills support - for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); - primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); - } - - //secondary skills support - for(size_t g=0; g< secSkillAreas.size(); ++g) - { - int skill = curHero->secSkills[g].first; - int level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first)); - std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); - std::string skillValue = CGI->generaltexth->levels[level-1]; - - secSkillAreas[g]->type = skill; - secSkillAreas[g]->bonusValue = level; - secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); - secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName); - secSkillImages[g]->setFrame(skill*3 + level + 2); - secSkillNames[g]->setText(skillName); - secSkillValues[g]->setText(skillValue); - } - - std::ostringstream expstr; - expstr << curHero->exp; - expValue->setText(expstr.str()); - - std::ostringstream manastr; - manastr << curHero->mana << '/' << curHero->manaLimit(); - manaValue->setText(manastr.str()); - - //printing experience - original format does not support ui64 - expArea->text = CGI->generaltexth->allTexts[2]; - boost::replace_first(expArea->text, "%d", std::to_string(curHero->level)); - boost::replace_first(expArea->text, "%d", std::to_string(CGI->heroh->reqExp(curHero->level+1))); - boost::replace_first(expArea->text, "%d", std::to_string(curHero->exp)); - - //printing spell points, boost::format can't be used due to locale issues - spellPointsArea->text = CGI->generaltexth->allTexts[205]; - boost::replace_first(spellPointsArea->text, "%s", curHero->getNameTranslated()); - boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->mana)); - boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->manaLimit())); - - //if we have exchange window with this curHero open - bool noDismiss=false; - - for(auto cew : GH.windows().findWindows()) - { - for(int g=0; g < cew->heroInst.size(); ++g) - if(cew->heroInst[g] == curHero) - noDismiss = true; - } - - //if player only have one hero and no towns - if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1) - noDismiss = true; - - if(curHero->isMissionCritical()) - noDismiss = true; - - dismissButton->block(noDismiss); - - if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0) - { - tacticsButton->block(true); - } - else - { - tacticsButton->block(false); - tacticsButton->addCallback([&](bool on){curHero->tacticFormationEnabled = on;}); - } - - formations->resetCallback(); - //setting formations - formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); - formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, value);}); - - morale->set(curHero); - luck->set(curHero); - - if(redrawNeeded) - redraw(); -} - -void CHeroWindow::dismissCurrent() -{ - CFunctionList ony = [=](){ close(); }; - ony += [=](){ LOCPLINT->cb->dismissHero(curHero); }; - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); -} - -void CHeroWindow::createBackpackWindow() -{ - GH.windows().createAndPushWindow(curHero); -} - -void CHeroWindow::commanderWindow() -{ - const auto pickedArtInst = getPickedArtifact(); - const auto hero = getHeroPickedArtifact(); - - if(pickedArtInst) - { - const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); - if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack! - { - ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(curHero->commander.get(), freeSlot); - - if(pickedArtInst->canBePutAt(dst, true)) - { //equip clicked stack - if(dst.getArt()) - { - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero, - ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); - } - LOCPLINT->cb->swapArtifacts(src, dst); - } - } - } - else - { - GH.windows().createAndPushWindow(curHero->commander, false); - } -} - -void CHeroWindow::updateGarrisons() -{ - garr->recreateSlots(); - morale->set(curHero); -} +/* + * CHeroWindow.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 "CHeroWindow.h" + +#include "CCreatureWindow.h" +#include "CHeroBackpackWindow.h" +#include "CKingdomInterface.h" +#include "GUIClasses.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/TextAlignment.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/TextControls.h" +#include "../widgets/Buttons.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" + +#include "../lib/ArtifactUtils.h" +#include "../lib/CArtHandler.h" +#include "../lib/CConfigHandler.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/CSkillHandler.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/NetPacksBase.h" + +void CHeroSwitcher::clickPressed(const Point & cursorPosition) +{ + //TODO: do not recreate window + if (false) + { + owner->update(hero, true); + } + else + { + const CGHeroInstance * buf = hero; + GH.windows().popWindows(1); + GH.windows().createAndPushWindow(buf); + } +} + +CHeroSwitcher::CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_) + : CIntObject(LCLICK), + owner(owner_), + hero(hero_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos += pos_; + + image = std::make_shared(AnimationPath::builtin("PortraitsSmall"), hero->getIconIndex()); + pos.w = image->pos.w; + pos.h = image->pos.h; +} + +CHeroWindow::CHeroWindow(const CGHeroInstance * hero) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("HeroScr4")) +{ + auto & heroscrn = CGI->generaltexth->heroscrn; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + curHero = hero; + + banner = std::make_shared(AnimationPath::builtin("CREST58"), LOCPLINT->playerID.getNum(), 0, 606, 8); + name = std::make_shared(190, 38, EFonts::FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); + title = std::make_shared(190, 65, EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); + + statusbar = CGStatusBar::create(7, 559, ImagePath::builtin("ADROLLVR.bmp"), 660); + + quitButton = std::make_shared(Point(609, 516), AnimationPath::builtin("hsbtns.def"), CButton::tooltip(heroscrn[17]), [=](){ close(); }, EShortcut::GLOBAL_RETURN); + + if(settings["general"]["enableUiEnhancements"].Bool()) + { + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + backpackButton = std::make_shared(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK); + backpackButton->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + dismissButton = std::make_shared(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + } + else + { + dismissLabel = std::make_shared(CGI->generaltexth->jktexts[8], Rect(370, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + questlogLabel = std::make_shared(CGI->generaltexth->jktexts[9], Rect(510, 430, 65, 35), 0, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE); + dismissButton = std::make_shared(Point(454, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS); + questlogButton = std::make_shared(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG); + } + + formations = std::make_shared(0); + formations->addToggle(0, std::make_shared(Point(481, 483), AnimationPath::builtin("hsbtns6.def"), std::make_pair(heroscrn[23], heroscrn[29]), 0, EShortcut::HERO_TIGHT_FORMATION)); + formations->addToggle(1, std::make_shared(Point(481, 519), AnimationPath::builtin("hsbtns7.def"), std::make_pair(heroscrn[24], heroscrn[30]), 0, EShortcut::HERO_LOOSE_FORMATION)); + + if(hero->commander) + { + commanderButton = std::make_shared(Point(317, 18), AnimationPath::builtin("buttons/commander"), CButton::tooltipLocalized("vcmi.heroWindow.openCommander"), [&](){ commanderWindow(); }, EShortcut::HERO_COMMANDER); + } + + //right list of heroes + for(int i=0; i < std::min(LOCPLINT->cb->howManyHeroes(false), 8); i++) + heroList.push_back(std::make_shared(this, Point(612, 87 + i * 54), LOCPLINT->cb->getHeroBySerial(i, false))); + + //areas + portraitArea = std::make_shared(Rect(18, 18, 58, 64)); + portraitImage = std::make_shared(AnimationPath::builtin("PortraitsLarge"), 0, 0, 19, 19); + + for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) + { + auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill); + area->text = CGI->generaltexth->arraytxt[2+v]; + area->type = v; + area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); + primSkillAreas.push_back(area); + + auto value = std::make_shared(53 + 70 * v, 166, FONT_SMALL, ETextAlignment::CENTER); + primSkillValues.push_back(value); + } + + auto primSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL42")); + primSkills->preload(); + primSkillImages.push_back(std::make_shared(primSkills, 0, 0, 32, 111)); + primSkillImages.push_back(std::make_shared(primSkills, 1, 0, 102, 111)); + primSkillImages.push_back(std::make_shared(primSkills, 2, 0, 172, 111)); + primSkillImages.push_back(std::make_shared(primSkills, 3, 0, 162, 230)); + primSkillImages.push_back(std::make_shared(primSkills, 4, 0, 20, 230)); + primSkillImages.push_back(std::make_shared(primSkills, 5, 0, 242, 111)); + + specImage = std::make_shared(AnimationPath::builtin("UN44"), 0, 0, 18, 180); + specArea = std::make_shared(Rect(18, 180, 136, 42), CGI->generaltexth->heroscrn[27]); + specName = std::make_shared(69, 205); + + expArea = std::make_shared(Rect(18, 228, 136, 42), CGI->generaltexth->heroscrn[9]); + morale = std::make_shared(true, Rect(175, 179, 53, 45)); + luck = std::make_shared(false, Rect(233, 179, 53, 45)); + spellPointsArea = std::make_shared(Rect(162,228, 136, 42), CGI->generaltexth->heroscrn[22]); + + expValue = std::make_shared(68, 252); + manaValue = std::make_shared(211, 252); + + auto secSkills = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSKILL")); + for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) + { + Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); + secSkillAreas.push_back(std::make_shared(r, CComponent::secskill)); + secSkillImages.push_back(std::make_shared(secSkills, 0, 0, r.x, r.y)); + + int x = (i % 2) ? 212 : 68; + int y = 280 + 48 * (i/2); + + secSkillValues.push_back(std::make_shared(x, y, FONT_SMALL, ETextAlignment::TOPLEFT)); + secSkillNames.push_back(std::make_shared(x, y+20, FONT_SMALL, ETextAlignment::TOPLEFT)); + } + + // various texts + labels.push_back(std::make_shared(52, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[1])); + labels.push_back(std::make_shared(123, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[2])); + labels.push_back(std::make_shared(193, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[3])); + labels.push_back(std::make_shared(262, 99, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[4])); + + labels.push_back(std::make_shared(69, 183, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[5])); + labels.push_back(std::make_shared(69, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[6])); + labels.push_back(std::make_shared(213, 232, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->jktexts[7])); + + update(hero); +} + +void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) +{ + auto & heroscrn = CGI->generaltexth->heroscrn; + + if(!hero) //something strange... no hero? it shouldn't happen + { + logGlobal->error("Set nullptr hero? no way..."); + return; + } + + assert(hero == curHero); + + name->setText(curHero->getNameTranslated()); + title->setText((boost::format(CGI->generaltexth->allTexts[342]) % curHero->level % curHero->type->heroClass->getNameTranslated()).str()); + + specArea->text = curHero->type->getSpecialtyDescriptionTranslated(); + specImage->setFrame(curHero->type->imageIndex); + specName->setText(curHero->type->getSpecialtyNameTranslated()); + + tacticsButton = std::make_shared(Point(539, 483), AnimationPath::builtin("hsbtns8.def"), std::make_pair(heroscrn[26], heroscrn[31]), 0, EShortcut::HERO_TOGGLE_TACTICS); + tacticsButton->addHoverText(CButton::HIGHLIGHTED, CGI->generaltexth->heroscrn[25]); + + dismissButton->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->heroscrn[16]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated())); + portraitArea->hoverText = boost::str(boost::format(CGI->generaltexth->allTexts[15]) % curHero->getNameTranslated() % curHero->type->heroClass->getNameTranslated()); + portraitArea->text = curHero->getBiographyTranslated(); + portraitImage->setFrame(curHero->getIconIndex()); + + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + if(!garr) + { + std::string helpBox = heroscrn[32]; + boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]); + + garr = std::make_shared(Point(15, 485), 8, Point(), curHero); + auto split = std::make_shared(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }); + garr->addSplitBtn(split); + } + if(!arts) + { + arts = std::make_shared(Point(-65, -8)); + arts->setHero(curHero); + addSetAndCallbacks(arts); + } + + int serial = LOCPLINT->cb->getHeroSerial(curHero, false); + + listSelection.reset(); + if(serial >= 0) + listSelection = std::make_shared(ImagePath::builtin("HPSYYY"), 612, 33 + serial * 54); + } + + //primary skills support + for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); + primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); + } + + //secondary skills support + for(size_t g=0; g< secSkillAreas.size(); ++g) + { + int skill = curHero->secSkills[g].first; + int level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first)); + std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); + std::string skillValue = CGI->generaltexth->levels[level-1]; + + secSkillAreas[g]->type = skill; + secSkillAreas[g]->bonusValue = level; + secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); + secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName); + secSkillImages[g]->setFrame(skill*3 + level + 2); + secSkillNames[g]->setText(skillName); + secSkillValues[g]->setText(skillValue); + } + + std::ostringstream expstr; + expstr << curHero->exp; + expValue->setText(expstr.str()); + + std::ostringstream manastr; + manastr << curHero->mana << '/' << curHero->manaLimit(); + manaValue->setText(manastr.str()); + + //printing experience - original format does not support ui64 + expArea->text = CGI->generaltexth->allTexts[2]; + boost::replace_first(expArea->text, "%d", std::to_string(curHero->level)); + boost::replace_first(expArea->text, "%d", std::to_string(CGI->heroh->reqExp(curHero->level+1))); + boost::replace_first(expArea->text, "%d", std::to_string(curHero->exp)); + + //printing spell points, boost::format can't be used due to locale issues + spellPointsArea->text = CGI->generaltexth->allTexts[205]; + boost::replace_first(spellPointsArea->text, "%s", curHero->getNameTranslated()); + boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->mana)); + boost::replace_first(spellPointsArea->text, "%d", std::to_string(curHero->manaLimit())); + + //if we have exchange window with this curHero open + bool noDismiss=false; + + for(auto cew : GH.windows().findWindows()) + { + for(int g=0; g < cew->heroInst.size(); ++g) + if(cew->heroInst[g] == curHero) + noDismiss = true; + } + + //if player only have one hero and no towns + if(!LOCPLINT->cb->howManyTowns() && LOCPLINT->cb->howManyHeroes() == 1) + noDismiss = true; + + if(curHero->isMissionCritical()) + noDismiss = true; + + dismissButton->block(noDismiss); + + if(curHero->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)) == 0) + { + tacticsButton->block(true); + } + else + { + tacticsButton->block(false); + tacticsButton->addCallback([&](bool on){curHero->tacticFormationEnabled = on;}); + } + + formations->resetCallback(); + //setting formations + formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); + formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, value);}); + + morale->set(curHero); + luck->set(curHero); + + if(redrawNeeded) + redraw(); +} + +void CHeroWindow::dismissCurrent() +{ + CFunctionList ony = [=](){ close(); }; + ony += [=](){ LOCPLINT->cb->dismissHero(curHero); }; + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr); +} + +void CHeroWindow::createBackpackWindow() +{ + GH.windows().createAndPushWindow(curHero); +} + +void CHeroWindow::commanderWindow() +{ + const auto pickedArtInst = getPickedArtifact(); + const auto hero = getHeroPickedArtifact(); + + if(pickedArtInst) + { + const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); + if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack! + { + ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); + ArtifactLocation dst(curHero->commander.get(), freeSlot); + + if(pickedArtInst->canBePutAt(dst, true)) + { //equip clicked stack + if(dst.getArt()) + { + LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero, + ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); + } + LOCPLINT->cb->swapArtifacts(src, dst); + } + } + } + else + { + GH.windows().createAndPushWindow(curHero->commander, false); + } +} + +void CHeroWindow::updateGarrisons() +{ + garr->recreateSlots(); + morale->set(curHero); +} diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index 8f87ac7e9..8f5bb2776 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -1,115 +1,115 @@ -/* - * CHeroWindow.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 "../widgets/CWindowWithArtifacts.h" -#include "CWindowObject.h" - -#include "../../lib/bonuses/IBonusBearer.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CHeroWindow; -class LClickableAreaHero; -class LRClickableAreaWText; -class LRClickableAreaWTextComp; -class CArtifactsOfHeroMain; -class MoraleLuckBox; -class CToggleButton; -class CToggleGroup; -class CGStatusBar; -class CTextBox; -class CGarrisonInt; - -/// Button which switches hero selection -class CHeroSwitcher : public CIntObject -{ - const CGHeroInstance * hero; - std::shared_ptr image; - CHeroWindow * owner; -public: - void clickPressed(const Point & cursorPosition) override; - - CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_); -}; - -class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts -{ - std::shared_ptr name; - std::shared_ptr title; - - std::shared_ptr banner; - - std::vector> heroList; - std::shared_ptr listSelection; - - std::shared_ptr portraitArea; - std::shared_ptr portraitImage; - - std::vector> primSkillAreas; - std::vector> primSkillImages; - std::vector> primSkillValues; - - std::shared_ptr expValue; - std::shared_ptr expArea; - - std::shared_ptr manaValue; - std::shared_ptr spellPointsArea; - - std::shared_ptr specArea; - std::shared_ptr specImage; - std::shared_ptr specName; - std::shared_ptr morale; - std::shared_ptr luck; - std::vector> secSkillAreas; - std::vector> secSkillImages; - std::vector> secSkillNames; - std::vector> secSkillValues; - - std::shared_ptr quitButton; - std::shared_ptr dismissLabel; - std::shared_ptr dismissButton; - std::shared_ptr questlogLabel; - std::shared_ptr questlogButton; - std::shared_ptr commanderButton; - std::shared_ptr backpackButton; - - std::shared_ptr tacticsButton; - std::shared_ptr formations; - - std::shared_ptr garr; - std::shared_ptr arts; - - std::vector> labels; - -public: - const CGHeroInstance * curHero; - - CHeroWindow(const CGHeroInstance * hero); - - void update(const CGHeroInstance * hero, bool redrawNeeded = false); //sets main displayed hero - - void dismissCurrent(); //dissmissed currently displayed hero (curHero) - void commanderWindow(); - void switchHero(); //changes displayed hero - void updateGarrisons() override; - void createBackpackWindow(); - - //friends - friend void CHeroArtPlace::clickPressed(const Point & cursorPosition); - friend class CPlayerInterface; -}; +/* + * CHeroWindow.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 "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +#include "../../lib/bonuses/IBonusBearer.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CHeroWindow; +class LClickableAreaHero; +class LRClickableAreaWText; +class LRClickableAreaWTextComp; +class CArtifactsOfHeroMain; +class MoraleLuckBox; +class CToggleButton; +class CToggleGroup; +class CGStatusBar; +class CTextBox; +class CGarrisonInt; + +/// Button which switches hero selection +class CHeroSwitcher : public CIntObject +{ + const CGHeroInstance * hero; + std::shared_ptr image; + CHeroWindow * owner; +public: + void clickPressed(const Point & cursorPosition) override; + + CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_); +}; + +class CHeroWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts +{ + std::shared_ptr name; + std::shared_ptr title; + + std::shared_ptr banner; + + std::vector> heroList; + std::shared_ptr listSelection; + + std::shared_ptr portraitArea; + std::shared_ptr portraitImage; + + std::vector> primSkillAreas; + std::vector> primSkillImages; + std::vector> primSkillValues; + + std::shared_ptr expValue; + std::shared_ptr expArea; + + std::shared_ptr manaValue; + std::shared_ptr spellPointsArea; + + std::shared_ptr specArea; + std::shared_ptr specImage; + std::shared_ptr specName; + std::shared_ptr morale; + std::shared_ptr luck; + std::vector> secSkillAreas; + std::vector> secSkillImages; + std::vector> secSkillNames; + std::vector> secSkillValues; + + std::shared_ptr quitButton; + std::shared_ptr dismissLabel; + std::shared_ptr dismissButton; + std::shared_ptr questlogLabel; + std::shared_ptr questlogButton; + std::shared_ptr commanderButton; + std::shared_ptr backpackButton; + + std::shared_ptr tacticsButton; + std::shared_ptr formations; + + std::shared_ptr garr; + std::shared_ptr arts; + + std::vector> labels; + +public: + const CGHeroInstance * curHero; + + CHeroWindow(const CGHeroInstance * hero); + + void update(const CGHeroInstance * hero, bool redrawNeeded = false); //sets main displayed hero + + void dismissCurrent(); //dissmissed currently displayed hero (curHero) + void commanderWindow(); + void switchHero(); //changes displayed hero + void updateGarrisons() override; + void createBackpackWindow(); + + //friends + friend void CHeroArtPlace::clickPressed(const Point & cursorPosition); + friend class CPlayerInterface; +}; diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index fa0e57687..2b951d2dd 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -1,361 +1,361 @@ -/* - * CKingdomInterface.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 "../widgets/CWindowWithArtifacts.h" -#include "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN -class CGObjectInstance; -VCMI_LIB_NAMESPACE_END - -class CButton; -class CAnimImage; -class CToggleGroup; -class CResDataBar; -class CSlider; -class CTownInfo; -class CCreaInfo; -class HeroSlots; -class LRClickableAreaOpenTown; -class CComponent; -class CHeroArea; -class MoraleLuckBox; -class CListBox; -class CTabbedInt; -class CGStatusBar; -class CGarrisonInt; - -class CKingdHeroList; -class CKingdTownList; -class IInfoBoxData; - -/* - * Several classes to display basically any data. - * Main part - class InfoBox which controls how data will be formatted\positioned - * InfoBox have image and 0-2 labels - * In constructor it should receive object that implements IInfoBoxData interface - * - * interface IInfoBoxData defines way to get data for use in InfoBox - * have several implementations: - * InfoBoxHeroData - to display one of fields from hero (e.g. absolute value of primary skills) - * InfoBoxCustomHeroData - to display one of hero fields without hero (e.g. bonuses from objects) - * InfoBoxTownData - data from town - * InfoBoxCustom - user-defined data - */ - -/// Displays one of object propertries with image and optional labels -class InfoBox : public CIntObject -{ -public: - enum InfoPos - { - POS_UP_DOWN, POS_DOWN, POS_RIGHT, POS_INSIDE, POS_CORNER, POS_NONE - }; - enum InfoSize - { - SIZE_TINY, SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG, SIZE_HUGE - }; - -private: - InfoSize size; - InfoPos infoPos; - std::shared_ptr data; - - std::shared_ptr value; - std::shared_ptr name; - std::shared_ptr image; - std::shared_ptr hover; - -public: - InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr Data); - ~InfoBox(); - - void showPopupWindow(const Point & cursorPosition) override; - void clickPressed(const Point & cursorPosition) override; -}; - -class IInfoBoxData -{ -public: - enum InfoType - { - HERO_PRIMARY_SKILL, HERO_MANA, HERO_EXPERIENCE, HERO_SPECIAL, HERO_SECONDARY_SKILL, - //TODO: Luck? Morale? Artifact? - ARMY_SLOT,//TODO - TOWN_GROWTH, TOWN_AVAILABLE, TOWN_BUILDING,//TODO - CUSTOM - }; - -protected: - InfoType type; - - IInfoBoxData(InfoType Type); - -public: - //methods that generate values for displaying - virtual std::string getValueText()=0; - virtual std::string getNameText()=0; - virtual AnimationPath getImageName(InfoBox::InfoSize size)=0; - virtual std::string getHoverText()=0; - virtual size_t getImageIndex()=0; - - //TODO: replace with something better - virtual void prepareMessage(std::string & text, std::shared_ptr & comp)=0; - - virtual ~IInfoBoxData(){}; -}; - -class InfoBoxAbstractHeroData : public IInfoBoxData -{ -protected: - virtual int getSubID()=0; - virtual si64 getValue()=0; - -public: - InfoBoxAbstractHeroData(InfoType Type); - - std::string getValueText() override; - std::string getNameText() override; - AnimationPath getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -class InfoBoxHeroData : public InfoBoxAbstractHeroData -{ - const CGHeroInstance * hero; - int index;//index of data in hero (0-7 for sec. skill, 0-3 for pr. skill) - - int getSubID() override; - si64 getValue() override; - -public: - InfoBoxHeroData(InfoType Type, const CGHeroInstance *Hero, int Index=0); - - //To get a bit different texts for hero window - std::string getHoverText() override; - std::string getValueText() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -class InfoBoxCustomHeroData : public InfoBoxAbstractHeroData -{ - int subID;//subID of data (0=attack...) - si64 value;//actual value of data, 64-bit to fit experience and negative values - - int getSubID() override; - si64 getValue() override; - -public: - InfoBoxCustomHeroData(InfoType Type, int subID, si64 value); -}; - -class InfoBoxCustom : public IInfoBoxData -{ -public: - std::string valueText; - std::string nameText; - AnimationPath imageName; - std::string hoverText; - size_t imageIndex; - - InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText=""); - - std::string getValueText() override; - std::string getNameText() override; - AnimationPath getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; - - void prepareMessage(std::string & text, std::shared_ptr & comp) override; -}; - -//TODO!!! -class InfoBoxTownData : public IInfoBoxData -{ - const CGTownInstance * town; - int index;//index of data in town - int value;//actual value of data - -public: - InfoBoxTownData(InfoType Type, const CGTownInstance * Town, int Index); - InfoBoxTownData(InfoType Type, int SubID, int Value); - - std::string getValueText() override; - std::string getNameText() override; - AnimationPath getImageName(InfoBox::InfoSize size) override; - std::string getHoverText() override; - size_t getImageIndex() override; -}; - -/// Class which holds all parts of kingdom overview window -class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder, public ITownHolder -{ -private: - struct OwnedObjectInfo - { - int imageID; - ui32 count; - std::string hoverText; - OwnedObjectInfo(): - imageID(0), - count(0) - {} - }; - std::vector objects; - - std::shared_ptr dwellingsList; - std::shared_ptr tabArea; - - //Main buttons - std::shared_ptr btnTowns; - std::shared_ptr btnHeroes; - std::shared_ptr btnExit; - - //Buttons for scrolling dwellings list - std::shared_ptr dwellUp; - std::shared_ptr dwellDown; - std::shared_ptr dwellTop; - std::shared_ptr dwellBottom; - - std::array, 7> minesBox; - - std::shared_ptr incomeArea; - std::shared_ptr incomeAmount; - - std::shared_ptr statusbar; - std::shared_ptr resdatabar; - - void activateTab(size_t which); - - //Internal functions used during construction - void generateButtons(); - void generateObjectsList(const std::vector &ownedObjects); - void generateMinesList(const std::vector &ownedObjects); - - std::shared_ptr createOwnedObject(size_t index); - std::shared_ptr createMainTab(size_t index); - -public: - CKingdomInterface(); - - void townChanged(const CGTownInstance *town); - void heroRemoved(); - void updateGarrisons() override; - void artifactRemoved(const ArtifactLocation &artLoc) override; - void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; - void artifactDisassembled(const ArtifactLocation &artLoc) override; - void artifactAssembled(const ArtifactLocation &artLoc) override; - void buildChanged() override; -}; - -/// List item with town -class CTownItem : public CIntObject, public IGarrisonHolder -{ - std::shared_ptr background; - std::shared_ptr picture; - std::shared_ptr name; - std::shared_ptr income; - std::shared_ptr garr; - - std::shared_ptr heroes; - std::shared_ptr hall; - std::shared_ptr fort; - - std::vector> available; - std::vector> growth; - - std::shared_ptr openTown; - - std::shared_ptr fastTownHall; - std::shared_ptr fastArmyPurchase; - std::shared_ptr fastMarket; - std::shared_ptr fastTavern; - std::shared_ptr fastTown; - -public: - const CGTownInstance * town; - - CTownItem(const CGTownInstance * Town); - - void updateGarrisons() override; - void update(); -}; - -/// List item with hero -class CHeroItem : public CIntObject, public IGarrisonHolder -{ - const CGHeroInstance * hero; - - std::vector> artTabs; - - std::shared_ptr portrait; - std::shared_ptr name; - std::shared_ptr heroArea; - - std::shared_ptr artsText; - std::shared_ptr artsTabs; - - std::shared_ptr artButtons; - std::vector> heroInfo; - std::shared_ptr morale; - std::shared_ptr luck; - - std::shared_ptr garr; - - void onArtChange(int tabIndex); - - std::shared_ptr onTabSelected(size_t index); - -public: - std::shared_ptr heroArts; - - void updateGarrisons() override; - - CHeroItem(const CGHeroInstance * hero); -}; - -/// Tab with all hero-specific data -class CKingdHeroList : public CIntObject, public IGarrisonHolder, public CWindowWithArtifacts -{ -private: - std::shared_ptr heroes; - std::shared_ptr title; - std::shared_ptr heroLabel; - std::shared_ptr skillsLabel; - - std::shared_ptr createHeroItem(size_t index); -public: - CKingdHeroList(size_t maxSize); - - void updateGarrisons() override; -}; - -/// Tab with all town-specific data -class CKingdTownList : public CIntObject, public IGarrisonHolder -{ -private: - std::shared_ptr towns; - std::shared_ptr title; - std::shared_ptr townLabel; - std::shared_ptr garrHeroLabel; - std::shared_ptr visitHeroLabel; - - std::shared_ptr createTownItem(size_t index); -public: - CKingdTownList(size_t maxSize); - - void townChanged(const CGTownInstance * town); - void updateGarrisons() override; -}; +/* + * CKingdomInterface.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 "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGObjectInstance; +VCMI_LIB_NAMESPACE_END + +class CButton; +class CAnimImage; +class CToggleGroup; +class CResDataBar; +class CSlider; +class CTownInfo; +class CCreaInfo; +class HeroSlots; +class LRClickableAreaOpenTown; +class CComponent; +class CHeroArea; +class MoraleLuckBox; +class CListBox; +class CTabbedInt; +class CGStatusBar; +class CGarrisonInt; + +class CKingdHeroList; +class CKingdTownList; +class IInfoBoxData; + +/* + * Several classes to display basically any data. + * Main part - class InfoBox which controls how data will be formatted\positioned + * InfoBox have image and 0-2 labels + * In constructor it should receive object that implements IInfoBoxData interface + * + * interface IInfoBoxData defines way to get data for use in InfoBox + * have several implementations: + * InfoBoxHeroData - to display one of fields from hero (e.g. absolute value of primary skills) + * InfoBoxCustomHeroData - to display one of hero fields without hero (e.g. bonuses from objects) + * InfoBoxTownData - data from town + * InfoBoxCustom - user-defined data + */ + +/// Displays one of object propertries with image and optional labels +class InfoBox : public CIntObject +{ +public: + enum InfoPos + { + POS_UP_DOWN, POS_DOWN, POS_RIGHT, POS_INSIDE, POS_CORNER, POS_NONE + }; + enum InfoSize + { + SIZE_TINY, SIZE_SMALL, SIZE_MEDIUM, SIZE_BIG, SIZE_HUGE + }; + +private: + InfoSize size; + InfoPos infoPos; + std::shared_ptr data; + + std::shared_ptr value; + std::shared_ptr name; + std::shared_ptr image; + std::shared_ptr hover; + +public: + InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr Data); + ~InfoBox(); + + void showPopupWindow(const Point & cursorPosition) override; + void clickPressed(const Point & cursorPosition) override; +}; + +class IInfoBoxData +{ +public: + enum InfoType + { + HERO_PRIMARY_SKILL, HERO_MANA, HERO_EXPERIENCE, HERO_SPECIAL, HERO_SECONDARY_SKILL, + //TODO: Luck? Morale? Artifact? + ARMY_SLOT,//TODO + TOWN_GROWTH, TOWN_AVAILABLE, TOWN_BUILDING,//TODO + CUSTOM + }; + +protected: + InfoType type; + + IInfoBoxData(InfoType Type); + +public: + //methods that generate values for displaying + virtual std::string getValueText()=0; + virtual std::string getNameText()=0; + virtual AnimationPath getImageName(InfoBox::InfoSize size)=0; + virtual std::string getHoverText()=0; + virtual size_t getImageIndex()=0; + + //TODO: replace with something better + virtual void prepareMessage(std::string & text, std::shared_ptr & comp)=0; + + virtual ~IInfoBoxData(){}; +}; + +class InfoBoxAbstractHeroData : public IInfoBoxData +{ +protected: + virtual int getSubID()=0; + virtual si64 getValue()=0; + +public: + InfoBoxAbstractHeroData(InfoType Type); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +class InfoBoxHeroData : public InfoBoxAbstractHeroData +{ + const CGHeroInstance * hero; + int index;//index of data in hero (0-7 for sec. skill, 0-3 for pr. skill) + + int getSubID() override; + si64 getValue() override; + +public: + InfoBoxHeroData(InfoType Type, const CGHeroInstance *Hero, int Index=0); + + //To get a bit different texts for hero window + std::string getHoverText() override; + std::string getValueText() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +class InfoBoxCustomHeroData : public InfoBoxAbstractHeroData +{ + int subID;//subID of data (0=attack...) + si64 value;//actual value of data, 64-bit to fit experience and negative values + + int getSubID() override; + si64 getValue() override; + +public: + InfoBoxCustomHeroData(InfoType Type, int subID, si64 value); +}; + +class InfoBoxCustom : public IInfoBoxData +{ +public: + std::string valueText; + std::string nameText; + AnimationPath imageName; + std::string hoverText; + size_t imageIndex; + + InfoBoxCustom(std::string ValueText, std::string NameText, const AnimationPath & ImageName, size_t ImageIndex, std::string HoverText=""); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; + + void prepareMessage(std::string & text, std::shared_ptr & comp) override; +}; + +//TODO!!! +class InfoBoxTownData : public IInfoBoxData +{ + const CGTownInstance * town; + int index;//index of data in town + int value;//actual value of data + +public: + InfoBoxTownData(InfoType Type, const CGTownInstance * Town, int Index); + InfoBoxTownData(InfoType Type, int SubID, int Value); + + std::string getValueText() override; + std::string getNameText() override; + AnimationPath getImageName(InfoBox::InfoSize size) override; + std::string getHoverText() override; + size_t getImageIndex() override; +}; + +/// Class which holds all parts of kingdom overview window +class CKingdomInterface : public CWindowObject, public IGarrisonHolder, public CArtifactHolder, public ITownHolder +{ +private: + struct OwnedObjectInfo + { + int imageID; + ui32 count; + std::string hoverText; + OwnedObjectInfo(): + imageID(0), + count(0) + {} + }; + std::vector objects; + + std::shared_ptr dwellingsList; + std::shared_ptr tabArea; + + //Main buttons + std::shared_ptr btnTowns; + std::shared_ptr btnHeroes; + std::shared_ptr btnExit; + + //Buttons for scrolling dwellings list + std::shared_ptr dwellUp; + std::shared_ptr dwellDown; + std::shared_ptr dwellTop; + std::shared_ptr dwellBottom; + + std::array, 7> minesBox; + + std::shared_ptr incomeArea; + std::shared_ptr incomeAmount; + + std::shared_ptr statusbar; + std::shared_ptr resdatabar; + + void activateTab(size_t which); + + //Internal functions used during construction + void generateButtons(); + void generateObjectsList(const std::vector &ownedObjects); + void generateMinesList(const std::vector &ownedObjects); + + std::shared_ptr createOwnedObject(size_t index); + std::shared_ptr createMainTab(size_t index); + +public: + CKingdomInterface(); + + void townChanged(const CGTownInstance *town); + void heroRemoved(); + void updateGarrisons() override; + void artifactRemoved(const ArtifactLocation &artLoc) override; + void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; + void artifactDisassembled(const ArtifactLocation &artLoc) override; + void artifactAssembled(const ArtifactLocation &artLoc) override; + void buildChanged() override; +}; + +/// List item with town +class CTownItem : public CIntObject, public IGarrisonHolder +{ + std::shared_ptr background; + std::shared_ptr picture; + std::shared_ptr name; + std::shared_ptr income; + std::shared_ptr garr; + + std::shared_ptr heroes; + std::shared_ptr hall; + std::shared_ptr fort; + + std::vector> available; + std::vector> growth; + + std::shared_ptr openTown; + + std::shared_ptr fastTownHall; + std::shared_ptr fastArmyPurchase; + std::shared_ptr fastMarket; + std::shared_ptr fastTavern; + std::shared_ptr fastTown; + +public: + const CGTownInstance * town; + + CTownItem(const CGTownInstance * Town); + + void updateGarrisons() override; + void update(); +}; + +/// List item with hero +class CHeroItem : public CIntObject, public IGarrisonHolder +{ + const CGHeroInstance * hero; + + std::vector> artTabs; + + std::shared_ptr portrait; + std::shared_ptr name; + std::shared_ptr heroArea; + + std::shared_ptr artsText; + std::shared_ptr artsTabs; + + std::shared_ptr artButtons; + std::vector> heroInfo; + std::shared_ptr morale; + std::shared_ptr luck; + + std::shared_ptr garr; + + void onArtChange(int tabIndex); + + std::shared_ptr onTabSelected(size_t index); + +public: + std::shared_ptr heroArts; + + void updateGarrisons() override; + + CHeroItem(const CGHeroInstance * hero); +}; + +/// Tab with all hero-specific data +class CKingdHeroList : public CIntObject, public IGarrisonHolder, public CWindowWithArtifacts +{ +private: + std::shared_ptr heroes; + std::shared_ptr title; + std::shared_ptr heroLabel; + std::shared_ptr skillsLabel; + + std::shared_ptr createHeroItem(size_t index); +public: + CKingdHeroList(size_t maxSize); + + void updateGarrisons() override; +}; + +/// Tab with all town-specific data +class CKingdTownList : public CIntObject, public IGarrisonHolder +{ +private: + std::shared_ptr towns; + std::shared_ptr title; + std::shared_ptr townLabel; + std::shared_ptr garrHeroLabel; + std::shared_ptr visitHeroLabel; + + std::shared_ptr createTownItem(size_t index); +public: + CKingdTownList(size_t maxSize); + + void townChanged(const CGTownInstance * town); + void updateGarrisons() override; +}; diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index ce508fa66..0684b4b14 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -1,549 +1,549 @@ -/* - * CMessage.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 "CMessage.h" - -#include "../CGameInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/TextOperations.h" - -#include "../windows/InfoWindows.h" -#include "../widgets/Buttons.h" -#include "../widgets/CComponent.h" -#include "../widgets/Slider.h" -#include "../widgets/TextControls.h" -#include "../gui/CGuiHandler.h" -#include "../render/CAnimation.h" -#include "../render/IImage.h" -#include "../render/IRenderHandler.h" -#include "../render/Canvas.h" -#include "../render/Graphics.h" -#include "../render/IFont.h" -#include "../renderSDL/SDL_Extensions.h" - -#include - -const int BETWEEN_COMPS_ROWS = 10; -const int BEFORE_COMPONENTS = 30; -const int BETWEEN_COMPS = 30; -const int SIDE_MARGIN = 30; - -template std::pair max(const std::pair &x, const std::pair &y) -{ - std::pair ret; - ret.first = std::max(x.first,y.first); - ret.second = std::max(x.second,y.second); - return ret; -} - -//One image component + subtitles below it -class ComponentResolved : public CIntObject -{ -public: - std::shared_ptr comp; - - //blit component with image centered at this position - void showAll(Canvas & to) override; - - //ComponentResolved(); - ComponentResolved(std::shared_ptr Comp); - ~ComponentResolved(); -}; -// Full set of components for blitting on dialog box -struct ComponentsToBlit -{ - std::vector< std::vector>> comps; - int w, h; - - void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); - ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); - ~ComponentsToBlit(); -}; - -namespace -{ - std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; - std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; - - std::shared_ptr background;//todo: should be CFilledTexture -} - -void CMessage::init() -{ - for(int i=0; ipreload(); - - for(int j=0; j < dialogBorders[i]->size(0); j++) - { - auto image = dialogBorders[i]->getImage(j, 0); - //assume blue color initially - if(i != 1) - image->playerColored(PlayerColor(i)); - piecesOfBox[i].push_back(image); - } - } - - background = GH.renderHandler().loadImage(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); -} - -void CMessage::dispose() -{ - for(auto & item : dialogBorders) - item.reset(); -} - -SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) -{ - //prepare surface - SDL_Surface * ret = CSDL_Ext::newSurface(w,h); - for (int i=0; iwidth())//background - { - for (int j=0; jheight()) - { - background->draw(ret, i, j); - } - } - - drawBorder(playerColor, ret, w, h); - return ret; -} - -std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) -{ - std::vector ret; - - boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); - - // each iteration generates one output line - while (text.length()) - { - ui32 lineWidth = 0; //in characters or given char metric - ui32 wordBreak = -1; //last position for line break (last space character) - ui32 currPos = 0; //current position in text - bool opened = false; //set to true when opening brace is found - std::string color = ""; //color found - - size_t symbolSize = 0; // width of character, in bytes - size_t glyphWidth = 0; // width of printable glyph, pixels - - // loops till line is full or end of text reached - while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) - { - symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); - glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); - - // candidate for line break - if (ui8(text[currPos]) <= ui8(' ')) - wordBreak = currPos; - - /* We don't count braces in string length. */ - if (text[currPos] == '{') - { - opened=true; - - std::smatch match; - std::regex expr("^\\{(.*?)\\|"); - std::string tmp = text.substr(currPos); - if(std::regex_search(tmp, match, expr)) - { - std::string colorText = match[1].str(); - if(auto c = Colors::parseColor(colorText)) - { - color = colorText + "|"; - currPos += colorText.length() + 1; - } - } - } - else if (text[currPos]=='}') - { - opened=false; - color = ""; - } - else - lineWidth += (ui32)glyphWidth; - currPos += (ui32)symbolSize; - } - - // long line, create line break - if (currPos < text.length() && (text[currPos] != 0x0a)) - { - if (wordBreak != ui32(-1)) - currPos = wordBreak; - else - currPos -= (ui32)symbolSize; - } - - //non-blank line - if(currPos != 0) - { - ret.push_back(text.substr(0, currPos)); - - if (opened) - /* Close the brace for the current line. */ - ret.back() += '}'; - - text.erase(0, currPos); - } - else if(text[currPos] == 0x0a) - { - ret.push_back(""); //add empty string, no extra actions needed - } - - if (text.length() != 0 && text[0] == 0x0a) - { - /* Remove LF */ - text.erase(0, 1); - } - else - { - // trim only if line does not starts with LF - // FIXME: necessary? All lines will be trimmed before returning anyway - boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); - } - - if (opened) - { - /* Add an opening brace for the next line. */ - if (text.length() != 0) - text.insert(0, "{" + color); - } - } - - /* Trim whitespaces of every line. */ - for (auto & elem : ret) - boost::algorithm::trim(elem); - - return ret; -} - -std::string CMessage::guessHeader(const std::string & msg) -{ - size_t begin = 0; - std::string delimeters = "{}"; - size_t start = msg.find_first_of(delimeters[0], begin); - size_t end = msg.find_first_of(delimeters[1], start); - if(start > msg.size() || end > msg.size()) - return ""; - return msg.substr(begin, end); -} - -int CMessage::guessHeight(const std::string & txt, int width, EFonts font) -{ - const auto f = graphics->fonts[font]; - auto lines = CMessage::breakText(txt, width, font); - int lineHeight = static_cast(f->getLineHeight()); - return lineHeight * (int)lines.size(); -} - -int CMessage::getEstimatedComponentHeight(int numComps) -{ - if (numComps > 8) //Bigger than 8 components - return invalid value - return std::numeric_limits::max(); - else if (numComps > 2) - return 160; // 32px * 1 row + 20 to offset - else if (numComps) - return 118; // 118 px to offset - return 0; -} - -void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) -{ - bool blitOr = false; - if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components - blitOr = true; - - const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; - - assert(ret && ret->text); - for(int i = 0; - i < std::size(sizes) - && sizes[i][0] < GH.screenDimensions().x - 150 - && sizes[i][1] < GH.screenDimensions().y - 150 - && ret->text->slider; - i++) - { - ret->text->resize(Point(sizes[i][0], sizes[i][1])); - } - - if(ret->text->slider) - { - ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); - } - else - { - ret->text->resize(ret->text->label->textSize + Point(10, 10)); - } - - std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size - - ComponentsToBlit comps(ret->components,500, blitOr); - if (ret->components.size()) - winSize.second += 10 + comps.h; //space to first component - - int bw = 0; - if (ret->buttons.size()) - { - int bh = 0; - // Compute total width of buttons - bw = 20*((int)ret->buttons.size()-1); // space between all buttons - for(auto & elem : ret->buttons) //and add buttons width - { - bw+=elem->pos.w; - vstd::amax(bh, elem->pos.h); - } - winSize.second += 20 + bh;//before button + button - } - - // Clip window size - vstd::amax(winSize.second, 50); - vstd::amax(winSize.first, 80); - vstd::amax(winSize.first, comps.w); - vstd::amax(winSize.first, bw); - - vstd::amin(winSize.first, GH.screenDimensions().x - 150); - - ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); - ret->pos.h=ret->bitmap->h; - ret->pos.w=ret->bitmap->w; - ret->center(); - - int curh = SIDE_MARGIN; - int xOffset = (ret->pos.w - ret->text->pos.w)/2; - - if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically - { - if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) - curh = (ret->bitmap->h - ret->text->pos.h)/2; - } - - ret->text->moveBy(Point(xOffset, curh)); - - curh += ret->text->pos.h; - - if (ret->components.size()) - { - curh += BEFORE_COMPONENTS; - comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); - } - if(ret->buttons.size()) - { - // Position the buttons at the bottom of the window - bw = (ret->bitmap->w/2) - (bw/2); - curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; - - for(auto & elem : ret->buttons) - { - elem->moveBy(Point(bw, curh)); - bw += elem->pos.w + 20; - } - } - for(size_t i=0; icomponents.size(); i++) - ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); -} - -void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) -{ - if(playerColor.isSpectator()) - playerColor = PlayerColor(1); - auto & box = piecesOfBox.at(playerColor.getNum()); - - // Note: this code assumes that the corner dimensions are all the same. - - // Horizontal borders - int start_x = x + box[0]->width(); - const int stop_x = x + w - box[1]->width(); - const int bottom_y = y+h-box[7]->height()+1; - while (start_x < stop_x) { - int cur_w = stop_x - start_x; - if (cur_w > box[6]->width()) - cur_w = box[6]->width(); - - // Top border - Rect srcR(0, 0, cur_w, box[6]->height()); - Rect dstR(start_x, y, 0, 0); - box[6]->draw(ret, &dstR, &srcR); - - // Bottom border - dstR.y = bottom_y; - box[7]->draw(ret, &dstR, &srcR); - - start_x += cur_w; - } - - // Vertical borders - int start_y = y + box[0]->height(); - const int stop_y = y + h - box[2]->height()+1; - const int right_x = x+w-box[5]->width(); - while (start_y < stop_y) { - int cur_h = stop_y - start_y; - if (cur_h > box[4]->height()) - cur_h = box[4]->height(); - - // Left border - Rect srcR(0, 0, box[4]->width(), cur_h); - Rect dstR(x, start_y, 0, 0); - box[4]->draw(ret, &dstR, &srcR); - - // Right border - dstR.x = right_x; - box[5]->draw(ret, &dstR, &srcR); - - start_y += cur_h; - } - - //corners - Rect dstR(x, y, box[0]->width(), box[0]->height()); - box[0]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); - box[1]->draw(ret, &dstR, nullptr); - - dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); - box[2]->draw(ret, &dstR, nullptr); - - dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); - box[3]->draw(ret, &dstR, nullptr); -} - -ComponentResolved::ComponentResolved(std::shared_ptr Comp): - comp(Comp) -{ - //Temporary assign ownership on comp - if (parent) - parent->removeChild(this); - if (comp->parent) - { - comp->parent->addChild(this); - comp->parent->removeChild(comp.get()); - } - - addChild(comp.get()); - defActions = 255 - DISPOSE; - pos.x = pos.y = 0; - - pos.w = comp->pos.w; - pos.h = comp->pos.h; -} - -ComponentResolved::~ComponentResolved() -{ - if (parent) - { - removeChild(comp.get()); - parent->addChild(comp.get()); - } -} - -void ComponentResolved::showAll(Canvas & to) -{ - CIntObject::showAll(to); - comp->showAll(to); -} - -ComponentsToBlit::~ComponentsToBlit() = default; - -ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - w = h = 0; - if(SComps.empty()) - return; - - comps.resize(1); - int curw = 0; - int curr = 0; //current row - - for(auto & SComp : SComps) - { - auto cur = std::make_shared(SComp); - - int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); - if (curw + toadd > maxw) - { - curr++; - vstd::amax(w,curw); - curw = cur->pos.w; - comps.resize(curr+1); - } - else - { - curw += toadd; - vstd::amax(w,curw); - } - - comps[curr].push_back(cur); - } - - for(auto & elem : comps) - { - int maxHeight = 0; - for(size_t j=0;jpos.h); - - h += maxHeight + BETWEEN_COMPS_ROWS; - } -} - -void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) -{ - int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); - - for (auto & elem : comps)//for each row - { - int totalw=0, maxHeight=0; - for(size_t j=0;jpos.w; - vstd::amax(maxHeight, cur->pos.h); - } - - //add space between comps in this row - if(blitOr) - totalw += (inter*2+orWidth) * ((int)elem.size() - 1); - else - totalw += (inter) * ((int)elem.size() - 1); - - int middleh = curh + maxHeight/2;//axis for image aligment - int curw = ret->w/2 - totalw/2; - - for(size_t j=0;jmoveTo(Point(curw, curh)); - - //blit component - Canvas canvas = Canvas::createFromSurface(ret); - - cur->showAll(canvas); - curw += cur->pos.w; - - //if there is subsequent component blit "or" - if(j<(elem.size()-1)) - { - if(blitOr) - { - curw+=inter; - - graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, - Point(curw,middleh-((int)graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); - - curw+=orWidth; - } - curw+=inter; - } - } - curh += maxHeight + BETWEEN_COMPS_ROWS; - } -} +/* + * CMessage.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 "CMessage.h" + +#include "../CGameInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/TextOperations.h" + +#include "../windows/InfoWindows.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../gui/CGuiHandler.h" +#include "../render/CAnimation.h" +#include "../render/IImage.h" +#include "../render/IRenderHandler.h" +#include "../render/Canvas.h" +#include "../render/Graphics.h" +#include "../render/IFont.h" +#include "../renderSDL/SDL_Extensions.h" + +#include + +const int BETWEEN_COMPS_ROWS = 10; +const int BEFORE_COMPONENTS = 30; +const int BETWEEN_COMPS = 30; +const int SIDE_MARGIN = 30; + +template std::pair max(const std::pair &x, const std::pair &y) +{ + std::pair ret; + ret.first = std::max(x.first,y.first); + ret.second = std::max(x.second,y.second); + return ret; +} + +//One image component + subtitles below it +class ComponentResolved : public CIntObject +{ +public: + std::shared_ptr comp; + + //blit component with image centered at this position + void showAll(Canvas & to) override; + + //ComponentResolved(); + ComponentResolved(std::shared_ptr Comp); + ~ComponentResolved(); +}; +// Full set of components for blitting on dialog box +struct ComponentsToBlit +{ + std::vector< std::vector>> comps; + int w, h; + + void blitCompsOnSur(bool blitOr, int inter, int &curh, SDL_Surface *ret); + ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr); + ~ComponentsToBlit(); +}; + +namespace +{ + std::array, PlayerColor::PLAYER_LIMIT_I> dialogBorders; + std::array>, PlayerColor::PLAYER_LIMIT_I> piecesOfBox; + + std::shared_ptr background;//todo: should be CFilledTexture +} + +void CMessage::init() +{ + for(int i=0; ipreload(); + + for(int j=0; j < dialogBorders[i]->size(0); j++) + { + auto image = dialogBorders[i]->getImage(j, 0); + //assume blue color initially + if(i != 1) + image->playerColored(PlayerColor(i)); + piecesOfBox[i].push_back(image); + } + } + + background = GH.renderHandler().loadImage(ImagePath::builtin("DIBOXBCK.BMP"), EImageBlitMode::OPAQUE); +} + +void CMessage::dispose() +{ + for(auto & item : dialogBorders) + item.reset(); +} + +SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) +{ + //prepare surface + SDL_Surface * ret = CSDL_Ext::newSurface(w,h); + for (int i=0; iwidth())//background + { + for (int j=0; jheight()) + { + background->draw(ret, i, j); + } + } + + drawBorder(playerColor, ret, w, h); + return ret; +} + +std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) +{ + std::vector ret; + + boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); + + // each iteration generates one output line + while (text.length()) + { + ui32 lineWidth = 0; //in characters or given char metric + ui32 wordBreak = -1; //last position for line break (last space character) + ui32 currPos = 0; //current position in text + bool opened = false; //set to true when opening brace is found + std::string color = ""; //color found + + size_t symbolSize = 0; // width of character, in bytes + size_t glyphWidth = 0; // width of printable glyph, pixels + + // loops till line is full or end of text reached + while(currPos < text.length() && text[currPos] != 0x0a && lineWidth < maxLineWidth) + { + symbolSize = TextOperations::getUnicodeCharacterSize(text[currPos]); + glyphWidth = graphics->fonts[font]->getGlyphWidth(text.data() + currPos); + + // candidate for line break + if (ui8(text[currPos]) <= ui8(' ')) + wordBreak = currPos; + + /* We don't count braces in string length. */ + if (text[currPos] == '{') + { + opened=true; + + std::smatch match; + std::regex expr("^\\{(.*?)\\|"); + std::string tmp = text.substr(currPos); + if(std::regex_search(tmp, match, expr)) + { + std::string colorText = match[1].str(); + if(auto c = Colors::parseColor(colorText)) + { + color = colorText + "|"; + currPos += colorText.length() + 1; + } + } + } + else if (text[currPos]=='}') + { + opened=false; + color = ""; + } + else + lineWidth += (ui32)glyphWidth; + currPos += (ui32)symbolSize; + } + + // long line, create line break + if (currPos < text.length() && (text[currPos] != 0x0a)) + { + if (wordBreak != ui32(-1)) + currPos = wordBreak; + else + currPos -= (ui32)symbolSize; + } + + //non-blank line + if(currPos != 0) + { + ret.push_back(text.substr(0, currPos)); + + if (opened) + /* Close the brace for the current line. */ + ret.back() += '}'; + + text.erase(0, currPos); + } + else if(text[currPos] == 0x0a) + { + ret.push_back(""); //add empty string, no extra actions needed + } + + if (text.length() != 0 && text[0] == 0x0a) + { + /* Remove LF */ + text.erase(0, 1); + } + else + { + // trim only if line does not starts with LF + // FIXME: necessary? All lines will be trimmed before returning anyway + boost::algorithm::trim_left_if(text,boost::algorithm::is_any_of(std::string(" "))); + } + + if (opened) + { + /* Add an opening brace for the next line. */ + if (text.length() != 0) + text.insert(0, "{" + color); + } + } + + /* Trim whitespaces of every line. */ + for (auto & elem : ret) + boost::algorithm::trim(elem); + + return ret; +} + +std::string CMessage::guessHeader(const std::string & msg) +{ + size_t begin = 0; + std::string delimeters = "{}"; + size_t start = msg.find_first_of(delimeters[0], begin); + size_t end = msg.find_first_of(delimeters[1], start); + if(start > msg.size() || end > msg.size()) + return ""; + return msg.substr(begin, end); +} + +int CMessage::guessHeight(const std::string & txt, int width, EFonts font) +{ + const auto f = graphics->fonts[font]; + auto lines = CMessage::breakText(txt, width, font); + int lineHeight = static_cast(f->getLineHeight()); + return lineHeight * (int)lines.size(); +} + +int CMessage::getEstimatedComponentHeight(int numComps) +{ + if (numComps > 8) //Bigger than 8 components - return invalid value + return std::numeric_limits::max(); + else if (numComps > 2) + return 160; // 32px * 1 row + 20 to offset + else if (numComps) + return 118; // 118 px to offset + return 0; +} + +void CMessage::drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player) +{ + bool blitOr = false; + if(dynamic_cast(ret)) //it's selection window, so we'll blit "or" between components + blitOr = true; + + const int sizes[][2] = {{400, 125}, {500, 150}, {600, 200}, {480, 400}}; + + assert(ret && ret->text); + for(int i = 0; + i < std::size(sizes) + && sizes[i][0] < GH.screenDimensions().x - 150 + && sizes[i][1] < GH.screenDimensions().y - 150 + && ret->text->slider; + i++) + { + ret->text->resize(Point(sizes[i][0], sizes[i][1])); + } + + if(ret->text->slider) + { + ret->text->slider->addUsedEvents(CIntObject::WHEEL | CIntObject::KEYBOARD); + } + else + { + ret->text->resize(ret->text->label->textSize + Point(10, 10)); + } + + std::pair winSize(ret->text->pos.w, ret->text->pos.h); //start with text size + + ComponentsToBlit comps(ret->components,500, blitOr); + if (ret->components.size()) + winSize.second += 10 + comps.h; //space to first component + + int bw = 0; + if (ret->buttons.size()) + { + int bh = 0; + // Compute total width of buttons + bw = 20*((int)ret->buttons.size()-1); // space between all buttons + for(auto & elem : ret->buttons) //and add buttons width + { + bw+=elem->pos.w; + vstd::amax(bh, elem->pos.h); + } + winSize.second += 20 + bh;//before button + button + } + + // Clip window size + vstd::amax(winSize.second, 50); + vstd::amax(winSize.first, 80); + vstd::amax(winSize.first, comps.w); + vstd::amax(winSize.first, bw); + + vstd::amin(winSize.first, GH.screenDimensions().x - 150); + + ret->bitmap = drawDialogBox (winSize.first + 2*SIDE_MARGIN, winSize.second + 2*SIDE_MARGIN, player); + ret->pos.h=ret->bitmap->h; + ret->pos.w=ret->bitmap->w; + ret->center(); + + int curh = SIDE_MARGIN; + int xOffset = (ret->pos.w - ret->text->pos.w)/2; + + if(!ret->buttons.size() && !ret->components.size()) //improvement for very small text only popups -> center text vertically + { + if(ret->bitmap->h > ret->text->pos.h + 2*SIDE_MARGIN) + curh = (ret->bitmap->h - ret->text->pos.h)/2; + } + + ret->text->moveBy(Point(xOffset, curh)); + + curh += ret->text->pos.h; + + if (ret->components.size()) + { + curh += BEFORE_COMPONENTS; + comps.blitCompsOnSur (blitOr, BETWEEN_COMPS, curh, ret->bitmap); + } + if(ret->buttons.size()) + { + // Position the buttons at the bottom of the window + bw = (ret->bitmap->w/2) - (bw/2); + curh = ret->bitmap->h - SIDE_MARGIN - ret->buttons[0]->pos.h; + + for(auto & elem : ret->buttons) + { + elem->moveBy(Point(bw, curh)); + bw += elem->pos.w + 20; + } + } + for(size_t i=0; icomponents.size(); i++) + ret->components[i]->moveBy(Point(ret->pos.x, ret->pos.y)); +} + +void CMessage::drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x, int y) +{ + if(playerColor.isSpectator()) + playerColor = PlayerColor(1); + auto & box = piecesOfBox.at(playerColor.getNum()); + + // Note: this code assumes that the corner dimensions are all the same. + + // Horizontal borders + int start_x = x + box[0]->width(); + const int stop_x = x + w - box[1]->width(); + const int bottom_y = y+h-box[7]->height()+1; + while (start_x < stop_x) { + int cur_w = stop_x - start_x; + if (cur_w > box[6]->width()) + cur_w = box[6]->width(); + + // Top border + Rect srcR(0, 0, cur_w, box[6]->height()); + Rect dstR(start_x, y, 0, 0); + box[6]->draw(ret, &dstR, &srcR); + + // Bottom border + dstR.y = bottom_y; + box[7]->draw(ret, &dstR, &srcR); + + start_x += cur_w; + } + + // Vertical borders + int start_y = y + box[0]->height(); + const int stop_y = y + h - box[2]->height()+1; + const int right_x = x+w-box[5]->width(); + while (start_y < stop_y) { + int cur_h = stop_y - start_y; + if (cur_h > box[4]->height()) + cur_h = box[4]->height(); + + // Left border + Rect srcR(0, 0, box[4]->width(), cur_h); + Rect dstR(x, start_y, 0, 0); + box[4]->draw(ret, &dstR, &srcR); + + // Right border + dstR.x = right_x; + box[5]->draw(ret, &dstR, &srcR); + + start_y += cur_h; + } + + //corners + Rect dstR(x, y, box[0]->width(), box[0]->height()); + box[0]->draw(ret, &dstR, nullptr); + + dstR=Rect(x+w-box[1]->width(), y, box[1]->width(), box[1]->height()); + box[1]->draw(ret, &dstR, nullptr); + + dstR=Rect(x, y+h-box[2]->height()+1, box[2]->width(), box[2]->height()); + box[2]->draw(ret, &dstR, nullptr); + + dstR=Rect(x+w-box[3]->width(), y+h-box[3]->height()+1, box[3]->width(), box[3]->height()); + box[3]->draw(ret, &dstR, nullptr); +} + +ComponentResolved::ComponentResolved(std::shared_ptr Comp): + comp(Comp) +{ + //Temporary assign ownership on comp + if (parent) + parent->removeChild(this); + if (comp->parent) + { + comp->parent->addChild(this); + comp->parent->removeChild(comp.get()); + } + + addChild(comp.get()); + defActions = 255 - DISPOSE; + pos.x = pos.y = 0; + + pos.w = comp->pos.w; + pos.h = comp->pos.h; +} + +ComponentResolved::~ComponentResolved() +{ + if (parent) + { + removeChild(comp.get()); + parent->addChild(comp.get()); + } +} + +void ComponentResolved::showAll(Canvas & to) +{ + CIntObject::showAll(to); + comp->showAll(to); +} + +ComponentsToBlit::~ComponentsToBlit() = default; + +ComponentsToBlit::ComponentsToBlit(std::vector> & SComps, int maxw, bool blitOr) +{ + int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); + + w = h = 0; + if(SComps.empty()) + return; + + comps.resize(1); + int curw = 0; + int curr = 0; //current row + + for(auto & SComp : SComps) + { + auto cur = std::make_shared(SComp); + + int toadd = (cur->pos.w + BETWEEN_COMPS + (blitOr ? orWidth : 0)); + if (curw + toadd > maxw) + { + curr++; + vstd::amax(w,curw); + curw = cur->pos.w; + comps.resize(curr+1); + } + else + { + curw += toadd; + vstd::amax(w,curw); + } + + comps[curr].push_back(cur); + } + + for(auto & elem : comps) + { + int maxHeight = 0; + for(size_t j=0;jpos.h); + + h += maxHeight + BETWEEN_COMPS_ROWS; + } +} + +void ComponentsToBlit::blitCompsOnSur( bool blitOr, int inter, int &curh, SDL_Surface *ret ) +{ + int orWidth = static_cast(graphics->fonts[FONT_MEDIUM]->getStringWidth(CGI->generaltexth->allTexts[4])); + + for (auto & elem : comps)//for each row + { + int totalw=0, maxHeight=0; + for(size_t j=0;jpos.w; + vstd::amax(maxHeight, cur->pos.h); + } + + //add space between comps in this row + if(blitOr) + totalw += (inter*2+orWidth) * ((int)elem.size() - 1); + else + totalw += (inter) * ((int)elem.size() - 1); + + int middleh = curh + maxHeight/2;//axis for image aligment + int curw = ret->w/2 - totalw/2; + + for(size_t j=0;jmoveTo(Point(curw, curh)); + + //blit component + Canvas canvas = Canvas::createFromSurface(ret); + + cur->showAll(canvas); + curw += cur->pos.w; + + //if there is subsequent component blit "or" + if(j<(elem.size()-1)) + { + if(blitOr) + { + curw+=inter; + + graphics->fonts[FONT_MEDIUM]->renderTextLeft(ret, CGI->generaltexth->allTexts[4], Colors::WHITE, + Point(curw,middleh-((int)graphics->fonts[FONT_MEDIUM]->getLineHeight()/2))); + + curw+=orWidth; + } + curw+=inter; + } + } + curh += maxHeight + BETWEEN_COMPS_ROWS; + } +} diff --git a/client/windows/CMessage.h b/client/windows/CMessage.h index 430c87a5b..84bbfada8 100644 --- a/client/windows/CMessage.h +++ b/client/windows/CMessage.h @@ -1,50 +1,50 @@ -/* - * CMessage.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 "../render/EFont.h" -#include "../../lib/GameConstants.h" - -struct SDL_Surface; -class CInfoWindow; -class CComponent; - -VCMI_LIB_NAMESPACE_BEGIN -class ColorRGBA; -VCMI_LIB_NAMESPACE_END - - -/// Class which draws formatted text messages and generates chat windows -class CMessage -{ - /// Draw simple dialog box (borders and background only) - static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); - -public: - /// Draw border on exiting surface - static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); - - static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); - - /// split text in lines - static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); - - /// Try to guess a header of a message - static std::string guessHeader(const std::string & msg); - - /// For convenience - static int guessHeight(const std::string & string, int width, EFonts fnt); - - static int getEstimatedComponentHeight(int numComps); - /// constructor - static void init(); - /// destructor - static void dispose(); -}; +/* + * CMessage.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 "../render/EFont.h" +#include "../../lib/GameConstants.h" + +struct SDL_Surface; +class CInfoWindow; +class CComponent; + +VCMI_LIB_NAMESPACE_BEGIN +class ColorRGBA; +VCMI_LIB_NAMESPACE_END + + +/// Class which draws formatted text messages and generates chat windows +class CMessage +{ + /// Draw simple dialog box (borders and background only) + static SDL_Surface * drawDialogBox(int w, int h, PlayerColor playerColor = PlayerColor(1)); + +public: + /// Draw border on exiting surface + static void drawBorder(PlayerColor playerColor, SDL_Surface * ret, int w, int h, int x=0, int y=0); + + static void drawIWindow(CInfoWindow * ret, std::string text, PlayerColor player); + + /// split text in lines + static std::vector breakText(std::string text, size_t maxLineWidth, EFonts font); + + /// Try to guess a header of a message + static std::string guessHeader(const std::string & msg); + + /// For convenience + static int guessHeight(const std::string & string, int width, EFonts fnt); + + static int getEstimatedComponentHeight(int numComps); + /// constructor + static void init(); + /// destructor + static void dispose(); +}; diff --git a/client/windows/CQuestLog.cpp b/client/windows/CQuestLog.cpp index c43870c9b..cd6480d8e 100644 --- a/client/windows/CQuestLog.cpp +++ b/client/windows/CQuestLog.cpp @@ -1,325 +1,325 @@ -/* - * CQuestLog.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 "CQuestLog.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../widgets/Buttons.h" -#include "../widgets/CComponent.h" -#include "../widgets/Slider.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../adventureMap/CMinimap.h" -#include "../render/Canvas.h" -#include "../renderSDL/SDL_Extensions.h" - -#include "../../CCallback.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CConfigHandler.h" -#include "../../lib/gameState/QuestInfo.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/MetaString.h" -#include "../../lib/mapObjects/CQuest.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class CAdvmapInterface; - -void CQuestLabel::clickPressed(const Point & cursorPosition) -{ - callback(); -} - -void CQuestLabel::showAll(Canvas & to) -{ - CMultiLineLabel::showAll (to); -} - -CQuestIcon::CQuestIcon (const AnimationPath &defname, int index, int x, int y) : - CAnimImage(defname, index, 0, x, y) -{ - addUsedEvents(LCLICK); -} - -void CQuestIcon::clickPressed(const Point & cursorPosition) -{ - callback(); -} - -void CQuestIcon::showAll(Canvas & to) -{ - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos); - CAnimImage::showAll(to); -} - -CQuestMinimap::CQuestMinimap(const Rect & position) - : CMinimap(position), - currentQuest(nullptr) -{ -} - -void CQuestMinimap::addQuestMarks (const QuestInfo * q) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - icons.clear(); - - int3 tile; - if (q->obj) - tile = q->obj->pos; - else - tile = q->tile; - - Point offset = tileToPixels(tile); - - onMapViewMoved(Rect(), tile.z); - - auto pic = std::make_shared(AnimationPath::builtin("VwSymbol.def"), 3, offset.x, offset.y); - - pic->moveBy (Point ( -pic->pos.w/2, -pic->pos.h/2)); - pic->callback = std::bind (&CQuestMinimap::iconClicked, this); - - icons.push_back(pic); -} - -void CQuestMinimap::update() -{ - CMinimap::update(); - if(currentQuest) - addQuestMarks(currentQuest); -} - -void CQuestMinimap::iconClicked() -{ - if(currentQuest->obj) - adventureInt->centerOnTile(currentQuest->obj->pos); - //moveAdvMapSelection(); -} - -void CQuestMinimap::showAll(Canvas & to) -{ - CIntObject::showAll(to); // blitting IntObject directly to hide radar -// for (auto pic : icons) -// pic->showAll(to); -} - -CQuestLog::CQuestLog (const std::vector & Quests) - : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("questDialog")), - questIndex(0), - currentQuest(nullptr), - hideComplete(false), - quests(Quests) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - minimap = std::make_shared(Rect(12, 12, 169, 169)); - // TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin - description = std::make_shared("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE); - ok = std::make_shared(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT); - // Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button - hideCompleteButton = std::make_shared(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); - hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover")); - slider = std::make_shared(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, 0, Orientation::VERTICAL, CSlider::BROWN); - slider->setPanningStep(32); - - recreateLabelList(); - recreateQuestList(0); -} - -void CQuestLog::recreateLabelList() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - labels.clear(); - - bool completeMissing = true; - int currentLabel = 0; - for (int i = 0; i < quests.size(); ++i) - { - // Quests without mision don't have text for them and can't be displayed - if (quests[i].quest->mission == Rewardable::Limiter{}) - continue; - - if (quests[i].quest->isCompleted) - { - completeMissing = false; - if (hideComplete) - continue; - } - - MetaString text; - quests[i].quest->getRolloverText (text, false); - if (quests[i].obj) - { - if (auto seersHut = dynamic_cast(quests[i].obj)) - { - MetaString toSeer; - toSeer.appendRawString(VLC->generaltexth->allTexts[347]); - toSeer.replaceRawString(seersHut->seerName); - text.replaceRawString(toSeer.toString()); - } - else - text.replaceRawString(quests[i].obj->getObjectName()); //get name of the object - } - auto label = std::make_shared(Rect(13, 195, 149,31), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, text.toString()); - label->disable(); - - label->callback = std::bind(&CQuestLog::selectQuest, this, i, currentLabel); - labels.push_back(label); - - // Select latest active quest - if(!quests[i].quest->isCompleted) - selectQuest(i, currentLabel); - - currentLabel = static_cast(labels.size()); - } - - if (completeMissing) // We can't use block(completeMissing) because if false button state reset to NORMAL - hideCompleteButton->block(true); - - slider->setAmount(currentLabel); - if (currentLabel > QUEST_COUNT) - { - slider->block(false); - slider->scrollToMax(); - } - else - { - slider->block(true); - slider->scrollToMin(); - } -} - -void CQuestLog::showAll(Canvas & to) -{ - CWindowObject::showAll(to); - if(questIndex >= 0 && questIndex < labels.size()) - { - //TODO: use child object to selection rect - Rect rect = Rect::createAround(labels[questIndex]->pos, 1); - rect.x -= 2; // Adjustment needed as we want selection box on top of border in graphics - rect.w += 2; - to.drawBorder(rect, Colors::METALLIC_GOLD); - } -} - -void CQuestLog::recreateQuestList (int newpos) -{ - for (int i = 0; i < labels.size(); ++i) - { - labels[i]->pos = Rect (pos.x + 14, pos.y + 195 + (i-newpos) * 32, 151, 31); - if (i >= newpos && i < newpos + QUEST_COUNT) - labels[i]->enable(); - else - labels[i]->disable(); - } - minimap->update(); -} - -void CQuestLog::selectQuest(int which, int labelId) -{ - questIndex = labelId; - currentQuest = &quests[which]; - minimap->currentQuest = currentQuest; - - MetaString text; - std::vector components; - currentQuest->quest->getVisitText(text, components, true); - if(description->slider) - description->slider->scrollToMin(); // scroll text to start position - description->setText(text.toString()); //TODO: use special log entry text - - componentsBox.reset(); - - int componentsSize = static_cast(components.size()); - int descriptionHeight = DESCRIPTION_HEIGHT_MAX; - if(componentsSize) - { - CComponent::ESize imageSize = CComponent::large; - if (componentsSize > 4) - { - imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space - descriptionHeight -= 155; - } - else - descriptionHeight -= 130; - /*switch (currentQuest->quest->missionType) - { - case CQuest::MISSION_ARMY: - { - if (componentsSize > 4) - descriptionHeight -= 195; - else - descriptionHeight -= 100; - - break; - } - case CQuest::MISSION_ART: - { - if (componentsSize > 4) - descriptionHeight -= 190; - else - descriptionHeight -= 90; - - break; - } - case CQuest::MISSION_PRIMARY_STAT: - case CQuest::MISSION_RESOURCES: - { - if (componentsSize > 4) - { - imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space - descriptionHeight -= 140; - } - else - descriptionHeight -= 125; - - break; - } - default: - descriptionHeight -= 115; - break; - }*/ - - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - std::vector> comps; - for(auto & component : components) - { - auto c = std::make_shared(component, imageSize); - comps.push_back(c); - } - - componentsBox = std::make_shared(comps, Rect(202, 20+descriptionHeight+15, 391, DESCRIPTION_HEIGHT_MAX-(20+descriptionHeight))); - } - description->resize(Point(385, descriptionHeight)); - - minimap->update(); - redraw(); -} - -void CQuestLog::sliderMoved(int newpos) -{ - recreateQuestList(newpos); //move components - redraw(); -} - -void CQuestLog::toggleComplete(bool on) -{ - hideComplete = on; - recreateLabelList(); - recreateQuestList(0); - redraw(); -} +/* + * CQuestLog.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 "CQuestLog.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" +#include "../widgets/CComponent.h" +#include "../widgets/Slider.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../adventureMap/CMinimap.h" +#include "../render/Canvas.h" +#include "../renderSDL/SDL_Extensions.h" + +#include "../../CCallback.h" +#include "../../lib/CArtHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/gameState/QuestInfo.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/MetaString.h" +#include "../../lib/mapObjects/CQuest.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class CAdvmapInterface; + +void CQuestLabel::clickPressed(const Point & cursorPosition) +{ + callback(); +} + +void CQuestLabel::showAll(Canvas & to) +{ + CMultiLineLabel::showAll (to); +} + +CQuestIcon::CQuestIcon (const AnimationPath &defname, int index, int x, int y) : + CAnimImage(defname, index, 0, x, y) +{ + addUsedEvents(LCLICK); +} + +void CQuestIcon::clickPressed(const Point & cursorPosition) +{ + callback(); +} + +void CQuestIcon::showAll(Canvas & to) +{ + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), parent->pos); + CAnimImage::showAll(to); +} + +CQuestMinimap::CQuestMinimap(const Rect & position) + : CMinimap(position), + currentQuest(nullptr) +{ +} + +void CQuestMinimap::addQuestMarks (const QuestInfo * q) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + icons.clear(); + + int3 tile; + if (q->obj) + tile = q->obj->pos; + else + tile = q->tile; + + Point offset = tileToPixels(tile); + + onMapViewMoved(Rect(), tile.z); + + auto pic = std::make_shared(AnimationPath::builtin("VwSymbol.def"), 3, offset.x, offset.y); + + pic->moveBy (Point ( -pic->pos.w/2, -pic->pos.h/2)); + pic->callback = std::bind (&CQuestMinimap::iconClicked, this); + + icons.push_back(pic); +} + +void CQuestMinimap::update() +{ + CMinimap::update(); + if(currentQuest) + addQuestMarks(currentQuest); +} + +void CQuestMinimap::iconClicked() +{ + if(currentQuest->obj) + adventureInt->centerOnTile(currentQuest->obj->pos); + //moveAdvMapSelection(); +} + +void CQuestMinimap::showAll(Canvas & to) +{ + CIntObject::showAll(to); // blitting IntObject directly to hide radar +// for (auto pic : icons) +// pic->showAll(to); +} + +CQuestLog::CQuestLog (const std::vector & Quests) + : CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin("questDialog")), + questIndex(0), + currentQuest(nullptr), + hideComplete(false), + quests(Quests) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + minimap = std::make_shared(Rect(12, 12, 169, 169)); + // TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin + description = std::make_shared("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE); + ok = std::make_shared(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT); + // Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button + hideCompleteButton = std::make_shared(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1)); + hideCompleteLabel = std::make_shared(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover")); + slider = std::make_shared(Point(166, 195), 191, std::bind(&CQuestLog::sliderMoved, this, _1), QUEST_COUNT, 0, 0, Orientation::VERTICAL, CSlider::BROWN); + slider->setPanningStep(32); + + recreateLabelList(); + recreateQuestList(0); +} + +void CQuestLog::recreateLabelList() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + labels.clear(); + + bool completeMissing = true; + int currentLabel = 0; + for (int i = 0; i < quests.size(); ++i) + { + // Quests without mision don't have text for them and can't be displayed + if (quests[i].quest->mission == Rewardable::Limiter{}) + continue; + + if (quests[i].quest->isCompleted) + { + completeMissing = false; + if (hideComplete) + continue; + } + + MetaString text; + quests[i].quest->getRolloverText (text, false); + if (quests[i].obj) + { + if (auto seersHut = dynamic_cast(quests[i].obj)) + { + MetaString toSeer; + toSeer.appendRawString(VLC->generaltexth->allTexts[347]); + toSeer.replaceRawString(seersHut->seerName); + text.replaceRawString(toSeer.toString()); + } + else + text.replaceRawString(quests[i].obj->getObjectName()); //get name of the object + } + auto label = std::make_shared(Rect(13, 195, 149,31), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, text.toString()); + label->disable(); + + label->callback = std::bind(&CQuestLog::selectQuest, this, i, currentLabel); + labels.push_back(label); + + // Select latest active quest + if(!quests[i].quest->isCompleted) + selectQuest(i, currentLabel); + + currentLabel = static_cast(labels.size()); + } + + if (completeMissing) // We can't use block(completeMissing) because if false button state reset to NORMAL + hideCompleteButton->block(true); + + slider->setAmount(currentLabel); + if (currentLabel > QUEST_COUNT) + { + slider->block(false); + slider->scrollToMax(); + } + else + { + slider->block(true); + slider->scrollToMin(); + } +} + +void CQuestLog::showAll(Canvas & to) +{ + CWindowObject::showAll(to); + if(questIndex >= 0 && questIndex < labels.size()) + { + //TODO: use child object to selection rect + Rect rect = Rect::createAround(labels[questIndex]->pos, 1); + rect.x -= 2; // Adjustment needed as we want selection box on top of border in graphics + rect.w += 2; + to.drawBorder(rect, Colors::METALLIC_GOLD); + } +} + +void CQuestLog::recreateQuestList (int newpos) +{ + for (int i = 0; i < labels.size(); ++i) + { + labels[i]->pos = Rect (pos.x + 14, pos.y + 195 + (i-newpos) * 32, 151, 31); + if (i >= newpos && i < newpos + QUEST_COUNT) + labels[i]->enable(); + else + labels[i]->disable(); + } + minimap->update(); +} + +void CQuestLog::selectQuest(int which, int labelId) +{ + questIndex = labelId; + currentQuest = &quests[which]; + minimap->currentQuest = currentQuest; + + MetaString text; + std::vector components; + currentQuest->quest->getVisitText(text, components, true); + if(description->slider) + description->slider->scrollToMin(); // scroll text to start position + description->setText(text.toString()); //TODO: use special log entry text + + componentsBox.reset(); + + int componentsSize = static_cast(components.size()); + int descriptionHeight = DESCRIPTION_HEIGHT_MAX; + if(componentsSize) + { + CComponent::ESize imageSize = CComponent::large; + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 155; + } + else + descriptionHeight -= 130; + /*switch (currentQuest->quest->missionType) + { + case CQuest::MISSION_ARMY: + { + if (componentsSize > 4) + descriptionHeight -= 195; + else + descriptionHeight -= 100; + + break; + } + case CQuest::MISSION_ART: + { + if (componentsSize > 4) + descriptionHeight -= 190; + else + descriptionHeight -= 90; + + break; + } + case CQuest::MISSION_PRIMARY_STAT: + case CQuest::MISSION_RESOURCES: + { + if (componentsSize > 4) + { + imageSize = CComponent::small; // Only small icons can be used for resources as 4+ icons take too much space + descriptionHeight -= 140; + } + else + descriptionHeight -= 125; + + break; + } + default: + descriptionHeight -= 115; + break; + }*/ + + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + std::vector> comps; + for(auto & component : components) + { + auto c = std::make_shared(component, imageSize); + comps.push_back(c); + } + + componentsBox = std::make_shared(comps, Rect(202, 20+descriptionHeight+15, 391, DESCRIPTION_HEIGHT_MAX-(20+descriptionHeight))); + } + description->resize(Point(385, descriptionHeight)); + + minimap->update(); + redraw(); +} + +void CQuestLog::sliderMoved(int newpos) +{ + recreateQuestList(newpos); //move components + redraw(); +} + +void CQuestLog::toggleComplete(bool on) +{ + hideComplete = on; + recreateLabelList(); + recreateQuestList(0); + redraw(); +} diff --git a/client/windows/CQuestLog.h b/client/windows/CQuestLog.h index 3d719d2dd..c9c4d7983 100644 --- a/client/windows/CQuestLog.h +++ b/client/windows/CQuestLog.h @@ -1,111 +1,111 @@ -/* - * CQuestLog.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 "../widgets/TextControls.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/Images.h" -#include "../adventureMap/CMinimap.h" -#include "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CCreature; -class CStackInstance; -class CGHeroInstance; -struct QuestInfo; - -VCMI_LIB_NAMESPACE_END - -class CButton; -class CToggleButton; -class CComponentBox; -class LRClickableAreaWText; -class CButton; -class CPicture; -class CCreaturePic; -class LRClickableAreaWTextComp; -class CSlider; -class CLabel; - -const int QUEST_COUNT = 6; -const int DESCRIPTION_HEIGHT_MAX = 355; - -class CQuestLabel : public LRClickableAreaWText, public CMultiLineLabel -{ -public: - std::function callback; - - CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA &Color = Colors::WHITE, const std::string &Text = "") - : CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){}; - void clickPressed(const Point & cursorPosition) override; - void showAll(Canvas & to) override; -}; - -class CQuestIcon : public CAnimImage -{ -public: - std::function callback; //TODO: merge with other similar classes? - - CQuestIcon(const AnimationPath & defname, int index, int x=0, int y=0); - - void clickPressed(const Point & cursorPosition) override; - void showAll(Canvas & to) override; -}; - -class CQuestMinimap : public CMinimap -{ - std::vector> icons; - - void clickPressed(const Point & cursorPosition) override{}; //minimap ignores clicking on its surface - void iconClicked(); - void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override{}; - -public: - const QuestInfo * currentQuest; - - CQuestMinimap(const Rect & position); - //should be called to invalidate whole map - different player or level - void update(); - void addQuestMarks (const QuestInfo * q); - - void showAll(Canvas & to) override; -}; - -class CQuestLog : public CWindowObject -{ - int questIndex; - const QuestInfo * currentQuest; - std::shared_ptr componentsBox; - bool hideComplete; - std::shared_ptr hideCompleteButton; - std::shared_ptr hideCompleteLabel; - - const std::vector quests; - std::vector> labels; - std::shared_ptr description; - std::shared_ptr minimap; - std::shared_ptr slider; //scrolls quests - std::shared_ptr ok; - -public: - CQuestLog(const std::vector & Quests); - - ~CQuestLog(){}; - - void selectQuest (int which, int labelId); - void updateMinimap (int which){}; - void printDescription (int which){}; - void sliderMoved (int newpos); - void recreateLabelList(); - void recreateQuestList (int pos); - void toggleComplete(bool on); - void showAll (Canvas & to) override; -}; +/* + * CQuestLog.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 "../widgets/TextControls.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/Images.h" +#include "../adventureMap/CMinimap.h" +#include "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CCreature; +class CStackInstance; +class CGHeroInstance; +struct QuestInfo; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CToggleButton; +class CComponentBox; +class LRClickableAreaWText; +class CButton; +class CPicture; +class CCreaturePic; +class LRClickableAreaWTextComp; +class CSlider; +class CLabel; + +const int QUEST_COUNT = 6; +const int DESCRIPTION_HEIGHT_MAX = 355; + +class CQuestLabel : public LRClickableAreaWText, public CMultiLineLabel +{ +public: + std::function callback; + + CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const ColorRGBA &Color = Colors::WHITE, const std::string &Text = "") + : CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){}; + void clickPressed(const Point & cursorPosition) override; + void showAll(Canvas & to) override; +}; + +class CQuestIcon : public CAnimImage +{ +public: + std::function callback; //TODO: merge with other similar classes? + + CQuestIcon(const AnimationPath & defname, int index, int x=0, int y=0); + + void clickPressed(const Point & cursorPosition) override; + void showAll(Canvas & to) override; +}; + +class CQuestMinimap : public CMinimap +{ + std::vector> icons; + + void clickPressed(const Point & cursorPosition) override{}; //minimap ignores clicking on its surface + void iconClicked(); + void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override{}; + +public: + const QuestInfo * currentQuest; + + CQuestMinimap(const Rect & position); + //should be called to invalidate whole map - different player or level + void update(); + void addQuestMarks (const QuestInfo * q); + + void showAll(Canvas & to) override; +}; + +class CQuestLog : public CWindowObject +{ + int questIndex; + const QuestInfo * currentQuest; + std::shared_ptr componentsBox; + bool hideComplete; + std::shared_ptr hideCompleteButton; + std::shared_ptr hideCompleteLabel; + + const std::vector quests; + std::vector> labels; + std::shared_ptr description; + std::shared_ptr minimap; + std::shared_ptr slider; //scrolls quests + std::shared_ptr ok; + +public: + CQuestLog(const std::vector & Quests); + + ~CQuestLog(){}; + + void selectQuest (int which, int labelId); + void updateMinimap (int which){}; + void printDescription (int which){}; + void sliderMoved (int newpos); + void recreateLabelList(); + void recreateQuestList (int pos); + void toggleComplete(bool on); + void showAll (Canvas & to) override; +}; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 8d28b145e..bc43ea654 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -1,685 +1,685 @@ -/* - * CSpellWindow.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 "CSpellWindow.h" - -#include "../../lib/ScopeGuard.h" - -#include "GUIClasses.h" -#include "InfoWindows.h" -#include "CCastleInterface.h" - -#include "../CGameInfo.h" -#include "../CPlayerInterface.h" -#include "../PlayerLocalState.h" -#include "../CVideoHandler.h" - -#include "../battle/BattleInterface.h" -#include "../gui/CGuiHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" -#include "../widgets/MiscWidgets.h" -#include "../widgets/CComponent.h" -#include "../widgets/TextControls.h" -#include "../adventureMap/AdventureMapInterface.h" -#include "../render/CAnimation.h" -#include "../render/IRenderHandler.h" -#include "../render/IImage.h" -#include "../render/IImageLoader.h" -#include "../render/Canvas.h" - -#include "../../CCallback.h" - -#include "../../lib/CConfigHandler.h" -#include "../../lib/CGeneralTextHandler.h" -#include "../../lib/spells/CSpellHandler.h" -#include "../../lib/spells/Problem.h" -#include "../../lib/GameConstants.h" - -#include "../../lib/mapObjects/CGHeroInstance.h" - -CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function funcL, int helpTextId, CSpellWindow * _owner) -{ - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - pos = myRect; - onLeft = funcL; - hoverText = CGI->generaltexth->zelp[helpTextId].first; - helpText = CGI->generaltexth->zelp[helpTextId].second; - owner = _owner; -} - -void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition) -{ - onLeft(); -} - -void CSpellWindow::InteractiveArea::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(helpText); -} - -void CSpellWindow::InteractiveArea::hover(bool on) -{ - if(on) - owner->statusBar->write(hoverText); - else - owner->statusBar->clear(); -} - -class SpellbookSpellSorter -{ -public: - bool operator()(const CSpell * A, const CSpell * B) - { - if(A->getLevel() < B->getLevel()) - return true; - if(A->getLevel() > B->getLevel()) - return false; - - - for(auto schoolId = 0; schoolId < GameConstants::DEFAULT_SCHOOLS; schoolId++) - { - if(A->school.at(SpellSchool(schoolId)) && !B->school.at(SpellSchool(schoolId))) - return true; - if(!A->school.at(SpellSchool(schoolId)) && B->school.at(SpellSchool(schoolId))) - return false; - } - - return A->getNameTranslated() < B->getNameTranslated(); - } -} spellsorter; - -CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED | (settings["general"]["enableUiEnhancements"].Bool() ? BORDERED : 0)), - battleSpellsOnly(openOnBattleSpells), - selectedTab(4), - currentPage(0), - myHero(_myHero), - myInt(_myInt), - isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()), - spellsPerPage(24), - offL(-11), - offR(195), - offRM(110), - offT(-37), - offB(56) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(isBigSpellbook) - { - background = std::make_shared(createBigSpellBook(), Point(0, 0)); - updateShadow(); - } - else - { - background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); - offL = offR = offT = offB = offRM = 0; - spellsPerPage = 12; - } - pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); - - //initializing castable spells - mySpells.reserve(CGI->spellh->objects.size()); - for(const CSpell * spell : CGI->spellh->objects) - { - if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell)) - mySpells.push_back(spell); - } - std::sort(mySpells.begin(), mySpells.end(), spellsorter); - - //initializing sizes of spellbook's parts - for(auto & elem : sitesPerTabAdv) - elem = 0; - for(auto & elem : sitesPerTabBattle) - elem = 0; - - for(const auto spell : mySpells) - { - int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; - - ++sitesPerOurTab[4]; - - spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) - { - ++sitesPerOurTab[school]; - }); - } - if(sitesPerTabAdv[4] % spellsPerPage == 0) - sitesPerTabAdv[4]/=spellsPerPage; - else - sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabAdv[v] <= spellsPerPage - 2) - sitesPerTabAdv[v] = 1; - else - { - if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; - else - sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; - } - } - - if(sitesPerTabBattle[4] % spellsPerPage == 0) - sitesPerTabBattle[4]/=spellsPerPage; - else - sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; - - for(int v=0; v<4; ++v) - { - if(sitesPerTabBattle[v] <= spellsPerPage - 2) - sitesPerTabBattle[v] = 1; - else - { - if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; - else - sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; - } - } - - - //numbers of spell pages computed - - leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT); - rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT); - - spellIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("Spells")); - - schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524 + offR, 88); - schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT); - - schoolBorders[0] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevA.def")); - schoolBorders[1] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevF.def")); - schoolBorders[2] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevW.def")); - schoolBorders[3] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevE.def")); - - for(auto item : schoolBorders) - item->preload(); - mana = std::make_shared(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); - - if(isBigSpellbook) - statusBar = CGStatusBar::create(400, 587); - else - statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); - - Rect schoolRect( 549 + pos.x + offR, 94 + pos.y, 45, 35); - interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); - interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); - interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); - interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); - interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 0), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); - interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 57), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); - interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); - interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); - interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); - - interactiveAreas.push_back(std::make_shared( Rect( 97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); - interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); - - //areas for spells - int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; - - for(int v=0; v( Rect(xpos, ypos, 65, 78), this); - - if(v == (spellsPerPage / 2) - 1) //to right page - { - xpos = offRM + 336 + pos.x; ypos = 90 + offT + pos.y; - } - else - { - if(v%(isBigSpellbook ? 3 : 2) == 0 || (v%3 == 1 && isBigSpellbook)) - { - xpos+=85; - } - else - { - xpos -= (isBigSpellbook ? 2 : 1)*85; ypos+=97; - } - } - } - - selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; - schoolTab->setFrame(selectedTab, 0); - int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap; - // spellbook last page battle index is not reset after battle, so this needs to stay here - vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1)); - setCurrentPage(cp); - computeSpellsPerArea(); - addUsedEvents(KEYBOARD); -} - -CSpellWindow::~CSpellWindow() -{ -} - -std::shared_ptr CSpellWindow::createBigSpellBook() -{ - std::shared_ptr img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack")); - Canvas canvas = Canvas(Point(800, 600)); - // edges - canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); - canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); - canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); - canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); - // left / right - Canvas tmp1 = Canvas(Point(90, 355 - 45)); - tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45)); - canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); - Canvas tmp2 = Canvas(Point(95, 355 - 45)); - tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); - canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415)); - // top / bottom - Canvas tmp3 = Canvas(Point(409, 45)); - tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45)); - canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45)); - Canvas tmp4 = Canvas(Point(409, 141)); - tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141)); - canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); - // middle - Canvas tmp5 = Canvas(Point(409, 141)); - tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); - canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); - - return GH.renderHandler().createImage(canvas.getInternalSurface()); -} - -void CSpellWindow::fexitb() -{ - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; - (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage; - - close(); -} - -void CSpellWindow::fadvSpellsb() -{ - if(battleSpellsOnly == true) - { - turnPageRight(); - battleSpellsOnly = false; - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fbattleSpellsb() -{ - if(battleSpellsOnly == false) - { - turnPageLeft(); - battleSpellsOnly = true; - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fmanaPtsb() -{ -} - -void CSpellWindow::selectSchool(int school) -{ - if(selectedTab != school) - { - if(selectedTab < school) - turnPageLeft(); - else - turnPageRight(); - selectedTab = school; - schoolTab->setFrame(selectedTab, 0); - setCurrentPage(0); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fLcornerb() -{ - if(currentPage>0) - { - turnPageLeft(); - setCurrentPage(currentPage - 1); - } - computeSpellsPerArea(); -} - -void CSpellWindow::fRcornerb() -{ - if((currentPage + 1) < (pagesWithinCurrentTab())) - { - turnPageRight(); - setCurrentPage(currentPage + 1); - } - computeSpellsPerArea(); -} - -void CSpellWindow::show(Canvas & to) -{ - statusBar->show(to); -} - -void CSpellWindow::computeSpellsPerArea() -{ - std::vector spellsCurSite; - spellsCurSite.reserve(mySpells.size()); - for(const CSpell * spell : mySpells) - { - if(spell->isCombat() ^ !battleSpellsOnly - && ((selectedTab == 4) || spell->school.at(SpellSchool(selectedTab))) - ) - { - spellsCurSite.push_back(spell); - } - } - - if(selectedTab == 4) - { - if(spellsCurSite.size() > spellsPerPage) - { - spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*spellsPerPage, spellsCurSite.end()); - if(spellsCurSite.size() > spellsPerPage) - { - spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); - } - } - } - else //selectedTab == 0, 1, 2 or 3 - { - if(spellsCurSite.size() > spellsPerPage - 2) - { - if(currentPage == 0) - { - spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage-2, spellsCurSite.end()); - } - else - { - spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*spellsPerPage + spellsPerPage-2, spellsCurSite.end()); - if(spellsCurSite.size() > spellsPerPage) - { - spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); - } - } - } - } - //applying - if(selectedTab == 4 || currentPage != 0) - { - for(size_t c=0; csetSpell(spellsCurSite[c]); - } - else - { - spellAreas[c]->setSpell(nullptr); - } - } - } - else - { - spellAreas[0]->setSpell(nullptr); - spellAreas[1]->setSpell(nullptr); - for(size_t c=0; csetSpell(spellsCurSite[c]); - else - spellAreas[c+2]->setSpell(nullptr); - } - } - redraw(); -} - -void CSpellWindow::setCurrentPage(int value) -{ - currentPage = value; - schoolPicture->visible = selectedTab!=4 && currentPage == 0; - if(selectedTab != 4) - schoolPicture->setFrame(selectedTab, 0); - leftCorner->visible = currentPage != 0; - rightCorner->visible = (currentPage+1) < pagesWithinCurrentTab(); - - mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book -} - -void CSpellWindow::turnPageLeft() -{ - if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); -} - -void CSpellWindow::turnPageRight() -{ - if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) - CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); -} - -void CSpellWindow::keyPressed(EShortcut key) -{ - switch(key) - { - case EShortcut::GLOBAL_RETURN: - fexitb(); - break; - - case EShortcut::MOVE_LEFT: - fLcornerb(); - break; - case EShortcut::MOVE_RIGHT: - fRcornerb(); - break; - case EShortcut::MOVE_UP: - case EShortcut::MOVE_DOWN: - { - bool down = key == EShortcut::MOVE_DOWN; - static const int schoolsOrder[] = { 0, 3, 1, 2, 4 }; - int index = -1; - while(schoolsOrder[++index] != selectedTab); - index += (down ? 1 : -1); - vstd::abetween(index, 0, std::size(schoolsOrder) - 1); - if(selectedTab != schoolsOrder[index]) - selectSchool(schoolsOrder[index]); - break; - } - case EShortcut::SPELLBOOK_TAB_COMBAT: - fbattleSpellsb(); - break; - case EShortcut::SPELLBOOK_TAB_ADVENTURE: - fadvSpellsb(); - break; - } -} - -int CSpellWindow::pagesWithinCurrentTab() -{ - return battleSpellsOnly ? sitesPerTabBattle[selectedTab] : sitesPerTabAdv[selectedTab]; -} - -CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner) -{ - this->pos = pos; - this->owner = owner; - addUsedEvents(LCLICK | SHOW_POPUP | HOVER); - - schoolLevel = -1; - mySpell = nullptr; - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - image = std::make_shared(owner->spellIcons, 0, 0); - image->visible = false; - - name = std::make_shared(39, 70, FONT_TINY, ETextAlignment::CENTER); - level = std::make_shared(39, 82, FONT_TINY, ETextAlignment::CENTER); - cost = std::make_shared(39, 94, FONT_TINY, ETextAlignment::CENTER); - - for(auto l : {name, level, cost}) - l->setAutoRedraw(false); -} - -CSpellWindow::SpellArea::~SpellArea() = default; - -void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) -{ - if(mySpell) - { - auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); - if(spellCost > owner->myHero->mana) //insufficient mana - { - LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana)); - return; - } - - //anything that is not combat spell is adventure spell - //this not an error in general to cast even creature ability with hero - const bool combatSpell = mySpell->isCombat(); - if(combatSpell == mySpell->isAdventure()) - { - logGlobal->error("Spell have invalid flags"); - return; - } - - const bool inCombat = owner->myInt->battleInt != nullptr; - const bool inCastle = owner->myInt->castleInt != nullptr; - - //battle spell on adv map or adventure map spell during combat => display infowindow, not cast - if((combatSpell ^ inCombat) || inCastle) - { - std::vector> hlp(1, std::make_shared(CComponent::spell, mySpell->id, 0)); - LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); - } - else if(combatSpell) - { - spells::detail::ProblemImpl problem; - if(mySpell->canBeCast(problem, owner->myInt->battleInt->getBattle().get(), spells::Mode::HERO, owner->myHero)) - { - owner->myInt->battleInt->castThisSpell(mySpell->id); - owner->fexitb(); - } - else - { - std::vector texts; - problem.getAll(texts); - if(!texts.empty()) - LOCPLINT->showInfoDialog(texts.front()); - else - LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); - } - } - else //adventure spell - { - const CGHeroInstance * h = owner->myHero; - GH.windows().popWindows(1); - - auto guard = vstd::makeScopeGuard([this]() - { - owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; - owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage; - }); - - if(mySpell->getTargetType() == spells::AimType::LOCATION) - adventureInt->enterCastingMode(mySpell); - else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) - owner->myInt->cb->castSpell(h, mySpell->id); - else - logGlobal->error("Invalid spell target type"); - } - } -} - -void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) -{ - if(mySpell) - { - std::string dmgInfo; - auto causedDmg = owner->myInt->cb->estimateSpellDamage(mySpell, owner->myHero); - if(causedDmg == 0 || mySpell->id == SpellID::TITANS_LIGHTNING_BOLT) //Titan's Lightning Bolt already has damage info included - dmgInfo.clear(); - else - { - dmgInfo = CGI->generaltexth->allTexts[343]; - boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); - } - - CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(CComponent::spell, mySpell->id)); - } -} - -void CSpellWindow::SpellArea::hover(bool on) -{ - if(mySpell) - { - if(on) - owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->getLevel()])); - else - owner->statusBar->clear(); - } -} - -void CSpellWindow::SpellArea::setSpell(const CSpell * spell) -{ - schoolBorder.reset(); - image->visible = false; - name->setText(""); - level->setText(""); - cost->setText(""); - mySpell = spell; - if(mySpell) - { - int32_t whichSchool = 0; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, - schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); - auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); - - image->setFrame(mySpell->id); - image->visible = true; - - { - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); - } - - ColorRGBA firstLineColor, secondLineColor; - if(spellCost > owner->myHero->mana) //hero cannot cast this spell - { - firstLineColor = Colors::WHITE; - secondLineColor = Colors::ORANGE; - } - else - { - firstLineColor = Colors::YELLOW; - secondLineColor = Colors::WHITE; - } - - name->color = firstLineColor; - name->setText(mySpell->getNameTranslated()); - - level->color = secondLineColor; - if(schoolLevel > 0) - { - boost::format fmt("%s/%s"); - fmt % CGI->generaltexth->allTexts[171 + mySpell->getLevel()]; - fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 - level->setText(fmt.str()); - } - else - level->setText(CGI->generaltexth->allTexts[171 + mySpell->getLevel()]); - - cost->color = secondLineColor; - boost::format costfmt("%s: %d"); - costfmt % CGI->generaltexth->allTexts[387] % spellCost; - cost->setText(costfmt.str()); - } -} +/* + * CSpellWindow.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 "CSpellWindow.h" + +#include "../../lib/ScopeGuard.h" + +#include "GUIClasses.h" +#include "InfoWindows.h" +#include "CCastleInterface.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" +#include "../PlayerLocalState.h" +#include "../CVideoHandler.h" + +#include "../battle/BattleInterface.h" +#include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" +#include "../widgets/MiscWidgets.h" +#include "../widgets/CComponent.h" +#include "../widgets/TextControls.h" +#include "../adventureMap/AdventureMapInterface.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" +#include "../render/IImage.h" +#include "../render/IImageLoader.h" +#include "../render/Canvas.h" + +#include "../../CCallback.h" + +#include "../../lib/CConfigHandler.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/spells/CSpellHandler.h" +#include "../../lib/spells/Problem.h" +#include "../../lib/GameConstants.h" + +#include "../../lib/mapObjects/CGHeroInstance.h" + +CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::function funcL, int helpTextId, CSpellWindow * _owner) +{ + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + pos = myRect; + onLeft = funcL; + hoverText = CGI->generaltexth->zelp[helpTextId].first; + helpText = CGI->generaltexth->zelp[helpTextId].second; + owner = _owner; +} + +void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition) +{ + onLeft(); +} + +void CSpellWindow::InteractiveArea::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(helpText); +} + +void CSpellWindow::InteractiveArea::hover(bool on) +{ + if(on) + owner->statusBar->write(hoverText); + else + owner->statusBar->clear(); +} + +class SpellbookSpellSorter +{ +public: + bool operator()(const CSpell * A, const CSpell * B) + { + if(A->getLevel() < B->getLevel()) + return true; + if(A->getLevel() > B->getLevel()) + return false; + + + for(auto schoolId = 0; schoolId < GameConstants::DEFAULT_SCHOOLS; schoolId++) + { + if(A->school.at(SpellSchool(schoolId)) && !B->school.at(SpellSchool(schoolId))) + return true; + if(!A->school.at(SpellSchool(schoolId)) && B->school.at(SpellSchool(schoolId))) + return false; + } + + return A->getNameTranslated() < B->getNameTranslated(); + } +} spellsorter; + +CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): + CWindowObject(PLAYER_COLORED | (settings["general"]["enableUiEnhancements"].Bool() ? BORDERED : 0)), + battleSpellsOnly(openOnBattleSpells), + selectedTab(4), + currentPage(0), + myHero(_myHero), + myInt(_myInt), + isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()), + spellsPerPage(24), + offL(-11), + offR(195), + offRM(110), + offT(-37), + offB(56) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + if(isBigSpellbook) + { + background = std::make_shared(createBigSpellBook(), Point(0, 0)); + updateShadow(); + } + else + { + background = std::make_shared(ImagePath::builtin("SpelBack"), 0, 0); + offL = offR = offT = offB = offRM = 0; + spellsPerPage = 12; + } + pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y)); + + //initializing castable spells + mySpells.reserve(CGI->spellh->objects.size()); + for(const CSpell * spell : CGI->spellh->objects) + { + if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell)) + mySpells.push_back(spell); + } + std::sort(mySpells.begin(), mySpells.end(), spellsorter); + + //initializing sizes of spellbook's parts + for(auto & elem : sitesPerTabAdv) + elem = 0; + for(auto & elem : sitesPerTabBattle) + elem = 0; + + for(const auto spell : mySpells) + { + int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv; + + ++sitesPerOurTab[4]; + + spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop) + { + ++sitesPerOurTab[school]; + }); + } + if(sitesPerTabAdv[4] % spellsPerPage == 0) + sitesPerTabAdv[4]/=spellsPerPage; + else + sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabAdv[v] <= spellsPerPage - 2) + sitesPerTabAdv[v] = 1; + else + { + if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1; + else + sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2; + } + } + + if(sitesPerTabBattle[4] % spellsPerPage == 0) + sitesPerTabBattle[4]/=spellsPerPage; + else + sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1; + + for(int v=0; v<4; ++v) + { + if(sitesPerTabBattle[v] <= spellsPerPage - 2) + sitesPerTabBattle[v] = 1; + else + { + if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0) + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1; + else + sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2; + } + } + + + //numbers of spell pages computed + + leftCorner = std::make_shared(ImagePath::builtin("SpelTrnL.bmp"), 97 + offL, 77 + offT); + rightCorner = std::make_shared(ImagePath::builtin("SpelTrnR.bmp"), 487 + offR, 72 + offT); + + spellIcons = GH.renderHandler().loadAnimation(AnimationPath::builtin("Spells")); + + schoolTab = std::make_shared(AnimationPath::builtin("SpelTab"), selectedTab, 0, 524 + offR, 88); + schoolPicture = std::make_shared(AnimationPath::builtin("Schools"), 0, 0, 117 + offL, 74 + offT); + + schoolBorders[0] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevA.def")); + schoolBorders[1] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevF.def")); + schoolBorders[2] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevW.def")); + schoolBorders[3] = GH.renderHandler().loadAnimation(AnimationPath::builtin("SplevE.def")); + + for(auto item : schoolBorders) + item->preload(); + mana = std::make_shared(435 + (isBigSpellbook ? 159 : 0), 426 + offB, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, std::to_string(myHero->mana)); + + if(isBigSpellbook) + statusBar = CGStatusBar::create(400, 587); + else + statusBar = CGStatusBar::create(7, 569, ImagePath::builtin("Spelroll.bmp")); + + Rect schoolRect( 549 + pos.x + offR, 94 + pos.y, 45, 35); + interactiveAreas.push_back(std::make_shared( Rect( 479 + pos.x + (isBigSpellbook ? 175 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fexitb, this), 460, this)); + interactiveAreas.push_back(std::make_shared( Rect( 221 + pos.x + (isBigSpellbook ? 43 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fbattleSpellsb, this), 453, this)); + interactiveAreas.push_back(std::make_shared( Rect( 355 + pos.x + (isBigSpellbook ? 110 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fadvSpellsb, this), 452, this)); + interactiveAreas.push_back(std::make_shared( Rect( 418 + pos.x + (isBigSpellbook ? 142 : 0), 405 + pos.y + offB, isBigSpellbook ? 60 : 36, 56), std::bind(&CSpellWindow::fmanaPtsb, this), 459, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 0), std::bind(&CSpellWindow::selectSchool, this, 0), 454, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 57), std::bind(&CSpellWindow::selectSchool, this, 3), 457, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 116), std::bind(&CSpellWindow::selectSchool, this, 1), 455, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 176), std::bind(&CSpellWindow::selectSchool, this, 2), 456, this)); + interactiveAreas.push_back(std::make_shared( schoolRect + Point(0, 236), std::bind(&CSpellWindow::selectSchool, this, 4), 458, this)); + + interactiveAreas.push_back(std::make_shared( Rect( 97 + offL + pos.x, 77 + offT + pos.y, leftCorner->pos.h, leftCorner->pos.w ), std::bind(&CSpellWindow::fLcornerb, this), 450, this)); + interactiveAreas.push_back(std::make_shared( Rect( 487 + offR + pos.x, 72 + offT + pos.y, rightCorner->pos.h, rightCorner->pos.w ), std::bind(&CSpellWindow::fRcornerb, this), 451, this)); + + //areas for spells + int xpos = 117 + offL + pos.x, ypos = 90 + offT + pos.y; + + for(int v=0; v( Rect(xpos, ypos, 65, 78), this); + + if(v == (spellsPerPage / 2) - 1) //to right page + { + xpos = offRM + 336 + pos.x; ypos = 90 + offT + pos.y; + } + else + { + if(v%(isBigSpellbook ? 3 : 2) == 0 || (v%3 == 1 && isBigSpellbook)) + { + xpos+=85; + } + else + { + xpos -= (isBigSpellbook ? 2 : 1)*85; ypos+=97; + } + } + } + + selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap; + schoolTab->setFrame(selectedTab, 0); + int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap; + // spellbook last page battle index is not reset after battle, so this needs to stay here + vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1)); + setCurrentPage(cp); + computeSpellsPerArea(); + addUsedEvents(KEYBOARD); +} + +CSpellWindow::~CSpellWindow() +{ +} + +std::shared_ptr CSpellWindow::createBigSpellBook() +{ + std::shared_ptr img = GH.renderHandler().loadImage(ImagePath::builtin("SpelBack")); + Canvas canvas = Canvas(Point(800, 600)); + // edges + canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45)); + canvas.draw(img, Point(0, 460), Rect(15, 400, 90, 141)); + canvas.draw(img, Point(705, 0), Rect(509, 38, 95, 45)); + canvas.draw(img, Point(705, 460), Rect(509, 400, 95, 141)); + // left / right + Canvas tmp1 = Canvas(Point(90, 355 - 45)); + tmp1.draw(img, Point(0, 0), Rect(15, 38 + 45, 90, 355 - 45)); + canvas.drawScaled(tmp1, Point(0, 45), Point(90, 415)); + Canvas tmp2 = Canvas(Point(95, 355 - 45)); + tmp2.draw(img, Point(0, 0), Rect(509, 38 + 45, 95, 355 - 45)); + canvas.drawScaled(tmp2, Point(705, 45), Point(95, 415)); + // top / bottom + Canvas tmp3 = Canvas(Point(409, 45)); + tmp3.draw(img, Point(0, 0), Rect(100, 38, 409, 45)); + canvas.drawScaled(tmp3, Point(90, 0), Point(615, 45)); + Canvas tmp4 = Canvas(Point(409, 141)); + tmp4.draw(img, Point(0, 0), Rect(100, 400, 409, 141)); + canvas.drawScaled(tmp4, Point(90, 460), Point(615, 141)); + // middle + Canvas tmp5 = Canvas(Point(409, 141)); + tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38)); + canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415)); + + return GH.renderHandler().createImage(canvas.getInternalSurface()); +} + +void CSpellWindow::fexitb() +{ + (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab; + (myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage; + + close(); +} + +void CSpellWindow::fadvSpellsb() +{ + if(battleSpellsOnly == true) + { + turnPageRight(); + battleSpellsOnly = false; + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fbattleSpellsb() +{ + if(battleSpellsOnly == false) + { + turnPageLeft(); + battleSpellsOnly = true; + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fmanaPtsb() +{ +} + +void CSpellWindow::selectSchool(int school) +{ + if(selectedTab != school) + { + if(selectedTab < school) + turnPageLeft(); + else + turnPageRight(); + selectedTab = school; + schoolTab->setFrame(selectedTab, 0); + setCurrentPage(0); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fLcornerb() +{ + if(currentPage>0) + { + turnPageLeft(); + setCurrentPage(currentPage - 1); + } + computeSpellsPerArea(); +} + +void CSpellWindow::fRcornerb() +{ + if((currentPage + 1) < (pagesWithinCurrentTab())) + { + turnPageRight(); + setCurrentPage(currentPage + 1); + } + computeSpellsPerArea(); +} + +void CSpellWindow::show(Canvas & to) +{ + statusBar->show(to); +} + +void CSpellWindow::computeSpellsPerArea() +{ + std::vector spellsCurSite; + spellsCurSite.reserve(mySpells.size()); + for(const CSpell * spell : mySpells) + { + if(spell->isCombat() ^ !battleSpellsOnly + && ((selectedTab == 4) || spell->school.at(SpellSchool(selectedTab))) + ) + { + spellsCurSite.push_back(spell); + } + } + + if(selectedTab == 4) + { + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite = std::vector(spellsCurSite.begin() + currentPage*spellsPerPage, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); + } + } + } + else //selectedTab == 0, 1, 2 or 3 + { + if(spellsCurSite.size() > spellsPerPage - 2) + { + if(currentPage == 0) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage-2, spellsCurSite.end()); + } + else + { + spellsCurSite = std::vector(spellsCurSite.begin() + (currentPage-1)*spellsPerPage + spellsPerPage-2, spellsCurSite.end()); + if(spellsCurSite.size() > spellsPerPage) + { + spellsCurSite.erase(spellsCurSite.begin()+spellsPerPage, spellsCurSite.end()); + } + } + } + } + //applying + if(selectedTab == 4 || currentPage != 0) + { + for(size_t c=0; csetSpell(spellsCurSite[c]); + } + else + { + spellAreas[c]->setSpell(nullptr); + } + } + } + else + { + spellAreas[0]->setSpell(nullptr); + spellAreas[1]->setSpell(nullptr); + for(size_t c=0; csetSpell(spellsCurSite[c]); + else + spellAreas[c+2]->setSpell(nullptr); + } + } + redraw(); +} + +void CSpellWindow::setCurrentPage(int value) +{ + currentPage = value; + schoolPicture->visible = selectedTab!=4 && currentPage == 0; + if(selectedTab != 4) + schoolPicture->setFrame(selectedTab, 0); + leftCorner->visible = currentPage != 0; + rightCorner->visible = (currentPage+1) < pagesWithinCurrentTab(); + + mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book +} + +void CSpellWindow::turnPageLeft() +{ + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNLFT.SMK"), pos.x+13, pos.y+15); +} + +void CSpellWindow::turnPageRight() +{ + if(settings["video"]["spellbookAnimation"].Bool() && !isBigSpellbook) + CCS->videoh->openAndPlayVideo(VideoPath::builtin("PGTRNRGH.SMK"), pos.x+13, pos.y+15); +} + +void CSpellWindow::keyPressed(EShortcut key) +{ + switch(key) + { + case EShortcut::GLOBAL_RETURN: + fexitb(); + break; + + case EShortcut::MOVE_LEFT: + fLcornerb(); + break; + case EShortcut::MOVE_RIGHT: + fRcornerb(); + break; + case EShortcut::MOVE_UP: + case EShortcut::MOVE_DOWN: + { + bool down = key == EShortcut::MOVE_DOWN; + static const int schoolsOrder[] = { 0, 3, 1, 2, 4 }; + int index = -1; + while(schoolsOrder[++index] != selectedTab); + index += (down ? 1 : -1); + vstd::abetween(index, 0, std::size(schoolsOrder) - 1); + if(selectedTab != schoolsOrder[index]) + selectSchool(schoolsOrder[index]); + break; + } + case EShortcut::SPELLBOOK_TAB_COMBAT: + fbattleSpellsb(); + break; + case EShortcut::SPELLBOOK_TAB_ADVENTURE: + fadvSpellsb(); + break; + } +} + +int CSpellWindow::pagesWithinCurrentTab() +{ + return battleSpellsOnly ? sitesPerTabBattle[selectedTab] : sitesPerTabAdv[selectedTab]; +} + +CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner) +{ + this->pos = pos; + this->owner = owner; + addUsedEvents(LCLICK | SHOW_POPUP | HOVER); + + schoolLevel = -1; + mySpell = nullptr; + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + image = std::make_shared(owner->spellIcons, 0, 0); + image->visible = false; + + name = std::make_shared(39, 70, FONT_TINY, ETextAlignment::CENTER); + level = std::make_shared(39, 82, FONT_TINY, ETextAlignment::CENTER); + cost = std::make_shared(39, 94, FONT_TINY, ETextAlignment::CENTER); + + for(auto l : {name, level, cost}) + l->setAutoRedraw(false); +} + +CSpellWindow::SpellArea::~SpellArea() = default; + +void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) +{ + if(mySpell) + { + auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); + if(spellCost > owner->myHero->mana) //insufficient mana + { + LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[206]) % spellCost % owner->myHero->mana)); + return; + } + + //anything that is not combat spell is adventure spell + //this not an error in general to cast even creature ability with hero + const bool combatSpell = mySpell->isCombat(); + if(combatSpell == mySpell->isAdventure()) + { + logGlobal->error("Spell have invalid flags"); + return; + } + + const bool inCombat = owner->myInt->battleInt != nullptr; + const bool inCastle = owner->myInt->castleInt != nullptr; + + //battle spell on adv map or adventure map spell during combat => display infowindow, not cast + if((combatSpell ^ inCombat) || inCastle) + { + std::vector> hlp(1, std::make_shared(CComponent::spell, mySpell->id, 0)); + LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); + } + else if(combatSpell) + { + spells::detail::ProblemImpl problem; + if(mySpell->canBeCast(problem, owner->myInt->battleInt->getBattle().get(), spells::Mode::HERO, owner->myHero)) + { + owner->myInt->battleInt->castThisSpell(mySpell->id); + owner->fexitb(); + } + else + { + std::vector texts; + problem.getAll(texts); + if(!texts.empty()) + LOCPLINT->showInfoDialog(texts.front()); + else + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem")); + } + } + else //adventure spell + { + const CGHeroInstance * h = owner->myHero; + GH.windows().popWindows(1); + + auto guard = vstd::makeScopeGuard([this]() + { + owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab; + owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage; + }); + + if(mySpell->getTargetType() == spells::AimType::LOCATION) + adventureInt->enterCastingMode(mySpell); + else if(mySpell->getTargetType() == spells::AimType::NO_TARGET) + owner->myInt->cb->castSpell(h, mySpell->id); + else + logGlobal->error("Invalid spell target type"); + } + } +} + +void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) +{ + if(mySpell) + { + std::string dmgInfo; + auto causedDmg = owner->myInt->cb->estimateSpellDamage(mySpell, owner->myHero); + if(causedDmg == 0 || mySpell->id == SpellID::TITANS_LIGHTNING_BOLT) //Titan's Lightning Bolt already has damage info included + dmgInfo.clear(); + else + { + dmgInfo = CGI->generaltexth->allTexts[343]; + boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); + } + + CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(CComponent::spell, mySpell->id)); + } +} + +void CSpellWindow::SpellArea::hover(bool on) +{ + if(mySpell) + { + if(on) + owner->statusBar->write(boost::str(boost::format("%s (%s)") % mySpell->getNameTranslated() % CGI->generaltexth->allTexts[171+mySpell->getLevel()])); + else + owner->statusBar->clear(); + } +} + +void CSpellWindow::SpellArea::setSpell(const CSpell * spell) +{ + schoolBorder.reset(); + image->visible = false; + name->setText(""); + level->setText(""); + cost->setText(""); + mySpell = spell; + if(mySpell) + { + int32_t whichSchool = 0; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, + schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); + auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); + + image->setFrame(mySpell->id); + image->visible = true; + + { + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); + } + + ColorRGBA firstLineColor, secondLineColor; + if(spellCost > owner->myHero->mana) //hero cannot cast this spell + { + firstLineColor = Colors::WHITE; + secondLineColor = Colors::ORANGE; + } + else + { + firstLineColor = Colors::YELLOW; + secondLineColor = Colors::WHITE; + } + + name->color = firstLineColor; + name->setText(mySpell->getNameTranslated()); + + level->color = secondLineColor; + if(schoolLevel > 0) + { + boost::format fmt("%s/%s"); + fmt % CGI->generaltexth->allTexts[171 + mySpell->getLevel()]; + fmt % CGI->generaltexth->levels[3+(schoolLevel-1)];//lines 4-6 + level->setText(fmt.str()); + } + else + level->setText(CGI->generaltexth->allTexts[171 + mySpell->getLevel()]); + + cost->color = secondLineColor; + boost::format costfmt("%s: %d"); + costfmt % CGI->generaltexth->allTexts[387] % spellCost; + cost->setText(costfmt.str()); + } +} diff --git a/client/windows/CSpellWindow.h b/client/windows/CSpellWindow.h index bbfe1b638..941de6428 100644 --- a/client/windows/CSpellWindow.h +++ b/client/windows/CSpellWindow.h @@ -1,128 +1,128 @@ -/* - * CSpellWindow.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 "CWindowObject.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CSpell; - -VCMI_LIB_NAMESPACE_END - -class IImage; -class CAnimation; -class CAnimImage; -class CPicture; -class CLabel; -class CGStatusBar; -class CPlayerInterface; -class CSpellWindow; - -/// The spell window -class CSpellWindow : public CWindowObject -{ - class SpellArea : public CIntObject - { - const CSpell * mySpell; - int schoolLevel; //range: 0 none, 3 - expert - CSpellWindow * owner; - std::shared_ptr image; - std::shared_ptr schoolBorder; - std::shared_ptr name; - std::shared_ptr level; - std::shared_ptr cost; - public: - SpellArea(Rect pos, CSpellWindow * owner); - ~SpellArea(); - void setSpell(const CSpell * spell); - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - }; - - class InteractiveArea : public CIntObject - { - std::function onLeft; - CSpellWindow * owner; - - std::string hoverText; - std::string helpText; - public: - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - - InteractiveArea(const Rect &myRect, std::function funcL, int helpTextId, CSpellWindow * _owner); - }; - - std::shared_ptr spellIcons; - std::array, 4> schoolBorders; //[0]: air, [1]: fire, [2]: water, [3]: earth - - std::shared_ptr leftCorner; - std::shared_ptr rightCorner; - - std::shared_ptr schoolTab; - std::shared_ptr schoolPicture; - - std::array, 24> spellAreas; - std::shared_ptr mana; - std::shared_ptr statusBar; - - std::vector> interactiveAreas; - - bool isBigSpellbook; - int spellsPerPage; - int offL; - int offR; - int offRM; - int offT; - int offB; - - int sitesPerTabAdv[5]; - int sitesPerTabBattle[5]; - - bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed - uint8_t selectedTab; // 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools - int currentPage; //changes when corners are clicked - std::vector mySpells; //all spels in this spellbook - - const CGHeroInstance * myHero; //hero whose spells are presented - CPlayerInterface * myInt; - - void computeSpellsPerArea(); //recalculates spellAreas::mySpell - - void setCurrentPage(int value); - void turnPageLeft(); - void turnPageRight(); - - std::shared_ptr createBigSpellBook(); - -public: - CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); - ~CSpellWindow(); - - void fexitb(); - void fadvSpellsb(); - void fbattleSpellsb(); - void fmanaPtsb(); - - void fLcornerb(); - void fRcornerb(); - - void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools - int pagesWithinCurrentTab(); - - void keyPressed(EShortcut key) override; - - void show(Canvas & to) override; -}; +/* + * CSpellWindow.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 "CWindowObject.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CSpell; + +VCMI_LIB_NAMESPACE_END + +class IImage; +class CAnimation; +class CAnimImage; +class CPicture; +class CLabel; +class CGStatusBar; +class CPlayerInterface; +class CSpellWindow; + +/// The spell window +class CSpellWindow : public CWindowObject +{ + class SpellArea : public CIntObject + { + const CSpell * mySpell; + int schoolLevel; //range: 0 none, 3 - expert + CSpellWindow * owner; + std::shared_ptr image; + std::shared_ptr schoolBorder; + std::shared_ptr name; + std::shared_ptr level; + std::shared_ptr cost; + public: + SpellArea(Rect pos, CSpellWindow * owner); + ~SpellArea(); + void setSpell(const CSpell * spell); + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + }; + + class InteractiveArea : public CIntObject + { + std::function onLeft; + CSpellWindow * owner; + + std::string hoverText; + std::string helpText; + public: + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + + InteractiveArea(const Rect &myRect, std::function funcL, int helpTextId, CSpellWindow * _owner); + }; + + std::shared_ptr spellIcons; + std::array, 4> schoolBorders; //[0]: air, [1]: fire, [2]: water, [3]: earth + + std::shared_ptr leftCorner; + std::shared_ptr rightCorner; + + std::shared_ptr schoolTab; + std::shared_ptr schoolPicture; + + std::array, 24> spellAreas; + std::shared_ptr mana; + std::shared_ptr statusBar; + + std::vector> interactiveAreas; + + bool isBigSpellbook; + int spellsPerPage; + int offL; + int offR; + int offRM; + int offT; + int offB; + + int sitesPerTabAdv[5]; + int sitesPerTabBattle[5]; + + bool battleSpellsOnly; //if true, only battle spells are displayed; if false, only adventure map spells are displayed + uint8_t selectedTab; // 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools + int currentPage; //changes when corners are clicked + std::vector mySpells; //all spels in this spellbook + + const CGHeroInstance * myHero; //hero whose spells are presented + CPlayerInterface * myInt; + + void computeSpellsPerArea(); //recalculates spellAreas::mySpell + + void setCurrentPage(int value); + void turnPageLeft(); + void turnPageRight(); + + std::shared_ptr createBigSpellBook(); + +public: + CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true); + ~CSpellWindow(); + + void fexitb(); + void fadvSpellsb(); + void fbattleSpellsb(); + void fmanaPtsb(); + + void fLcornerb(); + void fRcornerb(); + + void selectSchool(int school); //schools: 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, 4 - all schools + int pagesWithinCurrentTab(); + + void keyPressed(EShortcut key) override; + + void show(Canvas & to) override; +}; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 5b946b28b..dd06d2dab 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1,1755 +1,1755 @@ -/* - * GUIClasses.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 "GUIClasses.h" - -#include "CCastleInterface.h" -#include "CCreatureWindow.h" -#include "CHeroBackpackWindow.h" -#include "CHeroWindow.h" -#include "InfoWindows.h" - -#include "../CGameInfo.h" -#include "../CMusicHandler.h" -#include "../CPlayerInterface.h" -#include "../CVideoHandler.h" - -#include "../gui/CGuiHandler.h" -#include "../gui/CursorHandler.h" -#include "../gui/Shortcut.h" -#include "../gui/WindowHandler.h" - -#include "../widgets/CComponent.h" -#include "../widgets/CGarrisonInt.h" -#include "../widgets/CreatureCostBox.h" -#include "../widgets/Buttons.h" -#include "../widgets/Slider.h" -#include "../widgets/TextControls.h" -#include "../widgets/ObjectLists.h" - -#include "../render/Canvas.h" -#include "../render/CAnimation.h" -#include "../render/IRenderHandler.h" - -#include "../../CCallback.h" - -#include "../lib/mapObjectConstructors/CObjectClassesHandler.h" -#include "../lib/mapObjectConstructors/CommonConstructors.h" -#include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/mapObjects/CGMarket.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/mapObjects/ObjectTemplate.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/gameState/SThievesGuildInfo.h" -#include "../lib/CGeneralTextHandler.h" -#include "../lib/CHeroHandler.h" -#include "../lib/GameSettings.h" -#include "../lib/CondSh.h" -#include "../lib/CSkillHandler.h" -#include "../lib/filesystem/Filesystem.h" -#include "../lib/TextOperations.h" - -CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) - : CIntObject(LCLICK | SHOW_POPUP), - parent(window), - selected(false), - creature(crea), - amount(totalAmount) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - animation = std::make_shared(1, 1, creature, true, true); - // 1 + 1 px for borders - pos.w = animation->pos.w + 2; - pos.h = animation->pos.h + 2; -} - -void CRecruitmentWindow::CCreatureCard::select(bool on) -{ - selected = on; - redraw(); -} - -void CRecruitmentWindow::CCreatureCard::clickPressed(const Point & cursorPosition) -{ - parent->select(this->shared_from_this()); -} - -void CRecruitmentWindow::CCreatureCard::showPopupWindow(const Point & cursorPosition) -{ - GH.windows().createAndPushWindow(creature, true); -} - -void CRecruitmentWindow::CCreatureCard::showAll(Canvas & to) -{ - CIntObject::showAll(to); - if(selected) - to.drawBorder(pos, Colors::RED); - else - to.drawBorder(pos, Colors::YELLOW); -} - -void CRecruitmentWindow::select(std::shared_ptr card) -{ - if(card == selected) - return; - - if(selected) - selected->select(false); - - selected = card; - - if(selected) - selected->select(true); - - if(card) - { - si32 maxAmount = card->creature->maxAmount(LOCPLINT->cb->getResourceAmount()); - - vstd::amin(maxAmount, card->amount); - - slider->setAmount(maxAmount); - - if(slider->getValue() != maxAmount) - slider->scrollTo(maxAmount); - else // if slider already at 0 - emulate call to sliderMoved() - sliderMoved(maxAmount); - - costPerTroopValue->createItems(card->creature->getFullRecruitCost()); - totalCostValue->createItems(card->creature->getFullRecruitCost()); - - costPerTroopValue->set(card->creature->getFullRecruitCost()); - totalCostValue->set(card->creature->getFullRecruitCost() * maxAmount); - - //Recruit %s - title->setText(boost::str(boost::format(CGI->generaltexth->tcommands[21]) % card->creature->getNamePluralTranslated())); - - maxButton->block(maxAmount == 0); - slider->block(maxAmount == 0); - } -} - -void CRecruitmentWindow::close() -{ - if (onClose) - onClose(); - CStatusbarWindow::close(); -} - -void CRecruitmentWindow::buy() -{ - CreatureID crid = selected->creature->getId(); - SlotID dstslot = dst->getSlotFor(crid); - - if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot - { - std::pair toMerge; - bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); - - if (allowMerge && dst->mergableStacks(toMerge)) - { - LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); - } - else - { - std::string txt; - if(dst->ID == Obj::HERO) - { - txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. - boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); - } - else - { - txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. - } - - LOCPLINT->showInfoDialog(txt); - return; - } - } - - onRecruit(crid, slider->getValue()); - if(level >= 0) - close(); -} - -void CRecruitmentWindow::showAll(Canvas & to) -{ - CWindowObject::showAll(to); - - Rect(172, 222, 67, 42) + pos.topLeft(); - - // recruit\total values - to.drawBorder(Rect(172, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); - to.drawBorder(Rect(246, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); - - //cost boxes - to.drawBorder(Rect( 64, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); - to.drawBorder(Rect(322, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); - - //buttons borders - to.drawBorder(Rect(133, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); - to.drawBorder(Rect(211, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); - to.drawBorder(Rect(289, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); -} - -CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset): - CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPRCRT")), - onRecruit(Recruit), - onClose(onClose), - level(Level), - dst(Dst), - selected(nullptr), - dwelling(Dwelling) -{ - moveBy(Point(0, y_offset)); - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - slider = std::make_shared(Point(176, 279), 135, std::bind(&CRecruitmentWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - - maxButton = std::make_shared(Point(134, 313), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); - buyButton = std::make_shared(Point(212, 313), AnimationPath::builtin("IBY6432.DEF"), CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); - cancelButton = std::make_shared(Point(290, 313), AnimationPath::builtin("ICN6432.DEF"), CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); - - title = std::make_shared(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); - availableValue = std::make_shared(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - toRecruitValue = std::make_shared(279, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - costPerTroopValue = std::make_shared(Rect(65, 222, 97, 74), CGI->generaltexth->allTexts[346]); - totalCostValue = std::make_shared(Rect(323, 222, 97, 74), CGI->generaltexth->allTexts[466]); - - availableTitle = std::make_shared(205, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[465]); - toRecruitTitle = std::make_shared(279, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[16]); - - availableCreaturesChanged(); -} - -void CRecruitmentWindow::availableCreaturesChanged() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - - size_t selectedIndex = 0; - - if(!cards.empty() && selected) // find position of selected item - selectedIndex = std::find(cards.begin(), cards.end(), selected) - cards.begin(); - - select(nullptr); - - cards.clear(); - - for(int i=0; icreatures.size(); i++) - { - //find appropriate level - if(level >= 0 && i != level) - continue; - - int amount = dwelling->creatures[i].first; - - //create new cards - for(auto & creature : boost::adaptors::reverse(dwelling->creatures[i].second)) - cards.push_back(std::make_shared(this, CGI->creh->objects[creature], amount)); - } - - const int creatureWidth = 102; - - //normal distance between cards - 18px - int requiredSpace = 18; - //maximum distance we can use without reaching window borders - int availableSpace = pos.w - 50 - creatureWidth * (int)cards.size(); - - if (cards.size() > 1) // avoid division by zero - availableSpace /= (int)cards.size() - 1; - else - availableSpace = 0; - - assert(availableSpace >= 0); - - const int spaceBetween = std::min(requiredSpace, availableSpace); - const int totalCreatureWidth = spaceBetween + creatureWidth; - - //now we know total amount of cards and can move them to correct position - int curx = pos.w / 2 - (creatureWidth*(int)cards.size()/2) - (spaceBetween*((int)cards.size()-1)/2); - for(auto & card : cards) - { - card->moveBy(Point(curx, 64)); - curx += totalCreatureWidth; - } - - //restore selection - select(cards[selectedIndex]); - - if(slider->getValue() == slider->getAmount()) - slider->scrollToMax(); - else // if slider already at 0 - emulate call to sliderMoved() - sliderMoved(slider->getAmount()); -} - -void CRecruitmentWindow::sliderMoved(int to) -{ - if(!selected) - return; - - buyButton->block(!to); - availableValue->setText(std::to_string(selected->amount - to)); - toRecruitValue->setText(std::to_string(to)); - - totalCostValue->set(selected->creature->getFullRecruitCost() * to); -} - -CSplitWindow::CSplitWindow(const CCreature * creature, std::function callback_, int leftMin_, int rightMin_, int leftAmount_, int rightAmount_) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GPUCRDIV")), - callback(callback_), - leftAmount(leftAmount_), - rightAmount(rightAmount_), - leftMin(leftMin_), - rightMin(rightMin_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - int total = leftAmount + rightAmount; - int leftMax = total - rightMin; - int rightMax = total - leftMin; - - ok = std::make_shared(Point(20, 263), AnimationPath::builtin("IOK6432"), CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(214, 263), AnimationPath::builtin("ICN6432"), CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); - - int sliderPosition = total - leftMin - rightMin; - - leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); - rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); - - //add filters to allow only number input - leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); - rightInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, rightMin, rightMax); - - leftInput->setText(std::to_string(leftAmount), false); - rightInput->setText(std::to_string(rightAmount), false); - - animLeft = std::make_shared(20, 54, creature, true, false); - animRight = std::make_shared(177, 54,creature, true, false); - - slider = std::make_shared(Point(21, 194), 257, std::bind(&CSplitWindow::sliderMoved, this, _1), 0, sliderPosition, rightAmount - rightMin, Orientation::HORIZONTAL); - - std::string titleStr = CGI->generaltexth->allTexts[256]; - boost::algorithm::replace_first(titleStr,"%s", creature->getNamePluralTranslated()); - title = std::make_shared(150, 34, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleStr); -} - -void CSplitWindow::setAmountText(std::string text, bool left) -{ - int amount = 0; - if(text.length()) - { - try - { - amount = boost::lexical_cast(text); - } - catch(boost::bad_lexical_cast &) - { - amount = left ? leftAmount : rightAmount; - } - - int total = leftAmount + rightAmount; - if(amount > total) - amount = total; - } - - setAmount(amount, left); - slider->scrollTo(rightAmount - rightMin); -} - -void CSplitWindow::setAmount(int value, bool left) -{ - int total = leftAmount + rightAmount; - leftAmount = left ? value : total - value; - rightAmount = left ? total - value : value; - - leftInput->setText(std::to_string(leftAmount)); - rightInput->setText(std::to_string(rightAmount)); -} - -void CSplitWindow::apply() -{ - callback(leftAmount, rightAmount); - close(); -} - -void CSplitWindow::sliderMoved(int to) -{ - setAmount(rightMin + to, false); -} - -CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, std::function callback) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("LVLUPBKG")), - cb(callback) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - LOCPLINT->showingDialog->setn(true); - - if(!skills.empty()) - { - std::vector> comps; - for(auto & skill : skills) - { - auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); - comp->onChoose = std::bind(&CLevelWindow::close, this); - comps.push_back(comp); - } - - box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); - } - - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66); - ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); - - //%s has gained a level. - mainTitle = std::make_shared(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated())); - - //%s is now a level %d %s. - std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445"); - boost::replace_first(levelTitleText, "%s", hero->getNameTranslated()); - boost::replace_first(levelTitleText, "%d", std::to_string(hero->level)); - boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated()); - - levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); - - skillIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), static_cast(pskill), 0, 174, 190); - - skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[static_cast(pskill)] + " +1"); -} - - -CLevelWindow::~CLevelWindow() -{ - //FIXME: call callback if there was nothing to select? - if (box && box->selectedIndex() != -1) - cb(box->selectedIndex()); - - LOCPLINT->showingDialog->setn(false); -} - -CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), - onWindowClosed(onWindowClosed), - tavernObj(TavernObj) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - std::vector h = LOCPLINT->cb->getAvailableHeroes(TavernObj); - if(h.size() < 2) - h.resize(2, nullptr); - - selected = 0; - if(!h[0]) - selected = 1; - if(!h[0] && !h[1]) - selected = -1; - - oldSelected = -1; - - h1 = std::make_shared(selected, 0, 72, 299, h[0]); - h2 = std::make_shared(selected, 1, 162, 299, h[1]); - - title = std::make_shared(197, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[37]); - cost = std::make_shared(320, 328, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(GameConstants::HERO_GOLD_COST)); - heroDescription = std::make_shared("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - heroesForHire = std::make_shared(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]); - - rumor = std::make_shared(LOCPLINT->cb->getTavernRumor(tavernObj), Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - cancel = std::make_shared(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); - recruit = std::make_shared(Point(272, 355), AnimationPath::builtin("TPTAV01.DEF"), CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); - thiefGuild = std::make_shared(Point(22, 428), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); - - if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold - { - recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero - recruit->block(true); - } - else if(LOCPLINT->cb->howManyHeroes(true) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP)) - { - //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); - recruit->block(true); - } - else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) - { - //Cannot recruit. You already have %d Heroes. - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); - recruit->block(true); - } - else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero) - { - recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. - recruit->block(true); - } - else - { - if(selected == -1) - recruit->block(true); - } - if(LOCPLINT->castleInt) - CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); - else if(const auto * townObj = dynamic_cast(TavernObj)) - CCS->videoh->open(townObj->town->clientInfo.tavernVideo); - else - CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); -} - -void CTavernWindow::recruitb() -{ - const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; - const CGObjectInstance *obj = tavernObj; - - LOCPLINT->cb->recruitHero(obj, toBuy); - close(); -} - -void CTavernWindow::thievesguildb() -{ - GH.windows().createAndPushWindow(tavernObj); -} - -void CTavernWindow::close() -{ - if (onWindowClosed) - onWindowClosed(); - - CStatusbarWindow::close(); -} - -CTavernWindow::~CTavernWindow() -{ - CCS->videoh->close(); -} - -void CTavernWindow::show(Canvas & to) -{ - CWindowObject::show(to); - - if(selected >= 0) - { - auto sel = selected ? h2 : h1; - - if(selected != oldSelected) - { - // Selected hero just changed. Update RECRUIT button hover text if recruitment is allowed. - oldSelected = selected; - - heroDescription->setText(sel->description); - - //Recruit %s the %s - if (!recruit->isBlocked()) - recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated())); - - } - - to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2); - } - - CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false); -} - -void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition) -{ - if(h) - *_sel = _id; -} - -void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition) -{ - if(h) - GH.windows().createAndPushWindow(std::make_shared(h)); -} - -CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H) - : CIntObject(LCLICK | SHOW_POPUP | HOVER), - h(H), _sel(&sel), _id(id) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - h = H; - pos.x += x; - pos.y += y; - pos.w = 58; - pos.h = 64; - - if(H) - { - hoverName = CGI->generaltexth->tavernInfo[4]; - boost::algorithm::replace_first(hoverName,"%s",H->getNameTranslated()); - - int artifs = (int)h->artifactsWorn.size() + (int)h->artifactsInBackpack.size(); - for(int i=13; i<=17; i++) //war machines and spellbook don't count - if(vstd::contains(h->artifactsWorn, ArtifactPosition(i))) - artifs--; - - description = CGI->generaltexth->allTexts[215]; - boost::algorithm::replace_first(description, "%s", h->getNameTranslated()); - boost::algorithm::replace_first(description, "%d", std::to_string(h->level)); - boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); - boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); - - portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->getIconIndex()); - } -} - -void CTavernWindow::HeroPortrait::hover(bool on) -{ - //Hoverable::hover(on); - if(on) - GH.statusbar()->write(hoverName); - else - GH.statusbar()->clear(); -} - -static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange"; -static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE"; - -static bool isQuickExchangeLayoutAvailable() -{ - return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG)); -} - -CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) - : CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")), - controller(hero1, hero2), - moveStackLeftButtons(), - moveStackRightButtons() -{ - const bool qeLayout = isQuickExchangeLayoutAvailable(); - - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - heroInst[0] = LOCPLINT->cb->getHero(hero1); - heroInst[1] = LOCPLINT->cb->getHero(hero2); - - auto genTitle = [](const CGHeroInstance * h) - { - boost::format fmt(CGI->generaltexth->allTexts[138]); - fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated(); - return boost::str(fmt); - }; - - titles[0] = std::make_shared(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0])); - titles[1] = std::make_shared(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1])); - - auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32")); - PSKIL32->preload(); - - auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32")); - - for(int g = 0; g < 4; ++g) - { - if (qeLayout) - primSkillImages.push_back(std::make_shared(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22))); - else - primSkillImages.push_back(std::make_shared(PSKIL32, g, 0, 385, 19 + 36 * g)); - } - - for(int leftRight : {0, 1}) - { - const CGHeroInstance * hero = heroInst.at(leftRight); - - for(int m=0; m(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE)); - - - for(int m=0; m < hero->secSkills.size(); ++m) - secSkillIcons[leftRight].push_back(std::make_shared(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88)); - - specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); - - expImages[leftRight] = std::make_shared(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45); - expValues[leftRight] = std::make_shared(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - manaImages[leftRight] = std::make_shared(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45); - manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - } - - artifs[0] = std::make_shared(Point(-334, 150)); - artifs[0]->setHero(heroInst[0]); - artifs[1] = std::make_shared(Point(98, 150)); - artifs[1]->setHero(heroInst[1]); - - addSetAndCallbacks(artifs[0]); - addSetAndCallbacks(artifs[1]); - - for(int g=0; g<4; ++g) - { - primSkillAreas.push_back(std::make_shared()); - if (qeLayout) - primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22)); - else - primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); - primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; - primSkillAreas[g]->type = g; - primSkillAreas[g]->bonusValue = 0; - primSkillAreas[g]->baseType = 0; - primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; - boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); - } - - //heroes related thing - for(int b=0; b < heroInst.size(); b++) - { - const CGHeroInstance * hero = heroInst.at(b); - - //secondary skill's clickable areas - for(int g=0; gsecSkills.size(); ++g) - { - int skill = hero->secSkills[g].first, - level = hero->secSkills[g].second; // <1, 3> - secSkillAreas[b].push_back(std::make_shared()); - secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) ); - secSkillAreas[b][g]->baseType = 1; - - secSkillAreas[b][g]->type = skill; - secSkillAreas[b][g]->bonusValue = level; - secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); - - secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21]; - boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]); - boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated()); - } - - heroAreas[b] = std::make_shared(257 + 228 * b, 13, hero); - heroAreas[b]->addClickCallback([this, hero]() -> void - { - if(getPickedArtifact() == nullptr) - LOCPLINT->openHeroWindow(hero); - }); - - specialtyAreas[b] = std::make_shared(); - specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); - specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27]; - specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated(); - - experienceAreas[b] = std::make_shared(); - experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); - experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9]; - experienceAreas[b]->text = CGI->generaltexth->allTexts[2]; - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level)); - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1))); - boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp)); - - spellPointsAreas[b] = std::make_shared(); - spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); - spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22]; - spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205]; - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated()); - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana)); - boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit())); - - morale[b] = std::make_shared(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true); - luck[b] = std::make_shared(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true); - } - - quit = std::make_shared(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); - if(queryID.getNum() > 0) - quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); }); - - questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); - questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); - - Rect barRect(5, 578, 725, 18); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), barRect, 5, 578)); - - //garrison interface - - garr = std::make_shared(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true); - auto splitButtonCallback = [&](){ garr->splitClick(); }; - garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); - - if(qeLayout) - { - auto moveArtifacts = [](const std::function moveRoutine) -> void - { - bool moveEquipped = true; - bool moveBackpack = true; - - if(GH.isKeyboardCtrlDown()) - moveBackpack = false; - else if(GH.isKeyboardShiftDown()) - moveEquipped = false; - moveRoutine(moveEquipped, moveBackpack); - }; - - auto moveArmy = [this](const bool leftToRight) -> void - { - std::optional slotId = std::nullopt; - if(auto slot = getSelectedSlotID()) - slotId = slot->getSlot(); - controller.moveArmy(leftToRight, slotId); - }; - - auto openBackpack = [this](const CGHeroInstance * hero) -> void - { - GH.windows().createAndPushWindow(hero); - for(auto artSet : artSets) - GH.windows().topWindow()->addSet(artSet); - }; - - moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - std::bind(moveArmy, true)); - exchangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), - std::bind(&CExchangeController::swapArmy, &controller)); - moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - std::bind(moveArmy, false)); - moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), - std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);})); - exchangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), - std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); - moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), - std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); - backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), - std::bind(openBackpack, heroInst[0])); - backpackButtonLeft->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); - backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), - std::bind(openBackpack, heroInst[1])); - backpackButtonRight->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); - - auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID(); - auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID(); - moveAllGarrButtonLeft->block(leftHeroBlock); - exchangeGarrButton->block(leftHeroBlock || rightHeroBlock); - moveAllGarrButtonRight->block(rightHeroBlock); - moveArtifactsButtonLeft->block(leftHeroBlock); - exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock); - moveArtifactsButtonRight->block(rightHeroBlock); - backpackButtonLeft->block(leftHeroBlock); - backpackButtonRight->block(rightHeroBlock); - - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - moveStackLeftButtons.push_back( - std::make_shared( - Point(484 + 35 * i, 154), - AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); - moveStackLeftButtons.back()->block(leftHeroBlock); - - moveStackRightButtons.push_back( - std::make_shared( - Point(66 + 35 * i, 154), - AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), - CButton::tooltip(CGI->generaltexth->qeModCommands[1]), - std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); - moveStackLeftButtons.back()->block(rightHeroBlock); - } - } - - updateWidgets(); -} - -const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const -{ - return garr->getSelection(); -} - -void CExchangeWindow::updateGarrisons() -{ - garr->recreateSlots(); - - updateWidgets(); -} - -void CExchangeWindow::questlog(int whichHero) -{ - CCS->curh->dragAndDropCursor(nullptr); - LOCPLINT->showQuestLog(); -} - -void CExchangeWindow::updateWidgets() -{ - for(size_t leftRight : {0, 1}) - { - const CGHeroInstance * hero = heroInst.at(leftRight); - - for(int m=0; mgetPrimSkillLevel(static_cast(m)); - primSkillValues[leftRight][m]->setText(std::to_string(value)); - } - - for(int m=0; m < hero->secSkills.size(); ++m) - { - int id = hero->secSkills[m].first; - int level = hero->secSkills[m].second; - - secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level); - } - - expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3)); - manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3)); - - morale[leftRight]->set(hero); - luck[leftRight]->set(hero); - } -} - -CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSHIP")) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - bgWater = std::make_shared(ImagePath::builtin("TPSHIPBK"), 100, 69); - - auto handler = CGI->objtypeh->getHandlerFor(Obj::BOAT, boatType); - - auto boatConstructor = std::dynamic_pointer_cast(handler); - - assert(boatConstructor); - - if (boatConstructor) - { - AnimationPath boatFilename = boatConstructor->getBoatAnimationName(); - - Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2); - bgShip = std::make_shared(boatFilename, 0, 7, 120, 96, 0); - bgShip->center(waterCenter); - } - - // Create resource icons and costs. - std::string goldValue = std::to_string(cost[EGameResID::GOLD]); - std::string woodValue = std::to_string(cost[EGameResID::WOOD]); - - goldCost = std::make_shared(118, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, goldValue); - woodCost = std::make_shared(212, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, woodValue); - - goldPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 100, 244); - woodPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::WOOD), 0, 196, 244); - - quit = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); - build = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30"), CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); - build->addCallback(onBuy); - - for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) - { - if(cost[i] > LOCPLINT->cb->getResourceAmount(i)) - { - build->block(true); - break; - } - } - - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - title = std::make_shared(164, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[13]); - costLabel = std::make_shared(164, 220, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[14]); -} - -void CTransformerWindow::CItem::move() -{ - if(left) - moveBy(Point(289, 0)); - else - moveBy(Point(-289, 0)); - left = !left; -} - -void CTransformerWindow::CItem::clickPressed(const Point & cursorPosition) -{ - move(); - parent->redraw(); -} - -void CTransformerWindow::CItem::update() -{ - icon->setFrame(parent->army->getCreature(SlotID(id))->getId() + 2); -} - -CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id_) - : CIntObject(LCLICK), - id(id_), - size(size_), - parent(parent_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - left = true; - pos.w = 58; - pos.h = 64; - - pos.x += 45 + (id%3)*83 + id/6*83; - pos.y += 109 + (id/3)*98; - icon = std::make_shared(AnimationPath::builtin("TWCRPORT"), parent->army->getCreature(SlotID(id))->getId() + 2); - count = std::make_shared(28, 76,FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(size)); -} - -void CTransformerWindow::makeDeal() -{ - for(auto & elem : items) - { - if(!elem->left) - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, elem->id, 0, 0, hero); - } -} - -void CTransformerWindow::addAll() -{ - for(auto & elem : items) - { - if(elem->left) - elem->move(); - } - redraw(); -} - -void CTransformerWindow::updateGarrisons() -{ - for(auto & item : items) - item->update(); -} - -CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), - hero(_hero), - onWindowClosed(onWindowClosed), - market(_market) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - if(hero) - army = hero; - else - army = dynamic_cast(market); //for town only - - if(army) - { - for(int i = 0; i < GameConstants::ARMY_SIZE; i++) - { - if(army->getCreature(SlotID(i))) - items.push_back(std::make_shared(this, army->getStackCount(SlotID(i)), i)); - } - } - - all = std::make_shared(Point(146, 416), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); - convert = std::make_shared(Point(269, 416), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); - cancel = std::make_shared(Point(392, 416), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - titleLeft = std::make_shared(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area - titleRight = std::make_shared(153+295, 29, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[486]);//transformer - helpLeft = std::make_shared(CGI->generaltexth->allTexts[487], Rect(26, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//move creatures to create skeletons - helpRight = std::make_shared(CGI->generaltexth->allTexts[488], Rect(320, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//creatures here will become skeletons -} - -void CTransformerWindow::close() -{ - if (onWindowClosed) - onWindowClosed(); - - CStatusbarWindow::close(); -} - -CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int Y) - : CIntObject(LCLICK | SHOW_POPUP | HOVER), - ID(_ID), - parent(_parent) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.x += X; - pos.y += Y; - - topBar = std::make_shared(parent->bars, 0, 0, -28, -22); - bottomBar = std::make_shared(parent->bars, 0, 0, -28, 48); - - icon = std::make_shared(AnimationPath::builtin("SECSKILL"), _ID * 3 + 3, 0); - - name = std::make_shared(22, -13, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(ID)->getNameTranslated()); - level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); - - pos.h = icon->pos.h; - pos.w = icon->pos.w; -} - -void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) -{ - if(state() == 2) - GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); -} - -void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) -{ - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(CComponent::secskill, ID, 1)); -} - -void CUniversityWindow::CItem::hover(bool on) -{ - if(on) - GH.statusbar()->write(CGI->skillh->getByIndex(ID)->getNameTranslated()); - else - GH.statusbar()->clear(); -} - -int CUniversityWindow::CItem::state() -{ - if(parent->hero->getSecSkillLevel(SecondarySkill(ID)))//hero know this skill - return 1; - if(!parent->hero->canLearnSkill(SecondarySkill(ID)))//can't learn more skills - return 0; - return 2; -} - -void CUniversityWindow::CItem::showAll(Canvas & to) -{ - //TODO: update when state actually changes - auto stateIndex = state(); - topBar->setFrame(stateIndex); - bottomBar->setFrame(stateIndex); - - CIntObject::showAll(to); -} - -CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), - hero(_hero), - onWindowClosed(onWindowClosed), - market(_market) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - bars = GH.renderHandler().createAnimation(); - bars->setCustom("UNIVRED", 0, 0); - bars->setCustom("UNIVGOLD", 1, 0); - bars->setCustom("UNIVGREN", 2, 0); - bars->preload(); - - std::string titleStr = CGI->generaltexth->allTexts[602]; - std::string speechStr = CGI->generaltexth->allTexts[603]; - - if(auto town = dynamic_cast(_market)) - { - auto faction = town->town->faction->getId(); - auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid; - titlePic = std::make_shared((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid); - } - else if(auto uni = dynamic_cast(_market); uni->appearance) - { - titlePic = std::make_shared(uni->appearance->animationFile, 0); - titleStr = uni->title; - speechStr = uni->speech; - } - else - { - titlePic = std::make_shared(ImagePath::builtin("UNIVBLDG")); - } - - titlePic->center(Point(232 + pos.x, 76 + pos.y)); - - clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); - - std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); - - for(int i=0; i(this, goods[i], 54+i*104, 234)); - - cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); -} - -void CUniversityWindow::close() -{ - if (onWindowClosed) - onWindowClosed(); - - CStatusbarWindow::close(); -} - -void CUniversityWindow::makeDeal(int skill) -{ - LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); -} - - -CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), - owner(owner_) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - std::string text = CGI->generaltexth->allTexts[608]; - boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); - boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - boost::replace_first(text, "%d", "2000"); - - clerkSpeech = std::make_shared(text, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - - name = std::make_shared(230, 37, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - icon = std::make_shared(AnimationPath::builtin("SECSKILL"), SKILL*3+3, 0, 211, 51); - level = std::make_shared(230, 107, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[1]); - - costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 210, 210); - cost = std::make_shared(230, 267, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "2000"); - - std::string hoverText = CGI->generaltexth->allTexts[609]; - boost::replace_first(hoverText, "%s", CGI->generaltexth->levels[0]+ " " + CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - - text = CGI->generaltexth->zelp[633].second; - boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); - boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); - boost::replace_first(text, "%d", "2000"); - - confirm = std::make_shared(Point(148, 299), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); - confirm->block(!available); - - cancel = std::make_shared(Point(252,299), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); -} - -void CUnivConfirmWindow::makeDeal(int skill) -{ - owner->makeDeal(skill); - close(); -} - -CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GARRISON")) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - garr = std::make_shared(Point(92, 127), 4, Point(0,96), up, down, removableUnits); - { - auto split = std::make_shared(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } ); - garr->addSplitBtn(split); - } - quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); - - std::string titleText; - if(down->tempOwner == up->tempOwner) - { - titleText = CGI->generaltexth->allTexts[709]; - } - else - { - //assume that this is joining monsters dialog - if(up->Slots().size() > 0) - { - titleText = CGI->generaltexth->allTexts[35]; - boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->type->getNamePluralTranslated()); - } - else - { - logGlobal->error("Invalid armed instance for garrison window."); - } - } - title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); - - banner = std::make_shared(AnimationPath::builtin("CREST58"), up->getOwner().getNum(), 0, 28, 124); - portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->getIconIndex(), 0, 29, 222); -} - -void CGarrisonWindow::updateGarrisons() -{ - garr->recreateSlots(); -} - -CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) - : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), - fort(object), - hero(visitor) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - title = std::make_shared(325, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, fort->getObjectName()); - - heroPic = std::make_shared(30, 60, hero); - - for(int i=0; i < resCount; i++) - { - totalIcons[i] = std::make_shared(AnimationPath::builtin("SMALRES"), i, 0, 104 + 76 * i, 237); - totalLabels[i] = std::make_shared(166 + 76 * i, 253, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); - } - - for(int i = 0; i < slotsCount; i++) - { - upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath(), CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); - for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) - upgrade[i]->addImage(AnimationPath::builtin(image)); - - for(int j : {0,1}) - { - slotIcons[i][j] = std::make_shared(AnimationPath::builtin("SMALRES"), 0, 0, 104 + 76 * i, 128 + 20 * j); - slotLabels[i][j] = std::make_shared(168 + 76 * i, 144 + 20 * j, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); - } - } - - upgradeAll = std::make_shared(Point(30, 231), AnimationPath(), CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); - for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) - upgradeAll->addImage(AnimationPath::builtin(image)); - - quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); - statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - garr = std::make_shared(Point(108, 60), 18, Point(), hero, nullptr); - updateGarrisons(); -} - -void CHillFortWindow::updateGarrisons() -{ - std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade - - TResources totalSum; // totalSum[resource ID] = value - - for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); - if(info.newID.size())//we have upgrades here - update costs - { - costs[i] = info.cost[0] * hero->getStackCount(SlotID(i)); - totalSum += costs[i]; - } - } - - currState[i] = newState; - upgrade[i]->setIndex(currState[i] == -1 ? 0 : currState[i]); - upgrade[i]->block(currState[i] == -1); - upgrade[i]->addHoverText(CButton::NORMAL, getTextForSlot(SlotID(i))); - } - - //"Upgrade all" slot - int newState = 2; - { - TResources myRes = LOCPLINT->cb->getResourceAmount(); - - bool allUpgraded = true;//All creatures are upgraded? - for(int i=0; isetIndex(newState); - - garr->recreateSlots(); - - for(int i = 0; i < slotsCount; i++) - { - //hide all first - for(int j : {0,1}) - { - slotIcons[i][j]->visible = false; - slotLabels[i][j]->setText(""); - } - //if can upgrade or can not afford, draw cost - if(currState[i] == 0 || currState[i] == 2) - { - if(costs[i].nonZero()) - { - //reverse iterator is used to display gold as first element - int j = 0; - for(int res = (int)costs[i].size()-1; (res >= 0) && (j < 2); res--) - { - int val = costs[i][res]; - if(!val) - continue; - - slotIcons[i][j]->visible = true; - slotIcons[i][j]->setFrame(res); - - slotLabels[i][j]->setText(std::to_string(val)); - j++; - } - } - else//free upgrade - print gold image and "Free" text - { - slotIcons[i][0]->visible = true; - slotIcons[i][0]->setFrame(GameResID(EGameResID::GOLD)); - slotLabels[i][0]->setText(CGI->generaltexth->allTexts[344]); - } - } - } - - for(int i = 0; i < resCount; i++) - { - if(totalSum[i] == 0) - { - totalIcons[i]->visible = false; - totalLabels[i]->setText(""); - } - else - { - totalIcons[i]->visible = true; - totalLabels[i]->setText(std::to_string(totalSum[i])); - } - } -} - -void CHillFortWindow::makeDeal(SlotID slot) -{ - assert(slot.getNum()>=0); - int offset = (slot.getNum() == slotsCount)?2:0; - switch(currState[slot.getNum()]) - { - case 0: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314 + offset], std::vector>(), soundBase::sound_todo); - break; - case 1: - LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[313 + offset], std::vector>(), soundBase::sound_todo); - break; - case 2: - for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); - LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID[0]); - } - } - break; - } -} - -std::string CHillFortWindow::getTextForSlot(SlotID slot) -{ - if(!hero->getCreature(slot))//we don`t have creature here - return ""; - - std::string str = CGI->generaltexth->allTexts[318]; - int amount = hero->getStackCount(slot); - if(amount == 1) - boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNameSingularTranslated()); - else - boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNamePluralTranslated()); - - return str; -} - -int CHillFortWindow::getState(SlotID slot) -{ - TResources myRes = LOCPLINT->cb->getResourceAmount(); - - if(hero->slotEmpty(slot))//no creature here - return -1; - - UpgradeInfo info; - LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); - if(!info.newID.size())//already upgraded - return 1; - - if(!(info.cost[0] * hero->getStackCount(slot)).canBeAfforded(myRes)) - return 0; - - return 2;//can upgrade -} - -CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): - CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin("TpRank")), - owner(_owner) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - SThievesGuildInfo tgi; //info to be displayed - LOCPLINT->cb->getThievesGuildInfo(tgi, owner); - - exitb = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); - statusbar = CGStatusBar::create(3, 555, ImagePath::builtin("TStatBar.bmp"), 742); - - resdatabar = std::make_shared(); - resdatabar->moveBy(pos.topLeft(), true); - - //data for information table: - // fields[row][column] = list of id's of players for this box - static std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = - { &SThievesGuildInfo::numOfTowns, &SThievesGuildInfo::numOfHeroes, &SThievesGuildInfo::gold, - &SThievesGuildInfo::woodOre, &SThievesGuildInfo::mercSulfCrystGems, &SThievesGuildInfo::obelisks, - &SThievesGuildInfo::artifacts, &SThievesGuildInfo::army, &SThievesGuildInfo::income }; - - for(int g=0; g<12; ++g) - { - int posY[] = {400, 460, 510}; - int y; - if(g < 9) - y = 52 + 32*g; - else - y = posY[g-9]; - - std::string text = CGI->generaltexth->jktexts[24+g]; - boost::algorithm::trim_if(text,boost::algorithm::is_any_of("\"")); - rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); - } - - auto PRSTRIPS = GH.renderHandler().loadAnimation(AnimationPath::builtin("PRSTRIPS")); - PRSTRIPS->preload(); - - for(int g=1; g(PRSTRIPS, g-1, 0, 250 + 66*g, 7)); - - for(int g=0; g(283 + 66*g, 24, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[16+g])); - - auto itgflags = GH.renderHandler().loadAnimation(AnimationPath::builtin("itgflags")); - itgflags->preload(); - - //printing flags - for(int g = 0; g < std::size(fields); ++g) //by lines - { - for(int b=0; b<(tgi .* fields[g]).size(); ++b) //by places (1st, 2nd, ...) - { - std::vector &players = (tgi .* fields[g])[b]; //get players with this place in this line - - //position of box - int xpos = 259 + 66 * b; - int ypos = 41 + 32 * g; - - size_t rowLength[2]; //size of each row - rowLength[0] = std::min(players.size(), 4); - rowLength[1] = players.size() - rowLength[0]; - - for(size_t j=0; j < 2; j++) - { - // origin of this row | offset for 2nd row| shift right for short rows - //if we have 2 rows, start either from mid or beginning (depending on count), otherwise center the flags - int rowStartX = xpos + (j ? 6 + ((int)rowLength[j] < 3 ? 12 : 0) : 24 - 6 * (int)rowLength[j]); - int rowStartY = ypos + (j ? 4 : 0); - - for(size_t i=0; i < rowLength[j]; i++) - cells.push_back(std::make_shared(itgflags, players[i + j*4].getNum(), 0, rowStartX + (int)i*12, rowStartY)); - } - } - } - - static const std::string colorToBox[] = {"PRRED.BMP", "PRBLUE.BMP", "PRTAN.BMP", "PRGREEN.BMP", "PRORANGE.BMP", "PRPURPLE.BMP", "PRTEAL.BMP", "PRROSE.bmp"}; - - //printing best hero - int counter = 0; - for(auto & iter : tgi.colorToBestHero) - { - banners.push_back(std::make_shared(ImagePath::builtin(colorToBox[iter.first.getNum()]), 253 + 66 * counter, 334)); - if(iter.second.portraitSource.isValid()) - { - bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.getIconIndex(), 0, 260 + 66 * counter, 360)); - //TODO: r-click info: - // - r-click on hero - // - r-click on primary skill label - if(iter.second.details) - { - primSkillHeaders.push_back(std::make_shared(CGI->generaltexth->allTexts[184], Rect(260 + 66*counter, 396, 52, 64), - 0, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE)); - - for(int i=0; iprimskills.size(); ++i) - { - primSkillValues.push_back(std::make_shared(310 + 66 * counter, 407 + 11*i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, - std::to_string(iter.second.details->primskills[i]))); - } - } - } - counter++; - } - - //printing best creature - counter = 0; - for(auto & it : tgi.bestCreature) - { - if(it.second >= 0) - bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); - counter++; - } - - //printing personality - counter = 0; - for(auto & it : tgi.personality) - { - std::string text; - if(it.second == EAiTactic::NONE) - { - text = CGI->generaltexth->arraytxt[172]; - } - else if(it.second != EAiTactic::RANDOM) - { - text = CGI->generaltexth->arraytxt[168 + static_cast(it.second)]; - } - - personalities.push_back(std::make_shared(283 + 66*counter, 459, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text)); - - counter++; - } -} - -CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text) - : CIntObject(LCLICK | DOUBLECLICK), - parent(_parent), - index(_id) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - border = std::make_shared(ImagePath::builtin("TPGATES")); - pos = border->pos; - - setRedrawParent(true); - - text = std::make_shared(pos.w/2, pos.h/2, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _text); - select(index == parent->selected); -} - -void CObjectListWindow::CItem::select(bool on) -{ - ui8 mask = UPDATE | SHOWALL; - if(on) - border->recActions |= mask; - else - border->recActions &= ~mask; - redraw();//??? -} - -void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition) -{ - parent->changeSelection(index); -} - -void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) -{ - parent->elementSelected(); -} - -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), - onSelect(Callback), - selected(initialSelection) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - items.reserve(_items.size()); - - for(int id : _items) - { - items.push_back(std::make_pair(id, LOCPLINT->cb->getObjInstance(ObjectInstanceID(id))->getObjectName())); - } - - init(titleWidget_, _title, _descr); -} - -CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) - : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), - onSelect(Callback), - selected(initialSelection) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - items.reserve(_items.size()); - - for(size_t i=0; i<_items.size(); i++) - items.push_back(std::make_pair(int(i), _items[i])); - - init(titleWidget_, _title, _descr); -} - -void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr) -{ - titleWidget = titleWidget_; - - title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); - descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); - exit = std::make_shared( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); - - if(titleWidget) - { - addChild(titleWidget.get()); - titleWidget->recActions = 255-DISPOSE; - titleWidget->pos.x = pos.w/2 + pos.x - titleWidget->pos.w/2; - titleWidget->pos.y =75 + pos.y - titleWidget->pos.h/2; - } - list = std::make_shared(std::bind(&CObjectListWindow::genItem, this, _1), - Point(14, 151), Point(0, 25), 9, items.size(), 0, 1, Rect(262, -32, 256, 256) ); - list->setRedrawParent(true); - - ok = std::make_shared(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); - ok->block(!list->size()); -} - -std::shared_ptr CObjectListWindow::genItem(size_t index) -{ - if(index < items.size()) - return std::make_shared(this, index, items[index].second); - return std::shared_ptr(); -} - -void CObjectListWindow::elementSelected() -{ - std::function toCall = onSelect;//save - int where = items[selected].first; //required variables - close();//then destroy window - toCall(where);//and send selected object -} - -void CObjectListWindow::exitPressed() -{ - std::function toCall = onExit;//save - close();//then destroy window - if(toCall) - toCall(); -} - -void CObjectListWindow::changeSelection(size_t which) -{ - ok->block(false); - if(selected == which) - return; - - for(std::shared_ptr element : list->getItems()) - { - CItem * item = dynamic_cast(element.get()); - if(item) - { - if(item->index == selected) - item->select(false); - - if(item->index == which) - item->select(true); - } - } - selected = which; -} - -void CObjectListWindow::keyPressed (EShortcut key) -{ - int sel = static_cast(selected); - - switch(key) - { - break; case EShortcut::MOVE_UP: - sel -=1; - - break; case EShortcut::MOVE_DOWN: - sel +=1; - - break; case EShortcut::MOVE_PAGE_UP: - sel -=9; - - break; case EShortcut::MOVE_PAGE_DOWN: - sel +=9; - - break; case EShortcut::MOVE_FIRST: - sel = 0; - - break; case EShortcut::MOVE_LAST: - sel = static_cast(items.size()); - - break; default: - return; - } - - vstd::abetween(sel, 0, items.size()-1); - list->scrollTo(sel); - changeSelection(sel); -} +/* + * GUIClasses.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 "GUIClasses.h" + +#include "CCastleInterface.h" +#include "CCreatureWindow.h" +#include "CHeroBackpackWindow.h" +#include "CHeroWindow.h" +#include "InfoWindows.h" + +#include "../CGameInfo.h" +#include "../CMusicHandler.h" +#include "../CPlayerInterface.h" +#include "../CVideoHandler.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../gui/Shortcut.h" +#include "../gui/WindowHandler.h" + +#include "../widgets/CComponent.h" +#include "../widgets/CGarrisonInt.h" +#include "../widgets/CreatureCostBox.h" +#include "../widgets/Buttons.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../widgets/ObjectLists.h" + +#include "../render/Canvas.h" +#include "../render/CAnimation.h" +#include "../render/IRenderHandler.h" + +#include "../../CCallback.h" + +#include "../lib/mapObjectConstructors/CObjectClassesHandler.h" +#include "../lib/mapObjectConstructors/CommonConstructors.h" +#include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/mapObjects/CGMarket.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/mapObjects/ObjectTemplate.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/gameState/SThievesGuildInfo.h" +#include "../lib/CGeneralTextHandler.h" +#include "../lib/CHeroHandler.h" +#include "../lib/GameSettings.h" +#include "../lib/CondSh.h" +#include "../lib/CSkillHandler.h" +#include "../lib/filesystem/Filesystem.h" +#include "../lib/TextOperations.h" + +CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount) + : CIntObject(LCLICK | SHOW_POPUP), + parent(window), + selected(false), + creature(crea), + amount(totalAmount) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + animation = std::make_shared(1, 1, creature, true, true); + // 1 + 1 px for borders + pos.w = animation->pos.w + 2; + pos.h = animation->pos.h + 2; +} + +void CRecruitmentWindow::CCreatureCard::select(bool on) +{ + selected = on; + redraw(); +} + +void CRecruitmentWindow::CCreatureCard::clickPressed(const Point & cursorPosition) +{ + parent->select(this->shared_from_this()); +} + +void CRecruitmentWindow::CCreatureCard::showPopupWindow(const Point & cursorPosition) +{ + GH.windows().createAndPushWindow(creature, true); +} + +void CRecruitmentWindow::CCreatureCard::showAll(Canvas & to) +{ + CIntObject::showAll(to); + if(selected) + to.drawBorder(pos, Colors::RED); + else + to.drawBorder(pos, Colors::YELLOW); +} + +void CRecruitmentWindow::select(std::shared_ptr card) +{ + if(card == selected) + return; + + if(selected) + selected->select(false); + + selected = card; + + if(selected) + selected->select(true); + + if(card) + { + si32 maxAmount = card->creature->maxAmount(LOCPLINT->cb->getResourceAmount()); + + vstd::amin(maxAmount, card->amount); + + slider->setAmount(maxAmount); + + if(slider->getValue() != maxAmount) + slider->scrollTo(maxAmount); + else // if slider already at 0 - emulate call to sliderMoved() + sliderMoved(maxAmount); + + costPerTroopValue->createItems(card->creature->getFullRecruitCost()); + totalCostValue->createItems(card->creature->getFullRecruitCost()); + + costPerTroopValue->set(card->creature->getFullRecruitCost()); + totalCostValue->set(card->creature->getFullRecruitCost() * maxAmount); + + //Recruit %s + title->setText(boost::str(boost::format(CGI->generaltexth->tcommands[21]) % card->creature->getNamePluralTranslated())); + + maxButton->block(maxAmount == 0); + slider->block(maxAmount == 0); + } +} + +void CRecruitmentWindow::close() +{ + if (onClose) + onClose(); + CStatusbarWindow::close(); +} + +void CRecruitmentWindow::buy() +{ + CreatureID crid = selected->creature->getId(); + SlotID dstslot = dst->getSlotFor(crid); + + if(!dstslot.validSlot() && (selected->creature->warMachine == ArtifactID::NONE)) //no available slot + { + std::pair toMerge; + bool allowMerge = CGI->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED); + + if (allowMerge && dst->mergableStacks(toMerge)) + { + LOCPLINT->cb->mergeStacks( dst, dst, toMerge.first, toMerge.second); + } + else + { + std::string txt; + if(dst->ID == Obj::HERO) + { + txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. + boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); + } + else + { + txt = CGI->generaltexth->allTexts[17]; //There is no room in the garrison for this army. + } + + LOCPLINT->showInfoDialog(txt); + return; + } + } + + onRecruit(crid, slider->getValue()); + if(level >= 0) + close(); +} + +void CRecruitmentWindow::showAll(Canvas & to) +{ + CWindowObject::showAll(to); + + Rect(172, 222, 67, 42) + pos.topLeft(); + + // recruit\total values + to.drawBorder(Rect(172, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); + to.drawBorder(Rect(246, 222, 67, 42) + pos.topLeft(), Colors::YELLOW); + + //cost boxes + to.drawBorder(Rect( 64, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); + to.drawBorder(Rect(322, 222, 99, 76) + pos.topLeft(), Colors::YELLOW); + + //buttons borders + to.drawBorder(Rect(133, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); + to.drawBorder(Rect(211, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); + to.drawBorder(Rect(289, 312, 66, 34) + pos.topLeft(), Colors::METALLIC_GOLD); +} + +CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset): + CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPRCRT")), + onRecruit(Recruit), + onClose(onClose), + level(Level), + dst(Dst), + selected(nullptr), + dwelling(Dwelling) +{ + moveBy(Point(0, y_offset)); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + slider = std::make_shared(Point(176, 279), 135, std::bind(&CRecruitmentWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); + + maxButton = std::make_shared(Point(134, 313), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX); + buyButton = std::make_shared(Point(212, 313), AnimationPath::builtin("IBY6432.DEF"), CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT); + cancelButton = std::make_shared(Point(290, 313), AnimationPath::builtin("ICN6432.DEF"), CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL); + + title = std::make_shared(243, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW); + availableValue = std::make_shared(205, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + toRecruitValue = std::make_shared(279, 253, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + costPerTroopValue = std::make_shared(Rect(65, 222, 97, 74), CGI->generaltexth->allTexts[346]); + totalCostValue = std::make_shared(Rect(323, 222, 97, 74), CGI->generaltexth->allTexts[466]); + + availableTitle = std::make_shared(205, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[465]); + toRecruitTitle = std::make_shared(279, 233, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[16]); + + availableCreaturesChanged(); +} + +void CRecruitmentWindow::availableCreaturesChanged() +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); + + size_t selectedIndex = 0; + + if(!cards.empty() && selected) // find position of selected item + selectedIndex = std::find(cards.begin(), cards.end(), selected) - cards.begin(); + + select(nullptr); + + cards.clear(); + + for(int i=0; icreatures.size(); i++) + { + //find appropriate level + if(level >= 0 && i != level) + continue; + + int amount = dwelling->creatures[i].first; + + //create new cards + for(auto & creature : boost::adaptors::reverse(dwelling->creatures[i].second)) + cards.push_back(std::make_shared(this, CGI->creh->objects[creature], amount)); + } + + const int creatureWidth = 102; + + //normal distance between cards - 18px + int requiredSpace = 18; + //maximum distance we can use without reaching window borders + int availableSpace = pos.w - 50 - creatureWidth * (int)cards.size(); + + if (cards.size() > 1) // avoid division by zero + availableSpace /= (int)cards.size() - 1; + else + availableSpace = 0; + + assert(availableSpace >= 0); + + const int spaceBetween = std::min(requiredSpace, availableSpace); + const int totalCreatureWidth = spaceBetween + creatureWidth; + + //now we know total amount of cards and can move them to correct position + int curx = pos.w / 2 - (creatureWidth*(int)cards.size()/2) - (spaceBetween*((int)cards.size()-1)/2); + for(auto & card : cards) + { + card->moveBy(Point(curx, 64)); + curx += totalCreatureWidth; + } + + //restore selection + select(cards[selectedIndex]); + + if(slider->getValue() == slider->getAmount()) + slider->scrollToMax(); + else // if slider already at 0 - emulate call to sliderMoved() + sliderMoved(slider->getAmount()); +} + +void CRecruitmentWindow::sliderMoved(int to) +{ + if(!selected) + return; + + buyButton->block(!to); + availableValue->setText(std::to_string(selected->amount - to)); + toRecruitValue->setText(std::to_string(to)); + + totalCostValue->set(selected->creature->getFullRecruitCost() * to); +} + +CSplitWindow::CSplitWindow(const CCreature * creature, std::function callback_, int leftMin_, int rightMin_, int leftAmount_, int rightAmount_) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GPUCRDIV")), + callback(callback_), + leftAmount(leftAmount_), + rightAmount(rightAmount_), + leftMin(leftMin_), + rightMin(rightMin_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + int total = leftAmount + rightAmount; + int leftMax = total - rightMin; + int rightMax = total - leftMin; + + ok = std::make_shared(Point(20, 263), AnimationPath::builtin("IOK6432"), CButton::tooltip(), std::bind(&CSplitWindow::apply, this), EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(214, 263), AnimationPath::builtin("ICN6432"), CButton::tooltip(), std::bind(&CSplitWindow::close, this), EShortcut::GLOBAL_CANCEL); + + int sliderPosition = total - leftMin - rightMin; + + leftInput = std::make_shared(Rect(20, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, true)); + rightInput = std::make_shared(Rect(176, 218, 100, 36), FONT_BIG, std::bind(&CSplitWindow::setAmountText, this, _1, false)); + + //add filters to allow only number input + leftInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, leftMin, leftMax); + rightInput->filters += std::bind(&CTextInput::numberFilter, _1, _2, rightMin, rightMax); + + leftInput->setText(std::to_string(leftAmount), false); + rightInput->setText(std::to_string(rightAmount), false); + + animLeft = std::make_shared(20, 54, creature, true, false); + animRight = std::make_shared(177, 54,creature, true, false); + + slider = std::make_shared(Point(21, 194), 257, std::bind(&CSplitWindow::sliderMoved, this, _1), 0, sliderPosition, rightAmount - rightMin, Orientation::HORIZONTAL); + + std::string titleStr = CGI->generaltexth->allTexts[256]; + boost::algorithm::replace_first(titleStr,"%s", creature->getNamePluralTranslated()); + title = std::make_shared(150, 34, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleStr); +} + +void CSplitWindow::setAmountText(std::string text, bool left) +{ + int amount = 0; + if(text.length()) + { + try + { + amount = boost::lexical_cast(text); + } + catch(boost::bad_lexical_cast &) + { + amount = left ? leftAmount : rightAmount; + } + + int total = leftAmount + rightAmount; + if(amount > total) + amount = total; + } + + setAmount(amount, left); + slider->scrollTo(rightAmount - rightMin); +} + +void CSplitWindow::setAmount(int value, bool left) +{ + int total = leftAmount + rightAmount; + leftAmount = left ? value : total - value; + rightAmount = left ? total - value : value; + + leftInput->setText(std::to_string(leftAmount)); + rightInput->setText(std::to_string(rightAmount)); +} + +void CSplitWindow::apply() +{ + callback(leftAmount, rightAmount); + close(); +} + +void CSplitWindow::sliderMoved(int to) +{ + setAmount(rightMin + to, false); +} + +CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std::vector & skills, std::function callback) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("LVLUPBKG")), + cb(callback) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + LOCPLINT->showingDialog->setn(true); + + if(!skills.empty()) + { + std::vector> comps; + for(auto & skill : skills) + { + auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); + comp->onChoose = std::bind(&CLevelWindow::close, this); + comps.push_back(comp); + } + + box = std::make_shared(comps, Rect(75, 300, pos.w - 150, 100)); + } + + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66); + ok = std::make_shared(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT); + + //%s has gained a level. + mainTitle = std::make_shared(192, 33, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[444]) % hero->getNameTranslated())); + + //%s is now a level %d %s. + std::string levelTitleText = CGI->generaltexth->translate("core.genrltxt.445"); + boost::replace_first(levelTitleText, "%s", hero->getNameTranslated()); + boost::replace_first(levelTitleText, "%d", std::to_string(hero->level)); + boost::replace_first(levelTitleText, "%s", hero->type->heroClass->getNameTranslated()); + + levelTitle = std::make_shared(192, 162, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, levelTitleText); + + skillIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), static_cast(pskill), 0, 174, 190); + + skillValue = std::make_shared(192, 253, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->primarySkillNames[static_cast(pskill)] + " +1"); +} + + +CLevelWindow::~CLevelWindow() +{ + //FIXME: call callback if there was nothing to select? + if (box && box->selectedIndex() != -1) + cb(box->selectedIndex()); + + LOCPLINT->showingDialog->setn(false); +} + +CTavernWindow::CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPTAVERN")), + onWindowClosed(onWindowClosed), + tavernObj(TavernObj) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + std::vector h = LOCPLINT->cb->getAvailableHeroes(TavernObj); + if(h.size() < 2) + h.resize(2, nullptr); + + selected = 0; + if(!h[0]) + selected = 1; + if(!h[0] && !h[1]) + selected = -1; + + oldSelected = -1; + + h1 = std::make_shared(selected, 0, 72, 299, h[0]); + h2 = std::make_shared(selected, 1, 162, 299, h[1]); + + title = std::make_shared(197, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[37]); + cost = std::make_shared(320, 328, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(GameConstants::HERO_GOLD_COST)); + heroDescription = std::make_shared("", Rect(30, 373, 233, 35), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + heroesForHire = std::make_shared(145, 283, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[38]); + + rumor = std::make_shared(LOCPLINT->cb->getTavernRumor(tavernObj), Rect(32, 188, 330, 66), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + cancel = std::make_shared(Point(310, 428), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[7]), std::bind(&CTavernWindow::close, this), EShortcut::GLOBAL_CANCEL); + recruit = std::make_shared(Point(272, 355), AnimationPath::builtin("TPTAV01.DEF"), CButton::tooltip(), std::bind(&CTavernWindow::recruitb, this), EShortcut::GLOBAL_ACCEPT); + thiefGuild = std::make_shared(Point(22, 428), AnimationPath::builtin("TPTAV02.DEF"), CButton::tooltip(CGI->generaltexth->tavernInfo[5]), std::bind(&CTavernWindow::thievesguildb, this), EShortcut::ADVENTURE_THIEVES_GUILD); + + if(LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //not enough gold + { + recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[0]); //Cannot afford a Hero + recruit->block(true); + } + else if(LOCPLINT->cb->howManyHeroes(true) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP)) + { + //Cannot recruit. You already have %d Heroes. + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(true))); + recruit->block(true); + } + else if(LOCPLINT->cb->howManyHeroes(false) >= CGI->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)) + { + //Cannot recruit. You already have %d Heroes. + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[1]) % LOCPLINT->cb->howManyHeroes(false))); + recruit->block(true); + } + else if(dynamic_cast(TavernObj) && dynamic_cast(TavernObj)->visitingHero) + { + recruit->addHoverText(CButton::NORMAL, CGI->generaltexth->tavernInfo[2]); //Cannot recruit. You already have a Hero in this town. + recruit->block(true); + } + else + { + if(selected == -1) + recruit->block(true); + } + if(LOCPLINT->castleInt) + CCS->videoh->open(LOCPLINT->castleInt->town->town->clientInfo.tavernVideo); + else if(const auto * townObj = dynamic_cast(TavernObj)) + CCS->videoh->open(townObj->town->clientInfo.tavernVideo); + else + CCS->videoh->open(VideoPath::builtin("TAVERN.BIK")); +} + +void CTavernWindow::recruitb() +{ + const CGHeroInstance *toBuy = (selected ? h2 : h1)->h; + const CGObjectInstance *obj = tavernObj; + + LOCPLINT->cb->recruitHero(obj, toBuy); + close(); +} + +void CTavernWindow::thievesguildb() +{ + GH.windows().createAndPushWindow(tavernObj); +} + +void CTavernWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +CTavernWindow::~CTavernWindow() +{ + CCS->videoh->close(); +} + +void CTavernWindow::show(Canvas & to) +{ + CWindowObject::show(to); + + if(selected >= 0) + { + auto sel = selected ? h2 : h1; + + if(selected != oldSelected) + { + // Selected hero just changed. Update RECRUIT button hover text if recruitment is allowed. + oldSelected = selected; + + heroDescription->setText(sel->description); + + //Recruit %s the %s + if (!recruit->isBlocked()) + recruit->addHoverText(CButton::NORMAL, boost::str(boost::format(CGI->generaltexth->tavernInfo[3]) % sel->h->getNameTranslated() % sel->h->type->heroClass->getNameTranslated())); + + } + + to.drawBorder(Rect::createAround(sel->pos, 2), Colors::BRIGHT_YELLOW, 2); + } + + CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false); +} + +void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition) +{ + if(h) + *_sel = _id; +} + +void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition) +{ + if(h) + GH.windows().createAndPushWindow(std::make_shared(h)); +} + +CTavernWindow::HeroPortrait::HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H) + : CIntObject(LCLICK | SHOW_POPUP | HOVER), + h(H), _sel(&sel), _id(id) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + h = H; + pos.x += x; + pos.y += y; + pos.w = 58; + pos.h = 64; + + if(H) + { + hoverName = CGI->generaltexth->tavernInfo[4]; + boost::algorithm::replace_first(hoverName,"%s",H->getNameTranslated()); + + int artifs = (int)h->artifactsWorn.size() + (int)h->artifactsInBackpack.size(); + for(int i=13; i<=17; i++) //war machines and spellbook don't count + if(vstd::contains(h->artifactsWorn, ArtifactPosition(i))) + artifs--; + + description = CGI->generaltexth->allTexts[215]; + boost::algorithm::replace_first(description, "%s", h->getNameTranslated()); + boost::algorithm::replace_first(description, "%d", std::to_string(h->level)); + boost::algorithm::replace_first(description, "%s", h->type->heroClass->getNameTranslated()); + boost::algorithm::replace_first(description, "%d", std::to_string(artifs)); + + portrait = std::make_shared(AnimationPath::builtin("portraitsLarge"), h->getIconIndex()); + } +} + +void CTavernWindow::HeroPortrait::hover(bool on) +{ + //Hoverable::hover(on); + if(on) + GH.statusbar()->write(hoverName); + else + GH.statusbar()->clear(); +} + +static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange"; +static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE"; + +static bool isQuickExchangeLayoutAvailable() +{ + return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG)); +} + +CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID) + : CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")), + controller(hero1, hero2), + moveStackLeftButtons(), + moveStackRightButtons() +{ + const bool qeLayout = isQuickExchangeLayoutAvailable(); + + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + heroInst[0] = LOCPLINT->cb->getHero(hero1); + heroInst[1] = LOCPLINT->cb->getHero(hero2); + + auto genTitle = [](const CGHeroInstance * h) + { + boost::format fmt(CGI->generaltexth->allTexts[138]); + fmt % h->getNameTranslated() % h->level % h->type->heroClass->getNameTranslated(); + return boost::str(fmt); + }; + + titles[0] = std::make_shared(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0])); + titles[1] = std::make_shared(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1])); + + auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32")); + PSKIL32->preload(); + + auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32")); + + for(int g = 0; g < 4; ++g) + { + if (qeLayout) + primSkillImages.push_back(std::make_shared(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22))); + else + primSkillImages.push_back(std::make_shared(PSKIL32, g, 0, 385, 19 + 36 * g)); + } + + for(int leftRight : {0, 1}) + { + const CGHeroInstance * hero = heroInst.at(leftRight); + + for(int m=0; m(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE)); + + + for(int m=0; m < hero->secSkills.size(); ++m) + secSkillIcons[leftRight].push_back(std::make_shared(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88)); + + specImages[leftRight] = std::make_shared(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45); + + expImages[leftRight] = std::make_shared(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45); + expValues[leftRight] = std::make_shared(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + manaImages[leftRight] = std::make_shared(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45); + manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + } + + artifs[0] = std::make_shared(Point(-334, 150)); + artifs[0]->setHero(heroInst[0]); + artifs[1] = std::make_shared(Point(98, 150)); + artifs[1]->setHero(heroInst[1]); + + addSetAndCallbacks(artifs[0]); + addSetAndCallbacks(artifs[1]); + + for(int g=0; g<4; ++g) + { + primSkillAreas.push_back(std::make_shared()); + if (qeLayout) + primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22)); + else + primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); + primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; + primSkillAreas[g]->type = g; + primSkillAreas[g]->bonusValue = 0; + primSkillAreas[g]->baseType = 0; + primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; + boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); + } + + //heroes related thing + for(int b=0; b < heroInst.size(); b++) + { + const CGHeroInstance * hero = heroInst.at(b); + + //secondary skill's clickable areas + for(int g=0; gsecSkills.size(); ++g) + { + int skill = hero->secSkills[g].first, + level = hero->secSkills[g].second; // <1, 3> + secSkillAreas[b].push_back(std::make_shared()); + secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) ); + secSkillAreas[b][g]->baseType = 1; + + secSkillAreas[b][g]->type = skill; + secSkillAreas[b][g]->bonusValue = level; + secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); + + secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21]; + boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]); + boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated()); + } + + heroAreas[b] = std::make_shared(257 + 228 * b, 13, hero); + heroAreas[b]->addClickCallback([this, hero]() -> void + { + if(getPickedArtifact() == nullptr) + LOCPLINT->openHeroWindow(hero); + }); + + specialtyAreas[b] = std::make_shared(); + specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); + specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27]; + specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated(); + + experienceAreas[b] = std::make_shared(); + experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); + experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9]; + experienceAreas[b]->text = CGI->generaltexth->allTexts[2]; + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level)); + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1))); + boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp)); + + spellPointsAreas[b] = std::make_shared(); + spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32)); + spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22]; + spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205]; + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated()); + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana)); + boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit())); + + morale[b] = std::make_shared(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true); + luck[b] = std::make_shared(false, Rect(Point(212 + 490 * b, 39), Point(32, 32)), true); + } + + quit = std::make_shared(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT); + if(queryID.getNum() > 0) + quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); }); + + questlogButton[0] = std::make_shared(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0)); + questlogButton[1] = std::make_shared(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1)); + + Rect barRect(5, 578, 725, 18); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), barRect, 5, 578)); + + //garrison interface + + garr = std::make_shared(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true); + auto splitButtonCallback = [&](){ garr->splitClick(); }; + garr->addSplitBtn(std::make_shared( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + garr->addSplitBtn(std::make_shared( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback)); + + if(qeLayout) + { + auto moveArtifacts = [](const std::function moveRoutine) -> void + { + bool moveEquipped = true; + bool moveBackpack = true; + + if(GH.isKeyboardCtrlDown()) + moveBackpack = false; + else if(GH.isKeyboardShiftDown()) + moveEquipped = false; + moveRoutine(moveEquipped, moveBackpack); + }; + + auto moveArmy = [this](const bool leftToRight) -> void + { + std::optional slotId = std::nullopt; + if(auto slot = getSelectedSlotID()) + slotId = slot->getSlot(); + controller.moveArmy(leftToRight, slotId); + }; + + auto openBackpack = [this](const CGHeroInstance * hero) -> void + { + GH.windows().createAndPushWindow(hero); + for(auto artSet : artSets) + GH.windows().topWindow()->addSet(artSet); + }; + + moveAllGarrButtonLeft = std::make_shared(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, true)); + exchangeGarrButton = std::make_shared(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), + std::bind(&CExchangeController::swapArmy, &controller)); + moveAllGarrButtonRight = std::make_shared(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(moveArmy, false)); + moveArtifactsButtonLeft = std::make_shared(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);})); + exchangeArtifactsButton = std::make_shared(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);})); + moveArtifactsButtonRight = std::make_shared(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), + std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);})); + backpackButtonLeft = std::make_shared(Point(325, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[0])); + backpackButtonLeft->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + backpackButtonRight = std::make_shared(Point(419, 518), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"), + std::bind(openBackpack, heroInst[1])); + backpackButtonRight->addOverlay(std::make_shared(ImagePath::builtin("buttons/backpackButtonIcon"))); + + auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID(); + auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID(); + moveAllGarrButtonLeft->block(leftHeroBlock); + exchangeGarrButton->block(leftHeroBlock || rightHeroBlock); + moveAllGarrButtonRight->block(rightHeroBlock); + moveArtifactsButtonLeft->block(leftHeroBlock); + exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock); + moveArtifactsButtonRight->block(rightHeroBlock); + backpackButtonLeft->block(leftHeroBlock); + backpackButtonRight->block(rightHeroBlock); + + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + moveStackLeftButtons.push_back( + std::make_shared( + Point(484 + 35 * i, 154), + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"), + CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i)))); + moveStackLeftButtons.back()->block(leftHeroBlock); + + moveStackRightButtons.push_back( + std::make_shared( + Point(66 + 35 * i, 154), + AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"), + CButton::tooltip(CGI->generaltexth->qeModCommands[1]), + std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i)))); + moveStackLeftButtons.back()->block(rightHeroBlock); + } + } + + updateWidgets(); +} + +const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const +{ + return garr->getSelection(); +} + +void CExchangeWindow::updateGarrisons() +{ + garr->recreateSlots(); + + updateWidgets(); +} + +void CExchangeWindow::questlog(int whichHero) +{ + CCS->curh->dragAndDropCursor(nullptr); + LOCPLINT->showQuestLog(); +} + +void CExchangeWindow::updateWidgets() +{ + for(size_t leftRight : {0, 1}) + { + const CGHeroInstance * hero = heroInst.at(leftRight); + + for(int m=0; mgetPrimSkillLevel(static_cast(m)); + primSkillValues[leftRight][m]->setText(std::to_string(value)); + } + + for(int m=0; m < hero->secSkills.size(); ++m) + { + int id = hero->secSkills[m].first; + int level = hero->secSkills[m].second; + + secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level); + } + + expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3)); + manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3)); + + morale[leftRight]->set(hero); + luck[leftRight]->set(hero); + } +} + +CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("TPSHIP")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + bgWater = std::make_shared(ImagePath::builtin("TPSHIPBK"), 100, 69); + + auto handler = CGI->objtypeh->getHandlerFor(Obj::BOAT, boatType); + + auto boatConstructor = std::dynamic_pointer_cast(handler); + + assert(boatConstructor); + + if (boatConstructor) + { + AnimationPath boatFilename = boatConstructor->getBoatAnimationName(); + + Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2); + bgShip = std::make_shared(boatFilename, 0, 7, 120, 96, 0); + bgShip->center(waterCenter); + } + + // Create resource icons and costs. + std::string goldValue = std::to_string(cost[EGameResID::GOLD]); + std::string woodValue = std::to_string(cost[EGameResID::WOOD]); + + goldCost = std::make_shared(118, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, goldValue); + woodCost = std::make_shared(212, 294, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, woodValue); + + goldPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 100, 244); + woodPic = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::WOOD), 0, 196, 244); + + quit = std::make_shared(Point(224, 312), AnimationPath::builtin("ICANCEL"), CButton::tooltip(CGI->generaltexth->allTexts[599]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_CANCEL); + build = std::make_shared(Point(42, 312), AnimationPath::builtin("IBUY30"), CButton::tooltip(CGI->generaltexth->allTexts[598]), std::bind(&CShipyardWindow::close, this), EShortcut::GLOBAL_ACCEPT); + build->addCallback(onBuy); + + for(GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) + { + if(cost[i] > LOCPLINT->cb->getResourceAmount(i)) + { + build->block(true); + break; + } + } + + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + title = std::make_shared(164, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[13]); + costLabel = std::make_shared(164, 220, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[14]); +} + +void CTransformerWindow::CItem::move() +{ + if(left) + moveBy(Point(289, 0)); + else + moveBy(Point(-289, 0)); + left = !left; +} + +void CTransformerWindow::CItem::clickPressed(const Point & cursorPosition) +{ + move(); + parent->redraw(); +} + +void CTransformerWindow::CItem::update() +{ + icon->setFrame(parent->army->getCreature(SlotID(id))->getId() + 2); +} + +CTransformerWindow::CItem::CItem(CTransformerWindow * parent_, int size_, int id_) + : CIntObject(LCLICK), + id(id_), + size(size_), + parent(parent_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + left = true; + pos.w = 58; + pos.h = 64; + + pos.x += 45 + (id%3)*83 + id/6*83; + pos.y += 109 + (id/3)*98; + icon = std::make_shared(AnimationPath::builtin("TWCRPORT"), parent->army->getCreature(SlotID(id))->getId() + 2); + count = std::make_shared(28, 76,FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, std::to_string(size)); +} + +void CTransformerWindow::makeDeal() +{ + for(auto & elem : items) + { + if(!elem->left) + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, elem->id, 0, 0, hero); + } +} + +void CTransformerWindow::addAll() +{ + for(auto & elem : items) + { + if(elem->left) + elem->move(); + } + redraw(); +} + +void CTransformerWindow::updateGarrisons() +{ + for(auto & item : items) + item->update(); +} + +CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), + hero(_hero), + onWindowClosed(onWindowClosed), + market(_market) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + if(hero) + army = hero; + else + army = dynamic_cast(market); //for town only + + if(army) + { + for(int i = 0; i < GameConstants::ARMY_SIZE; i++) + { + if(army->getCreature(SlotID(i))) + items.push_back(std::make_shared(this, army->getStackCount(SlotID(i)), i)); + } + } + + all = std::make_shared(Point(146, 416), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[590], [&](){ addAll(); }, EShortcut::RECRUITMENT_UPGRADE_ALL); + convert = std::make_shared(Point(269, 416), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[591], [&](){ makeDeal(); }, EShortcut::GLOBAL_ACCEPT); + cancel = std::make_shared(Point(392, 416), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[592], [&](){ close(); },EShortcut::GLOBAL_CANCEL); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + titleLeft = std::make_shared(153, 29,FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[485]);//holding area + titleRight = std::make_shared(153+295, 29, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[486]);//transformer + helpLeft = std::make_shared(CGI->generaltexth->allTexts[487], Rect(26, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//move creatures to create skeletons + helpRight = std::make_shared(CGI->generaltexth->allTexts[488], Rect(320, 56, 255, 40), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW);//creatures here will become skeletons +} + +void CTransformerWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int Y) + : CIntObject(LCLICK | SHOW_POPUP | HOVER), + ID(_ID), + parent(_parent) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + pos.x += X; + pos.y += Y; + + topBar = std::make_shared(parent->bars, 0, 0, -28, -22); + bottomBar = std::make_shared(parent->bars, 0, 0, -28, 48); + + icon = std::make_shared(AnimationPath::builtin("SECSKILL"), _ID * 3 + 3, 0); + + name = std::make_shared(22, -13, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(ID)->getNameTranslated()); + level = std::make_shared(22, 57, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[0]); + + pos.h = icon->pos.h; + pos.w = icon->pos.w; +} + +void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) +{ + if(state() == 2) + GH.windows().createAndPushWindow(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000); +} + +void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) +{ + CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(CComponent::secskill, ID, 1)); +} + +void CUniversityWindow::CItem::hover(bool on) +{ + if(on) + GH.statusbar()->write(CGI->skillh->getByIndex(ID)->getNameTranslated()); + else + GH.statusbar()->clear(); +} + +int CUniversityWindow::CItem::state() +{ + if(parent->hero->getSecSkillLevel(SecondarySkill(ID)))//hero know this skill + return 1; + if(!parent->hero->canLearnSkill(SecondarySkill(ID)))//can't learn more skills + return 0; + return 2; +} + +void CUniversityWindow::CItem::showAll(Canvas & to) +{ + //TODO: update when state actually changes + auto stateIndex = state(); + topBar->setFrame(stateIndex); + bottomBar->setFrame(stateIndex); + + CIntObject::showAll(to); +} + +CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS1")), + hero(_hero), + onWindowClosed(onWindowClosed), + market(_market) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + bars = GH.renderHandler().createAnimation(); + bars->setCustom("UNIVRED", 0, 0); + bars->setCustom("UNIVGOLD", 1, 0); + bars->setCustom("UNIVGREN", 2, 0); + bars->preload(); + + std::string titleStr = CGI->generaltexth->allTexts[602]; + std::string speechStr = CGI->generaltexth->allTexts[603]; + + if(auto town = dynamic_cast(_market)) + { + auto faction = town->town->faction->getId(); + auto bid = town->town->getSpecialBuilding(BuildingSubID::MAGIC_UNIVERSITY)->bid; + titlePic = std::make_shared((*CGI->townh)[faction]->town->clientInfo.buildingsIcons, bid); + } + else if(auto uni = dynamic_cast(_market); uni->appearance) + { + titlePic = std::make_shared(uni->appearance->animationFile, 0); + titleStr = uni->title; + speechStr = uni->speech; + } + else + { + titlePic = std::make_shared(ImagePath::builtin("UNIVBLDG")); + } + + titlePic->center(Point(232 + pos.x, 76 + pos.y)); + + clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); + + std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); + + for(int i=0; i(this, goods[i], 54+i*104, 234)); + + cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CUniversityWindow::close() +{ + if (onWindowClosed) + onWindowClosed(); + + CStatusbarWindow::close(); +} + +void CUniversityWindow::makeDeal(int skill) +{ + LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); +} + + +CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), + owner(owner_) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + std::string text = CGI->generaltexth->allTexts[608]; + boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); + boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + boost::replace_first(text, "%d", "2000"); + + clerkSpeech = std::make_shared(text, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + name = std::make_shared(230, 37, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + icon = std::make_shared(AnimationPath::builtin("SECSKILL"), SKILL*3+3, 0, 211, 51); + level = std::make_shared(230, 107, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->levels[1]); + + costIcon = std::make_shared(AnimationPath::builtin("RESOURCE"), GameResID(EGameResID::GOLD), 0, 210, 210); + cost = std::make_shared(230, 267, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, "2000"); + + std::string hoverText = CGI->generaltexth->allTexts[609]; + boost::replace_first(hoverText, "%s", CGI->generaltexth->levels[0]+ " " + CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + + text = CGI->generaltexth->zelp[633].second; + boost::replace_first(text, "%s", CGI->generaltexth->levels[0]); + boost::replace_first(text, "%s", CGI->skillh->getByIndex(SKILL)->getNameTranslated()); + boost::replace_first(text, "%d", "2000"); + + confirm = std::make_shared(Point(148, 299), AnimationPath::builtin("IBY6432.DEF"), CButton::tooltip(hoverText, text), [=](){makeDeal(SKILL);}, EShortcut::GLOBAL_ACCEPT); + confirm->block(!available); + + cancel = std::make_shared(Point(252,299), AnimationPath::builtin("ICANCEL.DEF"), CGI->generaltexth->zelp[631], [&](){ close(); }, EShortcut::GLOBAL_CANCEL); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CUnivConfirmWindow::makeDeal(int skill) +{ + owner->makeDeal(skill); + close(); +} + +CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("GARRISON")) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + garr = std::make_shared(Point(92, 127), 4, Point(0,96), up, down, removableUnits); + { + auto split = std::make_shared(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } ); + garr->addSplitBtn(split); + } + quit = std::make_shared(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); + + std::string titleText; + if(down->tempOwner == up->tempOwner) + { + titleText = CGI->generaltexth->allTexts[709]; + } + else + { + //assume that this is joining monsters dialog + if(up->Slots().size() > 0) + { + titleText = CGI->generaltexth->allTexts[35]; + boost::algorithm::replace_first(titleText, "%s", up->Slots().begin()->second->type->getNamePluralTranslated()); + } + else + { + logGlobal->error("Invalid armed instance for garrison window."); + } + } + title = std::make_shared(275, 30, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleText); + + banner = std::make_shared(AnimationPath::builtin("CREST58"), up->getOwner().getNum(), 0, 28, 124); + portrait = std::make_shared(AnimationPath::builtin("PortraitsLarge"), down->getIconIndex(), 0, 29, 222); +} + +void CGarrisonWindow::updateGarrisons() +{ + garr->recreateSlots(); +} + +CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) + : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), + fort(object), + hero(visitor) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + title = std::make_shared(325, 32, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, fort->getObjectName()); + + heroPic = std::make_shared(30, 60, hero); + + for(int i=0; i < resCount; i++) + { + totalIcons[i] = std::make_shared(AnimationPath::builtin("SMALRES"), i, 0, 104 + 76 * i, 237); + totalLabels[i] = std::make_shared(166 + 76 * i, 253, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); + } + + for(int i = 0; i < slotsCount; i++) + { + upgrade[i] = std::make_shared(Point(107 + i * 76, 171), AnimationPath(), CButton::tooltip(getTextForSlot(SlotID(i))), [=](){ makeDeal(SlotID(i)); }, vstd::next(EShortcut::SELECT_INDEX_1, i)); + for(auto image : { "APHLF1R.DEF", "APHLF1Y.DEF", "APHLF1G.DEF" }) + upgrade[i]->addImage(AnimationPath::builtin(image)); + + for(int j : {0,1}) + { + slotIcons[i][j] = std::make_shared(AnimationPath::builtin("SMALRES"), 0, 0, 104 + 76 * i, 128 + 20 * j); + slotLabels[i][j] = std::make_shared(168 + 76 * i, 144 + 20 * j, FONT_SMALL, ETextAlignment::BOTTOMRIGHT); + } + } + + upgradeAll = std::make_shared(Point(30, 231), AnimationPath(), CButton::tooltip(CGI->generaltexth->allTexts[432]), [&](){ makeDeal(SlotID(slotsCount));}, EShortcut::RECRUITMENT_UPGRADE_ALL); + for(auto image : { "APHLF4R.DEF", "APHLF4Y.DEF", "APHLF4G.DEF" }) + upgradeAll->addImage(AnimationPath::builtin(image)); + + quit = std::make_shared(Point(294, 275), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CHillFortWindow::close, this), EShortcut::GLOBAL_ACCEPT); + statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); + + garr = std::make_shared(Point(108, 60), 18, Point(), hero, nullptr); + updateGarrisons(); +} + +void CHillFortWindow::updateGarrisons() +{ + std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade + + TResources totalSum; // totalSum[resource ID] = value + + for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); + if(info.newID.size())//we have upgrades here - update costs + { + costs[i] = info.cost[0] * hero->getStackCount(SlotID(i)); + totalSum += costs[i]; + } + } + + currState[i] = newState; + upgrade[i]->setIndex(currState[i] == -1 ? 0 : currState[i]); + upgrade[i]->block(currState[i] == -1); + upgrade[i]->addHoverText(CButton::NORMAL, getTextForSlot(SlotID(i))); + } + + //"Upgrade all" slot + int newState = 2; + { + TResources myRes = LOCPLINT->cb->getResourceAmount(); + + bool allUpgraded = true;//All creatures are upgraded? + for(int i=0; isetIndex(newState); + + garr->recreateSlots(); + + for(int i = 0; i < slotsCount; i++) + { + //hide all first + for(int j : {0,1}) + { + slotIcons[i][j]->visible = false; + slotLabels[i][j]->setText(""); + } + //if can upgrade or can not afford, draw cost + if(currState[i] == 0 || currState[i] == 2) + { + if(costs[i].nonZero()) + { + //reverse iterator is used to display gold as first element + int j = 0; + for(int res = (int)costs[i].size()-1; (res >= 0) && (j < 2); res--) + { + int val = costs[i][res]; + if(!val) + continue; + + slotIcons[i][j]->visible = true; + slotIcons[i][j]->setFrame(res); + + slotLabels[i][j]->setText(std::to_string(val)); + j++; + } + } + else//free upgrade - print gold image and "Free" text + { + slotIcons[i][0]->visible = true; + slotIcons[i][0]->setFrame(GameResID(EGameResID::GOLD)); + slotLabels[i][0]->setText(CGI->generaltexth->allTexts[344]); + } + } + } + + for(int i = 0; i < resCount; i++) + { + if(totalSum[i] == 0) + { + totalIcons[i]->visible = false; + totalLabels[i]->setText(""); + } + else + { + totalIcons[i]->visible = true; + totalLabels[i]->setText(std::to_string(totalSum[i])); + } + } +} + +void CHillFortWindow::makeDeal(SlotID slot) +{ + assert(slot.getNum()>=0); + int offset = (slot.getNum() == slotsCount)?2:0; + switch(currState[slot.getNum()]) + { + case 0: + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[314 + offset], std::vector>(), soundBase::sound_todo); + break; + case 1: + LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[313 + offset], std::vector>(), soundBase::sound_todo); + break; + case 2: + for(int i=0; icb->fillUpgradeInfo(hero, SlotID(i), info); + LOCPLINT->cb->upgradeCreature(hero, SlotID(i), info.newID[0]); + } + } + break; + } +} + +std::string CHillFortWindow::getTextForSlot(SlotID slot) +{ + if(!hero->getCreature(slot))//we don`t have creature here + return ""; + + std::string str = CGI->generaltexth->allTexts[318]; + int amount = hero->getStackCount(slot); + if(amount == 1) + boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNameSingularTranslated()); + else + boost::algorithm::replace_first(str,"%s",hero->getCreature(slot)->getNamePluralTranslated()); + + return str; +} + +int CHillFortWindow::getState(SlotID slot) +{ + TResources myRes = LOCPLINT->cb->getResourceAmount(); + + if(hero->slotEmpty(slot))//no creature here + return -1; + + UpgradeInfo info; + LOCPLINT->cb->fillUpgradeInfo(hero, slot, info); + if(!info.newID.size())//already upgraded + return 1; + + if(!(info.cost[0] * hero->getStackCount(slot)).canBeAfforded(myRes)) + return 0; + + return 2;//can upgrade +} + +CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): + CStatusbarWindow(PLAYER_COLORED | BORDERED, ImagePath::builtin("TpRank")), + owner(_owner) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + SThievesGuildInfo tgi; //info to be displayed + LOCPLINT->cb->getThievesGuildInfo(tgi, owner); + + exitb = std::make_shared(Point(748, 556), AnimationPath::builtin("TPMAGE1"), CButton::tooltip(CGI->generaltexth->allTexts[600]), [&](){ close();}, EShortcut::GLOBAL_RETURN); + statusbar = CGStatusBar::create(3, 555, ImagePath::builtin("TStatBar.bmp"), 742); + + resdatabar = std::make_shared(); + resdatabar->moveBy(pos.topLeft(), true); + + //data for information table: + // fields[row][column] = list of id's of players for this box + static std::vector< std::vector< PlayerColor > > SThievesGuildInfo::* fields[] = + { &SThievesGuildInfo::numOfTowns, &SThievesGuildInfo::numOfHeroes, &SThievesGuildInfo::gold, + &SThievesGuildInfo::woodOre, &SThievesGuildInfo::mercSulfCrystGems, &SThievesGuildInfo::obelisks, + &SThievesGuildInfo::artifacts, &SThievesGuildInfo::army, &SThievesGuildInfo::income }; + + for(int g=0; g<12; ++g) + { + int posY[] = {400, 460, 510}; + int y; + if(g < 9) + y = 52 + 32*g; + else + y = posY[g-9]; + + std::string text = CGI->generaltexth->jktexts[24+g]; + boost::algorithm::trim_if(text,boost::algorithm::is_any_of("\"")); + rowHeaders.push_back(std::make_shared(135, y, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, text)); + } + + auto PRSTRIPS = GH.renderHandler().loadAnimation(AnimationPath::builtin("PRSTRIPS")); + PRSTRIPS->preload(); + + for(int g=1; g(PRSTRIPS, g-1, 0, 250 + 66*g, 7)); + + for(int g=0; g(283 + 66*g, 24, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->jktexts[16+g])); + + auto itgflags = GH.renderHandler().loadAnimation(AnimationPath::builtin("itgflags")); + itgflags->preload(); + + //printing flags + for(int g = 0; g < std::size(fields); ++g) //by lines + { + for(int b=0; b<(tgi .* fields[g]).size(); ++b) //by places (1st, 2nd, ...) + { + std::vector &players = (tgi .* fields[g])[b]; //get players with this place in this line + + //position of box + int xpos = 259 + 66 * b; + int ypos = 41 + 32 * g; + + size_t rowLength[2]; //size of each row + rowLength[0] = std::min(players.size(), 4); + rowLength[1] = players.size() - rowLength[0]; + + for(size_t j=0; j < 2; j++) + { + // origin of this row | offset for 2nd row| shift right for short rows + //if we have 2 rows, start either from mid or beginning (depending on count), otherwise center the flags + int rowStartX = xpos + (j ? 6 + ((int)rowLength[j] < 3 ? 12 : 0) : 24 - 6 * (int)rowLength[j]); + int rowStartY = ypos + (j ? 4 : 0); + + for(size_t i=0; i < rowLength[j]; i++) + cells.push_back(std::make_shared(itgflags, players[i + j*4].getNum(), 0, rowStartX + (int)i*12, rowStartY)); + } + } + } + + static const std::string colorToBox[] = {"PRRED.BMP", "PRBLUE.BMP", "PRTAN.BMP", "PRGREEN.BMP", "PRORANGE.BMP", "PRPURPLE.BMP", "PRTEAL.BMP", "PRROSE.bmp"}; + + //printing best hero + int counter = 0; + for(auto & iter : tgi.colorToBestHero) + { + banners.push_back(std::make_shared(ImagePath::builtin(colorToBox[iter.first.getNum()]), 253 + 66 * counter, 334)); + if(iter.second.portraitSource.isValid()) + { + bestHeroes.push_back(std::make_shared(AnimationPath::builtin("PortraitsSmall"), iter.second.getIconIndex(), 0, 260 + 66 * counter, 360)); + //TODO: r-click info: + // - r-click on hero + // - r-click on primary skill label + if(iter.second.details) + { + primSkillHeaders.push_back(std::make_shared(CGI->generaltexth->allTexts[184], Rect(260 + 66*counter, 396, 52, 64), + 0, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE)); + + for(int i=0; iprimskills.size(); ++i) + { + primSkillValues.push_back(std::make_shared(310 + 66 * counter, 407 + 11*i, FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, + std::to_string(iter.second.details->primskills[i]))); + } + } + } + counter++; + } + + //printing best creature + counter = 0; + for(auto & it : tgi.bestCreature) + { + if(it.second >= 0) + bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); + counter++; + } + + //printing personality + counter = 0; + for(auto & it : tgi.personality) + { + std::string text; + if(it.second == EAiTactic::NONE) + { + text = CGI->generaltexth->arraytxt[172]; + } + else if(it.second != EAiTactic::RANDOM) + { + text = CGI->generaltexth->arraytxt[168 + static_cast(it.second)]; + } + + personalities.push_back(std::make_shared(283 + 66*counter, 459, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, text)); + + counter++; + } +} + +CObjectListWindow::CItem::CItem(CObjectListWindow * _parent, size_t _id, std::string _text) + : CIntObject(LCLICK | DOUBLECLICK), + parent(_parent), + index(_id) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + border = std::make_shared(ImagePath::builtin("TPGATES")); + pos = border->pos; + + setRedrawParent(true); + + text = std::make_shared(pos.w/2, pos.h/2, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _text); + select(index == parent->selected); +} + +void CObjectListWindow::CItem::select(bool on) +{ + ui8 mask = UPDATE | SHOWALL; + if(on) + border->recActions |= mask; + else + border->recActions &= ~mask; + redraw();//??? +} + +void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition) +{ + parent->changeSelection(index); +} + +void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition) +{ + parent->elementSelected(); +} + +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), + onSelect(Callback), + selected(initialSelection) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + items.reserve(_items.size()); + + for(int id : _items) + { + items.push_back(std::make_pair(id, LOCPLINT->cb->getObjInstance(ObjectInstanceID(id))->getObjectName())); + } + + init(titleWidget_, _title, _descr); +} + +CObjectListWindow::CObjectListWindow(const std::vector & _items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPGATE")), + onSelect(Callback), + selected(initialSelection) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + items.reserve(_items.size()); + + for(size_t i=0; i<_items.size(); i++) + items.push_back(std::make_pair(int(i), _items[i])); + + init(titleWidget_, _title, _descr); +} + +void CObjectListWindow::init(std::shared_ptr titleWidget_, std::string _title, std::string _descr) +{ + titleWidget = titleWidget_; + + title = std::make_shared(152, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, _title); + descr = std::make_shared(145, 133, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, _descr); + exit = std::make_shared( Point(228, 402), AnimationPath::builtin("ICANCEL.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::exitPressed, this), EShortcut::GLOBAL_CANCEL); + + if(titleWidget) + { + addChild(titleWidget.get()); + titleWidget->recActions = 255-DISPOSE; + titleWidget->pos.x = pos.w/2 + pos.x - titleWidget->pos.w/2; + titleWidget->pos.y =75 + pos.y - titleWidget->pos.h/2; + } + list = std::make_shared(std::bind(&CObjectListWindow::genItem, this, _1), + Point(14, 151), Point(0, 25), 9, items.size(), 0, 1, Rect(262, -32, 256, 256) ); + list->setRedrawParent(true); + + ok = std::make_shared(Point(15, 402), AnimationPath::builtin("IOKAY.DEF"), CButton::tooltip(), std::bind(&CObjectListWindow::elementSelected, this), EShortcut::GLOBAL_ACCEPT); + ok->block(!list->size()); +} + +std::shared_ptr CObjectListWindow::genItem(size_t index) +{ + if(index < items.size()) + return std::make_shared(this, index, items[index].second); + return std::shared_ptr(); +} + +void CObjectListWindow::elementSelected() +{ + std::function toCall = onSelect;//save + int where = items[selected].first; //required variables + close();//then destroy window + toCall(where);//and send selected object +} + +void CObjectListWindow::exitPressed() +{ + std::function toCall = onExit;//save + close();//then destroy window + if(toCall) + toCall(); +} + +void CObjectListWindow::changeSelection(size_t which) +{ + ok->block(false); + if(selected == which) + return; + + for(std::shared_ptr element : list->getItems()) + { + CItem * item = dynamic_cast(element.get()); + if(item) + { + if(item->index == selected) + item->select(false); + + if(item->index == which) + item->select(true); + } + } + selected = which; +} + +void CObjectListWindow::keyPressed (EShortcut key) +{ + int sel = static_cast(selected); + + switch(key) + { + break; case EShortcut::MOVE_UP: + sel -=1; + + break; case EShortcut::MOVE_DOWN: + sel +=1; + + break; case EShortcut::MOVE_PAGE_UP: + sel -=9; + + break; case EShortcut::MOVE_PAGE_DOWN: + sel +=9; + + break; case EShortcut::MOVE_FIRST: + sel = 0; + + break; case EShortcut::MOVE_LAST: + sel = static_cast(items.size()); + + break; default: + return; + } + + vstd::abetween(sel, 0, items.size()-1); + list->scrollTo(sel); + changeSelection(sel); +} diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 22233740a..5580c5a36 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -1,508 +1,508 @@ -/* - * GUIClasses.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/ResourceSet.h" -#include "../widgets/CExchangeController.h" -#include "../widgets/CWindowWithArtifacts.h" -#include "../widgets/Images.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGObjectInstance; -class CGDwelling; -class IMarket; - -VCMI_LIB_NAMESPACE_END - -class CreatureCostBox; -class CCreaturePic; -class MoraleLuckBox; -class CHeroArea; -class CSlider; -class CComponentBox; -class CTextInput; -class CListBox; -class CLabelGroup; -class CGStatusBar; -class CTextBox; -class CGarrisonInt; -class CGarrisonSlot; - -enum class EUserEvent; - -/// Recruitment window where you can recruit creatures -class CRecruitmentWindow : public CStatusbarWindow -{ - class CCreatureCard : public CIntObject, public std::enable_shared_from_this - { - CRecruitmentWindow * parent; - std::shared_ptr animation; - bool selected; - - public: - const CCreature * creature; - si32 amount; - - void select(bool on); - - CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount); - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void showAll(Canvas & to) override; - }; - - std::function onRecruit; //void (int ID, int amount) <-- call to recruit creatures - std::function onClose; - - int level; - const CArmedInstance * dst; - - std::shared_ptr selected; - std::vector> cards; - - std::shared_ptr slider; - std::shared_ptr maxButton; - std::shared_ptr buyButton; - std::shared_ptr cancelButton; - std::shared_ptr title; - std::shared_ptr availableValue; - std::shared_ptr toRecruitValue; - std::shared_ptr availableTitle; - std::shared_ptr toRecruitTitle; - std::shared_ptr costPerTroopValue; - std::shared_ptr totalCostValue; - - void select(std::shared_ptr card); - void buy(); - void sliderMoved(int to); - - void showAll(Canvas & to) override; -public: - const CGDwelling * const dwelling; - CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset = 0); - void availableCreaturesChanged(); - void close() override; -}; - -/// Split window where creatures can be split up into two single unit stacks -class CSplitWindow : public CWindowObject -{ - std::function callback; - int leftAmount; - int rightAmount; - - int leftMin; - int rightMin; - std::shared_ptr title; - std::shared_ptr slider; - std::shared_ptr animLeft; - std::shared_ptr animRight; - std::shared_ptr ok; - std::shared_ptr cancel; - std::shared_ptr leftInput; - std::shared_ptr rightInput; - - void setAmountText(std::string text, bool left); - void setAmount(int value, bool left); - void sliderMoved(int value); - void apply(); - -public: - /** - * creature - displayed creature - * callback(leftAmount, rightAmount) - function to call on close - * leftMin, rightMin - minimal amount of creatures in each stack - * leftAmount, rightAmount - amount of creatures in each stack - */ - CSplitWindow(const CCreature * creature, std::function callback, int leftMin, int rightMin, int leftAmount, int rightAmount); -}; - -/// Raised up level window where you can select one out of two skills -class CLevelWindow : public CWindowObject -{ - std::shared_ptr portrait; - std::shared_ptr ok; - std::shared_ptr mainTitle; - std::shared_ptr levelTitle; - std::shared_ptr skillIcon; - std::shared_ptr skillValue; - - std::shared_ptr box; //skills to select - std::function cb; - - void selectionChanged(unsigned to); - -public: - CLevelWindow(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, std::function callback); - ~CLevelWindow(); -}; - -/// Town portal, castle gate window -class CObjectListWindow : public CWindowObject -{ - class CItem : public CIntObject - { - CObjectListWindow * parent; - std::shared_ptr text; - std::shared_ptr border; - public: - const size_t index; - CItem(CObjectListWindow * parent, size_t id, std::string text); - - void select(bool on); - void clickPressed(const Point & cursorPosition) override; - void clickDouble(const Point & cursorPosition) override; - }; - - std::function onSelect;//called when OK button is pressed, returns id of selected item. - std::shared_ptr titleWidget; - std::shared_ptr title; - std::shared_ptr descr; - - std::shared_ptr list; - std::shared_ptr ok; - std::shared_ptr exit; - - std::vector< std::pair > items;//all items present in list - - void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr); - void exitPressed(); -public: - size_t selected;//index of currently selected item - - std::function onExit;//optional exit callback - - /// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item - /// Image can be nullptr - ///item names will be taken from map objects - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); - CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); - - std::shared_ptr genItem(size_t index); - void elementSelected();//call callback and close this window - void changeSelection(size_t which); - void keyPressed(EShortcut key) override; -}; - -class CTavernWindow : public CStatusbarWindow -{ - std::function onWindowClosed; - -public: - class HeroPortrait : public CIntObject - { - public: - std::string hoverName; - std::string description; // "XXX is a level Y ZZZ with N artifacts" - const CGHeroInstance * h; - - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover (bool on) override; - HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H); - - private: - int *_sel; - const int _id; - - std::shared_ptr portrait; - }; - - //recruitable heroes - std::shared_ptr h1; - std::shared_ptr h2; //recruitable heroes - - int selected;//0 (left) or 1 (right) - int oldSelected;//0 (left) or 1 (right) - - std::shared_ptr thiefGuild; - std::shared_ptr cancel; - std::shared_ptr recruit; - - const CGObjectInstance * tavernObj; - - std::shared_ptr title; - std::shared_ptr cost; - std::shared_ptr heroesForHire; - std::shared_ptr heroDescription; - - std::shared_ptr rumor; - - CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); - ~CTavernWindow(); - - void close() override; - void recruitb(); - void thievesguildb(); - void show(Canvas & to) override; -}; - -class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts -{ - std::array, 2> titles; - std::vector> primSkillImages;//shared for both heroes - std::array>, 2> primSkillValues; - std::array>, 2> secSkillIcons; - std::array, 2> specImages; - std::array, 2> expImages; - std::array, 2> expValues; - std::array, 2> manaImages; - std::array, 2> manaValues; - - std::vector> primSkillAreas; - std::array>, 2> secSkillAreas; - - std::array, 2> heroAreas; - std::array, 2> specialtyAreas; - std::array, 2> experienceAreas; - std::array, 2> spellPointsAreas; - - std::array, 2> morale; - std::array, 2> luck; - - std::shared_ptr quit; - std::array, 2> questlogButton; - - std::shared_ptr garr; - std::shared_ptr moveAllGarrButtonLeft; - std::shared_ptr exchangeGarrButton; - std::shared_ptr moveAllGarrButtonRight; - std::shared_ptr moveArtifactsButtonLeft; - std::shared_ptr exchangeArtifactsButton; - std::shared_ptr moveArtifactsButtonRight; - std::vector> moveStackLeftButtons; - std::vector> moveStackRightButtons; - std::shared_ptr backpackButtonLeft; - std::shared_ptr backpackButtonRight; - CExchangeController controller; - -public: - std::array heroInst; - std::array, 2> artifs; - - void updateGarrisons() override; - - void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right - - void updateWidgets(); - - const CGarrisonSlot * getSelectedSlotID() const; - - CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID); -}; - -/// Here you can buy ships -class CShipyardWindow : public CStatusbarWindow -{ - std::shared_ptr bgWater; - std::shared_ptr bgShip; - - std::shared_ptr title; - std::shared_ptr costLabel; - - std::shared_ptr woodPic; - std::shared_ptr goldPic; - std::shared_ptr woodCost; - std::shared_ptr goldCost; - - std::shared_ptr build; - std::shared_ptr quit; - -public: - CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy); -}; - -/// Creature transformer window -class CTransformerWindow : public CStatusbarWindow, public IGarrisonHolder -{ - class CItem : public CIntObject - { - public: - int id;//position of creature in hero army - bool left;//position of the item - int size; //size of creature stack - CTransformerWindow * parent; - std::shared_ptr icon; - std::shared_ptr count; - - void move(); - void clickPressed(const Point & cursorPosition) override; - void update(); - CItem(CTransformerWindow * parent, int size, int id); - }; - - const CArmedInstance * army;//object with army for transforming (hero or town) - const CGHeroInstance * hero;//only if we have hero in town - const IMarket * market;//market, town garrison is used if hero == nullptr - - std::shared_ptr titleLeft; - std::shared_ptr titleRight; - std::shared_ptr helpLeft; - std::shared_ptr helpRight; - - std::vector> items; - - std::shared_ptr all; - std::shared_ptr convert; - std::shared_ptr cancel; - - std::function onWindowClosed; -public: - - void makeDeal(); - void addAll(); - void close() override; - void updateGarrisons() override; - CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); -}; - -class CUniversityWindow : public CStatusbarWindow -{ - class CItem : public CIntObject - { - std::shared_ptr icon; - std::shared_ptr topBar; - std::shared_ptr bottomBar; - std::shared_ptr name; - std::shared_ptr level; - public: - int ID;//id of selected skill - CUniversityWindow * parent; - - void showAll(Canvas & to) override; - void clickPressed(const Point & cursorPosition) override; - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - int state();//0=can't learn, 1=learned, 2=can learn - CItem(CUniversityWindow * _parent, int _ID, int X, int Y); - }; - - const CGHeroInstance * hero; - const IMarket * market; - - std::shared_ptr bars; - - std::vector> items; - - std::shared_ptr cancel; - std::shared_ptr titlePic; - std::shared_ptr title; - std::shared_ptr clerkSpeech; - - std::function onWindowClosed; - -public: - CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); - - void makeDeal(int skill); - void close(); -}; - -/// Confirmation window for University -class CUnivConfirmWindow : public CStatusbarWindow -{ - std::shared_ptr clerkSpeech; - std::shared_ptr name; - std::shared_ptr level; - std::shared_ptr icon; - - CUniversityWindow * owner; - std::shared_ptr confirm; - std::shared_ptr cancel; - - std::shared_ptr costIcon; - std::shared_ptr cost; - - void makeDeal(int skill); - -public: - CUnivConfirmWindow(CUniversityWindow * PARENT, int SKILL, bool available); -}; - -/// Garrison window where you can take creatures out of the hero to place it on the garrison -class CGarrisonWindow : public CWindowObject, public IGarrisonHolder -{ - std::shared_ptr title; - std::shared_ptr banner; - std::shared_ptr portrait; - - std::shared_ptr garr; - -public: - std::shared_ptr quit; - - CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); - - void updateGarrisons() override; -}; - -/// Hill fort is the building where you can upgrade units -class CHillFortWindow : public CStatusbarWindow, public IGarrisonHolder -{ -private: - static const int slotsCount = 7; - //todo: mithril support - static const int resCount = 7; - - const CGObjectInstance * fort; - const CGHeroInstance * hero; - - std::shared_ptr title; - std::shared_ptr heroPic; - - std::array, resCount> totalIcons; - std::array, resCount> totalLabels; - - std::array, slotsCount> upgrade;//upgrade single creature - std::array currState;//current state of slot - to avoid calls to getState or updating buttons - - //there is a place for only 2 resources per slot - std::array< std::array, 2>, slotsCount> slotIcons; - std::array< std::array, 2>, slotsCount> slotLabels; - - std::shared_ptr upgradeAll; - std::shared_ptr quit; - - std::shared_ptr garr; - - std::string getDefForSlot(SlotID slot); - std::string getTextForSlot(SlotID slot); - - void makeDeal(SlotID slot);//-1 for upgrading all creatures - int getState(SlotID slot); //-1 = no creature 0=can't upgrade, 1=upgraded, 2=can upgrade -public: - CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); - void updateGarrisons() override;//update buttons after garrison changes -}; - -class CThievesGuildWindow : public CStatusbarWindow -{ - const CGObjectInstance * owner; - - std::shared_ptr exitb; - std::shared_ptr resdatabar; - - std::vector> rowHeaders; - std::vector> columnBackgrounds; - std::vector> columnHeaders; - std::vector> cells; - - std::vector> banners; - std::vector> bestHeroes; - std::vector> primSkillHeaders; - std::vector> primSkillValues; - std::vector> bestCreatures; - std::vector> personalities; -public: - CThievesGuildWindow(const CGObjectInstance * _owner); -}; - +/* + * GUIClasses.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/ResourceSet.h" +#include "../widgets/CExchangeController.h" +#include "../widgets/CWindowWithArtifacts.h" +#include "../widgets/Images.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGObjectInstance; +class CGDwelling; +class IMarket; + +VCMI_LIB_NAMESPACE_END + +class CreatureCostBox; +class CCreaturePic; +class MoraleLuckBox; +class CHeroArea; +class CSlider; +class CComponentBox; +class CTextInput; +class CListBox; +class CLabelGroup; +class CGStatusBar; +class CTextBox; +class CGarrisonInt; +class CGarrisonSlot; + +enum class EUserEvent; + +/// Recruitment window where you can recruit creatures +class CRecruitmentWindow : public CStatusbarWindow +{ + class CCreatureCard : public CIntObject, public std::enable_shared_from_this + { + CRecruitmentWindow * parent; + std::shared_ptr animation; + bool selected; + + public: + const CCreature * creature; + si32 amount; + + void select(bool on); + + CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount); + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void showAll(Canvas & to) override; + }; + + std::function onRecruit; //void (int ID, int amount) <-- call to recruit creatures + std::function onClose; + + int level; + const CArmedInstance * dst; + + std::shared_ptr selected; + std::vector> cards; + + std::shared_ptr slider; + std::shared_ptr maxButton; + std::shared_ptr buyButton; + std::shared_ptr cancelButton; + std::shared_ptr title; + std::shared_ptr availableValue; + std::shared_ptr toRecruitValue; + std::shared_ptr availableTitle; + std::shared_ptr toRecruitTitle; + std::shared_ptr costPerTroopValue; + std::shared_ptr totalCostValue; + + void select(std::shared_ptr card); + void buy(); + void sliderMoved(int to); + + void showAll(Canvas & to) override; +public: + const CGDwelling * const dwelling; + CRecruitmentWindow(const CGDwelling * Dwelling, int Level, const CArmedInstance * Dst, const std::function & Recruit, const std::function & onClose, int y_offset = 0); + void availableCreaturesChanged(); + void close() override; +}; + +/// Split window where creatures can be split up into two single unit stacks +class CSplitWindow : public CWindowObject +{ + std::function callback; + int leftAmount; + int rightAmount; + + int leftMin; + int rightMin; + std::shared_ptr title; + std::shared_ptr slider; + std::shared_ptr animLeft; + std::shared_ptr animRight; + std::shared_ptr ok; + std::shared_ptr cancel; + std::shared_ptr leftInput; + std::shared_ptr rightInput; + + void setAmountText(std::string text, bool left); + void setAmount(int value, bool left); + void sliderMoved(int value); + void apply(); + +public: + /** + * creature - displayed creature + * callback(leftAmount, rightAmount) - function to call on close + * leftMin, rightMin - minimal amount of creatures in each stack + * leftAmount, rightAmount - amount of creatures in each stack + */ + CSplitWindow(const CCreature * creature, std::function callback, int leftMin, int rightMin, int leftAmount, int rightAmount); +}; + +/// Raised up level window where you can select one out of two skills +class CLevelWindow : public CWindowObject +{ + std::shared_ptr portrait; + std::shared_ptr ok; + std::shared_ptr mainTitle; + std::shared_ptr levelTitle; + std::shared_ptr skillIcon; + std::shared_ptr skillValue; + + std::shared_ptr box; //skills to select + std::function cb; + + void selectionChanged(unsigned to); + +public: + CLevelWindow(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, std::function callback); + ~CLevelWindow(); +}; + +/// Town portal, castle gate window +class CObjectListWindow : public CWindowObject +{ + class CItem : public CIntObject + { + CObjectListWindow * parent; + std::shared_ptr text; + std::shared_ptr border; + public: + const size_t index; + CItem(CObjectListWindow * parent, size_t id, std::string text); + + void select(bool on); + void clickPressed(const Point & cursorPosition) override; + void clickDouble(const Point & cursorPosition) override; + }; + + std::function onSelect;//called when OK button is pressed, returns id of selected item. + std::shared_ptr titleWidget; + std::shared_ptr title; + std::shared_ptr descr; + + std::shared_ptr list; + std::shared_ptr ok; + std::shared_ptr exit; + + std::vector< std::pair > items;//all items present in list + + void init(std::shared_ptr titleWidget_, std::string _title, std::string _descr); + void exitPressed(); +public: + size_t selected;//index of currently selected item + + std::function onExit;//optional exit callback + + /// Callback will be called when OK button is pressed, returns id of selected item. initState = initially selected item + /// Image can be nullptr + ///item names will be taken from map objects + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); + CObjectListWindow(const std::vector &_items, std::shared_ptr titleWidget_, std::string _title, std::string _descr, std::function Callback, size_t initialSelection = 0); + + std::shared_ptr genItem(size_t index); + void elementSelected();//call callback and close this window + void changeSelection(size_t which); + void keyPressed(EShortcut key) override; +}; + +class CTavernWindow : public CStatusbarWindow +{ + std::function onWindowClosed; + +public: + class HeroPortrait : public CIntObject + { + public: + std::string hoverName; + std::string description; // "XXX is a level Y ZZZ with N artifacts" + const CGHeroInstance * h; + + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover (bool on) override; + HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H); + + private: + int *_sel; + const int _id; + + std::shared_ptr portrait; + }; + + //recruitable heroes + std::shared_ptr h1; + std::shared_ptr h2; //recruitable heroes + + int selected;//0 (left) or 1 (right) + int oldSelected;//0 (left) or 1 (right) + + std::shared_ptr thiefGuild; + std::shared_ptr cancel; + std::shared_ptr recruit; + + const CGObjectInstance * tavernObj; + + std::shared_ptr title; + std::shared_ptr cost; + std::shared_ptr heroesForHire; + std::shared_ptr heroDescription; + + std::shared_ptr rumor; + + CTavernWindow(const CGObjectInstance * TavernObj, const std::function & onWindowClosed); + ~CTavernWindow(); + + void close() override; + void recruitb(); + void thievesguildb(); + void show(Canvas & to) override; +}; + +class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts +{ + std::array, 2> titles; + std::vector> primSkillImages;//shared for both heroes + std::array>, 2> primSkillValues; + std::array>, 2> secSkillIcons; + std::array, 2> specImages; + std::array, 2> expImages; + std::array, 2> expValues; + std::array, 2> manaImages; + std::array, 2> manaValues; + + std::vector> primSkillAreas; + std::array>, 2> secSkillAreas; + + std::array, 2> heroAreas; + std::array, 2> specialtyAreas; + std::array, 2> experienceAreas; + std::array, 2> spellPointsAreas; + + std::array, 2> morale; + std::array, 2> luck; + + std::shared_ptr quit; + std::array, 2> questlogButton; + + std::shared_ptr garr; + std::shared_ptr moveAllGarrButtonLeft; + std::shared_ptr exchangeGarrButton; + std::shared_ptr moveAllGarrButtonRight; + std::shared_ptr moveArtifactsButtonLeft; + std::shared_ptr exchangeArtifactsButton; + std::shared_ptr moveArtifactsButtonRight; + std::vector> moveStackLeftButtons; + std::vector> moveStackRightButtons; + std::shared_ptr backpackButtonLeft; + std::shared_ptr backpackButtonRight; + CExchangeController controller; + +public: + std::array heroInst; + std::array, 2> artifs; + + void updateGarrisons() override; + + void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right + + void updateWidgets(); + + const CGarrisonSlot * getSelectedSlotID() const; + + CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID); +}; + +/// Here you can buy ships +class CShipyardWindow : public CStatusbarWindow +{ + std::shared_ptr bgWater; + std::shared_ptr bgShip; + + std::shared_ptr title; + std::shared_ptr costLabel; + + std::shared_ptr woodPic; + std::shared_ptr goldPic; + std::shared_ptr woodCost; + std::shared_ptr goldCost; + + std::shared_ptr build; + std::shared_ptr quit; + +public: + CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function & onBuy); +}; + +/// Creature transformer window +class CTransformerWindow : public CStatusbarWindow, public IGarrisonHolder +{ + class CItem : public CIntObject + { + public: + int id;//position of creature in hero army + bool left;//position of the item + int size; //size of creature stack + CTransformerWindow * parent; + std::shared_ptr icon; + std::shared_ptr count; + + void move(); + void clickPressed(const Point & cursorPosition) override; + void update(); + CItem(CTransformerWindow * parent, int size, int id); + }; + + const CArmedInstance * army;//object with army for transforming (hero or town) + const CGHeroInstance * hero;//only if we have hero in town + const IMarket * market;//market, town garrison is used if hero == nullptr + + std::shared_ptr titleLeft; + std::shared_ptr titleRight; + std::shared_ptr helpLeft; + std::shared_ptr helpRight; + + std::vector> items; + + std::shared_ptr all; + std::shared_ptr convert; + std::shared_ptr cancel; + + std::function onWindowClosed; +public: + + void makeDeal(); + void addAll(); + void close() override; + void updateGarrisons() override; + CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); +}; + +class CUniversityWindow : public CStatusbarWindow +{ + class CItem : public CIntObject + { + std::shared_ptr icon; + std::shared_ptr topBar; + std::shared_ptr bottomBar; + std::shared_ptr name; + std::shared_ptr level; + public: + int ID;//id of selected skill + CUniversityWindow * parent; + + void showAll(Canvas & to) override; + void clickPressed(const Point & cursorPosition) override; + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + int state();//0=can't learn, 1=learned, 2=can learn + CItem(CUniversityWindow * _parent, int _ID, int X, int Y); + }; + + const CGHeroInstance * hero; + const IMarket * market; + + std::shared_ptr bars; + + std::vector> items; + + std::shared_ptr cancel; + std::shared_ptr titlePic; + std::shared_ptr title; + std::shared_ptr clerkSpeech; + + std::function onWindowClosed; + +public: + CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); + + void makeDeal(int skill); + void close(); +}; + +/// Confirmation window for University +class CUnivConfirmWindow : public CStatusbarWindow +{ + std::shared_ptr clerkSpeech; + std::shared_ptr name; + std::shared_ptr level; + std::shared_ptr icon; + + CUniversityWindow * owner; + std::shared_ptr confirm; + std::shared_ptr cancel; + + std::shared_ptr costIcon; + std::shared_ptr cost; + + void makeDeal(int skill); + +public: + CUnivConfirmWindow(CUniversityWindow * PARENT, int SKILL, bool available); +}; + +/// Garrison window where you can take creatures out of the hero to place it on the garrison +class CGarrisonWindow : public CWindowObject, public IGarrisonHolder +{ + std::shared_ptr title; + std::shared_ptr banner; + std::shared_ptr portrait; + + std::shared_ptr garr; + +public: + std::shared_ptr quit; + + CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); + + void updateGarrisons() override; +}; + +/// Hill fort is the building where you can upgrade units +class CHillFortWindow : public CStatusbarWindow, public IGarrisonHolder +{ +private: + static const int slotsCount = 7; + //todo: mithril support + static const int resCount = 7; + + const CGObjectInstance * fort; + const CGHeroInstance * hero; + + std::shared_ptr title; + std::shared_ptr heroPic; + + std::array, resCount> totalIcons; + std::array, resCount> totalLabels; + + std::array, slotsCount> upgrade;//upgrade single creature + std::array currState;//current state of slot - to avoid calls to getState or updating buttons + + //there is a place for only 2 resources per slot + std::array< std::array, 2>, slotsCount> slotIcons; + std::array< std::array, 2>, slotsCount> slotLabels; + + std::shared_ptr upgradeAll; + std::shared_ptr quit; + + std::shared_ptr garr; + + std::string getDefForSlot(SlotID slot); + std::string getTextForSlot(SlotID slot); + + void makeDeal(SlotID slot);//-1 for upgrading all creatures + int getState(SlotID slot); //-1 = no creature 0=can't upgrade, 1=upgraded, 2=can upgrade +public: + CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); + void updateGarrisons() override;//update buttons after garrison changes +}; + +class CThievesGuildWindow : public CStatusbarWindow +{ + const CGObjectInstance * owner; + + std::shared_ptr exitb; + std::shared_ptr resdatabar; + + std::vector> rowHeaders; + std::vector> columnBackgrounds; + std::vector> columnHeaders; + std::vector> cells; + + std::vector> banners; + std::vector> bestHeroes; + std::vector> primSkillHeaders; + std::vector> primSkillValues; + std::vector> bestCreatures; + std::vector> personalities; +public: + CThievesGuildWindow(const CGObjectInstance * _owner); +}; + diff --git a/config/artifacts.json b/config/artifacts.json index 714405b47..51ecf31ba 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -1,2308 +1,2308 @@ -{ - "spellBook": - { - "index" : 0, - "type" : ["HERO"] - }, - "spellScroll": - { - "index" : 1, - "type" : ["HERO"] - }, - "grail": - { - "index" : 2, - "type" : ["HERO"] - }, - "catapult": - { - "index" : 3, - "type" : ["HERO"], - "warMachine" : "catapult" - }, - "ballista": - { - "index" : 4, - "type" : ["HERO"], - "warMachine" : "ballista" - }, - "ammoCart": - { - "index" : 5, - "type" : ["HERO"], - "warMachine" : "ammoCart" - }, - "firstAidTent": - { - "index" : 6, - "type" : ["HERO"], - "warMachine" : "firstAidTent" - }, - "centaurAxe": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 7, - "type" : ["HERO"] - }, - "blackshardOfTheDeadKnight": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 8, - "type" : ["HERO"] - }, - "greaterGnollsFlail": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 9, - "type" : ["HERO"] - }, - "ogresClubOfHavoc": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 10, - "type" : ["HERO"] - }, - "swordOfHellfire": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 11, - "type" : ["HERO"] - }, - "titansGladius": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 12, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : -3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 12, - "type" : ["HERO"] - }, - "shieldOfTheDwarvenLords": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 13, - "type" : ["HERO"] - }, - "shieldOfTheYawningDead": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 14, - "type" : ["HERO"] - }, - "bucklerOfTheGnollKing": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 15, - "type" : ["HERO"] - }, - "targOfTheRampagingOgre": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 16, - "type" : ["HERO"] - }, - "shieldOfTheDamned": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 17, - "type" : ["HERO"] - }, - "sentinelsShield": - { - "bonuses" : [ - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 12, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : 0, - "type" : "PRIMARY_SKILL", - "val" : -3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 18, - "type" : ["HERO"] - }, - "helmOfTheAlabasterUnicorn": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 19, - "type" : ["HERO"] - }, - "skullHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 20, - "type" : ["HERO"] - }, - "helmOfChaos": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 21, - "type" : ["HERO"] - }, - "crownOfTheSupremeMagi": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 22, - "type" : ["HERO"] - }, - "hellstormHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 23, - "type" : ["HERO"] - }, - "thunderHelmet": - { - "bonuses" : [ - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : -2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 24, - "type" : ["HERO"] - }, - "breastplateOfPetrifiedWood": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 25, - "type" : ["HERO"] - }, - "ribCage": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 26, - "type" : ["HERO"] - }, - "scalesOfTheGreaterBasilisk": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 27, - "type" : ["HERO"] - }, - "tunicOfTheCyclopsKing": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 28, - "type" : ["HERO"] - }, - "breastplateOfBrimstone": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 29, - "type" : ["HERO"] - }, - "titansCuirass": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 10, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : -2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 30, - "type" : ["HERO"] - }, - "armorOfWonder": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 31, - "type" : ["HERO"] - }, - "sandalsOfTheSaint": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 32, - "type" : ["HERO"] - }, - "celestialNecklaceOfBliss": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 33, - "type" : ["HERO"] - }, - "lionsShieldOfCourage": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 34, - "type" : ["HERO"] - }, - "swordOfJudgement": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 35, - "type" : ["HERO"] - }, - "helmOfHeavenlyEnlightenment": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 36, - "type" : ["HERO"] - }, - "quietEyeOfTheDragon": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 37, - "type" : ["HERO"] - }, - "redDragonFlameTongue": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 38, - "type" : ["HERO"] - }, - "dragonScaleShield": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 39, - "type" : ["HERO"] - }, - "dragonScaleArmor": - { - "bonuses" : [ - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 40, - "type" : ["HERO"] - }, - "dragonboneGreaves": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 41, - "type" : ["HERO"] - }, - "dragonWingTabard": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 42, - "type" : ["HERO"] - }, - "necklaceOfDragonteeth": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 43, - "type" : ["HERO"] - }, - "crownOfDragontooth": - { - "bonuses" : [ - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 44, - "type" : ["HERO"] - }, - "stillEyeOfTheDragon": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 45, - "type" : ["HERO"] - }, - "cloverOfFortune": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 46, - "type" : ["HERO"] - }, - "cardsOfProphecy": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 47, - "type" : ["HERO"] - }, - "ladybirdOfLuck": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 48, - "type" : ["HERO"] - }, - "badgeOfCourage": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MIND_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 49, - "type" : ["HERO"] - }, - "crestOfValor": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 50, - "type" : ["HERO"] - }, - "glyphOfGallantry": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 51, - "type" : ["HERO"] - }, - "speculum": - { - "bonuses" : [ - { - "type" : "SIGHT_RADIUS", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 52, - "type" : ["HERO"] - }, - "spyglass": - { - "bonuses" : [ - { - "type" : "SIGHT_RADIUS", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 53, - "type" : ["HERO"] - }, - "amuletOfTheUndertaker": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 5, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 54, - "type" : ["HERO"] - }, - "vampiresCowl": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 10, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 55, - "type" : ["HERO"] - }, - "deadMansBoots": - { - "bonuses" : [ - { - "type" : "UNDEAD_RAISE_PERCENTAGE", - "val" : 15, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 56, - "type" : ["HERO"] - }, - "garnitureOfInterference": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 57, - "type" : ["HERO"] - }, - "surcoatOfCounterpoise": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 58, - "type" : ["HERO"] - }, - "bootsOfPolarity": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "MAGIC_RESISTANCE", - "val" : 15, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 59, - "type" : ["HERO"] - }, - "bowOfElvenCherrywood": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 5, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 60, - "type" : ["HERO"] - }, - "bowstringOfTheUnicornsMane": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 10, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 61, - "type" : ["HERO"] - }, - "angelFeatherArrows": - { - "bonuses" : [ - { - "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, - "val" : 15, - "valueType" : "ADDITIVE_VALUE", - "limiters" : [ - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ - "PERCENTAGE_DAMAGE_BOOST", - 1, - { - "type" : "SECONDARY_SKILL", - "id" : "skill.archery" - } - ] - } - ] - } - ], - "index" : 62, - "type" : ["HERO"] - }, - "birdOfPerception": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 5, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 63, - "type" : ["HERO"] - }, - "stoicWatchman": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 10, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 64, - "type" : ["HERO"] - }, - "emblemOfCognizance": - { - "bonuses" : [ - { - "subtype" : 0, - "type" : "LEARN_BATTLE_SPELL_CHANCE", - "val" : 15, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 65, - "type" : ["HERO"] - }, - "statesmansMedal": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 66, - "type" : ["HERO"] - }, - "diplomatsRing": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 67, - "type" : ["HERO"] - }, - "ambassadorsSash": - { - "bonuses" : [ - { - "type" : "SURRENDER_DISCOUNT", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 68, - "type" : ["HERO"] - }, - "ringOfTheWayfarer": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 69, - "type" : ["HERO"] - }, - "equestriansGloves": - { - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 1, - "val" : 300, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 70, - "type" : ["HERO"] - }, - "necklaceOfOceanGuidance": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 0, - "val" : 1000, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 71, - "type" : ["HERO"] - }, - "angelWings": - { - "bonuses" : [ - { - "type" : "FLYING_MOVEMENT", - "val" : 0, - "valueType" : "INDEPENDENT_MIN" - } - ], - "index" : 72, - "type" : ["HERO"] - }, - "charmOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 73, - "type" : ["HERO"] - }, - "talismanOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 74, - "type" : ["HERO"] - }, - "mysticOrbOfMana": - { - "bonuses" : [ - { - "type" : "MANA_REGENERATION", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 75, - "type" : ["HERO"] - }, - "collarOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 76, - "type" : ["HERO"] - }, - "ringOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 77, - "type" : ["HERO"] - }, - "capeOfConjuring": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 78, - "type" : ["HERO"] - }, - "orbOfTheFirmament": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.air", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 79, - "type" : ["HERO"] - }, - "orbOfSilt": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.earth", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 80, - "type" : ["HERO"] - }, - "orbOfTempestuousFire": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.fire", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 81, - "type" : ["HERO"] - }, - "orbOfDrivingRain": - { - "bonuses" : [ - { - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.water", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 82, - "type" : ["HERO"] - }, - "recantersCloak": - { - "index" : 83, - "type" : ["HERO"], - "bonuses": [ - { - "type" : "BLOCK_MAGIC_ABOVE", - "val" : 2, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ] - }, - "spiritOfOppression": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 84, - "type" : ["HERO"] - }, - "hourglassOfTheEvilHour": - { - "bonuses" : [ - { - "type" : "LUCK", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 85, - "type" : ["HERO"] - }, - "tomeOfFireMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 1, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 86, - "type" : ["HERO"] - }, - "tomeOfAirMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 0, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 87, - "type" : ["HERO"] - }, - "tomeOfWaterMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 2, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 88, - "type" : ["HERO"] - }, - "tomeOfEarthMagic": - { - "bonuses" : [ - { - "type" : "SPELLS_OF_SCHOOL", - "subtype" : 3, - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 89, - "type" : ["HERO"] - }, - "bootsOfLevitation": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "WATER_WALKING", - "val" : 0, - "valueType" : "INDEPENDENT_MIN" - } - ], - "index" : 90, - "type" : ["HERO"] - }, - "goldenBow": - { - "bonuses" : [ - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_WALL_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 91, - "type" : ["HERO"] - }, - "sphereOfPermanence": - { - "bonuses" : [ - { - "subtype" : 35, - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER", - "addInfo" : 1 - } - ], - "index" : 92, - "type" : ["HERO"] - }, - "orbOfVulnerability": - { - "bonuses" : [ - { - "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 0, - "val" : 0, - "valueType" : "BASE_NUMBER", - "propagator": "BATTLE_WIDE" - }, - { - "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 1, - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MAGIC_RESISTANCE", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - }, - { - "type" : "SPELL_RESISTANCE_AURA", - "val" : 0, - "valueType" : "INDEPENDENT_MIN", - "propagator": "BATTLE_WIDE" - } - ], - "index" : 93, - "type" : ["HERO"] - }, - "ringOfVitality": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 1, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 94, - "type" : ["HERO"] - }, - "ringOfLife": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 1, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 95, - "type" : ["HERO"] - }, - "vialOfLifeblood": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 2, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 96, - "type" : ["HERO"] - }, - "necklaceOfSwiftness": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 1, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 97, - "type" : ["HERO"] - }, - "bootsOfSpeed": - { - "bonuses" : [ - { - "type" : "MOVEMENT", - "subtype" : 1, - "val" : 600, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 98, - "type" : ["HERO"] - }, - "capeOfVelocity": - { - "bonuses" : [ - { - "type" : "STACKS_SPEED", - "val" : 2, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 99, - "type" : ["HERO"] - }, - "pendantOfDispassion": - { - "bonuses" : [ - { - "subtype" : "spell.berserk", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 100, - "type" : ["HERO"] - }, - "pendantOfSecondSight": - { - "bonuses" : [ - { - "subtype" : "spell.blind", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 101, - "type" : ["HERO"] - }, - "pendantOfHoliness": - { - "bonuses" : [ - { - "subtype" : "spell.curse", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 102, - "type" : ["HERO"] - }, - "pendantOfLife": - { - "bonuses" : [ - { - "subtype" : "spell.deathRipple", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 103, - "type" : ["HERO"] - }, - "pendantOfDeath": - { - "bonuses" : [ - { - "limiters" : ["IS_UNDEAD"], - "subtype" : "spell.destroyUndead", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 104, - "type" : ["HERO"] - }, - "pendantOfFreeWill": - { - "bonuses" : [ - { - "subtype" : "spell.hypnotize", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 105, - "type" : ["HERO"] - }, - "pendantOfNegativity": - { - "bonuses" : [ - { - "subtype" : "spell.lightningBolt", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.chainLightning", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 106, - "type" : ["HERO"] - }, - "pendantOfTotalRecall": - { - "bonuses" : [ - { - "subtype" : "spell.forgetfulness", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 107, - "type" : ["HERO"] - }, - "pendantOfCourage": - { - "bonuses" : [ - { - "type" : "MORALE", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "LUCK", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 108, - "type" : ["HERO"] - }, - "everflowingCrystalCloak": - { - "bonuses" : [ - { - "subtype" : "resource.crystal", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 109, - "type" : ["HERO"] - }, - "ringOfInfiniteGems": - { - "bonuses" : [ - { - "subtype" : "resource.gems", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 110, - "type" : ["HERO"] - }, - "everpouringVialOfMercury": - { - "bonuses" : [ - { - "subtype" : "resource.mercury", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 111, - "type" : ["HERO"] - }, - "inexhaustibleCartOfOre": - { - "bonuses" : [ - { - "subtype" : "resource.ore", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 112, - "type" : ["HERO"] - }, - "eversmokingRingOfSulfur": - { - "bonuses" : [ - { - "subtype" : "resource.sulfur", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 113, - "type" : ["HERO"] - }, - "inexhaustibleCartOfLumber": - { - "bonuses" : [ - { - "subtype" : "resource.wood", - "type" : "GENERATE_RESOURCE", - "val" : 1, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 114, - "type" : ["HERO"] - }, - "endlessSackOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 1000, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 115, - "type" : ["HERO"] - }, - "endlessBagOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 750, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 116, - "type" : ["HERO"] - }, - "endlessPurseOfGold": - { - "bonuses" : [ - { - "subtype" : "resource.gold", - "type" : "GENERATE_RESOURCE", - "val" : 500, - "valueType" : "BASE_NUMBER", - "stacking" : "ALWAYS" - } - ], - "index" : 117, - "type" : ["HERO"] - }, - "legsOfLegion": - { - "index" : 118, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 1, - "val" : 5, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "loinsOfLegion": - { - "index" : 119, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 2, - "val" : 4, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "torsoOfLegion": - { - "index" : 120, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 3, - "val" : 3, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "armsOfLegion": - { - "index" : 121, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 4, - "val" : 2, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "headOfLegion": - { - "index" : 122, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH", - "subtype" : 5, - "val" : 1, - "propagator": "VISITED_TOWN_AND_VISITOR" - } - ] - }, - "seaCaptainsHat": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "WHIRLPOOL_PROTECTION", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "type" : "MOVEMENT", - "subtype" : 0, - "val" : 500, - "valueType" : "ADDITIVE_VALUE" - }, - { - "subtype" : "spell.summonBoat", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "spell.scuttleBoat", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - } - ], - "index" : 123, - "type" : ["HERO"] - }, - "spellbindersHat": - { - "bonuses" : [ - { - "subtype" : 5, - "type" : "SPELLS_OF_LEVEL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 124, - "type" : ["HERO"] - }, - "shacklesOfWar": - { - "index" : 125, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "BATTLE_NO_FLEEING", - "propagator": "BATTLE_WIDE" - } - ] - }, - "orbOfInhibition": - { - "index" : 126, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "BLOCK_ALL_MAGIC", - "propagator": "BATTLE_WIDE" - } - ] - }, - "vialOfDragonBlood": - { - "bonuses" : [ - { - "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - }, - { - "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 5, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 127, - "type" : ["HERO"] - }, - "armageddonsBlade": - { - "bonuses" : [ - { - "subtype" : "spell.armageddon", - "type" : "SPELL", - "val" : 3, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "spell.armageddon", - "type" : "SPELL_IMMUNITY", - "val" : 0, - "valueType" : "BASE_NUMBER", - "addInfo" : 1 - }, - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 3, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 128, - "type" : ["HERO"] - }, - "angelicAlliance": - { - "bonuses" : [ - { - "type" : "NONEVIL_ALIGNMENT_MIX", - "val" : 0, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.prayer", - "type" : "OPENING_BATTLE_SPELL", - "val" : 10, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 129, - "type" : ["HERO"], - "components": - [ - "armorOfWonder", - "sandalsOfTheSaint", - "celestialNecklaceOfBliss", - "lionsShieldOfCourage", - "swordOfJudgement", - "helmOfHeavenlyEnlightenment" - ] - }, - "cloakOfTheUndeadKing": - { - "bonuses" : [ - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.skeleton", - "addInfo" : 0 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.walkingDead", - "addInfo" : 1 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.wight", - "addInfo" : 2 - }, - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.lich", - "addInfo" : 3 - } - ], - "index" : 130, - "type" : ["HERO"], - "components": - [ - "amuletOfTheUndertaker", - "vampiresCowl", - "deadMansBoots" - ] - }, - "elixirOfLife": - { - "bonuses" : [ - { - "type" : "STACK_HEALTH", - "val" : 25, - "valueType" : "PERCENT_TO_BASE", - "limiters" : [ - "noneOf", - "IS_UNDEAD", - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "NON_LIVING" ] - }, - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "GARGOYLE" ] - } - ] - }, - { - "type" : "HP_REGENERATION", - "val" : 50, - "valueType" : "BASE_NUMBER", - "limiters" : [ - "noneOf", - "IS_UNDEAD", - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "NON_LIVING" ] - }, - { - "type" : "HAS_ANOTHER_BONUS_LIMITER", - "parameters" : [ "GARGOYLE" ] - } - ] - } - ], - "index" : 131, - "type" : ["HERO"], - "components": - [ - "ringOfVitality", - "ringOfLife", - "vialOfLifeblood" - ] - }, - "armorOfTheDamned": - { - "bonuses" : [ - { - "subtype" : "spell.slow", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.curse", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.weakness", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "spell.misfortune", - "type" : "OPENING_BATTLE_SPELL", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 132, - "type" : ["HERO"], - "components": - [ - "blackshardOfTheDeadKnight", - "shieldOfTheYawningDead", - "skullHelmet", - "ribCage" - ] - }, - "statueOfLegion": - { - "index" : 133, - "type" : ["HERO"], - "components": - [ - "legsOfLegion", - "loinsOfLegion", - "torsoOfLegion", - "armsOfLegion", - "headOfLegion" - ], - "bonuses" : [ - { - "type" : "CREATURE_GROWTH_PERCENT", - "val" : 50, - "propagator": "PLAYER_PROPAGATOR" - } - ] - }, - "powerOfTheDragonFather": - { - "index" : 134, - "type" : ["HERO"], - "bonuses" : [ - { - "type" : "LEVEL_SPELL_IMMUNITY", - "val" : 4, - "valueType" : "INDEPENDENT_MAX" - }, - { - "subtype" : "primSkill.attack", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.defence", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.spellpower", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "primSkill.knowledge", - "type" : "PRIMARY_SKILL", - "val" : 6, - "valueType" : "BASE_NUMBER" - } - ], - "components": - [ - "quietEyeOfTheDragon", - "redDragonFlameTongue", - "dragonScaleShield", - "dragonScaleArmor", - "dragonboneGreaves", - "dragonWingTabard", - "necklaceOfDragonteeth", - "crownOfDragontooth", - "stillEyeOfTheDragon" - ] - }, - "titansThunder": - { - "bonuses" : [ - { - "subtype" : "spell.titanBolt", - "type" : "SPELL", - "val" : 3, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 135, - "type" : ["HERO"], - "components": - [ - "titansGladius", - "sentinelsShield", - "thunderHelmet", - "titansCuirass" - ] - }, - "admiralsHat": - { - "onlyOnWaterMap" : true, - "bonuses" : [ - { - "type" : "FREE_SHIP_BOARDING", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 136, - "type" : ["HERO"], - "components": - [ - "necklaceOfOceanGuidance", - "seaCaptainsHat" - ] - }, - "bowOfTheSharpshooter": - { - "bonuses" : [ - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_DISTANCE_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "NO_WALL_PENALTY", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - }, - { - "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, - "type" : "FREE_SHOOTING", - "val" : 0, - "valueType" : "ADDITIVE_VALUE" - } - ], - "index" : 137, - "type" : ["HERO"], - "components": - [ - "bowOfElvenCherrywood", - "bowstringOfTheUnicornsMane", - "angelFeatherArrows" - ] - }, - "wizardsWell": - { - "bonuses" : [ - { - "type" : "FULL_MANA_REGENERATION", - "val" : 0, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 138, - "type" : ["HERO"], - "components": - [ - "charmOfMana", - "talismanOfMana", - "mysticOrbOfMana" - ] - }, - "ringOfTheMagi": - { - "bonuses" : [ - { - "type" : "SPELL_DURATION", - "val" : 50, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 139, - "type" : ["HERO"], - "components": - [ - "collarOfConjuring", - "ringOfConjuring", - "capeOfConjuring" - ] - }, - "cornucopia": - { - "bonuses" : [ - { - "subtype" : "resource.crystal", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.gems", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.mercury", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - }, - { - "subtype" : "resource.sulfur", - "type" : "GENERATE_RESOURCE", - "val" : 4, - "valueType" : "BASE_NUMBER" - } - ], - "index" : 140, - "type" : ["HERO"], - "components": - [ - "everflowingCrystalCloak", - "ringOfInfiniteGems", - "everpouringVialOfMercury", - "eversmokingRingOfSulfur" - ] - }, - // Misiokles: not having these 3 artifacts could lead to crashes. RMG will try to place them in some randoms maps (and crash the game) - "unusedArtifact1": - { - "index" : 141, - "class" : "SPECIAL", - "type" : ["CREATURE"] - }, - "unusedArtifact2": - { - "index" : 142, - "class" : "SPECIAL", - "type" : ["CREATURE"] - }, - "unusedArtifact3": - { - "index" : 143, - "class" : "SPECIAL", - "type" : ["CREATURE"] - } -} +{ + "spellBook": + { + "index" : 0, + "type" : ["HERO"] + }, + "spellScroll": + { + "index" : 1, + "type" : ["HERO"] + }, + "grail": + { + "index" : 2, + "type" : ["HERO"] + }, + "catapult": + { + "index" : 3, + "type" : ["HERO"], + "warMachine" : "catapult" + }, + "ballista": + { + "index" : 4, + "type" : ["HERO"], + "warMachine" : "ballista" + }, + "ammoCart": + { + "index" : 5, + "type" : ["HERO"], + "warMachine" : "ammoCart" + }, + "firstAidTent": + { + "index" : 6, + "type" : ["HERO"], + "warMachine" : "firstAidTent" + }, + "centaurAxe": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 7, + "type" : ["HERO"] + }, + "blackshardOfTheDeadKnight": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 8, + "type" : ["HERO"] + }, + "greaterGnollsFlail": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 9, + "type" : ["HERO"] + }, + "ogresClubOfHavoc": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 10, + "type" : ["HERO"] + }, + "swordOfHellfire": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 11, + "type" : ["HERO"] + }, + "titansGladius": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 12, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : -3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 12, + "type" : ["HERO"] + }, + "shieldOfTheDwarvenLords": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 13, + "type" : ["HERO"] + }, + "shieldOfTheYawningDead": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 14, + "type" : ["HERO"] + }, + "bucklerOfTheGnollKing": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 15, + "type" : ["HERO"] + }, + "targOfTheRampagingOgre": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 16, + "type" : ["HERO"] + }, + "shieldOfTheDamned": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 17, + "type" : ["HERO"] + }, + "sentinelsShield": + { + "bonuses" : [ + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 12, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : 0, + "type" : "PRIMARY_SKILL", + "val" : -3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 18, + "type" : ["HERO"] + }, + "helmOfTheAlabasterUnicorn": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 19, + "type" : ["HERO"] + }, + "skullHelmet": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 20, + "type" : ["HERO"] + }, + "helmOfChaos": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 21, + "type" : ["HERO"] + }, + "crownOfTheSupremeMagi": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 22, + "type" : ["HERO"] + }, + "hellstormHelmet": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 23, + "type" : ["HERO"] + }, + "thunderHelmet": + { + "bonuses" : [ + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 10, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : -2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 24, + "type" : ["HERO"] + }, + "breastplateOfPetrifiedWood": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 25, + "type" : ["HERO"] + }, + "ribCage": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 26, + "type" : ["HERO"] + }, + "scalesOfTheGreaterBasilisk": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 27, + "type" : ["HERO"] + }, + "tunicOfTheCyclopsKing": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 28, + "type" : ["HERO"] + }, + "breastplateOfBrimstone": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 29, + "type" : ["HERO"] + }, + "titansCuirass": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 10, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : -2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 30, + "type" : ["HERO"] + }, + "armorOfWonder": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 31, + "type" : ["HERO"] + }, + "sandalsOfTheSaint": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 32, + "type" : ["HERO"] + }, + "celestialNecklaceOfBliss": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 33, + "type" : ["HERO"] + }, + "lionsShieldOfCourage": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 34, + "type" : ["HERO"] + }, + "swordOfJudgement": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 35, + "type" : ["HERO"] + }, + "helmOfHeavenlyEnlightenment": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 36, + "type" : ["HERO"] + }, + "quietEyeOfTheDragon": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 37, + "type" : ["HERO"] + }, + "redDragonFlameTongue": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 38, + "type" : ["HERO"] + }, + "dragonScaleShield": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 39, + "type" : ["HERO"] + }, + "dragonScaleArmor": + { + "bonuses" : [ + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 40, + "type" : ["HERO"] + }, + "dragonboneGreaves": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 41, + "type" : ["HERO"] + }, + "dragonWingTabard": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 42, + "type" : ["HERO"] + }, + "necklaceOfDragonteeth": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 43, + "type" : ["HERO"] + }, + "crownOfDragontooth": + { + "bonuses" : [ + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 44, + "type" : ["HERO"] + }, + "stillEyeOfTheDragon": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 45, + "type" : ["HERO"] + }, + "cloverOfFortune": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 46, + "type" : ["HERO"] + }, + "cardsOfProphecy": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 47, + "type" : ["HERO"] + }, + "ladybirdOfLuck": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 48, + "type" : ["HERO"] + }, + "badgeOfCourage": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MIND_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 49, + "type" : ["HERO"] + }, + "crestOfValor": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 50, + "type" : ["HERO"] + }, + "glyphOfGallantry": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 51, + "type" : ["HERO"] + }, + "speculum": + { + "bonuses" : [ + { + "type" : "SIGHT_RADIUS", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 52, + "type" : ["HERO"] + }, + "spyglass": + { + "bonuses" : [ + { + "type" : "SIGHT_RADIUS", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 53, + "type" : ["HERO"] + }, + "amuletOfTheUndertaker": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 5, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 54, + "type" : ["HERO"] + }, + "vampiresCowl": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 10, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 55, + "type" : ["HERO"] + }, + "deadMansBoots": + { + "bonuses" : [ + { + "type" : "UNDEAD_RAISE_PERCENTAGE", + "val" : 15, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 56, + "type" : ["HERO"] + }, + "garnitureOfInterference": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "MAGIC_RESISTANCE", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 57, + "type" : ["HERO"] + }, + "surcoatOfCounterpoise": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "MAGIC_RESISTANCE", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 58, + "type" : ["HERO"] + }, + "bootsOfPolarity": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "MAGIC_RESISTANCE", + "val" : 15, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 59, + "type" : ["HERO"] + }, + "bowOfElvenCherrywood": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : 1, + "val" : 5, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + 1, + { + "type" : "SECONDARY_SKILL", + "id" : "skill.archery" + } + ] + } + ] + } + ], + "index" : 60, + "type" : ["HERO"] + }, + "bowstringOfTheUnicornsMane": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : 1, + "val" : 10, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + 1, + { + "type" : "SECONDARY_SKILL", + "id" : "skill.archery" + } + ] + } + ] + } + ], + "index" : 61, + "type" : ["HERO"] + }, + "angelFeatherArrows": + { + "bonuses" : [ + { + "type" : "PERCENTAGE_DAMAGE_BOOST", + "subtype" : 1, + "val" : 15, + "valueType" : "ADDITIVE_VALUE", + "limiters" : [ + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ + "PERCENTAGE_DAMAGE_BOOST", + 1, + { + "type" : "SECONDARY_SKILL", + "id" : "skill.archery" + } + ] + } + ] + } + ], + "index" : 62, + "type" : ["HERO"] + }, + "birdOfPerception": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 5, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 63, + "type" : ["HERO"] + }, + "stoicWatchman": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 10, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 64, + "type" : ["HERO"] + }, + "emblemOfCognizance": + { + "bonuses" : [ + { + "subtype" : 0, + "type" : "LEARN_BATTLE_SPELL_CHANCE", + "val" : 15, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 65, + "type" : ["HERO"] + }, + "statesmansMedal": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 66, + "type" : ["HERO"] + }, + "diplomatsRing": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 67, + "type" : ["HERO"] + }, + "ambassadorsSash": + { + "bonuses" : [ + { + "type" : "SURRENDER_DISCOUNT", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 68, + "type" : ["HERO"] + }, + "ringOfTheWayfarer": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 69, + "type" : ["HERO"] + }, + "equestriansGloves": + { + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : 1, + "val" : 300, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 70, + "type" : ["HERO"] + }, + "necklaceOfOceanGuidance": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : 0, + "val" : 1000, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 71, + "type" : ["HERO"] + }, + "angelWings": + { + "bonuses" : [ + { + "type" : "FLYING_MOVEMENT", + "val" : 0, + "valueType" : "INDEPENDENT_MIN" + } + ], + "index" : 72, + "type" : ["HERO"] + }, + "charmOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 73, + "type" : ["HERO"] + }, + "talismanOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 74, + "type" : ["HERO"] + }, + "mysticOrbOfMana": + { + "bonuses" : [ + { + "type" : "MANA_REGENERATION", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 75, + "type" : ["HERO"] + }, + "collarOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 76, + "type" : ["HERO"] + }, + "ringOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 77, + "type" : ["HERO"] + }, + "capeOfConjuring": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 78, + "type" : ["HERO"] + }, + "orbOfTheFirmament": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.air", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 79, + "type" : ["HERO"] + }, + "orbOfSilt": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.earth", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 80, + "type" : ["HERO"] + }, + "orbOfTempestuousFire": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.fire", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 81, + "type" : ["HERO"] + }, + "orbOfDrivingRain": + { + "bonuses" : [ + { + "type" : "SPELL_DAMAGE", + "subtype" : "spellSchool.water", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 82, + "type" : ["HERO"] + }, + "recantersCloak": + { + "index" : 83, + "type" : ["HERO"], + "bonuses": [ + { + "type" : "BLOCK_MAGIC_ABOVE", + "val" : 2, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ] + }, + "spiritOfOppression": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 84, + "type" : ["HERO"] + }, + "hourglassOfTheEvilHour": + { + "bonuses" : [ + { + "type" : "LUCK", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 85, + "type" : ["HERO"] + }, + "tomeOfFireMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : 1, + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 86, + "type" : ["HERO"] + }, + "tomeOfAirMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : 0, + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 87, + "type" : ["HERO"] + }, + "tomeOfWaterMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : 2, + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 88, + "type" : ["HERO"] + }, + "tomeOfEarthMagic": + { + "bonuses" : [ + { + "type" : "SPELLS_OF_SCHOOL", + "subtype" : 3, + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 89, + "type" : ["HERO"] + }, + "bootsOfLevitation": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "WATER_WALKING", + "val" : 0, + "valueType" : "INDEPENDENT_MIN" + } + ], + "index" : 90, + "type" : ["HERO"] + }, + "goldenBow": + { + "bonuses" : [ + { + "limiters" : ["SHOOTER_ONLY"], + "subtype" : 0, + "type" : "NO_DISTANCE_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "subtype" : 0, + "type" : "NO_WALL_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 91, + "type" : ["HERO"] + }, + "sphereOfPermanence": + { + "bonuses" : [ + { + "subtype" : 35, + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER", + "addInfo" : 1 + } + ], + "index" : 92, + "type" : ["HERO"] + }, + "orbOfVulnerability": + { + "bonuses" : [ + { + "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", + "subtype" : 0, + "val" : 0, + "valueType" : "BASE_NUMBER", + "propagator": "BATTLE_WIDE" + }, + { + "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", + "subtype" : 1, + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MAGIC_RESISTANCE", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + }, + { + "type" : "SPELL_RESISTANCE_AURA", + "val" : 0, + "valueType" : "INDEPENDENT_MIN", + "propagator": "BATTLE_WIDE" + } + ], + "index" : 93, + "type" : ["HERO"] + }, + "ringOfVitality": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 1, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 94, + "type" : ["HERO"] + }, + "ringOfLife": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 1, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 95, + "type" : ["HERO"] + }, + "vialOfLifeblood": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 2, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 96, + "type" : ["HERO"] + }, + "necklaceOfSwiftness": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 1, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 97, + "type" : ["HERO"] + }, + "bootsOfSpeed": + { + "bonuses" : [ + { + "type" : "MOVEMENT", + "subtype" : 1, + "val" : 600, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 98, + "type" : ["HERO"] + }, + "capeOfVelocity": + { + "bonuses" : [ + { + "type" : "STACKS_SPEED", + "val" : 2, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 99, + "type" : ["HERO"] + }, + "pendantOfDispassion": + { + "bonuses" : [ + { + "subtype" : "spell.berserk", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 100, + "type" : ["HERO"] + }, + "pendantOfSecondSight": + { + "bonuses" : [ + { + "subtype" : "spell.blind", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 101, + "type" : ["HERO"] + }, + "pendantOfHoliness": + { + "bonuses" : [ + { + "subtype" : "spell.curse", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 102, + "type" : ["HERO"] + }, + "pendantOfLife": + { + "bonuses" : [ + { + "subtype" : "spell.deathRipple", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 103, + "type" : ["HERO"] + }, + "pendantOfDeath": + { + "bonuses" : [ + { + "limiters" : ["IS_UNDEAD"], + "subtype" : "spell.destroyUndead", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 104, + "type" : ["HERO"] + }, + "pendantOfFreeWill": + { + "bonuses" : [ + { + "subtype" : "spell.hypnotize", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 105, + "type" : ["HERO"] + }, + "pendantOfNegativity": + { + "bonuses" : [ + { + "subtype" : "spell.lightningBolt", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.chainLightning", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 106, + "type" : ["HERO"] + }, + "pendantOfTotalRecall": + { + "bonuses" : [ + { + "subtype" : "spell.forgetfulness", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 107, + "type" : ["HERO"] + }, + "pendantOfCourage": + { + "bonuses" : [ + { + "type" : "MORALE", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "LUCK", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 108, + "type" : ["HERO"] + }, + "everflowingCrystalCloak": + { + "bonuses" : [ + { + "subtype" : "resource.crystal", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 109, + "type" : ["HERO"] + }, + "ringOfInfiniteGems": + { + "bonuses" : [ + { + "subtype" : "resource.gems", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 110, + "type" : ["HERO"] + }, + "everpouringVialOfMercury": + { + "bonuses" : [ + { + "subtype" : "resource.mercury", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 111, + "type" : ["HERO"] + }, + "inexhaustibleCartOfOre": + { + "bonuses" : [ + { + "subtype" : "resource.ore", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 112, + "type" : ["HERO"] + }, + "eversmokingRingOfSulfur": + { + "bonuses" : [ + { + "subtype" : "resource.sulfur", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 113, + "type" : ["HERO"] + }, + "inexhaustibleCartOfLumber": + { + "bonuses" : [ + { + "subtype" : "resource.wood", + "type" : "GENERATE_RESOURCE", + "val" : 1, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 114, + "type" : ["HERO"] + }, + "endlessSackOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 1000, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 115, + "type" : ["HERO"] + }, + "endlessBagOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 750, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 116, + "type" : ["HERO"] + }, + "endlessPurseOfGold": + { + "bonuses" : [ + { + "subtype" : "resource.gold", + "type" : "GENERATE_RESOURCE", + "val" : 500, + "valueType" : "BASE_NUMBER", + "stacking" : "ALWAYS" + } + ], + "index" : 117, + "type" : ["HERO"] + }, + "legsOfLegion": + { + "index" : 118, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : 1, + "val" : 5, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "loinsOfLegion": + { + "index" : 119, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : 2, + "val" : 4, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "torsoOfLegion": + { + "index" : 120, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : 3, + "val" : 3, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "armsOfLegion": + { + "index" : 121, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : 4, + "val" : 2, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "headOfLegion": + { + "index" : 122, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH", + "subtype" : 5, + "val" : 1, + "propagator": "VISITED_TOWN_AND_VISITOR" + } + ] + }, + "seaCaptainsHat": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "WHIRLPOOL_PROTECTION", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "type" : "MOVEMENT", + "subtype" : 0, + "val" : 500, + "valueType" : "ADDITIVE_VALUE" + }, + { + "subtype" : "spell.summonBoat", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "spell.scuttleBoat", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + } + ], + "index" : 123, + "type" : ["HERO"] + }, + "spellbindersHat": + { + "bonuses" : [ + { + "subtype" : 5, + "type" : "SPELLS_OF_LEVEL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 124, + "type" : ["HERO"] + }, + "shacklesOfWar": + { + "index" : 125, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "BATTLE_NO_FLEEING", + "propagator": "BATTLE_WIDE" + } + ] + }, + "orbOfInhibition": + { + "index" : 126, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "BLOCK_ALL_MAGIC", + "propagator": "BATTLE_WIDE" + } + ] + }, + "vialOfDragonBlood": + { + "bonuses" : [ + { + "limiters" : ["DRAGON_NATURE"], + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + }, + { + "limiters" : ["DRAGON_NATURE"], + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 5, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 127, + "type" : ["HERO"] + }, + "armageddonsBlade": + { + "bonuses" : [ + { + "subtype" : "spell.armageddon", + "type" : "SPELL", + "val" : 3, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "spell.armageddon", + "type" : "SPELL_IMMUNITY", + "val" : 0, + "valueType" : "BASE_NUMBER", + "addInfo" : 1 + }, + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 3, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 128, + "type" : ["HERO"] + }, + "angelicAlliance": + { + "bonuses" : [ + { + "type" : "NONEVIL_ALIGNMENT_MIX", + "val" : 0, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.prayer", + "type" : "OPENING_BATTLE_SPELL", + "val" : 10, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 129, + "type" : ["HERO"], + "components": + [ + "armorOfWonder", + "sandalsOfTheSaint", + "celestialNecklaceOfBliss", + "lionsShieldOfCourage", + "swordOfJudgement", + "helmOfHeavenlyEnlightenment" + ] + }, + "cloakOfTheUndeadKing": + { + "bonuses" : [ + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.skeleton", + "addInfo" : 0 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.wight", + "addInfo" : 2 + }, + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.lich", + "addInfo" : 3 + } + ], + "index" : 130, + "type" : ["HERO"], + "components": + [ + "amuletOfTheUndertaker", + "vampiresCowl", + "deadMansBoots" + ] + }, + "elixirOfLife": + { + "bonuses" : [ + { + "type" : "STACK_HEALTH", + "val" : 25, + "valueType" : "PERCENT_TO_BASE", + "limiters" : [ + "noneOf", + "IS_UNDEAD", + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "NON_LIVING" ] + }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "GARGOYLE" ] + } + ] + }, + { + "type" : "HP_REGENERATION", + "val" : 50, + "valueType" : "BASE_NUMBER", + "limiters" : [ + "noneOf", + "IS_UNDEAD", + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "NON_LIVING" ] + }, + { + "type" : "HAS_ANOTHER_BONUS_LIMITER", + "parameters" : [ "GARGOYLE" ] + } + ] + } + ], + "index" : 131, + "type" : ["HERO"], + "components": + [ + "ringOfVitality", + "ringOfLife", + "vialOfLifeblood" + ] + }, + "armorOfTheDamned": + { + "bonuses" : [ + { + "subtype" : "spell.slow", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.curse", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.weakness", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "spell.misfortune", + "type" : "OPENING_BATTLE_SPELL", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 132, + "type" : ["HERO"], + "components": + [ + "blackshardOfTheDeadKnight", + "shieldOfTheYawningDead", + "skullHelmet", + "ribCage" + ] + }, + "statueOfLegion": + { + "index" : 133, + "type" : ["HERO"], + "components": + [ + "legsOfLegion", + "loinsOfLegion", + "torsoOfLegion", + "armsOfLegion", + "headOfLegion" + ], + "bonuses" : [ + { + "type" : "CREATURE_GROWTH_PERCENT", + "val" : 50, + "propagator": "PLAYER_PROPAGATOR" + } + ] + }, + "powerOfTheDragonFather": + { + "index" : 134, + "type" : ["HERO"], + "bonuses" : [ + { + "type" : "LEVEL_SPELL_IMMUNITY", + "val" : 4, + "valueType" : "INDEPENDENT_MAX" + }, + { + "subtype" : "primSkill.attack", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.defence", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.spellpower", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "primSkill.knowledge", + "type" : "PRIMARY_SKILL", + "val" : 6, + "valueType" : "BASE_NUMBER" + } + ], + "components": + [ + "quietEyeOfTheDragon", + "redDragonFlameTongue", + "dragonScaleShield", + "dragonScaleArmor", + "dragonboneGreaves", + "dragonWingTabard", + "necklaceOfDragonteeth", + "crownOfDragontooth", + "stillEyeOfTheDragon" + ] + }, + "titansThunder": + { + "bonuses" : [ + { + "subtype" : "spell.titanBolt", + "type" : "SPELL", + "val" : 3, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 135, + "type" : ["HERO"], + "components": + [ + "titansGladius", + "sentinelsShield", + "thunderHelmet", + "titansCuirass" + ] + }, + "admiralsHat": + { + "onlyOnWaterMap" : true, + "bonuses" : [ + { + "type" : "FREE_SHIP_BOARDING", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 136, + "type" : ["HERO"], + "components": + [ + "necklaceOfOceanGuidance", + "seaCaptainsHat" + ] + }, + "bowOfTheSharpshooter": + { + "bonuses" : [ + { + "limiters" : ["SHOOTER_ONLY"], + "subtype" : 0, + "type" : "NO_DISTANCE_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "subtype" : 0, + "type" : "NO_WALL_PENALTY", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + }, + { + "limiters" : ["SHOOTER_ONLY"], + "subtype" : 0, + "type" : "FREE_SHOOTING", + "val" : 0, + "valueType" : "ADDITIVE_VALUE" + } + ], + "index" : 137, + "type" : ["HERO"], + "components": + [ + "bowOfElvenCherrywood", + "bowstringOfTheUnicornsMane", + "angelFeatherArrows" + ] + }, + "wizardsWell": + { + "bonuses" : [ + { + "type" : "FULL_MANA_REGENERATION", + "val" : 0, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 138, + "type" : ["HERO"], + "components": + [ + "charmOfMana", + "talismanOfMana", + "mysticOrbOfMana" + ] + }, + "ringOfTheMagi": + { + "bonuses" : [ + { + "type" : "SPELL_DURATION", + "val" : 50, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 139, + "type" : ["HERO"], + "components": + [ + "collarOfConjuring", + "ringOfConjuring", + "capeOfConjuring" + ] + }, + "cornucopia": + { + "bonuses" : [ + { + "subtype" : "resource.crystal", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.gems", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.mercury", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + }, + { + "subtype" : "resource.sulfur", + "type" : "GENERATE_RESOURCE", + "val" : 4, + "valueType" : "BASE_NUMBER" + } + ], + "index" : 140, + "type" : ["HERO"], + "components": + [ + "everflowingCrystalCloak", + "ringOfInfiniteGems", + "everpouringVialOfMercury", + "eversmokingRingOfSulfur" + ] + }, + // Misiokles: not having these 3 artifacts could lead to crashes. RMG will try to place them in some randoms maps (and crash the game) + "unusedArtifact1": + { + "index" : 141, + "class" : "SPECIAL", + "type" : ["CREATURE"] + }, + "unusedArtifact2": + { + "index" : 142, + "class" : "SPECIAL", + "type" : ["CREATURE"] + }, + "unusedArtifact3": + { + "index" : 143, + "class" : "SPECIAL", + "type" : ["CREATURE"] + } +} diff --git a/config/bonuses.json b/config/bonuses.json index 69017e7d9..877161cf8 100644 --- a/config/bonuses.json +++ b/config/bonuses.json @@ -1,572 +1,572 @@ -//TODO: selector-based config -// school immunities -// LEVEL_SPELL_IMMUNITY - -{ - "ADDITIONAL_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DOUBLE" - } - }, - - "ADDITIONAL_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL1" - } - }, - - "ATTACKS_ALL_ADJACENT": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_ROUND" - } - }, - - "BLOCKS_RANGED_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/RANGEDBLOCK" - } - }, - - "BLOCKS_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL" - } - }, - - "CATAPULT": - { - "graphics": - { - "icon": "zvs/Lib1.res/Catapult" - } - }, - - "CATAPULT_EXTRA_SHOTS": - { - "hidden": true - }, - - "CHANGES_SPELL_COST_FOR_ALLY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MANA" - } - }, - - "CHANGES_SPELL_COST_FOR_ENEMY": - { - "graphics": - { - "icon": "zvs/Lib1.res/MagicDamper" - } - }, - - "CHARGE_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/ChargeImmune" - } - }, - - "DARKNESS": - { - }, - - "DEATH_STARE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DEATH" - } - }, - - "DEFENSIVE_STANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DEFBON" - } - }, - - "DESTRUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/DESTROYER" - } - }, - - "DOUBLE_DAMAGE_CHANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DBLOW" - } - }, - - "DRAGON_NATURE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DRAGON" - } - }, - - "DISGUISED": - { - "hidden": true - }, - - "ENCHANTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST1" - } - }, - - "ENCHANTED": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_BLESS" - } - }, - - "ENEMY_DEFENCE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RDEF" - } - }, - - "FIRE_SHIELD": - { - "graphics": - { - "icon": "zvs/Lib1.res/FireShield" - } - }, - - "FIRST_STRIKE": - { - "graphics": - { - "icon": "zvs/Lib1.res/FIRSTSTRIKE" - } - }, - - "FEAR": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FEAR" - } - }, - - "FEARLESS": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FEARL" - } - }, - - "FLYING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_FLY" - } - - }, - - "FREE_SHOOTING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOTA" - } - - }, - - "GARGOYLE": - { - "graphics": - { - "icon": "zvs/Lib1.res/NonLiving" // Just use the NonLiving icon for now - } - }, - - "GENERAL_DAMAGE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/DamageReductionMelee" - } - }, - - "HATE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_HATE" - } - }, - - "HEALER": - { - "graphics": - { - "icon": "zvs/Lib1.res/Healer" - } - }, - - "HP_REGENERATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_TROLL" - } - }, - - "JOUSTING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CHAMP" - } - }, - - "KING": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_KING3" - } - }, - - "LEARN_BATTLE_SPELL_CHANCE": - { - "hidden": true - }, - - "LEARN_BATTLE_SPELL_LEVEL_LIMIT": - { - "hidden": true - }, - - "LEVEL_SPELL_IMMUNITY": - { - "graphics": - { - "icon": "" - } - }, - - "LIFE_DRAIN": - { - "graphics": - { - "icon": "zvs/Lib1.res/DrainLife" - } - }, - - "LIMITED_SHOOTING_RANGE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOT" - } - }, - - "MANA_CHANNELING": - { - "graphics": - { - "icon": "zvs/Lib1.res/ManaChannel" - } - }, - - "MANA_DRAIN": - { - "graphics": - { - "icon": "zvs/Lib1.res/ManaDrain" - } - }, - - "MAGIC_MIRROR": - { - "graphics": - { - "icon": "zvs/Lib1.res/MagicMirror" - } - }, - - "MAGIC_RESISTANCE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DWARF" - } - }, - - "MIND_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MIND" - } - }, - - "NONE": - { - "hidden": true - }, - - "NO_DISTANCE_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_DIST" - } - }, - - "NO_MELEE_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MELEE" - } - }, - - "NO_MORALE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_MORAL" - } - }, - - "NO_WALL_PENALTY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_OBST" - } - }, - - "NO_TERRAIN_PENALTY": - { - "hidden": true - }, - - "NON_LIVING": - { - "graphics": - { - "icon": "zvs/Lib1.res/NonLiving" - } - }, - - "RANDOM_SPELLCASTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/RandomBoost" - } - }, - - "PERCENTAGE_DAMAGE_BOOST": - { - "hidden": true - }, - - "RANGED_RETALIATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/RANGEDCOUNTER" - } - }, - - "RECEPTIVE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_NOFRIM" - } - }, - - "REBIRTH": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_REBIRTH" - } - }, - - "RETURN_AFTER_STRIKE": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_HARPY" - } - }, - - "SHOOTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SHOOT" - } - }, - - "SHOOTS_ALL_ADJACENT": - { - "graphics": - { - "icon": "zvs/Lib1.res/AREASHOT" - } - }, - - "SOUL_STEAL": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SUMMON2" - } - }, - - "SPELLCASTER": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CASTER" - } - }, - - "SPELL_AFTER_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST" - } - }, - - "SPELL_BEFORE_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_CAST2" - } - }, - - "SPELL_DAMAGE_REDUCTION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_GOLEM" - } - }, - - "SPELL_IMMUNITY": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPDISB" //todo: configurable use from spell handler - } - }, - - "SPELL_LIKE_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SPDFIRE" - } - }, - - "SPELL_RESISTANCE_AURA": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_UNIC" - } - }, - - "SUMMON_GUARDIANS": - { - "graphics": - { - "icon": "zvs/Lib1.res/SUMMONGUARDS" - } - }, - - "TWO_HEX_ATTACK_BREATH": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_BREATH" - } - }, - - "THREE_HEADED_ATTACK": - { - "graphics": - { - "icon": "zvs/Lib1.res/ThreeHeaded" - } - }, - - "TRANSMUTATION": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_SGTYPE" - } - }, - - "UNDEAD": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_UNDEAD" - } - }, - - "UNLIMITED_RETALIATIONS": - { - "graphics": - { - "icon": "zvs/Lib1.res/E_RETAIL1" - } - }, - - "VISIONS": - { - "hidden": true - }, - - "WIDE_BREATH": - { - "graphics": - { - "icon": "zvs/Lib1.res/MEGABREATH" - } - } -} - +//TODO: selector-based config +// school immunities +// LEVEL_SPELL_IMMUNITY + +{ + "ADDITIONAL_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DOUBLE" + } + }, + + "ADDITIONAL_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL1" + } + }, + + "ATTACKS_ALL_ADJACENT": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_ROUND" + } + }, + + "BLOCKS_RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDBLOCK" + } + }, + + "BLOCKS_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL" + } + }, + + "CATAPULT": + { + "graphics": + { + "icon": "zvs/Lib1.res/Catapult" + } + }, + + "CATAPULT_EXTRA_SHOTS": + { + "hidden": true + }, + + "CHANGES_SPELL_COST_FOR_ALLY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MANA" + } + }, + + "CHANGES_SPELL_COST_FOR_ENEMY": + { + "graphics": + { + "icon": "zvs/Lib1.res/MagicDamper" + } + }, + + "CHARGE_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/ChargeImmune" + } + }, + + "DARKNESS": + { + }, + + "DEATH_STARE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DEATH" + } + }, + + "DEFENSIVE_STANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DEFBON" + } + }, + + "DESTRUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/DESTROYER" + } + }, + + "DOUBLE_DAMAGE_CHANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DBLOW" + } + }, + + "DRAGON_NATURE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DRAGON" + } + }, + + "DISGUISED": + { + "hidden": true + }, + + "ENCHANTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST1" + } + }, + + "ENCHANTED": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_BLESS" + } + }, + + "ENEMY_DEFENCE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RDEF" + } + }, + + "FIRE_SHIELD": + { + "graphics": + { + "icon": "zvs/Lib1.res/FireShield" + } + }, + + "FIRST_STRIKE": + { + "graphics": + { + "icon": "zvs/Lib1.res/FIRSTSTRIKE" + } + }, + + "FEAR": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FEAR" + } + }, + + "FEARLESS": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FEARL" + } + }, + + "FLYING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_FLY" + } + + }, + + "FREE_SHOOTING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOTA" + } + + }, + + "GARGOYLE": + { + "graphics": + { + "icon": "zvs/Lib1.res/NonLiving" // Just use the NonLiving icon for now + } + }, + + "GENERAL_DAMAGE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/DamageReductionMelee" + } + }, + + "HATE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_HATE" + } + }, + + "HEALER": + { + "graphics": + { + "icon": "zvs/Lib1.res/Healer" + } + }, + + "HP_REGENERATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_TROLL" + } + }, + + "JOUSTING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CHAMP" + } + }, + + "KING": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_KING3" + } + }, + + "LEARN_BATTLE_SPELL_CHANCE": + { + "hidden": true + }, + + "LEARN_BATTLE_SPELL_LEVEL_LIMIT": + { + "hidden": true + }, + + "LEVEL_SPELL_IMMUNITY": + { + "graphics": + { + "icon": "" + } + }, + + "LIFE_DRAIN": + { + "graphics": + { + "icon": "zvs/Lib1.res/DrainLife" + } + }, + + "LIMITED_SHOOTING_RANGE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOT" + } + }, + + "MANA_CHANNELING": + { + "graphics": + { + "icon": "zvs/Lib1.res/ManaChannel" + } + }, + + "MANA_DRAIN": + { + "graphics": + { + "icon": "zvs/Lib1.res/ManaDrain" + } + }, + + "MAGIC_MIRROR": + { + "graphics": + { + "icon": "zvs/Lib1.res/MagicMirror" + } + }, + + "MAGIC_RESISTANCE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DWARF" + } + }, + + "MIND_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MIND" + } + }, + + "NONE": + { + "hidden": true + }, + + "NO_DISTANCE_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_DIST" + } + }, + + "NO_MELEE_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MELEE" + } + }, + + "NO_MORALE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_MORAL" + } + }, + + "NO_WALL_PENALTY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_OBST" + } + }, + + "NO_TERRAIN_PENALTY": + { + "hidden": true + }, + + "NON_LIVING": + { + "graphics": + { + "icon": "zvs/Lib1.res/NonLiving" + } + }, + + "RANDOM_SPELLCASTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/RandomBoost" + } + }, + + "PERCENTAGE_DAMAGE_BOOST": + { + "hidden": true + }, + + "RANGED_RETALIATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/RANGEDCOUNTER" + } + }, + + "RECEPTIVE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_NOFRIM" + } + }, + + "REBIRTH": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_REBIRTH" + } + }, + + "RETURN_AFTER_STRIKE": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_HARPY" + } + }, + + "SHOOTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SHOOT" + } + }, + + "SHOOTS_ALL_ADJACENT": + { + "graphics": + { + "icon": "zvs/Lib1.res/AREASHOT" + } + }, + + "SOUL_STEAL": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SUMMON2" + } + }, + + "SPELLCASTER": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CASTER" + } + }, + + "SPELL_AFTER_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST" + } + }, + + "SPELL_BEFORE_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_CAST2" + } + }, + + "SPELL_DAMAGE_REDUCTION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_GOLEM" + } + }, + + "SPELL_IMMUNITY": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SPDISB" //todo: configurable use from spell handler + } + }, + + "SPELL_LIKE_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SPDFIRE" + } + }, + + "SPELL_RESISTANCE_AURA": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_UNIC" + } + }, + + "SUMMON_GUARDIANS": + { + "graphics": + { + "icon": "zvs/Lib1.res/SUMMONGUARDS" + } + }, + + "TWO_HEX_ATTACK_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_BREATH" + } + }, + + "THREE_HEADED_ATTACK": + { + "graphics": + { + "icon": "zvs/Lib1.res/ThreeHeaded" + } + }, + + "TRANSMUTATION": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_SGTYPE" + } + }, + + "UNDEAD": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_UNDEAD" + } + }, + + "UNLIMITED_RETALIATIONS": + { + "graphics": + { + "icon": "zvs/Lib1.res/E_RETAIL1" + } + }, + + "VISIONS": + { + "hidden": true + }, + + "WIDE_BREATH": + { + "graphics": + { + "icon": "zvs/Lib1.res/MEGABREATH" + } + } +} + diff --git a/config/campaignMedia.json b/config/campaignMedia.json index a06a9afa8..c54185837 100644 --- a/config/campaignMedia.json +++ b/config/campaignMedia.json @@ -1,239 +1,239 @@ -{ - "videos": [ - //Restoration of Erathia - //Long live the Queen - "GOOD1A.SMK", //Good1_a - "GOOD1B.SMK", //Good1_b - "GOOD1C.SMK", //Good1_c - //Dungeons and devils - "EVIL1A.SMK", //Evil1_a - "EVIL1B.SMK", //Evil1_b - "EVIL1C.SMK", //Evil1_c - //Spoils of War - "NEUTRALA.SMK", //Neutral1_a - "NEUTRALB.SMK", //Neutral1_b - "NEUTRALC.SMK", //Neutral1_c - //Liberation - "GOOD2A.SMK", //Good2_a - "GOOD2B.SMK", //Good2_b - "GOOD2C.SMK", //Good2_c - "GOOD2D.SMK", //Good2_d - //Long Live the King - "EVIL2A.SMK", //Evil2_a - "EVIL2AP1.SMK", //Evil2ap1 - "EVIL2B.SMK", //Evil2_b - "EVIL2C.SMK", //Evil2_c - "EVIL2D.SMK", //Evil2_d - //Song for the Father - "GOOD3A.SMK", //Good3_a - "GOOD3B.SMK", //Good3_b - "GOOD3C.SMK", //Good3_c - //Seeds Of Discontent - "SECRETA.SMK", //Secret_a - "SECRETB.SMK", //Secret_b - "SECRETC.SMK", //Secret_c - //Armageddon's Blade - //Armageddon's Blade - "H3ABab1.smk", //ArmageddonsBlade_a - "H3ABab2.smk", //ArmageddonsBlade_b - "H3ABab3.smk", //ArmageddonsBlade_c - "H3ABab4.smk", //ArmageddonsBlade_d - "H3ABab5.smk", //ArmageddonsBlade_e - "H3ABab6.smk", //ArmageddonsBlade_f - "H3ABab7.smk", //ArmageddonsBlade_g - "H3ABab8.smk", //ArmageddonsBlade_h - "H3ABab9.smk", //ArmageddonsBlade_end - //Dragon's Blood - "H3ABdb1.smk", //DragonsBlood_a - "H3ABdb2.smk", //DragonsBlood_b - "H3ABdb3.smk", //DragonsBlood_c - "H3ABdb4.smk", //DragonsBlood_d - "H3ABdb5.smk", //DragonsBlood_end - //Dragon Slayer - "H3ABds1.smk", //DragonSlayer_a - "H3ABds2.smk", //DragonSlayer_b - "H3ABds3.smk", //DragonSlayer_c - "H3ABds4.smk", //DragonSlayer_d - "H3ABds5.smk", //DragonSlayer_end - //Festival of Life - "H3ABfl1.smk", //FestivalOfLife_a - "H3ABfl2.smk", //FestivalOfLife_b - "H3ABfl3.smk", //FestivalOfLife_c - "H3ABfl4.smk", //FestivalOfLife_d - "H3ABfl5.smk", //FestivalOfLife_end - //Foolhardy Waywardness - "H3ABfw1.smk", //FoolhardyWaywardness_a - "H3ABfw2.smk", //FoolhardyWaywardness_b - "H3ABfw3.smk", //FoolhardyWaywardness_c - "H3ABfw4.smk", //FoolhardyWaywardness_d - "H3ABfw5.smk", //FoolhardyWaywardness_end - //Playing with Fire - "H3ABpf1.smk", //PlayingWithFire_a - "H3ABpf2.smk", //PlayingWithFire_b - "3ABpf3.smk", //PlayingWithFire_c - "H3ABpf4.smk", //PlayingWithFire_end - //Shadow of Death Campaigns - //Birth of a Barbarian - "H3x2_BBa.smk", //BirthOfABarbarian_a - "H3x2_BBb.smk", //BirthOfABarbarian_b - "H3x2_BBc.smk", //BirthOfABarbarian_c - "H3x2_BBd.smk", //BirthOfABarbarian_d - "H3x2_BBe.smk", //BirthOfABarbarian_e - "H3x2_BBf.smk", //BirthOfABarbarian_end - //Elixir of Life - "H3x2_Ela.smk", //ElixirOfLife_a - "H3x2_Elb.smk", //ElixirOfLife_b - "H3x2_Elc.smk", //ElixirOfLife_c - "H3x2_Eld.smk", //ElixirOfLife_d - "H3x2_Ele.smk", //ElixirOfLife_end - //Hack and Slash - "H3x2_HSa.smk", //HackAndSlash_a - "EVIL2C.SMK", //HackAndSlash_b - "H3x2_HSc.smk", //HackAndSlash_c - "H3x2_HSd.smk", //HackAndSlash_d - "H3x2_HSe.smk", //HackAndSlash_end - //New Beginning - "H3x2_NBa.smk", //NewBeginning_a - "H3x2_NBb.smk", //NewBeginning_b - "H3x2_Nbc.smk", //NewBeginning_c - "H3x2_Nbd.smk", //NewBeginning_d - "H3x2_Nbe.smk", //NewBeginning_end - //Rise of the Necromancer - "H3x2_RNa.smk", //RiseOfTheNecromancer_a - "H3x2_RNb.smk", //RiseOfTheNecromancer_b - "H3x2_RNc.smk", //RiseOfTheNecromancer_c - "H3x2_RNd.smk", //RiseOfTheNecromancer_d - "H3x2_RNe1.smk", //RiseOfTheNecromancer_end - //Spectre of Power - "H3x2_SPa.smk", //SpectreOfPower_a - "H3x2_SPb.smk", //SpectreOfPower_b - "H3x2_SPc.smk", //SpectreOfPower_c - "H3x2_SPd.smk", //SpectreOfPower_d - "H3x2_SPe.smk", //SpectreOfPower_end - //Unholy Alliance - "H3x2_UAa.smk", //UnholyAlliance_a - "H3x2_UAb.smk", //UnholyAlliance_b - "H3x2_UAc.smk", //UnholyAlliance_c - "H3x2_UAd.smk", //UnholyAlliance_d - "H3x2_UAe.smk", //UnholyAlliance_e - "H3x2_UAf.smk", //UnholyAlliance_f - "H3x2_UAg.smk", //UnholyAlliance_g - "H3x2_UAh.smk", //UnholyAlliance_h - "H3x2_UAi.smk", //UnholyAlliance_i - "H3x2_UAj.smk", //UnholyAlliance_j - "H3x2_UAk.smk", //UnholyAlliance_k - "H3x2_UAl.smk", //UnholyAlliance_l - "H3x2_UAm.smk", //UnholyAlliance_end //H3x2_UAm.bik? - ], - - "music" : [ - // Use CmpMusic.txt from H3 instead - ], - - "voice" : [ - //Restoration of Erathia - "G1A", //Long live the Queen 1 - "G1B", //Long live the Queen 2 - "G1C", //Long live the Queen 3 - "E1A.wav", //Dungeons and Devils 1 - "E1B.wav", //Dungeons and Devils 2 - "E1C.wav", //Dungeons and Devils 3 - "N1A", //Spoils of War 1 - "N1B", //Spoils of War 2 - "N1C_D", //Spoils of War 3 - "G2A", //Liberation 1 - "G2B", //Liberation 2 - "G2C", //Liberation 3 - "G2D", //Liberation 4 - "E2A.wav", //Long live the King 1 - "E2AE.wav", //Long live the King 1end - "E2B.wav", //Long live the King 2 - "E2C.wav", //Long live the King 3 - "E2D.wav", //Long live the King 4 - "G3A", //Song for the Father 1 - "G3B", //Song for the Father 2 - "G3C", //Song for the Father 3 - "S1A", //Seeds of discontent 1 - "S1B", //Seeds of discontent 2 - "S1C", //Seeds of discontent 3 - //Armageddon's Blade - "ABvoAB1.wav", //Armageddon's Blade 1 - "ABvoAB2.wav", //Armageddon's Blade 2 - "ABvoAB3.wav", //Armageddon's blade 3 - "ABvoAB4.wav", //Armageddon's blade 4 - "ABvoAB5.wav", //Armageddon's blade 5 - "ABvoAB6.wav", //Armageddon's blade 6 - "ABvoAB7.wav", //Armageddon's blade 7 - "ABvoAB8.wav", //Armageddon's blade 8 - "ABvoAB9.wav", //Armageddon's blade 8end - "ABvoDB1.wav", //Dragon's Blood 1 - "ABvoDB2.wav", //Dragon's Blood 2 - "ABvoDB3.wav", //Dragon's Blood 3 - "ABvoDB4.wav", //Dragon's Blood 4 - "ABvoDB5.wav", //Dragon's Blood 4end - "ABvoDS1.wav", //Dragon Slayer 1 - "ABvoDS2.wav", //Dragon Slayer 2 - "ABvoDS3.wav", //Dragon Slayer 3 - "ABvoDS4.wav", //Dragon Slayer 4 - "ABvoDS5.wav", //Dragon Slayer 4end - "ABvoFL1.wav", //Festival of Life 1 - "ABvoFL2.wav", //Festival of Life 2 - "ABvoFL3.wav", //Festival of Life 3 - "ABvoFL4.wav", //Festival of Life 4 - "ABvoFL5.wav", //Festival of Life 4end - "ABvoFW1.wav", //Foolhardy Waywardness 1 - "ABvoFW2.wav", //Foolhardy Waywardness 2 - "ABvoFW3.wav", //Foolhardy Waywardness 3 - "ABvoFW4.wav", //Foolhardy Waywardness 4 - "ABvoFW5.wav", //Foolhardy Waywardness 4end - "ABvoPF1.wav", //Playing with Fire 1 - "ABvoPF2.wav", //Playing with Fire 2 - "ABvoPF3.wav", //Playing with Fire 3 - "ABvoPF4.wav", //Playing with Fire 3end - //Shadow of Death Campaigns - "H3x2BBa", //Birth of a Barbarian 1 - "H3x2BBb", //Birth of a Barbarian 2 - "H3x2BBc", //Birth of a Barbarian 3 - "H3x2BBd", //Birth of a Barbarian 4 - "H3x2BBe", //Birth of a Barbarian 5 - "H3x2BBf", //Birth of a Barbarian 5end - "H3x2ELa", //Elixir of life 1 - "H3x2ELb", //Elixir of life 2 - "H3x2ELc", //Elixir of life 3 - "H3x2ELd", //Elixir of life 4 - "H3x2ELe", //Elixir of life 4end - "H3x2HSa", //Hack and Slash 1 - "H3x2HSb", //Hack and Slash 2 - "H3x2HSc", //Hack and Slash 3 - "H3x2HSd", //Hack and Slash 4 - "H3x2HSe", //Hack and Slash 4end - "H3x2NBa", //New Beginning 1 - "H3x2NBb", //New Beginning 2 - "H3x2NBc", //New Beginning 3 - "H3x2NBd", //New Beginning 4 - "H3x2NBe", //New Beginning 4end - "H3x2RNa", //Rise of the Necromancer 1 - "H3x2RNb", //Rise of the Necromancer 2 - "H3x2RNc", //Rise of the Necromancer 3 - "H3x2RNd", //Rise of the Necromancer 4 - "H3x2RNe", //Rise of the Necromancer 4end - "H3x2SPa", //Spectre of Power 1 - "H3x2Spb", //Spectre of Power 2 - "H3x2Spc", //Spectre of Power 3 - "H3x2Spd", //Spectre of Power 4 - "H3x2Spe", //Spectre of Power 4end - "H3x2UAa", //Unholy alliance 1 - "H3x2UAb", //Unholy alliance 2 - "H3x2UAc", //Unholy alliance 3 - "H3x2UAd", //Unholy alliance 4 - "H3x2UAe", //Unholy alliance 5 - "H3x2UAf", //Unholy alliance 6 - "H3x2UAg", //Unholy alliance 7 - "H3x2UAh", //Unholy alliance 8 - "H3x2UAi", //Unholy alliance 9 - "H3x2UAj", //Unholy alliance 10 - "H3x2UAk", //Unholy alliance 11 - "H3x2UAl", //Unholy alliance 12 - "H3x2UAm" //Unholy alliance 12end - ] -} +{ + "videos": [ + //Restoration of Erathia + //Long live the Queen + "GOOD1A.SMK", //Good1_a + "GOOD1B.SMK", //Good1_b + "GOOD1C.SMK", //Good1_c + //Dungeons and devils + "EVIL1A.SMK", //Evil1_a + "EVIL1B.SMK", //Evil1_b + "EVIL1C.SMK", //Evil1_c + //Spoils of War + "NEUTRALA.SMK", //Neutral1_a + "NEUTRALB.SMK", //Neutral1_b + "NEUTRALC.SMK", //Neutral1_c + //Liberation + "GOOD2A.SMK", //Good2_a + "GOOD2B.SMK", //Good2_b + "GOOD2C.SMK", //Good2_c + "GOOD2D.SMK", //Good2_d + //Long Live the King + "EVIL2A.SMK", //Evil2_a + "EVIL2AP1.SMK", //Evil2ap1 + "EVIL2B.SMK", //Evil2_b + "EVIL2C.SMK", //Evil2_c + "EVIL2D.SMK", //Evil2_d + //Song for the Father + "GOOD3A.SMK", //Good3_a + "GOOD3B.SMK", //Good3_b + "GOOD3C.SMK", //Good3_c + //Seeds Of Discontent + "SECRETA.SMK", //Secret_a + "SECRETB.SMK", //Secret_b + "SECRETC.SMK", //Secret_c + //Armageddon's Blade + //Armageddon's Blade + "H3ABab1.smk", //ArmageddonsBlade_a + "H3ABab2.smk", //ArmageddonsBlade_b + "H3ABab3.smk", //ArmageddonsBlade_c + "H3ABab4.smk", //ArmageddonsBlade_d + "H3ABab5.smk", //ArmageddonsBlade_e + "H3ABab6.smk", //ArmageddonsBlade_f + "H3ABab7.smk", //ArmageddonsBlade_g + "H3ABab8.smk", //ArmageddonsBlade_h + "H3ABab9.smk", //ArmageddonsBlade_end + //Dragon's Blood + "H3ABdb1.smk", //DragonsBlood_a + "H3ABdb2.smk", //DragonsBlood_b + "H3ABdb3.smk", //DragonsBlood_c + "H3ABdb4.smk", //DragonsBlood_d + "H3ABdb5.smk", //DragonsBlood_end + //Dragon Slayer + "H3ABds1.smk", //DragonSlayer_a + "H3ABds2.smk", //DragonSlayer_b + "H3ABds3.smk", //DragonSlayer_c + "H3ABds4.smk", //DragonSlayer_d + "H3ABds5.smk", //DragonSlayer_end + //Festival of Life + "H3ABfl1.smk", //FestivalOfLife_a + "H3ABfl2.smk", //FestivalOfLife_b + "H3ABfl3.smk", //FestivalOfLife_c + "H3ABfl4.smk", //FestivalOfLife_d + "H3ABfl5.smk", //FestivalOfLife_end + //Foolhardy Waywardness + "H3ABfw1.smk", //FoolhardyWaywardness_a + "H3ABfw2.smk", //FoolhardyWaywardness_b + "H3ABfw3.smk", //FoolhardyWaywardness_c + "H3ABfw4.smk", //FoolhardyWaywardness_d + "H3ABfw5.smk", //FoolhardyWaywardness_end + //Playing with Fire + "H3ABpf1.smk", //PlayingWithFire_a + "H3ABpf2.smk", //PlayingWithFire_b + "3ABpf3.smk", //PlayingWithFire_c + "H3ABpf4.smk", //PlayingWithFire_end + //Shadow of Death Campaigns + //Birth of a Barbarian + "H3x2_BBa.smk", //BirthOfABarbarian_a + "H3x2_BBb.smk", //BirthOfABarbarian_b + "H3x2_BBc.smk", //BirthOfABarbarian_c + "H3x2_BBd.smk", //BirthOfABarbarian_d + "H3x2_BBe.smk", //BirthOfABarbarian_e + "H3x2_BBf.smk", //BirthOfABarbarian_end + //Elixir of Life + "H3x2_Ela.smk", //ElixirOfLife_a + "H3x2_Elb.smk", //ElixirOfLife_b + "H3x2_Elc.smk", //ElixirOfLife_c + "H3x2_Eld.smk", //ElixirOfLife_d + "H3x2_Ele.smk", //ElixirOfLife_end + //Hack and Slash + "H3x2_HSa.smk", //HackAndSlash_a + "EVIL2C.SMK", //HackAndSlash_b + "H3x2_HSc.smk", //HackAndSlash_c + "H3x2_HSd.smk", //HackAndSlash_d + "H3x2_HSe.smk", //HackAndSlash_end + //New Beginning + "H3x2_NBa.smk", //NewBeginning_a + "H3x2_NBb.smk", //NewBeginning_b + "H3x2_Nbc.smk", //NewBeginning_c + "H3x2_Nbd.smk", //NewBeginning_d + "H3x2_Nbe.smk", //NewBeginning_end + //Rise of the Necromancer + "H3x2_RNa.smk", //RiseOfTheNecromancer_a + "H3x2_RNb.smk", //RiseOfTheNecromancer_b + "H3x2_RNc.smk", //RiseOfTheNecromancer_c + "H3x2_RNd.smk", //RiseOfTheNecromancer_d + "H3x2_RNe1.smk", //RiseOfTheNecromancer_end + //Spectre of Power + "H3x2_SPa.smk", //SpectreOfPower_a + "H3x2_SPb.smk", //SpectreOfPower_b + "H3x2_SPc.smk", //SpectreOfPower_c + "H3x2_SPd.smk", //SpectreOfPower_d + "H3x2_SPe.smk", //SpectreOfPower_end + //Unholy Alliance + "H3x2_UAa.smk", //UnholyAlliance_a + "H3x2_UAb.smk", //UnholyAlliance_b + "H3x2_UAc.smk", //UnholyAlliance_c + "H3x2_UAd.smk", //UnholyAlliance_d + "H3x2_UAe.smk", //UnholyAlliance_e + "H3x2_UAf.smk", //UnholyAlliance_f + "H3x2_UAg.smk", //UnholyAlliance_g + "H3x2_UAh.smk", //UnholyAlliance_h + "H3x2_UAi.smk", //UnholyAlliance_i + "H3x2_UAj.smk", //UnholyAlliance_j + "H3x2_UAk.smk", //UnholyAlliance_k + "H3x2_UAl.smk", //UnholyAlliance_l + "H3x2_UAm.smk", //UnholyAlliance_end //H3x2_UAm.bik? + ], + + "music" : [ + // Use CmpMusic.txt from H3 instead + ], + + "voice" : [ + //Restoration of Erathia + "G1A", //Long live the Queen 1 + "G1B", //Long live the Queen 2 + "G1C", //Long live the Queen 3 + "E1A.wav", //Dungeons and Devils 1 + "E1B.wav", //Dungeons and Devils 2 + "E1C.wav", //Dungeons and Devils 3 + "N1A", //Spoils of War 1 + "N1B", //Spoils of War 2 + "N1C_D", //Spoils of War 3 + "G2A", //Liberation 1 + "G2B", //Liberation 2 + "G2C", //Liberation 3 + "G2D", //Liberation 4 + "E2A.wav", //Long live the King 1 + "E2AE.wav", //Long live the King 1end + "E2B.wav", //Long live the King 2 + "E2C.wav", //Long live the King 3 + "E2D.wav", //Long live the King 4 + "G3A", //Song for the Father 1 + "G3B", //Song for the Father 2 + "G3C", //Song for the Father 3 + "S1A", //Seeds of discontent 1 + "S1B", //Seeds of discontent 2 + "S1C", //Seeds of discontent 3 + //Armageddon's Blade + "ABvoAB1.wav", //Armageddon's Blade 1 + "ABvoAB2.wav", //Armageddon's Blade 2 + "ABvoAB3.wav", //Armageddon's blade 3 + "ABvoAB4.wav", //Armageddon's blade 4 + "ABvoAB5.wav", //Armageddon's blade 5 + "ABvoAB6.wav", //Armageddon's blade 6 + "ABvoAB7.wav", //Armageddon's blade 7 + "ABvoAB8.wav", //Armageddon's blade 8 + "ABvoAB9.wav", //Armageddon's blade 8end + "ABvoDB1.wav", //Dragon's Blood 1 + "ABvoDB2.wav", //Dragon's Blood 2 + "ABvoDB3.wav", //Dragon's Blood 3 + "ABvoDB4.wav", //Dragon's Blood 4 + "ABvoDB5.wav", //Dragon's Blood 4end + "ABvoDS1.wav", //Dragon Slayer 1 + "ABvoDS2.wav", //Dragon Slayer 2 + "ABvoDS3.wav", //Dragon Slayer 3 + "ABvoDS4.wav", //Dragon Slayer 4 + "ABvoDS5.wav", //Dragon Slayer 4end + "ABvoFL1.wav", //Festival of Life 1 + "ABvoFL2.wav", //Festival of Life 2 + "ABvoFL3.wav", //Festival of Life 3 + "ABvoFL4.wav", //Festival of Life 4 + "ABvoFL5.wav", //Festival of Life 4end + "ABvoFW1.wav", //Foolhardy Waywardness 1 + "ABvoFW2.wav", //Foolhardy Waywardness 2 + "ABvoFW3.wav", //Foolhardy Waywardness 3 + "ABvoFW4.wav", //Foolhardy Waywardness 4 + "ABvoFW5.wav", //Foolhardy Waywardness 4end + "ABvoPF1.wav", //Playing with Fire 1 + "ABvoPF2.wav", //Playing with Fire 2 + "ABvoPF3.wav", //Playing with Fire 3 + "ABvoPF4.wav", //Playing with Fire 3end + //Shadow of Death Campaigns + "H3x2BBa", //Birth of a Barbarian 1 + "H3x2BBb", //Birth of a Barbarian 2 + "H3x2BBc", //Birth of a Barbarian 3 + "H3x2BBd", //Birth of a Barbarian 4 + "H3x2BBe", //Birth of a Barbarian 5 + "H3x2BBf", //Birth of a Barbarian 5end + "H3x2ELa", //Elixir of life 1 + "H3x2ELb", //Elixir of life 2 + "H3x2ELc", //Elixir of life 3 + "H3x2ELd", //Elixir of life 4 + "H3x2ELe", //Elixir of life 4end + "H3x2HSa", //Hack and Slash 1 + "H3x2HSb", //Hack and Slash 2 + "H3x2HSc", //Hack and Slash 3 + "H3x2HSd", //Hack and Slash 4 + "H3x2HSe", //Hack and Slash 4end + "H3x2NBa", //New Beginning 1 + "H3x2NBb", //New Beginning 2 + "H3x2NBc", //New Beginning 3 + "H3x2NBd", //New Beginning 4 + "H3x2NBe", //New Beginning 4end + "H3x2RNa", //Rise of the Necromancer 1 + "H3x2RNb", //Rise of the Necromancer 2 + "H3x2RNc", //Rise of the Necromancer 3 + "H3x2RNd", //Rise of the Necromancer 4 + "H3x2RNe", //Rise of the Necromancer 4end + "H3x2SPa", //Spectre of Power 1 + "H3x2Spb", //Spectre of Power 2 + "H3x2Spc", //Spectre of Power 3 + "H3x2Spd", //Spectre of Power 4 + "H3x2Spe", //Spectre of Power 4end + "H3x2UAa", //Unholy alliance 1 + "H3x2UAb", //Unholy alliance 2 + "H3x2UAc", //Unholy alliance 3 + "H3x2UAd", //Unholy alliance 4 + "H3x2UAe", //Unholy alliance 5 + "H3x2UAf", //Unholy alliance 6 + "H3x2UAg", //Unholy alliance 7 + "H3x2UAh", //Unholy alliance 8 + "H3x2UAi", //Unholy alliance 9 + "H3x2UAj", //Unholy alliance 10 + "H3x2UAk", //Unholy alliance 11 + "H3x2UAl", //Unholy alliance 12 + "H3x2UAm" //Unholy alliance 12end + ] +} diff --git a/config/commanders.json b/config/commanders.json index f789003a8..64a5f4181 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -1,41 +1,41 @@ - -{ - //Commander receives these bonuses on level-up - "bonusPerLevel": - [ - ["CREATURE_DAMAGE", 1, 1, 0 ], //+1 minimum damage - ["CREATURE_DAMAGE", 2, 2, 0 ], //+2 maximum damage - ["STACK_HEALTH", 5, 0, 0 ] //+5 hp - ], - //Value of bonuses given by each skill level - "skillLevels": - [ - {"name": "ATTACK", "levels": [2, 5, 9, 15, 25]}, //0 - {"name": "DEFENSE", "levels": [4, 10, 18, 30, 50]}, //1 - {"name": "HEALTH", "levels": [10, 25, 45, 70, 100]}, //2 - {"name": "DAMAGE", "levels": [10, 25, 45, 70, 100]}, //3 - {"name": "SPEED", "levels": [1, 2, 3, 4, 6]}, //4 - {"name": "SPELL_POWER", "levels": [1, 3, 6, 14, 29]}, //5 - {"name": "CASTS", "levels": [1, 2, 3, 4, 5]}, - {"name": "RESISTANCE", "levels": [5, 15, 35, 60, 90]} - ], - "abilityRequirements": - //Two secondary skills needed for each special ability - [ - {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, 0, 0 ], "skills": [0, 1]}, - {"ability": ["FEAR", 0, 0, 0 ], "skills": [0, 2]}, - {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, -1, 0 ], "skills": [0, 3]}, - {"ability": ["SHOOTER", 0, 0, 0 ], "skills": [0, 4]}, - {"ability": ["BLOCKS_RETALIATION", 0, 1, 0 ], "skills": [0,5]}, - {"ability": ["UNLIMITED_RETALIATIONS", 0, 0, 0 ], "skills": [1, 2]}, - {"ability": ["ATTACKS_ALL_ADJACENT", 0, 0, 0 ], "skills": [1, 3]}, - {"ability": ["NONE", 30, 0, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn - {"ability": ["FIRE_SHIELD", 1, 1, 0 ], "skills": [1, 5]}, - {"ability": ["ADDITIONAL_ATTACK", 1, 0, 0 ], "skills": [2, 3]}, - {"ability": ["HP_REGENERATION", 50, 0, 0 ], "skills": [2, 4]}, - {"ability": ["SPELL_AFTER_ATTACK", 30, "spell.paralyze", 0 ], "skills": [2, 5]}, - {"ability": ["JOUSTING", 5, 0, 0 ], "skills": [3, 4]}, - {"ability": ["DEATH_STARE", 1, 1, 0 ], "skills": [3,5]}, - {"ability": ["FLYING", 0, 0, 0 ], "skills": [4,5]} - ] -} + +{ + //Commander receives these bonuses on level-up + "bonusPerLevel": + [ + ["CREATURE_DAMAGE", 1, 1, 0 ], //+1 minimum damage + ["CREATURE_DAMAGE", 2, 2, 0 ], //+2 maximum damage + ["STACK_HEALTH", 5, 0, 0 ] //+5 hp + ], + //Value of bonuses given by each skill level + "skillLevels": + [ + {"name": "ATTACK", "levels": [2, 5, 9, 15, 25]}, //0 + {"name": "DEFENSE", "levels": [4, 10, 18, 30, 50]}, //1 + {"name": "HEALTH", "levels": [10, 25, 45, 70, 100]}, //2 + {"name": "DAMAGE", "levels": [10, 25, 45, 70, 100]}, //3 + {"name": "SPEED", "levels": [1, 2, 3, 4, 6]}, //4 + {"name": "SPELL_POWER", "levels": [1, 3, 6, 14, 29]}, //5 + {"name": "CASTS", "levels": [1, 2, 3, 4, 5]}, + {"name": "RESISTANCE", "levels": [5, 15, 35, 60, 90]} + ], + "abilityRequirements": + //Two secondary skills needed for each special ability + [ + {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, 0, 0 ], "skills": [0, 1]}, + {"ability": ["FEAR", 0, 0, 0 ], "skills": [0, 2]}, + {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, -1, 0 ], "skills": [0, 3]}, + {"ability": ["SHOOTER", 0, 0, 0 ], "skills": [0, 4]}, + {"ability": ["BLOCKS_RETALIATION", 0, 1, 0 ], "skills": [0,5]}, + {"ability": ["UNLIMITED_RETALIATIONS", 0, 0, 0 ], "skills": [1, 2]}, + {"ability": ["ATTACKS_ALL_ADJACENT", 0, 0, 0 ], "skills": [1, 3]}, + {"ability": ["NONE", 30, 0, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn + {"ability": ["FIRE_SHIELD", 1, 1, 0 ], "skills": [1, 5]}, + {"ability": ["ADDITIONAL_ATTACK", 1, 0, 0 ], "skills": [2, 3]}, + {"ability": ["HP_REGENERATION", 50, 0, 0 ], "skills": [2, 4]}, + {"ability": ["SPELL_AFTER_ATTACK", 30, "spell.paralyze", 0 ], "skills": [2, 5]}, + {"ability": ["JOUSTING", 5, 0, 0 ], "skills": [3, 4]}, + {"ability": ["DEATH_STARE", 1, 1, 0 ], "skills": [3,5]}, + {"ability": ["FLYING", 0, 0, 0 ], "skills": [4,5]} + ] +} diff --git a/config/difficulty.json b/config/difficulty.json index 2ee96a7ad..a3e8beffa 100644 --- a/config/difficulty.json +++ b/config/difficulty.json @@ -1,70 +1,70 @@ -//Configured difficulty -{ - "human": - { - "pawn": - { - "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "knight": - { - "resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "rook": - { - "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "queen": - { - "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "king": - { - "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - } - }, - "ai": - { - "pawn": - { - "resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "knight": - { - "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "rook": - { - "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "queen": - { - "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - }, - "king": - { - "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, - "globalBonuses": [], - "battleBonuses": [] - } - }, -} - +//Configured difficulty +{ + "human": + { + "pawn": + { + "resources": { "wood" : 30, "mercury": 15, "ore": 30, "sulfur": 15, "crystal": 15, "gems": 15, "gold": 30000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 20, "mercury": 10, "ore": 20, "sulfur": 10, "crystal": 10, "gems": 10, "gold": 20000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 15000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 0, "mercury": 0, "ore": 0 , "sulfur": 0, "crystal": 0, "gems": 0, "gold": 0, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, + "ai": + { + "pawn": + { + "resources": { "wood" : 5, "mercury": 2, "ore": 5, "sulfur": 2, "crystal": 2, "gems": 2, "gold": 5000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "knight": + { + "resources": { "wood" : 10, "mercury": 4, "ore": 10, "sulfur": 4, "crystal": 4, "gems": 4, "gold": 7500, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "rook": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "queen": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + }, + "king": + { + "resources": { "wood" : 15, "mercury": 7, "ore": 15, "sulfur": 7, "crystal": 7, "gems": 7, "gold": 10000, "mithril": 0 }, + "globalBonuses": [], + "battleBonuses": [] + } + }, +} + diff --git a/config/obstacles.json b/config/obstacles.json index aae6c7530..52c8de672 100644 --- a/config/obstacles.json +++ b/config/obstacles.json @@ -1,1199 +1,1199 @@ -// Defines battle obstacles. We have two vectors of them: -// * "obstacles" are usual obtacles, that are randomly placed in the battlefield. -// * "absoluteObstacles" are a little special: there can be only one such obstacle in the battlefield and its position is always the same. -// -// Their properties: -// * "allowedTerrains" vector of terrain types (TT format) where obstacle is appropriate -// * "specialBattlefields" vector of battlefield images (BI format) where obstacle is appropriate. If there is a special battlefield image, then only this list is checked. Otherwise it's ignored. -// * "blockedTiles": for absolute obstacles contains absolute coordinates. For usual obstacles contains offsets relative to the obstacle position (that is bottom left corner). If obstacle is placed in an odd row (counting from 0) and the blocked tile is in an even row, position will be shifted one tile to the left. Thanks to that ie. -16 is always top-right hex, no matter where the obstale will get placed. -// * "width" for usual obstacles it's count of tiles that must be free to the right for obstacle to be placed. For absolute obstacles, it's x offset for the graphics. -// * "height" for usual obstacles it's count of tiles that must be free to the top for obstacle to be placed. For absolute obstacles, it's y offset for the graphics. -// * "animation" is name of the graphics. It's def file for usual obstacles and bitmap for the absolute ones. - -{ - "0": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObDino1.def", - "absolute" : false - }, - "1": - { - "allowedTerrains" : ["dirt", "sand", "rough", "subterra"], - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObDino2.def", - "foreground" : true, - "absolute" : false - }, - "2": - { - "allowedTerrains" : ["dirt"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, -14, -15, -16], - "animation" : "ObDino3.def", - "absolute" : false - }, - "3": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSkel1.def", - "absolute" : false - }, - "4": - { - "allowedTerrains" : ["dirt", "rough", "subterra"], - "specialBattlefields" : ["sand_shore", "cursed_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSkel2.def", - "absolute" : false - }, - "5": - { - "allowedTerrains" : ["dirt"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3], - "animation" : "ObBDT01.def", - "absolute" : false - }, - "6": - { - "allowedTerrains" : ["dirt"], - "width" : 3, - "height" : 2, - "blockedTiles" : [-15, -16], - "animation" : "ObDRk01.def", - "absolute" : false - }, - "7": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDRk02.def", - "foreground" : true, - "absolute" : false - }, - "8": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [-16], - "animation" : "ObDRk03.def", - "absolute" : false - }, - "9": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDRk04.def", - "foreground" : true, - "absolute" : false - }, - "10": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObDSh01.def", - "foreground" : true, - "absolute" : false - }, - "11": - { - "allowedTerrains" : ["dirt"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObDTF03.def", - "foreground" : true, - "absolute" : false - }, - "12": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3], - "animation" : "ObDtS03.def", - "foreground" : true, - "absolute" : false - }, - "13": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15], - "animation" : "ObDtS04.def", - "absolute" : false - }, - "14": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [2, -15, -16], - "animation" : "ObDtS14.def", - "absolute" : false - }, - "15": - { - "allowedTerrains" : ["dirt", "rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [1, -16, -33], - "animation" : "ObDtS15.def", - "absolute" : false - }, - "16": - { - "allowedTerrains" : ["sand"], - "width" : 4, - "height" : 4, - "blockedTiles" : [-15, -16, -32, -33, -48, -49], - "animation" : "ObDsM01.def", - "absolute" : false - }, - "17": - { - "allowedTerrains" : ["sand"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, -15, -16], - "animation" : "ObDsS02.def", - "absolute" : false - }, - "18": - { - "allowedTerrains" : ["sand"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObDsS17.def", - "absolute" : false - }, - "19": - { - "allowedTerrains" : ["grass", "swamp"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObGLg01.def", - "absolute" : false - }, - "20": - { - "allowedTerrains" : ["grass", "swamp"], - "specialBattlefields" : ["magic_plains"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1], - "animation" : "ObGRk01.def", - "foreground" : true, - "absolute" : false - }, - "21": - { - "allowedTerrains" : ["grass", "swamp"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObGSt01.def", - "absolute" : false - }, - "22": - { - "allowedTerrains" : ["grass"], - "specialBattlefields" : ["magic_plains"], - "width" : 6, - "height" : 2, - "blockedTiles" : [1, 2, 3, 4, -13, -14, -15, -16], - "animation" : "ObGrS01.def", - "absolute" : false - }, - "23": - { - "allowedTerrains" : ["grass"], - "width" : 7, - "height" : 1, - "blockedTiles" : [1, 2], - "animation" : "OBGrS02.def", - "absolute" : false - }, - "24": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS01.def", - "absolute" : false - }, - "25": - { - "allowedTerrains" : ["snow"], - "width" : 5, - "height" : 1, - "blockedTiles" : [1, 2, 3, 4], - "animation" : "ObSnS02.def", - "absolute" : false - }, - "26": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, -16, -33], - "animation" : "ObSnS03.def", - "absolute" : false - }, - "27": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS04.def", - "absolute" : false - }, - "28": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 1, - "blockedTiles" : [1], - "animation" : "ObSnS05.def", - "absolute" : false - }, - "29": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2], - "animation" : "ObSnS06.def", - "foreground" : true, - "absolute" : false - }, - "30": - { - "allowedTerrains" : ["snow"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSnS07.def", - "absolute" : false - }, - "31": - { - "allowedTerrains" : ["snow"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSnS08.def", - "foreground" : true, - "absolute" : false - }, - "32": - { - "allowedTerrains" : ["snow"], - "width" : 7, - "height" : 2, - "blockedTiles" : [2, 3, 4, 5, -13, -14, -15, -16], - "animation" : "ObSnS09.def", - "absolute" : false - }, - "33": - { - "allowedTerrains" : ["snow"], - "width" : 5, - "height" : 5, - "blockedTiles" : [3, -13, -14, -15, -33, -49, -66], - "animation" : "ObSnS10.def", - "absolute" : false - }, - "34": - { - "allowedTerrains" : ["swamp"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0], - "animation" : "ObSwS01.def", - "foreground" : true, - "absolute" : false - }, - "35": - { - "allowedTerrains" : ["swamp"], - "width" : 8, - "height" : 3, - "blockedTiles" : [-10, -11, -12, -13, -14, -15, -16], - "animation" : "ObSwS02.def", - "absolute" : false - }, - "36": - { - "allowedTerrains" : ["swamp"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObSwS03.def", - "foreground" : true, - "absolute" : false - }, - "37": - { - "allowedTerrains" : ["swamp"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSwS04.def", - "foreground" : true, - "absolute" : false - }, - "38": - { - "allowedTerrains" : ["swamp"], - "width" : 5, - "height" : 4, - "blockedTiles" : [-13, -14, -15, -16, -30, -31, -32, -33], - "animation" : "ObSwS11b.def", - "absolute" : false - }, - "39": - { - "allowedTerrains" : ["swamp"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-16, -17, -31, -32, -33, -34], - "animation" : "ObSwS13a.def", - "absolute" : false - }, - "40": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1, -16], - "animation" : "ObRgS01.def", - "absolute" : false - }, - "41": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-14, -15, -16, -32, -33], - "animation" : "ObRgS02.def", - "absolute" : false - }, - "42": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15, -16], - "animation" : "ObRgS03.def", - "absolute" : false - }, - "43": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-16, -32, -33], - "animation" : "ObRgS04.def", - "absolute" : false - }, - "44": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-15, -16, -32], - "animation" : "ObRgS05.def", - "absolute" : false - }, - "45": - { - "allowedTerrains" : ["subterra"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, -15, -16], - "animation" : "ObSuS01.def", - "foreground" : true, - "absolute" : false - }, - "46": - { - "allowedTerrains" : ["subterra"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObSuS02.def", - "foreground" : true, - "absolute" : false - }, - "47": - { - "allowedTerrains" : ["subterra"], - "width" : 4, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], - "animation" : "ObSuS11b.def", - "foreground" : true, - "absolute" : false - }, - "48": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 3, - "blockedTiles" : [-14, -32, -33], - "animation" : "ObLvS01.def", - "absolute" : false - }, - "49": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, 2, -14, -15, -16], - "animation" : "ObLvS02.def", - "absolute" : false - }, - "50": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -30, -31, -32, -33], - "animation" : "ObLvS03.def", - "absolute" : false - }, - "51": - { - "allowedTerrains" : ["lava"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObLvS04.def", - "foreground" : true, - "absolute" : false - }, - "52": - { - "allowedTerrains" : ["lava"], - "width" : 4, - "height" : 4, - "blockedTiles" : [-14, -15, -32, -33, -49, -50], - "animation" : "ObLvS09.def", - "absolute" : false - }, - "53": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -16, -30, -31], - "animation" : "ObLvS17.def", - "absolute" : false - }, - "54": - { - "allowedTerrains" : ["lava"], - "width" : 5, - "height" : 3, - "blockedTiles" : [-13, -14, -15, -16, -31, -32, -33], - "animation" : "ObLvS22.def", - "absolute" : false - }, - "55": - { - "allowedTerrains" : ["water"], - "width" : 3, - "height" : 3, - "blockedTiles" : [-15, -16, -33], - "animation" : "ObBtS04.def", - "absolute" : false - }, - "56": - { - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, -15, -16], - "animation" : "ObBhS02.def", - "absolute" : false - }, - "57": - { - "specialBattlefields" : ["sand_shore"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObBhS03.def", - "foreground" : true, - "absolute" : false - }, - "58": - { - "specialBattlefields" : ["sand_shore"], - "width" : 5, - "height" : 2, - "blockedTiles" : [1, 2, 3, -14, -15, -16], - "animation" : "ObBhS11a.def", - "absolute" : false - }, - "59": - { - "specialBattlefields" : ["sand_shore"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, -14, -15], - "animation" : "ObBhS12b.def", - "absolute" : false - }, - "60": - { - "specialBattlefields" : ["sand_shore"], - "width" : 2, - "height" : 2, - "blockedTiles" : [0, 1, -16], - "animation" : "ObBhS14b.def", - "absolute" : false - }, - "61": - { - "specialBattlefields" : ["holy_ground"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObHGs00.def", - "foreground" : true, - "absolute" : false - }, - "62": - { - "specialBattlefields" : ["holy_ground"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObHGs01.def", - "foreground" : true, - "absolute" : false - }, - "63": - { - "specialBattlefields" : ["holy_ground"], - "width" : 3, - "height" : 3, - "blockedTiles" : [1], - "animation" : "ObHGs02.def", - "foreground" : true, - "absolute" : false - }, - "64": - { - "specialBattlefields" : ["holy_ground"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObHGs03.def", - "foreground" : true, - "absolute" : false - }, - "65": - { - "specialBattlefields" : ["holy_ground"], - "width" : 4, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3], - "animation" : "ObHGs04.def", - "foreground" : true, - "absolute" : false - }, - "66": - { - "specialBattlefields" : ["evil_fog"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObEFs00.def", - "foreground" : true, - "absolute" : false - }, - "67": - { - "specialBattlefields" : ["evil_fog"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObEFs01.def", - "foreground" : true, - "absolute" : false - }, - "68": - { - "specialBattlefields" : ["evil_fog"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2], - "animation" : "ObEFs02.def", - "foreground" : true, - "absolute" : false - }, - "69": - { - "specialBattlefields" : ["evil_fog"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2], - "animation" : "ObEFs03.def", - "foreground" : true, - "absolute" : false - }, - "70": - { - "specialBattlefields" : ["evil_fog"], - "width" : 6, - "height" : 2, - "blockedTiles" : [1, 2, 3, -12, -13], - "animation" : "ObEFs04.def", - "foreground" : true, - "absolute" : false - }, - "71": - { - "specialBattlefields" : ["clover_field"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObCFs00.def", - "absolute" : false - }, - "72": - { - "specialBattlefields" : ["clover_field"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObCFs01.def", - "absolute" : false - }, - "73": - { - "specialBattlefields" : ["clover_field"], - "width" : 3, - "height" : 2, - "blockedTiles" : [1, 2, -15, -16], - "animation" : "ObCFs02.def", - "absolute" : false - }, - "74": - { - "specialBattlefields" : ["clover_field"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, 2, -14, -15, -16], - "animation" : "ObCFs03.def", - "absolute" : false - }, - "75": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObLPs00.def", - "absolute" : false - }, - "76": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObLPs01.def", - "absolute" : false - }, - "77": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, -15, -16], - "animation" : "ObLPs02.def", - "absolute" : false - }, - "78": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 5, - "height" : 2, - "blockedTiles" : [1, 2, 3, -13, -14, -15, -16], - "animation" : "ObLPs03.def", - "absolute" : false - }, - "79": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObFFs00.def", - "foreground" : true, - "absolute" : false - }, - "80": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObFFs01.def", - "foreground" : true, - "absolute" : false - }, - "81": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 3, - "height" : 2, - "blockedTiles" : [0, 1, 2, -15], - "animation" : "ObFFs02.def", - "foreground" : true, - "absolute" : false - }, - "82": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObFFs03.def", - "foreground" : true, - "absolute" : false - }, - "83": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 3, - "height" : 3, - "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], - "animation" : "ObFFs04.def", - "foreground" : true, - "absolute" : false - }, - "84": - { - "specialBattlefields" : ["rocklands"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObRLs00.def", - "foreground" : true, - "absolute" : false - }, - "85": - { - "specialBattlefields" : ["rocklands"], - "width" : 2, - "height" : 1, - "blockedTiles" : [0, 1], - "animation" : "ObRLs01.def", - "foreground" : true, - "absolute" : false - }, - "86": - { - "specialBattlefields" : ["rocklands"], - "width" : 3, - "height" : 1, - "blockedTiles" : [0, 1, 2], - "animation" : "ObRLs02.def", - "foreground" : true, - "absolute" : false - }, - "87": - { - "specialBattlefields" : ["rocklands"], - "width" : 4, - "height" : 2, - "blockedTiles" : [1, 2, 3, -15, -16], - "animation" : "ObRLs03.def", - "foreground" : true, - "absolute" : false - }, - "88": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 1, - "height" : 1, - "blockedTiles" : [0], - "animation" : "ObMCs00.def", - "absolute" : false - }, - "89": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 2, - "height" : 2, - "blockedTiles" : [1, -16], - "animation" : "ObMCs01.def", - "absolute" : false - }, - "90": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 4, - "height" : 2, - "blockedTiles" : [0, 1, -14, -15], - "animation" : "ObMCs02.def", - "absolute" : false - }, - - "100": - { - "allowedTerrains" : ["dirt"], - "width" : 124, - "height" : 254, - "blockedTiles" : [80, 94, 95, 96, 97, 105, 106, 107, 108, 109, 110], - "animation" : "ObDtL04.pcx", - "absolute" : true - }, - "101": - { - "allowedTerrains" : ["dirt"], - "width" : 256, - "height" : 254, - "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], - "animation" : "ObDtL06.pcx", - "absolute" : true - }, - "102": - { - "allowedTerrains" : ["dirt"], - "width" : 168, - "height" : 212, - "blockedTiles" : [60, 61, 62, 63, 64, 72, 73, 74, 75, 76, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149], - "animation" : "ObDtL10.pcx", - "absolute" : true - }, - "103": - { - "allowedTerrains" : ["dirt"], - "width" : 124, - "height" : 254, - "blockedTiles" : [88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObDtL02.pcx", - "absolute" : true - }, - "104": - { - "allowedTerrains" : ["dirt"], - "width" : 146, - "height" : 254, - "blockedTiles" : [76, 77, 78, 79, 80, 89, 90, 91, 92, 93], - "animation" : "ObDtL03.pcx", - "absolute" : true - }, - "105": - { - "allowedTerrains" : ["grass"], - "width" : 173, - "height" : 221, - "blockedTiles" : [55, 56, 57, 58, 75, 76, 77, 95, 112, 113, 131], - "animation" : "ObGrL01.pcx", - "absolute" : true - }, - "106": - { - "allowedTerrains" : ["grass"], - "width" : 180, - "height" : 264, - "blockedTiles" : [81, 91, 92, 93, 94, 95, 96, 97, 98, 106, 107, 123], - "animation" : "ObGrL02.pcx", - "absolute" : true - }, - "107": - { - "allowedTerrains" : ["snow"], - "width" : 166, - "height" : 255, - "blockedTiles" : [76, 77, 78, 79, 91, 92, 93, 97, 98, 106, 107, 108], - "animation" : "ObSnL01.pcx", - "absolute" : true - }, - "108": - { - "allowedTerrains" : ["snow"], - "width" : 302, - "height" : 172, - "blockedTiles" : [41, 42, 43, 58, 75, 92, 108, 126, 143], - "animation" : "ObSnL14.pcx", - "absolute" : true - }, - "109": - { - "allowedTerrains" : ["swamp"], - "width" : 300, - "height" : 170, - "blockedTiles" : [40, 41, 58, 59, 74, 75, 92, 93, 109, 110, 111, 127, 128, 129, 130], - "animation" : "ObSwL15.pcx", - "absolute" : true - }, - "110": - { - "allowedTerrains" : ["swamp"], - "width" : 278, - "height" : 171, - "blockedTiles" : [43, 60, 61, 77, 93, 94, 95, 109, 110, 126, 127], - "animation" : "ObSwL14.pcx", - "absolute" : true - }, - "111": - { - "allowedTerrains" : ["swamp"], - "width" : 256, - "height" : 254, - "blockedTiles" : [74, 75, 76, 77, 91, 92, 93, 94, 95, 109, 110, 111, 112], - "animation" : "ObSwL22.pcx", - "absolute" : true - }, - "112": - { - "allowedTerrains" : ["lava"], - "width" : 124, - "height" : 254, - "blockedTiles" : [77, 78, 79, 80, 81, 91, 92, 93, 94, 105, 106, 107], - "animation" : "ObLvL01.pcx", - "absolute" : true - }, - "113": - { - "allowedTerrains" : ["lava"], - "width" : 256, - "height" : 128, - "blockedTiles" : [43, 60, 61, 76, 77, 93, 109, 126, 127, 142, 143], - "animation" : "OBLvL02.pcx", - "absolute" : true - }, - "114": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 186, - "height" : 212, - "blockedTiles" : [55, 72, 90, 107, 125, 126, 127, 128, 129, 130, 131, 132], - "animation" : "ObRgL01.pcx", - "absolute" : true - }, - "115": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 347, - "height" : 174, - "blockedTiles" : [41, 59, 76, 94, 111, 129, 143, 144, 145], - "animation" : "ObRgL02.pcx", - "absolute" : true - }, - "116": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 294, - "height" : 169, - "blockedTiles" : [40, 41, 42, 43, 58, 75, 93, 110, 128, 145], - "animation" : "ObRgL03.pcx", - "absolute" : true - }, - "117": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 165, - "height" : 257, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 105], - "animation" : "ObRgL04.pcx", - "absolute" : true - }, - "118": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 208, - "height" : 268, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], - "animation" : "ObRgL05.pcx", - "absolute" : true - }, - "119": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 252, - "height" : 254, - "blockedTiles" : [73, 74, 75, 76, 77, 78, 91, 92, 93, 94], - "animation" : "ObRgL06.pcx", - "absolute" : true - }, - "120": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 278, - "height" : 128, - "blockedTiles" : [23, 40, 58, 75, 93, 110, 128, 145, 163], - "animation" : "ObRgL15.pcx", - "absolute" : true - }, - "121": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 208, - "height" : 268, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], - "animation" : "ObRgL05.pcx", - "absolute" : true - }, - "122": - { - "allowedTerrains" : ["rough"], - "specialBattlefields" : ["cursed_ground"], - "width" : 168, - "height" : 212, - "blockedTiles" : [73, 74, 75, 76, 77, 78, 79, 90, 91, 92, 93, 94, 95, 96, 97, 106, 107, 108, 109, 110, 111, 112], - "animation" : "ObRgL22.pcx", - "absolute" : true - }, - "123": - { - "specialBattlefields" : ["sand_shore"], - "width" : 147, - "height" : 264, - "blockedTiles" : [72, 73, 74, 75, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObBhL02.pcx", - "absolute" : true - }, - "124": - { - "specialBattlefields" : ["sand_shore"], - "width" : 178, - "height" : 262, - "blockedTiles" : [71, 72, 73, 74, 75, 76, 77, 78, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], - "animation" : "ObBhL03.pcx", - "absolute" : true - }, - "125": - { - "specialBattlefields" : ["sand_shore"], - "width" : 173, - "height" : 257, - "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 90, 105, 106], - "animation" : "ObBhL05.pcx", - "absolute" : true - }, - "126": - { - "specialBattlefields" : ["sand_shore"], - "width" : 241, - "height" : 272, - "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], - "animation" : "ObBhL06.pcx", - "absolute" : true - }, - "127": - { - "specialBattlefields" : ["sand_shore"], - "width" : 261, - "height" : 129, - "blockedTiles" : [27, 28, 43, 44, 60, 61, 76, 77, 93, 94, 109, 110, 126, 127, 142, 143, 159], - "animation" : "ObBhL14.pcx", - "absolute" : true - }, - "128": - { - "specialBattlefields" : ["sand_shore"], - "width" : 180, - "height" : 154, - "blockedTiles" : [22, 38, 39, 40, 44, 45, 46, 55, 56, 57, 62, 63, 123, 124, 125, 130, 131, 140, 141, 146, 147, 148], - "animation" : "ObBhL16.pcx", - "absolute" : true - }, - "129": - { - "specialBattlefields" : ["clover_field"], - "width" : 304, - "height" : 264, - "blockedTiles" : [76, 77, 92, 93, 94, 95, 109, 110, 111], - "animation" : "ObCFL00.pcx", - "absolute" : true - }, - "130": - { - "specialBattlefields" : ["lucid_pools"], - "width" : 256, - "height" : 257, - "blockedTiles" : [76, 77, 78, 92, 93, 94, 107, 108, 109], - "animation" : "ObLPL00.pcx", - "absolute" : true - }, - "131": - { - "specialBattlefields" : ["fiery_fields"], - "width" : 257, - "height" : 255, - "blockedTiles" : [76, 77, 91, 92, 93, 94, 95, 108, 109, 110, 111], - "animation" : "ObFFL00.pcx", - "absolute" : true - }, - "132": - { - "specialBattlefields" : ["rocklands"], - "width" : 277, - "height" : 218, - "blockedTiles" : [60, 61, 75, 76, 77, 91, 92, 93, 94, 95], - "animation" : "ObRLL00.pcx", - "absolute" : true - }, - "133": - { - "specialBattlefields" : ["magic_clouds"], - "width" : 300, - "height" : 214, - "blockedTiles" : [59, 60, 74, 75, 76, 93, 94, 95, 111, 112], - "animation" : "ObMCL00.pcx", - "absolute" : true - } -} +// Defines battle obstacles. We have two vectors of them: +// * "obstacles" are usual obtacles, that are randomly placed in the battlefield. +// * "absoluteObstacles" are a little special: there can be only one such obstacle in the battlefield and its position is always the same. +// +// Their properties: +// * "allowedTerrains" vector of terrain types (TT format) where obstacle is appropriate +// * "specialBattlefields" vector of battlefield images (BI format) where obstacle is appropriate. If there is a special battlefield image, then only this list is checked. Otherwise it's ignored. +// * "blockedTiles": for absolute obstacles contains absolute coordinates. For usual obstacles contains offsets relative to the obstacle position (that is bottom left corner). If obstacle is placed in an odd row (counting from 0) and the blocked tile is in an even row, position will be shifted one tile to the left. Thanks to that ie. -16 is always top-right hex, no matter where the obstale will get placed. +// * "width" for usual obstacles it's count of tiles that must be free to the right for obstacle to be placed. For absolute obstacles, it's x offset for the graphics. +// * "height" for usual obstacles it's count of tiles that must be free to the top for obstacle to be placed. For absolute obstacles, it's y offset for the graphics. +// * "animation" is name of the graphics. It's def file for usual obstacles and bitmap for the absolute ones. + +{ + "0": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObDino1.def", + "absolute" : false + }, + "1": + { + "allowedTerrains" : ["dirt", "sand", "rough", "subterra"], + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObDino2.def", + "foreground" : true, + "absolute" : false + }, + "2": + { + "allowedTerrains" : ["dirt"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, -14, -15, -16], + "animation" : "ObDino3.def", + "absolute" : false + }, + "3": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSkel1.def", + "absolute" : false + }, + "4": + { + "allowedTerrains" : ["dirt", "rough", "subterra"], + "specialBattlefields" : ["sand_shore", "cursed_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSkel2.def", + "absolute" : false + }, + "5": + { + "allowedTerrains" : ["dirt"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3], + "animation" : "ObBDT01.def", + "absolute" : false + }, + "6": + { + "allowedTerrains" : ["dirt"], + "width" : 3, + "height" : 2, + "blockedTiles" : [-15, -16], + "animation" : "ObDRk01.def", + "absolute" : false + }, + "7": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDRk02.def", + "foreground" : true, + "absolute" : false + }, + "8": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [-16], + "animation" : "ObDRk03.def", + "absolute" : false + }, + "9": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDRk04.def", + "foreground" : true, + "absolute" : false + }, + "10": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObDSh01.def", + "foreground" : true, + "absolute" : false + }, + "11": + { + "allowedTerrains" : ["dirt"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObDTF03.def", + "foreground" : true, + "absolute" : false + }, + "12": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3], + "animation" : "ObDtS03.def", + "foreground" : true, + "absolute" : false + }, + "13": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15], + "animation" : "ObDtS04.def", + "absolute" : false + }, + "14": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [2, -15, -16], + "animation" : "ObDtS14.def", + "absolute" : false + }, + "15": + { + "allowedTerrains" : ["dirt", "rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [1, -16, -33], + "animation" : "ObDtS15.def", + "absolute" : false + }, + "16": + { + "allowedTerrains" : ["sand"], + "width" : 4, + "height" : 4, + "blockedTiles" : [-15, -16, -32, -33, -48, -49], + "animation" : "ObDsM01.def", + "absolute" : false + }, + "17": + { + "allowedTerrains" : ["sand"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, -15, -16], + "animation" : "ObDsS02.def", + "absolute" : false + }, + "18": + { + "allowedTerrains" : ["sand"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObDsS17.def", + "absolute" : false + }, + "19": + { + "allowedTerrains" : ["grass", "swamp"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObGLg01.def", + "absolute" : false + }, + "20": + { + "allowedTerrains" : ["grass", "swamp"], + "specialBattlefields" : ["magic_plains"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1], + "animation" : "ObGRk01.def", + "foreground" : true, + "absolute" : false + }, + "21": + { + "allowedTerrains" : ["grass", "swamp"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObGSt01.def", + "absolute" : false + }, + "22": + { + "allowedTerrains" : ["grass"], + "specialBattlefields" : ["magic_plains"], + "width" : 6, + "height" : 2, + "blockedTiles" : [1, 2, 3, 4, -13, -14, -15, -16], + "animation" : "ObGrS01.def", + "absolute" : false + }, + "23": + { + "allowedTerrains" : ["grass"], + "width" : 7, + "height" : 1, + "blockedTiles" : [1, 2], + "animation" : "OBGrS02.def", + "absolute" : false + }, + "24": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS01.def", + "absolute" : false + }, + "25": + { + "allowedTerrains" : ["snow"], + "width" : 5, + "height" : 1, + "blockedTiles" : [1, 2, 3, 4], + "animation" : "ObSnS02.def", + "absolute" : false + }, + "26": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, -16, -33], + "animation" : "ObSnS03.def", + "absolute" : false + }, + "27": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS04.def", + "absolute" : false + }, + "28": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 1, + "blockedTiles" : [1], + "animation" : "ObSnS05.def", + "absolute" : false + }, + "29": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2], + "animation" : "ObSnS06.def", + "foreground" : true, + "absolute" : false + }, + "30": + { + "allowedTerrains" : ["snow"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSnS07.def", + "absolute" : false + }, + "31": + { + "allowedTerrains" : ["snow"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSnS08.def", + "foreground" : true, + "absolute" : false + }, + "32": + { + "allowedTerrains" : ["snow"], + "width" : 7, + "height" : 2, + "blockedTiles" : [2, 3, 4, 5, -13, -14, -15, -16], + "animation" : "ObSnS09.def", + "absolute" : false + }, + "33": + { + "allowedTerrains" : ["snow"], + "width" : 5, + "height" : 5, + "blockedTiles" : [3, -13, -14, -15, -33, -49, -66], + "animation" : "ObSnS10.def", + "absolute" : false + }, + "34": + { + "allowedTerrains" : ["swamp"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0], + "animation" : "ObSwS01.def", + "foreground" : true, + "absolute" : false + }, + "35": + { + "allowedTerrains" : ["swamp"], + "width" : 8, + "height" : 3, + "blockedTiles" : [-10, -11, -12, -13, -14, -15, -16], + "animation" : "ObSwS02.def", + "absolute" : false + }, + "36": + { + "allowedTerrains" : ["swamp"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObSwS03.def", + "foreground" : true, + "absolute" : false + }, + "37": + { + "allowedTerrains" : ["swamp"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSwS04.def", + "foreground" : true, + "absolute" : false + }, + "38": + { + "allowedTerrains" : ["swamp"], + "width" : 5, + "height" : 4, + "blockedTiles" : [-13, -14, -15, -16, -30, -31, -32, -33], + "animation" : "ObSwS11b.def", + "absolute" : false + }, + "39": + { + "allowedTerrains" : ["swamp"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-16, -17, -31, -32, -33, -34], + "animation" : "ObSwS13a.def", + "absolute" : false + }, + "40": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1, -16], + "animation" : "ObRgS01.def", + "absolute" : false + }, + "41": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-14, -15, -16, -32, -33], + "animation" : "ObRgS02.def", + "absolute" : false + }, + "42": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15, -16], + "animation" : "ObRgS03.def", + "absolute" : false + }, + "43": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-16, -32, -33], + "animation" : "ObRgS04.def", + "absolute" : false + }, + "44": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-15, -16, -32], + "animation" : "ObRgS05.def", + "absolute" : false + }, + "45": + { + "allowedTerrains" : ["subterra"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, -15, -16], + "animation" : "ObSuS01.def", + "foreground" : true, + "absolute" : false + }, + "46": + { + "allowedTerrains" : ["subterra"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObSuS02.def", + "foreground" : true, + "absolute" : false + }, + "47": + { + "allowedTerrains" : ["subterra"], + "width" : 4, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], + "animation" : "ObSuS11b.def", + "foreground" : true, + "absolute" : false + }, + "48": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 3, + "blockedTiles" : [-14, -32, -33], + "animation" : "ObLvS01.def", + "absolute" : false + }, + "49": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, 2, -14, -15, -16], + "animation" : "ObLvS02.def", + "absolute" : false + }, + "50": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -30, -31, -32, -33], + "animation" : "ObLvS03.def", + "absolute" : false + }, + "51": + { + "allowedTerrains" : ["lava"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObLvS04.def", + "foreground" : true, + "absolute" : false + }, + "52": + { + "allowedTerrains" : ["lava"], + "width" : 4, + "height" : 4, + "blockedTiles" : [-14, -15, -32, -33, -49, -50], + "animation" : "ObLvS09.def", + "absolute" : false + }, + "53": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -16, -30, -31], + "animation" : "ObLvS17.def", + "absolute" : false + }, + "54": + { + "allowedTerrains" : ["lava"], + "width" : 5, + "height" : 3, + "blockedTiles" : [-13, -14, -15, -16, -31, -32, -33], + "animation" : "ObLvS22.def", + "absolute" : false + }, + "55": + { + "allowedTerrains" : ["water"], + "width" : 3, + "height" : 3, + "blockedTiles" : [-15, -16, -33], + "animation" : "ObBtS04.def", + "absolute" : false + }, + "56": + { + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, -15, -16], + "animation" : "ObBhS02.def", + "absolute" : false + }, + "57": + { + "specialBattlefields" : ["sand_shore"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObBhS03.def", + "foreground" : true, + "absolute" : false + }, + "58": + { + "specialBattlefields" : ["sand_shore"], + "width" : 5, + "height" : 2, + "blockedTiles" : [1, 2, 3, -14, -15, -16], + "animation" : "ObBhS11a.def", + "absolute" : false + }, + "59": + { + "specialBattlefields" : ["sand_shore"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, -14, -15], + "animation" : "ObBhS12b.def", + "absolute" : false + }, + "60": + { + "specialBattlefields" : ["sand_shore"], + "width" : 2, + "height" : 2, + "blockedTiles" : [0, 1, -16], + "animation" : "ObBhS14b.def", + "absolute" : false + }, + "61": + { + "specialBattlefields" : ["holy_ground"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObHGs00.def", + "foreground" : true, + "absolute" : false + }, + "62": + { + "specialBattlefields" : ["holy_ground"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObHGs01.def", + "foreground" : true, + "absolute" : false + }, + "63": + { + "specialBattlefields" : ["holy_ground"], + "width" : 3, + "height" : 3, + "blockedTiles" : [1], + "animation" : "ObHGs02.def", + "foreground" : true, + "absolute" : false + }, + "64": + { + "specialBattlefields" : ["holy_ground"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObHGs03.def", + "foreground" : true, + "absolute" : false + }, + "65": + { + "specialBattlefields" : ["holy_ground"], + "width" : 4, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3], + "animation" : "ObHGs04.def", + "foreground" : true, + "absolute" : false + }, + "66": + { + "specialBattlefields" : ["evil_fog"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObEFs00.def", + "foreground" : true, + "absolute" : false + }, + "67": + { + "specialBattlefields" : ["evil_fog"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObEFs01.def", + "foreground" : true, + "absolute" : false + }, + "68": + { + "specialBattlefields" : ["evil_fog"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2], + "animation" : "ObEFs02.def", + "foreground" : true, + "absolute" : false + }, + "69": + { + "specialBattlefields" : ["evil_fog"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2], + "animation" : "ObEFs03.def", + "foreground" : true, + "absolute" : false + }, + "70": + { + "specialBattlefields" : ["evil_fog"], + "width" : 6, + "height" : 2, + "blockedTiles" : [1, 2, 3, -12, -13], + "animation" : "ObEFs04.def", + "foreground" : true, + "absolute" : false + }, + "71": + { + "specialBattlefields" : ["clover_field"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObCFs00.def", + "absolute" : false + }, + "72": + { + "specialBattlefields" : ["clover_field"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObCFs01.def", + "absolute" : false + }, + "73": + { + "specialBattlefields" : ["clover_field"], + "width" : 3, + "height" : 2, + "blockedTiles" : [1, 2, -15, -16], + "animation" : "ObCFs02.def", + "absolute" : false + }, + "74": + { + "specialBattlefields" : ["clover_field"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, 2, -14, -15, -16], + "animation" : "ObCFs03.def", + "absolute" : false + }, + "75": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObLPs00.def", + "absolute" : false + }, + "76": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObLPs01.def", + "absolute" : false + }, + "77": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, -15, -16], + "animation" : "ObLPs02.def", + "absolute" : false + }, + "78": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 5, + "height" : 2, + "blockedTiles" : [1, 2, 3, -13, -14, -15, -16], + "animation" : "ObLPs03.def", + "absolute" : false + }, + "79": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObFFs00.def", + "foreground" : true, + "absolute" : false + }, + "80": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObFFs01.def", + "foreground" : true, + "absolute" : false + }, + "81": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 3, + "height" : 2, + "blockedTiles" : [0, 1, 2, -15], + "animation" : "ObFFs02.def", + "foreground" : true, + "absolute" : false + }, + "82": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObFFs03.def", + "foreground" : true, + "absolute" : false + }, + "83": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 3, + "height" : 3, + "blockedTiles" : [0, 1, 2, 3, -14, -15, -16], + "animation" : "ObFFs04.def", + "foreground" : true, + "absolute" : false + }, + "84": + { + "specialBattlefields" : ["rocklands"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObRLs00.def", + "foreground" : true, + "absolute" : false + }, + "85": + { + "specialBattlefields" : ["rocklands"], + "width" : 2, + "height" : 1, + "blockedTiles" : [0, 1], + "animation" : "ObRLs01.def", + "foreground" : true, + "absolute" : false + }, + "86": + { + "specialBattlefields" : ["rocklands"], + "width" : 3, + "height" : 1, + "blockedTiles" : [0, 1, 2], + "animation" : "ObRLs02.def", + "foreground" : true, + "absolute" : false + }, + "87": + { + "specialBattlefields" : ["rocklands"], + "width" : 4, + "height" : 2, + "blockedTiles" : [1, 2, 3, -15, -16], + "animation" : "ObRLs03.def", + "foreground" : true, + "absolute" : false + }, + "88": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 1, + "height" : 1, + "blockedTiles" : [0], + "animation" : "ObMCs00.def", + "absolute" : false + }, + "89": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 2, + "height" : 2, + "blockedTiles" : [1, -16], + "animation" : "ObMCs01.def", + "absolute" : false + }, + "90": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 4, + "height" : 2, + "blockedTiles" : [0, 1, -14, -15], + "animation" : "ObMCs02.def", + "absolute" : false + }, + + "100": + { + "allowedTerrains" : ["dirt"], + "width" : 124, + "height" : 254, + "blockedTiles" : [80, 94, 95, 96, 97, 105, 106, 107, 108, 109, 110], + "animation" : "ObDtL04.pcx", + "absolute" : true + }, + "101": + { + "allowedTerrains" : ["dirt"], + "width" : 256, + "height" : 254, + "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], + "animation" : "ObDtL06.pcx", + "absolute" : true + }, + "102": + { + "allowedTerrains" : ["dirt"], + "width" : 168, + "height" : 212, + "blockedTiles" : [60, 61, 62, 63, 64, 72, 73, 74, 75, 76, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149], + "animation" : "ObDtL10.pcx", + "absolute" : true + }, + "103": + { + "allowedTerrains" : ["dirt"], + "width" : 124, + "height" : 254, + "blockedTiles" : [88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObDtL02.pcx", + "absolute" : true + }, + "104": + { + "allowedTerrains" : ["dirt"], + "width" : 146, + "height" : 254, + "blockedTiles" : [76, 77, 78, 79, 80, 89, 90, 91, 92, 93], + "animation" : "ObDtL03.pcx", + "absolute" : true + }, + "105": + { + "allowedTerrains" : ["grass"], + "width" : 173, + "height" : 221, + "blockedTiles" : [55, 56, 57, 58, 75, 76, 77, 95, 112, 113, 131], + "animation" : "ObGrL01.pcx", + "absolute" : true + }, + "106": + { + "allowedTerrains" : ["grass"], + "width" : 180, + "height" : 264, + "blockedTiles" : [81, 91, 92, 93, 94, 95, 96, 97, 98, 106, 107, 123], + "animation" : "ObGrL02.pcx", + "absolute" : true + }, + "107": + { + "allowedTerrains" : ["snow"], + "width" : 166, + "height" : 255, + "blockedTiles" : [76, 77, 78, 79, 91, 92, 93, 97, 98, 106, 107, 108], + "animation" : "ObSnL01.pcx", + "absolute" : true + }, + "108": + { + "allowedTerrains" : ["snow"], + "width" : 302, + "height" : 172, + "blockedTiles" : [41, 42, 43, 58, 75, 92, 108, 126, 143], + "animation" : "ObSnL14.pcx", + "absolute" : true + }, + "109": + { + "allowedTerrains" : ["swamp"], + "width" : 300, + "height" : 170, + "blockedTiles" : [40, 41, 58, 59, 74, 75, 92, 93, 109, 110, 111, 127, 128, 129, 130], + "animation" : "ObSwL15.pcx", + "absolute" : true + }, + "110": + { + "allowedTerrains" : ["swamp"], + "width" : 278, + "height" : 171, + "blockedTiles" : [43, 60, 61, 77, 93, 94, 95, 109, 110, 126, 127], + "animation" : "ObSwL14.pcx", + "absolute" : true + }, + "111": + { + "allowedTerrains" : ["swamp"], + "width" : 256, + "height" : 254, + "blockedTiles" : [74, 75, 76, 77, 91, 92, 93, 94, 95, 109, 110, 111, 112], + "animation" : "ObSwL22.pcx", + "absolute" : true + }, + "112": + { + "allowedTerrains" : ["lava"], + "width" : 124, + "height" : 254, + "blockedTiles" : [77, 78, 79, 80, 81, 91, 92, 93, 94, 105, 106, 107], + "animation" : "ObLvL01.pcx", + "absolute" : true + }, + "113": + { + "allowedTerrains" : ["lava"], + "width" : 256, + "height" : 128, + "blockedTiles" : [43, 60, 61, 76, 77, 93, 109, 126, 127, 142, 143], + "animation" : "OBLvL02.pcx", + "absolute" : true + }, + "114": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 186, + "height" : 212, + "blockedTiles" : [55, 72, 90, 107, 125, 126, 127, 128, 129, 130, 131, 132], + "animation" : "ObRgL01.pcx", + "absolute" : true + }, + "115": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 347, + "height" : 174, + "blockedTiles" : [41, 59, 76, 94, 111, 129, 143, 144, 145], + "animation" : "ObRgL02.pcx", + "absolute" : true + }, + "116": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 294, + "height" : 169, + "blockedTiles" : [40, 41, 42, 43, 58, 75, 93, 110, 128, 145], + "animation" : "ObRgL03.pcx", + "absolute" : true + }, + "117": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 165, + "height" : 257, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 105], + "animation" : "ObRgL04.pcx", + "absolute" : true + }, + "118": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 208, + "height" : 268, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], + "animation" : "ObRgL05.pcx", + "absolute" : true + }, + "119": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 252, + "height" : 254, + "blockedTiles" : [73, 74, 75, 76, 77, 78, 91, 92, 93, 94], + "animation" : "ObRgL06.pcx", + "absolute" : true + }, + "120": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 278, + "height" : 128, + "blockedTiles" : [23, 40, 58, 75, 93, 110, 128, 145, 163], + "animation" : "ObRgL15.pcx", + "absolute" : true + }, + "121": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 208, + "height" : 268, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 90, 91, 92, 93, 94, 95, 96, 97], + "animation" : "ObRgL05.pcx", + "absolute" : true + }, + "122": + { + "allowedTerrains" : ["rough"], + "specialBattlefields" : ["cursed_ground"], + "width" : 168, + "height" : 212, + "blockedTiles" : [73, 74, 75, 76, 77, 78, 79, 90, 91, 92, 93, 94, 95, 96, 97, 106, 107, 108, 109, 110, 111, 112], + "animation" : "ObRgL22.pcx", + "absolute" : true + }, + "123": + { + "specialBattlefields" : ["sand_shore"], + "width" : 147, + "height" : 264, + "blockedTiles" : [72, 73, 74, 75, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObBhL02.pcx", + "absolute" : true + }, + "124": + { + "specialBattlefields" : ["sand_shore"], + "width" : 178, + "height" : 262, + "blockedTiles" : [71, 72, 73, 74, 75, 76, 77, 78, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98], + "animation" : "ObBhL03.pcx", + "absolute" : true + }, + "125": + { + "specialBattlefields" : ["sand_shore"], + "width" : 173, + "height" : 257, + "blockedTiles" : [72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 89, 90, 105, 106], + "animation" : "ObBhL05.pcx", + "absolute" : true + }, + "126": + { + "specialBattlefields" : ["sand_shore"], + "width" : 241, + "height" : 272, + "blockedTiles" : [73, 91, 108, 109, 110, 111, 112, 113], + "animation" : "ObBhL06.pcx", + "absolute" : true + }, + "127": + { + "specialBattlefields" : ["sand_shore"], + "width" : 261, + "height" : 129, + "blockedTiles" : [27, 28, 43, 44, 60, 61, 76, 77, 93, 94, 109, 110, 126, 127, 142, 143, 159], + "animation" : "ObBhL14.pcx", + "absolute" : true + }, + "128": + { + "specialBattlefields" : ["sand_shore"], + "width" : 180, + "height" : 154, + "blockedTiles" : [22, 38, 39, 40, 44, 45, 46, 55, 56, 57, 62, 63, 123, 124, 125, 130, 131, 140, 141, 146, 147, 148], + "animation" : "ObBhL16.pcx", + "absolute" : true + }, + "129": + { + "specialBattlefields" : ["clover_field"], + "width" : 304, + "height" : 264, + "blockedTiles" : [76, 77, 92, 93, 94, 95, 109, 110, 111], + "animation" : "ObCFL00.pcx", + "absolute" : true + }, + "130": + { + "specialBattlefields" : ["lucid_pools"], + "width" : 256, + "height" : 257, + "blockedTiles" : [76, 77, 78, 92, 93, 94, 107, 108, 109], + "animation" : "ObLPL00.pcx", + "absolute" : true + }, + "131": + { + "specialBattlefields" : ["fiery_fields"], + "width" : 257, + "height" : 255, + "blockedTiles" : [76, 77, 91, 92, 93, 94, 95, 108, 109, 110, 111], + "animation" : "ObFFL00.pcx", + "absolute" : true + }, + "132": + { + "specialBattlefields" : ["rocklands"], + "width" : 277, + "height" : 218, + "blockedTiles" : [60, 61, 75, 76, 77, 91, 92, 93, 94, 95], + "animation" : "ObRLL00.pcx", + "absolute" : true + }, + "133": + { + "specialBattlefields" : ["magic_clouds"], + "width" : 300, + "height" : 214, + "blockedTiles" : [59, 60, 74, 75, 76, 93, 94, 95, 111, 112], + "animation" : "ObMCL00.pcx", + "absolute" : true + } +} diff --git a/config/schemas/skill.json b/config/schemas/skill.json index 10973ccbf..162f84de6 100644 --- a/config/schemas/skill.json +++ b/config/schemas/skill.json @@ -1,98 +1,98 @@ -{ - "type" : "object", - "$schema" : "http://json-schema.org/draft-04/schema", - "title" : "VCMI skill format", - "description" : "Format used to replace bonuses provided by secondary skills in VCMI", - "definitions" : { - "skillBonus" : { - "type" : "object", - "description" : "Set of bonuses provided by skill at given level", - "required" : ["description", "effects"], - "properties" : { - "description" : { - "type" : "string", - "description" : "localizable description" - }, - "images" : { - "type" : "object", - "description" : "Skill icons of varying size", - "properties" : { - "small" : { - "type" : "string", - "description" : "32x32 skill icon", - "format" : "imageFile" - }, - "medium" : { - "type" : "string", - "description" : "44x44 skill icon", - "format" : "imageFile" - }, - "large" : { - "type" : "string", - "description" : "82x93 skill icon", - "format" : "imageFile" - } - } - }, - "effects" : { - "type" : "object", - "additionalProperties" : { - "$ref" : "bonus.json" - } - } - } - } - }, - "required" : ["name", "basic", "advanced", "expert"], - "properties" : { - "name" : { - "type" : "string", - "description" : "Mandatory, localizable skill name" - }, - "index" : { - "type" : "number", - "description" : "Internal, numeric id of skill, required for existing skills" - }, - "obligatoryMajor" : { - "type" : "boolean", - "description" : "This skill is major obligatory (like H3 Wisdom)" - }, - "obligatoryMinor" : { - "type" : "boolean", - "description" : "This skill is minor obligatory (like H3 Magic school)" - }, - "gainChance" : { - "description" : "Chance for the skill to be offered on level-up (heroClass may override)", - "type" : "object", - "required" : ["might", "magic"], - "properties" : { - "might" : { - "type" : "number", - "description" : "Chance for hero classes with might affinity" - }, - "magic" : { - "type" : "number", - "description" : "Chance for hero classes with magic affinity" - } - } - }, - "base" : { - "type" : "object", - "description" : "will be merged with all levels", - "additionalProperties" : true - }, - "basic" : { - "$ref" : "#/definitions/skillBonus" - }, - "advanced" : { - "$ref" : "#/definitions/skillBonus" - }, - "expert" : { - "$ref" : "#/definitions/skillBonus" - } - }, - "onlyOnWaterMap" : { - "type" : "boolean", - "description" : "It true, skill won't be available on a map without water" - } -} +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI skill format", + "description" : "Format used to replace bonuses provided by secondary skills in VCMI", + "definitions" : { + "skillBonus" : { + "type" : "object", + "description" : "Set of bonuses provided by skill at given level", + "required" : ["description", "effects"], + "properties" : { + "description" : { + "type" : "string", + "description" : "localizable description" + }, + "images" : { + "type" : "object", + "description" : "Skill icons of varying size", + "properties" : { + "small" : { + "type" : "string", + "description" : "32x32 skill icon", + "format" : "imageFile" + }, + "medium" : { + "type" : "string", + "description" : "44x44 skill icon", + "format" : "imageFile" + }, + "large" : { + "type" : "string", + "description" : "82x93 skill icon", + "format" : "imageFile" + } + } + }, + "effects" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "bonus.json" + } + } + } + } + }, + "required" : ["name", "basic", "advanced", "expert"], + "properties" : { + "name" : { + "type" : "string", + "description" : "Mandatory, localizable skill name" + }, + "index" : { + "type" : "number", + "description" : "Internal, numeric id of skill, required for existing skills" + }, + "obligatoryMajor" : { + "type" : "boolean", + "description" : "This skill is major obligatory (like H3 Wisdom)" + }, + "obligatoryMinor" : { + "type" : "boolean", + "description" : "This skill is minor obligatory (like H3 Magic school)" + }, + "gainChance" : { + "description" : "Chance for the skill to be offered on level-up (heroClass may override)", + "type" : "object", + "required" : ["might", "magic"], + "properties" : { + "might" : { + "type" : "number", + "description" : "Chance for hero classes with might affinity" + }, + "magic" : { + "type" : "number", + "description" : "Chance for hero classes with magic affinity" + } + } + }, + "base" : { + "type" : "object", + "description" : "will be merged with all levels", + "additionalProperties" : true + }, + "basic" : { + "$ref" : "#/definitions/skillBonus" + }, + "advanced" : { + "$ref" : "#/definitions/skillBonus" + }, + "expert" : { + "$ref" : "#/definitions/skillBonus" + } + }, + "onlyOnWaterMap" : { + "type" : "boolean", + "description" : "It true, skill won't be available on a map without water" + } +} diff --git a/config/schemas/spell.json b/config/schemas/spell.json index cd2b55843..8a29cd88b 100644 --- a/config/schemas/spell.json +++ b/config/schemas/spell.json @@ -1,318 +1,318 @@ -{ - "type" : "object", - "$schema" : "http://json-schema.org/draft-04/schema", - "title" : "VCMI spell format", - "description" : "Format used to define new spells in VCMI", - "definitions" : { - "animationQueue" : { - "type" : "array", - "items" : { - "anyOf" :[ - { - //dummy animation, pause, Value - frame count - "type" : "number" - }, - { - //assumed verticalPosition: top - "type" : "string", - "format" : "defFile" - }, - { - "type" : "object", - "properties" : { - "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, - "defName" : {"type" : "string", "format" : "defFile"}, - "effectName" : { "type" : "string" } - }, - "additionalProperties" : false - } - ] - } - }, - "animation" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "affect" : {"$ref" : "#/definitions/animationQueue"}, - "hit" : {"$ref" : "#/definitions/animationQueue"}, - "cast" : {"$ref" : "#/definitions/animationQueue"}, - "projectile" : { - "type" : "array", - "items" : { - "type" : "object", - "properties" : { - "defName" : {"type" : "string", "format" : "defFile"}, - "minimumAngle" : {"type" : "number", "minimum" : 0} - }, - "additionalProperties" : false - } - } - } - }, - "flags" : { - "type" : "object", - "additionalProperties" : { - "type" : "boolean" - } - }, - "levelInfo" : { - "type" : "object", - "required" :["range", "description", "cost", "power", "aiValue"], - - "additionalProperties" : false, - "properties" : { - "description" : { - "type" : "string", - "description" : "Localizable description. Use {xxx} for formatting" - }, - "cost" : { - "type" : "number", - "description" : "Cost in mana points" - }, - "power" : { - "type" : "number" - }, - "range" : { - "type" : "string", - "description" : "spell range description in SRSL" - }, - "aiValue" : { - "type" : "number" - }, - "effects" : { - "type" : "object", - "description" : "Timed effects (updated by prolongation)", - "additionalProperties" : { - "$ref" : "bonus.json" - } - }, - "cumulativeEffects" : { - "type" : "object", - "description" : "Timed effects (updated by unique bonus)", - "additionalProperties" : { - "$ref" : "bonus.json" - } - }, - "battleEffects" : { - "type" : "object", - "additionalProperties" : { - "type" : "object" - } - }, - "targetModifier" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "smart" : { - "type" : "boolean", - "description" : "true: friendly/hostile based on positiveness; false: all targets" - }, - "clearTarget" : - { - "type" : "boolean", - "description" : "LOCATION target only. Target hex/tile must be clear" - }, - "clearAffected" : - { - "type" : "boolean", - "description" : "LOCATION target only. All affected hexes/tile must be clear" - } - } - } - } - }, - "texts" : { - "type" : "object", - "additionalProperties" : false - } - }, - "required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"], - "additionalProperties" : false, - "properties" : { - "index" : { - "type" : "number", - "description" : "numeric id of spell required only for original spells, prohibited for new spells" - }, - "name" : { - "type" : "string", - "description" : "Localizable name of this spell" - }, - "type" : { - "type" : "string", - "enum" : ["adventure", "combat", "ability"], - "description" : "Spell type" - }, - "school" : { - "type" : "object", - "description" : "List of spell schools this spell belongs to", - "additionalProperties" : false, - "properties" : { - "air" : {"type" : "boolean"}, - "fire" : {"type" : "boolean"}, - "earth" : {"type" : "boolean"}, - "water" : {"type" : "boolean"} - } - }, - "level" : { - "type" : "number", - "description" : "Spell level", - "minimum" : 0, - "maximum" : 5 - }, - "power" : { - "type" : "number", - "description" : "Base power of the spell" - }, - "defaultGainChance" : { - "type" : "number", - "description" : "Gain chance by default for all factions" - - }, - "gainChance" : { - "type" : "object", - "description" : "Chance for this spell to appear in Mage Guild of a specific faction", - "additionalProperties" : { - "type" : "number", - "minimum" : 0 - } - }, - "targetType" : { - "type" : "string", - "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", - "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] - }, - "counters" : { - "$ref" : "#/definitions/flags", - "description" : "Flags structure ids of countering spells" - }, - "flags" : { - "type" : "object", - "description" : "Various flags", - "additionalProperties" : false, - "properties" : { - "indifferent" : { - "type" : "boolean", - "description" : "Spell is indifferent for target" - }, - "negative" : { - "type" : "boolean", - "description" : "Spell is negative for target" - }, - "positive" : { - "type" : "boolean", - "description" : "Spell is positive for target" - }, - "damage" : { - "type" : "boolean", - "description" : "Spell does damage (direct or indirect)" - }, - "offensive" : { - "type" : "boolean", - "description" : "Spell does direct damage (implicitly sets damage and negative)" - }, - "rising" : { - "type" : "boolean", - "description" : "Rising spell (implicitly sets positive)" - }, - "special" : { - "type" : "boolean", - "description" : "Special spell. Can be given only by BonusType::SPELL" - }, - "nonMagical" : { - "type" : "boolean", - "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." - } - } - }, - "immunity" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, any one of these bonus grants immunity" - }, - "absoluteImmunity" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names. Any one of these bonus grants immunity, can't be negated" - }, - "limit" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, presence of all bonuses required to be affected by." - }, - "absoluteLimit" : { - "$ref" : "#/definitions/flags", - "description" : "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." - }, - "targetCondition" : { - "type" : "object", - "additionalProperties" : true - }, - "animation" : {"$ref" : "#/definitions/animation"}, - "graphics" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "iconBook" : { - "type" : "string", - "description" : "Resourse path of icon for spellbook" , - "format" : "imageFile" - }, - "iconScroll" : { - "type" : "string", - "description" : "Resourse path of icon for spell scrolls", - "format" : "imageFile" - }, - "iconEffect" : { - "type" : "string", - "description" : "Resourse path of icon for spell effects during battle" , - "format" : "imageFile" - }, - "iconImmune" : { - "type" : "string", - "description" : "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)", - "format" : "imageFile" - }, - "iconScenarioBonus" : { - "type" : "string", - "description" : "Resourse path of icon for scenario bonus" , - "format" : "imageFile" - } - } - }, - "sounds" : { - "type" : "object", - "additionalProperties" : false, - "properties" : { - "cast" : { - "type" : "string", - "description" : "Resourse path of cast sound" - } - } - }, - "levels" : { - "type" : "object", - "additionalProperties" : false, - "required" : ["none", "basic", "advanced", "expert"], - "properties" : { - "base" : { - "type" : "object", - "description" : "will be merged with all levels", - "additionalProperties" : true - }, - "none" : { - "$ref" : "#/definitions/levelInfo" - }, - "basic" : { - "$ref" : "#/definitions/levelInfo" - }, - "advanced" : { - "$ref" : "#/definitions/levelInfo" - }, - "expert" : { - "$ref" : "#/definitions/levelInfo" - } - } - }, - "onlyOnWaterMap" : { - "type" : "boolean", - "description" : "If true, spell won't be available on a map without water" - } - } -} +{ + "type" : "object", + "$schema" : "http://json-schema.org/draft-04/schema", + "title" : "VCMI spell format", + "description" : "Format used to define new spells in VCMI", + "definitions" : { + "animationQueue" : { + "type" : "array", + "items" : { + "anyOf" :[ + { + //dummy animation, pause, Value - frame count + "type" : "number" + }, + { + //assumed verticalPosition: top + "type" : "string", + "format" : "defFile" + }, + { + "type" : "object", + "properties" : { + "verticalPosition" : {"type" : "string", "enum" :["top","bottom"]}, + "defName" : {"type" : "string", "format" : "defFile"}, + "effectName" : { "type" : "string" } + }, + "additionalProperties" : false + } + ] + } + }, + "animation" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "affect" : {"$ref" : "#/definitions/animationQueue"}, + "hit" : {"$ref" : "#/definitions/animationQueue"}, + "cast" : {"$ref" : "#/definitions/animationQueue"}, + "projectile" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "defName" : {"type" : "string", "format" : "defFile"}, + "minimumAngle" : {"type" : "number", "minimum" : 0} + }, + "additionalProperties" : false + } + } + } + }, + "flags" : { + "type" : "object", + "additionalProperties" : { + "type" : "boolean" + } + }, + "levelInfo" : { + "type" : "object", + "required" :["range", "description", "cost", "power", "aiValue"], + + "additionalProperties" : false, + "properties" : { + "description" : { + "type" : "string", + "description" : "Localizable description. Use {xxx} for formatting" + }, + "cost" : { + "type" : "number", + "description" : "Cost in mana points" + }, + "power" : { + "type" : "number" + }, + "range" : { + "type" : "string", + "description" : "spell range description in SRSL" + }, + "aiValue" : { + "type" : "number" + }, + "effects" : { + "type" : "object", + "description" : "Timed effects (updated by prolongation)", + "additionalProperties" : { + "$ref" : "bonus.json" + } + }, + "cumulativeEffects" : { + "type" : "object", + "description" : "Timed effects (updated by unique bonus)", + "additionalProperties" : { + "$ref" : "bonus.json" + } + }, + "battleEffects" : { + "type" : "object", + "additionalProperties" : { + "type" : "object" + } + }, + "targetModifier" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "smart" : { + "type" : "boolean", + "description" : "true: friendly/hostile based on positiveness; false: all targets" + }, + "clearTarget" : + { + "type" : "boolean", + "description" : "LOCATION target only. Target hex/tile must be clear" + }, + "clearAffected" : + { + "type" : "boolean", + "description" : "LOCATION target only. All affected hexes/tile must be clear" + } + } + } + } + }, + "texts" : { + "type" : "object", + "additionalProperties" : false + } + }, + "required" : ["type", "name", "school", "level", "power","gainChance","flags","levels"], + "additionalProperties" : false, + "properties" : { + "index" : { + "type" : "number", + "description" : "numeric id of spell required only for original spells, prohibited for new spells" + }, + "name" : { + "type" : "string", + "description" : "Localizable name of this spell" + }, + "type" : { + "type" : "string", + "enum" : ["adventure", "combat", "ability"], + "description" : "Spell type" + }, + "school" : { + "type" : "object", + "description" : "List of spell schools this spell belongs to", + "additionalProperties" : false, + "properties" : { + "air" : {"type" : "boolean"}, + "fire" : {"type" : "boolean"}, + "earth" : {"type" : "boolean"}, + "water" : {"type" : "boolean"} + } + }, + "level" : { + "type" : "number", + "description" : "Spell level", + "minimum" : 0, + "maximum" : 5 + }, + "power" : { + "type" : "number", + "description" : "Base power of the spell" + }, + "defaultGainChance" : { + "type" : "number", + "description" : "Gain chance by default for all factions" + + }, + "gainChance" : { + "type" : "object", + "description" : "Chance for this spell to appear in Mage Guild of a specific faction", + "additionalProperties" : { + "type" : "number", + "minimum" : 0 + } + }, + "targetType" : { + "type" : "string", + "description" : "NO_TARGET - instant cast no aiming, default; CREATURE - target is unit; OBSTACLE - target is OBSTACLE; LOCATION - target is location", + "enum" : ["NO_TARGET","CREATURE","OBSTACLE","LOCATION"] + }, + "counters" : { + "$ref" : "#/definitions/flags", + "description" : "Flags structure ids of countering spells" + }, + "flags" : { + "type" : "object", + "description" : "Various flags", + "additionalProperties" : false, + "properties" : { + "indifferent" : { + "type" : "boolean", + "description" : "Spell is indifferent for target" + }, + "negative" : { + "type" : "boolean", + "description" : "Spell is negative for target" + }, + "positive" : { + "type" : "boolean", + "description" : "Spell is positive for target" + }, + "damage" : { + "type" : "boolean", + "description" : "Spell does damage (direct or indirect)" + }, + "offensive" : { + "type" : "boolean", + "description" : "Spell does direct damage (implicitly sets damage and negative)" + }, + "rising" : { + "type" : "boolean", + "description" : "Rising spell (implicitly sets positive)" + }, + "special" : { + "type" : "boolean", + "description" : "Special spell. Can be given only by BonusType::SPELL" + }, + "nonMagical" : { + "type" : "boolean", + "description" : "Non-magical ability. Usually used by some creatures. Should not be affected by sorcery and generic magic resistance. School resistances apply. Examples: dendroid bind, efreet fire shield." + } + } + }, + "immunity" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, any one of these bonus grants immunity" + }, + "absoluteImmunity" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names. Any one of these bonus grants immunity, can't be negated" + }, + "limit" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, presence of all bonuses required to be affected by." + }, + "absoluteLimit" : { + "$ref" : "#/definitions/flags", + "description" : "flags structure of bonus names, presence of all bonuses required to be affected by, can't be negated." + }, + "targetCondition" : { + "type" : "object", + "additionalProperties" : true + }, + "animation" : {"$ref" : "#/definitions/animation"}, + "graphics" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "iconBook" : { + "type" : "string", + "description" : "Resourse path of icon for spellbook" , + "format" : "imageFile" + }, + "iconScroll" : { + "type" : "string", + "description" : "Resourse path of icon for spell scrolls", + "format" : "imageFile" + }, + "iconEffect" : { + "type" : "string", + "description" : "Resourse path of icon for spell effects during battle" , + "format" : "imageFile" + }, + "iconImmune" : { + "type" : "string", + "description" : "Resourse path of icon for SPELL_IMMUNITY bonus (relative to DATA or SPRITES)", + "format" : "imageFile" + }, + "iconScenarioBonus" : { + "type" : "string", + "description" : "Resourse path of icon for scenario bonus" , + "format" : "imageFile" + } + } + }, + "sounds" : { + "type" : "object", + "additionalProperties" : false, + "properties" : { + "cast" : { + "type" : "string", + "description" : "Resourse path of cast sound" + } + } + }, + "levels" : { + "type" : "object", + "additionalProperties" : false, + "required" : ["none", "basic", "advanced", "expert"], + "properties" : { + "base" : { + "type" : "object", + "description" : "will be merged with all levels", + "additionalProperties" : true + }, + "none" : { + "$ref" : "#/definitions/levelInfo" + }, + "basic" : { + "$ref" : "#/definitions/levelInfo" + }, + "advanced" : { + "$ref" : "#/definitions/levelInfo" + }, + "expert" : { + "$ref" : "#/definitions/levelInfo" + } + } + }, + "onlyOnWaterMap" : { + "type" : "boolean", + "description" : "If true, spell won't be available on a map without water" + } + } +} diff --git a/docs/Readme.md b/docs/Readme.md index 77ccb0964..3c630514b 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,81 +1,81 @@ -[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.2) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) - -# VCMI Project - -VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. - -## Links - - * Homepage: https://vcmi.eu/ - * Forums: https://forum.vcmi.eu/ - * Bugtracker: https://github.com/vcmi/vcmi/issues - * Slack: https://slack.vcmi.eu/ - * Discord: https://discord.gg/chBT42V - -## Latest release - -Latest release can be found [in Github Releases page](https://github.com/vcmi/vcmi/releases/latest). As of right now we plan to have major releases around 3 times per year. Daily builds are still available at [builds.vcmi.download](https://builds.vcmi.download/branch/develop/) but they are not guaranteed to be stable. So we encourage everybody to use them and report found bugs so that we can fix them. -Loading saves made with different major version of VCMI is usually **not** supported, so you may want to finish your ongoing games before updating. -Please see corresponding installation guide articles for details for your platform. - -## Installation guides -- [Windows](players/Installation_Windows.md) -- [macOS](players/Installation_macOS.md) -- [Linux](players/Installation_Linux.md) -- [Android](players/Installation_Android.md) -- [iOS](players/Installation_iOS.md) - -## Documentation and guidelines for players - -- [General information about VCMI Project](players/Manual.md) -- [Frequently asked questions](https://vcmi.eu/faq/) (external link) -- [Game mechanics](players/Game_Mechanics.md) -- [Bug reporting guidelines](players/Bug_Reporting_Guidelines.md) -- [Cheat codes](players/Cheat_Codes.md) -- [Privacy Policy](players/Privacy_Policy.md) - -## Documentation and guidelines for game modders - -- [Modding Guidelines](modders/Readme.md) -- [Mod File Format](modders/Mod_File_Format.md) -- [Bonus Format](modders/Bonus_Format.md) -- [Translations](modders/Translations.md) -- [Map Editor](modders/Map_Editor.md) -- [Campaign Format](modders/Campaign_Format.md) -- [Configurable Widgets](modders/Configurable_Widgets.md) - -## Documentation and guidelines for developers - -Development environment setup instructions: -- [Building VCMI for Android](developers/Building_Android.md) -- [Building VCMI for iOS](developers/Building_iOS.md) -- [Building VCMI for Linux](developers/Building_Linux.md) -- [Building VCMI for macOS](developers/Building_macOS.md) -- [Building VCMI for Windows](developers/Building_Windows.md) -- [Conan](developers/Conan.md) - -Engine documentation: (NOTE: may be outdated) -- [Development with Qt Creator](developers/Development_with_Qt_Creator.md) -- [Coding Guidelines](developers/Coding_Guidelines.md) -- [Bonus System](developers/Bonus_System.md) -- [Code Structure](developers/Code_Structure.md) -- [Logging API](developers/Logging_API.md) -- [Lua Scripting System](developers/Lua_Scripting_System.md) -- [Serialization](developers/Serialization.md) - -## Documentation and guidelines for maintainers - -- [Project Infrastructure](maintainers/Project_Infrastructure.md) -- [Release Process](maintainers/Release_Process.md) -- [Ubuntu PPA](maintainers/Ubuntu_PPA.md) - -## Copyright and license - -VCMI Project source code is licensed under GPL version 2 or later. -VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets - -Copyright (C) 2007-2023 VCMI Team (check AUTHORS file for the contributors list) +[![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.2) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) + +# VCMI Project + +VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities. + +## Links + + * Homepage: https://vcmi.eu/ + * Forums: https://forum.vcmi.eu/ + * Bugtracker: https://github.com/vcmi/vcmi/issues + * Slack: https://slack.vcmi.eu/ + * Discord: https://discord.gg/chBT42V + +## Latest release + +Latest release can be found [in Github Releases page](https://github.com/vcmi/vcmi/releases/latest). As of right now we plan to have major releases around 3 times per year. Daily builds are still available at [builds.vcmi.download](https://builds.vcmi.download/branch/develop/) but they are not guaranteed to be stable. So we encourage everybody to use them and report found bugs so that we can fix them. +Loading saves made with different major version of VCMI is usually **not** supported, so you may want to finish your ongoing games before updating. +Please see corresponding installation guide articles for details for your platform. + +## Installation guides +- [Windows](players/Installation_Windows.md) +- [macOS](players/Installation_macOS.md) +- [Linux](players/Installation_Linux.md) +- [Android](players/Installation_Android.md) +- [iOS](players/Installation_iOS.md) + +## Documentation and guidelines for players + +- [General information about VCMI Project](players/Manual.md) +- [Frequently asked questions](https://vcmi.eu/faq/) (external link) +- [Game mechanics](players/Game_Mechanics.md) +- [Bug reporting guidelines](players/Bug_Reporting_Guidelines.md) +- [Cheat codes](players/Cheat_Codes.md) +- [Privacy Policy](players/Privacy_Policy.md) + +## Documentation and guidelines for game modders + +- [Modding Guidelines](modders/Readme.md) +- [Mod File Format](modders/Mod_File_Format.md) +- [Bonus Format](modders/Bonus_Format.md) +- [Translations](modders/Translations.md) +- [Map Editor](modders/Map_Editor.md) +- [Campaign Format](modders/Campaign_Format.md) +- [Configurable Widgets](modders/Configurable_Widgets.md) + +## Documentation and guidelines for developers + +Development environment setup instructions: +- [Building VCMI for Android](developers/Building_Android.md) +- [Building VCMI for iOS](developers/Building_iOS.md) +- [Building VCMI for Linux](developers/Building_Linux.md) +- [Building VCMI for macOS](developers/Building_macOS.md) +- [Building VCMI for Windows](developers/Building_Windows.md) +- [Conan](developers/Conan.md) + +Engine documentation: (NOTE: may be outdated) +- [Development with Qt Creator](developers/Development_with_Qt_Creator.md) +- [Coding Guidelines](developers/Coding_Guidelines.md) +- [Bonus System](developers/Bonus_System.md) +- [Code Structure](developers/Code_Structure.md) +- [Logging API](developers/Logging_API.md) +- [Lua Scripting System](developers/Lua_Scripting_System.md) +- [Serialization](developers/Serialization.md) + +## Documentation and guidelines for maintainers + +- [Project Infrastructure](maintainers/Project_Infrastructure.md) +- [Release Process](maintainers/Release_Process.md) +- [Ubuntu PPA](maintainers/Ubuntu_PPA.md) + +## Copyright and license + +VCMI Project source code is licensed under GPL version 2 or later. +VCMI Project assets are licensed under CC-BY-SA 4.0. Assets sources and information about contributors are available under following link: https://github.com/vcmi/vcmi-assets + +Copyright (C) 2007-2023 VCMI Team (check AUTHORS file for the contributors list) diff --git a/launcher/StdInc.cpp b/launcher/StdInc.cpp index b64b59be5..6237e6e6f 100644 --- a/launcher/StdInc.cpp +++ b/launcher/StdInc.cpp @@ -1 +1 @@ -#include "StdInc.h" +#include "StdInc.h" diff --git a/launcher/StdInc.h b/launcher/StdInc.h index e97483820..def7e8ba4 100644 --- a/launcher/StdInc.h +++ b/launcher/StdInc.h @@ -1,31 +1,31 @@ -#pragma once - -#include "../Global.h" - -#include -#include -#include -#include -#include -#include -#include - -VCMI_LIB_USING_NAMESPACE - -inline QString pathToQString(const boost::filesystem::path & path) -{ -#ifdef VCMI_WINDOWS - return QString::fromStdWString(path.wstring()); -#else - return QString::fromStdString(path.string()); -#endif -} - -inline boost::filesystem::path qstringToPath(const QString & path) -{ -#ifdef VCMI_WINDOWS - return boost::filesystem::path(path.toStdWString()); -#else - return boost::filesystem::path(path.toUtf8().data()); -#endif -} +#pragma once + +#include "../Global.h" + +#include +#include +#include +#include +#include +#include +#include + +VCMI_LIB_USING_NAMESPACE + +inline QString pathToQString(const boost::filesystem::path & path) +{ +#ifdef VCMI_WINDOWS + return QString::fromStdWString(path.wstring()); +#else + return QString::fromStdString(path.string()); +#endif +} + +inline boost::filesystem::path qstringToPath(const QString & path) +{ +#ifdef VCMI_WINDOWS + return boost::filesystem::path(path.toStdWString()); +#else + return boost::filesystem::path(path.toUtf8().data()); +#endif +} diff --git a/launcher/main.cpp b/launcher/main.cpp index 3b626346b..cb0b38ac1 100644 --- a/launcher/main.cpp +++ b/launcher/main.cpp @@ -1,96 +1,96 @@ -/* - * main.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 "main.h" -#include "mainwindow_moc.h" - -#include -#include -#include -#include "../lib/VCMIDirs.h" - -// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332 -#ifdef VCMI_IOS -#if __has_include("QIOSIntegrationPlugin.h") -#include "QIOSIntegrationPlugin.h" -#endif -int argcForClient; -char ** argvForClient; -#endif - -int main(int argc, char * argv[]) -{ - int result; -#ifdef VCMI_IOS - { -#endif - QApplication vcmilauncher(argc, argv); - - MainWindow mainWindow; - mainWindow.show(); - result = vcmilauncher.exec(); -#ifdef VCMI_IOS - } - if (result == 0) - launchGame(argcForClient, argvForClient); -#endif - return result; -} - -void startGame(const QStringList & args) -{ - logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString()); - -#ifdef Q_OS_IOS - static const char clientName[] = "vcmiclient"; - argcForClient = args.size() + 1; //first argument is omitted - argvForClient = new char*[argcForClient]; - argvForClient[0] = new char[strlen(clientName)+1]; - strcpy(argvForClient[0], clientName); - for(int i = 1; i < argcForClient; ++i) - { - std::string s = args.at(i - 1).toStdString(); - argvForClient[i] = new char[s.size() + 1]; - strcpy(argvForClient[i], s.c_str()); - } - qApp->quit(); -#else - startExecutable(pathToQString(VCMIDirs::get().clientPath()), args); -#endif -} - -void startEditor(const QStringList & args) -{ -#ifdef ENABLE_EDITOR - startExecutable(pathToQString(VCMIDirs::get().mapEditorPath()), args); -#endif -} - -#ifndef Q_OS_IOS -void startExecutable(QString name, const QStringList & args) -{ - QProcess process; - - // Start the executable - if(process.startDetached(name, args)) - { - qApp->quit(); - } - else - { - QMessageBox::critical(qApp->activeWindow(), - "Error starting executable", - "Failed to start " + name + "\n" - "Reason: " + process.errorString(), - QMessageBox::Ok, - QMessageBox::Ok); - } -} -#endif +/* + * main.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 "main.h" +#include "mainwindow_moc.h" + +#include +#include +#include +#include "../lib/VCMIDirs.h" + +// Conan workaround https://github.com/conan-io/conan-center-index/issues/13332 +#ifdef VCMI_IOS +#if __has_include("QIOSIntegrationPlugin.h") +#include "QIOSIntegrationPlugin.h" +#endif +int argcForClient; +char ** argvForClient; +#endif + +int main(int argc, char * argv[]) +{ + int result; +#ifdef VCMI_IOS + { +#endif + QApplication vcmilauncher(argc, argv); + + MainWindow mainWindow; + mainWindow.show(); + result = vcmilauncher.exec(); +#ifdef VCMI_IOS + } + if (result == 0) + launchGame(argcForClient, argvForClient); +#endif + return result; +} + +void startGame(const QStringList & args) +{ + logGlobal->warn("Starting game with the arguments: %s", args.join(" ").toStdString()); + +#ifdef Q_OS_IOS + static const char clientName[] = "vcmiclient"; + argcForClient = args.size() + 1; //first argument is omitted + argvForClient = new char*[argcForClient]; + argvForClient[0] = new char[strlen(clientName)+1]; + strcpy(argvForClient[0], clientName); + for(int i = 1; i < argcForClient; ++i) + { + std::string s = args.at(i - 1).toStdString(); + argvForClient[i] = new char[s.size() + 1]; + strcpy(argvForClient[i], s.c_str()); + } + qApp->quit(); +#else + startExecutable(pathToQString(VCMIDirs::get().clientPath()), args); +#endif +} + +void startEditor(const QStringList & args) +{ +#ifdef ENABLE_EDITOR + startExecutable(pathToQString(VCMIDirs::get().mapEditorPath()), args); +#endif +} + +#ifndef Q_OS_IOS +void startExecutable(QString name, const QStringList & args) +{ + QProcess process; + + // Start the executable + if(process.startDetached(name, args)) + { + qApp->quit(); + } + else + { + QMessageBox::critical(qApp->activeWindow(), + "Error starting executable", + "Failed to start " + name + "\n" + "Reason: " + process.errorString(), + QMessageBox::Ok, + QMessageBox::Ok); + } +} +#endif diff --git a/launcher/main.h b/launcher/main.h index ee7f1ea3f..566394363 100644 --- a/launcher/main.h +++ b/launcher/main.h @@ -1,19 +1,19 @@ -/* - * main.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 - -void startGame(const QStringList & args); -void startEditor(const QStringList & args); - -#ifdef VCMI_IOS -extern "C" void launchGame(int argc, char * argv[]); -#else -void startExecutable(QString name, const QStringList & args); -#endif +/* + * main.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 + +void startGame(const QStringList & args); +void startEditor(const QStringList & args); + +#ifdef VCMI_IOS +extern "C" void launchGame(int argc, char * argv[]); +#else +void startExecutable(QString name, const QStringList & args); +#endif diff --git a/lib/AI_Base.h b/lib/AI_Base.h index cf4bf9c13..a6761c38f 100644 --- a/lib/AI_Base.h +++ b/lib/AI_Base.h @@ -1,14 +1,14 @@ -/* - * AI_Base.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 "CGameInterface.h" - -#define AI_INTERFACE_VER 1 +/* + * AI_Base.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 "CGameInterface.h" + +#define AI_INTERFACE_VER 1 diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b991d7dda..f26ba8df8 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -1,1205 +1,1205 @@ -/* - * CArtHandler.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 "ArtifactUtils.h" -#include "CGeneralTextHandler.h" -#include "GameSettings.h" -#include "mapObjects/MapObjects.h" -#include "constants/StringConstants.h" - -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "serializer/JsonSerializeFormat.h" - -// Note: list must match entries in ArtTraits.txt -#define ART_POS_LIST \ - ART_POS(SPELLBOOK) \ - ART_POS(MACH4) \ - ART_POS(MACH3) \ - ART_POS(MACH2) \ - ART_POS(MACH1) \ - ART_POS(MISC5) \ - ART_POS(MISC4) \ - ART_POS(MISC3) \ - ART_POS(MISC2) \ - ART_POS(MISC1) \ - ART_POS(FEET) \ - ART_POS(LEFT_RING) \ - ART_POS(RIGHT_RING) \ - ART_POS(TORSO) \ - ART_POS(LEFT_HAND) \ - ART_POS(RIGHT_HAND) \ - ART_POS(NECK) \ - ART_POS(SHOULDERS) \ - ART_POS(HEAD) - -VCMI_LIB_NAMESPACE_BEGIN - -bool CCombinedArtifact::isCombined() const -{ - return !(constituents.empty()); -} - -const std::vector & CCombinedArtifact::getConstituents() const -{ - return constituents; -} - -const std::vector & CCombinedArtifact::getPartOf() const -{ - return partOf; -} - -bool CScrollArtifact::isScroll() const -{ - return static_cast(this)->getId() == ArtifactID::SPELL_SCROLL; -} - -bool CGrowingArtifact::isGrowing() const -{ - return !bonusesPerLevel.empty() || !thresholdBonuses.empty(); -} - -std::vector > & CGrowingArtifact::getBonusesPerLevel() -{ - return bonusesPerLevel; -} - -const std::vector > & CGrowingArtifact::getBonusesPerLevel() const -{ - return bonusesPerLevel; -} - -std::vector > & CGrowingArtifact::getThresholdBonuses() -{ - return thresholdBonuses; -} - -const std::vector > & CGrowingArtifact::getThresholdBonuses() const -{ - return thresholdBonuses; -} - -int32_t CArtifact::getIndex() const -{ - return id.toEnum(); -} - -int32_t CArtifact::getIconIndex() const -{ - return iconIndex; -} - -std::string CArtifact::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -void CArtifact::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "ARTIFACT", image); - cb(getIconIndex(), 0, "ARTIFACTLARGE", large); -} - -ArtifactID CArtifact::getId() const -{ - return id; -} - -const IBonusBearer * CArtifact::getBonusBearer() const -{ - return this; -} - -std::string CArtifact::getDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getDescriptionTextID()); -} - -std::string CArtifact::getEventTranslated() const -{ - return VLC->generaltexth->translate(getEventTextID()); -} - -std::string CArtifact::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CArtifact::getDescriptionTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "description").get(); -} - -std::string CArtifact::getEventTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "event").get(); -} - -std::string CArtifact::getNameTextID() const -{ - return TextIdentifier("artifact", modScope, identifier, "name").get(); -} - -uint32_t CArtifact::getPrice() const -{ - return price; -} - -CreatureID CArtifact::getWarMachine() const -{ - return warMachine; -} - -bool CArtifact::isBig() const -{ - return warMachine != CreatureID::NONE; -} - -bool CArtifact::isTradable() const -{ - switch(id) - { - case ArtifactID::SPELLBOOK: - case ArtifactID::GRAIL: - return false; - default: - return !isBig(); - } -} - -bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const -{ - auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool - { - if(ArtifactUtils::isSlotBackpack(slot)) - { - if(isBig() || !ArtifactUtils::isBackpackFreeSlots(artSet)) - return false; - return true; - } - - if(!vstd::contains(possibleSlots.at(artSet->bearerType()), slot)) - return false; - - return artSet->isPositionFree(slot, assumeDestRemoved); - }; - - auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool - { - if(isCombined()) - { - if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved)) - return false; - if(ArtifactUtils::isSlotBackpack(slot)) - return true; - - CArtifactFittingSet fittingSet(artSet->bearerType()); - fittingSet.artifactsWorn = artSet->artifactsWorn; - if(assumeDestRemoved) - fittingSet.removeArtifact(slot); - - for(const auto art : constituents) - { - auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId()); - if(ArtifactUtils::isSlotEquipment(possibleSlot)) - { - fittingSet.setNewArtSlot(possibleSlot, nullptr, true); - } - else - { - return false; - } - } - return true; - } - else - { - return simpleArtCanBePutAt(artSet, slot, assumeDestRemoved); - } - }; - - if(slot == ArtifactPosition::TRANSITION_POS) - return true; - - if(slot == ArtifactPosition::FIRST_AVAILABLE) - { - for(const auto & slot : possibleSlots.at(artSet->bearerType())) - { - if(artCanBePutAt(artSet, slot, assumeDestRemoved)) - return true; - } - return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); - } - else if(ArtifactUtils::isSlotBackpack(slot)) - { - return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); - } - else - { - return artCanBePutAt(artSet, slot, assumeDestRemoved); - } -} - -CArtifact::CArtifact() - : iconIndex(ArtifactID::NONE), - price(0) -{ - setNodeType(ARTIFACT); - possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty - possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty - possibleSlots[ArtBearer::COMMANDER]; -} - -//This destructor should be placed here to avoid side effects -CArtifact::~CArtifact() = default; - -int CArtifact::getArtClassSerial() const -{ - if(id == ArtifactID::SPELL_SCROLL) - return 4; - switch(aClass) - { - case ART_TREASURE: - return 0; - case ART_MINOR: - return 1; - case ART_MAJOR: - return 2; - case ART_RELIC: - return 3; - case ART_SPECIAL: - return 5; - } - - return -1; -} - -std::string CArtifact::nodeName() const -{ - return "Artifact: " + getNameTranslated(); -} - -void CArtifact::addNewBonus(const std::shared_ptr& b) -{ - b->source = BonusSource::ARTIFACT; - b->duration = BonusDuration::PERMANENT; - b->description = getNameTranslated(); - CBonusSystemNode::addNewBonus(b); -} - -const std::map> & CArtifact::getPossibleSlots() const -{ - return possibleSlots; -} - -void CArtifact::updateFrom(const JsonNode& data) -{ - //TODO:CArtifact::updateFrom -} - -void CArtifact::setImage(int32_t iconIndex, std::string image, std::string large) -{ - this->iconIndex = iconIndex; - this->image = image; - this->large = large; -} - -CArtHandler::~CArtHandler() = default; - -std::vector CArtHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ARTIFACT); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - #define ART_POS(x) #x , - const std::vector artSlots = { ART_POS_LIST }; - #undef ART_POS - - static std::map classes = - {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; - - CLegacyConfigParser parser(TextPath::builtin("DATA/ARTRAITS.TXT")); - CLegacyConfigParser events(TextPath::builtin("DATA/ARTEVENT.TXT")); - - parser.endLine(); // header - parser.endLine(); - - for (size_t i = 0; i < dataSize; i++) - { - JsonNode artData; - - artData["text"]["name"].String() = parser.readString(); - artData["text"]["event"].String() = events.readString(); - artData["value"].Float() = parser.readNumber(); - - for(const auto & artSlot : artSlots) - { - if(parser.readString() == "x") - { - artData["slot"].Vector().push_back(JsonNode()); - artData["slot"].Vector().back().String() = artSlot; - } - } - artData["class"].String() = classes[parser.readString()[0]]; - artData["text"]["description"].String() = parser.readString(); - - parser.endLine(); - events.endLine(); - h3Data.push_back(artData); - } - return h3Data; -} - -void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) -{ - auto * object = loadFromJson(scope, data, name, objects.size()); - - object->iconIndex = object->getIndex() + 5; - - objects.emplace_back(object); - - registerObject(scope, "artifact", name, object->id); -} - -void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto * object = loadFromJson(scope, data, name, index); - - object->iconIndex = object->getIndex(); - - assert(objects[index] == nullptr); // ensure that this id was not loaded before - objects[index] = object; - - registerObject(scope, "artifact", name, object->id); -} - -const std::vector & CArtHandler::getTypeNames() const -{ - static const std::vector typeNames = { "artifact" }; - return typeNames; -} - -CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - assert(!scope.empty()); - - CArtifact * art = new CArtifact(); - if(!node["growing"].isNull()) - { - for(auto bonus : node["growing"]["bonusesPerLevel"].Vector()) - { - art->bonusesPerLevel.emplace_back(static_cast(bonus["level"].Float()), Bonus()); - JsonUtils::parseBonus(bonus["bonus"], &art->bonusesPerLevel.back().second); - } - for(auto bonus : node["growing"]["thresholdBonuses"].Vector()) - { - art->thresholdBonuses.emplace_back(static_cast(bonus["level"].Float()), Bonus()); - JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second); - } - } - art->id = ArtifactID(index); - art->identifier = identifier; - art->modScope = scope; - - const JsonNode & text = node["text"]; - - VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String()); - VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String()); - VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String()); - - const JsonNode & graphics = node["graphics"]; - art->image = graphics["image"].String(); - - if(!graphics["large"].isNull()) - art->large = graphics["large"].String(); - else - art->large = art->image; - - art->advMapDef = graphics["map"].String(); - - art->price = static_cast(node["value"].Float()); - art->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); - - loadSlots(art, node); - loadClass(art, node); - loadType(art, node); - loadComponents(art, node); - - for(const auto & b : node["bonuses"].Vector()) - { - auto bonus = JsonUtils::parseBonus(b); - art->addNewBonus(bonus); - } - - const JsonNode & warMachine = node["warMachine"]; - if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) - { - VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id) - { - art->warMachine = CreatureID(id); - - //this assumes that creature object is stored before registration - VLC->creh->objects.at(id)->warMachine = art->id; - }); - } - - VLC->identifiers()->requestIdentifier(scope, "object", "artifact", [=](si32 index) - { - JsonNode conf; - conf.setMeta(scope); - - VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex()); - - if(!art->advMapDef.empty()) - { - JsonNode templ; - templ["animation"].String() = art->advMapDef; - templ.setMeta(scope); - - // add new template. - // Necessary for objects added via mods that don't have any templates in H3 - VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); - } - // object does not have any templates - this is not usable object (e.g. pseudo-art like lock) - if(VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::ARTIFACT, art->getIndex()); - }); - - return art; -} - -int32_t ArtifactPositionBase::decode(const std::string & slotName) -{ -#define ART_POS(x) { #x, ArtifactPosition::x }, - static const std::map artifactPositionMap = { ART_POS_LIST }; -#undef ART_POS - auto it = artifactPositionMap.find (slotName); - if (it != artifactPositionMap.end()) - return it->second; - else - return PRE_FIRST; -} - -void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const -{ - static const std::vector miscSlots = - { - ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 - }; - - static const std::vector ringSlots = - { - ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING - }; - - if (slotID == "MISC") - { - vstd::concatenate(art->possibleSlots[ArtBearer::HERO], miscSlots); - } - else if (slotID == "RING") - { - vstd::concatenate(art->possibleSlots[ArtBearer::HERO], ringSlots); - } - else - { - auto slot = ArtifactPosition::decode(slotID); - if (slot != ArtifactPosition::PRE_FIRST) - art->possibleSlots[ArtBearer::HERO].push_back(slot); - } -} - -void CArtHandler::loadSlots(CArtifact * art, const JsonNode & node) const -{ - if (!node["slot"].isNull()) //we assume non-hero slots are irrelevant? - { - if (node["slot"].getType() == JsonNode::JsonType::DATA_STRING) - addSlot(art, node["slot"].String()); - else - { - for (const JsonNode & slot : node["slot"].Vector()) - addSlot(art, slot.String()); - } - std::sort(art->possibleSlots.at(ArtBearer::HERO).begin(), art->possibleSlots.at(ArtBearer::HERO).end()); - } -} - -CArtifact::EartClass CArtHandler::stringToClass(const std::string & className) -{ - static const std::map artifactClassMap = - { - {"TREASURE", CArtifact::ART_TREASURE}, - {"MINOR", CArtifact::ART_MINOR}, - {"MAJOR", CArtifact::ART_MAJOR}, - {"RELIC", CArtifact::ART_RELIC}, - {"SPECIAL", CArtifact::ART_SPECIAL} - }; - - auto it = artifactClassMap.find (className); - if (it != artifactClassMap.end()) - return it->second; - - logMod->warn("Warning! Artifact rarity %s not recognized!", className); - return CArtifact::ART_SPECIAL; -} - -void CArtHandler::loadClass(CArtifact * art, const JsonNode & node) const -{ - art->aClass = stringToClass(node["class"].String()); -} - -void CArtHandler::loadType(CArtifact * art, const JsonNode & node) const -{ -#define ART_BEARER(x) { #x, ArtBearer::x }, - static const std::map artifactBearerMap = { ART_BEARER_LIST }; -#undef ART_BEARER - - for (const JsonNode & b : node["type"].Vector()) - { - auto it = artifactBearerMap.find (b.String()); - if (it != artifactBearerMap.end()) - { - int bearerType = it->second; - switch (bearerType) - { - case ArtBearer::HERO://TODO: allow arts having several possible bearers - break; - case ArtBearer::COMMANDER: - makeItCommanderArt (art); //original artifacts should have only one bearer type - break; - case ArtBearer::CREATURE: - makeItCreatureArt (art); - break; - } - } - else - logMod->warn("Warning! Artifact type %s not recognized!", b.String()); - } -} - -void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) -{ - if (!node["components"].isNull()) - { - for(const auto & component : node["components"].Vector()) - { - VLC->identifiers()->requestIdentifier("artifact", component, [=](si32 id) - { - // when this code is called both combinational art as well as component are loaded - // so it is safe to access any of them - art->constituents.push_back(objects[id]); - objects[id]->partOf.push_back(art); - }); - } - } -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) -{ - auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) - { - if (arts->empty()) //restock available arts - fillList(*arts, flag); - - for (auto & arts_i : *arts) - { - if (accepts(arts_i->id)) - { - CArtifact *art = arts_i; - out.emplace_back(art); - } - } - }; - - auto getAllowed = [&](std::vector > &out) - { - if (flags & CArtifact::ART_TREASURE) - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - if (flags & CArtifact::ART_MINOR) - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - if (flags & CArtifact::ART_MAJOR) - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - if (flags & CArtifact::ART_RELIC) - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - if(out.empty()) //no artifact of specified rarity, we need to take another one - { - getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); - getAllowedArts (out, &minors, CArtifact::ART_MINOR); - getAllowedArts (out, &majors, CArtifact::ART_MAJOR); - getAllowedArts (out, &relics, CArtifact::ART_RELIC); - } - if(out.empty()) //no arts are available at all - { - out.resize (64); - std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully) - } - }; - - std::vector > out; - getAllowed(out); - ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; - erasePickedArt(artID); - return artID; -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) -{ - return pickRandomArtifact(rand, 0xff, std::move(accepts)); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) -{ - return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); -} - -void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) -{ - if (onlyCreature) - { - a->possibleSlots[ArtBearer::HERO].clear(); - a->possibleSlots[ArtBearer::COMMANDER].clear(); - } - a->possibleSlots[ArtBearer::CREATURE].push_back(ArtifactPosition::CREATURE_SLOT); -} - -void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) -{ - if (onlyCommander) - { - a->possibleSlots[ArtBearer::HERO].clear(); - a->possibleSlots[ArtBearer::CREATURE].clear(); - } - for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i) - a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i)); -} - -bool CArtHandler::legalArtifact(const ArtifactID & id) -{ - auto art = objects[id]; - //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components - - if(art->isCombined()) - return false; //no combo artifacts spawning - - if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) - return false; // invalid class - - if(!art->possibleSlots[ArtBearer::HERO].empty()) - return true; - - if(!art->possibleSlots[ArtBearer::CREATURE].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) - return true; - - if(!art->possibleSlots[ArtBearer::COMMANDER].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) - return true; - - return false; -} - -void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) -{ - allowedArtifacts.clear(); - treasures.clear(); - minors.clear(); - majors.clear(); - relics.clear(); - - for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) - { - if (allowed[i] && legalArtifact(ArtifactID(i))) - allowedArtifacts.push_back(objects[i]); - //keep im mind that artifact can be worn by more than one type of bearer - } -} - -std::vector CArtHandler::getDefaultAllowed() const -{ - std::vector allowedArtifacts; - allowedArtifacts.resize(127, true); - allowedArtifacts.resize(141, false); - allowedArtifacts.resize(size(), true); - return allowedArtifacts; -} - -void CArtHandler::erasePickedArt(const ArtifactID & id) -{ - CArtifact *art = objects[id]; - - std::vector * artifactList = nullptr; - switch(art->aClass) - { - case CArtifact::ART_TREASURE: - artifactList = &treasures; - break; - case CArtifact::ART_MINOR: - artifactList = &minors; - break; - case CArtifact::ART_MAJOR: - artifactList = &majors; - break; - case CArtifact::ART_RELIC: - artifactList = &relics; - break; - default: - logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated()); - return; - } - - if(artifactList->empty()) - fillList(*artifactList, art->aClass); - - auto itr = vstd::find(*artifactList, art); - if(itr != artifactList->end()) - { - artifactList->erase(itr); - } - else - logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated()); -} - -void CArtHandler::fillList( std::vector &listToBeFilled, CArtifact::EartClass artifactClass ) -{ - assert(listToBeFilled.empty()); - for (auto & elem : allowedArtifacts) - { - if (elem->aClass == artifactClass) - listToBeFilled.push_back(elem); - } -} - -void CArtHandler::afterLoadFinalization() -{ - //All artifacts have their id, so we can properly update their bonuses' source ids. - for(auto &art : objects) - { - for(auto &bonus : art->getExportedBonusList()) - { - assert(art == objects[art->id]); - assert(bonus->source == BonusSource::ARTIFACT); - bonus->sid = art->id; - } - } - CBonusSystemNode::treeHasChanged(); -} - -CArtifactSet::~CArtifactSet() = default; - -const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const -{ - if(const ArtSlotInfo * si = getSlot(pos)) - { - if(si->artifact && (!excludeLocked || !si->locked)) - return si->artifact; - } - - return nullptr; -} - -CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) -{ - return const_cast((const_cast(this))->getArt(pos, excludeLocked)); -} - -ArtifactPosition CArtifactSet::getArtPos(const ArtifactID & aid, bool onlyWorn, bool allowLocked) const -{ - const auto result = getAllArtPositions(aid, onlyWorn, allowLocked, false); - return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0]; -} - -std::vector CArtifactSet::getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const -{ - std::vector result; - for(const auto & slotInfo : artifactsWorn) - if(slotInfo.second.artifact->getTypeId() == aid && (allowLocked || !slotInfo.second.locked)) - result.push_back(slotInfo.first); - - if(onlyWorn) - return result; - if(!getAll && !result.empty()) - return result; - - auto backpackPositions = getBackpackArtPositions(aid); - result.insert(result.end(), backpackPositions.begin(), backpackPositions.end()); - return result; -} - -std::vector CArtifactSet::getBackpackArtPositions(const ArtifactID & aid) const -{ - std::vector result; - - si32 backpackPosition = ArtifactPosition::BACKPACK_START; - for(const auto & artInfo : artifactsInBackpack) - { - const auto * art = artInfo.getArt(); - if(art && art->artType->getId() == aid) - result.emplace_back(backpackPosition); - backpackPosition++; - } - return result; -} - -ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance *art) const -{ - for(auto i : artifactsWorn) - if(i.second.artifact == art) - return i.first; - - for(int i = 0; i < artifactsInBackpack.size(); i++) - if(artifactsInBackpack[i].artifact == art) - return ArtifactPosition::BACKPACK_START + i; - - return ArtifactPosition::PRE_FIRST; -} - -const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const -{ - for(auto i : artifactsWorn) - if(i.second.artifact->getId() == artInstId) - return i.second.artifact; - - for(auto i : artifactsInBackpack) - if(i.artifact->getId() == artInstId) - return i.artifact; - - return nullptr; -} - -const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * artInst) const -{ - if(artInst) - { - for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType())) - if(getArt(slot) == artInst) - return slot; - - ArtifactPosition backpackSlot = ArtifactPosition::BACKPACK_START; - for(auto & slotInfo : artifactsInBackpack) - { - if(slotInfo.getArt() == artInst) - return backpackSlot; - backpackSlot = ArtifactPosition(backpackSlot + 1); - } - } - return ArtifactPosition::PRE_FIRST; -} - -bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const -{ - return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0; -} - -bool CArtifactSet::hasArtBackpack(const ArtifactID & aid) const -{ - return !getBackpackArtPositions(aid).empty(); -} - -unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const -{ - const auto allPositions = getAllArtPositions(aid, onlyWorn, allowLocked, true); - if(!allPositions.empty()) - return allPositions.size(); - - if(searchBackpackAssemblies && getHiddenArt(aid)) - return 1; - - return 0; -} - -CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) -{ - ArtPlacementMap resArtPlacement; - - setNewArtSlot(slot, art, false); - if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) - { - const CArtifactInstance * mainPart = nullptr; - for(const auto & part : art->getPartsInfo()) - if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot) - && (part.slot == ArtifactPosition::PRE_FIRST)) - { - mainPart = part.art; - break; - } - - for(const auto & part : art->getPartsInfo()) - { - if(part.art != mainPart) - { - auto partSlot = part.slot; - if(!part.art->artType->canBePutAt(this, partSlot)) - partSlot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); - - assert(ArtifactUtils::isSlotEquipment(partSlot)); - setNewArtSlot(partSlot, part.art, true); - resArtPlacement.emplace(std::make_pair(part.art, partSlot)); - } - else - { - resArtPlacement.emplace(std::make_pair(part.art, part.slot)); - } - } - } - return resArtPlacement; -} - -void CArtifactSet::removeArtifact(ArtifactPosition slot) -{ - auto art = getArt(slot, false); - if(art) - { - if(art->isCombined()) - { - for(auto & part : art->getPartsInfo()) - { - if(getArt(part.slot, false)) - eraseArtSlot(part.slot); - } - } - eraseArtSlot(slot); - } -} - -std::pair CArtifactSet::searchForConstituent(const ArtifactID & aid) const -{ - for(const auto & slot : artifactsInBackpack) - { - auto art = slot.artifact; - if(art->isCombined()) - { - for(auto & ci : art->getPartsInfo()) - { - if(ci.art->getTypeId() == aid) - { - return {art, ci.art}; - } - } - } - } - return {nullptr, nullptr}; -} - -const CArtifactInstance * CArtifactSet::getHiddenArt(const ArtifactID & aid) const -{ - return searchForConstituent(aid).second; -} - -const CArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const -{ - return searchForConstituent(aid).first; -} - -const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const -{ - if(pos == ArtifactPosition::TRANSITION_POS) - { - // Always add to the end. Always take from the beginning. - if(artifactsTransitionPos.empty()) - return nullptr; - else - return &(*artifactsTransitionPos.begin()); - } - if(vstd::contains(artifactsWorn, pos)) - return &artifactsWorn.at(pos); - if(pos >= ArtifactPosition::AFTER_LAST ) - { - int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START; - if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) - return nullptr; - else - return &artifactsInBackpack[backpackPos]; - } - - return nullptr; -} - -bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const -{ - if(const ArtSlotInfo *s = getSlot(pos)) - return (onlyLockCheck || !s->artifact) && !s->locked; - - return true; //no slot means not used -} - -void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked) -{ - assert(!vstd::contains(artifactsWorn, slot)); - - ArtSlotInfo * slotInfo; - if(slot == ArtifactPosition::TRANSITION_POS) - { - // Always add to the end. Always take from the beginning. - artifactsTransitionPos.emplace_back(); - slotInfo = &artifactsTransitionPos.back(); - } - else if(ArtifactUtils::isSlotEquipment(slot)) - { - slotInfo = &artifactsWorn[slot]; - } - else - { - auto position = artifactsInBackpack.begin() + slot - ArtifactPosition::BACKPACK_START; - slotInfo = &(*artifactsInBackpack.emplace(position, ArtSlotInfo())); - } - slotInfo->artifact = art; - slotInfo->locked = locked; -} - -void CArtifactSet::eraseArtSlot(const ArtifactPosition & slot) -{ - if(slot == ArtifactPosition::TRANSITION_POS) - { - assert(!artifactsTransitionPos.empty()); - artifactsTransitionPos.erase(artifactsTransitionPos.begin()); - } - else if(ArtifactUtils::isSlotBackpack(slot)) - { - auto backpackSlot = ArtifactPosition(slot - ArtifactPosition::BACKPACK_START); - - assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); - artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); - } - else - { - artifactsWorn.erase(slot); - } -} - -void CArtifactSet::artDeserializationFix(CBonusSystemNode *node) -{ - for(auto & elem : artifactsWorn) - if(elem.second.artifact && !elem.second.locked) - node->attachTo(*elem.second.artifact); -} - -void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map) -{ - //todo: creature and commander artifacts - if(handler.saving && artifactsInBackpack.empty() && artifactsWorn.empty()) - return; - - if(!handler.saving) - { - assert(map); - artifactsInBackpack.clear(); - artifactsWorn.clear(); - } - - auto s = handler.enterStruct(fieldName); - - switch(bearerType()) - { - case ArtBearer::HERO: - serializeJsonHero(handler, map); - break; - case ArtBearer::CREATURE: - serializeJsonCreature(handler, map); - break; - case ArtBearer::COMMANDER: - serializeJsonCommander(handler, map); - break; - default: - assert(false); - break; - } -} - -void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) -{ - for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1)) - { - serializeJsonSlot(handler, ap, map); - } - - std::vector backpackTemp; - - if(handler.saving) - { - backpackTemp.reserve(artifactsInBackpack.size()); - for(const ArtSlotInfo & info : artifactsInBackpack) - backpackTemp.push_back(info.artifact->getTypeId()); - } - handler.serializeIdArray(NArtifactPosition::backpack, backpackTemp); - if(!handler.saving) - { - for(const ArtifactID & artifactID : backpackTemp) - { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - auto slot = ArtifactPosition::BACKPACK_START + artifactsInBackpack.size(); - if(artifact->artType->canBePutAt(this, slot)) - { - auto artsMap = putArtifact(slot, artifact); - artifact->addPlacementMap(artsMap); - } - } - } -} - -void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler, CMap * map) -{ - logGlobal->error("CArtifactSet::serializeJsonCreature not implemented"); -} - -void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler, CMap * map) -{ - logGlobal->error("CArtifactSet::serializeJsonCommander not implemented"); -} - -void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map) -{ - ArtifactID artifactID; - - if(handler.saving) - { - const ArtSlotInfo * info = getSlot(slot); - - if(info != nullptr && !info->locked) - { - artifactID = info->artifact->getTypeId(); - handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); - } - } - else - { - handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); - - if(artifactID != ArtifactID::NONE) - { - auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); - - if(artifact->artType->canBePutAt(this, slot)) - { - auto artsMap = putArtifact(slot, artifact); - artifact->addPlacementMap(artsMap); - } - else - { - logGlobal->debug("Artifact can't be put at the specified location."); //TODO add more debugging information - } - } - } -} - -CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer Bearer): - Bearer(Bearer) -{ -} - -ArtBearer::ArtBearer CArtifactFittingSet::bearerType() const -{ - return this->Bearer; -} - -VCMI_LIB_NAMESPACE_END +/* + * CArtHandler.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 "ArtifactUtils.h" +#include "CGeneralTextHandler.h" +#include "GameSettings.h" +#include "mapObjects/MapObjects.h" +#include "constants/StringConstants.h" + +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "serializer/JsonSerializeFormat.h" + +// Note: list must match entries in ArtTraits.txt +#define ART_POS_LIST \ + ART_POS(SPELLBOOK) \ + ART_POS(MACH4) \ + ART_POS(MACH3) \ + ART_POS(MACH2) \ + ART_POS(MACH1) \ + ART_POS(MISC5) \ + ART_POS(MISC4) \ + ART_POS(MISC3) \ + ART_POS(MISC2) \ + ART_POS(MISC1) \ + ART_POS(FEET) \ + ART_POS(LEFT_RING) \ + ART_POS(RIGHT_RING) \ + ART_POS(TORSO) \ + ART_POS(LEFT_HAND) \ + ART_POS(RIGHT_HAND) \ + ART_POS(NECK) \ + ART_POS(SHOULDERS) \ + ART_POS(HEAD) + +VCMI_LIB_NAMESPACE_BEGIN + +bool CCombinedArtifact::isCombined() const +{ + return !(constituents.empty()); +} + +const std::vector & CCombinedArtifact::getConstituents() const +{ + return constituents; +} + +const std::vector & CCombinedArtifact::getPartOf() const +{ + return partOf; +} + +bool CScrollArtifact::isScroll() const +{ + return static_cast(this)->getId() == ArtifactID::SPELL_SCROLL; +} + +bool CGrowingArtifact::isGrowing() const +{ + return !bonusesPerLevel.empty() || !thresholdBonuses.empty(); +} + +std::vector > & CGrowingArtifact::getBonusesPerLevel() +{ + return bonusesPerLevel; +} + +const std::vector > & CGrowingArtifact::getBonusesPerLevel() const +{ + return bonusesPerLevel; +} + +std::vector > & CGrowingArtifact::getThresholdBonuses() +{ + return thresholdBonuses; +} + +const std::vector > & CGrowingArtifact::getThresholdBonuses() const +{ + return thresholdBonuses; +} + +int32_t CArtifact::getIndex() const +{ + return id.toEnum(); +} + +int32_t CArtifact::getIconIndex() const +{ + return iconIndex; +} + +std::string CArtifact::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +void CArtifact::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "ARTIFACT", image); + cb(getIconIndex(), 0, "ARTIFACTLARGE", large); +} + +ArtifactID CArtifact::getId() const +{ + return id; +} + +const IBonusBearer * CArtifact::getBonusBearer() const +{ + return this; +} + +std::string CArtifact::getDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getDescriptionTextID()); +} + +std::string CArtifact::getEventTranslated() const +{ + return VLC->generaltexth->translate(getEventTextID()); +} + +std::string CArtifact::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CArtifact::getDescriptionTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "description").get(); +} + +std::string CArtifact::getEventTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "event").get(); +} + +std::string CArtifact::getNameTextID() const +{ + return TextIdentifier("artifact", modScope, identifier, "name").get(); +} + +uint32_t CArtifact::getPrice() const +{ + return price; +} + +CreatureID CArtifact::getWarMachine() const +{ + return warMachine; +} + +bool CArtifact::isBig() const +{ + return warMachine != CreatureID::NONE; +} + +bool CArtifact::isTradable() const +{ + switch(id) + { + case ArtifactID::SPELLBOOK: + case ArtifactID::GRAIL: + return false; + default: + return !isBig(); + } +} + +bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const +{ + auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool + { + if(ArtifactUtils::isSlotBackpack(slot)) + { + if(isBig() || !ArtifactUtils::isBackpackFreeSlots(artSet)) + return false; + return true; + } + + if(!vstd::contains(possibleSlots.at(artSet->bearerType()), slot)) + return false; + + return artSet->isPositionFree(slot, assumeDestRemoved); + }; + + auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool + { + if(isCombined()) + { + if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved)) + return false; + if(ArtifactUtils::isSlotBackpack(slot)) + return true; + + CArtifactFittingSet fittingSet(artSet->bearerType()); + fittingSet.artifactsWorn = artSet->artifactsWorn; + if(assumeDestRemoved) + fittingSet.removeArtifact(slot); + + for(const auto art : constituents) + { + auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId()); + if(ArtifactUtils::isSlotEquipment(possibleSlot)) + { + fittingSet.setNewArtSlot(possibleSlot, nullptr, true); + } + else + { + return false; + } + } + return true; + } + else + { + return simpleArtCanBePutAt(artSet, slot, assumeDestRemoved); + } + }; + + if(slot == ArtifactPosition::TRANSITION_POS) + return true; + + if(slot == ArtifactPosition::FIRST_AVAILABLE) + { + for(const auto & slot : possibleSlots.at(artSet->bearerType())) + { + if(artCanBePutAt(artSet, slot, assumeDestRemoved)) + return true; + } + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); + } + else if(ArtifactUtils::isSlotBackpack(slot)) + { + return artCanBePutAt(artSet, ArtifactPosition::BACKPACK_START, assumeDestRemoved); + } + else + { + return artCanBePutAt(artSet, slot, assumeDestRemoved); + } +} + +CArtifact::CArtifact() + : iconIndex(ArtifactID::NONE), + price(0) +{ + setNodeType(ARTIFACT); + possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty + possibleSlots[ArtBearer::CREATURE]; //we want to generate map entry even if it will be empty + possibleSlots[ArtBearer::COMMANDER]; +} + +//This destructor should be placed here to avoid side effects +CArtifact::~CArtifact() = default; + +int CArtifact::getArtClassSerial() const +{ + if(id == ArtifactID::SPELL_SCROLL) + return 4; + switch(aClass) + { + case ART_TREASURE: + return 0; + case ART_MINOR: + return 1; + case ART_MAJOR: + return 2; + case ART_RELIC: + return 3; + case ART_SPECIAL: + return 5; + } + + return -1; +} + +std::string CArtifact::nodeName() const +{ + return "Artifact: " + getNameTranslated(); +} + +void CArtifact::addNewBonus(const std::shared_ptr& b) +{ + b->source = BonusSource::ARTIFACT; + b->duration = BonusDuration::PERMANENT; + b->description = getNameTranslated(); + CBonusSystemNode::addNewBonus(b); +} + +const std::map> & CArtifact::getPossibleSlots() const +{ + return possibleSlots; +} + +void CArtifact::updateFrom(const JsonNode& data) +{ + //TODO:CArtifact::updateFrom +} + +void CArtifact::setImage(int32_t iconIndex, std::string image, std::string large) +{ + this->iconIndex = iconIndex; + this->image = image; + this->large = large; +} + +CArtHandler::~CArtHandler() = default; + +std::vector CArtHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_ARTIFACT); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + #define ART_POS(x) #x , + const std::vector artSlots = { ART_POS_LIST }; + #undef ART_POS + + static std::map classes = + {{'S',"SPECIAL"}, {'T',"TREASURE"},{'N',"MINOR"},{'J',"MAJOR"},{'R',"RELIC"},}; + + CLegacyConfigParser parser(TextPath::builtin("DATA/ARTRAITS.TXT")); + CLegacyConfigParser events(TextPath::builtin("DATA/ARTEVENT.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for (size_t i = 0; i < dataSize; i++) + { + JsonNode artData; + + artData["text"]["name"].String() = parser.readString(); + artData["text"]["event"].String() = events.readString(); + artData["value"].Float() = parser.readNumber(); + + for(const auto & artSlot : artSlots) + { + if(parser.readString() == "x") + { + artData["slot"].Vector().push_back(JsonNode()); + artData["slot"].Vector().back().String() = artSlot; + } + } + artData["class"].String() = classes[parser.readString()[0]]; + artData["text"]["description"].String() = parser.readString(); + + parser.endLine(); + events.endLine(); + h3Data.push_back(artData); + } + return h3Data; +} + +void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data) +{ + auto * object = loadFromJson(scope, data, name, objects.size()); + + object->iconIndex = object->getIndex() + 5; + + objects.emplace_back(object); + + registerObject(scope, "artifact", name, object->id); +} + +void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto * object = loadFromJson(scope, data, name, index); + + object->iconIndex = object->getIndex(); + + assert(objects[index] == nullptr); // ensure that this id was not loaded before + objects[index] = object; + + registerObject(scope, "artifact", name, object->id); +} + +const std::vector & CArtHandler::getTypeNames() const +{ + static const std::vector typeNames = { "artifact" }; + return typeNames; +} + +CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + assert(!scope.empty()); + + CArtifact * art = new CArtifact(); + if(!node["growing"].isNull()) + { + for(auto bonus : node["growing"]["bonusesPerLevel"].Vector()) + { + art->bonusesPerLevel.emplace_back(static_cast(bonus["level"].Float()), Bonus()); + JsonUtils::parseBonus(bonus["bonus"], &art->bonusesPerLevel.back().second); + } + for(auto bonus : node["growing"]["thresholdBonuses"].Vector()) + { + art->thresholdBonuses.emplace_back(static_cast(bonus["level"].Float()), Bonus()); + JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second); + } + } + art->id = ArtifactID(index); + art->identifier = identifier; + art->modScope = scope; + + const JsonNode & text = node["text"]; + + VLC->generaltexth->registerString(scope, art->getNameTextID(), text["name"].String()); + VLC->generaltexth->registerString(scope, art->getDescriptionTextID(), text["description"].String()); + VLC->generaltexth->registerString(scope, art->getEventTextID(), text["event"].String()); + + const JsonNode & graphics = node["graphics"]; + art->image = graphics["image"].String(); + + if(!graphics["large"].isNull()) + art->large = graphics["large"].String(); + else + art->large = art->image; + + art->advMapDef = graphics["map"].String(); + + art->price = static_cast(node["value"].Float()); + art->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); + + loadSlots(art, node); + loadClass(art, node); + loadType(art, node); + loadComponents(art, node); + + for(const auto & b : node["bonuses"].Vector()) + { + auto bonus = JsonUtils::parseBonus(b); + art->addNewBonus(bonus); + } + + const JsonNode & warMachine = node["warMachine"]; + if(warMachine.getType() == JsonNode::JsonType::DATA_STRING && !warMachine.String().empty()) + { + VLC->identifiers()->requestIdentifier("creature", warMachine, [=](si32 id) + { + art->warMachine = CreatureID(id); + + //this assumes that creature object is stored before registration + VLC->creh->objects.at(id)->warMachine = art->id; + }); + } + + VLC->identifiers()->requestIdentifier(scope, "object", "artifact", [=](si32 index) + { + JsonNode conf; + conf.setMeta(scope); + + VLC->objtypeh->loadSubObject(art->identifier, conf, Obj::ARTIFACT, art->getIndex()); + + if(!art->advMapDef.empty()) + { + JsonNode templ; + templ["animation"].String() = art->advMapDef; + templ.setMeta(scope); + + // add new template. + // Necessary for objects added via mods that don't have any templates in H3 + VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); + } + // object does not have any templates - this is not usable object (e.g. pseudo-art like lock) + if(VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->getTemplates().empty()) + VLC->objtypeh->removeSubObject(Obj::ARTIFACT, art->getIndex()); + }); + + return art; +} + +int32_t ArtifactPositionBase::decode(const std::string & slotName) +{ +#define ART_POS(x) { #x, ArtifactPosition::x }, + static const std::map artifactPositionMap = { ART_POS_LIST }; +#undef ART_POS + auto it = artifactPositionMap.find (slotName); + if (it != artifactPositionMap.end()) + return it->second; + else + return PRE_FIRST; +} + +void CArtHandler::addSlot(CArtifact * art, const std::string & slotID) const +{ + static const std::vector miscSlots = + { + ArtifactPosition::MISC1, ArtifactPosition::MISC2, ArtifactPosition::MISC3, ArtifactPosition::MISC4, ArtifactPosition::MISC5 + }; + + static const std::vector ringSlots = + { + ArtifactPosition::RIGHT_RING, ArtifactPosition::LEFT_RING + }; + + if (slotID == "MISC") + { + vstd::concatenate(art->possibleSlots[ArtBearer::HERO], miscSlots); + } + else if (slotID == "RING") + { + vstd::concatenate(art->possibleSlots[ArtBearer::HERO], ringSlots); + } + else + { + auto slot = ArtifactPosition::decode(slotID); + if (slot != ArtifactPosition::PRE_FIRST) + art->possibleSlots[ArtBearer::HERO].push_back(slot); + } +} + +void CArtHandler::loadSlots(CArtifact * art, const JsonNode & node) const +{ + if (!node["slot"].isNull()) //we assume non-hero slots are irrelevant? + { + if (node["slot"].getType() == JsonNode::JsonType::DATA_STRING) + addSlot(art, node["slot"].String()); + else + { + for (const JsonNode & slot : node["slot"].Vector()) + addSlot(art, slot.String()); + } + std::sort(art->possibleSlots.at(ArtBearer::HERO).begin(), art->possibleSlots.at(ArtBearer::HERO).end()); + } +} + +CArtifact::EartClass CArtHandler::stringToClass(const std::string & className) +{ + static const std::map artifactClassMap = + { + {"TREASURE", CArtifact::ART_TREASURE}, + {"MINOR", CArtifact::ART_MINOR}, + {"MAJOR", CArtifact::ART_MAJOR}, + {"RELIC", CArtifact::ART_RELIC}, + {"SPECIAL", CArtifact::ART_SPECIAL} + }; + + auto it = artifactClassMap.find (className); + if (it != artifactClassMap.end()) + return it->second; + + logMod->warn("Warning! Artifact rarity %s not recognized!", className); + return CArtifact::ART_SPECIAL; +} + +void CArtHandler::loadClass(CArtifact * art, const JsonNode & node) const +{ + art->aClass = stringToClass(node["class"].String()); +} + +void CArtHandler::loadType(CArtifact * art, const JsonNode & node) const +{ +#define ART_BEARER(x) { #x, ArtBearer::x }, + static const std::map artifactBearerMap = { ART_BEARER_LIST }; +#undef ART_BEARER + + for (const JsonNode & b : node["type"].Vector()) + { + auto it = artifactBearerMap.find (b.String()); + if (it != artifactBearerMap.end()) + { + int bearerType = it->second; + switch (bearerType) + { + case ArtBearer::HERO://TODO: allow arts having several possible bearers + break; + case ArtBearer::COMMANDER: + makeItCommanderArt (art); //original artifacts should have only one bearer type + break; + case ArtBearer::CREATURE: + makeItCreatureArt (art); + break; + } + } + else + logMod->warn("Warning! Artifact type %s not recognized!", b.String()); + } +} + +void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) +{ + if (!node["components"].isNull()) + { + for(const auto & component : node["components"].Vector()) + { + VLC->identifiers()->requestIdentifier("artifact", component, [=](si32 id) + { + // when this code is called both combinational art as well as component are loaded + // so it is safe to access any of them + art->constituents.push_back(objects[id]); + objects[id]->partOf.push_back(art); + }); + } + } +} + +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) +{ + auto getAllowedArts = [&](std::vector > &out, std::vector *arts, CArtifact::EartClass flag) + { + if (arts->empty()) //restock available arts + fillList(*arts, flag); + + for (auto & arts_i : *arts) + { + if (accepts(arts_i->id)) + { + CArtifact *art = arts_i; + out.emplace_back(art); + } + } + }; + + auto getAllowed = [&](std::vector > &out) + { + if (flags & CArtifact::ART_TREASURE) + getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); + if (flags & CArtifact::ART_MINOR) + getAllowedArts (out, &minors, CArtifact::ART_MINOR); + if (flags & CArtifact::ART_MAJOR) + getAllowedArts (out, &majors, CArtifact::ART_MAJOR); + if (flags & CArtifact::ART_RELIC) + getAllowedArts (out, &relics, CArtifact::ART_RELIC); + if(out.empty()) //no artifact of specified rarity, we need to take another one + { + getAllowedArts (out, &treasures, CArtifact::ART_TREASURE); + getAllowedArts (out, &minors, CArtifact::ART_MINOR); + getAllowedArts (out, &majors, CArtifact::ART_MAJOR); + getAllowedArts (out, &relics, CArtifact::ART_RELIC); + } + if(out.empty()) //no arts are available at all + { + out.resize (64); + std::fill_n (out.begin(), 64, objects[2]); //Give Grail - this can't be banned (hopefully) + } + }; + + std::vector > out; + getAllowed(out); + ArtifactID artID = (*RandomGeneratorUtil::nextItem(out, rand))->id; + erasePickedArt(artID); + return artID; +} + +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) +{ + return pickRandomArtifact(rand, 0xff, std::move(accepts)); +} + +ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) +{ + return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); +} + +void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) +{ + if (onlyCreature) + { + a->possibleSlots[ArtBearer::HERO].clear(); + a->possibleSlots[ArtBearer::COMMANDER].clear(); + } + a->possibleSlots[ArtBearer::CREATURE].push_back(ArtifactPosition::CREATURE_SLOT); +} + +void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) +{ + if (onlyCommander) + { + a->possibleSlots[ArtBearer::HERO].clear(); + a->possibleSlots[ArtBearer::CREATURE].clear(); + } + for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i) + a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i)); +} + +bool CArtHandler::legalArtifact(const ArtifactID & id) +{ + auto art = objects[id]; + //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components + + if(art->isCombined()) + return false; //no combo artifacts spawning + + if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) + return false; // invalid class + + if(!art->possibleSlots[ArtBearer::HERO].empty()) + return true; + + if(!art->possibleSlots[ArtBearer::CREATURE].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) + return true; + + if(!art->possibleSlots[ArtBearer::COMMANDER].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + return true; + + return false; +} + +void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) +{ + allowedArtifacts.clear(); + treasures.clear(); + minors.clear(); + majors.clear(); + relics.clear(); + + for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) + { + if (allowed[i] && legalArtifact(ArtifactID(i))) + allowedArtifacts.push_back(objects[i]); + //keep im mind that artifact can be worn by more than one type of bearer + } +} + +std::vector CArtHandler::getDefaultAllowed() const +{ + std::vector allowedArtifacts; + allowedArtifacts.resize(127, true); + allowedArtifacts.resize(141, false); + allowedArtifacts.resize(size(), true); + return allowedArtifacts; +} + +void CArtHandler::erasePickedArt(const ArtifactID & id) +{ + CArtifact *art = objects[id]; + + std::vector * artifactList = nullptr; + switch(art->aClass) + { + case CArtifact::ART_TREASURE: + artifactList = &treasures; + break; + case CArtifact::ART_MINOR: + artifactList = &minors; + break; + case CArtifact::ART_MAJOR: + artifactList = &majors; + break; + case CArtifact::ART_RELIC: + artifactList = &relics; + break; + default: + logMod->warn("Problem: cannot find list for artifact %s, strange class. (special?)", art->getNameTranslated()); + return; + } + + if(artifactList->empty()) + fillList(*artifactList, art->aClass); + + auto itr = vstd::find(*artifactList, art); + if(itr != artifactList->end()) + { + artifactList->erase(itr); + } + else + logMod->warn("Problem: cannot erase artifact %s from list, it was not present", art->getNameTranslated()); +} + +void CArtHandler::fillList( std::vector &listToBeFilled, CArtifact::EartClass artifactClass ) +{ + assert(listToBeFilled.empty()); + for (auto & elem : allowedArtifacts) + { + if (elem->aClass == artifactClass) + listToBeFilled.push_back(elem); + } +} + +void CArtHandler::afterLoadFinalization() +{ + //All artifacts have their id, so we can properly update their bonuses' source ids. + for(auto &art : objects) + { + for(auto &bonus : art->getExportedBonusList()) + { + assert(art == objects[art->id]); + assert(bonus->source == BonusSource::ARTIFACT); + bonus->sid = art->id; + } + } + CBonusSystemNode::treeHasChanged(); +} + +CArtifactSet::~CArtifactSet() = default; + +const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const +{ + if(const ArtSlotInfo * si = getSlot(pos)) + { + if(si->artifact && (!excludeLocked || !si->locked)) + return si->artifact; + } + + return nullptr; +} + +CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) +{ + return const_cast((const_cast(this))->getArt(pos, excludeLocked)); +} + +ArtifactPosition CArtifactSet::getArtPos(const ArtifactID & aid, bool onlyWorn, bool allowLocked) const +{ + const auto result = getAllArtPositions(aid, onlyWorn, allowLocked, false); + return result.empty() ? ArtifactPosition{ArtifactPosition::PRE_FIRST} : result[0]; +} + +std::vector CArtifactSet::getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const +{ + std::vector result; + for(const auto & slotInfo : artifactsWorn) + if(slotInfo.second.artifact->getTypeId() == aid && (allowLocked || !slotInfo.second.locked)) + result.push_back(slotInfo.first); + + if(onlyWorn) + return result; + if(!getAll && !result.empty()) + return result; + + auto backpackPositions = getBackpackArtPositions(aid); + result.insert(result.end(), backpackPositions.begin(), backpackPositions.end()); + return result; +} + +std::vector CArtifactSet::getBackpackArtPositions(const ArtifactID & aid) const +{ + std::vector result; + + si32 backpackPosition = ArtifactPosition::BACKPACK_START; + for(const auto & artInfo : artifactsInBackpack) + { + const auto * art = artInfo.getArt(); + if(art && art->artType->getId() == aid) + result.emplace_back(backpackPosition); + backpackPosition++; + } + return result; +} + +ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance *art) const +{ + for(auto i : artifactsWorn) + if(i.second.artifact == art) + return i.first; + + for(int i = 0; i < artifactsInBackpack.size(); i++) + if(artifactsInBackpack[i].artifact == art) + return ArtifactPosition::BACKPACK_START + i; + + return ArtifactPosition::PRE_FIRST; +} + +const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const +{ + for(auto i : artifactsWorn) + if(i.second.artifact->getId() == artInstId) + return i.second.artifact; + + for(auto i : artifactsInBackpack) + if(i.artifact->getId() == artInstId) + return i.artifact; + + return nullptr; +} + +const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance * artInst) const +{ + if(artInst) + { + for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType())) + if(getArt(slot) == artInst) + return slot; + + ArtifactPosition backpackSlot = ArtifactPosition::BACKPACK_START; + for(auto & slotInfo : artifactsInBackpack) + { + if(slotInfo.getArt() == artInst) + return backpackSlot; + backpackSlot = ArtifactPosition(backpackSlot + 1); + } + } + return ArtifactPosition::PRE_FIRST; +} + +bool CArtifactSet::hasArt(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const +{ + return getArtPosCount(aid, onlyWorn, searchBackpackAssemblies, allowLocked) > 0; +} + +bool CArtifactSet::hasArtBackpack(const ArtifactID & aid) const +{ + return !getBackpackArtPositions(aid).empty(); +} + +unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, bool searchBackpackAssemblies, bool allowLocked) const +{ + const auto allPositions = getAllArtPositions(aid, onlyWorn, allowLocked, true); + if(!allPositions.empty()) + return allPositions.size(); + + if(searchBackpackAssemblies && getHiddenArt(aid)) + return 1; + + return 0; +} + +CArtifactSet::ArtPlacementMap CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art) +{ + ArtPlacementMap resArtPlacement; + + setNewArtSlot(slot, art, false); + if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot)) + { + const CArtifactInstance * mainPart = nullptr; + for(const auto & part : art->getPartsInfo()) + if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot) + && (part.slot == ArtifactPosition::PRE_FIRST)) + { + mainPart = part.art; + break; + } + + for(const auto & part : art->getPartsInfo()) + { + if(part.art != mainPart) + { + auto partSlot = part.slot; + if(!part.art->artType->canBePutAt(this, partSlot)) + partSlot = ArtifactUtils::getArtAnyPosition(this, part.art->getTypeId()); + + assert(ArtifactUtils::isSlotEquipment(partSlot)); + setNewArtSlot(partSlot, part.art, true); + resArtPlacement.emplace(std::make_pair(part.art, partSlot)); + } + else + { + resArtPlacement.emplace(std::make_pair(part.art, part.slot)); + } + } + } + return resArtPlacement; +} + +void CArtifactSet::removeArtifact(ArtifactPosition slot) +{ + auto art = getArt(slot, false); + if(art) + { + if(art->isCombined()) + { + for(auto & part : art->getPartsInfo()) + { + if(getArt(part.slot, false)) + eraseArtSlot(part.slot); + } + } + eraseArtSlot(slot); + } +} + +std::pair CArtifactSet::searchForConstituent(const ArtifactID & aid) const +{ + for(const auto & slot : artifactsInBackpack) + { + auto art = slot.artifact; + if(art->isCombined()) + { + for(auto & ci : art->getPartsInfo()) + { + if(ci.art->getTypeId() == aid) + { + return {art, ci.art}; + } + } + } + } + return {nullptr, nullptr}; +} + +const CArtifactInstance * CArtifactSet::getHiddenArt(const ArtifactID & aid) const +{ + return searchForConstituent(aid).second; +} + +const CArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const +{ + return searchForConstituent(aid).first; +} + +const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const +{ + if(pos == ArtifactPosition::TRANSITION_POS) + { + // Always add to the end. Always take from the beginning. + if(artifactsTransitionPos.empty()) + return nullptr; + else + return &(*artifactsTransitionPos.begin()); + } + if(vstd::contains(artifactsWorn, pos)) + return &artifactsWorn.at(pos); + if(pos >= ArtifactPosition::AFTER_LAST ) + { + int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START; + if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) + return nullptr; + else + return &artifactsInBackpack[backpackPos]; + } + + return nullptr; +} + +bool CArtifactSet::isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck) const +{ + if(const ArtSlotInfo *s = getSlot(pos)) + return (onlyLockCheck || !s->artifact) && !s->locked; + + return true; //no slot means not used +} + +void CArtifactSet::setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked) +{ + assert(!vstd::contains(artifactsWorn, slot)); + + ArtSlotInfo * slotInfo; + if(slot == ArtifactPosition::TRANSITION_POS) + { + // Always add to the end. Always take from the beginning. + artifactsTransitionPos.emplace_back(); + slotInfo = &artifactsTransitionPos.back(); + } + else if(ArtifactUtils::isSlotEquipment(slot)) + { + slotInfo = &artifactsWorn[slot]; + } + else + { + auto position = artifactsInBackpack.begin() + slot - ArtifactPosition::BACKPACK_START; + slotInfo = &(*artifactsInBackpack.emplace(position, ArtSlotInfo())); + } + slotInfo->artifact = art; + slotInfo->locked = locked; +} + +void CArtifactSet::eraseArtSlot(const ArtifactPosition & slot) +{ + if(slot == ArtifactPosition::TRANSITION_POS) + { + assert(!artifactsTransitionPos.empty()); + artifactsTransitionPos.erase(artifactsTransitionPos.begin()); + } + else if(ArtifactUtils::isSlotBackpack(slot)) + { + auto backpackSlot = ArtifactPosition(slot - ArtifactPosition::BACKPACK_START); + + assert(artifactsInBackpack.begin() + backpackSlot < artifactsInBackpack.end()); + artifactsInBackpack.erase(artifactsInBackpack.begin() + backpackSlot); + } + else + { + artifactsWorn.erase(slot); + } +} + +void CArtifactSet::artDeserializationFix(CBonusSystemNode *node) +{ + for(auto & elem : artifactsWorn) + if(elem.second.artifact && !elem.second.locked) + node->attachTo(*elem.second.artifact); +} + +void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map) +{ + //todo: creature and commander artifacts + if(handler.saving && artifactsInBackpack.empty() && artifactsWorn.empty()) + return; + + if(!handler.saving) + { + assert(map); + artifactsInBackpack.clear(); + artifactsWorn.clear(); + } + + auto s = handler.enterStruct(fieldName); + + switch(bearerType()) + { + case ArtBearer::HERO: + serializeJsonHero(handler, map); + break; + case ArtBearer::CREATURE: + serializeJsonCreature(handler, map); + break; + case ArtBearer::COMMANDER: + serializeJsonCommander(handler, map); + break; + default: + assert(false); + break; + } +} + +void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) +{ + for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1)) + { + serializeJsonSlot(handler, ap, map); + } + + std::vector backpackTemp; + + if(handler.saving) + { + backpackTemp.reserve(artifactsInBackpack.size()); + for(const ArtSlotInfo & info : artifactsInBackpack) + backpackTemp.push_back(info.artifact->getTypeId()); + } + handler.serializeIdArray(NArtifactPosition::backpack, backpackTemp); + if(!handler.saving) + { + for(const ArtifactID & artifactID : backpackTemp) + { + auto * artifact = ArtifactUtils::createArtifact(map, artifactID); + auto slot = ArtifactPosition::BACKPACK_START + artifactsInBackpack.size(); + if(artifact->artType->canBePutAt(this, slot)) + { + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); + } + } + } +} + +void CArtifactSet::serializeJsonCreature(JsonSerializeFormat & handler, CMap * map) +{ + logGlobal->error("CArtifactSet::serializeJsonCreature not implemented"); +} + +void CArtifactSet::serializeJsonCommander(JsonSerializeFormat & handler, CMap * map) +{ + logGlobal->error("CArtifactSet::serializeJsonCommander not implemented"); +} + +void CArtifactSet::serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map) +{ + ArtifactID artifactID; + + if(handler.saving) + { + const ArtSlotInfo * info = getSlot(slot); + + if(info != nullptr && !info->locked) + { + artifactID = info->artifact->getTypeId(); + handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); + } + } + else + { + handler.serializeId(NArtifactPosition::namesHero[slot.num], artifactID, ArtifactID::NONE); + + if(artifactID != ArtifactID::NONE) + { + auto * artifact = ArtifactUtils::createArtifact(map, artifactID.toEnum()); + + if(artifact->artType->canBePutAt(this, slot)) + { + auto artsMap = putArtifact(slot, artifact); + artifact->addPlacementMap(artsMap); + } + else + { + logGlobal->debug("Artifact can't be put at the specified location."); //TODO add more debugging information + } + } + } +} + +CArtifactFittingSet::CArtifactFittingSet(ArtBearer::ArtBearer Bearer): + Bearer(Bearer) +{ +} + +ArtBearer::ArtBearer CArtifactFittingSet::bearerType() const +{ + return this->Bearer; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 9f592a5f3..56ddf9b4a 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -1,314 +1,314 @@ -/* - * CArtHandler.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 -#include - -#include "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "GameConstants.h" -#include "IHandlerBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CArtHandler; -class CGHeroInstance; -class CArtifactSet; -class CArtifactInstance; -class CRandomGenerator; -class CMap; -class JsonSerializeFormat; - -#define ART_BEARER_LIST \ - ART_BEARER(HERO)\ - ART_BEARER(CREATURE)\ - ART_BEARER(COMMANDER) - -namespace ArtBearer -{ - enum ArtBearer - { -#define ART_BEARER(x) x, - ART_BEARER_LIST -#undef ART_BEARER - }; -} - -class DLL_LINKAGE CCombinedArtifact -{ -protected: - CCombinedArtifact() = default; - - std::vector constituents; // Artifacts IDs a combined artifact consists of, or nullptr. - std::vector partOf; // Reverse map of constituents - combined arts that include this art -public: - bool isCombined() const; - const std::vector & getConstituents() const; - const std::vector & getPartOf() const; - - template void serialize(Handler & h, const int version) - { - h & constituents; - h & partOf; - } -}; - -class DLL_LINKAGE CScrollArtifact -{ -protected: - CScrollArtifact() = default; -public: - bool isScroll() const; -}; - -class DLL_LINKAGE CGrowingArtifact -{ -protected: - CGrowingArtifact() = default; - - std::vector > bonusesPerLevel; // Bonus given each n levels - std::vector > thresholdBonuses; // After certain level they will be added once -public: - bool isGrowing() const; - - std::vector > & getBonusesPerLevel(); - const std::vector > & getBonusesPerLevel() const; - std::vector > & getThresholdBonuses(); - const std::vector > & getThresholdBonuses() const; - - template void serialize(Handler & h, const int version) - { - h & bonusesPerLevel; - h & thresholdBonuses; - } -}; - -// Container for artifacts. Not for instances. -class DLL_LINKAGE CArtifact - : public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact -{ - ArtifactID id; - std::string image; - std::string large; // big image for custom artifacts, used in drag & drop - std::string advMapDef; // used for adventure map object - std::string modScope; - std::string identifier; - int32_t iconIndex; - uint32_t price; - CreatureID warMachine; - // Bearer Type => ids of slots where artifact can be placed - std::map> possibleSlots; - -public: - enum EartClass {ART_SPECIAL=1, ART_TREASURE=2, ART_MINOR=4, ART_MAJOR=8, ART_RELIC=16}; //artifact classes - - EartClass aClass = ART_SPECIAL; - bool onlyOnWaterMap; - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - ArtifactID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; - - std::string getDescriptionTranslated() const override; - std::string getEventTranslated() const override; - std::string getNameTranslated() const override; - - std::string getDescriptionTextID() const override; - std::string getEventTextID() const override; - std::string getNameTextID() const override; - - uint32_t getPrice() const override; - CreatureID getWarMachine() const override; - bool isBig() const override; - bool isTradable() const override; - - int getArtClassSerial() const; //0 - treasure, 1 - minor, 2 - major, 3 - relic, 4 - spell scroll, 5 - other - std::string nodeName() const override; - void addNewBonus(const std::shared_ptr& b) override; - const std::map> & getPossibleSlots() const; - - virtual bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, - bool assumeDestRemoved = false) const; - void updateFrom(const JsonNode & data); - // Is used for testing purposes only - void setImage(int32_t iconIndex, std::string image, std::string large); - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & image; - h & large; - h & advMapDef; - h & iconIndex; - h & price; - h & possibleSlots; - h & aClass; - h & id; - h & modScope; - h & identifier; - h & warMachine; - h & onlyOnWaterMap; - } - - CArtifact(); - ~CArtifact(); - - friend class CArtHandler; -}; - -class DLL_LINKAGE CArtHandler : public CHandlerBase -{ -public: - std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! - - std::vector allowedArtifacts; - std::set growingArtifacts; - - void addBonuses(CArtifact *art, const JsonNode &bonusList); - - void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects - - static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor - - /// Gets a artifact ID randomly and removes the selected artifact from this handler. - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); - - bool legalArtifact(const ArtifactID & id); - void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed - static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); - static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); - - ~CArtHandler(); - - std::vector loadLegacyData() override; - - void loadObject(std::string scope, std::string name, const JsonNode & data) override; - void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void afterLoadFinalization() override; - - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & objects; - h & allowedArtifacts; - h & treasures; - h & minors; - h & majors; - h & relics; - h & growingArtifacts; - } - -protected: - const std::vector & getTypeNames() const override; - CArtifact * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; - -private: - void addSlot(CArtifact * art, const std::string & slotID) const; - void loadSlots(CArtifact * art, const JsonNode & node) const; - void loadClass(CArtifact * art, const JsonNode & node) const; - void loadType(CArtifact * art, const JsonNode & node) const; - void loadComponents(CArtifact * art, const JsonNode & node); - - void erasePickedArt(const ArtifactID & id); -}; - -struct DLL_LINKAGE ArtSlotInfo -{ - ConstTransitivePtr artifact; - ui8 locked; //if locked, then artifact points to the combined artifact - - ArtSlotInfo() : locked(false) {} - const CArtifactInstance * getArt() const; - - template void serialize(Handler & h, const int version) - { - h & artifact; - h & locked; - } -}; - -class DLL_LINKAGE CArtifactSet -{ -public: - using ArtPlacementMap = std::map; - - std::vector artifactsInBackpack; //hero's artifacts from bag - std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 - std::vector artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange - - void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked); - void eraseArtSlot(const ArtifactPosition & slot); - - const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const; - const CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; //nullptr - no artifact - CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true); //nullptr - no artifact - /// Looks for equipped artifact with given ID and returns its slot ID or -1 if none - /// (if more than one such artifact lower ID is returned) - ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; - ArtifactPosition getArtPos(const CArtifactInstance *art) const; - std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; - std::vector getBackpackArtPositions(const ArtifactID & aid) const; - const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; - const ArtifactPosition getSlotByInstance(const CArtifactInstance * artInst) const; - /// Search for constituents of assemblies in backpack which do not have an ArtifactPosition - const CArtifactInstance * getHiddenArt(const ArtifactID & aid) const; - const CArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const; - /// Checks if hero possess artifact of given id (either in backack or worn) - bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const; - bool hasArtBackpack(const ArtifactID & aid) const; - bool isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck = false) const; - unsigned getArtPosCount(const ArtifactID & aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const; - - virtual ArtBearer::ArtBearer bearerType() const = 0; - virtual ArtPlacementMap putArtifact(ArtifactPosition slot, CArtifactInstance * art); - virtual void removeArtifact(ArtifactPosition slot); - virtual ~CArtifactSet(); - - template void serialize(Handler &h, const int version) - { - h & artifactsInBackpack; - h & artifactsWorn; - } - - void artDeserializationFix(CBonusSystemNode *node); - - void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map); -protected: - std::pair searchForConstituent(const ArtifactID & aid) const; - -private: - void serializeJsonHero(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map); - void serializeJsonCommander(JsonSerializeFormat & handler, CMap * map); - - void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map);//normal slots -}; - -// Used to try on artifacts before the claimed changes have been applied -class DLL_LINKAGE CArtifactFittingSet : public CArtifactSet -{ -public: - CArtifactFittingSet(ArtBearer::ArtBearer Bearer); - ArtBearer::ArtBearer bearerType() const override; - -protected: - ArtBearer::ArtBearer Bearer; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CArtHandler.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 +#include + +#include "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "GameConstants.h" +#include "IHandlerBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArtHandler; +class CGHeroInstance; +class CArtifactSet; +class CArtifactInstance; +class CRandomGenerator; +class CMap; +class JsonSerializeFormat; + +#define ART_BEARER_LIST \ + ART_BEARER(HERO)\ + ART_BEARER(CREATURE)\ + ART_BEARER(COMMANDER) + +namespace ArtBearer +{ + enum ArtBearer + { +#define ART_BEARER(x) x, + ART_BEARER_LIST +#undef ART_BEARER + }; +} + +class DLL_LINKAGE CCombinedArtifact +{ +protected: + CCombinedArtifact() = default; + + std::vector constituents; // Artifacts IDs a combined artifact consists of, or nullptr. + std::vector partOf; // Reverse map of constituents - combined arts that include this art +public: + bool isCombined() const; + const std::vector & getConstituents() const; + const std::vector & getPartOf() const; + + template void serialize(Handler & h, const int version) + { + h & constituents; + h & partOf; + } +}; + +class DLL_LINKAGE CScrollArtifact +{ +protected: + CScrollArtifact() = default; +public: + bool isScroll() const; +}; + +class DLL_LINKAGE CGrowingArtifact +{ +protected: + CGrowingArtifact() = default; + + std::vector > bonusesPerLevel; // Bonus given each n levels + std::vector > thresholdBonuses; // After certain level they will be added once +public: + bool isGrowing() const; + + std::vector > & getBonusesPerLevel(); + const std::vector > & getBonusesPerLevel() const; + std::vector > & getThresholdBonuses(); + const std::vector > & getThresholdBonuses() const; + + template void serialize(Handler & h, const int version) + { + h & bonusesPerLevel; + h & thresholdBonuses; + } +}; + +// Container for artifacts. Not for instances. +class DLL_LINKAGE CArtifact + : public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact +{ + ArtifactID id; + std::string image; + std::string large; // big image for custom artifacts, used in drag & drop + std::string advMapDef; // used for adventure map object + std::string modScope; + std::string identifier; + int32_t iconIndex; + uint32_t price; + CreatureID warMachine; + // Bearer Type => ids of slots where artifact can be placed + std::map> possibleSlots; + +public: + enum EartClass {ART_SPECIAL=1, ART_TREASURE=2, ART_MINOR=4, ART_MAJOR=8, ART_RELIC=16}; //artifact classes + + EartClass aClass = ART_SPECIAL; + bool onlyOnWaterMap; + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + ArtifactID getId() const override; + virtual const IBonusBearer * getBonusBearer() const override; + + std::string getDescriptionTranslated() const override; + std::string getEventTranslated() const override; + std::string getNameTranslated() const override; + + std::string getDescriptionTextID() const override; + std::string getEventTextID() const override; + std::string getNameTextID() const override; + + uint32_t getPrice() const override; + CreatureID getWarMachine() const override; + bool isBig() const override; + bool isTradable() const override; + + int getArtClassSerial() const; //0 - treasure, 1 - minor, 2 - major, 3 - relic, 4 - spell scroll, 5 - other + std::string nodeName() const override; + void addNewBonus(const std::shared_ptr& b) override; + const std::map> & getPossibleSlots() const; + + virtual bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, + bool assumeDestRemoved = false) const; + void updateFrom(const JsonNode & data); + // Is used for testing purposes only + void setImage(int32_t iconIndex, std::string image, std::string large); + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & static_cast(*this); + h & static_cast(*this); + h & image; + h & large; + h & advMapDef; + h & iconIndex; + h & price; + h & possibleSlots; + h & aClass; + h & id; + h & modScope; + h & identifier; + h & warMachine; + h & onlyOnWaterMap; + } + + CArtifact(); + ~CArtifact(); + + friend class CArtHandler; +}; + +class DLL_LINKAGE CArtHandler : public CHandlerBase +{ +public: + std::vector treasures, minors, majors, relics; //tmp vectors!!! do not touch if you don't know what you are doing!!! + + std::vector allowedArtifacts; + std::set growingArtifacts; + + void addBonuses(CArtifact *art, const JsonNode &bonusList); + + void fillList(std::vector &listToBeFilled, CArtifact::EartClass artifactClass); //fills given empty list with allowed artifacts of given class. No side effects + + static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor + + /// Gets a artifact ID randomly and removes the selected artifact from this handler. + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); + + bool legalArtifact(const ArtifactID & id); + void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed + static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); + static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); + + ~CArtHandler(); + + std::vector loadLegacyData() override; + + void loadObject(std::string scope, std::string name, const JsonNode & data) override; + void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; + void afterLoadFinalization() override; + + std::vector getDefaultAllowed() const override; + + template void serialize(Handler &h, const int version) + { + h & objects; + h & allowedArtifacts; + h & treasures; + h & minors; + h & majors; + h & relics; + h & growingArtifacts; + } + +protected: + const std::vector & getTypeNames() const override; + CArtifact * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; + +private: + void addSlot(CArtifact * art, const std::string & slotID) const; + void loadSlots(CArtifact * art, const JsonNode & node) const; + void loadClass(CArtifact * art, const JsonNode & node) const; + void loadType(CArtifact * art, const JsonNode & node) const; + void loadComponents(CArtifact * art, const JsonNode & node); + + void erasePickedArt(const ArtifactID & id); +}; + +struct DLL_LINKAGE ArtSlotInfo +{ + ConstTransitivePtr artifact; + ui8 locked; //if locked, then artifact points to the combined artifact + + ArtSlotInfo() : locked(false) {} + const CArtifactInstance * getArt() const; + + template void serialize(Handler & h, const int version) + { + h & artifact; + h & locked; + } +}; + +class DLL_LINKAGE CArtifactSet +{ +public: + using ArtPlacementMap = std::map; + + std::vector artifactsInBackpack; //hero's artifacts from bag + std::map artifactsWorn; //map; positions: 0 - head; 1 - shoulders; 2 - neck; 3 - right hand; 4 - left hand; 5 - torso; 6 - right ring; 7 - left ring; 8 - feet; 9 - misc1; 10 - misc2; 11 - misc3; 12 - misc4; 13 - mach1; 14 - mach2; 15 - mach3; 16 - mach4; 17 - spellbook; 18 - misc5 + std::vector artifactsTransitionPos; // Used as transition position for dragAndDrop artifact exchange + + void setNewArtSlot(const ArtifactPosition & slot, ConstTransitivePtr art, bool locked); + void eraseArtSlot(const ArtifactPosition & slot); + + const ArtSlotInfo * getSlot(const ArtifactPosition & pos) const; + const CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true) const; //nullptr - no artifact + CArtifactInstance * getArt(const ArtifactPosition & pos, bool excludeLocked = true); //nullptr - no artifact + /// Looks for equipped artifact with given ID and returns its slot ID or -1 if none + /// (if more than one such artifact lower ID is returned) + ArtifactPosition getArtPos(const ArtifactID & aid, bool onlyWorn = true, bool allowLocked = true) const; + ArtifactPosition getArtPos(const CArtifactInstance *art) const; + std::vector getAllArtPositions(const ArtifactID & aid, bool onlyWorn, bool allowLocked, bool getAll) const; + std::vector getBackpackArtPositions(const ArtifactID & aid) const; + const CArtifactInstance * getArtByInstanceId(const ArtifactInstanceID & artInstId) const; + const ArtifactPosition getSlotByInstance(const CArtifactInstance * artInst) const; + /// Search for constituents of assemblies in backpack which do not have an ArtifactPosition + const CArtifactInstance * getHiddenArt(const ArtifactID & aid) const; + const CArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const; + /// Checks if hero possess artifact of given id (either in backack or worn) + bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const; + bool hasArtBackpack(const ArtifactID & aid) const; + bool isPositionFree(const ArtifactPosition & pos, bool onlyLockCheck = false) const; + unsigned getArtPosCount(const ArtifactID & aid, bool onlyWorn = true, bool searchBackpackAssemblies = true, bool allowLocked = true) const; + + virtual ArtBearer::ArtBearer bearerType() const = 0; + virtual ArtPlacementMap putArtifact(ArtifactPosition slot, CArtifactInstance * art); + virtual void removeArtifact(ArtifactPosition slot); + virtual ~CArtifactSet(); + + template void serialize(Handler &h, const int version) + { + h & artifactsInBackpack; + h & artifactsWorn; + } + + void artDeserializationFix(CBonusSystemNode *node); + + void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map); +protected: + std::pair searchForConstituent(const ArtifactID & aid) const; + +private: + void serializeJsonHero(JsonSerializeFormat & handler, CMap * map); + void serializeJsonCreature(JsonSerializeFormat & handler, CMap * map); + void serializeJsonCommander(JsonSerializeFormat & handler, CMap * map); + + void serializeJsonSlot(JsonSerializeFormat & handler, const ArtifactPosition & slot, CMap * map);//normal slots +}; + +// Used to try on artifacts before the claimed changes have been applied +class DLL_LINKAGE CArtifactFittingSet : public CArtifactSet +{ +public: + CArtifactFittingSet(ArtBearer::ArtBearer Bearer); + ArtBearer::ArtBearer bearerType() const override; + +protected: + ArtBearer::ArtBearer Bearer; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index f583288a4..8ca7d9936 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -1,249 +1,249 @@ -/* - * CBonusTypeHandler.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" - -#define INSTANTIATE_CBonusTypeHandler_HERE - -#include "CBonusTypeHandler.h" - -#include "JsonNode.h" -#include "filesystem/Filesystem.h" - -#include "GameConstants.h" -#include "CCreatureHandler.h" -#include "CGeneralTextHandler.h" -#include "spells/CSpellHandler.h" - -template class std::vector; - -VCMI_LIB_NAMESPACE_BEGIN - -///CBonusType - -CBonusType::CBonusType(): - hidden(true) -{} - -std::string CBonusType::getNameTextID() const -{ - return TextIdentifier( "core", "bonus", identifier, "name").get(); -} - -std::string CBonusType::getDescriptionTextID() const -{ - return TextIdentifier( "core", "bonus", identifier, "description").get(); -} - -///CBonusTypeHandler - -CBonusTypeHandler::CBonusTypeHandler() -{ - //register predefined bonus types - - #define BONUS_NAME(x) \ - do { \ - bonusTypes.push_back(CBonusType()); \ - } while(0); - - - BONUS_LIST; - #undef BONUS_NAME - - load(); -} - -CBonusTypeHandler::~CBonusTypeHandler() -{ - //dtor -} - -std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const -{ - const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; - if(bt.hidden) - return ""; - - std::string textID = description ? bt.getDescriptionTextID() : bt.getNameTextID(); - std::string text = VLC->generaltexth->translate(textID); - - if (text.find("${val}") != std::string::npos) - boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); - - if (text.find("${subtype.creature}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.creature}", CreatureID(bonus->subtype).toCreature()->getNamePluralTranslated()); - - if (text.find("${subtype.spell}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.spell}", SpellID(bonus->subtype).toSpell()->getNameTranslated()); - - return text; -} - -ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const -{ - std::string fileName; - bool fullPath = false; - - switch(bonus->type) - { - case BonusType::SPELL_IMMUNITY: - { - fullPath = true; - const CSpell * sp = SpellID(bonus->subtype).toSpell(); - fileName = sp->getIconImmune(); - break; - } - case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools - { - if (bonus->subtype == SpellSchool::ANY.getNum()) - fileName = "E_GOLEM.bmp"; - - if (bonus->subtype == SpellSchool::AIR.getNum()) - fileName = "E_LIGHT.bmp"; - - if (bonus->subtype == SpellSchool::FIRE.getNum()) - fileName = "E_FIRE.bmp"; - - if (bonus->subtype == SpellSchool::WATER.getNum()) - fileName = "E_COLD.bmp"; - - if (bonus->subtype == SpellSchool::EARTH.getNum()) - fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage - - break; - } - case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school - { - if (bonus->subtype == SpellSchool::AIR.getNum()) - fileName = "E_SPAIR.bmp"; - - if (bonus->subtype == SpellSchool::FIRE.getNum()) - fileName = "E_SPFIRE.bmp"; - - if (bonus->subtype == SpellSchool::WATER.getNum()) - fileName = "E_SPWATER.bmp"; - - if (bonus->subtype == SpellSchool::EARTH.getNum()) - fileName = "E_SPEATH.bmp"; - - break; - } - case BonusType::NEGATIVE_EFFECTS_IMMUNITY: - { - if (bonus->subtype == SpellSchool::AIR.getNum()) - fileName = "E_SPAIR1.bmp"; - - if (bonus->subtype == SpellSchool::FIRE.getNum()) - fileName = "E_SPFIRE1.bmp"; - - if (bonus->subtype == SpellSchool::WATER.getNum()) - fileName = "E_SPWATER1.bmp"; - - if (bonus->subtype == SpellSchool::EARTH.getNum()) - fileName = "E_SPEATH1.bmp"; - - break; - } - case BonusType::LEVEL_SPELL_IMMUNITY: - { - if(vstd::iswithin(bonus->val, 1, 5)) - { - fileName = "E_SPLVL" + std::to_string(bonus->val) + ".bmp"; - } - break; - } - case BonusType::KING: - { - if(vstd::iswithin(bonus->val, 0, 3)) - { - fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp"; - } - break; - } - case BonusType::GENERAL_DAMAGE_REDUCTION: - { - switch(bonus->subtype) - { - case 0: - fileName = "DamageReductionMelee.bmp"; - break; - case 1: - fileName = "DamageReductionRanged.bmp"; - break; - } - break; - } - - default: - { - const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; - fileName = bt.icon; - fullPath = true; - } - break; - } - - if(!fileName.empty() && !fullPath) - fileName = "zvs/Lib1.res/" + fileName; - return ImagePath::builtinTODO(fileName); -} - -void CBonusTypeHandler::load() -{ - const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); - const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); - load(config); -} - -void CBonusTypeHandler::load(const JsonNode & config) -{ - for(const auto & node : config.Struct()) - { - auto it = bonusNameMap.find(node.first); - - if(it == bonusNameMap.end()) - { - //TODO: new bonus -// CBonusType bt; -// loadItem(node.second, bt); -// -// auto new_id = bonusTypes.size(); -// -// bonusTypes.push_back(bt); - - logBonus->warn("Unrecognized bonus name! (%s)", node.first); - } - else - { - CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)]; - - loadItem(node.second, bt, node.first); - logBonus->trace("Loaded bonus type %s", node.first); - } - } -} - -void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const -{ - dest.identifier = name; - dest.hidden = source["hidden"].Bool(); //Null -> false - - if (!dest.hidden) - { - VLC->generaltexth->registerString( "core", dest.getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString( "core", dest.getDescriptionTextID(), source["description"].String()); - } - - const JsonNode & graphics = source["graphics"]; - - if(!graphics.isNull()) - dest.icon = graphics["icon"].String(); -} - -VCMI_LIB_NAMESPACE_END +/* + * CBonusTypeHandler.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" + +#define INSTANTIATE_CBonusTypeHandler_HERE + +#include "CBonusTypeHandler.h" + +#include "JsonNode.h" +#include "filesystem/Filesystem.h" + +#include "GameConstants.h" +#include "CCreatureHandler.h" +#include "CGeneralTextHandler.h" +#include "spells/CSpellHandler.h" + +template class std::vector; + +VCMI_LIB_NAMESPACE_BEGIN + +///CBonusType + +CBonusType::CBonusType(): + hidden(true) +{} + +std::string CBonusType::getNameTextID() const +{ + return TextIdentifier( "core", "bonus", identifier, "name").get(); +} + +std::string CBonusType::getDescriptionTextID() const +{ + return TextIdentifier( "core", "bonus", identifier, "description").get(); +} + +///CBonusTypeHandler + +CBonusTypeHandler::CBonusTypeHandler() +{ + //register predefined bonus types + + #define BONUS_NAME(x) \ + do { \ + bonusTypes.push_back(CBonusType()); \ + } while(0); + + + BONUS_LIST; + #undef BONUS_NAME + + load(); +} + +CBonusTypeHandler::~CBonusTypeHandler() +{ + //dtor +} + +std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const +{ + const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; + if(bt.hidden) + return ""; + + std::string textID = description ? bt.getDescriptionTextID() : bt.getNameTextID(); + std::string text = VLC->generaltexth->translate(textID); + + if (text.find("${val}") != std::string::npos) + boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); + + if (text.find("${subtype.creature}") != std::string::npos) + boost::algorithm::replace_all(text, "${subtype.creature}", CreatureID(bonus->subtype).toCreature()->getNamePluralTranslated()); + + if (text.find("${subtype.spell}") != std::string::npos) + boost::algorithm::replace_all(text, "${subtype.spell}", SpellID(bonus->subtype).toSpell()->getNameTranslated()); + + return text; +} + +ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonus) const +{ + std::string fileName; + bool fullPath = false; + + switch(bonus->type) + { + case BonusType::SPELL_IMMUNITY: + { + fullPath = true; + const CSpell * sp = SpellID(bonus->subtype).toSpell(); + fileName = sp->getIconImmune(); + break; + } + case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools + { + if (bonus->subtype == SpellSchool::ANY.getNum()) + fileName = "E_GOLEM.bmp"; + + if (bonus->subtype == SpellSchool::AIR.getNum()) + fileName = "E_LIGHT.bmp"; + + if (bonus->subtype == SpellSchool::FIRE.getNum()) + fileName = "E_FIRE.bmp"; + + if (bonus->subtype == SpellSchool::WATER.getNum()) + fileName = "E_COLD.bmp"; + + if (bonus->subtype == SpellSchool::EARTH.getNum()) + fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage + + break; + } + case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school + { + if (bonus->subtype == SpellSchool::AIR.getNum()) + fileName = "E_SPAIR.bmp"; + + if (bonus->subtype == SpellSchool::FIRE.getNum()) + fileName = "E_SPFIRE.bmp"; + + if (bonus->subtype == SpellSchool::WATER.getNum()) + fileName = "E_SPWATER.bmp"; + + if (bonus->subtype == SpellSchool::EARTH.getNum()) + fileName = "E_SPEATH.bmp"; + + break; + } + case BonusType::NEGATIVE_EFFECTS_IMMUNITY: + { + if (bonus->subtype == SpellSchool::AIR.getNum()) + fileName = "E_SPAIR1.bmp"; + + if (bonus->subtype == SpellSchool::FIRE.getNum()) + fileName = "E_SPFIRE1.bmp"; + + if (bonus->subtype == SpellSchool::WATER.getNum()) + fileName = "E_SPWATER1.bmp"; + + if (bonus->subtype == SpellSchool::EARTH.getNum()) + fileName = "E_SPEATH1.bmp"; + + break; + } + case BonusType::LEVEL_SPELL_IMMUNITY: + { + if(vstd::iswithin(bonus->val, 1, 5)) + { + fileName = "E_SPLVL" + std::to_string(bonus->val) + ".bmp"; + } + break; + } + case BonusType::KING: + { + if(vstd::iswithin(bonus->val, 0, 3)) + { + fileName = "E_KING" + std::to_string(std::max(1, bonus->val)) + ".bmp"; + } + break; + } + case BonusType::GENERAL_DAMAGE_REDUCTION: + { + switch(bonus->subtype) + { + case 0: + fileName = "DamageReductionMelee.bmp"; + break; + case 1: + fileName = "DamageReductionRanged.bmp"; + break; + } + break; + } + + default: + { + const CBonusType & bt = bonusTypes[vstd::to_underlying(bonus->type)]; + fileName = bt.icon; + fullPath = true; + } + break; + } + + if(!fileName.empty() && !fullPath) + fileName = "zvs/Lib1.res/" + fileName; + return ImagePath::builtinTODO(fileName); +} + +void CBonusTypeHandler::load() +{ + const JsonNode gameConf(JsonPath::builtin("config/gameConfig.json")); + const JsonNode config(JsonUtils::assembleFromFiles(gameConf["bonuses"].convertTo>())); + load(config); +} + +void CBonusTypeHandler::load(const JsonNode & config) +{ + for(const auto & node : config.Struct()) + { + auto it = bonusNameMap.find(node.first); + + if(it == bonusNameMap.end()) + { + //TODO: new bonus +// CBonusType bt; +// loadItem(node.second, bt); +// +// auto new_id = bonusTypes.size(); +// +// bonusTypes.push_back(bt); + + logBonus->warn("Unrecognized bonus name! (%s)", node.first); + } + else + { + CBonusType & bt = bonusTypes[vstd::to_underlying(it->second)]; + + loadItem(node.second, bt, node.first); + logBonus->trace("Loaded bonus type %s", node.first); + } + } +} + +void CBonusTypeHandler::loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const +{ + dest.identifier = name; + dest.hidden = source["hidden"].Bool(); //Null -> false + + if (!dest.hidden) + { + VLC->generaltexth->registerString( "core", dest.getNameTextID(), source["name"].String()); + VLC->generaltexth->registerString( "core", dest.getDescriptionTextID(), source["description"].String()); + } + + const JsonNode & graphics = source["graphics"]; + + if(!graphics.isNull()) + dest.icon = graphics["icon"].String(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBonusTypeHandler.h b/lib/CBonusTypeHandler.h index 01d36809f..2ddecfb30 100644 --- a/lib/CBonusTypeHandler.h +++ b/lib/CBonusTypeHandler.h @@ -1,75 +1,75 @@ -/* - * CBonusTypeHandler.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 "IBonusTypeHandler.h" -#include "IHandlerBase.h" -#include "bonuses/Bonus.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -class JsonNode; - -class DLL_LINKAGE CBonusType -{ -public: - CBonusType(); - - std::string getNameTextID() const; - std::string getDescriptionTextID() const; - - template void serialize(Handler & h, const int version) - { - h & icon; - h & identifier; - h & hidden; - - } - -private: - friend class CBonusTypeHandler; - - std::string icon; - std::string identifier; - - bool hidden; -}; - -class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler -{ -public: - CBonusTypeHandler(); - virtual ~CBonusTypeHandler(); - - std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; - ImagePath bonusToGraphics(const std::shared_ptr & bonus) const override; - - template void serialize(Handler & h, const int version) - { - //for now always use up to date configuration - //once modded bonus type will be implemented, serialize only them - std::vector ignore; - h & ignore; - } -private: - void load(); - void load(const JsonNode & config); - void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const; - - std::vector bonusTypes; //index = BonusType -}; - -VCMI_LIB_NAMESPACE_END - -#ifndef INSTANTIATE_CBonusTypeHandler_HERE -extern template class std::vector; -#endif +/* + * CBonusTypeHandler.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 "IBonusTypeHandler.h" +#include "IHandlerBase.h" +#include "bonuses/Bonus.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +class JsonNode; + +class DLL_LINKAGE CBonusType +{ +public: + CBonusType(); + + std::string getNameTextID() const; + std::string getDescriptionTextID() const; + + template void serialize(Handler & h, const int version) + { + h & icon; + h & identifier; + h & hidden; + + } + +private: + friend class CBonusTypeHandler; + + std::string icon; + std::string identifier; + + bool hidden; +}; + +class DLL_LINKAGE CBonusTypeHandler : public IBonusTypeHandler +{ +public: + CBonusTypeHandler(); + virtual ~CBonusTypeHandler(); + + std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const override; + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const override; + + template void serialize(Handler & h, const int version) + { + //for now always use up to date configuration + //once modded bonus type will be implemented, serialize only them + std::vector ignore; + h & ignore; + } +private: + void load(); + void load(const JsonNode & config); + void loadItem(const JsonNode & source, CBonusType & dest, const std::string & name) const; + + std::vector bonusTypes; //index = BonusType +}; + +VCMI_LIB_NAMESPACE_END + +#ifndef INSTANTIATE_CBonusTypeHandler_HERE +extern template class std::vector; +#endif diff --git a/lib/CBuildingHandler.cpp b/lib/CBuildingHandler.cpp index a4673e5ae..6db07ac15 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/CBuildingHandler.cpp @@ -1,85 +1,85 @@ -/* - * CBuildingHandler.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 "CBuildingHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set & builtBuildings) -{ - static const std::vector campToERMU = - { - BuildingID::TOWN_HALL, BuildingID::CITY_HALL, - BuildingID::CAPITOL, BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::TAVERN, - BuildingID::BLACKSMITH, BuildingID::MARKETPLACE, BuildingID::RESOURCE_SILO, BuildingID::NONE, - BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, - BuildingID::MAGES_GUILD_5, - BuildingID::SHIPYARD, BuildingID::GRAIL, - BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4 - }; //creature generators with banks - handled separately - - if (camp < campToERMU.size()) - { - return campToERMU[camp]; - } - - static const std::vector hordeLvlsPerTType[GameConstants::F_NUMBER] = - { - {2}, {1}, {1,4}, {0,2}, {0}, {0}, {0}, {0}, {0} - }; - - int curPos = static_cast(campToERMU.size()); - for (int i=0; i 1) - { - BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]); - - if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built - return BuildingID::HORDE_2_UPGR; - else //upgraded dwelling not presents - return BuildingID::HORDE_2; - } - } - } - } - curPos++; - } - } - assert(0); - return BuildingID::NONE; //not found -} - - -VCMI_LIB_NAMESPACE_END +/* + * CBuildingHandler.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 "CBuildingHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set & builtBuildings) +{ + static const std::vector campToERMU = + { + BuildingID::TOWN_HALL, BuildingID::CITY_HALL, + BuildingID::CAPITOL, BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::TAVERN, + BuildingID::BLACKSMITH, BuildingID::MARKETPLACE, BuildingID::RESOURCE_SILO, BuildingID::NONE, + BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_4, + BuildingID::MAGES_GUILD_5, + BuildingID::SHIPYARD, BuildingID::GRAIL, + BuildingID::SPECIAL_1, BuildingID::SPECIAL_2, BuildingID::SPECIAL_3, BuildingID::SPECIAL_4 + }; //creature generators with banks - handled separately + + if (camp < campToERMU.size()) + { + return campToERMU[camp]; + } + + static const std::vector hordeLvlsPerTType[GameConstants::F_NUMBER] = + { + {2}, {1}, {1,4}, {0,2}, {0}, {0}, {0}, {0}, {0} + }; + + int curPos = static_cast(campToERMU.size()); + for (int i=0; i 1) + { + BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]); + + if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built + return BuildingID::HORDE_2_UPGR; + else //upgraded dwelling not presents + return BuildingID::HORDE_2; + } + } + } + } + curPos++; + } + } + assert(0); + return BuildingID::NONE; //not found +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CBuildingHandler.h b/lib/CBuildingHandler.h index 04d5e9400..a0aab5615 100644 --- a/lib/CBuildingHandler.h +++ b/lib/CBuildingHandler.h @@ -1,22 +1,22 @@ -/* - * CBuildingHandler.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 "GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CBuildingHandler -{ -public: - static BuildingID campToERMU(int camp, int townType, const std::set & builtBuildings); -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBuildingHandler.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 "GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CBuildingHandler +{ +public: + static BuildingID campToERMU(int camp, int townType, const std::set & builtBuildings); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index c741e0e5e..025bac146 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -1,195 +1,195 @@ -/* - * CConfigHandler.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 "CConfigHandler.h" - -#include "../lib/filesystem/Filesystem.h" -#include "../lib/GameConstants.h" -#include "../lib/VCMIDirs.h" - -VCMI_LIB_NAMESPACE_BEGIN - -SettingsStorage settings; -SettingsStorage persistentStorage; - -template -SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): - parent(_parent), - path(std::move(_path)) -{ -} - -template -SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator[](const std::string & nextNode) const -{ - std::vector newPath = path; - newPath.push_back(nextNode); - return NodeAccessor(parent, newPath); -} - -template -SettingsStorage::NodeAccessor::operator Accessor() const -{ - return Accessor(parent, path); -} - -template -SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator () (std::vector _path) const -{ - std::vector newPath = path; - newPath.insert( newPath.end(), _path.begin(), _path.end()); - return NodeAccessor(parent, newPath); -} - -SettingsStorage::SettingsStorage(): - write(NodeAccessor(*this, std::vector() )), - listen(NodeAccessor(*this, std::vector() )) -{ -} - -void SettingsStorage::init(const std::string & dataFilename, const std::string & schema) -{ - this->dataFilename = dataFilename; - this->schema = schema; - - JsonPath confName = JsonPath::builtin(dataFilename); - - JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); - - // Probably new install. Create config file to save settings to - if (!CResourceHandler::get("local")->existsResource(confName)) - { - CResourceHandler::get("local")->createResource(dataFilename); - if(schema.empty()) - invalidateNode(std::vector()); - } - - if(!schema.empty()) - { - JsonUtils::maximize(config, schema); - JsonUtils::validate(config, schema, "settings"); - } -} - -void SettingsStorage::invalidateNode(const std::vector &changedPath) -{ - for(SettingsListener * listener : listeners) - listener->nodeInvalidated(changedPath); - - JsonNode savedConf = config; - savedConf.Struct().erase("session"); - if(!schema.empty()) - JsonUtils::minimize(savedConf, schema); - - std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << savedConf.toJson(); -} - -JsonNode & SettingsStorage::getNode(const std::vector & path) -{ - JsonNode *node = &config; - for(const std::string & value : path) - node = &(*node)[value]; - - return *node; -} - -Settings SettingsStorage::get(const std::vector & path) -{ - return Settings(*this, path); -} - -const JsonNode & SettingsStorage::operator[](const std::string & value) const -{ - return config[value]; -} - -const JsonNode & SettingsStorage::toJsonNode() const -{ - return config; -} - -SettingsListener::SettingsListener(SettingsStorage & _parent, std::vector _path): - parent(_parent), - path(std::move(_path)) -{ - parent.listeners.insert(this); -} - -SettingsListener::SettingsListener(const SettingsListener &sl): - parent(sl.parent), - path(sl.path), - callback(sl.callback) -{ - parent.listeners.insert(this); -} - -SettingsListener::~SettingsListener() -{ - parent.listeners.erase(this); -} - -void SettingsListener::nodeInvalidated(const std::vector &changedPath) -{ - if (!callback) - return; - - size_t min = std::min(path.size(), changedPath.size()); - size_t mismatch = std::mismatch(path.begin(), path.begin()+min, changedPath.begin()).first - path.begin(); - - if (min == mismatch) - callback(parent.getNode(path)); -} - -void SettingsListener::operator() (std::function _callback) -{ - callback = std::move(_callback); -} - -Settings::Settings(SettingsStorage &_parent, const std::vector &_path): - parent(_parent), - path(_path), - node(_parent.getNode(_path)), - copy(_parent.getNode(_path)) -{ -} - -Settings::~Settings() -{ - if (node != copy) - parent.invalidateNode(path); -} - -JsonNode* Settings::operator -> () -{ - return &node; -} - -const JsonNode* Settings::operator ->() const -{ - return &node; -} - -const JsonNode & Settings::operator[](const std::string & value) const -{ - return node[value]; -} - -JsonNode & Settings::operator[](const std::string & value) -{ - return node[value]; -} - -// Force instantiation of the SettingsStorage::NodeAccessor class template. -// That way method definitions can sit in the cpp file -template struct SettingsStorage::NodeAccessor; -template struct SettingsStorage::NodeAccessor; - -VCMI_LIB_NAMESPACE_END +/* + * CConfigHandler.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 "CConfigHandler.h" + +#include "../lib/filesystem/Filesystem.h" +#include "../lib/GameConstants.h" +#include "../lib/VCMIDirs.h" + +VCMI_LIB_NAMESPACE_BEGIN + +SettingsStorage settings; +SettingsStorage persistentStorage; + +template +SettingsStorage::NodeAccessor::NodeAccessor(SettingsStorage & _parent, std::vector _path): + parent(_parent), + path(std::move(_path)) +{ +} + +template +SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator[](const std::string & nextNode) const +{ + std::vector newPath = path; + newPath.push_back(nextNode); + return NodeAccessor(parent, newPath); +} + +template +SettingsStorage::NodeAccessor::operator Accessor() const +{ + return Accessor(parent, path); +} + +template +SettingsStorage::NodeAccessor SettingsStorage::NodeAccessor::operator () (std::vector _path) const +{ + std::vector newPath = path; + newPath.insert( newPath.end(), _path.begin(), _path.end()); + return NodeAccessor(parent, newPath); +} + +SettingsStorage::SettingsStorage(): + write(NodeAccessor(*this, std::vector() )), + listen(NodeAccessor(*this, std::vector() )) +{ +} + +void SettingsStorage::init(const std::string & dataFilename, const std::string & schema) +{ + this->dataFilename = dataFilename; + this->schema = schema; + + JsonPath confName = JsonPath::builtin(dataFilename); + + JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); + + // Probably new install. Create config file to save settings to + if (!CResourceHandler::get("local")->existsResource(confName)) + { + CResourceHandler::get("local")->createResource(dataFilename); + if(schema.empty()) + invalidateNode(std::vector()); + } + + if(!schema.empty()) + { + JsonUtils::maximize(config, schema); + JsonUtils::validate(config, schema, "settings"); + } +} + +void SettingsStorage::invalidateNode(const std::vector &changedPath) +{ + for(SettingsListener * listener : listeners) + listener->nodeInvalidated(changedPath); + + JsonNode savedConf = config; + savedConf.Struct().erase("session"); + if(!schema.empty()) + JsonUtils::minimize(savedConf, schema); + + std::fstream file(CResourceHandler::get()->getResourceName(JsonPath::builtin(dataFilename))->c_str(), std::ofstream::out | std::ofstream::trunc); + file << savedConf.toJson(); +} + +JsonNode & SettingsStorage::getNode(const std::vector & path) +{ + JsonNode *node = &config; + for(const std::string & value : path) + node = &(*node)[value]; + + return *node; +} + +Settings SettingsStorage::get(const std::vector & path) +{ + return Settings(*this, path); +} + +const JsonNode & SettingsStorage::operator[](const std::string & value) const +{ + return config[value]; +} + +const JsonNode & SettingsStorage::toJsonNode() const +{ + return config; +} + +SettingsListener::SettingsListener(SettingsStorage & _parent, std::vector _path): + parent(_parent), + path(std::move(_path)) +{ + parent.listeners.insert(this); +} + +SettingsListener::SettingsListener(const SettingsListener &sl): + parent(sl.parent), + path(sl.path), + callback(sl.callback) +{ + parent.listeners.insert(this); +} + +SettingsListener::~SettingsListener() +{ + parent.listeners.erase(this); +} + +void SettingsListener::nodeInvalidated(const std::vector &changedPath) +{ + if (!callback) + return; + + size_t min = std::min(path.size(), changedPath.size()); + size_t mismatch = std::mismatch(path.begin(), path.begin()+min, changedPath.begin()).first - path.begin(); + + if (min == mismatch) + callback(parent.getNode(path)); +} + +void SettingsListener::operator() (std::function _callback) +{ + callback = std::move(_callback); +} + +Settings::Settings(SettingsStorage &_parent, const std::vector &_path): + parent(_parent), + path(_path), + node(_parent.getNode(_path)), + copy(_parent.getNode(_path)) +{ +} + +Settings::~Settings() +{ + if (node != copy) + parent.invalidateNode(path); +} + +JsonNode* Settings::operator -> () +{ + return &node; +} + +const JsonNode* Settings::operator ->() const +{ + return &node; +} + +const JsonNode & Settings::operator[](const std::string & value) const +{ + return node[value]; +} + +JsonNode & Settings::operator[](const std::string & value) +{ + return node[value]; +} + +// Force instantiation of the SettingsStorage::NodeAccessor class template. +// That way method definitions can sit in the cpp file +template struct SettingsStorage::NodeAccessor; +template struct SettingsStorage::NodeAccessor; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConfigHandler.h b/lib/CConfigHandler.h index 6eeae392b..a446d8e5c 100644 --- a/lib/CConfigHandler.h +++ b/lib/CConfigHandler.h @@ -1,121 +1,121 @@ -/* - * CConfigHandler.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/JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Settings; -class SettingsListener; - -/// Main storage of game settings -class DLL_LINKAGE SettingsStorage -{ - //Helper struct to access specific node either via chain of operator[] or with one operator() (vector) - template - struct DLL_LINKAGE NodeAccessor - { - SettingsStorage & parent; - std::vector path; - - NodeAccessor(SettingsStorage & _parent, std::vector _path); - NodeAccessor operator[](const std::string & nextNode) const; - NodeAccessor operator () (std::vector _path) const; - operator Accessor() const; - }; - - std::set listeners; - JsonNode config; - - std::string dataFilename; - std::string schema; - - JsonNode & getNode(const std::vector & path); - - // Calls all required listeners - void invalidateNode(const std::vector &changedPath); - - Settings get(const std::vector & path); - -public: - // Initialize config structure - SettingsStorage(); - void init(const std::string & dataFilename, const std::string & schema); - - // Get write access to config node at path - const NodeAccessor write; - - // Get access to listener at path - const NodeAccessor listen; - - //Read access, see JsonNode::operator[] - const JsonNode & operator[](const std::string & value) const; - const JsonNode & toJsonNode() const; - - friend class SettingsListener; - friend class Settings; -}; - -/// Class for listening changes in specific part of configuration (e.g. change of music volume) -class DLL_LINKAGE SettingsListener -{ - SettingsStorage &parent; - // Path to this node - std::vector path; - // Callback - std::function callback; - - SettingsListener(SettingsStorage & _parent, std::vector _path); - - // Executes callback if changedpath begins with path - void nodeInvalidated(const std::vector & changedPath); - -public: - SettingsListener(const SettingsListener &sl); - ~SettingsListener(); - - // assign callback function - void operator()(std::function _callback); - - friend class SettingsStorage; -}; - -/// System options, provides write access to config tree with auto-saving on change -class DLL_LINKAGE Settings -{ - SettingsStorage &parent; - //path to this node - std::vector path; - JsonNode &node; - JsonNode copy; - - //Get access to node pointed by path - Settings(SettingsStorage &_parent, const std::vector &_path); - -public: - //Saves config if it was modified - ~Settings(); - - //Returns node selected during construction - JsonNode* operator ->(); - const JsonNode* operator ->() const; - - //Helper, replaces JsonNode::operator[] - JsonNode & operator[](const std::string & value); - const JsonNode & operator[](const std::string & value) const; - - friend class SettingsStorage; -}; - -extern DLL_LINKAGE SettingsStorage settings; -extern DLL_LINKAGE SettingsStorage persistentStorage; - -VCMI_LIB_NAMESPACE_END +/* + * CConfigHandler.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/JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Settings; +class SettingsListener; + +/// Main storage of game settings +class DLL_LINKAGE SettingsStorage +{ + //Helper struct to access specific node either via chain of operator[] or with one operator() (vector) + template + struct DLL_LINKAGE NodeAccessor + { + SettingsStorage & parent; + std::vector path; + + NodeAccessor(SettingsStorage & _parent, std::vector _path); + NodeAccessor operator[](const std::string & nextNode) const; + NodeAccessor operator () (std::vector _path) const; + operator Accessor() const; + }; + + std::set listeners; + JsonNode config; + + std::string dataFilename; + std::string schema; + + JsonNode & getNode(const std::vector & path); + + // Calls all required listeners + void invalidateNode(const std::vector &changedPath); + + Settings get(const std::vector & path); + +public: + // Initialize config structure + SettingsStorage(); + void init(const std::string & dataFilename, const std::string & schema); + + // Get write access to config node at path + const NodeAccessor write; + + // Get access to listener at path + const NodeAccessor listen; + + //Read access, see JsonNode::operator[] + const JsonNode & operator[](const std::string & value) const; + const JsonNode & toJsonNode() const; + + friend class SettingsListener; + friend class Settings; +}; + +/// Class for listening changes in specific part of configuration (e.g. change of music volume) +class DLL_LINKAGE SettingsListener +{ + SettingsStorage &parent; + // Path to this node + std::vector path; + // Callback + std::function callback; + + SettingsListener(SettingsStorage & _parent, std::vector _path); + + // Executes callback if changedpath begins with path + void nodeInvalidated(const std::vector & changedPath); + +public: + SettingsListener(const SettingsListener &sl); + ~SettingsListener(); + + // assign callback function + void operator()(std::function _callback); + + friend class SettingsStorage; +}; + +/// System options, provides write access to config tree with auto-saving on change +class DLL_LINKAGE Settings +{ + SettingsStorage &parent; + //path to this node + std::vector path; + JsonNode &node; + JsonNode copy; + + //Get access to node pointed by path + Settings(SettingsStorage &_parent, const std::vector &_path); + +public: + //Saves config if it was modified + ~Settings(); + + //Returns node selected during construction + JsonNode* operator ->(); + const JsonNode* operator ->() const; + + //Helper, replaces JsonNode::operator[] + JsonNode & operator[](const std::string & value); + const JsonNode & operator[](const std::string & value) const; + + friend class SettingsStorage; +}; + +extern DLL_LINKAGE SettingsStorage settings; +extern DLL_LINKAGE SettingsStorage persistentStorage; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 930e6b2ae..00dca3da3 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -1,296 +1,296 @@ -/* - * CConsoleHandler.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 "CConsoleHandler.h" -#include "CConfigHandler.h" - -#include "CThreadHelper.h" - -VCMI_LIB_NAMESPACE_BEGIN - -std::mutex CConsoleHandler::smx; - -DLL_LINKAGE CConsoleHandler * console = nullptr; - -VCMI_LIB_NAMESPACE_END - -#ifndef VCMI_WINDOWS - using TColor = std::string; - #define CONSOLE_GREEN "\x1b[1;32m" - #define CONSOLE_RED "\x1b[1;31m" - #define CONSOLE_MAGENTA "\x1b[1;35m" - #define CONSOLE_YELLOW "\x1b[1;33m" - #define CONSOLE_WHITE "\x1b[1;37m" - #define CONSOLE_GRAY "\x1b[1;30m" - #define CONSOLE_TEAL "\x1b[1;36m" -#else - #include - #include -#ifndef __MINGW32__ - #pragma comment(lib, "dbghelp.lib") -#endif - typedef WORD TColor; - HANDLE handleIn; - HANDLE handleOut; - HANDLE handleErr; - #define CONSOLE_GREEN FOREGROUND_GREEN | FOREGROUND_INTENSITY - #define CONSOLE_RED FOREGROUND_RED | FOREGROUND_INTENSITY - #define CONSOLE_MAGENTA FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY - #define CONSOLE_YELLOW FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY - #define CONSOLE_WHITE FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY - #define CONSOLE_GRAY FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE - #define CONSOLE_TEAL FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY - - static TColor defErrColor; -#endif - -static TColor defColor; - -VCMI_LIB_NAMESPACE_BEGIN - -#ifdef VCMI_WINDOWS - -void printWinError() -{ - //Get error code - int error = GetLastError(); - if(!error) - { - logGlobal->error("No Win error information set."); - return; - } - logGlobal->error("Error %d encountered:", error); - - //Get error description - char* pTemp = nullptr; - FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, - nullptr, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&pTemp, 1, nullptr); - logGlobal->error(pTemp); - LocalFree( pTemp ); -} - -const char* exceptionName(DWORD exc) -{ -#define EXC_CASE(EXC) case EXCEPTION_##EXC : return "EXCEPTION_" #EXC - switch (exc) - { - EXC_CASE(ACCESS_VIOLATION); - EXC_CASE(DATATYPE_MISALIGNMENT); - EXC_CASE(BREAKPOINT); - EXC_CASE(SINGLE_STEP); - EXC_CASE(ARRAY_BOUNDS_EXCEEDED); - EXC_CASE(FLT_DENORMAL_OPERAND); - EXC_CASE(FLT_DIVIDE_BY_ZERO); - EXC_CASE(FLT_INEXACT_RESULT); - EXC_CASE(FLT_INVALID_OPERATION); - EXC_CASE(FLT_OVERFLOW); - EXC_CASE(FLT_STACK_CHECK); - EXC_CASE(FLT_UNDERFLOW); - EXC_CASE(INT_DIVIDE_BY_ZERO); - EXC_CASE(INT_OVERFLOW); - EXC_CASE(PRIV_INSTRUCTION); - EXC_CASE(IN_PAGE_ERROR); - EXC_CASE(ILLEGAL_INSTRUCTION); - EXC_CASE(NONCONTINUABLE_EXCEPTION); - EXC_CASE(STACK_OVERFLOW); - EXC_CASE(INVALID_DISPOSITION); - EXC_CASE(GUARD_PAGE); - EXC_CASE(INVALID_HANDLE); - default: - return "UNKNOWN EXCEPTION"; - } -#undef EXC_CASE -} - - - -LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) -{ - logGlobal->error("Disaster happened."); - - PEXCEPTION_RECORD einfo = exception->ExceptionRecord; - logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); - - if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) - { - logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); - } - const DWORD threadId = ::GetCurrentThreadId(); - logGlobal->error("Thread ID: %d", threadId); - - //exception info to be placed in the dump - MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; - - //create file where dump will be placed - char *mname = nullptr; - char buffer[MAX_PATH + 1]; - HMODULE hModule = nullptr; - GetModuleFileNameA(hModule, buffer, MAX_PATH); - mname = strrchr(buffer, '\\'); - if (mname != 0) - mname++; - else - mname = buffer; - - strcat(mname, "_crashinfo.dmp"); - HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); - logGlobal->error("Crash info will be put in %s", mname); - - // flush loggers - std::string padding(1000, '@'); - - logGlobal->error(padding); - logAi->error(padding); - logNetwork->error(padding); - - auto dumpType = MiniDumpWithDataSegs; - - if(settings["general"]["extraDump"].Bool()) - { - dumpType = (MINIDUMP_TYPE)( - MiniDumpWithFullMemory - | MiniDumpWithFullMemoryInfo - | MiniDumpWithHandleData - | MiniDumpWithUnloadedModules - | MiniDumpWithThreadInfo); - } - - MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, &meinfo, 0, 0); - MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); - return EXCEPTION_EXECUTE_HANDLER; -} -#endif - - -void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) -{ - TColor colorCode; - switch(color) - { - case EConsoleTextColor::DEFAULT: - colorCode = defColor; - break; - case EConsoleTextColor::GREEN: - colorCode = CONSOLE_GREEN; - break; - case EConsoleTextColor::RED: - colorCode = CONSOLE_RED; - break; - case EConsoleTextColor::MAGENTA: - colorCode = CONSOLE_MAGENTA; - break; - case EConsoleTextColor::YELLOW: - colorCode = CONSOLE_YELLOW; - break; - case EConsoleTextColor::WHITE: - colorCode = CONSOLE_WHITE; - break; - case EConsoleTextColor::GRAY: - colorCode = CONSOLE_GRAY; - break; - case EConsoleTextColor::TEAL: - colorCode = CONSOLE_TEAL; - break; - default: - colorCode = defColor; - break; - } -#ifdef VCMI_WINDOWS - SetConsoleTextAttribute(handleOut, colorCode); - if (color == EConsoleTextColor::DEFAULT) - colorCode = defErrColor; - SetConsoleTextAttribute(handleErr, colorCode); -#else - std::cout << colorCode; -#endif -} - -int CConsoleHandler::run() const -{ - setThreadName("consoleHandler"); - //disabling sync to make in_avail() work (othervice always returns 0) - { - TLockGuard _(smx); - std::ios::sync_with_stdio(false); - } - std::string buffer; - - while ( std::cin.good() ) - { -#ifndef VCMI_WINDOWS - //check if we have some unreaded symbols - if (std::cin.rdbuf()->in_avail()) - { - if ( getline(std::cin, buffer).good() ) - if ( cb && *cb ) - (*cb)(buffer, false); - } - else - boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); - - boost::this_thread::interruption_point(); -#else - std::getline(std::cin, buffer); - if ( cb && *cb ) - (*cb)(buffer, false); -#endif - } - return -1; -} -CConsoleHandler::CConsoleHandler(): - cb(new std::function), - thread(nullptr) -{ -#ifdef VCMI_WINDOWS - handleIn = GetStdHandle(STD_INPUT_HANDLE); - handleOut = GetStdHandle(STD_OUTPUT_HANDLE); - handleErr = GetStdHandle(STD_ERROR_HANDLE); - - CONSOLE_SCREEN_BUFFER_INFO csbi; - GetConsoleScreenBufferInfo(handleOut,&csbi); - defColor = csbi.wAttributes; - - GetConsoleScreenBufferInfo(handleErr, &csbi); - defErrColor = csbi.wAttributes; -#ifndef _DEBUG - SetUnhandledExceptionFilter(onUnhandledException); -#endif -#else - defColor = "\x1b[0m"; -#endif -} -CConsoleHandler::~CConsoleHandler() -{ - logGlobal->info("Killing console..."); - end(); - delete cb; - logGlobal->info("Killing console... done!"); -} -void CConsoleHandler::end() -{ - if (thread) - { -#ifndef VCMI_WINDOWS - thread->interrupt(); -#else - TerminateThread(thread->native_handle(),0); -#endif - thread->join(); - delete thread; - thread = nullptr; - } -} - -void CConsoleHandler::start() -{ - thread = new boost::thread(std::bind(&CConsoleHandler::run,console)); -} - -VCMI_LIB_NAMESPACE_END +/* + * CConsoleHandler.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 "CConsoleHandler.h" +#include "CConfigHandler.h" + +#include "CThreadHelper.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::mutex CConsoleHandler::smx; + +DLL_LINKAGE CConsoleHandler * console = nullptr; + +VCMI_LIB_NAMESPACE_END + +#ifndef VCMI_WINDOWS + using TColor = std::string; + #define CONSOLE_GREEN "\x1b[1;32m" + #define CONSOLE_RED "\x1b[1;31m" + #define CONSOLE_MAGENTA "\x1b[1;35m" + #define CONSOLE_YELLOW "\x1b[1;33m" + #define CONSOLE_WHITE "\x1b[1;37m" + #define CONSOLE_GRAY "\x1b[1;30m" + #define CONSOLE_TEAL "\x1b[1;36m" +#else + #include + #include +#ifndef __MINGW32__ + #pragma comment(lib, "dbghelp.lib") +#endif + typedef WORD TColor; + HANDLE handleIn; + HANDLE handleOut; + HANDLE handleErr; + #define CONSOLE_GREEN FOREGROUND_GREEN | FOREGROUND_INTENSITY + #define CONSOLE_RED FOREGROUND_RED | FOREGROUND_INTENSITY + #define CONSOLE_MAGENTA FOREGROUND_RED | FOREGROUND_BLUE | FOREGROUND_INTENSITY + #define CONSOLE_YELLOW FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY + #define CONSOLE_WHITE FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY + #define CONSOLE_GRAY FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE + #define CONSOLE_TEAL FOREGROUND_GREEN | FOREGROUND_BLUE | FOREGROUND_INTENSITY + + static TColor defErrColor; +#endif + +static TColor defColor; + +VCMI_LIB_NAMESPACE_BEGIN + +#ifdef VCMI_WINDOWS + +void printWinError() +{ + //Get error code + int error = GetLastError(); + if(!error) + { + logGlobal->error("No Win error information set."); + return; + } + logGlobal->error("Error %d encountered:", error); + + //Get error description + char* pTemp = nullptr; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_IGNORE_INSERTS | FORMAT_MESSAGE_FROM_SYSTEM, + nullptr, error, MAKELANGID( LANG_NEUTRAL, SUBLANG_DEFAULT ), (LPSTR)&pTemp, 1, nullptr); + logGlobal->error(pTemp); + LocalFree( pTemp ); +} + +const char* exceptionName(DWORD exc) +{ +#define EXC_CASE(EXC) case EXCEPTION_##EXC : return "EXCEPTION_" #EXC + switch (exc) + { + EXC_CASE(ACCESS_VIOLATION); + EXC_CASE(DATATYPE_MISALIGNMENT); + EXC_CASE(BREAKPOINT); + EXC_CASE(SINGLE_STEP); + EXC_CASE(ARRAY_BOUNDS_EXCEEDED); + EXC_CASE(FLT_DENORMAL_OPERAND); + EXC_CASE(FLT_DIVIDE_BY_ZERO); + EXC_CASE(FLT_INEXACT_RESULT); + EXC_CASE(FLT_INVALID_OPERATION); + EXC_CASE(FLT_OVERFLOW); + EXC_CASE(FLT_STACK_CHECK); + EXC_CASE(FLT_UNDERFLOW); + EXC_CASE(INT_DIVIDE_BY_ZERO); + EXC_CASE(INT_OVERFLOW); + EXC_CASE(PRIV_INSTRUCTION); + EXC_CASE(IN_PAGE_ERROR); + EXC_CASE(ILLEGAL_INSTRUCTION); + EXC_CASE(NONCONTINUABLE_EXCEPTION); + EXC_CASE(STACK_OVERFLOW); + EXC_CASE(INVALID_DISPOSITION); + EXC_CASE(GUARD_PAGE); + EXC_CASE(INVALID_HANDLE); + default: + return "UNKNOWN EXCEPTION"; + } +#undef EXC_CASE +} + + + +LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) +{ + logGlobal->error("Disaster happened."); + + PEXCEPTION_RECORD einfo = exception->ExceptionRecord; + logGlobal->error("Reason: 0x%x - %s at %04x:%x", einfo->ExceptionCode, exceptionName(einfo->ExceptionCode), exception->ContextRecord->SegCs, (void*)einfo->ExceptionAddress); + + if (einfo->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) + { + logGlobal->error("Attempt to %s 0x%8x", (einfo->ExceptionInformation[0] == 1 ? "write to" : "read from"), (void*)einfo->ExceptionInformation[1]); + } + const DWORD threadId = ::GetCurrentThreadId(); + logGlobal->error("Thread ID: %d", threadId); + + //exception info to be placed in the dump + MINIDUMP_EXCEPTION_INFORMATION meinfo = {threadId, exception, TRUE}; + + //create file where dump will be placed + char *mname = nullptr; + char buffer[MAX_PATH + 1]; + HMODULE hModule = nullptr; + GetModuleFileNameA(hModule, buffer, MAX_PATH); + mname = strrchr(buffer, '\\'); + if (mname != 0) + mname++; + else + mname = buffer; + + strcat(mname, "_crashinfo.dmp"); + HANDLE dfile = CreateFileA(mname, GENERIC_READ|GENERIC_WRITE, FILE_SHARE_WRITE|FILE_SHARE_READ, 0, CREATE_ALWAYS, 0, 0); + logGlobal->error("Crash info will be put in %s", mname); + + // flush loggers + std::string padding(1000, '@'); + + logGlobal->error(padding); + logAi->error(padding); + logNetwork->error(padding); + + auto dumpType = MiniDumpWithDataSegs; + + if(settings["general"]["extraDump"].Bool()) + { + dumpType = (MINIDUMP_TYPE)( + MiniDumpWithFullMemory + | MiniDumpWithFullMemoryInfo + | MiniDumpWithHandleData + | MiniDumpWithUnloadedModules + | MiniDumpWithThreadInfo); + } + + MiniDumpWriteDump(GetCurrentProcess(), GetCurrentProcessId(), dfile, dumpType, &meinfo, 0, 0); + MessageBoxA(0, "VCMI has crashed. We are sorry. File with information about encountered problem has been created.", "VCMI Crashhandler", MB_OK | MB_ICONERROR); + return EXCEPTION_EXECUTE_HANDLER; +} +#endif + + +void CConsoleHandler::setColor(EConsoleTextColor::EConsoleTextColor color) +{ + TColor colorCode; + switch(color) + { + case EConsoleTextColor::DEFAULT: + colorCode = defColor; + break; + case EConsoleTextColor::GREEN: + colorCode = CONSOLE_GREEN; + break; + case EConsoleTextColor::RED: + colorCode = CONSOLE_RED; + break; + case EConsoleTextColor::MAGENTA: + colorCode = CONSOLE_MAGENTA; + break; + case EConsoleTextColor::YELLOW: + colorCode = CONSOLE_YELLOW; + break; + case EConsoleTextColor::WHITE: + colorCode = CONSOLE_WHITE; + break; + case EConsoleTextColor::GRAY: + colorCode = CONSOLE_GRAY; + break; + case EConsoleTextColor::TEAL: + colorCode = CONSOLE_TEAL; + break; + default: + colorCode = defColor; + break; + } +#ifdef VCMI_WINDOWS + SetConsoleTextAttribute(handleOut, colorCode); + if (color == EConsoleTextColor::DEFAULT) + colorCode = defErrColor; + SetConsoleTextAttribute(handleErr, colorCode); +#else + std::cout << colorCode; +#endif +} + +int CConsoleHandler::run() const +{ + setThreadName("consoleHandler"); + //disabling sync to make in_avail() work (othervice always returns 0) + { + TLockGuard _(smx); + std::ios::sync_with_stdio(false); + } + std::string buffer; + + while ( std::cin.good() ) + { +#ifndef VCMI_WINDOWS + //check if we have some unreaded symbols + if (std::cin.rdbuf()->in_avail()) + { + if ( getline(std::cin, buffer).good() ) + if ( cb && *cb ) + (*cb)(buffer, false); + } + else + boost::this_thread::sleep_for(boost::chrono::milliseconds(100)); + + boost::this_thread::interruption_point(); +#else + std::getline(std::cin, buffer); + if ( cb && *cb ) + (*cb)(buffer, false); +#endif + } + return -1; +} +CConsoleHandler::CConsoleHandler(): + cb(new std::function), + thread(nullptr) +{ +#ifdef VCMI_WINDOWS + handleIn = GetStdHandle(STD_INPUT_HANDLE); + handleOut = GetStdHandle(STD_OUTPUT_HANDLE); + handleErr = GetStdHandle(STD_ERROR_HANDLE); + + CONSOLE_SCREEN_BUFFER_INFO csbi; + GetConsoleScreenBufferInfo(handleOut,&csbi); + defColor = csbi.wAttributes; + + GetConsoleScreenBufferInfo(handleErr, &csbi); + defErrColor = csbi.wAttributes; +#ifndef _DEBUG + SetUnhandledExceptionFilter(onUnhandledException); +#endif +#else + defColor = "\x1b[0m"; +#endif +} +CConsoleHandler::~CConsoleHandler() +{ + logGlobal->info("Killing console..."); + end(); + delete cb; + logGlobal->info("Killing console... done!"); +} +void CConsoleHandler::end() +{ + if (thread) + { +#ifndef VCMI_WINDOWS + thread->interrupt(); +#else + TerminateThread(thread->native_handle(),0); +#endif + thread->join(); + delete thread; + thread = nullptr; + } +} + +void CConsoleHandler::start() +{ + thread = new boost::thread(std::bind(&CConsoleHandler::run,console)); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CConsoleHandler.h b/lib/CConsoleHandler.h index 28ee0ad0b..aef086329 100644 --- a/lib/CConsoleHandler.h +++ b/lib/CConsoleHandler.h @@ -1,98 +1,98 @@ -/* - * CConsoleHandler.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 - -VCMI_LIB_NAMESPACE_BEGIN - -namespace EConsoleTextColor -{ -/** The color enum is used for colored text console output. */ -enum EConsoleTextColor -{ - DEFAULT = -1, - GREEN, - RED, - MAGENTA, - YELLOW, - WHITE, - GRAY, - TEAL = -2 -}; -} - -/// Class which wraps the native console. It can print text based on -/// the chosen color -class DLL_LINKAGE CConsoleHandler -{ -public: - CConsoleHandler(); - ~CConsoleHandler(); - void start(); //starts listening thread - - template void print(const T &data, bool addNewLine = false, EConsoleTextColor::EConsoleTextColor color = EConsoleTextColor::DEFAULT, bool printToStdErr = false) - { - TLockGuard _(smx); -#ifndef VCMI_WINDOWS - // with love from ffmpeg - library is trying to print some warnings from separate thread - // this results in broken console on Linux. Lock stdout to print all our data at once - flockfile(stdout); -#endif - if(color != EConsoleTextColor::DEFAULT) setColor(color); - if(printToStdErr) - { - std::cerr << data; - if(addNewLine) - { - std::cerr << std::endl; - } - else - { - std::cerr << std::flush; - } - } - else - { - std::cout << data; - if(addNewLine) - { - std::cout << std::endl; - } - else - { - std::cout << std::flush; - } - } - - if(color != EConsoleTextColor::DEFAULT) setColor(EConsoleTextColor::DEFAULT); -#ifndef VCMI_WINDOWS - funlockfile(stdout); -#endif - } - //function to be called when message is received - string: message, bool: whether call was made from in-game console - std::function *cb; - -private: - int run() const; - - void end(); //kills listening thread - - static void setColor(EConsoleTextColor::EConsoleTextColor color); //sets color of text appropriate for given logging level - - /// FIXME: Implement CConsoleHandler as singleton, move some logic into CLogConsoleTarget, etc... needs to be disussed:) - /// Without static, application will crash complaining about mutex deleted. In short: CConsoleHandler gets deleted before - /// the logging system. - static std::mutex smx; - - boost::thread * thread; -}; - -extern DLL_LINKAGE CConsoleHandler * console; - -VCMI_LIB_NAMESPACE_END +/* + * CConsoleHandler.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 + +VCMI_LIB_NAMESPACE_BEGIN + +namespace EConsoleTextColor +{ +/** The color enum is used for colored text console output. */ +enum EConsoleTextColor +{ + DEFAULT = -1, + GREEN, + RED, + MAGENTA, + YELLOW, + WHITE, + GRAY, + TEAL = -2 +}; +} + +/// Class which wraps the native console. It can print text based on +/// the chosen color +class DLL_LINKAGE CConsoleHandler +{ +public: + CConsoleHandler(); + ~CConsoleHandler(); + void start(); //starts listening thread + + template void print(const T &data, bool addNewLine = false, EConsoleTextColor::EConsoleTextColor color = EConsoleTextColor::DEFAULT, bool printToStdErr = false) + { + TLockGuard _(smx); +#ifndef VCMI_WINDOWS + // with love from ffmpeg - library is trying to print some warnings from separate thread + // this results in broken console on Linux. Lock stdout to print all our data at once + flockfile(stdout); +#endif + if(color != EConsoleTextColor::DEFAULT) setColor(color); + if(printToStdErr) + { + std::cerr << data; + if(addNewLine) + { + std::cerr << std::endl; + } + else + { + std::cerr << std::flush; + } + } + else + { + std::cout << data; + if(addNewLine) + { + std::cout << std::endl; + } + else + { + std::cout << std::flush; + } + } + + if(color != EConsoleTextColor::DEFAULT) setColor(EConsoleTextColor::DEFAULT); +#ifndef VCMI_WINDOWS + funlockfile(stdout); +#endif + } + //function to be called when message is received - string: message, bool: whether call was made from in-game console + std::function *cb; + +private: + int run() const; + + void end(); //kills listening thread + + static void setColor(EConsoleTextColor::EConsoleTextColor color); //sets color of text appropriate for given logging level + + /// FIXME: Implement CConsoleHandler as singleton, move some logic into CLogConsoleTarget, etc... needs to be disussed:) + /// Without static, application will crash complaining about mutex deleted. In short: CConsoleHandler gets deleted before + /// the logging system. + static std::mutex smx; + + boost::thread * thread; +}; + +extern DLL_LINKAGE CConsoleHandler * console; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 91061b70d..79c7c50ea 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -1,1386 +1,1386 @@ -/* - * CCreatureHandler.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 "CCreatureHandler.h" - -#include "CGeneralTextHandler.h" -#include "ResourceSet.h" -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" -#include "CTownHandler.h" -#include "GameSettings.h" -#include "constants/StringConstants.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "serializer/JsonDeserializer.h" -#include "serializer/JsonUpdater.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/CModHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const std::map CCreature::creatureQuantityRanges = -{ - {CCreature::CreatureQuantityId::FEW, "1-4"}, - {CCreature::CreatureQuantityId::SEVERAL, "5-9"}, - {CCreature::CreatureQuantityId::PACK, "10-19"}, - {CCreature::CreatureQuantityId::LOTS, "20-49"}, - {CCreature::CreatureQuantityId::HORDE, "50-99"}, - {CCreature::CreatureQuantityId::THRONG, "100-249"}, - {CCreature::CreatureQuantityId::SWARM, "250-499"}, - {CCreature::CreatureQuantityId::ZOUNDS, "500-999"}, - {CCreature::CreatureQuantityId::LEGION, "1000+"} -}; - -int32_t CCreature::getIndex() const -{ - return idNumber.toEnum(); -} - -int32_t CCreature::getIconIndex() const -{ - return iconIndex; -} - -std::string CCreature::getJsonKey() const -{ - return modScope + ':' + identifier; -} - -void CCreature::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "CPRSMALL", smallIconName); - cb(getIconIndex(), 0, "TWCRPORT", largeIconName); -} - -CreatureID CCreature::getId() const -{ - return idNumber; -} - -const IBonusBearer * CCreature::getBonusBearer() const -{ - return this; -} - -int32_t CCreature::getAdvMapAmountMin() const -{ - return ammMin; -} - -int32_t CCreature::getAdvMapAmountMax() const -{ - return ammMax; -} - -int32_t CCreature::getAIValue() const -{ - return AIValue; -} - -int32_t CCreature::getFightValue() const -{ - return fightValue; -} - -int32_t CCreature::getLevel() const -{ - return level; -} - -int32_t CCreature::getGrowth() const -{ - return growth; -} - -int32_t CCreature::getHorde() const -{ - return hordeGrowth; -} - -FactionID CCreature::getFaction() const -{ - return FactionID(faction); -} - -int32_t CCreature::getBaseAttack() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDefense() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDamageMin() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseDamageMax() const -{ - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseHitPoints() const -{ - static const auto SELECTOR = Selector::type()(BonusType::STACK_HEALTH).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseSpellPoints() const -{ - static const auto SELECTOR = Selector::type()(BonusType::CASTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseSpeed() const -{ - static const auto SELECTOR = Selector::type()(BonusType::STACKS_SPEED).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getBaseShots() const -{ - static const auto SELECTOR = Selector::type()(BonusType::SHOTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); - return getExportedBonusList().valOfBonuses(SELECTOR); -} - -int32_t CCreature::getRecruitCost(GameResID resIndex) const -{ - if(resIndex.getNum() >= 0 && resIndex.getNum() < cost.size()) - return cost[resIndex]; - else - return 0; -} - -TResources CCreature::getFullRecruitCost() const -{ - return cost; -} - -bool CCreature::hasUpgrades() const -{ - return !upgrades.empty(); -} - -std::string CCreature::getNameTranslated() const -{ - return getNameSingularTranslated(); -} - -std::string CCreature::getNamePluralTranslated() const -{ - return VLC->generaltexth->translate(getNamePluralTextID()); -} - -std::string CCreature::getNameSingularTranslated() const -{ - return VLC->generaltexth->translate(getNameSingularTextID()); -} - -std::string CCreature::getNameTextID() const -{ - return getNameSingularTextID(); -} - -std::string CCreature::getNamePluralTextID() const -{ - return TextIdentifier("creatures", modScope, identifier, "name", "plural" ).get(); -} - -std::string CCreature::getNameSingularTextID() const -{ - return TextIdentifier("creatures", modScope, identifier, "name", "singular" ).get(); -} - -CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity) -{ - if (quantity<5) - return CCreature::CreatureQuantityId::FEW; - if (quantity<10) - return CCreature::CreatureQuantityId::SEVERAL; - if (quantity<20) - return CCreature::CreatureQuantityId::PACK; - if (quantity<50) - return CCreature::CreatureQuantityId::LOTS; - if (quantity<100) - return CCreature::CreatureQuantityId::HORDE; - if (quantity<250) - return CCreature::CreatureQuantityId::THRONG; - if (quantity<500) - return CCreature::CreatureQuantityId::SWARM; - if (quantity<1000) - return CCreature::CreatureQuantityId::ZOUNDS; - - return CCreature::CreatureQuantityId::LEGION; -} - -std::string CCreature::getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId) -{ - if(creatureQuantityRanges.find(quantityId) != creatureQuantityRanges.end()) - return creatureQuantityRanges.at(quantityId); - - logGlobal->error("Wrong quantityId: %d", (int)quantityId); - assert(0); - return "[ERROR]"; -} - -int CCreature::estimateCreatureCount(ui32 countID) -{ - static const int creature_count[] = { 0, 3, 8, 15, 35, 75, 175, 375, 750, 2500 }; - - if(countID > 9) - { - logGlobal->error("Wrong countID %d!", countID); - return 0; - } - else - return creature_count[countID]; -} - -bool CCreature::isDoubleWide() const -{ - return doubleWide; -} - -/** - * Determines if the creature is of a good alignment. - * @return true if the creture is good, false otherwise. - */ -bool CCreature::isGood () const -{ - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD; -} - -/** - * Determines if the creature is of an evil alignment. - * @return true if the creature is evil, false otherwise. - */ -bool CCreature::isEvil () const -{ - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL; -} - -si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought -{ - int ret = 2147483645; - int resAmnt = static_cast(std::min(res.size(),cost.size())); - for(int i=0;i(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, getIndex(), subtype, BonusValueType::BASE_NUMBER); - addNewBonus(added); - } - else - { - std::shared_ptr b = existing[0]; - b->val = val; - } -} - -bool CCreature::isMyUpgrade(const CCreature *anotherCre) const -{ - //TODO upgrade of upgrade? - return vstd::contains(upgrades, anotherCre->getId()); -} - -bool CCreature::valid() const -{ - return this == VLC->creh->objects[idNumber]; -} - -std::string CCreature::nodeName() const -{ - return "\"" + getNamePluralTextID() + "\""; -} - -void CCreature::updateFrom(const JsonNode & data) -{ - JsonUpdater handler(nullptr, data); - - { - auto configScope = handler.enterStruct("config"); - - const JsonNode & configNode = handler.getCurrent(); - - serializeJson(handler); - - if(!configNode["hitPoints"].isNull()) - addBonus(configNode["hitPoints"].Integer(), BonusType::STACK_HEALTH); - - if(!configNode["speed"].isNull()) - addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); - - if(!configNode["attack"].isNull()) - addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); - - if(!configNode["defense"].isNull()) - addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); - - if(!configNode["damage"]["min"].isNull()) - addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); - - if(!configNode["damage"]["max"].isNull()) - addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); - - if(!configNode["shots"].isNull()) - addBonus(configNode["shots"].Integer(), BonusType::SHOTS); - - if(!configNode["spellPoints"].isNull()) - addBonus(configNode["spellPoints"].Integer(), BonusType::CASTS); - } - - - handler.serializeBonuses("bonuses", this); -} - -void CCreature::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("fightValue", fightValue); - handler.serializeInt("aiValue", AIValue); - handler.serializeInt("growth", growth); - handler.serializeInt("horde", hordeGrowth);// Needed at least until configurable buildings - - { - auto advMapNode = handler.enterStruct("advMapAmount"); - handler.serializeInt("min", ammMin); - handler.serializeInt("max", ammMax); - } - - if(handler.updating) - { - cost.serializeJson(handler, "cost"); - handler.serializeInt("faction", faction);//TODO: unify with deferred resolve - } - - handler.serializeInt("level", level); - handler.serializeBool("doubleWide", doubleWide); - - if(!handler.saving) - { - if(ammMin > ammMax) - logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); - } -} - -CCreatureHandler::CCreatureHandler() - : expAfterUpgrade(0) -{ - VLC->creh = this; - loadCommanders(); -} - -const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const -{ - std::optional index = VLC->identifiers()->getIdentifier(scope, "creature", identifier); - - if(!index) - throw std::runtime_error("Creature not found "+identifier); - - return objects[*index]; -} - -void CCreatureHandler::loadCommanders() -{ - auto configResource = JsonPath::builtin("config/commanders.json"); - - std::string modSource = VLC->modh->findResourceOrigin(configResource); - JsonNode data(configResource); - data.setMeta(modSource); - - const JsonNode & config = data; // switch to const data accessors - - for (auto bonus : config["bonusPerLevel"].Vector()) - { - commanderLevelPremy.push_back(JsonUtils::parseBonus(bonus.Vector())); - } - - int i = 0; - for (auto skill : config["skillLevels"].Vector()) - { - skillLevels.emplace_back(); - for (auto skillLevel : skill["levels"].Vector()) - { - skillLevels[i].push_back(static_cast(skillLevel.Float())); - } - ++i; - } - - for (auto ability : config["abilityRequirements"].Vector()) - { - std::pair , std::pair > a; - a.first = JsonUtils::parseBonus (ability["ability"].Vector()); - a.second.first = static_cast(ability["skills"].Vector()[0].Float()); - a.second.second = static_cast(ability["skills"].Vector()[1].Float()); - skillRequirements.push_back (a); - } -} - -void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses) const -{ - auto makeBonusNode = [&](const std::string & type, double val = 0) -> JsonNode - { - JsonNode ret; - ret["type"].String() = type; - ret["val"].Float() = val; - return ret; - }; - - static const std::map abilityMap = - { - {"FLYING_ARMY", makeBonusNode("FLYING")}, - {"SHOOTING_ARMY", makeBonusNode("SHOOTER")}, - {"SIEGE_WEAPON", makeBonusNode("SIEGE_WEAPON")}, - {"const_free_attack", makeBonusNode("BLOCKS_RETALIATION")}, - {"IS_UNDEAD", makeBonusNode("UNDEAD")}, - {"const_no_melee_penalty", makeBonusNode("NO_MELEE_PENALTY")}, - {"const_jousting", makeBonusNode("JOUSTING", 5)}, - {"KING_1", makeBonusNode("KING")}, // Slayer with no expertise - {"KING_2", makeBonusNode("KING", 2)}, // Advanced Slayer or better - {"KING_3", makeBonusNode("KING", 3)}, // Expert Slayer only - {"const_no_wall_penalty", makeBonusNode("NO_WALL_PENALTY")}, - {"MULTI_HEADED", makeBonusNode("ATTACKS_ALL_ADJACENT")}, - {"IMMUNE_TO_MIND_SPELLS", makeBonusNode("MIND_IMMUNITY")}, - {"HAS_EXTENDED_ATTACK", makeBonusNode("TWO_HEX_ATTACK_BREATH")} - }; - - auto hasAbility = [&](const std::string & name) -> bool - { - return boost::algorithm::find_first(bonuses, name); - }; - - for(const auto & a : abilityMap) - { - if(hasAbility(a.first)) - creature["abilities"][a.first] = a.second; - } - if(hasAbility("DOUBLE_WIDE")) - creature["doubleWide"].Bool() = true; - - if(hasAbility("const_raises_morale")) - { - JsonNode node = makeBonusNode("MORALE"); - node["val"].Float() = 1; - node["propagator"].String() = "HERO"; - creature["abilities"]["const_raises_morale"] = node; - } -} - -std::vector CCreatureHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser parser(TextPath::builtin("DATA/CRTRAITS.TXT")); - - parser.endLine(); // header - - // this file is a bit different in some of Russian localisations: - //ENG: Singular Plural Wood ... - //RUS: Singular Plural Plural2 Wood ... - // Try to detect which version this is by header - // TODO: use 3rd name? Stand for "whose", e.g. pikemans' - size_t namesCount = 2; - { - if ( parser.readString() != "Singular" || parser.readString() != "Plural" ) - throw std::runtime_error("Incorrect format of CrTraits.txt"); - - if (parser.readString() == "Plural2") - namesCount = 3; - - parser.endLine(); - } - - for (size_t i=0; iidNumber = CreatureID(index); - cre->iconIndex = cre->getIndex() + 2; - cre->identifier = identifier; - cre->modScope = scope; - - JsonDeserializer handler(nullptr, node); - cre->serializeJson(handler); - - cre->cost = ResourceSet(node["cost"]); - - VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String()); - VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String()); - - cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); - cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); - cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); - cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); - - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); - - assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); - - if(!node["shots"].isNull()) - cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); - - loadStackExperience(cre, node["stackExperience"]); - loadJsonAnimation(cre, node["graphics"]); - loadCreatureJson(cre, node); - - for(const auto & extraName : node["extraNames"].Vector()) - { - for(const auto & type_name : getTypeNames()) - registerObject(scope, type_name, extraName.String(), cre->getIndex()); - } - - JsonNode advMapFile = node["graphics"]["map"]; - JsonNode advMapMask = node["graphics"]["mapMask"]; - - VLC->identifiers()->requestIdentifier(scope, "object", "monster", [=](si32 index) - { - JsonNode conf; - conf.setMeta(scope); - - VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); - if (!advMapFile.isNull()) - { - JsonNode templ; - templ["animation"] = advMapFile; - if (!advMapMask.isNull()) - templ["mask"] = advMapMask; - templ.setMeta(scope); - - // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead - VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); - VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ); - } - - // object does not have any templates - this is not usable object (e.g. pseudo-creature like Arrow Tower) - if (VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num); - }); - - return cre; -} - -const std::vector & CCreatureHandler::getTypeNames() const -{ - static const std::vector typeNames = { "creature" }; - return typeNames; -} - -std::vector CCreatureHandler::getDefaultAllowed() const -{ - std::vector ret; - - ret.reserve(objects.size()); - for(const CCreature * crea : objects) - { - ret.push_back(crea ? !crea->special : false); - } - return ret; -} - -void CCreatureHandler::loadCrExpMod() -{ - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values - { - //Calculate rank exp values, formula appears complicated bu no parsing needed - expRanks.resize(8); - int dif = 0; - int it = 8000; //ignore name of this variable - expRanks[0].push_back(it); - for (int j = 1; j < 10; ++j) //used for tiers 8-10, and all other probably - { - expRanks[0].push_back(expRanks[0][j-1] + it + dif); - dif += it/5; - } - for (int i = 1; i < 8; ++i) //used for tiers 1-7 - { - dif = 0; - it = 1000 * i; - expRanks[i].push_back(it); - for (int j = 1; j < 10; ++j) - { - expRanks[i].push_back(expRanks[i][j-1] + it + dif); - dif += it/5; - } - } - - CLegacyConfigParser expBonParser(TextPath::builtin("DATA/CREXPMOD.TXT")); - - expBonParser.endLine(); //header - - maxExpPerBattle.resize(8); - for (int i = 1; i < 8; ++i) - { - expBonParser.readString(); //index - expBonParser.readString(); //float multiplier -> hardcoded - expBonParser.readString(); //ignore upgrade mod? ->hardcoded - expBonParser.readString(); //already calculated - - maxExpPerBattle[i] = static_cast(expBonParser.readNumber()); - expRanks[i].push_back(expRanks[i].back() + static_cast(expBonParser.readNumber())); - - expBonParser.endLine(); - } - //exp for tier >7, rank 11 - expRanks[0].push_back(147000); - expAfterUpgrade = 75; //percent - maxExpPerBattle[0] = maxExpPerBattle[7]; - } -} - - -void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) -{ - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses - { - logGlobal->debug("\tLoading stack experience bonuses"); - auto addBonusForAllCreatures = [&](std::shared_ptr b) { - auto limiter = std::make_shared(); - b->addLimiter(limiter); - globalEffects.addNewBonus(b); - }; - auto addBonusForTier = [&](int tier, std::shared_ptr b) { - assert(vstd::iswithin(tier, 1, 7)); - //bonuses from level 7 are given to high-level creatures too - auto max = tier == GameConstants::CREATURES_PER_TOWN ? std::numeric_limits::max() : tier + 1; - auto limiter = std::make_shared(tier, max); - b->addLimiter(limiter); - globalEffects.addNewBonus(b); - }; - - CLegacyConfigParser parser(TextPath::builtin("DATA/CREXPBON.TXT")); - - Bonus b; //prototype with some default properties - b.source = BonusSource::STACK_EXPERIENCE; - b.duration = BonusDuration::PERMANENT; - b.valType = BonusValueType::ADDITIVE_VALUE; - b.effectRange = BonusLimitEffect::NO_LIMIT; - b.additionalInfo = 0; - b.turnsRemain = 0; - BonusList bl; - - parser.endLine(); - - parser.readString(); //ignore index - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForAllCreatures(b); //health bonus is common for all - parser.endLine(); - - for (int i = 1; i < 7; ++i) - { - for (int j = 0; j < 4; ++j) //four modifiers common for tiers - { - parser.readString(); //ignore index - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForTier(i, b); - parser.endLine(); - } - } - for (int j = 0; j < 4; ++j) //tier 7 - { - parser.readString(); //ignore index - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - addBonusForTier(7, b); - parser.endLine(); - } - do //parse everything that's left - { - auto sid = static_cast(parser.readNumber()); //id = this particular creature ID - - b.sid = sid; - bl.clear(); - loadStackExp(b, bl, parser); - for(const auto & b : bl) - { - objects[sid]->addNewBonus(b); //add directly to CCreature Node - } - } - while (parser.endLine()); - - }//end of Stack Experience -} - -void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const -{ - CLegacyConfigParser parser(TextPath::builtin("DATA/CRANIM.TXT")); - - parser.endLine(); // header - parser.endLine(); - - for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) - { - while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines - ; - - loadUnitAnimInfo(h3Data[dd]["graphics"], parser); - parser.endLine(); - } -} - -void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser & parser) const -{ - graphics["timeBetweenFidgets"].Float() = parser.readNumber(); - - JsonNode & animationTime = graphics["animationTime"]; - animationTime["walk"].Float() = parser.readNumber(); - animationTime["attack"].Float() = parser.readNumber(); - parser.readNumber(); // unused value "Flight animation time" - H3 actually uses "Walk animation time" even for flying creatures - animationTime["idle"].Float() = 10.0; - - JsonNode & missile = graphics["missile"]; - JsonNode & offsets = missile["offset"]; - - offsets["upperX"].Float() = parser.readNumber(); - offsets["upperY"].Float() = parser.readNumber(); - offsets["middleX"].Float() = parser.readNumber(); - offsets["middleY"].Float() = parser.readNumber(); - offsets["lowerX"].Float() = parser.readNumber(); - offsets["lowerY"].Float() = parser.readNumber(); - - for(int i=0; i<12; i++) - { - JsonNode entry; - entry.Float() = parser.readNumber(); - missile["frameAngles"].Vector().push_back(entry); - } - - // Unused property "troopCountLocationOffset" - parser.readNumber(); - - missile["attackClimaxFrame"].Float() = parser.readNumber(); - - // assume that creature is not a shooter and should not have whole missile field - if (missile["frameAngles"].Vector()[0].Float() == 0 && - missile["attackClimaxFrame"].Float() == 0) - graphics.Struct().erase("missile"); -} - -void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const -{ - cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); - - const JsonNode & animationTime = graphics["animationTime"]; - cre->animation.walkAnimationTime = animationTime["walk"].Float(); - cre->animation.idleAnimationTime = animationTime["idle"].Float(); - cre->animation.attackAnimationTime = animationTime["attack"].Float(); - - const JsonNode & missile = graphics["missile"]; - const JsonNode & offsets = missile["offset"]; - cre->animation.upperRightMissleOffsetX = static_cast(offsets["upperX"].Float()); - cre->animation.upperRightMissleOffsetY = static_cast(offsets["upperY"].Float()); - cre->animation.rightMissleOffsetX = static_cast(offsets["middleX"].Float()); - cre->animation.rightMissleOffsetY = static_cast(offsets["middleY"].Float()); - cre->animation.lowerRightMissleOffsetX = static_cast(offsets["lowerX"].Float()); - cre->animation.lowerRightMissleOffsetY = static_cast(offsets["lowerY"].Float()); - - cre->animation.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); - cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); - - cre->smallIconName = graphics["iconSmall"].String(); - cre->largeIconName = graphics["iconLarge"].String(); -} - -void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const -{ - creature->animDefName = AnimationPath::fromJson(config["graphics"]["animation"]); - - //FIXME: MOD COMPATIBILITY - if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) - { - for(const auto & ability : config["abilities"].Struct()) - { - if (!ability.second.isNull()) - { - auto b = JsonUtils::parseBonus(ability.second); - b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); - b->duration = BonusDuration::PERMANENT; - creature->addNewBonus(b); - } - } - } - else - { - for(const JsonNode &ability : config["abilities"].Vector()) - { - if(ability.getType() == JsonNode::JsonType::DATA_VECTOR) - { - logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey()); - } - else - { - auto b = JsonUtils::parseBonus(ability); - b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); - b->duration = BonusDuration::PERMANENT; - creature->addNewBonus(b); - } - } - } - - VLC->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction) - { - creature->faction = FactionID(faction); - }); - - for(const JsonNode &value : config["upgrades"].Vector()) - { - VLC->identifiers()->requestIdentifier("creature", value, [=](si32 identifier) - { - creature->upgrades.insert(CreatureID(identifier)); - }); - } - - creature->animation.projectileImageName = AnimationPath::fromJson(config["graphics"]["missile"]["projectile"]); - - for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) - { - CCreature::CreatureAnimation::RayColor color; - - color.start.r = value["start"].Vector()[0].Integer(); - color.start.g = value["start"].Vector()[1].Integer(); - color.start.b = value["start"].Vector()[2].Integer(); - color.start.a = value["start"].Vector()[3].Integer(); - - color.end.r = value["end"].Vector()[0].Integer(); - color.end.g = value["end"].Vector()[1].Integer(); - color.end.b = value["end"].Vector()[2].Integer(); - color.end.a = value["end"].Vector()[3].Integer(); - - creature->animation.projectileRay.push_back(color); - } - - creature->special = config["special"].Bool() || config["disabled"].Bool(); - - const JsonNode & sounds = config["sound"]; - creature->sounds.attack = AudioPath::fromJson(sounds["attack"]); - creature->sounds.defend = AudioPath::fromJson(sounds["defend"]); - creature->sounds.killed = AudioPath::fromJson(sounds["killed"]); - creature->sounds.move = AudioPath::fromJson(sounds["move"]); - creature->sounds.shoot = AudioPath::fromJson(sounds["shoot"]); - creature->sounds.wince = AudioPath::fromJson(sounds["wince"]); - creature->sounds.startMoving = AudioPath::fromJson(sounds["startMoving"]); - creature->sounds.endMoving = AudioPath::fromJson(sounds["endMoving"]); -} - -void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const -{ - for (const JsonNode &exp : input.Vector()) - { - const JsonVector &values = exp["values"].Vector(); - int lowerLimit = 1;//, upperLimit = 255; - if (values[0].getType() == JsonNode::JsonType::DATA_BOOL) - { - for (const JsonNode &val : values) - { - if(val.Bool()) - { - // parse each bonus separately - // we can not create copies since identifiers resolution does not tracks copies - // leading to unset identifier values in copies - auto bonus = JsonUtils::parseBonus (exp["bonus"]); - bonus->source = BonusSource::STACK_EXPERIENCE; - bonus->duration = BonusDuration::PERMANENT; - bonus->limiter = std::make_shared(RankRangeLimiter(lowerLimit)); - creature->addNewBonus (bonus); - break; //TODO: allow bonuses to turn off? - } - ++lowerLimit; - } - } - else - { - int lastVal = 0; - for (const JsonNode &val : values) - { - if (val.Float() != lastVal) - { - JsonNode bonusInput = exp["bonus"]; - bonusInput["val"].Float() = static_cast(val.Float()) - lastVal; - - auto bonus = JsonUtils::parseBonus (bonusInput); - bonus->source = BonusSource::STACK_EXPERIENCE; - bonus->duration = BonusDuration::PERMANENT; - bonus->limiter.reset (new RankRangeLimiter(lowerLimit)); - creature->addNewBonus (bonus); - } - lastVal = static_cast(val.Float()); - ++lowerLimit; - } - } - } -} - -void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const//help function for parsing CREXPBON.txt -{ - bool enable = false; //some bonuses are activated with values 2 or 1 - std::string buf = parser.readString(); - std::string mod = parser.readString(); - - switch (buf[0]) - { - case 'H': - b.type = BonusType::STACK_HEALTH; - b.valType = BonusValueType::PERCENT_TO_BASE; - break; - case 'A': - b.type = BonusType::PRIMARY_SKILL; - b.subtype = static_cast(PrimarySkill::ATTACK); - break; - case 'D': - b.type = BonusType::PRIMARY_SKILL; - b.subtype = static_cast(PrimarySkill::DEFENSE); - break; - case 'M': //Max damage - b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 2; - break; - case 'm': //Min damage - b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 1; - break; - case 'S': - b.type = BonusType::STACKS_SPEED; break; - case 'O': - b.type = BonusType::SHOTS; break; - case 'b': - b.type = BonusType::ENEMY_DEFENCE_REDUCTION; break; - case 'C': - b.type = BonusType::CHANGES_SPELL_COST_FOR_ALLY; break; - case 'd': - b.type = BonusType::DEFENSIVE_STANCE; break; - case 'e': - b.type = BonusType::DOUBLE_DAMAGE_CHANCE; - b.subtype = 0; - break; - case 'E': - b.type = BonusType::DEATH_STARE; - b.subtype = 0; //Gorgon - break; - case 'F': - b.type = BonusType::FEAR; break; - case 'g': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); - break; - case 'P': - b.type = BonusType::CASTS; break; - case 'R': - b.type = BonusType::ADDITIONAL_RETALIATION; break; - case 'W': - b.type = BonusType::MAGIC_RESISTANCE; - b.subtype = 0; //otherwise creature window goes crazy - break; - case 'f': //on-off skill - enable = true; //sometimes format is: 2 -> 0, 1 -> 1 - switch (mod[0]) - { - case 'A': - b.type = BonusType::ATTACKS_ALL_ADJACENT; break; - case 'b': - b.type = BonusType::RETURN_AFTER_STRIKE; break; - case 'B': - b.type = BonusType::TWO_HEX_ATTACK_BREATH; break; - case 'c': - b.type = BonusType::JOUSTING; - b.val = 5; - break; - case 'D': - b.type = BonusType::ADDITIONAL_ATTACK; break; - case 'f': - b.type = BonusType::FEARLESS; break; - case 'F': - b.type = BonusType::FLYING; break; - case 'm': - b.type = BonusType::MORALE; - b.val = 1; - b.valType = BonusValueType::INDEPENDENT_MAX; - break; - case 'M': - b.type = BonusType::NO_MORALE; break; - case 'p': //Mind spells - case 'P': - b.type = BonusType::MIND_IMMUNITY; break; - case 'r': - b.type = BonusType::REBIRTH; //on/off? makes sense? - b.subtype = 0; - b.val = 20; //arbitrary value - break; - case 'R': - b.type = BonusType::BLOCKS_RETALIATION; break; - case 's': - b.type = BonusType::FREE_SHOOTING; break; - case 'u': - b.type = BonusType::SPELL_RESISTANCE_AURA; break; - case 'U': - b.type = BonusType::UNDEAD; break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - break; - } - break; - case 'w': //specific spell immunities, enabled/disabled - enable = true; - switch (mod[0]) - { - case 'B': //Blind - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BLIND; - b.additionalInfo = 0;//normal immunity - break; - case 'H': //Hypnotize - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::HYPNOTIZE; - b.additionalInfo = 0;//normal immunity - break; - case 'I': //Implosion - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::IMPLOSION; - b.additionalInfo = 0;//normal immunity - break; - case 'K': //Berserk - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BERSERK; - b.additionalInfo = 0;//normal immunity - break; - case 'M': //Meteor Shower - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::METEOR_SHOWER; - b.additionalInfo = 0;//normal immunity - break; - case 'N': //dispell beneficial spells - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::DISPEL_HELPFUL_SPELLS; - b.additionalInfo = 0;//normal immunity - break; - case 'R': //Armageddon - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::ARMAGEDDON; - b.additionalInfo = 0;//normal immunity - break; - case 'S': //Slow - b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::SLOW; - b.additionalInfo = 0;//normal immunity - break; - case '6': - case '7': - case '8': - case '9': - b.type = BonusType::LEVEL_SPELL_IMMUNITY; - b.val = std::atoi(mod.c_str()) - 5; - break; - case ':': - b.type = BonusType::LEVEL_SPELL_IMMUNITY; - b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? - break; - case 'F': - b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::FIRE); - break; - case 'O': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::FIRE); - b.val = 100; //Full damage immunity - break; - case 'f': - b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::FIRE); - break; - case 'C': - b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::WATER); - break; - case 'W': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::WATER); - b.val = 100; //Full damage immunity - break; - case 'w': - b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::WATER); - break; - case 'E': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::EARTH); - b.val = 100; //Full damage immunity - break; - case 'e': - b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::EARTH); - break; - case 'A': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::AIR); - b.val = 100; //Full damage immunity - break; - case 'a': - b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::AIR); - break; - case 'D': - b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); - b.val = 100; //Full damage immunity - break; - case '0': - b.type = BonusType::RECEPTIVE; - break; - case 'm': - b.type = BonusType::MIND_IMMUNITY; - break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - } - break; - - case 'i': - enable = true; - b.type = BonusType::NO_DISTANCE_PENALTY; - break; - case 'o': - enable = true; - b.type = BonusType::NO_WALL_PENALTY; - break; - - case 'a': - case 'c': - case 'K': - case 'k': - b.type = BonusType::SPELL_AFTER_ATTACK; - b.subtype = stringToNumber(mod); - break; - case 'h': - b.type = BonusType::HATE; - b.subtype = stringToNumber(mod); - break; - case 'p': - case 'J': - b.type = BonusType::SPELL_BEFORE_ATTACK; - b.subtype = stringToNumber(mod); - b.additionalInfo = 3; //always expert? - break; - case 'r': - b.type = BonusType::HP_REGENERATION; - b.val = stringToNumber(mod); - break; - case 's': - b.type = BonusType::ENCHANTED; - b.subtype = stringToNumber(mod); - b.valType = BonusValueType::INDEPENDENT_MAX; - break; - default: - logGlobal->trace("Not parsed bonus %s %s", buf, mod); - return; - break; - } - switch (mod[0]) - { - case '+': - case '=': //should we allow percent values to stack or pick highest? - b.valType = BonusValueType::ADDITIVE_VALUE; - break; - } - - //limiters, range - si32 lastVal; - si32 curVal; - si32 lastLev = 0; - - if (enable) //0 and 2 means non-active, 1 - active - { - if (b.type != BonusType::REBIRTH) - b.val = 0; //on-off ability, no value specified - parser.readNumber(); // 0 level is never active - for (int i = 1; i < 11; ++i) - { - curVal = static_cast(parser.readNumber()); - if (curVal == 1) - { - b.limiter.reset (new RankRangeLimiter(i)); - bl.push_back(std::make_shared(b)); - break; //never turned off it seems - } - } - } - else - { - lastVal = static_cast(parser.readNumber()); - if (b.type == BonusType::HATE) - lastVal *= 10; //odd fix - //FIXME: value for zero level should be stored in our config files (independent of stack exp) - for (int i = 1; i < 11; ++i) - { - curVal = static_cast(parser.readNumber()); - if (b.type == BonusType::HATE) - curVal *= 10; //odd fix - if (curVal > lastVal) //threshold, add new bonus - { - b.val = curVal - lastVal; - lastVal = curVal; - b.limiter.reset (new RankRangeLimiter(i)); - bl.push_back(std::make_shared(b)); - lastLev = i; //start new range from here, i = previous rank - } - else if (curVal < lastVal) - { - b.val = lastVal; - b.limiter.reset (new RankRangeLimiter(lastLev, i)); - } - } - } -} - -int CCreatureHandler::stringToNumber(std::string & s) const -{ - boost::algorithm::replace_first(s,"#",""); //drop hash character - return std::atoi(s.c_str()); -} - -CCreatureHandler::~CCreatureHandler() -{ - for(auto & p : skillRequirements) - p.first = nullptr; -} - -CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const -{ - int r = 0; - if(tier == -1) //pick any allowed creature - { - do - { - r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId(); - } while (objects[r] && objects[r]->special); // find first "not special" creature - } - else - { - assert(vstd::iswithin(tier, 1, 7)); - std::vector allowed; - for(const auto & creature : objects) - { - if(!creature->special && creature->level == tier) - allowed.push_back(creature->getId()); - } - - if(allowed.empty()) - { - logGlobal->warn("Cannot pick a random creature of tier %d!", tier); - return CreatureID::NONE; - } - - return *RandomGeneratorUtil::nextItem(allowed, rand); - } - assert (r >= 0); //should always be, but it crashed - return CreatureID(r); -} - - -void CCreatureHandler::afterLoadFinalization() -{ - -} - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureHandler.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 "CCreatureHandler.h" + +#include "CGeneralTextHandler.h" +#include "ResourceSet.h" +#include "filesystem/Filesystem.h" +#include "VCMI_Lib.h" +#include "CTownHandler.h" +#include "GameSettings.h" +#include "constants/StringConstants.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.h" +#include "serializer/JsonDeserializer.h" +#include "serializer/JsonUpdater.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/CModHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const std::map CCreature::creatureQuantityRanges = +{ + {CCreature::CreatureQuantityId::FEW, "1-4"}, + {CCreature::CreatureQuantityId::SEVERAL, "5-9"}, + {CCreature::CreatureQuantityId::PACK, "10-19"}, + {CCreature::CreatureQuantityId::LOTS, "20-49"}, + {CCreature::CreatureQuantityId::HORDE, "50-99"}, + {CCreature::CreatureQuantityId::THRONG, "100-249"}, + {CCreature::CreatureQuantityId::SWARM, "250-499"}, + {CCreature::CreatureQuantityId::ZOUNDS, "500-999"}, + {CCreature::CreatureQuantityId::LEGION, "1000+"} +}; + +int32_t CCreature::getIndex() const +{ + return idNumber.toEnum(); +} + +int32_t CCreature::getIconIndex() const +{ + return iconIndex; +} + +std::string CCreature::getJsonKey() const +{ + return modScope + ':' + identifier; +} + +void CCreature::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "CPRSMALL", smallIconName); + cb(getIconIndex(), 0, "TWCRPORT", largeIconName); +} + +CreatureID CCreature::getId() const +{ + return idNumber; +} + +const IBonusBearer * CCreature::getBonusBearer() const +{ + return this; +} + +int32_t CCreature::getAdvMapAmountMin() const +{ + return ammMin; +} + +int32_t CCreature::getAdvMapAmountMax() const +{ + return ammMax; +} + +int32_t CCreature::getAIValue() const +{ + return AIValue; +} + +int32_t CCreature::getFightValue() const +{ + return fightValue; +} + +int32_t CCreature::getLevel() const +{ + return level; +} + +int32_t CCreature::getGrowth() const +{ + return growth; +} + +int32_t CCreature::getHorde() const +{ + return hordeGrowth; +} + +FactionID CCreature::getFaction() const +{ + return FactionID(faction); +} + +int32_t CCreature::getBaseAttack() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDefense() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDamageMin() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseDamageMax() const +{ + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseHitPoints() const +{ + static const auto SELECTOR = Selector::type()(BonusType::STACK_HEALTH).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseSpellPoints() const +{ + static const auto SELECTOR = Selector::type()(BonusType::CASTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseSpeed() const +{ + static const auto SELECTOR = Selector::type()(BonusType::STACKS_SPEED).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getBaseShots() const +{ + static const auto SELECTOR = Selector::type()(BonusType::SHOTS).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + return getExportedBonusList().valOfBonuses(SELECTOR); +} + +int32_t CCreature::getRecruitCost(GameResID resIndex) const +{ + if(resIndex.getNum() >= 0 && resIndex.getNum() < cost.size()) + return cost[resIndex]; + else + return 0; +} + +TResources CCreature::getFullRecruitCost() const +{ + return cost; +} + +bool CCreature::hasUpgrades() const +{ + return !upgrades.empty(); +} + +std::string CCreature::getNameTranslated() const +{ + return getNameSingularTranslated(); +} + +std::string CCreature::getNamePluralTranslated() const +{ + return VLC->generaltexth->translate(getNamePluralTextID()); +} + +std::string CCreature::getNameSingularTranslated() const +{ + return VLC->generaltexth->translate(getNameSingularTextID()); +} + +std::string CCreature::getNameTextID() const +{ + return getNameSingularTextID(); +} + +std::string CCreature::getNamePluralTextID() const +{ + return TextIdentifier("creatures", modScope, identifier, "name", "plural" ).get(); +} + +std::string CCreature::getNameSingularTextID() const +{ + return TextIdentifier("creatures", modScope, identifier, "name", "singular" ).get(); +} + +CCreature::CreatureQuantityId CCreature::getQuantityID(const int & quantity) +{ + if (quantity<5) + return CCreature::CreatureQuantityId::FEW; + if (quantity<10) + return CCreature::CreatureQuantityId::SEVERAL; + if (quantity<20) + return CCreature::CreatureQuantityId::PACK; + if (quantity<50) + return CCreature::CreatureQuantityId::LOTS; + if (quantity<100) + return CCreature::CreatureQuantityId::HORDE; + if (quantity<250) + return CCreature::CreatureQuantityId::THRONG; + if (quantity<500) + return CCreature::CreatureQuantityId::SWARM; + if (quantity<1000) + return CCreature::CreatureQuantityId::ZOUNDS; + + return CCreature::CreatureQuantityId::LEGION; +} + +std::string CCreature::getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId) +{ + if(creatureQuantityRanges.find(quantityId) != creatureQuantityRanges.end()) + return creatureQuantityRanges.at(quantityId); + + logGlobal->error("Wrong quantityId: %d", (int)quantityId); + assert(0); + return "[ERROR]"; +} + +int CCreature::estimateCreatureCount(ui32 countID) +{ + static const int creature_count[] = { 0, 3, 8, 15, 35, 75, 175, 375, 750, 2500 }; + + if(countID > 9) + { + logGlobal->error("Wrong countID %d!", countID); + return 0; + } + else + return creature_count[countID]; +} + +bool CCreature::isDoubleWide() const +{ + return doubleWide; +} + +/** + * Determines if the creature is of a good alignment. + * @return true if the creture is good, false otherwise. + */ +bool CCreature::isGood () const +{ + return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD; +} + +/** + * Determines if the creature is of an evil alignment. + * @return true if the creature is evil, false otherwise. + */ +bool CCreature::isEvil () const +{ + return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL; +} + +si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought +{ + int ret = 2147483645; + int resAmnt = static_cast(std::min(res.size(),cost.size())); + for(int i=0;i(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, getIndex(), subtype, BonusValueType::BASE_NUMBER); + addNewBonus(added); + } + else + { + std::shared_ptr b = existing[0]; + b->val = val; + } +} + +bool CCreature::isMyUpgrade(const CCreature *anotherCre) const +{ + //TODO upgrade of upgrade? + return vstd::contains(upgrades, anotherCre->getId()); +} + +bool CCreature::valid() const +{ + return this == VLC->creh->objects[idNumber]; +} + +std::string CCreature::nodeName() const +{ + return "\"" + getNamePluralTextID() + "\""; +} + +void CCreature::updateFrom(const JsonNode & data) +{ + JsonUpdater handler(nullptr, data); + + { + auto configScope = handler.enterStruct("config"); + + const JsonNode & configNode = handler.getCurrent(); + + serializeJson(handler); + + if(!configNode["hitPoints"].isNull()) + addBonus(configNode["hitPoints"].Integer(), BonusType::STACK_HEALTH); + + if(!configNode["speed"].isNull()) + addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); + + if(!configNode["attack"].isNull()) + addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); + + if(!configNode["defense"].isNull()) + addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); + + if(!configNode["damage"]["min"].isNull()) + addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); + + if(!configNode["damage"]["max"].isNull()) + addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); + + if(!configNode["shots"].isNull()) + addBonus(configNode["shots"].Integer(), BonusType::SHOTS); + + if(!configNode["spellPoints"].isNull()) + addBonus(configNode["spellPoints"].Integer(), BonusType::CASTS); + } + + + handler.serializeBonuses("bonuses", this); +} + +void CCreature::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("fightValue", fightValue); + handler.serializeInt("aiValue", AIValue); + handler.serializeInt("growth", growth); + handler.serializeInt("horde", hordeGrowth);// Needed at least until configurable buildings + + { + auto advMapNode = handler.enterStruct("advMapAmount"); + handler.serializeInt("min", ammMin); + handler.serializeInt("max", ammMax); + } + + if(handler.updating) + { + cost.serializeJson(handler, "cost"); + handler.serializeInt("faction", faction);//TODO: unify with deferred resolve + } + + handler.serializeInt("level", level); + handler.serializeBool("doubleWide", doubleWide); + + if(!handler.saving) + { + if(ammMin > ammMax) + logMod->error("Invalid creature '%s' configuration, advMapAmount.min > advMapAmount.max", identifier); + } +} + +CCreatureHandler::CCreatureHandler() + : expAfterUpgrade(0) +{ + VLC->creh = this; + loadCommanders(); +} + +const CCreature * CCreatureHandler::getCreature(const std::string & scope, const std::string & identifier) const +{ + std::optional index = VLC->identifiers()->getIdentifier(scope, "creature", identifier); + + if(!index) + throw std::runtime_error("Creature not found "+identifier); + + return objects[*index]; +} + +void CCreatureHandler::loadCommanders() +{ + auto configResource = JsonPath::builtin("config/commanders.json"); + + std::string modSource = VLC->modh->findResourceOrigin(configResource); + JsonNode data(configResource); + data.setMeta(modSource); + + const JsonNode & config = data; // switch to const data accessors + + for (auto bonus : config["bonusPerLevel"].Vector()) + { + commanderLevelPremy.push_back(JsonUtils::parseBonus(bonus.Vector())); + } + + int i = 0; + for (auto skill : config["skillLevels"].Vector()) + { + skillLevels.emplace_back(); + for (auto skillLevel : skill["levels"].Vector()) + { + skillLevels[i].push_back(static_cast(skillLevel.Float())); + } + ++i; + } + + for (auto ability : config["abilityRequirements"].Vector()) + { + std::pair , std::pair > a; + a.first = JsonUtils::parseBonus (ability["ability"].Vector()); + a.second.first = static_cast(ability["skills"].Vector()[0].Float()); + a.second.second = static_cast(ability["skills"].Vector()[1].Float()); + skillRequirements.push_back (a); + } +} + +void CCreatureHandler::loadBonuses(JsonNode & creature, std::string bonuses) const +{ + auto makeBonusNode = [&](const std::string & type, double val = 0) -> JsonNode + { + JsonNode ret; + ret["type"].String() = type; + ret["val"].Float() = val; + return ret; + }; + + static const std::map abilityMap = + { + {"FLYING_ARMY", makeBonusNode("FLYING")}, + {"SHOOTING_ARMY", makeBonusNode("SHOOTER")}, + {"SIEGE_WEAPON", makeBonusNode("SIEGE_WEAPON")}, + {"const_free_attack", makeBonusNode("BLOCKS_RETALIATION")}, + {"IS_UNDEAD", makeBonusNode("UNDEAD")}, + {"const_no_melee_penalty", makeBonusNode("NO_MELEE_PENALTY")}, + {"const_jousting", makeBonusNode("JOUSTING", 5)}, + {"KING_1", makeBonusNode("KING")}, // Slayer with no expertise + {"KING_2", makeBonusNode("KING", 2)}, // Advanced Slayer or better + {"KING_3", makeBonusNode("KING", 3)}, // Expert Slayer only + {"const_no_wall_penalty", makeBonusNode("NO_WALL_PENALTY")}, + {"MULTI_HEADED", makeBonusNode("ATTACKS_ALL_ADJACENT")}, + {"IMMUNE_TO_MIND_SPELLS", makeBonusNode("MIND_IMMUNITY")}, + {"HAS_EXTENDED_ATTACK", makeBonusNode("TWO_HEX_ATTACK_BREATH")} + }; + + auto hasAbility = [&](const std::string & name) -> bool + { + return boost::algorithm::find_first(bonuses, name); + }; + + for(const auto & a : abilityMap) + { + if(hasAbility(a.first)) + creature["abilities"][a.first] = a.second; + } + if(hasAbility("DOUBLE_WIDE")) + creature["doubleWide"].Bool() = true; + + if(hasAbility("const_raises_morale")) + { + JsonNode node = makeBonusNode("MORALE"); + node["val"].Float() = 1; + node["propagator"].String() = "HERO"; + creature["abilities"]["const_raises_morale"] = node; + } +} + +std::vector CCreatureHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser parser(TextPath::builtin("DATA/CRTRAITS.TXT")); + + parser.endLine(); // header + + // this file is a bit different in some of Russian localisations: + //ENG: Singular Plural Wood ... + //RUS: Singular Plural Plural2 Wood ... + // Try to detect which version this is by header + // TODO: use 3rd name? Stand for "whose", e.g. pikemans' + size_t namesCount = 2; + { + if ( parser.readString() != "Singular" || parser.readString() != "Plural" ) + throw std::runtime_error("Incorrect format of CrTraits.txt"); + + if (parser.readString() == "Plural2") + namesCount = 3; + + parser.endLine(); + } + + for (size_t i=0; iidNumber = CreatureID(index); + cre->iconIndex = cre->getIndex() + 2; + cre->identifier = identifier; + cre->modScope = scope; + + JsonDeserializer handler(nullptr, node); + cre->serializeJson(handler); + + cre->cost = ResourceSet(node["cost"]); + + VLC->generaltexth->registerString(scope, cre->getNameSingularTextID(), node["name"]["singular"].String()); + VLC->generaltexth->registerString(scope, cre->getNamePluralTextID(), node["name"]["plural"].String()); + + cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); + cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); + cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); + cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); + + cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); + cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); + + assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); + + if(!node["shots"].isNull()) + cre->addBonus(node["shots"].Integer(), BonusType::SHOTS); + + loadStackExperience(cre, node["stackExperience"]); + loadJsonAnimation(cre, node["graphics"]); + loadCreatureJson(cre, node); + + for(const auto & extraName : node["extraNames"].Vector()) + { + for(const auto & type_name : getTypeNames()) + registerObject(scope, type_name, extraName.String(), cre->getIndex()); + } + + JsonNode advMapFile = node["graphics"]["map"]; + JsonNode advMapMask = node["graphics"]["mapMask"]; + + VLC->identifiers()->requestIdentifier(scope, "object", "monster", [=](si32 index) + { + JsonNode conf; + conf.setMeta(scope); + + VLC->objtypeh->loadSubObject(cre->identifier, conf, Obj::MONSTER, cre->getId().num); + if (!advMapFile.isNull()) + { + JsonNode templ; + templ["animation"] = advMapFile; + if (!advMapMask.isNull()) + templ["mask"] = advMapMask; + templ.setMeta(scope); + + // if creature has custom advMapFile, reset any potentially imported H3M templates and use provided file instead + VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->clearTemplates(); + VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->addTemplate(templ); + } + + // object does not have any templates - this is not usable object (e.g. pseudo-creature like Arrow Tower) + if (VLC->objtypeh->getHandlerFor(Obj::MONSTER, cre->getId().num)->getTemplates().empty()) + VLC->objtypeh->removeSubObject(Obj::MONSTER, cre->getId().num); + }); + + return cre; +} + +const std::vector & CCreatureHandler::getTypeNames() const +{ + static const std::vector typeNames = { "creature" }; + return typeNames; +} + +std::vector CCreatureHandler::getDefaultAllowed() const +{ + std::vector ret; + + ret.reserve(objects.size()); + for(const CCreature * crea : objects) + { + ret.push_back(crea ? !crea->special : false); + } + return ret; +} + +void CCreatureHandler::loadCrExpMod() +{ + if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values + { + //Calculate rank exp values, formula appears complicated bu no parsing needed + expRanks.resize(8); + int dif = 0; + int it = 8000; //ignore name of this variable + expRanks[0].push_back(it); + for (int j = 1; j < 10; ++j) //used for tiers 8-10, and all other probably + { + expRanks[0].push_back(expRanks[0][j-1] + it + dif); + dif += it/5; + } + for (int i = 1; i < 8; ++i) //used for tiers 1-7 + { + dif = 0; + it = 1000 * i; + expRanks[i].push_back(it); + for (int j = 1; j < 10; ++j) + { + expRanks[i].push_back(expRanks[i][j-1] + it + dif); + dif += it/5; + } + } + + CLegacyConfigParser expBonParser(TextPath::builtin("DATA/CREXPMOD.TXT")); + + expBonParser.endLine(); //header + + maxExpPerBattle.resize(8); + for (int i = 1; i < 8; ++i) + { + expBonParser.readString(); //index + expBonParser.readString(); //float multiplier -> hardcoded + expBonParser.readString(); //ignore upgrade mod? ->hardcoded + expBonParser.readString(); //already calculated + + maxExpPerBattle[i] = static_cast(expBonParser.readNumber()); + expRanks[i].push_back(expRanks[i].back() + static_cast(expBonParser.readNumber())); + + expBonParser.endLine(); + } + //exp for tier >7, rank 11 + expRanks[0].push_back(147000); + expAfterUpgrade = 75; //percent + maxExpPerBattle[0] = maxExpPerBattle[7]; + } +} + + +void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) +{ + if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience bonuses + { + logGlobal->debug("\tLoading stack experience bonuses"); + auto addBonusForAllCreatures = [&](std::shared_ptr b) { + auto limiter = std::make_shared(); + b->addLimiter(limiter); + globalEffects.addNewBonus(b); + }; + auto addBonusForTier = [&](int tier, std::shared_ptr b) { + assert(vstd::iswithin(tier, 1, 7)); + //bonuses from level 7 are given to high-level creatures too + auto max = tier == GameConstants::CREATURES_PER_TOWN ? std::numeric_limits::max() : tier + 1; + auto limiter = std::make_shared(tier, max); + b->addLimiter(limiter); + globalEffects.addNewBonus(b); + }; + + CLegacyConfigParser parser(TextPath::builtin("DATA/CREXPBON.TXT")); + + Bonus b; //prototype with some default properties + b.source = BonusSource::STACK_EXPERIENCE; + b.duration = BonusDuration::PERMANENT; + b.valType = BonusValueType::ADDITIVE_VALUE; + b.effectRange = BonusLimitEffect::NO_LIMIT; + b.additionalInfo = 0; + b.turnsRemain = 0; + BonusList bl; + + parser.endLine(); + + parser.readString(); //ignore index + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForAllCreatures(b); //health bonus is common for all + parser.endLine(); + + for (int i = 1; i < 7; ++i) + { + for (int j = 0; j < 4; ++j) //four modifiers common for tiers + { + parser.readString(); //ignore index + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForTier(i, b); + parser.endLine(); + } + } + for (int j = 0; j < 4; ++j) //tier 7 + { + parser.readString(); //ignore index + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + addBonusForTier(7, b); + parser.endLine(); + } + do //parse everything that's left + { + auto sid = static_cast(parser.readNumber()); //id = this particular creature ID + + b.sid = sid; + bl.clear(); + loadStackExp(b, bl, parser); + for(const auto & b : bl) + { + objects[sid]->addNewBonus(b); //add directly to CCreature Node + } + } + while (parser.endLine()); + + }//end of Stack Experience +} + +void CCreatureHandler::loadAnimationInfo(std::vector &h3Data) const +{ + CLegacyConfigParser parser(TextPath::builtin("DATA/CRANIM.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for(int dd = 0; dd < VLC->settings()->getInteger(EGameSettings::TEXTS_CREATURE); ++dd) + { + while (parser.isNextEntryEmpty() && parser.endLine()) // skip empty lines + ; + + loadUnitAnimInfo(h3Data[dd]["graphics"], parser); + parser.endLine(); + } +} + +void CCreatureHandler::loadUnitAnimInfo(JsonNode & graphics, CLegacyConfigParser & parser) const +{ + graphics["timeBetweenFidgets"].Float() = parser.readNumber(); + + JsonNode & animationTime = graphics["animationTime"]; + animationTime["walk"].Float() = parser.readNumber(); + animationTime["attack"].Float() = parser.readNumber(); + parser.readNumber(); // unused value "Flight animation time" - H3 actually uses "Walk animation time" even for flying creatures + animationTime["idle"].Float() = 10.0; + + JsonNode & missile = graphics["missile"]; + JsonNode & offsets = missile["offset"]; + + offsets["upperX"].Float() = parser.readNumber(); + offsets["upperY"].Float() = parser.readNumber(); + offsets["middleX"].Float() = parser.readNumber(); + offsets["middleY"].Float() = parser.readNumber(); + offsets["lowerX"].Float() = parser.readNumber(); + offsets["lowerY"].Float() = parser.readNumber(); + + for(int i=0; i<12; i++) + { + JsonNode entry; + entry.Float() = parser.readNumber(); + missile["frameAngles"].Vector().push_back(entry); + } + + // Unused property "troopCountLocationOffset" + parser.readNumber(); + + missile["attackClimaxFrame"].Float() = parser.readNumber(); + + // assume that creature is not a shooter and should not have whole missile field + if (missile["frameAngles"].Vector()[0].Float() == 0 && + missile["attackClimaxFrame"].Float() == 0) + graphics.Struct().erase("missile"); +} + +void CCreatureHandler::loadJsonAnimation(CCreature * cre, const JsonNode & graphics) const +{ + cre->animation.timeBetweenFidgets = graphics["timeBetweenFidgets"].Float(); + + const JsonNode & animationTime = graphics["animationTime"]; + cre->animation.walkAnimationTime = animationTime["walk"].Float(); + cre->animation.idleAnimationTime = animationTime["idle"].Float(); + cre->animation.attackAnimationTime = animationTime["attack"].Float(); + + const JsonNode & missile = graphics["missile"]; + const JsonNode & offsets = missile["offset"]; + cre->animation.upperRightMissleOffsetX = static_cast(offsets["upperX"].Float()); + cre->animation.upperRightMissleOffsetY = static_cast(offsets["upperY"].Float()); + cre->animation.rightMissleOffsetX = static_cast(offsets["middleX"].Float()); + cre->animation.rightMissleOffsetY = static_cast(offsets["middleY"].Float()); + cre->animation.lowerRightMissleOffsetX = static_cast(offsets["lowerX"].Float()); + cre->animation.lowerRightMissleOffsetY = static_cast(offsets["lowerY"].Float()); + + cre->animation.attackClimaxFrame = static_cast(missile["attackClimaxFrame"].Float()); + cre->animation.missleFrameAngles = missile["frameAngles"].convertTo >(); + + cre->smallIconName = graphics["iconSmall"].String(); + cre->largeIconName = graphics["iconLarge"].String(); +} + +void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & config) const +{ + creature->animDefName = AnimationPath::fromJson(config["graphics"]["animation"]); + + //FIXME: MOD COMPATIBILITY + if (config["abilities"].getType() == JsonNode::JsonType::DATA_STRUCT) + { + for(const auto & ability : config["abilities"].Struct()) + { + if (!ability.second.isNull()) + { + auto b = JsonUtils::parseBonus(ability.second); + b->source = BonusSource::CREATURE_ABILITY; + b->sid = creature->getIndex(); + b->duration = BonusDuration::PERMANENT; + creature->addNewBonus(b); + } + } + } + else + { + for(const JsonNode &ability : config["abilities"].Vector()) + { + if(ability.getType() == JsonNode::JsonType::DATA_VECTOR) + { + logMod->error("Ignored outdated creature ability format in %s", creature->getJsonKey()); + } + else + { + auto b = JsonUtils::parseBonus(ability); + b->source = BonusSource::CREATURE_ABILITY; + b->sid = creature->getIndex(); + b->duration = BonusDuration::PERMANENT; + creature->addNewBonus(b); + } + } + } + + VLC->identifiers()->requestIdentifier("faction", config["faction"], [=](si32 faction) + { + creature->faction = FactionID(faction); + }); + + for(const JsonNode &value : config["upgrades"].Vector()) + { + VLC->identifiers()->requestIdentifier("creature", value, [=](si32 identifier) + { + creature->upgrades.insert(CreatureID(identifier)); + }); + } + + creature->animation.projectileImageName = AnimationPath::fromJson(config["graphics"]["missile"]["projectile"]); + + for(const JsonNode & value : config["graphics"]["missile"]["ray"].Vector()) + { + CCreature::CreatureAnimation::RayColor color; + + color.start.r = value["start"].Vector()[0].Integer(); + color.start.g = value["start"].Vector()[1].Integer(); + color.start.b = value["start"].Vector()[2].Integer(); + color.start.a = value["start"].Vector()[3].Integer(); + + color.end.r = value["end"].Vector()[0].Integer(); + color.end.g = value["end"].Vector()[1].Integer(); + color.end.b = value["end"].Vector()[2].Integer(); + color.end.a = value["end"].Vector()[3].Integer(); + + creature->animation.projectileRay.push_back(color); + } + + creature->special = config["special"].Bool() || config["disabled"].Bool(); + + const JsonNode & sounds = config["sound"]; + creature->sounds.attack = AudioPath::fromJson(sounds["attack"]); + creature->sounds.defend = AudioPath::fromJson(sounds["defend"]); + creature->sounds.killed = AudioPath::fromJson(sounds["killed"]); + creature->sounds.move = AudioPath::fromJson(sounds["move"]); + creature->sounds.shoot = AudioPath::fromJson(sounds["shoot"]); + creature->sounds.wince = AudioPath::fromJson(sounds["wince"]); + creature->sounds.startMoving = AudioPath::fromJson(sounds["startMoving"]); + creature->sounds.endMoving = AudioPath::fromJson(sounds["endMoving"]); +} + +void CCreatureHandler::loadStackExperience(CCreature * creature, const JsonNode & input) const +{ + for (const JsonNode &exp : input.Vector()) + { + const JsonVector &values = exp["values"].Vector(); + int lowerLimit = 1;//, upperLimit = 255; + if (values[0].getType() == JsonNode::JsonType::DATA_BOOL) + { + for (const JsonNode &val : values) + { + if(val.Bool()) + { + // parse each bonus separately + // we can not create copies since identifiers resolution does not tracks copies + // leading to unset identifier values in copies + auto bonus = JsonUtils::parseBonus (exp["bonus"]); + bonus->source = BonusSource::STACK_EXPERIENCE; + bonus->duration = BonusDuration::PERMANENT; + bonus->limiter = std::make_shared(RankRangeLimiter(lowerLimit)); + creature->addNewBonus (bonus); + break; //TODO: allow bonuses to turn off? + } + ++lowerLimit; + } + } + else + { + int lastVal = 0; + for (const JsonNode &val : values) + { + if (val.Float() != lastVal) + { + JsonNode bonusInput = exp["bonus"]; + bonusInput["val"].Float() = static_cast(val.Float()) - lastVal; + + auto bonus = JsonUtils::parseBonus (bonusInput); + bonus->source = BonusSource::STACK_EXPERIENCE; + bonus->duration = BonusDuration::PERMANENT; + bonus->limiter.reset (new RankRangeLimiter(lowerLimit)); + creature->addNewBonus (bonus); + } + lastVal = static_cast(val.Float()); + ++lowerLimit; + } + } + } +} + +void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const//help function for parsing CREXPBON.txt +{ + bool enable = false; //some bonuses are activated with values 2 or 1 + std::string buf = parser.readString(); + std::string mod = parser.readString(); + + switch (buf[0]) + { + case 'H': + b.type = BonusType::STACK_HEALTH; + b.valType = BonusValueType::PERCENT_TO_BASE; + break; + case 'A': + b.type = BonusType::PRIMARY_SKILL; + b.subtype = static_cast(PrimarySkill::ATTACK); + break; + case 'D': + b.type = BonusType::PRIMARY_SKILL; + b.subtype = static_cast(PrimarySkill::DEFENSE); + break; + case 'M': //Max damage + b.type = BonusType::CREATURE_DAMAGE; + b.subtype = 2; + break; + case 'm': //Min damage + b.type = BonusType::CREATURE_DAMAGE; + b.subtype = 1; + break; + case 'S': + b.type = BonusType::STACKS_SPEED; break; + case 'O': + b.type = BonusType::SHOTS; break; + case 'b': + b.type = BonusType::ENEMY_DEFENCE_REDUCTION; break; + case 'C': + b.type = BonusType::CHANGES_SPELL_COST_FOR_ALLY; break; + case 'd': + b.type = BonusType::DEFENSIVE_STANCE; break; + case 'e': + b.type = BonusType::DOUBLE_DAMAGE_CHANCE; + b.subtype = 0; + break; + case 'E': + b.type = BonusType::DEATH_STARE; + b.subtype = 0; //Gorgon + break; + case 'F': + b.type = BonusType::FEAR; break; + case 'g': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::ANY); + break; + case 'P': + b.type = BonusType::CASTS; break; + case 'R': + b.type = BonusType::ADDITIONAL_RETALIATION; break; + case 'W': + b.type = BonusType::MAGIC_RESISTANCE; + b.subtype = 0; //otherwise creature window goes crazy + break; + case 'f': //on-off skill + enable = true; //sometimes format is: 2 -> 0, 1 -> 1 + switch (mod[0]) + { + case 'A': + b.type = BonusType::ATTACKS_ALL_ADJACENT; break; + case 'b': + b.type = BonusType::RETURN_AFTER_STRIKE; break; + case 'B': + b.type = BonusType::TWO_HEX_ATTACK_BREATH; break; + case 'c': + b.type = BonusType::JOUSTING; + b.val = 5; + break; + case 'D': + b.type = BonusType::ADDITIONAL_ATTACK; break; + case 'f': + b.type = BonusType::FEARLESS; break; + case 'F': + b.type = BonusType::FLYING; break; + case 'm': + b.type = BonusType::MORALE; + b.val = 1; + b.valType = BonusValueType::INDEPENDENT_MAX; + break; + case 'M': + b.type = BonusType::NO_MORALE; break; + case 'p': //Mind spells + case 'P': + b.type = BonusType::MIND_IMMUNITY; break; + case 'r': + b.type = BonusType::REBIRTH; //on/off? makes sense? + b.subtype = 0; + b.val = 20; //arbitrary value + break; + case 'R': + b.type = BonusType::BLOCKS_RETALIATION; break; + case 's': + b.type = BonusType::FREE_SHOOTING; break; + case 'u': + b.type = BonusType::SPELL_RESISTANCE_AURA; break; + case 'U': + b.type = BonusType::UNDEAD; break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + break; + } + break; + case 'w': //specific spell immunities, enabled/disabled + enable = true; + switch (mod[0]) + { + case 'B': //Blind + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::BLIND; + b.additionalInfo = 0;//normal immunity + break; + case 'H': //Hypnotize + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::HYPNOTIZE; + b.additionalInfo = 0;//normal immunity + break; + case 'I': //Implosion + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::IMPLOSION; + b.additionalInfo = 0;//normal immunity + break; + case 'K': //Berserk + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::BERSERK; + b.additionalInfo = 0;//normal immunity + break; + case 'M': //Meteor Shower + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::METEOR_SHOWER; + b.additionalInfo = 0;//normal immunity + break; + case 'N': //dispell beneficial spells + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::DISPEL_HELPFUL_SPELLS; + b.additionalInfo = 0;//normal immunity + break; + case 'R': //Armageddon + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::ARMAGEDDON; + b.additionalInfo = 0;//normal immunity + break; + case 'S': //Slow + b.type = BonusType::SPELL_IMMUNITY; + b.subtype = SpellID::SLOW; + b.additionalInfo = 0;//normal immunity + break; + case '6': + case '7': + case '8': + case '9': + b.type = BonusType::LEVEL_SPELL_IMMUNITY; + b.val = std::atoi(mod.c_str()) - 5; + break; + case ':': + b.type = BonusType::LEVEL_SPELL_IMMUNITY; + b.val = GameConstants::SPELL_LEVELS; //in case someone adds higher level spells? + break; + case 'F': + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::FIRE); + break; + case 'O': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::FIRE); + b.val = 100; //Full damage immunity + break; + case 'f': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::FIRE); + break; + case 'C': + b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::WATER); + break; + case 'W': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::WATER); + b.val = 100; //Full damage immunity + break; + case 'w': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::WATER); + break; + case 'E': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::EARTH); + b.val = 100; //Full damage immunity + break; + case 'e': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::EARTH); + break; + case 'A': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::AIR); + b.val = 100; //Full damage immunity + break; + case 'a': + b.type = BonusType::SPELL_SCHOOL_IMMUNITY; + b.subtype = SpellSchool(ESpellSchool::AIR); + break; + case 'D': + b.type = BonusType::SPELL_DAMAGE_REDUCTION; + b.subtype = SpellSchool(ESpellSchool::ANY); + b.val = 100; //Full damage immunity + break; + case '0': + b.type = BonusType::RECEPTIVE; + break; + case 'm': + b.type = BonusType::MIND_IMMUNITY; + break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + } + break; + + case 'i': + enable = true; + b.type = BonusType::NO_DISTANCE_PENALTY; + break; + case 'o': + enable = true; + b.type = BonusType::NO_WALL_PENALTY; + break; + + case 'a': + case 'c': + case 'K': + case 'k': + b.type = BonusType::SPELL_AFTER_ATTACK; + b.subtype = stringToNumber(mod); + break; + case 'h': + b.type = BonusType::HATE; + b.subtype = stringToNumber(mod); + break; + case 'p': + case 'J': + b.type = BonusType::SPELL_BEFORE_ATTACK; + b.subtype = stringToNumber(mod); + b.additionalInfo = 3; //always expert? + break; + case 'r': + b.type = BonusType::HP_REGENERATION; + b.val = stringToNumber(mod); + break; + case 's': + b.type = BonusType::ENCHANTED; + b.subtype = stringToNumber(mod); + b.valType = BonusValueType::INDEPENDENT_MAX; + break; + default: + logGlobal->trace("Not parsed bonus %s %s", buf, mod); + return; + break; + } + switch (mod[0]) + { + case '+': + case '=': //should we allow percent values to stack or pick highest? + b.valType = BonusValueType::ADDITIVE_VALUE; + break; + } + + //limiters, range + si32 lastVal; + si32 curVal; + si32 lastLev = 0; + + if (enable) //0 and 2 means non-active, 1 - active + { + if (b.type != BonusType::REBIRTH) + b.val = 0; //on-off ability, no value specified + parser.readNumber(); // 0 level is never active + for (int i = 1; i < 11; ++i) + { + curVal = static_cast(parser.readNumber()); + if (curVal == 1) + { + b.limiter.reset (new RankRangeLimiter(i)); + bl.push_back(std::make_shared(b)); + break; //never turned off it seems + } + } + } + else + { + lastVal = static_cast(parser.readNumber()); + if (b.type == BonusType::HATE) + lastVal *= 10; //odd fix + //FIXME: value for zero level should be stored in our config files (independent of stack exp) + for (int i = 1; i < 11; ++i) + { + curVal = static_cast(parser.readNumber()); + if (b.type == BonusType::HATE) + curVal *= 10; //odd fix + if (curVal > lastVal) //threshold, add new bonus + { + b.val = curVal - lastVal; + lastVal = curVal; + b.limiter.reset (new RankRangeLimiter(i)); + bl.push_back(std::make_shared(b)); + lastLev = i; //start new range from here, i = previous rank + } + else if (curVal < lastVal) + { + b.val = lastVal; + b.limiter.reset (new RankRangeLimiter(lastLev, i)); + } + } + } +} + +int CCreatureHandler::stringToNumber(std::string & s) const +{ + boost::algorithm::replace_first(s,"#",""); //drop hash character + return std::atoi(s.c_str()); +} + +CCreatureHandler::~CCreatureHandler() +{ + for(auto & p : skillRequirements) + p.first = nullptr; +} + +CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const +{ + int r = 0; + if(tier == -1) //pick any allowed creature + { + do + { + r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId(); + } while (objects[r] && objects[r]->special); // find first "not special" creature + } + else + { + assert(vstd::iswithin(tier, 1, 7)); + std::vector allowed; + for(const auto & creature : objects) + { + if(!creature->special && creature->level == tier) + allowed.push_back(creature->getId()); + } + + if(allowed.empty()) + { + logGlobal->warn("Cannot pick a random creature of tier %d!", tier); + return CreatureID::NONE; + } + + return *RandomGeneratorUtil::nextItem(allowed, rand); + } + assert (r >= 0); //should always be, but it crashed + return CreatureID(r); +} + + +void CCreatureHandler::afterLoadFinalization() +{ + +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 654fe9d22..ee6ce8823 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -1,320 +1,320 @@ -/* - * CCreatureHandler.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 "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "ConstTransitivePtr.h" -#include "ResourceSet.h" -#include "GameConstants.h" -#include "JsonNode.h" -#include "IHandlerBase.h" -#include "CRandomGenerator.h" -#include "Color.h" -#include "filesystem/ResourcePath.h" - -#include -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class CLegacyConfigParser; -class CCreatureHandler; -class CCreature; -class JsonSerializeFormat; - -class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode -{ - friend class CCreatureHandler; - std::string modScope; - std::string identifier; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - - CreatureID idNumber; - - FactionID faction = FactionID::NEUTRAL; - ui8 level = 0; // 0 - unknown; 1-7 for "usual" creatures - - //stats that are not handled by bonus system - ui32 fightValue, AIValue, growth, hordeGrowth; - - bool doubleWide = false; - - TResources cost; //cost[res_id] - amount of that resource required to buy creature from dwelling - -public: - ui32 ammMin, ammMax; // initial size of stack of these creatures on adventure map (if not set in editor) - - bool special = true; // Creature is not available normally (war machines, commanders, several unused creatures, etc - - std::set upgrades; // IDs of creatures to which this creature can be upgraded - - AnimationPath animDefName; // creature animation used during battles - - si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now. - /// names of files with appropriate icons. Used only during loading - std::string smallIconName; - std::string largeIconName; - - enum class CreatureQuantityId - { - FEW = 1, - SEVERAL, - PACK, - LOTS, - HORDE, - THRONG, - SWARM, - ZOUNDS, - LEGION - }; - - struct CreatureAnimation - { - struct RayColor { - ColorRGBA start; - ColorRGBA end; - - template void serialize(Handler &h, const int version) - { - h & start & end; - } - }; - - double timeBetweenFidgets, idleAnimationTime, - walkAnimationTime, attackAnimationTime; - int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX, - upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; - - std::vector missleFrameAngles; - int attackClimaxFrame; - - AnimationPath projectileImageName; - std::vector projectileRay; - //bool projectileSpin; //if true, appropriate projectile is spinning during flight - - template void serialize(Handler &h, const int version) - { - h & timeBetweenFidgets; - h & idleAnimationTime; - h & walkAnimationTime; - h & attackAnimationTime; - h & upperRightMissleOffsetX; - h & rightMissleOffsetX; - h & lowerRightMissleOffsetX; - h & upperRightMissleOffsetY; - h & rightMissleOffsetY; - h & lowerRightMissleOffsetY; - h & missleFrameAngles; - h & attackClimaxFrame; - h & projectileImageName; - h & projectileRay; - } - } animation; - - //sound info - struct CreatureBattleSounds - { - AudioPath attack; - AudioPath defend; - AudioPath killed; // was killed or died - AudioPath move; - AudioPath shoot; // range attack - AudioPath wince; // attacked but did not die - AudioPath startMoving; - AudioPath endMoving; - - template void serialize(Handler &h, const int version) - { - h & attack; - h & defend; - h & killed; - h & move; - h & shoot; - h & wince; - h & startMoving; - h & endMoving; - } - } sounds; - - ArtifactID warMachine; - - std::string getNamePluralTranslated() const override; - std::string getNameSingularTranslated() const override; - - std::string getNamePluralTextID() const override; - std::string getNameSingularTextID() const override; - - FactionID getFaction() const override; - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - CreatureID getId() const override; - virtual const IBonusBearer * getBonusBearer() const override; - - int32_t getAdvMapAmountMin() const override; - int32_t getAdvMapAmountMax() const override; - int32_t getAIValue() const override; - int32_t getFightValue() const override; - int32_t getLevel() const override; - int32_t getGrowth() const override; - int32_t getHorde() const override; - - int32_t getBaseAttack() const override; - int32_t getBaseDefense() const override; - int32_t getBaseDamageMin() const override; - int32_t getBaseDamageMax() const override; - int32_t getBaseHitPoints() const override; - int32_t getBaseSpellPoints() const override; - int32_t getBaseSpeed() const override; - int32_t getBaseShots() const override; - - int32_t getRecruitCost(GameResID resIndex) const override; - TResources getFullRecruitCost() const override; - bool isDoubleWide() const override; //returns true if unit is double wide on battlefield - bool hasUpgrades() const override; - - bool isGood () const; - bool isEvil () const; - si32 maxAmount(const TResources &res) const; //how many creatures can be bought - static CCreature::CreatureQuantityId getQuantityID(const int & quantity); - static std::string getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId); - static int estimateCreatureCount(ui32 countID); //reverse version of above function, returns middle of range - bool isMyUpgrade(const CCreature *anotherCre) const; - - bool valid() const; - - void addBonus(int val, BonusType type, int subtype = -1); - std::string nodeName() const override; - - template - int getRandomAmount(RanGen ranGen) const - { - if(ammMax == ammMin) - return ammMax; - else - return ammMin + (ranGen() % (ammMax - ammMin)); - } - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & cost; - h & upgrades; - h & fightValue; - h & AIValue; - h & growth; - h & hordeGrowth; - h & ammMin; - h & ammMax; - h & level; - h & animDefName; - h & iconIndex; - h & smallIconName; - h & largeIconName; - - h & idNumber; - h & faction; - h & sounds; - h & animation; - - h & doubleWide; - h & special; - h & identifier; - h & modScope; - h & warMachine; - } - - CCreature(); - -private: - static const std::map creatureQuantityRanges; -}; - -class DLL_LINKAGE CCreatureHandler : public CHandlerBase -{ -private: - void loadJsonAnimation(CCreature * creature, const JsonNode & graphics) const; - void loadStackExperience(CCreature * creature, const JsonNode & input) const; - void loadCreatureJson(CCreature * creature, const JsonNode & config) const; - - /// adding abilities from ZCRTRAIT.TXT - void loadBonuses(JsonNode & creature, std::string bonuses) const; - /// load all creatures from H3 files - void load(); - void loadCommanders(); - /// load creature from json structure - void load(std::string creatureID, const JsonNode & node); - /// read cranim.txt file from H3 - void loadAnimationInfo(std::vector & h3Data) const; - /// read one line from cranim.txt - void loadUnitAnimInfo(JsonNode & unit, CLegacyConfigParser & parser) const; - /// parse crexpbon.txt file from H3 - void loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const; - /// help function for parsing CREXPBON.txt - int stringToNumber(std::string & s) const; - -protected: - const std::vector & getTypeNames() const override; - CCreature * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; - -public: - std::set doubledCreatures; //they get double week - - //stack exp - std::vector > expRanks; // stack experience needed for certain rank, index 0 for other tiers (?) - std::vector maxExpPerBattle; //%, tiers same as above - si8 expAfterUpgrade;//multiplier in % - - //Commanders - BonusList commanderLevelPremy; //bonus values added with each level-up - std::vector< std::vector > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE - std::vector , std::pair > > skillRequirements; // first - Bonus, second - which two skills are needed to use it - - const CCreature * getCreature(const std::string & scope, const std::string & identifier) const; - - CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any - - CCreatureHandler(); - ~CCreatureHandler(); - - /// load all stack experience bonuses from H3 files - void loadCrExpBon(CBonusSystemNode & globalEffects); - - /// load all stack modifier bonuses from H3 files. TODO: move this to json - void loadCrExpMod(); - - void afterLoadFinalization() override; - - std::vector loadLegacyData() override; - - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - //TODO: should be optimized, not all these informations needs to be serialized (same for ccreature) - h & doubledCreatures; - h & objects; - h & expRanks; - h & maxExpPerBattle; - h & expAfterUpgrade; - h & skillLevels; - h & skillRequirements; - h & commanderLevelPremy; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureHandler.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 "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "ConstTransitivePtr.h" +#include "ResourceSet.h" +#include "GameConstants.h" +#include "JsonNode.h" +#include "IHandlerBase.h" +#include "CRandomGenerator.h" +#include "Color.h" +#include "filesystem/ResourcePath.h" + +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class CLegacyConfigParser; +class CCreatureHandler; +class CCreature; +class JsonSerializeFormat; + +class DLL_LINKAGE CCreature : public Creature, public CBonusSystemNode +{ + friend class CCreatureHandler; + std::string modScope; + std::string identifier; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + + CreatureID idNumber; + + FactionID faction = FactionID::NEUTRAL; + ui8 level = 0; // 0 - unknown; 1-7 for "usual" creatures + + //stats that are not handled by bonus system + ui32 fightValue, AIValue, growth, hordeGrowth; + + bool doubleWide = false; + + TResources cost; //cost[res_id] - amount of that resource required to buy creature from dwelling + +public: + ui32 ammMin, ammMax; // initial size of stack of these creatures on adventure map (if not set in editor) + + bool special = true; // Creature is not available normally (war machines, commanders, several unused creatures, etc + + std::set upgrades; // IDs of creatures to which this creature can be upgraded + + AnimationPath animDefName; // creature animation used during battles + + si32 iconIndex = -1; // index of icon in files like twcrport, used in tests now. + /// names of files with appropriate icons. Used only during loading + std::string smallIconName; + std::string largeIconName; + + enum class CreatureQuantityId + { + FEW = 1, + SEVERAL, + PACK, + LOTS, + HORDE, + THRONG, + SWARM, + ZOUNDS, + LEGION + }; + + struct CreatureAnimation + { + struct RayColor { + ColorRGBA start; + ColorRGBA end; + + template void serialize(Handler &h, const int version) + { + h & start & end; + } + }; + + double timeBetweenFidgets, idleAnimationTime, + walkAnimationTime, attackAnimationTime; + int upperRightMissleOffsetX, rightMissleOffsetX, lowerRightMissleOffsetX, + upperRightMissleOffsetY, rightMissleOffsetY, lowerRightMissleOffsetY; + + std::vector missleFrameAngles; + int attackClimaxFrame; + + AnimationPath projectileImageName; + std::vector projectileRay; + //bool projectileSpin; //if true, appropriate projectile is spinning during flight + + template void serialize(Handler &h, const int version) + { + h & timeBetweenFidgets; + h & idleAnimationTime; + h & walkAnimationTime; + h & attackAnimationTime; + h & upperRightMissleOffsetX; + h & rightMissleOffsetX; + h & lowerRightMissleOffsetX; + h & upperRightMissleOffsetY; + h & rightMissleOffsetY; + h & lowerRightMissleOffsetY; + h & missleFrameAngles; + h & attackClimaxFrame; + h & projectileImageName; + h & projectileRay; + } + } animation; + + //sound info + struct CreatureBattleSounds + { + AudioPath attack; + AudioPath defend; + AudioPath killed; // was killed or died + AudioPath move; + AudioPath shoot; // range attack + AudioPath wince; // attacked but did not die + AudioPath startMoving; + AudioPath endMoving; + + template void serialize(Handler &h, const int version) + { + h & attack; + h & defend; + h & killed; + h & move; + h & shoot; + h & wince; + h & startMoving; + h & endMoving; + } + } sounds; + + ArtifactID warMachine; + + std::string getNamePluralTranslated() const override; + std::string getNameSingularTranslated() const override; + + std::string getNamePluralTextID() const override; + std::string getNameSingularTextID() const override; + + FactionID getFaction() const override; + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + CreatureID getId() const override; + virtual const IBonusBearer * getBonusBearer() const override; + + int32_t getAdvMapAmountMin() const override; + int32_t getAdvMapAmountMax() const override; + int32_t getAIValue() const override; + int32_t getFightValue() const override; + int32_t getLevel() const override; + int32_t getGrowth() const override; + int32_t getHorde() const override; + + int32_t getBaseAttack() const override; + int32_t getBaseDefense() const override; + int32_t getBaseDamageMin() const override; + int32_t getBaseDamageMax() const override; + int32_t getBaseHitPoints() const override; + int32_t getBaseSpellPoints() const override; + int32_t getBaseSpeed() const override; + int32_t getBaseShots() const override; + + int32_t getRecruitCost(GameResID resIndex) const override; + TResources getFullRecruitCost() const override; + bool isDoubleWide() const override; //returns true if unit is double wide on battlefield + bool hasUpgrades() const override; + + bool isGood () const; + bool isEvil () const; + si32 maxAmount(const TResources &res) const; //how many creatures can be bought + static CCreature::CreatureQuantityId getQuantityID(const int & quantity); + static std::string getQuantityRangeStringForId(const CCreature::CreatureQuantityId & quantityId); + static int estimateCreatureCount(ui32 countID); //reverse version of above function, returns middle of range + bool isMyUpgrade(const CCreature *anotherCre) const; + + bool valid() const; + + void addBonus(int val, BonusType type, int subtype = -1); + std::string nodeName() const override; + + template + int getRandomAmount(RanGen ranGen) const + { + if(ammMax == ammMin) + return ammMax; + else + return ammMin + (ranGen() % (ammMax - ammMin)); + } + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & cost; + h & upgrades; + h & fightValue; + h & AIValue; + h & growth; + h & hordeGrowth; + h & ammMin; + h & ammMax; + h & level; + h & animDefName; + h & iconIndex; + h & smallIconName; + h & largeIconName; + + h & idNumber; + h & faction; + h & sounds; + h & animation; + + h & doubleWide; + h & special; + h & identifier; + h & modScope; + h & warMachine; + } + + CCreature(); + +private: + static const std::map creatureQuantityRanges; +}; + +class DLL_LINKAGE CCreatureHandler : public CHandlerBase +{ +private: + void loadJsonAnimation(CCreature * creature, const JsonNode & graphics) const; + void loadStackExperience(CCreature * creature, const JsonNode & input) const; + void loadCreatureJson(CCreature * creature, const JsonNode & config) const; + + /// adding abilities from ZCRTRAIT.TXT + void loadBonuses(JsonNode & creature, std::string bonuses) const; + /// load all creatures from H3 files + void load(); + void loadCommanders(); + /// load creature from json structure + void load(std::string creatureID, const JsonNode & node); + /// read cranim.txt file from H3 + void loadAnimationInfo(std::vector & h3Data) const; + /// read one line from cranim.txt + void loadUnitAnimInfo(JsonNode & unit, CLegacyConfigParser & parser) const; + /// parse crexpbon.txt file from H3 + void loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigParser & parser) const; + /// help function for parsing CREXPBON.txt + int stringToNumber(std::string & s) const; + +protected: + const std::vector & getTypeNames() const override; + CCreature * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; + +public: + std::set doubledCreatures; //they get double week + + //stack exp + std::vector > expRanks; // stack experience needed for certain rank, index 0 for other tiers (?) + std::vector maxExpPerBattle; //%, tiers same as above + si8 expAfterUpgrade;//multiplier in % + + //Commanders + BonusList commanderLevelPremy; //bonus values added with each level-up + std::vector< std::vector > skillLevels; //how much of a bonus will be given to commander with every level. SPELL_POWER also gives CASTS and RESISTANCE + std::vector , std::pair > > skillRequirements; // first - Bonus, second - which two skills are needed to use it + + const CCreature * getCreature(const std::string & scope, const std::string & identifier) const; + + CreatureID pickRandomMonster(CRandomGenerator & rand, int tier = -1) const; //tier <1 - CREATURES_PER_TOWN> or -1 for any + + CCreatureHandler(); + ~CCreatureHandler(); + + /// load all stack experience bonuses from H3 files + void loadCrExpBon(CBonusSystemNode & globalEffects); + + /// load all stack modifier bonuses from H3 files. TODO: move this to json + void loadCrExpMod(); + + void afterLoadFinalization() override; + + std::vector loadLegacyData() override; + + std::vector getDefaultAllowed() const override; + + template void serialize(Handler &h, const int version) + { + //TODO: should be optimized, not all these informations needs to be serialized (same for ccreature) + h & doubledCreatures; + h & objects; + h & expRanks; + h & maxExpPerBattle; + h & expAfterUpgrade; + h & skillLevels; + h & skillRequirements; + h & commanderLevelPremy; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 9f8a3b20f..5659636e4 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1,1078 +1,1078 @@ -/* - * CCreatureSet.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 "CCreatureSet.h" - -#include "ArtifactUtils.h" -#include "CConfigHandler.h" -#include "CCreatureHandler.h" -#include "VCMI_Lib.h" -#include "GameSettings.h" -#include "mapObjects/CGHeroInstance.h" -#include "modding/ModScope.h" -#include "IGameCallback.h" -#include "CGeneralTextHandler.h" -#include "spells/CSpellHandler.h" -#include "CHeroHandler.h" -#include "IBonusTypeHandler.h" -#include "serializer/JsonSerializeFormat.h" -#include "NetPacksBase.h" - -#include -#include - -VCMI_LIB_NAMESPACE_BEGIN - - -bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs) -{ - return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting -} - -const CStackInstance & CCreatureSet::operator[](const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return *i->second; - else - throw std::runtime_error("That slot is empty!"); -} - -const CCreature * CCreatureSet::getCreature(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->type; - else - return nullptr; -} - -bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) /*slots 0 to 6 */ -{ - if(!slot.validSlot()) - { - logGlobal->error("Cannot set slot %d", slot.getNum()); - return false; - } - if(!quantity) - { - logGlobal->warn("Using set creature to delete stack?"); - eraseStack(slot); - return true; - } - - if(hasStackAtSlot(slot)) //remove old creature - eraseStack(slot); - - auto * armyObj = castToArmyObj(); - bool isHypotheticArmy = armyObj ? armyObj->isHypothetic() : false; - - putStack(slot, new CStackInstance(type, quantity, isHypotheticArmy)); - return true; -} - -SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ -{ - return getSlotFor(VLC->creh->objects[creature], slotsAmount); -} - -SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const -{ - assert(c && c->valid()); - for(const auto & elem : stacks) - { - assert(elem.second->type->valid()); - if(elem.second->type == c) - { - return elem.first; //if there is already such creature we return its slot id - } - } - return getFreeSlot(slotsAmount); -} - -bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) const -{ - assert(c && c->valid()); - for(const auto & elem : stacks) // elem is const - { - if(elem.first == exclude) // Check slot - continue; - - if(!elem.second || !elem.second->type) // Check creature - continue; - - assert(elem.second->type->valid()); - - if(elem.second->type == c) - return true; - } - return false; -} - -std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount) const -{ - assert(c && c->valid()); - std::vector result; - - for(const auto & elem : stacks) - { - if(elem.first == exclude) - continue; - - if(!elem.second || !elem.second->type || elem.second->type != c) - continue; - - if(elem.second->count == ignoreAmount || elem.second->count < 1) - continue; - - assert(elem.second->type->valid()); - result.push_back(elem.first); - } - return result; -} - -bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const -{ - assert(c && c->valid()); - TQuantity max = 0; - TQuantity min = std::numeric_limits::max(); - - for(const auto & elem : stacks) - { - if(!elem.second || !elem.second->type || elem.second->type != c) - continue; - - const auto count = elem.second->count; - - if(count == ignoreAmount || count < 1) - continue; - - assert(elem.second->type->valid()); - - if(count > max) - max = count; - if(count < min) - min = count; - if(max - min > 1) - return false; - } - return true; -} - -SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const -{ - for(ui32 i=0; i CCreatureSet::getFreeSlots(ui32 slotsAmount) const -{ - std::vector freeSlots; - - for(ui32 i = 0; i < slotsAmount; i++) - { - auto slot = SlotID(i); - - if(!vstd::contains(stacks, slot)) - freeSlots.push_back(slot); - } - return freeSlots; -} - -std::queue CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const -{ - std::queue freeSlots; - - for (ui32 i = 0; i < slotsAmount; i++) - { - auto slot = SlotID(i); - - if(!vstd::contains(stacks, slot)) - freeSlots.push(slot); - } - return freeSlots; -} - -TMapCreatureSlot CCreatureSet::getCreatureMap() const -{ - TMapCreatureSlot creatureMap; - TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp(); - - // https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find - // https://www.cplusplus.com/reference/map/map/key_comp/ - for(const auto & pair : stacks) - { - const auto * creature = pair.second->type; - auto slot = pair.first; - auto lb = creatureMap.lower_bound(creature); - - if(lb != creatureMap.end() && !(keyComp(creature, lb->first))) - continue; - - creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot)); - } - return creatureMap; -} - -TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const -{ - TCreatureQueue creatureQueue; - - for(const auto & pair : stacks) - { - if(pair.first == exclude) - continue; - creatureQueue.push(std::make_pair(pair.second->type, pair.first)); - } - return creatureQueue; -} - -TQuantity CCreatureSet::getStackCount(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->count; - else - return 0; //TODO? consider issuing a warning -} - -TExpType CCreatureSet::getStackExperience(const SlotID & slot) const -{ - auto i = stacks.find(slot); - if (i != stacks.end()) - return i->second->experience; - else - return 0; //TODO? consider issuing a warning -} - -bool CCreatureSet::mergableStacks(std::pair & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */ -{ - //try to match creature to our preferred stack - if(preferable.validSlot() && vstd::contains(stacks, preferable)) - { - const CCreature *cr = stacks.find(preferable)->second->type; - for(const auto & elem : stacks) - { - if(cr == elem.second->type && elem.first != preferable) - { - out.first = preferable; - out.second = elem.first; - return true; - } - } - } - - for(const auto & stack : stacks) - { - for(const auto & elem : stacks) - { - if(stack.second->type == elem.second->type && stack.first != elem.first) - { - out.first = stack.first; - out.second = elem.first; - return true; - } - } - } - return false; -} - -void CCreatureSet::sweep() -{ - for(auto i=stacks.begin(); i!=stacks.end(); ++i) - { - if(!i->second->count) - { - stacks.erase(i); - sweep(); - break; - } - } -} - -void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) -{ - const CCreature *c = VLC->creh->objects[cre]; - - if(!hasStackAtSlot(slot)) - { - setCreature(slot, cre, count); - } - else if(getCreature(slot) == c && allowMerging) //that slot was empty or contained same type creature - { - setStackCount(slot, getStackCount(slot) + count); - } - else - { - logGlobal->error("Failed adding to slot!"); - } -} - -void CCreatureSet::addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging) -{ - assert(stack->valid(true)); - - if(!hasStackAtSlot(slot)) - { - putStack(slot, stack); - } - else if(allowMerging && stack->type == getCreature(slot)) - { - joinStack(slot, stack); - } - else - { - logGlobal->error("Cannot add to slot %d stack %s", slot.getNum(), stack->nodeName()); - } -} - -bool CCreatureSet::validTypes(bool allowUnrandomized) const -{ - for(const auto & elem : stacks) - { - if(!elem.second->valid(allowUnrandomized)) - return false; - } - return true; -} - -bool CCreatureSet::slotEmpty(const SlotID & slot) const -{ - return !hasStackAtSlot(slot); -} - -bool CCreatureSet::needsLastStack() const -{ - return false; -} - -ui64 CCreatureSet::getArmyStrength() const -{ - ui64 ret = 0; - for(const auto & elem : stacks) - ret += elem.second->getPower(); - return ret; -} - -ui64 CCreatureSet::getPower(const SlotID & slot) const -{ - return getStack(slot).getPower(); -} - -std::string CCreatureSet::getRoughAmount(const SlotID & slot, int mode) const -{ - /// Mode represent return string format - /// "Pack" - 0, "A pack of" - 1, "a pack of" - 2 - CCreature::CreatureQuantityId quantity = CCreature::getQuantityID(getStackCount(slot)); - - if((int)quantity) - { - if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) - return CCreature::getQuantityRangeStringForId(quantity); - - return VLC->generaltexth->arraytxt[(174 + mode) + 3*(int)quantity]; - } - return ""; -} - -std::string CCreatureSet::getArmyDescription() const -{ - std::string text; - std::vector guards; - for(const auto & elem : stacks) - { - auto str = boost::str(boost::format("%s %s") % getRoughAmount(elem.first, 2) % getCreature(elem.first)->getNamePluralTranslated()); - guards.push_back(str); - } - if(!guards.empty()) - { - for(int i = 0; i < guards.size(); i++) - { - text += guards[i]; - if(i + 2 < guards.size()) - text += ", "; - else if(i + 2 == guards.size()) - text += VLC->generaltexth->allTexts[237]; - } - } - return text; -} - -int CCreatureSet::stacksCount() const -{ - return static_cast(stacks.size()); -} - -void CCreatureSet::setFormation(bool tight) -{ - if (tight) - formation = EArmyFormation::TIGHT; - else - formation = EArmyFormation::LOOSE; -} - -void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count) -{ - assert(hasStackAtSlot(slot)); - assert(stacks[slot]->count + count > 0); - if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) && count > stacks[slot]->count) - stacks[slot]->experience = static_cast(stacks[slot]->experience * (count / static_cast(stacks[slot]->count))); - stacks[slot]->count = count; - armyChanged(); -} - -void CCreatureSet::giveStackExp(TExpType exp) -{ - for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++) - i->second->giveStackExp(exp); -} -void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp) -{ - assert(hasStackAtSlot(slot)); - stacks[slot]->experience = exp; -} - -void CCreatureSet::clearSlots() -{ - while(!stacks.empty()) - { - eraseStack(stacks.begin()->first); - } -} - -const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const -{ - assert(hasStackAtSlot(slot)); - return *getStackPtr(slot); -} - -const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const -{ - if(hasStackAtSlot(slot)) - return stacks.find(slot)->second; - else return nullptr; -} - -void CCreatureSet::eraseStack(const SlotID & slot) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *toErase = detachStack(slot); - vstd::clear_pointer(toErase); -} - -bool CCreatureSet::contains(const CStackInstance *stack) const -{ - if(!stack) - return false; - - for(const auto & elem : stacks) - if(elem.second == stack) - return true; - - return false; -} - -SlotID CCreatureSet::findStack(const CStackInstance *stack) const -{ - const auto * h = dynamic_cast(this); - if (h && h->commander == stack) - return SlotID::COMMANDER_SLOT_PLACEHOLDER; - - if(!stack) - return SlotID(); - - for(const auto & elem : stacks) - if(elem.second == stack) - return elem.first; - - return SlotID(); -} - -CArmedInstance * CCreatureSet::castToArmyObj() -{ - return dynamic_cast(this); -} - -void CCreatureSet::putStack(const SlotID & slot, CStackInstance * stack) -{ - assert(slot.getNum() < GameConstants::ARMY_SIZE); - assert(!hasStackAtSlot(slot)); - stacks[slot] = stack; - stack->setArmyObj(castToArmyObj()); - armyChanged(); -} - -void CCreatureSet::joinStack(const SlotID & slot, CStackInstance * stack) -{ - [[maybe_unused]] const CCreature *c = getCreature(slot); - assert(c == stack->type); - assert(c); - - //TODO move stuff - changeStackCount(slot, stack->count); - vstd::clear_pointer(stack); -} - -void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd) -{ - setStackCount(slot, getStackCount(slot) + toAdd); -} - -CCreatureSet::~CCreatureSet() -{ - clearSlots(); -} - -void CCreatureSet::setToArmy(CSimpleArmy &src) -{ - clearSlots(); - while(src) - { - auto i = src.army.begin(); - - putStack(i->first, new CStackInstance(i->second.first, i->second.second)); - src.army.erase(i); - } -} - -CStackInstance * CCreatureSet::detachStack(const SlotID & slot) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *ret = stacks[slot]; - - //if(CArmedInstance *armedObj = castToArmyObj()) - if(ret) - { - ret->setArmyObj(nullptr); //detaches from current armyobj - assert(!ret->armyObj); //we failed detaching? - } - - stacks.erase(slot); - armyChanged(); - return ret; -} - -void CCreatureSet::setStackType(const SlotID & slot, const CreatureID & type) -{ - assert(hasStackAtSlot(slot)); - CStackInstance *s = stacks[slot]; - s->setType(type); - armyChanged(); -} - -bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks) const -{ - if(!allowMergingStacks) - { - int freeSlots = stacksCount() - GameConstants::ARMY_SIZE; - std::set cresToAdd; - for(const auto & elem : cs.stacks) - { - SlotID dest = getSlotFor(elem.second->type); - if(!dest.validSlot() || hasStackAtSlot(dest)) - cresToAdd.insert(elem.second->type); - } - return cresToAdd.size() <= freeSlots; - } - else - { - CCreatureSet cres; - SlotID j; - - //get types of creatures that need their own slot - for(const auto & elem : cs.stacks) - if ((j = cres.getSlotFor(elem.second->type)).validSlot()) - cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible - //cres.addToSlot(elem.first, elem.second->type->getId(), 1, true); - for(const auto & elem : stacks) - { - if ((j = cres.getSlotFor(elem.second->type)).validSlot()) - cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible - else - return false; //no place found - } - return true; //all stacks found their slots - } -} - -bool CCreatureSet::hasStackAtSlot(const SlotID & slot) const -{ - return vstd::contains(stacks, slot); -} - -CCreatureSet & CCreatureSet::operator=(const CCreatureSet&cs) -{ - assert(0); - return *this; -} - -void CCreatureSet::armyChanged() -{ - -} - -void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize) -{ - if(handler.saving && stacks.empty()) - return; - - handler.serializeEnum("formation", formation, NArmyFormation::names); - auto a = handler.enterArray(armyFieldName); - - - if(handler.saving) - { - size_t sz = 0; - - for(const auto & p : stacks) - vstd::amax(sz, p.first.getNum()+1); - - if(fixedSize) - vstd::amax(sz, fixedSize.value()); - - a.resize(sz, JsonNode::JsonType::DATA_STRUCT); - - for(const auto & p : stacks) - { - auto s = a.enterStruct(p.first.getNum()); - p.second->serializeJson(handler); - } - } - else - { - for(size_t idx = 0; idx < a.size(); idx++) - { - auto s = a.enterStruct(idx); - - TQuantity amount = 0; - - handler.serializeInt("amount", amount); - - if(amount > 0) - { - auto * new_stack = new CStackInstance(); - new_stack->serializeJson(handler); - putStack(SlotID(static_cast(idx)), new_stack); - } - } - } -} - -CStackInstance::CStackInstance() - : armyObj(_armyObj) -{ - init(); -} - -CStackInstance::CStackInstance(const CreatureID & id, TQuantity Count, bool isHypothetic): - CBonusSystemNode(isHypothetic), armyObj(_armyObj) -{ - init(); - setType(id); - count = Count; -} - -CStackInstance::CStackInstance(const CCreature *cre, TQuantity Count, bool isHypothetic) - : CBonusSystemNode(isHypothetic), armyObj(_armyObj) -{ - init(); - setType(cre); - count = Count; -} - -void CStackInstance::init() -{ - experience = 0; - count = 0; - type = nullptr; - _armyObj = nullptr; - setNodeType(STACK_INSTANCE); -} - -CCreature::CreatureQuantityId CStackInstance::getQuantityID() const -{ - return CCreature::getQuantityID(count); -} - -int CStackInstance::getExpRank() const -{ - if (!VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - return 0; - int tier = type->getLevel(); - if (vstd::iswithin(tier, 1, 7)) - { - for(int i = static_cast(VLC->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic! - { //exp values vary from 1st level to max exp at 11th level - if (experience >= VLC->creh->expRanks[tier][i]) - return ++i; //faster, but confusing - 0 index mean 1st level of experience - } - return 0; - } - else //higher tier - { - for(int i = static_cast(VLC->creh->expRanks[0].size()) - 2; i > -1; --i) - { - if (experience >= VLC->creh->expRanks[0][i]) - return ++i; - } - return 0; - } -} - -int CStackInstance::getLevel() const -{ - return std::max(1, static_cast(type->getLevel())); -} - -void CStackInstance::giveStackExp(TExpType exp) -{ - int level = type->getLevel(); - if (!vstd::iswithin(level, 1, 7)) - level = 0; - - CCreatureHandler * creh = VLC->creh; - ui32 maxExp = creh->expRanks[level].back(); - - vstd::amin(exp, static_cast(maxExp)); //prevent exp overflow due to different types - vstd::amin(exp, (maxExp * creh->maxExpPerBattle[level])/100); - vstd::amin(experience += exp, maxExp); //can't get more exp than this limit -} - -void CStackInstance::setType(const CreatureID & creID) -{ - if(creID.getNum() >= 0 && creID.getNum() < VLC->creh->objects.size()) - setType(VLC->creh->objects[creID]); - else - setType((const CCreature*)nullptr); -} - -void CStackInstance::setType(const CCreature *c) -{ - if(type) - { - detachFrom(const_cast(*type)); - if (type->isMyUpgrade(c) && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); - } - - CStackBasicDescriptor::setType(c); - - if(type) - attachTo(const_cast(*type)); -} -std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const -{ - return VLC->getBth()->bonusToString(bonus, this, description); -} - -ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const -{ - return VLC->getBth()->bonusToGraphics(bonus); -} - -void CStackInstance::setArmyObj(const CArmedInstance * ArmyObj) -{ - if(_armyObj) - detachFrom(const_cast(*_armyObj)); - - _armyObj = ArmyObj; - - if(ArmyObj) - attachTo(const_cast(*_armyObj)); -} - -std::string CStackInstance::getQuantityTXT(bool capitalized) const -{ - CCreature::CreatureQuantityId quantity = getQuantityID(); - - if ((int)quantity) - { - if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) - return CCreature::getQuantityRangeStringForId(quantity); - - return VLC->generaltexth->arraytxt[174 + (int)quantity*3 - 1 - capitalized]; - } - else - return ""; -} - -bool CStackInstance::valid(bool allowUnrandomized) const -{ - if(!randomStack) - { - return (type && type == VLC->creh->objects[type->getId()]); - } - else - return allowUnrandomized; -} - -std::string CStackInstance::nodeName() const -{ - std::ostringstream oss; - oss << "Stack of " << count << " of "; - if(type) - oss << type->getNamePluralTextID(); - else - oss << "[UNDEFINED TYPE]"; - - return oss.str(); -} - -PlayerColor CStackInstance::getOwner() const -{ - return _armyObj ? _armyObj->getOwner() : PlayerColor::NEUTRAL; -} - -void CStackInstance::deserializationFix() -{ - const CArmedInstance *armyBackup = _armyObj; - _armyObj = nullptr; - setArmyObj(armyBackup); - artDeserializationFix(this); -} - -CreatureID CStackInstance::getCreatureID() const -{ - if(type) - return type->getId(); - else - return CreatureID::NONE; -} - -std::string CStackInstance::getName() const -{ - return (count > 1) ? type->getNamePluralTranslated() : type->getNameSingularTranslated(); -} - -ui64 CStackInstance::getPower() const -{ - assert(type); - return type->getAIValue() * count; -} - -ArtBearer::ArtBearer CStackInstance::bearerType() const -{ - return ArtBearer::CREATURE; -} - -CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) -{ - assert(!getArt(pos)); - assert(art->artType->canBePutAt(this, pos)); - - attachTo(*art); - return CArtifactSet::putArtifact(pos, art); -} - -void CStackInstance::removeArtifact(ArtifactPosition pos) -{ - assert(getArt(pos)); - - detachFrom(*getArt(pos)); - CArtifactSet::removeArtifact(pos); -} - -void CStackInstance::serializeJson(JsonSerializeFormat & handler) -{ - //todo: artifacts - CStackBasicDescriptor::serializeJson(handler);//must be first - - if(handler.saving) - { - if(randomStack) - { - int level = randomStack->level; - int upgrade = randomStack->upgrade; - - handler.serializeInt("level", level, 0); - handler.serializeInt("upgraded", upgrade, 0); - } - } - else - { - //type set by CStackBasicDescriptor::serializeJson - if(type == nullptr) - { - uint8_t level = 0; - uint8_t upgrade = 0; - - handler.serializeInt("level", level, 0); - handler.serializeInt("upgrade", upgrade, 0); - - randomStack = RandomStackInfo{ level, upgrade }; - } - } -} - -FactionID CStackInstance::getFaction() const -{ - if(type) - return type->getFaction(); - - return FactionID::NEUTRAL; -} - -const IBonusBearer* CStackInstance::getBonusBearer() const -{ - return this; -} - -CCommanderInstance::CCommanderInstance() -{ - init(); -} - -CCommanderInstance::CCommanderInstance(const CreatureID & id): name("Commando") -{ - init(); - setType(id); - //TODO - parse them -} - -void CCommanderInstance::init() -{ - alive = true; - experience = 0; - level = 1; - count = 1; - type = nullptr; - _armyObj = nullptr; - setNodeType (CBonusSystemNode::COMMANDER); - secondarySkills.resize (ECommander::SPELL_POWER + 1); -} - -void CCommanderInstance::setAlive (bool Alive) -{ - //TODO: helm of immortality - alive = Alive; - if (!alive) - { - removeBonusesRecursive(Bonus::UntilCommanderKilled); - } -} - -void CCommanderInstance::giveStackExp (TExpType exp) -{ - if (alive) - experience += exp; -} - -int CCommanderInstance::getExpRank() const -{ - return VLC->heroh->level (experience); -} - -int CCommanderInstance::getLevel() const -{ - return std::max (1, getExpRank()); -} - -void CCommanderInstance::levelUp () -{ - level++; - for(const auto & bonus : VLC->creh->commanderLevelPremy) - { //grant all regular level-up bonuses - accumulateBonus(bonus); - } -} - -ArtBearer::ArtBearer CCommanderInstance::bearerType() const -{ - return ArtBearer::COMMANDER; -} - -bool CCommanderInstance::gainsLevel() const -{ - return experience >= static_cast(VLC->heroh->reqExp(level + 1)); -} - -//This constructor should be placed here to avoid side effects -CStackBasicDescriptor::CStackBasicDescriptor() = default; - -CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): - type(VLC->creh->objects[id]), - count(Count) -{ -} - -CStackBasicDescriptor::CStackBasicDescriptor(const CCreature *c, TQuantity Count) - : type(c), count(Count) -{ -} - -const Creature * CStackBasicDescriptor::getType() const -{ - return type; -} - -TQuantity CStackBasicDescriptor::getCount() const -{ - return count; -} - - -void CStackBasicDescriptor::setType(const CCreature * c) -{ - type = c; -} - -bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r) -{ - return (!l.type && !r.type) - || (l.type && r.type - && l.type->getId() == r.type->getId() - && l.count == r.count); -} - -void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("amount", count); - - if(handler.saving) - { - if(type) - { - std::string typeName = type->getJsonKey(); - handler.serializeString("type", typeName); - } - } - else - { - std::string typeName; - handler.serializeString("type", typeName); - if(!typeName.empty()) - setType(VLC->creh->getCreature(ModScope::scopeMap(), typeName)); - } -} - -void CSimpleArmy::clearSlots() -{ - army.clear(); -} - -CSimpleArmy::operator bool() const -{ - return !army.empty(); -} - -bool CSimpleArmy::setCreature(SlotID slot, CreatureID cre, TQuantity count) -{ - assert(!vstd::contains(army, slot)); - army[slot] = std::make_pair(cre, count); - return true; -} - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureSet.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 "CCreatureSet.h" + +#include "ArtifactUtils.h" +#include "CConfigHandler.h" +#include "CCreatureHandler.h" +#include "VCMI_Lib.h" +#include "GameSettings.h" +#include "mapObjects/CGHeroInstance.h" +#include "modding/ModScope.h" +#include "IGameCallback.h" +#include "CGeneralTextHandler.h" +#include "spells/CSpellHandler.h" +#include "CHeroHandler.h" +#include "IBonusTypeHandler.h" +#include "serializer/JsonSerializeFormat.h" +#include "NetPacksBase.h" + +#include +#include + +VCMI_LIB_NAMESPACE_BEGIN + + +bool CreatureSlotComparer::operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs) +{ + return lhs.first->getAIValue() < rhs.first->getAIValue(); // Descendant order sorting +} + +const CStackInstance & CCreatureSet::operator[](const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return *i->second; + else + throw std::runtime_error("That slot is empty!"); +} + +const CCreature * CCreatureSet::getCreature(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->type; + else + return nullptr; +} + +bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) /*slots 0 to 6 */ +{ + if(!slot.validSlot()) + { + logGlobal->error("Cannot set slot %d", slot.getNum()); + return false; + } + if(!quantity) + { + logGlobal->warn("Using set creature to delete stack?"); + eraseStack(slot); + return true; + } + + if(hasStackAtSlot(slot)) //remove old creature + eraseStack(slot); + + auto * armyObj = castToArmyObj(); + bool isHypotheticArmy = armyObj ? armyObj->isHypothetic() : false; + + putStack(slot, new CStackInstance(type, quantity, isHypotheticArmy)); + return true; +} + +SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ +{ + return getSlotFor(VLC->creh->objects[creature], slotsAmount); +} + +SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const +{ + assert(c && c->valid()); + for(const auto & elem : stacks) + { + assert(elem.second->type->valid()); + if(elem.second->type == c) + { + return elem.first; //if there is already such creature we return its slot id + } + } + return getFreeSlot(slotsAmount); +} + +bool CCreatureSet::hasCreatureSlots(const CCreature * c, const SlotID & exclude) const +{ + assert(c && c->valid()); + for(const auto & elem : stacks) // elem is const + { + if(elem.first == exclude) // Check slot + continue; + + if(!elem.second || !elem.second->type) // Check creature + continue; + + assert(elem.second->type->valid()); + + if(elem.second->type == c) + return true; + } + return false; +} + +std::vector CCreatureSet::getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + std::vector result; + + for(const auto & elem : stacks) + { + if(elem.first == exclude) + continue; + + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + if(elem.second->count == ignoreAmount || elem.second->count < 1) + continue; + + assert(elem.second->type->valid()); + result.push_back(elem.first); + } + return result; +} + +bool CCreatureSet::isCreatureBalanced(const CCreature * c, TQuantity ignoreAmount) const +{ + assert(c && c->valid()); + TQuantity max = 0; + TQuantity min = std::numeric_limits::max(); + + for(const auto & elem : stacks) + { + if(!elem.second || !elem.second->type || elem.second->type != c) + continue; + + const auto count = elem.second->count; + + if(count == ignoreAmount || count < 1) + continue; + + assert(elem.second->type->valid()); + + if(count > max) + max = count; + if(count < min) + min = count; + if(max - min > 1) + return false; + } + return true; +} + +SlotID CCreatureSet::getFreeSlot(ui32 slotsAmount) const +{ + for(ui32 i=0; i CCreatureSet::getFreeSlots(ui32 slotsAmount) const +{ + std::vector freeSlots; + + for(ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push_back(slot); + } + return freeSlots; +} + +std::queue CCreatureSet::getFreeSlotsQueue(ui32 slotsAmount) const +{ + std::queue freeSlots; + + for (ui32 i = 0; i < slotsAmount; i++) + { + auto slot = SlotID(i); + + if(!vstd::contains(stacks, slot)) + freeSlots.push(slot); + } + return freeSlots; +} + +TMapCreatureSlot CCreatureSet::getCreatureMap() const +{ + TMapCreatureSlot creatureMap; + TMapCreatureSlot::key_compare keyComp = creatureMap.key_comp(); + + // https://stackoverflow.com/questions/97050/stdmap-insert-or-stdmap-find + // https://www.cplusplus.com/reference/map/map/key_comp/ + for(const auto & pair : stacks) + { + const auto * creature = pair.second->type; + auto slot = pair.first; + auto lb = creatureMap.lower_bound(creature); + + if(lb != creatureMap.end() && !(keyComp(creature, lb->first))) + continue; + + creatureMap.insert(lb, TMapCreatureSlot::value_type(creature, slot)); + } + return creatureMap; +} + +TCreatureQueue CCreatureSet::getCreatureQueue(const SlotID & exclude) const +{ + TCreatureQueue creatureQueue; + + for(const auto & pair : stacks) + { + if(pair.first == exclude) + continue; + creatureQueue.push(std::make_pair(pair.second->type, pair.first)); + } + return creatureQueue; +} + +TQuantity CCreatureSet::getStackCount(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->count; + else + return 0; //TODO? consider issuing a warning +} + +TExpType CCreatureSet::getStackExperience(const SlotID & slot) const +{ + auto i = stacks.find(slot); + if (i != stacks.end()) + return i->second->experience; + else + return 0; //TODO? consider issuing a warning +} + +bool CCreatureSet::mergableStacks(std::pair & out, const SlotID & preferable) const /*looks for two same stacks, returns slot positions */ +{ + //try to match creature to our preferred stack + if(preferable.validSlot() && vstd::contains(stacks, preferable)) + { + const CCreature *cr = stacks.find(preferable)->second->type; + for(const auto & elem : stacks) + { + if(cr == elem.second->type && elem.first != preferable) + { + out.first = preferable; + out.second = elem.first; + return true; + } + } + } + + for(const auto & stack : stacks) + { + for(const auto & elem : stacks) + { + if(stack.second->type == elem.second->type && stack.first != elem.first) + { + out.first = stack.first; + out.second = elem.first; + return true; + } + } + } + return false; +} + +void CCreatureSet::sweep() +{ + for(auto i=stacks.begin(); i!=stacks.end(); ++i) + { + if(!i->second->count) + { + stacks.erase(i); + sweep(); + break; + } + } +} + +void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) +{ + const CCreature *c = VLC->creh->objects[cre]; + + if(!hasStackAtSlot(slot)) + { + setCreature(slot, cre, count); + } + else if(getCreature(slot) == c && allowMerging) //that slot was empty or contained same type creature + { + setStackCount(slot, getStackCount(slot) + count); + } + else + { + logGlobal->error("Failed adding to slot!"); + } +} + +void CCreatureSet::addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging) +{ + assert(stack->valid(true)); + + if(!hasStackAtSlot(slot)) + { + putStack(slot, stack); + } + else if(allowMerging && stack->type == getCreature(slot)) + { + joinStack(slot, stack); + } + else + { + logGlobal->error("Cannot add to slot %d stack %s", slot.getNum(), stack->nodeName()); + } +} + +bool CCreatureSet::validTypes(bool allowUnrandomized) const +{ + for(const auto & elem : stacks) + { + if(!elem.second->valid(allowUnrandomized)) + return false; + } + return true; +} + +bool CCreatureSet::slotEmpty(const SlotID & slot) const +{ + return !hasStackAtSlot(slot); +} + +bool CCreatureSet::needsLastStack() const +{ + return false; +} + +ui64 CCreatureSet::getArmyStrength() const +{ + ui64 ret = 0; + for(const auto & elem : stacks) + ret += elem.second->getPower(); + return ret; +} + +ui64 CCreatureSet::getPower(const SlotID & slot) const +{ + return getStack(slot).getPower(); +} + +std::string CCreatureSet::getRoughAmount(const SlotID & slot, int mode) const +{ + /// Mode represent return string format + /// "Pack" - 0, "A pack of" - 1, "a pack of" - 2 + CCreature::CreatureQuantityId quantity = CCreature::getQuantityID(getStackCount(slot)); + + if((int)quantity) + { + if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) + return CCreature::getQuantityRangeStringForId(quantity); + + return VLC->generaltexth->arraytxt[(174 + mode) + 3*(int)quantity]; + } + return ""; +} + +std::string CCreatureSet::getArmyDescription() const +{ + std::string text; + std::vector guards; + for(const auto & elem : stacks) + { + auto str = boost::str(boost::format("%s %s") % getRoughAmount(elem.first, 2) % getCreature(elem.first)->getNamePluralTranslated()); + guards.push_back(str); + } + if(!guards.empty()) + { + for(int i = 0; i < guards.size(); i++) + { + text += guards[i]; + if(i + 2 < guards.size()) + text += ", "; + else if(i + 2 == guards.size()) + text += VLC->generaltexth->allTexts[237]; + } + } + return text; +} + +int CCreatureSet::stacksCount() const +{ + return static_cast(stacks.size()); +} + +void CCreatureSet::setFormation(bool tight) +{ + if (tight) + formation = EArmyFormation::TIGHT; + else + formation = EArmyFormation::LOOSE; +} + +void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count) +{ + assert(hasStackAtSlot(slot)); + assert(stacks[slot]->count + count > 0); + if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE) && count > stacks[slot]->count) + stacks[slot]->experience = static_cast(stacks[slot]->experience * (count / static_cast(stacks[slot]->count))); + stacks[slot]->count = count; + armyChanged(); +} + +void CCreatureSet::giveStackExp(TExpType exp) +{ + for(TSlots::const_iterator i = stacks.begin(); i != stacks.end(); i++) + i->second->giveStackExp(exp); +} +void CCreatureSet::setStackExp(const SlotID & slot, TExpType exp) +{ + assert(hasStackAtSlot(slot)); + stacks[slot]->experience = exp; +} + +void CCreatureSet::clearSlots() +{ + while(!stacks.empty()) + { + eraseStack(stacks.begin()->first); + } +} + +const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const +{ + assert(hasStackAtSlot(slot)); + return *getStackPtr(slot); +} + +const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const +{ + if(hasStackAtSlot(slot)) + return stacks.find(slot)->second; + else return nullptr; +} + +void CCreatureSet::eraseStack(const SlotID & slot) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *toErase = detachStack(slot); + vstd::clear_pointer(toErase); +} + +bool CCreatureSet::contains(const CStackInstance *stack) const +{ + if(!stack) + return false; + + for(const auto & elem : stacks) + if(elem.second == stack) + return true; + + return false; +} + +SlotID CCreatureSet::findStack(const CStackInstance *stack) const +{ + const auto * h = dynamic_cast(this); + if (h && h->commander == stack) + return SlotID::COMMANDER_SLOT_PLACEHOLDER; + + if(!stack) + return SlotID(); + + for(const auto & elem : stacks) + if(elem.second == stack) + return elem.first; + + return SlotID(); +} + +CArmedInstance * CCreatureSet::castToArmyObj() +{ + return dynamic_cast(this); +} + +void CCreatureSet::putStack(const SlotID & slot, CStackInstance * stack) +{ + assert(slot.getNum() < GameConstants::ARMY_SIZE); + assert(!hasStackAtSlot(slot)); + stacks[slot] = stack; + stack->setArmyObj(castToArmyObj()); + armyChanged(); +} + +void CCreatureSet::joinStack(const SlotID & slot, CStackInstance * stack) +{ + [[maybe_unused]] const CCreature *c = getCreature(slot); + assert(c == stack->type); + assert(c); + + //TODO move stuff + changeStackCount(slot, stack->count); + vstd::clear_pointer(stack); +} + +void CCreatureSet::changeStackCount(const SlotID & slot, TQuantity toAdd) +{ + setStackCount(slot, getStackCount(slot) + toAdd); +} + +CCreatureSet::~CCreatureSet() +{ + clearSlots(); +} + +void CCreatureSet::setToArmy(CSimpleArmy &src) +{ + clearSlots(); + while(src) + { + auto i = src.army.begin(); + + putStack(i->first, new CStackInstance(i->second.first, i->second.second)); + src.army.erase(i); + } +} + +CStackInstance * CCreatureSet::detachStack(const SlotID & slot) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *ret = stacks[slot]; + + //if(CArmedInstance *armedObj = castToArmyObj()) + if(ret) + { + ret->setArmyObj(nullptr); //detaches from current armyobj + assert(!ret->armyObj); //we failed detaching? + } + + stacks.erase(slot); + armyChanged(); + return ret; +} + +void CCreatureSet::setStackType(const SlotID & slot, const CreatureID & type) +{ + assert(hasStackAtSlot(slot)); + CStackInstance *s = stacks[slot]; + s->setType(type); + armyChanged(); +} + +bool CCreatureSet::canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks) const +{ + if(!allowMergingStacks) + { + int freeSlots = stacksCount() - GameConstants::ARMY_SIZE; + std::set cresToAdd; + for(const auto & elem : cs.stacks) + { + SlotID dest = getSlotFor(elem.second->type); + if(!dest.validSlot() || hasStackAtSlot(dest)) + cresToAdd.insert(elem.second->type); + } + return cresToAdd.size() <= freeSlots; + } + else + { + CCreatureSet cres; + SlotID j; + + //get types of creatures that need their own slot + for(const auto & elem : cs.stacks) + if ((j = cres.getSlotFor(elem.second->type)).validSlot()) + cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible + //cres.addToSlot(elem.first, elem.second->type->getId(), 1, true); + for(const auto & elem : stacks) + { + if ((j = cres.getSlotFor(elem.second->type)).validSlot()) + cres.addToSlot(j, elem.second->type->getId(), 1, true); //merge if possible + else + return false; //no place found + } + return true; //all stacks found their slots + } +} + +bool CCreatureSet::hasStackAtSlot(const SlotID & slot) const +{ + return vstd::contains(stacks, slot); +} + +CCreatureSet & CCreatureSet::operator=(const CCreatureSet&cs) +{ + assert(0); + return *this; +} + +void CCreatureSet::armyChanged() +{ + +} + +void CCreatureSet::serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize) +{ + if(handler.saving && stacks.empty()) + return; + + handler.serializeEnum("formation", formation, NArmyFormation::names); + auto a = handler.enterArray(armyFieldName); + + + if(handler.saving) + { + size_t sz = 0; + + for(const auto & p : stacks) + vstd::amax(sz, p.first.getNum()+1); + + if(fixedSize) + vstd::amax(sz, fixedSize.value()); + + a.resize(sz, JsonNode::JsonType::DATA_STRUCT); + + for(const auto & p : stacks) + { + auto s = a.enterStruct(p.first.getNum()); + p.second->serializeJson(handler); + } + } + else + { + for(size_t idx = 0; idx < a.size(); idx++) + { + auto s = a.enterStruct(idx); + + TQuantity amount = 0; + + handler.serializeInt("amount", amount); + + if(amount > 0) + { + auto * new_stack = new CStackInstance(); + new_stack->serializeJson(handler); + putStack(SlotID(static_cast(idx)), new_stack); + } + } + } +} + +CStackInstance::CStackInstance() + : armyObj(_armyObj) +{ + init(); +} + +CStackInstance::CStackInstance(const CreatureID & id, TQuantity Count, bool isHypothetic): + CBonusSystemNode(isHypothetic), armyObj(_armyObj) +{ + init(); + setType(id); + count = Count; +} + +CStackInstance::CStackInstance(const CCreature *cre, TQuantity Count, bool isHypothetic) + : CBonusSystemNode(isHypothetic), armyObj(_armyObj) +{ + init(); + setType(cre); + count = Count; +} + +void CStackInstance::init() +{ + experience = 0; + count = 0; + type = nullptr; + _armyObj = nullptr; + setNodeType(STACK_INSTANCE); +} + +CCreature::CreatureQuantityId CStackInstance::getQuantityID() const +{ + return CCreature::getQuantityID(count); +} + +int CStackInstance::getExpRank() const +{ + if (!VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + return 0; + int tier = type->getLevel(); + if (vstd::iswithin(tier, 1, 7)) + { + for(int i = static_cast(VLC->creh->expRanks[tier].size()) - 2; i > -1; --i) //sic! + { //exp values vary from 1st level to max exp at 11th level + if (experience >= VLC->creh->expRanks[tier][i]) + return ++i; //faster, but confusing - 0 index mean 1st level of experience + } + return 0; + } + else //higher tier + { + for(int i = static_cast(VLC->creh->expRanks[0].size()) - 2; i > -1; --i) + { + if (experience >= VLC->creh->expRanks[0][i]) + return ++i; + } + return 0; + } +} + +int CStackInstance::getLevel() const +{ + return std::max(1, static_cast(type->getLevel())); +} + +void CStackInstance::giveStackExp(TExpType exp) +{ + int level = type->getLevel(); + if (!vstd::iswithin(level, 1, 7)) + level = 0; + + CCreatureHandler * creh = VLC->creh; + ui32 maxExp = creh->expRanks[level].back(); + + vstd::amin(exp, static_cast(maxExp)); //prevent exp overflow due to different types + vstd::amin(exp, (maxExp * creh->maxExpPerBattle[level])/100); + vstd::amin(experience += exp, maxExp); //can't get more exp than this limit +} + +void CStackInstance::setType(const CreatureID & creID) +{ + if(creID.getNum() >= 0 && creID.getNum() < VLC->creh->objects.size()) + setType(VLC->creh->objects[creID]); + else + setType((const CCreature*)nullptr); +} + +void CStackInstance::setType(const CCreature *c) +{ + if(type) + { + detachFrom(const_cast(*type)); + if (type->isMyUpgrade(c) && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + experience = static_cast(experience * VLC->creh->expAfterUpgrade / 100.0); + } + + CStackBasicDescriptor::setType(c); + + if(type) + attachTo(const_cast(*type)); +} +std::string CStackInstance::bonusToString(const std::shared_ptr& bonus, bool description) const +{ + return VLC->getBth()->bonusToString(bonus, this, description); +} + +ImagePath CStackInstance::bonusToGraphics(const std::shared_ptr & bonus) const +{ + return VLC->getBth()->bonusToGraphics(bonus); +} + +void CStackInstance::setArmyObj(const CArmedInstance * ArmyObj) +{ + if(_armyObj) + detachFrom(const_cast(*_armyObj)); + + _armyObj = ArmyObj; + + if(ArmyObj) + attachTo(const_cast(*_armyObj)); +} + +std::string CStackInstance::getQuantityTXT(bool capitalized) const +{ + CCreature::CreatureQuantityId quantity = getQuantityID(); + + if ((int)quantity) + { + if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) + return CCreature::getQuantityRangeStringForId(quantity); + + return VLC->generaltexth->arraytxt[174 + (int)quantity*3 - 1 - capitalized]; + } + else + return ""; +} + +bool CStackInstance::valid(bool allowUnrandomized) const +{ + if(!randomStack) + { + return (type && type == VLC->creh->objects[type->getId()]); + } + else + return allowUnrandomized; +} + +std::string CStackInstance::nodeName() const +{ + std::ostringstream oss; + oss << "Stack of " << count << " of "; + if(type) + oss << type->getNamePluralTextID(); + else + oss << "[UNDEFINED TYPE]"; + + return oss.str(); +} + +PlayerColor CStackInstance::getOwner() const +{ + return _armyObj ? _armyObj->getOwner() : PlayerColor::NEUTRAL; +} + +void CStackInstance::deserializationFix() +{ + const CArmedInstance *armyBackup = _armyObj; + _armyObj = nullptr; + setArmyObj(armyBackup); + artDeserializationFix(this); +} + +CreatureID CStackInstance::getCreatureID() const +{ + if(type) + return type->getId(); + else + return CreatureID::NONE; +} + +std::string CStackInstance::getName() const +{ + return (count > 1) ? type->getNamePluralTranslated() : type->getNameSingularTranslated(); +} + +ui64 CStackInstance::getPower() const +{ + assert(type); + return type->getAIValue() * count; +} + +ArtBearer::ArtBearer CStackInstance::bearerType() const +{ + return ArtBearer::CREATURE; +} + +CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) +{ + assert(!getArt(pos)); + assert(art->artType->canBePutAt(this, pos)); + + attachTo(*art); + return CArtifactSet::putArtifact(pos, art); +} + +void CStackInstance::removeArtifact(ArtifactPosition pos) +{ + assert(getArt(pos)); + + detachFrom(*getArt(pos)); + CArtifactSet::removeArtifact(pos); +} + +void CStackInstance::serializeJson(JsonSerializeFormat & handler) +{ + //todo: artifacts + CStackBasicDescriptor::serializeJson(handler);//must be first + + if(handler.saving) + { + if(randomStack) + { + int level = randomStack->level; + int upgrade = randomStack->upgrade; + + handler.serializeInt("level", level, 0); + handler.serializeInt("upgraded", upgrade, 0); + } + } + else + { + //type set by CStackBasicDescriptor::serializeJson + if(type == nullptr) + { + uint8_t level = 0; + uint8_t upgrade = 0; + + handler.serializeInt("level", level, 0); + handler.serializeInt("upgrade", upgrade, 0); + + randomStack = RandomStackInfo{ level, upgrade }; + } + } +} + +FactionID CStackInstance::getFaction() const +{ + if(type) + return type->getFaction(); + + return FactionID::NEUTRAL; +} + +const IBonusBearer* CStackInstance::getBonusBearer() const +{ + return this; +} + +CCommanderInstance::CCommanderInstance() +{ + init(); +} + +CCommanderInstance::CCommanderInstance(const CreatureID & id): name("Commando") +{ + init(); + setType(id); + //TODO - parse them +} + +void CCommanderInstance::init() +{ + alive = true; + experience = 0; + level = 1; + count = 1; + type = nullptr; + _armyObj = nullptr; + setNodeType (CBonusSystemNode::COMMANDER); + secondarySkills.resize (ECommander::SPELL_POWER + 1); +} + +void CCommanderInstance::setAlive (bool Alive) +{ + //TODO: helm of immortality + alive = Alive; + if (!alive) + { + removeBonusesRecursive(Bonus::UntilCommanderKilled); + } +} + +void CCommanderInstance::giveStackExp (TExpType exp) +{ + if (alive) + experience += exp; +} + +int CCommanderInstance::getExpRank() const +{ + return VLC->heroh->level (experience); +} + +int CCommanderInstance::getLevel() const +{ + return std::max (1, getExpRank()); +} + +void CCommanderInstance::levelUp () +{ + level++; + for(const auto & bonus : VLC->creh->commanderLevelPremy) + { //grant all regular level-up bonuses + accumulateBonus(bonus); + } +} + +ArtBearer::ArtBearer CCommanderInstance::bearerType() const +{ + return ArtBearer::COMMANDER; +} + +bool CCommanderInstance::gainsLevel() const +{ + return experience >= static_cast(VLC->heroh->reqExp(level + 1)); +} + +//This constructor should be placed here to avoid side effects +CStackBasicDescriptor::CStackBasicDescriptor() = default; + +CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): + type(VLC->creh->objects[id]), + count(Count) +{ +} + +CStackBasicDescriptor::CStackBasicDescriptor(const CCreature *c, TQuantity Count) + : type(c), count(Count) +{ +} + +const Creature * CStackBasicDescriptor::getType() const +{ + return type; +} + +TQuantity CStackBasicDescriptor::getCount() const +{ + return count; +} + + +void CStackBasicDescriptor::setType(const CCreature * c) +{ + type = c; +} + +bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r) +{ + return (!l.type && !r.type) + || (l.type && r.type + && l.type->getId() == r.type->getId() + && l.count == r.count); +} + +void CStackBasicDescriptor::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("amount", count); + + if(handler.saving) + { + if(type) + { + std::string typeName = type->getJsonKey(); + handler.serializeString("type", typeName); + } + } + else + { + std::string typeName; + handler.serializeString("type", typeName); + if(!typeName.empty()) + setType(VLC->creh->getCreature(ModScope::scopeMap(), typeName)); + } +} + +void CSimpleArmy::clearSlots() +{ + army.clear(); +} + +CSimpleArmy::operator bool() const +{ + return !army.empty(); +} + +bool CSimpleArmy::setCreature(SlotID slot, CreatureID cre, TQuantity count) +{ + assert(!vstd::contains(army, slot)); + army[slot] = std::make_pair(cre, count); + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index abc2d09d1..634e62ba4 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -1,303 +1,303 @@ -/* - * CCreatureSet.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 "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "GameConstants.h" -#include "CArtHandler.h" -#include "CArtifactInstance.h" -#include "CCreatureHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; -class CCreature; -class CGHeroInstance; -class CArmedInstance; -class CCreatureArtifactSet; -class JsonSerializeFormat; - -class DLL_LINKAGE CStackBasicDescriptor -{ -public: - const CCreature *type = nullptr; - TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army - - CStackBasicDescriptor(); - CStackBasicDescriptor(const CreatureID & id, TQuantity Count); - CStackBasicDescriptor(const CCreature *c, TQuantity Count); - virtual ~CStackBasicDescriptor() = default; - - const Creature * getType() const; - TQuantity getCount() const; - - virtual void setType(const CCreature * c); - - friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - auto idNumber = type ? type->getId() : CreatureID(CreatureID::NONE); - h & idNumber; - } - else - { - CreatureID idNumber; - h & idNumber; - if(idNumber != CreatureID::NONE) - setType(dynamic_cast(VLC->creatures()->getById(idNumber))); - else - type = nullptr; - } - h & count; - } - - void serializeJson(JsonSerializeFormat & handler); -}; - -class DLL_LINKAGE CStackInstance : public CBonusSystemNode, public CStackBasicDescriptor, public CArtifactSet, public ACreature -{ -protected: - const CArmedInstance *_armyObj; //stack must be part of some army, army must be part of some object - -public: - struct RandomStackInfo - { - uint8_t level; - uint8_t upgrade; - }; - // helper variable used during loading map, when object (hero or town) have creatures that must have same alignment. - std::optional randomStack; - - const CArmedInstance * const & armyObj; //stack must be part of some army, army must be part of some object - TExpType experience;//commander needs same amount of exp as hero - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & _armyObj; - h & experience; - BONUS_TREE_DESERIALIZATION_FIX - } - - void serializeJson(JsonSerializeFormat & handler); - - //overrides CBonusSystemNode - std::string bonusToString(const std::shared_ptr& bonus, bool description) const override; // how would bonus description look for this particular type of node - ImagePath bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others - - //IConstBonusProvider - const IBonusBearer* getBonusBearer() const override; - //INativeTerrainProvider - FactionID getFaction() const override; - - virtual ui64 getPower() const; - CCreature::CreatureQuantityId getQuantityID() const; - std::string getQuantityTXT(bool capitalized = true) const; - virtual int getExpRank() const; - virtual int getLevel() const; //different for regular stack and commander - CreatureID getCreatureID() const; //-1 if not available - std::string getName() const; //plural or singular - virtual void init(); - CStackInstance(); - CStackInstance(const CreatureID & id, TQuantity count, bool isHypothetic = false); - CStackInstance(const CCreature *cre, TQuantity count, bool isHypothetic = false); - virtual ~CStackInstance() = default; - - void setType(const CreatureID & creID); - void setType(const CCreature * c) override; - void setArmyObj(const CArmedInstance *ArmyObj); - virtual void giveStackExp(TExpType exp); - bool valid(bool allowUnrandomized) const; - ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet - void removeArtifact(ArtifactPosition pos) override; - ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - virtual std::string nodeName() const override; //from CBonusSystemnode - void deserializationFix(); - PlayerColor getOwner() const override; -}; - -class DLL_LINKAGE CCommanderInstance : public CStackInstance -{ -public: - //TODO: what if Commander is not a part of creature set? - - //commander class is determined by its base creature - ui8 alive; //maybe change to bool when breaking save compatibility? - ui8 level; //required only to count callbacks - std::string name; // each Commander has different name - std::vector secondarySkills; //ID -> level - std::set specialSkills; - //std::vector arts; - void init() override; - CCommanderInstance(); - CCommanderInstance(const CreatureID & id); - void setAlive (bool alive); - void giveStackExp (TExpType exp) override; - void levelUp (); - - bool gainsLevel() const; //true if commander has lower level than should upon his experience - ui64 getPower() const override {return 0;}; - int getExpRank() const override; - int getLevel() const override; - ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & alive; - h & level; - h & name; - h & secondarySkills; - h & specialSkills; - } -}; - -using TSlots = std::map; -using TSimpleSlots = std::map>; - -using TPairCreatureSlot = std::pair; -using TMapCreatureSlot = std::map; - -struct DLL_LINKAGE CreatureSlotComparer -{ - bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs); -}; - -using TCreatureQueue = std::priority_queue, CreatureSlotComparer>; - -class IArmyDescriptor -{ -public: - virtual void clearSlots() = 0; - virtual bool setCreature(SlotID slot, CreatureID cre, TQuantity count) = 0; -}; - -//simplified version of CCreatureSet -class DLL_LINKAGE CSimpleArmy : public IArmyDescriptor -{ -public: - TSimpleSlots army; - void clearSlots() override; - bool setCreature(SlotID slot, CreatureID cre, TQuantity count) override; - operator bool() const; - - template void serialize(Handler &h, const int version) - { - h & army; - } -}; - -enum class EArmyFormation : uint8_t -{ - LOOSE, - TIGHT -}; - -namespace NArmyFormation -{ - static const std::vector names{ "wide", "tight" }; -} - -class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures -{ - CCreatureSet(const CCreatureSet &) = delete; - CCreatureSet &operator=(const CCreatureSet&); -public: - - - TSlots stacks; //slots[slot_id]->> pair(creature_id,creature_quantity) - EArmyFormation formation = EArmyFormation::LOOSE; //0 - wide, 1 - tight - - CCreatureSet() = default; //Should be here to avoid compile errors - virtual ~CCreatureSet(); - virtual void armyChanged(); - - const CStackInstance & operator[](const SlotID & slot) const; - - const TSlots &Slots() const {return stacks;} - - void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature - void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature - void clearSlots() override; - void setFormation(bool tight); - CArmedInstance *castToArmyObj(); - - //basic operations - void putStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the army, slot must be empty - void setStackCount(const SlotID & slot, TQuantity count); //stack must exist! - CStackInstance * detachStack(const SlotID & slot); //removes stack from army but doesn't destroy it (so it can be moved somewhere else or safely deleted) - void setStackType(const SlotID & slot, const CreatureID & type); - void giveStackExp(TExpType exp); - void setStackExp(const SlotID & slot, TExpType exp); - - //derivative - void eraseStack(const SlotID & slot); //slot must be occupied - void joinStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the existing stack of the same type - void changeStackCount(const SlotID & slot, TQuantity toAdd); //stack must exist! - bool setCreature (SlotID slot, CreatureID type, TQuantity quantity) override; //replaces creature in stack; slots 0 to 6, if quantity=0 erases stack - void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. - - const CStackInstance & getStack(const SlotID & slot) const; //stack must exist - const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr - const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; - int getStackCount(const SlotID & slot) const; - TExpType getStackExperience(const SlotID & slot) const; - SlotID findStack(const CStackInstance *stack) const; //-1 if none - SlotID getSlotFor(const CreatureID & creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available - SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available - bool hasCreatureSlots(const CCreature * c, const SlotID & exclude) const; - std::vector getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount = -1) const; - bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots - - SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot - std::vector getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; - std::queue getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; - - TMapCreatureSlot getCreatureMap() const; - TCreatureQueue getCreatureQueue(const SlotID & exclude) const; - - bool mergableStacks(std::pair & out, const SlotID & preferable = SlotID()) const; //looks for two same stacks, returns slot positions; - bool validTypes(bool allowUnrandomized = false) const; //checks if all types of creatures are set properly - bool slotEmpty(const SlotID & slot) const; - int stacksCount() const; - virtual bool needsLastStack() const; //true if last stack cannot be taken - ui64 getArmyStrength() const; //sum of AI values of creatures - ui64 getPower(const SlotID & slot) const; //value of specific stack - std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack - std::string getArmyDescription() const; - bool hasStackAtSlot(const SlotID & slot) const; - - bool contains(const CStackInstance *stack) const; - bool canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks = true) const; - - template void serialize(Handler &h, const int version) - { - h & stacks; - h & formation; - } - - void serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize = std::nullopt); - - operator bool() const - { - return !stacks.empty(); - } - void sweep(); -}; - -VCMI_LIB_NAMESPACE_END +/* + * CCreatureSet.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 "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "GameConstants.h" +#include "CArtHandler.h" +#include "CArtifactInstance.h" +#include "CCreatureHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +class CCreature; +class CGHeroInstance; +class CArmedInstance; +class CCreatureArtifactSet; +class JsonSerializeFormat; + +class DLL_LINKAGE CStackBasicDescriptor +{ +public: + const CCreature *type = nullptr; + TQuantity count = -1; //exact quantity or quantity ID from CCreature::getQuantityID when getting info about enemy army + + CStackBasicDescriptor(); + CStackBasicDescriptor(const CreatureID & id, TQuantity Count); + CStackBasicDescriptor(const CCreature *c, TQuantity Count); + virtual ~CStackBasicDescriptor() = default; + + const Creature * getType() const; + TQuantity getCount() const; + + virtual void setType(const CCreature * c); + + friend bool operator== (const CStackBasicDescriptor & l, const CStackBasicDescriptor & r); + + template void serialize(Handler &h, const int version) + { + if(h.saving) + { + auto idNumber = type ? type->getId() : CreatureID(CreatureID::NONE); + h & idNumber; + } + else + { + CreatureID idNumber; + h & idNumber; + if(idNumber != CreatureID::NONE) + setType(dynamic_cast(VLC->creatures()->getById(idNumber))); + else + type = nullptr; + } + h & count; + } + + void serializeJson(JsonSerializeFormat & handler); +}; + +class DLL_LINKAGE CStackInstance : public CBonusSystemNode, public CStackBasicDescriptor, public CArtifactSet, public ACreature +{ +protected: + const CArmedInstance *_armyObj; //stack must be part of some army, army must be part of some object + +public: + struct RandomStackInfo + { + uint8_t level; + uint8_t upgrade; + }; + // helper variable used during loading map, when object (hero or town) have creatures that must have same alignment. + std::optional randomStack; + + const CArmedInstance * const & armyObj; //stack must be part of some army, army must be part of some object + TExpType experience;//commander needs same amount of exp as hero + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & static_cast(*this); + h & static_cast(*this); + h & _armyObj; + h & experience; + BONUS_TREE_DESERIALIZATION_FIX + } + + void serializeJson(JsonSerializeFormat & handler); + + //overrides CBonusSystemNode + std::string bonusToString(const std::shared_ptr& bonus, bool description) const override; // how would bonus description look for this particular type of node + ImagePath bonusToGraphics(const std::shared_ptr & bonus) const; //file name of graphics from StackSkills , in future possibly others + + //IConstBonusProvider + const IBonusBearer* getBonusBearer() const override; + //INativeTerrainProvider + FactionID getFaction() const override; + + virtual ui64 getPower() const; + CCreature::CreatureQuantityId getQuantityID() const; + std::string getQuantityTXT(bool capitalized = true) const; + virtual int getExpRank() const; + virtual int getLevel() const; //different for regular stack and commander + CreatureID getCreatureID() const; //-1 if not available + std::string getName() const; //plural or singular + virtual void init(); + CStackInstance(); + CStackInstance(const CreatureID & id, TQuantity count, bool isHypothetic = false); + CStackInstance(const CCreature *cre, TQuantity count, bool isHypothetic = false); + virtual ~CStackInstance() = default; + + void setType(const CreatureID & creID); + void setType(const CCreature * c) override; + void setArmyObj(const CArmedInstance *ArmyObj); + virtual void giveStackExp(TExpType exp); + bool valid(bool allowUnrandomized) const; + ArtPlacementMap putArtifact(ArtifactPosition pos, CArtifactInstance * art) override;//from CArtifactSet + void removeArtifact(ArtifactPosition pos) override; + ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet + virtual std::string nodeName() const override; //from CBonusSystemnode + void deserializationFix(); + PlayerColor getOwner() const override; +}; + +class DLL_LINKAGE CCommanderInstance : public CStackInstance +{ +public: + //TODO: what if Commander is not a part of creature set? + + //commander class is determined by its base creature + ui8 alive; //maybe change to bool when breaking save compatibility? + ui8 level; //required only to count callbacks + std::string name; // each Commander has different name + std::vector secondarySkills; //ID -> level + std::set specialSkills; + //std::vector arts; + void init() override; + CCommanderInstance(); + CCommanderInstance(const CreatureID & id); + void setAlive (bool alive); + void giveStackExp (TExpType exp) override; + void levelUp (); + + bool gainsLevel() const; //true if commander has lower level than should upon his experience + ui64 getPower() const override {return 0;}; + int getExpRank() const override; + int getLevel() const override; + ArtBearer::ArtBearer bearerType() const override; //from CArtifactSet + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & alive; + h & level; + h & name; + h & secondarySkills; + h & specialSkills; + } +}; + +using TSlots = std::map; +using TSimpleSlots = std::map>; + +using TPairCreatureSlot = std::pair; +using TMapCreatureSlot = std::map; + +struct DLL_LINKAGE CreatureSlotComparer +{ + bool operator()(const TPairCreatureSlot & lhs, const TPairCreatureSlot & rhs); +}; + +using TCreatureQueue = std::priority_queue, CreatureSlotComparer>; + +class IArmyDescriptor +{ +public: + virtual void clearSlots() = 0; + virtual bool setCreature(SlotID slot, CreatureID cre, TQuantity count) = 0; +}; + +//simplified version of CCreatureSet +class DLL_LINKAGE CSimpleArmy : public IArmyDescriptor +{ +public: + TSimpleSlots army; + void clearSlots() override; + bool setCreature(SlotID slot, CreatureID cre, TQuantity count) override; + operator bool() const; + + template void serialize(Handler &h, const int version) + { + h & army; + } +}; + +enum class EArmyFormation : uint8_t +{ + LOOSE, + TIGHT +}; + +namespace NArmyFormation +{ + static const std::vector names{ "wide", "tight" }; +} + +class DLL_LINKAGE CCreatureSet : public IArmyDescriptor //seven combined creatures +{ + CCreatureSet(const CCreatureSet &) = delete; + CCreatureSet &operator=(const CCreatureSet&); +public: + + + TSlots stacks; //slots[slot_id]->> pair(creature_id,creature_quantity) + EArmyFormation formation = EArmyFormation::LOOSE; //0 - wide, 1 - tight + + CCreatureSet() = default; //Should be here to avoid compile errors + virtual ~CCreatureSet(); + virtual void armyChanged(); + + const CStackInstance & operator[](const SlotID & slot) const; + + const TSlots &Slots() const {return stacks;} + + void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature + void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature + void clearSlots() override; + void setFormation(bool tight); + CArmedInstance *castToArmyObj(); + + //basic operations + void putStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the army, slot must be empty + void setStackCount(const SlotID & slot, TQuantity count); //stack must exist! + CStackInstance * detachStack(const SlotID & slot); //removes stack from army but doesn't destroy it (so it can be moved somewhere else or safely deleted) + void setStackType(const SlotID & slot, const CreatureID & type); + void giveStackExp(TExpType exp); + void setStackExp(const SlotID & slot, TExpType exp); + + //derivative + void eraseStack(const SlotID & slot); //slot must be occupied + void joinStack(const SlotID & slot, CStackInstance * stack); //adds new stack to the existing stack of the same type + void changeStackCount(const SlotID & slot, TQuantity toAdd); //stack must exist! + bool setCreature (SlotID slot, CreatureID type, TQuantity quantity) override; //replaces creature in stack; slots 0 to 6, if quantity=0 erases stack + void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. + + const CStackInstance & getStack(const SlotID & slot) const; //stack must exist + const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr + const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; + int getStackCount(const SlotID & slot) const; + TExpType getStackExperience(const SlotID & slot) const; + SlotID findStack(const CStackInstance *stack) const; //-1 if none + SlotID getSlotFor(const CreatureID & creature, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available + SlotID getSlotFor(const CCreature *c, ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns -1 if no slot available + bool hasCreatureSlots(const CCreature * c, const SlotID & exclude) const; + std::vector getCreatureSlots(const CCreature * c, const SlotID & exclude, TQuantity ignoreAmount = -1) const; + bool isCreatureBalanced(const CCreature* c, TQuantity ignoreAmount = 1) const; // Check if the creature is evenly distributed across slots + + SlotID getFreeSlot(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; //returns first free slot + std::vector getFreeSlots(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + std::queue getFreeSlotsQueue(ui32 slotsAmount = GameConstants::ARMY_SIZE) const; + + TMapCreatureSlot getCreatureMap() const; + TCreatureQueue getCreatureQueue(const SlotID & exclude) const; + + bool mergableStacks(std::pair & out, const SlotID & preferable = SlotID()) const; //looks for two same stacks, returns slot positions; + bool validTypes(bool allowUnrandomized = false) const; //checks if all types of creatures are set properly + bool slotEmpty(const SlotID & slot) const; + int stacksCount() const; + virtual bool needsLastStack() const; //true if last stack cannot be taken + ui64 getArmyStrength() const; //sum of AI values of creatures + ui64 getPower(const SlotID & slot) const; //value of specific stack + std::string getRoughAmount(const SlotID & slot, int mode = 0) const; //rough size of specific stack + std::string getArmyDescription() const; + bool hasStackAtSlot(const SlotID & slot) const; + + bool contains(const CStackInstance *stack) const; + bool canBeMergedWith(const CCreatureSet &cs, bool allowMergingStacks = true) const; + + template void serialize(Handler &h, const int version) + { + h & stacks; + h & formation; + } + + void serializeJson(JsonSerializeFormat & handler, const std::string & armyFieldName, const std::optional fixedSize = std::nullopt); + + operator bool() const + { + return !stacks.empty(); + } + void sweep(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInterface.cpp b/lib/CGameInterface.cpp index c2e49742d..619acd62b 100644 --- a/lib/CGameInterface.cpp +++ b/lib/CGameInterface.cpp @@ -1,272 +1,272 @@ -/* - * CGameInterface.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 "CGameInterface.h" - -#include "CStack.h" -#include "VCMIDirs.h" - -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" - -#ifdef STATIC_AI -# include "AI/VCAI/VCAI.h" -# include "AI/Nullkiller/AIGateway.h" -# include "AI/BattleAI/BattleAI.h" -# include "AI/StupidAI/StupidAI.h" -# include "AI/EmptyAI/CEmptyAI.h" -#else -# ifdef VCMI_WINDOWS -# include //for .dll libs -# else -# include -# endif // VCMI_WINDOWS -#endif // STATIC_AI - -VCMI_LIB_NAMESPACE_BEGIN - -template -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ -#ifdef STATIC_AI - // android currently doesn't support loading libs dynamically, so the access to the known libraries - // is possible only via specializations of this template - throw std::runtime_error("Could not resolve ai library " + libpath.generic_string()); -#else - using TGetAIFun = void (*)(std::shared_ptr &); - using TGetNameFun = void (*)(char *); - - char temp[150]; - - TGetAIFun getAI = nullptr; - TGetNameFun getName = nullptr; - -#ifdef VCMI_WINDOWS -#ifdef __MINGW32__ -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-function-type" -#endif - HMODULE dll = LoadLibraryW(libpath.c_str()); - if (dll) - { - getName = reinterpret_cast(GetProcAddress(dll, "GetAiName")); - getAI = reinterpret_cast(GetProcAddress(dll, methodName.c_str())); - } -#ifdef __MINGW32__ -#pragma GCC diagnostic pop -#endif -#else // !VCMI_WINDOWS - void *dll = dlopen(libpath.string().c_str(), RTLD_LOCAL | RTLD_LAZY); - if (dll) - { - getName = reinterpret_cast(dlsym(dll, "GetAiName")); - getAI = reinterpret_cast(dlsym(dll, methodName.c_str())); - } -#endif // VCMI_WINDOWS - - if (!dll) - { - logGlobal->error("Cannot open dynamic library (%s). Throwing...", libpath.string()); - throw std::runtime_error("Cannot open dynamic library"); - } - else if(!getName || !getAI) - { - logGlobal->error("%s does not export method %s", libpath.string(), methodName); -#ifdef VCMI_WINDOWS - FreeLibrary(dll); -#else - dlclose(dll); -#endif - throw std::runtime_error("Cannot find method " + methodName); - } - - getName(temp); - logGlobal->info("Loaded %s", temp); - - std::shared_ptr ret; - getAI(ret); - if(!ret) - logGlobal->error("Cannot get AI!"); - - return ret; -#endif // STATIC_AI -} - -#ifdef STATIC_AI - -template<> -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ - if(libpath.stem() == "libNullkiller") { - return std::make_shared(); - } - else{ - return std::make_shared(); - } -} - -template<> -std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) -{ - if(libpath.stem() == "libBattleAI") - return std::make_shared(); - else if(libpath.stem() == "libStupidAI") - return std::make_shared(); - return std::make_shared(); -} - -#endif // STATIC_AI - -template -std::shared_ptr createAnyAI(const std::string & dllname, const std::string & methodName) -{ - logGlobal->info("Opening %s", dllname); - - const boost::filesystem::path filePath = VCMIDirs::get().fullLibraryPath("AI", dllname); - auto ret = createAny(filePath, methodName); - ret->dllName = dllname; - return ret; -} - -std::shared_ptr CDynLibHandler::getNewAI(const std::string & dllname) -{ - return createAnyAI(dllname, "GetNewAI"); -} - -std::shared_ptr CDynLibHandler::getNewBattleAI(const std::string & dllname) -{ - return createAnyAI(dllname, "GetNewBattleAI"); -} - -#if SCRIPTING_ENABLED -std::shared_ptr CDynLibHandler::getNewScriptingModule(const boost::filesystem::path & dllname) -{ - return createAny(dllname, "GetNewModule"); -} -#endif - -CGlobalAI::CGlobalAI() -{ - human = false; -} - -void CAdventureAI::battleNewRound(const BattleID & battleID) -{ - battleAI->battleNewRound(battleID); -} - -void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) -{ - battleAI->battleCatapultAttacked(battleID, ca); -} - -void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, - const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) -{ - assert(!battleAI); - assert(cbc); - battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); - battleAI->initBattleInterface(env, cbc); - battleAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); -} - -void CAdventureAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) -{ - battleAI->battleStacksAttacked(battleID, bsa, ranged); -} - -void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & action) -{ - battleAI->actionStarted(battleID, action); -} - -void CAdventureAI::battleNewRoundFirst(const BattleID & battleID) -{ - battleAI->battleNewRoundFirst(battleID); -} - -void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action) -{ - battleAI->actionFinished(battleID, action); -} - -void CAdventureAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) -{ - battleAI->battleStacksEffectsSet(battleID, sse); -} - -void CAdventureAI::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) -{ - battleAI->battleObstaclesChanged(battleID, obstacles); -} - -void CAdventureAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) -{ - battleAI->battleStackMoved(battleID, stack, dest, distance, teleport); -} - -void CAdventureAI::battleAttack(const BattleID & battleID, const BattleAttack * ba) -{ - battleAI->battleAttack(battleID, ba); -} - -void CAdventureAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) -{ - battleAI->battleSpellCast(battleID, sc); -} - -void CAdventureAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) -{ - battleAI->battleEnd(battleID, br, queryID); - battleAI.reset(); -} - -void CAdventureAI::battleUnitsChanged(const BattleID & battleID, const std::vector & units) -{ - battleAI->battleUnitsChanged(battleID, units); -} - -void CAdventureAI::activeStack(const BattleID & battleID, const CStack * stack) -{ - battleAI->activeStack(battleID, stack); -} - -void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) -{ - battleAI->yourTacticPhase(battleID, distance); -} - -void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - bool hasBattleAI = static_cast(battleAI); - h & hasBattleAI; - if(hasBattleAI) - { - h & battleAI->dllName; - } -} - -void CAdventureAI::loadGame(BinaryDeserializer & h, const int version) /*loading */ -{ - LOG_TRACE_PARAMS(logAi, "version '%i'", version); - bool hasBattleAI = false; - h & hasBattleAI; - if(hasBattleAI) - { - std::string dllName; - h & dllName; - battleAI = CDynLibHandler::getNewBattleAI(dllName); - assert(cbc); //it should have been set by the one who new'ed us - battleAI->initBattleInterface(env, cbc); - } -} - -VCMI_LIB_NAMESPACE_END +/* + * CGameInterface.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 "CGameInterface.h" + +#include "CStack.h" +#include "VCMIDirs.h" + +#include "serializer/BinaryDeserializer.h" +#include "serializer/BinarySerializer.h" + +#ifdef STATIC_AI +# include "AI/VCAI/VCAI.h" +# include "AI/Nullkiller/AIGateway.h" +# include "AI/BattleAI/BattleAI.h" +# include "AI/StupidAI/StupidAI.h" +# include "AI/EmptyAI/CEmptyAI.h" +#else +# ifdef VCMI_WINDOWS +# include //for .dll libs +# else +# include +# endif // VCMI_WINDOWS +#endif // STATIC_AI + +VCMI_LIB_NAMESPACE_BEGIN + +template +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ +#ifdef STATIC_AI + // android currently doesn't support loading libs dynamically, so the access to the known libraries + // is possible only via specializations of this template + throw std::runtime_error("Could not resolve ai library " + libpath.generic_string()); +#else + using TGetAIFun = void (*)(std::shared_ptr &); + using TGetNameFun = void (*)(char *); + + char temp[150]; + + TGetAIFun getAI = nullptr; + TGetNameFun getName = nullptr; + +#ifdef VCMI_WINDOWS +#ifdef __MINGW32__ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wcast-function-type" +#endif + HMODULE dll = LoadLibraryW(libpath.c_str()); + if (dll) + { + getName = reinterpret_cast(GetProcAddress(dll, "GetAiName")); + getAI = reinterpret_cast(GetProcAddress(dll, methodName.c_str())); + } +#ifdef __MINGW32__ +#pragma GCC diagnostic pop +#endif +#else // !VCMI_WINDOWS + void *dll = dlopen(libpath.string().c_str(), RTLD_LOCAL | RTLD_LAZY); + if (dll) + { + getName = reinterpret_cast(dlsym(dll, "GetAiName")); + getAI = reinterpret_cast(dlsym(dll, methodName.c_str())); + } +#endif // VCMI_WINDOWS + + if (!dll) + { + logGlobal->error("Cannot open dynamic library (%s). Throwing...", libpath.string()); + throw std::runtime_error("Cannot open dynamic library"); + } + else if(!getName || !getAI) + { + logGlobal->error("%s does not export method %s", libpath.string(), methodName); +#ifdef VCMI_WINDOWS + FreeLibrary(dll); +#else + dlclose(dll); +#endif + throw std::runtime_error("Cannot find method " + methodName); + } + + getName(temp); + logGlobal->info("Loaded %s", temp); + + std::shared_ptr ret; + getAI(ret); + if(!ret) + logGlobal->error("Cannot get AI!"); + + return ret; +#endif // STATIC_AI +} + +#ifdef STATIC_AI + +template<> +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ + if(libpath.stem() == "libNullkiller") { + return std::make_shared(); + } + else{ + return std::make_shared(); + } +} + +template<> +std::shared_ptr createAny(const boost::filesystem::path & libpath, const std::string & methodName) +{ + if(libpath.stem() == "libBattleAI") + return std::make_shared(); + else if(libpath.stem() == "libStupidAI") + return std::make_shared(); + return std::make_shared(); +} + +#endif // STATIC_AI + +template +std::shared_ptr createAnyAI(const std::string & dllname, const std::string & methodName) +{ + logGlobal->info("Opening %s", dllname); + + const boost::filesystem::path filePath = VCMIDirs::get().fullLibraryPath("AI", dllname); + auto ret = createAny(filePath, methodName); + ret->dllName = dllname; + return ret; +} + +std::shared_ptr CDynLibHandler::getNewAI(const std::string & dllname) +{ + return createAnyAI(dllname, "GetNewAI"); +} + +std::shared_ptr CDynLibHandler::getNewBattleAI(const std::string & dllname) +{ + return createAnyAI(dllname, "GetNewBattleAI"); +} + +#if SCRIPTING_ENABLED +std::shared_ptr CDynLibHandler::getNewScriptingModule(const boost::filesystem::path & dllname) +{ + return createAny(dllname, "GetNewModule"); +} +#endif + +CGlobalAI::CGlobalAI() +{ + human = false; +} + +void CAdventureAI::battleNewRound(const BattleID & battleID) +{ + battleAI->battleNewRound(battleID); +} + +void CAdventureAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) +{ + battleAI->battleCatapultAttacked(battleID, ca); +} + +void CAdventureAI::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, + const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) +{ + assert(!battleAI); + assert(cbc); + battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName()); + battleAI->initBattleInterface(env, cbc); + battleAI->battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed); +} + +void CAdventureAI::battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) +{ + battleAI->battleStacksAttacked(battleID, bsa, ranged); +} + +void CAdventureAI::actionStarted(const BattleID & battleID, const BattleAction & action) +{ + battleAI->actionStarted(battleID, action); +} + +void CAdventureAI::battleNewRoundFirst(const BattleID & battleID) +{ + battleAI->battleNewRoundFirst(battleID); +} + +void CAdventureAI::actionFinished(const BattleID & battleID, const BattleAction & action) +{ + battleAI->actionFinished(battleID, action); +} + +void CAdventureAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) +{ + battleAI->battleStacksEffectsSet(battleID, sse); +} + +void CAdventureAI::battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) +{ + battleAI->battleObstaclesChanged(battleID, obstacles); +} + +void CAdventureAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) +{ + battleAI->battleStackMoved(battleID, stack, dest, distance, teleport); +} + +void CAdventureAI::battleAttack(const BattleID & battleID, const BattleAttack * ba) +{ + battleAI->battleAttack(battleID, ba); +} + +void CAdventureAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast * sc) +{ + battleAI->battleSpellCast(battleID, sc); +} + +void CAdventureAI::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) +{ + battleAI->battleEnd(battleID, br, queryID); + battleAI.reset(); +} + +void CAdventureAI::battleUnitsChanged(const BattleID & battleID, const std::vector & units) +{ + battleAI->battleUnitsChanged(battleID, units); +} + +void CAdventureAI::activeStack(const BattleID & battleID, const CStack * stack) +{ + battleAI->activeStack(battleID, stack); +} + +void CAdventureAI::yourTacticPhase(const BattleID & battleID, int distance) +{ + battleAI->yourTacticPhase(battleID, distance); +} + +void CAdventureAI::saveGame(BinarySerializer & h, const int version) /*saving */ +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + bool hasBattleAI = static_cast(battleAI); + h & hasBattleAI; + if(hasBattleAI) + { + h & battleAI->dllName; + } +} + +void CAdventureAI::loadGame(BinaryDeserializer & h, const int version) /*loading */ +{ + LOG_TRACE_PARAMS(logAi, "version '%i'", version); + bool hasBattleAI = false; + h & hasBattleAI; + if(hasBattleAI) + { + std::string dllName; + h & dllName; + battleAI = CDynLibHandler::getNewBattleAI(dllName); + assert(cbc); //it should have been set by the one who new'ed us + battleAI->initBattleInterface(env, cbc); + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInterface.h b/lib/CGameInterface.h index cb849755a..6aabc1dcc 100644 --- a/lib/CGameInterface.h +++ b/lib/CGameInterface.h @@ -1,169 +1,169 @@ -/* - * CGameInterface.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 "battle/AutocombatPreferences.h" -#include "IGameEventsReceiver.h" - -#include "spells/ViewSpellInt.h" - -class CBattleCallback; -class CCallback; - -VCMI_LIB_NAMESPACE_BEGIN - -using boost::logic::tribool; - -class Environment; - -class ICallback; -class CGlobalAI; -struct Component; -struct TryMoveHero; -class CGHeroInstance; -class CGTownInstance; -class CGObjectInstance; -class CGBlackMarket; -class CGDwelling; -class CCreatureSet; -class CArmedInstance; -class IShipyard; -class IMarket; -class BattleAction; -struct BattleResult; -struct BattleAttack; -struct BattleStackAttacked; -struct BattleSpellCast; -struct SetStackEffect; -struct Bonus; -struct PackageApplied; -struct SetObjectProperty; -struct CatapultAttack; -struct StackLocation; -class CStackInstance; -class CCommanderInstance; -class CStack; -class CCreature; -class CLoadFile; -class CSaveFile; -class BinaryDeserializer; -class BinarySerializer; -class BattleStateInfo; -struct ArtifactLocation; -class BattleStateInfoForRetreat; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class Module; -} -#endif - -using TTeleportExitsList = std::vector>; - -class DLL_LINKAGE CBattleGameInterface : public IBattleEventsReceiver -{ -public: - bool human; - PlayerColor playerID; - std::string dllName; - - virtual ~CBattleGameInterface() {}; - virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB){}; - virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences){}; - - //battle call-ins - virtual void activeStack(const BattleID & battleID, const CStack * stack)=0; //called when it's turn of that stack - virtual void yourTacticPhase(const BattleID & battleID, int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function -}; - -/// Central class for managing human player / AI interface logic -class DLL_LINKAGE CGameInterface : public CBattleGameInterface, public IGameEventsReceiver -{ -public: - virtual ~CGameInterface() = default; - virtual void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB){}; - virtual void yourTurn(QueryID askID){}; //called AFTER playerStartsTurn(player) - - //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id - virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; - virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID)=0; - - // Show a dialog, player must take decision. If selection then he has to choose between one of given components, - // if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called - // with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. - virtual void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) = 0; - - // all stacks operations between these objects become allowed, interface has to call onEnd when done - virtual void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) = 0; - virtual void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; - virtual void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) = 0; - virtual void finish(){}; //if for some reason we want to end - - virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; - - virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; - - virtual void saveGame(BinarySerializer & h, const int version) = 0; - virtual void loadGame(BinaryDeserializer & h, const int version) = 0; -}; - -class DLL_LINKAGE CDynLibHandler -{ -public: - static std::shared_ptr getNewAI(const std::string & dllname); - static std::shared_ptr getNewBattleAI(const std::string & dllname); -#if SCRIPTING_ENABLED - static std::shared_ptr getNewScriptingModule(const boost::filesystem::path & dllname); -#endif -}; - -class DLL_LINKAGE CGlobalAI : public CGameInterface // AI class (to derivate) -{ -public: - std::shared_ptr env; - CGlobalAI(); -}; - -//class to be inherited by adventure-only AIs, it cedes battle actions to given battle-AI -class DLL_LINKAGE CAdventureAI : public CGlobalAI -{ -public: - CAdventureAI() = default; - - std::shared_ptr battleAI; - std::shared_ptr cbc; - - virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used - - //battle interface - virtual void activeStack(const BattleID & battleID, const CStack * stack) override; - virtual void yourTacticPhase(const BattleID & battleID, int distance) override; - - virtual void battleNewRound(const BattleID & battleID) override; - virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; - virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; - virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; - virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; - virtual void battleNewRoundFirst(const BattleID & battleID) override; - virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; - virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; - virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; - virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; - virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; - virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; - virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; - virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; - - virtual void saveGame(BinarySerializer & h, const int version) override; - virtual void loadGame(BinaryDeserializer & h, const int version) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CGameInterface.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 "battle/AutocombatPreferences.h" +#include "IGameEventsReceiver.h" + +#include "spells/ViewSpellInt.h" + +class CBattleCallback; +class CCallback; + +VCMI_LIB_NAMESPACE_BEGIN + +using boost::logic::tribool; + +class Environment; + +class ICallback; +class CGlobalAI; +struct Component; +struct TryMoveHero; +class CGHeroInstance; +class CGTownInstance; +class CGObjectInstance; +class CGBlackMarket; +class CGDwelling; +class CCreatureSet; +class CArmedInstance; +class IShipyard; +class IMarket; +class BattleAction; +struct BattleResult; +struct BattleAttack; +struct BattleStackAttacked; +struct BattleSpellCast; +struct SetStackEffect; +struct Bonus; +struct PackageApplied; +struct SetObjectProperty; +struct CatapultAttack; +struct StackLocation; +class CStackInstance; +class CCommanderInstance; +class CStack; +class CCreature; +class CLoadFile; +class CSaveFile; +class BinaryDeserializer; +class BinarySerializer; +class BattleStateInfo; +struct ArtifactLocation; +class BattleStateInfoForRetreat; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class Module; +} +#endif + +using TTeleportExitsList = std::vector>; + +class DLL_LINKAGE CBattleGameInterface : public IBattleEventsReceiver +{ +public: + bool human; + PlayerColor playerID; + std::string dllName; + + virtual ~CBattleGameInterface() {}; + virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB){}; + virtual void initBattleInterface(std::shared_ptr ENV, std::shared_ptr CB, AutocombatPreferences autocombatPreferences){}; + + //battle call-ins + virtual void activeStack(const BattleID & battleID, const CStack * stack)=0; //called when it's turn of that stack + virtual void yourTacticPhase(const BattleID & battleID, int distance)=0; //called when interface has opportunity to use Tactics skill -> use cb->battleMakeTacticAction from this function +}; + +/// Central class for managing human player / AI interface logic +class DLL_LINKAGE CGameInterface : public CBattleGameInterface, public IGameEventsReceiver +{ +public: + virtual ~CGameInterface() = default; + virtual void initGameInterface(std::shared_ptr ENV, std::shared_ptr CB){}; + virtual void yourTurn(QueryID askID){}; //called AFTER playerStartsTurn(player) + + //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id + virtual void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector &skills, QueryID queryID)=0; + virtual void commanderGotLevel (const CCommanderInstance * commander, std::vector skills, QueryID queryID)=0; + + // Show a dialog, player must take decision. If selection then he has to choose between one of given components, + // if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called + // with number of selected component (1 - n) or 0 for cancel (if allowed) and askID. + virtual void showBlockingDialog(const std::string &text, const std::vector &components, QueryID askID, const int soundID, bool selection, bool cancel) = 0; + + // all stacks operations between these objects become allowed, interface has to call onEnd when done + virtual void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) = 0; + virtual void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) = 0; + virtual void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector & objects) = 0; + virtual void finish(){}; //if for some reason we want to end + + virtual void showWorldViewEx(const std::vector & objectPositions, bool showTerrain){}; + + virtual std::optional makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) = 0; + + virtual void saveGame(BinarySerializer & h, const int version) = 0; + virtual void loadGame(BinaryDeserializer & h, const int version) = 0; +}; + +class DLL_LINKAGE CDynLibHandler +{ +public: + static std::shared_ptr getNewAI(const std::string & dllname); + static std::shared_ptr getNewBattleAI(const std::string & dllname); +#if SCRIPTING_ENABLED + static std::shared_ptr getNewScriptingModule(const boost::filesystem::path & dllname); +#endif +}; + +class DLL_LINKAGE CGlobalAI : public CGameInterface // AI class (to derivate) +{ +public: + std::shared_ptr env; + CGlobalAI(); +}; + +//class to be inherited by adventure-only AIs, it cedes battle actions to given battle-AI +class DLL_LINKAGE CAdventureAI : public CGlobalAI +{ +public: + CAdventureAI() = default; + + std::shared_ptr battleAI; + std::shared_ptr cbc; + + virtual std::string getBattleAIName() const = 0; //has to return name of the battle AI to be used + + //battle interface + virtual void activeStack(const BattleID & battleID, const CStack * stack) override; + virtual void yourTacticPhase(const BattleID & battleID, int distance) override; + + virtual void battleNewRound(const BattleID & battleID) override; + virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged) override; + virtual void actionStarted(const BattleID & battleID, const BattleAction &action) override; + virtual void battleNewRoundFirst(const BattleID & battleID) override; + virtual void actionFinished(const BattleID & battleID, const BattleAction &action) override; + virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override; + virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles) override; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport) override; + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override; + virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units) override; + + virtual void saveGame(BinarySerializer & h, const int version) override; + virtual void loadGame(BinaryDeserializer & h, const int version) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.cpp b/lib/CGeneralTextHandler.cpp index f68d6b3d4..2e4f400dd 100644 --- a/lib/CGeneralTextHandler.cpp +++ b/lib/CGeneralTextHandler.cpp @@ -1,705 +1,705 @@ -/* - * CGeneralTextHandler.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 "CGeneralTextHandler.h" - -#include "filesystem/Filesystem.h" -#include "serializer/JsonSerializeFormat.h" -#include "CConfigHandler.h" -#include "GameSettings.h" -#include "mapObjects/CQuest.h" -#include "modding/CModHandler.h" -#include "VCMI_Lib.h" -#include "Languages.h" -#include "TextOperations.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Detects language and encoding of H3 text files based on matching against pregenerated footprints of H3 file -void CGeneralTextHandler::detectInstallParameters() -{ - using LanguageFootprint = std::array; - - static const std::array knownFootprints = - { { - { { 0.1602, 0.0000, 0.0357, 0.0054, 0.0038, 0.0017, 0.0077, 0.0214, 0.0000, 0.0000, 0.1264, 0.1947, 0.2012, 0.1406, 0.0480, 0.0532 } }, - { { 0.0559, 0.0000, 0.1983, 0.0051, 0.0222, 0.0183, 0.4596, 0.2405, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000 } }, - { { 0.0493, 0.0000, 0.1926, 0.0047, 0.0230, 0.0121, 0.4133, 0.2780, 0.0002, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0259, 0.0008 } }, - { { 0.0534, 0.0000, 0.1705, 0.0047, 0.0418, 0.0208, 0.4775, 0.2191, 0.0001, 0.0000, 0.0000, 0.0000, 0.0000, 0.0005, 0.0036, 0.0080 } }, - { { 0.0534, 0.0000, 0.1701, 0.0067, 0.0157, 0.0133, 0.4328, 0.2540, 0.0001, 0.0043, 0.0000, 0.0244, 0.0000, 0.0000, 0.0181, 0.0071 } }, - { { 0.0548, 0.0000, 0.1744, 0.0061, 0.0031, 0.0009, 0.0046, 0.0136, 0.0000, 0.0004, 0.0000, 0.0000, 0.0227, 0.0061, 0.4882, 0.2252 } }, - { { 0.0559, 0.0000, 0.1807, 0.0059, 0.0036, 0.0013, 0.0046, 0.0134, 0.0000, 0.0004, 0.0000, 0.0487, 0.0209, 0.0060, 0.4615, 0.1972 } }, - } }; - - static const std::array knownLanguages = - { { - "chinese", - "english", - "french", - "german", - "polish", - "russian", - "ukrainian" - } }; - - if(!CResourceHandler::get("core")->existsResource(TextPath::builtin("DATA/GENRLTXT.TXT"))) - { - Settings language = settings.write["session"]["language"]; - language->String() = "english"; - - Settings confidence = settings.write["session"]["languageDeviation"]; - confidence->Float() = 1.0; - - Settings encoding = settings.write["session"]["encoding"]; - encoding->String() = Languages::getLanguageOptions("english").encoding; - - return; - } - - // load file that will be used for footprint generation - // this is one of the most text-heavy files in game and consists solely from translated texts - auto resource = CResourceHandler::get("core")->load(TextPath::builtin("DATA/GENRLTXT.TXT")); - - std::array charCount{}; - std::array footprint{}; - std::array deviations{}; - - auto data = resource->readAll(); - - // compute how often each character occurs in input file - for (si64 i = 0; i < data.second; ++i) - charCount[data.first[i]] += 1; - - // and convert computed data into weights - // to reduce amount of data, group footprint data into 16-char blocks. - // While this will reduce precision, it should not affect output - // since we expect only tiny differences compared to reference footprints - for (size_t i = 0; i < 256; ++i) - footprint[i/16] += static_cast(charCount[i]) / data.second; - - logGlobal->debug("Language footprint: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f", - footprint[0], footprint[1], footprint[2], footprint[3], footprint[4], footprint[5], footprint[6], footprint[7], - footprint[8], footprint[9], footprint[10], footprint[11], footprint[12], footprint[13], footprint[14], footprint[15] - ); - - for (size_t i = 0; i < deviations.size(); ++i) - { - for (size_t j = 0; j < footprint.size(); ++j) - deviations[i] += std::abs((footprint[j] - knownFootprints[i][j])); - } - - size_t bestIndex = boost::range::min_element(deviations) - deviations.begin(); - - for (size_t i = 0; i < deviations.size(); ++i) - logGlobal->debug("Comparing to %s: %f", knownLanguages[i], deviations[i]); - - Settings language = settings.write["session"]["language"]; - language->String() = knownLanguages[bestIndex]; - - Settings confidence = settings.write["session"]["languageDeviation"]; - confidence->Float() = deviations[bestIndex]; - - Settings encoding = settings.write["session"]["encoding"]; - encoding->String() = Languages::getLanguageOptions(knownLanguages[bestIndex]).encoding; -} - -//Helper for string -> float conversion -class LocaleWithComma: public std::numpunct -{ -protected: - char do_decimal_point() const override - { - return ','; - } -}; - -CLegacyConfigParser::CLegacyConfigParser(const TextPath & resource) -{ - auto input = CResourceHandler::get()->load(resource); - std::string modName = VLC->modh->findResourceOrigin(resource); - std::string language = VLC->modh->getModLanguage(modName); - fileEncoding = Languages::getLanguageOptions(language).encoding; - - data.reset(new char[input->getSize()]); - input->read(reinterpret_cast(data.get()), input->getSize()); - - curr = data.get(); - end = curr + input->getSize(); -} - -std::string CLegacyConfigParser::extractQuotedPart() -{ - assert(*curr == '\"'); - - curr++; // skip quote - char * begin = curr; - - while (curr != end && *curr != '\"' && *curr != '\t') - curr++; - - return std::string(begin, curr++); //increment curr to close quote -} - -std::string CLegacyConfigParser::extractQuotedString() -{ - assert(*curr == '\"'); - - std::string ret; - while (true) - { - ret += extractQuotedPart(); - - // double quote - add it to string and continue quoted part - if (curr < end && *curr == '\"') - { - ret += '\"'; - } - //extract normal part - else if(curr < end && *curr != '\t' && *curr != '\r') - { - char * begin = curr; - - while (curr < end && *curr != '\t' && *curr != '\r' && *curr != '\"')//find end of string or next quoted part start - curr++; - - ret += std::string(begin, curr); - - if(curr>=end || *curr != '\"') - return ret; - } - else // end of string - return ret; - } -} - -std::string CLegacyConfigParser::extractNormalString() -{ - char * begin = curr; - - while (curr < end && *curr != '\t' && *curr != '\r')//find end of string - curr++; - - return std::string(begin, curr); -} - -std::string CLegacyConfigParser::readRawString() -{ - if (curr >= end || *curr == '\n') - return ""; - - std::string ret; - - if (*curr == '\"') - ret = extractQuotedString();// quoted text - find closing quote - else - ret = extractNormalString();//string without quotes - copy till \t or \r - - curr++; - return ret; -} - -std::string CLegacyConfigParser::readString() -{ - // do not convert strings that are already in ASCII - this will only slow down loading process - std::string str = readRawString(); - if (TextOperations::isValidASCII(str)) - return str; - return TextOperations::toUnicode(str, fileEncoding); -} - -float CLegacyConfigParser::readNumber() -{ - std::string input = readRawString(); - - std::istringstream stream(input); - - if(input.find(',') != std::string::npos) // code to handle conversion with comma as decimal separator - stream.imbue(std::locale(std::locale(), new LocaleWithComma())); - - float result; - if ( !(stream >> result) ) - return 0; - return result; -} - -bool CLegacyConfigParser::isNextEntryEmpty() const -{ - char * nextSymbol = curr; - while (nextSymbol < end && *nextSymbol == ' ') - nextSymbol++; //find next meaningfull symbol - - return nextSymbol >= end || *nextSymbol == '\n' || *nextSymbol == '\r' || *nextSymbol == '\t'; -} - -bool CLegacyConfigParser::endLine() -{ - while (curr < end && *curr != '\n') - readString(); - - curr++; - - return curr < end; -} - -void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) -{ - assert(!modContext.empty()); - assert(!language.empty()); - - // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment - auto & entry = stringsLocalizations[UID.get()]; - - entry.overrideLanguage = language; - entry.overrideValue = localized; - if (entry.modContext.empty()) - entry.modContext = modContext; -} - -void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) -{ - subContainers.push_back(&container); -} - -void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) -{ - subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); -} - -const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const -{ - if(stringsLocalizations.count(identifier.get()) == 0) - { - for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) - if((*containerIter)->identifierExists(identifier)) - return (*containerIter)->deserialize(identifier); - - logGlobal->error("Unable to find localization for string '%s'", identifier.get()); - return identifier.get(); - } - - const auto & entry = stringsLocalizations.at(identifier.get()); - - if (!entry.overrideValue.empty()) - return entry.overrideValue; - return entry.baseValue; -} - -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) -{ - assert(!modContext.empty()); - assert(!Languages::getLanguageOptions(language).identifier.empty()); - assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string - //assert(stringsLocalizations.count(UID.get()) == 0); // registering already registered string? - - if(stringsLocalizations.count(UID.get()) > 0) - { - auto & value = stringsLocalizations[UID.get()]; - value.baseLanguage = language; - value.baseValue = localized; - } - else - { - StringState value; - value.baseLanguage = language; - value.baseValue = localized; - value.modContext = modContext; - - stringsLocalizations[UID.get()] = value; - } -} - -void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) -{ - assert(!getModLanguage(modContext).empty()); - registerString(modContext, UID, localized, getModLanguage(modContext)); -} - -bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const -{ - bool allPresent = true; - - for(const auto & string : stringsLocalizations) - { - if (string.second.modContext != modContext) - continue; // Not our mod - - if (string.second.overrideLanguage == language) - continue; // Already translated - - if (string.second.baseLanguage == language && !string.second.baseValue.empty()) - continue; // Base string already uses our language - - if (string.second.baseLanguage.empty()) - continue; // String added in localization, not present in base language (e.g. maps/campaigns) - - if (config.Struct().count(string.first) > 0) - continue; - - if (allPresent) - logMod->warn("Translation into language '%s' in mod '%s' is incomplete! Missing lines:", language, modContext); - - std::string currentText; - if (string.second.overrideValue.empty()) - currentText = string.second.baseValue; - else - currentText = string.second.overrideValue; - - logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(currentText)); - allPresent = false; - } - - bool allFound = true; - -// for(const auto & string : config.Struct()) -// { -// if (stringsLocalizations.count(string.first) > 0) -// continue; -// -// if (allFound) -// logMod->warn("Translation into language '%s' in mod '%s' has unused lines:", language, modContext); -// -// logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(string.second.String())); -// allFound = false; -// } - - return allPresent && allFound; -} - -void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config) -{ - for(const auto & node : config.Struct()) - registerStringOverride(modContext, language, node.first, node.second.String()); -} - -bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const -{ - return stringsLocalizations.count(UID.get()); -} - -void TextLocalizationContainer::dumpAllTexts() -{ - logGlobal->info("BEGIN TEXT EXPORT"); - for(const auto & entry : stringsLocalizations) - { - if (!entry.second.overrideValue.empty()) - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); - else - logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); - } - - logGlobal->info("END TEXT EXPORT"); -} - -std::string TextLocalizationContainer::getModLanguage(const std::string & modContext) -{ - if (modContext == "core") - return CGeneralTextHandler::getInstalledLanguage(); - return VLC->modh->getModLanguage(modContext); -} - -void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const -{ - for(auto & s : stringsLocalizations) - { - dest.Struct()[s.first].String() = s.second.baseValue; - if(!s.second.overrideValue.empty()) - dest.Struct()[s.first].String() = s.second.overrideValue; - } -} - -void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) -{ - CLegacyConfigParser parser(TextPath::builtin(sourceName)); - size_t index = 0; - do - { - registerString( "core", {sourceID, index}, parser.readString()); - index += 1; - } - while (parser.endLine()); -} - -CGeneralTextHandler::CGeneralTextHandler(): - victoryConditions(*this, "core.vcdesc" ), - lossCondtions (*this, "core.lcdesc" ), - colors (*this, "core.plcolors" ), - tcommands (*this, "core.tcommand" ), - hcommands (*this, "core.hallinfo" ), - fcommands (*this, "core.castinfo" ), - advobtxt (*this, "core.advevent" ), - restypes (*this, "core.restypes" ), - randsign (*this, "core.randsign" ), - overview (*this, "core.overview" ), - arraytxt (*this, "core.arraytxt" ), - primarySkillNames(*this, "core.priskill" ), - jktexts (*this, "core.jktext" ), - tavernInfo (*this, "core.tvrninfo" ), - tavernRumors (*this, "core.randtvrn" ), - turnDurations (*this, "core.turndur" ), - heroscrn (*this, "core.heroscrn" ), - tentColors (*this, "core.tentcolr" ), - levels (*this, "core.skilllev" ), - zelp (*this, "core.help" ), - allTexts (*this, "core.genrltxt" ), - // pseudo-array, that don't have H3 file with same name - seerEmpty (*this, "core.seerhut.empty" ), - seerNames (*this, "core.seerhut.names" ), - capColors (*this, "vcmi.capitalColors" ), - znpc00 (*this, "vcmi.znpc00" ), // technically - wog - qeModCommands (*this, "vcmi.quickExchange" ) -{ - readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); - readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); - readToVector("core.tcommand", "DATA/TCOMMAND.TXT" ); - readToVector("core.hallinfo", "DATA/HALLINFO.TXT" ); - readToVector("core.castinfo", "DATA/CASTINFO.TXT" ); - readToVector("core.advevent", "DATA/ADVEVENT.TXT" ); - readToVector("core.restypes", "DATA/RESTYPES.TXT" ); - readToVector("core.randsign", "DATA/RANDSIGN.TXT" ); - readToVector("core.overview", "DATA/OVERVIEW.TXT" ); - readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" ); - readToVector("core.priskill", "DATA/PRISKILL.TXT" ); - readToVector("core.jktext", "DATA/JKTEXT.TXT" ); - readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" ); - readToVector("core.turndur", "DATA/TURNDUR.TXT" ); - readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" ); - readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" ); - readToVector("core.skilllev", "DATA/SKILLLEV.TXT" ); - readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); - readToVector("core.minename", "DATA/MINENAME.TXT" ); - readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); - - static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; - if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) - readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); - - { - CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT")); - parser.endLine(); - size_t index = 0; - do - { - std::string line = parser.readString(); - if(!line.empty()) - { - registerString("core", {"core.randtvrn", index}, line); - index += 1; - } - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser(TextPath::builtin("DATA/GENRLTXT.TXT")); - parser.endLine(); - size_t index = 0; - do - { - registerString("core", {"core.genrltxt", index}, parser.readString()); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser(TextPath::builtin("DATA/HELP.TXT")); - size_t index = 0; - do - { - std::string first = parser.readString(); - std::string second = parser.readString(); - registerString("core", "core.help." + std::to_string(index) + ".hover", first); - registerString("core", "core.help." + std::to_string(index) + ".help", second); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser(TextPath::builtin("DATA/PLCOLORS.TXT")); - size_t index = 0; - do - { - std::string color = parser.readString(); - - registerString("core", {"core.plcolors", index}, color); - color[0] = toupper(color[0]); - registerString("core", {"vcmi.capitalColors", index}, color); - index += 1; - } - while (parser.endLine()); - } - { - CLegacyConfigParser parser(TextPath::builtin("DATA/SEERHUT.TXT")); - - //skip header - parser.endLine(); - - for (size_t i = 0; i < 6; ++i) - { - registerString("core", {"core.seerhut.empty", i}, parser.readString()); - } - parser.endLine(); - - for (size_t i = 0; i < 9; ++i) //9 types of quests - { - std::string questName = CQuest::missionName(1+i); - - for (size_t j = 0; j < 5; ++j) - { - std::string questState = CQuest::missionState(j); - - parser.readString(); //front description - for (size_t k = 0; k < 6; ++k) - { - registerString("core", {"core.seerhut.quest", questName, questState, k}, parser.readString()); - } - parser.endLine(); - } - } - - for (size_t k = 0; k < 6; ++k) //Time limit - { - registerString("core", {"core.seerhut.time", k}, parser.readString()); - } - parser.endLine(); - - parser.endLine(); // empty line - parser.endLine(); // header - - for (size_t i = 0; i < 48; ++i) - { - registerString("core", {"core.seerhut.names", i}, parser.readString()); - parser.endLine(); - } - } - { - CLegacyConfigParser parser(TextPath::builtin("DATA/CAMPTEXT.TXT")); - - //skip header - parser.endLine(); - - std::string text; - size_t campaignsCount = 0; - do - { - text = parser.readString(); - if (!text.empty()) - { - registerString("core", {"core.camptext.names", campaignsCount}, text); - campaignsCount += 1; - } - } - while (parser.endLine() && !text.empty()); - - for (size_t campaign=0; campaignsettings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) - { - if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT"))) - readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); - } -} - -int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const -{ - if(textIndex == 0) - return 0; - if(textIndex < 0) - return -textIndex; - if(count == 1) - return textIndex; - - return textIndex + 1; -} - -size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const -{ - assert(campaignID < scenariosCountPerCampaign.size()); - - if(campaignID < scenariosCountPerCampaign.size()) - return scenariosCountPerCampaign[campaignID]; - return 0; -} - -std::string CGeneralTextHandler::getPreferredLanguage() -{ - assert(!settings["general"]["language"].String().empty()); - return settings["general"]["language"].String(); -} - -std::string CGeneralTextHandler::getInstalledLanguage() -{ - assert(!settings["session"]["language"].String().empty()); - return settings["session"]["language"].String(); -} - -std::string CGeneralTextHandler::getInstalledEncoding() -{ - assert(!settings["session"]["encoding"].String().empty()); - return settings["session"]["encoding"].String(); -} - -std::vector CGeneralTextHandler::findStringsWithPrefix(const std::string & prefix) -{ - std::vector result; - - for(const auto & entry : stringsLocalizations) - { - if(boost::algorithm::starts_with(entry.first, prefix)) - result.push_back(entry.first); - } - - return result; -} - -LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath): - owner(owner), - basePath(std::move(basePath)) -{} - -std::string LegacyTextContainer::operator[](size_t index) const -{ - return owner.translate(basePath, index); -} - -LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath): - owner(owner), - basePath(std::move(basePath)) -{} - -std::pair LegacyHelpContainer::operator[](size_t index) const -{ - return { - owner.translate(basePath + "." + std::to_string(index) + ".hover"), - owner.translate(basePath + "." + std::to_string(index) + ".help") - }; -} - -VCMI_LIB_NAMESPACE_END +/* + * CGeneralTextHandler.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 "CGeneralTextHandler.h" + +#include "filesystem/Filesystem.h" +#include "serializer/JsonSerializeFormat.h" +#include "CConfigHandler.h" +#include "GameSettings.h" +#include "mapObjects/CQuest.h" +#include "modding/CModHandler.h" +#include "VCMI_Lib.h" +#include "Languages.h" +#include "TextOperations.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Detects language and encoding of H3 text files based on matching against pregenerated footprints of H3 file +void CGeneralTextHandler::detectInstallParameters() +{ + using LanguageFootprint = std::array; + + static const std::array knownFootprints = + { { + { { 0.1602, 0.0000, 0.0357, 0.0054, 0.0038, 0.0017, 0.0077, 0.0214, 0.0000, 0.0000, 0.1264, 0.1947, 0.2012, 0.1406, 0.0480, 0.0532 } }, + { { 0.0559, 0.0000, 0.1983, 0.0051, 0.0222, 0.0183, 0.4596, 0.2405, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000 } }, + { { 0.0493, 0.0000, 0.1926, 0.0047, 0.0230, 0.0121, 0.4133, 0.2780, 0.0002, 0.0000, 0.0000, 0.0000, 0.0000, 0.0000, 0.0259, 0.0008 } }, + { { 0.0534, 0.0000, 0.1705, 0.0047, 0.0418, 0.0208, 0.4775, 0.2191, 0.0001, 0.0000, 0.0000, 0.0000, 0.0000, 0.0005, 0.0036, 0.0080 } }, + { { 0.0534, 0.0000, 0.1701, 0.0067, 0.0157, 0.0133, 0.4328, 0.2540, 0.0001, 0.0043, 0.0000, 0.0244, 0.0000, 0.0000, 0.0181, 0.0071 } }, + { { 0.0548, 0.0000, 0.1744, 0.0061, 0.0031, 0.0009, 0.0046, 0.0136, 0.0000, 0.0004, 0.0000, 0.0000, 0.0227, 0.0061, 0.4882, 0.2252 } }, + { { 0.0559, 0.0000, 0.1807, 0.0059, 0.0036, 0.0013, 0.0046, 0.0134, 0.0000, 0.0004, 0.0000, 0.0487, 0.0209, 0.0060, 0.4615, 0.1972 } }, + } }; + + static const std::array knownLanguages = + { { + "chinese", + "english", + "french", + "german", + "polish", + "russian", + "ukrainian" + } }; + + if(!CResourceHandler::get("core")->existsResource(TextPath::builtin("DATA/GENRLTXT.TXT"))) + { + Settings language = settings.write["session"]["language"]; + language->String() = "english"; + + Settings confidence = settings.write["session"]["languageDeviation"]; + confidence->Float() = 1.0; + + Settings encoding = settings.write["session"]["encoding"]; + encoding->String() = Languages::getLanguageOptions("english").encoding; + + return; + } + + // load file that will be used for footprint generation + // this is one of the most text-heavy files in game and consists solely from translated texts + auto resource = CResourceHandler::get("core")->load(TextPath::builtin("DATA/GENRLTXT.TXT")); + + std::array charCount{}; + std::array footprint{}; + std::array deviations{}; + + auto data = resource->readAll(); + + // compute how often each character occurs in input file + for (si64 i = 0; i < data.second; ++i) + charCount[data.first[i]] += 1; + + // and convert computed data into weights + // to reduce amount of data, group footprint data into 16-char blocks. + // While this will reduce precision, it should not affect output + // since we expect only tiny differences compared to reference footprints + for (size_t i = 0; i < 256; ++i) + footprint[i/16] += static_cast(charCount[i]) / data.second; + + logGlobal->debug("Language footprint: %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f %f", + footprint[0], footprint[1], footprint[2], footprint[3], footprint[4], footprint[5], footprint[6], footprint[7], + footprint[8], footprint[9], footprint[10], footprint[11], footprint[12], footprint[13], footprint[14], footprint[15] + ); + + for (size_t i = 0; i < deviations.size(); ++i) + { + for (size_t j = 0; j < footprint.size(); ++j) + deviations[i] += std::abs((footprint[j] - knownFootprints[i][j])); + } + + size_t bestIndex = boost::range::min_element(deviations) - deviations.begin(); + + for (size_t i = 0; i < deviations.size(); ++i) + logGlobal->debug("Comparing to %s: %f", knownLanguages[i], deviations[i]); + + Settings language = settings.write["session"]["language"]; + language->String() = knownLanguages[bestIndex]; + + Settings confidence = settings.write["session"]["languageDeviation"]; + confidence->Float() = deviations[bestIndex]; + + Settings encoding = settings.write["session"]["encoding"]; + encoding->String() = Languages::getLanguageOptions(knownLanguages[bestIndex]).encoding; +} + +//Helper for string -> float conversion +class LocaleWithComma: public std::numpunct +{ +protected: + char do_decimal_point() const override + { + return ','; + } +}; + +CLegacyConfigParser::CLegacyConfigParser(const TextPath & resource) +{ + auto input = CResourceHandler::get()->load(resource); + std::string modName = VLC->modh->findResourceOrigin(resource); + std::string language = VLC->modh->getModLanguage(modName); + fileEncoding = Languages::getLanguageOptions(language).encoding; + + data.reset(new char[input->getSize()]); + input->read(reinterpret_cast(data.get()), input->getSize()); + + curr = data.get(); + end = curr + input->getSize(); +} + +std::string CLegacyConfigParser::extractQuotedPart() +{ + assert(*curr == '\"'); + + curr++; // skip quote + char * begin = curr; + + while (curr != end && *curr != '\"' && *curr != '\t') + curr++; + + return std::string(begin, curr++); //increment curr to close quote +} + +std::string CLegacyConfigParser::extractQuotedString() +{ + assert(*curr == '\"'); + + std::string ret; + while (true) + { + ret += extractQuotedPart(); + + // double quote - add it to string and continue quoted part + if (curr < end && *curr == '\"') + { + ret += '\"'; + } + //extract normal part + else if(curr < end && *curr != '\t' && *curr != '\r') + { + char * begin = curr; + + while (curr < end && *curr != '\t' && *curr != '\r' && *curr != '\"')//find end of string or next quoted part start + curr++; + + ret += std::string(begin, curr); + + if(curr>=end || *curr != '\"') + return ret; + } + else // end of string + return ret; + } +} + +std::string CLegacyConfigParser::extractNormalString() +{ + char * begin = curr; + + while (curr < end && *curr != '\t' && *curr != '\r')//find end of string + curr++; + + return std::string(begin, curr); +} + +std::string CLegacyConfigParser::readRawString() +{ + if (curr >= end || *curr == '\n') + return ""; + + std::string ret; + + if (*curr == '\"') + ret = extractQuotedString();// quoted text - find closing quote + else + ret = extractNormalString();//string without quotes - copy till \t or \r + + curr++; + return ret; +} + +std::string CLegacyConfigParser::readString() +{ + // do not convert strings that are already in ASCII - this will only slow down loading process + std::string str = readRawString(); + if (TextOperations::isValidASCII(str)) + return str; + return TextOperations::toUnicode(str, fileEncoding); +} + +float CLegacyConfigParser::readNumber() +{ + std::string input = readRawString(); + + std::istringstream stream(input); + + if(input.find(',') != std::string::npos) // code to handle conversion with comma as decimal separator + stream.imbue(std::locale(std::locale(), new LocaleWithComma())); + + float result; + if ( !(stream >> result) ) + return 0; + return result; +} + +bool CLegacyConfigParser::isNextEntryEmpty() const +{ + char * nextSymbol = curr; + while (nextSymbol < end && *nextSymbol == ' ') + nextSymbol++; //find next meaningfull symbol + + return nextSymbol >= end || *nextSymbol == '\n' || *nextSymbol == '\r' || *nextSymbol == '\t'; +} + +bool CLegacyConfigParser::endLine() +{ + while (curr < end && *curr != '\n') + readString(); + + curr++; + + return curr < end; +} + +void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized) +{ + assert(!modContext.empty()); + assert(!language.empty()); + + // NOTE: implicitly creates entry, intended - strings added by maps, campaigns, vcmi and potentially - UI mods are not registered anywhere at the moment + auto & entry = stringsLocalizations[UID.get()]; + + entry.overrideLanguage = language; + entry.overrideValue = localized; + if (entry.modContext.empty()) + entry.modContext = modContext; +} + +void TextLocalizationContainer::addSubContainer(const TextLocalizationContainer & container) +{ + subContainers.push_back(&container); +} + +void TextLocalizationContainer::removeSubContainer(const TextLocalizationContainer & container) +{ + subContainers.erase(std::remove(subContainers.begin(), subContainers.end(), &container), subContainers.end()); +} + +const std::string & TextLocalizationContainer::deserialize(const TextIdentifier & identifier) const +{ + if(stringsLocalizations.count(identifier.get()) == 0) + { + for(auto containerIter = subContainers.rbegin(); containerIter != subContainers.rend(); ++containerIter) + if((*containerIter)->identifierExists(identifier)) + return (*containerIter)->deserialize(identifier); + + logGlobal->error("Unable to find localization for string '%s'", identifier.get()); + return identifier.get(); + } + + const auto & entry = stringsLocalizations.at(identifier.get()); + + if (!entry.overrideValue.empty()) + return entry.overrideValue; + return entry.baseValue; +} + +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language) +{ + assert(!modContext.empty()); + assert(!Languages::getLanguageOptions(language).identifier.empty()); + assert(UID.get().find("..") == std::string::npos); // invalid identifier - there is section that was evaluated to empty string + //assert(stringsLocalizations.count(UID.get()) == 0); // registering already registered string? + + if(stringsLocalizations.count(UID.get()) > 0) + { + auto & value = stringsLocalizations[UID.get()]; + value.baseLanguage = language; + value.baseValue = localized; + } + else + { + StringState value; + value.baseLanguage = language; + value.baseValue = localized; + value.modContext = modContext; + + stringsLocalizations[UID.get()] = value; + } +} + +void TextLocalizationContainer::registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized) +{ + assert(!getModLanguage(modContext).empty()); + registerString(modContext, UID, localized, getModLanguage(modContext)); +} + +bool TextLocalizationContainer::validateTranslation(const std::string & language, const std::string & modContext, const JsonNode & config) const +{ + bool allPresent = true; + + for(const auto & string : stringsLocalizations) + { + if (string.second.modContext != modContext) + continue; // Not our mod + + if (string.second.overrideLanguage == language) + continue; // Already translated + + if (string.second.baseLanguage == language && !string.second.baseValue.empty()) + continue; // Base string already uses our language + + if (string.second.baseLanguage.empty()) + continue; // String added in localization, not present in base language (e.g. maps/campaigns) + + if (config.Struct().count(string.first) > 0) + continue; + + if (allPresent) + logMod->warn("Translation into language '%s' in mod '%s' is incomplete! Missing lines:", language, modContext); + + std::string currentText; + if (string.second.overrideValue.empty()) + currentText = string.second.baseValue; + else + currentText = string.second.overrideValue; + + logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(currentText)); + allPresent = false; + } + + bool allFound = true; + +// for(const auto & string : config.Struct()) +// { +// if (stringsLocalizations.count(string.first) > 0) +// continue; +// +// if (allFound) +// logMod->warn("Translation into language '%s' in mod '%s' has unused lines:", language, modContext); +// +// logMod->warn(R"( "%s" : "%s",)", string.first, TextOperations::escapeString(string.second.String())); +// allFound = false; +// } + + return allPresent && allFound; +} + +void TextLocalizationContainer::loadTranslationOverrides(const std::string & language, const std::string & modContext, const JsonNode & config) +{ + for(const auto & node : config.Struct()) + registerStringOverride(modContext, language, node.first, node.second.String()); +} + +bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const +{ + return stringsLocalizations.count(UID.get()); +} + +void TextLocalizationContainer::dumpAllTexts() +{ + logGlobal->info("BEGIN TEXT EXPORT"); + for(const auto & entry : stringsLocalizations) + { + if (!entry.second.overrideValue.empty()) + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.overrideValue)); + else + logGlobal->info(R"("%s" : "%s",)", entry.first, TextOperations::escapeString(entry.second.baseValue)); + } + + logGlobal->info("END TEXT EXPORT"); +} + +std::string TextLocalizationContainer::getModLanguage(const std::string & modContext) +{ + if (modContext == "core") + return CGeneralTextHandler::getInstalledLanguage(); + return VLC->modh->getModLanguage(modContext); +} + +void TextLocalizationContainer::jsonSerialize(JsonNode & dest) const +{ + for(auto & s : stringsLocalizations) + { + dest.Struct()[s.first].String() = s.second.baseValue; + if(!s.second.overrideValue.empty()) + dest.Struct()[s.first].String() = s.second.overrideValue; + } +} + +void CGeneralTextHandler::readToVector(const std::string & sourceID, const std::string & sourceName) +{ + CLegacyConfigParser parser(TextPath::builtin(sourceName)); + size_t index = 0; + do + { + registerString( "core", {sourceID, index}, parser.readString()); + index += 1; + } + while (parser.endLine()); +} + +CGeneralTextHandler::CGeneralTextHandler(): + victoryConditions(*this, "core.vcdesc" ), + lossCondtions (*this, "core.lcdesc" ), + colors (*this, "core.plcolors" ), + tcommands (*this, "core.tcommand" ), + hcommands (*this, "core.hallinfo" ), + fcommands (*this, "core.castinfo" ), + advobtxt (*this, "core.advevent" ), + restypes (*this, "core.restypes" ), + randsign (*this, "core.randsign" ), + overview (*this, "core.overview" ), + arraytxt (*this, "core.arraytxt" ), + primarySkillNames(*this, "core.priskill" ), + jktexts (*this, "core.jktext" ), + tavernInfo (*this, "core.tvrninfo" ), + tavernRumors (*this, "core.randtvrn" ), + turnDurations (*this, "core.turndur" ), + heroscrn (*this, "core.heroscrn" ), + tentColors (*this, "core.tentcolr" ), + levels (*this, "core.skilllev" ), + zelp (*this, "core.help" ), + allTexts (*this, "core.genrltxt" ), + // pseudo-array, that don't have H3 file with same name + seerEmpty (*this, "core.seerhut.empty" ), + seerNames (*this, "core.seerhut.names" ), + capColors (*this, "vcmi.capitalColors" ), + znpc00 (*this, "vcmi.znpc00" ), // technically - wog + qeModCommands (*this, "vcmi.quickExchange" ) +{ + readToVector("core.vcdesc", "DATA/VCDESC.TXT" ); + readToVector("core.lcdesc", "DATA/LCDESC.TXT" ); + readToVector("core.tcommand", "DATA/TCOMMAND.TXT" ); + readToVector("core.hallinfo", "DATA/HALLINFO.TXT" ); + readToVector("core.castinfo", "DATA/CASTINFO.TXT" ); + readToVector("core.advevent", "DATA/ADVEVENT.TXT" ); + readToVector("core.restypes", "DATA/RESTYPES.TXT" ); + readToVector("core.randsign", "DATA/RANDSIGN.TXT" ); + readToVector("core.overview", "DATA/OVERVIEW.TXT" ); + readToVector("core.arraytxt", "DATA/ARRAYTXT.TXT" ); + readToVector("core.priskill", "DATA/PRISKILL.TXT" ); + readToVector("core.jktext", "DATA/JKTEXT.TXT" ); + readToVector("core.tvrninfo", "DATA/TVRNINFO.TXT" ); + readToVector("core.turndur", "DATA/TURNDUR.TXT" ); + readToVector("core.heroscrn", "DATA/HEROSCRN.TXT" ); + readToVector("core.tentcolr", "DATA/TENTCOLR.TXT" ); + readToVector("core.skilllev", "DATA/SKILLLEV.TXT" ); + readToVector("core.cmpmusic", "DATA/CMPMUSIC.TXT" ); + readToVector("core.minename", "DATA/MINENAME.TXT" ); + readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" ); + + static const char * QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT"; + if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS))) + readToVector("vcmi.quickExchange", QE_MOD_COMMANDS); + + { + CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT")); + parser.endLine(); + size_t index = 0; + do + { + std::string line = parser.readString(); + if(!line.empty()) + { + registerString("core", {"core.randtvrn", index}, line); + index += 1; + } + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/GENRLTXT.TXT")); + parser.endLine(); + size_t index = 0; + do + { + registerString("core", {"core.genrltxt", index}, parser.readString()); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/HELP.TXT")); + size_t index = 0; + do + { + std::string first = parser.readString(); + std::string second = parser.readString(); + registerString("core", "core.help." + std::to_string(index) + ".hover", first); + registerString("core", "core.help." + std::to_string(index) + ".help", second); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/PLCOLORS.TXT")); + size_t index = 0; + do + { + std::string color = parser.readString(); + + registerString("core", {"core.plcolors", index}, color); + color[0] = toupper(color[0]); + registerString("core", {"vcmi.capitalColors", index}, color); + index += 1; + } + while (parser.endLine()); + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/SEERHUT.TXT")); + + //skip header + parser.endLine(); + + for (size_t i = 0; i < 6; ++i) + { + registerString("core", {"core.seerhut.empty", i}, parser.readString()); + } + parser.endLine(); + + for (size_t i = 0; i < 9; ++i) //9 types of quests + { + std::string questName = CQuest::missionName(1+i); + + for (size_t j = 0; j < 5; ++j) + { + std::string questState = CQuest::missionState(j); + + parser.readString(); //front description + for (size_t k = 0; k < 6; ++k) + { + registerString("core", {"core.seerhut.quest", questName, questState, k}, parser.readString()); + } + parser.endLine(); + } + } + + for (size_t k = 0; k < 6; ++k) //Time limit + { + registerString("core", {"core.seerhut.time", k}, parser.readString()); + } + parser.endLine(); + + parser.endLine(); // empty line + parser.endLine(); // header + + for (size_t i = 0; i < 48; ++i) + { + registerString("core", {"core.seerhut.names", i}, parser.readString()); + parser.endLine(); + } + } + { + CLegacyConfigParser parser(TextPath::builtin("DATA/CAMPTEXT.TXT")); + + //skip header + parser.endLine(); + + std::string text; + size_t campaignsCount = 0; + do + { + text = parser.readString(); + if (!text.empty()) + { + registerString("core", {"core.camptext.names", campaignsCount}, text); + campaignsCount += 1; + } + } + while (parser.endLine() && !text.empty()); + + for (size_t campaign=0; campaignsettings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + { + if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT"))) + readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" ); + } +} + +int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const +{ + if(textIndex == 0) + return 0; + if(textIndex < 0) + return -textIndex; + if(count == 1) + return textIndex; + + return textIndex + 1; +} + +size_t CGeneralTextHandler::getCampaignLength(size_t campaignID) const +{ + assert(campaignID < scenariosCountPerCampaign.size()); + + if(campaignID < scenariosCountPerCampaign.size()) + return scenariosCountPerCampaign[campaignID]; + return 0; +} + +std::string CGeneralTextHandler::getPreferredLanguage() +{ + assert(!settings["general"]["language"].String().empty()); + return settings["general"]["language"].String(); +} + +std::string CGeneralTextHandler::getInstalledLanguage() +{ + assert(!settings["session"]["language"].String().empty()); + return settings["session"]["language"].String(); +} + +std::string CGeneralTextHandler::getInstalledEncoding() +{ + assert(!settings["session"]["encoding"].String().empty()); + return settings["session"]["encoding"].String(); +} + +std::vector CGeneralTextHandler::findStringsWithPrefix(const std::string & prefix) +{ + std::vector result; + + for(const auto & entry : stringsLocalizations) + { + if(boost::algorithm::starts_with(entry.first, prefix)) + result.push_back(entry.first); + } + + return result; +} + +LegacyTextContainer::LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath): + owner(owner), + basePath(std::move(basePath)) +{} + +std::string LegacyTextContainer::operator[](size_t index) const +{ + return owner.translate(basePath, index); +} + +LegacyHelpContainer::LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath): + owner(owner), + basePath(std::move(basePath)) +{} + +std::pair LegacyHelpContainer::operator[](size_t index) const +{ + return { + owner.translate(basePath + "." + std::to_string(index) + ".hover"), + owner.translate(basePath + "." + std::to_string(index) + ".help") + }; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CGeneralTextHandler.h b/lib/CGeneralTextHandler.h index 7bc7b3b94..980a3c03e 100644 --- a/lib/CGeneralTextHandler.h +++ b/lib/CGeneralTextHandler.h @@ -1,288 +1,288 @@ -/* - * CGeneralTextHandler.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 "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CInputStream; -class JsonNode; -class JsonSerializeFormat; - -/// Parser for any text files from H3 -class DLL_LINKAGE CLegacyConfigParser -{ - std::string fileEncoding; - - std::unique_ptr data; - char * curr; - char * end; - - /// extracts part of quoted string. - std::string extractQuotedPart(); - - /// extracts quoted string. Any end of lines are ignored, double-quote is considered as "escaping" - std::string extractQuotedString(); - - /// extracts non-quoted string - std::string extractNormalString(); - - /// reads "raw" string without encoding conversion - std::string readRawString(); - -public: - /// read one entry from current line. Return ""/0 if end of line reached - std::string readString(); - float readNumber(); - - template - std::vector readNumArray(size_t size) - { - std::vector ret; - ret.reserve(size); - while (size--) - ret.push_back((numeric)readNumber()); - return ret; - } - - /// returns true if next entry is empty - bool isNextEntryEmpty() const; - - /// end current line - bool endLine(); - - explicit CLegacyConfigParser(const TextPath & URI); -}; - -class CGeneralTextHandler; - -/// Small wrapper that provides text access API compatible with old code -class DLL_LINKAGE LegacyTextContainer -{ - CGeneralTextHandler & owner; - std::string basePath; - -public: - LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath); - std::string operator [](size_t index) const; -}; - -/// Small wrapper that provides help text access API compatible with old code -class DLL_LINKAGE LegacyHelpContainer -{ - CGeneralTextHandler & owner; - std::string basePath; - -public: - LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath); - std::pair operator[](size_t index) const; -}; - -class TextIdentifier -{ - std::string identifier; -public: - const std::string & get() const - { - return identifier; - } - - TextIdentifier(const char * id): - identifier(id) - {} - - TextIdentifier(const std::string & id): - identifier(id) - {} - - template - TextIdentifier(const std::string & id, size_t index, T... rest): - TextIdentifier(id + '.' + std::to_string(index), rest...) - {} - - template - TextIdentifier(const std::string & id, const std::string & id2, T... rest): - TextIdentifier(id + '.' + id2, rest...) - {} -}; - -class DLL_LINKAGE TextLocalizationContainer -{ -protected: - struct StringState - { - /// Human-readable string that was added on registration - std::string baseValue; - - /// Language of base string - std::string baseLanguage; - - /// Translated human-readable string - std::string overrideValue; - - /// Language of the override string - std::string overrideLanguage; - - /// ID of mod that created this string - std::string modContext; - - template - void serialize(Handler & h, const int Version) - { - h & baseValue; - h & baseLanguage; - h & modContext; - } - }; - - /// map identifier -> localization - std::unordered_map stringsLocalizations; - - std::vector subContainers; - - /// add selected string to internal storage as high-priority strings - void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); - - std::string getModLanguage(const std::string & modContext); - -public: - /// validates translation of specified language for specified mod - /// returns true if localization is valid and complete - /// any error messages will be written to log file - bool validateTranslation(const std::string & language, const std::string & modContext, JsonNode const & file) const; - - /// Loads translation from provided json - /// Any entries loaded by this will have priority over texts registered normally - void loadTranslationOverrides(const std::string & language, const std::string & modContext, JsonNode const & file); - - // returns true if identifier with such name was registered, even if not translated to current language - bool identifierExists(const TextIdentifier & UID) const; - - /// add selected string to internal storage - void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); - void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); - - /// returns translated version of a string that can be displayed to user - template - std::string translate(std::string arg1, Args ... args) const - { - TextIdentifier id(arg1, args ...); - return deserialize(id); - } - - /// converts identifier into user-readable string - const std::string & deserialize(const TextIdentifier & identifier) const; - - /// Debug method, dumps all currently known texts into console using Json-like format - void dumpAllTexts(); - - /// Add or override subcontainer which can store identifiers - void addSubContainer(const TextLocalizationContainer & container); - - /// Remove subcontainer with give name - void removeSubContainer(const TextLocalizationContainer & container); - - void jsonSerialize(JsonNode & dest) const; - - template - void serialize(Handler & h, const int Version) - { - std::string key; - auto sz = stringsLocalizations.size(); - h & sz; - if(h.saving) - { - for(auto s : stringsLocalizations) - { - key = s.first; - h & key; - h & s.second; - } - } - else - { - for(size_t i = 0; i < sz; ++i) - { - h & key; - h & stringsLocalizations[key]; - } - } - } -}; - -/// Handles all text-related data in game -class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer -{ - void readToVector(const std::string & sourceID, const std::string & sourceName); - - /// number of scenarios in specific campaign. TODO: move to a better location - std::vector scenariosCountPerCampaign; - -public: - LegacyTextContainer allTexts; - - LegacyTextContainer arraytxt; - LegacyTextContainer primarySkillNames; - LegacyTextContainer jktexts; - LegacyTextContainer heroscrn; - LegacyTextContainer overview;//text for Kingdom Overview window - LegacyTextContainer colors; //names of player colors ("red",...) - LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...) - LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited) - - //towns - LegacyTextContainer tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen - LegacyTextContainer tavernInfo; - LegacyTextContainer tavernRumors; - - LegacyTextContainer qeModCommands; - - LegacyHelpContainer zelp; - LegacyTextContainer lossCondtions; - LegacyTextContainer victoryConditions; - - //objects - LegacyTextContainer advobtxt; - LegacyTextContainer restypes; //names of resources - LegacyTextContainer randsign; - LegacyTextContainer seerEmpty; - LegacyTextContainer seerNames; - LegacyTextContainer tentColors; - - //sec skills - LegacyTextContainer levels; - //commanders - LegacyTextContainer znpc00; //more or less useful content of that file - - std::vector findStringsWithPrefix(const std::string & prefix); - - int32_t pluralText(int32_t textIndex, int32_t count) const; - - size_t getCampaignLength(size_t campaignID) const; - - CGeneralTextHandler(); - CGeneralTextHandler(const CGeneralTextHandler&) = delete; - CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; - - /// Attempts to detect encoding & language of H3 files - static void detectInstallParameters(); - - /// Returns name of language preferred by user - static std::string getPreferredLanguage(); - - /// Returns name of language of Heroes III text files - static std::string getInstalledLanguage(); - - /// Returns name of encoding of Heroes III text files - static std::string getInstalledEncoding(); -}; - -VCMI_LIB_NAMESPACE_END +/* + * CGeneralTextHandler.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 "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CInputStream; +class JsonNode; +class JsonSerializeFormat; + +/// Parser for any text files from H3 +class DLL_LINKAGE CLegacyConfigParser +{ + std::string fileEncoding; + + std::unique_ptr data; + char * curr; + char * end; + + /// extracts part of quoted string. + std::string extractQuotedPart(); + + /// extracts quoted string. Any end of lines are ignored, double-quote is considered as "escaping" + std::string extractQuotedString(); + + /// extracts non-quoted string + std::string extractNormalString(); + + /// reads "raw" string without encoding conversion + std::string readRawString(); + +public: + /// read one entry from current line. Return ""/0 if end of line reached + std::string readString(); + float readNumber(); + + template + std::vector readNumArray(size_t size) + { + std::vector ret; + ret.reserve(size); + while (size--) + ret.push_back((numeric)readNumber()); + return ret; + } + + /// returns true if next entry is empty + bool isNextEntryEmpty() const; + + /// end current line + bool endLine(); + + explicit CLegacyConfigParser(const TextPath & URI); +}; + +class CGeneralTextHandler; + +/// Small wrapper that provides text access API compatible with old code +class DLL_LINKAGE LegacyTextContainer +{ + CGeneralTextHandler & owner; + std::string basePath; + +public: + LegacyTextContainer(CGeneralTextHandler & owner, std::string basePath); + std::string operator [](size_t index) const; +}; + +/// Small wrapper that provides help text access API compatible with old code +class DLL_LINKAGE LegacyHelpContainer +{ + CGeneralTextHandler & owner; + std::string basePath; + +public: + LegacyHelpContainer(CGeneralTextHandler & owner, std::string basePath); + std::pair operator[](size_t index) const; +}; + +class TextIdentifier +{ + std::string identifier; +public: + const std::string & get() const + { + return identifier; + } + + TextIdentifier(const char * id): + identifier(id) + {} + + TextIdentifier(const std::string & id): + identifier(id) + {} + + template + TextIdentifier(const std::string & id, size_t index, T... rest): + TextIdentifier(id + '.' + std::to_string(index), rest...) + {} + + template + TextIdentifier(const std::string & id, const std::string & id2, T... rest): + TextIdentifier(id + '.' + id2, rest...) + {} +}; + +class DLL_LINKAGE TextLocalizationContainer +{ +protected: + struct StringState + { + /// Human-readable string that was added on registration + std::string baseValue; + + /// Language of base string + std::string baseLanguage; + + /// Translated human-readable string + std::string overrideValue; + + /// Language of the override string + std::string overrideLanguage; + + /// ID of mod that created this string + std::string modContext; + + template + void serialize(Handler & h, const int Version) + { + h & baseValue; + h & baseLanguage; + h & modContext; + } + }; + + /// map identifier -> localization + std::unordered_map stringsLocalizations; + + std::vector subContainers; + + /// add selected string to internal storage as high-priority strings + void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized); + + std::string getModLanguage(const std::string & modContext); + +public: + /// validates translation of specified language for specified mod + /// returns true if localization is valid and complete + /// any error messages will be written to log file + bool validateTranslation(const std::string & language, const std::string & modContext, JsonNode const & file) const; + + /// Loads translation from provided json + /// Any entries loaded by this will have priority over texts registered normally + void loadTranslationOverrides(const std::string & language, const std::string & modContext, JsonNode const & file); + + // returns true if identifier with such name was registered, even if not translated to current language + bool identifierExists(const TextIdentifier & UID) const; + + /// add selected string to internal storage + void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized); + void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language); + + /// returns translated version of a string that can be displayed to user + template + std::string translate(std::string arg1, Args ... args) const + { + TextIdentifier id(arg1, args ...); + return deserialize(id); + } + + /// converts identifier into user-readable string + const std::string & deserialize(const TextIdentifier & identifier) const; + + /// Debug method, dumps all currently known texts into console using Json-like format + void dumpAllTexts(); + + /// Add or override subcontainer which can store identifiers + void addSubContainer(const TextLocalizationContainer & container); + + /// Remove subcontainer with give name + void removeSubContainer(const TextLocalizationContainer & container); + + void jsonSerialize(JsonNode & dest) const; + + template + void serialize(Handler & h, const int Version) + { + std::string key; + auto sz = stringsLocalizations.size(); + h & sz; + if(h.saving) + { + for(auto s : stringsLocalizations) + { + key = s.first; + h & key; + h & s.second; + } + } + else + { + for(size_t i = 0; i < sz; ++i) + { + h & key; + h & stringsLocalizations[key]; + } + } + } +}; + +/// Handles all text-related data in game +class DLL_LINKAGE CGeneralTextHandler: public TextLocalizationContainer +{ + void readToVector(const std::string & sourceID, const std::string & sourceName); + + /// number of scenarios in specific campaign. TODO: move to a better location + std::vector scenariosCountPerCampaign; + +public: + LegacyTextContainer allTexts; + + LegacyTextContainer arraytxt; + LegacyTextContainer primarySkillNames; + LegacyTextContainer jktexts; + LegacyTextContainer heroscrn; + LegacyTextContainer overview;//text for Kingdom Overview window + LegacyTextContainer colors; //names of player colors ("red",...) + LegacyTextContainer capColors; //names of player colors with first letter capitalized ("Red",...) + LegacyTextContainer turnDurations; //turn durations for pregame (1 Minute ... Unlimited) + + //towns + LegacyTextContainer tcommands, hcommands, fcommands; //texts for town screen, town hall screen and fort screen + LegacyTextContainer tavernInfo; + LegacyTextContainer tavernRumors; + + LegacyTextContainer qeModCommands; + + LegacyHelpContainer zelp; + LegacyTextContainer lossCondtions; + LegacyTextContainer victoryConditions; + + //objects + LegacyTextContainer advobtxt; + LegacyTextContainer restypes; //names of resources + LegacyTextContainer randsign; + LegacyTextContainer seerEmpty; + LegacyTextContainer seerNames; + LegacyTextContainer tentColors; + + //sec skills + LegacyTextContainer levels; + //commanders + LegacyTextContainer znpc00; //more or less useful content of that file + + std::vector findStringsWithPrefix(const std::string & prefix); + + int32_t pluralText(int32_t textIndex, int32_t count) const; + + size_t getCampaignLength(size_t campaignID) const; + + CGeneralTextHandler(); + CGeneralTextHandler(const CGeneralTextHandler&) = delete; + CGeneralTextHandler operator=(const CGeneralTextHandler&) = delete; + + /// Attempts to detect encoding & language of H3 files + static void detectInstallParameters(); + + /// Returns name of language preferred by user + static std::string getPreferredLanguage(); + + /// Returns name of language of Heroes III text files + static std::string getInstalledLanguage(); + + /// Returns name of encoding of Heroes III text files + static std::string getInstalledEncoding(); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index e76241e2e..5d9d0fa0d 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -1,780 +1,780 @@ -/* - * CHeroHandler.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 "CHeroHandler.h" - -#include "CGeneralTextHandler.h" -#include "filesystem/Filesystem.h" -#include "VCMI_Lib.h" -#include "JsonNode.h" -#include "constants/StringConstants.h" -#include "battle/BattleHex.h" -#include "CCreatureHandler.h" -#include "GameSettings.h" -#include "CTownHandler.h" -#include "CSkillHandler.h" -#include "BattleFieldHandler.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/IdentifierStorage.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CHero::CHero() = default; -CHero::~CHero() = default; - -int32_t CHero::getIndex() const -{ - return ID.getNum(); -} - -int32_t CHero::getIconIndex() const -{ - return imageIndex; -} - -std::string CHero::getJsonKey() const -{ - return modScope + ':' + identifier;; -} - -HeroTypeID CHero::getId() const -{ - return ID; -} - -std::string CHero::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CHero::getBiographyTranslated() const -{ - return VLC->generaltexth->translate(getBiographyTextID()); -} - -std::string CHero::getSpecialtyNameTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyNameTextID()); -} - -std::string CHero::getSpecialtyDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyDescriptionTextID()); -} - -std::string CHero::getSpecialtyTooltipTranslated() const -{ - return VLC->generaltexth->translate(getSpecialtyTooltipTextID()); -} - -std::string CHero::getNameTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "name").get(); -} - -std::string CHero::getBiographyTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "biography").get(); -} - -std::string CHero::getSpecialtyNameTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "name").get(); -} - -std::string CHero::getSpecialtyDescriptionTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "description").get(); -} - -std::string CHero::getSpecialtyTooltipTextID() const -{ - return TextIdentifier("hero", modScope, identifier, "specialty", "tooltip").get(); -} - -void CHero::registerIcons(const IconRegistar & cb) const -{ - cb(getIconIndex(), 0, "UN32", iconSpecSmall); - cb(getIconIndex(), 0, "UN44", iconSpecLarge); - cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge); - cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall); -} - -void CHero::updateFrom(const JsonNode & data) -{ - //todo: CHero::updateFrom -} - -void CHero::serializeJson(JsonSerializeFormat & handler) -{ - -} - - -SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const //picks secondary skill out from given possibilities -{ - int totalProb = 0; - for(const auto & possible : possibles) - { - totalProb += secSkillProbability[possible]; - } - if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) - { - auto ran = rand.nextInt(totalProb - 1); - for(const auto & possible : possibles) - { - ran -= secSkillProbability[possible]; - if(ran < 0) - { - return possible; - } - } - } - // FIXME: select randomly? How H3 handles such rare situation? - return *possibles.begin(); -} - -bool CHeroClass::isMagicHero() const -{ - return affinity == MAGIC; -} - -EAlignment CHeroClass::getAlignment() const -{ - return VLC->factions()->getByIndex(faction)->getAlignment(); -} - -int32_t CHeroClass::getIndex() const -{ - return id.getNum(); -} - -int32_t CHeroClass::getIconIndex() const -{ - return getIndex(); -} - -std::string CHeroClass::getJsonKey() const -{ - return modScope + ':' + identifier;; -} - -HeroClassID CHeroClass::getId() const -{ - return id; -} - -void CHeroClass::registerIcons(const IconRegistar & cb) const -{ - -} - -std::string CHeroClass::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CHeroClass::getNameTextID() const -{ - return TextIdentifier("heroClass", modScope, identifier, "name").get(); -} - -void CHeroClass::updateFrom(const JsonNode & data) -{ - //TODO: CHeroClass::updateFrom -} - -void CHeroClass::serializeJson(JsonSerializeFormat & handler) -{ - -} - -CHeroClass::CHeroClass(): - faction(0), - affinity(0), - defaultTavernChance(0), - commander(nullptr) -{ -} - -void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const -{ - const auto & skillName = NPrimarySkill::names[static_cast(pSkill)]; - auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); - //minimal value is 0 for attack and defense and 1 for spell power and knowledge - auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; - - if(currentPrimarySkillValue < primarySkillLegalMinimum) - { - logMod->error("Hero class '%s' has incorrect initial value '%d' for skill '%s'. Value '%d' will be used instead.", - heroClass->getNameTranslated(), currentPrimarySkillValue, skillName, primarySkillLegalMinimum); - currentPrimarySkillValue = primarySkillLegalMinimum; - } - heroClass->primarySkillInitial.push_back(currentPrimarySkillValue); - heroClass->primarySkillLowLevel.push_back(static_cast(node["lowLevelChance"][skillName].Float())); - heroClass->primarySkillHighLevel.push_back(static_cast(node["highLevelChance"][skillName].Float())); -} - -const std::vector & CHeroClassHandler::getTypeNames() const -{ - static const std::vector typeNames = { "heroClass" }; - return typeNames; -} - -CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - assert(!scope.empty()); - - std::string affinityStr[2] = { "might", "magic" }; - - auto * heroClass = new CHeroClass(); - - heroClass->id = HeroClassID(index); - heroClass->identifier = identifier; - heroClass->modScope = scope; - heroClass->imageBattleFemale = AnimationPath::fromJson(node["animation"]["battle"]["female"]); - heroClass->imageBattleMale = AnimationPath::fromJson(node["animation"]["battle"]["male"]); - //MODS COMPATIBILITY FOR 0.96 - heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); - heroClass->imageMapMale = node["animation"]["map"]["male"].String(); - - VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); - - heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); - - fillPrimarySkillData(node, heroClass, PrimarySkill::ATTACK); - fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); - fillPrimarySkillData(node, heroClass, PrimarySkill::SPELL_POWER); - fillPrimarySkillData(node, heroClass, PrimarySkill::KNOWLEDGE); - - auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); - if(percentSumm != 100) - logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); - - percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); - if(percentSumm != 100) - logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); - - for(auto skillPair : node["secondarySkills"].Struct()) - { - int probability = static_cast(skillPair.second.Integer()); - VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) - { - if(heroClass->secSkillProbability.size() <= skillID) - heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later - heroClass->secSkillProbability[skillID] = probability; - }); - } - - VLC->identifiers()->requestIdentifier ("creature", node["commander"], - [=](si32 commanderID) - { - heroClass->commander = VLC->creh->objects[commanderID]; - }); - - heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); - for(const auto & tavern : node["tavern"].Struct()) - { - int value = static_cast(tavern.second.Float()); - - VLC->identifiers()->requestIdentifier(tavern.second.meta, "faction", tavern.first, - [=](si32 factionID) - { - heroClass->selectionProbability[FactionID(factionID)] = value; - }); - } - - VLC->identifiers()->requestIdentifier("faction", node["faction"], - [=](si32 factionID) - { - heroClass->faction.setNum(factionID); - }); - - VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) - { - JsonNode classConf = node["mapObject"]; - classConf["heroClass"].String() = identifier; - classConf.setMeta(scope); - VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); - }); - - return heroClass; -} - -std::vector CHeroClassHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO_CLASS); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser parser(TextPath::builtin("DATA/HCTRAITS.TXT")); - - parser.endLine(); // header - parser.endLine(); - - for (size_t i=0; i set selection probability if it was not set before in tavern entries - for(CHeroClass * heroClass : objects) - { - for(CFaction * faction : VLC->townh->objects) - { - if (!faction->town) - continue; - if (heroClass->selectionProbability.count(faction->getId())) - continue; - - auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); - heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it - } - // set default probabilities for gaining secondary skills where not loaded previously - heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); - for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) - { - if(heroClass->secSkillProbability[skillID] < 0) - { - const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; - logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); - heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity]; - } - } - } - - for(CHeroClass * hc : objects) - { - if (!hc->imageMapMale.empty()) - { - JsonNode templ; - templ["animation"].String() = hc->imageMapMale; - VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); - } - } -} - -std::vector CHeroClassHandler::getDefaultAllowed() const -{ - return std::vector(size(), true); -} - -CHeroClassHandler::~CHeroClassHandler() = default; - -CHeroHandler::~CHeroHandler() = default; - -CHeroHandler::CHeroHandler() -{ - loadExperience(); -} - -const std::vector & CHeroHandler::getTypeNames() const -{ - static const std::vector typeNames = { "hero" }; - return typeNames; -} - -CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - assert(!scope.empty()); - - auto * hero = new CHero(); - hero->ID = HeroTypeID(index); - hero->identifier = identifier; - hero->modScope = scope; - hero->gender = node["female"].Bool() ? EHeroGender::FEMALE : EHeroGender::MALE; - hero->special = node["special"].Bool(); - //Default - both false - hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); - hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool(); - - VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String()); - VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String()); - VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String()); - - hero->iconSpecSmall = node["images"]["specialtySmall"].String(); - hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); - hero->portraitSmall = node["images"]["small"].String(); - hero->portraitLarge = node["images"]["large"].String(); - hero->battleImage = AnimationPath::fromJson(node["battleImage"]); - - loadHeroArmy(hero, node); - loadHeroSkills(hero, node); - loadHeroSpecialty(hero, node); - - VLC->identifiers()->requestIdentifier("heroClass", node["class"], - [=](si32 classID) - { - hero->heroClass = classes[HeroClassID(classID)]; - }); - - return hero; -} - -void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const -{ - assert(node["army"].Vector().size() <= 3); // anything bigger is useless - army initialization uses up to 3 slots - - hero->initialArmy.resize(node["army"].Vector().size()); - - for (size_t i=0; i< hero->initialArmy.size(); i++) - { - const JsonNode & source = node["army"].Vector()[i]; - - hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); - hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); - - assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); - - VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) - { - hero->initialArmy[i].creature = CreatureID(creature); - }); - } -} - -void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const -{ - for(const JsonNode &set : node["skills"].Vector()) - { - int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); - if (skillLevel < SecSkillLevel::LEVELS_SIZE) - { - size_t currentIndex = hero->secSkillsInit.size(); - hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); - - VLC->identifiers()->requestIdentifier("skill", set["skill"], [=](si32 id) - { - hero->secSkillsInit[currentIndex].first = SecondarySkill(id); - }); - } - else - { - logMod->error("Unknown skill level: %s", set["level"].String()); - } - } - - // spellbook is considered present if hero have "spellbook" entry even when this is an empty set (0 spells) - hero->haveSpellBook = !node["spellbook"].isNull(); - - for(const JsonNode & spell : node["spellbook"].Vector()) - { - VLC->identifiers()->requestIdentifier("spell", spell, - [=](si32 spellID) - { - hero->spells.insert(SpellID(spellID)); - }); - } -} - -/// creates standard H3 hero specialty for creatures -static std::vector> createCreatureSpecialty(CreatureID baseCreatureID) -{ - std::vector> result; - std::set targets; - targets.insert(baseCreatureID); - - // go through entire upgrade chain and collect all creatures to which baseCreatureID can be upgraded - for (;;) - { - std::set oldTargets = targets; - - for (auto const & upgradeSourceID : oldTargets) - { - const CCreature * upgradeSource = VLC->creh->objects[upgradeSourceID]; - targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); - } - - if (oldTargets.size() == targets.size()) - break; - } - - for(CreatureID cid : targets) - { - const CCreature &specCreature = *VLC->creh->objects[cid]; - int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; - - { - std::shared_ptr bonus = std::make_shared(); - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->type = BonusType::STACKS_SPEED; - bonus->val = 1; - result.push_back(bonus); - } - - { - std::shared_ptr bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = static_cast(PrimarySkill::ATTACK); - bonus->val = 0; - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); - result.push_back(bonus); - } - - { - std::shared_ptr bonus = std::make_shared(); - bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = static_cast(PrimarySkill::DEFENSE); - bonus->val = 0; - bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); - bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); - result.push_back(bonus); - } - } - - return result; -} - -void CHeroHandler::beforeValidate(JsonNode & object) -{ - //handle "base" specialty info - JsonNode & specialtyNode = object["specialty"]; - if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) - { - const JsonNode & base = specialtyNode["base"]; - if(!base.isNull()) - { - if(specialtyNode["bonuses"].isNull()) - { - logMod->warn("specialty has base without bonuses"); - } - else - { - JsonMap & bonuses = specialtyNode["bonuses"].Struct(); - for(std::pair keyValue : bonuses) - JsonUtils::inherit(bonuses[keyValue.first], base); - } - } - } -} - -void CHeroHandler::afterLoadFinalization() -{ - for (auto const & functor : callAfterLoadFinalization) - functor(); - - callAfterLoadFinalization.clear(); -} - -void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) -{ - auto prepSpec = [=](std::shared_ptr bonus) - { - bonus->duration = BonusDuration::PERMANENT; - bonus->source = BonusSource::HERO_SPECIAL; - bonus->sid = hero->getIndex(); - return bonus; - }; - - //new format, using bonus system - const JsonNode & specialtyNode = node["specialty"]; - if(specialtyNode.getType() != JsonNode::JsonType::DATA_STRUCT) - { - logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated()); - return; - } - - //creature specialty - alias for simplicity - if(!specialtyNode["creature"].isNull()) - { - JsonNode creatureNode = specialtyNode["creature"]; - - std::function specialtyLoader = [creatureNode, hero, prepSpec] - { - VLC->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) - { - for (const auto & bonus : createCreatureSpecialty(CreatureID(creature))) - hero->specialty.push_back(prepSpec(bonus)); - }); - }; - - callAfterLoadFinalization.push_back(specialtyLoader); - } - - for(const auto & keyValue : specialtyNode["bonuses"].Struct()) - hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second))); -} - -void CHeroHandler::loadExperience() -{ - expPerLevel.push_back(0); - expPerLevel.push_back(1000); - expPerLevel.push_back(2000); - expPerLevel.push_back(3200); - expPerLevel.push_back(4600); - expPerLevel.push_back(6200); - expPerLevel.push_back(8000); - expPerLevel.push_back(10000); - expPerLevel.push_back(12200); - expPerLevel.push_back(14700); - expPerLevel.push_back(17500); - expPerLevel.push_back(20600); - expPerLevel.push_back(24320); - expPerLevel.push_back(28784); - expPerLevel.push_back(34140); - while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) - { - auto i = expPerLevel.size() - 1; - auto diff = expPerLevel[i] - expPerLevel[i-1]; - diff += diff / 5; - expPerLevel.push_back (expPerLevel[i] + diff); - } - expPerLevel.pop_back();//last value is broken -} - -/// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) -static std::string genRefName(std::string input) -{ - boost::algorithm::replace_all(input, " ", ""); //remove spaces - input[0] = std::tolower(input[0]); // to camelCase - return input; -} - -std::vector CHeroHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO); - - objects.resize(dataSize); - std::vector h3Data; - h3Data.reserve(dataSize); - - CLegacyConfigParser specParser(TextPath::builtin("DATA/HEROSPEC.TXT")); - CLegacyConfigParser bioParser(TextPath::builtin("DATA/HEROBIOS.TXT")); - CLegacyConfigParser parser(TextPath::builtin("DATA/HOTRAITS.TXT")); - - parser.endLine(); //ignore header - parser.endLine(); - - specParser.endLine(); //ignore header - specParser.endLine(); - - for (int i=0; iimageIndex = static_cast(index) + specialFramesCount; - - objects.emplace_back(object); - - registerObject(scope, "hero", name, object->getIndex()); -} - -void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto * object = loadFromJson(scope, data, name, index); - object->imageIndex = static_cast(index); - - assert(objects[index] == nullptr); // ensure that this id was not loaded before - objects[index] = object; - - registerObject(scope, "hero", name, object->getIndex()); -} - -ui32 CHeroHandler::level (ui64 experience) const -{ - return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); -} - -ui64 CHeroHandler::reqExp (ui32 level) const -{ - if(!level) - return 0; - - if (level <= expPerLevel.size()) - { - return expPerLevel[level-1]; - } - else - { - logGlobal->warn("A hero has reached unsupported amount of experience"); - return expPerLevel[expPerLevel.size()-1]; - } -} - -std::vector CHeroHandler::getDefaultAllowed() const -{ - // Look Data/HOTRAITS.txt for reference - std::vector allowedHeroes; - allowedHeroes.reserve(size()); - - for(const CHero * hero : objects) - { - allowedHeroes.push_back(hero && !hero->special); - } - - return allowedHeroes; -} - -VCMI_LIB_NAMESPACE_END +/* + * CHeroHandler.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 "CHeroHandler.h" + +#include "CGeneralTextHandler.h" +#include "filesystem/Filesystem.h" +#include "VCMI_Lib.h" +#include "JsonNode.h" +#include "constants/StringConstants.h" +#include "battle/BattleHex.h" +#include "CCreatureHandler.h" +#include "GameSettings.h" +#include "CTownHandler.h" +#include "CSkillHandler.h" +#include "BattleFieldHandler.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/IdentifierStorage.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CHero::CHero() = default; +CHero::~CHero() = default; + +int32_t CHero::getIndex() const +{ + return ID.getNum(); +} + +int32_t CHero::getIconIndex() const +{ + return imageIndex; +} + +std::string CHero::getJsonKey() const +{ + return modScope + ':' + identifier;; +} + +HeroTypeID CHero::getId() const +{ + return ID; +} + +std::string CHero::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CHero::getBiographyTranslated() const +{ + return VLC->generaltexth->translate(getBiographyTextID()); +} + +std::string CHero::getSpecialtyNameTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyNameTextID()); +} + +std::string CHero::getSpecialtyDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyDescriptionTextID()); +} + +std::string CHero::getSpecialtyTooltipTranslated() const +{ + return VLC->generaltexth->translate(getSpecialtyTooltipTextID()); +} + +std::string CHero::getNameTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "name").get(); +} + +std::string CHero::getBiographyTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "biography").get(); +} + +std::string CHero::getSpecialtyNameTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "name").get(); +} + +std::string CHero::getSpecialtyDescriptionTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "description").get(); +} + +std::string CHero::getSpecialtyTooltipTextID() const +{ + return TextIdentifier("hero", modScope, identifier, "specialty", "tooltip").get(); +} + +void CHero::registerIcons(const IconRegistar & cb) const +{ + cb(getIconIndex(), 0, "UN32", iconSpecSmall); + cb(getIconIndex(), 0, "UN44", iconSpecLarge); + cb(getIconIndex(), 0, "PORTRAITSLARGE", portraitLarge); + cb(getIconIndex(), 0, "PORTRAITSSMALL", portraitSmall); +} + +void CHero::updateFrom(const JsonNode & data) +{ + //todo: CHero::updateFrom +} + +void CHero::serializeJson(JsonSerializeFormat & handler) +{ + +} + + +SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const //picks secondary skill out from given possibilities +{ + int totalProb = 0; + for(const auto & possible : possibles) + { + totalProb += secSkillProbability[possible]; + } + if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) + { + auto ran = rand.nextInt(totalProb - 1); + for(const auto & possible : possibles) + { + ran -= secSkillProbability[possible]; + if(ran < 0) + { + return possible; + } + } + } + // FIXME: select randomly? How H3 handles such rare situation? + return *possibles.begin(); +} + +bool CHeroClass::isMagicHero() const +{ + return affinity == MAGIC; +} + +EAlignment CHeroClass::getAlignment() const +{ + return VLC->factions()->getByIndex(faction)->getAlignment(); +} + +int32_t CHeroClass::getIndex() const +{ + return id.getNum(); +} + +int32_t CHeroClass::getIconIndex() const +{ + return getIndex(); +} + +std::string CHeroClass::getJsonKey() const +{ + return modScope + ':' + identifier;; +} + +HeroClassID CHeroClass::getId() const +{ + return id; +} + +void CHeroClass::registerIcons(const IconRegistar & cb) const +{ + +} + +std::string CHeroClass::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CHeroClass::getNameTextID() const +{ + return TextIdentifier("heroClass", modScope, identifier, "name").get(); +} + +void CHeroClass::updateFrom(const JsonNode & data) +{ + //TODO: CHeroClass::updateFrom +} + +void CHeroClass::serializeJson(JsonSerializeFormat & handler) +{ + +} + +CHeroClass::CHeroClass(): + faction(0), + affinity(0), + defaultTavernChance(0), + commander(nullptr) +{ +} + +void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const +{ + const auto & skillName = NPrimarySkill::names[static_cast(pSkill)]; + auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); + //minimal value is 0 for attack and defense and 1 for spell power and knowledge + auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; + + if(currentPrimarySkillValue < primarySkillLegalMinimum) + { + logMod->error("Hero class '%s' has incorrect initial value '%d' for skill '%s'. Value '%d' will be used instead.", + heroClass->getNameTranslated(), currentPrimarySkillValue, skillName, primarySkillLegalMinimum); + currentPrimarySkillValue = primarySkillLegalMinimum; + } + heroClass->primarySkillInitial.push_back(currentPrimarySkillValue); + heroClass->primarySkillLowLevel.push_back(static_cast(node["lowLevelChance"][skillName].Float())); + heroClass->primarySkillHighLevel.push_back(static_cast(node["highLevelChance"][skillName].Float())); +} + +const std::vector & CHeroClassHandler::getTypeNames() const +{ + static const std::vector typeNames = { "heroClass" }; + return typeNames; +} + +CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + assert(!scope.empty()); + + std::string affinityStr[2] = { "might", "magic" }; + + auto * heroClass = new CHeroClass(); + + heroClass->id = HeroClassID(index); + heroClass->identifier = identifier; + heroClass->modScope = scope; + heroClass->imageBattleFemale = AnimationPath::fromJson(node["animation"]["battle"]["female"]); + heroClass->imageBattleMale = AnimationPath::fromJson(node["animation"]["battle"]["male"]); + //MODS COMPATIBILITY FOR 0.96 + heroClass->imageMapFemale = node["animation"]["map"]["female"].String(); + heroClass->imageMapMale = node["animation"]["map"]["male"].String(); + + VLC->generaltexth->registerString(scope, heroClass->getNameTextID(), node["name"].String()); + + heroClass->affinity = vstd::find_pos(affinityStr, node["affinity"].String()); + + fillPrimarySkillData(node, heroClass, PrimarySkill::ATTACK); + fillPrimarySkillData(node, heroClass, PrimarySkill::DEFENSE); + fillPrimarySkillData(node, heroClass, PrimarySkill::SPELL_POWER); + fillPrimarySkillData(node, heroClass, PrimarySkill::KNOWLEDGE); + + auto percentSumm = std::accumulate(heroClass->primarySkillLowLevel.begin(), heroClass->primarySkillLowLevel.end(), 0); + if(percentSumm != 100) + logMod->error("Hero class %s has wrong lowLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + + percentSumm = std::accumulate(heroClass->primarySkillHighLevel.begin(), heroClass->primarySkillHighLevel.end(), 0); + if(percentSumm != 100) + logMod->error("Hero class %s has wrong highLevelChance values: summ should be 100, but %d instead", heroClass->identifier, percentSumm); + + for(auto skillPair : node["secondarySkills"].Struct()) + { + int probability = static_cast(skillPair.second.Integer()); + VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) + { + if(heroClass->secSkillProbability.size() <= skillID) + heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later + heroClass->secSkillProbability[skillID] = probability; + }); + } + + VLC->identifiers()->requestIdentifier ("creature", node["commander"], + [=](si32 commanderID) + { + heroClass->commander = VLC->creh->objects[commanderID]; + }); + + heroClass->defaultTavernChance = static_cast(node["defaultTavern"].Float()); + for(const auto & tavern : node["tavern"].Struct()) + { + int value = static_cast(tavern.second.Float()); + + VLC->identifiers()->requestIdentifier(tavern.second.meta, "faction", tavern.first, + [=](si32 factionID) + { + heroClass->selectionProbability[FactionID(factionID)] = value; + }); + } + + VLC->identifiers()->requestIdentifier("faction", node["faction"], + [=](si32 factionID) + { + heroClass->faction.setNum(factionID); + }); + + VLC->identifiers()->requestIdentifier(scope, "object", "hero", [=](si32 index) + { + JsonNode classConf = node["mapObject"]; + classConf["heroClass"].String() = identifier; + classConf.setMeta(scope); + VLC->objtypeh->loadSubObject(identifier, classConf, index, heroClass->getIndex()); + }); + + return heroClass; +} + +std::vector CHeroClassHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO_CLASS); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser parser(TextPath::builtin("DATA/HCTRAITS.TXT")); + + parser.endLine(); // header + parser.endLine(); + + for (size_t i=0; i set selection probability if it was not set before in tavern entries + for(CHeroClass * heroClass : objects) + { + for(CFaction * faction : VLC->townh->objects) + { + if (!faction->town) + continue; + if (heroClass->selectionProbability.count(faction->getId())) + continue; + + auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); + heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it + } + // set default probabilities for gaining secondary skills where not loaded previously + heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); + for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) + { + if(heroClass->secSkillProbability[skillID] < 0) + { + const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; + logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); + heroClass->secSkillProbability[skillID] = skill->gainChance[heroClass->affinity]; + } + } + } + + for(CHeroClass * hc : objects) + { + if (!hc->imageMapMale.empty()) + { + JsonNode templ; + templ["animation"].String() = hc->imageMapMale; + VLC->objtypeh->getHandlerFor(Obj::HERO, hc->getIndex())->addTemplate(templ); + } + } +} + +std::vector CHeroClassHandler::getDefaultAllowed() const +{ + return std::vector(size(), true); +} + +CHeroClassHandler::~CHeroClassHandler() = default; + +CHeroHandler::~CHeroHandler() = default; + +CHeroHandler::CHeroHandler() +{ + loadExperience(); +} + +const std::vector & CHeroHandler::getTypeNames() const +{ + static const std::vector typeNames = { "hero" }; + return typeNames; +} + +CHero * CHeroHandler::loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + assert(!scope.empty()); + + auto * hero = new CHero(); + hero->ID = HeroTypeID(index); + hero->identifier = identifier; + hero->modScope = scope; + hero->gender = node["female"].Bool() ? EHeroGender::FEMALE : EHeroGender::MALE; + hero->special = node["special"].Bool(); + //Default - both false + hero->onlyOnWaterMap = node["onlyOnWaterMap"].Bool(); + hero->onlyOnMapWithoutWater = node["onlyOnMapWithoutWater"].Bool(); + + VLC->generaltexth->registerString(scope, hero->getNameTextID(), node["texts"]["name"].String()); + VLC->generaltexth->registerString(scope, hero->getBiographyTextID(), node["texts"]["biography"].String()); + VLC->generaltexth->registerString(scope, hero->getSpecialtyNameTextID(), node["texts"]["specialty"]["name"].String()); + VLC->generaltexth->registerString(scope, hero->getSpecialtyTooltipTextID(), node["texts"]["specialty"]["tooltip"].String()); + VLC->generaltexth->registerString(scope, hero->getSpecialtyDescriptionTextID(), node["texts"]["specialty"]["description"].String()); + + hero->iconSpecSmall = node["images"]["specialtySmall"].String(); + hero->iconSpecLarge = node["images"]["specialtyLarge"].String(); + hero->portraitSmall = node["images"]["small"].String(); + hero->portraitLarge = node["images"]["large"].String(); + hero->battleImage = AnimationPath::fromJson(node["battleImage"]); + + loadHeroArmy(hero, node); + loadHeroSkills(hero, node); + loadHeroSpecialty(hero, node); + + VLC->identifiers()->requestIdentifier("heroClass", node["class"], + [=](si32 classID) + { + hero->heroClass = classes[HeroClassID(classID)]; + }); + + return hero; +} + +void CHeroHandler::loadHeroArmy(CHero * hero, const JsonNode & node) const +{ + assert(node["army"].Vector().size() <= 3); // anything bigger is useless - army initialization uses up to 3 slots + + hero->initialArmy.resize(node["army"].Vector().size()); + + for (size_t i=0; i< hero->initialArmy.size(); i++) + { + const JsonNode & source = node["army"].Vector()[i]; + + hero->initialArmy[i].minAmount = static_cast(source["min"].Float()); + hero->initialArmy[i].maxAmount = static_cast(source["max"].Float()); + + assert(hero->initialArmy[i].minAmount <= hero->initialArmy[i].maxAmount); + + VLC->identifiers()->requestIdentifier("creature", source["creature"], [=](si32 creature) + { + hero->initialArmy[i].creature = CreatureID(creature); + }); + } +} + +void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const +{ + for(const JsonNode &set : node["skills"].Vector()) + { + int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); + if (skillLevel < SecSkillLevel::LEVELS_SIZE) + { + size_t currentIndex = hero->secSkillsInit.size(); + hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); + + VLC->identifiers()->requestIdentifier("skill", set["skill"], [=](si32 id) + { + hero->secSkillsInit[currentIndex].first = SecondarySkill(id); + }); + } + else + { + logMod->error("Unknown skill level: %s", set["level"].String()); + } + } + + // spellbook is considered present if hero have "spellbook" entry even when this is an empty set (0 spells) + hero->haveSpellBook = !node["spellbook"].isNull(); + + for(const JsonNode & spell : node["spellbook"].Vector()) + { + VLC->identifiers()->requestIdentifier("spell", spell, + [=](si32 spellID) + { + hero->spells.insert(SpellID(spellID)); + }); + } +} + +/// creates standard H3 hero specialty for creatures +static std::vector> createCreatureSpecialty(CreatureID baseCreatureID) +{ + std::vector> result; + std::set targets; + targets.insert(baseCreatureID); + + // go through entire upgrade chain and collect all creatures to which baseCreatureID can be upgraded + for (;;) + { + std::set oldTargets = targets; + + for (auto const & upgradeSourceID : oldTargets) + { + const CCreature * upgradeSource = VLC->creh->objects[upgradeSourceID]; + targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); + } + + if (oldTargets.size() == targets.size()) + break; + } + + for(CreatureID cid : targets) + { + const CCreature &specCreature = *VLC->creh->objects[cid]; + int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; + + { + std::shared_ptr bonus = std::make_shared(); + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->type = BonusType::STACKS_SPEED; + bonus->val = 1; + result.push_back(bonus); + } + + { + std::shared_ptr bonus = std::make_shared(); + bonus->type = BonusType::PRIMARY_SKILL; + bonus->subtype = static_cast(PrimarySkill::ATTACK); + bonus->val = 0; + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); + result.push_back(bonus); + } + + { + std::shared_ptr bonus = std::make_shared(); + bonus->type = BonusType::PRIMARY_SKILL; + bonus->subtype = static_cast(PrimarySkill::DEFENSE); + bonus->val = 0; + bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); + bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); + result.push_back(bonus); + } + } + + return result; +} + +void CHeroHandler::beforeValidate(JsonNode & object) +{ + //handle "base" specialty info + JsonNode & specialtyNode = object["specialty"]; + if(specialtyNode.getType() == JsonNode::JsonType::DATA_STRUCT) + { + const JsonNode & base = specialtyNode["base"]; + if(!base.isNull()) + { + if(specialtyNode["bonuses"].isNull()) + { + logMod->warn("specialty has base without bonuses"); + } + else + { + JsonMap & bonuses = specialtyNode["bonuses"].Struct(); + for(std::pair keyValue : bonuses) + JsonUtils::inherit(bonuses[keyValue.first], base); + } + } + } +} + +void CHeroHandler::afterLoadFinalization() +{ + for (auto const & functor : callAfterLoadFinalization) + functor(); + + callAfterLoadFinalization.clear(); +} + +void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) +{ + auto prepSpec = [=](std::shared_ptr bonus) + { + bonus->duration = BonusDuration::PERMANENT; + bonus->source = BonusSource::HERO_SPECIAL; + bonus->sid = hero->getIndex(); + return bonus; + }; + + //new format, using bonus system + const JsonNode & specialtyNode = node["specialty"]; + if(specialtyNode.getType() != JsonNode::JsonType::DATA_STRUCT) + { + logMod->error("Unsupported speciality format for hero %s!", hero->getNameTranslated()); + return; + } + + //creature specialty - alias for simplicity + if(!specialtyNode["creature"].isNull()) + { + JsonNode creatureNode = specialtyNode["creature"]; + + std::function specialtyLoader = [creatureNode, hero, prepSpec] + { + VLC->identifiers()->requestIdentifier("creature", creatureNode, [hero, prepSpec](si32 creature) + { + for (const auto & bonus : createCreatureSpecialty(CreatureID(creature))) + hero->specialty.push_back(prepSpec(bonus)); + }); + }; + + callAfterLoadFinalization.push_back(specialtyLoader); + } + + for(const auto & keyValue : specialtyNode["bonuses"].Struct()) + hero->specialty.push_back(prepSpec(JsonUtils::parseBonus(keyValue.second))); +} + +void CHeroHandler::loadExperience() +{ + expPerLevel.push_back(0); + expPerLevel.push_back(1000); + expPerLevel.push_back(2000); + expPerLevel.push_back(3200); + expPerLevel.push_back(4600); + expPerLevel.push_back(6200); + expPerLevel.push_back(8000); + expPerLevel.push_back(10000); + expPerLevel.push_back(12200); + expPerLevel.push_back(14700); + expPerLevel.push_back(17500); + expPerLevel.push_back(20600); + expPerLevel.push_back(24320); + expPerLevel.push_back(28784); + expPerLevel.push_back(34140); + while (expPerLevel[expPerLevel.size() - 1] > expPerLevel[expPerLevel.size() - 2]) + { + auto i = expPerLevel.size() - 1; + auto diff = expPerLevel[i] - expPerLevel[i-1]; + diff += diff / 5; + expPerLevel.push_back (expPerLevel[i] + diff); + } + expPerLevel.pop_back();//last value is broken +} + +/// convert h3-style ID (e.g. Gobin Wolf Rider) to vcmi (e.g. goblinWolfRider) +static std::string genRefName(std::string input) +{ + boost::algorithm::replace_all(input, " ", ""); //remove spaces + input[0] = std::tolower(input[0]); // to camelCase + return input; +} + +std::vector CHeroHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_HERO); + + objects.resize(dataSize); + std::vector h3Data; + h3Data.reserve(dataSize); + + CLegacyConfigParser specParser(TextPath::builtin("DATA/HEROSPEC.TXT")); + CLegacyConfigParser bioParser(TextPath::builtin("DATA/HEROBIOS.TXT")); + CLegacyConfigParser parser(TextPath::builtin("DATA/HOTRAITS.TXT")); + + parser.endLine(); //ignore header + parser.endLine(); + + specParser.endLine(); //ignore header + specParser.endLine(); + + for (int i=0; iimageIndex = static_cast(index) + specialFramesCount; + + objects.emplace_back(object); + + registerObject(scope, "hero", name, object->getIndex()); +} + +void CHeroHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto * object = loadFromJson(scope, data, name, index); + object->imageIndex = static_cast(index); + + assert(objects[index] == nullptr); // ensure that this id was not loaded before + objects[index] = object; + + registerObject(scope, "hero", name, object->getIndex()); +} + +ui32 CHeroHandler::level (ui64 experience) const +{ + return static_cast(boost::range::upper_bound(expPerLevel, experience) - std::begin(expPerLevel)); +} + +ui64 CHeroHandler::reqExp (ui32 level) const +{ + if(!level) + return 0; + + if (level <= expPerLevel.size()) + { + return expPerLevel[level-1]; + } + else + { + logGlobal->warn("A hero has reached unsupported amount of experience"); + return expPerLevel[expPerLevel.size()-1]; + } +} + +std::vector CHeroHandler::getDefaultAllowed() const +{ + // Look Data/HOTRAITS.txt for reference + std::vector allowedHeroes; + allowedHeroes.reserve(size()); + + for(const CHero * hero : objects) + { + allowedHeroes.push_back(hero && !hero->special); + } + + return allowedHeroes; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 86f328f8c..fbd5c24b3 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -1,283 +1,283 @@ -/* - * CHeroHandler.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 -#include -#include -#include - -#include "ConstTransitivePtr.h" -#include "GameConstants.h" -#include "bonuses/Bonus.h" -#include "bonuses/BonusList.h" -#include "IHandlerBase.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CHeroClass; -class CGHeroInstance; -struct BattleHex; -class JsonNode; -class CRandomGenerator; -class JsonSerializeFormat; -class BattleField; - -enum class EHeroGender : uint8_t -{ - MALE = 0, - FEMALE = 1, - DEFAULT = 0xff // from h3m, instance has same gender as hero type -}; - -class DLL_LINKAGE CHero : public HeroType -{ - friend class CHeroHandler; - - HeroTypeID ID; - std::string identifier; - std::string modScope; - -public: - struct InitialArmyStack - { - ui32 minAmount; - ui32 maxAmount; - CreatureID creature; - - template void serialize(Handler &h, const int version) - { - h & minAmount; - h & maxAmount; - h & creature; - } - }; - si32 imageIndex = 0; - - std::vector initialArmy; - - CHeroClass * heroClass{}; - std::vector > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) - BonusList specialty; - std::set spells; - bool haveSpellBook = false; - bool special = false; // hero is special and won't be placed in game (unless preset on map), e.g. campaign heroes - bool onlyOnWaterMap; // hero will be placed only if the map contains water - bool onlyOnMapWithoutWater; // hero will be placed only if the map does not contain water - EHeroGender gender = EHeroGender::MALE; // default sex: 0=male, 1=female - - /// Graphics - std::string iconSpecSmall; - std::string iconSpecLarge; - std::string portraitSmall; - std::string portraitLarge; - AnimationPath battleImage; - - CHero(); - virtual ~CHero(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - HeroTypeID getId() const override; - void registerIcons(const IconRegistar & cb) const override; - - std::string getNameTranslated() const override; - std::string getBiographyTranslated() const override; - std::string getSpecialtyNameTranslated() const override; - std::string getSpecialtyDescriptionTranslated() const override; - std::string getSpecialtyTooltipTranslated() const override; - - std::string getNameTextID() const override; - std::string getBiographyTextID() const override; - std::string getSpecialtyNameTextID() const override; - std::string getSpecialtyDescriptionTextID() const override; - std::string getSpecialtyTooltipTextID() const override; - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & imageIndex; - h & initialArmy; - h & heroClass; - h & secSkillsInit; - h & specialty; - h & spells; - h & haveSpellBook; - h & gender; - h & special; - h & onlyOnWaterMap; - h & onlyOnMapWithoutWater; - h & iconSpecSmall; - h & iconSpecLarge; - h & portraitSmall; - h & portraitLarge; - h & identifier; - h & modScope; - h & battleImage; - } -}; - -class DLL_LINKAGE CHeroClass : public HeroClass -{ - friend class CHeroClassHandler; - HeroClassID id; // use getId instead - std::string modScope; - std::string identifier; // use getJsonKey instead - -public: - enum EClassAffinity - { - MIGHT, - MAGIC - }; - - //double aggression; // not used in vcmi. - FactionID faction; - ui8 affinity; // affinity, using EClassAffinity enum - - // default chance for hero of specific class to appear in tavern, if field "tavern" was not set - // resulting chance = sqrt(town.chance * heroClass.chance) - ui32 defaultTavernChance; - - CCreature * commander; - - std::vector primarySkillInitial; // initial primary skills - std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level - std::vector primarySkillHighLevel;// same for high levels (> 10) - - std::vector secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order - - std::map selectionProbability; //probability of selection in towns - - AnimationPath imageBattleMale; - AnimationPath imageBattleFemale; - std::string imageMapMale; - std::string imageMapFemale; - - CHeroClass(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - HeroClassID getId() const override; - void registerIcons(const IconRegistar & cb) const override; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - - bool isMagicHero() const; - SecondarySkill chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const; //picks secondary skill out from given possibilities - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler & h, const int version) - { - h & modScope; - h & identifier; - h & faction; - h & id; - h & defaultTavernChance; - h & primarySkillInitial; - h & primarySkillLowLevel; - h & primarySkillHighLevel; - h & secSkillProbability; - h & selectionProbability; - h & affinity; - h & commander; - h & imageBattleMale; - h & imageBattleFemale; - h & imageMapMale; - h & imageMapFemale; - - if(!h.saving) - { - for(int & i : secSkillProbability) - vstd::amax(i, 0); - } - } - EAlignment getAlignment() const; -}; - -class DLL_LINKAGE CHeroClassHandler : public CHandlerBase -{ - void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const; - -public: - std::vector loadLegacyData() override; - - void afterLoadFinalization() override; - - std::vector getDefaultAllowed() const override; - - ~CHeroClassHandler(); - - template void serialize(Handler &h, const int version) - { - h & objects; - } - -protected: - const std::vector & getTypeNames() const override; - CHeroClass * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; - -}; - -class DLL_LINKAGE CHeroHandler : public CHandlerBase -{ - /// expPerLEvel[i] is amount of exp needed to reach level i; - /// consists of 201 values. Any higher levels require experience larger that ui64 can hold - std::vector expPerLevel; - - /// helpers for loading to avoid huge load functions - void loadHeroArmy(CHero * hero, const JsonNode & node) const; - void loadHeroSkills(CHero * hero, const JsonNode & node) const; - void loadHeroSpecialty(CHero * hero, const JsonNode & node); - - void loadExperience(); - - std::vector> callAfterLoadFinalization; - -public: - CHeroClassHandler classes; - - ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount - ui64 reqExp(ui32 level) const; //calculates experience required for given level - - std::vector loadLegacyData() override; - - void beforeValidate(JsonNode & object) override; - void loadObject(std::string scope, std::string name, const JsonNode & data) override; - void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void afterLoadFinalization() override; - - CHeroHandler(); - ~CHeroHandler(); - - std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - h & classes; - h & objects; - h & expPerLevel; - } - -protected: - const std::vector & getTypeNames() const override; - CHero * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CHeroHandler.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 +#include +#include +#include + +#include "ConstTransitivePtr.h" +#include "GameConstants.h" +#include "bonuses/Bonus.h" +#include "bonuses/BonusList.h" +#include "IHandlerBase.h" +#include "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CHeroClass; +class CGHeroInstance; +struct BattleHex; +class JsonNode; +class CRandomGenerator; +class JsonSerializeFormat; +class BattleField; + +enum class EHeroGender : uint8_t +{ + MALE = 0, + FEMALE = 1, + DEFAULT = 0xff // from h3m, instance has same gender as hero type +}; + +class DLL_LINKAGE CHero : public HeroType +{ + friend class CHeroHandler; + + HeroTypeID ID; + std::string identifier; + std::string modScope; + +public: + struct InitialArmyStack + { + ui32 minAmount; + ui32 maxAmount; + CreatureID creature; + + template void serialize(Handler &h, const int version) + { + h & minAmount; + h & maxAmount; + h & creature; + } + }; + si32 imageIndex = 0; + + std::vector initialArmy; + + CHeroClass * heroClass{}; + std::vector > secSkillsInit; //initial secondary skills; first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert) + BonusList specialty; + std::set spells; + bool haveSpellBook = false; + bool special = false; // hero is special and won't be placed in game (unless preset on map), e.g. campaign heroes + bool onlyOnWaterMap; // hero will be placed only if the map contains water + bool onlyOnMapWithoutWater; // hero will be placed only if the map does not contain water + EHeroGender gender = EHeroGender::MALE; // default sex: 0=male, 1=female + + /// Graphics + std::string iconSpecSmall; + std::string iconSpecLarge; + std::string portraitSmall; + std::string portraitLarge; + AnimationPath battleImage; + + CHero(); + virtual ~CHero(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + HeroTypeID getId() const override; + void registerIcons(const IconRegistar & cb) const override; + + std::string getNameTranslated() const override; + std::string getBiographyTranslated() const override; + std::string getSpecialtyNameTranslated() const override; + std::string getSpecialtyDescriptionTranslated() const override; + std::string getSpecialtyTooltipTranslated() const override; + + std::string getNameTextID() const override; + std::string getBiographyTextID() const override; + std::string getSpecialtyNameTextID() const override; + std::string getSpecialtyDescriptionTextID() const override; + std::string getSpecialtyTooltipTextID() const override; + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & ID; + h & imageIndex; + h & initialArmy; + h & heroClass; + h & secSkillsInit; + h & specialty; + h & spells; + h & haveSpellBook; + h & gender; + h & special; + h & onlyOnWaterMap; + h & onlyOnMapWithoutWater; + h & iconSpecSmall; + h & iconSpecLarge; + h & portraitSmall; + h & portraitLarge; + h & identifier; + h & modScope; + h & battleImage; + } +}; + +class DLL_LINKAGE CHeroClass : public HeroClass +{ + friend class CHeroClassHandler; + HeroClassID id; // use getId instead + std::string modScope; + std::string identifier; // use getJsonKey instead + +public: + enum EClassAffinity + { + MIGHT, + MAGIC + }; + + //double aggression; // not used in vcmi. + FactionID faction; + ui8 affinity; // affinity, using EClassAffinity enum + + // default chance for hero of specific class to appear in tavern, if field "tavern" was not set + // resulting chance = sqrt(town.chance * heroClass.chance) + ui32 defaultTavernChance; + + CCreature * commander; + + std::vector primarySkillInitial; // initial primary skills + std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level + std::vector primarySkillHighLevel;// same for high levels (> 10) + + std::vector secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order + + std::map selectionProbability; //probability of selection in towns + + AnimationPath imageBattleMale; + AnimationPath imageBattleFemale; + std::string imageMapMale; + std::string imageMapFemale; + + CHeroClass(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + HeroClassID getId() const override; + void registerIcons(const IconRegistar & cb) const override; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + + bool isMagicHero() const; + SecondarySkill chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const; //picks secondary skill out from given possibilities + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler & h, const int version) + { + h & modScope; + h & identifier; + h & faction; + h & id; + h & defaultTavernChance; + h & primarySkillInitial; + h & primarySkillLowLevel; + h & primarySkillHighLevel; + h & secSkillProbability; + h & selectionProbability; + h & affinity; + h & commander; + h & imageBattleMale; + h & imageBattleFemale; + h & imageMapMale; + h & imageMapFemale; + + if(!h.saving) + { + for(int & i : secSkillProbability) + vstd::amax(i, 0); + } + } + EAlignment getAlignment() const; +}; + +class DLL_LINKAGE CHeroClassHandler : public CHandlerBase +{ + void fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const; + +public: + std::vector loadLegacyData() override; + + void afterLoadFinalization() override; + + std::vector getDefaultAllowed() const override; + + ~CHeroClassHandler(); + + template void serialize(Handler &h, const int version) + { + h & objects; + } + +protected: + const std::vector & getTypeNames() const override; + CHeroClass * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; + +}; + +class DLL_LINKAGE CHeroHandler : public CHandlerBase +{ + /// expPerLEvel[i] is amount of exp needed to reach level i; + /// consists of 201 values. Any higher levels require experience larger that ui64 can hold + std::vector expPerLevel; + + /// helpers for loading to avoid huge load functions + void loadHeroArmy(CHero * hero, const JsonNode & node) const; + void loadHeroSkills(CHero * hero, const JsonNode & node) const; + void loadHeroSpecialty(CHero * hero, const JsonNode & node); + + void loadExperience(); + + std::vector> callAfterLoadFinalization; + +public: + CHeroClassHandler classes; + + ui32 level(ui64 experience) const; //calculates level corresponding to given experience amount + ui64 reqExp(ui32 level) const; //calculates experience required for given level + + std::vector loadLegacyData() override; + + void beforeValidate(JsonNode & object) override; + void loadObject(std::string scope, std::string name, const JsonNode & data) override; + void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; + void afterLoadFinalization() override; + + CHeroHandler(); + ~CHeroHandler(); + + std::vector getDefaultAllowed() const override; + + template void serialize(Handler &h, const int version) + { + h & classes; + h & objects; + h & expPerLevel; + } + +protected: + const std::vector & getTypeNames() const override; + CHero * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CScriptingModule.h b/lib/CScriptingModule.h index f615021a4..b76d60ed5 100644 --- a/lib/CScriptingModule.h +++ b/lib/CScriptingModule.h @@ -1,54 +1,54 @@ -/* - * CScriptingModule.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 - -#if SCRIPTING_ENABLED -#include - -VCMI_LIB_NAMESPACE_BEGIN - -namespace spells -{ - namespace effects - { - class Registry; - } -} - -namespace scripting -{ - -class DLL_LINKAGE ContextBase : public Context -{ -public: - ContextBase(vstd::CLoggerBase * logger_); - virtual ~ContextBase() = default; - -protected: - vstd::CLoggerBase * logger; -}; - -class DLL_LINKAGE Module -{ -public: - Module() = default; - virtual ~Module() = default; - - virtual std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const = 0; - - virtual std::shared_ptr createContextFor(const Script * source, const Environment * env) const = 0; - - virtual void registerSpellEffect(spells::effects::Registry * registry, const Script * source) const = 0; -}; - -} - -VCMI_LIB_NAMESPACE_END -#endif +/* + * CScriptingModule.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 + +#if SCRIPTING_ENABLED +#include + +VCMI_LIB_NAMESPACE_BEGIN + +namespace spells +{ + namespace effects + { + class Registry; + } +} + +namespace scripting +{ + +class DLL_LINKAGE ContextBase : public Context +{ +public: + ContextBase(vstd::CLoggerBase * logger_); + virtual ~ContextBase() = default; + +protected: + vstd::CLoggerBase * logger; +}; + +class DLL_LINKAGE Module +{ +public: + Module() = default; + virtual ~Module() = default; + + virtual std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const = 0; + + virtual std::shared_ptr createContextFor(const Script * source, const Environment * env) const = 0; + + virtual void registerSpellEffect(spells::effects::Registry * registry, const Script * source) const = 0; +}; + +} + +VCMI_LIB_NAMESPACE_END +#endif diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index da5c8777b..29cac52a7 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -1,129 +1,129 @@ -/* - * CSkillHandler.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 -#include - -#include "../lib/bonuses/Bonus.h" -#include "GameConstants.h" -#include "IHandlerBase.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonSerializeFormat; - -class DLL_LINKAGE CSkill : public Skill -{ -public: - struct LevelInfo - { - std::string iconSmall; - std::string iconMedium; - std::string iconLarge; - std::vector> effects; - - template void serialize(Handler & h, const int version) - { - h & iconSmall; - h & iconMedium; - h & iconLarge; - h & effects; - } - }; - -private: - std::vector levels; // bonuses provided by basic, advanced and expert level - void addNewBonus(const std::shared_ptr & b, int level); - - SecondarySkill id; - std::string modScope; - std::string identifier; - -public: - CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); - ~CSkill() = default; - - enum class Obligatory : ui8 - { - MAJOR = 0, - MINOR = 1, - }; - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - SecondarySkill getId() const override; - - std::string getNameTextID() const override; - std::string getNameTranslated() const override; - - std::string getDescriptionTextID(int level) const override; - std::string getDescriptionTranslated(int level) const override; - - const LevelInfo & at(int level) const; - LevelInfo & at(int level); - - std::string toString() const; - bool obligatory(Obligatory val) const { return val == Obligatory::MAJOR ? obligatoryMajor : obligatoryMinor; }; - - std::array gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - bool onlyOnWaterMap; - - template void serialize(Handler & h, const int version) - { - h & id; - h & identifier; - h & gainChance; - h & levels; - h & obligatoryMajor; - h & obligatoryMinor; - h & onlyOnWaterMap; - } - - friend class CSkillHandler; - friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); - friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); -private: - bool obligatoryMajor; - bool obligatoryMinor; -}; - -class DLL_LINKAGE CSkillHandler: public CHandlerBase -{ -public: - ///IHandler base - std::vector loadLegacyData() override; - void afterLoadFinalization() override; - void beforeValidate(JsonNode & object) override; - - std::vector getDefaultAllowed() const override; - - ///json serialization helpers - static si32 decodeSkill(const std::string & identifier); - static std::string encodeSkill(const si32 index); - static std::string encodeSkillWithType(const si32 index); - - template void serialize(Handler & h, const int version) - { - h & objects; - } - -protected: - const std::vector & getTypeNames() const override; - CSkill * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CSkillHandler.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 +#include + +#include "../lib/bonuses/Bonus.h" +#include "GameConstants.h" +#include "IHandlerBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonSerializeFormat; + +class DLL_LINKAGE CSkill : public Skill +{ +public: + struct LevelInfo + { + std::string iconSmall; + std::string iconMedium; + std::string iconLarge; + std::vector> effects; + + template void serialize(Handler & h, const int version) + { + h & iconSmall; + h & iconMedium; + h & iconLarge; + h & effects; + } + }; + +private: + std::vector levels; // bonuses provided by basic, advanced and expert level + void addNewBonus(const std::shared_ptr & b, int level); + + SecondarySkill id; + std::string modScope; + std::string identifier; + +public: + CSkill(const SecondarySkill & id = SecondarySkill::DEFAULT, std::string identifier = "default", bool obligatoryMajor = false, bool obligatoryMinor = false); + ~CSkill() = default; + + enum class Obligatory : ui8 + { + MAJOR = 0, + MINOR = 1, + }; + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + SecondarySkill getId() const override; + + std::string getNameTextID() const override; + std::string getNameTranslated() const override; + + std::string getDescriptionTextID(int level) const override; + std::string getDescriptionTranslated(int level) const override; + + const LevelInfo & at(int level) const; + LevelInfo & at(int level); + + std::string toString() const; + bool obligatory(Obligatory val) const { return val == Obligatory::MAJOR ? obligatoryMajor : obligatoryMinor; }; + + std::array gainChance; // gainChance[0/1] = default gain chance on level-up for might/magic heroes + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + bool onlyOnWaterMap; + + template void serialize(Handler & h, const int version) + { + h & id; + h & identifier; + h & gainChance; + h & levels; + h & obligatoryMajor; + h & obligatoryMinor; + h & onlyOnWaterMap; + } + + friend class CSkillHandler; + friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); + friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); +private: + bool obligatoryMajor; + bool obligatoryMinor; +}; + +class DLL_LINKAGE CSkillHandler: public CHandlerBase +{ +public: + ///IHandler base + std::vector loadLegacyData() override; + void afterLoadFinalization() override; + void beforeValidate(JsonNode & object) override; + + std::vector getDefaultAllowed() const override; + + ///json serialization helpers + static si32 decodeSkill(const std::string & identifier); + static std::string encodeSkill(const si32 index); + static std::string encodeSkillWithType(const si32 index); + + template void serialize(Handler & h, const int version) + { + h & objects; + } + +protected: + const std::vector & getTypeNames() const override; + CSkill * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CSoundBase.h b/lib/CSoundBase.h index 99edd485b..881296a10 100644 --- a/lib/CSoundBase.h +++ b/lib/CSoundBase.h @@ -1,1051 +1,1051 @@ -/* - * CSoundBase.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 - -VCMI_LIB_NAMESPACE_BEGIN - -// Use some magic to keep the list of files and their code name in sync. - -#define VCMI_SOUND_LIST \ -/* Sounds for map actions */ \ -VCMI_SOUND_NAME(KillFade) VCMI_SOUND_FILE(KILLFADE.wav) /* hero or monster disappears */ \ -/* Other sounds (TODO: separate out the sounds for units, spells and the rest */ \ -VCMI_SOUND_NAME(AAGLAttack) VCMI_SOUND_FILE(AAGLATTK.wav) \ -VCMI_SOUND_NAME(AAGLDefend) VCMI_SOUND_FILE(AAGLDFND.wav) \ -VCMI_SOUND_NAME(AAGLKill) VCMI_SOUND_FILE(AAGLKILL.wav) \ -VCMI_SOUND_NAME(AAGLMove) VCMI_SOUND_FILE(AAGLMOVE.wav) \ -VCMI_SOUND_NAME(AAGLWNCE) VCMI_SOUND_FILE(AAGLWNCE.wav) \ -VCMI_SOUND_NAME(acid) VCMI_SOUND_FILE(ACID.wav) \ -VCMI_SOUND_NAME(ADVLAttack) VCMI_SOUND_FILE(ADVLATTK.wav) \ -VCMI_SOUND_NAME(ADVLDefend) VCMI_SOUND_FILE(ADVLDFND.wav) \ -VCMI_SOUND_NAME(ADVLEXT1) VCMI_SOUND_FILE(ADVLEXT1.wav) \ -VCMI_SOUND_NAME(ADVLEXT2) VCMI_SOUND_FILE(ADVLEXT2.wav) \ -VCMI_SOUND_NAME(ADVLKill) VCMI_SOUND_FILE(ADVLKILL.wav) \ -VCMI_SOUND_NAME(ADVLMove) VCMI_SOUND_FILE(ADVLMOVE.wav) \ -VCMI_SOUND_NAME(ADVLWNCE) VCMI_SOUND_FILE(ADVLWNCE.wav) \ -VCMI_SOUND_NAME(AELMAttack) VCMI_SOUND_FILE(AELMATTK.wav) \ -VCMI_SOUND_NAME(AELMDefend) VCMI_SOUND_FILE(AELMDFND.wav) \ -VCMI_SOUND_NAME(AELMKill) VCMI_SOUND_FILE(AELMKILL.wav) \ -VCMI_SOUND_NAME(AELMMove) VCMI_SOUND_FILE(AELMMOVE.wav) \ -VCMI_SOUND_NAME(AELMWNCE) VCMI_SOUND_FILE(AELMWNCE.wav) \ -VCMI_SOUND_NAME(AGE) VCMI_SOUND_FILE(AGE.wav) \ -VCMI_SOUND_NAME(AGRMAttack) VCMI_SOUND_FILE(AGRMATTK.wav) \ -VCMI_SOUND_NAME(AGRMDefend) VCMI_SOUND_FILE(AGRMDFND.wav) \ -VCMI_SOUND_NAME(AGRMKill) VCMI_SOUND_FILE(AGRMKILL.wav) \ -VCMI_SOUND_NAME(AGRMMove) VCMI_SOUND_FILE(AGRMMOVE.wav) \ -VCMI_SOUND_NAME(AGRMShot) VCMI_SOUND_FILE(AGRMSHOT.wav) \ -VCMI_SOUND_NAME(AGRMWNCE) VCMI_SOUND_FILE(AGRMWNCE.wav) \ -VCMI_SOUND_NAME(AIRSHELD) VCMI_SOUND_FILE(AIRSHELD.wav) \ -VCMI_SOUND_NAME(ALIZAttack) VCMI_SOUND_FILE(ALIZATTK.wav) \ -VCMI_SOUND_NAME(ALIZDefend) VCMI_SOUND_FILE(ALIZDFND.wav) \ -VCMI_SOUND_NAME(ALIZKill) VCMI_SOUND_FILE(ALIZKILL.wav) \ -VCMI_SOUND_NAME(ALIZMove) VCMI_SOUND_FILE(ALIZMOVE.wav) \ -VCMI_SOUND_NAME(ALIZShot) VCMI_SOUND_FILE(ALIZSHOT.wav) \ -VCMI_SOUND_NAME(ALIZWNCE) VCMI_SOUND_FILE(ALIZWNCE.wav) \ -VCMI_SOUND_NAME(AMAGAttack) VCMI_SOUND_FILE(AMAGATTK.wav) \ -VCMI_SOUND_NAME(AMAGDefend) VCMI_SOUND_FILE(AMAGDFND.wav) \ -VCMI_SOUND_NAME(AMAGKill) VCMI_SOUND_FILE(AMAGKILL.wav) \ -VCMI_SOUND_NAME(AMAGMove) VCMI_SOUND_FILE(AMAGMOVE.wav) \ -VCMI_SOUND_NAME(AMAGShot) VCMI_SOUND_FILE(AMAGSHOT.wav) \ -VCMI_SOUND_NAME(AMAGWNCE) VCMI_SOUND_FILE(AMAGWNCE.wav) \ -VCMI_SOUND_NAME(ANGLAttack) VCMI_SOUND_FILE(ANGLATTK.wav) \ -VCMI_SOUND_NAME(ANGLDefend) VCMI_SOUND_FILE(ANGLDFND.wav) \ -VCMI_SOUND_NAME(ANGLKill) VCMI_SOUND_FILE(ANGLKILL.wav) \ -VCMI_SOUND_NAME(ANGLMove) VCMI_SOUND_FILE(ANGLMOVE.wav) \ -VCMI_SOUND_NAME(ANGLWNCE) VCMI_SOUND_FILE(ANGLWNCE.wav) \ -VCMI_SOUND_NAME(ANIMDEAD) VCMI_SOUND_FILE(ANIMDEAD.wav) \ -VCMI_SOUND_NAME(ANTIMAGK) VCMI_SOUND_FILE(ANTIMAGK.wav) \ -VCMI_SOUND_NAME(APEGAttack) VCMI_SOUND_FILE(APEGATTK.wav) \ -VCMI_SOUND_NAME(APEGDefend) VCMI_SOUND_FILE(APEGDFND.wav) \ -VCMI_SOUND_NAME(APEGKill) VCMI_SOUND_FILE(APEGKILL.wav) \ -VCMI_SOUND_NAME(APEGMove) VCMI_SOUND_FILE(APEGMOVE.wav) \ -VCMI_SOUND_NAME(APEGWNCE) VCMI_SOUND_FILE(APEGWNCE.wav) \ -VCMI_SOUND_NAME(ARMGEDN) VCMI_SOUND_FILE(ARMGEDN.wav) \ -VCMI_SOUND_NAME(AZURAttack) VCMI_SOUND_FILE(AZURATTK.wav) \ -VCMI_SOUND_NAME(AZURDefend) VCMI_SOUND_FILE(AZURDFND.wav) \ -VCMI_SOUND_NAME(AZURKill) VCMI_SOUND_FILE(AZURKILL.wav) \ -VCMI_SOUND_NAME(AZURMove) VCMI_SOUND_FILE(AZURMOVE.wav) \ -VCMI_SOUND_NAME(AZURWNCE) VCMI_SOUND_FILE(AZURWNCE.wav) \ -VCMI_SOUND_NAME(BACKLASH) VCMI_SOUND_FILE(BACKLASH.wav) \ -VCMI_SOUND_NAME(BADLUCK) VCMI_SOUND_FILE(BADLUCK.wav) \ -VCMI_SOUND_NAME(BADMRLE) VCMI_SOUND_FILE(BADMRLE.wav) \ -VCMI_SOUND_NAME(BALLKill) VCMI_SOUND_FILE(BALLKILL.wav) \ -VCMI_SOUND_NAME(BALLShot) VCMI_SOUND_FILE(BALLSHOT.wav) \ -VCMI_SOUND_NAME(BALLWNCE) VCMI_SOUND_FILE(BALLWNCE.wav) \ -VCMI_SOUND_NAME(BASLAttack) VCMI_SOUND_FILE(BASLATTK.wav) \ -VCMI_SOUND_NAME(BASLDefend) VCMI_SOUND_FILE(BASLDFND.wav) \ -VCMI_SOUND_NAME(BASLKill) VCMI_SOUND_FILE(BASLKILL.wav) \ -VCMI_SOUND_NAME(BASLMove) VCMI_SOUND_FILE(BASLMOVE.wav) \ -VCMI_SOUND_NAME(BASLWNCE) VCMI_SOUND_FILE(BASLWNCE.wav) \ -VCMI_SOUND_NAME(battle00) VCMI_SOUND_FILE(BATTLE00.wav) \ -VCMI_SOUND_NAME(battle01) VCMI_SOUND_FILE(BATTLE01.wav) \ -VCMI_SOUND_NAME(battle02) VCMI_SOUND_FILE(BATTLE02.wav) \ -VCMI_SOUND_NAME(battle03) VCMI_SOUND_FILE(BATTLE03.wav) \ -VCMI_SOUND_NAME(battle04) VCMI_SOUND_FILE(BATTLE04.wav) \ -VCMI_SOUND_NAME(battle05) VCMI_SOUND_FILE(BATTLE05.wav) \ -VCMI_SOUND_NAME(battle06) VCMI_SOUND_FILE(BATTLE06.wav) \ -VCMI_SOUND_NAME(battle07) VCMI_SOUND_FILE(BATTLE07.wav) \ -VCMI_SOUND_NAME(BDRFAttack) VCMI_SOUND_FILE(BDRFATTK.wav) \ -VCMI_SOUND_NAME(BDRFDefend) VCMI_SOUND_FILE(BDRFDFND.wav) \ -VCMI_SOUND_NAME(BDRFKill) VCMI_SOUND_FILE(BDRFKILL.wav) \ -VCMI_SOUND_NAME(BDRFMove) VCMI_SOUND_FILE(BDRFMOVE.wav) \ -VCMI_SOUND_NAME(BDRFWNCE) VCMI_SOUND_FILE(BDRFWNCE.wav) \ -VCMI_SOUND_NAME(BERSERK) VCMI_SOUND_FILE(BERSERK.wav) \ -VCMI_SOUND_NAME(BGORAttack) VCMI_SOUND_FILE(BGORATTK.wav) \ -VCMI_SOUND_NAME(BGORDefend) VCMI_SOUND_FILE(BGORDFND.wav) \ -VCMI_SOUND_NAME(BGORKill) VCMI_SOUND_FILE(BGORKILL.wav) \ -VCMI_SOUND_NAME(BGORMove) VCMI_SOUND_FILE(BGORMOVE.wav) \ -VCMI_SOUND_NAME(BGORWNCE) VCMI_SOUND_FILE(BGORWNCE.wav) \ -VCMI_SOUND_NAME(BHDRAttack) VCMI_SOUND_FILE(BHDRATTK.wav) \ -VCMI_SOUND_NAME(BHDRDETH) VCMI_SOUND_FILE(BHDRDETH.wav) \ -VCMI_SOUND_NAME(BHDRDefend) VCMI_SOUND_FILE(BHDRDFND.wav) \ -VCMI_SOUND_NAME(BHDRKill) VCMI_SOUND_FILE(BHDRKILL.wav) \ -VCMI_SOUND_NAME(BHDRMove) VCMI_SOUND_FILE(BHDRMOVE.wav) \ -VCMI_SOUND_NAME(BHDRShot) VCMI_SOUND_FILE(BHDRSHOT.wav) \ -VCMI_SOUND_NAME(BHDRWNCE) VCMI_SOUND_FILE(BHDRWNCE.wav) \ -VCMI_SOUND_NAME(BIND) VCMI_SOUND_FILE(BIND.wav) \ -VCMI_SOUND_NAME(BKDRAttack) VCMI_SOUND_FILE(BKDRATTK.wav) \ -VCMI_SOUND_NAME(BKDRDefend) VCMI_SOUND_FILE(BKDRDFND.wav) \ -VCMI_SOUND_NAME(BKDRKill) VCMI_SOUND_FILE(BKDRKILL.wav) \ -VCMI_SOUND_NAME(BKDRMove) VCMI_SOUND_FILE(BKDRMOVE.wav) \ -VCMI_SOUND_NAME(BKDRWNCE) VCMI_SOUND_FILE(BKDRWNCE.wav) \ -VCMI_SOUND_NAME(BKNTAttack) VCMI_SOUND_FILE(BKNTATTK.wav) \ -VCMI_SOUND_NAME(BKNTDefend) VCMI_SOUND_FILE(BKNTDFND.wav) \ -VCMI_SOUND_NAME(BKNTKill) VCMI_SOUND_FILE(BKNTKILL.wav) \ -VCMI_SOUND_NAME(BKNTMove) VCMI_SOUND_FILE(BKNTMOVE.wav) \ -VCMI_SOUND_NAME(BKNTWNCE) VCMI_SOUND_FILE(BKNTWNCE.wav) \ -VCMI_SOUND_NAME(bless) VCMI_SOUND_FILE(BLESS.wav) \ -VCMI_SOUND_NAME(blind) VCMI_SOUND_FILE(BLIND.wav) \ -VCMI_SOUND_NAME(bloodlus) VCMI_SOUND_FILE(BLOODLUS.wav) \ -VCMI_SOUND_NAME(BLRDAttack) VCMI_SOUND_FILE(BLRDATTK.wav) \ -VCMI_SOUND_NAME(BLRDDefend) VCMI_SOUND_FILE(BLRDDFND.wav) \ -VCMI_SOUND_NAME(BLRDKill) VCMI_SOUND_FILE(BLRDKILL.wav) \ -VCMI_SOUND_NAME(BLRDMove) VCMI_SOUND_FILE(BLRDMOVE.wav) \ -VCMI_SOUND_NAME(BLRDWNCE) VCMI_SOUND_FILE(BLRDWNCE.wav) \ -VCMI_SOUND_NAME(BMTHAttack) VCMI_SOUND_FILE(BMTHATTK.wav) \ -VCMI_SOUND_NAME(BMTHDefend) VCMI_SOUND_FILE(BMTHDFND.wav) \ -VCMI_SOUND_NAME(BMTHKill) VCMI_SOUND_FILE(BMTHKILL.wav) \ -VCMI_SOUND_NAME(BMTHMove) VCMI_SOUND_FILE(BMTHMOVE.wav) \ -VCMI_SOUND_NAME(BMTHWNCE) VCMI_SOUND_FILE(BMTHWNCE.wav) \ -VCMI_SOUND_NAME(BOARAttack) VCMI_SOUND_FILE(BOARATTK.wav) \ -VCMI_SOUND_NAME(BOARDefend) VCMI_SOUND_FILE(BOARDFND.wav) \ -VCMI_SOUND_NAME(BOARKill) VCMI_SOUND_FILE(BOARKILL.wav) \ -VCMI_SOUND_NAME(BOARMove) VCMI_SOUND_FILE(BOARMOVE.wav) \ -VCMI_SOUND_NAME(BOARWNCE) VCMI_SOUND_FILE(BOARWNCE.wav) \ -VCMI_SOUND_NAME(BODRAttack) VCMI_SOUND_FILE(BODRATTK.wav) \ -VCMI_SOUND_NAME(BODRDefend) VCMI_SOUND_FILE(BODRDFND.wav) \ -VCMI_SOUND_NAME(BODRKill) VCMI_SOUND_FILE(BODRKILL.wav) \ -VCMI_SOUND_NAME(BODRMove) VCMI_SOUND_FILE(BODRMOVE.wav) \ -VCMI_SOUND_NAME(BODRWNCE) VCMI_SOUND_FILE(BODRWNCE.wav) \ -VCMI_SOUND_NAME(BTREAttack) VCMI_SOUND_FILE(BTREATTK.wav) \ -VCMI_SOUND_NAME(BTREDefend) VCMI_SOUND_FILE(BTREDFND.wav) \ -VCMI_SOUND_NAME(BTREKill) VCMI_SOUND_FILE(BTREKILL.wav) \ -VCMI_SOUND_NAME(BTREMove) VCMI_SOUND_FILE(BTREMOVE.wav) \ -VCMI_SOUND_NAME(BTREWNCE) VCMI_SOUND_FILE(BTREWNCE.wav) \ -VCMI_SOUND_NAME(newBuilding) VCMI_SOUND_FILE(BUILDTWN.wav) \ -VCMI_SOUND_NAME(button) VCMI_SOUND_FILE(BUTTON.wav) \ -VCMI_SOUND_NAME(CALFAttack) VCMI_SOUND_FILE(CALFATTK.wav) \ -VCMI_SOUND_NAME(CALFDefend) VCMI_SOUND_FILE(CALFDFND.wav) \ -VCMI_SOUND_NAME(CALFKill) VCMI_SOUND_FILE(CALFKILL.wav) \ -VCMI_SOUND_NAME(CALFMove) VCMI_SOUND_FILE(CALFMOVE.wav) \ -VCMI_SOUND_NAME(CALFShot) VCMI_SOUND_FILE(CALFSHOT.wav) \ -VCMI_SOUND_NAME(CALFWNCE) VCMI_SOUND_FILE(CALFWNCE.wav) \ -VCMI_SOUND_NAME(CARTKill) VCMI_SOUND_FILE(CARTKILL.wav) \ -VCMI_SOUND_NAME(CARTWNCE) VCMI_SOUND_FILE(CARTWNCE.wav) \ -VCMI_SOUND_NAME(CATAKill) VCMI_SOUND_FILE(CATAKILL.wav) \ -VCMI_SOUND_NAME(CATAShot) VCMI_SOUND_FILE(CATASHOT.wav) \ -VCMI_SOUND_NAME(CATAWNCE) VCMI_SOUND_FILE(CATAWNCE.wav) \ -VCMI_SOUND_NAME(CAVAAttack) VCMI_SOUND_FILE(CAVAATTK.wav) \ -VCMI_SOUND_NAME(CAVADefend) VCMI_SOUND_FILE(CAVADFND.wav) \ -VCMI_SOUND_NAME(CAVAKill) VCMI_SOUND_FILE(CAVAKILL.wav) \ -VCMI_SOUND_NAME(CAVAMove) VCMI_SOUND_FILE(CAVAMOVE.wav) \ -VCMI_SOUND_NAME(CAVAWNCE) VCMI_SOUND_FILE(CAVAWNCE.wav) \ -VCMI_SOUND_NAME(CAVEHEAD) VCMI_SOUND_FILE(CAVEHEAD.wav) \ -VCMI_SOUND_NAME(CCYCAttack) VCMI_SOUND_FILE(CCYCATTK.wav) \ -VCMI_SOUND_NAME(CCYCDefend) VCMI_SOUND_FILE(CCYCDFND.wav) \ -VCMI_SOUND_NAME(CCYCKill) VCMI_SOUND_FILE(CCYCKILL.wav) \ -VCMI_SOUND_NAME(CCYCMove) VCMI_SOUND_FILE(CCYCMOVE.wav) \ -VCMI_SOUND_NAME(CCYCShot) VCMI_SOUND_FILE(CCYCSHOT.wav) \ -VCMI_SOUND_NAME(CCYCWNCE) VCMI_SOUND_FILE(CCYCWNCE.wav) \ -VCMI_SOUND_NAME(CERBAttack) VCMI_SOUND_FILE(CERBATTK.wav) \ -VCMI_SOUND_NAME(CERBDefend) VCMI_SOUND_FILE(CERBDFND.wav) \ -VCMI_SOUND_NAME(CERBKill) VCMI_SOUND_FILE(CERBKILL.wav) \ -VCMI_SOUND_NAME(CERBMove) VCMI_SOUND_FILE(CERBMOVE.wav) \ -VCMI_SOUND_NAME(CERBWNCE) VCMI_SOUND_FILE(CERBWNCE.wav) \ -VCMI_SOUND_NAME(CGORAttack) VCMI_SOUND_FILE(CGORATTK.wav) \ -VCMI_SOUND_NAME(CGORDefend) VCMI_SOUND_FILE(CGORDFND.wav) \ -VCMI_SOUND_NAME(CGORKill) VCMI_SOUND_FILE(CGORKILL.wav) \ -VCMI_SOUND_NAME(CGORMove) VCMI_SOUND_FILE(CGORMOVE.wav) \ -VCMI_SOUND_NAME(CGORWNCE) VCMI_SOUND_FILE(CGORWNCE.wav) \ -VCMI_SOUND_NAME(chainLigthning) VCMI_SOUND_FILE(CHAINLTE.wav) \ -VCMI_SOUND_NAME(chat) VCMI_SOUND_FILE(CHAT.wav) \ -VCMI_SOUND_NAME(chest) VCMI_SOUND_FILE(CHEST.wav) \ -VCMI_SOUND_NAME(CHMPAttack) VCMI_SOUND_FILE(CHMPATTK.wav) \ -VCMI_SOUND_NAME(CHMPDefend) VCMI_SOUND_FILE(CHMPDFND.wav) \ -VCMI_SOUND_NAME(CHMPKill) VCMI_SOUND_FILE(CHMPKILL.wav) \ -VCMI_SOUND_NAME(CHMPMove) VCMI_SOUND_FILE(CHMPMOVE.wav) \ -VCMI_SOUND_NAME(CHMPWNCE) VCMI_SOUND_FILE(CHMPWNCE.wav) \ -VCMI_SOUND_NAME(CHYDAttack) VCMI_SOUND_FILE(CHYDATTK.wav) \ -VCMI_SOUND_NAME(CHYDDefend) VCMI_SOUND_FILE(CHYDDFND.wav) \ -VCMI_SOUND_NAME(CHYDKill) VCMI_SOUND_FILE(CHYDKILL.wav) \ -VCMI_SOUND_NAME(CHYDMove) VCMI_SOUND_FILE(CHYDMOVE.wav) \ -VCMI_SOUND_NAME(CHYDWNCE) VCMI_SOUND_FILE(CHYDWNCE.wav) \ -VCMI_SOUND_NAME(CLIMAX) VCMI_SOUND_FILE(CLIMAX.wav) \ -VCMI_SOUND_NAME(CLONE) VCMI_SOUND_FILE(CLONE.wav) \ -VCMI_SOUND_NAME(CNTRAttack) VCMI_SOUND_FILE(CNTRATTK.wav) \ -VCMI_SOUND_NAME(CNTRDefend) VCMI_SOUND_FILE(CNTRDFND.wav) \ -VCMI_SOUND_NAME(CNTRKill) VCMI_SOUND_FILE(CNTRKILL.wav) \ -VCMI_SOUND_NAME(CNTRMove) VCMI_SOUND_FILE(CNTRMOVE.wav) \ -VCMI_SOUND_NAME(CNTRShot) VCMI_SOUND_FILE(CNTRSHOT.wav) \ -VCMI_SOUND_NAME(Counterstrike) VCMI_SOUND_FILE(CNTRSTRK.wav) \ -VCMI_SOUND_NAME(CNTRWNCE) VCMI_SOUND_FILE(CNTRWNCE.wav) \ -VCMI_SOUND_NAME(COLDRAY) VCMI_SOUND_FILE(COLDRAY.wav) \ -VCMI_SOUND_NAME(COLDRING) VCMI_SOUND_FILE(COLDRING.wav) \ -VCMI_SOUND_NAME(CRUSAttack) VCMI_SOUND_FILE(CRUSATTK.wav) \ -VCMI_SOUND_NAME(CRUSDefend) VCMI_SOUND_FILE(CRUSDFND.wav) \ -VCMI_SOUND_NAME(CRUSKill) VCMI_SOUND_FILE(CRUSKILL.wav) \ -VCMI_SOUND_NAME(CRUSMove) VCMI_SOUND_FILE(CRUSMOVE.wav) \ -VCMI_SOUND_NAME(CRUSWNCE) VCMI_SOUND_FILE(CRUSWNCE.wav) \ -VCMI_SOUND_NAME(CRYSAttack) VCMI_SOUND_FILE(CRYSATTK.wav) \ -VCMI_SOUND_NAME(CRYSDefend) VCMI_SOUND_FILE(CRYSDFND.wav) \ -VCMI_SOUND_NAME(CRYSKill) VCMI_SOUND_FILE(CRYSKILL.wav) \ -VCMI_SOUND_NAME(CRYSMove) VCMI_SOUND_FILE(CRYSMOVE.wav) \ -VCMI_SOUND_NAME(CRYSWNCE) VCMI_SOUND_FILE(CRYSWNCE.wav) \ -VCMI_SOUND_NAME(CURE) VCMI_SOUND_FILE(CURE.wav) \ -VCMI_SOUND_NAME(CURSE) VCMI_SOUND_FILE(CURSE.wav) \ -VCMI_SOUND_NAME(cyclopAttack) VCMI_SOUND_FILE(CYCLATTK.wav) \ -VCMI_SOUND_NAME(cyclopDefend) VCMI_SOUND_FILE(CYCLDFND.wav) \ -VCMI_SOUND_NAME(cyclopKill) VCMI_SOUND_FILE(CYCLKILL.wav) \ -VCMI_SOUND_NAME(cyclopMove) VCMI_SOUND_FILE(CYCLMOVE.wav) \ -VCMI_SOUND_NAME(cyclopShot) VCMI_SOUND_FILE(CYCLSHOT.wav) \ -VCMI_SOUND_NAME(cyclopWNCE) VCMI_SOUND_FILE(CYCLWNCE.wav) \ -VCMI_SOUND_NAME(DANGER) VCMI_SOUND_FILE(DANGER.wav) \ -VCMI_SOUND_NAME(deathBlow) VCMI_SOUND_FILE(DEATHBLO.wav) \ -VCMI_SOUND_NAME(deathCloud) VCMI_SOUND_FILE(DEATHCLD.wav) \ -VCMI_SOUND_NAME(deathRIP) VCMI_SOUND_FILE(DEATHRIP.wav) \ -VCMI_SOUND_NAME(deathSTR) VCMI_SOUND_FILE(DEATHSTR.wav) \ -VCMI_SOUND_NAME(DECAY) VCMI_SOUND_FILE(DECAY.wav) \ -VCMI_SOUND_NAME(DEFAULT) VCMI_SOUND_FILE(DEFAULT.wav) \ -VCMI_SOUND_NAME(DEVLAttack) VCMI_SOUND_FILE(DEVLATTK.wav) \ -VCMI_SOUND_NAME(DEVLDefend) VCMI_SOUND_FILE(DEVLDFND.wav) \ -VCMI_SOUND_NAME(DEVLEXT1) VCMI_SOUND_FILE(DEVLEXT1.wav) \ -VCMI_SOUND_NAME(DEVLEXT2) VCMI_SOUND_FILE(DEVLEXT2.wav) \ -VCMI_SOUND_NAME(DEVLKill) VCMI_SOUND_FILE(DEVLKILL.wav) \ -VCMI_SOUND_NAME(DEVLMove) VCMI_SOUND_FILE(DEVLMOVE.wav) \ -VCMI_SOUND_NAME(DEVLWNCE) VCMI_SOUND_FILE(DEVLWNCE.wav) \ -VCMI_SOUND_NAME(DFLYAttack) VCMI_SOUND_FILE(DFLYATTK.wav) \ -VCMI_SOUND_NAME(DFLYDefend) VCMI_SOUND_FILE(DFLYDFND.wav) \ -VCMI_SOUND_NAME(DFLYKill) VCMI_SOUND_FILE(DFLYKILL.wav) \ -VCMI_SOUND_NAME(DFLYMove) VCMI_SOUND_FILE(DFLYMOVE.wav) \ -VCMI_SOUND_NAME(DFLYWNCE) VCMI_SOUND_FILE(DFLYWNCE.wav) \ -VCMI_SOUND_NAME(DGLMAttack) VCMI_SOUND_FILE(DGLMATTK.wav) \ -VCMI_SOUND_NAME(DGLMDefend) VCMI_SOUND_FILE(DGLMDFND.wav) \ -VCMI_SOUND_NAME(DGLMKill) VCMI_SOUND_FILE(DGLMKILL.wav) \ -VCMI_SOUND_NAME(DGLMMove) VCMI_SOUND_FILE(DGLMMOVE.wav) \ -VCMI_SOUND_NAME(DGLMWNCE) VCMI_SOUND_FILE(DGLMWNCE.wav) \ -VCMI_SOUND_NAME(DHDMAttack) VCMI_SOUND_FILE(DHDMATTK.wav) \ -VCMI_SOUND_NAME(DHDMDefend) VCMI_SOUND_FILE(DHDMDFND.wav) \ -VCMI_SOUND_NAME(DHDMKill) VCMI_SOUND_FILE(DHDMKILL.wav) \ -VCMI_SOUND_NAME(DHDMMove) VCMI_SOUND_FILE(DHDMMOVE.wav) \ -VCMI_SOUND_NAME(DHDMWNCE) VCMI_SOUND_FILE(DHDMWNCE.wav) \ -VCMI_SOUND_NAME(Dig) VCMI_SOUND_FILE(DIGSOUND.wav) \ -VCMI_SOUND_NAME(DIPMAGK) VCMI_SOUND_FILE(DIPMAGK.wav) \ -VCMI_SOUND_NAME(DISEASE) VCMI_SOUND_FILE(DISEASE.wav) \ -VCMI_SOUND_NAME(DISGUISE) VCMI_SOUND_FILE(DISGUISE.wav) \ -VCMI_SOUND_NAME(DISPELL) VCMI_SOUND_FILE(DISPELL.wav) \ -VCMI_SOUND_NAME(DISRUPTR) VCMI_SOUND_FILE(DISRUPTR.wav) \ -VCMI_SOUND_NAME(dragonHall) VCMI_SOUND_FILE(DRAGON.wav) \ -VCMI_SOUND_NAME(DRAINLIF) VCMI_SOUND_FILE(DRAINLIF.wav) \ -VCMI_SOUND_NAME(DRAWBRG) VCMI_SOUND_FILE(DRAWBRG.wav) \ -VCMI_SOUND_NAME(DRGNSLAY) VCMI_SOUND_FILE(DRGNSLAY.wav) \ -VCMI_SOUND_NAME(DWRFAttack) VCMI_SOUND_FILE(DWRFATTK.wav) \ -VCMI_SOUND_NAME(DWRFDefend) VCMI_SOUND_FILE(DWRFDFND.wav) \ -VCMI_SOUND_NAME(DWRFKill) VCMI_SOUND_FILE(DWRFKILL.wav) \ -VCMI_SOUND_NAME(DWRFMove) VCMI_SOUND_FILE(DWRFMOVE.wav) \ -VCMI_SOUND_NAME(DWRFWNCE) VCMI_SOUND_FILE(DWRFWNCE.wav) \ -VCMI_SOUND_NAME(ECNTAttack) VCMI_SOUND_FILE(ECNTATTK.wav) \ -VCMI_SOUND_NAME(ECNTDefend) VCMI_SOUND_FILE(ECNTDFND.wav) \ -VCMI_SOUND_NAME(ECNTKill) VCMI_SOUND_FILE(ECNTKILL.wav) \ -VCMI_SOUND_NAME(ECNTMove) VCMI_SOUND_FILE(ECNTMOVE.wav) \ -VCMI_SOUND_NAME(ECNTWNCE) VCMI_SOUND_FILE(ECNTWNCE.wav) \ -VCMI_SOUND_NAME(EELMAttack) VCMI_SOUND_FILE(EELMATTK.wav) \ -VCMI_SOUND_NAME(EELMDefend) VCMI_SOUND_FILE(EELMDFND.wav) \ -VCMI_SOUND_NAME(EELMKill) VCMI_SOUND_FILE(EELMKILL.wav) \ -VCMI_SOUND_NAME(EELMMove) VCMI_SOUND_FILE(EELMMOVE.wav) \ -VCMI_SOUND_NAME(EELMWNCE) VCMI_SOUND_FILE(EELMWNCE.wav) \ -VCMI_SOUND_NAME(EFRTAttack) VCMI_SOUND_FILE(EFRTATTK.wav) \ -VCMI_SOUND_NAME(EFRTDefend) VCMI_SOUND_FILE(EFRTDFND.wav) \ -VCMI_SOUND_NAME(EFRTKill) VCMI_SOUND_FILE(EFRTKILL.wav) \ -VCMI_SOUND_NAME(EFRTMove) VCMI_SOUND_FILE(EFRTMOVE.wav) \ -VCMI_SOUND_NAME(EFRTWNCE) VCMI_SOUND_FILE(EFRTWNCE.wav) \ -VCMI_SOUND_NAME(ENCHAttack) VCMI_SOUND_FILE(ENCHATTK.wav) \ -VCMI_SOUND_NAME(ENCHDefend) VCMI_SOUND_FILE(ENCHDFND.wav) \ -VCMI_SOUND_NAME(ENCHKill) VCMI_SOUND_FILE(ENCHKILL.wav) \ -VCMI_SOUND_NAME(ENCHMove) VCMI_SOUND_FILE(ENCHMOVE.wav) \ -VCMI_SOUND_NAME(ENCHShot) VCMI_SOUND_FILE(ENCHSHOT.wav) \ -VCMI_SOUND_NAME(ENCHWNCE) VCMI_SOUND_FILE(ENCHWNCE.wav) \ -VCMI_SOUND_NAME(ENERAttack) VCMI_SOUND_FILE(ENERATTK.wav) \ -VCMI_SOUND_NAME(ENERDefend) VCMI_SOUND_FILE(ENERDFND.wav) \ -VCMI_SOUND_NAME(ENERKill) VCMI_SOUND_FILE(ENERKILL.wav) \ -VCMI_SOUND_NAME(ENERMove) VCMI_SOUND_FILE(ENERMOVE.wav) \ -VCMI_SOUND_NAME(ENERWNCE) VCMI_SOUND_FILE(ENERWNCE.wav) \ -VCMI_SOUND_NAME(ERTHQUAK) VCMI_SOUND_FILE(ERTHQUAK.wav) \ -VCMI_SOUND_NAME(ESULAttack) VCMI_SOUND_FILE(ESULATTK.wav) \ -VCMI_SOUND_NAME(ESULDefend) VCMI_SOUND_FILE(ESULDFND.wav) \ -VCMI_SOUND_NAME(ESULKill) VCMI_SOUND_FILE(ESULKILL.wav) \ -VCMI_SOUND_NAME(ESULMove) VCMI_SOUND_FILE(ESULMOVE.wav) \ -VCMI_SOUND_NAME(ESULShot) VCMI_SOUND_FILE(ESULSHOT.wav) \ -VCMI_SOUND_NAME(ESULWNCE) VCMI_SOUND_FILE(ESULWNCE.wav) \ -VCMI_SOUND_NAME(EVLIAttack) VCMI_SOUND_FILE(EVLIATTK.wav) \ -VCMI_SOUND_NAME(EVLIDETH) VCMI_SOUND_FILE(EVLIDETH.wav) \ -VCMI_SOUND_NAME(EVLIDefend) VCMI_SOUND_FILE(EVLIDFND.wav) \ -VCMI_SOUND_NAME(EVLIKill) VCMI_SOUND_FILE(EVLIKILL.wav) \ -VCMI_SOUND_NAME(EVLIMove) VCMI_SOUND_FILE(EVLIMOVE.wav) \ -VCMI_SOUND_NAME(EVLIShot) VCMI_SOUND_FILE(EVLISHOT.wav) \ -VCMI_SOUND_NAME(EVLIWNCE) VCMI_SOUND_FILE(EVLIWNCE.wav) \ -VCMI_SOUND_NAME(experience) VCMI_SOUND_FILE(EXPERNCE.wav) \ -VCMI_SOUND_NAME(FAERAttack) VCMI_SOUND_FILE(FAERATTK.wav) \ -VCMI_SOUND_NAME(FAERDefend) VCMI_SOUND_FILE(FAERDFND.wav) \ -VCMI_SOUND_NAME(faerie) VCMI_SOUND_FILE(FAERIE.wav) \ -VCMI_SOUND_NAME(FAERKill) VCMI_SOUND_FILE(FAERKILL.wav) \ -VCMI_SOUND_NAME(FAERMove) VCMI_SOUND_FILE(FAERMOVE.wav) \ -VCMI_SOUND_NAME(FAERShot) VCMI_SOUND_FILE(FAERSHOT.wav) \ -VCMI_SOUND_NAME(FAERWNCE) VCMI_SOUND_FILE(FAERWNCE.wav) \ -VCMI_SOUND_NAME(FAIDKill) VCMI_SOUND_FILE(FAIDKILL.wav) \ -VCMI_SOUND_NAME(FAIDWNCE) VCMI_SOUND_FILE(FAIDWNCE.wav) \ -VCMI_SOUND_NAME(FDFLAttack) VCMI_SOUND_FILE(FDFLATTK.wav) \ -VCMI_SOUND_NAME(FDFLDefend) VCMI_SOUND_FILE(FDFLDFND.wav) \ -VCMI_SOUND_NAME(FDFLKill) VCMI_SOUND_FILE(FDFLKILL.wav) \ -VCMI_SOUND_NAME(FDFLMove) VCMI_SOUND_FILE(FDFLMOVE.wav) \ -VCMI_SOUND_NAME(FDFLShot) VCMI_SOUND_FILE(FDFLSHOT.wav) \ -VCMI_SOUND_NAME(FDFLWNCE) VCMI_SOUND_FILE(FDFLWNCE.wav) \ -VCMI_SOUND_NAME(FEAR) VCMI_SOUND_FILE(FEAR.wav) \ -VCMI_SOUND_NAME(FELMAttack) VCMI_SOUND_FILE(FELMATTK.wav) \ -VCMI_SOUND_NAME(FELMDefend) VCMI_SOUND_FILE(FELMDFND.wav) \ -VCMI_SOUND_NAME(FELMKill) VCMI_SOUND_FILE(FELMKILL.wav) \ -VCMI_SOUND_NAME(FELMMove) VCMI_SOUND_FILE(FELMMOVE.wav) \ -VCMI_SOUND_NAME(FELMWNCE) VCMI_SOUND_FILE(FELMWNCE.wav) \ -VCMI_SOUND_NAME(FIRBAttack) VCMI_SOUND_FILE(FIRBATTK.wav) \ -VCMI_SOUND_NAME(FIRBDefend) VCMI_SOUND_FILE(FIRBDFND.wav) \ -VCMI_SOUND_NAME(FIRBKill) VCMI_SOUND_FILE(FIRBKILL.wav) \ -VCMI_SOUND_NAME(FIRBMove) VCMI_SOUND_FILE(FIRBMOVE.wav) \ -VCMI_SOUND_NAME(FIRBWNCE) VCMI_SOUND_FILE(FIRBWNCE.wav) \ -VCMI_SOUND_NAME(fireball) VCMI_SOUND_FILE(FIREBALL.wav) \ -VCMI_SOUND_NAME(fireblast) VCMI_SOUND_FILE(FIREBLST.wav) \ -VCMI_SOUND_NAME(FIRESHIE) VCMI_SOUND_FILE(FIRESHIE.wav) \ -VCMI_SOUND_NAME(FIRESHLD) VCMI_SOUND_FILE(FIRESHLD.wav) \ -VCMI_SOUND_NAME(fireStorm) VCMI_SOUND_FILE(FIRESTRM.wav) \ -VCMI_SOUND_NAME(fireWall) VCMI_SOUND_FILE(FIREWALL.wav) \ -VCMI_SOUND_NAME(FLAGMINE) VCMI_SOUND_FILE(FLAGMINE.wav) \ -VCMI_SOUND_NAME(FLYSPELL) VCMI_SOUND_FILE(FLYSPELL.wav) \ -VCMI_SOUND_NAME(FMLRAttack) VCMI_SOUND_FILE(FMLRATTK.wav) \ -VCMI_SOUND_NAME(FMLRDefend) VCMI_SOUND_FILE(FMLRDFND.wav) \ -VCMI_SOUND_NAME(FMLRKill) VCMI_SOUND_FILE(FMLRKILL.wav) \ -VCMI_SOUND_NAME(FMLRMove) VCMI_SOUND_FILE(FMLRMOVE.wav) \ -VCMI_SOUND_NAME(FMLRWNCE) VCMI_SOUND_FILE(FMLRWNCE.wav) \ -VCMI_SOUND_NAME(FORCEFLD) VCMI_SOUND_FILE(FORCEFLD.wav) \ -VCMI_SOUND_NAME(FORGET) VCMI_SOUND_FILE(FORGET.wav) \ -VCMI_SOUND_NAME(FORTUNE) VCMI_SOUND_FILE(FORTUNE.wav) \ -VCMI_SOUND_NAME(FRENZY) VCMI_SOUND_FILE(FRENZY.wav) \ -VCMI_SOUND_NAME(FROSTING) VCMI_SOUND_FILE(FROSTING.wav) \ -VCMI_SOUND_NAME(gazebo) VCMI_SOUND_FILE(GAZEBO.wav) \ -VCMI_SOUND_NAME(GBASAttack) VCMI_SOUND_FILE(GBASATTK.wav) \ -VCMI_SOUND_NAME(GBASDefend) VCMI_SOUND_FILE(GBASDFND.wav) \ -VCMI_SOUND_NAME(GBASKill) VCMI_SOUND_FILE(GBASKILL.wav) \ -VCMI_SOUND_NAME(GBASMove) VCMI_SOUND_FILE(GBASMOVE.wav) \ -VCMI_SOUND_NAME(GBASWNCE) VCMI_SOUND_FILE(GBASWNCE.wav) \ -VCMI_SOUND_NAME(GBLNAttack) VCMI_SOUND_FILE(GBLNATTK.wav) \ -VCMI_SOUND_NAME(GBLNDefend) VCMI_SOUND_FILE(GBLNDFND.wav) \ -VCMI_SOUND_NAME(GBLNKill) VCMI_SOUND_FILE(GBLNKILL.wav) \ -VCMI_SOUND_NAME(GBLNMove) VCMI_SOUND_FILE(GBLNMOVE.wav) \ -VCMI_SOUND_NAME(GBLNWNCE) VCMI_SOUND_FILE(GBLNWNCE.wav) \ -VCMI_SOUND_NAME(GELFAttack) VCMI_SOUND_FILE(GELFATTK.wav) \ -VCMI_SOUND_NAME(GELFDefend) VCMI_SOUND_FILE(GELFDFND.wav) \ -VCMI_SOUND_NAME(GELFKill) VCMI_SOUND_FILE(GELFKILL.wav) \ -VCMI_SOUND_NAME(GELFMove) VCMI_SOUND_FILE(GELFMOVE.wav) \ -VCMI_SOUND_NAME(GELFShot) VCMI_SOUND_FILE(GELFSHOT.wav) \ -VCMI_SOUND_NAME(GELFWNCE) VCMI_SOUND_FILE(GELFWNCE.wav) \ -VCMI_SOUND_NAME(GENIAttack) VCMI_SOUND_FILE(GENIATTK.wav) \ -VCMI_SOUND_NAME(GENIDefend) VCMI_SOUND_FILE(GENIDFND.wav) \ -VCMI_SOUND_NAME(GENIE) VCMI_SOUND_FILE(GENIE.wav) \ -VCMI_SOUND_NAME(GENIKill) VCMI_SOUND_FILE(GENIKILL.wav) \ -VCMI_SOUND_NAME(GENIMove) VCMI_SOUND_FILE(GENIMOVE.wav) \ -VCMI_SOUND_NAME(GENIWNCE) VCMI_SOUND_FILE(GENIWNCE.wav) \ -VCMI_SOUND_NAME(GETPROTECTION) VCMI_SOUND_FILE(GETPROTECTION.wav) \ -VCMI_SOUND_NAME(GGLMAttack) VCMI_SOUND_FILE(GGLMATTK.wav) \ -VCMI_SOUND_NAME(GGLMDefend) VCMI_SOUND_FILE(GGLMDFND.wav) \ -VCMI_SOUND_NAME(GGLMKill) VCMI_SOUND_FILE(GGLMKILL.wav) \ -VCMI_SOUND_NAME(GGLMMove) VCMI_SOUND_FILE(GGLMMOVE.wav) \ -VCMI_SOUND_NAME(GGLMWNCE) VCMI_SOUND_FILE(GGLMWNCE.wav) \ -VCMI_SOUND_NAME(GHDRAttack) VCMI_SOUND_FILE(GHDRATTK.wav) \ -VCMI_SOUND_NAME(GHDRDefend) VCMI_SOUND_FILE(GHDRDFND.wav) \ -VCMI_SOUND_NAME(GHDRKill) VCMI_SOUND_FILE(GHDRKILL.wav) \ -VCMI_SOUND_NAME(GHDRMove) VCMI_SOUND_FILE(GHDRMOVE.wav) \ -VCMI_SOUND_NAME(GHDRWNCE) VCMI_SOUND_FILE(GHDRWNCE.wav) \ -VCMI_SOUND_NAME(GNLMAttack) VCMI_SOUND_FILE(GNLMATTK.wav) \ -VCMI_SOUND_NAME(GNLMDefend) VCMI_SOUND_FILE(GNLMDFND.wav) \ -VCMI_SOUND_NAME(GNLMKill) VCMI_SOUND_FILE(GNLMKILL.wav) \ -VCMI_SOUND_NAME(GNLMMove) VCMI_SOUND_FILE(GNLMMOVE.wav) \ -VCMI_SOUND_NAME(GNLMWNCE) VCMI_SOUND_FILE(GNLMWNCE.wav) \ -VCMI_SOUND_NAME(GNOLAttack) VCMI_SOUND_FILE(GNOLATTK.wav) \ -VCMI_SOUND_NAME(GNOLDefend) VCMI_SOUND_FILE(GNOLDFND.wav) \ -VCMI_SOUND_NAME(GNOLKill) VCMI_SOUND_FILE(GNOLKILL.wav) \ -VCMI_SOUND_NAME(GNOLMove) VCMI_SOUND_FILE(GNOLMOVE.wav) \ -VCMI_SOUND_NAME(GNOLWNCE) VCMI_SOUND_FILE(GNOLWNCE.wav) \ -VCMI_SOUND_NAME(GODRAttack) VCMI_SOUND_FILE(GODRATTK.wav) \ -VCMI_SOUND_NAME(GODRDefend) VCMI_SOUND_FILE(GODRDFND.wav) \ -VCMI_SOUND_NAME(GODRKill) VCMI_SOUND_FILE(GODRKILL.wav) \ -VCMI_SOUND_NAME(GODRMove) VCMI_SOUND_FILE(GODRMOVE.wav) \ -VCMI_SOUND_NAME(GODRWNCE) VCMI_SOUND_FILE(GODRWNCE.wav) \ -VCMI_SOUND_NAME(GOGFLAME) VCMI_SOUND_FILE(GOGFLAME.wav) \ -VCMI_SOUND_NAME(GOGGAttack) VCMI_SOUND_FILE(GOGGATTK.wav) \ -VCMI_SOUND_NAME(GOGGDefend) VCMI_SOUND_FILE(GOGGDFND.wav) \ -VCMI_SOUND_NAME(GOGGKill) VCMI_SOUND_FILE(GOGGKILL.wav) \ -VCMI_SOUND_NAME(GOGGMove) VCMI_SOUND_FILE(GOGGMOVE.wav) \ -VCMI_SOUND_NAME(GOGGShot) VCMI_SOUND_FILE(GOGGSHOT.wav) \ -VCMI_SOUND_NAME(GOGGWNCE) VCMI_SOUND_FILE(GOGGWNCE.wav) \ -VCMI_SOUND_NAME(GOODLUCK) VCMI_SOUND_FILE(GOODLUCK.wav) \ -VCMI_SOUND_NAME(GOODMRLE) VCMI_SOUND_FILE(GOODMRLE.wav) \ -VCMI_SOUND_NAME(GRAVEYARD) VCMI_SOUND_FILE(GRAVEYARD.wav) \ -VCMI_SOUND_NAME(GRDRAttack) VCMI_SOUND_FILE(GRDRATTK.wav) \ -VCMI_SOUND_NAME(GRDRDefend) VCMI_SOUND_FILE(GRDRDFND.wav) \ -VCMI_SOUND_NAME(GRDRKill) VCMI_SOUND_FILE(GRDRKILL.wav) \ -VCMI_SOUND_NAME(GRDRMove) VCMI_SOUND_FILE(GRDRMOVE.wav) \ -VCMI_SOUND_NAME(GRDRWNCE) VCMI_SOUND_FILE(GRDRWNCE.wav) \ -VCMI_SOUND_NAME(GRIFAttack) VCMI_SOUND_FILE(GRIFATTK.wav) \ -VCMI_SOUND_NAME(GRIFDefend) VCMI_SOUND_FILE(GRIFDFND.wav) \ -VCMI_SOUND_NAME(GRIFKill) VCMI_SOUND_FILE(GRIFKILL.wav) \ -VCMI_SOUND_NAME(GRIFMove) VCMI_SOUND_FILE(GRIFMOVE.wav) \ -VCMI_SOUND_NAME(GRIFWNCE) VCMI_SOUND_FILE(GRIFWNCE.wav) \ -VCMI_SOUND_NAME(GTITAttack) VCMI_SOUND_FILE(GTITATTK.wav) \ -VCMI_SOUND_NAME(GTITDefend) VCMI_SOUND_FILE(GTITDFND.wav) \ -VCMI_SOUND_NAME(GTITKill) VCMI_SOUND_FILE(GTITKILL.wav) \ -VCMI_SOUND_NAME(GTITMove) VCMI_SOUND_FILE(GTITMOVE.wav) \ -VCMI_SOUND_NAME(GTITShot) VCMI_SOUND_FILE(GTITSHOT.wav) \ -VCMI_SOUND_NAME(GTITWNCE) VCMI_SOUND_FILE(GTITWNCE.wav) \ -VCMI_SOUND_NAME(GWRDAttack) VCMI_SOUND_FILE(GWRDATTK.wav) \ -VCMI_SOUND_NAME(GWRDDefend) VCMI_SOUND_FILE(GWRDDFND.wav) \ -VCMI_SOUND_NAME(GWRDKill) VCMI_SOUND_FILE(GWRDKILL.wav) \ -VCMI_SOUND_NAME(GWRDMove) VCMI_SOUND_FILE(GWRDMOVE.wav) \ -VCMI_SOUND_NAME(GWRDWNCE) VCMI_SOUND_FILE(GWRDWNCE.wav) \ -VCMI_SOUND_NAME(HALBAttack) VCMI_SOUND_FILE(HALBATTK.wav) \ -VCMI_SOUND_NAME(HALBDefend) VCMI_SOUND_FILE(HALBDFND.wav) \ -VCMI_SOUND_NAME(HALBKill) VCMI_SOUND_FILE(HALBKILL.wav) \ -VCMI_SOUND_NAME(HALBMove) VCMI_SOUND_FILE(HALBMOVE.wav) \ -VCMI_SOUND_NAME(HALBWNCE) VCMI_SOUND_FILE(HALBWNCE.wav) \ -VCMI_SOUND_NAME(HALFAttack) VCMI_SOUND_FILE(HALFATTK.wav) \ -VCMI_SOUND_NAME(HALFDefend) VCMI_SOUND_FILE(HALFDFND.wav) \ -VCMI_SOUND_NAME(HALFKill) VCMI_SOUND_FILE(HALFKILL.wav) \ -VCMI_SOUND_NAME(HALFMove) VCMI_SOUND_FILE(HALFMOVE.wav) \ -VCMI_SOUND_NAME(HALFShot) VCMI_SOUND_FILE(HALFSHOT.wav) \ -VCMI_SOUND_NAME(HALFWNCE) VCMI_SOUND_FILE(HALFWNCE.wav) \ -VCMI_SOUND_NAME(HARPAttack) VCMI_SOUND_FILE(HARPATTK.wav) \ -VCMI_SOUND_NAME(HARPDefend) VCMI_SOUND_FILE(HARPDFND.wav) \ -VCMI_SOUND_NAME(HARPKill) VCMI_SOUND_FILE(HARPKILL.wav) \ -VCMI_SOUND_NAME(HARPMove) VCMI_SOUND_FILE(HARPMOVE.wav) \ -VCMI_SOUND_NAME(HARPWNCE) VCMI_SOUND_FILE(HARPWNCE.wav) \ -VCMI_SOUND_NAME(HASTE) VCMI_SOUND_FILE(HASTE.wav) \ -VCMI_SOUND_NAME(HCRSAttack) VCMI_SOUND_FILE(HCRSATTK.wav) \ -VCMI_SOUND_NAME(HCRSDefend) VCMI_SOUND_FILE(HCRSDFND.wav) \ -VCMI_SOUND_NAME(HCRSKill) VCMI_SOUND_FILE(HCRSKILL.wav) \ -VCMI_SOUND_NAME(HCRSMove) VCMI_SOUND_FILE(HCRSMOVE.wav) \ -VCMI_SOUND_NAME(HCRSShot) VCMI_SOUND_FILE(HCRSSHOT.wav) \ -VCMI_SOUND_NAME(HCRSWNCE) VCMI_SOUND_FILE(HCRSWNCE.wav) \ -VCMI_SOUND_NAME(HGOBAttack) VCMI_SOUND_FILE(HGOBATTK.wav) \ -VCMI_SOUND_NAME(HGOBDefend) VCMI_SOUND_FILE(HGOBDFND.wav) \ -VCMI_SOUND_NAME(HGOBKill) VCMI_SOUND_FILE(HGOBKILL.wav) \ -VCMI_SOUND_NAME(HGOBMove) VCMI_SOUND_FILE(HGOBMOVE.wav) \ -VCMI_SOUND_NAME(HGOBWNCE) VCMI_SOUND_FILE(HGOBWNCE.wav) \ -VCMI_SOUND_NAME(HGWRAttack) VCMI_SOUND_FILE(HGWRATTK.wav) \ -VCMI_SOUND_NAME(HGWRDefend) VCMI_SOUND_FILE(HGWRDFND.wav) \ -VCMI_SOUND_NAME(HGWRKill) VCMI_SOUND_FILE(HGWRKILL.wav) \ -VCMI_SOUND_NAME(HGWRMove) VCMI_SOUND_FILE(HGWRMOVE.wav) \ -VCMI_SOUND_NAME(HGWRWNCE) VCMI_SOUND_FILE(HGWRWNCE.wav) \ -VCMI_SOUND_NAME(HHAGAttack) VCMI_SOUND_FILE(HHAGATTK.wav) \ -VCMI_SOUND_NAME(HHAGDefend) VCMI_SOUND_FILE(HHAGDFND.wav) \ -VCMI_SOUND_NAME(HHAGKill) VCMI_SOUND_FILE(HHAGKILL.wav) \ -VCMI_SOUND_NAME(HHAGMove) VCMI_SOUND_FILE(HHAGMOVE.wav) \ -VCMI_SOUND_NAME(HHAGShot) VCMI_SOUND_FILE(HHAGSHOT.wav) \ -VCMI_SOUND_NAME(HHAGWNCE) VCMI_SOUND_FILE(HHAGWNCE.wav) \ -VCMI_SOUND_NAME(HHNDAttack) VCMI_SOUND_FILE(HHNDATTK.wav) \ -VCMI_SOUND_NAME(HHNDDefend) VCMI_SOUND_FILE(HHNDDFND.wav) \ -VCMI_SOUND_NAME(HHNDKill) VCMI_SOUND_FILE(HHNDKILL.wav) \ -VCMI_SOUND_NAME(HHNDMove) VCMI_SOUND_FILE(HHNDMOVE.wav) \ -VCMI_SOUND_NAME(HHNDWNCE) VCMI_SOUND_FILE(HHNDWNCE.wav) \ -VCMI_SOUND_NAME(horseDirt) VCMI_SOUND_FILE(HORSE00.wav) \ -VCMI_SOUND_NAME(horseSand) VCMI_SOUND_FILE(HORSE01.wav) \ -VCMI_SOUND_NAME(horseGrass) VCMI_SOUND_FILE(HORSE02.wav) \ -VCMI_SOUND_NAME(horseSnow) VCMI_SOUND_FILE(HORSE03.wav) \ -VCMI_SOUND_NAME(horseSwamp) VCMI_SOUND_FILE(HORSE04.wav) \ -VCMI_SOUND_NAME(horseRough) VCMI_SOUND_FILE(HORSE05.wav) \ -VCMI_SOUND_NAME(horseSubterranean) VCMI_SOUND_FILE(HORSE06.wav) \ -VCMI_SOUND_NAME(horseLava) VCMI_SOUND_FILE(HORSE07.wav) \ -VCMI_SOUND_NAME(horseWater) VCMI_SOUND_FILE(HORSE08.wav) \ -VCMI_SOUND_NAME(horseRock) VCMI_SOUND_FILE(HORSE09.wav) \ -VCMI_SOUND_NAME(horseFly) VCMI_SOUND_FILE(HORSE10.wav) \ -VCMI_SOUND_NAME(horsePenaltyDirt) VCMI_SOUND_FILE(HORSE20.wav) \ -VCMI_SOUND_NAME(horsePenaltySand) VCMI_SOUND_FILE(HORSE21.wav) \ -VCMI_SOUND_NAME(horsePenaltyGrass) VCMI_SOUND_FILE(HORSE22.wav) \ -VCMI_SOUND_NAME(horsePenaltySnow) VCMI_SOUND_FILE(HORSE23.wav) \ -VCMI_SOUND_NAME(horsePenaltySwamp) VCMI_SOUND_FILE(HORSE24.wav) \ -VCMI_SOUND_NAME(horsePenaltyRough) VCMI_SOUND_FILE(HORSE25.wav) \ -VCMI_SOUND_NAME(horsePenaltySubterranean) VCMI_SOUND_FILE(HORSE26.wav) \ -VCMI_SOUND_NAME(horsePenaltyLava) VCMI_SOUND_FILE(HORSE27.wav) \ -VCMI_SOUND_NAME(horsePenaltyRock) VCMI_SOUND_FILE(HORSE29.wav) \ -VCMI_SOUND_NAME(hydraAttack) VCMI_SOUND_FILE(HYDRATTK.wav) \ -VCMI_SOUND_NAME(hydraDefend) VCMI_SOUND_FILE(HYDRDFND.wav) \ -VCMI_SOUND_NAME(hydraKill) VCMI_SOUND_FILE(HYDRKILL.wav) \ -VCMI_SOUND_NAME(hydraMove) VCMI_SOUND_FILE(HYDRMOVE.wav) \ -VCMI_SOUND_NAME(hydraWNCE) VCMI_SOUND_FILE(HYDRWNCE.wav) \ -VCMI_SOUND_NAME(HYPNOTIZ) VCMI_SOUND_FILE(HYPNOTIZ.wav) \ -VCMI_SOUND_NAME(ICELAttack) VCMI_SOUND_FILE(ICELATTK.wav) \ -VCMI_SOUND_NAME(ICELDefend) VCMI_SOUND_FILE(ICELDFND.wav) \ -VCMI_SOUND_NAME(ICELKill) VCMI_SOUND_FILE(ICELKILL.wav) \ -VCMI_SOUND_NAME(ICELMove) VCMI_SOUND_FILE(ICELMOVE.wav) \ -VCMI_SOUND_NAME(ICELShot) VCMI_SOUND_FILE(ICELSHOT.wav) \ -VCMI_SOUND_NAME(ICELWNCE) VCMI_SOUND_FILE(ICELWNCE.wav) \ -VCMI_SOUND_NAME(ICERAYEX) VCMI_SOUND_FILE(ICERAYEX.wav) \ -VCMI_SOUND_NAME(ICERAY) VCMI_SOUND_FILE(ICERAY.wav) \ -VCMI_SOUND_NAME(IGLMAttack) VCMI_SOUND_FILE(IGLMATTK.wav) \ -VCMI_SOUND_NAME(IGLMDefend) VCMI_SOUND_FILE(IGLMDFND.wav) \ -VCMI_SOUND_NAME(IGLMKill) VCMI_SOUND_FILE(IGLMKILL.wav) \ -VCMI_SOUND_NAME(IGLMMove) VCMI_SOUND_FILE(IGLMMOVE.wav) \ -VCMI_SOUND_NAME(IGLMWNCE) VCMI_SOUND_FILE(IGLMWNCE.wav) \ -VCMI_SOUND_NAME(IMPPAttack) VCMI_SOUND_FILE(IMPPATTK.wav) \ -VCMI_SOUND_NAME(IMPPDefend) VCMI_SOUND_FILE(IMPPDFND.wav) \ -VCMI_SOUND_NAME(IMPPKill) VCMI_SOUND_FILE(IMPPKILL.wav) \ -VCMI_SOUND_NAME(IMPPMove) VCMI_SOUND_FILE(IMPPMOVE.wav) \ -VCMI_SOUND_NAME(IMPPWNCE) VCMI_SOUND_FILE(IMPPWNCE.wav) \ -VCMI_SOUND_NAME(ITRGAttack) VCMI_SOUND_FILE(ITRGATTK.wav) \ -VCMI_SOUND_NAME(ITRGDefend) VCMI_SOUND_FILE(ITRGDFND.wav) \ -VCMI_SOUND_NAME(ITRGKill) VCMI_SOUND_FILE(ITRGKILL.wav) \ -VCMI_SOUND_NAME(ITRGMove) VCMI_SOUND_FILE(ITRGMOVE.wav) \ -VCMI_SOUND_NAME(ITRGWNCE) VCMI_SOUND_FILE(ITRGWNCE.wav) \ -VCMI_SOUND_NAME(KEEPShot) VCMI_SOUND_FILE(KEEPSHOT.wav) \ -VCMI_SOUND_NAME(LANDKill) VCMI_SOUND_FILE(LANDKILL.wav) \ -VCMI_SOUND_NAME(LANDMINE) VCMI_SOUND_FILE(LANDMINE.wav) \ -VCMI_SOUND_NAME(LCRSAttack) VCMI_SOUND_FILE(LCRSATTK.wav) \ -VCMI_SOUND_NAME(LCRSDefend) VCMI_SOUND_FILE(LCRSDFND.wav) \ -VCMI_SOUND_NAME(LCRSKill) VCMI_SOUND_FILE(LCRSKILL.wav) \ -VCMI_SOUND_NAME(LCRSMove) VCMI_SOUND_FILE(LCRSMOVE.wav) \ -VCMI_SOUND_NAME(LCRSShot) VCMI_SOUND_FILE(LCRSSHOT.wav) \ -VCMI_SOUND_NAME(LCRSWNCE) VCMI_SOUND_FILE(LCRSWNCE.wav) \ -VCMI_SOUND_NAME(LICHATK2) VCMI_SOUND_FILE(LICHATK2.wav) \ -VCMI_SOUND_NAME(LICHAttack) VCMI_SOUND_FILE(LICHATTK.wav) \ -VCMI_SOUND_NAME(LICHDefend) VCMI_SOUND_FILE(LICHDFND.wav) \ -VCMI_SOUND_NAME(LICHKill) VCMI_SOUND_FILE(LICHKILL.wav) \ -VCMI_SOUND_NAME(LICHMove) VCMI_SOUND_FILE(LICHMOVE.wav) \ -VCMI_SOUND_NAME(LICHShot) VCMI_SOUND_FILE(LICHSHOT.wav) \ -VCMI_SOUND_NAME(LICHWNCE) VCMI_SOUND_FILE(LICHWNCE.wav) \ -VCMI_SOUND_NAME(LIGHTBLT) VCMI_SOUND_FILE(LIGHTBLT.wav) \ -VCMI_SOUND_NAME(LIGHTHOUSE) VCMI_SOUND_FILE(LIGHTHOUSE.wav) \ -VCMI_SOUND_NAME(LOOPAIR) VCMI_SOUND_FILE(LOOPAIR.wav) \ -VCMI_SOUND_NAME(LOOPANIM) VCMI_SOUND_FILE(LOOPANIM.wav) \ -VCMI_SOUND_NAME(LOOPARCH) VCMI_SOUND_FILE(LOOPARCH.wav) \ -VCMI_SOUND_NAME(LOOPAREN) VCMI_SOUND_FILE(LOOPAREN.wav) \ -VCMI_SOUND_NAME(LOOPBEHE) VCMI_SOUND_FILE(LOOPBEHE.wav) \ -VCMI_SOUND_NAME(LOOPBIRD) VCMI_SOUND_FILE(LOOPBIRD.wav) \ -VCMI_SOUND_NAME(LOOPBUOY) VCMI_SOUND_FILE(LOOPBUOY.wav) \ -VCMI_SOUND_NAME(LOOPCAMP) VCMI_SOUND_FILE(LOOPCAMP.wav) \ -VCMI_SOUND_NAME(LOOPCAVE) VCMI_SOUND_FILE(LOOPCAVE.wav) \ -VCMI_SOUND_NAME(LOOPCRYS) VCMI_SOUND_FILE(LOOPCRYS.wav) \ -VCMI_SOUND_NAME(LOOPCURS) VCMI_SOUND_FILE(LOOPCURS.wav) \ -VCMI_SOUND_NAME(LOOPDEAD) VCMI_SOUND_FILE(LOOPDEAD.wav) \ -VCMI_SOUND_NAME(LOOPDEN) VCMI_SOUND_FILE(LOOPDEN.wav) \ -VCMI_SOUND_NAME(LOOPDEVL) VCMI_SOUND_FILE(LOOPDEVL.wav) \ -VCMI_SOUND_NAME(LOOPDOG) VCMI_SOUND_FILE(LOOPDOG.wav) \ -VCMI_SOUND_NAME(LOOPDRAG) VCMI_SOUND_FILE(LOOPDRAG.wav) \ -VCMI_SOUND_NAME(LOOPDWAR) VCMI_SOUND_FILE(LOOPDWAR.wav) \ -VCMI_SOUND_NAME(LOOPEART) VCMI_SOUND_FILE(LOOPEART.wav) \ -VCMI_SOUND_NAME(LOOPELF) VCMI_SOUND_FILE(LOOPELF.wav) \ -VCMI_SOUND_NAME(LOOPFACT) VCMI_SOUND_FILE(LOOPFACT.wav) \ -VCMI_SOUND_NAME(LOOPFAER) VCMI_SOUND_FILE(LOOPFAER.wav) \ -VCMI_SOUND_NAME(LOOPFALL) VCMI_SOUND_FILE(LOOPFALL.wav) \ -VCMI_SOUND_NAME(LOOPFIRE) VCMI_SOUND_FILE(LOOPFIRE.wav) \ -VCMI_SOUND_NAME(LOOPFLAG) VCMI_SOUND_FILE(LOOPFLAG.wav) \ -VCMI_SOUND_NAME(LOOPFOUN) VCMI_SOUND_FILE(LOOPFOUN.wav) \ -VCMI_SOUND_NAME(LOOPGARD) VCMI_SOUND_FILE(LOOPGARD.wav) \ -VCMI_SOUND_NAME(LOOPGATE) VCMI_SOUND_FILE(LOOPGATE.wav) \ -VCMI_SOUND_NAME(LOOPGEMP) VCMI_SOUND_FILE(LOOPGEMP.wav) \ -VCMI_SOUND_NAME(LOOPGOBL) VCMI_SOUND_FILE(LOOPGOBL.wav) \ -VCMI_SOUND_NAME(LOOPGREM) VCMI_SOUND_FILE(LOOPGREM.wav) \ -VCMI_SOUND_NAME(LOOPGRIF) VCMI_SOUND_FILE(LOOPGRIF.wav) \ -VCMI_SOUND_NAME(LOOPHARP) VCMI_SOUND_FILE(LOOPHARP.wav) \ -VCMI_SOUND_NAME(LOOPHORS) VCMI_SOUND_FILE(LOOPHORS.wav) \ -VCMI_SOUND_NAME(LOOPHYDR) VCMI_SOUND_FILE(LOOPHYDR.wav) \ -VCMI_SOUND_NAME(LOOPLEAR) VCMI_SOUND_FILE(LOOPLEAR.wav) \ -VCMI_SOUND_NAME(LOOPLEPR) VCMI_SOUND_FILE(LOOPLEPR.wav) \ -VCMI_SOUND_NAME(LOOPLUMB) VCMI_SOUND_FILE(LOOPLUMB.wav) \ -VCMI_SOUND_NAME(LOOPMAGI) VCMI_SOUND_FILE(LOOPMAGI.wav) \ -VCMI_SOUND_NAME(LOOPMANT) VCMI_SOUND_FILE(LOOPMANT.wav) \ -VCMI_SOUND_NAME(LOOPMARK) VCMI_SOUND_FILE(LOOPMARK.wav) \ -VCMI_SOUND_NAME(LOOPMEDU) VCMI_SOUND_FILE(LOOPMEDU.wav) \ -VCMI_SOUND_NAME(LOOPMERC) VCMI_SOUND_FILE(LOOPMERC.wav) \ -VCMI_SOUND_NAME(LOOPMILL) VCMI_SOUND_FILE(LOOPMILL.wav) \ -VCMI_SOUND_NAME(LOOPMINE) VCMI_SOUND_FILE(LOOPMINE.wav) \ -VCMI_SOUND_NAME(LOOPMON1) VCMI_SOUND_FILE(LOOPMON1.wav) \ -VCMI_SOUND_NAME(LOOPMON2) VCMI_SOUND_FILE(LOOPMON2.wav) \ -VCMI_SOUND_NAME(LOOPMONK) VCMI_SOUND_FILE(LOOPMONK.wav) \ -VCMI_SOUND_NAME(LOOPMONS) VCMI_SOUND_FILE(LOOPMONS.wav) \ -VCMI_SOUND_NAME(LOOPNAGA) VCMI_SOUND_FILE(LOOPNAGA.wav) \ -VCMI_SOUND_NAME(LOOPOCEA) VCMI_SOUND_FILE(LOOPOCEA.wav) \ -VCMI_SOUND_NAME(LOOPOGRE) VCMI_SOUND_FILE(LOOPOGRE.wav) \ -VCMI_SOUND_NAME(LOOPORC) VCMI_SOUND_FILE(LOOPORC.wav) \ -VCMI_SOUND_NAME(LOOPPEGA) VCMI_SOUND_FILE(LOOPPEGA.wav) \ -VCMI_SOUND_NAME(LOOPPIKE) VCMI_SOUND_FILE(LOOPPIKE.wav) \ -VCMI_SOUND_NAME(LOOPSANC) VCMI_SOUND_FILE(LOOPSANC.wav) \ -VCMI_SOUND_NAME(LOOPSHRIN) VCMI_SOUND_FILE(LOOPSHRIN.wav) \ -VCMI_SOUND_NAME(LOOPSIRE) VCMI_SOUND_FILE(LOOPSIRE.wav) \ -VCMI_SOUND_NAME(LOOPSKEL) VCMI_SOUND_FILE(LOOPSKEL.wav) \ -VCMI_SOUND_NAME(LOOPSTAR) VCMI_SOUND_FILE(LOOPSTAR.wav) \ -VCMI_SOUND_NAME(LOOPSULF) VCMI_SOUND_FILE(LOOPSULF.wav) \ -VCMI_SOUND_NAME(LOOPSWAR) VCMI_SOUND_FILE(LOOPSWAR.wav) \ -VCMI_SOUND_NAME(LOOPSWOR) VCMI_SOUND_FILE(LOOPSWOR.wav) \ -VCMI_SOUND_NAME(LOOPTAV) VCMI_SOUND_FILE(LOOPTAV.wav) \ -VCMI_SOUND_NAME(LOOPTITA) VCMI_SOUND_FILE(LOOPTITA.wav) \ -VCMI_SOUND_NAME(LOOPUNIC) VCMI_SOUND_FILE(LOOPUNIC.wav) \ -VCMI_SOUND_NAME(LOOPVENT) VCMI_SOUND_FILE(LOOPVENT.wav) \ -VCMI_SOUND_NAME(LOOPVOLC) VCMI_SOUND_FILE(LOOPVOLC.wav) \ -VCMI_SOUND_NAME(LOOPWHIR) VCMI_SOUND_FILE(LOOPWHIR.wav) \ -VCMI_SOUND_NAME(LOOPWIND) VCMI_SOUND_FILE(LOOPWIND.wav) \ -VCMI_SOUND_NAME(LOOPWOLF) VCMI_SOUND_FILE(LOOPWOLF.wav) \ -VCMI_SOUND_NAME(LTITAttack) VCMI_SOUND_FILE(LTITATTK.wav) \ -VCMI_SOUND_NAME(LTITDefend) VCMI_SOUND_FILE(LTITDFND.wav) \ -VCMI_SOUND_NAME(LTITKill) VCMI_SOUND_FILE(LTITKILL.wav) \ -VCMI_SOUND_NAME(LTITMove) VCMI_SOUND_FILE(LTITMOVE.wav) \ -VCMI_SOUND_NAME(LTITWNCE) VCMI_SOUND_FILE(LTITWNCE.wav) \ -VCMI_SOUND_NAME(LUCK) VCMI_SOUND_FILE(LUCK.wav) \ -VCMI_SOUND_NAME(MAGCAROW) VCMI_SOUND_FILE(MAGCAROW.wav) \ -VCMI_SOUND_NAME(MAGCHDRN) VCMI_SOUND_FILE(MAGCHDRN.wav) \ -VCMI_SOUND_NAME(MAGCHFIL) VCMI_SOUND_FILE(MAGCHFIL.wav) \ -VCMI_SOUND_NAME(MAGEAttack) VCMI_SOUND_FILE(MAGEATTK.wav) \ -VCMI_SOUND_NAME(MAGEDefend) VCMI_SOUND_FILE(MAGEDFND.wav) \ -VCMI_SOUND_NAME(MAGEKill) VCMI_SOUND_FILE(MAGEKILL.wav) \ -VCMI_SOUND_NAME(MAGEMove) VCMI_SOUND_FILE(MAGEMOVE.wav) \ -VCMI_SOUND_NAME(MAGEShot) VCMI_SOUND_FILE(MAGESHOT.wav) \ -VCMI_SOUND_NAME(MAGEWNCE) VCMI_SOUND_FILE(MAGEWNCE.wav) \ -VCMI_SOUND_NAME(MAGICBLT) VCMI_SOUND_FILE(MAGICBLT.wav) \ -VCMI_SOUND_NAME(MAGICRES) VCMI_SOUND_FILE(MAGICRES.wav) \ -VCMI_SOUND_NAME(MAGMAttack) VCMI_SOUND_FILE(MAGMATTK.wav) \ -VCMI_SOUND_NAME(MAGMDefend) VCMI_SOUND_FILE(MAGMDFND.wav) \ -VCMI_SOUND_NAME(MAGMKill) VCMI_SOUND_FILE(MAGMKILL.wav) \ -VCMI_SOUND_NAME(MAGMMove) VCMI_SOUND_FILE(MAGMMOVE.wav) \ -VCMI_SOUND_NAME(MAGMWNCE) VCMI_SOUND_FILE(MAGMWNCE.wav) \ -VCMI_SOUND_NAME(MANADRAI) VCMI_SOUND_FILE(MANADRAI.wav) \ -VCMI_SOUND_NAME(MANTAttack) VCMI_SOUND_FILE(MANTATTK.wav) \ -VCMI_SOUND_NAME(MANTDefend) VCMI_SOUND_FILE(MANTDFND.wav) \ -VCMI_SOUND_NAME(MANTKill) VCMI_SOUND_FILE(MANTKILL.wav) \ -VCMI_SOUND_NAME(MANTMove) VCMI_SOUND_FILE(MANTMOVE.wav) \ -VCMI_SOUND_NAME(MANTShot) VCMI_SOUND_FILE(MANTSHOT.wav) \ -VCMI_SOUND_NAME(MANTWNCE) VCMI_SOUND_FILE(MANTWNCE.wav) \ -VCMI_SOUND_NAME(MEDQAttack) VCMI_SOUND_FILE(MEDQATTK.wav) \ -VCMI_SOUND_NAME(MEDQDefend) VCMI_SOUND_FILE(MEDQDFND.wav) \ -VCMI_SOUND_NAME(MEDQKill) VCMI_SOUND_FILE(MEDQKILL.wav) \ -VCMI_SOUND_NAME(MEDQMove) VCMI_SOUND_FILE(MEDQMOVE.wav) \ -VCMI_SOUND_NAME(MEDQShot) VCMI_SOUND_FILE(MEDQSHOT.wav) \ -VCMI_SOUND_NAME(MEDQWNCE) VCMI_SOUND_FILE(MEDQWNCE.wav) \ -VCMI_SOUND_NAME(MEDUAttack) VCMI_SOUND_FILE(MEDUATTK.wav) \ -VCMI_SOUND_NAME(MEDUDefend) VCMI_SOUND_FILE(MEDUDFND.wav) \ -VCMI_SOUND_NAME(MEDUKill) VCMI_SOUND_FILE(MEDUKILL.wav) \ -VCMI_SOUND_NAME(MEDUMove) VCMI_SOUND_FILE(MEDUMOVE.wav) \ -VCMI_SOUND_NAME(MEDUShot) VCMI_SOUND_FILE(MEDUSHOT.wav) \ -VCMI_SOUND_NAME(MEDUWNCE) VCMI_SOUND_FILE(MEDUWNCE.wav) \ -VCMI_SOUND_NAME(METEOR) VCMI_SOUND_FILE(METEOR.wav) \ -VCMI_SOUND_NAME(MGELAttack) VCMI_SOUND_FILE(MGELATTK.wav) \ -VCMI_SOUND_NAME(MGELDefend) VCMI_SOUND_FILE(MGELDFND.wav) \ -VCMI_SOUND_NAME(MGELKill) VCMI_SOUND_FILE(MGELKILL.wav) \ -VCMI_SOUND_NAME(MGELMove) VCMI_SOUND_FILE(MGELMOVE.wav) \ -VCMI_SOUND_NAME(MGELWNCE) VCMI_SOUND_FILE(MGELWNCE.wav) \ -VCMI_SOUND_NAME(MGOGAttack) VCMI_SOUND_FILE(MGOGATTK.wav) \ -VCMI_SOUND_NAME(MGOGDefend) VCMI_SOUND_FILE(MGOGDFND.wav) \ -VCMI_SOUND_NAME(MGOGKill) VCMI_SOUND_FILE(MGOGKILL.wav) \ -VCMI_SOUND_NAME(MGOGMove) VCMI_SOUND_FILE(MGOGMOVE.wav) \ -VCMI_SOUND_NAME(MGOGShot) VCMI_SOUND_FILE(MGOGSHOT.wav) \ -VCMI_SOUND_NAME(MGOGWNCE) VCMI_SOUND_FILE(MGOGWNCE.wav) \ -VCMI_SOUND_NAME(MGRMAttack) VCMI_SOUND_FILE(MGRMATTK.wav) \ -VCMI_SOUND_NAME(MGRMDefend) VCMI_SOUND_FILE(MGRMDFND.wav) \ -VCMI_SOUND_NAME(MGRMKill) VCMI_SOUND_FILE(MGRMKILL.wav) \ -VCMI_SOUND_NAME(MGRMMove) VCMI_SOUND_FILE(MGRMMOVE.wav) \ -VCMI_SOUND_NAME(MGRMShot) VCMI_SOUND_FILE(MGRMSHOT.wav) \ -VCMI_SOUND_NAME(MGRMWNCE) VCMI_SOUND_FILE(MGRMWNCE.wav) \ -VCMI_SOUND_NAME(MILITARY) VCMI_SOUND_FILE(MILITARY.wav) \ -VCMI_SOUND_NAME(MINKAttack) VCMI_SOUND_FILE(MINKATTK.wav) \ -VCMI_SOUND_NAME(MINKDefend) VCMI_SOUND_FILE(MINKDFND.wav) \ -VCMI_SOUND_NAME(MINKKill) VCMI_SOUND_FILE(MINKKILL.wav) \ -VCMI_SOUND_NAME(MINKMove) VCMI_SOUND_FILE(MINKMOVE.wav) \ -VCMI_SOUND_NAME(MINKShot) VCMI_SOUND_FILE(MINKSHOT.wav) \ -VCMI_SOUND_NAME(MINKWNCE) VCMI_SOUND_FILE(MINKWNCE.wav) \ -VCMI_SOUND_NAME(MINOAttack) VCMI_SOUND_FILE(MINOATTK.wav) \ -VCMI_SOUND_NAME(MINODefend) VCMI_SOUND_FILE(MINODFND.wav) \ -VCMI_SOUND_NAME(MINOKill) VCMI_SOUND_FILE(MINOKILL.wav) \ -VCMI_SOUND_NAME(MINOMove) VCMI_SOUND_FILE(MINOMOVE.wav) \ -VCMI_SOUND_NAME(MINOWNCE) VCMI_SOUND_FILE(MINOWNCE.wav) \ -VCMI_SOUND_NAME(MIRRORIM) VCMI_SOUND_FILE(MIRRORIM.wav) \ -VCMI_SOUND_NAME(MIRTH) VCMI_SOUND_FILE(MIRTH.wav) \ -VCMI_SOUND_NAME(MISFORT) VCMI_SOUND_FILE(MISFORT.wav) \ -VCMI_SOUND_NAME(MNRDEATH) VCMI_SOUND_FILE(MNRDEATH.wav) \ -VCMI_SOUND_NAME(monkAttack) VCMI_SOUND_FILE(MONKATTK.wav) \ -VCMI_SOUND_NAME(monkDefend) VCMI_SOUND_FILE(MONKDFND.wav) \ -VCMI_SOUND_NAME(monkKill) VCMI_SOUND_FILE(MONKKILL.wav) \ -VCMI_SOUND_NAME(monkMove) VCMI_SOUND_FILE(MONKMOVE.wav) \ -VCMI_SOUND_NAME(monkShot) VCMI_SOUND_FILE(MONKSHOT.wav) \ -VCMI_SOUND_NAME(monkWNCE) VCMI_SOUND_FILE(MONKWNCE.wav) \ -VCMI_SOUND_NAME(MORALE) VCMI_SOUND_FILE(MORALE.wav) \ -VCMI_SOUND_NAME(MUCKMIRE) VCMI_SOUND_FILE(MUCKMIRE.wav) \ -VCMI_SOUND_NAME(MUMYAttack) VCMI_SOUND_FILE(MUMYATTK.wav) \ -VCMI_SOUND_NAME(MUMYDefend) VCMI_SOUND_FILE(MUMYDFND.wav) \ -VCMI_SOUND_NAME(MUMYKill) VCMI_SOUND_FILE(MUMYKILL.wav) \ -VCMI_SOUND_NAME(MUMYMove) VCMI_SOUND_FILE(MUMYMOVE.wav) \ -VCMI_SOUND_NAME(MUMYWNCE) VCMI_SOUND_FILE(MUMYWNCE.wav) \ -VCMI_SOUND_NAME(MYSTERY) VCMI_SOUND_FILE(MYSTERY.wav) \ -VCMI_SOUND_NAME(newDay) VCMI_SOUND_FILE(NEWDAY.wav) \ -VCMI_SOUND_NAME(newMonth) VCMI_SOUND_FILE(NEWMONTH.wav) \ -VCMI_SOUND_NAME(newWeek) VCMI_SOUND_FILE(NEWWEEK.wav) \ -VCMI_SOUND_NAME(NGRDAttack) VCMI_SOUND_FILE(NGRDATTK.wav) \ -VCMI_SOUND_NAME(NGRDDefend) VCMI_SOUND_FILE(NGRDDFND.wav) \ -VCMI_SOUND_NAME(NGRDKill) VCMI_SOUND_FILE(NGRDKILL.wav) \ -VCMI_SOUND_NAME(NGRDMove) VCMI_SOUND_FILE(NGRDMOVE.wav) \ -VCMI_SOUND_NAME(NGRDWNCE) VCMI_SOUND_FILE(NGRDWNCE.wav) \ -VCMI_SOUND_NAME(NMADAttack) VCMI_SOUND_FILE(NMADATTK.wav) \ -VCMI_SOUND_NAME(NMADDefend) VCMI_SOUND_FILE(NMADDFND.wav) \ -VCMI_SOUND_NAME(NMADKill) VCMI_SOUND_FILE(NMADKILL.wav) \ -VCMI_SOUND_NAME(NMADMove) VCMI_SOUND_FILE(NMADMOVE.wav) \ -VCMI_SOUND_NAME(NMADWNCE) VCMI_SOUND_FILE(NMADWNCE.wav) \ -VCMI_SOUND_NAME(NOMAD) VCMI_SOUND_FILE(NOMAD.wav) \ -VCMI_SOUND_NAME(NOSFAttack) VCMI_SOUND_FILE(NOSFATTK.wav) \ -VCMI_SOUND_NAME(NOSFDefend) VCMI_SOUND_FILE(NOSFDFND.wav) \ -VCMI_SOUND_NAME(NOSFEXT1) VCMI_SOUND_FILE(NOSFEXT1.wav) \ -VCMI_SOUND_NAME(NOSFEXT2) VCMI_SOUND_FILE(NOSFEXT2.wav) \ -VCMI_SOUND_NAME(NOSFKill) VCMI_SOUND_FILE(NOSFKILL.wav) \ -VCMI_SOUND_NAME(NOSFMove) VCMI_SOUND_FILE(NOSFMOVE.wav) \ -VCMI_SOUND_NAME(NOSFShot) VCMI_SOUND_FILE(NOSFSHOT.wav) \ -VCMI_SOUND_NAME(NOSFWNCE) VCMI_SOUND_FILE(NOSFWNCE.wav) \ -VCMI_SOUND_NAME(NSENAttack) VCMI_SOUND_FILE(NSENATTK.wav) \ -VCMI_SOUND_NAME(NSENDefend) VCMI_SOUND_FILE(NSENDFND.wav) \ -VCMI_SOUND_NAME(NSENKill) VCMI_SOUND_FILE(NSENKILL.wav) \ -VCMI_SOUND_NAME(NSENMove) VCMI_SOUND_FILE(NSENMOVE.wav) \ -VCMI_SOUND_NAME(NSENWNCE) VCMI_SOUND_FILE(NSENWNCE.wav) \ -VCMI_SOUND_NAME(heroNewLevel) VCMI_SOUND_FILE(NWHEROLV.wav) \ -VCMI_SOUND_NAME(OBELISK) VCMI_SOUND_FILE(OBELISK.wav) \ -VCMI_SOUND_NAME(OGREAttack) VCMI_SOUND_FILE(OGREATTK.wav) \ -VCMI_SOUND_NAME(OGREDefend) VCMI_SOUND_FILE(OGREDFND.wav) \ -VCMI_SOUND_NAME(OGREKill) VCMI_SOUND_FILE(OGREKILL.wav) \ -VCMI_SOUND_NAME(OGREMove) VCMI_SOUND_FILE(OGREMOVE.wav) \ -VCMI_SOUND_NAME(OGREWNCE) VCMI_SOUND_FILE(OGREWNCE.wav) \ -VCMI_SOUND_NAME(OGRGAttack) VCMI_SOUND_FILE(OGRGATTK.wav) \ -VCMI_SOUND_NAME(OGRGDefend) VCMI_SOUND_FILE(OGRGDFND.wav) \ -VCMI_SOUND_NAME(OGRGKill) VCMI_SOUND_FILE(OGRGKILL.wav) \ -VCMI_SOUND_NAME(OGRGMove) VCMI_SOUND_FILE(OGRGMOVE.wav) \ -VCMI_SOUND_NAME(OGRGWNCE) VCMI_SOUND_FILE(OGRGWNCE.wav) \ -VCMI_SOUND_NAME(OGRMAttack) VCMI_SOUND_FILE(OGRMATTK.wav) \ -VCMI_SOUND_NAME(OGRMDefend) VCMI_SOUND_FILE(OGRMDFND.wav) \ -VCMI_SOUND_NAME(OGRMKill) VCMI_SOUND_FILE(OGRMKILL.wav) \ -VCMI_SOUND_NAME(OGRMMove) VCMI_SOUND_FILE(OGRMMOVE.wav) \ -VCMI_SOUND_NAME(OGRMShot) VCMI_SOUND_FILE(OGRMSHOT.wav) \ -VCMI_SOUND_NAME(OGRMWNCE) VCMI_SOUND_FILE(OGRMWNCE.wav) \ -VCMI_SOUND_NAME(OORCAttack) VCMI_SOUND_FILE(OORCATTK.wav) \ -VCMI_SOUND_NAME(OORCDefend) VCMI_SOUND_FILE(OORCDFND.wav) \ -VCMI_SOUND_NAME(OORCKill) VCMI_SOUND_FILE(OORCKILL.wav) \ -VCMI_SOUND_NAME(OORCMove) VCMI_SOUND_FILE(OORCMOVE.wav) \ -VCMI_SOUND_NAME(OORCShot) VCMI_SOUND_FILE(OORCSHOT.wav) \ -VCMI_SOUND_NAME(OORCWNCE) VCMI_SOUND_FILE(OORCWNCE.wav) \ -VCMI_SOUND_NAME(ORCCAttack) VCMI_SOUND_FILE(ORCCATTK.wav) \ -VCMI_SOUND_NAME(ORCCDefend) VCMI_SOUND_FILE(ORCCDFND.wav) \ -VCMI_SOUND_NAME(ORCCKill) VCMI_SOUND_FILE(ORCCKILL.wav) \ -VCMI_SOUND_NAME(ORCCMove) VCMI_SOUND_FILE(ORCCMOVE.wav) \ -VCMI_SOUND_NAME(ORCCShot) VCMI_SOUND_FILE(ORCCSHOT.wav) \ -VCMI_SOUND_NAME(ORCCWNCE) VCMI_SOUND_FILE(ORCCWNCE.wav) \ -VCMI_SOUND_NAME(PARALYZE) VCMI_SOUND_FILE(PARALYZE.wav) \ -VCMI_SOUND_NAME(PEGAAttack) VCMI_SOUND_FILE(PEGAATTK.wav) \ -VCMI_SOUND_NAME(PEGADefend) VCMI_SOUND_FILE(PEGADFND.wav) \ -VCMI_SOUND_NAME(PEGAKill) VCMI_SOUND_FILE(PEGAKILL.wav) \ -VCMI_SOUND_NAME(PEGAMove) VCMI_SOUND_FILE(PEGAMOVE.wav) \ -VCMI_SOUND_NAME(PEGAWNCE) VCMI_SOUND_FILE(PEGAWNCE.wav) \ -VCMI_SOUND_NAME(PFNDAttack) VCMI_SOUND_FILE(PFNDATTK.wav) \ -VCMI_SOUND_NAME(PFNDDefend) VCMI_SOUND_FILE(PFNDDFND.wav) \ -VCMI_SOUND_NAME(PFNDKill) VCMI_SOUND_FILE(PFNDKILL.wav) \ -VCMI_SOUND_NAME(PFNDMove) VCMI_SOUND_FILE(PFNDMOVE.wav) \ -VCMI_SOUND_NAME(PFNDWNCE) VCMI_SOUND_FILE(PFNDWNCE.wav) \ -VCMI_SOUND_NAME(PFOEAttack) VCMI_SOUND_FILE(PFOEATTK.wav) \ -VCMI_SOUND_NAME(PFOEDefend) VCMI_SOUND_FILE(PFOEDFND.wav) \ -VCMI_SOUND_NAME(PFOEKill) VCMI_SOUND_FILE(PFOEKILL.wav) \ -VCMI_SOUND_NAME(PFOEMove) VCMI_SOUND_FILE(PFOEMOVE.wav) \ -VCMI_SOUND_NAME(PFOEWNCE) VCMI_SOUND_FILE(PFOEWNCE.wav) \ -VCMI_SOUND_NAME(PHOEAttack) VCMI_SOUND_FILE(PHOEATTK.wav) \ -VCMI_SOUND_NAME(PHOEDefend) VCMI_SOUND_FILE(PHOEDFND.wav) \ -VCMI_SOUND_NAME(PHOEKill) VCMI_SOUND_FILE(PHOEKILL.wav) \ -VCMI_SOUND_NAME(PHOEMove) VCMI_SOUND_FILE(PHOEMOVE.wav) \ -VCMI_SOUND_NAME(PHOEWNCE) VCMI_SOUND_FILE(PHOEWNCE.wav) \ -VCMI_SOUND_NAME(pickup01) VCMI_SOUND_FILE(PICKUP01.wav) \ -VCMI_SOUND_NAME(pickup02) VCMI_SOUND_FILE(PICKUP02.wav) \ -VCMI_SOUND_NAME(pickup03) VCMI_SOUND_FILE(PICKUP03.wav) \ -VCMI_SOUND_NAME(pickup04) VCMI_SOUND_FILE(PICKUP04.wav) \ -VCMI_SOUND_NAME(pickup05) VCMI_SOUND_FILE(PICKUP05.wav) \ -VCMI_SOUND_NAME(pickup06) VCMI_SOUND_FILE(PICKUP06.wav) \ -VCMI_SOUND_NAME(pickup07) VCMI_SOUND_FILE(PICKUP07.wav) \ -VCMI_SOUND_NAME(pikemanAttack) VCMI_SOUND_FILE(PIKEATTK.wav) \ -VCMI_SOUND_NAME(pikemanDefend) VCMI_SOUND_FILE(PIKEDFND.wav) \ -VCMI_SOUND_NAME(pikemanKill) VCMI_SOUND_FILE(PIKEKILL.wav) \ -VCMI_SOUND_NAME(pikemanMove) VCMI_SOUND_FILE(PIKEMOVE.wav) \ -VCMI_SOUND_NAME(pikemanWNCE) VCMI_SOUND_FILE(PIKEWNCE.wav) \ -VCMI_SOUND_NAME(pixieAttack) VCMI_SOUND_FILE(PIXIATTK.wav) \ -VCMI_SOUND_NAME(pixieDefend) VCMI_SOUND_FILE(PIXIDFND.wav) \ -VCMI_SOUND_NAME(pixieKill) VCMI_SOUND_FILE(PIXIKILL.wav) \ -VCMI_SOUND_NAME(pixieMove) VCMI_SOUND_FILE(PIXIMOVE.wav) \ -VCMI_SOUND_NAME(pixieWNCE) VCMI_SOUND_FILE(PIXIWNCE.wav) \ -VCMI_SOUND_NAME(PLAYCOME) VCMI_SOUND_FILE(PLAYCOME.wav) \ -VCMI_SOUND_NAME(PLAYEXIT) VCMI_SOUND_FILE(PLAYEXIT.wav) \ -VCMI_SOUND_NAME(PLAYTURN) VCMI_SOUND_FILE(PLAYTURN.wav) \ -VCMI_SOUND_NAME(PLCHAttack) VCMI_SOUND_FILE(PLCHATTK.wav) \ -VCMI_SOUND_NAME(PLCHDefend) VCMI_SOUND_FILE(PLCHDFND.wav) \ -VCMI_SOUND_NAME(PLCHKill) VCMI_SOUND_FILE(PLCHKILL.wav) \ -VCMI_SOUND_NAME(PLCHMove) VCMI_SOUND_FILE(PLCHMOVE.wav) \ -VCMI_SOUND_NAME(PLCHShot) VCMI_SOUND_FILE(PLCHSHOT.wav) \ -VCMI_SOUND_NAME(PLCHWNCE) VCMI_SOUND_FILE(PLCHWNCE.wav) \ -VCMI_SOUND_NAME(PLIZAttack) VCMI_SOUND_FILE(PLIZATTK.wav) \ -VCMI_SOUND_NAME(PLIZDefend) VCMI_SOUND_FILE(PLIZDFND.wav) \ -VCMI_SOUND_NAME(PLIZKill) VCMI_SOUND_FILE(PLIZKILL.wav) \ -VCMI_SOUND_NAME(PLIZMove) VCMI_SOUND_FILE(PLIZMOVE.wav) \ -VCMI_SOUND_NAME(PLIZShot) VCMI_SOUND_FILE(PLIZSHOT.wav) \ -VCMI_SOUND_NAME(PLIZWNCE) VCMI_SOUND_FILE(PLIZWNCE.wav) \ -VCMI_SOUND_NAME(POISON) VCMI_SOUND_FILE(POISON.wav) \ -VCMI_SOUND_NAME(PRAYER) VCMI_SOUND_FILE(PRAYER.wav) \ -VCMI_SOUND_NAME(PRECISON) VCMI_SOUND_FILE(PRECISON.wav) \ -VCMI_SOUND_NAME(PROTECTA) VCMI_SOUND_FILE(PROTECTA.wav) \ -VCMI_SOUND_NAME(PROTECTE) VCMI_SOUND_FILE(PROTECTE.wav) \ -VCMI_SOUND_NAME(PROTECTF) VCMI_SOUND_FILE(PROTECTF.wav) \ -VCMI_SOUND_NAME(PROTECT) VCMI_SOUND_FILE(PROTECT.wav) \ -VCMI_SOUND_NAME(PROTECTW) VCMI_SOUND_FILE(PROTECTW.wav) \ -VCMI_SOUND_NAME(PSNTAttack) VCMI_SOUND_FILE(PSNTATTK.wav) \ -VCMI_SOUND_NAME(PSNTDefend) VCMI_SOUND_FILE(PSNTDFND.wav) \ -VCMI_SOUND_NAME(PSNTKill) VCMI_SOUND_FILE(PSNTKILL.wav) \ -VCMI_SOUND_NAME(PSNTMove) VCMI_SOUND_FILE(PSNTMOVE.wav) \ -VCMI_SOUND_NAME(PSNTWNCE) VCMI_SOUND_FILE(PSNTWNCE.wav) \ -VCMI_SOUND_NAME(PSYCAttack) VCMI_SOUND_FILE(PSYCATTK.wav) \ -VCMI_SOUND_NAME(PSYCDefend) VCMI_SOUND_FILE(PSYCDFND.wav) \ -VCMI_SOUND_NAME(PSYCKill) VCMI_SOUND_FILE(PSYCKILL.wav) \ -VCMI_SOUND_NAME(PSYCMove) VCMI_SOUND_FILE(PSYCMOVE.wav) \ -VCMI_SOUND_NAME(PSYCWNCE) VCMI_SOUND_FILE(PSYCWNCE.wav) \ -VCMI_SOUND_NAME(QUEST) VCMI_SOUND_FILE(QUEST.wav) \ -VCMI_SOUND_NAME(QUIKSAND) VCMI_SOUND_FILE(QUIKSAND.wav) \ -VCMI_SOUND_NAME(RDDRAttack) VCMI_SOUND_FILE(RDDRATTK.wav) \ -VCMI_SOUND_NAME(RDDRDefend) VCMI_SOUND_FILE(RDDRDFND.wav) \ -VCMI_SOUND_NAME(RDDRKill) VCMI_SOUND_FILE(RDDRKILL.wav) \ -VCMI_SOUND_NAME(RDDRMove) VCMI_SOUND_FILE(RDDRMOVE.wav) \ -VCMI_SOUND_NAME(RDDRWNCE) VCMI_SOUND_FILE(RDDRWNCE.wav) \ -VCMI_SOUND_NAME(REGENER) VCMI_SOUND_FILE(REGENER.wav) \ -VCMI_SOUND_NAME(REMoveOB) VCMI_SOUND_FILE(REMOVEOB.wav) \ -VCMI_SOUND_NAME(RESURECT) VCMI_SOUND_FILE(RESURECT.wav) \ -VCMI_SOUND_NAME(RGRFAttack) VCMI_SOUND_FILE(RGRFATTK.wav) \ -VCMI_SOUND_NAME(RGRFDefend) VCMI_SOUND_FILE(RGRFDFND.wav) \ -VCMI_SOUND_NAME(RGRFKill) VCMI_SOUND_FILE(RGRFKILL.wav) \ -VCMI_SOUND_NAME(RGRFMove) VCMI_SOUND_FILE(RGRFMOVE.wav) \ -VCMI_SOUND_NAME(RGRFWNCE) VCMI_SOUND_FILE(RGRFWNCE.wav) \ -VCMI_SOUND_NAME(ROCCAttack) VCMI_SOUND_FILE(ROCCATTK.wav) \ -VCMI_SOUND_NAME(ROCCDefend) VCMI_SOUND_FILE(ROCCDFND.wav) \ -VCMI_SOUND_NAME(ROCCKill) VCMI_SOUND_FILE(ROCCKILL.wav) \ -VCMI_SOUND_NAME(ROCCMove) VCMI_SOUND_FILE(ROCCMOVE.wav) \ -VCMI_SOUND_NAME(ROCCWNCE) VCMI_SOUND_FILE(ROCCWNCE.wav) \ -VCMI_SOUND_NAME(ROGUAttack) VCMI_SOUND_FILE(ROGUATTK.wav) \ -VCMI_SOUND_NAME(ROGUDefend) VCMI_SOUND_FILE(ROGUDFND.wav) \ -VCMI_SOUND_NAME(ROGUE) VCMI_SOUND_FILE(ROGUE.wav) \ -VCMI_SOUND_NAME(ROGUKill) VCMI_SOUND_FILE(ROGUKILL.wav) \ -VCMI_SOUND_NAME(ROGUMove) VCMI_SOUND_FILE(ROGUMOVE.wav) \ -VCMI_SOUND_NAME(ROGUWNCE) VCMI_SOUND_FILE(ROGUWNCE.wav) \ -VCMI_SOUND_NAME(RSBRYFZL) VCMI_SOUND_FILE(RSBRYFZL.wav) \ -VCMI_SOUND_NAME(RUSTAttack) VCMI_SOUND_FILE(RUSTATTK.wav) \ -VCMI_SOUND_NAME(RUSTDefend) VCMI_SOUND_FILE(RUSTDFND.wav) \ -VCMI_SOUND_NAME(RUSTKill) VCMI_SOUND_FILE(RUSTKILL.wav) \ -VCMI_SOUND_NAME(RUSTMove) VCMI_SOUND_FILE(RUSTMOVE.wav) \ -VCMI_SOUND_NAME(RUSTWNCE) VCMI_SOUND_FILE(RUSTWNCE.wav) \ -VCMI_SOUND_NAME(SACBRETH) VCMI_SOUND_FILE(SACBRETH.wav) \ -VCMI_SOUND_NAME(SACRIF1) VCMI_SOUND_FILE(SACRIF1.wav) \ -VCMI_SOUND_NAME(SACRIF2) VCMI_SOUND_FILE(SACRIF2.wav) \ -VCMI_SOUND_NAME(SCRPAttack) VCMI_SOUND_FILE(SCRPATTK.wav) \ -VCMI_SOUND_NAME(SCRPDefend) VCMI_SOUND_FILE(SCRPDFND.wav) \ -VCMI_SOUND_NAME(SCRPKill) VCMI_SOUND_FILE(SCRPKILL.wav) \ -VCMI_SOUND_NAME(SCRPMove) VCMI_SOUND_FILE(SCRPMOVE.wav) \ -VCMI_SOUND_NAME(SCRPShot) VCMI_SOUND_FILE(SCRPSHOT.wav) \ -VCMI_SOUND_NAME(SCRPWNCE) VCMI_SOUND_FILE(SCRPWNCE.wav) \ -VCMI_SOUND_NAME(SCUTBOAT) VCMI_SOUND_FILE(SCUTBOAT.wav) \ -VCMI_SOUND_NAME(SGLMAttack) VCMI_SOUND_FILE(SGLMATTK.wav) \ -VCMI_SOUND_NAME(SGLMDefend) VCMI_SOUND_FILE(SGLMDFND.wav) \ -VCMI_SOUND_NAME(SGLMKill) VCMI_SOUND_FILE(SGLMKILL.wav) \ -VCMI_SOUND_NAME(SGLMMove) VCMI_SOUND_FILE(SGLMMOVE.wav) \ -VCMI_SOUND_NAME(SGLMWNCE) VCMI_SOUND_FILE(SGLMWNCE.wav) \ -VCMI_SOUND_NAME(SGRGAttack) VCMI_SOUND_FILE(SGRGATTK.wav) \ -VCMI_SOUND_NAME(SGRGDefend) VCMI_SOUND_FILE(SGRGDFND.wav) \ -VCMI_SOUND_NAME(SGRGKill) VCMI_SOUND_FILE(SGRGKILL.wav) \ -VCMI_SOUND_NAME(SGRGMove) VCMI_SOUND_FILE(SGRGMOVE.wav) \ -VCMI_SOUND_NAME(SGRGWNCE) VCMI_SOUND_FILE(SGRGWNCE.wav) \ -VCMI_SOUND_NAME(SHDMAttack) VCMI_SOUND_FILE(SHDMATTK.wav) \ -VCMI_SOUND_NAME(SHDMDefend) VCMI_SOUND_FILE(SHDMDFND.wav) \ -VCMI_SOUND_NAME(SHDMKill) VCMI_SOUND_FILE(SHDMKILL.wav) \ -VCMI_SOUND_NAME(SHDMMove) VCMI_SOUND_FILE(SHDMMOVE.wav) \ -VCMI_SOUND_NAME(SHDMWNCE) VCMI_SOUND_FILE(SHDMWNCE.wav) \ -VCMI_SOUND_NAME(SHIELD) VCMI_SOUND_FILE(SHIELD.wav) \ -VCMI_SOUND_NAME(SKELAttack) VCMI_SOUND_FILE(SKELATTK.wav) \ -VCMI_SOUND_NAME(SKELDefend) VCMI_SOUND_FILE(SKELDFND.wav) \ -VCMI_SOUND_NAME(SKELKill) VCMI_SOUND_FILE(SKELKILL.wav) \ -VCMI_SOUND_NAME(SKELMove) VCMI_SOUND_FILE(SKELMOVE.wav) \ -VCMI_SOUND_NAME(SKELWNCE) VCMI_SOUND_FILE(SKELWNCE.wav) \ -VCMI_SOUND_NAME(SKLWAttack) VCMI_SOUND_FILE(SKLWATTK.wav) \ -VCMI_SOUND_NAME(SKLWDefend) VCMI_SOUND_FILE(SKLWDFND.wav) \ -VCMI_SOUND_NAME(SKLWKill) VCMI_SOUND_FILE(SKLWKILL.wav) \ -VCMI_SOUND_NAME(SKLWMove) VCMI_SOUND_FILE(SKLWMOVE.wav) \ -VCMI_SOUND_NAME(SKLWWNCE) VCMI_SOUND_FILE(SKLWWNCE.wav) \ -VCMI_SOUND_NAME(SLAYER) VCMI_SOUND_FILE(SLAYER.wav) \ -VCMI_SOUND_NAME(SORROW) VCMI_SOUND_FILE(SORROW.wav) \ -VCMI_SOUND_NAME(SPONTCOMB) VCMI_SOUND_FILE(SPONTCOMB.wav) \ -VCMI_SOUND_NAME(SPRTAttack) VCMI_SOUND_FILE(SPRTATTK.wav) \ -VCMI_SOUND_NAME(SPRTDefend) VCMI_SOUND_FILE(SPRTDFND.wav) \ -VCMI_SOUND_NAME(SPRTKill) VCMI_SOUND_FILE(SPRTKILL.wav) \ -VCMI_SOUND_NAME(SPRTMove) VCMI_SOUND_FILE(SPRTMOVE.wav) \ -VCMI_SOUND_NAME(SPRTWNCE) VCMI_SOUND_FILE(SPRTWNCE.wav) \ -VCMI_SOUND_NAME(STORAttack) VCMI_SOUND_FILE(STORATTK.wav) \ -VCMI_SOUND_NAME(STORDefend) VCMI_SOUND_FILE(STORDFND.wav) \ -VCMI_SOUND_NAME(STORE) VCMI_SOUND_FILE(STORE.wav) \ -VCMI_SOUND_NAME(STORKill) VCMI_SOUND_FILE(STORKILL.wav) \ -VCMI_SOUND_NAME(STORMove) VCMI_SOUND_FILE(STORMOVE.wav) \ -VCMI_SOUND_NAME(STORM) VCMI_SOUND_FILE(STORM.wav) \ -VCMI_SOUND_NAME(STORShot) VCMI_SOUND_FILE(STORSHOT.wav) \ -VCMI_SOUND_NAME(STORWNCE) VCMI_SOUND_FILE(STORWNCE.wav) \ -VCMI_SOUND_NAME(SUMMBOAT) VCMI_SOUND_FILE(SUMMBOAT.wav) \ -VCMI_SOUND_NAME(SUMNELM) VCMI_SOUND_FILE(SUMNELM.wav) \ -VCMI_SOUND_NAME(SWRDAttack) VCMI_SOUND_FILE(SWRDATTK.wav) \ -VCMI_SOUND_NAME(SWRDDefend) VCMI_SOUND_FILE(SWRDDFND.wav) \ -VCMI_SOUND_NAME(SWRDKill) VCMI_SOUND_FILE(SWRDKILL.wav) \ -VCMI_SOUND_NAME(SWRDMove) VCMI_SOUND_FILE(SWRDMOVE.wav) \ -VCMI_SOUND_NAME(SWRDWNCE) VCMI_SOUND_FILE(SWRDWNCE.wav) \ -VCMI_SOUND_NAME(SYSMSG) VCMI_SOUND_FILE(SYSMSG.wav) \ -VCMI_SOUND_NAME(TAILWIND) VCMI_SOUND_FILE(TAILWIND.wav) \ -VCMI_SOUND_NAME(TBRDAttack) VCMI_SOUND_FILE(TBRDATTK.wav) \ -VCMI_SOUND_NAME(TBRDDefend) VCMI_SOUND_FILE(TBRDDFND.wav) \ -VCMI_SOUND_NAME(TBRDKill) VCMI_SOUND_FILE(TBRDKILL.wav) \ -VCMI_SOUND_NAME(TBRDMove) VCMI_SOUND_FILE(TBRDMOVE.wav) \ -VCMI_SOUND_NAME(TBRDWNCE) VCMI_SOUND_FILE(TBRDWNCE.wav) \ -VCMI_SOUND_NAME(TELEIN) VCMI_SOUND_FILE(TELEIN.wav) \ -VCMI_SOUND_NAME(TELPTIN) VCMI_SOUND_FILE(TELPTIN.wav) \ -VCMI_SOUND_NAME(TELPTOUT) VCMI_SOUND_FILE(TELPTOUT.wav) \ -VCMI_SOUND_NAME(temple) VCMI_SOUND_FILE(TEMPLE.wav) \ -VCMI_SOUND_NAME(timeOver) VCMI_SOUND_FILE(TIMEOVER.wav) \ -VCMI_SOUND_NAME(treasure) VCMI_SOUND_FILE(TREASURE.wav) \ -VCMI_SOUND_NAME(TREEAttack) VCMI_SOUND_FILE(TREEATTK.wav) \ -VCMI_SOUND_NAME(TREEDefend) VCMI_SOUND_FILE(TREEDFND.wav) \ -VCMI_SOUND_NAME(TREEKill) VCMI_SOUND_FILE(TREEKILL.wav) \ -VCMI_SOUND_NAME(TREEMove) VCMI_SOUND_FILE(TREEMOVE.wav) \ -VCMI_SOUND_NAME(TREEWNCE) VCMI_SOUND_FILE(TREEWNCE.wav) \ -VCMI_SOUND_NAME(TRLLAttack) VCMI_SOUND_FILE(TRLLATTK.wav) \ -VCMI_SOUND_NAME(TRLLDefend) VCMI_SOUND_FILE(TRLLDFND.wav) \ -VCMI_SOUND_NAME(TRLLKill) VCMI_SOUND_FILE(TRLLKILL.wav) \ -VCMI_SOUND_NAME(TRLLMove) VCMI_SOUND_FILE(TRLLMOVE.wav) \ -VCMI_SOUND_NAME(TRLLWNCE) VCMI_SOUND_FILE(TRLLWNCE.wav) \ -VCMI_SOUND_NAME(TROGAttack) VCMI_SOUND_FILE(TROGATTK.wav) \ -VCMI_SOUND_NAME(TROGDefend) VCMI_SOUND_FILE(TROGDFND.wav) \ -VCMI_SOUND_NAME(TROGKill) VCMI_SOUND_FILE(TROGKILL.wav) \ -VCMI_SOUND_NAME(TROGMove) VCMI_SOUND_FILE(TROGMOVE.wav) \ -VCMI_SOUND_NAME(TROGWNCE) VCMI_SOUND_FILE(TROGWNCE.wav) \ -VCMI_SOUND_NAME(TUFFSKIN) VCMI_SOUND_FILE(TUFFSKIN.wav) \ -VCMI_SOUND_NAME(ULTIMATEARTIFACT) VCMI_SOUND_FILE(ULTIMATEARTIFACT.wav) \ -VCMI_SOUND_NAME(UNICAttack) VCMI_SOUND_FILE(UNICATTK.wav) \ -VCMI_SOUND_NAME(UNICDefend) VCMI_SOUND_FILE(UNICDFND.wav) \ -VCMI_SOUND_NAME(UNICKill) VCMI_SOUND_FILE(UNICKILL.wav) \ -VCMI_SOUND_NAME(UNICMove) VCMI_SOUND_FILE(UNICMOVE.wav) \ -VCMI_SOUND_NAME(UNICWNCE) VCMI_SOUND_FILE(UNICWNCE.wav) \ -VCMI_SOUND_NAME(VAMPAttack) VCMI_SOUND_FILE(VAMPATTK.wav) \ -VCMI_SOUND_NAME(VAMPDefend) VCMI_SOUND_FILE(VAMPDFND.wav) \ -VCMI_SOUND_NAME(VAMPEXT1) VCMI_SOUND_FILE(VAMPEXT1.wav) \ -VCMI_SOUND_NAME(VAMPEXT2) VCMI_SOUND_FILE(VAMPEXT2.wav) \ -VCMI_SOUND_NAME(VAMPKill) VCMI_SOUND_FILE(VAMPKILL.wav) \ -VCMI_SOUND_NAME(VAMPMove) VCMI_SOUND_FILE(VAMPMOVE.wav) \ -VCMI_SOUND_NAME(VAMPWNCE) VCMI_SOUND_FILE(VAMPWNCE.wav) \ -VCMI_SOUND_NAME(VIEW) VCMI_SOUND_FILE(VIEW.wav) \ -VCMI_SOUND_NAME(VISIONS) VCMI_SOUND_FILE(VISIONS.wav) \ -VCMI_SOUND_NAME(WALLHIT) VCMI_SOUND_FILE(WALLHIT.wav) \ -VCMI_SOUND_NAME(WALLMISS) VCMI_SOUND_FILE(WALLMISS.wav) \ -VCMI_SOUND_NAME(WATRWALK) VCMI_SOUND_FILE(WATRWALK.wav) \ -VCMI_SOUND_NAME(WEAKNESS) VCMI_SOUND_FILE(WEAKNESS.wav) \ -VCMI_SOUND_NAME(WELFAttack) VCMI_SOUND_FILE(WELFATTK.wav) \ -VCMI_SOUND_NAME(WELFDefend) VCMI_SOUND_FILE(WELFDFND.wav) \ -VCMI_SOUND_NAME(WELFKill) VCMI_SOUND_FILE(WELFKILL.wav) \ -VCMI_SOUND_NAME(WELFMove) VCMI_SOUND_FILE(WELFMOVE.wav) \ -VCMI_SOUND_NAME(WELFShot) VCMI_SOUND_FILE(WELFSHOT.wav) \ -VCMI_SOUND_NAME(WELFWNCE) VCMI_SOUND_FILE(WELFWNCE.wav) \ -VCMI_SOUND_NAME(WELMAttack) VCMI_SOUND_FILE(WELMATTK.wav) \ -VCMI_SOUND_NAME(WELMDefend) VCMI_SOUND_FILE(WELMDFND.wav) \ -VCMI_SOUND_NAME(WELMKill) VCMI_SOUND_FILE(WELMKILL.wav) \ -VCMI_SOUND_NAME(WELMMove) VCMI_SOUND_FILE(WELMMOVE.wav) \ -VCMI_SOUND_NAME(WELMWNCE) VCMI_SOUND_FILE(WELMWNCE.wav) \ -VCMI_SOUND_NAME(WGHTAttack) VCMI_SOUND_FILE(WGHTATTK.wav) \ -VCMI_SOUND_NAME(WGHTDefend) VCMI_SOUND_FILE(WGHTDFND.wav) \ -VCMI_SOUND_NAME(WGHTKill) VCMI_SOUND_FILE(WGHTKILL.wav) \ -VCMI_SOUND_NAME(WGHTMove) VCMI_SOUND_FILE(WGHTMOVE.wav) \ -VCMI_SOUND_NAME(WGHTWNCE) VCMI_SOUND_FILE(WGHTWNCE.wav) \ -VCMI_SOUND_NAME(WRTHAttack) VCMI_SOUND_FILE(WRTHATTK.wav) \ -VCMI_SOUND_NAME(WRTHDefend) VCMI_SOUND_FILE(WRTHDFND.wav) \ -VCMI_SOUND_NAME(WRTHKill) VCMI_SOUND_FILE(WRTHKILL.wav) \ -VCMI_SOUND_NAME(WRTHMove) VCMI_SOUND_FILE(WRTHMOVE.wav) \ -VCMI_SOUND_NAME(WRTHWNCE) VCMI_SOUND_FILE(WRTHWNCE.wav) \ -VCMI_SOUND_NAME(WUNCAttack) VCMI_SOUND_FILE(WUNCATTK.wav) \ -VCMI_SOUND_NAME(WUNCDefend) VCMI_SOUND_FILE(WUNCDFND.wav) \ -VCMI_SOUND_NAME(WUNCKill) VCMI_SOUND_FILE(WUNCKILL.wav) \ -VCMI_SOUND_NAME(WUNCMove) VCMI_SOUND_FILE(WUNCMOVE.wav) \ -VCMI_SOUND_NAME(WUNCShot) VCMI_SOUND_FILE(WUNCSHOT.wav) \ -VCMI_SOUND_NAME(WUNCWNCE) VCMI_SOUND_FILE(WUNCWNCE.wav) \ -VCMI_SOUND_NAME(WYVMAttack) VCMI_SOUND_FILE(WYVMATTK.wav) \ -VCMI_SOUND_NAME(WYVMDefend) VCMI_SOUND_FILE(WYVMDFND.wav) \ -VCMI_SOUND_NAME(WYVMKill) VCMI_SOUND_FILE(WYVMKILL.wav) \ -VCMI_SOUND_NAME(WYVMMove) VCMI_SOUND_FILE(WYVMMOVE.wav) \ -VCMI_SOUND_NAME(WYVMWNCE) VCMI_SOUND_FILE(WYVMWNCE.wav) \ -VCMI_SOUND_NAME(WYVNAttack) VCMI_SOUND_FILE(WYVNATTK.wav) \ -VCMI_SOUND_NAME(WYVNDefend) VCMI_SOUND_FILE(WYVNDFND.wav) \ -VCMI_SOUND_NAME(WYVNKill) VCMI_SOUND_FILE(WYVNKILL.wav) \ -VCMI_SOUND_NAME(WYVNMove) VCMI_SOUND_FILE(WYVNMOVE.wav) \ -VCMI_SOUND_NAME(WYVNWNCE) VCMI_SOUND_FILE(WYVNWNCE.wav) \ -VCMI_SOUND_NAME(YBMHAttack) VCMI_SOUND_FILE(YBMHATTK.wav) \ -VCMI_SOUND_NAME(YBMHDefend) VCMI_SOUND_FILE(YBMHDFND.wav) \ -VCMI_SOUND_NAME(YBMHKill) VCMI_SOUND_FILE(YBMHKILL.wav) \ -VCMI_SOUND_NAME(YBMHMove) VCMI_SOUND_FILE(YBMHMOVE.wav) \ -VCMI_SOUND_NAME(YBMHWNCE) VCMI_SOUND_FILE(YBMHWNCE.wav) \ -VCMI_SOUND_NAME(zelotAttack) VCMI_SOUND_FILE(ZELTATTK.wav) \ -VCMI_SOUND_NAME(zelotDefend) VCMI_SOUND_FILE(ZELTDFND.wav) \ -VCMI_SOUND_NAME(zelotKill) VCMI_SOUND_FILE(ZELTKILL.wav) \ -VCMI_SOUND_NAME(zelotMove) VCMI_SOUND_FILE(ZELTMOVE.wav) \ -VCMI_SOUND_NAME(zelotShot) VCMI_SOUND_FILE(ZELTSHOT.wav) \ -VCMI_SOUND_NAME(zelotWNCE) VCMI_SOUND_FILE(ZELTWNCE.wav) \ -VCMI_SOUND_NAME(ZMBLAttack) VCMI_SOUND_FILE(ZMBLATTK.wav) \ -VCMI_SOUND_NAME(ZMBLDefend) VCMI_SOUND_FILE(ZMBLDFND.wav) \ -VCMI_SOUND_NAME(ZMBLKill) VCMI_SOUND_FILE(ZMBLKILL.wav) \ -VCMI_SOUND_NAME(ZMBLMove) VCMI_SOUND_FILE(ZMBLMOVE.wav) \ -VCMI_SOUND_NAME(ZMBLWNCE) VCMI_SOUND_FILE(ZMBLWNCE.wav) \ -VCMI_SOUND_NAME(ZOMBAttack) VCMI_SOUND_FILE(ZOMBATTK.wav) \ -VCMI_SOUND_NAME(ZOMBDefend) VCMI_SOUND_FILE(ZOMBDFND.wav) \ -VCMI_SOUND_NAME(ZOMBKill) VCMI_SOUND_FILE(ZOMBKILL.wav) \ -VCMI_SOUND_NAME(ZOMBMove) VCMI_SOUND_FILE(ZOMBMOVE.wav) \ -VCMI_SOUND_NAME(ZOMBWNCE) VCMI_SOUND_FILE(ZOMBWNCE.wav) - -class soundBase -{ -public: - // Make a list of enums - // We must keep an entry 0 for an invalid or no sound. -#define VCMI_SOUND_NAME(x) x, -#define VCMI_SOUND_FILE(y) - enum soundID { - invalid=0, - sound_todo=1, // temp entry until code is fixed - VCMI_SOUND_LIST - sound_after_last - }; -#undef VCMI_SOUND_FILE -#undef VCMI_SOUND_NAME -}; - -VCMI_LIB_NAMESPACE_END +/* + * CSoundBase.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 + +VCMI_LIB_NAMESPACE_BEGIN + +// Use some magic to keep the list of files and their code name in sync. + +#define VCMI_SOUND_LIST \ +/* Sounds for map actions */ \ +VCMI_SOUND_NAME(KillFade) VCMI_SOUND_FILE(KILLFADE.wav) /* hero or monster disappears */ \ +/* Other sounds (TODO: separate out the sounds for units, spells and the rest */ \ +VCMI_SOUND_NAME(AAGLAttack) VCMI_SOUND_FILE(AAGLATTK.wav) \ +VCMI_SOUND_NAME(AAGLDefend) VCMI_SOUND_FILE(AAGLDFND.wav) \ +VCMI_SOUND_NAME(AAGLKill) VCMI_SOUND_FILE(AAGLKILL.wav) \ +VCMI_SOUND_NAME(AAGLMove) VCMI_SOUND_FILE(AAGLMOVE.wav) \ +VCMI_SOUND_NAME(AAGLWNCE) VCMI_SOUND_FILE(AAGLWNCE.wav) \ +VCMI_SOUND_NAME(acid) VCMI_SOUND_FILE(ACID.wav) \ +VCMI_SOUND_NAME(ADVLAttack) VCMI_SOUND_FILE(ADVLATTK.wav) \ +VCMI_SOUND_NAME(ADVLDefend) VCMI_SOUND_FILE(ADVLDFND.wav) \ +VCMI_SOUND_NAME(ADVLEXT1) VCMI_SOUND_FILE(ADVLEXT1.wav) \ +VCMI_SOUND_NAME(ADVLEXT2) VCMI_SOUND_FILE(ADVLEXT2.wav) \ +VCMI_SOUND_NAME(ADVLKill) VCMI_SOUND_FILE(ADVLKILL.wav) \ +VCMI_SOUND_NAME(ADVLMove) VCMI_SOUND_FILE(ADVLMOVE.wav) \ +VCMI_SOUND_NAME(ADVLWNCE) VCMI_SOUND_FILE(ADVLWNCE.wav) \ +VCMI_SOUND_NAME(AELMAttack) VCMI_SOUND_FILE(AELMATTK.wav) \ +VCMI_SOUND_NAME(AELMDefend) VCMI_SOUND_FILE(AELMDFND.wav) \ +VCMI_SOUND_NAME(AELMKill) VCMI_SOUND_FILE(AELMKILL.wav) \ +VCMI_SOUND_NAME(AELMMove) VCMI_SOUND_FILE(AELMMOVE.wav) \ +VCMI_SOUND_NAME(AELMWNCE) VCMI_SOUND_FILE(AELMWNCE.wav) \ +VCMI_SOUND_NAME(AGE) VCMI_SOUND_FILE(AGE.wav) \ +VCMI_SOUND_NAME(AGRMAttack) VCMI_SOUND_FILE(AGRMATTK.wav) \ +VCMI_SOUND_NAME(AGRMDefend) VCMI_SOUND_FILE(AGRMDFND.wav) \ +VCMI_SOUND_NAME(AGRMKill) VCMI_SOUND_FILE(AGRMKILL.wav) \ +VCMI_SOUND_NAME(AGRMMove) VCMI_SOUND_FILE(AGRMMOVE.wav) \ +VCMI_SOUND_NAME(AGRMShot) VCMI_SOUND_FILE(AGRMSHOT.wav) \ +VCMI_SOUND_NAME(AGRMWNCE) VCMI_SOUND_FILE(AGRMWNCE.wav) \ +VCMI_SOUND_NAME(AIRSHELD) VCMI_SOUND_FILE(AIRSHELD.wav) \ +VCMI_SOUND_NAME(ALIZAttack) VCMI_SOUND_FILE(ALIZATTK.wav) \ +VCMI_SOUND_NAME(ALIZDefend) VCMI_SOUND_FILE(ALIZDFND.wav) \ +VCMI_SOUND_NAME(ALIZKill) VCMI_SOUND_FILE(ALIZKILL.wav) \ +VCMI_SOUND_NAME(ALIZMove) VCMI_SOUND_FILE(ALIZMOVE.wav) \ +VCMI_SOUND_NAME(ALIZShot) VCMI_SOUND_FILE(ALIZSHOT.wav) \ +VCMI_SOUND_NAME(ALIZWNCE) VCMI_SOUND_FILE(ALIZWNCE.wav) \ +VCMI_SOUND_NAME(AMAGAttack) VCMI_SOUND_FILE(AMAGATTK.wav) \ +VCMI_SOUND_NAME(AMAGDefend) VCMI_SOUND_FILE(AMAGDFND.wav) \ +VCMI_SOUND_NAME(AMAGKill) VCMI_SOUND_FILE(AMAGKILL.wav) \ +VCMI_SOUND_NAME(AMAGMove) VCMI_SOUND_FILE(AMAGMOVE.wav) \ +VCMI_SOUND_NAME(AMAGShot) VCMI_SOUND_FILE(AMAGSHOT.wav) \ +VCMI_SOUND_NAME(AMAGWNCE) VCMI_SOUND_FILE(AMAGWNCE.wav) \ +VCMI_SOUND_NAME(ANGLAttack) VCMI_SOUND_FILE(ANGLATTK.wav) \ +VCMI_SOUND_NAME(ANGLDefend) VCMI_SOUND_FILE(ANGLDFND.wav) \ +VCMI_SOUND_NAME(ANGLKill) VCMI_SOUND_FILE(ANGLKILL.wav) \ +VCMI_SOUND_NAME(ANGLMove) VCMI_SOUND_FILE(ANGLMOVE.wav) \ +VCMI_SOUND_NAME(ANGLWNCE) VCMI_SOUND_FILE(ANGLWNCE.wav) \ +VCMI_SOUND_NAME(ANIMDEAD) VCMI_SOUND_FILE(ANIMDEAD.wav) \ +VCMI_SOUND_NAME(ANTIMAGK) VCMI_SOUND_FILE(ANTIMAGK.wav) \ +VCMI_SOUND_NAME(APEGAttack) VCMI_SOUND_FILE(APEGATTK.wav) \ +VCMI_SOUND_NAME(APEGDefend) VCMI_SOUND_FILE(APEGDFND.wav) \ +VCMI_SOUND_NAME(APEGKill) VCMI_SOUND_FILE(APEGKILL.wav) \ +VCMI_SOUND_NAME(APEGMove) VCMI_SOUND_FILE(APEGMOVE.wav) \ +VCMI_SOUND_NAME(APEGWNCE) VCMI_SOUND_FILE(APEGWNCE.wav) \ +VCMI_SOUND_NAME(ARMGEDN) VCMI_SOUND_FILE(ARMGEDN.wav) \ +VCMI_SOUND_NAME(AZURAttack) VCMI_SOUND_FILE(AZURATTK.wav) \ +VCMI_SOUND_NAME(AZURDefend) VCMI_SOUND_FILE(AZURDFND.wav) \ +VCMI_SOUND_NAME(AZURKill) VCMI_SOUND_FILE(AZURKILL.wav) \ +VCMI_SOUND_NAME(AZURMove) VCMI_SOUND_FILE(AZURMOVE.wav) \ +VCMI_SOUND_NAME(AZURWNCE) VCMI_SOUND_FILE(AZURWNCE.wav) \ +VCMI_SOUND_NAME(BACKLASH) VCMI_SOUND_FILE(BACKLASH.wav) \ +VCMI_SOUND_NAME(BADLUCK) VCMI_SOUND_FILE(BADLUCK.wav) \ +VCMI_SOUND_NAME(BADMRLE) VCMI_SOUND_FILE(BADMRLE.wav) \ +VCMI_SOUND_NAME(BALLKill) VCMI_SOUND_FILE(BALLKILL.wav) \ +VCMI_SOUND_NAME(BALLShot) VCMI_SOUND_FILE(BALLSHOT.wav) \ +VCMI_SOUND_NAME(BALLWNCE) VCMI_SOUND_FILE(BALLWNCE.wav) \ +VCMI_SOUND_NAME(BASLAttack) VCMI_SOUND_FILE(BASLATTK.wav) \ +VCMI_SOUND_NAME(BASLDefend) VCMI_SOUND_FILE(BASLDFND.wav) \ +VCMI_SOUND_NAME(BASLKill) VCMI_SOUND_FILE(BASLKILL.wav) \ +VCMI_SOUND_NAME(BASLMove) VCMI_SOUND_FILE(BASLMOVE.wav) \ +VCMI_SOUND_NAME(BASLWNCE) VCMI_SOUND_FILE(BASLWNCE.wav) \ +VCMI_SOUND_NAME(battle00) VCMI_SOUND_FILE(BATTLE00.wav) \ +VCMI_SOUND_NAME(battle01) VCMI_SOUND_FILE(BATTLE01.wav) \ +VCMI_SOUND_NAME(battle02) VCMI_SOUND_FILE(BATTLE02.wav) \ +VCMI_SOUND_NAME(battle03) VCMI_SOUND_FILE(BATTLE03.wav) \ +VCMI_SOUND_NAME(battle04) VCMI_SOUND_FILE(BATTLE04.wav) \ +VCMI_SOUND_NAME(battle05) VCMI_SOUND_FILE(BATTLE05.wav) \ +VCMI_SOUND_NAME(battle06) VCMI_SOUND_FILE(BATTLE06.wav) \ +VCMI_SOUND_NAME(battle07) VCMI_SOUND_FILE(BATTLE07.wav) \ +VCMI_SOUND_NAME(BDRFAttack) VCMI_SOUND_FILE(BDRFATTK.wav) \ +VCMI_SOUND_NAME(BDRFDefend) VCMI_SOUND_FILE(BDRFDFND.wav) \ +VCMI_SOUND_NAME(BDRFKill) VCMI_SOUND_FILE(BDRFKILL.wav) \ +VCMI_SOUND_NAME(BDRFMove) VCMI_SOUND_FILE(BDRFMOVE.wav) \ +VCMI_SOUND_NAME(BDRFWNCE) VCMI_SOUND_FILE(BDRFWNCE.wav) \ +VCMI_SOUND_NAME(BERSERK) VCMI_SOUND_FILE(BERSERK.wav) \ +VCMI_SOUND_NAME(BGORAttack) VCMI_SOUND_FILE(BGORATTK.wav) \ +VCMI_SOUND_NAME(BGORDefend) VCMI_SOUND_FILE(BGORDFND.wav) \ +VCMI_SOUND_NAME(BGORKill) VCMI_SOUND_FILE(BGORKILL.wav) \ +VCMI_SOUND_NAME(BGORMove) VCMI_SOUND_FILE(BGORMOVE.wav) \ +VCMI_SOUND_NAME(BGORWNCE) VCMI_SOUND_FILE(BGORWNCE.wav) \ +VCMI_SOUND_NAME(BHDRAttack) VCMI_SOUND_FILE(BHDRATTK.wav) \ +VCMI_SOUND_NAME(BHDRDETH) VCMI_SOUND_FILE(BHDRDETH.wav) \ +VCMI_SOUND_NAME(BHDRDefend) VCMI_SOUND_FILE(BHDRDFND.wav) \ +VCMI_SOUND_NAME(BHDRKill) VCMI_SOUND_FILE(BHDRKILL.wav) \ +VCMI_SOUND_NAME(BHDRMove) VCMI_SOUND_FILE(BHDRMOVE.wav) \ +VCMI_SOUND_NAME(BHDRShot) VCMI_SOUND_FILE(BHDRSHOT.wav) \ +VCMI_SOUND_NAME(BHDRWNCE) VCMI_SOUND_FILE(BHDRWNCE.wav) \ +VCMI_SOUND_NAME(BIND) VCMI_SOUND_FILE(BIND.wav) \ +VCMI_SOUND_NAME(BKDRAttack) VCMI_SOUND_FILE(BKDRATTK.wav) \ +VCMI_SOUND_NAME(BKDRDefend) VCMI_SOUND_FILE(BKDRDFND.wav) \ +VCMI_SOUND_NAME(BKDRKill) VCMI_SOUND_FILE(BKDRKILL.wav) \ +VCMI_SOUND_NAME(BKDRMove) VCMI_SOUND_FILE(BKDRMOVE.wav) \ +VCMI_SOUND_NAME(BKDRWNCE) VCMI_SOUND_FILE(BKDRWNCE.wav) \ +VCMI_SOUND_NAME(BKNTAttack) VCMI_SOUND_FILE(BKNTATTK.wav) \ +VCMI_SOUND_NAME(BKNTDefend) VCMI_SOUND_FILE(BKNTDFND.wav) \ +VCMI_SOUND_NAME(BKNTKill) VCMI_SOUND_FILE(BKNTKILL.wav) \ +VCMI_SOUND_NAME(BKNTMove) VCMI_SOUND_FILE(BKNTMOVE.wav) \ +VCMI_SOUND_NAME(BKNTWNCE) VCMI_SOUND_FILE(BKNTWNCE.wav) \ +VCMI_SOUND_NAME(bless) VCMI_SOUND_FILE(BLESS.wav) \ +VCMI_SOUND_NAME(blind) VCMI_SOUND_FILE(BLIND.wav) \ +VCMI_SOUND_NAME(bloodlus) VCMI_SOUND_FILE(BLOODLUS.wav) \ +VCMI_SOUND_NAME(BLRDAttack) VCMI_SOUND_FILE(BLRDATTK.wav) \ +VCMI_SOUND_NAME(BLRDDefend) VCMI_SOUND_FILE(BLRDDFND.wav) \ +VCMI_SOUND_NAME(BLRDKill) VCMI_SOUND_FILE(BLRDKILL.wav) \ +VCMI_SOUND_NAME(BLRDMove) VCMI_SOUND_FILE(BLRDMOVE.wav) \ +VCMI_SOUND_NAME(BLRDWNCE) VCMI_SOUND_FILE(BLRDWNCE.wav) \ +VCMI_SOUND_NAME(BMTHAttack) VCMI_SOUND_FILE(BMTHATTK.wav) \ +VCMI_SOUND_NAME(BMTHDefend) VCMI_SOUND_FILE(BMTHDFND.wav) \ +VCMI_SOUND_NAME(BMTHKill) VCMI_SOUND_FILE(BMTHKILL.wav) \ +VCMI_SOUND_NAME(BMTHMove) VCMI_SOUND_FILE(BMTHMOVE.wav) \ +VCMI_SOUND_NAME(BMTHWNCE) VCMI_SOUND_FILE(BMTHWNCE.wav) \ +VCMI_SOUND_NAME(BOARAttack) VCMI_SOUND_FILE(BOARATTK.wav) \ +VCMI_SOUND_NAME(BOARDefend) VCMI_SOUND_FILE(BOARDFND.wav) \ +VCMI_SOUND_NAME(BOARKill) VCMI_SOUND_FILE(BOARKILL.wav) \ +VCMI_SOUND_NAME(BOARMove) VCMI_SOUND_FILE(BOARMOVE.wav) \ +VCMI_SOUND_NAME(BOARWNCE) VCMI_SOUND_FILE(BOARWNCE.wav) \ +VCMI_SOUND_NAME(BODRAttack) VCMI_SOUND_FILE(BODRATTK.wav) \ +VCMI_SOUND_NAME(BODRDefend) VCMI_SOUND_FILE(BODRDFND.wav) \ +VCMI_SOUND_NAME(BODRKill) VCMI_SOUND_FILE(BODRKILL.wav) \ +VCMI_SOUND_NAME(BODRMove) VCMI_SOUND_FILE(BODRMOVE.wav) \ +VCMI_SOUND_NAME(BODRWNCE) VCMI_SOUND_FILE(BODRWNCE.wav) \ +VCMI_SOUND_NAME(BTREAttack) VCMI_SOUND_FILE(BTREATTK.wav) \ +VCMI_SOUND_NAME(BTREDefend) VCMI_SOUND_FILE(BTREDFND.wav) \ +VCMI_SOUND_NAME(BTREKill) VCMI_SOUND_FILE(BTREKILL.wav) \ +VCMI_SOUND_NAME(BTREMove) VCMI_SOUND_FILE(BTREMOVE.wav) \ +VCMI_SOUND_NAME(BTREWNCE) VCMI_SOUND_FILE(BTREWNCE.wav) \ +VCMI_SOUND_NAME(newBuilding) VCMI_SOUND_FILE(BUILDTWN.wav) \ +VCMI_SOUND_NAME(button) VCMI_SOUND_FILE(BUTTON.wav) \ +VCMI_SOUND_NAME(CALFAttack) VCMI_SOUND_FILE(CALFATTK.wav) \ +VCMI_SOUND_NAME(CALFDefend) VCMI_SOUND_FILE(CALFDFND.wav) \ +VCMI_SOUND_NAME(CALFKill) VCMI_SOUND_FILE(CALFKILL.wav) \ +VCMI_SOUND_NAME(CALFMove) VCMI_SOUND_FILE(CALFMOVE.wav) \ +VCMI_SOUND_NAME(CALFShot) VCMI_SOUND_FILE(CALFSHOT.wav) \ +VCMI_SOUND_NAME(CALFWNCE) VCMI_SOUND_FILE(CALFWNCE.wav) \ +VCMI_SOUND_NAME(CARTKill) VCMI_SOUND_FILE(CARTKILL.wav) \ +VCMI_SOUND_NAME(CARTWNCE) VCMI_SOUND_FILE(CARTWNCE.wav) \ +VCMI_SOUND_NAME(CATAKill) VCMI_SOUND_FILE(CATAKILL.wav) \ +VCMI_SOUND_NAME(CATAShot) VCMI_SOUND_FILE(CATASHOT.wav) \ +VCMI_SOUND_NAME(CATAWNCE) VCMI_SOUND_FILE(CATAWNCE.wav) \ +VCMI_SOUND_NAME(CAVAAttack) VCMI_SOUND_FILE(CAVAATTK.wav) \ +VCMI_SOUND_NAME(CAVADefend) VCMI_SOUND_FILE(CAVADFND.wav) \ +VCMI_SOUND_NAME(CAVAKill) VCMI_SOUND_FILE(CAVAKILL.wav) \ +VCMI_SOUND_NAME(CAVAMove) VCMI_SOUND_FILE(CAVAMOVE.wav) \ +VCMI_SOUND_NAME(CAVAWNCE) VCMI_SOUND_FILE(CAVAWNCE.wav) \ +VCMI_SOUND_NAME(CAVEHEAD) VCMI_SOUND_FILE(CAVEHEAD.wav) \ +VCMI_SOUND_NAME(CCYCAttack) VCMI_SOUND_FILE(CCYCATTK.wav) \ +VCMI_SOUND_NAME(CCYCDefend) VCMI_SOUND_FILE(CCYCDFND.wav) \ +VCMI_SOUND_NAME(CCYCKill) VCMI_SOUND_FILE(CCYCKILL.wav) \ +VCMI_SOUND_NAME(CCYCMove) VCMI_SOUND_FILE(CCYCMOVE.wav) \ +VCMI_SOUND_NAME(CCYCShot) VCMI_SOUND_FILE(CCYCSHOT.wav) \ +VCMI_SOUND_NAME(CCYCWNCE) VCMI_SOUND_FILE(CCYCWNCE.wav) \ +VCMI_SOUND_NAME(CERBAttack) VCMI_SOUND_FILE(CERBATTK.wav) \ +VCMI_SOUND_NAME(CERBDefend) VCMI_SOUND_FILE(CERBDFND.wav) \ +VCMI_SOUND_NAME(CERBKill) VCMI_SOUND_FILE(CERBKILL.wav) \ +VCMI_SOUND_NAME(CERBMove) VCMI_SOUND_FILE(CERBMOVE.wav) \ +VCMI_SOUND_NAME(CERBWNCE) VCMI_SOUND_FILE(CERBWNCE.wav) \ +VCMI_SOUND_NAME(CGORAttack) VCMI_SOUND_FILE(CGORATTK.wav) \ +VCMI_SOUND_NAME(CGORDefend) VCMI_SOUND_FILE(CGORDFND.wav) \ +VCMI_SOUND_NAME(CGORKill) VCMI_SOUND_FILE(CGORKILL.wav) \ +VCMI_SOUND_NAME(CGORMove) VCMI_SOUND_FILE(CGORMOVE.wav) \ +VCMI_SOUND_NAME(CGORWNCE) VCMI_SOUND_FILE(CGORWNCE.wav) \ +VCMI_SOUND_NAME(chainLigthning) VCMI_SOUND_FILE(CHAINLTE.wav) \ +VCMI_SOUND_NAME(chat) VCMI_SOUND_FILE(CHAT.wav) \ +VCMI_SOUND_NAME(chest) VCMI_SOUND_FILE(CHEST.wav) \ +VCMI_SOUND_NAME(CHMPAttack) VCMI_SOUND_FILE(CHMPATTK.wav) \ +VCMI_SOUND_NAME(CHMPDefend) VCMI_SOUND_FILE(CHMPDFND.wav) \ +VCMI_SOUND_NAME(CHMPKill) VCMI_SOUND_FILE(CHMPKILL.wav) \ +VCMI_SOUND_NAME(CHMPMove) VCMI_SOUND_FILE(CHMPMOVE.wav) \ +VCMI_SOUND_NAME(CHMPWNCE) VCMI_SOUND_FILE(CHMPWNCE.wav) \ +VCMI_SOUND_NAME(CHYDAttack) VCMI_SOUND_FILE(CHYDATTK.wav) \ +VCMI_SOUND_NAME(CHYDDefend) VCMI_SOUND_FILE(CHYDDFND.wav) \ +VCMI_SOUND_NAME(CHYDKill) VCMI_SOUND_FILE(CHYDKILL.wav) \ +VCMI_SOUND_NAME(CHYDMove) VCMI_SOUND_FILE(CHYDMOVE.wav) \ +VCMI_SOUND_NAME(CHYDWNCE) VCMI_SOUND_FILE(CHYDWNCE.wav) \ +VCMI_SOUND_NAME(CLIMAX) VCMI_SOUND_FILE(CLIMAX.wav) \ +VCMI_SOUND_NAME(CLONE) VCMI_SOUND_FILE(CLONE.wav) \ +VCMI_SOUND_NAME(CNTRAttack) VCMI_SOUND_FILE(CNTRATTK.wav) \ +VCMI_SOUND_NAME(CNTRDefend) VCMI_SOUND_FILE(CNTRDFND.wav) \ +VCMI_SOUND_NAME(CNTRKill) VCMI_SOUND_FILE(CNTRKILL.wav) \ +VCMI_SOUND_NAME(CNTRMove) VCMI_SOUND_FILE(CNTRMOVE.wav) \ +VCMI_SOUND_NAME(CNTRShot) VCMI_SOUND_FILE(CNTRSHOT.wav) \ +VCMI_SOUND_NAME(Counterstrike) VCMI_SOUND_FILE(CNTRSTRK.wav) \ +VCMI_SOUND_NAME(CNTRWNCE) VCMI_SOUND_FILE(CNTRWNCE.wav) \ +VCMI_SOUND_NAME(COLDRAY) VCMI_SOUND_FILE(COLDRAY.wav) \ +VCMI_SOUND_NAME(COLDRING) VCMI_SOUND_FILE(COLDRING.wav) \ +VCMI_SOUND_NAME(CRUSAttack) VCMI_SOUND_FILE(CRUSATTK.wav) \ +VCMI_SOUND_NAME(CRUSDefend) VCMI_SOUND_FILE(CRUSDFND.wav) \ +VCMI_SOUND_NAME(CRUSKill) VCMI_SOUND_FILE(CRUSKILL.wav) \ +VCMI_SOUND_NAME(CRUSMove) VCMI_SOUND_FILE(CRUSMOVE.wav) \ +VCMI_SOUND_NAME(CRUSWNCE) VCMI_SOUND_FILE(CRUSWNCE.wav) \ +VCMI_SOUND_NAME(CRYSAttack) VCMI_SOUND_FILE(CRYSATTK.wav) \ +VCMI_SOUND_NAME(CRYSDefend) VCMI_SOUND_FILE(CRYSDFND.wav) \ +VCMI_SOUND_NAME(CRYSKill) VCMI_SOUND_FILE(CRYSKILL.wav) \ +VCMI_SOUND_NAME(CRYSMove) VCMI_SOUND_FILE(CRYSMOVE.wav) \ +VCMI_SOUND_NAME(CRYSWNCE) VCMI_SOUND_FILE(CRYSWNCE.wav) \ +VCMI_SOUND_NAME(CURE) VCMI_SOUND_FILE(CURE.wav) \ +VCMI_SOUND_NAME(CURSE) VCMI_SOUND_FILE(CURSE.wav) \ +VCMI_SOUND_NAME(cyclopAttack) VCMI_SOUND_FILE(CYCLATTK.wav) \ +VCMI_SOUND_NAME(cyclopDefend) VCMI_SOUND_FILE(CYCLDFND.wav) \ +VCMI_SOUND_NAME(cyclopKill) VCMI_SOUND_FILE(CYCLKILL.wav) \ +VCMI_SOUND_NAME(cyclopMove) VCMI_SOUND_FILE(CYCLMOVE.wav) \ +VCMI_SOUND_NAME(cyclopShot) VCMI_SOUND_FILE(CYCLSHOT.wav) \ +VCMI_SOUND_NAME(cyclopWNCE) VCMI_SOUND_FILE(CYCLWNCE.wav) \ +VCMI_SOUND_NAME(DANGER) VCMI_SOUND_FILE(DANGER.wav) \ +VCMI_SOUND_NAME(deathBlow) VCMI_SOUND_FILE(DEATHBLO.wav) \ +VCMI_SOUND_NAME(deathCloud) VCMI_SOUND_FILE(DEATHCLD.wav) \ +VCMI_SOUND_NAME(deathRIP) VCMI_SOUND_FILE(DEATHRIP.wav) \ +VCMI_SOUND_NAME(deathSTR) VCMI_SOUND_FILE(DEATHSTR.wav) \ +VCMI_SOUND_NAME(DECAY) VCMI_SOUND_FILE(DECAY.wav) \ +VCMI_SOUND_NAME(DEFAULT) VCMI_SOUND_FILE(DEFAULT.wav) \ +VCMI_SOUND_NAME(DEVLAttack) VCMI_SOUND_FILE(DEVLATTK.wav) \ +VCMI_SOUND_NAME(DEVLDefend) VCMI_SOUND_FILE(DEVLDFND.wav) \ +VCMI_SOUND_NAME(DEVLEXT1) VCMI_SOUND_FILE(DEVLEXT1.wav) \ +VCMI_SOUND_NAME(DEVLEXT2) VCMI_SOUND_FILE(DEVLEXT2.wav) \ +VCMI_SOUND_NAME(DEVLKill) VCMI_SOUND_FILE(DEVLKILL.wav) \ +VCMI_SOUND_NAME(DEVLMove) VCMI_SOUND_FILE(DEVLMOVE.wav) \ +VCMI_SOUND_NAME(DEVLWNCE) VCMI_SOUND_FILE(DEVLWNCE.wav) \ +VCMI_SOUND_NAME(DFLYAttack) VCMI_SOUND_FILE(DFLYATTK.wav) \ +VCMI_SOUND_NAME(DFLYDefend) VCMI_SOUND_FILE(DFLYDFND.wav) \ +VCMI_SOUND_NAME(DFLYKill) VCMI_SOUND_FILE(DFLYKILL.wav) \ +VCMI_SOUND_NAME(DFLYMove) VCMI_SOUND_FILE(DFLYMOVE.wav) \ +VCMI_SOUND_NAME(DFLYWNCE) VCMI_SOUND_FILE(DFLYWNCE.wav) \ +VCMI_SOUND_NAME(DGLMAttack) VCMI_SOUND_FILE(DGLMATTK.wav) \ +VCMI_SOUND_NAME(DGLMDefend) VCMI_SOUND_FILE(DGLMDFND.wav) \ +VCMI_SOUND_NAME(DGLMKill) VCMI_SOUND_FILE(DGLMKILL.wav) \ +VCMI_SOUND_NAME(DGLMMove) VCMI_SOUND_FILE(DGLMMOVE.wav) \ +VCMI_SOUND_NAME(DGLMWNCE) VCMI_SOUND_FILE(DGLMWNCE.wav) \ +VCMI_SOUND_NAME(DHDMAttack) VCMI_SOUND_FILE(DHDMATTK.wav) \ +VCMI_SOUND_NAME(DHDMDefend) VCMI_SOUND_FILE(DHDMDFND.wav) \ +VCMI_SOUND_NAME(DHDMKill) VCMI_SOUND_FILE(DHDMKILL.wav) \ +VCMI_SOUND_NAME(DHDMMove) VCMI_SOUND_FILE(DHDMMOVE.wav) \ +VCMI_SOUND_NAME(DHDMWNCE) VCMI_SOUND_FILE(DHDMWNCE.wav) \ +VCMI_SOUND_NAME(Dig) VCMI_SOUND_FILE(DIGSOUND.wav) \ +VCMI_SOUND_NAME(DIPMAGK) VCMI_SOUND_FILE(DIPMAGK.wav) \ +VCMI_SOUND_NAME(DISEASE) VCMI_SOUND_FILE(DISEASE.wav) \ +VCMI_SOUND_NAME(DISGUISE) VCMI_SOUND_FILE(DISGUISE.wav) \ +VCMI_SOUND_NAME(DISPELL) VCMI_SOUND_FILE(DISPELL.wav) \ +VCMI_SOUND_NAME(DISRUPTR) VCMI_SOUND_FILE(DISRUPTR.wav) \ +VCMI_SOUND_NAME(dragonHall) VCMI_SOUND_FILE(DRAGON.wav) \ +VCMI_SOUND_NAME(DRAINLIF) VCMI_SOUND_FILE(DRAINLIF.wav) \ +VCMI_SOUND_NAME(DRAWBRG) VCMI_SOUND_FILE(DRAWBRG.wav) \ +VCMI_SOUND_NAME(DRGNSLAY) VCMI_SOUND_FILE(DRGNSLAY.wav) \ +VCMI_SOUND_NAME(DWRFAttack) VCMI_SOUND_FILE(DWRFATTK.wav) \ +VCMI_SOUND_NAME(DWRFDefend) VCMI_SOUND_FILE(DWRFDFND.wav) \ +VCMI_SOUND_NAME(DWRFKill) VCMI_SOUND_FILE(DWRFKILL.wav) \ +VCMI_SOUND_NAME(DWRFMove) VCMI_SOUND_FILE(DWRFMOVE.wav) \ +VCMI_SOUND_NAME(DWRFWNCE) VCMI_SOUND_FILE(DWRFWNCE.wav) \ +VCMI_SOUND_NAME(ECNTAttack) VCMI_SOUND_FILE(ECNTATTK.wav) \ +VCMI_SOUND_NAME(ECNTDefend) VCMI_SOUND_FILE(ECNTDFND.wav) \ +VCMI_SOUND_NAME(ECNTKill) VCMI_SOUND_FILE(ECNTKILL.wav) \ +VCMI_SOUND_NAME(ECNTMove) VCMI_SOUND_FILE(ECNTMOVE.wav) \ +VCMI_SOUND_NAME(ECNTWNCE) VCMI_SOUND_FILE(ECNTWNCE.wav) \ +VCMI_SOUND_NAME(EELMAttack) VCMI_SOUND_FILE(EELMATTK.wav) \ +VCMI_SOUND_NAME(EELMDefend) VCMI_SOUND_FILE(EELMDFND.wav) \ +VCMI_SOUND_NAME(EELMKill) VCMI_SOUND_FILE(EELMKILL.wav) \ +VCMI_SOUND_NAME(EELMMove) VCMI_SOUND_FILE(EELMMOVE.wav) \ +VCMI_SOUND_NAME(EELMWNCE) VCMI_SOUND_FILE(EELMWNCE.wav) \ +VCMI_SOUND_NAME(EFRTAttack) VCMI_SOUND_FILE(EFRTATTK.wav) \ +VCMI_SOUND_NAME(EFRTDefend) VCMI_SOUND_FILE(EFRTDFND.wav) \ +VCMI_SOUND_NAME(EFRTKill) VCMI_SOUND_FILE(EFRTKILL.wav) \ +VCMI_SOUND_NAME(EFRTMove) VCMI_SOUND_FILE(EFRTMOVE.wav) \ +VCMI_SOUND_NAME(EFRTWNCE) VCMI_SOUND_FILE(EFRTWNCE.wav) \ +VCMI_SOUND_NAME(ENCHAttack) VCMI_SOUND_FILE(ENCHATTK.wav) \ +VCMI_SOUND_NAME(ENCHDefend) VCMI_SOUND_FILE(ENCHDFND.wav) \ +VCMI_SOUND_NAME(ENCHKill) VCMI_SOUND_FILE(ENCHKILL.wav) \ +VCMI_SOUND_NAME(ENCHMove) VCMI_SOUND_FILE(ENCHMOVE.wav) \ +VCMI_SOUND_NAME(ENCHShot) VCMI_SOUND_FILE(ENCHSHOT.wav) \ +VCMI_SOUND_NAME(ENCHWNCE) VCMI_SOUND_FILE(ENCHWNCE.wav) \ +VCMI_SOUND_NAME(ENERAttack) VCMI_SOUND_FILE(ENERATTK.wav) \ +VCMI_SOUND_NAME(ENERDefend) VCMI_SOUND_FILE(ENERDFND.wav) \ +VCMI_SOUND_NAME(ENERKill) VCMI_SOUND_FILE(ENERKILL.wav) \ +VCMI_SOUND_NAME(ENERMove) VCMI_SOUND_FILE(ENERMOVE.wav) \ +VCMI_SOUND_NAME(ENERWNCE) VCMI_SOUND_FILE(ENERWNCE.wav) \ +VCMI_SOUND_NAME(ERTHQUAK) VCMI_SOUND_FILE(ERTHQUAK.wav) \ +VCMI_SOUND_NAME(ESULAttack) VCMI_SOUND_FILE(ESULATTK.wav) \ +VCMI_SOUND_NAME(ESULDefend) VCMI_SOUND_FILE(ESULDFND.wav) \ +VCMI_SOUND_NAME(ESULKill) VCMI_SOUND_FILE(ESULKILL.wav) \ +VCMI_SOUND_NAME(ESULMove) VCMI_SOUND_FILE(ESULMOVE.wav) \ +VCMI_SOUND_NAME(ESULShot) VCMI_SOUND_FILE(ESULSHOT.wav) \ +VCMI_SOUND_NAME(ESULWNCE) VCMI_SOUND_FILE(ESULWNCE.wav) \ +VCMI_SOUND_NAME(EVLIAttack) VCMI_SOUND_FILE(EVLIATTK.wav) \ +VCMI_SOUND_NAME(EVLIDETH) VCMI_SOUND_FILE(EVLIDETH.wav) \ +VCMI_SOUND_NAME(EVLIDefend) VCMI_SOUND_FILE(EVLIDFND.wav) \ +VCMI_SOUND_NAME(EVLIKill) VCMI_SOUND_FILE(EVLIKILL.wav) \ +VCMI_SOUND_NAME(EVLIMove) VCMI_SOUND_FILE(EVLIMOVE.wav) \ +VCMI_SOUND_NAME(EVLIShot) VCMI_SOUND_FILE(EVLISHOT.wav) \ +VCMI_SOUND_NAME(EVLIWNCE) VCMI_SOUND_FILE(EVLIWNCE.wav) \ +VCMI_SOUND_NAME(experience) VCMI_SOUND_FILE(EXPERNCE.wav) \ +VCMI_SOUND_NAME(FAERAttack) VCMI_SOUND_FILE(FAERATTK.wav) \ +VCMI_SOUND_NAME(FAERDefend) VCMI_SOUND_FILE(FAERDFND.wav) \ +VCMI_SOUND_NAME(faerie) VCMI_SOUND_FILE(FAERIE.wav) \ +VCMI_SOUND_NAME(FAERKill) VCMI_SOUND_FILE(FAERKILL.wav) \ +VCMI_SOUND_NAME(FAERMove) VCMI_SOUND_FILE(FAERMOVE.wav) \ +VCMI_SOUND_NAME(FAERShot) VCMI_SOUND_FILE(FAERSHOT.wav) \ +VCMI_SOUND_NAME(FAERWNCE) VCMI_SOUND_FILE(FAERWNCE.wav) \ +VCMI_SOUND_NAME(FAIDKill) VCMI_SOUND_FILE(FAIDKILL.wav) \ +VCMI_SOUND_NAME(FAIDWNCE) VCMI_SOUND_FILE(FAIDWNCE.wav) \ +VCMI_SOUND_NAME(FDFLAttack) VCMI_SOUND_FILE(FDFLATTK.wav) \ +VCMI_SOUND_NAME(FDFLDefend) VCMI_SOUND_FILE(FDFLDFND.wav) \ +VCMI_SOUND_NAME(FDFLKill) VCMI_SOUND_FILE(FDFLKILL.wav) \ +VCMI_SOUND_NAME(FDFLMove) VCMI_SOUND_FILE(FDFLMOVE.wav) \ +VCMI_SOUND_NAME(FDFLShot) VCMI_SOUND_FILE(FDFLSHOT.wav) \ +VCMI_SOUND_NAME(FDFLWNCE) VCMI_SOUND_FILE(FDFLWNCE.wav) \ +VCMI_SOUND_NAME(FEAR) VCMI_SOUND_FILE(FEAR.wav) \ +VCMI_SOUND_NAME(FELMAttack) VCMI_SOUND_FILE(FELMATTK.wav) \ +VCMI_SOUND_NAME(FELMDefend) VCMI_SOUND_FILE(FELMDFND.wav) \ +VCMI_SOUND_NAME(FELMKill) VCMI_SOUND_FILE(FELMKILL.wav) \ +VCMI_SOUND_NAME(FELMMove) VCMI_SOUND_FILE(FELMMOVE.wav) \ +VCMI_SOUND_NAME(FELMWNCE) VCMI_SOUND_FILE(FELMWNCE.wav) \ +VCMI_SOUND_NAME(FIRBAttack) VCMI_SOUND_FILE(FIRBATTK.wav) \ +VCMI_SOUND_NAME(FIRBDefend) VCMI_SOUND_FILE(FIRBDFND.wav) \ +VCMI_SOUND_NAME(FIRBKill) VCMI_SOUND_FILE(FIRBKILL.wav) \ +VCMI_SOUND_NAME(FIRBMove) VCMI_SOUND_FILE(FIRBMOVE.wav) \ +VCMI_SOUND_NAME(FIRBWNCE) VCMI_SOUND_FILE(FIRBWNCE.wav) \ +VCMI_SOUND_NAME(fireball) VCMI_SOUND_FILE(FIREBALL.wav) \ +VCMI_SOUND_NAME(fireblast) VCMI_SOUND_FILE(FIREBLST.wav) \ +VCMI_SOUND_NAME(FIRESHIE) VCMI_SOUND_FILE(FIRESHIE.wav) \ +VCMI_SOUND_NAME(FIRESHLD) VCMI_SOUND_FILE(FIRESHLD.wav) \ +VCMI_SOUND_NAME(fireStorm) VCMI_SOUND_FILE(FIRESTRM.wav) \ +VCMI_SOUND_NAME(fireWall) VCMI_SOUND_FILE(FIREWALL.wav) \ +VCMI_SOUND_NAME(FLAGMINE) VCMI_SOUND_FILE(FLAGMINE.wav) \ +VCMI_SOUND_NAME(FLYSPELL) VCMI_SOUND_FILE(FLYSPELL.wav) \ +VCMI_SOUND_NAME(FMLRAttack) VCMI_SOUND_FILE(FMLRATTK.wav) \ +VCMI_SOUND_NAME(FMLRDefend) VCMI_SOUND_FILE(FMLRDFND.wav) \ +VCMI_SOUND_NAME(FMLRKill) VCMI_SOUND_FILE(FMLRKILL.wav) \ +VCMI_SOUND_NAME(FMLRMove) VCMI_SOUND_FILE(FMLRMOVE.wav) \ +VCMI_SOUND_NAME(FMLRWNCE) VCMI_SOUND_FILE(FMLRWNCE.wav) \ +VCMI_SOUND_NAME(FORCEFLD) VCMI_SOUND_FILE(FORCEFLD.wav) \ +VCMI_SOUND_NAME(FORGET) VCMI_SOUND_FILE(FORGET.wav) \ +VCMI_SOUND_NAME(FORTUNE) VCMI_SOUND_FILE(FORTUNE.wav) \ +VCMI_SOUND_NAME(FRENZY) VCMI_SOUND_FILE(FRENZY.wav) \ +VCMI_SOUND_NAME(FROSTING) VCMI_SOUND_FILE(FROSTING.wav) \ +VCMI_SOUND_NAME(gazebo) VCMI_SOUND_FILE(GAZEBO.wav) \ +VCMI_SOUND_NAME(GBASAttack) VCMI_SOUND_FILE(GBASATTK.wav) \ +VCMI_SOUND_NAME(GBASDefend) VCMI_SOUND_FILE(GBASDFND.wav) \ +VCMI_SOUND_NAME(GBASKill) VCMI_SOUND_FILE(GBASKILL.wav) \ +VCMI_SOUND_NAME(GBASMove) VCMI_SOUND_FILE(GBASMOVE.wav) \ +VCMI_SOUND_NAME(GBASWNCE) VCMI_SOUND_FILE(GBASWNCE.wav) \ +VCMI_SOUND_NAME(GBLNAttack) VCMI_SOUND_FILE(GBLNATTK.wav) \ +VCMI_SOUND_NAME(GBLNDefend) VCMI_SOUND_FILE(GBLNDFND.wav) \ +VCMI_SOUND_NAME(GBLNKill) VCMI_SOUND_FILE(GBLNKILL.wav) \ +VCMI_SOUND_NAME(GBLNMove) VCMI_SOUND_FILE(GBLNMOVE.wav) \ +VCMI_SOUND_NAME(GBLNWNCE) VCMI_SOUND_FILE(GBLNWNCE.wav) \ +VCMI_SOUND_NAME(GELFAttack) VCMI_SOUND_FILE(GELFATTK.wav) \ +VCMI_SOUND_NAME(GELFDefend) VCMI_SOUND_FILE(GELFDFND.wav) \ +VCMI_SOUND_NAME(GELFKill) VCMI_SOUND_FILE(GELFKILL.wav) \ +VCMI_SOUND_NAME(GELFMove) VCMI_SOUND_FILE(GELFMOVE.wav) \ +VCMI_SOUND_NAME(GELFShot) VCMI_SOUND_FILE(GELFSHOT.wav) \ +VCMI_SOUND_NAME(GELFWNCE) VCMI_SOUND_FILE(GELFWNCE.wav) \ +VCMI_SOUND_NAME(GENIAttack) VCMI_SOUND_FILE(GENIATTK.wav) \ +VCMI_SOUND_NAME(GENIDefend) VCMI_SOUND_FILE(GENIDFND.wav) \ +VCMI_SOUND_NAME(GENIE) VCMI_SOUND_FILE(GENIE.wav) \ +VCMI_SOUND_NAME(GENIKill) VCMI_SOUND_FILE(GENIKILL.wav) \ +VCMI_SOUND_NAME(GENIMove) VCMI_SOUND_FILE(GENIMOVE.wav) \ +VCMI_SOUND_NAME(GENIWNCE) VCMI_SOUND_FILE(GENIWNCE.wav) \ +VCMI_SOUND_NAME(GETPROTECTION) VCMI_SOUND_FILE(GETPROTECTION.wav) \ +VCMI_SOUND_NAME(GGLMAttack) VCMI_SOUND_FILE(GGLMATTK.wav) \ +VCMI_SOUND_NAME(GGLMDefend) VCMI_SOUND_FILE(GGLMDFND.wav) \ +VCMI_SOUND_NAME(GGLMKill) VCMI_SOUND_FILE(GGLMKILL.wav) \ +VCMI_SOUND_NAME(GGLMMove) VCMI_SOUND_FILE(GGLMMOVE.wav) \ +VCMI_SOUND_NAME(GGLMWNCE) VCMI_SOUND_FILE(GGLMWNCE.wav) \ +VCMI_SOUND_NAME(GHDRAttack) VCMI_SOUND_FILE(GHDRATTK.wav) \ +VCMI_SOUND_NAME(GHDRDefend) VCMI_SOUND_FILE(GHDRDFND.wav) \ +VCMI_SOUND_NAME(GHDRKill) VCMI_SOUND_FILE(GHDRKILL.wav) \ +VCMI_SOUND_NAME(GHDRMove) VCMI_SOUND_FILE(GHDRMOVE.wav) \ +VCMI_SOUND_NAME(GHDRWNCE) VCMI_SOUND_FILE(GHDRWNCE.wav) \ +VCMI_SOUND_NAME(GNLMAttack) VCMI_SOUND_FILE(GNLMATTK.wav) \ +VCMI_SOUND_NAME(GNLMDefend) VCMI_SOUND_FILE(GNLMDFND.wav) \ +VCMI_SOUND_NAME(GNLMKill) VCMI_SOUND_FILE(GNLMKILL.wav) \ +VCMI_SOUND_NAME(GNLMMove) VCMI_SOUND_FILE(GNLMMOVE.wav) \ +VCMI_SOUND_NAME(GNLMWNCE) VCMI_SOUND_FILE(GNLMWNCE.wav) \ +VCMI_SOUND_NAME(GNOLAttack) VCMI_SOUND_FILE(GNOLATTK.wav) \ +VCMI_SOUND_NAME(GNOLDefend) VCMI_SOUND_FILE(GNOLDFND.wav) \ +VCMI_SOUND_NAME(GNOLKill) VCMI_SOUND_FILE(GNOLKILL.wav) \ +VCMI_SOUND_NAME(GNOLMove) VCMI_SOUND_FILE(GNOLMOVE.wav) \ +VCMI_SOUND_NAME(GNOLWNCE) VCMI_SOUND_FILE(GNOLWNCE.wav) \ +VCMI_SOUND_NAME(GODRAttack) VCMI_SOUND_FILE(GODRATTK.wav) \ +VCMI_SOUND_NAME(GODRDefend) VCMI_SOUND_FILE(GODRDFND.wav) \ +VCMI_SOUND_NAME(GODRKill) VCMI_SOUND_FILE(GODRKILL.wav) \ +VCMI_SOUND_NAME(GODRMove) VCMI_SOUND_FILE(GODRMOVE.wav) \ +VCMI_SOUND_NAME(GODRWNCE) VCMI_SOUND_FILE(GODRWNCE.wav) \ +VCMI_SOUND_NAME(GOGFLAME) VCMI_SOUND_FILE(GOGFLAME.wav) \ +VCMI_SOUND_NAME(GOGGAttack) VCMI_SOUND_FILE(GOGGATTK.wav) \ +VCMI_SOUND_NAME(GOGGDefend) VCMI_SOUND_FILE(GOGGDFND.wav) \ +VCMI_SOUND_NAME(GOGGKill) VCMI_SOUND_FILE(GOGGKILL.wav) \ +VCMI_SOUND_NAME(GOGGMove) VCMI_SOUND_FILE(GOGGMOVE.wav) \ +VCMI_SOUND_NAME(GOGGShot) VCMI_SOUND_FILE(GOGGSHOT.wav) \ +VCMI_SOUND_NAME(GOGGWNCE) VCMI_SOUND_FILE(GOGGWNCE.wav) \ +VCMI_SOUND_NAME(GOODLUCK) VCMI_SOUND_FILE(GOODLUCK.wav) \ +VCMI_SOUND_NAME(GOODMRLE) VCMI_SOUND_FILE(GOODMRLE.wav) \ +VCMI_SOUND_NAME(GRAVEYARD) VCMI_SOUND_FILE(GRAVEYARD.wav) \ +VCMI_SOUND_NAME(GRDRAttack) VCMI_SOUND_FILE(GRDRATTK.wav) \ +VCMI_SOUND_NAME(GRDRDefend) VCMI_SOUND_FILE(GRDRDFND.wav) \ +VCMI_SOUND_NAME(GRDRKill) VCMI_SOUND_FILE(GRDRKILL.wav) \ +VCMI_SOUND_NAME(GRDRMove) VCMI_SOUND_FILE(GRDRMOVE.wav) \ +VCMI_SOUND_NAME(GRDRWNCE) VCMI_SOUND_FILE(GRDRWNCE.wav) \ +VCMI_SOUND_NAME(GRIFAttack) VCMI_SOUND_FILE(GRIFATTK.wav) \ +VCMI_SOUND_NAME(GRIFDefend) VCMI_SOUND_FILE(GRIFDFND.wav) \ +VCMI_SOUND_NAME(GRIFKill) VCMI_SOUND_FILE(GRIFKILL.wav) \ +VCMI_SOUND_NAME(GRIFMove) VCMI_SOUND_FILE(GRIFMOVE.wav) \ +VCMI_SOUND_NAME(GRIFWNCE) VCMI_SOUND_FILE(GRIFWNCE.wav) \ +VCMI_SOUND_NAME(GTITAttack) VCMI_SOUND_FILE(GTITATTK.wav) \ +VCMI_SOUND_NAME(GTITDefend) VCMI_SOUND_FILE(GTITDFND.wav) \ +VCMI_SOUND_NAME(GTITKill) VCMI_SOUND_FILE(GTITKILL.wav) \ +VCMI_SOUND_NAME(GTITMove) VCMI_SOUND_FILE(GTITMOVE.wav) \ +VCMI_SOUND_NAME(GTITShot) VCMI_SOUND_FILE(GTITSHOT.wav) \ +VCMI_SOUND_NAME(GTITWNCE) VCMI_SOUND_FILE(GTITWNCE.wav) \ +VCMI_SOUND_NAME(GWRDAttack) VCMI_SOUND_FILE(GWRDATTK.wav) \ +VCMI_SOUND_NAME(GWRDDefend) VCMI_SOUND_FILE(GWRDDFND.wav) \ +VCMI_SOUND_NAME(GWRDKill) VCMI_SOUND_FILE(GWRDKILL.wav) \ +VCMI_SOUND_NAME(GWRDMove) VCMI_SOUND_FILE(GWRDMOVE.wav) \ +VCMI_SOUND_NAME(GWRDWNCE) VCMI_SOUND_FILE(GWRDWNCE.wav) \ +VCMI_SOUND_NAME(HALBAttack) VCMI_SOUND_FILE(HALBATTK.wav) \ +VCMI_SOUND_NAME(HALBDefend) VCMI_SOUND_FILE(HALBDFND.wav) \ +VCMI_SOUND_NAME(HALBKill) VCMI_SOUND_FILE(HALBKILL.wav) \ +VCMI_SOUND_NAME(HALBMove) VCMI_SOUND_FILE(HALBMOVE.wav) \ +VCMI_SOUND_NAME(HALBWNCE) VCMI_SOUND_FILE(HALBWNCE.wav) \ +VCMI_SOUND_NAME(HALFAttack) VCMI_SOUND_FILE(HALFATTK.wav) \ +VCMI_SOUND_NAME(HALFDefend) VCMI_SOUND_FILE(HALFDFND.wav) \ +VCMI_SOUND_NAME(HALFKill) VCMI_SOUND_FILE(HALFKILL.wav) \ +VCMI_SOUND_NAME(HALFMove) VCMI_SOUND_FILE(HALFMOVE.wav) \ +VCMI_SOUND_NAME(HALFShot) VCMI_SOUND_FILE(HALFSHOT.wav) \ +VCMI_SOUND_NAME(HALFWNCE) VCMI_SOUND_FILE(HALFWNCE.wav) \ +VCMI_SOUND_NAME(HARPAttack) VCMI_SOUND_FILE(HARPATTK.wav) \ +VCMI_SOUND_NAME(HARPDefend) VCMI_SOUND_FILE(HARPDFND.wav) \ +VCMI_SOUND_NAME(HARPKill) VCMI_SOUND_FILE(HARPKILL.wav) \ +VCMI_SOUND_NAME(HARPMove) VCMI_SOUND_FILE(HARPMOVE.wav) \ +VCMI_SOUND_NAME(HARPWNCE) VCMI_SOUND_FILE(HARPWNCE.wav) \ +VCMI_SOUND_NAME(HASTE) VCMI_SOUND_FILE(HASTE.wav) \ +VCMI_SOUND_NAME(HCRSAttack) VCMI_SOUND_FILE(HCRSATTK.wav) \ +VCMI_SOUND_NAME(HCRSDefend) VCMI_SOUND_FILE(HCRSDFND.wav) \ +VCMI_SOUND_NAME(HCRSKill) VCMI_SOUND_FILE(HCRSKILL.wav) \ +VCMI_SOUND_NAME(HCRSMove) VCMI_SOUND_FILE(HCRSMOVE.wav) \ +VCMI_SOUND_NAME(HCRSShot) VCMI_SOUND_FILE(HCRSSHOT.wav) \ +VCMI_SOUND_NAME(HCRSWNCE) VCMI_SOUND_FILE(HCRSWNCE.wav) \ +VCMI_SOUND_NAME(HGOBAttack) VCMI_SOUND_FILE(HGOBATTK.wav) \ +VCMI_SOUND_NAME(HGOBDefend) VCMI_SOUND_FILE(HGOBDFND.wav) \ +VCMI_SOUND_NAME(HGOBKill) VCMI_SOUND_FILE(HGOBKILL.wav) \ +VCMI_SOUND_NAME(HGOBMove) VCMI_SOUND_FILE(HGOBMOVE.wav) \ +VCMI_SOUND_NAME(HGOBWNCE) VCMI_SOUND_FILE(HGOBWNCE.wav) \ +VCMI_SOUND_NAME(HGWRAttack) VCMI_SOUND_FILE(HGWRATTK.wav) \ +VCMI_SOUND_NAME(HGWRDefend) VCMI_SOUND_FILE(HGWRDFND.wav) \ +VCMI_SOUND_NAME(HGWRKill) VCMI_SOUND_FILE(HGWRKILL.wav) \ +VCMI_SOUND_NAME(HGWRMove) VCMI_SOUND_FILE(HGWRMOVE.wav) \ +VCMI_SOUND_NAME(HGWRWNCE) VCMI_SOUND_FILE(HGWRWNCE.wav) \ +VCMI_SOUND_NAME(HHAGAttack) VCMI_SOUND_FILE(HHAGATTK.wav) \ +VCMI_SOUND_NAME(HHAGDefend) VCMI_SOUND_FILE(HHAGDFND.wav) \ +VCMI_SOUND_NAME(HHAGKill) VCMI_SOUND_FILE(HHAGKILL.wav) \ +VCMI_SOUND_NAME(HHAGMove) VCMI_SOUND_FILE(HHAGMOVE.wav) \ +VCMI_SOUND_NAME(HHAGShot) VCMI_SOUND_FILE(HHAGSHOT.wav) \ +VCMI_SOUND_NAME(HHAGWNCE) VCMI_SOUND_FILE(HHAGWNCE.wav) \ +VCMI_SOUND_NAME(HHNDAttack) VCMI_SOUND_FILE(HHNDATTK.wav) \ +VCMI_SOUND_NAME(HHNDDefend) VCMI_SOUND_FILE(HHNDDFND.wav) \ +VCMI_SOUND_NAME(HHNDKill) VCMI_SOUND_FILE(HHNDKILL.wav) \ +VCMI_SOUND_NAME(HHNDMove) VCMI_SOUND_FILE(HHNDMOVE.wav) \ +VCMI_SOUND_NAME(HHNDWNCE) VCMI_SOUND_FILE(HHNDWNCE.wav) \ +VCMI_SOUND_NAME(horseDirt) VCMI_SOUND_FILE(HORSE00.wav) \ +VCMI_SOUND_NAME(horseSand) VCMI_SOUND_FILE(HORSE01.wav) \ +VCMI_SOUND_NAME(horseGrass) VCMI_SOUND_FILE(HORSE02.wav) \ +VCMI_SOUND_NAME(horseSnow) VCMI_SOUND_FILE(HORSE03.wav) \ +VCMI_SOUND_NAME(horseSwamp) VCMI_SOUND_FILE(HORSE04.wav) \ +VCMI_SOUND_NAME(horseRough) VCMI_SOUND_FILE(HORSE05.wav) \ +VCMI_SOUND_NAME(horseSubterranean) VCMI_SOUND_FILE(HORSE06.wav) \ +VCMI_SOUND_NAME(horseLava) VCMI_SOUND_FILE(HORSE07.wav) \ +VCMI_SOUND_NAME(horseWater) VCMI_SOUND_FILE(HORSE08.wav) \ +VCMI_SOUND_NAME(horseRock) VCMI_SOUND_FILE(HORSE09.wav) \ +VCMI_SOUND_NAME(horseFly) VCMI_SOUND_FILE(HORSE10.wav) \ +VCMI_SOUND_NAME(horsePenaltyDirt) VCMI_SOUND_FILE(HORSE20.wav) \ +VCMI_SOUND_NAME(horsePenaltySand) VCMI_SOUND_FILE(HORSE21.wav) \ +VCMI_SOUND_NAME(horsePenaltyGrass) VCMI_SOUND_FILE(HORSE22.wav) \ +VCMI_SOUND_NAME(horsePenaltySnow) VCMI_SOUND_FILE(HORSE23.wav) \ +VCMI_SOUND_NAME(horsePenaltySwamp) VCMI_SOUND_FILE(HORSE24.wav) \ +VCMI_SOUND_NAME(horsePenaltyRough) VCMI_SOUND_FILE(HORSE25.wav) \ +VCMI_SOUND_NAME(horsePenaltySubterranean) VCMI_SOUND_FILE(HORSE26.wav) \ +VCMI_SOUND_NAME(horsePenaltyLava) VCMI_SOUND_FILE(HORSE27.wav) \ +VCMI_SOUND_NAME(horsePenaltyRock) VCMI_SOUND_FILE(HORSE29.wav) \ +VCMI_SOUND_NAME(hydraAttack) VCMI_SOUND_FILE(HYDRATTK.wav) \ +VCMI_SOUND_NAME(hydraDefend) VCMI_SOUND_FILE(HYDRDFND.wav) \ +VCMI_SOUND_NAME(hydraKill) VCMI_SOUND_FILE(HYDRKILL.wav) \ +VCMI_SOUND_NAME(hydraMove) VCMI_SOUND_FILE(HYDRMOVE.wav) \ +VCMI_SOUND_NAME(hydraWNCE) VCMI_SOUND_FILE(HYDRWNCE.wav) \ +VCMI_SOUND_NAME(HYPNOTIZ) VCMI_SOUND_FILE(HYPNOTIZ.wav) \ +VCMI_SOUND_NAME(ICELAttack) VCMI_SOUND_FILE(ICELATTK.wav) \ +VCMI_SOUND_NAME(ICELDefend) VCMI_SOUND_FILE(ICELDFND.wav) \ +VCMI_SOUND_NAME(ICELKill) VCMI_SOUND_FILE(ICELKILL.wav) \ +VCMI_SOUND_NAME(ICELMove) VCMI_SOUND_FILE(ICELMOVE.wav) \ +VCMI_SOUND_NAME(ICELShot) VCMI_SOUND_FILE(ICELSHOT.wav) \ +VCMI_SOUND_NAME(ICELWNCE) VCMI_SOUND_FILE(ICELWNCE.wav) \ +VCMI_SOUND_NAME(ICERAYEX) VCMI_SOUND_FILE(ICERAYEX.wav) \ +VCMI_SOUND_NAME(ICERAY) VCMI_SOUND_FILE(ICERAY.wav) \ +VCMI_SOUND_NAME(IGLMAttack) VCMI_SOUND_FILE(IGLMATTK.wav) \ +VCMI_SOUND_NAME(IGLMDefend) VCMI_SOUND_FILE(IGLMDFND.wav) \ +VCMI_SOUND_NAME(IGLMKill) VCMI_SOUND_FILE(IGLMKILL.wav) \ +VCMI_SOUND_NAME(IGLMMove) VCMI_SOUND_FILE(IGLMMOVE.wav) \ +VCMI_SOUND_NAME(IGLMWNCE) VCMI_SOUND_FILE(IGLMWNCE.wav) \ +VCMI_SOUND_NAME(IMPPAttack) VCMI_SOUND_FILE(IMPPATTK.wav) \ +VCMI_SOUND_NAME(IMPPDefend) VCMI_SOUND_FILE(IMPPDFND.wav) \ +VCMI_SOUND_NAME(IMPPKill) VCMI_SOUND_FILE(IMPPKILL.wav) \ +VCMI_SOUND_NAME(IMPPMove) VCMI_SOUND_FILE(IMPPMOVE.wav) \ +VCMI_SOUND_NAME(IMPPWNCE) VCMI_SOUND_FILE(IMPPWNCE.wav) \ +VCMI_SOUND_NAME(ITRGAttack) VCMI_SOUND_FILE(ITRGATTK.wav) \ +VCMI_SOUND_NAME(ITRGDefend) VCMI_SOUND_FILE(ITRGDFND.wav) \ +VCMI_SOUND_NAME(ITRGKill) VCMI_SOUND_FILE(ITRGKILL.wav) \ +VCMI_SOUND_NAME(ITRGMove) VCMI_SOUND_FILE(ITRGMOVE.wav) \ +VCMI_SOUND_NAME(ITRGWNCE) VCMI_SOUND_FILE(ITRGWNCE.wav) \ +VCMI_SOUND_NAME(KEEPShot) VCMI_SOUND_FILE(KEEPSHOT.wav) \ +VCMI_SOUND_NAME(LANDKill) VCMI_SOUND_FILE(LANDKILL.wav) \ +VCMI_SOUND_NAME(LANDMINE) VCMI_SOUND_FILE(LANDMINE.wav) \ +VCMI_SOUND_NAME(LCRSAttack) VCMI_SOUND_FILE(LCRSATTK.wav) \ +VCMI_SOUND_NAME(LCRSDefend) VCMI_SOUND_FILE(LCRSDFND.wav) \ +VCMI_SOUND_NAME(LCRSKill) VCMI_SOUND_FILE(LCRSKILL.wav) \ +VCMI_SOUND_NAME(LCRSMove) VCMI_SOUND_FILE(LCRSMOVE.wav) \ +VCMI_SOUND_NAME(LCRSShot) VCMI_SOUND_FILE(LCRSSHOT.wav) \ +VCMI_SOUND_NAME(LCRSWNCE) VCMI_SOUND_FILE(LCRSWNCE.wav) \ +VCMI_SOUND_NAME(LICHATK2) VCMI_SOUND_FILE(LICHATK2.wav) \ +VCMI_SOUND_NAME(LICHAttack) VCMI_SOUND_FILE(LICHATTK.wav) \ +VCMI_SOUND_NAME(LICHDefend) VCMI_SOUND_FILE(LICHDFND.wav) \ +VCMI_SOUND_NAME(LICHKill) VCMI_SOUND_FILE(LICHKILL.wav) \ +VCMI_SOUND_NAME(LICHMove) VCMI_SOUND_FILE(LICHMOVE.wav) \ +VCMI_SOUND_NAME(LICHShot) VCMI_SOUND_FILE(LICHSHOT.wav) \ +VCMI_SOUND_NAME(LICHWNCE) VCMI_SOUND_FILE(LICHWNCE.wav) \ +VCMI_SOUND_NAME(LIGHTBLT) VCMI_SOUND_FILE(LIGHTBLT.wav) \ +VCMI_SOUND_NAME(LIGHTHOUSE) VCMI_SOUND_FILE(LIGHTHOUSE.wav) \ +VCMI_SOUND_NAME(LOOPAIR) VCMI_SOUND_FILE(LOOPAIR.wav) \ +VCMI_SOUND_NAME(LOOPANIM) VCMI_SOUND_FILE(LOOPANIM.wav) \ +VCMI_SOUND_NAME(LOOPARCH) VCMI_SOUND_FILE(LOOPARCH.wav) \ +VCMI_SOUND_NAME(LOOPAREN) VCMI_SOUND_FILE(LOOPAREN.wav) \ +VCMI_SOUND_NAME(LOOPBEHE) VCMI_SOUND_FILE(LOOPBEHE.wav) \ +VCMI_SOUND_NAME(LOOPBIRD) VCMI_SOUND_FILE(LOOPBIRD.wav) \ +VCMI_SOUND_NAME(LOOPBUOY) VCMI_SOUND_FILE(LOOPBUOY.wav) \ +VCMI_SOUND_NAME(LOOPCAMP) VCMI_SOUND_FILE(LOOPCAMP.wav) \ +VCMI_SOUND_NAME(LOOPCAVE) VCMI_SOUND_FILE(LOOPCAVE.wav) \ +VCMI_SOUND_NAME(LOOPCRYS) VCMI_SOUND_FILE(LOOPCRYS.wav) \ +VCMI_SOUND_NAME(LOOPCURS) VCMI_SOUND_FILE(LOOPCURS.wav) \ +VCMI_SOUND_NAME(LOOPDEAD) VCMI_SOUND_FILE(LOOPDEAD.wav) \ +VCMI_SOUND_NAME(LOOPDEN) VCMI_SOUND_FILE(LOOPDEN.wav) \ +VCMI_SOUND_NAME(LOOPDEVL) VCMI_SOUND_FILE(LOOPDEVL.wav) \ +VCMI_SOUND_NAME(LOOPDOG) VCMI_SOUND_FILE(LOOPDOG.wav) \ +VCMI_SOUND_NAME(LOOPDRAG) VCMI_SOUND_FILE(LOOPDRAG.wav) \ +VCMI_SOUND_NAME(LOOPDWAR) VCMI_SOUND_FILE(LOOPDWAR.wav) \ +VCMI_SOUND_NAME(LOOPEART) VCMI_SOUND_FILE(LOOPEART.wav) \ +VCMI_SOUND_NAME(LOOPELF) VCMI_SOUND_FILE(LOOPELF.wav) \ +VCMI_SOUND_NAME(LOOPFACT) VCMI_SOUND_FILE(LOOPFACT.wav) \ +VCMI_SOUND_NAME(LOOPFAER) VCMI_SOUND_FILE(LOOPFAER.wav) \ +VCMI_SOUND_NAME(LOOPFALL) VCMI_SOUND_FILE(LOOPFALL.wav) \ +VCMI_SOUND_NAME(LOOPFIRE) VCMI_SOUND_FILE(LOOPFIRE.wav) \ +VCMI_SOUND_NAME(LOOPFLAG) VCMI_SOUND_FILE(LOOPFLAG.wav) \ +VCMI_SOUND_NAME(LOOPFOUN) VCMI_SOUND_FILE(LOOPFOUN.wav) \ +VCMI_SOUND_NAME(LOOPGARD) VCMI_SOUND_FILE(LOOPGARD.wav) \ +VCMI_SOUND_NAME(LOOPGATE) VCMI_SOUND_FILE(LOOPGATE.wav) \ +VCMI_SOUND_NAME(LOOPGEMP) VCMI_SOUND_FILE(LOOPGEMP.wav) \ +VCMI_SOUND_NAME(LOOPGOBL) VCMI_SOUND_FILE(LOOPGOBL.wav) \ +VCMI_SOUND_NAME(LOOPGREM) VCMI_SOUND_FILE(LOOPGREM.wav) \ +VCMI_SOUND_NAME(LOOPGRIF) VCMI_SOUND_FILE(LOOPGRIF.wav) \ +VCMI_SOUND_NAME(LOOPHARP) VCMI_SOUND_FILE(LOOPHARP.wav) \ +VCMI_SOUND_NAME(LOOPHORS) VCMI_SOUND_FILE(LOOPHORS.wav) \ +VCMI_SOUND_NAME(LOOPHYDR) VCMI_SOUND_FILE(LOOPHYDR.wav) \ +VCMI_SOUND_NAME(LOOPLEAR) VCMI_SOUND_FILE(LOOPLEAR.wav) \ +VCMI_SOUND_NAME(LOOPLEPR) VCMI_SOUND_FILE(LOOPLEPR.wav) \ +VCMI_SOUND_NAME(LOOPLUMB) VCMI_SOUND_FILE(LOOPLUMB.wav) \ +VCMI_SOUND_NAME(LOOPMAGI) VCMI_SOUND_FILE(LOOPMAGI.wav) \ +VCMI_SOUND_NAME(LOOPMANT) VCMI_SOUND_FILE(LOOPMANT.wav) \ +VCMI_SOUND_NAME(LOOPMARK) VCMI_SOUND_FILE(LOOPMARK.wav) \ +VCMI_SOUND_NAME(LOOPMEDU) VCMI_SOUND_FILE(LOOPMEDU.wav) \ +VCMI_SOUND_NAME(LOOPMERC) VCMI_SOUND_FILE(LOOPMERC.wav) \ +VCMI_SOUND_NAME(LOOPMILL) VCMI_SOUND_FILE(LOOPMILL.wav) \ +VCMI_SOUND_NAME(LOOPMINE) VCMI_SOUND_FILE(LOOPMINE.wav) \ +VCMI_SOUND_NAME(LOOPMON1) VCMI_SOUND_FILE(LOOPMON1.wav) \ +VCMI_SOUND_NAME(LOOPMON2) VCMI_SOUND_FILE(LOOPMON2.wav) \ +VCMI_SOUND_NAME(LOOPMONK) VCMI_SOUND_FILE(LOOPMONK.wav) \ +VCMI_SOUND_NAME(LOOPMONS) VCMI_SOUND_FILE(LOOPMONS.wav) \ +VCMI_SOUND_NAME(LOOPNAGA) VCMI_SOUND_FILE(LOOPNAGA.wav) \ +VCMI_SOUND_NAME(LOOPOCEA) VCMI_SOUND_FILE(LOOPOCEA.wav) \ +VCMI_SOUND_NAME(LOOPOGRE) VCMI_SOUND_FILE(LOOPOGRE.wav) \ +VCMI_SOUND_NAME(LOOPORC) VCMI_SOUND_FILE(LOOPORC.wav) \ +VCMI_SOUND_NAME(LOOPPEGA) VCMI_SOUND_FILE(LOOPPEGA.wav) \ +VCMI_SOUND_NAME(LOOPPIKE) VCMI_SOUND_FILE(LOOPPIKE.wav) \ +VCMI_SOUND_NAME(LOOPSANC) VCMI_SOUND_FILE(LOOPSANC.wav) \ +VCMI_SOUND_NAME(LOOPSHRIN) VCMI_SOUND_FILE(LOOPSHRIN.wav) \ +VCMI_SOUND_NAME(LOOPSIRE) VCMI_SOUND_FILE(LOOPSIRE.wav) \ +VCMI_SOUND_NAME(LOOPSKEL) VCMI_SOUND_FILE(LOOPSKEL.wav) \ +VCMI_SOUND_NAME(LOOPSTAR) VCMI_SOUND_FILE(LOOPSTAR.wav) \ +VCMI_SOUND_NAME(LOOPSULF) VCMI_SOUND_FILE(LOOPSULF.wav) \ +VCMI_SOUND_NAME(LOOPSWAR) VCMI_SOUND_FILE(LOOPSWAR.wav) \ +VCMI_SOUND_NAME(LOOPSWOR) VCMI_SOUND_FILE(LOOPSWOR.wav) \ +VCMI_SOUND_NAME(LOOPTAV) VCMI_SOUND_FILE(LOOPTAV.wav) \ +VCMI_SOUND_NAME(LOOPTITA) VCMI_SOUND_FILE(LOOPTITA.wav) \ +VCMI_SOUND_NAME(LOOPUNIC) VCMI_SOUND_FILE(LOOPUNIC.wav) \ +VCMI_SOUND_NAME(LOOPVENT) VCMI_SOUND_FILE(LOOPVENT.wav) \ +VCMI_SOUND_NAME(LOOPVOLC) VCMI_SOUND_FILE(LOOPVOLC.wav) \ +VCMI_SOUND_NAME(LOOPWHIR) VCMI_SOUND_FILE(LOOPWHIR.wav) \ +VCMI_SOUND_NAME(LOOPWIND) VCMI_SOUND_FILE(LOOPWIND.wav) \ +VCMI_SOUND_NAME(LOOPWOLF) VCMI_SOUND_FILE(LOOPWOLF.wav) \ +VCMI_SOUND_NAME(LTITAttack) VCMI_SOUND_FILE(LTITATTK.wav) \ +VCMI_SOUND_NAME(LTITDefend) VCMI_SOUND_FILE(LTITDFND.wav) \ +VCMI_SOUND_NAME(LTITKill) VCMI_SOUND_FILE(LTITKILL.wav) \ +VCMI_SOUND_NAME(LTITMove) VCMI_SOUND_FILE(LTITMOVE.wav) \ +VCMI_SOUND_NAME(LTITWNCE) VCMI_SOUND_FILE(LTITWNCE.wav) \ +VCMI_SOUND_NAME(LUCK) VCMI_SOUND_FILE(LUCK.wav) \ +VCMI_SOUND_NAME(MAGCAROW) VCMI_SOUND_FILE(MAGCAROW.wav) \ +VCMI_SOUND_NAME(MAGCHDRN) VCMI_SOUND_FILE(MAGCHDRN.wav) \ +VCMI_SOUND_NAME(MAGCHFIL) VCMI_SOUND_FILE(MAGCHFIL.wav) \ +VCMI_SOUND_NAME(MAGEAttack) VCMI_SOUND_FILE(MAGEATTK.wav) \ +VCMI_SOUND_NAME(MAGEDefend) VCMI_SOUND_FILE(MAGEDFND.wav) \ +VCMI_SOUND_NAME(MAGEKill) VCMI_SOUND_FILE(MAGEKILL.wav) \ +VCMI_SOUND_NAME(MAGEMove) VCMI_SOUND_FILE(MAGEMOVE.wav) \ +VCMI_SOUND_NAME(MAGEShot) VCMI_SOUND_FILE(MAGESHOT.wav) \ +VCMI_SOUND_NAME(MAGEWNCE) VCMI_SOUND_FILE(MAGEWNCE.wav) \ +VCMI_SOUND_NAME(MAGICBLT) VCMI_SOUND_FILE(MAGICBLT.wav) \ +VCMI_SOUND_NAME(MAGICRES) VCMI_SOUND_FILE(MAGICRES.wav) \ +VCMI_SOUND_NAME(MAGMAttack) VCMI_SOUND_FILE(MAGMATTK.wav) \ +VCMI_SOUND_NAME(MAGMDefend) VCMI_SOUND_FILE(MAGMDFND.wav) \ +VCMI_SOUND_NAME(MAGMKill) VCMI_SOUND_FILE(MAGMKILL.wav) \ +VCMI_SOUND_NAME(MAGMMove) VCMI_SOUND_FILE(MAGMMOVE.wav) \ +VCMI_SOUND_NAME(MAGMWNCE) VCMI_SOUND_FILE(MAGMWNCE.wav) \ +VCMI_SOUND_NAME(MANADRAI) VCMI_SOUND_FILE(MANADRAI.wav) \ +VCMI_SOUND_NAME(MANTAttack) VCMI_SOUND_FILE(MANTATTK.wav) \ +VCMI_SOUND_NAME(MANTDefend) VCMI_SOUND_FILE(MANTDFND.wav) \ +VCMI_SOUND_NAME(MANTKill) VCMI_SOUND_FILE(MANTKILL.wav) \ +VCMI_SOUND_NAME(MANTMove) VCMI_SOUND_FILE(MANTMOVE.wav) \ +VCMI_SOUND_NAME(MANTShot) VCMI_SOUND_FILE(MANTSHOT.wav) \ +VCMI_SOUND_NAME(MANTWNCE) VCMI_SOUND_FILE(MANTWNCE.wav) \ +VCMI_SOUND_NAME(MEDQAttack) VCMI_SOUND_FILE(MEDQATTK.wav) \ +VCMI_SOUND_NAME(MEDQDefend) VCMI_SOUND_FILE(MEDQDFND.wav) \ +VCMI_SOUND_NAME(MEDQKill) VCMI_SOUND_FILE(MEDQKILL.wav) \ +VCMI_SOUND_NAME(MEDQMove) VCMI_SOUND_FILE(MEDQMOVE.wav) \ +VCMI_SOUND_NAME(MEDQShot) VCMI_SOUND_FILE(MEDQSHOT.wav) \ +VCMI_SOUND_NAME(MEDQWNCE) VCMI_SOUND_FILE(MEDQWNCE.wav) \ +VCMI_SOUND_NAME(MEDUAttack) VCMI_SOUND_FILE(MEDUATTK.wav) \ +VCMI_SOUND_NAME(MEDUDefend) VCMI_SOUND_FILE(MEDUDFND.wav) \ +VCMI_SOUND_NAME(MEDUKill) VCMI_SOUND_FILE(MEDUKILL.wav) \ +VCMI_SOUND_NAME(MEDUMove) VCMI_SOUND_FILE(MEDUMOVE.wav) \ +VCMI_SOUND_NAME(MEDUShot) VCMI_SOUND_FILE(MEDUSHOT.wav) \ +VCMI_SOUND_NAME(MEDUWNCE) VCMI_SOUND_FILE(MEDUWNCE.wav) \ +VCMI_SOUND_NAME(METEOR) VCMI_SOUND_FILE(METEOR.wav) \ +VCMI_SOUND_NAME(MGELAttack) VCMI_SOUND_FILE(MGELATTK.wav) \ +VCMI_SOUND_NAME(MGELDefend) VCMI_SOUND_FILE(MGELDFND.wav) \ +VCMI_SOUND_NAME(MGELKill) VCMI_SOUND_FILE(MGELKILL.wav) \ +VCMI_SOUND_NAME(MGELMove) VCMI_SOUND_FILE(MGELMOVE.wav) \ +VCMI_SOUND_NAME(MGELWNCE) VCMI_SOUND_FILE(MGELWNCE.wav) \ +VCMI_SOUND_NAME(MGOGAttack) VCMI_SOUND_FILE(MGOGATTK.wav) \ +VCMI_SOUND_NAME(MGOGDefend) VCMI_SOUND_FILE(MGOGDFND.wav) \ +VCMI_SOUND_NAME(MGOGKill) VCMI_SOUND_FILE(MGOGKILL.wav) \ +VCMI_SOUND_NAME(MGOGMove) VCMI_SOUND_FILE(MGOGMOVE.wav) \ +VCMI_SOUND_NAME(MGOGShot) VCMI_SOUND_FILE(MGOGSHOT.wav) \ +VCMI_SOUND_NAME(MGOGWNCE) VCMI_SOUND_FILE(MGOGWNCE.wav) \ +VCMI_SOUND_NAME(MGRMAttack) VCMI_SOUND_FILE(MGRMATTK.wav) \ +VCMI_SOUND_NAME(MGRMDefend) VCMI_SOUND_FILE(MGRMDFND.wav) \ +VCMI_SOUND_NAME(MGRMKill) VCMI_SOUND_FILE(MGRMKILL.wav) \ +VCMI_SOUND_NAME(MGRMMove) VCMI_SOUND_FILE(MGRMMOVE.wav) \ +VCMI_SOUND_NAME(MGRMShot) VCMI_SOUND_FILE(MGRMSHOT.wav) \ +VCMI_SOUND_NAME(MGRMWNCE) VCMI_SOUND_FILE(MGRMWNCE.wav) \ +VCMI_SOUND_NAME(MILITARY) VCMI_SOUND_FILE(MILITARY.wav) \ +VCMI_SOUND_NAME(MINKAttack) VCMI_SOUND_FILE(MINKATTK.wav) \ +VCMI_SOUND_NAME(MINKDefend) VCMI_SOUND_FILE(MINKDFND.wav) \ +VCMI_SOUND_NAME(MINKKill) VCMI_SOUND_FILE(MINKKILL.wav) \ +VCMI_SOUND_NAME(MINKMove) VCMI_SOUND_FILE(MINKMOVE.wav) \ +VCMI_SOUND_NAME(MINKShot) VCMI_SOUND_FILE(MINKSHOT.wav) \ +VCMI_SOUND_NAME(MINKWNCE) VCMI_SOUND_FILE(MINKWNCE.wav) \ +VCMI_SOUND_NAME(MINOAttack) VCMI_SOUND_FILE(MINOATTK.wav) \ +VCMI_SOUND_NAME(MINODefend) VCMI_SOUND_FILE(MINODFND.wav) \ +VCMI_SOUND_NAME(MINOKill) VCMI_SOUND_FILE(MINOKILL.wav) \ +VCMI_SOUND_NAME(MINOMove) VCMI_SOUND_FILE(MINOMOVE.wav) \ +VCMI_SOUND_NAME(MINOWNCE) VCMI_SOUND_FILE(MINOWNCE.wav) \ +VCMI_SOUND_NAME(MIRRORIM) VCMI_SOUND_FILE(MIRRORIM.wav) \ +VCMI_SOUND_NAME(MIRTH) VCMI_SOUND_FILE(MIRTH.wav) \ +VCMI_SOUND_NAME(MISFORT) VCMI_SOUND_FILE(MISFORT.wav) \ +VCMI_SOUND_NAME(MNRDEATH) VCMI_SOUND_FILE(MNRDEATH.wav) \ +VCMI_SOUND_NAME(monkAttack) VCMI_SOUND_FILE(MONKATTK.wav) \ +VCMI_SOUND_NAME(monkDefend) VCMI_SOUND_FILE(MONKDFND.wav) \ +VCMI_SOUND_NAME(monkKill) VCMI_SOUND_FILE(MONKKILL.wav) \ +VCMI_SOUND_NAME(monkMove) VCMI_SOUND_FILE(MONKMOVE.wav) \ +VCMI_SOUND_NAME(monkShot) VCMI_SOUND_FILE(MONKSHOT.wav) \ +VCMI_SOUND_NAME(monkWNCE) VCMI_SOUND_FILE(MONKWNCE.wav) \ +VCMI_SOUND_NAME(MORALE) VCMI_SOUND_FILE(MORALE.wav) \ +VCMI_SOUND_NAME(MUCKMIRE) VCMI_SOUND_FILE(MUCKMIRE.wav) \ +VCMI_SOUND_NAME(MUMYAttack) VCMI_SOUND_FILE(MUMYATTK.wav) \ +VCMI_SOUND_NAME(MUMYDefend) VCMI_SOUND_FILE(MUMYDFND.wav) \ +VCMI_SOUND_NAME(MUMYKill) VCMI_SOUND_FILE(MUMYKILL.wav) \ +VCMI_SOUND_NAME(MUMYMove) VCMI_SOUND_FILE(MUMYMOVE.wav) \ +VCMI_SOUND_NAME(MUMYWNCE) VCMI_SOUND_FILE(MUMYWNCE.wav) \ +VCMI_SOUND_NAME(MYSTERY) VCMI_SOUND_FILE(MYSTERY.wav) \ +VCMI_SOUND_NAME(newDay) VCMI_SOUND_FILE(NEWDAY.wav) \ +VCMI_SOUND_NAME(newMonth) VCMI_SOUND_FILE(NEWMONTH.wav) \ +VCMI_SOUND_NAME(newWeek) VCMI_SOUND_FILE(NEWWEEK.wav) \ +VCMI_SOUND_NAME(NGRDAttack) VCMI_SOUND_FILE(NGRDATTK.wav) \ +VCMI_SOUND_NAME(NGRDDefend) VCMI_SOUND_FILE(NGRDDFND.wav) \ +VCMI_SOUND_NAME(NGRDKill) VCMI_SOUND_FILE(NGRDKILL.wav) \ +VCMI_SOUND_NAME(NGRDMove) VCMI_SOUND_FILE(NGRDMOVE.wav) \ +VCMI_SOUND_NAME(NGRDWNCE) VCMI_SOUND_FILE(NGRDWNCE.wav) \ +VCMI_SOUND_NAME(NMADAttack) VCMI_SOUND_FILE(NMADATTK.wav) \ +VCMI_SOUND_NAME(NMADDefend) VCMI_SOUND_FILE(NMADDFND.wav) \ +VCMI_SOUND_NAME(NMADKill) VCMI_SOUND_FILE(NMADKILL.wav) \ +VCMI_SOUND_NAME(NMADMove) VCMI_SOUND_FILE(NMADMOVE.wav) \ +VCMI_SOUND_NAME(NMADWNCE) VCMI_SOUND_FILE(NMADWNCE.wav) \ +VCMI_SOUND_NAME(NOMAD) VCMI_SOUND_FILE(NOMAD.wav) \ +VCMI_SOUND_NAME(NOSFAttack) VCMI_SOUND_FILE(NOSFATTK.wav) \ +VCMI_SOUND_NAME(NOSFDefend) VCMI_SOUND_FILE(NOSFDFND.wav) \ +VCMI_SOUND_NAME(NOSFEXT1) VCMI_SOUND_FILE(NOSFEXT1.wav) \ +VCMI_SOUND_NAME(NOSFEXT2) VCMI_SOUND_FILE(NOSFEXT2.wav) \ +VCMI_SOUND_NAME(NOSFKill) VCMI_SOUND_FILE(NOSFKILL.wav) \ +VCMI_SOUND_NAME(NOSFMove) VCMI_SOUND_FILE(NOSFMOVE.wav) \ +VCMI_SOUND_NAME(NOSFShot) VCMI_SOUND_FILE(NOSFSHOT.wav) \ +VCMI_SOUND_NAME(NOSFWNCE) VCMI_SOUND_FILE(NOSFWNCE.wav) \ +VCMI_SOUND_NAME(NSENAttack) VCMI_SOUND_FILE(NSENATTK.wav) \ +VCMI_SOUND_NAME(NSENDefend) VCMI_SOUND_FILE(NSENDFND.wav) \ +VCMI_SOUND_NAME(NSENKill) VCMI_SOUND_FILE(NSENKILL.wav) \ +VCMI_SOUND_NAME(NSENMove) VCMI_SOUND_FILE(NSENMOVE.wav) \ +VCMI_SOUND_NAME(NSENWNCE) VCMI_SOUND_FILE(NSENWNCE.wav) \ +VCMI_SOUND_NAME(heroNewLevel) VCMI_SOUND_FILE(NWHEROLV.wav) \ +VCMI_SOUND_NAME(OBELISK) VCMI_SOUND_FILE(OBELISK.wav) \ +VCMI_SOUND_NAME(OGREAttack) VCMI_SOUND_FILE(OGREATTK.wav) \ +VCMI_SOUND_NAME(OGREDefend) VCMI_SOUND_FILE(OGREDFND.wav) \ +VCMI_SOUND_NAME(OGREKill) VCMI_SOUND_FILE(OGREKILL.wav) \ +VCMI_SOUND_NAME(OGREMove) VCMI_SOUND_FILE(OGREMOVE.wav) \ +VCMI_SOUND_NAME(OGREWNCE) VCMI_SOUND_FILE(OGREWNCE.wav) \ +VCMI_SOUND_NAME(OGRGAttack) VCMI_SOUND_FILE(OGRGATTK.wav) \ +VCMI_SOUND_NAME(OGRGDefend) VCMI_SOUND_FILE(OGRGDFND.wav) \ +VCMI_SOUND_NAME(OGRGKill) VCMI_SOUND_FILE(OGRGKILL.wav) \ +VCMI_SOUND_NAME(OGRGMove) VCMI_SOUND_FILE(OGRGMOVE.wav) \ +VCMI_SOUND_NAME(OGRGWNCE) VCMI_SOUND_FILE(OGRGWNCE.wav) \ +VCMI_SOUND_NAME(OGRMAttack) VCMI_SOUND_FILE(OGRMATTK.wav) \ +VCMI_SOUND_NAME(OGRMDefend) VCMI_SOUND_FILE(OGRMDFND.wav) \ +VCMI_SOUND_NAME(OGRMKill) VCMI_SOUND_FILE(OGRMKILL.wav) \ +VCMI_SOUND_NAME(OGRMMove) VCMI_SOUND_FILE(OGRMMOVE.wav) \ +VCMI_SOUND_NAME(OGRMShot) VCMI_SOUND_FILE(OGRMSHOT.wav) \ +VCMI_SOUND_NAME(OGRMWNCE) VCMI_SOUND_FILE(OGRMWNCE.wav) \ +VCMI_SOUND_NAME(OORCAttack) VCMI_SOUND_FILE(OORCATTK.wav) \ +VCMI_SOUND_NAME(OORCDefend) VCMI_SOUND_FILE(OORCDFND.wav) \ +VCMI_SOUND_NAME(OORCKill) VCMI_SOUND_FILE(OORCKILL.wav) \ +VCMI_SOUND_NAME(OORCMove) VCMI_SOUND_FILE(OORCMOVE.wav) \ +VCMI_SOUND_NAME(OORCShot) VCMI_SOUND_FILE(OORCSHOT.wav) \ +VCMI_SOUND_NAME(OORCWNCE) VCMI_SOUND_FILE(OORCWNCE.wav) \ +VCMI_SOUND_NAME(ORCCAttack) VCMI_SOUND_FILE(ORCCATTK.wav) \ +VCMI_SOUND_NAME(ORCCDefend) VCMI_SOUND_FILE(ORCCDFND.wav) \ +VCMI_SOUND_NAME(ORCCKill) VCMI_SOUND_FILE(ORCCKILL.wav) \ +VCMI_SOUND_NAME(ORCCMove) VCMI_SOUND_FILE(ORCCMOVE.wav) \ +VCMI_SOUND_NAME(ORCCShot) VCMI_SOUND_FILE(ORCCSHOT.wav) \ +VCMI_SOUND_NAME(ORCCWNCE) VCMI_SOUND_FILE(ORCCWNCE.wav) \ +VCMI_SOUND_NAME(PARALYZE) VCMI_SOUND_FILE(PARALYZE.wav) \ +VCMI_SOUND_NAME(PEGAAttack) VCMI_SOUND_FILE(PEGAATTK.wav) \ +VCMI_SOUND_NAME(PEGADefend) VCMI_SOUND_FILE(PEGADFND.wav) \ +VCMI_SOUND_NAME(PEGAKill) VCMI_SOUND_FILE(PEGAKILL.wav) \ +VCMI_SOUND_NAME(PEGAMove) VCMI_SOUND_FILE(PEGAMOVE.wav) \ +VCMI_SOUND_NAME(PEGAWNCE) VCMI_SOUND_FILE(PEGAWNCE.wav) \ +VCMI_SOUND_NAME(PFNDAttack) VCMI_SOUND_FILE(PFNDATTK.wav) \ +VCMI_SOUND_NAME(PFNDDefend) VCMI_SOUND_FILE(PFNDDFND.wav) \ +VCMI_SOUND_NAME(PFNDKill) VCMI_SOUND_FILE(PFNDKILL.wav) \ +VCMI_SOUND_NAME(PFNDMove) VCMI_SOUND_FILE(PFNDMOVE.wav) \ +VCMI_SOUND_NAME(PFNDWNCE) VCMI_SOUND_FILE(PFNDWNCE.wav) \ +VCMI_SOUND_NAME(PFOEAttack) VCMI_SOUND_FILE(PFOEATTK.wav) \ +VCMI_SOUND_NAME(PFOEDefend) VCMI_SOUND_FILE(PFOEDFND.wav) \ +VCMI_SOUND_NAME(PFOEKill) VCMI_SOUND_FILE(PFOEKILL.wav) \ +VCMI_SOUND_NAME(PFOEMove) VCMI_SOUND_FILE(PFOEMOVE.wav) \ +VCMI_SOUND_NAME(PFOEWNCE) VCMI_SOUND_FILE(PFOEWNCE.wav) \ +VCMI_SOUND_NAME(PHOEAttack) VCMI_SOUND_FILE(PHOEATTK.wav) \ +VCMI_SOUND_NAME(PHOEDefend) VCMI_SOUND_FILE(PHOEDFND.wav) \ +VCMI_SOUND_NAME(PHOEKill) VCMI_SOUND_FILE(PHOEKILL.wav) \ +VCMI_SOUND_NAME(PHOEMove) VCMI_SOUND_FILE(PHOEMOVE.wav) \ +VCMI_SOUND_NAME(PHOEWNCE) VCMI_SOUND_FILE(PHOEWNCE.wav) \ +VCMI_SOUND_NAME(pickup01) VCMI_SOUND_FILE(PICKUP01.wav) \ +VCMI_SOUND_NAME(pickup02) VCMI_SOUND_FILE(PICKUP02.wav) \ +VCMI_SOUND_NAME(pickup03) VCMI_SOUND_FILE(PICKUP03.wav) \ +VCMI_SOUND_NAME(pickup04) VCMI_SOUND_FILE(PICKUP04.wav) \ +VCMI_SOUND_NAME(pickup05) VCMI_SOUND_FILE(PICKUP05.wav) \ +VCMI_SOUND_NAME(pickup06) VCMI_SOUND_FILE(PICKUP06.wav) \ +VCMI_SOUND_NAME(pickup07) VCMI_SOUND_FILE(PICKUP07.wav) \ +VCMI_SOUND_NAME(pikemanAttack) VCMI_SOUND_FILE(PIKEATTK.wav) \ +VCMI_SOUND_NAME(pikemanDefend) VCMI_SOUND_FILE(PIKEDFND.wav) \ +VCMI_SOUND_NAME(pikemanKill) VCMI_SOUND_FILE(PIKEKILL.wav) \ +VCMI_SOUND_NAME(pikemanMove) VCMI_SOUND_FILE(PIKEMOVE.wav) \ +VCMI_SOUND_NAME(pikemanWNCE) VCMI_SOUND_FILE(PIKEWNCE.wav) \ +VCMI_SOUND_NAME(pixieAttack) VCMI_SOUND_FILE(PIXIATTK.wav) \ +VCMI_SOUND_NAME(pixieDefend) VCMI_SOUND_FILE(PIXIDFND.wav) \ +VCMI_SOUND_NAME(pixieKill) VCMI_SOUND_FILE(PIXIKILL.wav) \ +VCMI_SOUND_NAME(pixieMove) VCMI_SOUND_FILE(PIXIMOVE.wav) \ +VCMI_SOUND_NAME(pixieWNCE) VCMI_SOUND_FILE(PIXIWNCE.wav) \ +VCMI_SOUND_NAME(PLAYCOME) VCMI_SOUND_FILE(PLAYCOME.wav) \ +VCMI_SOUND_NAME(PLAYEXIT) VCMI_SOUND_FILE(PLAYEXIT.wav) \ +VCMI_SOUND_NAME(PLAYTURN) VCMI_SOUND_FILE(PLAYTURN.wav) \ +VCMI_SOUND_NAME(PLCHAttack) VCMI_SOUND_FILE(PLCHATTK.wav) \ +VCMI_SOUND_NAME(PLCHDefend) VCMI_SOUND_FILE(PLCHDFND.wav) \ +VCMI_SOUND_NAME(PLCHKill) VCMI_SOUND_FILE(PLCHKILL.wav) \ +VCMI_SOUND_NAME(PLCHMove) VCMI_SOUND_FILE(PLCHMOVE.wav) \ +VCMI_SOUND_NAME(PLCHShot) VCMI_SOUND_FILE(PLCHSHOT.wav) \ +VCMI_SOUND_NAME(PLCHWNCE) VCMI_SOUND_FILE(PLCHWNCE.wav) \ +VCMI_SOUND_NAME(PLIZAttack) VCMI_SOUND_FILE(PLIZATTK.wav) \ +VCMI_SOUND_NAME(PLIZDefend) VCMI_SOUND_FILE(PLIZDFND.wav) \ +VCMI_SOUND_NAME(PLIZKill) VCMI_SOUND_FILE(PLIZKILL.wav) \ +VCMI_SOUND_NAME(PLIZMove) VCMI_SOUND_FILE(PLIZMOVE.wav) \ +VCMI_SOUND_NAME(PLIZShot) VCMI_SOUND_FILE(PLIZSHOT.wav) \ +VCMI_SOUND_NAME(PLIZWNCE) VCMI_SOUND_FILE(PLIZWNCE.wav) \ +VCMI_SOUND_NAME(POISON) VCMI_SOUND_FILE(POISON.wav) \ +VCMI_SOUND_NAME(PRAYER) VCMI_SOUND_FILE(PRAYER.wav) \ +VCMI_SOUND_NAME(PRECISON) VCMI_SOUND_FILE(PRECISON.wav) \ +VCMI_SOUND_NAME(PROTECTA) VCMI_SOUND_FILE(PROTECTA.wav) \ +VCMI_SOUND_NAME(PROTECTE) VCMI_SOUND_FILE(PROTECTE.wav) \ +VCMI_SOUND_NAME(PROTECTF) VCMI_SOUND_FILE(PROTECTF.wav) \ +VCMI_SOUND_NAME(PROTECT) VCMI_SOUND_FILE(PROTECT.wav) \ +VCMI_SOUND_NAME(PROTECTW) VCMI_SOUND_FILE(PROTECTW.wav) \ +VCMI_SOUND_NAME(PSNTAttack) VCMI_SOUND_FILE(PSNTATTK.wav) \ +VCMI_SOUND_NAME(PSNTDefend) VCMI_SOUND_FILE(PSNTDFND.wav) \ +VCMI_SOUND_NAME(PSNTKill) VCMI_SOUND_FILE(PSNTKILL.wav) \ +VCMI_SOUND_NAME(PSNTMove) VCMI_SOUND_FILE(PSNTMOVE.wav) \ +VCMI_SOUND_NAME(PSNTWNCE) VCMI_SOUND_FILE(PSNTWNCE.wav) \ +VCMI_SOUND_NAME(PSYCAttack) VCMI_SOUND_FILE(PSYCATTK.wav) \ +VCMI_SOUND_NAME(PSYCDefend) VCMI_SOUND_FILE(PSYCDFND.wav) \ +VCMI_SOUND_NAME(PSYCKill) VCMI_SOUND_FILE(PSYCKILL.wav) \ +VCMI_SOUND_NAME(PSYCMove) VCMI_SOUND_FILE(PSYCMOVE.wav) \ +VCMI_SOUND_NAME(PSYCWNCE) VCMI_SOUND_FILE(PSYCWNCE.wav) \ +VCMI_SOUND_NAME(QUEST) VCMI_SOUND_FILE(QUEST.wav) \ +VCMI_SOUND_NAME(QUIKSAND) VCMI_SOUND_FILE(QUIKSAND.wav) \ +VCMI_SOUND_NAME(RDDRAttack) VCMI_SOUND_FILE(RDDRATTK.wav) \ +VCMI_SOUND_NAME(RDDRDefend) VCMI_SOUND_FILE(RDDRDFND.wav) \ +VCMI_SOUND_NAME(RDDRKill) VCMI_SOUND_FILE(RDDRKILL.wav) \ +VCMI_SOUND_NAME(RDDRMove) VCMI_SOUND_FILE(RDDRMOVE.wav) \ +VCMI_SOUND_NAME(RDDRWNCE) VCMI_SOUND_FILE(RDDRWNCE.wav) \ +VCMI_SOUND_NAME(REGENER) VCMI_SOUND_FILE(REGENER.wav) \ +VCMI_SOUND_NAME(REMoveOB) VCMI_SOUND_FILE(REMOVEOB.wav) \ +VCMI_SOUND_NAME(RESURECT) VCMI_SOUND_FILE(RESURECT.wav) \ +VCMI_SOUND_NAME(RGRFAttack) VCMI_SOUND_FILE(RGRFATTK.wav) \ +VCMI_SOUND_NAME(RGRFDefend) VCMI_SOUND_FILE(RGRFDFND.wav) \ +VCMI_SOUND_NAME(RGRFKill) VCMI_SOUND_FILE(RGRFKILL.wav) \ +VCMI_SOUND_NAME(RGRFMove) VCMI_SOUND_FILE(RGRFMOVE.wav) \ +VCMI_SOUND_NAME(RGRFWNCE) VCMI_SOUND_FILE(RGRFWNCE.wav) \ +VCMI_SOUND_NAME(ROCCAttack) VCMI_SOUND_FILE(ROCCATTK.wav) \ +VCMI_SOUND_NAME(ROCCDefend) VCMI_SOUND_FILE(ROCCDFND.wav) \ +VCMI_SOUND_NAME(ROCCKill) VCMI_SOUND_FILE(ROCCKILL.wav) \ +VCMI_SOUND_NAME(ROCCMove) VCMI_SOUND_FILE(ROCCMOVE.wav) \ +VCMI_SOUND_NAME(ROCCWNCE) VCMI_SOUND_FILE(ROCCWNCE.wav) \ +VCMI_SOUND_NAME(ROGUAttack) VCMI_SOUND_FILE(ROGUATTK.wav) \ +VCMI_SOUND_NAME(ROGUDefend) VCMI_SOUND_FILE(ROGUDFND.wav) \ +VCMI_SOUND_NAME(ROGUE) VCMI_SOUND_FILE(ROGUE.wav) \ +VCMI_SOUND_NAME(ROGUKill) VCMI_SOUND_FILE(ROGUKILL.wav) \ +VCMI_SOUND_NAME(ROGUMove) VCMI_SOUND_FILE(ROGUMOVE.wav) \ +VCMI_SOUND_NAME(ROGUWNCE) VCMI_SOUND_FILE(ROGUWNCE.wav) \ +VCMI_SOUND_NAME(RSBRYFZL) VCMI_SOUND_FILE(RSBRYFZL.wav) \ +VCMI_SOUND_NAME(RUSTAttack) VCMI_SOUND_FILE(RUSTATTK.wav) \ +VCMI_SOUND_NAME(RUSTDefend) VCMI_SOUND_FILE(RUSTDFND.wav) \ +VCMI_SOUND_NAME(RUSTKill) VCMI_SOUND_FILE(RUSTKILL.wav) \ +VCMI_SOUND_NAME(RUSTMove) VCMI_SOUND_FILE(RUSTMOVE.wav) \ +VCMI_SOUND_NAME(RUSTWNCE) VCMI_SOUND_FILE(RUSTWNCE.wav) \ +VCMI_SOUND_NAME(SACBRETH) VCMI_SOUND_FILE(SACBRETH.wav) \ +VCMI_SOUND_NAME(SACRIF1) VCMI_SOUND_FILE(SACRIF1.wav) \ +VCMI_SOUND_NAME(SACRIF2) VCMI_SOUND_FILE(SACRIF2.wav) \ +VCMI_SOUND_NAME(SCRPAttack) VCMI_SOUND_FILE(SCRPATTK.wav) \ +VCMI_SOUND_NAME(SCRPDefend) VCMI_SOUND_FILE(SCRPDFND.wav) \ +VCMI_SOUND_NAME(SCRPKill) VCMI_SOUND_FILE(SCRPKILL.wav) \ +VCMI_SOUND_NAME(SCRPMove) VCMI_SOUND_FILE(SCRPMOVE.wav) \ +VCMI_SOUND_NAME(SCRPShot) VCMI_SOUND_FILE(SCRPSHOT.wav) \ +VCMI_SOUND_NAME(SCRPWNCE) VCMI_SOUND_FILE(SCRPWNCE.wav) \ +VCMI_SOUND_NAME(SCUTBOAT) VCMI_SOUND_FILE(SCUTBOAT.wav) \ +VCMI_SOUND_NAME(SGLMAttack) VCMI_SOUND_FILE(SGLMATTK.wav) \ +VCMI_SOUND_NAME(SGLMDefend) VCMI_SOUND_FILE(SGLMDFND.wav) \ +VCMI_SOUND_NAME(SGLMKill) VCMI_SOUND_FILE(SGLMKILL.wav) \ +VCMI_SOUND_NAME(SGLMMove) VCMI_SOUND_FILE(SGLMMOVE.wav) \ +VCMI_SOUND_NAME(SGLMWNCE) VCMI_SOUND_FILE(SGLMWNCE.wav) \ +VCMI_SOUND_NAME(SGRGAttack) VCMI_SOUND_FILE(SGRGATTK.wav) \ +VCMI_SOUND_NAME(SGRGDefend) VCMI_SOUND_FILE(SGRGDFND.wav) \ +VCMI_SOUND_NAME(SGRGKill) VCMI_SOUND_FILE(SGRGKILL.wav) \ +VCMI_SOUND_NAME(SGRGMove) VCMI_SOUND_FILE(SGRGMOVE.wav) \ +VCMI_SOUND_NAME(SGRGWNCE) VCMI_SOUND_FILE(SGRGWNCE.wav) \ +VCMI_SOUND_NAME(SHDMAttack) VCMI_SOUND_FILE(SHDMATTK.wav) \ +VCMI_SOUND_NAME(SHDMDefend) VCMI_SOUND_FILE(SHDMDFND.wav) \ +VCMI_SOUND_NAME(SHDMKill) VCMI_SOUND_FILE(SHDMKILL.wav) \ +VCMI_SOUND_NAME(SHDMMove) VCMI_SOUND_FILE(SHDMMOVE.wav) \ +VCMI_SOUND_NAME(SHDMWNCE) VCMI_SOUND_FILE(SHDMWNCE.wav) \ +VCMI_SOUND_NAME(SHIELD) VCMI_SOUND_FILE(SHIELD.wav) \ +VCMI_SOUND_NAME(SKELAttack) VCMI_SOUND_FILE(SKELATTK.wav) \ +VCMI_SOUND_NAME(SKELDefend) VCMI_SOUND_FILE(SKELDFND.wav) \ +VCMI_SOUND_NAME(SKELKill) VCMI_SOUND_FILE(SKELKILL.wav) \ +VCMI_SOUND_NAME(SKELMove) VCMI_SOUND_FILE(SKELMOVE.wav) \ +VCMI_SOUND_NAME(SKELWNCE) VCMI_SOUND_FILE(SKELWNCE.wav) \ +VCMI_SOUND_NAME(SKLWAttack) VCMI_SOUND_FILE(SKLWATTK.wav) \ +VCMI_SOUND_NAME(SKLWDefend) VCMI_SOUND_FILE(SKLWDFND.wav) \ +VCMI_SOUND_NAME(SKLWKill) VCMI_SOUND_FILE(SKLWKILL.wav) \ +VCMI_SOUND_NAME(SKLWMove) VCMI_SOUND_FILE(SKLWMOVE.wav) \ +VCMI_SOUND_NAME(SKLWWNCE) VCMI_SOUND_FILE(SKLWWNCE.wav) \ +VCMI_SOUND_NAME(SLAYER) VCMI_SOUND_FILE(SLAYER.wav) \ +VCMI_SOUND_NAME(SORROW) VCMI_SOUND_FILE(SORROW.wav) \ +VCMI_SOUND_NAME(SPONTCOMB) VCMI_SOUND_FILE(SPONTCOMB.wav) \ +VCMI_SOUND_NAME(SPRTAttack) VCMI_SOUND_FILE(SPRTATTK.wav) \ +VCMI_SOUND_NAME(SPRTDefend) VCMI_SOUND_FILE(SPRTDFND.wav) \ +VCMI_SOUND_NAME(SPRTKill) VCMI_SOUND_FILE(SPRTKILL.wav) \ +VCMI_SOUND_NAME(SPRTMove) VCMI_SOUND_FILE(SPRTMOVE.wav) \ +VCMI_SOUND_NAME(SPRTWNCE) VCMI_SOUND_FILE(SPRTWNCE.wav) \ +VCMI_SOUND_NAME(STORAttack) VCMI_SOUND_FILE(STORATTK.wav) \ +VCMI_SOUND_NAME(STORDefend) VCMI_SOUND_FILE(STORDFND.wav) \ +VCMI_SOUND_NAME(STORE) VCMI_SOUND_FILE(STORE.wav) \ +VCMI_SOUND_NAME(STORKill) VCMI_SOUND_FILE(STORKILL.wav) \ +VCMI_SOUND_NAME(STORMove) VCMI_SOUND_FILE(STORMOVE.wav) \ +VCMI_SOUND_NAME(STORM) VCMI_SOUND_FILE(STORM.wav) \ +VCMI_SOUND_NAME(STORShot) VCMI_SOUND_FILE(STORSHOT.wav) \ +VCMI_SOUND_NAME(STORWNCE) VCMI_SOUND_FILE(STORWNCE.wav) \ +VCMI_SOUND_NAME(SUMMBOAT) VCMI_SOUND_FILE(SUMMBOAT.wav) \ +VCMI_SOUND_NAME(SUMNELM) VCMI_SOUND_FILE(SUMNELM.wav) \ +VCMI_SOUND_NAME(SWRDAttack) VCMI_SOUND_FILE(SWRDATTK.wav) \ +VCMI_SOUND_NAME(SWRDDefend) VCMI_SOUND_FILE(SWRDDFND.wav) \ +VCMI_SOUND_NAME(SWRDKill) VCMI_SOUND_FILE(SWRDKILL.wav) \ +VCMI_SOUND_NAME(SWRDMove) VCMI_SOUND_FILE(SWRDMOVE.wav) \ +VCMI_SOUND_NAME(SWRDWNCE) VCMI_SOUND_FILE(SWRDWNCE.wav) \ +VCMI_SOUND_NAME(SYSMSG) VCMI_SOUND_FILE(SYSMSG.wav) \ +VCMI_SOUND_NAME(TAILWIND) VCMI_SOUND_FILE(TAILWIND.wav) \ +VCMI_SOUND_NAME(TBRDAttack) VCMI_SOUND_FILE(TBRDATTK.wav) \ +VCMI_SOUND_NAME(TBRDDefend) VCMI_SOUND_FILE(TBRDDFND.wav) \ +VCMI_SOUND_NAME(TBRDKill) VCMI_SOUND_FILE(TBRDKILL.wav) \ +VCMI_SOUND_NAME(TBRDMove) VCMI_SOUND_FILE(TBRDMOVE.wav) \ +VCMI_SOUND_NAME(TBRDWNCE) VCMI_SOUND_FILE(TBRDWNCE.wav) \ +VCMI_SOUND_NAME(TELEIN) VCMI_SOUND_FILE(TELEIN.wav) \ +VCMI_SOUND_NAME(TELPTIN) VCMI_SOUND_FILE(TELPTIN.wav) \ +VCMI_SOUND_NAME(TELPTOUT) VCMI_SOUND_FILE(TELPTOUT.wav) \ +VCMI_SOUND_NAME(temple) VCMI_SOUND_FILE(TEMPLE.wav) \ +VCMI_SOUND_NAME(timeOver) VCMI_SOUND_FILE(TIMEOVER.wav) \ +VCMI_SOUND_NAME(treasure) VCMI_SOUND_FILE(TREASURE.wav) \ +VCMI_SOUND_NAME(TREEAttack) VCMI_SOUND_FILE(TREEATTK.wav) \ +VCMI_SOUND_NAME(TREEDefend) VCMI_SOUND_FILE(TREEDFND.wav) \ +VCMI_SOUND_NAME(TREEKill) VCMI_SOUND_FILE(TREEKILL.wav) \ +VCMI_SOUND_NAME(TREEMove) VCMI_SOUND_FILE(TREEMOVE.wav) \ +VCMI_SOUND_NAME(TREEWNCE) VCMI_SOUND_FILE(TREEWNCE.wav) \ +VCMI_SOUND_NAME(TRLLAttack) VCMI_SOUND_FILE(TRLLATTK.wav) \ +VCMI_SOUND_NAME(TRLLDefend) VCMI_SOUND_FILE(TRLLDFND.wav) \ +VCMI_SOUND_NAME(TRLLKill) VCMI_SOUND_FILE(TRLLKILL.wav) \ +VCMI_SOUND_NAME(TRLLMove) VCMI_SOUND_FILE(TRLLMOVE.wav) \ +VCMI_SOUND_NAME(TRLLWNCE) VCMI_SOUND_FILE(TRLLWNCE.wav) \ +VCMI_SOUND_NAME(TROGAttack) VCMI_SOUND_FILE(TROGATTK.wav) \ +VCMI_SOUND_NAME(TROGDefend) VCMI_SOUND_FILE(TROGDFND.wav) \ +VCMI_SOUND_NAME(TROGKill) VCMI_SOUND_FILE(TROGKILL.wav) \ +VCMI_SOUND_NAME(TROGMove) VCMI_SOUND_FILE(TROGMOVE.wav) \ +VCMI_SOUND_NAME(TROGWNCE) VCMI_SOUND_FILE(TROGWNCE.wav) \ +VCMI_SOUND_NAME(TUFFSKIN) VCMI_SOUND_FILE(TUFFSKIN.wav) \ +VCMI_SOUND_NAME(ULTIMATEARTIFACT) VCMI_SOUND_FILE(ULTIMATEARTIFACT.wav) \ +VCMI_SOUND_NAME(UNICAttack) VCMI_SOUND_FILE(UNICATTK.wav) \ +VCMI_SOUND_NAME(UNICDefend) VCMI_SOUND_FILE(UNICDFND.wav) \ +VCMI_SOUND_NAME(UNICKill) VCMI_SOUND_FILE(UNICKILL.wav) \ +VCMI_SOUND_NAME(UNICMove) VCMI_SOUND_FILE(UNICMOVE.wav) \ +VCMI_SOUND_NAME(UNICWNCE) VCMI_SOUND_FILE(UNICWNCE.wav) \ +VCMI_SOUND_NAME(VAMPAttack) VCMI_SOUND_FILE(VAMPATTK.wav) \ +VCMI_SOUND_NAME(VAMPDefend) VCMI_SOUND_FILE(VAMPDFND.wav) \ +VCMI_SOUND_NAME(VAMPEXT1) VCMI_SOUND_FILE(VAMPEXT1.wav) \ +VCMI_SOUND_NAME(VAMPEXT2) VCMI_SOUND_FILE(VAMPEXT2.wav) \ +VCMI_SOUND_NAME(VAMPKill) VCMI_SOUND_FILE(VAMPKILL.wav) \ +VCMI_SOUND_NAME(VAMPMove) VCMI_SOUND_FILE(VAMPMOVE.wav) \ +VCMI_SOUND_NAME(VAMPWNCE) VCMI_SOUND_FILE(VAMPWNCE.wav) \ +VCMI_SOUND_NAME(VIEW) VCMI_SOUND_FILE(VIEW.wav) \ +VCMI_SOUND_NAME(VISIONS) VCMI_SOUND_FILE(VISIONS.wav) \ +VCMI_SOUND_NAME(WALLHIT) VCMI_SOUND_FILE(WALLHIT.wav) \ +VCMI_SOUND_NAME(WALLMISS) VCMI_SOUND_FILE(WALLMISS.wav) \ +VCMI_SOUND_NAME(WATRWALK) VCMI_SOUND_FILE(WATRWALK.wav) \ +VCMI_SOUND_NAME(WEAKNESS) VCMI_SOUND_FILE(WEAKNESS.wav) \ +VCMI_SOUND_NAME(WELFAttack) VCMI_SOUND_FILE(WELFATTK.wav) \ +VCMI_SOUND_NAME(WELFDefend) VCMI_SOUND_FILE(WELFDFND.wav) \ +VCMI_SOUND_NAME(WELFKill) VCMI_SOUND_FILE(WELFKILL.wav) \ +VCMI_SOUND_NAME(WELFMove) VCMI_SOUND_FILE(WELFMOVE.wav) \ +VCMI_SOUND_NAME(WELFShot) VCMI_SOUND_FILE(WELFSHOT.wav) \ +VCMI_SOUND_NAME(WELFWNCE) VCMI_SOUND_FILE(WELFWNCE.wav) \ +VCMI_SOUND_NAME(WELMAttack) VCMI_SOUND_FILE(WELMATTK.wav) \ +VCMI_SOUND_NAME(WELMDefend) VCMI_SOUND_FILE(WELMDFND.wav) \ +VCMI_SOUND_NAME(WELMKill) VCMI_SOUND_FILE(WELMKILL.wav) \ +VCMI_SOUND_NAME(WELMMove) VCMI_SOUND_FILE(WELMMOVE.wav) \ +VCMI_SOUND_NAME(WELMWNCE) VCMI_SOUND_FILE(WELMWNCE.wav) \ +VCMI_SOUND_NAME(WGHTAttack) VCMI_SOUND_FILE(WGHTATTK.wav) \ +VCMI_SOUND_NAME(WGHTDefend) VCMI_SOUND_FILE(WGHTDFND.wav) \ +VCMI_SOUND_NAME(WGHTKill) VCMI_SOUND_FILE(WGHTKILL.wav) \ +VCMI_SOUND_NAME(WGHTMove) VCMI_SOUND_FILE(WGHTMOVE.wav) \ +VCMI_SOUND_NAME(WGHTWNCE) VCMI_SOUND_FILE(WGHTWNCE.wav) \ +VCMI_SOUND_NAME(WRTHAttack) VCMI_SOUND_FILE(WRTHATTK.wav) \ +VCMI_SOUND_NAME(WRTHDefend) VCMI_SOUND_FILE(WRTHDFND.wav) \ +VCMI_SOUND_NAME(WRTHKill) VCMI_SOUND_FILE(WRTHKILL.wav) \ +VCMI_SOUND_NAME(WRTHMove) VCMI_SOUND_FILE(WRTHMOVE.wav) \ +VCMI_SOUND_NAME(WRTHWNCE) VCMI_SOUND_FILE(WRTHWNCE.wav) \ +VCMI_SOUND_NAME(WUNCAttack) VCMI_SOUND_FILE(WUNCATTK.wav) \ +VCMI_SOUND_NAME(WUNCDefend) VCMI_SOUND_FILE(WUNCDFND.wav) \ +VCMI_SOUND_NAME(WUNCKill) VCMI_SOUND_FILE(WUNCKILL.wav) \ +VCMI_SOUND_NAME(WUNCMove) VCMI_SOUND_FILE(WUNCMOVE.wav) \ +VCMI_SOUND_NAME(WUNCShot) VCMI_SOUND_FILE(WUNCSHOT.wav) \ +VCMI_SOUND_NAME(WUNCWNCE) VCMI_SOUND_FILE(WUNCWNCE.wav) \ +VCMI_SOUND_NAME(WYVMAttack) VCMI_SOUND_FILE(WYVMATTK.wav) \ +VCMI_SOUND_NAME(WYVMDefend) VCMI_SOUND_FILE(WYVMDFND.wav) \ +VCMI_SOUND_NAME(WYVMKill) VCMI_SOUND_FILE(WYVMKILL.wav) \ +VCMI_SOUND_NAME(WYVMMove) VCMI_SOUND_FILE(WYVMMOVE.wav) \ +VCMI_SOUND_NAME(WYVMWNCE) VCMI_SOUND_FILE(WYVMWNCE.wav) \ +VCMI_SOUND_NAME(WYVNAttack) VCMI_SOUND_FILE(WYVNATTK.wav) \ +VCMI_SOUND_NAME(WYVNDefend) VCMI_SOUND_FILE(WYVNDFND.wav) \ +VCMI_SOUND_NAME(WYVNKill) VCMI_SOUND_FILE(WYVNKILL.wav) \ +VCMI_SOUND_NAME(WYVNMove) VCMI_SOUND_FILE(WYVNMOVE.wav) \ +VCMI_SOUND_NAME(WYVNWNCE) VCMI_SOUND_FILE(WYVNWNCE.wav) \ +VCMI_SOUND_NAME(YBMHAttack) VCMI_SOUND_FILE(YBMHATTK.wav) \ +VCMI_SOUND_NAME(YBMHDefend) VCMI_SOUND_FILE(YBMHDFND.wav) \ +VCMI_SOUND_NAME(YBMHKill) VCMI_SOUND_FILE(YBMHKILL.wav) \ +VCMI_SOUND_NAME(YBMHMove) VCMI_SOUND_FILE(YBMHMOVE.wav) \ +VCMI_SOUND_NAME(YBMHWNCE) VCMI_SOUND_FILE(YBMHWNCE.wav) \ +VCMI_SOUND_NAME(zelotAttack) VCMI_SOUND_FILE(ZELTATTK.wav) \ +VCMI_SOUND_NAME(zelotDefend) VCMI_SOUND_FILE(ZELTDFND.wav) \ +VCMI_SOUND_NAME(zelotKill) VCMI_SOUND_FILE(ZELTKILL.wav) \ +VCMI_SOUND_NAME(zelotMove) VCMI_SOUND_FILE(ZELTMOVE.wav) \ +VCMI_SOUND_NAME(zelotShot) VCMI_SOUND_FILE(ZELTSHOT.wav) \ +VCMI_SOUND_NAME(zelotWNCE) VCMI_SOUND_FILE(ZELTWNCE.wav) \ +VCMI_SOUND_NAME(ZMBLAttack) VCMI_SOUND_FILE(ZMBLATTK.wav) \ +VCMI_SOUND_NAME(ZMBLDefend) VCMI_SOUND_FILE(ZMBLDFND.wav) \ +VCMI_SOUND_NAME(ZMBLKill) VCMI_SOUND_FILE(ZMBLKILL.wav) \ +VCMI_SOUND_NAME(ZMBLMove) VCMI_SOUND_FILE(ZMBLMOVE.wav) \ +VCMI_SOUND_NAME(ZMBLWNCE) VCMI_SOUND_FILE(ZMBLWNCE.wav) \ +VCMI_SOUND_NAME(ZOMBAttack) VCMI_SOUND_FILE(ZOMBATTK.wav) \ +VCMI_SOUND_NAME(ZOMBDefend) VCMI_SOUND_FILE(ZOMBDFND.wav) \ +VCMI_SOUND_NAME(ZOMBKill) VCMI_SOUND_FILE(ZOMBKILL.wav) \ +VCMI_SOUND_NAME(ZOMBMove) VCMI_SOUND_FILE(ZOMBMOVE.wav) \ +VCMI_SOUND_NAME(ZOMBWNCE) VCMI_SOUND_FILE(ZOMBWNCE.wav) + +class soundBase +{ +public: + // Make a list of enums + // We must keep an entry 0 for an invalid or no sound. +#define VCMI_SOUND_NAME(x) x, +#define VCMI_SOUND_FILE(y) + enum soundID { + invalid=0, + sound_todo=1, // temp entry until code is fixed + VCMI_SOUND_LIST + sound_after_last + }; +#undef VCMI_SOUND_FILE +#undef VCMI_SOUND_NAME +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStack.cpp b/lib/CStack.cpp index ed18eb3a2..72dff4799 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -1,406 +1,406 @@ -/* - * CStack.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 "CStack.h" - -#include - -#include -#include - -#include "CGeneralTextHandler.h" -#include "battle/BattleInfo.h" -#include "spells/CSpellHandler.h" -#include "NetPacks.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -///CStack -CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, ui8 Side, const SlotID & S): - CBonusSystemNode(STACK_BATTLE), - base(Base), - ID(I), - type(Base->type), - baseAmount(Base->count), - owner(O), - slot(S), - side(Side) -{ - health.init(); //??? -} - -CStack::CStack(): - CBonusSystemNode(STACK_BATTLE), - owner(PlayerColor::NEUTRAL), - slot(SlotID(255)), - initialPosition(BattleHex()) -{ -} - -CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S): - CBonusSystemNode(STACK_BATTLE), - ID(I), - type(stack->type), - baseAmount(stack->count), - owner(O), - slot(S), - side(Side) -{ - health.init(); //??? -} - -void CStack::localInit(BattleInfo * battleInfo) -{ - battle = battleInfo; - assert(type); - - exportBonuses(); - if(base) //stack originating from "real" stack in garrison -> attach to it - { - attachTo(const_cast(*base)); - } - else //attach directly to obj to which stack belongs and creature type - { - CArmedInstance * army = battle->battleGetArmyObject(side); - assert(army); - attachTo(*army); - attachTo(const_cast(*type)); - } - nativeTerrain = getNativeTerrain(); //save nativeTerrain in the variable on the battle start to avoid dead lock - CUnitState::localInit(this); //it causes execution of the CStack::isOnNativeTerrain where nativeTerrain will be considered - position = initialPosition; -} - -ui32 CStack::level() const -{ - if(base) - return base->getLevel(); //creature or commander - else - return std::max(1, static_cast(unitType()->getLevel())); //war machine, clone etc -} - -si32 CStack::magicResistance() const -{ - auto magicResistance = AFactionMember::magicResistance(); - - si32 auraBonus = 0; - - for(const auto * one : battle->battleAdjacentUnits(this)) - { - if(one->unitOwner() == owner) - vstd::amax(auraBonus, one->valOfBonuses(BonusType::SPELL_RESISTANCE_AURA)); //max value - } - vstd::abetween(auraBonus, 0, 100); - vstd::abetween(magicResistance, 0, 100); - float castChance = (100 - magicResistance) * (100 - auraBonus)/100.0; - - return static_cast(100 - castChance); -} - -BattleHex::EDir CStack::destShiftDir() const -{ - if(doubleWide()) - { - if(side == BattleSide::ATTACKER) - return BattleHex::EDir::RIGHT; - else - return BattleHex::EDir::LEFT; - } - else - { - return BattleHex::EDir::NONE; - } -} - -std::vector CStack::activeSpells() const -{ - std::vector ret; - - std::stringstream cachingStr; - cachingStr << "!type_" << vstd::to_underlying(BonusType::NONE) << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT); - CSelector selector = Selector::sourceType()(BonusSource::SPELL_EFFECT) - .And(CSelector([](const Bonus * b)->bool - { - return b->type != BonusType::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure(); - })); - - TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); - for(const auto & it : *spellEffects) - { - if(!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects - ret.push_back(it->sid); - } - - return ret; -} - -CStack::~CStack() -{ - detachFromAll(); -} - -const CGHeroInstance * CStack::getMyHero() const -{ - if(base) - return dynamic_cast(base->armyObj); - else //we are attached directly? - for(const CBonusSystemNode * n : getParentNodes()) - if(n->getNodeType() == HERO) - return dynamic_cast(n); - - return nullptr; -} - -std::string CStack::nodeName() const -{ - std::ostringstream oss; - oss << owner.toString(); - oss << " battle stack [" << ID << "]: " << getCount() << " of "; - if(type) - oss << type->getNamePluralTextID(); - else - oss << "[UNDEFINED TYPE]"; - - oss << " from slot " << slot; - if(base && base->armyObj) - oss << " of armyobj=" << base->armyObj->id.getNum(); - return oss.str(); -} - -void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const -{ - auto newState = acquireState(); - prepareAttacked(bsa, rand, newState); -} - -void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const std::shared_ptr & customState) -{ - auto initialCount = customState->getCount(); - - // compute damage and update bsa.damageAmount - customState->damage(bsa.damageAmount); - - bsa.killedAmount = initialCount - customState->getCount(); - - if(!customState->alive() && customState->isClone()) - { - bsa.flags |= BattleStackAttacked::CLONE_KILLED; - } - else if(!customState->alive()) //stack killed - { - bsa.flags |= BattleStackAttacked::KILLED; - - auto resurrectValue = customState->valOfBonuses(BonusType::REBIRTH); - - if(resurrectValue > 0 && customState->canCast()) //there must be casts left - { - double resurrectFactor = resurrectValue / 100.0; - - auto baseAmount = customState->unitBaseAmount(); - - double resurrectedRaw = baseAmount * resurrectFactor; - - auto resurrectedCount = static_cast(floor(resurrectedRaw)); - - auto resurrectedAdd = static_cast(baseAmount - (resurrectedCount / resurrectFactor)); - - auto rangeGen = rand.getInt64Range(0, 99); - - for(int32_t i = 0; i < resurrectedAdd; i++) - { - if(resurrectValue > rangeGen()) - resurrectedCount += 1; - } - - if(customState->hasBonusOfType(BonusType::REBIRTH, 1)) - { - // resurrect at least one Sacred Phoenix - vstd::amax(resurrectedCount, 1); - } - - if(resurrectedCount > 0) - { - customState->casts.use(); - bsa.flags |= BattleStackAttacked::REBIRTH; - int64_t toHeal = customState->getMaxHealth() * resurrectedCount; - //TODO: add one-battle rebirth? - customState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); - customState->counterAttacks.use(customState->counterAttacks.available()); - } - } - } - - customState->save(bsa.newState.data); - bsa.newState.healthDelta = -bsa.damageAmount; - bsa.newState.id = customState->unitId(); - bsa.newState.operation = UnitChanges::EOperation::RESET_STATE; -} - -std::vector CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) -{ - int mask = 0; - std::vector res; - - if (!attackerPos.isValid()) - attackerPos = attacker->getPosition(); - if (!defenderPos.isValid()) - defenderPos = defender->getPosition(); - - BattleHex otherAttackerPos = attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1); - BattleHex otherDefenderPos = defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1); - - if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front - { - if((mask & 1) == 0) - { - mask |= 1; - res.push_back(defenderPos); - } - } - if (attacker->doubleWide() //back <=> front - && BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0) - { - if((mask & 1) == 0) - { - mask |= 1; - res.push_back(defenderPos); - } - } - if (defender->doubleWide()//front <=> back - && BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0) - { - if((mask & 2) == 0) - { - mask |= 2; - res.push_back(otherDefenderPos); - } - } - if (defender->doubleWide() && attacker->doubleWide()//back <=> back - && BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0) - { - if((mask & 2) == 0) - { - mask |= 2; - res.push_back(otherDefenderPos); - } - } - - return res; -} - -bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) -{ - return !meleeAttackHexes(attacker, defender, attackerPos, defenderPos).empty(); -} - -std::string CStack::getName() const -{ - return (getCount() == 1) ? type->getNameSingularTranslated() : type->getNamePluralTranslated(); //War machines can't use base -} - -bool CStack::canBeHealed() const -{ - return getFirstHPleft() < static_cast(getMaxHealth()) && isValidTarget() && !hasBonusOfType(BonusType::SIEGE_WEAPON); -} - -bool CStack::isOnNativeTerrain() const -{ - //this code is called from CreatureTerrainLimiter::limit on battle start - auto res = nativeTerrain == ETerrainId::ANY_TERRAIN || nativeTerrain == battle->getTerrainType(); - return res; -} - -bool CStack::isOnTerrain(TerrainId terrain) const -{ - return battle->getTerrainType() == terrain; -} - -const CCreature * CStack::unitType() const -{ - return type; -} - -int32_t CStack::unitBaseAmount() const -{ - return baseAmount; -} - -const IBonusBearer* CStack::getBonusBearer() const -{ - return this; -} - -bool CStack::unitHasAmmoCart(const battle::Unit * unit) const -{ - for(const CStack * st : battle->stacks) - { - if(battle->battleMatchOwner(st, unit, true) && st->unitType()->getId() == CreatureID::AMMO_CART) - { - return st->alive(); - } - } - //ammo cart works during creature bank battle while not on battlefield - const auto * ownerHero = battle->battleGetOwnerHero(unit); - if(ownerHero && ownerHero->artifactsWorn.find(ArtifactPosition::MACH2) != ownerHero->artifactsWorn.end()) - { - if(battle->battleGetOwnerHero(unit)->artifactsWorn.at(ArtifactPosition::MACH2).artifact->artType->getId() == ArtifactID::AMMO_CART) - { - return true; - } - } - return false; //will be always false if trying to examine enemy hero in "special battle" -} - -PlayerColor CStack::unitEffectiveOwner(const battle::Unit * unit) const -{ - return battle->battleGetOwner(unit); -} - -uint32_t CStack::unitId() const -{ - return ID; -} - -ui8 CStack::unitSide() const -{ - return side; -} - -PlayerColor CStack::unitOwner() const -{ - return owner; -} - -SlotID CStack::unitSlot() const -{ - return slot; -} - -std::string CStack::getDescription() const -{ - return nodeName(); -} - -void CStack::spendMana(ServerCallback * server, const int spellCost) const -{ - if(spellCost != 1) - logGlobal->warn("Unexpected spell cost %d for creature", spellCost); - - BattleSetStackProperty ssp; - ssp.battleID = battle->battleID; - ssp.stackID = unitId(); - ssp.which = BattleSetStackProperty::CASTS; - ssp.val = -spellCost; - ssp.absolute = false; - server->apply(&ssp); -} - -VCMI_LIB_NAMESPACE_END +/* + * CStack.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 "CStack.h" + +#include + +#include +#include + +#include "CGeneralTextHandler.h" +#include "battle/BattleInfo.h" +#include "spells/CSpellHandler.h" +#include "NetPacks.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +///CStack +CStack::CStack(const CStackInstance * Base, const PlayerColor & O, int I, ui8 Side, const SlotID & S): + CBonusSystemNode(STACK_BATTLE), + base(Base), + ID(I), + type(Base->type), + baseAmount(Base->count), + owner(O), + slot(S), + side(Side) +{ + health.init(); //??? +} + +CStack::CStack(): + CBonusSystemNode(STACK_BATTLE), + owner(PlayerColor::NEUTRAL), + slot(SlotID(255)), + initialPosition(BattleHex()) +{ +} + +CStack::CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S): + CBonusSystemNode(STACK_BATTLE), + ID(I), + type(stack->type), + baseAmount(stack->count), + owner(O), + slot(S), + side(Side) +{ + health.init(); //??? +} + +void CStack::localInit(BattleInfo * battleInfo) +{ + battle = battleInfo; + assert(type); + + exportBonuses(); + if(base) //stack originating from "real" stack in garrison -> attach to it + { + attachTo(const_cast(*base)); + } + else //attach directly to obj to which stack belongs and creature type + { + CArmedInstance * army = battle->battleGetArmyObject(side); + assert(army); + attachTo(*army); + attachTo(const_cast(*type)); + } + nativeTerrain = getNativeTerrain(); //save nativeTerrain in the variable on the battle start to avoid dead lock + CUnitState::localInit(this); //it causes execution of the CStack::isOnNativeTerrain where nativeTerrain will be considered + position = initialPosition; +} + +ui32 CStack::level() const +{ + if(base) + return base->getLevel(); //creature or commander + else + return std::max(1, static_cast(unitType()->getLevel())); //war machine, clone etc +} + +si32 CStack::magicResistance() const +{ + auto magicResistance = AFactionMember::magicResistance(); + + si32 auraBonus = 0; + + for(const auto * one : battle->battleAdjacentUnits(this)) + { + if(one->unitOwner() == owner) + vstd::amax(auraBonus, one->valOfBonuses(BonusType::SPELL_RESISTANCE_AURA)); //max value + } + vstd::abetween(auraBonus, 0, 100); + vstd::abetween(magicResistance, 0, 100); + float castChance = (100 - magicResistance) * (100 - auraBonus)/100.0; + + return static_cast(100 - castChance); +} + +BattleHex::EDir CStack::destShiftDir() const +{ + if(doubleWide()) + { + if(side == BattleSide::ATTACKER) + return BattleHex::EDir::RIGHT; + else + return BattleHex::EDir::LEFT; + } + else + { + return BattleHex::EDir::NONE; + } +} + +std::vector CStack::activeSpells() const +{ + std::vector ret; + + std::stringstream cachingStr; + cachingStr << "!type_" << vstd::to_underlying(BonusType::NONE) << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT); + CSelector selector = Selector::sourceType()(BonusSource::SPELL_EFFECT) + .And(CSelector([](const Bonus * b)->bool + { + return b->type != BonusType::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure(); + })); + + TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); + for(const auto & it : *spellEffects) + { + if(!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects + ret.push_back(it->sid); + } + + return ret; +} + +CStack::~CStack() +{ + detachFromAll(); +} + +const CGHeroInstance * CStack::getMyHero() const +{ + if(base) + return dynamic_cast(base->armyObj); + else //we are attached directly? + for(const CBonusSystemNode * n : getParentNodes()) + if(n->getNodeType() == HERO) + return dynamic_cast(n); + + return nullptr; +} + +std::string CStack::nodeName() const +{ + std::ostringstream oss; + oss << owner.toString(); + oss << " battle stack [" << ID << "]: " << getCount() << " of "; + if(type) + oss << type->getNamePluralTextID(); + else + oss << "[UNDEFINED TYPE]"; + + oss << " from slot " << slot; + if(base && base->armyObj) + oss << " of armyobj=" << base->armyObj->id.getNum(); + return oss.str(); +} + +void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const +{ + auto newState = acquireState(); + prepareAttacked(bsa, rand, newState); +} + +void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const std::shared_ptr & customState) +{ + auto initialCount = customState->getCount(); + + // compute damage and update bsa.damageAmount + customState->damage(bsa.damageAmount); + + bsa.killedAmount = initialCount - customState->getCount(); + + if(!customState->alive() && customState->isClone()) + { + bsa.flags |= BattleStackAttacked::CLONE_KILLED; + } + else if(!customState->alive()) //stack killed + { + bsa.flags |= BattleStackAttacked::KILLED; + + auto resurrectValue = customState->valOfBonuses(BonusType::REBIRTH); + + if(resurrectValue > 0 && customState->canCast()) //there must be casts left + { + double resurrectFactor = resurrectValue / 100.0; + + auto baseAmount = customState->unitBaseAmount(); + + double resurrectedRaw = baseAmount * resurrectFactor; + + auto resurrectedCount = static_cast(floor(resurrectedRaw)); + + auto resurrectedAdd = static_cast(baseAmount - (resurrectedCount / resurrectFactor)); + + auto rangeGen = rand.getInt64Range(0, 99); + + for(int32_t i = 0; i < resurrectedAdd; i++) + { + if(resurrectValue > rangeGen()) + resurrectedCount += 1; + } + + if(customState->hasBonusOfType(BonusType::REBIRTH, 1)) + { + // resurrect at least one Sacred Phoenix + vstd::amax(resurrectedCount, 1); + } + + if(resurrectedCount > 0) + { + customState->casts.use(); + bsa.flags |= BattleStackAttacked::REBIRTH; + int64_t toHeal = customState->getMaxHealth() * resurrectedCount; + //TODO: add one-battle rebirth? + customState->heal(toHeal, EHealLevel::RESURRECT, EHealPower::PERMANENT); + customState->counterAttacks.use(customState->counterAttacks.available()); + } + } + } + + customState->save(bsa.newState.data); + bsa.newState.healthDelta = -bsa.damageAmount; + bsa.newState.id = customState->unitId(); + bsa.newState.operation = UnitChanges::EOperation::RESET_STATE; +} + +std::vector CStack::meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) +{ + int mask = 0; + std::vector res; + + if (!attackerPos.isValid()) + attackerPos = attacker->getPosition(); + if (!defenderPos.isValid()) + defenderPos = defender->getPosition(); + + BattleHex otherAttackerPos = attackerPos + (attacker->unitSide() == BattleSide::ATTACKER ? -1 : 1); + BattleHex otherDefenderPos = defenderPos + (defender->unitSide() == BattleSide::ATTACKER ? -1 : 1); + + if(BattleHex::mutualPosition(attackerPos, defenderPos) >= 0) //front <=> front + { + if((mask & 1) == 0) + { + mask |= 1; + res.push_back(defenderPos); + } + } + if (attacker->doubleWide() //back <=> front + && BattleHex::mutualPosition(otherAttackerPos, defenderPos) >= 0) + { + if((mask & 1) == 0) + { + mask |= 1; + res.push_back(defenderPos); + } + } + if (defender->doubleWide()//front <=> back + && BattleHex::mutualPosition(attackerPos, otherDefenderPos) >= 0) + { + if((mask & 2) == 0) + { + mask |= 2; + res.push_back(otherDefenderPos); + } + } + if (defender->doubleWide() && attacker->doubleWide()//back <=> back + && BattleHex::mutualPosition(otherAttackerPos, otherDefenderPos) >= 0) + { + if((mask & 2) == 0) + { + mask |= 2; + res.push_back(otherDefenderPos); + } + } + + return res; +} + +bool CStack::isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos, BattleHex defenderPos) +{ + return !meleeAttackHexes(attacker, defender, attackerPos, defenderPos).empty(); +} + +std::string CStack::getName() const +{ + return (getCount() == 1) ? type->getNameSingularTranslated() : type->getNamePluralTranslated(); //War machines can't use base +} + +bool CStack::canBeHealed() const +{ + return getFirstHPleft() < static_cast(getMaxHealth()) && isValidTarget() && !hasBonusOfType(BonusType::SIEGE_WEAPON); +} + +bool CStack::isOnNativeTerrain() const +{ + //this code is called from CreatureTerrainLimiter::limit on battle start + auto res = nativeTerrain == ETerrainId::ANY_TERRAIN || nativeTerrain == battle->getTerrainType(); + return res; +} + +bool CStack::isOnTerrain(TerrainId terrain) const +{ + return battle->getTerrainType() == terrain; +} + +const CCreature * CStack::unitType() const +{ + return type; +} + +int32_t CStack::unitBaseAmount() const +{ + return baseAmount; +} + +const IBonusBearer* CStack::getBonusBearer() const +{ + return this; +} + +bool CStack::unitHasAmmoCart(const battle::Unit * unit) const +{ + for(const CStack * st : battle->stacks) + { + if(battle->battleMatchOwner(st, unit, true) && st->unitType()->getId() == CreatureID::AMMO_CART) + { + return st->alive(); + } + } + //ammo cart works during creature bank battle while not on battlefield + const auto * ownerHero = battle->battleGetOwnerHero(unit); + if(ownerHero && ownerHero->artifactsWorn.find(ArtifactPosition::MACH2) != ownerHero->artifactsWorn.end()) + { + if(battle->battleGetOwnerHero(unit)->artifactsWorn.at(ArtifactPosition::MACH2).artifact->artType->getId() == ArtifactID::AMMO_CART) + { + return true; + } + } + return false; //will be always false if trying to examine enemy hero in "special battle" +} + +PlayerColor CStack::unitEffectiveOwner(const battle::Unit * unit) const +{ + return battle->battleGetOwner(unit); +} + +uint32_t CStack::unitId() const +{ + return ID; +} + +ui8 CStack::unitSide() const +{ + return side; +} + +PlayerColor CStack::unitOwner() const +{ + return owner; +} + +SlotID CStack::unitSlot() const +{ + return slot; +} + +std::string CStack::getDescription() const +{ + return nodeName(); +} + +void CStack::spendMana(ServerCallback * server, const int spellCost) const +{ + if(spellCost != 1) + logGlobal->warn("Unexpected spell cost %d for creature", spellCost); + + BattleSetStackProperty ssp; + ssp.battleID = battle->battleID; + ssp.stackID = unitId(); + ssp.which = BattleSetStackProperty::CASTS; + ssp.val = -spellCost; + ssp.absolute = false; + server->apply(&ssp); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStack.h b/lib/CStack.h index a2f944a8d..c9292c6ae 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -1,150 +1,150 @@ -/* - * CStack.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 "JsonNode.h" -#include "bonuses/Bonus.h" -#include "bonuses/CBonusSystemNode.h" -#include "CCreatureHandler.h" //todo: remove -#include "battle/BattleHex.h" -#include "mapObjects/CGHeroInstance.h" // for commander serialization - -#include "battle/CUnitState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct BattleStackAttacked; -class BattleInfo; - -//Represents STACK_BATTLE nodes -class DLL_LINKAGE CStack : public CBonusSystemNode, public battle::CUnitState, public battle::IUnitEnvironment -{ -private: - ui32 ID = -1; //unique ID of stack - const CCreature * type = nullptr; - TerrainId nativeTerrain; //tmp variable to save native terrain value on battle init - ui32 baseAmount = -1; - - PlayerColor owner; //owner - player color (255 for neutrals) - ui8 side = 1; - - SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) - -public: - const CStackInstance * base = nullptr; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) - - BattleHex initialPosition; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower - - CStack(const CStackInstance * base, const PlayerColor & O, int I, ui8 Side, const SlotID & S); - CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S = SlotID(255)); - CStack(); - ~CStack(); - - std::string nodeName() const override; - - void localInit(BattleInfo * battleInfo); - std::string getName() const; //plural or singular - - bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines - bool isOnNativeTerrain() const; - bool isOnTerrain(TerrainId terrain) const; - - ui32 level() const; - si32 magicResistance() const override; //include aura of resistance - std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast - const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise - - static std::vector meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); - static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); - - BattleHex::EDir destShiftDir() const; - - void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmout filled - static void prepareAttacked(BattleStackAttacked & bsa, - vstd::RNG & rand, - const std::shared_ptr & customState); //requires bsa.damageAmout filled - - const CCreature * unitType() const override; - int32_t unitBaseAmount() const override; - - uint32_t unitId() const override; - ui8 unitSide() const override; - PlayerColor unitOwner() const override; - SlotID unitSlot() const override; - - std::string getDescription() const override; - - bool unitHasAmmoCart(const battle::Unit * unit) const override; - PlayerColor unitEffectiveOwner(const battle::Unit * unit) const override; - - void spendMana(ServerCallback * server, const int spellCost) const override; - - const IBonusBearer* getBonusBearer() const override; - - PlayerColor getOwner() const override - { - return this->owner; - } - - template void serialize(Handler & h, const int version) - { - //this assumes that stack objects is newly created - //stackState is not serialized here - assert(isIndependentNode()); - h & static_cast(*this); - h & type; - h & ID; - h & baseAmount; - h & owner; - h & slot; - h & side; - h & initialPosition; - - const CArmedInstance * army = (base ? base->armyObj : nullptr); - SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID()); - - if(h.saving) - { - h & army; - h & extSlot; - } - else - { - h & army; - h & extSlot; - - if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER) - { - const auto * hero = dynamic_cast(army); - assert(hero); - base = hero->commander; - } - else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT) - { - //no external slot possible, so no base stack - base = nullptr; - } - else if(!army || extSlot == SlotID() || !army->hasStackAtSlot(extSlot)) - { - base = nullptr; - logGlobal->warn("%s doesn't have a base stack!", type->getNameSingularTranslated()); - } - else - { - base = &army->getStack(extSlot); - } - } - } - -private: - const BattleInfo * battle; //do not serialize -}; - -VCMI_LIB_NAMESPACE_END +/* + * CStack.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 "JsonNode.h" +#include "bonuses/Bonus.h" +#include "bonuses/CBonusSystemNode.h" +#include "CCreatureHandler.h" //todo: remove +#include "battle/BattleHex.h" +#include "mapObjects/CGHeroInstance.h" // for commander serialization + +#include "battle/CUnitState.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct BattleStackAttacked; +class BattleInfo; + +//Represents STACK_BATTLE nodes +class DLL_LINKAGE CStack : public CBonusSystemNode, public battle::CUnitState, public battle::IUnitEnvironment +{ +private: + ui32 ID = -1; //unique ID of stack + const CCreature * type = nullptr; + TerrainId nativeTerrain; //tmp variable to save native terrain value on battle init + ui32 baseAmount = -1; + + PlayerColor owner; //owner - player color (255 for neutrals) + ui8 side = 1; + + SlotID slot; //slot - position in garrison (may be 255 for neutrals/called creatures) + +public: + const CStackInstance * base = nullptr; //garrison slot from which stack originates (nullptr for war machines, summoned cres, etc) + + BattleHex initialPosition; //position on battlefield; -2 - keep, -3 - lower tower, -4 - upper tower + + CStack(const CStackInstance * base, const PlayerColor & O, int I, ui8 Side, const SlotID & S); + CStack(const CStackBasicDescriptor * stack, const PlayerColor & O, int I, ui8 Side, const SlotID & S = SlotID(255)); + CStack(); + ~CStack(); + + std::string nodeName() const override; + + void localInit(BattleInfo * battleInfo); + std::string getName() const; //plural or singular + + bool canBeHealed() const; //for first aid tent - only harmed stacks that are not war machines + bool isOnNativeTerrain() const; + bool isOnTerrain(TerrainId terrain) const; + + ui32 level() const; + si32 magicResistance() const override; //include aura of resistance + std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast + const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise + + static std::vector meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); + static bool isMeleeAttackPossible(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); + + BattleHex::EDir destShiftDir() const; + + void prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand) const; //requires bsa.damageAmout filled + static void prepareAttacked(BattleStackAttacked & bsa, + vstd::RNG & rand, + const std::shared_ptr & customState); //requires bsa.damageAmout filled + + const CCreature * unitType() const override; + int32_t unitBaseAmount() const override; + + uint32_t unitId() const override; + ui8 unitSide() const override; + PlayerColor unitOwner() const override; + SlotID unitSlot() const override; + + std::string getDescription() const override; + + bool unitHasAmmoCart(const battle::Unit * unit) const override; + PlayerColor unitEffectiveOwner(const battle::Unit * unit) const override; + + void spendMana(ServerCallback * server, const int spellCost) const override; + + const IBonusBearer* getBonusBearer() const override; + + PlayerColor getOwner() const override + { + return this->owner; + } + + template void serialize(Handler & h, const int version) + { + //this assumes that stack objects is newly created + //stackState is not serialized here + assert(isIndependentNode()); + h & static_cast(*this); + h & type; + h & ID; + h & baseAmount; + h & owner; + h & slot; + h & side; + h & initialPosition; + + const CArmedInstance * army = (base ? base->armyObj : nullptr); + SlotID extSlot = (base ? base->armyObj->findStack(base) : SlotID()); + + if(h.saving) + { + h & army; + h & extSlot; + } + else + { + h & army; + h & extSlot; + + if(extSlot == SlotID::COMMANDER_SLOT_PLACEHOLDER) + { + const auto * hero = dynamic_cast(army); + assert(hero); + base = hero->commander; + } + else if(slot == SlotID::SUMMONED_SLOT_PLACEHOLDER || slot == SlotID::ARROW_TOWERS_SLOT || slot == SlotID::WAR_MACHINES_SLOT) + { + //no external slot possible, so no base stack + base = nullptr; + } + else if(!army || extSlot == SlotID() || !army->hasStackAtSlot(extSlot)) + { + base = nullptr; + logGlobal->warn("%s doesn't have a base stack!", type->getNameSingularTranslated()); + } + else + { + base = &army->getStack(extSlot); + } + } + } + +private: + const BattleInfo * battle; //do not serialize +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CStopWatch.h b/lib/CStopWatch.h index 46e3cb65c..08167eb3f 100644 --- a/lib/CStopWatch.h +++ b/lib/CStopWatch.h @@ -1,68 +1,68 @@ -/* - * CStopWatch.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 - -#ifdef __FreeBSD__ - #include - #include - #include - #define TO_MS_DIVISOR (1000) -#else - #include - #define TO_MS_DIVISOR (CLOCKS_PER_SEC / 1000) -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -class CStopWatch -{ - si64 start, last, mem; - -public: - CStopWatch() - : start(clock()) - { - last=clock(); - mem=0; - } - - si64 getDiff() //get diff in milliseconds - { - si64 ret = clock() - last; - last = clock(); - return ret / TO_MS_DIVISOR; - } - void update() - { - last=clock(); - } - void remember() - { - mem=clock(); - } - si64 memDif() - { - return (clock()-mem) / TO_MS_DIVISOR; - } - -private: - si64 clock() - { - #ifdef __FreeBSD__ // TODO: enable also for Apple? - struct rusage usage; - getrusage(RUSAGE_SELF, &usage); - return static_cast(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; - #else - return std::clock(); - #endif - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CStopWatch.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 + +#ifdef __FreeBSD__ + #include + #include + #include + #define TO_MS_DIVISOR (1000) +#else + #include + #define TO_MS_DIVISOR (CLOCKS_PER_SEC / 1000) +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +class CStopWatch +{ + si64 start, last, mem; + +public: + CStopWatch() + : start(clock()) + { + last=clock(); + mem=0; + } + + si64 getDiff() //get diff in milliseconds + { + si64 ret = clock() - last; + last = clock(); + return ret / TO_MS_DIVISOR; + } + void update() + { + last=clock(); + } + void remember() + { + mem=clock(); + } + si64 memDif() + { + return (clock()-mem) / TO_MS_DIVISOR; + } + +private: + si64 clock() + { + #ifdef __FreeBSD__ // TODO: enable also for Apple? + struct rusage usage; + getrusage(RUSAGE_SELF, &usage); + return static_cast(usage.ru_utime.tv_sec + usage.ru_stime.tv_sec) * 1000000 + usage.ru_utime.tv_usec + usage.ru_stime.tv_usec; + #else + return std::clock(); + #endif + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CThreadHelper.cpp b/lib/CThreadHelper.cpp index eb5307c54..66f3bf7f9 100644 --- a/lib/CThreadHelper.cpp +++ b/lib/CThreadHelper.cpp @@ -1,101 +1,101 @@ -/* - * CThreadHelper.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 "CThreadHelper.h" - -#ifdef VCMI_WINDOWS - #include -#elif defined(VCMI_HAIKU) - #include -#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && !defined(VCMI_HURD) - #include -#endif - -VCMI_LIB_NAMESPACE_BEGIN - -CThreadHelper::CThreadHelper(std::vector> * Tasks, int Threads): - currentTask(0), - amount(static_cast(Tasks->size())), - tasks(Tasks), - threads(Threads) -{ -} -void CThreadHelper::run() -{ - std::vector group; - for(int i=0;i lock(rtinm); - if((pom = currentTask) >= amount) - break; - else - ++currentTask; - } - (*tasks)[pom](); - } -} - -// set name for this thread. -// NOTE: on *nix string will be trimmed to 16 symbols -void setThreadName(const std::string &name) -{ -#ifdef VCMI_WINDOWS -#ifndef __GNUC__ - //follows http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx - const DWORD MS_VC_EXCEPTION=0x406D1388; -#pragma pack(push,8) - typedef struct tagTHREADNAME_INFO - { - DWORD dwType; // Must be 0x1000. - LPCSTR szName; // Pointer to name (in user addr space). - DWORD dwThreadID; // Thread ID (-1=caller thread). - DWORD dwFlags; // Reserved for future use, must be zero. - } THREADNAME_INFO; -#pragma pack(pop) - THREADNAME_INFO info; - info.dwType = 0x1000; - info.szName = name.c_str(); - info.dwThreadID = -1; - info.dwFlags = 0; - - - __try - { - RaiseException( MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info ); - } - __except(EXCEPTION_EXECUTE_HANDLER) - { - } -#else -//not supported -#endif - -#elif defined(__linux__) - prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); -#elif defined(VCMI_APPLE) - pthread_setname_np(name.c_str()); -#elif defined(VCMI_HAIKU) - rename_thread(find_thread(NULL), name.c_str()); -#endif -} - -VCMI_LIB_NAMESPACE_END +/* + * CThreadHelper.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 "CThreadHelper.h" + +#ifdef VCMI_WINDOWS + #include +#elif defined(VCMI_HAIKU) + #include +#elif !defined(VCMI_APPLE) && !defined(VCMI_FREEBSD) && !defined(VCMI_HURD) + #include +#endif + +VCMI_LIB_NAMESPACE_BEGIN + +CThreadHelper::CThreadHelper(std::vector> * Tasks, int Threads): + currentTask(0), + amount(static_cast(Tasks->size())), + tasks(Tasks), + threads(Threads) +{ +} +void CThreadHelper::run() +{ + std::vector group; + for(int i=0;i lock(rtinm); + if((pom = currentTask) >= amount) + break; + else + ++currentTask; + } + (*tasks)[pom](); + } +} + +// set name for this thread. +// NOTE: on *nix string will be trimmed to 16 symbols +void setThreadName(const std::string &name) +{ +#ifdef VCMI_WINDOWS +#ifndef __GNUC__ + //follows http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx + const DWORD MS_VC_EXCEPTION=0x406D1388; +#pragma pack(push,8) + typedef struct tagTHREADNAME_INFO + { + DWORD dwType; // Must be 0x1000. + LPCSTR szName; // Pointer to name (in user addr space). + DWORD dwThreadID; // Thread ID (-1=caller thread). + DWORD dwFlags; // Reserved for future use, must be zero. + } THREADNAME_INFO; +#pragma pack(pop) + THREADNAME_INFO info; + info.dwType = 0x1000; + info.szName = name.c_str(); + info.dwThreadID = -1; + info.dwFlags = 0; + + + __try + { + RaiseException( MS_VC_EXCEPTION, 0, sizeof(info)/sizeof(ULONG_PTR), (ULONG_PTR*)&info ); + } + __except(EXCEPTION_EXECUTE_HANDLER) + { + } +#else +//not supported +#endif + +#elif defined(__linux__) + prctl(PR_SET_NAME, name.c_str(), 0, 0, 0); +#elif defined(VCMI_APPLE) + pthread_setname_np(name.c_str()); +#elif defined(VCMI_HAIKU) + rename_thread(find_thread(NULL), name.c_str()); +#endif +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CThreadHelper.h b/lib/CThreadHelper.h index 023af5e29..86dcb7619 100644 --- a/lib/CThreadHelper.h +++ b/lib/CThreadHelper.h @@ -1,87 +1,87 @@ -/* - * CThreadHelper.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 - -VCMI_LIB_NAMESPACE_BEGIN - - -///DEPRECATED -/// Can assign CPU work to other threads/cores -class DLL_LINKAGE CThreadHelper -{ -public: - using Task = std::function; - CThreadHelper(std::vector > *Tasks, int Threads); - void run(); -private: - boost::mutex rtinm; - int currentTask, amount, threads; - std::vector *tasks; - - - void processTasks(); -}; - -template -class ThreadPool -{ -public: - using Task = std::function)>; - using Tasks = std::vector; - - ThreadPool(Tasks * tasks_, std::vector> context_) - : currentTask(0), - amount(tasks_->size()), - threads(context_.size()), - tasks(tasks_), - context(context_) - {} - - void run() - { - std::vector group; - for(size_t i=0; i payload = context.at(i); - group.emplace_back(std::bind(&ThreadPool::processTasks, this, payload)); - } - - for (auto & thread : group) - thread.join(); - - //thread group deletes threads, do not free manually - } -private: - boost::mutex rtinm; - size_t currentTask, amount, threads; - Tasks * tasks; - std::vector> context; - - void processTasks(std::shared_ptr payload) - { - while(true) - { - size_t pom; - { - boost::unique_lock lock(rtinm); - if((pom = currentTask) >= amount) - break; - else - ++currentTask; - } - (*tasks)[pom](payload); - } - } -}; - - -void DLL_LINKAGE setThreadName(const std::string &name); - -VCMI_LIB_NAMESPACE_END +/* + * CThreadHelper.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 + +VCMI_LIB_NAMESPACE_BEGIN + + +///DEPRECATED +/// Can assign CPU work to other threads/cores +class DLL_LINKAGE CThreadHelper +{ +public: + using Task = std::function; + CThreadHelper(std::vector > *Tasks, int Threads); + void run(); +private: + boost::mutex rtinm; + int currentTask, amount, threads; + std::vector *tasks; + + + void processTasks(); +}; + +template +class ThreadPool +{ +public: + using Task = std::function)>; + using Tasks = std::vector; + + ThreadPool(Tasks * tasks_, std::vector> context_) + : currentTask(0), + amount(tasks_->size()), + threads(context_.size()), + tasks(tasks_), + context(context_) + {} + + void run() + { + std::vector group; + for(size_t i=0; i payload = context.at(i); + group.emplace_back(std::bind(&ThreadPool::processTasks, this, payload)); + } + + for (auto & thread : group) + thread.join(); + + //thread group deletes threads, do not free manually + } +private: + boost::mutex rtinm; + size_t currentTask, amount, threads; + Tasks * tasks; + std::vector> context; + + void processTasks(std::shared_ptr payload) + { + while(true) + { + size_t pom; + { + boost::unique_lock lock(rtinm); + if((pom = currentTask) >= amount) + break; + else + ++currentTask; + } + (*tasks)[pom](payload); + } + } +}; + + +void DLL_LINKAGE setThreadName(const std::string &name); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index b5d2fcec9..9dea94ff1 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1,1266 +1,1266 @@ -/* - * CTownHandler.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 "CTownHandler.h" - -#include "VCMI_Lib.h" -#include "CGeneralTextHandler.h" -#include "JsonNode.h" -#include "constants/StringConstants.h" -#include "CCreatureHandler.h" -#include "CHeroHandler.h" -#include "CArtHandler.h" -#include "GameSettings.h" -#include "TerrainHandler.h" -#include "spells/CSpellHandler.h" -#include "filesystem/Filesystem.h" -#include "bonuses/Bonus.h" -#include "bonuses/Propagators.h" -#include "ResourceSet.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "modding/IdentifierStorage.h" -#include "modding/ModScope.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const int NAMES_PER_TOWN=16; // number of town names per faction in H3 files. Json can define any number - -const std::map CBuilding::MODES = -{ - { "normal", CBuilding::BUILD_NORMAL }, - { "auto", CBuilding::BUILD_AUTO }, - { "special", CBuilding::BUILD_SPECIAL }, - { "grail", CBuilding::BUILD_GRAIL } -}; - -const std::map CBuilding::TOWER_TYPES = -{ - { "low", CBuilding::HEIGHT_LOW }, - { "average", CBuilding::HEIGHT_AVERAGE }, - { "high", CBuilding::HEIGHT_HIGH }, - { "skyship", CBuilding::HEIGHT_SKYSHIP } -}; - -std::string CBuilding::getJsonKey() const -{ - return modScope + ':' + identifier;; -} - -std::string CBuilding::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CBuilding::getDescriptionTranslated() const -{ - return VLC->generaltexth->translate(getDescriptionTextID()); -} - -std::string CBuilding::getBaseTextID() const -{ - return TextIdentifier("building", modScope, town->faction->identifier, identifier).get(); -} - -std::string CBuilding::getNameTextID() const -{ - return TextIdentifier(getBaseTextID(), "name").get(); -} - -std::string CBuilding::getDescriptionTextID() const -{ - return TextIdentifier(getBaseTextID(), "description").get(); -} - -BuildingID CBuilding::getBase() const -{ - const CBuilding * build = this; - while (build->upgrade != BuildingID::NONE) - { - build = build->town->buildings.at(build->upgrade); - } - - return build->bid; -} - -si32 CBuilding::getDistance(const BuildingID & buildID) const -{ - const CBuilding * build = town->buildings.at(buildID); - int distance = 0; - while (build->upgrade != BuildingID::NONE && build != this) - { - build = build->town->buildings.at(build->upgrade); - distance++; - } - if (build == this) - return distance; - return -1; -} - -void CBuilding::addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const -{ - bonusList.push_back(b); -} - -CFaction::~CFaction() -{ - if (town) - { - delete town; - town = nullptr; - } -} - -int32_t CFaction::getIndex() const -{ - return index; -} - -int32_t CFaction::getIconIndex() const -{ - return index; //??? -} - -std::string CFaction::getJsonKey() const -{ - return modScope + ':' + identifier;; -} - -void CFaction::registerIcons(const IconRegistar & cb) const -{ - if(town) - { - auto & info = town->clientInfo; - cb(info.icons[0][0], 0, "ITPT", info.iconLarge[0][0]); - cb(info.icons[0][1], 0, "ITPT", info.iconLarge[0][1]); - cb(info.icons[1][0], 0, "ITPT", info.iconLarge[1][0]); - cb(info.icons[1][1], 0, "ITPT", info.iconLarge[1][1]); - - cb(info.icons[0][0] + 2, 0, "ITPA", info.iconSmall[0][0]); - cb(info.icons[0][1] + 2, 0, "ITPA", info.iconSmall[0][1]); - cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); - cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); - - cb(index, 1, "CPRSMALL", info.towerIconSmall); - cb(index, 1, "TWCRPORT", info.towerIconLarge); - - } -} - -std::string CFaction::getNameTranslated() const -{ - return VLC->generaltexth->translate(getNameTextID()); -} - -std::string CFaction::getNameTextID() const -{ - return TextIdentifier("faction", modScope, identifier, "name").get(); -} - -FactionID CFaction::getId() const -{ - return FactionID(index); -} - -FactionID CFaction::getFaction() const -{ - return FactionID(index); -} - -bool CFaction::hasTown() const -{ - return town != nullptr; -} - -EAlignment CFaction::getAlignment() const -{ - return alignment; -} - -BoatId CFaction::getBoatType() const -{ - return boatType; -} - -TerrainId CFaction::getNativeTerrain() const -{ - return nativeTerrain; -} - -void CFaction::updateFrom(const JsonNode & data) -{ - -} - -void CFaction::serializeJson(JsonSerializeFormat & handler) -{ - -} - - -CTown::CTown() - : faction(nullptr), mageLevel(0), primaryRes(0), moatAbility(SpellID::NONE), defaultTavernChance(0) -{ -} - -CTown::~CTown() -{ - for(auto & build : buildings) - build.second.dellNull(); - - for(auto & str : clientInfo.structures) - str.dellNull(); -} - -std::string CTown::getRandomNameTranslated(size_t index) const -{ - return VLC->generaltexth->translate(getRandomNameTextID(index)); -} - -std::string CTown::getRandomNameTextID(size_t index) const -{ - return TextIdentifier("faction", faction->modScope, faction->identifier, "randomName", index).get(); -} - -size_t CTown::getRandomNamesCount() const -{ - return namesCount; -} - -std::string CTown::getBuildingScope() const -{ - if(faction == nullptr) - //no faction == random faction - return "building"; - else - return "building." + faction->getJsonKey(); -} - -std::set CTown::getAllBuildings() const -{ - std::set res; - - for(const auto & b : buildings) - { - res.insert(b.first.num); - } - - return res; -} - -const CBuilding * CTown::getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const -{ - for(const auto & kvp : buildings) - { - if(kvp.second->subId == subID) - return buildings.at(kvp.first); - } - return nullptr; -} - -BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const -{ - const auto * building = getSpecialBuilding(subID); - return building == nullptr ? BuildingID::NONE : building->bid.num; -} - -std::string CTown::getGreeting(BuildingSubID::EBuildingSubID subID) const -{ - return CTownHandler::getMappedValue(subID, std::string(), specialMessages, false); -} - -void CTown::setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const -{ - specialMessages.insert(std::pair(subID, message)); -} - -CTownHandler::CTownHandler(): - randomTown(new CTown()), - randomFaction(new CFaction()) -{ - randomFaction->town = randomTown; - randomTown->faction = randomFaction; - randomFaction->identifier = "random"; - randomFaction->modScope = "core"; -} - -CTownHandler::~CTownHandler() -{ - delete randomTown; -} - -JsonNode readBuilding(CLegacyConfigParser & parser) -{ - JsonNode ret; - JsonNode & cost = ret["cost"]; - - //note: this code will try to parse mithril as well but wil always return 0 for it - for(const std::string & resID : GameConstants::RESOURCE_NAMES) - cost[resID].Float() = parser.readNumber(); - - cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator - - parser.endLine(); - - return ret; -} - -TPropagatorPtr & CTownHandler::emptyPropagator() -{ - static TPropagatorPtr emptyProp(nullptr); - return emptyProp; -} - -std::vector CTownHandler::loadLegacyData() -{ - size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION); - - std::vector dest(dataSize); - objects.resize(dataSize); - - auto getBuild = [&](size_t town, size_t building) -> JsonNode & - { - return dest[town]["town"]["buildings"][EBuildingType::names[building]]; - }; - - CLegacyConfigParser parser(TextPath::builtin("DATA/BUILDING.TXT")); - - parser.endLine(); // header - parser.endLine(); - - //Unique buildings - for (size_t town=0; town & bidsToLoad) const -{ - if (source.isNull()) - return; - - BuildingRequirementsHelper hlp; - hlp.building = building; - hlp.town = building->town; - hlp.json = source; - bidsToLoad.push_back(hlp); -} - -template -R CTownHandler::getMappedValue(const K key, const R defval, const std::map & map, bool required) -{ - auto it = map.find(key); - - if(it != map.end()) - return it->second; - - if(required) - logMod->warn("Warning: Property: '%s' is unknown. Correct the typo or update VCMI.", key); - return defval; -} - -template -R CTownHandler::getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required) -{ - if(!node.isNull() && node.getType() == JsonNode::JsonType::DATA_STRING) - return getMappedValue(node.String(), defval, map, required); - return defval; -} - -void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const -{ - std::shared_ptr b; - static TPropagatorPtr playerPropagator = std::make_shared(CBonusSystemNode::ENodeTypes::PLAYER); - - if(building->bid == BuildingID::TAVERN) - { - b = createBonus(building, BonusType::MORALE, +1); - } - - switch(building->subId) - { - case BuildingSubID::BROTHERHOOD_OF_SWORD: - b = createBonus(building, BonusType::MORALE, +2); - building->overrideBids.insert(BuildingID::TAVERN); - break; - case BuildingSubID::FOUNTAIN_OF_FORTUNE: - b = createBonus(building, BonusType::LUCK, +2); - break; - case BuildingSubID::SPELL_POWER_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::SPELL_POWER)); - break; - case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::ATTACK)); - break; - case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::DEFENSE)); - break; - case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, playerPropagator, 0); - break; - } - - if(b) - building->addNewBonus(b, building->buildingBonuses); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, int subtype) const -{ - return createBonus(build, type, val, emptyPropagator(), subtype); -} - -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype) const -{ - std::ostringstream descr; - descr << build->getNameTranslated(); - return createBonusImpl(build->bid, type, val, prop, descr.str(), subtype); -} - -std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building, - BonusType type, - int val, - TPropagatorPtr & prop, - const std::string & description, - int subtype) const -{ - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, description, subtype); - - if(prop) - b->addPropagator(prop); - - return b; -} - -void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) -{ - for(const auto & b : source.Vector()) - { - auto bonus = JsonUtils::parseBuildingBonus(b, building->bid, building->getNameTranslated()); - - if(bonus == nullptr) - continue; - - bonus->sid = Bonus::getSid32(building->town->faction->getIndex(), building->bid); - //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. - if(bonus->propagator != nullptr - && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) - bonus->addPropagator(emptyPropagator()); - building->addNewBonus(bonus, bonusList); - } -} - -void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source) -{ - assert(stringID.find(':') == std::string::npos); - assert(!source.meta.empty()); - - auto * ret = new CBuilding(); - ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); - ret->subId = BuildingSubID::NONE; - - if(ret->bid == BuildingID::NONE && !source["id"].isNull()) - { - // FIXME: A lot of false-positives with no clear way to handle them in mods - //logMod->warn("Building %s: id field is deprecated", stringID); - ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float()); - } - - if (ret->bid == BuildingID::NONE) - logMod->error("Building '%s' isn't recognized and won't work properly. Correct the typo or update VCMI.", stringID); - - ret->mode = ret->bid == BuildingID::GRAIL - ? CBuilding::BUILD_GRAIL - : getMappedValue(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); - - ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); - - ret->identifier = stringID; - ret->modScope = source.meta; - ret->town = town; - - VLC->generaltexth->registerString(source.meta, ret->getNameTextID(), source["name"].String()); - VLC->generaltexth->registerString(source.meta, ret->getDescriptionTextID(), source["description"].String()); - - ret->resources = TResources(source["cost"]); - ret->produce = TResources(source["produce"]); - - if(ret->bid == BuildingID::TAVERN) - addBonusesForVanilaBuilding(ret); - else if(ret->bid.IsSpecialOrGrail()) - { - loadSpecialBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); - - if(ret->buildingBonuses.empty()) - { - ret->subId = getMappedValue(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); - addBonusesForVanilaBuilding(ret); - } - - loadSpecialBuildingBonuses(source["onVisitBonuses"], ret->onVisitBonuses, ret); - - if(!ret->onVisitBonuses.empty()) - { - if(ret->subId == BuildingSubID::NONE) - ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; - - for(auto & bonus : ret->onVisitBonuses) - bonus->sid = Bonus::getSid32(ret->town->faction->getIndex(), ret->bid); - } - - if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) - { - ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD; - ret->rewardableObjectInfo.init(source, ret->getBaseTextID()); - } - } - //MODS COMPATIBILITY FOR 0.96 - if(!ret->produce.nonZero()) - { - switch (ret->bid) { - break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; - break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; - break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; - break; case BuildingID::CAPITOL : ret->produce[EGameResID::GOLD] = 4000; - break; case BuildingID::GRAIL : ret->produce[EGameResID::GOLD] = 5000; - break; case BuildingID::RESOURCE_SILO : - { - switch (ret->town->primaryRes.toEnum()) - { - case EGameResID::GOLD: - ret->produce[ret->town->primaryRes] = 500; - break; - case EGameResID::WOOD_AND_ORE: - ret->produce[EGameResID::WOOD] = 1; - ret->produce[EGameResID::ORE] = 1; - break; - default: - ret->produce[ret->town->primaryRes] = 1; - break; - } - } - } - } - loadBuildingRequirements(ret, source["requires"], requirementsToLoad); - - if(ret->bid.IsSpecialOrGrail()) - loadBuildingRequirements(ret, source["overrides"], overriddenBidsToLoad); - - if (!source["upgrades"].isNull()) - { - // building id and upgrades can't be the same - if(stringID == source["upgrades"].String()) - { - throw std::runtime_error(boost::str(boost::format("Building with ID '%s' of town '%s' can't be an upgrade of the same building.") % - stringID % ret->town->faction->getNameTranslated())); - } - - VLC->identifiers()->requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) - { - ret->upgrade = BuildingID(identifier); - }); - } - else - ret->upgrade = BuildingID::NONE; - - ret->town->buildings[ret->bid] = ret; - - registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid); -} - -void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) -{ - if(source.isStruct()) - { - for(const auto & node : source.Struct()) - { - if (!node.second.isNull()) - loadBuilding(town, node.first, node.second); - } - } -} - -void CTownHandler::loadStructure(CTown &town, const std::string & stringID, const JsonNode & source) const -{ - auto * ret = new CStructure(); - - ret->building = nullptr; - ret->buildable = nullptr; - - VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable - { - ret->building = town.buildings[BuildingID(identifier)]; - }); - - if (source["builds"].isNull()) - { - VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable - { - ret->building = town.buildings[BuildingID(identifier)]; - }); - } - else - { - VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable - { - ret->buildable = town.buildings[BuildingID(identifier)]; - }); - } - - ret->identifier = stringID; - ret->pos.x = static_cast(source["x"].Float()); - ret->pos.y = static_cast(source["y"].Float()); - ret->pos.z = static_cast(source["z"].Float()); - - ret->hiddenUpgrade = source["hidden"].Bool(); - ret->defName = AnimationPath::fromJson(source["animation"]); - ret->borderName = ImagePath::fromJson(source["border"]); - ret->areaName = ImagePath::fromJson(source["area"]); - - town.clientInfo.structures.emplace_back(ret); -} - -void CTownHandler::loadStructures(CTown &town, const JsonNode & source) const -{ - for(const auto & node : source.Struct()) - { - if (!node.second.isNull()) - loadStructure(town, node.first, node.second); - } -} - -void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const -{ - auto & dstSlots = town.clientInfo.hallSlots; - const auto & srcSlots = source.Vector(); - dstSlots.resize(srcSlots.size()); - - for(size_t i=0; iidentifiers()->requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) - { - dst = BuildingID(identifier); - }); - } - } - } -} - -Point JsonToPoint(const JsonNode & node) -{ - if(!node.isStruct()) - return Point::makeInvalid(); - - Point ret; - ret.x = static_cast(node["x"].Float()); - ret.y = static_cast(node["y"].Float()); - return ret; -} - -void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const -{ - town.clientInfo.siegePrefix = source["imagePrefix"].String(); - town.clientInfo.towerIconSmall = source["towerIconSmall"].String(); - town.clientInfo.towerIconLarge = source["towerIconLarge"].String(); - - VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature) - { - auto crId = CreatureID(creature); - if((*VLC->creh)[crId]->animation.missleFrameAngles.empty()) - logMod->error("Mod '%s' error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Siege will not work properly!" - , town.faction->getNameTranslated() - , (*VLC->creh)[crId]->getNameSingularTranslated()); - - town.clientInfo.siegeShooter = crId; - }); - - auto & pos = town.clientInfo.siegePositions; - pos.resize(21); - - pos[8] = JsonToPoint(source["towers"]["top"]["tower"]); - pos[17] = JsonToPoint(source["towers"]["top"]["battlement"]); - pos[20] = JsonToPoint(source["towers"]["top"]["creature"]); - - pos[2] = JsonToPoint(source["towers"]["keep"]["tower"]); - pos[15] = JsonToPoint(source["towers"]["keep"]["battlement"]); - pos[18] = JsonToPoint(source["towers"]["keep"]["creature"]); - - pos[3] = JsonToPoint(source["towers"]["bottom"]["tower"]); - pos[16] = JsonToPoint(source["towers"]["bottom"]["battlement"]); - pos[19] = JsonToPoint(source["towers"]["bottom"]["creature"]); - - pos[9] = JsonToPoint(source["gate"]["gate"]); - pos[10] = JsonToPoint(source["gate"]["arch"]); - - pos[7] = JsonToPoint(source["walls"]["upper"]); - pos[6] = JsonToPoint(source["walls"]["upperMid"]); - pos[5] = JsonToPoint(source["walls"]["bottomMid"]); - pos[4] = JsonToPoint(source["walls"]["bottom"]); - - pos[13] = JsonToPoint(source["moat"]["moat"]); - pos[14] = JsonToPoint(source["moat"]["bank"]); - - pos[11] = JsonToPoint(source["static"]["bottom"]); - pos[12] = JsonToPoint(source["static"]["top"]); - pos[1] = JsonToPoint(source["static"]["background"]); -} - -static void readIcon(JsonNode source, std::string & small, std::string & large) -{ - if (source.getType() == JsonNode::JsonType::DATA_STRUCT) // don't crash on old format - { - small = source["small"].String(); - large = source["large"].String(); - } -} - -void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const -{ - CTown::ClientInfo & info = town.clientInfo; - - readIcon(source["icons"]["village"]["normal"], info.iconSmall[0][0], info.iconLarge[0][0]); - readIcon(source["icons"]["village"]["built"], info.iconSmall[0][1], info.iconLarge[0][1]); - readIcon(source["icons"]["fort"]["normal"], info.iconSmall[1][0], info.iconLarge[1][0]); - readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]); - - info.hallBackground = ImagePath::fromJson(source["hallBackground"]); - info.musicTheme = AudioPath::fromJson(source["musicTheme"]); - info.townBackground = ImagePath::fromJson(source["townBackground"]); - info.guildWindow = ImagePath::fromJson(source["guildWindow"]); - info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); - - info.guildBackground = ImagePath::fromJson(source["guildBackground"]); - info.tavernVideo = VideoPath::fromJson(source["tavernVideo"]); - - loadTownHall(town, source["hallSlots"]); - loadStructures(town, source["structures"]); - loadSiegeScreen(town, source["siege"]); -} - -void CTownHandler::loadTown(CTown * town, const JsonNode & source) -{ - const auto * resIter = boost::find(GameConstants::RESOURCE_NAMES, source["primaryResource"].String()); - if(resIter == std::end(GameConstants::RESOURCE_NAMES)) - town->primaryRes = GameResID(EGameResID::WOOD_AND_ORE); //Wood + Ore - else - town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES)); - - warMachinesToLoad[town] = source["warMachine"]; - - town->mageLevel = static_cast(source["mageGuild"].Float()); - - town->namesCount = 0; - for(const auto & name : source["names"].Vector()) - { - VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String()); - town->namesCount += 1; - } - - if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code - { - VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) - { - town->moatAbility = SpellID(ability); - }); - } - else - { - VLC->identifiers()->requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) - { - town->moatAbility = SpellID(ability); - }); - } - - // Horde building creature level - for(const JsonNode &node : source["horde"].Vector()) - town->hordeLvl[static_cast(town->hordeLvl.size())] = static_cast(node.Float()); - - // town needs to have exactly 2 horde entries. Validation will take care of 2+ entries - // but anything below 2 must be handled here - for (size_t i=source["horde"].Vector().size(); i<2; i++) - town->hordeLvl[static_cast(i)] = -1; - - const JsonVector & creatures = source["creatures"].Vector(); - - town->creatures.resize(creatures.size()); - - for (size_t i=0; i< creatures.size(); i++) - { - const JsonVector & level = creatures[i].Vector(); - - town->creatures[i].resize(level.size()); - - for (size_t j=0; jidentifiers()->requestIdentifier("creature", level[j], [=](si32 creature) - { - town->creatures[i][j] = CreatureID(creature); - }); - } - } - - town->defaultTavernChance = static_cast(source["defaultTavern"].Float()); - /// set chance of specific hero class to appear in this town - for(const auto & node : source["tavern"].Struct()) - { - int chance = static_cast(node.second.Float()); - - VLC->identifiers()->requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) - { - VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; - }); - } - - for(const auto & node : source["guildSpells"].Struct()) - { - int chance = static_cast(node.second.Float()); - - VLC->identifiers()->requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) - { - VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; - }); - } - - for(const JsonNode & d : source["adventureMap"]["dwellings"].Vector()) - { - town->dwellings.push_back(d["graphics"].String()); - town->dwellingNames.push_back(d["name"].String()); - } - - loadBuildings(town, source["buildings"]); - loadClientData(*town, source); -} - -void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const -{ - faction.puzzleMap.reserve(GameConstants::PUZZLE_MAP_PIECES); - - std::string prefix = source["prefix"].String(); - for(const JsonNode &piece : source["pieces"].Vector()) - { - size_t index = faction.puzzleMap.size(); - SPuzzleInfo spi; - - spi.x = static_cast(piece["x"].Float()); - spi.y = static_cast(piece["y"].Float()); - spi.whenUncovered = static_cast(piece["index"].Float()); - spi.number = static_cast(index); - - // filename calculation - std::ostringstream suffix; - suffix << std::setfill('0') << std::setw(2) << index; - - spi.filename = ImagePath::builtinTODO(prefix + suffix.str()); - - faction.puzzleMap.push_back(spi); - } - assert(faction.puzzleMap.size() == GameConstants::PUZZLE_MAP_PIECES); -} - -CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode & source, const std::string & identifier, size_t index) -{ - assert(identifier.find(':') == std::string::npos); - - auto * faction = new CFaction(); - - faction->index = static_cast(index); - faction->modScope = scope; - faction->identifier = identifier; - - VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); - - faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]); - faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]); - - faction->boatType = BoatId::CASTLE; //Do not crash - if (!source["boat"].isNull()) - { - VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) - { - faction->boatType = BoatId(boatTypeID); - }); - } - - int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String()); - if (alignment == -1) - faction->alignment = EAlignment::NEUTRAL; - else - faction->alignment = static_cast(alignment); - - auto preferUndergound = source["preferUndergroundPlacement"]; - faction->preferUndergroundPlacement = preferUndergound.isNull() ? false : preferUndergound.Bool(); - - // NOTE: semi-workaround - normally, towns are supposed to have native terrains. - // Towns without one are exceptions. So, vcmi requires nativeTerrain to be defined - // But allows it to be defined with explicit value of "none" if town should not have native terrain - // This is better than allowing such terrain-less towns silently, leading to issues with RMG - faction->nativeTerrain = ETerrainId::NONE; - if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none") - { - VLC->identifiers()->requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ - faction->nativeTerrain = TerrainId(index); - - auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain); - - if (!terrain->isSurface() && !terrain->isUnderground()) - logMod->warn("Faction %s has terrain %s as native, but terrain is not suitable for either surface or subterranean layers!", faction->getJsonKey(), terrain->getJsonKey()); - }); - } - - if (!source["town"].isNull()) - { - faction->town = new CTown(); - faction->town->faction = faction; - loadTown(faction->town, source["town"]); - } - else - faction->town = nullptr; - - if (!source["puzzleMap"].isNull()) - loadPuzzle(*faction, source["puzzleMap"]); - - return faction; -} - -void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) -{ - auto * object = loadFromJson(scope, data, name, objects.size()); - - objects.emplace_back(object); - - if (object->town) - { - auto & info = object->town->clientInfo; - info.icons[0][0] = 8 + object->index * 4 + 0; - info.icons[0][1] = 8 + object->index * 4 + 1; - info.icons[1][0] = 8 + object->index * 4 + 2; - info.icons[1][1] = 8 + object->index * 4 + 3; - - VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) - { - // register town once objects are loaded - JsonNode config = data["town"]["mapObject"]; - config["faction"].String() = name; - config["faction"].meta = scope; - if (config.meta.empty())// MODS COMPATIBILITY FOR 0.96 - config.meta = scope; - VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); - - // MODS COMPATIBILITY FOR 0.96 - const auto & advMap = data["town"]["adventureMap"]; - if (!advMap.isNull()) - { - logMod->warn("Outdated town mod. Will try to generate valid templates out of fort"); - JsonNode config; - config["animation"] = advMap["castle"]; - VLC->objtypeh->getHandlerFor(index, object->index)->addTemplate(config); - } - }); - } - - registerObject(scope, "faction", name, object->index); -} - -void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) -{ - auto * object = loadFromJson(scope, data, name, index); - - if (objects.size() > index) - assert(objects[index] == nullptr); // ensure that this id was not loaded before - else - objects.resize(index + 1); - objects[index] = object; - - if (object->town) - { - auto & info = object->town->clientInfo; - info.icons[0][0] = (GameConstants::F_NUMBER + object->index) * 2 + 0; - info.icons[0][1] = (GameConstants::F_NUMBER + object->index) * 2 + 1; - info.icons[1][0] = object->index * 2 + 0; - info.icons[1][1] = object->index * 2 + 1; - - VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) - { - // register town once objects are loaded - JsonNode config = data["town"]["mapObject"]; - config["faction"].String() = name; - config["faction"].meta = scope; - VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); - }); - } - - registerObject(scope, "faction", name, object->index); -} - -void CTownHandler::loadRandomFaction() -{ - JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json")); - randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); - loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); -} - -void CTownHandler::loadCustom() -{ - loadRandomFaction(); -} - -void CTownHandler::afterLoadFinalization() -{ - initializeRequirements(); - initializeOverridden(); - initializeWarMachines(); -} - -void CTownHandler::initializeRequirements() -{ - // must be done separately after all ID's are known - for (auto & requirement : requirementsToLoad) - { - requirement.building->requirements = CBuilding::TRequired(requirement.json, [&](const JsonNode & node) -> BuildingID - { - if (node.Vector().size() > 1) - { - logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); - logMod->error("Entry contains: "); - logMod->error(node.toJson()); - } - - auto index = VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node[0]); - - if (!index.has_value()) - { - logMod->error("Unknown building in town buildings: %s", node[0].String()); - return BuildingID::NONE; - } - return BuildingID(index.value()); - }); - } - requirementsToLoad.clear(); -} - -void CTownHandler::initializeOverridden() -{ - for(auto & bidHelper : overriddenBidsToLoad) - { - auto jsonNode = bidHelper.json; - auto scope = bidHelper.town->getBuildingScope(); - - for(const auto & b : jsonNode.Vector()) - { - auto bid = BuildingID(VLC->identifiers()->getIdentifier(scope, b).value()); - bidHelper.building->overrideBids.insert(bid); - } - } - overriddenBidsToLoad.clear(); -} - -void CTownHandler::initializeWarMachines() -{ - // must be done separately after all objects are loaded - for(auto & p : warMachinesToLoad) - { - CTown * t = p.first; - JsonNode creatureKey = p.second; - - auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false); - - if(ret) - { - const CCreature * creature = CreatureID(*ret).toCreature(); - - t->warMachine = creature->warMachine; - } - } - - warMachinesToLoad.clear(); -} - -std::vector CTownHandler::getDefaultAllowed() const -{ - std::vector allowedFactions; - allowedFactions.reserve(objects.size()); - for(auto town : objects) - { - allowedFactions.push_back(town->town != nullptr); - } - return allowedFactions; -} - -std::set CTownHandler::getAllowedFactions(bool withTown) const -{ - std::set allowedFactions; - std::vector allowed; - if (withTown) - allowed = getDefaultAllowed(); - else - allowed.resize( objects.size(), true); - - for (size_t i=0; i(i)); - - return allowedFactions; -} - -const std::vector & CTownHandler::getTypeNames() const -{ - static const std::vector typeNames = { "faction", "town" }; - return typeNames; -} - - -VCMI_LIB_NAMESPACE_END +/* + * CTownHandler.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 "CTownHandler.h" + +#include "VCMI_Lib.h" +#include "CGeneralTextHandler.h" +#include "JsonNode.h" +#include "constants/StringConstants.h" +#include "CCreatureHandler.h" +#include "CHeroHandler.h" +#include "CArtHandler.h" +#include "GameSettings.h" +#include "TerrainHandler.h" +#include "spells/CSpellHandler.h" +#include "filesystem/Filesystem.h" +#include "bonuses/Bonus.h" +#include "bonuses/Propagators.h" +#include "ResourceSet.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const int NAMES_PER_TOWN=16; // number of town names per faction in H3 files. Json can define any number + +const std::map CBuilding::MODES = +{ + { "normal", CBuilding::BUILD_NORMAL }, + { "auto", CBuilding::BUILD_AUTO }, + { "special", CBuilding::BUILD_SPECIAL }, + { "grail", CBuilding::BUILD_GRAIL } +}; + +const std::map CBuilding::TOWER_TYPES = +{ + { "low", CBuilding::HEIGHT_LOW }, + { "average", CBuilding::HEIGHT_AVERAGE }, + { "high", CBuilding::HEIGHT_HIGH }, + { "skyship", CBuilding::HEIGHT_SKYSHIP } +}; + +std::string CBuilding::getJsonKey() const +{ + return modScope + ':' + identifier;; +} + +std::string CBuilding::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CBuilding::getDescriptionTranslated() const +{ + return VLC->generaltexth->translate(getDescriptionTextID()); +} + +std::string CBuilding::getBaseTextID() const +{ + return TextIdentifier("building", modScope, town->faction->identifier, identifier).get(); +} + +std::string CBuilding::getNameTextID() const +{ + return TextIdentifier(getBaseTextID(), "name").get(); +} + +std::string CBuilding::getDescriptionTextID() const +{ + return TextIdentifier(getBaseTextID(), "description").get(); +} + +BuildingID CBuilding::getBase() const +{ + const CBuilding * build = this; + while (build->upgrade != BuildingID::NONE) + { + build = build->town->buildings.at(build->upgrade); + } + + return build->bid; +} + +si32 CBuilding::getDistance(const BuildingID & buildID) const +{ + const CBuilding * build = town->buildings.at(buildID); + int distance = 0; + while (build->upgrade != BuildingID::NONE && build != this) + { + build = build->town->buildings.at(build->upgrade); + distance++; + } + if (build == this) + return distance; + return -1; +} + +void CBuilding::addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const +{ + bonusList.push_back(b); +} + +CFaction::~CFaction() +{ + if (town) + { + delete town; + town = nullptr; + } +} + +int32_t CFaction::getIndex() const +{ + return index; +} + +int32_t CFaction::getIconIndex() const +{ + return index; //??? +} + +std::string CFaction::getJsonKey() const +{ + return modScope + ':' + identifier;; +} + +void CFaction::registerIcons(const IconRegistar & cb) const +{ + if(town) + { + auto & info = town->clientInfo; + cb(info.icons[0][0], 0, "ITPT", info.iconLarge[0][0]); + cb(info.icons[0][1], 0, "ITPT", info.iconLarge[0][1]); + cb(info.icons[1][0], 0, "ITPT", info.iconLarge[1][0]); + cb(info.icons[1][1], 0, "ITPT", info.iconLarge[1][1]); + + cb(info.icons[0][0] + 2, 0, "ITPA", info.iconSmall[0][0]); + cb(info.icons[0][1] + 2, 0, "ITPA", info.iconSmall[0][1]); + cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); + cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); + + cb(index, 1, "CPRSMALL", info.towerIconSmall); + cb(index, 1, "TWCRPORT", info.towerIconLarge); + + } +} + +std::string CFaction::getNameTranslated() const +{ + return VLC->generaltexth->translate(getNameTextID()); +} + +std::string CFaction::getNameTextID() const +{ + return TextIdentifier("faction", modScope, identifier, "name").get(); +} + +FactionID CFaction::getId() const +{ + return FactionID(index); +} + +FactionID CFaction::getFaction() const +{ + return FactionID(index); +} + +bool CFaction::hasTown() const +{ + return town != nullptr; +} + +EAlignment CFaction::getAlignment() const +{ + return alignment; +} + +BoatId CFaction::getBoatType() const +{ + return boatType; +} + +TerrainId CFaction::getNativeTerrain() const +{ + return nativeTerrain; +} + +void CFaction::updateFrom(const JsonNode & data) +{ + +} + +void CFaction::serializeJson(JsonSerializeFormat & handler) +{ + +} + + +CTown::CTown() + : faction(nullptr), mageLevel(0), primaryRes(0), moatAbility(SpellID::NONE), defaultTavernChance(0) +{ +} + +CTown::~CTown() +{ + for(auto & build : buildings) + build.second.dellNull(); + + for(auto & str : clientInfo.structures) + str.dellNull(); +} + +std::string CTown::getRandomNameTranslated(size_t index) const +{ + return VLC->generaltexth->translate(getRandomNameTextID(index)); +} + +std::string CTown::getRandomNameTextID(size_t index) const +{ + return TextIdentifier("faction", faction->modScope, faction->identifier, "randomName", index).get(); +} + +size_t CTown::getRandomNamesCount() const +{ + return namesCount; +} + +std::string CTown::getBuildingScope() const +{ + if(faction == nullptr) + //no faction == random faction + return "building"; + else + return "building." + faction->getJsonKey(); +} + +std::set CTown::getAllBuildings() const +{ + std::set res; + + for(const auto & b : buildings) + { + res.insert(b.first.num); + } + + return res; +} + +const CBuilding * CTown::getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const +{ + for(const auto & kvp : buildings) + { + if(kvp.second->subId == subID) + return buildings.at(kvp.first); + } + return nullptr; +} + +BuildingID CTown::getBuildingType(BuildingSubID::EBuildingSubID subID) const +{ + const auto * building = getSpecialBuilding(subID); + return building == nullptr ? BuildingID::NONE : building->bid.num; +} + +std::string CTown::getGreeting(BuildingSubID::EBuildingSubID subID) const +{ + return CTownHandler::getMappedValue(subID, std::string(), specialMessages, false); +} + +void CTown::setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const +{ + specialMessages.insert(std::pair(subID, message)); +} + +CTownHandler::CTownHandler(): + randomTown(new CTown()), + randomFaction(new CFaction()) +{ + randomFaction->town = randomTown; + randomTown->faction = randomFaction; + randomFaction->identifier = "random"; + randomFaction->modScope = "core"; +} + +CTownHandler::~CTownHandler() +{ + delete randomTown; +} + +JsonNode readBuilding(CLegacyConfigParser & parser) +{ + JsonNode ret; + JsonNode & cost = ret["cost"]; + + //note: this code will try to parse mithril as well but wil always return 0 for it + for(const std::string & resID : GameConstants::RESOURCE_NAMES) + cost[resID].Float() = parser.readNumber(); + + cost.Struct().erase("mithril"); // erase mithril to avoid confusing validator + + parser.endLine(); + + return ret; +} + +TPropagatorPtr & CTownHandler::emptyPropagator() +{ + static TPropagatorPtr emptyProp(nullptr); + return emptyProp; +} + +std::vector CTownHandler::loadLegacyData() +{ + size_t dataSize = VLC->settings()->getInteger(EGameSettings::TEXTS_FACTION); + + std::vector dest(dataSize); + objects.resize(dataSize); + + auto getBuild = [&](size_t town, size_t building) -> JsonNode & + { + return dest[town]["town"]["buildings"][EBuildingType::names[building]]; + }; + + CLegacyConfigParser parser(TextPath::builtin("DATA/BUILDING.TXT")); + + parser.endLine(); // header + parser.endLine(); + + //Unique buildings + for (size_t town=0; town & bidsToLoad) const +{ + if (source.isNull()) + return; + + BuildingRequirementsHelper hlp; + hlp.building = building; + hlp.town = building->town; + hlp.json = source; + bidsToLoad.push_back(hlp); +} + +template +R CTownHandler::getMappedValue(const K key, const R defval, const std::map & map, bool required) +{ + auto it = map.find(key); + + if(it != map.end()) + return it->second; + + if(required) + logMod->warn("Warning: Property: '%s' is unknown. Correct the typo or update VCMI.", key); + return defval; +} + +template +R CTownHandler::getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required) +{ + if(!node.isNull() && node.getType() == JsonNode::JsonType::DATA_STRING) + return getMappedValue(node.String(), defval, map, required); + return defval; +} + +void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const +{ + std::shared_ptr b; + static TPropagatorPtr playerPropagator = std::make_shared(CBonusSystemNode::ENodeTypes::PLAYER); + + if(building->bid == BuildingID::TAVERN) + { + b = createBonus(building, BonusType::MORALE, +1); + } + + switch(building->subId) + { + case BuildingSubID::BROTHERHOOD_OF_SWORD: + b = createBonus(building, BonusType::MORALE, +2); + building->overrideBids.insert(BuildingID::TAVERN); + break; + case BuildingSubID::FOUNTAIN_OF_FORTUNE: + b = createBonus(building, BonusType::LUCK, +2); + break; + case BuildingSubID::SPELL_POWER_GARRISON_BONUS: + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::SPELL_POWER)); + break; + case BuildingSubID::ATTACK_GARRISON_BONUS: + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::ATTACK)); + break; + case BuildingSubID::DEFENSE_GARRISON_BONUS: + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::DEFENSE)); + break; + case BuildingSubID::LIGHTHOUSE: + b = createBonus(building, BonusType::MOVEMENT, +500, playerPropagator, 0); + break; + } + + if(b) + building->addNewBonus(b, building->buildingBonuses); +} + +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, int subtype) const +{ + return createBonus(build, type, val, emptyPropagator(), subtype); +} + +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype) const +{ + std::ostringstream descr; + descr << build->getNameTranslated(); + return createBonusImpl(build->bid, type, val, prop, descr.str(), subtype); +} + +std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building, + BonusType type, + int val, + TPropagatorPtr & prop, + const std::string & description, + int subtype) const +{ + auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, description, subtype); + + if(prop) + b->addPropagator(prop); + + return b; +} + +void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building) +{ + for(const auto & b : source.Vector()) + { + auto bonus = JsonUtils::parseBuildingBonus(b, building->bid, building->getNameTranslated()); + + if(bonus == nullptr) + continue; + + bonus->sid = Bonus::getSid32(building->town->faction->getIndex(), building->bid); + //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. + if(bonus->propagator != nullptr + && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) + bonus->addPropagator(emptyPropagator()); + building->addNewBonus(bonus, bonusList); + } +} + +void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source) +{ + assert(stringID.find(':') == std::string::npos); + assert(!source.meta.empty()); + + auto * ret = new CBuilding(); + ret->bid = getMappedValue(stringID, BuildingID::NONE, MappedKeys::BUILDING_NAMES_TO_TYPES, false); + ret->subId = BuildingSubID::NONE; + + if(ret->bid == BuildingID::NONE && !source["id"].isNull()) + { + // FIXME: A lot of false-positives with no clear way to handle them in mods + //logMod->warn("Building %s: id field is deprecated", stringID); + ret->bid = source["id"].isNull() ? BuildingID(BuildingID::NONE) : BuildingID(source["id"].Float()); + } + + if (ret->bid == BuildingID::NONE) + logMod->error("Building '%s' isn't recognized and won't work properly. Correct the typo or update VCMI.", stringID); + + ret->mode = ret->bid == BuildingID::GRAIL + ? CBuilding::BUILD_GRAIL + : getMappedValue(source["mode"], CBuilding::BUILD_NORMAL, CBuilding::MODES); + + ret->height = getMappedValue(source["height"], CBuilding::HEIGHT_NO_TOWER, CBuilding::TOWER_TYPES); + + ret->identifier = stringID; + ret->modScope = source.meta; + ret->town = town; + + VLC->generaltexth->registerString(source.meta, ret->getNameTextID(), source["name"].String()); + VLC->generaltexth->registerString(source.meta, ret->getDescriptionTextID(), source["description"].String()); + + ret->resources = TResources(source["cost"]); + ret->produce = TResources(source["produce"]); + + if(ret->bid == BuildingID::TAVERN) + addBonusesForVanilaBuilding(ret); + else if(ret->bid.IsSpecialOrGrail()) + { + loadSpecialBuildingBonuses(source["bonuses"], ret->buildingBonuses, ret); + + if(ret->buildingBonuses.empty()) + { + ret->subId = getMappedValue(source["type"], BuildingSubID::NONE, MappedKeys::SPECIAL_BUILDINGS); + addBonusesForVanilaBuilding(ret); + } + + loadSpecialBuildingBonuses(source["onVisitBonuses"], ret->onVisitBonuses, ret); + + if(!ret->onVisitBonuses.empty()) + { + if(ret->subId == BuildingSubID::NONE) + ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; + + for(auto & bonus : ret->onVisitBonuses) + bonus->sid = Bonus::getSid32(ret->town->faction->getIndex(), ret->bid); + } + + if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) + { + ret->subId = BuildingSubID::CUSTOM_VISITING_REWARD; + ret->rewardableObjectInfo.init(source, ret->getBaseTextID()); + } + } + //MODS COMPATIBILITY FOR 0.96 + if(!ret->produce.nonZero()) + { + switch (ret->bid) { + break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; + break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; + break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; + break; case BuildingID::CAPITOL : ret->produce[EGameResID::GOLD] = 4000; + break; case BuildingID::GRAIL : ret->produce[EGameResID::GOLD] = 5000; + break; case BuildingID::RESOURCE_SILO : + { + switch (ret->town->primaryRes.toEnum()) + { + case EGameResID::GOLD: + ret->produce[ret->town->primaryRes] = 500; + break; + case EGameResID::WOOD_AND_ORE: + ret->produce[EGameResID::WOOD] = 1; + ret->produce[EGameResID::ORE] = 1; + break; + default: + ret->produce[ret->town->primaryRes] = 1; + break; + } + } + } + } + loadBuildingRequirements(ret, source["requires"], requirementsToLoad); + + if(ret->bid.IsSpecialOrGrail()) + loadBuildingRequirements(ret, source["overrides"], overriddenBidsToLoad); + + if (!source["upgrades"].isNull()) + { + // building id and upgrades can't be the same + if(stringID == source["upgrades"].String()) + { + throw std::runtime_error(boost::str(boost::format("Building with ID '%s' of town '%s' can't be an upgrade of the same building.") % + stringID % ret->town->faction->getNameTranslated())); + } + + VLC->identifiers()->requestIdentifier(ret->town->getBuildingScope(), source["upgrades"], [=](si32 identifier) + { + ret->upgrade = BuildingID(identifier); + }); + } + else + ret->upgrade = BuildingID::NONE; + + ret->town->buildings[ret->bid] = ret; + + registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid); +} + +void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) +{ + if(source.isStruct()) + { + for(const auto & node : source.Struct()) + { + if (!node.second.isNull()) + loadBuilding(town, node.first, node.second); + } + } +} + +void CTownHandler::loadStructure(CTown &town, const std::string & stringID, const JsonNode & source) const +{ + auto * ret = new CStructure(); + + ret->building = nullptr; + ret->buildable = nullptr; + + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + { + ret->building = town.buildings[BuildingID(identifier)]; + }); + + if (source["builds"].isNull()) + { + VLC->identifiers()->tryRequestIdentifier( source.meta, "building." + town.faction->getJsonKey(), stringID, [=, &town](si32 identifier) mutable + { + ret->building = town.buildings[BuildingID(identifier)]; + }); + } + else + { + VLC->identifiers()->requestIdentifier("building." + town.faction->getJsonKey(), source["builds"], [=, &town](si32 identifier) mutable + { + ret->buildable = town.buildings[BuildingID(identifier)]; + }); + } + + ret->identifier = stringID; + ret->pos.x = static_cast(source["x"].Float()); + ret->pos.y = static_cast(source["y"].Float()); + ret->pos.z = static_cast(source["z"].Float()); + + ret->hiddenUpgrade = source["hidden"].Bool(); + ret->defName = AnimationPath::fromJson(source["animation"]); + ret->borderName = ImagePath::fromJson(source["border"]); + ret->areaName = ImagePath::fromJson(source["area"]); + + town.clientInfo.structures.emplace_back(ret); +} + +void CTownHandler::loadStructures(CTown &town, const JsonNode & source) const +{ + for(const auto & node : source.Struct()) + { + if (!node.second.isNull()) + loadStructure(town, node.first, node.second); + } +} + +void CTownHandler::loadTownHall(CTown &town, const JsonNode & source) const +{ + auto & dstSlots = town.clientInfo.hallSlots; + const auto & srcSlots = source.Vector(); + dstSlots.resize(srcSlots.size()); + + for(size_t i=0; iidentifiers()->requestIdentifier("building." + town.faction->getJsonKey(), src, [&](si32 identifier) + { + dst = BuildingID(identifier); + }); + } + } + } +} + +Point JsonToPoint(const JsonNode & node) +{ + if(!node.isStruct()) + return Point::makeInvalid(); + + Point ret; + ret.x = static_cast(node["x"].Float()); + ret.y = static_cast(node["y"].Float()); + return ret; +} + +void CTownHandler::loadSiegeScreen(CTown &town, const JsonNode & source) const +{ + town.clientInfo.siegePrefix = source["imagePrefix"].String(); + town.clientInfo.towerIconSmall = source["towerIconSmall"].String(); + town.clientInfo.towerIconLarge = source["towerIconLarge"].String(); + + VLC->identifiers()->requestIdentifier("creature", source["shooter"], [&town](si32 creature) + { + auto crId = CreatureID(creature); + if((*VLC->creh)[crId]->animation.missleFrameAngles.empty()) + logMod->error("Mod '%s' error: Creature '%s' on the Archer's tower is not a shooter. Mod should be fixed. Siege will not work properly!" + , town.faction->getNameTranslated() + , (*VLC->creh)[crId]->getNameSingularTranslated()); + + town.clientInfo.siegeShooter = crId; + }); + + auto & pos = town.clientInfo.siegePositions; + pos.resize(21); + + pos[8] = JsonToPoint(source["towers"]["top"]["tower"]); + pos[17] = JsonToPoint(source["towers"]["top"]["battlement"]); + pos[20] = JsonToPoint(source["towers"]["top"]["creature"]); + + pos[2] = JsonToPoint(source["towers"]["keep"]["tower"]); + pos[15] = JsonToPoint(source["towers"]["keep"]["battlement"]); + pos[18] = JsonToPoint(source["towers"]["keep"]["creature"]); + + pos[3] = JsonToPoint(source["towers"]["bottom"]["tower"]); + pos[16] = JsonToPoint(source["towers"]["bottom"]["battlement"]); + pos[19] = JsonToPoint(source["towers"]["bottom"]["creature"]); + + pos[9] = JsonToPoint(source["gate"]["gate"]); + pos[10] = JsonToPoint(source["gate"]["arch"]); + + pos[7] = JsonToPoint(source["walls"]["upper"]); + pos[6] = JsonToPoint(source["walls"]["upperMid"]); + pos[5] = JsonToPoint(source["walls"]["bottomMid"]); + pos[4] = JsonToPoint(source["walls"]["bottom"]); + + pos[13] = JsonToPoint(source["moat"]["moat"]); + pos[14] = JsonToPoint(source["moat"]["bank"]); + + pos[11] = JsonToPoint(source["static"]["bottom"]); + pos[12] = JsonToPoint(source["static"]["top"]); + pos[1] = JsonToPoint(source["static"]["background"]); +} + +static void readIcon(JsonNode source, std::string & small, std::string & large) +{ + if (source.getType() == JsonNode::JsonType::DATA_STRUCT) // don't crash on old format + { + small = source["small"].String(); + large = source["large"].String(); + } +} + +void CTownHandler::loadClientData(CTown &town, const JsonNode & source) const +{ + CTown::ClientInfo & info = town.clientInfo; + + readIcon(source["icons"]["village"]["normal"], info.iconSmall[0][0], info.iconLarge[0][0]); + readIcon(source["icons"]["village"]["built"], info.iconSmall[0][1], info.iconLarge[0][1]); + readIcon(source["icons"]["fort"]["normal"], info.iconSmall[1][0], info.iconLarge[1][0]); + readIcon(source["icons"]["fort"]["built"], info.iconSmall[1][1], info.iconLarge[1][1]); + + info.hallBackground = ImagePath::fromJson(source["hallBackground"]); + info.musicTheme = AudioPath::fromJson(source["musicTheme"]); + info.townBackground = ImagePath::fromJson(source["townBackground"]); + info.guildWindow = ImagePath::fromJson(source["guildWindow"]); + info.buildingsIcons = AnimationPath::fromJson(source["buildingsIcons"]); + + info.guildBackground = ImagePath::fromJson(source["guildBackground"]); + info.tavernVideo = VideoPath::fromJson(source["tavernVideo"]); + + loadTownHall(town, source["hallSlots"]); + loadStructures(town, source["structures"]); + loadSiegeScreen(town, source["siege"]); +} + +void CTownHandler::loadTown(CTown * town, const JsonNode & source) +{ + const auto * resIter = boost::find(GameConstants::RESOURCE_NAMES, source["primaryResource"].String()); + if(resIter == std::end(GameConstants::RESOURCE_NAMES)) + town->primaryRes = GameResID(EGameResID::WOOD_AND_ORE); //Wood + Ore + else + town->primaryRes = GameResID(resIter - std::begin(GameConstants::RESOURCE_NAMES)); + + warMachinesToLoad[town] = source["warMachine"]; + + town->mageLevel = static_cast(source["mageGuild"].Float()); + + town->namesCount = 0; + for(const auto & name : source["names"].Vector()) + { + VLC->generaltexth->registerString(town->faction->modScope, town->getRandomNameTextID(town->namesCount), name.String()); + town->namesCount += 1; + } + + if (!source["moatAbility"].isNull()) // VCMI 1.2 compatibility code + { + VLC->identifiers()->requestIdentifier( "spell", source["moatAbility"], [=](si32 ability) + { + town->moatAbility = SpellID(ability); + }); + } + else + { + VLC->identifiers()->requestIdentifier( source.meta, "spell", "castleMoat", [=](si32 ability) + { + town->moatAbility = SpellID(ability); + }); + } + + // Horde building creature level + for(const JsonNode &node : source["horde"].Vector()) + town->hordeLvl[static_cast(town->hordeLvl.size())] = static_cast(node.Float()); + + // town needs to have exactly 2 horde entries. Validation will take care of 2+ entries + // but anything below 2 must be handled here + for (size_t i=source["horde"].Vector().size(); i<2; i++) + town->hordeLvl[static_cast(i)] = -1; + + const JsonVector & creatures = source["creatures"].Vector(); + + town->creatures.resize(creatures.size()); + + for (size_t i=0; i< creatures.size(); i++) + { + const JsonVector & level = creatures[i].Vector(); + + town->creatures[i].resize(level.size()); + + for (size_t j=0; jidentifiers()->requestIdentifier("creature", level[j], [=](si32 creature) + { + town->creatures[i][j] = CreatureID(creature); + }); + } + } + + town->defaultTavernChance = static_cast(source["defaultTavern"].Float()); + /// set chance of specific hero class to appear in this town + for(const auto & node : source["tavern"].Struct()) + { + int chance = static_cast(node.second.Float()); + + VLC->identifiers()->requestIdentifier(node.second.meta, "heroClass",node.first, [=](si32 classID) + { + VLC->heroh->classes[HeroClassID(classID)]->selectionProbability[town->faction->getId()] = chance; + }); + } + + for(const auto & node : source["guildSpells"].Struct()) + { + int chance = static_cast(node.second.Float()); + + VLC->identifiers()->requestIdentifier(node.second.meta, "spell", node.first, [=](si32 spellID) + { + VLC->spellh->objects.at(spellID)->probabilities[town->faction->getId()] = chance; + }); + } + + for(const JsonNode & d : source["adventureMap"]["dwellings"].Vector()) + { + town->dwellings.push_back(d["graphics"].String()); + town->dwellingNames.push_back(d["name"].String()); + } + + loadBuildings(town, source["buildings"]); + loadClientData(*town, source); +} + +void CTownHandler::loadPuzzle(CFaction &faction, const JsonNode &source) const +{ + faction.puzzleMap.reserve(GameConstants::PUZZLE_MAP_PIECES); + + std::string prefix = source["prefix"].String(); + for(const JsonNode &piece : source["pieces"].Vector()) + { + size_t index = faction.puzzleMap.size(); + SPuzzleInfo spi; + + spi.x = static_cast(piece["x"].Float()); + spi.y = static_cast(piece["y"].Float()); + spi.whenUncovered = static_cast(piece["index"].Float()); + spi.number = static_cast(index); + + // filename calculation + std::ostringstream suffix; + suffix << std::setfill('0') << std::setw(2) << index; + + spi.filename = ImagePath::builtinTODO(prefix + suffix.str()); + + faction.puzzleMap.push_back(spi); + } + assert(faction.puzzleMap.size() == GameConstants::PUZZLE_MAP_PIECES); +} + +CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode & source, const std::string & identifier, size_t index) +{ + assert(identifier.find(':') == std::string::npos); + + auto * faction = new CFaction(); + + faction->index = static_cast(index); + faction->modScope = scope; + faction->identifier = identifier; + + VLC->generaltexth->registerString(scope, faction->getNameTextID(), source["name"].String()); + + faction->creatureBg120 = ImagePath::fromJson(source["creatureBackground"]["120px"]); + faction->creatureBg130 = ImagePath::fromJson(source["creatureBackground"]["130px"]); + + faction->boatType = BoatId::CASTLE; //Do not crash + if (!source["boat"].isNull()) + { + VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID) + { + faction->boatType = BoatId(boatTypeID); + }); + } + + int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, source["alignment"].String()); + if (alignment == -1) + faction->alignment = EAlignment::NEUTRAL; + else + faction->alignment = static_cast(alignment); + + auto preferUndergound = source["preferUndergroundPlacement"]; + faction->preferUndergroundPlacement = preferUndergound.isNull() ? false : preferUndergound.Bool(); + + // NOTE: semi-workaround - normally, towns are supposed to have native terrains. + // Towns without one are exceptions. So, vcmi requires nativeTerrain to be defined + // But allows it to be defined with explicit value of "none" if town should not have native terrain + // This is better than allowing such terrain-less towns silently, leading to issues with RMG + faction->nativeTerrain = ETerrainId::NONE; + if ( !source["nativeTerrain"].isNull() && source["nativeTerrain"].String() != "none") + { + VLC->identifiers()->requestIdentifier("terrain", source["nativeTerrain"], [=](int32_t index){ + faction->nativeTerrain = TerrainId(index); + + auto const & terrain = VLC->terrainTypeHandler->getById(faction->nativeTerrain); + + if (!terrain->isSurface() && !terrain->isUnderground()) + logMod->warn("Faction %s has terrain %s as native, but terrain is not suitable for either surface or subterranean layers!", faction->getJsonKey(), terrain->getJsonKey()); + }); + } + + if (!source["town"].isNull()) + { + faction->town = new CTown(); + faction->town->faction = faction; + loadTown(faction->town, source["town"]); + } + else + faction->town = nullptr; + + if (!source["puzzleMap"].isNull()) + loadPuzzle(*faction, source["puzzleMap"]); + + return faction; +} + +void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data) +{ + auto * object = loadFromJson(scope, data, name, objects.size()); + + objects.emplace_back(object); + + if (object->town) + { + auto & info = object->town->clientInfo; + info.icons[0][0] = 8 + object->index * 4 + 0; + info.icons[0][1] = 8 + object->index * 4 + 1; + info.icons[1][0] = 8 + object->index * 4 + 2; + info.icons[1][1] = 8 + object->index * 4 + 3; + + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) + { + // register town once objects are loaded + JsonNode config = data["town"]["mapObject"]; + config["faction"].String() = name; + config["faction"].meta = scope; + if (config.meta.empty())// MODS COMPATIBILITY FOR 0.96 + config.meta = scope; + VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); + + // MODS COMPATIBILITY FOR 0.96 + const auto & advMap = data["town"]["adventureMap"]; + if (!advMap.isNull()) + { + logMod->warn("Outdated town mod. Will try to generate valid templates out of fort"); + JsonNode config; + config["animation"] = advMap["castle"]; + VLC->objtypeh->getHandlerFor(index, object->index)->addTemplate(config); + } + }); + } + + registerObject(scope, "faction", name, object->index); +} + +void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) +{ + auto * object = loadFromJson(scope, data, name, index); + + if (objects.size() > index) + assert(objects[index] == nullptr); // ensure that this id was not loaded before + else + objects.resize(index + 1); + objects[index] = object; + + if (object->town) + { + auto & info = object->town->clientInfo; + info.icons[0][0] = (GameConstants::F_NUMBER + object->index) * 2 + 0; + info.icons[0][1] = (GameConstants::F_NUMBER + object->index) * 2 + 1; + info.icons[1][0] = object->index * 2 + 0; + info.icons[1][1] = object->index * 2 + 1; + + VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) + { + // register town once objects are loaded + JsonNode config = data["town"]["mapObject"]; + config["faction"].String() = name; + config["faction"].meta = scope; + VLC->objtypeh->loadSubObject(object->identifier, config, index, object->index); + }); + } + + registerObject(scope, "faction", name, object->index); +} + +void CTownHandler::loadRandomFaction() +{ + JsonNode randomFactionJson(JsonPath::builtin("config/factions/random.json")); + randomFactionJson.setMeta(ModScope::scopeBuiltin(), true); + loadBuildings(randomTown, randomFactionJson["random"]["town"]["buildings"]); +} + +void CTownHandler::loadCustom() +{ + loadRandomFaction(); +} + +void CTownHandler::afterLoadFinalization() +{ + initializeRequirements(); + initializeOverridden(); + initializeWarMachines(); +} + +void CTownHandler::initializeRequirements() +{ + // must be done separately after all ID's are known + for (auto & requirement : requirementsToLoad) + { + requirement.building->requirements = CBuilding::TRequired(requirement.json, [&](const JsonNode & node) -> BuildingID + { + if (node.Vector().size() > 1) + { + logMod->error("Unexpected length of town buildings requirements: %d", node.Vector().size()); + logMod->error("Entry contains: "); + logMod->error(node.toJson()); + } + + auto index = VLC->identifiers()->getIdentifier(requirement.town->getBuildingScope(), node[0]); + + if (!index.has_value()) + { + logMod->error("Unknown building in town buildings: %s", node[0].String()); + return BuildingID::NONE; + } + return BuildingID(index.value()); + }); + } + requirementsToLoad.clear(); +} + +void CTownHandler::initializeOverridden() +{ + for(auto & bidHelper : overriddenBidsToLoad) + { + auto jsonNode = bidHelper.json; + auto scope = bidHelper.town->getBuildingScope(); + + for(const auto & b : jsonNode.Vector()) + { + auto bid = BuildingID(VLC->identifiers()->getIdentifier(scope, b).value()); + bidHelper.building->overrideBids.insert(bid); + } + } + overriddenBidsToLoad.clear(); +} + +void CTownHandler::initializeWarMachines() +{ + // must be done separately after all objects are loaded + for(auto & p : warMachinesToLoad) + { + CTown * t = p.first; + JsonNode creatureKey = p.second; + + auto ret = VLC->identifiers()->getIdentifier("creature", creatureKey, false); + + if(ret) + { + const CCreature * creature = CreatureID(*ret).toCreature(); + + t->warMachine = creature->warMachine; + } + } + + warMachinesToLoad.clear(); +} + +std::vector CTownHandler::getDefaultAllowed() const +{ + std::vector allowedFactions; + allowedFactions.reserve(objects.size()); + for(auto town : objects) + { + allowedFactions.push_back(town->town != nullptr); + } + return allowedFactions; +} + +std::set CTownHandler::getAllowedFactions(bool withTown) const +{ + std::set allowedFactions; + std::vector allowed; + if (withTown) + allowed = getDefaultAllowed(); + else + allowed.resize( objects.size(), true); + + for (size_t i=0; i(i)); + + return allowedFactions; +} + +const std::vector & CTownHandler::getTypeNames() const +{ + static const std::vector typeNames = { "faction", "town" }; + return typeNames; +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 01c8c5dbe..45493cabe 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -1,458 +1,458 @@ -/* - * CTownHandler.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 -#include - -#include "ConstTransitivePtr.h" -#include "ResourceSet.h" -#include "int3.h" -#include "GameConstants.h" -#include "IHandlerBase.h" -#include "LogicalExpression.h" -#include "battle/BattleHex.h" -#include "bonuses/Bonus.h" -#include "bonuses/BonusList.h" -#include "Point.h" -#include "rewardable/Info.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CLegacyConfigParser; -class JsonNode; -class CTown; -class CFaction; -struct BattleHex; -class JsonSerializeFormat; - -/// a typical building encountered in every castle ;] -/// this is structure available to both client and server -/// contains all mechanics-related data about town structures - - - -class DLL_LINKAGE CBuilding -{ - std::string modScope; - std::string identifier; - -public: - using TRequired = LogicalExpression; - - CTown * town; // town this building belongs to - TResources resources; - TResources produce; - TRequired requirements; - - BuildingID bid; //structure ID - BuildingID upgrade; /// indicates that building "upgrade" can be improved by this, -1 = empty - BuildingSubID::EBuildingSubID subId; /// subtype for special buildings, -1 = the building is not special - std::set overrideBids; /// the building which bonuses should be overridden with bonuses of the current building - BonusList buildingBonuses; - BonusList onVisitBonuses; - - Rewardable::Info rewardableObjectInfo; ///configurable rewards for special buildings - - enum EBuildMode - { - BUILD_NORMAL, // 0 - normal, default - BUILD_AUTO, // 1 - auto - building appears when all requirements are built - BUILD_SPECIAL, // 2 - special - building can not be built normally - BUILD_GRAIL // 3 - grail - building reqires grail to be built - } mode; - - enum ETowerHeight // for lookup towers and some grails - { - HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability - HEIGHT_LOW = 10, // low lookout tower, but castle without lookout tower gives radius 5 - HEIGHT_AVERAGE = 15, - HEIGHT_HIGH = 20, // such tower is in the Tower town - HEIGHT_SKYSHIP = std::numeric_limits::max() // grail, open entire map - } height; - - static const std::map MODES; - static const std::map TOWER_TYPES; - - CBuilding() : town(nullptr), mode(BUILD_NORMAL) {}; - - std::string getJsonKey() const; - - std::string getNameTranslated() const; - std::string getDescriptionTranslated() const; - - std::string getBaseTextID() const; - std::string getNameTextID() const; - std::string getDescriptionTextID() const; - - //return base of upgrade(s) or this - BuildingID getBase() const; - - // returns how many times build has to be upgraded to become build - si32 getDistance(const BuildingID & build) const; - - STRONG_INLINE - bool IsTradeBuilding() const - { - return bid == BuildingID::MARKETPLACE || subId == BuildingSubID::ARTIFACT_MERCHANT || subId == BuildingSubID::FREELANCERS_GUILD; - } - - STRONG_INLINE - bool IsWeekBonus() const - { - return subId == BuildingSubID::STABLES || subId == BuildingSubID::MANA_VORTEX; - } - - STRONG_INLINE - bool IsVisitingBonus() const - { - return subId == BuildingSubID::ATTACK_VISITING_BONUS || - subId == BuildingSubID::DEFENSE_VISITING_BONUS || - subId == BuildingSubID::SPELL_POWER_VISITING_BONUS || - subId == BuildingSubID::KNOWLEDGE_VISITING_BONUS || - subId == BuildingSubID::EXPERIENCE_VISITING_BONUS || - subId == BuildingSubID::CUSTOM_VISITING_BONUS; - } - - void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & town; - h & bid; - h & resources; - h & produce; - h & requirements; - h & upgrade; - h & mode; - h & subId; - h & height; - h & overrideBids; - h & buildingBonuses; - h & onVisitBonuses; - h & rewardableObjectInfo; - } - - friend class CTownHandler; -}; - -/// This is structure used only by client -/// Consists of all gui-related data about town structures -/// Should be moved from lib to client -struct DLL_LINKAGE CStructure -{ - CBuilding * building; // base building. If null - this structure will be always present on screen - CBuilding * buildable; // building that will be used to determine built building and visible cost. Usually same as "building" - - int3 pos; - AnimationPath defName; - ImagePath borderName; - ImagePath areaName; - std::string identifier; - - bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) - template void serialize(Handler &h, const int version) - { - h & pos; - h & defName; - h & borderName; - h & areaName; - h & identifier; - h & building; - h & buildable; - h & hiddenUpgrade; - } -}; - -struct DLL_LINKAGE SPuzzleInfo -{ - ui16 number; //type of puzzle - si16 x, y; //position - ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) - ImagePath filename; //file with graphic of this puzzle - - template void serialize(Handler &h, const int version) - { - h & number; - h & x; - h & y; - h & whenUncovered; - h & filename; - } -}; - -class DLL_LINKAGE CFaction : public Faction -{ - friend class CTownHandler; - friend class CBuilding; - friend class CTown; - - std::string modScope; - std::string identifier; - - FactionID index = FactionID::NEUTRAL; - - FactionID getFaction() const override; //This function should not be used - -public: - TerrainId nativeTerrain; - EAlignment alignment = EAlignment::NEUTRAL; - bool preferUndergroundPlacement = false; - - /// Boat that will be used by town shipyard (if any) - /// and for placing heroes directly on boat (in map editor, water prisons & taverns) - BoatId boatType = BoatId::CASTLE; - - CTown * town = nullptr; //NOTE: can be null - - ImagePath creatureBg120; - ImagePath creatureBg130; - - std::vector puzzleMap; - - CFaction() = default; - ~CFaction(); - - int32_t getIndex() const override; - int32_t getIconIndex() const override; - std::string getJsonKey() const override; - void registerIcons(const IconRegistar & cb) const override; - FactionID getId() const override; - - std::string getNameTranslated() const override; - std::string getNameTextID() const override; - - bool hasTown() const override; - TerrainId getNativeTerrain() const override; - EAlignment getAlignment() const override; - BoatId getBoatType() const override; - - void updateFrom(const JsonNode & data); - void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & index; - h & nativeTerrain; - h & boatType; - h & alignment; - h & town; - h & creatureBg120; - h & creatureBg130; - h & puzzleMap; - } -}; - -class DLL_LINKAGE CTown -{ - friend class CTownHandler; - size_t namesCount = 0; - -public: - CTown(); - ~CTown(); - - std::string getBuildingScope() const; - std::set getAllBuildings() const; - const CBuilding * getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const; - std::string getGreeting(BuildingSubID::EBuildingSubID subID) const; - void setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const; //may affect only mutable field - BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; - - std::string getRandomNameTranslated(size_t index) const; - std::string getRandomNameTextID(size_t index) const; - size_t getRandomNamesCount() const; - - CFaction * faction; - - /// level -> list of creatures on this tier - // TODO: replace with pointers to CCreature - std::vector > creatures; - - std::map > buildings; - - std::vector dwellings; //defs for adventure map dwellings for new towns, [0] means tier 1 creatures etc. - std::vector dwellingNames; - - // should be removed at least from configs in favor of auto-detection - std::map hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present) - ui32 mageLevel; //max available mage guild level - GameResID primaryRes; - ArtifactID warMachine; - SpellID moatAbility; - - // default chance for hero of specific class to appear in tavern, if field "tavern" was not set - // resulting chance = sqrt(town.chance * heroClass.chance) - ui32 defaultTavernChance; - - // Client-only data. Should be moved away from lib - struct ClientInfo - { - //icons [fort is present?][build limit reached?] -> index of icon in def files - int icons[2][2]; - std::string iconSmall[2][2]; /// icon names used during loading - std::string iconLarge[2][2]; - VideoPath tavernVideo; - AudioPath musicTheme; - ImagePath townBackground; - ImagePath guildBackground; - ImagePath guildWindow; - AnimationPath buildingsIcons; - ImagePath hallBackground; - /// vector[row][column] = list of buildings in this slot - std::vector< std::vector< std::vector > > hallSlots; - - /// list of town screen structures. - /// NOTE: index in vector is meaningless. Vector used instead of list for a bit faster access - std::vector > structures; - - std::string siegePrefix; - std::vector siegePositions; - CreatureID siegeShooter; // shooter creature ID - std::string towerIconSmall; - std::string towerIconLarge; - - template void serialize(Handler &h, const int version) - { - h & icons; - h & iconSmall; - h & iconLarge; - h & tavernVideo; - h & musicTheme; - h & townBackground; - h & guildBackground; - h & guildWindow; - h & buildingsIcons; - h & hallBackground; - h & hallSlots; - h & structures; - h & siegePrefix; - h & siegePositions; - h & siegeShooter; - h & towerIconSmall; - h & towerIconLarge; - } - } clientInfo; - - template void serialize(Handler &h, const int version) - { - h & namesCount; - h & faction; - h & creatures; - h & dwellings; - h & dwellingNames; - h & buildings; - h & hordeLvl; - h & mageLevel; - h & primaryRes; - h & warMachine; - h & clientInfo; - h & moatAbility; - h & defaultTavernChance; - } - -private: - ///generated bonusing buildings messages for all towns of this type. - mutable std::map specialMessages; //may be changed by CGTownBuilding::getVisitingBonusGreeting() const -}; - -class DLL_LINKAGE CTownHandler : public CHandlerBase -{ - struct BuildingRequirementsHelper - { - JsonNode json; - CBuilding * building; - CTown * town; - }; - - std::map warMachinesToLoad; - std::vector requirementsToLoad; - std::vector overriddenBidsToLoad; //list of buildings, which bonuses should be overridden. - - static TPropagatorPtr & emptyPropagator(); - - void initializeRequirements(); - void initializeOverridden(); - void initializeWarMachines(); - - /// loads CBuilding's into town - void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector & bidsToLoad) const; - void loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source); - void loadBuildings(CTown * town, const JsonNode & source); - - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, int subtype = -1) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype = -1) const; - std::shared_ptr createBonusImpl(const BuildingID & building, - BonusType type, - int val, - TPropagatorPtr & prop, - const std::string & description, - int subtype = -1) const; - - /// loads CStructure's into town - void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; - void loadStructures(CTown & town, const JsonNode & source) const; - - /// loads town hall vector (hallSlots) - void loadTownHall(CTown & town, const JsonNode & source) const; - void loadSiegeScreen(CTown & town, const JsonNode & source) const; - - void loadClientData(CTown & town, const JsonNode & source) const; - - void loadTown(CTown * town, const JsonNode & source); - - void loadPuzzle(CFaction & faction, const JsonNode & source) const; - - void loadRandomFaction(); - - -public: - template - static R getMappedValue(const K key, const R defval, const std::map & map, bool required = true); - template - static R getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required = true); - - CTown * randomTown; - CFaction * randomFaction; - - CTownHandler(); - ~CTownHandler(); - - std::vector loadLegacyData() override; - - void loadObject(std::string scope, std::string name, const JsonNode & data) override; - void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void addBonusesForVanilaBuilding(CBuilding * building) const; - - void loadCustom() override; - void afterLoadFinalization() override; - - std::vector getDefaultAllowed() const override; - std::set getAllowedFactions(bool withTown = true) const; - - static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); - - template void serialize(Handler &h, const int version) - { - h & objects; - h & randomTown; - } - -protected: - const std::vector & getTypeNames() const override; - CFaction * loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CTownHandler.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 +#include + +#include "ConstTransitivePtr.h" +#include "ResourceSet.h" +#include "int3.h" +#include "GameConstants.h" +#include "IHandlerBase.h" +#include "LogicalExpression.h" +#include "battle/BattleHex.h" +#include "bonuses/Bonus.h" +#include "bonuses/BonusList.h" +#include "Point.h" +#include "rewardable/Info.h" +#include "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CLegacyConfigParser; +class JsonNode; +class CTown; +class CFaction; +struct BattleHex; +class JsonSerializeFormat; + +/// a typical building encountered in every castle ;] +/// this is structure available to both client and server +/// contains all mechanics-related data about town structures + + + +class DLL_LINKAGE CBuilding +{ + std::string modScope; + std::string identifier; + +public: + using TRequired = LogicalExpression; + + CTown * town; // town this building belongs to + TResources resources; + TResources produce; + TRequired requirements; + + BuildingID bid; //structure ID + BuildingID upgrade; /// indicates that building "upgrade" can be improved by this, -1 = empty + BuildingSubID::EBuildingSubID subId; /// subtype for special buildings, -1 = the building is not special + std::set overrideBids; /// the building which bonuses should be overridden with bonuses of the current building + BonusList buildingBonuses; + BonusList onVisitBonuses; + + Rewardable::Info rewardableObjectInfo; ///configurable rewards for special buildings + + enum EBuildMode + { + BUILD_NORMAL, // 0 - normal, default + BUILD_AUTO, // 1 - auto - building appears when all requirements are built + BUILD_SPECIAL, // 2 - special - building can not be built normally + BUILD_GRAIL // 3 - grail - building reqires grail to be built + } mode; + + enum ETowerHeight // for lookup towers and some grails + { + HEIGHT_NO_TOWER = 5, // building has not 'lookout tower' ability + HEIGHT_LOW = 10, // low lookout tower, but castle without lookout tower gives radius 5 + HEIGHT_AVERAGE = 15, + HEIGHT_HIGH = 20, // such tower is in the Tower town + HEIGHT_SKYSHIP = std::numeric_limits::max() // grail, open entire map + } height; + + static const std::map MODES; + static const std::map TOWER_TYPES; + + CBuilding() : town(nullptr), mode(BUILD_NORMAL) {}; + + std::string getJsonKey() const; + + std::string getNameTranslated() const; + std::string getDescriptionTranslated() const; + + std::string getBaseTextID() const; + std::string getNameTextID() const; + std::string getDescriptionTextID() const; + + //return base of upgrade(s) or this + BuildingID getBase() const; + + // returns how many times build has to be upgraded to become build + si32 getDistance(const BuildingID & build) const; + + STRONG_INLINE + bool IsTradeBuilding() const + { + return bid == BuildingID::MARKETPLACE || subId == BuildingSubID::ARTIFACT_MERCHANT || subId == BuildingSubID::FREELANCERS_GUILD; + } + + STRONG_INLINE + bool IsWeekBonus() const + { + return subId == BuildingSubID::STABLES || subId == BuildingSubID::MANA_VORTEX; + } + + STRONG_INLINE + bool IsVisitingBonus() const + { + return subId == BuildingSubID::ATTACK_VISITING_BONUS || + subId == BuildingSubID::DEFENSE_VISITING_BONUS || + subId == BuildingSubID::SPELL_POWER_VISITING_BONUS || + subId == BuildingSubID::KNOWLEDGE_VISITING_BONUS || + subId == BuildingSubID::EXPERIENCE_VISITING_BONUS || + subId == BuildingSubID::CUSTOM_VISITING_BONUS; + } + + void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; + + template void serialize(Handler &h, const int version) + { + h & modScope; + h & identifier; + h & town; + h & bid; + h & resources; + h & produce; + h & requirements; + h & upgrade; + h & mode; + h & subId; + h & height; + h & overrideBids; + h & buildingBonuses; + h & onVisitBonuses; + h & rewardableObjectInfo; + } + + friend class CTownHandler; +}; + +/// This is structure used only by client +/// Consists of all gui-related data about town structures +/// Should be moved from lib to client +struct DLL_LINKAGE CStructure +{ + CBuilding * building; // base building. If null - this structure will be always present on screen + CBuilding * buildable; // building that will be used to determine built building and visible cost. Usually same as "building" + + int3 pos; + AnimationPath defName; + ImagePath borderName; + ImagePath areaName; + std::string identifier; + + bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) + template void serialize(Handler &h, const int version) + { + h & pos; + h & defName; + h & borderName; + h & areaName; + h & identifier; + h & building; + h & buildable; + h & hiddenUpgrade; + } +}; + +struct DLL_LINKAGE SPuzzleInfo +{ + ui16 number; //type of puzzle + si16 x, y; //position + ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) + ImagePath filename; //file with graphic of this puzzle + + template void serialize(Handler &h, const int version) + { + h & number; + h & x; + h & y; + h & whenUncovered; + h & filename; + } +}; + +class DLL_LINKAGE CFaction : public Faction +{ + friend class CTownHandler; + friend class CBuilding; + friend class CTown; + + std::string modScope; + std::string identifier; + + FactionID index = FactionID::NEUTRAL; + + FactionID getFaction() const override; //This function should not be used + +public: + TerrainId nativeTerrain; + EAlignment alignment = EAlignment::NEUTRAL; + bool preferUndergroundPlacement = false; + + /// Boat that will be used by town shipyard (if any) + /// and for placing heroes directly on boat (in map editor, water prisons & taverns) + BoatId boatType = BoatId::CASTLE; + + CTown * town = nullptr; //NOTE: can be null + + ImagePath creatureBg120; + ImagePath creatureBg130; + + std::vector puzzleMap; + + CFaction() = default; + ~CFaction(); + + int32_t getIndex() const override; + int32_t getIconIndex() const override; + std::string getJsonKey() const override; + void registerIcons(const IconRegistar & cb) const override; + FactionID getId() const override; + + std::string getNameTranslated() const override; + std::string getNameTextID() const override; + + bool hasTown() const override; + TerrainId getNativeTerrain() const override; + EAlignment getAlignment() const override; + BoatId getBoatType() const override; + + void updateFrom(const JsonNode & data); + void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & modScope; + h & identifier; + h & index; + h & nativeTerrain; + h & boatType; + h & alignment; + h & town; + h & creatureBg120; + h & creatureBg130; + h & puzzleMap; + } +}; + +class DLL_LINKAGE CTown +{ + friend class CTownHandler; + size_t namesCount = 0; + +public: + CTown(); + ~CTown(); + + std::string getBuildingScope() const; + std::set getAllBuildings() const; + const CBuilding * getSpecialBuilding(BuildingSubID::EBuildingSubID subID) const; + std::string getGreeting(BuildingSubID::EBuildingSubID subID) const; + void setGreeting(BuildingSubID::EBuildingSubID subID, const std::string & message) const; //may affect only mutable field + BuildingID getBuildingType(BuildingSubID::EBuildingSubID subID) const; + + std::string getRandomNameTranslated(size_t index) const; + std::string getRandomNameTextID(size_t index) const; + size_t getRandomNamesCount() const; + + CFaction * faction; + + /// level -> list of creatures on this tier + // TODO: replace with pointers to CCreature + std::vector > creatures; + + std::map > buildings; + + std::vector dwellings; //defs for adventure map dwellings for new towns, [0] means tier 1 creatures etc. + std::vector dwellingNames; + + // should be removed at least from configs in favor of auto-detection + std::map hordeLvl; //[0] - first horde building creature level; [1] - second horde building (-1 if not present) + ui32 mageLevel; //max available mage guild level + GameResID primaryRes; + ArtifactID warMachine; + SpellID moatAbility; + + // default chance for hero of specific class to appear in tavern, if field "tavern" was not set + // resulting chance = sqrt(town.chance * heroClass.chance) + ui32 defaultTavernChance; + + // Client-only data. Should be moved away from lib + struct ClientInfo + { + //icons [fort is present?][build limit reached?] -> index of icon in def files + int icons[2][2]; + std::string iconSmall[2][2]; /// icon names used during loading + std::string iconLarge[2][2]; + VideoPath tavernVideo; + AudioPath musicTheme; + ImagePath townBackground; + ImagePath guildBackground; + ImagePath guildWindow; + AnimationPath buildingsIcons; + ImagePath hallBackground; + /// vector[row][column] = list of buildings in this slot + std::vector< std::vector< std::vector > > hallSlots; + + /// list of town screen structures. + /// NOTE: index in vector is meaningless. Vector used instead of list for a bit faster access + std::vector > structures; + + std::string siegePrefix; + std::vector siegePositions; + CreatureID siegeShooter; // shooter creature ID + std::string towerIconSmall; + std::string towerIconLarge; + + template void serialize(Handler &h, const int version) + { + h & icons; + h & iconSmall; + h & iconLarge; + h & tavernVideo; + h & musicTheme; + h & townBackground; + h & guildBackground; + h & guildWindow; + h & buildingsIcons; + h & hallBackground; + h & hallSlots; + h & structures; + h & siegePrefix; + h & siegePositions; + h & siegeShooter; + h & towerIconSmall; + h & towerIconLarge; + } + } clientInfo; + + template void serialize(Handler &h, const int version) + { + h & namesCount; + h & faction; + h & creatures; + h & dwellings; + h & dwellingNames; + h & buildings; + h & hordeLvl; + h & mageLevel; + h & primaryRes; + h & warMachine; + h & clientInfo; + h & moatAbility; + h & defaultTavernChance; + } + +private: + ///generated bonusing buildings messages for all towns of this type. + mutable std::map specialMessages; //may be changed by CGTownBuilding::getVisitingBonusGreeting() const +}; + +class DLL_LINKAGE CTownHandler : public CHandlerBase +{ + struct BuildingRequirementsHelper + { + JsonNode json; + CBuilding * building; + CTown * town; + }; + + std::map warMachinesToLoad; + std::vector requirementsToLoad; + std::vector overriddenBidsToLoad; //list of buildings, which bonuses should be overridden. + + static TPropagatorPtr & emptyPropagator(); + + void initializeRequirements(); + void initializeOverridden(); + void initializeWarMachines(); + + /// loads CBuilding's into town + void loadBuildingRequirements(CBuilding * building, const JsonNode & source, std::vector & bidsToLoad) const; + void loadBuilding(CTown * town, const std::string & stringID, const JsonNode & source); + void loadBuildings(CTown * town, const JsonNode & source); + + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, int subtype = -1) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype = -1) const; + std::shared_ptr createBonusImpl(const BuildingID & building, + BonusType type, + int val, + TPropagatorPtr & prop, + const std::string & description, + int subtype = -1) const; + + /// loads CStructure's into town + void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; + void loadStructures(CTown & town, const JsonNode & source) const; + + /// loads town hall vector (hallSlots) + void loadTownHall(CTown & town, const JsonNode & source) const; + void loadSiegeScreen(CTown & town, const JsonNode & source) const; + + void loadClientData(CTown & town, const JsonNode & source) const; + + void loadTown(CTown * town, const JsonNode & source); + + void loadPuzzle(CFaction & faction, const JsonNode & source) const; + + void loadRandomFaction(); + + +public: + template + static R getMappedValue(const K key, const R defval, const std::map & map, bool required = true); + template + static R getMappedValue(const JsonNode & node, const R defval, const std::map & map, bool required = true); + + CTown * randomTown; + CFaction * randomFaction; + + CTownHandler(); + ~CTownHandler(); + + std::vector loadLegacyData() override; + + void loadObject(std::string scope, std::string name, const JsonNode & data) override; + void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; + void addBonusesForVanilaBuilding(CBuilding * building) const; + + void loadCustom() override; + void afterLoadFinalization() override; + + std::vector getDefaultAllowed() const override; + std::set getAllowedFactions(bool withTown = true) const; + + static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); + + template void serialize(Handler &h, const int version) + { + h & objects; + h & randomTown; + } + +protected: + const std::vector & getTypeNames() const override; + CFaction * loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Color.h b/lib/Color.h index e6558e9f3..2c231f2b1 100644 --- a/lib/Color.h +++ b/lib/Color.h @@ -1,62 +1,62 @@ -/* - * Color.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 - -VCMI_LIB_NAMESPACE_BEGIN - -/// An object that represents RGBA color -class ColorRGBA -{ -public: - enum : uint8_t - { - ALPHA_OPAQUE = 255, - ALPHA_TRANSPARENT = 0, - }; - - uint8_t r; - uint8_t g; - uint8_t b; - uint8_t a; - - //constructors - constexpr ColorRGBA() - :r(0) - ,g(0) - ,b(0) - ,a(0) - { - } - - constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) - : r(r) - , g(g) - , b(b) - , a(a) - {} - - constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b) - : r(r) - , g(g) - , b(b) - , a(ALPHA_OPAQUE) - {} - - template - void serialize(Handler &h, const int version) - { - h & r; - h & g; - h & b; - h & a; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Color.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 + +VCMI_LIB_NAMESPACE_BEGIN + +/// An object that represents RGBA color +class ColorRGBA +{ +public: + enum : uint8_t + { + ALPHA_OPAQUE = 255, + ALPHA_TRANSPARENT = 0, + }; + + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + + //constructors + constexpr ColorRGBA() + :r(0) + ,g(0) + ,b(0) + ,a(0) + { + } + + constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) + : r(r) + , g(g) + , b(b) + , a(a) + {} + + constexpr ColorRGBA(uint8_t r, uint8_t g, uint8_t b) + : r(r) + , g(g) + , b(b) + , a(ALPHA_OPAQUE) + {} + + template + void serialize(Handler &h, const int version) + { + h & r; + h & g; + h & b; + h & a; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/CondSh.h b/lib/CondSh.h index 0120a36fa..cc3540431 100644 --- a/lib/CondSh.h +++ b/lib/CondSh.h @@ -1,71 +1,71 @@ -/* - * CondSh.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 - -VCMI_LIB_NAMESPACE_BEGIN - -/// Used for multithreading, wraps boost functions -template struct CondSh -{ - T data; - boost::condition_variable cond; - boost::mutex mx; - - CondSh() : data(T()) {} - - CondSh(T t) : data(t) {} - - // set data - void set(T t) - { - boost::unique_lock lock(mx); - data = t; - } - - // set data and notify - void setn(T t) - { - set(t); - cond.notify_all(); - }; - - // get stored value - T get() - { - boost::unique_lock lock(mx); - return data; - } - - // waits until data is set to false - void waitWhileTrue() - { - boost::unique_lock un(mx); - while(data) - cond.wait(un); - } - - // waits while data is set to arg - void waitWhile(const T & t) - { - boost::unique_lock un(mx); - while(data == t) - cond.wait(un); - } - - // waits until data is set to arg - void waitUntil(const T & t) - { - boost::unique_lock un(mx); - while(data != t) - cond.wait(un); - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CondSh.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 + +VCMI_LIB_NAMESPACE_BEGIN + +/// Used for multithreading, wraps boost functions +template struct CondSh +{ + T data; + boost::condition_variable cond; + boost::mutex mx; + + CondSh() : data(T()) {} + + CondSh(T t) : data(t) {} + + // set data + void set(T t) + { + boost::unique_lock lock(mx); + data = t; + } + + // set data and notify + void setn(T t) + { + set(t); + cond.notify_all(); + }; + + // get stored value + T get() + { + boost::unique_lock lock(mx); + return data; + } + + // waits until data is set to false + void waitWhileTrue() + { + boost::unique_lock un(mx); + while(data) + cond.wait(un); + } + + // waits while data is set to arg + void waitWhile(const T & t) + { + boost::unique_lock un(mx); + while(data == t) + cond.wait(un); + } + + // waits until data is set to arg + void waitUntil(const T & t) + { + boost::unique_lock un(mx); + while(data != t) + cond.wait(un); + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ConstTransitivePtr.h b/lib/ConstTransitivePtr.h index 9867bf65e..e29ccbced 100644 --- a/lib/ConstTransitivePtr.h +++ b/lib/ConstTransitivePtr.h @@ -1,80 +1,80 @@ -/* - * ConstTransitivePtr.h, part of VCMI engine - * - * Authors: listed in file AUTHORS in main folder - * - * License: GNU General Public License v2.0 or later - * Full text of license available in license.txt file, in main folder - * - */ -#pragma once - -class CGameHandler; - -VCMI_LIB_NAMESPACE_BEGIN - -template -class ConstTransitivePtr -{ - T *ptr = nullptr; - ConstTransitivePtr(const T *Ptr) - : ptr(const_cast(Ptr)) - {} -public: - ConstTransitivePtr(T *Ptr = nullptr) - : ptr(Ptr) - {} - ConstTransitivePtr(std::nullptr_t) - {} - const T& operator*() const - { - return *ptr; - } - T& operator*() - { - return *ptr; - } - operator const T*() const - { - return ptr; - } - T* get() - { - return ptr; - } - const T* get() const - { - return ptr; - } - operator T*() - { - return ptr; - } - T *operator->() - { - return ptr; - } - const T *operator->() const - { - return ptr; - } - const T*operator=(T *t) - { - return ptr = t; - } - - void dellNull() - { - delete ptr; - ptr = nullptr; - } - - template void serialize(Handler &h, const int version) - { - h & ptr; - } - - friend class ::CGameHandler; -}; - -VCMI_LIB_NAMESPACE_END +/* + * ConstTransitivePtr.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class CGameHandler; + +VCMI_LIB_NAMESPACE_BEGIN + +template +class ConstTransitivePtr +{ + T *ptr = nullptr; + ConstTransitivePtr(const T *Ptr) + : ptr(const_cast(Ptr)) + {} +public: + ConstTransitivePtr(T *Ptr = nullptr) + : ptr(Ptr) + {} + ConstTransitivePtr(std::nullptr_t) + {} + const T& operator*() const + { + return *ptr; + } + T& operator*() + { + return *ptr; + } + operator const T*() const + { + return ptr; + } + T* get() + { + return ptr; + } + const T* get() const + { + return ptr; + } + operator T*() + { + return ptr; + } + T *operator->() + { + return ptr; + } + const T *operator->() const + { + return ptr; + } + const T*operator=(T *t) + { + return ptr = t; + } + + void dellNull() + { + delete ptr; + ptr = nullptr; + } + + template void serialize(Handler &h, const int version) + { + h & ptr; + } + + friend class ::CGameHandler; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameConstants.h b/lib/GameConstants.h index cb88895ac..3659b5428 100644 --- a/lib/GameConstants.h +++ b/lib/GameConstants.h @@ -1,22 +1,22 @@ -/* - * GameConstants.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 "constants/NumericConstants.h" -#include "constants/Enumerations.h" -#include "constants/EntityIdentifiers.h" - -VCMI_LIB_NAMESPACE_BEGIN - -using TExpType = si64; -using TQuantity = si32; -using TRmgTemplateZoneId = int; - -VCMI_LIB_NAMESPACE_END +/* + * GameConstants.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 "constants/NumericConstants.h" +#include "constants/Enumerations.h" +#include "constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TExpType = si64; +using TQuantity = si32; +using TRmgTemplateZoneId = int; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.cpp b/lib/GameSettings.cpp index 3a2969704..15a593e1b 100644 --- a/lib/GameSettings.cpp +++ b/lib/GameSettings.cpp @@ -1,128 +1,128 @@ -/* - * GameSettings.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 "GameSettings.h" -#include "JsonNode.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool IGameSettings::getBoolean(EGameSettings option) const -{ - return getValue(option).Bool(); -} - -int64_t IGameSettings::getInteger(EGameSettings option) const -{ - return getValue(option).Integer(); -} - -double IGameSettings::getDouble(EGameSettings option) const -{ - return getValue(option).Float(); -} - -std::vector IGameSettings::getVector(EGameSettings option) const -{ - return getValue(option).convertTo>(); -} - -GameSettings::~GameSettings() = default; - -GameSettings::GameSettings() - : gameSettings(static_cast(EGameSettings::OPTIONS_COUNT)) -{ -} - -void GameSettings::load(const JsonNode & input) -{ - struct SettingOption - { - EGameSettings setting; - std::string group; - std::string key; - }; - - static const std::vector optionPath = { - {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, - {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, - {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" }, - {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" }, - {EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" }, - {EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" }, - {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" }, - {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap"}, - {EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" }, - {EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" }, - {EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" }, - {EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" }, - {EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" }, - {EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" }, - {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, - {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, - {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, - {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, - {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, - {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, - {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, - {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, - {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, - {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, - {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, - {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, - {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, - {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, - {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, - {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, - {EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, - {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, - {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, - {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, - {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, - {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, - {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, - {EGameSettings::TEXTS_FACTION, "textData", "faction" }, - {EGameSettings::TEXTS_HERO, "textData", "hero" }, - {EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" }, - {EGameSettings::TEXTS_OBJECT, "textData", "object" }, - {EGameSettings::TEXTS_RIVER, "textData", "river" }, - {EGameSettings::TEXTS_ROAD, "textData", "road" }, - {EGameSettings::TEXTS_SPELL, "textData", "spell" }, - {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, - {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, - {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, - {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, - {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, - {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, - }; - - for(const auto & option : optionPath) - { - const JsonNode & optionValue = input[option.group][option.key]; - size_t index = static_cast(option.setting); - - if(optionValue.isNull()) - continue; - - JsonUtils::mergeCopy(gameSettings[index], optionValue); - } -} - -const JsonNode & GameSettings::getValue(EGameSettings option) const -{ - assert(option < EGameSettings::OPTIONS_COUNT); - auto index = static_cast(option); - - assert(!gameSettings[index].isNull()); - return gameSettings[index]; -} - -VCMI_LIB_NAMESPACE_END +/* + * GameSettings.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 "GameSettings.h" +#include "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool IGameSettings::getBoolean(EGameSettings option) const +{ + return getValue(option).Bool(); +} + +int64_t IGameSettings::getInteger(EGameSettings option) const +{ + return getValue(option).Integer(); +} + +double IGameSettings::getDouble(EGameSettings option) const +{ + return getValue(option).Float(); +} + +std::vector IGameSettings::getVector(EGameSettings option) const +{ + return getValue(option).convertTo>(); +} + +GameSettings::~GameSettings() = default; + +GameSettings::GameSettings() + : gameSettings(static_cast(EGameSettings::OPTIONS_COUNT)) +{ +} + +void GameSettings::load(const JsonNode & input) +{ + struct SettingOption + { + EGameSettings setting; + std::string group; + std::string key; + }; + + static const std::vector optionPath = { + {EGameSettings::BONUSES_GLOBAL, "bonuses", "global" }, + {EGameSettings::BONUSES_PER_HERO, "bonuses", "perHero" }, + {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR, "combat", "attackPointDamageFactor" }, + {EGameSettings::COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, "combat", "attackPointDamageFactorCap" }, + {EGameSettings::COMBAT_BAD_LUCK_DICE, "combat", "badLuckDice" }, + {EGameSettings::COMBAT_BAD_MORALE_DICE, "combat", "badMoraleDice" }, + {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, "combat", "defensePointDamageFactor" }, + {EGameSettings::COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, "combat", "defensePointDamageFactorCap"}, + {EGameSettings::COMBAT_GOOD_LUCK_DICE, "combat", "goodLuckDice" }, + {EGameSettings::COMBAT_GOOD_MORALE_DICE, "combat", "goodMoraleDice" }, + {EGameSettings::COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, "combat", "oneHexTriggersObstacles" }, + {EGameSettings::CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, "creatures", "allowAllForDoubleMonth" }, + {EGameSettings::CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, "creatures", "allowRandomSpecialWeeks" }, + {EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE, "creatures", "dailyStackExperience" }, + {EGameSettings::CREATURES_WEEKLY_GROWTH_CAP, "creatures", "weeklyGrowthCap" }, + {EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT, "creatures", "weeklyGrowthPercent" }, + {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, "dwellings", "accumulateWhenNeutral" }, + {EGameSettings::DWELLINGS_ACCUMULATE_WHEN_OWNED, "dwellings", "accumulateWhenOwned" }, + {EGameSettings::DWELLINGS_MERGE_ON_RECRUIT, "dwellings", "mergeOnRecruit" }, + {EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP, "heroes", "perPlayerOnMapCap" }, + {EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP, "heroes", "perPlayerTotalCap" }, + {EGameSettings::HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, "heroes", "retreatOnWinWithoutTroops" }, + {EGameSettings::HEROES_STARTING_STACKS_CHANCES, "heroes", "startingStackChances" }, + {EGameSettings::HEROES_BACKPACK_CAP, "heroes", "backpackSize" }, + {EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA, "mapFormat", "restorationOfErathia" }, + {EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE, "mapFormat", "armageddonsBlade" }, + {EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH, "mapFormat", "shadowOfDeath" }, + {EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS, "mapFormat", "hornOfTheAbyss" }, + {EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS, "mapFormat", "inTheWakeOfGods" }, + {EGameSettings::MAP_FORMAT_JSON_VCMI, "mapFormat", "jsonVCMI" }, + {EGameSettings::MARKETS_BLACK_MARKET_RESTOCK_PERIOD, "markets", "blackMarketRestockPeriod" }, + {EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION, "banks", "showGuardsComposition" }, + {EGameSettings::MODULE_COMMANDERS, "modules", "commanders" }, + {EGameSettings::MODULE_STACK_ARTIFACT, "modules", "stackArtifact" }, + {EGameSettings::MODULE_STACK_EXPERIENCE, "modules", "stackExperience" }, + {EGameSettings::TEXTS_ARTIFACT, "textData", "artifact" }, + {EGameSettings::TEXTS_CREATURE, "textData", "creature" }, + {EGameSettings::TEXTS_FACTION, "textData", "faction" }, + {EGameSettings::TEXTS_HERO, "textData", "hero" }, + {EGameSettings::TEXTS_HERO_CLASS, "textData", "heroClass" }, + {EGameSettings::TEXTS_OBJECT, "textData", "object" }, + {EGameSettings::TEXTS_RIVER, "textData", "river" }, + {EGameSettings::TEXTS_ROAD, "textData", "road" }, + {EGameSettings::TEXTS_SPELL, "textData", "spell" }, + {EGameSettings::TEXTS_TERRAIN, "textData", "terrain" }, + {EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" }, + {EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" }, + {EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" }, + {EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" }, + {EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" }, + }; + + for(const auto & option : optionPath) + { + const JsonNode & optionValue = input[option.group][option.key]; + size_t index = static_cast(option.setting); + + if(optionValue.isNull()) + continue; + + JsonUtils::mergeCopy(gameSettings[index], optionValue); + } +} + +const JsonNode & GameSettings::getValue(EGameSettings option) const +{ + assert(option < EGameSettings::OPTIONS_COUNT); + auto index = static_cast(option); + + assert(!gameSettings[index].isNull()); + return gameSettings[index]; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/GameSettings.h b/lib/GameSettings.h index eae22cbda..750c9abab 100644 --- a/lib/GameSettings.h +++ b/lib/GameSettings.h @@ -1,104 +1,104 @@ -/* - * GameSettings.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; - -enum class EGameSettings -{ - BONUSES_GLOBAL, - BONUSES_PER_HERO, - COMBAT_ATTACK_POINT_DAMAGE_FACTOR, - COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, - COMBAT_BAD_LUCK_DICE, - COMBAT_BAD_MORALE_DICE, - COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, - COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, - COMBAT_GOOD_LUCK_DICE, - COMBAT_GOOD_MORALE_DICE, - CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, - CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, - CREATURES_DAILY_STACK_EXPERIENCE, - CREATURES_WEEKLY_GROWTH_CAP, - CREATURES_WEEKLY_GROWTH_PERCENT, - DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, - DWELLINGS_ACCUMULATE_WHEN_OWNED, - DWELLINGS_MERGE_ON_RECRUIT, - HEROES_PER_PLAYER_ON_MAP_CAP, - HEROES_PER_PLAYER_TOTAL_CAP, - HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, - HEROES_STARTING_STACKS_CHANCES, - HEROES_BACKPACK_CAP, - MARKETS_BLACK_MARKET_RESTOCK_PERIOD, - BANKS_SHOW_GUARDS_COMPOSITION, - MODULE_COMMANDERS, - MODULE_STACK_ARTIFACT, - MODULE_STACK_EXPERIENCE, - TEXTS_ARTIFACT, - TEXTS_CREATURE, - TEXTS_FACTION, - TEXTS_HERO, - TEXTS_HERO_CLASS, - TEXTS_OBJECT, - TEXTS_RIVER, - TEXTS_ROAD, - TEXTS_SPELL, - TEXTS_TERRAIN, - MAP_FORMAT_RESTORATION_OF_ERATHIA, - MAP_FORMAT_ARMAGEDDONS_BLADE, - MAP_FORMAT_SHADOW_OF_DEATH, - MAP_FORMAT_HORN_OF_THE_ABYSS, - MAP_FORMAT_JSON_VCMI, - MAP_FORMAT_IN_THE_WAKE_OF_GODS, - PATHFINDER_USE_BOAT, - PATHFINDER_USE_MONOLITH_TWO_WAY, - PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, - PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, - PATHFINDER_USE_WHIRLPOOL, - TOWNS_BUILDINGS_PER_TURN_CAP, - TOWNS_STARTING_DWELLING_CHANCES, - COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, - - OPTIONS_COUNT -}; - -class DLL_LINKAGE IGameSettings -{ -public: - virtual const JsonNode & getValue(EGameSettings option) const = 0; - virtual ~IGameSettings() = default; - - bool getBoolean(EGameSettings option) const; - int64_t getInteger(EGameSettings option) const; - double getDouble(EGameSettings option) const; - std::vector getVector(EGameSettings option) const; -}; - -class DLL_LINKAGE GameSettings final : public IGameSettings, boost::noncopyable -{ - std::vector gameSettings; - -public: - GameSettings(); - ~GameSettings(); - - void load(const JsonNode & input); - const JsonNode & getValue(EGameSettings option) const override; - - template - void serialize(Handler & h, const int version) - { - h & gameSettings; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * GameSettings.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; + +enum class EGameSettings +{ + BONUSES_GLOBAL, + BONUSES_PER_HERO, + COMBAT_ATTACK_POINT_DAMAGE_FACTOR, + COMBAT_ATTACK_POINT_DAMAGE_FACTOR_CAP, + COMBAT_BAD_LUCK_DICE, + COMBAT_BAD_MORALE_DICE, + COMBAT_DEFENSE_POINT_DAMAGE_FACTOR, + COMBAT_DEFENSE_POINT_DAMAGE_FACTOR_CAP, + COMBAT_GOOD_LUCK_DICE, + COMBAT_GOOD_MORALE_DICE, + CREATURES_ALLOW_ALL_FOR_DOUBLE_MONTH, + CREATURES_ALLOW_RANDOM_SPECIAL_WEEKS, + CREATURES_DAILY_STACK_EXPERIENCE, + CREATURES_WEEKLY_GROWTH_CAP, + CREATURES_WEEKLY_GROWTH_PERCENT, + DWELLINGS_ACCUMULATE_WHEN_NEUTRAL, + DWELLINGS_ACCUMULATE_WHEN_OWNED, + DWELLINGS_MERGE_ON_RECRUIT, + HEROES_PER_PLAYER_ON_MAP_CAP, + HEROES_PER_PLAYER_TOTAL_CAP, + HEROES_RETREAT_ON_WIN_WITHOUT_TROOPS, + HEROES_STARTING_STACKS_CHANCES, + HEROES_BACKPACK_CAP, + MARKETS_BLACK_MARKET_RESTOCK_PERIOD, + BANKS_SHOW_GUARDS_COMPOSITION, + MODULE_COMMANDERS, + MODULE_STACK_ARTIFACT, + MODULE_STACK_EXPERIENCE, + TEXTS_ARTIFACT, + TEXTS_CREATURE, + TEXTS_FACTION, + TEXTS_HERO, + TEXTS_HERO_CLASS, + TEXTS_OBJECT, + TEXTS_RIVER, + TEXTS_ROAD, + TEXTS_SPELL, + TEXTS_TERRAIN, + MAP_FORMAT_RESTORATION_OF_ERATHIA, + MAP_FORMAT_ARMAGEDDONS_BLADE, + MAP_FORMAT_SHADOW_OF_DEATH, + MAP_FORMAT_HORN_OF_THE_ABYSS, + MAP_FORMAT_JSON_VCMI, + MAP_FORMAT_IN_THE_WAKE_OF_GODS, + PATHFINDER_USE_BOAT, + PATHFINDER_USE_MONOLITH_TWO_WAY, + PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, + PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, + PATHFINDER_USE_WHIRLPOOL, + TOWNS_BUILDINGS_PER_TURN_CAP, + TOWNS_STARTING_DWELLING_CHANCES, + COMBAT_ONE_HEX_TRIGGERS_OBSTACLES, + + OPTIONS_COUNT +}; + +class DLL_LINKAGE IGameSettings +{ +public: + virtual const JsonNode & getValue(EGameSettings option) const = 0; + virtual ~IGameSettings() = default; + + bool getBoolean(EGameSettings option) const; + int64_t getInteger(EGameSettings option) const; + double getDouble(EGameSettings option) const; + std::vector getVector(EGameSettings option) const; +}; + +class DLL_LINKAGE GameSettings final : public IGameSettings, boost::noncopyable +{ + std::vector gameSettings; + +public: + GameSettings(); + ~GameSettings(); + + void load(const JsonNode & input); + const JsonNode & getValue(EGameSettings option) const override; + + template + void serialize(Handler & h, const int version) + { + h & gameSettings; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IBonusTypeHandler.h b/lib/IBonusTypeHandler.h index b38c56f3d..7604da02a 100644 --- a/lib/IBonusTypeHandler.h +++ b/lib/IBonusTypeHandler.h @@ -1,30 +1,30 @@ -/* - * IBonusTypeHandler.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 "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class IBonusBearer; -struct Bonus; - -///High level interface for BonusTypeHandler - -class DLL_LINKAGE IBonusTypeHandler -{ -public: - virtual ~IBonusTypeHandler() = default; - - virtual std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const = 0; - virtual ImagePath bonusToGraphics(const std::shared_ptr & bonus) const = 0; -}; - -VCMI_LIB_NAMESPACE_END +/* + * IBonusTypeHandler.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 "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IBonusBearer; +struct Bonus; + +///High level interface for BonusTypeHandler + +class DLL_LINKAGE IBonusTypeHandler +{ +public: + virtual ~IBonusTypeHandler() = default; + + virtual std::string bonusToString(const std::shared_ptr & bonus, const IBonusBearer * bearer, bool description) const = 0; + virtual ImagePath bonusToGraphics(const std::shared_ptr & bonus) const = 0; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 4efa55cab..4989a007a 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -1,295 +1,295 @@ -/* - * IGameCallback.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 "IGameCallback.h" - -#include "CHeroHandler.h" // for CHeroHandler -#include "spells/CSpellHandler.h"// for CSpell -#include "CSkillHandler.h"// for CSkill -#include "NetPacks.h" -#include "CBonusTypeHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "bonuses/CBonusSystemNode.h" -#include "bonuses/Limiters.h" -#include "bonuses/Propagators.h" -#include "bonuses/Updaters.h" - -#include "serializer/CSerializer.h" // for SAVEGAME_MAGIC -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" -#include "serializer/CLoadIntegrityValidator.h" -#include "rmg/CMapGenOptions.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "mapObjects/CObjectHandler.h" -#include "mapObjects/ObjectTemplate.h" -#include "campaign/CampaignState.h" -#include "StartInfo.h" -#include "gameState/CGameState.h" -#include "gameState/CGameStateCampaign.h" -#include "gameState/TavernHeroesPool.h" -#include "mapping/CMap.h" -#include "modding/CModHandler.h" -#include "modding/CModInfo.h" -#include "modding/IdentifierStorage.h" -#include "modding/CModVersion.h" -#include "CPlayerState.h" -#include "GameSettings.h" -#include "ScriptHandler.h" -#include "RoadHandler.h" -#include "RiverHandler.h" -#include "TerrainHandler.h" - -#include "serializer/Connection.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const -{ - std::vector floors; - floors.reserve(gs->map->levels()); - for(int b = 0; b < gs->map->levels(); ++b) - { - floors.push_back(b); - } - const TerrainTile * tinfo = nullptr; - for (auto zd : floors) - { - for (int xd = 0; xd < gs->map->width; xd++) - { - for (int yd = 0; yd < gs->map->height; yd++) - { - tinfo = getTile(int3 (xd,yd,zd)); - if (tinfo->terType->isLand() && tinfo->terType->isPassable() && !tinfo->blocked) //land and free - tiles.emplace_back(xd, yd, zd); - } - } - } -} - -void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, - const int3 & pos, - int radious, - std::optional player, - int mode, - int3::EDistanceFormula distanceFormula) const -{ - if(!!player && !player->isValidPlayer()) - { - logGlobal->error("Illegal call to getTilesInRange!"); - return; - } - if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map - getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE); - else - { - const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); - for (int xd = std::max(pos.x - radious , 0); xd <= std::min(pos.x + radious, gs->map->width - 1); xd++) - { - for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) - { - int3 tilePos(xd,yd,pos.z); - double distance = pos.dist(tilePos, distanceFormula); - - if(distance <= radious) - { - if(!player - || (mode == 1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) - || (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) - ) - tiles.insert(int3(xd,yd,pos.z)); - } - } - } - } -} - -void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const -{ - if(!!Player && !Player->isValidPlayer()) - { - logGlobal->error("Illegal call to getAllTiles !"); - return; - } - - std::vector floors; - if(level == -1) - { - for(int b = 0; b < gs->map->levels(); ++b) - { - floors.push_back(b); - } - } - else - floors.push_back(level); - - for(auto zd: floors) - { - for(int xd = 0; xd < gs->map->width; xd++) - { - for(int yd = 0; yd < gs->map->height; yd++) - { - bool isTileEligible = false; - - switch(tileFilterMode) - { - case MapTerrainFilterMode::NONE: - isTileEligible = true; - break; - case MapTerrainFilterMode::WATER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater(); - break; - case MapTerrainFilterMode::LAND: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand(); - break; - case MapTerrainFilterMode::LAND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible(); - break; - case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER: - isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible(); - break; - } - - if(isTileEligible) - tiles.insert(int3(xd, yd, zd)); - } - } - } -} - -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const -{ - for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]); - for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]); - - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]); -} - -void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) -{ - for (ui32 i = 0; i < gs->map->allowedSpells.size(); i++) //spellh size appears to be greater (?) - { - const spells::Spell * spell = SpellID(i).toSpell(); - - if (!isAllowed(0, spell->getIndex())) - continue; - - if (level.has_value() && spell->getLevel() != level) - continue; - - out.push_back(spell->getId()); - } -} - -CGameState * CPrivilegedInfoCallback::gameState() -{ - return gs; -} - -template -void CPrivilegedInfoCallback::loadCommonState(Loader & in) -{ - logGlobal->info("Loading lib part of game..."); - in.checkMagicBytes(SAVEGAME_MAGIC); - - CMapHeader dum; - StartInfo * si = nullptr; - - logGlobal->info("\tReading header"); - in.serializer & dum; - - logGlobal->info("\tReading options"); - in.serializer & si; - - logGlobal->info("\tReading handlers"); - in.serializer & *VLC; - - logGlobal->info("\tReading gamestate"); - in.serializer & gs; -} - -template -void CPrivilegedInfoCallback::saveCommonState(Saver & out) const -{ - logGlobal->info("Saving lib part of game..."); - out.putMagicBytes(SAVEGAME_MAGIC); - logGlobal->info("\tSaving header"); - out.serializer & static_cast(*gs->map); - logGlobal->info("\tSaving options"); - out.serializer & gs->scenarioOps; - logGlobal->info("\tSaving handlers"); - out.serializer & *VLC; - logGlobal->info("\tSaving gamestate"); - out.serializer & gs; -} - -// hardly memory usage for `-gdwarf-4` flag -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadIntegrityValidator &); -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadFile &); -template DLL_LINKAGE void CPrivilegedInfoCallback::saveCommonState(CSaveFile &) const; - -TerrainTile * CNonConstInfoCallback::getTile(const int3 & pos) -{ - if(!gs->map->isInTheMap(pos)) - return nullptr; - return &gs->map->getTile(pos); -} - -CGHeroInstance * CNonConstInfoCallback::getHero(const ObjectInstanceID & objid) -{ - return const_cast(CGameInfoCallback::getHero(objid)); -} - -CGTownInstance * CNonConstInfoCallback::getTown(const ObjectInstanceID & objid) -{ - return const_cast(CGameInfoCallback::getTown(objid)); -} - -TeamState * CNonConstInfoCallback::getTeam(const TeamID & teamID) -{ - return const_cast(CGameInfoCallback::getTeam(teamID)); -} - -TeamState * CNonConstInfoCallback::getPlayerTeam(const PlayerColor & color) -{ - return const_cast(CGameInfoCallback::getPlayerTeam(color)); -} - -PlayerState * CNonConstInfoCallback::getPlayerState(const PlayerColor & color, bool verbose) -{ - return const_cast(CGameInfoCallback::getPlayerState(color, verbose)); -} - -CArtifactInstance * CNonConstInfoCallback::getArtInstance(const ArtifactInstanceID & aid) -{ - return gs->map->artInstances.at(aid.num); -} - -CGObjectInstance * CNonConstInfoCallback::getObjInstance(const ObjectInstanceID & oid) -{ - return gs->map->objects.at(oid.num); -} - -CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID & oid) -{ - return dynamic_cast(getObjInstance(oid)); -} - -bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) -{ - //only server knows - logGlobal->error("isVisitCoveredByAnotherQuery call on client side"); - return false; -} - -VCMI_LIB_NAMESPACE_END +/* + * IGameCallback.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 "IGameCallback.h" + +#include "CHeroHandler.h" // for CHeroHandler +#include "spells/CSpellHandler.h"// for CSpell +#include "CSkillHandler.h"// for CSkill +#include "NetPacks.h" +#include "CBonusTypeHandler.h" +#include "BattleFieldHandler.h" +#include "ObstacleHandler.h" +#include "bonuses/CBonusSystemNode.h" +#include "bonuses/Limiters.h" +#include "bonuses/Propagators.h" +#include "bonuses/Updaters.h" + +#include "serializer/CSerializer.h" // for SAVEGAME_MAGIC +#include "serializer/BinaryDeserializer.h" +#include "serializer/BinarySerializer.h" +#include "serializer/CLoadIntegrityValidator.h" +#include "rmg/CMapGenOptions.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "mapObjects/CObjectHandler.h" +#include "mapObjects/ObjectTemplate.h" +#include "campaign/CampaignState.h" +#include "StartInfo.h" +#include "gameState/CGameState.h" +#include "gameState/CGameStateCampaign.h" +#include "gameState/TavernHeroesPool.h" +#include "mapping/CMap.h" +#include "modding/CModHandler.h" +#include "modding/CModInfo.h" +#include "modding/IdentifierStorage.h" +#include "modding/CModVersion.h" +#include "CPlayerState.h" +#include "GameSettings.h" +#include "ScriptHandler.h" +#include "RoadHandler.h" +#include "RiverHandler.h" +#include "TerrainHandler.h" + +#include "serializer/Connection.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CPrivilegedInfoCallback::getFreeTiles(std::vector & tiles) const +{ + std::vector floors; + floors.reserve(gs->map->levels()); + for(int b = 0; b < gs->map->levels(); ++b) + { + floors.push_back(b); + } + const TerrainTile * tinfo = nullptr; + for (auto zd : floors) + { + for (int xd = 0; xd < gs->map->width; xd++) + { + for (int yd = 0; yd < gs->map->height; yd++) + { + tinfo = getTile(int3 (xd,yd,zd)); + if (tinfo->terType->isLand() && tinfo->terType->isPassable() && !tinfo->blocked) //land and free + tiles.emplace_back(xd, yd, zd); + } + } + } +} + +void CPrivilegedInfoCallback::getTilesInRange(std::unordered_set & tiles, + const int3 & pos, + int radious, + std::optional player, + int mode, + int3::EDistanceFormula distanceFormula) const +{ + if(!!player && !player->isValidPlayer()) + { + logGlobal->error("Illegal call to getTilesInRange!"); + return; + } + if(radious == CBuilding::HEIGHT_SKYSHIP) //reveal entire map + getAllTiles (tiles, player, -1, MapTerrainFilterMode::NONE); + else + { + const TeamState * team = !player ? nullptr : gs->getPlayerTeam(*player); + for (int xd = std::max(pos.x - radious , 0); xd <= std::min(pos.x + radious, gs->map->width - 1); xd++) + { + for (int yd = std::max(pos.y - radious, 0); yd <= std::min(pos.y + radious, gs->map->height - 1); yd++) + { + int3 tilePos(xd,yd,pos.z); + double distance = pos.dist(tilePos, distanceFormula); + + if(distance <= radious) + { + if(!player + || (mode == 1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 0) + || (mode == -1 && (*team->fogOfWarMap)[pos.z][xd][yd] == 1) + ) + tiles.insert(int3(xd,yd,pos.z)); + } + } + } + } +} + +void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std::optional Player, int level, MapTerrainFilterMode tileFilterMode) const +{ + if(!!Player && !Player->isValidPlayer()) + { + logGlobal->error("Illegal call to getAllTiles !"); + return; + } + + std::vector floors; + if(level == -1) + { + for(int b = 0; b < gs->map->levels(); ++b) + { + floors.push_back(b); + } + } + else + floors.push_back(level); + + for(auto zd: floors) + { + for(int xd = 0; xd < gs->map->width; xd++) + { + for(int yd = 0; yd < gs->map->height; yd++) + { + bool isTileEligible = false; + + switch(tileFilterMode) + { + case MapTerrainFilterMode::NONE: + isTileEligible = true; + break; + case MapTerrainFilterMode::WATER: + isTileEligible = getTile(int3(xd, yd, zd))->terType->isWater(); + break; + case MapTerrainFilterMode::LAND: + isTileEligible = getTile(int3(xd, yd, zd))->terType->isLand(); + break; + case MapTerrainFilterMode::LAND_CARTOGRAPHER: + isTileEligible = getTile(int3(xd, yd, zd))->terType->isSurfaceCartographerCompatible(); + break; + case MapTerrainFilterMode::UNDERGROUND_CARTOGRAPHER: + isTileEligible = getTile(int3(xd, yd, zd))->terType->isUndergroundCartographerCompatible(); + break; + } + + if(isTileEligible) + tiles.insert(int3(xd, yd, zd)); + } + } + } +} + +void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const +{ + for (int j = 0; j < 3 ; j++) + out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]); + for (int j = 0; j < 3 ; j++) + out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]); + + out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]); +} + +void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) +{ + for (ui32 i = 0; i < gs->map->allowedSpells.size(); i++) //spellh size appears to be greater (?) + { + const spells::Spell * spell = SpellID(i).toSpell(); + + if (!isAllowed(0, spell->getIndex())) + continue; + + if (level.has_value() && spell->getLevel() != level) + continue; + + out.push_back(spell->getId()); + } +} + +CGameState * CPrivilegedInfoCallback::gameState() +{ + return gs; +} + +template +void CPrivilegedInfoCallback::loadCommonState(Loader & in) +{ + logGlobal->info("Loading lib part of game..."); + in.checkMagicBytes(SAVEGAME_MAGIC); + + CMapHeader dum; + StartInfo * si = nullptr; + + logGlobal->info("\tReading header"); + in.serializer & dum; + + logGlobal->info("\tReading options"); + in.serializer & si; + + logGlobal->info("\tReading handlers"); + in.serializer & *VLC; + + logGlobal->info("\tReading gamestate"); + in.serializer & gs; +} + +template +void CPrivilegedInfoCallback::saveCommonState(Saver & out) const +{ + logGlobal->info("Saving lib part of game..."); + out.putMagicBytes(SAVEGAME_MAGIC); + logGlobal->info("\tSaving header"); + out.serializer & static_cast(*gs->map); + logGlobal->info("\tSaving options"); + out.serializer & gs->scenarioOps; + logGlobal->info("\tSaving handlers"); + out.serializer & *VLC; + logGlobal->info("\tSaving gamestate"); + out.serializer & gs; +} + +// hardly memory usage for `-gdwarf-4` flag +template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadIntegrityValidator &); +template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadFile &); +template DLL_LINKAGE void CPrivilegedInfoCallback::saveCommonState(CSaveFile &) const; + +TerrainTile * CNonConstInfoCallback::getTile(const int3 & pos) +{ + if(!gs->map->isInTheMap(pos)) + return nullptr; + return &gs->map->getTile(pos); +} + +CGHeroInstance * CNonConstInfoCallback::getHero(const ObjectInstanceID & objid) +{ + return const_cast(CGameInfoCallback::getHero(objid)); +} + +CGTownInstance * CNonConstInfoCallback::getTown(const ObjectInstanceID & objid) +{ + return const_cast(CGameInfoCallback::getTown(objid)); +} + +TeamState * CNonConstInfoCallback::getTeam(const TeamID & teamID) +{ + return const_cast(CGameInfoCallback::getTeam(teamID)); +} + +TeamState * CNonConstInfoCallback::getPlayerTeam(const PlayerColor & color) +{ + return const_cast(CGameInfoCallback::getPlayerTeam(color)); +} + +PlayerState * CNonConstInfoCallback::getPlayerState(const PlayerColor & color, bool verbose) +{ + return const_cast(CGameInfoCallback::getPlayerState(color, verbose)); +} + +CArtifactInstance * CNonConstInfoCallback::getArtInstance(const ArtifactInstanceID & aid) +{ + return gs->map->artInstances.at(aid.num); +} + +CGObjectInstance * CNonConstInfoCallback::getObjInstance(const ObjectInstanceID & oid) +{ + return gs->map->objects.at(oid.num); +} + +CArmedInstance * CNonConstInfoCallback::getArmyInstance(const ObjectInstanceID & oid) +{ + return dynamic_cast(getObjInstance(oid)); +} + +bool IGameCallback::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) +{ + //only server knows + logGlobal->error("isVisitCoveredByAnotherQuery call on client side"); + return false; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index 03e96dbeb..6fe19a363 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -1,189 +1,189 @@ -/* - * IGameCallback.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 - -#include "CGameInfoCallback.h" // for CGameInfoCallback -#include "CRandomGenerator.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct SetMovePoints; -struct GiveBonus; -struct BlockingDialog; -struct TeleportDialog; -struct StackLocation; -struct ArtifactLocation; -class CCreatureSet; -class CStackBasicDescriptor; -class CGCreature; -enum class EOpenWindowMode : uint8_t; - -namespace spells -{ - class Caster; -} - -#if SCRIPTING_ENABLED -namespace scripting -{ - class Pool; -} -#endif - - -class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback -{ -public: - enum class MapTerrainFilterMode - { - NONE = 0, - LAND = 1, - WATER = 2, - LAND_CARTOGRAPHER = 3, - UNDERGROUND_CARTOGRAPHER = 4 - }; - - CGameState *gameState(); - - //used for random spawns - void getFreeTiles(std::vector &tiles) const; - - //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed - void getTilesInRange(std::unordered_set & tiles, - const int3 & pos, - int radious, - std::optional player = std::optional(), - int mode = 0, - int3::EDistanceFormula formula = int3::DIST_2D) const; - - //returns all tiles on given level (-1 - both levels, otherwise number of level) - void getAllTiles(std::unordered_set &tiles, std::optional player = std::optional(), - int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const; - - //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant - void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; - void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); - - template - void saveCommonState(Saver &out) const; //stores GS and VLC - - template - void loadCommonState(Loader &in); //loads GS and VLC -}; - -class DLL_LINKAGE IGameEventCallback -{ -public: - virtual void setObjProperty(ObjectInstanceID objid, int prop, si64 val) = 0; - - virtual void showInfoDialog(InfoWindow * iw) = 0; - virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; - - virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; - virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) = 0; - virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; - virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; - virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; - virtual void showBlockingDialog(BlockingDialog *iw) =0; - virtual void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) =0; //cb will be called when player closes garrison window - virtual void showTeleportDialog(TeleportDialog *iw) =0; - virtual void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) = 0; - virtual void giveResource(PlayerColor player, GameResID which, int val)=0; - virtual void giveResources(PlayerColor player, TResources resources)=0; - - virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0; - virtual void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) =0; - virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0; - virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0; - virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack - virtual bool eraseStack(const StackLocation &sl, bool forceRemoval = false) =0; - virtual bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) =0; - virtual bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) =0; //makes new stack or increases count of already existing - virtual void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) =0; //merges army from src do dst or opens a garrison window - virtual bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) = 0; - - virtual void removeAfterVisit(const CGObjectInstance *object) = 0; //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! - - virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) = 0; - virtual bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) = 0; - virtual void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) = 0; - virtual void removeArtifact(const ArtifactLocation &al) = 0; - virtual bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) = 0; - - virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; - virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0; - virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; - virtual void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr)=0; //use hero=nullptr for no hero - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false)=0; //if any of armies is hero, hero will be used - virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle - virtual bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; - virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; - virtual void giveHeroBonus(GiveBonus * bonus)=0; - virtual void setMovePoints(SetMovePoints * smp)=0; - virtual void setManaPoints(ObjectInstanceID hid, int val)=0; - virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0; - virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; - virtual void sendAndApply(CPackForClient * pack) = 0; - virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map - virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; - virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; - - virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; -}; - -class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback -{ -public: - //keep const version of callback accessible - using CGameInfoCallback::getPlayerState; - using CGameInfoCallback::getTeam; - using CGameInfoCallback::getPlayerTeam; - using CGameInfoCallback::getHero; - using CGameInfoCallback::getTown; - using CGameInfoCallback::getTile; - using CGameInfoCallback::getArtInstance; - using CGameInfoCallback::getObjInstance; - - PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true); - TeamState * getTeam(const TeamID & teamID); //get team by team ID - TeamState * getPlayerTeam(const PlayerColor & color); // get team by player color - CGHeroInstance * getHero(const ObjectInstanceID & objid); - CGTownInstance * getTown(const ObjectInstanceID & objid); - TerrainTile * getTile(const int3 & pos); - CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid); - CGObjectInstance * getObjInstance(const ObjectInstanceID & oid); - CArmedInstance * getArmyInstance(const ObjectInstanceID & oid); - - virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; -}; - -/// Interface class for handling general game logic and actions -class DLL_LINKAGE IGameCallback : public CPrivilegedInfoCallback, public IGameEventCallback -{ -public: - virtual ~IGameCallback(){}; - -#if SCRIPTING_ENABLED - virtual scripting::Pool * getGlobalContextPool() const = 0; -#endif - - //get info - virtual bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero); - - friend struct CPack; - friend struct CPackForClient; - friend struct CPackForServer; -}; - - -VCMI_LIB_NAMESPACE_END +/* + * IGameCallback.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 + +#include "CGameInfoCallback.h" // for CGameInfoCallback +#include "CRandomGenerator.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct SetMovePoints; +struct GiveBonus; +struct BlockingDialog; +struct TeleportDialog; +struct StackLocation; +struct ArtifactLocation; +class CCreatureSet; +class CStackBasicDescriptor; +class CGCreature; +enum class EOpenWindowMode : uint8_t; + +namespace spells +{ + class Caster; +} + +#if SCRIPTING_ENABLED +namespace scripting +{ + class Pool; +} +#endif + + +class DLL_LINKAGE CPrivilegedInfoCallback : public CGameInfoCallback +{ +public: + enum class MapTerrainFilterMode + { + NONE = 0, + LAND = 1, + WATER = 2, + LAND_CARTOGRAPHER = 3, + UNDERGROUND_CARTOGRAPHER = 4 + }; + + CGameState *gameState(); + + //used for random spawns + void getFreeTiles(std::vector &tiles) const; + + //mode 1 - only unrevealed tiles; mode 0 - all, mode -1 - only revealed + void getTilesInRange(std::unordered_set & tiles, + const int3 & pos, + int radious, + std::optional player = std::optional(), + int mode = 0, + int3::EDistanceFormula formula = int3::DIST_2D) const; + + //returns all tiles on given level (-1 - both levels, otherwise number of level) + void getAllTiles(std::unordered_set &tiles, std::optional player = std::optional(), + int level = -1, MapTerrainFilterMode tileFilterMode = MapTerrainFilterMode::NONE) const; + + //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant + void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; + void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); + + template + void saveCommonState(Saver &out) const; //stores GS and VLC + + template + void loadCommonState(Loader &in); //loads GS and VLC +}; + +class DLL_LINKAGE IGameEventCallback +{ +public: + virtual void setObjProperty(ObjectInstanceID objid, int prop, si64 val) = 0; + + virtual void showInfoDialog(InfoWindow * iw) = 0; + virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; + + virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; + virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; + virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) = 0; + virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; + virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; + virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; + virtual void showBlockingDialog(BlockingDialog *iw) =0; + virtual void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) =0; //cb will be called when player closes garrison window + virtual void showTeleportDialog(TeleportDialog *iw) =0; + virtual void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) = 0; + virtual void giveResource(PlayerColor player, GameResID which, int val)=0; + virtual void giveResources(PlayerColor player, TResources resources)=0; + + virtual void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) =0; + virtual void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) =0; + virtual bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) =0; + virtual bool changeStackType(const StackLocation &sl, const CCreature *c) =0; + virtual bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count = -1) =0; //count -1 => moves whole stack + virtual bool eraseStack(const StackLocation &sl, bool forceRemoval = false) =0; + virtual bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) =0; + virtual bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) =0; //makes new stack or increases count of already existing + virtual void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) =0; //merges army from src do dst or opens a garrison window + virtual bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count) = 0; + + virtual void removeAfterVisit(const CGObjectInstance *object) = 0; //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! + + virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) = 0; + virtual bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) = 0; + virtual void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) = 0; + virtual void removeArtifact(const ArtifactLocation &al) = 0; + virtual bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) = 0; + + virtual void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; + virtual void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero)=0; + virtual void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero)=0; + virtual void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr)=0; //use hero=nullptr for no hero + virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false)=0; //if any of armies is hero, hero will be used + virtual void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false)=0; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + virtual bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL)=0; + virtual bool swapGarrisonOnSiege(ObjectInstanceID tid)=0; + virtual void giveHeroBonus(GiveBonus * bonus)=0; + virtual void setMovePoints(SetMovePoints * smp)=0; + virtual void setManaPoints(ObjectInstanceID hid, int val)=0; + virtual void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) = 0; + virtual void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator)=0; + virtual void sendAndApply(CPackForClient * pack) = 0; + virtual void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2)=0; //when two heroes meet on adventure map + virtual void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) = 0; + virtual void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) = 0; + + virtual void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) = 0; +}; + +class DLL_LINKAGE CNonConstInfoCallback : public CPrivilegedInfoCallback +{ +public: + //keep const version of callback accessible + using CGameInfoCallback::getPlayerState; + using CGameInfoCallback::getTeam; + using CGameInfoCallback::getPlayerTeam; + using CGameInfoCallback::getHero; + using CGameInfoCallback::getTown; + using CGameInfoCallback::getTile; + using CGameInfoCallback::getArtInstance; + using CGameInfoCallback::getObjInstance; + + PlayerState * getPlayerState(const PlayerColor & color, bool verbose = true); + TeamState * getTeam(const TeamID & teamID); //get team by team ID + TeamState * getPlayerTeam(const PlayerColor & color); // get team by player color + CGHeroInstance * getHero(const ObjectInstanceID & objid); + CGTownInstance * getTown(const ObjectInstanceID & objid); + TerrainTile * getTile(const int3 & pos); + CArtifactInstance * getArtInstance(const ArtifactInstanceID & aid); + CGObjectInstance * getObjInstance(const ObjectInstanceID & oid); + CArmedInstance * getArmyInstance(const ObjectInstanceID & oid); + + virtual void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) = 0; +}; + +/// Interface class for handling general game logic and actions +class DLL_LINKAGE IGameCallback : public CPrivilegedInfoCallback, public IGameEventCallback +{ +public: + virtual ~IGameCallback(){}; + +#if SCRIPTING_ENABLED + virtual scripting::Pool * getGlobalContextPool() const = 0; +#endif + + //get info + virtual bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero); + + friend struct CPack; + friend struct CPackForClient; + friend struct CPackForServer; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 5c4acd5bd..959e28f8d 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -1,143 +1,143 @@ -/* - * IGameEventsReceiver.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 "NetPacksBase.h" -#include "battle/BattleHex.h" -#include "GameConstants.h" -#include "int3.h" - -class CCallback; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; -class CCreature; -class CArmedInstance; -struct StackLocation; -struct TryMoveHero; -struct ArtifactLocation; -class CGHeroInstance; -class IShipyard; -class CGDwelling; -struct Component; -class CStackInstance; -class CGBlackMarket; -class CGObjectInstance; -struct Bonus; -class IMarket; -struct SetObjectProperty; -struct PackageApplied; -class BattleAction; -struct BattleStackAttacked; -struct BattleResult; -struct BattleSpellCast; -struct CatapultAttack; -class CStack; -class CCreatureSet; -struct BattleAttack; -struct SetStackEffect; -struct BattleTriggerEffect; -struct CObstacleInstance; -struct CPackForServer; -class EVictoryLossCheckResult; -class MetaString; -class ObstacleChanges; -class UnitChanges; - -class DLL_LINKAGE IBattleEventsReceiver -{ -public: - virtual void actionFinished(const BattleID & battleID, const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero - virtual void actionStarted(const BattleID & battleID, const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero - virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack - virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) - virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){}; - virtual void battleNewRoundFirst(const BattleID & battleID){}; //called at the beginning of each turn before changes are applied; - virtual void battleNewRound(const BattleID & battleID){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn - virtual void battleLogMessage(const BattleID & battleID, const std::vector & lines){}; - virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport){}; - virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){}; - virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse){};//called when a specific effect is set to stacks - virtual void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte){}; //called for various one-shot effects - virtual void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start - virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right - virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units){}; - virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles){}; - virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca){}; //called when catapult makes an attack - virtual void battleGateStateChanged(const BattleID & battleID, const EGateState state){}; -}; - -class DLL_LINKAGE IGameEventsReceiver -{ -public: - virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what){}; //what: 1 - built, 2 - demolished - - virtual void battleResultsApplied(){}; //called when all effects of last battle are applied - - virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2){}; - - //artifacts operations - virtual void artifactPut(const ArtifactLocation &al){}; - virtual void artifactRemoved(const ArtifactLocation &al){}; - virtual void artifactAssembled(const ArtifactLocation &al){}; - virtual void artifactDisassembled(const ArtifactLocation &al){}; - virtual void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst){}; - virtual void bulkArtMovementStart(size_t numOfArts) {}; - virtual void askToAssembleArtifact(const ArtifactLocation & dst) {}; - - virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start){}; - virtual void heroCreated(const CGHeroInstance*){}; - virtual void heroInGarrisonChange(const CGTownInstance *town){}; - virtual void heroMoved(const TryMoveHero & details, bool verbose = true){}; - virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val){}; - virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val){}; - virtual void heroManaPointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after spell casts - virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement - virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){}; - virtual void receivedResource(){}; - virtual void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID){}; - virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID){} - virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can buid, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water - - virtual void showPuzzleMap(){}; - virtual void viewWorldMap(){}; - virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; - virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; - virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor){}; - virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; - virtual void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) {}; - virtual void showQuestLog(){}; - virtual void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID){}; //called when a hero casts a spell - virtual void tileHidden(const std::unordered_set &pos){}; - virtual void tileRevealed(const std::unordered_set &pos){}; - virtual void newObject(const CGObjectInstance * obj){}; //eg. ship built in shipyard - virtual void availableArtifactsChanged(const CGBlackMarket *bm = nullptr){}; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) - virtual void centerView (int3 pos, int focusTime){}; - virtual void availableCreaturesChanged(const CGDwelling *town){}; - virtual void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it - virtual void playerBonusChanged(const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it - virtual void requestSent(const CPackForServer *pack, int requestID){}; - virtual void requestRealized(PackageApplied *pa){}; - virtual void beforeObjectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged - virtual void objectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged - virtual void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator){}; //eg. collected resource, picked artifact, beaten hero - virtual void objectRemovedAfter(){}; //eg. collected resource, picked artifact, beaten hero - virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle - virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game - virtual void playerStartsTurn(PlayerColor player){}; - virtual void playerEndsTurn(PlayerColor player){}; - - //TODO shouldn't be moved down the tree? - virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){}; -}; - -VCMI_LIB_NAMESPACE_END +/* + * IGameEventsReceiver.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 "NetPacksBase.h" +#include "battle/BattleHex.h" +#include "GameConstants.h" +#include "int3.h" + +class CCallback; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; +class CCreature; +class CArmedInstance; +struct StackLocation; +struct TryMoveHero; +struct ArtifactLocation; +class CGHeroInstance; +class IShipyard; +class CGDwelling; +struct Component; +class CStackInstance; +class CGBlackMarket; +class CGObjectInstance; +struct Bonus; +class IMarket; +struct SetObjectProperty; +struct PackageApplied; +class BattleAction; +struct BattleStackAttacked; +struct BattleResult; +struct BattleSpellCast; +struct CatapultAttack; +class CStack; +class CCreatureSet; +struct BattleAttack; +struct SetStackEffect; +struct BattleTriggerEffect; +struct CObstacleInstance; +struct CPackForServer; +class EVictoryLossCheckResult; +class MetaString; +class ObstacleChanges; +class UnitChanges; + +class DLL_LINKAGE IBattleEventsReceiver +{ +public: + virtual void actionFinished(const BattleID & battleID, const BattleAction &action){};//occurs AFTER every action taken by any stack or by the hero + virtual void actionStarted(const BattleID & battleID, const BattleAction &action){};//occurs BEFORE every action taken by any stack or by the hero + virtual void battleAttack(const BattleID & battleID, const BattleAttack *ba){}; //called when stack is performing attack + virtual void battleStacksAttacked(const BattleID & battleID, const std::vector & bsa, bool ranged){}; //called when stack receives damage (after battleAttack()) + virtual void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID){}; + virtual void battleNewRoundFirst(const BattleID & battleID){}; //called at the beginning of each turn before changes are applied; + virtual void battleNewRound(const BattleID & battleID){}; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn + virtual void battleLogMessage(const BattleID & battleID, const std::vector & lines){}; + virtual void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector dest, int distance, bool teleport){}; + virtual void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc){}; + virtual void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse){};//called when a specific effect is set to stacks + virtual void battleTriggerEffect(const BattleID & battleID, const BattleTriggerEffect & bte){}; //called for various one-shot effects + virtual void battleStartBefore(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start + virtual void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right + virtual void battleUnitsChanged(const BattleID & battleID, const std::vector & units){}; + virtual void battleObstaclesChanged(const BattleID & battleID, const std::vector & obstacles){}; + virtual void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca){}; //called when catapult makes an attack + virtual void battleGateStateChanged(const BattleID & battleID, const EGateState state){}; +}; + +class DLL_LINKAGE IGameEventsReceiver +{ +public: + virtual void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what){}; //what: 1 - built, 2 - demolished + + virtual void battleResultsApplied(){}; //called when all effects of last battle are applied + + virtual void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2){}; + + //artifacts operations + virtual void artifactPut(const ArtifactLocation &al){}; + virtual void artifactRemoved(const ArtifactLocation &al){}; + virtual void artifactAssembled(const ArtifactLocation &al){}; + virtual void artifactDisassembled(const ArtifactLocation &al){}; + virtual void artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst){}; + virtual void bulkArtMovementStart(size_t numOfArts) {}; + virtual void askToAssembleArtifact(const ArtifactLocation & dst) {}; + + virtual void heroVisit(const CGHeroInstance *visitor, const CGObjectInstance *visitedObj, bool start){}; + virtual void heroCreated(const CGHeroInstance*){}; + virtual void heroInGarrisonChange(const CGTownInstance *town){}; + virtual void heroMoved(const TryMoveHero & details, bool verbose = true){}; + virtual void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val){}; + virtual void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val){}; + virtual void heroManaPointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after spell casts + virtual void heroMovePointsChanged(const CGHeroInstance * hero){} //not called at the beginning of turn and after movement + virtual void heroVisitsTown(const CGHeroInstance* hero, const CGTownInstance * town){}; + virtual void receivedResource(){}; + virtual void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector & components, int soundID){}; + virtual void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level, QueryID queryID){} + virtual void showShipyardDialog(const IShipyard *obj){} //obj may be town or shipyard; state: 0 - can buid, 1 - lack of resources, 2 - dest tile is blocked, 3 - no water + + virtual void showPuzzleMap(){}; + virtual void viewWorldMap(){}; + virtual void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; + virtual void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID){}; + virtual void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor){}; + virtual void showThievesGuildWindow (const CGObjectInstance * obj){}; + virtual void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) {}; + virtual void showQuestLog(){}; + virtual void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID){}; //called when a hero casts a spell + virtual void tileHidden(const std::unordered_set &pos){}; + virtual void tileRevealed(const std::unordered_set &pos){}; + virtual void newObject(const CGObjectInstance * obj){}; //eg. ship built in shipyard + virtual void availableArtifactsChanged(const CGBlackMarket *bm = nullptr){}; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns) + virtual void centerView (int3 pos, int focusTime){}; + virtual void availableCreaturesChanged(const CGDwelling *town){}; + virtual void heroBonusChanged(const CGHeroInstance *hero, const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it + virtual void playerBonusChanged(const Bonus &bonus, bool gain){};//if gain hero received bonus, else he lost it + virtual void requestSent(const CPackForServer *pack, int requestID){}; + virtual void requestRealized(PackageApplied *pa){}; + virtual void beforeObjectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged + virtual void objectPropertyChanged(const SetObjectProperty * sop){}; //eg. mine has been flagged + virtual void objectRemoved(const CGObjectInstance *obj, const PlayerColor & initiator){}; //eg. collected resource, picked artifact, beaten hero + virtual void objectRemovedAfter(){}; //eg. collected resource, picked artifact, beaten hero + virtual void playerBlocked(int reason, bool start){}; //reason: 0 - upcoming battle + virtual void gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult) {}; //player lost or won the game + virtual void playerStartsTurn(PlayerColor player){}; + virtual void playerEndsTurn(PlayerColor player){}; + + //TODO shouldn't be moved down the tree? + virtual void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID){}; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonDetail.cpp b/lib/JsonDetail.cpp index 9a873b077..8eaee0a25 100644 --- a/lib/JsonDetail.cpp +++ b/lib/JsonDetail.cpp @@ -1,1268 +1,1268 @@ -/* - * JsonDetail.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 "JsonDetail.h" - -#include "VCMI_Lib.h" -#include "TextOperations.h" - -#include "filesystem/Filesystem.h" -#include "modding/ModScope.h" -#include "modding/CModHandler.h" -#include "ScopeGuard.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static const JsonNode nullNode; - -template -void JsonWriter::writeContainer(Iterator begin, Iterator end) -{ - if (begin == end) - return; - - prefix += '\t'; - - writeEntry(begin++); - - while (begin != end) - { - out << (compactMode ? ", " : ",\n"); - writeEntry(begin++); - } - - out << (compactMode ? "" : "\n"); - prefix.resize(prefix.size()-1); -} - -void JsonWriter::writeEntry(JsonMap::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->second.meta.empty()) - out << prefix << " // " << entry->second.meta << "\n"; - if(!entry->second.flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->second.flags, ", ") << "\n"; - out << prefix; - } - writeString(entry->first); - out << " : "; - writeNode(entry->second); -} - -void JsonWriter::writeEntry(JsonVector::const_iterator entry) -{ - if(!compactMode) - { - if (!entry->meta.empty()) - out << prefix << " // " << entry->meta << "\n"; - if(!entry->flags.empty()) - out << prefix << " // flags: " << boost::algorithm::join(entry->flags, ", ") << "\n"; - out << prefix; - } - writeNode(*entry); -} - -void JsonWriter::writeString(const std::string &string) -{ - static const std::string escaped = "\"\\\b\f\n\r\t/"; - - static const std::array escaped_code = {'\"', '\\', 'b', 'f', 'n', 'r', 't', '/'}; - - out <<'\"'; - size_t pos = 0; - size_t start = 0; - for (; poswarn("File %s is not a valid JSON file!", fileName); - logMod->warn(errors); - } - return root; -} - -bool JsonParser::isValid() -{ - return errors.empty(); -} - -bool JsonParser::extractSeparator() -{ - if (!extractWhitespace()) - return false; - - if ( input[pos] !=':') - return error("Separator expected"); - - pos++; - return true; -} - -bool JsonParser::extractValue(JsonNode &node) -{ - if (!extractWhitespace()) - return false; - - switch (input[pos]) - { - case '\"': return extractString(node); - case 'n' : return extractNull(node); - case 't' : return extractTrue(node); - case 'f' : return extractFalse(node); - case '{' : return extractStruct(node); - case '[' : return extractArray(node); - case '-' : return extractFloat(node); - default: - { - if (input[pos] >= '0' && input[pos] <= '9') - return extractFloat(node); - return error("Value expected!"); - } - } -} - -bool JsonParser::extractWhitespace(bool verbose) -{ - while (true) - { - while(pos < input.size() && static_cast(input[pos]) <= ' ') - { - if (input[pos] == '\n') - { - lineCount++; - lineStart = pos+1; - } - pos++; - } - if (pos >= input.size() || input[pos] != '/') - break; - - pos++; - if (pos == input.size()) - break; - if (input[pos] == '/') - pos++; - else - error("Comments must consist from two slashes!", true); - - while (pos < input.size() && input[pos] != '\n') - pos++; - } - - if (pos >= input.size() && verbose) - return error("Unexpected end of file!"); - return true; -} - -bool JsonParser::extractEscaping(std::string &str) -{ - switch(input[pos]) - { - break; case '\"': str += '\"'; - break; case '\\': str += '\\'; - break; case 'b': str += '\b'; - break; case 'f': str += '\f'; - break; case 'n': str += '\n'; - break; case 'r': str += '\r'; - break; case 't': str += '\t'; - break; case '/': str += '/'; - break; default: return error("Unknown escape sequence!", true); - } - return true; -} - -bool JsonParser::extractString(std::string &str) -{ - if (input[pos] != '\"') - return error("String expected!"); - pos++; - - size_t first = pos; - - while (pos != input.size()) - { - if (input[pos] == '\"') // Correct end of string - { - str.append( &input[first], pos-first); - pos++; - return true; - } - if (input[pos] == '\\') // Escaping - { - str.append( &input[first], pos-first); - pos++; - if (pos == input.size()) - break; - extractEscaping(str); - first = pos + 1; - } - if (input[pos] == '\n') // end-of-line - { - str.append( &input[first], pos-first); - return error("Closing quote not found!", true); - } - if(static_cast(input[pos]) < ' ') // control character - { - str.append( &input[first], pos-first); - first = pos+1; - error("Illegal character in the string!", true); - } - pos++; - } - return error("Unterminated string!"); -} - -bool JsonParser::extractString(JsonNode &node) -{ - std::string str; - if (!extractString(str)) - return false; - - node.setType(JsonNode::JsonType::DATA_STRING); - node.String() = str; - return true; -} - -bool JsonParser::extractLiteral(const std::string &literal) -{ - if (literal.compare(0, literal.size(), &input[pos], literal.size()) != 0) - { - while (pos < input.size() && ((input[pos]>'a' && input[pos]<'z') - || (input[pos]>'A' && input[pos]<'Z'))) - pos++; - return error("Unknown literal found", true); - } - - pos += literal.size(); - return true; -} - -bool JsonParser::extractNull(JsonNode &node) -{ - if (!extractLiteral("null")) - return false; - - node.clear(); - return true; -} - -bool JsonParser::extractTrue(JsonNode &node) -{ - if (!extractLiteral("true")) - return false; - - node.Bool() = true; - return true; -} - -bool JsonParser::extractFalse(JsonNode &node) -{ - if (!extractLiteral("false")) - return false; - - node.Bool() = false; - return true; -} - -bool JsonParser::extractStruct(JsonNode &node) -{ - node.setType(JsonNode::JsonType::DATA_STRUCT); - pos++; - - if (!extractWhitespace()) - return false; - - //Empty struct found - if (input[pos] == '}') - { - pos++; - return true; - } - - while (true) - { - if (!extractWhitespace()) - return false; - - std::string key; - if (!extractString(key)) - return false; - - // split key string into actual key and meta-flags - std::vector keyAndFlags; - boost::split(keyAndFlags, key, boost::is_any_of("#")); - key = keyAndFlags[0]; - // check for unknown flags - helps with debugging - std::vector knownFlags = { "override" }; - for(int i = 1; i < keyAndFlags.size(); i++) - { - if(!vstd::contains(knownFlags, keyAndFlags[i])) - error("Encountered unknown flag #" + keyAndFlags[i], true); - } - - if (node.Struct().find(key) != node.Struct().end()) - error("Dublicated element encountered!", true); - - if (!extractSeparator()) - return false; - - if (!extractElement(node.Struct()[key], '}')) - return false; - - // flags from key string belong to referenced element - for(int i = 1; i < keyAndFlags.size(); i++) - node.Struct()[key].flags.push_back(keyAndFlags[i]); - - if (input[pos] == '}') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractArray(JsonNode &node) -{ - pos++; - node.setType(JsonNode::JsonType::DATA_VECTOR); - - if (!extractWhitespace()) - return false; - - //Empty array found - if (input[pos] == ']') - { - pos++; - return true; - } - - while (true) - { - //NOTE: currently 50% of time is this vector resizing. - //May be useful to use list during parsing and then swap() all items to vector - node.Vector().resize(node.Vector().size()+1); - - if (!extractElement(node.Vector().back(), ']')) - return false; - - if (input[pos] == ']') - { - pos++; - return true; - } - } -} - -bool JsonParser::extractElement(JsonNode &node, char terminator) -{ - if (!extractValue(node)) - return false; - - if (!extractWhitespace()) - return false; - - bool comma = (input[pos] == ','); - if (comma ) - { - pos++; - if (!extractWhitespace()) - return false; - } - - if (input[pos] == terminator) - { - //FIXME: MOD COMPATIBILITY: Too many of these right now, re-enable later - //if (comma) - //error("Extra comma found!", true); - return true; - } - - if (!comma) - error("Comma expected!", true); - - return true; -} - -bool JsonParser::extractFloat(JsonNode &node) -{ - assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); - bool negative=false; - double result=0; - si64 integerPart = 0; - bool isFloat = false; - - if (input[pos] == '-') - { - pos++; - negative = true; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Number expected!"); - - //Extract integer part - while (input[pos] >= '0' && input[pos] <= '9') - { - integerPart = integerPart*10+(input[pos]-'0'); - pos++; - } - - result = static_cast(integerPart); - - if (input[pos] == '.') - { - //extract fractional part - isFloat = true; - pos++; - double fractMult = 0.1; - if (input[pos] < '0' || input[pos] > '9') - return error("Decimal part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - result = result + fractMult*(input[pos]-'0'); - fractMult /= 10; - pos++; - } - } - - if(input[pos] == 'e') - { - //extract exponential part - pos++; - isFloat = true; - bool powerNegative = false; - double power = 0; - - if(input[pos] == '-') - { - pos++; - powerNegative = true; - } - else if(input[pos] == '+') - { - pos++; - } - - if (input[pos] < '0' || input[pos] > '9') - return error("Exponential part expected!"); - - while (input[pos] >= '0' && input[pos] <= '9') - { - power = power*10 + (input[pos]-'0'); - pos++; - } - - if(powerNegative) - power = -power; - - result *= std::pow(10, power); - } - - if(isFloat) - { - if(negative) - result = -result; - - node.setType(JsonNode::JsonType::DATA_FLOAT); - node.Float() = result; - } - else - { - if(negative) - integerPart = -integerPart; - - node.setType(JsonNode::JsonType::DATA_INTEGER); - node.Integer() = integerPart; - } - - return true; -} - -bool JsonParser::error(const std::string &message, bool warning) -{ - std::ostringstream stream; - std::string type(warning?" warning: ":" error: "); - - stream << "At line " << lineCount << ", position "< stringToType = -{ - {"null", JsonNode::JsonType::DATA_NULL}, - {"boolean", JsonNode::JsonType::DATA_BOOL}, - {"number", JsonNode::JsonType::DATA_FLOAT}, - {"string", JsonNode::JsonType::DATA_STRING}, - {"array", JsonNode::JsonType::DATA_VECTOR}, - {"object", JsonNode::JsonType::DATA_STRUCT} -}; - -namespace -{ - namespace Common - { - std::string emptyCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - // check is not needed - e.g. incorporated into another check - return ""; - } - - std::string notImplementedCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data) - { - return "Not implemented entry in schema"; - } - - std::string schemaListCheck(Validation::ValidationData & validator, - const JsonNode & baseSchema, - const JsonNode & schema, - const JsonNode & data, - const std::string & errorMsg, - const std::function & isValid) - { - std::string errors = "\n"; - size_t result = 0; - - for(const auto & schemaEntry : schema.Vector()) - { - std::string error = check(schemaEntry, data, validator); - if (error.empty()) - { - result++; - } - else - { - errors += error; - errors += "\n"; - } - } - if (isValid(result)) - return ""; - else - return validator.makeErrorMessage(errorMsg) + errors; - } - - std::string allOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&](size_t count) - { - return count == schema.Vector().size(); - }); - } - - std::string anyOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [&](size_t count) - { - return count > 0; - }); - } - - std::string oneOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [&](size_t count) - { - return count == 1; - }); - } - - std::string notCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (check(schema, data, validator).empty()) - return validator.makeErrorMessage("Successful validation against negative check"); - return ""; - } - - std::string enumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for(const auto & enumEntry : schema.Vector()) - { - if (data == enumEntry) - return ""; - } - return validator.makeErrorMessage("Key must have one of predefined values"); - } - - std::string typeCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - const auto & typeName = schema.String(); - auto it = stringToType.find(typeName); - if(it == stringToType.end()) - { - return validator.makeErrorMessage("Unknown type in schema:" + typeName); - } - - JsonNode::JsonType type = it->second; - - //FIXME: hack for integer values - if(data.isNumber() && type == JsonNode::JsonType::DATA_FLOAT) - return ""; - - if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) - return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); - return ""; - } - - std::string refCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string URI = schema.String(); - //node must be validated using schema pointed by this reference and not by data here - //Local reference. Turn it into more easy to handle remote ref - if (boost::algorithm::starts_with(URI, "#")) - { - const std::string name = validator.usedSchemas.back(); - const std::string nameClean = name.substr(0, name.find('#')); - URI = nameClean + URI; - } - return check(URI, data, validator); - } - - std::string formatCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - auto formats = Validation::getKnownFormats(); - std::string errors; - auto checker = formats.find(schema.String()); - if (checker != formats.end()) - { - if (data.isString()) - { - std::string result = checker->second(data); - if (!result.empty()) - errors += validator.makeErrorMessage(result); - } - else - { - errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); - } - } - else - errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); - return errors; - } - } - - namespace String - { - std::string maxLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); - return ""; - } - - std::string minLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.String().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); - return ""; - } - } - - namespace Number - { - - std::string maximumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMaximum"].Bool()) - { - if (data.Float() >= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - else - { - if (data.Float() > schema.Float()) - return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); - } - return ""; - } - - std::string minimumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (baseSchema["exclusiveMinimum"].Bool()) - { - if (data.Float() <= schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - else - { - if (data.Float() < schema.Float()) - return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); - } - return ""; - } - - std::string multipleOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - double result = data.Float() / schema.Float(); - if (floor(result) != result) - return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); - return ""; - } - } - - namespace Vector - { - std::string itemEntryCheck(Validation::ValidationData & validator, const JsonVector & items, const JsonNode & schema, size_t index) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().Float() = static_cast(index); - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - if (!schema.isNull()) - return check(schema, items[index], validator); - return ""; - } - - std::string itemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for (size_t i=0; i i) - errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); - } - else - { - errors += itemEntryCheck(validator, data.Vector(), schema, i); - } - } - return errors; - } - - std::string additionalItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - // "items" is struct or empty (defaults to empty struct) - validation always successful - const JsonNode & items = baseSchema["items"]; - if (items.getType() != JsonNode::JsonType::DATA_VECTOR) - return ""; - - for (size_t i=items.Vector().size(); i schema.Float()) - return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string uniqueItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (schema.Bool()) - { - for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) - { - auto itB = itA; - while (++itB != schema.Vector().end()) - { - if (*itA == *itB) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - } - return ""; - } - } - - namespace Struct - { - std::string maxPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() > schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); - return ""; - } - - std::string minPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - if (data.Struct().size() < schema.Float()) - return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); - return ""; - } - - std::string uniquePropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) - { - auto itB = itA; - while (++itB != data.Struct().end()) - { - if (itA->second == itB->second) - return validator.makeErrorMessage("List must consist from unique items"); - } - } - return ""; - } - - std::string requiredCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & required : schema.Vector()) - { - if (data[required.String()].isNull()) - errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); - } - return errors; - } - - std::string dependenciesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & deps : schema.Struct()) - { - if (!data[deps.first].isNull()) - { - if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) - { - JsonVector depList = deps.second.Vector(); - for(auto & depEntry : depList) - { - if (data[depEntry.String()].isNull()) - errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); - } - } - else - { - if (!check(deps.second, data, validator).empty()) - errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); - } - } - } - return errors; - } - - std::string propertyEntryCheck(Validation::ValidationData & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) - { - validator.currentPath.emplace_back(); - validator.currentPath.back().String() = nodeName; - auto onExit = vstd::makeScopeGuard([&]() - { - validator.currentPath.pop_back(); - }); - - // there is schema specifically for this item - if (!schema.isNull()) - return check(schema, node, validator); - return ""; - } - - std::string propertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - - for(const auto & entry : data.Struct()) - errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); - return errors; - } - - std::string additionalPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) - { - std::string errors; - for(const auto & entry : data.Struct()) - { - if (baseSchema["properties"].Struct().count(entry.first) == 0) - { - // try generic additionalItems schema - if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) - errors += propertyEntryCheck(validator, entry.second, schema, entry.first); - - // or, additionalItems field can be bool which indicates if such items are allowed - else if(!schema.isNull() && !schema.Bool()) // present and set to false - error - errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); - } - } - return errors; - } - } - - namespace Formats - { - bool testFilePresence(const std::string & scope, const ResourcePath & resource) - { - std::set allowedScopes; - if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies - { - //NOTE: recursive dependencies are not allowed at the moment - update code if this changes - bool found = true; - allowedScopes = VLC->modh->getModDependencies(scope, found); - - if(!found) - return false; - - allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files - } - allowedScopes.insert(scope); // mods can use their own files - - for(const auto & entry : allowedScopes) - { - if (CResourceHandler::get(entry)->existsResource(resource)) - return true; - } - return false; - } - - #define TEST_FILE(scope, prefix, file, type) \ - if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ - return "" - - std::string testAnimation(const std::string & path, const std::string & scope) - { - TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); - TEST_FILE(scope, "Sprites/", path, EResType::JSON); - return "Animation file \"" + path + "\" was not found"; - } - - std::string textFile(const JsonNode & node) - { - TEST_FILE(node.meta, "", node.String(), EResType::JSON); - return "Text file \"" + node.String() + "\" was not found"; - } - - std::string musicFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Music/", node.String(), EResType::SOUND); - TEST_FILE(node.meta, "", node.String(), EResType::SOUND); - return "Music file \"" + node.String() + "\" was not found"; - } - - std::string soundFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Sounds/", node.String(), EResType::SOUND); - return "Sound file \"" + node.String() + "\" was not found"; - } - - std::string defFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string animationFile(const JsonNode & node) - { - return testAnimation(node.String(), node.meta); - } - - std::string imageFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Data/", node.String(), EResType::IMAGE); - TEST_FILE(node.meta, "Sprites/", node.String(), EResType::IMAGE); - if (node.String().find(':') != std::string::npos) - return testAnimation(node.String().substr(0, node.String().find(':')), node.meta); - return "Image file \"" + node.String() + "\" was not found"; - } - - std::string videoFile(const JsonNode & node) - { - TEST_FILE(node.meta, "Video/", node.String(), EResType::VIDEO); - return "Video file \"" + node.String() + "\" was not found"; - } - - #undef TEST_FILE - } - - Validation::TValidatorMap createCommonFields() - { - Validation::TValidatorMap ret; - - ret["format"] = Common::formatCheck; - ret["allOf"] = Common::allOfCheck; - ret["anyOf"] = Common::anyOfCheck; - ret["oneOf"] = Common::oneOfCheck; - ret["enum"] = Common::enumCheck; - ret["type"] = Common::typeCheck; - ret["not"] = Common::notCheck; - ret["$ref"] = Common::refCheck; - - // fields that don't need implementation - ret["title"] = Common::emptyCheck; - ret["$schema"] = Common::emptyCheck; - ret["default"] = Common::emptyCheck; - ret["description"] = Common::emptyCheck; - ret["definitions"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createStringFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maxLength"] = String::maxLengthCheck; - ret["minLength"] = String::minLengthCheck; - - ret["pattern"] = Common::notImplementedCheck; - return ret; - } - - Validation::TValidatorMap createNumberFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["maximum"] = Number::maximumCheck; - ret["minimum"] = Number::minimumCheck; - ret["multipleOf"] = Number::multipleOfCheck; - - ret["exclusiveMaximum"] = Common::emptyCheck; - ret["exclusiveMinimum"] = Common::emptyCheck; - return ret; - } - - Validation::TValidatorMap createVectorFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["items"] = Vector::itemsCheck; - ret["minItems"] = Vector::minItemsCheck; - ret["maxItems"] = Vector::maxItemsCheck; - ret["uniqueItems"] = Vector::uniqueItemsCheck; - ret["additionalItems"] = Vector::additionalItemsCheck; - return ret; - } - - Validation::TValidatorMap createStructFields() - { - Validation::TValidatorMap ret = createCommonFields(); - ret["additionalProperties"] = Struct::additionalPropertiesCheck; - ret["uniqueProperties"] = Struct::uniquePropertiesCheck; - ret["maxProperties"] = Struct::maxPropertiesCheck; - ret["minProperties"] = Struct::minPropertiesCheck; - ret["dependencies"] = Struct::dependenciesCheck; - ret["properties"] = Struct::propertiesCheck; - ret["required"] = Struct::requiredCheck; - - ret["patternProperties"] = Common::notImplementedCheck; - return ret; - } - - Validation::TFormatMap createFormatMap() - { - Validation::TFormatMap ret; - ret["textFile"] = Formats::textFile; - ret["musicFile"] = Formats::musicFile; - ret["soundFile"] = Formats::soundFile; - ret["defFile"] = Formats::defFile; - ret["animationFile"] = Formats::animationFile; - ret["imageFile"] = Formats::imageFile; - ret["videoFile"] = Formats::videoFile; - - return ret; - } -} - -namespace Validation -{ - std::string ValidationData::makeErrorMessage(const std::string &message) - { - std::string errors; - errors += "At "; - if (!currentPath.empty()) - { - for(const JsonNode &path : currentPath) - { - errors += "/"; - if (path.getType() == JsonNode::JsonType::DATA_STRING) - errors += path.String(); - else - errors += std::to_string(static_cast(path.Float())); - } - } - else - errors += ""; - errors += "\n\t Error: " + message + "\n"; - return errors; - } - - std::string check(const std::string & schemaName, const JsonNode & data) - { - ValidationData validator; - return check(schemaName, data, validator); - } - - std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator) - { - validator.usedSchemas.push_back(schemaName); - auto onscopeExit = vstd::makeScopeGuard([&]() - { - validator.usedSchemas.pop_back(); - }); - return check(JsonUtils::getSchema(schemaName), data, validator); - } - - std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator) - { - const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); - std::string errors; - for(const auto & entry : schema.Struct()) - { - auto checker = knownFields.find(entry.first); - if (checker != knownFields.end()) - errors += checker->second(validator, schema, entry.second, data); - //else - // errors += validator.makeErrorMessage("Unknown entry in schema " + entry.first); - } - return errors; - } - - const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type) - { - static const TValidatorMap commonFields = createCommonFields(); - static const TValidatorMap numberFields = createNumberFields(); - static const TValidatorMap stringFields = createStringFields(); - static const TValidatorMap vectorFields = createVectorFields(); - static const TValidatorMap structFields = createStructFields(); - - switch (type) - { - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - return numberFields; - case JsonNode::JsonType::DATA_STRING: return stringFields; - case JsonNode::JsonType::DATA_VECTOR: return vectorFields; - case JsonNode::JsonType::DATA_STRUCT: return structFields; - default: return commonFields; - } - } - - const TFormatMap & getKnownFormats() - { - static TFormatMap knownFormats = createFormatMap(); - return knownFormats; - } - -} // Validation namespace - -VCMI_LIB_NAMESPACE_END +/* + * JsonDetail.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 "JsonDetail.h" + +#include "VCMI_Lib.h" +#include "TextOperations.h" + +#include "filesystem/Filesystem.h" +#include "modding/ModScope.h" +#include "modding/CModHandler.h" +#include "ScopeGuard.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static const JsonNode nullNode; + +template +void JsonWriter::writeContainer(Iterator begin, Iterator end) +{ + if (begin == end) + return; + + prefix += '\t'; + + writeEntry(begin++); + + while (begin != end) + { + out << (compactMode ? ", " : ",\n"); + writeEntry(begin++); + } + + out << (compactMode ? "" : "\n"); + prefix.resize(prefix.size()-1); +} + +void JsonWriter::writeEntry(JsonMap::const_iterator entry) +{ + if(!compactMode) + { + if (!entry->second.meta.empty()) + out << prefix << " // " << entry->second.meta << "\n"; + if(!entry->second.flags.empty()) + out << prefix << " // flags: " << boost::algorithm::join(entry->second.flags, ", ") << "\n"; + out << prefix; + } + writeString(entry->first); + out << " : "; + writeNode(entry->second); +} + +void JsonWriter::writeEntry(JsonVector::const_iterator entry) +{ + if(!compactMode) + { + if (!entry->meta.empty()) + out << prefix << " // " << entry->meta << "\n"; + if(!entry->flags.empty()) + out << prefix << " // flags: " << boost::algorithm::join(entry->flags, ", ") << "\n"; + out << prefix; + } + writeNode(*entry); +} + +void JsonWriter::writeString(const std::string &string) +{ + static const std::string escaped = "\"\\\b\f\n\r\t/"; + + static const std::array escaped_code = {'\"', '\\', 'b', 'f', 'n', 'r', 't', '/'}; + + out <<'\"'; + size_t pos = 0; + size_t start = 0; + for (; poswarn("File %s is not a valid JSON file!", fileName); + logMod->warn(errors); + } + return root; +} + +bool JsonParser::isValid() +{ + return errors.empty(); +} + +bool JsonParser::extractSeparator() +{ + if (!extractWhitespace()) + return false; + + if ( input[pos] !=':') + return error("Separator expected"); + + pos++; + return true; +} + +bool JsonParser::extractValue(JsonNode &node) +{ + if (!extractWhitespace()) + return false; + + switch (input[pos]) + { + case '\"': return extractString(node); + case 'n' : return extractNull(node); + case 't' : return extractTrue(node); + case 'f' : return extractFalse(node); + case '{' : return extractStruct(node); + case '[' : return extractArray(node); + case '-' : return extractFloat(node); + default: + { + if (input[pos] >= '0' && input[pos] <= '9') + return extractFloat(node); + return error("Value expected!"); + } + } +} + +bool JsonParser::extractWhitespace(bool verbose) +{ + while (true) + { + while(pos < input.size() && static_cast(input[pos]) <= ' ') + { + if (input[pos] == '\n') + { + lineCount++; + lineStart = pos+1; + } + pos++; + } + if (pos >= input.size() || input[pos] != '/') + break; + + pos++; + if (pos == input.size()) + break; + if (input[pos] == '/') + pos++; + else + error("Comments must consist from two slashes!", true); + + while (pos < input.size() && input[pos] != '\n') + pos++; + } + + if (pos >= input.size() && verbose) + return error("Unexpected end of file!"); + return true; +} + +bool JsonParser::extractEscaping(std::string &str) +{ + switch(input[pos]) + { + break; case '\"': str += '\"'; + break; case '\\': str += '\\'; + break; case 'b': str += '\b'; + break; case 'f': str += '\f'; + break; case 'n': str += '\n'; + break; case 'r': str += '\r'; + break; case 't': str += '\t'; + break; case '/': str += '/'; + break; default: return error("Unknown escape sequence!", true); + } + return true; +} + +bool JsonParser::extractString(std::string &str) +{ + if (input[pos] != '\"') + return error("String expected!"); + pos++; + + size_t first = pos; + + while (pos != input.size()) + { + if (input[pos] == '\"') // Correct end of string + { + str.append( &input[first], pos-first); + pos++; + return true; + } + if (input[pos] == '\\') // Escaping + { + str.append( &input[first], pos-first); + pos++; + if (pos == input.size()) + break; + extractEscaping(str); + first = pos + 1; + } + if (input[pos] == '\n') // end-of-line + { + str.append( &input[first], pos-first); + return error("Closing quote not found!", true); + } + if(static_cast(input[pos]) < ' ') // control character + { + str.append( &input[first], pos-first); + first = pos+1; + error("Illegal character in the string!", true); + } + pos++; + } + return error("Unterminated string!"); +} + +bool JsonParser::extractString(JsonNode &node) +{ + std::string str; + if (!extractString(str)) + return false; + + node.setType(JsonNode::JsonType::DATA_STRING); + node.String() = str; + return true; +} + +bool JsonParser::extractLiteral(const std::string &literal) +{ + if (literal.compare(0, literal.size(), &input[pos], literal.size()) != 0) + { + while (pos < input.size() && ((input[pos]>'a' && input[pos]<'z') + || (input[pos]>'A' && input[pos]<'Z'))) + pos++; + return error("Unknown literal found", true); + } + + pos += literal.size(); + return true; +} + +bool JsonParser::extractNull(JsonNode &node) +{ + if (!extractLiteral("null")) + return false; + + node.clear(); + return true; +} + +bool JsonParser::extractTrue(JsonNode &node) +{ + if (!extractLiteral("true")) + return false; + + node.Bool() = true; + return true; +} + +bool JsonParser::extractFalse(JsonNode &node) +{ + if (!extractLiteral("false")) + return false; + + node.Bool() = false; + return true; +} + +bool JsonParser::extractStruct(JsonNode &node) +{ + node.setType(JsonNode::JsonType::DATA_STRUCT); + pos++; + + if (!extractWhitespace()) + return false; + + //Empty struct found + if (input[pos] == '}') + { + pos++; + return true; + } + + while (true) + { + if (!extractWhitespace()) + return false; + + std::string key; + if (!extractString(key)) + return false; + + // split key string into actual key and meta-flags + std::vector keyAndFlags; + boost::split(keyAndFlags, key, boost::is_any_of("#")); + key = keyAndFlags[0]; + // check for unknown flags - helps with debugging + std::vector knownFlags = { "override" }; + for(int i = 1; i < keyAndFlags.size(); i++) + { + if(!vstd::contains(knownFlags, keyAndFlags[i])) + error("Encountered unknown flag #" + keyAndFlags[i], true); + } + + if (node.Struct().find(key) != node.Struct().end()) + error("Dublicated element encountered!", true); + + if (!extractSeparator()) + return false; + + if (!extractElement(node.Struct()[key], '}')) + return false; + + // flags from key string belong to referenced element + for(int i = 1; i < keyAndFlags.size(); i++) + node.Struct()[key].flags.push_back(keyAndFlags[i]); + + if (input[pos] == '}') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractArray(JsonNode &node) +{ + pos++; + node.setType(JsonNode::JsonType::DATA_VECTOR); + + if (!extractWhitespace()) + return false; + + //Empty array found + if (input[pos] == ']') + { + pos++; + return true; + } + + while (true) + { + //NOTE: currently 50% of time is this vector resizing. + //May be useful to use list during parsing and then swap() all items to vector + node.Vector().resize(node.Vector().size()+1); + + if (!extractElement(node.Vector().back(), ']')) + return false; + + if (input[pos] == ']') + { + pos++; + return true; + } + } +} + +bool JsonParser::extractElement(JsonNode &node, char terminator) +{ + if (!extractValue(node)) + return false; + + if (!extractWhitespace()) + return false; + + bool comma = (input[pos] == ','); + if (comma ) + { + pos++; + if (!extractWhitespace()) + return false; + } + + if (input[pos] == terminator) + { + //FIXME: MOD COMPATIBILITY: Too many of these right now, re-enable later + //if (comma) + //error("Extra comma found!", true); + return true; + } + + if (!comma) + error("Comma expected!", true); + + return true; +} + +bool JsonParser::extractFloat(JsonNode &node) +{ + assert(input[pos] == '-' || (input[pos] >= '0' && input[pos] <= '9')); + bool negative=false; + double result=0; + si64 integerPart = 0; + bool isFloat = false; + + if (input[pos] == '-') + { + pos++; + negative = true; + } + + if (input[pos] < '0' || input[pos] > '9') + return error("Number expected!"); + + //Extract integer part + while (input[pos] >= '0' && input[pos] <= '9') + { + integerPart = integerPart*10+(input[pos]-'0'); + pos++; + } + + result = static_cast(integerPart); + + if (input[pos] == '.') + { + //extract fractional part + isFloat = true; + pos++; + double fractMult = 0.1; + if (input[pos] < '0' || input[pos] > '9') + return error("Decimal part expected!"); + + while (input[pos] >= '0' && input[pos] <= '9') + { + result = result + fractMult*(input[pos]-'0'); + fractMult /= 10; + pos++; + } + } + + if(input[pos] == 'e') + { + //extract exponential part + pos++; + isFloat = true; + bool powerNegative = false; + double power = 0; + + if(input[pos] == '-') + { + pos++; + powerNegative = true; + } + else if(input[pos] == '+') + { + pos++; + } + + if (input[pos] < '0' || input[pos] > '9') + return error("Exponential part expected!"); + + while (input[pos] >= '0' && input[pos] <= '9') + { + power = power*10 + (input[pos]-'0'); + pos++; + } + + if(powerNegative) + power = -power; + + result *= std::pow(10, power); + } + + if(isFloat) + { + if(negative) + result = -result; + + node.setType(JsonNode::JsonType::DATA_FLOAT); + node.Float() = result; + } + else + { + if(negative) + integerPart = -integerPart; + + node.setType(JsonNode::JsonType::DATA_INTEGER); + node.Integer() = integerPart; + } + + return true; +} + +bool JsonParser::error(const std::string &message, bool warning) +{ + std::ostringstream stream; + std::string type(warning?" warning: ":" error: "); + + stream << "At line " << lineCount << ", position "< stringToType = +{ + {"null", JsonNode::JsonType::DATA_NULL}, + {"boolean", JsonNode::JsonType::DATA_BOOL}, + {"number", JsonNode::JsonType::DATA_FLOAT}, + {"string", JsonNode::JsonType::DATA_STRING}, + {"array", JsonNode::JsonType::DATA_VECTOR}, + {"object", JsonNode::JsonType::DATA_STRUCT} +}; + +namespace +{ + namespace Common + { + std::string emptyCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + // check is not needed - e.g. incorporated into another check + return ""; + } + + std::string notImplementedCheck(Validation::ValidationData & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data) + { + return "Not implemented entry in schema"; + } + + std::string schemaListCheck(Validation::ValidationData & validator, + const JsonNode & baseSchema, + const JsonNode & schema, + const JsonNode & data, + const std::string & errorMsg, + const std::function & isValid) + { + std::string errors = "\n"; + size_t result = 0; + + for(const auto & schemaEntry : schema.Vector()) + { + std::string error = check(schemaEntry, data, validator); + if (error.empty()) + { + result++; + } + else + { + errors += error; + errors += "\n"; + } + } + if (isValid(result)) + return ""; + else + return validator.makeErrorMessage(errorMsg) + errors; + } + + std::string allOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass all schemas", [&](size_t count) + { + return count == schema.Vector().size(); + }); + } + + std::string anyOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass any schema", [&](size_t count) + { + return count > 0; + }); + } + + std::string oneOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + return schemaListCheck(validator, baseSchema, schema, data, "Failed to pass exactly one schema", [&](size_t count) + { + return count == 1; + }); + } + + std::string notCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (check(schema, data, validator).empty()) + return validator.makeErrorMessage("Successful validation against negative check"); + return ""; + } + + std::string enumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + for(const auto & enumEntry : schema.Vector()) + { + if (data == enumEntry) + return ""; + } + return validator.makeErrorMessage("Key must have one of predefined values"); + } + + std::string typeCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + const auto & typeName = schema.String(); + auto it = stringToType.find(typeName); + if(it == stringToType.end()) + { + return validator.makeErrorMessage("Unknown type in schema:" + typeName); + } + + JsonNode::JsonType type = it->second; + + //FIXME: hack for integer values + if(data.isNumber() && type == JsonNode::JsonType::DATA_FLOAT) + return ""; + + if(type != data.getType() && data.getType() != JsonNode::JsonType::DATA_NULL) + return validator.makeErrorMessage("Type mismatch! Expected " + schema.String()); + return ""; + } + + std::string refCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string URI = schema.String(); + //node must be validated using schema pointed by this reference and not by data here + //Local reference. Turn it into more easy to handle remote ref + if (boost::algorithm::starts_with(URI, "#")) + { + const std::string name = validator.usedSchemas.back(); + const std::string nameClean = name.substr(0, name.find('#')); + URI = nameClean + URI; + } + return check(URI, data, validator); + } + + std::string formatCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + auto formats = Validation::getKnownFormats(); + std::string errors; + auto checker = formats.find(schema.String()); + if (checker != formats.end()) + { + if (data.isString()) + { + std::string result = checker->second(data); + if (!result.empty()) + errors += validator.makeErrorMessage(result); + } + else + { + errors += validator.makeErrorMessage("Format value must be string: " + schema.String()); + } + } + else + errors += validator.makeErrorMessage("Unsupported format type: " + schema.String()); + return errors; + } + } + + namespace String + { + std::string maxLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.String().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("String is longer than %d symbols") % schema.Float()).str()); + return ""; + } + + std::string minLengthCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.String().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("String is shorter than %d symbols") % schema.Float()).str()); + return ""; + } + } + + namespace Number + { + + std::string maximumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (baseSchema["exclusiveMaximum"].Bool()) + { + if (data.Float() >= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + } + else + { + if (data.Float() > schema.Float()) + return validator.makeErrorMessage((boost::format("Value is bigger than %d") % schema.Float()).str()); + } + return ""; + } + + std::string minimumCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (baseSchema["exclusiveMinimum"].Bool()) + { + if (data.Float() <= schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + } + else + { + if (data.Float() < schema.Float()) + return validator.makeErrorMessage((boost::format("Value is smaller than %d") % schema.Float()).str()); + } + return ""; + } + + std::string multipleOfCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + double result = data.Float() / schema.Float(); + if (floor(result) != result) + return validator.makeErrorMessage((boost::format("Value is not divisible by %d") % schema.Float()).str()); + return ""; + } + } + + namespace Vector + { + std::string itemEntryCheck(Validation::ValidationData & validator, const JsonVector & items, const JsonNode & schema, size_t index) + { + validator.currentPath.emplace_back(); + validator.currentPath.back().Float() = static_cast(index); + auto onExit = vstd::makeScopeGuard([&]() + { + validator.currentPath.pop_back(); + }); + + if (!schema.isNull()) + return check(schema, items[index], validator); + return ""; + } + + std::string itemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for (size_t i=0; i i) + errors += itemEntryCheck(validator, data.Vector(), schema.Vector()[i], i); + } + else + { + errors += itemEntryCheck(validator, data.Vector(), schema, i); + } + } + return errors; + } + + std::string additionalItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + // "items" is struct or empty (defaults to empty struct) - validation always successful + const JsonNode & items = baseSchema["items"]; + if (items.getType() != JsonNode::JsonType::DATA_VECTOR) + return ""; + + for (size_t i=items.Vector().size(); i schema.Float()) + return validator.makeErrorMessage((boost::format("Length is bigger than %d") % schema.Float()).str()); + return ""; + } + + std::string uniqueItemsCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (schema.Bool()) + { + for (auto itA = schema.Vector().begin(); itA != schema.Vector().end(); itA++) + { + auto itB = itA; + while (++itB != schema.Vector().end()) + { + if (*itA == *itB) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + } + return ""; + } + } + + namespace Struct + { + std::string maxPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.Struct().size() > schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is bigger than %d") % schema.Float()).str()); + return ""; + } + + std::string minPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + if (data.Struct().size() < schema.Float()) + return validator.makeErrorMessage((boost::format("Number of entries is less than %d") % schema.Float()).str()); + return ""; + } + + std::string uniquePropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + for (auto itA = data.Struct().begin(); itA != data.Struct().end(); itA++) + { + auto itB = itA; + while (++itB != data.Struct().end()) + { + if (itA->second == itB->second) + return validator.makeErrorMessage("List must consist from unique items"); + } + } + return ""; + } + + std::string requiredCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & required : schema.Vector()) + { + if (data[required.String()].isNull()) + errors += validator.makeErrorMessage("Required entry " + required.String() + " is missing"); + } + return errors; + } + + std::string dependenciesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & deps : schema.Struct()) + { + if (!data[deps.first].isNull()) + { + if (deps.second.getType() == JsonNode::JsonType::DATA_VECTOR) + { + JsonVector depList = deps.second.Vector(); + for(auto & depEntry : depList) + { + if (data[depEntry.String()].isNull()) + errors += validator.makeErrorMessage("Property " + depEntry.String() + " required for " + deps.first + " is missing"); + } + } + else + { + if (!check(deps.second, data, validator).empty()) + errors += validator.makeErrorMessage("Requirements for " + deps.first + " are not fulfilled"); + } + } + } + return errors; + } + + std::string propertyEntryCheck(Validation::ValidationData & validator, const JsonNode &node, const JsonNode & schema, const std::string & nodeName) + { + validator.currentPath.emplace_back(); + validator.currentPath.back().String() = nodeName; + auto onExit = vstd::makeScopeGuard([&]() + { + validator.currentPath.pop_back(); + }); + + // there is schema specifically for this item + if (!schema.isNull()) + return check(schema, node, validator); + return ""; + } + + std::string propertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + + for(const auto & entry : data.Struct()) + errors += propertyEntryCheck(validator, entry.second, schema[entry.first], entry.first); + return errors; + } + + std::string additionalPropertiesCheck(Validation::ValidationData & validator, const JsonNode & baseSchema, const JsonNode & schema, const JsonNode & data) + { + std::string errors; + for(const auto & entry : data.Struct()) + { + if (baseSchema["properties"].Struct().count(entry.first) == 0) + { + // try generic additionalItems schema + if (schema.getType() == JsonNode::JsonType::DATA_STRUCT) + errors += propertyEntryCheck(validator, entry.second, schema, entry.first); + + // or, additionalItems field can be bool which indicates if such items are allowed + else if(!schema.isNull() && !schema.Bool()) // present and set to false - error + errors += validator.makeErrorMessage("Unknown entry found: " + entry.first); + } + } + return errors; + } + } + + namespace Formats + { + bool testFilePresence(const std::string & scope, const ResourcePath & resource) + { + std::set allowedScopes; + if(scope != ModScope::scopeBuiltin() && !scope.empty()) // all real mods may have dependencies + { + //NOTE: recursive dependencies are not allowed at the moment - update code if this changes + bool found = true; + allowedScopes = VLC->modh->getModDependencies(scope, found); + + if(!found) + return false; + + allowedScopes.insert(ModScope::scopeBuiltin()); // all mods can use H3 files + } + allowedScopes.insert(scope); // mods can use their own files + + for(const auto & entry : allowedScopes) + { + if (CResourceHandler::get(entry)->existsResource(resource)) + return true; + } + return false; + } + + #define TEST_FILE(scope, prefix, file, type) \ + if (testFilePresence(scope, ResourcePath(prefix + file, type))) \ + return "" + + std::string testAnimation(const std::string & path, const std::string & scope) + { + TEST_FILE(scope, "Sprites/", path, EResType::ANIMATION); + TEST_FILE(scope, "Sprites/", path, EResType::JSON); + return "Animation file \"" + path + "\" was not found"; + } + + std::string textFile(const JsonNode & node) + { + TEST_FILE(node.meta, "", node.String(), EResType::JSON); + return "Text file \"" + node.String() + "\" was not found"; + } + + std::string musicFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Music/", node.String(), EResType::SOUND); + TEST_FILE(node.meta, "", node.String(), EResType::SOUND); + return "Music file \"" + node.String() + "\" was not found"; + } + + std::string soundFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Sounds/", node.String(), EResType::SOUND); + return "Sound file \"" + node.String() + "\" was not found"; + } + + std::string defFile(const JsonNode & node) + { + return testAnimation(node.String(), node.meta); + } + + std::string animationFile(const JsonNode & node) + { + return testAnimation(node.String(), node.meta); + } + + std::string imageFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Data/", node.String(), EResType::IMAGE); + TEST_FILE(node.meta, "Sprites/", node.String(), EResType::IMAGE); + if (node.String().find(':') != std::string::npos) + return testAnimation(node.String().substr(0, node.String().find(':')), node.meta); + return "Image file \"" + node.String() + "\" was not found"; + } + + std::string videoFile(const JsonNode & node) + { + TEST_FILE(node.meta, "Video/", node.String(), EResType::VIDEO); + return "Video file \"" + node.String() + "\" was not found"; + } + + #undef TEST_FILE + } + + Validation::TValidatorMap createCommonFields() + { + Validation::TValidatorMap ret; + + ret["format"] = Common::formatCheck; + ret["allOf"] = Common::allOfCheck; + ret["anyOf"] = Common::anyOfCheck; + ret["oneOf"] = Common::oneOfCheck; + ret["enum"] = Common::enumCheck; + ret["type"] = Common::typeCheck; + ret["not"] = Common::notCheck; + ret["$ref"] = Common::refCheck; + + // fields that don't need implementation + ret["title"] = Common::emptyCheck; + ret["$schema"] = Common::emptyCheck; + ret["default"] = Common::emptyCheck; + ret["description"] = Common::emptyCheck; + ret["definitions"] = Common::emptyCheck; + return ret; + } + + Validation::TValidatorMap createStringFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["maxLength"] = String::maxLengthCheck; + ret["minLength"] = String::minLengthCheck; + + ret["pattern"] = Common::notImplementedCheck; + return ret; + } + + Validation::TValidatorMap createNumberFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["maximum"] = Number::maximumCheck; + ret["minimum"] = Number::minimumCheck; + ret["multipleOf"] = Number::multipleOfCheck; + + ret["exclusiveMaximum"] = Common::emptyCheck; + ret["exclusiveMinimum"] = Common::emptyCheck; + return ret; + } + + Validation::TValidatorMap createVectorFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["items"] = Vector::itemsCheck; + ret["minItems"] = Vector::minItemsCheck; + ret["maxItems"] = Vector::maxItemsCheck; + ret["uniqueItems"] = Vector::uniqueItemsCheck; + ret["additionalItems"] = Vector::additionalItemsCheck; + return ret; + } + + Validation::TValidatorMap createStructFields() + { + Validation::TValidatorMap ret = createCommonFields(); + ret["additionalProperties"] = Struct::additionalPropertiesCheck; + ret["uniqueProperties"] = Struct::uniquePropertiesCheck; + ret["maxProperties"] = Struct::maxPropertiesCheck; + ret["minProperties"] = Struct::minPropertiesCheck; + ret["dependencies"] = Struct::dependenciesCheck; + ret["properties"] = Struct::propertiesCheck; + ret["required"] = Struct::requiredCheck; + + ret["patternProperties"] = Common::notImplementedCheck; + return ret; + } + + Validation::TFormatMap createFormatMap() + { + Validation::TFormatMap ret; + ret["textFile"] = Formats::textFile; + ret["musicFile"] = Formats::musicFile; + ret["soundFile"] = Formats::soundFile; + ret["defFile"] = Formats::defFile; + ret["animationFile"] = Formats::animationFile; + ret["imageFile"] = Formats::imageFile; + ret["videoFile"] = Formats::videoFile; + + return ret; + } +} + +namespace Validation +{ + std::string ValidationData::makeErrorMessage(const std::string &message) + { + std::string errors; + errors += "At "; + if (!currentPath.empty()) + { + for(const JsonNode &path : currentPath) + { + errors += "/"; + if (path.getType() == JsonNode::JsonType::DATA_STRING) + errors += path.String(); + else + errors += std::to_string(static_cast(path.Float())); + } + } + else + errors += ""; + errors += "\n\t Error: " + message + "\n"; + return errors; + } + + std::string check(const std::string & schemaName, const JsonNode & data) + { + ValidationData validator; + return check(schemaName, data, validator); + } + + std::string check(const std::string & schemaName, const JsonNode & data, ValidationData & validator) + { + validator.usedSchemas.push_back(schemaName); + auto onscopeExit = vstd::makeScopeGuard([&]() + { + validator.usedSchemas.pop_back(); + }); + return check(JsonUtils::getSchema(schemaName), data, validator); + } + + std::string check(const JsonNode & schema, const JsonNode & data, ValidationData & validator) + { + const TValidatorMap & knownFields = getKnownFieldsFor(data.getType()); + std::string errors; + for(const auto & entry : schema.Struct()) + { + auto checker = knownFields.find(entry.first); + if (checker != knownFields.end()) + errors += checker->second(validator, schema, entry.second, data); + //else + // errors += validator.makeErrorMessage("Unknown entry in schema " + entry.first); + } + return errors; + } + + const TValidatorMap & getKnownFieldsFor(JsonNode::JsonType type) + { + static const TValidatorMap commonFields = createCommonFields(); + static const TValidatorMap numberFields = createNumberFields(); + static const TValidatorMap stringFields = createStringFields(); + static const TValidatorMap vectorFields = createVectorFields(); + static const TValidatorMap structFields = createStructFields(); + + switch (type) + { + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + return numberFields; + case JsonNode::JsonType::DATA_STRING: return stringFields; + case JsonNode::JsonType::DATA_VECTOR: return vectorFields; + case JsonNode::JsonType::DATA_STRUCT: return structFields; + default: return commonFields; + } + } + + const TFormatMap & getKnownFormats() + { + static TFormatMap knownFormats = createFormatMap(); + return knownFormats; + } + +} // Validation namespace + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 44999cfb0..83282368e 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1,1504 +1,1504 @@ -/* - * JsonNode.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 "JsonNode.h" - -#include "ScopeGuard.h" - -#include "bonuses/BonusParams.h" -#include "bonuses/Bonus.h" -#include "bonuses/Limiters.h" -#include "bonuses/Propagators.h" -#include "bonuses/Updaters.h" -#include "filesystem/Filesystem.h" -#include "modding/IdentifierStorage.h" -#include "VCMI_Lib.h" //for identifier resolution -#include "CGeneralTextHandler.h" -#include "JsonDetail.h" -#include "constants/StringConstants.h" -#include "battle/BattleHex.h" - -namespace -{ -// to avoid duplicating const and non-const code -template -Node & resolvePointer(Node & in, const std::string & pointer) -{ - if(pointer.empty()) - return in; - assert(pointer[0] == '/'); - - size_t splitPos = pointer.find('/', 1); - - std::string entry = pointer.substr(1, splitPos - 1); - std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); - - if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) - { - if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string - throw std::runtime_error("Invalid Json pointer"); - - if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed - throw std::runtime_error("Invalid Json pointer"); - - auto index = boost::lexical_cast(entry); - - if (in.Vector().size() > index) - return in.Vector()[index].resolvePointer(remainer); - } - return in[entry].resolvePointer(remainer); -} -} - -VCMI_LIB_NAMESPACE_BEGIN - -using namespace JsonDetail; - -class LibClasses; -class CModHandler; - -static const JsonNode nullNode; - -JsonNode::JsonNode(JsonType Type): - type(JsonType::DATA_NULL) -{ - setType(Type); -} - -JsonNode::JsonNode(const char *data, size_t datasize): - type(JsonType::DATA_NULL) -{ - JsonParser parser(data, datasize); - *this = parser.parse(""); -} - -JsonNode::JsonNode(const JsonPath & fileURI): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI): -type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); -} - -JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax): - type(JsonType::DATA_NULL) -{ - auto file = CResourceHandler::get()->load(fileURI)->readAll(); - - JsonParser parser(reinterpret_cast(file.first.get()), file.second); - *this = parser.parse(fileURI.getName()); - isValidSyntax = parser.isValid(); -} - -JsonNode::JsonNode(const JsonNode ©): - type(JsonType::DATA_NULL), - meta(copy.meta), - flags(copy.flags) -{ - setType(copy.getType()); - switch(type) - { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: Bool() = copy.Bool(); - break; case JsonType::DATA_FLOAT: Float() = copy.Float(); - break; case JsonType::DATA_STRING: String() = copy.String(); - break; case JsonType::DATA_VECTOR: Vector() = copy.Vector(); - break; case JsonType::DATA_STRUCT: Struct() = copy.Struct(); - break; case JsonType::DATA_INTEGER:Integer() = copy.Integer(); - } -} - -JsonNode::~JsonNode() -{ - setType(JsonType::DATA_NULL); -} - -void JsonNode::swap(JsonNode &b) -{ - using std::swap; - swap(meta, b.meta); - swap(data, b.data); - swap(type, b.type); - swap(flags, b.flags); -} - -JsonNode & JsonNode::operator =(JsonNode node) -{ - swap(node); - return *this; -} - -bool JsonNode::operator == (const JsonNode &other) const -{ - if (getType() == other.getType()) - { - switch(type) - { - case JsonType::DATA_NULL: return true; - case JsonType::DATA_BOOL: return Bool() == other.Bool(); - case JsonType::DATA_FLOAT: return Float() == other.Float(); - case JsonType::DATA_STRING: return String() == other.String(); - case JsonType::DATA_VECTOR: return Vector() == other.Vector(); - case JsonType::DATA_STRUCT: return Struct() == other.Struct(); - case JsonType::DATA_INTEGER:return Integer()== other.Integer(); - } - } - return false; -} - -bool JsonNode::operator != (const JsonNode &other) const -{ - return !(*this == other); -} - -JsonNode::JsonType JsonNode::getType() const -{ - return type; -} - -void JsonNode::setMeta(const std::string & metadata, bool recursive) -{ - meta = metadata; - if (recursive) - { - switch (type) - { - break; case JsonType::DATA_VECTOR: - { - for(auto & node : Vector()) - { - node.setMeta(metadata); - } - } - break; case JsonType::DATA_STRUCT: - { - for(auto & node : Struct()) - { - node.second.setMeta(metadata); - } - } - } - } -} - -void JsonNode::setType(JsonType Type) -{ - if (type == Type) - return; - - //float<->int conversion - if(type == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) - { - si64 converted = static_cast(data.Float); - type = Type; - data.Integer = converted; - return; - } - else if(type == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) - { - auto converted = static_cast(data.Integer); - type = Type; - data.Float = converted; - return; - } - - //Reset node to nullptr - if (Type != JsonType::DATA_NULL) - setType(JsonType::DATA_NULL); - - switch (type) - { - break; case JsonType::DATA_STRING: delete data.String; - break; case JsonType::DATA_VECTOR: delete data.Vector; - break; case JsonType::DATA_STRUCT: delete data.Struct; - break; default: - break; - } - //Set new node type - type = Type; - switch(type) - { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: data.Bool = false; - break; case JsonType::DATA_FLOAT: data.Float = 0; - break; case JsonType::DATA_STRING: data.String = new std::string(); - break; case JsonType::DATA_VECTOR: data.Vector = new JsonVector(); - break; case JsonType::DATA_STRUCT: data.Struct = new JsonMap(); - break; case JsonType::DATA_INTEGER: data.Integer = 0; - } -} - -bool JsonNode::isNull() const -{ - return type == JsonType::DATA_NULL; -} - -bool JsonNode::isNumber() const -{ - return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT; -} - -bool JsonNode::isString() const -{ - return type == JsonType::DATA_STRING; -} - -bool JsonNode::isVector() const -{ - return type == JsonType::DATA_VECTOR; -} - -bool JsonNode::isStruct() const -{ - return type == JsonType::DATA_STRUCT; -} - -bool JsonNode::containsBaseData() const -{ - switch(type) - { - case JsonType::DATA_NULL: - return false; - case JsonType::DATA_STRUCT: - for(const auto & elem : *data.Struct) - { - if(elem.second.containsBaseData()) - return true; - } - return false; - default: - //other types (including vector) cannot be extended via merge - return true; - } -} - -bool JsonNode::isCompact() const -{ - switch(type) - { - case JsonType::DATA_VECTOR: - for(JsonNode & elem : *data.Vector) - { - if(!elem.isCompact()) - return false; - } - return true; - case JsonType::DATA_STRUCT: - { - auto propertyCount = data.Struct->size(); - if(propertyCount == 0) - return true; - else if(propertyCount == 1) - return data.Struct->begin()->second.isCompact(); - } - return false; - default: - return true; - } -} - -bool JsonNode::TryBoolFromString(bool & success) const -{ - success = true; - if(type == JsonNode::JsonType::DATA_BOOL) - return Bool(); - - success = type == JsonNode::JsonType::DATA_STRING; - if(success) - { - auto boolParamStr = String(); - boost::algorithm::trim(boolParamStr); - boost::algorithm::to_lower(boolParamStr); - success = boolParamStr == "true"; - - if(success) - return true; - - success = boolParamStr == "false"; - } - return false; -} - -void JsonNode::clear() -{ - setType(JsonType::DATA_NULL); -} - -bool & JsonNode::Bool() -{ - setType(JsonType::DATA_BOOL); - return data.Bool; -} - -double & JsonNode::Float() -{ - setType(JsonType::DATA_FLOAT); - return data.Float; -} - -si64 & JsonNode::Integer() -{ - setType(JsonType::DATA_INTEGER); - return data.Integer; -} - -std::string & JsonNode::String() -{ - setType(JsonType::DATA_STRING); - return *data.String; -} - -JsonVector & JsonNode::Vector() -{ - setType(JsonType::DATA_VECTOR); - return *data.Vector; -} - -JsonMap & JsonNode::Struct() -{ - setType(JsonType::DATA_STRUCT); - return *data.Struct; -} - -const bool boolDefault = false; -bool JsonNode::Bool() const -{ - if (type == JsonType::DATA_NULL) - return boolDefault; - assert(type == JsonType::DATA_BOOL); - return data.Bool; -} - -const double floatDefault = 0; -double JsonNode::Float() const -{ - if(type == JsonType::DATA_NULL) - return floatDefault; - else if(type == JsonType::DATA_INTEGER) - return static_cast(data.Integer); - - assert(type == JsonType::DATA_FLOAT); - return data.Float; -} - -const si64 integetDefault = 0; -si64 JsonNode::Integer() const -{ - if(type == JsonType::DATA_NULL) - return integetDefault; - else if(type == JsonType::DATA_FLOAT) - return static_cast(data.Float); - - assert(type == JsonType::DATA_INTEGER); - return data.Integer; -} - -const std::string stringDefault = std::string(); -const std::string & JsonNode::String() const -{ - if (type == JsonType::DATA_NULL) - return stringDefault; - assert(type == JsonType::DATA_STRING); - return *data.String; -} - -const JsonVector vectorDefault = JsonVector(); -const JsonVector & JsonNode::Vector() const -{ - if (type == JsonType::DATA_NULL) - return vectorDefault; - assert(type == JsonType::DATA_VECTOR); - return *data.Vector; -} - -const JsonMap mapDefault = JsonMap(); -const JsonMap & JsonNode::Struct() const -{ - if (type == JsonType::DATA_NULL) - return mapDefault; - assert(type == JsonType::DATA_STRUCT); - return *data.Struct; -} - -JsonNode & JsonNode::operator[](const std::string & child) -{ - return Struct()[child]; -} - -const JsonNode & JsonNode::operator[](const std::string & child) const -{ - auto it = Struct().find(child); - if (it != Struct().end()) - return it->second; - return nullNode; -} - -JsonNode & JsonNode::operator[](size_t child) -{ - if (child >= Vector().size() ) - Vector().resize(child + 1); - return Vector()[child]; -} - -const JsonNode & JsonNode::operator[](size_t child) const -{ - if (child < Vector().size() ) - return Vector()[child]; - - return nullNode; -} - -const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const -{ - return ::resolvePointer(*this, jsonPointer); -} - -JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) -{ - return ::resolvePointer(*this, jsonPointer); -} - -std::string JsonNode::toJson(bool compact) const -{ - std::ostringstream out; - JsonWriter writer(out, compact); - writer.writeNode(*this); - return out.str(); -} - -///JsonUtils - -void JsonUtils::parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest) -{ - dest->val = static_cast(source[1].Float()); - resolveIdentifier(source[2],dest->subtype); - dest->additionalInfo = static_cast(source[3].Float()); - dest->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) - dest->turnsRemain = 0; -} - -std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) -{ - auto b = std::make_shared(); - std::string type = ability_vec[0].String(); - auto it = bonusNameMap.find(type); - if (it == bonusNameMap.end()) - { - logMod->error("Error: invalid ability type %s.", type); - return b; - } - b->type = it->second; - - parseTypedBonusShort(ability_vec, b); - return b; -} - -template -const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) -{ - static T defaultValue = T(); - if (!val->isNull()) - { - const std::string & type = val->String(); - auto it = map.find(type); - if (it == map.end()) - { - logMod->error("Error: invalid %s%s.", err, type); - return defaultValue; - } - else - { - return it->second; - } - } - else - return defaultValue; -} - -template -const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) -{ - if(val->isNumber()) - return static_cast(val->Integer()); - else - return parseByMap(map, val, err); -} - -void JsonUtils::resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name) -{ - const JsonNode &value = node[name]; - if (!value.isNull()) - { - switch (value.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(value.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(value.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for value of %s.", name); - } - } -} - -void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) -{ - const JsonNode & value = node["addInfo"]; - if (!value.isNull()) - { - switch (value.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(value.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(value.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) - { - var = identifier; - }); - break; - case JsonNode::JsonType::DATA_VECTOR: - { - const JsonVector & vec = value.Vector(); - var.resize(vec.size()); - for(int i = 0; i < vec.size(); i++) - { - switch(vec[i].getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var[i] = static_cast(vec[i].Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var[i] = static_cast(vec[i].Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(vec[i], [&var,i](si32 identifier) - { - var[i] = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for value of addInfo[%d].", i); - } - } - break; - } - default: - logMod->error("Error! Wrong identifier used for value of addInfo."); - } - } -} - -void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var) -{ - switch (node.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(node.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(node.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(node, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for identifier!"); - } -} - -std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) -{ - switch(limiter.getType()) - { - case JsonNode::JsonType::DATA_VECTOR: - { - const JsonVector & subLimiters = limiter.Vector(); - if(subLimiters.empty()) - { - logMod->warn("Warning: empty limiter list"); - return std::make_shared(); - } - std::shared_ptr result; - int offset = 1; - // determine limiter type and offset for sub-limiters - if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING) - { - const std::string & aggregator = subLimiters[0].String(); - if(aggregator == AllOfLimiter::aggregator) - result = std::make_shared(); - else if(aggregator == AnyOfLimiter::aggregator) - result = std::make_shared(); - else if(aggregator == NoneOfLimiter::aggregator) - result = std::make_shared(); - } - if(!result) - { - // collapse for single limiter without explicit aggregate operator - if(subLimiters.size() == 1) - return parseLimiter(subLimiters[0]); - // implicit aggregator must be allOf - result = std::make_shared(); - offset = 0; - } - if(subLimiters.size() == offset) - logMod->warn("Warning: empty sub-limiter list"); - for(int sl = offset; sl < subLimiters.size(); ++sl) - result->add(parseLimiter(subLimiters[sl])); - return result; - } - break; - case JsonNode::JsonType::DATA_STRING: //pre-defined limiters - return parseByMap(bonusLimiterMap, &limiter, "limiter type "); - break; - case JsonNode::JsonType::DATA_STRUCT: //customizable limiters - { - std::string limiterType = limiter["type"].String(); - const JsonVector & parameters = limiter["parameters"].Vector(); - if(limiterType == "CREATURE_TYPE_LIMITER") - { - std::shared_ptr creatureLimiter = std::make_shared(); - VLC->identifiers()->requestIdentifier("creature", parameters[0], [=](si32 creature) - { - creatureLimiter->setCreature(CreatureID(creature)); - }); - auto includeUpgrades = false; - - if(parameters.size() > 1) - { - bool success = true; - includeUpgrades = parameters[1].TryBoolFromString(success); - - if(!success) - logMod->error("Second parameter of '%s' limiter should be Bool", limiterType); - } - creatureLimiter->includeUpgrades = includeUpgrades; - return creatureLimiter; - } - else if(limiterType == "HAS_ANOTHER_BONUS_LIMITER") - { - std::string anotherBonusType = parameters[0].String(); - auto it = bonusNameMap.find(anotherBonusType); - if(it == bonusNameMap.end()) - { - logMod->error("Error: invalid ability type %s.", anotherBonusType); - } - else - { - std::shared_ptr bonusLimiter = std::make_shared(); - bonusLimiter->type = it->second; - auto findSource = [&](const JsonNode & parameter) - { - if(parameter.getType() == JsonNode::JsonType::DATA_STRUCT) - { - auto sourceIt = bonusSourceMap.find(parameter["type"].String()); - if(sourceIt != bonusSourceMap.end()) - { - bonusLimiter->source = sourceIt->second; - bonusLimiter->isSourceRelevant = true; - if(!parameter["id"].isNull()) { - resolveIdentifier(parameter["id"], bonusLimiter->sid); - bonusLimiter->isSourceIDRelevant = true; - } - } - } - return false; - }; - if(parameters.size() > 1) - { - if(findSource(parameters[1]) && parameters.size() == 2) - return bonusLimiter; - else - { - resolveIdentifier(parameters[1], bonusLimiter->subtype); - bonusLimiter->isSubtypeRelevant = true; - if(parameters.size() > 2) - findSource(parameters[2]); - } - } - return bonusLimiter; - } - } - else if(limiterType == "CREATURE_ALIGNMENT_LIMITER") - { - int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, parameters[0].String()); - if(alignment == -1) - logMod->error("Error: invalid alignment %s.", parameters[0].String()); - else - return std::make_shared(static_cast(alignment)); - } - else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat - { - std::shared_ptr factionLimiter = std::make_shared(); - VLC->identifiers()->requestIdentifier("faction", parameters[0], [=](si32 faction) - { - factionLimiter->faction = FactionID(faction); - }); - return factionLimiter; - } - else if(limiterType == "CREATURE_LEVEL_LIMITER") - { - auto levelLimiter = std::make_shared(); - if(!parameters.empty()) //If parameters is empty, level limiter works as CREATURES_ONLY limiter - { - levelLimiter->minLevel = parameters[0].Integer(); - if(parameters[1].isNumber()) - levelLimiter->maxLevel = parameters[1].Integer(); - } - return levelLimiter; - } - else if(limiterType == "CREATURE_TERRAIN_LIMITER") - { - std::shared_ptr terrainLimiter = std::make_shared(); - if(!parameters.empty()) - { - VLC->identifiers()->requestIdentifier("terrain", parameters[0], [=](si32 terrain) - { - //TODO: support limiters - //terrainLimiter->terrainType = terrain; - }); - } - return terrainLimiter; - } - else if(limiterType == "UNIT_ON_HEXES") { - auto hexLimiter = std::make_shared(); - if(!parameters.empty()) - { - for (const auto & parameter: parameters){ - if(parameter.isNumber()) - hexLimiter->applicableHexes.insert(BattleHex(parameter.Integer())); - } - } - return hexLimiter; - } - else - { - logMod->error("Error: invalid customizable limiter type %s.", limiterType); - } - } - break; - default: - break; - } - return nullptr; -} - -std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) -{ - auto b = std::make_shared(); - if (!parseBonus(ability, b.get())) - { - // caller code can not handle this case and presumes that returned bonus is always valid - logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); - - b->type = BonusType::NONE; - assert(0); // or throw? Game *should* work with dummy bonus - - return b; - } - return b; -} - -std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description) -{ - /* duration = BonusDuration::PERMANENT - source = BonusSource::TOWN_STRUCTURE - bonusType, val, subtype - get from json - */ - auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description, -1); - - if(!parseBonus(ability, b.get())) - return nullptr; - return b; -} - -static BonusParams convertDeprecatedBonus(const JsonNode &ability) -{ - if(vstd::contains(deprecatedBonusSet, ability["type"].String())) - { - logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toJson()); - auto params = BonusParams(ability["type"].String(), - ability["subtype"].isString() ? ability["subtype"].String() : "", - ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); - if(params.isConverted) - { - if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special - { - params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; - params.targetType = BonusSource::SECONDARY_SKILL; - } - - logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); - return params; - } - else - logMod->error("Cannot convert bonus!\n%s", ability.toJson()); - } - BonusParams ret; - ret.isConverted = false; - return ret; -} - -static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) -{ - switch(updaterJson.getType()) - { - case JsonNode::JsonType::DATA_STRING: - return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); - break; - case JsonNode::JsonType::DATA_STRUCT: - if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") - { - std::shared_ptr updater = std::make_shared(); - const JsonVector param = updaterJson["parameters"].Vector(); - updater->valPer20 = static_cast(param[0].Integer()); - if(param.size() > 1) - updater->stepSize = static_cast(param[1].Integer()); - return updater; - } - else if (updaterJson["type"].String() == "ARMY_MOVEMENT") - { - std::shared_ptr updater = std::make_shared(); - if(updaterJson["parameters"].isVector()) - { - const auto & param = updaterJson["parameters"].Vector(); - if(param.size() < 4) - logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); - else - { - updater->base = static_cast(param.at(0).Integer()); - updater->divider = static_cast(param.at(1).Integer()); - updater->multiplier = static_cast(param.at(2).Integer()); - updater->max = static_cast(param.at(3).Integer()); - } - return updater; - } - } - else - logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); - break; - } - return nullptr; -} - -bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) -{ - const JsonNode * value = nullptr; - - std::string type = ability["type"].String(); - auto it = bonusNameMap.find(type); - auto params = std::make_unique(false); - if (it == bonusNameMap.end()) - { - params = std::make_unique(convertDeprecatedBonus(ability)); - if(!params->isConverted) - { - logMod->error("Error: invalid ability type %s.", type); - return false; - } - b->type = params->type; - b->val = params->val.value_or(0); - b->valType = params->valueType.value_or(BonusValueType::ADDITIVE_VALUE); - if(params->targetType) - b->targetSourceType = params->targetType.value(); - } - else - b->type = it->second; - - resolveIdentifier(b->subtype, params->isConverted ? params->toJson() : ability, "subtype"); - - if(!params->isConverted) - { - b->val = static_cast(ability["val"].Float()); - - value = &ability["valueType"]; - if (!value->isNull()) - b->valType = static_cast(parseByMapN(bonusValueMap, value, "value type ")); - } - - b->stacking = ability["stacking"].String(); - - resolveAddInfo(b->additionalInfo, ability); - - b->turnsRemain = static_cast(ability["turns"].Float()); - - b->sid = static_cast(ability["sourceID"].Float()); - - if(!ability["description"].isNull()) - { - if (ability["description"].isString()) - b->description = ability["description"].String(); - if (ability["description"].isNumber()) - b->description = VLC->generaltexth->translate("core.arraytxt", ability["description"].Integer()); - } - - value = &ability["effectRange"]; - if (!value->isNull()) - b->effectRange = static_cast(parseByMapN(bonusLimitEffect, value, "effect range ")); - - value = &ability["duration"]; - if (!value->isNull()) - { - switch (value->getType()) - { - case JsonNode::JsonType::DATA_STRING: - b->duration = parseByMap(bonusDurationMap, value, "duration type "); - break; - case JsonNode::JsonType::DATA_VECTOR: - { - BonusDuration::Type dur = 0; - for (const JsonNode & d : value->Vector()) - dur |= parseByMapN(bonusDurationMap, &d, "duration type "); - b->duration = dur; - } - break; - default: - logMod->error("Error! Wrong bonus duration format."); - } - } - - value = &ability["sourceType"]; - if (!value->isNull()) - b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); - - value = &ability["targetSourceType"]; - if (!value->isNull()) - b->targetSourceType = static_cast(parseByMap(bonusSourceMap, value, "target type ")); - - value = &ability["limiters"]; - if (!value->isNull()) - b->limiter = parseLimiter(*value); - - value = &ability["propagator"]; - if (!value->isNull()) - { - //ALL_CREATURES old propagator compatibility - if(value->String() == "ALL_CREATURES") - { - logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter"); - b->addLimiter(std::make_shared()); - b->propagator = bonusPropagatorMap.at("GLOBAL_EFFECT"); - } - else - b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type "); - } - - value = &ability["updater"]; - if(!value->isNull()) - b->addUpdater(parseUpdater(*value)); - value = &ability["propagationUpdater"]; - if(!value->isNull()) - b->propagationUpdater = parseUpdater(*value); - return true; -} - -CSelector JsonUtils::parseSelector(const JsonNode & ability) -{ - CSelector ret = Selector::all; - - // Recursive parsers for anyOf, allOf, noneOf - const auto * value = &ability["allOf"]; - if(value->isVector()) - { - for(const auto & andN : value->Vector()) - ret = ret.And(parseSelector(andN)); - } - - value = &ability["anyOf"]; - if(value->isVector()) - { - CSelector base = Selector::none; - for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); - - ret = ret.And(base); - } - - value = &ability["noneOf"]; - if(value->isVector()) - { - CSelector base = Selector::none; - for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); - - ret = ret.And(base.Not()); - } - - // Actual selector parser - value = &ability["type"]; - if(value->isString()) - { - auto it = bonusNameMap.find(value->String()); - if(it != bonusNameMap.end()) - ret = ret.And(Selector::type()(it->second)); - } - value = &ability["subtype"]; - if(!value->isNull()) - { - TBonusSubtype subtype; - resolveIdentifier(subtype, ability, "subtype"); - ret = ret.And(Selector::subtype()(subtype)); - } - value = &ability["sourceType"]; - std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized - std::optional id = std::nullopt; - if(value->isString()) - { - auto it = bonusSourceMap.find(value->String()); - if(it != bonusSourceMap.end()) - src = it->second; - } - - value = &ability["sourceID"]; - if(!value->isNull()) - { - id = -1; - resolveIdentifier(*id, ability, "sourceID"); - } - - if(src && id) - ret = ret.And(Selector::source(*src, *id)); - else if(src) - ret = ret.And(Selector::sourceTypeSel(*src)); - - - value = &ability["targetSourceType"]; - if(value->isString()) - { - auto it = bonusSourceMap.find(value->String()); - if(it != bonusSourceMap.end()) - ret = ret.And(Selector::targetSourceType()(it->second)); - } - value = &ability["valueType"]; - if(value->isString()) - { - auto it = bonusValueMap.find(value->String()); - if(it != bonusValueMap.end()) - ret = ret.And(Selector::valueType(it->second)); - } - CAddInfo info; - value = &ability["addInfo"]; - if(!value->isNull()) - { - resolveAddInfo(info, ability["addInfo"]); - ret = ret.And(Selector::info()(info)); - } - value = &ability["effectRange"]; - if(value->isString()) - { - auto it = bonusLimitEffect.find(value->String()); - if(it != bonusLimitEffect.end()) - ret = ret.And(Selector::effectRange()(it->second)); - } - value = &ability["lastsTurns"]; - if(value->isNumber()) - ret = ret.And(Selector::turns(value->Integer())); - value = &ability["lastsDays"]; - if(value->isNumber()) - ret = ret.And(Selector::days(value->Integer())); - - return ret; -} - -//returns first Key with value equal to given one -template -Key reverseMapFirst(const Val & val, const std::map & map) -{ - for(auto it : map) - { - if(it.second == val) - { - return it.first; - } - } - assert(0); - return ""; -} - -static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) -{ - const JsonNode & fieldProps = schema["properties"][fieldName]; - -#if defined(VCMI_IOS) - if (!fieldProps["defaultIOS"].isNull()) - return fieldProps["defaultIOS"]; -#elif defined(VCMI_ANDROID) - if (!fieldProps["defaultAndroid"].isNull()) - return fieldProps["defaultAndroid"]; -#elif !defined(VCMI_MOBILE) - if (!fieldProps["defaultDesktop"].isNull()) - return fieldProps["defaultDesktop"]; -#endif - return fieldProps["default"]; -} - -static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) -{ - assert(schema["type"].String() == "object"); - - std::set foundEntries; - - for(const auto & entry : schema["required"].Vector()) - foundEntries.insert(entry.String()); - - vstd::erase_if(node.Struct(), [&](const auto & node){ - return !vstd::contains(foundEntries, node.first); - }); -} - -static void minimizeNode(JsonNode & node, const JsonNode & schema) -{ - if (schema["type"].String() != "object") - return; - - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - minimizeNode(node[name], schema["properties"][name]); - - if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) - node.Struct().erase(name); - } - eraseOptionalNodes(node, schema); -} - -static void maximizeNode(JsonNode & node, const JsonNode & schema) -{ - // "required" entry can only be found in object/struct - if (schema["type"].String() != "object") - return; - - // check all required entries that have default version - for(const auto & entry : schema["required"].Vector()) - { - const std::string & name = entry.String(); - - if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) - node[name] = getDefaultValue(schema, name); - - maximizeNode(node[name], schema["properties"][name]); - } - - eraseOptionalNodes(node, schema); -} - -void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) -{ - minimizeNode(node, getSchema(schemaName)); -} - -void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) -{ - maximizeNode(node, getSchema(schemaName)); -} - -bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) -{ - std::string log = Validation::check(schemaName, node); - if (!log.empty()) - { - logMod->warn("Data in %s is invalid!", dataName); - logMod->warn(log); - logMod->trace("%s json: %s", dataName, node.toJson(true)); - } - return log.empty(); -} - -const JsonNode & getSchemaByName(const std::string & name) -{ - // cached schemas to avoid loading json data multiple times - static std::map loadedSchemas; - - if (vstd::contains(loadedSchemas, name)) - return loadedSchemas[name]; - - auto filename = JsonPath::builtin("config/schemas/" + name); - - if (CResourceHandler::get()->existsResource(filename)) - { - loadedSchemas[name] = JsonNode(filename); - return loadedSchemas[name]; - } - - logMod->error("Error: missing schema with name %s!", name); - assert(0); - return nullNode; -} - -const JsonNode & JsonUtils::getSchema(const std::string & URI) -{ - size_t posColon = URI.find(':'); - size_t posHash = URI.find('#'); - std::string filename; - if(posColon == std::string::npos) - { - filename = URI.substr(0, posHash); - } - else - { - std::string protocolName = URI.substr(0, posColon); - filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; - if(protocolName != "vcmi") - { - logMod->error("Error: unsupported URI protocol for schema: %s", URI); - return nullNode; - } - } - - // check if json pointer if present (section after hash in string) - if(posHash == std::string::npos || posHash == URI.size() - 1) - { - auto const & result = getSchemaByName(filename); - if (result.isNull()) - logMod->error("Error: missing schema %s", URI); - return result; - } - else - { - auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); - if (result.isNull()) - logMod->error("Error: missing schema %s", URI); - return result; - } -} - -void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) -{ - if (dest.getType() == JsonNode::JsonType::DATA_NULL) - { - std::swap(dest, source); - return; - } - - switch (source.getType()) - { - case JsonNode::JsonType::DATA_NULL: - { - dest.clear(); - break; - } - case JsonNode::JsonType::DATA_BOOL: - case JsonNode::JsonType::DATA_FLOAT: - case JsonNode::JsonType::DATA_INTEGER: - case JsonNode::JsonType::DATA_STRING: - case JsonNode::JsonType::DATA_VECTOR: - { - std::swap(dest, source); - break; - } - case JsonNode::JsonType::DATA_STRUCT: - { - if(!ignoreOverride && vstd::contains(source.flags, "override")) - { - std::swap(dest, source); - } - else - { - if (copyMeta) - dest.meta = source.meta; - - //recursively merge all entries from struct - for(auto & node : source.Struct()) - merge(dest[node.first], node.second, ignoreOverride); - } - } - } -} - -void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) -{ - // uses copy created in stack to safely merge two nodes - merge(dest, source, ignoreOverride, copyMeta); -} - -void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) -{ - JsonNode inheritedNode(base); - merge(inheritedNode, descendant, true, true); - descendant.swap(inheritedNode); -} - -JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) -{ - if(nodes.empty()) - return nullNode; - - JsonNode result = nodes[0]; - for(int i = 1; i < nodes.size(); i++) - { - if(result.isNull()) - break; - result = JsonUtils::intersect(result, nodes[i], pruneEmpty); - } - return result; -} - -JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty) -{ - if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // intersect individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : a.Struct()) - { - if(vstd::contains(b.Struct(), property.first)) - { - JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second); - if(pruneEmpty && !propertyIntersect.containsBaseData()) - continue; - result[property.first] = propertyIntersect; - } - } - return result; - } - else - { - // not a struct - same or different, no middle ground - if(a == b) - return a; - } - return nullNode; -} - -JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base) -{ - auto addsInfo = [](JsonNode diff) -> bool - { - switch(diff.getType()) - { - case JsonNode::JsonType::DATA_NULL: - return false; - case JsonNode::JsonType::DATA_STRUCT: - return !diff.Struct().empty(); - default: - return true; - } - }; - - if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT) - { - // subtract individual properties - JsonNode result(JsonNode::JsonType::DATA_STRUCT); - for(const auto & property : node.Struct()) - { - if(vstd::contains(base.Struct(), property.first)) - { - const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second); - if(addsInfo(propertyDifference)) - result[property.first] = propertyDifference; - } - else - { - result[property.first] = property.second; - } - } - return result; - } - else - { - if(node == base) - return nullNode; - } - return node; -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files) -{ - bool isValid = false; - return assembleFromFiles(files, isValid); -} - -JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) -{ - isValid = true; - JsonNode result; - - for(const auto & file : files) - { - bool isValidFile = false; - JsonNode section(JsonPath::builtinTODO(file), isValidFile); - merge(result, section); - isValid |= isValidFile; - } - return result; -} - -JsonNode JsonUtils::assembleFromFiles(const std::string & filename) -{ - JsonNode result; - JsonPath resID = JsonPath::builtinTODO(filename); - - for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) - { - // FIXME: some way to make this code more readable - auto stream = loader->load(resID); - std::unique_ptr textData(new ui8[stream->getSize()]); - stream->read(textData.get(), stream->getSize()); - - JsonNode section(reinterpret_cast(textData.get()), stream->getSize()); - merge(result, section); - } - return result; -} - -DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value) -{ - JsonNode node; - node.Bool() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::floatNode(double value) -{ - JsonNode node; - node.Float() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::stringNode(const std::string & value) -{ - JsonNode node; - node.String() = value; - return node; -} - -DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value) -{ - JsonNode node; - node.Integer() = value; - return node; -} - -VCMI_LIB_NAMESPACE_END +/* + * JsonNode.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 "JsonNode.h" + +#include "ScopeGuard.h" + +#include "bonuses/BonusParams.h" +#include "bonuses/Bonus.h" +#include "bonuses/Limiters.h" +#include "bonuses/Propagators.h" +#include "bonuses/Updaters.h" +#include "filesystem/Filesystem.h" +#include "modding/IdentifierStorage.h" +#include "VCMI_Lib.h" //for identifier resolution +#include "CGeneralTextHandler.h" +#include "JsonDetail.h" +#include "constants/StringConstants.h" +#include "battle/BattleHex.h" + +namespace +{ +// to avoid duplicating const and non-const code +template +Node & resolvePointer(Node & in, const std::string & pointer) +{ + if(pointer.empty()) + return in; + assert(pointer[0] == '/'); + + size_t splitPos = pointer.find('/', 1); + + std::string entry = pointer.substr(1, splitPos - 1); + std::string remainer = splitPos == std::string::npos ? "" : pointer.substr(splitPos); + + if(in.getType() == VCMI_LIB_WRAP_NAMESPACE(JsonNode)::JsonType::DATA_VECTOR) + { + if(entry.find_first_not_of("0123456789") != std::string::npos) // non-numbers in string + throw std::runtime_error("Invalid Json pointer"); + + if(entry.size() > 1 && entry[0] == '0') // leading zeros are not allowed + throw std::runtime_error("Invalid Json pointer"); + + auto index = boost::lexical_cast(entry); + + if (in.Vector().size() > index) + return in.Vector()[index].resolvePointer(remainer); + } + return in[entry].resolvePointer(remainer); +} +} + +VCMI_LIB_NAMESPACE_BEGIN + +using namespace JsonDetail; + +class LibClasses; +class CModHandler; + +static const JsonNode nullNode; + +JsonNode::JsonNode(JsonType Type): + type(JsonType::DATA_NULL) +{ + setType(Type); +} + +JsonNode::JsonNode(const char *data, size_t datasize): + type(JsonType::DATA_NULL) +{ + JsonParser parser(data, datasize); + *this = parser.parse(""); +} + +JsonNode::JsonNode(const JsonPath & fileURI): + type(JsonType::DATA_NULL) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI): +type(JsonType::DATA_NULL) +{ + auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); +} + +JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax): + type(JsonType::DATA_NULL) +{ + auto file = CResourceHandler::get()->load(fileURI)->readAll(); + + JsonParser parser(reinterpret_cast(file.first.get()), file.second); + *this = parser.parse(fileURI.getName()); + isValidSyntax = parser.isValid(); +} + +JsonNode::JsonNode(const JsonNode ©): + type(JsonType::DATA_NULL), + meta(copy.meta), + flags(copy.flags) +{ + setType(copy.getType()); + switch(type) + { + break; case JsonType::DATA_NULL: + break; case JsonType::DATA_BOOL: Bool() = copy.Bool(); + break; case JsonType::DATA_FLOAT: Float() = copy.Float(); + break; case JsonType::DATA_STRING: String() = copy.String(); + break; case JsonType::DATA_VECTOR: Vector() = copy.Vector(); + break; case JsonType::DATA_STRUCT: Struct() = copy.Struct(); + break; case JsonType::DATA_INTEGER:Integer() = copy.Integer(); + } +} + +JsonNode::~JsonNode() +{ + setType(JsonType::DATA_NULL); +} + +void JsonNode::swap(JsonNode &b) +{ + using std::swap; + swap(meta, b.meta); + swap(data, b.data); + swap(type, b.type); + swap(flags, b.flags); +} + +JsonNode & JsonNode::operator =(JsonNode node) +{ + swap(node); + return *this; +} + +bool JsonNode::operator == (const JsonNode &other) const +{ + if (getType() == other.getType()) + { + switch(type) + { + case JsonType::DATA_NULL: return true; + case JsonType::DATA_BOOL: return Bool() == other.Bool(); + case JsonType::DATA_FLOAT: return Float() == other.Float(); + case JsonType::DATA_STRING: return String() == other.String(); + case JsonType::DATA_VECTOR: return Vector() == other.Vector(); + case JsonType::DATA_STRUCT: return Struct() == other.Struct(); + case JsonType::DATA_INTEGER:return Integer()== other.Integer(); + } + } + return false; +} + +bool JsonNode::operator != (const JsonNode &other) const +{ + return !(*this == other); +} + +JsonNode::JsonType JsonNode::getType() const +{ + return type; +} + +void JsonNode::setMeta(const std::string & metadata, bool recursive) +{ + meta = metadata; + if (recursive) + { + switch (type) + { + break; case JsonType::DATA_VECTOR: + { + for(auto & node : Vector()) + { + node.setMeta(metadata); + } + } + break; case JsonType::DATA_STRUCT: + { + for(auto & node : Struct()) + { + node.second.setMeta(metadata); + } + } + } + } +} + +void JsonNode::setType(JsonType Type) +{ + if (type == Type) + return; + + //float<->int conversion + if(type == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) + { + si64 converted = static_cast(data.Float); + type = Type; + data.Integer = converted; + return; + } + else if(type == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) + { + auto converted = static_cast(data.Integer); + type = Type; + data.Float = converted; + return; + } + + //Reset node to nullptr + if (Type != JsonType::DATA_NULL) + setType(JsonType::DATA_NULL); + + switch (type) + { + break; case JsonType::DATA_STRING: delete data.String; + break; case JsonType::DATA_VECTOR: delete data.Vector; + break; case JsonType::DATA_STRUCT: delete data.Struct; + break; default: + break; + } + //Set new node type + type = Type; + switch(type) + { + break; case JsonType::DATA_NULL: + break; case JsonType::DATA_BOOL: data.Bool = false; + break; case JsonType::DATA_FLOAT: data.Float = 0; + break; case JsonType::DATA_STRING: data.String = new std::string(); + break; case JsonType::DATA_VECTOR: data.Vector = new JsonVector(); + break; case JsonType::DATA_STRUCT: data.Struct = new JsonMap(); + break; case JsonType::DATA_INTEGER: data.Integer = 0; + } +} + +bool JsonNode::isNull() const +{ + return type == JsonType::DATA_NULL; +} + +bool JsonNode::isNumber() const +{ + return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT; +} + +bool JsonNode::isString() const +{ + return type == JsonType::DATA_STRING; +} + +bool JsonNode::isVector() const +{ + return type == JsonType::DATA_VECTOR; +} + +bool JsonNode::isStruct() const +{ + return type == JsonType::DATA_STRUCT; +} + +bool JsonNode::containsBaseData() const +{ + switch(type) + { + case JsonType::DATA_NULL: + return false; + case JsonType::DATA_STRUCT: + for(const auto & elem : *data.Struct) + { + if(elem.second.containsBaseData()) + return true; + } + return false; + default: + //other types (including vector) cannot be extended via merge + return true; + } +} + +bool JsonNode::isCompact() const +{ + switch(type) + { + case JsonType::DATA_VECTOR: + for(JsonNode & elem : *data.Vector) + { + if(!elem.isCompact()) + return false; + } + return true; + case JsonType::DATA_STRUCT: + { + auto propertyCount = data.Struct->size(); + if(propertyCount == 0) + return true; + else if(propertyCount == 1) + return data.Struct->begin()->second.isCompact(); + } + return false; + default: + return true; + } +} + +bool JsonNode::TryBoolFromString(bool & success) const +{ + success = true; + if(type == JsonNode::JsonType::DATA_BOOL) + return Bool(); + + success = type == JsonNode::JsonType::DATA_STRING; + if(success) + { + auto boolParamStr = String(); + boost::algorithm::trim(boolParamStr); + boost::algorithm::to_lower(boolParamStr); + success = boolParamStr == "true"; + + if(success) + return true; + + success = boolParamStr == "false"; + } + return false; +} + +void JsonNode::clear() +{ + setType(JsonType::DATA_NULL); +} + +bool & JsonNode::Bool() +{ + setType(JsonType::DATA_BOOL); + return data.Bool; +} + +double & JsonNode::Float() +{ + setType(JsonType::DATA_FLOAT); + return data.Float; +} + +si64 & JsonNode::Integer() +{ + setType(JsonType::DATA_INTEGER); + return data.Integer; +} + +std::string & JsonNode::String() +{ + setType(JsonType::DATA_STRING); + return *data.String; +} + +JsonVector & JsonNode::Vector() +{ + setType(JsonType::DATA_VECTOR); + return *data.Vector; +} + +JsonMap & JsonNode::Struct() +{ + setType(JsonType::DATA_STRUCT); + return *data.Struct; +} + +const bool boolDefault = false; +bool JsonNode::Bool() const +{ + if (type == JsonType::DATA_NULL) + return boolDefault; + assert(type == JsonType::DATA_BOOL); + return data.Bool; +} + +const double floatDefault = 0; +double JsonNode::Float() const +{ + if(type == JsonType::DATA_NULL) + return floatDefault; + else if(type == JsonType::DATA_INTEGER) + return static_cast(data.Integer); + + assert(type == JsonType::DATA_FLOAT); + return data.Float; +} + +const si64 integetDefault = 0; +si64 JsonNode::Integer() const +{ + if(type == JsonType::DATA_NULL) + return integetDefault; + else if(type == JsonType::DATA_FLOAT) + return static_cast(data.Float); + + assert(type == JsonType::DATA_INTEGER); + return data.Integer; +} + +const std::string stringDefault = std::string(); +const std::string & JsonNode::String() const +{ + if (type == JsonType::DATA_NULL) + return stringDefault; + assert(type == JsonType::DATA_STRING); + return *data.String; +} + +const JsonVector vectorDefault = JsonVector(); +const JsonVector & JsonNode::Vector() const +{ + if (type == JsonType::DATA_NULL) + return vectorDefault; + assert(type == JsonType::DATA_VECTOR); + return *data.Vector; +} + +const JsonMap mapDefault = JsonMap(); +const JsonMap & JsonNode::Struct() const +{ + if (type == JsonType::DATA_NULL) + return mapDefault; + assert(type == JsonType::DATA_STRUCT); + return *data.Struct; +} + +JsonNode & JsonNode::operator[](const std::string & child) +{ + return Struct()[child]; +} + +const JsonNode & JsonNode::operator[](const std::string & child) const +{ + auto it = Struct().find(child); + if (it != Struct().end()) + return it->second; + return nullNode; +} + +JsonNode & JsonNode::operator[](size_t child) +{ + if (child >= Vector().size() ) + Vector().resize(child + 1); + return Vector()[child]; +} + +const JsonNode & JsonNode::operator[](size_t child) const +{ + if (child < Vector().size() ) + return Vector()[child]; + + return nullNode; +} + +const JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) const +{ + return ::resolvePointer(*this, jsonPointer); +} + +JsonNode & JsonNode::resolvePointer(const std::string &jsonPointer) +{ + return ::resolvePointer(*this, jsonPointer); +} + +std::string JsonNode::toJson(bool compact) const +{ + std::ostringstream out; + JsonWriter writer(out, compact); + writer.writeNode(*this); + return out.str(); +} + +///JsonUtils + +void JsonUtils::parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest) +{ + dest->val = static_cast(source[1].Float()); + resolveIdentifier(source[2],dest->subtype); + dest->additionalInfo = static_cast(source[3].Float()); + dest->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) + dest->turnsRemain = 0; +} + +std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) +{ + auto b = std::make_shared(); + std::string type = ability_vec[0].String(); + auto it = bonusNameMap.find(type); + if (it == bonusNameMap.end()) + { + logMod->error("Error: invalid ability type %s.", type); + return b; + } + b->type = it->second; + + parseTypedBonusShort(ability_vec, b); + return b; +} + +template +const T parseByMap(const std::map & map, const JsonNode * val, const std::string & err) +{ + static T defaultValue = T(); + if (!val->isNull()) + { + const std::string & type = val->String(); + auto it = map.find(type); + if (it == map.end()) + { + logMod->error("Error: invalid %s%s.", err, type); + return defaultValue; + } + else + { + return it->second; + } + } + else + return defaultValue; +} + +template +const T parseByMapN(const std::map & map, const JsonNode * val, const std::string & err) +{ + if(val->isNumber()) + return static_cast(val->Integer()); + else + return parseByMap(map, val, err); +} + +void JsonUtils::resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name) +{ + const JsonNode &value = node[name]; + if (!value.isNull()) + { + switch (value.getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var = static_cast(value.Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var = static_cast(value.Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) + { + var = identifier; + }); + break; + default: + logMod->error("Error! Wrong identifier used for value of %s.", name); + } + } +} + +void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) +{ + const JsonNode & value = node["addInfo"]; + if (!value.isNull()) + { + switch (value.getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var = static_cast(value.Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var = static_cast(value.Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) + { + var = identifier; + }); + break; + case JsonNode::JsonType::DATA_VECTOR: + { + const JsonVector & vec = value.Vector(); + var.resize(vec.size()); + for(int i = 0; i < vec.size(); i++) + { + switch(vec[i].getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var[i] = static_cast(vec[i].Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var[i] = static_cast(vec[i].Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(vec[i], [&var,i](si32 identifier) + { + var[i] = identifier; + }); + break; + default: + logMod->error("Error! Wrong identifier used for value of addInfo[%d].", i); + } + } + break; + } + default: + logMod->error("Error! Wrong identifier used for value of addInfo."); + } + } +} + +void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var) +{ + switch (node.getType()) + { + case JsonNode::JsonType::DATA_INTEGER: + var = static_cast(node.Integer()); + break; + case JsonNode::JsonType::DATA_FLOAT: + var = static_cast(node.Float()); + break; + case JsonNode::JsonType::DATA_STRING: + VLC->identifiers()->requestIdentifier(node, [&](si32 identifier) + { + var = identifier; + }); + break; + default: + logMod->error("Error! Wrong identifier used for identifier!"); + } +} + +std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) +{ + switch(limiter.getType()) + { + case JsonNode::JsonType::DATA_VECTOR: + { + const JsonVector & subLimiters = limiter.Vector(); + if(subLimiters.empty()) + { + logMod->warn("Warning: empty limiter list"); + return std::make_shared(); + } + std::shared_ptr result; + int offset = 1; + // determine limiter type and offset for sub-limiters + if(subLimiters[0].getType() == JsonNode::JsonType::DATA_STRING) + { + const std::string & aggregator = subLimiters[0].String(); + if(aggregator == AllOfLimiter::aggregator) + result = std::make_shared(); + else if(aggregator == AnyOfLimiter::aggregator) + result = std::make_shared(); + else if(aggregator == NoneOfLimiter::aggregator) + result = std::make_shared(); + } + if(!result) + { + // collapse for single limiter without explicit aggregate operator + if(subLimiters.size() == 1) + return parseLimiter(subLimiters[0]); + // implicit aggregator must be allOf + result = std::make_shared(); + offset = 0; + } + if(subLimiters.size() == offset) + logMod->warn("Warning: empty sub-limiter list"); + for(int sl = offset; sl < subLimiters.size(); ++sl) + result->add(parseLimiter(subLimiters[sl])); + return result; + } + break; + case JsonNode::JsonType::DATA_STRING: //pre-defined limiters + return parseByMap(bonusLimiterMap, &limiter, "limiter type "); + break; + case JsonNode::JsonType::DATA_STRUCT: //customizable limiters + { + std::string limiterType = limiter["type"].String(); + const JsonVector & parameters = limiter["parameters"].Vector(); + if(limiterType == "CREATURE_TYPE_LIMITER") + { + std::shared_ptr creatureLimiter = std::make_shared(); + VLC->identifiers()->requestIdentifier("creature", parameters[0], [=](si32 creature) + { + creatureLimiter->setCreature(CreatureID(creature)); + }); + auto includeUpgrades = false; + + if(parameters.size() > 1) + { + bool success = true; + includeUpgrades = parameters[1].TryBoolFromString(success); + + if(!success) + logMod->error("Second parameter of '%s' limiter should be Bool", limiterType); + } + creatureLimiter->includeUpgrades = includeUpgrades; + return creatureLimiter; + } + else if(limiterType == "HAS_ANOTHER_BONUS_LIMITER") + { + std::string anotherBonusType = parameters[0].String(); + auto it = bonusNameMap.find(anotherBonusType); + if(it == bonusNameMap.end()) + { + logMod->error("Error: invalid ability type %s.", anotherBonusType); + } + else + { + std::shared_ptr bonusLimiter = std::make_shared(); + bonusLimiter->type = it->second; + auto findSource = [&](const JsonNode & parameter) + { + if(parameter.getType() == JsonNode::JsonType::DATA_STRUCT) + { + auto sourceIt = bonusSourceMap.find(parameter["type"].String()); + if(sourceIt != bonusSourceMap.end()) + { + bonusLimiter->source = sourceIt->second; + bonusLimiter->isSourceRelevant = true; + if(!parameter["id"].isNull()) { + resolveIdentifier(parameter["id"], bonusLimiter->sid); + bonusLimiter->isSourceIDRelevant = true; + } + } + } + return false; + }; + if(parameters.size() > 1) + { + if(findSource(parameters[1]) && parameters.size() == 2) + return bonusLimiter; + else + { + resolveIdentifier(parameters[1], bonusLimiter->subtype); + bonusLimiter->isSubtypeRelevant = true; + if(parameters.size() > 2) + findSource(parameters[2]); + } + } + return bonusLimiter; + } + } + else if(limiterType == "CREATURE_ALIGNMENT_LIMITER") + { + int alignment = vstd::find_pos(GameConstants::ALIGNMENT_NAMES, parameters[0].String()); + if(alignment == -1) + logMod->error("Error: invalid alignment %s.", parameters[0].String()); + else + return std::make_shared(static_cast(alignment)); + } + else if(limiterType == "FACTION_LIMITER" || limiterType == "CREATURE_FACTION_LIMITER") //Second name is deprecated, 1.2 compat + { + std::shared_ptr factionLimiter = std::make_shared(); + VLC->identifiers()->requestIdentifier("faction", parameters[0], [=](si32 faction) + { + factionLimiter->faction = FactionID(faction); + }); + return factionLimiter; + } + else if(limiterType == "CREATURE_LEVEL_LIMITER") + { + auto levelLimiter = std::make_shared(); + if(!parameters.empty()) //If parameters is empty, level limiter works as CREATURES_ONLY limiter + { + levelLimiter->minLevel = parameters[0].Integer(); + if(parameters[1].isNumber()) + levelLimiter->maxLevel = parameters[1].Integer(); + } + return levelLimiter; + } + else if(limiterType == "CREATURE_TERRAIN_LIMITER") + { + std::shared_ptr terrainLimiter = std::make_shared(); + if(!parameters.empty()) + { + VLC->identifiers()->requestIdentifier("terrain", parameters[0], [=](si32 terrain) + { + //TODO: support limiters + //terrainLimiter->terrainType = terrain; + }); + } + return terrainLimiter; + } + else if(limiterType == "UNIT_ON_HEXES") { + auto hexLimiter = std::make_shared(); + if(!parameters.empty()) + { + for (const auto & parameter: parameters){ + if(parameter.isNumber()) + hexLimiter->applicableHexes.insert(BattleHex(parameter.Integer())); + } + } + return hexLimiter; + } + else + { + logMod->error("Error: invalid customizable limiter type %s.", limiterType); + } + } + break; + default: + break; + } + return nullptr; +} + +std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) +{ + auto b = std::make_shared(); + if (!parseBonus(ability, b.get())) + { + // caller code can not handle this case and presumes that returned bonus is always valid + logGlobal->error("Failed to parse bonus! Json config was %S ", ability.toJson()); + + b->type = BonusType::NONE; + assert(0); // or throw? Game *should* work with dummy bonus + + return b; + } + return b; +} + +std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description) +{ + /* duration = BonusDuration::PERMANENT + source = BonusSource::TOWN_STRUCTURE + bonusType, val, subtype - get from json + */ + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description, -1); + + if(!parseBonus(ability, b.get())) + return nullptr; + return b; +} + +static BonusParams convertDeprecatedBonus(const JsonNode &ability) +{ + if(vstd::contains(deprecatedBonusSet, ability["type"].String())) + { + logMod->warn("There is deprecated bonus found:\n%s\nTrying to convert...", ability.toJson()); + auto params = BonusParams(ability["type"].String(), + ability["subtype"].isString() ? ability["subtype"].String() : "", + ability["subtype"].isNumber() ? ability["subtype"].Integer() : -1); + if(params.isConverted) + { + if(ability["type"].String() == "SECONDARY_SKILL_PREMY" && bonusValueMap.find(ability["valueType"].String())->second == BonusValueType::PERCENT_TO_BASE) //assume secondary skill special + { + params.valueType = BonusValueType::PERCENT_TO_TARGET_TYPE; + params.targetType = BonusSource::SECONDARY_SKILL; + } + + logMod->warn("Please, use this bonus:\n%s\nConverted successfully!", params.toJson().toJson()); + return params; + } + else + logMod->error("Cannot convert bonus!\n%s", ability.toJson()); + } + BonusParams ret; + ret.isConverted = false; + return ret; +} + +static TUpdaterPtr parseUpdater(const JsonNode & updaterJson) +{ + switch(updaterJson.getType()) + { + case JsonNode::JsonType::DATA_STRING: + return parseByMap(bonusUpdaterMap, &updaterJson, "updater type "); + break; + case JsonNode::JsonType::DATA_STRUCT: + if(updaterJson["type"].String() == "GROWS_WITH_LEVEL") + { + std::shared_ptr updater = std::make_shared(); + const JsonVector param = updaterJson["parameters"].Vector(); + updater->valPer20 = static_cast(param[0].Integer()); + if(param.size() > 1) + updater->stepSize = static_cast(param[1].Integer()); + return updater; + } + else if (updaterJson["type"].String() == "ARMY_MOVEMENT") + { + std::shared_ptr updater = std::make_shared(); + if(updaterJson["parameters"].isVector()) + { + const auto & param = updaterJson["parameters"].Vector(); + if(param.size() < 4) + logMod->warn("Invalid ARMY_MOVEMENT parameters, using default!"); + else + { + updater->base = static_cast(param.at(0).Integer()); + updater->divider = static_cast(param.at(1).Integer()); + updater->multiplier = static_cast(param.at(2).Integer()); + updater->max = static_cast(param.at(3).Integer()); + } + return updater; + } + } + else + logMod->warn("Unknown updater type \"%s\"", updaterJson["type"].String()); + break; + } + return nullptr; +} + +bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) +{ + const JsonNode * value = nullptr; + + std::string type = ability["type"].String(); + auto it = bonusNameMap.find(type); + auto params = std::make_unique(false); + if (it == bonusNameMap.end()) + { + params = std::make_unique(convertDeprecatedBonus(ability)); + if(!params->isConverted) + { + logMod->error("Error: invalid ability type %s.", type); + return false; + } + b->type = params->type; + b->val = params->val.value_or(0); + b->valType = params->valueType.value_or(BonusValueType::ADDITIVE_VALUE); + if(params->targetType) + b->targetSourceType = params->targetType.value(); + } + else + b->type = it->second; + + resolveIdentifier(b->subtype, params->isConverted ? params->toJson() : ability, "subtype"); + + if(!params->isConverted) + { + b->val = static_cast(ability["val"].Float()); + + value = &ability["valueType"]; + if (!value->isNull()) + b->valType = static_cast(parseByMapN(bonusValueMap, value, "value type ")); + } + + b->stacking = ability["stacking"].String(); + + resolveAddInfo(b->additionalInfo, ability); + + b->turnsRemain = static_cast(ability["turns"].Float()); + + b->sid = static_cast(ability["sourceID"].Float()); + + if(!ability["description"].isNull()) + { + if (ability["description"].isString()) + b->description = ability["description"].String(); + if (ability["description"].isNumber()) + b->description = VLC->generaltexth->translate("core.arraytxt", ability["description"].Integer()); + } + + value = &ability["effectRange"]; + if (!value->isNull()) + b->effectRange = static_cast(parseByMapN(bonusLimitEffect, value, "effect range ")); + + value = &ability["duration"]; + if (!value->isNull()) + { + switch (value->getType()) + { + case JsonNode::JsonType::DATA_STRING: + b->duration = parseByMap(bonusDurationMap, value, "duration type "); + break; + case JsonNode::JsonType::DATA_VECTOR: + { + BonusDuration::Type dur = 0; + for (const JsonNode & d : value->Vector()) + dur |= parseByMapN(bonusDurationMap, &d, "duration type "); + b->duration = dur; + } + break; + default: + logMod->error("Error! Wrong bonus duration format."); + } + } + + value = &ability["sourceType"]; + if (!value->isNull()) + b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); + + value = &ability["targetSourceType"]; + if (!value->isNull()) + b->targetSourceType = static_cast(parseByMap(bonusSourceMap, value, "target type ")); + + value = &ability["limiters"]; + if (!value->isNull()) + b->limiter = parseLimiter(*value); + + value = &ability["propagator"]; + if (!value->isNull()) + { + //ALL_CREATURES old propagator compatibility + if(value->String() == "ALL_CREATURES") + { + logMod->warn("ALL_CREATURES propagator is deprecated. Use GLOBAL_EFFECT propagator with CREATURES_ONLY limiter"); + b->addLimiter(std::make_shared()); + b->propagator = bonusPropagatorMap.at("GLOBAL_EFFECT"); + } + else + b->propagator = parseByMap(bonusPropagatorMap, value, "propagator type "); + } + + value = &ability["updater"]; + if(!value->isNull()) + b->addUpdater(parseUpdater(*value)); + value = &ability["propagationUpdater"]; + if(!value->isNull()) + b->propagationUpdater = parseUpdater(*value); + return true; +} + +CSelector JsonUtils::parseSelector(const JsonNode & ability) +{ + CSelector ret = Selector::all; + + // Recursive parsers for anyOf, allOf, noneOf + const auto * value = &ability["allOf"]; + if(value->isVector()) + { + for(const auto & andN : value->Vector()) + ret = ret.And(parseSelector(andN)); + } + + value = &ability["anyOf"]; + if(value->isVector()) + { + CSelector base = Selector::none; + for(const auto & andN : value->Vector()) + base.Or(parseSelector(andN)); + + ret = ret.And(base); + } + + value = &ability["noneOf"]; + if(value->isVector()) + { + CSelector base = Selector::none; + for(const auto & andN : value->Vector()) + base.Or(parseSelector(andN)); + + ret = ret.And(base.Not()); + } + + // Actual selector parser + value = &ability["type"]; + if(value->isString()) + { + auto it = bonusNameMap.find(value->String()); + if(it != bonusNameMap.end()) + ret = ret.And(Selector::type()(it->second)); + } + value = &ability["subtype"]; + if(!value->isNull()) + { + TBonusSubtype subtype; + resolveIdentifier(subtype, ability, "subtype"); + ret = ret.And(Selector::subtype()(subtype)); + } + value = &ability["sourceType"]; + std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized + std::optional id = std::nullopt; + if(value->isString()) + { + auto it = bonusSourceMap.find(value->String()); + if(it != bonusSourceMap.end()) + src = it->second; + } + + value = &ability["sourceID"]; + if(!value->isNull()) + { + id = -1; + resolveIdentifier(*id, ability, "sourceID"); + } + + if(src && id) + ret = ret.And(Selector::source(*src, *id)); + else if(src) + ret = ret.And(Selector::sourceTypeSel(*src)); + + + value = &ability["targetSourceType"]; + if(value->isString()) + { + auto it = bonusSourceMap.find(value->String()); + if(it != bonusSourceMap.end()) + ret = ret.And(Selector::targetSourceType()(it->second)); + } + value = &ability["valueType"]; + if(value->isString()) + { + auto it = bonusValueMap.find(value->String()); + if(it != bonusValueMap.end()) + ret = ret.And(Selector::valueType(it->second)); + } + CAddInfo info; + value = &ability["addInfo"]; + if(!value->isNull()) + { + resolveAddInfo(info, ability["addInfo"]); + ret = ret.And(Selector::info()(info)); + } + value = &ability["effectRange"]; + if(value->isString()) + { + auto it = bonusLimitEffect.find(value->String()); + if(it != bonusLimitEffect.end()) + ret = ret.And(Selector::effectRange()(it->second)); + } + value = &ability["lastsTurns"]; + if(value->isNumber()) + ret = ret.And(Selector::turns(value->Integer())); + value = &ability["lastsDays"]; + if(value->isNumber()) + ret = ret.And(Selector::days(value->Integer())); + + return ret; +} + +//returns first Key with value equal to given one +template +Key reverseMapFirst(const Val & val, const std::map & map) +{ + for(auto it : map) + { + if(it.second == val) + { + return it.first; + } + } + assert(0); + return ""; +} + +static JsonNode getDefaultValue(const JsonNode & schema, std::string fieldName) +{ + const JsonNode & fieldProps = schema["properties"][fieldName]; + +#if defined(VCMI_IOS) + if (!fieldProps["defaultIOS"].isNull()) + return fieldProps["defaultIOS"]; +#elif defined(VCMI_ANDROID) + if (!fieldProps["defaultAndroid"].isNull()) + return fieldProps["defaultAndroid"]; +#elif !defined(VCMI_MOBILE) + if (!fieldProps["defaultDesktop"].isNull()) + return fieldProps["defaultDesktop"]; +#endif + return fieldProps["default"]; +} + +static void eraseOptionalNodes(JsonNode & node, const JsonNode & schema) +{ + assert(schema["type"].String() == "object"); + + std::set foundEntries; + + for(const auto & entry : schema["required"].Vector()) + foundEntries.insert(entry.String()); + + vstd::erase_if(node.Struct(), [&](const auto & node){ + return !vstd::contains(foundEntries, node.first); + }); +} + +static void minimizeNode(JsonNode & node, const JsonNode & schema) +{ + if (schema["type"].String() != "object") + return; + + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + minimizeNode(node[name], schema["properties"][name]); + + if (vstd::contains(node.Struct(), name) && node[name] == getDefaultValue(schema, name)) + node.Struct().erase(name); + } + eraseOptionalNodes(node, schema); +} + +static void maximizeNode(JsonNode & node, const JsonNode & schema) +{ + // "required" entry can only be found in object/struct + if (schema["type"].String() != "object") + return; + + // check all required entries that have default version + for(const auto & entry : schema["required"].Vector()) + { + const std::string & name = entry.String(); + + if (node[name].isNull() && !getDefaultValue(schema, name).isNull()) + node[name] = getDefaultValue(schema, name); + + maximizeNode(node[name], schema["properties"][name]); + } + + eraseOptionalNodes(node, schema); +} + +void JsonUtils::minimize(JsonNode & node, const std::string & schemaName) +{ + minimizeNode(node, getSchema(schemaName)); +} + +void JsonUtils::maximize(JsonNode & node, const std::string & schemaName) +{ + maximizeNode(node, getSchema(schemaName)); +} + +bool JsonUtils::validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName) +{ + std::string log = Validation::check(schemaName, node); + if (!log.empty()) + { + logMod->warn("Data in %s is invalid!", dataName); + logMod->warn(log); + logMod->trace("%s json: %s", dataName, node.toJson(true)); + } + return log.empty(); +} + +const JsonNode & getSchemaByName(const std::string & name) +{ + // cached schemas to avoid loading json data multiple times + static std::map loadedSchemas; + + if (vstd::contains(loadedSchemas, name)) + return loadedSchemas[name]; + + auto filename = JsonPath::builtin("config/schemas/" + name); + + if (CResourceHandler::get()->existsResource(filename)) + { + loadedSchemas[name] = JsonNode(filename); + return loadedSchemas[name]; + } + + logMod->error("Error: missing schema with name %s!", name); + assert(0); + return nullNode; +} + +const JsonNode & JsonUtils::getSchema(const std::string & URI) +{ + size_t posColon = URI.find(':'); + size_t posHash = URI.find('#'); + std::string filename; + if(posColon == std::string::npos) + { + filename = URI.substr(0, posHash); + } + else + { + std::string protocolName = URI.substr(0, posColon); + filename = URI.substr(posColon + 1, posHash - posColon - 1) + ".json"; + if(protocolName != "vcmi") + { + logMod->error("Error: unsupported URI protocol for schema: %s", URI); + return nullNode; + } + } + + // check if json pointer if present (section after hash in string) + if(posHash == std::string::npos || posHash == URI.size() - 1) + { + auto const & result = getSchemaByName(filename); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } + else + { + auto const & result = getSchemaByName(filename).resolvePointer(URI.substr(posHash + 1)); + if (result.isNull()) + logMod->error("Error: missing schema %s", URI); + return result; + } +} + +void JsonUtils::merge(JsonNode & dest, JsonNode & source, bool ignoreOverride, bool copyMeta) +{ + if (dest.getType() == JsonNode::JsonType::DATA_NULL) + { + std::swap(dest, source); + return; + } + + switch (source.getType()) + { + case JsonNode::JsonType::DATA_NULL: + { + dest.clear(); + break; + } + case JsonNode::JsonType::DATA_BOOL: + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + case JsonNode::JsonType::DATA_STRING: + case JsonNode::JsonType::DATA_VECTOR: + { + std::swap(dest, source); + break; + } + case JsonNode::JsonType::DATA_STRUCT: + { + if(!ignoreOverride && vstd::contains(source.flags, "override")) + { + std::swap(dest, source); + } + else + { + if (copyMeta) + dest.meta = source.meta; + + //recursively merge all entries from struct + for(auto & node : source.Struct()) + merge(dest[node.first], node.second, ignoreOverride); + } + } + } +} + +void JsonUtils::mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride, bool copyMeta) +{ + // uses copy created in stack to safely merge two nodes + merge(dest, source, ignoreOverride, copyMeta); +} + +void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) +{ + JsonNode inheritedNode(base); + merge(inheritedNode, descendant, true, true); + descendant.swap(inheritedNode); +} + +JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) +{ + if(nodes.empty()) + return nullNode; + + JsonNode result = nodes[0]; + for(int i = 1; i < nodes.size(); i++) + { + if(result.isNull()) + break; + result = JsonUtils::intersect(result, nodes[i], pruneEmpty); + } + return result; +} + +JsonNode JsonUtils::intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty) +{ + if(a.getType() == JsonNode::JsonType::DATA_STRUCT && b.getType() == JsonNode::JsonType::DATA_STRUCT) + { + // intersect individual properties + JsonNode result(JsonNode::JsonType::DATA_STRUCT); + for(const auto & property : a.Struct()) + { + if(vstd::contains(b.Struct(), property.first)) + { + JsonNode propertyIntersect = JsonUtils::intersect(property.second, b.Struct().find(property.first)->second); + if(pruneEmpty && !propertyIntersect.containsBaseData()) + continue; + result[property.first] = propertyIntersect; + } + } + return result; + } + else + { + // not a struct - same or different, no middle ground + if(a == b) + return a; + } + return nullNode; +} + +JsonNode JsonUtils::difference(const JsonNode & node, const JsonNode & base) +{ + auto addsInfo = [](JsonNode diff) -> bool + { + switch(diff.getType()) + { + case JsonNode::JsonType::DATA_NULL: + return false; + case JsonNode::JsonType::DATA_STRUCT: + return !diff.Struct().empty(); + default: + return true; + } + }; + + if(node.getType() == JsonNode::JsonType::DATA_STRUCT && base.getType() == JsonNode::JsonType::DATA_STRUCT) + { + // subtract individual properties + JsonNode result(JsonNode::JsonType::DATA_STRUCT); + for(const auto & property : node.Struct()) + { + if(vstd::contains(base.Struct(), property.first)) + { + const JsonNode propertyDifference = JsonUtils::difference(property.second, base.Struct().find(property.first)->second); + if(addsInfo(propertyDifference)) + result[property.first] = propertyDifference; + } + else + { + result[property.first] = property.second; + } + } + return result; + } + else + { + if(node == base) + return nullNode; + } + return node; +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files) +{ + bool isValid = false; + return assembleFromFiles(files, isValid); +} + +JsonNode JsonUtils::assembleFromFiles(const std::vector & files, bool & isValid) +{ + isValid = true; + JsonNode result; + + for(const auto & file : files) + { + bool isValidFile = false; + JsonNode section(JsonPath::builtinTODO(file), isValidFile); + merge(result, section); + isValid |= isValidFile; + } + return result; +} + +JsonNode JsonUtils::assembleFromFiles(const std::string & filename) +{ + JsonNode result; + JsonPath resID = JsonPath::builtinTODO(filename); + + for(auto & loader : CResourceHandler::get()->getResourcesWithName(resID)) + { + // FIXME: some way to make this code more readable + auto stream = loader->load(resID); + std::unique_ptr textData(new ui8[stream->getSize()]); + stream->read(textData.get(), stream->getSize()); + + JsonNode section(reinterpret_cast(textData.get()), stream->getSize()); + merge(result, section); + } + return result; +} + +DLL_LINKAGE JsonNode JsonUtils::boolNode(bool value) +{ + JsonNode node; + node.Bool() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::floatNode(double value) +{ + JsonNode node; + node.Float() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::stringNode(const std::string & value) +{ + JsonNode node; + node.String() = value; + return node; +} + +DLL_LINKAGE JsonNode JsonUtils::intNode(si64 value) +{ + JsonNode node; + node.Integer() = value; + return node; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 97833d7ff..dfe0529d5 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -1,371 +1,371 @@ -/* - * JsonNode.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 "GameConstants.h" -#include "filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class JsonNode; -using JsonMap = std::map; -using JsonVector = std::vector; - -struct Bonus; -class CSelector; -class CAddInfo; -class ILimiter; - -class DLL_LINKAGE JsonNode -{ -public: - enum class JsonType - { - DATA_NULL, - DATA_BOOL, - DATA_FLOAT, - DATA_STRING, - DATA_VECTOR, - DATA_STRUCT, - DATA_INTEGER - }; - -private: - union JsonData - { - bool Bool; - double Float; - std::string* String; - JsonVector* Vector; - JsonMap* Struct; - si64 Integer; - }; - - JsonType type; - JsonData data; - -public: - /// free to use metadata fields - std::string meta; - // meta-flags like override - std::vector flags; - - //Create empty node - JsonNode(JsonType Type = JsonType::DATA_NULL); - //Create tree from Json-formatted input - explicit JsonNode(const char * data, size_t datasize); - //Create tree from JSON file - explicit JsonNode(const JsonPath & fileURI); - explicit JsonNode(const std::string & modName, const JsonPath & fileURI); - explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); - //Copy c-tor - JsonNode(const JsonNode ©); - - ~JsonNode(); - - void swap(JsonNode &b); - JsonNode& operator =(JsonNode node); - - bool operator == (const JsonNode &other) const; - bool operator != (const JsonNode &other) const; - - void setMeta(const std::string & metadata, bool recursive = true); - - /// Convert node to another type. Converting to nullptr will clear all data - void setType(JsonType Type); - JsonType getType() const; - - bool isNull() const; - bool isNumber() const; - bool isString() const; - bool isVector() const; - bool isStruct() const; - /// true if node contains not-null data that cannot be extended via merging - /// used for generating common base node from multiple nodes (e.g. bonuses) - bool containsBaseData() const; - bool isCompact() const; - /// removes all data from node and sets type to null - void clear(); - - /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise - bool TryBoolFromString(bool & success) const; - - /// non-const accessors, node will change type on type mismatch - bool & Bool(); - double & Float(); - si64 & Integer(); - std::string & String(); - JsonVector & Vector(); - JsonMap & Struct(); - - /// const accessors, will cause assertion failure on type mismatch - bool Bool() const; - ///float and integer allowed - double Float() const; - ///only integer allowed - si64 Integer() const; - const std::string & String() const; - const JsonVector & Vector() const; - const JsonMap & Struct() const; - - /// returns resolved "json pointer" (string in format "/path/to/node") - const JsonNode & resolvePointer(const std::string & jsonPointer) const; - JsonNode & resolvePointer(const std::string & jsonPointer); - - /// convert json tree into specified type. Json tree must have same type as Type - /// Valid types: bool, string, any numeric, map and vector - /// example: convertTo< std::map< std::vector > >(); - template - Type convertTo() const; - - //operator [], for structs only - get child node by name - JsonNode & operator[](const std::string & child); - const JsonNode & operator[](const std::string & child) const; - - JsonNode & operator[](size_t child); - const JsonNode & operator[](size_t child) const; - - std::string toJson(bool compact = false) const; - - template void serialize(Handler &h, const int version) - { - h & meta; - h & flags; - h & type; - switch(type) - { - case JsonType::DATA_NULL: - break; - case JsonType::DATA_BOOL: - h & data.Bool; - break; - case JsonType::DATA_FLOAT: - h & data.Float; - break; - case JsonType::DATA_STRING: - h & data.String; - break; - case JsonType::DATA_VECTOR: - h & data.Vector; - break; - case JsonType::DATA_STRUCT: - h & data.Struct; - break; - case JsonType::DATA_INTEGER: - h & data.Integer; - break; - } - } -}; - -namespace JsonUtils -{ - /** - * @brief parse short bonus format, excluding type - * @note sets duration to Permament - */ - DLL_LINKAGE void parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest); - - /// - DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); - DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); - DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description); - DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); - DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); - DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); - DLL_LINKAGE void resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name); - DLL_LINKAGE void resolveIdentifier(const JsonNode & node, si32 & var); - DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will destroy data in source - */ - DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); - - /** - * @brief recursively merges source into dest, replacing identical fields - * struct : recursively calls this function - * arrays : each entry will be merged recursively - * values : value in source will replace value in dest - * null : if value in source is present but set to null it will delete entry in dest - * @note this function will preserve data stored in source by creating copy - */ - DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); - - /** @brief recursively merges descendant into copy of base node - * Result emulates inheritance semantic - */ - DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); - - /** - * @brief construct node representing the common structure of input nodes - * @param pruneEmpty - omit common properties whose intersection is empty - * different types: null - * struct: recursive intersect on common properties - * other: input if equal, null otherwise - */ - DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true); - DLL_LINKAGE JsonNode intersect(const std::vector & nodes, bool pruneEmpty = true); - - /** - * @brief construct node representing the difference "node - base" - * merging difference with base gives node - */ - DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base); - - /** - * @brief generate one Json structure from multiple files - * @param files - list of filenames with parts of json structure - */ - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); - DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); - - /// This version loads all files with same name (overridden by mods) - DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); - - /** - * @brief removes all nodes that are identical to default entry in schema - * @param node - JsonNode to minimize - * @param schemaName - name of schema to use - * @note for minimizing data must be valid against given schema - */ - DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); - /// opposed to minimize, adds all missing, required entries that have default value - DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); - - /** - * @brief validate node against specified schema - * @param node - JsonNode to check - * @param schemaName - name of schema to use - * @param dataName - some way to identify data (printed in console in case of errors) - * @returns true if data in node fully compilant with schema - */ - DLL_LINKAGE bool validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName); - - /// get schema by json URI: vcmi:# - /// example: schema "vcmi:settings" is used to check user settings - DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); - - /// for easy construction of JsonNodes; helps with inserting primitives into vector node - DLL_LINKAGE JsonNode boolNode(bool value); - DLL_LINKAGE JsonNode floatNode(double value); - DLL_LINKAGE JsonNode stringNode(const std::string & value); - DLL_LINKAGE JsonNode intNode(si64 value); -} - -namespace JsonDetail -{ - // conversion helpers for JsonNode::convertTo (partial template function instantiation is illegal in c++) - - template - struct JsonConvImpl; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T((int)node.Float()); - } - }; - - template - struct JsonConvImpl - { - static T convertImpl(const JsonNode & node) - { - return T(node.Float()); - } - }; - - template - struct JsonConverter - { - static Type convert(const JsonNode & node) - { - ///this should be triggered only for numeric types and enums - static_assert(boost::mpl::or_, std::is_enum, boost::is_class >::value, "Unsupported type for JsonNode::convertTo()!"); - return JsonConvImpl, boost::is_class >::value >::convertImpl(node); - - } - }; - - template - struct JsonConverter > - { - static std::map convert(const JsonNode & node) - { - std::map ret; - for (const JsonMap::value_type & entry : node.Struct()) - { - ret.insert(entry.first, entry.second.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::set convert(const JsonNode & node) - { - std::set ret; - for(const JsonVector::value_type & entry : node.Vector()) - { - ret.insert(entry.convertTo()); - } - return ret; - } - }; - - template - struct JsonConverter > - { - static std::vector convert(const JsonNode & node) - { - std::vector ret; - for (const JsonVector::value_type & entry: node.Vector()) - { - ret.push_back(entry.convertTo()); - } - return ret; - } - }; - - template<> - struct JsonConverter - { - static std::string convert(const JsonNode & node) - { - return node.String(); - } - }; - - template<> - struct JsonConverter - { - static bool convert(const JsonNode & node) - { - return node.Bool(); - } - }; -} - -template -Type JsonNode::convertTo() const -{ - return JsonDetail::JsonConverter::convert(*this); -} - -VCMI_LIB_NAMESPACE_END +/* + * JsonNode.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 "GameConstants.h" +#include "filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class JsonNode; +using JsonMap = std::map; +using JsonVector = std::vector; + +struct Bonus; +class CSelector; +class CAddInfo; +class ILimiter; + +class DLL_LINKAGE JsonNode +{ +public: + enum class JsonType + { + DATA_NULL, + DATA_BOOL, + DATA_FLOAT, + DATA_STRING, + DATA_VECTOR, + DATA_STRUCT, + DATA_INTEGER + }; + +private: + union JsonData + { + bool Bool; + double Float; + std::string* String; + JsonVector* Vector; + JsonMap* Struct; + si64 Integer; + }; + + JsonType type; + JsonData data; + +public: + /// free to use metadata fields + std::string meta; + // meta-flags like override + std::vector flags; + + //Create empty node + JsonNode(JsonType Type = JsonType::DATA_NULL); + //Create tree from Json-formatted input + explicit JsonNode(const char * data, size_t datasize); + //Create tree from JSON file + explicit JsonNode(const JsonPath & fileURI); + explicit JsonNode(const std::string & modName, const JsonPath & fileURI); + explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); + //Copy c-tor + JsonNode(const JsonNode ©); + + ~JsonNode(); + + void swap(JsonNode &b); + JsonNode& operator =(JsonNode node); + + bool operator == (const JsonNode &other) const; + bool operator != (const JsonNode &other) const; + + void setMeta(const std::string & metadata, bool recursive = true); + + /// Convert node to another type. Converting to nullptr will clear all data + void setType(JsonType Type); + JsonType getType() const; + + bool isNull() const; + bool isNumber() const; + bool isString() const; + bool isVector() const; + bool isStruct() const; + /// true if node contains not-null data that cannot be extended via merging + /// used for generating common base node from multiple nodes (e.g. bonuses) + bool containsBaseData() const; + bool isCompact() const; + /// removes all data from node and sets type to null + void clear(); + + /// returns bool or bool equivalent of string value if 'success' is true, or false otherwise + bool TryBoolFromString(bool & success) const; + + /// non-const accessors, node will change type on type mismatch + bool & Bool(); + double & Float(); + si64 & Integer(); + std::string & String(); + JsonVector & Vector(); + JsonMap & Struct(); + + /// const accessors, will cause assertion failure on type mismatch + bool Bool() const; + ///float and integer allowed + double Float() const; + ///only integer allowed + si64 Integer() const; + const std::string & String() const; + const JsonVector & Vector() const; + const JsonMap & Struct() const; + + /// returns resolved "json pointer" (string in format "/path/to/node") + const JsonNode & resolvePointer(const std::string & jsonPointer) const; + JsonNode & resolvePointer(const std::string & jsonPointer); + + /// convert json tree into specified type. Json tree must have same type as Type + /// Valid types: bool, string, any numeric, map and vector + /// example: convertTo< std::map< std::vector > >(); + template + Type convertTo() const; + + //operator [], for structs only - get child node by name + JsonNode & operator[](const std::string & child); + const JsonNode & operator[](const std::string & child) const; + + JsonNode & operator[](size_t child); + const JsonNode & operator[](size_t child) const; + + std::string toJson(bool compact = false) const; + + template void serialize(Handler &h, const int version) + { + h & meta; + h & flags; + h & type; + switch(type) + { + case JsonType::DATA_NULL: + break; + case JsonType::DATA_BOOL: + h & data.Bool; + break; + case JsonType::DATA_FLOAT: + h & data.Float; + break; + case JsonType::DATA_STRING: + h & data.String; + break; + case JsonType::DATA_VECTOR: + h & data.Vector; + break; + case JsonType::DATA_STRUCT: + h & data.Struct; + break; + case JsonType::DATA_INTEGER: + h & data.Integer; + break; + } + } +}; + +namespace JsonUtils +{ + /** + * @brief parse short bonus format, excluding type + * @note sets duration to Permament + */ + DLL_LINKAGE void parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest); + + /// + DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); + DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); + DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description); + DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); + DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); + DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); + DLL_LINKAGE void resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name); + DLL_LINKAGE void resolveIdentifier(const JsonNode & node, si32 & var); + DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); + + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will destroy data in source + */ + DLL_LINKAGE void merge(JsonNode & dest, JsonNode & source, bool ignoreOverride = false, bool copyMeta = false); + + /** + * @brief recursively merges source into dest, replacing identical fields + * struct : recursively calls this function + * arrays : each entry will be merged recursively + * values : value in source will replace value in dest + * null : if value in source is present but set to null it will delete entry in dest + * @note this function will preserve data stored in source by creating copy + */ + DLL_LINKAGE void mergeCopy(JsonNode & dest, JsonNode source, bool ignoreOverride = false, bool copyMeta = false); + + /** @brief recursively merges descendant into copy of base node + * Result emulates inheritance semantic + */ + DLL_LINKAGE void inherit(JsonNode & descendant, const JsonNode & base); + + /** + * @brief construct node representing the common structure of input nodes + * @param pruneEmpty - omit common properties whose intersection is empty + * different types: null + * struct: recursive intersect on common properties + * other: input if equal, null otherwise + */ + DLL_LINKAGE JsonNode intersect(const JsonNode & a, const JsonNode & b, bool pruneEmpty = true); + DLL_LINKAGE JsonNode intersect(const std::vector & nodes, bool pruneEmpty = true); + + /** + * @brief construct node representing the difference "node - base" + * merging difference with base gives node + */ + DLL_LINKAGE JsonNode difference(const JsonNode & node, const JsonNode & base); + + /** + * @brief generate one Json structure from multiple files + * @param files - list of filenames with parts of json structure + */ + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files); + DLL_LINKAGE JsonNode assembleFromFiles(const std::vector & files, bool & isValid); + + /// This version loads all files with same name (overridden by mods) + DLL_LINKAGE JsonNode assembleFromFiles(const std::string & filename); + + /** + * @brief removes all nodes that are identical to default entry in schema + * @param node - JsonNode to minimize + * @param schemaName - name of schema to use + * @note for minimizing data must be valid against given schema + */ + DLL_LINKAGE void minimize(JsonNode & node, const std::string & schemaName); + /// opposed to minimize, adds all missing, required entries that have default value + DLL_LINKAGE void maximize(JsonNode & node, const std::string & schemaName); + + /** + * @brief validate node against specified schema + * @param node - JsonNode to check + * @param schemaName - name of schema to use + * @param dataName - some way to identify data (printed in console in case of errors) + * @returns true if data in node fully compilant with schema + */ + DLL_LINKAGE bool validate(const JsonNode & node, const std::string & schemaName, const std::string & dataName); + + /// get schema by json URI: vcmi:# + /// example: schema "vcmi:settings" is used to check user settings + DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); + + /// for easy construction of JsonNodes; helps with inserting primitives into vector node + DLL_LINKAGE JsonNode boolNode(bool value); + DLL_LINKAGE JsonNode floatNode(double value); + DLL_LINKAGE JsonNode stringNode(const std::string & value); + DLL_LINKAGE JsonNode intNode(si64 value); +} + +namespace JsonDetail +{ + // conversion helpers for JsonNode::convertTo (partial template function instantiation is illegal in c++) + + template + struct JsonConvImpl; + + template + struct JsonConvImpl + { + static T convertImpl(const JsonNode & node) + { + return T((int)node.Float()); + } + }; + + template + struct JsonConvImpl + { + static T convertImpl(const JsonNode & node) + { + return T(node.Float()); + } + }; + + template + struct JsonConverter + { + static Type convert(const JsonNode & node) + { + ///this should be triggered only for numeric types and enums + static_assert(boost::mpl::or_, std::is_enum, boost::is_class >::value, "Unsupported type for JsonNode::convertTo()!"); + return JsonConvImpl, boost::is_class >::value >::convertImpl(node); + + } + }; + + template + struct JsonConverter > + { + static std::map convert(const JsonNode & node) + { + std::map ret; + for (const JsonMap::value_type & entry : node.Struct()) + { + ret.insert(entry.first, entry.second.convertTo()); + } + return ret; + } + }; + + template + struct JsonConverter > + { + static std::set convert(const JsonNode & node) + { + std::set ret; + for(const JsonVector::value_type & entry : node.Vector()) + { + ret.insert(entry.convertTo()); + } + return ret; + } + }; + + template + struct JsonConverter > + { + static std::vector convert(const JsonNode & node) + { + std::vector ret; + for (const JsonVector::value_type & entry: node.Vector()) + { + ret.push_back(entry.convertTo()); + } + return ret; + } + }; + + template<> + struct JsonConverter + { + static std::string convert(const JsonNode & node) + { + return node.String(); + } + }; + + template<> + struct JsonConverter + { + static bool convert(const JsonNode & node) + { + return node.Bool(); + } + }; +} + +template +Type JsonNode::convertTo() const +{ + return JsonDetail::JsonConverter::convert(*this); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Languages.h b/lib/Languages.h index d00ce6ff7..cfd47021d 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -1,112 +1,112 @@ -/* - * Languages.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 - -namespace Languages -{ - -enum class ELanguages -{ - CZECH, - CHINESE, - ENGLISH, - FINNISH, - FRENCH, - GERMAN, - HUNGARIAN, - ITALIAN, - KOREAN, - POLISH, - PORTUGUESE, - RUSSIAN, - SPANISH, - SWEDISH, - TURKISH, - UKRAINIAN, - VIETNAMESE, - - // Pseudo-languages, that have no translations but can define H3 encoding to use - OTHER_CP1250, - OTHER_CP1251, - OTHER_CP1252, - - COUNT -}; - -struct Options -{ - /// string identifier (ascii, lower-case), e.g. "english" - std::string identifier; - - /// human-readable name of language in English - std::string nameEnglish; - - /// human-readable name of language in its own language - std::string nameNative; - - /// encoding that is used by H3 for this language - std::string encoding; - - /// primary IETF language tag - std::string tagIETF; - - /// VCMI supports translations into this language - bool hasTranslation = false; -}; - -inline const auto & getLanguageList() -{ - static const std::array languages - { { - { "czech", "Czech", "Čeština", "CP1250", "cs", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, - { "french", "French", "Français", "CP1252", "fr", true }, - { "german", "German", "Deutsch", "CP1252", "de", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", true }, - { "korean", "Korean", "한국어", "CP949", "ko", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding - - { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", false } - } }; - static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); - - return languages; -} - -inline const Options & getLanguageOptions(ELanguages language) -{ - assert(language < ELanguages::COUNT); - return getLanguageList()[static_cast(language)]; -} - -inline const Options & getLanguageOptions(const std::string & language) -{ - for(const auto & entry : getLanguageList()) - if(entry.identifier == language) - return entry; - - static const Options emptyValue; - assert(0); - return emptyValue; -} - -} +/* + * Languages.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 + +namespace Languages +{ + +enum class ELanguages +{ + CZECH, + CHINESE, + ENGLISH, + FINNISH, + FRENCH, + GERMAN, + HUNGARIAN, + ITALIAN, + KOREAN, + POLISH, + PORTUGUESE, + RUSSIAN, + SPANISH, + SWEDISH, + TURKISH, + UKRAINIAN, + VIETNAMESE, + + // Pseudo-languages, that have no translations but can define H3 encoding to use + OTHER_CP1250, + OTHER_CP1251, + OTHER_CP1252, + + COUNT +}; + +struct Options +{ + /// string identifier (ascii, lower-case), e.g. "english" + std::string identifier; + + /// human-readable name of language in English + std::string nameEnglish; + + /// human-readable name of language in its own language + std::string nameNative; + + /// encoding that is used by H3 for this language + std::string encoding; + + /// primary IETF language tag + std::string tagIETF; + + /// VCMI supports translations into this language + bool hasTranslation = false; +}; + +inline const auto & getLanguageList() +{ + static const std::array languages + { { + { "czech", "Czech", "Čeština", "CP1250", "cs", true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", true }, + { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, + { "french", "French", "Français", "CP1252", "fr", true }, + { "german", "German", "Deutsch", "CP1252", "de", true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, + { "italian", "Italian", "Italiano", "CP1250", "it", true }, + { "korean", "Korean", "한국어", "CP949", "ko", true }, + { "polish", "Polish", "Polski", "CP1250", "pl", true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", true }, + { "spanish", "Spanish", "Español", "CP1252", "es", true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding + + { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", false } + } }; + static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); + + return languages; +} + +inline const Options & getLanguageOptions(ELanguages language) +{ + assert(language < ELanguages::COUNT); + return getLanguageList()[static_cast(language)]; +} + +inline const Options & getLanguageOptions(const std::string & language) +{ + for(const auto & entry : getLanguageList()) + if(entry.identifier == language) + return entry; + + static const Options emptyValue; + assert(0); + return emptyValue; +} + +} diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 132e2a94e..ac64bfbb4 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -1,2781 +1,2781 @@ -/* - * NetPacks.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 "NetPacksBase.h" - -#include "ConstTransitivePtr.h" -#include "MetaString.h" -#include "ResourceSet.h" -#include "TurnTimerInfo.h" -#include "int3.h" - -#include "battle/BattleAction.h" -#include "battle/CObstacleInstance.h" -#include "gameState/EVictoryLossCheckResult.h" -#include "gameState/TavernSlot.h" -#include "gameState/QuestInfo.h" -#include "mapObjects/CGHeroInstance.h" -#include "mapping/CMapDefines.h" -#include "spells/ViewSpellInt.h" - -class CClient; -class CGameHandler; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGameState; -class CArtifact; -class CGObjectInstance; -class CArtifactInstance; -struct StackLocation; -struct ArtSlotInfo; -struct QuestInfo; -class IBattleState; -class BattleInfo; - -// This one teleport-specific, but has to be available everywhere in callbacks and netpacks -// For now it's will be there till teleports code refactored and moved into own file -using TTeleportExitsList = std::vector>; - -struct DLL_LINKAGE Query : public CPackForClient -{ - QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) -}; - -struct StackLocation -{ - ConstTransitivePtr army; - SlotID slot; - - StackLocation() = default; - StackLocation(const CArmedInstance * Army, const SlotID & Slot) - : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - - DLL_LINKAGE const CStackInstance * getStack(); - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - } -}; - -/***********************************************************************************************************/ -struct DLL_LINKAGE PackageApplied : public CPackForClient -{ - PackageApplied() = default; - PackageApplied(ui8 Result) - : result(Result) - { - } - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK - ui32 packType = 0; //type id of applied package - ui32 requestID = 0; //an ID given by client to the request that was applied - PlayerColor player; - - template void serialize(Handler & h, const int version) - { - h & result; - h & packType; - h & requestID; - h & player; - } -}; - -struct DLL_LINKAGE SystemMessage : public CPackForClient -{ - SystemMessage(std::string Text) - : text(std::move(Text)) - { - } - SystemMessage() = default; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - std::string text; - - template void serialize(Handler & h, const int version) - { - h & text; - } -}; - -struct DLL_LINKAGE PlayerBlocked : public CPackForClient -{ - enum EReason { UPCOMING_BATTLE, ONGOING_MOVEMENT }; - enum EMode { BLOCKADE_STARTED, BLOCKADE_ENDED }; - - EReason reason = UPCOMING_BATTLE; - EMode startOrEnd = BLOCKADE_STARTED; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & reason; - h & startOrEnd; - h & player; - } -}; - -struct DLL_LINKAGE PlayerCheated : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - bool losingCheatCode = false; - bool winningCheatCode = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & losingCheatCode; - h & winningCheatCode; - } -}; - -struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - TurnTimerInfo turnTimer; - - template void serialize(Handler & h, const int version) - { - h & player; - h & turnTimer; - } -}; - -struct DLL_LINKAGE PlayerStartsTurn : public Query -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - } -}; - -struct DLL_LINKAGE DaysWithoutTown : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - std::optional daysWithoutCastle; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & daysWithoutCastle; - } -}; - -struct DLL_LINKAGE EntitiesChanged : public CPackForClient -{ - std::vector changes; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & changes; - } -}; - -struct DLL_LINKAGE SetResources : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - bool abs = true; //false - changes by value; 1 - sets to value - PlayerColor player; - TResources res; //res[resid] => res amount - - template void serialize(Handler & h, const int version) - { - h & abs; - h & player; - h & res; - } -}; - -struct DLL_LINKAGE SetPrimSkill : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 abs = 0; //0 - changes by value; 1 - sets to value - ObjectInstanceID id; - PrimarySkill which = PrimarySkill::ATTACK; - si64 val = 0; - - template void serialize(Handler & h, const int version) - { - h & abs; - h & id; - h & which; - h & val; - } -}; - -struct DLL_LINKAGE SetSecSkill : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 abs = 0; //0 - changes by value; 1 - sets to value - ObjectInstanceID id; - SecondarySkill which; - ui16 val = 0; - - template void serialize(Handler & h, const int version) - { - h & abs; - h & id; - h & which; - h & val; - } -}; - -struct DLL_LINKAGE HeroVisitCastle : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 flags = 0; //1 - start - ObjectInstanceID tid, hid; - - bool start() const //if hero is entering castle (if false - leaving) - { - return flags & 1; - } - - template void serialize(Handler & h, const int version) - { - h & flags; - h & tid; - h & hid; - } -}; - -struct DLL_LINKAGE ChangeSpells : public CPackForClient -{ - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ui8 learn = 1; //1 - gives spell, 0 - takes - ObjectInstanceID hid; - std::set spells; - - template void serialize(Handler & h, const int version) - { - h & learn; - h & hid; - h & spells; - } -}; - -struct DLL_LINKAGE SetMana : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ObjectInstanceID hid; - si32 val = 0; - bool absolute = true; - - template void serialize(Handler & h, const int version) - { - h & val; - h & hid; - h & absolute; - } -}; - -struct DLL_LINKAGE SetMovePoints : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID hid; - si32 val = 0; - bool absolute = true; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & val; - h & hid; - h & absolute; - } -}; - -struct DLL_LINKAGE FoWChange : public CPackForClient -{ - void applyGs(CGameState * gs); - - std::unordered_set tiles; - PlayerColor player; - ui8 mode = 0; //mode==0 - hide, mode==1 - reveal - bool waitForDialogs = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tiles; - h & player; - h & mode; - h & waitForDialogs; - } -}; - -struct DLL_LINKAGE SetAvailableHero : public CPackForClient -{ - SetAvailableHero() - { - army.clearSlots(); - } - void applyGs(CGameState * gs); - - TavernHeroSlot slotID; - TavernSlotRole roleID; - PlayerColor player; - HeroTypeID hid; //HeroTypeID::NONE if no hero - CSimpleArmy army; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & slotID; - h & roleID; - h & player; - h & hid; - h & army; - } -}; - -struct DLL_LINKAGE GiveBonus : public CPackForClient -{ - enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; - - GiveBonus(ETarget Who = ETarget::HERO) - :who(Who) - { - } - - void applyGs(CGameState * gs); - - ETarget who = ETarget::HERO; //who receives bonus - si32 id = 0; //hero. town or player id - whoever receives it - Bonus bonus; - MetaString bdescr; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & bonus; - h & id; - h & bdescr; - h & who; - assert(id != -1); - } -}; - -struct DLL_LINKAGE ChangeObjPos : public CPackForClient -{ - void applyGs(CGameState * gs); - - /// Object to move - ObjectInstanceID objid; - /// New position of visitable tile of an object - int3 nPos; - /// Player that initiated this action, if any - PlayerColor initiator; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & objid; - h & nPos; - h & initiator; - } -}; - -struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - } -}; - -struct DLL_LINKAGE PlayerEndsGame : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - EVictoryLossCheckResult victoryLossCheckResult; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & victoryLossCheckResult; - } -}; - -struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient -{ - void applyGs(CGameState * gs); - - std::vector players; - ui8 playerConnectionId; //PLAYER_AI for AI player - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & players; - h & playerConnectionId; - } -}; - -struct DLL_LINKAGE RemoveBonus : public CPackForClient -{ - RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) - :who(Who) - { - } - - void applyGs(CGameState * gs); - - GiveBonus::ETarget who; //who receives bonus - ui32 whoID = 0; //hero, town or player id - whoever loses bonus - - //vars to identify bonus: its source - ui8 source = 0; - ui32 id = 0; //source id - - //used locally: copy of removed bonus - Bonus bonus; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & source; - h & id; - h & who; - h & whoID; - } -}; - -struct DLL_LINKAGE SetCommanderProperty : public CPackForClient -{ - enum ECommanderProperty { ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL }; - - void applyGs(CGameState * gs); - - ObjectInstanceID heroid; - - ECommanderProperty which = ALIVE; - TExpType amount = 0; //0 for dead, >0 for alive - si32 additionalInfo = 0; //for secondary skills choice - Bonus accumulatedBonus; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroid; - h & which; - h & amount; - h & additionalInfo; - h & accumulatedBonus; - } -}; - -struct DLL_LINKAGE AddQuest : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - PlayerColor player; - QuestInfo quest; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & quest; - } -}; - -struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient -{ - std::vector treasures, minors, majors, relics; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & treasures; - h & minors; - h & majors; - h & relics; - } -}; - -struct DLL_LINKAGE UpdateMapEvents : public CPackForClient -{ - std::list events; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & events; - } -}; - -struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient -{ - ObjectInstanceID town; - std::list events; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & town; - h & events; - } -}; - -struct DLL_LINKAGE ChangeFormation : public CPackForClient -{ - ObjectInstanceID hid; - ui8 formation = 0; - - void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & hid; - h & formation; - } -}; - -struct DLL_LINKAGE RemoveObject : public CPackForClient -{ - RemoveObject() = default; - RemoveObject(const ObjectInstanceID & objectID, const PlayerColor & initiator) - : objectID(objectID) - , initiator(initiator) - { - } - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - /// ID of removed object - ObjectInstanceID objectID; - - /// Player that initiated this action, if any - PlayerColor initiator; - - template void serialize(Handler & h, const int version) - { - h & objectID; - h & initiator; - } -}; - -struct DLL_LINKAGE TryMoveHero : public CPackForClient -{ - void applyGs(CGameState * gs); - - enum EResult - { - FAILED, - SUCCESS, - TELEPORTATION, - BLOCKING_VISIT, - EMBARK, - DISEMBARK - }; - - ObjectInstanceID id; - ui32 movePoints = 0; - EResult result = FAILED; //uses EResult - int3 start, end; //h3m format - std::unordered_set fowRevealed; //revealed tiles - std::optional attackedFrom; // Set when stepping into endangered tile. - - virtual void visitTyped(ICPackVisitor & visitor) override; - - bool stopMovement() const - { - return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & result; - h & start; - h & end; - h & movePoints; - h & fowRevealed; - h & attackedFrom; - } -}; - -struct DLL_LINKAGE NewStructures : public CPackForClient -{ - void applyGs(CGameState * gs); - - ObjectInstanceID tid; - std::set bid; - si16 builded = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & bid; - h & builded; - } -}; - -struct DLL_LINKAGE RazeStructures : public CPackForClient -{ - void applyGs(CGameState * gs); - - ObjectInstanceID tid; - std::set bid; - si16 destroyed = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & bid; - h & destroyed; - } -}; - -struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID tid; - std::vector > > creatures; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & creatures; - } -}; - -struct DLL_LINKAGE SetHeroesInTown : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & tid; - h & visiting; - h & garrison; - } -}; - -struct DLL_LINKAGE HeroRecruited : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - HeroTypeID hid; //subID of hero - ObjectInstanceID tid; - ObjectInstanceID boatId; - int3 tile; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & hid; - h & tid; - h & boatId; - h & tile; - h & player; - } -}; - -struct DLL_LINKAGE GiveHero : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - ObjectInstanceID id; //object id - ObjectInstanceID boatId; - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & boatId; - h & player; - } -}; - -struct DLL_LINKAGE OpenWindow : public Query -{ - EOpenWindowMode window; - ObjectInstanceID object; - ObjectInstanceID visitor; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & window; - h & object; - h & visitor; - } -}; - -struct DLL_LINKAGE NewObject : public CPackForClient -{ - void applyGs(CGameState * gs); - - /// Object ID to create - Obj ID; - /// Object secondary ID to create - ui32 subID = 0; - /// Position of visitable tile of created object - int3 targetPos; - /// Which player initiated creation of this object - PlayerColor initiator; - - ObjectInstanceID createdObjectID; //used locally, filled during applyGs - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & ID; - h & subID; - h & targetPos; - h & initiator; - } -}; - -struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - si32 id = 0; //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) - std::vector arts; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & arts; - } -}; - -struct DLL_LINKAGE CGarrisonOperationPack : CPackForClient -{ -}; - -struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - TQuantity count; - bool absoluteValue; //if not -> count will be added (or subtracted if negative) - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & count; - h & absoluteValue; - } -}; - -struct DLL_LINKAGE SetStackType : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - CreatureID type; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & type; - } -}; - -struct DLL_LINKAGE EraseStack : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - } -}; - -struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack -{ - ObjectInstanceID srcArmy; - ObjectInstanceID dstArmy; - SlotID srcSlot; - SlotID dstSlot; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & srcArmy; - h & dstArmy; - h & srcSlot; - h & dstSlot; - } -}; - -struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack -{ - ObjectInstanceID army; - SlotID slot; - CreatureID type; - TQuantity count = 0; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - h & type; - h & count; - } -}; - -///moves creatures from src stack to dst slot, may be used for merging/splittint/moving stacks -struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack -{ - ObjectInstanceID srcArmy; - ObjectInstanceID dstArmy; - SlotID srcSlot; - SlotID dstSlot; - - TQuantity count; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & srcArmy; - h & dstArmy; - h & srcSlot; - h & dstSlot; - h & count; - } -}; - -struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack -{ - std::vector moves; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & moves; - } -}; - -struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack -{ - std::vector moves; - std::vector changes; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & moves; - h & changes; - } -}; - -struct GetEngagedHeroIds -{ - std::optional operator()(const ConstTransitivePtr & h) const - { - return h->id; - } - std::optional operator()(const ConstTransitivePtr & s) const - { - if(s->armyObj && s->armyObj->ID == Obj::HERO) - return s->armyObj->id; - return std::optional(); - } -}; - -struct DLL_LINKAGE CArtifactOperationPack : CPackForClient -{ -}; - -struct DLL_LINKAGE PutArtifact : CArtifactOperationPack -{ - PutArtifact() = default; - PutArtifact(ArtifactLocation * dst, bool askAssemble = true) - : al(*dst), askAssemble(askAssemble) - { - } - - ArtifactLocation al; - bool askAssemble = false; - ConstTransitivePtr art; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - h & askAssemble; - h & art; - } -}; - -struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack -{ - ConstTransitivePtr art; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & art; - } -}; - -struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack -{ - ArtifactLocation al; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - } -}; - -struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack -{ - MoveArtifact() = default; - MoveArtifact(ArtifactLocation * src, ArtifactLocation * dst, bool askAssemble = true) - : src(*src), dst(*dst), askAssemble(askAssemble) - { - } - ArtifactLocation src, dst; - bool askAssemble = true; - - void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & src; - h & dst; - h & askAssemble; - } -}; - -struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack -{ - struct LinkedSlots - { - ArtifactPosition srcPos; - ArtifactPosition dstPos; - - LinkedSlots() = default; - LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos) - : srcPos(srcPos) - , dstPos(dstPos) - { - } - template void serialize(Handler & h, const int version) - { - h & srcPos; - h & dstPos; - } - }; - - TArtHolder srcArtHolder; - TArtHolder dstArtHolder; - - BulkMoveArtifacts() - : swap(false) - { - } - BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) - : srcArtHolder(std::move(std::move(srcArtHolder))) - , dstArtHolder(std::move(std::move(dstArtHolder))) - , swap(swap) - { - } - - void applyGs(CGameState * gs); - - std::vector artsPack0; - std::vector artsPack1; - bool swap; - CArtifactSet * getSrcHolderArtSet(); - CArtifactSet * getDstHolderArtSet(); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & artsPack0; - h & artsPack1; - h & srcArtHolder; - h & dstArtHolder; - h & swap; - } -}; - -struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack -{ - ArtifactLocation al; //where assembly will be put - CArtifact * builtArt; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - h & builtArt; - } -}; - -struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack -{ - ArtifactLocation al; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & al; - } -}; - -struct DLL_LINKAGE HeroVisit : public CPackForClient -{ - PlayerColor player; - ObjectInstanceID heroId; - ObjectInstanceID objId; - - bool starting; //false -> ending - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & player; - h & heroId; - h & objId; - h & starting; - } -}; - -struct DLL_LINKAGE NewTurn : public CPackForClient -{ - enum weekType { NORMAL, DOUBLE_GROWTH, BONUS_GROWTH, DEITYOFFIRE, PLAGUE, NO_ACTION }; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - struct Hero - { - ObjectInstanceID id; - ui32 move, mana; //id is a general serial id - template void serialize(Handler & h, const int version) - { - h & id; - h & move; - h & mana; - } - bool operator<(const Hero & h)const { return id < h.id; } - }; - - std::set heroes; //updates movement and mana points - std::map res; //player ID => resource value[res_id] - std::map cres;//creatures to be placed in towns - ui32 day = 0; - ui8 specialWeek = 0; //weekType - CreatureID creatureid; //for creature weeks - - NewTurn() = default; - - template void serialize(Handler & h, const int version) - { - h & heroes; - h & cres; - h & res; - h & day; - h & specialWeek; - h & creatureid; - } -}; - -struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple info window -{ - EInfoWindowMode type = EInfoWindowMode::MODAL; - MetaString text; - std::vector components; - PlayerColor player; - ui16 soundID = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & type; - h & text; - h & components; - h & player; - h & soundID; - } - InfoWindow() = default; -}; - -namespace ObjProperty -{ - enum - { - OWNER = 1, BLOCKVIS = 2, PRIMARY_STACK_COUNT = 3, VISITORS = 4, VISITED = 5, ID = 6, AVAILABLE_CREATURE = 7, SUBID = 8, - MONSTER_COUNT = 10, MONSTER_POWER = 11, MONSTER_EXP = 12, MONSTER_RESTORE_TYPE = 13, MONSTER_REFUSED_JOIN, - - //town-specific - STRUCTURE_ADD_VISITING_HERO, STRUCTURE_CLEAR_VISITORS, STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state - BONUS_VALUE_FIRST, BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) - - //creature-bank specific - BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, - - //object with reward - REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED - }; -} - -struct DLL_LINKAGE SetObjectProperty : public CPackForClient -{ - void applyGs(CGameState * gs) const; - ObjectInstanceID id; - ui8 what = 0; // see ObjProperty enum - ui32 val = 0; - SetObjectProperty() = default; - SetObjectProperty(const ObjectInstanceID & ID, ui8 What, ui32 Val) - : id(ID) - , what(What) - , val(Val) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & id; - h & what; - h & val; - } -}; - -struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient -{ - enum VisitMode - { - VISITOR_ADD, // mark hero as one that have visited this object - VISITOR_ADD_TEAM, // mark team as one that have visited this object - VISITOR_REMOVE, // unmark visitor, reversed to ADD - VISITOR_CLEAR // clear all visitors from this object (object reset) - }; - ui32 mode = VISITOR_CLEAR; // uses VisitMode enum - ObjectInstanceID object; - ObjectInstanceID hero; // note: hero owner will be also marked as "visited" this object - - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - ChangeObjectVisitors() = default; - - ChangeObjectVisitors(ui32 mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) - : mode(mode) - , object(object) - , hero(heroID) - { - } - - template void serialize(Handler & h, const int version) - { - h & object; - h & hero; - h & mode; - } -}; - -struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient -{ - ObjectInstanceID heroId; - - /// Do not serialize, used by server only - std::vector skills; - - void applyGs(CGameState * gs); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroId; - } -}; - -struct DLL_LINKAGE HeroLevelUp : public Query -{ - PlayerColor player; - ObjectInstanceID heroId; - - PrimarySkill primskill = PrimarySkill::ATTACK; - std::vector skills; - - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & heroId; - h & primskill; - h & skills; - } -}; - -struct DLL_LINKAGE CommanderLevelUp : public Query -{ - PlayerColor player; - ObjectInstanceID heroId; - - std::vector skills; //0-5 - secondary skills, val-100 - special skill - - void applyGs(CGameState * gs) const; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & heroId; - h & skills; - } -}; - -//A dialog that requires making decision by player - it may contain components to choose between or has yes/no options -//Client responds with QueryReply, where answer: 0 - cancel pressed, choice doesn't matter; 1/2/... - first/second/... component selected and OK pressed -//Until sending reply player won't be allowed to take any actions -struct DLL_LINKAGE BlockingDialog : public Query -{ - enum { ALLOW_CANCEL = 1, SELECTION = 2 }; - MetaString text; - std::vector components; - PlayerColor player; - ui8 flags = 0; - ui16 soundID = 0; - - bool cancel() const - { - return flags & ALLOW_CANCEL; - } - bool selection() const - { - return flags & SELECTION; - } - - BlockingDialog(bool yesno, bool Selection) - { - if(yesno) flags |= ALLOW_CANCEL; - if(Selection) flags |= SELECTION; - } - BlockingDialog() = default; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & text; - h & components; - h & player; - h & flags; - h & soundID; - } -}; - -struct DLL_LINKAGE GarrisonDialog : public Query -{ - ObjectInstanceID objid, hid; - bool removableUnits = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & objid; - h & hid; - h & removableUnits; - } -}; - -struct DLL_LINKAGE ExchangeDialog : public Query -{ - PlayerColor player; - - ObjectInstanceID hero1; - ObjectInstanceID hero2; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & hero1; - h & hero2; - } -}; - -struct DLL_LINKAGE TeleportDialog : public Query -{ - TeleportDialog() = default; - - TeleportDialog(const ObjectInstanceID & hero, const TeleportChannelID & Channel) - : hero(hero) - , channel(Channel) - { - } - ObjectInstanceID hero; - TeleportChannelID channel; - TTeleportExitsList exits; - bool impassable = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & hero; - h & channel; - h & exits; - h & impassable; - } -}; - -struct DLL_LINKAGE MapObjectSelectDialog : public Query -{ - PlayerColor player; - Component icon; - MetaString title; - MetaString description; - std::vector objects; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & queryID; - h & player; - h & icon; - h & title; - h & description; - h & objects; - } -}; - -struct DLL_LINKAGE BattleStart : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - BattleInfo * info = nullptr; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & info; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleNextRound : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - ui32 stack = 0; - ui8 askPlayerInterface = true; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stack; - h & askPlayerInterface; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleCancelled: public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - - template void serialize(Handler & h, const int version) - { - h & battleID; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResultAccepted : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - struct HeroBattleResults - { - HeroBattleResults() - : hero(nullptr), army(nullptr), exp(0) {} - - CGHeroInstance * hero; - CArmedInstance * army; - TExpType exp; - - template void serialize(Handler & h, const int version) - { - h & hero; - h & army; - h & exp; - } - }; - - BattleID battleID = BattleID::NONE; - std::array heroResult; - ui8 winnerSide; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & heroResult; - h & winnerSide; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResult : public Query -{ - void applyFirstCl(CClient * cl); - - BattleID battleID = BattleID::NONE; - EBattleResult result = EBattleResult::NORMAL; - ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] - std::map casualties[2]; //first => casualties of attackers - map crid => number - TExpType exp[2] = {0, 0}; //exp for attacker and defender - std::set artifacts; //artifacts taken from loser to winner - currently unused - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & queryID; - h & result; - h & winner; - h & casualties[0]; - h & casualties[1]; - h & exp; - h & artifacts; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleLogMessage : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - std::vector lines; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & lines; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleStackMoved : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - ui32 stack = 0; - std::vector tilesToMove; - int distance = 0; - bool teleporting = false; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stack; - h & tilesToMove; - h & distance; - h & teleporting; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector changedStacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & changedStacks; - assert(battleID != BattleID::NONE); - } -}; - -struct BattleStackAttacked -{ - DLL_LINKAGE void applyGs(CGameState * gs); - DLL_LINKAGE void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - ui32 stackAttacked = 0, attackerID = 0; - ui32 killedAmount = 0; - int64_t damageAmount = 0; - UnitChanges newState; - enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; - ui32 flags = 0; //uses EFlags (above) - SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set - - bool killed() const//if target stack was killed - { - return flags & KILLED || flags & CLONE_KILLED; - } - bool cloneKilled() const - { - return flags & CLONE_KILLED; - } - bool isSecondary() const//if stack was not a primary target (receives no spell effects) - { - return flags & SECONDARY; - } - ///Attacked with spell (SPELL_LIKE_ATTACK) - bool isSpell() const - { - return flags & SPELL_EFFECT; - } - bool willRebirth() const//resurrection, e.g. Phoenix - { - return flags & REBIRTH; - } - bool fireShield() const - { - return flags & FIRE_SHIELD; - } - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackAttacked; - h & attackerID; - h & newState; - h & flags; - h & killedAmount; - h & damageAmount; - h & spellID; - assert(battleID != BattleID::NONE); - } - bool operator<(const BattleStackAttacked & b) const - { - return stackAttacked < b.stackAttacked; - } -}; - -struct DLL_LINKAGE BattleAttack : public CPackForClient -{ - void applyGs(CGameState * gs); - BattleUnitsChanged attackerChanges; - - BattleID battleID = BattleID::NONE; - std::vector bsa; - ui32 stackAttacking = 0; - ui32 flags = 0; //uses Eflags (below) - enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; - - BattleHex tile; - SpellID spellID = SpellID::NONE; //for SPELL_LIKE - - bool shot() const//distance attack - decrease number of shots - { - return flags & SHOT; - } - bool counter() const//is it counterattack? - { - return flags & COUNTER; - } - bool lucky() const - { - return flags & LUCKY; - } - bool unlucky() const - { - return flags & UNLUCKY; - } - bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg - { - return flags & BALLISTA_DOUBLE_DMG; - } - bool deathBlow() const - { - return flags & DEATH_BLOW; - } - bool spellLike() const - { - return flags & SPELL_LIKE; - } - bool lifeDrain() const - { - return flags & LIFE_DRAIN; - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & bsa; - h & stackAttacking; - h & flags; - h & tile; - h & spellID; - h & attackerChanges; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE StartAction : public CPackForClient -{ - StartAction() = default; - StartAction(BattleAction act) - : ba(std::move(act)) - { - } - void applyFirstCl(CClient * cl); - void applyGs(CGameState * gs); - - BattleID battleID = BattleID::NONE; - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & ba; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE EndAction : public CPackForClient -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - BattleID battleID = BattleID::NONE; - - template void serialize(Handler & h, const int version) - { - h & battleID; - } -}; - -struct DLL_LINKAGE BattleSpellCast : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - bool activeCast = true; - ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender - SpellID spellID; //id of spell - ui8 manaGained = 0; //mana channeling ability - BattleHex tile; //destination tile (may not be set in some global/mass spells - std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) - std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) - std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) - si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID - bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & side; - h & spellID; - h & manaGained; - h & tile; - h & affectedCres; - h & resistedCres; - h & reflectedCres; - h & casterStack; - h & castByHero; - h & activeCast; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE SetStackEffect : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector>> toAdd; - std::vector>> toUpdate; - std::vector>> toRemove; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & toAdd; - h & toUpdate; - h & toRemove; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE StacksInjured : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector stacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stacks; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResultsApplied : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - PlayerColor player1, player2; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & player1; - h & player2; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector changes; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & changes; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE CatapultAttack : public CPackForClient -{ - struct AttackInfo - { - si16 destinationTile; - EWallPart attackedPart; - ui8 damageDealt; - - template void serialize(Handler & h, const int version) - { - h & destinationTile; - h & attackedPart; - h & damageDealt; - } - }; - - CatapultAttack(); - ~CatapultAttack() override; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector< AttackInfo > attackedParts; - int attacker = -1; //if -1, then a spell caused this - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & attackedParts; - h & attacker; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient -{ - enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; - - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - int stackID = 0; - BattleStackProperty which = CASTS; - int val = 0; - int absolute = 0; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackID; - h & which; - h & val; - h & absolute; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -///activated at the beginning of turn -struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient -{ - void applyGs(CGameState * gs) const; //effect - - BattleID battleID = BattleID::NONE; - int stackID = 0; - int effect = 0; //use corresponding Bonus type - int val = 0; - int additionalInfo = 0; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackID; - h & effect; - h & val; - h & additionalInfo; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - EGateState state = EGateState::NONE; - template void serialize(Handler & h, const int version) - { - h & battleID; - h & state; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient -{ - ObjectInstanceID casterID; - SpellID spellID; - template void serialize(Handler & h, const int version) - { - h & casterID; - h & spellID; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient -{ - PlayerColor player; - bool showTerrain; // TODO: send terrain state - - std::vector objectPositions; - - template void serialize(Handler & h, const int version) - { - h & player; - h & showTerrain; - h & objectPositions; - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE GamePause : public CPackForServer -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - } -}; - -struct DLL_LINKAGE EndTurn : public CPackForServer -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - } -}; - -struct DLL_LINKAGE DismissHero : public CPackForServer -{ - DismissHero() = default; - DismissHero(const ObjectInstanceID & HID) - : hid(HID) - { - } - ObjectInstanceID hid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - } -}; - -struct DLL_LINKAGE MoveHero : public CPackForServer -{ - MoveHero() = default; - MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) - : dest(Dest) - , hid(HID) - , transit(Transit) - { - } - int3 dest; - ObjectInstanceID hid; - bool transit = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - h & transit; - } -}; - -struct DLL_LINKAGE CastleTeleportHero : public CPackForServer -{ - CastleTeleportHero() = default; - CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) - : dest(Dest) - , hid(HID) - , source(Source) - { - } - ObjectInstanceID dest; - ObjectInstanceID hid; - si8 source = 0; //who give teleporting, 1=castle gate - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - } -}; - -struct DLL_LINKAGE ArrangeStacks : public CPackForServer -{ - ArrangeStacks() = default; - ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) - : what(W) - , p1(P1) - , p2(P2) - , id1(ID1) - , id2(ID2) - , val(VAL) - { - } - - ui8 what = 0; //1 - swap; 2 - merge; 3 - split - SlotID p1, p2; //positions of first and second stack - ObjectInstanceID id1, id2; //ids of objects with garrison - si32 val = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & what; - h & p1; - h & p2; - h & id1; - h & id2; - h & val; - } -}; - -struct DLL_LINKAGE BulkMoveArmy : public CPackForServer -{ - SlotID srcSlot; - ObjectInstanceID srcArmy; - ObjectInstanceID destArmy; - - BulkMoveArmy() = default; - - BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) - : srcArmy(srcArmy) - , destArmy(destArmy) - , srcSlot(srcSlot) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcSlot; - h & srcArmy; - h & destArmy; - } -}; - -struct DLL_LINKAGE BulkSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - si32 amount = 0; - - BulkSplitStack() = default; - - BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) - : src(src) - , srcOwner(srcOwner) - , amount(howMany) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - h & amount; - } -}; - -struct DLL_LINKAGE BulkMergeStacks : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkMergeStacks() = default; - - BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkSmartSplitStack() = default; - - BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE DisbandCreature : public CPackForServer -{ - DisbandCreature() = default; - DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) - : pos(Pos) - , id(ID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - } -}; - -struct DLL_LINKAGE BuildStructure : public CPackForServer -{ - BuildStructure() = default; - BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) - : tid(TID) - , bid(BID) - { - } - ObjectInstanceID tid; //town id - BuildingID bid; //structure id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & bid; - } -}; - -struct DLL_LINKAGE RazeStructure : public BuildStructure -{ - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE RecruitCreatures : public CPackForServer -{ - RecruitCreatures() = default; - RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) - : tid(TID) - , dst(DST) - , crid(CRID) - , amount(Amount) - , level(Level) - { - } - ObjectInstanceID tid; //dwelling id, or town - ObjectInstanceID dst; //destination ID, e.g. hero - CreatureID crid; - ui32 amount = 0; //creature amount - si32 level = 0; //dwelling level to buy from, -1 if any - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & dst; - h & crid; - h & amount; - h & level; - } -}; - -struct DLL_LINKAGE UpgradeCreature : public CPackForServer -{ - UpgradeCreature() = default; - UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) - : pos(Pos) - , id(ID) - , cid(CRID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - CreatureID cid; //id of type to which we want make upgrade - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - h & cid; - } -}; - -struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer -{ - GarrisonHeroSwap() = default; - GarrisonHeroSwap(const ObjectInstanceID & TID) - : tid(TID) - { - } - ObjectInstanceID tid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - } -}; - -struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer -{ - ArtifactLocation src, dst; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & dst; - } -}; - -struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer -{ - ObjectInstanceID srcHero; - ObjectInstanceID dstHero; - bool swap = false; - bool equipped = true; - bool backpack = true; - - BulkExchangeArtifacts() = default; - BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) - : srcHero(srcHero) - , dstHero(dstHero) - , swap(swap) - , equipped(equipped) - , backpack(backpack) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcHero; - h & dstHero; - h & swap; - h & equipped; - h & backpack; - } -}; - -struct DLL_LINKAGE AssembleArtifacts : public CPackForServer -{ - AssembleArtifacts() = default; - AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) - : heroID(_heroID) - , artifactSlot(_artifactSlot) - , assemble(_assemble) - , assembleTo(_assembleTo) - { - } - ObjectInstanceID heroID; - ArtifactPosition artifactSlot; - bool assemble = false; // True to assemble artifact, false to disassemble. - ArtifactID assembleTo; // Artifact to assemble into. - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & heroID; - h & artifactSlot; - h & assemble; - h & assembleTo; - } -}; - -struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer -{ - EraseArtifactByClient() = default; - EraseArtifactByClient(const ArtifactLocation & al) - : al(al) - { - } - ArtifactLocation al; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & al; - } -}; - -struct DLL_LINKAGE BuyArtifact : public CPackForServer -{ - BuyArtifact() = default; - BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) - : hid(HID) - , aid(AID) - { - } - ObjectInstanceID hid; - ArtifactID aid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & aid; - } -}; - -struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer -{ - ObjectInstanceID marketId; - ObjectInstanceID heroId; - - EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; - std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] - std::vector val; //units of sold resource - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & marketId; - h & heroId; - h & mode; - h & r1; - h & r2; - h & val; - } -}; - -struct DLL_LINKAGE SetFormation : public CPackForServer -{ - SetFormation() = default; - ; - SetFormation(const ObjectInstanceID & HID, ui8 Formation) - : hid(HID) - , formation(Formation) - { - } - ObjectInstanceID hid; - ui8 formation = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & formation; - } -}; - -struct DLL_LINKAGE HireHero : public CPackForServer -{ - HireHero() = default; - HireHero(HeroTypeID HID, const ObjectInstanceID & TID) - : hid(HID) - , tid(TID) - { - } - HeroTypeID hid; //available hero serial - ObjectInstanceID tid; //town (tavern) id - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & tid; - h & player; - } -}; - -struct DLL_LINKAGE BuildBoat : public CPackForServer -{ - ObjectInstanceID objid; //where player wants to buy a boat - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & objid; - } -}; - -struct DLL_LINKAGE QueryReply : public CPackForServer -{ - QueryReply() = default; - QueryReply(const QueryID & QID, std::optional Reply) - : qid(QID) - , reply(Reply) - { - } - QueryID qid; - PlayerColor player; - std::optional reply; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & qid; - h & player; - h & reply; - } -}; - -struct DLL_LINKAGE MakeAction : public CPackForServer -{ - MakeAction() = default; - MakeAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - BattleID battleID; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - h & battleID; - } -}; - -struct DLL_LINKAGE DigWithHero : public CPackForServer -{ - ObjectInstanceID id; //digging hero id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & id; - } -}; - -struct DLL_LINKAGE CastAdvSpell : public CPackForServer -{ - ObjectInstanceID hid; //hero id - SpellID sid; //spell id - int3 pos; //selected tile (not always used) - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & sid; - h & pos; - } -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE SaveGame : public CPackForServer -{ - SaveGame() = default; - SaveGame(std::string Fname) - : fname(std::move(Fname)) - { - } - std::string fname; - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & fname; - } -}; - -struct DLL_LINKAGE PlayerMessage : public CPackForServer -{ - PlayerMessage() = default; - PlayerMessage(std::string Text, const ObjectInstanceID & obj) - : text(std::move(Text)) - , currObj(obj) - { - } - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - std::string text; - ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & text; - h & currObj; - } -}; - -struct DLL_LINKAGE PlayerMessageClient : public CPackForClient -{ - PlayerMessageClient() = default; - PlayerMessageClient(const PlayerColor & Player, std::string Text) - : player(Player) - , text(std::move(Text)) - { - } - virtual void visitTyped(ICPackVisitor & visitor) override; - - PlayerColor player; - std::string text; - - template void serialize(Handler & h, const int version) - { - h & player; - h & text; - } -}; - -struct DLL_LINKAGE CenterView : public CPackForClient -{ - PlayerColor player; - int3 pos; - ui32 focusTime = 0; //ms - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & pos; - h & player; - h & focusTime; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * NetPacks.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 "NetPacksBase.h" + +#include "ConstTransitivePtr.h" +#include "MetaString.h" +#include "ResourceSet.h" +#include "TurnTimerInfo.h" +#include "int3.h" + +#include "battle/BattleAction.h" +#include "battle/CObstacleInstance.h" +#include "gameState/EVictoryLossCheckResult.h" +#include "gameState/TavernSlot.h" +#include "gameState/QuestInfo.h" +#include "mapObjects/CGHeroInstance.h" +#include "mapping/CMapDefines.h" +#include "spells/ViewSpellInt.h" + +class CClient; +class CGameHandler; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGameState; +class CArtifact; +class CGObjectInstance; +class CArtifactInstance; +struct StackLocation; +struct ArtSlotInfo; +struct QuestInfo; +class IBattleState; +class BattleInfo; + +// This one teleport-specific, but has to be available everywhere in callbacks and netpacks +// For now it's will be there till teleports code refactored and moved into own file +using TTeleportExitsList = std::vector>; + +struct DLL_LINKAGE Query : public CPackForClient +{ + QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) +}; + +struct StackLocation +{ + ConstTransitivePtr army; + SlotID slot; + + StackLocation() = default; + StackLocation(const CArmedInstance * Army, const SlotID & Slot) + : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! + , slot(Slot) + { + } + + DLL_LINKAGE const CStackInstance * getStack(); + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + } +}; + +/***********************************************************************************************************/ +struct DLL_LINKAGE PackageApplied : public CPackForClient +{ + PackageApplied() = default; + PackageApplied(ui8 Result) + : result(Result) + { + } + virtual void visitTyped(ICPackVisitor & visitor) override; + + ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK + ui32 packType = 0; //type id of applied package + ui32 requestID = 0; //an ID given by client to the request that was applied + PlayerColor player; + + template void serialize(Handler & h, const int version) + { + h & result; + h & packType; + h & requestID; + h & player; + } +}; + +struct DLL_LINKAGE SystemMessage : public CPackForClient +{ + SystemMessage(std::string Text) + : text(std::move(Text)) + { + } + SystemMessage() = default; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + std::string text; + + template void serialize(Handler & h, const int version) + { + h & text; + } +}; + +struct DLL_LINKAGE PlayerBlocked : public CPackForClient +{ + enum EReason { UPCOMING_BATTLE, ONGOING_MOVEMENT }; + enum EMode { BLOCKADE_STARTED, BLOCKADE_ENDED }; + + EReason reason = UPCOMING_BATTLE; + EMode startOrEnd = BLOCKADE_STARTED; + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & reason; + h & startOrEnd; + h & player; + } +}; + +struct DLL_LINKAGE PlayerCheated : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + bool losingCheatCode = false; + bool winningCheatCode = false; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & losingCheatCode; + h & winningCheatCode; + } +}; + +struct DLL_LINKAGE TurnTimeUpdate : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + TurnTimerInfo turnTimer; + + template void serialize(Handler & h, const int version) + { + h & player; + h & turnTimer; + } +}; + +struct DLL_LINKAGE PlayerStartsTurn : public Query +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + } +}; + +struct DLL_LINKAGE DaysWithoutTown : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + std::optional daysWithoutCastle; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & daysWithoutCastle; + } +}; + +struct DLL_LINKAGE EntitiesChanged : public CPackForClient +{ + std::vector changes; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & changes; + } +}; + +struct DLL_LINKAGE SetResources : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + bool abs = true; //false - changes by value; 1 - sets to value + PlayerColor player; + TResources res; //res[resid] => res amount + + template void serialize(Handler & h, const int version) + { + h & abs; + h & player; + h & res; + } +}; + +struct DLL_LINKAGE SetPrimSkill : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ui8 abs = 0; //0 - changes by value; 1 - sets to value + ObjectInstanceID id; + PrimarySkill which = PrimarySkill::ATTACK; + si64 val = 0; + + template void serialize(Handler & h, const int version) + { + h & abs; + h & id; + h & which; + h & val; + } +}; + +struct DLL_LINKAGE SetSecSkill : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ui8 abs = 0; //0 - changes by value; 1 - sets to value + ObjectInstanceID id; + SecondarySkill which; + ui16 val = 0; + + template void serialize(Handler & h, const int version) + { + h & abs; + h & id; + h & which; + h & val; + } +}; + +struct DLL_LINKAGE HeroVisitCastle : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ui8 flags = 0; //1 - start + ObjectInstanceID tid, hid; + + bool start() const //if hero is entering castle (if false - leaving) + { + return flags & 1; + } + + template void serialize(Handler & h, const int version) + { + h & flags; + h & tid; + h & hid; + } +}; + +struct DLL_LINKAGE ChangeSpells : public CPackForClient +{ + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ui8 learn = 1; //1 - gives spell, 0 - takes + ObjectInstanceID hid; + std::set spells; + + template void serialize(Handler & h, const int version) + { + h & learn; + h & hid; + h & spells; + } +}; + +struct DLL_LINKAGE SetMana : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ObjectInstanceID hid; + si32 val = 0; + bool absolute = true; + + template void serialize(Handler & h, const int version) + { + h & val; + h & hid; + h & absolute; + } +}; + +struct DLL_LINKAGE SetMovePoints : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID hid; + si32 val = 0; + bool absolute = true; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & val; + h & hid; + h & absolute; + } +}; + +struct DLL_LINKAGE FoWChange : public CPackForClient +{ + void applyGs(CGameState * gs); + + std::unordered_set tiles; + PlayerColor player; + ui8 mode = 0; //mode==0 - hide, mode==1 - reveal + bool waitForDialogs = false; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tiles; + h & player; + h & mode; + h & waitForDialogs; + } +}; + +struct DLL_LINKAGE SetAvailableHero : public CPackForClient +{ + SetAvailableHero() + { + army.clearSlots(); + } + void applyGs(CGameState * gs); + + TavernHeroSlot slotID; + TavernSlotRole roleID; + PlayerColor player; + HeroTypeID hid; //HeroTypeID::NONE if no hero + CSimpleArmy army; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & slotID; + h & roleID; + h & player; + h & hid; + h & army; + } +}; + +struct DLL_LINKAGE GiveBonus : public CPackForClient +{ + enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; + + GiveBonus(ETarget Who = ETarget::HERO) + :who(Who) + { + } + + void applyGs(CGameState * gs); + + ETarget who = ETarget::HERO; //who receives bonus + si32 id = 0; //hero. town or player id - whoever receives it + Bonus bonus; + MetaString bdescr; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & bonus; + h & id; + h & bdescr; + h & who; + assert(id != -1); + } +}; + +struct DLL_LINKAGE ChangeObjPos : public CPackForClient +{ + void applyGs(CGameState * gs); + + /// Object to move + ObjectInstanceID objid; + /// New position of visitable tile of an object + int3 nPos; + /// Player that initiated this action, if any + PlayerColor initiator; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & objid; + h & nPos; + h & initiator; + } +}; + +struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + } +}; + +struct DLL_LINKAGE PlayerEndsGame : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + EVictoryLossCheckResult victoryLossCheckResult; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & victoryLossCheckResult; + } +}; + +struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient +{ + void applyGs(CGameState * gs); + + std::vector players; + ui8 playerConnectionId; //PLAYER_AI for AI player + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & players; + h & playerConnectionId; + } +}; + +struct DLL_LINKAGE RemoveBonus : public CPackForClient +{ + RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) + :who(Who) + { + } + + void applyGs(CGameState * gs); + + GiveBonus::ETarget who; //who receives bonus + ui32 whoID = 0; //hero, town or player id - whoever loses bonus + + //vars to identify bonus: its source + ui8 source = 0; + ui32 id = 0; //source id + + //used locally: copy of removed bonus + Bonus bonus; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & source; + h & id; + h & who; + h & whoID; + } +}; + +struct DLL_LINKAGE SetCommanderProperty : public CPackForClient +{ + enum ECommanderProperty { ALIVE, BONUS, SECONDARY_SKILL, EXPERIENCE, SPECIAL_SKILL }; + + void applyGs(CGameState * gs); + + ObjectInstanceID heroid; + + ECommanderProperty which = ALIVE; + TExpType amount = 0; //0 for dead, >0 for alive + si32 additionalInfo = 0; //for secondary skills choice + Bonus accumulatedBonus; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & heroid; + h & which; + h & amount; + h & additionalInfo; + h & accumulatedBonus; + } +}; + +struct DLL_LINKAGE AddQuest : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + PlayerColor player; + QuestInfo quest; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & quest; + } +}; + +struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient +{ + std::vector treasures, minors, majors, relics; + + void applyGs(CGameState * gs) const; + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & treasures; + h & minors; + h & majors; + h & relics; + } +}; + +struct DLL_LINKAGE UpdateMapEvents : public CPackForClient +{ + std::list events; + + void applyGs(CGameState * gs) const; + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & events; + } +}; + +struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient +{ + ObjectInstanceID town; + std::list events; + + void applyGs(CGameState * gs) const; + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & town; + h & events; + } +}; + +struct DLL_LINKAGE ChangeFormation : public CPackForClient +{ + ObjectInstanceID hid; + ui8 formation = 0; + + void applyGs(CGameState * gs) const; + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & hid; + h & formation; + } +}; + +struct DLL_LINKAGE RemoveObject : public CPackForClient +{ + RemoveObject() = default; + RemoveObject(const ObjectInstanceID & objectID, const PlayerColor & initiator) + : objectID(objectID) + , initiator(initiator) + { + } + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + /// ID of removed object + ObjectInstanceID objectID; + + /// Player that initiated this action, if any + PlayerColor initiator; + + template void serialize(Handler & h, const int version) + { + h & objectID; + h & initiator; + } +}; + +struct DLL_LINKAGE TryMoveHero : public CPackForClient +{ + void applyGs(CGameState * gs); + + enum EResult + { + FAILED, + SUCCESS, + TELEPORTATION, + BLOCKING_VISIT, + EMBARK, + DISEMBARK + }; + + ObjectInstanceID id; + ui32 movePoints = 0; + EResult result = FAILED; //uses EResult + int3 start, end; //h3m format + std::unordered_set fowRevealed; //revealed tiles + std::optional attackedFrom; // Set when stepping into endangered tile. + + virtual void visitTyped(ICPackVisitor & visitor) override; + + bool stopMovement() const + { + return result != SUCCESS && result != EMBARK && result != DISEMBARK && result != TELEPORTATION; + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & result; + h & start; + h & end; + h & movePoints; + h & fowRevealed; + h & attackedFrom; + } +}; + +struct DLL_LINKAGE NewStructures : public CPackForClient +{ + void applyGs(CGameState * gs); + + ObjectInstanceID tid; + std::set bid; + si16 builded = 0; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & bid; + h & builded; + } +}; + +struct DLL_LINKAGE RazeStructures : public CPackForClient +{ + void applyGs(CGameState * gs); + + ObjectInstanceID tid; + std::set bid; + si16 destroyed = 0; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & bid; + h & destroyed; + } +}; + +struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID tid; + std::vector > > creatures; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & creatures; + } +}; + +struct DLL_LINKAGE SetHeroesInTown : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & tid; + h & visiting; + h & garrison; + } +}; + +struct DLL_LINKAGE HeroRecruited : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + HeroTypeID hid; //subID of hero + ObjectInstanceID tid; + ObjectInstanceID boatId; + int3 tile; + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & hid; + h & tid; + h & boatId; + h & tile; + h & player; + } +}; + +struct DLL_LINKAGE GiveHero : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + ObjectInstanceID id; //object id + ObjectInstanceID boatId; + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & boatId; + h & player; + } +}; + +struct DLL_LINKAGE OpenWindow : public Query +{ + EOpenWindowMode window; + ObjectInstanceID object; + ObjectInstanceID visitor; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & window; + h & object; + h & visitor; + } +}; + +struct DLL_LINKAGE NewObject : public CPackForClient +{ + void applyGs(CGameState * gs); + + /// Object ID to create + Obj ID; + /// Object secondary ID to create + ui32 subID = 0; + /// Position of visitable tile of created object + int3 targetPos; + /// Which player initiated creation of this object + PlayerColor initiator; + + ObjectInstanceID createdObjectID; //used locally, filled during applyGs + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & ID; + h & subID; + h & targetPos; + h & initiator; + } +}; + +struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + si32 id = 0; //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) + std::vector arts; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & arts; + } +}; + +struct DLL_LINKAGE CGarrisonOperationPack : CPackForClient +{ +}; + +struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + TQuantity count; + bool absoluteValue; //if not -> count will be added (or subtracted if negative) + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & count; + h & absoluteValue; + } +}; + +struct DLL_LINKAGE SetStackType : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + CreatureID type; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & type; + } +}; + +struct DLL_LINKAGE EraseStack : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + } +}; + +struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack +{ + ObjectInstanceID srcArmy; + ObjectInstanceID dstArmy; + SlotID srcSlot; + SlotID dstSlot; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & srcArmy; + h & dstArmy; + h & srcSlot; + h & dstSlot; + } +}; + +struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack +{ + ObjectInstanceID army; + SlotID slot; + CreatureID type; + TQuantity count = 0; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + h & type; + h & count; + } +}; + +///moves creatures from src stack to dst slot, may be used for merging/splittint/moving stacks +struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack +{ + ObjectInstanceID srcArmy; + ObjectInstanceID dstArmy; + SlotID srcSlot; + SlotID dstSlot; + + TQuantity count; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & srcArmy; + h & dstArmy; + h & srcSlot; + h & dstSlot; + h & count; + } +}; + +struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & moves; + } +}; + +struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack +{ + std::vector moves; + std::vector changes; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & moves; + h & changes; + } +}; + +struct GetEngagedHeroIds +{ + std::optional operator()(const ConstTransitivePtr & h) const + { + return h->id; + } + std::optional operator()(const ConstTransitivePtr & s) const + { + if(s->armyObj && s->armyObj->ID == Obj::HERO) + return s->armyObj->id; + return std::optional(); + } +}; + +struct DLL_LINKAGE CArtifactOperationPack : CPackForClient +{ +}; + +struct DLL_LINKAGE PutArtifact : CArtifactOperationPack +{ + PutArtifact() = default; + PutArtifact(ArtifactLocation * dst, bool askAssemble = true) + : al(*dst), askAssemble(askAssemble) + { + } + + ArtifactLocation al; + bool askAssemble = false; + ConstTransitivePtr art; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + h & askAssemble; + h & art; + } +}; + +struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack +{ + ConstTransitivePtr art; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & art; + } +}; + +struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack +{ + ArtifactLocation al; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + } +}; + +struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack +{ + MoveArtifact() = default; + MoveArtifact(ArtifactLocation * src, ArtifactLocation * dst, bool askAssemble = true) + : src(*src), dst(*dst), askAssemble(askAssemble) + { + } + ArtifactLocation src, dst; + bool askAssemble = true; + + void applyGs(CGameState * gs); + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & src; + h & dst; + h & askAssemble; + } +}; + +struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack +{ + struct LinkedSlots + { + ArtifactPosition srcPos; + ArtifactPosition dstPos; + + LinkedSlots() = default; + LinkedSlots(const ArtifactPosition & srcPos, const ArtifactPosition & dstPos) + : srcPos(srcPos) + , dstPos(dstPos) + { + } + template void serialize(Handler & h, const int version) + { + h & srcPos; + h & dstPos; + } + }; + + TArtHolder srcArtHolder; + TArtHolder dstArtHolder; + + BulkMoveArtifacts() + : swap(false) + { + } + BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) + : srcArtHolder(std::move(std::move(srcArtHolder))) + , dstArtHolder(std::move(std::move(dstArtHolder))) + , swap(swap) + { + } + + void applyGs(CGameState * gs); + + std::vector artsPack0; + std::vector artsPack1; + bool swap; + CArtifactSet * getSrcHolderArtSet(); + CArtifactSet * getDstHolderArtSet(); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & artsPack0; + h & artsPack1; + h & srcArtHolder; + h & dstArtHolder; + h & swap; + } +}; + +struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack +{ + ArtifactLocation al; //where assembly will be put + CArtifact * builtArt; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + h & builtArt; + } +}; + +struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack +{ + ArtifactLocation al; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & al; + } +}; + +struct DLL_LINKAGE HeroVisit : public CPackForClient +{ + PlayerColor player; + ObjectInstanceID heroId; + ObjectInstanceID objId; + + bool starting; //false -> ending + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & player; + h & heroId; + h & objId; + h & starting; + } +}; + +struct DLL_LINKAGE NewTurn : public CPackForClient +{ + enum weekType { NORMAL, DOUBLE_GROWTH, BONUS_GROWTH, DEITYOFFIRE, PLAGUE, NO_ACTION }; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + struct Hero + { + ObjectInstanceID id; + ui32 move, mana; //id is a general serial id + template void serialize(Handler & h, const int version) + { + h & id; + h & move; + h & mana; + } + bool operator<(const Hero & h)const { return id < h.id; } + }; + + std::set heroes; //updates movement and mana points + std::map res; //player ID => resource value[res_id] + std::map cres;//creatures to be placed in towns + ui32 day = 0; + ui8 specialWeek = 0; //weekType + CreatureID creatureid; //for creature weeks + + NewTurn() = default; + + template void serialize(Handler & h, const int version) + { + h & heroes; + h & cres; + h & res; + h & day; + h & specialWeek; + h & creatureid; + } +}; + +struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple info window +{ + EInfoWindowMode type = EInfoWindowMode::MODAL; + MetaString text; + std::vector components; + PlayerColor player; + ui16 soundID = 0; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & type; + h & text; + h & components; + h & player; + h & soundID; + } + InfoWindow() = default; +}; + +namespace ObjProperty +{ + enum + { + OWNER = 1, BLOCKVIS = 2, PRIMARY_STACK_COUNT = 3, VISITORS = 4, VISITED = 5, ID = 6, AVAILABLE_CREATURE = 7, SUBID = 8, + MONSTER_COUNT = 10, MONSTER_POWER = 11, MONSTER_EXP = 12, MONSTER_RESTORE_TYPE = 13, MONSTER_REFUSED_JOIN, + + //town-specific + STRUCTURE_ADD_VISITING_HERO, STRUCTURE_CLEAR_VISITORS, STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state + BONUS_VALUE_FIRST, BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) + + //creature-bank specific + BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, + + //object with reward + REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED + }; +} + +struct DLL_LINKAGE SetObjectProperty : public CPackForClient +{ + void applyGs(CGameState * gs) const; + ObjectInstanceID id; + ui8 what = 0; // see ObjProperty enum + ui32 val = 0; + SetObjectProperty() = default; + SetObjectProperty(const ObjectInstanceID & ID, ui8 What, ui32 Val) + : id(ID) + , what(What) + , val(Val) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & id; + h & what; + h & val; + } +}; + +struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient +{ + enum VisitMode + { + VISITOR_ADD, // mark hero as one that have visited this object + VISITOR_ADD_TEAM, // mark team as one that have visited this object + VISITOR_REMOVE, // unmark visitor, reversed to ADD + VISITOR_CLEAR // clear all visitors from this object (object reset) + }; + ui32 mode = VISITOR_CLEAR; // uses VisitMode enum + ObjectInstanceID object; + ObjectInstanceID hero; // note: hero owner will be also marked as "visited" this object + + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + ChangeObjectVisitors() = default; + + ChangeObjectVisitors(ui32 mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) + : mode(mode) + , object(object) + , hero(heroID) + { + } + + template void serialize(Handler & h, const int version) + { + h & object; + h & hero; + h & mode; + } +}; + +struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient +{ + ObjectInstanceID heroId; + + /// Do not serialize, used by server only + std::vector skills; + + void applyGs(CGameState * gs); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & heroId; + } +}; + +struct DLL_LINKAGE HeroLevelUp : public Query +{ + PlayerColor player; + ObjectInstanceID heroId; + + PrimarySkill primskill = PrimarySkill::ATTACK; + std::vector skills; + + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & heroId; + h & primskill; + h & skills; + } +}; + +struct DLL_LINKAGE CommanderLevelUp : public Query +{ + PlayerColor player; + ObjectInstanceID heroId; + + std::vector skills; //0-5 - secondary skills, val-100 - special skill + + void applyGs(CGameState * gs) const; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & heroId; + h & skills; + } +}; + +//A dialog that requires making decision by player - it may contain components to choose between or has yes/no options +//Client responds with QueryReply, where answer: 0 - cancel pressed, choice doesn't matter; 1/2/... - first/second/... component selected and OK pressed +//Until sending reply player won't be allowed to take any actions +struct DLL_LINKAGE BlockingDialog : public Query +{ + enum { ALLOW_CANCEL = 1, SELECTION = 2 }; + MetaString text; + std::vector components; + PlayerColor player; + ui8 flags = 0; + ui16 soundID = 0; + + bool cancel() const + { + return flags & ALLOW_CANCEL; + } + bool selection() const + { + return flags & SELECTION; + } + + BlockingDialog(bool yesno, bool Selection) + { + if(yesno) flags |= ALLOW_CANCEL; + if(Selection) flags |= SELECTION; + } + BlockingDialog() = default; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & text; + h & components; + h & player; + h & flags; + h & soundID; + } +}; + +struct DLL_LINKAGE GarrisonDialog : public Query +{ + ObjectInstanceID objid, hid; + bool removableUnits = false; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & objid; + h & hid; + h & removableUnits; + } +}; + +struct DLL_LINKAGE ExchangeDialog : public Query +{ + PlayerColor player; + + ObjectInstanceID hero1; + ObjectInstanceID hero2; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & hero1; + h & hero2; + } +}; + +struct DLL_LINKAGE TeleportDialog : public Query +{ + TeleportDialog() = default; + + TeleportDialog(const ObjectInstanceID & hero, const TeleportChannelID & Channel) + : hero(hero) + , channel(Channel) + { + } + ObjectInstanceID hero; + TeleportChannelID channel; + TTeleportExitsList exits; + bool impassable = false; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & hero; + h & channel; + h & exits; + h & impassable; + } +}; + +struct DLL_LINKAGE MapObjectSelectDialog : public Query +{ + PlayerColor player; + Component icon; + MetaString title; + MetaString description; + std::vector objects; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & queryID; + h & player; + h & icon; + h & title; + h & description; + h & objects; + } +}; + +struct DLL_LINKAGE BattleStart : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + BattleInfo * info = nullptr; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & info; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleNextRound : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + ui8 askPlayerInterface = true; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & askPlayerInterface; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleCancelled: public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultAccepted : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + struct HeroBattleResults + { + HeroBattleResults() + : hero(nullptr), army(nullptr), exp(0) {} + + CGHeroInstance * hero; + CArmedInstance * army; + TExpType exp; + + template void serialize(Handler & h, const int version) + { + h & hero; + h & army; + h & exp; + } + }; + + BattleID battleID = BattleID::NONE; + std::array heroResult; + ui8 winnerSide; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & heroResult; + h & winnerSide; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResult : public Query +{ + void applyFirstCl(CClient * cl); + + BattleID battleID = BattleID::NONE; + EBattleResult result = EBattleResult::NORMAL; + ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] + std::map casualties[2]; //first => casualties of attackers - map crid => number + TExpType exp[2] = {0, 0}; //exp for attacker and defender + std::set artifacts; //artifacts taken from loser to winner - currently unused + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & queryID; + h & result; + h & winner; + h & casualties[0]; + h & casualties[1]; + h & exp; + h & artifacts; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleLogMessage : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + std::vector lines; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & lines; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleStackMoved : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + std::vector tilesToMove; + int distance = 0; + bool teleporting = false; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & tilesToMove; + h & distance; + h & teleporting; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changedStacks; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changedStacks; + assert(battleID != BattleID::NONE); + } +}; + +struct BattleStackAttacked +{ + DLL_LINKAGE void applyGs(CGameState * gs); + DLL_LINKAGE void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + ui32 stackAttacked = 0, attackerID = 0; + ui32 killedAmount = 0; + int64_t damageAmount = 0; + UnitChanges newState; + enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; + ui32 flags = 0; //uses EFlags (above) + SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set + + bool killed() const//if target stack was killed + { + return flags & KILLED || flags & CLONE_KILLED; + } + bool cloneKilled() const + { + return flags & CLONE_KILLED; + } + bool isSecondary() const//if stack was not a primary target (receives no spell effects) + { + return flags & SECONDARY; + } + ///Attacked with spell (SPELL_LIKE_ATTACK) + bool isSpell() const + { + return flags & SPELL_EFFECT; + } + bool willRebirth() const//resurrection, e.g. Phoenix + { + return flags & REBIRTH; + } + bool fireShield() const + { + return flags & FIRE_SHIELD; + } + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackAttacked; + h & attackerID; + h & newState; + h & flags; + h & killedAmount; + h & damageAmount; + h & spellID; + assert(battleID != BattleID::NONE); + } + bool operator<(const BattleStackAttacked & b) const + { + return stackAttacked < b.stackAttacked; + } +}; + +struct DLL_LINKAGE BattleAttack : public CPackForClient +{ + void applyGs(CGameState * gs); + BattleUnitsChanged attackerChanges; + + BattleID battleID = BattleID::NONE; + std::vector bsa; + ui32 stackAttacking = 0; + ui32 flags = 0; //uses Eflags (below) + enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; + + BattleHex tile; + SpellID spellID = SpellID::NONE; //for SPELL_LIKE + + bool shot() const//distance attack - decrease number of shots + { + return flags & SHOT; + } + bool counter() const//is it counterattack? + { + return flags & COUNTER; + } + bool lucky() const + { + return flags & LUCKY; + } + bool unlucky() const + { + return flags & UNLUCKY; + } + bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg + { + return flags & BALLISTA_DOUBLE_DMG; + } + bool deathBlow() const + { + return flags & DEATH_BLOW; + } + bool spellLike() const + { + return flags & SPELL_LIKE; + } + bool lifeDrain() const + { + return flags & LIFE_DRAIN; + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & bsa; + h & stackAttacking; + h & flags; + h & tile; + h & spellID; + h & attackerChanges; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StartAction : public CPackForClient +{ + StartAction() = default; + StartAction(BattleAction act) + : ba(std::move(act)) + { + } + void applyFirstCl(CClient * cl); + void applyGs(CGameState * gs); + + BattleID battleID = BattleID::NONE; + BattleAction ba; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & ba; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE EndAction : public CPackForClient +{ + virtual void visitTyped(ICPackVisitor & visitor) override; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + } +}; + +struct DLL_LINKAGE BattleSpellCast : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + bool activeCast = true; + ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender + SpellID spellID; //id of spell + ui8 manaGained = 0; //mana channeling ability + BattleHex tile; //destination tile (may not be set in some global/mass spells + std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) + std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) + std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) + si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID + bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & side; + h & spellID; + h & manaGained; + h & tile; + h & affectedCres; + h & resistedCres; + h & reflectedCres; + h & casterStack; + h & castByHero; + h & activeCast; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE SetStackEffect : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector>> toAdd; + std::vector>> toUpdate; + std::vector>> toRemove; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & toAdd; + h & toUpdate; + h & toRemove; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StacksInjured : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector stacks; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stacks; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultsApplied : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + PlayerColor player1, player2; + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & player1; + h & player2; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changes; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changes; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE CatapultAttack : public CPackForClient +{ + struct AttackInfo + { + si16 destinationTile; + EWallPart attackedPart; + ui8 damageDealt; + + template void serialize(Handler & h, const int version) + { + h & destinationTile; + h & attackedPart; + h & damageDealt; + } + }; + + CatapultAttack(); + ~CatapultAttack() override; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector< AttackInfo > attackedParts; + int attacker = -1; //if -1, then a spell caused this + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & attackedParts; + h & attacker; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient +{ + enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; + + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + int stackID = 0; + BattleStackProperty which = CASTS; + int val = 0; + int absolute = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & which; + h & val; + h & absolute; + assert(battleID != BattleID::NONE); + } + +protected: + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +///activated at the beginning of turn +struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient +{ + void applyGs(CGameState * gs) const; //effect + + BattleID battleID = BattleID::NONE; + int stackID = 0; + int effect = 0; //use corresponding Bonus type + int val = 0; + int additionalInfo = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & effect; + h & val; + h & additionalInfo; + assert(battleID != BattleID::NONE); + } + +protected: + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + EGateState state = EGateState::NONE; + template void serialize(Handler & h, const int version) + { + h & battleID; + h & state; + assert(battleID != BattleID::NONE); + } + +protected: + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient +{ + ObjectInstanceID casterID; + SpellID spellID; + template void serialize(Handler & h, const int version) + { + h & casterID; + h & spellID; + } + +protected: + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient +{ + PlayerColor player; + bool showTerrain; // TODO: send terrain state + + std::vector objectPositions; + + template void serialize(Handler & h, const int version) + { + h & player; + h & showTerrain; + h & objectPositions; + } + +protected: + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +/***********************************************************************************************************/ + +struct DLL_LINKAGE GamePause : public CPackForServer +{ + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE EndTurn : public CPackForServer +{ + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE DismissHero : public CPackForServer +{ + DismissHero() = default; + DismissHero(const ObjectInstanceID & HID) + : hid(HID) + { + } + ObjectInstanceID hid; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + } +}; + +struct DLL_LINKAGE MoveHero : public CPackForServer +{ + MoveHero() = default; + MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) + : dest(Dest) + , hid(HID) + , transit(Transit) + { + } + int3 dest; + ObjectInstanceID hid; + bool transit = false; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + h & transit; + } +}; + +struct DLL_LINKAGE CastleTeleportHero : public CPackForServer +{ + CastleTeleportHero() = default; + CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) + : dest(Dest) + , hid(HID) + , source(Source) + { + } + ObjectInstanceID dest; + ObjectInstanceID hid; + si8 source = 0; //who give teleporting, 1=castle gate + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + } +}; + +struct DLL_LINKAGE ArrangeStacks : public CPackForServer +{ + ArrangeStacks() = default; + ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) + : what(W) + , p1(P1) + , p2(P2) + , id1(ID1) + , id2(ID2) + , val(VAL) + { + } + + ui8 what = 0; //1 - swap; 2 - merge; 3 - split + SlotID p1, p2; //positions of first and second stack + ObjectInstanceID id1, id2; //ids of objects with garrison + si32 val = 0; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & what; + h & p1; + h & p2; + h & id1; + h & id2; + h & val; + } +}; + +struct DLL_LINKAGE BulkMoveArmy : public CPackForServer +{ + SlotID srcSlot; + ObjectInstanceID srcArmy; + ObjectInstanceID destArmy; + + BulkMoveArmy() = default; + + BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) + : srcArmy(srcArmy) + , destArmy(destArmy) + , srcSlot(srcSlot) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcSlot; + h & srcArmy; + h & destArmy; + } +}; + +struct DLL_LINKAGE BulkSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + si32 amount = 0; + + BulkSplitStack() = default; + + BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) + : src(src) + , srcOwner(srcOwner) + , amount(howMany) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + h & amount; + } +}; + +struct DLL_LINKAGE BulkMergeStacks : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkMergeStacks() = default; + + BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkSmartSplitStack() = default; + + BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE DisbandCreature : public CPackForServer +{ + DisbandCreature() = default; + DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) + : pos(Pos) + , id(ID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + } +}; + +struct DLL_LINKAGE BuildStructure : public CPackForServer +{ + BuildStructure() = default; + BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) + : tid(TID) + , bid(BID) + { + } + ObjectInstanceID tid; //town id + BuildingID bid; //structure id + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & bid; + } +}; + +struct DLL_LINKAGE RazeStructure : public BuildStructure +{ + virtual void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE RecruitCreatures : public CPackForServer +{ + RecruitCreatures() = default; + RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) + : tid(TID) + , dst(DST) + , crid(CRID) + , amount(Amount) + , level(Level) + { + } + ObjectInstanceID tid; //dwelling id, or town + ObjectInstanceID dst; //destination ID, e.g. hero + CreatureID crid; + ui32 amount = 0; //creature amount + si32 level = 0; //dwelling level to buy from, -1 if any + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & dst; + h & crid; + h & amount; + h & level; + } +}; + +struct DLL_LINKAGE UpgradeCreature : public CPackForServer +{ + UpgradeCreature() = default; + UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) + : pos(Pos) + , id(ID) + , cid(CRID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + CreatureID cid; //id of type to which we want make upgrade + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + h & cid; + } +}; + +struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer +{ + GarrisonHeroSwap() = default; + GarrisonHeroSwap(const ObjectInstanceID & TID) + : tid(TID) + { + } + ObjectInstanceID tid; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + } +}; + +struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer +{ + ArtifactLocation src, dst; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & dst; + } +}; + +struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer +{ + ObjectInstanceID srcHero; + ObjectInstanceID dstHero; + bool swap = false; + bool equipped = true; + bool backpack = true; + + BulkExchangeArtifacts() = default; + BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) + : srcHero(srcHero) + , dstHero(dstHero) + , swap(swap) + , equipped(equipped) + , backpack(backpack) + { + } + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcHero; + h & dstHero; + h & swap; + h & equipped; + h & backpack; + } +}; + +struct DLL_LINKAGE AssembleArtifacts : public CPackForServer +{ + AssembleArtifacts() = default; + AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) + : heroID(_heroID) + , artifactSlot(_artifactSlot) + , assemble(_assemble) + , assembleTo(_assembleTo) + { + } + ObjectInstanceID heroID; + ArtifactPosition artifactSlot; + bool assemble = false; // True to assemble artifact, false to disassemble. + ArtifactID assembleTo; // Artifact to assemble into. + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & heroID; + h & artifactSlot; + h & assemble; + h & assembleTo; + } +}; + +struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer +{ + EraseArtifactByClient() = default; + EraseArtifactByClient(const ArtifactLocation & al) + : al(al) + { + } + ArtifactLocation al; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & al; + } +}; + +struct DLL_LINKAGE BuyArtifact : public CPackForServer +{ + BuyArtifact() = default; + BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) + : hid(HID) + , aid(AID) + { + } + ObjectInstanceID hid; + ArtifactID aid; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & aid; + } +}; + +struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer +{ + ObjectInstanceID marketId; + ObjectInstanceID heroId; + + EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; + std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] + std::vector val; //units of sold resource + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & marketId; + h & heroId; + h & mode; + h & r1; + h & r2; + h & val; + } +}; + +struct DLL_LINKAGE SetFormation : public CPackForServer +{ + SetFormation() = default; + ; + SetFormation(const ObjectInstanceID & HID, ui8 Formation) + : hid(HID) + , formation(Formation) + { + } + ObjectInstanceID hid; + ui8 formation = 0; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & formation; + } +}; + +struct DLL_LINKAGE HireHero : public CPackForServer +{ + HireHero() = default; + HireHero(HeroTypeID HID, const ObjectInstanceID & TID) + : hid(HID) + , tid(TID) + { + } + HeroTypeID hid; //available hero serial + ObjectInstanceID tid; //town (tavern) id + PlayerColor player; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & tid; + h & player; + } +}; + +struct DLL_LINKAGE BuildBoat : public CPackForServer +{ + ObjectInstanceID objid; //where player wants to buy a boat + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & objid; + } +}; + +struct DLL_LINKAGE QueryReply : public CPackForServer +{ + QueryReply() = default; + QueryReply(const QueryID & QID, std::optional Reply) + : qid(QID) + , reply(Reply) + { + } + QueryID qid; + PlayerColor player; + std::optional reply; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & qid; + h & player; + h & reply; + } +}; + +struct DLL_LINKAGE MakeAction : public CPackForServer +{ + MakeAction() = default; + MakeAction(BattleAction BA) + : ba(std::move(BA)) + { + } + BattleAction ba; + BattleID battleID; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & ba; + h & battleID; + } +}; + +struct DLL_LINKAGE DigWithHero : public CPackForServer +{ + ObjectInstanceID id; //digging hero id + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & id; + } +}; + +struct DLL_LINKAGE CastAdvSpell : public CPackForServer +{ + ObjectInstanceID hid; //hero id + SpellID sid; //spell id + int3 pos; //selected tile (not always used) + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & sid; + h & pos; + } +}; + +/***********************************************************************************************************/ + +struct DLL_LINKAGE SaveGame : public CPackForServer +{ + SaveGame() = default; + SaveGame(std::string Fname) + : fname(std::move(Fname)) + { + } + std::string fname; + + void applyGs(CGameState * gs) {}; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & fname; + } +}; + +struct DLL_LINKAGE PlayerMessage : public CPackForServer +{ + PlayerMessage() = default; + PlayerMessage(std::string Text, const ObjectInstanceID & obj) + : text(std::move(Text)) + , currObj(obj) + { + } + + void applyGs(CGameState * gs) {}; + + virtual void visitTyped(ICPackVisitor & visitor) override; + + std::string text; + ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & text; + h & currObj; + } +}; + +struct DLL_LINKAGE PlayerMessageClient : public CPackForClient +{ + PlayerMessageClient() = default; + PlayerMessageClient(const PlayerColor & Player, std::string Text) + : player(Player) + , text(std::move(Text)) + { + } + virtual void visitTyped(ICPackVisitor & visitor) override; + + PlayerColor player; + std::string text; + + template void serialize(Handler & h, const int version) + { + h & player; + h & text; + } +}; + +struct DLL_LINKAGE CenterView : public CPackForClient +{ + PlayerColor player; + int3 pos; + ui32 focusTime = 0; //ms + + virtual void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & pos; + h & player; + h & focusTime; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacksBase.h b/lib/NetPacksBase.h index e7e7cb635..e3d878706 100644 --- a/lib/NetPacksBase.h +++ b/lib/NetPacksBase.h @@ -1,286 +1,286 @@ -/* - * NetPacksBase.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 - -#include "ConstTransitivePtr.h" -#include "GameConstants.h" -#include "JsonNode.h" - -class CClient; -class CGameHandler; -class CLobbyScreen; -class CServerHandler; -class CVCMIServer; - -VCMI_LIB_NAMESPACE_BEGIN - -class CGameState; -class CConnection; -class CStackBasicDescriptor; -class CGHeroInstance; -class CStackInstance; -class CArmedInstance; -class CArtifactSet; -class CBonusSystemNode; -struct ArtSlotInfo; - -class ICPackVisitor; - -enum class EInfoWindowMode : uint8_t -{ - AUTO, - MODAL, - INFO -}; - -enum class EOpenWindowMode : uint8_t -{ - EXCHANGE_WINDOW, - RECRUITMENT_FIRST, - RECRUITMENT_ALL, - SHIPYARD_WINDOW, - THIEVES_GUILD, - UNIVERSITY_WINDOW, - HILL_FORT_WINDOW, - MARKET_WINDOW, - PUZZLE_MAP, - TAVERN_WINDOW -}; - -struct DLL_LINKAGE CPack -{ - std::shared_ptr c; // Pointer to connection that pack received from - - CPack() = default; - virtual ~CPack() = default; - - template void serialize(Handler &h, const int version) - { - logNetwork->error("CPack serialized... this should not happen!"); - assert(false && "CPack serialized"); - } - - void applyGs(CGameState * gs) - {} - - void visit(ICPackVisitor & cpackVisitor); - -protected: - /// - /// For basic types of netpacks hierarchy like CPackForClient. Called first. - /// - virtual void visitBasic(ICPackVisitor & cpackVisitor); - - /// - /// For leaf types of netpacks hierarchy. Called after visitBasic. - /// - virtual void visitTyped(ICPackVisitor & cpackVisitor); -}; - -struct DLL_LINKAGE CPackForClient : public CPack -{ -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct DLL_LINKAGE CPackForServer : public CPack -{ - mutable PlayerColor player = PlayerColor::NEUTRAL; - mutable si32 requestID; - - template void serialize(Handler &h, const int version) - { - h & player; - h & requestID; - } - -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct DLL_LINKAGE CPackForLobby : public CPack -{ - virtual bool isForServer() const; - -protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -struct Component -{ - enum class EComponentType : uint8_t - { - PRIM_SKILL, - SEC_SKILL, - RESOURCE, - CREATURE, - ARTIFACT, - EXPERIENCE, - SPELL, - MORALE, - LUCK, - BUILDING, - HERO_PORTRAIT, - FLAG, - INVALID //should be last - }; - EComponentType id = EComponentType::INVALID; - ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels - si32 val = 0; // + give; - take - si16 when = 0; // 0 - now; +x - within x days; -x - per x days - - template void serialize(Handler &h, const int version) - { - h & id; - h & subtype; - h & val; - h & when; - } - Component() = default; - DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); - Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) - :id(Type),subtype(Subtype),val(Val),when(When) - { - } -}; - -using TArtHolder = std::variant, ConstTransitivePtr>; - -struct ArtifactLocation -{ - TArtHolder artHolder;//TODO: identify holder by id - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; - - ArtifactLocation() - : artHolder(ConstTransitivePtr()) - { - } - template - ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) - : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) - : artHolder(std::move(std::move(ArtHolder))) - , slot(Slot) - { - } - - template - bool isHolder(const T *t) const - { - if(auto ptrToT = std::get>(artHolder)) - { - return ptrToT == t; - } - return false; - } - - DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) - - DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner - DLL_LINKAGE PlayerColor owningPlayer() const; - DLL_LINKAGE CArtifactSet *getHolderArtSet(); - DLL_LINKAGE CBonusSystemNode *getHolderNode(); - DLL_LINKAGE CArtifactSet *getHolderArtSet() const; - DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; - - DLL_LINKAGE const CArtifactInstance *getArt() const; - DLL_LINKAGE CArtifactInstance *getArt(); - DLL_LINKAGE const ArtSlotInfo *getSlot() const; - template void serialize(Handler &h, const int version) - { - h & artHolder; - h & slot; - } -}; - -class EntityChanges -{ -public: - Metatype metatype = Metatype::UNKNOWN; - int32_t entityIndex = 0; - JsonNode data; - template void serialize(Handler & h, const int version) - { - h & metatype; - h & entityIndex; - h & data; - } -}; - -class BattleChanges -{ -public: - enum class EOperation : si8 - { - ADD, - RESET_STATE, - UPDATE, - REMOVE, - }; - - JsonNode data; - EOperation operation = EOperation::RESET_STATE; - - BattleChanges() = default; - BattleChanges(EOperation operation_) - : operation(operation_) - { - } -}; - -class UnitChanges : public BattleChanges -{ -public: - uint32_t id = 0; - int64_t healthDelta = 0; - - UnitChanges() = default; - UnitChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_) - , id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & healthDelta; - h & data; - h & operation; - } -}; - -class ObstacleChanges : public BattleChanges -{ -public: - uint32_t id = 0; - - ObstacleChanges() = default; - - ObstacleChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_), - id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & data; - h & operation; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * NetPacksBase.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 + +#include "ConstTransitivePtr.h" +#include "GameConstants.h" +#include "JsonNode.h" + +class CClient; +class CGameHandler; +class CLobbyScreen; +class CServerHandler; +class CVCMIServer; + +VCMI_LIB_NAMESPACE_BEGIN + +class CGameState; +class CConnection; +class CStackBasicDescriptor; +class CGHeroInstance; +class CStackInstance; +class CArmedInstance; +class CArtifactSet; +class CBonusSystemNode; +struct ArtSlotInfo; + +class ICPackVisitor; + +enum class EInfoWindowMode : uint8_t +{ + AUTO, + MODAL, + INFO +}; + +enum class EOpenWindowMode : uint8_t +{ + EXCHANGE_WINDOW, + RECRUITMENT_FIRST, + RECRUITMENT_ALL, + SHIPYARD_WINDOW, + THIEVES_GUILD, + UNIVERSITY_WINDOW, + HILL_FORT_WINDOW, + MARKET_WINDOW, + PUZZLE_MAP, + TAVERN_WINDOW +}; + +struct DLL_LINKAGE CPack +{ + std::shared_ptr c; // Pointer to connection that pack received from + + CPack() = default; + virtual ~CPack() = default; + + template void serialize(Handler &h, const int version) + { + logNetwork->error("CPack serialized... this should not happen!"); + assert(false && "CPack serialized"); + } + + void applyGs(CGameState * gs) + {} + + void visit(ICPackVisitor & cpackVisitor); + +protected: + /// + /// For basic types of netpacks hierarchy like CPackForClient. Called first. + /// + virtual void visitBasic(ICPackVisitor & cpackVisitor); + + /// + /// For leaf types of netpacks hierarchy. Called after visitBasic. + /// + virtual void visitTyped(ICPackVisitor & cpackVisitor); +}; + +struct DLL_LINKAGE CPackForClient : public CPack +{ +protected: + virtual void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +struct DLL_LINKAGE CPackForServer : public CPack +{ + mutable PlayerColor player = PlayerColor::NEUTRAL; + mutable si32 requestID; + + template void serialize(Handler &h, const int version) + { + h & player; + h & requestID; + } + +protected: + virtual void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +struct DLL_LINKAGE CPackForLobby : public CPack +{ + virtual bool isForServer() const; + +protected: + virtual void visitBasic(ICPackVisitor & cpackVisitor) override; +}; + +struct Component +{ + enum class EComponentType : uint8_t + { + PRIM_SKILL, + SEC_SKILL, + RESOURCE, + CREATURE, + ARTIFACT, + EXPERIENCE, + SPELL, + MORALE, + LUCK, + BUILDING, + HERO_PORTRAIT, + FLAG, + INVALID //should be last + }; + EComponentType id = EComponentType::INVALID; + ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels + si32 val = 0; // + give; - take + si16 when = 0; // 0 - now; +x - within x days; -x - per x days + + template void serialize(Handler &h, const int version) + { + h & id; + h & subtype; + h & val; + h & when; + } + Component() = default; + DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); + Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) + :id(Type),subtype(Subtype),val(Val),when(When) + { + } +}; + +using TArtHolder = std::variant, ConstTransitivePtr>; + +struct ArtifactLocation +{ + TArtHolder artHolder;//TODO: identify holder by id + ArtifactPosition slot = ArtifactPosition::PRE_FIRST; + + ArtifactLocation() + : artHolder(ConstTransitivePtr()) + { + } + template + ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) + : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! + , slot(Slot) + { + } + ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) + : artHolder(std::move(std::move(ArtHolder))) + , slot(Slot) + { + } + + template + bool isHolder(const T *t) const + { + if(auto ptrToT = std::get>(artHolder)) + { + return ptrToT == t; + } + return false; + } + + DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) + + DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner + DLL_LINKAGE PlayerColor owningPlayer() const; + DLL_LINKAGE CArtifactSet *getHolderArtSet(); + DLL_LINKAGE CBonusSystemNode *getHolderNode(); + DLL_LINKAGE CArtifactSet *getHolderArtSet() const; + DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; + + DLL_LINKAGE const CArtifactInstance *getArt() const; + DLL_LINKAGE CArtifactInstance *getArt(); + DLL_LINKAGE const ArtSlotInfo *getSlot() const; + template void serialize(Handler &h, const int version) + { + h & artHolder; + h & slot; + } +}; + +class EntityChanges +{ +public: + Metatype metatype = Metatype::UNKNOWN; + int32_t entityIndex = 0; + JsonNode data; + template void serialize(Handler & h, const int version) + { + h & metatype; + h & entityIndex; + h & data; + } +}; + +class BattleChanges +{ +public: + enum class EOperation : si8 + { + ADD, + RESET_STATE, + UPDATE, + REMOVE, + }; + + JsonNode data; + EOperation operation = EOperation::RESET_STATE; + + BattleChanges() = default; + BattleChanges(EOperation operation_) + : operation(operation_) + { + } +}; + +class UnitChanges : public BattleChanges +{ +public: + uint32_t id = 0; + int64_t healthDelta = 0; + + UnitChanges() = default; + UnitChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_) + , id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & healthDelta; + h & data; + h & operation; + } +}; + +class ObstacleChanges : public BattleChanges +{ +public: + uint32_t id = 0; + + ObstacleChanges() = default; + + ObstacleChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_), + id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & data; + h & operation; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index c37000719..7dc98dc5c 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1,2613 +1,2613 @@ -/* - * NetPacksLib.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 "ArtifactUtils.h" -#include "NetPacks.h" -#include "NetPackVisitor.h" -#include "CGeneralTextHandler.h" -#include "CArtHandler.h" -#include "CHeroHandler.h" -#include "VCMI_Lib.h" -#include "mapping/CMap.h" -#include "spells/CSpellHandler.h" -#include "CCreatureHandler.h" -#include "gameState/CGameState.h" -#include "gameState/TavernHeroesPool.h" -#include "CStack.h" -#include "battle/BattleInfo.h" -#include "CTownHandler.h" -#include "mapping/CMapInfo.h" -#include "StartInfo.h" -#include "CPlayerState.h" -#include "TerrainHandler.h" -#include "mapObjects/CGCreature.h" -#include "mapObjects/CGMarket.h" -#include "mapObjectConstructors/AObjectTypeHandler.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "campaign/CampaignState.h" -#include "GameSettings.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void CPack::visit(ICPackVisitor & visitor) -{ - visitBasic(visitor); - - // visitBasic may destroy this and in such cases we do not want to call visitTyped - if(visitor.callTyped()) - { - visitTyped(visitor); - } -} - -void CPack::visitBasic(ICPackVisitor & visitor) -{ -} - -void CPack::visitTyped(ICPackVisitor & visitor) -{ -} - -void CPackForClient::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForClient(*this); -} - -void CPackForServer::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForServer(*this); -} - -void CPackForLobby::visitBasic(ICPackVisitor & visitor) -{ - visitor.visitForLobby(*this); -} - -bool CPackForLobby::isForServer() const -{ - return false; -} - -bool CLobbyPackToServer::isForServer() const -{ - return true; -} - -void PackageApplied::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPackageApplied(*this); -} - -void SystemMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSystemMessage(*this); -} - -void PlayerBlocked::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerBlocked(*this); -} - -void PlayerCheated::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerCheated(*this); -} - -void PlayerStartsTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerStartsTurn(*this); -} - -void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDaysWithoutTown(*this); -} - -void EntitiesChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEntitiesChanged(*this); -} - -void SetResources::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetResources(*this); -} - -void SetPrimSkill::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetPrimSkill(*this); -} - -void SetSecSkill::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetSecSkill(*this); -} - -void HeroVisitCastle::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroVisitCastle(*this); -} - -void ChangeSpells::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeSpells(*this); -} - -void SetMana::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetMana(*this); -} - -void SetMovePoints::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetMovePoints(*this); -} - -void FoWChange::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitFoWChange(*this); -} - -void SetAvailableHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableHeroes(*this); -} - -void GiveBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGiveBonus(*this); -} - -void ChangeObjPos::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeObjPos(*this); -} - -void PlayerEndsTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerEndsTurn(*this); -} - -void PlayerEndsGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerEndsGame(*this); -} - -void PlayerReinitInterface::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerReinitInterface(*this); -} - -void RemoveBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRemoveBonus(*this); -} - -void SetCommanderProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetCommanderProperty(*this); -} - -void AddQuest::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAddQuest(*this); -} - -void UpdateArtHandlerLists::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpdateArtHandlerLists(*this); -} - -void UpdateMapEvents::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpdateMapEvents(*this); -} - -void UpdateCastleEvents::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpdateCastleEvents(*this); -} - -void ChangeFormation::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeFormation(*this); -} - -void RemoveObject::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRemoveObject(*this); -} - -void TryMoveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTryMoveHero(*this); -} - -void NewStructures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewStructures(*this); -} - -void RazeStructures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRazeStructures(*this); -} - -void SetAvailableCreatures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableCreatures(*this); -} - -void SetHeroesInTown::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetHeroesInTown(*this); -} - -void HeroRecruited::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroRecruited(*this); -} - -void GiveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGiveHero(*this); -} - -void OpenWindow::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitOpenWindow(*this); -} - -void NewObject::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewObject(*this); -} - -void SetAvailableArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetAvailableArtifacts(*this); -} - -void NewArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewArtifact(*this); -} - -void ChangeStackCount::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeStackCount(*this); -} - -void SetStackType::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetStackType(*this); -} - -void EraseStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseStack(*this); -} - -void SwapStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSwapStacks(*this); -} - -void InsertNewStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitInsertNewStack(*this); -} - -void RebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRebalanceStacks(*this); -} - -void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkRebalanceStacks(*this); -} - -void BulkSmartRebalanceStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSmartRebalanceStacks(*this); -} - -void PutArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPutArtifact(*this); -} - -void EraseArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseArtifact(*this); -} - -void MoveArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMoveArtifact(*this); -} - -void BulkMoveArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMoveArtifacts(*this); -} - -void AssembledArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAssembledArtifact(*this); -} - -void DisassembledArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDisassembledArtifact(*this); -} - -void HeroVisit::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroVisit(*this); -} - -void NewTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitNewTurn(*this); -} - -void InfoWindow::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitInfoWindow(*this); -} - -void SetObjectProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetObjectProperty(*this); -} - -void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitChangeObjectVisitors(*this); -} - -void PrepareHeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPrepareHeroLevelUp(*this); -} - -void HeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHeroLevelUp(*this); -} - -void CommanderLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCommanderLevelUp(*this); -} - -void BlockingDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBlockingDialog(*this); -} - -void GarrisonDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGarrisonDialog(*this); -} - -void ExchangeDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitExchangeDialog(*this); -} - -void TeleportDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTeleportDialog(*this); -} - -void MapObjectSelectDialog::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMapObjectSelectDialog(*this); -} - -void BattleStart::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleStart(*this); -} - -void BattleNextRound::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleNextRound(*this); -} - -void BattleSetActiveStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSetActiveStack(*this); -} - -void BattleResult::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleResult(*this); -} - -void BattleLogMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleLogMessage(*this); -} - -void BattleStackMoved::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleStackMoved(*this); -} - -void BattleUnitsChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleUnitsChanged(*this); -} - -void BattleAttack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleAttack(*this); -} - -void StartAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitStartAction(*this); -} - -void EndAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEndAction(*this); -} - -void BattleSpellCast::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSpellCast(*this); -} - -void SetStackEffect::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetStackEffect(*this); -} - -void StacksInjured::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitStacksInjured(*this); -} - -void BattleResultsApplied::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleResultsApplied(*this); -} - -void BattleObstaclesChanged::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleObstaclesChanged(*this); -} - -void BattleSetStackProperty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleSetStackProperty(*this); -} - -void BattleTriggerEffect::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleTriggerEffect(*this); -} - -void BattleUpdateGateState::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBattleUpdateGateState(*this); -} - -void AdvmapSpellCast::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAdvmapSpellCast(*this); -} - -void ShowWorldViewEx::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitShowWorldViewEx(*this); -} - -void EndTurn::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEndTurn(*this); -} - -void GamePause::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGamePause(*this); -} - -void DismissHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDismissHero(*this); -} - -void MoveHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMoveHero(*this); -} - -void CastleTeleportHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCastleTeleportHero(*this); -} - -void ArrangeStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitArrangeStacks(*this); -} - -void BulkMoveArmy::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMoveArmy(*this); -} - -void BulkSplitStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSplitStack(*this); -} - -void BulkMergeStacks::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkMergeStacks(*this); -} - -void BulkSmartSplitStack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkSmartSplitStack(*this); -} - -void DisbandCreature::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDisbandCreature(*this); -} - -void BuildStructure::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuildStructure(*this); -} - -void RazeStructure::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRazeStructure(*this); -} - -void RecruitCreatures::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitRecruitCreatures(*this); -} - -void UpgradeCreature::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitUpgradeCreature(*this); -} - -void GarrisonHeroSwap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitGarrisonHeroSwap(*this); -} - -void ExchangeArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitExchangeArtifacts(*this); -} - -void BulkExchangeArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBulkExchangeArtifacts(*this); -} - -void AssembleArtifacts::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitAssembleArtifacts(*this); -} - -void EraseArtifactByClient::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitEraseArtifactByClient(*this); -} - -void BuyArtifact::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuyArtifact(*this); -} - -void TradeOnMarketplace::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitTradeOnMarketplace(*this); -} - -void SetFormation::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSetFormation(*this); -} - -void HireHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitHireHero(*this); -} - -void BuildBoat::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitBuildBoat(*this); -} - -void QueryReply::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitQueryReply(*this); -} - -void MakeAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitMakeAction(*this); -} - -void DigWithHero::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitDigWithHero(*this); -} - -void CastAdvSpell::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCastAdvSpell(*this); -} - -void SaveGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitSaveGame(*this); -} - -void PlayerMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerMessage(*this); -} - -void PlayerMessageClient::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPlayerMessageClient(*this); -} - -void CenterView::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCenterView(*this); -} - -void LobbyClientConnected::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyClientConnected(*this); -} - -void LobbyClientDisconnected::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyClientDisconnected(*this); -} - -void LobbyChatMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChatMessage(*this); -} - -void LobbyGuiAction::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyGuiAction(*this); -} - -void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyLoadProgress(*this); -} - -void LobbyEndGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyEndGame(*this); -} - -void LobbyStartGame::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyStartGame(*this); -} - -void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChangeHost(*this); -} - -void LobbyUpdateState::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyUpdateState(*this); -} - -void LobbySetMap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetMap(*this); -} - -void LobbySetCampaign::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaign(*this); -} - -void LobbySetCampaignMap::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaignMap(*this); -} - -void LobbySetCampaignBonus::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetCampaignBonus(*this); -} - -void LobbyChangePlayerOption::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyChangePlayerOption(*this); -} - -void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetPlayer(*this); -} - -void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetPlayerName(*this); -} - -void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetSimturns(*this); -} - -void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetTurnTime(*this); -} - -void LobbySetDifficulty::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbySetDifficulty(*this); -} - -void LobbyForceSetPlayer::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyForceSetPlayer(*this); -} - -void LobbyShowMessage::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitLobbyShowMessage(*this); -} - -void SetResources::applyGs(CGameState * gs) const -{ - assert(player.isValidPlayer()); - if(abs) - gs->getPlayerState(player)->resources = res; - else - gs->getPlayerState(player)->resources += res; - - //just ensure that player resources are not negative - //server is responsible to check if player can afford deal - //but events on server side are allowed to take more than player have - gs->getPlayerState(player)->resources.positive(); -} - -void SetPrimSkill::applyGs(CGameState * gs) const -{ - CGHeroInstance * hero = gs->getHero(id); - assert(hero); - hero->setPrimarySkill(which, val, abs); -} - -void SetSecSkill::applyGs(CGameState * gs) const -{ - CGHeroInstance *hero = gs->getHero(id); - hero->setSecSkillLevel(which, val, abs); -} - -void SetCommanderProperty::applyGs(CGameState *gs) -{ - CCommanderInstance * commander = gs->getHero(heroid)->commander; - assert (commander); - - switch (which) - { - case BONUS: - commander->accumulateBonus (std::make_shared(accumulatedBonus)); - break; - case SPECIAL_SKILL: - commander->accumulateBonus (std::make_shared(accumulatedBonus)); - commander->specialSkills.insert (additionalInfo); - break; - case SECONDARY_SKILL: - commander->secondarySkills[additionalInfo] = static_cast(amount); - break; - case ALIVE: - if (amount) - commander->setAlive(true); - else - commander->setAlive(false); - break; - case EXPERIENCE: - commander->giveStackExp(amount); //TODO: allow setting exp for stacks via netpacks - break; - } -} - -void AddQuest::applyGs(CGameState * gs) const -{ - assert (vstd::contains(gs->players, player)); - auto * vec = &gs->players[player].quests; - if (!vstd::contains(*vec, quest)) - vec->push_back (quest); - else - logNetwork->warn("Warning! Attempt to add duplicated quest"); -} - -void UpdateArtHandlerLists::applyGs(CGameState * gs) const -{ - VLC->arth->minors = minors; - VLC->arth->majors = majors; - VLC->arth->treasures = treasures; - VLC->arth->relics = relics; -} - -void UpdateMapEvents::applyGs(CGameState * gs) const -{ - gs->map->events = events; -} - -void UpdateCastleEvents::applyGs(CGameState * gs) const -{ - auto * t = gs->getTown(town); - t->events = events; -} - -void ChangeFormation::applyGs(CGameState * gs) const -{ - gs->getHero(hid)->setFormation(formation); -} - -void HeroVisitCastle::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->getHero(hid); - CGTownInstance *t = gs->getTown(tid); - - assert(h); - assert(t); - - if(start()) - t->setVisitingHero(h); - else - t->setVisitingHero(nullptr); -} - -void ChangeSpells::applyGs(CGameState *gs) -{ - CGHeroInstance *hero = gs->getHero(hid); - - if(learn) - for(const auto & sid : spells) - hero->addSpellToSpellbook(sid); - else - for(const auto & sid : spells) - hero->removeSpellFromSpellbook(sid); -} - -void SetMana::applyGs(CGameState * gs) const -{ - CGHeroInstance * hero = gs->getHero(hid); - - assert(hero); - - if(absolute) - hero->mana = val; - else - hero->mana += val; - - vstd::amax(hero->mana, 0); //not less than 0 -} - -void SetMovePoints::applyGs(CGameState * gs) const -{ - CGHeroInstance *hero = gs->getHero(hid); - - assert(hero); - - if(absolute) - hero->setMovementPoints(val); - else - hero->setMovementPoints(hero->movementPointsRemaining() + val); -} - -void FoWChange::applyGs(CGameState *gs) -{ - TeamState * team = gs->getPlayerTeam(player); - auto fogOfWarMap = team->fogOfWarMap; - for(const int3 & t : tiles) - (*fogOfWarMap)[t.z][t.x][t.y] = mode; - if (mode == 0) //do not hide too much - { - std::unordered_set tilesRevealed; - for (auto & elem : gs->map->objects) - { - const CGObjectInstance *o = elem; - if (o) - { - switch(o->ID) - { - case Obj::HERO: - case Obj::MINE: - case Obj::TOWN: - case Obj::ABANDONED_MINE: - if(vstd::contains(team->players, o->tempOwner)) //check owned observators - gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1); - break; - } - } - } - for(const int3 & t : tilesRevealed) //probably not the most optimal solution ever - (*fogOfWarMap)[t.z][t.x][t.y] = 1; - } -} - -void SetAvailableHero::applyGs(CGameState *gs) -{ - gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); -} - -void GiveBonus::applyGs(CGameState *gs) -{ - CBonusSystemNode *cbsn = nullptr; - switch(who) - { - case ETarget::HERO: - cbsn = gs->getHero(ObjectInstanceID(id)); - break; - case ETarget::PLAYER: - cbsn = gs->getPlayerState(PlayerColor(id)); - break; - case ETarget::TOWN: - cbsn = gs->getTown(ObjectInstanceID(id)); - break; - case ETarget::BATTLE: - assert(Bonus::OneBattle(&bonus)); - cbsn = dynamic_cast(gs->getBattle(BattleID(id))); - break; - } - - assert(cbsn); - - if(Bonus::OneWeek(&bonus)) - bonus.turnsRemain = 8 - gs->getDate(Date::DAY_OF_WEEK); // set correct number of days before adding bonus - - auto b = std::make_shared(bonus); - cbsn->addNewBonus(b); - - std::string &descr = b->description; - - if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) - { - if (bonus.source == BonusSource::OBJECT) - { - descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" - } - else if(bonus.source == BonusSource::TOWN_STRUCTURE) - { - descr = bonus.description; - return; - } - else - { - descr = bdescr.toString(); - } - } - else - { - descr = bdescr.toString(); - } - // Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them - boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); - boost::replace_first(descr, "%s", std::to_string(std::abs(bonus.val))); -} - -void ChangeObjPos::applyGs(CGameState *gs) -{ - CGObjectInstance *obj = gs->getObjInstance(objid); - if(!obj) - { - logNetwork->error("Wrong ChangeObjPos: object %d doesn't exist!", objid.getNum()); - return; - } - gs->map->removeBlockVisTiles(obj); - obj->pos = nPos + obj->getVisitableOffset(); - gs->map->addBlockVisTiles(obj); -} - -void ChangeObjectVisitors::applyGs(CGameState * gs) const -{ - switch (mode) { - case VISITOR_ADD: - gs->getHero(hero)->visitedObjects.insert(object); - gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object); - break; - case VISITOR_ADD_TEAM: - { - TeamState *ts = gs->getPlayerTeam(gs->getHero(hero)->tempOwner); - for(const auto & color : ts->players) - { - gs->getPlayerState(color)->visitedObjects.insert(object); - } - } - break; - case VISITOR_CLEAR: - for (CGHeroInstance * hero : gs->map->allHeroes) - { - if (hero) - { - hero->visitedObjects.erase(object); // remove visit info from all heroes, including those that are not present on map - } - } - - for(auto &elem : gs->players) - { - elem.second.visitedObjects.erase(object); - } - - break; - case VISITOR_REMOVE: - gs->getHero(hero)->visitedObjects.erase(object); - break; - } -} - -void PlayerEndsGame::applyGs(CGameState * gs) const -{ - PlayerState *p = gs->getPlayerState(player); - if(victoryLossCheckResult.victory()) - { - p->status = EPlayerStatus::WINNER; - - // TODO: Campaign-specific code might as well go somewhere else - // keep all heroes from the winning player - if(p->human && gs->scenarioOps->campState) - { - std::vector crossoverHeroes; - for (CGHeroInstance * hero : gs->map->heroesOnMap) - if (hero->tempOwner == player) - crossoverHeroes.push_back(hero); - - gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); - } - } - else - { - p->status = EPlayerStatus::LOSER; - } - - // defeated player may be making turn right now - gs->actingPlayers.erase(player); -} - -void PlayerReinitInterface::applyGs(CGameState *gs) -{ - if(!gs || !gs->scenarioOps) - return; - - //TODO: what does mean if more that one player connected? - if(playerConnectionId == PlayerSettings::PLAYER_AI) - { - for(const auto & player : players) - gs->scenarioOps->getIthPlayersSettings(player).connectedPlayerIDs.clear(); - } -} - -void RemoveBonus::applyGs(CGameState *gs) -{ - CBonusSystemNode * node = nullptr; - if (who == GiveBonus::ETarget::HERO) - node = gs->getHero(ObjectInstanceID(whoID)); - else - node = gs->getPlayerState(PlayerColor(whoID)); - - BonusList &bonuses = node->getExportedBonusList(); - - for(const auto & b : bonuses) - { - if(vstd::to_underlying(b->source) == source && b->sid == id) - { - bonus = *b; //backup bonus (to show to interfaces later) - node->removeBonus(b); - break; - } - } -} - -void RemoveObject::applyGs(CGameState *gs) -{ - - CGObjectInstance *obj = gs->getObjInstance(objectID); - logGlobal->debug("removing object id=%d; address=%x; name=%s", objectID, (intptr_t)obj, obj->getObjectName()); - //unblock tiles - gs->map->removeBlockVisTiles(obj); - - if(obj->ID == Obj::HERO) //remove beaten hero - { - auto * beatenHero = dynamic_cast(obj); - assert(beatenHero); - PlayerState * p = gs->getPlayerState(beatenHero->tempOwner); - gs->map->heroesOnMap -= beatenHero; - p->heroes -= beatenHero; - - - auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs); - - // FIXME: workaround: - // hero should be attached to siegeNode after battle - // however this code might also be called on dismissing hero while in town - if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode)) - beatenHero->detachFrom(*siegeNode); - - beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero - vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) - { - return asi.artifact->artType->getId() == ArtifactID::GRAIL; - }); - - if(beatenHero->visitedTown) - { - if(beatenHero->visitedTown->garrisonHero == beatenHero) - beatenHero->visitedTown->garrisonHero = nullptr; - else - beatenHero->visitedTown->visitingHero = nullptr; - - beatenHero->visitedTown = nullptr; - beatenHero->inTownGarrison = false; - } - //return hero to the pool, so he may reappear in tavern - - gs->heroesPool->addHeroToPool(beatenHero); - gs->map->objects[objectID.getNum()] = nullptr; - - //If hero on Boat is removed, the Boat disappears - if(beatenHero->boat) - { - beatenHero->detachFrom(const_cast(*beatenHero->boat)); - gs->map->instanceNames.erase(beatenHero->boat->instanceName); - gs->map->objects[beatenHero->boat->id.getNum()].dellNull(); - beatenHero->boat = nullptr; - } - return; - } - - const auto * quest = dynamic_cast(obj); - if (quest) - { - gs->map->quests[quest->quest->qid] = nullptr; - for (auto &player : gs->players) - { - for (auto &q : player.second.quests) - { - if (q.obj == obj) - { - q.obj = nullptr; - } - } - } - } - - for (TriggeredEvent & event : gs->map->triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - if (cond.object == obj) - { - if (cond.condition == EventCondition::DESTROY || cond.condition == EventCondition::DESTROY_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 1; // destroyed object, from now on always fulfilled - } - else if (cond.condition == EventCondition::CONTROL || cond.condition == EventCondition::HAVE_0) - { - cond.condition = EventCondition::CONST_VALUE; - cond.value = 0; // destroyed object, from now on can not be fulfilled - } - } - return cond; - }; - event.trigger = event.trigger.morph(patcher); - } - gs->map->instanceNames.erase(obj->instanceName); - gs->map->objects[objectID.getNum()].dellNull(); - gs->map->calculateGuardingGreaturePositions(); -} - -static int getDir(const int3 & src, const int3 & dst) -{ - int ret = -1; - if(dst.x+1 == src.x && dst.y+1 == src.y) //tl - { - ret = 1; - } - else if(dst.x == src.x && dst.y+1 == src.y) //t - { - ret = 2; - } - else if(dst.x-1 == src.x && dst.y+1 == src.y) //tr - { - ret = 3; - } - else if(dst.x-1 == src.x && dst.y == src.y) //r - { - ret = 4; - } - else if(dst.x-1 == src.x && dst.y-1 == src.y) //br - { - ret = 5; - } - else if(dst.x == src.x && dst.y-1 == src.y) //b - { - ret = 6; - } - else if(dst.x+1 == src.x && dst.y-1 == src.y) //bl - { - ret = 7; - } - else if(dst.x+1 == src.x && dst.y == src.y) //l - { - ret = 8; - } - return ret; -} - -void TryMoveHero::applyGs(CGameState *gs) -{ - CGHeroInstance *h = gs->getHero(id); - if (!h) - { - logGlobal->error("Attempt ot move unavailable hero %d", id.getNum()); - return; - } - - h->setMovementPoints(movePoints); - - if((result == SUCCESS || result == BLOCKING_VISIT || result == EMBARK || result == DISEMBARK) && start != end) - { - auto dir = getDir(start,end); - if(dir > 0 && dir <= 8) - h->moveDir = dir; - //else don`t change move direction - hero might have traversed the subterranean gate, direction should be kept - } - - if(result == EMBARK) //hero enters boat at destination tile - { - const TerrainTile &tt = gs->map->getTile(h->convertToVisitablePos(end)); - assert(tt.visitableObjects.size() >= 1 && tt.visitableObjects.back()->ID == Obj::BOAT); //the only visitable object at destination is Boat - auto * boat = dynamic_cast(tt.visitableObjects.back()); - assert(boat); - - gs->map->removeBlockVisTiles(boat); //hero blockvis mask will be used, we don't need to duplicate it with boat - h->boat = boat; - h->attachTo(*boat); - boat->hero = h; - } - else if(result == DISEMBARK) //hero leaves boat to destination tile - { - auto * b = const_cast(h->boat); - b->direction = h->moveDir; - b->pos = start; - b->hero = nullptr; - gs->map->addBlockVisTiles(b); - h->detachFrom(*b); - h->boat = nullptr; - } - - if(start!=end && (result == SUCCESS || result == TELEPORTATION || result == EMBARK || result == DISEMBARK)) - { - gs->map->removeBlockVisTiles(h); - h->pos = end; - if(auto * b = const_cast(h->boat)) - b->pos = end; - gs->map->addBlockVisTiles(h); - } - - auto fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; - for(const int3 & t : fowRevealed) - (*fogOfWarMap)[t.z][t.x][t.y] = 1; -} - -void NewStructures::applyGs(CGameState *gs) -{ - CGTownInstance *t = gs->getTown(tid); - - for(const auto & id : bid) - { - assert(t->town->buildings.at(id) != nullptr); - t->builtBuildings.insert(id); - t->updateAppearance(); - auto currentBuilding = t->town->buildings.at(id); - - if(currentBuilding->overrideBids.empty()) - continue; - - for(const auto & overrideBid : currentBuilding->overrideBids) - { - t->overriddenBuildings.insert(overrideBid); - t->deleteTownBonus(overrideBid); - } - } - t->builded = builded; - t->recreateBuildingsBonuses(); -} - -void RazeStructures::applyGs(CGameState *gs) -{ - CGTownInstance *t = gs->getTown(tid); - for(const auto & id : bid) - { - t->builtBuildings.erase(id); - - t->updateAppearance(); - } - t->destroyed = destroyed; //yeaha - t->recreateBuildingsBonuses(); -} - -void SetAvailableCreatures::applyGs(CGameState * gs) const -{ - auto * dw = dynamic_cast(gs->getObjInstance(tid)); - assert(dw); - dw->creatures = creatures; -} - -void SetHeroesInTown::applyGs(CGameState * gs) const -{ - CGTownInstance *t = gs->getTown(tid); - - CGHeroInstance * v = gs->getHero(visiting); - CGHeroInstance * g = gs->getHero(garrison); - - bool newVisitorComesFromGarrison = v && v == t->garrisonHero; - bool newGarrisonComesFromVisiting = g && g == t->visitingHero; - - if(newVisitorComesFromGarrison) - t->setGarrisonedHero(nullptr); - if(newGarrisonComesFromVisiting) - t->setVisitingHero(nullptr); - if(!newGarrisonComesFromVisiting || v) - t->setVisitingHero(v); - if(!newVisitorComesFromGarrison || g) - t->setGarrisonedHero(g); - - if(v) - { - gs->map->addBlockVisTiles(v); - } - if(g) - { - gs->map->removeBlockVisTiles(g); - } -} - -void HeroRecruited::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); - CGTownInstance *t = gs->getTown(tid); - PlayerState *p = gs->getPlayerState(player); - - if (boatId != ObjectInstanceID::NONE) - { - CGObjectInstance *obj = gs->getObjInstance(boatId); - auto * boat = dynamic_cast(obj); - if (boat) - { - gs->map->removeBlockVisTiles(boat); - h->attachToBoat(boat); - } - } - - h->setOwner(player); - h->pos = tile; - h->initObj(gs->getRandomGenerator()); - - if(h->id == ObjectInstanceID()) - { - h->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - gs->map->objects.emplace_back(h); - } - else - gs->map->objects[h->id.getNum()] = h; - - gs->map->heroesOnMap.emplace_back(h); - p->heroes.emplace_back(h); - h->attachTo(*p); - gs->map->addBlockVisTiles(h); - - if(t) - t->setVisitingHero(h); -} - -void GiveHero::applyGs(CGameState * gs) const -{ - CGHeroInstance *h = gs->getHero(id); - - if (boatId != ObjectInstanceID::NONE) - { - CGObjectInstance *obj = gs->getObjInstance(boatId); - auto * boat = dynamic_cast(obj); - if (boat) - { - gs->map->removeBlockVisTiles(boat); - h->attachToBoat(boat); - } - } - - //bonus system - h->detachFrom(gs->globalEffects); - h->attachTo(*gs->getPlayerState(player)); - - auto oldVisitablePos = h->visitablePos(); - gs->map->removeBlockVisTiles(h,true); - h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); - - h->setOwner(player); - h->setMovementPoints(h->movementPointsLimit(true)); - h->pos = h->convertFromVisitablePos(oldVisitablePos); - gs->map->heroesOnMap.emplace_back(h); - gs->getPlayerState(h->getOwner())->heroes.emplace_back(h); - - gs->map->addBlockVisTiles(h); - h->inTownGarrison = false; -} - -void NewObject::applyGs(CGameState *gs) -{ - TerrainId terrainType = ETerrainId::NONE; - - if (!gs->isInTheMap(targetPos)) - { - logGlobal->error("Attempt to create object outside map at %s!", targetPos.toString()); - return; - } - - const TerrainTile & t = gs->map->getTile(targetPos); - terrainType = t.terType->getId(); - - auto handler = VLC->objtypeh->getHandlerFor(ID, subID); - - CGObjectInstance * o = handler->create(); - handler->configureObject(o, gs->getRandomGenerator()); - - if (ID == Obj::MONSTER) //probably more options will be needed - { - //CStackInstance hlp; - auto * cre = dynamic_cast(o); - //cre->slots[0] = hlp; - assert(cre); - cre->notGrowingTeam = cre->neverFlees = false; - cre->character = 2; - cre->gainedArtifact = ArtifactID::NONE; - cre->identifier = -1; - cre->addToSlot(SlotID(0), new CStackInstance(CreatureID(subID), -1)); //add placeholder stack - } - - assert(!handler->getTemplates(terrainType).empty()); - if (handler->getTemplates().empty()) - { - logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID); - return; - } - - if (!handler->getTemplates(terrainType).empty()) - o->appearance = handler->getTemplates(terrainType).front(); - else - o->appearance = handler->getTemplates().front(); - - o->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - o->ID = ID; - o->subID = subID; - o->pos = targetPos + o->getVisitableOffset(); - - gs->map->objects.emplace_back(o); - gs->map->addBlockVisTiles(o); - o->initObj(gs->getRandomGenerator()); - gs->map->calculateGuardingGreaturePositions(); - - createdObjectID = o->id; - - logGlobal->debug("Added object id=%d; address=%x; name=%s", o->id, (intptr_t)o, o->getObjectName()); -} - -void NewArtifact::applyGs(CGameState *gs) -{ - assert(!vstd::contains(gs->map->artInstances, art)); - assert(!art->getParentNodes().size()); - assert(art->artType); - - art->setType(art->artType); - if(art->isCombined()) - { - for(const auto & part : art->artType->getConstituents()) - art->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); - } - gs->map->addNewArtifactInstance(art); -} - -const CStackInstance * StackLocation::getStack() -{ - if(!army->hasStackAtSlot(slot)) - { - logNetwork->warn("%s don't have a stack at slot %d", army->nodeName(), slot.getNum()); - return nullptr; - } - return &army->getStack(slot); -} - -struct ObjectRetriever -{ - const CArmedInstance * operator()(const ConstTransitivePtr &h) const - { - return h; - } - const CArmedInstance * operator()(const ConstTransitivePtr &s) const - { - return s->armyObj; - } -}; -template -struct GetBase -{ - template - T * operator()(TArg &arg) const - { - return arg; - } -}; - - -void ArtifactLocation::removeArtifact() -{ - CArtifactInstance *a = getArt(); - assert(a); - a->removeFrom(*this); -} - -const CArmedInstance * ArtifactLocation::relatedObj() const -{ - return std::visit(ObjectRetriever(), artHolder); -} - -PlayerColor ArtifactLocation::owningPlayer() const -{ - const auto * obj = relatedObj(); - return obj ? obj->tempOwner : PlayerColor::NEUTRAL; -} - -CArtifactSet *ArtifactLocation::getHolderArtSet() -{ - return std::visit(GetBase(), artHolder); -} - -CBonusSystemNode *ArtifactLocation::getHolderNode() -{ - return std::visit(GetBase(), artHolder); -} - -const CArtifactInstance *ArtifactLocation::getArt() const -{ - const auto * s = getSlot(); - if(s) - return s->getArt(); - else - return nullptr; -} - -CArtifactSet * ArtifactLocation::getHolderArtSet() const -{ - auto * t = const_cast(this); - return t->getHolderArtSet(); -} - -const CBonusSystemNode * ArtifactLocation::getHolderNode() const -{ - auto * t = const_cast(this); - return t->getHolderNode(); -} - -CArtifactInstance *ArtifactLocation::getArt() -{ - const ArtifactLocation *t = this; - return const_cast(t->getArt()); -} - -const ArtSlotInfo *ArtifactLocation::getSlot() const -{ - return getHolderArtSet()->getSlot(slot); -} - -void ChangeStackCount::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] ChangeStackCount: invalid army object %d, possible game state corruption.", army.getNum()); - - if(absoluteValue) - srcObj->setStackCount(slot, count); - else - srcObj->changeStackCount(slot, count); -} - -void SetStackType::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] SetStackType: invalid army object %d, possible game state corruption.", army.getNum()); - - srcObj->setStackType(slot, type); -} - -void EraseStack::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(army); - if(!srcObj) - logNetwork->error("[CRITICAL] EraseStack: invalid army object %d, possible game state corruption.", army.getNum()); - - srcObj->eraseStack(slot); -} - -void SwapStacks::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(srcArmy); - if(!srcObj) - logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); - - auto * dstObj = gs->getArmyInstance(dstArmy); - if(!dstObj) - logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); - - CStackInstance * s1 = srcObj->detachStack(srcSlot); - CStackInstance * s2 = dstObj->detachStack(dstSlot); - - srcObj->putStack(srcSlot, s2); - dstObj->putStack(dstSlot, s1); -} - -void InsertNewStack::applyGs(CGameState *gs) -{ - if(auto * obj = gs->getArmyInstance(army)) - obj->putStack(slot, new CStackInstance(type, count)); - else - logNetwork->error("[CRITICAL] InsertNewStack: invalid army object %d, possible game state corruption.", army.getNum()); -} - -void RebalanceStacks::applyGs(CGameState * gs) -{ - auto * srcObj = gs->getArmyInstance(srcArmy); - if(!srcObj) - logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); - - auto * dstObj = gs->getArmyInstance(dstArmy); - if(!dstObj) - logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); - - StackLocation src(srcObj, srcSlot); - StackLocation dst(dstObj, dstSlot); - - const CCreature * srcType = src.army->getCreature(src.slot); - TQuantity srcCount = src.army->getStackCount(src.slot); - bool stackExp = VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); - - if(srcCount == count) //moving whole stack - { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); - - if(c) //stack at dest -> merge - { - assert(c == srcType); - auto alHere = ArtifactLocation (src.getStack(), ArtifactPosition::CREATURE_SLOT); - auto alDest = ArtifactLocation (dst.getStack(), ArtifactPosition::CREATURE_SLOT); - auto * artHere = alHere.getArt(); - auto * artDest = alDest.getArt(); - if (artHere) - { - if (alDest.getArt()) - { - auto * hero = dynamic_cast(src.army.get()); - auto dstSlot = ArtifactUtils::getArtBackpackPosition(hero, alDest.getArt()->getTypeId()); - if(hero && dstSlot != ArtifactPosition::PRE_FIRST) - { - artDest->move (alDest, ArtifactLocation (hero, dstSlot)); - } - //else - artifact cna be lost :/ - else - { - EraseArtifact ea; - ea.al = alDest; - ea.applyGs(gs); - logNetwork->warn("Cannot move artifact! No free slots"); - } - artHere->move (alHere, alDest); - //TODO: choose from dialog - } - else //just move to the other slot before stack gets erased - { - artHere->move (alHere, alDest); - } - } - if (stackExp) - { - ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); - src.army->eraseStack(src.slot); - dst.army->changeStackCount(dst.slot, count); - dst.army->setStackExp(dst.slot, totalExp /(dst.army->getStackCount(dst.slot))); //mean - } - else - { - src.army->eraseStack(src.slot); - dst.army->changeStackCount(dst.slot, count); - } - } - else //move stack to an empty slot, no exp change needed - { - CStackInstance *stackDetached = src.army->detachStack(src.slot); - dst.army->putStack(dst.slot, stackDetached); - } - } - else - { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); - if(c) //stack at dest -> rebalance - { - assert(c == srcType); - if (stackExp) - { - ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); - src.army->changeStackCount(src.slot, -count); - dst.army->changeStackCount(dst.slot, count); - dst.army->setStackExp(dst.slot, totalExp /(src.army->getStackCount(src.slot) + dst.army->getStackCount(dst.slot))); //mean - } - else - { - src.army->changeStackCount(src.slot, -count); - dst.army->changeStackCount(dst.slot, count); - } - } - else //split stack to an empty slot - { - src.army->changeStackCount(src.slot, -count); - dst.army->addToSlot(dst.slot, srcType->getId(), count, false); - if (stackExp) - dst.army->setStackExp(dst.slot, src.army->getStackExperience(src.slot)); - } - } - - CBonusSystemNode::treeHasChanged(); -} - -void BulkRebalanceStacks::applyGs(CGameState * gs) -{ - for(auto & move : moves) - move.applyGs(gs); -} - -void BulkSmartRebalanceStacks::applyGs(CGameState * gs) -{ - for(auto & move : moves) - move.applyGs(gs); - - for(auto & change : changes) - change.applyGs(gs); -} - -void PutArtifact::applyGs(CGameState *gs) -{ - assert(art->canBePutAt(al)); - // Ensure that artifact has been correctly added via NewArtifact pack - assert(vstd::contains(gs->map->artInstances, art)); - assert(!art->getParentNodes().empty()); - art->putAt(al); -} - -void EraseArtifact::applyGs(CGameState *gs) -{ - const auto * slot = al.getSlot(); - if(slot->locked) - { - logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); - DisassembledArtifact dis; - dis.al.artHolder = al.artHolder; - auto * aset = al.getHolderArtSet(); - #ifndef NDEBUG - bool found = false; - #endif - for(auto& p : aset->artifactsWorn) - { - auto art = p.second.artifact; - if(art->isCombined() && art->isPart(slot->artifact)) - { - dis.al.slot = aset->getArtPos(art); - #ifndef NDEBUG - found = true; - #endif - break; - } - } - assert(found && "Failed to determine the assembly this locked artifact belongs to"); - logGlobal->debug("Found the corresponding assembly: %s", dis.al.getSlot()->artifact->artType->getNameTranslated()); - dis.applyGs(gs); - } - else - { - logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); - } - al.removeArtifact(); -} - -void MoveArtifact::applyGs(CGameState * gs) -{ - CArtifactInstance * art = src.getArt(); - assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); - art->move(src, dst); -} - -void BulkMoveArtifacts::applyGs(CGameState * gs) -{ - enum class EBulkArtsOp - { - BULK_MOVE, - BULK_REMOVE, - BULK_PUT - }; - - auto bulkArtsOperation = [this](std::vector & artsPack, - CArtifactSet * artSet, EBulkArtsOp operation) -> void - { - int numBackpackArtifactsMoved = 0; - for(auto & slot : artsPack) - { - // When an object gets removed from the backpack, the backpack shrinks - // so all the following indices will be affected. Thus, we need to update - // the subsequent artifact slots to account for that - auto srcPos = slot.srcPos; - if(ArtifactUtils::isSlotBackpack(srcPos) && (operation != EBulkArtsOp::BULK_PUT)) - { - srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); - } - const auto * slotInfo = artSet->getSlot(srcPos); - assert(slotInfo); - auto * art = const_cast(slotInfo->getArt()); - assert(art); - switch(operation) - { - case EBulkArtsOp::BULK_MOVE: - const_cast(art)->move( - ArtifactLocation(srcArtHolder, srcPos), ArtifactLocation(dstArtHolder, slot.dstPos)); - break; - case EBulkArtsOp::BULK_REMOVE: - art->removeFrom(ArtifactLocation(dstArtHolder, srcPos)); - break; - case EBulkArtsOp::BULK_PUT: - art->putAt(ArtifactLocation(srcArtHolder, slot.dstPos)); - break; - default: - break; - } - - if(srcPos >= ArtifactPosition::BACKPACK_START) - { - numBackpackArtifactsMoved++; - } - } - }; - - if(swap) - { - // Swap - auto * leftSet = getSrcHolderArtSet(); - auto * rightSet = getDstHolderArtSet(); - CArtifactFittingSet artFittingSet(leftSet->bearerType()); - - artFittingSet.artifactsWorn = rightSet->artifactsWorn; - artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; - - bulkArtsOperation(artsPack1, rightSet, EBulkArtsOp::BULK_REMOVE); - bulkArtsOperation(artsPack0, leftSet, EBulkArtsOp::BULK_MOVE); - bulkArtsOperation(artsPack1, &artFittingSet, EBulkArtsOp::BULK_PUT); - } - else - { - bulkArtsOperation(artsPack0, getSrcHolderArtSet(), EBulkArtsOp::BULK_MOVE); - } -} - -void AssembledArtifact::applyGs(CGameState *gs) -{ - CArtifactSet * artSet = al.getHolderArtSet(); - const CArtifactInstance * transformedArt = al.getArt(); - assert(transformedArt); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool - { - return art->getId() == builtArt->getId(); - })); - - const auto transformedArtSlot = artSet->getSlotByInstance(transformedArt); - auto * combinedArt = new CArtifactInstance(builtArt); - gs->map->addNewArtifactInstance(combinedArt); - - // Find slots for all involved artifacts - std::vector slotsInvolved; - for(const auto constituent : builtArt->getConstituents()) - { - ArtifactPosition slot; - if(transformedArt->getTypeId() == constituent->getId()) - slot = transformedArtSlot; - else - slot = artSet->getArtPos(constituent->getId(), false, false); - - assert(slot != ArtifactPosition::PRE_FIRST); - slotsInvolved.emplace_back(slot); - } - std::sort(slotsInvolved.begin(), slotsInvolved.end(), std::greater<>()); - - // Find a slot for combined artifact - al.slot = transformedArtSlot; - for(const auto slot : slotsInvolved) - { - if(ArtifactUtils::isSlotEquipment(transformedArtSlot)) - { - - if(ArtifactUtils::isSlotBackpack(slot)) - { - al.slot = ArtifactPosition::BACKPACK_START; - break; - } - - if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), slot)) - al.slot = slot; - } - else - { - if(ArtifactUtils::isSlotBackpack(slot)) - al.slot = std::min(al.slot, slot); - } - } - - // Delete parts from hero - for(const auto slot : slotsInvolved) - { - const auto constituentInstance = artSet->getArt(slot); - constituentInstance->removeFrom(ArtifactLocation(al.artHolder, slot)); - - if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) - combinedArt->addPart(constituentInstance, slot); - else - combinedArt->addPart(constituentInstance, ArtifactPosition::PRE_FIRST); - } - - // Put new combined artifacts - combinedArt->putAt(al); -} - -void DisassembledArtifact::applyGs(CGameState *gs) -{ - auto * disassembled = al.getArt(); - assert(disassembled); - - auto parts = disassembled->getPartsInfo(); - disassembled->removeFrom(al); - for(auto & part : parts) - { - ArtifactLocation partLoc = al; - // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos - partLoc.slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); - disassembled->detachFrom(*part.art); - part.art->putAt(partLoc); - } - gs->map->eraseArtifactInstance(disassembled); -} - -void HeroVisit::applyGs(CGameState *gs) -{ -} - -void SetAvailableArtifacts::applyGs(CGameState * gs) const -{ - if(id >= 0) - { - if(auto * bm = dynamic_cast(gs->map->objects[id].get())) - { - bm->artifacts = arts; - } - else - { - logNetwork->error("Wrong black market id!"); - } - } - else - { - CGTownInstance::merchantArtifacts = arts; - } -} - -void NewTurn::applyGs(CGameState *gs) -{ - gs->day = day; - - // Update bonuses before doing anything else so hero don't get more MP than needed - gs->globalEffects.removeBonusesRecursive(Bonus::OneDay); //works for children -> all game objs - gs->globalEffects.reduceBonusDurations(Bonus::NDays); - gs->globalEffects.reduceBonusDurations(Bonus::OneWeek); - //TODO not really a single root hierarchy, what about bonuses placed elsewhere? [not an issue with H3 mechanics but in the future...] - - for(const NewTurn::Hero & h : heroes) //give mana/movement point - { - CGHeroInstance *hero = gs->getHero(h.id); - if(!hero) - { - logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum()); - continue; - } - - hero->setMovementPoints(h.move); - hero->mana = h.mana; - } - - gs->heroesPool->onNewDay(); - - for(const auto & re : res) - { - assert(re.first.isValidPlayer()); - gs->getPlayerState(re.first)->resources = re.second; - } - - for(const auto & creatureSet : cres) //set available creatures in towns - creatureSet.second.applyGs(gs); - - for(CGTownInstance* t : gs->map->towns) - t->builded = 0; - - if(gs->getDate(Date::DAY_OF_WEEK) == 1) - gs->updateRumor(); -} - -void SetObjectProperty::applyGs(CGameState * gs) const -{ - CGObjectInstance *obj = gs->getObjInstance(id); - if(!obj) - { - logNetwork->error("Wrong object ID - property cannot be set!"); - return; - } - - auto * cai = dynamic_cast(obj); - if(what == ObjProperty::OWNER && cai) - { - if(obj->ID == Obj::TOWN) - { - auto * t = dynamic_cast(obj); - assert(t); - - PlayerColor oldOwner = t->tempOwner; - if(oldOwner.isValidPlayer()) - { - auto * state = gs->getPlayerState(oldOwner); - state->towns -= t; - - if(state->towns.empty()) - *state->daysWithoutCastle = 0; - } - if(PlayerColor(val).isValidPlayer()) - { - PlayerState * p = gs->getPlayerState(PlayerColor(val)); - p->towns.emplace_back(t); - - //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured - if(p->daysWithoutCastle) - p->daysWithoutCastle = std::nullopt; - } - } - - CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); - nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); - obj->setProperty(what,val); - nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); - } - else //not an armed instance - { - obj->setProperty(what,val); - } -} - -void PrepareHeroLevelUp::applyGs(CGameState * gs) -{ - auto * hero = gs->getHero(heroId); - assert(hero); - - auto proposedSkills = hero->getLevelUpProposedSecondarySkills(); - - if(skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - skills.push_back(*RandomGeneratorUtil::nextItem(proposedSkills, hero->skillsInfo.rand)); - } - else - { - skills = proposedSkills; - } -} - -void HeroLevelUp::applyGs(CGameState * gs) const -{ - auto * hero = gs->getHero(heroId); - assert(hero); - hero->levelUp(skills); -} - -void CommanderLevelUp::applyGs(CGameState * gs) const -{ - auto * hero = gs->getHero(heroId); - assert(hero); - auto commander = hero->commander; - assert(commander); - commander->levelUp(); -} - -void BattleStart::applyGs(CGameState * gs) const -{ - assert(battleID == gs->nextBattleID); - - gs->currentBattles.emplace_back(info); - - info->battleID = gs->nextBattleID; - info->localInit(); - - gs->nextBattleID = vstd::next(gs->nextBattleID, 1); -} - -void BattleNextRound::applyGs(CGameState * gs) const -{ - gs->getBattle(battleID)->nextRound(); -} - -void BattleSetActiveStack::applyGs(CGameState * gs) const -{ - gs->getBattle(battleID)->nextTurn(stack); -} - -void BattleTriggerEffect::applyGs(CGameState * gs) const -{ - CStack * st = gs->getBattle(battleID)->getStack(stackID); - assert(st); - switch(static_cast(effect)) - { - case BonusType::HP_REGENERATION: - { - int64_t toHeal = val; - st->heal(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); - break; - } - case BonusType::MANA_DRAIN: - { - CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); - st->drainedMana = true; - h->mana -= val; - vstd::amax(h->mana, 0); - break; - } - case BonusType::POISON: - { - auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON) - .And(Selector::type()(BonusType::STACK_HEALTH))); - if (b) - b->val = val; - break; - } - case BonusType::ENCHANTER: - case BonusType::MORALE: - break; - case BonusType::FEAR: - st->fear = true; - break; - default: - logNetwork->error("Unrecognized trigger effect type %d", effect); - } -} - -void BattleUpdateGateState::applyGs(CGameState * gs) const -{ - if(gs->getBattle(battleID)) - gs->getBattle(battleID)->si.gateState = state; -} - -void BattleCancelled::applyGs(CGameState * gs) const -{ - auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) - { - return battle->battleID == battleID; - }); - - assert(currentBattle != gs->currentBattles.end()); - gs->currentBattles.erase(currentBattle); -} - -void BattleResultAccepted::applyGs(CGameState * gs) const -{ - // Remove any "until next battle" bonuses - for(auto & res : heroResult) - { - if(res.hero) - res.hero->removeBonusesRecursive(Bonus::OneBattle); - } - - if(winnerSide != 2) - { - // Grow up growing artifacts - const auto hero = heroResult[winnerSide].hero; - - if (hero) - { - if(hero->commander && hero->commander->alive) - { - for(auto & art : hero->commander->artifactsWorn) - art.second.artifact->growingUp(); - } - for(auto & art : hero->artifactsWorn) - { - art.second.artifact->growingUp(); - } - } - } - - if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - { - if(heroResult[0].army) - heroResult[0].army->giveStackExp(heroResult[0].exp); - if(heroResult[1].army) - heroResult[1].army->giveStackExp(heroResult[1].exp); - CBonusSystemNode::treeHasChanged(); - } - - auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) - { - return battle->battleID == battleID; - }); - - assert(currentBattle != gs->currentBattles.end()); - gs->currentBattles.erase(currentBattle); -} - -void BattleLogMessage::applyGs(CGameState *gs) -{ - //nothing -} - -void BattleLogMessage::applyBattle(IBattleState * battleState) -{ - //nothing -} - -void BattleStackMoved::applyGs(CGameState *gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void BattleStackMoved::applyBattle(IBattleState * battleState) -{ - battleState->moveUnit(stack, tilesToMove.back()); -} - -void BattleStackAttacked::applyGs(CGameState * gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void BattleStackAttacked::applyBattle(IBattleState * battleState) -{ - battleState->setUnitState(newState.id, newState.data, newState.healthDelta); -} - -void BattleAttack::applyGs(CGameState * gs) -{ - CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking); - assert(attacker); - - attackerChanges.applyGs(gs); - - for(BattleStackAttacked & stackAttacked : bsa) - stackAttacked.applyGs(gs); - - attacker->removeBonusesRecursive(Bonus::UntilAttack); -} - -void StartAction::applyGs(CGameState *gs) -{ - CStack *st = gs->getBattle(battleID)->getStack(ba.stackNumber); - - if(ba.actionType == EActionType::END_TACTIC_PHASE) - { - gs->getBattle(battleID)->tacticDistance = 0; - return; - } - - if(gs->getBattle(battleID)->tacticDistance) - { - // moves in tactics phase do not affect creature status - // (tactics stack queue is managed by client) - return; - } - - if (ba.isUnitAction()) - { - assert(st); // stack must exists for all non-hero actions - - switch(ba.actionType) - { - case EActionType::DEFEND: - st->waiting = false; - st->defending = true; - st->defendingAnim = true; - break; - case EActionType::WAIT: - st->defendingAnim = false; - st->waiting = true; - st->waitedThisTurn = true; - break; - case EActionType::HERO_SPELL: //no change in current stack state - break; - default: //any active stack action - attack, catapult, heal, spell... - st->waiting = false; - st->defendingAnim = false; - st->movedThisRound = true; - break; - } - } - else - { - if(ba.actionType == EActionType::HERO_SPELL) - gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell); - } -} - -void BattleSpellCast::applyGs(CGameState * gs) const -{ - if(castByHero) - { - if(side < 2) - { - gs->getBattle(battleID)->sides[side].castSpellsCount++; - } - } -} - -void SetStackEffect::applyGs(CGameState *gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void SetStackEffect::applyBattle(IBattleState * battleState) -{ - for(const auto & stackData : toRemove) - battleState->removeUnitBonus(stackData.first, stackData.second); - - for(const auto & stackData : toUpdate) - battleState->updateUnitBonus(stackData.first, stackData.second); - - for(const auto & stackData : toAdd) - battleState->addUnitBonus(stackData.first, stackData.second); -} - - -void StacksInjured::applyGs(CGameState *gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void StacksInjured::applyBattle(IBattleState * battleState) -{ - for(BattleStackAttacked stackAttacked : stacks) - stackAttacked.applyBattle(battleState); -} - -void BattleUnitsChanged::applyGs(CGameState *gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void BattleUnitsChanged::applyBattle(IBattleState * battleState) -{ - for(auto & elem : changedStacks) - { - switch(elem.operation) - { - case BattleChanges::EOperation::RESET_STATE: - battleState->setUnitState(elem.id, elem.data, elem.healthDelta); - break; - case BattleChanges::EOperation::REMOVE: - battleState->removeUnit(elem.id); - break; - case BattleChanges::EOperation::ADD: - battleState->addUnit(elem.id, elem.data); - break; - case BattleChanges::EOperation::UPDATE: - battleState->updateUnit(elem.id, elem.data); - break; - default: - logNetwork->error("Unknown unit operation %d", static_cast(elem.operation)); - break; - } - } -} - -void BattleObstaclesChanged::applyGs(CGameState * gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void BattleObstaclesChanged::applyBattle(IBattleState * battleState) -{ - for(const auto & change : changes) - { - switch(change.operation) - { - case BattleChanges::EOperation::REMOVE: - battleState->removeObstacle(change.id); - break; - case BattleChanges::EOperation::ADD: - battleState->addObstacle(change); - break; - case BattleChanges::EOperation::UPDATE: - battleState->updateObstacle(change); - break; - default: - logNetwork->error("Unknown obstacle operation %d", static_cast(change.operation)); - break; - } - } -} - -CatapultAttack::CatapultAttack() = default; - -CatapultAttack::~CatapultAttack() = default; - -void CatapultAttack::applyGs(CGameState * gs) -{ - applyBattle(gs->getBattle(battleID)); -} - -void CatapultAttack::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitCatapultAttack(*this); -} - -void CatapultAttack::applyBattle(IBattleState * battleState) -{ - const auto * town = battleState->getDefendedTown(); - if(!town) - return; - - if(town->fortLevel() == CGTownInstance::NONE) - return; - - for(const auto & part : attackedParts) - { - auto newWallState = SiegeInfo::applyDamage(battleState->getWallState(part.attackedPart), part.damageDealt); - battleState->setWallState(part.attackedPart, newWallState); - } -} - -void BattleSetStackProperty::applyGs(CGameState * gs) const -{ - CStack * stack = gs->getBattle(battleID)->getStack(stackID); - switch(which) - { - case CASTS: - { - if(absolute) - logNetwork->error("Can not change casts in absolute mode"); - else - stack->casts.use(-val); - break; - } - case ENCHANTER_COUNTER: - { - auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter; - if(absolute) - counter = val; - else - counter += val; - vstd::amax(counter, 0); - break; - } - case UNBIND: - { - stack->removeBonusesRecursive(Selector::type()(BonusType::BIND_EFFECT)); - break; - } - case CLONED: - { - stack->cloned = true; - break; - } - case HAS_CLONE: - { - stack->cloneID = val; - break; - } - } -} - -void PlayerCheated::applyGs(CGameState * gs) const -{ - if(!player.isValidPlayer()) - return; - - gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; - gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; - gs->getPlayerState(player)->cheated = true; -} - -void PlayerStartsTurn::applyGs(CGameState * gs) const -{ - assert(gs->actingPlayers.count(player) == 0); - gs->actingPlayers.insert(player); -} - -void PlayerEndsTurn::applyGs(CGameState * gs) const -{ - assert(gs->actingPlayers.count(player) == 1); - gs->actingPlayers.erase(player); -} - -void DaysWithoutTown::applyGs(CGameState * gs) const -{ - auto & playerState = gs->players[player]; - playerState.daysWithoutCastle = daysWithoutCastle; -} - -void TurnTimeUpdate::applyGs(CGameState *gs) const -{ - auto & playerState = gs->players[player]; - playerState.turnTimer = turnTimer; -} - -Component::Component(const CStackBasicDescriptor & stack) - : id(EComponentType::CREATURE) - , subtype(stack.type->getId()) - , val(stack.count) -{ -} - -void EntitiesChanged::applyGs(CGameState * gs) -{ - for(const auto & change : changes) - gs->updateEntity(change.metatype, change.entityIndex, change.data); -} - -const CArtifactInstance * ArtSlotInfo::getArt() const -{ - if(locked) - { - logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); - return nullptr; - } - return artifact; -} - -CArtifactSet * BulkMoveArtifacts::getSrcHolderArtSet() -{ - return std::visit(GetBase(), srcArtHolder); -} - -CArtifactSet * BulkMoveArtifacts::getDstHolderArtSet() -{ - return std::visit(GetBase(), dstArtHolder); -} - -VCMI_LIB_NAMESPACE_END +/* + * NetPacksLib.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 "ArtifactUtils.h" +#include "NetPacks.h" +#include "NetPackVisitor.h" +#include "CGeneralTextHandler.h" +#include "CArtHandler.h" +#include "CHeroHandler.h" +#include "VCMI_Lib.h" +#include "mapping/CMap.h" +#include "spells/CSpellHandler.h" +#include "CCreatureHandler.h" +#include "gameState/CGameState.h" +#include "gameState/TavernHeroesPool.h" +#include "CStack.h" +#include "battle/BattleInfo.h" +#include "CTownHandler.h" +#include "mapping/CMapInfo.h" +#include "StartInfo.h" +#include "CPlayerState.h" +#include "TerrainHandler.h" +#include "mapObjects/CGCreature.h" +#include "mapObjects/CGMarket.h" +#include "mapObjectConstructors/AObjectTypeHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "campaign/CampaignState.h" +#include "GameSettings.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CPack::visit(ICPackVisitor & visitor) +{ + visitBasic(visitor); + + // visitBasic may destroy this and in such cases we do not want to call visitTyped + if(visitor.callTyped()) + { + visitTyped(visitor); + } +} + +void CPack::visitBasic(ICPackVisitor & visitor) +{ +} + +void CPack::visitTyped(ICPackVisitor & visitor) +{ +} + +void CPackForClient::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForClient(*this); +} + +void CPackForServer::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForServer(*this); +} + +void CPackForLobby::visitBasic(ICPackVisitor & visitor) +{ + visitor.visitForLobby(*this); +} + +bool CPackForLobby::isForServer() const +{ + return false; +} + +bool CLobbyPackToServer::isForServer() const +{ + return true; +} + +void PackageApplied::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPackageApplied(*this); +} + +void SystemMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSystemMessage(*this); +} + +void PlayerBlocked::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerBlocked(*this); +} + +void PlayerCheated::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerCheated(*this); +} + +void PlayerStartsTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerStartsTurn(*this); +} + +void DaysWithoutTown::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDaysWithoutTown(*this); +} + +void EntitiesChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEntitiesChanged(*this); +} + +void SetResources::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetResources(*this); +} + +void SetPrimSkill::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetPrimSkill(*this); +} + +void SetSecSkill::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetSecSkill(*this); +} + +void HeroVisitCastle::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroVisitCastle(*this); +} + +void ChangeSpells::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeSpells(*this); +} + +void SetMana::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetMana(*this); +} + +void SetMovePoints::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetMovePoints(*this); +} + +void FoWChange::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitFoWChange(*this); +} + +void SetAvailableHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableHeroes(*this); +} + +void GiveBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGiveBonus(*this); +} + +void ChangeObjPos::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeObjPos(*this); +} + +void PlayerEndsTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerEndsTurn(*this); +} + +void PlayerEndsGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerEndsGame(*this); +} + +void PlayerReinitInterface::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerReinitInterface(*this); +} + +void RemoveBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRemoveBonus(*this); +} + +void SetCommanderProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetCommanderProperty(*this); +} + +void AddQuest::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAddQuest(*this); +} + +void UpdateArtHandlerLists::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitUpdateArtHandlerLists(*this); +} + +void UpdateMapEvents::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitUpdateMapEvents(*this); +} + +void UpdateCastleEvents::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitUpdateCastleEvents(*this); +} + +void ChangeFormation::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeFormation(*this); +} + +void RemoveObject::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRemoveObject(*this); +} + +void TryMoveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTryMoveHero(*this); +} + +void NewStructures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewStructures(*this); +} + +void RazeStructures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRazeStructures(*this); +} + +void SetAvailableCreatures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableCreatures(*this); +} + +void SetHeroesInTown::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetHeroesInTown(*this); +} + +void HeroRecruited::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroRecruited(*this); +} + +void GiveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGiveHero(*this); +} + +void OpenWindow::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitOpenWindow(*this); +} + +void NewObject::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewObject(*this); +} + +void SetAvailableArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetAvailableArtifacts(*this); +} + +void NewArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewArtifact(*this); +} + +void ChangeStackCount::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeStackCount(*this); +} + +void SetStackType::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetStackType(*this); +} + +void EraseStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseStack(*this); +} + +void SwapStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSwapStacks(*this); +} + +void InsertNewStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitInsertNewStack(*this); +} + +void RebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRebalanceStacks(*this); +} + +void BulkRebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkRebalanceStacks(*this); +} + +void BulkSmartRebalanceStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSmartRebalanceStacks(*this); +} + +void PutArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPutArtifact(*this); +} + +void EraseArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseArtifact(*this); +} + +void MoveArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMoveArtifact(*this); +} + +void BulkMoveArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMoveArtifacts(*this); +} + +void AssembledArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAssembledArtifact(*this); +} + +void DisassembledArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDisassembledArtifact(*this); +} + +void HeroVisit::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroVisit(*this); +} + +void NewTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitNewTurn(*this); +} + +void InfoWindow::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitInfoWindow(*this); +} + +void SetObjectProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetObjectProperty(*this); +} + +void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitChangeObjectVisitors(*this); +} + +void PrepareHeroLevelUp::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPrepareHeroLevelUp(*this); +} + +void HeroLevelUp::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHeroLevelUp(*this); +} + +void CommanderLevelUp::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCommanderLevelUp(*this); +} + +void BlockingDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBlockingDialog(*this); +} + +void GarrisonDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGarrisonDialog(*this); +} + +void ExchangeDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitExchangeDialog(*this); +} + +void TeleportDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTeleportDialog(*this); +} + +void MapObjectSelectDialog::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMapObjectSelectDialog(*this); +} + +void BattleStart::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleStart(*this); +} + +void BattleNextRound::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleNextRound(*this); +} + +void BattleSetActiveStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSetActiveStack(*this); +} + +void BattleResult::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleResult(*this); +} + +void BattleLogMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleLogMessage(*this); +} + +void BattleStackMoved::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleStackMoved(*this); +} + +void BattleUnitsChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleUnitsChanged(*this); +} + +void BattleAttack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleAttack(*this); +} + +void StartAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitStartAction(*this); +} + +void EndAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEndAction(*this); +} + +void BattleSpellCast::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSpellCast(*this); +} + +void SetStackEffect::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetStackEffect(*this); +} + +void StacksInjured::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitStacksInjured(*this); +} + +void BattleResultsApplied::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleResultsApplied(*this); +} + +void BattleObstaclesChanged::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleObstaclesChanged(*this); +} + +void BattleSetStackProperty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleSetStackProperty(*this); +} + +void BattleTriggerEffect::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleTriggerEffect(*this); +} + +void BattleUpdateGateState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBattleUpdateGateState(*this); +} + +void AdvmapSpellCast::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAdvmapSpellCast(*this); +} + +void ShowWorldViewEx::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitShowWorldViewEx(*this); +} + +void EndTurn::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEndTurn(*this); +} + +void GamePause::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGamePause(*this); +} + +void DismissHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDismissHero(*this); +} + +void MoveHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMoveHero(*this); +} + +void CastleTeleportHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCastleTeleportHero(*this); +} + +void ArrangeStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitArrangeStacks(*this); +} + +void BulkMoveArmy::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMoveArmy(*this); +} + +void BulkSplitStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSplitStack(*this); +} + +void BulkMergeStacks::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkMergeStacks(*this); +} + +void BulkSmartSplitStack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkSmartSplitStack(*this); +} + +void DisbandCreature::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDisbandCreature(*this); +} + +void BuildStructure::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuildStructure(*this); +} + +void RazeStructure::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRazeStructure(*this); +} + +void RecruitCreatures::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitRecruitCreatures(*this); +} + +void UpgradeCreature::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitUpgradeCreature(*this); +} + +void GarrisonHeroSwap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitGarrisonHeroSwap(*this); +} + +void ExchangeArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitExchangeArtifacts(*this); +} + +void BulkExchangeArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBulkExchangeArtifacts(*this); +} + +void AssembleArtifacts::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitAssembleArtifacts(*this); +} + +void EraseArtifactByClient::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitEraseArtifactByClient(*this); +} + +void BuyArtifact::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuyArtifact(*this); +} + +void TradeOnMarketplace::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitTradeOnMarketplace(*this); +} + +void SetFormation::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSetFormation(*this); +} + +void HireHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitHireHero(*this); +} + +void BuildBoat::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitBuildBoat(*this); +} + +void QueryReply::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitQueryReply(*this); +} + +void MakeAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitMakeAction(*this); +} + +void DigWithHero::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitDigWithHero(*this); +} + +void CastAdvSpell::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCastAdvSpell(*this); +} + +void SaveGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitSaveGame(*this); +} + +void PlayerMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerMessage(*this); +} + +void PlayerMessageClient::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitPlayerMessageClient(*this); +} + +void CenterView::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCenterView(*this); +} + +void LobbyClientConnected::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyClientConnected(*this); +} + +void LobbyClientDisconnected::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyClientDisconnected(*this); +} + +void LobbyChatMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChatMessage(*this); +} + +void LobbyGuiAction::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyGuiAction(*this); +} + +void LobbyLoadProgress::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyLoadProgress(*this); +} + +void LobbyEndGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyEndGame(*this); +} + +void LobbyStartGame::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyStartGame(*this); +} + +void LobbyChangeHost::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChangeHost(*this); +} + +void LobbyUpdateState::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyUpdateState(*this); +} + +void LobbySetMap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetMap(*this); +} + +void LobbySetCampaign::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaign(*this); +} + +void LobbySetCampaignMap::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaignMap(*this); +} + +void LobbySetCampaignBonus::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetCampaignBonus(*this); +} + +void LobbyChangePlayerOption::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyChangePlayerOption(*this); +} + +void LobbySetPlayer::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayer(*this); +} + +void LobbySetPlayerName::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetPlayerName(*this); +} + +void LobbySetSimturns::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetSimturns(*this); +} + +void LobbySetTurnTime::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetTurnTime(*this); +} + +void LobbySetDifficulty::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbySetDifficulty(*this); +} + +void LobbyForceSetPlayer::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyForceSetPlayer(*this); +} + +void LobbyShowMessage::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitLobbyShowMessage(*this); +} + +void SetResources::applyGs(CGameState * gs) const +{ + assert(player.isValidPlayer()); + if(abs) + gs->getPlayerState(player)->resources = res; + else + gs->getPlayerState(player)->resources += res; + + //just ensure that player resources are not negative + //server is responsible to check if player can afford deal + //but events on server side are allowed to take more than player have + gs->getPlayerState(player)->resources.positive(); +} + +void SetPrimSkill::applyGs(CGameState * gs) const +{ + CGHeroInstance * hero = gs->getHero(id); + assert(hero); + hero->setPrimarySkill(which, val, abs); +} + +void SetSecSkill::applyGs(CGameState * gs) const +{ + CGHeroInstance *hero = gs->getHero(id); + hero->setSecSkillLevel(which, val, abs); +} + +void SetCommanderProperty::applyGs(CGameState *gs) +{ + CCommanderInstance * commander = gs->getHero(heroid)->commander; + assert (commander); + + switch (which) + { + case BONUS: + commander->accumulateBonus (std::make_shared(accumulatedBonus)); + break; + case SPECIAL_SKILL: + commander->accumulateBonus (std::make_shared(accumulatedBonus)); + commander->specialSkills.insert (additionalInfo); + break; + case SECONDARY_SKILL: + commander->secondarySkills[additionalInfo] = static_cast(amount); + break; + case ALIVE: + if (amount) + commander->setAlive(true); + else + commander->setAlive(false); + break; + case EXPERIENCE: + commander->giveStackExp(amount); //TODO: allow setting exp for stacks via netpacks + break; + } +} + +void AddQuest::applyGs(CGameState * gs) const +{ + assert (vstd::contains(gs->players, player)); + auto * vec = &gs->players[player].quests; + if (!vstd::contains(*vec, quest)) + vec->push_back (quest); + else + logNetwork->warn("Warning! Attempt to add duplicated quest"); +} + +void UpdateArtHandlerLists::applyGs(CGameState * gs) const +{ + VLC->arth->minors = minors; + VLC->arth->majors = majors; + VLC->arth->treasures = treasures; + VLC->arth->relics = relics; +} + +void UpdateMapEvents::applyGs(CGameState * gs) const +{ + gs->map->events = events; +} + +void UpdateCastleEvents::applyGs(CGameState * gs) const +{ + auto * t = gs->getTown(town); + t->events = events; +} + +void ChangeFormation::applyGs(CGameState * gs) const +{ + gs->getHero(hid)->setFormation(formation); +} + +void HeroVisitCastle::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->getHero(hid); + CGTownInstance *t = gs->getTown(tid); + + assert(h); + assert(t); + + if(start()) + t->setVisitingHero(h); + else + t->setVisitingHero(nullptr); +} + +void ChangeSpells::applyGs(CGameState *gs) +{ + CGHeroInstance *hero = gs->getHero(hid); + + if(learn) + for(const auto & sid : spells) + hero->addSpellToSpellbook(sid); + else + for(const auto & sid : spells) + hero->removeSpellFromSpellbook(sid); +} + +void SetMana::applyGs(CGameState * gs) const +{ + CGHeroInstance * hero = gs->getHero(hid); + + assert(hero); + + if(absolute) + hero->mana = val; + else + hero->mana += val; + + vstd::amax(hero->mana, 0); //not less than 0 +} + +void SetMovePoints::applyGs(CGameState * gs) const +{ + CGHeroInstance *hero = gs->getHero(hid); + + assert(hero); + + if(absolute) + hero->setMovementPoints(val); + else + hero->setMovementPoints(hero->movementPointsRemaining() + val); +} + +void FoWChange::applyGs(CGameState *gs) +{ + TeamState * team = gs->getPlayerTeam(player); + auto fogOfWarMap = team->fogOfWarMap; + for(const int3 & t : tiles) + (*fogOfWarMap)[t.z][t.x][t.y] = mode; + if (mode == 0) //do not hide too much + { + std::unordered_set tilesRevealed; + for (auto & elem : gs->map->objects) + { + const CGObjectInstance *o = elem; + if (o) + { + switch(o->ID) + { + case Obj::HERO: + case Obj::MINE: + case Obj::TOWN: + case Obj::ABANDONED_MINE: + if(vstd::contains(team->players, o->tempOwner)) //check owned observators + gs->getTilesInRange(tilesRevealed, o->getSightCenter(), o->getSightRadius(), o->tempOwner, 1); + break; + } + } + } + for(const int3 & t : tilesRevealed) //probably not the most optimal solution ever + (*fogOfWarMap)[t.z][t.x][t.y] = 1; + } +} + +void SetAvailableHero::applyGs(CGameState *gs) +{ + gs->heroesPool->setHeroForPlayer(player, slotID, hid, army, roleID); +} + +void GiveBonus::applyGs(CGameState *gs) +{ + CBonusSystemNode *cbsn = nullptr; + switch(who) + { + case ETarget::HERO: + cbsn = gs->getHero(ObjectInstanceID(id)); + break; + case ETarget::PLAYER: + cbsn = gs->getPlayerState(PlayerColor(id)); + break; + case ETarget::TOWN: + cbsn = gs->getTown(ObjectInstanceID(id)); + break; + case ETarget::BATTLE: + assert(Bonus::OneBattle(&bonus)); + cbsn = dynamic_cast(gs->getBattle(BattleID(id))); + break; + } + + assert(cbsn); + + if(Bonus::OneWeek(&bonus)) + bonus.turnsRemain = 8 - gs->getDate(Date::DAY_OF_WEEK); // set correct number of days before adding bonus + + auto b = std::make_shared(bonus); + cbsn->addNewBonus(b); + + std::string &descr = b->description; + + if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) + { + if (bonus.source == BonusSource::OBJECT) + { + descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" + } + else if(bonus.source == BonusSource::TOWN_STRUCTURE) + { + descr = bonus.description; + return; + } + else + { + descr = bdescr.toString(); + } + } + else + { + descr = bdescr.toString(); + } + // Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them + boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); + boost::replace_first(descr, "%s", std::to_string(std::abs(bonus.val))); +} + +void ChangeObjPos::applyGs(CGameState *gs) +{ + CGObjectInstance *obj = gs->getObjInstance(objid); + if(!obj) + { + logNetwork->error("Wrong ChangeObjPos: object %d doesn't exist!", objid.getNum()); + return; + } + gs->map->removeBlockVisTiles(obj); + obj->pos = nPos + obj->getVisitableOffset(); + gs->map->addBlockVisTiles(obj); +} + +void ChangeObjectVisitors::applyGs(CGameState * gs) const +{ + switch (mode) { + case VISITOR_ADD: + gs->getHero(hero)->visitedObjects.insert(object); + gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjects.insert(object); + break; + case VISITOR_ADD_TEAM: + { + TeamState *ts = gs->getPlayerTeam(gs->getHero(hero)->tempOwner); + for(const auto & color : ts->players) + { + gs->getPlayerState(color)->visitedObjects.insert(object); + } + } + break; + case VISITOR_CLEAR: + for (CGHeroInstance * hero : gs->map->allHeroes) + { + if (hero) + { + hero->visitedObjects.erase(object); // remove visit info from all heroes, including those that are not present on map + } + } + + for(auto &elem : gs->players) + { + elem.second.visitedObjects.erase(object); + } + + break; + case VISITOR_REMOVE: + gs->getHero(hero)->visitedObjects.erase(object); + break; + } +} + +void PlayerEndsGame::applyGs(CGameState * gs) const +{ + PlayerState *p = gs->getPlayerState(player); + if(victoryLossCheckResult.victory()) + { + p->status = EPlayerStatus::WINNER; + + // TODO: Campaign-specific code might as well go somewhere else + // keep all heroes from the winning player + if(p->human && gs->scenarioOps->campState) + { + std::vector crossoverHeroes; + for (CGHeroInstance * hero : gs->map->heroesOnMap) + if (hero->tempOwner == player) + crossoverHeroes.push_back(hero); + + gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes); + } + } + else + { + p->status = EPlayerStatus::LOSER; + } + + // defeated player may be making turn right now + gs->actingPlayers.erase(player); +} + +void PlayerReinitInterface::applyGs(CGameState *gs) +{ + if(!gs || !gs->scenarioOps) + return; + + //TODO: what does mean if more that one player connected? + if(playerConnectionId == PlayerSettings::PLAYER_AI) + { + for(const auto & player : players) + gs->scenarioOps->getIthPlayersSettings(player).connectedPlayerIDs.clear(); + } +} + +void RemoveBonus::applyGs(CGameState *gs) +{ + CBonusSystemNode * node = nullptr; + if (who == GiveBonus::ETarget::HERO) + node = gs->getHero(ObjectInstanceID(whoID)); + else + node = gs->getPlayerState(PlayerColor(whoID)); + + BonusList &bonuses = node->getExportedBonusList(); + + for(const auto & b : bonuses) + { + if(vstd::to_underlying(b->source) == source && b->sid == id) + { + bonus = *b; //backup bonus (to show to interfaces later) + node->removeBonus(b); + break; + } + } +} + +void RemoveObject::applyGs(CGameState *gs) +{ + + CGObjectInstance *obj = gs->getObjInstance(objectID); + logGlobal->debug("removing object id=%d; address=%x; name=%s", objectID, (intptr_t)obj, obj->getObjectName()); + //unblock tiles + gs->map->removeBlockVisTiles(obj); + + if(obj->ID == Obj::HERO) //remove beaten hero + { + auto * beatenHero = dynamic_cast(obj); + assert(beatenHero); + PlayerState * p = gs->getPlayerState(beatenHero->tempOwner); + gs->map->heroesOnMap -= beatenHero; + p->heroes -= beatenHero; + + + auto * siegeNode = beatenHero->whereShouldBeAttachedOnSiege(gs); + + // FIXME: workaround: + // hero should be attached to siegeNode after battle + // however this code might also be called on dismissing hero while in town + if (siegeNode && vstd::contains(beatenHero->getParentNodes(), siegeNode)) + beatenHero->detachFrom(*siegeNode); + + beatenHero->tempOwner = PlayerColor::NEUTRAL; //no one owns beaten hero + vstd::erase_if(beatenHero->artifactsInBackpack, [](const ArtSlotInfo& asi) + { + return asi.artifact->artType->getId() == ArtifactID::GRAIL; + }); + + if(beatenHero->visitedTown) + { + if(beatenHero->visitedTown->garrisonHero == beatenHero) + beatenHero->visitedTown->garrisonHero = nullptr; + else + beatenHero->visitedTown->visitingHero = nullptr; + + beatenHero->visitedTown = nullptr; + beatenHero->inTownGarrison = false; + } + //return hero to the pool, so he may reappear in tavern + + gs->heroesPool->addHeroToPool(beatenHero); + gs->map->objects[objectID.getNum()] = nullptr; + + //If hero on Boat is removed, the Boat disappears + if(beatenHero->boat) + { + beatenHero->detachFrom(const_cast(*beatenHero->boat)); + gs->map->instanceNames.erase(beatenHero->boat->instanceName); + gs->map->objects[beatenHero->boat->id.getNum()].dellNull(); + beatenHero->boat = nullptr; + } + return; + } + + const auto * quest = dynamic_cast(obj); + if (quest) + { + gs->map->quests[quest->quest->qid] = nullptr; + for (auto &player : gs->players) + { + for (auto &q : player.second.quests) + { + if (q.obj == obj) + { + q.obj = nullptr; + } + } + } + } + + for (TriggeredEvent & event : gs->map->triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + if (cond.object == obj) + { + if (cond.condition == EventCondition::DESTROY || cond.condition == EventCondition::DESTROY_0) + { + cond.condition = EventCondition::CONST_VALUE; + cond.value = 1; // destroyed object, from now on always fulfilled + } + else if (cond.condition == EventCondition::CONTROL || cond.condition == EventCondition::HAVE_0) + { + cond.condition = EventCondition::CONST_VALUE; + cond.value = 0; // destroyed object, from now on can not be fulfilled + } + } + return cond; + }; + event.trigger = event.trigger.morph(patcher); + } + gs->map->instanceNames.erase(obj->instanceName); + gs->map->objects[objectID.getNum()].dellNull(); + gs->map->calculateGuardingGreaturePositions(); +} + +static int getDir(const int3 & src, const int3 & dst) +{ + int ret = -1; + if(dst.x+1 == src.x && dst.y+1 == src.y) //tl + { + ret = 1; + } + else if(dst.x == src.x && dst.y+1 == src.y) //t + { + ret = 2; + } + else if(dst.x-1 == src.x && dst.y+1 == src.y) //tr + { + ret = 3; + } + else if(dst.x-1 == src.x && dst.y == src.y) //r + { + ret = 4; + } + else if(dst.x-1 == src.x && dst.y-1 == src.y) //br + { + ret = 5; + } + else if(dst.x == src.x && dst.y-1 == src.y) //b + { + ret = 6; + } + else if(dst.x+1 == src.x && dst.y-1 == src.y) //bl + { + ret = 7; + } + else if(dst.x+1 == src.x && dst.y == src.y) //l + { + ret = 8; + } + return ret; +} + +void TryMoveHero::applyGs(CGameState *gs) +{ + CGHeroInstance *h = gs->getHero(id); + if (!h) + { + logGlobal->error("Attempt ot move unavailable hero %d", id.getNum()); + return; + } + + h->setMovementPoints(movePoints); + + if((result == SUCCESS || result == BLOCKING_VISIT || result == EMBARK || result == DISEMBARK) && start != end) + { + auto dir = getDir(start,end); + if(dir > 0 && dir <= 8) + h->moveDir = dir; + //else don`t change move direction - hero might have traversed the subterranean gate, direction should be kept + } + + if(result == EMBARK) //hero enters boat at destination tile + { + const TerrainTile &tt = gs->map->getTile(h->convertToVisitablePos(end)); + assert(tt.visitableObjects.size() >= 1 && tt.visitableObjects.back()->ID == Obj::BOAT); //the only visitable object at destination is Boat + auto * boat = dynamic_cast(tt.visitableObjects.back()); + assert(boat); + + gs->map->removeBlockVisTiles(boat); //hero blockvis mask will be used, we don't need to duplicate it with boat + h->boat = boat; + h->attachTo(*boat); + boat->hero = h; + } + else if(result == DISEMBARK) //hero leaves boat to destination tile + { + auto * b = const_cast(h->boat); + b->direction = h->moveDir; + b->pos = start; + b->hero = nullptr; + gs->map->addBlockVisTiles(b); + h->detachFrom(*b); + h->boat = nullptr; + } + + if(start!=end && (result == SUCCESS || result == TELEPORTATION || result == EMBARK || result == DISEMBARK)) + { + gs->map->removeBlockVisTiles(h); + h->pos = end; + if(auto * b = const_cast(h->boat)) + b->pos = end; + gs->map->addBlockVisTiles(h); + } + + auto fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; + for(const int3 & t : fowRevealed) + (*fogOfWarMap)[t.z][t.x][t.y] = 1; +} + +void NewStructures::applyGs(CGameState *gs) +{ + CGTownInstance *t = gs->getTown(tid); + + for(const auto & id : bid) + { + assert(t->town->buildings.at(id) != nullptr); + t->builtBuildings.insert(id); + t->updateAppearance(); + auto currentBuilding = t->town->buildings.at(id); + + if(currentBuilding->overrideBids.empty()) + continue; + + for(const auto & overrideBid : currentBuilding->overrideBids) + { + t->overriddenBuildings.insert(overrideBid); + t->deleteTownBonus(overrideBid); + } + } + t->builded = builded; + t->recreateBuildingsBonuses(); +} + +void RazeStructures::applyGs(CGameState *gs) +{ + CGTownInstance *t = gs->getTown(tid); + for(const auto & id : bid) + { + t->builtBuildings.erase(id); + + t->updateAppearance(); + } + t->destroyed = destroyed; //yeaha + t->recreateBuildingsBonuses(); +} + +void SetAvailableCreatures::applyGs(CGameState * gs) const +{ + auto * dw = dynamic_cast(gs->getObjInstance(tid)); + assert(dw); + dw->creatures = creatures; +} + +void SetHeroesInTown::applyGs(CGameState * gs) const +{ + CGTownInstance *t = gs->getTown(tid); + + CGHeroInstance * v = gs->getHero(visiting); + CGHeroInstance * g = gs->getHero(garrison); + + bool newVisitorComesFromGarrison = v && v == t->garrisonHero; + bool newGarrisonComesFromVisiting = g && g == t->visitingHero; + + if(newVisitorComesFromGarrison) + t->setGarrisonedHero(nullptr); + if(newGarrisonComesFromVisiting) + t->setVisitingHero(nullptr); + if(!newGarrisonComesFromVisiting || v) + t->setVisitingHero(v); + if(!newVisitorComesFromGarrison || g) + t->setGarrisonedHero(g); + + if(v) + { + gs->map->addBlockVisTiles(v); + } + if(g) + { + gs->map->removeBlockVisTiles(g); + } +} + +void HeroRecruited::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->heroesPool->takeHeroFromPool(hid); + CGTownInstance *t = gs->getTown(tid); + PlayerState *p = gs->getPlayerState(player); + + if (boatId != ObjectInstanceID::NONE) + { + CGObjectInstance *obj = gs->getObjInstance(boatId); + auto * boat = dynamic_cast(obj); + if (boat) + { + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); + } + } + + h->setOwner(player); + h->pos = tile; + h->initObj(gs->getRandomGenerator()); + + if(h->id == ObjectInstanceID()) + { + h->id = ObjectInstanceID(static_cast(gs->map->objects.size())); + gs->map->objects.emplace_back(h); + } + else + gs->map->objects[h->id.getNum()] = h; + + gs->map->heroesOnMap.emplace_back(h); + p->heroes.emplace_back(h); + h->attachTo(*p); + gs->map->addBlockVisTiles(h); + + if(t) + t->setVisitingHero(h); +} + +void GiveHero::applyGs(CGameState * gs) const +{ + CGHeroInstance *h = gs->getHero(id); + + if (boatId != ObjectInstanceID::NONE) + { + CGObjectInstance *obj = gs->getObjInstance(boatId); + auto * boat = dynamic_cast(obj); + if (boat) + { + gs->map->removeBlockVisTiles(boat); + h->attachToBoat(boat); + } + } + + //bonus system + h->detachFrom(gs->globalEffects); + h->attachTo(*gs->getPlayerState(player)); + + auto oldVisitablePos = h->visitablePos(); + gs->map->removeBlockVisTiles(h,true); + h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front(); + + h->setOwner(player); + h->setMovementPoints(h->movementPointsLimit(true)); + h->pos = h->convertFromVisitablePos(oldVisitablePos); + gs->map->heroesOnMap.emplace_back(h); + gs->getPlayerState(h->getOwner())->heroes.emplace_back(h); + + gs->map->addBlockVisTiles(h); + h->inTownGarrison = false; +} + +void NewObject::applyGs(CGameState *gs) +{ + TerrainId terrainType = ETerrainId::NONE; + + if (!gs->isInTheMap(targetPos)) + { + logGlobal->error("Attempt to create object outside map at %s!", targetPos.toString()); + return; + } + + const TerrainTile & t = gs->map->getTile(targetPos); + terrainType = t.terType->getId(); + + auto handler = VLC->objtypeh->getHandlerFor(ID, subID); + + CGObjectInstance * o = handler->create(); + handler->configureObject(o, gs->getRandomGenerator()); + + if (ID == Obj::MONSTER) //probably more options will be needed + { + //CStackInstance hlp; + auto * cre = dynamic_cast(o); + //cre->slots[0] = hlp; + assert(cre); + cre->notGrowingTeam = cre->neverFlees = false; + cre->character = 2; + cre->gainedArtifact = ArtifactID::NONE; + cre->identifier = -1; + cre->addToSlot(SlotID(0), new CStackInstance(CreatureID(subID), -1)); //add placeholder stack + } + + assert(!handler->getTemplates(terrainType).empty()); + if (handler->getTemplates().empty()) + { + logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID); + return; + } + + if (!handler->getTemplates(terrainType).empty()) + o->appearance = handler->getTemplates(terrainType).front(); + else + o->appearance = handler->getTemplates().front(); + + o->id = ObjectInstanceID(static_cast(gs->map->objects.size())); + o->ID = ID; + o->subID = subID; + o->pos = targetPos + o->getVisitableOffset(); + + gs->map->objects.emplace_back(o); + gs->map->addBlockVisTiles(o); + o->initObj(gs->getRandomGenerator()); + gs->map->calculateGuardingGreaturePositions(); + + createdObjectID = o->id; + + logGlobal->debug("Added object id=%d; address=%x; name=%s", o->id, (intptr_t)o, o->getObjectName()); +} + +void NewArtifact::applyGs(CGameState *gs) +{ + assert(!vstd::contains(gs->map->artInstances, art)); + assert(!art->getParentNodes().size()); + assert(art->artType); + + art->setType(art->artType); + if(art->isCombined()) + { + for(const auto & part : art->artType->getConstituents()) + art->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST); + } + gs->map->addNewArtifactInstance(art); +} + +const CStackInstance * StackLocation::getStack() +{ + if(!army->hasStackAtSlot(slot)) + { + logNetwork->warn("%s don't have a stack at slot %d", army->nodeName(), slot.getNum()); + return nullptr; + } + return &army->getStack(slot); +} + +struct ObjectRetriever +{ + const CArmedInstance * operator()(const ConstTransitivePtr &h) const + { + return h; + } + const CArmedInstance * operator()(const ConstTransitivePtr &s) const + { + return s->armyObj; + } +}; +template +struct GetBase +{ + template + T * operator()(TArg &arg) const + { + return arg; + } +}; + + +void ArtifactLocation::removeArtifact() +{ + CArtifactInstance *a = getArt(); + assert(a); + a->removeFrom(*this); +} + +const CArmedInstance * ArtifactLocation::relatedObj() const +{ + return std::visit(ObjectRetriever(), artHolder); +} + +PlayerColor ArtifactLocation::owningPlayer() const +{ + const auto * obj = relatedObj(); + return obj ? obj->tempOwner : PlayerColor::NEUTRAL; +} + +CArtifactSet *ArtifactLocation::getHolderArtSet() +{ + return std::visit(GetBase(), artHolder); +} + +CBonusSystemNode *ArtifactLocation::getHolderNode() +{ + return std::visit(GetBase(), artHolder); +} + +const CArtifactInstance *ArtifactLocation::getArt() const +{ + const auto * s = getSlot(); + if(s) + return s->getArt(); + else + return nullptr; +} + +CArtifactSet * ArtifactLocation::getHolderArtSet() const +{ + auto * t = const_cast(this); + return t->getHolderArtSet(); +} + +const CBonusSystemNode * ArtifactLocation::getHolderNode() const +{ + auto * t = const_cast(this); + return t->getHolderNode(); +} + +CArtifactInstance *ArtifactLocation::getArt() +{ + const ArtifactLocation *t = this; + return const_cast(t->getArt()); +} + +const ArtSlotInfo *ArtifactLocation::getSlot() const +{ + return getHolderArtSet()->getSlot(slot); +} + +void ChangeStackCount::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] ChangeStackCount: invalid army object %d, possible game state corruption.", army.getNum()); + + if(absoluteValue) + srcObj->setStackCount(slot, count); + else + srcObj->changeStackCount(slot, count); +} + +void SetStackType::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] SetStackType: invalid army object %d, possible game state corruption.", army.getNum()); + + srcObj->setStackType(slot, type); +} + +void EraseStack::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(army); + if(!srcObj) + logNetwork->error("[CRITICAL] EraseStack: invalid army object %d, possible game state corruption.", army.getNum()); + + srcObj->eraseStack(slot); +} + +void SwapStacks::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(srcArmy); + if(!srcObj) + logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); + + auto * dstObj = gs->getArmyInstance(dstArmy); + if(!dstObj) + logNetwork->error("[CRITICAL] SwapStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); + + CStackInstance * s1 = srcObj->detachStack(srcSlot); + CStackInstance * s2 = dstObj->detachStack(dstSlot); + + srcObj->putStack(srcSlot, s2); + dstObj->putStack(dstSlot, s1); +} + +void InsertNewStack::applyGs(CGameState *gs) +{ + if(auto * obj = gs->getArmyInstance(army)) + obj->putStack(slot, new CStackInstance(type, count)); + else + logNetwork->error("[CRITICAL] InsertNewStack: invalid army object %d, possible game state corruption.", army.getNum()); +} + +void RebalanceStacks::applyGs(CGameState * gs) +{ + auto * srcObj = gs->getArmyInstance(srcArmy); + if(!srcObj) + logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", srcArmy.getNum()); + + auto * dstObj = gs->getArmyInstance(dstArmy); + if(!dstObj) + logNetwork->error("[CRITICAL] RebalanceStacks: invalid army object %d, possible game state corruption.", dstArmy.getNum()); + + StackLocation src(srcObj, srcSlot); + StackLocation dst(dstObj, dstSlot); + + const CCreature * srcType = src.army->getCreature(src.slot); + TQuantity srcCount = src.army->getStackCount(src.slot); + bool stackExp = VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE); + + if(srcCount == count) //moving whole stack + { + [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); + + if(c) //stack at dest -> merge + { + assert(c == srcType); + auto alHere = ArtifactLocation (src.getStack(), ArtifactPosition::CREATURE_SLOT); + auto alDest = ArtifactLocation (dst.getStack(), ArtifactPosition::CREATURE_SLOT); + auto * artHere = alHere.getArt(); + auto * artDest = alDest.getArt(); + if (artHere) + { + if (alDest.getArt()) + { + auto * hero = dynamic_cast(src.army.get()); + auto dstSlot = ArtifactUtils::getArtBackpackPosition(hero, alDest.getArt()->getTypeId()); + if(hero && dstSlot != ArtifactPosition::PRE_FIRST) + { + artDest->move (alDest, ArtifactLocation (hero, dstSlot)); + } + //else - artifact cna be lost :/ + else + { + EraseArtifact ea; + ea.al = alDest; + ea.applyGs(gs); + logNetwork->warn("Cannot move artifact! No free slots"); + } + artHere->move (alHere, alDest); + //TODO: choose from dialog + } + else //just move to the other slot before stack gets erased + { + artHere->move (alHere, alDest); + } + } + if (stackExp) + { + ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); + src.army->eraseStack(src.slot); + dst.army->changeStackCount(dst.slot, count); + dst.army->setStackExp(dst.slot, totalExp /(dst.army->getStackCount(dst.slot))); //mean + } + else + { + src.army->eraseStack(src.slot); + dst.army->changeStackCount(dst.slot, count); + } + } + else //move stack to an empty slot, no exp change needed + { + CStackInstance *stackDetached = src.army->detachStack(src.slot); + dst.army->putStack(dst.slot, stackDetached); + } + } + else + { + [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); + if(c) //stack at dest -> rebalance + { + assert(c == srcType); + if (stackExp) + { + ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); + src.army->changeStackCount(src.slot, -count); + dst.army->changeStackCount(dst.slot, count); + dst.army->setStackExp(dst.slot, totalExp /(src.army->getStackCount(src.slot) + dst.army->getStackCount(dst.slot))); //mean + } + else + { + src.army->changeStackCount(src.slot, -count); + dst.army->changeStackCount(dst.slot, count); + } + } + else //split stack to an empty slot + { + src.army->changeStackCount(src.slot, -count); + dst.army->addToSlot(dst.slot, srcType->getId(), count, false); + if (stackExp) + dst.army->setStackExp(dst.slot, src.army->getStackExperience(src.slot)); + } + } + + CBonusSystemNode::treeHasChanged(); +} + +void BulkRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); +} + +void BulkSmartRebalanceStacks::applyGs(CGameState * gs) +{ + for(auto & move : moves) + move.applyGs(gs); + + for(auto & change : changes) + change.applyGs(gs); +} + +void PutArtifact::applyGs(CGameState *gs) +{ + assert(art->canBePutAt(al)); + // Ensure that artifact has been correctly added via NewArtifact pack + assert(vstd::contains(gs->map->artInstances, art)); + assert(!art->getParentNodes().empty()); + art->putAt(al); +} + +void EraseArtifact::applyGs(CGameState *gs) +{ + const auto * slot = al.getSlot(); + if(slot->locked) + { + logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); + DisassembledArtifact dis; + dis.al.artHolder = al.artHolder; + auto * aset = al.getHolderArtSet(); + #ifndef NDEBUG + bool found = false; + #endif + for(auto& p : aset->artifactsWorn) + { + auto art = p.second.artifact; + if(art->isCombined() && art->isPart(slot->artifact)) + { + dis.al.slot = aset->getArtPos(art); + #ifndef NDEBUG + found = true; + #endif + break; + } + } + assert(found && "Failed to determine the assembly this locked artifact belongs to"); + logGlobal->debug("Found the corresponding assembly: %s", dis.al.getSlot()->artifact->artType->getNameTranslated()); + dis.applyGs(gs); + } + else + { + logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); + } + al.removeArtifact(); +} + +void MoveArtifact::applyGs(CGameState * gs) +{ + CArtifactInstance * art = src.getArt(); + assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); + art->move(src, dst); +} + +void BulkMoveArtifacts::applyGs(CGameState * gs) +{ + enum class EBulkArtsOp + { + BULK_MOVE, + BULK_REMOVE, + BULK_PUT + }; + + auto bulkArtsOperation = [this](std::vector & artsPack, + CArtifactSet * artSet, EBulkArtsOp operation) -> void + { + int numBackpackArtifactsMoved = 0; + for(auto & slot : artsPack) + { + // When an object gets removed from the backpack, the backpack shrinks + // so all the following indices will be affected. Thus, we need to update + // the subsequent artifact slots to account for that + auto srcPos = slot.srcPos; + if(ArtifactUtils::isSlotBackpack(srcPos) && (operation != EBulkArtsOp::BULK_PUT)) + { + srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); + } + const auto * slotInfo = artSet->getSlot(srcPos); + assert(slotInfo); + auto * art = const_cast(slotInfo->getArt()); + assert(art); + switch(operation) + { + case EBulkArtsOp::BULK_MOVE: + const_cast(art)->move( + ArtifactLocation(srcArtHolder, srcPos), ArtifactLocation(dstArtHolder, slot.dstPos)); + break; + case EBulkArtsOp::BULK_REMOVE: + art->removeFrom(ArtifactLocation(dstArtHolder, srcPos)); + break; + case EBulkArtsOp::BULK_PUT: + art->putAt(ArtifactLocation(srcArtHolder, slot.dstPos)); + break; + default: + break; + } + + if(srcPos >= ArtifactPosition::BACKPACK_START) + { + numBackpackArtifactsMoved++; + } + } + }; + + if(swap) + { + // Swap + auto * leftSet = getSrcHolderArtSet(); + auto * rightSet = getDstHolderArtSet(); + CArtifactFittingSet artFittingSet(leftSet->bearerType()); + + artFittingSet.artifactsWorn = rightSet->artifactsWorn; + artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; + + bulkArtsOperation(artsPack1, rightSet, EBulkArtsOp::BULK_REMOVE); + bulkArtsOperation(artsPack0, leftSet, EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack1, &artFittingSet, EBulkArtsOp::BULK_PUT); + } + else + { + bulkArtsOperation(artsPack0, getSrcHolderArtSet(), EBulkArtsOp::BULK_MOVE); + } +} + +void AssembledArtifact::applyGs(CGameState *gs) +{ + CArtifactSet * artSet = al.getHolderArtSet(); + const CArtifactInstance * transformedArt = al.getArt(); + assert(transformedArt); + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool + { + return art->getId() == builtArt->getId(); + })); + + const auto transformedArtSlot = artSet->getSlotByInstance(transformedArt); + auto * combinedArt = new CArtifactInstance(builtArt); + gs->map->addNewArtifactInstance(combinedArt); + + // Find slots for all involved artifacts + std::vector slotsInvolved; + for(const auto constituent : builtArt->getConstituents()) + { + ArtifactPosition slot; + if(transformedArt->getTypeId() == constituent->getId()) + slot = transformedArtSlot; + else + slot = artSet->getArtPos(constituent->getId(), false, false); + + assert(slot != ArtifactPosition::PRE_FIRST); + slotsInvolved.emplace_back(slot); + } + std::sort(slotsInvolved.begin(), slotsInvolved.end(), std::greater<>()); + + // Find a slot for combined artifact + al.slot = transformedArtSlot; + for(const auto slot : slotsInvolved) + { + if(ArtifactUtils::isSlotEquipment(transformedArtSlot)) + { + + if(ArtifactUtils::isSlotBackpack(slot)) + { + al.slot = ArtifactPosition::BACKPACK_START; + break; + } + + if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) + && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), slot)) + al.slot = slot; + } + else + { + if(ArtifactUtils::isSlotBackpack(slot)) + al.slot = std::min(al.slot, slot); + } + } + + // Delete parts from hero + for(const auto slot : slotsInvolved) + { + const auto constituentInstance = artSet->getArt(slot); + constituentInstance->removeFrom(ArtifactLocation(al.artHolder, slot)); + + if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) + combinedArt->addPart(constituentInstance, slot); + else + combinedArt->addPart(constituentInstance, ArtifactPosition::PRE_FIRST); + } + + // Put new combined artifacts + combinedArt->putAt(al); +} + +void DisassembledArtifact::applyGs(CGameState *gs) +{ + auto * disassembled = al.getArt(); + assert(disassembled); + + auto parts = disassembled->getPartsInfo(); + disassembled->removeFrom(al); + for(auto & part : parts) + { + ArtifactLocation partLoc = al; + // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos + partLoc.slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); + disassembled->detachFrom(*part.art); + part.art->putAt(partLoc); + } + gs->map->eraseArtifactInstance(disassembled); +} + +void HeroVisit::applyGs(CGameState *gs) +{ +} + +void SetAvailableArtifacts::applyGs(CGameState * gs) const +{ + if(id >= 0) + { + if(auto * bm = dynamic_cast(gs->map->objects[id].get())) + { + bm->artifacts = arts; + } + else + { + logNetwork->error("Wrong black market id!"); + } + } + else + { + CGTownInstance::merchantArtifacts = arts; + } +} + +void NewTurn::applyGs(CGameState *gs) +{ + gs->day = day; + + // Update bonuses before doing anything else so hero don't get more MP than needed + gs->globalEffects.removeBonusesRecursive(Bonus::OneDay); //works for children -> all game objs + gs->globalEffects.reduceBonusDurations(Bonus::NDays); + gs->globalEffects.reduceBonusDurations(Bonus::OneWeek); + //TODO not really a single root hierarchy, what about bonuses placed elsewhere? [not an issue with H3 mechanics but in the future...] + + for(const NewTurn::Hero & h : heroes) //give mana/movement point + { + CGHeroInstance *hero = gs->getHero(h.id); + if(!hero) + { + logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum()); + continue; + } + + hero->setMovementPoints(h.move); + hero->mana = h.mana; + } + + gs->heroesPool->onNewDay(); + + for(const auto & re : res) + { + assert(re.first.isValidPlayer()); + gs->getPlayerState(re.first)->resources = re.second; + } + + for(const auto & creatureSet : cres) //set available creatures in towns + creatureSet.second.applyGs(gs); + + for(CGTownInstance* t : gs->map->towns) + t->builded = 0; + + if(gs->getDate(Date::DAY_OF_WEEK) == 1) + gs->updateRumor(); +} + +void SetObjectProperty::applyGs(CGameState * gs) const +{ + CGObjectInstance *obj = gs->getObjInstance(id); + if(!obj) + { + logNetwork->error("Wrong object ID - property cannot be set!"); + return; + } + + auto * cai = dynamic_cast(obj); + if(what == ObjProperty::OWNER && cai) + { + if(obj->ID == Obj::TOWN) + { + auto * t = dynamic_cast(obj); + assert(t); + + PlayerColor oldOwner = t->tempOwner; + if(oldOwner.isValidPlayer()) + { + auto * state = gs->getPlayerState(oldOwner); + state->towns -= t; + + if(state->towns.empty()) + *state->daysWithoutCastle = 0; + } + if(PlayerColor(val).isValidPlayer()) + { + PlayerState * p = gs->getPlayerState(PlayerColor(val)); + p->towns.emplace_back(t); + + //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured + if(p->daysWithoutCastle) + p->daysWithoutCastle = std::nullopt; + } + } + + CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); + nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); + obj->setProperty(what,val); + nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); + } + else //not an armed instance + { + obj->setProperty(what,val); + } +} + +void PrepareHeroLevelUp::applyGs(CGameState * gs) +{ + auto * hero = gs->getHero(heroId); + assert(hero); + + auto proposedSkills = hero->getLevelUpProposedSecondarySkills(); + + if(skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically + { + skills.push_back(*RandomGeneratorUtil::nextItem(proposedSkills, hero->skillsInfo.rand)); + } + else + { + skills = proposedSkills; + } +} + +void HeroLevelUp::applyGs(CGameState * gs) const +{ + auto * hero = gs->getHero(heroId); + assert(hero); + hero->levelUp(skills); +} + +void CommanderLevelUp::applyGs(CGameState * gs) const +{ + auto * hero = gs->getHero(heroId); + assert(hero); + auto commander = hero->commander; + assert(commander); + commander->levelUp(); +} + +void BattleStart::applyGs(CGameState * gs) const +{ + assert(battleID == gs->nextBattleID); + + gs->currentBattles.emplace_back(info); + + info->battleID = gs->nextBattleID; + info->localInit(); + + gs->nextBattleID = vstd::next(gs->nextBattleID, 1); +} + +void BattleNextRound::applyGs(CGameState * gs) const +{ + gs->getBattle(battleID)->nextRound(); +} + +void BattleSetActiveStack::applyGs(CGameState * gs) const +{ + gs->getBattle(battleID)->nextTurn(stack); +} + +void BattleTriggerEffect::applyGs(CGameState * gs) const +{ + CStack * st = gs->getBattle(battleID)->getStack(stackID); + assert(st); + switch(static_cast(effect)) + { + case BonusType::HP_REGENERATION: + { + int64_t toHeal = val; + st->heal(toHeal, EHealLevel::HEAL, EHealPower::PERMANENT); + break; + } + case BonusType::MANA_DRAIN: + { + CGHeroInstance * h = gs->getHero(ObjectInstanceID(additionalInfo)); + st->drainedMana = true; + h->mana -= val; + vstd::amax(h->mana, 0); + break; + } + case BonusType::POISON: + { + auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON) + .And(Selector::type()(BonusType::STACK_HEALTH))); + if (b) + b->val = val; + break; + } + case BonusType::ENCHANTER: + case BonusType::MORALE: + break; + case BonusType::FEAR: + st->fear = true; + break; + default: + logNetwork->error("Unrecognized trigger effect type %d", effect); + } +} + +void BattleUpdateGateState::applyGs(CGameState * gs) const +{ + if(gs->getBattle(battleID)) + gs->getBattle(battleID)->si.gateState = state; +} + +void BattleCancelled::applyGs(CGameState * gs) const +{ + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); +} + +void BattleResultAccepted::applyGs(CGameState * gs) const +{ + // Remove any "until next battle" bonuses + for(auto & res : heroResult) + { + if(res.hero) + res.hero->removeBonusesRecursive(Bonus::OneBattle); + } + + if(winnerSide != 2) + { + // Grow up growing artifacts + const auto hero = heroResult[winnerSide].hero; + + if (hero) + { + if(hero->commander && hero->commander->alive) + { + for(auto & art : hero->commander->artifactsWorn) + art.second.artifact->growingUp(); + } + for(auto & art : hero->artifactsWorn) + { + art.second.artifact->growingUp(); + } + } + } + + if(VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) + { + if(heroResult[0].army) + heroResult[0].army->giveStackExp(heroResult[0].exp); + if(heroResult[1].army) + heroResult[1].army->giveStackExp(heroResult[1].exp); + CBonusSystemNode::treeHasChanged(); + } + + auto currentBattle = boost::range::find_if(gs->currentBattles, [&](const auto & battle) + { + return battle->battleID == battleID; + }); + + assert(currentBattle != gs->currentBattles.end()); + gs->currentBattles.erase(currentBattle); +} + +void BattleLogMessage::applyGs(CGameState *gs) +{ + //nothing +} + +void BattleLogMessage::applyBattle(IBattleState * battleState) +{ + //nothing +} + +void BattleStackMoved::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleStackMoved::applyBattle(IBattleState * battleState) +{ + battleState->moveUnit(stack, tilesToMove.back()); +} + +void BattleStackAttacked::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleStackAttacked::applyBattle(IBattleState * battleState) +{ + battleState->setUnitState(newState.id, newState.data, newState.healthDelta); +} + +void BattleAttack::applyGs(CGameState * gs) +{ + CStack * attacker = gs->getBattle(battleID)->getStack(stackAttacking); + assert(attacker); + + attackerChanges.applyGs(gs); + + for(BattleStackAttacked & stackAttacked : bsa) + stackAttacked.applyGs(gs); + + attacker->removeBonusesRecursive(Bonus::UntilAttack); +} + +void StartAction::applyGs(CGameState *gs) +{ + CStack *st = gs->getBattle(battleID)->getStack(ba.stackNumber); + + if(ba.actionType == EActionType::END_TACTIC_PHASE) + { + gs->getBattle(battleID)->tacticDistance = 0; + return; + } + + if(gs->getBattle(battleID)->tacticDistance) + { + // moves in tactics phase do not affect creature status + // (tactics stack queue is managed by client) + return; + } + + if (ba.isUnitAction()) + { + assert(st); // stack must exists for all non-hero actions + + switch(ba.actionType) + { + case EActionType::DEFEND: + st->waiting = false; + st->defending = true; + st->defendingAnim = true; + break; + case EActionType::WAIT: + st->defendingAnim = false; + st->waiting = true; + st->waitedThisTurn = true; + break; + case EActionType::HERO_SPELL: //no change in current stack state + break; + default: //any active stack action - attack, catapult, heal, spell... + st->waiting = false; + st->defendingAnim = false; + st->movedThisRound = true; + break; + } + } + else + { + if(ba.actionType == EActionType::HERO_SPELL) + gs->getBattle(battleID)->sides[ba.side].usedSpellsHistory.push_back(ba.spell); + } +} + +void BattleSpellCast::applyGs(CGameState * gs) const +{ + if(castByHero) + { + if(side < 2) + { + gs->getBattle(battleID)->sides[side].castSpellsCount++; + } + } +} + +void SetStackEffect::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void SetStackEffect::applyBattle(IBattleState * battleState) +{ + for(const auto & stackData : toRemove) + battleState->removeUnitBonus(stackData.first, stackData.second); + + for(const auto & stackData : toUpdate) + battleState->updateUnitBonus(stackData.first, stackData.second); + + for(const auto & stackData : toAdd) + battleState->addUnitBonus(stackData.first, stackData.second); +} + + +void StacksInjured::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void StacksInjured::applyBattle(IBattleState * battleState) +{ + for(BattleStackAttacked stackAttacked : stacks) + stackAttacked.applyBattle(battleState); +} + +void BattleUnitsChanged::applyGs(CGameState *gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleUnitsChanged::applyBattle(IBattleState * battleState) +{ + for(auto & elem : changedStacks) + { + switch(elem.operation) + { + case BattleChanges::EOperation::RESET_STATE: + battleState->setUnitState(elem.id, elem.data, elem.healthDelta); + break; + case BattleChanges::EOperation::REMOVE: + battleState->removeUnit(elem.id); + break; + case BattleChanges::EOperation::ADD: + battleState->addUnit(elem.id, elem.data); + break; + case BattleChanges::EOperation::UPDATE: + battleState->updateUnit(elem.id, elem.data); + break; + default: + logNetwork->error("Unknown unit operation %d", static_cast(elem.operation)); + break; + } + } +} + +void BattleObstaclesChanged::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void BattleObstaclesChanged::applyBattle(IBattleState * battleState) +{ + for(const auto & change : changes) + { + switch(change.operation) + { + case BattleChanges::EOperation::REMOVE: + battleState->removeObstacle(change.id); + break; + case BattleChanges::EOperation::ADD: + battleState->addObstacle(change); + break; + case BattleChanges::EOperation::UPDATE: + battleState->updateObstacle(change); + break; + default: + logNetwork->error("Unknown obstacle operation %d", static_cast(change.operation)); + break; + } + } +} + +CatapultAttack::CatapultAttack() = default; + +CatapultAttack::~CatapultAttack() = default; + +void CatapultAttack::applyGs(CGameState * gs) +{ + applyBattle(gs->getBattle(battleID)); +} + +void CatapultAttack::visitTyped(ICPackVisitor & visitor) +{ + visitor.visitCatapultAttack(*this); +} + +void CatapultAttack::applyBattle(IBattleState * battleState) +{ + const auto * town = battleState->getDefendedTown(); + if(!town) + return; + + if(town->fortLevel() == CGTownInstance::NONE) + return; + + for(const auto & part : attackedParts) + { + auto newWallState = SiegeInfo::applyDamage(battleState->getWallState(part.attackedPart), part.damageDealt); + battleState->setWallState(part.attackedPart, newWallState); + } +} + +void BattleSetStackProperty::applyGs(CGameState * gs) const +{ + CStack * stack = gs->getBattle(battleID)->getStack(stackID); + switch(which) + { + case CASTS: + { + if(absolute) + logNetwork->error("Can not change casts in absolute mode"); + else + stack->casts.use(-val); + break; + } + case ENCHANTER_COUNTER: + { + auto & counter = gs->getBattle(battleID)->sides[gs->getBattle(battleID)->whatSide(stack->unitOwner())].enchanterCounter; + if(absolute) + counter = val; + else + counter += val; + vstd::amax(counter, 0); + break; + } + case UNBIND: + { + stack->removeBonusesRecursive(Selector::type()(BonusType::BIND_EFFECT)); + break; + } + case CLONED: + { + stack->cloned = true; + break; + } + case HAS_CLONE: + { + stack->cloneID = val; + break; + } + } +} + +void PlayerCheated::applyGs(CGameState * gs) const +{ + if(!player.isValidPlayer()) + return; + + gs->getPlayerState(player)->enteredLosingCheatCode = losingCheatCode; + gs->getPlayerState(player)->enteredWinningCheatCode = winningCheatCode; + gs->getPlayerState(player)->cheated = true; +} + +void PlayerStartsTurn::applyGs(CGameState * gs) const +{ + assert(gs->actingPlayers.count(player) == 0); + gs->actingPlayers.insert(player); +} + +void PlayerEndsTurn::applyGs(CGameState * gs) const +{ + assert(gs->actingPlayers.count(player) == 1); + gs->actingPlayers.erase(player); +} + +void DaysWithoutTown::applyGs(CGameState * gs) const +{ + auto & playerState = gs->players[player]; + playerState.daysWithoutCastle = daysWithoutCastle; +} + +void TurnTimeUpdate::applyGs(CGameState *gs) const +{ + auto & playerState = gs->players[player]; + playerState.turnTimer = turnTimer; +} + +Component::Component(const CStackBasicDescriptor & stack) + : id(EComponentType::CREATURE) + , subtype(stack.type->getId()) + , val(stack.count) +{ +} + +void EntitiesChanged::applyGs(CGameState * gs) +{ + for(const auto & change : changes) + gs->updateEntity(change.metatype, change.entityIndex, change.data); +} + +const CArtifactInstance * ArtSlotInfo::getArt() const +{ + if(locked) + { + logNetwork->warn("ArtifactLocation::getArt: This location is locked!"); + return nullptr; + } + return artifact; +} + +CArtifactSet * BulkMoveArtifacts::getSrcHolderArtSet() +{ + return std::visit(GetBase(), srcArtHolder); +} + +CArtifactSet * BulkMoveArtifacts::getDstHolderArtSet() +{ + return std::visit(GetBase(), dstArtHolder); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Point.h b/lib/Point.h index 5289b8c58..36f7a330c 100644 --- a/lib/Point.h +++ b/lib/Point.h @@ -1,132 +1,132 @@ -/* - * Point.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 - -VCMI_LIB_NAMESPACE_BEGIN -class int3; - -// A point with x/y coordinate, used mostly for graphic rendering -class Point -{ -public: - int x, y; - - //constructors - constexpr Point() : x(0), y(0) - { - } - - constexpr Point(int X, int Y) - : x(X) - , y(Y) - { - } - - constexpr static Point makeInvalid() - { - return Point(std::numeric_limits::min(), std::numeric_limits::min()); - } - - explicit DLL_LINKAGE Point(const int3 &a); - - template - constexpr Point operator+(const T &b) const - { - return Point(x+b.x,y+b.y); - } - - template - constexpr Point operator/(const T &div) const - { - return Point(x/div, y/div); - } - - template - constexpr Point operator*(const T &mul) const - { - return Point(x*mul, y*mul); - } - - constexpr Point operator*(const Point &b) const - { - return Point(x*b.x,y*b.y); - } - - template - constexpr Point& operator+=(const T &b) - { - x += b.x; - y += b.y; - return *this; - } - - constexpr Point operator-() const - { - return Point(-x, -y); - } - - template - constexpr Point operator-(const T &b) const - { - return Point(x - b.x, y - b.y); - } - - template - constexpr Point& operator-=(const T &b) - { - x -= b.x; - y -= b.y; - return *this; - } - - template constexpr Point& operator=(const T &t) - { - x = t.x; - y = t.y; - return *this; - } - template constexpr bool operator==(const T &t) const - { - return x == t.x && y == t.y; - } - template constexpr bool operator!=(const T &t) const - { - return !(*this == t); - } - - constexpr bool isValid() const - { - return x > std::numeric_limits::min() && y > std::numeric_limits::min(); - } - - constexpr int lengthSquared() const - { - return x * x + y * y; - } - - int length() const - { - return std::sqrt(lengthSquared()); - } - - double angle() const - { - return std::atan2(y, x); // rad - } - - template - void serialize(Handler &h, const int version) - { - h & x; - h & y; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Point.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 + +VCMI_LIB_NAMESPACE_BEGIN +class int3; + +// A point with x/y coordinate, used mostly for graphic rendering +class Point +{ +public: + int x, y; + + //constructors + constexpr Point() : x(0), y(0) + { + } + + constexpr Point(int X, int Y) + : x(X) + , y(Y) + { + } + + constexpr static Point makeInvalid() + { + return Point(std::numeric_limits::min(), std::numeric_limits::min()); + } + + explicit DLL_LINKAGE Point(const int3 &a); + + template + constexpr Point operator+(const T &b) const + { + return Point(x+b.x,y+b.y); + } + + template + constexpr Point operator/(const T &div) const + { + return Point(x/div, y/div); + } + + template + constexpr Point operator*(const T &mul) const + { + return Point(x*mul, y*mul); + } + + constexpr Point operator*(const Point &b) const + { + return Point(x*b.x,y*b.y); + } + + template + constexpr Point& operator+=(const T &b) + { + x += b.x; + y += b.y; + return *this; + } + + constexpr Point operator-() const + { + return Point(-x, -y); + } + + template + constexpr Point operator-(const T &b) const + { + return Point(x - b.x, y - b.y); + } + + template + constexpr Point& operator-=(const T &b) + { + x -= b.x; + y -= b.y; + return *this; + } + + template constexpr Point& operator=(const T &t) + { + x = t.x; + y = t.y; + return *this; + } + template constexpr bool operator==(const T &t) const + { + return x == t.x && y == t.y; + } + template constexpr bool operator!=(const T &t) const + { + return !(*this == t); + } + + constexpr bool isValid() const + { + return x > std::numeric_limits::min() && y > std::numeric_limits::min(); + } + + constexpr int lengthSquared() const + { + return x * x + y * y; + } + + int length() const + { + return std::sqrt(lengthSquared()); + } + + double angle() const + { + return std::atan2(y, x); // rad + } + + template + void serialize(Handler &h, const int version) + { + h & x; + h & y; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Rect.cpp b/lib/Rect.cpp index 1dfcd8fbc..db5b7a176 100644 --- a/lib/Rect.cpp +++ b/lib/Rect.cpp @@ -1,147 +1,147 @@ -/* - * Rect.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 "Rect.h" -#include "int3.h" - -VCMI_LIB_NAMESPACE_BEGIN - -Point::Point(const int3 & a) - : x(a.x) - , y(a.y) -{ -} - -/// Returns rect union - rect that covers both this rect and provided rect -Rect Rect::include(const Rect & other) const -{ - Point topLeft{ - std::min(this->left(), other.left()), - std::min(this->top(), other.top()) - }; - - Point bottomRight{ - std::max(this->right(), other.right()), - std::max(this->bottom(), other.bottom()) - }; - - return Rect(topLeft, bottomRight - topLeft); -} - -Rect Rect::createCentered( const Point & around, const Point & dimensions ) -{ - return Rect(around - dimensions/2, dimensions); -} - -Rect Rect::createAround(const Rect &r, int width) -{ - return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); -} - -Rect Rect::createCentered( const Rect & rect, const Point & dimensions) -{ - return createCentered(rect.center(), dimensions); -} - -bool Rect::intersectionTest(const Rect & other) const -{ - // this rect is above other rect - if(this->bottom() < other.top()) - return false; - - // this rect is below other rect - if(this->top() > other.bottom() ) - return false; - - // this rect is to the left of other rect - if(this->right() < other.left()) - return false; - - // this rect is to the right of other rect - if(this->left() > other.right()) - return false; - - return true; -} - -/// Algorithm to test whether line segment between points line1-line2 will intersect with -/// rectangle specified by top-left and bottom-right points -/// Note that in order to avoid floating point rounding errors algorithm uses integers with no divisions -bool Rect::intersectionTest(const Point & line1, const Point & line2) const -{ - // check whether segment is located to the left of our rect - if (line1.x < left() && line2.x < left()) - return false; - - // check whether segment is located to the right of our rect - if (line1.x > right() && line2.x > right()) - return false; - - // check whether segment is located on top of our rect - if (line1.y < top() && line2.y < top()) - return false; - - // check whether segment is located below of our rect - if (line1.y > bottom() && line2.y > bottom()) - return false; - - Point vector { line2.x - line1.x, line2.y - line1.y}; - - // compute position of corners relative to our line - int tlTest = vector.y*topLeft().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); - int trTest = vector.y*bottomRight().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); - int blTest = vector.y*topLeft().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); - int brTest = vector.y*bottomRight().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); - - // if all points are on the left of our line then there is no intersection - if ( tlTest > 0 && trTest > 0 && blTest > 0 && brTest > 0 ) - return false; - - // if all points are on the right of our line then there is no intersection - if ( tlTest < 0 && trTest < 0 && blTest < 0 && brTest < 0 ) - return false; - - // if all previous checks failed, this means that there is an intersection between line and AABB - return true; -} - - -Rect Rect::intersect(const Rect & other) const -{ - if(intersectionTest(other)) - { - Point topLeft{ - std::max(this->left(), other.left()), - std::max(this->top(), other.top()) - }; - - Point bottomRight{ - std::min(this->right(), other.right()), - std::min(this->bottom(), other.bottom()) - }; - - return Rect(topLeft, bottomRight - topLeft); - } - else - { - return Rect(); - } -} - -int Rect::distanceTo(const Point & target) const -{ - int distanceX = std::max({left() - target.x, 0, target.x - right()}); - int distanceY = std::max({top() - target.y, 0, target.y - bottom()}); - - return Point(distanceX, distanceY).length(); -} - -VCMI_LIB_NAMESPACE_END +/* + * Rect.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 "Rect.h" +#include "int3.h" + +VCMI_LIB_NAMESPACE_BEGIN + +Point::Point(const int3 & a) + : x(a.x) + , y(a.y) +{ +} + +/// Returns rect union - rect that covers both this rect and provided rect +Rect Rect::include(const Rect & other) const +{ + Point topLeft{ + std::min(this->left(), other.left()), + std::min(this->top(), other.top()) + }; + + Point bottomRight{ + std::max(this->right(), other.right()), + std::max(this->bottom(), other.bottom()) + }; + + return Rect(topLeft, bottomRight - topLeft); +} + +Rect Rect::createCentered( const Point & around, const Point & dimensions ) +{ + return Rect(around - dimensions/2, dimensions); +} + +Rect Rect::createAround(const Rect &r, int width) +{ + return Rect(r.x - width, r.y - width, r.w + width * 2, r.h + width * 2); +} + +Rect Rect::createCentered( const Rect & rect, const Point & dimensions) +{ + return createCentered(rect.center(), dimensions); +} + +bool Rect::intersectionTest(const Rect & other) const +{ + // this rect is above other rect + if(this->bottom() < other.top()) + return false; + + // this rect is below other rect + if(this->top() > other.bottom() ) + return false; + + // this rect is to the left of other rect + if(this->right() < other.left()) + return false; + + // this rect is to the right of other rect + if(this->left() > other.right()) + return false; + + return true; +} + +/// Algorithm to test whether line segment between points line1-line2 will intersect with +/// rectangle specified by top-left and bottom-right points +/// Note that in order to avoid floating point rounding errors algorithm uses integers with no divisions +bool Rect::intersectionTest(const Point & line1, const Point & line2) const +{ + // check whether segment is located to the left of our rect + if (line1.x < left() && line2.x < left()) + return false; + + // check whether segment is located to the right of our rect + if (line1.x > right() && line2.x > right()) + return false; + + // check whether segment is located on top of our rect + if (line1.y < top() && line2.y < top()) + return false; + + // check whether segment is located below of our rect + if (line1.y > bottom() && line2.y > bottom()) + return false; + + Point vector { line2.x - line1.x, line2.y - line1.y}; + + // compute position of corners relative to our line + int tlTest = vector.y*topLeft().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); + int trTest = vector.y*bottomRight().x - vector.x*topLeft().y + (line2.x*line1.y-line1.x*line2.y); + int blTest = vector.y*topLeft().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); + int brTest = vector.y*bottomRight().x - vector.x*bottomRight().y + (line2.x*line1.y-line1.x*line2.y); + + // if all points are on the left of our line then there is no intersection + if ( tlTest > 0 && trTest > 0 && blTest > 0 && brTest > 0 ) + return false; + + // if all points are on the right of our line then there is no intersection + if ( tlTest < 0 && trTest < 0 && blTest < 0 && brTest < 0 ) + return false; + + // if all previous checks failed, this means that there is an intersection between line and AABB + return true; +} + + +Rect Rect::intersect(const Rect & other) const +{ + if(intersectionTest(other)) + { + Point topLeft{ + std::max(this->left(), other.left()), + std::max(this->top(), other.top()) + }; + + Point bottomRight{ + std::min(this->right(), other.right()), + std::min(this->bottom(), other.bottom()) + }; + + return Rect(topLeft, bottomRight - topLeft); + } + else + { + return Rect(); + } +} + +int Rect::distanceTo(const Point & target) const +{ + int distanceX = std::max({left() - target.x, 0, target.x - right()}); + int distanceY = std::max({top() - target.y, 0, target.y - bottom()}); + + return Point(distanceX, distanceY).length(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/Rect.h b/lib/Rect.h index b14d5e131..9aaa3578e 100644 --- a/lib/Rect.h +++ b/lib/Rect.h @@ -1,175 +1,175 @@ -/* - * Rect.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 "Point.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Rectangle class, which have a position and a size -class Rect -{ -public: - int x; - int y; - int w; - int h; - - Rect() - { - x = y = w = h = -1; - } - Rect(int X, int Y, int W, int H) - { - x = X; - y = Y; - w = W; - h = H; - } - Rect(const Point & position, const Point & size) - { - x = position.x; - y = position.y; - w = size.x; - h = size.y; - } - Rect(const Rect& r) = default; - - DLL_LINKAGE static Rect createCentered( const Point & around, const Point & size ); - DLL_LINKAGE static Rect createCentered( const Rect & target, const Point & size ); - DLL_LINKAGE static Rect createAround(const Rect &r, int borderWidth); - - bool isInside(int qx, int qy) const - { - if (qx > x && qxy && qy - void serialize(Handler &h, const int version) - { - h & x; - h & y; - h & w; - h & this->h; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * Rect.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 "Point.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// Rectangle class, which have a position and a size +class Rect +{ +public: + int x; + int y; + int w; + int h; + + Rect() + { + x = y = w = h = -1; + } + Rect(int X, int Y, int W, int H) + { + x = X; + y = Y; + w = W; + h = H; + } + Rect(const Point & position, const Point & size) + { + x = position.x; + y = position.y; + w = size.x; + h = size.y; + } + Rect(const Rect& r) = default; + + DLL_LINKAGE static Rect createCentered( const Point & around, const Point & size ); + DLL_LINKAGE static Rect createCentered( const Rect & target, const Point & size ); + DLL_LINKAGE static Rect createAround(const Rect &r, int borderWidth); + + bool isInside(int qx, int qy) const + { + if (qx > x && qxy && qy + void serialize(Handler &h, const int version) + { + h & x; + h & y; + h & w; + h & this->h; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index e89855dcf..e9fa85243 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -1,169 +1,169 @@ -/* - * ResourceSet.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 "GameConstants.h" -#include "ResourceSet.h" -#include "constants/StringConstants.h" -#include "JsonNode.h" -#include "serializer/JsonSerializeFormat.h" -#include "mapObjects/CObjectHandler.h" -#include "VCMI_Lib.h" - -VCMI_LIB_NAMESPACE_BEGIN - -ResourceSet::ResourceSet(const JsonNode & node) -{ - for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); -} - -ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, - TResource gems, TResource gold, TResource mithril) -{ - container[GameResID(EGameResID::WOOD)] = wood; - container[GameResID(EGameResID::MERCURY)] = mercury; - container[GameResID(EGameResID::ORE)] = ore; - container[GameResID(EGameResID::SULFUR)] = sulfur; - container[GameResID(EGameResID::CRYSTAL)] = crystal; - container[GameResID(EGameResID::GEMS)] = gems; - container[GameResID(EGameResID::GOLD)] = gold; - container[GameResID(EGameResID::MITHRIL)] = mithril; -} - -void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) -{ - if(handler.saving && !nonZero()) - return; - auto s = handler.enterStruct(fieldName); - - //TODO: add proper support for mithril to map format - for(int idx = 0; idx < GameConstants::RESOURCE_QUANTITY - 1; idx ++) - handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], this->operator[](idx), 0); -} - -bool ResourceSet::nonZero() const -{ - for(const auto & elem : *this) - if(elem) - return true; - - return false; -} - -void ResourceSet::amax(const TResourceCap &val) -{ - for(auto & elem : *this) - vstd::amax(elem, val); -} - -void ResourceSet::amin(const TResourceCap &val) -{ - for(auto & elem : *this) - vstd::amin(elem, val); -} - -void ResourceSet::positive() -{ - for(auto & elem : *this) - vstd::amax(elem, 0); -} - -static bool canAfford(const ResourceSet &res, const ResourceSet &price) -{ - assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY); - for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - if(price[i] > res[i]) - return false; - - return true; -} - -bool ResourceSet::canBeAfforded(const ResourceSet &res) const -{ - return VCMI_LIB_WRAP_NAMESPACE(canAfford(res, *this)); -} - -bool ResourceSet::canAfford(const ResourceSet &price) const -{ - return VCMI_LIB_WRAP_NAMESPACE(canAfford(*this, price)); -} - -TResourceCap ResourceSet::marketValue() const -{ - TResourceCap total = 0; - for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) - total += static_cast(VLC->objh->resVals[i]) * static_cast(operator[](i)); - return total; -} - -std::string ResourceSet::toString() const -{ - std::ostringstream out; - out << "["; - for(auto it = begin(); it != end(); ++it) - { - out << *it; - if(std::prev(end()) != it) out << ", "; - } - out << "]"; - return out.str(); -} - -bool ResourceSet::nziterator::valid() const -{ - return cur.resType < GameResID::COUNT && cur.resVal; -} - -ResourceSet::nziterator ResourceSet::nziterator::operator++() -{ - advance(); - return *this; -} - -ResourceSet::nziterator ResourceSet::nziterator::operator++(int) -{ - nziterator ret = *this; - advance(); - return ret; -} - -const ResourceSet::nziterator::ResEntry& ResourceSet::nziterator::operator*() const -{ - return cur; -} - -const ResourceSet::nziterator::ResEntry * ResourceSet::nziterator::operator->() const -{ - return &cur; -} - -void ResourceSet::nziterator::advance() -{ - do - { - ++cur.resType; - } while(cur.resType < GameResID::COUNT && !(cur.resVal=rs[cur.resType])); - - if(cur.resType >= GameResID::COUNT) - cur.resVal = -1; -} - -ResourceSet::nziterator::nziterator(const ResourceSet &RS) - : rs(RS) -{ - cur.resType = EGameResID::WOOD; - cur.resVal = rs[EGameResID::WOOD]; - - if(!valid()) - advance(); -} - -VCMI_LIB_NAMESPACE_END +/* + * ResourceSet.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 "GameConstants.h" +#include "ResourceSet.h" +#include "constants/StringConstants.h" +#include "JsonNode.h" +#include "serializer/JsonSerializeFormat.h" +#include "mapObjects/CObjectHandler.h" +#include "VCMI_Lib.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ResourceSet::ResourceSet(const JsonNode & node) +{ + for(auto i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); +} + +ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, + TResource gems, TResource gold, TResource mithril) +{ + container[GameResID(EGameResID::WOOD)] = wood; + container[GameResID(EGameResID::MERCURY)] = mercury; + container[GameResID(EGameResID::ORE)] = ore; + container[GameResID(EGameResID::SULFUR)] = sulfur; + container[GameResID(EGameResID::CRYSTAL)] = crystal; + container[GameResID(EGameResID::GEMS)] = gems; + container[GameResID(EGameResID::GOLD)] = gold; + container[GameResID(EGameResID::MITHRIL)] = mithril; +} + +void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) +{ + if(handler.saving && !nonZero()) + return; + auto s = handler.enterStruct(fieldName); + + //TODO: add proper support for mithril to map format + for(int idx = 0; idx < GameConstants::RESOURCE_QUANTITY - 1; idx ++) + handler.serializeInt(GameConstants::RESOURCE_NAMES[idx], this->operator[](idx), 0); +} + +bool ResourceSet::nonZero() const +{ + for(const auto & elem : *this) + if(elem) + return true; + + return false; +} + +void ResourceSet::amax(const TResourceCap &val) +{ + for(auto & elem : *this) + vstd::amax(elem, val); +} + +void ResourceSet::amin(const TResourceCap &val) +{ + for(auto & elem : *this) + vstd::amin(elem, val); +} + +void ResourceSet::positive() +{ + for(auto & elem : *this) + vstd::amax(elem, 0); +} + +static bool canAfford(const ResourceSet &res, const ResourceSet &price) +{ + assert(res.size() == price.size() && price.size() == GameConstants::RESOURCE_QUANTITY); + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + if(price[i] > res[i]) + return false; + + return true; +} + +bool ResourceSet::canBeAfforded(const ResourceSet &res) const +{ + return VCMI_LIB_WRAP_NAMESPACE(canAfford(res, *this)); +} + +bool ResourceSet::canAfford(const ResourceSet &price) const +{ + return VCMI_LIB_WRAP_NAMESPACE(canAfford(*this, price)); +} + +TResourceCap ResourceSet::marketValue() const +{ + TResourceCap total = 0; + for(int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++) + total += static_cast(VLC->objh->resVals[i]) * static_cast(operator[](i)); + return total; +} + +std::string ResourceSet::toString() const +{ + std::ostringstream out; + out << "["; + for(auto it = begin(); it != end(); ++it) + { + out << *it; + if(std::prev(end()) != it) out << ", "; + } + out << "]"; + return out.str(); +} + +bool ResourceSet::nziterator::valid() const +{ + return cur.resType < GameResID::COUNT && cur.resVal; +} + +ResourceSet::nziterator ResourceSet::nziterator::operator++() +{ + advance(); + return *this; +} + +ResourceSet::nziterator ResourceSet::nziterator::operator++(int) +{ + nziterator ret = *this; + advance(); + return ret; +} + +const ResourceSet::nziterator::ResEntry& ResourceSet::nziterator::operator*() const +{ + return cur; +} + +const ResourceSet::nziterator::ResEntry * ResourceSet::nziterator::operator->() const +{ + return &cur; +} + +void ResourceSet::nziterator::advance() +{ + do + { + ++cur.resType; + } while(cur.resType < GameResID::COUNT && !(cur.resVal=rs[cur.resType])); + + if(cur.resType >= GameResID::COUNT) + cur.resVal = -1; +} + +ResourceSet::nziterator::nziterator(const ResourceSet &RS) + : rs(RS) +{ + cur.resType = EGameResID::WOOD; + cur.resVal = rs[EGameResID::WOOD]; + + if(!valid()) + advance(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 1b4d5908d..676ee3730 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -1,227 +1,227 @@ -/* - * ResourceSet.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 "GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -using TResource = int32_t; -using TResourceCap = int64_t; //to avoid overflow when adding integers. Signed values are easier to control. - -class JsonNode; -class JsonSerializeFormat; - -class ResourceSet; - -//class to be representing a vector of resource -class ResourceSet -{ -private: - std::array container; -public: - // read resources set from json. Format example: { "gold": 500, "wood":5 } - DLL_LINKAGE ResourceSet(const JsonNode & node); - DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0, - TResource gems = 0, TResource gold = 0, TResource mithril = 0); - - -#define scalarOperator(OPSIGN) \ - ResourceSet& operator OPSIGN ## =(const TResource &rhs) \ - { \ - for(auto i = 0; i < container.size(); i++) \ - container.at(i) OPSIGN ## = rhs; \ - \ - return *this; \ - } - -#define vectorOperator(OPSIGN) \ - ResourceSet& operator OPSIGN ## =(const ResourceSet &rhs) \ - { \ - for(auto i = 0; i < container.size(); i++) \ - container.at(i) OPSIGN ## = rhs[i]; \ - \ - return *this; \ - } - -#define twoOperands(OPSIGN, RHS_TYPE) \ - friend ResourceSet operator OPSIGN(ResourceSet lhs, const RHS_TYPE &rhs) \ - { \ - lhs OPSIGN ## = rhs; \ - return lhs; \ - } - - scalarOperator(+) - scalarOperator(-) - scalarOperator(*) - scalarOperator(/) - vectorOperator(+) - vectorOperator(-) - twoOperands(+, TResource) - twoOperands(-, TResource) - twoOperands(*, TResource) - twoOperands(/, TResource) - twoOperands(+, ResourceSet) - twoOperands(-, ResourceSet) - - -#undef scalarOperator -#undef vectorOperator -#undef twoOperands - - using const_reference = decltype(container)::const_reference; - using value_type = decltype(container)::value_type; - using const_iterator = decltype(container)::const_iterator; - using iterator = decltype(container)::iterator; - - // Array-like interface - TResource & operator[](GameResID index) - { - return operator[](index.getNum()); - } - - const TResource & operator[](GameResID index) const - { - return operator[](index.getNum()); - } - - TResource & operator[](size_t index) - { - return container.at(index); - } - - const TResource & operator[](size_t index) const - { - return container.at(index); - } - - bool empty () const - { - for(const auto & res : *this) - if(res) - return false; - - return true; - } - - // C++ range-based for support - auto begin () -> decltype (container.begin()) - { - return container.begin(); - } - - auto end () -> decltype (container.end()) - { - return container.end(); - } - - auto begin () const -> decltype (container.cbegin()) - { - return container.cbegin(); - } - - auto end () const -> decltype (container.cend()) - { - return container.cend(); - } - - auto size () const -> decltype (container.size()) - { - return container.size(); - } - - //to be used for calculations of type "how many units of sth can I afford?" - int operator/(const ResourceSet &rhs) - { - int ret = INT_MAX; - for(int i = 0; i < container.size(); i++) - if(rhs[i]) - vstd::amin(ret, container.at(i) / rhs[i]); - - return ret; - } - - ResourceSet & operator=(const TResource &rhs) - { - for(int & i : container) - i = rhs; - - return *this; - } - - ResourceSet operator-() const - { - ResourceSet ret; - for(int i = 0; i < container.size(); i++) - ret[i] = -container.at(i); - return ret; - } - - bool operator==(const ResourceSet &rhs) const - { - return this->container == rhs.container; - } - -// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] -// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a -// bool operator<(const ResourceSet &rhs) -// { -// for(int i = 0; i < size(); i++) -// if(at(i) >= rhs[i]) -// return false; -// -// return true; -// } - - template void serialize(Handler &h, const int version) - { - h & container; - } - - DLL_LINKAGE void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); - - DLL_LINKAGE void amax(const TResourceCap &val); //performs vstd::amax on each element - DLL_LINKAGE void amin(const TResourceCap &val); //performs vstd::amin on each element - DLL_LINKAGE void positive(); //values below 0 are set to 0 - upgrade cost can't be negative, for example - DLL_LINKAGE bool nonZero() const; //returns true if at least one value is non-zero; - DLL_LINKAGE bool canAfford(const ResourceSet &price) const; - DLL_LINKAGE bool canBeAfforded(const ResourceSet &res) const; - DLL_LINKAGE TResourceCap marketValue() const; - - DLL_LINKAGE std::string toString() const; - - //special iterator of iterating over non-zero resources in set - class DLL_LINKAGE nziterator - { - struct ResEntry - { - GameResID resType; - TResourceCap resVal; - } cur; - const ResourceSet &rs; - void advance(); - - public: - nziterator(const ResourceSet &RS); - bool valid() const; - nziterator operator++(); - nziterator operator++(int); - const ResEntry& operator*() const; - const ResEntry* operator->() const; - }; - - -}; - -using TResources = ResourceSet; - - -VCMI_LIB_NAMESPACE_END +/* + * ResourceSet.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 "GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TResource = int32_t; +using TResourceCap = int64_t; //to avoid overflow when adding integers. Signed values are easier to control. + +class JsonNode; +class JsonSerializeFormat; + +class ResourceSet; + +//class to be representing a vector of resource +class ResourceSet +{ +private: + std::array container; +public: + // read resources set from json. Format example: { "gold": 500, "wood":5 } + DLL_LINKAGE ResourceSet(const JsonNode & node); + DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0, + TResource gems = 0, TResource gold = 0, TResource mithril = 0); + + +#define scalarOperator(OPSIGN) \ + ResourceSet& operator OPSIGN ## =(const TResource &rhs) \ + { \ + for(auto i = 0; i < container.size(); i++) \ + container.at(i) OPSIGN ## = rhs; \ + \ + return *this; \ + } + +#define vectorOperator(OPSIGN) \ + ResourceSet& operator OPSIGN ## =(const ResourceSet &rhs) \ + { \ + for(auto i = 0; i < container.size(); i++) \ + container.at(i) OPSIGN ## = rhs[i]; \ + \ + return *this; \ + } + +#define twoOperands(OPSIGN, RHS_TYPE) \ + friend ResourceSet operator OPSIGN(ResourceSet lhs, const RHS_TYPE &rhs) \ + { \ + lhs OPSIGN ## = rhs; \ + return lhs; \ + } + + scalarOperator(+) + scalarOperator(-) + scalarOperator(*) + scalarOperator(/) + vectorOperator(+) + vectorOperator(-) + twoOperands(+, TResource) + twoOperands(-, TResource) + twoOperands(*, TResource) + twoOperands(/, TResource) + twoOperands(+, ResourceSet) + twoOperands(-, ResourceSet) + + +#undef scalarOperator +#undef vectorOperator +#undef twoOperands + + using const_reference = decltype(container)::const_reference; + using value_type = decltype(container)::value_type; + using const_iterator = decltype(container)::const_iterator; + using iterator = decltype(container)::iterator; + + // Array-like interface + TResource & operator[](GameResID index) + { + return operator[](index.getNum()); + } + + const TResource & operator[](GameResID index) const + { + return operator[](index.getNum()); + } + + TResource & operator[](size_t index) + { + return container.at(index); + } + + const TResource & operator[](size_t index) const + { + return container.at(index); + } + + bool empty () const + { + for(const auto & res : *this) + if(res) + return false; + + return true; + } + + // C++ range-based for support + auto begin () -> decltype (container.begin()) + { + return container.begin(); + } + + auto end () -> decltype (container.end()) + { + return container.end(); + } + + auto begin () const -> decltype (container.cbegin()) + { + return container.cbegin(); + } + + auto end () const -> decltype (container.cend()) + { + return container.cend(); + } + + auto size () const -> decltype (container.size()) + { + return container.size(); + } + + //to be used for calculations of type "how many units of sth can I afford?" + int operator/(const ResourceSet &rhs) + { + int ret = INT_MAX; + for(int i = 0; i < container.size(); i++) + if(rhs[i]) + vstd::amin(ret, container.at(i) / rhs[i]); + + return ret; + } + + ResourceSet & operator=(const TResource &rhs) + { + for(int & i : container) + i = rhs; + + return *this; + } + + ResourceSet operator-() const + { + ResourceSet ret; + for(int i = 0; i < container.size(); i++) + ret[i] = -container.at(i); + return ret; + } + + bool operator==(const ResourceSet &rhs) const + { + return this->container == rhs.container; + } + +// WARNING: comparison operators are used for "can afford" relation: a <= b means that foreach i a[i] <= b[i] +// that doesn't work the other way: a > b doesn't mean that a cannot be afforded with b, it's still b can afford a +// bool operator<(const ResourceSet &rhs) +// { +// for(int i = 0; i < size(); i++) +// if(at(i) >= rhs[i]) +// return false; +// +// return true; +// } + + template void serialize(Handler &h, const int version) + { + h & container; + } + + DLL_LINKAGE void serializeJson(JsonSerializeFormat & handler, const std::string & fieldName); + + DLL_LINKAGE void amax(const TResourceCap &val); //performs vstd::amax on each element + DLL_LINKAGE void amin(const TResourceCap &val); //performs vstd::amin on each element + DLL_LINKAGE void positive(); //values below 0 are set to 0 - upgrade cost can't be negative, for example + DLL_LINKAGE bool nonZero() const; //returns true if at least one value is non-zero; + DLL_LINKAGE bool canAfford(const ResourceSet &price) const; + DLL_LINKAGE bool canBeAfforded(const ResourceSet &res) const; + DLL_LINKAGE TResourceCap marketValue() const; + + DLL_LINKAGE std::string toString() const; + + //special iterator of iterating over non-zero resources in set + class DLL_LINKAGE nziterator + { + struct ResEntry + { + GameResID resType; + TResourceCap resVal; + } cur; + const ResourceSet &rs; + void advance(); + + public: + nziterator(const ResourceSet &RS); + bool valid() const; + nziterator operator++(); + nziterator operator++(int); + const ResEntry& operator*() const; + const ResEntry* operator->() const; + }; + + +}; + +using TResources = ResourceSet; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/StartInfo.h b/lib/StartInfo.h index dbf5b6a71..b4f563e7c 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -1,208 +1,208 @@ -/* - * StartInfo.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 "vstd/DateUtils.h" - -#include "GameConstants.h" -#include "TurnTimerInfo.h" -#include "campaign/CampaignConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CMapGenOptions; -class CampaignState; -class CMapInfo; -struct PlayerInfo; -class PlayerColor; - -struct DLL_LINKAGE SimturnsInfo -{ - /// Minimal number of turns that must be played simultaneously even if contact has been detected - int requiredTurns = 0; - /// Maximum number of turns that might be played simultaneously unless contact is detected - int optionalTurns = 0; - /// If set to true, human and 1 AI can act at the same time - bool allowHumanWithAI = true; - - template - void serialize(Handler &h, const int version) - { - h & requiredTurns; - h & optionalTurns; - h & allowHumanWithAI; - } -}; - -enum class PlayerStartingBonus : int8_t -{ - RANDOM = -1, - ARTIFACT = 0, - GOLD = 1, - RESOURCE = 2 -}; - -/// Struct which describes the name, the color, the starting bonus of a player -struct DLL_LINKAGE PlayerSettings -{ - enum { PLAYER_AI = 0 }; // for use in playerID - - PlayerStartingBonus bonus; - FactionID castle; - HeroTypeID hero; - HeroTypeID heroPortrait; //-1 if default, else ID - - std::string heroNameTextId; - PlayerColor color; //from 0 - - enum EHandicap {NO_HANDICAP, MILD, SEVERE}; - EHandicap handicap;//0-no, 1-mild, 2-severe - - std::string name; - std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids - bool compOnly; //true if this player is a computer only player; required for RMG - template - void serialize(Handler &h, const int version) - { - h & castle; - h & hero; - h & heroPortrait; - h & heroNameTextId; - h & bonus; - h & color; - h & handicap; - h & name; - h & connectedPlayerIDs; - h & compOnly; - } - - PlayerSettings(); - bool isControlledByAI() const; - bool isControlledByHuman() const; - - FactionID getCastleValidated() const; - HeroTypeID getHeroValidated() const; -}; - -/// Struct which describes the difficulty, the turn time,.. of a heroes match. -struct DLL_LINKAGE StartInfo -{ - enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; - - EMode mode; - ui8 difficulty; //0=easy; 4=impossible - - using TPlayerInfos = std::map; - TPlayerInfos playerInfos; //color indexed - - ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) - ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet - ui32 mapfileChecksum; //0 if not relevant - std::string startTimeIso8601; - std::string fileURI; - SimturnsInfo simturnsInfo; - TurnTimerInfo turnTimerInfo; - std::string mapname; // empty for random map, otherwise name of the map or savegame - bool createRandomMap() const { return mapGenOptions != nullptr; } - std::shared_ptr mapGenOptions; - - std::shared_ptr campState; - - PlayerSettings & getIthPlayersSettings(const PlayerColor & no); - const PlayerSettings & getIthPlayersSettings(const PlayerColor & no) const; - PlayerSettings * getPlayersSettings(const ui8 connectedPlayerId); - - // TODO: Must be client-side - std::string getCampaignName() const; - - template - void serialize(Handler &h, const int version) - { - h & mode; - h & difficulty; - h & playerInfos; - h & seedToBeUsed; - h & seedPostInit; - h & mapfileChecksum; - h & startTimeIso8601; - h & fileURI; - h & simturnsInfo; - h & turnTimerInfo; - h & mapname; - h & mapGenOptions; - h & campState; - } - - StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))), fileURI("") - { - - } -}; - -struct ClientPlayer -{ - int connection; - std::string name; - - template void serialize(Handler &h, const int version) - { - h & connection; - h & name; - } -}; - -struct DLL_LINKAGE LobbyState -{ - std::shared_ptr si; - std::shared_ptr mi; - std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" - int hostClientId; - // TODO: Campaign-only and we don't really need either of them. - // Before start both go into CCampaignState that is part of StartInfo - CampaignScenarioID campaignMap; - int campaignBonus; - - LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(CampaignScenarioID::NONE), campaignBonus(-1) {} - - template void serialize(Handler &h, const int version) - { - h & si; - h & mi; - h & playerNames; - h & hostClientId; - h & campaignMap; - h & campaignBonus; - } -}; - -struct DLL_LINKAGE LobbyInfo : public LobbyState -{ - boost::mutex stateMutex; - std::string uuid; - - LobbyInfo() {} - - void verifyStateBeforeStart(bool ignoreNoHuman = false) const; - - bool isClientHost(int clientId) const; - std::set getAllClientPlayers(int clientId); - std::vector getConnectedPlayerIdsForClient(int clientId) const; - - // Helpers for lobby state access - std::set clientHumanColors(int clientId); - PlayerColor clientFirstColor(int clientId) const; - bool isClientColor(int clientId, const PlayerColor & color) const; - ui8 clientFirstId(int clientId) const; // Used by chat only! - PlayerInfo & getPlayerInfo(PlayerColor color); - TeamID getPlayerTeamId(const PlayerColor & color); -}; - - -VCMI_LIB_NAMESPACE_END +/* + * StartInfo.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 "vstd/DateUtils.h" + +#include "GameConstants.h" +#include "TurnTimerInfo.h" +#include "campaign/CampaignConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CMapGenOptions; +class CampaignState; +class CMapInfo; +struct PlayerInfo; +class PlayerColor; + +struct DLL_LINKAGE SimturnsInfo +{ + /// Minimal number of turns that must be played simultaneously even if contact has been detected + int requiredTurns = 0; + /// Maximum number of turns that might be played simultaneously unless contact is detected + int optionalTurns = 0; + /// If set to true, human and 1 AI can act at the same time + bool allowHumanWithAI = true; + + template + void serialize(Handler &h, const int version) + { + h & requiredTurns; + h & optionalTurns; + h & allowHumanWithAI; + } +}; + +enum class PlayerStartingBonus : int8_t +{ + RANDOM = -1, + ARTIFACT = 0, + GOLD = 1, + RESOURCE = 2 +}; + +/// Struct which describes the name, the color, the starting bonus of a player +struct DLL_LINKAGE PlayerSettings +{ + enum { PLAYER_AI = 0 }; // for use in playerID + + PlayerStartingBonus bonus; + FactionID castle; + HeroTypeID hero; + HeroTypeID heroPortrait; //-1 if default, else ID + + std::string heroNameTextId; + PlayerColor color; //from 0 - + enum EHandicap {NO_HANDICAP, MILD, SEVERE}; + EHandicap handicap;//0-no, 1-mild, 2-severe + + std::string name; + std::set connectedPlayerIDs; //Empty - AI, or connectrd player ids + bool compOnly; //true if this player is a computer only player; required for RMG + template + void serialize(Handler &h, const int version) + { + h & castle; + h & hero; + h & heroPortrait; + h & heroNameTextId; + h & bonus; + h & color; + h & handicap; + h & name; + h & connectedPlayerIDs; + h & compOnly; + } + + PlayerSettings(); + bool isControlledByAI() const; + bool isControlledByHuman() const; + + FactionID getCastleValidated() const; + HeroTypeID getHeroValidated() const; +}; + +/// Struct which describes the difficulty, the turn time,.. of a heroes match. +struct DLL_LINKAGE StartInfo +{ + enum EMode {NEW_GAME, LOAD_GAME, CAMPAIGN, INVALID = 255}; + + EMode mode; + ui8 difficulty; //0=easy; 4=impossible + + using TPlayerInfos = std::map; + TPlayerInfos playerInfos; //color indexed + + ui32 seedToBeUsed; //0 if not sure (client requests server to decide, will be send in reply pack) + ui32 seedPostInit; //so we know that game is correctly synced at the start; 0 if not known yet + ui32 mapfileChecksum; //0 if not relevant + std::string startTimeIso8601; + std::string fileURI; + SimturnsInfo simturnsInfo; + TurnTimerInfo turnTimerInfo; + std::string mapname; // empty for random map, otherwise name of the map or savegame + bool createRandomMap() const { return mapGenOptions != nullptr; } + std::shared_ptr mapGenOptions; + + std::shared_ptr campState; + + PlayerSettings & getIthPlayersSettings(const PlayerColor & no); + const PlayerSettings & getIthPlayersSettings(const PlayerColor & no) const; + PlayerSettings * getPlayersSettings(const ui8 connectedPlayerId); + + // TODO: Must be client-side + std::string getCampaignName() const; + + template + void serialize(Handler &h, const int version) + { + h & mode; + h & difficulty; + h & playerInfos; + h & seedToBeUsed; + h & seedPostInit; + h & mapfileChecksum; + h & startTimeIso8601; + h & fileURI; + h & simturnsInfo; + h & turnTimerInfo; + h & mapname; + h & mapGenOptions; + h & campState; + } + + StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), + mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))), fileURI("") + { + + } +}; + +struct ClientPlayer +{ + int connection; + std::string name; + + template void serialize(Handler &h, const int version) + { + h & connection; + h & name; + } +}; + +struct DLL_LINKAGE LobbyState +{ + std::shared_ptr si; + std::shared_ptr mi; + std::map playerNames; // id of player <-> player name; 0 is reserved as ID of AI "players" + int hostClientId; + // TODO: Campaign-only and we don't really need either of them. + // Before start both go into CCampaignState that is part of StartInfo + CampaignScenarioID campaignMap; + int campaignBonus; + + LobbyState() : si(new StartInfo()), hostClientId(-1), campaignMap(CampaignScenarioID::NONE), campaignBonus(-1) {} + + template void serialize(Handler &h, const int version) + { + h & si; + h & mi; + h & playerNames; + h & hostClientId; + h & campaignMap; + h & campaignBonus; + } +}; + +struct DLL_LINKAGE LobbyInfo : public LobbyState +{ + boost::mutex stateMutex; + std::string uuid; + + LobbyInfo() {} + + void verifyStateBeforeStart(bool ignoreNoHuman = false) const; + + bool isClientHost(int clientId) const; + std::set getAllClientPlayers(int clientId); + std::vector getConnectedPlayerIdsForClient(int clientId) const; + + // Helpers for lobby state access + std::set clientHumanColors(int clientId); + PlayerColor clientFirstColor(int clientId) const; + bool isClientColor(int clientId, const PlayerColor & color) const; + ui8 clientFirstId(int clientId) const; // Used by chat only! + PlayerInfo & getPlayerInfo(PlayerColor color); + TeamID getPlayerTeamId(const PlayerColor & color); +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/StdInc.cpp b/lib/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/lib/StdInc.cpp +++ b/lib/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/lib/StdInc.h b/lib/StdInc.h index 25d8caf3d..82dfac8ad 100644 --- a/lib/StdInc.h +++ b/lib/StdInc.h @@ -1,7 +1,7 @@ -#pragma once - -#include "../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. +#pragma once + +#include "../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. diff --git a/lib/TextOperations.cpp b/lib/TextOperations.cpp index d5c966adb..df4771210 100644 --- a/lib/TextOperations.cpp +++ b/lib/TextOperations.cpp @@ -1,213 +1,213 @@ -/* - * TextOperations.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 "TextOperations.h" - -#include "CGeneralTextHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -size_t TextOperations::getUnicodeCharacterSize(char firstByte) -{ - // length of utf-8 character can be determined from 1st byte by counting number of highest bits set to 1: - // 0xxxxxxx -> 1 - ASCII chars - // 110xxxxx -> 2 - // 1110xxxx -> 3 - // 11110xxx -> 4 - last allowed in current standard - - auto value = static_cast(firstByte); - - if ((value & 0b10000000) == 0) - return 1; // ASCII - - if ((value & 0b11100000) == 0b11000000) - return 2; - - if ((value & 0b11110000) == 0b11100000) - return 3; - - if ((value & 0b11111000) == 0b11110000) - return 4; - - assert(0);// invalid unicode sequence - return 4; -} - -bool TextOperations::isValidUnicodeCharacter(const char * character, size_t maxSize) -{ - assert(maxSize > 0); - - auto value = static_cast(character[0]); - - // ASCII - if ( value < 0b10000000) - return maxSize > 0; - - // can't be first byte in UTF8 - if (value < 0b11000000) - return false; - - // above maximum allowed in standard (UTF codepoints are capped at 0x0010FFFF) - if (value > 0b11110000) - return false; - - // first character must follow rules checked in getUnicodeCharacterSize - size_t size = getUnicodeCharacterSize(character[0]); - - if (size > maxSize) - return false; - - // remaining characters must have highest bit set to 1 - for (size_t i = 1; i < size; i++) - { - auto characterValue = static_cast(character[i]); - if (characterValue < 0b10000000) - return false; - } - return true; -} - -bool TextOperations::isValidASCII(const std::string & text) -{ - for (const char & ch : text) - if (static_cast(ch) >= 0x80 ) - return false; - return true; -} - -bool TextOperations::isValidASCII(const char * data, size_t size) -{ - for (size_t i=0; i(data[i]) >= 0x80 ) - return false; - return true; -} - -bool TextOperations::isValidUnicodeString(const std::string & text) -{ - for (size_t i=0; i(data[0]) & 0b1111111; - case 2: - return - ((static_cast(data[0]) & 0b11111 ) << 6) + - ((static_cast(data[1]) & 0b111111) << 0) ; - case 3: - return - ((static_cast(data[0]) & 0b1111 ) << 12) + - ((static_cast(data[1]) & 0b111111) << 6) + - ((static_cast(data[2]) & 0b111111) << 0) ; - case 4: - return - ((static_cast(data[0]) & 0b111 ) << 18) + - ((static_cast(data[1]) & 0b111111) << 12) + - ((static_cast(data[2]) & 0b111111) << 6) + - ((static_cast(data[3]) & 0b111111) << 0) ; - } - - assert(0); - return 0; -} - -uint32_t TextOperations::getUnicodeCodepoint(char data, const std::string & encoding ) -{ - std::string stringNative(1, data); - std::string stringUnicode = toUnicode(stringNative, encoding); - - if (stringUnicode.empty()) - return 0; - - return getUnicodeCodepoint(stringUnicode.data(), stringUnicode.size()); -} - -std::string TextOperations::toUnicode(const std::string &text, const std::string &encoding) -{ - return boost::locale::conv::to_utf(text, encoding); -} - -std::string TextOperations::fromUnicode(const std::string &text, const std::string &encoding) -{ - return boost::locale::conv::from_utf(text, encoding); -} - -void TextOperations::trimRightUnicode(std::string & text, const size_t amount) -{ - if(text.empty()) - return; - //todo: more efficient algorithm - for(int i = 0; i< amount; i++){ - auto b = text.begin(); - auto e = text.end(); - size_t lastLen = 0; - size_t len = 0; - while (b != e) { - lastLen = len; - size_t n = getUnicodeCharacterSize(*b); - - if(!isValidUnicodeCharacter(&(*b),e-b)) - { - logGlobal->error("Invalid UTF8 sequence"); - break;//invalid sequence will be trimmed - } - - len += n; - b += n; - } - - text.resize(lastLen); - } -} - -size_t TextOperations::getUnicodeCharactersCount(const std::string & text) -{ - std::wstring_convert, char32_t> conv; - return conv.from_bytes(text).size(); -} - -std::string TextOperations::escapeString(std::string input) -{ - boost::replace_all(input, "\\", "\\\\"); - boost::replace_all(input, "\n", "\\n"); - boost::replace_all(input, "\r", "\\r"); - boost::replace_all(input, "\t", "\\t"); - boost::replace_all(input, "\"", "\\\""); - - return input; -} - -VCMI_LIB_NAMESPACE_END +/* + * TextOperations.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 "TextOperations.h" + +#include "CGeneralTextHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +size_t TextOperations::getUnicodeCharacterSize(char firstByte) +{ + // length of utf-8 character can be determined from 1st byte by counting number of highest bits set to 1: + // 0xxxxxxx -> 1 - ASCII chars + // 110xxxxx -> 2 + // 1110xxxx -> 3 + // 11110xxx -> 4 - last allowed in current standard + + auto value = static_cast(firstByte); + + if ((value & 0b10000000) == 0) + return 1; // ASCII + + if ((value & 0b11100000) == 0b11000000) + return 2; + + if ((value & 0b11110000) == 0b11100000) + return 3; + + if ((value & 0b11111000) == 0b11110000) + return 4; + + assert(0);// invalid unicode sequence + return 4; +} + +bool TextOperations::isValidUnicodeCharacter(const char * character, size_t maxSize) +{ + assert(maxSize > 0); + + auto value = static_cast(character[0]); + + // ASCII + if ( value < 0b10000000) + return maxSize > 0; + + // can't be first byte in UTF8 + if (value < 0b11000000) + return false; + + // above maximum allowed in standard (UTF codepoints are capped at 0x0010FFFF) + if (value > 0b11110000) + return false; + + // first character must follow rules checked in getUnicodeCharacterSize + size_t size = getUnicodeCharacterSize(character[0]); + + if (size > maxSize) + return false; + + // remaining characters must have highest bit set to 1 + for (size_t i = 1; i < size; i++) + { + auto characterValue = static_cast(character[i]); + if (characterValue < 0b10000000) + return false; + } + return true; +} + +bool TextOperations::isValidASCII(const std::string & text) +{ + for (const char & ch : text) + if (static_cast(ch) >= 0x80 ) + return false; + return true; +} + +bool TextOperations::isValidASCII(const char * data, size_t size) +{ + for (size_t i=0; i(data[i]) >= 0x80 ) + return false; + return true; +} + +bool TextOperations::isValidUnicodeString(const std::string & text) +{ + for (size_t i=0; i(data[0]) & 0b1111111; + case 2: + return + ((static_cast(data[0]) & 0b11111 ) << 6) + + ((static_cast(data[1]) & 0b111111) << 0) ; + case 3: + return + ((static_cast(data[0]) & 0b1111 ) << 12) + + ((static_cast(data[1]) & 0b111111) << 6) + + ((static_cast(data[2]) & 0b111111) << 0) ; + case 4: + return + ((static_cast(data[0]) & 0b111 ) << 18) + + ((static_cast(data[1]) & 0b111111) << 12) + + ((static_cast(data[2]) & 0b111111) << 6) + + ((static_cast(data[3]) & 0b111111) << 0) ; + } + + assert(0); + return 0; +} + +uint32_t TextOperations::getUnicodeCodepoint(char data, const std::string & encoding ) +{ + std::string stringNative(1, data); + std::string stringUnicode = toUnicode(stringNative, encoding); + + if (stringUnicode.empty()) + return 0; + + return getUnicodeCodepoint(stringUnicode.data(), stringUnicode.size()); +} + +std::string TextOperations::toUnicode(const std::string &text, const std::string &encoding) +{ + return boost::locale::conv::to_utf(text, encoding); +} + +std::string TextOperations::fromUnicode(const std::string &text, const std::string &encoding) +{ + return boost::locale::conv::from_utf(text, encoding); +} + +void TextOperations::trimRightUnicode(std::string & text, const size_t amount) +{ + if(text.empty()) + return; + //todo: more efficient algorithm + for(int i = 0; i< amount; i++){ + auto b = text.begin(); + auto e = text.end(); + size_t lastLen = 0; + size_t len = 0; + while (b != e) { + lastLen = len; + size_t n = getUnicodeCharacterSize(*b); + + if(!isValidUnicodeCharacter(&(*b),e-b)) + { + logGlobal->error("Invalid UTF8 sequence"); + break;//invalid sequence will be trimmed + } + + len += n; + b += n; + } + + text.resize(lastLen); + } +} + +size_t TextOperations::getUnicodeCharactersCount(const std::string & text) +{ + std::wstring_convert, char32_t> conv; + return conv.from_bytes(text).size(); +} + +std::string TextOperations::escapeString(std::string input) +{ + boost::replace_all(input, "\\", "\\\\"); + boost::replace_all(input, "\n", "\\n"); + boost::replace_all(input, "\r", "\\r"); + boost::replace_all(input, "\t", "\\t"); + boost::replace_all(input, "\"", "\\\""); + + return input; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/TextOperations.h b/lib/TextOperations.h index 0ca1ac69b..73768cef3 100644 --- a/lib/TextOperations.h +++ b/lib/TextOperations.h @@ -1,83 +1,83 @@ -/* - * TextOperations.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 - -VCMI_LIB_NAMESPACE_BEGIN - -/// Namespace that provides utilites for unicode support (UTF-8) -namespace TextOperations -{ - /// returns 32-bit UTF codepoint for UTF-8 character symbol - uint32_t DLL_LINKAGE getUnicodeCodepoint(const char *data, size_t maxSize); - - /// returns 32-bit UTF codepoint for character symbol in selected single-byte encoding - uint32_t DLL_LINKAGE getUnicodeCodepoint(char data, const std::string & encoding ); - - /// returns length (in bytes) of UTF-8 character starting from specified character - size_t DLL_LINKAGE getUnicodeCharacterSize(char firstByte); - - /// test if character is a valid UTF-8 symbol - /// maxSize - maximum number of bytes this symbol may consist from ( = remainer of string) - bool DLL_LINKAGE isValidUnicodeCharacter(const char * character, size_t maxSize); - - /// returns true if text contains valid ASCII-string - /// Note that since UTF-8 extends ASCII, any ASCII string is also UTF-8 string - bool DLL_LINKAGE isValidASCII(const std::string & text); - bool DLL_LINKAGE isValidASCII(const char * data, size_t size); - - /// test if text contains valid UTF-8 sequence - bool DLL_LINKAGE isValidUnicodeString(const std::string & text); - bool DLL_LINKAGE isValidUnicodeString(const char * data, size_t size); - - /// converts text to UTF-8 from specified encoding or from one specified in settings - std::string DLL_LINKAGE toUnicode(const std::string & text, const std::string & encoding); - - /// converts text from unicode to specified encoding or to one specified in settings - /// NOTE: usage of these functions should be avoided if possible - std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding); - - ///delete specified amount of UTF-8 characters from right - DLL_LINKAGE void trimRightUnicode(std::string & text, size_t amount = 1); - - /// give back amount of unicode characters - size_t DLL_LINKAGE getUnicodeCharactersCount(const std::string & text); - - /// converts number into string using metric system prefixes, e.g. 'k' or 'M' to keep resulting strings within specified size - /// Note that resulting string may have more symbols than digits: minus sign and prefix symbol - template - inline std::string formatMetric(Arithmetic number, int maxDigits); - - /// replaces all symbols that normally need escaping with appropriate escape sequences - std::string escapeString(std::string input); -}; - - - -template -inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits) -{ - Arithmetic max = std::pow(10, maxDigits); - if (std::abs(number) < max) - return std::to_string(number); - - std::string symbols = " kMGTPE"; - auto iter = symbols.begin(); - - while (std::abs(number) >= max) - { - number /= 1000; - iter++; - - assert(iter != symbols.end());//should be enough even for int64 - } - return std::to_string(number) + *iter; -} - -VCMI_LIB_NAMESPACE_END +/* + * TextOperations.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 + +VCMI_LIB_NAMESPACE_BEGIN + +/// Namespace that provides utilites for unicode support (UTF-8) +namespace TextOperations +{ + /// returns 32-bit UTF codepoint for UTF-8 character symbol + uint32_t DLL_LINKAGE getUnicodeCodepoint(const char *data, size_t maxSize); + + /// returns 32-bit UTF codepoint for character symbol in selected single-byte encoding + uint32_t DLL_LINKAGE getUnicodeCodepoint(char data, const std::string & encoding ); + + /// returns length (in bytes) of UTF-8 character starting from specified character + size_t DLL_LINKAGE getUnicodeCharacterSize(char firstByte); + + /// test if character is a valid UTF-8 symbol + /// maxSize - maximum number of bytes this symbol may consist from ( = remainer of string) + bool DLL_LINKAGE isValidUnicodeCharacter(const char * character, size_t maxSize); + + /// returns true if text contains valid ASCII-string + /// Note that since UTF-8 extends ASCII, any ASCII string is also UTF-8 string + bool DLL_LINKAGE isValidASCII(const std::string & text); + bool DLL_LINKAGE isValidASCII(const char * data, size_t size); + + /// test if text contains valid UTF-8 sequence + bool DLL_LINKAGE isValidUnicodeString(const std::string & text); + bool DLL_LINKAGE isValidUnicodeString(const char * data, size_t size); + + /// converts text to UTF-8 from specified encoding or from one specified in settings + std::string DLL_LINKAGE toUnicode(const std::string & text, const std::string & encoding); + + /// converts text from unicode to specified encoding or to one specified in settings + /// NOTE: usage of these functions should be avoided if possible + std::string DLL_LINKAGE fromUnicode(const std::string & text, const std::string & encoding); + + ///delete specified amount of UTF-8 characters from right + DLL_LINKAGE void trimRightUnicode(std::string & text, size_t amount = 1); + + /// give back amount of unicode characters + size_t DLL_LINKAGE getUnicodeCharactersCount(const std::string & text); + + /// converts number into string using metric system prefixes, e.g. 'k' or 'M' to keep resulting strings within specified size + /// Note that resulting string may have more symbols than digits: minus sign and prefix symbol + template + inline std::string formatMetric(Arithmetic number, int maxDigits); + + /// replaces all symbols that normally need escaping with appropriate escape sequences + std::string escapeString(std::string input); +}; + + + +template +inline std::string TextOperations::formatMetric(Arithmetic number, int maxDigits) +{ + Arithmetic max = std::pow(10, maxDigits); + if (std::abs(number) < max) + return std::to_string(number); + + std::string symbols = " kMGTPE"; + auto iter = symbols.begin(); + + while (std::abs(number) >= max) + { + number /= 1000; + iter++; + + assert(iter != symbols.end());//should be enough even for int64 + } + return std::to_string(number) + *iter; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/UnlockGuard.h b/lib/UnlockGuard.h index 39fb8ebb8..034979ff8 100644 --- a/lib/UnlockGuard.h +++ b/lib/UnlockGuard.h @@ -1,120 +1,120 @@ -/* - * UnlockGuard.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 - -VCMI_LIB_NAMESPACE_BEGIN - -namespace vstd -{ - namespace detail - { - template - class unlock_policy - { - protected: - void unlock(Mutex &m) - { - m.unlock(); - } - void lock(Mutex &m) - { - m.lock(); - } - }; - - template - class unlock_shared_policy - { - protected: - void unlock(Mutex &m) - { - m.unlock_shared(); - } - void lock(Mutex &m) - { - m.lock_shared(); - } - }; - } - - - //similar to boost::lock_guard but UNlocks for the scope + assertions - template > - class unlock_guard : LockingPolicy - { - private: - Mutex* m; - - explicit unlock_guard(unlock_guard&); - unlock_guard& operator=(unlock_guard&); - public: - explicit unlock_guard(Mutex& m_): - m(&m_) - { - this->unlock(*m); - } - - unlock_guard() - { - m = nullptr; - } - - unlock_guard(unlock_guard &&other) - : m(other.m) - { - other.m = nullptr; - } - - void release() - { - m = nullptr; - } - - ~unlock_guard() - { - if(m) - this->lock(*m); - } - }; - - template - unlock_guard > makeUnlockGuard(Mutex &m_) - { - return unlock_guard >(m_); - } - template - unlock_guard > makeEmptyGuard(Mutex &) - { - return unlock_guard >(); - } - template - unlock_guard > makeUnlockGuardIf(Mutex &m_, bool shallUnlock) - { - return shallUnlock - ? makeUnlockGuard(m_) - : unlock_guard >(); - } - template - unlock_guard > makeUnlockSharedGuard(Mutex &m_) - { - return unlock_guard >(m_); - } - template - unlock_guard > makeUnlockSharedGuardIf(Mutex &m_, bool shallUnlock) - { - return shallUnlock - ? makeUnlockSharedGuard(m_) - : unlock_guard >(); - } - - using unlock_shared_guard = unlock_guard>; -} - -VCMI_LIB_NAMESPACE_END +/* + * UnlockGuard.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 + +VCMI_LIB_NAMESPACE_BEGIN + +namespace vstd +{ + namespace detail + { + template + class unlock_policy + { + protected: + void unlock(Mutex &m) + { + m.unlock(); + } + void lock(Mutex &m) + { + m.lock(); + } + }; + + template + class unlock_shared_policy + { + protected: + void unlock(Mutex &m) + { + m.unlock_shared(); + } + void lock(Mutex &m) + { + m.lock_shared(); + } + }; + } + + + //similar to boost::lock_guard but UNlocks for the scope + assertions + template > + class unlock_guard : LockingPolicy + { + private: + Mutex* m; + + explicit unlock_guard(unlock_guard&); + unlock_guard& operator=(unlock_guard&); + public: + explicit unlock_guard(Mutex& m_): + m(&m_) + { + this->unlock(*m); + } + + unlock_guard() + { + m = nullptr; + } + + unlock_guard(unlock_guard &&other) + : m(other.m) + { + other.m = nullptr; + } + + void release() + { + m = nullptr; + } + + ~unlock_guard() + { + if(m) + this->lock(*m); + } + }; + + template + unlock_guard > makeUnlockGuard(Mutex &m_) + { + return unlock_guard >(m_); + } + template + unlock_guard > makeEmptyGuard(Mutex &) + { + return unlock_guard >(); + } + template + unlock_guard > makeUnlockGuardIf(Mutex &m_, bool shallUnlock) + { + return shallUnlock + ? makeUnlockGuard(m_) + : unlock_guard >(); + } + template + unlock_guard > makeUnlockSharedGuard(Mutex &m_) + { + return unlock_guard >(m_); + } + template + unlock_guard > makeUnlockSharedGuardIf(Mutex &m_, bool shallUnlock) + { + return shallUnlock + ? makeUnlockSharedGuard(m_) + : unlock_guard >(); + } + + using unlock_shared_guard = unlock_guard>; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 8c243c91c..3a4dea264 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -1,325 +1,325 @@ -/* - * VCMI_Lib.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 "VCMI_Lib.h" - -#include "CArtHandler.h" -#include "CBonusTypeHandler.h" -#include "CCreatureHandler.h" -#include "CHeroHandler.h" -#include "CTownHandler.h" -#include "CConfigHandler.h" -#include "RoadHandler.h" -#include "RiverHandler.h" -#include "TerrainHandler.h" -#include "CBuildingHandler.h" -#include "spells/CSpellHandler.h" -#include "spells/effects/Registry.h" -#include "CSkillHandler.h" -#include "CGeneralTextHandler.h" -#include "modding/CModHandler.h" -#include "modding/CModInfo.h" -#include "modding/IdentifierStorage.h" -#include "modding/CModVersion.h" -#include "IGameEventsReceiver.h" -#include "CStopWatch.h" -#include "VCMIDirs.h" -#include "filesystem/Filesystem.h" -#include "CConsoleHandler.h" -#include "rmg/CRmgTemplateStorage.h" -#include "mapObjectConstructors/CObjectClassesHandler.h" -#include "mapObjects/CObjectHandler.h" -#include "mapping/CMapEditManager.h" -#include "ScriptHandler.h" -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" -#include "GameSettings.h" - -VCMI_LIB_NAMESPACE_BEGIN - -LibClasses * VLC = nullptr; - -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) -{ - console = Console; - VLC = new LibClasses(); - VLC->loadFilesystem(extractArchives); - settings.init("config/settings.json", "vcmi:settings"); - persistentStorage.init("config/persistentStorage.json", ""); - VLC->loadModFilesystem(onlyEssential); - -} - -DLL_LINKAGE void loadDLLClasses(bool onlyEssential) -{ - VLC->init(onlyEssential); -} - -const ArtifactService * LibClasses::artifacts() const -{ - return arth; -} - -const CreatureService * LibClasses::creatures() const -{ - return creh; -} - -const FactionService * LibClasses::factions() const -{ - return townh; -} - -const HeroClassService * LibClasses::heroClasses() const -{ - return &heroh->classes; -} - -const HeroTypeService * LibClasses::heroTypes() const -{ - return heroh; -} - -#if SCRIPTING_ENABLED -const scripting::Service * LibClasses::scripts() const -{ - return scriptHandler; -} -#endif - -const spells::Service * LibClasses::spells() const -{ - return spellh; -} - -const SkillService * LibClasses::skills() const -{ - return skillh; -} - -const IBonusTypeHandler * LibClasses::getBth() const -{ - return bth; -} - -const CIdentifierStorage * LibClasses::identifiers() const -{ - return identifiersHandler; -} - -const spells::effects::Registry * LibClasses::spellEffects() const -{ - return spells::effects::GlobalRegistry::get(); -} - -spells::effects::Registry * LibClasses::spellEffects() -{ - return spells::effects::GlobalRegistry::get(); -} - -const BattleFieldService * LibClasses::battlefields() const -{ - return battlefieldsHandler; -} - -const ObstacleService * LibClasses::obstacles() const -{ - return obstacleHandler; -} - -const IGameSettings * LibClasses::settings() const -{ - return settingsHandler; -} - -void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) -{ - switch(metatype) - { - case Metatype::ARTIFACT: - arth->updateEntity(index, data); - break; - case Metatype::CREATURE: - creh->updateEntity(index, data); - break; - case Metatype::FACTION: - townh->updateEntity(index, data); - break; - case Metatype::HERO_CLASS: - heroh->classes.updateEntity(index, data); - break; - case Metatype::HERO_TYPE: - heroh->updateEntity(index, data); - break; - case Metatype::SKILL: - skillh->updateEntity(index, data); - break; - case Metatype::SPELL: - spellh->updateEntity(index, data); - break; - default: - logGlobal->error("Invalid Metatype id %d", static_cast(metatype)); - break; - } -} - -void LibClasses::loadFilesystem(bool extractArchives) -{ - CStopWatch loadTime; - - CResourceHandler::initialize(); - logGlobal->info("\tInitialization: %d ms", loadTime.getDiff()); - - CResourceHandler::load("config/filesystem.json", extractArchives); - logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); -} - -void LibClasses::loadModFilesystem(bool onlyEssential) -{ - CStopWatch loadTime; - modh = new CModHandler(); - identifiersHandler = new CIdentifierStorage(); - modh->loadMods(onlyEssential); - logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); - - modh->loadModFilesystems(); - logGlobal->info("\tMod filesystems: %d ms", loadTime.getDiff()); -} - -static void logHandlerLoaded(const std::string & name, CStopWatch & timer) -{ - logGlobal->info("\t\t %s handler: %d ms", name, timer.getDiff()); -} - -template void createHandler(Handler *&handler, const std::string &name, CStopWatch &timer) -{ - handler = new Handler(); - logHandlerLoaded(name, timer); -} - -void LibClasses::init(bool onlyEssential) -{ - CStopWatch pomtime; - CStopWatch totalTime; - - createHandler(settingsHandler, "Game Settings", pomtime); - modh->initializeConfig(); - - createHandler(generaltexth, "General text", pomtime); - createHandler(bth, "Bonus type", pomtime); - createHandler(roadTypeHandler, "Road", pomtime); - createHandler(riverTypeHandler, "River", pomtime); - createHandler(terrainTypeHandler, "Terrain", pomtime); - createHandler(heroh, "Hero", pomtime); - createHandler(arth, "Artifact", pomtime); - createHandler(creh, "Creature", pomtime); - createHandler(townh, "Town", pomtime); - createHandler(objh, "Object", pomtime); - createHandler(objtypeh, "Object types information", pomtime); - createHandler(spellh, "Spell", pomtime); - createHandler(skillh, "Skill", pomtime); - createHandler(terviewh, "Terrain view pattern", pomtime); - createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?) -#if SCRIPTING_ENABLED - createHandler(scriptHandler, "Script", pomtime); -#endif - createHandler(battlefieldsHandler, "Battlefields", pomtime); - createHandler(obstacleHandler, "Obstacles", pomtime); - logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); - - modh->load(); - modh->afterLoad(onlyEssential); -} - -void LibClasses::clear() -{ - delete heroh; - delete arth; - delete creh; - delete townh; - delete objh; - delete objtypeh; - delete spellh; - delete skillh; - delete modh; - delete bth; - delete tplh; - delete terviewh; -#if SCRIPTING_ENABLED - delete scriptHandler; -#endif - delete battlefieldsHandler; - delete generaltexth; - delete identifiersHandler; - makeNull(); -} - -void LibClasses::makeNull() -{ - generaltexth = nullptr; - heroh = nullptr; - arth = nullptr; - creh = nullptr; - townh = nullptr; - objh = nullptr; - objtypeh = nullptr; - spellh = nullptr; - skillh = nullptr; - modh = nullptr; - bth = nullptr; - tplh = nullptr; - terviewh = nullptr; -#if SCRIPTING_ENABLED - scriptHandler = nullptr; -#endif - battlefieldsHandler = nullptr; - identifiersHandler = nullptr; -} - -LibClasses::LibClasses() -{ - //init pointers to handlers - makeNull(); -} - -void LibClasses::callWhenDeserializing() -{ - //FIXME: check if any of these are needed - //generaltexth = new CGeneralTextHandler(); - //generaltexth->load(); - //arth->load(true); - //modh->recreateHandlers(); - //modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config -} - -#if SCRIPTING_ENABLED -void LibClasses::scriptsLoaded() -{ - scriptHandler->performRegistration(this); -} -#endif - -LibClasses::~LibClasses() -{ - clear(); -} - -std::shared_ptr LibClasses::getContent() const -{ - return modh->content; -} - -void LibClasses::setContent(std::shared_ptr content) -{ - modh->content = std::move(content); -} - -VCMI_LIB_NAMESPACE_END +/* + * VCMI_Lib.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 "VCMI_Lib.h" + +#include "CArtHandler.h" +#include "CBonusTypeHandler.h" +#include "CCreatureHandler.h" +#include "CHeroHandler.h" +#include "CTownHandler.h" +#include "CConfigHandler.h" +#include "RoadHandler.h" +#include "RiverHandler.h" +#include "TerrainHandler.h" +#include "CBuildingHandler.h" +#include "spells/CSpellHandler.h" +#include "spells/effects/Registry.h" +#include "CSkillHandler.h" +#include "CGeneralTextHandler.h" +#include "modding/CModHandler.h" +#include "modding/CModInfo.h" +#include "modding/IdentifierStorage.h" +#include "modding/CModVersion.h" +#include "IGameEventsReceiver.h" +#include "CStopWatch.h" +#include "VCMIDirs.h" +#include "filesystem/Filesystem.h" +#include "CConsoleHandler.h" +#include "rmg/CRmgTemplateStorage.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" +#include "mapObjects/CObjectHandler.h" +#include "mapping/CMapEditManager.h" +#include "ScriptHandler.h" +#include "BattleFieldHandler.h" +#include "ObstacleHandler.h" +#include "GameSettings.h" + +VCMI_LIB_NAMESPACE_BEGIN + +LibClasses * VLC = nullptr; + +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential, bool extractArchives) +{ + console = Console; + VLC = new LibClasses(); + VLC->loadFilesystem(extractArchives); + settings.init("config/settings.json", "vcmi:settings"); + persistentStorage.init("config/persistentStorage.json", ""); + VLC->loadModFilesystem(onlyEssential); + +} + +DLL_LINKAGE void loadDLLClasses(bool onlyEssential) +{ + VLC->init(onlyEssential); +} + +const ArtifactService * LibClasses::artifacts() const +{ + return arth; +} + +const CreatureService * LibClasses::creatures() const +{ + return creh; +} + +const FactionService * LibClasses::factions() const +{ + return townh; +} + +const HeroClassService * LibClasses::heroClasses() const +{ + return &heroh->classes; +} + +const HeroTypeService * LibClasses::heroTypes() const +{ + return heroh; +} + +#if SCRIPTING_ENABLED +const scripting::Service * LibClasses::scripts() const +{ + return scriptHandler; +} +#endif + +const spells::Service * LibClasses::spells() const +{ + return spellh; +} + +const SkillService * LibClasses::skills() const +{ + return skillh; +} + +const IBonusTypeHandler * LibClasses::getBth() const +{ + return bth; +} + +const CIdentifierStorage * LibClasses::identifiers() const +{ + return identifiersHandler; +} + +const spells::effects::Registry * LibClasses::spellEffects() const +{ + return spells::effects::GlobalRegistry::get(); +} + +spells::effects::Registry * LibClasses::spellEffects() +{ + return spells::effects::GlobalRegistry::get(); +} + +const BattleFieldService * LibClasses::battlefields() const +{ + return battlefieldsHandler; +} + +const ObstacleService * LibClasses::obstacles() const +{ + return obstacleHandler; +} + +const IGameSettings * LibClasses::settings() const +{ + return settingsHandler; +} + +void LibClasses::updateEntity(Metatype metatype, int32_t index, const JsonNode & data) +{ + switch(metatype) + { + case Metatype::ARTIFACT: + arth->updateEntity(index, data); + break; + case Metatype::CREATURE: + creh->updateEntity(index, data); + break; + case Metatype::FACTION: + townh->updateEntity(index, data); + break; + case Metatype::HERO_CLASS: + heroh->classes.updateEntity(index, data); + break; + case Metatype::HERO_TYPE: + heroh->updateEntity(index, data); + break; + case Metatype::SKILL: + skillh->updateEntity(index, data); + break; + case Metatype::SPELL: + spellh->updateEntity(index, data); + break; + default: + logGlobal->error("Invalid Metatype id %d", static_cast(metatype)); + break; + } +} + +void LibClasses::loadFilesystem(bool extractArchives) +{ + CStopWatch loadTime; + + CResourceHandler::initialize(); + logGlobal->info("\tInitialization: %d ms", loadTime.getDiff()); + + CResourceHandler::load("config/filesystem.json", extractArchives); + logGlobal->info("\tData loading: %d ms", loadTime.getDiff()); +} + +void LibClasses::loadModFilesystem(bool onlyEssential) +{ + CStopWatch loadTime; + modh = new CModHandler(); + identifiersHandler = new CIdentifierStorage(); + modh->loadMods(onlyEssential); + logGlobal->info("\tMod handler: %d ms", loadTime.getDiff()); + + modh->loadModFilesystems(); + logGlobal->info("\tMod filesystems: %d ms", loadTime.getDiff()); +} + +static void logHandlerLoaded(const std::string & name, CStopWatch & timer) +{ + logGlobal->info("\t\t %s handler: %d ms", name, timer.getDiff()); +} + +template void createHandler(Handler *&handler, const std::string &name, CStopWatch &timer) +{ + handler = new Handler(); + logHandlerLoaded(name, timer); +} + +void LibClasses::init(bool onlyEssential) +{ + CStopWatch pomtime; + CStopWatch totalTime; + + createHandler(settingsHandler, "Game Settings", pomtime); + modh->initializeConfig(); + + createHandler(generaltexth, "General text", pomtime); + createHandler(bth, "Bonus type", pomtime); + createHandler(roadTypeHandler, "Road", pomtime); + createHandler(riverTypeHandler, "River", pomtime); + createHandler(terrainTypeHandler, "Terrain", pomtime); + createHandler(heroh, "Hero", pomtime); + createHandler(arth, "Artifact", pomtime); + createHandler(creh, "Creature", pomtime); + createHandler(townh, "Town", pomtime); + createHandler(objh, "Object", pomtime); + createHandler(objtypeh, "Object types information", pomtime); + createHandler(spellh, "Spell", pomtime); + createHandler(skillh, "Skill", pomtime); + createHandler(terviewh, "Terrain view pattern", pomtime); + createHandler(tplh, "Template", pomtime); //templates need already resolved identifiers (refactor?) +#if SCRIPTING_ENABLED + createHandler(scriptHandler, "Script", pomtime); +#endif + createHandler(battlefieldsHandler, "Battlefields", pomtime); + createHandler(obstacleHandler, "Obstacles", pomtime); + logGlobal->info("\tInitializing handlers: %d ms", totalTime.getDiff()); + + modh->load(); + modh->afterLoad(onlyEssential); +} + +void LibClasses::clear() +{ + delete heroh; + delete arth; + delete creh; + delete townh; + delete objh; + delete objtypeh; + delete spellh; + delete skillh; + delete modh; + delete bth; + delete tplh; + delete terviewh; +#if SCRIPTING_ENABLED + delete scriptHandler; +#endif + delete battlefieldsHandler; + delete generaltexth; + delete identifiersHandler; + makeNull(); +} + +void LibClasses::makeNull() +{ + generaltexth = nullptr; + heroh = nullptr; + arth = nullptr; + creh = nullptr; + townh = nullptr; + objh = nullptr; + objtypeh = nullptr; + spellh = nullptr; + skillh = nullptr; + modh = nullptr; + bth = nullptr; + tplh = nullptr; + terviewh = nullptr; +#if SCRIPTING_ENABLED + scriptHandler = nullptr; +#endif + battlefieldsHandler = nullptr; + identifiersHandler = nullptr; +} + +LibClasses::LibClasses() +{ + //init pointers to handlers + makeNull(); +} + +void LibClasses::callWhenDeserializing() +{ + //FIXME: check if any of these are needed + //generaltexth = new CGeneralTextHandler(); + //generaltexth->load(); + //arth->load(true); + //modh->recreateHandlers(); + //modh->loadConfigFromFile ("defaultMods"); //TODO: remember last saved config +} + +#if SCRIPTING_ENABLED +void LibClasses::scriptsLoaded() +{ + scriptHandler->performRegistration(this); +} +#endif + +LibClasses::~LibClasses() +{ + clear(); +} + +std::shared_ptr LibClasses::getContent() const +{ + return modh->content; +} + +void LibClasses::setContent(std::shared_ptr content) +{ + modh->content = std::move(content); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index 2886eaee5..c6cd971f9 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -1,176 +1,176 @@ -/* - * VCMI_Lib.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 - -VCMI_LIB_NAMESPACE_BEGIN - -class CConsoleHandler; -class CArtHandler; -class CHeroHandler; -class CCreatureHandler; -class CSpellHandler; -class CSkillHandler; -class CBuildingHandler; -class CObjectHandler; -class CObjectClassesHandler; -class CTownHandler; -class CGeneralTextHandler; -class CModHandler; -class CContentHandler; -class BattleFieldHandler; -class IBonusTypeHandler; -class CBonusTypeHandler; -class TerrainTypeHandler; -class RoadTypeHandler; -class RiverTypeHandler; -class ObstacleHandler; -class CTerrainViewPatternConfig; -class CRmgTemplateStorage; -class IHandlerBase; -class IGameSettings; -class GameSettings; -class CIdentifierStorage; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class ScriptHandler; -} -#endif - - -/// Loads and constructs several handlers -class DLL_LINKAGE LibClasses : public Services -{ - CBonusTypeHandler * bth; - - void callWhenDeserializing(); //should be called only by serialize !!! - void makeNull(); //sets all handler pointers to null - std::shared_ptr getContent() const; - void setContent(std::shared_ptr content); - -public: - bool IS_AI_ENABLED = false; //unused? - - const ArtifactService * artifacts() const override; - const CreatureService * creatures() const override; - const FactionService * factions() const override; - const HeroClassService * heroClasses() const override; - const HeroTypeService * heroTypes() const override; -#if SCRIPTING_ENABLED - const scripting::Service * scripts() const override; -#endif - const spells::Service * spells() const override; - const SkillService * skills() const override; - const BattleFieldService * battlefields() const override; - const ObstacleService * obstacles() const override; - const IGameSettings * settings() const override; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; - - const spells::effects::Registry * spellEffects() const override; - spells::effects::Registry * spellEffects() override; - - const IBonusTypeHandler * getBth() const; //deprecated - const CIdentifierStorage * identifiers() const; - - CArtHandler * arth; - CHeroHandler * heroh; - CCreatureHandler * creh; - CSpellHandler * spellh; - CSkillHandler * skillh; - CObjectHandler * objh; - CObjectClassesHandler * objtypeh; - CTownHandler * townh; - CGeneralTextHandler * generaltexth; - CModHandler * modh; - - TerrainTypeHandler * terrainTypeHandler; - RoadTypeHandler * roadTypeHandler; - RiverTypeHandler * riverTypeHandler; - CIdentifierStorage * identifiersHandler; - - CTerrainViewPatternConfig * terviewh; - CRmgTemplateStorage * tplh; - BattleFieldHandler * battlefieldsHandler; - ObstacleHandler * obstacleHandler; - GameSettings * settingsHandler; -#if SCRIPTING_ENABLED - scripting::ScriptHandler * scriptHandler; -#endif - - LibClasses(); //c-tor, loads .lods and NULLs handlers - ~LibClasses(); - void init(bool onlyEssential); //uses standard config file - void clear(); //deletes all handlers and its data - - // basic initialization. should be called before init(). Can also extract original H3 archives - void loadFilesystem(bool extractArchives); - void loadModFilesystem(bool onlyEssential); - -#if SCRIPTING_ENABLED - void scriptsLoaded(); -#endif - - template void serialize(Handler &h, const int version) - { - h & identifiersHandler; // must be first - identifiers registry is used for handlers loading -#if SCRIPTING_ENABLED - h & scriptHandler;//must be first (or second after identifiers), it can modify factories other handlers depends on - if(!h.saving) - { - scriptsLoaded(); - } -#endif - - h & settingsHandler; - h & heroh; - h & arth; - h & creh; - h & townh; - h & objh; - h & objtypeh; - h & spellh; - h & skillh; - h & battlefieldsHandler; - h & obstacleHandler; - h & roadTypeHandler; - h & riverTypeHandler; - h & terrainTypeHandler; - - if(!h.saving) - { - //modh will be changed and modh->content will be empty after deserialization - auto content = getContent(); - h & modh; - setContent(content); - } - else - h & modh; - - h & IS_AI_ENABLED; - h & bth; - - if(!h.saving) - { - callWhenDeserializing(); - } - } -}; - -extern DLL_LINKAGE LibClasses * VLC; - -DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); -DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); - - -VCMI_LIB_NAMESPACE_END +/* + * VCMI_Lib.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class CConsoleHandler; +class CArtHandler; +class CHeroHandler; +class CCreatureHandler; +class CSpellHandler; +class CSkillHandler; +class CBuildingHandler; +class CObjectHandler; +class CObjectClassesHandler; +class CTownHandler; +class CGeneralTextHandler; +class CModHandler; +class CContentHandler; +class BattleFieldHandler; +class IBonusTypeHandler; +class CBonusTypeHandler; +class TerrainTypeHandler; +class RoadTypeHandler; +class RiverTypeHandler; +class ObstacleHandler; +class CTerrainViewPatternConfig; +class CRmgTemplateStorage; +class IHandlerBase; +class IGameSettings; +class GameSettings; +class CIdentifierStorage; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class ScriptHandler; +} +#endif + + +/// Loads and constructs several handlers +class DLL_LINKAGE LibClasses : public Services +{ + CBonusTypeHandler * bth; + + void callWhenDeserializing(); //should be called only by serialize !!! + void makeNull(); //sets all handler pointers to null + std::shared_ptr getContent() const; + void setContent(std::shared_ptr content); + +public: + bool IS_AI_ENABLED = false; //unused? + + const ArtifactService * artifacts() const override; + const CreatureService * creatures() const override; + const FactionService * factions() const override; + const HeroClassService * heroClasses() const override; + const HeroTypeService * heroTypes() const override; +#if SCRIPTING_ENABLED + const scripting::Service * scripts() const override; +#endif + const spells::Service * spells() const override; + const SkillService * skills() const override; + const BattleFieldService * battlefields() const override; + const ObstacleService * obstacles() const override; + const IGameSettings * settings() const override; + + void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + + const spells::effects::Registry * spellEffects() const override; + spells::effects::Registry * spellEffects() override; + + const IBonusTypeHandler * getBth() const; //deprecated + const CIdentifierStorage * identifiers() const; + + CArtHandler * arth; + CHeroHandler * heroh; + CCreatureHandler * creh; + CSpellHandler * spellh; + CSkillHandler * skillh; + CObjectHandler * objh; + CObjectClassesHandler * objtypeh; + CTownHandler * townh; + CGeneralTextHandler * generaltexth; + CModHandler * modh; + + TerrainTypeHandler * terrainTypeHandler; + RoadTypeHandler * roadTypeHandler; + RiverTypeHandler * riverTypeHandler; + CIdentifierStorage * identifiersHandler; + + CTerrainViewPatternConfig * terviewh; + CRmgTemplateStorage * tplh; + BattleFieldHandler * battlefieldsHandler; + ObstacleHandler * obstacleHandler; + GameSettings * settingsHandler; +#if SCRIPTING_ENABLED + scripting::ScriptHandler * scriptHandler; +#endif + + LibClasses(); //c-tor, loads .lods and NULLs handlers + ~LibClasses(); + void init(bool onlyEssential); //uses standard config file + void clear(); //deletes all handlers and its data + + // basic initialization. should be called before init(). Can also extract original H3 archives + void loadFilesystem(bool extractArchives); + void loadModFilesystem(bool onlyEssential); + +#if SCRIPTING_ENABLED + void scriptsLoaded(); +#endif + + template void serialize(Handler &h, const int version) + { + h & identifiersHandler; // must be first - identifiers registry is used for handlers loading +#if SCRIPTING_ENABLED + h & scriptHandler;//must be first (or second after identifiers), it can modify factories other handlers depends on + if(!h.saving) + { + scriptsLoaded(); + } +#endif + + h & settingsHandler; + h & heroh; + h & arth; + h & creh; + h & townh; + h & objh; + h & objtypeh; + h & spellh; + h & skillh; + h & battlefieldsHandler; + h & obstacleHandler; + h & roadTypeHandler; + h & riverTypeHandler; + h & terrainTypeHandler; + + if(!h.saving) + { + //modh will be changed and modh->content will be empty after deserialization + auto content = getContent(); + h & modh; + setContent(content); + } + else + h & modh; + + h & IS_AI_ENABLED; + h & bth; + + if(!h.saving) + { + callWhenDeserializing(); + } + } +}; + +extern DLL_LINKAGE LibClasses * VLC; + +DLL_LINKAGE void preinitDLL(CConsoleHandler * Console, bool onlyEssential = false, bool extractArchives = false); +DLL_LINKAGE void loadDLLClasses(bool onlyEssential = false); + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/AccessibilityInfo.cpp b/lib/battle/AccessibilityInfo.cpp index 8d5f8067d..a7a29dae5 100644 --- a/lib/battle/AccessibilityInfo.cpp +++ b/lib/battle/AccessibilityInfo.cpp @@ -1,53 +1,53 @@ -/* - * AccessibilityInfo.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 "AccessibilityInfo.h" -#include "BattleHex.h" -#include "Unit.h" -#include "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, ui8 side) const -{ - //at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) - if(at(tile) != EAccessibility::ACCESSIBLE) - if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER) - return false; - return true; -} - -bool AccessibilityInfo::accessible(BattleHex tile, const battle::Unit * stack) const -{ - return accessible(tile, stack->doubleWide(), stack->unitSide()); -} - -bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, ui8 side) const -{ - // All hexes that stack would cover if standing on tile have to be accessible. - //do not use getHexes for speed reasons - if(!tile.isValid()) - return false; - if(!tileAccessibleWithGate(tile, side)) - return false; - - if(doubleWide) - { - auto otherHex = battle::Unit::occupiedHex(tile, doubleWide, side); - if(!otherHex.isValid()) - return false; - if(!tileAccessibleWithGate(otherHex, side)) - return false; - } - - return true; -} - -VCMI_LIB_NAMESPACE_END +/* + * AccessibilityInfo.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 "AccessibilityInfo.h" +#include "BattleHex.h" +#include "Unit.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, ui8 side) const +{ + //at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER) + if(at(tile) != EAccessibility::ACCESSIBLE) + if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER) + return false; + return true; +} + +bool AccessibilityInfo::accessible(BattleHex tile, const battle::Unit * stack) const +{ + return accessible(tile, stack->doubleWide(), stack->unitSide()); +} + +bool AccessibilityInfo::accessible(BattleHex tile, bool doubleWide, ui8 side) const +{ + // All hexes that stack would cover if standing on tile have to be accessible. + //do not use getHexes for speed reasons + if(!tile.isValid()) + return false; + if(!tileAccessibleWithGate(tile, side)) + return false; + + if(doubleWide) + { + auto otherHex = battle::Unit::occupiedHex(tile, doubleWide, side); + if(!otherHex.isValid()) + return false; + if(!tileAccessibleWithGate(otherHex, side)) + return false; + } + + return true; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/AccessibilityInfo.h b/lib/battle/AccessibilityInfo.h index c84c1346d..dc3648412 100644 --- a/lib/battle/AccessibilityInfo.h +++ b/lib/battle/AccessibilityInfo.h @@ -1,45 +1,45 @@ -/* - * AccessibilityInfo.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 "BattleHex.h" -#include "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace battle -{ - class Unit; -} - -//Accessibility is property of hex in battle. It doesn't depend on stack, side's perspective and so on. -enum class EAccessibility -{ - ACCESSIBLE, - ALIVE_STACK, - OBSTACLE, - DESTRUCTIBLE_WALL, - GATE, //sieges -> gate opens only for defender stacks - UNAVAILABLE, //indestructible wall parts, special battlefields (like boat-to-boat) - SIDE_COLUMN //used for first and last columns of hexes that are unavailable but wat machines can stand there -}; - - -using TAccessibilityArray = std::array; - -struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray -{ - public: - bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide - bool accessible(BattleHex tile, bool doubleWide, ui8 side) const; //checks for both tiles if stack is double wide - private: - bool tileAccessibleWithGate(BattleHex tile, ui8 side) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * AccessibilityInfo.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 "BattleHex.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace battle +{ + class Unit; +} + +//Accessibility is property of hex in battle. It doesn't depend on stack, side's perspective and so on. +enum class EAccessibility +{ + ACCESSIBLE, + ALIVE_STACK, + OBSTACLE, + DESTRUCTIBLE_WALL, + GATE, //sieges -> gate opens only for defender stacks + UNAVAILABLE, //indestructible wall parts, special battlefields (like boat-to-boat) + SIDE_COLUMN //used for first and last columns of hexes that are unavailable but wat machines can stand there +}; + + +using TAccessibilityArray = std::array; + +struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray +{ + public: + bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide + bool accessible(BattleHex tile, bool doubleWide, ui8 side) const; //checks for both tiles if stack is double wide + private: + bool tileAccessibleWithGate(BattleHex tile, ui8 side) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.cpp b/lib/battle/BattleAction.cpp index 0e82ca884..e021cc1ba 100644 --- a/lib/battle/BattleAction.cpp +++ b/lib/battle/BattleAction.cpp @@ -1,244 +1,244 @@ -/* - * BattleAction.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 "BattleAction.h" -#include "Unit.h" -#include "CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static const int32_t INVALID_UNIT_ID = -1000; - -BattleAction::BattleAction(): - side(-1), - stackNumber(-1), - actionType(EActionType::NO_ACTION) -{ -} - -BattleAction BattleAction::makeHeal(const battle::Unit * healer, const battle::Unit * healed) -{ - BattleAction ba; - ba.side = healer->unitSide(); - ba.actionType = EActionType::STACK_HEAL; - ba.stackNumber = healer->unitId(); - ba.aimToUnit(healed); - return ba; -} - -BattleAction BattleAction::makeDefend(const battle::Unit * stack) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::DEFEND; - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack) -{ - BattleAction ba; - ba.side = stack->unitSide(); //FIXME: will it fail if stack mind controlled? - ba.actionType = EActionType::WALK_AND_ATTACK; - ba.stackNumber = stack->unitId(); - ba.aimToHex(attackFrom); - ba.aimToHex(destination); - if(returnAfterAttack && stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) - ba.aimToHex(stack->getPosition()); - return ba; -} - -BattleAction BattleAction::makeWait(const battle::Unit * stack) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::WAIT; - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeShotAttack(const battle::Unit * shooter, const battle::Unit * target) -{ - BattleAction ba; - ba.side = shooter->unitSide(); - ba.actionType = EActionType::SHOOT; - ba.stackNumber = shooter->unitId(); - ba.aimToUnit(target); - return ba; -} - -BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID) -{ - BattleAction ba; - ba.actionType = EActionType::MONSTER_SPELL; - ba.spell = spellID; - ba.setTarget(target); - ba.side = stack->unitSide(); - ba.stackNumber = stack->unitId(); - return ba; -} - -BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest) -{ - BattleAction ba; - ba.side = stack->unitSide(); - ba.actionType = EActionType::WALK; - ba.stackNumber = stack->unitId(); - ba.aimToHex(dest); - return ba; -} - -BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::END_TACTIC_PHASE; - return ba; -} - -BattleAction BattleAction::makeSurrender(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::SURRENDER; - return ba; -} - -BattleAction BattleAction::makeRetreat(ui8 side) -{ - BattleAction ba; - ba.side = side; - ba.actionType = EActionType::RETREAT; - return ba; -} - -std::string BattleAction::toString() const -{ - std::stringstream targetStream; - - for(const DestinationInfo & info : target) - { - if(info.unitValue == INVALID_UNIT_ID) - { - targetStream << info.hexValue; - } - else - { - targetStream << info.unitValue; - targetStream << "@"; - targetStream << info.hexValue; - } - targetStream << ","; - } - - boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); - fmt % static_cast(side) % stackNumber % static_cast(actionType) % spell.getNum() % targetStream.str(); - return fmt.str(); -} - -void BattleAction::aimToHex(const BattleHex & destination) -{ - DestinationInfo info; - info.hexValue = destination; - info.unitValue = INVALID_UNIT_ID; - - target.push_back(info); -} - -void BattleAction::aimToUnit(const battle::Unit * destination) -{ - DestinationInfo info; - info.hexValue = destination->getPosition(); - info.unitValue = destination->unitId(); - - target.push_back(info); -} - -battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const -{ - battle::Target ret; - - for(const auto & destination : target) - { - if(destination.unitValue == INVALID_UNIT_ID) - ret.emplace_back(destination.hexValue); - else - ret.emplace_back(cb->battleGetUnitByID(destination.unitValue)); - } - - return ret; -} - -void BattleAction::setTarget(const battle::Target & target_) -{ - target.clear(); - for(const auto & destination : target_) - { - if(destination.unitValue == nullptr) - aimToHex(destination.hexValue); - else - aimToUnit(destination.unitValue); - } -} - -bool BattleAction::isUnitAction() const -{ - static const std::array actions = { - EActionType::NO_ACTION, - EActionType::WALK, - EActionType::WAIT, - EActionType::DEFEND, - EActionType::WALK_AND_ATTACK, - EActionType::SHOOT, - EActionType::CATAPULT, - EActionType::MONSTER_SPELL, - EActionType::BAD_MORALE, - EActionType::STACK_HEAL - }; - return vstd::contains(actions, actionType); -} - -bool BattleAction::isSpellAction() const -{ - static const std::array actions = { - EActionType::HERO_SPELL, - EActionType::MONSTER_SPELL - }; - return vstd::contains(actions, actionType); -} - -bool BattleAction::isBattleEndAction() const -{ - static const std::array actions = { - EActionType::RETREAT, - EActionType::SURRENDER - }; - return vstd::contains(actions, actionType); -} - -bool BattleAction::isTacticsAction() const -{ - static const std::array actions = { - EActionType::WALK, - EActionType::END_TACTIC_PHASE, - EActionType::RETREAT, - EActionType::SURRENDER - }; - return vstd::contains(actions, actionType); -} - -std::ostream & operator<<(std::ostream & os, const BattleAction & ba) -{ - os << ba.toString(); - return os; -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleAction.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 "BattleAction.h" +#include "Unit.h" +#include "CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static const int32_t INVALID_UNIT_ID = -1000; + +BattleAction::BattleAction(): + side(-1), + stackNumber(-1), + actionType(EActionType::NO_ACTION) +{ +} + +BattleAction BattleAction::makeHeal(const battle::Unit * healer, const battle::Unit * healed) +{ + BattleAction ba; + ba.side = healer->unitSide(); + ba.actionType = EActionType::STACK_HEAL; + ba.stackNumber = healer->unitId(); + ba.aimToUnit(healed); + return ba; +} + +BattleAction BattleAction::makeDefend(const battle::Unit * stack) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::DEFEND; + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack) +{ + BattleAction ba; + ba.side = stack->unitSide(); //FIXME: will it fail if stack mind controlled? + ba.actionType = EActionType::WALK_AND_ATTACK; + ba.stackNumber = stack->unitId(); + ba.aimToHex(attackFrom); + ba.aimToHex(destination); + if(returnAfterAttack && stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) + ba.aimToHex(stack->getPosition()); + return ba; +} + +BattleAction BattleAction::makeWait(const battle::Unit * stack) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::WAIT; + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeShotAttack(const battle::Unit * shooter, const battle::Unit * target) +{ + BattleAction ba; + ba.side = shooter->unitSide(); + ba.actionType = EActionType::SHOOT; + ba.stackNumber = shooter->unitId(); + ba.aimToUnit(target); + return ba; +} + +BattleAction BattleAction::makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID) +{ + BattleAction ba; + ba.actionType = EActionType::MONSTER_SPELL; + ba.spell = spellID; + ba.setTarget(target); + ba.side = stack->unitSide(); + ba.stackNumber = stack->unitId(); + return ba; +} + +BattleAction BattleAction::makeMove(const battle::Unit * stack, BattleHex dest) +{ + BattleAction ba; + ba.side = stack->unitSide(); + ba.actionType = EActionType::WALK; + ba.stackNumber = stack->unitId(); + ba.aimToHex(dest); + return ba; +} + +BattleAction BattleAction::makeEndOFTacticPhase(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::END_TACTIC_PHASE; + return ba; +} + +BattleAction BattleAction::makeSurrender(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::SURRENDER; + return ba; +} + +BattleAction BattleAction::makeRetreat(ui8 side) +{ + BattleAction ba; + ba.side = side; + ba.actionType = EActionType::RETREAT; + return ba; +} + +std::string BattleAction::toString() const +{ + std::stringstream targetStream; + + for(const DestinationInfo & info : target) + { + if(info.unitValue == INVALID_UNIT_ID) + { + targetStream << info.hexValue; + } + else + { + targetStream << info.unitValue; + targetStream << "@"; + targetStream << info.hexValue; + } + targetStream << ","; + } + + boost::format fmt("{BattleAction: side '%d', stackNumber '%d', actionType '%s', actionSubtype '%d', target {%s}}"); + fmt % static_cast(side) % stackNumber % static_cast(actionType) % spell.getNum() % targetStream.str(); + return fmt.str(); +} + +void BattleAction::aimToHex(const BattleHex & destination) +{ + DestinationInfo info; + info.hexValue = destination; + info.unitValue = INVALID_UNIT_ID; + + target.push_back(info); +} + +void BattleAction::aimToUnit(const battle::Unit * destination) +{ + DestinationInfo info; + info.hexValue = destination->getPosition(); + info.unitValue = destination->unitId(); + + target.push_back(info); +} + +battle::Target BattleAction::getTarget(const CBattleInfoCallback * cb) const +{ + battle::Target ret; + + for(const auto & destination : target) + { + if(destination.unitValue == INVALID_UNIT_ID) + ret.emplace_back(destination.hexValue); + else + ret.emplace_back(cb->battleGetUnitByID(destination.unitValue)); + } + + return ret; +} + +void BattleAction::setTarget(const battle::Target & target_) +{ + target.clear(); + for(const auto & destination : target_) + { + if(destination.unitValue == nullptr) + aimToHex(destination.hexValue); + else + aimToUnit(destination.unitValue); + } +} + +bool BattleAction::isUnitAction() const +{ + static const std::array actions = { + EActionType::NO_ACTION, + EActionType::WALK, + EActionType::WAIT, + EActionType::DEFEND, + EActionType::WALK_AND_ATTACK, + EActionType::SHOOT, + EActionType::CATAPULT, + EActionType::MONSTER_SPELL, + EActionType::BAD_MORALE, + EActionType::STACK_HEAL + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isSpellAction() const +{ + static const std::array actions = { + EActionType::HERO_SPELL, + EActionType::MONSTER_SPELL + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isBattleEndAction() const +{ + static const std::array actions = { + EActionType::RETREAT, + EActionType::SURRENDER + }; + return vstd::contains(actions, actionType); +} + +bool BattleAction::isTacticsAction() const +{ + static const std::array actions = { + EActionType::WALK, + EActionType::END_TACTIC_PHASE, + EActionType::RETREAT, + EActionType::SURRENDER + }; + return vstd::contains(actions, actionType); +} + +std::ostream & operator<<(std::ostream & os, const BattleAction & ba) +{ + os << ba.toString(); + return os; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAction.h b/lib/battle/BattleAction.h index 593b6290f..8fc5d0bfc 100644 --- a/lib/battle/BattleAction.h +++ b/lib/battle/BattleAction.h @@ -1,85 +1,85 @@ -/* - * BattleAction.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 "Destination.h" -#include "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBattleInfoCallback; - -namespace battle -{ - class Unit; -} - -/// A struct which handles battle actions like defending, walking,... - represents a creature stack in a battle -class DLL_LINKAGE BattleAction -{ -public: - ui8 side; //who made this action - ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, - EActionType actionType; //use ActionType enum for values - - SpellID spell; - - BattleAction(); - - static BattleAction makeHeal(const battle::Unit * healer, const battle::Unit * healed); - static BattleAction makeDefend(const battle::Unit * stack); - static BattleAction makeWait(const battle::Unit * stack); - static BattleAction makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack = true); - static BattleAction makeShotAttack(const battle::Unit * shooter, const battle::Unit * target); - static BattleAction makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID); - static BattleAction makeMove(const battle::Unit * stack, BattleHex dest); - static BattleAction makeEndOFTacticPhase(ui8 side); - static BattleAction makeRetreat(ui8 side); - static BattleAction makeSurrender(ui8 side); - - bool isTacticsAction() const; - bool isUnitAction() const; - bool isSpellAction() const; - bool isBattleEndAction() const; - std::string toString() const; - - void aimToHex(const BattleHex & destination); - void aimToUnit(const battle::Unit * destination); - - battle::Target getTarget(const CBattleInfoCallback * cb) const; - void setTarget(const battle::Target & target_); - - template void serialize(Handler & h, const int version) - { - h & side; - h & stackNumber; - h & actionType; - h & spell; - h & target; - } -private: - - struct DestinationInfo - { - int32_t unitValue; - BattleHex hexValue; - - template void serialize(Handler & h, const int version) - { - h & unitValue; - h & hexValue; - } - }; - - std::vector target; -}; - -DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleAction & ba); //todo: remove - -VCMI_LIB_NAMESPACE_END +/* + * BattleAction.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 "Destination.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBattleInfoCallback; + +namespace battle +{ + class Unit; +} + +/// A struct which handles battle actions like defending, walking,... - represents a creature stack in a battle +class DLL_LINKAGE BattleAction +{ +public: + ui8 side; //who made this action + ui32 stackNumber; //stack ID, -1 left hero, -2 right hero, + EActionType actionType; //use ActionType enum for values + + SpellID spell; + + BattleAction(); + + static BattleAction makeHeal(const battle::Unit * healer, const battle::Unit * healed); + static BattleAction makeDefend(const battle::Unit * stack); + static BattleAction makeWait(const battle::Unit * stack); + static BattleAction makeMeleeAttack(const battle::Unit * stack, BattleHex destination, BattleHex attackFrom, bool returnAfterAttack = true); + static BattleAction makeShotAttack(const battle::Unit * shooter, const battle::Unit * target); + static BattleAction makeCreatureSpellcast(const battle::Unit * stack, const battle::Target & target, const SpellID & spellID); + static BattleAction makeMove(const battle::Unit * stack, BattleHex dest); + static BattleAction makeEndOFTacticPhase(ui8 side); + static BattleAction makeRetreat(ui8 side); + static BattleAction makeSurrender(ui8 side); + + bool isTacticsAction() const; + bool isUnitAction() const; + bool isSpellAction() const; + bool isBattleEndAction() const; + std::string toString() const; + + void aimToHex(const BattleHex & destination); + void aimToUnit(const battle::Unit * destination); + + battle::Target getTarget(const CBattleInfoCallback * cb) const; + void setTarget(const battle::Target & target_); + + template void serialize(Handler & h, const int version) + { + h & side; + h & stackNumber; + h & actionType; + h & spell; + h & target; + } +private: + + struct DestinationInfo + { + int32_t unitValue; + BattleHex hexValue; + + template void serialize(Handler & h, const int version) + { + h & unitValue; + h & hexValue; + } + }; + + std::vector target; +}; + +DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleAction & ba); //todo: remove + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAttackInfo.cpp b/lib/battle/BattleAttackInfo.cpp index 2746dc7cd..1e887b53d 100644 --- a/lib/battle/BattleAttackInfo.cpp +++ b/lib/battle/BattleAttackInfo.cpp @@ -1,34 +1,34 @@ -/* - * BattleAttackInfo.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 "BattleAttackInfo.h" -#include "CUnitState.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BattleAttackInfo::BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting) - : attacker(Attacker), - defender(Defender), - shooting(Shooting), - attackerPos(BattleHex::INVALID), - defenderPos(BattleHex::INVALID), - chargeDistance(chargeDistance) -{} - -BattleAttackInfo BattleAttackInfo::reverse() const -{ - BattleAttackInfo ret(defender, attacker, 0, false); - - ret.defenderPos = attackerPos; - ret.attackerPos = defenderPos; - return ret; -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleAttackInfo.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 "BattleAttackInfo.h" +#include "CUnitState.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleAttackInfo::BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting) + : attacker(Attacker), + defender(Defender), + shooting(Shooting), + attackerPos(BattleHex::INVALID), + defenderPos(BattleHex::INVALID), + chargeDistance(chargeDistance) +{} + +BattleAttackInfo BattleAttackInfo::reverse() const +{ + BattleAttackInfo ret(defender, attacker, 0, false); + + ret.defenderPos = attackerPos; + ret.attackerPos = defenderPos; + return ret; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleAttackInfo.h b/lib/battle/BattleAttackInfo.h index d221581fb..eef4f40c3 100644 --- a/lib/battle/BattleAttackInfo.h +++ b/lib/battle/BattleAttackInfo.h @@ -1,41 +1,41 @@ -/* - * BattleAttackInfo.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 "BattleHex.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace battle -{ - class Unit; - class CUnitState; -} - -struct DLL_LINKAGE BattleAttackInfo -{ - const battle::Unit * attacker; - const battle::Unit * defender; - - BattleHex attackerPos; - BattleHex defenderPos; - - int chargeDistance = 0; - bool shooting = false; - bool luckyStrike = false; - bool unluckyStrike = false; - bool deathBlow = false; - bool doubleDamage = false; - - BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting); - BattleAttackInfo reverse() const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * BattleAttackInfo.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 "BattleHex.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace battle +{ + class Unit; + class CUnitState; +} + +struct DLL_LINKAGE BattleAttackInfo +{ + const battle::Unit * attacker; + const battle::Unit * defender; + + BattleHex attackerPos; + BattleHex defenderPos; + + int chargeDistance = 0; + bool shooting = false; + bool luckyStrike = false; + bool unluckyStrike = false; + bool deathBlow = false; + bool doubleDamage = false; + + BattleAttackInfo(const battle::Unit * Attacker, const battle::Unit * Defender, int chargeDistance, bool Shooting); + BattleAttackInfo reverse() const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleHex.cpp b/lib/battle/BattleHex.cpp index 4c934f79c..f6eaf319f 100644 --- a/lib/battle/BattleHex.cpp +++ b/lib/battle/BattleHex.cpp @@ -1,245 +1,245 @@ -/* - * BattleHex.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 "BattleHex.h" - -VCMI_LIB_NAMESPACE_BEGIN - -BattleHex::BattleHex() : hex(INVALID) {} - -BattleHex::BattleHex(si16 _hex) : hex(_hex) {} - -BattleHex::BattleHex(si16 x, si16 y) -{ - setXY(x, y); -} - -BattleHex::BattleHex(std::pair xy) -{ - setXY(xy); -} - -BattleHex::operator si16() const -{ - return hex; -} - -bool BattleHex::isValid() const -{ - return hex >= 0 && hex < GameConstants::BFIELD_SIZE; -} - -bool BattleHex::isAvailable() const -{ - return isValid() && getX() > 0 && getX() < GameConstants::BFIELD_WIDTH-1; -} - -void BattleHex::setX(si16 x) -{ - setXY(x, getY()); -} - -void BattleHex::setY(si16 y) -{ - setXY(getX(), y); -} - -void BattleHex::setXY(si16 x, si16 y, bool hasToBeValid) -{ - if(hasToBeValid) - { - if(x < 0 || x >= GameConstants::BFIELD_WIDTH || y < 0 || y >= GameConstants::BFIELD_HEIGHT) - throw std::runtime_error("Valid hex required"); - } - - hex = x + y * GameConstants::BFIELD_WIDTH; -} - -void BattleHex::setXY(std::pair xy) -{ - setXY(xy.first, xy.second); -} - -si16 BattleHex::getX() const -{ - return hex % GameConstants::BFIELD_WIDTH; -} - -si16 BattleHex::getY() const -{ - return hex / GameConstants::BFIELD_WIDTH; -} - -std::pair BattleHex::getXY() const -{ - return std::make_pair(getX(), getY()); -} - -BattleHex& BattleHex::moveInDirection(EDir dir, bool hasToBeValid) -{ - si16 x = getX(); - si16 y = getY(); - switch(dir) - { - case TOP_LEFT: - setXY((y%2) ? x-1 : x, y-1, hasToBeValid); - break; - case TOP_RIGHT: - setXY((y%2) ? x : x+1, y-1, hasToBeValid); - break; - case RIGHT: - setXY(x+1, y, hasToBeValid); - break; - case BOTTOM_RIGHT: - setXY((y%2) ? x : x+1, y+1, hasToBeValid); - break; - case BOTTOM_LEFT: - setXY((y%2) ? x-1 : x, y+1, hasToBeValid); - break; - case LEFT: - setXY(x-1, y, hasToBeValid); - break; - case NONE: - break; - default: - throw std::runtime_error("Disaster: wrong direction in BattleHex::operator+=!\n"); - break; - } - return *this; -} - -BattleHex &BattleHex::operator+=(BattleHex::EDir dir) -{ - return moveInDirection(dir); -} - -BattleHex BattleHex::cloneInDirection(BattleHex::EDir dir, bool hasToBeValid) const -{ - BattleHex result(hex); - result.moveInDirection(dir, hasToBeValid); - return result; -} - -BattleHex BattleHex::operator+(BattleHex::EDir dir) const -{ - return cloneInDirection(dir); -} - -std::vector BattleHex::neighbouringTiles() const -{ - std::vector ret; - ret.reserve(6); - for(auto dir : hexagonalDirections()) - checkAndPush(cloneInDirection(dir, false), ret); - return ret; -} - -std::vector BattleHex::allNeighbouringTiles() const -{ - std::vector ret; - ret.resize(6); - - for(auto dir : hexagonalDirections()) - ret[dir] = cloneInDirection(dir, false); - - return ret; -} - -BattleHex::EDir BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) -{ - for(auto dir : hexagonalDirections()) - if(hex2 == hex1.cloneInDirection(dir, false)) - return dir; - return NONE; -} - -uint8_t BattleHex::getDistance(BattleHex hex1, BattleHex hex2) -{ - int y1 = hex1.getY(); - int y2 = hex2.getY(); - - // FIXME: why there was * 0.5 instead of / 2? - int x1 = static_cast(hex1.getX() + y1 / 2); - int x2 = static_cast(hex2.getX() + y2 / 2); - - int xDst = x2 - x1; - int yDst = y2 - y1; - - if ((xDst >= 0 && yDst >= 0) || (xDst < 0 && yDst < 0)) - return std::max(std::abs(xDst), std::abs(yDst)); - - return std::abs(xDst) + std::abs(yDst); -} - -void BattleHex::checkAndPush(BattleHex tile, std::vector & ret) -{ - if(tile.isAvailable()) - ret.push_back(tile); -} - -BattleHex BattleHex::getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities) -{ - std::vector sortedTiles (possibilities.begin(), possibilities.end()); //set can't be sorted properly :( - BattleHex initialHex = BattleHex(initialPos); - auto compareDistance = [initialHex](const BattleHex left, const BattleHex right) -> bool - { - return initialHex.getDistance (initialHex, left) < initialHex.getDistance (initialHex, right); - }; - boost::sort (sortedTiles, compareDistance); //closest tiles at front - int closestDistance = initialHex.getDistance(initialPos, sortedTiles.front()); //sometimes closest tiles can be many hexes away - auto notClosest = [closestDistance, initialPos](const BattleHex here) -> bool - { - return closestDistance < here.getDistance (initialPos, here); - }; - vstd::erase_if(sortedTiles, notClosest); //only closest tiles are interesting - auto compareHorizontal = [side, initialPos](const BattleHex left, const BattleHex right) -> bool - { - if(left.getX() != right.getX()) - { - if(side == BattleSide::ATTACKER) - return left.getX() > right.getX(); //find furthest right - else - return left.getX() < right.getX(); //find furthest left - } - else - { - //Prefer tiles in the same row. - return std::abs(left.getY() - initialPos.getY()) < std::abs(right.getY() - initialPos.getY()); - } - }; - boost::sort (sortedTiles, compareHorizontal); - return sortedTiles.front(); -} - -std::ostream & operator<<(std::ostream & os, const BattleHex & hex) -{ - return os << boost::str(boost::format("{BattleHex: x '%d', y '%d', hex '%d'}") % hex.getX() % hex.getY() % hex.hex); -} - -static BattleHex::NeighbouringTilesCache calculateNeighbouringTiles() -{ - BattleHex::NeighbouringTilesCache ret; - ret.resize(GameConstants::BFIELD_SIZE); - - for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) - { - auto hexes = BattleHex(hex).neighbouringTiles(); - - size_t index = 0; - for(auto neighbour : hexes) - ret[hex].at(index++) = neighbour; - } - - return ret; -} - -const BattleHex::NeighbouringTilesCache BattleHex::neighbouringTilesCache = calculateNeighbouringTiles(); - -VCMI_LIB_NAMESPACE_END +/* + * BattleHex.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 "BattleHex.h" + +VCMI_LIB_NAMESPACE_BEGIN + +BattleHex::BattleHex() : hex(INVALID) {} + +BattleHex::BattleHex(si16 _hex) : hex(_hex) {} + +BattleHex::BattleHex(si16 x, si16 y) +{ + setXY(x, y); +} + +BattleHex::BattleHex(std::pair xy) +{ + setXY(xy); +} + +BattleHex::operator si16() const +{ + return hex; +} + +bool BattleHex::isValid() const +{ + return hex >= 0 && hex < GameConstants::BFIELD_SIZE; +} + +bool BattleHex::isAvailable() const +{ + return isValid() && getX() > 0 && getX() < GameConstants::BFIELD_WIDTH-1; +} + +void BattleHex::setX(si16 x) +{ + setXY(x, getY()); +} + +void BattleHex::setY(si16 y) +{ + setXY(getX(), y); +} + +void BattleHex::setXY(si16 x, si16 y, bool hasToBeValid) +{ + if(hasToBeValid) + { + if(x < 0 || x >= GameConstants::BFIELD_WIDTH || y < 0 || y >= GameConstants::BFIELD_HEIGHT) + throw std::runtime_error("Valid hex required"); + } + + hex = x + y * GameConstants::BFIELD_WIDTH; +} + +void BattleHex::setXY(std::pair xy) +{ + setXY(xy.first, xy.second); +} + +si16 BattleHex::getX() const +{ + return hex % GameConstants::BFIELD_WIDTH; +} + +si16 BattleHex::getY() const +{ + return hex / GameConstants::BFIELD_WIDTH; +} + +std::pair BattleHex::getXY() const +{ + return std::make_pair(getX(), getY()); +} + +BattleHex& BattleHex::moveInDirection(EDir dir, bool hasToBeValid) +{ + si16 x = getX(); + si16 y = getY(); + switch(dir) + { + case TOP_LEFT: + setXY((y%2) ? x-1 : x, y-1, hasToBeValid); + break; + case TOP_RIGHT: + setXY((y%2) ? x : x+1, y-1, hasToBeValid); + break; + case RIGHT: + setXY(x+1, y, hasToBeValid); + break; + case BOTTOM_RIGHT: + setXY((y%2) ? x : x+1, y+1, hasToBeValid); + break; + case BOTTOM_LEFT: + setXY((y%2) ? x-1 : x, y+1, hasToBeValid); + break; + case LEFT: + setXY(x-1, y, hasToBeValid); + break; + case NONE: + break; + default: + throw std::runtime_error("Disaster: wrong direction in BattleHex::operator+=!\n"); + break; + } + return *this; +} + +BattleHex &BattleHex::operator+=(BattleHex::EDir dir) +{ + return moveInDirection(dir); +} + +BattleHex BattleHex::cloneInDirection(BattleHex::EDir dir, bool hasToBeValid) const +{ + BattleHex result(hex); + result.moveInDirection(dir, hasToBeValid); + return result; +} + +BattleHex BattleHex::operator+(BattleHex::EDir dir) const +{ + return cloneInDirection(dir); +} + +std::vector BattleHex::neighbouringTiles() const +{ + std::vector ret; + ret.reserve(6); + for(auto dir : hexagonalDirections()) + checkAndPush(cloneInDirection(dir, false), ret); + return ret; +} + +std::vector BattleHex::allNeighbouringTiles() const +{ + std::vector ret; + ret.resize(6); + + for(auto dir : hexagonalDirections()) + ret[dir] = cloneInDirection(dir, false); + + return ret; +} + +BattleHex::EDir BattleHex::mutualPosition(BattleHex hex1, BattleHex hex2) +{ + for(auto dir : hexagonalDirections()) + if(hex2 == hex1.cloneInDirection(dir, false)) + return dir; + return NONE; +} + +uint8_t BattleHex::getDistance(BattleHex hex1, BattleHex hex2) +{ + int y1 = hex1.getY(); + int y2 = hex2.getY(); + + // FIXME: why there was * 0.5 instead of / 2? + int x1 = static_cast(hex1.getX() + y1 / 2); + int x2 = static_cast(hex2.getX() + y2 / 2); + + int xDst = x2 - x1; + int yDst = y2 - y1; + + if ((xDst >= 0 && yDst >= 0) || (xDst < 0 && yDst < 0)) + return std::max(std::abs(xDst), std::abs(yDst)); + + return std::abs(xDst) + std::abs(yDst); +} + +void BattleHex::checkAndPush(BattleHex tile, std::vector & ret) +{ + if(tile.isAvailable()) + ret.push_back(tile); +} + +BattleHex BattleHex::getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities) +{ + std::vector sortedTiles (possibilities.begin(), possibilities.end()); //set can't be sorted properly :( + BattleHex initialHex = BattleHex(initialPos); + auto compareDistance = [initialHex](const BattleHex left, const BattleHex right) -> bool + { + return initialHex.getDistance (initialHex, left) < initialHex.getDistance (initialHex, right); + }; + boost::sort (sortedTiles, compareDistance); //closest tiles at front + int closestDistance = initialHex.getDistance(initialPos, sortedTiles.front()); //sometimes closest tiles can be many hexes away + auto notClosest = [closestDistance, initialPos](const BattleHex here) -> bool + { + return closestDistance < here.getDistance (initialPos, here); + }; + vstd::erase_if(sortedTiles, notClosest); //only closest tiles are interesting + auto compareHorizontal = [side, initialPos](const BattleHex left, const BattleHex right) -> bool + { + if(left.getX() != right.getX()) + { + if(side == BattleSide::ATTACKER) + return left.getX() > right.getX(); //find furthest right + else + return left.getX() < right.getX(); //find furthest left + } + else + { + //Prefer tiles in the same row. + return std::abs(left.getY() - initialPos.getY()) < std::abs(right.getY() - initialPos.getY()); + } + }; + boost::sort (sortedTiles, compareHorizontal); + return sortedTiles.front(); +} + +std::ostream & operator<<(std::ostream & os, const BattleHex & hex) +{ + return os << boost::str(boost::format("{BattleHex: x '%d', y '%d', hex '%d'}") % hex.getX() % hex.getY() % hex.hex); +} + +static BattleHex::NeighbouringTilesCache calculateNeighbouringTiles() +{ + BattleHex::NeighbouringTilesCache ret; + ret.resize(GameConstants::BFIELD_SIZE); + + for(si16 hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) + { + auto hexes = BattleHex(hex).neighbouringTiles(); + + size_t index = 0; + for(auto neighbour : hexes) + ret[hex].at(index++) = neighbour; + } + + return ret; +} + +const BattleHex::NeighbouringTilesCache BattleHex::neighbouringTilesCache = calculateNeighbouringTiles(); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleHex.h b/lib/battle/BattleHex.h index 731252284..69de2b811 100644 --- a/lib/battle/BattleHex.h +++ b/lib/battle/BattleHex.h @@ -1,126 +1,126 @@ -/* - * BattleHex.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 - -VCMI_LIB_NAMESPACE_BEGIN - -//TODO: change to enum class - -namespace BattleSide -{ - enum Type - { - ATTACKER = 0, - DEFENDER = 1 - }; -} - -namespace GameConstants -{ - const int BFIELD_WIDTH = 17; - const int BFIELD_HEIGHT = 11; - const int BFIELD_SIZE = BFIELD_WIDTH * BFIELD_HEIGHT; -} - -using BattleSideOpt = std::optional; - -// for battle stacks' positions -struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class for better code design -{ - // helpers for siege - static constexpr si16 CASTLE_CENTRAL_TOWER = -2; - static constexpr si16 CASTLE_BOTTOM_TOWER = -3; - static constexpr si16 CASTLE_UPPER_TOWER = -4; - - // hexes for interaction with heroes - static constexpr si16 HERO_ATTACKER = 0; - static constexpr si16 HERO_DEFENDER = GameConstants::BFIELD_WIDTH - 1; - - // helpers for rendering - static constexpr si16 HEX_BEFORE_ALL = std::numeric_limits::min(); - static constexpr si16 HEX_AFTER_ALL = std::numeric_limits::max(); - - static constexpr si16 DESTRUCTIBLE_WALL_1 = 29; - static constexpr si16 DESTRUCTIBLE_WALL_2 = 78; - static constexpr si16 DESTRUCTIBLE_WALL_3 = 130; - static constexpr si16 DESTRUCTIBLE_WALL_4 = 182; - static constexpr si16 GATE_BRIDGE = 94; - static constexpr si16 GATE_OUTER = 95; - static constexpr si16 GATE_INNER = 96; - - si16 hex; - static constexpr si16 INVALID = -1; - enum EDir - { - NONE = -1, - - TOP_LEFT, - TOP_RIGHT, - RIGHT, - BOTTOM_RIGHT, - BOTTOM_LEFT, - LEFT, - - //Note: unused by BattleHex class, used by other code - TOP, - BOTTOM - }; - - BattleHex(); - BattleHex(si16 _hex); - BattleHex(si16 x, si16 y); - BattleHex(std::pair xy); - operator si16() const; - bool isValid() const; - bool isAvailable() const; //valid position not in first or last column - void setX(si16 x); - void setY(si16 y); - void setXY(si16 x, si16 y, bool hasToBeValid = true); - void setXY(std::pair xy); - si16 getX() const; - si16 getY() const; - std::pair getXY() const; - BattleHex& moveInDirection(EDir dir, bool hasToBeValid = true); - BattleHex& operator+=(EDir dir); - BattleHex cloneInDirection(EDir dir, bool hasToBeValid = true) const; - BattleHex operator+(EDir dir) const; - - /// returns all valid neighbouring tiles - std::vector neighbouringTiles() const; - - /// returns all tiles, unavailable tiles will be set as invalid - /// order of returned tiles matches EDir enim - std::vector allNeighbouringTiles() const; - - static EDir mutualPosition(BattleHex hex1, BattleHex hex2); - static uint8_t getDistance(BattleHex hex1, BattleHex hex2); - static void checkAndPush(BattleHex tile, std::vector & ret); - static BattleHex getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad - - template - void serialize(Handler &h, const int version) - { - h & hex; - } - - using NeighbouringTiles = std::array; - using NeighbouringTilesCache = std::vector; - - static const NeighbouringTilesCache neighbouringTilesCache; -private: - //Constexpr defined array with all directions used in battle - static constexpr auto hexagonalDirections() { - return std::array{BattleHex::TOP_LEFT, BattleHex::TOP_RIGHT, BattleHex::RIGHT, BattleHex::BOTTOM_RIGHT, BattleHex::BOTTOM_LEFT, BattleHex::LEFT}; - } -}; - -DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleHex & hex); - -VCMI_LIB_NAMESPACE_END +/* + * BattleHex.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 + +VCMI_LIB_NAMESPACE_BEGIN + +//TODO: change to enum class + +namespace BattleSide +{ + enum Type + { + ATTACKER = 0, + DEFENDER = 1 + }; +} + +namespace GameConstants +{ + const int BFIELD_WIDTH = 17; + const int BFIELD_HEIGHT = 11; + const int BFIELD_SIZE = BFIELD_WIDTH * BFIELD_HEIGHT; +} + +using BattleSideOpt = std::optional; + +// for battle stacks' positions +struct DLL_LINKAGE BattleHex //TODO: decide if this should be changed to class for better code design +{ + // helpers for siege + static constexpr si16 CASTLE_CENTRAL_TOWER = -2; + static constexpr si16 CASTLE_BOTTOM_TOWER = -3; + static constexpr si16 CASTLE_UPPER_TOWER = -4; + + // hexes for interaction with heroes + static constexpr si16 HERO_ATTACKER = 0; + static constexpr si16 HERO_DEFENDER = GameConstants::BFIELD_WIDTH - 1; + + // helpers for rendering + static constexpr si16 HEX_BEFORE_ALL = std::numeric_limits::min(); + static constexpr si16 HEX_AFTER_ALL = std::numeric_limits::max(); + + static constexpr si16 DESTRUCTIBLE_WALL_1 = 29; + static constexpr si16 DESTRUCTIBLE_WALL_2 = 78; + static constexpr si16 DESTRUCTIBLE_WALL_3 = 130; + static constexpr si16 DESTRUCTIBLE_WALL_4 = 182; + static constexpr si16 GATE_BRIDGE = 94; + static constexpr si16 GATE_OUTER = 95; + static constexpr si16 GATE_INNER = 96; + + si16 hex; + static constexpr si16 INVALID = -1; + enum EDir + { + NONE = -1, + + TOP_LEFT, + TOP_RIGHT, + RIGHT, + BOTTOM_RIGHT, + BOTTOM_LEFT, + LEFT, + + //Note: unused by BattleHex class, used by other code + TOP, + BOTTOM + }; + + BattleHex(); + BattleHex(si16 _hex); + BattleHex(si16 x, si16 y); + BattleHex(std::pair xy); + operator si16() const; + bool isValid() const; + bool isAvailable() const; //valid position not in first or last column + void setX(si16 x); + void setY(si16 y); + void setXY(si16 x, si16 y, bool hasToBeValid = true); + void setXY(std::pair xy); + si16 getX() const; + si16 getY() const; + std::pair getXY() const; + BattleHex& moveInDirection(EDir dir, bool hasToBeValid = true); + BattleHex& operator+=(EDir dir); + BattleHex cloneInDirection(EDir dir, bool hasToBeValid = true) const; + BattleHex operator+(EDir dir) const; + + /// returns all valid neighbouring tiles + std::vector neighbouringTiles() const; + + /// returns all tiles, unavailable tiles will be set as invalid + /// order of returned tiles matches EDir enim + std::vector allNeighbouringTiles() const; + + static EDir mutualPosition(BattleHex hex1, BattleHex hex2); + static uint8_t getDistance(BattleHex hex1, BattleHex hex2); + static void checkAndPush(BattleHex tile, std::vector & ret); + static BattleHex getClosestTile(ui8 side, BattleHex initialPos, std::set & possibilities); //TODO: vector or set? copying one to another is bad + + template + void serialize(Handler &h, const int version) + { + h & hex; + } + + using NeighbouringTiles = std::array; + using NeighbouringTilesCache = std::vector; + + static const NeighbouringTilesCache neighbouringTilesCache; +private: + //Constexpr defined array with all directions used in battle + static constexpr auto hexagonalDirections() { + return std::array{BattleHex::TOP_LEFT, BattleHex::TOP_RIGHT, BattleHex::RIGHT, BattleHex::BOTTOM_RIGHT, BattleHex::BOTTOM_LEFT, BattleHex::LEFT}; + } +}; + +DLL_EXPORT std::ostream & operator<<(std::ostream & os, const BattleHex & hex); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 89cdf6cbe..99345d87a 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -1,1054 +1,1054 @@ -/* - * BattleInfo.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 "BattleInfo.h" -#include "bonuses/Limiters.h" -#include "bonuses/Updaters.h" -#include "../CStack.h" -#include "../CHeroHandler.h" -#include "../NetPacks.h" -#include "../filesystem/Filesystem.h" -#include "../mapObjects/CGTownInstance.h" -#include "../CGeneralTextHandler.h" -#include "../BattleFieldHandler.h" -#include "../ObstacleHandler.h" - -//TODO: remove -#include "../IGameCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -///BattleInfo -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) -{ - PlayerColor owner = sides[side].color; - assert(!owner.isValidPlayer() || (base.armyObj && base.armyObj->tempOwner == owner)); - - auto * ret = new CStack(&base, owner, id, side, slot); - ret->initialPosition = getAvaliableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found? - stacks.push_back(ret); - return ret; -} - -CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position) -{ - PlayerColor owner = sides[side].color; - auto * ret = new CStack(&base, owner, id, side, slot); - ret->initialPosition = position; - stacks.push_back(ret); - return ret; -} - -void BattleInfo::localInit() -{ - for(int i = 0; i < 2; i++) - { - auto * armyObj = battleGetArmyObject(i); - armyObj->battle = this; - armyObj->attachTo(*this); - } - - for(CStack * s : stacks) - s->localInit(this); - - exportBonuses(); -} - -namespace CGH -{ - static void readBattlePositions(const JsonNode &node, std::vector< std::vector > & dest) - { - for(const JsonNode &level : node.Vector()) - { - std::vector pom; - for(const JsonNode &value : level.Vector()) - { - pom.push_back(static_cast(value.Float())); - } - - dest.push_back(pom); - } - } -} - -//RNG that works like H3 one -struct RandGen -{ - ui32 seed; - - void srand(ui32 s) - { - seed = s; - } - void srand(const int3 & pos) - { - srand(110291 * static_cast(pos.x) + 167801 * static_cast(pos.y) + 81569); - } - int rand() - { - seed = 214013 * seed + 2531011; - return (seed >> 16) & 0x7FFF; - } - int rand(int min, int max) - { - if(min == max) - return min; - if(min > max) - return min; - return min + rand() % (max - min + 1); - } -}; - -struct RangeGenerator -{ - class ExhaustedPossibilities : public std::exception - { - }; - - RangeGenerator(int _min, int _max, std::function _myRand): - min(_min), - remainingCount(_max - _min + 1), - remaining(remainingCount, true), - myRand(std::move(_myRand)) - { - } - - int generateNumber() const - { - if(!remainingCount) - throw ExhaustedPossibilities(); - if(remainingCount == 1) - return 0; - return myRand() % remainingCount; - } - - //get number fulfilling predicate. Never gives the same number twice. - int getSuchNumber(const std::function & goodNumberPred = nullptr) - { - int ret = -1; - do - { - int n = generateNumber(); - int i = 0; - for(;;i++) - { - assert(i < (int)remaining.size()); - if(!remaining[i]) - continue; - if(!n) - break; - n--; - } - - remainingCount--; - remaining[i] = false; - ret = i + min; - } while(goodNumberPred && !goodNumberPred(ret)); - return ret; - } - - int min, remainingCount; - std::vector remaining; - std::function myRand; -}; - -BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town) -{ - CMP_stack cmpst; - auto * curB = new BattleInfo(); - - for(auto i = 0u; i < curB->sides.size(); i++) - curB->sides[i].init(heroes[i], armies[i]); - - - std::vector & stacks = (curB->stacks); - - curB->tile = tile; - curB->battlefieldType = battlefieldType; - curB->round = -2; - curB->activeStack = -1; - curB->creatureBank = creatureBank; - curB->replayAllowed = false; - - if(town) - { - curB->town = town; - curB->terrainType = town->getNativeTerrain(); - } - else - { - curB->town = nullptr; - curB->terrainType = terrain; - } - - //setting up siege obstacles - if (town && town->hasFort()) - { - curB->si.gateState = EGateState::CLOSED; - - curB->si.wallState[EWallPart::GATE] = EWallState::INTACT; - - for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL}) - { - if (town->hasBuilt(BuildingID::CASTLE)) - curB->si.wallState[wall] = EWallState::REINFORCED; - else - curB->si.wallState[wall] = EWallState::INTACT; - } - - if (town->hasBuilt(BuildingID::CITADEL)) - curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT; - - if (town->hasBuilt(BuildingID::CASTLE)) - { - curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT; - curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT; - } - } - - //randomize obstacles - if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank - { - RandGen r{}; - auto ourRand = [&](){ return r.rand(); }; - r.srand(tile); - r.rand(1,8); //battle sound ID to play... can't do anything with it here - int tilesToBlock = r.rand(5,12); - - std::vector blockedTiles; - - auto appropriateAbsoluteObstacle = [&](int id) - { - const auto * info = Obstacle(id).getInfo(); - return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); - }; - auto appropriateUsualObstacle = [&](int id) - { - const auto * info = Obstacle(id).getInfo(); - return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); - }; - - if(r.rand(1,100) <= 40) //put cliff-like obstacle - { - try - { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); - auto obstPtr = std::make_shared(); - obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE; - obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle); - obstPtr->uniqueID = static_cast(curB->obstacles.size()); - curB->obstacles.push_back(obstPtr); - - for(BattleHex blocked : obstPtr->getBlockedTiles()) - blockedTiles.push_back(blocked); - tilesToBlock -= Obstacle(obstPtr->ID).getInfo()->blockedTiles.size() / 2; - } - catch(RangeGenerator::ExhaustedPossibilities &) - { - //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place absolute obstacle"); - } - } - - try - { - while(tilesToBlock > 0) - { - RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); - auto tileAccessibility = curB->getAccesibility(); - const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); - const ObstacleInfo &obi = *Obstacle(obid).getInfo(); - - auto validPosition = [&](BattleHex pos) -> bool - { - if(obi.height >= pos.getY()) - return false; - if(pos.getX() == 0) - return false; - if(pos.getX() + obi.width > 15) - return false; - if(vstd::contains(blockedTiles, pos)) - return false; - - for(BattleHex blocked : obi.getBlocked(pos)) - { - if(tileAccessibility[blocked] == EAccessibility::UNAVAILABLE) //for ship-to-ship battlefield - exclude hardcoded unavailable tiles - return false; - if(vstd::contains(blockedTiles, blocked)) - return false; - int x = blocked.getX(); - if(x <= 2 || x >= 14) - return false; - } - - return true; - }; - - RangeGenerator posgenerator(18, 168, ourRand); - - auto obstPtr = std::make_shared(); - obstPtr->ID = obid; - obstPtr->pos = posgenerator.getSuchNumber(validPosition); - obstPtr->uniqueID = static_cast(curB->obstacles.size()); - curB->obstacles.push_back(obstPtr); - - for(BattleHex blocked : obstPtr->getBlockedTiles()) - blockedTiles.push_back(blocked); - tilesToBlock -= static_cast(obi.blockedTiles.size()); - } - } - catch(RangeGenerator::ExhaustedPossibilities &) - { - logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place usual obstacle"); - } - } - - //reading battleStartpos - add creatures AFTER random obstacles are generated - //TODO: parse once to some structure - std::vector> looseFormations[2]; - std::vector> tightFormations[2]; - std::vector> creBankFormations[2]; - std::vector commanderField; - std::vector commanderBank; - const JsonNode config(JsonPath::builtin("config/battleStartpos.json")); - const JsonVector &positions = config["battle_positions"].Vector(); - - CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]); - CGH::readBattlePositions(positions[1]["levels"], looseFormations[1]); - CGH::readBattlePositions(positions[2]["levels"], tightFormations[0]); - CGH::readBattlePositions(positions[3]["levels"], tightFormations[1]); - CGH::readBattlePositions(positions[4]["levels"], creBankFormations[0]); - CGH::readBattlePositions(positions[5]["levels"], creBankFormations[1]); - - for (auto position : config["commanderPositions"]["field"].Vector()) - { - commanderField.push_back(static_cast(position.Float())); - } - for (auto position : config["commanderPositions"]["creBank"].Vector()) - { - commanderBank.push_back(static_cast(position.Float())); - } - - - //adding war machines - if(!creatureBank) - { - //Checks if hero has artifact and create appropriate stack - auto handleWarMachine = [&](int side, const ArtifactPosition & artslot, BattleHex hex) - { - const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); - - if(nullptr != warMachineArt) - { - CreatureID cre = warMachineArt->artType->getWarMachine(); - - if(cre != CreatureID::NONE) - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); - } - }; - - if(heroes[0]) - { - - handleWarMachine(0, ArtifactPosition::MACH1, 52); - handleWarMachine(0, ArtifactPosition::MACH2, 18); - handleWarMachine(0, ArtifactPosition::MACH3, 154); - if(town && town->hasFort()) - handleWarMachine(0, ArtifactPosition::MACH4, 120); - } - - if(heroes[1]) - { - if(!town) //defending hero shouldn't receive ballista (bug #551) - handleWarMachine(1, ArtifactPosition::MACH1, 66); - handleWarMachine(1, ArtifactPosition::MACH2, 32); - handleWarMachine(1, ArtifactPosition::MACH3, 168); - } - } - //war machines added - - //battleStartpos read - for(int side = 0; side < 2; side++) - { - int formationNo = armies[side]->stacksCount() - 1; - vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1); - - int k = 0; //stack serial - for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++) - { - std::vector *formationVector = nullptr; - if(armies[side]->formation == EArmyFormation::TIGHT ) - formationVector = &tightFormations[side][formationNo]; - else - formationVector = &looseFormations[side][formationNo]; - - if(creatureBank) - formationVector = &creBankFormations[side][formationNo]; - - BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0); - if(creatureBank && i->second->type->isDoubleWide()) - pos += side ? BattleHex::LEFT : BattleHex::RIGHT; - - curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); - } - } - - //adding commanders - for (int i = 0; i < 2; ++i) - { - if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive) - { - curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]); - } - - } - - if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL) - { - // keep tower - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER); - - if (curB->town->fortLevel() >= CGTownInstance::CASTLE) - { - // lower tower + upper tower - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER); - - curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); - } - - //Moat generating is done on server - } - - std::stable_sort(stacks.begin(),stacks.end(),cmpst); - - auto neutral = std::make_shared(EAlignment::NEUTRAL); - auto good = std::make_shared(EAlignment::GOOD); - auto evil = std::make_shared(EAlignment::EVIL); - - const auto * bgInfo = VLC->battlefields()->getById(battlefieldType); - - for(const std::shared_ptr & bonus : bgInfo->bonuses) - { - curB->addNewBonus(bonus); - } - - //native terrain bonuses - static auto nativeTerrain = std::make_shared(); - - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); - ////////////////////////////////////////////////////////////////////////// - - //tactics - bool isTacticsAllowed = !creatureBank; //no tactics in creature banks - - constexpr int sideSize = 2; - - std::array battleRepositionHex = {}; - std::array battleRepositionHexBlock = {}; - for(int i = 0; i < sideSize; i++) - { - if(heroes[i]) - { - battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)); - battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION_BLOCK)); - } - } - int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER]; - int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER]; - - /* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides - when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this - will not happen because tactics block opposite tactics on same value. - TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if - double tactics will be implemented. - */ - - if(isTacticsAllowed) - { - if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) - logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); - if(tacticsSkillDiffAttacker > 0) - { - curB->tacticsSide = BattleSide::ATTACKER; - //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffAttacker; - } - else if(tacticsSkillDiffDefender > 0) - { - curB->tacticsSide = BattleSide::DEFENDER; - //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics - curB->tacticDistance = 1 + tacticsSkillDiffDefender; - } - else - curB->tacticDistance = 0; - } - - return curB; -} - -const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const -{ - for(const auto & side : sides) - if(side.color == player) - return side.hero; - - logGlobal->error("Player %s is not in battle!", player.toString()); - return nullptr; -} - -ui8 BattleInfo::whatSide(const PlayerColor & player) const -{ - for(int i = 0; i < sides.size(); i++) - if(sides[i].color == player) - return i; - - logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.toString()); - return -1; -} - -CStack * BattleInfo::getStack(int stackID, bool onlyAlive) -{ - return const_cast(battleGetStackByID(stackID, onlyAlive)); -} - -BattleInfo::BattleInfo(): - round(-1), - activeStack(-1), - town(nullptr), - tile(-1,-1,-1), - battlefieldType(BattleField::NONE), - tacticsSide(0), - tacticDistance(0) -{ - setNodeType(BATTLE); -} - -BattleID BattleInfo::getBattleID() const -{ - return battleID; -} - -const IBattleInfo * BattleInfo::getBattle() const -{ - return this; -} - -std::optional BattleInfo::getPlayerID() const -{ - return std::nullopt; -} - -BattleInfo::~BattleInfo() -{ - for (auto & elem : stacks) - delete elem; - - for(int i = 0; i < 2; i++) - if(auto * _armyObj = battleGetArmyObject(i)) - _armyObj->battle = nullptr; -} - -int32_t BattleInfo::getActiveStackID() const -{ - return activeStack; -} - -TStacks BattleInfo::getStacksIf(TStackFilter predicate) const -{ - TStacks ret; - vstd::copy_if(stacks, std::back_inserter(ret), predicate); - return ret; -} - -battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const -{ - battle::Units ret; - vstd::copy_if(stacks, std::back_inserter(ret), predicate); - return ret; -} - - -BattleField BattleInfo::getBattlefieldType() const -{ - return battlefieldType; -} - -TerrainId BattleInfo::getTerrainType() const -{ - return terrainType; -} - -IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const -{ - ObstacleCList ret; - - for(const auto & obstacle : obstacles) - ret.push_back(obstacle); - - return ret; -} - -PlayerColor BattleInfo::getSidePlayer(ui8 side) const -{ - return sides.at(side).color; -} - -const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const -{ - return sides.at(side).armyObject; -} - -const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const -{ - return sides.at(side).hero; -} - -ui8 BattleInfo::getTacticDist() const -{ - return tacticDistance; -} - -ui8 BattleInfo::getTacticsSide() const -{ - return tacticsSide; -} - -const CGTownInstance * BattleInfo::getDefendedTown() const -{ - return town; -} - -EWallState BattleInfo::getWallState(EWallPart partOfWall) const -{ - return si.wallState.at(partOfWall); -} - -EGateState BattleInfo::getGateState() const -{ - return si.gateState; -} - -uint32_t BattleInfo::getCastSpells(ui8 side) const -{ - return sides.at(side).castSpellsCount; -} - -int32_t BattleInfo::getEnchanterCounter(ui8 side) const -{ - return sides.at(side).enchanterCounter; -} - -const IBonusBearer * BattleInfo::getBonusBearer() const -{ - return this; -} - -int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const -{ - if(damage.min != damage.max) - { - int64_t sum = 0; - - auto howManyToAv = std::min(10, attackerCount); - auto rangeGen = rng.getInt64Range(damage.min, damage.max); - - for(int32_t g = 0; g < howManyToAv; ++g) - sum += rangeGen(); - - return sum / howManyToAv; - } - else - { - return damage.min; - } -} - -int3 BattleInfo::getLocation() const -{ - return tile; -} - -bool BattleInfo::isCreatureBank() const -{ - return creatureBank; -} - - -std::vector BattleInfo::getUsedSpells(ui8 side) const -{ - return sides.at(side).usedSpellsHistory; -} - -void BattleInfo::nextRound() -{ - for(int i = 0; i < 2; ++i) - { - sides.at(i).castSpellsCount = 0; - vstd::amax(--sides.at(i).enchanterCounter, 0); - } - round += 1; - - for(CStack * s : stacks) - { - // new turn effects - s->reduceBonusDurations(Bonus::NTurns); - - s->afterNewRound(); - } - - for(auto & obst : obstacles) - obst->battleTurnPassed(); -} - -void BattleInfo::nextTurn(uint32_t unitId) -{ - activeStack = unitId; - - CStack * st = getStack(activeStack); - - //remove bonuses that last until when stack gets new turn - st->removeBonusesRecursive(Bonus::UntilGetsTurn); - - st->afterGetsTurn(); -} - -void BattleInfo::addUnit(uint32_t id, const JsonNode & data) -{ - battle::UnitInfo info; - info.load(id, data); - CStackBasicDescriptor base(info.type, info.count); - - PlayerColor owner = getSidePlayer(info.side); - - auto * ret = new CStack(&base, owner, info.id, info.side, SlotID::SUMMONED_SLOT_PLACEHOLDER); - ret->initialPosition = info.position; - stacks.push_back(ret); - ret->localInit(this); - ret->summoned = info.summoned; -} - -void BattleInfo::moveUnit(uint32_t id, BattleHex destination) -{ - auto * sta = getStack(id); - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - sta->position = destination; - //Bonuses can be limited by unit placement, so, change tree version - //to force updating a bonus. TODO: update version only when such bonuses are present - CBonusSystemNode::treeHasChanged(); -} - -void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) -{ - CStack * changedStack = getStack(id, false); - if(!changedStack) - throw std::runtime_error("Invalid unit id in BattleInfo update"); - - if(!changedStack->alive() && healthDelta > 0) - { - //checking if we resurrect a stack that is under a living stack - auto accessibility = getAccesibility(); - - if(!accessibility.accessible(changedStack->getPosition(), changedStack)) - { - logNetwork->error("Cannot resurrect %s because hex %d is occupied!", changedStack->nodeName(), changedStack->getPosition().hex); - return; //position is already occupied - } - } - - bool killed = (-healthDelta) >= changedStack->getAvailableHealth();//todo: check using alive state once rebirth will be handled separately - - bool resurrected = !changedStack->alive() && healthDelta > 0; - - //applying changes - changedStack->load(data); - - - if(healthDelta < 0) - { - changedStack->removeBonusesRecursive(Bonus::UntilBeingAttacked); - } - - resurrected = resurrected || (killed && changedStack->alive()); - - if(killed) - { - if(changedStack->cloneID >= 0) - { - //remove clone as well - CStack * clone = getStack(changedStack->cloneID); - if(clone) - clone->makeGhost(); - - changedStack->cloneID = -1; - } - } - - if(resurrected || killed) - { - //removing all spells effects - auto selector = [](const Bonus * b) - { - //Special case: DISRUPTING_RAY is absolutely permanent - return b->source == BonusSource::SPELL_EFFECT && b->sid != SpellID::DISRUPTING_RAY; - }; - changedStack->removeBonusesRecursive(selector); - } - - if(!changedStack->alive() && changedStack->isClone()) - { - for(CStack * s : stacks) - { - if(s->cloneID == changedStack->unitId()) - s->cloneID = -1; - } - } -} - -void BattleInfo::removeUnit(uint32_t id) -{ - std::set ids; - ids.insert(id); - - while(!ids.empty()) - { - auto toRemoveId = *ids.begin(); - auto * toRemove = getStack(toRemoveId, false); - - if(!toRemove) - { - logGlobal->error("Cannot find stack %d", toRemoveId); - return; - } - - if(!toRemove->ghost) - { - toRemove->onRemoved(); - toRemove->detachFromAll(); - - //stack may be removed instantly (not being killed first) - //handle clone remove also here - if(toRemove->cloneID >= 0) - { - ids.insert(toRemove->cloneID); - toRemove->cloneID = -1; - } - - //cleanup remaining clone links if any - for(auto * s : stacks) - { - if(s->cloneID == toRemoveId) - s->cloneID = -1; - } - } - - ids.erase(toRemoveId); - } -} - -void BattleInfo::updateUnit(uint32_t id, const JsonNode & data) -{ - //TODO -} - -void BattleInfo::addUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & b : bonus) - addOrUpdateUnitBonus(sta, b, true); -} - -void BattleInfo::updateUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & b : bonus) - addOrUpdateUnitBonus(sta, b, false); -} - -void BattleInfo::removeUnitBonus(uint32_t id, const std::vector & bonus) -{ - CStack * sta = getStack(id, false); - - if(!sta) - { - logGlobal->error("Cannot find stack %d", id); - return; - } - - for(const Bonus & one : bonus) - { - auto selector = [one](const Bonus * b) - { - //compare everything but turnsRemain, limiter and propagator - return one.duration == b->duration - && one.type == b->type - && one.subtype == b->subtype - && one.source == b->source - && one.val == b->val - && one.sid == b->sid - && one.valType == b->valType - && one.additionalInfo == b->additionalInfo - && one.effectRange == b->effectRange - && one.description == b->description; - }; - sta->removeBonusesRecursive(selector); - } -} - -uint32_t BattleInfo::nextUnitId() const -{ - return static_cast(stacks.size()); -} - -void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd) -{ - if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtype(value.type, value.subtype)))) - { - //no such effect or cumulative - add new - logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description()); - sta->addNewBonus(std::make_shared(value)); - } - else - { - logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description()); - - for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize - { - if(stackBonus->source == value.source && stackBonus->sid == value.sid && stackBonus->type == value.type && stackBonus->subtype == value.subtype) - { - stackBonus->turnsRemain = std::max(stackBonus->turnsRemain, value.turnsRemain); - } - } - CBonusSystemNode::treeHasChanged(); - } -} - -void BattleInfo::setWallState(EWallPart partOfWall, EWallState state) -{ - si.wallState[partOfWall] = state; -} - -void BattleInfo::addObstacle(const ObstacleChanges & changes) -{ - std::shared_ptr obstacle = std::make_shared(); - obstacle->fromInfo(changes); - obstacles.push_back(obstacle); -} - -void BattleInfo::updateObstacle(const ObstacleChanges& changes) -{ - std::shared_ptr changedObstacle = std::make_shared(); - changedObstacle->fromInfo(changes); - - for(auto & obstacle : obstacles) - { - if(obstacle->uniqueID == changes.id) // update this obstacle - { - auto * spellObstacle = dynamic_cast(obstacle.get()); - assert(spellObstacle); - - // Currently we only support to update the "revealed" property - spellObstacle->revealed = changedObstacle->revealed; - - break; - } - } -} - -void BattleInfo::removeObstacle(uint32_t id) -{ - for(int i=0; i < obstacles.size(); ++i) - { - if(obstacles[i]->uniqueID == id) //remove this obstacle - { - obstacles.erase(obstacles.begin() + i); - break; - } - } -} - -CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const -{ - return const_cast(CBattleInfoEssentials::battleGetArmyObject(side)); -} - -CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const -{ - return const_cast(CBattleInfoEssentials::battleGetFightingHero(side)); -} - -#if SCRIPTING_ENABLED -scripting::Pool * BattleInfo::getContextPool() const -{ - //this is real battle, use global scripting context pool - //TODO: make this line not ugly - return IObjectInterface::cb->getGlobalContextPool(); -} -#endif - -bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const -{ - switch(phase) - { - case 0: //catapult moves after turrets - return a->creatureIndex() > b->creatureIndex(); //catapult is 145 and turrets are 149 - case 1: - case 2: - case 3: - { - int as = a->getInitiative(turn); - int bs = b->getInitiative(turn); - - if(as != bs) - return as > bs; - - if(a->unitSide() == b->unitSide()) - return a->unitSlot() < b->unitSlot(); - - return (a->unitSide() == side || b->unitSide() == side) - ? a->unitSide() != side - : a->unitSide() < b->unitSide(); - } - default: - assert(false); - return false; - } - - assert(false); - return false; -} - -CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side): - phase(Phase), - turn(Turn), - side(Side) -{ -} - -VCMI_LIB_NAMESPACE_END +/* + * BattleInfo.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 "BattleInfo.h" +#include "bonuses/Limiters.h" +#include "bonuses/Updaters.h" +#include "../CStack.h" +#include "../CHeroHandler.h" +#include "../NetPacks.h" +#include "../filesystem/Filesystem.h" +#include "../mapObjects/CGTownInstance.h" +#include "../CGeneralTextHandler.h" +#include "../BattleFieldHandler.h" +#include "../ObstacleHandler.h" + +//TODO: remove +#include "../IGameCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +///BattleInfo +CStack * BattleInfo::generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position) +{ + PlayerColor owner = sides[side].color; + assert(!owner.isValidPlayer() || (base.armyObj && base.armyObj->tempOwner == owner)); + + auto * ret = new CStack(&base, owner, id, side, slot); + ret->initialPosition = getAvaliableHex(base.getCreatureID(), side, position); //TODO: what if no free tile on battlefield was found? + stacks.push_back(ret); + return ret; +} + +CStack * BattleInfo::generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position) +{ + PlayerColor owner = sides[side].color; + auto * ret = new CStack(&base, owner, id, side, slot); + ret->initialPosition = position; + stacks.push_back(ret); + return ret; +} + +void BattleInfo::localInit() +{ + for(int i = 0; i < 2; i++) + { + auto * armyObj = battleGetArmyObject(i); + armyObj->battle = this; + armyObj->attachTo(*this); + } + + for(CStack * s : stacks) + s->localInit(this); + + exportBonuses(); +} + +namespace CGH +{ + static void readBattlePositions(const JsonNode &node, std::vector< std::vector > & dest) + { + for(const JsonNode &level : node.Vector()) + { + std::vector pom; + for(const JsonNode &value : level.Vector()) + { + pom.push_back(static_cast(value.Float())); + } + + dest.push_back(pom); + } + } +} + +//RNG that works like H3 one +struct RandGen +{ + ui32 seed; + + void srand(ui32 s) + { + seed = s; + } + void srand(const int3 & pos) + { + srand(110291 * static_cast(pos.x) + 167801 * static_cast(pos.y) + 81569); + } + int rand() + { + seed = 214013 * seed + 2531011; + return (seed >> 16) & 0x7FFF; + } + int rand(int min, int max) + { + if(min == max) + return min; + if(min > max) + return min; + return min + rand() % (max - min + 1); + } +}; + +struct RangeGenerator +{ + class ExhaustedPossibilities : public std::exception + { + }; + + RangeGenerator(int _min, int _max, std::function _myRand): + min(_min), + remainingCount(_max - _min + 1), + remaining(remainingCount, true), + myRand(std::move(_myRand)) + { + } + + int generateNumber() const + { + if(!remainingCount) + throw ExhaustedPossibilities(); + if(remainingCount == 1) + return 0; + return myRand() % remainingCount; + } + + //get number fulfilling predicate. Never gives the same number twice. + int getSuchNumber(const std::function & goodNumberPred = nullptr) + { + int ret = -1; + do + { + int n = generateNumber(); + int i = 0; + for(;;i++) + { + assert(i < (int)remaining.size()); + if(!remaining[i]) + continue; + if(!n) + break; + n--; + } + + remainingCount--; + remaining[i] = false; + ret = i + min; + } while(goodNumberPred && !goodNumberPred(ret)); + return ret; + } + + int min, remainingCount; + std::vector remaining; + std::function myRand; +}; + +BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town) +{ + CMP_stack cmpst; + auto * curB = new BattleInfo(); + + for(auto i = 0u; i < curB->sides.size(); i++) + curB->sides[i].init(heroes[i], armies[i]); + + + std::vector & stacks = (curB->stacks); + + curB->tile = tile; + curB->battlefieldType = battlefieldType; + curB->round = -2; + curB->activeStack = -1; + curB->creatureBank = creatureBank; + curB->replayAllowed = false; + + if(town) + { + curB->town = town; + curB->terrainType = town->getNativeTerrain(); + } + else + { + curB->town = nullptr; + curB->terrainType = terrain; + } + + //setting up siege obstacles + if (town && town->hasFort()) + { + curB->si.gateState = EGateState::CLOSED; + + curB->si.wallState[EWallPart::GATE] = EWallState::INTACT; + + for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL}) + { + if (town->hasBuilt(BuildingID::CASTLE)) + curB->si.wallState[wall] = EWallState::REINFORCED; + else + curB->si.wallState[wall] = EWallState::INTACT; + } + + if (town->hasBuilt(BuildingID::CITADEL)) + curB->si.wallState[EWallPart::KEEP] = EWallState::INTACT; + + if (town->hasBuilt(BuildingID::CASTLE)) + { + curB->si.wallState[EWallPart::UPPER_TOWER] = EWallState::INTACT; + curB->si.wallState[EWallPart::BOTTOM_TOWER] = EWallState::INTACT; + } + } + + //randomize obstacles + if (town == nullptr && !creatureBank) //do it only when it's not siege and not creature bank + { + RandGen r{}; + auto ourRand = [&](){ return r.rand(); }; + r.srand(tile); + r.rand(1,8); //battle sound ID to play... can't do anything with it here + int tilesToBlock = r.rand(5,12); + + std::vector blockedTiles; + + auto appropriateAbsoluteObstacle = [&](int id) + { + const auto * info = Obstacle(id).getInfo(); + return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + }; + auto appropriateUsualObstacle = [&](int id) + { + const auto * info = Obstacle(id).getInfo(); + return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType); + }; + + if(r.rand(1,100) <= 40) //put cliff-like obstacle + { + try + { + RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + auto obstPtr = std::make_shared(); + obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE; + obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle); + obstPtr->uniqueID = static_cast(curB->obstacles.size()); + curB->obstacles.push_back(obstPtr); + + for(BattleHex blocked : obstPtr->getBlockedTiles()) + blockedTiles.push_back(blocked); + tilesToBlock -= Obstacle(obstPtr->ID).getInfo()->blockedTiles.size() / 2; + } + catch(RangeGenerator::ExhaustedPossibilities &) + { + //silently ignore, if we can't place absolute obstacle, we'll go with the usual ones + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place absolute obstacle"); + } + } + + try + { + while(tilesToBlock > 0) + { + RangeGenerator obidgen(0, VLC->obstacleHandler->objects.size() - 1, ourRand); + auto tileAccessibility = curB->getAccesibility(); + const int obid = obidgen.getSuchNumber(appropriateUsualObstacle); + const ObstacleInfo &obi = *Obstacle(obid).getInfo(); + + auto validPosition = [&](BattleHex pos) -> bool + { + if(obi.height >= pos.getY()) + return false; + if(pos.getX() == 0) + return false; + if(pos.getX() + obi.width > 15) + return false; + if(vstd::contains(blockedTiles, pos)) + return false; + + for(BattleHex blocked : obi.getBlocked(pos)) + { + if(tileAccessibility[blocked] == EAccessibility::UNAVAILABLE) //for ship-to-ship battlefield - exclude hardcoded unavailable tiles + return false; + if(vstd::contains(blockedTiles, blocked)) + return false; + int x = blocked.getX(); + if(x <= 2 || x >= 14) + return false; + } + + return true; + }; + + RangeGenerator posgenerator(18, 168, ourRand); + + auto obstPtr = std::make_shared(); + obstPtr->ID = obid; + obstPtr->pos = posgenerator.getSuchNumber(validPosition); + obstPtr->uniqueID = static_cast(curB->obstacles.size()); + curB->obstacles.push_back(obstPtr); + + for(BattleHex blocked : obstPtr->getBlockedTiles()) + blockedTiles.push_back(blocked); + tilesToBlock -= static_cast(obi.blockedTiles.size()); + } + } + catch(RangeGenerator::ExhaustedPossibilities &) + { + logGlobal->debug("RangeGenerator::ExhaustedPossibilities exception occurred - cannot place usual obstacle"); + } + } + + //reading battleStartpos - add creatures AFTER random obstacles are generated + //TODO: parse once to some structure + std::vector> looseFormations[2]; + std::vector> tightFormations[2]; + std::vector> creBankFormations[2]; + std::vector commanderField; + std::vector commanderBank; + const JsonNode config(JsonPath::builtin("config/battleStartpos.json")); + const JsonVector &positions = config["battle_positions"].Vector(); + + CGH::readBattlePositions(positions[0]["levels"], looseFormations[0]); + CGH::readBattlePositions(positions[1]["levels"], looseFormations[1]); + CGH::readBattlePositions(positions[2]["levels"], tightFormations[0]); + CGH::readBattlePositions(positions[3]["levels"], tightFormations[1]); + CGH::readBattlePositions(positions[4]["levels"], creBankFormations[0]); + CGH::readBattlePositions(positions[5]["levels"], creBankFormations[1]); + + for (auto position : config["commanderPositions"]["field"].Vector()) + { + commanderField.push_back(static_cast(position.Float())); + } + for (auto position : config["commanderPositions"]["creBank"].Vector()) + { + commanderBank.push_back(static_cast(position.Float())); + } + + + //adding war machines + if(!creatureBank) + { + //Checks if hero has artifact and create appropriate stack + auto handleWarMachine = [&](int side, const ArtifactPosition & artslot, BattleHex hex) + { + const CArtifactInstance * warMachineArt = heroes[side]->getArt(artslot); + + if(nullptr != warMachineArt) + { + CreatureID cre = warMachineArt->artType->getWarMachine(); + + if(cre != CreatureID::NONE) + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex); + } + }; + + if(heroes[0]) + { + + handleWarMachine(0, ArtifactPosition::MACH1, 52); + handleWarMachine(0, ArtifactPosition::MACH2, 18); + handleWarMachine(0, ArtifactPosition::MACH3, 154); + if(town && town->hasFort()) + handleWarMachine(0, ArtifactPosition::MACH4, 120); + } + + if(heroes[1]) + { + if(!town) //defending hero shouldn't receive ballista (bug #551) + handleWarMachine(1, ArtifactPosition::MACH1, 66); + handleWarMachine(1, ArtifactPosition::MACH2, 32); + handleWarMachine(1, ArtifactPosition::MACH3, 168); + } + } + //war machines added + + //battleStartpos read + for(int side = 0; side < 2; side++) + { + int formationNo = armies[side]->stacksCount() - 1; + vstd::abetween(formationNo, 0, GameConstants::ARMY_SIZE - 1); + + int k = 0; //stack serial + for(auto i = armies[side]->Slots().begin(); i != armies[side]->Slots().end(); i++, k++) + { + std::vector *formationVector = nullptr; + if(armies[side]->formation == EArmyFormation::TIGHT ) + formationVector = &tightFormations[side][formationNo]; + else + formationVector = &looseFormations[side][formationNo]; + + if(creatureBank) + formationVector = &creBankFormations[side][formationNo]; + + BattleHex pos = (k < formationVector->size() ? formationVector->at(k) : 0); + if(creatureBank && i->second->type->isDoubleWide()) + pos += side ? BattleHex::LEFT : BattleHex::RIGHT; + + curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos); + } + } + + //adding commanders + for (int i = 0; i < 2; ++i) + { + if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive) + { + curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, creatureBank ? commanderBank[i] : commanderField[i]); + } + + } + + if (curB->town && curB->town->fortLevel() >= CGTownInstance::CITADEL) + { + // keep tower + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER); + + if (curB->town->fortLevel() >= CGTownInstance::CASTLE) + { + // lower tower + upper tower + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER); + + curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), 1, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER); + } + + //Moat generating is done on server + } + + std::stable_sort(stacks.begin(),stacks.end(),cmpst); + + auto neutral = std::make_shared(EAlignment::NEUTRAL); + auto good = std::make_shared(EAlignment::GOOD); + auto evil = std::make_shared(EAlignment::EVIL); + + const auto * bgInfo = VLC->battlefields()->getById(battlefieldType); + + for(const std::shared_ptr & bonus : bgInfo->bonuses) + { + curB->addNewBonus(bonus); + } + + //native terrain bonuses + static auto nativeTerrain = std::make_shared(); + + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + ////////////////////////////////////////////////////////////////////////// + + //tactics + bool isTacticsAllowed = !creatureBank; //no tactics in creature banks + + constexpr int sideSize = 2; + + std::array battleRepositionHex = {}; + std::array battleRepositionHexBlock = {}; + for(int i = 0; i < sideSize; i++) + { + if(heroes[i]) + { + battleRepositionHex[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION)); + battleRepositionHexBlock[i] += heroes[i]->valOfBonuses(Selector::type()(BonusType::BEFORE_BATTLE_REPOSITION_BLOCK)); + } + } + int tacticsSkillDiffAttacker = battleRepositionHex[BattleSide::ATTACKER] - battleRepositionHexBlock[BattleSide::DEFENDER]; + int tacticsSkillDiffDefender = battleRepositionHex[BattleSide::DEFENDER] - battleRepositionHexBlock[BattleSide::ATTACKER]; + + /* for current tactics, we need to choose one side, so, we will choose side when first - second > 0, and ignore sides + when first - second <= 0. If there will be situations when both > 0, attacker will be chosen. Anyway, in OH3 this + will not happen because tactics block opposite tactics on same value. + TODO: For now, it is an error to use BEFORE_BATTLE_REPOSITION bonus without counterpart, but it can be changed if + double tactics will be implemented. + */ + + if(isTacticsAllowed) + { + if(tacticsSkillDiffAttacker > 0 && tacticsSkillDiffDefender > 0) + logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!"); + if(tacticsSkillDiffAttacker > 0) + { + curB->tacticsSide = BattleSide::ATTACKER; + //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics + curB->tacticDistance = 1 + tacticsSkillDiffAttacker; + } + else if(tacticsSkillDiffDefender > 0) + { + curB->tacticsSide = BattleSide::DEFENDER; + //bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics + curB->tacticDistance = 1 + tacticsSkillDiffDefender; + } + else + curB->tacticDistance = 0; + } + + return curB; +} + +const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const +{ + for(const auto & side : sides) + if(side.color == player) + return side.hero; + + logGlobal->error("Player %s is not in battle!", player.toString()); + return nullptr; +} + +ui8 BattleInfo::whatSide(const PlayerColor & player) const +{ + for(int i = 0; i < sides.size(); i++) + if(sides[i].color == player) + return i; + + logGlobal->warn("BattleInfo::whatSide: Player %s is not in battle!", player.toString()); + return -1; +} + +CStack * BattleInfo::getStack(int stackID, bool onlyAlive) +{ + return const_cast(battleGetStackByID(stackID, onlyAlive)); +} + +BattleInfo::BattleInfo(): + round(-1), + activeStack(-1), + town(nullptr), + tile(-1,-1,-1), + battlefieldType(BattleField::NONE), + tacticsSide(0), + tacticDistance(0) +{ + setNodeType(BATTLE); +} + +BattleID BattleInfo::getBattleID() const +{ + return battleID; +} + +const IBattleInfo * BattleInfo::getBattle() const +{ + return this; +} + +std::optional BattleInfo::getPlayerID() const +{ + return std::nullopt; +} + +BattleInfo::~BattleInfo() +{ + for (auto & elem : stacks) + delete elem; + + for(int i = 0; i < 2; i++) + if(auto * _armyObj = battleGetArmyObject(i)) + _armyObj->battle = nullptr; +} + +int32_t BattleInfo::getActiveStackID() const +{ + return activeStack; +} + +TStacks BattleInfo::getStacksIf(TStackFilter predicate) const +{ + TStacks ret; + vstd::copy_if(stacks, std::back_inserter(ret), predicate); + return ret; +} + +battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const +{ + battle::Units ret; + vstd::copy_if(stacks, std::back_inserter(ret), predicate); + return ret; +} + + +BattleField BattleInfo::getBattlefieldType() const +{ + return battlefieldType; +} + +TerrainId BattleInfo::getTerrainType() const +{ + return terrainType; +} + +IBattleInfo::ObstacleCList BattleInfo::getAllObstacles() const +{ + ObstacleCList ret; + + for(const auto & obstacle : obstacles) + ret.push_back(obstacle); + + return ret; +} + +PlayerColor BattleInfo::getSidePlayer(ui8 side) const +{ + return sides.at(side).color; +} + +const CArmedInstance * BattleInfo::getSideArmy(ui8 side) const +{ + return sides.at(side).armyObject; +} + +const CGHeroInstance * BattleInfo::getSideHero(ui8 side) const +{ + return sides.at(side).hero; +} + +ui8 BattleInfo::getTacticDist() const +{ + return tacticDistance; +} + +ui8 BattleInfo::getTacticsSide() const +{ + return tacticsSide; +} + +const CGTownInstance * BattleInfo::getDefendedTown() const +{ + return town; +} + +EWallState BattleInfo::getWallState(EWallPart partOfWall) const +{ + return si.wallState.at(partOfWall); +} + +EGateState BattleInfo::getGateState() const +{ + return si.gateState; +} + +uint32_t BattleInfo::getCastSpells(ui8 side) const +{ + return sides.at(side).castSpellsCount; +} + +int32_t BattleInfo::getEnchanterCounter(ui8 side) const +{ + return sides.at(side).enchanterCounter; +} + +const IBonusBearer * BattleInfo::getBonusBearer() const +{ + return this; +} + +int64_t BattleInfo::getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const +{ + if(damage.min != damage.max) + { + int64_t sum = 0; + + auto howManyToAv = std::min(10, attackerCount); + auto rangeGen = rng.getInt64Range(damage.min, damage.max); + + for(int32_t g = 0; g < howManyToAv; ++g) + sum += rangeGen(); + + return sum / howManyToAv; + } + else + { + return damage.min; + } +} + +int3 BattleInfo::getLocation() const +{ + return tile; +} + +bool BattleInfo::isCreatureBank() const +{ + return creatureBank; +} + + +std::vector BattleInfo::getUsedSpells(ui8 side) const +{ + return sides.at(side).usedSpellsHistory; +} + +void BattleInfo::nextRound() +{ + for(int i = 0; i < 2; ++i) + { + sides.at(i).castSpellsCount = 0; + vstd::amax(--sides.at(i).enchanterCounter, 0); + } + round += 1; + + for(CStack * s : stacks) + { + // new turn effects + s->reduceBonusDurations(Bonus::NTurns); + + s->afterNewRound(); + } + + for(auto & obst : obstacles) + obst->battleTurnPassed(); +} + +void BattleInfo::nextTurn(uint32_t unitId) +{ + activeStack = unitId; + + CStack * st = getStack(activeStack); + + //remove bonuses that last until when stack gets new turn + st->removeBonusesRecursive(Bonus::UntilGetsTurn); + + st->afterGetsTurn(); +} + +void BattleInfo::addUnit(uint32_t id, const JsonNode & data) +{ + battle::UnitInfo info; + info.load(id, data); + CStackBasicDescriptor base(info.type, info.count); + + PlayerColor owner = getSidePlayer(info.side); + + auto * ret = new CStack(&base, owner, info.id, info.side, SlotID::SUMMONED_SLOT_PLACEHOLDER); + ret->initialPosition = info.position; + stacks.push_back(ret); + ret->localInit(this); + ret->summoned = info.summoned; +} + +void BattleInfo::moveUnit(uint32_t id, BattleHex destination) +{ + auto * sta = getStack(id); + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + sta->position = destination; + //Bonuses can be limited by unit placement, so, change tree version + //to force updating a bonus. TODO: update version only when such bonuses are present + CBonusSystemNode::treeHasChanged(); +} + +void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) +{ + CStack * changedStack = getStack(id, false); + if(!changedStack) + throw std::runtime_error("Invalid unit id in BattleInfo update"); + + if(!changedStack->alive() && healthDelta > 0) + { + //checking if we resurrect a stack that is under a living stack + auto accessibility = getAccesibility(); + + if(!accessibility.accessible(changedStack->getPosition(), changedStack)) + { + logNetwork->error("Cannot resurrect %s because hex %d is occupied!", changedStack->nodeName(), changedStack->getPosition().hex); + return; //position is already occupied + } + } + + bool killed = (-healthDelta) >= changedStack->getAvailableHealth();//todo: check using alive state once rebirth will be handled separately + + bool resurrected = !changedStack->alive() && healthDelta > 0; + + //applying changes + changedStack->load(data); + + + if(healthDelta < 0) + { + changedStack->removeBonusesRecursive(Bonus::UntilBeingAttacked); + } + + resurrected = resurrected || (killed && changedStack->alive()); + + if(killed) + { + if(changedStack->cloneID >= 0) + { + //remove clone as well + CStack * clone = getStack(changedStack->cloneID); + if(clone) + clone->makeGhost(); + + changedStack->cloneID = -1; + } + } + + if(resurrected || killed) + { + //removing all spells effects + auto selector = [](const Bonus * b) + { + //Special case: DISRUPTING_RAY is absolutely permanent + return b->source == BonusSource::SPELL_EFFECT && b->sid != SpellID::DISRUPTING_RAY; + }; + changedStack->removeBonusesRecursive(selector); + } + + if(!changedStack->alive() && changedStack->isClone()) + { + for(CStack * s : stacks) + { + if(s->cloneID == changedStack->unitId()) + s->cloneID = -1; + } + } +} + +void BattleInfo::removeUnit(uint32_t id) +{ + std::set ids; + ids.insert(id); + + while(!ids.empty()) + { + auto toRemoveId = *ids.begin(); + auto * toRemove = getStack(toRemoveId, false); + + if(!toRemove) + { + logGlobal->error("Cannot find stack %d", toRemoveId); + return; + } + + if(!toRemove->ghost) + { + toRemove->onRemoved(); + toRemove->detachFromAll(); + + //stack may be removed instantly (not being killed first) + //handle clone remove also here + if(toRemove->cloneID >= 0) + { + ids.insert(toRemove->cloneID); + toRemove->cloneID = -1; + } + + //cleanup remaining clone links if any + for(auto * s : stacks) + { + if(s->cloneID == toRemoveId) + s->cloneID = -1; + } + } + + ids.erase(toRemoveId); + } +} + +void BattleInfo::updateUnit(uint32_t id, const JsonNode & data) +{ + //TODO +} + +void BattleInfo::addUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & b : bonus) + addOrUpdateUnitBonus(sta, b, true); +} + +void BattleInfo::updateUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & b : bonus) + addOrUpdateUnitBonus(sta, b, false); +} + +void BattleInfo::removeUnitBonus(uint32_t id, const std::vector & bonus) +{ + CStack * sta = getStack(id, false); + + if(!sta) + { + logGlobal->error("Cannot find stack %d", id); + return; + } + + for(const Bonus & one : bonus) + { + auto selector = [one](const Bonus * b) + { + //compare everything but turnsRemain, limiter and propagator + return one.duration == b->duration + && one.type == b->type + && one.subtype == b->subtype + && one.source == b->source + && one.val == b->val + && one.sid == b->sid + && one.valType == b->valType + && one.additionalInfo == b->additionalInfo + && one.effectRange == b->effectRange + && one.description == b->description; + }; + sta->removeBonusesRecursive(selector); + } +} + +uint32_t BattleInfo::nextUnitId() const +{ + return static_cast(stacks.size()); +} + +void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd) +{ + if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtype(value.type, value.subtype)))) + { + //no such effect or cumulative - add new + logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description()); + sta->addNewBonus(std::make_shared(value)); + } + else + { + logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description()); + + for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize + { + if(stackBonus->source == value.source && stackBonus->sid == value.sid && stackBonus->type == value.type && stackBonus->subtype == value.subtype) + { + stackBonus->turnsRemain = std::max(stackBonus->turnsRemain, value.turnsRemain); + } + } + CBonusSystemNode::treeHasChanged(); + } +} + +void BattleInfo::setWallState(EWallPart partOfWall, EWallState state) +{ + si.wallState[partOfWall] = state; +} + +void BattleInfo::addObstacle(const ObstacleChanges & changes) +{ + std::shared_ptr obstacle = std::make_shared(); + obstacle->fromInfo(changes); + obstacles.push_back(obstacle); +} + +void BattleInfo::updateObstacle(const ObstacleChanges& changes) +{ + std::shared_ptr changedObstacle = std::make_shared(); + changedObstacle->fromInfo(changes); + + for(auto & obstacle : obstacles) + { + if(obstacle->uniqueID == changes.id) // update this obstacle + { + auto * spellObstacle = dynamic_cast(obstacle.get()); + assert(spellObstacle); + + // Currently we only support to update the "revealed" property + spellObstacle->revealed = changedObstacle->revealed; + + break; + } + } +} + +void BattleInfo::removeObstacle(uint32_t id) +{ + for(int i=0; i < obstacles.size(); ++i) + { + if(obstacles[i]->uniqueID == id) //remove this obstacle + { + obstacles.erase(obstacles.begin() + i); + break; + } + } +} + +CArmedInstance * BattleInfo::battleGetArmyObject(ui8 side) const +{ + return const_cast(CBattleInfoEssentials::battleGetArmyObject(side)); +} + +CGHeroInstance * BattleInfo::battleGetFightingHero(ui8 side) const +{ + return const_cast(CBattleInfoEssentials::battleGetFightingHero(side)); +} + +#if SCRIPTING_ENABLED +scripting::Pool * BattleInfo::getContextPool() const +{ + //this is real battle, use global scripting context pool + //TODO: make this line not ugly + return IObjectInterface::cb->getGlobalContextPool(); +} +#endif + +bool CMP_stack::operator()(const battle::Unit * a, const battle::Unit * b) const +{ + switch(phase) + { + case 0: //catapult moves after turrets + return a->creatureIndex() > b->creatureIndex(); //catapult is 145 and turrets are 149 + case 1: + case 2: + case 3: + { + int as = a->getInitiative(turn); + int bs = b->getInitiative(turn); + + if(as != bs) + return as > bs; + + if(a->unitSide() == b->unitSide()) + return a->unitSlot() < b->unitSlot(); + + return (a->unitSide() == side || b->unitSide() == side) + ? a->unitSide() != side + : a->unitSide() < b->unitSide(); + } + default: + assert(false); + return false; + } + + assert(false); + return false; +} + +CMP_stack::CMP_stack(int Phase, int Turn, uint8_t Side): + phase(Phase), + turn(Turn), + side(Side) +{ +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index ff30dbe76..030f56051 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -1,180 +1,180 @@ -/* - * BattleInfo.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 "../int3.h" -#include "../bonuses/Bonus.h" -#include "../bonuses/CBonusSystemNode.h" -#include "CBattleInfoCallback.h" -#include "IBattleState.h" -#include "SiegeInfo.h" -#include "SideInBattle.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CStack; -class CStackInstance; -class CStackBasicDescriptor; -class BattleField; - -class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState -{ -public: - BattleID battleID = BattleID(0); - - enum BattleSide - { - ATTACKER = 0, - DEFENDER - }; - std::array sides; //sides[0] - attacker, sides[1] - defender - si32 round, activeStack; - const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) - int3 tile; //for background and bonuses - bool creatureBank; //auxilary field, do not serialize - bool replayAllowed; - std::vector stacks; - std::vector > obstacles; - SiegeInfo si; - - BattleField battlefieldType; //like !!BA:B - TerrainId terrainType; //used for some stack nativity checks (not the bonus limiters though that have their own copy) - - ui8 tacticsSide; //which side is requested to play tactics phase - ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) - - template void serialize(Handler &h, const int version) - { - h & battleID; - h & sides; - h & round; - h & activeStack; - h & town; - h & tile; - h & stacks; - h & obstacles; - h & si; - h & battlefieldType; - h & terrainType; - h & tacticsSide; - h & tacticDistance; - h & static_cast(*this); - if (version > 824) - h & replayAllowed; - else - replayAllowed = false; - } - - ////////////////////////////////////////////////////////////////////////// - BattleInfo(); - virtual ~BattleInfo(); - - const IBattleInfo * getBattle() const override; - std::optional getPlayerID() const override; - - ////////////////////////////////////////////////////////////////////////// - // IBattleInfo - - BattleID getBattleID() const override; - - int32_t getActiveStackID() const override; - - TStacks getStacksIf(TStackFilter predicate) const override; - - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; - - BattleField getBattlefieldType() const override; - TerrainId getTerrainType() const override; - - ObstacleCList getAllObstacles() const override; - - PlayerColor getSidePlayer(ui8 side) const override; - const CArmedInstance * getSideArmy(ui8 side) const override; - const CGHeroInstance * getSideHero(ui8 side) const override; - - ui8 getTacticDist() const override; - ui8 getTacticsSide() const override; - - const CGTownInstance * getDefendedTown() const override; - EWallState getWallState(EWallPart partOfWall) const override; - EGateState getGateState() const override; - - uint32_t getCastSpells(ui8 side) const override; - int32_t getEnchanterCounter(ui8 side) const override; - - const IBonusBearer * getBonusBearer() const override; - - uint32_t nextUnitId() const override; - - int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; - - int3 getLocation() const override; - bool isCreatureBank() const override; - - std::vector getUsedSpells(ui8 side) const override; - - ////////////////////////////////////////////////////////////////////////// - // IBattleState - - void nextRound() override; - void nextTurn(uint32_t unitId) override; - - void addUnit(uint32_t id, const JsonNode & data) override; - void moveUnit(uint32_t id, BattleHex destination) override; - void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; - void removeUnit(uint32_t id) override; - void updateUnit(uint32_t id, const JsonNode & data) override; - - void addUnitBonus(uint32_t id, const std::vector & bonus) override; - void updateUnitBonus(uint32_t id, const std::vector & bonus) override; - void removeUnitBonus(uint32_t id, const std::vector & bonus) override; - - void setWallState(EWallPart partOfWall, EWallState state) override; - - void addObstacle(const ObstacleChanges & changes) override; - void updateObstacle(const ObstacleChanges& changes) override; - void removeObstacle(uint32_t id) override; - - static void addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd); - - ////////////////////////////////////////////////////////////////////////// - CStack * getStack(int stackID, bool onlyAlive = true); - using CBattleInfoEssentials::battleGetArmyObject; - CArmedInstance * battleGetArmyObject(ui8 side) const; - using CBattleInfoEssentials::battleGetFightingHero; - CGHeroInstance * battleGetFightingHero(ui8 side) const; - - CStack * generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position); - CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position); - - const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player - - void localInit(); - static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town); - - ui8 whatSide(const PlayerColor & player) const; - -protected: -#if SCRIPTING_ENABLED - scripting::Pool * getContextPool() const override; -#endif -}; - - -class DLL_LINKAGE CMP_stack -{ - int phase; //rules of which phase will be used - int turn; - uint8_t side; -public: - bool operator()(const battle::Unit * a, const battle::Unit * b) const; - CMP_stack(int Phase = 1, int Turn = 0, uint8_t Side = BattleSide::ATTACKER); -}; - -VCMI_LIB_NAMESPACE_END +/* + * BattleInfo.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 "../int3.h" +#include "../bonuses/Bonus.h" +#include "../bonuses/CBonusSystemNode.h" +#include "CBattleInfoCallback.h" +#include "IBattleState.h" +#include "SiegeInfo.h" +#include "SideInBattle.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CStack; +class CStackInstance; +class CStackBasicDescriptor; +class BattleField; + +class DLL_LINKAGE BattleInfo : public CBonusSystemNode, public CBattleInfoCallback, public IBattleState +{ +public: + BattleID battleID = BattleID(0); + + enum BattleSide + { + ATTACKER = 0, + DEFENDER + }; + std::array sides; //sides[0] - attacker, sides[1] - defender + si32 round, activeStack; + const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege) + int3 tile; //for background and bonuses + bool creatureBank; //auxilary field, do not serialize + bool replayAllowed; + std::vector stacks; + std::vector > obstacles; + SiegeInfo si; + + BattleField battlefieldType; //like !!BA:B + TerrainId terrainType; //used for some stack nativity checks (not the bonus limiters though that have their own copy) + + ui8 tacticsSide; //which side is requested to play tactics phase + ui8 tacticDistance; //how many hexes we can go forward (1 = only hexes adjacent to margin line) + + template void serialize(Handler &h, const int version) + { + h & battleID; + h & sides; + h & round; + h & activeStack; + h & town; + h & tile; + h & stacks; + h & obstacles; + h & si; + h & battlefieldType; + h & terrainType; + h & tacticsSide; + h & tacticDistance; + h & static_cast(*this); + if (version > 824) + h & replayAllowed; + else + replayAllowed = false; + } + + ////////////////////////////////////////////////////////////////////////// + BattleInfo(); + virtual ~BattleInfo(); + + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + + ////////////////////////////////////////////////////////////////////////// + // IBattleInfo + + BattleID getBattleID() const override; + + int32_t getActiveStackID() const override; + + TStacks getStacksIf(TStackFilter predicate) const override; + + battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + + BattleField getBattlefieldType() const override; + TerrainId getTerrainType() const override; + + ObstacleCList getAllObstacles() const override; + + PlayerColor getSidePlayer(ui8 side) const override; + const CArmedInstance * getSideArmy(ui8 side) const override; + const CGHeroInstance * getSideHero(ui8 side) const override; + + ui8 getTacticDist() const override; + ui8 getTacticsSide() const override; + + const CGTownInstance * getDefendedTown() const override; + EWallState getWallState(EWallPart partOfWall) const override; + EGateState getGateState() const override; + + uint32_t getCastSpells(ui8 side) const override; + int32_t getEnchanterCounter(ui8 side) const override; + + const IBonusBearer * getBonusBearer() const override; + + uint32_t nextUnitId() const override; + + int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; + + int3 getLocation() const override; + bool isCreatureBank() const override; + + std::vector getUsedSpells(ui8 side) const override; + + ////////////////////////////////////////////////////////////////////////// + // IBattleState + + void nextRound() override; + void nextTurn(uint32_t unitId) override; + + void addUnit(uint32_t id, const JsonNode & data) override; + void moveUnit(uint32_t id, BattleHex destination) override; + void setUnitState(uint32_t id, const JsonNode & data, int64_t healthDelta) override; + void removeUnit(uint32_t id) override; + void updateUnit(uint32_t id, const JsonNode & data) override; + + void addUnitBonus(uint32_t id, const std::vector & bonus) override; + void updateUnitBonus(uint32_t id, const std::vector & bonus) override; + void removeUnitBonus(uint32_t id, const std::vector & bonus) override; + + void setWallState(EWallPart partOfWall, EWallState state) override; + + void addObstacle(const ObstacleChanges & changes) override; + void updateObstacle(const ObstacleChanges& changes) override; + void removeObstacle(uint32_t id) override; + + static void addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool forceAdd); + + ////////////////////////////////////////////////////////////////////////// + CStack * getStack(int stackID, bool onlyAlive = true); + using CBattleInfoEssentials::battleGetArmyObject; + CArmedInstance * battleGetArmyObject(ui8 side) const; + using CBattleInfoEssentials::battleGetFightingHero; + CGHeroInstance * battleGetFightingHero(ui8 side) const; + + CStack * generateNewStack(uint32_t id, const CStackInstance & base, ui8 side, const SlotID & slot, BattleHex position); + CStack * generateNewStack(uint32_t id, const CStackBasicDescriptor & base, ui8 side, const SlotID & slot, BattleHex position); + + const CGHeroInstance * getHero(const PlayerColor & player) const; //returns fighting hero that belongs to given player + + void localInit(); + static BattleInfo * setupBattle(const int3 & tile, TerrainId, const BattleField & battlefieldType, const CArmedInstance * armies[2], const CGHeroInstance * heroes[2], bool creatureBank, const CGTownInstance * town); + + ui8 whatSide(const PlayerColor & player) const; + +protected: +#if SCRIPTING_ENABLED + scripting::Pool * getContextPool() const override; +#endif +}; + + +class DLL_LINKAGE CMP_stack +{ + int phase; //rules of which phase will be used + int turn; + uint8_t side; +public: + bool operator()(const battle::Unit * a, const battle::Unit * b) const; + CMP_stack(int Phase = 1, int Turn = 0, uint8_t Side = BattleSide::ATTACKER); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index b8876b946..239e51559 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1,1880 +1,1880 @@ -/* - * CBattleInfoCallback.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 "CBattleInfoCallback.h" - -#include - -#include "../CStack.h" -#include "BattleInfo.h" -#include "CObstacleInstance.h" -#include "DamageCalculator.h" -#include "PossiblePlayerBattleAction.h" -#include "../NetPacks.h" -#include "../spells/ObstacleCasterProxy.h" -#include "../spells/ISpellMechanics.h" -#include "../spells/Problem.h" -#include "../spells/CSpellHandler.h" -#include "../mapObjects/CGTownInstance.h" -#include "../BattleFieldHandler.h" -#include "../Rect.h" - -VCMI_LIB_NAMESPACE_BEGIN - -namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO -{ - -static BattleHex lineToWallHex(int line) //returns hex with wall in given line (y coordinate) -{ - static const BattleHex lineToHex[] = {12, 29, 45, 62, 78, 96, 112, 130, 147, 165, 182}; - - return lineToHex[line]; -} - -static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) -{ - const int wallInStackLine = lineToWallHex(pos1.getY()); - const int wallInDestLine = lineToWallHex(pos2.getY()); - - const bool stackLeft = pos1 < wallInStackLine; - const bool destLeft = pos2 < wallInDestLine; - - return stackLeft == destLeft; -} - -// parts of wall -static const std::pair wallParts[] = -{ - std::make_pair(50, EWallPart::KEEP), - std::make_pair(183, EWallPart::BOTTOM_TOWER), - std::make_pair(182, EWallPart::BOTTOM_WALL), - std::make_pair(130, EWallPart::BELOW_GATE), - std::make_pair(78, EWallPart::OVER_GATE), - std::make_pair(29, EWallPart::UPPER_WALL), - std::make_pair(12, EWallPart::UPPER_TOWER), - std::make_pair(95, EWallPart::INDESTRUCTIBLE_PART_OF_GATE), - std::make_pair(96, EWallPart::GATE), - std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(62, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART), - std::make_pair(165, EWallPart::INDESTRUCTIBLE_PART) -}; - -static EWallPart hexToWallPart(BattleHex hex) -{ - for(const auto & elem : wallParts) - { - if(elem.first == hex) - return elem.second; - } - - return EWallPart::INVALID; //not found! -} - -static BattleHex WallPartToHex(EWallPart part) -{ - for(const auto & elem : wallParts) - { - if(elem.second == part) - return elem.first; - } - - return BattleHex::INVALID; //not found! -} -} - -using namespace SiegeStuffThatShouldBeMovedToHandlers; - -ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const -{ - RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); - if(caster == nullptr) - { - logGlobal->error("CBattleInfoCallback::battleCanCastSpell: no spellcaster."); - return ESpellCastProblem::INVALID; - } - const PlayerColor player = caster->getCasterOwner(); - const auto side = playerToSide(player); - if(!side) - return ESpellCastProblem::INVALID; - if(!battleDoWeKnowAbout(side.value())) - { - logGlobal->warn("You can't check if enemy can cast given spell!"); - return ESpellCastProblem::INVALID; - } - - if(battleTacticDist()) - return ESpellCastProblem::ONGOING_TACTIC_PHASE; - - switch(mode) - { - case spells::Mode::HERO: - { - if(battleCastSpells(side.value()) > 0) - return ESpellCastProblem::CASTS_PER_TURN_LIMIT; - - const auto * hero = dynamic_cast(caster); - - if(!hero) - return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; - if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)) - return ESpellCastProblem::MAGIC_IS_BLOCKED; - } - break; - default: - break; - } - - return ESpellCastProblem::OK; -} - -std::pair< std::vector, int > CBattleInfoCallback::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const -{ - auto reachability = getReachability(stack); - - if(reachability.predecessors[dest] == -1) //cannot reach destination - { - return std::make_pair(std::vector(), 0); - } - - //making the Path - std::vector path; - BattleHex curElem = dest; - while(curElem != start) - { - path.push_back(curElem); - curElem = reachability.predecessors[curElem]; - } - - return std::make_pair(path, reachability.distances[dest]); -} - -bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const -{ - auto isTileBlocked = [&](BattleHex tile) - { - EWallPart wallPart = battleHexToWallPart(tile); - if (wallPart == EWallPart::INVALID) - return false; // there is no wall here - if (wallPart == EWallPart::INDESTRUCTIBLE_PART_OF_GATE) - return false; // does not blocks ranged attacks - if (wallPart == EWallPart::INDESTRUCTIBLE_PART) - return true; // always blocks ranged attacks - - return isWallPartAttackable(wallPart); - }; - // Count wall penalty requirement by shortest path, not by arbitrary line, to avoid various OH3 bugs - auto getShortestPath = [](BattleHex from, BattleHex dest) -> std::vector - { - //Out early - if(from == dest) - return {}; - - std::vector ret; - auto next = from; - //Not a real direction, only to indicate to which side we should search closest tile - auto direction = from.getX() > dest.getX() ? BattleSide::DEFENDER : BattleSide::ATTACKER; - - while (next != dest) - { - auto tiles = next.neighbouringTiles(); - std::set possibilities = {tiles.begin(), tiles.end()}; - next = BattleHex::getClosestTile(direction, dest, possibilities); - ret.push_back(next); - } - assert(!ret.empty()); - ret.pop_back(); //Remove destination hex - return ret; - }; - - RETURN_IF_NOT_BATTLE(false); - auto checkNeeded = !sameSideOfWall(from, dest); - bool pathHasWall = false; - bool pathHasMoat = false; - - for(const auto & hex : getShortestPath(from, dest)) - { - pathHasWall |= isTileBlocked(hex); - if(!checkMoat) - continue; - - auto obstacles = battleGetAllObstaclesOnPos(hex, false); - - if(hex != BattleHex::GATE_BRIDGE || (battleIsGatePassable())) - for(const auto & obst : obstacles) - if(obst->obstacleType == CObstacleInstance::MOAT) - pathHasMoat |= true; - } - - return checkNeeded && ( (checkWall && pathHasWall) || (checkMoat && pathHasMoat) ); -} - -bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const -{ - RETURN_IF_NOT_BATTLE(false); - if(!battleGetSiegeLevel()) - return false; - - const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY"; - static const auto selectorNoWallPenalty = Selector::type()(BonusType::NO_WALL_PENALTY); - - if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty)) - return false; - - const auto shooterOutsideWalls = shooterPosition < lineToWallHex(shooterPosition.getY()); - - return shooterOutsideWalls && battleHasPenaltyOnLine(shooterPosition, destHex, true, false); -} - -std::vector CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data) -{ - RETURN_IF_NOT_BATTLE(std::vector()); - std::vector allowedActionList; - if(data.tacticsMode) //would "if(battleGetTacticDist() > 0)" work? - { - allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_TACTICS); - allowedActionList.push_back(PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK); - } - else - { - if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? - { - if(stack->hasBonusOfType(BonusType::SPELLCASTER)) - { - for(const auto & spellID : data.creatureSpellsToCast) - { - const CSpell *spell = spellID.toSpell(); - PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); - allowedActionList.push_back(act); - } - } - if(stack->hasBonusOfType(BonusType::RANDOM_SPELLCASTER)) - allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); - } - if(stack->canShoot()) - allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT); - if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) - allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN); - - allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack - allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere - - if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks - allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); - - const auto * siegedTown = battleGetDefendedTown(); - if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots - allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT); - if(stack->hasBonusOfType(BonusType::HEALER)) - allowedActionList.push_back(PossiblePlayerBattleAction::HEAL); - } - - return allowedActionList; -} - -PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const -{ - RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID); - auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; - - const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); - - if(ti.massive || ti.type == spells::AimType::NO_TARGET) - spellSelMode = PossiblePlayerBattleAction::NO_LOCATION; - else if(ti.type == spells::AimType::LOCATION && ti.clearAffected) - spellSelMode = PossiblePlayerBattleAction::FREE_LOCATION; - else if(ti.type == spells::AimType::CREATURE) - spellSelMode = PossiblePlayerBattleAction::AIMED_SPELL_CREATURE; - else if(ti.type == spells::AimType::OBSTACLE) - spellSelMode = PossiblePlayerBattleAction::OBSTACLE; - - return PossiblePlayerBattleAction(spellSelMode, spell->id); -} - -std::set CBattleInfoCallback::battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const -{ - std::set attackedHexes; - RETURN_IF_NOT_BATTLE(attackedHexes); - - AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - for (BattleHex tile : at.hostileCreaturePositions) - { - const auto * st = battleGetUnitByPos(tile, true); - if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? - { - attackedHexes.insert(tile); - } - } - for (BattleHex tile : at.friendlyCreaturePositions) - { - if(battleGetUnitByPos(tile, true)) //friendly stacks can also be damaged by Dragon Breath - { - attackedHexes.insert(tile); - } - } - return attackedHexes; -} - -SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const -{ - switch (mode) - { - case RANDOM_GENIE: - return getRandomBeneficialSpell(rand, stack); //target - break; - case RANDOM_AIMED: - return getRandomCastedSpell(rand, stack); //caster - break; - default: - logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast(mode)); - return SpellID::NONE; - } -} - -const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - for(const auto * s : battleGetAllStacks(true)) - if(vstd::contains(s->getHexes(), pos) && (!onlyAlive || s->alive())) - return s; - - return nullptr; -} - -const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - auto ret = battleGetUnitsIf([=](const battle::Unit * unit) - { - return !unit->isGhost() - && vstd::contains(battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()), pos) - && (!onlyAlive || unit->alive()); - }); - - if(!ret.empty()) - return ret.front(); - else - return nullptr; -} - -battle::Units CBattleInfoCallback::battleAliveUnits() const -{ - return battleGetUnitsIf([](const battle::Unit * unit) - { - return unit->isValidTarget(false); - }); -} - -battle::Units CBattleInfoCallback::battleAliveUnits(ui8 side) const -{ - return battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->isValidTarget(false) && unit->unitSide() == side; - }); -} - -using namespace battle; - -//T is battle::Unit descendant -template -const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & sideThatLastMoved, int phase) -{ - const T * returnedUnit = nullptr; - size_t currentUnitIndex = 0; - - for(size_t i = 0; i < allUnits.size(); i++) - { - int32_t currentUnitInitiative = -1; - int32_t returnedUnitInitiative = -1; - - if(returnedUnit) - returnedUnitInitiative = returnedUnit->getInitiative(turn); - - if(!allUnits[i]) - continue; - - auto currentUnit = allUnits[i]; - currentUnitInitiative = currentUnit->getInitiative(turn); - - switch(phase) - { - case BattlePhases::NORMAL: // Faster first, attacker priority, higher slot first - if(returnedUnit == nullptr || currentUnitInitiative > returnedUnitInitiative) - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - else if(currentUnitInitiative == returnedUnitInitiative) - { - if(sideThatLastMoved == -1 && turn <= 0 && currentUnit->unitSide() == BattleSide::ATTACKER - && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Turn 0 attacker priority - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - else if(sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved - && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - } - break; - case BattlePhases::WAIT_MORALE: // Slower first, higher slot first - case BattlePhases::WAIT: - if(returnedUnit == nullptr || currentUnitInitiative < returnedUnitInitiative) - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved - && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units - { - returnedUnit = currentUnit; - currentUnitIndex = i; - } - break; - default: - break; - } - } - - if(!returnedUnit) - return nullptr; - - allUnits[currentUnitIndex] = nullptr; - - return returnedUnit; -} - -void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, int8_t sideThatLastMoved) const -{ - RETURN_IF_NOT_BATTLE(); - - if(maxUnits == 0 && maxTurns == 0) - { - logGlobal->error("Attempt to get infinite battle queue"); - return; - } - - auto actualTurn = turn > 0 ? turn : 0; - - auto turnsIsFull = [&]() -> bool - { - if(maxUnits == 0) - return false;//no limit - - size_t turnsSize = 0; - for(const auto & oneTurn : turns) - turnsSize += oneTurn.size(); - return turnsSize >= maxUnits; - }; - - turns.emplace_back(); - - // We'll split creatures with remaining movement to 4 buckets (SIEGE, NORMAL, WAIT_MORALE, WAIT) - std::array phases; // Access using BattlePhases enum - - const battle::Unit * activeUnit = battleActiveUnit(); - - if(activeUnit) - { - //its first turn and active unit hasn't taken any action yet - must be placed at the beginning of queue, no matter what - if(turn == 0 && activeUnit->willMove() && !activeUnit->waited()) - { - turns.back().push_back(activeUnit); - if(turnsIsFull()) - return; - } - - //its first or current turn, turn priority for active stack side - //TODO: what if active stack mind-controlled? - if(turn <= 0 && sideThatLastMoved < 0) - sideThatLastMoved = activeUnit->unitSide(); - } - - auto allUnits = battleGetUnitsIf([](const battle::Unit * unit) - { - return !unit->isGhost(); - }); - - // If no unit will be EVER! able to move, battle is over. - if(!vstd::contains_if(allUnits, [](const battle::Unit * unit) { return unit->willMove(100000); })) //little evil, but 100000 should be enough for all effects to disappear - { - turns.clear(); - return; - } - - for(const auto * unit : allUnits) - { - if((actualTurn == 0 && !unit->willMove()) //we are considering current round and unit won't move - || (actualTurn > 0 && !unit->canMove(turn)) //unit won't be able to move in later rounds - || (actualTurn == 0 && unit == activeUnit && !turns.at(0).empty() && unit == turns.front().front())) //it's active unit already added at the beginning of queue - { - continue; - } - - int unitPhase = unit->battleQueuePhase(turn); - - phases[unitPhase].push_back(unit); - } - - boost::sort(phases[BattlePhases::SIEGE], CMP_stack(BattlePhases::SIEGE, actualTurn, sideThatLastMoved)); - std::copy(phases[BattlePhases::SIEGE].begin(), phases[BattlePhases::SIEGE].end(), std::back_inserter(turns.back())); - - if(turnsIsFull()) - return; - - for(uint8_t phase = BattlePhases::NORMAL; phase < BattlePhases::NUMBER_OF_PHASES; phase++) - boost::sort(phases[phase], CMP_stack(phase, actualTurn, sideThatLastMoved)); - - uint8_t phase = BattlePhases::NORMAL; - while(!turnsIsFull() && phase < BattlePhases::NUMBER_OF_PHASES) - { - const battle::Unit * currentUnit = nullptr; - if(phases[phase].empty()) - phase++; - else - { - currentUnit = takeOneUnit(phases[phase], actualTurn, sideThatLastMoved, phase); - if(!currentUnit) - { - phase++; - } - else - { - turns.back().push_back(currentUnit); - sideThatLastMoved = currentUnit->unitSide(); - } - } - } - - if(sideThatLastMoved < 0) - sideThatLastMoved = BattleSide::ATTACKER; - - if(!turnsIsFull() && (maxTurns == 0 || turns.size() < maxTurns)) - battleGetTurnOrder(turns, maxUnits, maxTurns, actualTurn + 1, sideThatLastMoved); -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const -{ - - RETURN_IF_NOT_BATTLE(std::vector()); - if(!unit->getPosition().isValid()) //turrets - return std::vector(); - - auto reachability = getReachability(unit); - - return battleGetAvailableHexes(reachability, unit, obtainMovementRange); -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const -{ - std::vector ret; - - RETURN_IF_NOT_BATTLE(ret); - if(!unit->getPosition().isValid()) //turrets - return ret; - - auto unitSpeed = unit->speed(0, true); - - const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); - - for(int i = 0; i < GameConstants::BFIELD_SIZE; ++i) - { - // If obstacles or other stacks makes movement impossible, it can't be helped. - if(!cache.isReachable(i)) - continue; - - if(tacticsPhase && !obtainMovementRange) // if obtainMovementRange requested do not return tactics range - { - // Stack has to perform tactic-phase movement -> can enter any reachable tile within given range - if(!isInTacticRange(i)) - continue; - } - else - { - // Not tactics phase -> destination must be reachable and within unit range. - if(cache.distances[i] > static_cast(unitSpeed)) - continue; - } - - ret.emplace_back(i); - } - - return ret; -} - -std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const -{ - std::vector ret = battleGetAvailableHexes(unit, obtainMovementRange); - - if(ret.empty()) - return ret; - - if(addOccupiable && unit->doubleWide()) - { - std::vector occupiable; - - occupiable.reserve(ret.size()); - for(auto hex : ret) - occupiable.push_back(unit->occupiedHex(hex)); - - vstd::concatenate(ret, occupiable); - } - - - if(attackable) - { - auto meleeAttackable = [&](BattleHex hex) -> bool - { - // Return true if given hex has at least one available neighbour. - // Available hexes are already present in ret vector. - auto availableNeighbor = boost::find_if(ret, [=] (BattleHex availableHex) - { - return BattleHex::mutualPosition(hex, availableHex) >= 0; - }); - return availableNeighbor != ret.end(); - }; - for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide()))) - { - if(!otherSt->isValidTarget(false)) - continue; - - std::vector occupied = otherSt->getHexes(); - - if(battleCanShoot(unit, otherSt->getPosition())) - { - attackable->insert(attackable->end(), occupied.begin(), occupied.end()); - continue; - } - - for(BattleHex he : occupied) - { - if(meleeAttackable(he)) - attackable->push_back(he); - } - } - } - - //adding occupiable likely adds duplicates to ret -> clean it up - boost::sort(ret); - ret.erase(boost::unique(ret).end(), ret.end()); - return ret; -} - -bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(battleTacticDist()) - return false; - - if (!stack || !target) - return false; - - if(!battleMatchOwner(stack, target)) - return false; - - auto id = stack->unitType()->getId(); - if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) - return false; - - return target->alive(); -} - -bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(battleTacticDist()) //no shooting during tactics - return false; - - if (!attacker) - return false; - if (attacker->creatureIndex() == CreatureID::CATAPULT) //catapult cannot attack creatures - return false; - - //forgetfulness - TConstBonusListPtr forgetfulList = attacker->getBonuses(Selector::type()(BonusType::FORGETFULL)); - if(!forgetfulList->empty()) - { - int forgetful = forgetfulList->valOfBonuses(Selector::type()(BonusType::FORGETFULL)); - - //advanced+ level - if(forgetful > 1) - return false; - } - - return attacker->canShoot() && (!battleIsUnitBlocked(attacker) - || attacker->hasBonusOfType(BonusType::FREE_SHOOTING)); -} - -bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - - const battle::Unit * defender = battleGetUnitByPos(dest); - if(!attacker || !defender) - return false; - - if(battleMatchOwner(attacker, defender) && defender->alive()) - { - if(battleCanShoot(attacker)) - { - auto limitedRangeBonus = attacker->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); - if(limitedRangeBonus == nullptr) - { - return true; - } - - int shootingRange = limitedRangeBonus->val; - return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); - } - } - - return false; -} - -DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const -{ - DamageCalculator calculator(*this, info); - - return calculator.calculateDmgRange(); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - auto reachability = battleGetDistances(attacker, attacker->getPosition()); - int movementDistance = reachability[attackerPosition]; - return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - const bool shooting = battleCanShoot(attacker, defender->getPosition()); - const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); - return battleEstimateDamage(bai, retaliationDmg); -} - -DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const -{ - RETURN_IF_NOT_BATTLE({}); - - DamageEstimation ret = calculateDmgRange(bai); - - if(retaliationDmg) - { - if(bai.shooting) - { - //FIXME: handle RANGED_RETALIATION - *retaliationDmg = DamageEstimation(); - } - else - { - //TODO: rewrite using boost::numeric::interval - //TODO: rewire once more using interval-based fuzzy arithmetic - - const auto & estimateRetaliation = [&](int64_t damage) - { - auto retaliationAttack = bai.reverse(); - auto state = retaliationAttack.attacker->acquireState(); - state->damage(damage); - retaliationAttack.attacker = state.get(); - return calculateDmgRange(retaliationAttack); - }; - - DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); - DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); - - retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); - retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); - - retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); - retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); - } - } - - return ret; -} - -std::vector> CBattleInfoCallback::battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking) const -{ - std::vector> obstacles = std::vector>(); - RETURN_IF_NOT_BATTLE(obstacles); - for(auto & obs : battleGetAllObstacles()) - { - if(vstd::contains(obs->getBlockedTiles(), tile) - || (!onlyBlocking && vstd::contains(obs->getAffectedTiles(), tile))) - { - obstacles.push_back(obs); - } - } - return obstacles; -} - -std::vector> CBattleInfoCallback::getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const -{ - auto affectedObstacles = std::vector>(); - RETURN_IF_NOT_BATTLE(affectedObstacles); - if(unit->alive()) - { - if(!passed.count(unit->getPosition())) - affectedObstacles = battleGetAllObstaclesOnPos(unit->getPosition(), false); - if(unit->doubleWide()) - { - BattleHex otherHex = unit->occupiedHex(); - if(otherHex.isValid() && !passed.count(otherHex)) - for(auto & i : battleGetAllObstaclesOnPos(otherHex, false)) - if(!vstd::contains(affectedObstacles, i)) - affectedObstacles.push_back(i); - } - for(auto hex : unit->getHexes()) - if(hex == BattleHex::GATE_BRIDGE && battleIsGatePassable()) - for(int i=0; iobstacleType == CObstacleInstance::MOAT) - affectedObstacles.erase(affectedObstacles.begin()+i); - } - return affectedObstacles; -} - -bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed) const -{ - if(!unit.alive()) - return false; - bool movementStopped = false; - for(auto & obstacle : getAllAffectedObstaclesByStack(&unit, passed)) - { - //helper info - const SpellCreatedObstacle * spellObstacle = dynamic_cast(obstacle.get()); - - if(spellObstacle) - { - auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void - { - // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage - auto operation = ObstacleChanges::EOperation::UPDATE; - if (spellObstacle.removeOnTrigger) - operation = ObstacleChanges::EOperation::REMOVE; - - SpellCreatedObstacle changedObstacle; - changedObstacle.uniqueID = spellObstacle.uniqueID; - changedObstacle.revealed = true; - - BattleObstaclesChanged bocp; - bocp.battleID = getBattle()->getBattleID(); - bocp.changes.emplace_back(spellObstacle.uniqueID, operation); - changedObstacle.toInfo(bocp.changes.back(), operation); - spellEnv.apply(&bocp); - }; - const auto side = unit.unitSide(); - auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side); - const auto * hero = battleGetFightingHero(spellObstacle->casterSide); - auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); - const auto * sp = obstacle->getTrigger().toSpell(); - if(obstacle->triggersEffects() && sp) - { - auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp); - spells::detail::ProblemImpl ignored; - auto target = spells::Target(1, spells::Destination(&unit)); - if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures - { - if(shouldReveal) { //hidden obstacle triggers effects after revealed - revealObstacles(*spellObstacle); - cast.cast(&spellEnv, target); - } - } - } - else if(shouldReveal) - revealObstacles(*spellObstacle); - } - - if(!unit.alive()) - return false; - - if(obstacle->stopsMovement()) - movementStopped = true; - } - - return unit.alive() && !movementStopped; -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility() const -{ - AccessibilityInfo ret; - ret.fill(EAccessibility::ACCESSIBLE); - - //removing accessibility for side columns of hexes - for(int y = 0; y < GameConstants::BFIELD_HEIGHT; y++) - { - ret[BattleHex(GameConstants::BFIELD_WIDTH - 1, y)] = EAccessibility::SIDE_COLUMN; - ret[BattleHex(0, y)] = EAccessibility::SIDE_COLUMN; - } - - //special battlefields with logically unavailable tiles - auto bFieldType = battleGetBattlefieldType(); - - if(bFieldType != BattleField::NONE) - { - std::vector impassableHexes = bFieldType.getInfo()->impassableHexes; - - for(auto hex : impassableHexes) - ret[hex] = EAccessibility::UNAVAILABLE; - } - - //gate -> should be before stacks - if(battleGetSiegeLevel() > 0) - { - EAccessibility accessability = EAccessibility::ACCESSIBLE; - switch(battleGetGateState()) - { - case EGateState::CLOSED: - accessability = EAccessibility::GATE; - break; - - case EGateState::BLOCKED: - accessability = EAccessibility::UNAVAILABLE; - break; - } - ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessability; - } - - //tiles occupied by standing stacks - for(const auto * unit : battleAliveUnits()) - { - for(auto hex : unit->getHexes()) - if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns - ret[hex] = EAccessibility::ALIVE_STACK; - } - - //obstacles - for(const auto &obst : battleGetAllObstacles()) - { - for(auto hex : obst->getBlockedTiles()) - ret[hex] = EAccessibility::OBSTACLE; - } - - //walls - if(battleGetSiegeLevel() > 0) - { - static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165}; - for(auto hex : permanentlyLocked) - ret[hex] = EAccessibility::UNAVAILABLE; - - //TODO likely duplicated logic - static const std::pair lockedIfNotDestroyed[] = - { - //which part of wall, which hex is blocked if this part of wall is not destroyed - std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_4)), - std::make_pair(EWallPart::BELOW_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_3)), - std::make_pair(EWallPart::OVER_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_2)), - std::make_pair(EWallPart::UPPER_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_1)) - }; - - for(const auto & elem : lockedIfNotDestroyed) - { - if(battleGetWallState(elem.first) != EWallState::DESTROYED) - ret[elem.second] = EAccessibility::DESTRUCTIBLE_WALL; - } - } - - return ret; -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility(const battle::Unit * stack) const -{ - return getAccesibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); -} - -AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector & accessibleHexes) const -{ - auto ret = getAccesibility(); - for(auto hex : accessibleHexes) - if(hex.isValid()) - ret[hex] = EAccessibility::ACCESSIBLE; - - return ret; -} - -ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters & params) const -{ - ReachabilityInfo ret; - ret.accessibility = accessibility; - ret.params = params; - - ret.predecessors.fill(BattleHex::INVALID); - ret.distances.fill(ReachabilityInfo::INFINITE_DIST); - - if(!params.startPosition.isValid()) //if got call for arrow turrets - return ret; - - const std::set obstacles = getStoppers(params.perspective); - auto checkParams = params; - checkParams.ignoreKnownAccessible = true; //Ignore starting hexes obstacles - - std::queue hexq; //bfs queue - - //first element - hexq.push(params.startPosition); - ret.distances[params.startPosition] = 0; - - std::array accessibleCache{}; - for(int hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) - accessibleCache[hex] = accessibility.accessible(hex, params.doubleWide, params.side); - - while(!hexq.empty()) //bfs loop - { - const BattleHex curHex = hexq.front(); - hexq.pop(); - - //walking stack can't step past the obstacles - if(isInObstacle(curHex, obstacles, checkParams)) - continue; - - const int costToNeighbour = ret.distances[curHex.hex] + 1; - for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) - { - if(neighbour.isValid()) - { - const int costFoundSoFar = ret.distances[neighbour.hex]; - - if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) - { - hexq.push(neighbour); - ret.distances[neighbour.hex] = costToNeighbour; - ret.predecessors[neighbour.hex] = curHex; - } - } - } - } - - return ret; -} - -bool CBattleInfoCallback::isInObstacle( - BattleHex hex, - const std::set & obstacles, - const ReachabilityInfo::Parameters & params) const -{ - auto occupiedHexes = battle::Unit::getHexes(hex, params.doubleWide, params.side); - - for(auto occupiedHex : occupiedHexes) - { - if(params.ignoreKnownAccessible && vstd::contains(params.knownAccessible, occupiedHex)) - continue; - - if(vstd::contains(obstacles, occupiedHex)) - { - if(occupiedHex == BattleHex::GATE_BRIDGE) - { - if(battleGetGateState() != EGateState::DESTROYED && params.side == BattleSide::ATTACKER) - return true; - } - else - return true; - } - } - - return false; -} - -std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const -{ - std::set ret; - RETURN_IF_NOT_BATTLE(ret); - - for(auto &oi : battleGetAllObstacles(whichSidePerspective)) - { - if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) - continue; - - for(const auto & hex : oi->getStoppingTile()) - { - if(hex == BattleHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) - { - if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) - continue; // this tile is disabled by drawbridge on top of it - } - ret.insert(hex); - } - } - return ret; -} - -std::pair CBattleInfoCallback::getNearestStack(const battle::Unit * closest) const -{ - auto reachability = getReachability(closest); - auto avHexes = battleGetAvailableHexes(reachability, closest, false); - - // I hate std::pairs with their undescriptive member names first / second - struct DistStack - { - uint32_t distanceToPred; - BattleHex destination; - const battle::Unit * stack; - }; - - std::vector stackPairs; - - std::vector possible = battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->isValidTarget(false) && unit != closest; - }); - - for(const battle::Unit * st : possible) - { - for(BattleHex hex : avHexes) - if(CStack::isMeleeAttackPossible(closest, st, hex)) - { - DistStack hlp = {reachability.distances[hex], hex, st}; - stackPairs.push_back(hlp); - } - } - - if(!stackPairs.empty()) - { - auto comparator = [](DistStack lhs, DistStack rhs) { return lhs.distanceToPred < rhs.distanceToPred; }; - auto minimal = boost::min_element(stackPairs, comparator); - return std::make_pair(minimal->stack, minimal->destination); - } - else - return std::make_pair(nullptr, BattleHex::INVALID); -} - -BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const -{ - bool twoHex = VLC->creh->objects[creID]->isDoubleWide(); - - int pos; - if (initialPos > -1) - pos = initialPos; - else //summon elementals depending on player side - { - if(side == BattleSide::ATTACKER) - pos = 0; //top left - else - pos = GameConstants::BFIELD_WIDTH - 1; //top right - } - - auto accessibility = getAccesibility(); - - std::set occupyable; - for(int i = 0; i < accessibility.size(); i++) - if(accessibility.accessible(i, twoHex, side)) - occupyable.insert(i); - - if(occupyable.empty()) - { - return BattleHex::INVALID; //all tiles are covered - } - - return BattleHex::getClosestTile(side, pos, occupyable); -} - -si8 CBattleInfoCallback::battleGetTacticDist() const -{ - RETURN_IF_NOT_BATTLE(0); - - //TODO get rid of this method - if(battleDoWeKnowAbout(battleGetTacticsSide())) - return battleTacticDist(); - - return 0; -} - -bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const -{ - RETURN_IF_NOT_BATTLE(false); - auto side = battleGetTacticsSide(); - auto dist = battleGetTacticDist(); - - return ((!side && dest.getX() > 0 && dest.getX() <= dist) - || (side && dest.getX() < GameConstants::BFIELD_WIDTH - 1 && dest.getX() >= GameConstants::BFIELD_WIDTH - dist - 1)); -} - -ReachabilityInfo CBattleInfoCallback::getReachability(const battle::Unit * unit) const -{ - ReachabilityInfo::Parameters params(unit, unit->getPosition()); - - if(!battleDoWeKnowAbout(unit->unitSide())) - { - //Stack is held by enemy, we can't use his perspective to check for reachability. - // Happens ie. when hovering enemy stack for its range. The arg could be set properly, but it's easier to fix it here. - params.perspective = battleGetMySide(); - } - - return getReachability(params); -} - -ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Parameters ¶ms) const -{ - if(params.flying) - return getFlyingReachability(params); - else - return makeBFS(getAccesibility(params.knownAccessible), params); -} - -ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const -{ - ReachabilityInfo ret; - ret.accessibility = getAccesibility(params.knownAccessible); - - for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) - { - if(ret.accessibility.accessible(i, params.doubleWide, params.side)) - { - ret.predecessors[i] = params.startPosition; - ret.distances[i] = BattleHex::getDistance(params.startPosition, i); - } - } - - return ret; -} - -AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const -{ - //does not return hex attacked directly - AttackableTiles at; - RETURN_IF_NOT_BATTLE(at); - - BattleHex attackOriginHex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position - - const auto * defender = battleGetUnitByPos(destinationTile, true); - if (!defender) - return at; // can't attack thin air - - bool reverse = isToReverse(attacker, defender); - if(reverse && attacker->doubleWide()) - { - attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on - } - if(attacker->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT)) - { - boost::copy(attacker->getSurroundingHexes(attackerPos), vstd::set_inserter(at.hostileCreaturePositions)); - } - if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK)) - { - std::vector hexes = attacker->getSurroundingHexes(attackerPos); - for(BattleHex tile : hexes) - { - if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, attackOriginHex) > -1)) //adjacent both to attacker's head and attacked tile - { - const auto * st = battleGetUnitByPos(tile, true); - if(st && battleMatchOwner(st, attacker)) //only hostile stacks - does it work well with Berserk? - at.hostileCreaturePositions.insert(tile); - } - } - } - if(attacker->hasBonusOfType(BonusType::WIDE_BREATH)) - { - std::vector hexes = destinationTile.neighbouringTiles(); - for(int i = 0; ihasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) - { - auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); - if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation - { - BattleHex nextHex = destinationTile.cloneInDirection(direction, false); - - if ( defender->doubleWide() ) - { - auto secondHex = destinationTile == defender->getPosition() ? - defender->occupiedHex(): - defender->getPosition(); - - // if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin) - // then dragon breath should target tile on the opposite side of targeted creature - if (BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) - nextHex = secondHex.cloneInDirection(direction, false); - } - - if (nextHex.isValid()) - { - //friendly stacks can also be damaged by Dragon Breath - const auto * st = battleGetUnitByPos(nextHex, true); - if(st != nullptr) - at.friendlyCreaturePositions.insert(nextHex); - } - } - } - return at; -} - -AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const -{ - //does not return hex attacked directly - AttackableTiles at; - RETURN_IF_NOT_BATTLE(at); - - if(attacker->hasBonusOfType(BonusType::SHOOTS_ALL_ADJACENT) && !vstd::contains(attackerPos.neighbouringTiles(), destinationTile)) - { - std::vector targetHexes = destinationTile.neighbouringTiles(); - targetHexes.push_back(destinationTile); - boost::copy(targetHexes, vstd::set_inserter(at.hostileCreaturePositions)); - } - - return at; -} - -std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const -{ - std::vector units; - RETURN_IF_NOT_BATTLE(units); - - AttackableTiles at; - - if (rangedAttack) - at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); - else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - units = battleGetUnitsIf([=](const battle::Unit * unit) - { - if (unit->isGhost() || !unit->alive()) - return false; - - for (BattleHex hex : battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide())) - { - if (vstd::contains(at.hostileCreaturePositions, hex)) - return true; - if (vstd::contains(at.friendlyCreaturePositions, hex)) - return true; - } - return false; - }); - - return units; -} - -std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const -{ - std::set attackedCres; - RETURN_IF_NOT_BATTLE(attackedCres); - - AttackableTiles at; - - if(rangedAttack) - at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); - else - at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); - - for (BattleHex tile : at.hostileCreaturePositions) //all around & three-headed attack - { - const CStack * st = battleGetStackByPos(tile, true); - if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? - { - attackedCres.insert(st); - } - } - for (BattleHex tile : at.friendlyCreaturePositions) - { - const CStack * st = battleGetStackByPos(tile, true); - if(st) //friendly stacks can also be damaged by Dragon Breath - { - attackedCres.insert(st); - } - } - return attackedCres; -} - -static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side ) -{ - static const std::set rightDirs { BattleHex::BOTTOM_RIGHT, BattleHex::TOP_RIGHT, BattleHex::RIGHT }; - static const std::set leftDirs { BattleHex::BOTTOM_LEFT, BattleHex::TOP_LEFT, BattleHex::LEFT }; - - auto mutualPos = BattleHex::mutualPosition(hex, testHex); - - if (side == BattleSide::ATTACKER) - return rightDirs.count(mutualPos); - else - return leftDirs.count(mutualPos); -} - -//TODO: this should apply also to mechanics and cursor interface -bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const -{ - BattleHex attackerHex = attacker->getPosition(); - BattleHex defenderHex = defender->getPosition(); - - if (attackerHex < 0 ) //turret - return false; - - if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) - return false; - - if (defender->doubleWide()) - { - if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) - return false; - } - - if (attacker->doubleWide()) - { - if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) - return false; - } - - // a bit weird case since here defender is slightly behind attacker, so reversing seems preferable, - // but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks - if (attacker->doubleWide() && defender->doubleWide()) - { - if(isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), static_cast(attacker->unitSide()))) - return false; - } - return true; -} - -ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const -{ - ReachabilityInfo::TDistances ret; - ret.fill(-1); - RETURN_IF_NOT_BATTLE(ret); - - auto reachability = getReachability(unit); - - boost::copy(reachability.distances, ret.begin()); - - return ret; -} - -bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const -{ - RETURN_IF_NOT_BATTLE(false); - - const std::string cachingStrNoDistancePenalty = "type_NO_DISTANCE_PENALTY"; - static const auto selectorNoDistancePenalty = Selector::type()(BonusType::NO_DISTANCE_PENALTY); - - if(shooter->hasBonus(selectorNoDistancePenalty, cachingStrNoDistancePenalty)) - return false; - - if(const auto * target = battleGetUnitByPos(destHex, true)) - { - //If any hex of target creature is within range, there is no penalty - int range = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE; - - auto bonus = shooter->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); - if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE) - range = bonus->additionalInfo[0]; - - if(isEnemyUnitWithinSpecifiedRange(shooterPosition, target, range)) - return false; - } - else - { - if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE) - return false; - } - - return true; -} - -bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const -{ - for(auto hex : defenderUnit->getHexes()) - if(BattleHex::getDistance(attackerPosition, hex) <= range) - return true; - - return false; -} - -BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const -{ - RETURN_IF_NOT_BATTLE(BattleHex::INVALID); - return WallPartToHex(part); -} - -EWallPart CBattleInfoCallback::battleHexToWallPart(BattleHex hex) const -{ - RETURN_IF_NOT_BATTLE(EWallPart::INVALID); - return hexToWallPart(hex); -} - -bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) const -{ - RETURN_IF_NOT_BATTLE(false); - return wallPart != EWallPart::INDESTRUCTIBLE_PART && wallPart != EWallPart::INDESTRUCTIBLE_PART_OF_GATE && - wallPart != EWallPart::INVALID; -} - -bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(isWallPartPotentiallyAttackable(wallPart)) - { - auto wallState = battleGetWallState(wallPart); - return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED); - } - return false; -} - -std::vector CBattleInfoCallback::getAttackableBattleHexes() const -{ - std::vector attackableBattleHexes; - RETURN_IF_NOT_BATTLE(attackableBattleHexes); - - for(const auto & wallPartPair : wallParts) - { - if(isWallPartAttackable(wallPartPair.second)) - attackableBattleHexes.emplace_back(wallPartPair.first); - } - - return attackableBattleHexes; -} - -int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const -{ - RETURN_IF_NOT_BATTLE(-1); - //TODO should be replaced using bonus system facilities (propagation onto battle node) - - int32_t ret = caster->getSpellCost(sp); - - //checking for friendly stacks reducing cost of the spell and - //enemy stacks increasing it - int32_t manaReduction = 0; - int32_t manaIncrease = 0; - - for(const auto * unit : battleAliveUnits()) - { - if(unit->unitOwner() == caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ALLY)) - { - vstd::amax(manaReduction, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ALLY)); - } - if(unit->unitOwner() != caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)) - { - vstd::amax(manaIncrease, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)); - } - } - - return std::max(0, ret - manaReduction + manaIncrease); -} - -bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const -{ - return battleHasDistancePenalty(shooter, shooter->getPosition(), destHex) || battleHasWallPenalty(shooter, shooter->getPosition(), destHex); -} - -bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(false); - - if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked - return false; - - for(const auto * adjacent : battleAdjacentUnits(unit)) - { - if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack - return true; - } - return false; -} - -std::set CBattleInfoCallback::battleAdjacentUnits(const battle::Unit * unit) const -{ - std::set ret; - RETURN_IF_NOT_BATTLE(ret); - - for(auto hex : unit->getSurroundingHexes()) - { - if(const auto * neighbour = battleGetUnitByPos(hex, true)) - ret.insert(neighbour); - } - - return ret; -} - -SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const -{ - RETURN_IF_NOT_BATTLE(SpellID::NONE); - //This is complete list. No spells from mods. - //todo: this should be Spellbook of caster Stack - static const std::set allPossibleSpells = - { - SpellID::AIR_SHIELD, - SpellID::ANTI_MAGIC, - SpellID::BLESS, - SpellID::BLOODLUST, - SpellID::COUNTERSTRIKE, - SpellID::CURE, - SpellID::FIRE_SHIELD, - SpellID::FORTUNE, - SpellID::HASTE, - SpellID::MAGIC_MIRROR, - SpellID::MIRTH, - SpellID::PRAYER, - SpellID::PRECISION, - SpellID::PROTECTION_FROM_AIR, - SpellID::PROTECTION_FROM_EARTH, - SpellID::PROTECTION_FROM_FIRE, - SpellID::PROTECTION_FROM_WATER, - SpellID::SHIELD, - SpellID::SLAYER, - SpellID::STONE_SKIN - }; - std::vector beneficialSpells; - - auto getAliveEnemy = [=](const std::function & pred) -> const CStack * - { - auto stacks = battleGetStacksIf([=](const CStack * stack) - { - return pred(stack) && stack->unitOwner() != subject->unitOwner() && stack->isValidTarget(false); - }); - - if(stacks.empty()) - return nullptr; - else - return stacks.front(); - }; - - for(const SpellID& spellID : allPossibleSpells) - { - std::stringstream cachingStr; - cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; - - if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, spellID), Selector::all, cachingStr.str()) - //TODO: this ability has special limitations - || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) - continue; - - switch (spellID) - { - case SpellID::SHIELD: - case SpellID::FIRE_SHIELD: // not if all enemy units are shooters - { - const auto * walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack - { - return !stack->canShoot(); - }); - - if(!walker) - continue; - } - break; - case SpellID::AIR_SHIELD: //only against active shooters - { - const auto * shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack - { - return stack->canShoot(); - }); - if(!shooter) - continue; - } - break; - case SpellID::ANTI_MAGIC: - case SpellID::MAGIC_MIRROR: - case SpellID::PROTECTION_FROM_AIR: - case SpellID::PROTECTION_FROM_EARTH: - case SpellID::PROTECTION_FROM_FIRE: - case SpellID::PROTECTION_FROM_WATER: - { - const ui8 enemySide = 1 - subject->unitSide(); - //todo: only if enemy has spellbook - if (!battleHasHero(enemySide)) //only if there is enemy hero - continue; - } - break; - case SpellID::CURE: //only damaged units - { - //do not cast on affected by debuffs - if(!subject->canBeHealed()) - continue; - } - break; - case SpellID::BLOODLUST: - { - if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent - continue; - } - break; - case SpellID::PRECISION: - { - if(!subject->canShoot()) - continue; - } - break; - case SpellID::SLAYER://only if monsters are present - { - const auto * kingMonster = getAliveEnemy([&](const CStack * stack) -> bool //look for enemy, non-shooting stack - { - const auto isKing = Selector::type()(BonusType::KING); - - return stack->hasBonus(isKing); - }); - - if (!kingMonster) - continue; - } - break; - } - beneficialSpells.push_back(spellID); - } - - if(!beneficialSpells.empty()) - { - return *RandomGeneratorUtil::nextItem(beneficialSpells, rand); - } - else - { - return SpellID::NONE; - } -} - -SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const CStack * caster) const -{ - RETURN_IF_NOT_BATTLE(SpellID::NONE); - - TConstBonusListPtr bl = caster->getBonuses(Selector::type()(BonusType::SPELLCASTER)); - if (!bl->size()) - return SpellID::NONE; - - if(bl->size() == 1) - return SpellID(bl->front()->subtype); - - int totalWeight = 0; - for(const auto & b : *bl) - { - totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them - } - - if (totalWeight == 0) - return SpellID::NONE; - - int randomPos = rand.nextInt(totalWeight - 1); - for(const auto & b : *bl) - { - randomPos -= std::max(b->additionalInfo[0], 0); - if(randomPos < 0) - { - return SpellID(b->subtype); - } - } - - return SpellID::NONE; -} - -int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) const -{ - RETURN_IF_NOT_BATTLE(-3); - if(!battleCanSurrender(Player)) - return -1; - - const auto sideOpt = playerToSide(Player); - if(!sideOpt) - return -1; - const auto side = sideOpt.value(); - - int ret = 0; - double discount = 0; - - for(const auto * unit : battleAliveUnits(side)) - ret += unit->getRawSurrenderCost(); - - if(const CGHeroInstance * h = battleGetFightingHero(side)) - discount += h->valOfBonuses(BonusType::SURRENDER_DISCOUNT); - - ret = static_cast(ret * (100.0 - discount) / 100.0); - vstd::amax(ret, 0); //no negative costs for >100% discounts (impossible in original H3 mechanics, but some day...) - return ret; -} - -si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const -{ - const IBonusBearer * node = nullptr; - if(const CGHeroInstance * h = battleGetFightingHero(side)) - node = h; - else - node = getBonusBearer(); - - if(!node) - return 0; - - auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_BELOW)); - if(b->size()) - return b->totalValue(); - - return 0; -} - -si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const -{ - const IBonusBearer *node = nullptr; - if(const CGHeroInstance * h = battleGetFightingHero(side)) - node = h; - else - node = getBonusBearer(); - - if(!node) - return GameConstants::SPELL_LEVELS; - - //We can't "just get value" - it'd be 0 if there are bonuses (and all would be blocked) - auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); - if(b->size()) - return b->totalValue(); - - return GameConstants::SPELL_LEVELS; -} - -std::optional CBattleInfoCallback::battleIsFinished() const -{ - auto units = battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->alive() && !unit->isTurret() && !unit->hasBonusOfType(BonusType::SIEGE_WEAPON); - }); - - std::array hasUnit = {false, false}; //index is BattleSide - - for(auto & unit : units) - { - //todo: move SIEGE_WEAPON check to Unit state - hasUnit.at(unit->unitSide()) = true; - - if(hasUnit[0] && hasUnit[1]) - return std::nullopt; - } - - hasUnit = {false, false}; - - for(auto & unit : units) - { - if(!unit->isClone() && !unit->acquireState()->summoned && !dynamic_cast (unit)) - { - hasUnit.at(unit->unitSide()) = true; - } - } - - if(!hasUnit[0] && !hasUnit[1]) - return 2; - if(!hasUnit[1]) - return 0; - else - return 1; -} - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoCallback.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 "CBattleInfoCallback.h" + +#include + +#include "../CStack.h" +#include "BattleInfo.h" +#include "CObstacleInstance.h" +#include "DamageCalculator.h" +#include "PossiblePlayerBattleAction.h" +#include "../NetPacks.h" +#include "../spells/ObstacleCasterProxy.h" +#include "../spells/ISpellMechanics.h" +#include "../spells/Problem.h" +#include "../spells/CSpellHandler.h" +#include "../mapObjects/CGTownInstance.h" +#include "../BattleFieldHandler.h" +#include "../Rect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +namespace SiegeStuffThatShouldBeMovedToHandlers // <=== TODO +{ + +static BattleHex lineToWallHex(int line) //returns hex with wall in given line (y coordinate) +{ + static const BattleHex lineToHex[] = {12, 29, 45, 62, 78, 96, 112, 130, 147, 165, 182}; + + return lineToHex[line]; +} + +static bool sameSideOfWall(BattleHex pos1, BattleHex pos2) +{ + const int wallInStackLine = lineToWallHex(pos1.getY()); + const int wallInDestLine = lineToWallHex(pos2.getY()); + + const bool stackLeft = pos1 < wallInStackLine; + const bool destLeft = pos2 < wallInDestLine; + + return stackLeft == destLeft; +} + +// parts of wall +static const std::pair wallParts[] = +{ + std::make_pair(50, EWallPart::KEEP), + std::make_pair(183, EWallPart::BOTTOM_TOWER), + std::make_pair(182, EWallPart::BOTTOM_WALL), + std::make_pair(130, EWallPart::BELOW_GATE), + std::make_pair(78, EWallPart::OVER_GATE), + std::make_pair(29, EWallPart::UPPER_WALL), + std::make_pair(12, EWallPart::UPPER_TOWER), + std::make_pair(95, EWallPart::INDESTRUCTIBLE_PART_OF_GATE), + std::make_pair(96, EWallPart::GATE), + std::make_pair(45, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(62, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(112, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(147, EWallPart::INDESTRUCTIBLE_PART), + std::make_pair(165, EWallPart::INDESTRUCTIBLE_PART) +}; + +static EWallPart hexToWallPart(BattleHex hex) +{ + for(const auto & elem : wallParts) + { + if(elem.first == hex) + return elem.second; + } + + return EWallPart::INVALID; //not found! +} + +static BattleHex WallPartToHex(EWallPart part) +{ + for(const auto & elem : wallParts) + { + if(elem.second == part) + return elem.first; + } + + return BattleHex::INVALID; //not found! +} +} + +using namespace SiegeStuffThatShouldBeMovedToHandlers; + +ESpellCastProblem CBattleInfoCallback::battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const +{ + RETURN_IF_NOT_BATTLE(ESpellCastProblem::INVALID); + if(caster == nullptr) + { + logGlobal->error("CBattleInfoCallback::battleCanCastSpell: no spellcaster."); + return ESpellCastProblem::INVALID; + } + const PlayerColor player = caster->getCasterOwner(); + const auto side = playerToSide(player); + if(!side) + return ESpellCastProblem::INVALID; + if(!battleDoWeKnowAbout(side.value())) + { + logGlobal->warn("You can't check if enemy can cast given spell!"); + return ESpellCastProblem::INVALID; + } + + if(battleTacticDist()) + return ESpellCastProblem::ONGOING_TACTIC_PHASE; + + switch(mode) + { + case spells::Mode::HERO: + { + if(battleCastSpells(side.value()) > 0) + return ESpellCastProblem::CASTS_PER_TURN_LIMIT; + + const auto * hero = dynamic_cast(caster); + + if(!hero) + return ESpellCastProblem::NO_HERO_TO_CAST_SPELL; + if(hero->hasBonusOfType(BonusType::BLOCK_ALL_MAGIC)) + return ESpellCastProblem::MAGIC_IS_BLOCKED; + } + break; + default: + break; + } + + return ESpellCastProblem::OK; +} + +std::pair< std::vector, int > CBattleInfoCallback::getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const +{ + auto reachability = getReachability(stack); + + if(reachability.predecessors[dest] == -1) //cannot reach destination + { + return std::make_pair(std::vector(), 0); + } + + //making the Path + std::vector path; + BattleHex curElem = dest; + while(curElem != start) + { + path.push_back(curElem); + curElem = reachability.predecessors[curElem]; + } + + return std::make_pair(path, reachability.distances[dest]); +} + +bool CBattleInfoCallback::battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const +{ + auto isTileBlocked = [&](BattleHex tile) + { + EWallPart wallPart = battleHexToWallPart(tile); + if (wallPart == EWallPart::INVALID) + return false; // there is no wall here + if (wallPart == EWallPart::INDESTRUCTIBLE_PART_OF_GATE) + return false; // does not blocks ranged attacks + if (wallPart == EWallPart::INDESTRUCTIBLE_PART) + return true; // always blocks ranged attacks + + return isWallPartAttackable(wallPart); + }; + // Count wall penalty requirement by shortest path, not by arbitrary line, to avoid various OH3 bugs + auto getShortestPath = [](BattleHex from, BattleHex dest) -> std::vector + { + //Out early + if(from == dest) + return {}; + + std::vector ret; + auto next = from; + //Not a real direction, only to indicate to which side we should search closest tile + auto direction = from.getX() > dest.getX() ? BattleSide::DEFENDER : BattleSide::ATTACKER; + + while (next != dest) + { + auto tiles = next.neighbouringTiles(); + std::set possibilities = {tiles.begin(), tiles.end()}; + next = BattleHex::getClosestTile(direction, dest, possibilities); + ret.push_back(next); + } + assert(!ret.empty()); + ret.pop_back(); //Remove destination hex + return ret; + }; + + RETURN_IF_NOT_BATTLE(false); + auto checkNeeded = !sameSideOfWall(from, dest); + bool pathHasWall = false; + bool pathHasMoat = false; + + for(const auto & hex : getShortestPath(from, dest)) + { + pathHasWall |= isTileBlocked(hex); + if(!checkMoat) + continue; + + auto obstacles = battleGetAllObstaclesOnPos(hex, false); + + if(hex != BattleHex::GATE_BRIDGE || (battleIsGatePassable())) + for(const auto & obst : obstacles) + if(obst->obstacleType == CObstacleInstance::MOAT) + pathHasMoat |= true; + } + + return checkNeeded && ( (checkWall && pathHasWall) || (checkMoat && pathHasMoat) ); +} + +bool CBattleInfoCallback::battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const +{ + RETURN_IF_NOT_BATTLE(false); + if(!battleGetSiegeLevel()) + return false; + + const std::string cachingStrNoWallPenalty = "type_NO_WALL_PENALTY"; + static const auto selectorNoWallPenalty = Selector::type()(BonusType::NO_WALL_PENALTY); + + if(shooter->hasBonus(selectorNoWallPenalty, cachingStrNoWallPenalty)) + return false; + + const auto shooterOutsideWalls = shooterPosition < lineToWallHex(shooterPosition.getY()); + + return shooterOutsideWalls && battleHasPenaltyOnLine(shooterPosition, destHex, true, false); +} + +std::vector CBattleInfoCallback::getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data) +{ + RETURN_IF_NOT_BATTLE(std::vector()); + std::vector allowedActionList; + if(data.tacticsMode) //would "if(battleGetTacticDist() > 0)" work? + { + allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_TACTICS); + allowedActionList.push_back(PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK); + } + else + { + if(stack->canCast()) //TODO: check for battlefield effects that prevent casting? + { + if(stack->hasBonusOfType(BonusType::SPELLCASTER)) + { + for(const auto & spellID : data.creatureSpellsToCast) + { + const CSpell *spell = spellID.toSpell(); + PossiblePlayerBattleAction act = getCasterAction(spell, stack, spells::Mode::CREATURE_ACTIVE); + allowedActionList.push_back(act); + } + } + if(stack->hasBonusOfType(BonusType::RANDOM_SPELLCASTER)) + allowedActionList.push_back(PossiblePlayerBattleAction::RANDOM_GENIE_SPELL); + } + if(stack->canShoot()) + allowedActionList.push_back(PossiblePlayerBattleAction::SHOOT); + if(stack->hasBonusOfType(BonusType::RETURN_AFTER_STRIKE)) + allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK_AND_RETURN); + + allowedActionList.push_back(PossiblePlayerBattleAction::ATTACK); //all active stacks can attack + allowedActionList.push_back(PossiblePlayerBattleAction::WALK_AND_ATTACK); //not all stacks can always walk, but we will check this elsewhere + + if(stack->canMove() && stack->speed(0, true)) //probably no reason to try move war machines or bound stacks + allowedActionList.push_back(PossiblePlayerBattleAction::MOVE_STACK); + + const auto * siegedTown = battleGetDefendedTown(); + if(siegedTown && siegedTown->hasFort() && stack->hasBonusOfType(BonusType::CATAPULT)) //TODO: check shots + allowedActionList.push_back(PossiblePlayerBattleAction::CATAPULT); + if(stack->hasBonusOfType(BonusType::HEALER)) + allowedActionList.push_back(PossiblePlayerBattleAction::HEAL); + } + + return allowedActionList; +} + +PossiblePlayerBattleAction CBattleInfoCallback::getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const +{ + RETURN_IF_NOT_BATTLE(PossiblePlayerBattleAction::INVALID); + auto spellSelMode = PossiblePlayerBattleAction::ANY_LOCATION; + + const CSpell::TargetInfo ti(spell, caster->getSpellSchoolLevel(spell), mode); + + if(ti.massive || ti.type == spells::AimType::NO_TARGET) + spellSelMode = PossiblePlayerBattleAction::NO_LOCATION; + else if(ti.type == spells::AimType::LOCATION && ti.clearAffected) + spellSelMode = PossiblePlayerBattleAction::FREE_LOCATION; + else if(ti.type == spells::AimType::CREATURE) + spellSelMode = PossiblePlayerBattleAction::AIMED_SPELL_CREATURE; + else if(ti.type == spells::AimType::OBSTACLE) + spellSelMode = PossiblePlayerBattleAction::OBSTACLE; + + return PossiblePlayerBattleAction(spellSelMode, spell->id); +} + +std::set CBattleInfoCallback::battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const +{ + std::set attackedHexes; + RETURN_IF_NOT_BATTLE(attackedHexes); + + AttackableTiles at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + for (BattleHex tile : at.hostileCreaturePositions) + { + const auto * st = battleGetUnitByPos(tile, true); + if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? + { + attackedHexes.insert(tile); + } + } + for (BattleHex tile : at.friendlyCreaturePositions) + { + if(battleGetUnitByPos(tile, true)) //friendly stacks can also be damaged by Dragon Breath + { + attackedHexes.insert(tile); + } + } + return attackedHexes; +} + +SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const +{ + switch (mode) + { + case RANDOM_GENIE: + return getRandomBeneficialSpell(rand, stack); //target + break; + case RANDOM_AIMED: + return getRandomCastedSpell(rand, stack); //caster + break; + default: + logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast(mode)); + return SpellID::NONE; + } +} + +const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + for(const auto * s : battleGetAllStacks(true)) + if(vstd::contains(s->getHexes(), pos) && (!onlyAlive || s->alive())) + return s; + + return nullptr; +} + +const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + auto ret = battleGetUnitsIf([=](const battle::Unit * unit) + { + return !unit->isGhost() + && vstd::contains(battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()), pos) + && (!onlyAlive || unit->alive()); + }); + + if(!ret.empty()) + return ret.front(); + else + return nullptr; +} + +battle::Units CBattleInfoCallback::battleAliveUnits() const +{ + return battleGetUnitsIf([](const battle::Unit * unit) + { + return unit->isValidTarget(false); + }); +} + +battle::Units CBattleInfoCallback::battleAliveUnits(ui8 side) const +{ + return battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->isValidTarget(false) && unit->unitSide() == side; + }); +} + +using namespace battle; + +//T is battle::Unit descendant +template +const T * takeOneUnit(std::vector & allUnits, const int turn, int8_t & sideThatLastMoved, int phase) +{ + const T * returnedUnit = nullptr; + size_t currentUnitIndex = 0; + + for(size_t i = 0; i < allUnits.size(); i++) + { + int32_t currentUnitInitiative = -1; + int32_t returnedUnitInitiative = -1; + + if(returnedUnit) + returnedUnitInitiative = returnedUnit->getInitiative(turn); + + if(!allUnits[i]) + continue; + + auto currentUnit = allUnits[i]; + currentUnitInitiative = currentUnit->getInitiative(turn); + + switch(phase) + { + case BattlePhases::NORMAL: // Faster first, attacker priority, higher slot first + if(returnedUnit == nullptr || currentUnitInitiative > returnedUnitInitiative) + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + else if(currentUnitInitiative == returnedUnitInitiative) + { + if(sideThatLastMoved == -1 && turn <= 0 && currentUnit->unitSide() == BattleSide::ATTACKER + && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Turn 0 attacker priority + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + else if(sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved + && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + } + break; + case BattlePhases::WAIT_MORALE: // Slower first, higher slot first + case BattlePhases::WAIT: + if(returnedUnit == nullptr || currentUnitInitiative < returnedUnitInitiative) + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + else if(currentUnitInitiative == returnedUnitInitiative && sideThatLastMoved != -1 && currentUnit->unitSide() != sideThatLastMoved + && !(returnedUnit->unitSide() == currentUnit->unitSide() && returnedUnit->unitSlot() < currentUnit->unitSlot())) // Alternate equal speeds units + { + returnedUnit = currentUnit; + currentUnitIndex = i; + } + break; + default: + break; + } + } + + if(!returnedUnit) + return nullptr; + + allUnits[currentUnitIndex] = nullptr; + + return returnedUnit; +} + +void CBattleInfoCallback::battleGetTurnOrder(std::vector & turns, const size_t maxUnits, const int maxTurns, const int turn, int8_t sideThatLastMoved) const +{ + RETURN_IF_NOT_BATTLE(); + + if(maxUnits == 0 && maxTurns == 0) + { + logGlobal->error("Attempt to get infinite battle queue"); + return; + } + + auto actualTurn = turn > 0 ? turn : 0; + + auto turnsIsFull = [&]() -> bool + { + if(maxUnits == 0) + return false;//no limit + + size_t turnsSize = 0; + for(const auto & oneTurn : turns) + turnsSize += oneTurn.size(); + return turnsSize >= maxUnits; + }; + + turns.emplace_back(); + + // We'll split creatures with remaining movement to 4 buckets (SIEGE, NORMAL, WAIT_MORALE, WAIT) + std::array phases; // Access using BattlePhases enum + + const battle::Unit * activeUnit = battleActiveUnit(); + + if(activeUnit) + { + //its first turn and active unit hasn't taken any action yet - must be placed at the beginning of queue, no matter what + if(turn == 0 && activeUnit->willMove() && !activeUnit->waited()) + { + turns.back().push_back(activeUnit); + if(turnsIsFull()) + return; + } + + //its first or current turn, turn priority for active stack side + //TODO: what if active stack mind-controlled? + if(turn <= 0 && sideThatLastMoved < 0) + sideThatLastMoved = activeUnit->unitSide(); + } + + auto allUnits = battleGetUnitsIf([](const battle::Unit * unit) + { + return !unit->isGhost(); + }); + + // If no unit will be EVER! able to move, battle is over. + if(!vstd::contains_if(allUnits, [](const battle::Unit * unit) { return unit->willMove(100000); })) //little evil, but 100000 should be enough for all effects to disappear + { + turns.clear(); + return; + } + + for(const auto * unit : allUnits) + { + if((actualTurn == 0 && !unit->willMove()) //we are considering current round and unit won't move + || (actualTurn > 0 && !unit->canMove(turn)) //unit won't be able to move in later rounds + || (actualTurn == 0 && unit == activeUnit && !turns.at(0).empty() && unit == turns.front().front())) //it's active unit already added at the beginning of queue + { + continue; + } + + int unitPhase = unit->battleQueuePhase(turn); + + phases[unitPhase].push_back(unit); + } + + boost::sort(phases[BattlePhases::SIEGE], CMP_stack(BattlePhases::SIEGE, actualTurn, sideThatLastMoved)); + std::copy(phases[BattlePhases::SIEGE].begin(), phases[BattlePhases::SIEGE].end(), std::back_inserter(turns.back())); + + if(turnsIsFull()) + return; + + for(uint8_t phase = BattlePhases::NORMAL; phase < BattlePhases::NUMBER_OF_PHASES; phase++) + boost::sort(phases[phase], CMP_stack(phase, actualTurn, sideThatLastMoved)); + + uint8_t phase = BattlePhases::NORMAL; + while(!turnsIsFull() && phase < BattlePhases::NUMBER_OF_PHASES) + { + const battle::Unit * currentUnit = nullptr; + if(phases[phase].empty()) + phase++; + else + { + currentUnit = takeOneUnit(phases[phase], actualTurn, sideThatLastMoved, phase); + if(!currentUnit) + { + phase++; + } + else + { + turns.back().push_back(currentUnit); + sideThatLastMoved = currentUnit->unitSide(); + } + } + } + + if(sideThatLastMoved < 0) + sideThatLastMoved = BattleSide::ATTACKER; + + if(!turnsIsFull() && (maxTurns == 0 || turns.size() < maxTurns)) + battleGetTurnOrder(turns, maxUnits, maxTurns, actualTurn + 1, sideThatLastMoved); +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const +{ + + RETURN_IF_NOT_BATTLE(std::vector()); + if(!unit->getPosition().isValid()) //turrets + return std::vector(); + + auto reachability = getReachability(unit); + + return battleGetAvailableHexes(reachability, unit, obtainMovementRange); +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const +{ + std::vector ret; + + RETURN_IF_NOT_BATTLE(ret); + if(!unit->getPosition().isValid()) //turrets + return ret; + + auto unitSpeed = unit->speed(0, true); + + const bool tacticsPhase = battleTacticDist() && battleGetTacticsSide() == unit->unitSide(); + + for(int i = 0; i < GameConstants::BFIELD_SIZE; ++i) + { + // If obstacles or other stacks makes movement impossible, it can't be helped. + if(!cache.isReachable(i)) + continue; + + if(tacticsPhase && !obtainMovementRange) // if obtainMovementRange requested do not return tactics range + { + // Stack has to perform tactic-phase movement -> can enter any reachable tile within given range + if(!isInTacticRange(i)) + continue; + } + else + { + // Not tactics phase -> destination must be reachable and within unit range. + if(cache.distances[i] > static_cast(unitSpeed)) + continue; + } + + ret.emplace_back(i); + } + + return ret; +} + +std::vector CBattleInfoCallback::battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const +{ + std::vector ret = battleGetAvailableHexes(unit, obtainMovementRange); + + if(ret.empty()) + return ret; + + if(addOccupiable && unit->doubleWide()) + { + std::vector occupiable; + + occupiable.reserve(ret.size()); + for(auto hex : ret) + occupiable.push_back(unit->occupiedHex(hex)); + + vstd::concatenate(ret, occupiable); + } + + + if(attackable) + { + auto meleeAttackable = [&](BattleHex hex) -> bool + { + // Return true if given hex has at least one available neighbour. + // Available hexes are already present in ret vector. + auto availableNeighbor = boost::find_if(ret, [=] (BattleHex availableHex) + { + return BattleHex::mutualPosition(hex, availableHex) >= 0; + }); + return availableNeighbor != ret.end(); + }; + for(const auto * otherSt : battleAliveUnits(otherSide(unit->unitSide()))) + { + if(!otherSt->isValidTarget(false)) + continue; + + std::vector occupied = otherSt->getHexes(); + + if(battleCanShoot(unit, otherSt->getPosition())) + { + attackable->insert(attackable->end(), occupied.begin(), occupied.end()); + continue; + } + + for(BattleHex he : occupied) + { + if(meleeAttackable(he)) + attackable->push_back(he); + } + } + } + + //adding occupiable likely adds duplicates to ret -> clean it up + boost::sort(ret); + ret.erase(boost::unique(ret).end(), ret.end()); + return ret; +} + +bool CBattleInfoCallback::battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(battleTacticDist()) + return false; + + if (!stack || !target) + return false; + + if(!battleMatchOwner(stack, target)) + return false; + + auto id = stack->unitType()->getId(); + if (id == CreatureID::FIRST_AID_TENT || id == CreatureID::CATAPULT) + return false; + + return target->alive(); +} + +bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(battleTacticDist()) //no shooting during tactics + return false; + + if (!attacker) + return false; + if (attacker->creatureIndex() == CreatureID::CATAPULT) //catapult cannot attack creatures + return false; + + //forgetfulness + TConstBonusListPtr forgetfulList = attacker->getBonuses(Selector::type()(BonusType::FORGETFULL)); + if(!forgetfulList->empty()) + { + int forgetful = forgetfulList->valOfBonuses(Selector::type()(BonusType::FORGETFULL)); + + //advanced+ level + if(forgetful > 1) + return false; + } + + return attacker->canShoot() && (!battleIsUnitBlocked(attacker) + || attacker->hasBonusOfType(BonusType::FREE_SHOOTING)); +} + +bool CBattleInfoCallback::battleCanShoot(const battle::Unit * attacker, BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + + const battle::Unit * defender = battleGetUnitByPos(dest); + if(!attacker || !defender) + return false; + + if(battleMatchOwner(attacker, defender) && defender->alive()) + { + if(battleCanShoot(attacker)) + { + auto limitedRangeBonus = attacker->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); + if(limitedRangeBonus == nullptr) + { + return true; + } + + int shootingRange = limitedRangeBonus->val; + return isEnemyUnitWithinSpecifiedRange(attacker->getPosition(), defender, shootingRange); + } + } + + return false; +} + +DamageEstimation CBattleInfoCallback::calculateDmgRange(const BattleAttackInfo & info) const +{ + DamageCalculator calculator(*this, info); + + return calculator.calculateDmgRange(); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + auto reachability = battleGetDistances(attacker, attacker->getPosition()); + int movementDistance = reachability[attackerPosition]; + return battleEstimateDamage(attacker, defender, movementDistance, retaliationDmg); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + const bool shooting = battleCanShoot(attacker, defender->getPosition()); + const BattleAttackInfo bai(attacker, defender, movementDistance, shooting); + return battleEstimateDamage(bai, retaliationDmg); +} + +DamageEstimation CBattleInfoCallback::battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg) const +{ + RETURN_IF_NOT_BATTLE({}); + + DamageEstimation ret = calculateDmgRange(bai); + + if(retaliationDmg) + { + if(bai.shooting) + { + //FIXME: handle RANGED_RETALIATION + *retaliationDmg = DamageEstimation(); + } + else + { + //TODO: rewrite using boost::numeric::interval + //TODO: rewire once more using interval-based fuzzy arithmetic + + const auto & estimateRetaliation = [&](int64_t damage) + { + auto retaliationAttack = bai.reverse(); + auto state = retaliationAttack.attacker->acquireState(); + state->damage(damage); + retaliationAttack.attacker = state.get(); + return calculateDmgRange(retaliationAttack); + }; + + DamageEstimation retaliationMin = estimateRetaliation(ret.damage.min); + DamageEstimation retaliationMax = estimateRetaliation(ret.damage.min); + + retaliationDmg->damage.min = std::min(retaliationMin.damage.min, retaliationMax.damage.min); + retaliationDmg->damage.max = std::max(retaliationMin.damage.max, retaliationMax.damage.max); + + retaliationDmg->kills.min = std::min(retaliationMin.kills.min, retaliationMax.kills.min); + retaliationDmg->kills.max = std::max(retaliationMin.kills.max, retaliationMax.kills.max); + } + } + + return ret; +} + +std::vector> CBattleInfoCallback::battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking) const +{ + std::vector> obstacles = std::vector>(); + RETURN_IF_NOT_BATTLE(obstacles); + for(auto & obs : battleGetAllObstacles()) + { + if(vstd::contains(obs->getBlockedTiles(), tile) + || (!onlyBlocking && vstd::contains(obs->getAffectedTiles(), tile))) + { + obstacles.push_back(obs); + } + } + return obstacles; +} + +std::vector> CBattleInfoCallback::getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const +{ + auto affectedObstacles = std::vector>(); + RETURN_IF_NOT_BATTLE(affectedObstacles); + if(unit->alive()) + { + if(!passed.count(unit->getPosition())) + affectedObstacles = battleGetAllObstaclesOnPos(unit->getPosition(), false); + if(unit->doubleWide()) + { + BattleHex otherHex = unit->occupiedHex(); + if(otherHex.isValid() && !passed.count(otherHex)) + for(auto & i : battleGetAllObstaclesOnPos(otherHex, false)) + if(!vstd::contains(affectedObstacles, i)) + affectedObstacles.push_back(i); + } + for(auto hex : unit->getHexes()) + if(hex == BattleHex::GATE_BRIDGE && battleIsGatePassable()) + for(int i=0; iobstacleType == CObstacleInstance::MOAT) + affectedObstacles.erase(affectedObstacles.begin()+i); + } + return affectedObstacles; +} + +bool CBattleInfoCallback::handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed) const +{ + if(!unit.alive()) + return false; + bool movementStopped = false; + for(auto & obstacle : getAllAffectedObstaclesByStack(&unit, passed)) + { + //helper info + const SpellCreatedObstacle * spellObstacle = dynamic_cast(obstacle.get()); + + if(spellObstacle) + { + auto revealObstacles = [&](const SpellCreatedObstacle & spellObstacle) -> void + { + // For the hidden spell created obstacles, e.g. QuickSand, it should be revealed after taking damage + auto operation = ObstacleChanges::EOperation::UPDATE; + if (spellObstacle.removeOnTrigger) + operation = ObstacleChanges::EOperation::REMOVE; + + SpellCreatedObstacle changedObstacle; + changedObstacle.uniqueID = spellObstacle.uniqueID; + changedObstacle.revealed = true; + + BattleObstaclesChanged bocp; + bocp.battleID = getBattle()->getBattleID(); + bocp.changes.emplace_back(spellObstacle.uniqueID, operation); + changedObstacle.toInfo(bocp.changes.back(), operation); + spellEnv.apply(&bocp); + }; + const auto side = unit.unitSide(); + auto shouldReveal = !spellObstacle->hidden || !battleIsObstacleVisibleForSide(*obstacle, (BattlePerspective::BattlePerspective)side); + const auto * hero = battleGetFightingHero(spellObstacle->casterSide); + auto caster = spells::ObstacleCasterProxy(getBattle()->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle); + const auto * sp = obstacle->getTrigger().toSpell(); + if(obstacle->triggersEffects() && sp) + { + auto cast = spells::BattleCast(this, &caster, spells::Mode::PASSIVE, sp); + spells::detail::ProblemImpl ignored; + auto target = spells::Target(1, spells::Destination(&unit)); + if(sp->battleMechanics(&cast)->canBeCastAt(target, ignored)) // Obstacles should not be revealed by immune creatures + { + if(shouldReveal) { //hidden obstacle triggers effects after revealed + revealObstacles(*spellObstacle); + cast.cast(&spellEnv, target); + } + } + } + else if(shouldReveal) + revealObstacles(*spellObstacle); + } + + if(!unit.alive()) + return false; + + if(obstacle->stopsMovement()) + movementStopped = true; + } + + return unit.alive() && !movementStopped; +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility() const +{ + AccessibilityInfo ret; + ret.fill(EAccessibility::ACCESSIBLE); + + //removing accessibility for side columns of hexes + for(int y = 0; y < GameConstants::BFIELD_HEIGHT; y++) + { + ret[BattleHex(GameConstants::BFIELD_WIDTH - 1, y)] = EAccessibility::SIDE_COLUMN; + ret[BattleHex(0, y)] = EAccessibility::SIDE_COLUMN; + } + + //special battlefields with logically unavailable tiles + auto bFieldType = battleGetBattlefieldType(); + + if(bFieldType != BattleField::NONE) + { + std::vector impassableHexes = bFieldType.getInfo()->impassableHexes; + + for(auto hex : impassableHexes) + ret[hex] = EAccessibility::UNAVAILABLE; + } + + //gate -> should be before stacks + if(battleGetSiegeLevel() > 0) + { + EAccessibility accessability = EAccessibility::ACCESSIBLE; + switch(battleGetGateState()) + { + case EGateState::CLOSED: + accessability = EAccessibility::GATE; + break; + + case EGateState::BLOCKED: + accessability = EAccessibility::UNAVAILABLE; + break; + } + ret[BattleHex::GATE_OUTER] = ret[BattleHex::GATE_INNER] = accessability; + } + + //tiles occupied by standing stacks + for(const auto * unit : battleAliveUnits()) + { + for(auto hex : unit->getHexes()) + if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns + ret[hex] = EAccessibility::ALIVE_STACK; + } + + //obstacles + for(const auto &obst : battleGetAllObstacles()) + { + for(auto hex : obst->getBlockedTiles()) + ret[hex] = EAccessibility::OBSTACLE; + } + + //walls + if(battleGetSiegeLevel() > 0) + { + static const int permanentlyLocked[] = {12, 45, 62, 112, 147, 165}; + for(auto hex : permanentlyLocked) + ret[hex] = EAccessibility::UNAVAILABLE; + + //TODO likely duplicated logic + static const std::pair lockedIfNotDestroyed[] = + { + //which part of wall, which hex is blocked if this part of wall is not destroyed + std::make_pair(EWallPart::BOTTOM_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_4)), + std::make_pair(EWallPart::BELOW_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_3)), + std::make_pair(EWallPart::OVER_GATE, BattleHex(BattleHex::DESTRUCTIBLE_WALL_2)), + std::make_pair(EWallPart::UPPER_WALL, BattleHex(BattleHex::DESTRUCTIBLE_WALL_1)) + }; + + for(const auto & elem : lockedIfNotDestroyed) + { + if(battleGetWallState(elem.first) != EWallState::DESTROYED) + ret[elem.second] = EAccessibility::DESTRUCTIBLE_WALL; + } + } + + return ret; +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility(const battle::Unit * stack) const +{ + return getAccesibility(battle::Unit::getHexes(stack->getPosition(), stack->doubleWide(), stack->unitSide())); +} + +AccessibilityInfo CBattleInfoCallback::getAccesibility(const std::vector & accessibleHexes) const +{ + auto ret = getAccesibility(); + for(auto hex : accessibleHexes) + if(hex.isValid()) + ret[hex] = EAccessibility::ACCESSIBLE; + + return ret; +} + +ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibility, const ReachabilityInfo::Parameters & params) const +{ + ReachabilityInfo ret; + ret.accessibility = accessibility; + ret.params = params; + + ret.predecessors.fill(BattleHex::INVALID); + ret.distances.fill(ReachabilityInfo::INFINITE_DIST); + + if(!params.startPosition.isValid()) //if got call for arrow turrets + return ret; + + const std::set obstacles = getStoppers(params.perspective); + auto checkParams = params; + checkParams.ignoreKnownAccessible = true; //Ignore starting hexes obstacles + + std::queue hexq; //bfs queue + + //first element + hexq.push(params.startPosition); + ret.distances[params.startPosition] = 0; + + std::array accessibleCache{}; + for(int hex = 0; hex < GameConstants::BFIELD_SIZE; hex++) + accessibleCache[hex] = accessibility.accessible(hex, params.doubleWide, params.side); + + while(!hexq.empty()) //bfs loop + { + const BattleHex curHex = hexq.front(); + hexq.pop(); + + //walking stack can't step past the obstacles + if(isInObstacle(curHex, obstacles, checkParams)) + continue; + + const int costToNeighbour = ret.distances[curHex.hex] + 1; + for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex]) + { + if(neighbour.isValid()) + { + const int costFoundSoFar = ret.distances[neighbour.hex]; + + if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar) + { + hexq.push(neighbour); + ret.distances[neighbour.hex] = costToNeighbour; + ret.predecessors[neighbour.hex] = curHex; + } + } + } + } + + return ret; +} + +bool CBattleInfoCallback::isInObstacle( + BattleHex hex, + const std::set & obstacles, + const ReachabilityInfo::Parameters & params) const +{ + auto occupiedHexes = battle::Unit::getHexes(hex, params.doubleWide, params.side); + + for(auto occupiedHex : occupiedHexes) + { + if(params.ignoreKnownAccessible && vstd::contains(params.knownAccessible, occupiedHex)) + continue; + + if(vstd::contains(obstacles, occupiedHex)) + { + if(occupiedHex == BattleHex::GATE_BRIDGE) + { + if(battleGetGateState() != EGateState::DESTROYED && params.side == BattleSide::ATTACKER) + return true; + } + else + return true; + } + } + + return false; +} + +std::set CBattleInfoCallback::getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const +{ + std::set ret; + RETURN_IF_NOT_BATTLE(ret); + + for(auto &oi : battleGetAllObstacles(whichSidePerspective)) + { + if(!battleIsObstacleVisibleForSide(*oi, whichSidePerspective)) + continue; + + for(const auto & hex : oi->getStoppingTile()) + { + if(hex == BattleHex::GATE_BRIDGE && oi->obstacleType == CObstacleInstance::MOAT) + { + if(battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED) + continue; // this tile is disabled by drawbridge on top of it + } + ret.insert(hex); + } + } + return ret; +} + +std::pair CBattleInfoCallback::getNearestStack(const battle::Unit * closest) const +{ + auto reachability = getReachability(closest); + auto avHexes = battleGetAvailableHexes(reachability, closest, false); + + // I hate std::pairs with their undescriptive member names first / second + struct DistStack + { + uint32_t distanceToPred; + BattleHex destination; + const battle::Unit * stack; + }; + + std::vector stackPairs; + + std::vector possible = battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->isValidTarget(false) && unit != closest; + }); + + for(const battle::Unit * st : possible) + { + for(BattleHex hex : avHexes) + if(CStack::isMeleeAttackPossible(closest, st, hex)) + { + DistStack hlp = {reachability.distances[hex], hex, st}; + stackPairs.push_back(hlp); + } + } + + if(!stackPairs.empty()) + { + auto comparator = [](DistStack lhs, DistStack rhs) { return lhs.distanceToPred < rhs.distanceToPred; }; + auto minimal = boost::min_element(stackPairs, comparator); + return std::make_pair(minimal->stack, minimal->destination); + } + else + return std::make_pair(nullptr, BattleHex::INVALID); +} + +BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const +{ + bool twoHex = VLC->creh->objects[creID]->isDoubleWide(); + + int pos; + if (initialPos > -1) + pos = initialPos; + else //summon elementals depending on player side + { + if(side == BattleSide::ATTACKER) + pos = 0; //top left + else + pos = GameConstants::BFIELD_WIDTH - 1; //top right + } + + auto accessibility = getAccesibility(); + + std::set occupyable; + for(int i = 0; i < accessibility.size(); i++) + if(accessibility.accessible(i, twoHex, side)) + occupyable.insert(i); + + if(occupyable.empty()) + { + return BattleHex::INVALID; //all tiles are covered + } + + return BattleHex::getClosestTile(side, pos, occupyable); +} + +si8 CBattleInfoCallback::battleGetTacticDist() const +{ + RETURN_IF_NOT_BATTLE(0); + + //TODO get rid of this method + if(battleDoWeKnowAbout(battleGetTacticsSide())) + return battleTacticDist(); + + return 0; +} + +bool CBattleInfoCallback::isInTacticRange(BattleHex dest) const +{ + RETURN_IF_NOT_BATTLE(false); + auto side = battleGetTacticsSide(); + auto dist = battleGetTacticDist(); + + return ((!side && dest.getX() > 0 && dest.getX() <= dist) + || (side && dest.getX() < GameConstants::BFIELD_WIDTH - 1 && dest.getX() >= GameConstants::BFIELD_WIDTH - dist - 1)); +} + +ReachabilityInfo CBattleInfoCallback::getReachability(const battle::Unit * unit) const +{ + ReachabilityInfo::Parameters params(unit, unit->getPosition()); + + if(!battleDoWeKnowAbout(unit->unitSide())) + { + //Stack is held by enemy, we can't use his perspective to check for reachability. + // Happens ie. when hovering enemy stack for its range. The arg could be set properly, but it's easier to fix it here. + params.perspective = battleGetMySide(); + } + + return getReachability(params); +} + +ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Parameters ¶ms) const +{ + if(params.flying) + return getFlyingReachability(params); + else + return makeBFS(getAccesibility(params.knownAccessible), params); +} + +ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters ¶ms) const +{ + ReachabilityInfo ret; + ret.accessibility = getAccesibility(params.knownAccessible); + + for(int i = 0; i < GameConstants::BFIELD_SIZE; i++) + { + if(ret.accessibility.accessible(i, params.doubleWide, params.side)) + { + ret.predecessors[i] = params.startPosition; + ret.distances[i] = BattleHex::getDistance(params.startPosition, i); + } + } + + return ret; +} + +AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const +{ + //does not return hex attacked directly + AttackableTiles at; + RETURN_IF_NOT_BATTLE(at); + + BattleHex attackOriginHex = (attackerPos != BattleHex::INVALID) ? attackerPos : attacker->getPosition(); //real or hypothetical (cursor) position + + const auto * defender = battleGetUnitByPos(destinationTile, true); + if (!defender) + return at; // can't attack thin air + + bool reverse = isToReverse(attacker, defender); + if(reverse && attacker->doubleWide()) + { + attackOriginHex = attacker->occupiedHex(attackOriginHex); //the other hex stack stands on + } + if(attacker->hasBonusOfType(BonusType::ATTACKS_ALL_ADJACENT)) + { + boost::copy(attacker->getSurroundingHexes(attackerPos), vstd::set_inserter(at.hostileCreaturePositions)); + } + if(attacker->hasBonusOfType(BonusType::THREE_HEADED_ATTACK)) + { + std::vector hexes = attacker->getSurroundingHexes(attackerPos); + for(BattleHex tile : hexes) + { + if((BattleHex::mutualPosition(tile, destinationTile) > -1 && BattleHex::mutualPosition(tile, attackOriginHex) > -1)) //adjacent both to attacker's head and attacked tile + { + const auto * st = battleGetUnitByPos(tile, true); + if(st && battleMatchOwner(st, attacker)) //only hostile stacks - does it work well with Berserk? + at.hostileCreaturePositions.insert(tile); + } + } + } + if(attacker->hasBonusOfType(BonusType::WIDE_BREATH)) + { + std::vector hexes = destinationTile.neighbouringTiles(); + for(int i = 0; ihasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH)) + { + auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile); + if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation + { + BattleHex nextHex = destinationTile.cloneInDirection(direction, false); + + if ( defender->doubleWide() ) + { + auto secondHex = destinationTile == defender->getPosition() ? + defender->occupiedHex(): + defender->getPosition(); + + // if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin) + // then dragon breath should target tile on the opposite side of targeted creature + if (BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE) + nextHex = secondHex.cloneInDirection(direction, false); + } + + if (nextHex.isValid()) + { + //friendly stacks can also be damaged by Dragon Breath + const auto * st = battleGetUnitByPos(nextHex, true); + if(st != nullptr) + at.friendlyCreaturePositions.insert(nextHex); + } + } + } + return at; +} + +AttackableTiles CBattleInfoCallback::getPotentiallyShootableHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos) const +{ + //does not return hex attacked directly + AttackableTiles at; + RETURN_IF_NOT_BATTLE(at); + + if(attacker->hasBonusOfType(BonusType::SHOOTS_ALL_ADJACENT) && !vstd::contains(attackerPos.neighbouringTiles(), destinationTile)) + { + std::vector targetHexes = destinationTile.neighbouringTiles(); + targetHexes.push_back(destinationTile); + boost::copy(targetHexes, vstd::set_inserter(at.hostileCreaturePositions)); + } + + return at; +} + +std::vector CBattleInfoCallback::getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +{ + std::vector units; + RETURN_IF_NOT_BATTLE(units); + + AttackableTiles at; + + if (rangedAttack) + at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); + else + at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + units = battleGetUnitsIf([=](const battle::Unit * unit) + { + if (unit->isGhost() || !unit->alive()) + return false; + + for (BattleHex hex : battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide())) + { + if (vstd::contains(at.hostileCreaturePositions, hex)) + return true; + if (vstd::contains(at.friendlyCreaturePositions, hex)) + return true; + } + return false; + }); + + return units; +} + +std::set CBattleInfoCallback::getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos) const +{ + std::set attackedCres; + RETURN_IF_NOT_BATTLE(attackedCres); + + AttackableTiles at; + + if(rangedAttack) + at = getPotentiallyShootableHexes(attacker, destinationTile, attackerPos); + else + at = getPotentiallyAttackableHexes(attacker, destinationTile, attackerPos); + + for (BattleHex tile : at.hostileCreaturePositions) //all around & three-headed attack + { + const CStack * st = battleGetStackByPos(tile, true); + if(st && st->unitOwner() != attacker->unitOwner()) //only hostile stacks - does it work well with Berserk? + { + attackedCres.insert(st); + } + } + for (BattleHex tile : at.friendlyCreaturePositions) + { + const CStack * st = battleGetStackByPos(tile, true); + if(st) //friendly stacks can also be damaged by Dragon Breath + { + attackedCres.insert(st); + } + } + return attackedCres; +} + +static bool isHexInFront(BattleHex hex, BattleHex testHex, BattleSide::Type side ) +{ + static const std::set rightDirs { BattleHex::BOTTOM_RIGHT, BattleHex::TOP_RIGHT, BattleHex::RIGHT }; + static const std::set leftDirs { BattleHex::BOTTOM_LEFT, BattleHex::TOP_LEFT, BattleHex::LEFT }; + + auto mutualPos = BattleHex::mutualPosition(hex, testHex); + + if (side == BattleSide::ATTACKER) + return rightDirs.count(mutualPos); + else + return leftDirs.count(mutualPos); +} + +//TODO: this should apply also to mechanics and cursor interface +bool CBattleInfoCallback::isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const +{ + BattleHex attackerHex = attacker->getPosition(); + BattleHex defenderHex = defender->getPosition(); + + if (attackerHex < 0 ) //turret + return false; + + if(isHexInFront(attackerHex, defenderHex, static_cast(attacker->unitSide()))) + return false; + + if (defender->doubleWide()) + { + if(isHexInFront(attackerHex, defender->occupiedHex(), static_cast(attacker->unitSide()))) + return false; + } + + if (attacker->doubleWide()) + { + if(isHexInFront(attacker->occupiedHex(), defenderHex, static_cast(attacker->unitSide()))) + return false; + } + + // a bit weird case since here defender is slightly behind attacker, so reversing seems preferable, + // but this is how H3 handles it which is important, e.g. for direction of dragon breath attacks + if (attacker->doubleWide() && defender->doubleWide()) + { + if(isHexInFront(attacker->occupiedHex(), defender->occupiedHex(), static_cast(attacker->unitSide()))) + return false; + } + return true; +} + +ReachabilityInfo::TDistances CBattleInfoCallback::battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const +{ + ReachabilityInfo::TDistances ret; + ret.fill(-1); + RETURN_IF_NOT_BATTLE(ret); + + auto reachability = getReachability(unit); + + boost::copy(reachability.distances, ret.begin()); + + return ret; +} + +bool CBattleInfoCallback::battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const +{ + RETURN_IF_NOT_BATTLE(false); + + const std::string cachingStrNoDistancePenalty = "type_NO_DISTANCE_PENALTY"; + static const auto selectorNoDistancePenalty = Selector::type()(BonusType::NO_DISTANCE_PENALTY); + + if(shooter->hasBonus(selectorNoDistancePenalty, cachingStrNoDistancePenalty)) + return false; + + if(const auto * target = battleGetUnitByPos(destHex, true)) + { + //If any hex of target creature is within range, there is no penalty + int range = GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE; + + auto bonus = shooter->getBonus(Selector::type()(BonusType::LIMITED_SHOOTING_RANGE)); + if(bonus != nullptr && bonus->additionalInfo != CAddInfo::NONE) + range = bonus->additionalInfo[0]; + + if(isEnemyUnitWithinSpecifiedRange(shooterPosition, target, range)) + return false; + } + else + { + if(BattleHex::getDistance(shooterPosition, destHex) <= GameConstants::BATTLE_SHOOTING_PENALTY_DISTANCE) + return false; + } + + return true; +} + +bool CBattleInfoCallback::isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const +{ + for(auto hex : defenderUnit->getHexes()) + if(BattleHex::getDistance(attackerPosition, hex) <= range) + return true; + + return false; +} + +BattleHex CBattleInfoCallback::wallPartToBattleHex(EWallPart part) const +{ + RETURN_IF_NOT_BATTLE(BattleHex::INVALID); + return WallPartToHex(part); +} + +EWallPart CBattleInfoCallback::battleHexToWallPart(BattleHex hex) const +{ + RETURN_IF_NOT_BATTLE(EWallPart::INVALID); + return hexToWallPart(hex); +} + +bool CBattleInfoCallback::isWallPartPotentiallyAttackable(EWallPart wallPart) const +{ + RETURN_IF_NOT_BATTLE(false); + return wallPart != EWallPart::INDESTRUCTIBLE_PART && wallPart != EWallPart::INDESTRUCTIBLE_PART_OF_GATE && + wallPart != EWallPart::INVALID; +} + +bool CBattleInfoCallback::isWallPartAttackable(EWallPart wallPart) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(isWallPartPotentiallyAttackable(wallPart)) + { + auto wallState = battleGetWallState(wallPart); + return (wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED); + } + return false; +} + +std::vector CBattleInfoCallback::getAttackableBattleHexes() const +{ + std::vector attackableBattleHexes; + RETURN_IF_NOT_BATTLE(attackableBattleHexes); + + for(const auto & wallPartPair : wallParts) + { + if(isWallPartAttackable(wallPartPair.second)) + attackableBattleHexes.emplace_back(wallPartPair.first); + } + + return attackableBattleHexes; +} + +int32_t CBattleInfoCallback::battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const +{ + RETURN_IF_NOT_BATTLE(-1); + //TODO should be replaced using bonus system facilities (propagation onto battle node) + + int32_t ret = caster->getSpellCost(sp); + + //checking for friendly stacks reducing cost of the spell and + //enemy stacks increasing it + int32_t manaReduction = 0; + int32_t manaIncrease = 0; + + for(const auto * unit : battleAliveUnits()) + { + if(unit->unitOwner() == caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ALLY)) + { + vstd::amax(manaReduction, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ALLY)); + } + if(unit->unitOwner() != caster->tempOwner && unit->hasBonusOfType(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)) + { + vstd::amax(manaIncrease, unit->valOfBonuses(BonusType::CHANGES_SPELL_COST_FOR_ENEMY)); + } + } + + return std::max(0, ret - manaReduction + manaIncrease); +} + +bool CBattleInfoCallback::battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const +{ + return battleHasDistancePenalty(shooter, shooter->getPosition(), destHex) || battleHasWallPenalty(shooter, shooter->getPosition(), destHex); +} + +bool CBattleInfoCallback::battleIsUnitBlocked(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(false); + + if(unit->hasBonusOfType(BonusType::SIEGE_WEAPON)) //siege weapons cannot be blocked + return false; + + for(const auto * adjacent : battleAdjacentUnits(unit)) + { + if(adjacent->unitOwner() != unit->unitOwner()) //blocked by enemy stack + return true; + } + return false; +} + +std::set CBattleInfoCallback::battleAdjacentUnits(const battle::Unit * unit) const +{ + std::set ret; + RETURN_IF_NOT_BATTLE(ret); + + for(auto hex : unit->getSurroundingHexes()) + { + if(const auto * neighbour = battleGetUnitByPos(hex, true)) + ret.insert(neighbour); + } + + return ret; +} + +SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const +{ + RETURN_IF_NOT_BATTLE(SpellID::NONE); + //This is complete list. No spells from mods. + //todo: this should be Spellbook of caster Stack + static const std::set allPossibleSpells = + { + SpellID::AIR_SHIELD, + SpellID::ANTI_MAGIC, + SpellID::BLESS, + SpellID::BLOODLUST, + SpellID::COUNTERSTRIKE, + SpellID::CURE, + SpellID::FIRE_SHIELD, + SpellID::FORTUNE, + SpellID::HASTE, + SpellID::MAGIC_MIRROR, + SpellID::MIRTH, + SpellID::PRAYER, + SpellID::PRECISION, + SpellID::PROTECTION_FROM_AIR, + SpellID::PROTECTION_FROM_EARTH, + SpellID::PROTECTION_FROM_FIRE, + SpellID::PROTECTION_FROM_WATER, + SpellID::SHIELD, + SpellID::SLAYER, + SpellID::STONE_SKIN + }; + std::vector beneficialSpells; + + auto getAliveEnemy = [=](const std::function & pred) -> const CStack * + { + auto stacks = battleGetStacksIf([=](const CStack * stack) + { + return pred(stack) && stack->unitOwner() != subject->unitOwner() && stack->isValidTarget(false); + }); + + if(stacks.empty()) + return nullptr; + else + return stacks.front(); + }; + + for(const SpellID& spellID : allPossibleSpells) + { + std::stringstream cachingStr; + cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; + + if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, spellID), Selector::all, cachingStr.str()) + //TODO: this ability has special limitations + || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) + continue; + + switch (spellID) + { + case SpellID::SHIELD: + case SpellID::FIRE_SHIELD: // not if all enemy units are shooters + { + const auto * walker = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack + { + return !stack->canShoot(); + }); + + if(!walker) + continue; + } + break; + case SpellID::AIR_SHIELD: //only against active shooters + { + const auto * shooter = getAliveEnemy([&](const CStack * stack) //look for enemy, non-shooting stack + { + return stack->canShoot(); + }); + if(!shooter) + continue; + } + break; + case SpellID::ANTI_MAGIC: + case SpellID::MAGIC_MIRROR: + case SpellID::PROTECTION_FROM_AIR: + case SpellID::PROTECTION_FROM_EARTH: + case SpellID::PROTECTION_FROM_FIRE: + case SpellID::PROTECTION_FROM_WATER: + { + const ui8 enemySide = 1 - subject->unitSide(); + //todo: only if enemy has spellbook + if (!battleHasHero(enemySide)) //only if there is enemy hero + continue; + } + break; + case SpellID::CURE: //only damaged units + { + //do not cast on affected by debuffs + if(!subject->canBeHealed()) + continue; + } + break; + case SpellID::BLOODLUST: + { + if(subject->canShoot()) //TODO: if can shoot - only if enemy units are adjacent + continue; + } + break; + case SpellID::PRECISION: + { + if(!subject->canShoot()) + continue; + } + break; + case SpellID::SLAYER://only if monsters are present + { + const auto * kingMonster = getAliveEnemy([&](const CStack * stack) -> bool //look for enemy, non-shooting stack + { + const auto isKing = Selector::type()(BonusType::KING); + + return stack->hasBonus(isKing); + }); + + if (!kingMonster) + continue; + } + break; + } + beneficialSpells.push_back(spellID); + } + + if(!beneficialSpells.empty()) + { + return *RandomGeneratorUtil::nextItem(beneficialSpells, rand); + } + else + { + return SpellID::NONE; + } +} + +SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const CStack * caster) const +{ + RETURN_IF_NOT_BATTLE(SpellID::NONE); + + TConstBonusListPtr bl = caster->getBonuses(Selector::type()(BonusType::SPELLCASTER)); + if (!bl->size()) + return SpellID::NONE; + + if(bl->size() == 1) + return SpellID(bl->front()->subtype); + + int totalWeight = 0; + for(const auto & b : *bl) + { + totalWeight += std::max(b->additionalInfo[0], 0); //spells with 0 weight are non-random, exclude them + } + + if (totalWeight == 0) + return SpellID::NONE; + + int randomPos = rand.nextInt(totalWeight - 1); + for(const auto & b : *bl) + { + randomPos -= std::max(b->additionalInfo[0], 0); + if(randomPos < 0) + { + return SpellID(b->subtype); + } + } + + return SpellID::NONE; +} + +int CBattleInfoCallback::battleGetSurrenderCost(const PlayerColor & Player) const +{ + RETURN_IF_NOT_BATTLE(-3); + if(!battleCanSurrender(Player)) + return -1; + + const auto sideOpt = playerToSide(Player); + if(!sideOpt) + return -1; + const auto side = sideOpt.value(); + + int ret = 0; + double discount = 0; + + for(const auto * unit : battleAliveUnits(side)) + ret += unit->getRawSurrenderCost(); + + if(const CGHeroInstance * h = battleGetFightingHero(side)) + discount += h->valOfBonuses(BonusType::SURRENDER_DISCOUNT); + + ret = static_cast(ret * (100.0 - discount) / 100.0); + vstd::amax(ret, 0); //no negative costs for >100% discounts (impossible in original H3 mechanics, but some day...) + return ret; +} + +si8 CBattleInfoCallback::battleMinSpellLevel(ui8 side) const +{ + const IBonusBearer * node = nullptr; + if(const CGHeroInstance * h = battleGetFightingHero(side)) + node = h; + else + node = getBonusBearer(); + + if(!node) + return 0; + + auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_BELOW)); + if(b->size()) + return b->totalValue(); + + return 0; +} + +si8 CBattleInfoCallback::battleMaxSpellLevel(ui8 side) const +{ + const IBonusBearer *node = nullptr; + if(const CGHeroInstance * h = battleGetFightingHero(side)) + node = h; + else + node = getBonusBearer(); + + if(!node) + return GameConstants::SPELL_LEVELS; + + //We can't "just get value" - it'd be 0 if there are bonuses (and all would be blocked) + auto b = node->getBonuses(Selector::type()(BonusType::BLOCK_MAGIC_ABOVE)); + if(b->size()) + return b->totalValue(); + + return GameConstants::SPELL_LEVELS; +} + +std::optional CBattleInfoCallback::battleIsFinished() const +{ + auto units = battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->alive() && !unit->isTurret() && !unit->hasBonusOfType(BonusType::SIEGE_WEAPON); + }); + + std::array hasUnit = {false, false}; //index is BattleSide + + for(auto & unit : units) + { + //todo: move SIEGE_WEAPON check to Unit state + hasUnit.at(unit->unitSide()) = true; + + if(hasUnit[0] && hasUnit[1]) + return std::nullopt; + } + + hasUnit = {false, false}; + + for(auto & unit : units) + { + if(!unit->isClone() && !unit->acquireState()->summoned && !dynamic_cast (unit)) + { + hasUnit.at(unit->unitSide()) = true; + } + } + + if(!hasUnit[0] && !hasUnit[1]) + return 2; + if(!hasUnit[1]) + return 0; + else + return 1; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoCallback.h b/lib/battle/CBattleInfoCallback.h index b0d30a0c3..a05027a5d 100644 --- a/lib/battle/CBattleInfoCallback.h +++ b/lib/battle/CBattleInfoCallback.h @@ -1,156 +1,156 @@ -/* - * CBattleInfoCallback.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 - -#include "ReachabilityInfo.h" -#include "BattleAttackInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CStack; -class ISpellCaster; -class SpellCastEnvironment; -class CSpell; -struct CObstacleInstance; -class IBonusBearer; -class CRandomGenerator; -class PossiblePlayerBattleAction; - -namespace spells -{ - class Caster; - class Spell; -} - -struct DLL_LINKAGE AttackableTiles -{ - std::set hostileCreaturePositions; - std::set friendlyCreaturePositions; //for Dragon Breath - template void serialize(Handler &h, const int version) - { - h & hostileCreaturePositions; - h & friendlyCreaturePositions; - } -}; - -struct DLL_LINKAGE BattleClientInterfaceData -{ - std::vector creatureSpellsToCast; - ui8 tacticsMode; -}; - -class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials -{ -public: - enum ERandomSpell - { - RANDOM_GENIE, RANDOM_AIMED - }; - - std::optional battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw - - std::vector> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override; - std::vector> getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const override; - //Handle obstacle damage here, requires SpellCastEnvironment - bool handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed = {}) const; - - const CStack * battleGetStackByPos(BattleHex pos, bool onlyAlive = true) const; - - const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const override; - - ///returns all alive units excluding turrets - battle::Units battleAliveUnits() const; - ///returns all alive units from particular side excluding turrets - battle::Units battleAliveUnits(ui8 side) const; - - void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, int8_t lastMoved = -1) const; - - ///returns reachable hexes (valid movement destinations), DOES contain stack current position - std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const; - - ///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version) - std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const; - - std::vector battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const; - - int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible - ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const; - std::set battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const; - bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const; - - std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; - - bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination - bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination - bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle - bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack - std::set battleAdjacentUnits(const battle::Unit * unit) const; - - DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; - - /// estimates damage dealt by attacker to defender; - /// only non-random bonuses are considered in estimation - /// returns pair - DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; - DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; - DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const; - - bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const; - bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; - bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; - bool battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const; - - BattleHex wallPartToBattleHex(EWallPart part) const; - EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found - bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not - bool isWallPartAttackable(EWallPart wallPart) const; // returns true if the wall part is actually attackable, false if not - std::vector getAttackableBattleHexes() const; - - si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned - si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned - int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell - ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell - - SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; - SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; - SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon - - std::vector getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data); - PossiblePlayerBattleAction getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const; - - //convenience methods using the ones above - bool isInTacticRange(BattleHex dest) const; - si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side) - - AttackableTiles getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker - AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; - std::vector getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks - std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks - bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const; //determines if attacker standing at attackerHex should reverse in order to attack defender - - ReachabilityInfo getReachability(const battle::Unit * unit) const; - ReachabilityInfo getReachability(const ReachabilityInfo::Parameters & params) const; - AccessibilityInfo getAccesibility() const; - AccessibilityInfo getAccesibility(const battle::Unit * stack) const; //Hexes ocupied by stack will be marked as accessible. - AccessibilityInfo getAccesibility(const std::vector & accessibleHexes) const; //given hexes will be marked as accessible - std::pair getNearestStack(const battle::Unit * closest) const; - - BattleHex getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos = -1) const; //find place for adding new stack -protected: - ReachabilityInfo getFlyingReachability(const ReachabilityInfo::Parameters & params) const; - ReachabilityInfo makeBFS(const AccessibilityInfo & accessibility, const ReachabilityInfo::Parameters & params) const; - bool isInObstacle(BattleHex hex, const std::set & obstacles, const ReachabilityInfo::Parameters & params) const; - std::set getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const; //get hexes with stopping obstacles (quicksands) -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoCallback.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 + +#include "ReachabilityInfo.h" +#include "BattleAttackInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CStack; +class ISpellCaster; +class SpellCastEnvironment; +class CSpell; +struct CObstacleInstance; +class IBonusBearer; +class CRandomGenerator; +class PossiblePlayerBattleAction; + +namespace spells +{ + class Caster; + class Spell; +} + +struct DLL_LINKAGE AttackableTiles +{ + std::set hostileCreaturePositions; + std::set friendlyCreaturePositions; //for Dragon Breath + template void serialize(Handler &h, const int version) + { + h & hostileCreaturePositions; + h & friendlyCreaturePositions; + } +}; + +struct DLL_LINKAGE BattleClientInterfaceData +{ + std::vector creatureSpellsToCast; + ui8 tacticsMode; +}; + +class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials +{ +public: + enum ERandomSpell + { + RANDOM_GENIE, RANDOM_AIMED + }; + + std::optional battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw + + std::vector> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override; + std::vector> getAllAffectedObstaclesByStack(const battle::Unit * unit, const std::set & passed) const override; + //Handle obstacle damage here, requires SpellCastEnvironment + bool handleObstacleTriggersForUnit(SpellCastEnvironment & spellEnv, const battle::Unit & unit, const std::set & passed = {}) const; + + const CStack * battleGetStackByPos(BattleHex pos, bool onlyAlive = true) const; + + const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const override; + + ///returns all alive units excluding turrets + battle::Units battleAliveUnits() const; + ///returns all alive units from particular side excluding turrets + battle::Units battleAliveUnits(ui8 side) const; + + void battleGetTurnOrder(std::vector & out, const size_t maxUnits, const int maxTurns, const int turn = 0, int8_t lastMoved = -1) const; + + ///returns reachable hexes (valid movement destinations), DOES contain stack current position + std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange, bool addOccupiable, std::vector * attackable) const; + + ///returns reachable hexes (valid movement destinations), DOES contain stack current position (lite version) + std::vector battleGetAvailableHexes(const battle::Unit * unit, bool obtainMovementRange) const; + + std::vector battleGetAvailableHexes(const ReachabilityInfo & cache, const battle::Unit * unit, bool obtainMovementRange) const; + + int battleGetSurrenderCost(const PlayerColor & Player) const; //returns cost of surrendering battle, -1 if surrendering is not possible + ReachabilityInfo::TDistances battleGetDistances(const battle::Unit * unit, BattleHex assumedPosition) const; + std::set battleGetAttackedHexes(const battle::Unit * attacker, BattleHex destinationTile, BattleHex attackerPos = BattleHex::INVALID) const; + bool isEnemyUnitWithinSpecifiedRange(BattleHex attackerPosition, const battle::Unit * defenderUnit, unsigned int range) const; + + std::pair< std::vector, int > getPath(BattleHex start, BattleHex dest, const battle::Unit * stack) const; + + bool battleCanAttack(const battle::Unit * stack, const battle::Unit * target, BattleHex dest) const; //determines if stack with given ID can attack target at the selected destination + bool battleCanShoot(const battle::Unit * attacker, BattleHex dest) const; //determines if stack with given ID shoot at the selected destination + bool battleCanShoot(const battle::Unit * attacker) const; //determines if stack with given ID shoot in principle + bool battleIsUnitBlocked(const battle::Unit * unit) const; //returns true if there is neighboring enemy stack + std::set battleAdjacentUnits(const battle::Unit * unit) const; + + DamageEstimation calculateDmgRange(const BattleAttackInfo & info) const; + + /// estimates damage dealt by attacker to defender; + /// only non-random bonuses are considered in estimation + /// returns pair + DamageEstimation battleEstimateDamage(const BattleAttackInfo & bai, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPosition, DamageEstimation * retaliationDmg = nullptr) const; + DamageEstimation battleEstimateDamage(const battle::Unit * attacker, const battle::Unit * defender, int movementDistance, DamageEstimation * retaliationDmg = nullptr) const; + + bool battleHasPenaltyOnLine(BattleHex from, BattleHex dest, bool checkWall, bool checkMoat) const; + bool battleHasDistancePenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; + bool battleHasWallPenalty(const IBonusBearer * shooter, BattleHex shooterPosition, BattleHex destHex) const; + bool battleHasShootingPenalty(const battle::Unit * shooter, BattleHex destHex) const; + + BattleHex wallPartToBattleHex(EWallPart part) const; + EWallPart battleHexToWallPart(BattleHex hex) const; //returns part of destructible wall / gate / keep under given hex or -1 if not found + bool isWallPartPotentiallyAttackable(EWallPart wallPart) const; // returns true if the wall part is potentially attackable (independent of wall state), false if not + bool isWallPartAttackable(EWallPart wallPart) const; // returns true if the wall part is actually attackable, false if not + std::vector getAttackableBattleHexes() const; + + si8 battleMinSpellLevel(ui8 side) const; //calculates maximum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned + si8 battleMaxSpellLevel(ui8 side) const; //calculates minimum spell level possible to be cast on battlefield - takes into account artifacts of both heroes; if no effects are set, 0 is returned + int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell + ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell + + SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const; + SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const; + SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon + + std::vector getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data); + PossiblePlayerBattleAction getCasterAction(const CSpell * spell, const spells::Caster * caster, spells::Mode mode) const; + + //convenience methods using the ones above + bool isInTacticRange(BattleHex dest) const; + si8 battleGetTacticDist() const; //returns tactic distance for calling player or 0 if this player is not in tactic phase (for ALL_KNOWING actual distance for tactic side) + + AttackableTiles getPotentiallyAttackableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; //TODO: apply rotation to two-hex attacker + AttackableTiles getPotentiallyShootableHexes(const battle::Unit* attacker, BattleHex destinationTile, BattleHex attackerPos) const; + std::vector getAttackedBattleUnits(const battle::Unit* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + std::set getAttackedCreatures(const CStack* attacker, BattleHex destinationTile, bool rangedAttack, BattleHex attackerPos = BattleHex::INVALID) const; //calculates range of multi-hex attacks + bool isToReverse(const battle::Unit * attacker, const battle::Unit * defender) const; //determines if attacker standing at attackerHex should reverse in order to attack defender + + ReachabilityInfo getReachability(const battle::Unit * unit) const; + ReachabilityInfo getReachability(const ReachabilityInfo::Parameters & params) const; + AccessibilityInfo getAccesibility() const; + AccessibilityInfo getAccesibility(const battle::Unit * stack) const; //Hexes ocupied by stack will be marked as accessible. + AccessibilityInfo getAccesibility(const std::vector & accessibleHexes) const; //given hexes will be marked as accessible + std::pair getNearestStack(const battle::Unit * closest) const; + + BattleHex getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos = -1) const; //find place for adding new stack +protected: + ReachabilityInfo getFlyingReachability(const ReachabilityInfo::Parameters & params) const; + ReachabilityInfo makeBFS(const AccessibilityInfo & accessibility, const ReachabilityInfo::Parameters & params) const; + bool isInObstacle(BattleHex hex, const std::set & obstacles, const ReachabilityInfo::Parameters & params) const; + std::set getStoppers(BattlePerspective::BattlePerspective whichSidePerspective) const; //get hexes with stopping obstacles (quicksands) +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index 36d827e80..5e896a9eb 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -1,441 +1,441 @@ -/* - * CBattleInfoEssentials.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 "CBattleInfoEssentials.h" -#include "../CStack.h" -#include "BattleInfo.h" -#include "../NetPacks.h" -#include "../mapObjects/CGTownInstance.h" -#include "../gameState/InfoAboutArmy.h" - -VCMI_LIB_NAMESPACE_BEGIN - -bool CBattleInfoEssentials::duringBattle() const -{ - return getBattle() != nullptr; -} - -TerrainId CBattleInfoEssentials::battleTerrainType() const -{ - RETURN_IF_NOT_BATTLE(TerrainId()); - return getBattle()->getTerrainType(); -} - -BattleField CBattleInfoEssentials::battleGetBattlefieldType() const -{ - RETURN_IF_NOT_BATTLE(BattleField::NONE); - return getBattle()->getBattlefieldType(); -} - -int32_t CBattleInfoEssentials::battleGetEnchanterCounter(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(0); - return getBattle()->getEnchanterCounter(side); -} - -std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const -{ - std::vector > ret; - RETURN_IF_NOT_BATTLE(ret); - - if(!perspective) - { - //if no particular perspective request, use default one - perspective = std::make_optional(battleGetMySide()); - } - else - { - if(!!getPlayerID() && *perspective != battleGetMySide()) - logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell"); - } - - for(const auto & obstacle : getBattle()->getAllObstacles()) - { - if(battleIsObstacleVisibleForSide(*(obstacle), *perspective)) - ret.push_back(obstacle); - } - - return ret; -} - -std::shared_ptr CBattleInfoEssentials::battleGetObstacleByID(uint32_t ID) const -{ - std::shared_ptr ret; - - RETURN_IF_NOT_BATTLE(std::shared_ptr()); - - for(auto obstacle : getBattle()->getAllObstacles()) - { - if(obstacle->uniqueID == ID) - return obstacle; - } - - logGlobal->error("Invalid obstacle ID %d", ID); - return std::shared_ptr(); -} - -bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const -{ - RETURN_IF_NOT_BATTLE(false); - return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); -} - -bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - - for(const auto * s : battleGetAllStacks()) - { - if(s->unitSide() == side && s->isNativeTerrain(getBattle()->getTerrainType())) - return true; - } - - return false; -} - -TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const -{ - return battleGetStacksIf([=](const CStack * s) - { - return !s->isGhost() && (includeTurrets || !s->isTurret()); - }); -} - -TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate) const -{ - RETURN_IF_NOT_BATTLE(TStacks()); - return getBattle()->getStacksIf(std::move(predicate)); -} - -battle::Units CBattleInfoEssentials::battleGetUnitsIf(battle::UnitFilter predicate) const -{ - RETURN_IF_NOT_BATTLE(battle::Units()); - return getBattle()->getUnitsIf(predicate); -} - -const battle::Unit * CBattleInfoEssentials::battleGetUnitByID(uint32_t ID) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - //TODO: consider using map ID -> Unit - - auto ret = battleGetUnitsIf([=](const battle::Unit * unit) - { - return unit->unitId() == ID; - }); - - if(ret.empty()) - return nullptr; - else - return ret[0]; -} - -const battle::Unit * CBattleInfoEssentials::battleActiveUnit() const -{ - RETURN_IF_NOT_BATTLE(nullptr); - auto id = getBattle()->getActiveStackID(); - if(id >= 0) - return battleGetUnitByID(static_cast(id)); - else - return nullptr; -} - -uint32_t CBattleInfoEssentials::battleNextUnitId() const -{ - return getBattle()->nextUnitId(); -} - -const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - return getBattle()->getDefendedTown(); -} - -BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const -{ - RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); - if(!getPlayerID() || getPlayerID()->isSpectator()) - return BattlePerspective::ALL_KNOWING; - if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER)) - return BattlePerspective::LEFT_SIDE; - if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER)) - return BattlePerspective::RIGHT_SIDE; - - logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString()); - return BattlePerspective::INVALID; -} - -const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - - auto stacks = battleGetStacksIf([=](const CStack * s) - { - return s->unitId() == ID && (!onlyAlive || s->alive()); - }); - - if(stacks.empty()) - return nullptr; - else - return stacks[0]; -} - -bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - auto p = battleGetMySide(); - return p == BattlePerspective::ALL_KNOWING || p == side; -} - -si8 CBattleInfoEssentials::battleTacticDist() const -{ - RETURN_IF_NOT_BATTLE(0); - return getBattle()->getTacticDist(); -} - -si8 CBattleInfoEssentials::battleGetTacticsSide() const -{ - RETURN_IF_NOT_BATTLE(-1); - return getBattle()->getTacticsSide(); -} - -const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) - { - logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); - return nullptr; - } - - if(!battleDoWeKnowAbout(side)) - { - logGlobal->error("FIXME: %s access check ", __FUNCTION__); - return nullptr; - } - - return getBattle()->getSideHero(side); -} - -const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - if(side > 1) - { - logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); - return nullptr; - } - if(!battleDoWeKnowAbout(side)) - { - logGlobal->error("FIXME: %s access check!", __FUNCTION__); - return nullptr; - } - return getBattle()->getSideArmy(side); -} - -InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const -{ - const auto * hero = getBattle()->getSideHero(side); - if(!hero) - { - return InfoAboutHero(); - } - InfoAboutHero::EInfoLevel infoLevel = battleDoWeKnowAbout(side) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC; - return InfoAboutHero(hero, infoLevel); -} - -uint32_t CBattleInfoEssentials::battleCastSpells(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(-1); - return getBattle()->getCastSpells(side); -} - -const IBonusBearer * CBattleInfoEssentials::getBonusBearer() const -{ - return getBattle()->getBonusBearer(); -} - -bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(!side) - return false; - - const CGHeroInstance * myHero = battleGetFightingHero(side.value()); - - //current player have no hero - if(!myHero) - return false; - - //eg. one of heroes is wearing shakles of war - if(myHero->hasBonusOfType(BonusType::BATTLE_NO_FLEEING)) - return false; - - //we are besieged defender - if(side == BattleSide::DEFENDER && battleGetSiegeLevel()) - { - const auto * town = battleGetDefendedTown(); - if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) - return false; - } - - return true; -} - -BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(std::nullopt); - - if(getBattle()->getSidePlayer(BattleSide::ATTACKER) == player) - return BattleSideOpt(BattleSide::ATTACKER); - - if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) - return BattleSideOpt(BattleSide::DEFENDER); - - logGlobal->warn("Cannot find side for player %s", player.toString()); - - return std::nullopt; -} - -PlayerColor CBattleInfoEssentials::sideToPlayer(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - return getBattle()->getSidePlayer(side); -} - -ui8 CBattleInfoEssentials::otherSide(ui8 side) const -{ - if(side == BattleSide::ATTACKER) - return BattleSide::DEFENDER; - else - return BattleSide::ATTACKER; -} - -PlayerColor CBattleInfoEssentials::otherPlayer(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - - auto side = playerToSide(player); - if(!side) - return PlayerColor::CANNOT_DETERMINE; - - return getBattle()->getSidePlayer(otherSide(side.value())); -} - -bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(side) - { - auto opponentSide = otherSide(side.value()); - if(getBattle()->getSideHero(opponentSide) == h) - return true; - } - return false; -} - -ui8 CBattleInfoEssentials::battleGetSiegeLevel() const -{ - RETURN_IF_NOT_BATTLE(CGTownInstance::NONE); - return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE; -} - -bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const -{ - RETURN_IF_NOT_BATTLE(false); - const auto side = playerToSide(player); - if(!side) - return false; - bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && battleGetSiegeLevel()); - //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero - return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); -} - -bool CBattleInfoEssentials::battleHasHero(ui8 side) const -{ - RETURN_IF_NOT_BATTLE(false); - return getBattle()->getSideHero(side) != nullptr; -} - -EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const -{ - RETURN_IF_NOT_BATTLE(EWallState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return EWallState::NONE; - - return getBattle()->getWallState(partOfWall); -} - -EGateState CBattleInfoEssentials::battleGetGateState() const -{ - RETURN_IF_NOT_BATTLE(EGateState::NONE); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return EGateState::NONE; - - return getBattle()->getGateState(); -} - -bool CBattleInfoEssentials::battleIsGatePassable() const -{ - RETURN_IF_NOT_BATTLE(true); - if(battleGetSiegeLevel() == CGTownInstance::NONE) - return true; - - return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; -} - -PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); - - PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); - - static CSelector selector = Selector::type()(BonusType::HYPNOTIZED); - static std::string cachingString = "type_103s-1"; - - if(unit->hasBonus(selector, cachingString)) - return otherPlayer(initialOwner); - else - return initialOwner; -} - -const CGHeroInstance * CBattleInfoEssentials::battleGetOwnerHero(const battle::Unit * unit) const -{ - RETURN_IF_NOT_BATTLE(nullptr); - const auto side = playerToSide(battleGetOwner(unit)); - if(!side) - return nullptr; - return getBattle()->getSideHero(side.value()); -} - -bool CBattleInfoEssentials::battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const -{ - RETURN_IF_NOT_BATTLE(false); - if(boost::logic::indeterminate(positivness)) - return true; - else if(attacker->unitId() == defender->unitId()) - return (bool)positivness; - else - return battleMatchOwner(battleGetOwner(attacker), defender, positivness); -} - -bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const -{ - RETURN_IF_NOT_BATTLE(false); - - PlayerColor initialOwner = getBattle()->getSidePlayer(defender->unitSide()); - - return boost::logic::indeterminate(positivness) || (attacker == initialOwner) == (bool)positivness; -} - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoEssentials.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 "CBattleInfoEssentials.h" +#include "../CStack.h" +#include "BattleInfo.h" +#include "../NetPacks.h" +#include "../mapObjects/CGTownInstance.h" +#include "../gameState/InfoAboutArmy.h" + +VCMI_LIB_NAMESPACE_BEGIN + +bool CBattleInfoEssentials::duringBattle() const +{ + return getBattle() != nullptr; +} + +TerrainId CBattleInfoEssentials::battleTerrainType() const +{ + RETURN_IF_NOT_BATTLE(TerrainId()); + return getBattle()->getTerrainType(); +} + +BattleField CBattleInfoEssentials::battleGetBattlefieldType() const +{ + RETURN_IF_NOT_BATTLE(BattleField::NONE); + return getBattle()->getBattlefieldType(); +} + +int32_t CBattleInfoEssentials::battleGetEnchanterCounter(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(0); + return getBattle()->getEnchanterCounter(side); +} + +std::vector> CBattleInfoEssentials::battleGetAllObstacles(std::optional perspective) const +{ + std::vector > ret; + RETURN_IF_NOT_BATTLE(ret); + + if(!perspective) + { + //if no particular perspective request, use default one + perspective = std::make_optional(battleGetMySide()); + } + else + { + if(!!getPlayerID() && *perspective != battleGetMySide()) + logGlobal->warn("Unauthorized obstacles access attempt, assuming massive spell"); + } + + for(const auto & obstacle : getBattle()->getAllObstacles()) + { + if(battleIsObstacleVisibleForSide(*(obstacle), *perspective)) + ret.push_back(obstacle); + } + + return ret; +} + +std::shared_ptr CBattleInfoEssentials::battleGetObstacleByID(uint32_t ID) const +{ + std::shared_ptr ret; + + RETURN_IF_NOT_BATTLE(std::shared_ptr()); + + for(auto obstacle : getBattle()->getAllObstacles()) + { + if(obstacle->uniqueID == ID) + return obstacle; + } + + logGlobal->error("Invalid obstacle ID %d", ID); + return std::shared_ptr(); +} + +bool CBattleInfoEssentials::battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const +{ + RETURN_IF_NOT_BATTLE(false); + return side == BattlePerspective::ALL_KNOWING || coi.visibleForSide(side, battleHasNativeStack(side)); +} + +bool CBattleInfoEssentials::battleHasNativeStack(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + + for(const auto * s : battleGetAllStacks()) + { + if(s->unitSide() == side && s->isNativeTerrain(getBattle()->getTerrainType())) + return true; + } + + return false; +} + +TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const +{ + return battleGetStacksIf([=](const CStack * s) + { + return !s->isGhost() && (includeTurrets || !s->isTurret()); + }); +} + +TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate) const +{ + RETURN_IF_NOT_BATTLE(TStacks()); + return getBattle()->getStacksIf(std::move(predicate)); +} + +battle::Units CBattleInfoEssentials::battleGetUnitsIf(battle::UnitFilter predicate) const +{ + RETURN_IF_NOT_BATTLE(battle::Units()); + return getBattle()->getUnitsIf(predicate); +} + +const battle::Unit * CBattleInfoEssentials::battleGetUnitByID(uint32_t ID) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + //TODO: consider using map ID -> Unit + + auto ret = battleGetUnitsIf([=](const battle::Unit * unit) + { + return unit->unitId() == ID; + }); + + if(ret.empty()) + return nullptr; + else + return ret[0]; +} + +const battle::Unit * CBattleInfoEssentials::battleActiveUnit() const +{ + RETURN_IF_NOT_BATTLE(nullptr); + auto id = getBattle()->getActiveStackID(); + if(id >= 0) + return battleGetUnitByID(static_cast(id)); + else + return nullptr; +} + +uint32_t CBattleInfoEssentials::battleNextUnitId() const +{ + return getBattle()->nextUnitId(); +} + +const CGTownInstance * CBattleInfoEssentials::battleGetDefendedTown() const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + return getBattle()->getDefendedTown(); +} + +BattlePerspective::BattlePerspective CBattleInfoEssentials::battleGetMySide() const +{ + RETURN_IF_NOT_BATTLE(BattlePerspective::INVALID); + if(!getPlayerID() || getPlayerID()->isSpectator()) + return BattlePerspective::ALL_KNOWING; + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::ATTACKER)) + return BattlePerspective::LEFT_SIDE; + if(*getPlayerID() == getBattle()->getSidePlayer(BattleSide::DEFENDER)) + return BattlePerspective::RIGHT_SIDE; + + logGlobal->error("Cannot find player %s in battle!", getPlayerID()->toString()); + return BattlePerspective::INVALID; +} + +const CStack* CBattleInfoEssentials::battleGetStackByID(int ID, bool onlyAlive) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + + auto stacks = battleGetStacksIf([=](const CStack * s) + { + return s->unitId() == ID && (!onlyAlive || s->alive()); + }); + + if(stacks.empty()) + return nullptr; + else + return stacks[0]; +} + +bool CBattleInfoEssentials::battleDoWeKnowAbout(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + auto p = battleGetMySide(); + return p == BattlePerspective::ALL_KNOWING || p == side; +} + +si8 CBattleInfoEssentials::battleTacticDist() const +{ + RETURN_IF_NOT_BATTLE(0); + return getBattle()->getTacticDist(); +} + +si8 CBattleInfoEssentials::battleGetTacticsSide() const +{ + RETURN_IF_NOT_BATTLE(-1); + return getBattle()->getTacticsSide(); +} + +const CGHeroInstance * CBattleInfoEssentials::battleGetFightingHero(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + if(side > 1) + { + logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); + return nullptr; + } + + if(!battleDoWeKnowAbout(side)) + { + logGlobal->error("FIXME: %s access check ", __FUNCTION__); + return nullptr; + } + + return getBattle()->getSideHero(side); +} + +const CArmedInstance * CBattleInfoEssentials::battleGetArmyObject(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + if(side > 1) + { + logGlobal->error("FIXME: %s wrong argument!", __FUNCTION__); + return nullptr; + } + if(!battleDoWeKnowAbout(side)) + { + logGlobal->error("FIXME: %s access check!", __FUNCTION__); + return nullptr; + } + return getBattle()->getSideArmy(side); +} + +InfoAboutHero CBattleInfoEssentials::battleGetHeroInfo(ui8 side) const +{ + const auto * hero = getBattle()->getSideHero(side); + if(!hero) + { + return InfoAboutHero(); + } + InfoAboutHero::EInfoLevel infoLevel = battleDoWeKnowAbout(side) ? InfoAboutHero::EInfoLevel::DETAILED : InfoAboutHero::EInfoLevel::BASIC; + return InfoAboutHero(hero, infoLevel); +} + +uint32_t CBattleInfoEssentials::battleCastSpells(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(-1); + return getBattle()->getCastSpells(side); +} + +const IBonusBearer * CBattleInfoEssentials::getBonusBearer() const +{ + return getBattle()->getBonusBearer(); +} + +bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(!side) + return false; + + const CGHeroInstance * myHero = battleGetFightingHero(side.value()); + + //current player have no hero + if(!myHero) + return false; + + //eg. one of heroes is wearing shakles of war + if(myHero->hasBonusOfType(BonusType::BATTLE_NO_FLEEING)) + return false; + + //we are besieged defender + if(side == BattleSide::DEFENDER && battleGetSiegeLevel()) + { + const auto * town = battleGetDefendedTown(); + if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) + return false; + } + + return true; +} + +BattleSideOpt CBattleInfoEssentials::playerToSide(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(std::nullopt); + + if(getBattle()->getSidePlayer(BattleSide::ATTACKER) == player) + return BattleSideOpt(BattleSide::ATTACKER); + + if(getBattle()->getSidePlayer(BattleSide::DEFENDER) == player) + return BattleSideOpt(BattleSide::DEFENDER); + + logGlobal->warn("Cannot find side for player %s", player.toString()); + + return std::nullopt; +} + +PlayerColor CBattleInfoEssentials::sideToPlayer(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + return getBattle()->getSidePlayer(side); +} + +ui8 CBattleInfoEssentials::otherSide(ui8 side) const +{ + if(side == BattleSide::ATTACKER) + return BattleSide::DEFENDER; + else + return BattleSide::ATTACKER; +} + +PlayerColor CBattleInfoEssentials::otherPlayer(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + + auto side = playerToSide(player); + if(!side) + return PlayerColor::CANNOT_DETERMINE; + + return getBattle()->getSidePlayer(otherSide(side.value())); +} + +bool CBattleInfoEssentials::playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(side) + { + auto opponentSide = otherSide(side.value()); + if(getBattle()->getSideHero(opponentSide) == h) + return true; + } + return false; +} + +ui8 CBattleInfoEssentials::battleGetSiegeLevel() const +{ + RETURN_IF_NOT_BATTLE(CGTownInstance::NONE); + return getBattle()->getDefendedTown() ? getBattle()->getDefendedTown()->fortLevel() : CGTownInstance::NONE; +} + +bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const +{ + RETURN_IF_NOT_BATTLE(false); + const auto side = playerToSide(player); + if(!side) + return false; + bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && battleGetSiegeLevel()); + //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero + return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); +} + +bool CBattleInfoEssentials::battleHasHero(ui8 side) const +{ + RETURN_IF_NOT_BATTLE(false); + return getBattle()->getSideHero(side) != nullptr; +} + +EWallState CBattleInfoEssentials::battleGetWallState(EWallPart partOfWall) const +{ + RETURN_IF_NOT_BATTLE(EWallState::NONE); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return EWallState::NONE; + + return getBattle()->getWallState(partOfWall); +} + +EGateState CBattleInfoEssentials::battleGetGateState() const +{ + RETURN_IF_NOT_BATTLE(EGateState::NONE); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return EGateState::NONE; + + return getBattle()->getGateState(); +} + +bool CBattleInfoEssentials::battleIsGatePassable() const +{ + RETURN_IF_NOT_BATTLE(true); + if(battleGetSiegeLevel() == CGTownInstance::NONE) + return true; + + return battleGetGateState() == EGateState::OPENED || battleGetGateState() == EGateState::DESTROYED; +} + +PlayerColor CBattleInfoEssentials::battleGetOwner(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(PlayerColor::CANNOT_DETERMINE); + + PlayerColor initialOwner = getBattle()->getSidePlayer(unit->unitSide()); + + static CSelector selector = Selector::type()(BonusType::HYPNOTIZED); + static std::string cachingString = "type_103s-1"; + + if(unit->hasBonus(selector, cachingString)) + return otherPlayer(initialOwner); + else + return initialOwner; +} + +const CGHeroInstance * CBattleInfoEssentials::battleGetOwnerHero(const battle::Unit * unit) const +{ + RETURN_IF_NOT_BATTLE(nullptr); + const auto side = playerToSide(battleGetOwner(unit)); + if(!side) + return nullptr; + return getBattle()->getSideHero(side.value()); +} + +bool CBattleInfoEssentials::battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const +{ + RETURN_IF_NOT_BATTLE(false); + if(boost::logic::indeterminate(positivness)) + return true; + else if(attacker->unitId() == defender->unitId()) + return (bool)positivness; + else + return battleMatchOwner(battleGetOwner(attacker), defender, positivness); +} + +bool CBattleInfoEssentials::battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness) const +{ + RETURN_IF_NOT_BATTLE(false); + + PlayerColor initialOwner = getBattle()->getSidePlayer(defender->unitSide()); + + return boost::logic::indeterminate(positivness) || (attacker == initialOwner) == (bool)positivness; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index d1921e965..0dc3cab1b 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -1,120 +1,120 @@ -/* - * CBattleInfoEssentials.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 "IBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGTownInstance; -class CGHeroInstance; -class CStack; -class IBonusBearer; -struct InfoAboutHero; -class CArmedInstance; - -using TStacks = std::vector; -using TStackFilter = std::function; - -namespace BattlePerspective -{ - enum BattlePerspective - { - INVALID = -2, - ALL_KNOWING = -1, - LEFT_SIDE, - RIGHT_SIDE - }; -} - -class DLL_LINKAGE CBattleInfoEssentials : public IBattleInfoCallback -{ -protected: - bool battleDoWeKnowAbout(ui8 side) const; - -public: - enum EStackOwnership - { - ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY - }; - - bool duringBattle() const; - BattlePerspective::BattlePerspective battleGetMySide() const; - const IBonusBearer * getBonusBearer() const override; - - TerrainId battleTerrainType() const override; - BattleField battleGetBattlefieldType() const override; - int32_t battleGetEnchanterCounter(ui8 side) const; - - std::vector > battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield - - std::shared_ptr battleGetObstacleByID(uint32_t ID) const; - - /** @brief Main method for getting battle stacks - * returns also turrets and removed stacks - * @param predicate Functor that shall return true for desired stack - * @return filtered stacks - * - */ - TStacks battleGetStacksIf(TStackFilter predicate) const; //deprecated - battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const override; - - const battle::Unit * battleGetUnitByID(uint32_t ID) const override; - const battle::Unit * battleActiveUnit() const override; - - uint32_t battleNextUnitId() const override; - - bool battleHasNativeStack(ui8 side) const; - const CGTownInstance * battleGetDefendedTown() const; //returns defended town if current battle is a siege, nullptr instead - - si8 battleTacticDist() const override; //returns tactic distance in current tactics phase; 0 if not in tactics phase - si8 battleGetTacticsSide() const override; //returns which side is in tactics phase, undefined if none (?) - - bool battleCanFlee(const PlayerColor & player) const; - bool battleCanSurrender(const PlayerColor & player) const; - - ui8 otherSide(ui8 side) const; - PlayerColor otherPlayer(const PlayerColor & player) const; - - BattleSideOpt playerToSide(const PlayerColor & player) const; - PlayerColor sideToPlayer(ui8 side) const; - bool playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const; - ui8 battleGetSiegeLevel() const; //returns 0 when there is no siege, 1 if fort, 2 is citadel, 3 is castle - bool battleHasHero(ui8 side) const; - uint32_t battleCastSpells(ui8 side) const; //how many spells has given side cast - const CGHeroInstance * battleGetFightingHero(ui8 side) const; //deprecated for players callback, easy to get wrong - const CArmedInstance * battleGetArmyObject(ui8 side) const; - InfoAboutHero battleGetHeroInfo(ui8 side) const; - - // for determining state of a part of the wall; format: parameter [0] - keep, [1] - bottom tower, [2] - bottom wall, - // [3] - below gate, [4] - over gate, [5] - upper wall, [6] - uppert tower, [7] - gate; returned value: 1 - intact, 2 - damaged, 3 - destroyed; 0 - no battle - EWallState battleGetWallState(EWallPart partOfWall) const; - EGateState battleGetGateState() const; - bool battleIsGatePassable() const; - - //helpers - ///returns all stacks, alive or dead or undead or mechanical :) - TStacks battleGetAllStacks(bool includeTurrets = false) const; - - const CStack * battleGetStackByID(int ID, bool onlyAlive = true) const; //returns stack info by given ID - bool battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const; - - ///returns player that controls given stack; mind control included - PlayerColor battleGetOwner(const battle::Unit * unit) const; - - ///returns hero that controls given stack; nullptr if none; mind control included - const CGHeroInstance * battleGetOwnerHero(const battle::Unit * unit) const; - - ///check that stacks are controlled by same|other player(s) depending on positiveness - ///mind control included - bool battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; - bool battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CBattleInfoEssentials.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 "IBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGTownInstance; +class CGHeroInstance; +class CStack; +class IBonusBearer; +struct InfoAboutHero; +class CArmedInstance; + +using TStacks = std::vector; +using TStackFilter = std::function; + +namespace BattlePerspective +{ + enum BattlePerspective + { + INVALID = -2, + ALL_KNOWING = -1, + LEFT_SIDE, + RIGHT_SIDE + }; +} + +class DLL_LINKAGE CBattleInfoEssentials : public IBattleInfoCallback +{ +protected: + bool battleDoWeKnowAbout(ui8 side) const; + +public: + enum EStackOwnership + { + ONLY_MINE, ONLY_ENEMY, MINE_AND_ENEMY + }; + + bool duringBattle() const; + BattlePerspective::BattlePerspective battleGetMySide() const; + const IBonusBearer * getBonusBearer() const override; + + TerrainId battleTerrainType() const override; + BattleField battleGetBattlefieldType() const override; + int32_t battleGetEnchanterCounter(ui8 side) const; + + std::vector > battleGetAllObstacles(std::optional perspective = std::nullopt) const; //returns all obstacles on the battlefield + + std::shared_ptr battleGetObstacleByID(uint32_t ID) const; + + /** @brief Main method for getting battle stacks + * returns also turrets and removed stacks + * @param predicate Functor that shall return true for desired stack + * @return filtered stacks + * + */ + TStacks battleGetStacksIf(TStackFilter predicate) const; //deprecated + battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const override; + + const battle::Unit * battleGetUnitByID(uint32_t ID) const override; + const battle::Unit * battleActiveUnit() const override; + + uint32_t battleNextUnitId() const override; + + bool battleHasNativeStack(ui8 side) const; + const CGTownInstance * battleGetDefendedTown() const; //returns defended town if current battle is a siege, nullptr instead + + si8 battleTacticDist() const override; //returns tactic distance in current tactics phase; 0 if not in tactics phase + si8 battleGetTacticsSide() const override; //returns which side is in tactics phase, undefined if none (?) + + bool battleCanFlee(const PlayerColor & player) const; + bool battleCanSurrender(const PlayerColor & player) const; + + ui8 otherSide(ui8 side) const; + PlayerColor otherPlayer(const PlayerColor & player) const; + + BattleSideOpt playerToSide(const PlayerColor & player) const; + PlayerColor sideToPlayer(ui8 side) const; + bool playerHasAccessToHeroInfo(const PlayerColor & player, const CGHeroInstance * h) const; + ui8 battleGetSiegeLevel() const; //returns 0 when there is no siege, 1 if fort, 2 is citadel, 3 is castle + bool battleHasHero(ui8 side) const; + uint32_t battleCastSpells(ui8 side) const; //how many spells has given side cast + const CGHeroInstance * battleGetFightingHero(ui8 side) const; //deprecated for players callback, easy to get wrong + const CArmedInstance * battleGetArmyObject(ui8 side) const; + InfoAboutHero battleGetHeroInfo(ui8 side) const; + + // for determining state of a part of the wall; format: parameter [0] - keep, [1] - bottom tower, [2] - bottom wall, + // [3] - below gate, [4] - over gate, [5] - upper wall, [6] - uppert tower, [7] - gate; returned value: 1 - intact, 2 - damaged, 3 - destroyed; 0 - no battle + EWallState battleGetWallState(EWallPart partOfWall) const; + EGateState battleGetGateState() const; + bool battleIsGatePassable() const; + + //helpers + ///returns all stacks, alive or dead or undead or mechanical :) + TStacks battleGetAllStacks(bool includeTurrets = false) const; + + const CStack * battleGetStackByID(int ID, bool onlyAlive = true) const; //returns stack info by given ID + bool battleIsObstacleVisibleForSide(const CObstacleInstance & coi, BattlePerspective::BattlePerspective side) const; + + ///returns player that controls given stack; mind control included + PlayerColor battleGetOwner(const battle::Unit * unit) const; + + ///returns hero that controls given stack; nullptr if none; mind control included + const CGHeroInstance * battleGetOwnerHero(const battle::Unit * unit) const; + + ///check that stacks are controlled by same|other player(s) depending on positiveness + ///mind control included + bool battleMatchOwner(const battle::Unit * attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; + bool battleMatchOwner(const PlayerColor & attacker, const battle::Unit * defender, const boost::logic::tribool positivness = false) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index 729409727..426859fb3 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -1,269 +1,269 @@ -/* - * CObstacleInstance.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 "CObstacleInstance.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../ObstacleHandler.h" -#include "../VCMI_Lib.h" -#include "../NetPacksBase.h" - -#include "../serializer/JsonDeserializer.h" -#include "../serializer/JsonSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const ObstacleInfo & CObstacleInstance::getInfo() const -{ - assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE); - - return *Obstacle(ID).getInfo(); -} - -std::vector CObstacleInstance::getBlockedTiles() const -{ - if(blocksTiles()) - return getAffectedTiles(); - return std::vector(); -} - -std::vector CObstacleInstance::getStoppingTile() const -{ - if(stopsMovement()) - return getAffectedTiles(); - return std::vector(); -} - -std::vector CObstacleInstance::getAffectedTiles() const -{ - switch(obstacleType) - { - case ABSOLUTE_OBSTACLE: - case USUAL: - return getInfo().getBlocked(pos); - default: - assert(0); - return std::vector(); - } -} - -bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const -{ - //by default obstacle is visible for everyone - return true; -} - -const AnimationPath & CObstacleInstance::getAnimation() const -{ - return getInfo().animation; -} - -const AnimationPath & CObstacleInstance::getAppearAnimation() const -{ - return getInfo().appearAnimation; -} - -const AudioPath & CObstacleInstance::getAppearSound() const -{ - return getInfo().appearSound; -} - -int CObstacleInstance::getAnimationYOffset(int imageHeight) const -{ - int offset = imageHeight % 42; - if(obstacleType == CObstacleInstance::USUAL) - { - if(getInfo().blockedTiles.front() < 0 || offset > 37) //second or part is for holy ground ID=62,65,63 - offset -= 42; - } - return offset; -} - -bool CObstacleInstance::stopsMovement() const -{ - return obstacleType == MOAT; -} - -bool CObstacleInstance::blocksTiles() const -{ - return obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE ; -} - -bool CObstacleInstance::triggersEffects() const -{ - return getTrigger() != SpellID::NONE; -} - -SpellID CObstacleInstance::getTrigger() const -{ - return SpellID::NONE; -} - -void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) -{ - auto obstacleInfo = getInfo(); - auto hidden = false; - auto needAnimationOffsetFix = obstacleType == CObstacleInstance::USUAL; - int animationYOffset = 0; - - if(getInfo().blockedTiles.front() < 0) //TODO: holy ground ID=62,65,63 - animationYOffset -= 42; - - //We need only a subset of obstacle info for correct render - handler.serializeInt("position", pos); - handler.serializeStruct("appearSound", obstacleInfo.appearSound); - handler.serializeStruct("appearAnimation", obstacleInfo.appearAnimation); - handler.serializeStruct("animation", obstacleInfo.animation); - handler.serializeInt("animationYOffset", animationYOffset); - - handler.serializeBool("hidden", hidden); - handler.serializeBool("needAnimationOffsetFix", needAnimationOffsetFix); -} - -void CObstacleInstance::toInfo(ObstacleChanges & info, BattleChanges::EOperation operation) -{ - info.id = uniqueID; - info.operation = operation; - - info.data.clear(); - JsonSerializer ser(nullptr, info.data); - ser.serializeStruct("obstacle", *this); -} - -SpellCreatedObstacle::SpellCreatedObstacle() - : turnsRemaining(-1), - casterSpellPower(0), - spellLevel(0), - casterSide(0), - hidden(false), - passable(false), - trigger(false), - trap(false), - removeOnTrigger(false), - revealed(false), - animationYOffset(0), - nativeVisible(true), - minimalDamage(0) -{ - obstacleType = SPELL_CREATED; -} - -bool SpellCreatedObstacle::visibleForSide(ui8 side, bool hasNativeStack) const -{ - //we hide mines and not discovered quicksands - //quicksands are visible to the caster or if owned unit stepped into that particular patch - //additionally if side has a native unit, mines/quicksands will be visible - //but it is not a case for a moat, so, hasNativeStack should not work for moats - - auto nativeVis = hasNativeStack && nativeVisible; - - return casterSide == side || !hidden || revealed || nativeVis; -} - -bool SpellCreatedObstacle::blocksTiles() const -{ - return !passable; -} - -bool SpellCreatedObstacle::stopsMovement() const -{ - return trap; -} - -SpellID SpellCreatedObstacle::getTrigger() const -{ - return trigger; -} - -void SpellCreatedObstacle::fromInfo(const ObstacleChanges & info) -{ - uniqueID = info.id; - - if(info.operation != ObstacleChanges::EOperation::ADD && info.operation != ObstacleChanges::EOperation::UPDATE) - logGlobal->error("ADD or UPDATE operation expected"); - - JsonDeserializer deser(nullptr, info.data); - deser.serializeStruct("obstacle", *this); -} - -void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeInt("spell", ID); - handler.serializeInt("position", pos); - - handler.serializeInt("turnsRemaining", turnsRemaining); - handler.serializeInt("casterSpellPower", casterSpellPower); - handler.serializeInt("spellLevel", spellLevel); - handler.serializeInt("casterSide", casterSide); - handler.serializeInt("minimalDamage", minimalDamage); - handler.serializeInt("type", obstacleType); - - handler.serializeBool("hidden", hidden); - handler.serializeBool("revealed", revealed); - handler.serializeBool("passable", passable); - handler.serializeId("trigger", trigger, SpellID::NONE); - handler.serializeBool("trap", trap); - handler.serializeBool("removeOnTrigger", removeOnTrigger); - handler.serializeBool("nativeVisible", nativeVisible); - - handler.serializeStruct("appearSound", appearSound); - handler.serializeStruct("appearAnimation", appearAnimation); - handler.serializeStruct("animation", animation); - - handler.serializeInt("animationYOffset", animationYOffset); - - { - JsonArraySerializer customSizeJson = handler.enterArray("customSize"); - customSizeJson.syncSize(customSize, JsonNode::JsonType::DATA_INTEGER); - - for(size_t index = 0; index < customSizeJson.size(); index++) - customSizeJson.serializeInt(index, customSize.at(index)); - } -} - -std::vector SpellCreatedObstacle::getAffectedTiles() const -{ - return customSize; -} - -void SpellCreatedObstacle::battleTurnPassed() -{ - if(turnsRemaining > 0) - turnsRemaining--; -} - -const AnimationPath & SpellCreatedObstacle::getAnimation() const -{ - return animation; -} - -const AnimationPath & SpellCreatedObstacle::getAppearAnimation() const -{ - return appearAnimation; -} - -const AudioPath & SpellCreatedObstacle::getAppearSound() const -{ - return appearSound; -} - -int SpellCreatedObstacle::getAnimationYOffset(int imageHeight) const -{ - int offset = imageHeight % 42; - - if(obstacleType == CObstacleInstance::SPELL_CREATED || obstacleType == CObstacleInstance::MOAT) - { - offset += animationYOffset; - } - - return offset; -} - -VCMI_LIB_NAMESPACE_END +/* + * CObstacleInstance.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 "CObstacleInstance.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../ObstacleHandler.h" +#include "../VCMI_Lib.h" +#include "../NetPacksBase.h" + +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const ObstacleInfo & CObstacleInstance::getInfo() const +{ + assert( obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE); + + return *Obstacle(ID).getInfo(); +} + +std::vector CObstacleInstance::getBlockedTiles() const +{ + if(blocksTiles()) + return getAffectedTiles(); + return std::vector(); +} + +std::vector CObstacleInstance::getStoppingTile() const +{ + if(stopsMovement()) + return getAffectedTiles(); + return std::vector(); +} + +std::vector CObstacleInstance::getAffectedTiles() const +{ + switch(obstacleType) + { + case ABSOLUTE_OBSTACLE: + case USUAL: + return getInfo().getBlocked(pos); + default: + assert(0); + return std::vector(); + } +} + +bool CObstacleInstance::visibleForSide(ui8 side, bool hasNativeStack) const +{ + //by default obstacle is visible for everyone + return true; +} + +const AnimationPath & CObstacleInstance::getAnimation() const +{ + return getInfo().animation; +} + +const AnimationPath & CObstacleInstance::getAppearAnimation() const +{ + return getInfo().appearAnimation; +} + +const AudioPath & CObstacleInstance::getAppearSound() const +{ + return getInfo().appearSound; +} + +int CObstacleInstance::getAnimationYOffset(int imageHeight) const +{ + int offset = imageHeight % 42; + if(obstacleType == CObstacleInstance::USUAL) + { + if(getInfo().blockedTiles.front() < 0 || offset > 37) //second or part is for holy ground ID=62,65,63 + offset -= 42; + } + return offset; +} + +bool CObstacleInstance::stopsMovement() const +{ + return obstacleType == MOAT; +} + +bool CObstacleInstance::blocksTiles() const +{ + return obstacleType == USUAL || obstacleType == ABSOLUTE_OBSTACLE ; +} + +bool CObstacleInstance::triggersEffects() const +{ + return getTrigger() != SpellID::NONE; +} + +SpellID CObstacleInstance::getTrigger() const +{ + return SpellID::NONE; +} + +void CObstacleInstance::serializeJson(JsonSerializeFormat & handler) +{ + auto obstacleInfo = getInfo(); + auto hidden = false; + auto needAnimationOffsetFix = obstacleType == CObstacleInstance::USUAL; + int animationYOffset = 0; + + if(getInfo().blockedTiles.front() < 0) //TODO: holy ground ID=62,65,63 + animationYOffset -= 42; + + //We need only a subset of obstacle info for correct render + handler.serializeInt("position", pos); + handler.serializeStruct("appearSound", obstacleInfo.appearSound); + handler.serializeStruct("appearAnimation", obstacleInfo.appearAnimation); + handler.serializeStruct("animation", obstacleInfo.animation); + handler.serializeInt("animationYOffset", animationYOffset); + + handler.serializeBool("hidden", hidden); + handler.serializeBool("needAnimationOffsetFix", needAnimationOffsetFix); +} + +void CObstacleInstance::toInfo(ObstacleChanges & info, BattleChanges::EOperation operation) +{ + info.id = uniqueID; + info.operation = operation; + + info.data.clear(); + JsonSerializer ser(nullptr, info.data); + ser.serializeStruct("obstacle", *this); +} + +SpellCreatedObstacle::SpellCreatedObstacle() + : turnsRemaining(-1), + casterSpellPower(0), + spellLevel(0), + casterSide(0), + hidden(false), + passable(false), + trigger(false), + trap(false), + removeOnTrigger(false), + revealed(false), + animationYOffset(0), + nativeVisible(true), + minimalDamage(0) +{ + obstacleType = SPELL_CREATED; +} + +bool SpellCreatedObstacle::visibleForSide(ui8 side, bool hasNativeStack) const +{ + //we hide mines and not discovered quicksands + //quicksands are visible to the caster or if owned unit stepped into that particular patch + //additionally if side has a native unit, mines/quicksands will be visible + //but it is not a case for a moat, so, hasNativeStack should not work for moats + + auto nativeVis = hasNativeStack && nativeVisible; + + return casterSide == side || !hidden || revealed || nativeVis; +} + +bool SpellCreatedObstacle::blocksTiles() const +{ + return !passable; +} + +bool SpellCreatedObstacle::stopsMovement() const +{ + return trap; +} + +SpellID SpellCreatedObstacle::getTrigger() const +{ + return trigger; +} + +void SpellCreatedObstacle::fromInfo(const ObstacleChanges & info) +{ + uniqueID = info.id; + + if(info.operation != ObstacleChanges::EOperation::ADD && info.operation != ObstacleChanges::EOperation::UPDATE) + logGlobal->error("ADD or UPDATE operation expected"); + + JsonDeserializer deser(nullptr, info.data); + deser.serializeStruct("obstacle", *this); +} + +void SpellCreatedObstacle::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeInt("spell", ID); + handler.serializeInt("position", pos); + + handler.serializeInt("turnsRemaining", turnsRemaining); + handler.serializeInt("casterSpellPower", casterSpellPower); + handler.serializeInt("spellLevel", spellLevel); + handler.serializeInt("casterSide", casterSide); + handler.serializeInt("minimalDamage", minimalDamage); + handler.serializeInt("type", obstacleType); + + handler.serializeBool("hidden", hidden); + handler.serializeBool("revealed", revealed); + handler.serializeBool("passable", passable); + handler.serializeId("trigger", trigger, SpellID::NONE); + handler.serializeBool("trap", trap); + handler.serializeBool("removeOnTrigger", removeOnTrigger); + handler.serializeBool("nativeVisible", nativeVisible); + + handler.serializeStruct("appearSound", appearSound); + handler.serializeStruct("appearAnimation", appearAnimation); + handler.serializeStruct("animation", animation); + + handler.serializeInt("animationYOffset", animationYOffset); + + { + JsonArraySerializer customSizeJson = handler.enterArray("customSize"); + customSizeJson.syncSize(customSize, JsonNode::JsonType::DATA_INTEGER); + + for(size_t index = 0; index < customSizeJson.size(); index++) + customSizeJson.serializeInt(index, customSize.at(index)); + } +} + +std::vector SpellCreatedObstacle::getAffectedTiles() const +{ + return customSize; +} + +void SpellCreatedObstacle::battleTurnPassed() +{ + if(turnsRemaining > 0) + turnsRemaining--; +} + +const AnimationPath & SpellCreatedObstacle::getAnimation() const +{ + return animation; +} + +const AnimationPath & SpellCreatedObstacle::getAppearAnimation() const +{ + return appearAnimation; +} + +const AudioPath & SpellCreatedObstacle::getAppearSound() const +{ + return appearSound; +} + +int SpellCreatedObstacle::getAnimationYOffset(int imageHeight) const +{ + int offset = imageHeight % 42; + + if(obstacleType == CObstacleInstance::SPELL_CREATED || obstacleType == CObstacleInstance::MOAT) + { + offset += animationYOffset; + } + + return offset; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index b3759d8c4..99178c20c 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -1,139 +1,139 @@ -/* - * CObstacleInstance.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 "BattleHex.h" -#include "NetPacksBase.h" -#include "../filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class ObstacleInfo; -class ObstacleChanges; -class JsonSerializeFormat; -class SpellID; - -struct DLL_LINKAGE CObstacleInstance -{ - enum EObstacleType : ui8 - { - //ABSOLUTE needs an underscore because it's a Win - USUAL, ABSOLUTE_OBSTACLE, SPELL_CREATED, MOAT - }; - - BattleHex pos; //position on battlefield, typically left bottom corner - EObstacleType obstacleType = USUAL; - si32 uniqueID = -1; - si32 ID = -1; //ID of obstacle (defines type of it) - - virtual ~CObstacleInstance() = default; - - const ObstacleInfo &getInfo() const; //allowed only when not generated by spell (usual or absolute) - - std::vector getBlockedTiles() const; - std::vector getStoppingTile() const; //hexes that will stop stack move - - //The two functions below describe how the obstacle affects affected tiles - //additional effects (like hurting stack or disappearing) are hardcoded for appropriate obstacleTypes - virtual bool blocksTiles() const; - virtual bool stopsMovement() const; //if unit stepping onto obstacle, can't continue movement (in general, doesn't checks for the side) - virtual bool triggersEffects() const; - virtual SpellID getTrigger() const; - - virtual std::vector getAffectedTiles() const; - virtual bool visibleForSide(ui8 side, bool hasNativeStack) const; //0 attacker - - virtual void battleTurnPassed(){}; - - //Client helper functions, make it easier to render animations - virtual const AnimationPath & getAnimation() const; - virtual const AnimationPath & getAppearAnimation() const; - virtual const AudioPath & getAppearSound() const; - - virtual int getAnimationYOffset(int imageHeight) const; - - void toInfo(ObstacleChanges & info, BattleChanges::EOperation operation = BattleChanges::EOperation::ADD); - - virtual void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & pos; - h & obstacleType; - h & uniqueID; - } -}; - -struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance -{ - int32_t turnsRemaining; - int32_t casterSpellPower; - int32_t spellLevel; - int32_t minimalDamage; //How many damage should it do regardless of power and level of caster - si8 casterSide; //0 - obstacle created by attacker; 1 - by defender - - SpellID trigger; - - bool hidden; - bool passable; - bool trap; - bool removeOnTrigger; - bool revealed; - bool nativeVisible; //Should native terrain creatures reveal obstacle - - AudioPath appearSound; - AnimationPath appearAnimation; - AnimationPath animation; - - int animationYOffset; - - std::vector customSize; - - SpellCreatedObstacle(); - - std::vector getAffectedTiles() const override; - bool visibleForSide(ui8 side, bool hasNativeStack) const override; - - bool blocksTiles() const override; - bool stopsMovement() const override; - SpellID getTrigger() const override; - - void battleTurnPassed() override; - - //Client helper functions, make it easier to render animations - const AnimationPath & getAnimation() const override; - const AnimationPath & getAppearAnimation() const override; - const AudioPath & getAppearSound() const override; - int getAnimationYOffset(int imageHeight) const override; - - void fromInfo(const ObstacleChanges & info); - - void serializeJson(JsonSerializeFormat & handler) override; - - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & turnsRemaining; - h & casterSpellPower; - h & spellLevel; - h & casterSide; - - h & hidden; - h & nativeVisible; - h & passable; - h & trigger; - h & minimalDamage; - h & trap; - - h & customSize; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CObstacleInstance.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 "BattleHex.h" +#include "NetPacksBase.h" +#include "../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ObstacleInfo; +class ObstacleChanges; +class JsonSerializeFormat; +class SpellID; + +struct DLL_LINKAGE CObstacleInstance +{ + enum EObstacleType : ui8 + { + //ABSOLUTE needs an underscore because it's a Win + USUAL, ABSOLUTE_OBSTACLE, SPELL_CREATED, MOAT + }; + + BattleHex pos; //position on battlefield, typically left bottom corner + EObstacleType obstacleType = USUAL; + si32 uniqueID = -1; + si32 ID = -1; //ID of obstacle (defines type of it) + + virtual ~CObstacleInstance() = default; + + const ObstacleInfo &getInfo() const; //allowed only when not generated by spell (usual or absolute) + + std::vector getBlockedTiles() const; + std::vector getStoppingTile() const; //hexes that will stop stack move + + //The two functions below describe how the obstacle affects affected tiles + //additional effects (like hurting stack or disappearing) are hardcoded for appropriate obstacleTypes + virtual bool blocksTiles() const; + virtual bool stopsMovement() const; //if unit stepping onto obstacle, can't continue movement (in general, doesn't checks for the side) + virtual bool triggersEffects() const; + virtual SpellID getTrigger() const; + + virtual std::vector getAffectedTiles() const; + virtual bool visibleForSide(ui8 side, bool hasNativeStack) const; //0 attacker + + virtual void battleTurnPassed(){}; + + //Client helper functions, make it easier to render animations + virtual const AnimationPath & getAnimation() const; + virtual const AnimationPath & getAppearAnimation() const; + virtual const AudioPath & getAppearSound() const; + + virtual int getAnimationYOffset(int imageHeight) const; + + void toInfo(ObstacleChanges & info, BattleChanges::EOperation operation = BattleChanges::EOperation::ADD); + + virtual void serializeJson(JsonSerializeFormat & handler); + + template void serialize(Handler &h, const int version) + { + h & ID; + h & pos; + h & obstacleType; + h & uniqueID; + } +}; + +struct DLL_LINKAGE SpellCreatedObstacle : CObstacleInstance +{ + int32_t turnsRemaining; + int32_t casterSpellPower; + int32_t spellLevel; + int32_t minimalDamage; //How many damage should it do regardless of power and level of caster + si8 casterSide; //0 - obstacle created by attacker; 1 - by defender + + SpellID trigger; + + bool hidden; + bool passable; + bool trap; + bool removeOnTrigger; + bool revealed; + bool nativeVisible; //Should native terrain creatures reveal obstacle + + AudioPath appearSound; + AnimationPath appearAnimation; + AnimationPath animation; + + int animationYOffset; + + std::vector customSize; + + SpellCreatedObstacle(); + + std::vector getAffectedTiles() const override; + bool visibleForSide(ui8 side, bool hasNativeStack) const override; + + bool blocksTiles() const override; + bool stopsMovement() const override; + SpellID getTrigger() const override; + + void battleTurnPassed() override; + + //Client helper functions, make it easier to render animations + const AnimationPath & getAnimation() const override; + const AnimationPath & getAppearAnimation() const override; + const AudioPath & getAppearSound() const override; + int getAnimationYOffset(int imageHeight) const override; + + void fromInfo(const ObstacleChanges & info); + + void serializeJson(JsonSerializeFormat & handler) override; + + template void serialize(Handler &h, const int version) + { + h & static_cast(*this); + h & turnsRemaining; + h & casterSpellPower; + h & spellLevel; + h & casterSide; + + h & hidden; + h & nativeVisible; + h & passable; + h & trigger; + h & minimalDamage; + h & trap; + + h & customSize; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CPlayerBattleCallback.cpp b/lib/battle/CPlayerBattleCallback.cpp index 89a2e2861..c1256e531 100644 --- a/lib/battle/CPlayerBattleCallback.cpp +++ b/lib/battle/CPlayerBattleCallback.cpp @@ -1,83 +1,83 @@ -/* - * CPlayerBattleCallback.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 "CPlayerBattleCallback.h" -#include "../CStack.h" -#include "../gameState/InfoAboutArmy.h" -#include "../CGameInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player): - battle(battle), - player(player) -{ - -} - -#if SCRIPTING_ENABLED -scripting::Pool * CPlayerBattleCallback::getContextPool() const -{ - return nullptr; //TODO cl->getGlobalContextPool(); -} -#endif - -const IBattleInfo * CPlayerBattleCallback::getBattle() const -{ - return battle; -} - -std::optional CPlayerBattleCallback::getPlayerID() const -{ - return player; -} - -bool CPlayerBattleCallback::battleCanFlee() const -{ - RETURN_IF_NOT_BATTLE(false); - ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoEssentials::battleCanFlee(*getPlayerID()); -} - -TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const -{ - if(whose != MINE_AND_ENEMY) - { - ASSERT_IF_CALLED_WITH_PLAYER - } - - return battleGetStacksIf([=](const CStack * s){ - const bool ownerMatches = (whose == MINE_AND_ENEMY) - || (whose == ONLY_MINE && s->unitOwner() == getPlayerID()) - || (whose == ONLY_ENEMY && s->unitOwner() != getPlayerID()); - - return ownerMatches && s->isValidTarget(!onlyAlive); - }); -} - -int CPlayerBattleCallback::battleGetSurrenderCost() const -{ - RETURN_IF_NOT_BATTLE(-3) - ASSERT_IF_CALLED_WITH_PLAYER - return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID()); -} - -const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const -{ - return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide()); -} - -InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const -{ - return battleGetHeroInfo(!battleGetMySide()); -} - - -VCMI_LIB_NAMESPACE_END +/* + * CPlayerBattleCallback.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 "CPlayerBattleCallback.h" +#include "../CStack.h" +#include "../gameState/InfoAboutArmy.h" +#include "../CGameInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CPlayerBattleCallback::CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player): + battle(battle), + player(player) +{ + +} + +#if SCRIPTING_ENABLED +scripting::Pool * CPlayerBattleCallback::getContextPool() const +{ + return nullptr; //TODO cl->getGlobalContextPool(); +} +#endif + +const IBattleInfo * CPlayerBattleCallback::getBattle() const +{ + return battle; +} + +std::optional CPlayerBattleCallback::getPlayerID() const +{ + return player; +} + +bool CPlayerBattleCallback::battleCanFlee() const +{ + RETURN_IF_NOT_BATTLE(false); + ASSERT_IF_CALLED_WITH_PLAYER + return CBattleInfoEssentials::battleCanFlee(*getPlayerID()); +} + +TStacks CPlayerBattleCallback::battleGetStacks(EStackOwnership whose, bool onlyAlive) const +{ + if(whose != MINE_AND_ENEMY) + { + ASSERT_IF_CALLED_WITH_PLAYER + } + + return battleGetStacksIf([=](const CStack * s){ + const bool ownerMatches = (whose == MINE_AND_ENEMY) + || (whose == ONLY_MINE && s->unitOwner() == getPlayerID()) + || (whose == ONLY_ENEMY && s->unitOwner() != getPlayerID()); + + return ownerMatches && s->isValidTarget(!onlyAlive); + }); +} + +int CPlayerBattleCallback::battleGetSurrenderCost() const +{ + RETURN_IF_NOT_BATTLE(-3) + ASSERT_IF_CALLED_WITH_PLAYER + return CBattleInfoCallback::battleGetSurrenderCost(*getPlayerID()); +} + +const CGHeroInstance * CPlayerBattleCallback::battleGetMyHero() const +{ + return CBattleInfoEssentials::battleGetFightingHero(battleGetMySide()); +} + +InfoAboutHero CPlayerBattleCallback::battleGetEnemyHero() const +{ + return battleGetHeroInfo(!battleGetMySide()); +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/CPlayerBattleCallback.h b/lib/battle/CPlayerBattleCallback.h index 27bbb16d3..864fb7002 100644 --- a/lib/battle/CPlayerBattleCallback.h +++ b/lib/battle/CPlayerBattleCallback.h @@ -1,42 +1,42 @@ -/* - * CPlayerBattleCallback.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 "CBattleInfoCallback.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; - -class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback -{ - const IBattleInfo * battle; - PlayerColor player; - -public: - CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player); - -#if SCRIPTING_ENABLED - scripting::Pool * getContextPool() const override; -#endif - - const IBattleInfo * getBattle() const override; - std::optional getPlayerID() const override; - - bool battleCanFlee() const; //returns true if caller can flee from the battle - TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield - - int battleGetSurrenderCost() const; //returns cost of surrendering battle, -1 if surrendering is not possible - - const CGHeroInstance * battleGetMyHero() const; - InfoAboutHero battleGetEnemyHero() const; -}; - - -VCMI_LIB_NAMESPACE_END +/* + * CPlayerBattleCallback.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 "CBattleInfoCallback.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; + +class DLL_LINKAGE CPlayerBattleCallback : public CBattleInfoCallback +{ + const IBattleInfo * battle; + PlayerColor player; + +public: + CPlayerBattleCallback(const IBattleInfo * battle, PlayerColor player); + +#if SCRIPTING_ENABLED + scripting::Pool * getContextPool() const override; +#endif + + const IBattleInfo * getBattle() const override; + std::optional getPlayerID() const override; + + bool battleCanFlee() const; //returns true if caller can flee from the battle + TStacks battleGetStacks(EStackOwnership whose = MINE_AND_ENEMY, bool onlyAlive = true) const; //returns stacks on battlefield + + int battleGetSurrenderCost() const; //returns cost of surrendering battle, -1 if surrendering is not possible + + const CGHeroInstance * battleGetMyHero() const; + InfoAboutHero battleGetEnemyHero() const; +}; + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/ReachabilityInfo.cpp b/lib/battle/ReachabilityInfo.cpp index 078fe44c8..ecb6a32bf 100644 --- a/lib/battle/ReachabilityInfo.cpp +++ b/lib/battle/ReachabilityInfo.cpp @@ -1,75 +1,75 @@ -/* - * ReachabilityInfo.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 "ReachabilityInfo.h" -#include "Unit.h" - -VCMI_LIB_NAMESPACE_BEGIN - -ReachabilityInfo::Parameters::Parameters(const battle::Unit * Stack, BattleHex StartPosition): - perspective(static_cast(Stack->unitSide())), - startPosition(StartPosition), - doubleWide(Stack->doubleWide()), - side(Stack->unitSide()), - flying(Stack->hasBonusOfType(BonusType::FLYING)) -{ - knownAccessible = battle::Unit::getHexes(startPosition, doubleWide, side); -} - -ReachabilityInfo::ReachabilityInfo() -{ - distances.fill(INFINITE_DIST); - predecessors.fill(BattleHex::INVALID); -} - -bool ReachabilityInfo::isReachable(BattleHex hex) const -{ - return distances[hex] < INFINITE_DIST; -} - -uint32_t ReachabilityInfo::distToNearestNeighbour( - const std::vector & targetHexes, - BattleHex * chosenHex) const -{ - uint32_t ret = 1000000; - - for(auto targetHex : targetHexes) - { - for(auto & n : targetHex.neighbouringTiles()) - { - if(distances[n] < ret) - { - ret = distances[n]; - if(chosenHex) - *chosenHex = n; - } - } - } - - return ret; -} - -uint32_t ReachabilityInfo::distToNearestNeighbour( - const battle::Unit * attacker, - const battle::Unit * defender, - BattleHex * chosenHex) const -{ - auto attackableHexes = defender->getHexes(); - - if(attacker->doubleWide()) - { - vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide())); - } - - return distToNearestNeighbour(attackableHexes, chosenHex); -} - -VCMI_LIB_NAMESPACE_END +/* + * ReachabilityInfo.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 "ReachabilityInfo.h" +#include "Unit.h" + +VCMI_LIB_NAMESPACE_BEGIN + +ReachabilityInfo::Parameters::Parameters(const battle::Unit * Stack, BattleHex StartPosition): + perspective(static_cast(Stack->unitSide())), + startPosition(StartPosition), + doubleWide(Stack->doubleWide()), + side(Stack->unitSide()), + flying(Stack->hasBonusOfType(BonusType::FLYING)) +{ + knownAccessible = battle::Unit::getHexes(startPosition, doubleWide, side); +} + +ReachabilityInfo::ReachabilityInfo() +{ + distances.fill(INFINITE_DIST); + predecessors.fill(BattleHex::INVALID); +} + +bool ReachabilityInfo::isReachable(BattleHex hex) const +{ + return distances[hex] < INFINITE_DIST; +} + +uint32_t ReachabilityInfo::distToNearestNeighbour( + const std::vector & targetHexes, + BattleHex * chosenHex) const +{ + uint32_t ret = 1000000; + + for(auto targetHex : targetHexes) + { + for(auto & n : targetHex.neighbouringTiles()) + { + if(distances[n] < ret) + { + ret = distances[n]; + if(chosenHex) + *chosenHex = n; + } + } + } + + return ret; +} + +uint32_t ReachabilityInfo::distToNearestNeighbour( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex * chosenHex) const +{ + auto attackableHexes = defender->getHexes(); + + if(attacker->doubleWide()) + { + vstd::concatenate(attackableHexes, battle::Unit::getHexes(defender->occupiedHex(), true, attacker->unitSide())); + } + + return distToNearestNeighbour(attackableHexes, chosenHex); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/ReachabilityInfo.h b/lib/battle/ReachabilityInfo.h index 6931e1662..8f6bbb663 100644 --- a/lib/battle/ReachabilityInfo.h +++ b/lib/battle/ReachabilityInfo.h @@ -1,60 +1,60 @@ -/* - * ReachabilityInfo.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 "BattleHex.h" -#include "CBattleInfoEssentials.h" -#include "AccessibilityInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -// Reachability info is result of BFS calculation. It's dependent on stack (it's owner, whether it's flying), -// startPosition and perpective. -struct DLL_LINKAGE ReachabilityInfo -{ - using TDistances = std::array; - using TPredecessors = std::array; - - enum { INFINITE_DIST = 1000000 }; - - struct DLL_LINKAGE Parameters - { - ui8 side = 0; - bool doubleWide = false; - bool flying = false; - bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes - std::vector knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) - - BattleHex startPosition; //assumed position of stack - BattlePerspective::BattlePerspective perspective = BattlePerspective::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side - - Parameters() = default; - Parameters(const battle::Unit * Stack, BattleHex StartPosition); - }; - - Parameters params; - AccessibilityInfo accessibility; - TDistances distances; - TPredecessors predecessors; - - ReachabilityInfo(); - - bool isReachable(BattleHex hex) const; - - uint32_t distToNearestNeighbour( - const std::vector & targetHexes, - BattleHex * chosenHex = nullptr) const; - - uint32_t distToNearestNeighbour( - const battle::Unit * attacker, - const battle::Unit * defender, - BattleHex * chosenHex = nullptr) const; -}; - -VCMI_LIB_NAMESPACE_END +/* + * ReachabilityInfo.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 "BattleHex.h" +#include "CBattleInfoEssentials.h" +#include "AccessibilityInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +// Reachability info is result of BFS calculation. It's dependent on stack (it's owner, whether it's flying), +// startPosition and perpective. +struct DLL_LINKAGE ReachabilityInfo +{ + using TDistances = std::array; + using TPredecessors = std::array; + + enum { INFINITE_DIST = 1000000 }; + + struct DLL_LINKAGE Parameters + { + ui8 side = 0; + bool doubleWide = false; + bool flying = false; + bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes + std::vector knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself) + + BattleHex startPosition; //assumed position of stack + BattlePerspective::BattlePerspective perspective = BattlePerspective::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side + + Parameters() = default; + Parameters(const battle::Unit * Stack, BattleHex StartPosition); + }; + + Parameters params; + AccessibilityInfo accessibility; + TDistances distances; + TPredecessors predecessors; + + ReachabilityInfo(); + + bool isReachable(BattleHex hex) const; + + uint32_t distToNearestNeighbour( + const std::vector & targetHexes, + BattleHex * chosenHex = nullptr) const; + + uint32_t distToNearestNeighbour( + const battle::Unit * attacker, + const battle::Unit * defender, + BattleHex * chosenHex = nullptr) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index aeafb243c..25fe84127 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -1,37 +1,37 @@ -/* - * SideInBattle.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 "SideInBattle.h" -#include "../mapObjects/CArmedInstance.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army) -{ - hero = Hero; - armyObject = Army; - - switch(armyObject->ID) - { - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR2: - case Obj::CREATURE_GENERATOR3: - case Obj::CREATURE_GENERATOR4: - color = PlayerColor::NEUTRAL; - break; - default: - color = armyObject->getOwner(); - } - - if(color == PlayerColor::UNFLAGGABLE) - color = PlayerColor::NEUTRAL; -} - -VCMI_LIB_NAMESPACE_END +/* + * SideInBattle.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 "SideInBattle.h" +#include "../mapObjects/CArmedInstance.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army) +{ + hero = Hero; + armyObject = Army; + + switch(armyObject->ID) + { + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + color = PlayerColor::NEUTRAL; + break; + default: + color = armyObject->getOwner(); + } + + if(color == PlayerColor::UNFLAGGABLE) + color = PlayerColor::NEUTRAL; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SideInBattle.h b/lib/battle/SideInBattle.h index d7ca30dc2..152b79361 100644 --- a/lib/battle/SideInBattle.h +++ b/lib/battle/SideInBattle.h @@ -1,42 +1,42 @@ -/* - * SideInBattle.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 "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class CArmedInstance; - -struct DLL_LINKAGE SideInBattle -{ - PlayerColor color = PlayerColor::CANNOT_DETERMINE; - const CGHeroInstance * hero = nullptr; //may be NULL if army is not commanded by hero - const CArmedInstance * armyObject = nullptr; //adv. map object with army that participates in battle; may be same as hero - - uint32_t castSpellsCount = 0; //how many spells each side has been cast this turn - std::vector usedSpellsHistory; //every time hero casts spell, it's inserted here -> eagle eye skill - int32_t enchanterCounter = 0; //tends to pass through 0, so sign is needed - - void init(const CGHeroInstance * Hero, const CArmedInstance * Army); - - - template void serialize(Handler &h, const int version) - { - h & color; - h & hero; - h & armyObject; - h & castSpellsCount; - h & usedSpellsHistory; - h & enchanterCounter; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * SideInBattle.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 "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CArmedInstance; + +struct DLL_LINKAGE SideInBattle +{ + PlayerColor color = PlayerColor::CANNOT_DETERMINE; + const CGHeroInstance * hero = nullptr; //may be NULL if army is not commanded by hero + const CArmedInstance * armyObject = nullptr; //adv. map object with army that participates in battle; may be same as hero + + uint32_t castSpellsCount = 0; //how many spells each side has been cast this turn + std::vector usedSpellsHistory; //every time hero casts spell, it's inserted here -> eagle eye skill + int32_t enchanterCounter = 0; //tends to pass through 0, so sign is needed + + void init(const CGHeroInstance * Hero, const CArmedInstance * Army); + + + template void serialize(Handler &h, const int version) + { + h & color; + h & hero; + h & armyObject; + h & castSpellsCount; + h & usedSpellsHistory; + h & enchanterCounter; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SiegeInfo.cpp b/lib/battle/SiegeInfo.cpp index 10877860f..be7a9d4b8 100644 --- a/lib/battle/SiegeInfo.cpp +++ b/lib/battle/SiegeInfo.cpp @@ -1,45 +1,45 @@ -/* - * SiegeInfo.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 "SiegeInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -SiegeInfo::SiegeInfo() -{ - for(int i = 0; i < static_cast(EWallPart::PARTS_COUNT); ++i) - { - wallState[static_cast(i)] = EWallState::NONE; - } - gateState = EGateState::NONE; -} - -EWallState SiegeInfo::applyDamage(EWallState state, unsigned int value) -{ - if(value == 0) - return state; - - switch(applyDamage(state, value - 1)) - { - case EWallState::REINFORCED: - return EWallState::INTACT; - case EWallState::INTACT: - return EWallState::DAMAGED; - case EWallState::DAMAGED: - return EWallState::DESTROYED; - case EWallState::DESTROYED: - return EWallState::DESTROYED; - default: - return EWallState::NONE; - } -} - -VCMI_LIB_NAMESPACE_END +/* + * SiegeInfo.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 "SiegeInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +SiegeInfo::SiegeInfo() +{ + for(int i = 0; i < static_cast(EWallPart::PARTS_COUNT); ++i) + { + wallState[static_cast(i)] = EWallState::NONE; + } + gateState = EGateState::NONE; +} + +EWallState SiegeInfo::applyDamage(EWallState state, unsigned int value) +{ + if(value == 0) + return state; + + switch(applyDamage(state, value - 1)) + { + case EWallState::REINFORCED: + return EWallState::INTACT; + case EWallState::INTACT: + return EWallState::DAMAGED; + case EWallState::DAMAGED: + return EWallState::DESTROYED; + case EWallState::DESTROYED: + return EWallState::DESTROYED; + default: + return EWallState::NONE; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/battle/SiegeInfo.h b/lib/battle/SiegeInfo.h index 680888201..6f69d8041 100644 --- a/lib/battle/SiegeInfo.h +++ b/lib/battle/SiegeInfo.h @@ -1,33 +1,33 @@ -/* - * SiegeInfo.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 "../GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -//only for use in BattleInfo -struct DLL_LINKAGE SiegeInfo -{ - std::map wallState; - EGateState gateState; - - SiegeInfo(); - - // return EWallState decreased by value of damage points - static EWallState applyDamage(EWallState state, unsigned int value); - - template void serialize(Handler &h, const int version) - { - h & wallState; - h & gateState; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * SiegeInfo.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 "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +//only for use in BattleInfo +struct DLL_LINKAGE SiegeInfo +{ + std::map wallState; + EGateState gateState; + + SiegeInfo(); + + // return EWallState decreased by value of damage points + static EWallState applyDamage(EWallState state, unsigned int value); + + template void serialize(Handler &h, const int version) + { + h & wallState; + h & gateState; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 6ae6bacfc..bf0fe2c87 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -1,302 +1,302 @@ -/* - * Bonus.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 "Bonus.h" -#include "CBonusSystemNode.h" -#include "Limiters.h" -#include "Updaters.h" -#include "Propagators.h" - -#include "../VCMI_Lib.h" -#include "../spells/CSpellHandler.h" -#include "../CCreatureHandler.h" -#include "../CCreatureSet.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CSkillHandler.h" -#include "../CArtHandler.h" -#include "../TerrainHandler.h" -#include "../constants/StringConstants.h" -#include "../battle/BattleInfo.h" -#include "../modding/ModUtility.h" - -VCMI_LIB_NAMESPACE_BEGIN - -//This constructor should be placed here to avoid side effects -CAddInfo::CAddInfo() = default; - -CAddInfo::CAddInfo(si32 value) -{ - if(value != CAddInfo::NONE) - push_back(value); -} - -bool CAddInfo::operator==(si32 value) const -{ - switch(size()) - { - case 0: - return value == CAddInfo::NONE; - case 1: - return operator[](0) == value; - default: - return false; - } -} - -bool CAddInfo::operator!=(si32 value) const -{ - return !operator==(value); -} - -si32 & CAddInfo::operator[](size_type pos) -{ - if(pos >= size()) - resize(pos + 1, CAddInfo::NONE); - return vector::operator[](pos); -} - -si32 CAddInfo::operator[](size_type pos) const -{ - return pos < size() ? vector::operator[](pos) : CAddInfo::NONE; -} - -std::string CAddInfo::toString() const -{ - return toJsonNode().toJson(true); -} - -JsonNode CAddInfo::toJsonNode() const -{ - if(size() < 2) - { - return JsonUtils::intNode(operator[](0)); - } - else - { - JsonNode node(JsonNode::JsonType::DATA_VECTOR); - for(si32 value : *this) - node.Vector().push_back(JsonUtils::intNode(value)); - return node; - } -} -std::string Bonus::Description(std::optional customValue) const -{ - std::ostringstream str; - - if(description.empty()) - { - if(stacking.empty() || stacking == "ALWAYS") - { - switch(source) - { - case BonusSource::ARTIFACT: - str << ArtifactID(sid).toArtifact(VLC->artifacts())->getNameTranslated(); - break; - case BonusSource::SPELL_EFFECT: - str << SpellID(sid).toSpell(VLC->spells())->getNameTranslated(); - break; - case BonusSource::CREATURE_ABILITY: - str << CreatureID(sid).toCreature(VLC->creatures())->getNamePluralTranslated(); - break; - case BonusSource::SECONDARY_SKILL: - str << VLC->skills()->getByIndex(sid)->getNameTranslated(); - break; - case BonusSource::HERO_SPECIAL: - str << VLC->heroTypes()->getByIndex(sid)->getNameTranslated(); - break; - default: - //todo: handle all possible sources - str << "Unknown"; - break; - } - } - else - str << stacking; - } - else - { - str << description; - } - - if(auto value = customValue.value_or(val)) - str << " " << std::showpos << value; - - return str.str(); -} - -static JsonNode subtypeToJson(BonusType type, int subtype) -{ - switch(type) - { - case BonusType::PRIMARY_SKILL: - return JsonUtils::stringNode("primSkill." + NPrimarySkill::names[subtype]); - case BonusType::SPECIAL_SPELL_LEV: - case BonusType::SPECIFIC_SPELL_DAMAGE: - case BonusType::SPELL: - case BonusType::SPECIAL_PECULIAR_ENCHANT: - case BonusType::SPECIAL_ADD_VALUE_ENCHANT: - case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "spell", SpellID::encode(subtype))); - case BonusType::IMPROVED_NECROMANCY: - case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); - case BonusType::GENERATE_RESOURCE: - return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]); - default: - return JsonUtils::intNode(subtype); - } -} - -static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) -{ - switch(type) - { - case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); - default: - return addInfo.toJsonNode(); - } -} - -JsonNode Bonus::toJsonNode() const -{ - JsonNode root(JsonNode::JsonType::DATA_STRUCT); - // only add values that might reasonably be found in config files - root["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtype != -1) - root["subtype"] = subtypeToJson(type, subtype); - if(additionalInfo != CAddInfo::NONE) - root["addInfo"] = additionalInfoToJson(type, additionalInfo); - if(source != BonusSource::OTHER) - root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); - if(targetSourceType != BonusSource::OTHER) - root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); - if(sid != 0) - root["sourceID"].Integer() = sid; - if(val != 0) - root["val"].Integer() = val; - if(valType != BonusValueType::ADDITIVE_VALUE) - root["valueType"].String() = vstd::findKey(bonusValueMap, valType); - if(!stacking.empty()) - root["stacking"].String() = stacking; - if(!description.empty()) - root["description"].String() = description; - if(effectRange != BonusLimitEffect::NO_LIMIT) - root["effectRange"].String() = vstd::findKey(bonusLimitEffect, effectRange); - if(duration != BonusDuration::PERMANENT) - root["duration"] = BonusDuration::toJson(duration); - if(turnsRemain) - root["turns"].Integer() = turnsRemain; - if(limiter) - root["limiters"] = limiter->toJsonNode(); - if(updater) - root["updater"] = updater->toJsonNode(); - if(propagator) - root["propagator"].String() = vstd::findKey(bonusPropagatorMap, propagator); - return root; -} - -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype): - duration(Duration), - type(Type), - subtype(Subtype), - source(Src), - val(Val), - sid(ID), - description(std::move(Desc)) -{ - boost::algorithm::trim(description); - targetSourceType = BonusSource::OTHER; -} - -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype, BonusValueType ValType): - duration(Duration), - type(Type), - subtype(Subtype), - source(Src), - val(Val), - sid(ID), - valType(ValType) -{ - turnsRemain = 0; - effectRange = BonusLimitEffect::NO_LIMIT; - targetSourceType = BonusSource::OTHER; -} - -std::shared_ptr Bonus::addPropagator(const TPropagatorPtr & Propagator) -{ - propagator = Propagator; - return this->shared_from_this(); -} - -DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) -{ - for(const auto & i : bonusNameMap) - if(i.second == bonus.type) - out << "\tType: " << i.first << " \t"; - -#define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" - printField(val); - printField(subtype); - printField(duration.to_ulong()); - printField(source); - printField(sid); - if(bonus.additionalInfo != CAddInfo::NONE) - out << "\taddInfo: " << bonus.additionalInfo.toString() << "\n"; - printField(turnsRemain); - printField(valType); - if(!bonus.stacking.empty()) - out << "\tstacking: \"" << bonus.stacking << "\"\n"; - printField(effectRange); -#undef printField - - if(bonus.limiter) - out << "\tLimiter: " << bonus.limiter->toString() << "\n"; - if(bonus.updater) - out << "\tUpdater: " << bonus.updater->toString() << "\n"; - - return out; -} - -std::shared_ptr Bonus::addLimiter(const TLimiterPtr & Limiter) -{ - if (limiter) - { - //If we already have limiter list, retrieve it - auto limiterList = std::dynamic_pointer_cast(limiter); - if(!limiterList) - { - //Create a new limiter list with old limiter and the new one will be pushed later - limiterList = std::make_shared(); - limiterList->add(limiter); - limiter = limiterList; - } - - limiterList->add(Limiter); - } - else - { - limiter = Limiter; - } - return this->shared_from_this(); -} - -// Updaters - -std::shared_ptr Bonus::addUpdater(const TUpdaterPtr & Updater) -{ - updater = Updater; - return this->shared_from_this(); -} - -VCMI_LIB_NAMESPACE_END +/* + * Bonus.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 "Bonus.h" +#include "CBonusSystemNode.h" +#include "Limiters.h" +#include "Updaters.h" +#include "Propagators.h" + +#include "../VCMI_Lib.h" +#include "../spells/CSpellHandler.h" +#include "../CCreatureHandler.h" +#include "../CCreatureSet.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CSkillHandler.h" +#include "../CArtHandler.h" +#include "../TerrainHandler.h" +#include "../constants/StringConstants.h" +#include "../battle/BattleInfo.h" +#include "../modding/ModUtility.h" + +VCMI_LIB_NAMESPACE_BEGIN + +//This constructor should be placed here to avoid side effects +CAddInfo::CAddInfo() = default; + +CAddInfo::CAddInfo(si32 value) +{ + if(value != CAddInfo::NONE) + push_back(value); +} + +bool CAddInfo::operator==(si32 value) const +{ + switch(size()) + { + case 0: + return value == CAddInfo::NONE; + case 1: + return operator[](0) == value; + default: + return false; + } +} + +bool CAddInfo::operator!=(si32 value) const +{ + return !operator==(value); +} + +si32 & CAddInfo::operator[](size_type pos) +{ + if(pos >= size()) + resize(pos + 1, CAddInfo::NONE); + return vector::operator[](pos); +} + +si32 CAddInfo::operator[](size_type pos) const +{ + return pos < size() ? vector::operator[](pos) : CAddInfo::NONE; +} + +std::string CAddInfo::toString() const +{ + return toJsonNode().toJson(true); +} + +JsonNode CAddInfo::toJsonNode() const +{ + if(size() < 2) + { + return JsonUtils::intNode(operator[](0)); + } + else + { + JsonNode node(JsonNode::JsonType::DATA_VECTOR); + for(si32 value : *this) + node.Vector().push_back(JsonUtils::intNode(value)); + return node; + } +} +std::string Bonus::Description(std::optional customValue) const +{ + std::ostringstream str; + + if(description.empty()) + { + if(stacking.empty() || stacking == "ALWAYS") + { + switch(source) + { + case BonusSource::ARTIFACT: + str << ArtifactID(sid).toArtifact(VLC->artifacts())->getNameTranslated(); + break; + case BonusSource::SPELL_EFFECT: + str << SpellID(sid).toSpell(VLC->spells())->getNameTranslated(); + break; + case BonusSource::CREATURE_ABILITY: + str << CreatureID(sid).toCreature(VLC->creatures())->getNamePluralTranslated(); + break; + case BonusSource::SECONDARY_SKILL: + str << VLC->skills()->getByIndex(sid)->getNameTranslated(); + break; + case BonusSource::HERO_SPECIAL: + str << VLC->heroTypes()->getByIndex(sid)->getNameTranslated(); + break; + default: + //todo: handle all possible sources + str << "Unknown"; + break; + } + } + else + str << stacking; + } + else + { + str << description; + } + + if(auto value = customValue.value_or(val)) + str << " " << std::showpos << value; + + return str.str(); +} + +static JsonNode subtypeToJson(BonusType type, int subtype) +{ + switch(type) + { + case BonusType::PRIMARY_SKILL: + return JsonUtils::stringNode("primSkill." + NPrimarySkill::names[subtype]); + case BonusType::SPECIAL_SPELL_LEV: + case BonusType::SPECIFIC_SPELL_DAMAGE: + case BonusType::SPELL: + case BonusType::SPECIAL_PECULIAR_ENCHANT: + case BonusType::SPECIAL_ADD_VALUE_ENCHANT: + case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "spell", SpellID::encode(subtype))); + case BonusType::IMPROVED_NECROMANCY: + case BonusType::SPECIAL_UPGRADE: + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); + case BonusType::GENERATE_RESOURCE: + return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]); + default: + return JsonUtils::intNode(subtype); + } +} + +static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) +{ + switch(type) + { + case BonusType::SPECIAL_UPGRADE: + return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(addInfo[0]))); + default: + return addInfo.toJsonNode(); + } +} + +JsonNode Bonus::toJsonNode() const +{ + JsonNode root(JsonNode::JsonType::DATA_STRUCT); + // only add values that might reasonably be found in config files + root["type"].String() = vstd::findKey(bonusNameMap, type); + if(subtype != -1) + root["subtype"] = subtypeToJson(type, subtype); + if(additionalInfo != CAddInfo::NONE) + root["addInfo"] = additionalInfoToJson(type, additionalInfo); + if(source != BonusSource::OTHER) + root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); + if(targetSourceType != BonusSource::OTHER) + root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); + if(sid != 0) + root["sourceID"].Integer() = sid; + if(val != 0) + root["val"].Integer() = val; + if(valType != BonusValueType::ADDITIVE_VALUE) + root["valueType"].String() = vstd::findKey(bonusValueMap, valType); + if(!stacking.empty()) + root["stacking"].String() = stacking; + if(!description.empty()) + root["description"].String() = description; + if(effectRange != BonusLimitEffect::NO_LIMIT) + root["effectRange"].String() = vstd::findKey(bonusLimitEffect, effectRange); + if(duration != BonusDuration::PERMANENT) + root["duration"] = BonusDuration::toJson(duration); + if(turnsRemain) + root["turns"].Integer() = turnsRemain; + if(limiter) + root["limiters"] = limiter->toJsonNode(); + if(updater) + root["updater"] = updater->toJsonNode(); + if(propagator) + root["propagator"].String() = vstd::findKey(bonusPropagatorMap, propagator); + return root; +} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype): + duration(Duration), + type(Type), + subtype(Subtype), + source(Src), + val(Val), + sid(ID), + description(std::move(Desc)) +{ + boost::algorithm::trim(description); + targetSourceType = BonusSource::OTHER; +} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype, BonusValueType ValType): + duration(Duration), + type(Type), + subtype(Subtype), + source(Src), + val(Val), + sid(ID), + valType(ValType) +{ + turnsRemain = 0; + effectRange = BonusLimitEffect::NO_LIMIT; + targetSourceType = BonusSource::OTHER; +} + +std::shared_ptr Bonus::addPropagator(const TPropagatorPtr & Propagator) +{ + propagator = Propagator; + return this->shared_from_this(); +} + +DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) +{ + for(const auto & i : bonusNameMap) + if(i.second == bonus.type) + out << "\tType: " << i.first << " \t"; + +#define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" + printField(val); + printField(subtype); + printField(duration.to_ulong()); + printField(source); + printField(sid); + if(bonus.additionalInfo != CAddInfo::NONE) + out << "\taddInfo: " << bonus.additionalInfo.toString() << "\n"; + printField(turnsRemain); + printField(valType); + if(!bonus.stacking.empty()) + out << "\tstacking: \"" << bonus.stacking << "\"\n"; + printField(effectRange); +#undef printField + + if(bonus.limiter) + out << "\tLimiter: " << bonus.limiter->toString() << "\n"; + if(bonus.updater) + out << "\tUpdater: " << bonus.updater->toString() << "\n"; + + return out; +} + +std::shared_ptr Bonus::addLimiter(const TLimiterPtr & Limiter) +{ + if (limiter) + { + //If we already have limiter list, retrieve it + auto limiterList = std::dynamic_pointer_cast(limiter); + if(!limiterList) + { + //Create a new limiter list with old limiter and the new one will be pushed later + limiterList = std::make_shared(); + limiterList->add(limiter); + limiter = limiterList; + } + + limiterList->add(Limiter); + } + else + { + limiter = Limiter; + } + return this->shared_from_this(); +} + +// Updaters + +std::shared_ptr Bonus::addUpdater(const TUpdaterPtr & Updater) +{ + updater = Updater; + return this->shared_from_this(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 20bc16f3d..3c9613d1b 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -1,191 +1,191 @@ -/* - * Bonus.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 "BonusEnum.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct Bonus; -class IBonusBearer; -class CBonusSystemNode; -class ILimiter; -class IPropagator; -class IUpdater; -class BonusList; -class CSelector; - -using TBonusSubtype = int32_t; -using TBonusListPtr = std::shared_ptr; -using TConstBonusListPtr = std::shared_ptr; -using TLimiterPtr = std::shared_ptr; -using TPropagatorPtr = std::shared_ptr; -using TUpdaterPtr = std::shared_ptr; - -class DLL_LINKAGE CAddInfo : public std::vector -{ -public: - enum { NONE = -1 }; - - CAddInfo(); - CAddInfo(si32 value); - - bool operator==(si32 value) const; - bool operator!=(si32 value) const; - - si32 & operator[](size_type pos); - si32 operator[](size_type pos) const; - - std::string toString() const; - JsonNode toJsonNode() const; -}; - -#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); - -/// Struct for handling bonuses of several types. Can be transferred to any hero -struct DLL_LINKAGE Bonus : public std::enable_shared_from_this -{ - BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values - si16 turnsRemain = 0; //used if duration is N_TURNS, N_DAYS or ONE_WEEK - - BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte - TBonusSubtype subtype = -1; //-1 if not applicable - 4 bytes - - BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus - BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. - si32 val = 0; - ui32 sid = 0; //source id: id of object/artifact/spell - BonusValueType valType = BonusValueType::ADDITIVE_VALUE; - std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus) - - CAddInfo additionalInfo; - BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; //if not NO_LIMIT, bonus will be omitted by default - - TLimiterPtr limiter; - TPropagatorPtr propagator; - TUpdaterPtr updater; - TUpdaterPtr propagationUpdater; - - std::string description; - - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype=-1); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype=-1, BonusValueType ValType = BonusValueType::ADDITIVE_VALUE); - Bonus() = default; - - template void serialize(Handler &h, const int version) - { - h & duration; - h & type; - h & subtype; - h & source; - h & val; - h & sid; - h & description; - h & additionalInfo; - h & turnsRemain; - h & valType; - h & stacking; - h & effectRange; - h & limiter; - h & propagator; - h & updater; - h & propagationUpdater; - h & targetSourceType; - } - - template - static bool compareByAdditionalInfo(const Ptr& a, const Ptr& b) - { - return a->additionalInfo < b->additionalInfo; - } - static bool NDays(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::N_DAYS; - return set.any(); - } - static bool NTurns(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::N_TURNS; - return set.any(); - } - static bool OneDay(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_DAY; - return set.any(); - } - static bool OneWeek(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_WEEK; - return set.any(); - } - static bool OneBattle(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::ONE_BATTLE; - return set.any(); - } - static bool Permanent(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::PERMANENT; - return set.any(); - } - static bool UntilGetsTurn(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::STACK_GETS_TURN; - return set.any(); - } - static bool UntilAttack(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::UNTIL_ATTACK; - return set.any(); - } - static bool UntilBeingAttacked(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::UNTIL_BEING_ATTACKED; - return set.any(); - } - static bool UntilCommanderKilled(const Bonus *hb) - { - auto set = hb->duration & BonusDuration::COMMANDER_KILLED; - return set.any(); - } - inline bool operator == (const BonusType & cf) const - { - return type == cf; - } - inline void operator += (const ui32 Val) //no return - { - val += Val; - } - STRONG_INLINE static ui32 getSid32(ui32 high, ui32 low) - { - return (high << 16) + low; - } - - STRONG_INLINE static ui32 getHighFromSid32(ui32 sid) - { - return sid >> 16; - } - - STRONG_INLINE static ui32 getLowFromSid32(ui32 sid) - { - return sid & 0x0000FFFF; - } - - std::string Description(std::optional customValue = {}) const; - JsonNode toJsonNode() const; - - std::shared_ptr addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls - std::shared_ptr addPropagator(const TPropagatorPtr & Propagator); //returns this for convenient chain-calls - std::shared_ptr addUpdater(const TUpdaterPtr & Updater); //returns this for convenient chain-calls -}; - -DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus); - -VCMI_LIB_NAMESPACE_END +/* + * Bonus.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 "BonusEnum.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct Bonus; +class IBonusBearer; +class CBonusSystemNode; +class ILimiter; +class IPropagator; +class IUpdater; +class BonusList; +class CSelector; + +using TBonusSubtype = int32_t; +using TBonusListPtr = std::shared_ptr; +using TConstBonusListPtr = std::shared_ptr; +using TLimiterPtr = std::shared_ptr; +using TPropagatorPtr = std::shared_ptr; +using TUpdaterPtr = std::shared_ptr; + +class DLL_LINKAGE CAddInfo : public std::vector +{ +public: + enum { NONE = -1 }; + + CAddInfo(); + CAddInfo(si32 value); + + bool operator==(si32 value) const; + bool operator!=(si32 value) const; + + si32 & operator[](size_type pos); + si32 operator[](size_type pos) const; + + std::string toString() const; + JsonNode toJsonNode() const; +}; + +#define BONUS_TREE_DESERIALIZATION_FIX if(!h.saving && h.smartPointerSerialization) deserializationFix(); + +/// Struct for handling bonuses of several types. Can be transferred to any hero +struct DLL_LINKAGE Bonus : public std::enable_shared_from_this +{ + BonusDuration::Type duration = BonusDuration::PERMANENT; //uses BonusDuration values + si16 turnsRemain = 0; //used if duration is N_TURNS, N_DAYS or ONE_WEEK + + BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte + TBonusSubtype subtype = -1; //-1 if not applicable - 4 bytes + + BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus + BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. + si32 val = 0; + ui32 sid = 0; //source id: id of object/artifact/spell + BonusValueType valType = BonusValueType::ADDITIVE_VALUE; + std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus) + + CAddInfo additionalInfo; + BonusLimitEffect effectRange = BonusLimitEffect::NO_LIMIT; //if not NO_LIMIT, bonus will be omitted by default + + TLimiterPtr limiter; + TPropagatorPtr propagator; + TUpdaterPtr updater; + TUpdaterPtr propagationUpdater; + + std::string description; + + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype=-1); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype=-1, BonusValueType ValType = BonusValueType::ADDITIVE_VALUE); + Bonus() = default; + + template void serialize(Handler &h, const int version) + { + h & duration; + h & type; + h & subtype; + h & source; + h & val; + h & sid; + h & description; + h & additionalInfo; + h & turnsRemain; + h & valType; + h & stacking; + h & effectRange; + h & limiter; + h & propagator; + h & updater; + h & propagationUpdater; + h & targetSourceType; + } + + template + static bool compareByAdditionalInfo(const Ptr& a, const Ptr& b) + { + return a->additionalInfo < b->additionalInfo; + } + static bool NDays(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::N_DAYS; + return set.any(); + } + static bool NTurns(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::N_TURNS; + return set.any(); + } + static bool OneDay(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_DAY; + return set.any(); + } + static bool OneWeek(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_WEEK; + return set.any(); + } + static bool OneBattle(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::ONE_BATTLE; + return set.any(); + } + static bool Permanent(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::PERMANENT; + return set.any(); + } + static bool UntilGetsTurn(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::STACK_GETS_TURN; + return set.any(); + } + static bool UntilAttack(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::UNTIL_ATTACK; + return set.any(); + } + static bool UntilBeingAttacked(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::UNTIL_BEING_ATTACKED; + return set.any(); + } + static bool UntilCommanderKilled(const Bonus *hb) + { + auto set = hb->duration & BonusDuration::COMMANDER_KILLED; + return set.any(); + } + inline bool operator == (const BonusType & cf) const + { + return type == cf; + } + inline void operator += (const ui32 Val) //no return + { + val += Val; + } + STRONG_INLINE static ui32 getSid32(ui32 high, ui32 low) + { + return (high << 16) + low; + } + + STRONG_INLINE static ui32 getHighFromSid32(ui32 sid) + { + return sid >> 16; + } + + STRONG_INLINE static ui32 getLowFromSid32(ui32 sid) + { + return sid & 0x0000FFFF; + } + + std::string Description(std::optional customValue = {}) const; + JsonNode toJsonNode() const; + + std::shared_ptr addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls + std::shared_ptr addPropagator(const TPropagatorPtr & Propagator); //returns this for convenient chain-calls + std::shared_ptr addUpdater(const TUpdaterPtr & Updater); //returns this for convenient chain-calls +}; + +DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignHandler.cpp b/lib/campaign/CampaignHandler.cpp index 5d1dd9340..ad59559df 100644 --- a/lib/campaign/CampaignHandler.cpp +++ b/lib/campaign/CampaignHandler.cpp @@ -1,622 +1,622 @@ -/* - * CCampaignHandler.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 "CampaignHandler.h" - -#include "CampaignState.h" - -#include "../filesystem/Filesystem.h" -#include "../filesystem/CCompressedStream.h" -#include "../filesystem/CMemoryStream.h" -#include "../filesystem/CBinaryReader.h" -#include "../modding/IdentifierStorage.h" -#include "../VCMI_Lib.h" -#include "../CGeneralTextHandler.h" -#include "../TextOperations.h" -#include "../Languages.h" -#include "../constants/StringConstants.h" -#include "../mapping/CMapHeader.h" -#include "../mapping/CMapService.h" -#include "../modding/CModHandler.h" -#include "../modding/ModScope.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void CampaignHandler::readCampaign(Campaign * ret, const std::vector & input, std::string filename, std::string modName, std::string encoding) -{ - if (input.front() < uint8_t(' ')) // binary format - { - CMemoryStream stream(input.data(), input.size()); - CBinaryReader reader(&stream); - - readHeaderFromMemory(*ret, reader, filename, modName, encoding); - - for(int g = 0; g < ret->numberOfScenarios; ++g) - { - auto scenarioID = static_cast(ret->scenarios.size()); - ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); - } - } - else // text format (json) - { - JsonNode jsonCampaign((const char*)input.data(), input.size()); - readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); - - for(auto & scenario : jsonCampaign["scenarios"].Vector()) - { - auto scenarioID = static_cast(ret->scenarios.size()); - ret->scenarios[scenarioID] = readScenarioFromJson(scenario); - } - } -} - -std::unique_ptr CampaignHandler::getHeader( const std::string & name) -{ - ResourcePath resourceID(name, EResType::CAMPAIGN); - std::string modName = VLC->modh->findResourceOrigin(resourceID); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; - - auto ret = std::make_unique(); - auto fileStream = CResourceHandler::get(modName)->load(resourceID); - std::vector cmpgn = getFile(std::move(fileStream), true)[0]; - - readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding); - - return ret; -} - -std::shared_ptr CampaignHandler::getCampaign( const std::string & name ) -{ - ResourcePath resourceID(name, EResType::CAMPAIGN); - std::string modName = VLC->modh->findResourceOrigin(resourceID); - std::string language = VLC->modh->getModLanguage(modName); - std::string encoding = Languages::getLanguageOptions(language).encoding; - - auto ret = std::make_unique(); - - auto fileStream = CResourceHandler::get(modName)->load(resourceID); - - std::vector> files = getFile(std::move(fileStream), false); - - readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding); - - //first entry is campaign header. start loop from 1 - for(int scenarioIndex = 0, fileIndex = 1; fileIndex < files.size() && scenarioIndex < ret->numberOfScenarios; scenarioIndex++) - { - auto scenarioID = static_cast(scenarioIndex); - - if(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios - continue; - - std::string scenarioName = resourceID.getName(); - boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(fileIndex - 1); - - //set map piece appropriately, convert vector to string - ret->mapPieces[scenarioID] = files[fileIndex]; - - auto hdr = ret->getMapHeader(scenarioID); - ret->scenarios[scenarioID].scenarioName = hdr->name; - fileIndex++; - } - - return ret; -} - -static std::string convertMapName(std::string input) -{ - boost::algorithm::to_lower(input); - boost::algorithm::trim(input); - - size_t slashPos = input.find_last_of("/"); - - if (slashPos != std::string::npos) - return input.substr(slashPos + 1); - - return input; -} - -std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) -{ - TextIdentifier stringID( "campaign", convertMapName(filename), identifier); - - std::string input = TextOperations::toUnicode(reader.readBaseString(), encoding); - - if (input.empty()) - return ""; - - VLC->generaltexth->registerString(modName, stringID, input); - return stringID.get(); -} - -void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding) -{ - ret.version = static_cast(reader["version"].Integer()); - if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX) - { - logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, static_cast(ret.version)); - return; - } - - ret.version = CampaignVersion::VCMI; - ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); - ret.numberOfScenarios = reader["scenarios"].Vector().size(); - ret.name.appendTextID(reader["name"].String()); - ret.description.appendTextID(reader["description"].String()); - ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); - ret.music = AudioPath::fromJson(reader["music"]); - ret.filename = filename; - ret.modName = modName; - ret.encoding = encoding; -} - -CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) -{ - auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog - { - CampaignScenarioPrologEpilog ret; - ret.hasPrologEpilog = !identifier.isNull(); - if(ret.hasPrologEpilog) - { - ret.prologVideo = VideoPath::fromJson(identifier["video"]); - ret.prologMusic = AudioPath::fromJson(identifier["music"]); - ret.prologVoice = AudioPath::fromJson(identifier["voice"]); - ret.prologText.jsonDeserialize(identifier["text"]); - } - return ret; - }; - - CampaignScenario ret; - ret.mapName = reader["map"].String(); - for(auto & g : reader["preconditions"].Vector()) - ret.preconditionRegions.insert(static_cast(g.Integer())); - - ret.regionColor = reader["color"].Integer(); - ret.difficulty = reader["difficulty"].Integer(); - ret.regionText.jsonDeserialize(reader["regionText"]); - ret.prolog = prologEpilogReader(reader["prolog"]); - ret.epilog = prologEpilogReader(reader["epilog"]); - - ret.travelOptions = readScenarioTravelFromJson(reader); - - return ret; -} - -CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) -{ - CampaignTravel ret; - - std::map startOptionsMap = { - {"none", CampaignStartOptions::NONE}, - {"bonus", CampaignStartOptions::START_BONUS}, - {"crossover", CampaignStartOptions::HERO_CROSSOVER}, - {"hero", CampaignStartOptions::HERO_OPTIONS} - }; - - std::map bonusTypeMap = { - {"spell", CampaignBonusType::SPELL}, - {"creature", CampaignBonusType::MONSTER}, - {"building", CampaignBonusType::BUILDING}, - {"artifact", CampaignBonusType::ARTIFACT}, - {"scroll", CampaignBonusType::SPELL_SCROLL}, - {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, - {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, - {"resource", CampaignBonusType::RESOURCE}, - //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, - //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, - }; - - std::map primarySkillsMap = { - {"attack", 0}, - {"defence", 8}, - {"spellpower", 16}, - {"knowledge", 24}, - }; - - std::map heroSpecialMap = { - {"strongest", 0xFFFD}, - {"generated", 0xFFFE}, - {"random", 0xFFFF} - }; - - std::map resourceTypeMap = { - //FD - wood+ore - //FE - mercury+sulfur+crystal+gem - {"wood", 0}, - {"mercury", 1}, - {"ore", 2}, - {"sulfur", 3}, - {"crystal", 4}, - {"gems", 5}, - {"gold", 6}, - {"common", 0xFD}, - {"rare", 0xFE} - }; - - for(auto & k : reader["heroKeeps"].Vector()) - { - if(k.String() == "experience") ret.whatHeroKeeps.experience = true; - if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true; - if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true; - if(k.String() == "spells") ret.whatHeroKeeps.spells = true; - if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true; - } - - for(auto & k : reader["keepCreatures"].Vector()) - { - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "creature", k.String())) - ret.monstersKeptByHero.insert(CreatureID(identifier.value())); - else - logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); - } - for(auto & k : reader["keepArtifacts"].Vector()) - { - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "artifact", k.String())) - ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); - else - logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); - } - - ret.startOptions = startOptionsMap[reader["startOptions"].String()]; - switch(ret.startOptions) - { - case CampaignStartOptions::NONE: - //no bonuses. Seems to be OK - break; - case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose - { - ret.playerColor = PlayerColor(PlayerColor::decode(reader["playerColor"].String())); - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = bonusTypeMap[bjson["what"].String()]; - switch (bonus.type) - { - case CampaignBonusType::RESOURCE: - bonus.info1 = resourceTypeMap[bjson["type"].String()]; - bonus.info2 = bjson["amount"].Integer(); - break; - - case CampaignBonusType::BUILDING: - bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String()); - if(bonus.info1 == -1) - logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String()); - break; - - default: - if(int heroId = heroSpecialMap[bjson["hero"].String()]) - bonus.info1 = heroId; - else - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) - bonus.info1 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); - - bonus.info3 = bjson["amount"].Integer(); - - switch(bonus.type) - { - case CampaignBonusType::SPELL: - case CampaignBonusType::MONSTER: - case CampaignBonusType::SECONDARY_SKILL: - case CampaignBonusType::ARTIFACT: - if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), bjson["what"].String(), bjson["type"].String())) - bonus.info2 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); - break; - - case CampaignBonusType::SPELL_SCROLL: - if(auto Identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "spell", bjson["type"].String())) - bonus.info2 = Identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); - break; - - case CampaignBonusType::PRIMARY_SKILL: - for(auto & ps : primarySkillsMap) - bonus.info2 |= bjson[ps.first].Integer() << ps.second; - break; - - default: - bonus.info2 = bjson["type"].Integer(); - } - break; - } - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose - { - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO; - bonus.info1 = bjson["playerColor"].Integer(); //player color - bonus.info2 = bjson["scenario"].Integer(); //from what scenario - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_OPTIONS: //heroes player can choose between - { - for(auto & bjson : reader["bonuses"].Vector()) - { - CampaignBonus bonus; - bonus.type = CampaignBonusType::HERO; - bonus.info1 = bjson["playerColor"].Integer(); //player color - - if(int heroId = heroSpecialMap[bjson["hero"].String()]) - bonus.info2 = heroId; - else - if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) - bonus.info2 = identifier.value(); - else - logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); - - ret.bonusesToChoose.push_back(bonus); - } - break; - } - default: - { - logGlobal->warn("VCMP Loading: Unsupported start options value"); - break; - } - } - - return ret; -} - - -void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) -{ - ret.version = static_cast(reader.readUInt32()); - ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] - ret.loadLegacyData(campId); - ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); - ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); - if (ret.version > CampaignVersion::RoE) - ret.difficultyChoosenByPlayer = reader.readInt8(); - else - ret.difficultyChoosenByPlayer = false; - - ret.music = prologMusicName(reader.readInt8()); - ret.filename = filename; - ret.modName = modName; - ret.encoding = encoding; -} - -CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) -{ - auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog - { - CampaignScenarioPrologEpilog ret; - ret.hasPrologEpilog = reader.readUInt8(); - if(ret.hasPrologEpilog) - { - bool isOriginalCampaign = boost::starts_with(header.getFilename(), "DATA/"); - - ui8 index = reader.readUInt8(); - ret.prologVideo = CampaignHandler::prologVideoName(index); - ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); - ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); - ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier)); - } - return ret; - }; - - CampaignScenario ret; - ret.mapName = reader.readBaseString(); - reader.readUInt32(); //packedMapSize - not used - if(header.numberOfScenarios > 8) //unholy alliance - { - ret.loadPreconditionRegions(reader.readUInt16()); - } - else - { - ret.loadPreconditionRegions(reader.readUInt8()); - } - ret.regionColor = reader.readUInt8(); - ret.difficulty = reader.readUInt8(); - ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); - ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); - ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); - - ret.travelOptions = readScenarioTravelFromMemory(reader, header.version); - - return ret; -} - -template -static void readContainer(std::set & container, CBinaryReader & reader, int sizeBytes) -{ - for(int iId = 0, byte = 0; iId < sizeBytes * 8; ++iId) - { - if(iId % 8 == 0) - byte = reader.readUInt8(); - if(byte & (1 << iId % 8)) - container.insert(Identifier(iId)); - } -} - -CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version ) -{ - CampaignTravel ret; - - ui8 whatHeroKeeps = reader.readUInt8(); - ret.whatHeroKeeps.experience = whatHeroKeeps & 1; - ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2; - ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4; - ret.whatHeroKeeps.spells = whatHeroKeeps & 8; - ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16; - - readContainer(ret.monstersKeptByHero, reader, 19); - readContainer(ret.artifactsKeptByHero, reader, version < CampaignVersion::SoD ? 17 : 18); - - ret.startOptions = static_cast(reader.readUInt8()); - - switch(ret.startOptions) - { - case CampaignStartOptions::NONE: - //no bonuses. Seems to be OK - break; - case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose - { - ret.playerColor.setNum(reader.readUInt8()); - ui8 numOfBonuses = reader.readUInt8(); - for (int g=0; g(reader.readUInt8()); - //hero: FFFD means 'most powerful' and FFFE means 'generated' - switch(bonus.type) - { - case CampaignBonusType::SPELL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //spell ID - break; - } - case CampaignBonusType::MONSTER: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt16(); //monster type - bonus.info3 = reader.readUInt16(); //monster count - break; - } - case CampaignBonusType::BUILDING: - { - bonus.info1 = reader.readUInt8(); //building ID (0 - town hall, 1 - city hall, 2 - capitol, etc) - break; - } - case CampaignBonusType::ARTIFACT: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt16(); //artifact ID - break; - } - case CampaignBonusType::SPELL_SCROLL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //spell ID - break; - } - case CampaignBonusType::PRIMARY_SKILL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt32(); //bonuses (4 bytes for 4 skills) - break; - } - case CampaignBonusType::SECONDARY_SKILL: - { - bonus.info1 = reader.readUInt16(); //hero - bonus.info2 = reader.readUInt8(); //skill ID - bonus.info3 = reader.readUInt8(); //skill level - break; - } - case CampaignBonusType::RESOURCE: - { - bonus.info1 = reader.readUInt8(); //type - //FD - wood+ore - //FE - mercury+sulfur+crystal+gem - bonus.info2 = reader.readUInt32(); //count - break; - } - default: - logGlobal->warn("Corrupted h3c file"); - break; - } - ret.bonusesToChoose.push_back(bonus); - } - break; - } - case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose - { - ui8 numOfBonuses = reader.readUInt8(); - for (int g=0; gwarn("Corrupted h3c file"); - break; - } - } - - return ret; -} - -std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, bool headerOnly) -{ - CCompressedStream stream(std::move(file), true); - - std::vector< std::vector > ret; - do - { - std::vector block(stream.getSize()); - stream.read(block.data(), block.size()); - ret.push_back(block); - ret.back().shrink_to_fit(); - } - while (!headerOnly && stream.getNextBlock()); - - return ret; -} - -VideoPath CampaignHandler::prologVideoName(ui8 index) -{ - JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); - auto vids = config["videos"].Vector(); - if(index < vids.size()) - return VideoPath::fromJson(vids[index]); - return VideoPath(); -} - -AudioPath CampaignHandler::prologMusicName(ui8 index) -{ - std::vector music; - return AudioPath::builtinTODO(VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index)))); -} - -AudioPath CampaignHandler::prologVoiceName(ui8 index) -{ - JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); - auto audio = config["voice"].Vector(); - if(index < audio.size()) - return AudioPath::fromJson(audio[index]); - return AudioPath(); -} - -VCMI_LIB_NAMESPACE_END +/* + * CCampaignHandler.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 "CampaignHandler.h" + +#include "CampaignState.h" + +#include "../filesystem/Filesystem.h" +#include "../filesystem/CCompressedStream.h" +#include "../filesystem/CMemoryStream.h" +#include "../filesystem/CBinaryReader.h" +#include "../modding/IdentifierStorage.h" +#include "../VCMI_Lib.h" +#include "../CGeneralTextHandler.h" +#include "../TextOperations.h" +#include "../Languages.h" +#include "../constants/StringConstants.h" +#include "../mapping/CMapHeader.h" +#include "../mapping/CMapService.h" +#include "../modding/CModHandler.h" +#include "../modding/ModScope.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void CampaignHandler::readCampaign(Campaign * ret, const std::vector & input, std::string filename, std::string modName, std::string encoding) +{ + if (input.front() < uint8_t(' ')) // binary format + { + CMemoryStream stream(input.data(), input.size()); + CBinaryReader reader(&stream); + + readHeaderFromMemory(*ret, reader, filename, modName, encoding); + + for(int g = 0; g < ret->numberOfScenarios; ++g) + { + auto scenarioID = static_cast(ret->scenarios.size()); + ret->scenarios[scenarioID] = readScenarioFromMemory(reader, *ret); + } + } + else // text format (json) + { + JsonNode jsonCampaign((const char*)input.data(), input.size()); + readHeaderFromJson(*ret, jsonCampaign, filename, modName, encoding); + + for(auto & scenario : jsonCampaign["scenarios"].Vector()) + { + auto scenarioID = static_cast(ret->scenarios.size()); + ret->scenarios[scenarioID] = readScenarioFromJson(scenario); + } + } +} + +std::unique_ptr CampaignHandler::getHeader( const std::string & name) +{ + ResourcePath resourceID(name, EResType::CAMPAIGN); + std::string modName = VLC->modh->findResourceOrigin(resourceID); + std::string language = VLC->modh->getModLanguage(modName); + std::string encoding = Languages::getLanguageOptions(language).encoding; + + auto ret = std::make_unique(); + auto fileStream = CResourceHandler::get(modName)->load(resourceID); + std::vector cmpgn = getFile(std::move(fileStream), true)[0]; + + readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding); + + return ret; +} + +std::shared_ptr CampaignHandler::getCampaign( const std::string & name ) +{ + ResourcePath resourceID(name, EResType::CAMPAIGN); + std::string modName = VLC->modh->findResourceOrigin(resourceID); + std::string language = VLC->modh->getModLanguage(modName); + std::string encoding = Languages::getLanguageOptions(language).encoding; + + auto ret = std::make_unique(); + + auto fileStream = CResourceHandler::get(modName)->load(resourceID); + + std::vector> files = getFile(std::move(fileStream), false); + + readCampaign(ret.get(), files[0], resourceID.getName(), modName, encoding); + + //first entry is campaign header. start loop from 1 + for(int scenarioIndex = 0, fileIndex = 1; fileIndex < files.size() && scenarioIndex < ret->numberOfScenarios; scenarioIndex++) + { + auto scenarioID = static_cast(scenarioIndex); + + if(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios + continue; + + std::string scenarioName = resourceID.getName(); + boost::to_lower(scenarioName); + scenarioName += ':' + std::to_string(fileIndex - 1); + + //set map piece appropriately, convert vector to string + ret->mapPieces[scenarioID] = files[fileIndex]; + + auto hdr = ret->getMapHeader(scenarioID); + ret->scenarios[scenarioID].scenarioName = hdr->name; + fileIndex++; + } + + return ret; +} + +static std::string convertMapName(std::string input) +{ + boost::algorithm::to_lower(input); + boost::algorithm::trim(input); + + size_t slashPos = input.find_last_of("/"); + + if (slashPos != std::string::npos) + return input.substr(slashPos + 1); + + return input; +} + +std::string CampaignHandler::readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier) +{ + TextIdentifier stringID( "campaign", convertMapName(filename), identifier); + + std::string input = TextOperations::toUnicode(reader.readBaseString(), encoding); + + if (input.empty()) + return ""; + + VLC->generaltexth->registerString(modName, stringID, input); + return stringID.get(); +} + +void CampaignHandler::readHeaderFromJson(CampaignHeader & ret, JsonNode & reader, std::string filename, std::string modName, std::string encoding) +{ + ret.version = static_cast(reader["version"].Integer()); + if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX) + { + logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, static_cast(ret.version)); + return; + } + + ret.version = CampaignVersion::VCMI; + ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]); + ret.numberOfScenarios = reader["scenarios"].Vector().size(); + ret.name.appendTextID(reader["name"].String()); + ret.description.appendTextID(reader["description"].String()); + ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool(); + ret.music = AudioPath::fromJson(reader["music"]); + ret.filename = filename; + ret.modName = modName; + ret.encoding = encoding; +} + +CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader) +{ + auto prologEpilogReader = [](JsonNode & identifier) -> CampaignScenarioPrologEpilog + { + CampaignScenarioPrologEpilog ret; + ret.hasPrologEpilog = !identifier.isNull(); + if(ret.hasPrologEpilog) + { + ret.prologVideo = VideoPath::fromJson(identifier["video"]); + ret.prologMusic = AudioPath::fromJson(identifier["music"]); + ret.prologVoice = AudioPath::fromJson(identifier["voice"]); + ret.prologText.jsonDeserialize(identifier["text"]); + } + return ret; + }; + + CampaignScenario ret; + ret.mapName = reader["map"].String(); + for(auto & g : reader["preconditions"].Vector()) + ret.preconditionRegions.insert(static_cast(g.Integer())); + + ret.regionColor = reader["color"].Integer(); + ret.difficulty = reader["difficulty"].Integer(); + ret.regionText.jsonDeserialize(reader["regionText"]); + ret.prolog = prologEpilogReader(reader["prolog"]); + ret.epilog = prologEpilogReader(reader["epilog"]); + + ret.travelOptions = readScenarioTravelFromJson(reader); + + return ret; +} + +CampaignTravel CampaignHandler::readScenarioTravelFromJson(JsonNode & reader) +{ + CampaignTravel ret; + + std::map startOptionsMap = { + {"none", CampaignStartOptions::NONE}, + {"bonus", CampaignStartOptions::START_BONUS}, + {"crossover", CampaignStartOptions::HERO_CROSSOVER}, + {"hero", CampaignStartOptions::HERO_OPTIONS} + }; + + std::map bonusTypeMap = { + {"spell", CampaignBonusType::SPELL}, + {"creature", CampaignBonusType::MONSTER}, + {"building", CampaignBonusType::BUILDING}, + {"artifact", CampaignBonusType::ARTIFACT}, + {"scroll", CampaignBonusType::SPELL_SCROLL}, + {"primarySkill", CampaignBonusType::PRIMARY_SKILL}, + {"secondarySkill", CampaignBonusType::SECONDARY_SKILL}, + {"resource", CampaignBonusType::RESOURCE}, + //{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO}, + //{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO}, + }; + + std::map primarySkillsMap = { + {"attack", 0}, + {"defence", 8}, + {"spellpower", 16}, + {"knowledge", 24}, + }; + + std::map heroSpecialMap = { + {"strongest", 0xFFFD}, + {"generated", 0xFFFE}, + {"random", 0xFFFF} + }; + + std::map resourceTypeMap = { + //FD - wood+ore + //FE - mercury+sulfur+crystal+gem + {"wood", 0}, + {"mercury", 1}, + {"ore", 2}, + {"sulfur", 3}, + {"crystal", 4}, + {"gems", 5}, + {"gold", 6}, + {"common", 0xFD}, + {"rare", 0xFE} + }; + + for(auto & k : reader["heroKeeps"].Vector()) + { + if(k.String() == "experience") ret.whatHeroKeeps.experience = true; + if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true; + if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true; + if(k.String() == "spells") ret.whatHeroKeeps.spells = true; + if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true; + } + + for(auto & k : reader["keepCreatures"].Vector()) + { + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "creature", k.String())) + ret.monstersKeptByHero.insert(CreatureID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String()); + } + for(auto & k : reader["keepArtifacts"].Vector()) + { + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "artifact", k.String())) + ret.artifactsKeptByHero.insert(ArtifactID(identifier.value())); + else + logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String()); + } + + ret.startOptions = startOptionsMap[reader["startOptions"].String()]; + switch(ret.startOptions) + { + case CampaignStartOptions::NONE: + //no bonuses. Seems to be OK + break; + case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose + { + ret.playerColor = PlayerColor(PlayerColor::decode(reader["playerColor"].String())); + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = bonusTypeMap[bjson["what"].String()]; + switch (bonus.type) + { + case CampaignBonusType::RESOURCE: + bonus.info1 = resourceTypeMap[bjson["type"].String()]; + bonus.info2 = bjson["amount"].Integer(); + break; + + case CampaignBonusType::BUILDING: + bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String()); + if(bonus.info1 == -1) + logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String()); + break; + + default: + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info1 = heroId; + else + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) + bonus.info1 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + bonus.info3 = bjson["amount"].Integer(); + + switch(bonus.type) + { + case CampaignBonusType::SPELL: + case CampaignBonusType::MONSTER: + case CampaignBonusType::SECONDARY_SKILL: + case CampaignBonusType::ARTIFACT: + if(auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), bjson["what"].String(), bjson["type"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String()); + break; + + case CampaignBonusType::SPELL_SCROLL: + if(auto Identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "spell", bjson["type"].String())) + bonus.info2 = Identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String()); + break; + + case CampaignBonusType::PRIMARY_SKILL: + for(auto & ps : primarySkillsMap) + bonus.info2 |= bjson[ps.first].Integer() << ps.second; + break; + + default: + bonus.info2 = bjson["type"].Integer(); + } + break; + } + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + bonus.info2 = bjson["scenario"].Integer(); //from what scenario + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_OPTIONS: //heroes player can choose between + { + for(auto & bjson : reader["bonuses"].Vector()) + { + CampaignBonus bonus; + bonus.type = CampaignBonusType::HERO; + bonus.info1 = bjson["playerColor"].Integer(); //player color + + if(int heroId = heroSpecialMap[bjson["hero"].String()]) + bonus.info2 = heroId; + else + if (auto identifier = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", bjson["hero"].String())) + bonus.info2 = identifier.value(); + else + logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String()); + + ret.bonusesToChoose.push_back(bonus); + } + break; + } + default: + { + logGlobal->warn("VCMP Loading: Unsupported start options value"); + break; + } + } + + return ret; +} + + +void CampaignHandler::readHeaderFromMemory( CampaignHeader & ret, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding ) +{ + ret.version = static_cast(reader.readUInt32()); + ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19] + ret.loadLegacyData(campId); + ret.name.appendTextID(readLocalizedString(reader, filename, modName, encoding, "name")); + ret.description.appendTextID(readLocalizedString(reader, filename, modName, encoding, "description")); + if (ret.version > CampaignVersion::RoE) + ret.difficultyChoosenByPlayer = reader.readInt8(); + else + ret.difficultyChoosenByPlayer = false; + + ret.music = prologMusicName(reader.readInt8()); + ret.filename = filename; + ret.modName = modName; + ret.encoding = encoding; +} + +CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CampaignHeader & header) +{ + auto prologEpilogReader = [&](const std::string & identifier) -> CampaignScenarioPrologEpilog + { + CampaignScenarioPrologEpilog ret; + ret.hasPrologEpilog = reader.readUInt8(); + if(ret.hasPrologEpilog) + { + bool isOriginalCampaign = boost::starts_with(header.getFilename(), "DATA/"); + + ui8 index = reader.readUInt8(); + ret.prologVideo = CampaignHandler::prologVideoName(index); + ret.prologMusic = CampaignHandler::prologMusicName(reader.readUInt8()); + ret.prologVoice = isOriginalCampaign ? CampaignHandler::prologVoiceName(index) : AudioPath(); + ret.prologText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier)); + } + return ret; + }; + + CampaignScenario ret; + ret.mapName = reader.readBaseString(); + reader.readUInt32(); //packedMapSize - not used + if(header.numberOfScenarios > 8) //unholy alliance + { + ret.loadPreconditionRegions(reader.readUInt16()); + } + else + { + ret.loadPreconditionRegions(reader.readUInt8()); + } + ret.regionColor = reader.readUInt8(); + ret.difficulty = reader.readUInt8(); + ret.regionText.appendTextID(readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region")); + ret.prolog = prologEpilogReader(ret.mapName + ".prolog"); + ret.epilog = prologEpilogReader(ret.mapName + ".epilog"); + + ret.travelOptions = readScenarioTravelFromMemory(reader, header.version); + + return ret; +} + +template +static void readContainer(std::set & container, CBinaryReader & reader, int sizeBytes) +{ + for(int iId = 0, byte = 0; iId < sizeBytes * 8; ++iId) + { + if(iId % 8 == 0) + byte = reader.readUInt8(); + if(byte & (1 << iId % 8)) + container.insert(Identifier(iId)); + } +} + +CampaignTravel CampaignHandler::readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version ) +{ + CampaignTravel ret; + + ui8 whatHeroKeeps = reader.readUInt8(); + ret.whatHeroKeeps.experience = whatHeroKeeps & 1; + ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2; + ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4; + ret.whatHeroKeeps.spells = whatHeroKeeps & 8; + ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16; + + readContainer(ret.monstersKeptByHero, reader, 19); + readContainer(ret.artifactsKeptByHero, reader, version < CampaignVersion::SoD ? 17 : 18); + + ret.startOptions = static_cast(reader.readUInt8()); + + switch(ret.startOptions) + { + case CampaignStartOptions::NONE: + //no bonuses. Seems to be OK + break; + case CampaignStartOptions::START_BONUS: //reading of bonuses player can choose + { + ret.playerColor.setNum(reader.readUInt8()); + ui8 numOfBonuses = reader.readUInt8(); + for (int g=0; g(reader.readUInt8()); + //hero: FFFD means 'most powerful' and FFFE means 'generated' + switch(bonus.type) + { + case CampaignBonusType::SPELL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //spell ID + break; + } + case CampaignBonusType::MONSTER: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt16(); //monster type + bonus.info3 = reader.readUInt16(); //monster count + break; + } + case CampaignBonusType::BUILDING: + { + bonus.info1 = reader.readUInt8(); //building ID (0 - town hall, 1 - city hall, 2 - capitol, etc) + break; + } + case CampaignBonusType::ARTIFACT: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt16(); //artifact ID + break; + } + case CampaignBonusType::SPELL_SCROLL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //spell ID + break; + } + case CampaignBonusType::PRIMARY_SKILL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt32(); //bonuses (4 bytes for 4 skills) + break; + } + case CampaignBonusType::SECONDARY_SKILL: + { + bonus.info1 = reader.readUInt16(); //hero + bonus.info2 = reader.readUInt8(); //skill ID + bonus.info3 = reader.readUInt8(); //skill level + break; + } + case CampaignBonusType::RESOURCE: + { + bonus.info1 = reader.readUInt8(); //type + //FD - wood+ore + //FE - mercury+sulfur+crystal+gem + bonus.info2 = reader.readUInt32(); //count + break; + } + default: + logGlobal->warn("Corrupted h3c file"); + break; + } + ret.bonusesToChoose.push_back(bonus); + } + break; + } + case CampaignStartOptions::HERO_CROSSOVER: //reading of players (colors / scenarios ?) player can choose + { + ui8 numOfBonuses = reader.readUInt8(); + for (int g=0; gwarn("Corrupted h3c file"); + break; + } + } + + return ret; +} + +std::vector< std::vector > CampaignHandler::getFile(std::unique_ptr file, bool headerOnly) +{ + CCompressedStream stream(std::move(file), true); + + std::vector< std::vector > ret; + do + { + std::vector block(stream.getSize()); + stream.read(block.data(), block.size()); + ret.push_back(block); + ret.back().shrink_to_fit(); + } + while (!headerOnly && stream.getNextBlock()); + + return ret; +} + +VideoPath CampaignHandler::prologVideoName(ui8 index) +{ + JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); + auto vids = config["videos"].Vector(); + if(index < vids.size()) + return VideoPath::fromJson(vids[index]); + return VideoPath(); +} + +AudioPath CampaignHandler::prologMusicName(ui8 index) +{ + std::vector music; + return AudioPath::builtinTODO(VLC->generaltexth->translate("core.cmpmusic." + std::to_string(static_cast(index)))); +} + +AudioPath CampaignHandler::prologVoiceName(ui8 index) +{ + JsonNode config(JsonPath::builtin("CONFIG/campaignMedia")); + auto audio = config["voice"].Vector(); + if(index < audio.size()) + return AudioPath::fromJson(audio[index]); + return AudioPath(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/campaign/CampaignHandler.h b/lib/campaign/CampaignHandler.h index d7d0c4a8a..d6e2d0579 100644 --- a/lib/campaign/CampaignHandler.h +++ b/lib/campaign/CampaignHandler.h @@ -1,46 +1,46 @@ -/* - * CampaignHandler.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 "CampaignState.h" // Convenience include - not required for build, but required for any user of CampaignHandler -#include "../filesystem/ResourcePath.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE CampaignHandler -{ - static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); - - static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); - - //parsers for VCMI campaigns (*.vcmp) - static void readHeaderFromJson(CampaignHeader & target, JsonNode & reader, std::string filename, std::string modName, std::string encoding); - static CampaignScenario readScenarioFromJson(JsonNode & reader); - static CampaignTravel readScenarioTravelFromJson(JsonNode & reader); - - //parsers for original H3C campaigns - static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); - static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header); - static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version); - /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) - /// headerOnly - only header will be decompressed, returned vector wont have any maps - static std::vector> getFile(std::unique_ptr file, bool headerOnly); - - static VideoPath prologVideoName(ui8 index); - static AudioPath prologMusicName(ui8 index); - static AudioPath prologVoiceName(ui8 index); - -public: - static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file - - static std::shared_ptr getCampaign(const std::string & name); //name - name of appropriate file -}; - -VCMI_LIB_NAMESPACE_END +/* + * CampaignHandler.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 "CampaignState.h" // Convenience include - not required for build, but required for any user of CampaignHandler +#include "../filesystem/ResourcePath.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CampaignHandler +{ + static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier); + + static void readCampaign(Campaign * target, const std::vector & stream, std::string filename, std::string modName, std::string encoding); + + //parsers for VCMI campaigns (*.vcmp) + static void readHeaderFromJson(CampaignHeader & target, JsonNode & reader, std::string filename, std::string modName, std::string encoding); + static CampaignScenario readScenarioFromJson(JsonNode & reader); + static CampaignTravel readScenarioTravelFromJson(JsonNode & reader); + + //parsers for original H3C campaigns + static void readHeaderFromMemory(CampaignHeader & target, CBinaryReader & reader, std::string filename, std::string modName, std::string encoding); + static CampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CampaignHeader & header); + static CampaignTravel readScenarioTravelFromMemory(CBinaryReader & reader, CampaignVersion version); + /// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m) + /// headerOnly - only header will be decompressed, returned vector wont have any maps + static std::vector> getFile(std::unique_ptr file, bool headerOnly); + + static VideoPath prologVideoName(ui8 index); + static AudioPath prologMusicName(ui8 index); + static AudioPath prologVoiceName(ui8 index); + +public: + static std::unique_ptr getHeader( const std::string & name); //name - name of appropriate file + + static std::shared_ptr getCampaign(const std::string & name); //name - name of appropriate file +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index eaa790c9b..881f8a277 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -1,326 +1,326 @@ -/* - * EntityIdentifiers.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" - -#ifndef VCMI_NO_EXTRA_VERSION -#include "../Version.h" -#endif -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "modding/IdentifierStorage.h" -#include "modding/ModScope.h" -#include "VCMI_Lib.h" -#include "CArtHandler.h"//todo: remove -#include "CCreatureHandler.h"//todo: remove -#include "spells/CSpellHandler.h" //todo: remove -#include "CSkillHandler.h"//todo: remove -#include "constants/StringConstants.h" -#include "CGeneralTextHandler.h" -#include "TerrainHandler.h" //TODO: remove -#include "BattleFieldHandler.h" -#include "ObstacleHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const BattleID BattleID::NONE(-1); -const QueryID QueryID::NONE(-1); -const QueryID QueryID::CLIENT(-2); -const HeroTypeID HeroTypeID::NONE(-1); -const HeroTypeID HeroTypeID::RANDOM(-2); -const ObjectInstanceID ObjectInstanceID::NONE(-1); - -const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER(-2); -const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER(-3); -const SlotID SlotID::WAR_MACHINES_SLOT(-4); -const SlotID SlotID::ARROW_TOWERS_SLOT(-5); - -const PlayerColor PlayerColor::SPECTATOR(-4); -const PlayerColor PlayerColor::CANNOT_DETERMINE(-3); -const PlayerColor PlayerColor::UNFLAGGABLE(-2); -const PlayerColor PlayerColor::NEUTRAL(-1); -const PlayerColor PlayerColor::PLAYER_LIMIT(PLAYER_LIMIT_I); -const TeamID TeamID::NO_TEAM(-1); - -const SpellSchool SpellSchool::ANY(-1); -const SpellSchool SpellSchool::AIR(0); -const SpellSchool SpellSchool::FIRE(1); -const SpellSchool SpellSchool::WATER(2); -const SpellSchool SpellSchool::EARTH(3); - -const FactionID FactionID::NONE(-2); -const FactionID FactionID::DEFAULT(-1); -const FactionID FactionID::RANDOM(-1); -const FactionID FactionID::ANY(-1); -const FactionID FactionID::CASTLE(0); -const FactionID FactionID::RAMPART(1); -const FactionID FactionID::TOWER(2); -const FactionID FactionID::INFERNO(3); -const FactionID FactionID::NECROPOLIS(4); -const FactionID FactionID::DUNGEON(5); -const FactionID FactionID::STRONGHOLD(6); -const FactionID FactionID::FORTRESS(7); -const FactionID FactionID::CONFLUX(8); -const FactionID FactionID::NEUTRAL(9); - -const BoatId BoatId::NONE(-1); -const BoatId BoatId::NECROPOLIS(0); -const BoatId BoatId::CASTLE(1); -const BoatId BoatId::FORTRESS(2); - -const RiverId RiverId::NO_RIVER(0); -const RiverId RiverId::WATER_RIVER(1); -const RiverId RiverId::ICY_RIVER(2); -const RiverId RiverId::MUD_RIVER(3); -const RiverId RiverId::LAVA_RIVER(4); - -const RoadId RoadId::NO_ROAD(0); -const RoadId RoadId::DIRT_ROAD(1); -const RoadId RoadId::GRAVEL_ROAD(2); -const RoadId RoadId::COBBLESTONE_ROAD(3); - -namespace GameConstants -{ -#ifdef VCMI_NO_EXTRA_VERSION - const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING; -#else - const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING "." + std::string{GIT_SHA1}; -#endif -} - -si32 HeroClassID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string HeroClassID::encode(const si32 index) -{ - return VLC->heroClasses()->getByIndex(index)->getJsonKey(); -} - -std::string HeroClassID::entityType() -{ - return "heroClass"; -} - -si32 HeroTypeID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string HeroTypeID::encode(const si32 index) -{ - return VLC->heroTypes()->getByIndex(index)->getJsonKey(); -} - -std::string HeroTypeID::entityType() -{ - return "hero"; -} - -const CArtifact * ArtifactIDBase::toArtifact() const -{ - return dynamic_cast(toArtifact(VLC->artifacts())); -} - -const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const -{ - return service->getByIndex(num); -} - -si32 ArtifactID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string ArtifactID::encode(const si32 index) -{ - return VLC->artifacts()->getByIndex(index)->getJsonKey(); -} - -std::string ArtifactID::entityType() -{ - return "artifact"; -} - -const CCreature * CreatureIDBase::toCreature() const -{ - return VLC->creh->objects.at(num); -} - -const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const -{ - return creatures->getByIndex(num); -} - -si32 CreatureID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string CreatureID::encode(const si32 index) -{ - return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); -} - -std::string CreatureID::entityType() -{ - return "creature"; -} - -const CSpell * SpellIDBase::toSpell() const -{ - if(num < 0 || num >= VLC->spellh->objects.size()) - { - logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); - return nullptr; - } - return VLC->spellh->objects[num]; -} - -const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const -{ - return service->getByIndex(num); -} - -si32 SpellID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string SpellID::encode(const si32 index) -{ - return VLC->spells()->getByIndex(index)->getJsonKey(); -} - -std::string SpellID::entityType() -{ - return "spell"; -} - -bool PlayerColor::isValidPlayer() const -{ - return num >= 0 && num < PLAYER_LIMIT_I; -} - -bool PlayerColor::isSpectator() const -{ - return num == SPECTATOR.num; -} - -std::string PlayerColor::toString() const -{ - if (num == -1) - return "neutral"; - return encode(num); -} - -si32 PlayerColor::decode(const std::string & identifier) -{ - return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); -} - -std::string PlayerColor::encode(const si32 index) -{ - if (index < 0 || index >= std::size(GameConstants::PLAYER_COLOR_NAMES)) - { - assert(0); - return "invalid"; - } - - return GameConstants::PLAYER_COLOR_NAMES[index]; -} - -std::string PlayerColor::entityType() -{ - return "playerColor"; -} - -si32 FactionID::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return FactionID::DEFAULT; -} - -std::string FactionID::encode(const si32 index) -{ - return VLC->factions()->getByIndex(index)->getJsonKey(); -} - -std::string FactionID::entityType() -{ - return "faction"; -} - -si32 TerrainId::decode(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return static_cast(TerrainId::NONE); -} - -std::string TerrainId::encode(const si32 index) -{ - return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); -} - -std::string TerrainId::entityType() -{ - return "terrain"; -} - -const BattleField BattleField::NONE; - -const BattleFieldInfo * BattleField::getInfo() const -{ - return VLC->battlefields()->getById(*this); -} - -const ObstacleInfo * Obstacle::getInfo() const -{ - return VLC->obstacles()->getById(*this); -} - -VCMI_LIB_NAMESPACE_END +/* + * EntityIdentifiers.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" + +#ifndef VCMI_NO_EXTRA_VERSION +#include "../Version.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "modding/IdentifierStorage.h" +#include "modding/ModScope.h" +#include "VCMI_Lib.h" +#include "CArtHandler.h"//todo: remove +#include "CCreatureHandler.h"//todo: remove +#include "spells/CSpellHandler.h" //todo: remove +#include "CSkillHandler.h"//todo: remove +#include "constants/StringConstants.h" +#include "CGeneralTextHandler.h" +#include "TerrainHandler.h" //TODO: remove +#include "BattleFieldHandler.h" +#include "ObstacleHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const BattleID BattleID::NONE(-1); +const QueryID QueryID::NONE(-1); +const QueryID QueryID::CLIENT(-2); +const HeroTypeID HeroTypeID::NONE(-1); +const HeroTypeID HeroTypeID::RANDOM(-2); +const ObjectInstanceID ObjectInstanceID::NONE(-1); + +const SlotID SlotID::COMMANDER_SLOT_PLACEHOLDER(-2); +const SlotID SlotID::SUMMONED_SLOT_PLACEHOLDER(-3); +const SlotID SlotID::WAR_MACHINES_SLOT(-4); +const SlotID SlotID::ARROW_TOWERS_SLOT(-5); + +const PlayerColor PlayerColor::SPECTATOR(-4); +const PlayerColor PlayerColor::CANNOT_DETERMINE(-3); +const PlayerColor PlayerColor::UNFLAGGABLE(-2); +const PlayerColor PlayerColor::NEUTRAL(-1); +const PlayerColor PlayerColor::PLAYER_LIMIT(PLAYER_LIMIT_I); +const TeamID TeamID::NO_TEAM(-1); + +const SpellSchool SpellSchool::ANY(-1); +const SpellSchool SpellSchool::AIR(0); +const SpellSchool SpellSchool::FIRE(1); +const SpellSchool SpellSchool::WATER(2); +const SpellSchool SpellSchool::EARTH(3); + +const FactionID FactionID::NONE(-2); +const FactionID FactionID::DEFAULT(-1); +const FactionID FactionID::RANDOM(-1); +const FactionID FactionID::ANY(-1); +const FactionID FactionID::CASTLE(0); +const FactionID FactionID::RAMPART(1); +const FactionID FactionID::TOWER(2); +const FactionID FactionID::INFERNO(3); +const FactionID FactionID::NECROPOLIS(4); +const FactionID FactionID::DUNGEON(5); +const FactionID FactionID::STRONGHOLD(6); +const FactionID FactionID::FORTRESS(7); +const FactionID FactionID::CONFLUX(8); +const FactionID FactionID::NEUTRAL(9); + +const BoatId BoatId::NONE(-1); +const BoatId BoatId::NECROPOLIS(0); +const BoatId BoatId::CASTLE(1); +const BoatId BoatId::FORTRESS(2); + +const RiverId RiverId::NO_RIVER(0); +const RiverId RiverId::WATER_RIVER(1); +const RiverId RiverId::ICY_RIVER(2); +const RiverId RiverId::MUD_RIVER(3); +const RiverId RiverId::LAVA_RIVER(4); + +const RoadId RoadId::NO_ROAD(0); +const RoadId RoadId::DIRT_ROAD(1); +const RoadId RoadId::GRAVEL_ROAD(2); +const RoadId RoadId::COBBLESTONE_ROAD(3); + +namespace GameConstants +{ +#ifdef VCMI_NO_EXTRA_VERSION + const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING; +#else + const std::string VCMI_VERSION = "VCMI " VCMI_VERSION_STRING "." + std::string{GIT_SHA1}; +#endif +} + +si32 HeroClassID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string HeroClassID::encode(const si32 index) +{ + return VLC->heroClasses()->getByIndex(index)->getJsonKey(); +} + +std::string HeroClassID::entityType() +{ + return "heroClass"; +} + +si32 HeroTypeID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string HeroTypeID::encode(const si32 index) +{ + return VLC->heroTypes()->getByIndex(index)->getJsonKey(); +} + +std::string HeroTypeID::entityType() +{ + return "hero"; +} + +const CArtifact * ArtifactIDBase::toArtifact() const +{ + return dynamic_cast(toArtifact(VLC->artifacts())); +} + +const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const +{ + return service->getByIndex(num); +} + +si32 ArtifactID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string ArtifactID::encode(const si32 index) +{ + return VLC->artifacts()->getByIndex(index)->getJsonKey(); +} + +std::string ArtifactID::entityType() +{ + return "artifact"; +} + +const CCreature * CreatureIDBase::toCreature() const +{ + return VLC->creh->objects.at(num); +} + +const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const +{ + return creatures->getByIndex(num); +} + +si32 CreatureID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string CreatureID::encode(const si32 index) +{ + return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); +} + +std::string CreatureID::entityType() +{ + return "creature"; +} + +const CSpell * SpellIDBase::toSpell() const +{ + if(num < 0 || num >= VLC->spellh->objects.size()) + { + logGlobal->error("Unable to get spell of invalid ID %d", static_cast(num)); + return nullptr; + } + return VLC->spellh->objects[num]; +} + +const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const +{ + return service->getByIndex(num); +} + +si32 SpellID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string SpellID::encode(const si32 index) +{ + return VLC->spells()->getByIndex(index)->getJsonKey(); +} + +std::string SpellID::entityType() +{ + return "spell"; +} + +bool PlayerColor::isValidPlayer() const +{ + return num >= 0 && num < PLAYER_LIMIT_I; +} + +bool PlayerColor::isSpectator() const +{ + return num == SPECTATOR.num; +} + +std::string PlayerColor::toString() const +{ + if (num == -1) + return "neutral"; + return encode(num); +} + +si32 PlayerColor::decode(const std::string & identifier) +{ + return vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, identifier); +} + +std::string PlayerColor::encode(const si32 index) +{ + if (index < 0 || index >= std::size(GameConstants::PLAYER_COLOR_NAMES)) + { + assert(0); + return "invalid"; + } + + return GameConstants::PLAYER_COLOR_NAMES[index]; +} + +std::string PlayerColor::entityType() +{ + return "playerColor"; +} + +si32 FactionID::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + if(rawId) + return rawId.value(); + else + return FactionID::DEFAULT; +} + +std::string FactionID::encode(const si32 index) +{ + return VLC->factions()->getByIndex(index)->getJsonKey(); +} + +std::string FactionID::entityType() +{ + return "faction"; +} + +si32 TerrainId::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + if(rawId) + return rawId.value(); + else + return static_cast(TerrainId::NONE); +} + +std::string TerrainId::encode(const si32 index) +{ + return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string TerrainId::entityType() +{ + return "terrain"; +} + +const BattleField BattleField::NONE; + +const BattleFieldInfo * BattleField::getInfo() const +{ + return VLC->battlefields()->getById(*this); +} + +const ObstacleInfo * Obstacle::getInfo() const +{ + return VLC->obstacles()->getById(*this); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 7ca9a1333..7ff0f39cf 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -1,959 +1,959 @@ -/* - * EntityIdentifiers.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 "NumericConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class Artifact; -class ArtifactService; -class Creature; -class CreatureService; - -namespace spells -{ - class Spell; - class Service; -} - -class CArtifact; -class CArtifactInstance; -class CCreature; -class CHero; -class CSpell; -class CSkill; -class CGameInfoCallback; -class CNonConstInfoCallback; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class IdentifierBase -{ -protected: - constexpr IdentifierBase(): - num(-1) - {} - - explicit constexpr IdentifierBase(int32_t value): - num(value) - {} - - ~IdentifierBase() = default; -public: - int32_t num; - - constexpr int32_t getNum() const - { - return num; - } - - constexpr void setNum(int32_t value) - { - num = value; - } - - struct hash - { - size_t operator()(const IdentifierBase & id) const - { - return std::hash()(id.num); - } - }; - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr operator int32_t () const - { - return num; - } - - friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) - { - return os << dt.num; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons -template -class Identifier : public IdentifierBase -{ - using BaseClass = IdentifierBase; -public: - constexpr Identifier() - {} - - explicit constexpr Identifier(int32_t value): - IdentifierBase(value) - {} - - constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass & operator--() - { - --BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator--(int) - { - FinalClass ret(num); - --BaseClass::num; - return ret; - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(num); - ++BaseClass::num; - return ret; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template -class IdentifierWithEnum : public BaseClass -{ - using EnumType = typename BaseClass::Type; - - static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); -public: - constexpr EnumType toEnum() const - { - return static_cast(BaseClass::num); - } - - constexpr IdentifierWithEnum(const EnumType & enumValue) - { - BaseClass::num = static_cast(enumValue); - } - - constexpr IdentifierWithEnum(int32_t _num = -1) - { - BaseClass::num = _num; - } - - constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } - constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } - constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } - constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } - constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } - constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } - - constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(BaseClass::num); - ++BaseClass::num; - return ret; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class ArtifactInstanceID : public Identifier -{ -public: - using Identifier::Identifier; -}; - -class QueryID : public Identifier -{ -public: - using Identifier::Identifier; - DLL_LINKAGE static const QueryID NONE; - DLL_LINKAGE static const QueryID CLIENT; -}; - -class BattleID : public Identifier -{ -public: - using Identifier::Identifier; - DLL_LINKAGE static const BattleID NONE; -}; -class ObjectInstanceID : public Identifier -{ -public: - using Identifier::Identifier; - DLL_LINKAGE static const ObjectInstanceID NONE; -}; - -class HeroClassID : public Identifier -{ -public: - using Identifier::Identifier; - ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); -}; - -class HeroTypeID : public Identifier -{ -public: - using Identifier::Identifier; - ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); - - DLL_LINKAGE static const HeroTypeID NONE; - DLL_LINKAGE static const HeroTypeID RANDOM; - - bool isValid() const - { - return getNum() >= 0; - } -}; - -class SlotID : public Identifier -{ -public: - using Identifier::Identifier; - - DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; - DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; - } -}; - -class DLL_LINKAGE PlayerColor : public Identifier -{ -public: - using Identifier::Identifier; - - enum EPlayerColor - { - PLAYER_LIMIT_I = 8, - }; - - static const PlayerColor SPECTATOR; //252 - static const PlayerColor CANNOT_DETERMINE; //253 - static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) - static const PlayerColor NEUTRAL; //255 - static const PlayerColor PLAYER_LIMIT; //player limit per map - - bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) - bool isSpectator() const; - - std::string toString() const; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); - static std::string entityType(); -}; - -class TeamID : public Identifier -{ -public: - using Identifier::Identifier; - - DLL_LINKAGE static const TeamID NO_TEAM; -}; - -class TeleportChannelID : public Identifier -{ -public: - using Identifier::Identifier; -}; - -class SecondarySkillBase : public IdentifierBase -{ -public: - enum Type : int32_t - { - WRONG = -2, - DEFAULT = -1, - PATHFINDING = 0, - ARCHERY, - LOGISTICS, - SCOUTING, - DIPLOMACY, - NAVIGATION, - LEADERSHIP, - WISDOM, - MYSTICISM, - LUCK, - BALLISTICS, - EAGLE_EYE, - NECROMANCY, - ESTATES, - FIRE_MAGIC, - AIR_MAGIC, - WATER_MAGIC, - EARTH_MAGIC, - SCHOLAR, - TACTICS, - ARTILLERY, - LEARNING, - OFFENCE, - ARMORER, - INTELLIGENCE, - SORCERY, - RESISTANCE, - FIRST_AID, - SKILL_SIZE - }; - static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); -}; - -class SecondarySkill : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -class DLL_LINKAGE FactionID : public Identifier -{ -public: - using Identifier::Identifier; - - static const FactionID NONE; - static const FactionID DEFAULT; - static const FactionID RANDOM; - static const FactionID ANY; - static const FactionID CASTLE; - static const FactionID RAMPART; - static const FactionID TOWER; - static const FactionID INFERNO; - static const FactionID NECROPOLIS; - static const FactionID DUNGEON; - static const FactionID STRONGHOLD; - static const FactionID FORTRESS; - static const FactionID CONFLUX; - static const FactionID NEUTRAL; - - static si32 decode(const std::string& identifier); - static std::string encode(const si32 index); - static std::string entityType(); - - bool isValid() const - { - return getNum() >= 0; - } -}; - -class BuildingIDBase : public IdentifierBase -{ -public: - //Quite useful as long as most of building mechanics hardcoded - // NOTE: all building with completely configurable mechanics will be removed from list - enum Type - { - DEFAULT = -50, - HORDE_PLACEHOLDER7 = -36, - HORDE_PLACEHOLDER6 = -35, - HORDE_PLACEHOLDER5 = -34, - HORDE_PLACEHOLDER4 = -33, - HORDE_PLACEHOLDER3 = -32, - HORDE_PLACEHOLDER2 = -31, - HORDE_PLACEHOLDER1 = -30, - NONE = -1, - FIRST_REGULAR_ID = 0, - MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, - TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, - VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, - RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, - SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, - HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, - DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, - DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, - DWELL_LVL_6_UP, DWELL_UP_LAST=43, - - DWELL_LVL_1 = DWELL_FIRST, - DWELL_LVL_7 = DWELL_LAST, - DWELL_LVL_1_UP = DWELL_UP_FIRST, - DWELL_LVL_7_UP = DWELL_UP_LAST, - - DWELL_UP2_FIRST = DWELL_LVL_7_UP + 1, - -// //Special buildings for towns. - CASTLE_GATE = SPECIAL_3, //Inferno - FREELANCERS_GUILD = SPECIAL_2, //Stronghold - ARTIFACT_MERCHANT = SPECIAL_1, - - }; - - bool IsSpecialOrGrail() const - { - return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; - } -}; - -class BuildingID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -class ObjBase : public IdentifierBase -{ -public: - enum Type - { - NO_OBJ = -1, - ALTAR_OF_SACRIFICE [[deprecated]] = 2, - ANCHOR_POINT = 3, - ARENA = 4, - ARTIFACT = 5, - PANDORAS_BOX = 6, - BLACK_MARKET [[deprecated]] = 7, - BOAT = 8, - BORDERGUARD = 9, - KEYMASTER = 10, - BUOY = 11, - CAMPFIRE = 12, - CARTOGRAPHER = 13, - SWAN_POND = 14, - COVER_OF_DARKNESS = 15, - CREATURE_BANK = 16, - CREATURE_GENERATOR1 = 17, - CREATURE_GENERATOR2 = 18, - CREATURE_GENERATOR3 = 19, - CREATURE_GENERATOR4 = 20, - CURSED_GROUND1 = 21, - CORPSE = 22, - MARLETTO_TOWER = 23, - DERELICT_SHIP = 24, - DRAGON_UTOPIA = 25, - EVENT = 26, - EYE_OF_MAGI = 27, - FAERIE_RING = 28, - FLOTSAM = 29, - FOUNTAIN_OF_FORTUNE = 30, - FOUNTAIN_OF_YOUTH = 31, - GARDEN_OF_REVELATION = 32, - GARRISON = 33, - HERO = 34, - HILL_FORT = 35, - GRAIL = 36, - HUT_OF_MAGI = 37, - IDOL_OF_FORTUNE = 38, - LEAN_TO = 39, - LIBRARY_OF_ENLIGHTENMENT = 41, - LIGHTHOUSE = 42, - MONOLITH_ONE_WAY_ENTRANCE = 43, - MONOLITH_ONE_WAY_EXIT = 44, - MONOLITH_TWO_WAY = 45, - MAGIC_PLAINS1 = 46, - SCHOOL_OF_MAGIC = 47, - MAGIC_SPRING = 48, - MAGIC_WELL = 49, - MARKET_OF_TIME = 50, - MERCENARY_CAMP = 51, - MERMAID = 52, - MINE = 53, - MONSTER = 54, - MYSTICAL_GARDEN = 55, - OASIS = 56, - OBELISK = 57, - REDWOOD_OBSERVATORY = 58, - OCEAN_BOTTLE = 59, - PILLAR_OF_FIRE = 60, - STAR_AXIS = 61, - PRISON = 62, - PYRAMID = 63,//subtype 0 - WOG_OBJECT = 63,//subtype > 0 - RALLY_FLAG = 64, - RANDOM_ART = 65, - RANDOM_TREASURE_ART = 66, - RANDOM_MINOR_ART = 67, - RANDOM_MAJOR_ART = 68, - RANDOM_RELIC_ART = 69, - RANDOM_HERO = 70, - RANDOM_MONSTER = 71, - RANDOM_MONSTER_L1 = 72, - RANDOM_MONSTER_L2 = 73, - RANDOM_MONSTER_L3 = 74, - RANDOM_MONSTER_L4 = 75, - RANDOM_RESOURCE = 76, - RANDOM_TOWN = 77, - REFUGEE_CAMP = 78, - RESOURCE = 79, - SANCTUARY = 80, - SCHOLAR = 81, - SEA_CHEST = 82, - SEER_HUT = 83, - CRYPT = 84, - SHIPWRECK = 85, - SHIPWRECK_SURVIVOR = 86, - SHIPYARD = 87, - SHRINE_OF_MAGIC_INCANTATION = 88, - SHRINE_OF_MAGIC_GESTURE = 89, - SHRINE_OF_MAGIC_THOUGHT = 90, - SIGN = 91, - SIRENS = 92, - SPELL_SCROLL = 93, - STABLES = 94, - TAVERN = 95, - TEMPLE = 96, - DEN_OF_THIEVES = 97, - TOWN = 98, - TRADING_POST [[deprecated]] = 99, - LEARNING_STONE = 100, - TREASURE_CHEST = 101, - TREE_OF_KNOWLEDGE = 102, - SUBTERRANEAN_GATE = 103, - UNIVERSITY [[deprecated]] = 104, - WAGON = 105, - WAR_MACHINE_FACTORY = 106, - SCHOOL_OF_WAR = 107, - WARRIORS_TOMB = 108, - WATER_WHEEL = 109, - WATERING_HOLE = 110, - WHIRLPOOL = 111, - WINDMILL = 112, - WITCH_HUT = 113, - HOLE = 124, - RANDOM_MONSTER_L5 = 162, - RANDOM_MONSTER_L6 = 163, - RANDOM_MONSTER_L7 = 164, - BORDER_GATE = 212, - FREELANCERS_GUILD [[deprecated]] = 213, - HERO_PLACEHOLDER = 214, - QUEST_GUARD = 215, - RANDOM_DWELLING = 216, - RANDOM_DWELLING_LVL = 217, //subtype = creature level - RANDOM_DWELLING_FACTION = 218, //subtype = faction - GARRISON2 = 219, - ABANDONED_MINE = 220, - TRADING_POST_SNOW [[deprecated]] = 221, - CLOVER_FIELD = 222, - CURSED_GROUND2 = 223, - EVIL_FOG = 224, - FAVORABLE_WINDS = 225, - FIERY_FIELDS = 226, - HOLY_GROUNDS = 227, - LUCID_POOLS = 228, - MAGIC_CLOUDS = 229, - MAGIC_PLAINS2 = 230, - ROCKLANDS = 231, - }; -}; - -class Obj : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -class DLL_LINKAGE RoadId : public Identifier -{ -public: - using Identifier::Identifier; - - static const RoadId NO_ROAD; - static const RoadId DIRT_ROAD; - static const RoadId GRAVEL_ROAD; - static const RoadId COBBLESTONE_ROAD; -}; - -class DLL_LINKAGE RiverId : public Identifier -{ -public: - using Identifier::Identifier; - - static const RiverId NO_RIVER; - static const RiverId WATER_RIVER; - static const RiverId ICY_RIVER; - static const RiverId MUD_RIVER; - static const RiverId LAVA_RIVER; -}; - -class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase -{ -public: - enum Type : int32_t - { - LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO - }; -}; - -class EPathfindingLayer : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -class ArtifactPositionBase : public IdentifierBase -{ -public: - enum Type - { - TRANSITION_POS = -3, - FIRST_AVAILABLE = -2, - PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack - HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 - RIGHT_RING, LEFT_RING, FEET, //8 - MISC1, MISC2, MISC3, MISC4, //12 - MACH1, MACH2, MACH3, MACH4, //16 - SPELLBOOK, MISC5, //18 - AFTER_LAST, - //cres - CREATURE_SLOT = 0, - COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST, - - BACKPACK_START = 19 - }; - - static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); - - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); -}; - -class ArtifactPosition : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -class ArtifactIDBase : public IdentifierBase -{ -public: - enum Type - { - NONE = -1, - SPELLBOOK = 0, - SPELL_SCROLL = 1, - GRAIL = 2, - CATAPULT = 3, - BALLISTA = 4, - AMMO_CART = 5, - FIRST_AID_TENT = 6, - VIAL_OF_DRAGON_BLOOD = 127, - ARMAGEDDONS_BLADE = 128, - TITANS_THUNDER = 135, - ART_SELECTION = 144, - ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 - }; - - DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; -}; - -class ArtifactID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; - - ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); -}; - -class CreatureIDBase : public IdentifierBase -{ -public: - enum Type - { - NONE = -1, - ARCHER = 2, // for debug / fallback - IMP = 42, // for Deity of Fire - SKELETON = 56, // for Skeleton Transformer - BONE_DRAGON = 68, // for Skeleton Transformer - TROGLODYTES = 70, // for Abandoned Mine - MEDUSA = 76, // for Siege UI workaround - HYDRA = 110, // for Skeleton Transformer - CHAOS_HYDRA = 111, // for Skeleton Transformer - AIR_ELEMENTAL = 112, // for tests - FIRE_ELEMENTAL = 114, // for tests - PSYCHIC_ELEMENTAL = 120, // for hardcoded ability - MAGIC_ELEMENTAL = 121, // for hardcoded ability - CATAPULT = 145, - BALLISTA = 146, - FIRST_AID_TENT = 147, - AMMO_CART = 148, - ARROW_TOWERS = 149 - }; - - DLL_LINKAGE const CCreature * toCreature() const; - DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; -}; - -class CreatureID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; - - ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); -}; - -class SpellIDBase : public IdentifierBase -{ -public: - enum Type - { - // Special ID's - SPELLBOOK_PRESET = -3, - PRESET = -2, - NONE = -1, - - // Adventure map spells - SUMMON_BOAT = 0, - SCUTTLE_BOAT = 1, - VISIONS = 2, - VIEW_EARTH = 3, - DISGUISE = 4, - VIEW_AIR = 5, - FLY = 6, - WATER_WALK = 7, - DIMENSION_DOOR = 8, - TOWN_PORTAL = 9, - - // Combar spells - QUICKSAND = 10, - LAND_MINE = 11, - FORCE_FIELD = 12, - FIRE_WALL = 13, - EARTHQUAKE = 14, - MAGIC_ARROW = 15, - ICE_BOLT = 16, - LIGHTNING_BOLT = 17, - IMPLOSION = 18, - CHAIN_LIGHTNING = 19, - FROST_RING = 20, - FIREBALL = 21, - INFERNO = 22, - METEOR_SHOWER = 23, - DEATH_RIPPLE = 24, - DESTROY_UNDEAD = 25, - ARMAGEDDON = 26, - SHIELD = 27, - AIR_SHIELD = 28, - FIRE_SHIELD = 29, - PROTECTION_FROM_AIR = 30, - PROTECTION_FROM_FIRE = 31, - PROTECTION_FROM_WATER = 32, - PROTECTION_FROM_EARTH = 33, - ANTI_MAGIC = 34, - DISPEL = 35, - MAGIC_MIRROR = 36, - CURE = 37, - RESURRECTION = 38, - ANIMATE_DEAD = 39, - SACRIFICE = 40, - BLESS = 41, - CURSE = 42, - BLOODLUST = 43, - PRECISION = 44, - WEAKNESS = 45, - STONE_SKIN = 46, - DISRUPTING_RAY = 47, - PRAYER = 48, - MIRTH = 49, - SORROW = 50, - FORTUNE = 51, - MISFORTUNE = 52, - HASTE = 53, - SLOW = 54, - SLAYER = 55, - FRENZY = 56, - TITANS_LIGHTNING_BOLT = 57, - COUNTERSTRIKE = 58, - BERSERK = 59, - HYPNOTIZE = 60, - FORGETFULNESS = 61, - BLIND = 62, - TELEPORT = 63, - REMOVE_OBSTACLE = 64, - CLONE = 65, - SUMMON_FIRE_ELEMENTAL = 66, - SUMMON_EARTH_ELEMENTAL = 67, - SUMMON_WATER_ELEMENTAL = 68, - SUMMON_AIR_ELEMENTAL = 69, - - // Creature abilities - STONE_GAZE = 70, - POISON = 71, - BIND = 72, - DISEASE = 73, - PARALYZE = 74, - AGE = 75, - DEATH_CLOUD = 76, - THUNDERBOLT = 77, - DISPEL_HELPFUL_SPELLS = 78, - DEATH_STARE = 79, - ACID_BREATH_DEFENSE = 80, - ACID_BREATH_DAMAGE = 81, - - // Special ID's - FIRST_NON_SPELL = 70, - AFTER_LAST = 82 - }; - - DLL_LINKAGE const CSpell * toSpell() const; //deprecated - DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; -}; - -class SpellID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; - - ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); -}; - -class BattleFieldInfo; -class BattleField : public Identifier -{ -public: - using Identifier::Identifier; - - DLL_LINKAGE static const BattleField NONE; - DLL_LINKAGE const BattleFieldInfo * getInfo() const; -}; - -class DLL_LINKAGE BoatId : public Identifier -{ -public: - using Identifier::Identifier; - - static const BoatId NONE; - static const BoatId NECROPOLIS; - static const BoatId CASTLE; - static const BoatId FORTRESS; -}; - -class TerrainIdBase : public IdentifierBase -{ -public: - enum Type : int32_t - { - NATIVE_TERRAIN = -4, - ANY_TERRAIN = -3, - NONE = -1, - FIRST_REGULAR_TERRAIN = 0, - DIRT = 0, - SAND, - GRASS, - SNOW, - SWAMP, - ROUGH, - SUBTERRANEAN, - LAVA, - WATER, - ROCK, - ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK - }; -}; - -class TerrainId : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; - - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); -}; - -class ObstacleInfo; -class Obstacle : public Identifier -{ -public: - using Identifier::Identifier; - DLL_LINKAGE const ObstacleInfo * getInfo() const; -}; - -class DLL_LINKAGE SpellSchool : public Identifier -{ -public: - using Identifier::Identifier; - - static const SpellSchool ANY; - static const SpellSchool AIR; - static const SpellSchool FIRE; - static const SpellSchool WATER; - static const SpellSchool EARTH; -}; - -class GameResIDBase : public IdentifierBase -{ -public: - enum Type : int32_t - { - WOOD = 0, - MERCURY, - ORE, - SULFUR, - CRYSTAL, - GEMS, - GOLD, - MITHRIL, - COUNT, - - WOOD_AND_ORE = 127, // special case for town bonus resource - INVALID = -1 - }; -}; - -class GameResID : public IdentifierWithEnum -{ -public: - using IdentifierWithEnum::IdentifierWithEnum; -}; - -// Deprecated -// TODO: remove -using ESpellSchool = SpellSchool; -using ETownType = FactionID; -using EGameResID = GameResID; -using River = RiverId; -using Road = RoadId; -using ETerrainId = TerrainId; - -VCMI_LIB_NAMESPACE_END +/* + * EntityIdentifiers.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 "NumericConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class Artifact; +class ArtifactService; +class Creature; +class CreatureService; + +namespace spells +{ + class Spell; + class Service; +} + +class CArtifact; +class CArtifactInstance; +class CCreature; +class CHero; +class CSpell; +class CSkill; +class CGameInfoCallback; +class CNonConstInfoCallback; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class IdentifierBase +{ +protected: + constexpr IdentifierBase(): + num(-1) + {} + + explicit constexpr IdentifierBase(int32_t value): + num(value) + {} + + ~IdentifierBase() = default; +public: + int32_t num; + + constexpr int32_t getNum() const + { + return num; + } + + constexpr void setNum(int32_t value) + { + num = value; + } + + struct hash + { + size_t operator()(const IdentifierBase & id) const + { + return std::hash()(id.num); + } + }; + + template void serialize(Handler &h, const int version) + { + h & num; + } + + constexpr void advance(int change) + { + num += change; + } + + constexpr operator int32_t () const + { + return num; + } + + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) + { + return os << dt.num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons +template +class Identifier : public IdentifierBase +{ + using BaseClass = IdentifierBase; +public: + constexpr Identifier() + {} + + explicit constexpr Identifier(int32_t value): + IdentifierBase(value) + {} + + constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass & operator--() + { + --BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator--(int) + { + FinalClass ret(num); + --BaseClass::num; + return ret; + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class IdentifierWithEnum : public BaseClass +{ + using EnumType = typename BaseClass::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr EnumType toEnum() const + { + return static_cast(BaseClass::num); + } + + constexpr IdentifierWithEnum(const EnumType & enumValue) + { + BaseClass::num = static_cast(enumValue); + } + + constexpr IdentifierWithEnum(int32_t _num = -1) + { + BaseClass::num = _num; + } + + constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(BaseClass::num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +class ArtifactInstanceID : public Identifier +{ +public: + using Identifier::Identifier; +}; + +class QueryID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const QueryID NONE; + DLL_LINKAGE static const QueryID CLIENT; +}; + +class BattleID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const BattleID NONE; +}; +class ObjectInstanceID : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE static const ObjectInstanceID NONE; +}; + +class HeroClassID : public Identifier +{ +public: + using Identifier::Identifier; + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class HeroTypeID : public Identifier +{ +public: + using Identifier::Identifier; + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); + + DLL_LINKAGE static const HeroTypeID NONE; + DLL_LINKAGE static const HeroTypeID RANDOM; + + bool isValid() const + { + return getNum() >= 0; + } +}; + +class SlotID : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; + DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; ///= 0 && getNum() < GameConstants::ARMY_SIZE; + } +}; + +class DLL_LINKAGE PlayerColor : public Identifier +{ +public: + using Identifier::Identifier; + + enum EPlayerColor + { + PLAYER_LIMIT_I = 8, + }; + + static const PlayerColor SPECTATOR; //252 + static const PlayerColor CANNOT_DETERMINE; //253 + static const PlayerColor UNFLAGGABLE; //254 - neutral objects (pandora, banks) + static const PlayerColor NEUTRAL; //255 + static const PlayerColor PLAYER_LIMIT; //player limit per map + + bool isValidPlayer() const; //valid means < PLAYER_LIMIT (especially non-neutral) + bool isSpectator() const; + + std::string toString() const; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + +class TeamID : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const TeamID NO_TEAM; +}; + +class TeleportChannelID : public Identifier +{ +public: + using Identifier::Identifier; +}; + +class SecondarySkillBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + WRONG = -2, + DEFAULT = -1, + PATHFINDING = 0, + ARCHERY, + LOGISTICS, + SCOUTING, + DIPLOMACY, + NAVIGATION, + LEADERSHIP, + WISDOM, + MYSTICISM, + LUCK, + BALLISTICS, + EAGLE_EYE, + NECROMANCY, + ESTATES, + FIRE_MAGIC, + AIR_MAGIC, + WATER_MAGIC, + EARTH_MAGIC, + SCHOLAR, + TACTICS, + ARTILLERY, + LEARNING, + OFFENCE, + ARMORER, + INTELLIGENCE, + SORCERY, + RESISTANCE, + FIRST_AID, + SKILL_SIZE + }; + static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); +}; + +class SecondarySkill : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class DLL_LINKAGE FactionID : public Identifier +{ +public: + using Identifier::Identifier; + + static const FactionID NONE; + static const FactionID DEFAULT; + static const FactionID RANDOM; + static const FactionID ANY; + static const FactionID CASTLE; + static const FactionID RAMPART; + static const FactionID TOWER; + static const FactionID INFERNO; + static const FactionID NECROPOLIS; + static const FactionID DUNGEON; + static const FactionID STRONGHOLD; + static const FactionID FORTRESS; + static const FactionID CONFLUX; + static const FactionID NEUTRAL; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); + + bool isValid() const + { + return getNum() >= 0; + } +}; + +class BuildingIDBase : public IdentifierBase +{ +public: + //Quite useful as long as most of building mechanics hardcoded + // NOTE: all building with completely configurable mechanics will be removed from list + enum Type + { + DEFAULT = -50, + HORDE_PLACEHOLDER7 = -36, + HORDE_PLACEHOLDER6 = -35, + HORDE_PLACEHOLDER5 = -34, + HORDE_PLACEHOLDER4 = -33, + HORDE_PLACEHOLDER3 = -32, + HORDE_PLACEHOLDER2 = -31, + HORDE_PLACEHOLDER1 = -30, + NONE = -1, + FIRST_REGULAR_ID = 0, + MAGES_GUILD_1 = 0, MAGES_GUILD_2, MAGES_GUILD_3, MAGES_GUILD_4, MAGES_GUILD_5, + TAVERN, SHIPYARD, FORT, CITADEL, CASTLE, + VILLAGE_HALL, TOWN_HALL, CITY_HALL, CAPITOL, MARKETPLACE, + RESOURCE_SILO, BLACKSMITH, SPECIAL_1, HORDE_1, HORDE_1_UPGR, + SHIP, SPECIAL_2, SPECIAL_3, SPECIAL_4, HORDE_2, + HORDE_2_UPGR, GRAIL, EXTRA_TOWN_HALL, EXTRA_CITY_HALL, EXTRA_CAPITOL, + DWELL_FIRST=30, DWELL_LVL_2, DWELL_LVL_3, DWELL_LVL_4, DWELL_LVL_5, DWELL_LVL_6, DWELL_LAST=36, + DWELL_UP_FIRST=37, DWELL_LVL_2_UP, DWELL_LVL_3_UP, DWELL_LVL_4_UP, DWELL_LVL_5_UP, + DWELL_LVL_6_UP, DWELL_UP_LAST=43, + + DWELL_LVL_1 = DWELL_FIRST, + DWELL_LVL_7 = DWELL_LAST, + DWELL_LVL_1_UP = DWELL_UP_FIRST, + DWELL_LVL_7_UP = DWELL_UP_LAST, + + DWELL_UP2_FIRST = DWELL_LVL_7_UP + 1, + +// //Special buildings for towns. + CASTLE_GATE = SPECIAL_3, //Inferno + FREELANCERS_GUILD = SPECIAL_2, //Stronghold + ARTIFACT_MERCHANT = SPECIAL_1, + + }; + + bool IsSpecialOrGrail() const + { + return num == SPECIAL_1 || num == SPECIAL_2 || num == SPECIAL_3 || num == SPECIAL_4 || num == GRAIL; + } +}; + +class BuildingID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ObjBase : public IdentifierBase +{ +public: + enum Type + { + NO_OBJ = -1, + ALTAR_OF_SACRIFICE [[deprecated]] = 2, + ANCHOR_POINT = 3, + ARENA = 4, + ARTIFACT = 5, + PANDORAS_BOX = 6, + BLACK_MARKET [[deprecated]] = 7, + BOAT = 8, + BORDERGUARD = 9, + KEYMASTER = 10, + BUOY = 11, + CAMPFIRE = 12, + CARTOGRAPHER = 13, + SWAN_POND = 14, + COVER_OF_DARKNESS = 15, + CREATURE_BANK = 16, + CREATURE_GENERATOR1 = 17, + CREATURE_GENERATOR2 = 18, + CREATURE_GENERATOR3 = 19, + CREATURE_GENERATOR4 = 20, + CURSED_GROUND1 = 21, + CORPSE = 22, + MARLETTO_TOWER = 23, + DERELICT_SHIP = 24, + DRAGON_UTOPIA = 25, + EVENT = 26, + EYE_OF_MAGI = 27, + FAERIE_RING = 28, + FLOTSAM = 29, + FOUNTAIN_OF_FORTUNE = 30, + FOUNTAIN_OF_YOUTH = 31, + GARDEN_OF_REVELATION = 32, + GARRISON = 33, + HERO = 34, + HILL_FORT = 35, + GRAIL = 36, + HUT_OF_MAGI = 37, + IDOL_OF_FORTUNE = 38, + LEAN_TO = 39, + LIBRARY_OF_ENLIGHTENMENT = 41, + LIGHTHOUSE = 42, + MONOLITH_ONE_WAY_ENTRANCE = 43, + MONOLITH_ONE_WAY_EXIT = 44, + MONOLITH_TWO_WAY = 45, + MAGIC_PLAINS1 = 46, + SCHOOL_OF_MAGIC = 47, + MAGIC_SPRING = 48, + MAGIC_WELL = 49, + MARKET_OF_TIME = 50, + MERCENARY_CAMP = 51, + MERMAID = 52, + MINE = 53, + MONSTER = 54, + MYSTICAL_GARDEN = 55, + OASIS = 56, + OBELISK = 57, + REDWOOD_OBSERVATORY = 58, + OCEAN_BOTTLE = 59, + PILLAR_OF_FIRE = 60, + STAR_AXIS = 61, + PRISON = 62, + PYRAMID = 63,//subtype 0 + WOG_OBJECT = 63,//subtype > 0 + RALLY_FLAG = 64, + RANDOM_ART = 65, + RANDOM_TREASURE_ART = 66, + RANDOM_MINOR_ART = 67, + RANDOM_MAJOR_ART = 68, + RANDOM_RELIC_ART = 69, + RANDOM_HERO = 70, + RANDOM_MONSTER = 71, + RANDOM_MONSTER_L1 = 72, + RANDOM_MONSTER_L2 = 73, + RANDOM_MONSTER_L3 = 74, + RANDOM_MONSTER_L4 = 75, + RANDOM_RESOURCE = 76, + RANDOM_TOWN = 77, + REFUGEE_CAMP = 78, + RESOURCE = 79, + SANCTUARY = 80, + SCHOLAR = 81, + SEA_CHEST = 82, + SEER_HUT = 83, + CRYPT = 84, + SHIPWRECK = 85, + SHIPWRECK_SURVIVOR = 86, + SHIPYARD = 87, + SHRINE_OF_MAGIC_INCANTATION = 88, + SHRINE_OF_MAGIC_GESTURE = 89, + SHRINE_OF_MAGIC_THOUGHT = 90, + SIGN = 91, + SIRENS = 92, + SPELL_SCROLL = 93, + STABLES = 94, + TAVERN = 95, + TEMPLE = 96, + DEN_OF_THIEVES = 97, + TOWN = 98, + TRADING_POST [[deprecated]] = 99, + LEARNING_STONE = 100, + TREASURE_CHEST = 101, + TREE_OF_KNOWLEDGE = 102, + SUBTERRANEAN_GATE = 103, + UNIVERSITY [[deprecated]] = 104, + WAGON = 105, + WAR_MACHINE_FACTORY = 106, + SCHOOL_OF_WAR = 107, + WARRIORS_TOMB = 108, + WATER_WHEEL = 109, + WATERING_HOLE = 110, + WHIRLPOOL = 111, + WINDMILL = 112, + WITCH_HUT = 113, + HOLE = 124, + RANDOM_MONSTER_L5 = 162, + RANDOM_MONSTER_L6 = 163, + RANDOM_MONSTER_L7 = 164, + BORDER_GATE = 212, + FREELANCERS_GUILD [[deprecated]] = 213, + HERO_PLACEHOLDER = 214, + QUEST_GUARD = 215, + RANDOM_DWELLING = 216, + RANDOM_DWELLING_LVL = 217, //subtype = creature level + RANDOM_DWELLING_FACTION = 218, //subtype = faction + GARRISON2 = 219, + ABANDONED_MINE = 220, + TRADING_POST_SNOW [[deprecated]] = 221, + CLOVER_FIELD = 222, + CURSED_GROUND2 = 223, + EVIL_FOG = 224, + FAVORABLE_WINDS = 225, + FIERY_FIELDS = 226, + HOLY_GROUNDS = 227, + LUCID_POOLS = 228, + MAGIC_CLOUDS = 229, + MAGIC_PLAINS2 = 230, + ROCKLANDS = 231, + }; +}; + +class Obj : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class DLL_LINKAGE RoadId : public Identifier +{ +public: + using Identifier::Identifier; + + static const RoadId NO_ROAD; + static const RoadId DIRT_ROAD; + static const RoadId GRAVEL_ROAD; + static const RoadId COBBLESTONE_ROAD; +}; + +class DLL_LINKAGE RiverId : public Identifier +{ +public: + using Identifier::Identifier; + + static const RiverId NO_RIVER; + static const RiverId WATER_RIVER; + static const RiverId ICY_RIVER; + static const RiverId MUD_RIVER; + static const RiverId LAVA_RIVER; +}; + +class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + LAND = 0, SAIL = 1, WATER, AIR, NUM_LAYERS, WRONG, AUTO + }; +}; + +class EPathfindingLayer : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ArtifactPositionBase : public IdentifierBase +{ +public: + enum Type + { + TRANSITION_POS = -3, + FIRST_AVAILABLE = -2, + PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack + HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 + RIGHT_RING, LEFT_RING, FEET, //8 + MISC1, MISC2, MISC3, MISC4, //12 + MACH1, MACH2, MACH3, MACH4, //16 + SPELLBOOK, MISC5, //18 + AFTER_LAST, + //cres + CREATURE_SLOT = 0, + COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST, + + BACKPACK_START = 19 + }; + + static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); + + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); +}; + +class ArtifactPosition : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +class ArtifactIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + SPELLBOOK = 0, + SPELL_SCROLL = 1, + GRAIL = 2, + CATAPULT = 3, + BALLISTA = 4, + AMMO_CART = 5, + FIRST_AID_TENT = 6, + VIAL_OF_DRAGON_BLOOD = 127, + ARMAGEDDONS_BLADE = 128, + TITANS_THUNDER = 135, + ART_SELECTION = 144, + ART_LOCK = 145, // FIXME: We must get rid of this one since it's conflict with artifact from mods. See issue 2455 + }; + + DLL_LINKAGE const CArtifact * toArtifact() const; + DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; +}; + +class ArtifactID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class CreatureIDBase : public IdentifierBase +{ +public: + enum Type + { + NONE = -1, + ARCHER = 2, // for debug / fallback + IMP = 42, // for Deity of Fire + SKELETON = 56, // for Skeleton Transformer + BONE_DRAGON = 68, // for Skeleton Transformer + TROGLODYTES = 70, // for Abandoned Mine + MEDUSA = 76, // for Siege UI workaround + HYDRA = 110, // for Skeleton Transformer + CHAOS_HYDRA = 111, // for Skeleton Transformer + AIR_ELEMENTAL = 112, // for tests + FIRE_ELEMENTAL = 114, // for tests + PSYCHIC_ELEMENTAL = 120, // for hardcoded ability + MAGIC_ELEMENTAL = 121, // for hardcoded ability + CATAPULT = 145, + BALLISTA = 146, + FIRST_AID_TENT = 147, + AMMO_CART = 148, + ARROW_TOWERS = 149 + }; + + DLL_LINKAGE const CCreature * toCreature() const; + DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; +}; + +class CreatureID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class SpellIDBase : public IdentifierBase +{ +public: + enum Type + { + // Special ID's + SPELLBOOK_PRESET = -3, + PRESET = -2, + NONE = -1, + + // Adventure map spells + SUMMON_BOAT = 0, + SCUTTLE_BOAT = 1, + VISIONS = 2, + VIEW_EARTH = 3, + DISGUISE = 4, + VIEW_AIR = 5, + FLY = 6, + WATER_WALK = 7, + DIMENSION_DOOR = 8, + TOWN_PORTAL = 9, + + // Combar spells + QUICKSAND = 10, + LAND_MINE = 11, + FORCE_FIELD = 12, + FIRE_WALL = 13, + EARTHQUAKE = 14, + MAGIC_ARROW = 15, + ICE_BOLT = 16, + LIGHTNING_BOLT = 17, + IMPLOSION = 18, + CHAIN_LIGHTNING = 19, + FROST_RING = 20, + FIREBALL = 21, + INFERNO = 22, + METEOR_SHOWER = 23, + DEATH_RIPPLE = 24, + DESTROY_UNDEAD = 25, + ARMAGEDDON = 26, + SHIELD = 27, + AIR_SHIELD = 28, + FIRE_SHIELD = 29, + PROTECTION_FROM_AIR = 30, + PROTECTION_FROM_FIRE = 31, + PROTECTION_FROM_WATER = 32, + PROTECTION_FROM_EARTH = 33, + ANTI_MAGIC = 34, + DISPEL = 35, + MAGIC_MIRROR = 36, + CURE = 37, + RESURRECTION = 38, + ANIMATE_DEAD = 39, + SACRIFICE = 40, + BLESS = 41, + CURSE = 42, + BLOODLUST = 43, + PRECISION = 44, + WEAKNESS = 45, + STONE_SKIN = 46, + DISRUPTING_RAY = 47, + PRAYER = 48, + MIRTH = 49, + SORROW = 50, + FORTUNE = 51, + MISFORTUNE = 52, + HASTE = 53, + SLOW = 54, + SLAYER = 55, + FRENZY = 56, + TITANS_LIGHTNING_BOLT = 57, + COUNTERSTRIKE = 58, + BERSERK = 59, + HYPNOTIZE = 60, + FORGETFULNESS = 61, + BLIND = 62, + TELEPORT = 63, + REMOVE_OBSTACLE = 64, + CLONE = 65, + SUMMON_FIRE_ELEMENTAL = 66, + SUMMON_EARTH_ELEMENTAL = 67, + SUMMON_WATER_ELEMENTAL = 68, + SUMMON_AIR_ELEMENTAL = 69, + + // Creature abilities + STONE_GAZE = 70, + POISON = 71, + BIND = 72, + DISEASE = 73, + PARALYZE = 74, + AGE = 75, + DEATH_CLOUD = 76, + THUNDERBOLT = 77, + DISPEL_HELPFUL_SPELLS = 78, + DEATH_STARE = 79, + ACID_BREATH_DEFENSE = 80, + ACID_BREATH_DAMAGE = 81, + + // Special ID's + FIRST_NON_SPELL = 70, + AFTER_LAST = 82 + }; + + DLL_LINKAGE const CSpell * toSpell() const; //deprecated + DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; +}; + +class SpellID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + ///json serialization helpers + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class BattleFieldInfo; +class BattleField : public Identifier +{ +public: + using Identifier::Identifier; + + DLL_LINKAGE static const BattleField NONE; + DLL_LINKAGE const BattleFieldInfo * getInfo() const; +}; + +class DLL_LINKAGE BoatId : public Identifier +{ +public: + using Identifier::Identifier; + + static const BoatId NONE; + static const BoatId NECROPOLIS; + static const BoatId CASTLE; + static const BoatId FORTRESS; +}; + +class TerrainIdBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + NATIVE_TERRAIN = -4, + ANY_TERRAIN = -3, + NONE = -1, + FIRST_REGULAR_TERRAIN = 0, + DIRT = 0, + SAND, + GRASS, + SNOW, + SWAMP, + ROUGH, + SUBTERRANEAN, + LAVA, + WATER, + ROCK, + ORIGINAL_REGULAR_TERRAIN_COUNT = ROCK + }; +}; + +class TerrainId : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); +}; + +class ObstacleInfo; +class Obstacle : public Identifier +{ +public: + using Identifier::Identifier; + DLL_LINKAGE const ObstacleInfo * getInfo() const; +}; + +class DLL_LINKAGE SpellSchool : public Identifier +{ +public: + using Identifier::Identifier; + + static const SpellSchool ANY; + static const SpellSchool AIR; + static const SpellSchool FIRE; + static const SpellSchool WATER; + static const SpellSchool EARTH; +}; + +class GameResIDBase : public IdentifierBase +{ +public: + enum Type : int32_t + { + WOOD = 0, + MERCURY, + ORE, + SULFUR, + CRYSTAL, + GEMS, + GOLD, + MITHRIL, + COUNT, + + WOOD_AND_ORE = 127, // special case for town bonus resource + INVALID = -1 + }; +}; + +class GameResID : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; +}; + +// Deprecated +// TODO: remove +using ESpellSchool = SpellSchool; +using ETownType = FactionID; +using EGameResID = GameResID; +using River = RiverId; +using Road = RoadId; +using ETerrainId = TerrainId; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index 1c2cd7b06..34f51ecd4 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -1,253 +1,253 @@ -/* - * Enumerations.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 - -VCMI_LIB_NAMESPACE_BEGIN - -enum class PrimarySkill : int8_t -{ - NONE = -1, - ATTACK, - DEFENSE, - SPELL_POWER, - KNOWLEDGE, - EXPERIENCE = 4 //for some reason changePrimSkill uses it -}; - -enum class EAlignment : int8_t -{ - GOOD, - EVIL, - NEUTRAL -}; - -namespace BuildingSubID -{ - enum EBuildingSubID - { - DEFAULT = -50, - NONE = -1, - STABLES, - BROTHERHOOD_OF_SWORD, - CASTLE_GATE, - CREATURE_TRANSFORMER, - MYSTIC_POND, - FOUNTAIN_OF_FORTUNE, - ARTIFACT_MERCHANT, - LOOKOUT_TOWER, - LIBRARY, - MANA_VORTEX, - PORTAL_OF_SUMMONING, - ESCAPE_TUNNEL, - FREELANCERS_GUILD, - BALLISTA_YARD, - ATTACK_VISITING_BONUS, - MAGIC_UNIVERSITY, - SPELL_POWER_GARRISON_BONUS, - ATTACK_GARRISON_BONUS, - DEFENSE_GARRISON_BONUS, - DEFENSE_VISITING_BONUS, - SPELL_POWER_VISITING_BONUS, - KNOWLEDGE_VISITING_BONUS, - EXPERIENCE_VISITING_BONUS, - LIGHTHOUSE, - TREASURY, - CUSTOM_VISITING_BONUS, - CUSTOM_VISITING_REWARD - }; -} - -enum class EMarketMode : int8_t -{ - RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, - ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, - MARTKET_AFTER_LAST_PLACEHOLDER -}; - -enum class EAiTactic : int8_t -{ - NONE = -1, - RANDOM, - WARRIOR, - BUILDER, - EXPLORER -}; - -enum class EBuildingState : int8_t -{ - HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, - NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED -}; - -enum class ESpellCastProblem : int8_t -{ - OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, - HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, - SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, - NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, - MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all - INVALID -}; - -namespace ECommander -{ - enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; - const int MAX_SKILL_LEVEL = 5; -} - -enum class EWallPart : int8_t -{ - INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, - KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, - PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ -}; - -enum class EWallState : int8_t -{ - NONE = -1, //no wall - DESTROYED, - DAMAGED, - INTACT, - REINFORCED, // walls in towns with castle -}; - -enum class EGateState : int8_t -{ - NONE, - CLOSED, - BLOCKED, // gate is blocked in closed state, e.g. by creature - OPENED, - DESTROYED -}; - -enum class ETileType : int8_t -{ - FREE, - POSSIBLE, - BLOCKED, - USED -}; - -enum class ETeleportChannelType : int8_t -{ - IMPASSABLE, - BIDIRECTIONAL, - UNIDIRECTIONAL, - MIXED -}; - -namespace SecSkillLevel -{ - enum SecSkillLevel - { - NONE, - BASIC, - ADVANCED, - EXPERT, - LEVELS_SIZE - }; -} - -enum class Date : int8_t -{ - DAY = 0, - DAY_OF_WEEK = 1, - WEEK = 2, - MONTH = 3, - DAY_OF_MONTH -}; - -enum class EActionType : int8_t -{ - NO_ACTION, - - END_TACTIC_PHASE, - RETREAT, - SURRENDER, - - HERO_SPELL, - - WALK, - WAIT, - DEFEND, - WALK_AND_ATTACK, - SHOOT, - CATAPULT, - MONSTER_SPELL, - BAD_MORALE, - STACK_HEAL, -}; - -enum class EDiggingStatus : int8_t -{ - UNKNOWN = -1, - CAN_DIG = 0, - LACK_OF_MOVEMENT, - WRONG_TERRAIN, - TILE_OCCUPIED, - BACKPACK_IS_FULL -}; - -enum class EPlayerStatus : int8_t -{ - WRONG = -1, - INGAME, - LOSER, - WINNER -}; - -enum class PlayerRelations : int8_t -{ - ENEMIES, - ALLIES, - SAME_PLAYER -}; - -enum class EMetaclass : int8_t -{ - INVALID = 0, - ARTIFACT, - CREATURE, - FACTION, - EXPERIENCE, - HERO, - HEROCLASS, - LUCK, - MANA, - MORALE, - MOVEMENT, - OBJECT, - PRIMARY_SKILL, - SECONDARY_SKILL, - SPELL, - RESOURCE -}; - -enum class EHealLevel: int8_t -{ - HEAL, - RESURRECT, - OVERHEAL -}; - -enum class EHealPower : int8_t -{ - ONE_BATTLE, - PERMANENT -}; - -enum class EBattleResult : int8_t -{ - NORMAL = 0, - ESCAPE = 1, - SURRENDER = 2, -}; - -VCMI_LIB_NAMESPACE_END +/* + * Enumerations.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 + +VCMI_LIB_NAMESPACE_BEGIN + +enum class PrimarySkill : int8_t +{ + NONE = -1, + ATTACK, + DEFENSE, + SPELL_POWER, + KNOWLEDGE, + EXPERIENCE = 4 //for some reason changePrimSkill uses it +}; + +enum class EAlignment : int8_t +{ + GOOD, + EVIL, + NEUTRAL +}; + +namespace BuildingSubID +{ + enum EBuildingSubID + { + DEFAULT = -50, + NONE = -1, + STABLES, + BROTHERHOOD_OF_SWORD, + CASTLE_GATE, + CREATURE_TRANSFORMER, + MYSTIC_POND, + FOUNTAIN_OF_FORTUNE, + ARTIFACT_MERCHANT, + LOOKOUT_TOWER, + LIBRARY, + MANA_VORTEX, + PORTAL_OF_SUMMONING, + ESCAPE_TUNNEL, + FREELANCERS_GUILD, + BALLISTA_YARD, + ATTACK_VISITING_BONUS, + MAGIC_UNIVERSITY, + SPELL_POWER_GARRISON_BONUS, + ATTACK_GARRISON_BONUS, + DEFENSE_GARRISON_BONUS, + DEFENSE_VISITING_BONUS, + SPELL_POWER_VISITING_BONUS, + KNOWLEDGE_VISITING_BONUS, + EXPERIENCE_VISITING_BONUS, + LIGHTHOUSE, + TREASURY, + CUSTOM_VISITING_BONUS, + CUSTOM_VISITING_REWARD + }; +} + +enum class EMarketMode : int8_t +{ + RESOURCE_RESOURCE, RESOURCE_PLAYER, CREATURE_RESOURCE, RESOURCE_ARTIFACT, + ARTIFACT_RESOURCE, ARTIFACT_EXP, CREATURE_EXP, CREATURE_UNDEAD, RESOURCE_SKILL, + MARTKET_AFTER_LAST_PLACEHOLDER +}; + +enum class EAiTactic : int8_t +{ + NONE = -1, + RANDOM, + WARRIOR, + BUILDER, + EXPLORER +}; + +enum class EBuildingState : int8_t +{ + HAVE_CAPITAL, NO_WATER, FORBIDDEN, ADD_MAGES_GUILD, ALREADY_PRESENT, CANT_BUILD_TODAY, + NO_RESOURCES, ALLOWED, PREREQUIRES, MISSING_BASE, BUILDING_ERROR, TOWN_NOT_OWNED +}; + +enum class ESpellCastProblem : int8_t +{ + OK, NO_HERO_TO_CAST_SPELL, CASTS_PER_TURN_LIMIT, NO_SPELLBOOK, + HERO_DOESNT_KNOW_SPELL, NOT_ENOUGH_MANA, ADVMAP_SPELL_INSTEAD_OF_BATTLE_SPELL, + SPELL_LEVEL_LIMIT_EXCEEDED, NO_SPELLS_TO_DISPEL, + NO_APPROPRIATE_TARGET, STACK_IMMUNE_TO_SPELL, WRONG_SPELL_TARGET, ONGOING_TACTIC_PHASE, + MAGIC_IS_BLOCKED, //For Orb of Inhibition and similar - no casting at all + INVALID +}; + +namespace ECommander +{ + enum SecondarySkills {ATTACK, DEFENSE, HEALTH, DAMAGE, SPEED, SPELL_POWER, CASTS, RESISTANCE}; + const int MAX_SKILL_LEVEL = 5; +} + +enum class EWallPart : int8_t +{ + INDESTRUCTIBLE_PART_OF_GATE = -3, INDESTRUCTIBLE_PART = -2, INVALID = -1, + KEEP = 0, BOTTOM_TOWER, BOTTOM_WALL, BELOW_GATE, OVER_GATE, UPPER_WALL, UPPER_TOWER, GATE, + PARTS_COUNT /* This constant SHOULD always stay as the last item in the enum. */ +}; + +enum class EWallState : int8_t +{ + NONE = -1, //no wall + DESTROYED, + DAMAGED, + INTACT, + REINFORCED, // walls in towns with castle +}; + +enum class EGateState : int8_t +{ + NONE, + CLOSED, + BLOCKED, // gate is blocked in closed state, e.g. by creature + OPENED, + DESTROYED +}; + +enum class ETileType : int8_t +{ + FREE, + POSSIBLE, + BLOCKED, + USED +}; + +enum class ETeleportChannelType : int8_t +{ + IMPASSABLE, + BIDIRECTIONAL, + UNIDIRECTIONAL, + MIXED +}; + +namespace SecSkillLevel +{ + enum SecSkillLevel + { + NONE, + BASIC, + ADVANCED, + EXPERT, + LEVELS_SIZE + }; +} + +enum class Date : int8_t +{ + DAY = 0, + DAY_OF_WEEK = 1, + WEEK = 2, + MONTH = 3, + DAY_OF_MONTH +}; + +enum class EActionType : int8_t +{ + NO_ACTION, + + END_TACTIC_PHASE, + RETREAT, + SURRENDER, + + HERO_SPELL, + + WALK, + WAIT, + DEFEND, + WALK_AND_ATTACK, + SHOOT, + CATAPULT, + MONSTER_SPELL, + BAD_MORALE, + STACK_HEAL, +}; + +enum class EDiggingStatus : int8_t +{ + UNKNOWN = -1, + CAN_DIG = 0, + LACK_OF_MOVEMENT, + WRONG_TERRAIN, + TILE_OCCUPIED, + BACKPACK_IS_FULL +}; + +enum class EPlayerStatus : int8_t +{ + WRONG = -1, + INGAME, + LOSER, + WINNER +}; + +enum class PlayerRelations : int8_t +{ + ENEMIES, + ALLIES, + SAME_PLAYER +}; + +enum class EMetaclass : int8_t +{ + INVALID = 0, + ARTIFACT, + CREATURE, + FACTION, + EXPERIENCE, + HERO, + HEROCLASS, + LUCK, + MANA, + MORALE, + MOVEMENT, + OBJECT, + PRIMARY_SKILL, + SECONDARY_SKILL, + SPELL, + RESOURCE +}; + +enum class EHealLevel: int8_t +{ + HEAL, + RESURRECT, + OVERHEAL +}; + +enum class EHealPower : int8_t +{ + ONE_BATTLE, + PERMANENT +}; + +enum class EBattleResult : int8_t +{ + NORMAL = 0, + ESCAPE = 1, + SURRENDER = 2, +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index 0fd235c03..9d61935ff 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -1,55 +1,55 @@ -/* - * NumericConstants.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 - -VCMI_LIB_NAMESPACE_BEGIN - -namespace GameConstants -{ - DLL_LINKAGE extern const std::string VCMI_VERSION; - - constexpr int PUZZLE_MAP_PIECES = 48; - - constexpr int MAX_HEROES_PER_PLAYER = 8; - constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; - - constexpr int ALL_PLAYERS = 255; //bitfield - - constexpr int CREATURES_PER_TOWN = 7; //without upgrades - constexpr int SPELL_LEVELS = 5; - constexpr int SPELL_SCHOOL_LEVELS = 4; - constexpr int DEFAULT_SCHOOLS = 4; - constexpr int CRE_LEVELS = 10; // number of creature experience levels - - constexpr int HERO_GOLD_COST = 2500; - constexpr int SPELLBOOK_GOLD_COST = 500; - constexpr int SKILL_GOLD_COST = 2000; - constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty - constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit - constexpr int ARMY_SIZE = 7; - constexpr int SKILL_PER_HERO = 8; - constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order - - constexpr int SKILL_QUANTITY=28; - constexpr int PRIMARY_SKILLS=4; - constexpr int RESOURCE_QUANTITY=8; - constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type - - // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase - constexpr int F_NUMBER = 9; - constexpr int ARTIFACTS_QUANTITY=171; - constexpr int HEROES_QUANTITY=156; - constexpr int SPELLS_QUANTITY=70; - constexpr int CREATURES_COUNT = 197; - - constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement -} - -VCMI_LIB_NAMESPACE_END +/* + * NumericConstants.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 + +VCMI_LIB_NAMESPACE_BEGIN + +namespace GameConstants +{ + DLL_LINKAGE extern const std::string VCMI_VERSION; + + constexpr int PUZZLE_MAP_PIECES = 48; + + constexpr int MAX_HEROES_PER_PLAYER = 8; + constexpr int AVAILABLE_HEROES_PER_PLAYER = 2; + + constexpr int ALL_PLAYERS = 255; //bitfield + + constexpr int CREATURES_PER_TOWN = 7; //without upgrades + constexpr int SPELL_LEVELS = 5; + constexpr int SPELL_SCHOOL_LEVELS = 4; + constexpr int DEFAULT_SCHOOLS = 4; + constexpr int CRE_LEVELS = 10; // number of creature experience levels + + constexpr int HERO_GOLD_COST = 2500; + constexpr int SPELLBOOK_GOLD_COST = 500; + constexpr int SKILL_GOLD_COST = 2000; + constexpr int BATTLE_SHOOTING_PENALTY_DISTANCE = 10; //if the distance is > than this, then shooting stack has distance penalty + constexpr int BATTLE_SHOOTING_RANGE_DISTANCE = std::numeric_limits::max(); // used when shooting stack has no shooting range limit + constexpr int ARMY_SIZE = 7; + constexpr int SKILL_PER_HERO = 8; + constexpr ui32 HERO_HIGH_LEVEL = 10; // affects primary skill upgrade order + + constexpr int SKILL_QUANTITY=28; + constexpr int PRIMARY_SKILLS=4; + constexpr int RESOURCE_QUANTITY=8; + constexpr int HEROES_PER_TYPE=8; //amount of heroes of each type + + // amounts of OH3 objects. Can be changed by mods, should be used only during H3 loading phase + constexpr int F_NUMBER = 9; + constexpr int ARTIFACTS_QUANTITY=171; + constexpr int HEROES_QUANTITY=156; + constexpr int SPELLS_QUANTITY=70; + constexpr int CREATURES_COUNT = 197; + + constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 8ba75e2f9..1d7972099 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -1,235 +1,235 @@ -/* - * CGameState.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 "bonuses/CBonusSystemNode.h" -#include "IGameCallback.h" -#include "LoadProgress.h" -#include "ConstTransitivePtr.h" - -namespace boost -{ -class shared_mutex; -} - -VCMI_LIB_NAMESPACE_BEGIN - -class EVictoryLossCheckResult; -class Services; -class IMapService; -class CMap; -struct CPack; -class CHeroClass; -struct EventCondition; -struct CampaignTravel; -class CStackInstance; -class CGameStateCampaign; -class TavernHeroesPool; -struct SThievesGuildInfo; - -template class CApplier; -class CBaseForGSApply; - -struct DLL_LINKAGE RumorState -{ - enum ERumorType : ui8 - { - TYPE_NONE = 0, TYPE_RAND, TYPE_SPECIAL, TYPE_MAP - }; - - enum ERumorTypeSpecial : ui8 - { - RUMOR_OBELISKS = 208, - RUMOR_ARTIFACTS = 209, - RUMOR_ARMY = 210, - RUMOR_INCOME = 211, - RUMOR_GRAIL = 212 - }; - - ERumorType type; - std::map> last; - - RumorState(){type = TYPE_NONE;}; - bool update(int id, int extra); - - template void serialize(Handler &h, const int version) - { - h & type; - h & last; - } -}; - -struct UpgradeInfo -{ - CreatureID oldID; //creature to be upgraded - std::vector newID; //possible upgrades - std::vector cost; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) - UpgradeInfo(){oldID = CreatureID::NONE;}; -}; - -class BattleInfo; - -DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult); - -class DLL_LINKAGE CGameState : public CNonConstInfoCallback -{ - friend class CGameStateCampaign; - -public: - /// List of currently ongoing battles - std::vector> currentBattles; - /// ID that can be allocated to next battle - BattleID nextBattleID = BattleID(0); - - //we have here all heroes available on this map that are not hired - std::unique_ptr heroesPool; - - /// list of players currently making turn. Usually - just one, except for simturns - std::set actingPlayers; - - CGameState(); - virtual ~CGameState(); - - void preInit(Services * services); - - void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = false); - void updateOnLoad(StartInfo * si); - - ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) - ui32 day; //total number of days in game - ConstTransitivePtr map; - std::map players; - std::map teams; - CBonusSystemNode globalEffects; - RumorState rumor; - - static boost::shared_mutex mutex; - - void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; - - bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); - - void apply(CPack *pack); - BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); - - void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; - PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; - bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile - void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists - void calculatePaths(const std::shared_ptr & config) override; - int3 guardingCreaturePosition (int3 pos) const override; - std::vector guardingCreatures (int3 pos) const; - void updateRumor(); - - /// Returns battle in which selected player is engaged, or nullptr if none. - /// Can NOT be used with neutral player, use battle by ID instead - const BattleInfo * getBattle(const PlayerColor & player) const; - /// Returns battle by its unique identifier, or nullptr if not found - const BattleInfo * getBattle(const BattleID & battle) const; - BattleInfo * getBattle(const BattleID & battle); - - // ----- victory, loss condition checks ----- - - EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const; - bool checkForVictory(const PlayerColor & player, const EventCondition & condition) const; //checks if given player is winner - PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner - bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game - - void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild - - bool isVisible(int3 pos, const std::optional & player) const override; - bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; - - int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month - - // ----- getters, setters ----- - - /// This RNG should only be used inside GS or CPackForClient-derived applyGs - /// If this doesn't work for your code that mean you need a new netpack - /// - /// Client-side must use CRandomGenerator::getDefault which is not serialized - /// - /// CGameHandler have it's own getter for CRandomGenerator::getDefault - /// Any server-side code outside of GH must use CRandomGenerator::getDefault - CRandomGenerator & getRandomGenerator(); - - template void serialize(Handler &h, const int version) - { - h & scenarioOps; - h & initialOpts; - h & actingPlayers; - h & day; - h & map; - h & players; - h & teams; - h & heroesPool; - h & globalEffects; - h & rand; - h & rumor; - h & campaign; - - BONUS_TREE_DESERIALIZATION_FIX - } - -private: - // ----- initialization ----- - void preInitAuto(); - void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); - void checkMapChecksum(); - void initGlobalBonuses(); - void initGrailPosition(); - void initRandomFactionsForPlayers(); - void randomizeMapObjects(); - void randomizeObject(CGObjectInstance *cur); - void initPlayerStates(); - void placeStartingHeroes(); - void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); - void removeHeroPlaceholders(); - void initDifficulty(); - void initHeroes(); - void placeHeroesInTowns(); - void initFogOfWar(); - void initStartingBonus(); - void initTowns(); - void initMapObjects(); - void initVisitingAndGarrisonedHeroes(); - void initCampaign(); - - // ----- bonus system handling ----- - - void buildBonusSystemTree(); - void attachArmedObjects(); - void buildGlobalTeamPlayerTree(); - void deserializationFix(); - - // ---- misc helpers ----- - - CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; - bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons - std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; - std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns - HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly - HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly - UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; - - // ---- data ----- - std::shared_ptr> applier; - CRandomGenerator rand; - Services * services; - - /// Ponter to campaign state manager. Nullptr for single scenarios - std::unique_ptr campaign; - - friend class IGameCallback; - friend class CMapHandler; - friend class CGameHandler; -}; - -VCMI_LIB_NAMESPACE_END +/* + * CGameState.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 "bonuses/CBonusSystemNode.h" +#include "IGameCallback.h" +#include "LoadProgress.h" +#include "ConstTransitivePtr.h" + +namespace boost +{ +class shared_mutex; +} + +VCMI_LIB_NAMESPACE_BEGIN + +class EVictoryLossCheckResult; +class Services; +class IMapService; +class CMap; +struct CPack; +class CHeroClass; +struct EventCondition; +struct CampaignTravel; +class CStackInstance; +class CGameStateCampaign; +class TavernHeroesPool; +struct SThievesGuildInfo; + +template class CApplier; +class CBaseForGSApply; + +struct DLL_LINKAGE RumorState +{ + enum ERumorType : ui8 + { + TYPE_NONE = 0, TYPE_RAND, TYPE_SPECIAL, TYPE_MAP + }; + + enum ERumorTypeSpecial : ui8 + { + RUMOR_OBELISKS = 208, + RUMOR_ARTIFACTS = 209, + RUMOR_ARMY = 210, + RUMOR_INCOME = 211, + RUMOR_GRAIL = 212 + }; + + ERumorType type; + std::map> last; + + RumorState(){type = TYPE_NONE;}; + bool update(int id, int extra); + + template void serialize(Handler &h, const int version) + { + h & type; + h & last; + } +}; + +struct UpgradeInfo +{ + CreatureID oldID; //creature to be upgraded + std::vector newID; //possible upgrades + std::vector cost; // cost[upgrade_serial] -> set of pairs; cost is for single unit (not entire stack) + UpgradeInfo(){oldID = CreatureID::NONE;}; +}; + +class BattleInfo; + +DLL_LINKAGE std::ostream & operator<<(std::ostream & os, const EVictoryLossCheckResult & victoryLossCheckResult); + +class DLL_LINKAGE CGameState : public CNonConstInfoCallback +{ + friend class CGameStateCampaign; + +public: + /// List of currently ongoing battles + std::vector> currentBattles; + /// ID that can be allocated to next battle + BattleID nextBattleID = BattleID(0); + + //we have here all heroes available on this map that are not hired + std::unique_ptr heroesPool; + + /// list of players currently making turn. Usually - just one, except for simturns + std::set actingPlayers; + + CGameState(); + virtual ~CGameState(); + + void preInit(Services * services); + + void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = false); + void updateOnLoad(StartInfo * si); + + ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) + ui32 day; //total number of days in game + ConstTransitivePtr map; + std::map players; + std::map teams; + CBonusSystemNode globalEffects; + RumorState rumor; + + static boost::shared_mutex mutex; + + void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; + + bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); + + void apply(CPack *pack); + BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); + + void fillUpgradeInfo(const CArmedInstance *obj, SlotID stackPos, UpgradeInfo &out) const override; + PlayerRelations getPlayerRelations(PlayerColor color1, PlayerColor color2) const override; + bool checkForVisitableDir(const int3 & src, const int3 & dst) const; //check if src tile is visitable from dst tile + void calculatePaths(const CGHeroInstance *hero, CPathsInfo &out) override; //calculates possible paths for hero, by default uses current hero position and movement left; returns pointer to newly allocated CPath or nullptr if path does not exists + void calculatePaths(const std::shared_ptr & config) override; + int3 guardingCreaturePosition (int3 pos) const override; + std::vector guardingCreatures (int3 pos) const; + void updateRumor(); + + /// Returns battle in which selected player is engaged, or nullptr if none. + /// Can NOT be used with neutral player, use battle by ID instead + const BattleInfo * getBattle(const PlayerColor & player) const; + /// Returns battle by its unique identifier, or nullptr if not found + const BattleInfo * getBattle(const BattleID & battle) const; + BattleInfo * getBattle(const BattleID & battle); + + // ----- victory, loss condition checks ----- + + EVictoryLossCheckResult checkForVictoryAndLoss(const PlayerColor & player) const; + bool checkForVictory(const PlayerColor & player, const EventCondition & condition) const; //checks if given player is winner + PlayerColor checkForStandardWin() const; //returns color of player that accomplished standard victory conditions or 255 (NEUTRAL) if no winner + bool checkForStandardLoss(const PlayerColor & player) const; //checks if given player lost the game + + void obtainPlayersStats(SThievesGuildInfo & tgi, int level); //fills tgi with info about other players that is available at given level of thieves' guild + + bool isVisible(int3 pos, const std::optional & player) const override; + bool isVisible(const CGObjectInstance * obj, const std::optional & player) const override; + + int getDate(Date mode=Date::DAY) const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month + + // ----- getters, setters ----- + + /// This RNG should only be used inside GS or CPackForClient-derived applyGs + /// If this doesn't work for your code that mean you need a new netpack + /// + /// Client-side must use CRandomGenerator::getDefault which is not serialized + /// + /// CGameHandler have it's own getter for CRandomGenerator::getDefault + /// Any server-side code outside of GH must use CRandomGenerator::getDefault + CRandomGenerator & getRandomGenerator(); + + template void serialize(Handler &h, const int version) + { + h & scenarioOps; + h & initialOpts; + h & actingPlayers; + h & day; + h & map; + h & players; + h & teams; + h & heroesPool; + h & globalEffects; + h & rand; + h & rumor; + h & campaign; + + BONUS_TREE_DESERIALIZATION_FIX + } + +private: + // ----- initialization ----- + void preInitAuto(); + void initNewGame(const IMapService * mapService, bool allowSavingRandomMap, Load::ProgressAccumulator & progressTracking); + void checkMapChecksum(); + void initGlobalBonuses(); + void initGrailPosition(); + void initRandomFactionsForPlayers(); + void randomizeMapObjects(); + void randomizeObject(CGObjectInstance *cur); + void initPlayerStates(); + void placeStartingHeroes(); + void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); + void removeHeroPlaceholders(); + void initDifficulty(); + void initHeroes(); + void placeHeroesInTowns(); + void initFogOfWar(); + void initStartingBonus(); + void initTowns(); + void initMapObjects(); + void initVisitingAndGarrisonedHeroes(); + void initCampaign(); + + // ----- bonus system handling ----- + + void buildBonusSystemTree(); + void attachArmedObjects(); + void buildGlobalTeamPlayerTree(); + void deserializationFix(); + + // ---- misc helpers ----- + + CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; + bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons + std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; + std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns + HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly + HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly + UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; + + // ---- data ----- + std::shared_ptr> applier; + CRandomGenerator rand; + Services * services; + + /// Ponter to campaign state manager. Nullptr for single scenarios + std::unique_ptr campaign; + + friend class IGameCallback; + friend class CMapHandler; + friend class CGameHandler; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/int3.h b/lib/int3.h index aa060703a..c70d5827d 100644 --- a/lib/int3.h +++ b/lib/int3.h @@ -1,217 +1,217 @@ -/* - * int3.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 - -VCMI_LIB_NAMESPACE_BEGIN - -/// Class which consists of three integer values. Represents position on adventure map. -class int3 -{ -public: - si32 x, y, z; - - //c-tor: x, y, z initialized to 0 - constexpr int3() : x(0), y(0), z(0) {} // I think that x, y, z should be left uninitialized. - //c-tor: x, y, z initialized to i - explicit constexpr int3(const si32 i) : x(i), y(i), z(i) {} - //c-tor: x, y, z initialized to X, Y, Z - constexpr int3(const si32 X, const si32 Y, const si32 Z) : x(X), y(Y), z(Z) {} - constexpr int3(const int3 & c) = default; - - constexpr int3 & operator=(const int3 & c) = default; - constexpr int3 operator-() const { return int3(-x, -y, -z); } - - constexpr int3 operator+(const int3 & i) const { return int3(x + i.x, y + i.y, z + i.z); } - constexpr int3 operator-(const int3 & i) const { return int3(x - i.x, y - i.y, z - i.z); } - //returns int3 with coordinates increased by given number - constexpr int3 operator+(const si32 i) const { return int3(x + i, y + i, z + i); } - //returns int3 with coordinates decreased by given number - constexpr int3 operator-(const si32 i) const { return int3(x - i, y - i, z - i); } - - //returns int3 with coordinates multiplied by given number - constexpr int3 operator*(const double i) const { return int3((int)(x * i), (int)(y * i), (int)(z * i)); } - //returns int3 with coordinates divided by given number - constexpr int3 operator/(const double i) const { return int3((int)(x / i), (int)(y / i), (int)(z / i)); } - - //returns int3 with coordinates multiplied by given number - constexpr int3 operator*(const si32 i) const { return int3(x * i, y * i, z * i); } - //returns int3 with coordinates divided by given number - constexpr int3 operator/(const si32 i) const { return int3(x / i, y / i, z / i); } - - constexpr int3 & operator+=(const int3 & i) - { - x += i.x; - y += i.y; - z += i.z; - return *this; - } - constexpr int3 & operator-=(const int3 & i) - { - x -= i.x; - y -= i.y; - z -= i.z; - return *this; - } - - //increases all coordinates by given number - constexpr int3 & operator+=(const si32 i) - { - x += i; - y += i; - z += i; - return *this; - } - //decreases all coordinates by given number - constexpr int3 & operator-=(const si32 i) - { - x -= i; - y -= i; - z -= i; - return *this; - } - - constexpr bool operator==(const int3 & i) const { return (x == i.x && y == i.y && z == i.z); } - constexpr bool operator!=(const int3 & i) const { return (x != i.x || y != i.y || z != i.z); } - - constexpr bool operator<(const int3 & i) const - { - if (z < i.z) - return true; - if (z > i.z) - return false; - if (y < i.y) - return true; - if (y > i.y) - return false; - if (x < i.x) - return true; - if (x > i.x) - return false; - return false; - } - - enum EDistanceFormula - { - DIST_2D = 0, - DIST_MANHATTAN, // patrol distance - DIST_CHEBYSHEV, // ambient sound distance - DIST_2DSQ - }; - - ui32 dist(const int3 & o, EDistanceFormula formula) const - { - switch(formula) - { - case DIST_2D: - return static_cast(dist2d(o)); - case DIST_MANHATTAN: - return static_cast(mandist2d(o)); - case DIST_CHEBYSHEV: - return static_cast(chebdist2d(o)); - case DIST_2DSQ: - return dist2dSQ(o); - default: - return 0; - } - } - - //returns squared distance on Oxy plane (z coord is not used) - constexpr ui32 dist2dSQ(const int3 & o) const - { - const si32 dx = (x - o.x); - const si32 dy = (y - o.y); - return (ui32)(dx*dx) + (ui32)(dy*dy); - } - //returns distance on Oxy plane (z coord is not used) - double dist2d(const int3 & o) const - { - return std::sqrt((double)dist2dSQ(o)); - } - //manhattan distance used for patrol radius (z coord is not used) - constexpr double mandist2d(const int3 & o) const - { - return vstd::abs(o.x - x) + vstd::abs(o.y - y); - } - //chebyshev distance used for ambient sounds (z coord is not used) - constexpr double chebdist2d(const int3 & o) const - { - return std::max(vstd::abs(o.x - x), vstd::abs(o.y - y)); - } - - constexpr bool areNeighbours(const int3 & o) const - { - return (dist2dSQ(o) < 4) && (z == o.z); - } - - //returns "(x y z)" string - std::string toString() const - { - //Performance is important here - std::string result = "(" + - std::to_string(x) + " " + - std::to_string(y) + " " + - std::to_string(z) + ")"; - - return result; - } - - constexpr bool valid() const //Should be named "isValid"? - { - return z >= 0; //minimal condition that needs to be fulfilled for tiles in the map - } - - template - void serialize(Handler &h, const int version) - { - h & x; - h & y; - h & z; - } - - constexpr static std::array getDirs() - { - return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), - int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } }; - } -}; - -template -int3 findClosestTile (Container & container, int3 dest) -{ - static_assert(std::is_same::value, - "findClosestTile requires container."); - - int3 result(-1, -1, -1); - ui32 distance = std::numeric_limits::max(); - for (const int3& tile : container) - { - const ui32 currentDistance = dest.dist2dSQ(tile); - if (currentDistance < distance) - { - result = tile; - distance = currentDistance; - } - } - return result; -} - -VCMI_LIB_NAMESPACE_END - - -template<> -struct std::hash { - size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const - { - size_t ret = std::hash()(pos.x); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y); - VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z); - return ret; - } -}; +/* + * int3.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 + +VCMI_LIB_NAMESPACE_BEGIN + +/// Class which consists of three integer values. Represents position on adventure map. +class int3 +{ +public: + si32 x, y, z; + + //c-tor: x, y, z initialized to 0 + constexpr int3() : x(0), y(0), z(0) {} // I think that x, y, z should be left uninitialized. + //c-tor: x, y, z initialized to i + explicit constexpr int3(const si32 i) : x(i), y(i), z(i) {} + //c-tor: x, y, z initialized to X, Y, Z + constexpr int3(const si32 X, const si32 Y, const si32 Z) : x(X), y(Y), z(Z) {} + constexpr int3(const int3 & c) = default; + + constexpr int3 & operator=(const int3 & c) = default; + constexpr int3 operator-() const { return int3(-x, -y, -z); } + + constexpr int3 operator+(const int3 & i) const { return int3(x + i.x, y + i.y, z + i.z); } + constexpr int3 operator-(const int3 & i) const { return int3(x - i.x, y - i.y, z - i.z); } + //returns int3 with coordinates increased by given number + constexpr int3 operator+(const si32 i) const { return int3(x + i, y + i, z + i); } + //returns int3 with coordinates decreased by given number + constexpr int3 operator-(const si32 i) const { return int3(x - i, y - i, z - i); } + + //returns int3 with coordinates multiplied by given number + constexpr int3 operator*(const double i) const { return int3((int)(x * i), (int)(y * i), (int)(z * i)); } + //returns int3 with coordinates divided by given number + constexpr int3 operator/(const double i) const { return int3((int)(x / i), (int)(y / i), (int)(z / i)); } + + //returns int3 with coordinates multiplied by given number + constexpr int3 operator*(const si32 i) const { return int3(x * i, y * i, z * i); } + //returns int3 with coordinates divided by given number + constexpr int3 operator/(const si32 i) const { return int3(x / i, y / i, z / i); } + + constexpr int3 & operator+=(const int3 & i) + { + x += i.x; + y += i.y; + z += i.z; + return *this; + } + constexpr int3 & operator-=(const int3 & i) + { + x -= i.x; + y -= i.y; + z -= i.z; + return *this; + } + + //increases all coordinates by given number + constexpr int3 & operator+=(const si32 i) + { + x += i; + y += i; + z += i; + return *this; + } + //decreases all coordinates by given number + constexpr int3 & operator-=(const si32 i) + { + x -= i; + y -= i; + z -= i; + return *this; + } + + constexpr bool operator==(const int3 & i) const { return (x == i.x && y == i.y && z == i.z); } + constexpr bool operator!=(const int3 & i) const { return (x != i.x || y != i.y || z != i.z); } + + constexpr bool operator<(const int3 & i) const + { + if (z < i.z) + return true; + if (z > i.z) + return false; + if (y < i.y) + return true; + if (y > i.y) + return false; + if (x < i.x) + return true; + if (x > i.x) + return false; + return false; + } + + enum EDistanceFormula + { + DIST_2D = 0, + DIST_MANHATTAN, // patrol distance + DIST_CHEBYSHEV, // ambient sound distance + DIST_2DSQ + }; + + ui32 dist(const int3 & o, EDistanceFormula formula) const + { + switch(formula) + { + case DIST_2D: + return static_cast(dist2d(o)); + case DIST_MANHATTAN: + return static_cast(mandist2d(o)); + case DIST_CHEBYSHEV: + return static_cast(chebdist2d(o)); + case DIST_2DSQ: + return dist2dSQ(o); + default: + return 0; + } + } + + //returns squared distance on Oxy plane (z coord is not used) + constexpr ui32 dist2dSQ(const int3 & o) const + { + const si32 dx = (x - o.x); + const si32 dy = (y - o.y); + return (ui32)(dx*dx) + (ui32)(dy*dy); + } + //returns distance on Oxy plane (z coord is not used) + double dist2d(const int3 & o) const + { + return std::sqrt((double)dist2dSQ(o)); + } + //manhattan distance used for patrol radius (z coord is not used) + constexpr double mandist2d(const int3 & o) const + { + return vstd::abs(o.x - x) + vstd::abs(o.y - y); + } + //chebyshev distance used for ambient sounds (z coord is not used) + constexpr double chebdist2d(const int3 & o) const + { + return std::max(vstd::abs(o.x - x), vstd::abs(o.y - y)); + } + + constexpr bool areNeighbours(const int3 & o) const + { + return (dist2dSQ(o) < 4) && (z == o.z); + } + + //returns "(x y z)" string + std::string toString() const + { + //Performance is important here + std::string result = "(" + + std::to_string(x) + " " + + std::to_string(y) + " " + + std::to_string(z) + ")"; + + return result; + } + + constexpr bool valid() const //Should be named "isValid"? + { + return z >= 0; //minimal condition that needs to be fulfilled for tiles in the map + } + + template + void serialize(Handler &h, const int version) + { + h & x; + h & y; + h & z; + } + + constexpr static std::array getDirs() + { + return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), + int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } }; + } +}; + +template +int3 findClosestTile (Container & container, int3 dest) +{ + static_assert(std::is_same::value, + "findClosestTile requires container."); + + int3 result(-1, -1, -1); + ui32 distance = std::numeric_limits::max(); + for (const int3& tile : container) + { + const ui32 currentDistance = dest.dist2dSQ(tile); + if (currentDistance < distance) + { + result = tile; + distance = currentDistance; + } + } + return result; +} + +VCMI_LIB_NAMESPACE_END + + +template<> +struct std::hash { + size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const + { + size_t ret = std::hash()(pos.x); + VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y); + VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z); + return ret; + } +}; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index fd9062164..806c953c1 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -1,704 +1,704 @@ -/* - * CMap.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 "CMap.h" - -#include "../CArtHandler.h" -#include "../VCMI_Lib.h" -#include "../CCreatureHandler.h" -#include "../CTownHandler.h" -#include "../CHeroHandler.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../CGeneralTextHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CSkillHandler.h" -#include "CMapEditManager.h" -#include "CMapOperation.h" -#include "../serializer/JsonSerializeFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -void Rumor::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeString("name", name); - handler.serializeStruct("text", text); -} - -DisposedHero::DisposedHero() : heroId(0), portrait(255) -{ - -} - -CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), - firstOccurence(0), nextOccurence(0) -{ - -} - -bool CMapEvent::earlierThan(const CMapEvent & other) const -{ - return firstOccurence < other.firstOccurence; -} - -bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const -{ - return firstOccurence <= other.firstOccurence; -} - -void CMapEvent::serializeJson(JsonSerializeFormat & handler) -{ - handler.serializeString("name", name); - handler.serializeStruct("message", message); - handler.serializeInt("players", players); - handler.serializeInt("humanAffected", humanAffected); - handler.serializeInt("computerAffected", computerAffected); - handler.serializeInt("firstOccurence", firstOccurence); - handler.serializeInt("nextOccurence", nextOccurence); - resources.serializeJson(handler, "resources"); -} - -void CCastleEvent::serializeJson(JsonSerializeFormat & handler) -{ - CMapEvent::serializeJson(handler); - { - std::vector temp(buildings.begin(), buildings.end()); - auto a = handler.enterArray("buildings"); - a.syncSize(temp); - for(int i = 0; i < temp.size(); ++i) - { - a.serializeInt(i, temp[i]); - buildings.insert(temp[i]); - } - } - { - auto a = handler.enterArray("creatures"); - a.syncSize(creatures); - for(int i = 0; i < creatures.size(); ++i) - a.serializeInt(i, creatures[i]); - } -} - -TerrainTile::TerrainTile(): - terType(nullptr), - terView(0), - riverType(VLC->riverTypeHandler->getById(River::NO_RIVER)), - riverDir(0), - roadType(VLC->roadTypeHandler->getById(Road::NO_ROAD)), - roadDir(0), - extTileFlags(0), - visitable(false), - blocked(false) -{ -} - -bool TerrainTile::entrableTerrain(const TerrainTile * from) const -{ - return entrableTerrain(from ? from->terType->isLand() : true, from ? from->terType->isWater() : true); -} - -bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const -{ - return terType->isPassable() - && ((allowSea && terType->isWater()) || (allowLand && terType->isLand())); -} - -bool TerrainTile::isClear(const TerrainTile * from) const -{ - return entrableTerrain(from) && !blocked; -} - -Obj TerrainTile::topVisitableId(bool excludeTop) const -{ - return topVisitableObj(excludeTop) ? topVisitableObj(excludeTop)->ID : Obj(Obj::NO_OBJ); -} - -CGObjectInstance * TerrainTile::topVisitableObj(bool excludeTop) const -{ - if(visitableObjects.empty() || (excludeTop && visitableObjects.size() == 1)) - return nullptr; - - if(excludeTop) - return visitableObjects[visitableObjects.size()-2]; - - return visitableObjects.back(); -} - -EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const -{ - if(terType->isWater() || !terType->isPassable()) - return EDiggingStatus::WRONG_TERRAIN; - - int allowedBlocked = excludeTop ? 1 : 0; - if(blockingObjects.size() > allowedBlocked || topVisitableObj(excludeTop)) - return EDiggingStatus::TILE_OCCUPIED; - else - return EDiggingStatus::CAN_DIG; -} - -bool TerrainTile::hasFavorableWinds() const -{ - return extTileFlags & 128; -} - -bool TerrainTile::isWater() const -{ - return terType->isWater(); -} - -CMap::CMap() - : checksum(0) - , grailPos(-1, -1, -1) - , grailRadius(0) - , uidCounter(0) -{ - allHeroes.resize(allowedHeroes.size()); - allowedAbilities = VLC->skillh->getDefaultAllowed(); - allowedArtifact = VLC->arth->getDefaultAllowed(); - allowedSpells = VLC->spellh->getDefaultAllowed(); -} - -CMap::~CMap() -{ - getEditManager()->getUndoManager().clearAll(); - - for(auto obj : objects) - obj.dellNull(); - - for(auto quest : quests) - quest.dellNull(); - - resetStaticData(); -} - -void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) -{ - const int zVal = obj->pos.z; - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - int xVal = obj->pos.x - fx; - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int yVal = obj->pos.y - fy; - if(xVal>=0 && xVal < width && yVal>=0 && yVal < height) - { - TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(total || obj->visitableAt(xVal, yVal)) - { - curt.visitableObjects -= obj; - curt.visitable = curt.visitableObjects.size(); - } - if(total || obj->blockingAt(xVal, yVal)) - { - curt.blockingObjects -= obj; - curt.blocked = curt.blockingObjects.size(); - } - } - } - } -} - -void CMap::addBlockVisTiles(CGObjectInstance * obj) -{ - const int zVal = obj->pos.z; - for(int fx = 0; fx < obj->getWidth(); ++fx) - { - int xVal = obj->pos.x - fx; - for(int fy = 0; fy < obj->getHeight(); ++fy) - { - int yVal = obj->pos.y - fy; - if(xVal>=0 && xVal < width && yVal >= 0 && yVal < height) - { - TerrainTile & curt = terrain[zVal][xVal][yVal]; - if(obj->visitableAt(xVal, yVal)) - { - curt.visitableObjects.push_back(obj); - curt.visitable = true; - } - if(obj->blockingAt(xVal, yVal)) - { - curt.blockingObjects.push_back(obj); - curt.blocked = true; - } - } - } - } -} - -void CMap::calculateGuardingGreaturePositions() -{ - int levels = twoLevel ? 2 : 1; - for(int z = 0; z < levels; z++) - { - for(int x = 0; x < width; x++) - { - for(int y = 0; y < height; y++) - { - guardingCreaturePositions[z][x][y] = guardingCreaturePosition(int3(x, y, z)); - } - } - } -} - -CGHeroInstance * CMap::getHero(int heroID) -{ - for(auto & elem : heroesOnMap) - if(elem->subID == heroID) - return elem; - return nullptr; -} - -bool CMap::isCoastalTile(const int3 & pos) const -{ - //todo: refactoring: extract neighbor tile iterator and use it in GameState - static const int3 dirs[] = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), - int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) }; - - if(!isInTheMap(pos)) - { - logGlobal->error("Coastal check outside of map: %s", pos.toString()); - return false; - } - - if(isWaterTile(pos)) - return false; - - for(const auto & dir : dirs) - { - const int3 hlp = pos + dir; - - if(!isInTheMap(hlp)) - continue; - const TerrainTile &hlpt = getTile(hlp); - if(hlpt.isWater()) - return true; - } - - return false; -} - -bool CMap::isInTheMap(const int3 & pos) const -{ - return pos.x >= 0 && pos.y >= 0 && pos.z >= 0 && pos.x < width && pos.y < height && pos.z <= (twoLevel ? 1 : 0); -} - -TerrainTile & CMap::getTile(const int3 & tile) -{ - assert(isInTheMap(tile)); - return terrain[tile.z][tile.x][tile.y]; -} - -const TerrainTile & CMap::getTile(const int3 & tile) const -{ - assert(isInTheMap(tile)); - return terrain[tile.z][tile.x][tile.y]; -} - -bool CMap::isWaterTile(const int3 &pos) const -{ - return isInTheMap(pos) && getTile(pos).isWater(); -} -bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const -{ - const TerrainTile * dstTile = &getTile(dst); - const TerrainTile * srcTile = &getTile(src); - return checkForVisitableDir(src, dstTile, dst) && checkForVisitableDir(dst, srcTile, src); -} - -bool CMap::checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const -{ - if (!pom->entrableTerrain()) //rock is never accessible - return false; - for(auto * obj : pom->visitableObjects) //checking destination tile - { - if(!vstd::contains(pom->blockingObjects, obj)) //this visitable object is not blocking, ignore - continue; - - if (!obj->appearance->isVisitableFrom(src.x - dst.x, src.y - dst.y)) - return false; - } - return true; -} - -int3 CMap::guardingCreaturePosition (int3 pos) const -{ - const int3 originalPos = pos; - // Give monster at position priority. - if (!isInTheMap(pos)) - return int3(-1, -1, -1); - const TerrainTile &posTile = getTile(pos); - if (posTile.visitable) - { - for (CGObjectInstance* obj : posTile.visitableObjects) - { - if(obj->isBlockedVisitable()) - { - if (obj->ID == Obj::MONSTER) // Monster - return pos; - else - return int3(-1, -1, -1); //blockvis objects are not guarded by neighbouring creatures - } - } - } - - // See if there are any monsters adjacent. - bool water = posTile.isWater(); - - pos -= int3(1, 1, 0); // Start with top left. - for (int dx = 0; dx < 3; dx++) - { - for (int dy = 0; dy < 3; dy++) - { - if (isInTheMap(pos)) - { - const auto & tile = getTile(pos); - if (tile.visitable && (tile.isWater() == water)) - { - for (CGObjectInstance* obj : tile.visitableObjects) - { - if (obj->ID == Obj::MONSTER && checkForVisitableDir(pos, &posTile, originalPos)) // Monster being able to attack investigated tile - { - return pos; - } - } - } - } - - pos.y++; - } - pos.y -= 3; - pos.x++; - } - - return int3(-1, -1, -1); -} - -const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type) -{ - for (CGObjectInstance * object : getTile(pos).visitableObjects) - { - if (object->ID == type) - return object; - } - // There is weird bug because of which sometimes heroes will not be found properly despite having correct position - // Try to workaround that and find closest object that we can use - - logGlobal->error("Failed to find object of type %d at %s", static_cast(type), pos.toString()); - logGlobal->error("Will try to find closest matching object"); - - CGObjectInstance * bestMatch = nullptr; - for (CGObjectInstance * object : objects) - { - if (object && object->ID == type) - { - if (bestMatch == nullptr) - bestMatch = object; - else - { - if (object->pos.dist2dSQ(pos) < bestMatch->pos.dist2dSQ(pos)) - bestMatch = object;// closer than one we already found - } - } - } - assert(bestMatch != nullptr); // if this happens - victory conditions or map itself is very, very broken - - logGlobal->error("Will use %s from %s", bestMatch->getObjectName(), bestMatch->pos.toString()); - return bestMatch; -} - -void CMap::checkForObjectives() -{ - // NOTE: probably should be moved to MapFormatH3M.cpp - for (TriggeredEvent & event : triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - switch (cond.condition) - { - case EventCondition::HAVE_ARTIFACT: - event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID()); - break; - - case EventCondition::HAVE_CREATURES: - event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID()); - event.onFulfill.replaceNumber(cond.value); - break; - - case EventCondition::HAVE_RESOURCES: - event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType); - event.onFulfill.replaceNumber(cond.value); - break; - - case EventCondition::HAVE_BUILDING: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); - break; - - case EventCondition::CONTROL: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); - - if (cond.object) - { - const auto * town = dynamic_cast(cond.object); - if (town) - event.onFulfill.replaceRawString(town->getNameTranslated()); - const auto * hero = dynamic_cast(cond.object); - if (hero) - event.onFulfill.replaceRawString(hero->getNameTranslated()); - } - break; - - case EventCondition::DESTROY: - if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); - - if (cond.object) - { - const auto * hero = dynamic_cast(cond.object); - if (hero) - event.onFulfill.replaceRawString(hero->getNameTranslated()); - } - break; - case EventCondition::TRANSPORT: - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); - break; - //break; case EventCondition::DAYS_PASSED: - //break; case EventCondition::IS_HUMAN: - //break; case EventCondition::DAYS_WITHOUT_TOWN: - //break; case EventCondition::STANDARD_WIN: - - //TODO: support new condition format - case EventCondition::HAVE_0: - break; - case EventCondition::DESTROY_0: - break; - case EventCondition::HAVE_BUILDING_0: - break; - } - return cond; - }; - event.trigger = event.trigger.morph(patcher); - } -} - -void CMap::addNewArtifactInstance(ConstTransitivePtr art) -{ - art->setId(static_cast(artInstances.size())); - artInstances.emplace_back(art); -} - -void CMap::eraseArtifactInstance(CArtifactInstance * art) -{ - //TODO: handle for artifacts removed in map editor - assert(artInstances[art->getId().getNum()] == art); - artInstances[art->getId().getNum()].dellNull(); -} - -void CMap::addNewQuestInstance(CQuest* quest) -{ - quest->qid = static_cast(quests.size()); - quests.emplace_back(quest); -} - -void CMap::removeQuestInstance(CQuest * quest) - -{ - //TODO: should be called only by map editor. - //During game, completed quests or quests from removed objects stay forever - - //Shift indexes - auto iter = std::next(quests.begin(), quest->qid); - iter = quests.erase(iter); - for (int i = quest->qid; iter != quests.end(); ++i, ++iter) - { - (*iter)->qid = i; - } -} - -void CMap::setUniqueInstanceName(CGObjectInstance * obj) -{ - //this gives object unique name even if objects are removed later - - auto uid = uidCounter++; - - boost::format fmt("%s_%d"); - fmt % obj->typeName % uid; - obj->instanceName = fmt.str(); -} - -void CMap::addNewObject(CGObjectInstance * obj) -{ - if(obj->id != ObjectInstanceID(static_cast(objects.size()))) - throw std::runtime_error("Invalid object instance id"); - - if(obj->instanceName.empty()) - throw std::runtime_error("Object instance name missing"); - - if (vstd::contains(instanceNames, obj->instanceName)) - throw std::runtime_error("Object instance name duplicated: "+obj->instanceName); - - objects.emplace_back(obj); - instanceNames[obj->instanceName] = obj; - addBlockVisTiles(obj); - - //TODO: how about defeated heroes recruited again? - - obj->afterAddToMap(this); -} - -void CMap::moveObject(CGObjectInstance * obj, const int3 & pos) -{ - removeBlockVisTiles(obj); - obj->pos = pos; - addBlockVisTiles(obj); -} - -void CMap::removeObject(CGObjectInstance * obj) -{ - removeBlockVisTiles(obj); - instanceNames.erase(obj->instanceName); - - //update indeces - auto iter = std::next(objects.begin(), obj->id.getNum()); - iter = objects.erase(iter); - for(int i = obj->id.getNum(); iter != objects.end(); ++i, ++iter) - { - (*iter)->id = ObjectInstanceID(i); - } - - obj->afterRemoveFromMap(this); - - //TOOD: Clean artifact instances (mostly worn by hero?) and quests related to this object -} - -bool CMap::isWaterMap() const -{ - return waterMap; -} - -bool CMap::calculateWaterContent() -{ - size_t totalTiles = height * width * levels(); - size_t waterTiles = 0; - - for(auto tile = terrain.origin(); tile < (terrain.origin() + terrain.num_elements()); ++tile) - { - if (tile->isWater()) - { - waterTiles++; - } - } - - if (waterTiles >= totalTiles / 100) //At least 1% of area is water - { - waterMap = true; - } - - return waterMap; -} - -void CMap::banWaterContent() -{ - banWaterHeroes(); - banWaterArtifacts(); - banWaterSpells(); - banWaterSkills(); -} - -void CMap::banWaterSpells() -{ - for (int j = 0; j < allowedSpells.size(); j++) - { - if (allowedSpells[j]) - { - auto* spell = dynamic_cast(VLC->spells()->getByIndex(j)); - if (spell->onlyOnWaterMap && !isWaterMap()) - { - allowedSpells[j] = false; - } - } - } -} - -void CMap::banWaterArtifacts() -{ - for (int j = 0; j < allowedArtifact.size(); j++) - { - if (allowedArtifact[j]) - { - auto* art = dynamic_cast(VLC->artifacts()->getByIndex(j)); - if (art->onlyOnWaterMap && !isWaterMap()) - { - allowedArtifact[j] = false; - } - } - } -} - -void CMap::banWaterSkills() -{ - for (int j = 0; j < allowedAbilities.size(); j++) - { - if (allowedAbilities[j]) - { - auto* skill = dynamic_cast(VLC->skills()->getByIndex(j)); - if (skill->onlyOnWaterMap && !isWaterMap()) - { - allowedAbilities[j] = false; - } - } - } -} - -void CMap::banWaterHeroes() -{ - for (int j = 0; j < allowedHeroes.size(); j++) - { - if (allowedHeroes[j]) - { - auto* h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap()) || (h->onlyOnMapWithoutWater && isWaterMap())) - { - banHero(HeroTypeID(j)); - } - } - } -} - -void CMap::banHero(const HeroTypeID & id) -{ - allowedHeroes.at(id) = false; -} - -void CMap::initTerrain() -{ - terrain.resize(boost::extents[levels()][width][height]); - guardingCreaturePositions.resize(boost::extents[levels()][width][height]); -} - -CMapEditManager * CMap::getEditManager() -{ - if(!editManager) editManager = std::make_unique(this); - return editManager.get(); -} - -void CMap::resetStaticData() -{ - CGKeys::reset(); - CGMagi::reset(); - CGObelisk::reset(); - CGTownInstance::reset(); -} - -VCMI_LIB_NAMESPACE_END +/* + * CMap.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 "CMap.h" + +#include "../CArtHandler.h" +#include "../VCMI_Lib.h" +#include "../CCreatureHandler.h" +#include "../CTownHandler.h" +#include "../CHeroHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../TerrainHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../CGeneralTextHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CSkillHandler.h" +#include "CMapEditManager.h" +#include "CMapOperation.h" +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +void Rumor::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeString("name", name); + handler.serializeStruct("text", text); +} + +DisposedHero::DisposedHero() : heroId(0), portrait(255) +{ + +} + +CMapEvent::CMapEvent() : players(0), humanAffected(0), computerAffected(0), + firstOccurence(0), nextOccurence(0) +{ + +} + +bool CMapEvent::earlierThan(const CMapEvent & other) const +{ + return firstOccurence < other.firstOccurence; +} + +bool CMapEvent::earlierThanOrEqual(const CMapEvent & other) const +{ + return firstOccurence <= other.firstOccurence; +} + +void CMapEvent::serializeJson(JsonSerializeFormat & handler) +{ + handler.serializeString("name", name); + handler.serializeStruct("message", message); + handler.serializeInt("players", players); + handler.serializeInt("humanAffected", humanAffected); + handler.serializeInt("computerAffected", computerAffected); + handler.serializeInt("firstOccurence", firstOccurence); + handler.serializeInt("nextOccurence", nextOccurence); + resources.serializeJson(handler, "resources"); +} + +void CCastleEvent::serializeJson(JsonSerializeFormat & handler) +{ + CMapEvent::serializeJson(handler); + { + std::vector temp(buildings.begin(), buildings.end()); + auto a = handler.enterArray("buildings"); + a.syncSize(temp); + for(int i = 0; i < temp.size(); ++i) + { + a.serializeInt(i, temp[i]); + buildings.insert(temp[i]); + } + } + { + auto a = handler.enterArray("creatures"); + a.syncSize(creatures); + for(int i = 0; i < creatures.size(); ++i) + a.serializeInt(i, creatures[i]); + } +} + +TerrainTile::TerrainTile(): + terType(nullptr), + terView(0), + riverType(VLC->riverTypeHandler->getById(River::NO_RIVER)), + riverDir(0), + roadType(VLC->roadTypeHandler->getById(Road::NO_ROAD)), + roadDir(0), + extTileFlags(0), + visitable(false), + blocked(false) +{ +} + +bool TerrainTile::entrableTerrain(const TerrainTile * from) const +{ + return entrableTerrain(from ? from->terType->isLand() : true, from ? from->terType->isWater() : true); +} + +bool TerrainTile::entrableTerrain(bool allowLand, bool allowSea) const +{ + return terType->isPassable() + && ((allowSea && terType->isWater()) || (allowLand && terType->isLand())); +} + +bool TerrainTile::isClear(const TerrainTile * from) const +{ + return entrableTerrain(from) && !blocked; +} + +Obj TerrainTile::topVisitableId(bool excludeTop) const +{ + return topVisitableObj(excludeTop) ? topVisitableObj(excludeTop)->ID : Obj(Obj::NO_OBJ); +} + +CGObjectInstance * TerrainTile::topVisitableObj(bool excludeTop) const +{ + if(visitableObjects.empty() || (excludeTop && visitableObjects.size() == 1)) + return nullptr; + + if(excludeTop) + return visitableObjects[visitableObjects.size()-2]; + + return visitableObjects.back(); +} + +EDiggingStatus TerrainTile::getDiggingStatus(const bool excludeTop) const +{ + if(terType->isWater() || !terType->isPassable()) + return EDiggingStatus::WRONG_TERRAIN; + + int allowedBlocked = excludeTop ? 1 : 0; + if(blockingObjects.size() > allowedBlocked || topVisitableObj(excludeTop)) + return EDiggingStatus::TILE_OCCUPIED; + else + return EDiggingStatus::CAN_DIG; +} + +bool TerrainTile::hasFavorableWinds() const +{ + return extTileFlags & 128; +} + +bool TerrainTile::isWater() const +{ + return terType->isWater(); +} + +CMap::CMap() + : checksum(0) + , grailPos(-1, -1, -1) + , grailRadius(0) + , uidCounter(0) +{ + allHeroes.resize(allowedHeroes.size()); + allowedAbilities = VLC->skillh->getDefaultAllowed(); + allowedArtifact = VLC->arth->getDefaultAllowed(); + allowedSpells = VLC->spellh->getDefaultAllowed(); +} + +CMap::~CMap() +{ + getEditManager()->getUndoManager().clearAll(); + + for(auto obj : objects) + obj.dellNull(); + + for(auto quest : quests) + quest.dellNull(); + + resetStaticData(); +} + +void CMap::removeBlockVisTiles(CGObjectInstance * obj, bool total) +{ + const int zVal = obj->pos.z; + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + int xVal = obj->pos.x - fx; + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int yVal = obj->pos.y - fy; + if(xVal>=0 && xVal < width && yVal>=0 && yVal < height) + { + TerrainTile & curt = terrain[zVal][xVal][yVal]; + if(total || obj->visitableAt(xVal, yVal)) + { + curt.visitableObjects -= obj; + curt.visitable = curt.visitableObjects.size(); + } + if(total || obj->blockingAt(xVal, yVal)) + { + curt.blockingObjects -= obj; + curt.blocked = curt.blockingObjects.size(); + } + } + } + } +} + +void CMap::addBlockVisTiles(CGObjectInstance * obj) +{ + const int zVal = obj->pos.z; + for(int fx = 0; fx < obj->getWidth(); ++fx) + { + int xVal = obj->pos.x - fx; + for(int fy = 0; fy < obj->getHeight(); ++fy) + { + int yVal = obj->pos.y - fy; + if(xVal>=0 && xVal < width && yVal >= 0 && yVal < height) + { + TerrainTile & curt = terrain[zVal][xVal][yVal]; + if(obj->visitableAt(xVal, yVal)) + { + curt.visitableObjects.push_back(obj); + curt.visitable = true; + } + if(obj->blockingAt(xVal, yVal)) + { + curt.blockingObjects.push_back(obj); + curt.blocked = true; + } + } + } + } +} + +void CMap::calculateGuardingGreaturePositions() +{ + int levels = twoLevel ? 2 : 1; + for(int z = 0; z < levels; z++) + { + for(int x = 0; x < width; x++) + { + for(int y = 0; y < height; y++) + { + guardingCreaturePositions[z][x][y] = guardingCreaturePosition(int3(x, y, z)); + } + } + } +} + +CGHeroInstance * CMap::getHero(int heroID) +{ + for(auto & elem : heroesOnMap) + if(elem->subID == heroID) + return elem; + return nullptr; +} + +bool CMap::isCoastalTile(const int3 & pos) const +{ + //todo: refactoring: extract neighbor tile iterator and use it in GameState + static const int3 dirs[] = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0), + int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) }; + + if(!isInTheMap(pos)) + { + logGlobal->error("Coastal check outside of map: %s", pos.toString()); + return false; + } + + if(isWaterTile(pos)) + return false; + + for(const auto & dir : dirs) + { + const int3 hlp = pos + dir; + + if(!isInTheMap(hlp)) + continue; + const TerrainTile &hlpt = getTile(hlp); + if(hlpt.isWater()) + return true; + } + + return false; +} + +bool CMap::isInTheMap(const int3 & pos) const +{ + return pos.x >= 0 && pos.y >= 0 && pos.z >= 0 && pos.x < width && pos.y < height && pos.z <= (twoLevel ? 1 : 0); +} + +TerrainTile & CMap::getTile(const int3 & tile) +{ + assert(isInTheMap(tile)); + return terrain[tile.z][tile.x][tile.y]; +} + +const TerrainTile & CMap::getTile(const int3 & tile) const +{ + assert(isInTheMap(tile)); + return terrain[tile.z][tile.x][tile.y]; +} + +bool CMap::isWaterTile(const int3 &pos) const +{ + return isInTheMap(pos) && getTile(pos).isWater(); +} +bool CMap::canMoveBetween(const int3 &src, const int3 &dst) const +{ + const TerrainTile * dstTile = &getTile(dst); + const TerrainTile * srcTile = &getTile(src); + return checkForVisitableDir(src, dstTile, dst) && checkForVisitableDir(dst, srcTile, src); +} + +bool CMap::checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const +{ + if (!pom->entrableTerrain()) //rock is never accessible + return false; + for(auto * obj : pom->visitableObjects) //checking destination tile + { + if(!vstd::contains(pom->blockingObjects, obj)) //this visitable object is not blocking, ignore + continue; + + if (!obj->appearance->isVisitableFrom(src.x - dst.x, src.y - dst.y)) + return false; + } + return true; +} + +int3 CMap::guardingCreaturePosition (int3 pos) const +{ + const int3 originalPos = pos; + // Give monster at position priority. + if (!isInTheMap(pos)) + return int3(-1, -1, -1); + const TerrainTile &posTile = getTile(pos); + if (posTile.visitable) + { + for (CGObjectInstance* obj : posTile.visitableObjects) + { + if(obj->isBlockedVisitable()) + { + if (obj->ID == Obj::MONSTER) // Monster + return pos; + else + return int3(-1, -1, -1); //blockvis objects are not guarded by neighbouring creatures + } + } + } + + // See if there are any monsters adjacent. + bool water = posTile.isWater(); + + pos -= int3(1, 1, 0); // Start with top left. + for (int dx = 0; dx < 3; dx++) + { + for (int dy = 0; dy < 3; dy++) + { + if (isInTheMap(pos)) + { + const auto & tile = getTile(pos); + if (tile.visitable && (tile.isWater() == water)) + { + for (CGObjectInstance* obj : tile.visitableObjects) + { + if (obj->ID == Obj::MONSTER && checkForVisitableDir(pos, &posTile, originalPos)) // Monster being able to attack investigated tile + { + return pos; + } + } + } + } + + pos.y++; + } + pos.y -= 3; + pos.x++; + } + + return int3(-1, -1, -1); +} + +const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type) +{ + for (CGObjectInstance * object : getTile(pos).visitableObjects) + { + if (object->ID == type) + return object; + } + // There is weird bug because of which sometimes heroes will not be found properly despite having correct position + // Try to workaround that and find closest object that we can use + + logGlobal->error("Failed to find object of type %d at %s", static_cast(type), pos.toString()); + logGlobal->error("Will try to find closest matching object"); + + CGObjectInstance * bestMatch = nullptr; + for (CGObjectInstance * object : objects) + { + if (object && object->ID == type) + { + if (bestMatch == nullptr) + bestMatch = object; + else + { + if (object->pos.dist2dSQ(pos) < bestMatch->pos.dist2dSQ(pos)) + bestMatch = object;// closer than one we already found + } + } + } + assert(bestMatch != nullptr); // if this happens - victory conditions or map itself is very, very broken + + logGlobal->error("Will use %s from %s", bestMatch->getObjectName(), bestMatch->pos.toString()); + return bestMatch; +} + +void CMap::checkForObjectives() +{ + // NOTE: probably should be moved to MapFormatH3M.cpp + for (TriggeredEvent & event : triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + switch (cond.condition) + { + case EventCondition::HAVE_ARTIFACT: + event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID()); + break; + + case EventCondition::HAVE_CREATURES: + event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID()); + event.onFulfill.replaceNumber(cond.value); + break; + + case EventCondition::HAVE_RESOURCES: + event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType); + event.onFulfill.replaceNumber(cond.value); + break; + + case EventCondition::HAVE_BUILDING: + if (isInTheMap(cond.position)) + cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + break; + + case EventCondition::CONTROL: + if (isInTheMap(cond.position)) + cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + + if (cond.object) + { + const auto * town = dynamic_cast(cond.object); + if (town) + event.onFulfill.replaceRawString(town->getNameTranslated()); + const auto * hero = dynamic_cast(cond.object); + if (hero) + event.onFulfill.replaceRawString(hero->getNameTranslated()); + } + break; + + case EventCondition::DESTROY: + if (isInTheMap(cond.position)) + cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + + if (cond.object) + { + const auto * hero = dynamic_cast(cond.object); + if (hero) + event.onFulfill.replaceRawString(hero->getNameTranslated()); + } + break; + case EventCondition::TRANSPORT: + cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + break; + //break; case EventCondition::DAYS_PASSED: + //break; case EventCondition::IS_HUMAN: + //break; case EventCondition::DAYS_WITHOUT_TOWN: + //break; case EventCondition::STANDARD_WIN: + + //TODO: support new condition format + case EventCondition::HAVE_0: + break; + case EventCondition::DESTROY_0: + break; + case EventCondition::HAVE_BUILDING_0: + break; + } + return cond; + }; + event.trigger = event.trigger.morph(patcher); + } +} + +void CMap::addNewArtifactInstance(ConstTransitivePtr art) +{ + art->setId(static_cast(artInstances.size())); + artInstances.emplace_back(art); +} + +void CMap::eraseArtifactInstance(CArtifactInstance * art) +{ + //TODO: handle for artifacts removed in map editor + assert(artInstances[art->getId().getNum()] == art); + artInstances[art->getId().getNum()].dellNull(); +} + +void CMap::addNewQuestInstance(CQuest* quest) +{ + quest->qid = static_cast(quests.size()); + quests.emplace_back(quest); +} + +void CMap::removeQuestInstance(CQuest * quest) + +{ + //TODO: should be called only by map editor. + //During game, completed quests or quests from removed objects stay forever + + //Shift indexes + auto iter = std::next(quests.begin(), quest->qid); + iter = quests.erase(iter); + for (int i = quest->qid; iter != quests.end(); ++i, ++iter) + { + (*iter)->qid = i; + } +} + +void CMap::setUniqueInstanceName(CGObjectInstance * obj) +{ + //this gives object unique name even if objects are removed later + + auto uid = uidCounter++; + + boost::format fmt("%s_%d"); + fmt % obj->typeName % uid; + obj->instanceName = fmt.str(); +} + +void CMap::addNewObject(CGObjectInstance * obj) +{ + if(obj->id != ObjectInstanceID(static_cast(objects.size()))) + throw std::runtime_error("Invalid object instance id"); + + if(obj->instanceName.empty()) + throw std::runtime_error("Object instance name missing"); + + if (vstd::contains(instanceNames, obj->instanceName)) + throw std::runtime_error("Object instance name duplicated: "+obj->instanceName); + + objects.emplace_back(obj); + instanceNames[obj->instanceName] = obj; + addBlockVisTiles(obj); + + //TODO: how about defeated heroes recruited again? + + obj->afterAddToMap(this); +} + +void CMap::moveObject(CGObjectInstance * obj, const int3 & pos) +{ + removeBlockVisTiles(obj); + obj->pos = pos; + addBlockVisTiles(obj); +} + +void CMap::removeObject(CGObjectInstance * obj) +{ + removeBlockVisTiles(obj); + instanceNames.erase(obj->instanceName); + + //update indeces + auto iter = std::next(objects.begin(), obj->id.getNum()); + iter = objects.erase(iter); + for(int i = obj->id.getNum(); iter != objects.end(); ++i, ++iter) + { + (*iter)->id = ObjectInstanceID(i); + } + + obj->afterRemoveFromMap(this); + + //TOOD: Clean artifact instances (mostly worn by hero?) and quests related to this object +} + +bool CMap::isWaterMap() const +{ + return waterMap; +} + +bool CMap::calculateWaterContent() +{ + size_t totalTiles = height * width * levels(); + size_t waterTiles = 0; + + for(auto tile = terrain.origin(); tile < (terrain.origin() + terrain.num_elements()); ++tile) + { + if (tile->isWater()) + { + waterTiles++; + } + } + + if (waterTiles >= totalTiles / 100) //At least 1% of area is water + { + waterMap = true; + } + + return waterMap; +} + +void CMap::banWaterContent() +{ + banWaterHeroes(); + banWaterArtifacts(); + banWaterSpells(); + banWaterSkills(); +} + +void CMap::banWaterSpells() +{ + for (int j = 0; j < allowedSpells.size(); j++) + { + if (allowedSpells[j]) + { + auto* spell = dynamic_cast(VLC->spells()->getByIndex(j)); + if (spell->onlyOnWaterMap && !isWaterMap()) + { + allowedSpells[j] = false; + } + } + } +} + +void CMap::banWaterArtifacts() +{ + for (int j = 0; j < allowedArtifact.size(); j++) + { + if (allowedArtifact[j]) + { + auto* art = dynamic_cast(VLC->artifacts()->getByIndex(j)); + if (art->onlyOnWaterMap && !isWaterMap()) + { + allowedArtifact[j] = false; + } + } + } +} + +void CMap::banWaterSkills() +{ + for (int j = 0; j < allowedAbilities.size(); j++) + { + if (allowedAbilities[j]) + { + auto* skill = dynamic_cast(VLC->skills()->getByIndex(j)); + if (skill->onlyOnWaterMap && !isWaterMap()) + { + allowedAbilities[j] = false; + } + } + } +} + +void CMap::banWaterHeroes() +{ + for (int j = 0; j < allowedHeroes.size(); j++) + { + if (allowedHeroes[j]) + { + auto* h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); + if ((h->onlyOnWaterMap && !isWaterMap()) || (h->onlyOnMapWithoutWater && isWaterMap())) + { + banHero(HeroTypeID(j)); + } + } + } +} + +void CMap::banHero(const HeroTypeID & id) +{ + allowedHeroes.at(id) = false; +} + +void CMap::initTerrain() +{ + terrain.resize(boost::extents[levels()][width][height]); + guardingCreaturePositions.resize(boost::extents[levels()][width][height]); +} + +CMapEditManager * CMap::getEditManager() +{ + if(!editManager) editManager = std::make_unique(this); + return editManager.get(); +} + +void CMap::resetStaticData() +{ + CGKeys::reset(); + CGMagi::reset(); + CGObelisk::reset(); + CGTownInstance::reset(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index abed40d06..de9d5e5a2 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -1,205 +1,205 @@ -/* - * CMap.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 "CMapHeader.h" -#include "../MetaString.h" -#include "../mapObjects/MiscObjects.h" // To serialize static props -#include "../mapObjects/CQuest.h" // To serialize static props -#include "../mapObjects/CGTownInstance.h" // To serialize static props -#include "CMapDefines.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CArtifactInstance; -class CGObjectInstance; -class CGHeroInstance; -class CCommanderInstance; -class CGCreature; -class CQuest; -class CGTownInstance; -class IModableArt; -class IQuestObject; -class CInputStream; -class CMapEditManager; -class JsonSerializeFormat; -struct TeleportChannel; - -/// The rumor struct consists of a rumor name and text. -struct DLL_LINKAGE Rumor -{ - std::string name; - MetaString text; - - Rumor() = default; - ~Rumor() = default; - - template - void serialize(Handler & h, const int version) - { - h & name; - h & text; - } - - void serializeJson(JsonSerializeFormat & handler); -}; - -/// The disposed hero struct describes which hero can be hired from which player. -struct DLL_LINKAGE DisposedHero -{ - DisposedHero(); - - HeroTypeID heroId; - HeroTypeID portrait; /// The portrait id of the hero, -1 is default. - std::string name; - std::set players; /// Who can hire this hero (bitfield). - - template - void serialize(Handler & h, const int version) - { - h & heroId; - h & portrait; - h & name; - h & players; - } -}; - -/// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... -class DLL_LINKAGE CMap : public CMapHeader -{ -public: - CMap(); - ~CMap(); - void initTerrain(); - - CMapEditManager * getEditManager(); - TerrainTile & getTile(const int3 & tile); - const TerrainTile & getTile(const int3 & tile) const; - bool isCoastalTile(const int3 & pos) const; - bool isInTheMap(const int3 & pos) const; - bool isWaterTile(const int3 & pos) const; - - bool canMoveBetween(const int3 &src, const int3 &dst) const; - bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; - int3 guardingCreaturePosition (int3 pos) const; - - void addBlockVisTiles(CGObjectInstance * obj); - void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); - void calculateGuardingGreaturePositions(); - - void addNewArtifactInstance(ConstTransitivePtr art); - void eraseArtifactInstance(CArtifactInstance * art); - - void addNewQuestInstance(CQuest * quest); - void removeQuestInstance(CQuest * quest); - - void setUniqueInstanceName(CGObjectInstance * obj); - ///Use only this method when creating new map object instances - void addNewObject(CGObjectInstance * obj); - void moveObject(CGObjectInstance * obj, const int3 & dst); - void removeObject(CGObjectInstance * obj); - - bool isWaterMap() const; - bool calculateWaterContent(); - void banWaterArtifacts(); - void banWaterHeroes(); - void banHero(const HeroTypeID& id); - void banWaterSpells(); - void banWaterSkills(); - void banWaterContent(); - - /// Gets object of specified type on requested position - const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); - CGHeroInstance * getHero(int heroId); - - /// Sets the victory/loss condition objectives ?? - void checkForObjectives(); - - void resetStaticData(); - - ui32 checksum; - std::vector rumors; - std::vector disposedHeroes; - std::vector > predefinedHeroes; - std::vector allowedSpells; - std::vector allowedArtifact; - std::vector allowedAbilities; - std::list events; - int3 grailPos; - int grailRadius; - - //Central lists of items in game. Position of item in the vectors below is their (instance) id. - std::vector< ConstTransitivePtr > objects; - std::vector< ConstTransitivePtr > towns; - std::vector< ConstTransitivePtr > artInstances; - std::vector< ConstTransitivePtr > quests; - std::vector< ConstTransitivePtr > allHeroes; //indexed by [hero_type_id]; on map, disposed, prisons, etc. - - //Helper lists - std::vector< ConstTransitivePtr > heroesOnMap; - std::map > teleportChannels; - - /// associative list to identify which hero/creature id belongs to which object id(index for objects) - std::map questIdentifierToId; - - std::unique_ptr editManager; - boost::multi_array guardingCreaturePositions; - - std::map > instanceNames; - - bool waterMap; - -private: - /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground - boost::multi_array terrain; - - si32 uidCounter; //TODO: initialize when loading an old map - -public: - template - void serialize(Handler &h, const int formatVersion) - { - h & static_cast(*this); - h & triggeredEvents; //from CMapHeader - h & rumors; - h & allowedSpells; - h & allowedAbilities; - h & allowedArtifact; - h & events; - h & grailPos; - h & artInstances; - h & quests; - h & allHeroes; - h & questIdentifierToId; - - //TODO: viccondetails - h & terrain; - h & guardingCreaturePositions; - - h & objects; - h & heroesOnMap; - h & teleportChannels; - h & towns; - h & artInstances; - - // static members - h & CGKeys::playerKeyMap; - h & CGMagi::eyelist; - h & CGObelisk::obeliskCount; - h & CGObelisk::visited; - h & CGTownInstance::merchantArtifacts; - h & CGTownInstance::universitySkills; - - h & instanceNames; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMap.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 "CMapHeader.h" +#include "../MetaString.h" +#include "../mapObjects/MiscObjects.h" // To serialize static props +#include "../mapObjects/CQuest.h" // To serialize static props +#include "../mapObjects/CGTownInstance.h" // To serialize static props +#include "CMapDefines.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArtifactInstance; +class CGObjectInstance; +class CGHeroInstance; +class CCommanderInstance; +class CGCreature; +class CQuest; +class CGTownInstance; +class IModableArt; +class IQuestObject; +class CInputStream; +class CMapEditManager; +class JsonSerializeFormat; +struct TeleportChannel; + +/// The rumor struct consists of a rumor name and text. +struct DLL_LINKAGE Rumor +{ + std::string name; + MetaString text; + + Rumor() = default; + ~Rumor() = default; + + template + void serialize(Handler & h, const int version) + { + h & name; + h & text; + } + + void serializeJson(JsonSerializeFormat & handler); +}; + +/// The disposed hero struct describes which hero can be hired from which player. +struct DLL_LINKAGE DisposedHero +{ + DisposedHero(); + + HeroTypeID heroId; + HeroTypeID portrait; /// The portrait id of the hero, -1 is default. + std::string name; + std::set players; /// Who can hire this hero (bitfield). + + template + void serialize(Handler & h, const int version) + { + h & heroId; + h & portrait; + h & name; + h & players; + } +}; + +/// The map contains the map header, the tiles of the terrain, objects, heroes, towns, rumors... +class DLL_LINKAGE CMap : public CMapHeader +{ +public: + CMap(); + ~CMap(); + void initTerrain(); + + CMapEditManager * getEditManager(); + TerrainTile & getTile(const int3 & tile); + const TerrainTile & getTile(const int3 & tile) const; + bool isCoastalTile(const int3 & pos) const; + bool isInTheMap(const int3 & pos) const; + bool isWaterTile(const int3 & pos) const; + + bool canMoveBetween(const int3 &src, const int3 &dst) const; + bool checkForVisitableDir(const int3 & src, const TerrainTile * pom, const int3 & dst) const; + int3 guardingCreaturePosition (int3 pos) const; + + void addBlockVisTiles(CGObjectInstance * obj); + void removeBlockVisTiles(CGObjectInstance * obj, bool total = false); + void calculateGuardingGreaturePositions(); + + void addNewArtifactInstance(ConstTransitivePtr art); + void eraseArtifactInstance(CArtifactInstance * art); + + void addNewQuestInstance(CQuest * quest); + void removeQuestInstance(CQuest * quest); + + void setUniqueInstanceName(CGObjectInstance * obj); + ///Use only this method when creating new map object instances + void addNewObject(CGObjectInstance * obj); + void moveObject(CGObjectInstance * obj, const int3 & dst); + void removeObject(CGObjectInstance * obj); + + bool isWaterMap() const; + bool calculateWaterContent(); + void banWaterArtifacts(); + void banWaterHeroes(); + void banHero(const HeroTypeID& id); + void banWaterSpells(); + void banWaterSkills(); + void banWaterContent(); + + /// Gets object of specified type on requested position + const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); + CGHeroInstance * getHero(int heroId); + + /// Sets the victory/loss condition objectives ?? + void checkForObjectives(); + + void resetStaticData(); + + ui32 checksum; + std::vector rumors; + std::vector disposedHeroes; + std::vector > predefinedHeroes; + std::vector allowedSpells; + std::vector allowedArtifact; + std::vector allowedAbilities; + std::list events; + int3 grailPos; + int grailRadius; + + //Central lists of items in game. Position of item in the vectors below is their (instance) id. + std::vector< ConstTransitivePtr > objects; + std::vector< ConstTransitivePtr > towns; + std::vector< ConstTransitivePtr > artInstances; + std::vector< ConstTransitivePtr > quests; + std::vector< ConstTransitivePtr > allHeroes; //indexed by [hero_type_id]; on map, disposed, prisons, etc. + + //Helper lists + std::vector< ConstTransitivePtr > heroesOnMap; + std::map > teleportChannels; + + /// associative list to identify which hero/creature id belongs to which object id(index for objects) + std::map questIdentifierToId; + + std::unique_ptr editManager; + boost::multi_array guardingCreaturePositions; + + std::map > instanceNames; + + bool waterMap; + +private: + /// a 3-dimensional array of terrain tiles, access is as follows: x, y, level. where level=1 is underground + boost::multi_array terrain; + + si32 uidCounter; //TODO: initialize when loading an old map + +public: + template + void serialize(Handler &h, const int formatVersion) + { + h & static_cast(*this); + h & triggeredEvents; //from CMapHeader + h & rumors; + h & allowedSpells; + h & allowedAbilities; + h & allowedArtifact; + h & events; + h & grailPos; + h & artInstances; + h & quests; + h & allHeroes; + h & questIdentifierToId; + + //TODO: viccondetails + h & terrain; + h & guardingCreaturePositions; + + h & objects; + h & heroesOnMap; + h & teleportChannels; + h & towns; + h & artInstances; + + // static members + h & CGKeys::playerKeyMap; + h & CGMagi::eyelist; + h & CGObelisk::obeliskCount; + h & CGObelisk::visited; + h & CGTownInstance::merchantArtifacts; + h & CGTownInstance::universitySkills; + + h & instanceNames; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapDefines.h b/lib/mapping/CMapDefines.h index f36a0889e..85a2da1a1 100644 --- a/lib/mapping/CMapDefines.h +++ b/lib/mapping/CMapDefines.h @@ -1,131 +1,131 @@ -/* - * CMapDefines.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 "../ResourceSet.h" -#include "../MetaString.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class TerrainType; -class RiverType; -class RoadType; -class CGObjectInstance; -class CGTownInstance; -class JsonSerializeFormat; - -/// The map event is an event which e.g. gives or takes resources of a specific -/// amount to/from players and can appear regularly or once a time. -class DLL_LINKAGE CMapEvent -{ -public: - CMapEvent(); - virtual ~CMapEvent() = default; - - bool earlierThan(const CMapEvent & other) const; - bool earlierThanOrEqual(const CMapEvent & other) const; - - std::string name; - MetaString message; - TResources resources; - ui8 players; // affected players, bit field? - ui8 humanAffected; - ui8 computerAffected; - ui32 firstOccurence; - ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time - - template - void serialize(Handler & h, const int version) - { - h & name; - h & message; - h & resources; - h & players; - h & humanAffected; - h & computerAffected; - h & firstOccurence; - h & nextOccurence; - } - - virtual void serializeJson(JsonSerializeFormat & handler); -}; - -/// The castle event builds/adds buildings/creatures for a specific town. -class DLL_LINKAGE CCastleEvent: public CMapEvent -{ -public: - CCastleEvent() = default; - - std::set buildings; - std::vector creatures; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & buildings; - h & creatures; - } - - void serializeJson(JsonSerializeFormat & handler) override; -}; - -/// The terrain tile describes the terrain type and the visual representation of the terrain. -/// Furthermore the struct defines whether the tile is visitable or/and blocked and which objects reside in it. -struct DLL_LINKAGE TerrainTile -{ - TerrainTile(); - - /// Gets true if the terrain is not a rock. If from is water/land, same type is also required. - bool entrableTerrain(const TerrainTile * from = nullptr) const; - bool entrableTerrain(bool allowLand, bool allowSea) const; - /// Checks for blocking objects and terraint type (water / land). - bool isClear(const TerrainTile * from = nullptr) const; - /// Gets the ID of the top visitable object or -1 if there is none. - Obj topVisitableId(bool excludeTop = false) const; - CGObjectInstance * topVisitableObj(bool excludeTop = false) const; - bool isWater() const; - EDiggingStatus getDiggingStatus(const bool excludeTop = true) const; - bool hasFavorableWinds() const; - - const TerrainType * terType; - ui8 terView; - const RiverType * riverType; - ui8 riverDir; - const RoadType * roadType; - ui8 roadDir; - /// first two bits - how to rotate terrain graphic (next two - river graphic, next two - road); - /// 7th bit - whether tile is coastal (allows disembarking if land or block movement if water); 8th bit - Favorable Winds effect - ui8 extTileFlags; - bool visitable; - bool blocked; - - std::vector visitableObjects; - std::vector blockingObjects; - - template - void serialize(Handler & h, const int version) - { - h & terType; - h & terView; - h & riverType; - h & riverDir; - h & roadType; - h & roadDir; - h & extTileFlags; - h & visitable; - h & blocked; - h & visitableObjects; - h & blockingObjects; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMapDefines.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 "../ResourceSet.h" +#include "../MetaString.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class TerrainType; +class RiverType; +class RoadType; +class CGObjectInstance; +class CGTownInstance; +class JsonSerializeFormat; + +/// The map event is an event which e.g. gives or takes resources of a specific +/// amount to/from players and can appear regularly or once a time. +class DLL_LINKAGE CMapEvent +{ +public: + CMapEvent(); + virtual ~CMapEvent() = default; + + bool earlierThan(const CMapEvent & other) const; + bool earlierThanOrEqual(const CMapEvent & other) const; + + std::string name; + MetaString message; + TResources resources; + ui8 players; // affected players, bit field? + ui8 humanAffected; + ui8 computerAffected; + ui32 firstOccurence; + ui32 nextOccurence; /// specifies after how many days the event will occur the next time; 0 if event occurs only one time + + template + void serialize(Handler & h, const int version) + { + h & name; + h & message; + h & resources; + h & players; + h & humanAffected; + h & computerAffected; + h & firstOccurence; + h & nextOccurence; + } + + virtual void serializeJson(JsonSerializeFormat & handler); +}; + +/// The castle event builds/adds buildings/creatures for a specific town. +class DLL_LINKAGE CCastleEvent: public CMapEvent +{ +public: + CCastleEvent() = default; + + std::set buildings; + std::vector creatures; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & buildings; + h & creatures; + } + + void serializeJson(JsonSerializeFormat & handler) override; +}; + +/// The terrain tile describes the terrain type and the visual representation of the terrain. +/// Furthermore the struct defines whether the tile is visitable or/and blocked and which objects reside in it. +struct DLL_LINKAGE TerrainTile +{ + TerrainTile(); + + /// Gets true if the terrain is not a rock. If from is water/land, same type is also required. + bool entrableTerrain(const TerrainTile * from = nullptr) const; + bool entrableTerrain(bool allowLand, bool allowSea) const; + /// Checks for blocking objects and terraint type (water / land). + bool isClear(const TerrainTile * from = nullptr) const; + /// Gets the ID of the top visitable object or -1 if there is none. + Obj topVisitableId(bool excludeTop = false) const; + CGObjectInstance * topVisitableObj(bool excludeTop = false) const; + bool isWater() const; + EDiggingStatus getDiggingStatus(const bool excludeTop = true) const; + bool hasFavorableWinds() const; + + const TerrainType * terType; + ui8 terView; + const RiverType * riverType; + ui8 riverDir; + const RoadType * roadType; + ui8 roadDir; + /// first two bits - how to rotate terrain graphic (next two - river graphic, next two - road); + /// 7th bit - whether tile is coastal (allows disembarking if land or block movement if water); 8th bit - Favorable Winds effect + ui8 extTileFlags; + bool visitable; + bool blocked; + + std::vector visitableObjects; + std::vector blockingObjects; + + template + void serialize(Handler & h, const int version) + { + h & terType; + h & terView; + h & riverType; + h & riverDir; + h & roadType; + h & roadDir; + h & extTileFlags; + h & visitable; + h & blocked; + h & visitableObjects; + h & blockingObjects; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index 8fecd28e9..ede7a16e6 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -1,208 +1,208 @@ -/* - * CMapInfo.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 "CMapInfo.h" - -#include - -#include "../filesystem/ResourcePath.h" -#include "../StartInfo.h" -#include "../GameConstants.h" -#include "CMapService.h" -#include "CMapHeader.h" -#include "MapFormat.h" - -#include "../campaign/CampaignHandler.h" -#include "../filesystem/Filesystem.h" -#include "../serializer/CMemorySerializer.h" -#include "../CGeneralTextHandler.h" -#include "../rmg/CMapGenOptions.h" -#include "../CCreatureHandler.h" -#include "../GameSettings.h" -#include "../CHeroHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CMapInfo::CMapInfo() - : scenarioOptionsOfSave(nullptr), amountOfPlayersOnMap(0), amountOfHumanControllablePlayers(0), amountOfHumanPlayersInSave(0), isRandomMap(false) -{ - -} - -CMapInfo::~CMapInfo() -{ - vstd::clear_pointer(scenarioOptionsOfSave); -} - -void CMapInfo::mapInit(const std::string & fname) -{ - fileURI = fname; - CMapService mapService; - ResourcePath resource = ResourcePath(fname, EResType::MAP); - originalFileURI = resource.getOriginalName(); - fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); - mapHeader = mapService.loadMapHeader(resource); - countPlayers(); -} - -void CMapInfo::saveInit(const ResourcePath & file) -{ - CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); - lf.checkMagicBytes(SAVEGAME_MAGIC); - - mapHeader = std::make_unique(); - lf >> *(mapHeader) >> scenarioOptionsOfSave; - fileURI = file.getName(); - originalFileURI = file.getOriginalName(); - fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); - countPlayers(); - std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); - date = vstd::getFormattedDateTime(time); - - // We absolutely not need this data for lobby and server will read it from save - // FIXME: actually we don't want them in CMapHeader! - mapHeader->triggeredEvents.clear(); -} - -void CMapInfo::campaignInit() -{ - ResourcePath resource = ResourcePath(fileURI, EResType::CAMPAIGN); - originalFileURI = resource.getOriginalName(); - fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); - campaign = CampaignHandler::getHeader(fileURI); -} - -void CMapInfo::countPlayers() -{ - for(int i=0; iplayers[i].canHumanPlay) - { - amountOfPlayersOnMap++; - amountOfHumanControllablePlayers++; - } - else if(mapHeader->players[i].canComputerPlay) - { - amountOfPlayersOnMap++; - } - } - - if(scenarioOptionsOfSave) - for(const auto & playerInfo : scenarioOptionsOfSave->playerInfos) - if(playerInfo.second.isControlledByHuman()) - amountOfHumanPlayersInSave++; -} - -std::string CMapInfo::getNameTranslated() const -{ - if(campaign && !campaign->getNameTranslated().empty()) - return campaign->getNameTranslated(); - else if(mapHeader && !mapHeader->name.empty()) - { - mapHeader->registerMapStrings(); - return mapHeader->name.toString(); - } - else - return VLC->generaltexth->allTexts[508]; -} - -std::string CMapInfo::getNameForList() const -{ - if(scenarioOptionsOfSave) - { - // TODO: this could be handled differently - std::vector path; - boost::split(path, originalFileURI, boost::is_any_of("\\/")); - return path[path.size()-1]; - } - else - { - return getNameTranslated(); - } -} - -std::string CMapInfo::getDescriptionTranslated() const -{ - if(campaign) - return campaign->getDescriptionTranslated(); - else - return mapHeader->description.toString(); -} - -int CMapInfo::getMapSizeIconId() const -{ - if(!mapHeader) - return 4; - - switch(mapHeader->width) - { - case CMapHeader::MAP_SIZE_SMALL: - return 0; - case CMapHeader::MAP_SIZE_MIDDLE: - return 1; - case CMapHeader::MAP_SIZE_LARGE: - return 2; - case CMapHeader::MAP_SIZE_XLARGE: - return 3; - case CMapHeader::MAP_SIZE_HUGE: - return 4; - case CMapHeader::MAP_SIZE_XHUGE: - return 5; - case CMapHeader::MAP_SIZE_GIANT: - return 6; - default: - return 4; - } -} - -int CMapInfo::getMapSizeFormatIconId() const -{ - switch(mapHeader->version) - { - case EMapFormat::ROE: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); - case EMapFormat::AB: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); - case EMapFormat::SOD: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); - case EMapFormat::WOG: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer(); - case EMapFormat::HOTA: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["iconIndex"].Integer(); - case EMapFormat::VCMI: - return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); - } - return 0; -} - -std::string CMapInfo::getMapSizeName() const -{ - switch(mapHeader->width) - { - case CMapHeader::MAP_SIZE_SMALL: - return "S"; - case CMapHeader::MAP_SIZE_MIDDLE: - return "M"; - case CMapHeader::MAP_SIZE_LARGE: - return "L"; - case CMapHeader::MAP_SIZE_XLARGE: - return "XL"; - case CMapHeader::MAP_SIZE_HUGE: - return "H"; - case CMapHeader::MAP_SIZE_XHUGE: - return "XH"; - case CMapHeader::MAP_SIZE_GIANT: - return "G"; - default: - return "C"; - } -} - -VCMI_LIB_NAMESPACE_END +/* + * CMapInfo.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 "CMapInfo.h" + +#include + +#include "../filesystem/ResourcePath.h" +#include "../StartInfo.h" +#include "../GameConstants.h" +#include "CMapService.h" +#include "CMapHeader.h" +#include "MapFormat.h" + +#include "../campaign/CampaignHandler.h" +#include "../filesystem/Filesystem.h" +#include "../serializer/CMemorySerializer.h" +#include "../CGeneralTextHandler.h" +#include "../rmg/CMapGenOptions.h" +#include "../CCreatureHandler.h" +#include "../GameSettings.h" +#include "../CHeroHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CMapInfo::CMapInfo() + : scenarioOptionsOfSave(nullptr), amountOfPlayersOnMap(0), amountOfHumanControllablePlayers(0), amountOfHumanPlayersInSave(0), isRandomMap(false) +{ + +} + +CMapInfo::~CMapInfo() +{ + vstd::clear_pointer(scenarioOptionsOfSave); +} + +void CMapInfo::mapInit(const std::string & fname) +{ + fileURI = fname; + CMapService mapService; + ResourcePath resource = ResourcePath(fname, EResType::MAP); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); + mapHeader = mapService.loadMapHeader(resource); + countPlayers(); +} + +void CMapInfo::saveInit(const ResourcePath & file) +{ + CLoadFile lf(*CResourceHandler::get()->getResourceName(file), MINIMAL_SERIALIZATION_VERSION); + lf.checkMagicBytes(SAVEGAME_MAGIC); + + mapHeader = std::make_unique(); + lf >> *(mapHeader) >> scenarioOptionsOfSave; + fileURI = file.getName(); + originalFileURI = file.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(file)).string(); + countPlayers(); + std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(file)); + date = vstd::getFormattedDateTime(time); + + // We absolutely not need this data for lobby and server will read it from save + // FIXME: actually we don't want them in CMapHeader! + mapHeader->triggeredEvents.clear(); +} + +void CMapInfo::campaignInit() +{ + ResourcePath resource = ResourcePath(fileURI, EResType::CAMPAIGN); + originalFileURI = resource.getOriginalName(); + fullFileURI = boost::filesystem::canonical(*CResourceHandler::get()->getResourceName(resource)).string(); + campaign = CampaignHandler::getHeader(fileURI); +} + +void CMapInfo::countPlayers() +{ + for(int i=0; iplayers[i].canHumanPlay) + { + amountOfPlayersOnMap++; + amountOfHumanControllablePlayers++; + } + else if(mapHeader->players[i].canComputerPlay) + { + amountOfPlayersOnMap++; + } + } + + if(scenarioOptionsOfSave) + for(const auto & playerInfo : scenarioOptionsOfSave->playerInfos) + if(playerInfo.second.isControlledByHuman()) + amountOfHumanPlayersInSave++; +} + +std::string CMapInfo::getNameTranslated() const +{ + if(campaign && !campaign->getNameTranslated().empty()) + return campaign->getNameTranslated(); + else if(mapHeader && !mapHeader->name.empty()) + { + mapHeader->registerMapStrings(); + return mapHeader->name.toString(); + } + else + return VLC->generaltexth->allTexts[508]; +} + +std::string CMapInfo::getNameForList() const +{ + if(scenarioOptionsOfSave) + { + // TODO: this could be handled differently + std::vector path; + boost::split(path, originalFileURI, boost::is_any_of("\\/")); + return path[path.size()-1]; + } + else + { + return getNameTranslated(); + } +} + +std::string CMapInfo::getDescriptionTranslated() const +{ + if(campaign) + return campaign->getDescriptionTranslated(); + else + return mapHeader->description.toString(); +} + +int CMapInfo::getMapSizeIconId() const +{ + if(!mapHeader) + return 4; + + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return 0; + case CMapHeader::MAP_SIZE_MIDDLE: + return 1; + case CMapHeader::MAP_SIZE_LARGE: + return 2; + case CMapHeader::MAP_SIZE_XLARGE: + return 3; + case CMapHeader::MAP_SIZE_HUGE: + return 4; + case CMapHeader::MAP_SIZE_XHUGE: + return 5; + case CMapHeader::MAP_SIZE_GIANT: + return 6; + default: + return 4; + } +} + +int CMapInfo::getMapSizeFormatIconId() const +{ + switch(mapHeader->version) + { + case EMapFormat::ROE: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)["iconIndex"].Integer(); + case EMapFormat::AB: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)["iconIndex"].Integer(); + case EMapFormat::SOD: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)["iconIndex"].Integer(); + case EMapFormat::WOG: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)["iconIndex"].Integer(); + case EMapFormat::HOTA: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)["iconIndex"].Integer(); + case EMapFormat::VCMI: + return VLC->settings()->getValue(EGameSettings::MAP_FORMAT_JSON_VCMI)["iconIndex"].Integer(); + } + return 0; +} + +std::string CMapInfo::getMapSizeName() const +{ + switch(mapHeader->width) + { + case CMapHeader::MAP_SIZE_SMALL: + return "S"; + case CMapHeader::MAP_SIZE_MIDDLE: + return "M"; + case CMapHeader::MAP_SIZE_LARGE: + return "L"; + case CMapHeader::MAP_SIZE_XLARGE: + return "XL"; + case CMapHeader::MAP_SIZE_HUGE: + return "H"; + case CMapHeader::MAP_SIZE_XHUGE: + return "XH"; + case CMapHeader::MAP_SIZE_GIANT: + return "G"; + default: + return "C"; + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/CMapInfo.h b/lib/mapping/CMapInfo.h index ba09a3c15..512cff481 100644 --- a/lib/mapping/CMapInfo.h +++ b/lib/mapping/CMapInfo.h @@ -1,74 +1,74 @@ -/* - * CMapInfo.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 - -VCMI_LIB_NAMESPACE_BEGIN - -struct StartInfo; - -class CMapHeader; -class Campaign; -class ResourcePath; - -/** - * A class which stores the count of human players and all players, the filename, - * scenario options, the map header information,... - */ -class DLL_LINKAGE CMapInfo -{ -public: - std::unique_ptr mapHeader; //may be nullptr if campaign - std::unique_ptr campaign; //may be nullptr if scenario - StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) - std::string fileURI; - std::string originalFileURI; - std::string fullFileURI; - std::string date; - int amountOfPlayersOnMap; - int amountOfHumanControllablePlayers; - int amountOfHumanPlayersInSave; - bool isRandomMap; - - CMapInfo(); - virtual ~CMapInfo(); - - CMapInfo(CMapInfo &&other) = delete; - CMapInfo(const CMapInfo &other) = delete; - - CMapInfo &operator=(CMapInfo &&other) = delete; - CMapInfo &operator=(const CMapInfo &other) = delete; - - void mapInit(const std::string & fname); - void saveInit(const ResourcePath & file); - void campaignInit(); - void countPlayers(); - - std::string getNameTranslated() const; - std::string getNameForList() const; - std::string getDescriptionTranslated() const; - int getMapSizeIconId() const; - int getMapSizeFormatIconId() const; - std::string getMapSizeName() const; - - template void serialize(Handler &h, const int Version) - { - h & mapHeader; - h & campaign; - h & scenarioOptionsOfSave; - h & fileURI; - h & date; - h & amountOfPlayersOnMap; - h & amountOfHumanControllablePlayers; - h & amountOfHumanPlayersInSave; - h & isRandomMap; - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CMapInfo.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 + +VCMI_LIB_NAMESPACE_BEGIN + +struct StartInfo; + +class CMapHeader; +class Campaign; +class ResourcePath; + +/** + * A class which stores the count of human players and all players, the filename, + * scenario options, the map header information,... + */ +class DLL_LINKAGE CMapInfo +{ +public: + std::unique_ptr mapHeader; //may be nullptr if campaign + std::unique_ptr campaign; //may be nullptr if scenario + StartInfo * scenarioOptionsOfSave; // Options with which scenario has been started (used only with saved games) + std::string fileURI; + std::string originalFileURI; + std::string fullFileURI; + std::string date; + int amountOfPlayersOnMap; + int amountOfHumanControllablePlayers; + int amountOfHumanPlayersInSave; + bool isRandomMap; + + CMapInfo(); + virtual ~CMapInfo(); + + CMapInfo(CMapInfo &&other) = delete; + CMapInfo(const CMapInfo &other) = delete; + + CMapInfo &operator=(CMapInfo &&other) = delete; + CMapInfo &operator=(const CMapInfo &other) = delete; + + void mapInit(const std::string & fname); + void saveInit(const ResourcePath & file); + void campaignInit(); + void countPlayers(); + + std::string getNameTranslated() const; + std::string getNameForList() const; + std::string getDescriptionTranslated() const; + int getMapSizeIconId() const; + int getMapSizeFormatIconId() const; + std::string getMapSizeName() const; + + template void serialize(Handler &h, const int Version) + { + h & mapHeader; + h & campaign; + h & scenarioOptionsOfSave; + h & fileURI; + h & date; + h & amountOfPlayersOnMap; + h & amountOfHumanControllablePlayers; + h & amountOfHumanPlayersInSave; + h & isRandomMap; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index f1f63e12f..4152e16cd 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -1,158 +1,158 @@ -/* - * MapFeaturesH3M.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 "MapFeaturesH3M.h" - -#include "MapFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hotaVersion) -{ - switch(format) - { - case EMapFormat::ROE: - return getFeaturesROE(); - case EMapFormat::AB: - return getFeaturesAB(); - case EMapFormat::SOD: - return getFeaturesSOD(); - case EMapFormat::WOG: - return getFeaturesWOG(); - case EMapFormat::HOTA: - return getFeaturesHOTA(hotaVersion); - default: - throw std::runtime_error("Invalid map format!"); - } -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() -{ - MapFormatFeaturesH3M result; - result.levelROE = true; - - result.factionsBytes = 1; - result.heroesBytes = 16; - result.artifactsBytes = 16; - result.skillsBytes = 4; - result.resourcesBytes = 4; - result.spellsBytes = 9; - result.buildingsBytes = 6; - - result.factionsCount = 8; - result.heroesCount = 128; - result.heroesPortraitsCount = 130; // +General Kendal, +Catherine (portrait-only in RoE) - result.artifactsCount = 127; - result.resourcesCount = 7; - result.creaturesCount = 118; - result.spellsCount = 70; - result.skillsCount = 28; - result.terrainsCount = 10; - result.artifactSlotsCount = 18; - result.buildingsCount = 41; - result.roadsCount = 3; - result.riversCount = 4; - - result.heroIdentifierInvalid = 0xff; - result.artifactIdentifierInvalid = 0xff; - result.creatureIdentifierInvalid = 0xff; - result.spellIdentifierInvalid = 0xff; - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesAB() -{ - MapFormatFeaturesH3M result = getFeaturesROE(); - result.levelAB = true; - - result.factionsBytes = 2; // + Conflux - result.factionsCount = 9; - - result.creaturesCount = 145; // + Conflux and new neutrals - - result.heroesCount = 156; // + Conflux and campaign heroes - result.heroesPortraitsCount = 159; // +Kendal, +young Cristian, +Ordwald - result.heroesBytes = 20; - - result.artifactsCount = 129; // + Armaggedon Blade and Vial of Dragon Blood - result.artifactsBytes = 17; - - result.artifactIdentifierInvalid = 0xffff; // Now uses 2 bytes / object - result.creatureIdentifierInvalid = 0xffff; // Now uses 2 bytes / object - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() -{ - MapFormatFeaturesH3M result = getFeaturesAB(); - result.levelSOD = true; - - result.artifactsCount = 141; // + Combined artifacts - result.artifactsBytes = 18; - - result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog - - result.artifactSlotsCount = 19; // + MISC_5 slot - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() -{ - MapFormatFeaturesH3M result = getFeaturesSOD(); - result.levelWOG = true; - - return result; -} - -MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion) -{ - // even if changes are minimal, we might not be able to parse map header in map selection screen - // throw exception - to be catched by map selection screen & excluded as invalid - if(hotaVersion > 3) - throw std::runtime_error("Invalid map format!"); - - MapFormatFeaturesH3M result = getFeaturesSOD(); - result.levelHOTA0 = true; - result.levelHOTA1 = hotaVersion > 0; - //result.levelHOTA2 = hotaVersion > 1; // HOTA2 seems to be identical to HOTA1 so far - result.levelHOTA3 = hotaVersion > 2; - - result.artifactsBytes = 21; - result.heroesBytes = 23; - - result.terrainsCount = 12; // +Highlands +Wasteland - result.skillsCount = 29; // + Interference - result.factionsCount = 10; // + Cove - result.creaturesCount = 171; // + Cove + neutrals - - if(hotaVersion < 3) - { - result.artifactsCount = 163; // + HotA artifacts - result.heroesCount = 178; // + Cove - result.heroesPortraitsCount = 186; // + Cove - } - if(hotaVersion == 3) - { - result.artifactsCount = 165; // + HotA artifacts - result.heroesCount = 179; // + Cove + Giselle - result.heroesPortraitsCount = 188; // + Cove + Giselle - } - - assert((result.heroesCount + 7) / 8 == result.heroesBytes); - assert((result.artifactsCount + 7) / 8 == result.artifactsBytes); - - return result; -} - -VCMI_LIB_NAMESPACE_END +/* + * MapFeaturesH3M.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 "MapFeaturesH3M.h" + +#include "MapFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +MapFormatFeaturesH3M MapFormatFeaturesH3M::find(EMapFormat format, uint32_t hotaVersion) +{ + switch(format) + { + case EMapFormat::ROE: + return getFeaturesROE(); + case EMapFormat::AB: + return getFeaturesAB(); + case EMapFormat::SOD: + return getFeaturesSOD(); + case EMapFormat::WOG: + return getFeaturesWOG(); + case EMapFormat::HOTA: + return getFeaturesHOTA(hotaVersion); + default: + throw std::runtime_error("Invalid map format!"); + } +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesROE() +{ + MapFormatFeaturesH3M result; + result.levelROE = true; + + result.factionsBytes = 1; + result.heroesBytes = 16; + result.artifactsBytes = 16; + result.skillsBytes = 4; + result.resourcesBytes = 4; + result.spellsBytes = 9; + result.buildingsBytes = 6; + + result.factionsCount = 8; + result.heroesCount = 128; + result.heroesPortraitsCount = 130; // +General Kendal, +Catherine (portrait-only in RoE) + result.artifactsCount = 127; + result.resourcesCount = 7; + result.creaturesCount = 118; + result.spellsCount = 70; + result.skillsCount = 28; + result.terrainsCount = 10; + result.artifactSlotsCount = 18; + result.buildingsCount = 41; + result.roadsCount = 3; + result.riversCount = 4; + + result.heroIdentifierInvalid = 0xff; + result.artifactIdentifierInvalid = 0xff; + result.creatureIdentifierInvalid = 0xff; + result.spellIdentifierInvalid = 0xff; + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesAB() +{ + MapFormatFeaturesH3M result = getFeaturesROE(); + result.levelAB = true; + + result.factionsBytes = 2; // + Conflux + result.factionsCount = 9; + + result.creaturesCount = 145; // + Conflux and new neutrals + + result.heroesCount = 156; // + Conflux and campaign heroes + result.heroesPortraitsCount = 159; // +Kendal, +young Cristian, +Ordwald + result.heroesBytes = 20; + + result.artifactsCount = 129; // + Armaggedon Blade and Vial of Dragon Blood + result.artifactsBytes = 17; + + result.artifactIdentifierInvalid = 0xffff; // Now uses 2 bytes / object + result.creatureIdentifierInvalid = 0xffff; // Now uses 2 bytes / object + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() +{ + MapFormatFeaturesH3M result = getFeaturesAB(); + result.levelSOD = true; + + result.artifactsCount = 141; // + Combined artifacts + result.artifactsBytes = 18; + + result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog + + result.artifactSlotsCount = 19; // + MISC_5 slot + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesWOG() +{ + MapFormatFeaturesH3M result = getFeaturesSOD(); + result.levelWOG = true; + + return result; +} + +MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesHOTA(uint32_t hotaVersion) +{ + // even if changes are minimal, we might not be able to parse map header in map selection screen + // throw exception - to be catched by map selection screen & excluded as invalid + if(hotaVersion > 3) + throw std::runtime_error("Invalid map format!"); + + MapFormatFeaturesH3M result = getFeaturesSOD(); + result.levelHOTA0 = true; + result.levelHOTA1 = hotaVersion > 0; + //result.levelHOTA2 = hotaVersion > 1; // HOTA2 seems to be identical to HOTA1 so far + result.levelHOTA3 = hotaVersion > 2; + + result.artifactsBytes = 21; + result.heroesBytes = 23; + + result.terrainsCount = 12; // +Highlands +Wasteland + result.skillsCount = 29; // + Interference + result.factionsCount = 10; // + Cove + result.creaturesCount = 171; // + Cove + neutrals + + if(hotaVersion < 3) + { + result.artifactsCount = 163; // + HotA artifacts + result.heroesCount = 178; // + Cove + result.heroesPortraitsCount = 186; // + Cove + } + if(hotaVersion == 3) + { + result.artifactsCount = 165; // + HotA artifacts + result.heroesCount = 179; // + Cove + Giselle + result.heroesPortraitsCount = 188; // + Cove + Giselle + } + + assert((result.heroesCount + 7) / 8 == result.heroesBytes); + assert((result.artifactsCount + 7) / 8 == result.artifactsBytes); + + return result; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFeaturesH3M.h b/lib/mapping/MapFeaturesH3M.h index 75e1515d5..d9fe3fc68 100644 --- a/lib/mapping/MapFeaturesH3M.h +++ b/lib/mapping/MapFeaturesH3M.h @@ -1,73 +1,73 @@ -/* - * MapFeaturesH3M.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 - -VCMI_LIB_NAMESPACE_BEGIN - -enum class EMapFormat : uint8_t; - -struct MapFormatFeaturesH3M -{ -public: - static MapFormatFeaturesH3M find(EMapFormat format, uint32_t hotaVersion); - static MapFormatFeaturesH3M getFeaturesROE(); - static MapFormatFeaturesH3M getFeaturesAB(); - static MapFormatFeaturesH3M getFeaturesSOD(); - static MapFormatFeaturesH3M getFeaturesWOG(); - static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); - - MapFormatFeaturesH3M() = default; - - // number of bytes in bitmask of appropriate type - - int factionsBytes; - int heroesBytes; - int artifactsBytes; - int resourcesBytes; - int skillsBytes; - int spellsBytes; - int buildingsBytes; - - // total number of elements of appropriate type - - int factionsCount; - int heroesCount; - int heroesPortraitsCount; - int artifactsCount; - int resourcesCount; - int creaturesCount; - int spellsCount; - int skillsCount; - int terrainsCount; - int roadsCount; - int riversCount; - int artifactSlotsCount; - int buildingsCount; - - // identifier that should be treated as "invalid", usually - '-1' - - int heroIdentifierInvalid; - int artifactIdentifierInvalid; - int creatureIdentifierInvalid; - int spellIdentifierInvalid; - - // features from which map format are available - - bool levelROE = false; - bool levelAB = false; - bool levelSOD = false; - bool levelWOG = false; - bool levelHOTA0 = false; - bool levelHOTA1 = false; - bool levelHOTA3 = false; -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFeaturesH3M.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 + +VCMI_LIB_NAMESPACE_BEGIN + +enum class EMapFormat : uint8_t; + +struct MapFormatFeaturesH3M +{ +public: + static MapFormatFeaturesH3M find(EMapFormat format, uint32_t hotaVersion); + static MapFormatFeaturesH3M getFeaturesROE(); + static MapFormatFeaturesH3M getFeaturesAB(); + static MapFormatFeaturesH3M getFeaturesSOD(); + static MapFormatFeaturesH3M getFeaturesWOG(); + static MapFormatFeaturesH3M getFeaturesHOTA(uint32_t hotaVersion); + + MapFormatFeaturesH3M() = default; + + // number of bytes in bitmask of appropriate type + + int factionsBytes; + int heroesBytes; + int artifactsBytes; + int resourcesBytes; + int skillsBytes; + int spellsBytes; + int buildingsBytes; + + // total number of elements of appropriate type + + int factionsCount; + int heroesCount; + int heroesPortraitsCount; + int artifactsCount; + int resourcesCount; + int creaturesCount; + int spellsCount; + int skillsCount; + int terrainsCount; + int roadsCount; + int riversCount; + int artifactSlotsCount; + int buildingsCount; + + // identifier that should be treated as "invalid", usually - '-1' + + int heroIdentifierInvalid; + int artifactIdentifierInvalid; + int creatureIdentifierInvalid; + int spellIdentifierInvalid; + + // features from which map format are available + + bool levelROE = false; + bool levelAB = false; + bool levelSOD = false; + bool levelWOG = false; + bool levelHOTA0 = false; + bool levelHOTA1 = false; + bool levelHOTA3 = false; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 3df10a799..449886c34 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1,2325 +1,2325 @@ -/* - * MapFormatH3M.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 "MapFormatH3M.h" - -#include "CMap.h" -#include "MapReaderH3M.h" -#include "MapFormat.h" - -#include "../ArtifactUtils.h" -#include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CHeroHandler.h" -#include "../CSkillHandler.h" -#include "../CStopWatch.h" -#include "../GameSettings.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../TextOperations.h" -#include "../VCMI_Lib.h" -#include "../filesystem/CBinaryReader.h" -#include "../filesystem/Filesystem.h" -#include "../mapObjectConstructors/AObjectTypeHandler.h" -#include "../mapObjectConstructors/CObjectClassesHandler.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../spells/CSpellHandler.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -static std::string convertMapName(std::string input) -{ - boost::algorithm::to_lower(input); - boost::algorithm::trim(input); - boost::algorithm::erase_all(input, "."); - - size_t slashPos = input.find_last_of('/'); - - if(slashPos != std::string::npos) - return input.substr(slashPos + 1); - - return input; -} - -CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream) - : map(nullptr) - , reader(new MapReaderH3M(stream)) - , inputStream(stream) - , mapName(convertMapName(mapName)) - , modName(modName) - , fileEncoding(encodingName) -{ -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CMapLoaderH3M::~CMapLoaderH3M() = default; - -std::unique_ptr CMapLoaderH3M::loadMap() -{ - // Init map object by parsing the input buffer - map = new CMap(); - mapHeader = std::unique_ptr(dynamic_cast(map)); - init(); - - return std::unique_ptr(dynamic_cast(mapHeader.release())); -} - -std::unique_ptr CMapLoaderH3M::loadMapHeader() -{ - // Read header - mapHeader = std::make_unique(); - readHeader(); - - return std::move(mapHeader); -} - -void CMapLoaderH3M::init() -{ - //TODO: get rid of double input process - si64 temp_size = inputStream->getSize(); - inputStream->seek(0); - - auto * temp_buffer = new ui8[temp_size]; - inputStream->read(temp_buffer, temp_size); - - // Compute checksum - boost::crc_32_type result; - result.process_bytes(temp_buffer, temp_size); - map->checksum = result.checksum(); - - delete[] temp_buffer; - inputStream->seek(0); - - readHeader(); - map->allHeroes.resize(map->allowedHeroes.size()); - - readDisposedHeroes(); - readMapOptions(); - readAllowedArtifacts(); - readAllowedSpellsAbilities(); - readRumors(); - readPredefinedHeroes(); - readTerrain(); - readObjectTemplates(); - readObjects(); - readEvents(); - - map->calculateGuardingGreaturePositions(); - afterRead(); - //map->banWaterContent(); //Not sure if force this for custom scenarios -} - -void CMapLoaderH3M::readHeader() -{ - // Map version - mapHeader->version = static_cast(reader->readUInt32()); - - if(mapHeader->version == EMapFormat::HOTA) - { - uint32_t hotaVersion = reader->readUInt32(); - features = MapFormatFeaturesH3M::find(mapHeader->version, hotaVersion); - reader->setFormatLevel(features); - - if(hotaVersion > 0) - { - bool isMirrorMap = reader->readBool(); - bool isArenaMap = reader->readBool(); - - //TODO: HotA - if (isMirrorMap) - logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName); - - if (isArenaMap) - logGlobal->warn("Map '%s': Arena maps are not supported!", mapName); - } - - if(hotaVersion > 1) - { - [[maybe_unused]] uint8_t unknown = reader->readUInt32(); - assert(unknown == 12); - } - } - else - { - features = MapFormatFeaturesH3M::find(mapHeader->version, 0); - reader->setFormatLevel(features); - } - MapIdentifiersH3M identifierMapper; - - if (features.levelROE) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); - if (features.levelAB) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); - if (features.levelSOD) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); - if (features.levelWOG) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); - if (features.levelHOTA0) - identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); - - reader->setIdentifierRemapper(identifierMapper); - - // include basic mod - if(mapHeader->version == EMapFormat::WOG) - mapHeader->mods["wake-of-gods"]; - - // Read map name, description, dimensions,... - mapHeader->areAnyPlayers = reader->readBool(); - mapHeader->height = mapHeader->width = reader->readInt32(); - mapHeader->twoLevel = reader->readBool(); - mapHeader->name.appendTextID(readLocalizedString("header.name")); - mapHeader->description.appendTextID(readLocalizedString("header.description")); - mapHeader->difficulty = reader->readInt8(); - - if(features.levelAB) - mapHeader->levelLimit = reader->readUInt8(); - else - mapHeader->levelLimit = 0; - - readPlayerInfo(); - readVictoryLossConditions(); - readTeamInfo(); - readAllowedHeroes(); -} - -void CMapLoaderH3M::readPlayerInfo() -{ - for(int i = 0; i < mapHeader->players.size(); ++i) - { - auto & playerInfo = mapHeader->players[i]; - - playerInfo.canHumanPlay = reader->readBool(); - playerInfo.canComputerPlay = reader->readBool(); - - // If nobody can play with this player - skip loading of these properties - if((!(playerInfo.canHumanPlay || playerInfo.canComputerPlay))) - { - if(features.levelROE) - reader->skipUnused(6); - if(features.levelAB) - reader->skipUnused(6); - if(features.levelSOD) - reader->skipUnused(1); - continue; - } - - playerInfo.aiTactic = static_cast(reader->readUInt8()); - - if(features.levelSOD) - reader->skipUnused(1); //TODO: check meaning? - - std::set allowedFactions; - - reader->readBitmaskFactions(allowedFactions, false); - - const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool(); - const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount; - - if(!allFactionsAllowed) - playerInfo.allowedFactions = allowedFactions; - - playerInfo.hasMainTown = reader->readBool(); - if(playerInfo.hasMainTown) - { - if(features.levelAB) - { - playerInfo.generateHeroAtMainTown = reader->readBool(); - reader->skipUnused(1); //TODO: check meaning? - } - else - { - playerInfo.generateHeroAtMainTown = true; - } - - playerInfo.posOfMainTown = reader->readInt3(); - } - - playerInfo.hasRandomHero = reader->readBool(); - playerInfo.mainCustomHeroId = reader->readHero(); - - if(playerInfo.mainCustomHeroId != HeroTypeID::NONE) - { - playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); - playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); - } - - if(features.levelAB) - { - reader->skipUnused(1); //TODO: check meaning? - uint32_t heroCount = reader->readUInt32(); - for(int pp = 0; pp < heroCount; ++pp) - { - SHeroName vv; - vv.heroId = reader->readHero(); - vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); - - playerInfo.heroesNames.push_back(vv); - } - } - } -} - -enum class EVictoryConditionType : uint8_t -{ - ARTIFACT = 0, - GATHERTROOP = 1, - GATHERRESOURCE = 2, - BUILDCITY = 3, - BUILDGRAIL = 4, - BEATHERO = 5, - CAPTURECITY = 6, - BEATMONSTER = 7, - TAKEDWELLINGS = 8, - TAKEMINES = 9, - TRANSPORTITEM = 10, - HOTA_ELIMINATE_ALL_MONSTERS = 11, - HOTA_SURVIVE_FOR_DAYS = 12, - WINSTANDARD = 255 -}; - -enum class ELossConditionType : uint8_t -{ - LOSSCASTLE = 0, - LOSSHERO = 1, - TIMEEXPIRES = 2, - LOSSSTANDARD = 255 -}; - -void CMapLoaderH3M::readVictoryLossConditions() -{ - mapHeader->triggeredEvents.clear(); - mapHeader->victoryMessage.clear(); - mapHeader->defeatMessage.clear(); - - auto vicCondition = static_cast(reader->readUInt8()); - - EventCondition victoryCondition(EventCondition::STANDARD_WIN); - EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); - defeatCondition.value = 7; - - TriggeredEvent standardVictory; - standardVictory.effect.type = EventEffect::VICTORY; - standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - standardVictory.identifier = "standardVictory"; - standardVictory.description.clear(); // TODO: display in quest window - standardVictory.onFulfill.appendTextID("core.genrltxt.659"); - standardVictory.trigger = EventExpression(victoryCondition); - - TriggeredEvent standardDefeat; - standardDefeat.effect.type = EventEffect::DEFEAT; - standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); - standardDefeat.identifier = "standardDefeat"; - standardDefeat.description.clear(); // TODO: display in quest window - standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); - standardDefeat.trigger = EventExpression(defeatCondition); - - // Specific victory conditions - if(vicCondition == EVictoryConditionType::WINSTANDARD) - { - // create normal condition - mapHeader->triggeredEvents.push_back(standardVictory); - mapHeader->victoryIconIndex = 11; - mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); - } - else - { - TriggeredEvent specialVictory; - specialVictory.effect.type = EventEffect::VICTORY; - specialVictory.identifier = "specialVictory"; - specialVictory.description.clear(); // TODO: display in quest window - - mapHeader->victoryIconIndex = static_cast(vicCondition); - - bool allowNormalVictory = reader->readBool(); - bool appliesToAI = reader->readBool(); - - switch(vicCondition) - { - case EVictoryConditionType::ARTIFACT: - { - assert(allowNormalVictory == true); // not selectable in editor - EventCondition cond(EventCondition::HAVE_ARTIFACT); - cond.objectType = reader->readArtifact(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); - specialVictory.onFulfill.appendTextID("core.genrltxt.280"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.1"); - break; - } - case EVictoryConditionType::GATHERTROOP: - { - EventCondition cond(EventCondition::HAVE_CREATURES); - cond.objectType = reader->readCreature(); - cond.value = reader->readInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); - specialVictory.onFulfill.appendTextID("core.genrltxt.276"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.2"); - break; - } - case EVictoryConditionType::GATHERRESOURCE: - { - EventCondition cond(EventCondition::HAVE_RESOURCES); - cond.objectType = reader->readUInt8(); - cond.value = reader->readInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); - specialVictory.onFulfill.appendTextID("core.genrltxt.278"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.3"); - break; - } - case EVictoryConditionType::BUILDCITY: - { - assert(appliesToAI == true); // not selectable in editor - EventExpression::OperatorAll oper; - EventCondition cond(EventCondition::HAVE_BUILDING); - cond.position = reader->readInt3(); - cond.objectType = BuildingID::TOWN_HALL + reader->readUInt8(); - oper.expressions.emplace_back(cond); - cond.objectType = BuildingID::FORT + reader->readUInt8(); - oper.expressions.emplace_back(cond); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); - specialVictory.onFulfill.appendTextID("core.genrltxt.282"); - specialVictory.trigger = EventExpression(oper); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.4"); - break; - } - case EVictoryConditionType::BUILDGRAIL: - { - assert(allowNormalVictory == true); // not selectable in editor - assert(appliesToAI == true); // not selectable in editor - EventCondition cond(EventCondition::HAVE_BUILDING); - cond.objectType = BuildingID::GRAIL; - cond.position = reader->readInt3(); - if(cond.position.z > 2) - cond.position = int3(-1, -1, -1); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285"); - specialVictory.onFulfill.appendTextID("core.genrltxt.284"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.5"); - break; - } - case EVictoryConditionType::BEATHERO: - { - if (!allowNormalVictory) - logGlobal->debug("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); - allowNormalVictory = true; // H3 behavior - assert(appliesToAI == false); // not selectable in editor - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::HERO; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); - specialVictory.onFulfill.appendTextID("core.genrltxt.252"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.6"); - break; - } - case EVictoryConditionType::CAPTURECITY: - { - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); - specialVictory.onFulfill.appendTextID("core.genrltxt.249"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.7"); - break; - } - case EVictoryConditionType::BEATMONSTER: - { - assert(appliesToAI == true); // not selectable in editor - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); - specialVictory.onFulfill.appendTextID("core.genrltxt.286"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.8"); - break; - } - case EVictoryConditionType::TAKEDWELLINGS: - { - EventExpression::OperatorAll oper; - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1)); - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4)); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); - specialVictory.onFulfill.appendTextID("core.genrltxt.288"); - specialVictory.trigger = EventExpression(oper); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.9"); - break; - } - case EVictoryConditionType::TAKEMINES: - { - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::MINE; - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); - specialVictory.onFulfill.appendTextID("core.genrltxt.290"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.10"); - break; - } - case EVictoryConditionType::TRANSPORTITEM: - { - assert(allowNormalVictory == true); // not selectable in editor - EventCondition cond(EventCondition::TRANSPORT); - cond.objectType = reader->readUInt8(); - cond.position = reader->readInt3(); - - specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); - specialVictory.onFulfill.appendTextID("core.genrltxt.292"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.11"); - break; - } - case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: - { - assert(appliesToAI == false); // not selectable in editor - EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; - - specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); - specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.12"); - mapHeader->victoryIconIndex = 12; - break; - } - case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: - { - assert(appliesToAI == false); // not selectable in editor - EventCondition cond(EventCondition::DAYS_PASSED); - cond.value = reader->readUInt32(); - - specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers"); - specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf"); - specialVictory.trigger = EventExpression(cond); - - mapHeader->victoryMessage.appendTextID("core.vcdesc.13"); - mapHeader->victoryIconIndex = 13; - break; - } - default: - assert(0); - } - - if(allowNormalVictory) - { - size_t playersOnMap = boost::range::count_if( - mapHeader->players, - [](const PlayerInfo & info) - { - return info.canAnyonePlay(); - } - ); - - if(playersOnMap == 1) - { - logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); - allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! - } - } - - // if condition is human-only turn it into following construction: AllOf(human, condition) - if(!appliesToAI) - { - EventExpression::OperatorAll oper; - EventCondition notAI(EventCondition::IS_HUMAN); - notAI.value = 1; - oper.expressions.emplace_back(notAI); - oper.expressions.push_back(specialVictory.trigger.get()); - specialVictory.trigger = EventExpression(oper); - } - - // if normal victory allowed - add one more quest - if(allowNormalVictory) - { - mapHeader->victoryMessage.appendRawString(" / "); - mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); - mapHeader->triggeredEvents.push_back(standardVictory); - } - mapHeader->triggeredEvents.push_back(specialVictory); - } - - // Read loss conditions - auto lossCond = static_cast(reader->readUInt8()); - if(lossCond == ELossConditionType::LOSSSTANDARD) - { - mapHeader->defeatIconIndex = 3; - mapHeader->defeatMessage.appendTextID("core.lcdesc.0"); - } - else - { - TriggeredEvent specialDefeat; - specialDefeat.effect.type = EventEffect::DEFEAT; - specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5"); - specialDefeat.identifier = "specialDefeat"; - specialDefeat.description.clear(); // TODO: display in quest window - - mapHeader->defeatIconIndex = static_cast(lossCond); - - switch(lossCond) - { - case ELossConditionType::LOSSCASTLE: - { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; - cond.position = reader->readInt3(); - - noneOf.expressions.emplace_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); - specialDefeat.trigger = EventExpression(noneOf); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.1"); - break; - } - case ELossConditionType::LOSSHERO: - { - EventExpression::OperatorNone noneOf; - EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; - cond.position = reader->readInt3(); - - noneOf.expressions.emplace_back(cond); - specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); - specialDefeat.trigger = EventExpression(noneOf); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.2"); - break; - } - case ELossConditionType::TIMEEXPIRES: - { - EventCondition cond(EventCondition::DAYS_PASSED); - cond.value = reader->readUInt16(); - - specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); - specialDefeat.trigger = EventExpression(cond); - - mapHeader->defeatMessage.appendTextID("core.lcdesc.3"); - break; - } - } - // turn simple loss condition into complete one that can be evaluated later: - // - any of : - // - days without town: 7 - // - all of: - // - is human - // - (expression) - - EventExpression::OperatorAll allOf; - EventCondition isHuman(EventCondition::IS_HUMAN); - isHuman.value = 1; - - allOf.expressions.emplace_back(isHuman); - allOf.expressions.push_back(specialDefeat.trigger.get()); - specialDefeat.trigger = EventExpression(allOf); - - mapHeader->triggeredEvents.push_back(specialDefeat); - } - mapHeader->triggeredEvents.push_back(standardDefeat); -} - -void CMapLoaderH3M::readTeamInfo() -{ - mapHeader->howManyTeams = reader->readUInt8(); - if(mapHeader->howManyTeams > 0) - { - // Teams - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) - mapHeader->players[i].team = TeamID(reader->readUInt8()); - } - else - { - // No alliances - for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) - if(mapHeader->players[i].canComputerPlay || mapHeader->players[i].canHumanPlay) - mapHeader->players[i].team = TeamID(mapHeader->howManyTeams++); - } -} - -void CMapLoaderH3M::readAllowedHeroes() -{ - mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); - - if(features.levelHOTA0) - reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); - else - reader->readBitmaskHeroes(mapHeader->allowedHeroes, false); - - if(features.levelAB) - { - uint32_t placeholdersQty = reader->readUInt32(); - - for (uint32_t i = 0; i < placeholdersQty; ++i) - { - auto heroID = reader->readHero(); - mapHeader->reservedCampaignHeroes.push_back(heroID); - } - } -} - -void CMapLoaderH3M::readDisposedHeroes() -{ - // Reading disposed heroes (20 bytes) - if(features.levelSOD) - { - ui8 disp = reader->readUInt8(); - map->disposedHeroes.resize(disp); - for(int g = 0; g < disp; ++g) - { - map->disposedHeroes[g].heroId = reader->readHero(); - map->disposedHeroes[g].portrait.setNum(reader->readHeroPortrait()); - map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); - reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); - } - } -} - -void CMapLoaderH3M::readMapOptions() -{ - //omitting NULLS - reader->skipZero(31); - - if(features.levelHOTA0) - { - //TODO: HotA - bool allowSpecialMonths = reader->readBool(); - if(!allowSpecialMonths) - logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName); - reader->skipZero(3); - } - - if(features.levelHOTA1) - { - // Unknown, may be another "sized bitmap", e.g - // 4 bytes - size of bitmap (16) - // 2 bytes - bitmap data (16 bits / 2 bytes) - [[maybe_unused]] uint8_t unknownConstant = reader->readUInt8(); - assert(unknownConstant == 16); - reader->skipZero(5); - } - - if(features.levelHOTA3) - { - //TODO: HotA - int32_t roundLimit = reader->readInt32(); - if(roundLimit != -1) - logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit); - } -} - -void CMapLoaderH3M::readAllowedArtifacts() -{ - map->allowedArtifact = VLC->arth->getDefaultAllowed(); - - if(features.levelAB) - { - if(features.levelHOTA0) - reader->readBitmaskArtifactsSized(map->allowedArtifact, true); - else - reader->readBitmaskArtifacts(map->allowedArtifact, true); - } - - // ban combo artifacts - if(!features.levelSOD) - { - for(CArtifact * artifact : VLC->arth->objects) - if(artifact->isCombined()) - map->allowedArtifact[artifact->getId()] = false; - } - - if(!features.levelAB) - { - map->allowedArtifact[ArtifactID::VIAL_OF_DRAGON_BLOOD] = false; - map->allowedArtifact[ArtifactID::ARMAGEDDONS_BLADE] = false; - } - - // Messy, but needed - for(TriggeredEvent & event : map->triggeredEvents) - { - auto patcher = [&](EventCondition cond) -> EventExpression::Variant - { - if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) - { - map->allowedArtifact[cond.objectType] = false; - } - return cond; - }; - - event.trigger = event.trigger.morph(patcher); - } -} - -void CMapLoaderH3M::readAllowedSpellsAbilities() -{ - map->allowedSpells = VLC->spellh->getDefaultAllowed(); - map->allowedAbilities = VLC->skillh->getDefaultAllowed(); - - if(features.levelSOD) - { - reader->readBitmaskSpells(map->allowedSpells, true); - reader->readBitmaskSkills(map->allowedAbilities, true); - } -} - -void CMapLoaderH3M::readRumors() -{ - uint32_t rumorsCount = reader->readUInt32(); - assert(rumorsCount < 1000); // sanity check - - for(int it = 0; it < rumorsCount; it++) - { - Rumor ourRumor; - ourRumor.name = readBasicString(); - ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text"))); - map->rumors.push_back(ourRumor); - } -} - -void CMapLoaderH3M::readPredefinedHeroes() -{ - if(!features.levelSOD) - return; - - uint32_t heroesCount = features.heroesCount; - - if(features.levelHOTA0) - heroesCount = reader->readUInt32(); - - assert(heroesCount <= features.heroesCount); - - for(int heroID = 0; heroID < heroesCount; heroID++) - { - bool custom = reader->readBool(); - if(!custom) - continue; - - auto * hero = new CGHeroInstance(); - hero->ID = Obj::HERO; - hero->subID = heroID; - - bool hasExp = reader->readBool(); - if(hasExp) - { - hero->exp = reader->readUInt32(); - } - else - { - hero->exp = 0; - } - - bool hasSecSkills = reader->readBool(); - if(hasSecSkills) - { - uint32_t howMany = reader->readUInt32(); - hero->secSkills.resize(howMany); - for(int yy = 0; yy < howMany; ++yy) - { - hero->secSkills[yy].first = reader->readSkill(); - hero->secSkills[yy].second = reader->readUInt8(); - } - } - - loadArtifactsOfHero(hero); - - bool hasCustomBio = reader->readBool(); - if(hasCustomBio) - hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); - - // 0xFF is default, 00 male, 01 female - hero->gender = static_cast(reader->readUInt8()); - assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); - - bool hasCustomSpells = reader->readBool(); - if(hasCustomSpells) - reader->readBitmaskSpells(hero->spells, false); - - bool hasCustomPrimSkills = reader->readBool(); - if(hasCustomPrimSkills) - { - for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++) - { - hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); - } - } - map->predefinedHeroes.emplace_back(hero); - - logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getByIndex(hero->subID)->getJsonKey()); - } -} - -void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) -{ - bool hasArtSet = reader->readBool(); - - // True if artifact set is not default (hero has some artifacts) - if(!hasArtSet) - return; - - // Workaround - if hero has customized artifacts game should not attempt to add spellbook based on hero type - hero->spells.insert(SpellID::SPELLBOOK_PRESET); - - if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) - { - logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); - - hero->artifactsInBackpack.clear(); - while(!hero->artifactsWorn.empty()) - hero->eraseArtSlot(hero->artifactsWorn.begin()->first); - } - - for(int i = 0; i < features.artifactSlotsCount; i++) - loadArtifactToSlot(hero, i); - - // bag artifacts - // number of artifacts in hero's bag - int amount = reader->readUInt16(); - for(int i = 0; i < amount; ++i) - { - loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); - } -} - -bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) -{ - ArtifactID artifactID = reader->readArtifact(); - - if(artifactID == ArtifactID::NONE) - return false; - - const Artifact * art = artifactID.toArtifact(VLC->artifacts()); - - if(!art) - { - logGlobal->warn("Map '%s': Invalid artifact in hero's backpack, ignoring...", mapName); - return false; - } - - if(art->isBig() && slot >= ArtifactPosition::BACKPACK_START) - { - logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName); - return false; - } - - // H3 bug workaround - Enemy hero on 3rd scenario of Good1.h3c campaign ("Long Live The Queen") - // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor - // Artifact seems to be missing in game, so skip artifacts that don't fit target slot - auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - auto dstLoc = ArtifactLocation(hero, ArtifactPosition(slot)); - if(artifact->canBePutAt(dstLoc)) - { - artifact->putAt(dstLoc); - } - else - { - logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->getNameTranslated(), slot); - return false; - } - - return true; -} - -void CMapLoaderH3M::readTerrain() -{ - map->initTerrain(); - - // Read terrain - int3 pos; - for(pos.z = 0; pos.z < map->levels(); ++pos.z) - { - //OH3 format is [z][y][x] - for(pos.y = 0; pos.y < map->height; pos.y++) - { - for(pos.x = 0; pos.x < map->width; pos.x++) - { - auto & tile = map->getTile(pos); - tile.terType = VLC->terrainTypeHandler->getById(reader->readTerrain()); - tile.terView = reader->readUInt8(); - tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver()); - tile.riverDir = reader->readUInt8(); - tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad()); - tile.roadDir = reader->readUInt8(); - tile.extTileFlags = reader->readUInt8(); - tile.blocked = !tile.terType->isPassable(); - tile.visitable = false; - - assert(tile.terType->getId() != ETerrainId::NONE); - } - } - } - map->calculateWaterContent(); -} - -void CMapLoaderH3M::readObjectTemplates() -{ - uint32_t defAmount = reader->readUInt32(); - - templates.reserve(defAmount); - - // Read custom defs - for(int defID = 0; defID < defAmount; ++defID) - { - auto tmpl = reader->readObjectTemplate(); - templates.push_back(tmpl); - - if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/"))) - logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); - } -} - -CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) -{ - auto * object = new CGEvent(); - - readBoxContent(object, mapPosition, idToBeGiven); - - reader->readBitmaskPlayers(object->availableFor, false); - object->computerActivate = reader->readBool(); - object->removeAfterVisit = reader->readBool(); - - reader->skipZero(4); - - if(features.levelHOTA3) - object->humanActivate = reader->readBool(); - else - object->humanActivate = true; - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) -{ - auto * object = new CGPandoraBox(); - readBoxContent(object, mapPosition, idToBeGiven); - return object; -} - -void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) -{ - readMessageAndGuards(object->message, object, mapPosition); - Rewardable::VisitInfo vinfo; - auto & reward = vinfo.reward; - - reward.heroExperience = reader->readUInt32(); - reward.manaDiff = reader->readInt32(); - if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, idToBeGiven); - if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, idToBeGiven); - - reader->readResourses(reward.resources); - for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) - reward.primary.at(x) = reader->readUInt8(); - - int gabn = reader->readUInt8(); //number of gained abilities - for(int oo = 0; oo < gabn; ++oo) - { - auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); - - reward.secondary[rId] = rVal; - } - int gart = reader->readUInt8(); //number of gained artifacts - for(int oo = 0; oo < gart; ++oo) - reward.artifacts.push_back(reader->readArtifact()); - - int gspel = reader->readUInt8(); //number of gained spells - for(int oo = 0; oo < gspel; ++oo) - reward.spells.push_back(reader->readSpell()); - - int gcre = reader->readUInt8(); //number of gained creatures - for(int oo = 0; oo < gcre; ++oo) - { - auto rId = reader->readCreature(); - auto rVal = reader->readUInt16(); - - reward.creatures.emplace_back(rId, rVal); - } - - vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - object->configuration.info.push_back(vinfo); - - reader->skipZero(8); -} - -CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - auto * object = new CGCreature(); - - if(features.levelAB) - { - object->identifier = reader->readUInt32(); - map->questIdentifierToId[object->identifier] = objectInstanceID; - } - - auto * hlp = new CStackInstance(); - hlp->count = reader->readUInt16(); - - //type will be set during initialization - object->putStack(SlotID(0), hlp); - - object->character = reader->readInt8(); - - bool hasMessage = reader->readBool(); - if(hasMessage) - { - object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); - reader->readResourses(object->resources); - object->gainedArtifact = reader->readArtifact(); - } - object->neverFlees = reader->readBool(); - object->notGrowingTeam = reader->readBool(); - reader->skipZero(2); - - if(features.levelHOTA3) - { - //TODO: HotA - int32_t agressionExact = reader->readInt32(); // -1 = default, 1-10 = possible values range - bool joinOnlyForMoney = reader->readBool(); // if true, monsters will only join for money - int32_t joinPercent = reader->readInt32(); // 100 = default, percent of monsters that will join on succesfull agression check - int32_t upgradedStack = reader->readInt32(); // Presence of upgraded stack, -1 = random, 0 = never, 1 = always - int32_t stacksCount = reader->readInt32(); // TODO: check possible values. How many creature stacks will be present on battlefield, -1 = default - - if(agressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) - logGlobal->warn( - "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", - mapName, - mapPosition.toString(), - agressionExact, - int(joinOnlyForMoney), - joinPercent, - upgradedStack, - stacksCount - ); - } - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) -{ - auto * object = new CGSignBottle(); - object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); - reader->skipZero(4); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readWitchHut() -{ - auto * object = new CGWitchHut(); - - // AB and later maps have allowed abilities defined in H3M - if(features.levelAB) - { - reader->readBitmaskSkills(object->allowedAbilities, false); - - if(object->allowedAbilities.size() != 1) - { - auto defaultAllowed = VLC->skillh->getDefaultAllowed(); - - for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) - if(defaultAllowed[skillID]) - object->allowedAbilities.insert(SecondarySkill(skillID)); - } - } - return object; -} - -CGObjectInstance * CMapLoaderH3M::readScholar() -{ - auto * object = new CGScholar(); - object->bonusType = static_cast(reader->readUInt8()); - object->bonusID = reader->readUInt8(); - reader->skipZero(6); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) -{ - auto * object = new CGGarrison(); - - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - readCreatureSet(object, 7); - if(features.levelAB) - object->removableUnits = reader->readBool(); - else - object->removableUnits = true; - - reader->skipZero(8); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - ArtifactID artID = ArtifactID::NONE; //random, set later - SpellID spellID = SpellID::NONE; - auto * object = new CGArtifact(); - - readMessageAndGuards(object->message, object, mapPosition); - - if(objectTemplate->id == Obj::SPELL_SCROLL) - { - spellID = reader->readSpell32(); - artID = ArtifactID::SPELL_SCROLL; - } - else if(objectTemplate->id == Obj::ARTIFACT) - { - //specific artifact - artID = ArtifactID(objectTemplate->subid); - } - - object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGResource(); - - readMessageAndGuards(object->message, object, mapPosition); - - object->amount = reader->readUInt32(); - if(objectTemplate->subid == GameResID(EGameResID::GOLD)) - { - // Gold is multiplied by 100. - object->amount *= 100; - } - reader->skipZero(4); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGMine(); - if(objectTemplate->subid < 7) - { - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - } - else - { - object->setOwner(PlayerColor::NEUTRAL); - reader->readBitmaskResources(object->abandonedMineResources, false); - } - return object; -} - -CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position) -{ - auto * object = new CGDwelling(); - setOwnerAndValidate(position, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = new CGDwelling(); - - CSpecObjInfo * spec = nullptr; - switch(objectTemplate->id) - { - case Obj::RANDOM_DWELLING: - spec = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: - spec = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: - spec = new CCreGenLeveledInfo(); - break; - default: - throw std::runtime_error("Invalid random dwelling format"); - } - spec->owner = object; - - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - - //216 and 217 - if(auto * castleSpec = dynamic_cast(spec)) - { - castleSpec->instanceId = ""; - castleSpec->identifier = reader->readUInt32(); - if(!castleSpec->identifier) - { - castleSpec->asCastle = false; - const int MASK_SIZE = 8; - ui8 mask[2]; - mask[0] = reader->readUInt8(); - mask[1] = reader->readUInt8(); - - castleSpec->allowedFactions.clear(); - castleSpec->allowedFactions.resize(VLC->townh->size(), false); - - for(int i = 0; i < MASK_SIZE; i++) - castleSpec->allowedFactions[i] = ((mask[0] & (1 << i)) > 0); - - for(int i = 0; i < (GameConstants::F_NUMBER - MASK_SIZE); i++) - castleSpec->allowedFactions[i + MASK_SIZE] = ((mask[1] & (1 << i)) > 0); - } - else - { - castleSpec->asCastle = true; - } - } - - //216 and 218 - if(auto * lvlSpec = dynamic_cast(spec)) - { - lvlSpec->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; - lvlSpec->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; - } - object->info = spec; - return object; -} - -CGObjectInstance * CMapLoaderH3M::readShrine() -{ - auto * object = new CGShrine(); - object->spell = reader->readSpell32(); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) -{ - auto * object = new CGHeroPlaceholder(); - - setOwnerAndValidate(mapPosition, object, reader->readPlayer()); - - HeroTypeID htid = reader->readHero(); //hero type id - - if(htid.getNum() == -1) - { - object->powerRank = reader->readUInt8(); - logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); - } - else - { - object->heroType = htid; - logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); - } - - return object; -} - -CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if (objectTemplate->subid < 1000) - { - map->grailPos = mapPosition; - map->grailRadius = reader->readInt32(); - } - else - { - // Battle location for arena mode in HotA - logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); - } - return nullptr; -} - -CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) - return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); - - logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString()); - return new CGObjectInstance(); -} - -CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(objectTemplate->subid == 0) - return new CBank(); - - return new CGObjectInstance(); -} - -CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) -{ - auto * guard = new CGQuestGuard(); - readQuest(guard, mapPosition); - return guard; -} - -CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - auto * object = readGeneric(mapPosition, objectTemplate); - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) -{ - auto * object = new CGLighthouse(); - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared_ptr objectTemplate) -{ - if(features.levelHOTA3) - { - //TODO: HotA - // index of guards preset. -1 = random, 0-4 = index of possible guards settings - int32_t guardsPresetIndex = reader->readInt32(); - - // presence of upgraded stack: -1 = random, 0 = never, 1 = always - int8_t upgradedStackPresence = reader->readInt8(); - - assert(vstd::iswithin(guardsPresetIndex, -1, 4)); - assert(vstd::iswithin(upgradedStackPresence, -1, 1)); - - // list of possible artifacts in reward - // - if list is empty, artifacts are either not present in reward or random - // - if non-empty, then list always have same number of elements as number of artifacts in bank - // - ArtifactID::NONE indictates random artifact, other values indicate artifact that should be used as reward - std::vector artifacts; - int artNumber = reader->readUInt32(); - for(int yy = 0; yy < artNumber; ++yy) - { - artifacts.push_back(reader->readArtifact32()); - } - - if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty()) - logGlobal->warn( - "Map '%s: creature bank at %s settings %d %d %d are not implemented!", - mapName, - mapPosition.toString(), - guardsPresetIndex, - int(upgradedStackPresence), - artifacts.size() - ); - } - - return readGeneric(mapPosition, objectTemplate); -} - -CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - switch(objectTemplate->id) - { - case Obj::EVENT: - return readEvent(mapPosition, objectInstanceID); - - case Obj::HERO: - case Obj::RANDOM_HERO: - case Obj::PRISON: - return readHero(mapPosition, objectInstanceID); - - case Obj::MONSTER: - case Obj::RANDOM_MONSTER: - case Obj::RANDOM_MONSTER_L1: - case Obj::RANDOM_MONSTER_L2: - case Obj::RANDOM_MONSTER_L3: - case Obj::RANDOM_MONSTER_L4: - case Obj::RANDOM_MONSTER_L5: - case Obj::RANDOM_MONSTER_L6: - case Obj::RANDOM_MONSTER_L7: - return readMonster(mapPosition, objectInstanceID); - - case Obj::OCEAN_BOTTLE: - case Obj::SIGN: - return readSign(mapPosition); - - case Obj::SEER_HUT: - return readSeerHut(mapPosition, objectInstanceID); - - case Obj::WITCH_HUT: - return readWitchHut(); - case Obj::SCHOLAR: - return readScholar(); - - case Obj::GARRISON: - case Obj::GARRISON2: - return readGarrison(mapPosition); - - case Obj::ARTIFACT: - case Obj::RANDOM_ART: - case Obj::RANDOM_TREASURE_ART: - case Obj::RANDOM_MINOR_ART: - case Obj::RANDOM_MAJOR_ART: - case Obj::RANDOM_RELIC_ART: - case Obj::SPELL_SCROLL: - return readArtifact(mapPosition, objectTemplate); - - case Obj::RANDOM_RESOURCE: - case Obj::RESOURCE: - return readResource(mapPosition, objectTemplate); - case Obj::RANDOM_TOWN: - case Obj::TOWN: - return readTown(mapPosition, objectTemplate); - - case Obj::MINE: - case Obj::ABANDONED_MINE: - return readMine(mapPosition, objectTemplate); - - case Obj::CREATURE_GENERATOR1: - case Obj::CREATURE_GENERATOR2: - case Obj::CREATURE_GENERATOR3: - case Obj::CREATURE_GENERATOR4: - return readDwelling(mapPosition); - - case Obj::SHRINE_OF_MAGIC_INCANTATION: - case Obj::SHRINE_OF_MAGIC_GESTURE: - case Obj::SHRINE_OF_MAGIC_THOUGHT: - return readShrine(); - - case Obj::PANDORAS_BOX: - return readPandora(mapPosition, objectInstanceID); - - case Obj::GRAIL: - return readGrail(mapPosition, objectTemplate); - - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - return readDwellingRandom(mapPosition, objectTemplate); - - case Obj::QUEST_GUARD: - return readQuestGuard(mapPosition); - - case Obj::SHIPYARD: - return readShipyard(mapPosition, objectTemplate); - - case Obj::HERO_PLACEHOLDER: - return readHeroPlaceholder(mapPosition); - - case Obj::PYRAMID: - return readPyramid(mapPosition, objectTemplate); - - case Obj::LIGHTHOUSE: - return readLighthouse(mapPosition); - - case Obj::CREATURE_BANK: - case Obj::DERELICT_SHIP: - case Obj::DRAGON_UTOPIA: - case Obj::CRYPT: - case Obj::SHIPWRECK: - return readBank(mapPosition, objectTemplate); - - default: //any other object - return readGeneric(mapPosition, objectTemplate); - } -} - -void CMapLoaderH3M::readObjects() -{ - uint32_t objectsCount = reader->readUInt32(); - - for(uint32_t i = 0; i < objectsCount; ++i) - { - int3 mapPosition = reader->readInt3(); - - uint32_t defIndex = reader->readUInt32(); - ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast(map->objects.size())); - - std::shared_ptr objectTemplate = templates.at(defIndex); - reader->skipZero(5); - - CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID); - - if(!newObject) - continue; - - newObject->pos = mapPosition; - newObject->ID = objectTemplate->id; - newObject->id = objectInstanceID; - if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) - { - newObject->subID = objectTemplate->subid; - } - newObject->appearance = objectTemplate; - assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); - - if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) - logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString()); - - { - //TODO: define valid typeName and subtypeName for H3M maps - //boost::format fmt("%s_%d"); - //fmt % nobj->typeName % nobj->id.getNum(); - boost::format fmt("obj_%d"); - fmt % newObject->id.getNum(); - newObject->instanceName = fmt.str(); - } - map->addNewObject(newObject); - } - - std::sort( - map->heroesOnMap.begin(), - map->heroesOnMap.end(), - [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) - { - return a->subID < b->subID; - } - ); -} - -void CMapLoaderH3M::readCreatureSet(CCreatureSet * out, int number) -{ - for(int index = 0; index < number; ++index) - { - CreatureID creatureID = reader->readCreature(); - int count = reader->readUInt16(); - - // Empty slot - if(creatureID == CreatureID::NONE) - continue; - - auto * result = new CStackInstance(); - result->count = count; - - if(creatureID < CreatureID::NONE) - { - int value = -creatureID.getNum() - 2; - assert(value >= 0 && value < 14); - uint8_t level = value / 2; - uint8_t upgrade = value % 2; - - //this will happen when random object has random army - result->randomStack = CStackInstance::RandomStackInfo{level, upgrade}; - } - else - { - result->setType(creatureID); - } - - out->putStack(SlotID(index), result); - } - - out->validTypes(true); -} - -void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner) -{ - assert(owner.isValidPlayer() || owner == PlayerColor::NEUTRAL); - - if(owner == PlayerColor::NEUTRAL) - { - object->setOwner(PlayerColor::NEUTRAL); - return; - } - - if(!owner.isValidPlayer()) - { - object->setOwner(PlayerColor::NEUTRAL); - logGlobal->warn("Map '%s': Object at %s - owned by invalid player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum())); - return; - } - - if(!mapHeader->players[owner.getNum()].canAnyonePlay()) - { - object->setOwner(PlayerColor::NEUTRAL); - logGlobal->warn("Map '%s': Object at %s - owned by non-existing player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum()) - ); - return; - } - - object->setOwner(owner); -} - -CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) -{ - auto * object = new CGHeroInstance(); - - if(features.levelAB) - { - unsigned int identifier = reader->readUInt32(); - map->questIdentifierToId[identifier] = objectInstanceID; - } - - PlayerColor owner = reader->readPlayer(); - object->subID = reader->readHero().getNum(); - - //If hero of this type has been predefined, use that as a base. - //Instance data will overwrite the predefined values where appropriate. - for(auto & elem : map->predefinedHeroes) - { - if(elem->subID == object->subID) - { - logGlobal->debug("Hero %d will be taken from the predefined heroes list.", object->subID); - delete object; - object = elem; - break; - } - } - - setOwnerAndValidate(mapPosition, object, owner); - - for(auto & elem : map->disposedHeroes) - { - if(elem.heroId.getNum() == object->subID) - { - object->nameCustomTextId = elem.name; - object->customPortraitSource = elem.portrait; - break; - } - } - - bool hasName = reader->readBool(); - if(hasName) - object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); - - if(features.levelSOD) - { - bool hasCustomExperience = reader->readBool(); - if(hasCustomExperience) - object->exp = reader->readUInt32(); - } - else - { - object->exp = reader->readUInt32(); - - //0 means "not set" in <=AB maps - if(!object->exp) - object->exp = CGHeroInstance::UNINITIALIZED_EXPERIENCE; - } - - bool hasPortrait = reader->readBool(); - if(hasPortrait) - object->customPortraitSource = reader->readHeroPortrait(); - - bool hasSecSkills = reader->readBool(); - if(hasSecSkills) - { - if(!object->secSkills.empty()) - { - if(object->secSkills[0].first != SecondarySkill::DEFAULT) - logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); - object->secSkills.clear(); - } - - uint32_t skillsCount = reader->readUInt32(); - object->secSkills.resize(skillsCount); - for(int i = 0; i < skillsCount; ++i) - { - object->secSkills[i].first = reader->readSkill(); - object->secSkills[i].second = reader->readUInt8(); - } - } - - bool hasGarison = reader->readBool(); - if(hasGarison) - readCreatureSet(object, 7); - - object->formation = static_cast(reader->readUInt8()); - assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); - - loadArtifactsOfHero(object); - object->patrol.patrolRadius = reader->readUInt8(); - object->patrol.patrolling = (object->patrol.patrolRadius != 0xff); - - if(features.levelAB) - { - bool hasCustomBiography = reader->readBool(); - if(hasCustomBiography) - object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); - - object->gender = static_cast(reader->readUInt8()); - assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); - } - else - { - object->gender = EHeroGender::DEFAULT; - } - - // Spells - if(features.levelSOD) - { - bool hasCustomSpells = reader->readBool(); - if(hasCustomSpells) - { - if(!object->spells.empty()) - { - object->spells.clear(); - logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); - } - - object->spells.insert(SpellID::PRESET); //placeholder "preset spells" - - reader->readBitmaskSpells(object->spells, false); - } - } - else if(features.levelAB) - { - //we can read one spell - SpellID spell = reader->readSpell(); - - if(spell != SpellID::NONE) - object->spells.insert(spell); - } - - if(features.levelSOD) - { - bool hasCustomPrimSkills = reader->readBool(); - if(hasCustomPrimSkills) - { - auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); - if(ps->size()) - { - logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); - for(const auto & b : *ps) - object->removeBonus(b); - } - - for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx) - { - object->pushPrimSkill(static_cast(xx), reader->readUInt8()); - } - } - } - - if (object->subID != -1) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); - else - logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); - - reader->skipZero(16); - return object; -} - -CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven) -{ - auto * hut = new CGSeerHut(); - - uint32_t questsCount = 1; - - if(features.levelHOTA3) - questsCount = reader->readUInt32(); - - //TODO: HotA - if(questsCount > 1) - logGlobal->warn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount); - - for(size_t i = 0; i < questsCount; ++i) - readSeerHutQuest(hut, position, idToBeGiven); - - if(features.levelHOTA3) - { - uint32_t repeateableQuestsCount = reader->readUInt32(); - hut->quest->repeatedQuest = repeateableQuestsCount != 0; - - if(repeateableQuestsCount != 0) - logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); - - for(size_t i = 0; i < repeateableQuestsCount; ++i) - readSeerHutQuest(hut, position, idToBeGiven); - } - - reader->skipZero(2); - - return hut; -} - -enum class ESeerHutRewardType : uint8_t -{ - NOTHING = 0, - EXPERIENCE = 1, - MANA_POINTS = 2, - MORALE = 3, - LUCK = 4, - RESOURCES = 5, - PRIMARY_SKILL = 6, - SECONDARY_SKILL = 7, - ARTIFACT = 8, - SPELL = 9, - CREATURE = 10, -}; - -enum class EQuestMission { - NONE = 0, - LEVEL = 1, - PRIMARY_SKILL = 2, - KILL_HERO = 3, - KILL_CREATURE = 4, - ARTIFACT = 5, - ARMY = 6, - RESOURCES = 7, - HERO = 8, - PLAYER = 9, - HOTA_MULTI = 10, - // end of H3 missions - KEYMASTER = 100, - HOTA_HERO_CLASS = 101, - HOTA_REACH_DATE = 102 -}; - -void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) -{ - EQuestMission missionType = EQuestMission::NONE; - if(features.levelAB) - { - missionType = static_cast(readQuest(hut, position)); - } - else - { - //RoE - auto artID = reader->readArtifact(); - if(artID != ArtifactID::NONE) - { - //not none quest - hut->quest->mission.artifacts.push_back(artID); - missionType = EQuestMission::ARTIFACT; - } - hut->quest->lastDay = -1; //no timeout - hut->quest->isCustomFirst = false; - hut->quest->isCustomNext = false; - hut->quest->isCustomComplete = false; - } - - if(missionType != EQuestMission::NONE) - { - auto rewardType = static_cast(reader->readUInt8()); - Rewardable::VisitInfo vinfo; - auto & reward = vinfo.reward; - switch(rewardType) - { - case ESeerHutRewardType::NOTHING: - { - // no-op - break; - } - case ESeerHutRewardType::EXPERIENCE: - { - reward.heroExperience = reader->readUInt32(); - break; - } - case ESeerHutRewardType::MANA_POINTS: - { - reward.manaDiff = reader->readUInt32(); - break; - } - case ESeerHutRewardType::MORALE: - { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); - break; - } - case ESeerHutRewardType::LUCK: - { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); - break; - } - case ESeerHutRewardType::RESOURCES: - { - auto rId = reader->readUInt8(); - auto rVal = reader->readUInt32(); - - assert(rId < features.resourcesCount); - - reward.resources[rId] = rVal; - break; - } - case ESeerHutRewardType::PRIMARY_SKILL: - { - auto rId = reader->readUInt8(); - auto rVal = reader->readUInt8(); - - reward.primary.at(rId) = rVal; - break; - } - case ESeerHutRewardType::SECONDARY_SKILL: - { - auto rId = reader->readSkill(); - auto rVal = reader->readUInt8(); - - reward.secondary[rId] = rVal; - break; - } - case ESeerHutRewardType::ARTIFACT: - { - reward.artifacts.push_back(reader->readArtifact()); - break; - } - case ESeerHutRewardType::SPELL: - { - reward.spells.push_back(reader->readSpell()); - break; - } - case ESeerHutRewardType::CREATURE: - { - auto rId = reader->readCreature(); - auto rVal = reader->readUInt16(); - - reward.creatures.emplace_back(rId, rVal); - break; - } - default: - { - assert(0); - } - } - - vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - hut->configuration.info.push_back(vinfo); - } - else - { - // missionType==255 - reader->skipZero(1); - } -} - -int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) -{ - auto missionId = reader->readUInt8(); - - switch(static_cast(missionId)) - { - case EQuestMission::NONE: - return missionId; - case EQuestMission::PRIMARY_SKILL: - { - for(int x = 0; x < 4; ++x) - { - guard->quest->mission.primary[x] = reader->readUInt8(); - } - break; - } - case EQuestMission::LEVEL: - { - guard->quest->mission.heroLevel = reader->readUInt32(); - break; - } - case EQuestMission::KILL_HERO: - case EQuestMission::KILL_CREATURE: - { - guard->quest->killTarget = ObjectInstanceID(reader->readUInt32()); - break; - } - case EQuestMission::ARTIFACT: - { - int artNumber = reader->readUInt8(); - for(int yy = 0; yy < artNumber; ++yy) - { - auto artid = reader->readArtifact(); - guard->quest->mission.artifacts.push_back(artid); - map->allowedArtifact[artid] = false; //these are unavailable for random generation - } - break; - } - case EQuestMission::ARMY: - { - int typeNumber = reader->readUInt8(); - guard->quest->mission.creatures.resize(typeNumber); - for(int hh = 0; hh < typeNumber; ++hh) - { - guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; - guard->quest->mission.creatures[hh].count = reader->readUInt16(); - } - break; - } - case EQuestMission::RESOURCES: - { - for(int x = 0; x < 7; ++x) - guard->quest->mission.resources[x] = reader->readUInt32(); - - break; - } - case EQuestMission::HERO: - { - guard->quest->mission.heroes.push_back(reader->readHero()); - break; - } - case EQuestMission::PLAYER: - { - guard->quest->mission.players.push_back(reader->readPlayer()); - break; - } - case EQuestMission::HOTA_MULTI: - { - uint32_t missionSubID = reader->readUInt32(); - - if(missionSubID == 0) - { - missionId = int(EQuestMission::HOTA_HERO_CLASS); - std::set heroClasses; - reader->readBitmaskHeroClassesSized(heroClasses, false); - for(auto & hc : heroClasses) - guard->quest->mission.heroClasses.push_back(hc); - break; - } - if(missionSubID == 1) - { - missionId = int(EQuestMission::HOTA_REACH_DATE); - guard->quest->mission.daysPassed = reader->readUInt32() + 1; - break; - } - break; - } - default: - { - assert(0); - } - } - - guard->quest->lastDay = reader->readInt32(); - guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit"))); - guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit"))); - guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed"))); - guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); - guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); - guard->quest->isCustomComplete = !guard->quest->completedText.empty(); - return missionId; -} - -CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) -{ - auto * object = new CGTownInstance(); - if(features.levelAB) - object->identifier = reader->readUInt32(); - - setOwnerAndValidate(position, object, reader->readPlayer()); - - std::optional faction; - if (objectTemplate->id == Obj::TOWN) - faction = FactionID(objectTemplate->subid); - - bool hasName = reader->readBool(); - if(hasName) - object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); - - bool hasGarrison = reader->readBool(); - if(hasGarrison) - readCreatureSet(object, 7); - - object->formation = static_cast(reader->readUInt8()); - assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); - - bool hasCustomBuildings = reader->readBool(); - if(hasCustomBuildings) - { - reader->readBitmaskBuildings(object->builtBuildings, faction); - reader->readBitmaskBuildings(object->forbiddenBuildings, faction); - } - // Standard buildings - else - { - bool hasFort = reader->readBool(); - if(hasFort) - object->builtBuildings.insert(BuildingID::FORT); - - //means that set of standard building should be included - object->builtBuildings.insert(BuildingID::DEFAULT); - } - - if(features.levelAB) - { - std::set spellsMask; - - reader->readBitmaskSpells(spellsMask, false); - std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->obligatorySpells)); - } - - { - std::set spellsMask; - - reader->readBitmaskSpells(spellsMask, true); - std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells)); - - auto defaultAllowed = VLC->spellh->getDefaultAllowed(); - - //add all spells from mods - for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) - if(defaultAllowed[i]) - object->possibleSpells.emplace_back(i); - } - - if(features.levelHOTA1) - { - // TODO: HOTA support - [[maybe_unused]] bool spellResearchAvailable = reader->readBool(); - } - - // Read castle events - uint32_t eventsCount = reader->readUInt32(); - - for(int eventID = 0; eventID < eventsCount; ++eventID) - { - CCastleEvent event; - event.name = readBasicString(); - event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); - - reader->readResourses(event.resources); - - event.players = reader->readUInt8(); - if(features.levelSOD) - event.humanAffected = reader->readBool(); - else - event.humanAffected = true; - - event.computerAffected = reader->readUInt8(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); - - reader->skipZero(17); - - // New buildings - reader->readBitmaskBuildings(event.buildings, faction); - - event.creatures.resize(7); - for(int i = 0; i < 7; ++i) - event.creatures[i] = reader->readUInt16(); - - reader->skipZero(4); - object->events.push_back(event); - } - - if(features.levelSOD) - { - object->alignmentToPlayer = PlayerColor::NEUTRAL; // "same as owner or random" - - uint8_t alignment = reader->readUInt8(); - - if(alignment != 255) - { - if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) - { - if (mapHeader->players[alignment].canAnyonePlay()) - object->alignmentToPlayer = PlayerColor(alignment); - else - logGlobal->warn("%s - Aligment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment)); - } - else - { - // TODO: HOTA support - uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum(); - - if(invertedAlignment < PlayerColor::PLAYER_LIMIT.getNum()) - { - logGlobal->warn("%s - Aligment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum()); - } - else - { - logGlobal->warn("%s - Aligment of town at %s is corrupted!!", mapName, position.toString()); - } - } - } - } - reader->skipZero(3); - - return object; -} - -void CMapLoaderH3M::readEvents() -{ - uint32_t eventsCount = reader->readUInt32(); - for(int eventID = 0; eventID < eventsCount; ++eventID) - { - CMapEvent event; - event.name = readBasicString(); - event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); - - reader->readResourses(event.resources); - event.players = reader->readUInt8(); - if(features.levelSOD) - { - event.humanAffected = reader->readBool(); - } - else - { - event.humanAffected = true; - } - event.computerAffected = reader->readBool(); - event.firstOccurence = reader->readUInt16(); - event.nextOccurence = reader->readUInt8(); - - reader->skipZero(17); - - map->events.push_back(event); - } -} - -void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position) -{ - bool hasMessage = reader->readBool(); - if(hasMessage) - { - message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message"))); - bool hasGuards = reader->readBool(); - if(hasGuards) - readCreatureSet(guards, 7); - - reader->skipZero(4); - } -} - -std::string CMapLoaderH3M::readBasicString() -{ - return TextOperations::toUnicode(reader->readBaseString(), fileEncoding); -} - -std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIdentifier) -{ - std::string mapString = TextOperations::toUnicode(reader->readBaseString(), fileEncoding); - TextIdentifier fullIdentifier("map", mapName, stringIdentifier.get()); - - if(mapString.empty()) - return ""; - - return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString); -} - -void CMapLoaderH3M::afterRead() -{ - //convert main town positions for all players to actual object position, in H3M it is position of active tile - - for(auto & p : map->players) - { - int3 posOfMainTown = p.posOfMainTown; - if(posOfMainTown.valid() && map->isInTheMap(posOfMainTown)) - { - const TerrainTile & t = map->getTile(posOfMainTown); - - const CGObjectInstance * mainTown = nullptr; - - for(auto * obj : t.visitableObjects) - { - if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN) - { - mainTown = obj; - break; - } - } - - if(mainTown == nullptr) - continue; - - p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset(); - } - } -} - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatH3M.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 "MapFormatH3M.h" + +#include "CMap.h" +#include "MapReaderH3M.h" +#include "MapFormat.h" + +#include "../ArtifactUtils.h" +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CHeroHandler.h" +#include "../CSkillHandler.h" +#include "../CStopWatch.h" +#include "../GameSettings.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../TerrainHandler.h" +#include "../TextOperations.h" +#include "../VCMI_Lib.h" +#include "../filesystem/CBinaryReader.h" +#include "../filesystem/Filesystem.h" +#include "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGCreature.h" +#include "../mapObjects/MapObjects.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../spells/CSpellHandler.h" + +#include + +VCMI_LIB_NAMESPACE_BEGIN + +static std::string convertMapName(std::string input) +{ + boost::algorithm::to_lower(input); + boost::algorithm::trim(input); + boost::algorithm::erase_all(input, "."); + + size_t slashPos = input.find_last_of('/'); + + if(slashPos != std::string::npos) + return input.substr(slashPos + 1); + + return input; +} + +CMapLoaderH3M::CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream) + : map(nullptr) + , reader(new MapReaderH3M(stream)) + , inputStream(stream) + , mapName(convertMapName(mapName)) + , modName(modName) + , fileEncoding(encodingName) +{ +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CMapLoaderH3M::~CMapLoaderH3M() = default; + +std::unique_ptr CMapLoaderH3M::loadMap() +{ + // Init map object by parsing the input buffer + map = new CMap(); + mapHeader = std::unique_ptr(dynamic_cast(map)); + init(); + + return std::unique_ptr(dynamic_cast(mapHeader.release())); +} + +std::unique_ptr CMapLoaderH3M::loadMapHeader() +{ + // Read header + mapHeader = std::make_unique(); + readHeader(); + + return std::move(mapHeader); +} + +void CMapLoaderH3M::init() +{ + //TODO: get rid of double input process + si64 temp_size = inputStream->getSize(); + inputStream->seek(0); + + auto * temp_buffer = new ui8[temp_size]; + inputStream->read(temp_buffer, temp_size); + + // Compute checksum + boost::crc_32_type result; + result.process_bytes(temp_buffer, temp_size); + map->checksum = result.checksum(); + + delete[] temp_buffer; + inputStream->seek(0); + + readHeader(); + map->allHeroes.resize(map->allowedHeroes.size()); + + readDisposedHeroes(); + readMapOptions(); + readAllowedArtifacts(); + readAllowedSpellsAbilities(); + readRumors(); + readPredefinedHeroes(); + readTerrain(); + readObjectTemplates(); + readObjects(); + readEvents(); + + map->calculateGuardingGreaturePositions(); + afterRead(); + //map->banWaterContent(); //Not sure if force this for custom scenarios +} + +void CMapLoaderH3M::readHeader() +{ + // Map version + mapHeader->version = static_cast(reader->readUInt32()); + + if(mapHeader->version == EMapFormat::HOTA) + { + uint32_t hotaVersion = reader->readUInt32(); + features = MapFormatFeaturesH3M::find(mapHeader->version, hotaVersion); + reader->setFormatLevel(features); + + if(hotaVersion > 0) + { + bool isMirrorMap = reader->readBool(); + bool isArenaMap = reader->readBool(); + + //TODO: HotA + if (isMirrorMap) + logGlobal->warn("Map '%s': Mirror maps are not yet supported!", mapName); + + if (isArenaMap) + logGlobal->warn("Map '%s': Arena maps are not supported!", mapName); + } + + if(hotaVersion > 1) + { + [[maybe_unused]] uint8_t unknown = reader->readUInt32(); + assert(unknown == 12); + } + } + else + { + features = MapFormatFeaturesH3M::find(mapHeader->version, 0); + reader->setFormatLevel(features); + } + MapIdentifiersH3M identifierMapper; + + if (features.levelROE) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_RESTORATION_OF_ERATHIA)); + if (features.levelAB) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_ARMAGEDDONS_BLADE)); + if (features.levelSOD) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_SHADOW_OF_DEATH)); + if (features.levelWOG) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); + if (features.levelHOTA0) + identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); + + reader->setIdentifierRemapper(identifierMapper); + + // include basic mod + if(mapHeader->version == EMapFormat::WOG) + mapHeader->mods["wake-of-gods"]; + + // Read map name, description, dimensions,... + mapHeader->areAnyPlayers = reader->readBool(); + mapHeader->height = mapHeader->width = reader->readInt32(); + mapHeader->twoLevel = reader->readBool(); + mapHeader->name.appendTextID(readLocalizedString("header.name")); + mapHeader->description.appendTextID(readLocalizedString("header.description")); + mapHeader->difficulty = reader->readInt8(); + + if(features.levelAB) + mapHeader->levelLimit = reader->readUInt8(); + else + mapHeader->levelLimit = 0; + + readPlayerInfo(); + readVictoryLossConditions(); + readTeamInfo(); + readAllowedHeroes(); +} + +void CMapLoaderH3M::readPlayerInfo() +{ + for(int i = 0; i < mapHeader->players.size(); ++i) + { + auto & playerInfo = mapHeader->players[i]; + + playerInfo.canHumanPlay = reader->readBool(); + playerInfo.canComputerPlay = reader->readBool(); + + // If nobody can play with this player - skip loading of these properties + if((!(playerInfo.canHumanPlay || playerInfo.canComputerPlay))) + { + if(features.levelROE) + reader->skipUnused(6); + if(features.levelAB) + reader->skipUnused(6); + if(features.levelSOD) + reader->skipUnused(1); + continue; + } + + playerInfo.aiTactic = static_cast(reader->readUInt8()); + + if(features.levelSOD) + reader->skipUnused(1); //TODO: check meaning? + + std::set allowedFactions; + + reader->readBitmaskFactions(allowedFactions, false); + + const bool isFactionRandom = playerInfo.isFactionRandom = reader->readBool(); + const bool allFactionsAllowed = isFactionRandom && allowedFactions.size() == features.factionsCount; + + if(!allFactionsAllowed) + playerInfo.allowedFactions = allowedFactions; + + playerInfo.hasMainTown = reader->readBool(); + if(playerInfo.hasMainTown) + { + if(features.levelAB) + { + playerInfo.generateHeroAtMainTown = reader->readBool(); + reader->skipUnused(1); //TODO: check meaning? + } + else + { + playerInfo.generateHeroAtMainTown = true; + } + + playerInfo.posOfMainTown = reader->readInt3(); + } + + playerInfo.hasRandomHero = reader->readBool(); + playerInfo.mainCustomHeroId = reader->readHero(); + + if(playerInfo.mainCustomHeroId != HeroTypeID::NONE) + { + playerInfo.mainCustomHeroPortrait = reader->readHeroPortrait(); + playerInfo.mainCustomHeroNameTextId = readLocalizedString(TextIdentifier("header", "player", i, "mainHeroName")); + } + + if(features.levelAB) + { + reader->skipUnused(1); //TODO: check meaning? + uint32_t heroCount = reader->readUInt32(); + for(int pp = 0; pp < heroCount; ++pp) + { + SHeroName vv; + vv.heroId = reader->readHero(); + vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); + + playerInfo.heroesNames.push_back(vv); + } + } + } +} + +enum class EVictoryConditionType : uint8_t +{ + ARTIFACT = 0, + GATHERTROOP = 1, + GATHERRESOURCE = 2, + BUILDCITY = 3, + BUILDGRAIL = 4, + BEATHERO = 5, + CAPTURECITY = 6, + BEATMONSTER = 7, + TAKEDWELLINGS = 8, + TAKEMINES = 9, + TRANSPORTITEM = 10, + HOTA_ELIMINATE_ALL_MONSTERS = 11, + HOTA_SURVIVE_FOR_DAYS = 12, + WINSTANDARD = 255 +}; + +enum class ELossConditionType : uint8_t +{ + LOSSCASTLE = 0, + LOSSHERO = 1, + TIMEEXPIRES = 2, + LOSSSTANDARD = 255 +}; + +void CMapLoaderH3M::readVictoryLossConditions() +{ + mapHeader->triggeredEvents.clear(); + mapHeader->victoryMessage.clear(); + mapHeader->defeatMessage.clear(); + + auto vicCondition = static_cast(reader->readUInt8()); + + EventCondition victoryCondition(EventCondition::STANDARD_WIN); + EventCondition defeatCondition(EventCondition::DAYS_WITHOUT_TOWN); + defeatCondition.value = 7; + + TriggeredEvent standardVictory; + standardVictory.effect.type = EventEffect::VICTORY; + standardVictory.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + standardVictory.identifier = "standardVictory"; + standardVictory.description.clear(); // TODO: display in quest window + standardVictory.onFulfill.appendTextID("core.genrltxt.659"); + standardVictory.trigger = EventExpression(victoryCondition); + + TriggeredEvent standardDefeat; + standardDefeat.effect.type = EventEffect::DEFEAT; + standardDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.8"); + standardDefeat.identifier = "standardDefeat"; + standardDefeat.description.clear(); // TODO: display in quest window + standardDefeat.onFulfill.appendTextID("core.genrltxt.7"); + standardDefeat.trigger = EventExpression(defeatCondition); + + // Specific victory conditions + if(vicCondition == EVictoryConditionType::WINSTANDARD) + { + // create normal condition + mapHeader->triggeredEvents.push_back(standardVictory); + mapHeader->victoryIconIndex = 11; + mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); + } + else + { + TriggeredEvent specialVictory; + specialVictory.effect.type = EventEffect::VICTORY; + specialVictory.identifier = "specialVictory"; + specialVictory.description.clear(); // TODO: display in quest window + + mapHeader->victoryIconIndex = static_cast(vicCondition); + + bool allowNormalVictory = reader->readBool(); + bool appliesToAI = reader->readBool(); + + switch(vicCondition) + { + case EVictoryConditionType::ARTIFACT: + { + assert(allowNormalVictory == true); // not selectable in editor + EventCondition cond(EventCondition::HAVE_ARTIFACT); + cond.objectType = reader->readArtifact(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); + specialVictory.onFulfill.appendTextID("core.genrltxt.280"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.1"); + break; + } + case EVictoryConditionType::GATHERTROOP: + { + EventCondition cond(EventCondition::HAVE_CREATURES); + cond.objectType = reader->readCreature(); + cond.value = reader->readInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); + specialVictory.onFulfill.appendTextID("core.genrltxt.276"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.2"); + break; + } + case EVictoryConditionType::GATHERRESOURCE: + { + EventCondition cond(EventCondition::HAVE_RESOURCES); + cond.objectType = reader->readUInt8(); + cond.value = reader->readInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); + specialVictory.onFulfill.appendTextID("core.genrltxt.278"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.3"); + break; + } + case EVictoryConditionType::BUILDCITY: + { + assert(appliesToAI == true); // not selectable in editor + EventExpression::OperatorAll oper; + EventCondition cond(EventCondition::HAVE_BUILDING); + cond.position = reader->readInt3(); + cond.objectType = BuildingID::TOWN_HALL + reader->readUInt8(); + oper.expressions.emplace_back(cond); + cond.objectType = BuildingID::FORT + reader->readUInt8(); + oper.expressions.emplace_back(cond); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); + specialVictory.onFulfill.appendTextID("core.genrltxt.282"); + specialVictory.trigger = EventExpression(oper); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.4"); + break; + } + case EVictoryConditionType::BUILDGRAIL: + { + assert(allowNormalVictory == true); // not selectable in editor + assert(appliesToAI == true); // not selectable in editor + EventCondition cond(EventCondition::HAVE_BUILDING); + cond.objectType = BuildingID::GRAIL; + cond.position = reader->readInt3(); + if(cond.position.z > 2) + cond.position = int3(-1, -1, -1); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.285"); + specialVictory.onFulfill.appendTextID("core.genrltxt.284"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.5"); + break; + } + case EVictoryConditionType::BEATHERO: + { + if (!allowNormalVictory) + logGlobal->debug("Map %s: Has 'beat hero' as victory condition, but 'allow normal victory' not set. Ignoring", mapName); + allowNormalVictory = true; // H3 behavior + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = Obj::HERO; + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); + specialVictory.onFulfill.appendTextID("core.genrltxt.252"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.6"); + break; + } + case EVictoryConditionType::CAPTURECITY: + { + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::TOWN; + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); + specialVictory.onFulfill.appendTextID("core.genrltxt.249"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.7"); + break; + } + case EVictoryConditionType::BEATMONSTER: + { + assert(appliesToAI == true); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = Obj::MONSTER; + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); + specialVictory.onFulfill.appendTextID("core.genrltxt.286"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.8"); + break; + } + case EVictoryConditionType::TAKEDWELLINGS: + { + EventExpression::OperatorAll oper; + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1)); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4)); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); + specialVictory.onFulfill.appendTextID("core.genrltxt.288"); + specialVictory.trigger = EventExpression(oper); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.9"); + break; + } + case EVictoryConditionType::TAKEMINES: + { + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::MINE; + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); + specialVictory.onFulfill.appendTextID("core.genrltxt.290"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.10"); + break; + } + case EVictoryConditionType::TRANSPORTITEM: + { + assert(allowNormalVictory == true); // not selectable in editor + EventCondition cond(EventCondition::TRANSPORT); + cond.objectType = reader->readUInt8(); + cond.position = reader->readInt3(); + + specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); + specialVictory.onFulfill.appendTextID("core.genrltxt.292"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.11"); + break; + } + case EVictoryConditionType::HOTA_ELIMINATE_ALL_MONSTERS: + { + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DESTROY); + cond.objectType = Obj::MONSTER; + + specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); + specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.12"); + mapHeader->victoryIconIndex = 12; + break; + } + case EVictoryConditionType::HOTA_SURVIVE_FOR_DAYS: + { + assert(appliesToAI == false); // not selectable in editor + EventCondition cond(EventCondition::DAYS_PASSED); + cond.value = reader->readUInt32(); + + specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.daysPassed.toOthers"); + specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.daysPassed.toSelf"); + specialVictory.trigger = EventExpression(cond); + + mapHeader->victoryMessage.appendTextID("core.vcdesc.13"); + mapHeader->victoryIconIndex = 13; + break; + } + default: + assert(0); + } + + if(allowNormalVictory) + { + size_t playersOnMap = boost::range::count_if( + mapHeader->players, + [](const PlayerInfo & info) + { + return info.canAnyonePlay(); + } + ); + + if(playersOnMap == 1) + { + logGlobal->warn("Map %s: Only one player exists, but normal victory allowed!", mapName); + allowNormalVictory = false; // makes sense? Not much. Works as H3? Yes! + } + } + + // if condition is human-only turn it into following construction: AllOf(human, condition) + if(!appliesToAI) + { + EventExpression::OperatorAll oper; + EventCondition notAI(EventCondition::IS_HUMAN); + notAI.value = 1; + oper.expressions.emplace_back(notAI); + oper.expressions.push_back(specialVictory.trigger.get()); + specialVictory.trigger = EventExpression(oper); + } + + // if normal victory allowed - add one more quest + if(allowNormalVictory) + { + mapHeader->victoryMessage.appendRawString(" / "); + mapHeader->victoryMessage.appendTextID("core.vcdesc.0"); + mapHeader->triggeredEvents.push_back(standardVictory); + } + mapHeader->triggeredEvents.push_back(specialVictory); + } + + // Read loss conditions + auto lossCond = static_cast(reader->readUInt8()); + if(lossCond == ELossConditionType::LOSSSTANDARD) + { + mapHeader->defeatIconIndex = 3; + mapHeader->defeatMessage.appendTextID("core.lcdesc.0"); + } + else + { + TriggeredEvent specialDefeat; + specialDefeat.effect.type = EventEffect::DEFEAT; + specialDefeat.effect.toOtherMessage.appendTextID("core.genrltxt.5"); + specialDefeat.identifier = "specialDefeat"; + specialDefeat.description.clear(); // TODO: display in quest window + + mapHeader->defeatIconIndex = static_cast(lossCond); + + switch(lossCond) + { + case ELossConditionType::LOSSCASTLE: + { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::TOWN; + cond.position = reader->readInt3(); + + noneOf.expressions.emplace_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.251"); + specialDefeat.trigger = EventExpression(noneOf); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.1"); + break; + } + case ELossConditionType::LOSSHERO: + { + EventExpression::OperatorNone noneOf; + EventCondition cond(EventCondition::CONTROL); + cond.objectType = Obj::HERO; + cond.position = reader->readInt3(); + + noneOf.expressions.emplace_back(cond); + specialDefeat.onFulfill.appendTextID("core.genrltxt.253"); + specialDefeat.trigger = EventExpression(noneOf); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.2"); + break; + } + case ELossConditionType::TIMEEXPIRES: + { + EventCondition cond(EventCondition::DAYS_PASSED); + cond.value = reader->readUInt16(); + + specialDefeat.onFulfill.appendTextID("core.genrltxt.254"); + specialDefeat.trigger = EventExpression(cond); + + mapHeader->defeatMessage.appendTextID("core.lcdesc.3"); + break; + } + } + // turn simple loss condition into complete one that can be evaluated later: + // - any of : + // - days without town: 7 + // - all of: + // - is human + // - (expression) + + EventExpression::OperatorAll allOf; + EventCondition isHuman(EventCondition::IS_HUMAN); + isHuman.value = 1; + + allOf.expressions.emplace_back(isHuman); + allOf.expressions.push_back(specialDefeat.trigger.get()); + specialDefeat.trigger = EventExpression(allOf); + + mapHeader->triggeredEvents.push_back(specialDefeat); + } + mapHeader->triggeredEvents.push_back(standardDefeat); +} + +void CMapLoaderH3M::readTeamInfo() +{ + mapHeader->howManyTeams = reader->readUInt8(); + if(mapHeader->howManyTeams > 0) + { + // Teams + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + mapHeader->players[i].team = TeamID(reader->readUInt8()); + } + else + { + // No alliances + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; i++) + if(mapHeader->players[i].canComputerPlay || mapHeader->players[i].canHumanPlay) + mapHeader->players[i].team = TeamID(mapHeader->howManyTeams++); + } +} + +void CMapLoaderH3M::readAllowedHeroes() +{ + mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); + + if(features.levelHOTA0) + reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); + else + reader->readBitmaskHeroes(mapHeader->allowedHeroes, false); + + if(features.levelAB) + { + uint32_t placeholdersQty = reader->readUInt32(); + + for (uint32_t i = 0; i < placeholdersQty; ++i) + { + auto heroID = reader->readHero(); + mapHeader->reservedCampaignHeroes.push_back(heroID); + } + } +} + +void CMapLoaderH3M::readDisposedHeroes() +{ + // Reading disposed heroes (20 bytes) + if(features.levelSOD) + { + ui8 disp = reader->readUInt8(); + map->disposedHeroes.resize(disp); + for(int g = 0; g < disp; ++g) + { + map->disposedHeroes[g].heroId = reader->readHero(); + map->disposedHeroes[g].portrait.setNum(reader->readHeroPortrait()); + map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); + reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); + } + } +} + +void CMapLoaderH3M::readMapOptions() +{ + //omitting NULLS + reader->skipZero(31); + + if(features.levelHOTA0) + { + //TODO: HotA + bool allowSpecialMonths = reader->readBool(); + if(!allowSpecialMonths) + logGlobal->warn("Map '%s': Option 'allow special months' is not implemented!", mapName); + reader->skipZero(3); + } + + if(features.levelHOTA1) + { + // Unknown, may be another "sized bitmap", e.g + // 4 bytes - size of bitmap (16) + // 2 bytes - bitmap data (16 bits / 2 bytes) + [[maybe_unused]] uint8_t unknownConstant = reader->readUInt8(); + assert(unknownConstant == 16); + reader->skipZero(5); + } + + if(features.levelHOTA3) + { + //TODO: HotA + int32_t roundLimit = reader->readInt32(); + if(roundLimit != -1) + logGlobal->warn("Map '%s': roundLimit of %d is not implemented!", mapName, roundLimit); + } +} + +void CMapLoaderH3M::readAllowedArtifacts() +{ + map->allowedArtifact = VLC->arth->getDefaultAllowed(); + + if(features.levelAB) + { + if(features.levelHOTA0) + reader->readBitmaskArtifactsSized(map->allowedArtifact, true); + else + reader->readBitmaskArtifacts(map->allowedArtifact, true); + } + + // ban combo artifacts + if(!features.levelSOD) + { + for(CArtifact * artifact : VLC->arth->objects) + if(artifact->isCombined()) + map->allowedArtifact[artifact->getId()] = false; + } + + if(!features.levelAB) + { + map->allowedArtifact[ArtifactID::VIAL_OF_DRAGON_BLOOD] = false; + map->allowedArtifact[ArtifactID::ARMAGEDDONS_BLADE] = false; + } + + // Messy, but needed + for(TriggeredEvent & event : map->triggeredEvents) + { + auto patcher = [&](EventCondition cond) -> EventExpression::Variant + { + if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) + { + map->allowedArtifact[cond.objectType] = false; + } + return cond; + }; + + event.trigger = event.trigger.morph(patcher); + } +} + +void CMapLoaderH3M::readAllowedSpellsAbilities() +{ + map->allowedSpells = VLC->spellh->getDefaultAllowed(); + map->allowedAbilities = VLC->skillh->getDefaultAllowed(); + + if(features.levelSOD) + { + reader->readBitmaskSpells(map->allowedSpells, true); + reader->readBitmaskSkills(map->allowedAbilities, true); + } +} + +void CMapLoaderH3M::readRumors() +{ + uint32_t rumorsCount = reader->readUInt32(); + assert(rumorsCount < 1000); // sanity check + + for(int it = 0; it < rumorsCount; it++) + { + Rumor ourRumor; + ourRumor.name = readBasicString(); + ourRumor.text.appendTextID(readLocalizedString(TextIdentifier("header", "rumor", it, "text"))); + map->rumors.push_back(ourRumor); + } +} + +void CMapLoaderH3M::readPredefinedHeroes() +{ + if(!features.levelSOD) + return; + + uint32_t heroesCount = features.heroesCount; + + if(features.levelHOTA0) + heroesCount = reader->readUInt32(); + + assert(heroesCount <= features.heroesCount); + + for(int heroID = 0; heroID < heroesCount; heroID++) + { + bool custom = reader->readBool(); + if(!custom) + continue; + + auto * hero = new CGHeroInstance(); + hero->ID = Obj::HERO; + hero->subID = heroID; + + bool hasExp = reader->readBool(); + if(hasExp) + { + hero->exp = reader->readUInt32(); + } + else + { + hero->exp = 0; + } + + bool hasSecSkills = reader->readBool(); + if(hasSecSkills) + { + uint32_t howMany = reader->readUInt32(); + hero->secSkills.resize(howMany); + for(int yy = 0; yy < howMany; ++yy) + { + hero->secSkills[yy].first = reader->readSkill(); + hero->secSkills[yy].second = reader->readUInt8(); + } + } + + loadArtifactsOfHero(hero); + + bool hasCustomBio = reader->readBool(); + if(hasCustomBio) + hero->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", heroID, "biography")); + + // 0xFF is default, 00 male, 01 female + hero->gender = static_cast(reader->readUInt8()); + assert(hero->gender == EHeroGender::MALE || hero->gender == EHeroGender::FEMALE || hero->gender == EHeroGender::DEFAULT); + + bool hasCustomSpells = reader->readBool(); + if(hasCustomSpells) + reader->readBitmaskSpells(hero->spells, false); + + bool hasCustomPrimSkills = reader->readBool(); + if(hasCustomPrimSkills) + { + for(int skillID = 0; skillID < GameConstants::PRIMARY_SKILLS; skillID++) + { + hero->pushPrimSkill(static_cast(skillID), reader->readUInt8()); + } + } + map->predefinedHeroes.emplace_back(hero); + + logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getByIndex(hero->subID)->getJsonKey()); + } +} + +void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) +{ + bool hasArtSet = reader->readBool(); + + // True if artifact set is not default (hero has some artifacts) + if(!hasArtSet) + return; + + // Workaround - if hero has customized artifacts game should not attempt to add spellbook based on hero type + hero->spells.insert(SpellID::SPELLBOOK_PRESET); + + if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) + { + logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); + + hero->artifactsInBackpack.clear(); + while(!hero->artifactsWorn.empty()) + hero->eraseArtSlot(hero->artifactsWorn.begin()->first); + } + + for(int i = 0; i < features.artifactSlotsCount; i++) + loadArtifactToSlot(hero, i); + + // bag artifacts + // number of artifacts in hero's bag + int amount = reader->readUInt16(); + for(int i = 0; i < amount; ++i) + { + loadArtifactToSlot(hero, ArtifactPosition::BACKPACK_START + static_cast(hero->artifactsInBackpack.size())); + } +} + +bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) +{ + ArtifactID artifactID = reader->readArtifact(); + + if(artifactID == ArtifactID::NONE) + return false; + + const Artifact * art = artifactID.toArtifact(VLC->artifacts()); + + if(!art) + { + logGlobal->warn("Map '%s': Invalid artifact in hero's backpack, ignoring...", mapName); + return false; + } + + if(art->isBig() && slot >= ArtifactPosition::BACKPACK_START) + { + logGlobal->warn("Map '%s': A big artifact (war machine) in hero's backpack, ignoring...", mapName); + return false; + } + + // H3 bug workaround - Enemy hero on 3rd scenario of Good1.h3c campaign ("Long Live The Queen") + // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor + // Artifact seems to be missing in game, so skip artifacts that don't fit target slot + auto * artifact = ArtifactUtils::createArtifact(map, artifactID); + auto dstLoc = ArtifactLocation(hero, ArtifactPosition(slot)); + if(artifact->canBePutAt(dstLoc)) + { + artifact->putAt(dstLoc); + } + else + { + logGlobal->warn("Map '%s': Artifact '%s' can't be put at the slot %d", mapName, artifact->artType->getNameTranslated(), slot); + return false; + } + + return true; +} + +void CMapLoaderH3M::readTerrain() +{ + map->initTerrain(); + + // Read terrain + int3 pos; + for(pos.z = 0; pos.z < map->levels(); ++pos.z) + { + //OH3 format is [z][y][x] + for(pos.y = 0; pos.y < map->height; pos.y++) + { + for(pos.x = 0; pos.x < map->width; pos.x++) + { + auto & tile = map->getTile(pos); + tile.terType = VLC->terrainTypeHandler->getById(reader->readTerrain()); + tile.terView = reader->readUInt8(); + tile.riverType = VLC->riverTypeHandler->getById(reader->readRiver()); + tile.riverDir = reader->readUInt8(); + tile.roadType = VLC->roadTypeHandler->getById(reader->readRoad()); + tile.roadDir = reader->readUInt8(); + tile.extTileFlags = reader->readUInt8(); + tile.blocked = !tile.terType->isPassable(); + tile.visitable = false; + + assert(tile.terType->getId() != ETerrainId::NONE); + } + } + } + map->calculateWaterContent(); +} + +void CMapLoaderH3M::readObjectTemplates() +{ + uint32_t defAmount = reader->readUInt32(); + + templates.reserve(defAmount); + + // Read custom defs + for(int defID = 0; defID < defAmount; ++defID) + { + auto tmpl = reader->readObjectTemplate(); + templates.push_back(tmpl); + + if (!CResourceHandler::get()->existsResource(tmpl->animationFile.addPrefix("SPRITES/"))) + logMod->warn("Template animation %s of type (%d %d) is missing!", tmpl->animationFile.getOriginalName(), tmpl->id, tmpl->subid ); + } +} + +CGObjectInstance * CMapLoaderH3M::readEvent(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + auto * object = new CGEvent(); + + readBoxContent(object, mapPosition, idToBeGiven); + + reader->readBitmaskPlayers(object->availableFor, false); + object->computerActivate = reader->readBool(); + object->removeAfterVisit = reader->readBool(); + + reader->skipZero(4); + + if(features.levelHOTA3) + object->humanActivate = reader->readBool(); + else + object->humanActivate = true; + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readPandora(const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + auto * object = new CGPandoraBox(); + readBoxContent(object, mapPosition, idToBeGiven); + return object; +} + +void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPosition, const ObjectInstanceID & idToBeGiven) +{ + readMessageAndGuards(object->message, object, mapPosition); + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + + reward.heroExperience = reader->readUInt32(); + reward.manaDiff = reader->readInt32(); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, idToBeGiven); + if(auto val = reader->readUInt8()) + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, idToBeGiven); + + reader->readResourses(reward.resources); + for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) + reward.primary.at(x) = reader->readUInt8(); + + int gabn = reader->readUInt8(); //number of gained abilities + for(int oo = 0; oo < gabn; ++oo) + { + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; + } + int gart = reader->readUInt8(); //number of gained artifacts + for(int oo = 0; oo < gart; ++oo) + reward.artifacts.push_back(reader->readArtifact()); + + int gspel = reader->readUInt8(); //number of gained spells + for(int oo = 0; oo < gspel; ++oo) + reward.spells.push_back(reader->readSpell()); + + int gcre = reader->readUInt8(); //number of gained creatures + for(int oo = 0; oo < gcre; ++oo) + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + object->configuration.info.push_back(vinfo); + + reader->skipZero(8); +} + +CGObjectInstance * CMapLoaderH3M::readMonster(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + auto * object = new CGCreature(); + + if(features.levelAB) + { + object->identifier = reader->readUInt32(); + map->questIdentifierToId[object->identifier] = objectInstanceID; + } + + auto * hlp = new CStackInstance(); + hlp->count = reader->readUInt16(); + + //type will be set during initialization + object->putStack(SlotID(0), hlp); + + object->character = reader->readInt8(); + + bool hasMessage = reader->readBool(); + if(hasMessage) + { + object->message.appendTextID(readLocalizedString(TextIdentifier("monster", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); + reader->readResourses(object->resources); + object->gainedArtifact = reader->readArtifact(); + } + object->neverFlees = reader->readBool(); + object->notGrowingTeam = reader->readBool(); + reader->skipZero(2); + + if(features.levelHOTA3) + { + //TODO: HotA + int32_t agressionExact = reader->readInt32(); // -1 = default, 1-10 = possible values range + bool joinOnlyForMoney = reader->readBool(); // if true, monsters will only join for money + int32_t joinPercent = reader->readInt32(); // 100 = default, percent of monsters that will join on succesfull agression check + int32_t upgradedStack = reader->readInt32(); // Presence of upgraded stack, -1 = random, 0 = never, 1 = always + int32_t stacksCount = reader->readInt32(); // TODO: check possible values. How many creature stacks will be present on battlefield, -1 = default + + if(agressionExact != -1 || joinOnlyForMoney || joinPercent != 100 || upgradedStack != -1 || stacksCount != -1) + logGlobal->warn( + "Map '%s': Wandering monsters %s settings %d %d %d %d %d are not implemented!", + mapName, + mapPosition.toString(), + agressionExact, + int(joinOnlyForMoney), + joinPercent, + upgradedStack, + stacksCount + ); + } + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readSign(const int3 & mapPosition) +{ + auto * object = new CGSignBottle(); + object->message.appendTextID(readLocalizedString(TextIdentifier("sign", mapPosition.x, mapPosition.y, mapPosition.z, "message"))); + reader->skipZero(4); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readWitchHut() +{ + auto * object = new CGWitchHut(); + + // AB and later maps have allowed abilities defined in H3M + if(features.levelAB) + { + reader->readBitmaskSkills(object->allowedAbilities, false); + + if(object->allowedAbilities.size() != 1) + { + auto defaultAllowed = VLC->skillh->getDefaultAllowed(); + + for(int skillID = 0; skillID < VLC->skillh->size(); ++skillID) + if(defaultAllowed[skillID]) + object->allowedAbilities.insert(SecondarySkill(skillID)); + } + } + return object; +} + +CGObjectInstance * CMapLoaderH3M::readScholar() +{ + auto * object = new CGScholar(); + object->bonusType = static_cast(reader->readUInt8()); + object->bonusID = reader->readUInt8(); + reader->skipZero(6); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readGarrison(const int3 & mapPosition) +{ + auto * object = new CGGarrison(); + + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + readCreatureSet(object, 7); + if(features.levelAB) + object->removableUnits = reader->readBool(); + else + object->removableUnits = true; + + reader->skipZero(8); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readArtifact(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + ArtifactID artID = ArtifactID::NONE; //random, set later + SpellID spellID = SpellID::NONE; + auto * object = new CGArtifact(); + + readMessageAndGuards(object->message, object, mapPosition); + + if(objectTemplate->id == Obj::SPELL_SCROLL) + { + spellID = reader->readSpell32(); + artID = ArtifactID::SPELL_SCROLL; + } + else if(objectTemplate->id == Obj::ARTIFACT) + { + //specific artifact + artID = ArtifactID(objectTemplate->subid); + } + + object->storedArtifact = ArtifactUtils::createArtifact(map, artID, spellID.getNum()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGResource(); + + readMessageAndGuards(object->message, object, mapPosition); + + object->amount = reader->readUInt32(); + if(objectTemplate->subid == GameResID(EGameResID::GOLD)) + { + // Gold is multiplied by 100. + object->amount *= 100; + } + reader->skipZero(4); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readMine(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGMine(); + if(objectTemplate->subid < 7) + { + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + } + else + { + object->setOwner(PlayerColor::NEUTRAL); + reader->readBitmaskResources(object->abandonedMineResources, false); + } + return object; +} + +CGObjectInstance * CMapLoaderH3M::readDwelling(const int3 & position) +{ + auto * object = new CGDwelling(); + setOwnerAndValidate(position, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = new CGDwelling(); + + CSpecObjInfo * spec = nullptr; + switch(objectTemplate->id) + { + case Obj::RANDOM_DWELLING: + spec = new CCreGenLeveledCastleInfo(); + break; + case Obj::RANDOM_DWELLING_LVL: + spec = new CCreGenAsCastleInfo(); + break; + case Obj::RANDOM_DWELLING_FACTION: + spec = new CCreGenLeveledInfo(); + break; + default: + throw std::runtime_error("Invalid random dwelling format"); + } + spec->owner = object; + + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + + //216 and 217 + if(auto * castleSpec = dynamic_cast(spec)) + { + castleSpec->instanceId = ""; + castleSpec->identifier = reader->readUInt32(); + if(!castleSpec->identifier) + { + castleSpec->asCastle = false; + const int MASK_SIZE = 8; + ui8 mask[2]; + mask[0] = reader->readUInt8(); + mask[1] = reader->readUInt8(); + + castleSpec->allowedFactions.clear(); + castleSpec->allowedFactions.resize(VLC->townh->size(), false); + + for(int i = 0; i < MASK_SIZE; i++) + castleSpec->allowedFactions[i] = ((mask[0] & (1 << i)) > 0); + + for(int i = 0; i < (GameConstants::F_NUMBER - MASK_SIZE); i++) + castleSpec->allowedFactions[i + MASK_SIZE] = ((mask[1] & (1 << i)) > 0); + } + else + { + castleSpec->asCastle = true; + } + } + + //216 and 218 + if(auto * lvlSpec = dynamic_cast(spec)) + { + lvlSpec->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; + lvlSpec->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; + } + object->info = spec; + return object; +} + +CGObjectInstance * CMapLoaderH3M::readShrine() +{ + auto * object = new CGShrine(); + object->spell = reader->readSpell32(); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition) +{ + auto * object = new CGHeroPlaceholder(); + + setOwnerAndValidate(mapPosition, object, reader->readPlayer()); + + HeroTypeID htid = reader->readHero(); //hero type id + + if(htid.getNum() == -1) + { + object->powerRank = reader->readUInt8(); + logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); + } + else + { + object->heroType = htid; + logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + } + + return object; +} + +CGObjectInstance * CMapLoaderH3M::readGrail(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if (objectTemplate->subid < 1000) + { + map->grailPos = mapPosition; + map->grailRadius = reader->readInt32(); + } + else + { + // Battle location for arena mode in HotA + logGlobal->warn("Map '%s': Arena mode is not supported!", mapName); + } + return nullptr; +} + +CGObjectInstance * CMapLoaderH3M::readGeneric(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(VLC->objtypeh->knownSubObjects(objectTemplate->id).count(objectTemplate->subid)) + return VLC->objtypeh->getHandlerFor(objectTemplate->id, objectTemplate->subid)->create(objectTemplate); + + logGlobal->warn("Map '%s': Unrecognized object %d:%d ('%s') at %s found!", mapName, objectTemplate->id.toEnum(), objectTemplate->subid, objectTemplate->animationFile.getOriginalName(), mapPosition.toString()); + return new CGObjectInstance(); +} + +CGObjectInstance * CMapLoaderH3M::readPyramid(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(objectTemplate->subid == 0) + return new CBank(); + + return new CGObjectInstance(); +} + +CGObjectInstance * CMapLoaderH3M::readQuestGuard(const int3 & mapPosition) +{ + auto * guard = new CGQuestGuard(); + readQuest(guard, mapPosition); + return guard; +} + +CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + auto * object = readGeneric(mapPosition, objectTemplate); + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition) +{ + auto * object = new CGLighthouse(); + setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared_ptr objectTemplate) +{ + if(features.levelHOTA3) + { + //TODO: HotA + // index of guards preset. -1 = random, 0-4 = index of possible guards settings + int32_t guardsPresetIndex = reader->readInt32(); + + // presence of upgraded stack: -1 = random, 0 = never, 1 = always + int8_t upgradedStackPresence = reader->readInt8(); + + assert(vstd::iswithin(guardsPresetIndex, -1, 4)); + assert(vstd::iswithin(upgradedStackPresence, -1, 1)); + + // list of possible artifacts in reward + // - if list is empty, artifacts are either not present in reward or random + // - if non-empty, then list always have same number of elements as number of artifacts in bank + // - ArtifactID::NONE indictates random artifact, other values indicate artifact that should be used as reward + std::vector artifacts; + int artNumber = reader->readUInt32(); + for(int yy = 0; yy < artNumber; ++yy) + { + artifacts.push_back(reader->readArtifact32()); + } + + if(guardsPresetIndex != -1 || upgradedStackPresence != -1 || !artifacts.empty()) + logGlobal->warn( + "Map '%s: creature bank at %s settings %d %d %d are not implemented!", + mapName, + mapPosition.toString(), + guardsPresetIndex, + int(upgradedStackPresence), + artifacts.size() + ); + } + + return readGeneric(mapPosition, objectTemplate); +} + +CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + switch(objectTemplate->id) + { + case Obj::EVENT: + return readEvent(mapPosition, objectInstanceID); + + case Obj::HERO: + case Obj::RANDOM_HERO: + case Obj::PRISON: + return readHero(mapPosition, objectInstanceID); + + case Obj::MONSTER: + case Obj::RANDOM_MONSTER: + case Obj::RANDOM_MONSTER_L1: + case Obj::RANDOM_MONSTER_L2: + case Obj::RANDOM_MONSTER_L3: + case Obj::RANDOM_MONSTER_L4: + case Obj::RANDOM_MONSTER_L5: + case Obj::RANDOM_MONSTER_L6: + case Obj::RANDOM_MONSTER_L7: + return readMonster(mapPosition, objectInstanceID); + + case Obj::OCEAN_BOTTLE: + case Obj::SIGN: + return readSign(mapPosition); + + case Obj::SEER_HUT: + return readSeerHut(mapPosition, objectInstanceID); + + case Obj::WITCH_HUT: + return readWitchHut(); + case Obj::SCHOLAR: + return readScholar(); + + case Obj::GARRISON: + case Obj::GARRISON2: + return readGarrison(mapPosition); + + case Obj::ARTIFACT: + case Obj::RANDOM_ART: + case Obj::RANDOM_TREASURE_ART: + case Obj::RANDOM_MINOR_ART: + case Obj::RANDOM_MAJOR_ART: + case Obj::RANDOM_RELIC_ART: + case Obj::SPELL_SCROLL: + return readArtifact(mapPosition, objectTemplate); + + case Obj::RANDOM_RESOURCE: + case Obj::RESOURCE: + return readResource(mapPosition, objectTemplate); + case Obj::RANDOM_TOWN: + case Obj::TOWN: + return readTown(mapPosition, objectTemplate); + + case Obj::MINE: + case Obj::ABANDONED_MINE: + return readMine(mapPosition, objectTemplate); + + case Obj::CREATURE_GENERATOR1: + case Obj::CREATURE_GENERATOR2: + case Obj::CREATURE_GENERATOR3: + case Obj::CREATURE_GENERATOR4: + return readDwelling(mapPosition); + + case Obj::SHRINE_OF_MAGIC_INCANTATION: + case Obj::SHRINE_OF_MAGIC_GESTURE: + case Obj::SHRINE_OF_MAGIC_THOUGHT: + return readShrine(); + + case Obj::PANDORAS_BOX: + return readPandora(mapPosition, objectInstanceID); + + case Obj::GRAIL: + return readGrail(mapPosition, objectTemplate); + + case Obj::RANDOM_DWELLING: + case Obj::RANDOM_DWELLING_LVL: + case Obj::RANDOM_DWELLING_FACTION: + return readDwellingRandom(mapPosition, objectTemplate); + + case Obj::QUEST_GUARD: + return readQuestGuard(mapPosition); + + case Obj::SHIPYARD: + return readShipyard(mapPosition, objectTemplate); + + case Obj::HERO_PLACEHOLDER: + return readHeroPlaceholder(mapPosition); + + case Obj::PYRAMID: + return readPyramid(mapPosition, objectTemplate); + + case Obj::LIGHTHOUSE: + return readLighthouse(mapPosition); + + case Obj::CREATURE_BANK: + case Obj::DERELICT_SHIP: + case Obj::DRAGON_UTOPIA: + case Obj::CRYPT: + case Obj::SHIPWRECK: + return readBank(mapPosition, objectTemplate); + + default: //any other object + return readGeneric(mapPosition, objectTemplate); + } +} + +void CMapLoaderH3M::readObjects() +{ + uint32_t objectsCount = reader->readUInt32(); + + for(uint32_t i = 0; i < objectsCount; ++i) + { + int3 mapPosition = reader->readInt3(); + + uint32_t defIndex = reader->readUInt32(); + ObjectInstanceID objectInstanceID = ObjectInstanceID(static_cast(map->objects.size())); + + std::shared_ptr objectTemplate = templates.at(defIndex); + reader->skipZero(5); + + CGObjectInstance * newObject = readObject(objectTemplate, mapPosition, objectInstanceID); + + if(!newObject) + continue; + + newObject->pos = mapPosition; + newObject->ID = objectTemplate->id; + newObject->id = objectInstanceID; + if(newObject->ID != Obj::HERO && newObject->ID != Obj::HERO_PLACEHOLDER && newObject->ID != Obj::PRISON) + { + newObject->subID = objectTemplate->subid; + } + newObject->appearance = objectTemplate; + assert(objectInstanceID == ObjectInstanceID((si32)map->objects.size())); + + if (newObject->isVisitable() && !map->isInTheMap(newObject->visitablePos())) + logGlobal->error("Map '%s': Object at %s - outside of map borders!", mapName, mapPosition.toString()); + + { + //TODO: define valid typeName and subtypeName for H3M maps + //boost::format fmt("%s_%d"); + //fmt % nobj->typeName % nobj->id.getNum(); + boost::format fmt("obj_%d"); + fmt % newObject->id.getNum(); + newObject->instanceName = fmt.str(); + } + map->addNewObject(newObject); + } + + std::sort( + map->heroesOnMap.begin(), + map->heroesOnMap.end(), + [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) + { + return a->subID < b->subID; + } + ); +} + +void CMapLoaderH3M::readCreatureSet(CCreatureSet * out, int number) +{ + for(int index = 0; index < number; ++index) + { + CreatureID creatureID = reader->readCreature(); + int count = reader->readUInt16(); + + // Empty slot + if(creatureID == CreatureID::NONE) + continue; + + auto * result = new CStackInstance(); + result->count = count; + + if(creatureID < CreatureID::NONE) + { + int value = -creatureID.getNum() - 2; + assert(value >= 0 && value < 14); + uint8_t level = value / 2; + uint8_t upgrade = value % 2; + + //this will happen when random object has random army + result->randomStack = CStackInstance::RandomStackInfo{level, upgrade}; + } + else + { + result->setType(creatureID); + } + + out->putStack(SlotID(index), result); + } + + out->validTypes(true); +} + +void CMapLoaderH3M::setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner) +{ + assert(owner.isValidPlayer() || owner == PlayerColor::NEUTRAL); + + if(owner == PlayerColor::NEUTRAL) + { + object->setOwner(PlayerColor::NEUTRAL); + return; + } + + if(!owner.isValidPlayer()) + { + object->setOwner(PlayerColor::NEUTRAL); + logGlobal->warn("Map '%s': Object at %s - owned by invalid player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum())); + return; + } + + if(!mapHeader->players[owner.getNum()].canAnyonePlay()) + { + object->setOwner(PlayerColor::NEUTRAL); + logGlobal->warn("Map '%s': Object at %s - owned by non-existing player %d! Will be set to neutral!", mapName, mapPosition.toString(), int(owner.getNum()) + ); + return; + } + + object->setOwner(owner); +} + +CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) +{ + auto * object = new CGHeroInstance(); + + if(features.levelAB) + { + unsigned int identifier = reader->readUInt32(); + map->questIdentifierToId[identifier] = objectInstanceID; + } + + PlayerColor owner = reader->readPlayer(); + object->subID = reader->readHero().getNum(); + + //If hero of this type has been predefined, use that as a base. + //Instance data will overwrite the predefined values where appropriate. + for(auto & elem : map->predefinedHeroes) + { + if(elem->subID == object->subID) + { + logGlobal->debug("Hero %d will be taken from the predefined heroes list.", object->subID); + delete object; + object = elem; + break; + } + } + + setOwnerAndValidate(mapPosition, object, owner); + + for(auto & elem : map->disposedHeroes) + { + if(elem.heroId.getNum() == object->subID) + { + object->nameCustomTextId = elem.name; + object->customPortraitSource = elem.portrait; + break; + } + } + + bool hasName = reader->readBool(); + if(hasName) + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); + + if(features.levelSOD) + { + bool hasCustomExperience = reader->readBool(); + if(hasCustomExperience) + object->exp = reader->readUInt32(); + } + else + { + object->exp = reader->readUInt32(); + + //0 means "not set" in <=AB maps + if(!object->exp) + object->exp = CGHeroInstance::UNINITIALIZED_EXPERIENCE; + } + + bool hasPortrait = reader->readBool(); + if(hasPortrait) + object->customPortraitSource = reader->readHeroPortrait(); + + bool hasSecSkills = reader->readBool(); + if(hasSecSkills) + { + if(!object->secSkills.empty()) + { + if(object->secSkills[0].first != SecondarySkill::DEFAULT) + logGlobal->debug("Map '%s': Hero %s subID=%d has set secondary skills twice (in map properties and on adventure map instance). Using the latter set...", mapName, object->getNameTextID(), object->subID); + object->secSkills.clear(); + } + + uint32_t skillsCount = reader->readUInt32(); + object->secSkills.resize(skillsCount); + for(int i = 0; i < skillsCount; ++i) + { + object->secSkills[i].first = reader->readSkill(); + object->secSkills[i].second = reader->readUInt8(); + } + } + + bool hasGarison = reader->readBool(); + if(hasGarison) + readCreatureSet(object, 7); + + object->formation = static_cast(reader->readUInt8()); + assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); + + loadArtifactsOfHero(object); + object->patrol.patrolRadius = reader->readUInt8(); + object->patrol.patrolling = (object->patrol.patrolRadius != 0xff); + + if(features.levelAB) + { + bool hasCustomBiography = reader->readBool(); + if(hasCustomBiography) + object->biographyCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "biography")); + + object->gender = static_cast(reader->readUInt8()); + assert(object->gender == EHeroGender::MALE || object->gender == EHeroGender::FEMALE || object->gender == EHeroGender::DEFAULT); + } + else + { + object->gender = EHeroGender::DEFAULT; + } + + // Spells + if(features.levelSOD) + { + bool hasCustomSpells = reader->readBool(); + if(hasCustomSpells) + { + if(!object->spells.empty()) + { + object->spells.clear(); + logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); + } + + object->spells.insert(SpellID::PRESET); //placeholder "preset spells" + + reader->readBitmaskSpells(object->spells, false); + } + } + else if(features.levelAB) + { + //we can read one spell + SpellID spell = reader->readSpell(); + + if(spell != SpellID::NONE) + object->spells.insert(spell); + } + + if(features.levelSOD) + { + bool hasCustomPrimSkills = reader->readBool(); + if(hasCustomPrimSkills) + { + auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); + if(ps->size()) + { + logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); + for(const auto & b : *ps) + object->removeBonus(b); + } + + for(int xx = 0; xx < GameConstants::PRIMARY_SKILLS; ++xx) + { + object->pushPrimSkill(static_cast(xx), reader->readUInt8()); + } + } + } + + if (object->subID != -1) + logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + else + logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); + + reader->skipZero(16); + return object; +} + +CGObjectInstance * CMapLoaderH3M::readSeerHut(const int3 & position, const ObjectInstanceID & idToBeGiven) +{ + auto * hut = new CGSeerHut(); + + uint32_t questsCount = 1; + + if(features.levelHOTA3) + questsCount = reader->readUInt32(); + + //TODO: HotA + if(questsCount > 1) + logGlobal->warn("Map '%s': Seer Hut at %s - %d quests are not implemented!", mapName, position.toString(), questsCount); + + for(size_t i = 0; i < questsCount; ++i) + readSeerHutQuest(hut, position, idToBeGiven); + + if(features.levelHOTA3) + { + uint32_t repeateableQuestsCount = reader->readUInt32(); + hut->quest->repeatedQuest = repeateableQuestsCount != 0; + + if(repeateableQuestsCount != 0) + logGlobal->warn("Map '%s': Seer Hut at %s - %d repeatable quests are not implemented!", mapName, position.toString(), repeateableQuestsCount); + + for(size_t i = 0; i < repeateableQuestsCount; ++i) + readSeerHutQuest(hut, position, idToBeGiven); + } + + reader->skipZero(2); + + return hut; +} + +enum class ESeerHutRewardType : uint8_t +{ + NOTHING = 0, + EXPERIENCE = 1, + MANA_POINTS = 2, + MORALE = 3, + LUCK = 4, + RESOURCES = 5, + PRIMARY_SKILL = 6, + SECONDARY_SKILL = 7, + ARTIFACT = 8, + SPELL = 9, + CREATURE = 10, +}; + +enum class EQuestMission { + NONE = 0, + LEVEL = 1, + PRIMARY_SKILL = 2, + KILL_HERO = 3, + KILL_CREATURE = 4, + ARTIFACT = 5, + ARMY = 6, + RESOURCES = 7, + HERO = 8, + PLAYER = 9, + HOTA_MULTI = 10, + // end of H3 missions + KEYMASTER = 100, + HOTA_HERO_CLASS = 101, + HOTA_REACH_DATE = 102 +}; + +void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven) +{ + EQuestMission missionType = EQuestMission::NONE; + if(features.levelAB) + { + missionType = static_cast(readQuest(hut, position)); + } + else + { + //RoE + auto artID = reader->readArtifact(); + if(artID != ArtifactID::NONE) + { + //not none quest + hut->quest->mission.artifacts.push_back(artID); + missionType = EQuestMission::ARTIFACT; + } + hut->quest->lastDay = -1; //no timeout + hut->quest->isCustomFirst = false; + hut->quest->isCustomNext = false; + hut->quest->isCustomComplete = false; + } + + if(missionType != EQuestMission::NONE) + { + auto rewardType = static_cast(reader->readUInt8()); + Rewardable::VisitInfo vinfo; + auto & reward = vinfo.reward; + switch(rewardType) + { + case ESeerHutRewardType::NOTHING: + { + // no-op + break; + } + case ESeerHutRewardType::EXPERIENCE: + { + reward.heroExperience = reader->readUInt32(); + break; + } + case ESeerHutRewardType::MANA_POINTS: + { + reward.manaDiff = reader->readUInt32(); + break; + } + case ESeerHutRewardType::MORALE: + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + break; + } + case ESeerHutRewardType::LUCK: + { + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + break; + } + case ESeerHutRewardType::RESOURCES: + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt32(); + + assert(rId < features.resourcesCount); + + reward.resources[rId] = rVal; + break; + } + case ESeerHutRewardType::PRIMARY_SKILL: + { + auto rId = reader->readUInt8(); + auto rVal = reader->readUInt8(); + + reward.primary.at(rId) = rVal; + break; + } + case ESeerHutRewardType::SECONDARY_SKILL: + { + auto rId = reader->readSkill(); + auto rVal = reader->readUInt8(); + + reward.secondary[rId] = rVal; + break; + } + case ESeerHutRewardType::ARTIFACT: + { + reward.artifacts.push_back(reader->readArtifact()); + break; + } + case ESeerHutRewardType::SPELL: + { + reward.spells.push_back(reader->readSpell()); + break; + } + case ESeerHutRewardType::CREATURE: + { + auto rId = reader->readCreature(); + auto rVal = reader->readUInt16(); + + reward.creatures.emplace_back(rId, rVal); + break; + } + default: + { + assert(0); + } + } + + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; + hut->configuration.info.push_back(vinfo); + } + else + { + // missionType==255 + reader->skipZero(1); + } +} + +int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) +{ + auto missionId = reader->readUInt8(); + + switch(static_cast(missionId)) + { + case EQuestMission::NONE: + return missionId; + case EQuestMission::PRIMARY_SKILL: + { + for(int x = 0; x < 4; ++x) + { + guard->quest->mission.primary[x] = reader->readUInt8(); + } + break; + } + case EQuestMission::LEVEL: + { + guard->quest->mission.heroLevel = reader->readUInt32(); + break; + } + case EQuestMission::KILL_HERO: + case EQuestMission::KILL_CREATURE: + { + guard->quest->killTarget = ObjectInstanceID(reader->readUInt32()); + break; + } + case EQuestMission::ARTIFACT: + { + int artNumber = reader->readUInt8(); + for(int yy = 0; yy < artNumber; ++yy) + { + auto artid = reader->readArtifact(); + guard->quest->mission.artifacts.push_back(artid); + map->allowedArtifact[artid] = false; //these are unavailable for random generation + } + break; + } + case EQuestMission::ARMY: + { + int typeNumber = reader->readUInt8(); + guard->quest->mission.creatures.resize(typeNumber); + for(int hh = 0; hh < typeNumber; ++hh) + { + guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->mission.creatures[hh].count = reader->readUInt16(); + } + break; + } + case EQuestMission::RESOURCES: + { + for(int x = 0; x < 7; ++x) + guard->quest->mission.resources[x] = reader->readUInt32(); + + break; + } + case EQuestMission::HERO: + { + guard->quest->mission.heroes.push_back(reader->readHero()); + break; + } + case EQuestMission::PLAYER: + { + guard->quest->mission.players.push_back(reader->readPlayer()); + break; + } + case EQuestMission::HOTA_MULTI: + { + uint32_t missionSubID = reader->readUInt32(); + + if(missionSubID == 0) + { + missionId = int(EQuestMission::HOTA_HERO_CLASS); + std::set heroClasses; + reader->readBitmaskHeroClassesSized(heroClasses, false); + for(auto & hc : heroClasses) + guard->quest->mission.heroClasses.push_back(hc); + break; + } + if(missionSubID == 1) + { + missionId = int(EQuestMission::HOTA_REACH_DATE); + guard->quest->mission.daysPassed = reader->readUInt32() + 1; + break; + } + break; + } + default: + { + assert(0); + } + } + + guard->quest->lastDay = reader->readInt32(); + guard->quest->firstVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "firstVisit"))); + guard->quest->nextVisitText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "nextVisit"))); + guard->quest->completedText.appendTextID(readLocalizedString(TextIdentifier("quest", position.x, position.y, position.z, "completed"))); + guard->quest->isCustomFirst = !guard->quest->firstVisitText.empty(); + guard->quest->isCustomNext = !guard->quest->nextVisitText.empty(); + guard->quest->isCustomComplete = !guard->quest->completedText.empty(); + return missionId; +} + +CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_ptr objectTemplate) +{ + auto * object = new CGTownInstance(); + if(features.levelAB) + object->identifier = reader->readUInt32(); + + setOwnerAndValidate(position, object, reader->readPlayer()); + + std::optional faction; + if (objectTemplate->id == Obj::TOWN) + faction = FactionID(objectTemplate->subid); + + bool hasName = reader->readBool(); + if(hasName) + object->setNameTextId(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "name"))); + + bool hasGarrison = reader->readBool(); + if(hasGarrison) + readCreatureSet(object, 7); + + object->formation = static_cast(reader->readUInt8()); + assert(object->formation == EArmyFormation::LOOSE || object->formation == EArmyFormation::TIGHT); + + bool hasCustomBuildings = reader->readBool(); + if(hasCustomBuildings) + { + reader->readBitmaskBuildings(object->builtBuildings, faction); + reader->readBitmaskBuildings(object->forbiddenBuildings, faction); + } + // Standard buildings + else + { + bool hasFort = reader->readBool(); + if(hasFort) + object->builtBuildings.insert(BuildingID::FORT); + + //means that set of standard building should be included + object->builtBuildings.insert(BuildingID::DEFAULT); + } + + if(features.levelAB) + { + std::set spellsMask; + + reader->readBitmaskSpells(spellsMask, false); + std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->obligatorySpells)); + } + + { + std::set spellsMask; + + reader->readBitmaskSpells(spellsMask, true); + std::copy(spellsMask.begin(), spellsMask.end(), std::back_inserter(object->possibleSpells)); + + auto defaultAllowed = VLC->spellh->getDefaultAllowed(); + + //add all spells from mods + for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) + if(defaultAllowed[i]) + object->possibleSpells.emplace_back(i); + } + + if(features.levelHOTA1) + { + // TODO: HOTA support + [[maybe_unused]] bool spellResearchAvailable = reader->readBool(); + } + + // Read castle events + uint32_t eventsCount = reader->readUInt32(); + + for(int eventID = 0; eventID < eventsCount; ++eventID) + { + CCastleEvent event; + event.name = readBasicString(); + event.message.appendTextID(readLocalizedString(TextIdentifier("town", position.x, position.y, position.z, "event", eventID, "description"))); + + reader->readResourses(event.resources); + + event.players = reader->readUInt8(); + if(features.levelSOD) + event.humanAffected = reader->readBool(); + else + event.humanAffected = true; + + event.computerAffected = reader->readUInt8(); + event.firstOccurence = reader->readUInt16(); + event.nextOccurence = reader->readUInt8(); + + reader->skipZero(17); + + // New buildings + reader->readBitmaskBuildings(event.buildings, faction); + + event.creatures.resize(7); + for(int i = 0; i < 7; ++i) + event.creatures[i] = reader->readUInt16(); + + reader->skipZero(4); + object->events.push_back(event); + } + + if(features.levelSOD) + { + object->alignmentToPlayer = PlayerColor::NEUTRAL; // "same as owner or random" + + uint8_t alignment = reader->readUInt8(); + + if(alignment != 255) + { + if(alignment < PlayerColor::PLAYER_LIMIT.getNum()) + { + if (mapHeader->players[alignment].canAnyonePlay()) + object->alignmentToPlayer = PlayerColor(alignment); + else + logGlobal->warn("%s - Aligment of town at %s is invalid! Player %d is not present on map!", mapName, position.toString(), int(alignment)); + } + else + { + // TODO: HOTA support + uint8_t invertedAlignment = alignment - PlayerColor::PLAYER_LIMIT.getNum(); + + if(invertedAlignment < PlayerColor::PLAYER_LIMIT.getNum()) + { + logGlobal->warn("%s - Aligment of town at %s 'not as player %d' is not implemented!", mapName, position.toString(), alignment - PlayerColor::PLAYER_LIMIT.getNum()); + } + else + { + logGlobal->warn("%s - Aligment of town at %s is corrupted!!", mapName, position.toString()); + } + } + } + } + reader->skipZero(3); + + return object; +} + +void CMapLoaderH3M::readEvents() +{ + uint32_t eventsCount = reader->readUInt32(); + for(int eventID = 0; eventID < eventsCount; ++eventID) + { + CMapEvent event; + event.name = readBasicString(); + event.message.appendTextID(readLocalizedString(TextIdentifier("event", eventID, "description"))); + + reader->readResourses(event.resources); + event.players = reader->readUInt8(); + if(features.levelSOD) + { + event.humanAffected = reader->readBool(); + } + else + { + event.humanAffected = true; + } + event.computerAffected = reader->readBool(); + event.firstOccurence = reader->readUInt16(); + event.nextOccurence = reader->readUInt8(); + + reader->skipZero(17); + + map->events.push_back(event); + } +} + +void CMapLoaderH3M::readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position) +{ + bool hasMessage = reader->readBool(); + if(hasMessage) + { + message.appendTextID(readLocalizedString(TextIdentifier("guards", position.x, position.y, position.z, "message"))); + bool hasGuards = reader->readBool(); + if(hasGuards) + readCreatureSet(guards, 7); + + reader->skipZero(4); + } +} + +std::string CMapLoaderH3M::readBasicString() +{ + return TextOperations::toUnicode(reader->readBaseString(), fileEncoding); +} + +std::string CMapLoaderH3M::readLocalizedString(const TextIdentifier & stringIdentifier) +{ + std::string mapString = TextOperations::toUnicode(reader->readBaseString(), fileEncoding); + TextIdentifier fullIdentifier("map", mapName, stringIdentifier.get()); + + if(mapString.empty()) + return ""; + + return mapRegisterLocalizedString(modName, *mapHeader, fullIdentifier, mapString); +} + +void CMapLoaderH3M::afterRead() +{ + //convert main town positions for all players to actual object position, in H3M it is position of active tile + + for(auto & p : map->players) + { + int3 posOfMainTown = p.posOfMainTown; + if(posOfMainTown.valid() && map->isInTheMap(posOfMainTown)) + { + const TerrainTile & t = map->getTile(posOfMainTown); + + const CGObjectInstance * mainTown = nullptr; + + for(auto * obj : t.visitableObjects) + { + if(obj->ID == Obj::TOWN || obj->ID == Obj::RANDOM_TOWN) + { + mainTown = obj; + break; + } + } + + if(mainTown == nullptr) + continue; + + p.posOfMainTown = posOfMainTown + mainTown->getVisitableOffset(); + } + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatH3M.h b/lib/mapping/MapFormatH3M.h index 3eb7a3226..41bf69d69 100644 --- a/lib/mapping/MapFormatH3M.h +++ b/lib/mapping/MapFormatH3M.h @@ -1,254 +1,254 @@ -/* - * MapFormatH3M.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 "CMapService.h" -#include "MapFeaturesH3M.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CGHeroInstance; -class MapReaderH3M; -class MetaString; -class CArtifactInstance; -class CGObjectInstance; -class CGSeerHut; -class IQuestObject; -class CGTownInstance; -class CCreatureSet; -class CInputStream; -class TextIdentifier; -class CGPandoraBox; - -class ObjectInstanceID; -class BuildingID; -class ObjectTemplate; -class SpellID; -class PlayerColor; -class int3; - -class DLL_LINKAGE CMapLoaderH3M : public IMapLoader -{ -public: - /** - * Default constructor. - * - * @param stream a stream containing the map data - */ - CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream); - - /** - * Destructor. - */ - ~CMapLoaderH3M(); - - /** - * Loads the VCMI/H3 map file. - * - * @return a unique ptr of the loaded map class - */ - std::unique_ptr loadMap() override; - - /** - * Loads the VCMI/H3 map header. - * - * @return a unique ptr of the loaded map header class - */ - std::unique_ptr loadMapHeader() override; - -private: - /** - * Initializes the map object from parsing the input buffer. - */ - void init(); - - /** - * Reads the map header. - */ - void readHeader(); - - /** - * Reads player information. - */ - void readPlayerInfo(); - - /** - * Reads victory/loss conditions. - */ - void readVictoryLossConditions(); - - /** - * Reads team information. - */ - void readTeamInfo(); - - /** - * Reads the list of map flags. - */ - void readMapOptions(); - - /** - * Reads the list of allowed heroes. - */ - void readAllowedHeroes(); - - /** - * Reads the list of disposed heroes. - */ - void readDisposedHeroes(); - - /** - * Reads the list of allowed artifacts. - */ - void readAllowedArtifacts(); - - /** - * Reads the list of allowed spells and abilities. - */ - void readAllowedSpellsAbilities(); - - /** - * Loads artifacts of a hero. - * - * @param hero the hero which should hold those artifacts - */ - void loadArtifactsOfHero(CGHeroInstance * hero); - - /** - * Loads an artifact to the given slot of the specified hero. - * - * @param hero the hero which should hold that artifact - * @param slot the artifact slot where to place that artifact - * @return true if it loaded an artifact - */ - bool loadArtifactToSlot(CGHeroInstance * hero, int slot); - - /** - * Read rumors. - */ - void readRumors(); - - /** - * Reads predefined heroes. - */ - void readPredefinedHeroes(); - - /** - * Reads terrain data. - */ - void readTerrain(); - - /** - * Reads custom(map) def information. - */ - void readObjectTemplates(); - - /** - * Reads objects(towns, mines,...). - */ - void readObjects(); - - /// Reads single object from input stream based on template - CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - - CGObjectInstance * readEvent(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readHero(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readSign(const int3 & position); - CGObjectInstance * readWitchHut(); - CGObjectInstance * readScholar(); - CGObjectInstance * readGarrison(const int3 & mapPosition); - CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); - CGObjectInstance * readDwelling(const int3 & position); - CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readShrine(); - CGObjectInstance * readHeroPlaceholder(const int3 & position); - CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); - CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); - CGObjectInstance * readQuestGuard(const int3 & position); - CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate); - CGObjectInstance * readLighthouse(const int3 & mapPosition); - CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr objectTemplate); - CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); - - /** - * Reads a creature set. - * - * @param out the loaded creature set - * @param number the count of creatures to read - */ - void readCreatureSet(CCreatureSet * out, int number); - - /** - * Reads a quest for the given quest guard. - * - * @param guard the quest guard where that quest should be applied to - */ - void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); - - /** - * Reads a quest for the given quest guard. - * - * @param guard the quest guard where that quest should be applied to - */ - int readQuest(IQuestObject * guard, const int3 & position); - - void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); - - /** - * Reads events. - */ - void readEvents(); - - /** - * read optional message and optional guards - */ - void readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position); - - /// reads string from input stream and converts it to unicode - std::string readBasicString(); - - /// reads string from input stream, converts it to unicode and attempts to translate it - std::string readLocalizedString(const TextIdentifier & identifier); - - void setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner); - - void afterRead(); - - MapFormatFeaturesH3M features; - - /** List of templates loaded from the map, used on later stage to create - * objects but not needed for fully functional CMap */ - std::vector> templates; - - /** ptr to the map object which gets filled by data from the buffer */ - CMap * map; - - /** - * ptr to the map header object which gets filled by data from the buffer. - * (when loading a map then the mapHeader ptr points to the same object) - */ - std::unique_ptr mapHeader; - std::unique_ptr reader; - CInputStream * inputStream; - - std::string mapName; - std::string modName; - std::string fileEncoding; - -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatH3M.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 "CMapService.h" +#include "MapFeaturesH3M.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class MapReaderH3M; +class MetaString; +class CArtifactInstance; +class CGObjectInstance; +class CGSeerHut; +class IQuestObject; +class CGTownInstance; +class CCreatureSet; +class CInputStream; +class TextIdentifier; +class CGPandoraBox; + +class ObjectInstanceID; +class BuildingID; +class ObjectTemplate; +class SpellID; +class PlayerColor; +class int3; + +class DLL_LINKAGE CMapLoaderH3M : public IMapLoader +{ +public: + /** + * Default constructor. + * + * @param stream a stream containing the map data + */ + CMapLoaderH3M(const std::string & mapName, const std::string & modName, const std::string & encodingName, CInputStream * stream); + + /** + * Destructor. + */ + ~CMapLoaderH3M(); + + /** + * Loads the VCMI/H3 map file. + * + * @return a unique ptr of the loaded map class + */ + std::unique_ptr loadMap() override; + + /** + * Loads the VCMI/H3 map header. + * + * @return a unique ptr of the loaded map header class + */ + std::unique_ptr loadMapHeader() override; + +private: + /** + * Initializes the map object from parsing the input buffer. + */ + void init(); + + /** + * Reads the map header. + */ + void readHeader(); + + /** + * Reads player information. + */ + void readPlayerInfo(); + + /** + * Reads victory/loss conditions. + */ + void readVictoryLossConditions(); + + /** + * Reads team information. + */ + void readTeamInfo(); + + /** + * Reads the list of map flags. + */ + void readMapOptions(); + + /** + * Reads the list of allowed heroes. + */ + void readAllowedHeroes(); + + /** + * Reads the list of disposed heroes. + */ + void readDisposedHeroes(); + + /** + * Reads the list of allowed artifacts. + */ + void readAllowedArtifacts(); + + /** + * Reads the list of allowed spells and abilities. + */ + void readAllowedSpellsAbilities(); + + /** + * Loads artifacts of a hero. + * + * @param hero the hero which should hold those artifacts + */ + void loadArtifactsOfHero(CGHeroInstance * hero); + + /** + * Loads an artifact to the given slot of the specified hero. + * + * @param hero the hero which should hold that artifact + * @param slot the artifact slot where to place that artifact + * @return true if it loaded an artifact + */ + bool loadArtifactToSlot(CGHeroInstance * hero, int slot); + + /** + * Read rumors. + */ + void readRumors(); + + /** + * Reads predefined heroes. + */ + void readPredefinedHeroes(); + + /** + * Reads terrain data. + */ + void readTerrain(); + + /** + * Reads custom(map) def information. + */ + void readObjectTemplates(); + + /** + * Reads objects(towns, mines,...). + */ + void readObjects(); + + /// Reads single object from input stream based on template + CGObjectInstance * readObject(std::shared_ptr objectTemplate, const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + + CGObjectInstance * readEvent(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readMonster(const int3 & objectPosition, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readHero(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readSeerHut(const int3 & initialPos, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readTown(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readSign(const int3 & position); + CGObjectInstance * readWitchHut(); + CGObjectInstance * readScholar(); + CGObjectInstance * readGarrison(const int3 & mapPosition); + CGObjectInstance * readArtifact(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readResource(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readMine(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readPandora(const int3 & position, const ObjectInstanceID & idToBeGiven); + CGObjectInstance * readDwelling(const int3 & position); + CGObjectInstance * readDwellingRandom(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readShrine(); + CGObjectInstance * readHeroPlaceholder(const int3 & position); + CGObjectInstance * readGrail(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr objTempl); + CGObjectInstance * readQuestGuard(const int3 & position); + CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr objectTemplate); + CGObjectInstance * readLighthouse(const int3 & mapPosition); + CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr objectTemplate); + CGObjectInstance * readBank(const int3 & position, std::shared_ptr objectTemplate); + + /** + * Reads a creature set. + * + * @param out the loaded creature set + * @param number the count of creatures to read + */ + void readCreatureSet(CCreatureSet * out, int number); + + /** + * Reads a quest for the given quest guard. + * + * @param guard the quest guard where that quest should be applied to + */ + void readBoxContent(CGPandoraBox * object, const int3 & position, const ObjectInstanceID & idToBeGiven); + + /** + * Reads a quest for the given quest guard. + * + * @param guard the quest guard where that quest should be applied to + */ + int readQuest(IQuestObject * guard, const int3 & position); + + void readSeerHutQuest(CGSeerHut * hut, const int3 & position, const ObjectInstanceID & idToBeGiven); + + /** + * Reads events. + */ + void readEvents(); + + /** + * read optional message and optional guards + */ + void readMessageAndGuards(MetaString & message, CCreatureSet * guards, const int3 & position); + + /// reads string from input stream and converts it to unicode + std::string readBasicString(); + + /// reads string from input stream, converts it to unicode and attempts to translate it + std::string readLocalizedString(const TextIdentifier & identifier); + + void setOwnerAndValidate(const int3 & mapPosition, CGObjectInstance * object, const PlayerColor & owner); + + void afterRead(); + + MapFormatFeaturesH3M features; + + /** List of templates loaded from the map, used on later stage to create + * objects but not needed for fully functional CMap */ + std::vector> templates; + + /** ptr to the map object which gets filled by data from the buffer */ + CMap * map; + + /** + * ptr to the map header object which gets filled by data from the buffer. + * (when loading a map then the mapHeader ptr points to the same object) + */ + std::unique_ptr mapHeader; + std::unique_ptr reader; + CInputStream * inputStream; + + std::string mapName; + std::string modName; + std::string fileEncoding; + +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index a26c8b378..0208204a1 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1,1480 +1,1480 @@ -/* - * MapFormatJson.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 "MapFormatJson.h" - -#include "../filesystem/CInputStream.h" -#include "../filesystem/COutputStream.h" -#include "../JsonDetail.h" -#include "CMap.h" -#include "MapFormat.h" -#include "../ArtifactUtils.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../VCMI_Lib.h" -#include "../RiverHandler.h" -#include "../RoadHandler.h" -#include "../TerrainHandler.h" -#include "../mapObjectConstructors/AObjectTypeHandler.h" -#include "../mapObjectConstructors/CObjectClassesHandler.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../mapObjects/CGHeroInstance.h" -#include "../mapObjects/CGTownInstance.h" -#include "../modding/ModScope.h" -#include "../modding/ModUtility.h" -#include "../spells/CSpellHandler.h" -#include "../CSkillHandler.h" -#include "../constants/StringConstants.h" -#include "../serializer/JsonDeserializer.h" -#include "../serializer/JsonSerializer.h" -#include "../Languages.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class MapObjectResolver: public IInstanceResolver -{ -public: - MapObjectResolver(const CMapFormatJson * owner_); - - si32 decode (const std::string & identifier) const override; - std::string encode(si32 identifier) const override; - -private: - const CMapFormatJson * owner; -}; - -MapObjectResolver::MapObjectResolver(const CMapFormatJson * owner_): - owner(owner_) -{ - -} - -si32 MapObjectResolver::decode(const std::string & identifier) const -{ - //always decode as ObjectInstanceID - - auto it = owner->map->instanceNames.find(identifier); - - if(it != owner->map->instanceNames.end()) - { - return (*it).second->id.getNum(); - } - else - { - logGlobal->error("Object not found: %s", identifier); - return -1; - } -} - -std::string MapObjectResolver::encode(si32 identifier) const -{ - ObjectInstanceID id; - - //use h3m questIdentifiers if they are present - if(owner->map->questIdentifierToId.empty()) - { - id = ObjectInstanceID(identifier); - } - else - { - id = owner->map->questIdentifierToId[identifier]; - } - - si32 oid = id.getNum(); - if(oid < 0 || oid >= owner->map->objects.size()) - { - logGlobal->error("Cannot get object with id %d", oid); - return ""; - } - - return owner->map->objects[oid]->instanceName; -} - -namespace HeaderDetail -{ - static const std::vector difficultyMap = - { - "EASY", - "NORMAL", - "HARD", - "EXPERT", - "IMPOSSIBLE" - }; - - enum class ECanPlay - { - NONE = 0, - PLAYER_OR_AI = 1, - PLAYER_ONLY = 2, - AI_ONLY = 3 - }; - - static const std::vector canPlayMap = - { - "", - "PlayerOrAI", - "PlayerOnly", - "AIOnly" - }; -} - -namespace TriggeredEventsDetail -{ - static const std::array conditionNames = - { - "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", - "control", "destroy", "transport", "daysPassed", - "isHuman", "daysWithoutTown", "standardWin", "constValue", - - "have_0", "haveBuilding_0", "destroy_0" - }; - - static const std::array typeNames = { "victory", "defeat" }; - - static EMetaclass decodeMetaclass(const std::string & source) - { - if(source.empty()) - return EMetaclass::INVALID; - auto rawId = vstd::find_pos(NMetaclass::names, source); - - if(rawId >= 0) - return static_cast(rawId); - else - return EMetaclass::INVALID; - } - - static std::string encodeIdentifier(EMetaclass metaType, si32 type) - { - std::string metaclassName = NMetaclass::names[static_cast(metaType)]; - std::string identifier; - - switch(metaType) - { - case EMetaclass::ARTIFACT: - { - identifier = ArtifactID::encode(type); - } - break; - case EMetaclass::CREATURE: - { - identifier = CreatureID::encode(type); - } - break; - case EMetaclass::OBJECT: - { - //TODO - std::set subtypes = VLC->objtypeh->knownSubObjects(type); - if(!subtypes.empty()) - { - si32 subtype = *subtypes.begin(); - auto handler = VLC->objtypeh->getHandlerFor(type, subtype); - identifier = handler->getTypeName(); - } - } - break; - case EMetaclass::RESOURCE: - { - identifier = GameConstants::RESOURCE_NAMES[type]; - } - break; - default: - { - logGlobal->error("Unsupported metaclass %s for event condition", metaclassName); - return ""; - } - break; - } - - return ModUtility::makeFullIdentifier("", metaclassName, identifier); - } - - static EventCondition JsonToCondition(const JsonNode & node) - { - EventCondition event; - - const auto & conditionName = node.Vector()[0].String(); - - auto pos = vstd::find_pos(conditionNames, conditionName); - - event.condition = static_cast(pos); - - if (node.Vector().size() > 1) - { - const JsonNode & data = node.Vector()[1]; - - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - std::string fullIdentifier = data["type"].String(); - std::string metaTypeName; - std::string scope; - std::string identifier; - ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - event.metaType = decodeMetaclass(metaTypeName); - - auto type = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), fullIdentifier, false); - - if(type) - event.objectType = type.value(); - event.objectInstanceName = data["object"].String(); - if(data["value"].isNumber()) - event.value = static_cast(data["value"].Integer()); - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) - { - auto identifier = VLC->identifiers()->getIdentifier(data["type"]); - if(identifier) - event.objectType = identifier.value(); - else - throw std::runtime_error("Identifier resolution failed in event condition"); - } - - if (data["type"].isNumber()) - event.objectType = static_cast(data["type"].Float()); - - if (!data["value"].isNull()) - event.value = static_cast(data["value"].Float()); - } - break; - } - - if (!data["position"].isNull()) - { - const auto & position = data["position"].Vector(); - event.position.x = static_cast(position.at(0).Float()); - event.position.y = static_cast(position.at(1).Float()); - event.position.z = static_cast(position.at(2).Float()); - } - } - return event; - } - - static JsonNode ConditionToJson(const EventCondition & event) - { - JsonNode json; - - JsonVector & asVector = json.Vector(); - - JsonNode condition; - condition.String() = conditionNames.at(event.condition); - asVector.push_back(condition); - - JsonNode data; - - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - if(event.metaType != EMetaclass::INVALID) - data["type"].String() = encodeIdentifier(event.metaType, event.objectType); - - if(event.value > 0) - data["value"].Integer() = event.value; - - if(!event.objectInstanceName.empty()) - data["object"].String() = event.objectInstanceName; - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if(event.objectType != -1) - data["type"].Integer() = event.objectType; - - if(event.value != -1) - data["value"].Integer() = event.value; - } - break; - } - - if(event.position != int3(-1, -1, -1)) - { - auto & position = data["position"].Vector(); - position.resize(3); - position[0].Float() = event.position.x; - position[1].Float() = event.position.y; - position[2].Float() = event.position.z; - } - - if(!data.isNull()) - asVector.push_back(data); - - return json; - } -}//namespace TriggeredEventsDetail - -namespace TerrainDetail -{ - static const std::array flipCodes = - { - '_', '-', '|', '+' - }; -} - -///CMapFormatJson -const int CMapFormatJson::VERSION_MAJOR = 2; -const int CMapFormatJson::VERSION_MINOR = 0; - -const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; -const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; -const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"}; - -CMapFormatJson::CMapFormatJson(): - fileVersionMajor(0), fileVersionMinor(0), - mapObjectResolver(std::make_unique(this)), - map(nullptr), mapHeader(nullptr) -{ - -} - -TerrainType * CMapFormatJson::getTerrainByCode(const std::string & code) -{ - for(const auto & object : VLC->terrainTypeHandler->objects) - { - if(object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -RiverType * CMapFormatJson::getRiverByCode(const std::string & code) -{ - for(const auto & object : VLC->riverTypeHandler->objects) - { - if (object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -RoadType * CMapFormatJson::getRoadByCode(const std::string & code) -{ - for(const auto & object : VLC->roadTypeHandler->objects) - { - if (object->shortIdentifier == code) - return const_cast(object.get()); - } - return nullptr; -} - -void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const -{ - //TODO: unify allowed factions with others - make them std::vector - - std::vector temp; - temp.resize(VLC->townh->size(), false); - auto standard = VLC->townh->getDefaultAllowed(); - - if(handler.saving) - { - for(auto faction : VLC->townh->objects) - if(faction->town && vstd::contains(value, faction->getId())) - temp[static_cast(faction->getIndex())] = true; - } - - handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, standard, temp); - - if(!handler.saving) - { - value.clear(); - for (std::size_t i=0; i(i)); - } -} - -void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) -{ - handler.serializeStruct("name", mapHeader->name); - handler.serializeStruct("description", mapHeader->description); - handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); - - //todo: support arbitrary percentage - handler.serializeEnum("difficulty", mapHeader->difficulty, HeaderDetail::difficultyMap); - - serializePlayerInfo(handler); - - handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes); - - handler.serializeStruct("victoryMessage", mapHeader->victoryMessage); - handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); - - handler.serializeStruct("defeatMessage", mapHeader->defeatMessage); - handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); -} - -void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) -{ - auto playersData = handler.enterStruct("players"); - - for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) - { - PlayerInfo & info = mapHeader->players[player]; - - if(handler.saving) - { - if(!info.canAnyonePlay()) - continue; - } - - auto playerData = handler.enterStruct(GameConstants::PLAYER_COLOR_NAMES[player]); - - if(!handler.saving) - { - if(handler.getCurrent().isNull()) - { - info.canComputerPlay = false; - info.canHumanPlay = false; - continue; - } - } - - serializeAllowedFactions(handler, info.allowedFactions); - - HeaderDetail::ECanPlay canPlay = HeaderDetail::ECanPlay::NONE; - - if(handler.saving) - { - if(info.canComputerPlay) - { - canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_OR_AI : HeaderDetail::ECanPlay::AI_ONLY; - } - else - { - canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_ONLY : HeaderDetail::ECanPlay::NONE; - } - } - - handler.serializeEnum("canPlay", canPlay, HeaderDetail::canPlayMap); - - if(!handler.saving) - { - switch(canPlay) - { - case HeaderDetail::ECanPlay::PLAYER_OR_AI: - info.canComputerPlay = true; - info.canHumanPlay = true; - break; - case HeaderDetail::ECanPlay::PLAYER_ONLY: - info.canComputerPlay = false; - info.canHumanPlay = true; - break; - case HeaderDetail::ECanPlay::AI_ONLY: - info.canComputerPlay = true; - info.canHumanPlay = false; - break; - default: - info.canComputerPlay = false; - info.canHumanPlay = false; - break; - } - } - - //saving whole structure only if position is valid - if(!handler.saving || info.posOfMainTown.valid()) - { - auto mainTown = handler.enterStruct("mainTown"); - handler.serializeBool("generateHero", info.generateHeroAtMainTown); - handler.serializeInt("x", info.posOfMainTown.x, -1); - handler.serializeInt("y", info.posOfMainTown.y, -1); - handler.serializeInt("l", info.posOfMainTown.z, -1); - } - if(!handler.saving) - { - info.hasMainTown = info.posOfMainTown.valid(); - } - - handler.serializeString("mainHero", info.mainHeroInstance);//must be before "heroes" - - //heroes - if(handler.saving) - { - //ignoring heroesNames and saving from actual map objects - //TODO: optimize - for(auto & obj : map->objects) - { - if((obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO) && obj->tempOwner == PlayerColor(player)) - { - auto * hero = dynamic_cast(obj.get()); - - auto heroes = handler.enterStruct("heroes"); - if(hero) - { - auto heroData = handler.enterStruct(hero->instanceName); - heroData->serializeString("name", hero->nameCustomTextId); - - if(hero->ID == Obj::HERO) - { - std::string temp; - if(hero->type) - { - temp = hero->type->getJsonKey(); - } - else - { - temp = VLC->heroh->objects[hero->subID]->getJsonKey(); - } - handler.serializeString("type", temp); - } - } - } - } - } - else - { - info.heroesNames.clear(); - - auto heroes = handler.enterStruct("heroes"); - - for(const auto & hero : handler.getCurrent().Struct()) - { - const JsonNode & data = hero.second; - const std::string instanceName = hero.first; - - SHeroName hname; - hname.heroId = HeroTypeID::NONE; - std::string rawId = data["type"].String(); - - if(!rawId.empty()) - hname.heroId = HeroTypeID(HeroTypeID::decode(rawId)); - - hname.heroName = data["name"].String(); - - if(instanceName == info.mainHeroInstance) - { - //this is main hero - info.mainCustomHeroNameTextId = hname.heroName; - info.hasRandomHero = (hname.heroId == HeroTypeID::NONE); - info.mainCustomHeroId = hname.heroId; - info.mainCustomHeroPortrait = HeroTypeID::NONE; - //todo:mainHeroPortrait - } - - info.heroesNames.push_back(hname); - } - } - - handler.serializeBool("randomFaction", info.isFactionRandom); - } -} - -void CMapFormatJson::readTeams(JsonDeserializer & handler) -{ - auto teams = handler.enterArray("teams"); - const JsonNode & src = teams->getCurrent(); - - if(src.getType() != JsonNode::JsonType::DATA_VECTOR) - { - // No alliances - if(src.getType() != JsonNode::JsonType::DATA_NULL) - logGlobal->error("Invalid teams field type"); - - mapHeader->howManyTeams = 0; - for(auto & player : mapHeader->players) - if(player.canAnyonePlay()) - player.team = TeamID(mapHeader->howManyTeams++); - } - else - { - const JsonVector & srcVector = src.Vector(); - mapHeader->howManyTeams = static_cast(srcVector.size()); - - for(int team = 0; team < mapHeader->howManyTeams; team++) - for(const JsonNode & playerData : srcVector[team].Vector()) - { - PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); - if(player.isValidPlayer()) - if(mapHeader->players[player.getNum()].canAnyonePlay()) - mapHeader->players[player.getNum()].team = TeamID(team); - } - - for(PlayerInfo & player : mapHeader->players) - if(player.canAnyonePlay() && player.team == TeamID::NO_TEAM) - player.team = TeamID(mapHeader->howManyTeams++); - } -} - -void CMapFormatJson::writeTeams(JsonSerializer & handler) -{ - std::vector> teamsData; - - teamsData.resize(mapHeader->howManyTeams); - - //get raw data - for(int idx = 0; idx < mapHeader->players.size(); idx++) - { - const PlayerInfo & player = mapHeader->players.at(idx); - int team = player.team.getNum(); - if(vstd::iswithin(team, 0, mapHeader->howManyTeams-1) && player.canAnyonePlay()) - teamsData.at(team).insert(PlayerColor(idx)); - } - - //remove single-member teams - vstd::erase_if(teamsData, [](std::set & elem) -> bool - { - return elem.size() <= 1; - }); - - if(!teamsData.empty()) - { - JsonNode dest; - - //construct output - dest.setType(JsonNode::JsonType::DATA_VECTOR); - - for(const std::set & teamData : teamsData) - { - JsonNode team(JsonNode::JsonType::DATA_VECTOR); - for(const PlayerColor & player : teamData) - { - JsonNode member(JsonNode::JsonType::DATA_STRING); - member.String() = GameConstants::PLAYER_COLOR_NAMES[player.getNum()]; - team.Vector().push_back(std::move(member)); - } - dest.Vector().push_back(std::move(team)); - } - handler.serializeRaw("teams", dest, std::nullopt); - } -} - -void CMapFormatJson::readTriggeredEvents(JsonDeserializer & handler) -{ - const JsonNode & input = handler.getCurrent(); - - mapHeader->triggeredEvents.clear(); - - for(const auto & entry : input["triggeredEvents"].Struct()) - { - TriggeredEvent event; - event.identifier = entry.first; - readTriggeredEvent(event, entry.second); - mapHeader->triggeredEvents.push_back(event); - } -} - -void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const -{ - using namespace TriggeredEventsDetail; - - event.onFulfill.jsonDeserialize(source["message"]); - event.description.jsonDeserialize(source["description"]); - event.effect.type = vstd::find_pos(typeNames, source["effect"]["type"].String()); - event.effect.toOtherMessage.jsonDeserialize(source["effect"]["messageToSend"]); - event.trigger = EventExpression(source["condition"], JsonToCondition); // logical expression -} - -void CMapFormatJson::writeTriggeredEvents(JsonSerializer & handler) -{ - JsonNode triggeredEvents(JsonNode::JsonType::DATA_STRUCT); - - for(const auto & event : mapHeader->triggeredEvents) - writeTriggeredEvent(event, triggeredEvents[event.identifier]); - - handler.serializeRaw("triggeredEvents", triggeredEvents, std::nullopt); -} - -void CMapFormatJson::writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const -{ - using namespace TriggeredEventsDetail; - - if(!event.onFulfill.empty()) - event.onFulfill.jsonSerialize(dest["message"]); - - if(!event.description.empty()) - event.description.jsonSerialize(dest["description"]); - - dest["effect"]["type"].String() = typeNames.at(static_cast(event.effect.type)); - - if(!event.effect.toOtherMessage.empty()) - event.description.jsonSerialize(dest["effect"]["messageToSend"]); - - dest["condition"] = event.trigger.toJson(ConditionToJson); -} - -void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) -{ - auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format - - const JsonNode & data = handler.getCurrent(); - - for(const auto & entry : data.Struct()) - { - HeroTypeID type(HeroTypeID::decode(entry.first)); - - std::set mask; - - for(const JsonNode & playerData : entry.second["availableFor"].Vector()) - { - PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); - if(player.isValidPlayer()) - mask.insert(player); - } - - if(!mask.empty() && mask.size() != PlayerColor::PLAYER_LIMIT_I && type.getNum() >= 0) - { - DisposedHero hero; - - hero.heroId = type; - hero.players = mask; - //name and portrait are not used - - map->disposedHeroes.push_back(hero); - } - } -} - -void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) -{ - if(map->disposedHeroes.empty()) - return; - - auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format - - for(DisposedHero & hero : map->disposedHeroes) - { - std::string type = HeroTypeID::encode(hero.heroId); - - auto definition = definitions->enterStruct(type); - - JsonNode players(JsonNode::JsonType::DATA_VECTOR); - definition->serializeIdArray("availableFor", hero.players); - } -} - -void CMapFormatJson::serializeRumors(JsonSerializeFormat & handler) -{ - auto rumors = handler.enterArray("rumors"); - rumors.serializeStruct(map->rumors); -} - -void CMapFormatJson::serializeTimedEvents(JsonSerializeFormat & handler) -{ - auto events = handler.enterArray("events"); - std::vector temp(map->events.begin(), map->events.end()); - events.serializeStruct(temp); - map->events.assign(temp.begin(), temp.end()); -} - -void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) -{ - //todo:serializePredefinedHeroes - - if(handler.saving) - { - if(!map->predefinedHeroes.empty()) - { - auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - - for(auto & hero : map->predefinedHeroes) - { - auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); - - hero->serializeJsonDefinition(handler); - } - } - } - else - { - auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); - - const JsonNode & data = handler.getCurrent(); - - for(const auto & p : data.Struct()) - { - auto predefinedHero = handler.enterStruct(p.first); - - auto * hero = new CGHeroInstance(); - hero->ID = Obj::HERO; - hero->setHeroTypeName(p.first); - hero->serializeJsonDefinition(handler); - - map->predefinedHeroes.emplace_back(hero); - } - } -} - -void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) -{ - serializeRumors(handler); - - serializeTimedEvents(handler); - - serializePredefinedHeroes(handler); - - handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); - - handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); - - handler.serializeLIC("allowedSpells", &SpellID::decode, &SpellID::encode, VLC->spellh->getDefaultAllowed(), map->allowedSpells); - - //todo:events -} - -void CMapFormatJson::readOptions(JsonDeserializer & handler) -{ - readDisposedHeroes(handler); - serializeOptions(handler); -} - -void CMapFormatJson::writeOptions(JsonSerializer & handler) -{ - writeDisposedHeroes(handler); - serializeOptions(handler); -} - -///CMapPatcher -CMapPatcher::CMapPatcher(const JsonNode & stream): input(stream) -{ - //todo: update map patches and change this - fileVersionMajor = 0; - fileVersionMinor = 0; -} - -void CMapPatcher::patchMapHeader(std::unique_ptr & header) -{ - map = nullptr; - mapHeader = header.get(); - if (!input.isNull()) - readPatchData(); -} - -void CMapPatcher::readPatchData() -{ - JsonDeserializer handler(mapObjectResolver.get(), input); - readTriggeredEvents(handler); - - handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); - handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); - handler.serializeStruct("victoryString", mapHeader->victoryMessage); - handler.serializeStruct("defeatString", mapHeader->defeatMessage); -} - -///CMapLoaderJson -CMapLoaderJson::CMapLoaderJson(CInputStream * stream) - : buffer(stream) - , ioApi(new CProxyROIOApi(buffer)) - , loader("", "_", ioApi) -{ -} - -std::unique_ptr CMapLoaderJson::loadMap() -{ - LOG_TRACE(logGlobal); - std::unique_ptr result = std::make_unique(); - map = result.get(); - mapHeader = map; - readMap(); - return result; -} - -std::unique_ptr CMapLoaderJson::loadMapHeader() -{ - LOG_TRACE(logGlobal); - map = nullptr; - std::unique_ptr result = std::make_unique(); - mapHeader = result.get(); - readHeader(false); - return result; -} - -bool CMapLoaderJson::isExistArchive(const std::string & archiveFilename) -{ - return loader.existsResource(JsonPath::builtin(archiveFilename)); -} - -JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) -{ - JsonPath resource = JsonPath::builtin(archiveFilename); - - if(!loader.existsResource(resource)) - throw std::runtime_error(archiveFilename+" not found"); - - auto data = loader.load(resource)->readAll(); - - JsonNode res(reinterpret_cast(data.first.get()), data.second); - - return res; -} - -void CMapLoaderJson::readMap() -{ - LOG_TRACE(logGlobal); - readHeader(true); - map->initTerrain(); - readTerrain(); - readObjects(); - - map->calculateGuardingGreaturePositions(); -} - -void CMapLoaderJson::readHeader(const bool complete) -{ - //do not use map field here, use only mapHeader - JsonNode header = getFromArchive(HEADER_FILE_NAME); - - fileVersionMajor = static_cast(header["versionMajor"].Integer()); - - if(fileVersionMajor > VERSION_MAJOR) - { - logGlobal->error("Unsupported map format version: %d", fileVersionMajor); - throw std::runtime_error("Unsupported map format version"); - } - - fileVersionMinor = static_cast(header["versionMinor"].Integer()); - - if(fileVersionMinor > VERSION_MINOR) - { - logGlobal->warn("Too new map format revision: %d. This map should work but some of map features may be ignored.", fileVersionMinor); - } - - JsonDeserializer handler(mapObjectResolver.get(), header); - - mapHeader->version = EMapFormat::VCMI;//todo: new version field - - //loading mods - if(!header["mods"].isNull()) - { - for(auto & mod : header["mods"].Vector()) - { - CModInfo::VerificationInfo info; - info.version = CModVersion::fromString(mod["version"].String()); - info.checksum = mod["checksum"].Integer(); - info.name = mod["name"].String(); - info.parent = mod["parent"].String(); - info.impactsGameplay = true; - - if(!mod["modId"].isNull()) - mapHeader->mods[mod["modId"].String()] = info; - else - mapHeader->mods[mod["name"].String()] = info; - } - } - - //todo: multilevel map load support - { - auto levels = handler.enterStruct("mapLevels"); - - { - auto surface = handler.enterStruct("surface"); - handler.serializeInt("height", mapHeader->height); - handler.serializeInt("width", mapHeader->width); - } - { - auto underground = handler.enterStruct("underground"); - mapHeader->twoLevel = !underground->getCurrent().isNull(); - } - } - - serializeHeader(handler); - - readTriggeredEvents(handler); - - readTeams(handler); - //TODO: check mods - - if(complete) - readOptions(handler); - - readTranslations(); -} - -void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile) -{ - try - { - using namespace TerrainDetail; - {//terrain type - const std::string typeCode = src.substr(0, 2); - tile.terType = getTerrainByCode(typeCode); - } - int startPos = 2; //0+typeCode fixed length - {//terrain view - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid terrain view in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.terView = atoi(rawCode.c_str()); - startPos += len; - } - {//terrain flip - int terrainFlip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (terrainFlip < 0) - throw std::runtime_error("Invalid terrain flip in " + src); - else - tile.extTileFlags = terrainFlip; - } - if (startPos >= src.size()) - return; - bool hasRoad = true; - {//road type - const std::string typeCode = src.substr(startPos, 2); - startPos += 2; - tile.roadType = getRoadByCode(typeCode); - if(!tile.roadType) //it's not a road, it's a river - { - tile.roadType = VLC->roadTypeHandler->getById(Road::NO_ROAD); - tile.riverType = getRiverByCode(typeCode); - hasRoad = false; - if(!tile.riverType) - { - throw std::runtime_error("Invalid river type in " + src); - } - } - } - if (hasRoad) - {//road dir - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid road dir in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.roadDir = atoi(rawCode.c_str()); - startPos += len; - } - if (hasRoad) - {//road flip - int flip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (flip < 0) - throw std::runtime_error("Invalid road flip in " + src); - else - tile.extTileFlags |= (flip << 4); - } - if (startPos >= src.size()) - return; - if (hasRoad) - {//river type - const std::string typeCode = src.substr(startPos, 2); - startPos += 2; - tile.riverType = getRiverByCode(typeCode); - } - {//river dir - int pos = startPos; - while (isdigit(src.at(pos))) - pos++; - int len = pos - startPos; - if (len <= 0) - throw std::runtime_error("Invalid river dir in " + src); - const std::string rawCode = src.substr(startPos, len); - tile.riverDir = atoi(rawCode.c_str()); - startPos += len; - } - {//river flip - int flip = vstd::find_pos(flipCodes, src.at(startPos++)); - if (flip < 0) - throw std::runtime_error("Invalid road flip in " + src); - else - tile.extTileFlags |= (flip << 2); - } - } - catch (const std::exception &) - { - logGlobal->error("Failed to read terrain tile: %s"); - } -} - -void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index) -{ - int3 pos(0, 0, index); - - const JsonVector & rows = src.Vector(); - - if(rows.size() != map->height) - throw std::runtime_error("Invalid terrain data"); - - for(pos.y = 0; pos.y < map->height; pos.y++) - { - const JsonVector & tiles = rows[pos.y].Vector(); - - if(tiles.size() != map->width) - throw std::runtime_error("Invalid terrain data"); - - for(pos.x = 0; pos.x < map->width; pos.x++) - readTerrainTile(tiles[pos.x].String(), map->getTile(pos)); - } -} - -void CMapLoaderJson::readTerrain() -{ - { - const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]); - readTerrainLevel(surface, 0); - } - if(map->twoLevel) - { - const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]); - readTerrainLevel(underground, 1); - } - - map->calculateWaterContent(); -} - -CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json): - owner(_owner), instance(nullptr), id(-1), jsonKey(json.first), configuration(json.second) -{ - -} - -void CMapLoaderJson::MapObjectLoader::construct() -{ - //TODO:consider move to ObjectTypeHandler - //find type handler - std::string typeName = configuration["type"].String(); - std::string subtypeName = configuration["subtype"].String(); - if(typeName.empty()) - { - logGlobal->error("Object type missing"); - logGlobal->debug(configuration.toJson()); - return; - } - - int3 pos; - pos.x = static_cast(configuration["x"].Float()); - pos.y = static_cast(configuration["y"].Float()); - pos.z = static_cast(configuration["l"].Float()); - - //special case for grail - if(typeName == "grail") - { - owner->map->grailPos = pos; - - owner->map->grailRadius = static_cast(configuration["options"]["grailRadius"].Float()); - return; - } - else if(subtypeName.empty()) - { - logGlobal->error("Object subtype missing"); - logGlobal->debug(configuration.toJson()); - return; - } - - auto handler = VLC->objtypeh->getHandlerFor( ModScope::scopeMap(), typeName, subtypeName); - - auto * appearance = new ObjectTemplate; - - appearance->id = Obj(handler->getIndex()); - appearance->subid = handler->getSubIndex(); - appearance->readJson(configuration["template"], false); - - // Will be destroyed soon and replaced with shared template - instance = handler->create(std::shared_ptr(appearance)); - - instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); - instance->instanceName = jsonKey; - instance->pos = pos; - owner->map->addNewObject(instance); -} - -void CMapLoaderJson::MapObjectLoader::configure() -{ - if(nullptr == instance) - return; - - JsonDeserializer handler(owner->mapObjectResolver.get(), configuration); - - instance->serializeJson(handler); - - //artifact instance serialization requires access to Map object, handle it here for now - //todo: find better solution for artifact instance serialization - - if(auto * art = dynamic_cast(instance)) - { - ArtifactID artID = ArtifactID::NONE; - SpellID spellID = SpellID::NONE; - - if(art->ID == Obj::SPELL_SCROLL) - { - auto spellIdentifier = configuration["options"]["spell"].String(); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", spellIdentifier); - if(rawId) - spellID = rawId.value(); - else - spellID = 0; - artID = ArtifactID::SPELL_SCROLL; - } - else if(art->ID == Obj::ARTIFACT) - { - //specific artifact - artID = ArtifactID(art->subID); - } - - art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); - } - - if(auto * hero = dynamic_cast(instance)) - { - auto o = handler.enterStruct("options"); - hero->serializeJsonArtifacts(handler, "artifacts", owner->map); - } -} - -void CMapLoaderJson::readObjects() -{ - LOG_TRACE(logGlobal); - - std::vector> loaders;//todo: optimize MapObjectLoader memory layout - - JsonNode data = getFromArchive(OBJECTS_FILE_NAME); - - //get raw data - for(auto & p : data.Struct()) - loaders.push_back(std::make_unique(this, p)); - - for(auto & ptr : loaders) - ptr->construct(); - - //configure objects after all objects are constructed so we may resolve internal IDs even to actual pointers OTF - for(auto & ptr : loaders) - ptr->configure(); - - std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) - { - return a->subID < b->subID; - }); -} - -void CMapLoaderJson::readTranslations() -{ - std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; - for(auto & language : Languages::getLanguageList()) - { - if(isExistArchive(language.identifier + ".json")) - mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json"); - } - mapHeader->registerMapStrings(); -} - - -///CMapSaverJson -CMapSaverJson::CMapSaverJson(CInputOutputStream * stream) - : buffer(stream) - , ioApi(new CProxyIOApi(buffer)) - , saver(ioApi, "_") -{ - fileVersionMajor = VERSION_MAJOR; - fileVersionMinor = VERSION_MINOR; -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CMapSaverJson::~CMapSaverJson() = default; - -void CMapSaverJson::addToArchive(const JsonNode & data, const std::string & filename) -{ - std::ostringstream out; - JsonWriter writer(out); - writer.writeNode(data); - out.flush(); - - { - auto s = out.str(); - std::unique_ptr stream = saver.addFile(filename); - - if(stream->write(reinterpret_cast(s.c_str()), s.size()) != s.size()) - throw std::runtime_error("CMapSaverJson::saveHeader() zip compression failed."); - } -} - -void CMapSaverJson::saveMap(const std::unique_ptr& map) -{ - this->map = map.get(); - this->mapHeader = this->map; - writeHeader(); - writeTerrain(); - writeObjects(); -} - -void CMapSaverJson::writeHeader() -{ - logGlobal->trace("Saving header"); - - JsonNode header; - JsonSerializer handler(mapObjectResolver.get(), header); - - header["versionMajor"].Float() = VERSION_MAJOR; - header["versionMinor"].Float() = VERSION_MINOR; - - //write mods - JsonNode & mods = header["mods"]; - for(const auto & mod : mapHeader->mods) - { - JsonNode modWriter; - modWriter["modId"].String() = mod.first; - modWriter["name"].String() = mod.second.name; - modWriter["parent"].String() = mod.second.parent; - modWriter["version"].String() = mod.second.version.toString(); - modWriter["checksum"].Integer() = mod.second.checksum; - mods.Vector().push_back(modWriter); - } - - //todo: multilevel map save support - JsonNode & levels = header["mapLevels"]; - levels["surface"]["height"].Float() = mapHeader->height; - levels["surface"]["width"].Float() = mapHeader->width; - levels["surface"]["index"].Float() = 0; - - if(mapHeader->twoLevel) - { - levels["underground"]["height"].Float() = mapHeader->height; - levels["underground"]["width"].Float() = mapHeader->width; - levels["underground"]["index"].Float() = 1; - } - - serializeHeader(handler); - - writeTriggeredEvents(handler); - - writeTeams(handler); - - writeOptions(handler); - - writeTranslations(); - - addToArchive(header, HEADER_FILE_NAME); -} - -std::string CMapSaverJson::writeTerrainTile(const TerrainTile & tile) -{ - using namespace TerrainDetail; - - std::ostringstream out; - out.setf(std::ios::dec, std::ios::basefield); - out.unsetf(std::ios::showbase); - - out << tile.terType->shortIdentifier << static_cast(tile.terView) << flipCodes[tile.extTileFlags % 4]; - - if(tile.roadType->getId() != Road::NO_ROAD) - out << tile.roadType->shortIdentifier << static_cast(tile.roadDir) << flipCodes[(tile.extTileFlags >> 4) % 4]; - - if(tile.riverType->getId() != River::NO_RIVER) - out << tile.riverType->shortIdentifier << static_cast(tile.riverDir) << flipCodes[(tile.extTileFlags >> 2) % 4]; - - return out.str(); -} - -JsonNode CMapSaverJson::writeTerrainLevel(const int index) -{ - JsonNode data; - int3 pos(0,0,index); - - data.Vector().resize(map->height); - - for(pos.y = 0; pos.y < map->height; pos.y++) - { - JsonNode & row = data.Vector()[pos.y]; - - row.Vector().resize(map->width); - - for(pos.x = 0; pos.x < map->width; pos.x++) - row.Vector()[pos.x].String() = writeTerrainTile(map->getTile(pos)); - } - - return data; -} - -void CMapSaverJson::writeTerrain() -{ - logGlobal->trace("Saving terrain"); - //todo: multilevel map save support - - JsonNode surface = writeTerrainLevel(0); - addToArchive(surface, TERRAIN_FILE_NAMES[0]); - - if(map->twoLevel) - { - JsonNode underground = writeTerrainLevel(1); - addToArchive(underground, TERRAIN_FILE_NAMES[1]); - } -} - -void CMapSaverJson::writeObjects() -{ - logGlobal->trace("Saving objects"); - JsonNode data(JsonNode::JsonType::DATA_STRUCT); - - JsonSerializer handler(mapObjectResolver.get(), data); - - for(CGObjectInstance * obj : map->objects) - { - //logGlobal->trace("\t%s", obj->instanceName); - auto temp = handler.enterStruct(obj->instanceName); - - obj->serializeJson(handler); - } - - if(map->grailPos.valid()) - { - JsonNode grail(JsonNode::JsonType::DATA_STRUCT); - grail["type"].String() = "grail"; - - grail["x"].Float() = map->grailPos.x; - grail["y"].Float() = map->grailPos.y; - grail["l"].Float() = map->grailPos.z; - - grail["options"]["radius"].Float() = map->grailRadius; - - std::string grailId = boost::str(boost::format("grail_%d") % map->objects.size()); - - data[grailId] = grail; - } - - //cleanup empty options - for(auto & p : data.Struct()) - { - JsonNode & obj = p.second; - if(obj["options"].Struct().empty()) - obj.Struct().erase("options"); - } - - addToArchive(data, OBJECTS_FILE_NAME); -} - -void CMapSaverJson::writeTranslations() -{ - for(auto & s : mapHeader->translations.Struct()) - { - auto & language = s.first; - if(Languages::getLanguageOptions(language).identifier.empty()) - { - logGlobal->error("Serializing of unsupported language %s is not permitted", language); - continue;; - } - logGlobal->trace("Saving translations, language: %s", language); - addToArchive(s.second, language + ".json"); - } -} - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatJson.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 "MapFormatJson.h" + +#include "../filesystem/CInputStream.h" +#include "../filesystem/COutputStream.h" +#include "../JsonDetail.h" +#include "CMap.h" +#include "MapFormat.h" +#include "../ArtifactUtils.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../VCMI_Lib.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" +#include "../TerrainHandler.h" +#include "../mapObjectConstructors/AObjectTypeHandler.h" +#include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../mapObjects/CGTownInstance.h" +#include "../modding/ModScope.h" +#include "../modding/ModUtility.h" +#include "../spells/CSpellHandler.h" +#include "../CSkillHandler.h" +#include "../constants/StringConstants.h" +#include "../serializer/JsonDeserializer.h" +#include "../serializer/JsonSerializer.h" +#include "../Languages.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class MapObjectResolver: public IInstanceResolver +{ +public: + MapObjectResolver(const CMapFormatJson * owner_); + + si32 decode (const std::string & identifier) const override; + std::string encode(si32 identifier) const override; + +private: + const CMapFormatJson * owner; +}; + +MapObjectResolver::MapObjectResolver(const CMapFormatJson * owner_): + owner(owner_) +{ + +} + +si32 MapObjectResolver::decode(const std::string & identifier) const +{ + //always decode as ObjectInstanceID + + auto it = owner->map->instanceNames.find(identifier); + + if(it != owner->map->instanceNames.end()) + { + return (*it).second->id.getNum(); + } + else + { + logGlobal->error("Object not found: %s", identifier); + return -1; + } +} + +std::string MapObjectResolver::encode(si32 identifier) const +{ + ObjectInstanceID id; + + //use h3m questIdentifiers if they are present + if(owner->map->questIdentifierToId.empty()) + { + id = ObjectInstanceID(identifier); + } + else + { + id = owner->map->questIdentifierToId[identifier]; + } + + si32 oid = id.getNum(); + if(oid < 0 || oid >= owner->map->objects.size()) + { + logGlobal->error("Cannot get object with id %d", oid); + return ""; + } + + return owner->map->objects[oid]->instanceName; +} + +namespace HeaderDetail +{ + static const std::vector difficultyMap = + { + "EASY", + "NORMAL", + "HARD", + "EXPERT", + "IMPOSSIBLE" + }; + + enum class ECanPlay + { + NONE = 0, + PLAYER_OR_AI = 1, + PLAYER_ONLY = 2, + AI_ONLY = 3 + }; + + static const std::vector canPlayMap = + { + "", + "PlayerOrAI", + "PlayerOnly", + "AIOnly" + }; +} + +namespace TriggeredEventsDetail +{ + static const std::array conditionNames = + { + "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", + "control", "destroy", "transport", "daysPassed", + "isHuman", "daysWithoutTown", "standardWin", "constValue", + + "have_0", "haveBuilding_0", "destroy_0" + }; + + static const std::array typeNames = { "victory", "defeat" }; + + static EMetaclass decodeMetaclass(const std::string & source) + { + if(source.empty()) + return EMetaclass::INVALID; + auto rawId = vstd::find_pos(NMetaclass::names, source); + + if(rawId >= 0) + return static_cast(rawId); + else + return EMetaclass::INVALID; + } + + static std::string encodeIdentifier(EMetaclass metaType, si32 type) + { + std::string metaclassName = NMetaclass::names[static_cast(metaType)]; + std::string identifier; + + switch(metaType) + { + case EMetaclass::ARTIFACT: + { + identifier = ArtifactID::encode(type); + } + break; + case EMetaclass::CREATURE: + { + identifier = CreatureID::encode(type); + } + break; + case EMetaclass::OBJECT: + { + //TODO + std::set subtypes = VLC->objtypeh->knownSubObjects(type); + if(!subtypes.empty()) + { + si32 subtype = *subtypes.begin(); + auto handler = VLC->objtypeh->getHandlerFor(type, subtype); + identifier = handler->getTypeName(); + } + } + break; + case EMetaclass::RESOURCE: + { + identifier = GameConstants::RESOURCE_NAMES[type]; + } + break; + default: + { + logGlobal->error("Unsupported metaclass %s for event condition", metaclassName); + return ""; + } + break; + } + + return ModUtility::makeFullIdentifier("", metaclassName, identifier); + } + + static EventCondition JsonToCondition(const JsonNode & node) + { + EventCondition event; + + const auto & conditionName = node.Vector()[0].String(); + + auto pos = vstd::find_pos(conditionNames, conditionName); + + event.condition = static_cast(pos); + + if (node.Vector().size() > 1) + { + const JsonNode & data = node.Vector()[1]; + + switch (event.condition) + { + case EventCondition::HAVE_0: + case EventCondition::DESTROY_0: + { + //todo: support subtypes + + std::string fullIdentifier = data["type"].String(); + std::string metaTypeName; + std::string scope; + std::string identifier; + ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); + + event.metaType = decodeMetaclass(metaTypeName); + + auto type = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), fullIdentifier, false); + + if(type) + event.objectType = type.value(); + event.objectInstanceName = data["object"].String(); + if(data["value"].isNumber()) + event.value = static_cast(data["value"].Integer()); + } + break; + case EventCondition::HAVE_BUILDING_0: + { + //todo: support of new condition format HAVE_BUILDING_0 + } + break; + default: + { + //old format + if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) + { + auto identifier = VLC->identifiers()->getIdentifier(data["type"]); + if(identifier) + event.objectType = identifier.value(); + else + throw std::runtime_error("Identifier resolution failed in event condition"); + } + + if (data["type"].isNumber()) + event.objectType = static_cast(data["type"].Float()); + + if (!data["value"].isNull()) + event.value = static_cast(data["value"].Float()); + } + break; + } + + if (!data["position"].isNull()) + { + const auto & position = data["position"].Vector(); + event.position.x = static_cast(position.at(0).Float()); + event.position.y = static_cast(position.at(1).Float()); + event.position.z = static_cast(position.at(2).Float()); + } + } + return event; + } + + static JsonNode ConditionToJson(const EventCondition & event) + { + JsonNode json; + + JsonVector & asVector = json.Vector(); + + JsonNode condition; + condition.String() = conditionNames.at(event.condition); + asVector.push_back(condition); + + JsonNode data; + + switch (event.condition) + { + case EventCondition::HAVE_0: + case EventCondition::DESTROY_0: + { + //todo: support subtypes + + if(event.metaType != EMetaclass::INVALID) + data["type"].String() = encodeIdentifier(event.metaType, event.objectType); + + if(event.value > 0) + data["value"].Integer() = event.value; + + if(!event.objectInstanceName.empty()) + data["object"].String() = event.objectInstanceName; + } + break; + case EventCondition::HAVE_BUILDING_0: + { + //todo: support of new condition format HAVE_BUILDING_0 + } + break; + default: + { + //old format + if(event.objectType != -1) + data["type"].Integer() = event.objectType; + + if(event.value != -1) + data["value"].Integer() = event.value; + } + break; + } + + if(event.position != int3(-1, -1, -1)) + { + auto & position = data["position"].Vector(); + position.resize(3); + position[0].Float() = event.position.x; + position[1].Float() = event.position.y; + position[2].Float() = event.position.z; + } + + if(!data.isNull()) + asVector.push_back(data); + + return json; + } +}//namespace TriggeredEventsDetail + +namespace TerrainDetail +{ + static const std::array flipCodes = + { + '_', '-', '|', '+' + }; +} + +///CMapFormatJson +const int CMapFormatJson::VERSION_MAJOR = 2; +const int CMapFormatJson::VERSION_MINOR = 0; + +const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; +const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; +const std::string CMapFormatJson::TERRAIN_FILE_NAMES[2] = {"surface_terrain.json", "underground_terrain.json"}; + +CMapFormatJson::CMapFormatJson(): + fileVersionMajor(0), fileVersionMinor(0), + mapObjectResolver(std::make_unique(this)), + map(nullptr), mapHeader(nullptr) +{ + +} + +TerrainType * CMapFormatJson::getTerrainByCode(const std::string & code) +{ + for(const auto & object : VLC->terrainTypeHandler->objects) + { + if(object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +RiverType * CMapFormatJson::getRiverByCode(const std::string & code) +{ + for(const auto & object : VLC->riverTypeHandler->objects) + { + if (object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +RoadType * CMapFormatJson::getRoadByCode(const std::string & code) +{ + for(const auto & object : VLC->roadTypeHandler->objects) + { + if (object->shortIdentifier == code) + return const_cast(object.get()); + } + return nullptr; +} + +void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const +{ + //TODO: unify allowed factions with others - make them std::vector + + std::vector temp; + temp.resize(VLC->townh->size(), false); + auto standard = VLC->townh->getDefaultAllowed(); + + if(handler.saving) + { + for(auto faction : VLC->townh->objects) + if(faction->town && vstd::contains(value, faction->getId())) + temp[static_cast(faction->getIndex())] = true; + } + + handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, standard, temp); + + if(!handler.saving) + { + value.clear(); + for (std::size_t i=0; i(i)); + } +} + +void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) +{ + handler.serializeStruct("name", mapHeader->name); + handler.serializeStruct("description", mapHeader->description); + handler.serializeInt("heroLevelLimit", mapHeader->levelLimit, 0); + + //todo: support arbitrary percentage + handler.serializeEnum("difficulty", mapHeader->difficulty, HeaderDetail::difficultyMap); + + serializePlayerInfo(handler); + + handler.serializeLIC("allowedHeroes", &HeroTypeID::decode, &HeroTypeID::encode, VLC->heroh->getDefaultAllowed(), mapHeader->allowedHeroes); + + handler.serializeStruct("victoryMessage", mapHeader->victoryMessage); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + + handler.serializeStruct("defeatMessage", mapHeader->defeatMessage); + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); +} + +void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) +{ + auto playersData = handler.enterStruct("players"); + + for(int player = 0; player < PlayerColor::PLAYER_LIMIT_I; player++) + { + PlayerInfo & info = mapHeader->players[player]; + + if(handler.saving) + { + if(!info.canAnyonePlay()) + continue; + } + + auto playerData = handler.enterStruct(GameConstants::PLAYER_COLOR_NAMES[player]); + + if(!handler.saving) + { + if(handler.getCurrent().isNull()) + { + info.canComputerPlay = false; + info.canHumanPlay = false; + continue; + } + } + + serializeAllowedFactions(handler, info.allowedFactions); + + HeaderDetail::ECanPlay canPlay = HeaderDetail::ECanPlay::NONE; + + if(handler.saving) + { + if(info.canComputerPlay) + { + canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_OR_AI : HeaderDetail::ECanPlay::AI_ONLY; + } + else + { + canPlay = info.canHumanPlay ? HeaderDetail::ECanPlay::PLAYER_ONLY : HeaderDetail::ECanPlay::NONE; + } + } + + handler.serializeEnum("canPlay", canPlay, HeaderDetail::canPlayMap); + + if(!handler.saving) + { + switch(canPlay) + { + case HeaderDetail::ECanPlay::PLAYER_OR_AI: + info.canComputerPlay = true; + info.canHumanPlay = true; + break; + case HeaderDetail::ECanPlay::PLAYER_ONLY: + info.canComputerPlay = false; + info.canHumanPlay = true; + break; + case HeaderDetail::ECanPlay::AI_ONLY: + info.canComputerPlay = true; + info.canHumanPlay = false; + break; + default: + info.canComputerPlay = false; + info.canHumanPlay = false; + break; + } + } + + //saving whole structure only if position is valid + if(!handler.saving || info.posOfMainTown.valid()) + { + auto mainTown = handler.enterStruct("mainTown"); + handler.serializeBool("generateHero", info.generateHeroAtMainTown); + handler.serializeInt("x", info.posOfMainTown.x, -1); + handler.serializeInt("y", info.posOfMainTown.y, -1); + handler.serializeInt("l", info.posOfMainTown.z, -1); + } + if(!handler.saving) + { + info.hasMainTown = info.posOfMainTown.valid(); + } + + handler.serializeString("mainHero", info.mainHeroInstance);//must be before "heroes" + + //heroes + if(handler.saving) + { + //ignoring heroesNames and saving from actual map objects + //TODO: optimize + for(auto & obj : map->objects) + { + if((obj->ID == Obj::HERO || obj->ID == Obj::RANDOM_HERO) && obj->tempOwner == PlayerColor(player)) + { + auto * hero = dynamic_cast(obj.get()); + + auto heroes = handler.enterStruct("heroes"); + if(hero) + { + auto heroData = handler.enterStruct(hero->instanceName); + heroData->serializeString("name", hero->nameCustomTextId); + + if(hero->ID == Obj::HERO) + { + std::string temp; + if(hero->type) + { + temp = hero->type->getJsonKey(); + } + else + { + temp = VLC->heroh->objects[hero->subID]->getJsonKey(); + } + handler.serializeString("type", temp); + } + } + } + } + } + else + { + info.heroesNames.clear(); + + auto heroes = handler.enterStruct("heroes"); + + for(const auto & hero : handler.getCurrent().Struct()) + { + const JsonNode & data = hero.second; + const std::string instanceName = hero.first; + + SHeroName hname; + hname.heroId = HeroTypeID::NONE; + std::string rawId = data["type"].String(); + + if(!rawId.empty()) + hname.heroId = HeroTypeID(HeroTypeID::decode(rawId)); + + hname.heroName = data["name"].String(); + + if(instanceName == info.mainHeroInstance) + { + //this is main hero + info.mainCustomHeroNameTextId = hname.heroName; + info.hasRandomHero = (hname.heroId == HeroTypeID::NONE); + info.mainCustomHeroId = hname.heroId; + info.mainCustomHeroPortrait = HeroTypeID::NONE; + //todo:mainHeroPortrait + } + + info.heroesNames.push_back(hname); + } + } + + handler.serializeBool("randomFaction", info.isFactionRandom); + } +} + +void CMapFormatJson::readTeams(JsonDeserializer & handler) +{ + auto teams = handler.enterArray("teams"); + const JsonNode & src = teams->getCurrent(); + + if(src.getType() != JsonNode::JsonType::DATA_VECTOR) + { + // No alliances + if(src.getType() != JsonNode::JsonType::DATA_NULL) + logGlobal->error("Invalid teams field type"); + + mapHeader->howManyTeams = 0; + for(auto & player : mapHeader->players) + if(player.canAnyonePlay()) + player.team = TeamID(mapHeader->howManyTeams++); + } + else + { + const JsonVector & srcVector = src.Vector(); + mapHeader->howManyTeams = static_cast(srcVector.size()); + + for(int team = 0; team < mapHeader->howManyTeams; team++) + for(const JsonNode & playerData : srcVector[team].Vector()) + { + PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); + if(player.isValidPlayer()) + if(mapHeader->players[player.getNum()].canAnyonePlay()) + mapHeader->players[player.getNum()].team = TeamID(team); + } + + for(PlayerInfo & player : mapHeader->players) + if(player.canAnyonePlay() && player.team == TeamID::NO_TEAM) + player.team = TeamID(mapHeader->howManyTeams++); + } +} + +void CMapFormatJson::writeTeams(JsonSerializer & handler) +{ + std::vector> teamsData; + + teamsData.resize(mapHeader->howManyTeams); + + //get raw data + for(int idx = 0; idx < mapHeader->players.size(); idx++) + { + const PlayerInfo & player = mapHeader->players.at(idx); + int team = player.team.getNum(); + if(vstd::iswithin(team, 0, mapHeader->howManyTeams-1) && player.canAnyonePlay()) + teamsData.at(team).insert(PlayerColor(idx)); + } + + //remove single-member teams + vstd::erase_if(teamsData, [](std::set & elem) -> bool + { + return elem.size() <= 1; + }); + + if(!teamsData.empty()) + { + JsonNode dest; + + //construct output + dest.setType(JsonNode::JsonType::DATA_VECTOR); + + for(const std::set & teamData : teamsData) + { + JsonNode team(JsonNode::JsonType::DATA_VECTOR); + for(const PlayerColor & player : teamData) + { + JsonNode member(JsonNode::JsonType::DATA_STRING); + member.String() = GameConstants::PLAYER_COLOR_NAMES[player.getNum()]; + team.Vector().push_back(std::move(member)); + } + dest.Vector().push_back(std::move(team)); + } + handler.serializeRaw("teams", dest, std::nullopt); + } +} + +void CMapFormatJson::readTriggeredEvents(JsonDeserializer & handler) +{ + const JsonNode & input = handler.getCurrent(); + + mapHeader->triggeredEvents.clear(); + + for(const auto & entry : input["triggeredEvents"].Struct()) + { + TriggeredEvent event; + event.identifier = entry.first; + readTriggeredEvent(event, entry.second); + mapHeader->triggeredEvents.push_back(event); + } +} + +void CMapFormatJson::readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const +{ + using namespace TriggeredEventsDetail; + + event.onFulfill.jsonDeserialize(source["message"]); + event.description.jsonDeserialize(source["description"]); + event.effect.type = vstd::find_pos(typeNames, source["effect"]["type"].String()); + event.effect.toOtherMessage.jsonDeserialize(source["effect"]["messageToSend"]); + event.trigger = EventExpression(source["condition"], JsonToCondition); // logical expression +} + +void CMapFormatJson::writeTriggeredEvents(JsonSerializer & handler) +{ + JsonNode triggeredEvents(JsonNode::JsonType::DATA_STRUCT); + + for(const auto & event : mapHeader->triggeredEvents) + writeTriggeredEvent(event, triggeredEvents[event.identifier]); + + handler.serializeRaw("triggeredEvents", triggeredEvents, std::nullopt); +} + +void CMapFormatJson::writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const +{ + using namespace TriggeredEventsDetail; + + if(!event.onFulfill.empty()) + event.onFulfill.jsonSerialize(dest["message"]); + + if(!event.description.empty()) + event.description.jsonSerialize(dest["description"]); + + dest["effect"]["type"].String() = typeNames.at(static_cast(event.effect.type)); + + if(!event.effect.toOtherMessage.empty()) + event.description.jsonSerialize(dest["effect"]["messageToSend"]); + + dest["condition"] = event.trigger.toJson(ConditionToJson); +} + +void CMapFormatJson::readDisposedHeroes(JsonSerializeFormat & handler) +{ + auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format + + const JsonNode & data = handler.getCurrent(); + + for(const auto & entry : data.Struct()) + { + HeroTypeID type(HeroTypeID::decode(entry.first)); + + std::set mask; + + for(const JsonNode & playerData : entry.second["availableFor"].Vector()) + { + PlayerColor player = PlayerColor(vstd::find_pos(GameConstants::PLAYER_COLOR_NAMES, playerData.String())); + if(player.isValidPlayer()) + mask.insert(player); + } + + if(!mask.empty() && mask.size() != PlayerColor::PLAYER_LIMIT_I && type.getNum() >= 0) + { + DisposedHero hero; + + hero.heroId = type; + hero.players = mask; + //name and portrait are not used + + map->disposedHeroes.push_back(hero); + } + } +} + +void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) +{ + if(map->disposedHeroes.empty()) + return; + + auto definitions = handler.enterStruct("predefinedHeroes");//DisposedHeroes are part of predefinedHeroes in VCMI map format + + for(DisposedHero & hero : map->disposedHeroes) + { + std::string type = HeroTypeID::encode(hero.heroId); + + auto definition = definitions->enterStruct(type); + + JsonNode players(JsonNode::JsonType::DATA_VECTOR); + definition->serializeIdArray("availableFor", hero.players); + } +} + +void CMapFormatJson::serializeRumors(JsonSerializeFormat & handler) +{ + auto rumors = handler.enterArray("rumors"); + rumors.serializeStruct(map->rumors); +} + +void CMapFormatJson::serializeTimedEvents(JsonSerializeFormat & handler) +{ + auto events = handler.enterArray("events"); + std::vector temp(map->events.begin(), map->events.end()); + events.serializeStruct(temp); + map->events.assign(temp.begin(), temp.end()); +} + +void CMapFormatJson::serializePredefinedHeroes(JsonSerializeFormat & handler) +{ + //todo:serializePredefinedHeroes + + if(handler.saving) + { + if(!map->predefinedHeroes.empty()) + { + auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); + + for(auto & hero : map->predefinedHeroes) + { + auto predefinedHero = handler.enterStruct(hero->getHeroTypeName()); + + hero->serializeJsonDefinition(handler); + } + } + } + else + { + auto predefinedHeroes = handler.enterStruct("predefinedHeroes"); + + const JsonNode & data = handler.getCurrent(); + + for(const auto & p : data.Struct()) + { + auto predefinedHero = handler.enterStruct(p.first); + + auto * hero = new CGHeroInstance(); + hero->ID = Obj::HERO; + hero->setHeroTypeName(p.first); + hero->serializeJsonDefinition(handler); + + map->predefinedHeroes.emplace_back(hero); + } + } +} + +void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) +{ + serializeRumors(handler); + + serializeTimedEvents(handler); + + serializePredefinedHeroes(handler); + + handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); + + handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); + + handler.serializeLIC("allowedSpells", &SpellID::decode, &SpellID::encode, VLC->spellh->getDefaultAllowed(), map->allowedSpells); + + //todo:events +} + +void CMapFormatJson::readOptions(JsonDeserializer & handler) +{ + readDisposedHeroes(handler); + serializeOptions(handler); +} + +void CMapFormatJson::writeOptions(JsonSerializer & handler) +{ + writeDisposedHeroes(handler); + serializeOptions(handler); +} + +///CMapPatcher +CMapPatcher::CMapPatcher(const JsonNode & stream): input(stream) +{ + //todo: update map patches and change this + fileVersionMajor = 0; + fileVersionMinor = 0; +} + +void CMapPatcher::patchMapHeader(std::unique_ptr & header) +{ + map = nullptr; + mapHeader = header.get(); + if (!input.isNull()) + readPatchData(); +} + +void CMapPatcher::readPatchData() +{ + JsonDeserializer handler(mapObjectResolver.get(), input); + readTriggeredEvents(handler); + + handler.serializeInt("defeatIconIndex", mapHeader->defeatIconIndex); + handler.serializeInt("victoryIconIndex", mapHeader->victoryIconIndex); + handler.serializeStruct("victoryString", mapHeader->victoryMessage); + handler.serializeStruct("defeatString", mapHeader->defeatMessage); +} + +///CMapLoaderJson +CMapLoaderJson::CMapLoaderJson(CInputStream * stream) + : buffer(stream) + , ioApi(new CProxyROIOApi(buffer)) + , loader("", "_", ioApi) +{ +} + +std::unique_ptr CMapLoaderJson::loadMap() +{ + LOG_TRACE(logGlobal); + std::unique_ptr result = std::make_unique(); + map = result.get(); + mapHeader = map; + readMap(); + return result; +} + +std::unique_ptr CMapLoaderJson::loadMapHeader() +{ + LOG_TRACE(logGlobal); + map = nullptr; + std::unique_ptr result = std::make_unique(); + mapHeader = result.get(); + readHeader(false); + return result; +} + +bool CMapLoaderJson::isExistArchive(const std::string & archiveFilename) +{ + return loader.existsResource(JsonPath::builtin(archiveFilename)); +} + +JsonNode CMapLoaderJson::getFromArchive(const std::string & archiveFilename) +{ + JsonPath resource = JsonPath::builtin(archiveFilename); + + if(!loader.existsResource(resource)) + throw std::runtime_error(archiveFilename+" not found"); + + auto data = loader.load(resource)->readAll(); + + JsonNode res(reinterpret_cast(data.first.get()), data.second); + + return res; +} + +void CMapLoaderJson::readMap() +{ + LOG_TRACE(logGlobal); + readHeader(true); + map->initTerrain(); + readTerrain(); + readObjects(); + + map->calculateGuardingGreaturePositions(); +} + +void CMapLoaderJson::readHeader(const bool complete) +{ + //do not use map field here, use only mapHeader + JsonNode header = getFromArchive(HEADER_FILE_NAME); + + fileVersionMajor = static_cast(header["versionMajor"].Integer()); + + if(fileVersionMajor > VERSION_MAJOR) + { + logGlobal->error("Unsupported map format version: %d", fileVersionMajor); + throw std::runtime_error("Unsupported map format version"); + } + + fileVersionMinor = static_cast(header["versionMinor"].Integer()); + + if(fileVersionMinor > VERSION_MINOR) + { + logGlobal->warn("Too new map format revision: %d. This map should work but some of map features may be ignored.", fileVersionMinor); + } + + JsonDeserializer handler(mapObjectResolver.get(), header); + + mapHeader->version = EMapFormat::VCMI;//todo: new version field + + //loading mods + if(!header["mods"].isNull()) + { + for(auto & mod : header["mods"].Vector()) + { + CModInfo::VerificationInfo info; + info.version = CModVersion::fromString(mod["version"].String()); + info.checksum = mod["checksum"].Integer(); + info.name = mod["name"].String(); + info.parent = mod["parent"].String(); + info.impactsGameplay = true; + + if(!mod["modId"].isNull()) + mapHeader->mods[mod["modId"].String()] = info; + else + mapHeader->mods[mod["name"].String()] = info; + } + } + + //todo: multilevel map load support + { + auto levels = handler.enterStruct("mapLevels"); + + { + auto surface = handler.enterStruct("surface"); + handler.serializeInt("height", mapHeader->height); + handler.serializeInt("width", mapHeader->width); + } + { + auto underground = handler.enterStruct("underground"); + mapHeader->twoLevel = !underground->getCurrent().isNull(); + } + } + + serializeHeader(handler); + + readTriggeredEvents(handler); + + readTeams(handler); + //TODO: check mods + + if(complete) + readOptions(handler); + + readTranslations(); +} + +void CMapLoaderJson::readTerrainTile(const std::string & src, TerrainTile & tile) +{ + try + { + using namespace TerrainDetail; + {//terrain type + const std::string typeCode = src.substr(0, 2); + tile.terType = getTerrainByCode(typeCode); + } + int startPos = 2; //0+typeCode fixed length + {//terrain view + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid terrain view in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.terView = atoi(rawCode.c_str()); + startPos += len; + } + {//terrain flip + int terrainFlip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (terrainFlip < 0) + throw std::runtime_error("Invalid terrain flip in " + src); + else + tile.extTileFlags = terrainFlip; + } + if (startPos >= src.size()) + return; + bool hasRoad = true; + {//road type + const std::string typeCode = src.substr(startPos, 2); + startPos += 2; + tile.roadType = getRoadByCode(typeCode); + if(!tile.roadType) //it's not a road, it's a river + { + tile.roadType = VLC->roadTypeHandler->getById(Road::NO_ROAD); + tile.riverType = getRiverByCode(typeCode); + hasRoad = false; + if(!tile.riverType) + { + throw std::runtime_error("Invalid river type in " + src); + } + } + } + if (hasRoad) + {//road dir + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid road dir in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.roadDir = atoi(rawCode.c_str()); + startPos += len; + } + if (hasRoad) + {//road flip + int flip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (flip < 0) + throw std::runtime_error("Invalid road flip in " + src); + else + tile.extTileFlags |= (flip << 4); + } + if (startPos >= src.size()) + return; + if (hasRoad) + {//river type + const std::string typeCode = src.substr(startPos, 2); + startPos += 2; + tile.riverType = getRiverByCode(typeCode); + } + {//river dir + int pos = startPos; + while (isdigit(src.at(pos))) + pos++; + int len = pos - startPos; + if (len <= 0) + throw std::runtime_error("Invalid river dir in " + src); + const std::string rawCode = src.substr(startPos, len); + tile.riverDir = atoi(rawCode.c_str()); + startPos += len; + } + {//river flip + int flip = vstd::find_pos(flipCodes, src.at(startPos++)); + if (flip < 0) + throw std::runtime_error("Invalid road flip in " + src); + else + tile.extTileFlags |= (flip << 2); + } + } + catch (const std::exception &) + { + logGlobal->error("Failed to read terrain tile: %s"); + } +} + +void CMapLoaderJson::readTerrainLevel(const JsonNode & src, const int index) +{ + int3 pos(0, 0, index); + + const JsonVector & rows = src.Vector(); + + if(rows.size() != map->height) + throw std::runtime_error("Invalid terrain data"); + + for(pos.y = 0; pos.y < map->height; pos.y++) + { + const JsonVector & tiles = rows[pos.y].Vector(); + + if(tiles.size() != map->width) + throw std::runtime_error("Invalid terrain data"); + + for(pos.x = 0; pos.x < map->width; pos.x++) + readTerrainTile(tiles[pos.x].String(), map->getTile(pos)); + } +} + +void CMapLoaderJson::readTerrain() +{ + { + const JsonNode surface = getFromArchive(TERRAIN_FILE_NAMES[0]); + readTerrainLevel(surface, 0); + } + if(map->twoLevel) + { + const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]); + readTerrainLevel(underground, 1); + } + + map->calculateWaterContent(); +} + +CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json): + owner(_owner), instance(nullptr), id(-1), jsonKey(json.first), configuration(json.second) +{ + +} + +void CMapLoaderJson::MapObjectLoader::construct() +{ + //TODO:consider move to ObjectTypeHandler + //find type handler + std::string typeName = configuration["type"].String(); + std::string subtypeName = configuration["subtype"].String(); + if(typeName.empty()) + { + logGlobal->error("Object type missing"); + logGlobal->debug(configuration.toJson()); + return; + } + + int3 pos; + pos.x = static_cast(configuration["x"].Float()); + pos.y = static_cast(configuration["y"].Float()); + pos.z = static_cast(configuration["l"].Float()); + + //special case for grail + if(typeName == "grail") + { + owner->map->grailPos = pos; + + owner->map->grailRadius = static_cast(configuration["options"]["grailRadius"].Float()); + return; + } + else if(subtypeName.empty()) + { + logGlobal->error("Object subtype missing"); + logGlobal->debug(configuration.toJson()); + return; + } + + auto handler = VLC->objtypeh->getHandlerFor( ModScope::scopeMap(), typeName, subtypeName); + + auto * appearance = new ObjectTemplate; + + appearance->id = Obj(handler->getIndex()); + appearance->subid = handler->getSubIndex(); + appearance->readJson(configuration["template"], false); + + // Will be destroyed soon and replaced with shared template + instance = handler->create(std::shared_ptr(appearance)); + + instance->id = ObjectInstanceID(static_cast(owner->map->objects.size())); + instance->instanceName = jsonKey; + instance->pos = pos; + owner->map->addNewObject(instance); +} + +void CMapLoaderJson::MapObjectLoader::configure() +{ + if(nullptr == instance) + return; + + JsonDeserializer handler(owner->mapObjectResolver.get(), configuration); + + instance->serializeJson(handler); + + //artifact instance serialization requires access to Map object, handle it here for now + //todo: find better solution for artifact instance serialization + + if(auto * art = dynamic_cast(instance)) + { + ArtifactID artID = ArtifactID::NONE; + SpellID spellID = SpellID::NONE; + + if(art->ID == Obj::SPELL_SCROLL) + { + auto spellIdentifier = configuration["options"]["spell"].String(); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), "spell", spellIdentifier); + if(rawId) + spellID = rawId.value(); + else + spellID = 0; + artID = ArtifactID::SPELL_SCROLL; + } + else if(art->ID == Obj::ARTIFACT) + { + //specific artifact + artID = ArtifactID(art->subID); + } + + art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); + } + + if(auto * hero = dynamic_cast(instance)) + { + auto o = handler.enterStruct("options"); + hero->serializeJsonArtifacts(handler, "artifacts", owner->map); + } +} + +void CMapLoaderJson::readObjects() +{ + LOG_TRACE(logGlobal); + + std::vector> loaders;//todo: optimize MapObjectLoader memory layout + + JsonNode data = getFromArchive(OBJECTS_FILE_NAME); + + //get raw data + for(auto & p : data.Struct()) + loaders.push_back(std::make_unique(this, p)); + + for(auto & ptr : loaders) + ptr->construct(); + + //configure objects after all objects are constructed so we may resolve internal IDs even to actual pointers OTF + for(auto & ptr : loaders) + ptr->configure(); + + std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) + { + return a->subID < b->subID; + }); +} + +void CMapLoaderJson::readTranslations() +{ + std::list languages{Languages::getLanguageList().begin(), Languages::getLanguageList().end()}; + for(auto & language : Languages::getLanguageList()) + { + if(isExistArchive(language.identifier + ".json")) + mapHeader->translations.Struct()[language.identifier] = getFromArchive(language.identifier + ".json"); + } + mapHeader->registerMapStrings(); +} + + +///CMapSaverJson +CMapSaverJson::CMapSaverJson(CInputOutputStream * stream) + : buffer(stream) + , ioApi(new CProxyIOApi(buffer)) + , saver(ioApi, "_") +{ + fileVersionMajor = VERSION_MAJOR; + fileVersionMinor = VERSION_MINOR; +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CMapSaverJson::~CMapSaverJson() = default; + +void CMapSaverJson::addToArchive(const JsonNode & data, const std::string & filename) +{ + std::ostringstream out; + JsonWriter writer(out); + writer.writeNode(data); + out.flush(); + + { + auto s = out.str(); + std::unique_ptr stream = saver.addFile(filename); + + if(stream->write(reinterpret_cast(s.c_str()), s.size()) != s.size()) + throw std::runtime_error("CMapSaverJson::saveHeader() zip compression failed."); + } +} + +void CMapSaverJson::saveMap(const std::unique_ptr& map) +{ + this->map = map.get(); + this->mapHeader = this->map; + writeHeader(); + writeTerrain(); + writeObjects(); +} + +void CMapSaverJson::writeHeader() +{ + logGlobal->trace("Saving header"); + + JsonNode header; + JsonSerializer handler(mapObjectResolver.get(), header); + + header["versionMajor"].Float() = VERSION_MAJOR; + header["versionMinor"].Float() = VERSION_MINOR; + + //write mods + JsonNode & mods = header["mods"]; + for(const auto & mod : mapHeader->mods) + { + JsonNode modWriter; + modWriter["modId"].String() = mod.first; + modWriter["name"].String() = mod.second.name; + modWriter["parent"].String() = mod.second.parent; + modWriter["version"].String() = mod.second.version.toString(); + modWriter["checksum"].Integer() = mod.second.checksum; + mods.Vector().push_back(modWriter); + } + + //todo: multilevel map save support + JsonNode & levels = header["mapLevels"]; + levels["surface"]["height"].Float() = mapHeader->height; + levels["surface"]["width"].Float() = mapHeader->width; + levels["surface"]["index"].Float() = 0; + + if(mapHeader->twoLevel) + { + levels["underground"]["height"].Float() = mapHeader->height; + levels["underground"]["width"].Float() = mapHeader->width; + levels["underground"]["index"].Float() = 1; + } + + serializeHeader(handler); + + writeTriggeredEvents(handler); + + writeTeams(handler); + + writeOptions(handler); + + writeTranslations(); + + addToArchive(header, HEADER_FILE_NAME); +} + +std::string CMapSaverJson::writeTerrainTile(const TerrainTile & tile) +{ + using namespace TerrainDetail; + + std::ostringstream out; + out.setf(std::ios::dec, std::ios::basefield); + out.unsetf(std::ios::showbase); + + out << tile.terType->shortIdentifier << static_cast(tile.terView) << flipCodes[tile.extTileFlags % 4]; + + if(tile.roadType->getId() != Road::NO_ROAD) + out << tile.roadType->shortIdentifier << static_cast(tile.roadDir) << flipCodes[(tile.extTileFlags >> 4) % 4]; + + if(tile.riverType->getId() != River::NO_RIVER) + out << tile.riverType->shortIdentifier << static_cast(tile.riverDir) << flipCodes[(tile.extTileFlags >> 2) % 4]; + + return out.str(); +} + +JsonNode CMapSaverJson::writeTerrainLevel(const int index) +{ + JsonNode data; + int3 pos(0,0,index); + + data.Vector().resize(map->height); + + for(pos.y = 0; pos.y < map->height; pos.y++) + { + JsonNode & row = data.Vector()[pos.y]; + + row.Vector().resize(map->width); + + for(pos.x = 0; pos.x < map->width; pos.x++) + row.Vector()[pos.x].String() = writeTerrainTile(map->getTile(pos)); + } + + return data; +} + +void CMapSaverJson::writeTerrain() +{ + logGlobal->trace("Saving terrain"); + //todo: multilevel map save support + + JsonNode surface = writeTerrainLevel(0); + addToArchive(surface, TERRAIN_FILE_NAMES[0]); + + if(map->twoLevel) + { + JsonNode underground = writeTerrainLevel(1); + addToArchive(underground, TERRAIN_FILE_NAMES[1]); + } +} + +void CMapSaverJson::writeObjects() +{ + logGlobal->trace("Saving objects"); + JsonNode data(JsonNode::JsonType::DATA_STRUCT); + + JsonSerializer handler(mapObjectResolver.get(), data); + + for(CGObjectInstance * obj : map->objects) + { + //logGlobal->trace("\t%s", obj->instanceName); + auto temp = handler.enterStruct(obj->instanceName); + + obj->serializeJson(handler); + } + + if(map->grailPos.valid()) + { + JsonNode grail(JsonNode::JsonType::DATA_STRUCT); + grail["type"].String() = "grail"; + + grail["x"].Float() = map->grailPos.x; + grail["y"].Float() = map->grailPos.y; + grail["l"].Float() = map->grailPos.z; + + grail["options"]["radius"].Float() = map->grailRadius; + + std::string grailId = boost::str(boost::format("grail_%d") % map->objects.size()); + + data[grailId] = grail; + } + + //cleanup empty options + for(auto & p : data.Struct()) + { + JsonNode & obj = p.second; + if(obj["options"].Struct().empty()) + obj.Struct().erase("options"); + } + + addToArchive(data, OBJECTS_FILE_NAME); +} + +void CMapSaverJson::writeTranslations() +{ + for(auto & s : mapHeader->translations.Struct()) + { + auto & language = s.first; + if(Languages::getLanguageOptions(language).identifier.empty()) + { + logGlobal->error("Serializing of unsupported language %s is not permitted", language); + continue;; + } + logGlobal->trace("Saving translations, language: %s", language); + addToArchive(s.second, language + ".json"); + } +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapFormatJson.h b/lib/mapping/MapFormatJson.h index 9f5ee3e36..6efe186b4 100644 --- a/lib/mapping/MapFormatJson.h +++ b/lib/mapping/MapFormatJson.h @@ -1,292 +1,292 @@ -/* - * MapFormatJson.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 "CMapService.h" -#include "../JsonNode.h" - -#include "../filesystem/CZipSaver.h" -#include "../filesystem/CZipLoader.h" -#include "../GameConstants.h" - -#include "../serializer/JsonSerializeFormat.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct TriggeredEvent; -struct TerrainTile; -struct PlayerInfo; -class CGObjectInstance; -class AObjectTypeHandler; -class TerrainType; -class RoadType; -class RiverType; - -class JsonSerializeFormat; -class JsonDeserializer; -class JsonSerializer; - -class DLL_LINKAGE CMapFormatJson -{ -public: - static const int VERSION_MAJOR; - static const int VERSION_MINOR; - - static const std::string HEADER_FILE_NAME; - static const std::string OBJECTS_FILE_NAME; - static const std::string TERRAIN_FILE_NAMES[2]; - - int fileVersionMajor; - int fileVersionMinor; -protected: - friend class MapObjectResolver; - std::unique_ptr mapObjectResolver; - - /** ptr to the map object which gets filled by data from the buffer or written to buffer */ - CMap * map; - - /** - * ptr to the map header object which gets filled by data from the buffer. - * (when loading map and mapHeader point to the same object) - */ - CMapHeader * mapHeader; - - CMapFormatJson(); - - static TerrainType * getTerrainByCode(const std::string & code); - static RiverType * getRiverByCode(const std::string & code); - static RoadType * getRoadByCode(const std::string & code); - - void serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const; - - ///common part of header saving/loading - void serializeHeader(JsonSerializeFormat & handler); - - ///player information saving/loading - void serializePlayerInfo(JsonSerializeFormat & handler); - - /** - * Reads team settings to header - */ - void readTeams(JsonDeserializer & handler); - - /** - * Saves team settings to header - */ - void writeTeams(JsonSerializer & handler); - - /** - * Reads triggered events, including victory/loss conditions - */ - void readTriggeredEvents(JsonDeserializer & handler); - - /** - * Writes triggered events, including victory/loss conditions - */ - void writeTriggeredEvents(JsonSerializer & handler); - - /** - * Reads one of triggered events - */ - void readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const; - - /** - * Writes one of triggered events - */ - void writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const; - - void writeDisposedHeroes(JsonSerializeFormat & handler); - - void readDisposedHeroes(JsonSerializeFormat & handler); - - void serializePredefinedHeroes(JsonSerializeFormat & handler); - - void serializeRumors(JsonSerializeFormat & handler); - - void serializeTimedEvents(JsonSerializeFormat & handler); - - ///common part of map attributes saving/loading - void serializeOptions(JsonSerializeFormat & handler); - - /** - * Loads map attributes except header ones - */ - void readOptions(JsonDeserializer & handler); - - /** - * Saves map attributes except header ones - */ - void writeOptions(JsonSerializer & handler); -}; - -class DLL_LINKAGE CMapPatcher : public CMapFormatJson, public IMapPatcher -{ -public: - /** - * Default constructor. - * - * @param stream. A stream containing the map data. - */ - CMapPatcher(const JsonNode & stream); - -public: //IMapPatcher - /** - * Modifies supplied map header using Json data - * - */ - void patchMapHeader(std::unique_ptr & header) override; - -private: - /** - * Reads subset of header that can be replaced by patching. - */ - void readPatchData(); - - JsonNode input; -}; - -class DLL_LINKAGE CMapLoaderJson : public CMapFormatJson, public IMapLoader -{ -public: - /** - * Constructor. - * - * @param stream a stream containing the map data - */ - CMapLoaderJson(CInputStream * stream); - - /** - * Loads the VCMI/Json map file. - * - * @return a unique ptr of the loaded map class - */ - std::unique_ptr loadMap() override; - - /** - * Loads the VCMI/Json map header. - * - * @return a unique ptr of the loaded map header class - */ - std::unique_ptr loadMapHeader() override; - - struct MapObjectLoader - { - MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json); - CMapLoaderJson * owner; - CGObjectInstance * instance; - ObjectInstanceID id; - std::string jsonKey;//full id defined by map creator - JsonNode & configuration; - - ///constructs object (without configuration) - void construct(); - - ///configures object - void configure(); - }; - - /** - * Reads the map header. - */ - void readHeader(const bool complete); - - /** - * Reads complete map. - */ - void readMap(); - - /** - * Reads texts and translations - */ - void readTranslations(); - - static void readTerrainTile(const std::string & src, TerrainTile & tile); - - void readTerrainLevel(const JsonNode & src, const int index); - - void readTerrain(); - - /** - * Loads all map objects from zip archive - */ - void readObjects(); - - bool isExistArchive(const std::string & archiveFilename); - JsonNode getFromArchive(const std::string & archiveFilename); - -private: - CInputStream * buffer; - std::shared_ptr ioApi; - - CZipLoader loader;///< object to handle zip archive operations -}; - -class DLL_LINKAGE CMapSaverJson : public CMapFormatJson, public IMapSaver -{ -public: - /** - * Constructor. - * - * @param stream a stream to save the map to, will contain zip archive - */ - CMapSaverJson(CInputOutputStream * stream); - - ~CMapSaverJson(); - - /** - * Actually saves the VCMI/Json map into stream. - */ - void saveMap(const std::unique_ptr & map) override; - - /** - * Saves @data as json file with specified @filename - */ - void addToArchive(const JsonNode & data, const std::string & filename); - - /** - * Saves header to zip archive - */ - void writeHeader(); - - /** - * Saves texts and translations to zip archive - */ - void writeTranslations(); - - /** - * Encodes one tile into string - * @param tile tile to serialize - */ - static std::string writeTerrainTile(const TerrainTile & tile); - - /** - * Saves map level into json - * @param index z coordinate - */ - JsonNode writeTerrainLevel(const int index); - - /** - * Saves all terrain into zip archive - */ - void writeTerrain(); - - /** - * Saves all map objects into zip archive - */ - void writeObjects(); - -private: - CInputOutputStream * buffer; - std::shared_ptr ioApi; - CZipSaver saver;///< object to handle zip archive operations -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapFormatJson.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 "CMapService.h" +#include "../JsonNode.h" + +#include "../filesystem/CZipSaver.h" +#include "../filesystem/CZipLoader.h" +#include "../GameConstants.h" + +#include "../serializer/JsonSerializeFormat.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct TriggeredEvent; +struct TerrainTile; +struct PlayerInfo; +class CGObjectInstance; +class AObjectTypeHandler; +class TerrainType; +class RoadType; +class RiverType; + +class JsonSerializeFormat; +class JsonDeserializer; +class JsonSerializer; + +class DLL_LINKAGE CMapFormatJson +{ +public: + static const int VERSION_MAJOR; + static const int VERSION_MINOR; + + static const std::string HEADER_FILE_NAME; + static const std::string OBJECTS_FILE_NAME; + static const std::string TERRAIN_FILE_NAMES[2]; + + int fileVersionMajor; + int fileVersionMinor; +protected: + friend class MapObjectResolver; + std::unique_ptr mapObjectResolver; + + /** ptr to the map object which gets filled by data from the buffer or written to buffer */ + CMap * map; + + /** + * ptr to the map header object which gets filled by data from the buffer. + * (when loading map and mapHeader point to the same object) + */ + CMapHeader * mapHeader; + + CMapFormatJson(); + + static TerrainType * getTerrainByCode(const std::string & code); + static RiverType * getRiverByCode(const std::string & code); + static RoadType * getRoadByCode(const std::string & code); + + void serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const; + + ///common part of header saving/loading + void serializeHeader(JsonSerializeFormat & handler); + + ///player information saving/loading + void serializePlayerInfo(JsonSerializeFormat & handler); + + /** + * Reads team settings to header + */ + void readTeams(JsonDeserializer & handler); + + /** + * Saves team settings to header + */ + void writeTeams(JsonSerializer & handler); + + /** + * Reads triggered events, including victory/loss conditions + */ + void readTriggeredEvents(JsonDeserializer & handler); + + /** + * Writes triggered events, including victory/loss conditions + */ + void writeTriggeredEvents(JsonSerializer & handler); + + /** + * Reads one of triggered events + */ + void readTriggeredEvent(TriggeredEvent & event, const JsonNode & source) const; + + /** + * Writes one of triggered events + */ + void writeTriggeredEvent(const TriggeredEvent & event, JsonNode & dest) const; + + void writeDisposedHeroes(JsonSerializeFormat & handler); + + void readDisposedHeroes(JsonSerializeFormat & handler); + + void serializePredefinedHeroes(JsonSerializeFormat & handler); + + void serializeRumors(JsonSerializeFormat & handler); + + void serializeTimedEvents(JsonSerializeFormat & handler); + + ///common part of map attributes saving/loading + void serializeOptions(JsonSerializeFormat & handler); + + /** + * Loads map attributes except header ones + */ + void readOptions(JsonDeserializer & handler); + + /** + * Saves map attributes except header ones + */ + void writeOptions(JsonSerializer & handler); +}; + +class DLL_LINKAGE CMapPatcher : public CMapFormatJson, public IMapPatcher +{ +public: + /** + * Default constructor. + * + * @param stream. A stream containing the map data. + */ + CMapPatcher(const JsonNode & stream); + +public: //IMapPatcher + /** + * Modifies supplied map header using Json data + * + */ + void patchMapHeader(std::unique_ptr & header) override; + +private: + /** + * Reads subset of header that can be replaced by patching. + */ + void readPatchData(); + + JsonNode input; +}; + +class DLL_LINKAGE CMapLoaderJson : public CMapFormatJson, public IMapLoader +{ +public: + /** + * Constructor. + * + * @param stream a stream containing the map data + */ + CMapLoaderJson(CInputStream * stream); + + /** + * Loads the VCMI/Json map file. + * + * @return a unique ptr of the loaded map class + */ + std::unique_ptr loadMap() override; + + /** + * Loads the VCMI/Json map header. + * + * @return a unique ptr of the loaded map header class + */ + std::unique_ptr loadMapHeader() override; + + struct MapObjectLoader + { + MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json); + CMapLoaderJson * owner; + CGObjectInstance * instance; + ObjectInstanceID id; + std::string jsonKey;//full id defined by map creator + JsonNode & configuration; + + ///constructs object (without configuration) + void construct(); + + ///configures object + void configure(); + }; + + /** + * Reads the map header. + */ + void readHeader(const bool complete); + + /** + * Reads complete map. + */ + void readMap(); + + /** + * Reads texts and translations + */ + void readTranslations(); + + static void readTerrainTile(const std::string & src, TerrainTile & tile); + + void readTerrainLevel(const JsonNode & src, const int index); + + void readTerrain(); + + /** + * Loads all map objects from zip archive + */ + void readObjects(); + + bool isExistArchive(const std::string & archiveFilename); + JsonNode getFromArchive(const std::string & archiveFilename); + +private: + CInputStream * buffer; + std::shared_ptr ioApi; + + CZipLoader loader;///< object to handle zip archive operations +}; + +class DLL_LINKAGE CMapSaverJson : public CMapFormatJson, public IMapSaver +{ +public: + /** + * Constructor. + * + * @param stream a stream to save the map to, will contain zip archive + */ + CMapSaverJson(CInputOutputStream * stream); + + ~CMapSaverJson(); + + /** + * Actually saves the VCMI/Json map into stream. + */ + void saveMap(const std::unique_ptr & map) override; + + /** + * Saves @data as json file with specified @filename + */ + void addToArchive(const JsonNode & data, const std::string & filename); + + /** + * Saves header to zip archive + */ + void writeHeader(); + + /** + * Saves texts and translations to zip archive + */ + void writeTranslations(); + + /** + * Encodes one tile into string + * @param tile tile to serialize + */ + static std::string writeTerrainTile(const TerrainTile & tile); + + /** + * Saves map level into json + * @param index z coordinate + */ + JsonNode writeTerrainLevel(const int index); + + /** + * Saves all terrain into zip archive + */ + void writeTerrain(); + + /** + * Saves all map objects into zip archive + */ + void writeObjects(); + +private: + CInputOutputStream * buffer; + std::shared_ptr ioApi; + CZipSaver saver;///< object to handle zip archive operations +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 0ec268575..772c251de 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -1,443 +1,443 @@ -/* - * MapReaderH3M.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 "MapReaderH3M.h" - -#include "../filesystem/CBinaryReader.h" -#include "../int3.h" -#include "../mapObjects/ObjectTemplate.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template<> -BuildingID MapReaderH3M::remapIdentifier(const BuildingID & identifier) -{ - return identifier; -} - -template<> -GameResID MapReaderH3M::remapIdentifier(const GameResID & identifier) -{ - return identifier; -} - -template<> -SpellID MapReaderH3M::remapIdentifier(const SpellID & identifier) -{ - return identifier; -} - -template<> -PlayerColor MapReaderH3M::remapIdentifier(const PlayerColor & identifier) -{ - return identifier; -} - -template -Identifier MapReaderH3M::remapIdentifier(const Identifier & identifier) -{ - return remapper.remap(identifier); -} - -MapReaderH3M::MapReaderH3M(CInputStream * stream) - : reader(std::make_unique(stream)) -{ -} - -void MapReaderH3M::setFormatLevel(const MapFormatFeaturesH3M & newFeatures) -{ - features = newFeatures; -} - -void MapReaderH3M::setIdentifierRemapper(const MapIdentifiersH3M & newRemapper) -{ - remapper = newRemapper; -} - -ArtifactID MapReaderH3M::readArtifact() -{ - ArtifactID result; - - if(features.levelAB) - result = ArtifactID(reader->readUInt16()); - else - result = ArtifactID(reader->readUInt8()); - - if(result.getNum() == features.artifactIdentifierInvalid) - return ArtifactID::NONE; - - if (result.getNum() < features.artifactsCount) - return remapIdentifier(result); - - logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); - return ArtifactID::NONE; -} - -ArtifactID MapReaderH3M::readArtifact32() -{ - ArtifactID result(reader->readInt32()); - - if(result == ArtifactID::NONE) - return ArtifactID::NONE; - - if (result.getNum() < features.artifactsCount) - return remapIdentifier(result); - - logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); - return ArtifactID::NONE; -} - -HeroTypeID MapReaderH3M::readHero() -{ - HeroTypeID result(reader->readUInt8()); - - if(result.getNum() == features.heroIdentifierInvalid) - return HeroTypeID(-1); - - assert(result.getNum() < features.heroesCount); - return remapIdentifier(result); -} - -HeroTypeID MapReaderH3M::readHeroPortrait() -{ - HeroTypeID result(reader->readUInt8()); - - if(result.getNum() == features.heroIdentifierInvalid) - return HeroTypeID::NONE; - - if (result.getNum() >= features.heroesPortraitsCount) - { - logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); - return HeroTypeID::NONE; - } - - return remapper.remapPortrait(result); -} - -CreatureID MapReaderH3M::readCreature() -{ - CreatureID result; - - if(features.levelAB) - result = CreatureID(reader->readUInt16()); - else - result = CreatureID(reader->readUInt8()); - - if(result.getNum() == features.creatureIdentifierInvalid) - return CreatureID::NONE; - - if(result.getNum() < features.creaturesCount) - return remapIdentifier(result);; - - // this may be random creature in army/town, to be randomized later - CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); - assert(randomIndex < CreatureID::NONE); - - if (randomIndex.getNum() > -16) - return randomIndex; - - logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); - return CreatureID::NONE; -} - -TerrainId MapReaderH3M::readTerrain() -{ - TerrainId result(readUInt8()); - assert(result.getNum() < features.terrainsCount); - return remapIdentifier(result);; -} - -RoadId MapReaderH3M::readRoad() -{ - RoadId result(readInt8()); - assert(result.getNum() <= features.roadsCount); - return result; -} - -RiverId MapReaderH3M::readRiver() -{ - RiverId result(readInt8()); - assert(result.getNum() <= features.riversCount); - return result; -} - -SecondarySkill MapReaderH3M::readSkill() -{ - SecondarySkill result(readUInt8()); - assert(result.getNum() < features.skillsCount); - return remapIdentifier(result);; -} - -SpellID MapReaderH3M::readSpell() -{ - SpellID result(readUInt8()); - if(result.getNum() == features.spellIdentifierInvalid) - return SpellID::NONE; - if(result.getNum() == features.spellIdentifierInvalid - 1) - return SpellID::PRESET; - - assert(result.getNum() < features.spellsCount); - return remapIdentifier(result);; -} - -SpellID MapReaderH3M::readSpell32() -{ - SpellID result(readInt32()); - if(result.getNum() == features.spellIdentifierInvalid) - return SpellID::NONE; - assert(result.getNum() < features.spellsCount); - return result; -} - -PlayerColor MapReaderH3M::readPlayer() -{ - uint8_t value = readUInt8(); - - if (value == 255) - return PlayerColor::NEUTRAL; - - if (value >= PlayerColor::PLAYER_LIMIT_I) - { - logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); - return PlayerColor::NEUTRAL; - } - - return PlayerColor(value); -} - -PlayerColor MapReaderH3M::readPlayer32() -{ - uint32_t value = readUInt32(); - - if (value == 255) - return PlayerColor::NEUTRAL; - - if (value >= PlayerColor::PLAYER_LIMIT_I) - { - logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); - return PlayerColor::NEUTRAL; - } - - return PlayerColor(value); -} - -void MapReaderH3M::readBitmaskBuildings(std::set & dest, std::optional faction) -{ - std::set h3m; - readBitmask(h3m, features.buildingsBytes, features.buildingsCount, false); - - for (auto const & h3mEntry : h3m) - { - BuildingID mapped = remapper.remapBuilding(faction, h3mEntry); - - if (mapped != BuildingID::NONE) // artifact merchant may be set in random town, but not present in actual town - dest.insert(mapped); - } -} - -void MapReaderH3M::readBitmaskFactions(std::set & dest, bool invert) -{ - readBitmask(dest, features.factionsBytes, features.factionsCount, invert); -} - -void MapReaderH3M::readBitmaskPlayers(std::set & dest, bool invert) -{ - readBitmask(dest, 1, 8, invert); -} - -void MapReaderH3M::readBitmaskResources(std::set & dest, bool invert) -{ - readBitmask(dest, features.resourcesBytes, features.resourcesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, bool invert) -{ - uint32_t classesCount = reader->readUInt32(); - uint32_t classesBytes = (classesCount + 7) / 8; - - readBitmask(dest, classesBytes, classesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroes(std::vector & dest, bool invert) -{ - readBitmask(dest, features.heroesBytes, features.heroesCount, invert); -} - -void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) -{ - uint32_t heroesCount = readUInt32(); - uint32_t heroesBytes = (heroesCount + 7) / 8; - assert(heroesCount <= features.heroesCount); - - readBitmask(dest, heroesBytes, heroesCount, invert); -} - -void MapReaderH3M::readBitmaskArtifacts(std::vector &dest, bool invert) -{ - readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); -} - -void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool invert) -{ - uint32_t artifactsCount = reader->readUInt32(); - uint32_t artifactsBytes = (artifactsCount + 7) / 8; - assert(artifactsCount <= features.artifactsCount); - - readBitmask(dest, artifactsBytes, artifactsCount, invert); -} - -void MapReaderH3M::readBitmaskSpells(std::vector & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - -void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - -void MapReaderH3M::readBitmaskSkills(std::vector & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - -void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - -template -void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, const int objectsToRead, bool invert) -{ - for(int byte = 0; byte < bytesToRead; ++byte) - { - const ui8 mask = reader->readUInt8(); - for(int bit = 0; bit < 8; ++bit) - { - if(byte * 8 + bit < objectsToRead) - { - const size_t index = byte * 8 + bit; - const bool flag = mask & (1 << bit); - const bool result = (flag != invert); - - Identifier h3mID(index); - Identifier vcmiID = remapIdentifier(h3mID); - - if (vcmiID.getNum() >= dest.size()) - dest.resize(vcmiID.getNum() + 1); - dest[vcmiID.getNum()] = result; - } - } - } -} - -template -void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) -{ - std::vector bitmap; - bitmap.resize(objectsToRead, false); - readBitmask(bitmap, bytesToRead, objectsToRead, invert); - - for(int i = 0; i < bitmap.size(); i++) - if(bitmap[i]) - dest.insert(static_cast(i)); -} - -int3 MapReaderH3M::readInt3() -{ - int3 p; - p.x = reader->readUInt8(); - p.y = reader->readUInt8(); - p.z = reader->readUInt8(); - return p; -} - -std::shared_ptr MapReaderH3M::readObjectTemplate() -{ - auto tmpl = std::make_shared(); - tmpl->readMap(*reader); - remapper.remapTemplate(*tmpl); - return tmpl; -} - -void MapReaderH3M::skipUnused(size_t amount) -{ - reader->skip(amount); -} - -void MapReaderH3M::skipZero(size_t amount) -{ -#ifdef NDEBUG - skipUnused(amount); -#else - for(size_t i = 0; i < amount; ++i) - { - uint8_t value = reader->readUInt8(); - assert(value == 0); - } -#endif -} - -void MapReaderH3M::readResourses(TResources & resources) -{ - for(int x = 0; x < features.resourcesCount; ++x) - resources[x] = reader->readInt32(); -} - -bool MapReaderH3M::readBool() -{ - uint8_t result = readUInt8(); - assert(result == 0 || result == 1); - - return result != 0; -} - -ui8 MapReaderH3M::readUInt8() -{ - return reader->readUInt8(); -} - -si8 MapReaderH3M::readInt8() -{ - return reader->readInt8(); -} - -ui16 MapReaderH3M::readUInt16() -{ - return reader->readUInt16(); -} - -si16 MapReaderH3M::readInt16() -{ - return reader->readInt16(); -} - -ui32 MapReaderH3M::readUInt32() -{ - return reader->readUInt32(); -} - -si32 MapReaderH3M::readInt32() -{ - return reader->readInt32(); -} - -std::string MapReaderH3M::readBaseString() -{ - return reader->readBaseString(); -} - -CBinaryReader & MapReaderH3M::getInternalReader() -{ - return *reader; -} - -VCMI_LIB_NAMESPACE_END +/* + * MapReaderH3M.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 "MapReaderH3M.h" + +#include "../filesystem/CBinaryReader.h" +#include "../int3.h" +#include "../mapObjects/ObjectTemplate.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template<> +BuildingID MapReaderH3M::remapIdentifier(const BuildingID & identifier) +{ + return identifier; +} + +template<> +GameResID MapReaderH3M::remapIdentifier(const GameResID & identifier) +{ + return identifier; +} + +template<> +SpellID MapReaderH3M::remapIdentifier(const SpellID & identifier) +{ + return identifier; +} + +template<> +PlayerColor MapReaderH3M::remapIdentifier(const PlayerColor & identifier) +{ + return identifier; +} + +template +Identifier MapReaderH3M::remapIdentifier(const Identifier & identifier) +{ + return remapper.remap(identifier); +} + +MapReaderH3M::MapReaderH3M(CInputStream * stream) + : reader(std::make_unique(stream)) +{ +} + +void MapReaderH3M::setFormatLevel(const MapFormatFeaturesH3M & newFeatures) +{ + features = newFeatures; +} + +void MapReaderH3M::setIdentifierRemapper(const MapIdentifiersH3M & newRemapper) +{ + remapper = newRemapper; +} + +ArtifactID MapReaderH3M::readArtifact() +{ + ArtifactID result; + + if(features.levelAB) + result = ArtifactID(reader->readUInt16()); + else + result = ArtifactID(reader->readUInt8()); + + if(result.getNum() == features.artifactIdentifierInvalid) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + +ArtifactID MapReaderH3M::readArtifact32() +{ + ArtifactID result(reader->readInt32()); + + if(result == ArtifactID::NONE) + return ArtifactID::NONE; + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + +HeroTypeID MapReaderH3M::readHero() +{ + HeroTypeID result(reader->readUInt8()); + + if(result.getNum() == features.heroIdentifierInvalid) + return HeroTypeID(-1); + + assert(result.getNum() < features.heroesCount); + return remapIdentifier(result); +} + +HeroTypeID MapReaderH3M::readHeroPortrait() +{ + HeroTypeID result(reader->readUInt8()); + + if(result.getNum() == features.heroIdentifierInvalid) + return HeroTypeID::NONE; + + if (result.getNum() >= features.heroesPortraitsCount) + { + logGlobal->warn("Map contains invalid hero portrait ID %d. Will be reset!", result.getNum() ); + return HeroTypeID::NONE; + } + + return remapper.remapPortrait(result); +} + +CreatureID MapReaderH3M::readCreature() +{ + CreatureID result; + + if(features.levelAB) + result = CreatureID(reader->readUInt16()); + else + result = CreatureID(reader->readUInt8()); + + if(result.getNum() == features.creatureIdentifierInvalid) + return CreatureID::NONE; + + if(result.getNum() < features.creaturesCount) + return remapIdentifier(result);; + + // this may be random creature in army/town, to be randomized later + CreatureID randomIndex(result.getNum() - features.creatureIdentifierInvalid - 1); + assert(randomIndex < CreatureID::NONE); + + if (randomIndex.getNum() > -16) + return randomIndex; + + logGlobal->warn("Map contains invalid creature %d. Will be removed!", result.getNum()); + return CreatureID::NONE; +} + +TerrainId MapReaderH3M::readTerrain() +{ + TerrainId result(readUInt8()); + assert(result.getNum() < features.terrainsCount); + return remapIdentifier(result);; +} + +RoadId MapReaderH3M::readRoad() +{ + RoadId result(readInt8()); + assert(result.getNum() <= features.roadsCount); + return result; +} + +RiverId MapReaderH3M::readRiver() +{ + RiverId result(readInt8()); + assert(result.getNum() <= features.riversCount); + return result; +} + +SecondarySkill MapReaderH3M::readSkill() +{ + SecondarySkill result(readUInt8()); + assert(result.getNum() < features.skillsCount); + return remapIdentifier(result);; +} + +SpellID MapReaderH3M::readSpell() +{ + SpellID result(readUInt8()); + if(result.getNum() == features.spellIdentifierInvalid) + return SpellID::NONE; + if(result.getNum() == features.spellIdentifierInvalid - 1) + return SpellID::PRESET; + + assert(result.getNum() < features.spellsCount); + return remapIdentifier(result);; +} + +SpellID MapReaderH3M::readSpell32() +{ + SpellID result(readInt32()); + if(result.getNum() == features.spellIdentifierInvalid) + return SpellID::NONE; + assert(result.getNum() < features.spellsCount); + return result; +} + +PlayerColor MapReaderH3M::readPlayer() +{ + uint8_t value = readUInt8(); + + if (value == 255) + return PlayerColor::NEUTRAL; + + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + + return PlayerColor(value); +} + +PlayerColor MapReaderH3M::readPlayer32() +{ + uint32_t value = readUInt32(); + + if (value == 255) + return PlayerColor::NEUTRAL; + + if (value >= PlayerColor::PLAYER_LIMIT_I) + { + logGlobal->warn("Map contains invalid player ID %d. Will be reset!", value ); + return PlayerColor::NEUTRAL; + } + + return PlayerColor(value); +} + +void MapReaderH3M::readBitmaskBuildings(std::set & dest, std::optional faction) +{ + std::set h3m; + readBitmask(h3m, features.buildingsBytes, features.buildingsCount, false); + + for (auto const & h3mEntry : h3m) + { + BuildingID mapped = remapper.remapBuilding(faction, h3mEntry); + + if (mapped != BuildingID::NONE) // artifact merchant may be set in random town, but not present in actual town + dest.insert(mapped); + } +} + +void MapReaderH3M::readBitmaskFactions(std::set & dest, bool invert) +{ + readBitmask(dest, features.factionsBytes, features.factionsCount, invert); +} + +void MapReaderH3M::readBitmaskPlayers(std::set & dest, bool invert) +{ + readBitmask(dest, 1, 8, invert); +} + +void MapReaderH3M::readBitmaskResources(std::set & dest, bool invert) +{ + readBitmask(dest, features.resourcesBytes, features.resourcesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, bool invert) +{ + uint32_t classesCount = reader->readUInt32(); + uint32_t classesBytes = (classesCount + 7) / 8; + + readBitmask(dest, classesBytes, classesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroes(std::vector & dest, bool invert) +{ + readBitmask(dest, features.heroesBytes, features.heroesCount, invert); +} + +void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) +{ + uint32_t heroesCount = readUInt32(); + uint32_t heroesBytes = (heroesCount + 7) / 8; + assert(heroesCount <= features.heroesCount); + + readBitmask(dest, heroesBytes, heroesCount, invert); +} + +void MapReaderH3M::readBitmaskArtifacts(std::vector &dest, bool invert) +{ + readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); +} + +void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool invert) +{ + uint32_t artifactsCount = reader->readUInt32(); + uint32_t artifactsBytes = (artifactsCount + 7) / 8; + assert(artifactsCount <= features.artifactsCount); + + readBitmask(dest, artifactsBytes, artifactsCount, invert); +} + +void MapReaderH3M::readBitmaskSpells(std::vector & dest, bool invert) +{ + readBitmask(dest, features.spellsBytes, features.spellsCount, invert); +} + +void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) +{ + readBitmask(dest, features.spellsBytes, features.spellsCount, invert); +} + +void MapReaderH3M::readBitmaskSkills(std::vector & dest, bool invert) +{ + readBitmask(dest, features.skillsBytes, features.skillsCount, invert); +} + +void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) +{ + readBitmask(dest, features.skillsBytes, features.skillsCount, invert); +} + +template +void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, const int objectsToRead, bool invert) +{ + for(int byte = 0; byte < bytesToRead; ++byte) + { + const ui8 mask = reader->readUInt8(); + for(int bit = 0; bit < 8; ++bit) + { + if(byte * 8 + bit < objectsToRead) + { + const size_t index = byte * 8 + bit; + const bool flag = mask & (1 << bit); + const bool result = (flag != invert); + + Identifier h3mID(index); + Identifier vcmiID = remapIdentifier(h3mID); + + if (vcmiID.getNum() >= dest.size()) + dest.resize(vcmiID.getNum() + 1); + dest[vcmiID.getNum()] = result; + } + } + } +} + +template +void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) +{ + std::vector bitmap; + bitmap.resize(objectsToRead, false); + readBitmask(bitmap, bytesToRead, objectsToRead, invert); + + for(int i = 0; i < bitmap.size(); i++) + if(bitmap[i]) + dest.insert(static_cast(i)); +} + +int3 MapReaderH3M::readInt3() +{ + int3 p; + p.x = reader->readUInt8(); + p.y = reader->readUInt8(); + p.z = reader->readUInt8(); + return p; +} + +std::shared_ptr MapReaderH3M::readObjectTemplate() +{ + auto tmpl = std::make_shared(); + tmpl->readMap(*reader); + remapper.remapTemplate(*tmpl); + return tmpl; +} + +void MapReaderH3M::skipUnused(size_t amount) +{ + reader->skip(amount); +} + +void MapReaderH3M::skipZero(size_t amount) +{ +#ifdef NDEBUG + skipUnused(amount); +#else + for(size_t i = 0; i < amount; ++i) + { + uint8_t value = reader->readUInt8(); + assert(value == 0); + } +#endif +} + +void MapReaderH3M::readResourses(TResources & resources) +{ + for(int x = 0; x < features.resourcesCount; ++x) + resources[x] = reader->readInt32(); +} + +bool MapReaderH3M::readBool() +{ + uint8_t result = readUInt8(); + assert(result == 0 || result == 1); + + return result != 0; +} + +ui8 MapReaderH3M::readUInt8() +{ + return reader->readUInt8(); +} + +si8 MapReaderH3M::readInt8() +{ + return reader->readInt8(); +} + +ui16 MapReaderH3M::readUInt16() +{ + return reader->readUInt16(); +} + +si16 MapReaderH3M::readInt16() +{ + return reader->readInt16(); +} + +ui32 MapReaderH3M::readUInt32() +{ + return reader->readUInt32(); +} + +si32 MapReaderH3M::readInt32() +{ + return reader->readInt32(); +} + +std::string MapReaderH3M::readBaseString() +{ + return reader->readBaseString(); +} + +CBinaryReader & MapReaderH3M::getInternalReader() +{ + return *reader; +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index c9e90fe80..9c34aba3d 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -1,100 +1,100 @@ -/* - * MapReaderH3M.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 "../GameConstants.h" -#include "../ResourceSet.h" -#include "MapFeaturesH3M.h" -#include "MapIdentifiersH3M.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CBinaryReader; -class CInputStream; -struct MapFormatFeaturesH3M; -class int3; -enum class EMapFormat : uint8_t; - -class MapReaderH3M -{ -public: - explicit MapReaderH3M(CInputStream * stream); - - void setFormatLevel(const MapFormatFeaturesH3M & features); - void setIdentifierRemapper(const MapIdentifiersH3M & remapper); - - ArtifactID readArtifact(); - ArtifactID readArtifact32(); - CreatureID readCreature(); - HeroTypeID readHero(); - HeroTypeID readHeroPortrait(); - TerrainId readTerrain(); - RoadId readRoad(); - RiverId readRiver(); - SecondarySkill readSkill(); - SpellID readSpell(); - SpellID readSpell32(); - PlayerColor readPlayer(); - PlayerColor readPlayer32(); - - void readBitmaskBuildings(std::set & dest, std::optional faction); - void readBitmaskFactions(std::set & dest, bool invert); - void readBitmaskPlayers(std::set & dest, bool invert); - void readBitmaskResources(std::set & dest, bool invert); - void readBitmaskHeroClassesSized(std::set & dest, bool invert); - void readBitmaskHeroes(std::vector & dest, bool invert); - void readBitmaskHeroesSized(std::vector & dest, bool invert); - void readBitmaskArtifacts(std::vector & dest, bool invert); - void readBitmaskArtifactsSized(std::vector & dest, bool invert); - void readBitmaskSpells(std::vector & dest, bool invert); - void readBitmaskSpells(std::set & dest, bool invert); - void readBitmaskSkills(std::vector & dest, bool invert); - void readBitmaskSkills(std::set & dest, bool invert); - - int3 readInt3(); - - std::shared_ptr readObjectTemplate(); - - void skipUnused(size_t amount); - void skipZero(size_t amount); - - void readResourses(TResources & resources); - - bool readBool(); - - ui8 readUInt8(); - si8 readInt8(); - ui16 readUInt16(); - si16 readInt16(); - ui32 readUInt32(); - si32 readInt32(); - - std::string readBaseString(); - - CBinaryReader & getInternalReader(); - -private: - template - Identifier remapIdentifier(const Identifier & identifier); - - template - void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); - - template - void readBitmask(std::vector & dest, int bytesToRead, int objectsToRead, bool invert); - - MapFormatFeaturesH3M features; - MapIdentifiersH3M remapper; - - std::unique_ptr reader; -}; - -VCMI_LIB_NAMESPACE_END +/* + * MapReaderH3M.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 "../GameConstants.h" +#include "../ResourceSet.h" +#include "MapFeaturesH3M.h" +#include "MapIdentifiersH3M.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CBinaryReader; +class CInputStream; +struct MapFormatFeaturesH3M; +class int3; +enum class EMapFormat : uint8_t; + +class MapReaderH3M +{ +public: + explicit MapReaderH3M(CInputStream * stream); + + void setFormatLevel(const MapFormatFeaturesH3M & features); + void setIdentifierRemapper(const MapIdentifiersH3M & remapper); + + ArtifactID readArtifact(); + ArtifactID readArtifact32(); + CreatureID readCreature(); + HeroTypeID readHero(); + HeroTypeID readHeroPortrait(); + TerrainId readTerrain(); + RoadId readRoad(); + RiverId readRiver(); + SecondarySkill readSkill(); + SpellID readSpell(); + SpellID readSpell32(); + PlayerColor readPlayer(); + PlayerColor readPlayer32(); + + void readBitmaskBuildings(std::set & dest, std::optional faction); + void readBitmaskFactions(std::set & dest, bool invert); + void readBitmaskPlayers(std::set & dest, bool invert); + void readBitmaskResources(std::set & dest, bool invert); + void readBitmaskHeroClassesSized(std::set & dest, bool invert); + void readBitmaskHeroes(std::vector & dest, bool invert); + void readBitmaskHeroesSized(std::vector & dest, bool invert); + void readBitmaskArtifacts(std::vector & dest, bool invert); + void readBitmaskArtifactsSized(std::vector & dest, bool invert); + void readBitmaskSpells(std::vector & dest, bool invert); + void readBitmaskSpells(std::set & dest, bool invert); + void readBitmaskSkills(std::vector & dest, bool invert); + void readBitmaskSkills(std::set & dest, bool invert); + + int3 readInt3(); + + std::shared_ptr readObjectTemplate(); + + void skipUnused(size_t amount); + void skipZero(size_t amount); + + void readResourses(TResources & resources); + + bool readBool(); + + ui8 readUInt8(); + si8 readInt8(); + ui16 readUInt16(); + si16 readInt16(); + ui32 readUInt32(); + si32 readInt32(); + + std::string readBaseString(); + + CBinaryReader & getInternalReader(); + +private: + template + Identifier remapIdentifier(const Identifier & identifier); + + template + void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); + + template + void readBitmask(std::vector & dest, int bytesToRead, int objectsToRead, bool invert); + + MapFormatFeaturesH3M features; + MapIdentifiersH3M remapper; + + std::unique_ptr reader; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 1414817f5..620411cd2 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -1,557 +1,557 @@ -/* - * CModHandler.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 "CModHandler.h" - -#include "CModInfo.h" -#include "ModScope.h" -#include "ContentTypeHandler.h" -#include "IdentifierStorage.h" -#include "ModIncompatibility.h" - -#include "../CCreatureHandler.h" -#include "../CGeneralTextHandler.h" -#include "../CStopWatch.h" -#include "../GameSettings.h" -#include "../Languages.h" -#include "../ScriptHandler.h" -#include "../constants/StringConstants.h" -#include "../filesystem/Filesystem.h" -#include "../spells/CSpellHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -static JsonNode loadModSettings(const JsonPath & path) -{ - if (CResourceHandler::get("local")->existsResource(ResourcePath(path))) - { - return JsonNode(path); - } - // Probably new install. Create initial configuration - CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json"); - return JsonNode(); -} - -CModHandler::CModHandler() - : content(std::make_shared()) - , coreMod(std::make_unique()) -{ -} - -CModHandler::~CModHandler() = default; - -// currentList is passed by value to get current list of depending mods -bool CModHandler::hasCircularDependency(const TModID & modID, std::set currentList) const -{ - const CModInfo & mod = allMods.at(modID); - - // Mod already present? We found a loop - if (vstd::contains(currentList, modID)) - { - logMod->error("Error: Circular dependency detected! Printing dependency list:"); - logMod->error("\t%s -> ", mod.getVerificationInfo().name); - return true; - } - - currentList.insert(modID); - - // recursively check every dependency of this mod - for(const TModID & dependency : mod.dependencies) - { - if (hasCircularDependency(dependency, currentList)) - { - logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list - return true; - } - } - return false; -} - -// Returned vector affects the resource loaders call order (see CFilesystemList::load). -// The loaders call order matters when dependent mod overrides resources in its dependencies. -std::vector CModHandler::validateAndSortDependencies(std::vector modsToResolve) const -{ - // Topological sort algorithm. - // TODO: Investigate possible ways to improve performance. - boost::range::sort(modsToResolve); // Sort mods per name - std::vector sortedValidMods; // Vector keeps order of elements (LIFO) - sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation - std::set resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements - - // Mod is resolved if it has not dependencies or all its dependencies are already resolved - auto isResolved = [&](const CModInfo & mod) -> CModInfo::EValidationStatus - { - if(mod.dependencies.size() > resolvedModIDs.size()) - return CModInfo::PENDING; - - for(const TModID & dependency : mod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency)) - return CModInfo::PENDING; - } - return CModInfo::PASSED; - }; - - while(true) - { - std::set resolvedOnCurrentTreeLevel; - for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree - { - if(isResolved(allMods.at(*it)) == CModInfo::PASSED) - { - resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node childs will be resolved on the next iteration - sortedValidMods.push_back(*it); - it = modsToResolve.erase(it); - continue; - } - it++; - } - if(!resolvedOnCurrentTreeLevel.empty()) - { - resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); - continue; - } - // If there're no valid mods on the current mods tree level, no more mod can be resolved, should be end. - break; - } - - // Left mods have unresolved dependencies, output all to log. - for(const auto & brokenModID : modsToResolve) - { - const CModInfo & brokenMod = allMods.at(brokenModID); - for(const TModID & dependency : brokenMod.dependencies) - { - if(!vstd::contains(resolvedModIDs, dependency)) - logMod->error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.getVerificationInfo().name, dependency); - } - } - return sortedValidMods; -} - -std::vector CModHandler::getModList(const std::string & path) const -{ - std::string modDir = boost::to_upper_copy(path + "MODS/"); - size_t depth = boost::range::count(modDir, '/'); - - auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool - { - if (id.getType() != EResType::DIRECTORY) - return false; - if (!boost::algorithm::starts_with(id.getName(), modDir)) - return false; - if (boost::range::count(id.getName(), '/') != depth ) - return false; - return true; - }); - - //storage for found mods - std::vector foundMods; - for(const auto & entry : list) - { - std::string name = entry.getName(); - name.erase(0, modDir.size()); //Remove path prefix - - if (!name.empty()) - foundMods.push_back(name); - } - return foundMods; -} - - - -void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - for(const std::string & modName : getModList(path)) - loadOneMod(modName, parent, modSettings, enableMods); -} - -void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods) -{ - boost::to_lower(modName); - std::string modFullName = parent.empty() ? modName : parent + '.' + modName; - - if ( ModScope::isScopeReserved(modFullName)) - { - logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); - return; - } - - if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) - { - CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); - if (!parent.empty()) // this is submod, add parent to dependencies - mod.dependencies.insert(parent); - - allMods[modFullName] = mod; - if (mod.isEnabled() && enableMods) - activeMods.push_back(modFullName); - - loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], enableMods && mod.isEnabled()); - } -} - -void CModHandler::loadMods(bool onlyEssential) -{ - JsonNode modConfig; - - if(onlyEssential) - { - loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods - } - else - { - modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); - loadMods("", "", modConfig["activeMods"], true); - } - - coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); -} - -std::vector CModHandler::getAllMods() -{ - std::vector modlist; - modlist.reserve(allMods.size()); - for (auto & entry : allMods) - modlist.push_back(entry.first); - return modlist; -} - -std::vector CModHandler::getActiveMods() -{ - return activeMods; -} - -const CModInfo & CModHandler::getModInfo(const TModID & modId) const -{ - return allMods.at(modId); -} - -static JsonNode genDefaultFS() -{ - // default FS config for mods: directory "Content" that acts as H3 root directory - JsonNode defaultFS; - defaultFS[""].Vector().resize(2); - defaultFS[""].Vector()[0]["type"].String() = "zip"; - defaultFS[""].Vector()[0]["path"].String() = "/Content.zip"; - defaultFS[""].Vector()[1]["type"].String() = "dir"; - defaultFS[""].Vector()[1]["path"].String() = "/Content"; - return defaultFS; -} - -static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf) -{ - static const JsonNode defaultFS = genDefaultFS(); - - if (!conf["filesystem"].isNull()) - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]); - else - return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS); -} - -static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem) -{ - boost::crc_32_type modChecksum; - // first - add current VCMI version into checksum to force re-validation on VCMI updates - modChecksum.process_bytes(reinterpret_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); - - // second - add mod.json into checksum because filesystem does not contains this file - // FIXME: remove workaround for core mod - if (modName != ModScope::scopeBuiltin()) - { - auto modConfFile = CModInfo::getModFile(modName); - ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); - } - // third - add all detected text files from this mod into checksum - auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) - { - return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) && - ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); - }); - - for (const ResourcePath & file : files) - { - ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); - modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); - } - return modChecksum.checksum(); -} - -void CModHandler::loadModFilesystems() -{ - CGeneralTextHandler::detectInstallParameters(); - - activeMods = validateAndSortDependencies(activeMods); - - coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); - - for(std::string & modName : activeMods) - { - CModInfo & mod = allMods[modName]; - CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); - } -} - -TModID CModHandler::findResourceOrigin(const ResourcePath & name) -{ - for(const auto & modID : boost::adaptors::reverse(activeMods)) - { - if(CResourceHandler::get(modID)->existsResource(name)) - return modID; - } - - if(CResourceHandler::get("core")->existsResource(name)) - return "core"; - - if(CResourceHandler::get("mapEditor")->existsResource(name)) - return "core"; // Workaround for loading maps via map editor - - assert(0); - return ""; -} - -std::string CModHandler::getModLanguage(const TModID& modId) const -{ - if(modId == "core") - return VLC->generaltexth->getInstalledLanguage(); - if(modId == "map") - return VLC->generaltexth->getPreferredLanguage(); - return allMods.at(modId).baseLanguage; -} - -std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const -{ - auto it = allMods.find(modId); - isModFound = (it != allMods.end()); - - if(isModFound) - return it->second.dependencies; - - logMod->error("Mod not found: '%s'", modId); - return {}; -} - -void CModHandler::initializeConfig() -{ - VLC->settingsHandler->load(coreMod->config["settings"]); - - for(const TModID & modName : activeMods) - { - const auto & mod = allMods[modName]; - if (!mod.config["settings"].isNull()) - VLC->settingsHandler->load(mod.config["settings"]); - } -} - -CModVersion CModHandler::getModVersion(TModID modName) const -{ - if (allMods.count(modName)) - return allMods.at(modName).getVerificationInfo().version; - return {}; -} - -bool CModHandler::validateTranslations(TModID modName) const -{ - bool result = true; - const auto & mod = allMods.at(modName); - - { - auto fileList = mod.config["translations"].convertTo >(); - JsonNode json = JsonUtils::assembleFromFiles(fileList); - result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json); - } - - for(const auto & language : Languages::getLanguageList()) - { - if (!language.hasTranslation) - continue; - - if (mod.config[language.identifier].isNull()) - continue; - - if (mod.config[language.identifier]["skipValidation"].Bool()) - continue; - - auto fileList = mod.config[language.identifier]["translations"].convertTo >(); - JsonNode json = JsonUtils::assembleFromFiles(fileList); - result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json); - } - - return result; -} - -void CModHandler::loadTranslation(const TModID & modName) -{ - const auto & mod = allMods[modName]; - - std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); - std::string modBaseLanguage = allMods[modName].baseLanguage; - - auto baseTranslationList = mod.config["translations"].convertTo >(); - auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo >(); - - JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList); - JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList); - - VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation); - VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation); -} - -void CModHandler::load() -{ - CStopWatch totalTime; - CStopWatch timer; - - logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); - - content->init(); - - for(const TModID & modName : activeMods) - { - logMod->trace("Generating checksum for %s", modName); - allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName))); - } - - // first - load virtual builtin mod that contains all data - // TODO? move all data into real mods? RoE, AB, SoD, WoG - content->preloadData(*coreMod); - for(const TModID & modName : activeMods) - content->preloadData(allMods[modName]); - logMod->info("\tParsing mod data: %d ms", timer.getDiff()); - - content->load(*coreMod); - for(const TModID & modName : activeMods) - content->load(allMods[modName]); - -#if SCRIPTING_ENABLED - VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load -#endif - - content->loadCustom(); - - for(const TModID & modName : activeMods) - loadTranslation(modName); - - for(const TModID & modName : activeMods) - if (!validateTranslations(modName)) - allMods[modName].validation = CModInfo::FAILED; - - logMod->info("\tLoading mod data: %d ms", timer.getDiff()); - VLC->creh->loadCrExpMod(); - VLC->identifiersHandler->finalize(); - logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); - - content->afterLoadFinalization(); - logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); - logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); -} - -void CModHandler::afterLoad(bool onlyEssential) -{ - JsonNode modSettings; - for (auto & modEntry : allMods) - { - std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); - - modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); - } - modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); - modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; - - if(!onlyEssential) - { - std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); - file << modSettings.toJson(); - } - -} - -void CModHandler::trySetActiveMods(const std::vector> & modList) -{ - auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* - { - for(auto & i : modList) - if(i.first == m) - return &i.second; - return nullptr; - }; - - std::vector missingMods, excessiveMods; - ModIncompatibility::ModListWithVersion missingModsResult; - ModIncompatibility::ModList excessiveModsResult; - - for(const auto & m : activeMods) - { - if(searchVerificationInfo(m)) - continue; - - //TODO: support actual disabling of these mods - if(getModInfo(m).checkModGameplayAffecting()) - excessiveMods.push_back(m); - } - - for(const auto & infoPair : modList) - { - auto & remoteModId = infoPair.first; - auto & remoteModInfo = infoPair.second; - - bool modAffectsGameplay = remoteModInfo.impactsGameplay; - //parent mod affects gameplay if child affects too - for(const auto & subInfoPair : modList) - modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); - - if(!allMods.count(remoteModId)) - { - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //mod is not installed - continue; - } - - auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); - bool modVersionCompatible = localModInfo.version.isNull() - || remoteModInfo.version.isNull() - || localModInfo.version.compatible(remoteModInfo.version); - bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); - - if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) - continue; - - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //incompatible mod impacts gameplay - } - - //filter mods - for(auto & m : missingMods) - { - if(auto * vInfo = searchVerificationInfo(m)) - { - assert(vInfo->parent != m); - if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) - continue; - missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); - } - } - for(auto & m : excessiveMods) - { - auto & vInfo = getModInfo(m).getVerificationInfo(); - assert(vInfo.parent != m); - if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) - continue; - excessiveModsResult.push_back(vInfo.name); - } - - if(!missingModsResult.empty() || !excessiveModsResult.empty()) - throw ModIncompatibility(missingModsResult, excessiveModsResult); - - //TODO: support actual enabling of required mods -} - -VCMI_LIB_NAMESPACE_END +/* + * CModHandler.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 "CModHandler.h" + +#include "CModInfo.h" +#include "ModScope.h" +#include "ContentTypeHandler.h" +#include "IdentifierStorage.h" +#include "ModIncompatibility.h" + +#include "../CCreatureHandler.h" +#include "../CGeneralTextHandler.h" +#include "../CStopWatch.h" +#include "../GameSettings.h" +#include "../Languages.h" +#include "../ScriptHandler.h" +#include "../constants/StringConstants.h" +#include "../filesystem/Filesystem.h" +#include "../spells/CSpellHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +static JsonNode loadModSettings(const JsonPath & path) +{ + if (CResourceHandler::get("local")->existsResource(ResourcePath(path))) + { + return JsonNode(path); + } + // Probably new install. Create initial configuration + CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json"); + return JsonNode(); +} + +CModHandler::CModHandler() + : content(std::make_shared()) + , coreMod(std::make_unique()) +{ +} + +CModHandler::~CModHandler() = default; + +// currentList is passed by value to get current list of depending mods +bool CModHandler::hasCircularDependency(const TModID & modID, std::set currentList) const +{ + const CModInfo & mod = allMods.at(modID); + + // Mod already present? We found a loop + if (vstd::contains(currentList, modID)) + { + logMod->error("Error: Circular dependency detected! Printing dependency list:"); + logMod->error("\t%s -> ", mod.getVerificationInfo().name); + return true; + } + + currentList.insert(modID); + + // recursively check every dependency of this mod + for(const TModID & dependency : mod.dependencies) + { + if (hasCircularDependency(dependency, currentList)) + { + logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list + return true; + } + } + return false; +} + +// Returned vector affects the resource loaders call order (see CFilesystemList::load). +// The loaders call order matters when dependent mod overrides resources in its dependencies. +std::vector CModHandler::validateAndSortDependencies(std::vector modsToResolve) const +{ + // Topological sort algorithm. + // TODO: Investigate possible ways to improve performance. + boost::range::sort(modsToResolve); // Sort mods per name + std::vector sortedValidMods; // Vector keeps order of elements (LIFO) + sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation + std::set resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements + + // Mod is resolved if it has not dependencies or all its dependencies are already resolved + auto isResolved = [&](const CModInfo & mod) -> CModInfo::EValidationStatus + { + if(mod.dependencies.size() > resolvedModIDs.size()) + return CModInfo::PENDING; + + for(const TModID & dependency : mod.dependencies) + { + if(!vstd::contains(resolvedModIDs, dependency)) + return CModInfo::PENDING; + } + return CModInfo::PASSED; + }; + + while(true) + { + std::set resolvedOnCurrentTreeLevel; + for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree + { + if(isResolved(allMods.at(*it)) == CModInfo::PASSED) + { + resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node childs will be resolved on the next iteration + sortedValidMods.push_back(*it); + it = modsToResolve.erase(it); + continue; + } + it++; + } + if(!resolvedOnCurrentTreeLevel.empty()) + { + resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end()); + continue; + } + // If there're no valid mods on the current mods tree level, no more mod can be resolved, should be end. + break; + } + + // Left mods have unresolved dependencies, output all to log. + for(const auto & brokenModID : modsToResolve) + { + const CModInfo & brokenMod = allMods.at(brokenModID); + for(const TModID & dependency : brokenMod.dependencies) + { + if(!vstd::contains(resolvedModIDs, dependency)) + logMod->error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.getVerificationInfo().name, dependency); + } + } + return sortedValidMods; +} + +std::vector CModHandler::getModList(const std::string & path) const +{ + std::string modDir = boost::to_upper_copy(path + "MODS/"); + size_t depth = boost::range::count(modDir, '/'); + + auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool + { + if (id.getType() != EResType::DIRECTORY) + return false; + if (!boost::algorithm::starts_with(id.getName(), modDir)) + return false; + if (boost::range::count(id.getName(), '/') != depth ) + return false; + return true; + }); + + //storage for found mods + std::vector foundMods; + for(const auto & entry : list) + { + std::string name = entry.getName(); + name.erase(0, modDir.size()); //Remove path prefix + + if (!name.empty()) + foundMods.push_back(name); + } + return foundMods; +} + + + +void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods) +{ + for(const std::string & modName : getModList(path)) + loadOneMod(modName, parent, modSettings, enableMods); +} + +void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods) +{ + boost::to_lower(modName); + std::string modFullName = parent.empty() ? modName : parent + '.' + modName; + + if ( ModScope::isScopeReserved(modFullName)) + { + logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName); + return; + } + + if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName))) + { + CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName))); + if (!parent.empty()) // this is submod, add parent to dependencies + mod.dependencies.insert(parent); + + allMods[modFullName] = mod; + if (mod.isEnabled() && enableMods) + activeMods.push_back(modFullName); + + loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], enableMods && mod.isEnabled()); + } +} + +void CModHandler::loadMods(bool onlyEssential) +{ + JsonNode modConfig; + + if(onlyEssential) + { + loadOneMod("vcmi", "", modConfig, true);//only vcmi and submods + } + else + { + modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json")); + loadMods("", "", modConfig["activeMods"], true); + } + + coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); +} + +std::vector CModHandler::getAllMods() +{ + std::vector modlist; + modlist.reserve(allMods.size()); + for (auto & entry : allMods) + modlist.push_back(entry.first); + return modlist; +} + +std::vector CModHandler::getActiveMods() +{ + return activeMods; +} + +const CModInfo & CModHandler::getModInfo(const TModID & modId) const +{ + return allMods.at(modId); +} + +static JsonNode genDefaultFS() +{ + // default FS config for mods: directory "Content" that acts as H3 root directory + JsonNode defaultFS; + defaultFS[""].Vector().resize(2); + defaultFS[""].Vector()[0]["type"].String() = "zip"; + defaultFS[""].Vector()[0]["path"].String() = "/Content.zip"; + defaultFS[""].Vector()[1]["type"].String() = "dir"; + defaultFS[""].Vector()[1]["path"].String() = "/Content"; + return defaultFS; +} + +static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf) +{ + static const JsonNode defaultFS = genDefaultFS(); + + if (!conf["filesystem"].isNull()) + return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]); + else + return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS); +} + +static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem) +{ + boost::crc_32_type modChecksum; + // first - add current VCMI version into checksum to force re-validation on VCMI updates + modChecksum.process_bytes(reinterpret_cast(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size()); + + // second - add mod.json into checksum because filesystem does not contains this file + // FIXME: remove workaround for core mod + if (modName != ModScope::scopeBuiltin()) + { + auto modConfFile = CModInfo::getModFile(modName); + ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32(); + modChecksum.process_bytes(reinterpret_cast(&configChecksum), sizeof(configChecksum)); + } + // third - add all detected text files from this mod into checksum + auto files = filesystem->getFilteredFiles([](const ResourcePath & resID) + { + return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) && + ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG")); + }); + + for (const ResourcePath & file : files) + { + ui32 fileChecksum = filesystem->load(file)->calculateCRC32(); + modChecksum.process_bytes(reinterpret_cast(&fileChecksum), sizeof(fileChecksum)); + } + return modChecksum.checksum(); +} + +void CModHandler::loadModFilesystems() +{ + CGeneralTextHandler::detectInstallParameters(); + + activeMods = validateAndSortDependencies(activeMods); + + coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); + + for(std::string & modName : activeMods) + { + CModInfo & mod = allMods[modName]; + CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); + } +} + +TModID CModHandler::findResourceOrigin(const ResourcePath & name) +{ + for(const auto & modID : boost::adaptors::reverse(activeMods)) + { + if(CResourceHandler::get(modID)->existsResource(name)) + return modID; + } + + if(CResourceHandler::get("core")->existsResource(name)) + return "core"; + + if(CResourceHandler::get("mapEditor")->existsResource(name)) + return "core"; // Workaround for loading maps via map editor + + assert(0); + return ""; +} + +std::string CModHandler::getModLanguage(const TModID& modId) const +{ + if(modId == "core") + return VLC->generaltexth->getInstalledLanguage(); + if(modId == "map") + return VLC->generaltexth->getPreferredLanguage(); + return allMods.at(modId).baseLanguage; +} + +std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const +{ + auto it = allMods.find(modId); + isModFound = (it != allMods.end()); + + if(isModFound) + return it->second.dependencies; + + logMod->error("Mod not found: '%s'", modId); + return {}; +} + +void CModHandler::initializeConfig() +{ + VLC->settingsHandler->load(coreMod->config["settings"]); + + for(const TModID & modName : activeMods) + { + const auto & mod = allMods[modName]; + if (!mod.config["settings"].isNull()) + VLC->settingsHandler->load(mod.config["settings"]); + } +} + +CModVersion CModHandler::getModVersion(TModID modName) const +{ + if (allMods.count(modName)) + return allMods.at(modName).getVerificationInfo().version; + return {}; +} + +bool CModHandler::validateTranslations(TModID modName) const +{ + bool result = true; + const auto & mod = allMods.at(modName); + + { + auto fileList = mod.config["translations"].convertTo >(); + JsonNode json = JsonUtils::assembleFromFiles(fileList); + result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json); + } + + for(const auto & language : Languages::getLanguageList()) + { + if (!language.hasTranslation) + continue; + + if (mod.config[language.identifier].isNull()) + continue; + + if (mod.config[language.identifier]["skipValidation"].Bool()) + continue; + + auto fileList = mod.config[language.identifier]["translations"].convertTo >(); + JsonNode json = JsonUtils::assembleFromFiles(fileList); + result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json); + } + + return result; +} + +void CModHandler::loadTranslation(const TModID & modName) +{ + const auto & mod = allMods[modName]; + + std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage(); + std::string modBaseLanguage = allMods[modName].baseLanguage; + + auto baseTranslationList = mod.config["translations"].convertTo >(); + auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo >(); + + JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList); + JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList); + + VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation); + VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation); +} + +void CModHandler::load() +{ + CStopWatch totalTime; + CStopWatch timer; + + logMod->info("\tInitializing content handler: %d ms", timer.getDiff()); + + content->init(); + + for(const TModID & modName : activeMods) + { + logMod->trace("Generating checksum for %s", modName); + allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName))); + } + + // first - load virtual builtin mod that contains all data + // TODO? move all data into real mods? RoE, AB, SoD, WoG + content->preloadData(*coreMod); + for(const TModID & modName : activeMods) + content->preloadData(allMods[modName]); + logMod->info("\tParsing mod data: %d ms", timer.getDiff()); + + content->load(*coreMod); + for(const TModID & modName : activeMods) + content->load(allMods[modName]); + +#if SCRIPTING_ENABLED + VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load +#endif + + content->loadCustom(); + + for(const TModID & modName : activeMods) + loadTranslation(modName); + + for(const TModID & modName : activeMods) + if (!validateTranslations(modName)) + allMods[modName].validation = CModInfo::FAILED; + + logMod->info("\tLoading mod data: %d ms", timer.getDiff()); + VLC->creh->loadCrExpMod(); + VLC->identifiersHandler->finalize(); + logMod->info("\tResolving identifiers: %d ms", timer.getDiff()); + + content->afterLoadFinalization(); + logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff()); + logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff()); +} + +void CModHandler::afterLoad(bool onlyEssential) +{ + JsonNode modSettings; + for (auto & modEntry : allMods) + { + std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/"); + + modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); + } + modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); + modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; + + if(!onlyEssential) + { + std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); + file << modSettings.toJson(); + } + +} + +void CModHandler::trySetActiveMods(const std::vector> & modList) +{ + auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* + { + for(auto & i : modList) + if(i.first == m) + return &i.second; + return nullptr; + }; + + std::vector missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + + for(const auto & m : activeMods) + { + if(searchVerificationInfo(m)) + continue; + + //TODO: support actual disabling of these mods + if(getModInfo(m).checkModGameplayAffecting()) + excessiveMods.push_back(m); + } + + for(const auto & infoPair : modList) + { + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + + if(!allMods.count(remoteModId)) + { + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed + continue; + } + + auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); + modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + continue; + + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //incompatible mod impacts gameplay + } + + //filter mods + for(auto & m : missingMods) + { + if(auto * vInfo = searchVerificationInfo(m)) + { + assert(vInfo->parent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + for(auto & m : excessiveMods) + { + auto & vInfo = getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } + + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); + + //TODO: support actual enabling of required mods +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 2062909bf..af0959fe9 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -1,110 +1,110 @@ -/* - * CModHandler.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 "CModInfo.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class CModHandler; -class CModIndentifier; -class JsonNode; -class IHandlerBase; -class CIdentifierStorage; -class CContentHandler; -class ResourcePath; - -using TModID = std::string; - -class DLL_LINKAGE CModHandler : boost::noncopyable -{ - std::map allMods; - std::vector activeMods;//active mods, in order in which they were loaded - std::unique_ptr coreMod; - - bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; - - /** - * 1. Set apart mods with resolved dependencies from mods which have unresolved dependencies - * 2. Sort resolved mods using topological algorithm - * 3. Log all problem mods and their unresolved dependencies - * - * @param modsToResolve list of valid mod IDs (checkDependencies returned true - TODO: Clarify it.) - * @return a vector of the topologically sorted resolved mods: child nodes (dependent mods) have greater index than parents - */ - std::vector validateAndSortDependencies(std::vector modsToResolve) const; - - std::vector getModList(const std::string & path) const; - void loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods); - void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods); - void loadTranslation(const TModID & modName); - - bool validateTranslations(TModID modName) const; - - CModVersion getModVersion(TModID modName) const; - - /// Attempt to set active mods according to provided list of mods from save, throws on failure - void trySetActiveMods(const std::vector> & modList); - -public: - std::shared_ptr content; //(!)Do not serialize FIXME: make private - - /// receives list of available mods and trying to load mod.json from all of them - void initializeConfig(); - void loadMods(bool onlyEssential = false); - void loadModFilesystems(); - - /// returns ID of mod that provides selected file resource - TModID findResourceOrigin(const ResourcePath & name); - - std::string getModLanguage(const TModID & modId) const; - - std::set getModDependencies(const TModID & modId, bool & isModFound) const; - - /// returns list of all (active) mods - std::vector getAllMods(); - std::vector getActiveMods(); - - const CModInfo & getModInfo(const TModID & modId) const; - - /// load content from all available mods - void load(); - void afterLoad(bool onlyEssential); - - CModHandler(); - virtual ~CModHandler(); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - h & activeMods; - for(const auto & m : activeMods) - h & getModInfo(m).getVerificationInfo(); - } - else - { - loadMods(); - std::vector saveActiveMods; - h & saveActiveMods; - - std::vector> saveModInfos(saveActiveMods.size()); - for(int i = 0; i < saveActiveMods.size(); ++i) - { - saveModInfos[i].first = saveActiveMods[i]; - h & saveModInfos[i].second; - } - - trySetActiveMods(saveModInfos); - } - } -}; - -VCMI_LIB_NAMESPACE_END +/* + * CModHandler.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 "CModInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CModHandler; +class CModIndentifier; +class JsonNode; +class IHandlerBase; +class CIdentifierStorage; +class CContentHandler; +class ResourcePath; + +using TModID = std::string; + +class DLL_LINKAGE CModHandler : boost::noncopyable +{ + std::map allMods; + std::vector activeMods;//active mods, in order in which they were loaded + std::unique_ptr coreMod; + + bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; + + /** + * 1. Set apart mods with resolved dependencies from mods which have unresolved dependencies + * 2. Sort resolved mods using topological algorithm + * 3. Log all problem mods and their unresolved dependencies + * + * @param modsToResolve list of valid mod IDs (checkDependencies returned true - TODO: Clarify it.) + * @return a vector of the topologically sorted resolved mods: child nodes (dependent mods) have greater index than parents + */ + std::vector validateAndSortDependencies(std::vector modsToResolve) const; + + std::vector getModList(const std::string & path) const; + void loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods); + void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods); + void loadTranslation(const TModID & modName); + + bool validateTranslations(TModID modName) const; + + CModVersion getModVersion(TModID modName) const; + + /// Attempt to set active mods according to provided list of mods from save, throws on failure + void trySetActiveMods(const std::vector> & modList); + +public: + std::shared_ptr content; //(!)Do not serialize FIXME: make private + + /// receives list of available mods and trying to load mod.json from all of them + void initializeConfig(); + void loadMods(bool onlyEssential = false); + void loadModFilesystems(); + + /// returns ID of mod that provides selected file resource + TModID findResourceOrigin(const ResourcePath & name); + + std::string getModLanguage(const TModID & modId) const; + + std::set getModDependencies(const TModID & modId, bool & isModFound) const; + + /// returns list of all (active) mods + std::vector getAllMods(); + std::vector getActiveMods(); + + const CModInfo & getModInfo(const TModID & modId) const; + + /// load content from all available mods + void load(); + void afterLoad(bool onlyEssential); + + CModHandler(); + virtual ~CModHandler(); + + template void serialize(Handler &h, const int version) + { + if(h.saving) + { + h & activeMods; + for(const auto & m : activeMods) + h & getModInfo(m).getVerificationInfo(); + } + else + { + loadMods(); + std::vector saveActiveMods; + h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } + + trySetActiveMods(saveModInfos); + } + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp index 73c1dc5ff..9408fa7ff 100644 --- a/lib/registerTypes/RegisterTypes.cpp +++ b/lib/registerTypes/RegisterTypes.cpp @@ -1,52 +1,52 @@ -/* - * RegisterTypes.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" -#define INSTANTIATE_REGISTER_TYPES_HERE -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../spells/CSpellHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -// For reference: peak memory usage by gcc during compilation of register type templates -// registerTypesMapObjects: 1.9 Gb -// registerTypes2: 2.2 Gb -// registerTypesClientPacks1 1.6 Gb -// registerTypesClientPacks2 1.6 Gb -// registerTypesServerPacks: 1.3 Gb -// registerTypes4: 1.3 Gb - - -#define DEFINE_EXTERNAL_METHOD(METHODNAME) \ -extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ -extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); \ -extern template DLL_LINKAGE void METHODNAME(CTypeList & s); \ - -//DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) -DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) -DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) - -template void registerTypes(BinaryDeserializer & s); -template void registerTypes(BinarySerializer & s); -template void registerTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END +/* + * RegisterTypes.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" +#define INSTANTIATE_REGISTER_TYPES_HERE +#include "RegisterTypes.h" + +#include "../mapping/CMapInfo.h" +#include "../StartInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../spells/CSpellHandler.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + +// For reference: peak memory usage by gcc during compilation of register type templates +// registerTypesMapObjects: 1.9 Gb +// registerTypes2: 2.2 Gb +// registerTypesClientPacks1 1.6 Gb +// registerTypesClientPacks2 1.6 Gb +// registerTypesServerPacks: 1.3 Gb +// registerTypes4: 1.3 Gb + + +#define DEFINE_EXTERNAL_METHOD(METHODNAME) \ +extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ +extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); \ +extern template DLL_LINKAGE void METHODNAME(CTypeList & s); \ + +//DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) +DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) +DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) +DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) +DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) +DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) +DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) + +template void registerTypes(BinaryDeserializer & s); +template void registerTypes(BinarySerializer & s); +template void registerTypes(CTypeList & s); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index cde5b7c44..5f2ef3622 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -1,430 +1,430 @@ -/* - * RegisterTypes.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 "../NetPacks.h" -#include "../NetPacksLobby.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CCreatureSet.h" -#include "../CPlayerState.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../mapObjectConstructors/CRewardableConstructor.h" -#include "../mapObjectConstructors/CommonConstructors.h" -#include "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjectConstructors/DwellingInstanceConstructor.h" -#include "../mapObjectConstructors/HillFortInstanceConstructor.h" -#include "../mapObjectConstructors/ShipyardInstanceConstructor.h" -#include "../mapObjectConstructors/ShrineInstanceConstructor.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGTownBuilding.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../battle/CObstacleInstance.h" -#include "../bonuses/CBonusSystemNode.h" -#include "../bonuses/Limiters.h" -#include "../bonuses/Updaters.h" -#include "../bonuses/Propagators.h" -#include "../CStack.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class BinarySerializer; -class BinaryDeserializer; -class CTypeList; - -template -void registerTypesMapObjects1(Serializer &s) -{ - ////////////////////////////////////////////////////////////////////////// - // Adventure map objects - ////////////////////////////////////////////////////////////////////////// - s.template registerType(); - - // Non-armed objects - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); s.template registerType(); s.template registerType(); - - // Armed objects - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); -} - -template -void registerTypesMapObjectTypes(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -#define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() - - REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CCartographer); - REGISTER_GENERIC_HANDLER(CGArtifact); - REGISTER_GENERIC_HANDLER(CGBlackMarket); - REGISTER_GENERIC_HANDLER(CGBoat); - REGISTER_GENERIC_HANDLER(CGBorderGate); - REGISTER_GENERIC_HANDLER(CGBorderGuard); - REGISTER_GENERIC_HANDLER(CGCreature); - REGISTER_GENERIC_HANDLER(CGDenOfthieves); - REGISTER_GENERIC_HANDLER(CGDwelling); - REGISTER_GENERIC_HANDLER(CGEvent); - REGISTER_GENERIC_HANDLER(CGGarrison); - REGISTER_GENERIC_HANDLER(CGHeroPlaceholder); - REGISTER_GENERIC_HANDLER(CGHeroInstance); - REGISTER_GENERIC_HANDLER(CGKeymasterTent); - REGISTER_GENERIC_HANDLER(CGLighthouse); - REGISTER_GENERIC_HANDLER(CGTerrainPatch); - REGISTER_GENERIC_HANDLER(CGMagi); - REGISTER_GENERIC_HANDLER(CGMarket); - REGISTER_GENERIC_HANDLER(CGMine); - REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGObservatory); - REGISTER_GENERIC_HANDLER(CGPandoraBox); - REGISTER_GENERIC_HANDLER(CGQuestGuard); - REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGScholar); - REGISTER_GENERIC_HANDLER(CGSeerHut); - REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGShrine); - REGISTER_GENERIC_HANDLER(CGSignBottle); - REGISTER_GENERIC_HANDLER(CGSirens); - REGISTER_GENERIC_HANDLER(CGMonolith); - REGISTER_GENERIC_HANDLER(CGSubterraneanGate); - REGISTER_GENERIC_HANDLER(CGWhirlpool); - REGISTER_GENERIC_HANDLER(CGTownInstance); - REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(CGWitchHut); - REGISTER_GENERIC_HANDLER(HillFort); - -#undef REGISTER_GENERIC_HANDLER - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - //new types (other than netpacks) must register here - //order of type registration is critical for loading old savegames -} - -template -void registerTypesMapObjects2(Serializer &s) -{ - //Other object-related - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - //s.template registerType(); - //s.template registerType(); - - //end of objects - - ////////////////////////////////////////////////////////////////////////// - // Bonus system - ////////////////////////////////////////////////////////////////////////// - //s.template registerType(); - s.template registerType(); - - // Limiters - //s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -// s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); //TODO - //s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); - s.template registerType(); - - //s.template registerType(); - s.template registerType(); -} -template -void registerTypesClientPacks1(Serializer &s) -{ - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesClientPacks2(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesServerPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesLobbyPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - - // Any client can sent - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only host client send - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only server send - s.template registerType(); - s.template registerType(); - - // For client with permissions - s.template registerType(); - // Only for host client - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypes(Serializer &s) -{ - registerTypesMapObjects1(s); - registerTypesMapObjects2(s); - registerTypesMapObjectTypes(s); - registerTypesClientPacks1(s); - registerTypesClientPacks2(s); - registerTypesServerPacks(s); - registerTypesLobbyPacks(s); -} - -#ifndef INSTANTIATE_REGISTER_TYPES_HERE - -extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); -extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); -extern template DLL_LINKAGE void registerTypes(CTypeList & s); - -#endif - - -VCMI_LIB_NAMESPACE_END +/* + * RegisterTypes.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 "../NetPacks.h" +#include "../NetPacksLobby.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CCreatureSet.h" +#include "../CPlayerState.h" +#include "../CHeroHandler.h" +#include "../CTownHandler.h" +#include "../mapObjectConstructors/CRewardableConstructor.h" +#include "../mapObjectConstructors/CommonConstructors.h" +#include "../mapObjectConstructors/CBankInstanceConstructor.h" +#include "../mapObjectConstructors/DwellingInstanceConstructor.h" +#include "../mapObjectConstructors/HillFortInstanceConstructor.h" +#include "../mapObjectConstructors/ShipyardInstanceConstructor.h" +#include "../mapObjectConstructors/ShrineInstanceConstructor.h" +#include "../mapObjects/MapObjects.h" +#include "../mapObjects/CGCreature.h" +#include "../mapObjects/CGTownBuilding.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../battle/CObstacleInstance.h" +#include "../bonuses/CBonusSystemNode.h" +#include "../bonuses/Limiters.h" +#include "../bonuses/Updaters.h" +#include "../bonuses/Propagators.h" +#include "../CStack.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BinarySerializer; +class BinaryDeserializer; +class CTypeList; + +template +void registerTypesMapObjects1(Serializer &s) +{ + ////////////////////////////////////////////////////////////////////////// + // Adventure map objects + ////////////////////////////////////////////////////////////////////////// + s.template registerType(); + + // Non-armed objects + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); s.template registerType(); s.template registerType(); + + // Armed objects + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); +} + +template +void registerTypesMapObjectTypes(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + +#define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() + + REGISTER_GENERIC_HANDLER(CGObjectInstance); + REGISTER_GENERIC_HANDLER(CCartographer); + REGISTER_GENERIC_HANDLER(CGArtifact); + REGISTER_GENERIC_HANDLER(CGBlackMarket); + REGISTER_GENERIC_HANDLER(CGBoat); + REGISTER_GENERIC_HANDLER(CGBorderGate); + REGISTER_GENERIC_HANDLER(CGBorderGuard); + REGISTER_GENERIC_HANDLER(CGCreature); + REGISTER_GENERIC_HANDLER(CGDenOfthieves); + REGISTER_GENERIC_HANDLER(CGDwelling); + REGISTER_GENERIC_HANDLER(CGEvent); + REGISTER_GENERIC_HANDLER(CGGarrison); + REGISTER_GENERIC_HANDLER(CGHeroPlaceholder); + REGISTER_GENERIC_HANDLER(CGHeroInstance); + REGISTER_GENERIC_HANDLER(CGKeymasterTent); + REGISTER_GENERIC_HANDLER(CGLighthouse); + REGISTER_GENERIC_HANDLER(CGTerrainPatch); + REGISTER_GENERIC_HANDLER(CGMagi); + REGISTER_GENERIC_HANDLER(CGMarket); + REGISTER_GENERIC_HANDLER(CGMine); + REGISTER_GENERIC_HANDLER(CGObelisk); + REGISTER_GENERIC_HANDLER(CGObservatory); + REGISTER_GENERIC_HANDLER(CGPandoraBox); + REGISTER_GENERIC_HANDLER(CGQuestGuard); + REGISTER_GENERIC_HANDLER(CGResource); + REGISTER_GENERIC_HANDLER(CGScholar); + REGISTER_GENERIC_HANDLER(CGSeerHut); + REGISTER_GENERIC_HANDLER(CGShipyard); + REGISTER_GENERIC_HANDLER(CGShrine); + REGISTER_GENERIC_HANDLER(CGSignBottle); + REGISTER_GENERIC_HANDLER(CGSirens); + REGISTER_GENERIC_HANDLER(CGMonolith); + REGISTER_GENERIC_HANDLER(CGSubterraneanGate); + REGISTER_GENERIC_HANDLER(CGWhirlpool); + REGISTER_GENERIC_HANDLER(CGTownInstance); + REGISTER_GENERIC_HANDLER(CGUniversity); + REGISTER_GENERIC_HANDLER(CGWitchHut); + REGISTER_GENERIC_HANDLER(HillFort); + +#undef REGISTER_GENERIC_HANDLER + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + //new types (other than netpacks) must register here + //order of type registration is critical for loading old savegames +} + +template +void registerTypesMapObjects2(Serializer &s) +{ + //Other object-related + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + //s.template registerType(); + //s.template registerType(); + + //end of objects + + ////////////////////////////////////////////////////////////////////////// + // Bonus system + ////////////////////////////////////////////////////////////////////////// + //s.template registerType(); + s.template registerType(); + + // Limiters + //s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + +// s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); //TODO + //s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); + s.template registerType(); + + //s.template registerType(); + s.template registerType(); +} +template +void registerTypesClientPacks1(Serializer &s) +{ + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +template +void registerTypesClientPacks2(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +template +void registerTypesServerPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +template +void registerTypesLobbyPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + + // Any client can sent + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only host client send + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only server send + s.template registerType(); + s.template registerType(); + + // For client with permissions + s.template registerType(); + // Only for host client + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +template +void registerTypes(Serializer &s) +{ + registerTypesMapObjects1(s); + registerTypesMapObjects2(s); + registerTypesMapObjectTypes(s); + registerTypesClientPacks1(s); + registerTypesClientPacks2(s); + registerTypesServerPacks(s); + registerTypesLobbyPacks(s); +} + +#ifndef INSTANTIATE_REGISTER_TYPES_HERE + +extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); +extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); +extern template DLL_LINKAGE void registerTypes(CTypeList & s); + +#endif + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp index 614b9d5d6..a27008c89 100644 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ b/lib/registerTypes/TypesClientPacks1.cpp @@ -1,34 +1,34 @@ -/* - * TypesClientPacks1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks1(BinaryDeserializer & s); -template void registerTypesClientPacks1(BinarySerializer & s); -template void registerTypesClientPacks1(CTypeList & s); - -VCMI_LIB_NAMESPACE_END +/* + * TypesClientPacks1.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 "RegisterTypes.h" + +#include "../StartInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../NetPacks.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +template void registerTypesClientPacks1(BinaryDeserializer & s); +template void registerTypesClientPacks1(BinarySerializer & s); +template void registerTypesClientPacks1(CTypeList & s); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp index 482047ad0..cfa8edc7e 100644 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ b/lib/registerTypes/TypesClientPacks2.cpp @@ -1,38 +1,38 @@ -/* - * TypesClientPacks2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks2(BinaryDeserializer & s); -template void registerTypesClientPacks2(BinarySerializer & s); -template void registerTypesClientPacks2(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END +/* + * TypesClientPacks2.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 "RegisterTypes.h" + +#include "../StartInfo.h" +#include "../CStack.h" +#include "../battle/BattleInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../NetPacks.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +template void registerTypesClientPacks2(BinaryDeserializer & s); +template void registerTypesClientPacks2(BinarySerializer & s); +template void registerTypesClientPacks2(CTypeList & s); + + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp index dbb424350..2e4df3a01 100644 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ b/lib/registerTypes/TypesMapObjects1.cpp @@ -1,35 +1,35 @@ -/* - * TypesMapObjects1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjects1(BinaryDeserializer & s); -template void registerTypesMapObjects1(BinarySerializer & s); -template void registerTypesMapObjects1(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END +/* + * TypesMapObjects1.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 "RegisterTypes.h" + +#include "../StartInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../NetPacks.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template void registerTypesMapObjects1(BinaryDeserializer & s); +template void registerTypesMapObjects1(BinarySerializer & s); +template void registerTypesMapObjects1(CTypeList & s); + + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp index b68767f1d..0bcb062d5 100644 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ b/lib/registerTypes/TypesMapObjects2.cpp @@ -1,37 +1,37 @@ -/* - * TypesMapObjects2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesMapObjects2(BinaryDeserializer & s); -template void registerTypesMapObjects2(BinarySerializer & s); -template void registerTypesMapObjects2(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END +/* + * TypesMapObjects2.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 "RegisterTypes.h" + +#include "../StartInfo.h" +#include "../CStack.h" +#include "../battle/BattleInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../NetPacks.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + + +template void registerTypesMapObjects2(BinaryDeserializer & s); +template void registerTypesMapObjects2(BinarySerializer & s); +template void registerTypesMapObjects2(CTypeList & s); + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp index e46124d7d..7fa0991a7 100644 --- a/lib/registerTypes/TypesServerPacks.cpp +++ b/lib/registerTypes/TypesServerPacks.cpp @@ -1,33 +1,33 @@ -/* - * TypesServerPacks.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../NetPacks.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesServerPacks(BinaryDeserializer & s); -template void registerTypesServerPacks(BinarySerializer & s); -template void registerTypesServerPacks(CTypeList & s); - -VCMI_LIB_NAMESPACE_END +/* + * TypesServerPacks.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 "RegisterTypes.h" + +#include "../StartInfo.h" +#include "../mapObjects/CObjectHandler.h" +#include "../CCreatureHandler.h" +#include "../VCMI_Lib.h" +#include "../CArtHandler.h" +#include "../CHeroHandler.h" +#include "../spells/CSpellHandler.h" +#include "../CTownHandler.h" +#include "../NetPacks.h" + +#include "../serializer/BinaryDeserializer.h" +#include "../serializer/BinarySerializer.h" +#include "../serializer/CTypeList.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template void registerTypesServerPacks(BinaryDeserializer & s); +template void registerTypesServerPacks(BinarySerializer & s); +template void registerTypesServerPacks(CTypeList & s); + +VCMI_LIB_NAMESPACE_END diff --git a/lib/vcmi_endian.h b/lib/vcmi_endian.h index c1a81357d..fe0b17581 100644 --- a/lib/vcmi_endian.h +++ b/lib/vcmi_endian.h @@ -1,81 +1,81 @@ -/* - * vcmi_endian.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 //FIXME: use std::byteswap in C++23 - -VCMI_LIB_NAMESPACE_BEGIN - -/* Reading values from memory. - * - * read_le_u16, read_le_u32 : read a little endian value from - * memory. On big endian machines, the value will be byteswapped. - */ - -#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER) - -#if defined(_MSC_VER) -#define PACKED_STRUCT_BEGIN __pragma( pack(push, 1) ) -#define PACKED_STRUCT_END __pragma( pack(pop) ) -#else -#define PACKED_STRUCT_BEGIN -#define PACKED_STRUCT_END __attribute__(( packed )) -#endif - -PACKED_STRUCT_BEGIN struct unaligned_Uint16 { ui16 val; } PACKED_STRUCT_END; -PACKED_STRUCT_BEGIN struct unaligned_Uint32 { ui32 val; } PACKED_STRUCT_END; - -static inline ui16 read_unaligned_u16(const void *p) -{ - const auto * v = reinterpret_cast(p); - return v->val; -} - -static inline ui32 read_unaligned_u32(const void *p) -{ - const auto * v = reinterpret_cast(p); - return v->val; -} - -#define read_le_u16(p) (boost::endian::native_to_little(read_unaligned_u16(p))) -#define read_le_u32(p) (boost::endian::native_to_little(read_unaligned_u32(p))) - -#else - -#warning UB: unaligned memory access - -#define read_le_u16(p) (boost::endian::native_to_little(* reinterpret_cast(p))) -#define read_le_u32(p) (boost::endian::native_to_little(* reinterpret_cast(p))) - -#define PACKED_STRUCT_BEGIN -#define PACKED_STRUCT_END - -#endif - -static inline char readChar(const ui8 * buffer, int & i) -{ - return buffer[i++]; -} - -static inline std::string readString(const ui8 * buffer, int & i) -{ - int len = read_le_u32(buffer + i); - i += 4; - assert(len >= 0 && len <= 500000); //not too long - std::string ret; - ret.reserve(len); - for(int gg = 0; gg < len; ++gg) - { - ret += buffer[i++]; - } - return ret; -} - -VCMI_LIB_NAMESPACE_END +/* + * vcmi_endian.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 //FIXME: use std::byteswap in C++23 + +VCMI_LIB_NAMESPACE_BEGIN + +/* Reading values from memory. + * + * read_le_u16, read_le_u32 : read a little endian value from + * memory. On big endian machines, the value will be byteswapped. + */ + +#if defined(__clang__) || defined(__GNUC__) || defined(_MSC_VER) + +#if defined(_MSC_VER) +#define PACKED_STRUCT_BEGIN __pragma( pack(push, 1) ) +#define PACKED_STRUCT_END __pragma( pack(pop) ) +#else +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END __attribute__(( packed )) +#endif + +PACKED_STRUCT_BEGIN struct unaligned_Uint16 { ui16 val; } PACKED_STRUCT_END; +PACKED_STRUCT_BEGIN struct unaligned_Uint32 { ui32 val; } PACKED_STRUCT_END; + +static inline ui16 read_unaligned_u16(const void *p) +{ + const auto * v = reinterpret_cast(p); + return v->val; +} + +static inline ui32 read_unaligned_u32(const void *p) +{ + const auto * v = reinterpret_cast(p); + return v->val; +} + +#define read_le_u16(p) (boost::endian::native_to_little(read_unaligned_u16(p))) +#define read_le_u32(p) (boost::endian::native_to_little(read_unaligned_u32(p))) + +#else + +#warning UB: unaligned memory access + +#define read_le_u16(p) (boost::endian::native_to_little(* reinterpret_cast(p))) +#define read_le_u32(p) (boost::endian::native_to_little(* reinterpret_cast(p))) + +#define PACKED_STRUCT_BEGIN +#define PACKED_STRUCT_END + +#endif + +static inline char readChar(const ui8 * buffer, int & i) +{ + return buffer[i++]; +} + +static inline std::string readString(const ui8 * buffer, int & i) +{ + int len = read_le_u32(buffer + i); + i += 4; + assert(len >= 0 && len <= 500000); //not too long + std::string ret; + ret.reserve(len); + for(int gg = 0; gg < len; ++gg) + { + ret += buffer[i++]; + } + return ret; +} + +VCMI_LIB_NAMESPACE_END diff --git a/license.txt b/license.txt index 60a54ffbd..88f62e95c 100644 --- a/license.txt +++ b/license.txt @@ -1,280 +1,280 @@ - GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 - - Copyright (C) 1989, 1991 Free Software Foundation, Inc., - 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The licenses for most software are designed to take away your -freedom to share and change it. By contrast, the GNU General Public -License is intended to guarantee your freedom to share and change free -software--to make sure the software is free for all its users. This -General Public License applies to most of the Free Software -Foundation's software and to any other program whose authors commit to -using it. (Some other Free Software Foundation software is covered by -the GNU Lesser General Public License instead.) You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -this service if you wish), that you receive source code or can get it -if you want it, that you can change the software or use pieces of it -in new free programs; and that you know you can do these things. - - To protect your rights, we need to make restrictions that forbid -anyone to deny you these rights or to ask you to surrender the rights. -These restrictions translate to certain responsibilities for you if you -distribute copies of the software, or if you modify it. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must give the recipients all the rights that -you have. You must make sure that they, too, receive or can get the -source code. And you must show them these terms so they know their -rights. - - We protect your rights with two steps: (1) copyright the software, and -(2) offer you this license which gives you legal permission to copy, -distribute and/or modify the software. - - Also, for each author's protection and ours, we want to make certain -that everyone understands that there is no warranty for this free -software. If the software is modified by someone else and passed on, we -want its recipients to know that what they have is not the original, so -that any problems introduced by others will not reflect on the original -authors' reputations. - - Finally, any free program is threatened constantly by software -patents. We wish to avoid the danger that redistributors of a free -program will individually obtain patent licenses, in effect making the -program proprietary. To prevent this, we have made it clear that any -patent must be licensed for everyone's free use or not licensed at all. - - The precise terms and conditions for copying, distribution and -modification follow. - - GNU GENERAL PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION - - 0. This License applies to any program or other work which contains -a notice placed by the copyright holder saying it may be distributed -under the terms of this General Public License. The "Program", below, -refers to any such program or work, and a "work based on the Program" -means either the Program or any derivative work under copyright law: -that is to say, a work containing the Program or a portion of it, -either verbatim or with modifications and/or translated into another -language. (Hereinafter, translation is included without limitation in -the term "modification".) Each licensee is addressed as "you". - -Activities other than copying, distribution and modification are not -covered by this License; they are outside its scope. The act of -running the Program is not restricted, and the output from the Program -is covered only if its contents constitute a work based on the -Program (independent of having been made by running the Program). -Whether that is true depends on what the Program does. - - 1. You may copy and distribute verbatim copies of the Program's -source code as you receive it, in any medium, provided that you -conspicuously and appropriately publish on each copy an appropriate -copyright notice and disclaimer of warranty; keep intact all the -notices that refer to this License and to the absence of any warranty; -and give any other recipients of the Program a copy of this License -along with the Program. - -You may charge a fee for the physical act of transferring a copy, and -you may at your option offer warranty protection in exchange for a fee. - - 2. You may modify your copy or copies of the Program or any portion -of it, thus forming a work based on the Program, and copy and -distribute such modifications or work under the terms of Section 1 -above, provided that you also meet all of these conditions: - - a) You must cause the modified files to carry prominent notices - stating that you changed the files and the date of any change. - - b) You must cause any work that you distribute or publish, that in - whole or in part contains or is derived from the Program or any - part thereof, to be licensed as a whole at no charge to all third - parties under the terms of this License. - - c) If the modified program normally reads commands interactively - when run, you must cause it, when started running for such - interactive use in the most ordinary way, to print or display an - announcement including an appropriate copyright notice and a - notice that there is no warranty (or else, saying that you provide - a warranty) and that users may redistribute the program under - these conditions, and telling the user how to view a copy of this - License. (Exception: if the Program itself is interactive but - does not normally print such an announcement, your work based on - the Program is not required to print an announcement.) - -These requirements apply to the modified work as a whole. If -identifiable sections of that work are not derived from the Program, -and can be reasonably considered independent and separate works in -themselves, then this License, and its terms, do not apply to those -sections when you distribute them as separate works. But when you -distribute the same sections as part of a whole which is a work based -on the Program, the distribution of the whole must be on the terms of -this License, whose permissions for other licensees extend to the -entire whole, and thus to each and every part regardless of who wrote it. - -Thus, it is not the intent of this section to claim rights or contest -your rights to work written entirely by you; rather, the intent is to -exercise the right to control the distribution of derivative or -collective works based on the Program. - -In addition, mere aggregation of another work not based on the Program -with the Program (or with a work based on the Program) on a volume of -a storage or distribution medium does not bring the other work under -the scope of this License. - - 3. You may copy and distribute the Program (or a work based on it, -under Section 2) in object code or executable form under the terms of -Sections 1 and 2 above provided that you also do one of the following: - - a) Accompany it with the complete corresponding machine-readable - source code, which must be distributed under the terms of Sections - 1 and 2 above on a medium customarily used for software interchange; or, - - b) Accompany it with a written offer, valid for at least three - years, to give any third party, for a charge no more than your - cost of physically performing source distribution, a complete - machine-readable copy of the corresponding source code, to be - distributed under the terms of Sections 1 and 2 above on a medium - customarily used for software interchange; or, - - c) Accompany it with the information you received as to the offer - to distribute corresponding source code. (This alternative is - allowed only for noncommercial distribution and only if you - received the program in object code or executable form with such - an offer, in accord with Subsection b above.) - -The source code for a work means the preferred form of the work for -making modifications to it. For an executable work, complete source -code means all the source code for all modules it contains, plus any -associated interface definition files, plus the scripts used to -control compilation and installation of the executable. However, as a -special exception, the source code distributed need not include -anything that is normally distributed (in either source or binary -form) with the major components (compiler, kernel, and so on) of the -operating system on which the executable runs, unless that component -itself accompanies the executable. - -If distribution of executable or object code is made by offering -access to copy from a designated place, then offering equivalent -access to copy the source code from the same place counts as -distribution of the source code, even though third parties are not -compelled to copy the source along with the object code. - - 4. You may not copy, modify, sublicense, or distribute the Program -except as expressly provided under this License. Any attempt -otherwise to copy, modify, sublicense or distribute the Program is -void, and will automatically terminate your rights under this License. -However, parties who have received copies, or rights, from you under -this License will not have their licenses terminated so long as such -parties remain in full compliance. - - 5. You are not required to accept this License, since you have not -signed it. However, nothing else grants you permission to modify or -distribute the Program or its derivative works. These actions are -prohibited by law if you do not accept this License. Therefore, by -modifying or distributing the Program (or any work based on the -Program), you indicate your acceptance of this License to do so, and -all its terms and conditions for copying, distributing or modifying -the Program or works based on it. - - 6. Each time you redistribute the Program (or any work based on the -Program), the recipient automatically receives a license from the -original licensor to copy, distribute or modify the Program subject to -these terms and conditions. You may not impose any further -restrictions on the recipients' exercise of the rights granted herein. -You are not responsible for enforcing compliance by third parties to -this License. - - 7. If, as a consequence of a court judgment or allegation of patent -infringement or for any other reason (not limited to patent issues), -conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot -distribute so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you -may not distribute the Program at all. For example, if a patent -license would not permit royalty-free redistribution of the Program by -all those who receive copies directly or indirectly through you, then -the only way you could satisfy both it and this License would be to -refrain entirely from distribution of the Program. - -If any portion of this section is held invalid or unenforceable under -any particular circumstance, the balance of the section is intended to -apply and the section as a whole is intended to apply in other -circumstances. - -It is not the purpose of this section to induce you to infringe any -patents or other property right claims or to contest validity of any -such claims; this section has the sole purpose of protecting the -integrity of the free software distribution system, which is -implemented by public license practices. Many people have made -generous contributions to the wide range of software distributed -through that system in reliance on consistent application of that -system; it is up to the author/donor to decide if he or she is willing -to distribute software through any other system and a licensee cannot -impose that choice. - -This section is intended to make thoroughly clear what is believed to -be a consequence of the rest of this License. - - 8. If the distribution and/or use of the Program is restricted in -certain countries either by patents or by copyrighted interfaces, the -original copyright holder who places the Program under this License -may add an explicit geographical distribution limitation excluding -those countries, so that distribution is permitted only in or among -countries not thus excluded. In such case, this License incorporates -the limitation as if written in the body of this License. - - 9. The Free Software Foundation may publish revised and/or new versions -of the General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - -Each version is given a distinguishing version number. If the Program -specifies a version number of this License which applies to it and "any -later version", you have the option of following the terms and conditions -either of that version or of any later version published by the Free -Software Foundation. If the Program does not specify a version number of -this License, you may choose any version ever published by the Free Software -Foundation. - - 10. If you wish to incorporate parts of the Program into other free -programs whose distribution conditions are different, write to the author -to ask for permission. For software which is copyrighted by the Free -Software Foundation, write to the Free Software Foundation; we sometimes -make exceptions for this. Our decision will be guided by the two goals -of preserving the free status of all derivatives of our free software and -of promoting the sharing and reuse of software generally. - - NO WARRANTY - - 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY -FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES -PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED -OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS -TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE -PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, -REPAIR OR CORRECTION. - - 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR -REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, -INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING -OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED -TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY -YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER -PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE -POSSIBILITY OF SUCH DAMAGES. - + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + END OF TERMS AND CONDITIONS \ No newline at end of file diff --git a/mapeditor/StdInc.cpp b/mapeditor/StdInc.cpp index b64b59be5..6237e6e6f 100644 --- a/mapeditor/StdInc.cpp +++ b/mapeditor/StdInc.cpp @@ -1 +1 @@ -#include "StdInc.h" +#include "StdInc.h" diff --git a/mapeditor/StdInc.h b/mapeditor/StdInc.h index 778a04843..4dc45a2ef 100644 --- a/mapeditor/StdInc.h +++ b/mapeditor/StdInc.h @@ -1,55 +1,55 @@ -#pragma once - -#include "../Global.h" - -#define VCMI_EDITOR_VERSION "0.2" -#define VCMI_EDITOR_NAME "VCMI Map Editor" - -#include -#include -#include -#include -#include -#include -#include - -VCMI_LIB_USING_NAMESPACE - -using NumericPointer = typename std::conditional::type; - -template -NumericPointer data_cast(Type * _pointer) -{ - static_assert(sizeof(Type *) == sizeof(NumericPointer), - "Cannot compile for that architecture, see NumericPointer definition"); - - return reinterpret_cast(_pointer); -} - -template -Type * data_cast(NumericPointer _numeric) -{ - static_assert(sizeof(Type *) == sizeof(NumericPointer), - "Cannot compile for that architecture, see NumericPointer definition"); - - return reinterpret_cast(_numeric); -} - -inline QString pathToQString(const boost::filesystem::path & path) -{ -#ifdef VCMI_WINDOWS - return QString::fromStdWString(path.wstring()); -#else - return QString::fromStdString(path.string()); -#endif -} - -inline boost::filesystem::path qstringToPath(const QString & path) -{ -#ifdef VCMI_WINDOWS - return boost::filesystem::path(path.toStdWString()); -#else - return boost::filesystem::path(path.toUtf8().data()); -#endif -} +#pragma once + +#include "../Global.h" + +#define VCMI_EDITOR_VERSION "0.2" +#define VCMI_EDITOR_NAME "VCMI Map Editor" + +#include +#include +#include +#include +#include +#include +#include + +VCMI_LIB_USING_NAMESPACE + +using NumericPointer = typename std::conditional::type; + +template +NumericPointer data_cast(Type * _pointer) +{ + static_assert(sizeof(Type *) == sizeof(NumericPointer), + "Cannot compile for that architecture, see NumericPointer definition"); + + return reinterpret_cast(_pointer); +} + +template +Type * data_cast(NumericPointer _numeric) +{ + static_assert(sizeof(Type *) == sizeof(NumericPointer), + "Cannot compile for that architecture, see NumericPointer definition"); + + return reinterpret_cast(_numeric); +} + +inline QString pathToQString(const boost::filesystem::path & path) +{ +#ifdef VCMI_WINDOWS + return QString::fromStdWString(path.wstring()); +#else + return QString::fromStdString(path.string()); +#endif +} + +inline boost::filesystem::path qstringToPath(const QString & path) +{ +#ifdef VCMI_WINDOWS + return boost::filesystem::path(path.toStdWString()); +#else + return boost::filesystem::path(path.toUtf8().data()); +#endif +} diff --git a/scripting/erm/ERMInterpreter.cpp b/scripting/erm/ERMInterpreter.cpp index e4f60105a..757d0697d 100644 --- a/scripting/erm/ERMInterpreter.cpp +++ b/scripting/erm/ERMInterpreter.cpp @@ -1,1774 +1,1774 @@ -/* - * ERMInterpreter.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 "ERMInterpreter.h" - -#include - -namespace spirit = boost::spirit; -using ::scripting::ContextBase; -using namespace ::VERMInterpreter; - -typedef int TUnusedType; - -namespace ERMConverter -{ - //console printer - using namespace ERM; - - static const std::map CMP_OPERATION = - { - {"<", "<"}, - {">", ">"}, - {">=", ">="}, - {"=>", ">="}, - {"<=", "<="}, - {"=<", "<="}, - {"=", "=="}, - {"<>", "~="}, - {"><", "~="}, - }; - - - struct Variable - { - std::string name = ""; - std::string macro = ""; - int index = 0; - - Variable(const std::string & name_, int index_) - { - name = name_; - index = index_; - } - - Variable(const std::string & macro_) - { - macro = macro_; - } - - bool isEmpty() const - { - return (name == "") && (macro == ""); - } - - bool isMacro() const - { - return (name == "") && (macro != ""); - } - - bool isSpecial() const - { - return (name.size() > 0) && (name[0] == 'd'); - } - - std::string str() const - { - if(isEmpty()) - { - return std::to_string(index); - } - else if(isMacro()) - { - return boost::to_string(boost::format("M['%s']") % macro); - } - else if(isSpecial() && (name.size() == 1)) - { - boost::format fmt; - fmt.parse("{'d', %d}"); - fmt % index; - return fmt.str(); - } - else if(isSpecial() && (name.size() != 1)) - { - - std::string ret; - - { - boost::format fmt; - - if(index == 0) - { - fmt.parse("Q['%s']"); - fmt % name[name.size()-1]; - } - else - { - fmt.parse("%s['%d']"); - fmt % name[name.size()-1] % index; - } - ret = fmt.str(); - } - - for(int i = ((int) name.size())-2; i > 0; i--) - { - boost::format fmt("%s[tostring(%s)]"); - - fmt % name[i] % ret; - - ret = fmt.str(); - } - - { - boost::format fmt; - fmt.parse("{'d', %s}"); - fmt % ret; - return fmt.str(); - } - } - else - { - std::string ret; - { - boost::format fmt; - - if(index == 0) - { - fmt.parse("Q['%s']"); - fmt % name[name.size()-1]; - } - else - { - fmt.parse("%s['%d']"); - fmt % name[name.size()-1] % index; - } - ret = fmt.str(); - } - - for(int i = ((int) name.size())-2; i >= 0; i--) - { - boost::format fmt("%s[tostring(%s)]"); - - fmt % name[i] % ret; - - ret = fmt.str(); - } - - return ret; - } - } - }; - - struct LVL2IexpToVar - { - LVL2IexpToVar() = default; - - Variable operator()(const TVarExpNotMacro & val) const - { - if(val.val.has_value()) - return Variable(val.varsym, *val.val); - else - return Variable(val.varsym, 0); - } - - Variable operator()(const TMacroUsage & val) const - { - return Variable(val.macro); - } - }; - - struct LVL1IexpToVar - { - LVL1IexpToVar() = default; - - Variable operator()(const int & constant) const - { - return Variable("", constant); - } - - Variable operator()(const TVarExp & var) const - { - return std::visit(LVL2IexpToVar(), var); - } - }; - - struct Condition - { - std::string operator()(const TComparison & cmp) const - { - Variable lhs = std::visit(LVL1IexpToVar(), cmp.lhs); - Variable rhs = std::visit(LVL1IexpToVar(), cmp.rhs); - - auto sign = CMP_OPERATION.find(cmp.compSign); - if(sign == std::end(CMP_OPERATION)) - throw EScriptExecError(std::string("Wrong comparison sign: ") + cmp.compSign); - - boost::format fmt("(%s %s %s)"); - fmt % lhs.str() % sign->second % rhs.str(); - return fmt.str(); - } - std::string operator()(const int & flag) const - { - return boost::to_string(boost::format("F['%d']") % flag); - } - }; - - struct ParamIO - { - ParamIO() = default; - std::string name = ""; - bool isInput = false; - bool semi = false; - std::string semiCmpSign = ""; - }; - - struct Converter - { - mutable std::ostream * out; - Converter(std::ostream * out_) - : out(out_) - {} - protected: - - void put(const std::string & text) const - { - (*out) << text; - } - - void putLine(const std::string & line) const - { - (*out) << line << std::endl; - } - - void endLine() const - { - (*out) << std::endl; - } - }; - - struct GetBodyOption - { - virtual std::string operator()(const TVarConcatString & cmp) const - { - throw EScriptExecError("String concatenation not allowed in this receiver"); - } - virtual std::string operator()(const TStringConstant & cmp) const - { - throw EScriptExecError("String constant not allowed in this receiver"); - } - virtual std::string operator()(const TCurriedString & cmp) const - { - throw EScriptExecError("Curried string not allowed in this receiver"); - } - virtual std::string operator()(const TSemiCompare & cmp) const - { - throw EScriptExecError("Semi comparison not allowed in this receiver"); - } - virtual std::string operator()(const TMacroDef & cmp) const - { - throw EScriptExecError("Macro definition not allowed in this receiver"); - } - virtual std::string operator()(const TIexp & cmp) const - { - throw EScriptExecError("i-expression not allowed in this receiver"); - } - virtual std::string operator()(const TVarpExp & cmp) const - { - throw EScriptExecError("Varp expression not allowed in this receiver"); - } - virtual std::string operator()(const spirit::unused_type & cmp) const - { - throw EScriptExecError("\'Nothing\' not allowed in this receiver"); - } - }; - - struct BodyOption - { - ParamIO operator()(const TVarConcatString & cmp) const - { - throw EScriptExecError(std::string("String concatenation not allowed in this receiver|")+cmp.string.str+"|"); - } - - ParamIO operator()(const TStringConstant & cmp) const - { - boost::format fmt("[===[%s]===]"); - fmt % cmp.str; - - ParamIO ret; - ret.isInput = true; - ret.name = fmt.str(); - return ret; - } - - ParamIO operator()(const TCurriedString & cmp) const - { - throw EScriptExecError("Curried string not allowed in this receiver"); - } - - ParamIO operator()(const TSemiCompare & cmp) const - { - ParamIO ret; - ret.isInput = false; - ret.semi = true; - ret.semiCmpSign = cmp.compSign; - ret.name = (std::visit(LVL1IexpToVar(), cmp.rhs)).str(); - return ret; - } - - ParamIO operator()(const TMacroDef & cmp) const - { - throw EScriptExecError("Macro definition not allowed in this receiver"); - } - - ParamIO operator()(const TIexp & cmp) const - { - ParamIO ret; - ret.isInput = true; - ret.name = (std::visit(LVL1IexpToVar(), cmp)).str();; - return ret; - } - - ParamIO operator()(const TVarpExp & cmp) const - { - ParamIO ret; - ret.isInput = false; - - ret.name = (std::visit(LVL2IexpToVar(), cmp.var)).str(); - return ret; - } - - ParamIO operator()(const spirit::unused_type & cmp) const - { - throw EScriptExecError("\'Nothing\' not allowed in this receiver"); - } - }; - - struct Receiver : public Converter - { - Receiver(std::ostream * out_) - : Converter(out_) - {} - - virtual void operator()(const TVRLogic & trig) const - { - throw EInterpreterError("VR logic is not allowed in this receiver!"); - } - - virtual void operator()(const TVRArithmetic & trig) const - { - throw EInterpreterError("VR arithmetic is not allowed in this receiver!"); - } - - virtual void operator()(const TNormalBodyOption & trig) const - { - throw EInterpreterError("Normal body is not allowed in this receiver!"); - } - - }; - - struct GenericReceiver : public Receiver - { - std::string name; - bool specialSemiCompare = false; - - GenericReceiver(std::ostream * out_, const std::string & name_, bool specialSemiCompare_) - : Receiver(out_), - name(name_), - specialSemiCompare(specialSemiCompare_) - {} - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & trig) const override - { - std::string outParams; - std::string inParams; - - std::string semiCompareDecl; - - std::vector compares; - - bool hasOutput = false; - bool hasSemiCompare = false; - - std::vector optionParams; - - if(trig.params.has_value()) - { - for(auto & p : *trig.params) - optionParams.push_back(std::visit(BodyOption(), p)); - } - - int idx = 1; - int fidx = 1; - - for(const ParamIO & p : optionParams) - { - if(p.isInput) - { - if(outParams.empty()) - outParams = "_"; - else - outParams += ", _"; - - inParams += ", "; - inParams += p.name; - } - else if(p.semi) - { - hasOutput = true; - hasSemiCompare = true; - - std::string tempVar = std::string("s")+std::to_string(idx); - - if(semiCompareDecl.empty()) - { - semiCompareDecl = "local "+tempVar; - } - else - { - semiCompareDecl += ", "; - semiCompareDecl += tempVar; - } - - if(outParams.empty()) - { - outParams = tempVar; - } - else - { - outParams += ", "; - outParams += tempVar; - } - - inParams += ", nil"; - - - auto sign = CMP_OPERATION.find(p.semiCmpSign); - if(sign == std::end(CMP_OPERATION)) - throw EScriptExecError(std::string("Wrong comparison sign: ") + p.semiCmpSign); - - boost::format cmpFmt("F[%d] = (%s %s %s)"); - cmpFmt % fidx % p.name % sign->second % tempVar; - compares.push_back(cmpFmt.str()); - - fidx++; - } - else - { - hasOutput = true; - - if(outParams.empty()) - { - outParams = p.name; - } - else - { - outParams += ", "; - outParams += p.name; - } - - inParams += ", nil"; - } - - idx++; - } - - if(hasSemiCompare) - { - putLine(semiCompareDecl); - } - - boost::format callFormat; - - if(hasOutput) - { - callFormat.parse("%s = %s:%s(x%s)"); - callFormat % outParams; - } - else - { - callFormat.parse("%s:%s(x%s)"); - } - - callFormat % name; - callFormat % trig.optionCode; - callFormat % inParams; - - putLine(callFormat.str()); - - for(auto & str : compares) - putLine(str); - } - }; - - struct FU : public Receiver - { - Variable v; - - FU(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - FU(std::ostream * out_) - : Receiver(out_), - v("", 0) - { - } - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & trig) const override - { - switch(trig.optionCode) - { - case 'E': - { - putLine("do return end"); - } - break; - default: - throw EInterpreterError("Unknown opcode in FU receiver"); - break; - } - } - }; - - struct MC_S : public GetBodyOption - { - MC_S() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TMacroDef & cmp) const override - { - return cmp.macro; - } - }; - - struct MC : public Receiver - { - Variable v; - - MC(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - MC(std::ostream * out_) - : Receiver(out_), - v("", 0) - { - } - - using Receiver::operator(); - - void operator()(const TNormalBodyOption & option) const override - { - switch(option.optionCode) - { - case 'S': - { - if(option.params.has_value()) - { - for(auto & p : *option.params) - { - std::string macroName = std::visit(MC_S(), p); - - boost::format callFormat; - - if(v.isEmpty()) - { - callFormat.parse("ERM:addMacro('%s', 'v', '%s')"); - callFormat % macroName % macroName; - } - else - { - callFormat.parse("ERM:addMacro('%s', '%s', '%d')"); - callFormat % macroName % v.name % v.index; - } - - putLine(callFormat.str()); - } - } - } - break; - default: - throw EInterpreterError("Unknown opcode in MC receiver"); - break; - } - } - }; - - struct VR_S : public GetBodyOption - { - VR_S() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - auto v = std::visit(LVL1IexpToVar(), cmp); - return v.str(); - } - std::string operator()(const TStringConstant & cmp) const override - { - boost::format fmt("[===[%s]===]"); - fmt % cmp.str; - return fmt.str(); - } - }; - - struct VR_H : public GetBodyOption - { - VR_H() - {} - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - Variable p = std::visit(LVL1IexpToVar(), cmp); - - if(p.index <= 0) - throw EScriptExecError("VR:H requires flag index"); - - if(p.name != "") - throw EScriptExecError("VR:H accept only flag index"); - - - boost::format fmt("'%d'"); - fmt % p.index; - return fmt.str(); - } - }; - - struct VR_X : public GetBodyOption - { - VR_X() - { - } - - using GetBodyOption::operator(); - - std::string operator()(const TIexp & cmp) const override - { - Variable p = std::visit(LVL1IexpToVar(), cmp); - - return p.str(); - } - }; - - struct VR : public Receiver - { - Variable v; - - VR(std::ostream * out_, const ERM::TIexp & tid) - : Receiver(out_), - v(std::visit(LVL1IexpToVar(), tid)) - { - } - - using Receiver::operator(); - - void operator()(const TVRLogic & trig) const override - { - Variable rhs = std::visit(LVL1IexpToVar(), trig.var); - - std::string opcode; - - switch (trig.opcode) - { - case '&': - opcode = "bit.band"; - break; - case '|': - opcode = "bit.bor"; - break; - default: - throw EInterpreterError("Wrong opcode in VR logic expression!"); - break; - } - - boost::format fmt("%s = %s(%s, %s)"); - fmt % v.str() % opcode % v.str() % rhs.str(); - putLine(fmt.str()); - } - - void operator()(const TVRArithmetic & trig) const override - { - Variable rhs = std::visit(LVL1IexpToVar(), trig.rhs); - - std::string opcode; - - switch (trig.opcode) - { - case '+': - opcode = v.name[0] == 'z' ? ".." : "+"; - break; - case '-': - case '*': - case '%': - opcode = trig.opcode; - break; - case ':': - opcode = "/"; - break; - default: - throw EInterpreterError("Wrong opcode in VR arithmetic!"); - break; - } - - boost::format fmt("%s = %s %s %s"); - fmt % v.str() % v.str() % opcode % rhs.str(); - putLine(fmt.str()); - } - - void operator()(const TNormalBodyOption & trig) const override - { - switch(trig.optionCode) - { - case 'C': //setting/checking v vars - { - if(v.index <= 0) - throw EScriptExecError("VR:C requires indexed variable"); - - std::vector optionParams; - - if(trig.params.has_value()) - { - for(auto & p : *trig.params) - optionParams.push_back(std::visit(BodyOption(), p)); - } - - auto index = v.index; - - for(auto & p : optionParams) - { - boost::format fmt; - if(p.isInput) - fmt.parse("%s['%d'] = %s") % v.name % index % p.name; - else - fmt.parse("%s = %s['%d']") % p.name % v.name % index; - putLine(fmt.str()); - index++; - } - } - break; - case 'H': //checking if string is empty - { - if(!trig.params.has_value() || trig.params->size() != 1) - throw EScriptExecError("VR:H option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_H(), (*trig.params)[0]); - boost::format fmt("ERM.VR(%s):H(%s)"); - fmt % v.str() % opt; - putLine(fmt.str()); - } - break; - case 'U': - { - if(!trig.params.has_value() || trig.params->size() != 1) - throw EScriptExecError("VR:H/U need 1 parameter!"); - - std::string opt = std::visit(VR_S(), (*trig.params)[0]); - boost::format fmt("ERM.VR(%s):%c(%s)"); - fmt % v.str() % (trig.optionCode) % opt; - putLine(fmt.str()); - } - break; - case 'M': //string operations - { - if(!trig.params.has_value() || trig.params->size() < 2) - throw EScriptExecError("VR:M needs at least 2 parameters!"); - - std::string opt = std::visit(VR_X(), (*trig.params)[0]); - int paramIndex = 1; - - if(opt == "3") - { - boost::format fmt("%s = ERM.VR(%s):M3("); - fmt % v.str() % v.str(); - put(fmt.str()); - } - else - { - auto target = std::visit(VR_X(), (*trig.params)[paramIndex++]); - - boost::format fmt("%s = ERM.VR(%s):M%s("); - fmt % target % v.str() % opt; - put(fmt.str()); - } - - for(int i = paramIndex; i < trig.params->size(); i++) - { - opt = std::visit(VR_X(), (*trig.params)[i]); - if(i > paramIndex) put(","); - put(opt); - } - - putLine(")"); - } - break; - case 'X': //bit xor - { - if(!trig.params.has_value() || trig.params->size() != 1) - throw EScriptExecError("VR:X option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_X(), (*trig.params)[0]); - - boost::format fmt("%s = bit.bxor(%s, %s)"); - fmt % v.str() % v.str() % opt;putLine(fmt.str()); - } - break; - case 'R': //random variables - { - //TODO - putLine("--VR:R not implemented"); - } - break; - case 'S': //setting variable - { - if(!trig.params.has_value() || trig.params->size() != 1) - throw EScriptExecError("VR:S option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_S(), (*trig.params)[0]); - put(v.str()); - put(" = "); - put(opt); - endLine(); - } - break; - case 'T': //random variables - { - //TODO - putLine("--VR:T not implemented"); - } - break; - case 'V': //convert string to value - { - if(!trig.params.has_value() || trig.params->size() != 1) - throw EScriptExecError("VR:V option takes exactly 1 parameter!"); - - std::string opt = std::visit(VR_X(), (*trig.params)[0]); - boost::format fmt("%s = tostring(%s)"); - fmt % v.str() % opt; - putLine(fmt.str()); - } - break; - default: - throw EScriptExecError("Wrong VR receiver option!"); - break; - } - } - }; - - - struct ERMExp : public Converter - { - ERMExp(std::ostream * out_) - : Converter(out_) - {} - - template - void performBody(const std::optional & body, const Visitor & visitor) const - { - if(body.has_value()) - { - const ERM::Tbody & bo = *body; - for(int g=0; g & identifier, const std::optional & body) const - { - if(name == "VR") - { - if(!identifier.has_value()) - throw EScriptExecError("VR receiver requires arguments"); - - ERM::Tidentifier tid = identifier.value(); - if(tid.size() != 1) - throw EScriptExecError("VR receiver takes exactly 1 argument"); - - performBody(body, VR(out, tid[0])); - } - else if(name == "re") - { - if(!identifier.has_value()) - throw EScriptExecError("re receiver requires arguments"); - - ERM::Tidentifier tid = identifier.value(); - - auto argc = tid.size(); - - if(argc > 0) - { - std::string loopCounter = (std::visit(LVL1IexpToVar(), tid.at(0))).str(); - - std::string startVal = argc > 1 ? (std::visit(LVL1IexpToVar(), tid.at(1))).str() : loopCounter; - std::string stopVal = argc > 2 ? (std::visit(LVL1IexpToVar(), tid.at(2))).str() : loopCounter; - std::string increment = argc > 3 ? (std::visit(LVL1IexpToVar(), tid.at(3))).str() : "1"; - - boost::format fmt("for __iter = %s, %s, %s do"); - - - fmt % startVal % stopVal % increment; - putLine(fmt.str()); - fmt.parse("%s = __iter"); - fmt % loopCounter; - putLine(fmt.str()); - } - else - { - throw EScriptExecError("re receiver requires arguments"); - } - } - else if(name == "FU" && !identifier.has_value()) - { - performBody(body, FU(out)); //assume FU:E - } - else if(name == "MC") - { - if(identifier.has_value()) - { - ERM::Tidentifier tid = identifier.value(); - if(tid.size() != 1) - throw EScriptExecError("MC receiver takes no more than 1 argument"); - - performBody(body, MC(out, tid[0])); - } - else - { - performBody(body, MC(out)); - } - } - else - { - std::vector identifiers; - - if(identifier.has_value()) - { - for(const auto & id : identifier.value()) - { - Variable v = std::visit(LVL1IexpToVar(), id); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in receiver identifier"); - identifiers.push_back(v.str()); - } - } - - std::string params; - - for(auto iter = std::begin(identifiers); iter != std::end(identifiers); ++iter) - { - if(!params.empty()) - params += ", "; - params += *iter; - } - - if(body.has_value()) - { - const ERM::Tbody & bo = *body; - if(bo.size() == 1) - { - boost::format fmt("ERM.%s(%s)"); - fmt % name; - fmt % params; - - GenericReceiver gr(out, fmt.str(), (name == "DO")); - std::visit(gr,bo[0]); - } - else - { - putLine("do"); - boost::format fmt("local %s = ERM.%s(%s)"); - fmt % name; - fmt % name; - fmt % params; - - putLine(fmt.str()); - - performBody(body, GenericReceiver(out, name, (name=="DO") )); - - putLine("end"); - } - } - else - { - //is it an error? - logMod->warn("ERM receiver '%s %s' w/o body", name, params); - } - - - } - } - - void convertConditionInner(const Tcondition & cond, char op) const - { - std::string lhs = std::visit(Condition(), cond.cond); - - if(cond.ctype != '/') - op = cond.ctype; - - switch (op) - { - case '&': - put(" and "); - break; - case '|': - put(" or "); - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - put(lhs); - - if(cond.rhs.has_value()) - { - switch (op) - { - case '&': - case '|': - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - convertConditionInner(cond.rhs->get(), op); - } - } - - void convertCondition(const Tcondition & cond) const - { - //&c1/c2/c3|c4/c5/c6 -> (c1 & c2 & c3) | c4 | c5 | c6 - std::string lhs = std::visit(Condition(), cond.cond); - put("if "); - put(lhs); - - if(cond.rhs.has_value()) - { - switch (cond.ctype) - { - case '&': - case '|': - break; - default: - throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); - break; - } - - convertConditionInner(cond.rhs->get(), cond.ctype); - } - - putLine(" then "); - } - - void convertReceiverOrInstruction(const std::optional & condition, - const std::string & name, - const std::optional & identifier, - const std::optional & body) const - { - if(name=="if") - { - if(condition.has_value()) - convertCondition(*condition); - else - putLine("if true then"); - } - else if(name=="el") - { - putLine("else"); - } - else if(name=="en") - { - putLine("end"); - } - else - { - if(condition.has_value()) - { - convertCondition(*condition); - convert(name, identifier, body); - putLine("end"); - } - else - { - convert(name, identifier, body); - } - } - } - - void operator()(const Ttrigger & trig) const - { - throw EInterpreterError("Triggers cannot be executed!"); - } - - void operator()(const TPostTrigger & trig) const - { - throw EInterpreterError("Post-triggers cannot be executed!"); - } - - void operator()(const Tinstruction & trig) const - { - convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, std::make_optional(trig.body)); - } - - void operator()(const Treceiver & trig) const - { - convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, trig.body); - } - }; - - struct Command : public Converter - { - Command(std::ostream * out_) - : Converter(out_) - {} - - void operator()(const Tcommand & cmd) const - { - std::visit(ERMExp(out), cmd.cmd); - } - void operator()(const std::string & comment) const - { - (*out) << "-- " << comment; - endLine(); - } - - void operator()(const spirit::unused_type &) const - { - } - }; - - struct TLiteralEval - { - - std::string operator()(const char & val) - { - return "{\"'\",'"+ std::to_string(val) +"'}"; - } - std::string operator()(const double & val) - { - return std::to_string(val); - } - std::string operator()(const int & val) - { - return std::to_string(val); - } - std::string operator()(const std::string & val) - { - return "{\"'\",[===[" + val + "]===]}"; - } - }; - - struct VOptionEval : public Converter - { - VOptionEval(std::ostream * out_) - : Converter(out_) - {} - - void operator()(VNIL const & opt) const - { - (*out) << "{}"; - } - void operator()(const boost::recursive_wrapper & opt) const; - - void operator()(const VSymbol & opt) const - { - (*out) << "\"" << opt.text << "\""; - } - void operator()(const TLiteral & opt) const - { - TLiteralEval tmp; - (*out) << std::visit(tmp, opt); - } - void operator()(const ERM::Tcommand & opt) const - { - //this is how FP works, evaluation == producing side effects - //TODO: can we evaluate to smth more useful? - //??? - throw EVermScriptExecError("Using ERM options in VERM expression is not (yet) allowed"); -// std::visit(ERMExp(out), opt.cmd); - } - }; - - void VOptionEval::operator()(const boost::recursive_wrapper & opt) const - { - VNode tmpn(opt.get()); - - (*out) << "{"; - - for(VOption & op : tmpn.children) - { - std::visit(VOptionEval(out), op); - (*out) << ","; - } - (*out) << "}"; - } - - struct Line : public Converter - { - Line(std::ostream * out_) - : Converter(out_) - {} - - void operator()(const TVExp & cmd) const - { - put("VERM:E"); - - VNode line(cmd); - - VOptionEval eval(out); - eval(line); - - - endLine(); - } - void operator()(const TERMline & cmd) const - { - std::visit(Command(out), cmd); - } - }; - - void convertInstructions(std::ostream & out, ERMInterpreter * owner) - { - out << "local function instructions()" << std::endl; - out << "local e, x, y = {}, {}, {}" << std::endl; - - Line lineConverter(&out); - - for(const LinePointer & lp : owner->instructions) - { - ERM::TLine & line = owner->retrieveLine(lp); - - std::visit(lineConverter, line); - } - - out << "end" << std::endl; - } - - void convertFunctions(std::ostream & out, ERMInterpreter * owner, const std::vector & triggers) - { - Line lineConverter(&out); - - for(const VERMInterpreter::Trigger & trigger : triggers) - { - ERM::TLine & firstLine = owner->retrieveLine(trigger.line); - - const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); - - //TODO: condition - - out << "ERM:addTrigger({" << std::endl; - - if(!trig.identifier.has_value()) - throw EInterpreterError("Function must have identifier"); - - ERM::Tidentifier tid = trig.identifier.value(); - - if(tid.empty()) - throw EInterpreterError("Function must have identifier"); - - Variable v = std::visit(LVL1IexpToVar(), tid[0]); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in function definition"); - - out << "id = {" << v.str() << "}," << std::endl; - out << "name = 'FU'," << std::endl; - out << "fn = function (e, y, x)" << std::endl; - out << "local _" << std::endl; - - LinePointer lp = trigger.line; - ++lp; - - for(; lp.isValid(); ++lp) - { - ERM::TLine curLine = owner->retrieveLine(lp); - if(owner->isATrigger(curLine)) - break; - - std::visit(lineConverter, curLine); - } - - out << "end," << std::endl; - out << "})" << std::endl; - } - } - - void convertTriggers(std::ostream & out, ERMInterpreter * owner, const VERMInterpreter::TriggerType & type, const std::vector & triggers) - { - Line lineConverter(&out); - - for(const VERMInterpreter::Trigger & trigger : triggers) - { - ERM::TLine & firstLine = owner->retrieveLine(trigger.line); - - const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); - - //TODO: condition - - out << "ERM:addTrigger({" << std::endl; - - std::vector identifiers; - - if(trig.identifier.has_value()) - { - for(const auto & id : trig.identifier.value()) - { - Variable v = std::visit(LVL1IexpToVar(), id); - - if(v.isSpecial()) - throw ELineProblem("Special variable syntax ('d') is not allowed in trigger definition"); - identifiers.push_back(v.str()); - } - } - - out << "id = {"; - for(const auto & id : identifiers) - out << "'" << id << "',"; - out << "}," << std::endl; - - out << "name = '" << trig.name << "'," << std::endl; - out << "fn = function (e, y)" << std::endl; - out << "local _" << std::endl; - - LinePointer lp = trigger.line; - ++lp; - - for(; lp.isValid(); ++lp) - { - ERM::TLine curLine = owner->retrieveLine(lp); - if(owner->isATrigger(curLine)) - break; - - std::visit(lineConverter, curLine); - } - - out << "end," << std::endl; - out << "})" << std::endl; - } - } -} - -struct ScriptScanner -{ - ERMInterpreter * interpreter; - LinePointer lp; - - ScriptScanner(ERMInterpreter * interpr, const LinePointer & _lp) : interpreter(interpr), lp(_lp) - {} - - void operator()(const TVExp & cmd) const - { - // - } - void operator()(const TERMline & cmd) const - { - if(std::holds_alternative(cmd)) //TCommand - { - Tcommand tcmd = std::get(cmd); - struct Visitor - { - void operator()(const ERM::Ttrigger& t) const - { - Trigger trig; - trig.line = l; - i->triggers[ TriggerType(t.name) ].push_back(trig); - } - void operator()(const ERM::Tinstruction&) const - { - i->instructions.push_back(l); - } - void operator()(const ERM::Treceiver&) const {} - void operator()(const ERM::TPostTrigger& pt) const - { - Trigger trig; - trig.line = l; - i->postTriggers[ TriggerType(pt.name) ].push_back(trig); - } - const decltype(interpreter)& i; - const LinePointer& l; - }; - - Visitor v{interpreter, lp}; - std::visit(v, tcmd.cmd); - } - } -}; - - -ERMInterpreter::ERMInterpreter(vstd::CLoggerBase * logger_) - : logger(logger_) -{ - -} - -ERMInterpreter::~ERMInterpreter() -{ - -} - -bool ERMInterpreter::isATrigger( const ERM::TLine & line ) -{ - if(std::holds_alternative(line)) - { - TVExp vexp = std::get(line); - if(vexp.children.empty()) - return false; - - switch (getExpType(vexp.children[0])) - { - case SYMBOL: - return false; - break; - case TCMD: - return isCMDATrigger( std::get(vexp.children[0]) ); - break; - default: - return false; - break; - } - } - else if(std::holds_alternative(line)) - { - TERMline ermline = std::get(line); - return std::holds_alternative(ermline) && isCMDATrigger( std::get(ermline) ); - } - else - { - assert(0); - } - return false; -} - -ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) -{ - struct Visitor - { - ERM::EVOtions operator()(const boost::recursive_wrapper&) const - { - return ERM::EVOtions::VEXP; - } - ERM::EVOtions operator()(const ERM::TSymbol&) const - { - return ERM::EVOtions::SYMBOL; - } - ERM::EVOtions operator()(char) const - { - return ERM::EVOtions::CHAR; - } - ERM::EVOtions operator()(double) const - { - return ERM::EVOtions::DOUBLE; - } - ERM::EVOtions operator()(int) const - { - return ERM::EVOtions::INT; - } - ERM::EVOtions operator()(const ERM::Tcommand&) const - { - return ERM::EVOtions::TCMD; - } - ERM::EVOtions operator()(const ERM::TStringConstant&) const - { - return ERM::EVOtions::STRINGC; - } - }; - const Visitor v; - return std::visit(v, opt); -} - -bool ERMInterpreter::isCMDATrigger(const ERM::Tcommand & cmd) -{ - struct Visitor - { - bool operator()(const ERM::Ttrigger&) const { return true; } - bool operator()(const ERM::TPostTrigger&) const { return true; } - bool operator()(const ERM::Tinstruction&) const { return false; } - bool operator()(const ERM::Treceiver&) const { return false; } - }; - const Visitor v; - return std::visit(v, cmd.cmd); -} - -ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) -{ - return scripts.find(linePtr)->second; -} - -ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) -{ - if(std::holds_alternative(line)) - { - ERM::TERMline &tl = std::get(line); - if(std::holds_alternative(tl)) - { - ERM::Tcommand &tcm = std::get(tl); - if(std::holds_alternative(tcm.cmd)) - { - return std::get(tcm.cmd); - } - else if(std::holds_alternative(tcm.cmd)) - { - return std::get(tcm.cmd); - } - throw ELineProblem("Given line is not a trigger!"); - } - throw ELineProblem("Given line is not a command!"); - } - throw ELineProblem("Given line is not an ERM trigger!"); -} - -std::string ERMInterpreter::loadScript(const std::string & name, const std::string & source) -{ - CERMPreprocessor preproc(source); - - const bool isVERM = preproc.version == CERMPreprocessor::Version::VERM; - - ERMParser ep; - - std::vector buf = ep.parseFile(preproc); - - for(int g=0; g(buf.size()), g, buf[g].realLineNum)] = buf[g].tl; - - for(auto p : scripts) - std::visit(ScriptScanner(this, p.first), p.second); - - std::stringstream out; - - out << "local ERM = require(\"core:erm\")" << std::endl; - - if(isVERM) - { - out << "local VERM = require(\"core:verm\")" << std::endl; - } - - out << "local _" << std::endl; - out << "local v, w, z, F, M, Q = ERM.v, ERM.w, ERM.z, ERM.F, ERM.M, ERM.Q" << std::endl; - - ERMConverter::convertInstructions(out, this); - - for(const auto & p : triggers) - { - const VERMInterpreter::TriggerType & tt = p.first; - - if(tt.type == VERMInterpreter::TriggerType::FU) - { - ERMConverter::convertFunctions(out, this, p.second); - } - else - { - ERMConverter::convertTriggers(out, this, tt, p.second); - } - } - - for(const auto & p : postTriggers) - ;//TODO:postTriggers - - out << "ERM:callInstructions(instructions)" << std::endl; - - return out.str(); -} - -namespace VERMInterpreter -{ - VOption convertToVOption(const ERM::TVOption & tvo) - { - struct OptionConverterVisitor - { - VOption operator()(const boost::recursive_wrapper& cmd) const - { - return boost::recursive_wrapper(VNode(cmd.get())); - } - VOption operator()(const ERM::TSymbol & cmd) const - { - if(cmd.symModifier.empty()) - return VSymbol(cmd.sym); - else - return boost::recursive_wrapper(VNode(cmd)); - } - VOption operator()(const char & cmd) const - { - return TLiteral(cmd); - } - VOption operator()(const double & cmd) const - { - return TLiteral(cmd); - } - VOption operator()(const int & cmd) const - { - return TLiteral(cmd); - } - VOption operator()(const ERM::Tcommand & cmd) const - { - return cmd; - } - VOption operator()(const ERM::TStringConstant & cmd) const - { - return TLiteral(cmd.str); - } - }; - return std::visit(OptionConverterVisitor(), tvo); - } - - VNode::VNode( const ERM::TVExp & exp ) - { - for(int i=0; i & modifierList, bool asSymbol ) - { - for(int g=0; g0; i--) - { - children[i] = children[i-1]; - } - } - else - { - children.cdr() = VNode(children); - } - - if(modifierList[g] == "`") - { - children.car() = VSymbol("`"); - } - else if(modifierList[g] == ",!") - { - children.car() = VSymbol("comma-unlist"); - } - else if(modifierList[g] == ",") - { - children.car() = VSymbol(","); - } - else if(modifierList[g] == "#'") - { - children.car() = VSymbol("get-func"); - } - else if(modifierList[g] == "'") - { - children.car() = VSymbol("'"); - } - else - throw EInterpreterError("Incorrect value of modifier!"); - } - } - - VermTreeIterator & VermTreeIterator::operator=( const VOption & opt ) - { - switch (state) - { - case CAR: - if(parent->size() <= basePos) - parent->push_back(opt); - else - (*parent)[basePos] = opt; - break; - case NORM: - parent->resize(basePos+1); - (*parent)[basePos] = opt; - break; - default://should never happen - break; - } - return *this; - } - - VermTreeIterator & VermTreeIterator::operator=( const std::vector & opt ) - { - switch (state) - { - case CAR: - //TODO: implement me - break; - case NORM: - parent->resize(basePos+1); - parent->insert(parent->begin()+basePos, opt.begin(), opt.end()); - break; - default://should never happen - break; - } - return *this; - } - VermTreeIterator & VermTreeIterator::operator=( const VOptionList & opt ) - { - return *this = opt; - } - VOption & VermTreeIterator::getAsItem() - { - if(state == CAR) - return (*parent)[basePos]; - else - throw EInterpreterError("iterator is not in car state, cannot get as list"); - } - - size_t VermTreeIterator::size() const - { - return parent->size() - basePos; - } - - VERMInterpreter::VOptionList VermTreeIterator::getAsList() - { - VOptionList ret; - for(int g = basePos; gsize(); ++g) - { - ret.push_back((*parent)[g]); - } - return ret; - } - - VermTreeIterator VOptionList::cdr() - { - VermTreeIterator ret(*this); - ret.basePos = 1; - return ret; - } - - VermTreeIterator VOptionList::car() - { - VermTreeIterator ret(*this); - ret.state = VermTreeIterator::CAR; - return ret; - } - -} +/* + * ERMInterpreter.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 "ERMInterpreter.h" + +#include + +namespace spirit = boost::spirit; +using ::scripting::ContextBase; +using namespace ::VERMInterpreter; + +typedef int TUnusedType; + +namespace ERMConverter +{ + //console printer + using namespace ERM; + + static const std::map CMP_OPERATION = + { + {"<", "<"}, + {">", ">"}, + {">=", ">="}, + {"=>", ">="}, + {"<=", "<="}, + {"=<", "<="}, + {"=", "=="}, + {"<>", "~="}, + {"><", "~="}, + }; + + + struct Variable + { + std::string name = ""; + std::string macro = ""; + int index = 0; + + Variable(const std::string & name_, int index_) + { + name = name_; + index = index_; + } + + Variable(const std::string & macro_) + { + macro = macro_; + } + + bool isEmpty() const + { + return (name == "") && (macro == ""); + } + + bool isMacro() const + { + return (name == "") && (macro != ""); + } + + bool isSpecial() const + { + return (name.size() > 0) && (name[0] == 'd'); + } + + std::string str() const + { + if(isEmpty()) + { + return std::to_string(index); + } + else if(isMacro()) + { + return boost::to_string(boost::format("M['%s']") % macro); + } + else if(isSpecial() && (name.size() == 1)) + { + boost::format fmt; + fmt.parse("{'d', %d}"); + fmt % index; + return fmt.str(); + } + else if(isSpecial() && (name.size() != 1)) + { + + std::string ret; + + { + boost::format fmt; + + if(index == 0) + { + fmt.parse("Q['%s']"); + fmt % name[name.size()-1]; + } + else + { + fmt.parse("%s['%d']"); + fmt % name[name.size()-1] % index; + } + ret = fmt.str(); + } + + for(int i = ((int) name.size())-2; i > 0; i--) + { + boost::format fmt("%s[tostring(%s)]"); + + fmt % name[i] % ret; + + ret = fmt.str(); + } + + { + boost::format fmt; + fmt.parse("{'d', %s}"); + fmt % ret; + return fmt.str(); + } + } + else + { + std::string ret; + { + boost::format fmt; + + if(index == 0) + { + fmt.parse("Q['%s']"); + fmt % name[name.size()-1]; + } + else + { + fmt.parse("%s['%d']"); + fmt % name[name.size()-1] % index; + } + ret = fmt.str(); + } + + for(int i = ((int) name.size())-2; i >= 0; i--) + { + boost::format fmt("%s[tostring(%s)]"); + + fmt % name[i] % ret; + + ret = fmt.str(); + } + + return ret; + } + } + }; + + struct LVL2IexpToVar + { + LVL2IexpToVar() = default; + + Variable operator()(const TVarExpNotMacro & val) const + { + if(val.val.has_value()) + return Variable(val.varsym, *val.val); + else + return Variable(val.varsym, 0); + } + + Variable operator()(const TMacroUsage & val) const + { + return Variable(val.macro); + } + }; + + struct LVL1IexpToVar + { + LVL1IexpToVar() = default; + + Variable operator()(const int & constant) const + { + return Variable("", constant); + } + + Variable operator()(const TVarExp & var) const + { + return std::visit(LVL2IexpToVar(), var); + } + }; + + struct Condition + { + std::string operator()(const TComparison & cmp) const + { + Variable lhs = std::visit(LVL1IexpToVar(), cmp.lhs); + Variable rhs = std::visit(LVL1IexpToVar(), cmp.rhs); + + auto sign = CMP_OPERATION.find(cmp.compSign); + if(sign == std::end(CMP_OPERATION)) + throw EScriptExecError(std::string("Wrong comparison sign: ") + cmp.compSign); + + boost::format fmt("(%s %s %s)"); + fmt % lhs.str() % sign->second % rhs.str(); + return fmt.str(); + } + std::string operator()(const int & flag) const + { + return boost::to_string(boost::format("F['%d']") % flag); + } + }; + + struct ParamIO + { + ParamIO() = default; + std::string name = ""; + bool isInput = false; + bool semi = false; + std::string semiCmpSign = ""; + }; + + struct Converter + { + mutable std::ostream * out; + Converter(std::ostream * out_) + : out(out_) + {} + protected: + + void put(const std::string & text) const + { + (*out) << text; + } + + void putLine(const std::string & line) const + { + (*out) << line << std::endl; + } + + void endLine() const + { + (*out) << std::endl; + } + }; + + struct GetBodyOption + { + virtual std::string operator()(const TVarConcatString & cmp) const + { + throw EScriptExecError("String concatenation not allowed in this receiver"); + } + virtual std::string operator()(const TStringConstant & cmp) const + { + throw EScriptExecError("String constant not allowed in this receiver"); + } + virtual std::string operator()(const TCurriedString & cmp) const + { + throw EScriptExecError("Curried string not allowed in this receiver"); + } + virtual std::string operator()(const TSemiCompare & cmp) const + { + throw EScriptExecError("Semi comparison not allowed in this receiver"); + } + virtual std::string operator()(const TMacroDef & cmp) const + { + throw EScriptExecError("Macro definition not allowed in this receiver"); + } + virtual std::string operator()(const TIexp & cmp) const + { + throw EScriptExecError("i-expression not allowed in this receiver"); + } + virtual std::string operator()(const TVarpExp & cmp) const + { + throw EScriptExecError("Varp expression not allowed in this receiver"); + } + virtual std::string operator()(const spirit::unused_type & cmp) const + { + throw EScriptExecError("\'Nothing\' not allowed in this receiver"); + } + }; + + struct BodyOption + { + ParamIO operator()(const TVarConcatString & cmp) const + { + throw EScriptExecError(std::string("String concatenation not allowed in this receiver|")+cmp.string.str+"|"); + } + + ParamIO operator()(const TStringConstant & cmp) const + { + boost::format fmt("[===[%s]===]"); + fmt % cmp.str; + + ParamIO ret; + ret.isInput = true; + ret.name = fmt.str(); + return ret; + } + + ParamIO operator()(const TCurriedString & cmp) const + { + throw EScriptExecError("Curried string not allowed in this receiver"); + } + + ParamIO operator()(const TSemiCompare & cmp) const + { + ParamIO ret; + ret.isInput = false; + ret.semi = true; + ret.semiCmpSign = cmp.compSign; + ret.name = (std::visit(LVL1IexpToVar(), cmp.rhs)).str(); + return ret; + } + + ParamIO operator()(const TMacroDef & cmp) const + { + throw EScriptExecError("Macro definition not allowed in this receiver"); + } + + ParamIO operator()(const TIexp & cmp) const + { + ParamIO ret; + ret.isInput = true; + ret.name = (std::visit(LVL1IexpToVar(), cmp)).str();; + return ret; + } + + ParamIO operator()(const TVarpExp & cmp) const + { + ParamIO ret; + ret.isInput = false; + + ret.name = (std::visit(LVL2IexpToVar(), cmp.var)).str(); + return ret; + } + + ParamIO operator()(const spirit::unused_type & cmp) const + { + throw EScriptExecError("\'Nothing\' not allowed in this receiver"); + } + }; + + struct Receiver : public Converter + { + Receiver(std::ostream * out_) + : Converter(out_) + {} + + virtual void operator()(const TVRLogic & trig) const + { + throw EInterpreterError("VR logic is not allowed in this receiver!"); + } + + virtual void operator()(const TVRArithmetic & trig) const + { + throw EInterpreterError("VR arithmetic is not allowed in this receiver!"); + } + + virtual void operator()(const TNormalBodyOption & trig) const + { + throw EInterpreterError("Normal body is not allowed in this receiver!"); + } + + }; + + struct GenericReceiver : public Receiver + { + std::string name; + bool specialSemiCompare = false; + + GenericReceiver(std::ostream * out_, const std::string & name_, bool specialSemiCompare_) + : Receiver(out_), + name(name_), + specialSemiCompare(specialSemiCompare_) + {} + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & trig) const override + { + std::string outParams; + std::string inParams; + + std::string semiCompareDecl; + + std::vector compares; + + bool hasOutput = false; + bool hasSemiCompare = false; + + std::vector optionParams; + + if(trig.params.has_value()) + { + for(auto & p : *trig.params) + optionParams.push_back(std::visit(BodyOption(), p)); + } + + int idx = 1; + int fidx = 1; + + for(const ParamIO & p : optionParams) + { + if(p.isInput) + { + if(outParams.empty()) + outParams = "_"; + else + outParams += ", _"; + + inParams += ", "; + inParams += p.name; + } + else if(p.semi) + { + hasOutput = true; + hasSemiCompare = true; + + std::string tempVar = std::string("s")+std::to_string(idx); + + if(semiCompareDecl.empty()) + { + semiCompareDecl = "local "+tempVar; + } + else + { + semiCompareDecl += ", "; + semiCompareDecl += tempVar; + } + + if(outParams.empty()) + { + outParams = tempVar; + } + else + { + outParams += ", "; + outParams += tempVar; + } + + inParams += ", nil"; + + + auto sign = CMP_OPERATION.find(p.semiCmpSign); + if(sign == std::end(CMP_OPERATION)) + throw EScriptExecError(std::string("Wrong comparison sign: ") + p.semiCmpSign); + + boost::format cmpFmt("F[%d] = (%s %s %s)"); + cmpFmt % fidx % p.name % sign->second % tempVar; + compares.push_back(cmpFmt.str()); + + fidx++; + } + else + { + hasOutput = true; + + if(outParams.empty()) + { + outParams = p.name; + } + else + { + outParams += ", "; + outParams += p.name; + } + + inParams += ", nil"; + } + + idx++; + } + + if(hasSemiCompare) + { + putLine(semiCompareDecl); + } + + boost::format callFormat; + + if(hasOutput) + { + callFormat.parse("%s = %s:%s(x%s)"); + callFormat % outParams; + } + else + { + callFormat.parse("%s:%s(x%s)"); + } + + callFormat % name; + callFormat % trig.optionCode; + callFormat % inParams; + + putLine(callFormat.str()); + + for(auto & str : compares) + putLine(str); + } + }; + + struct FU : public Receiver + { + Variable v; + + FU(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + FU(std::ostream * out_) + : Receiver(out_), + v("", 0) + { + } + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & trig) const override + { + switch(trig.optionCode) + { + case 'E': + { + putLine("do return end"); + } + break; + default: + throw EInterpreterError("Unknown opcode in FU receiver"); + break; + } + } + }; + + struct MC_S : public GetBodyOption + { + MC_S() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TMacroDef & cmp) const override + { + return cmp.macro; + } + }; + + struct MC : public Receiver + { + Variable v; + + MC(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + MC(std::ostream * out_) + : Receiver(out_), + v("", 0) + { + } + + using Receiver::operator(); + + void operator()(const TNormalBodyOption & option) const override + { + switch(option.optionCode) + { + case 'S': + { + if(option.params.has_value()) + { + for(auto & p : *option.params) + { + std::string macroName = std::visit(MC_S(), p); + + boost::format callFormat; + + if(v.isEmpty()) + { + callFormat.parse("ERM:addMacro('%s', 'v', '%s')"); + callFormat % macroName % macroName; + } + else + { + callFormat.parse("ERM:addMacro('%s', '%s', '%d')"); + callFormat % macroName % v.name % v.index; + } + + putLine(callFormat.str()); + } + } + } + break; + default: + throw EInterpreterError("Unknown opcode in MC receiver"); + break; + } + } + }; + + struct VR_S : public GetBodyOption + { + VR_S() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + auto v = std::visit(LVL1IexpToVar(), cmp); + return v.str(); + } + std::string operator()(const TStringConstant & cmp) const override + { + boost::format fmt("[===[%s]===]"); + fmt % cmp.str; + return fmt.str(); + } + }; + + struct VR_H : public GetBodyOption + { + VR_H() + {} + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + Variable p = std::visit(LVL1IexpToVar(), cmp); + + if(p.index <= 0) + throw EScriptExecError("VR:H requires flag index"); + + if(p.name != "") + throw EScriptExecError("VR:H accept only flag index"); + + + boost::format fmt("'%d'"); + fmt % p.index; + return fmt.str(); + } + }; + + struct VR_X : public GetBodyOption + { + VR_X() + { + } + + using GetBodyOption::operator(); + + std::string operator()(const TIexp & cmp) const override + { + Variable p = std::visit(LVL1IexpToVar(), cmp); + + return p.str(); + } + }; + + struct VR : public Receiver + { + Variable v; + + VR(std::ostream * out_, const ERM::TIexp & tid) + : Receiver(out_), + v(std::visit(LVL1IexpToVar(), tid)) + { + } + + using Receiver::operator(); + + void operator()(const TVRLogic & trig) const override + { + Variable rhs = std::visit(LVL1IexpToVar(), trig.var); + + std::string opcode; + + switch (trig.opcode) + { + case '&': + opcode = "bit.band"; + break; + case '|': + opcode = "bit.bor"; + break; + default: + throw EInterpreterError("Wrong opcode in VR logic expression!"); + break; + } + + boost::format fmt("%s = %s(%s, %s)"); + fmt % v.str() % opcode % v.str() % rhs.str(); + putLine(fmt.str()); + } + + void operator()(const TVRArithmetic & trig) const override + { + Variable rhs = std::visit(LVL1IexpToVar(), trig.rhs); + + std::string opcode; + + switch (trig.opcode) + { + case '+': + opcode = v.name[0] == 'z' ? ".." : "+"; + break; + case '-': + case '*': + case '%': + opcode = trig.opcode; + break; + case ':': + opcode = "/"; + break; + default: + throw EInterpreterError("Wrong opcode in VR arithmetic!"); + break; + } + + boost::format fmt("%s = %s %s %s"); + fmt % v.str() % v.str() % opcode % rhs.str(); + putLine(fmt.str()); + } + + void operator()(const TNormalBodyOption & trig) const override + { + switch(trig.optionCode) + { + case 'C': //setting/checking v vars + { + if(v.index <= 0) + throw EScriptExecError("VR:C requires indexed variable"); + + std::vector optionParams; + + if(trig.params.has_value()) + { + for(auto & p : *trig.params) + optionParams.push_back(std::visit(BodyOption(), p)); + } + + auto index = v.index; + + for(auto & p : optionParams) + { + boost::format fmt; + if(p.isInput) + fmt.parse("%s['%d'] = %s") % v.name % index % p.name; + else + fmt.parse("%s = %s['%d']") % p.name % v.name % index; + putLine(fmt.str()); + index++; + } + } + break; + case 'H': //checking if string is empty + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:H option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_H(), (*trig.params)[0]); + boost::format fmt("ERM.VR(%s):H(%s)"); + fmt % v.str() % opt; + putLine(fmt.str()); + } + break; + case 'U': + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:H/U need 1 parameter!"); + + std::string opt = std::visit(VR_S(), (*trig.params)[0]); + boost::format fmt("ERM.VR(%s):%c(%s)"); + fmt % v.str() % (trig.optionCode) % opt; + putLine(fmt.str()); + } + break; + case 'M': //string operations + { + if(!trig.params.has_value() || trig.params->size() < 2) + throw EScriptExecError("VR:M needs at least 2 parameters!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + int paramIndex = 1; + + if(opt == "3") + { + boost::format fmt("%s = ERM.VR(%s):M3("); + fmt % v.str() % v.str(); + put(fmt.str()); + } + else + { + auto target = std::visit(VR_X(), (*trig.params)[paramIndex++]); + + boost::format fmt("%s = ERM.VR(%s):M%s("); + fmt % target % v.str() % opt; + put(fmt.str()); + } + + for(int i = paramIndex; i < trig.params->size(); i++) + { + opt = std::visit(VR_X(), (*trig.params)[i]); + if(i > paramIndex) put(","); + put(opt); + } + + putLine(")"); + } + break; + case 'X': //bit xor + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:X option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + + boost::format fmt("%s = bit.bxor(%s, %s)"); + fmt % v.str() % v.str() % opt;putLine(fmt.str()); + } + break; + case 'R': //random variables + { + //TODO + putLine("--VR:R not implemented"); + } + break; + case 'S': //setting variable + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:S option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_S(), (*trig.params)[0]); + put(v.str()); + put(" = "); + put(opt); + endLine(); + } + break; + case 'T': //random variables + { + //TODO + putLine("--VR:T not implemented"); + } + break; + case 'V': //convert string to value + { + if(!trig.params.has_value() || trig.params->size() != 1) + throw EScriptExecError("VR:V option takes exactly 1 parameter!"); + + std::string opt = std::visit(VR_X(), (*trig.params)[0]); + boost::format fmt("%s = tostring(%s)"); + fmt % v.str() % opt; + putLine(fmt.str()); + } + break; + default: + throw EScriptExecError("Wrong VR receiver option!"); + break; + } + } + }; + + + struct ERMExp : public Converter + { + ERMExp(std::ostream * out_) + : Converter(out_) + {} + + template + void performBody(const std::optional & body, const Visitor & visitor) const + { + if(body.has_value()) + { + const ERM::Tbody & bo = *body; + for(int g=0; g & identifier, const std::optional & body) const + { + if(name == "VR") + { + if(!identifier.has_value()) + throw EScriptExecError("VR receiver requires arguments"); + + ERM::Tidentifier tid = identifier.value(); + if(tid.size() != 1) + throw EScriptExecError("VR receiver takes exactly 1 argument"); + + performBody(body, VR(out, tid[0])); + } + else if(name == "re") + { + if(!identifier.has_value()) + throw EScriptExecError("re receiver requires arguments"); + + ERM::Tidentifier tid = identifier.value(); + + auto argc = tid.size(); + + if(argc > 0) + { + std::string loopCounter = (std::visit(LVL1IexpToVar(), tid.at(0))).str(); + + std::string startVal = argc > 1 ? (std::visit(LVL1IexpToVar(), tid.at(1))).str() : loopCounter; + std::string stopVal = argc > 2 ? (std::visit(LVL1IexpToVar(), tid.at(2))).str() : loopCounter; + std::string increment = argc > 3 ? (std::visit(LVL1IexpToVar(), tid.at(3))).str() : "1"; + + boost::format fmt("for __iter = %s, %s, %s do"); + + + fmt % startVal % stopVal % increment; + putLine(fmt.str()); + fmt.parse("%s = __iter"); + fmt % loopCounter; + putLine(fmt.str()); + } + else + { + throw EScriptExecError("re receiver requires arguments"); + } + } + else if(name == "FU" && !identifier.has_value()) + { + performBody(body, FU(out)); //assume FU:E + } + else if(name == "MC") + { + if(identifier.has_value()) + { + ERM::Tidentifier tid = identifier.value(); + if(tid.size() != 1) + throw EScriptExecError("MC receiver takes no more than 1 argument"); + + performBody(body, MC(out, tid[0])); + } + else + { + performBody(body, MC(out)); + } + } + else + { + std::vector identifiers; + + if(identifier.has_value()) + { + for(const auto & id : identifier.value()) + { + Variable v = std::visit(LVL1IexpToVar(), id); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in receiver identifier"); + identifiers.push_back(v.str()); + } + } + + std::string params; + + for(auto iter = std::begin(identifiers); iter != std::end(identifiers); ++iter) + { + if(!params.empty()) + params += ", "; + params += *iter; + } + + if(body.has_value()) + { + const ERM::Tbody & bo = *body; + if(bo.size() == 1) + { + boost::format fmt("ERM.%s(%s)"); + fmt % name; + fmt % params; + + GenericReceiver gr(out, fmt.str(), (name == "DO")); + std::visit(gr,bo[0]); + } + else + { + putLine("do"); + boost::format fmt("local %s = ERM.%s(%s)"); + fmt % name; + fmt % name; + fmt % params; + + putLine(fmt.str()); + + performBody(body, GenericReceiver(out, name, (name=="DO") )); + + putLine("end"); + } + } + else + { + //is it an error? + logMod->warn("ERM receiver '%s %s' w/o body", name, params); + } + + + } + } + + void convertConditionInner(const Tcondition & cond, char op) const + { + std::string lhs = std::visit(Condition(), cond.cond); + + if(cond.ctype != '/') + op = cond.ctype; + + switch (op) + { + case '&': + put(" and "); + break; + case '|': + put(" or "); + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + put(lhs); + + if(cond.rhs.has_value()) + { + switch (op) + { + case '&': + case '|': + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + convertConditionInner(cond.rhs->get(), op); + } + } + + void convertCondition(const Tcondition & cond) const + { + //&c1/c2/c3|c4/c5/c6 -> (c1 & c2 & c3) | c4 | c5 | c6 + std::string lhs = std::visit(Condition(), cond.cond); + put("if "); + put(lhs); + + if(cond.rhs.has_value()) + { + switch (cond.ctype) + { + case '&': + case '|': + break; + default: + throw EInterpreterProblem(std::string("Wrong condition connection (") + cond.ctype + ") !"); + break; + } + + convertConditionInner(cond.rhs->get(), cond.ctype); + } + + putLine(" then "); + } + + void convertReceiverOrInstruction(const std::optional & condition, + const std::string & name, + const std::optional & identifier, + const std::optional & body) const + { + if(name=="if") + { + if(condition.has_value()) + convertCondition(*condition); + else + putLine("if true then"); + } + else if(name=="el") + { + putLine("else"); + } + else if(name=="en") + { + putLine("end"); + } + else + { + if(condition.has_value()) + { + convertCondition(*condition); + convert(name, identifier, body); + putLine("end"); + } + else + { + convert(name, identifier, body); + } + } + } + + void operator()(const Ttrigger & trig) const + { + throw EInterpreterError("Triggers cannot be executed!"); + } + + void operator()(const TPostTrigger & trig) const + { + throw EInterpreterError("Post-triggers cannot be executed!"); + } + + void operator()(const Tinstruction & trig) const + { + convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, std::make_optional(trig.body)); + } + + void operator()(const Treceiver & trig) const + { + convertReceiverOrInstruction(trig.condition, trig.name, trig.identifier, trig.body); + } + }; + + struct Command : public Converter + { + Command(std::ostream * out_) + : Converter(out_) + {} + + void operator()(const Tcommand & cmd) const + { + std::visit(ERMExp(out), cmd.cmd); + } + void operator()(const std::string & comment) const + { + (*out) << "-- " << comment; + endLine(); + } + + void operator()(const spirit::unused_type &) const + { + } + }; + + struct TLiteralEval + { + + std::string operator()(const char & val) + { + return "{\"'\",'"+ std::to_string(val) +"'}"; + } + std::string operator()(const double & val) + { + return std::to_string(val); + } + std::string operator()(const int & val) + { + return std::to_string(val); + } + std::string operator()(const std::string & val) + { + return "{\"'\",[===[" + val + "]===]}"; + } + }; + + struct VOptionEval : public Converter + { + VOptionEval(std::ostream * out_) + : Converter(out_) + {} + + void operator()(VNIL const & opt) const + { + (*out) << "{}"; + } + void operator()(const boost::recursive_wrapper & opt) const; + + void operator()(const VSymbol & opt) const + { + (*out) << "\"" << opt.text << "\""; + } + void operator()(const TLiteral & opt) const + { + TLiteralEval tmp; + (*out) << std::visit(tmp, opt); + } + void operator()(const ERM::Tcommand & opt) const + { + //this is how FP works, evaluation == producing side effects + //TODO: can we evaluate to smth more useful? + //??? + throw EVermScriptExecError("Using ERM options in VERM expression is not (yet) allowed"); +// std::visit(ERMExp(out), opt.cmd); + } + }; + + void VOptionEval::operator()(const boost::recursive_wrapper & opt) const + { + VNode tmpn(opt.get()); + + (*out) << "{"; + + for(VOption & op : tmpn.children) + { + std::visit(VOptionEval(out), op); + (*out) << ","; + } + (*out) << "}"; + } + + struct Line : public Converter + { + Line(std::ostream * out_) + : Converter(out_) + {} + + void operator()(const TVExp & cmd) const + { + put("VERM:E"); + + VNode line(cmd); + + VOptionEval eval(out); + eval(line); + + + endLine(); + } + void operator()(const TERMline & cmd) const + { + std::visit(Command(out), cmd); + } + }; + + void convertInstructions(std::ostream & out, ERMInterpreter * owner) + { + out << "local function instructions()" << std::endl; + out << "local e, x, y = {}, {}, {}" << std::endl; + + Line lineConverter(&out); + + for(const LinePointer & lp : owner->instructions) + { + ERM::TLine & line = owner->retrieveLine(lp); + + std::visit(lineConverter, line); + } + + out << "end" << std::endl; + } + + void convertFunctions(std::ostream & out, ERMInterpreter * owner, const std::vector & triggers) + { + Line lineConverter(&out); + + for(const VERMInterpreter::Trigger & trigger : triggers) + { + ERM::TLine & firstLine = owner->retrieveLine(trigger.line); + + const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); + + //TODO: condition + + out << "ERM:addTrigger({" << std::endl; + + if(!trig.identifier.has_value()) + throw EInterpreterError("Function must have identifier"); + + ERM::Tidentifier tid = trig.identifier.value(); + + if(tid.empty()) + throw EInterpreterError("Function must have identifier"); + + Variable v = std::visit(LVL1IexpToVar(), tid[0]); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in function definition"); + + out << "id = {" << v.str() << "}," << std::endl; + out << "name = 'FU'," << std::endl; + out << "fn = function (e, y, x)" << std::endl; + out << "local _" << std::endl; + + LinePointer lp = trigger.line; + ++lp; + + for(; lp.isValid(); ++lp) + { + ERM::TLine curLine = owner->retrieveLine(lp); + if(owner->isATrigger(curLine)) + break; + + std::visit(lineConverter, curLine); + } + + out << "end," << std::endl; + out << "})" << std::endl; + } + } + + void convertTriggers(std::ostream & out, ERMInterpreter * owner, const VERMInterpreter::TriggerType & type, const std::vector & triggers) + { + Line lineConverter(&out); + + for(const VERMInterpreter::Trigger & trigger : triggers) + { + ERM::TLine & firstLine = owner->retrieveLine(trigger.line); + + const ERM::TTriggerBase & trig = ERMInterpreter::retrieveTrigger(firstLine); + + //TODO: condition + + out << "ERM:addTrigger({" << std::endl; + + std::vector identifiers; + + if(trig.identifier.has_value()) + { + for(const auto & id : trig.identifier.value()) + { + Variable v = std::visit(LVL1IexpToVar(), id); + + if(v.isSpecial()) + throw ELineProblem("Special variable syntax ('d') is not allowed in trigger definition"); + identifiers.push_back(v.str()); + } + } + + out << "id = {"; + for(const auto & id : identifiers) + out << "'" << id << "',"; + out << "}," << std::endl; + + out << "name = '" << trig.name << "'," << std::endl; + out << "fn = function (e, y)" << std::endl; + out << "local _" << std::endl; + + LinePointer lp = trigger.line; + ++lp; + + for(; lp.isValid(); ++lp) + { + ERM::TLine curLine = owner->retrieveLine(lp); + if(owner->isATrigger(curLine)) + break; + + std::visit(lineConverter, curLine); + } + + out << "end," << std::endl; + out << "})" << std::endl; + } + } +} + +struct ScriptScanner +{ + ERMInterpreter * interpreter; + LinePointer lp; + + ScriptScanner(ERMInterpreter * interpr, const LinePointer & _lp) : interpreter(interpr), lp(_lp) + {} + + void operator()(const TVExp & cmd) const + { + // + } + void operator()(const TERMline & cmd) const + { + if(std::holds_alternative(cmd)) //TCommand + { + Tcommand tcmd = std::get(cmd); + struct Visitor + { + void operator()(const ERM::Ttrigger& t) const + { + Trigger trig; + trig.line = l; + i->triggers[ TriggerType(t.name) ].push_back(trig); + } + void operator()(const ERM::Tinstruction&) const + { + i->instructions.push_back(l); + } + void operator()(const ERM::Treceiver&) const {} + void operator()(const ERM::TPostTrigger& pt) const + { + Trigger trig; + trig.line = l; + i->postTriggers[ TriggerType(pt.name) ].push_back(trig); + } + const decltype(interpreter)& i; + const LinePointer& l; + }; + + Visitor v{interpreter, lp}; + std::visit(v, tcmd.cmd); + } + } +}; + + +ERMInterpreter::ERMInterpreter(vstd::CLoggerBase * logger_) + : logger(logger_) +{ + +} + +ERMInterpreter::~ERMInterpreter() +{ + +} + +bool ERMInterpreter::isATrigger( const ERM::TLine & line ) +{ + if(std::holds_alternative(line)) + { + TVExp vexp = std::get(line); + if(vexp.children.empty()) + return false; + + switch (getExpType(vexp.children[0])) + { + case SYMBOL: + return false; + break; + case TCMD: + return isCMDATrigger( std::get(vexp.children[0]) ); + break; + default: + return false; + break; + } + } + else if(std::holds_alternative(line)) + { + TERMline ermline = std::get(line); + return std::holds_alternative(ermline) && isCMDATrigger( std::get(ermline) ); + } + else + { + assert(0); + } + return false; +} + +ERM::EVOtions ERMInterpreter::getExpType(const ERM::TVOption & opt) +{ + struct Visitor + { + ERM::EVOtions operator()(const boost::recursive_wrapper&) const + { + return ERM::EVOtions::VEXP; + } + ERM::EVOtions operator()(const ERM::TSymbol&) const + { + return ERM::EVOtions::SYMBOL; + } + ERM::EVOtions operator()(char) const + { + return ERM::EVOtions::CHAR; + } + ERM::EVOtions operator()(double) const + { + return ERM::EVOtions::DOUBLE; + } + ERM::EVOtions operator()(int) const + { + return ERM::EVOtions::INT; + } + ERM::EVOtions operator()(const ERM::Tcommand&) const + { + return ERM::EVOtions::TCMD; + } + ERM::EVOtions operator()(const ERM::TStringConstant&) const + { + return ERM::EVOtions::STRINGC; + } + }; + const Visitor v; + return std::visit(v, opt); +} + +bool ERMInterpreter::isCMDATrigger(const ERM::Tcommand & cmd) +{ + struct Visitor + { + bool operator()(const ERM::Ttrigger&) const { return true; } + bool operator()(const ERM::TPostTrigger&) const { return true; } + bool operator()(const ERM::Tinstruction&) const { return false; } + bool operator()(const ERM::Treceiver&) const { return false; } + }; + const Visitor v; + return std::visit(v, cmd.cmd); +} + +ERM::TLine & ERMInterpreter::retrieveLine(const LinePointer & linePtr) +{ + return scripts.find(linePtr)->second; +} + +ERM::TTriggerBase & ERMInterpreter::retrieveTrigger(ERM::TLine & line) +{ + if(std::holds_alternative(line)) + { + ERM::TERMline &tl = std::get(line); + if(std::holds_alternative(tl)) + { + ERM::Tcommand &tcm = std::get(tl); + if(std::holds_alternative(tcm.cmd)) + { + return std::get(tcm.cmd); + } + else if(std::holds_alternative(tcm.cmd)) + { + return std::get(tcm.cmd); + } + throw ELineProblem("Given line is not a trigger!"); + } + throw ELineProblem("Given line is not a command!"); + } + throw ELineProblem("Given line is not an ERM trigger!"); +} + +std::string ERMInterpreter::loadScript(const std::string & name, const std::string & source) +{ + CERMPreprocessor preproc(source); + + const bool isVERM = preproc.version == CERMPreprocessor::Version::VERM; + + ERMParser ep; + + std::vector buf = ep.parseFile(preproc); + + for(int g=0; g(buf.size()), g, buf[g].realLineNum)] = buf[g].tl; + + for(auto p : scripts) + std::visit(ScriptScanner(this, p.first), p.second); + + std::stringstream out; + + out << "local ERM = require(\"core:erm\")" << std::endl; + + if(isVERM) + { + out << "local VERM = require(\"core:verm\")" << std::endl; + } + + out << "local _" << std::endl; + out << "local v, w, z, F, M, Q = ERM.v, ERM.w, ERM.z, ERM.F, ERM.M, ERM.Q" << std::endl; + + ERMConverter::convertInstructions(out, this); + + for(const auto & p : triggers) + { + const VERMInterpreter::TriggerType & tt = p.first; + + if(tt.type == VERMInterpreter::TriggerType::FU) + { + ERMConverter::convertFunctions(out, this, p.second); + } + else + { + ERMConverter::convertTriggers(out, this, tt, p.second); + } + } + + for(const auto & p : postTriggers) + ;//TODO:postTriggers + + out << "ERM:callInstructions(instructions)" << std::endl; + + return out.str(); +} + +namespace VERMInterpreter +{ + VOption convertToVOption(const ERM::TVOption & tvo) + { + struct OptionConverterVisitor + { + VOption operator()(const boost::recursive_wrapper& cmd) const + { + return boost::recursive_wrapper(VNode(cmd.get())); + } + VOption operator()(const ERM::TSymbol & cmd) const + { + if(cmd.symModifier.empty()) + return VSymbol(cmd.sym); + else + return boost::recursive_wrapper(VNode(cmd)); + } + VOption operator()(const char & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const double & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const int & cmd) const + { + return TLiteral(cmd); + } + VOption operator()(const ERM::Tcommand & cmd) const + { + return cmd; + } + VOption operator()(const ERM::TStringConstant & cmd) const + { + return TLiteral(cmd.str); + } + }; + return std::visit(OptionConverterVisitor(), tvo); + } + + VNode::VNode( const ERM::TVExp & exp ) + { + for(int i=0; i & modifierList, bool asSymbol ) + { + for(int g=0; g0; i--) + { + children[i] = children[i-1]; + } + } + else + { + children.cdr() = VNode(children); + } + + if(modifierList[g] == "`") + { + children.car() = VSymbol("`"); + } + else if(modifierList[g] == ",!") + { + children.car() = VSymbol("comma-unlist"); + } + else if(modifierList[g] == ",") + { + children.car() = VSymbol(","); + } + else if(modifierList[g] == "#'") + { + children.car() = VSymbol("get-func"); + } + else if(modifierList[g] == "'") + { + children.car() = VSymbol("'"); + } + else + throw EInterpreterError("Incorrect value of modifier!"); + } + } + + VermTreeIterator & VermTreeIterator::operator=( const VOption & opt ) + { + switch (state) + { + case CAR: + if(parent->size() <= basePos) + parent->push_back(opt); + else + (*parent)[basePos] = opt; + break; + case NORM: + parent->resize(basePos+1); + (*parent)[basePos] = opt; + break; + default://should never happen + break; + } + return *this; + } + + VermTreeIterator & VermTreeIterator::operator=( const std::vector & opt ) + { + switch (state) + { + case CAR: + //TODO: implement me + break; + case NORM: + parent->resize(basePos+1); + parent->insert(parent->begin()+basePos, opt.begin(), opt.end()); + break; + default://should never happen + break; + } + return *this; + } + VermTreeIterator & VermTreeIterator::operator=( const VOptionList & opt ) + { + return *this = opt; + } + VOption & VermTreeIterator::getAsItem() + { + if(state == CAR) + return (*parent)[basePos]; + else + throw EInterpreterError("iterator is not in car state, cannot get as list"); + } + + size_t VermTreeIterator::size() const + { + return parent->size() - basePos; + } + + VERMInterpreter::VOptionList VermTreeIterator::getAsList() + { + VOptionList ret; + for(int g = basePos; gsize(); ++g) + { + ret.push_back((*parent)[g]); + } + return ret; + } + + VermTreeIterator VOptionList::cdr() + { + VermTreeIterator ret(*this); + ret.basePos = 1; + return ret; + } + + VermTreeIterator VOptionList::car() + { + VermTreeIterator ret(*this); + ret.state = VermTreeIterator::CAR; + return ret; + } + +} diff --git a/scripting/erm/ERMInterpreter.h b/scripting/erm/ERMInterpreter.h index ed2550977..748b632e9 100644 --- a/scripting/erm/ERMInterpreter.h +++ b/scripting/erm/ERMInterpreter.h @@ -1,318 +1,318 @@ -/* - * ERMInterpreter.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 "ERMParser.h" -#include "ERMScriptModule.h" - -class ERMInterpreter; - -namespace VERMInterpreter -{ - using namespace ERM; - - //different exceptions that can be thrown during interpreting - class EInterpreterProblem : public std::exception - { - std::string problem; - public: - const char * what() const throw() override - { - return problem.c_str(); - } - ~EInterpreterProblem() throw() - {} - EInterpreterProblem(const std::string & problemDesc) : problem(problemDesc) - {} - }; - - struct ESymbolNotFound : public EInterpreterProblem - { - ESymbolNotFound(const std::string & sym) : - EInterpreterProblem(std::string("Symbol \"") + sym + std::string("\" not found!")) - {} - }; - - struct EInvalidTrigger : public EInterpreterProblem - { - EInvalidTrigger(const std::string & sym) : - EInterpreterProblem(std::string("Trigger \"") + sym + std::string("\" is invalid!")) - {} - }; - - struct EUsageOfUndefinedMacro : public EInterpreterProblem - { - EUsageOfUndefinedMacro(const std::string & macro) : - EInterpreterProblem(std::string("Macro ") + macro + " is undefined") - {} - }; - - struct EIexpProblem : public EInterpreterProblem - { - EIexpProblem(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - struct ELineProblem : public EInterpreterProblem - { - ELineProblem(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - struct EExecutionError : public EInterpreterProblem - { - EExecutionError(const std::string & desc) : - EInterpreterProblem(desc) - {} - }; - - //internal interpreter error related to execution - struct EInterpreterError : public EExecutionError - { - EInterpreterError(const std::string & desc) : - EExecutionError(desc) - {} - }; - - //wrong script - struct EScriptExecError : public EExecutionError - { - EScriptExecError(const std::string & desc) : - EExecutionError(desc) - {} - }; - - //wrong script - struct EVermScriptExecError : public EScriptExecError - { - EVermScriptExecError(const std::string & desc) : - EScriptExecError(desc) - {} - }; - - // All numeric variables are integer variables and have a range of -2147483647...+2147483647 - // c stores game active day number //indirect variable - // d current value //not an actual variable but a modifier - // e1..e100 Function floating point variables //local - // e-1..e-100 Trigger local floating point variables //local - // 'f'..'t' Standard variables ('quick variables') //global - // v1..v1000 Standard variables //global - // w1..w100 Hero variables - // w101..w200 Hero variables - // x1..x16 Function parameters //local - // y1..y100 Function local variables //local - // y-1..y-100 Trigger-based local integer variables //local - // z1..z1000 String variables //global - // z-1..z-10 Function local string variables //local - - - struct TriggerType - { - //the same order of trigger types in this enum and in validTriggers array is obligatory! - enum ETrigType - { - AE, BA, BF, BG, BR, CM, CO, FU, GE, GM, HE, HL, HM, IP, LE, MF, MG, MM, MR, MW, OB, PI, SN, TH, TM - }; - - ETrigType type; - - static ETrigType convertTrigger(const std::string & trig) - { - static const std::string validTriggers[] = - { - "AE", "BA", "BF", "BG", "BR", "CM", "CO", "FU", - "GE", "GM", "HE", "HL", "HM", "IP", "LE", "MF", "MG", "MM", "MR", "MW", "OB", "PI", "SN", - "TH", "TM" - }; - - for(int i=0; i(i); - } - throw EInvalidTrigger(trig); - } - - bool operator<(const TriggerType & t2) const - { - return type < t2.type; - } - - TriggerType(const std::string & sym) - { - type = convertTrigger(sym); - } - - }; - - struct LinePointer - { - int lineNum; - int realLineNum; - int fileLength; - - LinePointer() - : fileLength(-1) - {} - - LinePointer(int _fileLength, int line, int _realLineNum) - : fileLength(_fileLength), - lineNum(line), - realLineNum(_realLineNum) - {} - - bool operator<(const LinePointer & rhs) const - { - return lineNum < rhs.lineNum; - } - - bool operator!=(const LinePointer & rhs) const - { - return lineNum != rhs.lineNum; - } - LinePointer & operator++() - { - ++lineNum; - return *this; - } - bool isValid() const - { - return fileLength > 0 && lineNum < fileLength; - } - }; - - struct Trigger - { - LinePointer line; - Trigger() - {} - }; - - - //verm goodies - struct VSymbol - { - std::string text; - VSymbol(const std::string & txt) : text(txt) - {} - }; - - struct VNode; - struct VOptionList; - - struct VNIL - {}; - - - typedef std::variant TLiteral; - - typedef std::variant, VSymbol, TLiteral, ERM::Tcommand> VOption; //options in v-expression, VNIl should be the default - - template - T& getAs(SecType & opt) - { - if(opt.type() == typeid(T)) - return std::get(opt); - else - throw EVermScriptExecError("Wrong type!"); - } - - template - bool isA(const SecType & opt) - { - if(opt.type() == typeid(T)) - return true; - else - return false; - } - - struct VermTreeIterator - { - private: - friend struct VOptionList; - VOptionList * parent; - enum Estate {NORM, CAR} state; - int basePos; //car/cdr offset - public: - VermTreeIterator(VOptionList & _parent) : parent(&_parent), state(NORM), basePos(0) - {} - VermTreeIterator() : parent(nullptr), state(NORM) - {} - - VermTreeIterator & operator=(const VOption & opt); - VermTreeIterator & operator=(const std::vector & opt); - VermTreeIterator & operator=(const VOptionList & opt); - VOption & getAsItem(); - VOptionList getAsList(); - size_t size() const; - - VermTreeIterator& operator=(const VermTreeIterator & rhs) - { - if(this == &rhs) - { - return *this; - } - parent = rhs.parent; - state = rhs.state; - basePos = rhs.basePos; - - return *this; - } - }; - - struct VOptionList : public std::vector - { - private: - friend struct VermTreeIterator; - public: - VermTreeIterator car(); - VermTreeIterator cdr(); - }; - - struct VNode - { - private: - void processModifierList(const std::vector & modifierList, bool asSymbol); - public: - VOptionList children; - VNode( const ERM::TVExp & exp); - VNode( const VOptionList & cdren ); - VNode( const ERM::TSymbol & sym ); //only in case sym has modifiers! - VNode( const VOption & first, const VOptionList & rest); //merges given arguments into [a, rest]; - void setVnode( const VOption & first, const VOptionList & rest); - }; -} - -class ERMInterpreter -{ -/*not so*/ public: - - std::map scripts; - - typedef std::map > TtriggerListType; - TtriggerListType triggers; - TtriggerListType postTriggers; - std::vector instructions; - - static bool isCMDATrigger(const ERM::Tcommand & cmd); - static bool isATrigger(const ERM::TLine & line); - static ERM::EVOtions getExpType(const ERM::TVOption & opt); - ERM::TLine & retrieveLine(const VERMInterpreter::LinePointer & linePtr); - static ERM::TTriggerBase & retrieveTrigger(ERM::TLine & line); -public: - vstd::CLoggerBase * logger; - - ERMInterpreter(vstd::CLoggerBase * logger_); - virtual ~ERMInterpreter(); - - std::string loadScript(const std::string & name, const std::string & source); -}; +/* + * ERMInterpreter.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 "ERMParser.h" +#include "ERMScriptModule.h" + +class ERMInterpreter; + +namespace VERMInterpreter +{ + using namespace ERM; + + //different exceptions that can be thrown during interpreting + class EInterpreterProblem : public std::exception + { + std::string problem; + public: + const char * what() const throw() override + { + return problem.c_str(); + } + ~EInterpreterProblem() throw() + {} + EInterpreterProblem(const std::string & problemDesc) : problem(problemDesc) + {} + }; + + struct ESymbolNotFound : public EInterpreterProblem + { + ESymbolNotFound(const std::string & sym) : + EInterpreterProblem(std::string("Symbol \"") + sym + std::string("\" not found!")) + {} + }; + + struct EInvalidTrigger : public EInterpreterProblem + { + EInvalidTrigger(const std::string & sym) : + EInterpreterProblem(std::string("Trigger \"") + sym + std::string("\" is invalid!")) + {} + }; + + struct EUsageOfUndefinedMacro : public EInterpreterProblem + { + EUsageOfUndefinedMacro(const std::string & macro) : + EInterpreterProblem(std::string("Macro ") + macro + " is undefined") + {} + }; + + struct EIexpProblem : public EInterpreterProblem + { + EIexpProblem(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + struct ELineProblem : public EInterpreterProblem + { + ELineProblem(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + struct EExecutionError : public EInterpreterProblem + { + EExecutionError(const std::string & desc) : + EInterpreterProblem(desc) + {} + }; + + //internal interpreter error related to execution + struct EInterpreterError : public EExecutionError + { + EInterpreterError(const std::string & desc) : + EExecutionError(desc) + {} + }; + + //wrong script + struct EScriptExecError : public EExecutionError + { + EScriptExecError(const std::string & desc) : + EExecutionError(desc) + {} + }; + + //wrong script + struct EVermScriptExecError : public EScriptExecError + { + EVermScriptExecError(const std::string & desc) : + EScriptExecError(desc) + {} + }; + + // All numeric variables are integer variables and have a range of -2147483647...+2147483647 + // c stores game active day number //indirect variable + // d current value //not an actual variable but a modifier + // e1..e100 Function floating point variables //local + // e-1..e-100 Trigger local floating point variables //local + // 'f'..'t' Standard variables ('quick variables') //global + // v1..v1000 Standard variables //global + // w1..w100 Hero variables + // w101..w200 Hero variables + // x1..x16 Function parameters //local + // y1..y100 Function local variables //local + // y-1..y-100 Trigger-based local integer variables //local + // z1..z1000 String variables //global + // z-1..z-10 Function local string variables //local + + + struct TriggerType + { + //the same order of trigger types in this enum and in validTriggers array is obligatory! + enum ETrigType + { + AE, BA, BF, BG, BR, CM, CO, FU, GE, GM, HE, HL, HM, IP, LE, MF, MG, MM, MR, MW, OB, PI, SN, TH, TM + }; + + ETrigType type; + + static ETrigType convertTrigger(const std::string & trig) + { + static const std::string validTriggers[] = + { + "AE", "BA", "BF", "BG", "BR", "CM", "CO", "FU", + "GE", "GM", "HE", "HL", "HM", "IP", "LE", "MF", "MG", "MM", "MR", "MW", "OB", "PI", "SN", + "TH", "TM" + }; + + for(int i=0; i(i); + } + throw EInvalidTrigger(trig); + } + + bool operator<(const TriggerType & t2) const + { + return type < t2.type; + } + + TriggerType(const std::string & sym) + { + type = convertTrigger(sym); + } + + }; + + struct LinePointer + { + int lineNum; + int realLineNum; + int fileLength; + + LinePointer() + : fileLength(-1) + {} + + LinePointer(int _fileLength, int line, int _realLineNum) + : fileLength(_fileLength), + lineNum(line), + realLineNum(_realLineNum) + {} + + bool operator<(const LinePointer & rhs) const + { + return lineNum < rhs.lineNum; + } + + bool operator!=(const LinePointer & rhs) const + { + return lineNum != rhs.lineNum; + } + LinePointer & operator++() + { + ++lineNum; + return *this; + } + bool isValid() const + { + return fileLength > 0 && lineNum < fileLength; + } + }; + + struct Trigger + { + LinePointer line; + Trigger() + {} + }; + + + //verm goodies + struct VSymbol + { + std::string text; + VSymbol(const std::string & txt) : text(txt) + {} + }; + + struct VNode; + struct VOptionList; + + struct VNIL + {}; + + + typedef std::variant TLiteral; + + typedef std::variant, VSymbol, TLiteral, ERM::Tcommand> VOption; //options in v-expression, VNIl should be the default + + template + T& getAs(SecType & opt) + { + if(opt.type() == typeid(T)) + return std::get(opt); + else + throw EVermScriptExecError("Wrong type!"); + } + + template + bool isA(const SecType & opt) + { + if(opt.type() == typeid(T)) + return true; + else + return false; + } + + struct VermTreeIterator + { + private: + friend struct VOptionList; + VOptionList * parent; + enum Estate {NORM, CAR} state; + int basePos; //car/cdr offset + public: + VermTreeIterator(VOptionList & _parent) : parent(&_parent), state(NORM), basePos(0) + {} + VermTreeIterator() : parent(nullptr), state(NORM) + {} + + VermTreeIterator & operator=(const VOption & opt); + VermTreeIterator & operator=(const std::vector & opt); + VermTreeIterator & operator=(const VOptionList & opt); + VOption & getAsItem(); + VOptionList getAsList(); + size_t size() const; + + VermTreeIterator& operator=(const VermTreeIterator & rhs) + { + if(this == &rhs) + { + return *this; + } + parent = rhs.parent; + state = rhs.state; + basePos = rhs.basePos; + + return *this; + } + }; + + struct VOptionList : public std::vector + { + private: + friend struct VermTreeIterator; + public: + VermTreeIterator car(); + VermTreeIterator cdr(); + }; + + struct VNode + { + private: + void processModifierList(const std::vector & modifierList, bool asSymbol); + public: + VOptionList children; + VNode( const ERM::TVExp & exp); + VNode( const VOptionList & cdren ); + VNode( const ERM::TSymbol & sym ); //only in case sym has modifiers! + VNode( const VOption & first, const VOptionList & rest); //merges given arguments into [a, rest]; + void setVnode( const VOption & first, const VOptionList & rest); + }; +} + +class ERMInterpreter +{ +/*not so*/ public: + + std::map scripts; + + typedef std::map > TtriggerListType; + TtriggerListType triggers; + TtriggerListType postTriggers; + std::vector instructions; + + static bool isCMDATrigger(const ERM::Tcommand & cmd); + static bool isATrigger(const ERM::TLine & line); + static ERM::EVOtions getExpType(const ERM::TVOption & opt); + ERM::TLine & retrieveLine(const VERMInterpreter::LinePointer & linePtr); + static ERM::TTriggerBase & retrieveTrigger(ERM::TLine & line); +public: + vstd::CLoggerBase * logger; + + ERMInterpreter(vstd::CLoggerBase * logger_); + virtual ~ERMInterpreter(); + + std::string loadScript(const std::string & name, const std::string & source); +}; diff --git a/scripting/erm/ERMParser.cpp b/scripting/erm/ERMParser.cpp index e13d0a9a5..1747d0f03 100644 --- a/scripting/erm/ERMParser.cpp +++ b/scripting/erm/ERMParser.cpp @@ -1,529 +1,529 @@ -/* - * ERMParser.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 "ERMParser.h" - - -#include -#include -#include -#include -#include -#include -#include - - -namespace qi = boost::spirit::qi; -namespace ascii = spirit::ascii; -namespace phoenix = boost::phoenix; - - -//Greenspun's Tenth Rule of Programming: -//Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, -//bug-ridden, slow implementation of half of Common Lisp. -//actually these macros help in dealing with std::variant - - -CERMPreprocessor::CERMPreprocessor(const std::string & source) - : sourceStream(source), - lineNo(0), - version(Version::INVALID) -{ - //check header - std::string header; - getline(header); - - if(header == "ZVSE") - version = Version::ERM; - else if(header == "VERM") - version = Version::VERM; - else - logGlobal->error("File %s has wrong header", fname); -} - -class ParseErrorException : public std::exception -{ - -}; - -std::string CERMPreprocessor::retrieveCommandLine() -{ - std::string wholeCommand; - - //parse file - bool verm = false; - bool openedString = false; - int openedBraces = 0; - - while(sourceStream.good()) - { - std::string line ; - getline(line); //reading line - - size_t dash = line.find_first_of('^'); - bool inTheMiddle = openedBraces || openedString; - - if(!inTheMiddle) - { - if(line.size() < 2) - continue; - if(line[0] != '!' ) //command lines must begin with ! -> otherwise treat as comment - continue; - verm = line[1] == '['; - } - - if(openedString) - { - wholeCommand += "\n"; - if(dash != std::string::npos) - { - wholeCommand += line.substr(0, dash + 1); - line.erase(0,dash + 1); - openedString = false; - } - else //no closing marker -> the whole line is further part of string - { - wholeCommand += line; - continue; - } - } - - int i = 0; - for(; i < line.length(); i++) - { - char c = line[i]; - if(!openedString) - { - if(c == '[') - openedBraces++; - else if(c == ']') - { - openedBraces--; - if(!openedBraces) //the last brace has been matched -> stop "parsing", everything else in the line is comment - { - i++; - break; - } - } - else if(c == '^') - openedString = true; - else if(c == ';' && !verm) //do not allow comments inside VExp for now - { - line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands - break; - } -// else if(c == ';') // a ';' that is in command line (and not in string) ends the command -> throw away rest -// { -// line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands -// break; -// } - } - else if(c == '^') - openedString = false; - } - - if(verm && !openedBraces && i < line.length()) - { - line.erase(i, line.length() - i); - } - - if(wholeCommand.size()) //separate lines with a space - wholeCommand += " "; - - wholeCommand += line; - if(!openedBraces && !openedString) - return wholeCommand; - - //loop end - } - - if(openedBraces || openedString) - { - logGlobal->error("Ill-formed file: %s", fname); - throw ParseErrorException(); - } - return ""; -} - -void CERMPreprocessor::getline(std::string &ret) -{ - lineNo++; - std::getline(sourceStream, ret); - boost::trim(ret); //get rid of wspace -} - -ERMParser::ERMParser() -{ - ERMgrammar = std::make_shared>(); - -} - -ERMParser::~ERMParser() = default; - -std::vector ERMParser::parseFile(CERMPreprocessor & preproc) -{ - std::vector ret; - try - { - while(1) - { - std::string command = preproc.retrieveCommandLine(); - if(command.length() == 0) - break; - - repairEncoding(command); - LineInfo li; - li.realLineNum = preproc.getCurLineNo(); - li.tl = parseLine(command, li.realLineNum); - ret.push_back(li); - } - } - catch (ParseErrorException & e) - { - logGlobal->error("ERM Parser Error. File: '%s' Line: %d Exception: '%s'" - , preproc.getCurFileName(), preproc.getCurLineNo(), e.what()); - throw; - } - return ret; -} - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TStringConstant, - (std::string, str) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TMacroUsage, - (std::string, macro) - ) - -// BOOST_FUSION_ADAPT_STRUCT( -// ERM::TQMacroUsage, -// (std::string, qmacro) -// ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TMacroDef, - (std::string, macro) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarExpNotMacro, - (std::optional, questionMark) - (std::string, varsym) - (ERM::TVarExpNotMacro::Tval, val) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TArithmeticOp, - (ERM::TIexp, lhs) - (char, opcode) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarpExp, - (ERM::TVarExp, var) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVRLogic, - (char, opcode) - (ERM::TIexp, var) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVRArithmetic, - (char, opcode) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TNormalBodyOption, - (char, optionCode) - (std::optional, params) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Ttrigger, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TComparison, - (ERM::TIexp, lhs) - (std::string, compSign) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TSemiCompare, - (std::string, compSign) - (ERM::TIexp, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TCurriedString, - (ERM::TIexp, iexp) - (ERM::TStringConstant, string) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVarConcatString, - (ERM::TVarExp, var) - (ERM::TStringConstant, string) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tcondition, - (char, ctype) - (ERM::Tcondition::Tcond, cond) - (ERM::TconditionNode, rhs) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tinstruction, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - (ERM::Tbody, body) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Treceiver, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - (std::optional, body) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TPostTrigger, - (ERM::TCmdName, name) - (std::optional, identifier) - (std::optional, condition) - ) - -//BOOST_FUSION_ADAPT_STRUCT( -// ERM::Tcommand, -// (ERM::Tcommand::Tcmd, cmd) -// (std::string, comment) -// ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::Tcommand, - (ERM::Tcommand::Tcmd, cmd) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TVExp, - (std::vector, modifier) - (std::vector, children) - ) - -BOOST_FUSION_ADAPT_STRUCT( - ERM::TSymbol, - (std::vector, symModifier) - (std::string, sym) - ) - -namespace ERM -{ - template - struct ERM_grammar : qi::grammar - { - ERM_grammar() : ERM_grammar::base_type(vline, "VERM script line") - { - //do not build too complicated expressions, e.g. (a >> b) | c, qi has problems with them - ERMmacroUsage %= qi::lexeme[qi::lit('$') >> *(qi::char_ - '$') >> qi::lit('$')]; - ERMmacroDef %= qi::lexeme[qi::lit('@') >> *(qi::char_ - '@') >> qi::lit('@')]; - varExpNotMacro %= -qi::char_("?") >> (+(qi::char_("a-z") - 'u')) >> -qi::int_; - //TODO: mixed var/macro expressions like in !!HE-1&407:Id$cost$; [script 13] - /*qERMMacroUsage %= qi::lexeme[qi::lit("?$") >> *(qi::char_ - '$') >> qi::lit('$')];*/ - varExp %= varExpNotMacro | ERMmacroUsage; - iexp %= varExp | qi::int_; - varp %= qi::lit("?") >> varExp; - comment %= *qi::char_; - commentLine %= (~qi::char_("!") >> comment | (qi::char_('!') >> (~qi::char_("?!$#[")) >> comment )); - cmdName %= qi::lexeme[qi::repeat(2)[qi::char_]]; - arithmeticOp %= iexp >> qi::char_ >> iexp; - //??? - //identifier is usually a vector of i-expressions but VR receiver performs arithmetic operations on it - - //identifier %= (iexp | arithmeticOp) % qi::lit('/'); - identifier %= iexp % qi::lit('/'); - - comparison %= iexp >> (*qi::char_("<=>")) >> iexp; - condition %= qi::char_("&|/") >> (comparison | qi::int_) >> -condition; - - trigger %= cmdName >> -identifier >> -condition > qi::lit(";"); ///// - string %= qi::lexeme['^' >> *(qi::char_ - '^') >> '^']; - - VRLogic %= qi::char_("&|") >> iexp; - VRarithmetic %= qi::char_("+*:/%-") >> iexp; - semiCompare %= +qi::char_("<=>") >> iexp; - curStr %= iexp >> string; - varConcatString %= varExp >> qi::lit("+") >> string; - bodyOptionItem %= varConcatString | curStr | string | semiCompare | ERMmacroDef | varp | iexp ; - exactBodyOptionList %= (bodyOptionItem % qi::lit("/")); - normalBodyOption = qi::char_("A-Z") > -(exactBodyOptionList); - bodyOption %= VRLogic | VRarithmetic | normalBodyOption; - body %= qi::lit(":") >> *(bodyOption) > qi::lit(";"); - - instruction %= cmdName >> -identifier >> -condition >> body; - receiver %= cmdName >> -identifier >> -condition >> body; - postTrigger %= cmdName >> -identifier >> -condition > qi::lit(";"); - - command %= (qi::lit("!") >> - ( - (qi::lit("?") >> trigger) | - (qi::lit("!") >> receiver) | - (qi::lit("#") >> instruction) | - (qi::lit("$") >> postTrigger) - ) //>> comment - ); - - rline %= - ( - command | commentLine | spirit::eps - ); - - vmod %= qi::string("`") | qi::string(",!") | qi::string(",") | qi::string("#'") | qi::string("'"); - vsym %= *vmod >> qi::lexeme[+qi::char_("+*/$%&_=<>~a-zA-Z0-9-")]; - - qi::real_parser > strict_double; - vopt %= qi::lexeme[(qi::lit("!") >> qi::char_ >> qi::lit("!"))] | qi::lexeme[strict_double] | qi::lexeme[qi::int_] | command | vexp | string | vsym; - vexp %= *vmod >> qi::lit("[") >> *(vopt) >> qi::lit("]"); - - vline %= (( qi::lit("!") >>vexp) | rline ) > spirit::eoi; - - //error handling - - string.name("string constant"); - ERMmacroUsage.name("macro usage"); - /*qERMMacroUsage.name("macro usage with ?");*/ - ERMmacroDef.name("macro definition"); - varExpNotMacro.name("variable expression (not macro)"); - varExp.name("variable expression"); - iexp.name("i-expression"); - comment.name("comment"); - commentLine.name("comment line"); - cmdName.name("name of a command"); - identifier.name("identifier"); - condition.name("condition"); - trigger.name("trigger"); - body.name("body"); - instruction.name("instruction"); - receiver.name("receiver"); - postTrigger.name("post trigger"); - command.name("command"); - rline.name("ERM script line"); - vsym.name("V symbol"); - vopt.name("V option"); - vexp.name("V expression"); - vline.name("VERM line"); - - qi::on_error - ( - vline - , std::cout //or phoenix::ref(std::count), is there any difference? - << phoenix::val("Error! Expecting ") - << qi::_4 // what failed? - << phoenix::val(" here: \"") - << phoenix::construct(qi::_3, qi::_2) // iterators to error-pos, end - << phoenix::val("\"") - ); - - } - - qi::rule string; - - qi::rule ERMmacroUsage; - /*qi::rule qERMMacroUsage;*/ - qi::rule ERMmacroDef; - qi::rule varExpNotMacro; - qi::rule varExp; - qi::rule iexp; - qi::rule varp; - qi::rule arithmeticOp; - qi::rule comment; - qi::rule commentLine; - qi::rule cmdName; - qi::rule identifier; - qi::rule comparison; - qi::rule condition; - qi::rule VRLogic; - qi::rule VRarithmetic; - qi::rule semiCompare; - qi::rule curStr; - qi::rule varConcatString; - qi::rule bodyOptionItem; - qi::rule exactBodyOptionList; - qi::rule normalBodyOption; - qi::rule bodyOption; - qi::rule trigger; - qi::rule body; - qi::rule instruction; - qi::rule receiver; - qi::rule postTrigger; - qi::rule command; - qi::rule rline; - qi::rule vsym; - qi::rule vmod; - qi::rule vopt; - qi::rule vexp; - qi::rule vline; - }; -} - -ERM::TLine ERMParser::parseLine(const std::string & line, int realLineNo) -{ - try - { - return parseLine(line); - } - catch(...) - { - //logGlobal->error("Parse error occurred in file %s (line %d): %s", fname, realLineNo, line); - throw; - } -} - -ERM::TLine ERMParser::parseLine(const std::string & line) -{ - auto beg = line.begin(); - auto end = line.end(); - - ERM::TLine AST; - - bool r = qi::phrase_parse(beg, end, *ERMgrammar.get(), ascii::space, AST); - if(!r || beg != end) - { - logGlobal->error("Parse error: cannot parse: %s", std::string(beg, end)); - throw ParseErrorException(); - } - return AST; -} - -void ERMParser::repairEncoding(std::string & str) const -{ - for(int g=0; g +#include +#include +#include +#include +#include +#include + + +namespace qi = boost::spirit::qi; +namespace ascii = spirit::ascii; +namespace phoenix = boost::phoenix; + + +//Greenspun's Tenth Rule of Programming: +//Any sufficiently complicated C or Fortran program contains an ad hoc, informally-specified, +//bug-ridden, slow implementation of half of Common Lisp. +//actually these macros help in dealing with std::variant + + +CERMPreprocessor::CERMPreprocessor(const std::string & source) + : sourceStream(source), + lineNo(0), + version(Version::INVALID) +{ + //check header + std::string header; + getline(header); + + if(header == "ZVSE") + version = Version::ERM; + else if(header == "VERM") + version = Version::VERM; + else + logGlobal->error("File %s has wrong header", fname); +} + +class ParseErrorException : public std::exception +{ + +}; + +std::string CERMPreprocessor::retrieveCommandLine() +{ + std::string wholeCommand; + + //parse file + bool verm = false; + bool openedString = false; + int openedBraces = 0; + + while(sourceStream.good()) + { + std::string line ; + getline(line); //reading line + + size_t dash = line.find_first_of('^'); + bool inTheMiddle = openedBraces || openedString; + + if(!inTheMiddle) + { + if(line.size() < 2) + continue; + if(line[0] != '!' ) //command lines must begin with ! -> otherwise treat as comment + continue; + verm = line[1] == '['; + } + + if(openedString) + { + wholeCommand += "\n"; + if(dash != std::string::npos) + { + wholeCommand += line.substr(0, dash + 1); + line.erase(0,dash + 1); + openedString = false; + } + else //no closing marker -> the whole line is further part of string + { + wholeCommand += line; + continue; + } + } + + int i = 0; + for(; i < line.length(); i++) + { + char c = line[i]; + if(!openedString) + { + if(c == '[') + openedBraces++; + else if(c == ']') + { + openedBraces--; + if(!openedBraces) //the last brace has been matched -> stop "parsing", everything else in the line is comment + { + i++; + break; + } + } + else if(c == '^') + openedString = true; + else if(c == ';' && !verm) //do not allow comments inside VExp for now + { + line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands + break; + } +// else if(c == ';') // a ';' that is in command line (and not in string) ends the command -> throw away rest +// { +// line.erase(i+!verm, line.length() - i - !verm); //leave ';' at the end only at ERM commands +// break; +// } + } + else if(c == '^') + openedString = false; + } + + if(verm && !openedBraces && i < line.length()) + { + line.erase(i, line.length() - i); + } + + if(wholeCommand.size()) //separate lines with a space + wholeCommand += " "; + + wholeCommand += line; + if(!openedBraces && !openedString) + return wholeCommand; + + //loop end + } + + if(openedBraces || openedString) + { + logGlobal->error("Ill-formed file: %s", fname); + throw ParseErrorException(); + } + return ""; +} + +void CERMPreprocessor::getline(std::string &ret) +{ + lineNo++; + std::getline(sourceStream, ret); + boost::trim(ret); //get rid of wspace +} + +ERMParser::ERMParser() +{ + ERMgrammar = std::make_shared>(); + +} + +ERMParser::~ERMParser() = default; + +std::vector ERMParser::parseFile(CERMPreprocessor & preproc) +{ + std::vector ret; + try + { + while(1) + { + std::string command = preproc.retrieveCommandLine(); + if(command.length() == 0) + break; + + repairEncoding(command); + LineInfo li; + li.realLineNum = preproc.getCurLineNo(); + li.tl = parseLine(command, li.realLineNum); + ret.push_back(li); + } + } + catch (ParseErrorException & e) + { + logGlobal->error("ERM Parser Error. File: '%s' Line: %d Exception: '%s'" + , preproc.getCurFileName(), preproc.getCurLineNo(), e.what()); + throw; + } + return ret; +} + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TStringConstant, + (std::string, str) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TMacroUsage, + (std::string, macro) + ) + +// BOOST_FUSION_ADAPT_STRUCT( +// ERM::TQMacroUsage, +// (std::string, qmacro) +// ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TMacroDef, + (std::string, macro) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarExpNotMacro, + (std::optional, questionMark) + (std::string, varsym) + (ERM::TVarExpNotMacro::Tval, val) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TArithmeticOp, + (ERM::TIexp, lhs) + (char, opcode) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarpExp, + (ERM::TVarExp, var) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVRLogic, + (char, opcode) + (ERM::TIexp, var) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVRArithmetic, + (char, opcode) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TNormalBodyOption, + (char, optionCode) + (std::optional, params) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Ttrigger, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TComparison, + (ERM::TIexp, lhs) + (std::string, compSign) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TSemiCompare, + (std::string, compSign) + (ERM::TIexp, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TCurriedString, + (ERM::TIexp, iexp) + (ERM::TStringConstant, string) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVarConcatString, + (ERM::TVarExp, var) + (ERM::TStringConstant, string) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tcondition, + (char, ctype) + (ERM::Tcondition::Tcond, cond) + (ERM::TconditionNode, rhs) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tinstruction, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + (ERM::Tbody, body) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Treceiver, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + (std::optional, body) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TPostTrigger, + (ERM::TCmdName, name) + (std::optional, identifier) + (std::optional, condition) + ) + +//BOOST_FUSION_ADAPT_STRUCT( +// ERM::Tcommand, +// (ERM::Tcommand::Tcmd, cmd) +// (std::string, comment) +// ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::Tcommand, + (ERM::Tcommand::Tcmd, cmd) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TVExp, + (std::vector, modifier) + (std::vector, children) + ) + +BOOST_FUSION_ADAPT_STRUCT( + ERM::TSymbol, + (std::vector, symModifier) + (std::string, sym) + ) + +namespace ERM +{ + template + struct ERM_grammar : qi::grammar + { + ERM_grammar() : ERM_grammar::base_type(vline, "VERM script line") + { + //do not build too complicated expressions, e.g. (a >> b) | c, qi has problems with them + ERMmacroUsage %= qi::lexeme[qi::lit('$') >> *(qi::char_ - '$') >> qi::lit('$')]; + ERMmacroDef %= qi::lexeme[qi::lit('@') >> *(qi::char_ - '@') >> qi::lit('@')]; + varExpNotMacro %= -qi::char_("?") >> (+(qi::char_("a-z") - 'u')) >> -qi::int_; + //TODO: mixed var/macro expressions like in !!HE-1&407:Id$cost$; [script 13] + /*qERMMacroUsage %= qi::lexeme[qi::lit("?$") >> *(qi::char_ - '$') >> qi::lit('$')];*/ + varExp %= varExpNotMacro | ERMmacroUsage; + iexp %= varExp | qi::int_; + varp %= qi::lit("?") >> varExp; + comment %= *qi::char_; + commentLine %= (~qi::char_("!") >> comment | (qi::char_('!') >> (~qi::char_("?!$#[")) >> comment )); + cmdName %= qi::lexeme[qi::repeat(2)[qi::char_]]; + arithmeticOp %= iexp >> qi::char_ >> iexp; + //??? + //identifier is usually a vector of i-expressions but VR receiver performs arithmetic operations on it + + //identifier %= (iexp | arithmeticOp) % qi::lit('/'); + identifier %= iexp % qi::lit('/'); + + comparison %= iexp >> (*qi::char_("<=>")) >> iexp; + condition %= qi::char_("&|/") >> (comparison | qi::int_) >> -condition; + + trigger %= cmdName >> -identifier >> -condition > qi::lit(";"); ///// + string %= qi::lexeme['^' >> *(qi::char_ - '^') >> '^']; + + VRLogic %= qi::char_("&|") >> iexp; + VRarithmetic %= qi::char_("+*:/%-") >> iexp; + semiCompare %= +qi::char_("<=>") >> iexp; + curStr %= iexp >> string; + varConcatString %= varExp >> qi::lit("+") >> string; + bodyOptionItem %= varConcatString | curStr | string | semiCompare | ERMmacroDef | varp | iexp ; + exactBodyOptionList %= (bodyOptionItem % qi::lit("/")); + normalBodyOption = qi::char_("A-Z") > -(exactBodyOptionList); + bodyOption %= VRLogic | VRarithmetic | normalBodyOption; + body %= qi::lit(":") >> *(bodyOption) > qi::lit(";"); + + instruction %= cmdName >> -identifier >> -condition >> body; + receiver %= cmdName >> -identifier >> -condition >> body; + postTrigger %= cmdName >> -identifier >> -condition > qi::lit(";"); + + command %= (qi::lit("!") >> + ( + (qi::lit("?") >> trigger) | + (qi::lit("!") >> receiver) | + (qi::lit("#") >> instruction) | + (qi::lit("$") >> postTrigger) + ) //>> comment + ); + + rline %= + ( + command | commentLine | spirit::eps + ); + + vmod %= qi::string("`") | qi::string(",!") | qi::string(",") | qi::string("#'") | qi::string("'"); + vsym %= *vmod >> qi::lexeme[+qi::char_("+*/$%&_=<>~a-zA-Z0-9-")]; + + qi::real_parser > strict_double; + vopt %= qi::lexeme[(qi::lit("!") >> qi::char_ >> qi::lit("!"))] | qi::lexeme[strict_double] | qi::lexeme[qi::int_] | command | vexp | string | vsym; + vexp %= *vmod >> qi::lit("[") >> *(vopt) >> qi::lit("]"); + + vline %= (( qi::lit("!") >>vexp) | rline ) > spirit::eoi; + + //error handling + + string.name("string constant"); + ERMmacroUsage.name("macro usage"); + /*qERMMacroUsage.name("macro usage with ?");*/ + ERMmacroDef.name("macro definition"); + varExpNotMacro.name("variable expression (not macro)"); + varExp.name("variable expression"); + iexp.name("i-expression"); + comment.name("comment"); + commentLine.name("comment line"); + cmdName.name("name of a command"); + identifier.name("identifier"); + condition.name("condition"); + trigger.name("trigger"); + body.name("body"); + instruction.name("instruction"); + receiver.name("receiver"); + postTrigger.name("post trigger"); + command.name("command"); + rline.name("ERM script line"); + vsym.name("V symbol"); + vopt.name("V option"); + vexp.name("V expression"); + vline.name("VERM line"); + + qi::on_error + ( + vline + , std::cout //or phoenix::ref(std::count), is there any difference? + << phoenix::val("Error! Expecting ") + << qi::_4 // what failed? + << phoenix::val(" here: \"") + << phoenix::construct(qi::_3, qi::_2) // iterators to error-pos, end + << phoenix::val("\"") + ); + + } + + qi::rule string; + + qi::rule ERMmacroUsage; + /*qi::rule qERMMacroUsage;*/ + qi::rule ERMmacroDef; + qi::rule varExpNotMacro; + qi::rule varExp; + qi::rule iexp; + qi::rule varp; + qi::rule arithmeticOp; + qi::rule comment; + qi::rule commentLine; + qi::rule cmdName; + qi::rule identifier; + qi::rule comparison; + qi::rule condition; + qi::rule VRLogic; + qi::rule VRarithmetic; + qi::rule semiCompare; + qi::rule curStr; + qi::rule varConcatString; + qi::rule bodyOptionItem; + qi::rule exactBodyOptionList; + qi::rule normalBodyOption; + qi::rule bodyOption; + qi::rule trigger; + qi::rule body; + qi::rule instruction; + qi::rule receiver; + qi::rule postTrigger; + qi::rule command; + qi::rule rline; + qi::rule vsym; + qi::rule vmod; + qi::rule vopt; + qi::rule vexp; + qi::rule vline; + }; +} + +ERM::TLine ERMParser::parseLine(const std::string & line, int realLineNo) +{ + try + { + return parseLine(line); + } + catch(...) + { + //logGlobal->error("Parse error occurred in file %s (line %d): %s", fname, realLineNo, line); + throw; + } +} + +ERM::TLine ERMParser::parseLine(const std::string & line) +{ + auto beg = line.begin(); + auto end = line.end(); + + ERM::TLine AST; + + bool r = qi::phrase_parse(beg, end, *ERMgrammar.get(), ascii::space, AST); + if(!r || beg != end) + { + logGlobal->error("Parse error: cannot parse: %s", std::string(beg, end)); + throw ParseErrorException(); + } + return AST; +} + +void ERMParser::repairEncoding(std::string & str) const +{ + for(int g=0; g -#include - -namespace spirit = boost::spirit; - -class CERMPreprocessor -{ - std::string fname; - std::stringstream sourceStream; - int lineNo; - - - void getline(std::string &ret); - -public: - enum class Version : ui8 - { - INVALID, - ERM, - VERM - }; - Version version; - - CERMPreprocessor(const std::string & source); - std::string retrieveCommandLine(); - int getCurLineNo() const - { - return lineNo; - } - - const std::string& getCurFileName() const - { - return fname; - } -}; - -//various classes that represent ERM/VERM AST -namespace ERM -{ - using ValType = int; //todo: set to int64_t - using IType = int; //todo: set to int32_t - - struct TStringConstant - { - std::string str; - }; - struct TMacroUsage - { - std::string macro; - }; - -// //macro with '?', for write only -// struct TQMacroUsage -// { -// std::string qmacro; -// }; - - //definition of a macro - struct TMacroDef - { - std::string macro; - }; - typedef std::string TCmdName; - - struct TVarExpNotMacro - { - typedef std::optional Tval; - std::optional questionMark; - std::string varsym; - Tval val; - }; - - typedef std::variant TVarExp; - - //write-only variable expression - struct TVarpExp - { - TVarExp var; - }; - - //i-expression (identifier expression) - an integral constant, variable symbol or array symbol - typedef std::variant TIexp; - - struct TArithmeticOp - { - TIexp lhs, rhs; - char opcode; - }; - - struct TVRLogic - { - char opcode; - TIexp var; - }; - - struct TVRArithmetic - { - char opcode; - TIexp rhs; - }; - - struct TSemiCompare - { - std::string compSign; - TIexp rhs; - }; - - struct TCurriedString - { - TIexp iexp; - TStringConstant string; - }; - - struct TVarConcatString - { - TVarExp var; - TStringConstant string; - }; - - typedef std::variant TBodyOptionItem; - - typedef std::vector TNormalBodyOptionList; - - struct TNormalBodyOption - { - char optionCode; - std::optional params; - }; - typedef std::variant TBodyOption; - -// typedef std::variant TIdentifierInternal; - typedef std::vector< TIexp > Tidentifier; - - struct TComparison - { - std::string compSign; - TIexp lhs, rhs; - }; - - struct Tcondition; - typedef std::optional> TconditionNode; - - struct Tcondition - { - typedef std::variant< - TComparison, - int> - Tcond; //comparison or condition flag - char ctype; - Tcond cond; - TconditionNode rhs; - }; - - struct TTriggerBase - { - bool pre; //if false it's !$ post-trigger, elsewise it's !# (pre)trigger - TCmdName name; - std::optional identifier; - std::optional condition; - }; - - struct Ttrigger : TTriggerBase - { - Ttrigger() - { - pre = true; - } - }; - - struct TPostTrigger : TTriggerBase - { - TPostTrigger() - { - pre = false; - } - }; - - //a dirty workaround for preprocessor magic that prevents the use types with comma in it in BOOST_FUSION_ADAPT_STRUCT - //see http://comments.gmane.org/gmane.comp.lib.boost.user/62501 for some info - // - //moreover, I encountered a quite serious bug in boost: http://boost.2283326.n4.nabble.com/container-hpp-111-error-C2039-value-type-is-not-a-member-of-td3352328.html - //not sure how serious it is... - - //typedef std::variant bodyItem; - typedef std::vector Tbody; - - struct Tinstruction - { - TCmdName name; - std::optional identifier; - std::optional condition; - Tbody body; - }; - - struct Treceiver - { - TCmdName name; - std::optional identifier; - std::optional condition; - std::optional body; - }; - - struct Tcommand - { - typedef std::variant< - Ttrigger, - Tinstruction, - Treceiver, - TPostTrigger - > - Tcmd; - Tcmd cmd; - //std::string comment; - }; - - //vector expression - - - typedef std::variant TERMline; - - typedef std::string TVModifier; //'`', ',', ',@', '#'' - - struct TSymbol - { - std::vector symModifier; - std::string sym; - }; - - //for #'symbol expression - - enum EVOtions{VEXP, SYMBOL, CHAR, DOUBLE, INT, TCMD, STRINGC}; - struct TVExp; - typedef std::variant, TSymbol, char, double, int, Tcommand, TStringConstant > TVOption; //options in v-expression - //v-expression - struct TVExp - { - std::vector modifier; - std::vector children; - }; - - //script line - typedef std::variant TLine; - - template struct ERM_grammar; -} - -struct LineInfo -{ - ERM::TLine tl; - int realLineNum; -}; - -class ERMParser -{ -public: - std::shared_ptr> ERMgrammar; - - ERMParser(); - virtual ~ERMParser(); - - std::vector parseFile(CERMPreprocessor & preproc); -private: - void repairEncoding(char * str, int len) const; //removes nonstandard ascii characters from string - void repairEncoding(std::string & str) const; //removes nonstandard ascii characters from string - ERM::TLine parseLine(const std::string & line, int realLineNo); - ERM::TLine parseLine(const std::string & line); -}; +/* + * ERMParser.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 +#include + +namespace spirit = boost::spirit; + +class CERMPreprocessor +{ + std::string fname; + std::stringstream sourceStream; + int lineNo; + + + void getline(std::string &ret); + +public: + enum class Version : ui8 + { + INVALID, + ERM, + VERM + }; + Version version; + + CERMPreprocessor(const std::string & source); + std::string retrieveCommandLine(); + int getCurLineNo() const + { + return lineNo; + } + + const std::string& getCurFileName() const + { + return fname; + } +}; + +//various classes that represent ERM/VERM AST +namespace ERM +{ + using ValType = int; //todo: set to int64_t + using IType = int; //todo: set to int32_t + + struct TStringConstant + { + std::string str; + }; + struct TMacroUsage + { + std::string macro; + }; + +// //macro with '?', for write only +// struct TQMacroUsage +// { +// std::string qmacro; +// }; + + //definition of a macro + struct TMacroDef + { + std::string macro; + }; + typedef std::string TCmdName; + + struct TVarExpNotMacro + { + typedef std::optional Tval; + std::optional questionMark; + std::string varsym; + Tval val; + }; + + typedef std::variant TVarExp; + + //write-only variable expression + struct TVarpExp + { + TVarExp var; + }; + + //i-expression (identifier expression) - an integral constant, variable symbol or array symbol + typedef std::variant TIexp; + + struct TArithmeticOp + { + TIexp lhs, rhs; + char opcode; + }; + + struct TVRLogic + { + char opcode; + TIexp var; + }; + + struct TVRArithmetic + { + char opcode; + TIexp rhs; + }; + + struct TSemiCompare + { + std::string compSign; + TIexp rhs; + }; + + struct TCurriedString + { + TIexp iexp; + TStringConstant string; + }; + + struct TVarConcatString + { + TVarExp var; + TStringConstant string; + }; + + typedef std::variant TBodyOptionItem; + + typedef std::vector TNormalBodyOptionList; + + struct TNormalBodyOption + { + char optionCode; + std::optional params; + }; + typedef std::variant TBodyOption; + +// typedef std::variant TIdentifierInternal; + typedef std::vector< TIexp > Tidentifier; + + struct TComparison + { + std::string compSign; + TIexp lhs, rhs; + }; + + struct Tcondition; + typedef std::optional> TconditionNode; + + struct Tcondition + { + typedef std::variant< + TComparison, + int> + Tcond; //comparison or condition flag + char ctype; + Tcond cond; + TconditionNode rhs; + }; + + struct TTriggerBase + { + bool pre; //if false it's !$ post-trigger, elsewise it's !# (pre)trigger + TCmdName name; + std::optional identifier; + std::optional condition; + }; + + struct Ttrigger : TTriggerBase + { + Ttrigger() + { + pre = true; + } + }; + + struct TPostTrigger : TTriggerBase + { + TPostTrigger() + { + pre = false; + } + }; + + //a dirty workaround for preprocessor magic that prevents the use types with comma in it in BOOST_FUSION_ADAPT_STRUCT + //see http://comments.gmane.org/gmane.comp.lib.boost.user/62501 for some info + // + //moreover, I encountered a quite serious bug in boost: http://boost.2283326.n4.nabble.com/container-hpp-111-error-C2039-value-type-is-not-a-member-of-td3352328.html + //not sure how serious it is... + + //typedef std::variant bodyItem; + typedef std::vector Tbody; + + struct Tinstruction + { + TCmdName name; + std::optional identifier; + std::optional condition; + Tbody body; + }; + + struct Treceiver + { + TCmdName name; + std::optional identifier; + std::optional condition; + std::optional body; + }; + + struct Tcommand + { + typedef std::variant< + Ttrigger, + Tinstruction, + Treceiver, + TPostTrigger + > + Tcmd; + Tcmd cmd; + //std::string comment; + }; + + //vector expression + + + typedef std::variant TERMline; + + typedef std::string TVModifier; //'`', ',', ',@', '#'' + + struct TSymbol + { + std::vector symModifier; + std::string sym; + }; + + //for #'symbol expression + + enum EVOtions{VEXP, SYMBOL, CHAR, DOUBLE, INT, TCMD, STRINGC}; + struct TVExp; + typedef std::variant, TSymbol, char, double, int, Tcommand, TStringConstant > TVOption; //options in v-expression + //v-expression + struct TVExp + { + std::vector modifier; + std::vector children; + }; + + //script line + typedef std::variant TLine; + + template struct ERM_grammar; +} + +struct LineInfo +{ + ERM::TLine tl; + int realLineNum; +}; + +class ERMParser +{ +public: + std::shared_ptr> ERMgrammar; + + ERMParser(); + virtual ~ERMParser(); + + std::vector parseFile(CERMPreprocessor & preproc); +private: + void repairEncoding(char * str, int len) const; //removes nonstandard ascii characters from string + void repairEncoding(std::string & str) const; //removes nonstandard ascii characters from string + ERM::TLine parseLine(const std::string & line, int realLineNo); + ERM::TLine parseLine(const std::string & line); +}; diff --git a/scripting/erm/ERMScriptModule.cpp b/scripting/erm/ERMScriptModule.cpp index 2aa9d12b3..d82e7cd9f 100644 --- a/scripting/erm/ERMScriptModule.cpp +++ b/scripting/erm/ERMScriptModule.cpp @@ -1,68 +1,68 @@ -/* - * ERMScriptModule.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 "ERMScriptModule.h" - -#include "ERMInterpreter.h" - -#ifdef __GNUC__ -#define strcpy_s(a, b, c) strncpy(a, c, b) -#endif - -const char *g_cszAiName = "(V)ERM interpreter"; - -extern "C" DLL_EXPORT void GetAiName(char* name) -{ - strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); -} - -extern "C" DLL_EXPORT void GetNewModule(std::shared_ptr &out) -{ - out = std::make_shared(); -} - -ERMScriptModule::ERMScriptModule() -{ - -} - -std::string ERMScriptModule::compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const -{ - std::shared_ptr interp = std::make_shared(logger); - - try - { - return interp->loadScript(name, source); - } - catch(const std::exception & ex) - { - logger->error(ex.what()); - } - catch(const std::string & ex) - { - logger->error(ex); - } - catch(...) - { - logger->error("Sorry, caught unknown exception type. No more info available."); - } - - return ""; -} - -std::shared_ptr ERMScriptModule::createContextFor(const scripting::Script * source, const Environment * env) const -{ - throw std::runtime_error("ERM context creation is not possible"); -} - -void ERMScriptModule::registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const -{ - throw std::runtime_error("ERM spell effect registration is not possible"); -} +/* + * ERMScriptModule.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 "ERMScriptModule.h" + +#include "ERMInterpreter.h" + +#ifdef __GNUC__ +#define strcpy_s(a, b, c) strncpy(a, c, b) +#endif + +const char *g_cszAiName = "(V)ERM interpreter"; + +extern "C" DLL_EXPORT void GetAiName(char* name) +{ + strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName); +} + +extern "C" DLL_EXPORT void GetNewModule(std::shared_ptr &out) +{ + out = std::make_shared(); +} + +ERMScriptModule::ERMScriptModule() +{ + +} + +std::string ERMScriptModule::compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const +{ + std::shared_ptr interp = std::make_shared(logger); + + try + { + return interp->loadScript(name, source); + } + catch(const std::exception & ex) + { + logger->error(ex.what()); + } + catch(const std::string & ex) + { + logger->error(ex); + } + catch(...) + { + logger->error("Sorry, caught unknown exception type. No more info available."); + } + + return ""; +} + +std::shared_ptr ERMScriptModule::createContextFor(const scripting::Script * source, const Environment * env) const +{ + throw std::runtime_error("ERM context creation is not possible"); +} + +void ERMScriptModule::registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const +{ + throw std::runtime_error("ERM spell effect registration is not possible"); +} diff --git a/scripting/erm/ERMScriptModule.h b/scripting/erm/ERMScriptModule.h index c62b1813d..2b64471a9 100644 --- a/scripting/erm/ERMScriptModule.h +++ b/scripting/erm/ERMScriptModule.h @@ -1,25 +1,25 @@ -/* - * ERMScriptModule.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/CScriptingModule.h" - -class ERMScriptModule : public scripting::Module -{ -public: - ERMScriptModule(); - - std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const override; - - std::shared_ptr createContextFor(const scripting::Script * source, const Environment * env) const override; - - void registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const override; -}; - +/* + * ERMScriptModule.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/CScriptingModule.h" + +class ERMScriptModule : public scripting::Module +{ +public: + ERMScriptModule(); + + std::string compile(const std::string & name, const std::string & source, vstd::CLoggerBase * logger) const override; + + std::shared_ptr createContextFor(const scripting::Script * source, const Environment * env) const override; + + void registerSpellEffect(spells::effects::Registry * registry, const scripting::Script * source) const override; +}; + diff --git a/scripting/erm/StdInc.cpp b/scripting/erm/StdInc.cpp index c8f4ddf05..c17377322 100644 --- a/scripting/erm/StdInc.cpp +++ b/scripting/erm/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" \ No newline at end of file diff --git a/scripting/erm/StdInc.h b/scripting/erm/StdInc.h index 02b2c08f3..020481377 100644 --- a/scripting/erm/StdInc.h +++ b/scripting/erm/StdInc.h @@ -1,9 +1,9 @@ -#pragma once - -#include "../../Global.h" - -// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. - -// Here you can add specific libraries and macros which are specific to this project. - -VCMI_LIB_USING_NAMESPACE +#pragma once + +#include "../../Global.h" + +// This header should be treated as a pre compiled header file(PCH) in the compiler building settings. + +// Here you can add specific libraries and macros which are specific to this project. + +VCMI_LIB_USING_NAMESPACE diff --git a/server/CGameHandler.h b/server/CGameHandler.h index 2784aa6a7..acf1bc7e7 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -1,298 +1,298 @@ -/* - * CGameHandler.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 - -#include "../lib/IGameCallback.h" -#include "../lib/LoadProgress.h" -#include "../lib/ScriptHandler.h" -#include "TurnTimerHandler.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct SideInBattle; -class IMarket; -class SpellCastEnvironment; -class CConnection; -class CCommanderInstance; -class EVictoryLossCheckResult; - -struct CPack; -struct CPackForServer; -struct NewTurn; -struct CGarrisonOperationPack; -struct SetResources; -struct NewStructures; - -#if SCRIPTING_ENABLED -namespace scripting -{ - class PoolImpl; -} -#endif - -template class CApplier; - -VCMI_LIB_NAMESPACE_END - -class HeroPoolProcessor; -class CVCMIServer; -class CBaseForGHApply; -class PlayerMessageProcessor; -class BattleProcessor; -class TurnOrderProcessor; -class QueriesProcessor; -class CObjectVisitQuery; - -class CGameHandler : public IGameCallback, public Environment -{ - CVCMIServer * lobby; - std::shared_ptr> applier; - -public: - std::unique_ptr heroPool; - std::unique_ptr battles; - std::unique_ptr queries; - std::unique_ptr turnOrder; - - //use enums as parameters, because doMove(sth, true, false, true) is not readable - enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; - enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; - enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; - - std::unique_ptr playerMessages; - - std::map>> connections; //player color -> connection to client with interface of that player - - //queries stuff - boost::recursive_mutex gsm; - ui32 QID; - - SpellCastEnvironment * spellEnv; - - TurnTimerHandler turnTimerHandler; - - const Services * services() const override; - const BattleCb * battle(const BattleID & battleID) const override; - const GameCb * game() const override; - vstd::CLoggerBase * logger() const override; - events::EventBus * eventBus() const override; - CVCMIServer * gameLobby() const; - - bool isValidObject(const CGObjectInstance *obj) const; - bool isBlockedByQueries(const CPack *pack, PlayerColor player); - bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); - void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); - - CGameHandler(); - CGameHandler(CVCMIServer * lobby); - ~CGameHandler(); - - ////////////////////////////////////////////////////////////////////////// - //from IGameCallback - //do sth - void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; - bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override; - void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; - void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; - void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; - - void showBlockingDialog(BlockingDialog *iw) override; - void showTeleportDialog(TeleportDialog *iw) override; - void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override; - void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override; - void giveResource(PlayerColor player, GameResID which, int val) override; - void giveResources(PlayerColor player, TResources resources) override; - - void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override; - void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) override; - bool changeStackType(const StackLocation &sl, const CCreature *c) override; - bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override; - bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override; - bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override; - bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override; - bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override; - void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override; - bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count = -1) override; - - void removeAfterVisit(const CGObjectInstance *object) override; - - bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; - void removeArtifact(const ArtifactLocation &al) override; - bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; - bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); - bool eraseArtifactByClient(const ArtifactLocation & al); - void synchronizeArtifactHandlerLists(); - - void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; - void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; - void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override; //if any of armies is hero, hero will be used - void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle - bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; - void giveHeroBonus(GiveBonus * bonus) override; - void setMovePoints(SetMovePoints * smp) override; - void setManaPoints(ObjectInstanceID hid, int val) override; - void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override; - void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; - void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; - - void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; - void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override; - - void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; - - /// Returns hero that is currently visiting this object, or nullptr if no visit is active - const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); - bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; - void showInfoDialog(InfoWindow * iw) override; - void showInfoDialog(const std::string & msg, PlayerColor player) override; - - ////////////////////////////////////////////////////////////////////////// - void useScholarSkill(ObjectInstanceID hero1, ObjectInstanceID hero2); - void setPortalDwelling(const CGTownInstance * town, bool forced, bool clear); - void visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h); - bool teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker = PlayerColor::NEUTRAL); - void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override; - void levelUpHero(const CGHeroInstance * hero, SecondarySkill skill);//handle client respond and send one more request if needed - void levelUpHero(const CGHeroInstance * hero);//initial call - check if hero have remaining levelups & handle them - void levelUpCommander (const CCommanderInstance * c, int skill); //secondary skill 1 to 6, special skill : skill - 100 - void levelUpCommander (const CCommanderInstance * c); - - void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero - ////////////////////////////////////////////////////////////////////////// - - void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); - void handleClientDisconnection(std::shared_ptr c); - void handleReceivedPack(CPackForServer * pack); - bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; - bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; - - bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); - bool buildBoat( ObjectInstanceID objid, PlayerColor player ); - bool setFormation( ObjectInstanceID hid, ui8 formation ); - bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); - bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); - bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); - bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); - bool transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot); - bool assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo); - bool buyArtifact( ObjectInstanceID hid, ArtifactID aid ); //for blacksmith and mage guild only -> buying for gold in common buildings - bool buyArtifact( const IMarket *m, const CGHeroInstance *h, GameResID rid, ArtifactID aid); //for artifact merchant and black market -> buying for any resource in special building / advobject - bool sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, GameResID rid); //for artifact merchant selling - //void lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts); //after battle - move al arts to winer - bool buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill); - bool garrisonSwap(ObjectInstanceID tid); - bool swapGarrisonOnSiege(ObjectInstanceID tid) override; - bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ); - bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level, PlayerColor player); - bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings - bool razeStructure(ObjectInstanceID tid, BuildingID bid); - bool disbandCreature( ObjectInstanceID id, SlotID pos ); - bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player); - bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot); - bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany); - bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner); - bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner); - void save(const std::string &fname); - bool load(const std::string &fname); - - void onPlayerTurnStarted(PlayerColor which); - void onPlayerTurnEnded(PlayerColor which); - void onNewTurn(); - - void handleTimeEvents(); - void handleTownEvents(CGTownInstance *town, NewTurn &n); - bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true - void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ); - void objectVisitEnded(const CObjectVisitQuery &query); - bool dig(const CGHeroInstance *h); - void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); - - template void serialize(Handler &h, const int version) - { - h & QID; - h & getRandomGenerator(); - h & *battles; - h & *heroPool; - h & *playerMessages; - h & *turnOrder; - -#if SCRIPTING_ENABLED - JsonNode scriptsState; - if(h.saving) - serverScripts->serializeState(h.saving, scriptsState); - h & scriptsState; - if(!h.saving) - serverScripts->serializeState(h.saving, scriptsState); -#endif - } - - void sendToAllClients(CPackForClient * pack); - void sendAndApply(CPackForClient * pack) override; - void sendAndApply(CGarrisonOperationPack * pack); - void sendAndApply(SetResources * pack); - void sendAndApply(NewStructures * pack); - - void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); - /// Unconditionally throws with "Action not allowed" message - void throwNotAllowedAction(CPackForServer * pack); - /// Throws if player stated in pack is not making turn right now - void throwIfPlayerNotActive(CPackForServer * pack); - /// Throws if object is not owned by pack sender - void throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id); - /// Throws if player is not present on connection of this pack - void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player); - void throwIfWrongPlayer(CPackForServer * pack); - void throwAndComplain(CPackForServer * pack, std::string txt); - - bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); - - void run(bool resume); - bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); - void spawnWanderingMonsters(CreatureID creatureID); - - // Check for victory and loss conditions - void checkVictoryLossConditionsForPlayer(PlayerColor player); - void checkVictoryLossConditions(const std::set & playerColors); - void checkVictoryLossConditionsForAll(); - - CRandomGenerator & getRandomGenerator(); - -#if SCRIPTING_ENABLED - scripting::Pool * getGlobalContextPool() const override; -// scripting::Pool * getContextPool() const override; -#endif - - friend class CVCMIServer; -private: - std::unique_ptr serverEventBus; -#if SCRIPTING_ENABLED - std::shared_ptr serverScripts; -#endif - - void reinitScripting(); - - void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; - - const std::string complainNoCreatures; - const std::string complainNotEnoughCreatures; - const std::string complainInvalidSlot; -}; - -class ExceptionNotAllowedAction : public std::exception -{ - -}; +/* + * CGameHandler.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 + +#include "../lib/IGameCallback.h" +#include "../lib/LoadProgress.h" +#include "../lib/ScriptHandler.h" +#include "TurnTimerHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct SideInBattle; +class IMarket; +class SpellCastEnvironment; +class CConnection; +class CCommanderInstance; +class EVictoryLossCheckResult; + +struct CPack; +struct CPackForServer; +struct NewTurn; +struct CGarrisonOperationPack; +struct SetResources; +struct NewStructures; + +#if SCRIPTING_ENABLED +namespace scripting +{ + class PoolImpl; +} +#endif + +template class CApplier; + +VCMI_LIB_NAMESPACE_END + +class HeroPoolProcessor; +class CVCMIServer; +class CBaseForGHApply; +class PlayerMessageProcessor; +class BattleProcessor; +class TurnOrderProcessor; +class QueriesProcessor; +class CObjectVisitQuery; + +class CGameHandler : public IGameCallback, public Environment +{ + CVCMIServer * lobby; + std::shared_ptr> applier; + +public: + std::unique_ptr heroPool; + std::unique_ptr battles; + std::unique_ptr queries; + std::unique_ptr turnOrder; + + //use enums as parameters, because doMove(sth, true, false, true) is not readable + enum EGuardLook {CHECK_FOR_GUARDS, IGNORE_GUARDS}; + enum EVisitDest {VISIT_DEST, DONT_VISIT_DEST}; + enum ELEaveTile {LEAVING_TILE, REMAINING_ON_TILE}; + + std::unique_ptr playerMessages; + + std::map>> connections; //player color -> connection to client with interface of that player + + //queries stuff + boost::recursive_mutex gsm; + ui32 QID; + + SpellCastEnvironment * spellEnv; + + TurnTimerHandler turnTimerHandler; + + const Services * services() const override; + const BattleCb * battle(const BattleID & battleID) const override; + const GameCb * game() const override; + vstd::CLoggerBase * logger() const override; + events::EventBus * eventBus() const override; + CVCMIServer * gameLobby() const; + + bool isValidObject(const CGObjectInstance *obj) const; + bool isBlockedByQueries(const CPack *pack, PlayerColor player); + bool isAllowedExchange(ObjectInstanceID id1, ObjectInstanceID id2); + void giveSpells(const CGTownInstance *t, const CGHeroInstance *h); + + CGameHandler(); + CGameHandler(CVCMIServer * lobby); + ~CGameHandler(); + + ////////////////////////////////////////////////////////////////////////// + //from IGameCallback + //do sth + void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; + bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override; + void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; + void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; + void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; + + void showBlockingDialog(BlockingDialog *iw) override; + void showTeleportDialog(TeleportDialog *iw) override; + void showGarrisonDialog(ObjectInstanceID upobj, ObjectInstanceID hid, bool removableUnits) override; + void showObjectWindow(const CGObjectInstance * object, EOpenWindowMode window, const CGHeroInstance * visitor, bool addQuery) override; + void giveResource(PlayerColor player, GameResID which, int val) override; + void giveResources(PlayerColor player, TResources resources) override; + + void giveCreatures(const CArmedInstance *objid, const CGHeroInstance * h, const CCreatureSet &creatures, bool remove) override; + void takeCreatures(ObjectInstanceID objid, const std::vector &creatures) override; + bool changeStackType(const StackLocation &sl, const CCreature *c) override; + bool changeStackCount(const StackLocation &sl, TQuantity count, bool absoluteValue = false) override; + bool insertNewStack(const StackLocation &sl, const CCreature *c, TQuantity count) override; + bool eraseStack(const StackLocation &sl, bool forceRemoval = false) override; + bool swapStacks(const StackLocation &sl1, const StackLocation &sl2) override; + bool addToSlot(const StackLocation &sl, const CCreature *c, TQuantity count) override; + void tryJoiningArmy(const CArmedInstance *src, const CArmedInstance *dst, bool removeObjWhenFinished, bool allowMerging) override; + bool moveStack(const StackLocation &src, const StackLocation &dst, TQuantity count = -1) override; + + void removeAfterVisit(const CGObjectInstance *object) override; + + bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; + bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; + void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; + void removeArtifact(const ArtifactLocation &al) override; + bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; + bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); + bool eraseArtifactByClient(const ArtifactLocation & al); + void synchronizeArtifactHandlerLists(); + + void heroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; + void stopHeroVisitCastle(const CGTownInstance * obj, const CGHeroInstance * hero) override; + void startBattlePrimary(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool creatureBank = false, const CGTownInstance *town = nullptr) override; //use hero=nullptr for no hero + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, int3 tile, bool creatureBank = false) override; //if any of armies is hero, hero will be used + void startBattleI(const CArmedInstance *army1, const CArmedInstance *army2, bool creatureBank = false) override; //if any of armies is hero, hero will be used, visitable tile of second obj is place of battle + bool moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, bool transit = false, PlayerColor asker = PlayerColor::NEUTRAL) override; + void giveHeroBonus(GiveBonus * bonus) override; + void setMovePoints(SetMovePoints * smp) override; + void setManaPoints(ObjectInstanceID hid, int val) override; + void giveHero(ObjectInstanceID id, PlayerColor player, ObjectInstanceID boatId = ObjectInstanceID()) override; + void changeObjPos(ObjectInstanceID objid, int3 newPos, const PlayerColor & initiator) override; + void heroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2) override; + + void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override; + void changeFogOfWar(std::unordered_set &tiles, PlayerColor player, bool hide) override; + + void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override; + + /// Returns hero that is currently visiting this object, or nullptr if no visit is active + const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); + bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; + void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; + void showInfoDialog(InfoWindow * iw) override; + void showInfoDialog(const std::string & msg, PlayerColor player) override; + + ////////////////////////////////////////////////////////////////////////// + void useScholarSkill(ObjectInstanceID hero1, ObjectInstanceID hero2); + void setPortalDwelling(const CGTownInstance * town, bool forced, bool clear); + void visitObjectOnTile(const TerrainTile &t, const CGHeroInstance * h); + bool teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui8 source, PlayerColor asker = PlayerColor::NEUTRAL); + void visitCastleObjects(const CGTownInstance * obj, const CGHeroInstance * hero) override; + void levelUpHero(const CGHeroInstance * hero, SecondarySkill skill);//handle client respond and send one more request if needed + void levelUpHero(const CGHeroInstance * hero);//initial call - check if hero have remaining levelups & handle them + void levelUpCommander (const CCommanderInstance * c, int skill); //secondary skill 1 to 6, special skill : skill - 100 + void levelUpCommander (const CCommanderInstance * c); + + void expGiven(const CGHeroInstance *hero); //triggers needed level-ups, handles also commander of this hero + ////////////////////////////////////////////////////////////////////////// + + void init(StartInfo *si, Load::ProgressAccumulator & progressTracking); + void handleClientDisconnection(std::shared_ptr c); + void handleReceivedPack(CPackForServer * pack); + bool hasPlayerAt(PlayerColor player, std::shared_ptr c) const; + bool hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const; + + bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); + bool buildBoat( ObjectInstanceID objid, PlayerColor player ); + bool setFormation( ObjectInstanceID hid, ui8 formation ); + bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); + bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); + bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); + bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); + bool transformInUndead(const IMarket *market, const CGHeroInstance * hero, SlotID slot); + bool assembleArtifacts (ObjectInstanceID heroID, ArtifactPosition artifactSlot, bool assemble, ArtifactID assembleTo); + bool buyArtifact( ObjectInstanceID hid, ArtifactID aid ); //for blacksmith and mage guild only -> buying for gold in common buildings + bool buyArtifact( const IMarket *m, const CGHeroInstance *h, GameResID rid, ArtifactID aid); //for artifact merchant and black market -> buying for any resource in special building / advobject + bool sellArtifact( const IMarket *m, const CGHeroInstance *h, ArtifactInstanceID aid, GameResID rid); //for artifact merchant selling + //void lootArtifacts (TArtHolder source, TArtHolder dest, std::vector &arts); //after battle - move al arts to winer + bool buySecSkill( const IMarket *m, const CGHeroInstance *h, SecondarySkill skill); + bool garrisonSwap(ObjectInstanceID tid); + bool swapGarrisonOnSiege(ObjectInstanceID tid) override; + bool upgradeCreature( ObjectInstanceID objid, SlotID pos, CreatureID upgID ); + bool recruitCreatures(ObjectInstanceID objid, ObjectInstanceID dst, CreatureID crid, ui32 cram, si32 level, PlayerColor player); + bool buildStructure(ObjectInstanceID tid, BuildingID bid, bool force=false);//force - for events: no cost, no checkings + bool razeStructure(ObjectInstanceID tid, BuildingID bid); + bool disbandCreature( ObjectInstanceID id, SlotID pos ); + bool arrangeStacks( ObjectInstanceID id1, ObjectInstanceID id2, ui8 what, SlotID p1, SlotID p2, si32 val, PlayerColor player); + bool bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destArmy, SlotID srcSlot); + bool bulkSplitStack(SlotID src, ObjectInstanceID srcOwner, si32 howMany); + bool bulkMergeStacks(SlotID slotSrc, ObjectInstanceID srcOwner); + bool bulkSmartSplitStack(SlotID slotSrc, ObjectInstanceID srcOwner); + void save(const std::string &fname); + bool load(const std::string &fname); + + void onPlayerTurnStarted(PlayerColor which); + void onPlayerTurnEnded(PlayerColor which); + void onNewTurn(); + + void handleTimeEvents(); + void handleTownEvents(CGTownInstance *town, NewTurn &n); + bool complain(const std::string &problem); //sends message to all clients, prints on the logs and return true + void objectVisited( const CGObjectInstance * obj, const CGHeroInstance * h ); + void objectVisitEnded(const CObjectVisitQuery &query); + bool dig(const CGHeroInstance *h); + void moveArmy(const CArmedInstance *src, const CArmedInstance *dst, bool allowMerging); + + template void serialize(Handler &h, const int version) + { + h & QID; + h & getRandomGenerator(); + h & *battles; + h & *heroPool; + h & *playerMessages; + h & *turnOrder; + +#if SCRIPTING_ENABLED + JsonNode scriptsState; + if(h.saving) + serverScripts->serializeState(h.saving, scriptsState); + h & scriptsState; + if(!h.saving) + serverScripts->serializeState(h.saving, scriptsState); +#endif + } + + void sendToAllClients(CPackForClient * pack); + void sendAndApply(CPackForClient * pack) override; + void sendAndApply(CGarrisonOperationPack * pack); + void sendAndApply(SetResources * pack); + void sendAndApply(NewStructures * pack); + + void wrongPlayerMessage(CPackForServer * pack, PlayerColor expectedplayer); + /// Unconditionally throws with "Action not allowed" message + void throwNotAllowedAction(CPackForServer * pack); + /// Throws if player stated in pack is not making turn right now + void throwIfPlayerNotActive(CPackForServer * pack); + /// Throws if object is not owned by pack sender + void throwIfWrongOwner(CPackForServer * pack, ObjectInstanceID id); + /// Throws if player is not present on connection of this pack + void throwIfWrongPlayer(CPackForServer * pack, PlayerColor player); + void throwIfWrongPlayer(CPackForServer * pack); + void throwAndComplain(CPackForServer * pack, std::string txt); + + bool isPlayerOwns(CPackForServer * pack, ObjectInstanceID id); + + void run(bool resume); + bool sacrificeArtifact(const IMarket * m, const CGHeroInstance * hero, const std::vector & slot); + void spawnWanderingMonsters(CreatureID creatureID); + + // Check for victory and loss conditions + void checkVictoryLossConditionsForPlayer(PlayerColor player); + void checkVictoryLossConditions(const std::set & playerColors); + void checkVictoryLossConditionsForAll(); + + CRandomGenerator & getRandomGenerator(); + +#if SCRIPTING_ENABLED + scripting::Pool * getGlobalContextPool() const override; +// scripting::Pool * getContextPool() const override; +#endif + + friend class CVCMIServer; +private: + std::unique_ptr serverEventBus; +#if SCRIPTING_ENABLED + std::shared_ptr serverScripts; +#endif + + void reinitScripting(); + + void getVictoryLossMessage(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult, InfoWindow & out) const; + + const std::string complainNoCreatures; + const std::string complainNotEnoughCreatures; + const std::string complainInvalidSlot; +}; + +class ExceptionNotAllowedAction : public std::exception +{ + +}; diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 3447272d7..540751603 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -1,344 +1,344 @@ -/* - * NetPacksServer.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 "ServerNetPackVisitors.h" - -#include "CGameHandler.h" -#include "battles/BattleProcessor.h" -#include "processors/HeroPoolProcessor.h" -#include "processors/PlayerMessageProcessor.h" -#include "processors/TurnOrderProcessor.h" -#include "queries/QueriesProcessor.h" -#include "queries/MapQueries.h" - -#include "../lib/IGameCallback.h" -#include "../lib/mapObjects/CGTownInstance.h" -#include "../lib/gameState/CGameState.h" -#include "../lib/battle/IBattleState.h" -#include "../lib/battle/BattleAction.h" -#include "../lib/battle/Unit.h" -#include "../lib/serializer/Connection.h" -#include "../lib/spells/CSpellHandler.h" -#include "../lib/spells/ISpellMechanics.h" -#include "../lib/serializer/Cast.h" - -void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) -{ - gh.save(pack.fname); - logGlobal->info("Game has been saved as %s", pack.fname); - result = true; -} - -void ApplyGhNetPackVisitor::visitGamePause(GamePause & pack) -{ - auto turnQuery = std::make_shared(&gh, pack.player); - turnQuery->queryID = QueryID::CLIENT; - gh.queries->addQuery(turnQuery); - result = true; -} - -void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) -{ - gh.throwIfWrongPlayer(&pack); - result = gh.turnOrder->onPlayerEndsTurn(pack.player); -} - -void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.removeObject(gh.getObj(pack.hid), pack.player); -} - -void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, pack.player); -} - -void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - - result = gh.teleportHero(pack.hid, pack.dest, pack.source, pack.player); -} - -void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) -{ - gh.throwIfWrongPlayer(&pack); - result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, pack.player); -} - -void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) -{ - gh.throwIfWrongOwner(&pack, pack.srcArmy); - result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); -} - -void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack) -{ - gh.throwIfWrongPlayer(&pack); - result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount); -} - -void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack) -{ - gh.throwIfWrongPlayer(&pack); - result = gh.bulkMergeStacks(pack.src, pack.srcOwner); -} - -void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack) -{ - gh.throwIfWrongPlayer(&pack); - result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner); -} - -void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack) -{ - gh.throwIfWrongOwner(&pack, pack.id); - result = gh.disbandCreature(pack.id, pack.pos); -} - -void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) -{ - gh.throwIfWrongOwner(&pack, pack.tid); - result = gh.buildStructure(pack.tid, pack.bid); -} - -void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) -{ - gh.throwIfWrongPlayer(&pack); - // ownership checks are inside recruitCreatures - result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level, pack.player); -} - -void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) -{ - gh.throwIfWrongOwner(&pack, pack.id); - result = gh.upgradeCreature(pack.id, pack.pos, pack.cid); -} - -void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) -{ - const CGTownInstance * town = gh.getTown(pack.tid); - if(!gh.isPlayerOwns(&pack, pack.tid) && !(town->garrisonHero && gh.isPlayerOwns(&pack, town->garrisonHero->id))) - gh.throwNotAllowedAction(&pack); //neither town nor garrisoned hero (if present) is ours - result = gh.garrisonSwap(pack.tid); -} - -void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) -{ - gh.throwIfWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally - result = gh.moveArtifact(pack.src, pack.dst); -} - -void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) -{ - gh.throwIfWrongOwner(&pack, pack.srcHero); - if(pack.swap) - gh.throwIfWrongOwner(&pack, pack.dstHero); - result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); -} - -void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) -{ - gh.throwIfWrongOwner(&pack, pack.heroID); - result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo); -} - -void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) -{ - gh.throwIfWrongPlayer(&pack, pack.al.owningPlayer()); - result = gh.eraseArtifactByClient(pack.al); -} - -void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.buyArtifact(pack.hid, pack.aid); -} - -void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) -{ - const CGObjectInstance * object = gh.getObj(pack.marketId); - const CGHeroInstance * hero = gh.getHero(pack.heroId); - const IMarket * market = IMarket::castFrom(object); - - gh.throwIfWrongPlayer(&pack); - - if(!object) - gh.throwAndComplain(&pack, "Invalid market object"); - - if(!market) - gh.throwAndComplain(&pack, "market is not-a-market! :/"); - - bool heroCanBeInvalid = false; - - if (pack.mode == EMarketMode::RESOURCE_RESOURCE || pack.mode == EMarketMode::RESOURCE_PLAYER) - { - // For resource exchange we must use our own market or visit neutral market - if (object->getOwner().isValidPlayer()) - { - gh.throwIfWrongOwner(&pack, pack.marketId); - heroCanBeInvalid = true; - } - } - - if (pack.mode == EMarketMode::CREATURE_UNDEAD) - { - // For skeleton transformer, if hero is null then object must be owned - if (!hero) - { - gh.throwIfWrongOwner(&pack, pack.marketId); - heroCanBeInvalid = true; - } - } - - if (!heroCanBeInvalid) - { - gh.throwIfWrongOwner(&pack, pack.heroId); - - if (!hero) - gh.throwAndComplain(&pack, "Can not trade - no hero!"); - - // TODO: check that object is actually being visited (e.g. Query exists) - if (!object->visitableAt(hero->visitablePos().x, hero->visitablePos().y)) - gh.throwAndComplain(&pack, "Can not trade - object not visited!"); - - if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES) - gh.throwAndComplain(&pack, "Can not trade - market not owned!"); - } - - result = true; - - switch(pack.mode) - { - case EMarketMode::RESOURCE_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i], pack.r2[i]); - break; - case EMarketMode::RESOURCE_PLAYER: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sendResources(pack.val[i], pack.player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); - break; - case EMarketMode::CREATURE_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellCreatures(pack.val[i], market, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); - break; - case EMarketMode::RESOURCE_ARTIFACT: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.buyArtifact(market, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); - break; - case EMarketMode::ARTIFACT_RESOURCE: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellArtifact(market, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); - break; - case EMarketMode::CREATURE_UNDEAD: - for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.transformInUndead(market, hero, SlotID(pack.r1[i])); - break; - case EMarketMode::RESOURCE_SKILL: - for(int i = 0; i < pack.r2.size(); ++i) - result &= gh.buySecSkill(market, hero, SecondarySkill(pack.r2[i])); - break; - case EMarketMode::CREATURE_EXP: - { - std::vector slotIDs(pack.r1.begin(), pack.r1.end()); - std::vector count(pack.val.begin(), pack.val.end()); - result = gh.sacrificeCreatures(market, hero, slotIDs, count); - return; - } - case EMarketMode::ARTIFACT_EXP: - { - std::vector positions(pack.r1.begin(), pack.r1.end()); - result = gh.sacrificeArtifact(market, hero, positions); - return; - } - default: - gh.throwAndComplain(&pack, "Unknown exchange pack.mode!"); - } -} - -void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - result = gh.setFormation(pack.hid, pack.formation); -} - -void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) -{ - gh.throwIfWrongPlayer(&pack); - - result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); -} - -void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) -{ - gh.throwIfWrongPlayer(&pack); - - if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES) - gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard"); - - result = gh.buildBoat(pack.objid, pack.player); -} - -void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) -{ - gh.throwIfWrongPlayer(&pack); - - auto playerToConnection = gh.connections.find(pack.player); - if(playerToConnection == gh.connections.end()) - gh.throwAndComplain(&pack, "No such pack.player!"); - if(!vstd::contains(playerToConnection->second, pack.c)) - gh.throwAndComplain(&pack, "Message came from wrong connection!"); - if(pack.qid == QueryID(-1)) - gh.throwAndComplain(&pack, "Cannot answer the query with pack.id -1!"); - - result = gh.queryReply(pack.qid, pack.reply, pack.player); -} - -void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) -{ - gh.throwIfWrongPlayer(&pack); - - result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba); -} - -void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) -{ - gh.throwIfWrongOwner(&pack, pack.id); - result = gh.dig(gh.getHero(pack.id)); -} - -void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) -{ - gh.throwIfWrongOwner(&pack, pack.hid); - - const CSpell * s = pack.sid.toSpell(); - if(!s) - gh.throwNotAllowedAction(&pack); - const CGHeroInstance * h = gh.getHero(pack.hid); - if(!h) - gh.throwNotAllowedAction(&pack); - - AdventureSpellCastParameters p; - p.caster = h; - p.pos = pack.pos; - - result = s->adventureCast(gh.spellEnv, p); -} - -void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) -{ - if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions - gh.throwIfWrongPlayer(&pack, pack.player); - - gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); - result = true; -} +/* + * NetPacksServer.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 "ServerNetPackVisitors.h" + +#include "CGameHandler.h" +#include "battles/BattleProcessor.h" +#include "processors/HeroPoolProcessor.h" +#include "processors/PlayerMessageProcessor.h" +#include "processors/TurnOrderProcessor.h" +#include "queries/QueriesProcessor.h" +#include "queries/MapQueries.h" + +#include "../lib/IGameCallback.h" +#include "../lib/mapObjects/CGTownInstance.h" +#include "../lib/gameState/CGameState.h" +#include "../lib/battle/IBattleState.h" +#include "../lib/battle/BattleAction.h" +#include "../lib/battle/Unit.h" +#include "../lib/serializer/Connection.h" +#include "../lib/spells/CSpellHandler.h" +#include "../lib/spells/ISpellMechanics.h" +#include "../lib/serializer/Cast.h" + +void ApplyGhNetPackVisitor::visitSaveGame(SaveGame & pack) +{ + gh.save(pack.fname); + logGlobal->info("Game has been saved as %s", pack.fname); + result = true; +} + +void ApplyGhNetPackVisitor::visitGamePause(GamePause & pack) +{ + auto turnQuery = std::make_shared(&gh, pack.player); + turnQuery->queryID = QueryID::CLIENT; + gh.queries->addQuery(turnQuery); + result = true; +} + +void ApplyGhNetPackVisitor::visitEndTurn(EndTurn & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.turnOrder->onPlayerEndsTurn(pack.player); +} + +void ApplyGhNetPackVisitor::visitDismissHero(DismissHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.removeObject(gh.getObj(pack.hid), pack.player); +} + +void ApplyGhNetPackVisitor::visitMoveHero(MoveHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.moveHero(pack.hid, pack.dest, 0, pack.transit, pack.player); +} + +void ApplyGhNetPackVisitor::visitCastleTeleportHero(CastleTeleportHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + + result = gh.teleportHero(pack.hid, pack.dest, pack.source, pack.player); +} + +void ApplyGhNetPackVisitor::visitArrangeStacks(ArrangeStacks & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.arrangeStacks(pack.id1, pack.id2, pack.what, pack.p1, pack.p2, pack.val, pack.player); +} + +void ApplyGhNetPackVisitor::visitBulkMoveArmy(BulkMoveArmy & pack) +{ + gh.throwIfWrongOwner(&pack, pack.srcArmy); + result = gh.bulkMoveArmy(pack.srcArmy, pack.destArmy, pack.srcSlot); +} + +void ApplyGhNetPackVisitor::visitBulkSplitStack(BulkSplitStack & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkSplitStack(pack.src, pack.srcOwner, pack.amount); +} + +void ApplyGhNetPackVisitor::visitBulkMergeStacks(BulkMergeStacks & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkMergeStacks(pack.src, pack.srcOwner); +} + +void ApplyGhNetPackVisitor::visitBulkSmartSplitStack(BulkSmartSplitStack & pack) +{ + gh.throwIfWrongPlayer(&pack); + result = gh.bulkSmartSplitStack(pack.src, pack.srcOwner); +} + +void ApplyGhNetPackVisitor::visitDisbandCreature(DisbandCreature & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.disbandCreature(pack.id, pack.pos); +} + +void ApplyGhNetPackVisitor::visitBuildStructure(BuildStructure & pack) +{ + gh.throwIfWrongOwner(&pack, pack.tid); + result = gh.buildStructure(pack.tid, pack.bid); +} + +void ApplyGhNetPackVisitor::visitRecruitCreatures(RecruitCreatures & pack) +{ + gh.throwIfWrongPlayer(&pack); + // ownership checks are inside recruitCreatures + result = gh.recruitCreatures(pack.tid, pack.dst, pack.crid, pack.amount, pack.level, pack.player); +} + +void ApplyGhNetPackVisitor::visitUpgradeCreature(UpgradeCreature & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.upgradeCreature(pack.id, pack.pos, pack.cid); +} + +void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) +{ + const CGTownInstance * town = gh.getTown(pack.tid); + if(!gh.isPlayerOwns(&pack, pack.tid) && !(town->garrisonHero && gh.isPlayerOwns(&pack, town->garrisonHero->id))) + gh.throwNotAllowedAction(&pack); //neither town nor garrisoned hero (if present) is ours + result = gh.garrisonSwap(pack.tid); +} + +void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) +{ + gh.throwIfWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally + result = gh.moveArtifact(pack.src, pack.dst); +} + +void ApplyGhNetPackVisitor::visitBulkExchangeArtifacts(BulkExchangeArtifacts & pack) +{ + gh.throwIfWrongOwner(&pack, pack.srcHero); + if(pack.swap) + gh.throwIfWrongOwner(&pack, pack.dstHero); + result = gh.bulkMoveArtifacts(pack.srcHero, pack.dstHero, pack.swap, pack.equipped, pack.backpack); +} + +void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) +{ + gh.throwIfWrongOwner(&pack, pack.heroID); + result = gh.assembleArtifacts(pack.heroID, pack.artifactSlot, pack.assemble, pack.assembleTo); +} + +void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) +{ + gh.throwIfWrongPlayer(&pack, pack.al.owningPlayer()); + result = gh.eraseArtifactByClient(pack.al); +} + +void ApplyGhNetPackVisitor::visitBuyArtifact(BuyArtifact & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.buyArtifact(pack.hid, pack.aid); +} + +void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) +{ + const CGObjectInstance * object = gh.getObj(pack.marketId); + const CGHeroInstance * hero = gh.getHero(pack.heroId); + const IMarket * market = IMarket::castFrom(object); + + gh.throwIfWrongPlayer(&pack); + + if(!object) + gh.throwAndComplain(&pack, "Invalid market object"); + + if(!market) + gh.throwAndComplain(&pack, "market is not-a-market! :/"); + + bool heroCanBeInvalid = false; + + if (pack.mode == EMarketMode::RESOURCE_RESOURCE || pack.mode == EMarketMode::RESOURCE_PLAYER) + { + // For resource exchange we must use our own market or visit neutral market + if (object->getOwner().isValidPlayer()) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } + + if (pack.mode == EMarketMode::CREATURE_UNDEAD) + { + // For skeleton transformer, if hero is null then object must be owned + if (!hero) + { + gh.throwIfWrongOwner(&pack, pack.marketId); + heroCanBeInvalid = true; + } + } + + if (!heroCanBeInvalid) + { + gh.throwIfWrongOwner(&pack, pack.heroId); + + if (!hero) + gh.throwAndComplain(&pack, "Can not trade - no hero!"); + + // TODO: check that object is actually being visited (e.g. Query exists) + if (!object->visitableAt(hero->visitablePos().x, hero->visitablePos().y)) + gh.throwAndComplain(&pack, "Can not trade - object not visited!"); + + if (object->getOwner().isValidPlayer() && gh.getPlayerRelations(object->getOwner(), hero->getOwner()) == PlayerRelations::ENEMIES) + gh.throwAndComplain(&pack, "Can not trade - market not owned!"); + } + + result = true; + + switch(pack.mode) + { + case EMarketMode::RESOURCE_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i], pack.r2[i]); + break; + case EMarketMode::RESOURCE_PLAYER: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sendResources(pack.val[i], pack.player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); + break; + case EMarketMode::CREATURE_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sellCreatures(pack.val[i], market, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); + break; + case EMarketMode::RESOURCE_ARTIFACT: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.buyArtifact(market, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); + break; + case EMarketMode::ARTIFACT_RESOURCE: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.sellArtifact(market, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); + break; + case EMarketMode::CREATURE_UNDEAD: + for(int i = 0; i < pack.r1.size(); ++i) + result &= gh.transformInUndead(market, hero, SlotID(pack.r1[i])); + break; + case EMarketMode::RESOURCE_SKILL: + for(int i = 0; i < pack.r2.size(); ++i) + result &= gh.buySecSkill(market, hero, SecondarySkill(pack.r2[i])); + break; + case EMarketMode::CREATURE_EXP: + { + std::vector slotIDs(pack.r1.begin(), pack.r1.end()); + std::vector count(pack.val.begin(), pack.val.end()); + result = gh.sacrificeCreatures(market, hero, slotIDs, count); + return; + } + case EMarketMode::ARTIFACT_EXP: + { + std::vector positions(pack.r1.begin(), pack.r1.end()); + result = gh.sacrificeArtifact(market, hero, positions); + return; + } + default: + gh.throwAndComplain(&pack, "Unknown exchange pack.mode!"); + } +} + +void ApplyGhNetPackVisitor::visitSetFormation(SetFormation & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + result = gh.setFormation(pack.hid, pack.formation); +} + +void ApplyGhNetPackVisitor::visitHireHero(HireHero & pack) +{ + gh.throwIfWrongPlayer(&pack); + + result = gh.heroPool->hireHero(pack.tid, pack.hid, pack.player); +} + +void ApplyGhNetPackVisitor::visitBuildBoat(BuildBoat & pack) +{ + gh.throwIfWrongPlayer(&pack); + + if(gh.getPlayerRelations(gh.getOwner(pack.objid), pack.player) == PlayerRelations::ENEMIES) + gh.throwAndComplain(&pack, "Can't build boat at enemy shipyard"); + + result = gh.buildBoat(pack.objid, pack.player); +} + +void ApplyGhNetPackVisitor::visitQueryReply(QueryReply & pack) +{ + gh.throwIfWrongPlayer(&pack); + + auto playerToConnection = gh.connections.find(pack.player); + if(playerToConnection == gh.connections.end()) + gh.throwAndComplain(&pack, "No such pack.player!"); + if(!vstd::contains(playerToConnection->second, pack.c)) + gh.throwAndComplain(&pack, "Message came from wrong connection!"); + if(pack.qid == QueryID(-1)) + gh.throwAndComplain(&pack, "Cannot answer the query with pack.id -1!"); + + result = gh.queryReply(pack.qid, pack.reply, pack.player); +} + +void ApplyGhNetPackVisitor::visitMakeAction(MakeAction & pack) +{ + gh.throwIfWrongPlayer(&pack); + + result = gh.battles->makePlayerBattleAction(pack.battleID, pack.player, pack.ba); +} + +void ApplyGhNetPackVisitor::visitDigWithHero(DigWithHero & pack) +{ + gh.throwIfWrongOwner(&pack, pack.id); + result = gh.dig(gh.getHero(pack.id)); +} + +void ApplyGhNetPackVisitor::visitCastAdvSpell(CastAdvSpell & pack) +{ + gh.throwIfWrongOwner(&pack, pack.hid); + + const CSpell * s = pack.sid.toSpell(); + if(!s) + gh.throwNotAllowedAction(&pack); + const CGHeroInstance * h = gh.getHero(pack.hid); + if(!h) + gh.throwNotAllowedAction(&pack); + + AdventureSpellCastParameters p; + p.caster = h; + p.pos = pack.pos; + + result = s->adventureCast(gh.spellEnv, p); +} + +void ApplyGhNetPackVisitor::visitPlayerMessage(PlayerMessage & pack) +{ + if(!pack.player.isSpectator()) // TODO: clearly not a great way to verify permissions + gh.throwIfWrongPlayer(&pack, pack.player); + + gh.playerMessages->playerMessage(pack.player, pack.text, pack.currObj); + result = true; +} diff --git a/server/StdInc.cpp b/server/StdInc.cpp index f500fe6d0..dd7f66cb8 100644 --- a/server/StdInc.cpp +++ b/server/StdInc.cpp @@ -1,2 +1,2 @@ -// Creates the precompiled header +// Creates the precompiled header #include "StdInc.h" diff --git a/server/StdInc.h b/server/StdInc.h index d26d2298c..d03216bdf 100644 --- a/server/StdInc.h +++ b/server/StdInc.h @@ -1,14 +1,14 @@ -/* - * StdInc.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 "../Global.h" - -VCMI_LIB_USING_NAMESPACE +/* + * StdInc.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 "../Global.h" + +VCMI_LIB_USING_NAMESPACE diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index c167c7b8a..406d0bee9 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -1,196 +1,196 @@ -/* - * CQuery.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 "CQuery.h" - -#include "QueriesProcessor.h" - -#include "../CGameHandler.h" - -#include "../../lib/serializer/Cast.h" -#include "../../lib/NetPacks.h" - -template -std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") -{ - std::string ret = opener; - auto itr = std::begin(c); - if(itr != std::end(c)) - { - ret += std::to_string(*itr); - while(++itr != std::end(c)) - { - ret += delimeter; - ret += std::to_string(*itr); - } - } - ret += closer; - return ret; -} - -std::ostream & operator<<(std::ostream & out, const CQuery & query) -{ - return out << query.toString(); -} - -std::ostream & operator<<(std::ostream & out, QueryPtr query) -{ - return out << "[" << query.get() << "] " << query->toString(); -} - -CQuery::CQuery(CGameHandler * gameHandler) - : owner(gameHandler->queries.get()) - , gh(gameHandler) -{ - boost::unique_lock l(QueriesProcessor::mx); - - static QueryID QID = QueryID(0); - - queryID = ++QID; - logGlobal->trace("Created a new query with id %d", queryID); -} - -CQuery::~CQuery() -{ - logGlobal->trace("Destructed the query with id %d", queryID); -} - -void CQuery::addPlayer(PlayerColor color) -{ - if(color.isValidPlayer()) - players.push_back(color); -} - -std::string CQuery::toString() const -{ - const auto size = players.size(); - const std::string plural = size > 1 ? "s" : ""; - std::string names; - - for(size_t i = 0; i < size; i++) - { - names += boost::to_upper_copy(players[i].toString()); - - if(i < size - 2) - names += ", "; - else if(size > 1 && i == size - 2) - names += " and "; - } - std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") - % typeid(*this).name() - % queryID - % plural - % names - ); - return ret; -} - -bool CQuery::endsByPlayerAnswer() const -{ - return false; -} - -void CQuery::onRemoval(PlayerColor color) -{ - -} - -bool CQuery::blocksPack(const CPack * pack) const -{ - return false; -} - -void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const -{ - -} - -void CQuery::onExposure(QueryPtr topQuery) -{ - logGlobal->trace("Exposed query with id %d", queryID); - owner->popQuery(*this); -} - -void CQuery::onAdding(PlayerColor color) -{ - -} - -void CQuery::onAdded(PlayerColor color) -{ - -} - -void CQuery::setReply(std::optional reply) -{ - -} - -bool CQuery::blockAllButReply(const CPack * pack) const -{ - //We accept only query replies from correct player - if(auto reply = dynamic_ptr_cast(pack)) - return !vstd::contains(players, reply->player); - - return true; -} - -CDialogQuery::CDialogQuery(CGameHandler * owner): - CQuery(owner) -{ - -} - -bool CDialogQuery::endsByPlayerAnswer() const -{ - return true; -} - -bool CDialogQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -void CDialogQuery::setReply(std::optional reply) -{ - if(reply.has_value()) - answer = *reply; -} - -CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback): - CQuery(gh), callback(Callback) -{ - addPlayer(color); -} - -bool CGenericQuery::blocksPack(const CPack * pack) const -{ - return blockAllButReply(pack); -} - -bool CGenericQuery::endsByPlayerAnswer() const -{ - return true; -} - -void CGenericQuery::onExposure(QueryPtr topQuery) -{ - //do nothing -} - -void CGenericQuery::setReply(std::optional reply) -{ - this->reply = reply; -} - -void CGenericQuery::onRemoval(PlayerColor color) -{ - callback(reply); -} +/* + * CQuery.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 "CQuery.h" + +#include "QueriesProcessor.h" + +#include "../CGameHandler.h" + +#include "../../lib/serializer/Cast.h" +#include "../../lib/NetPacks.h" + +template +std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") +{ + std::string ret = opener; + auto itr = std::begin(c); + if(itr != std::end(c)) + { + ret += std::to_string(*itr); + while(++itr != std::end(c)) + { + ret += delimeter; + ret += std::to_string(*itr); + } + } + ret += closer; + return ret; +} + +std::ostream & operator<<(std::ostream & out, const CQuery & query) +{ + return out << query.toString(); +} + +std::ostream & operator<<(std::ostream & out, QueryPtr query) +{ + return out << "[" << query.get() << "] " << query->toString(); +} + +CQuery::CQuery(CGameHandler * gameHandler) + : owner(gameHandler->queries.get()) + , gh(gameHandler) +{ + boost::unique_lock l(QueriesProcessor::mx); + + static QueryID QID = QueryID(0); + + queryID = ++QID; + logGlobal->trace("Created a new query with id %d", queryID); +} + +CQuery::~CQuery() +{ + logGlobal->trace("Destructed the query with id %d", queryID); +} + +void CQuery::addPlayer(PlayerColor color) +{ + if(color.isValidPlayer()) + players.push_back(color); +} + +std::string CQuery::toString() const +{ + const auto size = players.size(); + const std::string plural = size > 1 ? "s" : ""; + std::string names; + + for(size_t i = 0; i < size; i++) + { + names += boost::to_upper_copy(players[i].toString()); + + if(i < size - 2) + names += ", "; + else if(size > 1 && i == size - 2) + names += " and "; + } + std::string ret = boost::str(boost::format("A query of type '%s' and qid = %d affecting player%s %s") + % typeid(*this).name() + % queryID + % plural + % names + ); + return ret; +} + +bool CQuery::endsByPlayerAnswer() const +{ + return false; +} + +void CQuery::onRemoval(PlayerColor color) +{ + +} + +bool CQuery::blocksPack(const CPack * pack) const +{ + return false; +} + +void CQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const +{ + +} + +void CQuery::onExposure(QueryPtr topQuery) +{ + logGlobal->trace("Exposed query with id %d", queryID); + owner->popQuery(*this); +} + +void CQuery::onAdding(PlayerColor color) +{ + +} + +void CQuery::onAdded(PlayerColor color) +{ + +} + +void CQuery::setReply(std::optional reply) +{ + +} + +bool CQuery::blockAllButReply(const CPack * pack) const +{ + //We accept only query replies from correct player + if(auto reply = dynamic_ptr_cast(pack)) + return !vstd::contains(players, reply->player); + + return true; +} + +CDialogQuery::CDialogQuery(CGameHandler * owner): + CQuery(owner) +{ + +} + +bool CDialogQuery::endsByPlayerAnswer() const +{ + return true; +} + +bool CDialogQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +void CDialogQuery::setReply(std::optional reply) +{ + if(reply.has_value()) + answer = *reply; +} + +CGenericQuery::CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback): + CQuery(gh), callback(Callback) +{ + addPlayer(color); +} + +bool CGenericQuery::blocksPack(const CPack * pack) const +{ + return blockAllButReply(pack); +} + +bool CGenericQuery::endsByPlayerAnswer() const +{ + return true; +} + +void CGenericQuery::onExposure(QueryPtr topQuery) +{ + //do nothing +} + +void CGenericQuery::setReply(std::optional reply) +{ + this->reply = reply; +} + +void CGenericQuery::onRemoval(PlayerColor color) +{ + callback(reply); +} diff --git a/server/queries/CQuery.h b/server/queries/CQuery.h index 5c17c44bc..cf6e18254 100644 --- a/server/queries/CQuery.h +++ b/server/queries/CQuery.h @@ -1,91 +1,91 @@ -/* - * CQuery.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/GameConstants.h" - -VCMI_LIB_NAMESPACE_BEGIN - -struct CPack; - -VCMI_LIB_NAMESPACE_END - -class CObjectVisitQuery; -class QueriesProcessor; -class CQuery; -class CGameHandler; - -using QueryPtr = std::shared_ptr; - -// This class represents any kind of prolonged interaction that may need to do something special after it is over. -// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. -// Examples: -// - all kinds of blocking dialog windows -// - battle -// - object visit -// - hero movement -// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. -class CQuery -{ -public: - std::vector players; //players that are affected (often "blocked") by query - QueryID queryID; - - CQuery(CGameHandler * gh); - - virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. - - virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs) - virtual void onAdding(PlayerColor color); //called just before query is pushed on stack - virtual void onAdded(PlayerColor color); //called right after query is pushed on stack - virtual void onRemoval(PlayerColor color); //called after query is removed from stack - virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top) - virtual std::string toString() const; - - virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; - - virtual void setReply(std::optional reply); - - virtual ~CQuery(); -protected: - QueriesProcessor * owner; - CGameHandler * gh; - void addPlayer(PlayerColor color); - bool blockAllButReply(const CPack * pack) const; -}; - -std::ostream &operator<<(std::ostream &out, const CQuery &query); -std::ostream &operator<<(std::ostream &out, QueryPtr query); - -class CDialogQuery : public CQuery -{ -public: - CDialogQuery(CGameHandler * owner); - virtual bool endsByPlayerAnswer() const override; - virtual bool blocksPack(const CPack *pack) const override; - void setReply(std::optional reply) override; -protected: - std::optional answer; -}; - -class CGenericQuery : public CQuery -{ -public: - CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback); - - bool blocksPack(const CPack * pack) const override; - bool endsByPlayerAnswer() const override; - void onExposure(QueryPtr topQuery) override; - void setReply(std::optional reply) override; - void onRemoval(PlayerColor color) override; -private: - std::function)> callback; - std::optional reply; -}; +/* + * CQuery.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/GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct CPack; + +VCMI_LIB_NAMESPACE_END + +class CObjectVisitQuery; +class QueriesProcessor; +class CQuery; +class CGameHandler; + +using QueryPtr = std::shared_ptr; + +// This class represents any kind of prolonged interaction that may need to do something special after it is over. +// It does not necessarily has to be "query" requiring player action, it can be also used internally within server. +// Examples: +// - all kinds of blocking dialog windows +// - battle +// - object visit +// - hero movement +// Queries can cause another queries, forming a stack of queries for each player. Eg: hero movement -> object visit -> dialog. +class CQuery +{ +public: + std::vector players; //players that are affected (often "blocked") by query + QueryID queryID; + + CQuery(CGameHandler * gh); + + virtual bool blocksPack(const CPack *pack) const; //query can block attempting actions by player. Eg. he can't move hero during the battle. + + virtual bool endsByPlayerAnswer() const; //query is removed after player gives answer (like dialogs) + virtual void onAdding(PlayerColor color); //called just before query is pushed on stack + virtual void onAdded(PlayerColor color); //called right after query is pushed on stack + virtual void onRemoval(PlayerColor color); //called after query is removed from stack + virtual void onExposure(QueryPtr topQuery);//called when query immediately above is removed and this is exposed (becomes top) + virtual std::string toString() const; + + virtual void notifyObjectAboutRemoval(const CObjectVisitQuery &objectVisit) const; + + virtual void setReply(std::optional reply); + + virtual ~CQuery(); +protected: + QueriesProcessor * owner; + CGameHandler * gh; + void addPlayer(PlayerColor color); + bool blockAllButReply(const CPack * pack) const; +}; + +std::ostream &operator<<(std::ostream &out, const CQuery &query); +std::ostream &operator<<(std::ostream &out, QueryPtr query); + +class CDialogQuery : public CQuery +{ +public: + CDialogQuery(CGameHandler * owner); + virtual bool endsByPlayerAnswer() const override; + virtual bool blocksPack(const CPack *pack) const override; + void setReply(std::optional reply) override; +protected: + std::optional answer; +}; + +class CGenericQuery : public CQuery +{ +public: + CGenericQuery(CGameHandler * gh, PlayerColor color, std::function)> Callback); + + bool blocksPack(const CPack * pack) const override; + bool endsByPlayerAnswer() const override; + void onExposure(QueryPtr topQuery) override; + void setReply(std::optional reply) override; + void onRemoval(PlayerColor color) override; +private: + std::function)> callback; + std::optional reply; +}; diff --git a/test/StdInc.cpp b/test/StdInc.cpp index 201d9a246..02b98775f 100644 --- a/test/StdInc.cpp +++ b/test/StdInc.cpp @@ -1,12 +1,12 @@ -/* - * StdInc.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 - * - */ -// Creates the precompiled header -#include "StdInc.h" - +/* + * StdInc.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 + * + */ +// Creates the precompiled header +#include "StdInc.h" + diff --git a/test/StdInc.h b/test/StdInc.h index 51b284fcc..e4db74bc5 100644 --- a/test/StdInc.h +++ b/test/StdInc.h @@ -1,13 +1,13 @@ -/* - * StdInc.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 "gtest/gtest.h" -#include "gmock/gmock.h" -#include "../Global.h" +/* + * StdInc.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 "gtest/gtest.h" +#include "gmock/gmock.h" +#include "../Global.h" From 068a7d5d4ae14b78ee0aa34b10a330b4878fff58 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Thu, 19 Oct 2023 21:22:26 +0300 Subject: [PATCH 0877/1248] crash fixed --- client/widgets/CArtifactHolder.cpp | 51 ++++++++++++++---------------- client/widgets/CArtifactHolder.h | 2 +- client/widgets/Images.cpp | 12 +++++++ client/widgets/Images.h | 6 +++- 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index f7434a679..4b19e92c9 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -30,46 +30,48 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) { - baseType = -1; // By default we don't store any component ourArt = artInst; if(!artInst) { image->disable(); - imageSpell->disable(); text.clear(); hoverText = CGI->generaltexth->allTexts[507]; return; } - image->enable(); - imageSpell->disable(); - image->setFrame(artInst->artType->getIconIndex()); - + imageIndex = artInst->artType->getIconIndex(); if(artInst->getTypeId() == ArtifactID::SPELL_SCROLL) { auto spellID = artInst->getScrollSpellID(); - if(spellID.num >= 0) - { - // Add spell component info (used to provide a pic in r-click popup) - baseType = CComponent::spell; - type = spellID; - bonusValue = 0; + assert(spellID.num >= 0); - if(settings["general"]["enableUiEnhancements"].Bool()) + if(settings["general"]["enableUiEnhancements"].Bool()) + { + imageIndex = spellID.num; + if(baseType != CComponent::spell) { - imageSpell->enable(); - image->disable(); - imageSpell->setFrame(spellID.num); + image->setScale(Point(pos.w, 34)); + image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex); + image->moveTo(Point(pos.x, pos.y + 4)); } } + // Add spell component info (used to provide a pic in r-click popup) + baseType = CComponent::spell; + type = spellID; } else { + if(settings["general"]["enableUiEnhancements"].Bool() && baseType != CComponent::artifact) + { + image->setScale(Point()); + image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex); + image->moveTo(Point(pos.x, pos.y)); + } baseType = CComponent::artifact; type = artInst->getTypeId(); - bonusValue = 0; } - + bonusValue = 0; + image->enable(); text = artInst->getDescription(); } @@ -99,7 +101,7 @@ void CCommanderArtPlace::createImage() { OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - int imageIndex = 0; + imageIndex = 0; if(ourArt) imageIndex = ourArt->artType->getIconIndex(); @@ -165,7 +167,7 @@ void CHeroArtPlace::lockSlot(bool on) if(on) image->setFrame(ArtifactID::ART_LOCK); else if(ourArt) - image->setFrame(ourArt->artType->getIconIndex()); + image->setFrame(imageIndex); else image->setFrame(0); } @@ -220,7 +222,7 @@ void CHeroArtPlace::setArtifact(const CArtifactInstance * art) setInternals(art); if(art) { - image->setFrame(locked ? ArtifactID::ART_LOCK : art->artType->getIconIndex()); + image->setFrame(locked ? static_cast(ArtifactID::ART_LOCK) : imageIndex); if(locked) // Locks should appear as empty. hoverText = CGI->generaltexth->allTexts[507]; @@ -261,13 +263,8 @@ void CHeroArtPlace::createImage() else if(ourArt) imageIndex = ourArt->artType->getIconIndex(); - imageSpell = std::make_shared(GH.renderHandler().loadAnimation(AnimationPath::builtin("spellscr")), 0, Rect(0, 5, 44, 34)); image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - if(!ourArt) - { - image->disable(); - imageSpell->disable(); - } + image->disable(); selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); selection->disable(); diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index 16dd79764..5f8648a3f 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -34,8 +34,8 @@ class CArtPlace : public LRClickableAreaWTextComp { protected: std::shared_ptr image; - std::shared_ptr imageSpell; const CArtifactInstance * ourArt; + int imageIndex; void setInternals(const CArtifactInstance * artInst); virtual void createImage()=0; diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 8e59f81cb..cac50a7eb 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -275,6 +275,18 @@ void CAnimImage::showAll(Canvas & to) } } +void CAnimImage::setAnimationPath(const AnimationPath & name, size_t frame) +{ + this->frame = frame; + anim = GH.renderHandler().loadAnimation(name); + init(); +} + +void CAnimImage::setScale(Point scale) +{ + scaledSize = scale; +} + void CAnimImage::setFrame(size_t Frame, size_t Group) { if (frame == Frame && group==Group) diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 7716cf8e9..0e2fcf5a0 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -95,7 +95,7 @@ private: size_t frame; size_t group; ui8 flags; - const Point scaledSize; + Point scaledSize; /// If set, then image is colored using player-specific palette std::optional player; @@ -124,6 +124,10 @@ public: bool isPlayerColored() const; void showAll(Canvas & to) override; + + void setAnimationPath(const AnimationPath & name, size_t frame); + + void setScale(Point scale); }; /// Base class for displaying animation, used as superclass for different animations From b9a660f6c34c2e3b935d0cb7d9c3c03f74b80d8c Mon Sep 17 00:00:00 2001 From: nordsoft Date: Fri, 20 Oct 2023 01:25:06 +0200 Subject: [PATCH 0878/1248] Redesign map editor rendering --- lib/mapping/ObstacleProxy.cpp | 3 +- lib/mapping/ObstacleProxy.h | 4 +- mapeditor/mainwindow.cpp | 2 - mapeditor/mapcontroller.cpp | 16 +- mapeditor/maphandler.cpp | 308 ++++++++++++---------------------- mapeditor/maphandler.h | 50 +++--- mapeditor/scenelayer.cpp | 8 +- 7 files changed, 151 insertions(+), 240 deletions(-) diff --git a/lib/mapping/ObstacleProxy.cpp b/lib/mapping/ObstacleProxy.cpp index d6a30de33..fd295572b 100644 --- a/lib/mapping/ObstacleProxy.cpp +++ b/lib/mapping/ObstacleProxy.cpp @@ -208,10 +208,11 @@ bool EditorObstaclePlacer::isInTheMap(const int3& tile) return map->isInTheMap(tile); } -void EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) +std::set EditorObstaclePlacer::placeObstacles(CRandomGenerator & rand) { auto obstacles = createObstacles(rand); finalInsertion(map->getEditManager(), obstacles); + return obstacles; } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapping/ObstacleProxy.h b/lib/mapping/ObstacleProxy.h index ef30e4bd0..70f1f46df 100644 --- a/lib/mapping/ObstacleProxy.h +++ b/lib/mapping/ObstacleProxy.h @@ -67,10 +67,10 @@ public: bool isInTheMap(const int3& tile) override; - void placeObstacles(CRandomGenerator& rand); + std::set placeObstacles(CRandomGenerator& rand); private: CMap* map; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 206dd6c8e..680e4337a 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -1095,9 +1095,7 @@ void MainWindow::on_actionUpdate_appearance_triggered() } app = templates.front(); } - auto tiles = controller.mapHandler()->getTilesUnderObject(obj); obj->appearance = app; - controller.mapHandler()->invalidate(tiles); controller.mapHandler()->invalidate(obj); controller.scene(mapLevel)->selectionObjectsView.deselectObject(obj); } diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index ee0ba16fc..4912541de 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -350,10 +350,10 @@ void MapController::commitObjectErase(int level) return; } - for (auto obj : selectedObjects) + for (auto & obj : selectedObjects) { //invalidate tiles under objects - _mapHandler->invalidate(_mapHandler->getTilesUnderObject(obj)); + _mapHandler->removeObject(obj); _scenes[level]->objectsView.setDirty(obj); } @@ -454,14 +454,16 @@ void MapController::commitObstacleFill(int level) for(auto & sel : _obstaclePainters) { - sel.second->placeObstacles(CRandomGenerator::getDefault()); + for(auto * o : sel.second->placeObstacles(CRandomGenerator::getDefault())) + { + _mapHandler->invalidate(o); + _scenes[level]->objectsView.setDirty(o); + } } - - _mapHandler->invalidateObjects(); _scenes[level]->selectionTerrainView.clear(); _scenes[level]->selectionTerrainView.draw(); - _scenes[level]->objectsView.draw(false); //TODO: enable smart invalidation (setDirty) + _scenes[level]->objectsView.draw(); _scenes[level]->passabilityView.update(); _miniscenes[level]->updateViews(); @@ -500,10 +502,8 @@ void MapController::commitObjectShift(int level) pos.z = level; pos.x += shift.x(); pos.y += shift.y(); - auto prevPositions = _mapHandler->getTilesUnderObject(obj); _scenes[level]->objectsView.setDirty(obj); //set dirty before movement _map->getEditManager()->moveObject(obj, pos); - _mapHandler->invalidate(prevPositions); _mapHandler->invalidate(obj); } } diff --git a/mapeditor/maphandler.cpp b/mapeditor/maphandler.cpp index 922e38345..8d4ff8b30 100644 --- a/mapeditor/maphandler.cpp +++ b/mapeditor/maphandler.cpp @@ -25,14 +25,14 @@ const int tileSize = 32; -static bool objectBlitOrderSorter(const TileObject & a, const TileObject & b) +static bool objectBlitOrderSorter(const ObjectRect & a, const ObjectRect & b) { return MapHandler::compareObjectBlitOrder(a.obj, b.obj); } int MapHandler::index(int x, int y, int z) const { - return z * (sizes.x * sizes.y) + y * sizes.x + x; + return z * (map->width * map->height) + y * map->width + x; } int MapHandler::index(const int3 & p) const @@ -48,14 +48,7 @@ MapHandler::MapHandler() void MapHandler::reset(const CMap * Map) { - ttiles.clear(); map = Map; - - //sizes of terrain - sizes.x = map->width; - sizes.y = map->height; - sizes.z = map->twoLevel ? 2 : 1; - initObjectRects(); logGlobal->info("\tMaking object rects"); } @@ -176,67 +169,101 @@ void setPlayerColor(QImage * sur, PlayerColor player) logGlobal->warn("Warning, setPlayerColor called on not 8bpp surface!"); } -void MapHandler::initObjectRects() +std::shared_ptr MapHandler::getObjectImage(const CGObjectInstance * obj) { - ttiles.resize(sizes.x * sizes.y * sizes.z); - - //initializing objects / rects - for(const CGObjectInstance * elem : map->objects) + if( !obj + || (obj->ID==Obj::HERO && static_cast(obj)->inTownGarrison) //garrisoned hero + || (obj->ID==Obj::BOAT && static_cast(obj)->hero)) //boat with hero (hero graphics is used) { - CGObjectInstance *obj = const_cast(elem); - if( !obj - || (obj->ID==Obj::HERO && static_cast(obj)->inTownGarrison) //garrisoned hero - || (obj->ID==Obj::BOAT && static_cast(obj)->hero)) //boat with hero (hero graphics is used) + return nullptr; + } + + std::shared_ptr animation = graphics->getAnimation(obj); + + //no animation at all + if(!animation) + return nullptr; + + //empty animation + if(animation->size(0) == 0) + return nullptr; + + auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); + if(!image) + { + //workaround for prisons + image = animation->getImage(0, 0); + } + + return image; +} + +std::set MapHandler::removeObject(const CGObjectInstance *object) +{ + std::set result = tilesCache[object]; + for(auto & t : result) + { + auto & objects = getObjects(t); + for(auto iter = objects.begin(); iter != objects.end(); ++iter) { - continue; - } - - std::shared_ptr animation = graphics->getAnimation(obj); - - //no animation at all - if(!animation) - continue; - - //empty animation - if(animation->size(0) == 0) - continue; - - auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); - if(!image) - { - //workaround for prisons - image = animation->getImage(0, 0); - if(!image) - continue; - } - - - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) + if(iter->obj == object) { - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - QRect cr(image->width() - fx * tileSize - tileSize, - image->height() - fy * tileSize - tileSize, - image->width(), - image->height()); - - TileObject toAdd(obj, cr); - - if( map->isInTheMap(currTile) && // within map - cr.x() + cr.width() > 0 && // image has data on this tile - cr.y() + cr.height() > 0) - { - ttiles[index(currTile)].push_back(toAdd); - } + objects.erase(iter); + break; } } } - for(auto & tt : ttiles) + tilesCache.erase(object); + return result; +} + +std::set MapHandler::addObject(const CGObjectInstance * object) +{ + auto image = getObjectImage(object); + if(!image) + return std::set{}; + + for(int fx = 0; fx < object->getWidth(); ++fx) { - stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter); + for(int fy = 0; fy < object->getHeight(); ++fy) + { + int3 currTile(object->pos.x - fx, object->pos.y - fy, object->pos.z); + QRect cr(image->width() - fx * tileSize - tileSize, + image->height() - fy * tileSize - tileSize, + tileSize, + tileSize); + + if( map->isInTheMap(currTile) && // within map + cr.x() + cr.width() > 0 && // image has data on this tile + cr.y() + cr.height() > 0) + { + getObjects(currTile).emplace_back(object, cr); + tilesCache[object].insert(currTile); + } + } } + + return tilesCache[object]; +} + +void MapHandler::initObjectRects() +{ + tileObjects.clear(); + tilesCache.clear(); + if(!map) + return; + + tileObjects.resize(map->width * map->height * (map->twoLevel ? 2 : 1)); + + //initializing objects / rects + for(const CGObjectInstance * elem : map->objects) + { + addObject(elem); + } + + for(auto & tt : tileObjects) + stable_sort(tt.begin(), tt.end(), objectBlitOrderSorter); } bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) @@ -265,13 +292,13 @@ bool MapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObje return false; } -TileObject::TileObject(CGObjectInstance * obj_, QRect rect_) +ObjectRect::ObjectRect(const CGObjectInstance * obj_, QRect rect_) : obj(obj_), rect(rect_) { } -TileObject::~TileObject() +ObjectRect::~ObjectRect() { } @@ -295,31 +322,36 @@ std::shared_ptr MapHandler::findFlagBitmapInternal(std::shared_ptrgetImage((anim / 4) % groupSize, group); } -MapHandler::AnimBitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const +MapHandler::BitmapHolder MapHandler::findObjectBitmap(const CGObjectInstance * obj, int anim, int group) const { if(!obj) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); // normal object std::shared_ptr animation = graphics->getAnimation(obj); size_t groupSize = animation->size(group); if(groupSize == 0) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); animation->playerColored(obj->tempOwner); auto bitmap = animation->getImage(anim % groupSize, group); if(!bitmap) - return MapHandler::AnimBitmapHolder(); + return MapHandler::BitmapHolder(); setPlayerColor(bitmap.get(), obj->tempOwner); - return MapHandler::AnimBitmapHolder(bitmap); + return MapHandler::BitmapHolder(bitmap); } -std::vector & MapHandler::getObjects(int x, int y, int z) +std::vector & MapHandler::getObjects(const int3 & tile) { - return ttiles[index(x, y, z)]; + return tileObjects[index(tile)]; +} + +std::vector & MapHandler::getObjects(int x, int y, int z) +{ + return tileObjects[index(x, y, z)]; } void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked) @@ -364,36 +396,6 @@ void MapHandler::drawObjects(QPainter & painter, int x, int y, int z, const std: } } -void MapHandler::drawObject(QPainter & painter, const TileObject & object) -{ - const CGObjectInstance * obj = object.obj; - if (!obj) - { - logGlobal->error("Stray map object that isn't fading"); - return; - } - - uint8_t animationFrame = 0; - - auto objData = findObjectBitmap(obj, animationFrame, obj->ID == Obj::HERO ? 2 : 0); - if(obj->ID == Obj::HERO && obj->tempOwner.isValidPlayer()) - objData.flagBitmap = findFlagBitmap(dynamic_cast(obj), 0, obj->tempOwner, 0); - - if (objData.objBitmap) - { - auto pos = obj->getPosition(); - - painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.objBitmap); - - if (objData.flagBitmap) - { - if(object.rect.x() == pos.x && object.rect.y() == pos.y) - painter.drawImage(pos.x * tileSize - object.rect.x(), pos.y * tileSize - object.rect.y(), *objData.flagBitmap); - } - } -} - - void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, int x, int y) { if (!obj) @@ -410,10 +412,10 @@ void MapHandler::drawObjectAt(QPainter & painter, const CGObjectInstance * obj, if (objData.objBitmap) { - painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.objBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.objBitmap); if (objData.flagBitmap) - painter.drawImage(QPoint((x + 1) * 32 - objData.objBitmap->width(), (y + 1) * 32 - objData.objBitmap->height()), *objData.flagBitmap); + painter.drawImage(QPoint((x + 1) * tileSize - objData.objBitmap->width(), (y + 1) * tileSize - objData.objBitmap->height()), *objData.flagBitmap); } } @@ -450,107 +452,19 @@ void MapHandler::drawMinimapTile(QPainter & painter, int x, int y, int z) painter.drawPoint(x, y); } -void MapHandler::invalidate(int x, int y, int z) +std::set MapHandler::invalidate(const CGObjectInstance * obj) { - auto & objects = getObjects(x, y, z); + auto t1 = removeObject(obj); + auto t2 = addObject(obj); + t1.insert(t2.begin(), t2.end()); - for(auto obj = objects.begin(); obj != objects.end();) - { - //object was removed - if(std::find(map->objects.begin(), map->objects.end(), obj->obj) == map->objects.end()) - { - obj = objects.erase(obj); - continue; - } - - //object was moved - auto & pos = obj->obj->pos; - if(pos.z != z || pos.x < x || pos.y < y || pos.x - obj->obj->getWidth() >= x || pos.y - obj->obj->getHeight() >= y) - { - obj = objects.erase(obj); - continue; - } - - ++obj; - } + for(auto & tt : t2) + stable_sort(tileObjects[index(tt)].begin(), tileObjects[index(tt)].end(), objectBlitOrderSorter); - stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); -} - -void MapHandler::invalidate(CGObjectInstance * obj) -{ - std::shared_ptr animation = graphics->getAnimation(obj); - - //no animation at all or empty animation - if(!animation || animation->size(0) == 0) - return; - - auto image = animation->getImage(0, obj->ID == Obj::HERO ? 2 : 0); - if(!image) - return; - - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) - { - //object presented on the tile - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - QRect cr(image->width() - fx * tileSize - tileSize, image->height() - fy * tileSize - tileSize, image->width(), image->height()); - - if( map->isInTheMap(currTile) && // within map - cr.x() + cr.width() > 0 && // image has data on this tile - cr.y() + cr.height() > 0) - { - auto & objects = ttiles[index(currTile)]; - bool found = false; - for(auto & o : objects) - { - if(o.obj == obj) - { - o.rect = cr; - found = true; - break; - } - } - if(!found) - objects.emplace_back(obj, cr); - - stable_sort(objects.begin(), objects.end(), objectBlitOrderSorter); - } - } - } -} - -std::vector MapHandler::getTilesUnderObject(CGObjectInstance * obj) const -{ - std::vector result; - for(int fx=0; fx < obj->getWidth(); ++fx) - { - for(int fy=0; fy < obj->getHeight(); ++fy) - { - //object presented on the tile - int3 currTile(obj->pos.x - fx, obj->pos.y - fy, obj->pos.z); - if(map->isInTheMap(currTile)) // within map - { - result.push_back(currTile); - } - } - } - return result; + return t1; } void MapHandler::invalidateObjects() { - for(auto obj : map->objects) - { - invalidate(obj); - } -} - -void MapHandler::invalidate(const std::vector & tiles) -{ - for(auto & currTile : tiles) - { - invalidate(currTile.x, currTile.y, currTile.z); - } + initObjectRects(); } diff --git a/mapeditor/maphandler.h b/mapeditor/maphandler.h index 27b0518e8..76c7cfe41 100644 --- a/mapeditor/maphandler.h +++ b/mapeditor/maphandler.h @@ -29,26 +29,26 @@ class PlayerColor; VCMI_LIB_NAMESPACE_END -struct TileObject +struct ObjectRect { - CGObjectInstance *obj; + const CGObjectInstance * obj; QRect rect; - TileObject(CGObjectInstance *obj_, QRect rect_); - ~TileObject(); + ObjectRect(const CGObjectInstance * obj_, QRect rect_); + ~ObjectRect(); }; -using TileObjects = std::vector; //pointers to objects being on this tile with rects to be easier to blit this tile on screen +using TileObjects = std::vector; //pointers to objects being on this tile with rects to be easier to blit this tile on screen class MapHandler { public: - struct AnimBitmapHolder + struct BitmapHolder { std::shared_ptr objBitmap; // main object bitmap std::shared_ptr flagBitmap; // flag bitmap for the object (probably only for heroes and boats with heroes) - AnimBitmapHolder(std::shared_ptr objBitmap_ = nullptr, std::shared_ptr flagBitmap_ = nullptr) + BitmapHolder(std::shared_ptr objBitmap_ = nullptr, std::shared_ptr flagBitmap_ = nullptr) : objBitmap(objBitmap_), flagBitmap(flagBitmap_) {} @@ -61,7 +61,7 @@ private: std::shared_ptr findFlagBitmapInternal(std::shared_ptr animation, int anim, int group, ui8 dir, bool moving) const; std::shared_ptr findFlagBitmap(const CGHeroInstance * obj, int anim, const PlayerColor color, int group) const; - AnimBitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const; + BitmapHolder findObjectBitmap(const CGObjectInstance * obj, int anim, int group = 0) const; //FIXME: unique_ptr should be enough, but fails to compile in MSVS 2013 typedef std::map> TFlippedAnimations; //[type, rotation] @@ -76,28 +76,22 @@ private: TFlippedAnimations riverAnimations;//[river type, rotation] TFlippedCache riverImages;//[river type, view type, rotation] - std::vector ttiles; //informations about map tiles - int3 sizes; //map size (x = width, y = height, z = number of levels) - const CMap * map; - - enum class EMapCacheType : char - { - TERRAIN, OBJECTS, ROADS, RIVERS, FOW, HEROES, HERO_FLAGS, FRAME, AFTER_LAST - }; + std::vector tileObjects; //informations about map tiles + std::map> tilesCache; //set of tiles beloging to object + + const CMap * map = nullptr; void initObjectRects(); void initTerrainGraphics(); QRgb getTileColor(int x, int y, int z); - - QPolygon lockBitMask; + + std::shared_ptr getObjectImage(const CGObjectInstance * obj); public: MapHandler(); ~MapHandler() = default; void reset(const CMap * Map); - - void updateWater(); void drawTerrainTile(QPainter & painter, int x, int y, int z); /// draws a river segment on current tile @@ -105,17 +99,21 @@ public: /// draws a road segment on current tile void drawRoad(QPainter & painter, int x, int y, int z); - void invalidate(int x, int y, int z); //invalidates all objects in particular tile - void invalidate(CGObjectInstance *); //invalidates object rects - void invalidate(const std::vector &); //invalidates all tiles + std::set invalidate(const CGObjectInstance *); //invalidates object rects void invalidateObjects(); //invalidates all objects on the map - std::vector getTilesUnderObject(CGObjectInstance *) const; + const std::set & getTilesUnderObject(const CGObjectInstance *) const; + + //get objects at position + std::vector & getObjects(const int3 & tile); + std::vector & getObjects(int x, int y, int z); + + //returns set of tiles to draw + std::set removeObject(const CGObjectInstance * object); + std::set addObject(const CGObjectInstance * object); /// draws all objects on current tile (higher-level logic, unlike other draw*** methods) void drawObjects(QPainter & painter, int x, int y, int z, const std::set & locked); - void drawObject(QPainter & painter, const TileObject & object); void drawObjectAt(QPainter & painter, const CGObjectInstance * object, int x, int y); - std::vector & getObjects(int x, int y, int z); void drawMinimapTile(QPainter & painter, int x, int y, int z); diff --git a/mapeditor/scenelayer.cpp b/mapeditor/scenelayer.cpp index 6d49c3f25..33e8997e8 100644 --- a/mapeditor/scenelayer.cpp +++ b/mapeditor/scenelayer.cpp @@ -518,7 +518,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(object.obj->visitableAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } @@ -530,7 +530,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(object.obj->blockingAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } @@ -542,7 +542,7 @@ CGObjectInstance * SelectionObjectsLayer::selectObjectAt(int x, int y, const CGO if(object.obj->coveringAt(x, y)) { - return object.obj; + return const_cast(object.obj); } } @@ -568,7 +568,7 @@ void SelectionObjectsLayer::selectObjects(int x1, int y1, int x2, int y2) { for(auto & o : handler->getObjects(i, j, scene->level)) if(!lockedObjects.count(o.obj)) - selectObject(o.obj, false); //do not inform about each object added + selectObject(const_cast(o.obj), false); //do not inform about each object added } } } From b9540fa507382fb003ce8273b18f5ebedb574884 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 00:31:48 +0300 Subject: [PATCH 0879/1248] Split object jsons into smaller files --- config/gameConfig.json | 24 +- config/objects/cartographer.json | 97 ++++++++ config/objects/coverOfDarkness.json | 35 +++ config/objects/magicSpring.json | 44 ++++ config/objects/magicWell.json | 39 ++++ config/objects/observatory.json | 79 +++++++ config/objects/rewardableObservatories.json | 209 ------------------ config/objects/rewardableOncePerWeek.json | 80 ------- .../{rewardableScholar.json => scholar.json} | 0 .../{rewardableShrine.json => shrine.json} | 0 ...{rewardableWitchHut.json => witchHut.json} | 0 11 files changed, 308 insertions(+), 299 deletions(-) create mode 100644 config/objects/cartographer.json create mode 100644 config/objects/coverOfDarkness.json create mode 100644 config/objects/magicSpring.json create mode 100644 config/objects/magicWell.json create mode 100644 config/objects/observatory.json delete mode 100644 config/objects/rewardableObservatories.json rename config/objects/{rewardableScholar.json => scholar.json} (100%) rename config/objects/{rewardableShrine.json => shrine.json} (100%) rename config/objects/{rewardableWitchHut.json => witchHut.json} (100%) diff --git a/config/gameConfig.json b/config/gameConfig.json index 27842d3f9..df6092966 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -48,19 +48,23 @@ "objects" : [ - "config/objects/generic.json", - "config/objects/moddables.json", + "config/objects/cartographer.json", + "config/objects/coverOfDarkness.json", "config/objects/creatureBanks.json", "config/objects/dwellings.json", - "config/objects/rewardableObservatories.json", - "config/objects/rewardableWitchHut.json", - "config/objects/rewardableShrine.json", - "config/objects/rewardableScholar.json", - "config/objects/rewardableOncePerWeek.json", - "config/objects/rewardablePickable.json", - "config/objects/rewardableOnceVisitable.json", + "config/objects/generic.json", + "config/objects/magicSpring.json", + "config/objects/magicWell.json", + "config/objects/moddables.json", + "config/objects/observatory.json", + "config/objects/rewardableBonusing.json", "config/objects/rewardableOncePerHero.json", - "config/objects/rewardableBonusing.json" + "config/objects/rewardableOncePerWeek.json", + "config/objects/rewardableOnceVisitable.json", + "config/objects/rewardablePickable.json", + "config/objects/scholar.json", + "config/objects/shrine.json", + "config/objects/witchHut.json" ], "artifacts" : diff --git a/config/objects/cartographer.json b/config/objects/cartographer.json new file mode 100644 index 000000000..4df883c5c --- /dev/null +++ b/config/objects/cartographer.json @@ -0,0 +1,97 @@ +{ + "cartographer" : { + "index" :13, + "handler": "configurable", + "lastReservedIndex" : 2, + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "cartographerWater" : { + "index" : 0, + "aiValue" : 5000, + "rmg" : { + "zoneLimit" : 1, + "value" : 5000, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "water" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 25, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "water" : 1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerLand" : { + "index" : 1, + "aiValue": 10000, + "rmg" : { + "zoneLimit" : 1, + "value" : 10000, + "rarity" : 2 + }, + "compatibilityIdentifiers" : [ "land" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 26, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "surface" : 1, + "water" : -1, + "rock" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + }, + "cartographerSubterranean" : { + "index" : 2, + "aiValue" : 7500, + "rmg" : { + "zoneLimit" : 1, + "value" : 7500, + "rarity" : 20 + }, + "compatibilityIdentifiers" : [ "subterra" ], + "visitMode" : "unlimited", + "canRefuse" : true, + "rewards" : [ + { + "limiter" : { "resources" : { "gold" : 1000 } }, + "message" : 27, + "resources" : { + "gold" : -1000 + }, + "revealTiles" : { + "subterra" : 1, + "water" : -1, + "rock" : -1, + "surface" : -1 + } + } + ], + "onEmptyMessage" : 28, + "onVisitedMessage" : 24 + } + } + } +} \ No newline at end of file diff --git a/config/objects/coverOfDarkness.json b/config/objects/coverOfDarkness.json new file mode 100644 index 000000000..0cb733547 --- /dev/null +++ b/config/objects/coverOfDarkness.json @@ -0,0 +1,35 @@ +{ + "coverOfDarkness" : { + "index" :15, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "coverOfDarkness" : { + "index" : 0, + "aiValue" : 100, + "rmg" : { + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 31, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/magicSpring.json b/config/objects/magicSpring.json new file mode 100644 index 000000000..6013353c3 --- /dev/null +++ b/config/objects/magicSpring.json @@ -0,0 +1,44 @@ +{ + "magicSpring" : { + "index" : 48, + "handler": "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFOUN"], + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicSpring" : { + "index" : 0, + "aiValue" : 500, + //banned due to problems with 2 viistable offsets + //"rmg" : { + // "zoneLimit" : 1, + // "value" : 500, + // "rarity" : 50 + //}, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 76, + "onVisitedMessage" : 75, + "description" : "@core.xtrainfo.15", + "resetParameters" : { + "period" : 7, + "visitors" : true + }, + "visitMode" : "once", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 200 } ] + }, + "message" : 74, + "manaPercentage" : 200 + } + ] + } + } + } +} diff --git a/config/objects/magicWell.json b/config/objects/magicWell.json new file mode 100644 index 000000000..4f15e1aa2 --- /dev/null +++ b/config/objects/magicWell.json @@ -0,0 +1,39 @@ +{ + "magicWell" : { + "index" :49, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["FAERIE"] + } + }, + "types" : { + "magicWell" : { + "index" : 0, + "aiValue" : 250, + "rmg" : { + "zoneLimit" : 1, + "value" : 250, + "rarity" : 100 + }, + "compatibilityIdentifiers" : [ "object" ], + + "onEmptyMessage" : 79, + "onVisitedMessage" : 78, + "description" : "@core.xtrainfo.25", + "visitMode" : "bonus", + "selectMode" : "selectFirst", + "rewards" : [ + { + "limiter" : { + "noneOf" : [ { "manaPercentage" : 100 } ] + }, + "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], + "message" : 77, + "manaPercentage" : 100 + } + ] + }, + } + } +} diff --git a/config/objects/observatory.json b/config/objects/observatory.json new file mode 100644 index 000000000..c4999b4aa --- /dev/null +++ b/config/objects/observatory.json @@ -0,0 +1,79 @@ +{ + "redwoodObservatory" : { + "index" :58, + "handler" : "configurable", + "base" : { + "sounds" : { + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "redwoodObservatory" : { + "index" : 0, + "aiValue" : 750, + "templates" : + { + "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, + "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } + }, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 98, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + }, + + "pillarOfFire" : { + "index" :60, + "handler" : "configurable", + "base" : { + "sounds" : { + "ambient" : ["LOOPFIRE"], + "visit" : ["LIGHTHOUSE"] + } + }, + "types" : { + "pillarOfFire" : { + "index" : 0, + "aiValue" : 750, + "rmg" : { + "zoneLimit" : 1, + "value" : 750, + "rarity" : 100 + }, + + "compatibilityIdentifiers" : [ "object" ], + "visitMode" : "unlimited", + "rewards" : [ + { + "message" : 99, + "revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1 + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/config/objects/rewardableObservatories.json b/config/objects/rewardableObservatories.json deleted file mode 100644 index 7e6d60fdf..000000000 --- a/config/objects/rewardableObservatories.json +++ /dev/null @@ -1,209 +0,0 @@ -{ - "redwoodObservatory" : { - "index" :58, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "redwoodObservatory" : { - "index" : 0, - "aiValue" : 750, - "templates" : - { - "base" : { "animation" : "avxredw.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["grass", "swamp", "dirt", "sand", "lava", "rough"] }, - "snow" : { "animation" : "avxreds0.def", "visitableFrom" : [ "---", "+++", "+++" ], "mask" : [ "VV", "VV", "VA"], "allowedTerrains":["snow"] } - }, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 98, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1 - } - } - ] - } - } - }, - - "pillarOfFire" : { - "index" :60, - "handler" : "configurable", - "base" : { - "sounds" : { - "ambient" : ["LOOPFIRE"], - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "pillarOfFire" : { - "index" : 0, - "aiValue" : 750, - "rmg" : { - "zoneLimit" : 1, - "value" : 750, - "rarity" : 100 - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 99, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1 - } - } - ] - } - } - }, - - "coverOfDarkness" : { - "index" :15, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "coverOfDarkness" : { - "index" : 0, - "aiValue" : 100, - "rmg" : { - }, - - "compatibilityIdentifiers" : [ "object" ], - "visitMode" : "unlimited", - "rewards" : [ - { - "message" : 31, - "revealTiles" : { - "radius" : 20, - "surface" : 1, - "subterra" : 1, - "water" : 1, - "rock" : 1, - "hide" : true - } - } - ] - } - } - }, - - "cartographer" : { - "index" :13, - "handler": "configurable", - "lastReservedIndex" : 2, - "base" : { - "sounds" : { - "visit" : ["LIGHTHOUSE"] - } - }, - "types" : { - "cartographerWater" : { - "index" : 0, - "aiValue" : 5000, - "rmg" : { - "zoneLimit" : 1, - "value" : 5000, - "rarity" : 20 - }, - "compatibilityIdentifiers" : [ "water" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 25, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "water" : 1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - }, - "cartographerLand" : { - "index" : 1, - "aiValue": 10000, - "rmg" : { - "zoneLimit" : 1, - "value" : 10000, - "rarity" : 2 - }, - "compatibilityIdentifiers" : [ "land" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 26, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "surface" : 1, - "water" : -1, - "rock" : -1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - }, - "cartographerSubterranean" : { - "index" : 2, - "aiValue" : 7500, - "rmg" : { - "zoneLimit" : 1, - "value" : 7500, - "rarity" : 20 - }, - "compatibilityIdentifiers" : [ "subterra" ], - "visitMode" : "unlimited", - "canRefuse" : true, - "rewards" : [ - { - "limiter" : { "resources" : { "gold" : 1000 } }, - "message" : 27, - "resources" : { - "gold" : -1000 - }, - "revealTiles" : { - "subterra" : 1, - "water" : -1, - "rock" : -1, - "surface" : -1 - } - } - ], - "onEmptyMessage" : 28, - "onVisitedMessage" : 24 - } - } - } -} \ No newline at end of file diff --git a/config/objects/rewardableOncePerWeek.json b/config/objects/rewardableOncePerWeek.json index 110943c32..7b2005848 100644 --- a/config/objects/rewardableOncePerWeek.json +++ b/config/objects/rewardableOncePerWeek.json @@ -1,84 +1,4 @@ { - /// These are objects that covered by concept of "configurable object" and have their entire configuration in this config - "magicWell" : { - "index" :49, - "handler" : "configurable", - "base" : { - "sounds" : { - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicWell" : { - "index" : 0, - "aiValue" : 250, - "rmg" : { - "zoneLimit" : 1, - "value" : 250, - "rarity" : 100 - }, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 79, - "onVisitedMessage" : 78, - "description" : "@core.xtrainfo.25", - "visitMode" : "bonus", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 100 } ] - }, - "bonuses" : [ { "type" : "NONE", "duration" : "ONE_DAY"} ], - "message" : 77, - "manaPercentage" : 100 - } - ] - }, - } - }, - "magicSpring" : { - "index" : 48, - "handler": "configurable", - "base" : { - "sounds" : { - "ambient" : ["LOOPFOUN"], - "visit" : ["FAERIE"] - } - }, - "types" : { - "magicSpring" : { - "index" : 0, - "aiValue" : 500, - //banned due to problems with 2 viistable offsets - //"rmg" : { - // "zoneLimit" : 1, - // "value" : 500, - // "rarity" : 50 - //}, - "compatibilityIdentifiers" : [ "object" ], - - "onEmptyMessage" : 76, - "onVisitedMessage" : 75, - "description" : "@core.xtrainfo.15", - "resetParameters" : { - "period" : 7, - "visitors" : true - }, - "visitMode" : "once", - "selectMode" : "selectFirst", - "rewards" : [ - { - "limiter" : { - "noneOf" : [ { "manaPercentage" : 200 } ] - }, - "message" : 74, - "manaPercentage" : 200 - } - ] - } - } - }, "mysticalGarden" : { "index" : 55, "handler": "configurable", diff --git a/config/objects/rewardableScholar.json b/config/objects/scholar.json similarity index 100% rename from config/objects/rewardableScholar.json rename to config/objects/scholar.json diff --git a/config/objects/rewardableShrine.json b/config/objects/shrine.json similarity index 100% rename from config/objects/rewardableShrine.json rename to config/objects/shrine.json diff --git a/config/objects/rewardableWitchHut.json b/config/objects/witchHut.json similarity index 100% rename from config/objects/rewardableWitchHut.json rename to config/objects/witchHut.json From 1c4a14284964a94e4491c0e7850f9a97dd2d5bc7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 00:44:57 +0300 Subject: [PATCH 0880/1248] Fix remaining TODO's --- lib/rewardable/Configuration.cpp | 18 ++++++- lib/rewardable/Info.cpp | 83 ++++++++++++-------------------- lib/rewardable/Info.h | 6 +++ lib/rewardable/Reward.cpp | 7 ++- 4 files changed, 59 insertions(+), 55 deletions(-) diff --git a/lib/rewardable/Configuration.cpp b/lib/rewardable/Configuration.cpp index 18788473f..4cab55235 100644 --- a/lib/rewardable/Configuration.cpp +++ b/lib/rewardable/Configuration.cpp @@ -73,7 +73,23 @@ void Rewardable::VisitInfo::serializeJson(JsonSerializeFormat & handler) void Rewardable::Variables::serializeJson(JsonSerializeFormat & handler) { - // TODO + if (handler.saving) + { + JsonNode presetNode; + for (auto const & entry : preset) + presetNode[entry.first] = entry.second; + + handler.serializeRaw("preset", presetNode, {}); + } + else + { + preset.clear(); + JsonNode presetNode; + handler.serializeRaw("preset", presetNode, {}); + + for (auto const & entry : presetNode.Struct()) + preset[entry.first] = entry.second; + } } void Rewardable::Configuration::serializeJson(JsonSerializeFormat & handler) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 61d12282f..aadf30285 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -233,14 +233,6 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR if (category.first == "spell") value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); - // TODO - // if (category.first == "creature") - // value = JsonRandom::loadCreature(input, rng, object.variables.values).type->getId(); - - // TODO - // if (category.first == "resource") - // value = JsonRandom::loadResource(input, rng, object.variables.values).getNum(); - if (category.first == "primarySkill") value = static_cast(JsonRandom::loadPrimary(input, rng, object.variables.values)); @@ -252,6 +244,32 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR } } +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables) const +{ + for (const auto & variable : variables.values ) + { + if( boost::algorithm::starts_with(variable.first, "spell")) + target.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + + if( boost::algorithm::starts_with(variable.first, "secondarySkill")) + target.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + } +} + +void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const +{ + for (const auto & artifact : info.reward.artifacts ) + target.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + + for (const auto & artifact : info.reward.spells ) + target.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + + for (const auto & secondary : info.reward.secondary ) + target.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + + replaceTextPlaceholders(target, variables); +} + void Rewardable::Info::configureRewards( Rewardable::Configuration & object, CRandomGenerator & rng, const @@ -304,37 +322,8 @@ void Rewardable::Info::configureRewards( info.message = loadMessage(reward["message"], TextIdentifier(objectTextID, modeName, i)); info.description = loadMessage(reward["description"], TextIdentifier(objectTextID, "description", modeName, i), EMetaText::GENERAL_TXT); - for (const auto & artifact : info.reward.artifacts ) - { - info.message.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); - info.description.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); - } - - for (const auto & artifact : info.reward.spells ) - { - info.message.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); - info.description.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); - } - - for (const auto & secondary : info.reward.secondary ) - { - info.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); - info.description.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); - } - - for (const auto & variable : object.variables.values ) - { - if( boost::algorithm::starts_with(variable.first, "spell")) - { - info.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); - info.description.replaceLocalString(EMetaText::SPELL_NAME, variable.second); - } - if( boost::algorithm::starts_with(variable.first, "secondarySkill")) - { - info.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); - info.description.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); - } - } + replaceTextPlaceholders(info.message, object.variables, info); + replaceTextPlaceholders(info.description, object.variables, info); object.info.push_back(info); } @@ -366,13 +355,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onVisited; onVisited.visitType = Rewardable::EEventType::EVENT_ALREADY_VISITED; onVisited.message = loadMessage(parameters["onVisitedMessage"], TextIdentifier(objectTextID, "onVisited")); - for (const auto & variable : object.variables.values ) - { - if( boost::algorithm::starts_with(variable.first, "spell")) - onVisited.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); - if( boost::algorithm::starts_with(variable.first, "secondarySkill")) - onVisited.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); - } + replaceTextPlaceholders(onVisited.message, object.variables); object.info.push_back(onVisited); } @@ -382,13 +365,7 @@ void Rewardable::Info::configureObject(Rewardable::Configuration & object, CRand Rewardable::VisitInfo onEmpty; onEmpty.visitType = Rewardable::EEventType::EVENT_NOT_AVAILABLE; onEmpty.message = loadMessage(parameters["onEmptyMessage"], TextIdentifier(objectTextID, "onEmpty")); - for (const auto & variable : object.variables.values ) - { - if( boost::algorithm::starts_with(variable.first, "spell")) - onEmpty.message.replaceLocalString(EMetaText::SPELL_NAME, variable.second); - if( boost::algorithm::starts_with(variable.first, "secondarySkill")) - onEmpty.message.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); - } + replaceTextPlaceholders(onEmpty.message, object.variables); object.info.push_back(onEmpty); } diff --git a/lib/rewardable/Info.h b/lib/rewardable/Info.h index 1bf145c39..5ad96ca40 100644 --- a/lib/rewardable/Info.h +++ b/lib/rewardable/Info.h @@ -16,6 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CRandomGenerator; +class MetaString; namespace Rewardable { @@ -24,6 +25,8 @@ struct Limiter; using LimitersList = std::vector>; struct Reward; struct Configuration; +struct Variables; +struct VisitInfo; struct ResetInfo; enum class EEventType; @@ -32,6 +35,9 @@ class DLL_LINKAGE Info : public IObjectInfo JsonNode parameters; std::string objectTextID; + void replaceTextPlaceholders(MetaString & target, const Variables & variables) const; + void replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const; + void configureVariables(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source) const; void configureRewards(Rewardable::Configuration & object, CRandomGenerator & rng, const JsonNode & source, Rewardable::EEventType mode, const std::string & textPrefix) const; diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 71480aae1..5b965011b 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -20,7 +20,12 @@ VCMI_LIB_NAMESPACE_BEGIN void Rewardable::RewardRevealTiles::serializeJson(JsonSerializeFormat & handler) { - // TODO + handler.serializeBool("hide", hide); + handler.serializeInt("scoreSurface", scoreSurface); + handler.serializeInt("scoreSubterra", scoreSubterra); + handler.serializeInt("scoreWater", scoreWater); + handler.serializeInt("scoreRock", scoreRock); + handler.serializeInt("radius", radius); } Rewardable::Reward::Reward() From 1baac4449534f278250d7ef073ee48c79e03973e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 00:45:08 +0300 Subject: [PATCH 0881/1248] Update documentation --- docs/modders/Map_Objects/Rewardable.md | 117 ++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 3 deletions(-) diff --git a/docs/modders/Map_Objects/Rewardable.md b/docs/modders/Map_Objects/Rewardable.md index c1030a738..245719060 100644 --- a/docs/modders/Map_Objects/Rewardable.md +++ b/docs/modders/Map_Objects/Rewardable.md @@ -4,6 +4,7 @@ - [Base object definition](#base-object-definition) - [Configurable object definition](#configurable-object-definition) - [Base object definition](#base-object-definition) +- [Variables Parameters definition](#variables-parameters-definition) - [Reset Parameters definition](#reset-parameters-definition) - [Appear Chance definition](#appear-chance-definition) - [Configurable Properties](#configurable-properties) @@ -17,12 +18,18 @@ - - [Movement Percentage](#movement-percentage) - - [Primary Skills](#primary-skills) - - [Secondary Skills](#secondary-skills) +- - [Can learn skills](#can-learn-skills) - - [Bonus System](#bonus-system) - - [Artifacts](#artifacts) - - [Spells](#spells) +- - [Can learn spells](#can-learn-spells) - - [Creatures](#creatures) - - [Creatures Change](#creatures-change) - - [Spell cast](#spell-cast) +- - [Fog of War](#fog-of-war) +- - [Player color](#player-color) +- - [Hero types](#hero-types) +- - [Hero classes](#hero-classes) ## Base object definition Rewardable object is defined similarly to other objects, with key difference being `handler`. This field must be set to `"handler" : "configurable"` in order for vcmi to use this mode. @@ -92,20 +99,47 @@ Rewardable object is defined similarly to other objects, with key difference bei // message that will be shown if this is the only available award "message": "{Warehouse of Crystal}" + // Alternative object description that will be used in place of generic description after player visits this object and reveals its content + // For example, Tree of Knowledge will display cost of levelup (gems or gold) only after object has been visited once + "description" : "", + // object will be disappeared after taking reward is set to true - "removeObject": false + "removeObject": false, // See "Configurable Properties" section for additional parameters } ], +/// List of variables shared between all rewards and limiters +/// See "Variables" section for description +"variables" : { +} + // If true, hero can not move to visitable tile of the object and will access this object from adjacent tile (e.g. Treasure Chest) "blockedVisitable" : true, // Message that will be shown if there are no applicable awards "onEmptyMessage": "", +// Object description that will be shown when player right-clicks object +"description" : "", + +// If set to true, right-clicking previously visited object would show preview of its content. For example, Witch Hut will show icon with provided skill +"showScoutedPreview" : true, + +// Text that should be used if hero has not visited this object. If not specified, game will use standard "(Not visited)" text +"notVisitedTooltip" : "", + +// Text that should be used if hero has already visited this object. If not specified, game will use standard "(Already visited)" text +"visitedTooltip" : "", + +// Used only if visitMode is set to "limiter" +// Hero that passes this limiter will be considered to have visited this object +// Note that if player or his allies have never visited this object, it will always show up as "not visited" +"visitLimiter" : { +}, + // Alternatively, rewards for empty state: // Format is identical to "rewards" section, allowing to fine-tune behavior in this case, including giving awards or different messages to explain why object is "empty". For example, Tree of Knowledge will give different messages depending on whether it asks for gold or crystals "onEmpty" : [ @@ -133,6 +167,7 @@ Rewardable object is defined similarly to other objects, with key difference bei // determines who can revisit object before reset // "once", - object can only be visited once. First visitor takes it all. // "hero", - object can be visited if this hero has not visited it before +// "limiter", - object can be visited if hero fails to fulfill provided limiter // "player", - object can be visited if this player has not visited it before // "bonus" - object can be visited if hero no longer has bonus from this object (including any other object of the same type) // "unlimited" - no restriction on revisiting. @@ -147,6 +182,39 @@ Rewardable object is defined similarly to other objects, with key difference bei } ``` +## Variables Parameters definition + +This property allows defining "variables" that are shared between all rewards and limiters of this object. +Variables are randomized only once, so you can use them multiple times for example, to give skill only if hero does not have this skill (e.g. Witch Hut). + +Example of creation of a variable named "gainedSkill" of type "secondarySkill": +```json +"variables" : { + "secondarySkill" : { + "gainedSkill" : { + "noneOf" : [ + "leadership", + "necromancy" + ] + } + } +} +``` + +Possible variable types: +- number: can be used in any place that expects a number +- artifact +- spell +- primarySkill +- secondarySkill + +To reference variable in limiter prepend variable name with '@' symbol: +```json +"secondary" : { + "@gainedSkill" : 1 +}, +``` + ## Reset Parameters definition This property describes how object state should be reset. Objects without this field will never reset its state. - Period describes interval between object resets in day. Periods are counted from game start and not from hero visit, so reset duration of 7 will always reset object on new week & duration of 28 will always reset on new month. @@ -349,13 +417,21 @@ Keep in mind, that all randomization is performed on map load and on object rese ] ``` +### Can learn skills + +- Can be used as limiter. Hero must have free skill slot to pass limiter + +```json + "canLearnSkills" : true +``` + ### Bonus System - Can be used as reward, to grant bonus to player - if present, MORALE and LUCK bonus will add corresponding image component to UI. - Note that unlike most values, parameter of bonuses can NOT be randomized - Description can be string or number of corresponding string from `arraytxt.txt` -```jsonc +```json "bonuses" : [ { "type" : "MORALE", @@ -414,6 +490,22 @@ Keep in mind, that all randomization is performed on map load and on object rese ], ``` +### Can learn spells + +- Can be used as limiter. Hero must be able to learn spell to pass the limiter +- Hero is considered to not able to learn the spell if: +- - he already has specified spell +- - he does not have a spellbook +- - he does not have sufficient Wisdom level for this spell + +```json + "canLearnSpells" : [ + "magicArrow" +], +``` + +canLearnSpells + ### Creatures - Can be used as limiter - Can be used as reward, to give new creatures to a hero @@ -445,13 +537,32 @@ Keep in mind, that all randomization is performed on map load and on object rese - As reward, instantly casts adventure map spell for visiting hero. All checks for spell book, wisdom or presence of mana will be ignored. It's possible to specify school level at which spell will be casted. If it's necessary to reduce player's mana or do some checks, they shall be introduced as limiters and other rewards - School level possible values: 1 (basic), 2 (advanced), 3 (expert) -```jsonc +```json "spellCast" : { "spell" : "townPortal", "schoolLevel": 3 } ``` +### Fog of War + +- Can NOT be used as limiter +- Can be used as reward, to reveal or hide affected tiles +- If radius is not specified, then all matching tiles on the map will be affected +- It is possible to specify which terrain classes should be affected. Tile will be affected if sum of values its classes is positive. For example, `"water" : 1` will affect all water tiles, while `"surface" : 1, "subterra" : -1` will include terrains that have "surface" flag but do not have "subterra" flag +- If 'hide' is set to true, then instead of revealing terrain, game will hide affected tiles for all other players + +```json +"revealTiles" : { + "radius" : 20, + "surface" : 1, + "subterra" : 1, + "water" : 1, + "rock" : 1, + "hide" : true +} +``` + ### Player color - Can be used as limiter - Can NOT be used as reward From 0eba0ee6865f63962e6830fe6c29329c42e6faca Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 02:12:34 +0200 Subject: [PATCH 0882/1248] show random maps --- client/lobby/CSelectionBase.cpp | 6 ++++++ client/lobby/RandomMapTab.cpp | 12 ++++++++++++ client/lobby/SelectionTab.cpp | 10 ++++++++-- client/lobby/SelectionTab.h | 1 + lib/gameState/CGameState.cpp | 7 +++++-- lib/gameState/CGameState.h | 2 +- 6 files changed, 33 insertions(+), 5 deletions(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 57d3ced51..357bff3a8 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -111,6 +111,12 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) { curTab.reset(); } + + if(tabSel->showRandom) + tabSel->curFolder = ""; + tabSel->showRandom = false; + tabSel->filter(0, true); + GH.windows().totalRedraw(); } diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index b5bdd9a61..65169dff7 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -11,6 +11,8 @@ #include "RandomMapTab.h" #include "CSelectionBase.h" +#include "CLobbyScreen.h" +#include "SelectionTab.h" #include "../CGameInfo.h" #include "../CServerHandler.h" @@ -121,6 +123,16 @@ RandomMapTab::RandomMapTab(): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTab.json")); build(config); + if(auto w = widget("buttonShowRandomMaps")) + { + w->addCallback([&]() + { + (static_cast(parent))->toggleTab((static_cast(parent))->tabSel); + (static_cast(parent))->tabSel->showRandom = true; + (static_cast(parent))->tabSel->filter(0, true); + }); + } + //set combo box callbacks if(auto w = widget("templateList")) { diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 6faa7fc2c..5e5e7fa68 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -147,7 +147,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type) } SelectionTab::SelectionTab(ESelectionScreen Type) - : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0) + : CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false) { OBJ_CONSTRUCTION; @@ -427,9 +427,15 @@ void SelectionTab::filter(int size, bool selectFirst) { if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList) { + if(showRandom) + curFolder = "RANDOMMAPS/"; + auto [folderName, baseFolder, parentExists, fileInFolder] = checkSubfolder(elem->originalFileURI); - if(parentExists) + if((showRandom && baseFolder != "RANDOMMAPS") || (!showRandom && baseFolder == "RANDOMMAPS")) + continue; + + if(parentExists && !showRandom) { auto folder = std::make_shared(); folder->isFolder = true; diff --git a/client/lobby/SelectionTab.h b/client/lobby/SelectionTab.h index 50f501769..b457bcef8 100644 --- a/client/lobby/SelectionTab.h +++ b/client/lobby/SelectionTab.h @@ -78,6 +78,7 @@ public: ESortBy generalSortingBy; bool sortModeAscending; int currentMapSizeFilter = 0; + bool showRandom; std::shared_ptr inputName; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index ffdd990df..183c6e4ec 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -566,17 +566,20 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan { try { - auto path = VCMIDirs::get().userCachePath() / "RandomMaps"; + auto path = VCMIDirs::get().userDataPath() / "Maps" / "RandomMaps"; boost::filesystem::create_directories(path); std::shared_ptr options = scenarioOps->mapGenOptions; const std::string templateName = options->getMapTemplate()->getName(); const ui32 seed = scenarioOps->seedToBeUsed; + const std::string dt = vstd::getDateTimeISO8601Basic(std::time(0)); - const std::string fileName = boost::str(boost::format("%s_%d.vmap") % templateName % seed ); + const std::string fileName = boost::str(boost::format("%s_%s_%d.vmap") % dt % templateName % seed ); const auto fullPath = path / fileName; + randomMap->name.appendRawString(boost::str(boost::format(" %s") % dt)); + mapService->saveMap(randomMap, fullPath); logGlobal->info("Random map has been saved to:"); diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 8ba75e2f9..08b6863d3 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -99,7 +99,7 @@ public: void preInit(Services * services); - void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = false); + void init(const IMapService * mapService, StartInfo * si, Load::ProgressAccumulator &, bool allowSavingRandomMap = true); void updateOnLoad(StartInfo * si); ConstTransitivePtr scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized) From 9a116f9b20abf256536fadebfe6f4d6b606645da Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 02:16:42 +0200 Subject: [PATCH 0883/1248] small optimisation --- client/lobby/CSelectionBase.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 357bff3a8..d8367bef9 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -113,9 +113,11 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) } if(tabSel->showRandom) + { tabSel->curFolder = ""; - tabSel->showRandom = false; - tabSel->filter(0, true); + tabSel->filter(0, true); + tabSel->showRandom = false; + } GH.windows().totalRedraw(); } From ceb70af528cdb94515618ffbe13f85df810718ed Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 02:22:22 +0200 Subject: [PATCH 0884/1248] fix --- client/lobby/CSelectionBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index d8367bef9..5a52d3dc2 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -115,8 +115,8 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) if(tabSel->showRandom) { tabSel->curFolder = ""; - tabSel->filter(0, true); tabSel->showRandom = false; + tabSel->filter(0, true); } GH.windows().totalRedraw(); From 86e6f7d1561d890dc944b24de88d759428ccc3f2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 02:26:03 +0200 Subject: [PATCH 0885/1248] fix --- client/lobby/CSelectionBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 5a52d3dc2..8bdeee141 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -112,7 +112,7 @@ void CSelectionBase::toggleTab(std::shared_ptr tab) curTab.reset(); } - if(tabSel->showRandom) + if(tabSel->showRandom && tab != tabOpt) { tabSel->curFolder = ""; tabSel->showRandom = false; From e03f2a9d3aa41fea6ec2fbd3e074e1c6627f121c Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:49:50 +0200 Subject: [PATCH 0886/1248] improvements for non quadratic maps --- client/adventureMap/CMinimap.cpp | 16 ++++++++++++---- client/lobby/CSelectionBase.cpp | 3 +++ client/lobby/CSelectionBase.h | 1 + client/render/Canvas.cpp | 5 +++++ client/render/Canvas.h | 3 +++ client/windows/CMapOverview.cpp | 8 +++++++- 6 files changed, 31 insertions(+), 5 deletions(-) diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 358cd6de4..019c92f20 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -91,10 +91,19 @@ CMinimap::CMinimap(const Rect & position) level(0) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - pos.w = position.w; - pos.h = position.h; - aiShield = std::make_shared(ImagePath::builtin("AIShield")); + double maxSideLenghtSrc = std::max(LOCPLINT->cb->getMapSize().x, LOCPLINT->cb->getMapSize().y); + double maxSideLenghtDst = std::max(position.w, position.h); + double resize = maxSideLenghtSrc / maxSideLenghtDst; + Point newMinimapSize = Point(LOCPLINT->cb->getMapSize().x/ resize, LOCPLINT->cb->getMapSize().y / resize); + Point offset = Point((std::max(newMinimapSize.x, newMinimapSize.y) - newMinimapSize.x) / 2, (std::max(newMinimapSize.x, newMinimapSize.y) - newMinimapSize.y) / 2); + + pos.x += offset.x; + pos.y += offset.y; + pos.w = newMinimapSize.x; + pos.h = newMinimapSize.y; + + aiShield = std::make_shared(ImagePath::builtin("AIShield"), -offset); aiShield->disable(); } @@ -238,4 +247,3 @@ void CMinimap::updateTiles(const std::unordered_set & positions) } redraw(); } - diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 57d3ced51..bb7fea8f1 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -123,6 +123,7 @@ InfoCard::InfoCard() pos.y += 6; labelSaveDate = std::make_shared(310, 38, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE); + labelMapSize = std::make_shared(332, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE); mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); @@ -185,6 +186,7 @@ InfoCard::InfoCard() void InfoCard::disableLabelRedraws() { labelSaveDate->setAutoRedraw(false); + labelMapSize->setAutoRedraw(false); mapName->setAutoRedraw(false); mapDescription->label->setAutoRedraw(false); labelVictoryConditionText->setAutoRedraw(false); @@ -200,6 +202,7 @@ void InfoCard::changeSelection() return; labelSaveDate->setText(mapInfo->date); + labelMapSize->setText(std::to_string(mapInfo->mapHeader->width) + "x" + std::to_string(mapInfo->mapHeader->height)); mapName->setText(mapInfo->getNameTranslated()); mapDescription->setText(mapInfo->getDescriptionTranslated()); diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 2c3171c26..92b0c2b4f 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -80,6 +80,7 @@ class InfoCard : public CIntObject std::shared_ptr iconsMapSizes; std::shared_ptr labelSaveDate; + std::shared_ptr labelMapSize; std::shared_ptr labelScenarioName; std::shared_ptr labelScenarioDescription; std::shared_ptr labelVictoryCondition; diff --git a/client/render/Canvas.cpp b/client/render/Canvas.cpp index d468250e5..89afe33cd 100644 --- a/client/render/Canvas.cpp +++ b/client/render/Canvas.cpp @@ -186,3 +186,8 @@ SDL_Surface * Canvas::getInternalSurface() { return surface; } + +Rect Canvas::getRenderArea() const +{ + return renderArea; +} diff --git a/client/render/Canvas.h b/client/render/Canvas.h index 591e8fc07..9f9bd324b 100644 --- a/client/render/Canvas.h +++ b/client/render/Canvas.h @@ -101,4 +101,7 @@ public: /// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished. SDL_Surface * getInternalSurface(); + + /// get the render area + Rect getRenderArea() const; }; diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 43e726707..335839991 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -135,8 +135,14 @@ std::shared_ptr CMapOverviewWidget::buildDrawMinimap(const JsonNode & if(id >= minimaps.size()) return nullptr; + Rect minimapRect = minimaps[id].getRenderArea(); + double maxSideLenghtSrc = std::max(minimapRect.w, minimapRect.h); + double maxSideLenghtDst = std::max(rect.w, rect.h); + double resize = maxSideLenghtSrc / maxSideLenghtDst; + Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize); + Canvas canvasScaled = Canvas(Point(rect.w, rect.h)); - canvasScaled.drawScaled(minimaps[id], Point(0, 0), Point(rect.w, rect.h)); + canvasScaled.drawScaled(minimaps[id], Point((rect.w - newMinimapSize.x) / 2, (rect.h - newMinimapSize.y) / 2), newMinimapSize); std::shared_ptr img = GH.renderHandler().createImage(canvasScaled.getInternalSurface()); return std::make_shared(img, Point(rect.x, rect.y)); From 737525756f4c8058ae038ac93b5201d7e4a36907 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 17:28:23 +0200 Subject: [PATCH 0887/1248] fix redraw --- client/adventureMap/CMinimap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/adventureMap/CMinimap.cpp b/client/adventureMap/CMinimap.cpp index 019c92f20..bdc142e01 100644 --- a/client/adventureMap/CMinimap.cpp +++ b/client/adventureMap/CMinimap.cpp @@ -176,7 +176,7 @@ void CMinimap::mouseDragged(const Point & cursorPosition, const Point & lastUpda void CMinimap::showAll(Canvas & to) { - CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos); + CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), aiShield->pos); CIntObject::showAll(to); if(minimap) From d215ffa01148671397cf1fc6c0c75a95dcfcb36d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 18:45:45 +0200 Subject: [PATCH 0888/1248] optics --- client/lobby/CSelectionBase.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index bb7fea8f1..0627fcf07 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -123,7 +123,7 @@ InfoCard::InfoCard() pos.y += 6; labelSaveDate = std::make_shared(310, 38, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE); - labelMapSize = std::make_shared(332, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE); + labelMapSize = std::make_shared(333, 56, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE); mapName = std::make_shared(26, 39, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW); Rect descriptionRect(26, 149, 320, 115); mapDescription = std::make_shared("", descriptionRect, 1); From a042cc46c2401c50afb389108161499988358807 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:10:02 +0200 Subject: [PATCH 0889/1248] no town selection on loadscreen --- client/lobby/OptionsTab.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 042410270..26c7ed634 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -1001,6 +1001,9 @@ void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition) void OptionsTab::SelectedBox::clickReleased(const Point & cursorPosition) { + if(SEL->screenType != ESelectionScreen::newGame) + return; + PlayerInfo pi = SEL->getPlayerInfo(playerSettings.color); const bool foreignPlayer = CSH->isGuest() && !CSH->isMyColor(playerSettings.color); From 9003e5515801a883d42202774a07cf7cfef23b88 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 19:31:44 +0200 Subject: [PATCH 0890/1248] color mode to rgb --- Mods/vcmi/Data/radialMenu/dismissHero.png | Bin 352 -> 1050 bytes Mods/vcmi/Data/radialMenu/tradeHeroes.png | Bin 378 -> 1056 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Mods/vcmi/Data/radialMenu/dismissHero.png b/Mods/vcmi/Data/radialMenu/dismissHero.png index 1fb2b1115392e81094660ca70dbec53108fc6641..fe196b7d6417bec3ca83af32c3a8c5a3ca60f906 100644 GIT binary patch delta 1040 zcmV+r1n>Lc0-6Yr8Gi-<006xf2u}b20fcEoLr_UWLm+T+Z)Rz1WdHyuk$sUpNW(xJ z#a~mUQYs==5EaQ#o$R6_ju~=whrHxt9)QG2uBdVrTzL0TQ<-EmND^*zYp8SQOoW8uo zb(+ISU;&GeAb&zZ1sf>AMwC{a6bmWZk9+tB8-9sg3b{7G$T5#HG{_A<_#gc4*2+&# zxJjWn(D~vxAEQ8E7pPYq=lj@k>L-Bz8MxA0{&EeN`6RvC(jrGd&o*#z-O`jj;Bp5T zcrs*DZb*KbLOu_?pV2pEfxcTHxa#)S+{ftykfyGZH-EsvAuv{;>@|;fceVHS@0n(Q zKcTU5x%LycrvLx|24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0006i zNklsnd+^2%`a9KZ_9Zkak`f{D_6Sf4OfEevc))_>JM@cJDOz0c^%rz@qIm9)Ars}{Bv zq-Yle0Ne~#-&l%Lb)jqj@rMppEsXg-E6i45CTFz$u{zkdx3*tk@w`INgY8FYBjN79Pq$AQ(lfN3u5EA2or7ec`DdQW zQ~X2VAK2H|26AUsVtzY)PZ{P523~(4W`9q2J*Z`94Cy^56=iT5+g~tS(l`A1`|JF?Z-)aBeY5(15|K4l= z-)jHeWdGe||J+;u-eUjYYyaMA|I$4F-edpSRR7#w|K4Zwq8X?F0004WQchCu~=whrHxt9)QG2uBdVrTzL0TQ<-EmND^*zYp8SQOoW8uo zb(+ISU;&GeAb&zZ1sf>AMwC{a6bmWZk9+tB8-9sg3b{7G$T5#HG{_A<_#gc4*2+&# zxJjWn(D~vxAEQ8E7pPYq=lj@k>L-Bz8MxA0{&EeN`6RvC(jrGd&o*#z-O`jj;Bp5T zcrs*DZb*KbLOu_?pV2pEfxcTHxa#)S+{ftykfyGZH-EsvAuv{;>@|;fceVHS@0n(Q zKcTU5x%LycrvLx|24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_0006o zNklpnOzY=2qAy3mf&#Fs7v;~SW5 zl`0rb(WN0KsVO3KEysXf2@ zc%I05!1;R{yMiW{Dp-Hd-GQUSp7xxzIE;}EqMKQKr5r~%g~JjkO0=Eqhs2sL4dX{}~+J!=-3xs~vUD zH?qY2E~1N{8bFgsMa!G3zKS=S+P&H#Ie)%|_`2jc;eLIvPqxURJ)PeniLO+Fz)Susrf-ud+ogyS3^Air&+%T z>ib>@u9;C)d;A+yTruNMM9R?jKThWqLI@#*5JCtcgb+dqA%qY@h$TJ&9VNLD!w;1n QB>(^b07*qoM6N<$f~xT1bpQYW delta 362 zcmV-w0hRus2>Jq$8Gi!+004@}_|JF?Z-)aBeY5(15|J`Q) z-fREgYX9A3|J+^w++P3OTmRl-|KMx?-fI8SJpbNf|JhXk-DLmXXF8tK1^@s60d!JM zQvg8b*k%9#0O?6YK~#7F?A6h#6EPTt(Yt*EXx~0{sPERIJ`staO0g*-AMKgH80MCV>pbtX>00^I;8h+z#N)kGp63ZZ zMrXEC^YO;(C*9{VGI*9Jw|(;qY?GCmcMazdFh(jh&(H)pp-on5-s$fR+$Sp~@4)^m zvlTTTCyn_^&0}}8d_}^y*-Fg^kaJ|RQu9vt{VLmK Date: Sat, 21 Oct 2023 19:48:07 +0300 Subject: [PATCH 0891/1248] JsonNode now uses std::variant internally. Fixes crash on deserialize --- lib/CConfigHandler.cpp | 2 +- lib/JsonNode.cpp | 203 +++++++++------------------- lib/JsonNode.h | 43 +----- lib/modding/ContentTypeHandler.cpp | 2 +- lib/serializer/BinaryDeserializer.h | 5 + lib/serializer/BinarySerializer.h | 5 + 6 files changed, 81 insertions(+), 179 deletions(-) diff --git a/lib/CConfigHandler.cpp b/lib/CConfigHandler.cpp index c741e0e5e..cd97ed365 100644 --- a/lib/CConfigHandler.cpp +++ b/lib/CConfigHandler.cpp @@ -61,7 +61,7 @@ void SettingsStorage::init(const std::string & dataFilename, const std::string & JsonPath confName = JsonPath::builtin(dataFilename); - JsonUtils::assembleFromFiles(confName.getOriginalName()).swap(config); + config = JsonUtils::assembleFromFiles(confName.getOriginalName()); // Probably new install. Create config file to save settings to if (!CResourceHandler::get("local")->existsResource(confName)) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 44999cfb0..1c2e9c0e8 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -67,21 +67,18 @@ class CModHandler; static const JsonNode nullNode; -JsonNode::JsonNode(JsonType Type): - type(JsonType::DATA_NULL) +JsonNode::JsonNode(JsonType Type) { setType(Type); } -JsonNode::JsonNode(const char *data, size_t datasize): - type(JsonType::DATA_NULL) +JsonNode::JsonNode(const char *data, size_t datasize) { JsonParser parser(data, datasize); *this = parser.parse(""); } -JsonNode::JsonNode(const JsonPath & fileURI): - type(JsonType::DATA_NULL) +JsonNode::JsonNode(const JsonPath & fileURI) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -89,8 +86,7 @@ JsonNode::JsonNode(const JsonPath & fileURI): *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI): -type(JsonType::DATA_NULL) +JsonNode::JsonNode(const std::string & idx, const JsonPath & fileURI) { auto file = CResourceHandler::get(idx)->load(fileURI)->readAll(); @@ -98,8 +94,7 @@ type(JsonType::DATA_NULL) *this = parser.parse(fileURI.getName()); } -JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax): - type(JsonType::DATA_NULL) +JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax) { auto file = CResourceHandler::get()->load(fileURI)->readAll(); @@ -108,60 +103,9 @@ JsonNode::JsonNode(const JsonPath & fileURI, bool &isValidSyntax): isValidSyntax = parser.isValid(); } -JsonNode::JsonNode(const JsonNode ©): - type(JsonType::DATA_NULL), - meta(copy.meta), - flags(copy.flags) -{ - setType(copy.getType()); - switch(type) - { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: Bool() = copy.Bool(); - break; case JsonType::DATA_FLOAT: Float() = copy.Float(); - break; case JsonType::DATA_STRING: String() = copy.String(); - break; case JsonType::DATA_VECTOR: Vector() = copy.Vector(); - break; case JsonType::DATA_STRUCT: Struct() = copy.Struct(); - break; case JsonType::DATA_INTEGER:Integer() = copy.Integer(); - } -} - -JsonNode::~JsonNode() -{ - setType(JsonType::DATA_NULL); -} - -void JsonNode::swap(JsonNode &b) -{ - using std::swap; - swap(meta, b.meta); - swap(data, b.data); - swap(type, b.type); - swap(flags, b.flags); -} - -JsonNode & JsonNode::operator =(JsonNode node) -{ - swap(node); - return *this; -} - bool JsonNode::operator == (const JsonNode &other) const { - if (getType() == other.getType()) - { - switch(type) - { - case JsonType::DATA_NULL: return true; - case JsonType::DATA_BOOL: return Bool() == other.Bool(); - case JsonType::DATA_FLOAT: return Float() == other.Float(); - case JsonType::DATA_STRING: return String() == other.String(); - case JsonType::DATA_VECTOR: return Vector() == other.Vector(); - case JsonType::DATA_STRUCT: return Struct() == other.Struct(); - case JsonType::DATA_INTEGER:return Integer()== other.Integer(); - } - } - return false; + return data == other.data; } bool JsonNode::operator != (const JsonNode &other) const @@ -171,7 +115,7 @@ bool JsonNode::operator != (const JsonNode &other) const JsonNode::JsonType JsonNode::getType() const { - return type; + return static_cast(data.index()); } void JsonNode::setMeta(const std::string & metadata, bool recursive) @@ -179,7 +123,7 @@ void JsonNode::setMeta(const std::string & metadata, bool recursive) meta = metadata; if (recursive) { - switch (type) + switch (getType()) { break; case JsonType::DATA_VECTOR: { @@ -201,84 +145,69 @@ void JsonNode::setMeta(const std::string & metadata, bool recursive) void JsonNode::setType(JsonType Type) { - if (type == Type) + if (getType() == Type) return; //float<->int conversion - if(type == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) + if(getType() == JsonType::DATA_FLOAT && Type == JsonType::DATA_INTEGER) { - si64 converted = static_cast(data.Float); - type = Type; - data.Integer = converted; + si64 converted = static_cast(std::get(data)); + data = JsonData(converted); return; } - else if(type == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) + else if(getType() == JsonType::DATA_INTEGER && Type == JsonType::DATA_FLOAT) { - auto converted = static_cast(data.Integer); - type = Type; - data.Float = converted; + double converted = static_cast(std::get(data)); + data = JsonData(converted); return; } - //Reset node to nullptr - if (Type != JsonType::DATA_NULL) - setType(JsonType::DATA_NULL); - - switch (type) - { - break; case JsonType::DATA_STRING: delete data.String; - break; case JsonType::DATA_VECTOR: delete data.Vector; - break; case JsonType::DATA_STRUCT: delete data.Struct; - break; default: - break; - } //Set new node type - type = Type; - switch(type) + switch(Type) { - break; case JsonType::DATA_NULL: - break; case JsonType::DATA_BOOL: data.Bool = false; - break; case JsonType::DATA_FLOAT: data.Float = 0; - break; case JsonType::DATA_STRING: data.String = new std::string(); - break; case JsonType::DATA_VECTOR: data.Vector = new JsonVector(); - break; case JsonType::DATA_STRUCT: data.Struct = new JsonMap(); - break; case JsonType::DATA_INTEGER: data.Integer = 0; + break; case JsonType::DATA_NULL: data = JsonData(); + break; case JsonType::DATA_BOOL: data = JsonData(false); + break; case JsonType::DATA_FLOAT: data = JsonData(static_cast(0.0)); + break; case JsonType::DATA_STRING: data = JsonData(std::string()); + break; case JsonType::DATA_VECTOR: data = JsonData(JsonVector()); + break; case JsonType::DATA_STRUCT: data = JsonData(JsonMap()); + break; case JsonType::DATA_INTEGER: data = JsonData(static_cast(0)); } } bool JsonNode::isNull() const { - return type == JsonType::DATA_NULL; + return getType() == JsonType::DATA_NULL; } bool JsonNode::isNumber() const { - return type == JsonType::DATA_INTEGER || type == JsonType::DATA_FLOAT; + return getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT; } bool JsonNode::isString() const { - return type == JsonType::DATA_STRING; + return getType() == JsonType::DATA_STRING; } bool JsonNode::isVector() const { - return type == JsonType::DATA_VECTOR; + return getType() == JsonType::DATA_VECTOR; } bool JsonNode::isStruct() const { - return type == JsonType::DATA_STRUCT; + return getType() == JsonType::DATA_STRUCT; } bool JsonNode::containsBaseData() const { - switch(type) + switch(getType()) { case JsonType::DATA_NULL: return false; case JsonType::DATA_STRUCT: - for(const auto & elem : *data.Struct) + for(const auto & elem : Struct()) { if(elem.second.containsBaseData()) return true; @@ -292,10 +221,10 @@ bool JsonNode::containsBaseData() const bool JsonNode::isCompact() const { - switch(type) + switch(getType()) { case JsonType::DATA_VECTOR: - for(JsonNode & elem : *data.Vector) + for(const JsonNode & elem : Vector()) { if(!elem.isCompact()) return false; @@ -303,11 +232,11 @@ bool JsonNode::isCompact() const return true; case JsonType::DATA_STRUCT: { - auto propertyCount = data.Struct->size(); + auto propertyCount = Struct().size(); if(propertyCount == 0) return true; else if(propertyCount == 1) - return data.Struct->begin()->second.isCompact(); + return Struct().begin()->second.isCompact(); } return false; default: @@ -318,10 +247,10 @@ bool JsonNode::isCompact() const bool JsonNode::TryBoolFromString(bool & success) const { success = true; - if(type == JsonNode::JsonType::DATA_BOOL) + if(getType() == JsonNode::JsonType::DATA_BOOL) return Bool(); - success = type == JsonNode::JsonType::DATA_STRING; + success = getType() == JsonNode::JsonType::DATA_STRING; if(success) { auto boolParamStr = String(); @@ -345,97 +274,99 @@ void JsonNode::clear() bool & JsonNode::Bool() { setType(JsonType::DATA_BOOL); - return data.Bool; + return std::get(data); } double & JsonNode::Float() { setType(JsonType::DATA_FLOAT); - return data.Float; + return std::get(data); } si64 & JsonNode::Integer() { setType(JsonType::DATA_INTEGER); - return data.Integer; + return std::get(data); } std::string & JsonNode::String() { setType(JsonType::DATA_STRING); - return *data.String; + return std::get(data); } JsonVector & JsonNode::Vector() { setType(JsonType::DATA_VECTOR); - return *data.Vector; + return std::get(data); } JsonMap & JsonNode::Struct() { setType(JsonType::DATA_STRUCT); - return *data.Struct; + return std::get(data); } const bool boolDefault = false; bool JsonNode::Bool() const { - if (type == JsonType::DATA_NULL) + if (getType() == JsonType::DATA_NULL) return boolDefault; - assert(type == JsonType::DATA_BOOL); - return data.Bool; + assert(getType() == JsonType::DATA_BOOL); + return std::get(data); } const double floatDefault = 0; double JsonNode::Float() const { - if(type == JsonType::DATA_NULL) + if(getType() == JsonType::DATA_NULL) return floatDefault; - else if(type == JsonType::DATA_INTEGER) - return static_cast(data.Integer); - assert(type == JsonType::DATA_FLOAT); - return data.Float; + if(getType() == JsonType::DATA_INTEGER) + return static_cast(std::get(data)); + + assert(getType() == JsonType::DATA_FLOAT); + return std::get(data); } const si64 integetDefault = 0; si64 JsonNode::Integer() const { - if(type == JsonType::DATA_NULL) + if(getType() == JsonType::DATA_NULL) return integetDefault; - else if(type == JsonType::DATA_FLOAT) - return static_cast(data.Float); - assert(type == JsonType::DATA_INTEGER); - return data.Integer; + if(getType() == JsonType::DATA_FLOAT) + return static_cast(std::get(data)); + + assert(getType() == JsonType::DATA_INTEGER); + return std::get(data); } const std::string stringDefault = std::string(); const std::string & JsonNode::String() const { - if (type == JsonType::DATA_NULL) + if (getType() == JsonType::DATA_NULL) return stringDefault; - assert(type == JsonType::DATA_STRING); - return *data.String; + assert(getType() == JsonType::DATA_STRING); + return std::get(data); } const JsonVector vectorDefault = JsonVector(); const JsonVector & JsonNode::Vector() const { - if (type == JsonType::DATA_NULL) + if (getType() == JsonType::DATA_NULL) return vectorDefault; - assert(type == JsonType::DATA_VECTOR); - return *data.Vector; + assert(getType() == JsonType::DATA_VECTOR); + return std::get(data); } const JsonMap mapDefault = JsonMap(); const JsonMap & JsonNode::Struct() const { - if (type == JsonType::DATA_NULL) + if (getType() == JsonType::DATA_NULL) return mapDefault; - assert(type == JsonType::DATA_STRUCT); - return *data.Struct; + assert(getType() == JsonType::DATA_STRUCT); + return std::get(data); } JsonNode & JsonNode::operator[](const std::string & child) @@ -1347,7 +1278,7 @@ void JsonUtils::inherit(JsonNode & descendant, const JsonNode & base) { JsonNode inheritedNode(base); merge(inheritedNode, descendant, true, true); - descendant.swap(inheritedNode); + std::swap(descendant, inheritedNode); } JsonNode JsonUtils::intersect(const std::vector & nodes, bool pruneEmpty) diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 97833d7ff..1e463aeb6 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -37,17 +37,8 @@ public: }; private: - union JsonData - { - bool Bool; - double Float; - std::string* String; - JsonVector* Vector; - JsonMap* Struct; - si64 Integer; - }; + using JsonData = std::variant; - JsonType type; JsonData data; public: @@ -64,13 +55,6 @@ public: explicit JsonNode(const JsonPath & fileURI); explicit JsonNode(const std::string & modName, const JsonPath & fileURI); explicit JsonNode(const JsonPath & fileURI, bool & isValidSyntax); - //Copy c-tor - JsonNode(const JsonNode ©); - - ~JsonNode(); - - void swap(JsonNode &b); - JsonNode& operator =(JsonNode node); bool operator == (const JsonNode &other) const; bool operator != (const JsonNode &other) const; @@ -137,30 +121,7 @@ public: { h & meta; h & flags; - h & type; - switch(type) - { - case JsonType::DATA_NULL: - break; - case JsonType::DATA_BOOL: - h & data.Bool; - break; - case JsonType::DATA_FLOAT: - h & data.Float; - break; - case JsonType::DATA_STRING: - h & data.String; - break; - case JsonType::DATA_VECTOR: - h & data.Vector; - break; - case JsonType::DATA_STRUCT: - h & data.Struct; - break; - case JsonType::DATA_INTEGER: - h & data.Integer; - break; - } + h & data; } }; diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 9d018f3e9..457e6fdc5 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -63,7 +63,7 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: if (colon == std::string::npos) { // normal object, local to this mod - modInfo.modData[entry.first].swap(entry.second); + std::swap(modInfo.modData[entry.first], entry.second); } else { diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 1264eee9b..2ed7d553d 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -404,6 +404,11 @@ public: data.reset(); } + void load(std::monostate & data) + { + // no-op + } + template void load(std::shared_ptr & data) { diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 8d8f7a297..0afcab95e 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -238,6 +238,11 @@ public: const_cast(data).serialize(*this, SERIALIZATION_VERSION); } + void save(const std::monostate & data) + { + // no-op + } + template void save(const std::shared_ptr &data) { From b50ebba1bab55a41ef5de374d5fc51d072280546 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 23:55:20 +0300 Subject: [PATCH 0892/1248] Added mod type "Compatibility" that is hidden in launcher --- config/schemas/mod.json | 2 +- docs/modders/Mod_File_Format.md | 5 +++-- launcher/modManager/cmodlist.cpp | 12 ++++++++++-- launcher/modManager/cmodlist.h | 6 ++++-- launcher/modManager/cmodlistmodel_moc.cpp | 7 ++++++- launcher/modManager/cmodlistview_moc.cpp | 8 ++++---- launcher/modManager/cmodmanager.cpp | 4 ++-- lib/modding/CModHandler.cpp | 4 ++-- lib/modding/CModInfo.cpp | 24 ++++++++++------------- lib/modding/CModInfo.h | 1 - 10 files changed, 42 insertions(+), 31 deletions(-) diff --git a/config/schemas/mod.json b/config/schemas/mod.json index 48b2f0680..36bc7baa8 100644 --- a/config/schemas/mod.json +++ b/config/schemas/mod.json @@ -55,7 +55,7 @@ }, "modType" : { "type" : "string", - "enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Artifacts", "AI" ], + "enum" : [ "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Artifacts", "AI" ], "description" : "Type of mod, e.g. Town, Artifacts, Graphical." }, "author" : { diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index 900980457..868003564 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -30,11 +30,12 @@ "version" : "1.2.3" // Type of mod, list of all possible values: - // "Translation", "Town", "Test", "Templates", "Spells", "Music", "Sounds", "Skills", "Other", "Objects", - // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Artifacts", "AI" + // "Translation", "Town", "Test", "Templates", "Spells", "Music", "Maps", "Sounds", "Skills", "Other", "Objects", + // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Artifacts", "AI" // // Some mod types have additional effects on your mod: // Translation: mod of this type is only active if player uses base language of this mod. See "language" property. + // Compatibility: mods of this type are hidden in UI and will be automatically activated if all mod dependencies are active. Intended to be used to provide compatibility patches between mods "modType" : "Graphical", // Base language of the mod, before applying localizations. By default vcmi assumes English diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index b6cf4cc07..ee6cbda59 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -90,14 +90,22 @@ bool CModEntry::isInstalled() const return !localData.isEmpty(); } -bool CModEntry::isValid() const +bool CModEntry::isVisible() const { + if (getBaseValue("modType").toString() == "Compatibility" && isSubmod()) + return false; + return !localData.isEmpty() || !repository.isEmpty(); } bool CModEntry::isTranslation() const { - return getBaseValue("modType").toString().toLower() == "translation"; + return getBaseValue("modType").toString() == "Translation"; +} + +bool CModEntry::isSubmod() const +{ + return getName().contains('.'); } int CModEntry::getModStatus() const diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index 4143ed4b8..c82a0b88c 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -60,10 +60,12 @@ public: bool isEssential() const; // checks if verison is compatible with vcmi bool isCompatible() const; - // returns if has any data - bool isValid() const; + // returns true if mod should be visible in Launcher + bool isVisible() const; // installed and enabled bool isTranslation() const; + // returns true if this is a submod + bool isSubmod() const; // see ModStatus enum int getModStatus() const; diff --git a/launcher/modManager/cmodlistmodel_moc.cpp b/launcher/modManager/cmodlistmodel_moc.cpp index 9bde9860e..cece26b53 100644 --- a/launcher/modManager/cmodlistmodel_moc.cpp +++ b/launcher/modManager/cmodlistmodel_moc.cpp @@ -45,6 +45,7 @@ QString CModListModel::modTypeName(QString modTypeID) const {"Templates", tr("Templates") }, {"Spells", tr("Spells") }, {"Music", tr("Music") }, + {"Maps", tr("Maps") }, {"Sounds", tr("Sounds") }, {"Skills", tr("Skills") }, {"Other", tr("Other") }, @@ -58,6 +59,7 @@ QString CModListModel::modTypeName(QString modTypeID) const {"Graphical", tr("Graphical") }, {"Expansion", tr("Expansion") }, {"Creatures", tr("Creatures") }, + {"Compatibility", tr("Compatibility") }, {"Artifacts", tr("Artifacts") }, {"AI", tr("AI") }, }; @@ -257,7 +259,6 @@ bool CModFilterModel::filterMatchesThis(const QModelIndex & source) const { CModEntry mod = base->getMod(source.data(ModRoles::ModNameRole).toString()); return (mod.getModStatus() & filterMask) == filteredType && - mod.isValid() && QSortFilterProxyModel::filterAcceptsRow(source.row(), source.parent()); } @@ -265,6 +266,10 @@ bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex & sourc { QModelIndex index = base->index(source_row, 0, source_parent); + CModEntry mod = base->getMod(index.data(ModRoles::ModNameRole).toString()); + if (!mod.isVisible()) + return false; + if(filterMatchesThis(index)) { return true; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index f4e8c7be5..0e713bfda 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -332,7 +332,7 @@ QString CModListView::genModInfoText(CModEntry & mod) if(mod.isInstalled()) notes += replaceIfNotEmpty(getModNames(findDependentMods(mod.getName(), false)), listTemplate.arg(hasDependentMods)); - if(mod.getName().contains('.')) + if(mod.isSubmod()) notes += noteTemplate.arg(thisIsSubmod); if(notes.size()) @@ -374,8 +374,8 @@ void CModListView::selectMod(const QModelIndex & index) ui->disableButton->setVisible(mod.isEnabled()); ui->enableButton->setVisible(mod.isDisabled()); - ui->installButton->setVisible(mod.isAvailable() && !mod.getName().contains('.')); - ui->uninstallButton->setVisible(mod.isInstalled() && !mod.getName().contains('.')); + ui->installButton->setVisible(mod.isAvailable() && !mod.isSubmod()); + ui->uninstallButton->setVisible(mod.isInstalled() && !mod.isSubmod()); ui->updateButton->setVisible(mod.isUpdateable()); // Block buttons if action is not allowed at this time @@ -921,7 +921,7 @@ void CModListView::on_allModsView_doubleClicked(const QModelIndex &index) bool hasBlockingMods = !findBlockingMods(modName).empty(); bool hasDependentMods = !findDependentMods(modName, true).empty(); - if(!hasInvalidDeps && mod.isAvailable() && !mod.getName().contains('.')) + if(!hasInvalidDeps && mod.isAvailable() && !mod.isSubmod()) { on_installButton_clicked(); return; diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 4e0c37e59..8e9ed4ee3 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -154,7 +154,7 @@ bool CModManager::canInstallMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.getName().contains('.')) + if(mod.isSubmod()) return addError(modname, "Can not install submod"); if(mod.isInstalled()) @@ -169,7 +169,7 @@ bool CModManager::canUninstallMod(QString modname) { auto mod = modList->getMod(modname); - if(mod.getName().contains('.')) + if(mod.isSubmod()) return addError(modname, "Can not uninstall submod"); if(!mod.isInstalled()) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 1414817f5..829ce6077 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -128,8 +128,8 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.getVerificationInfo().name, dependency); + if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility") + logMod->error("Mod '%s' has been disabled: dependency '%s' is missing.", brokenMod.getVerificationInfo().name, dependency); } } return sortedValidMods; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index d90ce08ae..4c665de1e 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -32,7 +32,6 @@ CModInfo::CModInfo(): CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): identifier(identifier), - description(config["description"].String()), dependencies(config["depends"].convertTo>()), conflicts(config["conflicts"].convertTo>()), explicitlyEnabled(false), @@ -45,7 +44,7 @@ CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); if(verificationInfo.parent == identifier) verificationInfo.parent.clear(); - + if(!config["compatibility"].isNull()) { vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); @@ -98,11 +97,7 @@ void CModInfo::loadLocalData(const JsonNode & data) implicitlyEnabled = true; explicitlyEnabled = !config["keepDisabled"].Bool(); verificationInfo.checksum = 0; - if (data.getType() == JsonNode::JsonType::DATA_BOOL) - { - explicitlyEnabled = data.Bool(); - } - if (data.getType() == JsonNode::JsonType::DATA_STRUCT) + if (data.isStruct()) { explicitlyEnabled = data["active"].Bool(); validated = data["validated"].Bool(); @@ -116,7 +111,7 @@ void CModInfo::loadLocalData(const JsonNode & data) if(!implicitlyEnabled) logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name); - if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment + if (config["modType"].String() == "Translation") { if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) { @@ -124,12 +119,18 @@ void CModInfo::loadLocalData(const JsonNode & data) implicitlyEnabled = false; } } + if (config["modType"].String() == "Compatibility") + { + // compatibility mods are always explicitly enabled + // however they may be implicitly disabled - if one of their dependencies is missing + explicitlyEnabled = true; + } if (isEnabled()) validation = validated ? PASSED : PENDING; else validation = validated ? PASSED : FAILED; - + verificationInfo.impactsGameplay = checkModGameplayAffecting(); } @@ -185,9 +186,4 @@ bool CModInfo::isEnabled() const return implicitlyEnabled && explicitlyEnabled; } -void CModInfo::setEnabled(bool on) -{ - explicitlyEnabled = on; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 7469e1f19..f9f227e2a 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -87,7 +87,6 @@ public: void updateChecksum(ui32 newChecksum); bool isEnabled() const; - void setEnabled(bool on); static std::string getModDir(const std::string & name); static JsonPath getModFile(const std::string & name); From adf58fa8340a561b14184fce850386b08e77211b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 00:07:21 +0300 Subject: [PATCH 0893/1248] Hide not installed translation mods to languages other than selected one --- docs/modders/Mod_File_Format.md | 3 ++- launcher/modManager/cmodlist.cpp | 17 +++++++++++++++-- lib/modding/CModInfo.cpp | 7 ++++++- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/modders/Mod_File_Format.md b/docs/modders/Mod_File_Format.md index 868003564..2b507974d 100644 --- a/docs/modders/Mod_File_Format.md +++ b/docs/modders/Mod_File_Format.md @@ -34,7 +34,8 @@ // "Mechanics", "Interface", "Heroes", "Graphical", "Expansion", "Creatures", "Compatibility", "Artifacts", "AI" // // Some mod types have additional effects on your mod: - // Translation: mod of this type is only active if player uses base language of this mod. See "language" property. + // Translation: mod of this type is only active if player uses base language of this mod. See "language" property. + // Additionally, if such type is used for submod it will be hidden in UI and automatically activated if player uses base language of this mod. This allows to provide locale-specific resources for a mod // Compatibility: mods of this type are hidden in UI and will be automatically activated if all mod dependencies are active. Intended to be used to provide compatibility patches between mods "modType" : "Graphical", diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index ee6cbda59..0545de998 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -92,8 +92,21 @@ bool CModEntry::isInstalled() const bool CModEntry::isVisible() const { - if (getBaseValue("modType").toString() == "Compatibility" && isSubmod()) - return false; + if (getBaseValue("modType").toString() == "Compatibility") + { + if (isSubmod()) + return false; + } + + if (getBaseValue("modType").toString() == "Translation") + { + // Do not show not installed translation mods to languages other than player language + if (localData.empty() && getBaseValue("language") != QString::fromStdString(settings["general"]["language"].String()) ) + return false; + + if (isSubmod()) + return false; + } return !localData.isEmpty() || !repository.isEmpty(); } diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 4c665de1e..1fc37283a 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -115,9 +115,14 @@ void CModInfo::loadLocalData(const JsonNode & data) { if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) { - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); + if (identifier.find_last_of('.') == std::string::npos) + logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); implicitlyEnabled = false; } + + // Translation submods are always explicitly enabled + if (identifier.find_last_of('.') != std::string::npos) + explicitlyEnabled = true; } if (config["modType"].String() == "Compatibility") { From 50a0ed03dbf9d784ed3208fb17c75feffbf34ecf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 00:17:00 +0300 Subject: [PATCH 0894/1248] Regenerated launcher translations --- launcher/translation/chinese.ts | 332 +++++++++++++++++----------- launcher/translation/english.ts | 332 +++++++++++++++++----------- launcher/translation/french.ts | 332 +++++++++++++++++----------- launcher/translation/german.ts | 332 +++++++++++++++++----------- launcher/translation/polish.ts | 332 +++++++++++++++++----------- launcher/translation/russian.ts | 332 +++++++++++++++++----------- launcher/translation/spanish.ts | 332 +++++++++++++++++----------- launcher/translation/ukrainian.ts | 332 +++++++++++++++++----------- launcher/translation/vietnamese.ts | 341 ++++++++++++++++++----------- 9 files changed, 1858 insertions(+), 1139 deletions(-) diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 6bdd419ce..518ce01ed 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -120,81 +120,91 @@ + Maps + + + + Sounds 音效 - + Skills 技能 - - + + Other 其他 - + Objects 物件 - + Mechanics 无法确定是否分类是游戏机制或者是游戏中的战争器械 机制 - + Interface 界面 - + Heroes 英雄 - + Graphical 图像 - + Expansion 扩展包 - + Creatures 生物 - + + Compatibility + 兼容性 + + + Artifacts 宝物 - + AI AI - + Name 名称 - + Type 类型 - + Version 版本 @@ -242,164 +252,211 @@ 下载并刷新仓库 - - + + Description 详细介绍 - + Changelog 修改日志 - + Screenshots 截图 - + Uninstall 卸载 - + Enable 激活 - + Disable 禁用 - + Update 更新 - + Install 安装 - + %p% (%v KB out of %m KB) %p% (%v KB 完成,总共 %m KB) - + Abort 终止 - + Mod name MOD名称 - + Installed version 已安装的版本 - + Latest version 最新版本 - + + Size + + + + Download size 下载大小 - + Authors 作者 - + License 授权许可 - + Contact 联系方式 - + Compatibility 兼容性 - - + + Required VCMI version 需要VCMI版本 - + Supported VCMI version 支持的VCMI版本 - + Supported VCMI versions 支持的VCMI版本 - + Languages 语言 - + Required mods 前置MODs - + Conflicting mods 冲突的MODs - + This mod can not be installed or enabled because the following dependencies are not present 这个模组无法被安装或者激活,因为下列依赖项未满足 - + This mod can not be enabled because the following mods are incompatible with it 这个模组无法被激活,因为下列模组与其不兼容 - + This mod cannot be disabled because it is required by the following mods 这个模组无法被禁用,因为它被下列模组所依赖 - + This mod cannot be uninstalled or updated because it is required by the following mods 这个模组无法被卸载或者更新,因为它被下列模组所依赖 - + This is a submod and it cannot be installed or uninstalled separately from its parent mod 这是一个附属模组它无法在所属模组外被直接被安装或者卸载 - + Notes 笔记注释 - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 截图 %1 - + Mod is incompatible MOD不兼容 @@ -407,123 +464,123 @@ CSettingsView - - - + + + Off 关闭 - - + + Artificial Intelligence 人工智能 - - + + Mod Repositories 模组仓库 - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On 开启 - + Cursor 鼠标指针 - + Heroes III Data Language 英雄无敌3数据语言 - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -534,104 +591,137 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Reserved screen area - + Hardware 硬件 - + Software 软件 - + Heroes III Translation 发布版本里找不到这个项,不太清楚意义 英雄无敌3翻译 - + Check on startup 启动时检查更新 - + Fullscreen 全屏 - - + + General 通用设置 - + VCMI Language VCMI语言 - + Resolution 分辨率 - + Autosave 自动存档 - + + VSync + + + + Display index 显示器序号 - + Network port 网络端口 - - + + Video 视频设置 - + Show intro 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -938,88 +1028,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect 连接 - + Username 用户名 - + Server 服务器 - - Lobby chat - 大厅聊天 - - - + Session 会话 - + Players 玩家 - + Resolve 解决 - + New game 新游戏 - + Load game 加载游戏 - + New room 新房间 - - Players in lobby - 大厅中的玩家 - - - + Join room 加入房间 - + Ready 准备 - + Mods mismatch MODs不匹配 - + Leave 离开 - + Kick player 踢出玩家 - + Players in the room 大厅中的玩家 @@ -1029,7 +1109,7 @@ Heroes® of Might and Magic® III HD is currently not supported! 断开 - + No issues detected 没有发现问题 diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 4d6ad1deb..94a2f2716 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -120,80 +120,90 @@ - Sounds + Maps - Skills + Sounds - - Other + Skills - Objects + + Other + Objects + + + + Mechanics - + Interface - + Heroes - + Graphical - + Expansion - + Creatures - + + Compatibility + + + + Artifacts - + AI - + Name - + Type - + Version @@ -241,164 +251,211 @@ - - + + Description - + Changelog - + Screenshots - + Uninstall - + Enable - + Disable - + Update - + Install - + %p% (%v KB out of %m KB) - + Abort - + Mod name - + Installed version - + Latest version - + + Size + + + + Download size - + Authors - + License - + Contact - + Compatibility - - + + Required VCMI version - + Supported VCMI version - + Supported VCMI versions - + Languages - + Required mods - + Conflicting mods - + This mod can not be installed or enabled because the following dependencies are not present - + This mod can not be enabled because the following mods are incompatible with it - + This mod cannot be disabled because it is required by the following mods - + This mod cannot be uninstalled or updated because it is required by the following mods - + This is a submod and it cannot be installed or uninstalled separately from its parent mod - + Notes - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 - + Mod is incompatible @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off - - + + Artificial Intelligence - - + + Mod Repositories - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On - + Cursor - + Heroes III Data Language - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -533,103 +590,136 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Reserved screen area - + Hardware - + Software - + Heroes III Translation - + Check on startup - + Fullscreen - - + + General - + VCMI Language - + Resolution - + Autosave - + + VSync + + + + Display index - + Network port - - + + Video - + Show intro - + Active - + Disabled - + Enable - + Not Installed - + Install + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -930,88 +1020,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect - + Username - + Server - - Lobby chat - - - - + Session - + Players - + Resolve - + New game - + Load game - + New room - - Players in lobby - - - - + Join room - + Ready - + Mods mismatch - + Leave - + Kick player - + Players in the room @@ -1021,7 +1101,7 @@ Heroes® of Might and Magic® III HD is currently not supported! - + No issues detected diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 0bf2c7db8..3d02b580c 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Sons - + Skills Compétences - - + + Other Autre - + Objects Objets - + Mechanics Mécaniques - + Interface Interface - + Heroes Héros - + Graphical Graphisme - + Expansion Extension - + Creatures Créatures - + + Compatibility + Compatibilité + + + Artifacts Artefacts - + AI IA - + Name Nom - + Type Type - + Version Version @@ -241,169 +251,216 @@ Télécharger et rafraîchir les dépôts - - + + Description Description - + Changelog Journal - + Screenshots Impressions écran - + %p% (%v KB out of %m KB) %p% (%v Ko sur %m Ko) - + Uninstall Désinstaller - + Enable Activer - + Disable Désactiver - + Update Mettre à jour - + Install Installer - + Abort Abandonner - + Mod name Nom du mod - + Installed version Version installée - + Latest version Dernière version - + + Size + + + + Download size Taille de téléchargement - + Authors Auteur(s) - + License Licence - + Contact Contact - + Compatibility Compatibilité - - + + Required VCMI version Version requise de VCMI - + Supported VCMI version Version supportée de VCMI - + Supported VCMI versions Versions supportées de VCMI - + Languages Langues - + Required mods Mods requis - + Conflicting mods Mods en conflit - + This mod can not be installed or enabled because the following dependencies are not present Ce mod ne peut pas être installé ou activé car les dépendances suivantes ne sont pas présents - + This mod can not be enabled because the following mods are incompatible with it Ce mod ne peut pas être installé ou activé, car les dépendances suivantes sont incompatibles avec lui - + This mod cannot be disabled because it is required by the following mods Ce mod ne peut pas être désactivé car il est requis pour les dépendances suivantes - + This mod cannot be uninstalled or updated because it is required by the following mods Ce mod ne peut pas être désinstallé ou mis à jour car il est requis pour les dépendances suivantes - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Ce sous-mod ne peut pas être installé ou mis à jour séparément du mod parent - + Notes Notes - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Impression écran %1 - + Mod is incompatible Ce mod est incompatible @@ -411,43 +468,48 @@ CSettingsView - - - + + + Off Désactivé - - + + Artificial Intelligence Intelligence Artificielle - - + + Mod Repositories Dépôts de Mod - - - + + + On Activé - + Enemy AI in battles IA ennemie dans les batailles - + Default repository Dépôt par défaut - + + VSync + + + + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -464,183 +526,211 @@ Mode fenêtré sans bord - le jeu s"exécutera dans une fenêtre qui couvre Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écran et utilisera la résolution sélectionnée. - + Windowed Fenêtré - + Borderless fullscreen Fenêtré sans bord - + Exclusive fullscreen Plein écran exclusif - + Reserved screen area - + Neutral AI in battles IA neutre dans les batailles - + Autosave limit (0 = off) - + Adventure Map Enemies Ennemis de la carte d"aventure - + Autosave prefix - + empty = map name prefix - + Interface Scaling Mise à l"échelle de l"interface - + Cursor Curseur - + Heroes III Data Language Langue des Données de Heroes III - + Framerate Limit Limite de fréquence d"images - + Hardware Matériel - + Software Logiciel - + Heroes III Translation Traduction de Heroes III - + Adventure Map Allies Alliés de la carte d"aventure - + Additional repository Dépôt supplémentaire - + Check on startup Vérifier au démarrage - + Refresh now Actualiser maintenant - + Friendly AI in battles IA amicale dans les batailles - + Fullscreen Plein écran - - + + General Général - + VCMI Language Langue de VCMI - + Resolution Résolution - + Autosave Sauvegarde automatique - + Display index Index d'affichage - + Network port Port de réseau - - + + Video Vidéo - + Show intro Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -947,88 +1037,78 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Lobby - + Username Nom d'utilisateur - - + + Connect Connecter - + Server Serveur - - Players in lobby - Joueurs à la salle d'attente - - - - Lobby chat - Discussion de salle d'attente - - - + New room Nouveau salon - + Join room Rejoindre le salon - + Session Session - + Players Joueurs - + Kick player Jeter le joueur - + Players in the room Joueurs dans le salon - + Leave Quitter - + Mods mismatch Incohérence de mods - + Ready Prêt - + Resolve Résoudre - + New game Nouvelle partie - + Load game Charger une partie @@ -1038,7 +1118,7 @@ Heroes® of Might and Magic® III HD n"est actuellement pas pris en charge Déconnecter - + No issues detected Pas de problème détecté diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index fd803264f..e08a8a7ad 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Sounds - + Skills Fertigkeiten - - + + Other Andere - + Objects Objekte - + Mechanics Mechaniken - + Interface Schnittstelle - + Heroes Helden - + Graphical Grafisches - + Expansion Erweiterung - + Creatures Kreaturen - + + Compatibility + Kompatibilität + + + Artifacts Artefakte - + AI KI - + Name Name - + Type Typ - + Version Version @@ -241,164 +251,211 @@ Repositories herunterladen && aktualisieren - - + + Description Beschreibung - + Changelog Änderungslog - + Screenshots Screenshots - + Uninstall Deinstallieren - + Enable Aktivieren - + Disable Deaktivieren - + Update Aktualisieren - + Install Installieren - + %p% (%v KB out of %m KB) %p% (%v КB von %m КB) - + Abort Abbrechen - + Mod name Mod-Name - + Installed version Installierte Version - + Latest version Letzte Version - + + Size + + + + Download size Downloadgröße - + Authors Autoren - + License Lizenz - + Contact Kontakt - + Compatibility Kompatibilität - - + + Required VCMI version Benötigte VCMI Version - + Supported VCMI version Unterstützte VCMI Version - + Supported VCMI versions Unterstützte VCMI Versionen - + Languages Sprachen - + Required mods Benötigte Mods - + Conflicting mods Mods mit Konflikt - + This mod can not be installed or enabled because the following dependencies are not present Diese Mod kann nicht installiert oder aktiviert werden, da die folgenden Abhängigkeiten nicht vorhanden sind - + This mod can not be enabled because the following mods are incompatible with it Diese Mod kann nicht aktiviert werden, da folgende Mods nicht mit dieser Mod kompatibel sind - + This mod cannot be disabled because it is required by the following mods Diese Mod kann nicht deaktiviert werden, da sie zum Ausführen der folgenden Mods erforderlich ist - + This mod cannot be uninstalled or updated because it is required by the following mods Diese Mod kann nicht deinstalliert oder aktualisiert werden, da sie für die folgenden Mods erforderlich ist - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Dies ist eine Submod und kann nicht separat von der Hauptmod installiert oder deinstalliert werden - + Notes Anmerkungen - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Screenshot %1 - + Mod is incompatible Mod ist inkompatibel @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off Aus - - + + Artificial Intelligence Künstliche Intelligenz - - + + Mod Repositories Mod-Repositorien - + Interface Scaling Skalierung der Benutzeroberfläche - + Neutral AI in battles Neutrale KI in Kämpfen - + Enemy AI in battles Gegnerische KI in Kämpfen - + Additional repository Zusätzliches Repository - + Adventure Map Allies Abenteuerkarte Verbündete - + Adventure Map Enemies Abenteuerkarte Feinde - + Windowed Fenstermodus - + Borderless fullscreen Randloser Vollbildmodus - + Exclusive fullscreen Exklusiver Vollbildmodus - + Autosave limit (0 = off) Limit für Autospeicherung (0 = aus) - + Friendly AI in battles Freundliche KI in Kämpfen - + Framerate Limit Limit der Bildrate - + Autosave prefix Präfix für Autospeicherung - + empty = map name prefix leer = Kartenname als Präfix - + Refresh now Jetzt aktualisieren - + Default repository Standard Repository - - - + + + On An - + Cursor Zeiger - + Heroes III Data Language Sprache der Heroes III Daten - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,103 +596,136 @@ Randloser Fenstermodus - das Spiel läuft in einem Fenster, das den gesamten Bil Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwendet die gewählte Auflösung. - + Reserved screen area - + Hardware Hardware - + Software Software - + Heroes III Translation Heroes III Übersetzung - + Check on startup Beim Start prüfen - + Fullscreen Vollbild - - + + General Allgemein - + VCMI Language VCMI-Sprache - + Resolution Auflösung - + Autosave Autospeichern - + + VSync + + + + Display index Anzeige-Index - + Network port Netzwerk-Port - - + + Video Video - + Show intro Intro anzeigen - + Active Aktiv - + Disabled Deaktiviert - + Enable Aktivieren - + Not Installed Nicht installiert - + Install Installieren + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -942,88 +1032,78 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Lobby - - + + Connect Verbinden - + Username Benutzername - + Server Server - - Lobby chat - Lobby-Chat - - - + Session Sitzung - + Players Spieler - + Resolve Auflösen - + New game Neues Spiel - + Load game Spiel laden - + New room Neuer Raum - - Players in lobby - Spieler in der Lobby - - - + Join room Raum beitreten - + Ready Bereit - + Mods mismatch Mods stimmen nicht überein - + Leave Verlassen - + Kick player Spieler kicken - + Players in the room Spieler im Raum @@ -1033,7 +1113,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt! Verbindung trennen - + No issues detected Keine Probleme festgestellt diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 75ea0498f..4116e62f7 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Dźwięki - + Skills Umiejętności - - + + Other Inne - + Objects Obiekty - + Mechanics Mechaniki - + Interface Interfejs - + Heroes Bohaterowie - + Graphical Graficzny - + Expansion Dodatek - + Creatures Stworzenia - + + Compatibility + Kompatybilność + + + Artifacts Artefakty - + AI AI - + Name Nazwa - + Type Typ - + Version Wersja @@ -241,164 +251,211 @@ Pobierz i odśwież repozytoria - - + + Description Opis - + Changelog Lista zmian - + Screenshots Zrzuty ekranu - + Uninstall Odinstaluj - + Enable Włącz - + Disable Wyłącz - + Update Zaktualizuj - + Install Zainstaluj - + %p% (%v KB out of %m KB) %p% (%v KB z %m KB) - + Abort Przerwij - + Mod name Nazwa moda - + Installed version Zainstalowana wersja - + Latest version Najnowsza wersja - + + Size + + + + Download size Rozmiar pobierania - + Authors Autorzy - + License Licencja - + Contact Kontakt - + Compatibility Kompatybilność - - + + Required VCMI version Wymagana wersja VCMI - + Supported VCMI version Wspierana wersja VCMI - + Supported VCMI versions Wspierane wersje VCMI - + Languages Języki - + Required mods Wymagane mody - + Conflicting mods Konfliktujące mody - + This mod can not be installed or enabled because the following dependencies are not present Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione - + This mod can not be enabled because the following mods are incompatible with it Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne - + This mod cannot be disabled because it is required by the following mods Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów - + This mod cannot be uninstalled or updated because it is required by the following mods Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów - + This is a submod and it cannot be installed or uninstalled separately from its parent mod To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego - + Notes Uwagi - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Zrzut ekranu %1 - + Mod is incompatible Mod jest niekompatybilny @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off Wyłączony - - + + Artificial Intelligence Sztuczna Inteligencja - - + + Mod Repositories Repozytoria modów - + Interface Scaling Skala interfejsu - + Neutral AI in battles AI bitewne jednostek neutralnych - + Enemy AI in battles AI bitewne wrogów - + Additional repository Dodatkowe repozytorium - + Adventure Map Allies AI sojuszników mapy przygody - + Adventure Map Enemies AI wrogów mapy przygody - + Windowed Okno - + Borderless fullscreen Pełny ekran (tryb okna) - + Exclusive fullscreen Pełny ekran klasyczny - + Autosave limit (0 = off) Limit autozapisów (0 = brak) - + Friendly AI in battles AI bitewne sojuszników - + Framerate Limit Limit FPS - + Autosave prefix Przedrostek autozapisu - + empty = map name prefix puste = przedrostek z nazwy mapy - + Refresh now Odśwież - + Default repository Domyślne repozytorium - - - + + + On Włączony - + Cursor Kursor - + Heroes III Data Language Język plików Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,103 +596,136 @@ Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały e Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu. - + Reserved screen area - + Hardware Sprzętowy - + Software Programowy - + Heroes III Translation Tłumaczenie Heroes III - + Check on startup Sprawdzaj przy uruchomieniu - + Fullscreen Pełny ekran - - + + General Ogólne - + VCMI Language Język VCMI - + Resolution Rozdzielczość - + Autosave Autozapis - + + VSync + + + + Display index Numer wyświetlacza - + Network port Port sieciowy - - + + Video Obraz - + Show intro Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -942,88 +1032,78 @@ Heroes III: HD Edition nie jest obecnie wspierane! Lobby - - + + Connect Połącz - + Username Nazwa użytkownika - + Server Serwer - - Lobby chat - Czat lobby - - - + Session Sesja - + Players Gracze - + Resolve Rozwiąż - + New game Nowa gra - + Load game Wczytaj grę - + New room Nowy pokój - - Players in lobby - Gracze w lobby - - - + Join room Dołącz - + Ready Zgłoś gotowość - + Mods mismatch Niezgodność modów - + Leave Wyjdź - + Kick player Wyrzuć gracza - + Players in the room Gracze w pokoju @@ -1033,7 +1113,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Rozłącz - + No issues detected Nie znaleziono problemów diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index a5ef0dd5c..718618d7d 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Звуки - + Skills Навыки - - + + Other Иное - + Objects Объекты - + Mechanics Механика - + Interface Интерфейс - + Heroes Герои - + Graphical Графика - + Expansion Дополнение - + Creatures Существа - + + Compatibility + Совместимость + + + Artifacts Артефакт - + AI ИИ - + Name Название - + Type Тип - + Version Версия @@ -241,164 +251,211 @@ Обновить репозиторий - - + + Description Описание - + Changelog Изменения - + Screenshots Скриншоты - + Uninstall Удалить - + Enable Включить - + Disable Отключить - + Update Обновить - + Install Установить - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) - + Abort Отмена - + Mod name Название мода - + Installed version Установленная версия - + Latest version Последняя версия - + + Size + + + + Download size Размер загрузки - + Authors Авторы - + License Лицензия - + Contact Контакты - + Compatibility Совместимость - - + + Required VCMI version Требуемая версия VCMI - + Supported VCMI version Поддерживаемая версия VCMI - + Supported VCMI versions Поддерживаемые версии VCMI - + Languages Языки - + Required mods Зависимости - + Conflicting mods Конфликтующие моды - + This mod can not be installed or enabled because the following dependencies are not present Этот мод не может быть установлен или активирован, так как отсутствуют следующие зависимости - + This mod can not be enabled because the following mods are incompatible with it Этот мод не может быть установлен или активирован, так как следующие моды несовместимы с этим - + This mod cannot be disabled because it is required by the following mods Этот мод не может быть выключен, так как он является зависимостью для следующих - + This mod cannot be uninstalled or updated because it is required by the following mods Этот мод не может быть удален или обновлен, так как является зависимостью для следующих модов - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Это вложенный мод, он не может быть установлен или удален отдельно от родительского - + Notes Замечания - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Скриншот %1 - + Mod is incompatible Мод несовместим @@ -406,149 +463,154 @@ CSettingsView - + Interface Scaling - - - + + + Off Отключено - - - + + + On Включено - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Check on startup Проверять при запуске - + Fullscreen Полноэкранный режим - - + + General Общее - + VCMI Language Язык VCMI - + Cursor Курсор - - + + Artificial Intelligence Искусственный интеллект - - + + Mod Repositories Репозитории модов - + Adventure Map Allies - + Refresh now - + Adventure Map Enemies - + + VSync + + + + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Reserved screen area - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Default repository - + Heroes III Data Language Язык данных Героев III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -559,77 +621,105 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Аппаратный - + Software Программный - + Heroes III Translation Перевод Героев III - + Resolution Разрешение экрана - + Autosave Автосохранение - + Display index Дисплей - + Network port Сетевой порт - - + + Video Графика - + Show intro Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -936,88 +1026,78 @@ Heroes® of Might and Magic® III HD is currently not supported! Lobby - - + + Connect Подключиться - + Username Имя пользователя - + Server Сервер - - Lobby chat - Чат лобби - - - + Session Сессия - + Players Игроки - + Resolve Скорректировать - + New game Новая игра - + Load game Загрузить игру - + New room Создать комнату - - Players in lobby - Люди в лобби - - - + Join room Присоединиться к комнате - + Ready Готово - + Mods mismatch Моды не совпадают - + Leave Выйти - + Kick player Выгнать игрока - + Players in the room Игроки в комнате @@ -1027,7 +1107,7 @@ Heroes® of Might and Magic® III HD is currently not supported! Отключиться - + No issues detected Проблем не обнаружено diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index 04499cedf..b0a953ab7 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Sonidos - + Skills Habilidades - - + + Other Otro - + Objects Objetos - + Mechanics Mecánicas - + Interface Interfaz - + Heroes Heroes - + Graphical Gráficos - + Expansion Expansión - + Creatures Criaturas - + + Compatibility + Compatibilidad + + + Artifacts Artefactos - + AI IA - + Name Nombre - + Type Tipo - + Version Versión @@ -241,164 +251,211 @@ Descargar y actualizar repositorios - - + + Description Descripción - + Changelog Registro de cambios - + Screenshots Capturas de pantalla - + Uninstall Desinstalar - + Enable Activar - + Disable Desactivar - + Update Actualizar - + Install Instalar - + %p% (%v KB out of %m KB) %p% (%v KB de %m KB) - + Abort Cancelar - + Mod name Nombre del mod - + Installed version Versión instalada - + Latest version Última versión - + + Size + + + + Download size Tamaño de descarga - + Authors Autores - + License Licencia - + Contact Contacto - + Compatibility Compatibilidad - - + + Required VCMI version Versión de VCMI requerida - + Supported VCMI version Versión de VCMI compatible - + Supported VCMI versions Versiones de VCMI compatibles - + Languages Idiomas - + Required mods Mods requeridos - + Conflicting mods Mods conflictivos - + This mod can not be installed or enabled because the following dependencies are not present Este mod no se puede instalar o habilitar porque no están presentes las siguientes dependencias - + This mod can not be enabled because the following mods are incompatible with it Este mod no se puede habilitar porque los siguientes mods son incompatibles con él - + This mod cannot be disabled because it is required by the following mods No se puede desactivar este mod porque es necesario para ejecutar los siguientes mods - + This mod cannot be uninstalled or updated because it is required by the following mods No se puede desinstalar o actualizar este mod porque es necesario para ejecutar los siguientes mods - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Este es un submod y no se puede instalar o desinstalar por separado del mod principal - + Notes Notas - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Captura de pantalla %1 - + Mod is incompatible El mod es incompatible @@ -406,170 +463,175 @@ CSettingsView - - - + + + Off Desactivado - - + + Artificial Intelligence Inteligencia Artificial - - + + Mod Repositories Repositorios de Mods - + Interface Scaling - + Neutral AI in battles - + Enemy AI in battles - + Additional repository - + Adventure Map Allies - + Adventure Map Enemies - + Windowed - + Borderless fullscreen - + Exclusive fullscreen - + Autosave limit (0 = off) - + Friendly AI in battles - + Framerate Limit - + Autosave prefix - + empty = map name prefix - + Refresh now - + Default repository - - - + + + On Encendido - + Cursor Cursor - + Heroes III Translation Traducción de Heroes III - + Reserved screen area - + Fullscreen Pantalla completa - - + + General General - + VCMI Language Idioma de VCMI - + Resolution Resolución - + Autosave Autoguardado - + + VSync + + + + Display index Mostrar índice - + Network port Puerto de red - - + + Video Vídeo - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -580,56 +642,84 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Hardware Hardware - + Software Software - + Show intro Mostrar introducción - + Check on startup Comprovar al inicio - + Heroes III Data Language Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -936,88 +1026,78 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Lobby - - + + Connect Conectar - + Username Nombre de usuario - + Server Servidor - - Lobby chat - Charlar en la sala - - - + Session Sesión - + Players Jugadores - + Resolve Resolver - + New game Nueva partida - + Load game Cargar partida - + New room Nueva sala - - Players in lobby - Jugadores en la sala - - - + Join room Unirse a la sala - + Ready Listo - + Mods mismatch No coinciden los mods - + Leave Salir - + Kick player Expulsar jugador - + Players in the room Jugadores en la sala @@ -1027,7 +1107,7 @@ Ten en cuenta que para usar VCMI debes ser dueño de los archivos de datos origi Desconectar - + No issues detected No se han detectado problemas diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index d3015e426..9b097eb48 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Звуки - + Skills Вміння - - + + Other Інше - + Objects Об'єкти - + Mechanics Механіки - + Interface Інтерфейс - + Heroes Герої - + Graphical Графічний - + Expansion Розширення - + Creatures Істоти - + + Compatibility + Сумісність + + + Artifacts Артефакти - + AI ШІ - + Name Назва - + Type Тип - + Version Версія @@ -241,164 +251,211 @@ Оновити репозиторії - - + + Description Опис - + Changelog Зміни - + Screenshots Знімки - + Uninstall Видалити - + Enable Активувати - + Disable Деактивувати - + Update Оновити - + Install Встановити - + %p% (%v KB out of %m KB) %p% (%v КБ з %m КБ) - + Abort Відмінити - + Mod name Назва модифікації - + Installed version Встановлена версія - + Latest version Найновіша версія - + + Size + + + + Download size Розмір для завантаження - + Authors Автори - + License Ліцензія - + Contact Контакти - + Compatibility Сумісність - - + + Required VCMI version Необхідна версія VCMI - + Supported VCMI version Підтримувана версія VCMI - + Supported VCMI versions Підтримувані версії VCMI - + Languages Мови - + Required mods Необхідні модифікації - + Conflicting mods Конфліктуючі модифікації - + This mod can not be installed or enabled because the following dependencies are not present Цю модифікацію не можна встановити чи активувати, оскільки відсутні наступні залежності - + This mod can not be enabled because the following mods are incompatible with it Цю модифікацію не можна ввімкнути, оскільки наступні модифікації несумісні з цією модифікацією - + This mod cannot be disabled because it is required by the following mods Цю модифікацію не можна відключити, оскільки вона необхідна для запуску наступних модифікацій - + This mod cannot be uninstalled or updated because it is required by the following mods Цю модифікацію не можна видалити або оновити, оскільки вона необхідна для запуску наступних модифікацій - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Це вкладена модифікація, і її не можна встановити або видалити окремо від батьківської модифікації - + Notes Примітки - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Знімок екрану %1 - + Mod is incompatible Модифікація несумісна @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off Вимкнено - - + + Artificial Intelligence Штучний інтелект - - + + Mod Repositories Репозиторії модифікацій - + Interface Scaling Масштабування інтерфейсу - + Neutral AI in battles Нейтральний ШІ в боях - + Enemy AI in battles Ворожий ШІ в боях - + Additional repository Додатковий репозиторій - + Adventure Map Allies Союзники на мапі пригод - + Adventure Map Enemies Вороги на мапі пригод - + Windowed У вікні - + Borderless fullscreen Повноекранне вікно - + Exclusive fullscreen Повноекранний (ексклюзивно) - + Autosave limit (0 = off) Кількість автозбережень - + Friendly AI in battles Дружній ШІ в боях - + Framerate Limit Обмеження частоти кадрів - + Autosave prefix Префікс назв автозбережень - + empty = map name prefix (використовувати назву карти) - + Refresh now Оновити зараз - + Default repository Стандартний репозиторій - - - + + + On Увімкнено - + Cursor Курсор - + Heroes III Data Language Мова Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,103 +596,136 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Повноекранний ексклюзивний режим - гра займатиме весь екран і використовуватиме вибрану роздільну здатність. - + Reserved screen area Зарезервована зона екрану - + Hardware Апаратний - + Software Програмний - + Heroes III Translation Переклад Heroes III - + Check on startup Перевіряти на старті - + Fullscreen Повноекранний режим - - + + General Загальні налаштування - + VCMI Language Мова VCMI - + Resolution Роздільна здатність - + Autosave Автозбереження - + + VSync + + + + Display index Дісплей - + Network port Мережевий порт - - + + Video Графіка - + Show intro Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -942,88 +1032,78 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Lobby - - + + Connect Підключитися - + Username Ім'я користувача - + Server Сервер - - Lobby chat - Лобі чат - - - + Session Сесія - + Players Гравці - + Resolve Розв'язати - + New game Нова гра - + Load game Завантажити гру - + New room Створити кімнату - - Players in lobby - Гравці у лобі - - - + Join room Приєднатися до кімнати - + Ready Готовність - + Mods mismatch Модифікації, що не збігаються - + Leave Вийти з кімнати - + Kick player Виключити гравця - + Players in the room Гравці у кімнаті @@ -1033,7 +1113,7 @@ Heroes® of Might and Magic® III HD наразі не підтримуєтьс Від'єднатися - + No issues detected Проблем не виявлено diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index 79c7518ad..f92e9e7fe 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -120,80 +120,90 @@ + Maps + + + + Sounds Âm thanh - + Skills Kĩ năng - - + + Other Khác - + Objects Đối tượng - + Mechanics Cơ chế - + Interface Giao diện - + Heroes Tướng - + Graphical Đồ họa - + Expansion Bản mở rộng - + Creatures Quái - + + Compatibility + Tương thích + + + Artifacts Vật phẩm - + AI Trí tuệ nhân tạo - + Name Tên - + Type Loại - + Version Phiên bản @@ -241,164 +251,211 @@ Tải lại - - + + Description Mô tả - + Changelog Các thay đổi - + Screenshots Hình ảnh - + Uninstall Gỡ bỏ - + Enable Bật - + Disable Tắt - + Update Cập nhật - + Install Cài đặt - + %p% (%v KB out of %m KB) %p% (%v KB trong số %m KB) - + Abort Hủy - + Mod name Tên bản sửa đổi - + Installed version Phiên bản cài đặt - + Latest version Phiên bản mới nhất - + + Size + + + + Download size Kích thước tải về - + Authors Tác giả - + License Giấy phép - + Contact Liên hệ - + Compatibility Tương thích - - + + Required VCMI version Cần phiên bản VCMI - + Supported VCMI version Hỗ trợ phiên bản VCMI - + Supported VCMI versions Phiên bản VCMI hỗ trợ - + Languages Ngôn ngữ - + Required mods Cần các bản sửa đổi - + Conflicting mods Bản sửa đổi không tương thích - + This mod can not be installed or enabled because the following dependencies are not present Bản sửa đổi này không thể cài đặt hoặc kích hoạt do thiếu các bản sửa đổi sau - + This mod can not be enabled because the following mods are incompatible with it Bản sửa đổi này không thể kích hoạt do không tương thích các bản sửa đổi sau - + This mod cannot be disabled because it is required by the following mods Bản sửa đổi này không thể tắt do cần thiết cho các bản sửa đổi sau - + This mod cannot be uninstalled or updated because it is required by the following mods Bản sửa đổi này không thể gỡ bỏ hoặc nâng cấp do cần thiết cho các bản sửa đổi sau - + This is a submod and it cannot be installed or uninstalled separately from its parent mod Đây là bản con, không thể cài đặt hoặc gỡ bỏ tách biệt với bản cha - + Notes Ghi chú - + + Downloading %s%. %p% (%v MB out of %m MB) finished + + + + + Download failed + + + + + Unable to download all files. + +Encountered errors: + + + + + + + + +Install successfully downloaded? + + + + + Installing mod %1 + + + + + Operation failed + + + + + Encountered errors: + + + + + Screenshot %1 Hình ảnh %1 - + Mod is incompatible Bản sửa đổi này không tương thích @@ -406,123 +463,123 @@ CSettingsView - - - + + + Off Tắt - - + + Artificial Intelligence Trí tuệ nhân tạo - - + + Mod Repositories Nguồn bản sửa đổi - + Interface Scaling Phóng đại giao diện - + Neutral AI in battles Máy hoang dã trong trận đánh - + Enemy AI in battles Máy đối thủ trong trận đánh - + Additional repository Nguồn bổ sung - + Adventure Map Allies Máy liên minh ở bản đồ phiêu lưu - + Adventure Map Enemies Máy đối thủ ở bản đồ phiêu lưu - + Windowed Cửa sổ - + Borderless fullscreen Toàn màn hình không viền - + Exclusive fullscreen Toàn màn hình riêng biệt - + Autosave limit (0 = off) Giới hạn lưu tự động (0 = không giới hạn) - + Friendly AI in battles Máy liên minh trong trận đánh - + Framerate Limit Giới hạn khung hình - + Autosave prefix Thêm tiền tố vào lưu tự động - + empty = map name prefix Rỗng = tên bản đồ - + Refresh now Làm mới - + Default repository Nguồn mặc định - - - + + + On Bật - + Cursor Con trỏ - + Heroes III Data Language Ngôn ngữ dữ liệu Heroes III - + Select display mode for game Windowed - game will run inside a window that covers part of your screen @@ -539,103 +596,136 @@ Toàn màn hình không viền - Trò chơi chạy toàn màn hình, dùng chung Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng độ phân giải được chọn. - + Reserved screen area Diện tích màn hình dành riêng - + Hardware Phần cứng - + Software Phần mềm - + Heroes III Translation Bản dịch Heroes III - + Check on startup Kiểm tra khi khởi động - + Fullscreen Toàn màn hình - - + + General Chung - + VCMI Language Ngôn ngữ VCMI - + Resolution Độ phân giải - + Autosave Tự động lưu - + + VSync + + + + Display index Mục hiện thị - + Network port Cổng mạng - - + + Video Phim ảnh - + Show intro Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt + + Chat + + + Form + + + + + Users in lobby + + + + + Global chat + + + + + type you message + + + + + send + + + FirstLaunchView @@ -942,88 +1032,78 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD! Lobby - - + + Connect Kết nối - + Username Tên đăng nhập - + Server Máy chủ - - Lobby chat - Trò chuyện - - - + Session Phiên - + Players Người chơi - + Resolve Phân tích - + New game Tạo mới - + Load game Tải lại - + New room Tạo phòng - - Players in lobby - Người chơi trong sảnh - - - + Join room Vào phòng - + Ready Sẵn sàng - + Mods mismatch Bản sửa đổi chưa giống - + Leave Rời khỏi - + Kick player Mời ra - + Players in the room Người chơi trong phòng @@ -1033,7 +1113,7 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!Thoát - + No issues detected Không có vấn đề @@ -1103,19 +1183,18 @@ Hiện tại chưa hỗ trợ Heroes® of Might and Magic® III HD!UpdateDialog - You have latest version - Bạn đã có phiên bản mới nhất + You have the latest version + - Close Đóng - Check updates on startup - Cập nhật khi khởi động + Check for updates on startup + From b0d2342c6668d7e0025287dd759a5fa2e8d87175 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 00:23:58 +0300 Subject: [PATCH 0895/1248] Updated Ukrainian translation --- launcher/translation/ukrainian.ts | 39 ++++++++++++++++++------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 9b097eb48..37c76d07c 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -121,7 +121,7 @@ Maps - + Мапи @@ -180,7 +180,7 @@ Compatibility - Сумісність + Сумісність @@ -319,7 +319,7 @@ Size - + Розмір @@ -410,12 +410,12 @@ Downloading %s%. %p% (%v MB out of %m MB) finished - + Завантажуємо %s%. %p% (%v МБ з %m Мб) виконано Download failed - + Помилка завантаження @@ -424,30 +424,37 @@ Encountered errors: - + Не вдалося завантажити усі файли. + +Виникли помилки: + + Install successfully downloaded? - + + +Встановити успішно завантажені? Installing mod %1 - + Встановлення модифікації %1 Operation failed - + Операція завершилася невдало Encountered errors: - + Виникли помилки: + @@ -649,7 +656,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use VSync - + Вертикальна синхронізація @@ -703,27 +710,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Form - + Users in lobby - + Гравців у лобі Global chat - + Загальний чат type you message - + введіть повідомлення send - + Відправити From 693998902674c490754b5f763affefe421faa1e2 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 21 Oct 2023 23:37:59 +0200 Subject: [PATCH 0896/1248] try to fix --- client/gui/CIntObject.cpp | 10 ++++++++++ client/gui/CIntObject.h | 4 ++++ client/gui/EventDispatcher.cpp | 8 +------- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 74c946d16..16e91010f 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -258,6 +258,16 @@ void CIntObject::redraw() } } +void CIntObject::clickPressed(const Point & cursorPosition, bool lastActivated) +{ + AEventsReceiver::clickPressed(cursorPosition); +} + +void CIntObject::clickReleased(const Point & cursorPosition, bool lastActivated) +{ + AEventsReceiver::clickReleased(cursorPosition); +} + bool CIntObject::receiveEvent(const Point & position, int eventType) const { return pos.isInside(position); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 6ea72ed56..b04683ce9 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -99,6 +99,10 @@ public: //request complete redraw of this object void redraw() override; + // events are overloaded + void clickPressed(const Point & cursorPosition, bool lastActivated) override; + void clickReleased(const Point & cursorPosition, bool lastActivated) override; + /// returns true if this element is a popup window /// called only for windows bool isPopupWindow() const override; diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 721954848..3a7222520 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -213,16 +213,10 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement) { if(isPressed) - { - i->clickPressed(position); - i->clickPressed(position, lastActivated); - } + i->clickReleased(position, lastActivated); if (i->mouseClickedState && !isPressed) - { - i->clickReleased(position); i->clickReleased(position, lastActivated); - } i->mouseClickedState = isPressed; lastActivated = false; From 05a9f0fe39dda8d9398ef96019c90dc9bfc6cb99 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:05:14 +0200 Subject: [PATCH 0897/1248] move to EventReceiver --- client/gui/CIntObject.cpp | 10 ---------- client/gui/CIntObject.h | 5 ----- client/gui/EventsReceiver.cpp | 10 ++++++++++ client/gui/EventsReceiver.h | 4 ++-- 4 files changed, 12 insertions(+), 17 deletions(-) diff --git a/client/gui/CIntObject.cpp b/client/gui/CIntObject.cpp index 16e91010f..74c946d16 100644 --- a/client/gui/CIntObject.cpp +++ b/client/gui/CIntObject.cpp @@ -258,16 +258,6 @@ void CIntObject::redraw() } } -void CIntObject::clickPressed(const Point & cursorPosition, bool lastActivated) -{ - AEventsReceiver::clickPressed(cursorPosition); -} - -void CIntObject::clickReleased(const Point & cursorPosition, bool lastActivated) -{ - AEventsReceiver::clickReleased(cursorPosition); -} - bool CIntObject::receiveEvent(const Point & position, int eventType) const { return pos.isInside(position); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index b04683ce9..6536376e8 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -98,11 +98,6 @@ public: void showAll(Canvas & to) override; //request complete redraw of this object void redraw() override; - - // events are overloaded - void clickPressed(const Point & cursorPosition, bool lastActivated) override; - void clickReleased(const Point & cursorPosition, bool lastActivated) override; - /// returns true if this element is a popup window /// called only for windows bool isPopupWindow() const override; diff --git a/client/gui/EventsReceiver.cpp b/client/gui/EventsReceiver.cpp index 2dbed3a33..d19447add 100644 --- a/client/gui/EventsReceiver.cpp +++ b/client/gui/EventsReceiver.cpp @@ -68,3 +68,13 @@ void AEventsReceiver::deactivateEvents(ui16 what) // if (!(activeState & HOVER)) // hoveredState = false; } + +void AEventsReceiver::clickPressed(const Point & cursorPosition, bool lastActivated) +{ + clickPressed(cursorPosition); +} + +void AEventsReceiver::clickReleased(const Point & cursorPosition, bool lastActivated) +{ + clickReleased(cursorPosition); +} diff --git a/client/gui/EventsReceiver.h b/client/gui/EventsReceiver.h index 690c9b9a8..2d5fedede 100644 --- a/client/gui/EventsReceiver.h +++ b/client/gui/EventsReceiver.h @@ -45,8 +45,8 @@ protected: public: virtual void clickPressed(const Point & cursorPosition) {} virtual void clickReleased(const Point & cursorPosition) {} - virtual void clickPressed(const Point & cursorPosition, bool lastActivated) {} - virtual void clickReleased(const Point & cursorPosition, bool lastActivated) {} + virtual void clickPressed(const Point & cursorPosition, bool lastActivated); + virtual void clickReleased(const Point & cursorPosition, bool lastActivated); virtual void clickCancel(const Point & cursorPosition) {} virtual void showPopupWindow(const Point & cursorPosition) {} virtual void clickDouble(const Point & cursorPosition) {} From ab59c153b3f31fa288f8c64220535075d3ac6e6a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:22:16 +0200 Subject: [PATCH 0898/1248] fix --- client/gui/CIntObject.h | 1 + client/gui/EventDispatcher.cpp | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 6536376e8..adffdfde6 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -98,6 +98,7 @@ public: void showAll(Canvas & to) override; //request complete redraw of this object void redraw() override; + /// returns true if this element is a popup window /// called only for windows bool isPopupWindow() const override; diff --git a/client/gui/EventDispatcher.cpp b/client/gui/EventDispatcher.cpp index 3a7222520..4c9dc9726 100644 --- a/client/gui/EventDispatcher.cpp +++ b/client/gui/EventDispatcher.cpp @@ -213,7 +213,7 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement) { if(isPressed) - i->clickReleased(position, lastActivated); + i->clickPressed(position, lastActivated); if (i->mouseClickedState && !isPressed) i->clickReleased(position, lastActivated); From 0b0082eebca50a4e4e5287512fb39e51944fd7b0 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 22 Oct 2023 00:22:49 +0200 Subject: [PATCH 0899/1248] format --- client/gui/CIntObject.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index adffdfde6..6ea72ed56 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -98,7 +98,7 @@ public: void showAll(Canvas & to) override; //request complete redraw of this object void redraw() override; - + /// returns true if this element is a popup window /// called only for windows bool isPopupWindow() const override; From 7b370d1967fc183e7e90c562fb1c714b7a585808 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 22 Oct 2023 15:00:04 +0200 Subject: [PATCH 0900/1248] fix team alignments --- client/lobby/RandomMapTab.cpp | 38 +++++++++++++++++++++++++---------- client/lobby/RandomMapTab.h | 9 +++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 65169dff7..38c7a630d 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -107,7 +107,7 @@ RandomMapTab::RandomMapTab(): //new callbacks available only from mod addCallback("teamAlignments", [&](int) { - GH.windows().createAndPushWindow(*this); + GH.windows().createAndPushWindow(*this); }); for(auto road : VLC->roadTypeHandler->objects) @@ -122,7 +122,7 @@ RandomMapTab::RandomMapTab(): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTab.json")); build(config); - + if(auto w = widget("buttonShowRandomMaps")) { w->addCallback([&]() @@ -132,7 +132,7 @@ RandomMapTab::RandomMapTab(): (static_cast(parent))->tabSel->filter(0, true); }); } - + //set combo box callbacks if(auto w = widget("templateList")) { @@ -397,6 +397,25 @@ std::vector RandomMapTab::getPossibleMapSizes() return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_GIANT}; } +TeamAlignments::TeamAlignments(RandomMapTab & randomMapTab) + : CWindowObject(BORDERED) +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + widget = std::make_shared(randomMapTab); + + pos = widget->pos; + + backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); + backgroundTexture->playerColored(PlayerColor(1)); + + std::swap(GH.createdObj.front()->children.end()[-1], GH.createdObj.front()->children.end()[-2]); // widget to top + + updateShadow(); + + center(); +} + TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): InterfaceObjectConfigurable() { @@ -413,10 +432,6 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): pos.w = variables["windowSize"]["x"].Integer() + totalPlayers * variables["cellMargin"]["x"].Integer(); pos.h = variables["windowSize"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); - variables["backgroundRect"]["x"].Integer() = pos.x; - variables["backgroundRect"]["y"].Integer() = pos.y; - variables["backgroundRect"]["w"].Integer() = pos.w; - variables["backgroundRect"]["h"].Integer() = pos.h; variables["okButtonPosition"]["x"].Integer() = variables["buttonsOffset"]["ok"]["x"].Integer(); variables["okButtonPosition"]["y"].Integer() = variables["buttonsOffset"]["ok"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); variables["cancelButtonPosition"]["x"].Integer() = variables["buttonsOffset"]["cancel"]["x"].Integer(); @@ -429,14 +444,15 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): randomMapTab.obtainMapGenOptions().setPlayerTeam(PlayerColor(plId), TeamID(players[plId]->getSelected())); } randomMapTab.updateMapInfoByHost(); - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); + + for(auto & window : GH.windows().findWindows()) + GH.windows().popWindow(window); }); addCallback("cancel", [&](int) { - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); + for(auto & window : GH.windows().findWindows()) + GH.windows().popWindow(window); }); build(config); diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index e23657548..5c113b44c 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -27,6 +27,7 @@ class CLabel; class CLabelGroup; class CSlider; class CPicture; +class FilledTexturePlayerColored; class RandomMapTab : public InterfaceObjectConfigurable { @@ -63,3 +64,11 @@ private: std::vector> players; std::vector> placeholders; }; + +class TeamAlignments: public CWindowObject +{ + std::shared_ptr widget; + std::shared_ptr backgroundTexture; +public: + TeamAlignments(RandomMapTab & randomMapTab); +}; From b67548e7cf4df945b842949ef6dfd9b059aa6a36 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 22 Oct 2023 15:06:12 +0200 Subject: [PATCH 0901/1248] format --- client/lobby/RandomMapTab.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 38c7a630d..8e67eb48e 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -122,7 +122,7 @@ RandomMapTab::RandomMapTab(): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTab.json")); build(config); - + if(auto w = widget("buttonShowRandomMaps")) { w->addCallback([&]() @@ -132,7 +132,7 @@ RandomMapTab::RandomMapTab(): (static_cast(parent))->tabSel->filter(0, true); }); } - + //set combo box callbacks if(auto w = widget("templateList")) { From 7e034814c1b19a08d1b94cca578b90e451326818 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 16:51:08 +0300 Subject: [PATCH 0902/1248] Partially revert "Translation" type changes to avoid breaking mods --- launcher/modManager/cmodlist.cpp | 3 --- lib/modding/CModInfo.cpp | 4 ---- 2 files changed, 7 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 0545de998..7e887b5d6 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -103,9 +103,6 @@ bool CModEntry::isVisible() const // Do not show not installed translation mods to languages other than player language if (localData.empty() && getBaseValue("language") != QString::fromStdString(settings["general"]["language"].String()) ) return false; - - if (isSubmod()) - return false; } return !localData.isEmpty() || !repository.isEmpty(); diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 1fc37283a..0e534da45 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -119,10 +119,6 @@ void CModInfo::loadLocalData(const JsonNode & data) logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); implicitlyEnabled = false; } - - // Translation submods are always explicitly enabled - if (identifier.find_last_of('.') != std::string::npos) - explicitlyEnabled = true; } if (config["modType"].String() == "Compatibility") { From 0a10fc30b845a27812757da50de4fe5cd49b5061 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 5 Oct 2023 16:13:52 +0300 Subject: [PATCH 0903/1248] (lib) Bonus subtype is now stored as metaidentifier that can store any other identifier inside it --- AI/Nullkiller/Analyzers/HeroManager.cpp | 6 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 2 +- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 8 +- .../Rules/AILayerTransitionRule.cpp | 2 +- AI/VCAI/Pathfinding/AINodeStorage.cpp | 2 +- .../Rules/AILayerTransitionRule.cpp | 2 +- config/schemas/bonus.json | 5 +- docs/modders/Bonus/Bonus_Types.md | 8 +- include/vcmi/FactionMember.h | 2 +- lib/ArtifactUtils.cpp | 2 +- lib/BasicTypes.cpp | 12 +- lib/CArtifactInstance.cpp | 2 +- lib/CBonusTypeHandler.cpp | 43 ++++--- lib/CCreatureHandler.cpp | 88 +++++++------- lib/CCreatureHandler.h | 3 +- lib/CGameInfoCallback.cpp | 6 +- lib/CHeroHandler.cpp | 6 +- lib/CStack.cpp | 2 +- lib/CTownHandler.cpp | 18 +-- lib/CTownHandler.h | 7 +- lib/JsonNode.cpp | 98 ++++++---------- lib/JsonNode.h | 9 -- lib/battle/BattleInfo.cpp | 6 +- lib/battle/CBattleInfoCallback.cpp | 4 +- lib/battle/CUnitState.cpp | 12 +- lib/battle/DamageCalculator.cpp | 25 ++-- lib/bonuses/Bonus.cpp | 33 +----- lib/bonuses/Bonus.h | 47 +++++++- lib/bonuses/BonusParams.cpp | 75 ++++++------ lib/bonuses/BonusParams.h | 3 +- lib/bonuses/IBonusBearer.cpp | 10 +- lib/bonuses/IBonusBearer.h | 6 +- lib/bonuses/Limiters.cpp | 8 +- lib/constants/EntityIdentifiers.h | 108 ++++++++++++++++++ lib/constants/Enumerations.h | 14 +-- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameStateCampaign.cpp | 28 +++-- lib/mapObjects/CGCreature.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 64 +++++------ lib/mapObjects/CGHeroInstance.h | 2 +- lib/mapObjects/CGTownBuilding.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 4 +- lib/pathfinder/CPathfinder.cpp | 4 +- lib/pathfinder/CPathfinder.h | 2 +- lib/pathfinder/TurnInfo.cpp | 8 +- lib/pathfinder/TurnInfo.h | 6 +- lib/rewardable/Reward.cpp | 2 +- lib/spells/AbilityCaster.cpp | 2 +- lib/spells/CSpellHandler.cpp | 10 +- lib/spells/TargetCondition.cpp | 12 +- lib/spells/effects/Damage.cpp | 4 +- lib/spells/effects/Timed.cpp | 8 +- lib/spells/effects/UnitEffect.cpp | 2 +- 54 files changed, 455 insertions(+), 395 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 4397620b6..cf6621a21 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -313,7 +313,7 @@ void ExistingSkillRule::evaluateScore(const CGHeroInstance * hero, SecondarySkil if(heroSkill.first == skill) return; - upgradesLeft += SecSkillLevel::EXPERT - heroSkill.second; + upgradesLeft += MasteryLevel::EXPERT - heroSkill.second; } if(score >= 2 || (score >= 1 && upgradesLeft <= 1)) @@ -327,7 +327,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill auto wisdomLevel = hero->getSecSkillLevel(SecondarySkill::WISDOM); - if(hero->level > 10 && wisdomLevel == SecSkillLevel::NONE) + if(hero->level > 10 && wisdomLevel == MasteryLevel::NONE) score += 1.5; } @@ -345,7 +345,7 @@ void AtLeastOneMagicRule::evaluateScore(const CGHeroInstance * hero, SecondarySk bool heroHasAnyMagic = vstd::contains_if(magicSchools, [&](SecondarySkill skill) -> bool { - return hero->getSecSkillLevel(skill) > SecSkillLevel::NONE; + return hero->getSecSkillLevel(skill) > MasteryLevel::NONE; }); if(!heroHasAnyMagic) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index eda7e7faa..d3725d0f7 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -538,7 +538,7 @@ float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, if(!hut->wasVisited(hero->tempOwner)) return role == HeroRole::SCOUT ? 2 : 0; - if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE + if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) return 0; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index 067525c3e..e53c921a9 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -984,7 +984,7 @@ std::vector AINodeStorage::calculateTeleportations( struct TowmPortalFinder { const std::vector & initialNodes; - SecSkillLevel::SecSkillLevel townPortalSkillLevel; + MasteryLevel townPortalSkillLevel; uint64_t movementNeeded; const ChainActor * actor; const CGHeroInstance * hero; @@ -1006,8 +1006,8 @@ struct TowmPortalFinder townPortal = spellID.toSpell(); // TODO: Copy/Paste from TownPortalMechanics - townPortalSkillLevel = SecSkillLevel::SecSkillLevel(hero->getSpellSchoolLevel(townPortal)); - movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= SecSkillLevel::EXPERT ? 2 : 3); + townPortalSkillLevel = MasteryLevel(hero->getSpellSchoolLevel(townPortal)); + movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3); } bool actorCanCastTownPortal() @@ -1028,7 +1028,7 @@ struct TowmPortalFinder continue; } - if(townPortalSkillLevel < SecSkillLevel::ADVANCED) + if(townPortalSkillLevel < MasteryLevel::ADVANCED) { const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int { diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index 7dbac9010..241a1d9b6 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -139,7 +139,7 @@ namespace AIPathfinding auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) + && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoats[hero] = std::make_shared(); diff --git a/AI/VCAI/Pathfinding/AINodeStorage.cpp b/AI/VCAI/Pathfinding/AINodeStorage.cpp index eee053ac9..8e7bc4a8b 100644 --- a/AI/VCAI/Pathfinding/AINodeStorage.cpp +++ b/AI/VCAI/Pathfinding/AINodeStorage.cpp @@ -250,7 +250,7 @@ void AINodeStorage::calculateTownPortalTeleportations( return; } - if(skillLevel < SecSkillLevel::ADVANCED) + if(skillLevel < MasteryLevel::ADVANCED) { const CGTownInstance * nearestTown = *vstd::minElementByFun(towns, [&](const CGTownInstance * t) -> int { diff --git a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp index eaf4459a6..9603189fc 100644 --- a/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/VCAI/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -77,7 +77,7 @@ namespace AIPathfinding auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) - && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) + && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoat.reset(new SummonBoatAction()); diff --git a/config/schemas/bonus.json b/config/schemas/bonus.json index e578243e0..959348661 100644 --- a/config/schemas/bonus.json +++ b/config/schemas/bonus.json @@ -44,10 +44,7 @@ "description" : "type" }, "subtype" : { - "anyOf" : [ - { "type" : "string" }, - { "type" : "number" } - ], + "type" : "string", "description" : "subtype" }, "sourceID" : { diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 3e6cfbe97..323f17661 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -294,8 +294,9 @@ Heroes affected by this bonus can not retreat or surrender in battle ### NEGATE_ALL_NATURAL_IMMUNITIES -- subtype: TODO -Orb of Vulnerability +Negates all natural immunities for affected stacks. (Orb of Vulnerability) + +- subtype: 0 - battle-wide immunity negation, 1 - negation only for enemy stacks ### OPENING_BATTLE_SPELL @@ -878,13 +879,14 @@ Affected unit will deal more damage in all attacks (Adela specialty) Affected heroes will be under effect of Disguise spell, hiding some of their information from opposing players -- subtype: spell mastery level +- val: spell mastery level ### VISIONS Affected heroes will be under effect of Visions spell, revealing information of enemy objects in specific range - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range +- subtype: 0 - reveal information on monsters, 1 - reveal information on heroes, 2 - reveal information on towns ### BLOCK_MAGIC_BELOW diff --git a/include/vcmi/FactionMember.h b/include/vcmi/FactionMember.h index d60509f95..c87049ecb 100644 --- a/include/vcmi/FactionMember.h +++ b/include/vcmi/FactionMember.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN class BonusList; -enum class PrimarySkill : int8_t; +class PrimarySkill; class DLL_LINKAGE AFactionMember: public IConstBonusProvider, public INativeTerrainProvider { diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index df3dd4655..b634dfd04 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -145,7 +145,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) { auto ret = new CArtifactInstance(VLC->arth->objects[ArtifactID::SPELL_SCROLL]); auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, - BonusSource::ARTIFACT_INSTANCE, -1, ArtifactID::SPELL_SCROLL, sid); + BonusSource::ARTIFACT_INSTANCE, -1, ArtifactID::SPELL_SCROLL, TBonusSubtype(sid)); ret->addNewBonus(bonus); return ret; } diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index f2edcdf9a..692551944 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -35,7 +35,7 @@ TerrainId AFactionMember::getNativeTerrain() const { constexpr auto any = TerrainId(ETerrainId::ANY_TERRAIN); const std::string cachingStringNoTerrainPenalty = "type_NO_TERRAIN_PENALTY_sANY"; - static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, any); + static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, TBonusSubtype(any)); //this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses //and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties. @@ -54,7 +54,7 @@ int AFactionMember::getAttack(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_ATTACK"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -63,7 +63,7 @@ int AFactionMember::getDefense(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_DEFENSE"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -71,14 +71,14 @@ int AFactionMember::getDefense(bool ranged) const int AFactionMember::getMinDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_1"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } int AFactionMember::getMaxDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_2"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -87,7 +87,7 @@ int AFactionMember::getPrimSkillLevel(PrimarySkill id) const static const CSelector selectorAllSkills = Selector::type()(BonusType::PRIMARY_SKILL); static const std::string keyAllSkills = "type_PRIMARY_SKILL"; auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills); - auto ret = allSkills->valOfBonuses(Selector::subtype()(static_cast(id))); + auto ret = allSkills->valOfBonuses(Selector::subtype()(TBonusSubtype(id))); auto minSkillValue = (id == PrimarySkill::SPELL_POWER || id == PrimarySkill::KNOWLEDGE) ? 1 : 0; return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves } diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 8c178eda0..3b1f5305e 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -68,7 +68,7 @@ SpellID CScrollArtifactInstance::getScrollSpellID() const logMod->warn("Warning: %s doesn't bear any spell!", artInst->nodeName()); return SpellID::NONE; } - return SpellID(bonus->subtype); + return bonus->subtype.as(); } void CGrowingArtifactInstance::growingUp() diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index f583288a4..c4a35052b 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -77,10 +77,10 @@ std::string CBonusTypeHandler::bonusToString(const std::shared_ptr & bonu boost::algorithm::replace_all(text, "${val}", std::to_string(bearer->valOfBonuses(Selector::typeSubtype(bonus->type, bonus->subtype)))); if (text.find("${subtype.creature}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.creature}", CreatureID(bonus->subtype).toCreature()->getNamePluralTranslated()); + boost::algorithm::replace_all(text, "${subtype.creature}", bonus->subtype.as().toCreature()->getNamePluralTranslated()); if (text.find("${subtype.spell}") != std::string::npos) - boost::algorithm::replace_all(text, "${subtype.spell}", SpellID(bonus->subtype).toSpell()->getNameTranslated()); + boost::algorithm::replace_all(text, "${subtype.spell}", bonus->subtype.as().toSpell()->getNameTranslated()); return text; } @@ -95,57 +95,57 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu case BonusType::SPELL_IMMUNITY: { fullPath = true; - const CSpell * sp = SpellID(bonus->subtype).toSpell(); + const CSpell * sp = bonus->subtype.as().toSpell(); fileName = sp->getIconImmune(); break; } case BonusType::SPELL_DAMAGE_REDUCTION: //Spell damage reduction for all schools { - if (bonus->subtype == SpellSchool::ANY.getNum()) + if (bonus->subtype.as() == SpellSchool::ANY) fileName = "E_GOLEM.bmp"; - if (bonus->subtype == SpellSchool::AIR.getNum()) + if (bonus->subtype.as() == SpellSchool::AIR) fileName = "E_LIGHT.bmp"; - if (bonus->subtype == SpellSchool::FIRE.getNum()) + if (bonus->subtype.as() == SpellSchool::FIRE) fileName = "E_FIRE.bmp"; - if (bonus->subtype == SpellSchool::WATER.getNum()) + if (bonus->subtype.as() == SpellSchool::WATER) fileName = "E_COLD.bmp"; - if (bonus->subtype == SpellSchool::EARTH.getNum()) + if (bonus->subtype.as() == SpellSchool::EARTH) fileName = "E_SPEATH1.bmp"; //No separate icon for earth damage break; } case BonusType::SPELL_SCHOOL_IMMUNITY: //for all school { - if (bonus->subtype == SpellSchool::AIR.getNum()) + if (bonus->subtype.as() == SpellSchool::AIR) fileName = "E_SPAIR.bmp"; - if (bonus->subtype == SpellSchool::FIRE.getNum()) + if (bonus->subtype.as() == SpellSchool::FIRE) fileName = "E_SPFIRE.bmp"; - if (bonus->subtype == SpellSchool::WATER.getNum()) + if (bonus->subtype.as() == SpellSchool::WATER) fileName = "E_SPWATER.bmp"; - if (bonus->subtype == SpellSchool::EARTH.getNum()) + if (bonus->subtype.as() == SpellSchool::EARTH) fileName = "E_SPEATH.bmp"; break; } case BonusType::NEGATIVE_EFFECTS_IMMUNITY: { - if (bonus->subtype == SpellSchool::AIR.getNum()) + if (bonus->subtype.as() == SpellSchool::AIR) fileName = "E_SPAIR1.bmp"; - if (bonus->subtype == SpellSchool::FIRE.getNum()) + if (bonus->subtype.as() == SpellSchool::FIRE) fileName = "E_SPFIRE1.bmp"; - if (bonus->subtype == SpellSchool::WATER.getNum()) + if (bonus->subtype.as() == SpellSchool::WATER) fileName = "E_SPWATER1.bmp"; - if (bonus->subtype == SpellSchool::EARTH.getNum()) + if (bonus->subtype.as() == SpellSchool::EARTH) fileName = "E_SPEATH1.bmp"; break; @@ -168,15 +168,12 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu } case BonusType::GENERAL_DAMAGE_REDUCTION: { - switch(bonus->subtype) - { - case 0: + if (bonus->subtype == BonusSubtypes::damageTypeMelee) fileName = "DamageReductionMelee.bmp"; - break; - case 1: + + if (bonus->subtype == BonusSubtypes::damageTypeRanged) fileName = "DamageReductionRanged.bmp"; - break; - } + break; } diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 91061b70d..9c936068c 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -113,25 +113,25 @@ FactionID CCreature::getFaction() const int32_t CCreature::getBaseAttack() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDefense() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMin() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMax() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } @@ -291,7 +291,7 @@ CCreature::CCreature() fightValue = AIValue = growth = hordeGrowth = ammMin = ammMax = 0; } -void CCreature::addBonus(int val, BonusType type, int subtype) +void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) { auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, getIndex())); BonusList & exported = getExportedBonusList(); @@ -345,16 +345,16 @@ void CCreature::updateFrom(const JsonNode & data) addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); if(!configNode["attack"].isNull()) - addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); + addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); if(!configNode["defense"].isNull()) - addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); + addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); if(!configNode["damage"]["min"].isNull()) - addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); + addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin); if(!configNode["damage"]["max"].isNull()) - addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); + addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax); if(!configNode["shots"].isNull()) addBonus(configNode["shots"].Integer(), BonusType::SHOTS); @@ -604,11 +604,11 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); - cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)); - cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)); + cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); + cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, 1); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, 2); + cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin); + cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax); assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); @@ -1025,19 +1025,19 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'A': b.type = BonusType::PRIMARY_SKILL; - b.subtype = static_cast(PrimarySkill::ATTACK); + b.subtype = TBonusSubtype(PrimarySkill::ATTACK); break; case 'D': b.type = BonusType::PRIMARY_SKILL; - b.subtype = static_cast(PrimarySkill::DEFENSE); + b.subtype = TBonusSubtype(PrimarySkill::DEFENSE); break; case 'M': //Max damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 2; + b.subtype = BonusSubtypes::creatureDamageMax; break; case 'm': //Min damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = 1; + b.subtype = BonusSubtypes::creatureDamageMin; break; case 'S': b.type = BonusType::STACKS_SPEED; break; @@ -1051,17 +1051,16 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.type = BonusType::DEFENSIVE_STANCE; break; case 'e': b.type = BonusType::DOUBLE_DAMAGE_CHANCE; - b.subtype = 0; break; case 'E': b.type = BonusType::DEATH_STARE; - b.subtype = 0; //Gorgon + b.subtype = BonusSubtypes::deathStareGorgon; break; case 'F': b.type = BonusType::FEAR; break; case 'g': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); + b.subtype = TBonusSubtype(SpellSchool::ANY); break; case 'P': b.type = BonusType::CASTS; break; @@ -1069,7 +1068,6 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.type = BonusType::ADDITIONAL_RETALIATION; break; case 'W': b.type = BonusType::MAGIC_RESISTANCE; - b.subtype = 0; //otherwise creature window goes crazy break; case 'f': //on-off skill enable = true; //sometimes format is: 2 -> 0, 1 -> 1 @@ -1103,7 +1101,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.type = BonusType::MIND_IMMUNITY; break; case 'r': b.type = BonusType::REBIRTH; //on/off? makes sense? - b.subtype = 0; + b.subtype = BonusSubtypes::rebirthRegular; b.val = 20; //arbitrary value break; case 'R': @@ -1126,42 +1124,42 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars { case 'B': //Blind b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BLIND; + b.subtype = TBonusSubtype(SpellID(SpellID::BLIND)); b.additionalInfo = 0;//normal immunity break; case 'H': //Hypnotize b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::HYPNOTIZE; + b.subtype = TBonusSubtype(SpellID(SpellID::HYPNOTIZE)); b.additionalInfo = 0;//normal immunity break; case 'I': //Implosion b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::IMPLOSION; + b.subtype = TBonusSubtype(SpellID(SpellID::IMPLOSION)); b.additionalInfo = 0;//normal immunity break; case 'K': //Berserk b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::BERSERK; + b.subtype = TBonusSubtype(SpellID(SpellID::BERSERK)); b.additionalInfo = 0;//normal immunity break; case 'M': //Meteor Shower b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::METEOR_SHOWER; + b.subtype = TBonusSubtype(SpellID(SpellID::METEOR_SHOWER)); b.additionalInfo = 0;//normal immunity break; case 'N': //dispell beneficial spells b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::DISPEL_HELPFUL_SPELLS; + b.subtype = TBonusSubtype(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); b.additionalInfo = 0;//normal immunity break; case 'R': //Armageddon b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::ARMAGEDDON; + b.subtype = TBonusSubtype(SpellID(SpellID::ARMAGEDDON)); b.additionalInfo = 0;//normal immunity break; case 'S': //Slow b.type = BonusType::SPELL_IMMUNITY; - b.subtype = SpellID::SLOW; + b.subtype = TBonusSubtype(SpellID(SpellID::SLOW)); b.additionalInfo = 0;//normal immunity break; case '6': @@ -1177,51 +1175,51 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'F': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::FIRE); + b.subtype = TBonusSubtype(SpellSchool::FIRE); break; case 'O': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::FIRE); + b.subtype = TBonusSubtype(SpellSchool::FIRE); b.val = 100; //Full damage immunity break; case 'f': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::FIRE); + b.subtype = TBonusSubtype(SpellSchool::FIRE); break; case 'C': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::WATER); + b.subtype = TBonusSubtype(SpellSchool::WATER); break; case 'W': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::WATER); + b.subtype = TBonusSubtype(SpellSchool::WATER); b.val = 100; //Full damage immunity break; case 'w': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::WATER); + b.subtype = TBonusSubtype(SpellSchool::WATER); break; case 'E': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::EARTH); + b.subtype = TBonusSubtype(SpellSchool::EARTH); b.val = 100; //Full damage immunity break; case 'e': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::EARTH); + b.subtype = TBonusSubtype(SpellSchool::EARTH); break; case 'A': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::AIR); + b.subtype = TBonusSubtype(SpellSchool::AIR); b.val = 100; //Full damage immunity break; case 'a': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = SpellSchool(ESpellSchool::AIR); + b.subtype = TBonusSubtype(SpellSchool::AIR); break; case 'D': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = SpellSchool(ESpellSchool::ANY); + b.subtype = TBonusSubtype(SpellSchool::ANY); b.val = 100; //Full damage immunity break; case '0': @@ -1250,16 +1248,16 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars case 'K': case 'k': b.type = BonusType::SPELL_AFTER_ATTACK; - b.subtype = stringToNumber(mod); + b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); break; case 'h': b.type = BonusType::HATE; - b.subtype = stringToNumber(mod); + b.subtype = TBonusSubtype(CreatureID(stringToNumber(mod))); break; case 'p': case 'J': b.type = BonusType::SPELL_BEFORE_ATTACK; - b.subtype = stringToNumber(mod); + b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); b.additionalInfo = 3; //always expert? break; case 'r': @@ -1268,7 +1266,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 's': b.type = BonusType::ENCHANTED; - b.subtype = stringToNumber(mod); + b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); b.valType = BonusValueType::INDEPENDENT_MAX; break; default: diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index 654fe9d22..b1d6b964c 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -194,7 +194,8 @@ public: bool valid() const; - void addBonus(int val, BonusType type, int subtype = -1); + void addBonus(int val, BonusType type); + void addBonus(int val, BonusType type, TBonusSubtype subtype); std::string nodeName() const override; template diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 2be582779..e0f290ef9 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -268,7 +268,7 @@ bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - detailed = selectedHero->hasVisions(town, 1); + detailed = selectedHero->hasVisions(town, BonusSubtypes::visionsTowns); } dest.initFromTown(dynamic_cast(town), detailed); @@ -322,7 +322,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - if(selectedHero->hasVisions(hero, 1)) + if(selectedHero->hasVisions(hero, BonusSubtypes::visionsHeroes)) infoLevel = InfoAboutHero::EInfoLevel::DETAILED; } @@ -332,7 +332,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero if(getPlayerRelations(*getPlayerID(), hero->tempOwner) == PlayerRelations::ENEMIES) { //todo: bonus cashing - int disguiseLevel = h->valOfBonuses(Selector::typeSubtype(BonusType::DISGUISED, 0)); + int disguiseLevel = h->valOfBonuses(BonusType::DISGUISED); auto doBasicDisguise = [](InfoAboutHero & info) { diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index e76241e2e..725979c6b 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -480,7 +480,7 @@ void CHeroHandler::loadHeroSkills(CHero * hero, const JsonNode & node) const for(const JsonNode &set : node["skills"].Vector()) { int skillLevel = static_cast(boost::range::find(NSecondarySkill::levels, set["level"].String()) - std::begin(NSecondarySkill::levels)); - if (skillLevel < SecSkillLevel::LEVELS_SIZE) + if (skillLevel < MasteryLevel::LEVELS_SIZE) { size_t currentIndex = hero->secSkillsInit.size(); hero->secSkillsInit.emplace_back(SecondarySkill(-1), skillLevel); @@ -547,7 +547,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = static_cast(PrimarySkill::ATTACK); + bonus->subtype = TBonusSubtype(PrimarySkill::ATTACK); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); @@ -557,7 +557,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = static_cast(PrimarySkill::DEFENSE); + bonus->subtype = TBonusSubtype(PrimarySkill::DEFENSE); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); diff --git a/lib/CStack.cpp b/lib/CStack.cpp index ed18eb3a2..3eb7d91d2 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -220,7 +220,7 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const resurrectedCount += 1; } - if(customState->hasBonusOfType(BonusType::REBIRTH, 1)) + if(customState->hasBonusOfType(BonusType::REBIRTH, BonusSubtypes::rebirthSpecial)) { // resurrect at least one Sacred Phoenix vstd::amax(resurrectedCount, 1); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index b5d2fcec9..38f43edd6 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -527,16 +527,16 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const b = createBonus(building, BonusType::LUCK, +2); break; case BuildingSubID::SPELL_POWER_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::SPELL_POWER)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::SPELL_POWER)); break; case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::ATTACK)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::ATTACK)); break; case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, static_cast(PrimarySkill::DEFENSE)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::DEFENSE)); break; case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, playerPropagator, 0); + b = createBonus(building, BonusType::MOVEMENT, +500, BonusSubtypes::heroMovementSea, playerPropagator); break; } @@ -544,12 +544,12 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const building->addNewBonus(b, building->buildingBonuses); } -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, int subtype) const +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const { - return createBonus(build, type, val, emptyPropagator(), subtype); + return createBonus(build, type, val, subtype, emptyPropagator()); } -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype) const +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype, TPropagatorPtr & prop) const { std::ostringstream descr; descr << build->getNameTranslated(); @@ -561,9 +561,9 @@ std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building int val, TPropagatorPtr & prop, const std::string & description, - int subtype) const + TBonusSubtype subtype) const { - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, description, subtype); + auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, subtype, description); if(prop) b->addPropagator(prop); diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 01c8c5dbe..a781aaa5e 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -392,14 +392,15 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase createBonus(CBuilding * build, BonusType type, int val, int subtype = -1) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TPropagatorPtr & prop, int subtype = -1) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype, TPropagatorPtr & prop) const; std::shared_ptr createBonusImpl(const BuildingID & building, BonusType type, int val, TPropagatorPtr & prop, const std::string & description, - int subtype = -1) const; + TBonusSubtype subtype) const; /// loads CStructure's into town void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 249b36060..26f56bb6a 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -417,13 +417,31 @@ std::string JsonNode::toJson(bool compact) const ///JsonUtils -void JsonUtils::parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest) +static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const JsonNode & node) { - dest->val = static_cast(source[1].Float()); - resolveIdentifier(source[2],dest->subtype); - dest->additionalInfo = static_cast(source[3].Float()); - dest->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) - dest->turnsRemain = 0; + if (node.isNull()) + { + subtype = TBonusSubtype::NONE; + return; + } + + if (!node.isString()) + { + logMod->warn("Bonus subtype must be string!"); + subtype = TBonusSubtype::NONE; + return; + } + + VLC->identifiers()->requestIdentifier(node, [&subtype, node](int32_t identifier) + { + assert(0); //TODO + subtype = TBonusSubtype("type", node.String(), identifier); + }); +} + +static void loadBonusSourceInstance(int32_t & sourceInstance, BonusSource sourceType, const JsonNode & node) +{ + } std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) @@ -438,7 +456,11 @@ std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) } b->type = it->second; - parseTypedBonusShort(ability_vec, b); + b->val = static_cast(ability_vec[1].Float()); + loadBonusSubtype(b->subtype, b->type, ability_vec[2]); + b->additionalInfo = static_cast(ability_vec[3].Float()); + b->duration = BonusDuration::PERMANENT; //TODO: handle flags (as integer) + b->turnsRemain = 0; return b; } @@ -473,31 +495,6 @@ const T parseByMapN(const std::map & map, const JsonNode * val, return parseByMap(map, val, err); } -void JsonUtils::resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name) -{ - const JsonNode &value = node[name]; - if (!value.isNull()) - { - switch (value.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(value.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(value.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(value, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for value of %s.", name); - } - } -} - void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) { const JsonNode & value = node["addInfo"]; @@ -549,27 +546,6 @@ void JsonUtils::resolveAddInfo(CAddInfo & var, const JsonNode & node) } } -void JsonUtils::resolveIdentifier(const JsonNode &node, si32 &var) -{ - switch (node.getType()) - { - case JsonNode::JsonType::DATA_INTEGER: - var = static_cast(node.Integer()); - break; - case JsonNode::JsonType::DATA_FLOAT: - var = static_cast(node.Float()); - break; - case JsonNode::JsonType::DATA_STRING: - VLC->identifiers()->requestIdentifier(node, [&](si32 identifier) - { - var = identifier; - }); - break; - default: - logMod->error("Error! Wrong identifier used for identifier!"); - } -} - std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) { switch(limiter.getType()) @@ -660,7 +636,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) bonusLimiter->source = sourceIt->second; bonusLimiter->isSourceRelevant = true; if(!parameter["id"].isNull()) { - resolveIdentifier(parameter["id"], bonusLimiter->sid); + loadBonusSourceInstance(bonusLimiter->sid, bonusLimiter->source, parameter["id"]); bonusLimiter->isSourceIDRelevant = true; } } @@ -673,7 +649,7 @@ std::shared_ptr JsonUtils::parseLimiter(const JsonNode & limiter) return bonusLimiter; else { - resolveIdentifier(parameters[1], bonusLimiter->subtype); + loadBonusSubtype(bonusLimiter->subtype, bonusLimiter->type, parameters[1]); bonusLimiter->isSubtypeRelevant = true; if(parameters.size() > 2) findSource(parameters[2]); @@ -765,7 +741,7 @@ std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, c source = BonusSource::TOWN_STRUCTURE bonusType, val, subtype - get from json */ - auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description, -1); + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description); if(!parseBonus(ability, b.get())) return nullptr; @@ -865,7 +841,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) else b->type = it->second; - resolveIdentifier(b->subtype, params->isConverted ? params->toJson() : ability, "subtype"); + loadBonusSubtype(b->subtype, b->type, params->isConverted ? params->toJson() : ability); if(!params->isConverted) { @@ -996,7 +972,8 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) if(!value->isNull()) { TBonusSubtype subtype; - resolveIdentifier(subtype, ability, "subtype"); + assert(0); //TODO + loadBonusSubtype(subtype, BonusType::NONE, ability); ret = ret.And(Selector::subtype()(subtype)); } value = &ability["sourceType"]; @@ -1010,10 +987,9 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) } value = &ability["sourceID"]; - if(!value->isNull()) + if(!value->isNull() && src.has_value()) { - id = -1; - resolveIdentifier(*id, ability, "sourceID"); + loadBonusSourceInstance(*id, *src, ability); } if(src && id) diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 1e463aeb6..0a3d90178 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -127,21 +127,12 @@ public: namespace JsonUtils { - /** - * @brief parse short bonus format, excluding type - * @note sets duration to Permament - */ - DLL_LINKAGE void parseTypedBonusShort(const JsonVector & source, const std::shared_ptr & dest); - - /// DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description); DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); - DLL_LINKAGE void resolveIdentifier(si32 & var, const JsonNode & node, const std::string & name); - DLL_LINKAGE void resolveIdentifier(const JsonNode & node, si32 & var); DLL_LINKAGE void resolveAddInfo(CAddInfo & var, const JsonNode & node); /** diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 89cdf6cbe..368bfbcc5 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -442,9 +442,9 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const //native terrain bonuses static auto nativeTerrain = std::make_shared(); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0, 0)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, static_cast(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0)->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index b8876b946..5dac8edc8 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1755,7 +1755,7 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const return SpellID::NONE; if(bl->size() == 1) - return SpellID(bl->front()->subtype); + return bl->front()->subtype.as(); int totalWeight = 0; for(const auto & b : *bl) @@ -1772,7 +1772,7 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(CRandomGenerator & rand,const randomPos -= std::max(b->additionalInfo[0], 0); if(randomPos < 0) { - return SpellID(b->subtype); + return b->subtype.as(); } } diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 098b4e746..5768c834a 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -340,10 +340,10 @@ CUnitState::CUnitState(): health(this), shots(this), totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), - minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 1)), 0), - maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 0).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, 2)), 0), - attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)), 0), - defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)), 0), + minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin)), 0), + maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax)), 0), + attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)), 0), + defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, SpellID::CLONE))), cloneID(-1) @@ -430,7 +430,7 @@ const CGHeroInstance * CUnitState::getHeroCaster() const int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const { - int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, spell->getIndex())); + int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, TBonusSubtype(spell->getId()))); vstd::abetween(skill, 0, 3); return skill; } @@ -466,7 +466,7 @@ int32_t CUnitState::getEnchantPower(const spells::Spell * spell) const int64_t CUnitState::getEffectValue(const spells::Spell * spell) const { - return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, spell->getIndex()); + return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, TBonusSubtype(spell->getId())); } PlayerColor CUnitState::getCasterOwner() const diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index b9c7cedb7..97489e7ab 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -52,7 +52,7 @@ DamageRange DamageCalculator::getBaseDamageSingle() const { auto retrieveHeroPrimSkill = [&](PrimarySkill skill) -> int { - std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(skill)))); + std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(skill)))); return b ? b->val : 0; }; @@ -142,8 +142,9 @@ int DamageCalculator::getActorAttackSlayer() const if(isAffected) { - int attackBonus = SpellID(SpellID::SLAYER).toSpell()->getLevelPower(spLevel); - if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, SpellID::SLAYER)) + SpellID spell(SpellID::SLAYER); + int attackBonus = spell.toSpell()->getLevelPower(spLevel); + if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, TBonusSubtype(spell))) { ui8 attackerTier = info.attacker->unitType()->getLevel(); ui8 specialtyBonus = std::max(5 - attackerTier, 0); @@ -205,11 +206,11 @@ double DamageCalculator::getAttackOffenseArcheryFactor() const if(info.shooting) { const std::string cachingStrArchery = "type_PERCENTAGE_DAMAGE_BOOSTs_1"; - static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, 1); + static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypes::damageTypeRanged); return info.attacker->valOfBonuses(selectorArchery, cachingStrArchery) / 100.0; } const std::string cachingStrOffence = "type_PERCENTAGE_DAMAGE_BOOSTs_0"; - static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, 0); + static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypes::damageTypeMelee); return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0; } @@ -231,7 +232,7 @@ double DamageCalculator::getAttackDoubleDamageFactor() const { if(info.doubleDamage) { const auto cachingStr = "type_BONUS_DAMAGE_PERCENTAGEs_" + std::to_string(info.attacker->creatureIndex()); - const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, info.attacker->creatureIndex()); + const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, TBonusSubtype(info.attacker->creatureId())); return info.attacker->valOfBonuses(selector, cachingStr) / 100.0; } return 0.0; @@ -259,7 +260,7 @@ double DamageCalculator::getAttackHateFactor() const auto allHateEffects = info.attacker->getBonuses(selectorHate, cachingStrHate); - return allHateEffects->valOfBonuses(Selector::subtype()(info.defender->creatureIndex())) / 100.0; + return allHateEffects->valOfBonuses(Selector::subtype()(TBonusSubtype(info.defender->creatureId()))) / 100.0; } double DamageCalculator::getDefenseSkillFactor() const @@ -281,7 +282,7 @@ double DamageCalculator::getDefenseSkillFactor() const double DamageCalculator::getDefenseArmorerFactor() const { const std::string cachingStrArmorer = "type_GENERAL_DAMAGE_REDUCTIONs_N1_NsrcSPELL_EFFECT"; - static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); + static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); return info.defender->valOfBonuses(selectorArmorer, cachingStrArmorer) / 100.0; } @@ -289,10 +290,10 @@ double DamageCalculator::getDefenseArmorerFactor() const double DamageCalculator::getDefenseMagicShieldFactor() const { const std::string cachingStrMeleeReduction = "type_GENERAL_DAMAGE_REDUCTIONs_0"; - static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, 0); + static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeMelee); const std::string cachingStrRangedReduction = "type_GENERAL_DAMAGE_REDUCTIONs_1"; - static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, 1); + static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeRanged); //handling spell effects - shield and air shield if(info.shooting) @@ -313,7 +314,7 @@ double DamageCalculator::getDefenseRangePenaltiesFactor() const { return bonus->source == BonusSource::SPELL_EFFECT && bonus->sid == SpellID::AIR_SHIELD - && bonus->val >= SecSkillLevel::ADVANCED; + && bonus->val >= MasteryLevel::ADVANCED; }; const bool distPenalty = callback.battleHasDistancePenalty(info.attacker, attackerPos, defenderPos); @@ -386,7 +387,7 @@ double DamageCalculator::getDefensePetrificationFactor() const { // Creatures that are petrified by a Basilisk's Petrifying attack or a Medusa's Stone gaze take 50% damage (R8 = 0.50) from ranged and melee attacks. Taking damage also deactivates the effect. const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1_srcSPELL_EFFECT"; - static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, -1).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); + static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); return info.defender->valOfBonuses(selectorAllReduction, cachingStrAllReduction) / 100.0; } diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 6ae6bacfc..bf15aca05 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -134,29 +134,6 @@ std::string Bonus::Description(std::optional customValue) const return str.str(); } -static JsonNode subtypeToJson(BonusType type, int subtype) -{ - switch(type) - { - case BonusType::PRIMARY_SKILL: - return JsonUtils::stringNode("primSkill." + NPrimarySkill::names[subtype]); - case BonusType::SPECIAL_SPELL_LEV: - case BonusType::SPECIFIC_SPELL_DAMAGE: - case BonusType::SPELL: - case BonusType::SPECIAL_PECULIAR_ENCHANT: - case BonusType::SPECIAL_ADD_VALUE_ENCHANT: - case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "spell", SpellID::encode(subtype))); - case BonusType::IMPROVED_NECROMANCY: - case BonusType::SPECIAL_UPGRADE: - return JsonUtils::stringNode(ModUtility::makeFullIdentifier("", "creature", CreatureID::encode(subtype))); - case BonusType::GENERATE_RESOURCE: - return JsonUtils::stringNode("resource." + GameConstants::RESOURCE_NAMES[subtype]); - default: - return JsonUtils::intNode(subtype); - } -} - static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) { switch(type) @@ -173,8 +150,8 @@ JsonNode Bonus::toJsonNode() const JsonNode root(JsonNode::JsonType::DATA_STRUCT); // only add values that might reasonably be found in config files root["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtype != -1) - root["subtype"] = subtypeToJson(type, subtype); + if(subtype != TBonusSubtype::NONE) + root["subtype"].String() = subtype.toString(); if(additionalInfo != CAddInfo::NONE) root["addInfo"] = additionalInfoToJson(type, additionalInfo); if(source != BonusSource::OTHER) @@ -206,7 +183,7 @@ JsonNode Bonus::toJsonNode() const return root; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype, std::string Desc): duration(Duration), type(Type), subtype(Subtype), @@ -219,7 +196,7 @@ Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 targetSourceType = BonusSource::OTHER; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype, BonusValueType ValType): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype, BonusValueType ValType): duration(Duration), type(Type), subtype(Subtype), @@ -247,7 +224,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) #define printField(field) out << "\t" #field ": " << (int)bonus.field << "\n" printField(val); - printField(subtype); + out << "\tSubtype: " << bonus.subtype.toString() << "\n"; printField(duration.to_ulong()); printField(source); printField(sid); diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 20bc16f3d..43c0a888f 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -10,6 +10,7 @@ #pragma once #include "BonusEnum.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -22,13 +23,48 @@ class IUpdater; class BonusList; class CSelector; -using TBonusSubtype = int32_t; +using TBonusSubtype = MetaIdentifier; using TBonusListPtr = std::shared_ptr; using TConstBonusListPtr = std::shared_ptr; using TLimiterPtr = std::shared_ptr; using TPropagatorPtr = std::shared_ptr; using TUpdaterPtr = std::shared_ptr; +namespace BonusSubtypes +{ + +static const TBonusSubtype creatureDamageBoth; // 0 +static const TBonusSubtype creatureDamageMin; // 1 +static const TBonusSubtype creatureDamageMax; // 2 + +static const TBonusSubtype damageTypeAll; // -1 +static const TBonusSubtype damageTypeMelee; // 0 +static const TBonusSubtype damageTypeRanged; // 1 + +static const TBonusSubtype heroMovementLand; // 1 +static const TBonusSubtype heroMovementSea; // 0 + +static const TBonusSubtype heroMovementPenalty; // 2 +static const TBonusSubtype heroMovementFull; // 1 + +static const TBonusSubtype deathStareGorgon; // 0 +static const TBonusSubtype deathStareCommander; + +static const TBonusSubtype rebirthRegular; // 0 +static const TBonusSubtype rebirthSpecial; // 1 + +static const TBonusSubtype visionsMonsters; // 0 +static const TBonusSubtype visionsHeroes; // 1 +static const TBonusSubtype visionsTowns; // 2 + +static const TBonusSubtype immunityBattleWide; // 0 +static const TBonusSubtype immunityEnemyHero; // 1 + +TBonusSubtype spellLevel(int level); +TBonusSubtype creatureLevel(int level); + +} + class DLL_LINKAGE CAddInfo : public std::vector { public: @@ -56,7 +92,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this si16 turnsRemain = 0; //used if duration is N_TURNS, N_DAYS or ONE_WEEK BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte - TBonusSubtype subtype = -1; //-1 if not applicable - 4 bytes + TBonusSubtype subtype; BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. @@ -75,8 +111,11 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this std::string description; - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc, si32 Subtype=-1); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, si32 Subtype=-1, BonusValueType ValType = BonusValueType::ADDITIVE_VALUE); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype, BonusValueType ValType); Bonus() = default; template void serialize(Handler &h, const int version) diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index fe2daa6c6..ad109f79f 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -81,76 +81,76 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::ANY); + subtype = TBonusSubtype(ESpellSchool::ANY); } else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar") type = BonusType::LEARN_MEETING_SPELL_LIMIT; else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery") { - subtype = 1; + subtype = BonusSubtypes::damageTypeRanged; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence") { - subtype = 0; + subtype = BonusSubtypes::damageTypeMelee; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer") { - subtype = -1; + subtype = BonusSubtypes::damageTypeAll; type = BonusType::GENERAL_DAMAGE_REDUCTION; } else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation") { - subtype = 0; + subtype = BonusSubtypes::heroMovementSea; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics") { - subtype = 1; + subtype = BonusSubtypes::heroMovementLand; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::ESTATES || deprecatedSubtypeStr == "skill.estates") { type = BonusType::GENERATE_RESOURCE; - subtype = GameResID(EGameResID::GOLD); + subtype = TBonusSubtype(GameResID(EGameResID::GOLD)); } else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = TBonusSubtype(ESpellSchool::AIR); } else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = TBonusSubtype(ESpellSchool::WATER); } else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = TBonusSubtype(ESpellSchool::FIRE); } else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = TBonusSubtype(ESpellSchool::EARTH); } else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::BONUS_DAMAGE_CHANCE; - subtypeStr = "core:creature.ballista"; + subtype = TBonusSubtype(CreatureID(CreatureID::BALLISTA)); } else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid") { type = BonusType::SPECIFIC_SPELL_POWER; - subtypeStr = "core:spell.firstAid"; + subtype = TBonusSubtype("spell", "firstAid"); } else if (deprecatedSubtype == SecondarySkill::BALLISTICS || deprecatedSubtypeStr == "skill.ballistics") { type = BonusType::CATAPULT_EXTRA_SHOTS; - subtypeStr = "core:spell.catapultShot"; + subtype = TBonusSubtype("spell", "catapultShot"); } else isConverted = false; @@ -162,27 +162,27 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::HERO_GRANTS_ATTACKS; - subtypeStr = "core:creature.ballista"; + subtype = TBonusSubtype(CreatureID(CreatureID::BALLISTA)); } else isConverted = false; } else if (deprecatedTypeStr == "SEA_MOVEMENT") { - subtype = 0; + subtype = BonusSubtypes::heroMovementSea; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "LAND_MOVEMENT") { - subtype = 1; + subtype = BonusSubtypes::heroMovementLand; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "MAXED_SPELL") { type = BonusType::SPELL; - subtypeStr = deprecatedSubtypeStr; + subtype = TBonusSubtype("spell", deprecatedSubtypeStr); valueType = BonusValueType::INDEPENDENT_MAX; val = 3; } @@ -223,52 +223,52 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedTypeStr == "DIRECT_DAMAGE_IMMUNITY") { type = BonusType::SPELL_DAMAGE_REDUCTION; - subtype = SpellSchool(ESpellSchool::ANY); + subtype = MetaIdentifier(SpellSchool::ANY); val = 100; } else if (deprecatedTypeStr == "AIR_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = MetaIdentifier(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = MetaIdentifier(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = MetaIdentifier(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = MetaIdentifier(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::AIR); + subtype = MetaIdentifier(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = MetaIdentifier(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::WATER); + subtype = MetaIdentifier(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = MetaIdentifier(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_IMMUNITY") { - subtype = SpellSchool(ESpellSchool::AIR); + subtype = MetaIdentifier(SpellSchool::AIR); switch(deprecatedSubtype) { case 0: @@ -284,7 +284,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "FIRE_IMMUNITY") { - subtype = SpellSchool(ESpellSchool::FIRE); + subtype = MetaIdentifier(SpellSchool::FIRE); switch(deprecatedSubtype) { case 0: @@ -300,7 +300,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "WATER_IMMUNITY") { - subtype = SpellSchool(ESpellSchool::WATER); + subtype = MetaIdentifier(SpellSchool::WATER); switch(deprecatedSubtype) { case 0: @@ -316,7 +316,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "EARTH_IMMUNITY") { - subtype = SpellSchool(ESpellSchool::EARTH); + subtype = MetaIdentifier(SpellSchool::EARTH); switch(deprecatedSubtype) { case 0: @@ -340,10 +340,8 @@ const JsonNode & BonusParams::toJson() if(ret.isNull()) { ret["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtypeStr) - ret["subtype"].String() = *subtypeStr; - else if(subtype) - ret["subtype"].Integer() = *subtype; + if(subtype) + ret["subtype"].String() = subtype->toString(); if(valueType) ret["valueType"].String() = vstd::findKey(bonusValueMap, *valueType); if(val) @@ -358,11 +356,6 @@ const JsonNode & BonusParams::toJson() CSelector BonusParams::toSelector() { assert(isConverted); - if(subtypeStr) - { - subtype = -1; - JsonUtils::resolveIdentifier(*subtype, toJson(), "subtype"); - } auto ret = Selector::type()(type); if(subtype) @@ -374,4 +367,4 @@ CSelector BonusParams::toSelector() return ret; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusParams.h b/lib/bonuses/BonusParams.h index 31e86b38d..4aa7bfbf6 100644 --- a/lib/bonuses/BonusParams.h +++ b/lib/bonuses/BonusParams.h @@ -20,7 +20,6 @@ struct DLL_LINKAGE BonusParams { bool isConverted; BonusType type = BonusType::NONE; std::optional subtype = std::nullopt; - std::optional subtypeStr = std::nullopt; std::optional valueType = std::nullopt; std::optional val = std::nullopt; std::optional targetType = std::nullopt; @@ -36,4 +35,4 @@ private: extern DLL_LINKAGE const std::set deprecatedBonusSet; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 219d730bd..33fccaf67 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -62,20 +62,20 @@ bool IBonusBearer::hasBonusOfType(BonusType type) const return hasBonus(s, cachingStr); } -int IBonusBearer::valOfBonuses(BonusType type, int subtype) const +int IBonusBearer::valOfBonuses(BonusType type, TBonusSubtype subtype) const { //This part is performance-critical - std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + std::to_string(subtype); + std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); CSelector s = Selector::typeSubtype(type, subtype); return valOfBonuses(s, cachingStr); } -bool IBonusBearer::hasBonusOfType(BonusType type, int subtype) const +bool IBonusBearer::hasBonusOfType(BonusType type, TBonusSubtype subtype) const { //This part is performance-critical - std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + std::to_string(subtype); + std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); CSelector s = Selector::typeSubtype(type, subtype); @@ -95,4 +95,4 @@ std::shared_ptr IBonusBearer::getBonus(const CSelector &selector) c return bonuses->getFirst(Selector::all); } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index 242c66f83..98bd4a890 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -35,11 +35,11 @@ public: //Optimized interface (with auto-caching) int valOfBonuses(BonusType type) const; //subtype -> subtype of bonus; bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype) - int valOfBonuses(BonusType type, int subtype) const; //subtype -> subtype of bonus; - bool hasBonusOfType(BonusType type, int subtype) const;//determines if hero has a bonus of given type (and optionally subtype) + int valOfBonuses(BonusType type, TBonusSubtype subtype) const; //subtype -> subtype of bonus; + bool hasBonusOfType(BonusType type, TBonusSubtype subtype) const;//determines if hero has a bonus of given type (and optionally subtype) bool hasBonusFrom(BonusSource source, ui32 sourceID) const; virtual int64_t getTreeVersion() const = 0; }; -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index 7916edb42..6a543a2f6 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -136,7 +136,7 @@ JsonNode CCreatureTypeLimiter::toJsonNode() const } HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus ) - : type(bonus), subtype(0), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false) + : type(bonus), isSubtypeRelevant(false), isSourceRelevant(false), isSourceIDRelevant(false) { } @@ -184,8 +184,8 @@ std::string HasAnotherBonusLimiter::toString() const std::string typeName = vstd::findKey(bonusNameMap, type); if(isSubtypeRelevant) { - boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%d)"); - fmt % typeName % subtype; + boost::format fmt("HasAnotherBonusLimiter(type=%s, subtype=%s)"); + fmt % typeName % subtype.toString(); return fmt.str(); } else @@ -205,7 +205,7 @@ JsonNode HasAnotherBonusLimiter::toJsonNode() const root["type"].String() = "HAS_ANOTHER_BONUS_LIMITER"; root["parameters"].Vector().push_back(JsonUtils::stringNode(typeName)); if(isSubtypeRelevant) - root["parameters"].Vector().push_back(JsonUtils::intNode(subtype)); + root["parameters"].Vector().push_back(JsonUtils::stringNode(subtype.toString())); if(isSourceRelevant) root["parameters"].Vector().push_back(JsonUtils::stringNode(sourceTypeName)); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index f48be1217..23a3cf334 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -349,6 +349,27 @@ public: static std::string entityType(); }; +class DLL_LINKAGE PrimarySkill : public Identifier +{ +public: + using Identifier::Identifier; + + static const PrimarySkill NONE; + static const PrimarySkill ATTACK; + static const PrimarySkill DEFENSE; + static const PrimarySkill SPELL_POWER; + static const PrimarySkill KNOWLEDGE; + + static const PrimarySkill BEGIN; + static const PrimarySkill END; + + static const PrimarySkill EXPERIENCE; + + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); + static std::string entityType(); +}; + class DLL_LINKAGE FactionID : public Identifier { public: @@ -919,6 +940,10 @@ public: static const SpellSchool FIRE; static const SpellSchool WATER; static const SpellSchool EARTH; + + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); }; class GameResIDBase : public IdentifierBase @@ -946,6 +971,8 @@ class GameResID : public IdentifierWithEnum public: using IdentifierWithEnum::IdentifierWithEnum; + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; @@ -958,4 +985,85 @@ using River = RiverId; using Road = RoadId; using ETerrainId = TerrainId; +/// This class represents field that may contain value of multiple different identifer types +class MetaIdentifier +{ + std::string entityType; + std::string stringForm; + int32_t integerForm; + + void onDeserialized(); +public: + + static const MetaIdentifier NONE; + + MetaIdentifier(): + integerForm(-1) + {} + + explicit MetaIdentifier(const std::string & entityType, const std::string & identifier) + : entityType(entityType) + , stringForm(identifier) + , integerForm(-1) + { + onDeserialized(); + } + + explicit MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value) + : entityType(entityType) + , stringForm(identifier) + , integerForm(value) + { + } + + template + explicit MetaIdentifier(const IdentifierType & identifier ) + : entityType(IdentifierType::entityType()) + , integerForm(identifier.getNum()) + , stringForm(IdentifierType::encode(identifier.getNum())) + { + static_assert(std::is_base_of::value, "MetaIdentifier can only be constructed from Identifer class"); + } + + int32_t getNum() const + { + return integerForm; + } + + std::string toString() const + { + return stringForm; + } + + template + IdentifierType as() const + { + IdentifierType result(integerForm); + return result; + } + + template void serialize(Handler &h, const int version) + { + h & stringForm; + + if (!h.saving) + onDeserialized(); + } + + bool operator == (const MetaIdentifier & other) const + { + assert( (stringForm == other.stringForm) ? (integerForm == other.integerForm) : true ); + + return stringForm == other.stringForm; + } + + bool operator != (const MetaIdentifier & other) const + { + return !(*this == other); + } + + bool operator < (const MetaIdentifier & other) const; +}; + + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index c594cc271..c65f0cb75 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -11,16 +11,6 @@ VCMI_LIB_NAMESPACE_BEGIN -enum class PrimarySkill : int8_t -{ - NONE = -1, - ATTACK, - DEFENSE, - SPELL_POWER, - KNOWLEDGE, - EXPERIENCE = 4 //for some reason changePrimSkill uses it -}; - enum class EAlignment : int8_t { GOOD, @@ -143,9 +133,9 @@ enum class ETeleportChannelType : int8_t MIXED }; -namespace SecSkillLevel +namespace MasteryLevel { - enum SecSkillLevel + enum Type { NONE, BASIC, diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 6fc187834..ed7a94105 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1871,7 +1871,7 @@ struct statsHLP //Heroes can produce gold as well - skill, specialty or arts for(const auto & h : ps->heroes) { - totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, GameResID(EGameResID::GOLD))); + totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, TBonusSubtype(GameResID(GameResID::GOLD)))); if(!heroOrTown) heroOrTown = h; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index f69f1f876..a338cf61b 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -84,10 +84,10 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorgetBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g]; @@ -118,7 +118,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartifactsWorn; - for (auto const & art : artifactsWorn) + for(const auto & art : artifactsWorn) checkAndRemoveArtifact(art.first); // process in reverse - removal of artifact will shift all artifacts after this one @@ -308,16 +308,14 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) case CampaignBonusType::PRIMARY_SKILL: { const ui8 * ptr = reinterpret_cast(&curBonus->info2); - for(int g = 0; g < GameConstants::PRIMARY_SKILLS; ++g) + for(auto g = PrimarySkill::BEGIN; g < PrimarySkill::END; ++g) { - int val = ptr[g]; + int val = ptr[g.getNum()]; if(val == 0) - { continue; - } - auto bb = std::make_shared( - BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, static_cast(*gameState->scenarioOps->campState->currentScenario()), g - ); + + int currentScenario = static_cast(*gameState->scenarioOps->campState->currentScenario()); + auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, currentScenario, TBonusSubtype(g) ); hero->addNewBonus(bb); } break; @@ -386,9 +384,9 @@ std::vector CGameStateCampaign::generateCampaignHeroesT } //selecting heroes by type - for (auto const * placeholder : placeholdersByType) + for(const auto * placeholder : placeholdersByType) { - auto const & node = campaignState->getHeroByType(*placeholder->heroType); + const auto & node = campaignState->getHeroByType(*placeholder->heroType); if (node.isNull()) { logGlobal->info("Hero crossover: Unable to replace placeholder for %d (%s)!", placeholder->heroType->getNum(), VLC->heroTypes()->getById(*placeholder->heroType)->getNameTranslated()); @@ -412,10 +410,10 @@ std::vector CGameStateCampaign::generateCampaignHeroesT return *a->powerRank > *b->powerRank; }); - auto const & nodeList = campaignState->getHeroesByPower(lastScenario.value()); + const auto & nodeList = campaignState->getHeroesByPower(lastScenario.value()); auto nodeListIter = nodeList.begin(); - for (auto const * placeholder : placeholdersByPower) + for(const auto * placeholder : placeholdersByPower) { if (nodeListIter == nodeList.end()) break; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 21ddb93f6..b60d28207 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -46,7 +46,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { std::string hoverName; - if(hero->hasVisions(this, 0)) + if(hero->hasVisions(this, BonusSubtypes::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 0d53467ff..8461335d6 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -95,7 +95,7 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain } else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus - !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, from.terType->getIndex())) //no special movement bonus + !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, TBonusSubtype(from.terType->getId()))) //no special movement bonus { ret = VLC->terrainTypeHandler->getById(from.terType->getId())->moveCost; @@ -249,14 +249,14 @@ void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) c lowestCreatureSpeed = realLowestSpeed; //Let updaters run again treeHasChanged(); - ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(!!onLand)); + ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusSubtypes::heroMovementLand : BonusSubtypes::heroMovementSea)); } } int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const { updateArmyMovementBonus(onLand, ti); - return ti->valOfBonuses(BonusType::MOVEMENT, !!onLand); + return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusSubtypes::heroMovementLand : BonusSubtypes::heroMovementSea); } CGHeroInstance::CGHeroInstance(): @@ -638,7 +638,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, cnf); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; @@ -647,8 +647,8 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t } }); - vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(ESpellSchool::ANY))); //any school bonus - vstd::amax(skill, valOfBonuses(BonusType::SPELL, spell->getIndex())); //given by artifact or other effect + vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(SpellSchool::ANY))); //any school bonus + vstd::amax(skill, valOfBonuses(BonusType::SPELL, TBonusSubtype(spell->getId()))); //given by artifact or other effect vstd::amax(skill, 0); //in case we don't know any school vstd::amin(skill, 3); @@ -660,28 +660,28 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, //applying sorcery secondary skill if(spell->isMagical()) - base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, SpellSchool(ESpellSchool::ANY))) / 100.0); + base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, TBonusSubtype(SpellSchool::ANY))) / 100.0); - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, TBonusSubtype(spell->getId()))) / 100.0); int maxSchoolBonus = 0; spell->forEachSchool([&maxSchoolBonus, this](const SpellSchool & cnf, bool & stop) { - vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, cnf)); + vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, TBonusSubtype(cnf))); }); base = static_cast(base * (100 + maxSchoolBonus) / 100.0); if(affectedStack && affectedStack->creatureLevel() > 0) //Hero specials like Solmyr, Deemer - base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, spell->getIndex()) / affectedStack->creatureLevel()) / 100.0); + base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, TBonusSubtype(spell->getId())) / affectedStack->creatureLevel()) / 100.0); return base; } int64_t CGHeroInstance::getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const { - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, spell->getIndex())) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, TBonusSubtype(spell->getId()))) / 100.0); return base; } @@ -751,19 +751,19 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->getIndex()); const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook(); - const bool specificBonus = hasBonusOfType(BonusType::SPELL, spell->getIndex()); + const bool specificBonus = hasBonusOfType(BonusType::SPELL, TBonusSubtype(spell->getId())); bool schoolBonus = false; spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop) { - if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, cnf)) + if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, TBonusSubtype(cnf))) { schoolBonus = stop = true; } }); - const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, spell->getLevel()); + const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusSubtypes::spellLevel(spell->getLevel())); if(spell->isSpecial()) { @@ -845,13 +845,6 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b TConstBonusListPtr improvedNecromancy = getBonuses(Selector::type()(BonusType::IMPROVED_NECROMANCY)); if(!improvedNecromancy->empty()) { - auto getCreatureID = [](const std::shared_ptr & bonus) -> CreatureID - { - assert(bonus->subtype >=0); - if(bonus->subtype >= 0) - return CreatureID(bonus->subtype); - return CreatureID::NONE; - }; int maxCasualtyLevel = 1; for(const auto & casualty : casualties) vstd::amax(maxCasualtyLevel, VLC->creatures()->getByIndex(casualty.first)->getLevel()); @@ -868,9 +861,9 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } else { - auto quality = [getCreatureID](const std::shared_ptr & pick) -> std::tuple + auto quality = [](const std::shared_ptr & pick) -> std::tuple { - const auto * c = getCreatureID(pick).toCreature(); + const auto * c = pick->subtype.as().toCreature(); return std::tuple {c->getLevel(), static_cast(c->getFullRecruitCost().marketValue()), -pick->additionalInfo[1]}; }; if(quality(topPick) < quality(newPick)) @@ -879,7 +872,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } if(topPick) { - creatureTypeRaised = getCreatureID(topPick); + creatureTypeRaised = topPick->subtype.as(); requiredCasualtyLevel = std::max(topPick->additionalInfo[1], 1); } } @@ -1015,12 +1008,12 @@ int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) { - auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(which)) + auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(which)) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); if(hasBonus(sel)) removeBonuses(sel); - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), static_cast(which))); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), TBonusSubtype(which))); } EAlignment CGHeroInstance::getAlignment() const @@ -1283,7 +1276,7 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() for(const auto & elem : secSkills) { - if(elem.second < SecSkillLevel::EXPERT) + if(elem.second < MasteryLevel::EXPERT) basicAndAdv.insert(elem.first); else expert.insert(elem.first); @@ -1403,7 +1396,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 if(primarySkill < PrimarySkill::EXPERIENCE) { auto skill = getBonusLocalFirst(Selector::type()(BonusType::PRIMARY_SKILL) - .And(Selector::subtype()(static_cast(primarySkill))) + .And(Selector::subtype()(TBonusSubtype(primarySkill))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); assert(skill); @@ -1474,13 +1467,10 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) } } -bool CGHeroInstance::hasVisions(const CGObjectInstance * target, const int subtype) const +bool CGHeroInstance::hasVisions(const CGObjectInstance * target, TBonusSubtype subtype) const { //VISIONS spell support - - const auto cached = "type_" + std::to_string(vstd::to_underlying(BonusType::VISIONS)) + "__subtype_" + std::to_string(subtype); - - const int visionsMultiplier = valOfBonuses(Selector::typeSubtype(BonusType::VISIONS,subtype), cached); + const int visionsMultiplier = valOfBonuses(BonusType::VISIONS, subtype); int visionsRange = visionsMultiplier * getPrimSkillLevel(PrimarySkill::SPELL_POWER); @@ -1567,11 +1557,11 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { auto primarySkills = handler.enterStruct("primarySkills"); - for(int i = 0; i < GameConstants::PRIMARY_SKILLS; ++i) + for(auto i = PrimarySkill::BEGIN; i < PrimarySkill::END; ++i) { - int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, i).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); + int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(i)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); - handler.serializeInt(NPrimarySkill::names[i], value, 0); + handler.serializeInt(NPrimarySkill::names[i.getNum()], value, 0); } } } @@ -1762,7 +1752,7 @@ bool CGHeroInstance::isMissionCritical() const void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { - TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, stack.type->getId())); + TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, TBonusSubtype(stack.type->getId()))); for(const auto & it : *lista) { auto nid = CreatureID(it->additionalInfo[0]); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a8610b41e..ce0196a04 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -248,7 +248,7 @@ public: void fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const override; - bool hasVisions(const CGObjectInstance * target, const int subtype) const; + bool hasVisions(const CGObjectInstance * target, TBonusSubtype masteryLevel) const; /// If this hero perishes, the scenario is failed bool isMissionCritical() const; diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 72d998d7f..c1011c015 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -146,7 +146,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const if(!h->hasBonusFrom(BonusSource::OBJECT, Obj::STABLES)) //does not stack with advMap Stables { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, 94, VLC->generaltexth->arraytxt[100], 1); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, Obj::STABLES, BonusSubtypes::heroMovementLand, VLC->generaltexth->arraytxt[100]); gb.id = heroID.getNum(); cb->giveHeroBonus(&gb); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 36543e448..61a179f23 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -160,7 +160,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::type()(BonusType::CREATURE_GROWTH).And(Selector::subtype()(level))); + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusSubtypes::creatureLevel(level))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 86b8dd933..3bc766b83 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -836,7 +836,7 @@ void CGArtifact::serializeJsonOptions(JsonSerializeFormat& handler) if(handler.saving && ID == Obj::SPELL_SCROLL) { const std::shared_ptr b = storedArtifact->getBonusLocalFirst(Selector::type()(BonusType::SPELL)); - SpellID spellId(b->subtype); + SpellID spellId(b->subtype.as()); handler.serializeId("spell", spellId, SpellID::NONE); } @@ -1204,7 +1204,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT; gb.bonus.sid = id.getNum(); - gb.bonus.subtype = 0; + gb.bonus.subtype = BonusSubtypes::heroMovementSea; // FIXME: This is really dirty hack // Proper fix would be to make CGLighthouse into bonus system node diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index c5d8cf9a3..ee237bfe5 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -531,9 +531,9 @@ const TurnInfo * CPathfinderHelper::getTurnInfo() const return turnsInfo[turn]; } -bool CPathfinderHelper::hasBonusOfType(const BonusType type, const int subtype) const +bool CPathfinderHelper::hasBonusOfType(const BonusType type) const { - return turnsInfo[turn]->hasBonusOfType(type, subtype); + return turnsInfo[turn]->hasBonusOfType(type); } int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index a52de7b65..af12ca56a 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -83,7 +83,7 @@ public: void updateTurnInfo(const int turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; const TurnInfo * getTurnInfo() const; - bool hasBonusOfType(const BonusType type, const int subtype = -1) const; + bool hasBonusOfType(BonusType type) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; std::vector getCastleGates(const PathNodeInfo & source) const; diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 6442a54f8..6fad68381 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -23,7 +23,7 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) for(const auto & terrain : VLC->terrainTypeHandler->objects) { noTerrainPenalty.push_back(static_cast( - bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(terrain->getIndex()))))); + bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(TBonusSubtype(terrain->getId())))))); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); @@ -71,7 +71,7 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const return true; } -bool TurnInfo::hasBonusOfType(BonusType type, int subtype) const +bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const { switch(type) { @@ -82,14 +82,14 @@ bool TurnInfo::hasBonusOfType(BonusType type, int subtype) const case BonusType::WATER_WALKING: return bonusCache->waterWalking; case BonusType::NO_TERRAIN_PENALTY: - return bonusCache->noTerrainPenalty[subtype]; + return bonusCache->noTerrainPenalty[subtype.getNum()]; } return static_cast( bonuses->getFirst(Selector::type()(type).And(Selector::subtype()(subtype)))); } -int TurnInfo::valOfBonuses(BonusType type, int subtype) const +int TurnInfo::valOfBonuses(BonusType type, TBonusSubtype subtype) const { switch(type) { diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index aa7d77b40..6bfa20098 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -42,8 +42,10 @@ struct DLL_LINKAGE TurnInfo TurnInfo(const CGHeroInstance * Hero, const int Turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; - bool hasBonusOfType(const BonusType type, const int subtype = -1) const; - int valOfBonuses(const BonusType type, const int subtype = -1) const; + bool hasBonusOfType(const BonusType type) const; + bool hasBonusOfType(const BonusType type, const TBonusSubtype subtype) const; + int valOfBonuses(const BonusType type) const; + int valOfBonuses(const BonusType type, const TBonusSubtype subtype) const; void updateHeroBonuses(BonusType type, const CSelector& sel) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; }; diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 5b965011b..cb080adca 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -37,7 +37,7 @@ Rewardable::Reward::Reward() , movePercentage(-1) , primary(4, 0) , removeObject(false) - , spellCast(SpellID::NONE, SecSkillLevel::NONE) + , spellCast(SpellID::NONE, MasteryLevel::NONE) { } diff --git a/lib/spells/AbilityCaster.cpp b/lib/spells/AbilityCaster.cpp index d7e138d11..d7507c451 100644 --- a/lib/spells/AbilityCaster.cpp +++ b/lib/spells/AbilityCaster.cpp @@ -35,7 +35,7 @@ int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSel if(spell->getLevel() > 0) { - vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, SpellSchool(ESpellSchool::ANY))); + vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(SpellSchool::ANY))); } vstd::amax(skill, 0); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index f80749aeb..0462247e0 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -383,15 +383,15 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni //applying protections - when spell has more then one elements, only one protection should be applied (I think) forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, cnf)) + if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf))) { - ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf); + ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf)); ret /= 100; stop = true; //only bonus from one school is used } }); - CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)); + CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::ANY)); auto cachingStr = "type_SPELL_DAMAGE_REDUCTION_s_ANY"; //general spell dmg reduction, works only on magical effects @@ -402,9 +402,9 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni } //dmg increasing - if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, id)) + if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, TBonusSubtype(id))) { - ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, id.toEnum()); + ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, TBonusSubtype(id)); ret /= 100; } } diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index 75cb838d6..b26889284 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -157,7 +157,7 @@ protected: { std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, m->getSpellIndex(), 1), cachingStr.str()); + return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId()), 1), cachingStr.str()); } }; @@ -179,14 +179,14 @@ protected: m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, cnf)) + if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, TBonusSubtype(cnf))) { elementalImmune = true; stop = true; //only bonus from one school is used } else if(!m->isPositiveSpell()) //negative or indifferent { - if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, cnf)) + if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, TBonusSubtype(cnf))) { elementalImmune = true; stop = true; //only bonus from one school is used @@ -231,7 +231,7 @@ public: protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, m->getSpellIndex()); + return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId())); } }; @@ -292,8 +292,8 @@ class ImmunityNegationCondition : public TargetConditionItemBase protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, 0); - const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, 1); + const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypes::immunityBattleWide); + const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypes::immunityEnemyHero); //Non-magical effects is not affected by orb of vulnerability if(!m->isMagicalEffect()) return false; diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 9e29a56b1..ef7ad2425 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -89,11 +89,11 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const if(!UnitEffect::isReceptive(m, unit)) return false; - bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::ANY)) >= 100); //General spell damage immunity + bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::ANY)) >= 100); //General spell damage immunity //elemental immunity for damage m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, cnf) >= 100); //100% reduction is immunity + isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf)) >= 100); //100% reduction is immunity }); return !isImmune; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 947e264fd..fb630542d 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -48,7 +48,7 @@ static void describeEffect(std::vector & log, const Mechanics * m, c { case BonusType::NOT_ACTIVE: { - switch(bonus.subtype) + switch(bonus.subtype.as().toEnum()) { case SpellID::STONE_GAZE: addLogLine(558, boost::logic::indeterminate); @@ -109,9 +109,9 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg const auto *casterHero = dynamic_cast(m->caster); if(casterHero) { - peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, m->getSpellIndex())); - addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, m->getSpellIndex())); - fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, m->getSpellIndex())); + peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, TBonusSubtype(m->getSpellId()))); + addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, TBonusSubtype(m->getSpellId()))); + fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, TBonusSubtype(m->getSpellId()))); } //TODO: does hero specialty should affects his stack casting spells? diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index 646f54aec..abc9d3036 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -253,7 +253,7 @@ bool UnitEffect::isReceptive(const Mechanics * m, const battle::Unit * unit) con //SPELL_IMMUNITY absolute case std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, m->getSpellIndex(), 1), cachingStr.str()); + return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId()), 1), cachingStr.str()); } else { From 910ad504176cdabbe4f1e99cb41af5d387c9bdae Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 5 Oct 2023 18:18:14 +0300 Subject: [PATCH 0904/1248] Fix client & server compilation --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 10 +-- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 4 +- docs/modders/Bonus/Bonus_Types.md | 2 - lib/bonuses/Bonus.h | 9 +++ lib/bonuses/BonusParams.cpp | 10 +-- lib/constants/EntityIdentifiers.h | 1 - lib/modding/IdentifierStorage.cpp | 2 +- lib/spells/CSpellHandler.cpp | 16 ++--- server/CGameHandler.cpp | 14 ++-- server/battles/BattleActionProcessor.cpp | 67 ++++++++++--------- server/battles/BattleFlowProcessor.cpp | 16 ++--- server/battles/BattleResultProcessor.cpp | 4 +- server/processors/PlayerMessageProcessor.cpp | 2 +- test/battle/CUnitStateMagicTest.cpp | 4 +- test/battle/CUnitStateTest.cpp | 12 ++-- test/entity/CCreatureTest.cpp | 11 ++- test/spells/AbilityCasterTest.cpp | 4 +- test/spells/effects/TimedTest.cpp | 4 +- .../AbsoluteSpellConditionTest.cpp | 4 +- .../targetConditions/BonusConditionTest.cpp | 2 +- .../ElementalConditionTest.cpp | 14 ++-- .../ImmunityNegationConditionTest.cpp | 4 +- .../NormalSpellConditionTest.cpp | 4 +- 23 files changed, 112 insertions(+), 108 deletions(-) diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index d3725d0f7..efbfefe66 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -242,13 +242,13 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) return 1500; auto statsValue = - 10 * art->valOfBonuses(BonusType::MOVEMENT, 1) + 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusSubtypes::heroMovementLand) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 700 * art->valOfBonuses(BonusType::MORALE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::ATTACK)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::KNOWLEDGE)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::SPELL_POWER)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::KNOWLEDGE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::SPELL_POWER)) + 500 * art->valOfBonuses(BonusType::LUCK); auto classValue = 0; diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index e53c921a9..f40aa89dd 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -984,7 +984,7 @@ std::vector AINodeStorage::calculateTeleportations( struct TowmPortalFinder { const std::vector & initialNodes; - MasteryLevel townPortalSkillLevel; + MasteryLevel::Type townPortalSkillLevel; uint64_t movementNeeded; const ChainActor * actor; const CGHeroInstance * hero; @@ -1006,7 +1006,7 @@ struct TowmPortalFinder townPortal = spellID.toSpell(); // TODO: Copy/Paste from TownPortalMechanics - townPortalSkillLevel = MasteryLevel(hero->getSpellSchoolLevel(townPortal)); + townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal)); movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3); } diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 323f17661..6274a047f 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -118,14 +118,12 @@ Determines chance for affected heroes to learn spell casted by enemy hero after Allows affected heroes to learn spell casted by enemy hero after battle -- subtype: must be set to -1 - val: maximal level of spell that can be learned ### LEARN_MEETING_SPELL_LIMIT Allows affected heroes to learn spells from each other during hero exchange -- subtype: must be set to -1 - val: maximal level of spell that can be learned ### ROUGH_TERRAIN_DISCOUNT diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 43c0a888f..cca9dd27f 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -60,6 +60,15 @@ static const TBonusSubtype visionsTowns; // 2 static const TBonusSubtype immunityBattleWide; // 0 static const TBonusSubtype immunityEnemyHero; // 1 +static const TBonusSubtype transmutationPerHealth; // 0 +static const TBonusSubtype transmutationPerUnit; // 1 + +static const TBonusSubtype destructionKillPercentage; // 0 +static const TBonusSubtype destructionKillAmount; // 1 + +static const TBonusSubtype soulStealPermanent; // 0 +static const TBonusSubtype soulStealBattle; // 1 + TBonusSubtype spellLevel(int level); TBonusSubtype creatureLevel(int level); diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index ad109f79f..6cd6793d9 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -81,7 +81,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(ESpellSchool::ANY); + subtype = TBonusSubtype(SpellSchool::ANY); } else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar") type = BonusType::LEARN_MEETING_SPELL_LIMIT; @@ -120,22 +120,22 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(ESpellSchool::AIR); + subtype = TBonusSubtype(SpellSchool::AIR); } else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(ESpellSchool::WATER); + subtype = TBonusSubtype(SpellSchool::WATER); } else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(ESpellSchool::FIRE); + subtype = TBonusSubtype(SpellSchool::FIRE); } else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(ESpellSchool::EARTH); + subtype = TBonusSubtype(SpellSchool::EARTH); } else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 23a3cf334..ab1d31992 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -978,7 +978,6 @@ public: // Deprecated // TODO: remove -using ESpellSchool = SpellSchool; using ETownType = FactionID; using EGameResID = GameResID; using River = RiverId; diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index a797ebb78..615995ea4 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -28,7 +28,7 @@ CIdentifierStorage::CIdentifierStorage() for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); - registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(ESpellSchool::ANY)); + registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(SpellSchool::ANY)); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 0462247e0..31b39997a 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -42,19 +42,19 @@ static const std::string LEVEL_NAMES[] = {"none", "basic", "advanced", "expert"} const spells::SchoolInfo SCHOOL[4] = { { - ESpellSchool::AIR, + SpellSchool::AIR, "air" }, { - ESpellSchool::FIRE, + SpellSchool::FIRE, "fire" }, { - ESpellSchool::WATER, + SpellSchool::WATER, "water" }, { - ESpellSchool::EARTH, + SpellSchool::EARTH, "earth" } }; @@ -62,10 +62,10 @@ const spells::SchoolInfo SCHOOL[4] = //order as described in http://bugs.vcmi.eu/view.php?id=91 static const SpellSchool SCHOOL_ORDER[4] = { - ESpellSchool::AIR, //=0 - ESpellSchool::FIRE, //=1 - ESpellSchool::EARTH,//=3(!) - ESpellSchool::WATER //=2(!) + SpellSchool::AIR, //=0 + SpellSchool::FIRE, //=1 + SpellSchool::EARTH,//=3(!) + SpellSchool::WATER //=2(!) }; } //namespace SpellConfig diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index fec358bf8..db99a1e9b 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -229,7 +229,6 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) return; } - scp.accumulatedBonus.subtype = 0; scp.accumulatedBonus.additionalInfo = 0; scp.accumulatedBonus.duration = BonusDuration::PERMANENT; scp.accumulatedBonus.turnsRemain = 0; @@ -249,11 +248,11 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) { case ECommander::ATTACK: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = static_cast(PrimarySkill::ATTACK); + scp.accumulatedBonus.subtype = TBonusSubtype(PrimarySkill::ATTACK); break; case ECommander::DEFENSE: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = static_cast(PrimarySkill::DEFENSE); + scp.accumulatedBonus.subtype = TBonusSubtype(PrimarySkill::DEFENSE); break; case ECommander::HEALTH: scp.accumulatedBonus.type = BonusType::STACK_HEALTH; @@ -261,7 +260,6 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) break; case ECommander::DAMAGE: scp.accumulatedBonus.type = BonusType::CREATURE_DAMAGE; - scp.accumulatedBonus.subtype = 0; scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE; break; case ECommander::SPEED: @@ -788,9 +786,9 @@ void CGameHandler::onNewTurn() if (!firstTurn) //not first day { - for (int k = 0; k < GameConstants::RESOURCE_QUANTITY; k++) + for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) { - n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, k); + n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, TBonusSubtype(k)); } } } @@ -1539,8 +1537,8 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t { const CGHeroInstance * h1 = getHero(fromHero); const CGHeroInstance * h2 = getHero(toHero); - int h1_scholarSpellLevel = h1->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT, -1); - int h2_scholarSpellLevel = h2->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT, -1); + int h1_scholarSpellLevel = h1->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT); + int h2_scholarSpellLevel = h2->valOfBonuses(BonusType::LEARN_MEETING_SPELL_LIMIT); if (h1_scholarSpellLevel < h2_scholarSpellLevel) { diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index fc491160c..4fa653fd4 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -162,11 +162,11 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c SetStackEffect sse; sse.battleID = battle.getBattle()->getBattleID(); - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, static_cast(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, static_cast(PrimarySkill::DEFENSE))); + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE))); int oldDefenceValue = defence.totalValue(); defence.push_back(std::make_shared(defenseBonusToAdd)); @@ -263,7 +263,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, TBonusSubtype(stack->creatureId())); } const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); @@ -355,7 +355,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, stack->creatureIndex()); + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, TBonusSubtype(stack->creatureId())); } for(int i = 1; i < totalRangedAttacks; ++i) @@ -382,13 +382,13 @@ bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, return false; std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype < 0) + if(!catapultAbility || catapultAbility->subtype == TBonusSubtype::NONE) { gameHandler->complain("We do not know how to shoot :P"); } else { - const CSpell * spell = SpellID(catapultAbility->subtype).toSpell(); + const CSpell * spell = catapultAbility->subtype.as().toSpell(); spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can shot infinitely by catapult auto shotLevel = stack->valOfBonuses(Selector::typeSubtype(BonusType::CATAPULT_EXTRA_SHOTS, catapultAbility->subtype)); parameters.setSpellLevel(shotLevel); @@ -407,7 +407,7 @@ bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle return false; std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, spellID)); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, TBonusSubtype(spellID))); //TODO special bonus for genies ability if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) @@ -452,13 +452,13 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con else destStack = battle.battleGetUnitByPos(target.at(0).hexValue); - if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype < 0) + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == TBonusSubtype::NONE) { gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); } else { - const CSpell * spell = SpellID(healerAbility->subtype).toSpell(); + const CSpell * spell = healerAbility->subtype.as().toSpell(); spells::BattleCast parameters(&battle, stack, spells::Mode::SPELL_LIKE_ATTACK, spell); //We can heal infinitely by first aid tent auto dest = battle::Destination(destStack, target.at(0).hexValue); parameters.setSpellLevel(0); @@ -907,7 +907,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const const auto * owner = battle.battleGetFightingHero(attacker->unitSide()); if(owner) { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, attacker->creatureIndex()); + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, TBonusSubtype(attacker->creatureId())); if (chance > gameHandler->getRandomGenerator().nextInt(99)) bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; } @@ -931,7 +931,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { //this is need for displaying hit animation bat.flags |= BattleAttack::SPELL_LIKE; - bat.spellID = SpellID(bonus->subtype); + bat.spellID = bonus->subtype.as(); //TODO: should spell override creature`s projectile? @@ -962,7 +962,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { //this is need for displaying affect animation bsa.flags |= BattleStackAttacked::SPELL_EFFECT; - bsa.spellID = SpellID(bonus->subtype); + bsa.spellID = bonus->subtype.as(); } } } @@ -1084,7 +1084,7 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo TConstBonusListPtr spells = attacker->getBonuses(Selector::type()(attackMode)); for(const auto & sf : *spells) { - spellsToCast.insert(SpellID(sf->subtype)); + spellsToCast.insert(sf->subtype.as()); } for(SpellID spellID : spellsToCast) { @@ -1095,7 +1095,7 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo return; } int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, spellID)); + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, TBonusSubtype(spellID))); for(const auto & sf : *spellsByType) { int meleeRanged; @@ -1113,7 +1113,7 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) castMe = true; } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, spellID))); + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, TBonusSubtype(spellID)))); vstd::amin(chance, 100); const CSpell * spell = SpellID(spellID).toSpell(); @@ -1168,7 +1168,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution //original formula x = min(x, (gorgons_count + 9)/10); - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, 0) / 100.0f; + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypes::deathStareGorgon) / 100.0f; vstd::amin(chanceToKill, 1); //cap at 100% std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); @@ -1179,7 +1179,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count vstd::amin(staredCreatures, maxToKill); - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, 1)) / defender->level(); + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypes::deathStareCommander)) / defender->level(); if(staredCreatures) { //TODO: death stare was not originally available for multiple-hex attacks, but... @@ -1249,9 +1249,9 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & else resurrectInfo.type = attacker->creatureId(); - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), 0)) + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypes::transmutationPerHealth)) resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), 1)) + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypes::transmutationPerUnit)) resurrectInfo.count = defender->getCount(); else return; //wrong subtype @@ -1273,21 +1273,21 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & gameHandler->sendAndApply(&fakeEvent); } - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0) || attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount)) { double chanceToTrigger = 0; int amountToDie = 0; - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 0)) //killing by percentage + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage)) //killing by percentage { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 0) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(0)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypes::destructionKillPercentage)))->additionalInfo[0]; amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, 1)) //killing by count + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount)) //killing by count { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, 1) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(1)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypes::destructionKillAmount)))->additionalInfo[0]; } vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% @@ -1348,12 +1348,13 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba { //we can have two bonuses - one with subtype 0 and another with subtype 1 //try to use permanent first, use only one of two - for(si32 subtype = 1; subtype >= 0; subtype--) + for(const auto & subtype : { BonusSubtypes::soulStealBattle, BonusSubtypes::soulStealPermanent}) { if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) { int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - attackerState->heal(toHeal, EHealLevel::OVERHEAL, ((subtype == 0) ? EHealPower::ONE_BATTLE : EHealPower::PERMANENT)); + bool permanent = subtype == BonusSubtypes::soulStealPermanent; + attackerState->heal(toHeal, EHealLevel::OVERHEAL, (permanent ? EHealPower::PERMANENT : EHealPower::ONE_BATTLE)); drainedLife += toHeal; break; } @@ -1365,9 +1366,9 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba if(!bat.shot() && !def->isClone() && def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, SpellSchool(ESpellSchool::FIRE)) && - !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, SpellSchool(ESpellSchool::FIRE)) && - attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, SpellSchool(ESpellSchool::FIRE)) < 100 && + !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, TBonusSubtype(SpellSchool::FIRE)) && + !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, TBonusSubtype(SpellSchool::FIRE)) && + attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::FIRE)) < 100 && CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) ) { diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 8be9c9f0f..e1d8055a1 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -144,7 +144,7 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle, std::shared_ptr summonInfo = stack->getBonus(Selector::type()(BonusType::SUMMON_GUARDIANS)); auto accessibility = battle.getAccesibility(); - CreatureID creatureData = CreatureID(summonInfo->subtype); + CreatureID creatureData = summonInfo->subtype.as(); std::vector targetHexes; const bool targetIsBig = stack->unitType()->isDoubleWide(); //target = creature to guard const bool guardianIsBig = creatureData.toCreature()->isDoubleWide(); @@ -198,7 +198,7 @@ void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle) { spells::BonusCaster caster(h, b); - const CSpell * spell = SpellID(b->subtype).toSpell(); + const CSpell * spell = b->subtype.as().toSpell(); spells::BattleCast parameters(&battle, &caster, spells::Mode::PASSIVE, spell); parameters.setSpellLevel(3); @@ -380,10 +380,10 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat } const CGHeroInstance * curOwner = battle.battleGetOwnerHero(next); - const int stackCreatureId = next->unitType()->getId(); + const CreatureID stackCreatureId = next->unitType()->getId(); if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, stackCreatureId))) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(stackCreatureId)))) { BattleAction attack; attack.actionType = EActionType::SHOOT; @@ -428,7 +428,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat return true; } - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::CATAPULT)) + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(CreatureID(CreatureID::CATAPULT)))) { BattleAction attack; attack.actionType = EActionType::CATAPULT; @@ -453,7 +453,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat return true; } - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, CreatureID::FIRST_AID_TENT)) + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(CreatureID(CreatureID::FIRST_AID_TENT)))) { RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); const CStack * toBeHealed = possibleStacks.front(); @@ -583,7 +583,7 @@ void BattleFlowProcessor::stackEnchantedTrigger(const CBattleInfoCallback & batt auto bl = *(st->getBonuses(Selector::type()(BonusType::ENCHANTED))); for(auto b : bl) { - const CSpell * sp = SpellID(b->subtype).toSpell(); + const CSpell * sp = b->subtype.as().toSpell(); if(!sp) continue; @@ -719,7 +719,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c while(!bl.empty() && !cast) { auto bonus = *RandomGeneratorUtil::nextItem(bl, gameHandler->getRandomGenerator()); - auto spellID = SpellID(bonus->subtype); + auto spellID = bonus->subtype.as(); const CSpell * spell = SpellID(spellID).toSpell(); bl.remove_if([&bonus](const Bonus * b) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index b8167a9e7..1369cb8de 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -316,9 +316,9 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(!finishingBattle->isDraw() && finishingBattle->winnerHero) { - if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT, -1)) + if (int eagleEyeLevel = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_LEVEL_LIMIT)) { - double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE, 0); + double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) { auto spell = spellId.toSpell(VLC->spells()); diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 62b30a72e..245d25851 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -144,7 +144,7 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { - giveBonus.bonus.subtype = level; + giveBonus.bonus.subtype = BonusSubtypes::spellLevel(level); gameHandler->sendAndApply(&giveBonus); } diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index eea48a4ff..52f31bdd5 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -55,7 +55,7 @@ public: void makeNormalCaster() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, 0, DEFAULT_SPELL_INDEX)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, 0, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); } }; @@ -171,7 +171,7 @@ TEST_F(UnitStateMagicTest, effectValue) const int32_t EFFECT_VALUE = 456; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, 0, DEFAULT_SPELL_INDEX)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, 0, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); makeNormalCaster(); EXPECT_EQ(subject.getEffectValue(&spellMock), EFFECT_VALUE * DEFAULT_AMOUNT); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 52b20ad34..47ef88928 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -53,8 +53,8 @@ public: { bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, 0)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, static_cast(PrimarySkill::ATTACK))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, static_cast(PrimarySkill::DEFENSE))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, TBonusSubtype(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, TBonusSubtype(PrimarySkill::DEFENSE))); bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, 0)); @@ -248,10 +248,10 @@ TEST_F(UnitStateTest, getMinDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, BonusSubtypes::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, 1); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, BonusSubtypes::creatureDamageMin); bonusMock.addNewBonus(bonus); } @@ -264,10 +264,10 @@ TEST_F(UnitStateTest, getMaxDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, BonusSubtypes::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, 2); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, BonusSubtypes::creatureDamageMax); bonusMock.addNewBonus(bonus); } diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index 78fc13bc8..c89a4112d 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -107,7 +107,7 @@ TEST_F(CCreatureTest, JsonAddBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); JsonNode & toAdd = data["bonuses"]["toAdd"]; @@ -122,7 +122,7 @@ TEST_F(CCreatureTest, JsonAddBonus) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) && (bonus->sid == 42) - && (bonus->subtype == 43) + && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -133,10 +133,10 @@ TEST_F(CCreatureTest, JsonRemoveBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b1); - std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, 42, 43, BonusValueType::BASE_NUMBER); + std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b2); @@ -153,7 +153,7 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) && (bonus->sid == 42) - && (bonus->subtype == 43) + && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -166,7 +166,6 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 18) && (bonus->sid == 42) - && (bonus->subtype == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index e9f9a47e7..c4f68b010 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -56,7 +56,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, SpellSchool(ESpellSchool::ANY))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, TBonusSubtype(SpellSchool::ANY))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); @@ -70,7 +70,7 @@ TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, SpellSchool(ESpellSchool::AIR))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, TBonusSubtype(SpellSchool::AIR))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index d9205f4c5..b61ed1915 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -71,9 +71,9 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, static_cast(PrimarySkill::KNOWLEDGE)); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, TBonusSubtype(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, static_cast(PrimarySkill::KNOWLEDGE)); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, TBonusSubtype(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index 9a776f4de..15e3eb3ee 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 91fa9b411..23aa8ba3e 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -49,7 +49,7 @@ TEST_F(BonusConditionTest, ReceptiveIfMatchesType) TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, SpellSchool(ESpellSchool::FIRE))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, SpellSchool(SpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index e513b9ce7..899889407 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -30,8 +30,8 @@ public: EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { bool stop = false; - cb(SpellSchool(ESpellSchool::AIR), stop); - cb(SpellSchool(ESpellSchool::FIRE), stop); + cb(SpellSchool(SpellSchool::AIR), stop); + cb(SpellSchool(SpellSchool::FIRE), stop); }); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); @@ -56,7 +56,7 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -64,7 +64,7 @@ TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::WATER))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::WATER))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -72,7 +72,7 @@ TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -80,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, SpellSchool(ESpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index 8652d9db1..0c48181fb 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -57,7 +57,7 @@ TEST_P(ImmunityNegationConditionTest, WithHeroNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, 1)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, BonusSubtypes::immunityEnemyHero)); EXPECT_EQ(isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -66,7 +66,7 @@ TEST_P(ImmunityNegationConditionTest, WithBattleWideNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, BonusSubtypes::immunityBattleWide)); //This should return if ownerMatches, because anyone should cast onto owner's stacks, but not on enemyStacks EXPECT_EQ(ownerMatches && isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 93f3b83ec..57915b74a 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) TEST_P(NormalSpellConditionTest, ChecksNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, immuneSpell); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); if(immuneSpell == castSpell) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); From 77facf93879f2cb6fc953b22a6a8405d953f039e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 5 Oct 2023 21:18:11 +0300 Subject: [PATCH 0905/1248] Implement missing functions, fixes linking errors --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 1 + client/battle/BattleActionsController.cpp | 2 +- client/battle/BattleStacksController.cpp | 3 +- client/windows/CKingdomInterface.cpp | 2 +- cmake_modules/VCMI_lib.cmake | 5 + lib/BasicTypes.cpp | 1 + lib/CBonusTypeHandler.cpp | 1 + lib/CCreatureHandler.cpp | 6 + lib/CGameInfoCallback.cpp | 1 + lib/CStack.cpp | 1 + lib/CTownHandler.cpp | 6 + lib/battle/CUnitState.cpp | 2 + lib/battle/DamageCalculator.cpp | 1 + lib/bonuses/Bonus.cpp | 12 ++ lib/bonuses/Bonus.h | 46 +---- lib/bonuses/BonusParams.cpp | 1 + lib/bonuses/BonusSubtypes.cpp | 56 ++++++ lib/bonuses/BonusSubtypes.h | 64 +++++++ lib/constants/EntityIdentifiers.cpp | 54 ++++++ lib/constants/EntityIdentifiers.h | 166 ++---------------- lib/constants/IdentifierBase.h | 64 +++++++ lib/constants/MetaIdentifier.cpp | 69 ++++++++ lib/constants/MetaIdentifier.h | 65 +++++++ lib/mapObjects/CGCreature.cpp | 1 + lib/mapObjects/CGHeroInstance.cpp | 1 + lib/mapObjects/CGTownBuilding.cpp | 1 + lib/mapObjects/CGTownInstance.cpp | 1 + lib/mapObjects/MiscObjects.cpp | 1 + lib/pathfinder/TurnInfo.cpp | 10 ++ lib/spells/TargetCondition.cpp | 1 + server/battles/BattleActionProcessor.cpp | 1 + server/processors/PlayerMessageProcessor.cpp | 5 +- test/battle/CUnitStateTest.cpp | 1 + .../ImmunityNegationConditionTest.cpp | 1 + 34 files changed, 452 insertions(+), 201 deletions(-) create mode 100644 lib/bonuses/BonusSubtypes.cpp create mode 100644 lib/bonuses/BonusSubtypes.h create mode 100644 lib/constants/IdentifierBase.h create mode 100644 lib/constants/MetaIdentifier.cpp create mode 100644 lib/constants/MetaIdentifier.h diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index efbfefe66..b2eccc938 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -11,6 +11,7 @@ #include #include "Nullkiller.h" +#include "../../../lib/bonuses/BonusSubtypes.h" #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 745bb325b..7fde1eb12 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -898,7 +898,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS for(const auto & bonus : *bl) { if (bonus->additionalInfo[0] <= 0) - creatureSpells.push_back(SpellID(bonus->subtype).toSpell()); + creatureSpells.push_back(bonus->subtype.as().toSpell()); } } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 9f0a86c3e..a6308dfb4 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -34,6 +34,7 @@ #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" +#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" #include "../../lib/TextOperations.h" @@ -534,7 +535,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorhasBonus(Selector::typeSubtype(BonusType::FLYING, 1))) + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusSubtypes::movementFlying))) { owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() { diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 7e04aaef5..ba0082556 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -579,7 +579,7 @@ void CKingdomInterface::generateMinesList(const std::vector heroes = LOCPLINT->cb->getHeroesInfo(true); for(auto & heroe : heroes) { - totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, GameResID(EGameResID::GOLD))); + totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, TBonusSubtype(GameResID(EGameResID::GOLD)))); } //Add town income of all towns diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index b00149829..17a996fd3 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -31,6 +31,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.cpp ${MAIN_LIB_DIR}/bonuses/BonusParams.cpp ${MAIN_LIB_DIR}/bonuses/BonusSelector.cpp + ${MAIN_LIB_DIR}/bonuses/BonusSubtypes.cpp ${MAIN_LIB_DIR}/bonuses/CBonusProxy.cpp ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.cpp ${MAIN_LIB_DIR}/bonuses/IBonusBearer.cpp @@ -42,6 +43,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignState.cpp ${MAIN_LIB_DIR}/constants/EntityIdentifiers.cpp + ${MAIN_LIB_DIR}/constants/MetaIdentifier.cpp ${MAIN_LIB_DIR}/events/ApplyDamage.cpp ${MAIN_LIB_DIR}/events/GameResumed.cpp @@ -357,6 +359,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.h ${MAIN_LIB_DIR}/bonuses/BonusParams.h ${MAIN_LIB_DIR}/bonuses/BonusSelector.h + ${MAIN_LIB_DIR}/bonuses/BonusSubtypes.h ${MAIN_LIB_DIR}/bonuses/CBonusProxy.h ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.h ${MAIN_LIB_DIR}/bonuses/IBonusBearer.h @@ -371,6 +374,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/constants/EntityIdentifiers.h ${MAIN_LIB_DIR}/constants/Enumerations.h + ${MAIN_LIB_DIR}/constants/IdentifierBase.h + ${MAIN_LIB_DIR}/constants/MetaIdentifier.h ${MAIN_LIB_DIR}/constants/NumericConstants.h ${MAIN_LIB_DIR}/constants/StringConstants.h diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index 692551944..c3a55bf63 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -17,6 +17,7 @@ #include "bonuses/BonusList.h" #include "bonuses/Bonus.h" #include "bonuses/IBonusBearer.h" +#include "bonuses/BonusSubtypes.h" #include #include diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index c4a35052b..6f988d3f9 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -20,6 +20,7 @@ #include "CCreatureHandler.h" #include "CGeneralTextHandler.h" #include "spells/CSpellHandler.h" +#include "bonuses/BonusSubtypes.h" template class std::vector; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 9c936068c..46f0cfd0e 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -19,6 +19,7 @@ #include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" +#include "bonuses/BonusSubtypes.h" #include "serializer/JsonDeserializer.h" #include "serializer/JsonUpdater.h" #include "mapObjectConstructors/AObjectTypeHandler.h" @@ -291,6 +292,11 @@ CCreature::CCreature() fightValue = AIValue = growth = hordeGrowth = ammMin = ammMax = 0; } +void CCreature::addBonus(int val, BonusType type) +{ + addBonus(val, type, TBonusSubtype::NONE); +} + void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) { auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, getIndex())); diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index e0f290ef9..5a4a234b1 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -17,6 +17,7 @@ #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo +#include "bonuses/BonusSubtypes.h" #include "NetPacks.h" // for InfoWindow #include "GameSettings.h" #include "TerrainHandler.h" diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 3eb7d91d2..3256a17bf 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -17,6 +17,7 @@ #include "CGeneralTextHandler.h" #include "battle/BattleInfo.h" +#include "bonuses/BonusSubtypes.h" #include "spells/CSpellHandler.h" #include "NetPacks.h" diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 38f43edd6..f37f02d59 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -23,6 +23,7 @@ #include "filesystem/Filesystem.h" #include "bonuses/Bonus.h" #include "bonuses/Propagators.h" +#include "bonuses/BonusSubtypes.h" #include "ResourceSet.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" @@ -544,6 +545,11 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const building->addNewBonus(b, building->buildingBonuses); } +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val) const +{ + return createBonus(build, type, val, TBonusSubtype::NONE, emptyPropagator()); +} + std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const { return createBonus(build, type, val, subtype, emptyPropagator()); diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 5768c834a..3b87e7b0a 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -16,6 +16,8 @@ #include "../NetPacks.h" #include "../CCreatureHandler.h" +#include "../bonuses/BonusSubtypes.h" + #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 97489e7ab..11d542b7c 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -14,6 +14,7 @@ #include "Unit.h" #include "../bonuses/Bonus.h" +#include "../bonuses/BonusSubtypes.h" #include "../mapObjects/CGTownInstance.h" #include "../spells/CSpellHandler.h" #include "../GameSettings.h" diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index bf15aca05..67f300912 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -183,6 +183,18 @@ JsonNode Bonus::toJsonNode() const return root; } +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID) + : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, std::string()) +{} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc) + : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, Desc) +{} + +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype) + : Bonus(Duration, Type, Src, Val, ID, Subtype, std::string()) +{} + Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype, std::string Desc): duration(Duration), type(Type), diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index cca9dd27f..2a47aa6cc 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -10,7 +10,7 @@ #pragma once #include "BonusEnum.h" -#include "../constants/EntityIdentifiers.h" +#include "../constants/MetaIdentifier.h" VCMI_LIB_NAMESPACE_BEGIN @@ -30,50 +30,6 @@ using TLimiterPtr = std::shared_ptr; using TPropagatorPtr = std::shared_ptr; using TUpdaterPtr = std::shared_ptr; -namespace BonusSubtypes -{ - -static const TBonusSubtype creatureDamageBoth; // 0 -static const TBonusSubtype creatureDamageMin; // 1 -static const TBonusSubtype creatureDamageMax; // 2 - -static const TBonusSubtype damageTypeAll; // -1 -static const TBonusSubtype damageTypeMelee; // 0 -static const TBonusSubtype damageTypeRanged; // 1 - -static const TBonusSubtype heroMovementLand; // 1 -static const TBonusSubtype heroMovementSea; // 0 - -static const TBonusSubtype heroMovementPenalty; // 2 -static const TBonusSubtype heroMovementFull; // 1 - -static const TBonusSubtype deathStareGorgon; // 0 -static const TBonusSubtype deathStareCommander; - -static const TBonusSubtype rebirthRegular; // 0 -static const TBonusSubtype rebirthSpecial; // 1 - -static const TBonusSubtype visionsMonsters; // 0 -static const TBonusSubtype visionsHeroes; // 1 -static const TBonusSubtype visionsTowns; // 2 - -static const TBonusSubtype immunityBattleWide; // 0 -static const TBonusSubtype immunityEnemyHero; // 1 - -static const TBonusSubtype transmutationPerHealth; // 0 -static const TBonusSubtype transmutationPerUnit; // 1 - -static const TBonusSubtype destructionKillPercentage; // 0 -static const TBonusSubtype destructionKillAmount; // 1 - -static const TBonusSubtype soulStealPermanent; // 0 -static const TBonusSubtype soulStealBattle; // 1 - -TBonusSubtype spellLevel(int level); -TBonusSubtype creatureLevel(int level); - -} - class DLL_LINKAGE CAddInfo : public std::vector { public: diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index 6cd6793d9..e760794d6 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -13,6 +13,7 @@ #include "BonusEnum.h" #include "BonusParams.h" #include "BonusSelector.h" +#include "BonusSubtypes.h" #include "../ResourceSet.h" diff --git a/lib/bonuses/BonusSubtypes.cpp b/lib/bonuses/BonusSubtypes.cpp new file mode 100644 index 000000000..579c299c4 --- /dev/null +++ b/lib/bonuses/BonusSubtypes.cpp @@ -0,0 +1,56 @@ +/* + * Bonus.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 "BonusSubtypes.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const TBonusSubtype BonusSubtypes::creatureDamageBoth("", "", 0); +const TBonusSubtype BonusSubtypes::creatureDamageMin("", "", 1); +const TBonusSubtype BonusSubtypes::creatureDamageMax("", "", 2); +const TBonusSubtype BonusSubtypes::damageTypeAll("", "", -1); +const TBonusSubtype BonusSubtypes::damageTypeMelee("", "", 0); +const TBonusSubtype BonusSubtypes::damageTypeRanged("", "", 1); +const TBonusSubtype BonusSubtypes::heroMovementLand("", "", 1); +const TBonusSubtype BonusSubtypes::heroMovementSea("", "", 0); +const TBonusSubtype BonusSubtypes::heroMovementPenalty("", "", 2); +const TBonusSubtype BonusSubtypes::heroMovementFull("", "", 1); +const TBonusSubtype BonusSubtypes::deathStareGorgon("", "", 0); +const TBonusSubtype BonusSubtypes::deathStareCommander("", "", 1); +const TBonusSubtype BonusSubtypes::rebirthRegular("", "", 0); +const TBonusSubtype BonusSubtypes::rebirthSpecial("", "", 1); +const TBonusSubtype BonusSubtypes::visionsMonsters("", "", 0); +const TBonusSubtype BonusSubtypes::visionsHeroes("", "", 1); +const TBonusSubtype BonusSubtypes::visionsTowns("", "", 2); +const TBonusSubtype BonusSubtypes::immunityBattleWide("", "", 0); +const TBonusSubtype BonusSubtypes::immunityEnemyHero("", "", 1); +const TBonusSubtype BonusSubtypes::transmutationPerHealth("", "", 0); +const TBonusSubtype BonusSubtypes::transmutationPerUnit("", "", 1); +const TBonusSubtype BonusSubtypes::destructionKillPercentage("", "", 0); +const TBonusSubtype BonusSubtypes::destructionKillAmount("", "", 1); +const TBonusSubtype BonusSubtypes::soulStealPermanent("", "", 0); +const TBonusSubtype BonusSubtypes::soulStealBattle("", "", 1); +const TBonusSubtype BonusSubtypes::movementFlying("", "", 0); +const TBonusSubtype BonusSubtypes::movementTeleporting("", "", 1); + +TBonusSubtype BonusSubtypes::spellLevel(int level) +{ + assert(0); //todo + return TBonusSubtype(); +} + +TBonusSubtype BonusSubtypes::creatureLevel(int level) +{ + assert(0); //todo + return TBonusSubtype(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSubtypes.h b/lib/bonuses/BonusSubtypes.h new file mode 100644 index 000000000..24ed6deeb --- /dev/null +++ b/lib/bonuses/BonusSubtypes.h @@ -0,0 +1,64 @@ +/* + * BonusSubtypes.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 "../constants/MetaIdentifier.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TBonusSubtype = MetaIdentifier; + +class DLL_LINKAGE BonusSubtypes +{ +public: + static const TBonusSubtype creatureDamageBoth; // 0 + static const TBonusSubtype creatureDamageMin; // 1 + static const TBonusSubtype creatureDamageMax; // 2 + + static const TBonusSubtype damageTypeAll; // -1 + static const TBonusSubtype damageTypeMelee; // 0 + static const TBonusSubtype damageTypeRanged; // 1 + + static const TBonusSubtype heroMovementLand; // 1 + static const TBonusSubtype heroMovementSea; // 0 + + static const TBonusSubtype heroMovementPenalty; // 2 + static const TBonusSubtype heroMovementFull; // 1 + + static const TBonusSubtype deathStareGorgon; // 0 + static const TBonusSubtype deathStareCommander; + + static const TBonusSubtype rebirthRegular; // 0 + static const TBonusSubtype rebirthSpecial; // 1 + + static const TBonusSubtype visionsMonsters; // 0 + static const TBonusSubtype visionsHeroes; // 1 + static const TBonusSubtype visionsTowns; // 2 + + static const TBonusSubtype immunityBattleWide; // 0 + static const TBonusSubtype immunityEnemyHero; // 1 + + static const TBonusSubtype transmutationPerHealth; // 0 + static const TBonusSubtype transmutationPerUnit; // 1 + + static const TBonusSubtype destructionKillPercentage; // 0 + static const TBonusSubtype destructionKillAmount; // 1 + + static const TBonusSubtype soulStealPermanent; // 0 + static const TBonusSubtype soulStealBattle; // 1 + + static const TBonusSubtype movementFlying; // 0 + static const TBonusSubtype movementTeleporting; // 1 + + static TBonusSubtype spellLevel(int level); + static TBonusSubtype creatureLevel(int level); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 4c8e86d15..be491aae3 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -80,6 +80,15 @@ const FactionID FactionID::FORTRESS(7); const FactionID FactionID::CONFLUX(8); const FactionID FactionID::NEUTRAL(9); +const PrimarySkill PrimarySkill::NONE(-1); +const PrimarySkill PrimarySkill::ATTACK(0); +const PrimarySkill PrimarySkill::DEFENSE(1); +const PrimarySkill PrimarySkill::SPELL_POWER(2); +const PrimarySkill PrimarySkill::KNOWLEDGE(3); +const PrimarySkill PrimarySkill::BEGIN(0); +const PrimarySkill PrimarySkill::END(4); +const PrimarySkill PrimarySkill::EXPERIENCE(4); + const BoatId BoatId::NONE(-1); const BoatId BoatId::NECROPOLIS(0); const BoatId BoatId::CASTLE(1); @@ -273,6 +282,21 @@ std::string PlayerColor::entityType() return "playerColor"; } +si32 PrimarySkill::decode(const std::string& identifier) +{ + return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); +} + +std::string PrimarySkill::encode(const si32 index) +{ + return NPrimarySkill::names[index]; +} + +std::string PrimarySkill::entityType() +{ + return "primarySkill"; +} + si32 FactionID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); @@ -323,6 +347,36 @@ const ObstacleInfo * Obstacle::getInfo() const return VLC->obstacles()->getById(*this); } +si32 SpellSchool::decode(const std::string & identifier) +{ + return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); +} + +std::string SpellSchool::encode(const si32 index) +{ + return SpellConfig::SCHOOL[index].jsonName; +} + +std::string SpellSchool::entityType() +{ + return "spellSchool"; +} + +si32 GameResID::decode(const std::string & identifier) +{ + return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); +} + +std::string GameResID::encode(const si32 index) +{ + return GameConstants::RESOURCE_NAMES[index]; +} + +std::string GameResID::entityType() +{ + return "resource"; +} + std::string GameResID::entityType() { return "resource"; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index ab1d31992..80db1b81c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -10,6 +10,7 @@ #pragma once #include "NumericConstants.h" +#include "IdentifierBase.h" VCMI_LIB_NAMESPACE_BEGIN @@ -35,62 +36,6 @@ class CNonConstInfoCallback; /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -class IdentifierBase -{ -protected: - constexpr IdentifierBase(): - num(-1) - {} - - explicit constexpr IdentifierBase(int32_t value): - num(value) - {} - - ~IdentifierBase() = default; -public: - int32_t num; - - constexpr int32_t getNum() const - { - return num; - } - - constexpr void setNum(int32_t value) - { - num = value; - } - - struct hash - { - size_t operator()(const IdentifierBase & id) const - { - return std::hash()(id.num); - } - }; - - template void serialize(Handler &h, const int version) - { - h & num; - } - - constexpr void advance(int change) - { - num += change; - } - - constexpr operator int32_t () const - { - return num; - } - - friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) - { - return os << dt.num; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Note: use template to force different type, blocking any Identifier <=> Identifier comparisons template class Identifier : public IdentifierBase @@ -739,18 +684,18 @@ public: DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; }; -class CreatureID : public IdentifierWithEnum +class DLL_LINKAGE CreatureID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); }; -class SpellIDBase : public IdentifierBase +class DLL_LINKAGE SpellIDBase : public IdentifierBase { public: enum Type @@ -853,18 +798,18 @@ public: AFTER_LAST = 82 }; - DLL_LINKAGE const CSpell * toSpell() const; //deprecated - DLL_LINKAGE const spells::Spell * toSpell(const spells::Service * service) const; + const CSpell * toSpell() const; //deprecated + const spells::Spell * toSpell(const spells::Service * service) const; }; -class SpellID : public IdentifierWithEnum +class DLL_LINKAGE SpellID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); }; @@ -941,8 +886,8 @@ public: static const SpellSchool WATER; static const SpellSchool EARTH; - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); }; @@ -966,13 +911,13 @@ public: }; }; -class GameResID : public IdentifierWithEnum +class DLL_LINKAGE GameResID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); }; @@ -984,85 +929,4 @@ using River = RiverId; using Road = RoadId; using ETerrainId = TerrainId; -/// This class represents field that may contain value of multiple different identifer types -class MetaIdentifier -{ - std::string entityType; - std::string stringForm; - int32_t integerForm; - - void onDeserialized(); -public: - - static const MetaIdentifier NONE; - - MetaIdentifier(): - integerForm(-1) - {} - - explicit MetaIdentifier(const std::string & entityType, const std::string & identifier) - : entityType(entityType) - , stringForm(identifier) - , integerForm(-1) - { - onDeserialized(); - } - - explicit MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value) - : entityType(entityType) - , stringForm(identifier) - , integerForm(value) - { - } - - template - explicit MetaIdentifier(const IdentifierType & identifier ) - : entityType(IdentifierType::entityType()) - , integerForm(identifier.getNum()) - , stringForm(IdentifierType::encode(identifier.getNum())) - { - static_assert(std::is_base_of::value, "MetaIdentifier can only be constructed from Identifer class"); - } - - int32_t getNum() const - { - return integerForm; - } - - std::string toString() const - { - return stringForm; - } - - template - IdentifierType as() const - { - IdentifierType result(integerForm); - return result; - } - - template void serialize(Handler &h, const int version) - { - h & stringForm; - - if (!h.saving) - onDeserialized(); - } - - bool operator == (const MetaIdentifier & other) const - { - assert( (stringForm == other.stringForm) ? (integerForm == other.integerForm) : true ); - - return stringForm == other.stringForm; - } - - bool operator != (const MetaIdentifier & other) const - { - return !(*this == other); - } - - bool operator < (const MetaIdentifier & other) const; -}; - - VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h new file mode 100644 index 000000000..5ad0a8dd3 --- /dev/null +++ b/lib/constants/IdentifierBase.h @@ -0,0 +1,64 @@ +/* + * IdentifierBase.h, part of VCMI engine + * + * Authors: listed in file AUTHORS in main folder + * + * License: GNU General Public License v2.0 or later + * Full text of license available in license.txt file, in main folder + * + */ +#pragma once + +class IdentifierBase +{ +protected: + constexpr IdentifierBase(): + num(-1) + {} + + explicit constexpr IdentifierBase(int32_t value): + num(value) + {} + + ~IdentifierBase() = default; +public: + int32_t num; + + constexpr int32_t getNum() const + { + return num; + } + + constexpr void setNum(int32_t value) + { + num = value; + } + + struct hash + { + size_t operator()(const IdentifierBase & id) const + { + return std::hash()(id.num); + } + }; + + template void serialize(Handler &h, const int version) + { + h & num; + } + + constexpr void advance(int change) + { + num += change; + } + + constexpr operator int32_t () const + { + return num; + } + + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) + { + return os << dt.num; + } +}; diff --git a/lib/constants/MetaIdentifier.cpp b/lib/constants/MetaIdentifier.cpp new file mode 100644 index 000000000..140a6a558 --- /dev/null +++ b/lib/constants/MetaIdentifier.cpp @@ -0,0 +1,69 @@ +/* + * EntityIdentifiers.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 "MetaIdentifier.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const MetaIdentifier MetaIdentifier::NONE("", "", -1); + +MetaIdentifier::MetaIdentifier(): + integerForm(-1) +{} + +MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier) + : entityType(entityType) + , stringForm(identifier) + , integerForm(-1) +{ + onDeserialized(); +} + +MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value) + : entityType(entityType) + , stringForm(identifier) + , integerForm(value) +{ +} + +bool MetaIdentifier::operator == (const MetaIdentifier & other) const +{ + assert( (stringForm == other.stringForm) ? (integerForm == other.integerForm) : true ); + + return stringForm == other.stringForm; +} + +bool MetaIdentifier::operator != (const MetaIdentifier & other) const +{ + return !(*this == other); +} + +bool MetaIdentifier::operator < (const MetaIdentifier & other) const +{ + assert(0); +} + +int32_t MetaIdentifier::getNum() const +{ + return integerForm; +} + +std::string MetaIdentifier::toString() const +{ + return stringForm; +} + +void MetaIdentifier::onDeserialized() +{ + assert(0); //TODO +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/MetaIdentifier.h b/lib/constants/MetaIdentifier.h new file mode 100644 index 000000000..785338f41 --- /dev/null +++ b/lib/constants/MetaIdentifier.h @@ -0,0 +1,65 @@ +/* + * MetaIdentifier.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 "IdentifierBase.h" + +VCMI_LIB_NAMESPACE_BEGIN + +/// This class represents field that may contain value of multiple different identifer types +class DLL_LINKAGE MetaIdentifier +{ + std::string entityType; + std::string stringForm; + int32_t integerForm; + + void onDeserialized(); +public: + + static const MetaIdentifier NONE; + + MetaIdentifier(); + MetaIdentifier(const std::string & entityType, const std::string & identifier); + MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value); + + template + explicit MetaIdentifier(const IdentifierType & identifier ) + : entityType(IdentifierType::entityType()) + , integerForm(identifier.getNum()) + , stringForm(IdentifierType::encode(identifier.getNum())) + { + static_assert(std::is_base_of::value, "MetaIdentifier can only be constructed from Identifer class"); + } + + int32_t getNum() const; + std::string toString() const; + + template + IdentifierType as() const + { + static_assert(std::is_base_of::value, "MetaIdentifier can only be converted to Identifer class"); + IdentifierType result(integerForm); + return result; + } + + template void serialize(Handler &h, const int version) + { + h & stringForm; + + if (!h.saving) + onDeserialized(); + } + + bool operator == (const MetaIdentifier & other) const; + bool operator != (const MetaIdentifier & other) const; + bool operator < (const MetaIdentifier & other) const; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index b60d28207..51ecaf4f6 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -16,6 +16,7 @@ #include "../CConfigHandler.h" #include "../GameSettings.h" #include "../IGameCallback.h" +#include "../bonuses/BonusSubtypes.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8461335d6..ba3c6896e 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -37,6 +37,7 @@ #include "../modding/ModScope.h" #include "../constants/StringConstants.h" #include "../battle/Unit.h" +#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index c1011c015..c048fbec4 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -15,6 +15,7 @@ #include "../NetPacks.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" +#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 61a179f23..68bb9f1ab 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -13,6 +13,7 @@ #include "CGTownBuilding.h" #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" +#include "../bonuses/BonusSubtypes.h" #include "../battle/IBattleInfoCallback.h" #include "../NetPacks.h" #include "../CConfigHandler.h" diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 3bc766b83..b27f2a2f1 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -25,6 +25,7 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../modding/ModScope.h" +#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 6fad68381..3d805847e 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -71,6 +71,11 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const return true; } +bool TurnInfo::hasBonusOfType(BonusType type) const +{ + return hasBonusOfType(type, TBonusSubtype::NONE); +} + bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const { switch(type) @@ -89,6 +94,11 @@ bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const bonuses->getFirst(Selector::type()(type).And(Selector::subtype()(subtype)))); } +int TurnInfo::valOfBonuses(BonusType type) const +{ + return valOfBonuses(type, TBonusSubtype::NONE); +} + int TurnInfo::valOfBonuses(BonusType type, TBonusSubtype subtype) const { switch(type) diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index b26889284..8acb6f6fd 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -17,6 +17,7 @@ #include "../battle/Unit.h" #include "../bonuses/BonusParams.h" #include "../bonuses/BonusList.h" +#include "../bonuses/BonusSubtypes.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModUtility.h" diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 4fa653fd4..37932a107 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -20,6 +20,7 @@ #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/BattleAction.h" +#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/NetPacks.h" #include "../../lib/spells/AbilityCaster.h" diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 245d25851..53fc7f31f 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -16,6 +16,7 @@ #include "../../lib/serializer/Connection.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" +#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameConstants.h" @@ -23,8 +24,8 @@ #include "../../lib/StartInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" -#include "../lib/modding/IdentifierStorage.h" -#include "../lib/modding/ModScope.h" +#include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/modding/ModScope.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 47ef88928..e7b61ceb5 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -12,6 +12,7 @@ #include "mock/mock_BonusBearer.h" #include "mock/mock_UnitInfo.h" #include "mock/mock_UnitEnvironment.h" +#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/battle/CUnitState.h" #include "../../lib/CCreatureHandler.h" diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index 0c48181fb..2399c14de 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "TargetConditionItemFixture.h" +#include "../../../lib/bonuses/BonusSubtypes.h" //FIXME: Orb of vulnerability mechanics is not such trivial (mantis issue 1791) //TODO: NEGATE_ALL_NATURAL_IMMUNITIES special cases: dispel, chain lightning From b394158dc9278da97165d7cf4a4afb8214249892 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 10 Oct 2023 18:05:18 +0300 Subject: [PATCH 0906/1248] Bonus Source ID now uses metaidentifier --- AI/Nullkiller/Analyzers/HeroManager.cpp | 4 +- client/NetPacksClient.cpp | 4 +- client/battle/BattleStacksController.cpp | 4 +- client/battle/BattleWindow.cpp | 2 +- client/windows/CCreatureWindow.cpp | 8 ++-- docs/modders/Game_Identifiers.md | 4 +- lib/ArtifactUtils.cpp | 2 +- lib/BattleFieldHandler.cpp | 2 +- lib/CArtHandler.cpp | 2 +- lib/CCreatureHandler.cpp | 12 +++--- lib/CHeroHandler.cpp | 2 +- lib/CSkillHandler.cpp | 2 +- lib/CStack.cpp | 10 ++--- lib/CStack.h | 2 +- lib/CTownHandler.cpp | 30 +++++++++++++-- lib/CTownHandler.h | 5 +-- lib/JsonNode.cpp | 12 +++--- lib/NetPacks.h | 4 +- lib/NetPacksLib.cpp | 4 +- lib/battle/BattleInfo.cpp | 8 ++-- lib/battle/CBattleInfoCallback.cpp | 2 +- lib/battle/CUnitState.cpp | 4 +- lib/battle/DamageCalculator.cpp | 2 +- lib/bonuses/Bonus.cpp | 26 ++++++------- lib/bonuses/Bonus.h | 27 ++++--------- lib/bonuses/BonusSelector.cpp | 6 +-- lib/bonuses/BonusSelector.h | 4 +- lib/bonuses/IBonusBearer.cpp | 6 +-- lib/bonuses/IBonusBearer.h | 2 +- lib/bonuses/Limiters.cpp | 4 +- lib/bonuses/Limiters.h | 2 +- lib/campaign/CampaignConstants.h | 8 +--- lib/constants/EntityIdentifiers.cpp | 1 + lib/constants/EntityIdentifiers.h | 38 ++++++++++++++++--- lib/constants/MetaIdentifier.cpp | 6 +-- lib/constants/MetaIdentifier.h | 9 ++--- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameStateCampaign.cpp | 4 +- .../CRewardableConstructor.cpp | 2 +- lib/mapObjects/CArmedInstance.cpp | 4 +- lib/mapObjects/CBank.cpp | 4 +- lib/mapObjects/CGHeroInstance.cpp | 6 +-- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CGTownBuilding.cpp | 13 ++++--- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/CRewardableObject.cpp | 4 +- lib/mapObjects/MiscObjects.cpp | 10 ++--- lib/mapping/MapFormatH3M.cpp | 8 ++-- lib/spells/AdventureSpellMechanics.cpp | 4 +- lib/spells/BattleSpellMechanics.cpp | 2 +- lib/spells/CSpellHandler.cpp | 4 +- lib/spells/ISpellMechanics.cpp | 4 +- lib/spells/TargetCondition.cpp | 2 +- lib/spells/effects/Clone.cpp | 2 +- lib/spells/effects/Dispel.cpp | 2 +- lib/spells/effects/Moat.cpp | 4 +- lib/spells/effects/Timed.cpp | 6 +-- mapeditor/inspector/rewardswidget.cpp | 2 +- server/battles/BattleActionProcessor.cpp | 6 +-- server/battles/BattleFlowProcessor.cpp | 2 +- server/processors/PlayerMessageProcessor.cpp | 2 +- test/battle/CBattleInfoCallbackTest.cpp | 20 +++++----- test/battle/CHealthTest.cpp | 4 +- test/battle/CUnitStateMagicTest.cpp | 12 +++--- test/battle/CUnitStateTest.cpp | 30 +++++++-------- test/entity/CCreatureTest.cpp | 12 +++--- test/spells/AbilityCasterTest.cpp | 4 +- test/spells/effects/CloneTest.cpp | 2 +- test/spells/effects/DamageTest.cpp | 6 +-- test/spells/effects/DispelTest.cpp | 14 +++---- test/spells/effects/HealTest.cpp | 10 ++--- test/spells/effects/SacrificeTest.cpp | 4 +- test/spells/effects/SummonTest.cpp | 4 +- test/spells/effects/TimedTest.cpp | 6 +-- .../AbsoluteLevelConditionTest.cpp | 8 ++-- .../AbsoluteSpellConditionTest.cpp | 4 +- .../targetConditions/BonusConditionTest.cpp | 4 +- .../ElementalConditionTest.cpp | 10 ++--- .../ImmunityNegationConditionTest.cpp | 4 +- .../NormalLevelConditionTest.cpp | 6 +-- .../NormalSpellConditionTest.cpp | 4 +- .../ReceptiveFeatureConditionTest.cpp | 2 +- .../SpellEffectConditionTest.cpp | 6 +-- 85 files changed, 295 insertions(+), 262 deletions(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index cf6621a21..037a50b2d 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -71,7 +71,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const { - auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, hero->type->getIndex()); + auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, TBonusSourceID(hero->type->getId())); auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL); auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus)); auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL)); @@ -83,7 +83,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const if(hasBonus) { - SecondarySkill bonusSkill = SecondarySkill(bonus->sid); + SecondarySkill bonusSkill = bonus->sid.as(); float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill); if(bonusScore > 0) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 483bb1126..af10003b4 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -433,14 +433,14 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) { case GiveBonus::ETarget::HERO: { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); + const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.whoID)); callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); } break; case GiveBonus::ETarget::PLAYER: { //const PlayerState *p = gs.getPlayerState(pack.id); - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); + callInterfaceIfPresent(cl, PlayerColor(pack.whoID), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); } break; } diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index a6308dfb4..0f5b24c18 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -265,7 +265,7 @@ bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const std::shared_ptr BattleStacksController::getStackAmountBox(const CStack * stack) { - std::vector activeSpells = stack->activeSpells(); + std::vector activeSpells = stack->activeSpells(); if ( activeSpells.empty()) return amountNormal; @@ -798,7 +798,7 @@ void BattleStacksController::removeExpiredColorFilters() { if (!filter.persistent) { - if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, filter.source->id), Selector::all)) + if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(filter.source->id)), Selector::all)) return true; if (filter.effect == ColorFilter::genEmptyShifter()) return true; diff --git a/client/battle/BattleWindow.cpp b/client/battle/BattleWindow.cpp index 81c5b12f4..6cb1d6874 100644 --- a/client/battle/BattleWindow.cpp +++ b/client/battle/BattleWindow.cpp @@ -548,7 +548,7 @@ void BattleWindow::bSpellf() if (blockingBonus->source == BonusSource::ARTIFACT) { - const auto artID = ArtifactID(blockingBonus->sid); + const auto artID = blockingBonus->sid.as(); //If we have artifact, put name of our hero. Otherwise assume it's the enemy. //TODO check who *really* is source of bonus std::string heroName = myHero->hasArt(artID) ? myHero->getNameTranslated() : owner.enemyHero().name; diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index c83d04d38..cc5c7a253 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -208,10 +208,10 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int //spell effects int printed=0; //how many effect pics have been printed - std::vector spells = battleStack->activeSpells(); - for(si32 effect : spells) + std::vector spells = battleStack->activeSpells(); + for(SpellID effect : spells) { - const spells::Spell * spell = CGI->spells()->getByIndex(effect); + const spells::Spell * spell = CGI->spells()->getById(effect); std::string spellText; @@ -224,7 +224,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." boost::replace_first(spellText, "%s", spell->getNameTranslated()); //FIXME: support permanent duration - int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT,effect))->turnsRemain; + int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(effect)))->turnsRemain; boost::replace_first(spellText, "%d", std::to_string(duration)); spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); diff --git a/docs/modders/Game_Identifiers.md b/docs/modders/Game_Identifiers.md index be3ae9865..14c3742e6 100644 --- a/docs/modders/Game_Identifiers.md +++ b/docs/modders/Game_Identifiers.md @@ -548,7 +548,7 @@ This is a list of all game identifiers available to modders. Note that only iden ### primSkill -Deprected, please use primarySkill instead +Deprecated, please use primarySkill instead - primSkill.attack - primSkill.defence @@ -619,7 +619,7 @@ Deprected, please use primarySkill instead ### skill -Deprected, please use secondarySkill instead +Deprecated, please use secondarySkill instead - skill.airMagic - skill.archery diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index b634dfd04..9fd4749f0 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -145,7 +145,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) { auto ret = new CArtifactInstance(VLC->arth->objects[ArtifactID::SPELL_SCROLL]); auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, - BonusSource::ARTIFACT_INSTANCE, -1, ArtifactID::SPELL_SCROLL, TBonusSubtype(sid)); + BonusSource::ARTIFACT_INSTANCE, -1, TBonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), TBonusSubtype(sid)); ret->addNewBonus(bonus); return ret; } diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index 9c559ffe7..a9393af87 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -29,7 +29,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto bonus = JsonUtils::parseBonus(b); bonus->source = BonusSource::TERRAIN_OVERLAY; - bonus->sid = info->getIndex(); + bonus->sid = TBonusSourceID(info->getId()); bonus->duration = BonusDuration::ONE_BATTLE; info->bonuses.push_back(bonus); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index b297198f3..a1f11cfb6 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -751,7 +751,7 @@ void CArtHandler::afterLoadFinalization() { assert(art == objects[art->id]); assert(bonus->source == BonusSource::ARTIFACT); - bonus->sid = art->id; + bonus->sid = TBonusSourceID(art->id); } } CBonusSystemNode::treeHasChanged(); diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 46f0cfd0e..b1cc82421 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -299,7 +299,7 @@ void CCreature::addBonus(int val, BonusType type) void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) { - auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, getIndex())); + auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, TBonusSourceID(getId()))); BonusList & exported = getExportedBonusList(); BonusList existing; @@ -307,7 +307,7 @@ void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) if(existing.empty()) { - auto added = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, getIndex(), subtype, BonusValueType::BASE_NUMBER); + auto added = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, TBonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER); addNewBonus(added); } else @@ -791,9 +791,9 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) } do //parse everything that's left { - auto sid = static_cast(parser.readNumber()); //id = this particular creature ID + CreatureID sid = static_cast(parser.readNumber()); //id = this particular creature ID - b.sid = sid; + b.sid = TBonusSourceID(sid); bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) @@ -899,7 +899,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c { auto b = JsonUtils::parseBonus(ability.second); b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); + b->sid = TBonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } @@ -917,7 +917,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c { auto b = JsonUtils::parseBonus(ability); b->source = BonusSource::CREATURE_ABILITY; - b->sid = creature->getIndex(); + b->sid = TBonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 725979c6b..4fd22e1bd 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -605,7 +605,7 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) { bonus->duration = BonusDuration::PERMANENT; bonus->source = BonusSource::HERO_SPECIAL; - bonus->sid = hero->getIndex(); + bonus->sid = TBonusSourceID(hero->getId()); return bonus; }; diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 355a44198..68ee05294 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -93,7 +93,7 @@ SecondarySkill CSkill::getId() const void CSkill::addNewBonus(const std::shared_ptr & b, int level) { b->source = BonusSource::SECONDARY_SKILL; - b->sid = id; + b->sid = TBonusSourceID(id); b->duration = BonusDuration::PERMANENT; b->description = getNameTranslated(); levels[level-1].effects.push_back(b); diff --git a/lib/CStack.cpp b/lib/CStack.cpp index 3256a17bf..b776aed2a 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -121,23 +121,23 @@ BattleHex::EDir CStack::destShiftDir() const } } -std::vector CStack::activeSpells() const +std::vector CStack::activeSpells() const { - std::vector ret; + std::vector ret; std::stringstream cachingStr; cachingStr << "!type_" << vstd::to_underlying(BonusType::NONE) << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT); CSelector selector = Selector::sourceType()(BonusSource::SPELL_EFFECT) .And(CSelector([](const Bonus * b)->bool { - return b->type != BonusType::NONE && SpellID(b->sid).toSpell() && !SpellID(b->sid).toSpell()->isAdventure(); + return b->type != BonusType::NONE && b->sid.as().toSpell() && !b->sid.as().toSpell()->isAdventure(); })); TConstBonusListPtr spellEffects = getBonuses(selector, Selector::all, cachingStr.str()); for(const auto & it : *spellEffects) { - if(!vstd::contains(ret, it->sid)) //do not duplicate spells with multiple effects - ret.push_back(it->sid); + if(!vstd::contains(ret, it->sid.as())) //do not duplicate spells with multiple effects + ret.push_back(it->sid.as()); } return ret; diff --git a/lib/CStack.h b/lib/CStack.h index a2f944a8d..2a89822ba 100644 --- a/lib/CStack.h +++ b/lib/CStack.h @@ -58,7 +58,7 @@ public: ui32 level() const; si32 magicResistance() const override; //include aura of resistance - std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast + std::vector activeSpells() const; //returns vector of active spell IDs sorted by time of cast const CGHeroInstance * getMyHero() const; //if stack belongs to hero (directly or was by him summoned) returns hero, nullptr otherwise static std::vector meleeAttackHexes(const battle::Unit * attacker, const battle::Unit * defender, BattleHex attackerPos = BattleHex::INVALID, BattleHex defenderPos = BattleHex::INVALID); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index f37f02d59..4cee0e923 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -50,6 +50,30 @@ const std::map CBuilding::TOWER_TYPES = { "skyship", CBuilding::HEIGHT_SKYSHIP } }; +BuildingTypeUniqueID::BuildingTypeUniqueID(FactionID factionID, BuildingID buildingID ): + BuildingTypeUniqueID(factionID.getNum() * 0x10000 + buildingID.getNum()) +{ + assert(factionID.getNum() >= 0); + assert(factionID.getNum() < 0x10000); + assert(buildingID.getNum() >= 0); + assert(buildingID.getNum() < 0x10000); +} + +BuildingID BuildingTypeUniqueID::getBuilding() const +{ + return BuildingID(getNum() % 0x10000); +} + +FactionID BuildingTypeUniqueID::getFaction() const +{ + return FactionID(getNum() / 0x10000); +} + +const BuildingTypeUniqueID CBuilding::getUniqueTypeID() const +{ + return BuildingTypeUniqueID(town->faction->getId(), bid); +} + std::string CBuilding::getJsonKey() const { return modScope + ':' + identifier;; @@ -569,7 +593,7 @@ std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building const std::string & description, TBonusSubtype subtype) const { - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, building, subtype, description); + auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, TBonusSourceID(building), subtype, description); if(prop) b->addPropagator(prop); @@ -586,7 +610,7 @@ void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList if(bonus == nullptr) continue; - bonus->sid = Bonus::getSid32(building->town->faction->getIndex(), building->bid); + bonus->sid = TBonusSourceID(building->getUniqueTypeID()); //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. if(bonus->propagator != nullptr && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) @@ -650,7 +674,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; for(auto & bonus : ret->onVisitBonuses) - bonus->sid = Bonus::getSid32(ret->town->faction->getIndex(), ret->bid); + bonus->sid = TBonusSourceID(ret->getUniqueTypeID()); } if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index a781aaa5e..cf1b793ff 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -37,9 +37,6 @@ class JsonSerializeFormat; /// a typical building encountered in every castle ;] /// this is structure available to both client and server /// contains all mechanics-related data about town structures - - - class DLL_LINKAGE CBuilding { std::string modScope; @@ -84,6 +81,8 @@ public: CBuilding() : town(nullptr), mode(BUILD_NORMAL) {}; + const BuildingTypeUniqueID getUniqueTypeID() const; + std::string getJsonKey() const; std::string getNameTranslated() const; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 26f56bb6a..33cd9e0f5 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -439,9 +439,9 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json }); } -static void loadBonusSourceInstance(int32_t & sourceInstance, BonusSource sourceType, const JsonNode & node) +static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) { - + assert(0); } std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) @@ -741,7 +741,7 @@ std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, c source = BonusSource::TOWN_STRUCTURE bonusType, val, subtype - get from json */ - auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, building, description); + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, TBonusSourceID(building), description); if(!parseBonus(ability, b.get())) return nullptr; @@ -858,8 +858,6 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) b->turnsRemain = static_cast(ability["turns"].Float()); - b->sid = static_cast(ability["sourceID"].Float()); - if(!ability["description"].isNull()) { if (ability["description"].isString()) @@ -897,6 +895,8 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) if (!value->isNull()) b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); + loadBonusSourceInstance(b->sid, b->source, ability["sourceID"]); + value = &ability["targetSourceType"]; if (!value->isNull()) b->targetSourceType = static_cast(parseByMap(bonusSourceMap, value, "target type ")); @@ -978,7 +978,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) } value = &ability["sourceType"]; std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized - std::optional id = std::nullopt; + std::optional id = std::nullopt; if(value->isString()) { auto it = bonusSourceMap.find(value->String()); diff --git a/lib/NetPacks.h b/lib/NetPacks.h index 6bc6e3f9d..fb90861af 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -496,8 +496,8 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient ui32 whoID = 0; //hero, town or player id - whoever loses bonus //vars to identify bonus: its source - ui8 source = 0; - ui32 id = 0; //source id + BonusSource source; + TBonusSourceID id; //source id //used locally: copy of removed bonus Bonus bonus; diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 817565efa..611c042d4 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -1119,7 +1119,7 @@ void RemoveBonus::applyGs(CGameState *gs) for(const auto & b : bonuses) { - if(vstd::to_underlying(b->source) == source && b->sid == id) + if(b->source == source && b->sid == id) { bonus = *b; //backup bonus (to show to interfaces later) node->removeBonus(b); @@ -2201,7 +2201,7 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const } case BonusType::POISON: { - auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON) + auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSubtype(SpellID(SpellID::POISON))) .And(Selector::type()(BonusType::STACK_HEALTH))); if (b) b->val = val; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 368bfbcc5..478e8d9e2 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -442,9 +442,9 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const //native terrain bonuses static auto nativeTerrain = std::make_shared(); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, 0)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, 0, TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE)->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics @@ -802,7 +802,7 @@ void BattleInfo::setUnitState(uint32_t id, const JsonNode & data, int64_t health auto selector = [](const Bonus * b) { //Special case: DISRUPTING_RAY is absolutely permanent - return b->source == BonusSource::SPELL_EFFECT && b->sid != SpellID::DISRUPTING_RAY; + return b->source == BonusSource::SPELL_EFFECT && b->sid.as() != SpellID::DISRUPTING_RAY; }; changedStack->removeBonusesRecursive(selector); } diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 5dac8edc8..9cddd17b6 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1658,7 +1658,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; - if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, spellID), Selector::all, cachingStr.str()) + if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(spellID)), Selector::all, cachingStr.str()) //TODO: this ability has special limitations || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) continue; diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 3b87e7b0a..d9e05c900 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -347,7 +347,7 @@ CUnitState::CUnitState(): attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)), 0), defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), - cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, SpellID::CLONE))), + cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::CLONE))))), cloneID(-1) { @@ -513,7 +513,7 @@ bool CUnitState::isGhost() const bool CUnitState::isFrozen() const { - return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, SpellID::STONE_GAZE), Selector::all); + return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::STONE_GAZE))), Selector::all); } bool CUnitState::isValidTarget(bool allowDead) const diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 11d542b7c..cd99a191a 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -314,7 +314,7 @@ double DamageCalculator::getDefenseRangePenaltiesFactor() const auto isAdvancedAirShield = [](const Bonus* bonus) { return bonus->source == BonusSource::SPELL_EFFECT - && bonus->sid == SpellID::AIR_SHIELD + && bonus->sid == TBonusSourceID(SpellID(SpellID::AIR_SHIELD)) && bonus->val >= MasteryLevel::ADVANCED; }; diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 67f300912..92431ec5c 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -100,19 +100,19 @@ std::string Bonus::Description(std::optional customValue) const switch(source) { case BonusSource::ARTIFACT: - str << ArtifactID(sid).toArtifact(VLC->artifacts())->getNameTranslated(); + str << sid.as().toArtifact(VLC->artifacts())->getNameTranslated(); break; case BonusSource::SPELL_EFFECT: - str << SpellID(sid).toSpell(VLC->spells())->getNameTranslated(); + str << sid.as().toSpell(VLC->spells())->getNameTranslated(); break; case BonusSource::CREATURE_ABILITY: - str << CreatureID(sid).toCreature(VLC->creatures())->getNamePluralTranslated(); + str << sid.as().toCreature(VLC->creatures())->getNamePluralTranslated(); break; case BonusSource::SECONDARY_SKILL: - str << VLC->skills()->getByIndex(sid)->getNameTranslated(); + str << VLC->skills()->getById(sid.as())->getNameTranslated(); break; case BonusSource::HERO_SPECIAL: - str << VLC->heroTypes()->getByIndex(sid)->getNameTranslated(); + str << VLC->heroTypes()->getById(sid.as())->getNameTranslated(); break; default: //todo: handle all possible sources @@ -158,8 +158,8 @@ JsonNode Bonus::toJsonNode() const root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); if(targetSourceType != BonusSource::OTHER) root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); - if(sid != 0) - root["sourceID"].Integer() = sid; + if(sid != TBonusSourceID::NONE) + root["sourceID"].String() = sid.toString(); if(val != 0) root["val"].Integer() = val; if(valType != BonusValueType::ADDITIVE_VALUE) @@ -183,19 +183,19 @@ JsonNode Bonus::toJsonNode() const return root; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID) : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, std::string()) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, std::string Desc) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, std::string Desc) : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, Desc) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype) : Bonus(Duration, Type, Src, Val, ID, Subtype, std::string()) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype, std::string Desc): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype, std::string Desc): duration(Duration), type(Type), subtype(Subtype), @@ -208,7 +208,7 @@ Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 targetSourceType = BonusSource::OTHER; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 ID, TBonusSubtype Subtype, BonusValueType ValType): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype, BonusValueType ValType): duration(Duration), type(Type), subtype(Subtype), @@ -239,7 +239,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const Bonus &bonus) out << "\tSubtype: " << bonus.subtype.toString() << "\n"; printField(duration.to_ulong()); printField(source); - printField(sid); + out << "\tSource ID: " << bonus.sid.toString() << "\n"; if(bonus.additionalInfo != CAddInfo::NONE) out << "\taddInfo: " << bonus.additionalInfo.toString() << "\n"; printField(turnsRemain); diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 2a47aa6cc..029fe78af 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -24,6 +24,7 @@ class BonusList; class CSelector; using TBonusSubtype = MetaIdentifier; +using TBonusSourceID = MetaIdentifier; using TBonusListPtr = std::shared_ptr; using TConstBonusListPtr = std::shared_ptr; using TLimiterPtr = std::shared_ptr; @@ -62,7 +63,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. si32 val = 0; - ui32 sid = 0; //source id: id of object/artifact/spell + TBonusSourceID sid; //source id: id of object/artifact/spell BonusValueType valType = BonusValueType::ADDITIVE_VALUE; std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus) @@ -76,11 +77,11 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this std::string description; - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, std::string Desc); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype, std::string Desc); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, ui32 sourceID, TBonusSubtype subtype, BonusValueType ValType); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype, BonusValueType ValType); Bonus() = default; template void serialize(Handler &h, const int version) @@ -167,20 +168,6 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this { val += Val; } - STRONG_INLINE static ui32 getSid32(ui32 high, ui32 low) - { - return (high << 16) + low; - } - - STRONG_INLINE static ui32 getHighFromSid32(ui32 sid) - { - return sid >> 16; - } - - STRONG_INLINE static ui32 getLowFromSid32(ui32 sid) - { - return sid & 0x0000FFFF; - } std::string Description(std::optional customValue = {}) const; JsonNode toJsonNode() const; diff --git a/lib/bonuses/BonusSelector.cpp b/lib/bonuses/BonusSelector.cpp index bfc4dab86..42dae43db 100644 --- a/lib/bonuses/BonusSelector.cpp +++ b/lib/bonuses/BonusSelector.cpp @@ -66,10 +66,10 @@ namespace Selector .And(CSelectFieldEqual(&Bonus::additionalInfo)(info)); } - CSelector DLL_LINKAGE source(BonusSource source, ui32 sourceID) + CSelector DLL_LINKAGE source(BonusSource source, TBonusSourceID sourceID) { return CSelectFieldEqual(&Bonus::source)(source) - .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); + .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); } CSelector DLL_LINKAGE sourceTypeSel(BonusSource source) @@ -86,4 +86,4 @@ namespace Selector DLL_LINKAGE CSelector none([](const Bonus * b){return false;}); } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSelector.h b/lib/bonuses/BonusSelector.h index b74710539..5719fdaf0 100644 --- a/lib/bonuses/BonusSelector.h +++ b/lib/bonuses/BonusSelector.h @@ -136,7 +136,7 @@ namespace Selector CSelector DLL_LINKAGE typeSubtype(BonusType Type, TBonusSubtype Subtype); CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, TBonusSubtype subtype, const CAddInfo & info); - CSelector DLL_LINKAGE source(BonusSource source, ui32 sourceID); + CSelector DLL_LINKAGE source(BonusSource source, TBonusSourceID sourceID); CSelector DLL_LINKAGE sourceTypeSel(BonusSource source); CSelector DLL_LINKAGE valueType(BonusValueType valType); @@ -153,4 +153,4 @@ namespace Selector extern DLL_LINKAGE CSelector none; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 33fccaf67..01f229b32 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -82,10 +82,10 @@ bool IBonusBearer::hasBonusOfType(BonusType type, TBonusSubtype subtype) const return hasBonus(s, cachingStr); } -bool IBonusBearer::hasBonusFrom(BonusSource source, ui32 sourceID) const +bool IBonusBearer::hasBonusFrom(BonusSource source, TBonusSourceID sourceID) const { - boost::format fmt("source_%did_%d"); - fmt % static_cast(source) % sourceID; + boost::format fmt("source_%did_%s"); + fmt % static_cast(source) % sourceID.toString(); return hasBonus(Selector::source(source,sourceID), fmt.str()); } diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index 98bd4a890..d0edac056 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -37,7 +37,7 @@ public: bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype) int valOfBonuses(BonusType type, TBonusSubtype subtype) const; //subtype -> subtype of bonus; bool hasBonusOfType(BonusType type, TBonusSubtype subtype) const;//determines if hero has a bonus of given type (and optionally subtype) - bool hasBonusFrom(BonusSource source, ui32 sourceID) const; + bool hasBonusFrom(BonusSource source, TBonusSourceID sourceID) const; virtual int64_t getTreeVersion() const = 0; }; diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index 6a543a2f6..e937f9934 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -303,10 +303,10 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) switch(context.b.source) { case BonusSource::CREATURE_ABILITY: - return bearer->getFaction() == CreatureID(context.b.sid).toCreature()->getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFaction() == context.b.sid.as().toCreature()->getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; case BonusSource::TOWN_STRUCTURE: - return bearer->getFaction() == FactionID(Bonus::getHighFromSid32(context.b.sid)) ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; + return bearer->getFaction() == context.b.sid.as().getFaction() ? ILimiter::EDecision::ACCEPT : ILimiter::EDecision::DISCARD; //TODO: other sources of bonuses } diff --git a/lib/bonuses/Limiters.h b/lib/bonuses/Limiters.h index b4a27c7b8..9c4b58924 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -118,7 +118,7 @@ public: BonusType type; TBonusSubtype subtype; BonusSource source; - si32 sid; + TBonusSourceID sid; bool isSubtypeRelevant; //check for subtype only if this is true bool isSourceRelevant; //check for bonus source only if this is true bool isSourceIDRelevant; //check for bonus source only if this is true diff --git a/lib/campaign/CampaignConstants.h b/lib/campaign/CampaignConstants.h index cc31d1f78..d8469a566 100644 --- a/lib/campaign/CampaignConstants.h +++ b/lib/campaign/CampaignConstants.h @@ -11,6 +11,8 @@ VCMI_LIB_NAMESPACE_BEGIN +#include "../constants/EntityIdentifiers.h" + enum class CampaignVersion : uint8_t { NONE = 0, @@ -48,10 +50,4 @@ enum class CampaignBonusType : int8_t HERO }; -enum class CampaignScenarioID : int8_t -{ - NONE = -1, - // no members - fake enum to create integer type that is not implicitly convertible to int -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index be491aae3..116f31d83 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -40,6 +40,7 @@ VCMI_LIB_NAMESPACE_BEGIN +const CampaignScenarioID CampaignScenarioID::NONE(-1); const BattleID BattleID::NONE(-1); const QueryID QueryID::NONE(-1); const QueryID QueryID::CLIENT(-2); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 80db1b81c..d999b76da 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -157,11 +157,14 @@ public: using Identifier::Identifier; DLL_LINKAGE static const BattleID NONE; }; -class ObjectInstanceID : public Identifier +class DLL_LINKAGE ObjectInstanceID : public Identifier { public: using Identifier::Identifier; - DLL_LINKAGE static const ObjectInstanceID NONE; + static const ObjectInstanceID NONE; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); }; class HeroClassID : public Identifier @@ -292,6 +295,8 @@ class SecondarySkill : public IdentifierWithEnum::IdentifierWithEnum; static std::string entityType(); + static si32 decode(const std::string& identifier); + static std::string encode(const si32 index); }; class DLL_LINKAGE PrimarySkill : public Identifier @@ -396,6 +401,10 @@ class BuildingID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + DLL_LINKAGE static si32 decode(const std::string & identifier); + DLL_LINKAGE static std::string encode(const si32 index); + static std::string entityType(); }; class ObjBase : public IdentifierBase @@ -814,13 +823,13 @@ public: }; class BattleFieldInfo; -class BattleField : public Identifier +class DLL_LINKAGE BattleField : public Identifier { public: using Identifier::Identifier; - DLL_LINKAGE static const BattleField NONE; - DLL_LINKAGE const BattleFieldInfo * getInfo() const; + static const BattleField NONE; + const BattleFieldInfo * getInfo() const; }; class DLL_LINKAGE BoatId : public Identifier @@ -921,6 +930,25 @@ public: static std::string entityType(); }; +class BuildingTypeUniqueID : public Identifier +{ +public: + BuildingTypeUniqueID(FactionID faction, BuildingID building ); + + BuildingID getBuilding() const; + FactionID getFaction() const; + + using Identifier::Identifier; +}; + +class DLL_LINKAGE CampaignScenarioID : public Identifier +{ +public: + using Identifier::Identifier; + + static const CampaignScenarioID NONE; +}; + // Deprecated // TODO: remove using ETownType = FactionID; diff --git a/lib/constants/MetaIdentifier.cpp b/lib/constants/MetaIdentifier.cpp index 140a6a558..a15cc74c6 100644 --- a/lib/constants/MetaIdentifier.cpp +++ b/lib/constants/MetaIdentifier.cpp @@ -20,16 +20,14 @@ MetaIdentifier::MetaIdentifier(): {} MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier) - : entityType(entityType) - , stringForm(identifier) + : stringForm(identifier) , integerForm(-1) { onDeserialized(); } MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value) - : entityType(entityType) - , stringForm(identifier) + : stringForm(identifier) , integerForm(value) { } diff --git a/lib/constants/MetaIdentifier.h b/lib/constants/MetaIdentifier.h index 785338f41..c9149348f 100644 --- a/lib/constants/MetaIdentifier.h +++ b/lib/constants/MetaIdentifier.h @@ -16,7 +16,6 @@ VCMI_LIB_NAMESPACE_BEGIN /// This class represents field that may contain value of multiple different identifer types class DLL_LINKAGE MetaIdentifier { - std::string entityType; std::string stringForm; int32_t integerForm; @@ -30,10 +29,8 @@ public: MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value); template - explicit MetaIdentifier(const IdentifierType & identifier ) - : entityType(IdentifierType::entityType()) - , integerForm(identifier.getNum()) - , stringForm(IdentifierType::encode(identifier.getNum())) + explicit MetaIdentifier(const IdentifierType & identifier) + : integerForm(identifier.getNum()) { static_assert(std::is_base_of::value, "MetaIdentifier can only be constructed from Identifer class"); } @@ -51,7 +48,7 @@ public: template void serialize(Handler &h, const int version) { - h & stringForm; + h & integerForm; if (!h.saving) onDeserialized(); diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index ed7a94105..3a911cdef 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -655,7 +655,7 @@ void CGameState::initGlobalBonuses() { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::GLOBAL;//for all - bonus->sid = -1; //there is one global object + bonus->sid = TBonusSourceID::NONE; //there is one global object globalEffects.addNewBonus(bonus); } VLC->creh->loadCrExpBon(globalEffects); diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index a338cf61b..0c35b9511 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -314,8 +314,8 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) if(val == 0) continue; - int currentScenario = static_cast(*gameState->scenarioOps->campState->currentScenario()); - auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, currentScenario, TBonusSubtype(g) ); + auto currentScenario = *gameState->scenarioOps->campState->currentScenario(); + auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, TBonusSourceID(currentScenario), TBonusSubtype(g) ); hero->addNewBonus(bb); } break; diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index ad87c06fe..af68c31a3 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -50,7 +50,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG for (auto & bonus : rewardInfo.reward.bonuses) { bonus.source = BonusSource::OBJECT; - bonus.sid = rewardableObject->ID; + bonus.sid = TBonusSourceID(rewardableObject->ID); } } assert(!rewardableObject->configuration.info.empty()); diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index bc13a206d..7d88da3ab 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -59,7 +59,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, -1); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID::NONE); addNewBonus(b); } @@ -120,7 +120,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() CBonusSystemNode::treeHasChanged(); //-1 modifier for any Undead unit in army - const ui8 UNDEAD_MODIFIER_ID = -2; + const TBonusSourceID UNDEAD_MODIFIER_ID( "", "", -2); auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, UNDEAD_MODIFIER_ID)); if(hasUndead) { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 7ff668945..01401fd77 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -213,7 +213,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const gbonus.id = hero->id.getNum(); gbonus.bonus.duration = BonusDuration::ONE_BATTLE; gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = ID; + gbonus.bonus.sid = TBonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; switch (ID) @@ -239,7 +239,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const case Obj::PYRAMID: { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, id.getNum(), VLC->generaltexth->arraytxt[70]); + gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, TBonusSourceID(id), VLC->generaltexth->arraytxt[70]); gb.id = hero->id.getNum(); cb->giveHeroBonus(&gb); textID = 107; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ba3c6896e..fd2d12f47 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -370,7 +370,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::HERO_BASE_SKILL; - bonus->sid = id.getNum(); + bonus->sid = TBonusSourceID(id); bonus->duration = BonusDuration::PERMANENT; addNewBonus(bonus); } @@ -590,7 +590,7 @@ void CGHeroInstance::recreateSecondarySkillsBonuses() void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val) { - removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, which)); + removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, TBonusSourceID(which))); auto skillBonus = (*VLC->skillh)[which]->at(val).effects; for(const auto & b : skillBonus) addNewBonus(std::make_shared(*b)); @@ -1014,7 +1014,7 @@ void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) if(hasBonus(sel)) removeBonuses(sel); - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, id.getNum(), TBonusSubtype(which))); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, TBonusSourceID(id), TBonusSubtype(which))); } EAlignment CGHeroInstance::getAlignment() const diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index e162e64c1..ffbf6d416 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -222,7 +222,7 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura gbonus.id = heroID.getNum(); gbonus.bonus.duration = duration; gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = ID; + gbonus.bonus.sid = TBonusSourceID(ID); cb->giveHeroBonus(&gbonus); } diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index e4f1a1360..38a89f73a 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -227,11 +227,11 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) int val; handler.serializeInt("morale", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(id)); handler.serializeInt("luck", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(id)); vinfo.reward.resources.serializeJson(handler, "resources"); { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index c048fbec4..e36d39784 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -144,10 +144,10 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const switch (this->bType) { case BuildingSubID::STABLES: - if(!h->hasBonusFrom(BonusSource::OBJECT, Obj::STABLES)) //does not stack with advMap Stables + if(!h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, Obj::STABLES, BonusSubtypes::heroMovementLand, VLC->generaltexth->arraytxt[100]); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, TBonusSourceID(Obj(Obj::STABLES)), BonusSubtypes::heroMovementLand, VLC->generaltexth->arraytxt[100]); gb.id = heroID.getNum(); cb->giveHeroBonus(&gb); @@ -236,7 +236,7 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const case BuildingSubID::CUSTOM_VISITING_BONUS: const auto building = town->getTown()->buildings.at(bID); - if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(building->town->faction->getIndex(), building->bid))) + if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, TBonusSourceID(building->getUniqueTypeID()))) { const auto & bonuses = building->onVisitBonuses; applyBonuses(const_cast(h), bonuses); @@ -307,7 +307,7 @@ void CTownRewardableBuilding::initObj(CRandomGenerator & rand) for (auto & bonus : rewardInfo.reward.bonuses) { bonus.source = BonusSource::TOWN_STRUCTURE; - bonus.sid = bID; + bonus.sid = TBonusSourceID(building->getUniqueTypeID()); } } } @@ -394,7 +394,10 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer case Rewardable::VISIT_PLAYER: return false; //not supported case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, Bonus::getSid32(town->town->faction->getIndex(), bID)); + { + const auto building = town->getTown()->buildings.at(bID); + return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, TBonusSourceID(building->getUniqueTypeID())); + } case Rewardable::VISIT_HERO: return visitors.find(contextHero->id) != visitors.end(); case Rewardable::VISIT_LIMITER: diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 68bb9f1ab..9cb7144e8 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -788,7 +788,7 @@ void CGTownInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, -1); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID::NONE); addNewBonus(b); } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 69eefc237..dd15cff91 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -676,9 +676,9 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(metaTypeName == "mana") reward.manaDiff = val; if(metaTypeName == "morale") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(id)); if(metaTypeName == "luck") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, id); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(id)); if(metaTypeName == "resource") { auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index f2e1a6953..9b1be30c9 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -197,7 +197,7 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con case Rewardable::VISIT_PLAYER: return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id)); case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::OBJECT, ID); + return contextHero->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID)); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: @@ -234,7 +234,7 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const switch (configuration.visitMode) { case Rewardable::VISIT_BONUS: - return h->hasBonusFrom(BonusSource::OBJECT, ID); + return h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID)); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index b27f2a2f1..8f873550d 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -988,14 +988,14 @@ void CGSirens::initObj(CRandomGenerator & rand) std::string CGSirens::getHoverText(const CGHeroInstance * hero) const { - return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT,ID)); + return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID))); } void CGSirens::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->tempOwner; - if(h->hasBonusFrom(BonusSource::OBJECT,ID)) //has already visited Sirens + if(h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID))) //has already visited Sirens { iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::ADVOB_TXT,133); @@ -1180,8 +1180,8 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const { RemoveBonus rb(GiveBonus::ETarget::PLAYER); rb.whoID = oldOwner.getNum(); - rb.source = vstd::to_underlying(BonusSource::OBJECT); - rb.id = id.getNum(); + rb.source = BonusSource::OBJECT; + rb.id = TBonusSourceID(id); cb->sendAndApply(&rb); } } @@ -1204,7 +1204,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const gb.id = player.getNum(); gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT; - gb.bonus.sid = id.getNum(); + gb.bonus.sid = TBonusSourceID(id); gb.bonus.subtype = BonusSubtypes::heroMovementSea; // FIXME: This is really dirty hack diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index cc3e1b0a5..9613331cd 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1040,9 +1040,9 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, idToBeGiven); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(idToBeGiven)); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, idToBeGiven); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(idToBeGiven)); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) @@ -2008,12 +2008,12 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), TBonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), idToBeGiven); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), TBonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::RESOURCES: diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 5c6dac7a5..8c3c0b07d 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -312,7 +312,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; - if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, owner->id), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn + if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(owner->id)), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -324,7 +324,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm GiveBonus gb; gb.id = parameters.caster->getCasterUnitId(); - gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, owner->id); + gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(owner->id)); env->apply(&gb); if(!dest->isClear(curr)) //wrong dest tile diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index cff47bbc1..e656e70e2 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -476,7 +476,7 @@ bool BattleSpellMechanics::counteringSelector(const Bonus * bonus) const for(const SpellID & id : owner->counteredSpells) { - if(bonus->sid == id.toEnum()) + if(bonus->sid.as() == id) return true; } diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 31b39997a..11fb7486f 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -925,7 +925,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = spell->id; //for all + b->sid = TBonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) @@ -940,7 +940,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = spell->id; //for all + b->sid = TBonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index a7d22fcb0..755d36f16 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -490,11 +490,11 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem source, Problem & target) con { //The %s prevents %s from casting 3rd level or higher spells. text.appendLocalString(EMetaText::GENERAL_TXT, 536); - text.replaceLocalString(EMetaText::ART_NAMES, b->sid); + text.replaceLocalString(EMetaText::ART_NAMES, b->sid.as()); caster->getCasterName(text); target.add(std::move(text), spells::Problem::NORMAL); } - else if(b && b->source == BonusSource::TERRAIN_OVERLAY && VLC->battlefields()->getByIndex(b->sid)->identifier == "cursed_ground") + else if(b && b->source == BonusSource::TERRAIN_OVERLAY && VLC->battlefields()->getById(b->sid.as())->identifier == "cursed_ground") { text.appendLocalString(EMetaText::GENERAL_TXT, 537); target.add(std::move(text), spells::Problem::NORMAL); diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index 8acb6f6fd..ffd316702 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -260,7 +260,7 @@ public: builder << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; cachingString = builder.str(); - selector = Selector::source(BonusSource::SPELL_EFFECT, spellID.num); + selector = Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(spellID)); } protected: diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index eb600091f..a443ae665 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -94,7 +94,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg SetStackEffect sse; sse.battleID = m->battle()->getBattle()->getBattleID(); - Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, SpellID::CLONE); //TODO: use special bonus type + Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(SpellID(SpellID::CLONE))); //TODO: use special bonus type lifeTimeMarker.turnsRemain = m->getEffectDuration(); std::vector buffer; buffer.push_back(lifeTimeMarker); diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 3d42872f7..19d4fe470 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -90,7 +90,7 @@ std::shared_ptr Dispel::getBonuses(const Mechanics * m, const b { if(bonus->source == BonusSource::SPELL_EFFECT) { - const Spell * sourceSpell = SpellID(bonus->sid).toSpell(m->spells()); + const Spell * sourceSpell = bonus->sid.as().toSpell(m->spells()); if(!sourceSpell) return false;//error diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 0ef725589..571481553 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -84,12 +84,12 @@ void Moat::convertBonus(const Mechanics * m, std::vector & converted) con if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL) { - nb.sid = Bonus::getSid32(m->battle()->battleGetDefendedTown()->getFaction(), BuildingID::CITADEL); + nb.sid = TBonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); nb.source = BonusSource::TOWN_STRUCTURE; } else { - nb.sid = m->getSpellIndex(); //for all + nb.sid = TBonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all } std::set flatMoatHexes; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index fb630542d..98d60ad44 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -222,14 +222,14 @@ void Timed::convertBonus(const Mechanics * m, int32_t & duration, std::vectorgetSpellIndex(); //for all + nb.sid = TBonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all //fix to original config: shield should display damage reduction - if((nb.sid == SpellID::SHIELD || nb.sid == SpellID::AIR_SHIELD) && (nb.type == BonusType::GENERAL_DAMAGE_REDUCTION)) + if((nb.sid.as() == SpellID::SHIELD || nb.sid.as() == SpellID::AIR_SHIELD) && (nb.type == BonusType::GENERAL_DAMAGE_REDUCTION)) nb.val = 100 - nb.val; //we need to know who cast Bind - else if(nb.sid == SpellID::BIND && nb.type == BonusType::BIND_EFFECT && m->caster->getHeroCaster() == nullptr) + else if(nb.sid.as() == SpellID::BIND && nb.type == BonusType::BIND_EFFECT && m->caster->getHeroCaster() == nullptr) nb.additionalInfo = m->caster->getCasterUnitId(); converted.push_back(nb); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 92f773f01..a404c582e 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -333,7 +333,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString()); auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString()); auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt(); - vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, object.id); + vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, TBonusSourceID(object.id)); } vinfo.limiter.dayOfWeek = ui->lDayOfWeek->currentIndex(); diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 37932a107..19113ca00 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -163,9 +163,9 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c SetStackEffect sse; sse.battleID = battle.getBattle()->getBattleID(); - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, -1, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE))); int oldDefenceValue = defence.totalValue(); diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index e1d8055a1..de88f2222 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -663,7 +663,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (st->hasBonusOfType(BonusType::POISON)) { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID::POISON).And(Selector::type()(BonusType::STACK_HEALTH))); + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); if (b) //TODO: what if not?... { bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 53fc7f31f..f82631f84 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -141,7 +141,7 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns ///Give all spells with bonus (to allow banned spells) GiveBonus giveBonus(GiveBonus::ETarget::HERO); giveBonus.id = hero->id.getNum(); - giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, 0); + giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, TBonusSourceID::NONE); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index e99d2d3a9..164a1f02b 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -49,7 +49,7 @@ public: void makeWarMachine() { - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, 0)); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); } void redirectBonusesToFake() @@ -331,7 +331,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToSelf) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); setDefaultExpectations(); @@ -362,7 +362,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -382,7 +382,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedAlly) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); setDefaultExpectations(); @@ -397,11 +397,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); setDefaultExpectations(); @@ -433,7 +433,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -453,7 +453,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedEnemy) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); setDefaultExpectations(); @@ -468,11 +468,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, 0)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); setDefaultExpectations(); diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index ed3a32c50..37b19f5b5 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -33,7 +33,7 @@ public: EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, TBonusSourceID::NONE)); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(UNIT_AMOUNT)); } @@ -239,7 +239,7 @@ TEST_F(HealthTest, singleUnitStack) EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, TBonusSourceID::NONE)); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(1)); diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index 52f31bdd5..3cc3c7b54 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -55,7 +55,7 @@ public: void makeNormalCaster() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, 0, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, TBonusSourceID::NONE, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); } }; @@ -63,7 +63,7 @@ TEST_F(UnitStateMagicTest, initialNormal) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, TBonusSourceID::NONE)); initUnit(); @@ -125,7 +125,7 @@ TEST_F(UnitStateMagicTest, effectPower) const int32_t EFFECT_POWER = 12 * 100; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, TBonusSourceID::NONE)); makeNormalCaster(); EXPECT_EQ(subject.getEffectPower(&spellMock), 12 * DEFAULT_AMOUNT); @@ -148,7 +148,7 @@ TEST_F(UnitStateMagicTest, enchantPower) const int32_t ENCHANT_POWER = 42; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, TBonusSourceID::NONE)); makeNormalCaster(); @@ -171,7 +171,7 @@ TEST_F(UnitStateMagicTest, effectValue) const int32_t EFFECT_VALUE = 456; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, 0, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, TBonusSourceID::NONE, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); makeNormalCaster(); EXPECT_EQ(subject.getEffectValue(&spellMock), EFFECT_VALUE * DEFAULT_AMOUNT); @@ -201,7 +201,7 @@ TEST_F(UnitStateMagicTest, spendMana) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); initUnit(); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index e7b61ceb5..8355f5e6b 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -52,12 +52,12 @@ public: void setDefaultExpectations() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, TBonusSourceID::NONE)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, 0, TBonusSubtype(PrimarySkill::ATTACK))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, 0, TBonusSubtype(PrimarySkill::DEFENSE))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, TBonusSourceID::NONE)); EXPECT_CALL(infoMock, unitBaseAmount()).WillRepeatedly(Return(DEFAULT_AMOUNT)); EXPECT_CALL(infoMock, unitType()).WillRepeatedly(Return(pikeman)); @@ -67,8 +67,8 @@ public: void makeShooter(int32_t ammo) { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, 0)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, TBonusSourceID::NONE)); } void initUnit() @@ -180,7 +180,7 @@ TEST_F(UnitStateTest, attackWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID::NONE)); int expectedAttack = static_cast(DEFAULT_ATTACK + 0.5 * DEFAULT_DEFENCE); @@ -192,7 +192,7 @@ TEST_F(UnitStateTest, defenceWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, 0)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID::NONE)); int expectedDefence = 0; @@ -205,7 +205,7 @@ TEST_F(UnitStateTest, additionalAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); bonusMock.addNewBonus(bonus); } @@ -219,7 +219,7 @@ TEST_F(UnitStateTest, additionalMeleeAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; bonusMock.addNewBonus(bonus); @@ -234,7 +234,7 @@ TEST_F(UnitStateTest, additionalRangedAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, 0); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; bonusMock.addNewBonus(bonus); @@ -249,10 +249,10 @@ TEST_F(UnitStateTest, getMinDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, BonusSubtypes::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID::NONE, BonusSubtypes::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, BonusSubtypes::creatureDamageMin); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID::NONE, BonusSubtypes::creatureDamageMin); bonusMock.addNewBonus(bonus); } @@ -265,10 +265,10 @@ TEST_F(UnitStateTest, getMaxDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, 0, BonusSubtypes::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID::NONE, BonusSubtypes::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, 0, BonusSubtypes::creatureDamageMax); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID::NONE, BonusSubtypes::creatureDamageMax); bonusMock.addNewBonus(bonus); } diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index c89a4112d..ed5901a57 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -107,7 +107,7 @@ TEST_F(CCreatureTest, JsonAddBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); JsonNode & toAdd = data["bonuses"]["toAdd"]; @@ -121,7 +121,7 @@ TEST_F(CCreatureTest, JsonAddBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) - && (bonus->sid == 42) + && (bonus->sid.as().getNum() == 42) && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -133,10 +133,10 @@ TEST_F(CCreatureTest, JsonRemoveBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b1); - std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, 42, TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b2); @@ -152,7 +152,7 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 17) - && (bonus->sid == 42) + && (bonus->sid.as().getNum() == 42) && (bonus->subtype.as().getNum() == 43) && (bonus->valType == BonusValueType::BASE_NUMBER); }; @@ -165,7 +165,7 @@ TEST_F(CCreatureTest, JsonRemoveBonus) && (bonus->type == BonusType::BLOCKS_RETALIATION) && (bonus->source == BonusSource::CREATURE_ABILITY) && (bonus->val == 18) - && (bonus->sid == 42) + && (bonus->sid.as().getNum() == 42) && (bonus->valType == BonusValueType::BASE_NUMBER); }; diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index c4f68b010..25a606f12 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -56,7 +56,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, TBonusSubtype(SpellSchool::ANY))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::ANY))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); @@ -70,7 +70,7 @@ TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, 0, TBonusSubtype(SpellSchool::AIR))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); diff --git a/test/spells/effects/CloneTest.cpp b/test/spells/effects/CloneTest.cpp index dd6a16181..acb7e0736 100644 --- a/test/spells/effects/CloneTest.cpp +++ b/test/spells/effects/CloneTest.cpp @@ -138,7 +138,7 @@ public: EXPECT_EQ(marker.duration, BonusDuration::N_TURNS); EXPECT_EQ(marker.turnsRemain, effectDuration); EXPECT_EQ(marker.source, BonusSource::SPELL_EFFECT); - EXPECT_EQ(marker.sid, SpellID::CLONE); + EXPECT_EQ(marker.sid, TBonusSourceID(SpellID(SpellID::CLONE))); } void setDefaultExpectations() diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index a0fdb17fb..af84f0046 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -95,7 +95,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); @@ -157,7 +157,7 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, getCount()).WillOnce(Return(unitAmount)); @@ -202,7 +202,7 @@ TEST_F(DamageApplyTest, DoesDamageByCount) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); diff --git a/test/spells/effects/DispelTest.cpp b/test/spells/effects/DispelTest.cpp index 13ad0070f..050517402 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -74,7 +74,7 @@ TEST_F(DispelTest, ApplicableToAliveUnitWithTimedEffect) auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(SpellID(negativeID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).WillOnce(Return(true)); @@ -101,7 +101,7 @@ TEST_F(DispelTest, IgnoresOwnEffects) } auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, neutralID.toEnum())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(SpellID(neutralID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).Times(AtMost(1)).WillRepeatedly(Return(true)); @@ -182,23 +182,23 @@ TEST_F(DispelApplyTest, RemovesEffects) EXPECT_CALL(unit1, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(unitIds[1])); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum()); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, negativeID.toEnum()); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, negativeID.toEnum()); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, positiveID.toEnum()); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, TBonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, positiveID.toEnum()); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); } diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index 1b26abb1b..d43d5881c 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -91,7 +91,7 @@ TEST_F(HealTest, ApplicableIfActuallyResurrects) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(1000)); EXPECT_CALL(mechanicsMock, isSmart()).WillOnce(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -117,7 +117,7 @@ TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -143,7 +143,7 @@ TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -271,7 +271,7 @@ TEST_F(HealTest, NotApplicableIfEffectValueTooLow) EXPECT_CALL(unit, getTotalHealth()).WillOnce(Return(200)); EXPECT_CALL(unit, getAvailableHealth()).WillOnce(Return(100)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(199)); @@ -348,7 +348,7 @@ TEST_P(HealApplyTest, Heals) EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitType()).WillRepeatedly(Return(pikeman)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); unitsFake.setDefaultBonusExpectations(); diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index f8384540d..7e0d47bb7 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -179,13 +179,13 @@ TEST_F(SacrificeApplyTest, ResurrectsTarget) EXPECT_CALL(mechanicsMock, applySpellBonus(_, Eq(&targetUnit))).WillOnce(ReturnArg<0>()); EXPECT_CALL(mechanicsMock, calculateRawEffectValue(_,_)).WillOnce(Return(effectValue)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, 0)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); auto & victim = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(victim, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(victimId)); EXPECT_CALL(victim, getCount()).Times(AtLeast(1)).WillRepeatedly(Return(victimCount)); - victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, 0)); + victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, TBonusSourceID::NONE)); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId), _, Eq(expectedHealValue))).Times(1); diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index 214b72f35..cb2cfe32d 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -244,12 +244,12 @@ TEST_P(SummonApplyTest, UpdatesOldUnit) setDefaultExpectaions(); acquired = std::make_shared(); - acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, 0)); + acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID::NONE)); acquired->redirectBonusesToFake(); acquired->expectAnyBonusSystemCall(); auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, 0)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID::NONE)); { EXPECT_CALL(unit, acquire()).WillOnce(Return(acquired)); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index b61ed1915..07bdae186 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -71,9 +71,9 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, 0, TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); @@ -103,7 +103,7 @@ TEST_P(TimedApplyTest, ChangesBonuses) for(auto & bonus : expectedBonus) { bonus.source = BonusSource::SPELL_EFFECT; - bonus.sid = spellIndex; + bonus.sid = TBonusSourceID(SpellID(spellIndex)); } if(cumulative) diff --git a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp index 2ec3ffff1..a500b707a 100644 --- a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp @@ -54,7 +54,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID::NONE); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -67,7 +67,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID::NONE); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -79,7 +79,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -90,7 +90,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) TEST_F(AbsoluteLevelConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index 15e3eb3ee..fb23ad84e 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 23aa8ba3e..403ceb818 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -42,14 +42,14 @@ TEST_F(BonusConditionTest, ImmuneByDefault) TEST_F(BonusConditionTest, ReceptiveIfMatchesType) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, TBonusSourceID::NONE)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, SpellSchool(SpellSchool::FIRE))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, TBonusSourceID(SpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 899889407..669f13fe5 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -64,7 +64,7 @@ TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::WATER))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::WATER))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -72,7 +72,7 @@ TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -80,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, 0, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index 2399c14de..10aee7053 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -58,7 +58,7 @@ TEST_P(ImmunityNegationConditionTest, WithHeroNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, BonusSubtypes::immunityEnemyHero)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID::NONE, BonusSubtypes::immunityEnemyHero)); EXPECT_EQ(isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -67,7 +67,7 @@ TEST_P(ImmunityNegationConditionTest, WithBattleWideNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, 0, BonusSubtypes::immunityBattleWide)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID::NONE, BonusSubtypes::immunityBattleWide)); //This should return if ownerMatches, because anyone should cast onto owner's stacks, but not on enemyStacks EXPECT_EQ(ownerMatches && isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalLevelConditionTest.cpp b/test/spells/targetConditions/NormalLevelConditionTest.cpp index e6546d7af..6dada367b 100644 --- a/test/spells/targetConditions/NormalLevelConditionTest.cpp +++ b/test/spells/targetConditions/NormalLevelConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(NormalLevelConditionTest, DefaultForNormal) TEST_P(NormalLevelConditionTest, ReceptiveNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID::NONE)); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(4)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -66,7 +66,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveNormal) TEST_P(NormalLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID::NONE)); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(0)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -75,7 +75,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveAbility) TEST_P(NormalLevelConditionTest, ImmuneNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE)); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(2)); EXPECT_EQ(!isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 57915b74a..4e30b8f1a 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) TEST_P(NormalSpellConditionTest, ChecksNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, 0, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); if(immuneSpell == castSpell) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp index ecd8525de..fac4e3bed 100644 --- a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp +++ b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp @@ -31,7 +31,7 @@ public: EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); if(hasBonus) - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, TBonusSourceID::NONE)); } protected: diff --git a/test/spells/targetConditions/SpellEffectConditionTest.cpp b/test/spells/targetConditions/SpellEffectConditionTest.cpp index fdbbae891..59af850aa 100644 --- a/test/spells/targetConditions/SpellEffectConditionTest.cpp +++ b/test/spells/targetConditions/SpellEffectConditionTest.cpp @@ -45,7 +45,7 @@ TEST_F(SpellEffectConditionTest, ImmuneByDefault) TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, SpellID::AGE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(SpellID(SpellID::AGE)))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -53,14 +53,14 @@ TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) TEST_F(SpellEffectConditionTest, ImmuneIfHasEffectFromOtherSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, SpellID::AIR_SHIELD)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(SpellID(SpellID::AIR_SHIELD)))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(SpellEffectConditionTest, ImmuneIfHasNoSpellEffects) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, 0)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, TBonusSourceID::NONE)); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } From 80e64859657460d2df00a8c9ca929aac19dcb147 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 14 Oct 2023 18:13:59 +0300 Subject: [PATCH 0907/1248] MetaIdentifier now uses std::variant internally --- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 3 +- client/battle/BattleStacksController.cpp | 3 +- cmake_modules/VCMI_lib.cmake | 5 +- lib/BasicTypes.cpp | 5 +- lib/CBonusTypeHandler.cpp | 5 +- lib/CCreatureHandler.cpp | 23 +++--- lib/CGameInfoCallback.cpp | 5 +- lib/CStack.cpp | 3 +- lib/CTownHandler.cpp | 12 +-- lib/CTownHandler.h | 1 + lib/JsonNode.cpp | 10 +-- lib/JsonNode.h | 2 +- lib/NetPacksLib.cpp | 2 +- lib/battle/BattleInfo.cpp | 6 +- lib/battle/CUnitState.cpp | 6 +- lib/battle/DamageCalculator.cpp | 13 ++- lib/bonuses/Bonus.cpp | 8 +- lib/bonuses/Bonus.h | 6 +- lib/bonuses/BonusList.cpp | 2 +- lib/bonuses/BonusList.h | 2 +- lib/bonuses/BonusParams.cpp | 50 ++++++------ lib/bonuses/BonusSubtypeID.cpp | 76 +++++++++++++++++ lib/bonuses/BonusSubtypeID.h | 78 ++++++++++++++++++ lib/bonuses/BonusSubtypes.cpp | 56 ------------- lib/bonuses/BonusSubtypes.h | 64 --------------- lib/constants/EntityIdentifiers.cpp | 81 ++++++++++++++++++- lib/constants/EntityIdentifiers.h | 14 +++- lib/constants/MetaIdentifier.cpp | 67 --------------- lib/constants/MetaIdentifier.h | 68 ++++++++++------ lib/gameState/CGameState.cpp | 2 +- .../CObjectClassesHandler.cpp | 5 ++ .../CObjectClassesHandler.h | 2 + lib/mapObjects/CArmedInstance.cpp | 7 +- lib/mapObjects/CGCreature.cpp | 3 +- lib/mapObjects/CGHeroInstance.cpp | 7 +- lib/mapObjects/CGTownBuilding.cpp | 3 +- lib/mapObjects/CGTownInstance.cpp | 5 +- lib/mapObjects/MiscObjects.cpp | 3 +- lib/pathfinder/TurnInfo.cpp | 4 +- lib/spells/TargetCondition.cpp | 5 +- server/battles/BattleActionProcessor.cpp | 37 +++++---- server/processors/PlayerMessageProcessor.cpp | 5 +- test/battle/CBattleInfoCallbackTest.cpp | 20 ++--- test/battle/CHealthTest.cpp | 4 +- test/battle/CUnitStateMagicTest.cpp | 12 +-- test/battle/CUnitStateTest.cpp | 31 ++++--- test/spells/AbilityCasterTest.cpp | 4 +- test/spells/effects/DamageTest.cpp | 6 +- test/spells/effects/HealTest.cpp | 10 +-- test/spells/effects/SacrificeTest.cpp | 4 +- test/spells/effects/SummonTest.cpp | 4 +- test/spells/effects/TimedTest.cpp | 4 +- .../AbsoluteLevelConditionTest.cpp | 8 +- .../AbsoluteSpellConditionTest.cpp | 4 +- .../targetConditions/BonusConditionTest.cpp | 4 +- .../ElementalConditionTest.cpp | 10 +-- .../ImmunityNegationConditionTest.cpp | 5 +- .../NormalLevelConditionTest.cpp | 6 +- .../NormalSpellConditionTest.cpp | 4 +- .../ReceptiveFeatureConditionTest.cpp | 2 +- .../SpellEffectConditionTest.cpp | 2 +- 61 files changed, 487 insertions(+), 421 deletions(-) create mode 100644 lib/bonuses/BonusSubtypeID.cpp create mode 100644 lib/bonuses/BonusSubtypeID.h delete mode 100644 lib/bonuses/BonusSubtypes.cpp delete mode 100644 lib/bonuses/BonusSubtypes.h delete mode 100644 lib/constants/MetaIdentifier.cpp diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index b2eccc938..654470198 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -11,7 +11,6 @@ #include #include "Nullkiller.h" -#include "../../../lib/bonuses/BonusSubtypes.h" #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" @@ -243,7 +242,7 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) return 1500; auto statsValue = - 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusSubtypes::heroMovementLand) + 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusSubtypeID::heroMovementLand) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 700 * art->valOfBonuses(BonusType::MORALE) + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 0f5b24c18..2ca550d4e 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -34,7 +34,6 @@ #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/battle/BattleHex.h" -#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/CStack.h" #include "../../lib/CondSh.h" #include "../../lib/TextOperations.h" @@ -535,7 +534,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorhasBonus(Selector::typeSubtype(BonusType::FLYING, BonusSubtypes::movementFlying))) + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusSubtypeID::movementFlying))) { owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() { diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 17a996fd3..d019bc146 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -31,7 +31,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.cpp ${MAIN_LIB_DIR}/bonuses/BonusParams.cpp ${MAIN_LIB_DIR}/bonuses/BonusSelector.cpp - ${MAIN_LIB_DIR}/bonuses/BonusSubtypes.cpp + ${MAIN_LIB_DIR}/bonuses/BonusSubtypeID.cpp ${MAIN_LIB_DIR}/bonuses/CBonusProxy.cpp ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.cpp ${MAIN_LIB_DIR}/bonuses/IBonusBearer.cpp @@ -43,7 +43,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/campaign/CampaignState.cpp ${MAIN_LIB_DIR}/constants/EntityIdentifiers.cpp - ${MAIN_LIB_DIR}/constants/MetaIdentifier.cpp ${MAIN_LIB_DIR}/events/ApplyDamage.cpp ${MAIN_LIB_DIR}/events/GameResumed.cpp @@ -359,7 +358,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.h ${MAIN_LIB_DIR}/bonuses/BonusParams.h ${MAIN_LIB_DIR}/bonuses/BonusSelector.h - ${MAIN_LIB_DIR}/bonuses/BonusSubtypes.h + ${MAIN_LIB_DIR}/bonuses/BonusSubtypeID.h ${MAIN_LIB_DIR}/bonuses/CBonusProxy.h ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.h ${MAIN_LIB_DIR}/bonuses/IBonusBearer.h diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index c3a55bf63..3f476ec32 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -17,7 +17,6 @@ #include "bonuses/BonusList.h" #include "bonuses/Bonus.h" #include "bonuses/IBonusBearer.h" -#include "bonuses/BonusSubtypes.h" #include #include @@ -72,14 +71,14 @@ int AFactionMember::getDefense(bool ranged) const int AFactionMember::getMinDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_1"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } int AFactionMember::getMaxDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_2"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 6f988d3f9..70636a9b3 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -20,7 +20,6 @@ #include "CCreatureHandler.h" #include "CGeneralTextHandler.h" #include "spells/CSpellHandler.h" -#include "bonuses/BonusSubtypes.h" template class std::vector; @@ -169,10 +168,10 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu } case BonusType::GENERAL_DAMAGE_REDUCTION: { - if (bonus->subtype == BonusSubtypes::damageTypeMelee) + if (bonus->subtype == BonusSubtypeID::damageTypeMelee) fileName = "DamageReductionMelee.bmp"; - if (bonus->subtype == BonusSubtypes::damageTypeRanged) + if (bonus->subtype == BonusSubtypeID::damageTypeRanged) fileName = "DamageReductionRanged.bmp"; break; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index b1cc82421..413398f7b 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -19,7 +19,6 @@ #include "constants/StringConstants.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" -#include "bonuses/BonusSubtypes.h" #include "serializer/JsonDeserializer.h" #include "serializer/JsonUpdater.h" #include "mapObjectConstructors/AObjectTypeHandler.h" @@ -126,13 +125,13 @@ int32_t CCreature::getBaseDefense() const int32_t CCreature::getBaseDamageMin() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMax() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } @@ -294,7 +293,7 @@ CCreature::CCreature() void CCreature::addBonus(int val, BonusType type) { - addBonus(val, type, TBonusSubtype::NONE); + addBonus(val, type, TBonusSubtype()); } void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) @@ -357,10 +356,10 @@ void CCreature::updateFrom(const JsonNode & data) addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); if(!configNode["damage"]["min"].isNull()) - addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin); + addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin); if(!configNode["damage"]["max"].isNull()) - addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax); + addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax); if(!configNode["shots"].isNull()) addBonus(configNode["shots"].Integer(), BonusType::SHOTS); @@ -613,8 +612,8 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax); + cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin); + cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax); assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); @@ -1039,11 +1038,11 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'M': //Max damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = BonusSubtypes::creatureDamageMax; + b.subtype = BonusSubtypeID::creatureDamageMax; break; case 'm': //Min damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = BonusSubtypes::creatureDamageMin; + b.subtype = BonusSubtypeID::creatureDamageMin; break; case 'S': b.type = BonusType::STACKS_SPEED; break; @@ -1060,7 +1059,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'E': b.type = BonusType::DEATH_STARE; - b.subtype = BonusSubtypes::deathStareGorgon; + b.subtype = BonusSubtypeID::deathStareGorgon; break; case 'F': b.type = BonusType::FEAR; break; @@ -1107,7 +1106,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.type = BonusType::MIND_IMMUNITY; break; case 'r': b.type = BonusType::REBIRTH; //on/off? makes sense? - b.subtype = BonusSubtypes::rebirthRegular; + b.subtype = BonusSubtypeID::rebirthRegular; b.val = 20; //arbitrary value break; case 'R': diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 5a4a234b1..cfbccd99d 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -17,7 +17,6 @@ #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo -#include "bonuses/BonusSubtypes.h" #include "NetPacks.h" // for InfoWindow #include "GameSettings.h" #include "TerrainHandler.h" @@ -269,7 +268,7 @@ bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - detailed = selectedHero->hasVisions(town, BonusSubtypes::visionsTowns); + detailed = selectedHero->hasVisions(town, BonusSubtypeID::visionsTowns); } dest.initFromTown(dynamic_cast(town), detailed); @@ -323,7 +322,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - if(selectedHero->hasVisions(hero, BonusSubtypes::visionsHeroes)) + if(selectedHero->hasVisions(hero, BonusSubtypeID::visionsHeroes)) infoLevel = InfoAboutHero::EInfoLevel::DETAILED; } diff --git a/lib/CStack.cpp b/lib/CStack.cpp index b776aed2a..a1c1371b1 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -17,7 +17,6 @@ #include "CGeneralTextHandler.h" #include "battle/BattleInfo.h" -#include "bonuses/BonusSubtypes.h" #include "spells/CSpellHandler.h" #include "NetPacks.h" @@ -221,7 +220,7 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const resurrectedCount += 1; } - if(customState->hasBonusOfType(BonusType::REBIRTH, BonusSubtypes::rebirthSpecial)) + if(customState->hasBonusOfType(BonusType::REBIRTH, BonusSubtypeID::rebirthSpecial)) { // resurrect at least one Sacred Phoenix vstd::amax(resurrectedCount, 1); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 4cee0e923..6f03059ce 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -23,7 +23,6 @@ #include "filesystem/Filesystem.h" #include "bonuses/Bonus.h" #include "bonuses/Propagators.h" -#include "bonuses/BonusSubtypes.h" #include "ResourceSet.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" @@ -561,7 +560,7 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::DEFENSE)); break; case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, BonusSubtypes::heroMovementSea, playerPropagator); + b = createBonus(building, BonusType::MOVEMENT, +500, BonusSubtypeID::heroMovementSea, playerPropagator); break; } @@ -571,7 +570,7 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val) const { - return createBonus(build, type, val, TBonusSubtype::NONE, emptyPropagator()); + return createBonus(build, type, val, TBonusSubtype(), emptyPropagator()); } std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const @@ -583,17 +582,18 @@ std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType ty { std::ostringstream descr; descr << build->getNameTranslated(); - return createBonusImpl(build->bid, type, val, prop, descr.str(), subtype); + return createBonusImpl(build->bid, build->town->faction->getId(), type, val, prop, descr.str(), subtype); } std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building, + const FactionID & faction, BonusType type, int val, TPropagatorPtr & prop, const std::string & description, TBonusSubtype subtype) const { - auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, TBonusSourceID(building), subtype, description); + auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, BuildingTypeUniqueID(faction, building), subtype, description); if(prop) b->addPropagator(prop); @@ -605,7 +605,7 @@ void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList { for(const auto & b : source.Vector()) { - auto bonus = JsonUtils::parseBuildingBonus(b, building->bid, building->getNameTranslated()); + auto bonus = JsonUtils::parseBuildingBonus(b, building->town->faction->getId(), building->bid, building->getNameTranslated()); if(bonus == nullptr) continue; diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index cf1b793ff..4fa1fb1f0 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -395,6 +395,7 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const; std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype, TPropagatorPtr & prop) const; std::shared_ptr createBonusImpl(const BuildingID & building, + const FactionID & faction, BonusType type, int val, TPropagatorPtr & prop, diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 33cd9e0f5..ed74ec600 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -421,21 +421,21 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json { if (node.isNull()) { - subtype = TBonusSubtype::NONE; + subtype = TBonusSubtype(); return; } if (!node.isString()) { logMod->warn("Bonus subtype must be string!"); - subtype = TBonusSubtype::NONE; + subtype = TBonusSubtype(); return; } VLC->identifiers()->requestIdentifier(node, [&subtype, node](int32_t identifier) { assert(0); //TODO - subtype = TBonusSubtype("type", node.String(), identifier); + subtype = BonusSubtypeID(identifier); }); } @@ -735,13 +735,13 @@ std::shared_ptr JsonUtils::parseBonus(const JsonNode &ability) return b; } -std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description) +std::shared_ptr JsonUtils::parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description) { /* duration = BonusDuration::PERMANENT source = BonusSource::TOWN_STRUCTURE bonusType, val, subtype - get from json */ - auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, TBonusSourceID(building), description); + auto b = std::make_shared(BonusDuration::PERMANENT, BonusType::NONE, BonusSource::TOWN_STRUCTURE, 0, BuildingTypeUniqueID(faction, building), description); if(!parseBonus(ability, b.get())) return nullptr; diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 0a3d90178..466521cc4 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -129,7 +129,7 @@ namespace JsonUtils { DLL_LINKAGE std::shared_ptr parseBonus(const JsonVector & ability_vec); DLL_LINKAGE std::shared_ptr parseBonus(const JsonNode & ability); - DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const BuildingID & building, const std::string & description); + DLL_LINKAGE std::shared_ptr parseBuildingBonus(const JsonNode & ability, const FactionID & faction, const BuildingID & building, const std::string & description); DLL_LINKAGE bool parseBonus(const JsonNode & ability, Bonus * placement); DLL_LINKAGE std::shared_ptr parseLimiter(const JsonNode & limiter); DLL_LINKAGE CSelector parseSelector(const JsonNode &ability); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 611c042d4..12f89f7e7 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2201,7 +2201,7 @@ void BattleTriggerEffect::applyGs(CGameState * gs) const } case BonusType::POISON: { - auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSubtype(SpellID(SpellID::POISON))) + auto b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, SpellID(SpellID::POISON)) .And(Selector::type()(BonusType::STACK_HEALTH))); if (b) b->val = val; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 478e8d9e2..bc4e02c1c 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -442,9 +442,9 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const //native terrain bonuses static auto nativeTerrain = std::make_shared(); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE)->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID())->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index d9e05c900..214287d9f 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -16,8 +16,6 @@ #include "../NetPacks.h" #include "../CCreatureHandler.h" -#include "../bonuses/BonusSubtypes.h" - #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" @@ -342,8 +340,8 @@ CUnitState::CUnitState(): health(this), shots(this), totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), - minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMin)), 0), - maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypes::creatureDamageMax)), 0), + minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin)), 0), + maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax)), 0), attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)), 0), defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index cd99a191a..77342135a 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -14,7 +14,6 @@ #include "Unit.h" #include "../bonuses/Bonus.h" -#include "../bonuses/BonusSubtypes.h" #include "../mapObjects/CGTownInstance.h" #include "../spells/CSpellHandler.h" #include "../GameSettings.h" @@ -207,11 +206,11 @@ double DamageCalculator::getAttackOffenseArcheryFactor() const if(info.shooting) { const std::string cachingStrArchery = "type_PERCENTAGE_DAMAGE_BOOSTs_1"; - static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypes::damageTypeRanged); + static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypeID::damageTypeRanged); return info.attacker->valOfBonuses(selectorArchery, cachingStrArchery) / 100.0; } const std::string cachingStrOffence = "type_PERCENTAGE_DAMAGE_BOOSTs_0"; - static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypes::damageTypeMelee); + static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypeID::damageTypeMelee); return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0; } @@ -283,7 +282,7 @@ double DamageCalculator::getDefenseSkillFactor() const double DamageCalculator::getDefenseArmorerFactor() const { const std::string cachingStrArmorer = "type_GENERAL_DAMAGE_REDUCTIONs_N1_NsrcSPELL_EFFECT"; - static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); + static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); return info.defender->valOfBonuses(selectorArmorer, cachingStrArmorer) / 100.0; } @@ -291,10 +290,10 @@ double DamageCalculator::getDefenseArmorerFactor() const double DamageCalculator::getDefenseMagicShieldFactor() const { const std::string cachingStrMeleeReduction = "type_GENERAL_DAMAGE_REDUCTIONs_0"; - static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeMelee); + static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeMelee); const std::string cachingStrRangedReduction = "type_GENERAL_DAMAGE_REDUCTIONs_1"; - static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeRanged); + static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeRanged); //handling spell effects - shield and air shield if(info.shooting) @@ -388,7 +387,7 @@ double DamageCalculator::getDefensePetrificationFactor() const { // Creatures that are petrified by a Basilisk's Petrifying attack or a Medusa's Stone gaze take 50% damage (R8 = 0.50) from ranged and melee attacks. Taking damage also deactivates the effect. const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1_srcSPELL_EFFECT"; - static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypes::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); + static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); return info.defender->valOfBonuses(selectorAllReduction, cachingStrAllReduction) / 100.0; } diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 92431ec5c..ea50c4d16 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -150,7 +150,7 @@ JsonNode Bonus::toJsonNode() const JsonNode root(JsonNode::JsonType::DATA_STRUCT); // only add values that might reasonably be found in config files root["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtype != TBonusSubtype::NONE) + if(subtype != TBonusSubtype()) root["subtype"].String() = subtype.toString(); if(additionalInfo != CAddInfo::NONE) root["addInfo"] = additionalInfoToJson(type, additionalInfo); @@ -158,7 +158,7 @@ JsonNode Bonus::toJsonNode() const root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); if(targetSourceType != BonusSource::OTHER) root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); - if(sid != TBonusSourceID::NONE) + if(sid != TBonusSourceID()) root["sourceID"].String() = sid.toString(); if(val != 0) root["val"].Integer() = val; @@ -184,11 +184,11 @@ JsonNode Bonus::toJsonNode() const } Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID) - : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, std::string()) + : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype(), std::string()) {} Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, std::string Desc) - : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype::NONE, Desc) + : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype(), Desc) {} Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype) diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 029fe78af..4abbe9cc9 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -10,7 +10,9 @@ #pragma once #include "BonusEnum.h" +#include "BonusSubtypeID.h" #include "../constants/MetaIdentifier.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -23,8 +25,8 @@ class IUpdater; class BonusList; class CSelector; -using TBonusSubtype = MetaIdentifier; -using TBonusSourceID = MetaIdentifier; +using TBonusSubtype = MetaIdentifier; +using TBonusSourceID = MetaIdentifier; using TBonusListPtr = std::shared_ptr; using TConstBonusListPtr = std::shared_ptr; using TLimiterPtr = std::shared_ptr; diff --git a/lib/bonuses/BonusList.cpp b/lib/bonuses/BonusList.cpp index f3e0b12bf..4920881b3 100644 --- a/lib/bonuses/BonusList.cpp +++ b/lib/bonuses/BonusList.cpp @@ -275,4 +275,4 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const BonusList &bonusL return out; } -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusList.h b/lib/bonuses/BonusList.h index d219e8a97..eef5a8777 100644 --- a/lib/bonuses/BonusList.h +++ b/lib/bonuses/BonusList.h @@ -111,4 +111,4 @@ public: DLL_LINKAGE std::ostream & operator<<(std::ostream &out, const BonusList &bonusList); -VCMI_LIB_NAMESPACE_END \ No newline at end of file +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index e760794d6..e7822d023 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -13,9 +13,11 @@ #include "BonusEnum.h" #include "BonusParams.h" #include "BonusSelector.h" -#include "BonusSubtypes.h" #include "../ResourceSet.h" +#include "../VCMI_Lib.h" +#include "../modding/IdentifierStorage.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -88,28 +90,28 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu type = BonusType::LEARN_MEETING_SPELL_LIMIT; else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery") { - subtype = BonusSubtypes::damageTypeRanged; + subtype = BonusSubtypeID::damageTypeRanged; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence") { - subtype = BonusSubtypes::damageTypeMelee; + subtype = BonusSubtypeID::damageTypeMelee; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer") { - subtype = BonusSubtypes::damageTypeAll; + subtype = BonusSubtypeID::damageTypeAll; type = BonusType::GENERAL_DAMAGE_REDUCTION; } else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation") { - subtype = BonusSubtypes::heroMovementSea; + subtype = BonusSubtypeID::heroMovementSea; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics") { - subtype = BonusSubtypes::heroMovementLand; + subtype = BonusSubtypeID::heroMovementLand; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } @@ -146,12 +148,12 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid") { type = BonusType::SPECIFIC_SPELL_POWER; - subtype = TBonusSubtype("spell", "firstAid"); + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "firstAid")); } else if (deprecatedSubtype == SecondarySkill::BALLISTICS || deprecatedSubtypeStr == "skill.ballistics") { type = BonusType::CATAPULT_EXTRA_SHOTS; - subtype = TBonusSubtype("spell", "catapultShot"); + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", "catapultShot")); } else isConverted = false; @@ -170,20 +172,20 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "SEA_MOVEMENT") { - subtype = BonusSubtypes::heroMovementSea; + subtype = BonusSubtypeID::heroMovementSea; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "LAND_MOVEMENT") { - subtype = BonusSubtypes::heroMovementLand; + subtype = BonusSubtypeID::heroMovementLand; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "MAXED_SPELL") { type = BonusType::SPELL; - subtype = TBonusSubtype("spell", deprecatedSubtypeStr); + subtype = SpellID(*VLC->identifiers()->getIdentifier( ModScope::scopeGame(), "spell", deprecatedSubtypeStr)); valueType = BonusValueType::INDEPENDENT_MAX; val = 3; } @@ -224,52 +226,52 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedTypeStr == "DIRECT_DAMAGE_IMMUNITY") { type = BonusType::SPELL_DAMAGE_REDUCTION; - subtype = MetaIdentifier(SpellSchool::ANY); + subtype = TBonusSubtype(SpellSchool::ANY); val = 100; } else if (deprecatedTypeStr == "AIR_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = MetaIdentifier(SpellSchool::AIR); + subtype = TBonusSubtype(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = MetaIdentifier(SpellSchool::FIRE); + subtype = TBonusSubtype(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = MetaIdentifier(SpellSchool::WATER); + subtype = TBonusSubtype(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = MetaIdentifier(SpellSchool::EARTH); + subtype = TBonusSubtype(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = MetaIdentifier(SpellSchool::AIR); + subtype = TBonusSubtype(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = MetaIdentifier(SpellSchool::FIRE); + subtype = TBonusSubtype(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = MetaIdentifier(SpellSchool::WATER); + subtype = TBonusSubtype(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = MetaIdentifier(SpellSchool::EARTH); + subtype = TBonusSubtype(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_IMMUNITY") { - subtype = MetaIdentifier(SpellSchool::AIR); + subtype = TBonusSubtype(SpellSchool::AIR); switch(deprecatedSubtype) { case 0: @@ -285,7 +287,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "FIRE_IMMUNITY") { - subtype = MetaIdentifier(SpellSchool::FIRE); + subtype = TBonusSubtype(SpellSchool::FIRE); switch(deprecatedSubtype) { case 0: @@ -301,7 +303,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "WATER_IMMUNITY") { - subtype = MetaIdentifier(SpellSchool::WATER); + subtype = TBonusSubtype(SpellSchool::WATER); switch(deprecatedSubtype) { case 0: @@ -317,7 +319,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "EARTH_IMMUNITY") { - subtype = MetaIdentifier(SpellSchool::EARTH); + subtype = TBonusSubtype(SpellSchool::EARTH); switch(deprecatedSubtype) { case 0: diff --git a/lib/bonuses/BonusSubtypeID.cpp b/lib/bonuses/BonusSubtypeID.cpp new file mode 100644 index 000000000..b6a509597 --- /dev/null +++ b/lib/bonuses/BonusSubtypeID.cpp @@ -0,0 +1,76 @@ +/* + * Bonus.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 "BonusSubtypeID.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const BonusSubtypeID BonusSubtypeID::creatureDamageBoth(0); +const BonusSubtypeID BonusSubtypeID::creatureDamageMin(1); +const BonusSubtypeID BonusSubtypeID::creatureDamageMax(2); +const BonusSubtypeID BonusSubtypeID::damageTypeAll(-1); +const BonusSubtypeID BonusSubtypeID::damageTypeMelee(0); +const BonusSubtypeID BonusSubtypeID::damageTypeRanged(1); +const BonusSubtypeID BonusSubtypeID::heroMovementLand(1); +const BonusSubtypeID BonusSubtypeID::heroMovementSea(0); +const BonusSubtypeID BonusSubtypeID::heroMovementPenalty(2); +const BonusSubtypeID BonusSubtypeID::heroMovementFull(1); +const BonusSubtypeID BonusSubtypeID::deathStareGorgon(0); +const BonusSubtypeID BonusSubtypeID::deathStareCommander(1); +const BonusSubtypeID BonusSubtypeID::rebirthRegular(0); +const BonusSubtypeID BonusSubtypeID::rebirthSpecial(1); +const BonusSubtypeID BonusSubtypeID::visionsMonsters(0); +const BonusSubtypeID BonusSubtypeID::visionsHeroes(1); +const BonusSubtypeID BonusSubtypeID::visionsTowns(2); +const BonusSubtypeID BonusSubtypeID::immunityBattleWide(0); +const BonusSubtypeID BonusSubtypeID::immunityEnemyHero(1); +const BonusSubtypeID BonusSubtypeID::transmutationPerHealth(0); +const BonusSubtypeID BonusSubtypeID::transmutationPerUnit(1); +const BonusSubtypeID BonusSubtypeID::destructionKillPercentage(0); +const BonusSubtypeID BonusSubtypeID::destructionKillAmount(1); +const BonusSubtypeID BonusSubtypeID::soulStealPermanent(0); +const BonusSubtypeID BonusSubtypeID::soulStealBattle(1); +const BonusSubtypeID BonusSubtypeID::movementFlying(0); +const BonusSubtypeID BonusSubtypeID::movementTeleporting(1); + +const BonusSourceID BonusSourceID::undeadMoraleDebuff(-2); + +BonusSubtypeID BonusSubtypeID::spellLevel(int level) +{ + return BonusSubtypeID(level); +} + +BonusSubtypeID BonusSubtypeID::creatureLevel(int level) +{ + return BonusSubtypeID(level); +} + +si32 BonusSubtypeID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusSubtypeID::encode(const si32 index) +{ + return std::to_string(index); +} + +si32 BonusSourceID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusSourceID::encode(const si32 index) +{ + return std::to_string(index); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSubtypeID.h b/lib/bonuses/BonusSubtypeID.h new file mode 100644 index 000000000..9db601983 --- /dev/null +++ b/lib/bonuses/BonusSubtypeID.h @@ -0,0 +1,78 @@ +/* + * BonusSubtypeID.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 "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE BonusSourceID : public Identifier +{ +public: + using Identifier::Identifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusSourceID undeadMoraleDebuff; // -2 +}; + +class DLL_LINKAGE BonusSubtypeID : public Identifier +{ +public: + using Identifier::Identifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusSubtypeID creatureDamageBoth; // 0 + static const BonusSubtypeID creatureDamageMin; // 1 + static const BonusSubtypeID creatureDamageMax; // 2 + + static const BonusSubtypeID damageTypeAll; // -1 + static const BonusSubtypeID damageTypeMelee; // 0 + static const BonusSubtypeID damageTypeRanged; // 1 + + static const BonusSubtypeID heroMovementLand; // 1 + static const BonusSubtypeID heroMovementSea; // 0 + + static const BonusSubtypeID heroMovementPenalty; // 2 + static const BonusSubtypeID heroMovementFull; // 1 + + static const BonusSubtypeID deathStareGorgon; // 0 + static const BonusSubtypeID deathStareCommander; + + static const BonusSubtypeID rebirthRegular; // 0 + static const BonusSubtypeID rebirthSpecial; // 1 + + static const BonusSubtypeID visionsMonsters; // 0 + static const BonusSubtypeID visionsHeroes; // 1 + static const BonusSubtypeID visionsTowns; // 2 + + static const BonusSubtypeID immunityBattleWide; // 0 + static const BonusSubtypeID immunityEnemyHero; // 1 + + static const BonusSubtypeID transmutationPerHealth; // 0 + static const BonusSubtypeID transmutationPerUnit; // 1 + + static const BonusSubtypeID destructionKillPercentage; // 0 + static const BonusSubtypeID destructionKillAmount; // 1 + + static const BonusSubtypeID soulStealPermanent; // 0 + static const BonusSubtypeID soulStealBattle; // 1 + + static const BonusSubtypeID movementFlying; // 0 + static const BonusSubtypeID movementTeleporting; // 1 + + static BonusSubtypeID spellLevel(int level); + static BonusSubtypeID creatureLevel(int level); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSubtypes.cpp b/lib/bonuses/BonusSubtypes.cpp deleted file mode 100644 index 579c299c4..000000000 --- a/lib/bonuses/BonusSubtypes.cpp +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Bonus.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 "BonusSubtypes.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const TBonusSubtype BonusSubtypes::creatureDamageBoth("", "", 0); -const TBonusSubtype BonusSubtypes::creatureDamageMin("", "", 1); -const TBonusSubtype BonusSubtypes::creatureDamageMax("", "", 2); -const TBonusSubtype BonusSubtypes::damageTypeAll("", "", -1); -const TBonusSubtype BonusSubtypes::damageTypeMelee("", "", 0); -const TBonusSubtype BonusSubtypes::damageTypeRanged("", "", 1); -const TBonusSubtype BonusSubtypes::heroMovementLand("", "", 1); -const TBonusSubtype BonusSubtypes::heroMovementSea("", "", 0); -const TBonusSubtype BonusSubtypes::heroMovementPenalty("", "", 2); -const TBonusSubtype BonusSubtypes::heroMovementFull("", "", 1); -const TBonusSubtype BonusSubtypes::deathStareGorgon("", "", 0); -const TBonusSubtype BonusSubtypes::deathStareCommander("", "", 1); -const TBonusSubtype BonusSubtypes::rebirthRegular("", "", 0); -const TBonusSubtype BonusSubtypes::rebirthSpecial("", "", 1); -const TBonusSubtype BonusSubtypes::visionsMonsters("", "", 0); -const TBonusSubtype BonusSubtypes::visionsHeroes("", "", 1); -const TBonusSubtype BonusSubtypes::visionsTowns("", "", 2); -const TBonusSubtype BonusSubtypes::immunityBattleWide("", "", 0); -const TBonusSubtype BonusSubtypes::immunityEnemyHero("", "", 1); -const TBonusSubtype BonusSubtypes::transmutationPerHealth("", "", 0); -const TBonusSubtype BonusSubtypes::transmutationPerUnit("", "", 1); -const TBonusSubtype BonusSubtypes::destructionKillPercentage("", "", 0); -const TBonusSubtype BonusSubtypes::destructionKillAmount("", "", 1); -const TBonusSubtype BonusSubtypes::soulStealPermanent("", "", 0); -const TBonusSubtype BonusSubtypes::soulStealBattle("", "", 1); -const TBonusSubtype BonusSubtypes::movementFlying("", "", 0); -const TBonusSubtype BonusSubtypes::movementTeleporting("", "", 1); - -TBonusSubtype BonusSubtypes::spellLevel(int level) -{ - assert(0); //todo - return TBonusSubtype(); -} - -TBonusSubtype BonusSubtypes::creatureLevel(int level) -{ - assert(0); //todo - return TBonusSubtype(); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSubtypes.h b/lib/bonuses/BonusSubtypes.h deleted file mode 100644 index 24ed6deeb..000000000 --- a/lib/bonuses/BonusSubtypes.h +++ /dev/null @@ -1,64 +0,0 @@ -/* - * BonusSubtypes.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 "../constants/MetaIdentifier.h" - -VCMI_LIB_NAMESPACE_BEGIN - -using TBonusSubtype = MetaIdentifier; - -class DLL_LINKAGE BonusSubtypes -{ -public: - static const TBonusSubtype creatureDamageBoth; // 0 - static const TBonusSubtype creatureDamageMin; // 1 - static const TBonusSubtype creatureDamageMax; // 2 - - static const TBonusSubtype damageTypeAll; // -1 - static const TBonusSubtype damageTypeMelee; // 0 - static const TBonusSubtype damageTypeRanged; // 1 - - static const TBonusSubtype heroMovementLand; // 1 - static const TBonusSubtype heroMovementSea; // 0 - - static const TBonusSubtype heroMovementPenalty; // 2 - static const TBonusSubtype heroMovementFull; // 1 - - static const TBonusSubtype deathStareGorgon; // 0 - static const TBonusSubtype deathStareCommander; - - static const TBonusSubtype rebirthRegular; // 0 - static const TBonusSubtype rebirthSpecial; // 1 - - static const TBonusSubtype visionsMonsters; // 0 - static const TBonusSubtype visionsHeroes; // 1 - static const TBonusSubtype visionsTowns; // 2 - - static const TBonusSubtype immunityBattleWide; // 0 - static const TBonusSubtype immunityEnemyHero; // 1 - - static const TBonusSubtype transmutationPerHealth; // 0 - static const TBonusSubtype transmutationPerUnit; // 1 - - static const TBonusSubtype destructionKillPercentage; // 0 - static const TBonusSubtype destructionKillAmount; // 1 - - static const TBonusSubtype soulStealPermanent; // 0 - static const TBonusSubtype soulStealBattle; // 1 - - static const TBonusSubtype movementFlying; // 0 - static const TBonusSubtype movementTeleporting; // 1 - - static TBonusSubtype spellLevel(int level); - static TBonusSubtype creatureLevel(int level); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 116f31d83..f9224d88a 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -37,6 +37,8 @@ #include "TerrainHandler.h" //TODO: remove #include "BattleFieldHandler.h" #include "ObstacleHandler.h" +#include "CTownHandler.h" +#include "mapObjectConstructors/CObjectClassesHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -134,6 +136,40 @@ std::string HeroClassID::entityType() return "heroClass"; } +si32 ObjectInstanceID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string ObjectInstanceID::encode(const si32 index) +{ + return std::to_string(index); +} + +si32 CampaignScenarioID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string CampaignScenarioID::encode(const si32 index) +{ + return std::to_string(index); +} + +std::string Obj::encode(int32_t index) +{ + return VLC->objtypeh->getObjectHandlerName(index); +} + +si32 Obj::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + si32 HeroTypeID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); @@ -182,6 +218,20 @@ std::string ArtifactID::entityType() return "artifact"; } +si32 SecondarySkill::decode(const std::string& identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string SecondarySkill::encode(const si32 index) +{ + return VLC->skills()->getById(SecondarySkill(index))->getJsonKey(); +} + const CCreature * CreatureIDBase::toCreature() const { return VLC->creh->objects.at(num); @@ -240,6 +290,20 @@ std::string SpellID::encode(const si32 index) return VLC->spells()->getByIndex(index)->getJsonKey(); } +si32 BattleField::decode(const std::string & identifier) +{ + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); + if(rawId) + return rawId.value(); + else + return -1; +} + +std::string BattleField::encode(const si32 index) +{ + return VLC->spells()->getByIndex(index)->getJsonKey(); +} + std::string SpellID::entityType() { return "spell"; @@ -257,8 +321,6 @@ bool PlayerColor::isSpectator() const std::string PlayerColor::toString() const { - if (num == -1) - return "neutral"; return encode(num); } @@ -269,6 +331,9 @@ si32 PlayerColor::decode(const std::string & identifier) std::string PlayerColor::encode(const si32 index) { + if (index == -1) + return "neutral"; + if (index < 0 || index >= std::size(GameConstants::PLAYER_COLOR_NAMES)) { assert(0); @@ -373,6 +438,18 @@ std::string GameResID::encode(const si32 index) return GameConstants::RESOURCE_NAMES[index]; } +si32 BuildingTypeUniqueID::decode(const std::string & identifier) +{ + assert(0); //TODO + return -1; +} + +std::string BuildingTypeUniqueID::encode(const si32 index) +{ + assert(0); // TODO + return ""; +} + std::string GameResID::entityType() { return "resource"; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index d999b76da..c8f9bc467 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -552,10 +552,13 @@ public: }; }; -class Obj : public IdentifierWithEnum +class DLL_LINKAGE Obj : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); }; class DLL_LINKAGE RoadId : public Identifier @@ -830,6 +833,9 @@ public: static const BattleField NONE; const BattleFieldInfo * getInfo() const; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); }; class DLL_LINKAGE BoatId : public Identifier @@ -935,6 +941,9 @@ class BuildingTypeUniqueID : public Identifier public: BuildingTypeUniqueID(FactionID faction, BuildingID building ); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + BuildingID getBuilding() const; FactionID getFaction() const; @@ -946,6 +955,9 @@ class DLL_LINKAGE CampaignScenarioID : public Identifier public: using Identifier::Identifier; + static si32 decode(const std::string & identifier); + static std::string encode(int32_t index); + static const CampaignScenarioID NONE; }; diff --git a/lib/constants/MetaIdentifier.cpp b/lib/constants/MetaIdentifier.cpp deleted file mode 100644 index a15cc74c6..000000000 --- a/lib/constants/MetaIdentifier.cpp +++ /dev/null @@ -1,67 +0,0 @@ -/* - * EntityIdentifiers.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 "MetaIdentifier.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const MetaIdentifier MetaIdentifier::NONE("", "", -1); - -MetaIdentifier::MetaIdentifier(): - integerForm(-1) -{} - -MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier) - : stringForm(identifier) - , integerForm(-1) -{ - onDeserialized(); -} - -MetaIdentifier::MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value) - : stringForm(identifier) - , integerForm(value) -{ -} - -bool MetaIdentifier::operator == (const MetaIdentifier & other) const -{ - assert( (stringForm == other.stringForm) ? (integerForm == other.integerForm) : true ); - - return stringForm == other.stringForm; -} - -bool MetaIdentifier::operator != (const MetaIdentifier & other) const -{ - return !(*this == other); -} - -bool MetaIdentifier::operator < (const MetaIdentifier & other) const -{ - assert(0); -} - -int32_t MetaIdentifier::getNum() const -{ - return integerForm; -} - -std::string MetaIdentifier::toString() const -{ - return stringForm; -} - -void MetaIdentifier::onDeserialized() -{ - assert(0); //TODO -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/MetaIdentifier.h b/lib/constants/MetaIdentifier.h index c9149348f..d882f9e0d 100644 --- a/lib/constants/MetaIdentifier.h +++ b/lib/constants/MetaIdentifier.h @@ -14,49 +14,69 @@ VCMI_LIB_NAMESPACE_BEGIN /// This class represents field that may contain value of multiple different identifer types +template class DLL_LINKAGE MetaIdentifier { - std::string stringForm; - int32_t integerForm; - - void onDeserialized(); + std::variant value; public: - static const MetaIdentifier NONE; - - MetaIdentifier(); - MetaIdentifier(const std::string & entityType, const std::string & identifier); - MetaIdentifier(const std::string & entityType, const std::string & identifier, int32_t value); + MetaIdentifier() + {} template - explicit MetaIdentifier(const IdentifierType & identifier) - : integerForm(identifier.getNum()) + MetaIdentifier(const IdentifierType & identifier) + : value(identifier) + {} + + int32_t getNum() const { - static_assert(std::is_base_of::value, "MetaIdentifier can only be constructed from Identifer class"); + std::optional result; + + std::visit([&result] (const auto& v) { result = v.getNum(); }, value); + + assert(result.has_value()); + return result.value_or(-1); } - int32_t getNum() const; - std::string toString() const; + std::string toString() const + { + std::optional result; + + std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value); + + assert(result.has_value()); + return result.value_or(""); + } template IdentifierType as() const { - static_assert(std::is_base_of::value, "MetaIdentifier can only be converted to Identifer class"); - IdentifierType result(integerForm); - return result; + auto * result = std::get_if(&value); + assert(result); + + if (result) + return *result; + else + return IdentifierType(); } template void serialize(Handler &h, const int version) { - h & integerForm; - - if (!h.saving) - onDeserialized(); + h & value; } - bool operator == (const MetaIdentifier & other) const; - bool operator != (const MetaIdentifier & other) const; - bool operator < (const MetaIdentifier & other) const; + bool operator == (const MetaIdentifier & other) const + { + return value == other.value; + } + bool operator != (const MetaIdentifier & other) const + { + return value != other.value; + } + bool operator < (const MetaIdentifier & other) const + { + return value < other.value; + } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 3a911cdef..5889e8730 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -655,7 +655,7 @@ void CGameState::initGlobalBonuses() { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::GLOBAL;//for all - bonus->sid = TBonusSourceID::NONE; //there is one global object + bonus->sid = TBonusSourceID(); //there is one global object globalEffects.addNewBonus(bonus); } VLC->creh->loadCrExpBon(globalEffects); diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index e7fd552b4..84ccbde4e 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -488,4 +488,9 @@ std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const return objects.at(type)->handlerName; } +std::string CObjectClassesHandler::getJsonKey(si32 type) const +{ + return objects.at(type)->getJsonKey(); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 16817063d..f75f9c434 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -129,6 +129,8 @@ public: /// Returns handler string describing the handler (for use in client) std::string getObjectHandlerName(si32 type) const; + std::string getJsonKey(si32 type) const; + template void serialize(Handler &h, const int version) { h & objects; diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 7d88da3ab..d7f8f4036 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -59,7 +59,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID::NONE); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID()); addNewBonus(b); } @@ -120,13 +120,12 @@ void CArmedInstance::updateMoraleBonusFromArmy() CBonusSystemNode::treeHasChanged(); //-1 modifier for any Undead unit in army - const TBonusSourceID UNDEAD_MODIFIER_ID( "", "", -2); - auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, UNDEAD_MODIFIER_ID)); + auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, BonusSourceID::undeadMoraleDebuff)); if(hasUndead) { if(!undeadModifier) { - undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, UNDEAD_MODIFIER_ID, VLC->generaltexth->arraytxt[116]); + undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, BonusSourceID::undeadMoraleDebuff, VLC->generaltexth->arraytxt[116]); undeadModifier->description = undeadModifier->description.substr(0, undeadModifier->description.size()-2);//trim value addNewBonus(undeadModifier); } diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 51ecaf4f6..85b82a2f6 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -16,7 +16,6 @@ #include "../CConfigHandler.h" #include "../GameSettings.h" #include "../IGameCallback.h" -#include "../bonuses/BonusSubtypes.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN @@ -47,7 +46,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { std::string hoverName; - if(hero->hasVisions(this, BonusSubtypes::visionsMonsters)) + if(hero->hasVisions(this, BonusSubtypeID::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index fd2d12f47..d3a58ac0e 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -37,7 +37,6 @@ #include "../modding/ModScope.h" #include "../constants/StringConstants.h" #include "../battle/Unit.h" -#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN @@ -250,14 +249,14 @@ void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) c lowestCreatureSpeed = realLowestSpeed; //Let updaters run again treeHasChanged(); - ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusSubtypes::heroMovementLand : BonusSubtypes::heroMovementSea)); + ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusSubtypeID::heroMovementLand : BonusSubtypeID::heroMovementSea)); } } int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const { updateArmyMovementBonus(onLand, ti); - return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusSubtypes::heroMovementLand : BonusSubtypes::heroMovementSea); + return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusSubtypeID::heroMovementLand : BonusSubtypeID::heroMovementSea); } CGHeroInstance::CGHeroInstance(): @@ -764,7 +763,7 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const } }); - const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusSubtypes::spellLevel(spell->getLevel())); + const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusSubtypeID::spellLevel(spell->getLevel())); if(spell->isSpecial()) { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index e36d39784..38994ebb9 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -15,7 +15,6 @@ #include "../NetPacks.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" -#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN @@ -147,7 +146,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const if(!h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, TBonusSourceID(Obj(Obj::STABLES)), BonusSubtypes::heroMovementLand, VLC->generaltexth->arraytxt[100]); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, TBonusSourceID(Obj(Obj::STABLES)), BonusSubtypeID::heroMovementLand, VLC->generaltexth->arraytxt[100]); gb.id = heroID.getNum(); cb->giveHeroBonus(&gb); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9cb7144e8..05b1c3b78 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -13,7 +13,6 @@ #include "CGTownBuilding.h" #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" -#include "../bonuses/BonusSubtypes.h" #include "../battle/IBattleInfoCallback.h" #include "../NetPacks.h" #include "../CConfigHandler.h" @@ -161,7 +160,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusSubtypes::creatureLevel(level))); + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusSubtypeID::creatureLevel(level))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); @@ -788,7 +787,7 @@ void CGTownInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID::NONE); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID()); addNewBonus(b); } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 8f873550d..847c0b1ad 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -25,7 +25,6 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../modding/ModScope.h" -#include "../bonuses/BonusSubtypes.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1205,7 +1204,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT; gb.bonus.sid = TBonusSourceID(id); - gb.bonus.subtype = BonusSubtypes::heroMovementSea; + gb.bonus.subtype = BonusSubtypeID::heroMovementSea; // FIXME: This is really dirty hack // Proper fix would be to make CGLighthouse into bonus system node diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 3d805847e..f8256d120 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -73,7 +73,7 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const bool TurnInfo::hasBonusOfType(BonusType type) const { - return hasBonusOfType(type, TBonusSubtype::NONE); + return hasBonusOfType(type, TBonusSubtype()); } bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const @@ -96,7 +96,7 @@ bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const int TurnInfo::valOfBonuses(BonusType type) const { - return valOfBonuses(type, TBonusSubtype::NONE); + return valOfBonuses(type, TBonusSubtype()); } int TurnInfo::valOfBonuses(BonusType type, TBonusSubtype subtype) const diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index ffd316702..107cdf5d6 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -17,7 +17,6 @@ #include "../battle/Unit.h" #include "../bonuses/BonusParams.h" #include "../bonuses/BonusList.h" -#include "../bonuses/BonusSubtypes.h" #include "../modding/IdentifierStorage.h" #include "../modding/ModUtility.h" @@ -293,8 +292,8 @@ class ImmunityNegationCondition : public TargetConditionItemBase protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypes::immunityBattleWide); - const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypes::immunityEnemyHero); + const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypeID::immunityBattleWide); + const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypeID::immunityEnemyHero); //Non-magical effects is not affected by orb of vulnerability if(!m->isMagicalEffect()) return false; diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 19113ca00..45e0862f1 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -20,7 +20,6 @@ #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/BattleAction.h" -#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/NetPacks.h" #include "../../lib/spells/AbilityCaster.h" @@ -163,9 +162,9 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c SetStackEffect sse; sse.battleID = battle.getBattle()->getBattleID(); - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE))); int oldDefenceValue = defence.totalValue(); @@ -383,7 +382,7 @@ bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, return false; std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype == TBonusSubtype::NONE) + if(!catapultAbility || catapultAbility->subtype == TBonusSubtype()) { gameHandler->complain("We do not know how to shoot :P"); } @@ -453,7 +452,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con else destStack = battle.battleGetUnitByPos(target.at(0).hexValue); - if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == TBonusSubtype::NONE) + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == TBonusSubtype()) { gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); } @@ -1169,7 +1168,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution //original formula x = min(x, (gorgons_count + 9)/10); - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypes::deathStareGorgon) / 100.0f; + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypeID::deathStareGorgon) / 100.0f; vstd::amin(chanceToKill, 1); //cap at 100% std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); @@ -1180,7 +1179,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count vstd::amin(staredCreatures, maxToKill); - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypes::deathStareCommander)) / defender->level(); + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypeID::deathStareCommander)) / defender->level(); if(staredCreatures) { //TODO: death stare was not originally available for multiple-hex attacks, but... @@ -1250,9 +1249,9 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & else resurrectInfo.type = attacker->creatureId(); - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypes::transmutationPerHealth)) + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypeID::transmutationPerHealth)) resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypes::transmutationPerUnit)) + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypeID::transmutationPerUnit)) resurrectInfo.count = defender->getCount(); else return; //wrong subtype @@ -1274,21 +1273,21 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & gameHandler->sendAndApply(&fakeEvent); } - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount)) + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount)) { double chanceToTrigger = 0; int amountToDie = 0; - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage)) //killing by percentage + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage)) //killing by percentage { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypes::destructionKillPercentage) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypes::destructionKillPercentage)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypeID::destructionKillPercentage)))->additionalInfo[0]; amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount)) //killing by count + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount)) //killing by count { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypes::destructionKillAmount) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypes::destructionKillAmount)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypeID::destructionKillAmount)))->additionalInfo[0]; } vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% @@ -1349,12 +1348,12 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba { //we can have two bonuses - one with subtype 0 and another with subtype 1 //try to use permanent first, use only one of two - for(const auto & subtype : { BonusSubtypes::soulStealBattle, BonusSubtypes::soulStealPermanent}) + for(const auto & subtype : { BonusSubtypeID::soulStealBattle, BonusSubtypeID::soulStealPermanent}) { if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) { int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - bool permanent = subtype == BonusSubtypes::soulStealPermanent; + bool permanent = subtype == BonusSubtypeID::soulStealPermanent; attackerState->heal(toHeal, EHealLevel::OVERHEAL, (permanent ? EHealPower::PERMANENT : EHealPower::ONE_BATTLE)); drainedLife += toHeal; break; diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index f82631f84..b9b5c1382 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -16,7 +16,6 @@ #include "../../lib/serializer/Connection.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameConstants.h" @@ -141,11 +140,11 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns ///Give all spells with bonus (to allow banned spells) GiveBonus giveBonus(GiveBonus::ETarget::HERO); giveBonus.id = hero->id.getNum(); - giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, TBonusSourceID::NONE); + giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, TBonusSourceID()); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { - giveBonus.bonus.subtype = BonusSubtypes::spellLevel(level); + giveBonus.bonus.subtype = BonusSubtypeID::spellLevel(level); gameHandler->sendAndApply(&giveBonus); } diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index 164a1f02b..5ff9d891c 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -49,7 +49,7 @@ public: void makeWarMachine() { - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); } void redirectBonusesToFake() @@ -331,7 +331,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToSelf) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); setDefaultExpectations(); @@ -362,7 +362,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -382,7 +382,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedAlly) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); setDefaultExpectations(); @@ -397,11 +397,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); setDefaultExpectations(); @@ -433,7 +433,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -453,7 +453,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedEnemy) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); setDefaultExpectations(); @@ -468,11 +468,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID::NONE)); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); setDefaultExpectations(); diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index 37b19f5b5..fd74e4171 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -33,7 +33,7 @@ public: EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, TBonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(UNIT_AMOUNT)); } @@ -239,7 +239,7 @@ TEST_F(HealthTest, singleUnitStack) EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, TBonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(1)); diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index 3cc3c7b54..b3ce7358b 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -55,7 +55,7 @@ public: void makeNormalCaster() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, TBonusSourceID::NONE, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, TBonusSourceID(), TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); } }; @@ -63,7 +63,7 @@ TEST_F(UnitStateMagicTest, initialNormal) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, TBonusSourceID())); initUnit(); @@ -125,7 +125,7 @@ TEST_F(UnitStateMagicTest, effectPower) const int32_t EFFECT_POWER = 12 * 100; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, TBonusSourceID())); makeNormalCaster(); EXPECT_EQ(subject.getEffectPower(&spellMock), 12 * DEFAULT_AMOUNT); @@ -148,7 +148,7 @@ TEST_F(UnitStateMagicTest, enchantPower) const int32_t ENCHANT_POWER = 42; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, TBonusSourceID())); makeNormalCaster(); @@ -171,7 +171,7 @@ TEST_F(UnitStateMagicTest, effectValue) const int32_t EFFECT_VALUE = 456; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, TBonusSourceID::NONE, TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, TBonusSourceID(), TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); makeNormalCaster(); EXPECT_EQ(subject.getEffectValue(&spellMock), EFFECT_VALUE * DEFAULT_AMOUNT); @@ -201,7 +201,7 @@ TEST_F(UnitStateMagicTest, spendMana) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); initUnit(); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 8355f5e6b..0486efcc8 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -12,7 +12,6 @@ #include "mock/mock_BonusBearer.h" #include "mock/mock_UnitInfo.h" #include "mock/mock_UnitEnvironment.h" -#include "../../lib/bonuses/BonusSubtypes.h" #include "../../lib/battle/CUnitState.h" #include "../../lib/CCreatureHandler.h" @@ -52,12 +51,12 @@ public: void setDefaultExpectations() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, TBonusSourceID())); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::ATTACK))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::DEFENSE))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, TBonusSourceID(), TBonusSubtype(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, TBonusSourceID())); EXPECT_CALL(infoMock, unitBaseAmount()).WillRepeatedly(Return(DEFAULT_AMOUNT)); EXPECT_CALL(infoMock, unitType()).WillRepeatedly(Return(pikeman)); @@ -67,8 +66,8 @@ public: void makeShooter(int32_t ammo) { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID::NONE)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, TBonusSourceID())); } void initUnit() @@ -180,7 +179,7 @@ TEST_F(UnitStateTest, attackWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID())); int expectedAttack = static_cast(DEFAULT_ATTACK + 0.5 * DEFAULT_DEFENCE); @@ -192,7 +191,7 @@ TEST_F(UnitStateTest, defenceWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID::NONE)); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID())); int expectedDefence = 0; @@ -205,7 +204,7 @@ TEST_F(UnitStateTest, additionalAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); bonusMock.addNewBonus(bonus); } @@ -219,7 +218,7 @@ TEST_F(UnitStateTest, additionalMeleeAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; bonusMock.addNewBonus(bonus); @@ -234,7 +233,7 @@ TEST_F(UnitStateTest, additionalRangedAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; bonusMock.addNewBonus(bonus); @@ -249,10 +248,10 @@ TEST_F(UnitStateTest, getMinDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID::NONE, BonusSubtypes::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID(), BonusSubtypeID::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID::NONE, BonusSubtypes::creatureDamageMin); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID(), BonusSubtypeID::creatureDamageMin); bonusMock.addNewBonus(bonus); } @@ -265,10 +264,10 @@ TEST_F(UnitStateTest, getMaxDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID::NONE, BonusSubtypes::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID(), BonusSubtypeID::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID::NONE, BonusSubtypes::creatureDamageMax); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID(), BonusSubtypeID::creatureDamageMax); bonusMock.addNewBonus(bonus); } diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index 25a606f12..663e3895c 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -56,7 +56,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::ANY))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID(), TBonusSubtype(SpellSchool::ANY))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); @@ -70,7 +70,7 @@ TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index af84f0046..e35766ce2 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -95,7 +95,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); @@ -157,7 +157,7 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, getCount()).WillOnce(Return(unitAmount)); @@ -202,7 +202,7 @@ TEST_F(DamageApplyTest, DoesDamageByCount) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index d43d5881c..a652ae831 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -91,7 +91,7 @@ TEST_F(HealTest, ApplicableIfActuallyResurrects) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(1000)); EXPECT_CALL(mechanicsMock, isSmart()).WillOnce(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -117,7 +117,7 @@ TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -143,7 +143,7 @@ TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -271,7 +271,7 @@ TEST_F(HealTest, NotApplicableIfEffectValueTooLow) EXPECT_CALL(unit, getTotalHealth()).WillOnce(Return(200)); EXPECT_CALL(unit, getAvailableHealth()).WillOnce(Return(100)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID::NONE)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(199)); @@ -348,7 +348,7 @@ TEST_P(HealApplyTest, Heals) EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitType()).WillRepeatedly(Return(pikeman)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); unitsFake.setDefaultBonusExpectations(); diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index 7e0d47bb7..d53f32bbe 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -179,13 +179,13 @@ TEST_F(SacrificeApplyTest, ResurrectsTarget) EXPECT_CALL(mechanicsMock, applySpellBonus(_, Eq(&targetUnit))).WillOnce(ReturnArg<0>()); EXPECT_CALL(mechanicsMock, calculateRawEffectValue(_,_)).WillOnce(Return(effectValue)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID::NONE)); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); auto & victim = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(victim, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(victimId)); EXPECT_CALL(victim, getCount()).Times(AtLeast(1)).WillRepeatedly(Return(victimCount)); - victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, TBonusSourceID::NONE)); + victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, TBonusSourceID())); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId), _, Eq(expectedHealValue))).Times(1); diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index cb2cfe32d..c835692a7 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -244,12 +244,12 @@ TEST_P(SummonApplyTest, UpdatesOldUnit) setDefaultExpectaions(); acquired = std::make_shared(); - acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID::NONE)); + acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID())); acquired->redirectBonusesToFake(); acquired->expectAnyBonusSystemCall(); auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID::NONE)); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID())); { EXPECT_CALL(unit, acquire()).WillOnce(Return(acquired)); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index 07bdae186..0bfebd03a 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -71,9 +71,9 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID(), TBonusSubtype(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID::NONE, TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID(), TBonusSubtype(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); diff --git a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp index a500b707a..f75303aea 100644 --- a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp @@ -54,7 +54,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -67,7 +67,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -79,7 +79,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -90,7 +90,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) TEST_F(AbsoluteLevelConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID()); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index fb23ad84e..8b260437b 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 403ceb818..87ce6b655 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -42,14 +42,14 @@ TEST_F(BonusConditionTest, ImmuneByDefault) TEST_F(BonusConditionTest, ReceptiveIfMatchesType) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, TBonusSourceID())); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, TBonusSourceID(SpellSchool::FIRE))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 669f13fe5..9d606a798 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -64,7 +64,7 @@ TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::WATER))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::WATER))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -72,7 +72,7 @@ TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -80,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID::NONE, TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index 10aee7053..a4ca85066 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -10,7 +10,6 @@ #include "StdInc.h" #include "TargetConditionItemFixture.h" -#include "../../../lib/bonuses/BonusSubtypes.h" //FIXME: Orb of vulnerability mechanics is not such trivial (mantis issue 1791) //TODO: NEGATE_ALL_NATURAL_IMMUNITIES special cases: dispel, chain lightning @@ -58,7 +57,7 @@ TEST_P(ImmunityNegationConditionTest, WithHeroNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID::NONE, BonusSubtypes::immunityEnemyHero)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID(), BonusSubtypeID::immunityEnemyHero)); EXPECT_EQ(isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -67,7 +66,7 @@ TEST_P(ImmunityNegationConditionTest, WithBattleWideNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID::NONE, BonusSubtypes::immunityBattleWide)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID(), BonusSubtypeID::immunityBattleWide)); //This should return if ownerMatches, because anyone should cast onto owner's stacks, but not on enemyStacks EXPECT_EQ(ownerMatches && isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalLevelConditionTest.cpp b/test/spells/targetConditions/NormalLevelConditionTest.cpp index 6dada367b..6d74de6ac 100644 --- a/test/spells/targetConditions/NormalLevelConditionTest.cpp +++ b/test/spells/targetConditions/NormalLevelConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(NormalLevelConditionTest, DefaultForNormal) TEST_P(NormalLevelConditionTest, ReceptiveNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(4)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -66,7 +66,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveNormal) TEST_P(NormalLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(0)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -75,7 +75,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveAbility) TEST_P(NormalLevelConditionTest, ImmuneNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(2)); EXPECT_EQ(!isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 4e30b8f1a..264f409d1 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) TEST_P(NormalSpellConditionTest, ChecksNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID::NONE, TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); if(immuneSpell == castSpell) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp index fac4e3bed..fac906601 100644 --- a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp +++ b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp @@ -31,7 +31,7 @@ public: EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); if(hasBonus) - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, TBonusSourceID())); } protected: diff --git a/test/spells/targetConditions/SpellEffectConditionTest.cpp b/test/spells/targetConditions/SpellEffectConditionTest.cpp index 59af850aa..f96af453f 100644 --- a/test/spells/targetConditions/SpellEffectConditionTest.cpp +++ b/test/spells/targetConditions/SpellEffectConditionTest.cpp @@ -60,7 +60,7 @@ TEST_F(SpellEffectConditionTest, ImmuneIfHasEffectFromOtherSpell) TEST_F(SpellEffectConditionTest, ImmuneIfHasNoSpellEffects) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, TBonusSourceID::NONE)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, TBonusSourceID())); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } From 3b010d9596c195218ecd871e423e629a0fbcb16a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 15 Oct 2023 16:50:24 +0300 Subject: [PATCH 0908/1248] Renamed MetaIdentifier to VariantIdentifier --- cmake_modules/VCMI_lib.cmake | 2 +- lib/JsonNode.cpp | 129 ++++++++++++++++-- lib/bonuses/Bonus.h | 6 +- .../{MetaIdentifier.h => VariantIdentifier.h} | 24 ++-- 4 files changed, 135 insertions(+), 26 deletions(-) rename lib/constants/{MetaIdentifier.h => VariantIdentifier.h} (69%) diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index d019bc146..0cf3914ab 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -374,7 +374,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/constants/EntityIdentifiers.h ${MAIN_LIB_DIR}/constants/Enumerations.h ${MAIN_LIB_DIR}/constants/IdentifierBase.h - ${MAIN_LIB_DIR}/constants/MetaIdentifier.h + ${MAIN_LIB_DIR}/constants/VariantIdentifier.h ${MAIN_LIB_DIR}/constants/NumericConstants.h ${MAIN_LIB_DIR}/constants/StringConstants.h diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index ed74ec600..5cf575c84 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -425,6 +425,14 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json return; } + if (node.isNumber()) // Compatibility code for 1.3 or older + { + VLC->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier) + { + subtype = BonusSubtypeID(identifier); + }); + } + if (!node.isString()) { logMod->warn("Bonus subtype must be string!"); @@ -432,16 +440,114 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json return; } - VLC->identifiers()->requestIdentifier(node, [&subtype, node](int32_t identifier) + switch (type) { - assert(0); //TODO - subtype = BonusSubtypeID(identifier); - }); + case BonusType::MAGIC_SCHOOL_SKILL: + case BonusType::SPELL_DAMAGE: + case BonusType::SPELLS_OF_SCHOOL: + case BonusType::SPELL_DAMAGE_REDUCTION: + { + VLC->identifiers()->requestIdentifier( "spellSchool", node, [&subtype](int32_t identifier) + { + subtype = SpellSchool(identifier); + }); + break; + } + case BonusType::NO_TERRAIN_PENALTY: + { + VLC->identifiers()->requestIdentifier( "terrain", node, [&subtype](int32_t identifier) + { + subtype = TerrainId(identifier); + }); + break; + } + case BonusType::PRIMARY_SKILL: + { + VLC->identifiers()->requestIdentifier( "primarySkill", node, [&subtype](int32_t identifier) + { + subtype = PrimarySkill(identifier); + }); + break; + } + case BonusType::IMPROVED_NECROMANCY: + case BonusType::HERO_GRANTS_ATTACKS: + case BonusType::BONUS_DAMAGE_CHANCE: + case BonusType::BONUS_DAMAGE_PERCENTAGE: + case BonusType::SPECIAL_UPGRADE: + case BonusType::HATE: + case BonusType::SUMMON_GUARDIANS: + { + VLC->identifiers()->requestIdentifier( "creature", node, [&subtype](int32_t identifier) + { + subtype = CreatureID(identifier); + }); + break; + } + case BonusType::SPECIAL_SPELL_LEV: + case BonusType::SPECIFIC_SPELL_DAMAGE: + case BonusType::SPELL: + case BonusType::OPENING_BATTLE_SPELL: + case BonusType::SPELL_LIKE_ATTACK: + case BonusType::CATAPULT: + case BonusType::CATAPULT_EXTRA_SHOTS: + case BonusType::HEALER: + case BonusType::SPELLCASTER: + case BonusType::ENCHANTER: + case BonusType::SPELL_AFTER_ATTACK: + case BonusType::SPELL_BEFORE_ATTACK: + case BonusType::SPECIFIC_SPELL_POWER: + case BonusType::ENCHANTED: + case BonusType::MORE_DAMAGE_FROM_SPELL: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier) + { + subtype = SpellID(identifier); + }); + break; + } + case BonusType::GENERATE_RESOURCE: + { + VLC->identifiers()->requestIdentifier( "resource", node, [&subtype](int32_t identifier) + { + subtype = GameResID(identifier); + }); + break; + } + case BonusType::MOVEMENT: + case BonusType::WATER_WALKING: + case BonusType::FLYING_MOVEMENT: + case BonusType::SPECIAL_PECULIAR_ENCHANT: + case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES: + case BonusType::CREATURE_DAMAGE: + case BonusType::FLYING: + case BonusType::GENERAL_DAMAGE_REDUCTION: + case BonusType::PERCENTAGE_DAMAGE_BOOST: + case BonusType::SOUL_STEAL: + case BonusType::TRANSMUTATION: + case BonusType::DESTRUCTION: + case BonusType::DEATH_STARE: + case BonusType::REBIRTH: + case BonusType::VISIONS: + case BonusType::SPELLS_OF_LEVEL: // spell level + case BonusType::CREATURE_GROWTH: // creature level + { + VLC->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier) + { + subtype = BonusSubtypeID(identifier); + }); + break; + } + default: + for(const auto & i : bonusNameMap) + if(i.second == type) + logMod->warn("Bonus type %s does not supports subtypes!", i.first ); + subtype = TBonusSubtype(); + } } static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) { - assert(0); + assert(0);//TODO } std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) @@ -895,7 +1001,8 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) if (!value->isNull()) b->source = static_cast(parseByMap(bonusSourceMap, value, "source type ")); - loadBonusSourceInstance(b->sid, b->source, ability["sourceID"]); + if (!ability["sourceID"].isNull()) + loadBonusSourceInstance(b->sid, b->source, ability["sourceID"]); value = &ability["targetSourceType"]; if (!value->isNull()) @@ -960,20 +1067,24 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) ret = ret.And(base.Not()); } + BonusType type = BonusType::NONE; + // Actual selector parser value = &ability["type"]; if(value->isString()) { auto it = bonusNameMap.find(value->String()); if(it != bonusNameMap.end()) + { + type = it->second; ret = ret.And(Selector::type()(it->second)); + } } value = &ability["subtype"]; - if(!value->isNull()) + if(!value->isNull() && type != BonusType::NONE) { TBonusSubtype subtype; - assert(0); //TODO - loadBonusSubtype(subtype, BonusType::NONE, ability); + loadBonusSubtype(subtype, type, ability); ret = ret.And(Selector::subtype()(subtype)); } value = &ability["sourceType"]; diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 4abbe9cc9..47273f73d 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -11,7 +11,7 @@ #include "BonusEnum.h" #include "BonusSubtypeID.h" -#include "../constants/MetaIdentifier.h" +#include "../constants/VariantIdentifier.h" #include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,8 +25,8 @@ class IUpdater; class BonusList; class CSelector; -using TBonusSubtype = MetaIdentifier; -using TBonusSourceID = MetaIdentifier; +using TBonusSubtype = VariantIdentifier; +using TBonusSourceID = VariantIdentifier; using TBonusListPtr = std::shared_ptr; using TConstBonusListPtr = std::shared_ptr; using TLimiterPtr = std::shared_ptr; diff --git a/lib/constants/MetaIdentifier.h b/lib/constants/VariantIdentifier.h similarity index 69% rename from lib/constants/MetaIdentifier.h rename to lib/constants/VariantIdentifier.h index d882f9e0d..6b0394f65 100644 --- a/lib/constants/MetaIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -1,5 +1,5 @@ /* - * MetaIdentifier.h, part of VCMI engine + * VariantIdentifier.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -15,37 +15,35 @@ VCMI_LIB_NAMESPACE_BEGIN /// This class represents field that may contain value of multiple different identifer types template -class DLL_LINKAGE MetaIdentifier +class DLL_LINKAGE VariantIdentifier { std::variant value; public: - MetaIdentifier() + VariantIdentifier() {} template - MetaIdentifier(const IdentifierType & identifier) + VariantIdentifier(const IdentifierType & identifier) : value(identifier) {} int32_t getNum() const { - std::optional result; + int32_t result; std::visit([&result] (const auto& v) { result = v.getNum(); }, value); - assert(result.has_value()); - return result.value_or(-1); + return result; } std::string toString() const { - std::optional result; + std::string result; std::visit([&result] (const auto& v) { result = v.encode(v.getNum()); }, value); - assert(result.has_value()); - return result.value_or(""); + return result; } template @@ -65,15 +63,15 @@ public: h & value; } - bool operator == (const MetaIdentifier & other) const + bool operator == (const VariantIdentifier & other) const { return value == other.value; } - bool operator != (const MetaIdentifier & other) const + bool operator != (const VariantIdentifier & other) const { return value != other.value; } - bool operator < (const MetaIdentifier & other) const + bool operator < (const VariantIdentifier & other) const { return value < other.value; } From e54ba7b97772fc6a79d162893c83d0ab5008c44a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 15 Oct 2023 17:27:57 +0300 Subject: [PATCH 0909/1248] Implement loading of bonus source type --- lib/JsonNode.cpp | 102 ++++++++++++++++++++++++++++++++++++++-- lib/bonuses/BonusEnum.h | 1 - 2 files changed, 97 insertions(+), 6 deletions(-) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 5cf575c84..977878123 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -427,10 +427,9 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json if (node.isNumber()) // Compatibility code for 1.3 or older { - VLC->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier) - { - subtype = BonusSubtypeID(identifier); - }); + logMod->warn("Bonus subtype must be string!"); + subtype = BonusSubtypeID(node.Integer()); + return; } if (!node.isString()) @@ -547,7 +546,100 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) { - assert(0);//TODO + if (node.isNull()) + { + sourceInstance = TBonusSourceID(); + return; + } + + if (node.isNumber()) // Compatibility code for 1.3 or older + { + logMod->warn("Bonus source must be string!"); + sourceInstance = TBonusSourceID(node.Integer()); + return; + } + + if (!node.isString()) + { + logMod->warn("Bonus source must be string!"); + sourceInstance = TBonusSourceID(); + return; + } + + switch (sourceType) + { + case BonusSource::ARTIFACT: + case BonusSource::ARTIFACT_INSTANCE: + { + VLC->identifiers()->requestIdentifier( "artifact", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = ArtifactID(identifier); + }); + break; + } + case BonusSource::OBJECT: + case BonusSource::HERO_BASE_SKILL: + assert(0); // TODO + sourceInstance = ObjectInstanceID(); + break; + case BonusSource::CREATURE_ABILITY: + { + VLC->identifiers()->requestIdentifier( "creature", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = CreatureID(identifier); + }); + break; + } + case BonusSource::TERRAIN_OVERLAY: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = BattleField(identifier); + }); + break; + } + case BonusSource::SPELL_EFFECT: + { + VLC->identifiers()->requestIdentifier( "spell", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = SpellID(identifier); + }); + break; + } + case BonusSource::TOWN_STRUCTURE: + assert(0); // TODO + sourceInstance = BuildingTypeUniqueID(); + break; + case BonusSource::SECONDARY_SKILL: + { + VLC->identifiers()->requestIdentifier( "secondarySkill", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = SecondarySkill(identifier); + }); + break; + } + case BonusSource::HERO_SPECIAL: + { + VLC->identifiers()->requestIdentifier( "hero", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = HeroTypeID(identifier); + }); + break; + } + case BonusSource::CAMPAIGN_BONUS: + assert(0); // TODO + sourceInstance = CampaignScenarioID(); + break; + case BonusSource::ARMY: + case BonusSource::STACK_EXPERIENCE: + case BonusSource::COMMANDER: + case BonusSource::GLOBAL: + case BonusSource::TERRAIN_NATIVE: + case BonusSource::OTHER: + default: + sourceInstance = TBonusSourceID(); + break; + } } std::shared_ptr JsonUtils::parseBonus(const JsonVector & ability_vec) diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 9030b14a7..2118eaeec 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -186,7 +186,6 @@ class JsonNode; BONUS_SOURCE(HERO_SPECIAL)\ BONUS_SOURCE(ARMY)\ BONUS_SOURCE(CAMPAIGN_BONUS)\ - BONUS_SOURCE(SPECIAL_WEEK)\ BONUS_SOURCE(STACK_EXPERIENCE)\ BONUS_SOURCE(COMMANDER) /*TODO: consider using simply STACK_INSTANCE */\ BONUS_SOURCE(GLOBAL) /*used for base bonuses which all heroes or all stacks should have*/\ From 36a1d6c4158b8aeb46004798c54f3846424d32ef Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 15 Oct 2023 18:36:04 +0300 Subject: [PATCH 0910/1248] Removed remaining integer bonus subtypes from configs --- config/artifacts.json | 219 ++++++++++++------------- config/battlefields.json | 3 - config/commanders.json | 34 ++-- config/creatures/fortress.json | 2 +- config/creatures/inferno.json | 4 +- config/creatures/necropolis.json | 6 +- config/creatures/neutral.json | 6 +- config/factions/dungeon.json | 2 +- config/factions/fortress.json | 4 +- config/factions/stronghold.json | 2 +- config/factions/tower.json | 2 +- config/gameConfig.json | 4 +- config/heroes/castle.json | 7 +- config/heroes/conflux.json | 36 ++-- config/heroes/dungeon.json | 3 +- config/heroes/fortress.json | 5 +- config/heroes/necropolis.json | 1 - config/heroes/rampart.json | 5 +- config/heroes/special.json | 24 +-- config/heroes/stronghold.json | 5 +- config/heroes/tower.json | 3 +- config/objects/rewardableBonusing.json | 4 +- config/skills.json | 13 +- config/spells/ability.json | 6 +- config/spells/adventure.json | 15 +- config/spells/moats.json | 14 +- config/spells/timed.json | 22 ++- docs/modders/Bonus/Bonus_Types.md | 18 +- lib/JsonNode.cpp | 16 +- lib/bonuses/BonusParams.cpp | 1 + lib/modding/IdentifierStorage.cpp | 42 ++++- 31 files changed, 279 insertions(+), 249 deletions(-) diff --git a/config/artifacts.json b/config/artifacts.json index 714405b47..486618aac 100644 --- a/config/artifacts.json +++ b/config/artifacts.json @@ -42,7 +42,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -55,7 +55,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -68,7 +68,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -81,7 +81,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -94,7 +94,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" @@ -107,13 +107,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 12, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : -3, "valueType" : "BASE_NUMBER" @@ -126,7 +126,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -139,7 +139,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -152,7 +152,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -165,7 +165,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -178,7 +178,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" @@ -191,13 +191,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 12, "valueType" : "BASE_NUMBER" }, { - "subtype" : 0, + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : -3, "valueType" : "BASE_NUMBER" @@ -210,7 +210,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" @@ -223,7 +223,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -236,7 +236,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -249,7 +249,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -262,7 +262,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -275,13 +275,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 10, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : -2, "valueType" : "BASE_NUMBER" @@ -294,7 +294,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" @@ -307,7 +307,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -320,7 +320,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -333,7 +333,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -346,7 +346,7 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -359,13 +359,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 10, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : -2, "valueType" : "BASE_NUMBER" @@ -378,25 +378,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" @@ -409,25 +409,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -440,25 +440,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -471,25 +471,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -502,25 +502,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -533,25 +533,25 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" @@ -564,13 +564,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" @@ -583,13 +583,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -602,13 +602,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -621,13 +621,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -640,13 +640,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 1, "valueType" : "BASE_NUMBER" @@ -659,13 +659,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 2, "valueType" : "BASE_NUMBER" @@ -678,13 +678,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -697,13 +697,13 @@ { "bonuses" : [ { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 4, "valueType" : "BASE_NUMBER" @@ -870,7 +870,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "MAGIC_RESISTANCE", "val" : 5, "valueType" : "BASE_NUMBER" @@ -883,7 +882,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "MAGIC_RESISTANCE", "val" : 10, "valueType" : "BASE_NUMBER" @@ -896,7 +894,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "MAGIC_RESISTANCE", "val" : 15, "valueType" : "BASE_NUMBER" @@ -910,7 +907,7 @@ "bonuses" : [ { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "val" : 5, "valueType" : "ADDITIVE_VALUE", "limiters" : [ @@ -918,10 +915,10 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "PERCENTAGE_DAMAGE_BOOST", - 1, + "damageTypeRanged", { "type" : "SECONDARY_SKILL", - "id" : "skill.archery" + "id" : "secondarySkill.archery" } ] } @@ -936,7 +933,7 @@ "bonuses" : [ { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "val" : 10, "valueType" : "ADDITIVE_VALUE", "limiters" : [ @@ -944,10 +941,10 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "PERCENTAGE_DAMAGE_BOOST", - 1, + "damageTypeRanged", { "type" : "SECONDARY_SKILL", - "id" : "skill.archery" + "id" : "secondarySkill.archery" } ] } @@ -962,7 +959,7 @@ "bonuses" : [ { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "val" : 15, "valueType" : "ADDITIVE_VALUE", "limiters" : [ @@ -970,10 +967,10 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "PERCENTAGE_DAMAGE_BOOST", - 1, + "damageTypeRanged", { "type" : "SECONDARY_SKILL", - "id" : "skill.archery" + "id" : "secondarySkill.archery" } ] } @@ -987,7 +984,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "val" : 5, "valueType" : "ADDITIVE_VALUE" @@ -1000,7 +996,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "val" : 10, "valueType" : "ADDITIVE_VALUE" @@ -1013,7 +1008,6 @@ { "bonuses" : [ { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "val" : 15, "valueType" : "ADDITIVE_VALUE" @@ -1075,7 +1069,7 @@ "bonuses" : [ { "type" : "MOVEMENT", - "subtype" : 1, + "subtype" : "heroMovementLand", "val" : 300, "valueType" : "ADDITIVE_VALUE" } @@ -1089,7 +1083,7 @@ "bonuses" : [ { "type" : "MOVEMENT", - "subtype" : 0, + "subtype" : "heroMovementSea", "val" : 1000, "valueType" : "ADDITIVE_VALUE" } @@ -1277,7 +1271,7 @@ "bonuses" : [ { "type" : "SPELLS_OF_SCHOOL", - "subtype" : 1, + "subtype" : "fire", "val" : 0, "valueType" : "BASE_NUMBER" } @@ -1290,7 +1284,7 @@ "bonuses" : [ { "type" : "SPELLS_OF_SCHOOL", - "subtype" : 0, + "subtype" : "air", "val" : 0, "valueType" : "BASE_NUMBER" } @@ -1303,7 +1297,7 @@ "bonuses" : [ { "type" : "SPELLS_OF_SCHOOL", - "subtype" : 2, + "subtype" : "water", "val" : 0, "valueType" : "BASE_NUMBER" } @@ -1316,7 +1310,7 @@ "bonuses" : [ { "type" : "SPELLS_OF_SCHOOL", - "subtype" : 3, + "subtype" : "earth", "val" : 0, "valueType" : "BASE_NUMBER" } @@ -1342,14 +1336,12 @@ "bonuses" : [ { "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, "type" : "NO_DISTANCE_PENALTY", "val" : 0, "valueType" : "ADDITIVE_VALUE" }, { "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, "type" : "NO_WALL_PENALTY", "val" : 0, "valueType" : "ADDITIVE_VALUE" @@ -1362,7 +1354,7 @@ { "bonuses" : [ { - "subtype" : 35, + "subtype" : "dispel", "type" : "SPELL_IMMUNITY", "val" : 0, "valueType" : "BASE_NUMBER", @@ -1377,14 +1369,14 @@ "bonuses" : [ { "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 0, + "subtype" : "immunityBattleWide", "val" : 0, "valueType" : "BASE_NUMBER", "propagator": "BATTLE_WIDE" }, { "type" : "NEGATE_ALL_NATURAL_IMMUNITIES", - "subtype" : 1, + "subtype" : "immunityEnemyHero", "val" : 0, "valueType" : "BASE_NUMBER" }, @@ -1457,7 +1449,7 @@ "bonuses" : [ { "type" : "MOVEMENT", - "subtype" : 1, + "subtype" : "heroMovementLand", "val" : 600, "valueType" : "ADDITIVE_VALUE" } @@ -1738,7 +1730,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : 1, + "subtype" : "creatureLevel1", "val" : 5, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1751,7 +1743,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : 2, + "subtype" : "creatureLevel2", "val" : 4, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1764,7 +1756,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : 3, + "subtype" : "creatureLevel3", "val" : 3, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1777,7 +1769,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : 4, + "subtype" : "creatureLevel4", "val" : 2, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1790,7 +1782,7 @@ "bonuses" : [ { "type" : "CREATURE_GROWTH", - "subtype" : 5, + "subtype" : "creatureLevel5", "val" : 1, "propagator": "VISITED_TOWN_AND_VISITOR" } @@ -1807,7 +1799,7 @@ }, { "type" : "MOVEMENT", - "subtype" : 0, + "subtype" : "heroMovementSea", "val" : 500, "valueType" : "ADDITIVE_VALUE" }, @@ -1831,7 +1823,7 @@ { "bonuses" : [ { - "subtype" : 5, + "subtype" : "spellLevel5", "type" : "SPELLS_OF_LEVEL", "val" : 3, "valueType" : "BASE_NUMBER" @@ -1867,14 +1859,14 @@ "bonuses" : [ { "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" }, { "limiters" : ["DRAGON_NATURE"], - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5, "valueType" : "BASE_NUMBER" @@ -1900,25 +1892,25 @@ "addInfo" : 1 }, { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 3, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" @@ -2103,25 +2095,25 @@ "valueType" : "INDEPENDENT_MAX" }, { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.spellpower", + "subtype" : "primarySkill.spellpower", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" }, { - "subtype" : "primSkill.knowledge", + "subtype" : "primarySkill.knowledge", "type" : "PRIMARY_SKILL", "val" : 6, "valueType" : "BASE_NUMBER" @@ -2183,21 +2175,18 @@ "bonuses" : [ { "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, "type" : "NO_DISTANCE_PENALTY", "val" : 0, "valueType" : "ADDITIVE_VALUE" }, { "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, "type" : "NO_WALL_PENALTY", "val" : 0, "valueType" : "ADDITIVE_VALUE" }, { "limiters" : ["SHOOTER_ONLY"], - "subtype" : 0, "type" : "FREE_SHOOTING", "val" : 0, "valueType" : "ADDITIVE_VALUE" diff --git a/config/battlefields.json b/config/battlefields.json index b79f705a3..c711bfc46 100644 --- a/config/battlefields.json +++ b/config/battlefields.json @@ -130,21 +130,18 @@ "bonuses": [ { "type" : "NO_MORALE", - "subtype" : 0, "val" : 0, "valueType" : "INDEPENDENT_MIN", "description" : "Creatures on Cursed Ground" }, { "type" : "NO_LUCK", - "subtype" : 0, "val" : 0, "valueType" : "INDEPENDENT_MIN", "description" : "Creatures on Cursed Ground" }, { "type" : "BLOCK_MAGIC_ABOVE", - "subtype" : 0, "val" : 1, "valueType" : "INDEPENDENT_MIN" } diff --git a/config/commanders.json b/config/commanders.json index f789003a8..95530ab39 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -3,9 +3,9 @@ //Commander receives these bonuses on level-up "bonusPerLevel": [ - ["CREATURE_DAMAGE", 1, 1, 0 ], //+1 minimum damage - ["CREATURE_DAMAGE", 2, 2, 0 ], //+2 maximum damage - ["STACK_HEALTH", 5, 0, 0 ] //+5 hp + ["CREATURE_DAMAGE", 1, "creatureDamageMin", 0 ], //+1 minimum damage + ["CREATURE_DAMAGE", 2, "creatureDamageMax", 0 ], //+2 maximum damage + ["STACK_HEALTH", 5, null, 0 ] //+5 hp ], //Value of bonuses given by each skill level "skillLevels": @@ -22,20 +22,20 @@ "abilityRequirements": //Two secondary skills needed for each special ability [ - {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, 0, 0 ], "skills": [0, 1]}, - {"ability": ["FEAR", 0, 0, 0 ], "skills": [0, 2]}, - {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, -1, 0 ], "skills": [0, 3]}, - {"ability": ["SHOOTER", 0, 0, 0 ], "skills": [0, 4]}, - {"ability": ["BLOCKS_RETALIATION", 0, 1, 0 ], "skills": [0,5]}, - {"ability": ["UNLIMITED_RETALIATIONS", 0, 0, 0 ], "skills": [1, 2]}, - {"ability": ["ATTACKS_ALL_ADJACENT", 0, 0, 0 ], "skills": [1, 3]}, - {"ability": ["NONE", 30, 0, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn - {"ability": ["FIRE_SHIELD", 1, 1, 0 ], "skills": [1, 5]}, - {"ability": ["ADDITIONAL_ATTACK", 1, 0, 0 ], "skills": [2, 3]}, - {"ability": ["HP_REGENERATION", 50, 0, 0 ], "skills": [2, 4]}, + {"ability": ["ENEMY_DEFENCE_REDUCTION", 50, null, 0 ], "skills": [0, 1]}, + {"ability": ["FEAR", 0, null, 0 ], "skills": [0, 2]}, + {"ability": ["ALWAYS_MAXIMUM_DAMAGE", 0, null, 0 ], "skills": [0, 3]}, + {"ability": ["SHOOTER", 0, null, 0 ], "skills": [0, 4]}, + {"ability": ["BLOCKS_RETALIATION", 0, null, 0 ], "skills": [0,5]}, + {"ability": ["UNLIMITED_RETALIATIONS", 0, null, 0 ], "skills": [1, 2]}, + {"ability": ["ATTACKS_ALL_ADJACENT", 0, null, 0 ], "skills": [1, 3]}, + {"ability": ["NONE", 30, null, 0 ], "skills": [1, 4]}, // TODO: Implement bonus that gives chance to completely block one enemy attack per turn + {"ability": ["FIRE_SHIELD", 1, null, 0 ], "skills": [1, 5]}, + {"ability": ["ADDITIONAL_ATTACK", 1, null, 0 ], "skills": [2, 3]}, + {"ability": ["HP_REGENERATION", 50, null, 0 ], "skills": [2, 4]}, {"ability": ["SPELL_AFTER_ATTACK", 30, "spell.paralyze", 0 ], "skills": [2, 5]}, - {"ability": ["JOUSTING", 5, 0, 0 ], "skills": [3, 4]}, - {"ability": ["DEATH_STARE", 1, 1, 0 ], "skills": [3,5]}, - {"ability": ["FLYING", 0, 0, 0 ], "skills": [4,5]} + {"ability": ["JOUSTING", 5, null, 0 ], "skills": [3, 4]}, + {"ability": ["DEATH_STARE", 1, "deathStareCommander", 0 ], "skills": [3,5]}, + {"ability": ["FLYING", 0, "movementFlying", 0 ], "skills": [4,5]} ] } diff --git a/config/creatures/fortress.json b/config/creatures/fortress.json index 4353e40f6..1b3169052 100644 --- a/config/creatures/fortress.json +++ b/config/creatures/fortress.json @@ -114,7 +114,7 @@ "deathStare" : { "type" : "DEATH_STARE", - "subtype" : 0, + "subtype" : "deathStareGorgon", "val" : 10 } }, diff --git a/config/creatures/inferno.json b/config/creatures/inferno.json index 91a928c64..204432246 100755 --- a/config/creatures/inferno.json +++ b/config/creatures/inferno.json @@ -361,7 +361,7 @@ "FLYING_ARMY" : { // type loaded from crtraits - "subtype" : 1 // teleports + "subtype" : "movementTeleporting" }, "descreaseLuck" : { @@ -415,7 +415,7 @@ "FLYING_ARMY" : { // type loaded from crtraits - "subtype" : 1 // teleports + "subtype" : "movementTeleporting" }, "descreaseLuck" : { diff --git a/config/creatures/necropolis.json b/config/creatures/necropolis.json index f38abe04a..54f1e8cec 100644 --- a/config/creatures/necropolis.json +++ b/config/creatures/necropolis.json @@ -151,8 +151,7 @@ { "noRetalitation" : { - "type" : "BLOCKS_RETALIATION", - "subtype" : 1 + "type" : "BLOCKS_RETALIATION" } }, "upgrades": ["vampireLord"], @@ -180,8 +179,7 @@ { "noRetalitation" : { - "type" : "BLOCKS_RETALIATION", - "subtype" : 1 + "type" : "BLOCKS_RETALIATION" }, "drainsLife" : { diff --git a/config/creatures/neutral.json b/config/creatures/neutral.json index a5f073ba6..a967218cd 100644 --- a/config/creatures/neutral.json +++ b/config/creatures/neutral.json @@ -529,7 +529,7 @@ "visionsMonsters" : { "type" : "VISIONS", - "subtype" : 0, + "subtype" : "visionsMonsters", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" @@ -537,7 +537,7 @@ "visionsHeroes" : { "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" @@ -545,7 +545,7 @@ "visionsTowns" : { "type" : "VISIONS", - "subtype" : 2, + "subtype" : "visionsTowns", "val" : 3, "valueType" : "INDEPENDENT_MAX", "propagator" : "HERO" diff --git a/config/factions/dungeon.json b/config/factions/dungeon.json index ac347b9b1..f864b282e 100644 --- a/config/factions/dungeon.json +++ b/config/factions/dungeon.json @@ -178,7 +178,7 @@ "special3": { "type" : "portalOfSummoning" }, "special4": { "type" : "experienceVisitingBonus" }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, - "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.spellpower", "val": 12 } ] }, + "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.spellpower", "val": 12 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/factions/fortress.json b/config/factions/fortress.json index 403d840ca..38002253a 100644 --- a/config/factions/fortress.json +++ b/config/factions/fortress.json @@ -177,8 +177,8 @@ "special3": { "type" : "attackGarrisonBonus", "requires" : [ "special2" ] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, "bonuses": [ - { "type": "PRIMARY_SKILL", "subtype": "primSkill.attack", "val": 10 }, - { "type": "PRIMARY_SKILL", "subtype": "primSkill.defence", "val": 10 } + { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 10 }, + { "type": "PRIMARY_SKILL", "subtype": "primarySkill.defence", "val": 10 } ] }, diff --git a/config/factions/stronghold.json b/config/factions/stronghold.json index eed97bb94..5e8afda84 100644 --- a/config/factions/stronghold.json +++ b/config/factions/stronghold.json @@ -173,7 +173,7 @@ "special3": { "type" : "ballistaYard", "requires" : [ "blacksmith" ] }, "special4": { "type" : "attackVisitingBonus", "requires" : [ "fort" ] }, "grail": { "id" : 26, "mode" : "grail", "produce": { "gold": 5000 }, - "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.attack", "val": 20 } ] }, + "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.attack", "val": 20 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/factions/tower.json b/config/factions/tower.json index 572a9e156..488759b91 100644 --- a/config/factions/tower.json +++ b/config/factions/tower.json @@ -175,7 +175,7 @@ "special2": { "type" : "lookoutTower", "height" : "high", "requires" : [ "fort" ] }, "special3": { "type" : "library", "requires" : [ "mageGuild1" ] }, "special4": { "type" : "knowledgeVisitingBonus", "requires" : [ "mageGuild1" ] }, - "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primSkill.knowledge", "val": 15 } ] }, + "grail": { "height" : "skyship", "produce" : { "gold": 5000 }, "bonuses": [ { "type": "PRIMARY_SKILL", "subtype": "primarySkill.knowledge", "val": 15 } ] }, "dwellingLvl1": { "id" : 30, "requires" : [ "fort" ] }, "dwellingLvl2": { "id" : 31, "requires" : [ "dwellingLvl1" ] }, diff --git a/config/gameConfig.json b/config/gameConfig.json index df6092966..5b6e17b5c 100644 --- a/config/gameConfig.json +++ b/config/gameConfig.json @@ -430,7 +430,7 @@ "landMovement" : { "type" : "MOVEMENT", //Basic land movement - "subtype" : 1, + "subtype" : "heroMovementLand", "val" : 1300, "valueType" : "BASE_NUMBER", "updater" : { @@ -446,7 +446,7 @@ "seaMovement" : { "type" : "MOVEMENT", //Basic sea movement - "subtype" : 0, + "subtype" : "heroMovementSea", "val" : 1500, "valueType" : "BASE_NUMBER" } diff --git a/config/heroes/castle.json b/config/heroes/castle.json index 634897819..9397c3316 100644 --- a/config/heroes/castle.json +++ b/config/heroes/castle.json @@ -13,7 +13,7 @@ "bonuses" : { "archery" : { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "updater" : "TIMES_HERO_LEVEL", "val" : 5, "valueType" : "PERCENT_TO_TARGET_TYPE", @@ -64,7 +64,7 @@ "bonuses" : { "navigation" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -183,7 +183,7 @@ "type" : "HAS_ANOTHER_BONUS_LIMITER", "parameters" : [ "GENERAL_DAMAGE_PREMY", - 1, + null, { "type" : "SPELL_EFFECT", "id" : "spell.bless" @@ -268,7 +268,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/conflux.json b/config/heroes/conflux.json index 9a530a24c..1bda11cee 100755 --- a/config/heroes/conflux.json +++ b/config/heroes/conflux.json @@ -21,8 +21,8 @@ "val" : 3 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -48,16 +48,16 @@ "bonuses" : { "damage" : { "type" : "CREATURE_DAMAGE", - "subtype" : 0, + "subtype" : "creatureDamageBoth", "val" : 5 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1 } @@ -85,17 +85,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 2 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -120,7 +120,7 @@ "type" : "CREATURE_TYPE_LIMITER" } ], - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -149,8 +149,8 @@ "val" : 3 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -176,16 +176,16 @@ "bonuses" : { "damage" : { "type" : "CREATURE_DAMAGE", - "subtype" : 0, + "subtype" : "creatureDamageMin", "val" : 5 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 1 } @@ -212,17 +212,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 2 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 1 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 } @@ -248,7 +248,7 @@ "type" : "CREATURE_TYPE_LIMITER" } ], - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 2 } diff --git a/config/heroes/dungeon.json b/config/heroes/dungeon.json index 60de72575..e08959d35 100644 --- a/config/heroes/dungeon.json +++ b/config/heroes/dungeon.json @@ -88,7 +88,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -229,7 +229,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/fortress.json b/config/heroes/fortress.json index 206164ff3..e43c41ec9 100644 --- a/config/heroes/fortress.json +++ b/config/heroes/fortress.json @@ -69,7 +69,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -192,7 +192,7 @@ "bonuses" : { "navigation" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -307,7 +307,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/necropolis.json b/config/heroes/necropolis.json index f738b387e..491d377f8 100644 --- a/config/heroes/necropolis.json +++ b/config/heroes/necropolis.json @@ -214,7 +214,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/rampart.json b/config/heroes/rampart.json index a4bfbfc8b..1001e553b 100644 --- a/config/heroes/rampart.json +++ b/config/heroes/rampart.json @@ -13,7 +13,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -132,7 +132,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -245,7 +245,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/special.json b/config/heroes/special.json index e26c2ff58..b6b945461 100644 --- a/config/heroes/special.json +++ b/config/heroes/special.json @@ -117,17 +117,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 10 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5 } @@ -156,17 +156,17 @@ }, "bonuses" : { "damage" : { - "subtype" : 0, + "subtype" : "creatureDamageBoth", "type" : "CREATURE_DAMAGE", "val" : 10 }, "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 5 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 5 } @@ -197,8 +197,8 @@ "val" : 5 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } } }, @@ -241,8 +241,8 @@ "val" : 5 }, "bonuses" : { - "attack" : { "subtype" : "primSkill.attack" }, - "defence" : { "subtype" : "primSkill.defence" } + "attack" : { "subtype" : "primarySkill.attack" }, + "defence" : { "subtype" : "primarySkill.defence" } } }, "army" : @@ -313,12 +313,12 @@ }, "bonuses" : { "attack" : { - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "type" : "PRIMARY_SKILL", "val" : 4 }, "defence" : { - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "type" : "PRIMARY_SKILL", "val" : 2 }, diff --git a/config/heroes/stronghold.json b/config/heroes/stronghold.json index 6f08653b6..e940ef6a8 100644 --- a/config/heroes/stronghold.json +++ b/config/heroes/stronghold.json @@ -95,7 +95,7 @@ "specialty" : { "bonuses" : { "offence" : { - "subtype" : 0, + "subtype" : "damageTypeMelee", "type" : "PERCENTAGE_DAMAGE_BOOST", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -173,7 +173,7 @@ "bonuses" : { "logistics" : { "targetSourceType" : "SECONDARY_SKILL", - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -262,7 +262,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/heroes/tower.json b/config/heroes/tower.json index 582f242d2..41380fc1c 100644 --- a/config/heroes/tower.json +++ b/config/heroes/tower.json @@ -58,7 +58,7 @@ "bonuses" : { "armorer" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "targetSourceType" : "SECONDARY_SKILL", "updater" : "TIMES_HERO_LEVEL", "val" : 5, @@ -191,7 +191,6 @@ "specialty" : { "bonuses" : { "eagleEye" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "updater" : "TIMES_HERO_LEVEL", "val" : 5, diff --git a/config/objects/rewardableBonusing.json b/config/objects/rewardableBonusing.json index b7ee79698..9461689ec 100644 --- a/config/objects/rewardableBonusing.json +++ b/config/objects/rewardableBonusing.json @@ -356,7 +356,7 @@ }, "message" : 138, "movePoints" : 400, - "bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1, "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ], + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ], "changeCreatures" : { "cavalier" : "champion" } @@ -364,7 +364,7 @@ { "message" : 137, "movePoints" : 400, - "bonuses" : [ { "type" : "MOVEMENT", "subtype" : 1, "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] + "bonuses" : [ { "type" : "MOVEMENT", "subtype" : "heroMovementLand", "val" : 400, "valueType" : "ADDITIVE_VALUE", "duration" : "ONE_WEEK"} ] } ] } diff --git a/config/skills.json b/config/skills.json index 601d48d1d..71974db01 100644 --- a/config/skills.json +++ b/config/skills.json @@ -31,7 +31,7 @@ "effects" : { "main" : { "type" : "PERCENTAGE_DAMAGE_BOOST", - "subtype" : 1, + "subtype" : "damageTypeRanged", "valueType" : "BASE_NUMBER" } } @@ -57,7 +57,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 1, + "subtype" : "heroMovementLand", "type" : "MOVEMENT", "valueType" : "PERCENT_TO_BASE" } @@ -144,7 +144,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, + "subtype" : "heroMovementSea", "type" : "MOVEMENT", "valueType" : "PERCENT_TO_BASE" } @@ -310,12 +310,10 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, "type" : "LEARN_BATTLE_SPELL_CHANCE", "valueType" : "BASE_NUMBER" }, "val2" : { - "subtype" : -1, "type" : "LEARN_BATTLE_SPELL_LEVEL_LIMIT", "valueType" : "BASE_NUMBER" } @@ -518,7 +516,6 @@ "base" : { "effects" : { "main" : { - "subtype" : -1, "type" : "LEARN_MEETING_SPELL_LIMIT", "valueType" : "BASE_NUMBER" } @@ -657,7 +654,7 @@ "base" : { "effects" : { "main" : { - "subtype" : 0, + "subtype" : "damageTypeMelee", "type" : "PERCENTAGE_DAMAGE_BOOST", "valueType" : "BASE_NUMBER" } @@ -685,7 +682,7 @@ "effects" : { "main" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : -1, + "subtype" : "damageTypeAll", "valueType" : "BASE_NUMBER" } } diff --git a/config/spells/ability.json b/config/spells/ability.json index b864ca09f..d9e945722 100644 --- a/config/spells/ability.json +++ b/config/spells/ability.json @@ -141,13 +141,13 @@ "attack" : { "val" : -2, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "duration" : "N_TURNS" }, "defence" : { "val" : -2, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "duration" : "N_TURNS" } } @@ -384,7 +384,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "duration" : "PERMANENT", "valueType" : "ADDITIVE_VALUE" } diff --git a/config/spells/adventure.json b/config/spells/adventure.json index aa6c63787..ef79c679e 100644 --- a/config/spells/adventure.json +++ b/config/spells/adventure.json @@ -46,7 +46,7 @@ "effects" : { "visionsMonsters" : { "type" : "VISIONS", - "subtype" : 0, + "subtype" : "visionsMonsters", "duration" : "ONE_DAY", "val" : 1, "valueType" : "INDEPENDENT_MAX" @@ -60,10 +60,10 @@ }, "visionsHeroes" :{ "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "duration" : "ONE_DAY", "val" : 2, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" } } @@ -75,17 +75,17 @@ }, "visionsHeroes" :{ "type" : "VISIONS", - "subtype" : 1, + "subtype" : "visionsHeroes", "duration" : "ONE_DAY", "val" : 3, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" }, "visionsTowns" :{ "type" : "VISIONS", - "subtype" : 2, + "subtype" : "visionsTowns", "duration" : "ONE_DAY", "val" : 3, - "valueType" : "INDEPENDENT_MAX" + "valueType" : "INDEPENDENT_MAX" } } } @@ -123,7 +123,6 @@ "effects" : { "stealth" : { "type" : "DISGUISED", - "subtype" : 0, //required "duration" : "ONE_DAY", "val" : 1, "valueType" : "INDEPENDENT_MAX" diff --git a/config/spells/moats.json b/config/spells/moats.json index d7869b469..71eea7fe8 100644 --- a/config/spells/moats.json +++ b/config/spells/moats.json @@ -72,7 +72,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -169,7 +169,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -317,7 +317,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -414,7 +414,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -511,7 +511,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -608,7 +608,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } @@ -705,7 +705,7 @@ "primarySkill" : { "val" : -3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "valueType" : "ADDITIVE_VALUE" } } diff --git a/config/spells/timed.json b/config/spells/timed.json index 758e28abb..0817cbad1 100644 --- a/config/spells/timed.json +++ b/config/spells/timed.json @@ -17,7 +17,7 @@ "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : 0, + "subtype" : "damageTypeMelee", "duration" : "N_TURNS" } } @@ -48,7 +48,7 @@ "effects" : { "generalDamageReduction" : { "type" : "GENERAL_DAMAGE_REDUCTION", - "subtype" : 1, + "subtype" : "damageTypeRanged", "duration" : "N_TURNS" } } @@ -367,7 +367,6 @@ "alwaysMaximumDamage" : { "val" : 0, "type" : "ALWAYS_MAXIMUM_DAMAGE", - "subtype" : -1, "valueType" : "INDEPENDENT_MAX", "duration" : "N_TURNS" } @@ -421,7 +420,6 @@ "addInfo" : 0, "val" : 0, "type" : "ALWAYS_MINIMUM_DAMAGE", - "subtype" : -1, "valueType" : "INDEPENDENT_MAX", "duration" : "N_TURNS" } @@ -476,7 +474,7 @@ "primarySkill" : { "val" : 3, "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "effectRange" : "ONLY_MELEE_FIGHT", "duration" : "N_TURNS" } @@ -527,7 +525,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : 3, "effectRange" : "ONLY_DISTANCE_FIGHT", "duration" : "N_TURNS" @@ -576,7 +574,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : -3, "duration" : "N_TURNS" } @@ -623,7 +621,7 @@ "effects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : 3, "duration" : "N_TURNS" } @@ -667,7 +665,7 @@ "cumulativeEffects" : { "primarySkill" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : -3, "valueType" : "ADDITIVE_VALUE", "duration" : "PERMANENT" @@ -710,13 +708,13 @@ "effects" : { "attack" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.attack", + "subtype" : "primarySkill.attack", "val" : 2, "duration" : "N_TURNS" }, "defence" : { "type" : "PRIMARY_SKILL", - "subtype" : "primSkill.defence", + "subtype" : "primarySkill.defence", "val" : 2, "duration" : "N_TURNS" }, @@ -1401,7 +1399,7 @@ "notActive" : { "val" : 0, "type" : "NOT_ACTIVE", - "subtype" : 62, + "subtype" : "blind", "duration" : [ "UNTIL_BEING_ATTACKED", "N_TURNS" diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 6274a047f..02f7c18af 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -222,16 +222,20 @@ Gives additional bonus to effect of specific spell TODO: blesses and curses with id = val dependent on unit's level -- subtype: 0 or 1 for Coronius +- subtype: affected spell identifier ### SPECIAL_ADD_VALUE_ENCHANT TODO: specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add +- subtype: affected spell identifier + ### SPECIAL_FIXED_VALUE_ENCHANT TODO: specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. +- subtype: affected spell identifier + ### SPECIAL_UPGRADE Allows creature upgrade for affected armies @@ -575,7 +579,7 @@ Defines spell mastery level for spell used by CATAPULT bonus Hero can control war machine affected by this bonus -- id: creature identifier of affected war machine +- subtype: creature identifier of affected war machine - val: chance to control unit, percentage ### CHANGES_SPELL_COST_FOR_ALLY @@ -795,7 +799,13 @@ Affected creature is immune to all mind spells and receives reduced damage from Affected unit is completely immune to effects of specific spell -- subid: identifier of spell to which unit is immune +- subtype: identifier of spell to which unit is immune + +### SPELL_SCHOOL_IMMUNITY + +Affected unit is immune to all spells of a specified spell school + +- subtype: spell school to which this unit is immune to ### RECEPTIVE @@ -825,6 +835,8 @@ Affected unit has its ranged attack power reduced (Forgetfulness) Affected unit can not act and is excluded from turn order (Blind, Stone Gaze, Paralyze) +- subtype: spell that caused this effect, optional + ### ALWAYS_MINIMUM_DAMAGE Affected creature always deals its minimum damage diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 977878123..8927d15b5 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -445,6 +445,7 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json case BonusType::SPELL_DAMAGE: case BonusType::SPELLS_OF_SCHOOL: case BonusType::SPELL_DAMAGE_REDUCTION: + case BonusType::SPELL_SCHOOL_IMMUNITY: { VLC->identifiers()->requestIdentifier( "spellSchool", node, [&subtype](int32_t identifier) { @@ -475,6 +476,7 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json case BonusType::SPECIAL_UPGRADE: case BonusType::HATE: case BonusType::SUMMON_GUARDIANS: + case BonusType::MANUAL_CONTROL: { VLC->identifiers()->requestIdentifier( "creature", node, [&subtype](int32_t identifier) { @@ -482,6 +484,10 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json }); break; } + case BonusType::SPELL_IMMUNITY: + case BonusType::SPECIAL_ADD_VALUE_ENCHANT: + case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: + case BonusType::SPECIAL_PECULIAR_ENCHANT: case BonusType::SPECIAL_SPELL_LEV: case BonusType::SPECIFIC_SPELL_DAMAGE: case BonusType::SPELL: @@ -497,6 +503,7 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json case BonusType::SPECIFIC_SPELL_POWER: case BonusType::ENCHANTED: case BonusType::MORE_DAMAGE_FROM_SPELL: + case BonusType::NOT_ACTIVE: { VLC->identifiers()->requestIdentifier( "spell", node, [&subtype](int32_t identifier) { @@ -515,7 +522,6 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json case BonusType::MOVEMENT: case BonusType::WATER_WALKING: case BonusType::FLYING_MOVEMENT: - case BonusType::SPECIAL_PECULIAR_ENCHANT: case BonusType::NEGATE_ALL_NATURAL_IMMUNITIES: case BonusType::CREATURE_DAMAGE: case BonusType::FLYING: @@ -548,21 +554,21 @@ static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource { if (node.isNull()) { - sourceInstance = TBonusSourceID(); + sourceInstance = BonusSourceID(); return; } if (node.isNumber()) // Compatibility code for 1.3 or older { logMod->warn("Bonus source must be string!"); - sourceInstance = TBonusSourceID(node.Integer()); + sourceInstance = BonusSourceID(node.Integer()); return; } if (!node.isString()) { logMod->warn("Bonus source must be string!"); - sourceInstance = TBonusSourceID(); + sourceInstance = BonusSourceID(); return; } @@ -1039,7 +1045,7 @@ bool JsonUtils::parseBonus(const JsonNode &ability, Bonus *b) else b->type = it->second; - loadBonusSubtype(b->subtype, b->type, params->isConverted ? params->toJson() : ability); + loadBonusSubtype(b->subtype, b->type, params->isConverted ? params->toJson()["subtype"] : ability["subtype"]); if(!params->isConverted) { diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index e7822d023..e03e7891b 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -353,6 +353,7 @@ const JsonNode & BonusParams::toJson() ret["targetSourceType"].String() = vstd::findKey(bonusSourceMap, *targetType); jsonCreated = true; } + ret.setMeta(ModScope::scopeGame()); return ret; }; diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index 615995ea4..ac4107630 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -42,6 +42,46 @@ CIdentifierStorage::CIdentifierStorage() registerObject(ModScope::scopeBuiltin(), "primSkill", NPrimarySkill::names[i], i); registerObject(ModScope::scopeBuiltin(), "primarySkill", NPrimarySkill::names[i], i); } + + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageBoth", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageMin", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureDamageMax", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeAll", -1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeMelee", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeRanged", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementLand", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementPenalty", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementFull", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthSpecial", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsMonsters", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsHeroes", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "visionsTowns", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "immunityBattleWide", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "immunityEnemyHero", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "transmutationPerHealth", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "transmutationPerUnit", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "destructionKillPercentage", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "destructionKillAmount", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "soulStealPermanent", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "soulStealBattle", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "movementFlying", 0); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "movementTeleporting", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel1", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel2", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel3", 3); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel4", 4); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "spellLevel5", 5); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel1", 1); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel2", 2); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel3", 3); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel4", 4); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel5", 5); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel6", 6); + registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "creatureLevel7", 7); } void CIdentifierStorage::checkIdentifier(std::string & ID) @@ -107,7 +147,7 @@ CIdentifierStorage::ObjectCallback CIdentifierStorage::ObjectCallback::fromNameA if(!typeAndName.first.empty()) { if (typeAndName.first != type) - logMod->error("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); + logMod->warn("Identifier '%s' from mod '%s' requested with different type! Type '%s' expected!", fullName, scope, type); else logMod->debug("Target type for identifier '%s' defined in mod '%s' is redundant!", fullName, scope); } From 31cf3442afb0527e26e6a913b4e5ec8ac1e5b468 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 15 Oct 2023 19:13:11 +0300 Subject: [PATCH 0911/1248] Update docs --- docs/modders/Bonus/Bonus_Types.md | 61 ++++++++++++++++++++++--------- lib/bonuses/BonusSubtypeID.cpp | 2 - lib/bonuses/BonusSubtypeID.h | 3 -- lib/modding/IdentifierStorage.cpp | 2 - scripting/lua/api/BonusSystem.cpp | 4 +- 5 files changed, 46 insertions(+), 26 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 02f7c18af..f3719613e 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -39,20 +39,22 @@ On each turn, hides area in fog of war around affected town for all players othe Increases amount of movement points available to affected hero on new turn -- subtype: 0 - sea, subtype 1 - land +- subtype: +- - heroMovementLand: only land movement will be affected +- - heroMovementSea: only sea movement will be affected - val: number of movement points (100 points for a tile) ### WATER_WALKING Allows movement over water for affected heroes -- subtype: 1 - without penalty, 2 - with penalty +- val: TODO ### FLYING_MOVEMENT Allows flying movement for affected heroes -- subtype: 1 - without penalty, 2 - with penalty +- val: TODO ### NO_TERRAIN_PENALTY @@ -298,7 +300,9 @@ Heroes affected by this bonus can not retreat or surrender in battle Negates all natural immunities for affected stacks. (Orb of Vulnerability) -- subtype: 0 - battle-wide immunity negation, 1 - negation only for enemy stacks +- subtype: +- - immunityBattleWide: Entire battle will be affected by bonus +- - immunityEnemyHero: Only enemy hero will be affected by bonus ### OPENING_BATTLE_SPELL @@ -333,7 +337,10 @@ Increases movement speed of units in battle Increases base damage of creature in battle -- subtype: 0 = both min and max, 1 = min, 2 = max +- subtype: +- - creatureDamageMin: increases only minimal damage +- - creatureDamageMax: increases only maximal damage +- - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -390,7 +397,9 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield -- subtype: 0 - flying unit, 1 - teleporting unit +- subtype: +- - movementFlying: creature will fly (slowly move across battlefield) +- - movementTeleporting: creature will instantly teleport to destination ### SHOOTER @@ -426,7 +435,7 @@ Affected unit will deal more damage based on movement distance (Champions) Affected unit will deal more damage when attacking specific creature -- subtype - identifier of hated creature, +- subtype - identifier of hated creature - val - additional damage, percentage ### SPELL_LIKE_ATTACK @@ -463,14 +472,19 @@ Affected unit will ignore specified percentage of attacked unit defence (Behemot Affected units will receive reduced damage from attacks by other units - val: damage reduction, percentage -- subtype: 0 - melee damage (Shield spell), 1 - ranged damage (Air Shield), -1 - all damage (Armorer skill) +- subtype: +- - damageTypeMelee: only melee damage will be reduced +- - damageTypeRanged: only ranged damage will be reduced +- - damageTypeAll: all damage will be reduced ### PERCENTAGE_DAMAGE_BOOST Affected units will deal increased damage when attacking other units - val: damage increase, percentage -- subtype: 0 - melee damage (Offense skill), 1 - ranged damage (Archery skill) +- subtype: +- - damageTypeMelee: only melee damage will increased +- - damageTypeRanged: only ranged damage will increased ### GENERAL_ATTACK_REDUCTION @@ -509,14 +523,18 @@ Affected unit will never receive retaliations when attacking Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed -- subtype: 0 - gained units disappear after battle, 1 - gained units are permanent +- subtype: +- - soulStealPermanent: creature will stay after the battle +- - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION Affected units have chance to transform attacked unit to other creature type - val: chance for ability to trigger, percentage -- subtype: 0 - transformed unit will have same HP pool as original stack, 1 - transformed unit will have same number of units as original stack +- subtype: +- - transmutationPerHealth: transformed unit will have same HP pool as original stack, +- - transmutationPerUnit: transformed unit will have same number of units as original stack - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS @@ -551,7 +569,9 @@ Affected unit will attack units on all hexes that surround attacked hex in range Affected unit will kills additional units after attack - val: chance to trigger, percentage -- subtype: 0 - kill percentage of units, 1 - kill amount +- subtype: +- - destructionKillPercentage: kill percentage of units, +- - destructionKillAmount: kill amount - addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE @@ -663,10 +683,12 @@ Affected unit will deal additional damage after attack Affected unit will kill additional units after attack -- subtype: 0 - random amount (Gorgons), 1 - fixed amount (Commanders) +- subtype: +- - deathStareGorgon: random amount +- - deathStareCommander: fixed amount - val: -- - for subtype 0: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula -- - for subtype 1: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val +- - for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula +- - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val ### SPECIAL_CRYSTAL_GENERATION @@ -752,7 +774,9 @@ Affected stack will resurrect after death TODO: recheck math - val - percent of total stack HP restored -- subtype = 0 - regular, 1 - at least one unit (sacred Phoenix) +- subtype: +- - rebirthRegular: (Phoenix) +- - rebirthSpecial: at least one unit will always resurrect (sacred Phoenix) ### ENCHANTED @@ -896,7 +920,10 @@ Affected heroes will be under effect of Disguise spell, hiding some of their inf Affected heroes will be under effect of Visions spell, revealing information of enemy objects in specific range - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range -- subtype: 0 - reveal information on monsters, 1 - reveal information on heroes, 2 - reveal information on towns +- subtype: +- - visionsMonsters: reveal information on monsters, +- - visionsHeroes: reveal information on heroes, +- - visionsTowns: reveal information on towns ### BLOCK_MAGIC_BELOW diff --git a/lib/bonuses/BonusSubtypeID.cpp b/lib/bonuses/BonusSubtypeID.cpp index b6a509597..9ca0f6bb9 100644 --- a/lib/bonuses/BonusSubtypeID.cpp +++ b/lib/bonuses/BonusSubtypeID.cpp @@ -21,8 +21,6 @@ const BonusSubtypeID BonusSubtypeID::damageTypeMelee(0); const BonusSubtypeID BonusSubtypeID::damageTypeRanged(1); const BonusSubtypeID BonusSubtypeID::heroMovementLand(1); const BonusSubtypeID BonusSubtypeID::heroMovementSea(0); -const BonusSubtypeID BonusSubtypeID::heroMovementPenalty(2); -const BonusSubtypeID BonusSubtypeID::heroMovementFull(1); const BonusSubtypeID BonusSubtypeID::deathStareGorgon(0); const BonusSubtypeID BonusSubtypeID::deathStareCommander(1); const BonusSubtypeID BonusSubtypeID::rebirthRegular(0); diff --git a/lib/bonuses/BonusSubtypeID.h b/lib/bonuses/BonusSubtypeID.h index 9db601983..c73572cc2 100644 --- a/lib/bonuses/BonusSubtypeID.h +++ b/lib/bonuses/BonusSubtypeID.h @@ -43,9 +43,6 @@ public: static const BonusSubtypeID heroMovementLand; // 1 static const BonusSubtypeID heroMovementSea; // 0 - static const BonusSubtypeID heroMovementPenalty; // 2 - static const BonusSubtypeID heroMovementFull; // 1 - static const BonusSubtypeID deathStareGorgon; // 0 static const BonusSubtypeID deathStareCommander; diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index ac4107630..cce9700c5 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -51,8 +51,6 @@ CIdentifierStorage::CIdentifierStorage() registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "damageTypeRanged", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementLand", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementSea", 0); - registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementPenalty", 2); - registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "heroMovementFull", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareGorgon", 0); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "deathStareCommander", 1); registerObject(ModScope::scopeBuiltin(), "bonusSubtype", "rebirthRegular", 0); diff --git a/scripting/lua/api/BonusSystem.cpp b/scripting/lua/api/BonusSystem.cpp index be0d981a0..2ce613ab0 100644 --- a/scripting/lua/api/BonusSystem.cpp +++ b/scripting/lua/api/BonusSystem.cpp @@ -62,7 +62,7 @@ int BonusProxy::getSubtype(lua_State * L) std::shared_ptr object; if(!S.tryGet(1, object)) return S.retNil(); - return LuaStack::quickRetInt(L, object->subtype); + return LuaStack::quickRetInt(L, object->subtype.getNum()); } int BonusProxy::getDuration(lua_State * L) @@ -116,7 +116,7 @@ int BonusProxy::getSourceID(lua_State * L) std::shared_ptr object; if(!S.tryGet(1, object)) return S.retNil(); - return LuaStack::quickRetInt(L, object->sid); + return LuaStack::quickRetInt(L, object->sid.getNum()); } int BonusProxy::getEffectRange(lua_State * L) From ac925bb7860c4c01a74f919a147a01a3ff0fa299 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 14:50:42 +0300 Subject: [PATCH 0912/1248] Renamed new types for consistency with code style --- AI/Nullkiller/Analyzers/HeroManager.cpp | 2 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 10 +- client/battle/BattleStacksController.cpp | 4 +- client/windows/CCreatureWindow.cpp | 2 +- client/windows/CKingdomInterface.cpp | 2 +- cmake_modules/VCMI_lib.cmake | 4 +- lib/ArtifactUtils.cpp | 2 +- lib/BasicTypes.cpp | 12 +-- lib/BattleFieldHandler.cpp | 2 +- lib/CArtHandler.cpp | 2 +- lib/CBonusTypeHandler.cpp | 4 +- lib/CCreatureHandler.cpp | 98 +++++++++---------- lib/CCreatureHandler.h | 2 +- lib/CGameInfoCallback.cpp | 4 +- lib/CHeroHandler.cpp | 6 +- lib/CSkillHandler.cpp | 2 +- lib/CStack.cpp | 2 +- lib/CTownHandler.cpp | 20 ++-- lib/CTownHandler.h | 6 +- lib/JsonNode.cpp | 26 ++--- lib/NetPacks.h | 2 +- lib/battle/BattleInfo.cpp | 6 +- lib/battle/CBattleInfoCallback.cpp | 2 +- lib/battle/CUnitState.cpp | 16 +-- lib/battle/DamageCalculator.cpp | 22 ++--- lib/bonuses/Bonus.cpp | 18 ++-- lib/bonuses/Bonus.h | 20 ++-- lib/bonuses/BonusCustomTypes.cpp | 74 ++++++++++++++ lib/bonuses/BonusCustomTypes.h | 75 ++++++++++++++ lib/bonuses/BonusParams.cpp | 56 +++++------ lib/bonuses/BonusParams.h | 2 +- lib/bonuses/BonusSelector.cpp | 14 +-- lib/bonuses/BonusSelector.h | 8 +- lib/bonuses/BonusSubtypeID.cpp | 74 -------------- lib/bonuses/BonusSubtypeID.h | 75 -------------- lib/bonuses/IBonusBearer.cpp | 6 +- lib/bonuses/IBonusBearer.h | 6 +- lib/bonuses/Limiters.cpp | 4 +- lib/bonuses/Limiters.h | 8 +- lib/campaign/CampaignConstants.h | 2 - lib/gameState/CGameState.cpp | 4 +- lib/gameState/CGameStateCampaign.cpp | 4 +- .../CRewardableConstructor.cpp | 2 +- lib/mapObjects/CArmedInstance.cpp | 6 +- lib/mapObjects/CBank.cpp | 4 +- lib/mapObjects/CGCreature.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 44 ++++----- lib/mapObjects/CGHeroInstance.h | 2 +- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CGTownBuilding.cpp | 10 +- lib/mapObjects/CGTownInstance.cpp | 4 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/CRewardableObject.cpp | 4 +- lib/mapObjects/MiscObjects.cpp | 10 +- lib/mapping/MapFormatH3M.cpp | 8 +- lib/pathfinder/TurnInfo.cpp | 10 +- lib/pathfinder/TurnInfo.h | 4 +- lib/spells/AbilityCaster.cpp | 2 +- lib/spells/AdventureSpellMechanics.cpp | 4 +- lib/spells/CSpellHandler.cpp | 14 +-- lib/spells/TargetCondition.cpp | 14 +-- lib/spells/effects/Clone.cpp | 2 +- lib/spells/effects/Damage.cpp | 4 +- lib/spells/effects/Moat.cpp | 4 +- lib/spells/effects/Timed.cpp | 8 +- lib/spells/effects/UnitEffect.cpp | 2 +- mapeditor/inspector/rewardswidget.cpp | 2 +- server/CGameHandler.cpp | 6 +- server/battles/BattleActionProcessor.cpp | 56 +++++------ server/battles/BattleFlowProcessor.cpp | 8 +- server/processors/PlayerMessageProcessor.cpp | 4 +- test/battle/CBattleInfoCallbackTest.cpp | 20 ++-- test/battle/CHealthTest.cpp | 4 +- test/battle/CUnitStateMagicTest.cpp | 12 +-- test/battle/CUnitStateTest.cpp | 30 +++--- test/entity/CCreatureTest.cpp | 6 +- test/spells/AbilityCasterTest.cpp | 4 +- test/spells/effects/CloneTest.cpp | 2 +- test/spells/effects/DamageTest.cpp | 6 +- test/spells/effects/DispelTest.cpp | 14 +-- test/spells/effects/HealTest.cpp | 10 +- test/spells/effects/SacrificeTest.cpp | 4 +- test/spells/effects/SummonTest.cpp | 4 +- test/spells/effects/TimedTest.cpp | 6 +- .../AbsoluteLevelConditionTest.cpp | 8 +- .../AbsoluteSpellConditionTest.cpp | 4 +- .../targetConditions/BonusConditionTest.cpp | 4 +- .../ElementalConditionTest.cpp | 10 +- .../ImmunityNegationConditionTest.cpp | 4 +- .../NormalLevelConditionTest.cpp | 6 +- .../NormalSpellConditionTest.cpp | 4 +- .../ReceptiveFeatureConditionTest.cpp | 2 +- .../SpellEffectConditionTest.cpp | 6 +- 94 files changed, 564 insertions(+), 566 deletions(-) create mode 100644 lib/bonuses/BonusCustomTypes.cpp create mode 100644 lib/bonuses/BonusCustomTypes.h delete mode 100644 lib/bonuses/BonusSubtypeID.cpp delete mode 100644 lib/bonuses/BonusSubtypeID.h diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index 037a50b2d..bd43d4df3 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -71,7 +71,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const { - auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, TBonusSourceID(hero->type->getId())); + auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId())); auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL); auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus)); auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL)); diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 654470198..9b3ef27c0 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -242,13 +242,13 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art) return 1500; auto statsValue = - 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusSubtypeID::heroMovementLand) + 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 700 * art->valOfBonuses(BonusType::MORALE) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::KNOWLEDGE)) - + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::SPELL_POWER)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::KNOWLEDGE)) + + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::SPELL_POWER)) + 500 * art->valOfBonuses(BonusType::LUCK); auto classValue = 0; diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 2ca550d4e..a2512ba6d 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -534,7 +534,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorhasBonus(Selector::typeSubtype(BonusType::FLYING, BonusSubtypeID::movementFlying))) + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementFlying))) { owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() { @@ -797,7 +797,7 @@ void BattleStacksController::removeExpiredColorFilters() { if (!filter.persistent) { - if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(filter.source->id)), Selector::all)) + if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all)) return true; if (filter.effect == ColorFilter::genEmptyShifter()) return true; diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index cc5c7a253..44f1da1a6 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -224,7 +224,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int spellText = CGI->generaltexth->allTexts[610]; //"%s, duration: %d rounds." boost::replace_first(spellText, "%s", spell->getNameTranslated()); //FIXME: support permanent duration - int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(effect)))->turnsRemain; + int duration = battleStack->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)))->turnsRemain; boost::replace_first(spellText, "%d", std::to_string(duration)); spellIcons.push_back(std::make_shared(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed)); diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index ba0082556..85d157bbe 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -579,7 +579,7 @@ void CKingdomInterface::generateMinesList(const std::vector heroes = LOCPLINT->cb->getHeroesInfo(true); for(auto & heroe : heroes) { - totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, TBonusSubtype(GameResID(EGameResID::GOLD)))); + totalIncome += heroe->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(EGameResID::GOLD)))); } //Add town income of all towns diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 0cf3914ab..7846aa0f4 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -31,7 +31,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.cpp ${MAIN_LIB_DIR}/bonuses/BonusParams.cpp ${MAIN_LIB_DIR}/bonuses/BonusSelector.cpp - ${MAIN_LIB_DIR}/bonuses/BonusSubtypeID.cpp + ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.cpp ${MAIN_LIB_DIR}/bonuses/CBonusProxy.cpp ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.cpp ${MAIN_LIB_DIR}/bonuses/IBonusBearer.cpp @@ -358,7 +358,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/bonuses/BonusList.h ${MAIN_LIB_DIR}/bonuses/BonusParams.h ${MAIN_LIB_DIR}/bonuses/BonusSelector.h - ${MAIN_LIB_DIR}/bonuses/BonusSubtypeID.h + ${MAIN_LIB_DIR}/bonuses/BonusCustomTypes.h ${MAIN_LIB_DIR}/bonuses/CBonusProxy.h ${MAIN_LIB_DIR}/bonuses/CBonusSystemNode.h ${MAIN_LIB_DIR}/bonuses/IBonusBearer.h diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 9fd4749f0..92da87763 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -145,7 +145,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid) { auto ret = new CArtifactInstance(VLC->arth->objects[ArtifactID::SPELL_SCROLL]); auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::SPELL, - BonusSource::ARTIFACT_INSTANCE, -1, TBonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), TBonusSubtype(sid)); + BonusSource::ARTIFACT_INSTANCE, -1, BonusSourceID(ArtifactID(ArtifactID::SPELL_SCROLL)), BonusSubtypeID(sid)); ret->addNewBonus(bonus); return ret; } diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index 3f476ec32..af58094f0 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -35,7 +35,7 @@ TerrainId AFactionMember::getNativeTerrain() const { constexpr auto any = TerrainId(ETerrainId::ANY_TERRAIN); const std::string cachingStringNoTerrainPenalty = "type_NO_TERRAIN_PENALTY_sANY"; - static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, TBonusSubtype(any)); + static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(any)); //this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses //and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties. @@ -54,7 +54,7 @@ int AFactionMember::getAttack(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_ATTACK"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -63,7 +63,7 @@ int AFactionMember::getDefense(bool ranged) const { const std::string cachingStr = "type_PRIMARY_SKILLs_DEFENSE"; - static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); + static const auto selector = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -71,14 +71,14 @@ int AFactionMember::getDefense(bool ranged) const int AFactionMember::getMinDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_1"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } int AFactionMember::getMaxDamage(bool ranged) const { const std::string cachingStr = "type_CREATURE_DAMAGEs_0Otype_CREATURE_DAMAGEs_2"; - static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax)); + static const auto selector = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)); return getBonusBearer()->valOfBonuses(selector, cachingStr); } @@ -87,7 +87,7 @@ int AFactionMember::getPrimSkillLevel(PrimarySkill id) const static const CSelector selectorAllSkills = Selector::type()(BonusType::PRIMARY_SKILL); static const std::string keyAllSkills = "type_PRIMARY_SKILL"; auto allSkills = getBonusBearer()->getBonuses(selectorAllSkills, keyAllSkills); - auto ret = allSkills->valOfBonuses(Selector::subtype()(TBonusSubtype(id))); + auto ret = allSkills->valOfBonuses(Selector::subtype()(BonusSubtypeID(id))); auto minSkillValue = (id == PrimarySkill::SPELL_POWER || id == PrimarySkill::KNOWLEDGE) ? 1 : 0; return std::max(ret, minSkillValue); //otherwise, some artifacts may cause negative skill value effect, sp=0 works in old saves } diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index a9393af87..b5bcedb90 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -29,7 +29,7 @@ BattleFieldInfo * BattleFieldHandler::loadFromJson(const std::string & scope, co auto bonus = JsonUtils::parseBonus(b); bonus->source = BonusSource::TERRAIN_OVERLAY; - bonus->sid = TBonusSourceID(info->getId()); + bonus->sid = BonusSourceID(info->getId()); bonus->duration = BonusDuration::ONE_BATTLE; info->bonuses.push_back(bonus); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index a1f11cfb6..236dd32e9 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -751,7 +751,7 @@ void CArtHandler::afterLoadFinalization() { assert(art == objects[art->id]); assert(bonus->source == BonusSource::ARTIFACT); - bonus->sid = TBonusSourceID(art->id); + bonus->sid = BonusSourceID(art->id); } } CBonusSystemNode::treeHasChanged(); diff --git a/lib/CBonusTypeHandler.cpp b/lib/CBonusTypeHandler.cpp index 70636a9b3..eb5870509 100644 --- a/lib/CBonusTypeHandler.cpp +++ b/lib/CBonusTypeHandler.cpp @@ -168,10 +168,10 @@ ImagePath CBonusTypeHandler::bonusToGraphics(const std::shared_ptr & bonu } case BonusType::GENERAL_DAMAGE_REDUCTION: { - if (bonus->subtype == BonusSubtypeID::damageTypeMelee) + if (bonus->subtype == BonusCustomSubtype::damageTypeMelee) fileName = "DamageReductionMelee.bmp"; - if (bonus->subtype == BonusSubtypeID::damageTypeRanged) + if (bonus->subtype == BonusCustomSubtype::damageTypeRanged) fileName = "DamageReductionRanged.bmp"; break; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 413398f7b..eeeebf4e4 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -113,25 +113,25 @@ FactionID CCreature::getFaction() const int32_t CCreature::getBaseAttack() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDefense() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMin() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } int32_t CCreature::getBaseDamageMax() const { - static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); + static const auto SELECTOR = Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax).And(Selector::sourceTypeSel(BonusSource::CREATURE_ABILITY)); return getExportedBonusList().valOfBonuses(SELECTOR); } @@ -293,12 +293,12 @@ CCreature::CCreature() void CCreature::addBonus(int val, BonusType type) { - addBonus(val, type, TBonusSubtype()); + addBonus(val, type, BonusSubtypeID()); } -void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) +void CCreature::addBonus(int val, BonusType type, BonusSubtypeID subtype) { - auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, TBonusSourceID(getId()))); + auto selector = Selector::typeSubtype(type, subtype).And(Selector::source(BonusSource::CREATURE_ABILITY, BonusSourceID(getId()))); BonusList & exported = getExportedBonusList(); BonusList existing; @@ -306,7 +306,7 @@ void CCreature::addBonus(int val, BonusType type, TBonusSubtype subtype) if(existing.empty()) { - auto added = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, TBonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER); + auto added = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::CREATURE_ABILITY, val, BonusSourceID(getId()), subtype, BonusValueType::BASE_NUMBER); addNewBonus(added); } else @@ -350,16 +350,16 @@ void CCreature::updateFrom(const JsonNode & data) addBonus(configNode["speed"].Integer(), BonusType::STACKS_SPEED); if(!configNode["attack"].isNull()) - addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); + addBonus(configNode["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); if(!configNode["defense"].isNull()) - addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); + addBonus(configNode["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); if(!configNode["damage"]["min"].isNull()) - addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin); + addBonus(configNode["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); if(!configNode["damage"]["max"].isNull()) - addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax); + addBonus(configNode["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); if(!configNode["shots"].isNull()) addBonus(configNode["shots"].Integer(), BonusType::SHOTS); @@ -609,11 +609,11 @@ CCreature * CCreatureHandler::loadFromJson(const std::string & scope, const Json cre->addBonus(node["hitPoints"].Integer(), BonusType::STACK_HEALTH); cre->addBonus(node["speed"].Integer(), BonusType::STACKS_SPEED); - cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)); - cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)); + cre->addBonus(node["attack"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)); + cre->addBonus(node["defense"].Integer(), BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)); - cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin); - cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax); + cre->addBonus(node["damage"]["min"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin); + cre->addBonus(node["damage"]["max"].Integer(), BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax); assert(node["damage"]["min"].Integer() <= node["damage"]["max"].Integer()); @@ -792,7 +792,7 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) { CreatureID sid = static_cast(parser.readNumber()); //id = this particular creature ID - b.sid = TBonusSourceID(sid); + b.sid = BonusSourceID(sid); bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) @@ -898,7 +898,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c { auto b = JsonUtils::parseBonus(ability.second); b->source = BonusSource::CREATURE_ABILITY; - b->sid = TBonusSourceID(creature->getId()); + b->sid = BonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } @@ -916,7 +916,7 @@ void CCreatureHandler::loadCreatureJson(CCreature * creature, const JsonNode & c { auto b = JsonUtils::parseBonus(ability); b->source = BonusSource::CREATURE_ABILITY; - b->sid = TBonusSourceID(creature->getId()); + b->sid = BonusSourceID(creature->getId()); b->duration = BonusDuration::PERMANENT; creature->addNewBonus(b); } @@ -1030,19 +1030,19 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'A': b.type = BonusType::PRIMARY_SKILL; - b.subtype = TBonusSubtype(PrimarySkill::ATTACK); + b.subtype = BonusSubtypeID(PrimarySkill::ATTACK); break; case 'D': b.type = BonusType::PRIMARY_SKILL; - b.subtype = TBonusSubtype(PrimarySkill::DEFENSE); + b.subtype = BonusSubtypeID(PrimarySkill::DEFENSE); break; case 'M': //Max damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = BonusSubtypeID::creatureDamageMax; + b.subtype = BonusCustomSubtype::creatureDamageMax; break; case 'm': //Min damage b.type = BonusType::CREATURE_DAMAGE; - b.subtype = BonusSubtypeID::creatureDamageMin; + b.subtype = BonusCustomSubtype::creatureDamageMin; break; case 'S': b.type = BonusType::STACKS_SPEED; break; @@ -1059,13 +1059,13 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'E': b.type = BonusType::DEATH_STARE; - b.subtype = BonusSubtypeID::deathStareGorgon; + b.subtype = BonusCustomSubtype::deathStareGorgon; break; case 'F': b.type = BonusType::FEAR; break; case 'g': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::ANY); + b.subtype = BonusSubtypeID(SpellSchool::ANY); break; case 'P': b.type = BonusType::CASTS; break; @@ -1106,7 +1106,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars b.type = BonusType::MIND_IMMUNITY; break; case 'r': b.type = BonusType::REBIRTH; //on/off? makes sense? - b.subtype = BonusSubtypeID::rebirthRegular; + b.subtype = BonusCustomSubtype::rebirthRegular; b.val = 20; //arbitrary value break; case 'R': @@ -1129,42 +1129,42 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars { case 'B': //Blind b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::BLIND)); + b.subtype = BonusSubtypeID(SpellID(SpellID::BLIND)); b.additionalInfo = 0;//normal immunity break; case 'H': //Hypnotize b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::HYPNOTIZE)); + b.subtype = BonusSubtypeID(SpellID(SpellID::HYPNOTIZE)); b.additionalInfo = 0;//normal immunity break; case 'I': //Implosion b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::IMPLOSION)); + b.subtype = BonusSubtypeID(SpellID(SpellID::IMPLOSION)); b.additionalInfo = 0;//normal immunity break; case 'K': //Berserk b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::BERSERK)); + b.subtype = BonusSubtypeID(SpellID(SpellID::BERSERK)); b.additionalInfo = 0;//normal immunity break; case 'M': //Meteor Shower b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::METEOR_SHOWER)); + b.subtype = BonusSubtypeID(SpellID(SpellID::METEOR_SHOWER)); b.additionalInfo = 0;//normal immunity break; case 'N': //dispell beneficial spells b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); + b.subtype = BonusSubtypeID(SpellID(SpellID::DISPEL_HELPFUL_SPELLS)); b.additionalInfo = 0;//normal immunity break; case 'R': //Armageddon b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::ARMAGEDDON)); + b.subtype = BonusSubtypeID(SpellID(SpellID::ARMAGEDDON)); b.additionalInfo = 0;//normal immunity break; case 'S': //Slow b.type = BonusType::SPELL_IMMUNITY; - b.subtype = TBonusSubtype(SpellID(SpellID::SLOW)); + b.subtype = BonusSubtypeID(SpellID(SpellID::SLOW)); b.additionalInfo = 0;//normal immunity break; case '6': @@ -1180,51 +1180,51 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 'F': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::FIRE); + b.subtype = BonusSubtypeID(SpellSchool::FIRE); break; case 'O': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::FIRE); + b.subtype = BonusSubtypeID(SpellSchool::FIRE); b.val = 100; //Full damage immunity break; case 'f': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::FIRE); + b.subtype = BonusSubtypeID(SpellSchool::FIRE); break; case 'C': b.type = BonusType::NEGATIVE_EFFECTS_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::WATER); + b.subtype = BonusSubtypeID(SpellSchool::WATER); break; case 'W': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::WATER); + b.subtype = BonusSubtypeID(SpellSchool::WATER); b.val = 100; //Full damage immunity break; case 'w': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::WATER); + b.subtype = BonusSubtypeID(SpellSchool::WATER); break; case 'E': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::EARTH); + b.subtype = BonusSubtypeID(SpellSchool::EARTH); b.val = 100; //Full damage immunity break; case 'e': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::EARTH); + b.subtype = BonusSubtypeID(SpellSchool::EARTH); break; case 'A': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::AIR); + b.subtype = BonusSubtypeID(SpellSchool::AIR); b.val = 100; //Full damage immunity break; case 'a': b.type = BonusType::SPELL_SCHOOL_IMMUNITY; - b.subtype = TBonusSubtype(SpellSchool::AIR); + b.subtype = BonusSubtypeID(SpellSchool::AIR); break; case 'D': b.type = BonusType::SPELL_DAMAGE_REDUCTION; - b.subtype = TBonusSubtype(SpellSchool::ANY); + b.subtype = BonusSubtypeID(SpellSchool::ANY); b.val = 100; //Full damage immunity break; case '0': @@ -1253,16 +1253,16 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars case 'K': case 'k': b.type = BonusType::SPELL_AFTER_ATTACK; - b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); break; case 'h': b.type = BonusType::HATE; - b.subtype = TBonusSubtype(CreatureID(stringToNumber(mod))); + b.subtype = BonusSubtypeID(CreatureID(stringToNumber(mod))); break; case 'p': case 'J': b.type = BonusType::SPELL_BEFORE_ATTACK; - b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); b.additionalInfo = 3; //always expert? break; case 'r': @@ -1271,7 +1271,7 @@ void CCreatureHandler::loadStackExp(Bonus & b, BonusList & bl, CLegacyConfigPars break; case 's': b.type = BonusType::ENCHANTED; - b.subtype = TBonusSubtype(SpellID(stringToNumber(mod))); + b.subtype = BonusSubtypeID(SpellID(stringToNumber(mod))); b.valType = BonusValueType::INDEPENDENT_MAX; break; default: diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index b1d6b964c..c77e7864e 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -195,7 +195,7 @@ public: bool valid() const; void addBonus(int val, BonusType type); - void addBonus(int val, BonusType type, TBonusSubtype subtype); + void addBonus(int val, BonusType type, BonusSubtypeID subtype); std::string nodeName() const override; template diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index cfbccd99d..674b20023 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -268,7 +268,7 @@ bool CGameInfoCallback::getTownInfo(const CGObjectInstance * town, InfoAboutTown { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - detailed = selectedHero->hasVisions(town, BonusSubtypeID::visionsTowns); + detailed = selectedHero->hasVisions(town, BonusCustomSubtype::visionsTowns); } dest.initFromTown(dynamic_cast(town), detailed); @@ -322,7 +322,7 @@ bool CGameInfoCallback::getHeroInfo(const CGObjectInstance * hero, InfoAboutHero { const auto * selectedHero = dynamic_cast(selectedObject); if(nullptr != selectedHero) - if(selectedHero->hasVisions(hero, BonusSubtypeID::visionsHeroes)) + if(selectedHero->hasVisions(hero, BonusCustomSubtype::visionsHeroes)) infoLevel = InfoAboutHero::EInfoLevel::DETAILED; } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 4fd22e1bd..dfdf726c8 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -547,7 +547,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = TBonusSubtype(PrimarySkill::ATTACK); + bonus->subtype = BonusSubtypeID(PrimarySkill::ATTACK); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getAttack(false), stepSize)); @@ -557,7 +557,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba { std::shared_ptr bonus = std::make_shared(); bonus->type = BonusType::PRIMARY_SKILL; - bonus->subtype = TBonusSubtype(PrimarySkill::DEFENSE); + bonus->subtype = BonusSubtypeID(PrimarySkill::DEFENSE); bonus->val = 0; bonus->limiter.reset(new CCreatureTypeLimiter(specCreature, false)); bonus->updater.reset(new GrowsWithLevelUpdater(specCreature.getDefense(false), stepSize)); @@ -605,7 +605,7 @@ void CHeroHandler::loadHeroSpecialty(CHero * hero, const JsonNode & node) { bonus->duration = BonusDuration::PERMANENT; bonus->source = BonusSource::HERO_SPECIAL; - bonus->sid = TBonusSourceID(hero->getId()); + bonus->sid = BonusSourceID(hero->getId()); return bonus; }; diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 68ee05294..82338746c 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -93,7 +93,7 @@ SecondarySkill CSkill::getId() const void CSkill::addNewBonus(const std::shared_ptr & b, int level) { b->source = BonusSource::SECONDARY_SKILL; - b->sid = TBonusSourceID(id); + b->sid = BonusSourceID(id); b->duration = BonusDuration::PERMANENT; b->description = getNameTranslated(); levels[level-1].effects.push_back(b); diff --git a/lib/CStack.cpp b/lib/CStack.cpp index a1c1371b1..c5e7b2293 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -220,7 +220,7 @@ void CStack::prepareAttacked(BattleStackAttacked & bsa, vstd::RNG & rand, const resurrectedCount += 1; } - if(customState->hasBonusOfType(BonusType::REBIRTH, BonusSubtypeID::rebirthSpecial)) + if(customState->hasBonusOfType(BonusType::REBIRTH, BonusCustomSubtype::rebirthSpecial)) { // resurrect at least one Sacred Phoenix vstd::amax(resurrectedCount, 1); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 6f03059ce..8eca0918e 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -551,16 +551,16 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const b = createBonus(building, BonusType::LUCK, +2); break; case BuildingSubID::SPELL_POWER_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::SPELL_POWER)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::SPELL_POWER)); break; case BuildingSubID::ATTACK_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::ATTACK)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::ATTACK)); break; case BuildingSubID::DEFENSE_GARRISON_BONUS: - b = createBonus(building, BonusType::PRIMARY_SKILL, +2, TBonusSubtype(PrimarySkill::DEFENSE)); + b = createBonus(building, BonusType::PRIMARY_SKILL, +2, BonusSubtypeID(PrimarySkill::DEFENSE)); break; case BuildingSubID::LIGHTHOUSE: - b = createBonus(building, BonusType::MOVEMENT, +500, BonusSubtypeID::heroMovementSea, playerPropagator); + b = createBonus(building, BonusType::MOVEMENT, +500, BonusCustomSubtype::heroMovementSea, playerPropagator); break; } @@ -570,15 +570,15 @@ void CTownHandler::addBonusesForVanilaBuilding(CBuilding * building) const std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val) const { - return createBonus(build, type, val, TBonusSubtype(), emptyPropagator()); + return createBonus(build, type, val, BonusSubtypeID(), emptyPropagator()); } -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype) const { return createBonus(build, type, val, subtype, emptyPropagator()); } -std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype, TPropagatorPtr & prop) const +std::shared_ptr CTownHandler::createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, TPropagatorPtr & prop) const { std::ostringstream descr; descr << build->getNameTranslated(); @@ -591,7 +591,7 @@ std::shared_ptr CTownHandler::createBonusImpl(const BuildingID & building int val, TPropagatorPtr & prop, const std::string & description, - TBonusSubtype subtype) const + BonusSubtypeID subtype) const { auto b = std::make_shared(BonusDuration::PERMANENT, type, BonusSource::TOWN_STRUCTURE, val, BuildingTypeUniqueID(faction, building), subtype, description); @@ -610,7 +610,7 @@ void CTownHandler::loadSpecialBuildingBonuses(const JsonNode & source, BonusList if(bonus == nullptr) continue; - bonus->sid = TBonusSourceID(building->getUniqueTypeID()); + bonus->sid = BonusSourceID(building->getUniqueTypeID()); //JsonUtils::parseBuildingBonus produces UNKNOWN type propagator instead of empty. if(bonus->propagator != nullptr && bonus->propagator->getPropagatorType() == CBonusSystemNode::ENodeTypes::UNKNOWN) @@ -674,7 +674,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->subId = BuildingSubID::CUSTOM_VISITING_BONUS; for(auto & bonus : ret->onVisitBonuses) - bonus->sid = TBonusSourceID(ret->getUniqueTypeID()); + bonus->sid = BonusSourceID(ret->getUniqueTypeID()); } if(source["type"].String() == "configurable" && ret->subId == BuildingSubID::NONE) diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 4fa1fb1f0..a8fbeccd7 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -392,15 +392,15 @@ class DLL_LINKAGE CTownHandler : public CHandlerBase createBonus(CBuilding * build, BonusType type, int val) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype) const; - std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, TBonusSubtype subtype, TPropagatorPtr & prop) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype) const; + std::shared_ptr createBonus(CBuilding * build, BonusType type, int val, BonusSubtypeID subtype, TPropagatorPtr & prop) const; std::shared_ptr createBonusImpl(const BuildingID & building, const FactionID & faction, BonusType type, int val, TPropagatorPtr & prop, const std::string & description, - TBonusSubtype subtype) const; + BonusSubtypeID subtype) const; /// loads CStructure's into town void loadStructure(CTown & town, const std::string & stringID, const JsonNode & source) const; diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 8927d15b5..c312cdba7 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -417,25 +417,25 @@ std::string JsonNode::toJson(bool compact) const ///JsonUtils -static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const JsonNode & node) +static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const JsonNode & node) { if (node.isNull()) { - subtype = TBonusSubtype(); + subtype = BonusSubtypeID(); return; } if (node.isNumber()) // Compatibility code for 1.3 or older { logMod->warn("Bonus subtype must be string!"); - subtype = BonusSubtypeID(node.Integer()); + subtype = BonusCustomSubtype(node.Integer()); return; } if (!node.isString()) { logMod->warn("Bonus subtype must be string!"); - subtype = TBonusSubtype(); + subtype = BonusSubtypeID(); return; } @@ -538,7 +538,7 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json { VLC->identifiers()->requestIdentifier( "bonusSubtype", node, [&subtype](int32_t identifier) { - subtype = BonusSubtypeID(identifier); + subtype = BonusCustomSubtype(identifier); }); break; } @@ -546,29 +546,29 @@ static void loadBonusSubtype(TBonusSubtype & subtype, BonusType type, const Json for(const auto & i : bonusNameMap) if(i.second == type) logMod->warn("Bonus type %s does not supports subtypes!", i.first ); - subtype = TBonusSubtype(); + subtype = BonusSubtypeID(); } } -static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) +static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource sourceType, const JsonNode & node) { if (node.isNull()) { - sourceInstance = BonusSourceID(); + sourceInstance = BonusCustomSource(); return; } if (node.isNumber()) // Compatibility code for 1.3 or older { logMod->warn("Bonus source must be string!"); - sourceInstance = BonusSourceID(node.Integer()); + sourceInstance = BonusCustomSource(node.Integer()); return; } if (!node.isString()) { logMod->warn("Bonus source must be string!"); - sourceInstance = BonusSourceID(); + sourceInstance = BonusCustomSource(); return; } @@ -643,7 +643,7 @@ static void loadBonusSourceInstance(TBonusSourceID & sourceInstance, BonusSource case BonusSource::TERRAIN_NATIVE: case BonusSource::OTHER: default: - sourceInstance = TBonusSourceID(); + sourceInstance = BonusSourceID(); break; } } @@ -1181,13 +1181,13 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) value = &ability["subtype"]; if(!value->isNull() && type != BonusType::NONE) { - TBonusSubtype subtype; + BonusSubtypeID subtype; loadBonusSubtype(subtype, type, ability); ret = ret.And(Selector::subtype()(subtype)); } value = &ability["sourceType"]; std::optional src = std::nullopt; //Fixes for GCC false maybe-uninitialized - std::optional id = std::nullopt; + std::optional id = std::nullopt; if(value->isString()) { auto it = bonusSourceMap.find(value->String()); diff --git a/lib/NetPacks.h b/lib/NetPacks.h index fb90861af..a5b20c901 100644 --- a/lib/NetPacks.h +++ b/lib/NetPacks.h @@ -497,7 +497,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient //vars to identify bonus: its source BonusSource source; - TBonusSourceID id; //source id + BonusSourceID id; //source id //used locally: copy of removed bonus Bonus bonus; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index bc4e02c1c..a4a30139c 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -442,9 +442,9 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const //native terrain bonuses static auto nativeTerrain = std::make_shared(); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID())->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); - curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID())->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain)); + curB->addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain)); ////////////////////////////////////////////////////////////////////////// //tactics diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 9cddd17b6..391f5248e 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1658,7 +1658,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; - if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(spellID)), Selector::all, cachingStr.str()) + if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str()) //TODO: this ability has special limitations || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) continue; diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 214287d9f..099ae50be 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -340,12 +340,12 @@ CUnitState::CUnitState(): health(this), shots(this), totalAttacks(this, Selector::type()(BonusType::ADDITIONAL_ATTACK), 1), - minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMin)), 0), - maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusSubtypeID::creatureDamageMax)), 0), - attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::ATTACK)), 0), - defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE)), 0), + minDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMin)), 0), + maxDamage(this, Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageBoth).Or(Selector::typeSubtype(BonusType::CREATURE_DAMAGE, BonusCustomSubtype::creatureDamageMax)), 0), + attack(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)), 0), + defence(this, Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE)), 0), inFrenzy(this, Selector::type()(BonusType::IN_FRENZY)), - cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::CLONE))))), + cloneLifetimeMarker(this, Selector::type()(BonusType::NONE).And(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::CLONE))))), cloneID(-1) { @@ -430,7 +430,7 @@ const CGHeroInstance * CUnitState::getHeroCaster() const int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const { - int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, TBonusSubtype(spell->getId()))); + int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spell->getId()))); vstd::abetween(skill, 0, 3); return skill; } @@ -466,7 +466,7 @@ int32_t CUnitState::getEnchantPower(const spells::Spell * spell) const int64_t CUnitState::getEffectValue(const spells::Spell * spell) const { - return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, TBonusSubtype(spell->getId())); + return static_cast(getCount()) * valOfBonuses(BonusType::SPECIFIC_SPELL_POWER, BonusSubtypeID(spell->getId())); } PlayerColor CUnitState::getCasterOwner() const @@ -511,7 +511,7 @@ bool CUnitState::isGhost() const bool CUnitState::isFrozen() const { - return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::STONE_GAZE))), Selector::all); + return hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::STONE_GAZE))), Selector::all); } bool CUnitState::isValidTarget(bool allowDead) const diff --git a/lib/battle/DamageCalculator.cpp b/lib/battle/DamageCalculator.cpp index 77342135a..dfed47953 100644 --- a/lib/battle/DamageCalculator.cpp +++ b/lib/battle/DamageCalculator.cpp @@ -52,7 +52,7 @@ DamageRange DamageCalculator::getBaseDamageSingle() const { auto retrieveHeroPrimSkill = [&](PrimarySkill skill) -> int { - std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(skill)))); + std::shared_ptr b = info.attacker->getBonus(Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL).And(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(skill)))); return b ? b->val : 0; }; @@ -144,7 +144,7 @@ int DamageCalculator::getActorAttackSlayer() const { SpellID spell(SpellID::SLAYER); int attackBonus = spell.toSpell()->getLevelPower(spLevel); - if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, TBonusSubtype(spell))) + if(info.attacker->hasBonusOfType(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(spell))) { ui8 attackerTier = info.attacker->unitType()->getLevel(); ui8 specialtyBonus = std::max(5 - attackerTier, 0); @@ -206,11 +206,11 @@ double DamageCalculator::getAttackOffenseArcheryFactor() const if(info.shooting) { const std::string cachingStrArchery = "type_PERCENTAGE_DAMAGE_BOOSTs_1"; - static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypeID::damageTypeRanged); + static const auto selectorArchery = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusCustomSubtype::damageTypeRanged); return info.attacker->valOfBonuses(selectorArchery, cachingStrArchery) / 100.0; } const std::string cachingStrOffence = "type_PERCENTAGE_DAMAGE_BOOSTs_0"; - static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusSubtypeID::damageTypeMelee); + static const auto selectorOffence = Selector::typeSubtype(BonusType::PERCENTAGE_DAMAGE_BOOST, BonusCustomSubtype::damageTypeMelee); return info.attacker->valOfBonuses(selectorOffence, cachingStrOffence) / 100.0; } @@ -232,7 +232,7 @@ double DamageCalculator::getAttackDoubleDamageFactor() const { if(info.doubleDamage) { const auto cachingStr = "type_BONUS_DAMAGE_PERCENTAGEs_" + std::to_string(info.attacker->creatureIndex()); - const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, TBonusSubtype(info.attacker->creatureId())); + const auto selector = Selector::typeSubtype(BonusType::BONUS_DAMAGE_PERCENTAGE, BonusSubtypeID(info.attacker->creatureId())); return info.attacker->valOfBonuses(selector, cachingStr) / 100.0; } return 0.0; @@ -260,7 +260,7 @@ double DamageCalculator::getAttackHateFactor() const auto allHateEffects = info.attacker->getBonuses(selectorHate, cachingStrHate); - return allHateEffects->valOfBonuses(Selector::subtype()(TBonusSubtype(info.defender->creatureId()))) / 100.0; + return allHateEffects->valOfBonuses(Selector::subtype()(BonusSubtypeID(info.defender->creatureId()))) / 100.0; } double DamageCalculator::getDefenseSkillFactor() const @@ -282,7 +282,7 @@ double DamageCalculator::getDefenseSkillFactor() const double DamageCalculator::getDefenseArmorerFactor() const { const std::string cachingStrArmorer = "type_GENERAL_DAMAGE_REDUCTIONs_N1_NsrcSPELL_EFFECT"; - static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); + static const auto selectorArmorer = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT).Not()); return info.defender->valOfBonuses(selectorArmorer, cachingStrArmorer) / 100.0; } @@ -290,10 +290,10 @@ double DamageCalculator::getDefenseArmorerFactor() const double DamageCalculator::getDefenseMagicShieldFactor() const { const std::string cachingStrMeleeReduction = "type_GENERAL_DAMAGE_REDUCTIONs_0"; - static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeMelee); + static const auto selectorMeleeReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeMelee); const std::string cachingStrRangedReduction = "type_GENERAL_DAMAGE_REDUCTIONs_1"; - static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeRanged); + static const auto selectorRangedReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeRanged); //handling spell effects - shield and air shield if(info.shooting) @@ -313,7 +313,7 @@ double DamageCalculator::getDefenseRangePenaltiesFactor() const auto isAdvancedAirShield = [](const Bonus* bonus) { return bonus->source == BonusSource::SPELL_EFFECT - && bonus->sid == TBonusSourceID(SpellID(SpellID::AIR_SHIELD)) + && bonus->sid == BonusSourceID(SpellID(SpellID::AIR_SHIELD)) && bonus->val >= MasteryLevel::ADVANCED; }; @@ -387,7 +387,7 @@ double DamageCalculator::getDefensePetrificationFactor() const { // Creatures that are petrified by a Basilisk's Petrifying attack or a Medusa's Stone gaze take 50% damage (R8 = 0.50) from ranged and melee attacks. Taking damage also deactivates the effect. const std::string cachingStrAllReduction = "type_GENERAL_DAMAGE_REDUCTIONs_N1_srcSPELL_EFFECT"; - static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusSubtypeID::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); + static const auto selectorAllReduction = Selector::typeSubtype(BonusType::GENERAL_DAMAGE_REDUCTION, BonusCustomSubtype::damageTypeAll).And(Selector::sourceTypeSel(BonusSource::SPELL_EFFECT)); return info.defender->valOfBonuses(selectorAllReduction, cachingStrAllReduction) / 100.0; } diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index ea50c4d16..3bc76e1eb 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -150,7 +150,7 @@ JsonNode Bonus::toJsonNode() const JsonNode root(JsonNode::JsonType::DATA_STRUCT); // only add values that might reasonably be found in config files root["type"].String() = vstd::findKey(bonusNameMap, type); - if(subtype != TBonusSubtype()) + if(subtype != BonusSubtypeID()) root["subtype"].String() = subtype.toString(); if(additionalInfo != CAddInfo::NONE) root["addInfo"] = additionalInfoToJson(type, additionalInfo); @@ -158,7 +158,7 @@ JsonNode Bonus::toJsonNode() const root["sourceType"].String() = vstd::findKey(bonusSourceMap, source); if(targetSourceType != BonusSource::OTHER) root["targetSourceType"].String() = vstd::findKey(bonusSourceMap, targetSourceType); - if(sid != TBonusSourceID()) + if(sid != BonusSourceID()) root["sourceID"].String() = sid.toString(); if(val != 0) root["val"].Integer() = val; @@ -183,19 +183,19 @@ JsonNode Bonus::toJsonNode() const return root; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID) - : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype(), std::string()) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID) + : Bonus(Duration, Type, Src, Val, ID, BonusSubtypeID(), std::string()) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, std::string Desc) - : Bonus(Duration, Type, Src, Val, ID, TBonusSubtype(), Desc) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, std::string Desc) + : Bonus(Duration, Type, Src, Val, ID, BonusSubtypeID(), Desc) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype) +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype) : Bonus(Duration, Type, Src, Val, ID, Subtype, std::string()) {} -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype, std::string Desc): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype, std::string Desc): duration(Duration), type(Type), subtype(Subtype), @@ -208,7 +208,7 @@ Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 targetSourceType = BonusSource::OTHER; } -Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID ID, TBonusSubtype Subtype, BonusValueType ValType): +Bonus::Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID ID, BonusSubtypeID Subtype, BonusValueType ValType): duration(Duration), type(Type), subtype(Subtype), diff --git a/lib/bonuses/Bonus.h b/lib/bonuses/Bonus.h index 47273f73d..46b510e8c 100644 --- a/lib/bonuses/Bonus.h +++ b/lib/bonuses/Bonus.h @@ -10,7 +10,7 @@ #pragma once #include "BonusEnum.h" -#include "BonusSubtypeID.h" +#include "BonusCustomTypes.h" #include "../constants/VariantIdentifier.h" #include "../constants/EntityIdentifiers.h" @@ -25,8 +25,8 @@ class IUpdater; class BonusList; class CSelector; -using TBonusSubtype = VariantIdentifier; -using TBonusSourceID = VariantIdentifier; +using BonusSubtypeID = VariantIdentifier; +using BonusSourceID = VariantIdentifier; using TBonusListPtr = std::shared_ptr; using TConstBonusListPtr = std::shared_ptr; using TLimiterPtr = std::shared_ptr; @@ -60,12 +60,12 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this si16 turnsRemain = 0; //used if duration is N_TURNS, N_DAYS or ONE_WEEK BonusType type = BonusType::NONE; //uses BonusType values - says to what is this bonus - 1 byte - TBonusSubtype subtype; + BonusSubtypeID subtype; BonusSource source = BonusSource::OTHER; //source type" uses BonusSource values - what gave that bonus BonusSource targetSourceType;//Bonuses of what origin this amplifies, uses BonusSource values. Needed for PERCENT_TO_TARGET_TYPE. si32 val = 0; - TBonusSourceID sid; //source id: id of object/artifact/spell + BonusSourceID sid; //source id: id of object/artifact/spell BonusValueType valType = BonusValueType::ADDITIVE_VALUE; std::string stacking; // bonuses with the same stacking value don't stack (e.g. Angel/Archangel morale bonus) @@ -79,11 +79,11 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this std::string description; - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, std::string Desc); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype, std::string Desc); - Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, TBonusSourceID sourceID, TBonusSubtype subtype, BonusValueType ValType); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, std::string Desc); + Bonus(BonusDuration::Type Duration, BonusType Type, BonusSource Src, si32 Val, BonusSourceID sourceID, BonusSubtypeID subtype, BonusValueType ValType); Bonus() = default; template void serialize(Handler &h, const int version) diff --git a/lib/bonuses/BonusCustomTypes.cpp b/lib/bonuses/BonusCustomTypes.cpp new file mode 100644 index 000000000..4e793ac21 --- /dev/null +++ b/lib/bonuses/BonusCustomTypes.cpp @@ -0,0 +1,74 @@ +/* + * BonusCustomTypes.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 "BonusCustomTypes.h" + +VCMI_LIB_NAMESPACE_BEGIN + +const BonusCustomSubtype BonusCustomSubtype::creatureDamageBoth(0); +const BonusCustomSubtype BonusCustomSubtype::creatureDamageMin(1); +const BonusCustomSubtype BonusCustomSubtype::creatureDamageMax(2); +const BonusCustomSubtype BonusCustomSubtype::damageTypeAll(-1); +const BonusCustomSubtype BonusCustomSubtype::damageTypeMelee(0); +const BonusCustomSubtype BonusCustomSubtype::damageTypeRanged(1); +const BonusCustomSubtype BonusCustomSubtype::heroMovementLand(1); +const BonusCustomSubtype BonusCustomSubtype::heroMovementSea(0); +const BonusCustomSubtype BonusCustomSubtype::deathStareGorgon(0); +const BonusCustomSubtype BonusCustomSubtype::deathStareCommander(1); +const BonusCustomSubtype BonusCustomSubtype::rebirthRegular(0); +const BonusCustomSubtype BonusCustomSubtype::rebirthSpecial(1); +const BonusCustomSubtype BonusCustomSubtype::visionsMonsters(0); +const BonusCustomSubtype BonusCustomSubtype::visionsHeroes(1); +const BonusCustomSubtype BonusCustomSubtype::visionsTowns(2); +const BonusCustomSubtype BonusCustomSubtype::immunityBattleWide(0); +const BonusCustomSubtype BonusCustomSubtype::immunityEnemyHero(1); +const BonusCustomSubtype BonusCustomSubtype::transmutationPerHealth(0); +const BonusCustomSubtype BonusCustomSubtype::transmutationPerUnit(1); +const BonusCustomSubtype BonusCustomSubtype::destructionKillPercentage(0); +const BonusCustomSubtype BonusCustomSubtype::destructionKillAmount(1); +const BonusCustomSubtype BonusCustomSubtype::soulStealPermanent(0); +const BonusCustomSubtype BonusCustomSubtype::soulStealBattle(1); +const BonusCustomSubtype BonusCustomSubtype::movementFlying(0); +const BonusCustomSubtype BonusCustomSubtype::movementTeleporting(1); + +const BonusCustomSource BonusCustomSource::undeadMoraleDebuff(-2); + +BonusCustomSubtype BonusCustomSubtype::spellLevel(int level) +{ + return BonusCustomSubtype(level); +} + +BonusCustomSubtype BonusCustomSubtype::creatureLevel(int level) +{ + return BonusCustomSubtype(level); +} + +si32 BonusCustomSubtype::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusCustomSubtype::encode(const si32 index) +{ + return std::to_string(index); +} + +si32 BonusCustomSource::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + +std::string BonusCustomSource::encode(const si32 index) +{ + return std::to_string(index); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h new file mode 100644 index 000000000..2a2f3df48 --- /dev/null +++ b/lib/bonuses/BonusCustomTypes.h @@ -0,0 +1,75 @@ +/* + * BonusCustomTypes.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 "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE BonusCustomSource : public Identifier +{ +public: + using Identifier::Identifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusCustomSource undeadMoraleDebuff; // -2 +}; + +class DLL_LINKAGE BonusCustomSubtype : public Identifier +{ +public: + using Identifier::Identifier; + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + + static const BonusCustomSubtype creatureDamageBoth; // 0 + static const BonusCustomSubtype creatureDamageMin; // 1 + static const BonusCustomSubtype creatureDamageMax; // 2 + + static const BonusCustomSubtype damageTypeAll; // -1 + static const BonusCustomSubtype damageTypeMelee; // 0 + static const BonusCustomSubtype damageTypeRanged; // 1 + + static const BonusCustomSubtype heroMovementLand; // 1 + static const BonusCustomSubtype heroMovementSea; // 0 + + static const BonusCustomSubtype deathStareGorgon; // 0 + static const BonusCustomSubtype deathStareCommander; + + static const BonusCustomSubtype rebirthRegular; // 0 + static const BonusCustomSubtype rebirthSpecial; // 1 + + static const BonusCustomSubtype visionsMonsters; // 0 + static const BonusCustomSubtype visionsHeroes; // 1 + static const BonusCustomSubtype visionsTowns; // 2 + + static const BonusCustomSubtype immunityBattleWide; // 0 + static const BonusCustomSubtype immunityEnemyHero; // 1 + + static const BonusCustomSubtype transmutationPerHealth; // 0 + static const BonusCustomSubtype transmutationPerUnit; // 1 + + static const BonusCustomSubtype destructionKillPercentage; // 0 + static const BonusCustomSubtype destructionKillAmount; // 1 + + static const BonusCustomSubtype soulStealPermanent; // 0 + static const BonusCustomSubtype soulStealBattle; // 1 + + static const BonusCustomSubtype movementFlying; // 0 + static const BonusCustomSubtype movementTeleporting; // 1 + + static BonusCustomSubtype spellLevel(int level); + static BonusCustomSubtype creatureLevel(int level); +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusParams.cpp b/lib/bonuses/BonusParams.cpp index e03e7891b..7274c0d9e 100644 --- a/lib/bonuses/BonusParams.cpp +++ b/lib/bonuses/BonusParams.cpp @@ -84,66 +84,66 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if(deprecatedSubtype == SecondarySkill::SORCERY || deprecatedSubtypeStr == "skill.sorcery") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(SpellSchool::ANY); + subtype = BonusSubtypeID(SpellSchool::ANY); } else if(deprecatedSubtype == SecondarySkill::SCHOLAR || deprecatedSubtypeStr == "skill.scholar") type = BonusType::LEARN_MEETING_SPELL_LIMIT; else if(deprecatedSubtype == SecondarySkill::ARCHERY|| deprecatedSubtypeStr == "skill.archery") { - subtype = BonusSubtypeID::damageTypeRanged; + subtype = BonusCustomSubtype::damageTypeRanged; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::OFFENCE || deprecatedSubtypeStr == "skill.offence") { - subtype = BonusSubtypeID::damageTypeMelee; + subtype = BonusCustomSubtype::damageTypeMelee; type = BonusType::PERCENTAGE_DAMAGE_BOOST; } else if(deprecatedSubtype == SecondarySkill::ARMORER || deprecatedSubtypeStr == "skill.armorer") { - subtype = BonusSubtypeID::damageTypeAll; + subtype = BonusCustomSubtype::damageTypeAll; type = BonusType::GENERAL_DAMAGE_REDUCTION; } else if(deprecatedSubtype == SecondarySkill::NAVIGATION || deprecatedSubtypeStr == "skill.navigation") { - subtype = BonusSubtypeID::heroMovementSea; + subtype = BonusCustomSubtype::heroMovementSea; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::LOGISTICS || deprecatedSubtypeStr == "skill.logistics") { - subtype = BonusSubtypeID::heroMovementLand; + subtype = BonusCustomSubtype::heroMovementLand; valueType = BonusValueType::PERCENT_TO_BASE; type = BonusType::MOVEMENT; } else if(deprecatedSubtype == SecondarySkill::ESTATES || deprecatedSubtypeStr == "skill.estates") { type = BonusType::GENERATE_RESOURCE; - subtype = TBonusSubtype(GameResID(EGameResID::GOLD)); + subtype = BonusSubtypeID(GameResID(EGameResID::GOLD)); } else if(deprecatedSubtype == SecondarySkill::AIR_MAGIC || deprecatedSubtypeStr == "skill.airMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(SpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if(deprecatedSubtype == SecondarySkill::WATER_MAGIC || deprecatedSubtypeStr == "skill.waterMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(SpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if(deprecatedSubtype == SecondarySkill::FIRE_MAGIC || deprecatedSubtypeStr == "skill.fireMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(SpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if(deprecatedSubtype == SecondarySkill::EARTH_MAGIC || deprecatedSubtypeStr == "skill.earthMagic") { type = BonusType::MAGIC_SCHOOL_SKILL; - subtype = TBonusSubtype(SpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); } else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::BONUS_DAMAGE_CHANCE; - subtype = TBonusSubtype(CreatureID(CreatureID::BALLISTA)); + subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA)); } else if (deprecatedSubtype == SecondarySkill::FIRST_AID || deprecatedSubtypeStr == "skill.firstAid") { @@ -165,20 +165,20 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedSubtype == SecondarySkill::ARTILLERY || deprecatedSubtypeStr == "skill.artillery") { type = BonusType::HERO_GRANTS_ATTACKS; - subtype = TBonusSubtype(CreatureID(CreatureID::BALLISTA)); + subtype = BonusSubtypeID(CreatureID(CreatureID::BALLISTA)); } else isConverted = false; } else if (deprecatedTypeStr == "SEA_MOVEMENT") { - subtype = BonusSubtypeID::heroMovementSea; + subtype = BonusCustomSubtype::heroMovementSea; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } else if (deprecatedTypeStr == "LAND_MOVEMENT") { - subtype = BonusSubtypeID::heroMovementLand; + subtype = BonusCustomSubtype::heroMovementLand; valueType = BonusValueType::ADDITIVE_VALUE; type = BonusType::MOVEMENT; } @@ -226,52 +226,52 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu else if (deprecatedTypeStr == "DIRECT_DAMAGE_IMMUNITY") { type = BonusType::SPELL_DAMAGE_REDUCTION; - subtype = TBonusSubtype(SpellSchool::ANY); + subtype = BonusSubtypeID(SpellSchool::ANY); val = 100; } else if (deprecatedTypeStr == "AIR_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(SpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(SpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(SpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELL_DMG_PREMY") { type = BonusType::SPELL_DAMAGE; - subtype = TBonusSubtype(SpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = TBonusSubtype(SpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); } else if (deprecatedTypeStr == "FIRE_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = TBonusSubtype(SpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); } else if (deprecatedTypeStr == "WATER_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = TBonusSubtype(SpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); } else if (deprecatedTypeStr == "EARTH_SPELLS") { type = BonusType::SPELLS_OF_SCHOOL; - subtype = TBonusSubtype(SpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); } else if (deprecatedTypeStr == "AIR_IMMUNITY") { - subtype = TBonusSubtype(SpellSchool::AIR); + subtype = BonusSubtypeID(SpellSchool::AIR); switch(deprecatedSubtype) { case 0: @@ -287,7 +287,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "FIRE_IMMUNITY") { - subtype = TBonusSubtype(SpellSchool::FIRE); + subtype = BonusSubtypeID(SpellSchool::FIRE); switch(deprecatedSubtype) { case 0: @@ -303,7 +303,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "WATER_IMMUNITY") { - subtype = TBonusSubtype(SpellSchool::WATER); + subtype = BonusSubtypeID(SpellSchool::WATER); switch(deprecatedSubtype) { case 0: @@ -319,7 +319,7 @@ BonusParams::BonusParams(std::string deprecatedTypeStr, std::string deprecatedSu } else if (deprecatedTypeStr == "EARTH_IMMUNITY") { - subtype = TBonusSubtype(SpellSchool::EARTH); + subtype = BonusSubtypeID(SpellSchool::EARTH); switch(deprecatedSubtype) { case 0: diff --git a/lib/bonuses/BonusParams.h b/lib/bonuses/BonusParams.h index 4aa7bfbf6..b0b2d3ef2 100644 --- a/lib/bonuses/BonusParams.h +++ b/lib/bonuses/BonusParams.h @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct DLL_LINKAGE BonusParams { bool isConverted; BonusType type = BonusType::NONE; - std::optional subtype = std::nullopt; + std::optional subtype = std::nullopt; std::optional valueType = std::nullopt; std::optional val = std::nullopt; std::optional targetType = std::nullopt; diff --git a/lib/bonuses/BonusSelector.cpp b/lib/bonuses/BonusSelector.cpp index 42dae43db..1d57e8e13 100644 --- a/lib/bonuses/BonusSelector.cpp +++ b/lib/bonuses/BonusSelector.cpp @@ -21,9 +21,9 @@ namespace Selector return stype; } - DLL_LINKAGE CSelectFieldEqual & subtype() + DLL_LINKAGE CSelectFieldEqual & subtype() { - static CSelectFieldEqual ssubtype(&Bonus::subtype); + static CSelectFieldEqual ssubtype(&Bonus::subtype); return ssubtype; } @@ -54,22 +54,22 @@ namespace Selector DLL_LINKAGE CWillLastTurns turns; DLL_LINKAGE CWillLastDays days; - CSelector DLL_LINKAGE typeSubtype(BonusType Type, TBonusSubtype Subtype) + CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype) { return type()(Type).And(subtype()(Subtype)); } - CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, TBonusSubtype subtype, const CAddInfo & info) + CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, BonusSubtypeID subtype, const CAddInfo & info) { return CSelectFieldEqual(&Bonus::type)(type) - .And(CSelectFieldEqual(&Bonus::subtype)(subtype)) + .And(CSelectFieldEqual(&Bonus::subtype)(subtype)) .And(CSelectFieldEqual(&Bonus::additionalInfo)(info)); } - CSelector DLL_LINKAGE source(BonusSource source, TBonusSourceID sourceID) + CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID) { return CSelectFieldEqual(&Bonus::source)(source) - .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); + .And(CSelectFieldEqual(&Bonus::sid)(sourceID)); } CSelector DLL_LINKAGE sourceTypeSel(BonusSource source) diff --git a/lib/bonuses/BonusSelector.h b/lib/bonuses/BonusSelector.h index 5719fdaf0..672fcfdc2 100644 --- a/lib/bonuses/BonusSelector.h +++ b/lib/bonuses/BonusSelector.h @@ -126,7 +126,7 @@ public: namespace Selector { extern DLL_LINKAGE CSelectFieldEqual & type(); - extern DLL_LINKAGE CSelectFieldEqual & subtype(); + extern DLL_LINKAGE CSelectFieldEqual & subtype(); extern DLL_LINKAGE CSelectFieldEqual & info(); extern DLL_LINKAGE CSelectFieldEqual & sourceType(); extern DLL_LINKAGE CSelectFieldEqual & targetSourceType(); @@ -134,9 +134,9 @@ namespace Selector extern DLL_LINKAGE CWillLastTurns turns; extern DLL_LINKAGE CWillLastDays days; - CSelector DLL_LINKAGE typeSubtype(BonusType Type, TBonusSubtype Subtype); - CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, TBonusSubtype subtype, const CAddInfo & info); - CSelector DLL_LINKAGE source(BonusSource source, TBonusSourceID sourceID); + CSelector DLL_LINKAGE typeSubtype(BonusType Type, BonusSubtypeID Subtype); + CSelector DLL_LINKAGE typeSubtypeInfo(BonusType type, BonusSubtypeID subtype, const CAddInfo & info); + CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID); CSelector DLL_LINKAGE sourceTypeSel(BonusSource source); CSelector DLL_LINKAGE valueType(BonusValueType valType); diff --git a/lib/bonuses/BonusSubtypeID.cpp b/lib/bonuses/BonusSubtypeID.cpp deleted file mode 100644 index 9ca0f6bb9..000000000 --- a/lib/bonuses/BonusSubtypeID.cpp +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Bonus.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 "BonusSubtypeID.h" - -VCMI_LIB_NAMESPACE_BEGIN - -const BonusSubtypeID BonusSubtypeID::creatureDamageBoth(0); -const BonusSubtypeID BonusSubtypeID::creatureDamageMin(1); -const BonusSubtypeID BonusSubtypeID::creatureDamageMax(2); -const BonusSubtypeID BonusSubtypeID::damageTypeAll(-1); -const BonusSubtypeID BonusSubtypeID::damageTypeMelee(0); -const BonusSubtypeID BonusSubtypeID::damageTypeRanged(1); -const BonusSubtypeID BonusSubtypeID::heroMovementLand(1); -const BonusSubtypeID BonusSubtypeID::heroMovementSea(0); -const BonusSubtypeID BonusSubtypeID::deathStareGorgon(0); -const BonusSubtypeID BonusSubtypeID::deathStareCommander(1); -const BonusSubtypeID BonusSubtypeID::rebirthRegular(0); -const BonusSubtypeID BonusSubtypeID::rebirthSpecial(1); -const BonusSubtypeID BonusSubtypeID::visionsMonsters(0); -const BonusSubtypeID BonusSubtypeID::visionsHeroes(1); -const BonusSubtypeID BonusSubtypeID::visionsTowns(2); -const BonusSubtypeID BonusSubtypeID::immunityBattleWide(0); -const BonusSubtypeID BonusSubtypeID::immunityEnemyHero(1); -const BonusSubtypeID BonusSubtypeID::transmutationPerHealth(0); -const BonusSubtypeID BonusSubtypeID::transmutationPerUnit(1); -const BonusSubtypeID BonusSubtypeID::destructionKillPercentage(0); -const BonusSubtypeID BonusSubtypeID::destructionKillAmount(1); -const BonusSubtypeID BonusSubtypeID::soulStealPermanent(0); -const BonusSubtypeID BonusSubtypeID::soulStealBattle(1); -const BonusSubtypeID BonusSubtypeID::movementFlying(0); -const BonusSubtypeID BonusSubtypeID::movementTeleporting(1); - -const BonusSourceID BonusSourceID::undeadMoraleDebuff(-2); - -BonusSubtypeID BonusSubtypeID::spellLevel(int level) -{ - return BonusSubtypeID(level); -} - -BonusSubtypeID BonusSubtypeID::creatureLevel(int level) -{ - return BonusSubtypeID(level); -} - -si32 BonusSubtypeID::decode(const std::string & identifier) -{ - return std::stoi(identifier); -} - -std::string BonusSubtypeID::encode(const si32 index) -{ - return std::to_string(index); -} - -si32 BonusSourceID::decode(const std::string & identifier) -{ - return std::stoi(identifier); -} - -std::string BonusSourceID::encode(const si32 index) -{ - return std::to_string(index); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/BonusSubtypeID.h b/lib/bonuses/BonusSubtypeID.h deleted file mode 100644 index c73572cc2..000000000 --- a/lib/bonuses/BonusSubtypeID.h +++ /dev/null @@ -1,75 +0,0 @@ -/* - * BonusSubtypeID.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 "../constants/EntityIdentifiers.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE BonusSourceID : public Identifier -{ -public: - using Identifier::Identifier; - - static std::string encode(int32_t index); - static si32 decode(const std::string & identifier); - - static const BonusSourceID undeadMoraleDebuff; // -2 -}; - -class DLL_LINKAGE BonusSubtypeID : public Identifier -{ -public: - using Identifier::Identifier; - - static std::string encode(int32_t index); - static si32 decode(const std::string & identifier); - - static const BonusSubtypeID creatureDamageBoth; // 0 - static const BonusSubtypeID creatureDamageMin; // 1 - static const BonusSubtypeID creatureDamageMax; // 2 - - static const BonusSubtypeID damageTypeAll; // -1 - static const BonusSubtypeID damageTypeMelee; // 0 - static const BonusSubtypeID damageTypeRanged; // 1 - - static const BonusSubtypeID heroMovementLand; // 1 - static const BonusSubtypeID heroMovementSea; // 0 - - static const BonusSubtypeID deathStareGorgon; // 0 - static const BonusSubtypeID deathStareCommander; - - static const BonusSubtypeID rebirthRegular; // 0 - static const BonusSubtypeID rebirthSpecial; // 1 - - static const BonusSubtypeID visionsMonsters; // 0 - static const BonusSubtypeID visionsHeroes; // 1 - static const BonusSubtypeID visionsTowns; // 2 - - static const BonusSubtypeID immunityBattleWide; // 0 - static const BonusSubtypeID immunityEnemyHero; // 1 - - static const BonusSubtypeID transmutationPerHealth; // 0 - static const BonusSubtypeID transmutationPerUnit; // 1 - - static const BonusSubtypeID destructionKillPercentage; // 0 - static const BonusSubtypeID destructionKillAmount; // 1 - - static const BonusSubtypeID soulStealPermanent; // 0 - static const BonusSubtypeID soulStealBattle; // 1 - - static const BonusSubtypeID movementFlying; // 0 - static const BonusSubtypeID movementTeleporting; // 1 - - static BonusSubtypeID spellLevel(int level); - static BonusSubtypeID creatureLevel(int level); -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/bonuses/IBonusBearer.cpp b/lib/bonuses/IBonusBearer.cpp index 01f229b32..63ab0b631 100644 --- a/lib/bonuses/IBonusBearer.cpp +++ b/lib/bonuses/IBonusBearer.cpp @@ -62,7 +62,7 @@ bool IBonusBearer::hasBonusOfType(BonusType type) const return hasBonus(s, cachingStr); } -int IBonusBearer::valOfBonuses(BonusType type, TBonusSubtype subtype) const +int IBonusBearer::valOfBonuses(BonusType type, BonusSubtypeID subtype) const { //This part is performance-critical std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); @@ -72,7 +72,7 @@ int IBonusBearer::valOfBonuses(BonusType type, TBonusSubtype subtype) const return valOfBonuses(s, cachingStr); } -bool IBonusBearer::hasBonusOfType(BonusType type, TBonusSubtype subtype) const +bool IBonusBearer::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const { //This part is performance-critical std::string cachingStr = "type_" + std::to_string(static_cast(type)) + "_" + subtype.toString(); @@ -82,7 +82,7 @@ bool IBonusBearer::hasBonusOfType(BonusType type, TBonusSubtype subtype) const return hasBonus(s, cachingStr); } -bool IBonusBearer::hasBonusFrom(BonusSource source, TBonusSourceID sourceID) const +bool IBonusBearer::hasBonusFrom(BonusSource source, BonusSourceID sourceID) const { boost::format fmt("source_%did_%s"); fmt % static_cast(source) % sourceID.toString(); diff --git a/lib/bonuses/IBonusBearer.h b/lib/bonuses/IBonusBearer.h index d0edac056..cae96f06a 100644 --- a/lib/bonuses/IBonusBearer.h +++ b/lib/bonuses/IBonusBearer.h @@ -35,9 +35,9 @@ public: //Optimized interface (with auto-caching) int valOfBonuses(BonusType type) const; //subtype -> subtype of bonus; bool hasBonusOfType(BonusType type) const;//determines if hero has a bonus of given type (and optionally subtype) - int valOfBonuses(BonusType type, TBonusSubtype subtype) const; //subtype -> subtype of bonus; - bool hasBonusOfType(BonusType type, TBonusSubtype subtype) const;//determines if hero has a bonus of given type (and optionally subtype) - bool hasBonusFrom(BonusSource source, TBonusSourceID sourceID) const; + int valOfBonuses(BonusType type, BonusSubtypeID subtype) const; //subtype -> subtype of bonus; + bool hasBonusOfType(BonusType type, BonusSubtypeID subtype) const;//determines if hero has a bonus of given type (and optionally subtype) + bool hasBonusFrom(BonusSource source, BonusSourceID sourceID) const; virtual int64_t getTreeVersion() const = 0; }; diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index e937f9934..a2461b1e7 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -140,7 +140,7 @@ HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus ) { } -HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, TBonusSubtype _subtype ) +HasAnotherBonusLimiter::HasAnotherBonusLimiter( BonusType bonus, BonusSubtypeID _subtype ) : type(bonus), subtype(_subtype), isSubtypeRelevant(true), isSourceRelevant(false), isSourceIDRelevant(false) { } @@ -150,7 +150,7 @@ HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSource src) { } -HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype, BonusSource src) +HasAnotherBonusLimiter::HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src) : type(bonus), subtype(_subtype), isSubtypeRelevant(true), source(src), isSourceRelevant(true), isSourceIDRelevant(false) { } diff --git a/lib/bonuses/Limiters.h b/lib/bonuses/Limiters.h index 9c4b58924..2e0b60126 100644 --- a/lib/bonuses/Limiters.h +++ b/lib/bonuses/Limiters.h @@ -116,17 +116,17 @@ class DLL_LINKAGE HasAnotherBonusLimiter : public ILimiter //applies only to nod { public: BonusType type; - TBonusSubtype subtype; + BonusSubtypeID subtype; BonusSource source; - TBonusSourceID sid; + BonusSourceID sid; bool isSubtypeRelevant; //check for subtype only if this is true bool isSourceRelevant; //check for bonus source only if this is true bool isSourceIDRelevant; //check for bonus source only if this is true HasAnotherBonusLimiter(BonusType bonus = BonusType::NONE); - HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype); + HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype); HasAnotherBonusLimiter(BonusType bonus, BonusSource src); - HasAnotherBonusLimiter(BonusType bonus, TBonusSubtype _subtype, BonusSource src); + HasAnotherBonusLimiter(BonusType bonus, BonusSubtypeID _subtype, BonusSource src); EDecision limit(const BonusLimitationContext &context) const override; std::string toString() const override; diff --git a/lib/campaign/CampaignConstants.h b/lib/campaign/CampaignConstants.h index d8469a566..e4a615d47 100644 --- a/lib/campaign/CampaignConstants.h +++ b/lib/campaign/CampaignConstants.h @@ -11,8 +11,6 @@ VCMI_LIB_NAMESPACE_BEGIN -#include "../constants/EntityIdentifiers.h" - enum class CampaignVersion : uint8_t { NONE = 0, diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 5889e8730..c349e851d 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -655,7 +655,7 @@ void CGameState::initGlobalBonuses() { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::GLOBAL;//for all - bonus->sid = TBonusSourceID(); //there is one global object + bonus->sid = BonusSourceID(); //there is one global object globalEffects.addNewBonus(bonus); } VLC->creh->loadCrExpBon(globalEffects); @@ -1871,7 +1871,7 @@ struct statsHLP //Heroes can produce gold as well - skill, specialty or arts for(const auto & h : ps->heroes) { - totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, TBonusSubtype(GameResID(GameResID::GOLD)))); + totalIncome += h->valOfBonuses(Selector::typeSubtype(BonusType::GENERATE_RESOURCE, BonusSubtypeID(GameResID(GameResID::GOLD)))); if(!heroOrTown) heroOrTown = h; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 0c35b9511..443aedf9f 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -87,7 +87,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorgetBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g]; @@ -315,7 +315,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) continue; auto currentScenario = *gameState->scenarioOps->campState->currentScenario(); - auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, TBonusSourceID(currentScenario), TBonusSubtype(g) ); + auto bb = std::make_shared( BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, BonusSourceID(currentScenario), BonusSubtypeID(g) ); hero->addNewBonus(bb); } break; diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index af68c31a3..1726b7beb 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -50,7 +50,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG for (auto & bonus : rewardInfo.reward.bonuses) { bonus.source = BonusSource::OBJECT; - bonus.sid = TBonusSourceID(rewardableObject->ID); + bonus.sid = BonusSourceID(rewardableObject->ID); } } assert(!rewardableObject->configuration.info.empty()); diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index d7f8f4036..39f7ed971 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -59,7 +59,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID()); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, BonusSourceID()); addNewBonus(b); } @@ -120,12 +120,12 @@ void CArmedInstance::updateMoraleBonusFromArmy() CBonusSystemNode::treeHasChanged(); //-1 modifier for any Undead unit in army - auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, BonusSourceID::undeadMoraleDebuff)); + auto undeadModifier = getExportedBonusList().getFirst(Selector::source(BonusSource::ARMY, BonusCustomSource::undeadMoraleDebuff)); if(hasUndead) { if(!undeadModifier) { - undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, BonusSourceID::undeadMoraleDebuff, VLC->generaltexth->arraytxt[116]); + undeadModifier = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, -1, BonusCustomSource::undeadMoraleDebuff, VLC->generaltexth->arraytxt[116]); undeadModifier->description = undeadModifier->description.substr(0, undeadModifier->description.size()-2);//trim value addNewBonus(undeadModifier); } diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 01401fd77..7e372b4cf 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -213,7 +213,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const gbonus.id = hero->id.getNum(); gbonus.bonus.duration = BonusDuration::ONE_BATTLE; gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = TBonusSourceID(ID); + gbonus.bonus.sid = BonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; switch (ID) @@ -239,7 +239,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const case Obj::PYRAMID: { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, TBonusSourceID(id), VLC->generaltexth->arraytxt[70]); + gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); gb.id = hero->id.getNum(); cb->giveHeroBonus(&gb); textID = 107; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 85b82a2f6..62883711b 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -46,7 +46,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { std::string hoverName; - if(hero->hasVisions(this, BonusSubtypeID::visionsMonsters)) + if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index d3a58ac0e..dc22f4ae5 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -95,7 +95,7 @@ ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const Terrain } else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus - !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, TBonusSubtype(from.terType->getId()))) //no special movement bonus + !ti->hasBonusOfType(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(from.terType->getId()))) //no special movement bonus { ret = VLC->terrainTypeHandler->getById(from.terType->getId())->moveCost; @@ -249,14 +249,14 @@ void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) c lowestCreatureSpeed = realLowestSpeed; //Let updaters run again treeHasChanged(); - ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusSubtypeID::heroMovementLand : BonusSubtypeID::heroMovementSea)); + ti->updateHeroBonuses(BonusType::MOVEMENT, Selector::subtype()(onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea)); } } int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const { updateArmyMovementBonus(onLand, ti); - return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusSubtypeID::heroMovementLand : BonusSubtypeID::heroMovementSea); + return ti->valOfBonuses(BonusType::MOVEMENT, onLand ? BonusCustomSubtype::heroMovementLand : BonusCustomSubtype::heroMovementSea); } CGHeroInstance::CGHeroInstance(): @@ -369,7 +369,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { auto bonus = JsonUtils::parseBonus(b.second); bonus->source = BonusSource::HERO_BASE_SKILL; - bonus->sid = TBonusSourceID(id); + bonus->sid = BonusSourceID(id); bonus->duration = BonusDuration::PERMANENT; addNewBonus(bonus); } @@ -589,7 +589,7 @@ void CGHeroInstance::recreateSecondarySkillsBonuses() void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val) { - removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, TBonusSourceID(which))); + removeBonuses(Selector::source(BonusSource::SECONDARY_SKILL, BonusSourceID(which))); auto skillBonus = (*VLC->skillh)[which]->at(val).effects; for(const auto & b : skillBonus) addNewBonus(std::make_shared(*b)); @@ -638,7 +638,7 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t spell->forEachSchool([&, this](const SpellSchool & cnf, bool & stop) { - int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) + int32_t thisSchool = valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(cnf)); //FIXME: Bonus shouldn't be additive (Witchking Artifacts : Crown of Skies) if(thisSchool > skill) { skill = thisSchool; @@ -647,8 +647,8 @@ int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t } }); - vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(SpellSchool::ANY))); //any school bonus - vstd::amax(skill, valOfBonuses(BonusType::SPELL, TBonusSubtype(spell->getId()))); //given by artifact or other effect + vstd::amax(skill, valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); //any school bonus + vstd::amax(skill, valOfBonuses(BonusType::SPELL, BonusSubtypeID(spell->getId()))); //given by artifact or other effect vstd::amax(skill, 0); //in case we don't know any school vstd::amin(skill, 3); @@ -660,28 +660,28 @@ int64_t CGHeroInstance::getSpellBonus(const spells::Spell * spell, int64_t base, //applying sorcery secondary skill if(spell->isMagical()) - base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, TBonusSubtype(SpellSchool::ANY))) / 100.0); + base = static_cast(base * (valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(SpellSchool::ANY))) / 100.0); - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, TBonusSubtype(spell->getId()))) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0); int maxSchoolBonus = 0; spell->forEachSchool([&maxSchoolBonus, this](const SpellSchool & cnf, bool & stop) { - vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, TBonusSubtype(cnf))); + vstd::amax(maxSchoolBonus, valOfBonuses(BonusType::SPELL_DAMAGE, BonusSubtypeID(cnf))); }); base = static_cast(base * (100 + maxSchoolBonus) / 100.0); if(affectedStack && affectedStack->creatureLevel() > 0) //Hero specials like Solmyr, Deemer - base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, TBonusSubtype(spell->getId())) / affectedStack->creatureLevel()) / 100.0); + base = static_cast(base * static_cast(100 + valOfBonuses(BonusType::SPECIAL_SPELL_LEV, BonusSubtypeID(spell->getId())) / affectedStack->creatureLevel()) / 100.0); return base; } int64_t CGHeroInstance::getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const { - base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, TBonusSubtype(spell->getId()))) / 100.0); + base = static_cast(base * (100 + valOfBonuses(BonusType::SPECIFIC_SPELL_DAMAGE, BonusSubtypeID(spell->getId()))) / 100.0); return base; } @@ -751,19 +751,19 @@ bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->getIndex()); const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook(); - const bool specificBonus = hasBonusOfType(BonusType::SPELL, TBonusSubtype(spell->getId())); + const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId())); bool schoolBonus = false; spell->forEachSchool([this, &schoolBonus](const SpellSchool & cnf, bool & stop) { - if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, TBonusSubtype(cnf))) + if(hasBonusOfType(BonusType::SPELLS_OF_SCHOOL, BonusSubtypeID(cnf))) { schoolBonus = stop = true; } }); - const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusSubtypeID::spellLevel(spell->getLevel())); + const bool levelBonus = hasBonusOfType(BonusType::SPELLS_OF_LEVEL, BonusCustomSubtype::spellLevel(spell->getLevel())); if(spell->isSpecial()) { @@ -1008,12 +1008,12 @@ int32_t CGHeroInstance::getSpellCost(const spells::Spell * sp) const void CGHeroInstance::pushPrimSkill( PrimarySkill which, int val ) { - auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(which)) + auto sel = Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(which)) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)); if(hasBonus(sel)) removeBonuses(sel); - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, TBonusSourceID(id), TBonusSubtype(which))); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::HERO_BASE_SKILL, val, BonusSourceID(id), BonusSubtypeID(which))); } EAlignment CGHeroInstance::getAlignment() const @@ -1396,7 +1396,7 @@ void CGHeroInstance::setPrimarySkill(PrimarySkill primarySkill, si64 value, ui8 if(primarySkill < PrimarySkill::EXPERIENCE) { auto skill = getBonusLocalFirst(Selector::type()(BonusType::PRIMARY_SKILL) - .And(Selector::subtype()(TBonusSubtype(primarySkill))) + .And(Selector::subtype()(BonusSubtypeID(primarySkill))) .And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); assert(skill); @@ -1467,7 +1467,7 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) } } -bool CGHeroInstance::hasVisions(const CGObjectInstance * target, TBonusSubtype subtype) const +bool CGHeroInstance::hasVisions(const CGObjectInstance * target, BonusSubtypeID subtype) const { //VISIONS spell support const int visionsMultiplier = valOfBonuses(BonusType::VISIONS, subtype); @@ -1559,7 +1559,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(auto i = PrimarySkill::BEGIN; i < PrimarySkill::END; ++i) { - int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(i)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); + int value = valOfBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(i)).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))); handler.serializeInt(NPrimarySkill::names[i.getNum()], value, 0); } @@ -1752,7 +1752,7 @@ bool CGHeroInstance::isMissionCritical() const void CGHeroInstance::fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const { - TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, TBonusSubtype(stack.type->getId()))); + TConstBonusListPtr lista = getBonuses(Selector::typeSubtype(BonusType::SPECIAL_UPGRADE, BonusSubtypeID(stack.type->getId()))); for(const auto & it : *lista) { auto nid = CreatureID(it->additionalInfo[0]); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index ce0196a04..2035a488b 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -248,7 +248,7 @@ public: void fillUpgradeInfo(UpgradeInfo & info, const CStackInstance &stack) const override; - bool hasVisions(const CGObjectInstance * target, TBonusSubtype masteryLevel) const; + bool hasVisions(const CGObjectInstance * target, BonusSubtypeID masteryLevel) const; /// If this hero perishes, the scenario is failed bool isMissionCritical() const; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index ffbf6d416..7b95c108f 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -222,7 +222,7 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura gbonus.id = heroID.getNum(); gbonus.bonus.duration = duration; gbonus.bonus.source = BonusSource::OBJECT; - gbonus.bonus.sid = TBonusSourceID(ID); + gbonus.bonus.sid = BonusSourceID(ID); cb->giveHeroBonus(&gbonus); } diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 38a89f73a..808088c36 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -227,11 +227,11 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) int val; handler.serializeInt("morale", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(id)); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(id)); handler.serializeInt("luck", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(id)); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(id)); vinfo.reward.resources.serializeJson(handler, "resources"); { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 38994ebb9..0c3851d35 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -143,10 +143,10 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const switch (this->bType) { case BuildingSubID::STABLES: - if(!h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables + if(!h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, TBonusSourceID(Obj(Obj::STABLES)), BonusSubtypeID::heroMovementLand, VLC->generaltexth->arraytxt[100]); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); gb.id = heroID.getNum(); cb->giveHeroBonus(&gb); @@ -235,7 +235,7 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const case BuildingSubID::CUSTOM_VISITING_BONUS: const auto building = town->getTown()->buildings.at(bID); - if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, TBonusSourceID(building->getUniqueTypeID()))) + if(!h->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID()))) { const auto & bonuses = building->onVisitBonuses; applyBonuses(const_cast(h), bonuses); @@ -306,7 +306,7 @@ void CTownRewardableBuilding::initObj(CRandomGenerator & rand) for (auto & bonus : rewardInfo.reward.bonuses) { bonus.source = BonusSource::TOWN_STRUCTURE; - bonus.sid = TBonusSourceID(building->getUniqueTypeID()); + bonus.sid = BonusSourceID(building->getUniqueTypeID()); } } } @@ -395,7 +395,7 @@ bool CTownRewardableBuilding::wasVisitedBefore(const CGHeroInstance * contextHer case Rewardable::VISIT_BONUS: { const auto building = town->getTown()->buildings.at(bID); - return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, TBonusSourceID(building->getUniqueTypeID())); + return contextHero->hasBonusFrom(BonusSource::TOWN_STRUCTURE, BonusSourceID(building->getUniqueTypeID())); } case Rewardable::VISIT_HERO: return visitors.find(contextHero->id) != visitors.end(); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 05b1c3b78..9d8abbcd3 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -160,7 +160,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const } //other *-of-legion-like bonuses (%d to growth cumulative with grail) - TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusSubtypeID::creatureLevel(level))); + TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level))); for(const auto & b : *bonuses) ret.entries.emplace_back(b->val, b->Description()); @@ -787,7 +787,7 @@ void CGTownInstance::updateMoraleBonusFromArmy() auto b = getExportedBonusList().getFirst(Selector::sourceType()(BonusSource::ARMY).And(Selector::type()(BonusType::MORALE))); if(!b) { - b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, TBonusSourceID()); + b = std::make_shared(BonusDuration::PERMANENT, BonusType::MORALE, BonusSource::ARMY, 0, BonusSourceID()); addNewBonus(b); } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index dd15cff91..09d98a063 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -676,9 +676,9 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(metaTypeName == "mana") reward.manaDiff = val; if(metaTypeName == "morale") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(id)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(id)); if(metaTypeName == "luck") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(id)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(id)); if(metaTypeName == "resource") { auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 9b1be30c9..8eac99996 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -197,7 +197,7 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con case Rewardable::VISIT_PLAYER: return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id)); case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID)); + return contextHero->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: @@ -234,7 +234,7 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const switch (configuration.visitMode) { case Rewardable::VISIT_BONUS: - return h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID)); + return h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 847c0b1ad..0ae6b808e 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -987,14 +987,14 @@ void CGSirens::initObj(CRandomGenerator & rand) std::string CGSirens::getHoverText(const CGHeroInstance * hero) const { - return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID))); + return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID))); } void CGSirens::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->tempOwner; - if(h->hasBonusFrom(BonusSource::OBJECT, TBonusSourceID(ID))) //has already visited Sirens + if(h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID))) //has already visited Sirens { iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::ADVOB_TXT,133); @@ -1180,7 +1180,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const RemoveBonus rb(GiveBonus::ETarget::PLAYER); rb.whoID = oldOwner.getNum(); rb.source = BonusSource::OBJECT; - rb.id = TBonusSourceID(id); + rb.id = BonusSourceID(id); cb->sendAndApply(&rb); } } @@ -1203,8 +1203,8 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const gb.id = player.getNum(); gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT; - gb.bonus.sid = TBonusSourceID(id); - gb.bonus.subtype = BonusSubtypeID::heroMovementSea; + gb.bonus.sid = BonusSourceID(id); + gb.bonus.subtype = BonusCustomSubtype::heroMovementSea; // FIXME: This is really dirty hack // Proper fix would be to make CGLighthouse into bonus system node diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 9613331cd..750ea4dcb 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1040,9 +1040,9 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, TBonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(idToBeGiven)); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, TBonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(idToBeGiven)); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) @@ -2008,12 +2008,12 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), TBonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), TBonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::RESOURCES: diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index f8256d120..f9cedc6b3 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -23,7 +23,7 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) for(const auto & terrain : VLC->terrainTypeHandler->objects) { noTerrainPenalty.push_back(static_cast( - bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(TBonusSubtype(terrain->getId())))))); + bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(BonusSubtypeID(terrain->getId())))))); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); @@ -73,10 +73,10 @@ bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const bool TurnInfo::hasBonusOfType(BonusType type) const { - return hasBonusOfType(type, TBonusSubtype()); + return hasBonusOfType(type, BonusSubtypeID()); } -bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const +bool TurnInfo::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const { switch(type) { @@ -96,10 +96,10 @@ bool TurnInfo::hasBonusOfType(BonusType type, TBonusSubtype subtype) const int TurnInfo::valOfBonuses(BonusType type) const { - return valOfBonuses(type, TBonusSubtype()); + return valOfBonuses(type, BonusSubtypeID()); } -int TurnInfo::valOfBonuses(BonusType type, TBonusSubtype subtype) const +int TurnInfo::valOfBonuses(BonusType type, BonusSubtypeID subtype) const { switch(type) { diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index 6bfa20098..4390e13c6 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -43,9 +43,9 @@ struct DLL_LINKAGE TurnInfo TurnInfo(const CGHeroInstance * Hero, const int Turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; bool hasBonusOfType(const BonusType type) const; - bool hasBonusOfType(const BonusType type, const TBonusSubtype subtype) const; + bool hasBonusOfType(const BonusType type, const BonusSubtypeID subtype) const; int valOfBonuses(const BonusType type) const; - int valOfBonuses(const BonusType type, const TBonusSubtype subtype) const; + int valOfBonuses(const BonusType type, const BonusSubtypeID subtype) const; void updateHeroBonuses(BonusType type, const CSelector& sel) const; int getMaxMovePoints(const EPathfindingLayer & layer) const; }; diff --git a/lib/spells/AbilityCaster.cpp b/lib/spells/AbilityCaster.cpp index d7507c451..01b1c105a 100644 --- a/lib/spells/AbilityCaster.cpp +++ b/lib/spells/AbilityCaster.cpp @@ -35,7 +35,7 @@ int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSel if(spell->getLevel() > 0) { - vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, TBonusSubtype(SpellSchool::ANY))); + vstd::amax(skill, unit->valOfBonuses(BonusType::MAGIC_SCHOOL_SKILL, BonusSubtypeID(SpellSchool::ANY))); } vstd::amax(skill, 0); diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 8c3c0b07d..9f21a598c 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -312,7 +312,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm std::stringstream cachingStr; cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << owner->id.num; - if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(owner->id)), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn + if(parameters.caster->getHeroCaster()->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(owner->id)), Selector::all, cachingStr.str())->size() >= owner->getLevelPower(schoolLevel)) //limit casts per turn { InfoWindow iw; iw.player = parameters.caster->getCasterOwner(); @@ -324,7 +324,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm GiveBonus gb; gb.id = parameters.caster->getCasterUnitId(); - gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(owner->id)); + gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); env->apply(&gb); if(!dest->isClear(curr)) //wrong dest tile diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 11fb7486f..ff235b245 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -383,15 +383,15 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni //applying protections - when spell has more then one elements, only one protection should be applied (I think) forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf))) + if(bearer->hasBonusOfType(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf))) { - ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf)); + ret *= 100 - bearer->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf)); ret /= 100; stop = true; //only bonus from one school is used } }); - CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::ANY)); + CSelector selector = Selector::typeSubtype(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::ANY)); auto cachingStr = "type_SPELL_DAMAGE_REDUCTION_s_ANY"; //general spell dmg reduction, works only on magical effects @@ -402,9 +402,9 @@ int64_t CSpell::adjustRawDamage(const spells::Caster * caster, const battle::Uni } //dmg increasing - if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, TBonusSubtype(id))) + if(bearer->hasBonusOfType(BonusType::MORE_DAMAGE_FROM_SPELL, BonusSubtypeID(id))) { - ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, TBonusSubtype(id)); + ret *= 100 + bearer->valOfBonuses(BonusType::MORE_DAMAGE_FROM_SPELL, BonusSubtypeID(id)); ret /= 100; } } @@ -925,7 +925,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = TBonusSourceID(spell->id); //for all + b->sid = BonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) @@ -940,7 +940,7 @@ CSpell * CSpellHandler::loadFromJson(const std::string & scope, const JsonNode & auto b = JsonUtils::parseBonus(bonusNode); const bool usePowerAsValue = bonusNode["val"].isNull(); - b->sid = TBonusSourceID(spell->id); //for all + b->sid = BonusSourceID(spell->id); //for all b->source = BonusSource::SPELL_EFFECT;//for all if(usePowerAsValue) diff --git a/lib/spells/TargetCondition.cpp b/lib/spells/TargetCondition.cpp index 107cdf5d6..bda1f32df 100644 --- a/lib/spells/TargetCondition.cpp +++ b/lib/spells/TargetCondition.cpp @@ -157,7 +157,7 @@ protected: { std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId()), 1), cachingStr.str()); + return !target->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId()), 1), cachingStr.str()); } }; @@ -179,14 +179,14 @@ protected: m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, TBonusSubtype(cnf))) + if (bearer->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, BonusSubtypeID(cnf))) { elementalImmune = true; stop = true; //only bonus from one school is used } else if(!m->isPositiveSpell()) //negative or indifferent { - if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, TBonusSubtype(cnf))) + if (bearer->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSubtypeID(cnf))) { elementalImmune = true; stop = true; //only bonus from one school is used @@ -231,7 +231,7 @@ public: protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId())); + return !target->hasBonusOfType(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId())); } }; @@ -259,7 +259,7 @@ public: builder << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num; cachingString = builder.str(); - selector = Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(spellID)); + selector = Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)); } protected: @@ -292,8 +292,8 @@ class ImmunityNegationCondition : public TargetConditionItemBase protected: bool check(const Mechanics * m, const battle::Unit * target) const override { - const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypeID::immunityBattleWide); - const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSubtypeID::immunityEnemyHero); + const bool battleWideNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusCustomSubtype::immunityBattleWide); + const bool heroNegation = target->hasBonusOfType(BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusCustomSubtype::immunityEnemyHero); //Non-magical effects is not affected by orb of vulnerability if(!m->isMagicalEffect()) return false; diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index a443ae665..65bd7df44 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -94,7 +94,7 @@ void Clone::apply(ServerCallback * server, const Mechanics * m, const EffectTarg SetStackEffect sse; sse.battleID = m->battle()->getBattle()->getBattleID(); - Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(SpellID(SpellID::CLONE))); //TODO: use special bonus type + Bonus lifeTimeMarker(BonusDuration::N_TURNS, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(SpellID(SpellID::CLONE))); //TODO: use special bonus type lifeTimeMarker.turnsRemain = m->getEffectDuration(); std::vector buffer; buffer.push_back(lifeTimeMarker); diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index ef7ad2425..0d3b17b76 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -89,11 +89,11 @@ bool Damage::isReceptive(const Mechanics * m, const battle::Unit * unit) const if(!UnitEffect::isReceptive(m, unit)) return false; - bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::ANY)) >= 100); //General spell damage immunity + bool isImmune = m->getSpell()->isMagical() && (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::ANY)) >= 100); //General spell damage immunity //elemental immunity for damage m->getSpell()->forEachSchool([&](const SpellSchool & cnf, bool & stop) { - isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(cnf)) >= 100); //100% reduction is immunity + isImmune |= (unit->getBonusBearer()->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(cnf)) >= 100); //100% reduction is immunity }); return !isImmune; diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 571481553..92b96dfab 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -84,12 +84,12 @@ void Moat::convertBonus(const Mechanics * m, std::vector & converted) con if(m->battle()->battleGetDefendedTown() && m->battle()->battleGetSiegeLevel() >= CGTownInstance::CITADEL) { - nb.sid = TBonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); + nb.sid = BonusSourceID(m->battle()->battleGetDefendedTown()->town->buildings.at(BuildingID::CITADEL)->getUniqueTypeID()); nb.source = BonusSource::TOWN_STRUCTURE; } else { - nb.sid = TBonusSourceID(m->getSpellId()); //for all + nb.sid = BonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all } std::set flatMoatHexes; diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 98d60ad44..31e85de67 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -109,9 +109,9 @@ void Timed::apply(ServerCallback * server, const Mechanics * m, const EffectTarg const auto *casterHero = dynamic_cast(m->caster); if(casterHero) { - peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, TBonusSubtype(m->getSpellId()))); - addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, TBonusSubtype(m->getSpellId()))); - fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, TBonusSubtype(m->getSpellId()))); + peculiarBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_PECULIAR_ENCHANT, BonusSubtypeID(m->getSpellId()))); + addedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_ADD_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); + fixedValueBonus = casterHero->getBonusLocalFirst(Selector::typeSubtype(BonusType::SPECIAL_FIXED_VALUE_ENCHANT, BonusSubtypeID(m->getSpellId()))); } //TODO: does hero specialty should affects his stack casting spells? @@ -222,7 +222,7 @@ void Timed::convertBonus(const Mechanics * m, int32_t & duration, std::vectorgetSpellId()); //for all + nb.sid = BonusSourceID(m->getSpellId()); //for all nb.source = BonusSource::SPELL_EFFECT;//for all //fix to original config: shield should display damage reduction diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index abc9d3036..c9dc5ff98 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -253,7 +253,7 @@ bool UnitEffect::isReceptive(const Mechanics * m, const battle::Unit * unit) con //SPELL_IMMUNITY absolute case std::stringstream cachingStr; cachingStr << "type_" << vstd::to_underlying(BonusType::SPELL_IMMUNITY) << "subtype_" << m->getSpellIndex() << "addInfo_1"; - return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, TBonusSubtype(m->getSpellId()), 1), cachingStr.str()); + return !unit->hasBonus(Selector::typeSubtypeInfo(BonusType::SPELL_IMMUNITY, BonusSubtypeID(m->getSpellId()), 1), cachingStr.str()); } else { diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index a404c582e..661dbf0fd 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -333,7 +333,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString()); auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString()); auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt(); - vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, TBonusSourceID(object.id)); + vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, BonusSourceID(object.id)); } vinfo.limiter.dayOfWeek = ui->lDayOfWeek->currentIndex(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index db99a1e9b..2fa72d5d1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -248,11 +248,11 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill) { case ECommander::ATTACK: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = TBonusSubtype(PrimarySkill::ATTACK); + scp.accumulatedBonus.subtype = BonusSubtypeID(PrimarySkill::ATTACK); break; case ECommander::DEFENSE: scp.accumulatedBonus.type = BonusType::PRIMARY_SKILL; - scp.accumulatedBonus.subtype = TBonusSubtype(PrimarySkill::DEFENSE); + scp.accumulatedBonus.subtype = BonusSubtypeID(PrimarySkill::DEFENSE); break; case ECommander::HEALTH: scp.accumulatedBonus.type = BonusType::STACK_HEALTH; @@ -788,7 +788,7 @@ void CGameHandler::onNewTurn() { for (GameResID k = GameResID::WOOD; k < GameResID::COUNT; k++) { - n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, TBonusSubtype(k)); + n.res[elem.first][k] += h->valOfBonuses(BonusType::GENERATE_RESOURCE, BonusSubtypeID(k)); } } } diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 45e0862f1..53a70645a 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -162,11 +162,11 @@ bool BattleActionProcessor::doDefendAction(const CBattleInfoCallback & battle, c SetStackEffect sse; sse.battleID = battle.getBattle()->getBattleID(); - Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); - Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus defenseBonusToAdd(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 20, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::PERCENT_TO_ALL); + Bonus bonus2(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, stack->valOfBonuses(BonusType::DEFENSIVE_STANCE), BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); + Bonus alternativeWeakCreatureBonus(BonusDuration::STACK_GETS_TURN, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE), BonusValueType::ADDITIVE_VALUE); - BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, TBonusSubtype(PrimarySkill::DEFENSE))); + BonusList defence = *stack->getBonuses(Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE))); int oldDefenceValue = defence.totalValue(); defence.push_back(std::make_shared(defenseBonusToAdd)); @@ -263,7 +263,7 @@ bool BattleActionProcessor::doAttackAction(const CBattleInfoCallback & battle, c const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { - totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, TBonusSubtype(stack->creatureId())); + totalAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } const bool firstStrike = destinationStack->hasBonusOfType(BonusType::FIRST_STRIKE); @@ -355,7 +355,7 @@ bool BattleActionProcessor::doShootAction(const CBattleInfoCallback & battle, co const auto * attackingHero = battle.battleGetFightingHero(ba.side); if(attackingHero) { - totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, TBonusSubtype(stack->creatureId())); + totalRangedAttacks += attackingHero->valOfBonuses(BonusType::HERO_GRANTS_ATTACKS, BonusSubtypeID(stack->creatureId())); } for(int i = 1; i < totalRangedAttacks; ++i) @@ -382,7 +382,7 @@ bool BattleActionProcessor::doCatapultAction(const CBattleInfoCallback & battle, return false; std::shared_ptr catapultAbility = stack->getBonusLocalFirst(Selector::type()(BonusType::CATAPULT)); - if(!catapultAbility || catapultAbility->subtype == TBonusSubtype()) + if(!catapultAbility || catapultAbility->subtype == BonusSubtypeID()) { gameHandler->complain("We do not know how to shoot :P"); } @@ -407,7 +407,7 @@ bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle return false; std::shared_ptr randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER)); - std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, TBonusSubtype(spellID))); + std::shared_ptr spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spellID))); //TODO special bonus for genies ability if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE) @@ -452,7 +452,7 @@ bool BattleActionProcessor::doHealAction(const CBattleInfoCallback & battle, con else destStack = battle.battleGetUnitByPos(target.at(0).hexValue); - if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == TBonusSubtype()) + if(stack == nullptr || destStack == nullptr || !healerAbility || healerAbility->subtype == BonusSubtypeID()) { gameHandler->complain("There is either no healer, no destination, or healer cannot heal :P"); } @@ -907,7 +907,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const const auto * owner = battle.battleGetFightingHero(attacker->unitSide()); if(owner) { - int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, TBonusSubtype(attacker->creatureId())); + int chance = owner->valOfBonuses(BonusType::BONUS_DAMAGE_CHANCE, BonusSubtypeID(attacker->creatureId())); if (chance > gameHandler->getRandomGenerator().nextInt(99)) bat.flags |= BattleAttack::BALLISTA_DOUBLE_DMG; } @@ -1095,7 +1095,7 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo return; } int32_t spellLevel = 0; - TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, TBonusSubtype(spellID))); + TConstBonusListPtr spellsByType = attacker->getBonuses(Selector::typeSubtype(attackMode, BonusSubtypeID(spellID))); for(const auto & sf : *spellsByType) { int meleeRanged; @@ -1113,7 +1113,7 @@ void BattleActionProcessor::attackCasting(const CBattleInfoCallback & battle, bo if (meleeRanged == 0 || (meleeRanged == 1 && ranged) || (meleeRanged == 2 && !ranged)) castMe = true; } - int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, TBonusSubtype(spellID)))); + int chance = attacker->valOfBonuses((Selector::typeSubtype(attackMode, BonusSubtypeID(spellID)))); vstd::amin(chance, 100); const CSpell * spell = SpellID(spellID).toSpell(); @@ -1168,7 +1168,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & // each gorgon have 10% chance to kill (counted separately in H3) -> binomial distribution //original formula x = min(x, (gorgons_count + 9)/10); - double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypeID::deathStareGorgon) / 100.0f; + double chanceToKill = attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareGorgon) / 100.0f; vstd::amin(chanceToKill, 1); //cap at 100% std::binomial_distribution<> distribution(attacker->getCount(), chanceToKill); @@ -1179,7 +1179,7 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & int maxToKill = static_cast((attacker->getCount() + cap - 1) / cap); //not much more than chance * count vstd::amin(staredCreatures, maxToKill); - staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusSubtypeID::deathStareCommander)) / defender->level(); + staredCreatures += (attacker->level() * attacker->valOfBonuses(BonusType::DEATH_STARE, BonusCustomSubtype::deathStareCommander)) / defender->level(); if(staredCreatures) { //TODO: death stare was not originally available for multiple-hex attacks, but... @@ -1249,9 +1249,9 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & else resurrectInfo.type = attacker->creatureId(); - if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypeID::transmutationPerHealth)) + if(attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusCustomSubtype::transmutationPerHealth)) resurrectInfo.count = std::max((defender->getCount() * defender->getMaxHealth()) / resurrectInfo.type.toCreature()->getMaxHealth(), 1u); - else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusSubtypeID::transmutationPerUnit)) + else if (attacker->hasBonusOfType((BonusType::TRANSMUTATION), BonusCustomSubtype::transmutationPerUnit)) resurrectInfo.count = defender->getCount(); else return; //wrong subtype @@ -1273,21 +1273,21 @@ void BattleActionProcessor::handleAfterAttackCasting(const CBattleInfoCallback & gameHandler->sendAndApply(&fakeEvent); } - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount)) + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage) || attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount)) { double chanceToTrigger = 0; int amountToDie = 0; - if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage)) //killing by percentage + if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage)) //killing by percentage { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillPercentage) / 100.0f; - int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypeID::destructionKillPercentage)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillPercentage) / 100.0f; + int percentageToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusCustomSubtype::destructionKillPercentage)))->additionalInfo[0]; amountToDie = static_cast(defender->getCount() * percentageToDie * 0.01f); } - else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount)) //killing by count + else if(attacker->hasBonusOfType(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount)) //killing by count { - chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusSubtypeID::destructionKillAmount) / 100.0f; - amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusSubtypeID::destructionKillAmount)))->additionalInfo[0]; + chanceToTrigger = attacker->valOfBonuses(BonusType::DESTRUCTION, BonusCustomSubtype::destructionKillAmount) / 100.0f; + amountToDie = attacker->getBonus(Selector::type()(BonusType::DESTRUCTION).And(Selector::subtype()(BonusCustomSubtype::destructionKillAmount)))->additionalInfo[0]; } vstd::amin(chanceToTrigger, 1); //cap trigger chance at 100% @@ -1348,12 +1348,12 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba { //we can have two bonuses - one with subtype 0 and another with subtype 1 //try to use permanent first, use only one of two - for(const auto & subtype : { BonusSubtypeID::soulStealBattle, BonusSubtypeID::soulStealPermanent}) + for(const auto & subtype : { BonusCustomSubtype::soulStealBattle, BonusCustomSubtype::soulStealPermanent}) { if(attackerState->hasBonusOfType(BonusType::SOUL_STEAL, subtype)) { int64_t toHeal = bsa.killedAmount * attackerState->valOfBonuses(BonusType::SOUL_STEAL, subtype) * attackerState->getMaxHealth(); - bool permanent = subtype == BonusSubtypeID::soulStealPermanent; + bool permanent = subtype == BonusCustomSubtype::soulStealPermanent; attackerState->heal(toHeal, EHealLevel::OVERHEAL, (permanent ? EHealPower::PERMANENT : EHealPower::ONE_BATTLE)); drainedLife += toHeal; break; @@ -1366,9 +1366,9 @@ int64_t BattleActionProcessor::applyBattleEffects(const CBattleInfoCallback & ba if(!bat.shot() && !def->isClone() && def->hasBonusOfType(BonusType::FIRE_SHIELD) && - !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, TBonusSubtype(SpellSchool::FIRE)) && - !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, TBonusSubtype(SpellSchool::FIRE)) && - attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, TBonusSubtype(SpellSchool::FIRE)) < 100 && + !attackerState->hasBonusOfType(BonusType::SPELL_SCHOOL_IMMUNITY, BonusSubtypeID(SpellSchool::FIRE)) && + !attackerState->hasBonusOfType(BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSubtypeID(SpellSchool::FIRE)) && + attackerState->valOfBonuses(BonusType::SPELL_DAMAGE_REDUCTION, BonusSubtypeID(SpellSchool::FIRE)) < 100 && CStack::isMeleeAttackPossible(attackerState.get(), def) // attacked needs to be adjacent to defender for fire shield to trigger (e.g. Dragon Breath attack) ) { diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index de88f2222..501408660 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -383,7 +383,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat const CreatureID stackCreatureId = next->unitType()->getId(); if ((stackCreatureId == CreatureID::ARROW_TOWERS || stackCreatureId == CreatureID::BALLISTA) - && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(stackCreatureId)))) + && (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(stackCreatureId)))) { BattleAction attack; attack.actionType = EActionType::SHOOT; @@ -428,7 +428,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat return true; } - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(CreatureID(CreatureID::CATAPULT)))) + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(CreatureID(CreatureID::CATAPULT)))) { BattleAction attack; attack.actionType = EActionType::CATAPULT; @@ -453,7 +453,7 @@ bool BattleFlowProcessor::tryMakeAutomaticAction(const CBattleInfoCallback & bat return true; } - if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, TBonusSubtype(CreatureID(CreatureID::FIRST_AID_TENT)))) + if (!curOwner || gameHandler->getRandomGenerator().nextInt(99) >= curOwner->valOfBonuses(BonusType::MANUAL_CONTROL, BonusSubtypeID(CreatureID(CreatureID::FIRST_AID_TENT)))) { RandomGeneratorUtil::randomShuffle(possibleStacks, gameHandler->getRandomGenerator()); const CStack * toBeHealed = possibleStacks.front(); @@ -663,7 +663,7 @@ void BattleFlowProcessor::stackTurnTrigger(const CBattleInfoCallback & battle, c if (st->hasBonusOfType(BonusType::POISON)) { - std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, TBonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); + std::shared_ptr b = st->getBonusLocalFirst(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(SpellID(SpellID::POISON))).And(Selector::type()(BonusType::STACK_HEALTH))); if (b) //TODO: what if not?... { bte.val = std::max (b->val - 10, -(st->valOfBonuses(BonusType::POISON))); diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index b9b5c1382..74aeb36d7 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -140,11 +140,11 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns ///Give all spells with bonus (to allow banned spells) GiveBonus giveBonus(GiveBonus::ETarget::HERO); giveBonus.id = hero->id.getNum(); - giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, TBonusSourceID()); + giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, BonusSourceID()); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) { - giveBonus.bonus.subtype = BonusSubtypeID::spellLevel(level); + giveBonus.bonus.subtype = BonusCustomSubtype::spellLevel(level); gameHandler->sendAndApply(&giveBonus); } diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index 5ff9d891c..391401b05 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -49,7 +49,7 @@ public: void makeWarMachine() { - addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); + addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SIEGE_WEAPON, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); } void redirectBonusesToFake() @@ -331,7 +331,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToSelf) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -362,7 +362,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -382,7 +382,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedAlly) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -397,11 +397,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedAlly) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -433,7 +433,7 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToNormalEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); @@ -453,7 +453,7 @@ TEST_F(BattleMatchOwnerTest, normalToHypnotizedEnemy) EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); @@ -468,11 +468,11 @@ TEST_F(BattleMatchOwnerTest, hypnotizedToHypnotizedEnemy) { UnitFake & unit1 = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(unit1, unitId()).WillRepeatedly(Return(42)); - unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit1.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); UnitFake & unit2 = unitsFake.add(BattleSide::DEFENDER); EXPECT_CALL(unit2, unitId()).WillRepeatedly(Return(4242)); - unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, TBonusSourceID())); + unit2.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::HYPNOTIZED, BonusSource::CREATURE_ABILITY, 0, BonusSourceID())); setDefaultExpectations(); diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index fd74e4171..6ea80a7d9 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -33,7 +33,7 @@ public: EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, UNIT_HEALTH, BonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(UNIT_AMOUNT)); } @@ -239,7 +239,7 @@ TEST_F(HealthTest, singleUnitStack) EXPECT_CALL(mock, getAllBonuses(_, _, _, _)).WillRepeatedly(Invoke(&bonusMock, &BonusBearerMock::getAllBonuses)); EXPECT_CALL(mock, getTreeVersion()).WillRepeatedly(Return(1)); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 300, BonusSourceID())); EXPECT_CALL(mock, unitBaseAmount()).WillRepeatedly(Return(1)); diff --git a/test/battle/CUnitStateMagicTest.cpp b/test/battle/CUnitStateMagicTest.cpp index b3ce7358b..9926548ae 100644 --- a/test/battle/CUnitStateMagicTest.cpp +++ b/test/battle/CUnitStateMagicTest.cpp @@ -55,7 +55,7 @@ public: void makeNormalCaster() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, TBonusSourceID(), TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPELLCASTER, BonusSource::CREATURE_ABILITY, DEFAULT_SCHOOL_LEVEL, BonusSourceID(), BonusSubtypeID(SpellID(DEFAULT_SPELL_INDEX)))); } }; @@ -63,7 +63,7 @@ TEST_F(UnitStateMagicTest, initialNormal) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 567, BonusSourceID())); initUnit(); @@ -125,7 +125,7 @@ TEST_F(UnitStateMagicTest, effectPower) const int32_t EFFECT_POWER = 12 * 100; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_POWER, BonusSourceID())); makeNormalCaster(); EXPECT_EQ(subject.getEffectPower(&spellMock), 12 * DEFAULT_AMOUNT); @@ -148,7 +148,7 @@ TEST_F(UnitStateMagicTest, enchantPower) const int32_t ENCHANT_POWER = 42; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_ENCHANT_POWER, BonusSource::CREATURE_ABILITY, ENCHANT_POWER, BonusSourceID())); makeNormalCaster(); @@ -171,7 +171,7 @@ TEST_F(UnitStateMagicTest, effectValue) const int32_t EFFECT_VALUE = 456; - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, TBonusSourceID(), TBonusSubtype(SpellID(DEFAULT_SPELL_INDEX)))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SPECIFIC_SPELL_POWER, BonusSource::CREATURE_ABILITY, EFFECT_VALUE, BonusSourceID(), BonusSubtypeID(SpellID(DEFAULT_SPELL_INDEX)))); makeNormalCaster(); EXPECT_EQ(subject.getEffectValue(&spellMock), EFFECT_VALUE * DEFAULT_AMOUNT); @@ -201,7 +201,7 @@ TEST_F(UnitStateMagicTest, spendMana) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::CASTS, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); initUnit(); diff --git a/test/battle/CUnitStateTest.cpp b/test/battle/CUnitStateTest.cpp index 0486efcc8..b4eb111c6 100644 --- a/test/battle/CUnitStateTest.cpp +++ b/test/battle/CUnitStateTest.cpp @@ -51,12 +51,12 @@ public: void setDefaultExpectations() { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACKS_SPEED, BonusSource::CREATURE_ABILITY, DEFAULT_SPEED, BonusSourceID())); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, TBonusSourceID(), TBonusSubtype(PrimarySkill::ATTACK))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, TBonusSourceID(), TBonusSubtype(PrimarySkill::DEFENSE))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_ATTACK, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CREATURE_ABILITY, DEFAULT_DEFENCE, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, DEFAULT_HP, BonusSourceID())); EXPECT_CALL(infoMock, unitBaseAmount()).WillRepeatedly(Return(DEFAULT_AMOUNT)); EXPECT_CALL(infoMock, unitType()).WillRepeatedly(Return(pikeman)); @@ -66,8 +66,8 @@ public: void makeShooter(int32_t ammo) { - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, TBonusSourceID())); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOOTER, BonusSource::CREATURE_ABILITY, 1, BonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::SHOTS, BonusSource::CREATURE_ABILITY, ammo, BonusSourceID())); } void initUnit() @@ -179,7 +179,7 @@ TEST_F(UnitStateTest, attackWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, BonusSourceID())); int expectedAttack = static_cast(DEFAULT_ATTACK + 0.5 * DEFAULT_DEFENCE); @@ -191,7 +191,7 @@ TEST_F(UnitStateTest, defenceWithFrenzy) { setDefaultExpectations(); - bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, TBonusSourceID())); + bonusMock.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::IN_FRENZY, BonusSource::SPELL_EFFECT, 50, BonusSourceID())); int expectedDefence = 0; @@ -204,7 +204,7 @@ TEST_F(UnitStateTest, additionalAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonusMock.addNewBonus(bonus); } @@ -218,7 +218,7 @@ TEST_F(UnitStateTest, additionalMeleeAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_MELEE_FIGHT; bonusMock.addNewBonus(bonus); @@ -233,7 +233,7 @@ TEST_F(UnitStateTest, additionalRangedAttack) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::ADDITIONAL_ATTACK, BonusSource::SPELL_EFFECT, 41, BonusSourceID()); bonus->effectRange = BonusLimitEffect::ONLY_DISTANCE_FIGHT; bonusMock.addNewBonus(bonus); @@ -248,10 +248,10 @@ TEST_F(UnitStateTest, getMinDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID(), BonusSubtypeID::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, BonusSourceID(), BonusCustomSubtype::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID(), BonusSubtypeID::creatureDamageMin); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMin); bonusMock.addNewBonus(bonus); } @@ -264,10 +264,10 @@ TEST_F(UnitStateTest, getMaxDamage) setDefaultExpectations(); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, TBonusSourceID(), BonusSubtypeID::creatureDamageBoth); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, 30, BonusSourceID(), BonusCustomSubtype::creatureDamageBoth); bonusMock.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, TBonusSourceID(), BonusSubtypeID::creatureDamageMax); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::CREATURE_DAMAGE, BonusSource::SPELL_EFFECT, -20, BonusSourceID(), BonusCustomSubtype::creatureDamageMax); bonusMock.addNewBonus(bonus); } diff --git a/test/entity/CCreatureTest.cpp b/test/entity/CCreatureTest.cpp index ed5901a57..81befa660 100644 --- a/test/entity/CCreatureTest.cpp +++ b/test/entity/CCreatureTest.cpp @@ -107,7 +107,7 @@ TEST_F(CCreatureTest, JsonAddBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); JsonNode & toAdd = data["bonuses"]["toAdd"]; @@ -133,10 +133,10 @@ TEST_F(CCreatureTest, JsonRemoveBonus) { JsonNode data(JsonNode::JsonType::DATA_STRUCT); - std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b1 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 17, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b1); - std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, TBonusSourceID(CreatureID(42)), TBonusSubtype(CreatureID(43)), BonusValueType::BASE_NUMBER); + std::shared_ptr b2 = std::make_shared(BonusDuration::PERMANENT, BonusType::BLOCKS_RETALIATION, BonusSource::CREATURE_ABILITY, 18, BonusSourceID(CreatureID(42)), BonusSubtypeID(CreatureID(43)), BonusValueType::BASE_NUMBER); subject->addNewBonus(b2); diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index 663e3895c..1172c4ca3 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -56,7 +56,7 @@ TEST_F(AbilityCasterTest, MagicAbilityAffectedByGenericBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID(), TBonusSubtype(SpellSchool::ANY))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, BonusSourceID(), BonusSubtypeID(SpellSchool::ANY))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); @@ -70,7 +70,7 @@ TEST_F(AbilityCasterTest, MagicAbilityIngoresSchoolBonus) { EXPECT_CALL(spellMock, getLevel()).WillRepeatedly(Return(1)); - casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); + casterBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::MAGIC_SCHOOL_SKILL, BonusSource::OTHER, 2, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_CALL(actualCaster, getAllBonuses(_, _, _, _)).Times(AtLeast(1)); EXPECT_CALL(actualCaster, getTreeVersion()).Times(AtLeast(0)); diff --git a/test/spells/effects/CloneTest.cpp b/test/spells/effects/CloneTest.cpp index acb7e0736..a9599ea44 100644 --- a/test/spells/effects/CloneTest.cpp +++ b/test/spells/effects/CloneTest.cpp @@ -138,7 +138,7 @@ public: EXPECT_EQ(marker.duration, BonusDuration::N_TURNS); EXPECT_EQ(marker.turnsRemain, effectDuration); EXPECT_EQ(marker.source, BonusSource::SPELL_EFFECT); - EXPECT_EQ(marker.sid, TBonusSourceID(SpellID(SpellID::CLONE))); + EXPECT_EQ(marker.sid, BonusSourceID(SpellID(SpellID::CLONE))); } void setDefaultExpectations() diff --git a/test/spells/effects/DamageTest.cpp b/test/spells/effects/DamageTest.cpp index e35766ce2..11a998a6c 100644 --- a/test/spells/effects/DamageTest.cpp +++ b/test/spells/effects/DamageTest.cpp @@ -95,7 +95,7 @@ TEST_F(DamageApplyTest, DoesDamageToAliveUnit) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); @@ -157,7 +157,7 @@ TEST_F(DamageApplyTest, DoesDamageByPercent) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, getCount()).WillOnce(Return(unitAmount)); @@ -202,7 +202,7 @@ TEST_F(DamageApplyTest, DoesDamageByCount) const uint32_t unitId = 42; auto & targetUnit = unitsFake.add(BattleSide::ATTACKER); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitBaseAmount()).WillRepeatedly(Return(unitAmount)); EXPECT_CALL(targetUnit, alive()).WillRepeatedly(Return(true)); diff --git a/test/spells/effects/DispelTest.cpp b/test/spells/effects/DispelTest.cpp index 050517402..10ac75f0f 100644 --- a/test/spells/effects/DispelTest.cpp +++ b/test/spells/effects/DispelTest.cpp @@ -74,7 +74,7 @@ TEST_F(DispelTest, ApplicableToAliveUnitWithTimedEffect) auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(SpellID(negativeID)))); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(SpellID(negativeID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).WillOnce(Return(true)); @@ -101,7 +101,7 @@ TEST_F(DispelTest, IgnoresOwnEffects) } auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(SpellID(neutralID)))); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(SpellID(neutralID)))); EXPECT_CALL(unit, isValidTarget(Eq(false))).Times(AtMost(1)).WillRepeatedly(Return(true)); @@ -182,23 +182,23 @@ TEST_F(DispelApplyTest, RemovesEffects) EXPECT_CALL(unit1, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(unitIds[1])); { - auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(negativeID)); + auto bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, TBonusSourceID(negativeID)); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 1, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(negativeID)); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, BonusSourceID(negativeID)); expectedBonus[0].emplace_back(*bonus); unit0.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, TBonusSourceID(positiveID)); + bonus = std::make_shared(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::SPELL_EFFECT, 5, BonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); - bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(positiveID)); + bonus = std::make_shared(BonusDuration::N_TURNS, BonusType::GENERAL_ATTACK_REDUCTION, BonusSource::SPELL_EFFECT, 3, BonusSourceID(positiveID)); expectedBonus[1].emplace_back(*bonus); unit1.addNewBonus(bonus); } diff --git a/test/spells/effects/HealTest.cpp b/test/spells/effects/HealTest.cpp index a652ae831..24da47673 100644 --- a/test/spells/effects/HealTest.cpp +++ b/test/spells/effects/HealTest.cpp @@ -91,7 +91,7 @@ TEST_F(HealTest, ApplicableIfActuallyResurrects) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(1000)); EXPECT_CALL(mechanicsMock, isSmart()).WillOnce(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -117,7 +117,7 @@ TEST_F(HealTest, NotApplicableIfNotEnoughCasualties) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -143,7 +143,7 @@ TEST_F(HealTest, NotApplicableIfResurrectsLessThanRequired) EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(999)); EXPECT_CALL(mechanicsMock, isSmart()).WillRepeatedly(Return(false)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); EffectTarget target; @@ -271,7 +271,7 @@ TEST_F(HealTest, NotApplicableIfEffectValueTooLow) EXPECT_CALL(unit, getTotalHealth()).WillOnce(Return(200)); EXPECT_CALL(unit, getAvailableHealth()).WillOnce(Return(100)); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, TBonusSourceID())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 200, BonusSourceID())); EXPECT_CALL(mechanicsMock, getEffectValue()).Times(AtLeast(1)).WillRepeatedly(Return(199)); @@ -348,7 +348,7 @@ TEST_P(HealApplyTest, Heals) EXPECT_CALL(targetUnit, unitId()).WillRepeatedly(Return(unitId)); EXPECT_CALL(targetUnit, unitType()).WillRepeatedly(Return(pikeman)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); unitsFake.setDefaultBonusExpectations(); diff --git a/test/spells/effects/SacrificeTest.cpp b/test/spells/effects/SacrificeTest.cpp index d53f32bbe..c0a3b5117 100644 --- a/test/spells/effects/SacrificeTest.cpp +++ b/test/spells/effects/SacrificeTest.cpp @@ -179,13 +179,13 @@ TEST_F(SacrificeApplyTest, ResurrectsTarget) EXPECT_CALL(mechanicsMock, applySpellBonus(_, Eq(&targetUnit))).WillOnce(ReturnArg<0>()); EXPECT_CALL(mechanicsMock, calculateRawEffectValue(_,_)).WillOnce(Return(effectValue)); - targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, TBonusSourceID())); + targetUnit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHP, BonusSourceID())); auto & victim = unitsFake.add(BattleSide::ATTACKER); EXPECT_CALL(victim, unitId()).Times(AtLeast(1)).WillRepeatedly(Return(victimId)); EXPECT_CALL(victim, getCount()).Times(AtLeast(1)).WillRepeatedly(Return(victimCount)); - victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, TBonusSourceID())); + victim.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, victimUnitHP, BonusSourceID())); EXPECT_CALL(*battleFake, setUnitState(Eq(unitId), _, Eq(expectedHealValue))).Times(1); diff --git a/test/spells/effects/SummonTest.cpp b/test/spells/effects/SummonTest.cpp index c835692a7..33e4cde61 100644 --- a/test/spells/effects/SummonTest.cpp +++ b/test/spells/effects/SummonTest.cpp @@ -244,12 +244,12 @@ TEST_P(SummonApplyTest, UpdatesOldUnit) setDefaultExpectaions(); acquired = std::make_shared(); - acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID())); + acquired->addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, BonusSourceID())); acquired->redirectBonusesToFake(); acquired->expectAnyBonusSystemCall(); auto & unit = unitsFake.add(BattleSide::ATTACKER); - unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, TBonusSourceID())); + unit.addNewBonus(std::make_shared(BonusDuration::PERMANENT, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, unitHealth, BonusSourceID())); { EXPECT_CALL(unit, acquire()).WillOnce(Return(acquired)); diff --git a/test/spells/effects/TimedTest.cpp b/test/spells/effects/TimedTest.cpp index 0bfebd03a..f399eb81e 100644 --- a/test/spells/effects/TimedTest.cpp +++ b/test/spells/effects/TimedTest.cpp @@ -71,9 +71,9 @@ protected: TEST_P(TimedApplyTest, ChangesBonuses) { - Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID(), TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus1(BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); - Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, TBonusSourceID(), TBonusSubtype(PrimarySkill::KNOWLEDGE)); + Bonus testBonus2(BonusDuration::N_TURNS, BonusType::PRIMARY_SKILL, BonusSource::OTHER, 3, BonusSourceID(), BonusSubtypeID(PrimarySkill::KNOWLEDGE)); testBonus2.turnsRemain = 4; JsonNode options(JsonNode::JsonType::DATA_STRUCT); @@ -103,7 +103,7 @@ TEST_P(TimedApplyTest, ChangesBonuses) for(auto & bonus : expectedBonus) { bonus.source = BonusSource::SPELL_EFFECT; - bonus.sid = TBonusSourceID(SpellID(spellIndex)); + bonus.sid = BonusSourceID(SpellID(spellIndex)); } if(cumulative) diff --git a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp index f75303aea..ae2641c6e 100644 --- a/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteLevelConditionTest.cpp @@ -54,7 +54,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -67,7 +67,7 @@ TEST_F(AbsoluteLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -79,7 +79,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID()); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -90,7 +90,7 @@ TEST_F(AbsoluteLevelConditionTest, ImmuneNormalSpell) TEST_F(AbsoluteLevelConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID()); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID()); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp index 8b260437b..1e062eca6 100644 --- a/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp +++ b/test/spells/targetConditions/AbsoluteSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(AbsoluteSpellConditionTest, ChecksAbsoluteCase) TEST_P(AbsoluteSpellConditionTest, IgnoresNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/BonusConditionTest.cpp b/test/spells/targetConditions/BonusConditionTest.cpp index 87ce6b655..4e18d1c62 100644 --- a/test/spells/targetConditions/BonusConditionTest.cpp +++ b/test/spells/targetConditions/BonusConditionTest.cpp @@ -42,14 +42,14 @@ TEST_F(BonusConditionTest, ImmuneByDefault) TEST_F(BonusConditionTest, ReceptiveIfMatchesType) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_DAMAGE_REDUCTION, BonusSource::OTHER, 100, BonusSourceID())); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(BonusConditionTest, ImmuneIfTypeMismatch) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::FIRE))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::OTHER, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::FIRE))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 9d606a798..61df6eb5a 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(ElementalConditionTest, ReceptiveIfNoBonus) TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -64,7 +64,7 @@ TEST_P(ElementalConditionTest, ImmuneIfBonusMatches) TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::WATER))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::WATER))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -72,7 +72,7 @@ TEST_P(ElementalConditionTest, NotImmuneIfBonusMismatches) TEST_P(ElementalConditionTest, DependsOnPositivness) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_EQ(isPositive, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -80,8 +80,8 @@ TEST_P(ElementalConditionTest, DependsOnPositivness) TEST_P(ElementalConditionTest, ImmuneIfBothBonusesPresent) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, TBonusSourceID(), TBonusSubtype(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_SCHOOL_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATIVE_EFFECTS_IMMUNITY, BonusSource::SPELL_EFFECT, 0, BonusSourceID(), BonusSubtypeID(SpellSchool::AIR))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } diff --git a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp index a4ca85066..d6aa51b98 100644 --- a/test/spells/targetConditions/ImmunityNegationConditionTest.cpp +++ b/test/spells/targetConditions/ImmunityNegationConditionTest.cpp @@ -57,7 +57,7 @@ TEST_P(ImmunityNegationConditionTest, WithHeroNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID(), BonusSubtypeID::immunityEnemyHero)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, BonusSourceID(), BonusCustomSubtype::immunityEnemyHero)); EXPECT_EQ(isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -66,7 +66,7 @@ TEST_P(ImmunityNegationConditionTest, WithBattleWideNegation) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, TBonusSourceID(), BonusSubtypeID::immunityBattleWide)); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::NEGATE_ALL_NATURAL_IMMUNITIES, BonusSource::OTHER, 0, BonusSourceID(), BonusCustomSubtype::immunityBattleWide)); //This should return if ownerMatches, because anyone should cast onto owner's stacks, but not on enemyStacks EXPECT_EQ(ownerMatches && isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalLevelConditionTest.cpp b/test/spells/targetConditions/NormalLevelConditionTest.cpp index 6d74de6ac..e6895e232 100644 --- a/test/spells/targetConditions/NormalLevelConditionTest.cpp +++ b/test/spells/targetConditions/NormalLevelConditionTest.cpp @@ -56,7 +56,7 @@ TEST_P(NormalLevelConditionTest, DefaultForNormal) TEST_P(NormalLevelConditionTest, ReceptiveNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 3, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(4)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -66,7 +66,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveNormal) TEST_P(NormalLevelConditionTest, ReceptiveAbility) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 5, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(0)); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); @@ -75,7 +75,7 @@ TEST_P(NormalLevelConditionTest, ReceptiveAbility) TEST_P(NormalLevelConditionTest, ImmuneNormal) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::LEVEL_SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID())); if(isMagicalEffect) EXPECT_CALL(mechanicsMock, getSpellLevel()).Times(AtLeast(1)).WillRepeatedly(Return(2)); EXPECT_EQ(!isMagicalEffect, subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/NormalSpellConditionTest.cpp b/test/spells/targetConditions/NormalSpellConditionTest.cpp index 264f409d1..3abf12ae9 100644 --- a/test/spells/targetConditions/NormalSpellConditionTest.cpp +++ b/test/spells/targetConditions/NormalSpellConditionTest.cpp @@ -43,7 +43,7 @@ public: TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); bonus->additionalInfo = 1; unitBonuses.addNewBonus(bonus); @@ -57,7 +57,7 @@ TEST_P(NormalSpellConditionTest, ChecksAbsoluteCase) TEST_P(NormalSpellConditionTest, ChecksNormalCase) { setDefaultExpectations(); - auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, TBonusSourceID(), TBonusSubtype(SpellID(immuneSpell))); + auto bonus = std::make_shared(BonusDuration::ONE_BATTLE, BonusType::SPELL_IMMUNITY, BonusSource::OTHER, 4, BonusSourceID(), BonusSubtypeID(SpellID(immuneSpell))); unitBonuses.addNewBonus(bonus); if(immuneSpell == castSpell) EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); diff --git a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp index fac906601..5aef4f202 100644 --- a/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp +++ b/test/spells/targetConditions/ReceptiveFeatureConditionTest.cpp @@ -31,7 +31,7 @@ public: EXPECT_CALL(unitMock, getTreeVersion()).Times(AtLeast(0)); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); if(hasBonus) - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::RECEPTIVE, BonusSource::OTHER, 0, BonusSourceID())); } protected: diff --git a/test/spells/targetConditions/SpellEffectConditionTest.cpp b/test/spells/targetConditions/SpellEffectConditionTest.cpp index f96af453f..af887798a 100644 --- a/test/spells/targetConditions/SpellEffectConditionTest.cpp +++ b/test/spells/targetConditions/SpellEffectConditionTest.cpp @@ -45,7 +45,7 @@ TEST_F(SpellEffectConditionTest, ImmuneByDefault) TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(SpellID(SpellID::AGE)))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, BonusSourceID(SpellID(SpellID::AGE)))); EXPECT_TRUE(subject->isReceptive(&mechanicsMock, &unitMock)); } @@ -53,14 +53,14 @@ TEST_F(SpellEffectConditionTest, ReceptiveIfHasEffectFromDesiredSpell) TEST_F(SpellEffectConditionTest, ImmuneIfHasEffectFromOtherSpell) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, TBonusSourceID(SpellID(SpellID::AIR_SHIELD)))); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::SPELL_EFFECT, 3, BonusSourceID(SpellID(SpellID::AIR_SHIELD)))); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } TEST_F(SpellEffectConditionTest, ImmuneIfHasNoSpellEffects) { setDefaultExpectations(); - unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, TBonusSourceID())); + unitBonuses.addNewBonus(std::make_shared(BonusDuration::ONE_BATTLE, BonusType::STACK_HEALTH, BonusSource::CREATURE_ABILITY, 3, BonusSourceID())); EXPECT_FALSE(subject->isReceptive(&mechanicsMock, &unitMock)); } From 4f47555977f697cff6ad935a2fcf878569d83375 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 15:06:18 +0300 Subject: [PATCH 0913/1248] Split OBJECT bonus source into OBJECT_TYPE and OBJECT_INSTANCE --- AI/Nullkiller/Analyzers/ArmyManager.cpp | 2 +- lib/JsonNode.cpp | 10 +++++++++- lib/NetPacksLib.cpp | 2 +- lib/bonuses/BonusEnum.h | 3 ++- lib/mapObjectConstructors/CRewardableConstructor.cpp | 2 +- lib/mapObjects/CBank.cpp | 4 ++-- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 4 ++-- lib/mapObjects/CGTownBuilding.cpp | 4 ++-- lib/mapObjects/CQuest.cpp | 4 ++-- lib/mapObjects/CRewardableObject.cpp | 4 ++-- lib/mapObjects/MiscObjects.cpp | 8 ++++---- lib/mapping/MapFormatH3M.cpp | 8 ++++---- mapeditor/inspector/rewardswidget.cpp | 2 +- 14 files changed, 34 insertions(+), 25 deletions(-) diff --git a/AI/Nullkiller/Analyzers/ArmyManager.cpp b/AI/Nullkiller/Analyzers/ArmyManager.cpp index 66a86592d..e0eb6333b 100644 --- a/AI/Nullkiller/Analyzers/ArmyManager.cpp +++ b/AI/Nullkiller/Analyzers/ArmyManager.cpp @@ -153,7 +153,7 @@ std::vector ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, for(auto bonus : *bonusModifiers) { // army bonuses will change and object bonuses are temporary - if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT) + if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE) { newArmyInstance.addNewBonus(std::make_shared(*bonus)); } diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index c312cdba7..b25116670 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -583,7 +583,15 @@ static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource }); break; } - case BonusSource::OBJECT: + case BonusSource::OBJECT_TYPE: + { + VLC->identifiers()->requestIdentifier( "object", node, [&sourceInstance](int32_t identifier) + { + sourceInstance = Obj(identifier); + }); + break; + } + case BonusSource::OBJECT_INSTANCE: case BonusSource::HERO_BASE_SKILL: assert(0); // TODO sourceInstance = ObjectInstanceID(); diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 12f89f7e7..cf04905a8 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -993,7 +993,7 @@ void GiveBonus::applyGs(CGameState *gs) if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) { - if (bonus.source == BonusSource::OBJECT) + if (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE) { descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" } diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 2118eaeec..06cdc8f96 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -175,7 +175,8 @@ class JsonNode; #define BONUS_SOURCE_LIST \ BONUS_SOURCE(ARTIFACT)\ BONUS_SOURCE(ARTIFACT_INSTANCE)\ - BONUS_SOURCE(OBJECT)\ + BONUS_SOURCE(OBJECT_TYPE)\ + BONUS_SOURCE(OBJECT_INSTANCE)\ BONUS_SOURCE(CREATURE_ABILITY)\ BONUS_SOURCE(TERRAIN_NATIVE)\ BONUS_SOURCE(TERRAIN_OVERLAY)\ diff --git a/lib/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 1726b7beb..c8b802586 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -49,7 +49,7 @@ void CRewardableConstructor::configureObject(CGObjectInstance * object, CRandomG { for (auto & bonus : rewardInfo.reward.bonuses) { - bonus.source = BonusSource::OBJECT; + bonus.source = BonusSource::OBJECT_TYPE; bonus.sid = BonusSourceID(rewardableObject->ID); } } diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 7e372b4cf..8b26f3655 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -212,7 +212,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const GiveBonus gbonus; gbonus.id = hero->id.getNum(); gbonus.bonus.duration = BonusDuration::ONE_BATTLE; - gbonus.bonus.source = BonusSource::OBJECT; + gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; @@ -239,7 +239,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const case Obj::PYRAMID: { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); + gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); gb.id = hero->id.getNum(); cb->giveHeroBonus(&gb); textID = 107; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 7b95c108f..e4cda9ab0 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -221,7 +221,7 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura gbonus.bonus.type = BonusType::NONE; gbonus.id = heroID.getNum(); gbonus.bonus.duration = duration; - gbonus.bonus.source = BonusSource::OBJECT; + gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); cb->giveHeroBonus(&gbonus); } diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 808088c36..81035cbb1 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -227,11 +227,11 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) int val; handler.serializeInt("morale", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(id)); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); handler.serializeInt("luck", val, 0); if(val) - vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(id)); + vinfo.reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); vinfo.reward.resources.serializeJson(handler, "resources"); { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 0c3851d35..1590985e3 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -143,10 +143,10 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const switch (this->bType) { case BuildingSubID::STABLES: - if(!h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables + if(!h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(Obj(Obj::STABLES)))) //does not stack with advMap Stables { GiveBonus gb; - gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); + gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT_TYPE, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); gb.id = heroID.getNum(); cb->giveHeroBonus(&gb); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 09d98a063..d79549f04 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -676,9 +676,9 @@ void CGSeerHut::serializeJsonOptions(JsonSerializeFormat & handler) if(metaTypeName == "mana") reward.manaDiff = val; if(metaTypeName == "morale") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(id)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); if(metaTypeName == "luck") - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(id)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(id)); if(metaTypeName == "resource") { auto rawId = *VLC->identifiers()->getIdentifier(ModScope::scopeMap(), fullIdentifier, false); diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 8eac99996..341318dbf 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -197,7 +197,7 @@ bool CRewardableObject::wasVisitedBefore(const CGHeroInstance * contextHero) con case Rewardable::VISIT_PLAYER: return vstd::contains(cb->getPlayerState(contextHero->getOwner())->visitedObjects, ObjectInstanceID(id)); case Rewardable::VISIT_BONUS: - return contextHero->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID)); + return contextHero->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return contextHero->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: @@ -234,7 +234,7 @@ bool CRewardableObject::wasVisited(const CGHeroInstance * h) const switch (configuration.visitMode) { case Rewardable::VISIT_BONUS: - return h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID)); + return h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID)); case Rewardable::VISIT_HERO: return h->visitedObjects.count(ObjectInstanceID(id)); case Rewardable::VISIT_LIMITER: diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 0ae6b808e..c0c49975e 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -987,14 +987,14 @@ void CGSirens::initObj(CRandomGenerator & rand) std::string CGSirens::getHoverText(const CGHeroInstance * hero) const { - return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID))); + return getObjectName() + " " + visitedTxt(hero->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID))); } void CGSirens::onHeroVisit( const CGHeroInstance * h ) const { InfoWindow iw; iw.player = h->tempOwner; - if(h->hasBonusFrom(BonusSource::OBJECT, BonusSourceID(ID))) //has already visited Sirens + if(h->hasBonusFrom(BonusSource::OBJECT_TYPE, BonusSourceID(ID))) //has already visited Sirens { iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::ADVOB_TXT,133); @@ -1179,7 +1179,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const { RemoveBonus rb(GiveBonus::ETarget::PLAYER); rb.whoID = oldOwner.getNum(); - rb.source = BonusSource::OBJECT; + rb.source = BonusSource::OBJECT_INSTANCE; rb.id = BonusSourceID(id); cb->sendAndApply(&rb); } @@ -1202,7 +1202,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const gb.bonus.val = 500; gb.id = player.getNum(); gb.bonus.duration = BonusDuration::PERMANENT; - gb.bonus.source = BonusSource::OBJECT; + gb.bonus.source = BonusSource::OBJECT_INSTANCE; gb.bonus.sid = BonusSourceID(id); gb.bonus.subtype = BonusCustomSubtype::heroMovementSea; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 750ea4dcb..49f12ebdd 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1040,9 +1040,9 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, val, BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); if(auto val = reader->readUInt8()) - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, val, BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(idToBeGiven)); reader->readResourses(reward.resources); for(int x = 0; x < GameConstants::PRIMARY_SKILLS; ++x) @@ -2008,12 +2008,12 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con } case ESeerHutRewardType::MORALE: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::MORALE, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::LUCK: { - reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT, reader->readUInt8(), BonusSourceID(idToBeGiven)); + reward.bonuses.emplace_back(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, reader->readUInt8(), BonusSourceID(idToBeGiven)); break; } case ESeerHutRewardType::RESOURCES: diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 661dbf0fd..6ec62d84b 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -333,7 +333,7 @@ void RewardsWidget::saveCurrentVisitInfo(int index) auto dur = bonusDurationMap.at(ui->bonuses->item(i, 0)->text().toStdString()); auto typ = bonusNameMap.at(ui->bonuses->item(i, 1)->text().toStdString()); auto val = ui->bonuses->item(i, 2)->data(Qt::UserRole).toInt(); - vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT, val, BonusSourceID(object.id)); + vinfo.reward.bonuses.emplace_back(dur, typ, BonusSource::OBJECT_INSTANCE, val, BonusSourceID(object.id)); } vinfo.limiter.dayOfWeek = ui->lDayOfWeek->currentIndex(); From 8df39c43a070b292381ac09c4e820dcf43aceaa1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 21 Oct 2023 19:01:15 +0300 Subject: [PATCH 0914/1248] Fix game loading --- lib/JsonNode.cpp | 6 ++---- lib/NetPacksLib.cpp | 2 +- lib/constants/EntityIdentifiers.cpp | 5 ----- lib/serializer/CSerializer.h | 4 ++-- 4 files changed, 5 insertions(+), 12 deletions(-) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index b25116670..9b95dd966 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -593,8 +593,7 @@ static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource } case BonusSource::OBJECT_INSTANCE: case BonusSource::HERO_BASE_SKILL: - assert(0); // TODO - sourceInstance = ObjectInstanceID(); + sourceInstance = ObjectInstanceID(ObjectInstanceID::decode(node.String())); break; case BonusSource::CREATURE_ABILITY: { @@ -641,8 +640,7 @@ static void loadBonusSourceInstance(BonusSourceID & sourceInstance, BonusSource break; } case BonusSource::CAMPAIGN_BONUS: - assert(0); // TODO - sourceInstance = CampaignScenarioID(); + sourceInstance = CampaignScenarioID(CampaignScenarioID::decode(node.String())); break; case BonusSource::ARMY: case BonusSource::STACK_EXPERIENCE: diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index cf04905a8..c5a967069 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2553,7 +2553,7 @@ void PlayerCheated::applyGs(CGameState * gs) const void PlayerStartsTurn::applyGs(CGameState * gs) const { - assert(gs->actingPlayers.count(player) == 0); + //assert(gs->actingPlayers.count(player) == 0);//Legal - may happen after loading of deserialized map state gs->actingPlayers.insert(player); } diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index f9224d88a..2dcebd5e8 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -455,11 +455,6 @@ std::string GameResID::entityType() return "resource"; } -std::string GameResID::entityType() -{ - return "resource"; -} - std::string SecondarySkill::entityType() { return "secondarySkill"; diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index c49fb86fc..fa3dedefc 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 827; -const ui32 MINIMAL_SERIALIZATION_VERSION = 827; +const ui32 SERIALIZATION_VERSION = 828; +const ui32 MINIMAL_SERIALIZATION_VERSION = 828; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From 85f902fa26f6b10e77ac40b5aa80dac8e5a5748c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 17:12:34 +0300 Subject: [PATCH 0915/1248] Update docs --- docs/modders/Bonus/Bonus_Types.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index f3719613e..8d5a4c2a8 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -264,7 +264,7 @@ Allows affected heroes to cast specified spell, even if this spell is banned in Allows affected heroes to cast any spell of specified level. Does not grant spells banned in map options. -- subtype: spell level +- subtype: spell level, in form "spellLevelX" where X is desired level (1-5) ### SPELLS_OF_SCHOOL @@ -284,7 +284,7 @@ Affected heroes will add specified resources amounts to player treasure on new d Increases weekly growth of creatures in affected towns (Legion artifacts) - value: number of additional weekly creatures -- subtype: level of affected dwellings +- subtype: dwelling level, in form "creatureLevelX" where X is desired level (1-7) ### CREATURE_GROWTH_PERCENT From 9e84ddc0aa208cd363996eaded0fc4e906bea086 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 22 Oct 2023 15:58:21 +0000 Subject: [PATCH 0916/1248] Add CI stage to ensure LF line endings Based on @josch's command: https://github.com/vcmi/vcmi/pull/3078#issuecomment-1772150744 --- .github/workflows/github.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 76b7bd3c6..aa3604c5c 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -145,6 +145,14 @@ jobs: with: submodules: recursive + - name: Ensure LF line endings + if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} + run: | + find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./android -prune \ + -o -path ./osx -prune -o -path ./test/googletest -prune -o -type f \ + -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' \ + -not -name '*.wav' -not -name '*.ico' -print0 | ( ! xargs -0 grep -l -z -P '\r\n' ) + - name: Validate JSON # the Python yaml module doesn't seem to work on mac-arm # also, running it on multiple presets is redundant and slightly increases already long CI built times From 0ff423ff0e4fbdf99d8fd9eba5ecdd4884aa634b Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 22 Oct 2023 16:41:09 +0000 Subject: [PATCH 0917/1248] Change remaining CRLF line endings to LF find . -type f -name "*.JSON" -exec dos2unix {} \+; --- .../config/vcmi/rmg/hdmod/aroundamarsh.JSON | 376 ++--- Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON | 1064 ++++++------- Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON | 234 +-- Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON | 456 +++--- Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON | 366 ++--- .../config/vcmi/rmg/hdmod/frozenDragons.JSON | 510 +++--- .../config/vcmi/rmg/hdmod/gimlisRevenge.JSON | 204 +-- Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON | 664 ++++---- .../config/vcmi/rmg/hdmod/headquarters.JSON | 316 ++-- .../vcmi/config/vcmi/rmg/hdmod/hypercube.JSON | 460 +++--- Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON | 582 +++---- Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON | 1278 +++++++-------- .../config/vcmi/rmg/hdmod/miniNostalgia.JSON | 358 ++--- .../vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON | 584 +++---- .../config/vcmi/rmg/hdmod/oceansEleven.JSON | 298 ++-- Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON | 346 ++-- .../config/vcmi/rmg/hdmod/shaaafworld.JSON | 122 +- Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON | 466 +++--- Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON | 216 +-- Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON | 186 +-- Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON | 574 +++---- .../rmg/hdmodUnused/balance m+u 200%.JSON | 330 ++-- .../rmg/hdmodUnused/skirmish m-u 200%.JSON | 222 +-- .../vcmi/rmg/hdmodUnused/true random.JSON | 1416 ++++++++--------- .../vcmi/rmg/heroes3/dwarvenTunnels.JSON | 452 +++--- .../vcmi/rmg/heroes3/golemsAplenty.JSON | 218 +-- .../config/vcmi/rmg/heroes3/monksRetreat.JSON | 278 ++-- .../config/vcmi/rmg/heroes3/newcomers.JSON | 354 ++--- .../config/vcmi/rmg/heroes3/smallRing.JSON | 220 +-- .../config/vcmi/rmg/heroes3/southOfHell.JSON | 330 ++-- .../config/vcmi/rmg/heroes3unused/ring.JSON | 410 ++--- .../vcmi/rmg/heroes3unused/riseOfPhoenix.JSON | 388 ++--- .../vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON | 210 +-- .../vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON | 134 +- .../config/vcmi/rmg/symmetric/2sm2b(2).JSON | 174 +- .../vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON | 174 +- .../vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON | 192 +-- .../config/vcmi/rmg/symmetric/2sm2f(2).JSON | 172 +- .../vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON | 168 +- .../config/vcmi/rmg/symmetric/2sm2h(2).JSON | 160 +- .../vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON | 154 +- .../config/vcmi/rmg/symmetric/2sm2i(2).JSON | 150 +- .../vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON | 152 +- .../config/vcmi/rmg/symmetric/2sm4d(2).JSON | 268 ++-- .../config/vcmi/rmg/symmetric/2sm4d(3).JSON | 268 ++-- .../vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON | 268 ++-- .../vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON | 210 +-- .../vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON | 246 +-- .../vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON | 328 ++-- .../vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON | 240 +-- .../vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON | 214 +-- .../vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON | 222 +-- .../vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON | 300 ++-- .../vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON | 242 +-- .../vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON | 194 +-- .../vcmi/config/vcmi/rmg/symmetric/6lm10.JSON | 612 +++---- .../config/vcmi/rmg/symmetric/6lm10a.JSON | 634 ++++---- .../vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON | 264 +-- .../vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON | 170 +- .../vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON | 350 ++-- .../vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON | 210 +-- .../vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON | 202 +-- .../vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON | 256 +-- Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON | 614 +++---- .../vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON | 622 ++++---- .../vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON | 336 ++-- .../vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON | 302 ++-- .../vcmi/config/vcmi/rmg/symmetric/8xm12.JSON | 806 +++++----- .../config/vcmi/rmg/symmetric/8xm12a.JSON | 754 ++++----- Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON | 1030 ++++++------ .../config/vcmi/rmg/unknownUnused/2mm2h.JSON | 174 +- .../vcmi/rmg/unknownUnused/2x2sm4d(3).JSON | 470 +++--- .../config/vcmi/rmg/unknownUnused/4mm2h.JSON | 174 +- .../config/vcmi/rmg/unknownUnused/4sm3i.JSON | 232 +-- .../config/vcmi/rmg/unknownUnused/6lm10a.JSON | 616 +++---- .../vcmi/rmg/unknownUnused/8xm12 huge.JSON | 762 ++++----- .../vcmi/rmg/unknownUnused/8xm8 huge.JSON | 1030 ++++++------ .../config/vcmi/rmg/unknownUnused/cross.JSON | 376 ++--- .../config/vcmi/rmg/unknownUnused/cross2.JSON | 446 +++--- .../config/vcmi/rmg/unknownUnused/cross3.JSON | 680 ++++---- .../vcmi/rmg/unknownUnused/doubled 8mm6.JSON | 1072 ++++++------- .../config/vcmi/rmg/unknownUnused/elka.JSON | 336 ++-- .../vcmi/rmg/unknownUnused/greatSands.JSON | 642 ++++---- .../config/vcmi/rmg/unknownUnused/kite.JSON | 282 ++-- .../config/vcmi/rmg/unknownUnused/wheel.JSON | 308 ++-- 85 files changed, 16940 insertions(+), 16940 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON index 738cf04c2..f98ee579f 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/aroundamarsh.JSON @@ -1,188 +1,188 @@ -{ - "Around A Marsh" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 9500, "density" : 1 }, - { "min" : 3500, "max" : 5500, "density" : 3 }, - { "min" : 350, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["fortress", "necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 28000, "density" : 1 }, - { "min" : 6000, "max" : 9500, "density" : 7 }, - { "min" : 350, "max" : 2000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["fortress", "necropolis"], - "terrainTypeLikeZone" : 5, - "mines" : { "mercury" : 1, "crystal" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "swamp" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 120000, "density" : 6 }, - { "min" : 9000, "max" : 9500, "density" : 1 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "fortress" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "3", "b" : "7", "guard" : 5000 }, - { "a" : "4", "b" : "8", "guard" : 5000 }, - { "a" : "5", "b" : "6", "guard" : 25000 }, - { "a" : "6", "b" : "7", "guard" : 35000 }, - { "a" : "7", "b" : "8", "guard" : 25000 }, - { "a" : "5", "b" : "8", "guard" : 35000 }, - - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "11", "b" : "12", "guard" : 0 }, - { "a" : "11", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - { "a" : "9", "b" : "10", "guard" : 0 }, - - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "5", "b" : "9", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "7", "b" : "11", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 }, - { "a" : "8", "b" : "12", "guard" : 0 } - ] - } -} +{ + "Around A Marsh" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 9500, "density" : 1 }, + { "min" : 3500, "max" : 5500, "density" : 3 }, + { "min" : 350, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["fortress", "necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 28000, "density" : 1 }, + { "min" : 6000, "max" : 9500, "density" : 7 }, + { "min" : 350, "max" : 2000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["fortress", "necropolis"], + "terrainTypeLikeZone" : 5, + "mines" : { "mercury" : 1, "crystal" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "swamp" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 120000, "density" : 6 }, + { "min" : 9000, "max" : 9500, "density" : 1 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "fortress" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "3", "b" : "7", "guard" : 5000 }, + { "a" : "4", "b" : "8", "guard" : 5000 }, + { "a" : "5", "b" : "6", "guard" : 25000 }, + { "a" : "6", "b" : "7", "guard" : 35000 }, + { "a" : "7", "b" : "8", "guard" : 25000 }, + { "a" : "5", "b" : "8", "guard" : 35000 }, + + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "11", "b" : "12", "guard" : 0 }, + { "a" : "11", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + { "a" : "9", "b" : "10", "guard" : 0 }, + + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "5", "b" : "9", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "7", "b" : "11", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 }, + { "a" : "8", "b" : "12", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON index f8d95f0cb..30e62b040 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/balance.JSON @@ -1,532 +1,532 @@ -{ - "Balance M" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 7 }, - { "min" : 8000, "max" : 9500, "density" : 7 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 5 }, - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 6000, "max" : 8000, "density" : 7 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "2", "b" : "9", "guard" : 5000 }, - { "a" : "6", "b" : "3", "guard" : 8000 }, - { "a" : "7", "b" : "4", "guard" : 9000 }, - { "a" : "8", "b" : "3", "guard" : 8000 }, - { "a" : "9", "b" : "4", "guard" : 9000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 20000 }, - { "a" : "2", "b" : "5", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 14000 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - }, - "Balance L" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 4900, "density" : 6 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "mercury" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 7 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 3 }, - { "min" : 8000, "max" : 9500, "density" : 5 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6500 }, - { "a" : "6", "b" : "3", "guard" : 11000 }, - { "a" : "7", "b" : "4", "guard" : 12000 }, - { "a" : "8", "b" : "3", "guard" : 11000 }, - { "a" : "9", "b" : "4", "guard" : 12000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 30000 }, - { "a" : "2", "b" : "5", "guard" : 30000 }, - { "a" : "5", "b" : "10", "guard" : 22000 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - }, - "Balance XL" : - { - "minSize" : "xl", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 4900, "density" : 6 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "mercury" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 1 }, - { "min" : 5000, "max" : 5000, "density" : 2 }, - { "min" : 7000, "max" : 9000, "density" : 6 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 3 }, - { "min" : 8000, "max" : 9500, "density" : 5 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 5 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 2 }, - { "min" : 3500, "max" : 5000, "density" : 2 }, - { "min" : 6000, "max" : 8000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 11 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6500 }, - { "a" : "1", "b" : "7", "guard" : 7000 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "9", "guard" : 7000 }, - { "a" : "6", "b" : "3", "guard" : 12000 }, - { "a" : "7", "b" : "4", "guard" : 13000 }, - { "a" : "8", "b" : "3", "guard" : 12000 }, - { "a" : "9", "b" : "4", "guard" : 13000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 40000 }, - { "a" : "2", "b" : "5", "guard" : 40000 }, - { "a" : "5", "b" : "10", "guard" : 30000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "1", "b" : "11", "guard" : 6500 }, - { "a" : "2", "b" : "12", "guard" : 6500 }, - { "a" : "11", "b" : "3", "guard" : 12000 }, - { "a" : "12", "b" : "3", "guard" : 12000 }, - { "a" : "11", "b" : "4", "guard" : 13000 }, - { "a" : "12", "b" : "4", "guard" : 13000 } - ] - } -} +{ + "Balance M" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 7 }, + { "min" : 8000, "max" : 9500, "density" : 7 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 5 }, + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 6000, "max" : 8000, "density" : 7 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "2", "b" : "9", "guard" : 5000 }, + { "a" : "6", "b" : "3", "guard" : 8000 }, + { "a" : "7", "b" : "4", "guard" : 9000 }, + { "a" : "8", "b" : "3", "guard" : 8000 }, + { "a" : "9", "b" : "4", "guard" : 9000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 20000 }, + { "a" : "2", "b" : "5", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 14000 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + }, + "Balance L" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 4900, "density" : 6 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "mercury" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 7 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 3 }, + { "min" : 8000, "max" : 9500, "density" : 5 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6500 }, + { "a" : "6", "b" : "3", "guard" : 11000 }, + { "a" : "7", "b" : "4", "guard" : 12000 }, + { "a" : "8", "b" : "3", "guard" : 11000 }, + { "a" : "9", "b" : "4", "guard" : 12000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 30000 }, + { "a" : "2", "b" : "5", "guard" : 30000 }, + { "a" : "5", "b" : "10", "guard" : 22000 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + }, + "Balance XL" : + { + "minSize" : "xl", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 4900, "density" : 6 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "mercury" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 1 }, + { "min" : 5000, "max" : 5000, "density" : 2 }, + { "min" : 7000, "max" : 9000, "density" : 6 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 3 }, + { "min" : 8000, "max" : 9500, "density" : 5 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 5 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 2 }, + { "min" : 3500, "max" : 5000, "density" : 2 }, + { "min" : 6000, "max" : 8000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 11 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6500 }, + { "a" : "1", "b" : "7", "guard" : 7000 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "9", "guard" : 7000 }, + { "a" : "6", "b" : "3", "guard" : 12000 }, + { "a" : "7", "b" : "4", "guard" : 13000 }, + { "a" : "8", "b" : "3", "guard" : 12000 }, + { "a" : "9", "b" : "4", "guard" : 13000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 40000 }, + { "a" : "2", "b" : "5", "guard" : 40000 }, + { "a" : "5", "b" : "10", "guard" : 30000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "1", "b" : "11", "guard" : 6500 }, + { "a" : "2", "b" : "12", "guard" : 6500 }, + { "a" : "11", "b" : "3", "guard" : 12000 }, + { "a" : "12", "b" : "3", "guard" : 12000 }, + { "a" : "11", "b" : "4", "guard" : 13000 }, + { "a" : "12", "b" : "4", "guard" : 13000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON index 642165c63..70baf74fd 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/cube.JSON @@ -1,117 +1,117 @@ -{ - "Cube" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9200, "density" : 2 }, - { "min" : 3500, "max" : 6000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 6 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 9800, "density" : 12 }, - { "min" : 1500, "max" : 2000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "1", "b" : "4", "guard" : 4500 }, - { "a" : "1", "b" : "5", "guard" : 4500 }, - { "a" : "2", "b" : "6", "guard" : 4500 }, - { "a" : "2", "b" : "7", "guard" : 4500 }, - { "a" : "2", "b" : "8", "guard" : 4500 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "8", "guard" : 11000 }, - { "a" : "5", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "8", "guard" : 11000 } - ] - } -} +{ + "Cube" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9200, "density" : 2 }, + { "min" : 3500, "max" : 6000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 6 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 9800, "density" : 12 }, + { "min" : 1500, "max" : 2000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp", "rough", "subterra", "lava" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "1", "b" : "4", "guard" : 4500 }, + { "a" : "1", "b" : "5", "guard" : 4500 }, + { "a" : "2", "b" : "6", "guard" : 4500 }, + { "a" : "2", "b" : "7", "guard" : 4500 }, + { "a" : "2", "b" : "8", "guard" : 4500 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "8", "guard" : 11000 }, + { "a" : "5", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "8", "guard" : 11000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON index ae3989c17..c83a506c4 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/diamond.JSON @@ -1,228 +1,228 @@ -{ - "Diamond" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 8999, "density" : 3 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 12 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 3 }, - { "min" : 9000, "max" : 20000, "density" : 9 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "gems" : 2 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 6000 }, - { "a" : "5", "b" : "10", "guard" : 6000 }, - { "a" : "3", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "15", "guard" : 6000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "8", "b" : "15", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "4", "b" : "13", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "4", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "16", "guard" : 6000 }, - { "a" : "3", "b" : "16", "guard" : 6000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "2", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "13", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "16", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 } - ] - } -} +{ + "Diamond" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 8999, "density" : 3 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 12 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 3 }, + { "min" : 9000, "max" : 20000, "density" : 9 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "gems" : 2 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 6000 }, + { "a" : "5", "b" : "10", "guard" : 6000 }, + { "a" : "3", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "15", "guard" : 6000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "8", "b" : "15", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "4", "b" : "13", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "4", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "16", "guard" : 6000 }, + { "a" : "3", "b" : "16", "guard" : 6000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "2", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "13", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "16", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON index b31dac257..798115f21 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/fear.JSON @@ -1,183 +1,183 @@ -{ - "Fear" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 2000, "density" : 18 }, - { "min" : 3000, "max" : 5000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 12500, "density" : 2 }, - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 300, "max" : 2000, "density" : 12 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 2, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 2 - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 1400, "density" : 17 }, - { "min" : 3000, "max" : 4000, "density" : 5 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "normal", - "terrainTypeLikeZone" : 4, - "treasure" : - [ - { "min" : 800, "max" : 800, "density" : 25 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 800, "max" : 4000, "density" : 8 }, - { "min" : 5000, "max" : 8000, "density" : 12 }, - { "min" : 10000, "max" : 15000, "density" : 5 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 10 }, - { "min" : 20000, "max" : 35000, "density" : 5 } - ] - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 300, "max" : 2000, "density" : 15 }, - { "min" : 3000, "max" : 5000, "density" : 6 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 3, - "treasureLikeZone" : 2 - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasure" : - [ - { "min" : 100, "max" : 1400, "density" : 18 }, - { "min" : 3000, "max" : 4000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 6 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 0 }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "5", "b" : "11", "guard" : 0 }, - { "a" : "11", "b" : "8", "guard" : 0 }, - { "a" : "2", "b" : "6", "guard" : 0 }, - { "a" : "3", "b" : "12", "guard" : 0 }, - { "a" : "12", "b" : "9", "guard" : 0 }, - { "a" : "6", "b" : "10", "guard" : 0 }, - { "a" : "9", "b" : "8", "guard" : 0 }, - { "a" : "10", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 } - ] - } -} +{ + "Fear" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 2000, "density" : 18 }, + { "min" : 3000, "max" : 5000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 12500, "density" : 2 }, + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 300, "max" : 2000, "density" : 12 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 2, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 2 + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 1400, "density" : 17 }, + { "min" : 3000, "max" : 4000, "density" : 5 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "normal", + "terrainTypeLikeZone" : 4, + "treasure" : + [ + { "min" : 800, "max" : 800, "density" : 25 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 800, "max" : 4000, "density" : 8 }, + { "min" : 5000, "max" : 8000, "density" : 12 }, + { "min" : 10000, "max" : 15000, "density" : 5 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 10 }, + { "min" : 20000, "max" : 35000, "density" : 5 } + ] + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 300, "max" : 2000, "density" : 15 }, + { "min" : 3000, "max" : 5000, "density" : 6 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 3, + "treasureLikeZone" : 2 + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasure" : + [ + { "min" : 100, "max" : 1400, "density" : 18 }, + { "min" : 3000, "max" : 4000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 6 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 0 }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "5", "b" : "11", "guard" : 0 }, + { "a" : "11", "b" : "8", "guard" : 0 }, + { "a" : "2", "b" : "6", "guard" : 0 }, + { "a" : "3", "b" : "12", "guard" : 0 }, + { "a" : "12", "b" : "9", "guard" : 0 }, + { "a" : "6", "b" : "10", "guard" : 0 }, + { "a" : "9", "b" : "8", "guard" : 0 }, + { "a" : "10", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON index ae231c0f7..8adfb2478 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/frozenDragons.JSON @@ -1,255 +1,255 @@ -{ - "Frozen Dragons" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 5 }, - { "min" : 2500, "max" : 3000, "density" : 3 }, - { "min" : 17000, "max" : 20000, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["castle", "necropolis", "conflux"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 4 }, - { "min" : 3500, "max" : 7000, "density" : 4 }, - { "min" : 9000, "max" : 9300, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass", "lava" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "tower", "stronghold", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 4 }, - { "min" : 5000, "max" : 9000, "density" : 4 }, - { "min" : 15000, "max" : 19000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "tower", "stronghold", "conflux" ], - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 3, - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 350, "max" : 9000, "density" : 3 }, - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 25000, "max" : 28000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "mines" : { "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 19000, "density" : 4 }, - { "min" : 110000, "max" : 120000, "density" : 2 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 5, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis", "castle"], - "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "mines" : { "wood" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3500, "density" : 1 }, - { "min" : 9000, "max" : 30000, "density" : 2 }, - { "min" : 80000, "max" : 80000, "density" : 6 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 11000, "density" : 2 }, - { "min" : 80000, "max" : 80000, "density" : 6 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasure" : - [ - { "min" : 80000, "max" : 80000, "density" : 7 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 13, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3500 }, - { "a" : "2", "b" : "4", "guard" : 3500 }, - { "a" : "3", "b" : "5", "guard" : 5000 }, - { "a" : "4", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "5", "guard" : 5000 }, - { "a" : "2", "b" : "6", "guard" : 5000 }, - { "a" : "5", "b" : "7", "guard" : 15000 }, - { "a" : "6", "b" : "8", "guard" : 15000 }, - { "a" : "7", "b" : "9", "guard" : 30000 }, - { "a" : "8", "b" : "10", "guard" : 30000 }, - { "a" : "9", "b" : "11", "guard" : 60000 }, - { "a" : "9", "b" : "11", "guard" : 60000 }, - { "a" : "10", "b" : "12", "guard" : 60000 }, - { "a" : "10", "b" : "12", "guard" : 60000 }, - { "a" : "11", "b" : "13", "guard" : 0 }, - { "a" : "11", "b" : "14", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "12", "b" : "14", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 240000 }, - { "a" : "11", "b" : "13", "guard" : 0 }, - { "a" : "11", "b" : "14", "guard" : 0 }, - { "a" : "12", "b" : "13", "guard" : 0 }, - { "a" : "12", "b" : "14", "guard" : 0 }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 }, - { "a" : "15", "b" : "16", "guard" : 140000 } - ] - } -} +{ + "Frozen Dragons" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 5 }, + { "min" : 2500, "max" : 3000, "density" : 3 }, + { "min" : 17000, "max" : 20000, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["castle", "necropolis", "conflux"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "neutral", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 4 }, + { "min" : 3500, "max" : 7000, "density" : 4 }, + { "min" : 9000, "max" : 9300, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass", "lava" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "tower", "stronghold", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 4 }, + { "min" : 5000, "max" : 9000, "density" : 4 }, + { "min" : 15000, "max" : 19000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "tower", "stronghold", "conflux" ], + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 3, + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 350, "max" : 9000, "density" : 3 }, + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 25000, "max" : 28000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "castle", "tower", "inferno", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "mines" : { "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 19000, "density" : 4 }, + { "min" : 110000, "max" : 120000, "density" : 2 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 5, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis", "castle"], + "allowedMonsters" : [ "rampart", "necropolis", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "mines" : { "wood" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3500, "density" : 1 }, + { "min" : 9000, "max" : 30000, "density" : 2 }, + { "min" : 80000, "max" : 80000, "density" : 6 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 11000, "density" : 2 }, + { "min" : 80000, "max" : 80000, "density" : 6 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasure" : + [ + { "min" : 80000, "max" : 80000, "density" : 7 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "rampart", "dungeon", "neutral" ], + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 13, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3500 }, + { "a" : "2", "b" : "4", "guard" : 3500 }, + { "a" : "3", "b" : "5", "guard" : 5000 }, + { "a" : "4", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "5", "guard" : 5000 }, + { "a" : "2", "b" : "6", "guard" : 5000 }, + { "a" : "5", "b" : "7", "guard" : 15000 }, + { "a" : "6", "b" : "8", "guard" : 15000 }, + { "a" : "7", "b" : "9", "guard" : 30000 }, + { "a" : "8", "b" : "10", "guard" : 30000 }, + { "a" : "9", "b" : "11", "guard" : 60000 }, + { "a" : "9", "b" : "11", "guard" : 60000 }, + { "a" : "10", "b" : "12", "guard" : 60000 }, + { "a" : "10", "b" : "12", "guard" : 60000 }, + { "a" : "11", "b" : "13", "guard" : 0 }, + { "a" : "11", "b" : "14", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "12", "b" : "14", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 240000 }, + { "a" : "11", "b" : "13", "guard" : 0 }, + { "a" : "11", "b" : "14", "guard" : 0 }, + { "a" : "12", "b" : "13", "guard" : 0 }, + { "a" : "12", "b" : "14", "guard" : 0 }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 }, + { "a" : "15", "b" : "16", "guard" : 140000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON index 214ffd059..0b4573168 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/gimlisRevenge.JSON @@ -1,102 +1,102 @@ -{ - "Gimlis Revenge" : - { - "minSize" : "m", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "junction", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 12000 }, - { "a" : "6", "b" : "8", "guard" : 12000 }, - { "a" : "5", "b" : "8", "guard" : 12000 }, - { "a" : "4", "b" : "7", "guard" : 12000 } - ] - } -} +{ + "Gimlis Revenge" : + { + "minSize" : "m", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "junction", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 12000 }, + { "a" : "6", "b" : "8", "guard" : 12000 }, + { "a" : "5", "b" : "8", "guard" : 12000 }, + { "a" : "4", "b" : "7", "guard" : 12000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON index cbe0a1c48..55f601301 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/guerilla.JSON @@ -1,332 +1,332 @@ -{ - "Guerilla" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 25 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "10", "b" : "13", "guard" : 3000 }, - { "a" : "11", "b" : "13", "guard" : 3000 }, - { "a" : "11", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "1", "b" : "16", "guard" : 3000 }, - { "a" : "14", "b" : "20", "guard" : 6000 }, - { "a" : "13", "b" : "20", "guard" : 6000 }, - { "a" : "16", "b" : "17", "guard" : 6000 }, - { "a" : "6", "b" : "17", "guard" : 6000 }, - { "a" : "6", "b" : "18", "guard" : 6000 }, - { "a" : "7", "b" : "18", "guard" : 6000 }, - { "a" : "7", "b" : "24", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 6000 }, - { "a" : "5", "b" : "23", "guard" : 6000 }, - { "a" : "12", "b" : "23", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "13", "b" : "19", "guard" : 6000 }, - { "a" : "14", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "21", "guard" : 6000 }, - { "a" : "16", "b" : "21", "guard" : 6000 }, - { "a" : "21", "b" : "25", "guard" : 12500 }, - { "a" : "22", "b" : "28", "guard" : 12500 }, - { "a" : "17", "b" : "25", "guard" : 12500 }, - { "a" : "18", "b" : "26", "guard" : 12500 }, - { "a" : "24", "b" : "26", "guard" : 12500 }, - { "a" : "23", "b" : "27", "guard" : 12500 }, - { "a" : "19", "b" : "27", "guard" : 12500 }, - { "a" : "20", "b" : "28", "guard" : 12500 }, - { "a" : "27", "b" : "28", "guard" : 12500 }, - { "a" : "26", "b" : "27", "guard" : 12500 }, - { "a" : "25", "b" : "28", "guard" : 12500 }, - { "a" : "25", "b" : "26", "guard" : 12500 } - ] - } -} +{ + "Guerilla" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 25 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "10", "b" : "13", "guard" : 3000 }, + { "a" : "11", "b" : "13", "guard" : 3000 }, + { "a" : "11", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "1", "b" : "16", "guard" : 3000 }, + { "a" : "14", "b" : "20", "guard" : 6000 }, + { "a" : "13", "b" : "20", "guard" : 6000 }, + { "a" : "16", "b" : "17", "guard" : 6000 }, + { "a" : "6", "b" : "17", "guard" : 6000 }, + { "a" : "6", "b" : "18", "guard" : 6000 }, + { "a" : "7", "b" : "18", "guard" : 6000 }, + { "a" : "7", "b" : "24", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 6000 }, + { "a" : "5", "b" : "23", "guard" : 6000 }, + { "a" : "12", "b" : "23", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "13", "b" : "19", "guard" : 6000 }, + { "a" : "14", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "21", "guard" : 6000 }, + { "a" : "16", "b" : "21", "guard" : 6000 }, + { "a" : "21", "b" : "25", "guard" : 12500 }, + { "a" : "22", "b" : "28", "guard" : 12500 }, + { "a" : "17", "b" : "25", "guard" : 12500 }, + { "a" : "18", "b" : "26", "guard" : 12500 }, + { "a" : "24", "b" : "26", "guard" : 12500 }, + { "a" : "23", "b" : "27", "guard" : 12500 }, + { "a" : "19", "b" : "27", "guard" : 12500 }, + { "a" : "20", "b" : "28", "guard" : 12500 }, + { "a" : "27", "b" : "28", "guard" : 12500 }, + { "a" : "26", "b" : "27", "guard" : 12500 }, + { "a" : "25", "b" : "28", "guard" : 12500 }, + { "a" : "25", "b" : "26", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON index 6a236597b..2e2b1cfe0 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/headquarters.JSON @@ -1,158 +1,158 @@ -{ - "Headquarters" : - { - "minSize" : "l", "maxSize" : "l+u", - "players" : "2-7", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "fortress", "neutral" ], - "mines" : { "wood" : 2, "mercury" : 1, "ore" : 2, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 12000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 16000, "density" : 6 }, - { "min" : 300, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "tower" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "dungeon" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "neutral" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "allowedMonsters" : [ "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 20 } - ] - }, - "9" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 2 }, - "allowedTowns" : [ "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough" ], - "mines" : { "wood" : 2, "ore" : 2, "gold" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 3 }, - { "min" : 25000, "max" : 28000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 45000 }, - { "a" : "2", "b" : "9", "guard" : 45000 }, - { "a" : "3", "b" : "9", "guard" : 45000 }, - { "a" : "4", "b" : "9", "guard" : 45000 }, - { "a" : "5", "b" : "9", "guard" : 45000 }, - { "a" : "6", "b" : "9", "guard" : 45000 }, - { "a" : "7", "b" : "9", "guard" : 45000 }, - { "a" : "8", "b" : "9", "guard" : 45000 }, - { "a" : "1", "b" : "8", "guard" : 45000 }, - { "a" : "2", "b" : "8", "guard" : 45000 }, - { "a" : "3", "b" : "8", "guard" : 45000 }, - { "a" : "4", "b" : "8", "guard" : 45000 }, - { "a" : "5", "b" : "8", "guard" : 45000 }, - { "a" : "6", "b" : "8", "guard" : 45000 }, - { "a" : "7", "b" : "8", "guard" : 45000 } - ] - } -} +{ + "Headquarters" : + { + "minSize" : "l", "maxSize" : "l+u", + "players" : "2-7", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "fortress", "neutral" ], + "mines" : { "wood" : 2, "mercury" : 1, "ore" : 2, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 12000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 16000, "density" : 6 }, + { "min" : 300, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "tower" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "dungeon" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "neutral" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "allowedMonsters" : [ "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 20 } + ] + }, + "9" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 2 }, + "allowedTowns" : [ "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "necropolis", "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough" ], + "mines" : { "wood" : 2, "ore" : 2, "gold" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 3 }, + { "min" : 25000, "max" : 28000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 45000 }, + { "a" : "2", "b" : "9", "guard" : 45000 }, + { "a" : "3", "b" : "9", "guard" : 45000 }, + { "a" : "4", "b" : "9", "guard" : 45000 }, + { "a" : "5", "b" : "9", "guard" : 45000 }, + { "a" : "6", "b" : "9", "guard" : 45000 }, + { "a" : "7", "b" : "9", "guard" : 45000 }, + { "a" : "8", "b" : "9", "guard" : 45000 }, + { "a" : "1", "b" : "8", "guard" : 45000 }, + { "a" : "2", "b" : "8", "guard" : 45000 }, + { "a" : "3", "b" : "8", "guard" : 45000 }, + { "a" : "4", "b" : "8", "guard" : 45000 }, + { "a" : "5", "b" : "8", "guard" : 45000 }, + { "a" : "6", "b" : "8", "guard" : 45000 }, + { "a" : "7", "b" : "8", "guard" : 45000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON index e4ecf08dc..6443f0d6b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/hypercube.JSON @@ -1,230 +1,230 @@ -{ - "HyperCube" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "8", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "1", "b" : "17", "guard" : 3000 }, - { "a" : "7", "b" : "17", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "8", "b" : "20", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "3", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "19", "guard" : 3000 } - ] - } -} +{ + "HyperCube" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "8", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "1", "b" : "17", "guard" : 3000 }, + { "a" : "7", "b" : "17", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "8", "b" : "20", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "3", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "19", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON index 0d9780a74..036052ff2 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/longRun.JSON @@ -1,291 +1,291 @@ -{ - "Long Run M" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "subterra" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 30 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, - "treasure" : - [ - { "min" : 20000, "max" : 21000, "density" : 6 }, - { "min" : 12000, "max" : 16000, "density" : 5 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra" ], - "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 2 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 }, - "treasure" : - [ - { "min" : 30000, "max" : 90000, "density" : 25 } - ] - }, - "6" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "8" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 5000 }, - { "a" : "2", "b" : "3", "guard" : 10500 }, - { "a" : "3", "b" : "4", "guard" : 21000 }, - { "a" : "4", "b" : "5", "guard" : 45000 }, - { "a" : "5", "b" : "9", "guard" : 45000 }, - { "a" : "6", "b" : "7", "guard" : 5000 }, - { "a" : "7", "b" : "8", "guard" : 10500 }, - { "a" : "8", "b" : "9", "guard" : 21000 } - ] - }, - "Long Run XL" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, - "treasure" : - [ - { "min" : 300, "max" : 3000, "density" : 12 }, - { "min" : 5000, "max" : 9000, "density" : 6 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 70, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, - "treasure" : - [ - { "min" : 20000, "max" : 21000, "density" : 6 }, - { "min" : 12000, "max" : 16000, "density" : 5 }, - { "min" : 300, "max" : 3000, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 80, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt" ], - "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 5 }, - "treasure" : - [ - { "min" : 25000, "max" : 30000, "density" : 10 }, - { "min" : 300, "max" : 3000, "density" : 10 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 100, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 2, - "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 }, - "treasure" : - [ - { "min" : 30000, "max" : 90000, "density" : 25 } - ] - }, - "6" : - { - "type" : "playerStart", - "size" : 50, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "bannedTowns" : ["necropolis"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 60, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "8" : - { - "type" : "treasure", - "size" : 70, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2, "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 80, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis"], - "terrainTypeLikeZone" : 4, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 7000 }, - { "a" : "2", "b" : "3", "guard" : 14000 }, - { "a" : "3", "b" : "4", "guard" : 28000 }, - { "a" : "4", "b" : "5", "guard" : 60000 }, - { "a" : "5", "b" : "9", "guard" : 60000 }, - { "a" : "6", "b" : "7", "guard" : 7000 }, - { "a" : "7", "b" : "8", "guard" : 14000 }, - { "a" : "8", "b" : "9", "guard" : 28000 } - ] - } -} +{ + "Long Run M" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 3000, "density" : 12 }, + { "min" : 5000, "max" : 9000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "subterra" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 30 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, + "treasure" : + [ + { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra" ], + "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 2 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 10 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 4 }, + "treasure" : + [ + { "min" : 30000, "max" : 90000, "density" : 25 } + ] + }, + "6" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "8" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 5000 }, + { "a" : "2", "b" : "3", "guard" : 10500 }, + { "a" : "3", "b" : "4", "guard" : 21000 }, + { "a" : "4", "b" : "5", "guard" : 45000 }, + { "a" : "5", "b" : "9", "guard" : 45000 }, + { "a" : "6", "b" : "7", "guard" : 5000 }, + { "a" : "7", "b" : "8", "guard" : 10500 }, + { "a" : "8", "b" : "9", "guard" : 21000 } + ] + }, + "Long Run XL" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "mines" : { "wood" : 1, "mercury" : 0, "ore" : 1, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 1 }, + "treasure" : + [ + { "min" : 300, "max" : 3000, "density" : 12 }, + { "min" : 5000, "max" : 9000, "density" : 6 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 70, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 0 }, + "treasure" : + [ + { "min" : 20000, "max" : 21000, "density" : 6 }, + { "min" : 12000, "max" : 16000, "density" : 5 }, + { "min" : 300, "max" : 3000, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 80, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt" ], + "mines" : { "wood" : 0, "mercury" : 1, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 5 }, + "treasure" : + [ + { "min" : 25000, "max" : 30000, "density" : 10 }, + { "min" : 300, "max" : 3000, "density" : 10 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 100, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 2, + "mines" : { "wood" : 0, "mercury" : 0, "ore" : 0, "sulfur" : 0, "crystal" : 0, "gems" : 0, "gold" : 10 }, + "treasure" : + [ + { "min" : 30000, "max" : 90000, "density" : 25 } + ] + }, + "6" : + { + "type" : "playerStart", + "size" : 50, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "bannedTowns" : ["necropolis"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 60, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "8" : + { + "type" : "treasure", + "size" : 70, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2, "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 80, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis"], + "terrainTypeLikeZone" : 4, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 7000 }, + { "a" : "2", "b" : "3", "guard" : 14000 }, + { "a" : "3", "b" : "4", "guard" : 28000 }, + { "a" : "4", "b" : "5", "guard" : 60000 }, + { "a" : "5", "b" : "9", "guard" : 60000 }, + { "a" : "6", "b" : "7", "guard" : 7000 }, + { "a" : "7", "b" : "8", "guard" : 14000 }, + { "a" : "8", "b" : "9", "guard" : 28000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON index b68c54f06..cf9e8e970 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/marathon.JSON @@ -1,639 +1,639 @@ -{ - "Marathon XL" : - { - "minSize" : "xl", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 5000, "density" : 4 }, - { "min" : 300, "max" : 2000, "density" : 7 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : [ "castle", "conflux"], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9600, "density" : 7 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : [ "rampart", "conflux" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 8000, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "stronghold", "conflux"], - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 10000, "density" : 1 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 6 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "conflux"], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["rampart", "conflux"], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle", "stronghold", "conflux"], - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 8000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 6 }, - { "min" : 1000, "max" : 2000, "density" : 3 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "minesLikeZone" : 10, - "treasureLikeZone" : 10 - }, - "14" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow", "swamp", "rough" ], - "treasure" : - [ - { "min" : 35000, "max" : 45000, "density" : 1 }, - { "min" : 20000, "max" : 22000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 7 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "21" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 80000, "max" : 120000, "density" : 2 }, - { "min" : 40000, "max" : 70000, "density" : 1 }, - { "min" : 10000, "max" : 29000, "density" : 1 } - ] - }, - "22" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - }, - "23" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 7000 }, - { "a" : "2", "b" : "6", "guard" : 7000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 18000 }, - { "a" : "4", "b" : "9", "guard" : 23000 }, - { "a" : "4", "b" : "10", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 23000 }, - { "a" : "5", "b" : "11", "guard" : 16000 }, - { "a" : "3", "b" : "11", "guard" : 27000 }, - { "a" : "6", "b" : "12", "guard" : 18000 }, - { "a" : "7", "b" : "12", "guard" : 23000 }, - { "a" : "8", "b" : "13", "guard" : 23000 }, - { "a" : "8", "b" : "14", "guard" : 18000 }, - { "a" : "7", "b" : "13", "guard" : 20000 }, - { "a" : "6", "b" : "14", "guard" : 27000 }, - { "a" : "9", "b" : "15", "guard" : 40000 }, - { "a" : "10", "b" : "16", "guard" : 45000 }, - { "a" : "11", "b" : "17", "guard" : 35000 }, - { "a" : "12", "b" : "18", "guard" : 40000 }, - { "a" : "13", "b" : "19", "guard" : 45000 }, - { "a" : "14", "b" : "20", "guard" : 35000 }, - { "a" : "15", "b" : "21", "guard" : 60000 }, - { "a" : "16", "b" : "22", "guard" : 80000 }, - { "a" : "17", "b" : "23", "guard" : 70000 }, - { "a" : "18", "b" : "21", "guard" : 70000 }, - { "a" : "19", "b" : "22", "guard" : 80000 }, - { "a" : "20", "b" : "23", "guard" : 60000 }, - { "a" : "21", "b" : "22", "guard" : 280000 }, - { "a" : "22", "b" : "23", "guard" : 280000 }, - { "a" : "23", "b" : "21", "guard" : 280000 } - ] - }, - "Marathon XXL": - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 300, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9600, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 3500, "max" : 8000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 9000, "max" : 10000, "density" : 1 }, - { "min" : 3500, "max" : 6000, "density" : 5 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 22000, "density" : 1 }, - { "min" : 5000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 8000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "mines" : { "crystal" : 1 }, - "treasure" : - [ - { "min" : 25000, "max" : 29000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "rampart", "dungeon" ], - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno" ], - "minesLikeZone" : 10, - "treasureLikeZone" : 10 - }, - "14" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "necropolis" ], - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow", "swamp", "rough" ], - "treasure" : - [ - { "min" : 35000, "max" : 45000, "density" : 1 }, - { "min" : 20000, "max" : 22000, "density" : 1 }, - { "min" : 7000, "max" : 9300, "density" : 5 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "17" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "18" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "19" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "20" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], - "terrainTypeLikeZone" : 15, - "treasureLikeZone" : 15 - }, - "21" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "treasure" : - [ - { "min" : 110000, "max" : 120000, "density" : 2 }, - { "min" : 60000, "max" : 70000, "density" : 1 }, - { "min" : 30000, "max" : 60000, "density" : 1 } - ] - }, - "22" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - }, - "23" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "strong", - "allowedMonsters" : [ "castle", "rampart", "dungeon" ], - "terrainTypeLikeZone" : 21, - "treasureLikeZone" : 21 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 5000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 7000 }, - { "a" : "2", "b" : "6", "guard" : 7000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "3", "b" : "9", "guard" : 18000 }, - { "a" : "4", "b" : "9", "guard" : 25000 }, - { "a" : "4", "b" : "10", "guard" : 22000 }, - { "a" : "5", "b" : "10", "guard" : 23000 }, - { "a" : "5", "b" : "11", "guard" : 16000 }, - { "a" : "3", "b" : "11", "guard" : 29000 }, - { "a" : "6", "b" : "12", "guard" : 18000 }, - { "a" : "7", "b" : "12", "guard" : 25000 }, - { "a" : "7", "b" : "13", "guard" : 22000 }, - { "a" : "8", "b" : "13", "guard" : 23000 }, - { "a" : "8", "b" : "14", "guard" : 18000 }, - { "a" : "6", "b" : "14", "guard" : 29000 }, - { "a" : "9", "b" : "15", "guard" : 45000 }, - { "a" : "10", "b" : "16", "guard" : 50000 }, - { "a" : "11", "b" : "17", "guard" : 40000 }, - { "a" : "12", "b" : "18", "guard" : 45000 }, - { "a" : "13", "b" : "19", "guard" : 50000 }, - { "a" : "14", "b" : "20", "guard" : 40000 }, - { "a" : "15", "b" : "21", "guard" : 70000 }, - { "a" : "16", "b" : "22", "guard" : 90000 }, - { "a" : "17", "b" : "23", "guard" : 80000 }, - { "a" : "18", "b" : "21", "guard" : 80000 }, - { "a" : "19", "b" : "22", "guard" : 90000 }, - { "a" : "20", "b" : "23", "guard" : 70000 }, - { "a" : "21", "b" : "22", "guard" : 350000 }, - { "a" : "22", "b" : "23", "guard" : 350000 }, - { "a" : "23", "b" : "21", "guard" : 350000 } - ] - } -} +{ + "Marathon XL" : + { + "minSize" : "xl", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 5000, "density" : 4 }, + { "min" : 300, "max" : 2000, "density" : 7 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : [ "castle", "conflux"], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9600, "density" : 7 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : [ "rampart", "conflux" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 8000, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "stronghold", "conflux"], + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 10000, "density" : 1 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 6 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "conflux"], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["rampart", "conflux"], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle", "stronghold", "conflux"], + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 8000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 6 }, + { "min" : 1000, "max" : 2000, "density" : 3 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "minesLikeZone" : 10, + "treasureLikeZone" : 10 + }, + "14" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow", "swamp", "rough" ], + "treasure" : + [ + { "min" : 35000, "max" : 45000, "density" : 1 }, + { "min" : 20000, "max" : 22000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 7 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "21" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 80000, "max" : 120000, "density" : 2 }, + { "min" : 40000, "max" : 70000, "density" : 1 }, + { "min" : 10000, "max" : 29000, "density" : 1 } + ] + }, + "22" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + }, + "23" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 7000 }, + { "a" : "2", "b" : "6", "guard" : 7000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 18000 }, + { "a" : "4", "b" : "9", "guard" : 23000 }, + { "a" : "4", "b" : "10", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 23000 }, + { "a" : "5", "b" : "11", "guard" : 16000 }, + { "a" : "3", "b" : "11", "guard" : 27000 }, + { "a" : "6", "b" : "12", "guard" : 18000 }, + { "a" : "7", "b" : "12", "guard" : 23000 }, + { "a" : "8", "b" : "13", "guard" : 23000 }, + { "a" : "8", "b" : "14", "guard" : 18000 }, + { "a" : "7", "b" : "13", "guard" : 20000 }, + { "a" : "6", "b" : "14", "guard" : 27000 }, + { "a" : "9", "b" : "15", "guard" : 40000 }, + { "a" : "10", "b" : "16", "guard" : 45000 }, + { "a" : "11", "b" : "17", "guard" : 35000 }, + { "a" : "12", "b" : "18", "guard" : 40000 }, + { "a" : "13", "b" : "19", "guard" : 45000 }, + { "a" : "14", "b" : "20", "guard" : 35000 }, + { "a" : "15", "b" : "21", "guard" : 60000 }, + { "a" : "16", "b" : "22", "guard" : 80000 }, + { "a" : "17", "b" : "23", "guard" : 70000 }, + { "a" : "18", "b" : "21", "guard" : 70000 }, + { "a" : "19", "b" : "22", "guard" : 80000 }, + { "a" : "20", "b" : "23", "guard" : 60000 }, + { "a" : "21", "b" : "22", "guard" : 280000 }, + { "a" : "22", "b" : "23", "guard" : 280000 }, + { "a" : "23", "b" : "21", "guard" : 280000 } + ] + }, + "Marathon XXL": + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 300, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9600, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 3500, "max" : 8000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "mines" : { "mercury" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 9000, "max" : 10000, "density" : 1 }, + { "min" : 3500, "max" : 6000, "density" : 5 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "castle", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "rampart", "tower", "inferno", "necropolis", "dungeon", "fortress" ], + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 22000, "density" : 1 }, + { "min" : 5000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 8000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "mines" : { "crystal" : 1 }, + "treasure" : + [ + { "min" : 25000, "max" : 29000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "rampart", "dungeon" ], + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno" ], + "minesLikeZone" : 10, + "treasureLikeZone" : 10 + }, + "14" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "necropolis" ], + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow", "swamp", "rough" ], + "treasure" : + [ + { "min" : 35000, "max" : 45000, "density" : 1 }, + { "min" : 20000, "max" : 22000, "density" : 1 }, + { "min" : 7000, "max" : 9300, "density" : 5 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "17" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "18" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "19" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "20" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "castle", "rampart", "tower", "dungeon", "stronghold", "fortress", "conflux" ], + "terrainTypeLikeZone" : 15, + "treasureLikeZone" : 15 + }, + "21" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "treasure" : + [ + { "min" : 110000, "max" : 120000, "density" : 2 }, + { "min" : 60000, "max" : 70000, "density" : 1 }, + { "min" : 30000, "max" : 60000, "density" : 1 } + ] + }, + "22" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + }, + "23" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "strong", + "allowedMonsters" : [ "castle", "rampart", "dungeon" ], + "terrainTypeLikeZone" : 21, + "treasureLikeZone" : 21 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 5000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 7000 }, + { "a" : "2", "b" : "6", "guard" : 7000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "3", "b" : "9", "guard" : 18000 }, + { "a" : "4", "b" : "9", "guard" : 25000 }, + { "a" : "4", "b" : "10", "guard" : 22000 }, + { "a" : "5", "b" : "10", "guard" : 23000 }, + { "a" : "5", "b" : "11", "guard" : 16000 }, + { "a" : "3", "b" : "11", "guard" : 29000 }, + { "a" : "6", "b" : "12", "guard" : 18000 }, + { "a" : "7", "b" : "12", "guard" : 25000 }, + { "a" : "7", "b" : "13", "guard" : 22000 }, + { "a" : "8", "b" : "13", "guard" : 23000 }, + { "a" : "8", "b" : "14", "guard" : 18000 }, + { "a" : "6", "b" : "14", "guard" : 29000 }, + { "a" : "9", "b" : "15", "guard" : 45000 }, + { "a" : "10", "b" : "16", "guard" : 50000 }, + { "a" : "11", "b" : "17", "guard" : 40000 }, + { "a" : "12", "b" : "18", "guard" : 45000 }, + { "a" : "13", "b" : "19", "guard" : 50000 }, + { "a" : "14", "b" : "20", "guard" : 40000 }, + { "a" : "15", "b" : "21", "guard" : 70000 }, + { "a" : "16", "b" : "22", "guard" : 90000 }, + { "a" : "17", "b" : "23", "guard" : 80000 }, + { "a" : "18", "b" : "21", "guard" : 80000 }, + { "a" : "19", "b" : "22", "guard" : 90000 }, + { "a" : "20", "b" : "23", "guard" : 70000 }, + { "a" : "21", "b" : "22", "guard" : 350000 }, + { "a" : "22", "b" : "23", "guard" : 350000 }, + { "a" : "23", "b" : "21", "guard" : 350000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON index d4cf5ac3b..ee48447b3 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/miniNostalgia.JSON @@ -1,179 +1,179 @@ -{ - "Mini Nostalgia" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "playerStart", - "size" : 40, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "11" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "14" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "15" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "3", "guard" : 45000 }, - { "a" : "5", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "8", "b" : "13", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 } - ] - } -} +{ + "Mini Nostalgia" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "playerStart", + "size" : 40, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "11" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "14" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "15" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "3", "guard" : 45000 }, + { "a" : "5", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "8", "b" : "13", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON index 1703a3ba1..22ea8d9da 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/nostalgia.JSON @@ -1,292 +1,292 @@ -{ - "Nostalgia" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "playerStart", - "size" : 23, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 17000, "max" : 20000, "density" : 3 }, - { "min" : 10000, "max" : 17000, "density" : 9 }, - { "min" : 6000, "max" : 8999, "density" : 9 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "18" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "19" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "20" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "21" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "22" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "23" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - }, - "24" : - { - "type" : "treasure", - "size" : 23, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 16, - "treasureLikeZone" : 16 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "8", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "9", "b" : "14", "guard" : 3000 }, - { "a" : "10", "b" : "17", "guard" : 6000 }, - { "a" : "15", "b" : "17", "guard" : 6000 }, - { "a" : "12", "b" : "16", "guard" : 6000 }, - { "a" : "13", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "18", "guard" : 6000 }, - { "a" : "12", "b" : "18", "guard" : 6000 }, - { "a" : "13", "b" : "21", "guard" : 6000 }, - { "a" : "14", "b" : "21", "guard" : 6000 }, - { "a" : "15", "b" : "20", "guard" : 6000 }, - { "a" : "14", "b" : "20", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "15", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "24", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 } - ] - } -} +{ + "Nostalgia" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "playerStart", + "size" : 23, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 17000, "max" : 20000, "density" : 3 }, + { "min" : 10000, "max" : 17000, "density" : 9 }, + { "min" : 6000, "max" : 8999, "density" : 9 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "18" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "19" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "20" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "21" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "22" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "23" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + }, + "24" : + { + "type" : "treasure", + "size" : 23, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 16, + "treasureLikeZone" : 16 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "8", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "9", "b" : "14", "guard" : 3000 }, + { "a" : "10", "b" : "17", "guard" : 6000 }, + { "a" : "15", "b" : "17", "guard" : 6000 }, + { "a" : "12", "b" : "16", "guard" : 6000 }, + { "a" : "13", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "18", "guard" : 6000 }, + { "a" : "12", "b" : "18", "guard" : 6000 }, + { "a" : "13", "b" : "21", "guard" : 6000 }, + { "a" : "14", "b" : "21", "guard" : 6000 }, + { "a" : "15", "b" : "20", "guard" : 6000 }, + { "a" : "14", "b" : "20", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "15", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "24", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON index 56bc94559..ddf7cd326 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/oceansEleven.JSON @@ -1,149 +1,149 @@ -{ - "Oceans Eleven" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "grass" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 6000, "density" : 1 }, - { "min" : 4000, "max" : 5000, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4000, "max" : 6000, "density" : 1 }, - { "min" : 4000, "max" : 6000, "density" : 10 }, - { "min" : 2000, "max" : 3000, "density" : 9 } - ] - }, - "3" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 7000, "max" : 8000, "density" : 1 }, - { "min" : 6000, "max" : 8000, "density" : 9 }, - { "min" : 2000, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 9000, "max" : 11000, "density" : 1 }, - { "min" : 5000, "max" : 7000, "density" : 9 }, - { "min" : 3000, "max" : 4000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "lava" ], - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 6000, "max" : 8000, "density" : 8 }, - { "min" : 2000, "max" : 3000, "density" : 10 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 2 - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypes" : [ "grass" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 5000 }, - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 7000 }, - { "a" : "3", "b" : "5", "guard" : 7000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 9000 }, - { "a" : "6", "b" : "8", "guard" : 9000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 7000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "10", "b" : "11", "guard" : 5000 } - ] - } -} +{ + "Oceans Eleven" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "grass" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 6000, "density" : 1 }, + { "min" : 4000, "max" : 5000, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4000, "max" : 6000, "density" : 1 }, + { "min" : 4000, "max" : 6000, "density" : 10 }, + { "min" : 2000, "max" : 3000, "density" : 9 } + ] + }, + "3" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 7000, "max" : 8000, "density" : 1 }, + { "min" : 6000, "max" : 8000, "density" : 9 }, + { "min" : 2000, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 9000, "max" : 11000, "density" : 1 }, + { "min" : 5000, "max" : 7000, "density" : 9 }, + { "min" : 3000, "max" : 4000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "snow", "swamp", "lava" ], + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 6000, "max" : 8000, "density" : 8 }, + { "min" : 2000, "max" : 3000, "density" : 10 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 2 + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypes" : [ "grass" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 5000 }, + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 7000 }, + { "a" : "3", "b" : "5", "guard" : 7000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 9000 }, + { "a" : "6", "b" : "8", "guard" : 9000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 7000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "10", "b" : "11", "guard" : 5000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON index 937fba84f..773b68625 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/panic.JSON @@ -1,173 +1,173 @@ -{ - "Panic" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 6000, "density" : 2 }, - { "min" : 3500, "max" : 4500, "density" : 5 }, - { "min" : 300, "max" : 2000, "density" : 4 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 3500, "max" : 7000, "density" : 7 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 300, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 6000, "max" : 8000, "density" : 6 }, - { "min" : 3500, "max" : 6000, "density" : 3 }, - { "min" : 800, "max" : 800, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 9500, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 3 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "11" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "12" : - { - "type" : "treasure", - "size" : 12, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "13" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 10500 }, - { "a" : "5", "b" : "6", "guard" : 20000 }, - { "a" : "6", "b" : "7", "guard" : 20000 }, - { "a" : "7", "b" : "4", "guard" : 10500 }, - { "a" : "3", "b" : "8", "guard" : 10500 }, - { "a" : "8", "b" : "9", "guard" : 20000 }, - { "a" : "9", "b" : "10", "guard" : 20000 }, - { "a" : "10", "b" : "4", "guard" : 10500 }, - { "a" : "3", "b" : "11", "guard" : 10500 }, - { "a" : "11", "b" : "12", "guard" : 20000 }, - { "a" : "12", "b" : "13", "guard" : 20000 }, - { "a" : "13", "b" : "4", "guard" : 10500 }, - { "a" : "5", "b" : "7", "guard" : 10500 }, - { "a" : "8", "b" : "10", "guard" : 10500 }, - { "a" : "11", "b" : "13", "guard" : 10500 }, - { "a" : "3", "b" : "4", "guard" : 70000 } - ] - } -} +{ + "Panic" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 6000, "density" : 2 }, + { "min" : 3500, "max" : 4500, "density" : 5 }, + { "min" : 300, "max" : 2000, "density" : 4 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 3500, "max" : 7000, "density" : 7 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 300, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 6000, "max" : 8000, "density" : 6 }, + { "min" : 3500, "max" : 6000, "density" : 3 }, + { "min" : 800, "max" : 800, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 9500, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 3 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "11" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "12" : + { + "type" : "treasure", + "size" : 12, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "13" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 10500 }, + { "a" : "5", "b" : "6", "guard" : 20000 }, + { "a" : "6", "b" : "7", "guard" : 20000 }, + { "a" : "7", "b" : "4", "guard" : 10500 }, + { "a" : "3", "b" : "8", "guard" : 10500 }, + { "a" : "8", "b" : "9", "guard" : 20000 }, + { "a" : "9", "b" : "10", "guard" : 20000 }, + { "a" : "10", "b" : "4", "guard" : 10500 }, + { "a" : "3", "b" : "11", "guard" : 10500 }, + { "a" : "11", "b" : "12", "guard" : 20000 }, + { "a" : "12", "b" : "13", "guard" : 20000 }, + { "a" : "13", "b" : "4", "guard" : 10500 }, + { "a" : "5", "b" : "7", "guard" : 10500 }, + { "a" : "8", "b" : "10", "guard" : 10500 }, + { "a" : "11", "b" : "13", "guard" : 10500 }, + { "a" : "3", "b" : "4", "guard" : 70000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON index 9acc81b5c..cee02bf8a 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/shaaafworld.JSON @@ -1,61 +1,61 @@ -{ - "Schaafworld" : - { - "minSize" : "l", "maxSize" : "xl", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 14000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 5, - "owner" : 3, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 14000, "max" : 20000, "density" : 3 }, - { "min" : 5000, "max" : 8000, "density" : 6 }, - { "min" : 1000, "max" : 5000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 80000 }, - { "a" : "2", "b" : "3", "guard" : 80000 } - ] - } -} +{ + "Schaafworld" : + { + "minSize" : "l", "maxSize" : "xl", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 14000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 5, + "owner" : 3, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 14000, "max" : 20000, "density" : 3 }, + { "min" : 5000, "max" : 8000, "density" : 6 }, + { "min" : 1000, "max" : 5000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 80000 }, + { "a" : "2", "b" : "3", "guard" : 80000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON index 11cf29d22..99080a813 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/skirmish.JSON @@ -1,233 +1,233 @@ -{ - "Skirmish M" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 7 }, - { "min" : 330, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 40000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 }, - { "min" : 350, "max" : 2000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 9000 }, - { "a" : "3", "b" : "6", "guard" : 9000 }, - { "a" : "3", "b" : "7", "guard" : 9000 }, - { "a" : "4", "b" : "5", "guard" : 9000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 14000 }, - { "a" : "6", "b" : "7", "guard" : 14000 }, - { "a" : "7", "b" : "5", "guard" : 14000 } - ] - }, - "Skirmish L" : - { - "minSize" : "m+u", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 4 }, - { "min" : 330, "max" : 1000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "allowedMonsters" : [ "castle", "rampart", "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "allowedMonsters" : [ "dungeon" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 60000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 11000 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "5", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "6", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 }, - { "a" : "7", "b" : "5", "guard" : 16000 } - ] - } -} +{ + "Skirmish M" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 7 }, + { "min" : 330, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 40000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 }, + { "min" : 350, "max" : 2000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 9000 }, + { "a" : "3", "b" : "6", "guard" : 9000 }, + { "a" : "3", "b" : "7", "guard" : 9000 }, + { "a" : "4", "b" : "5", "guard" : 9000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 14000 }, + { "a" : "6", "b" : "7", "guard" : 14000 }, + { "a" : "7", "b" : "5", "guard" : 14000 } + ] + }, + "Skirmish L" : + { + "minSize" : "m+u", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 4 }, + { "min" : 330, "max" : 1000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "allowedMonsters" : [ "castle", "rampart", "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "allowedMonsters" : [ "dungeon" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 60000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedMonsters" : [ "inferno", "stronghold", "neutral" ], + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 11000 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "5", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "6", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 }, + { "a" : "7", "b" : "5", "guard" : 16000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON index 6bf29f7b0..7d275252b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/speed1.JSON @@ -1,108 +1,108 @@ -{ - "Speed 1" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6500, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 12 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 7, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "weak", - "neutralTowns" : { "castles" : 2 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand", "subterra" ], - "mines" : { "gold" : 3 }, - "treasure" : - [ - { "min" : 35000, "max" : 55000, "density" : 3 }, - { "min" : 25000, "max" : 35000, "density" : 10 }, - { "min" : 10000, "max" : 25000, "density" : 10 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "7", "guard" : 17500 }, - { "a" : "1", "b" : "7", "guard" : 17500 } - ] - } -} +{ + "Speed 1" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6500, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 12 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 7, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "weak", + "neutralTowns" : { "castles" : 2 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress", "neutral" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand", "subterra" ], + "mines" : { "gold" : 3 }, + "treasure" : + [ + { "min" : 35000, "max" : 55000, "density" : 3 }, + { "min" : 25000, "max" : 35000, "density" : 10 }, + { "min" : 10000, "max" : 25000, "density" : 10 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "7", "guard" : 17500 }, + { "a" : "1", "b" : "7", "guard" : 17500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON index b503b3ae0..804e833a5 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/speed2.JSON @@ -1,93 +1,93 @@ -{ - "Speed 2" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 1000, "max" : 2000, "density" : 5 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "castles" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 1500, "density" : 6 }, - { "min" : 2000, "max" : 3000, "density" : 8 }, - { "min" : 5000, "max" : 9000, "density" : 7 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "neutralTowns" : { "towns" : 2 }, - "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "2", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 4500 }, - { "a" : "1", "b" : "2", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 4500 }, - { "a" : "1", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "Speed 2" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 1000, "max" : 2000, "density" : 5 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "castles" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 1500, "density" : 6 }, + { "min" : 2000, "max" : 3000, "density" : 8 }, + { "min" : 5000, "max" : 9000, "density" : 7 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "neutralTowns" : { "towns" : 2 }, + "allowedTowns" : [ "castle", "rampart", "necropolis", "dungeon", "conflux" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "2", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 4500 }, + { "a" : "1", "b" : "2", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 4500 }, + { "a" : "1", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON b/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON index f7e257f48..ca7125434 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/spider.JSON @@ -1,287 +1,287 @@ -{ - "Spider" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 9999, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 0, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 2 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 2 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 2 }, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 13, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 2 }, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 15, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 16000, "max" : 20000, "density" : 3 }, - { "min" : 10000, "max" : 16000, "density" : 9 }, - { "min" : 6000, "max" : 8999, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "1", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "5", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "16", "guard" : 3000 }, - { "a" : "7", "b" : "11", "guard" : 3000 }, - { "a" : "7", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "15", "guard" : 3000 }, - { "a" : "8", "b" : "12", "guard" : 3000 }, - { "a" : "8", "b" : "14", "guard" : 3000 }, - { "a" : "8", "b" : "16", "guard" : 3000 }, - { "a" : "9", "b" : "17", "guard" : 12500 }, - { "a" : "9", "b" : "17", "guard" : 12500 }, - { "a" : "10", "b" : "18", "guard" : 12500 }, - { "a" : "10", "b" : "18", "guard" : 12500 }, - { "a" : "11", "b" : "19", "guard" : 12500 }, - { "a" : "11", "b" : "19", "guard" : 12500 }, - { "a" : "12", "b" : "20", "guard" : 12500 }, - { "a" : "12", "b" : "20", "guard" : 12500 }, - { "a" : "13", "b" : "24", "guard" : 12500 }, - { "a" : "13", "b" : "24", "guard" : 12500 }, - { "a" : "15", "b" : "23", "guard" : 12500 }, - { "a" : "15", "b" : "23", "guard" : 12500 }, - { "a" : "14", "b" : "21", "guard" : 12500 }, - { "a" : "14", "b" : "21", "guard" : 12500 }, - { "a" : "16", "b" : "22", "guard" : 12500 }, - { "a" : "16", "b" : "22", "guard" : 12500 } - ] - } -} +{ + "Spider" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 9999, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 0, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 2 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 2 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 2 }, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 13, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 2 }, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 15, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 16000, "max" : 20000, "density" : 3 }, + { "min" : 10000, "max" : 16000, "density" : 9 }, + { "min" : 6000, "max" : 8999, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "1", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "5", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "16", "guard" : 3000 }, + { "a" : "7", "b" : "11", "guard" : 3000 }, + { "a" : "7", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "15", "guard" : 3000 }, + { "a" : "8", "b" : "12", "guard" : 3000 }, + { "a" : "8", "b" : "14", "guard" : 3000 }, + { "a" : "8", "b" : "16", "guard" : 3000 }, + { "a" : "9", "b" : "17", "guard" : 12500 }, + { "a" : "9", "b" : "17", "guard" : 12500 }, + { "a" : "10", "b" : "18", "guard" : 12500 }, + { "a" : "10", "b" : "18", "guard" : 12500 }, + { "a" : "11", "b" : "19", "guard" : 12500 }, + { "a" : "11", "b" : "19", "guard" : 12500 }, + { "a" : "12", "b" : "20", "guard" : 12500 }, + { "a" : "12", "b" : "20", "guard" : 12500 }, + { "a" : "13", "b" : "24", "guard" : 12500 }, + { "a" : "13", "b" : "24", "guard" : 12500 }, + { "a" : "15", "b" : "23", "guard" : 12500 }, + { "a" : "15", "b" : "23", "guard" : 12500 }, + { "a" : "14", "b" : "21", "guard" : 12500 }, + { "a" : "14", "b" : "21", "guard" : 12500 }, + { "a" : "16", "b" : "22", "guard" : 12500 }, + { "a" : "16", "b" : "22", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON index 039075c55..6dcd06d10 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/balance m+u 200%.JSON @@ -1,165 +1,165 @@ -{ - "Balance (sc2tv tourney edition)" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], - "treasure" : - [ - { "min" : 100, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 3, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2000, "density" : 3 }, - { "min" : 4000, "max" : 5000, "density" : 6 }, - { "min" : 7000, "max" : 9000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 5000, "density" : 5 }, - { "min" : 8000, "max" : 8500, "density" : 7 }, - { "min" : 8000, "max" : 9500, "density" : 7 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "sulfur" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 800, "max" : 2100, "density" : 5 }, - { "min" : 4000, "max" : 5000, "density" : 5 }, - { "min" : 6000, "max" : 8000, "density" : 7 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle"], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "minesLikeZone" : 7, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "bannedTowns" : ["castle"], - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedTowns" : [ "necropolis", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 9500, "max" : 9900, "density" : 50 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 5000 }, - { "a" : "1", "b" : "7", "guard" : 5000 }, - { "a" : "2", "b" : "8", "guard" : 5000 }, - { "a" : "2", "b" : "9", "guard" : 5000 }, - { "a" : "6", "b" : "3", "guard" : 11000 }, - { "a" : "7", "b" : "4", "guard" : 12000 }, - { "a" : "8", "b" : "3", "guard" : 11000 }, - { "a" : "9", "b" : "4", "guard" : 12000 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "3", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 20000 }, - { "a" : "2", "b" : "5", "guard" : 20000 }, - { "a" : "5", "b" : "10", "guard" : 16000 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 } - ] - } -} +{ + "Balance (sc2tv tourney edition)" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough", "subterra", "lava" ], + "treasure" : + [ + { "min" : 100, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 3, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2000, "density" : 3 }, + { "min" : 4000, "max" : 5000, "density" : 6 }, + { "min" : 7000, "max" : 9000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 5000, "density" : 5 }, + { "min" : 8000, "max" : 8500, "density" : 7 }, + { "min" : 8000, "max" : 9500, "density" : 7 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "sulfur" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 800, "max" : 2100, "density" : 5 }, + { "min" : 4000, "max" : 5000, "density" : 5 }, + { "min" : 6000, "max" : 8000, "density" : 7 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle"], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "minesLikeZone" : 7, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "bannedTowns" : ["castle"], + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedTowns" : [ "necropolis", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 9500, "max" : 9900, "density" : 50 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 5000 }, + { "a" : "1", "b" : "7", "guard" : 5000 }, + { "a" : "2", "b" : "8", "guard" : 5000 }, + { "a" : "2", "b" : "9", "guard" : 5000 }, + { "a" : "6", "b" : "3", "guard" : 11000 }, + { "a" : "7", "b" : "4", "guard" : 12000 }, + { "a" : "8", "b" : "3", "guard" : 11000 }, + { "a" : "9", "b" : "4", "guard" : 12000 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "3", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 20000 }, + { "a" : "2", "b" : "5", "guard" : 20000 }, + { "a" : "5", "b" : "10", "guard" : 16000 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON index 12447741e..11c9a025d 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/skirmish m-u 200%.JSON @@ -1,111 +1,111 @@ -{ - "Skirmish (sc2tv tourney edition)" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 3300, "max" : 3500, "density" : 4 }, - { "min" : 1000, "max" : 2000, "density" : 7 }, - { "min" : 330, "max" : 1000, "density" : 3 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "bannedTowns" : ["necropolis", "conflux"], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 8 }, - { "min" : 1500, "max" : 2000, "density" : 2 }, - { "min" : 330, "max" : 1500, "density" : 5 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "necropolis" ], - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasure" : - [ - { "min" : 10000, "max" : 40000, "density" : 1 }, - { "min" : 8000, "max" : 9150, "density" : 10 }, - { "min" : 350, "max" : 2000, "density" : 3 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 4500 }, - { "a" : "2", "b" : "4", "guard" : 4500 }, - { "a" : "3", "b" : "5", "guard" : 11000 }, - { "a" : "3", "b" : "6", "guard" : 11000 }, - { "a" : "3", "b" : "7", "guard" : 11000 }, - { "a" : "4", "b" : "5", "guard" : 11000 }, - { "a" : "4", "b" : "6", "guard" : 11000 }, - { "a" : "4", "b" : "7", "guard" : 11000 }, - { "a" : "5", "b" : "6", "guard" : 16000 }, - { "a" : "6", "b" : "7", "guard" : 16000 }, - { "a" : "7", "b" : "5", "guard" : 16000 } - ] - } -} +{ + "Skirmish (sc2tv tourney edition)" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 3300, "max" : 3500, "density" : 4 }, + { "min" : 1000, "max" : 2000, "density" : 7 }, + { "min" : 330, "max" : 1000, "density" : 3 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "bannedTowns" : ["necropolis", "conflux"], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 8 }, + { "min" : 1500, "max" : 2000, "density" : 2 }, + { "min" : 330, "max" : 1500, "density" : 5 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "necropolis" ], + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasure" : + [ + { "min" : 10000, "max" : 40000, "density" : 1 }, + { "min" : 8000, "max" : 9150, "density" : 10 }, + { "min" : 350, "max" : 2000, "density" : 3 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 4500 }, + { "a" : "2", "b" : "4", "guard" : 4500 }, + { "a" : "3", "b" : "5", "guard" : 11000 }, + { "a" : "3", "b" : "6", "guard" : 11000 }, + { "a" : "3", "b" : "7", "guard" : 11000 }, + { "a" : "4", "b" : "5", "guard" : 11000 }, + { "a" : "4", "b" : "6", "guard" : 11000 }, + { "a" : "4", "b" : "7", "guard" : 11000 }, + { "a" : "5", "b" : "6", "guard" : 16000 }, + { "a" : "6", "b" : "7", "guard" : 16000 }, + { "a" : "7", "b" : "5", "guard" : 16000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON index 1457bf7ea..83b828e4b 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/true random.JSON @@ -1,708 +1,708 @@ -{ - "True Random" : - { - "minSize" : "l", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 6 }, - { "min" : 4000, "max" : 5500, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 7 }, - { "min" : 15000, "max" : 18000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 7 }, - { "min" : 50000, "max" : 70000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "normal", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 7 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 12 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 3 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 12 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 8, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "12", "guard" : 3000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 8000 }, - { "a" : "10", "b" : "14", "guard" : 8000 }, - { "a" : "13", "b" : "14", "guard" : 12000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - }, - "True Random_2" : - { - "minSize" : "xl", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 4000, "max" : 5500, "density" : 5 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 15000, "max" : 18000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 60000, "max" : 80000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 4 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 4000 }, - { "a" : "2", "b" : "12", "guard" : 4000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 9000 }, - { "a" : "10", "b" : "14", "guard" : 9000 }, - { "a" : "13", "b" : "14", "guard" : 14000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - }, - "True Random_3" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 5 }, - { "min" : 4000, "max" : 5500, "density" : 5 }, - { "min" : 800, "max" : 800, "density" : 1 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 6, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8000, "max" : 10500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "sulfur" : 1, "crystal" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 15000, "max" : 18000, "density" : 2 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "mines" : { "mercury" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 350, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 8500, "density" : 5 }, - { "min" : 70000, "max" : 100000, "density" : 1 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 3, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 3, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasure" : - [ - { "min" : 800, "max" : 1000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 8500, "max" : 9500, "density" : 1 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 2, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "terrainTypeLikeZone" : 9, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 1000, "max" : 2000, "density" : 2 }, - { "min" : 4000, "max" : 7000, "density" : 5 }, - { "min" : 15000, "max" : 20000, "density" : 2 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 10000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "16" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "allowedMonsters" : [ "neutral" ], - "matchTerrainToTown" : false, - "mines" : { "gold" : 5 }, - "treasure" : - [ - { "min" : 8000, "max" : 25000, "density" : 2 }, - { "min" : 30000, "max" : 100000, "density" : 7 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "17" : - { - "type" : "treasure", - "size" : 4, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 15 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 4500 }, - { "a" : "2", "b" : "12", "guard" : 4500 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 5500 }, - { "a" : "1", "b" : "7", "guard" : 6500 }, - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 5500 }, - { "a" : "2", "b" : "8", "guard" : 6500 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "13", "guard" : 9000 }, - { "a" : "10", "b" : "14", "guard" : 9000 }, - { "a" : "13", "b" : "14", "guard" : 15000 }, - { "a" : "5", "b" : "15", "guard" : 18000 }, - { "a" : "6", "b" : "15", "guard" : 18000 }, - { "a" : "3", "b" : "17", "guard" : 19000 }, - { "a" : "4", "b" : "17", "guard" : 19000 }, - { "a" : "7", "b" : "17", "guard" : 20000 }, - { "a" : "8", "b" : "17", "guard" : 20000 }, - { "a" : "15", "b" : "16", "guard" : 30000 }, - { "a" : "17", "b" : "16", "guard" : 30000 } - ] - } -} +{ + "True Random" : + { + "minSize" : "l", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 6 }, + { "min" : 4000, "max" : 5500, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 7 }, + { "min" : 15000, "max" : 18000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 7 }, + { "min" : 50000, "max" : 70000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "normal", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 7 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 12 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 3 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 12 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 8, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "12", "guard" : 3000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 8000 }, + { "a" : "10", "b" : "14", "guard" : 8000 }, + { "a" : "13", "b" : "14", "guard" : 12000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + }, + "True Random_2" : + { + "minSize" : "xl", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 4000, "max" : 5500, "density" : 5 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 15000, "max" : 18000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 60000, "max" : 80000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 4 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 4000 }, + { "a" : "2", "b" : "12", "guard" : 4000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 9000 }, + { "a" : "10", "b" : "14", "guard" : 9000 }, + { "a" : "13", "b" : "14", "guard" : 14000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + }, + "True Random_3" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 5 }, + { "min" : 4000, "max" : 5500, "density" : 5 }, + { "min" : 800, "max" : 800, "density" : 1 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 6, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "sand", "grass", "rough", "subterra", "lava" ], + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8000, "max" : 10500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "sulfur" : 1, "crystal" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 15000, "max" : 18000, "density" : 2 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "mines" : { "mercury" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 350, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 8500, "density" : 5 }, + { "min" : 70000, "max" : 100000, "density" : 1 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 3, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra", "lava" ], + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 3, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasure" : + [ + { "min" : 800, "max" : 1000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 8500, "max" : 9500, "density" : 1 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 2, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "terrainTypeLikeZone" : 9, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 1000, "max" : 2000, "density" : 2 }, + { "min" : 4000, "max" : 7000, "density" : 5 }, + { "min" : 15000, "max" : 20000, "density" : 2 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 10000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "16" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "allowedMonsters" : [ "neutral" ], + "matchTerrainToTown" : false, + "mines" : { "gold" : 5 }, + "treasure" : + [ + { "min" : 8000, "max" : 25000, "density" : 2 }, + { "min" : 30000, "max" : 100000, "density" : 7 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "17" : + { + "type" : "treasure", + "size" : 4, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 15 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 4500 }, + { "a" : "2", "b" : "12", "guard" : 4500 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 5500 }, + { "a" : "1", "b" : "7", "guard" : 6500 }, + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 5500 }, + { "a" : "2", "b" : "8", "guard" : 6500 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "13", "guard" : 9000 }, + { "a" : "10", "b" : "14", "guard" : 9000 }, + { "a" : "13", "b" : "14", "guard" : 15000 }, + { "a" : "5", "b" : "15", "guard" : 18000 }, + { "a" : "6", "b" : "15", "guard" : 18000 }, + { "a" : "3", "b" : "17", "guard" : 19000 }, + { "a" : "4", "b" : "17", "guard" : 19000 }, + { "a" : "7", "b" : "17", "guard" : 20000 }, + { "a" : "8", "b" : "17", "guard" : 20000 }, + { "a" : "15", "b" : "16", "guard" : 30000 }, + { "a" : "17", "b" : "16", "guard" : 30000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON index 57849e428..8a4802b01 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/dwarvenTunnels.JSON @@ -1,226 +1,226 @@ -{ - "Dwarven Tunnels" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "towns" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "9" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "12" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 8 - }, - "15" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "16" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "junction", - "size" : 25, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 8 - }, - "18" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 9000 }, - { "a" : "2", "b" : "8", "guard" : 9000 }, - { "a" : "3", "b" : "9", "guard" : 9000 }, - { "a" : "4", "b" : "17", "guard" : 9000 }, - - { "a" : "12", "b" : "5", "guard" : 9000 }, - { "a" : "12", "b" : "9", "guard" : 12500 }, - { "a" : "12", "b" : "11", "guard" : 12500 }, - { "a" : "12", "b" : "13", "guard" : 15000 }, - { "a" : "12", "b" : "16", "guard" : 15000 }, - { "a" : "12", "b" : "17", "guard" : 12500 }, - { "a" : "12", "b" : "18", "guard" : 12500 }, - - { "a" : "14", "b" : "6", "guard" : 9000 }, - { "a" : "14", "b" : "7", "guard" : 9000 }, - { "a" : "14", "b" : "11", "guard" : 12500 }, - { "a" : "14", "b" : "15", "guard" : 12500 }, - - { "a" : "8", "b" : "18", "guard" : 12500 }, - { "a" : "11", "b" : "18", "guard" : 12500 }, - - { "a" : "8", "b" : "9", "guard" : 12500 }, - { "a" : "8", "b" : "11", "guard" : 12500 }, - { "a" : "15", "b" : "11", "guard" : 12500 }, - { "a" : "15", "b" : "17", "guard" : 12500 }, - { "a" : "10", "b" : "11", "guard" : 15000 } - ] - } -} +{ + "Dwarven Tunnels" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "towns" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "9" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "12" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 8 + }, + "15" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "16" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "junction", + "size" : 25, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 8 + }, + "18" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 9000 }, + { "a" : "2", "b" : "8", "guard" : 9000 }, + { "a" : "3", "b" : "9", "guard" : 9000 }, + { "a" : "4", "b" : "17", "guard" : 9000 }, + + { "a" : "12", "b" : "5", "guard" : 9000 }, + { "a" : "12", "b" : "9", "guard" : 12500 }, + { "a" : "12", "b" : "11", "guard" : 12500 }, + { "a" : "12", "b" : "13", "guard" : 15000 }, + { "a" : "12", "b" : "16", "guard" : 15000 }, + { "a" : "12", "b" : "17", "guard" : 12500 }, + { "a" : "12", "b" : "18", "guard" : 12500 }, + + { "a" : "14", "b" : "6", "guard" : 9000 }, + { "a" : "14", "b" : "7", "guard" : 9000 }, + { "a" : "14", "b" : "11", "guard" : 12500 }, + { "a" : "14", "b" : "15", "guard" : 12500 }, + + { "a" : "8", "b" : "18", "guard" : 12500 }, + { "a" : "11", "b" : "18", "guard" : 12500 }, + + { "a" : "8", "b" : "9", "guard" : 12500 }, + { "a" : "8", "b" : "11", "guard" : 12500 }, + { "a" : "15", "b" : "11", "guard" : 12500 }, + { "a" : "15", "b" : "17", "guard" : 12500 }, + { "a" : "10", "b" : "11", "guard" : 15000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON index bada8c1cf..84ff216ce 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/golemsAplenty.JSON @@ -1,109 +1,109 @@ -{ - "Golems Aplenty" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 1000, "max" : 4000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "allowedTowns" : [ "tower" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "snow" ], - "treasure" : - [ - { "min" : 3000, "max" : 9000, "density" : 10 }, - { "min" : 5000, "max" : 18000, "density" : 6 }, - { "min" : 9000, "max" : 60000, "density" : 4 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "subterra" ], - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "terrainTypeLikeZone" : 6, - "treasureLikeZone" : 6 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 8000 }, - { "a" : "1", "b" : "6", "guard" : 10000 }, - { "a" : "2", "b" : "5", "guard" : 8000 }, - { "a" : "3", "b" : "5", "guard" : 8000 }, - { "a" : "3", "b" : "7", "guard" : 10000 }, - { "a" : "4", "b" : "5", "guard" : 8000 }, - { "a" : "5", "b" : "6", "guard" : 7000 }, - { "a" : "5", "b" : "7", "guard" : 7000 } - ] - } -} +{ + "Golems Aplenty" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 1000, "max" : 4000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "allowedTowns" : [ "tower" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "snow" ], + "treasure" : + [ + { "min" : 3000, "max" : 9000, "density" : 10 }, + { "min" : 5000, "max" : 18000, "density" : 6 }, + { "min" : 9000, "max" : 60000, "density" : 4 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "subterra" ], + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "terrainTypeLikeZone" : 6, + "treasureLikeZone" : 6 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 8000 }, + { "a" : "1", "b" : "6", "guard" : 10000 }, + { "a" : "2", "b" : "5", "guard" : 8000 }, + { "a" : "3", "b" : "5", "guard" : 8000 }, + { "a" : "3", "b" : "7", "guard" : 10000 }, + { "a" : "4", "b" : "5", "guard" : 8000 }, + { "a" : "5", "b" : "6", "guard" : 7000 }, + { "a" : "5", "b" : "7", "guard" : 7000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON index 87e64680c..e78b47d5c 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/monksRetreat.JSON @@ -1,139 +1,139 @@ -{ - "Monk's Retreat" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 2, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "lava" ], - "mines" : { "wood" : 1, "mercury" : 1, "gold" : 2 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 10 }, - { "min" : 6000, "max" : 9000, "density" : 8 }, - { "min" : 9000, "max" : 30000, "density" : 3 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 35, - "monsters" : "strong", - "neutralTowns" : { "towns" : 2 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "swamp" ], - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 6000 }, - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - - { "a" : "1", "b" : "9", "guard" : 18000 }, - { "a" : "2", "b" : "9", "guard" : 18000 }, - { "a" : "3", "b" : "8", "guard" : 18000 }, - { "a" : "4", "b" : "8", "guard" : 18000 }, - { "a" : "5", "b" : "7", "guard" : 18000 }, - { "a" : "6", "b" : "7", "guard" : 18000 }, - - { "a" : "8", "b" : "7", "guard" : 12000 }, - { "a" : "8", "b" : "9", "guard" : 12000 } - ] - } -} +{ + "Monk's Retreat" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 2, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "lava" ], + "mines" : { "wood" : 1, "mercury" : 1, "gold" : 2 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 10 }, + { "min" : 6000, "max" : 9000, "density" : 8 }, + { "min" : 9000, "max" : 30000, "density" : 3 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 35, + "monsters" : "strong", + "neutralTowns" : { "towns" : 2 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "swamp" ], + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 6000 }, + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + + { "a" : "1", "b" : "9", "guard" : 18000 }, + { "a" : "2", "b" : "9", "guard" : 18000 }, + { "a" : "3", "b" : "8", "guard" : 18000 }, + { "a" : "4", "b" : "8", "guard" : 18000 }, + { "a" : "5", "b" : "7", "guard" : 18000 }, + { "a" : "6", "b" : "7", "guard" : 18000 }, + + { "a" : "8", "b" : "7", "guard" : 12000 }, + { "a" : "8", "b" : "9", "guard" : 12000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON index 1c18e5574..337e38523 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON @@ -1,177 +1,177 @@ -{ - "Newcomers" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-3", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 6000, "max" : 15000, "density" : 6 }, - { "min" : 15000, "max" : 20000, "density" : 1 } - ] - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - } - }, - "connections" : - [ - { "a" : "12", "b" : "1", "guard" : 6000 }, - { "a" : "12", "b" : "3", "guard" : 6000 }, - { "a" : "12", "b" : "7", "guard" : 6000 }, - { "a" : "12", "b" : "9", "guard" : 6000 }, - - { "a" : "13", "b" : "2", "guard" : 6000 }, - { "a" : "13", "b" : "5", "guard" : 6000 }, - { "a" : "13", "b" : "7", "guard" : 6000 }, - { "a" : "13", "b" : "8", "guard" : 6000 }, - { "a" : "13", "b" : "10", "guard" : 6000 }, - - { "a" : "14", "b" : "4", "guard" : 6000 }, - { "a" : "14", "b" : "6", "guard" : 6000 }, - { "a" : "14", "b" : "8", "guard" : 6000 }, - { "a" : "14", "b" : "11", "guard" : 6000 } - ] - } -} +{ + "Newcomers" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-3", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 6000, "max" : 15000, "density" : 6 }, + { "min" : 15000, "max" : 20000, "density" : 1 } + ] + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + } + }, + "connections" : + [ + { "a" : "12", "b" : "1", "guard" : 6000 }, + { "a" : "12", "b" : "3", "guard" : 6000 }, + { "a" : "12", "b" : "7", "guard" : 6000 }, + { "a" : "12", "b" : "9", "guard" : 6000 }, + + { "a" : "13", "b" : "2", "guard" : 6000 }, + { "a" : "13", "b" : "5", "guard" : 6000 }, + { "a" : "13", "b" : "7", "guard" : 6000 }, + { "a" : "13", "b" : "8", "guard" : 6000 }, + { "a" : "13", "b" : "10", "guard" : 6000 }, + + { "a" : "14", "b" : "4", "guard" : 6000 }, + { "a" : "14", "b" : "6", "guard" : 6000 }, + { "a" : "14", "b" : "8", "guard" : 6000 }, + { "a" : "14", "b" : "11", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON index f2ac532b6..2f41adc27 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/smallRing.JSON @@ -1,110 +1,110 @@ -{ - "Small Ring" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 4500 }, - { "a" : "2", "b" : "3", "guard" : 4500 }, - { "a" : "3", "b" : "4", "guard" : 4500 }, - { "a" : "4", "b" : "5", "guard" : 4500 }, - { "a" : "5", "b" : "6", "guard" : 4500 }, - { "a" : "6", "b" : "7", "guard" : 4500 }, - { "a" : "7", "b" : "8", "guard" : 4500 }, - { "a" : "8", "b" : "1", "guard" : 4500 }, - { "a" : "4", "b" : "1", "guard" : 4500 }, - { "a" : "5", "b" : "1", "guard" : 4500 }, - { "a" : "6", "b" : "1", "guard" : 4500 }, - { "a" : "7", "b" : "1", "guard" : 4500 } - ] - } -} +{ + "Small Ring" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 4500 }, + { "a" : "2", "b" : "3", "guard" : 4500 }, + { "a" : "3", "b" : "4", "guard" : 4500 }, + { "a" : "4", "b" : "5", "guard" : 4500 }, + { "a" : "5", "b" : "6", "guard" : 4500 }, + { "a" : "6", "b" : "7", "guard" : 4500 }, + { "a" : "7", "b" : "8", "guard" : 4500 }, + { "a" : "8", "b" : "1", "guard" : 4500 }, + { "a" : "4", "b" : "1", "guard" : 4500 }, + { "a" : "5", "b" : "1", "guard" : 4500 }, + { "a" : "6", "b" : "1", "guard" : 4500 }, + { "a" : "7", "b" : "1", "guard" : 4500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON index 2be3522d0..053516b20 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON @@ -1,165 +1,165 @@ -{ - "South of Hell" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 35, - "owner" : 3, - "monsters" : "strong", - "playerTowns" : { "towns" : 2, "castles" : 3 }, - "allowedTowns" : [ "inferno" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "lava" ], - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 12 }, - { "min" : 3000, "max" : 9000, "density" : 5 }, - { "min" : 9000, "max" : 30000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 1000, "max" : 4000, "density" : 12 }, - { "min" : 4000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 40000, "density" : 1 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 4 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 2 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "9" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 3 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "3", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 8000 }, - { "a" : "2", "b" : "9", "guard" : 8000 }, - { "a" : "3", "b" : "10", "guard" : 10000 }, - { "a" : "8", "b" : "11", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "7", "guard" : 6000 } - ] - } -} +{ + "South of Hell" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 35, + "owner" : 3, + "monsters" : "strong", + "playerTowns" : { "towns" : 2, "castles" : 3 }, + "allowedTowns" : [ "inferno" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "lava" ], + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 12 }, + { "min" : 3000, "max" : 9000, "density" : 5 }, + { "min" : 9000, "max" : 30000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 1000, "max" : 4000, "density" : 12 }, + { "min" : 4000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 40000, "density" : 1 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 4 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 2 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "9" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 3 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "3", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 8000 }, + { "a" : "2", "b" : "9", "guard" : 8000 }, + { "a" : "3", "b" : "10", "guard" : 10000 }, + { "a" : "8", "b" : "11", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "7", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON index 6df0e8501..3e63bdc95 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/ring.JSON @@ -1,205 +1,205 @@ -{ - "Ring" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "rough", "subterra" ], - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 25000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 5 }, - { "min" : 500, "max" : 3000, "density" : 7 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 4500, "max" : 7500, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 3 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "16" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "type": "wide" }, - { "a" : "1", "b" : "3", "guard" : 0 }, - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "1", "b" : "5", "guard" : 2000 }, - { "a" : "1", "b" : "6", "guard" : 2000 }, - { "a" : "1", "b" : "9", "guard" : 2000 }, - { "a" : "1", "b" : "10", "guard" : 2000 }, - { "a" : "1", "b" : "13", "guard" : 2000 }, - { "a" : "1", "b" : "14", "guard" : 2000 }, - { "a" : "2", "b" : "3", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "5", "guard" : 2000 }, - { "a" : "5", "b" : "6", "type": "wide" }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "5", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "6", "b" : "9", "guard" : 2000 }, - { "a" : "9", "b" : "10", "type": "wide" }, - { "a" : "9", "b" : "11", "guard" : 0 }, - { "a" : "9", "b" : "12", "guard" : 0 }, - { "a" : "10", "b" : "11", "guard" : 0 }, - { "a" : "10", "b" : "12", "guard" : 0 }, - { "a" : "10", "b" : "13", "guard" : 2000 }, - { "a" : "13", "b" : "14", "type": "wide" }, - { "a" : "13", "b" : "15", "guard" : 0 }, - { "a" : "13", "b" : "16", "guard" : 0 }, - { "a" : "14", "b" : "15", "guard" : 0 }, - { "a" : "14", "b" : "16", "guard" : 0 } - ] - } -} +{ + "Ring" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "rough", "subterra" ], + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 25000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 5 }, + { "min" : 500, "max" : 3000, "density" : 7 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 4500, "max" : 7500, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 3 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "16" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "type": "wide" }, + { "a" : "1", "b" : "3", "guard" : 0 }, + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "1", "b" : "5", "guard" : 2000 }, + { "a" : "1", "b" : "6", "guard" : 2000 }, + { "a" : "1", "b" : "9", "guard" : 2000 }, + { "a" : "1", "b" : "10", "guard" : 2000 }, + { "a" : "1", "b" : "13", "guard" : 2000 }, + { "a" : "1", "b" : "14", "guard" : 2000 }, + { "a" : "2", "b" : "3", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "5", "guard" : 2000 }, + { "a" : "5", "b" : "6", "type": "wide" }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "5", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "6", "b" : "9", "guard" : 2000 }, + { "a" : "9", "b" : "10", "type": "wide" }, + { "a" : "9", "b" : "11", "guard" : 0 }, + { "a" : "9", "b" : "12", "guard" : 0 }, + { "a" : "10", "b" : "11", "guard" : 0 }, + { "a" : "10", "b" : "12", "guard" : 0 }, + { "a" : "10", "b" : "13", "guard" : 2000 }, + { "a" : "13", "b" : "14", "type": "wide" }, + { "a" : "13", "b" : "15", "guard" : 0 }, + { "a" : "13", "b" : "16", "guard" : 0 }, + { "a" : "14", "b" : "15", "guard" : 0 }, + { "a" : "14", "b" : "16", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON index 7c193b03a..0616fc083 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/riseOfPhoenix.JSON @@ -1,194 +1,194 @@ -{ - "Rise of Phoenix" : - { - "minSize" : "l", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "dirt", "grass", "snow", "rough" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 20000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 8 }, - { "min" : 6000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 2 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 9000, "density" : 8 }, - { "min" : 9000, "max" : 18000, "density" : 3 }, - { "min" : 12000, "max" : 30000, "density" : 2 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "junction", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "inferno", "stronghold" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "rough", "lava" ], - "treasure" : - [ - { "min" : 9000, "max" : 18000, "density" : 3 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "allowedMonsters" : [ "inferno" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "lava" ], - "treasure" : - [ - { "min" : 25000, "max" : 40000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 12000 }, - { "a" : "2", "b" : "6", "guard" : 12000 }, - { "a" : "3", "b" : "7", "guard" : 12000 }, - { "a" : "4", "b" : "8", "guard" : 12000 }, - - { "a" : "5", "b" : "10", "guard" : 9000 }, - { "a" : "6", "b" : "11", "guard" : 9000 }, - { "a" : "7", "b" : "12", "guard" : 9000 }, - { "a" : "8", "b" : "9", "guard" : 9000 }, - - { "a" : "9", "b" : "13", "guard" : 20000 }, - { "a" : "10", "b" : "13", "guard" : 20000 }, - { "a" : "11", "b" : "13", "guard" : 20000 }, - { "a" : "12", "b" : "13", "guard" : 12000 }, - { "a" : "13", "b" : "14", "guard" : 24000 } - ] - } -} +{ + "Rise of Phoenix" : + { + "minSize" : "l", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "dirt", "grass", "snow", "rough" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 20000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 8 }, + { "min" : 6000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 2 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 9000, "density" : 8 }, + { "min" : 9000, "max" : 18000, "density" : 3 }, + { "min" : 12000, "max" : 30000, "density" : 2 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "junction", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "inferno", "stronghold" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "rough", "lava" ], + "treasure" : + [ + { "min" : 9000, "max" : 18000, "density" : 3 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "allowedMonsters" : [ "inferno" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "lava" ], + "treasure" : + [ + { "min" : 25000, "max" : 40000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 12000 }, + { "a" : "2", "b" : "6", "guard" : 12000 }, + { "a" : "3", "b" : "7", "guard" : 12000 }, + { "a" : "4", "b" : "8", "guard" : 12000 }, + + { "a" : "5", "b" : "10", "guard" : 9000 }, + { "a" : "6", "b" : "11", "guard" : 9000 }, + { "a" : "7", "b" : "12", "guard" : 9000 }, + { "a" : "8", "b" : "9", "guard" : 9000 }, + + { "a" : "9", "b" : "13", "guard" : 20000 }, + { "a" : "10", "b" : "13", "guard" : 20000 }, + { "a" : "11", "b" : "13", "guard" : 20000 }, + { "a" : "12", "b" : "13", "guard" : 12000 }, + { "a" : "13", "b" : "14", "guard" : 24000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON index 5a8c3ca18..764d31a7a 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON @@ -1,105 +1,105 @@ -{ - "2SM0k" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "type": "wide" }, - { "a" : "1", "b" : "7", "type": "wide" }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "type": "wide" }, - { "a" : "2", "b" : "8", "type": "wide" }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM0k" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "cpu" : "6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "type": "wide" }, + { "a" : "1", "b" : "7", "type": "wide" }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "type": "wide" }, + { "a" : "2", "b" : "8", "type": "wide" }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON index b0baf73d1..20bc458c2 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2a.JSON @@ -1,67 +1,67 @@ -{ - "2SM2a" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 12500 }, - { "a" : "2", "b" : "3", "guard" : 12500 } - ] - } -} +{ + "2SM2a" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 12500 }, + { "a" : "2", "b" : "3", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON index 5dd44d974..104baeabc 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b(2).JSON @@ -1,87 +1,87 @@ -{ - "2SM2b(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 12500 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 12500 }, - { "a" : "2", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "2SM2b(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 12500 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 12500 }, + { "a" : "2", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON index cb7aecb4b..575860e39 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2b.JSON @@ -1,87 +1,87 @@ -{ - "2SM2b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 12500 }, - { "a" : "2", "b" : "3", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "2SM2b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 12500 }, + { "a" : "2", "b" : "3", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON index 9dbe1aa17..8f153ed12 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2c.JSON @@ -1,96 +1,96 @@ -{ - "2SM2c" : - { - "minSize" : "s", "maxSize" : "s+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 12500 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 12500 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2c" : + { + "minSize" : "s", "maxSize" : "s+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 12500 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 12500 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON index 9f2997591..946ca05d7 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f(2).JSON @@ -1,86 +1,86 @@ -{ - "2SM2f(2)" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2f(2)" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON index 5acae7bfd..35f06217f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2f.JSON @@ -1,84 +1,84 @@ -{ - "2SM2f" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "2SM2f" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON index bd98c40a2..6acaba717 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h(2).JSON @@ -1,80 +1,80 @@ -{ - "2SM2h(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2h(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON index acda62106..e40139fa5 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2h.JSON @@ -1,77 +1,77 @@ -{ - "2SM2h" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2h" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "mines" : { "wood" : 1, "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index b93afd0c0..6dc559750 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -1,75 +1,75 @@ -{ - "2SM2i(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 } - ] - } -} +{ + "2SM2i(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON index 0ffee6aab..818386a00 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i.JSON @@ -1,76 +1,76 @@ -{ - "2SM2i" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 6000 } - ] - } -} +{ + "2SM2i" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON index 49ad8bc09..872fe17da 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(2).JSON @@ -1,134 +1,134 @@ -{ - "2SM4d(2)" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d(2)" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON index 52d4e8c2d..01ee4ad1f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d(3).JSON @@ -1,134 +1,134 @@ -{ - "2SM4d(3)" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d(3)" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON index 566664a2a..1959a32e0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm4d.JSON @@ -1,134 +1,134 @@ -{ - "2SM4d" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 45000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "10", "b" : "7", "guard" : 6000 }, - { "a" : "10", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM4d" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 45000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "10", "b" : "7", "guard" : 6000 }, + { "a" : "10", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON index 61b8fd0aa..e3f7174b3 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON @@ -1,105 +1,105 @@ -{ - "3SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "3SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON index 47647e043..08909bd9b 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON @@ -1,123 +1,123 @@ -{ - "3SB0c" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "7", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "8", "b" : "6", "guard" : 3000 }, - { "a" : "8", "b" : "10", "guard" : 3000 }, - { "a" : "7", "b" : "9", "guard" : 3000 }, - { "a" : "9", "b" : "10", "guard" : 3000 } - ] - } -} +{ + "3SB0c" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "7", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "8", "b" : "6", "guard" : 3000 }, + { "a" : "8", "b" : "10", "guard" : 3000 }, + { "a" : "7", "b" : "9", "guard" : 3000 }, + { "a" : "9", "b" : "10", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON index a4c75f4be..740da0b96 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON @@ -1,164 +1,164 @@ -{ - "3SM3d" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-3", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 25, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 100, "max" : 1000, "density" : 10 }, - { "min" : 1000, "max" : 3000, "density" : 1 } - ] - }, - "12" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - }, - "14" : - { - "type" : "junction", - "size" : 25, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 11 - } - }, - "connections" : - [ - { "a" : "1", "b" : "11", "guard" : 3000 }, - { "a" : "1", "b" : "12", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "8", "b" : "14", "guard" : 3000 }, - { "a" : "9", "b" : "13", "guard" : 3000 }, - { "a" : "10", "b" : "14", "guard" : 3000 } - ] - } -} +{ + "3SM3d" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-3", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 25, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 100, "max" : 1000, "density" : 10 }, + { "min" : 1000, "max" : 3000, "density" : 1 } + ] + }, + "12" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + }, + "14" : + { + "type" : "junction", + "size" : 25, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 11 + } + }, + "connections" : + [ + { "a" : "1", "b" : "11", "guard" : 3000 }, + { "a" : "1", "b" : "12", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "8", "b" : "14", "guard" : 3000 }, + { "a" : "9", "b" : "13", "guard" : 3000 }, + { "a" : "10", "b" : "14", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON index cd41be625..fb4cb46c5 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0d.JSON @@ -1,120 +1,120 @@ -{ - "4SM0d" : - { - "minSize" : "s", "maxSize" : "l+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "4SM0d" : + { + "minSize" : "s", "maxSize" : "l+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON index 26216b071..b443fa584 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0f.JSON @@ -1,107 +1,107 @@ -{ - "4SM0f" : - { - "minSize" : "s", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "4SM0f" : + { + "minSize" : "s", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON index 795f9dd98..85ef75807 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm0g.JSON @@ -1,111 +1,111 @@ -{ - "4SM0g" : - { - "minSize" : "s", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 0 }, - { "a" : "5", "b" : "7", "guard" : 0 }, - { "a" : "6", "b" : "8", "guard" : 0 }, - { "a" : "7", "b" : "8", "guard" : 0 } - ] - } -} +{ + "4SM0g" : + { + "minSize" : "s", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 0 }, + { "a" : "5", "b" : "7", "guard" : 0 }, + { "a" : "6", "b" : "8", "guard" : 0 }, + { "a" : "7", "b" : "8", "guard" : 0 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON index a425806e6..0a9a38af3 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/4sm4e.JSON @@ -1,150 +1,150 @@ -{ - "4SM4e" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 6 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 6 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "5", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "7", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "10", "guard" : 6000 }, - { "a" : "10", "b" : "11", "guard" : 6000 }, - { "a" : "9", "b" : "12", "guard" : 6000 }, - { "a" : "11", "b" : "12", "guard" : 6000 }, - { "a" : "6", "b" : "8", "guard" : 6000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "8", "b" : "11", "guard" : 6000 }, - { "a" : "9", "b" : "11", "guard" : 6000 } - ] - } -} +{ + "4SM4e" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 6 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 6 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "5", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "7", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "10", "guard" : 6000 }, + { "a" : "10", "b" : "11", "guard" : 6000 }, + { "a" : "9", "b" : "12", "guard" : 6000 }, + { "a" : "11", "b" : "12", "guard" : 6000 }, + { "a" : "6", "b" : "8", "guard" : 6000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "8", "b" : "11", "guard" : 6000 }, + { "a" : "9", "b" : "11", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON index 774a10cfa..905ebb6bd 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON @@ -1,121 +1,121 @@ -{ - "5SB0a" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 4 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "8", "guard" : 9000 }, - { "a" : "5", "b" : "9", "guard" : 9000 } - ] - } -} +{ + "5SB0a" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "cpu" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 4 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "8", "guard" : 9000 }, + { "a" : "5", "b" : "9", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON index cae17f7f8..96197f7c0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON @@ -1,97 +1,97 @@ -{ - "5SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 } - ] - } -} +{ + "5SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "cpu" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON index 4eb00d2d0..6ed8fcbb8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10.JSON @@ -1,306 +1,306 @@ -{ - "6LM10" : - { - "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "junction", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "15", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - { "a" : "21", "b" : "22", "type": "wide" }, - { "a" : "24", "b" : "25", "type": "wide" } - ] - } -} +{ + "6LM10" : + { + "minSize" : "l", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "junction", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "15", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + { "a" : "21", "b" : "22", "type": "wide" }, + { "a" : "24", "b" : "25", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON index b0d44cf41..1df537fd3 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6lm10a.JSON @@ -1,317 +1,317 @@ -{ - "6LM10a" : - { - "minSize" : "xl+u", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 8, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - { "a" : "16", "b" : "25", "guard" : 6000 }, - - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "13", "b" : "14", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "14", "b" : "25", "guard" : 6000 }, - - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - - { "a" : "21", "b" : "22", "type": "wide" }, - { "a" : "24", "b" : "25", "type": "wide" } - ] - } -} +{ + "6LM10a" : + { + "minSize" : "xl+u", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 8, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + { "a" : "16", "b" : "25", "guard" : 6000 }, + + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "13", "b" : "14", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "14", "b" : "25", "guard" : 6000 }, + + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + + { "a" : "21", "b" : "22", "type": "wide" }, + { "a" : "24", "b" : "25", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON index dab2c71b1..c7813cc65 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0b.JSON @@ -1,132 +1,132 @@ -{ - "6SM0b" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "4", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 } - ] - } -} +{ + "6SM0b" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "4", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON index ab7ae0c6a..911a70020 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0d.JSON @@ -1,85 +1,85 @@ -{ - "6SM0d" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "6SM0d" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON index a8ffe28ba..ae12f4d89 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/6sm0e.JSON @@ -1,175 +1,175 @@ -{ - "6SM0e" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "13", "b" : "14", "guard" : 3000 } - ] - } -} +{ + "6SM0e" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "13", "b" : "14", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON index eb3c34ba6..854f2a371 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON @@ -1,105 +1,105 @@ -{ - "7SB0b" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "4", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0b" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2-6", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "4", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index 978ea9453..3729a33eb 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -1,101 +1,101 @@ -{ - "7SB0c" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0c" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-6", "cpu" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON index 14f30b43b..6bdf4b3a8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm0e.JSON @@ -1,128 +1,128 @@ -{ - "8MM0e" : - { - "minSize" : "m", "maxSize" : "m+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "7", "guard" : 3000 }, - { "a" : "8", "b" : "1", "guard" : 3000 }, - { "a" : "9", "b" : "1", "guard" : 6000 }, - { "a" : "9", "b" : "2", "guard" : 6000 }, - { "a" : "9", "b" : "3", "guard" : 6000 }, - { "a" : "9", "b" : "4", "guard" : 6000 }, - { "a" : "9", "b" : "5", "guard" : 6000 }, - { "a" : "9", "b" : "6", "guard" : 6000 }, - { "a" : "9", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 }, - { "a" : "9", "b" : "7", "guard" : 6000 }, - { "a" : "9", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "8MM0e" : + { + "minSize" : "m", "maxSize" : "m+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "7", "guard" : 3000 }, + { "a" : "8", "b" : "1", "guard" : 3000 }, + { "a" : "9", "b" : "1", "guard" : 6000 }, + { "a" : "9", "b" : "2", "guard" : 6000 }, + { "a" : "9", "b" : "3", "guard" : 6000 }, + { "a" : "9", "b" : "4", "guard" : 6000 }, + { "a" : "9", "b" : "5", "guard" : 6000 }, + { "a" : "9", "b" : "6", "guard" : 6000 }, + { "a" : "9", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 }, + { "a" : "9", "b" : "7", "guard" : 6000 }, + { "a" : "9", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON index ccf6c43c9..c02038abe 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6.JSON @@ -1,307 +1,307 @@ -{ - "8MM6" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "19", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "4", "b" : "20", "guard" : 3000 }, - { "a" : "5", "b" : "21", "guard" : 3000 }, - { "a" : "5", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "22", "guard" : 3000 }, - { "a" : "6", "b" : "26", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 3000 }, - { "a" : "7", "b" : "25", "guard" : 3000 }, - { "a" : "8", "b" : "22", "guard" : 3000 }, - { "a" : "8", "b" : "26", "guard" : 3000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 } - ] - } -} +{ + "8MM6" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "19", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "4", "b" : "20", "guard" : 3000 }, + { "a" : "5", "b" : "21", "guard" : 3000 }, + { "a" : "5", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "22", "guard" : 3000 }, + { "a" : "6", "b" : "26", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 3000 }, + { "a" : "7", "b" : "25", "guard" : 3000 }, + { "a" : "8", "b" : "22", "guard" : 3000 }, + { "a" : "8", "b" : "26", "guard" : 3000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON index fd05ad3a2..0876a97a2 100755 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8mm6a.JSON @@ -1,311 +1,311 @@ -{ - "8MM6a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 9 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 3000 }, - { "a" : "1", "b" : "19", "guard" : 3000 }, - { "a" : "2", "b" : "16", "guard" : 3000 }, - { "a" : "2", "b" : "20", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "3", "b" : "19", "guard" : 3000 }, - { "a" : "4", "b" : "16", "guard" : 3000 }, - { "a" : "4", "b" : "20", "guard" : 3000 }, - { "a" : "5", "b" : "21", "guard" : 3000 }, - { "a" : "5", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "22", "guard" : 3000 }, - { "a" : "6", "b" : "26", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 3000 }, - { "a" : "7", "b" : "25", "guard" : 3000 }, - { "a" : "8", "b" : "22", "guard" : 3000 }, - { "a" : "8", "b" : "26", "guard" : 3000 }, - - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "20", "guard" : 6000 }, - { "a" : "11", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "21", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "25", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - { "a" : "14", "b" : "26", "guard" : 6000 }, - - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "21", "b" : "23", "guard" : 12500 }, - { "a" : "21", "b" : "23", "guard" : 12500 }, - { "a" : "22", "b" : "24", "guard" : 12500 }, - { "a" : "22", "b" : "24", "guard" : 12500 } - ] - } -} +{ + "8MM6a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 9 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 3000 }, + { "a" : "1", "b" : "19", "guard" : 3000 }, + { "a" : "2", "b" : "16", "guard" : 3000 }, + { "a" : "2", "b" : "20", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "3", "b" : "19", "guard" : 3000 }, + { "a" : "4", "b" : "16", "guard" : 3000 }, + { "a" : "4", "b" : "20", "guard" : 3000 }, + { "a" : "5", "b" : "21", "guard" : 3000 }, + { "a" : "5", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "22", "guard" : 3000 }, + { "a" : "6", "b" : "26", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 3000 }, + { "a" : "7", "b" : "25", "guard" : 3000 }, + { "a" : "8", "b" : "22", "guard" : 3000 }, + { "a" : "8", "b" : "26", "guard" : 3000 }, + + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "20", "guard" : 6000 }, + { "a" : "11", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "21", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "25", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + { "a" : "14", "b" : "26", "guard" : 6000 }, + + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "21", "b" : "23", "guard" : 12500 }, + { "a" : "21", "b" : "23", "guard" : 12500 }, + { "a" : "22", "b" : "24", "guard" : 12500 }, + { "a" : "22", "b" : "24", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON index 6796e7df0..7e0323041 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0c.JSON @@ -1,168 +1,168 @@ -{ - "8SM0c" : - { - "minSize" : "m", "maxSize" : "l", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 3, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 3000 }, - { "a" : "7", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "12", "guard" : 3000 }, - { "a" : "7", "b" : "13", "guard" : 3000 }, - { "a" : "8", "b" : "9", "guard" : 3000 }, - { "a" : "8", "b" : "11", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 3000 }, - { "a" : "9", "b" : "10", "type": "wide" }, - { "a" : "9", "b" : "12", "type": "wide" }, - { "a" : "10", "b" : "13", "type": "wide" }, - { "a" : "12", "b" : "13", "type": "wide" } - ] - } -} +{ + "8SM0c" : + { + "minSize" : "m", "maxSize" : "l", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 3, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 3000 }, + { "a" : "7", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "12", "guard" : 3000 }, + { "a" : "7", "b" : "13", "guard" : 3000 }, + { "a" : "8", "b" : "9", "guard" : 3000 }, + { "a" : "8", "b" : "11", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 3000 }, + { "a" : "9", "b" : "10", "type": "wide" }, + { "a" : "9", "b" : "12", "type": "wide" }, + { "a" : "10", "b" : "13", "type": "wide" }, + { "a" : "12", "b" : "13", "type": "wide" } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON index 43b7211ed..1461f43c1 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8sm0f.JSON @@ -1,151 +1,151 @@ -{ - "8SM0f" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6000 }, - { "a" : "2", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "11", "guard" : 6000 }, - { "a" : "3", "b" : "12", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "12", "guard" : 6000 }, - { "a" : "5", "b" : "9", "guard" : 6000 }, - { "a" : "6", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "12", "guard" : 6000 }, - { "a" : "8", "b" : "10", "guard" : 6000 } - ] - } -} +{ + "8SM0f" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6000 }, + { "a" : "2", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "11", "guard" : 6000 }, + { "a" : "3", "b" : "12", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "12", "guard" : 6000 }, + { "a" : "5", "b" : "9", "guard" : 6000 }, + { "a" : "6", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "12", "guard" : 6000 }, + { "a" : "8", "b" : "10", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON index a6a5dd19b..4c00d0ab0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12.JSON @@ -1,403 +1,403 @@ -{ - "8XM12" : - { - "minSize" : "xl", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "junction", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 9000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 9000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 9000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 9000 }, - - { "a" : "21", "b" : "24", "guard" : 3000 }, - { "a" : "23", "b" : "25", "guard" : 3000 }, - { "a" : "28", "b" : "30", "guard" : 3000 }, - { "a" : "29", "b" : "32", "guard" : 3000 } - ] - } -} +{ + "8XM12" : + { + "minSize" : "xl", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "junction", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 9000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 9000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 9000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 9000 }, + + { "a" : "21", "b" : "24", "guard" : 3000 }, + { "a" : "23", "b" : "25", "guard" : 3000 }, + { "a" : "28", "b" : "30", "guard" : 3000 }, + { "a" : "29", "b" : "32", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON index 4ae30574c..dccafef07 100755 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm12a.JSON @@ -1,377 +1,377 @@ -{ - "8XM12a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 9000 }, - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 9000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 9000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 9000 }, - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - { "a" : "24", "b" : "26", "guard" : 3000 }, - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "21", "b" : "22", "guard" : 3000 }, - { "a" : "26", "b" : "28", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - { "a" : "30", "b" : "31", "guard" : 3000 }, - { "a" : "22", "b" : "23", "guard" : 3000 }, - { "a" : "25", "b" : "27", "guard" : 3000 }, - { "a" : "31", "b" : "32", "guard" : 3000 }, - { "a" : "27", "b" : "29", "guard" : 3000 } - ] - } -} +{ + "8XM12a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 9000 }, + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 9000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 9000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 9000 }, + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + { "a" : "24", "b" : "26", "guard" : 3000 }, + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "21", "b" : "22", "guard" : 3000 }, + { "a" : "26", "b" : "28", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + { "a" : "30", "b" : "31", "guard" : 3000 }, + { "a" : "22", "b" : "23", "guard" : 3000 }, + { "a" : "25", "b" : "27", "guard" : 3000 }, + { "a" : "31", "b" : "32", "guard" : 3000 }, + { "a" : "27", "b" : "29", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON index 5ce959c15..a06befaa9 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/8xm8.JSON @@ -1,515 +1,515 @@ -{ - "8XM8" : - { - "minSize" : "l", "maxSize" : "h+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 12000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 7500, "max" : 10000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "35" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "36" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "43" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "17", "guard" : 3000 }, - { "a" : "1", "b" : "18", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "24", "guard" : 3000 }, - { "a" : "3", "b" : "27", "guard" : 3000 }, - { "a" : "3", "b" : "28", "guard" : 3000 }, - { "a" : "4", "b" : "29", "guard" : 3000 }, - { "a" : "4", "b" : "30", "guard" : 3000 }, - { "a" : "5", "b" : "35", "guard" : 3000 }, - { "a" : "5", "b" : "36", "guard" : 3000 }, - { "a" : "6", "b" : "37", "guard" : 3000 }, - { "a" : "6", "b" : "38", "guard" : 3000 }, - { "a" : "7", "b" : "41", "guard" : 3000 }, - { "a" : "7", "b" : "42", "guard" : 3000 }, - { "a" : "8", "b" : "47", "guard" : 3000 }, - { "a" : "8", "b" : "48", "guard" : 3000 }, - { "a" : "9", "b" : "19", "guard" : 6000 }, - { "a" : "9", "b" : "20", "guard" : 6000 }, - { "a" : "10", "b" : "21", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "11", "b" : "25", "guard" : 6000 }, - { "a" : "11", "b" : "26", "guard" : 6000 }, - { "a" : "12", "b" : "31", "guard" : 6000 }, - { "a" : "12", "b" : "32", "guard" : 6000 }, - { "a" : "13", "b" : "33", "guard" : 6000 }, - { "a" : "13", "b" : "34", "guard" : 6000 }, - { "a" : "14", "b" : "39", "guard" : 6000 }, - { "a" : "14", "b" : "40", "guard" : 6000 }, - { "a" : "15", "b" : "43", "guard" : 6000 }, - { "a" : "15", "b" : "44", "guard" : 6000 }, - { "a" : "16", "b" : "45", "guard" : 6000 }, - { "a" : "16", "b" : "46", "guard" : 6000 }, - { "a" : "17", "b" : "25", "guard" : 3000 }, - { "a" : "18", "b" : "19", "guard" : 3000 }, - { "a" : "20", "b" : "21", "guard" : 3000 }, - { "a" : "20", "b" : "28", "guard" : 3000 }, - { "a" : "21", "b" : "29", "guard" : 3000 }, - { "a" : "22", "b" : "23", "guard" : 3000 }, - { "a" : "24", "b" : "32", "guard" : 3000 }, - { "a" : "26", "b" : "27", "guard" : 3000 }, - { "a" : "26", "b" : "34", "guard" : 3000 }, - { "a" : "30", "b" : "31", "guard" : 3000 }, - { "a" : "31", "b" : "39", "guard" : 3000 }, - { "a" : "33", "b" : "41", "guard" : 3000 }, - { "a" : "34", "b" : "35", "guard" : 3000 }, - { "a" : "36", "b" : "44", "guard" : 3000 }, - { "a" : "37", "b" : "45", "guard" : 3000 }, - { "a" : "38", "b" : "39", "guard" : 3000 }, - { "a" : "40", "b" : "48", "guard" : 3000 }, - { "a" : "42", "b" : "43", "guard" : 3000 }, - { "a" : "44", "b" : "45", "guard" : 3000 }, - { "a" : "46", "b" : "47", "guard" : 3000 } - ] - } -} +{ + "8XM8" : + { + "minSize" : "l", "maxSize" : "h+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 12000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 7500, "max" : 10000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "35" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "36" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "43" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "17", "guard" : 3000 }, + { "a" : "1", "b" : "18", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "24", "guard" : 3000 }, + { "a" : "3", "b" : "27", "guard" : 3000 }, + { "a" : "3", "b" : "28", "guard" : 3000 }, + { "a" : "4", "b" : "29", "guard" : 3000 }, + { "a" : "4", "b" : "30", "guard" : 3000 }, + { "a" : "5", "b" : "35", "guard" : 3000 }, + { "a" : "5", "b" : "36", "guard" : 3000 }, + { "a" : "6", "b" : "37", "guard" : 3000 }, + { "a" : "6", "b" : "38", "guard" : 3000 }, + { "a" : "7", "b" : "41", "guard" : 3000 }, + { "a" : "7", "b" : "42", "guard" : 3000 }, + { "a" : "8", "b" : "47", "guard" : 3000 }, + { "a" : "8", "b" : "48", "guard" : 3000 }, + { "a" : "9", "b" : "19", "guard" : 6000 }, + { "a" : "9", "b" : "20", "guard" : 6000 }, + { "a" : "10", "b" : "21", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "11", "b" : "25", "guard" : 6000 }, + { "a" : "11", "b" : "26", "guard" : 6000 }, + { "a" : "12", "b" : "31", "guard" : 6000 }, + { "a" : "12", "b" : "32", "guard" : 6000 }, + { "a" : "13", "b" : "33", "guard" : 6000 }, + { "a" : "13", "b" : "34", "guard" : 6000 }, + { "a" : "14", "b" : "39", "guard" : 6000 }, + { "a" : "14", "b" : "40", "guard" : 6000 }, + { "a" : "15", "b" : "43", "guard" : 6000 }, + { "a" : "15", "b" : "44", "guard" : 6000 }, + { "a" : "16", "b" : "45", "guard" : 6000 }, + { "a" : "16", "b" : "46", "guard" : 6000 }, + { "a" : "17", "b" : "25", "guard" : 3000 }, + { "a" : "18", "b" : "19", "guard" : 3000 }, + { "a" : "20", "b" : "21", "guard" : 3000 }, + { "a" : "20", "b" : "28", "guard" : 3000 }, + { "a" : "21", "b" : "29", "guard" : 3000 }, + { "a" : "22", "b" : "23", "guard" : 3000 }, + { "a" : "24", "b" : "32", "guard" : 3000 }, + { "a" : "26", "b" : "27", "guard" : 3000 }, + { "a" : "26", "b" : "34", "guard" : 3000 }, + { "a" : "30", "b" : "31", "guard" : 3000 }, + { "a" : "31", "b" : "39", "guard" : 3000 }, + { "a" : "33", "b" : "41", "guard" : 3000 }, + { "a" : "34", "b" : "35", "guard" : 3000 }, + { "a" : "36", "b" : "44", "guard" : 3000 }, + { "a" : "37", "b" : "45", "guard" : 3000 }, + { "a" : "38", "b" : "39", "guard" : 3000 }, + { "a" : "40", "b" : "48", "guard" : 3000 }, + { "a" : "42", "b" : "43", "guard" : 3000 }, + { "a" : "44", "b" : "45", "guard" : 3000 }, + { "a" : "46", "b" : "47", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON index 7a2e687c1..d3acb2674 100644 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2mm2h.JSON @@ -1,87 +1,87 @@ -{ - "4MM2h" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "4MM2h" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON index 3a324306e..64deddcf1 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/2x2sm4d(3).JSON @@ -1,235 +1,235 @@ -{ - "2x2sm4d(3)" : - { - "minSize" : "xl", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gold" : 1 }, - "treasure" : - [ - { "min" : 100, "max" : 500, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "14" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "15" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "16" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "17" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "18" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "19" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - }, - "20" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 13, - "treasureLikeZone" : 13 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "1", "b" : "12", "guard" : 6000 }, - { "a" : "1", "b" : "4", "guard" : 45000 }, - { "a" : "2", "b" : "3", "guard" : 45000 }, - { "a" : "1", "b" : "2", "guard" : 2000 }, - { "a" : "3", "b" : "4", "guard" : 2000 }, - { "a" : "12", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 6000 }, - { "a" : "11", "b" : "18", "guard" : 6000 }, - { "a" : "10", "b" : "18", "guard" : 6000 }, - { "a" : "10", "b" : "17", "guard" : 6000 }, - { "a" : "9", "b" : "17", "guard" : 6000 }, - { "a" : "9", "b" : "16", "guard" : 6000 }, - { "a" : "8", "b" : "16", "guard" : 6000 }, - { "a" : "8", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "15", "guard" : 6000 }, - { "a" : "7", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "20", "guard" : 6000 }, - { "a" : "12", "b" : "20", "guard" : 6000 } - ] - } -} +{ + "2x2sm4d(3)" : + { + "minSize" : "xl", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gold" : 1 }, + "treasure" : + [ + { "min" : 100, "max" : 500, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "14" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "15" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "16" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "17" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "18" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "19" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + }, + "20" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 13, + "treasureLikeZone" : 13 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "1", "b" : "12", "guard" : 6000 }, + { "a" : "1", "b" : "4", "guard" : 45000 }, + { "a" : "2", "b" : "3", "guard" : 45000 }, + { "a" : "1", "b" : "2", "guard" : 2000 }, + { "a" : "3", "b" : "4", "guard" : 2000 }, + { "a" : "12", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 6000 }, + { "a" : "11", "b" : "18", "guard" : 6000 }, + { "a" : "10", "b" : "18", "guard" : 6000 }, + { "a" : "10", "b" : "17", "guard" : 6000 }, + { "a" : "9", "b" : "17", "guard" : 6000 }, + { "a" : "9", "b" : "16", "guard" : 6000 }, + { "a" : "8", "b" : "16", "guard" : 6000 }, + { "a" : "8", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "15", "guard" : 6000 }, + { "a" : "7", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "20", "guard" : 6000 }, + { "a" : "12", "b" : "20", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON index 7a2e687c1..d3acb2674 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4mm2h.JSON @@ -1,87 +1,87 @@ -{ - "4MM2h" : - { - "minSize" : "m", "maxSize" : "m", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 5, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 } - ] - } -} +{ + "4MM2h" : + { + "minSize" : "m", "maxSize" : "m", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 5, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON index 5fb9be1d6..5779f8cf4 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/4sm3i.JSON @@ -1,116 +1,116 @@ -{ - "4SM3i" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 6, - "treasureLikeZone" : 6 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 6000 }, - { "a" : "6", "b" : "7", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "7", "b" : "9", "guard" : 6000 } - ] - } -} +{ + "4SM3i" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 6, + "treasureLikeZone" : 6 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 6000 }, + { "a" : "6", "b" : "7", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "7", "b" : "9", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON index 122659efb..f8fa0d969 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/6lm10a.JSON @@ -1,308 +1,308 @@ -{ - "6LM10a" : - { - "minSize" : "xl+u", "maxSize" : "xl+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 1 }, - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "playerStart", - "size" : 30, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 7, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 8, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 7, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 8, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 10, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 50, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 21, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "9", "guard" : 6000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "2", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "21", "guard" : 3000 }, - { "a" : "3", "b" : "23", "guard" : 3000 }, - { "a" : "3", "b" : "24", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "23", "guard" : 3000 }, - { "a" : "4", "b" : "25", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 6000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "5", "b" : "24", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 6000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "6", "b" : "25", "guard" : 3000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "9", "b" : "21", "guard" : 6000 }, - { "a" : "9", "b" : "23", "guard" : 6000 }, - { "a" : "10", "b" : "22", "guard" : 6000 }, - { "a" : "10", "b" : "23", "guard" : 6000 }, - { "a" : "11", "b" : "21", "guard" : 6000 }, - { "a" : "11", "b" : "24", "guard" : 6000 }, - { "a" : "12", "b" : "22", "guard" : 6000 }, - { "a" : "12", "b" : "25", "guard" : 6000 }, - { "a" : "13", "b" : "23", "guard" : 6000 }, - { "a" : "13", "b" : "24", "guard" : 6000 }, - { "a" : "13", "b" : "13", "guard" : 6000 }, - { "a" : "14", "b" : "23", "guard" : 6000 }, - { "a" : "15", "b" : "24", "guard" : 3000 }, - { "a" : "15", "b" : "24", "guard" : 3000 }, - { "a" : "16", "b" : "25", "guard" : 3000 }, - { "a" : "16", "b" : "25", "guard" : 3000 }, - { "a" : "17", "b" : "21", "guard" : 9000 }, - { "a" : "18", "b" : "22", "guard" : 9000 }, - { "a" : "19", "b" : "24", "guard" : 9000 }, - { "a" : "20", "b" : "25", "guard" : 9000 }, - { "a" : "21", "b" : "22", "guard" : 0 }, - { "a" : "24", "b" : "25", "guard" : 0 }, - { "a" : "13", "b" : "14", "guard" : 6000 }, - { "a" : "14", "b" : "25", "guard" : 6000 } - ] - } -} +{ + "6LM10a" : + { + "minSize" : "xl+u", "maxSize" : "xl+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 1 }, + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "playerStart", + "size" : 30, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 7, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 8, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 7, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 8, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 10, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 50, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 21, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "9", "guard" : 6000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "2", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "21", "guard" : 3000 }, + { "a" : "3", "b" : "23", "guard" : 3000 }, + { "a" : "3", "b" : "24", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "23", "guard" : 3000 }, + { "a" : "4", "b" : "25", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 6000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "5", "b" : "24", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 6000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "6", "b" : "25", "guard" : 3000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "9", "b" : "21", "guard" : 6000 }, + { "a" : "9", "b" : "23", "guard" : 6000 }, + { "a" : "10", "b" : "22", "guard" : 6000 }, + { "a" : "10", "b" : "23", "guard" : 6000 }, + { "a" : "11", "b" : "21", "guard" : 6000 }, + { "a" : "11", "b" : "24", "guard" : 6000 }, + { "a" : "12", "b" : "22", "guard" : 6000 }, + { "a" : "12", "b" : "25", "guard" : 6000 }, + { "a" : "13", "b" : "23", "guard" : 6000 }, + { "a" : "13", "b" : "24", "guard" : 6000 }, + { "a" : "13", "b" : "13", "guard" : 6000 }, + { "a" : "14", "b" : "23", "guard" : 6000 }, + { "a" : "15", "b" : "24", "guard" : 3000 }, + { "a" : "15", "b" : "24", "guard" : 3000 }, + { "a" : "16", "b" : "25", "guard" : 3000 }, + { "a" : "16", "b" : "25", "guard" : 3000 }, + { "a" : "17", "b" : "21", "guard" : 9000 }, + { "a" : "18", "b" : "22", "guard" : 9000 }, + { "a" : "19", "b" : "24", "guard" : 9000 }, + { "a" : "20", "b" : "25", "guard" : 9000 }, + { "a" : "21", "b" : "22", "guard" : 0 }, + { "a" : "24", "b" : "25", "guard" : 0 }, + { "a" : "13", "b" : "14", "guard" : 6000 }, + { "a" : "14", "b" : "25", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON index a2ed87446..b18fd8795 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm12 huge.JSON @@ -1,381 +1,381 @@ -{ - "8XM12 Huge" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 5, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 15, - "owner" : 8, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 10000, "max" : 20000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 9 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 9 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 9 - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "15" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "16" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "17" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 9 - }, - "18" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 9 - }, - "19" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 9 - }, - "20" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 9 - }, - "21" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "24" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "25" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "30" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "31" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "21", "guard" : 3000 }, - { "a" : "1", "b" : "24", "guard" : 3000 }, - { "a" : "2", "b" : "23", "guard" : 3000 }, - { "a" : "2", "b" : "25", "guard" : 3000 }, - { "a" : "3", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "14", "guard" : 3000 }, - { "a" : "3", "b" : "22", "guard" : 3000 }, - { "a" : "3", "b" : "26", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "15", "guard" : 3000 }, - { "a" : "4", "b" : "22", "guard" : 3000 }, - { "a" : "4", "b" : "27", "guard" : 3000 }, - { "a" : "5", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "18", "guard" : 3000 }, - { "a" : "5", "b" : "26", "guard" : 3000 }, - { "a" : "5", "b" : "31", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "18", "guard" : 3000 }, - { "a" : "6", "b" : "27", "guard" : 3000 }, - { "a" : "6", "b" : "31", "guard" : 3000 }, - { "a" : "7", "b" : "28", "guard" : 3000 }, - { "a" : "7", "b" : "30", "guard" : 3000 }, - { "a" : "8", "b" : "29", "guard" : 3000 }, - { "a" : "8", "b" : "32", "guard" : 3000 }, - { "a" : "9", "b" : "21", "guard" : 3000 }, - { "a" : "9", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "22", "guard" : 3000 }, - { "a" : "10", "b" : "23", "guard" : 3000 }, - { "a" : "11", "b" : "3", "guard" : 3000 }, - { "a" : "11", "b" : "4", "guard" : 3000 }, - { "a" : "11", "b" : "22", "guard" : 3000 }, - { "a" : "11", "b" : "33", "guard" : 12500 }, - { "a" : "12", "b" : "24", "guard" : 3000 }, - { "a" : "12", "b" : "26", "guard" : 3000 }, - { "a" : "13", "b" : "25", "guard" : 3000 }, - { "a" : "13", "b" : "27", "guard" : 3000 }, - { "a" : "14", "b" : "3", "guard" : 3000 }, - { "a" : "14", "b" : "5", "guard" : 3000 }, - { "a" : "14", "b" : "26", "guard" : 3000 }, - { "a" : "14", "b" : "33", "guard" : 12500 }, - { "a" : "15", "b" : "4", "guard" : 3000 }, - { "a" : "15", "b" : "6", "guard" : 3000 }, - { "a" : "15", "b" : "27", "guard" : 3000 }, - { "a" : "15", "b" : "33", "guard" : 12500 }, - { "a" : "16", "b" : "26", "guard" : 3000 }, - { "a" : "16", "b" : "28", "guard" : 3000 }, - { "a" : "17", "b" : "27", "guard" : 3000 }, - { "a" : "17", "b" : "29", "guard" : 3000 }, - { "a" : "18", "b" : "5", "guard" : 3000 }, - { "a" : "18", "b" : "6", "guard" : 3000 }, - { "a" : "18", "b" : "31", "guard" : 3000 }, - { "a" : "18", "b" : "33", "guard" : 12500 }, - { "a" : "19", "b" : "30", "guard" : 3000 }, - { "a" : "19", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "31", "guard" : 3000 }, - { "a" : "20", "b" : "32", "guard" : 3000 }, - { "a" : "21", "b" : "24", "guard" : 3000 }, - { "a" : "23", "b" : "25", "guard" : 3000 }, - { "a" : "28", "b" : "30", "guard" : 3000 }, - { "a" : "29", "b" : "32", "guard" : 3000 } - ] - } -} +{ + "8XM12 Huge" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 5, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 15, + "owner" : 8, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 10000, "max" : 20000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 9 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 9 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 9 + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "15" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "16" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "17" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 9 + }, + "18" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 9 + }, + "19" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 9 + }, + "20" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 9 + }, + "21" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "24" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "25" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "30" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "31" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "21", "guard" : 3000 }, + { "a" : "1", "b" : "24", "guard" : 3000 }, + { "a" : "2", "b" : "23", "guard" : 3000 }, + { "a" : "2", "b" : "25", "guard" : 3000 }, + { "a" : "3", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "14", "guard" : 3000 }, + { "a" : "3", "b" : "22", "guard" : 3000 }, + { "a" : "3", "b" : "26", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "15", "guard" : 3000 }, + { "a" : "4", "b" : "22", "guard" : 3000 }, + { "a" : "4", "b" : "27", "guard" : 3000 }, + { "a" : "5", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "18", "guard" : 3000 }, + { "a" : "5", "b" : "26", "guard" : 3000 }, + { "a" : "5", "b" : "31", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "18", "guard" : 3000 }, + { "a" : "6", "b" : "27", "guard" : 3000 }, + { "a" : "6", "b" : "31", "guard" : 3000 }, + { "a" : "7", "b" : "28", "guard" : 3000 }, + { "a" : "7", "b" : "30", "guard" : 3000 }, + { "a" : "8", "b" : "29", "guard" : 3000 }, + { "a" : "8", "b" : "32", "guard" : 3000 }, + { "a" : "9", "b" : "21", "guard" : 3000 }, + { "a" : "9", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "22", "guard" : 3000 }, + { "a" : "10", "b" : "23", "guard" : 3000 }, + { "a" : "11", "b" : "3", "guard" : 3000 }, + { "a" : "11", "b" : "4", "guard" : 3000 }, + { "a" : "11", "b" : "22", "guard" : 3000 }, + { "a" : "11", "b" : "33", "guard" : 12500 }, + { "a" : "12", "b" : "24", "guard" : 3000 }, + { "a" : "12", "b" : "26", "guard" : 3000 }, + { "a" : "13", "b" : "25", "guard" : 3000 }, + { "a" : "13", "b" : "27", "guard" : 3000 }, + { "a" : "14", "b" : "3", "guard" : 3000 }, + { "a" : "14", "b" : "5", "guard" : 3000 }, + { "a" : "14", "b" : "26", "guard" : 3000 }, + { "a" : "14", "b" : "33", "guard" : 12500 }, + { "a" : "15", "b" : "4", "guard" : 3000 }, + { "a" : "15", "b" : "6", "guard" : 3000 }, + { "a" : "15", "b" : "27", "guard" : 3000 }, + { "a" : "15", "b" : "33", "guard" : 12500 }, + { "a" : "16", "b" : "26", "guard" : 3000 }, + { "a" : "16", "b" : "28", "guard" : 3000 }, + { "a" : "17", "b" : "27", "guard" : 3000 }, + { "a" : "17", "b" : "29", "guard" : 3000 }, + { "a" : "18", "b" : "5", "guard" : 3000 }, + { "a" : "18", "b" : "6", "guard" : 3000 }, + { "a" : "18", "b" : "31", "guard" : 3000 }, + { "a" : "18", "b" : "33", "guard" : 12500 }, + { "a" : "19", "b" : "30", "guard" : 3000 }, + { "a" : "19", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "31", "guard" : 3000 }, + { "a" : "20", "b" : "32", "guard" : 3000 }, + { "a" : "21", "b" : "24", "guard" : 3000 }, + { "a" : "23", "b" : "25", "guard" : 3000 }, + { "a" : "28", "b" : "30", "guard" : 3000 }, + { "a" : "29", "b" : "32", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON index 88bac0686..0d6d07223 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/8xm8 huge.JSON @@ -1,515 +1,515 @@ -{ - "8XM8 Huge" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 6000, "max" : 12000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 9, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 10, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 17 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 17 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 17 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "23" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "24" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "27" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "28" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "31" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "32" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "35" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "36" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "43" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 17, - "treasureLikeZone" : 17 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 18, - "treasureLikeZone" : 17 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 19, - "treasureLikeZone" : 17 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "matchTerrainToTown" : false, - "minesLikeZone" : 20, - "treasureLikeZone" : 17 - } - }, - "connections" : - [ - { "a" : "1", "b" : "17", "guard" : 6000 }, - { "a" : "1", "b" : "18", "guard" : 6000 }, - { "a" : "2", "b" : "23", "guard" : 6000 }, - { "a" : "2", "b" : "24", "guard" : 6000 }, - { "a" : "3", "b" : "27", "guard" : 6000 }, - { "a" : "3", "b" : "28", "guard" : 6000 }, - { "a" : "4", "b" : "29", "guard" : 6000 }, - { "a" : "4", "b" : "30", "guard" : 6000 }, - { "a" : "5", "b" : "35", "guard" : 6000 }, - { "a" : "5", "b" : "36", "guard" : 6000 }, - { "a" : "6", "b" : "37", "guard" : 6000 }, - { "a" : "6", "b" : "38", "guard" : 6000 }, - { "a" : "7", "b" : "41", "guard" : 6000 }, - { "a" : "7", "b" : "42", "guard" : 6000 }, - { "a" : "8", "b" : "47", "guard" : 6000 }, - { "a" : "8", "b" : "48", "guard" : 6000 }, - { "a" : "9", "b" : "19", "guard" : 12500 }, - { "a" : "9", "b" : "20", "guard" : 12500 }, - { "a" : "10", "b" : "21", "guard" : 12500 }, - { "a" : "10", "b" : "22", "guard" : 12500 }, - { "a" : "11", "b" : "25", "guard" : 12500 }, - { "a" : "11", "b" : "26", "guard" : 12500 }, - { "a" : "12", "b" : "31", "guard" : 12500 }, - { "a" : "12", "b" : "32", "guard" : 12500 }, - { "a" : "13", "b" : "33", "guard" : 12500 }, - { "a" : "13", "b" : "34", "guard" : 12500 }, - { "a" : "14", "b" : "39", "guard" : 12500 }, - { "a" : "14", "b" : "40", "guard" : 12500 }, - { "a" : "15", "b" : "43", "guard" : 12500 }, - { "a" : "15", "b" : "44", "guard" : 12500 }, - { "a" : "16", "b" : "45", "guard" : 12500 }, - { "a" : "16", "b" : "46", "guard" : 12500 }, - { "a" : "17", "b" : "25", "guard" : 6000 }, - { "a" : "18", "b" : "19", "guard" : 6000 }, - { "a" : "20", "b" : "21", "guard" : 6000 }, - { "a" : "20", "b" : "28", "guard" : 6000 }, - { "a" : "21", "b" : "29", "guard" : 6000 }, - { "a" : "22", "b" : "23", "guard" : 6000 }, - { "a" : "24", "b" : "32", "guard" : 6000 }, - { "a" : "26", "b" : "27", "guard" : 6000 }, - { "a" : "26", "b" : "34", "guard" : 6000 }, - { "a" : "30", "b" : "31", "guard" : 6000 }, - { "a" : "31", "b" : "39", "guard" : 6000 }, - { "a" : "33", "b" : "41", "guard" : 6000 }, - { "a" : "34", "b" : "35", "guard" : 6000 }, - { "a" : "36", "b" : "44", "guard" : 6000 }, - { "a" : "37", "b" : "45", "guard" : 6000 }, - { "a" : "38", "b" : "39", "guard" : 6000 }, - { "a" : "40", "b" : "48", "guard" : 6000 }, - { "a" : "42", "b" : "43", "guard" : 6000 }, - { "a" : "44", "b" : "45", "guard" : 6000 }, - { "a" : "46", "b" : "47", "guard" : 6000 } - ] - } -} +{ + "8XM8 Huge" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 6000, "max" : 12000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 9, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 10, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 17 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 17 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 17 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "23" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "24" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "27" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "28" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "31" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "32" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "35" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "36" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "43" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 17, + "treasureLikeZone" : 17 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 18, + "treasureLikeZone" : 17 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 19, + "treasureLikeZone" : 17 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "matchTerrainToTown" : false, + "minesLikeZone" : 20, + "treasureLikeZone" : 17 + } + }, + "connections" : + [ + { "a" : "1", "b" : "17", "guard" : 6000 }, + { "a" : "1", "b" : "18", "guard" : 6000 }, + { "a" : "2", "b" : "23", "guard" : 6000 }, + { "a" : "2", "b" : "24", "guard" : 6000 }, + { "a" : "3", "b" : "27", "guard" : 6000 }, + { "a" : "3", "b" : "28", "guard" : 6000 }, + { "a" : "4", "b" : "29", "guard" : 6000 }, + { "a" : "4", "b" : "30", "guard" : 6000 }, + { "a" : "5", "b" : "35", "guard" : 6000 }, + { "a" : "5", "b" : "36", "guard" : 6000 }, + { "a" : "6", "b" : "37", "guard" : 6000 }, + { "a" : "6", "b" : "38", "guard" : 6000 }, + { "a" : "7", "b" : "41", "guard" : 6000 }, + { "a" : "7", "b" : "42", "guard" : 6000 }, + { "a" : "8", "b" : "47", "guard" : 6000 }, + { "a" : "8", "b" : "48", "guard" : 6000 }, + { "a" : "9", "b" : "19", "guard" : 12500 }, + { "a" : "9", "b" : "20", "guard" : 12500 }, + { "a" : "10", "b" : "21", "guard" : 12500 }, + { "a" : "10", "b" : "22", "guard" : 12500 }, + { "a" : "11", "b" : "25", "guard" : 12500 }, + { "a" : "11", "b" : "26", "guard" : 12500 }, + { "a" : "12", "b" : "31", "guard" : 12500 }, + { "a" : "12", "b" : "32", "guard" : 12500 }, + { "a" : "13", "b" : "33", "guard" : 12500 }, + { "a" : "13", "b" : "34", "guard" : 12500 }, + { "a" : "14", "b" : "39", "guard" : 12500 }, + { "a" : "14", "b" : "40", "guard" : 12500 }, + { "a" : "15", "b" : "43", "guard" : 12500 }, + { "a" : "15", "b" : "44", "guard" : 12500 }, + { "a" : "16", "b" : "45", "guard" : 12500 }, + { "a" : "16", "b" : "46", "guard" : 12500 }, + { "a" : "17", "b" : "25", "guard" : 6000 }, + { "a" : "18", "b" : "19", "guard" : 6000 }, + { "a" : "20", "b" : "21", "guard" : 6000 }, + { "a" : "20", "b" : "28", "guard" : 6000 }, + { "a" : "21", "b" : "29", "guard" : 6000 }, + { "a" : "22", "b" : "23", "guard" : 6000 }, + { "a" : "24", "b" : "32", "guard" : 6000 }, + { "a" : "26", "b" : "27", "guard" : 6000 }, + { "a" : "26", "b" : "34", "guard" : 6000 }, + { "a" : "30", "b" : "31", "guard" : 6000 }, + { "a" : "31", "b" : "39", "guard" : 6000 }, + { "a" : "33", "b" : "41", "guard" : 6000 }, + { "a" : "34", "b" : "35", "guard" : 6000 }, + { "a" : "36", "b" : "44", "guard" : 6000 }, + { "a" : "37", "b" : "45", "guard" : 6000 }, + { "a" : "38", "b" : "39", "guard" : 6000 }, + { "a" : "40", "b" : "48", "guard" : 6000 }, + { "a" : "42", "b" : "43", "guard" : 6000 }, + { "a" : "44", "b" : "45", "guard" : 6000 }, + { "a" : "46", "b" : "47", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON index 601790fef..d50253961 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross.JSON @@ -1,188 +1,188 @@ -{ - "Cross" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - }, - "13" : - { - "type" : "treasure", - "size" : 18, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 25000, "density" : 9 }, - { "min" : 20000, "max" : 30000, "density" : 3 } - ] - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "8", "b" : "13", "guard" : 12500 }, - { "a" : "5", "b" : "13", "guard" : 12500 }, - { "a" : "7", "b" : "13", "guard" : 12500 }, - { "a" : "6", "b" : "13", "guard" : 12500 } - ] - } -} +{ + "Cross" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + }, + "13" : + { + "type" : "treasure", + "size" : 18, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 25000, "density" : 9 }, + { "min" : 20000, "max" : 30000, "density" : 3 } + ] + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "8", "b" : "13", "guard" : 12500 }, + { "a" : "5", "b" : "13", "guard" : 12500 }, + { "a" : "7", "b" : "13", "guard" : 12500 }, + { "a" : "6", "b" : "13", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON index 1ea221acc..214b283ba 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross2.JSON @@ -1,223 +1,223 @@ -{ - "Cross 2" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 7 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 7 - }, - "11" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "12" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 7 - }, - "13" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "14" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 7 - }, - "15" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 7 - }, - "16" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - }, - "17" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - }, - "18" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 15, - "treasureLikeZone" : 7 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 }, - { "a" : "3", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "11", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "4", "b" : "13", "guard" : 3000 }, - { "a" : "4", "b" : "14", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "6", "b" : "12", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "12", "guard" : 3000 }, - { "a" : "13", "b" : "14", "guard" : 3000 }, - { "a" : "11", "b" : "15", "guard" : 6000 }, - { "a" : "9", "b" : "15", "guard" : 6000 }, - { "a" : "8", "b" : "17", "guard" : 6000 }, - { "a" : "7", "b" : "17", "guard" : 6000 }, - { "a" : "10", "b" : "16", "guard" : 6000 }, - { "a" : "12", "b" : "16", "guard" : 6000 }, - { "a" : "14", "b" : "18", "guard" : 6000 }, - { "a" : "13", "b" : "18", "guard" : 6000 } - ] - } -} +{ + "Cross 2" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 7 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 7 + }, + "11" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "12" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 7 + }, + "13" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "14" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 7 + }, + "15" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 7 + }, + "16" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + }, + "17" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + }, + "18" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 15, + "treasureLikeZone" : 7 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 }, + { "a" : "3", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "11", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "4", "b" : "13", "guard" : 3000 }, + { "a" : "4", "b" : "14", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "6", "b" : "12", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "12", "guard" : 3000 }, + { "a" : "13", "b" : "14", "guard" : 3000 }, + { "a" : "11", "b" : "15", "guard" : 6000 }, + { "a" : "9", "b" : "15", "guard" : 6000 }, + { "a" : "8", "b" : "17", "guard" : 6000 }, + { "a" : "7", "b" : "17", "guard" : 6000 }, + { "a" : "10", "b" : "16", "guard" : 6000 }, + { "a" : "12", "b" : "16", "guard" : 6000 }, + { "a" : "14", "b" : "18", "guard" : 6000 }, + { "a" : "13", "b" : "18", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON index 58ec239e2..31eb2cf18 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/cross3.JSON @@ -1,340 +1,340 @@ -{ - "Cross 3_1" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "2-4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 } - ] - }, - "Cross 3_2" : - { - "minSize" : "m", "maxSize" : "xl+u", - "players" : "4", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 18, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 5000, "max" : 15000, "density" : 10 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "weak", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 9, - "treasureLikeZone" : 10 - } - }, - "connections" : - [ - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 6000 }, - { "a" : "5", "b" : "11", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "8", "b" : "12", "guard" : 6000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "8", "guard" : 3000 } - ] - } -} +{ + "Cross 3_1" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "2-4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 } + ] + }, + "Cross 3_2" : + { + "minSize" : "m", "maxSize" : "xl+u", + "players" : "4", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 18, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 5000, "max" : 15000, "density" : 10 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "weak", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 9, + "treasureLikeZone" : 10 + } + }, + "connections" : + [ + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 6000 }, + { "a" : "5", "b" : "11", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "8", "b" : "12", "guard" : 6000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "8", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON index f726a3b53..c822ed62a 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/doubled 8mm6.JSON @@ -1,536 +1,536 @@ -{ - "Doubled 8MM6" : - { - "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 3000, "max" : 6000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 3 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone": 11, - "treasureLikeZone" : 3 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone": 12, - "treasureLikeZone" : 3 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "16" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "17" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 30000, "max" : 60000, "density" : 1 }, - { "min" : 20000, "max" : 30000, "density" : 8 }, - { "min" : 7500, "max" : 8000, "density" : 8 } - ] - }, - "18" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "19" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "20" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "21" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "22" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "23" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "24" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "25" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "26" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "27" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "28" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "29" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "30" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "31" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "32" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "33" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "34" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 2, - "treasureLikeZone" : 3 - }, - "37" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 3 - }, - "38" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 3 - }, - "39" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 3 - }, - "40" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "strong", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 3 - }, - "41" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "42" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "43" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 17 - }, - "44" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 18 - }, - "45" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "46" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "47" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "48" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - }, - "49" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "50" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 13 - }, - "51" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 11, - "treasureLikeZone" : 1 - }, - "52" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 12, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "15", "guard" : 6000 }, - { "a" : "1", "b" : "19", "guard" : 6000 }, - { "a" : "2", "b" : "16", "guard" : 6000 }, - { "a" : "2", "b" : "20", "guard" : 6000 }, - { "a" : "3", "b" : "15", "guard" : 6000 }, - { "a" : "3", "b" : "19", "guard" : 6000 }, - { "a" : "4", "b" : "16", "guard" : 6000 }, - { "a" : "4", "b" : "20", "guard" : 6000 }, - { "a" : "5", "b" : "21", "guard" : 6000 }, - { "a" : "5", "b" : "25", "guard" : 6000 }, - { "a" : "6", "b" : "22", "guard" : 6000 }, - { "a" : "6", "b" : "26", "guard" : 6000 }, - { "a" : "7", "b" : "21", "guard" : 6000 }, - { "a" : "7", "b" : "25", "guard" : 6000 }, - { "a" : "8", "b" : "22", "guard" : 6000 }, - { "a" : "8", "b" : "26", "guard" : 6000 }, - { "a" : "11", "b" : "19", "guard" : 9000 }, - { "a" : "11", "b" : "20", "guard" : 9000 }, - { "a" : "11", "b" : "22", "guard" : 9000 }, - { "a" : "12", "b" : "19", "guard" : 9000 }, - { "a" : "12", "b" : "21", "guard" : 9000 }, - { "a" : "12", "b" : "22", "guard" : 9000 }, - { "a" : "13", "b" : "25", "guard" : 9000 }, - { "a" : "14", "b" : "26", "guard" : 9000 }, - { "a" : "17", "b" : "19", "guard" : 12500 }, - { "a" : "18", "b" : "20", "guard" : 12500 }, - { "a" : "23", "b" : "21", "guard" : 12500 }, - { "a" : "24", "b" : "22", "guard" : 12500 }, - { "a" : "27", "b" : "41", "guard" : 6000 }, - { "a" : "27", "b" : "45", "guard" : 6000 }, - { "a" : "28", "b" : "42", "guard" : 6000 }, - { "a" : "28", "b" : "46", "guard" : 6000 }, - { "a" : "29", "b" : "41", "guard" : 6000 }, - { "a" : "29", "b" : "45", "guard" : 6000 }, - { "a" : "30", "b" : "42", "guard" : 6000 }, - { "a" : "30", "b" : "46", "guard" : 6000 }, - { "a" : "31", "b" : "47", "guard" : 6000 }, - { "a" : "31", "b" : "51", "guard" : 6000 }, - { "a" : "32", "b" : "48", "guard" : 6000 }, - { "a" : "32", "b" : "52", "guard" : 6000 }, - { "a" : "33", "b" : "47", "guard" : 6000 }, - { "a" : "33", "b" : "51", "guard" : 6000 }, - { "a" : "34", "b" : "48", "guard" : 6000 }, - { "a" : "34", "b" : "52", "guard" : 6000 }, - { "a" : "37", "b" : "45", "guard" : 9000 }, - { "a" : "37", "b" : "46", "guard" : 9000 }, - { "a" : "37", "b" : "48", "guard" : 9000 }, - { "a" : "38", "b" : "45", "guard" : 9000 }, - { "a" : "38", "b" : "47", "guard" : 9000 }, - { "a" : "38", "b" : "48", "guard" : 9000 }, - { "a" : "39", "b" : "51", "guard" : 9000 }, - { "a" : "40", "b" : "52", "guard" : 9000 }, - { "a" : "43", "b" : "45", "guard" : 12500 }, - { "a" : "44", "b" : "46", "guard" : 12500 }, - { "a" : "49", "b" : "47", "guard" : 12500 }, - { "a" : "50", "b" : "48", "guard" : 12500 }, - { "a" : "3", "b" : "29", "guard" : 12500 }, - { "a" : "4", "b" : "30", "guard" : 12500 }, - { "a" : "7", "b" : "33", "guard" : 12500 }, - { "a" : "8", "b" : "34", "guard" : 12500 } - ] - } -} +{ + "Doubled 8MM6" : + { + "minSize" : "xl+u", "maxSize" : "g+u", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 3000, "max" : 6000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 3 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone": 11, + "treasureLikeZone" : 3 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone": 12, + "treasureLikeZone" : 3 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "16" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "17" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 30000, "max" : 60000, "density" : 1 }, + { "min" : 20000, "max" : 30000, "density" : 8 }, + { "min" : 7500, "max" : 8000, "density" : 8 } + ] + }, + "18" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "19" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "20" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "21" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "22" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "23" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "24" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "25" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "26" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "27" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "28" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "29" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "30" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "31" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "32" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "33" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "34" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 2, + "treasureLikeZone" : 3 + }, + "37" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 3 + }, + "38" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 3 + }, + "39" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 3 + }, + "40" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "strong", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 3 + }, + "41" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "42" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "43" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 17 + }, + "44" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 18 + }, + "45" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "46" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "47" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "48" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + }, + "49" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "50" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 13 + }, + "51" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 11, + "treasureLikeZone" : 1 + }, + "52" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 12, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "15", "guard" : 6000 }, + { "a" : "1", "b" : "19", "guard" : 6000 }, + { "a" : "2", "b" : "16", "guard" : 6000 }, + { "a" : "2", "b" : "20", "guard" : 6000 }, + { "a" : "3", "b" : "15", "guard" : 6000 }, + { "a" : "3", "b" : "19", "guard" : 6000 }, + { "a" : "4", "b" : "16", "guard" : 6000 }, + { "a" : "4", "b" : "20", "guard" : 6000 }, + { "a" : "5", "b" : "21", "guard" : 6000 }, + { "a" : "5", "b" : "25", "guard" : 6000 }, + { "a" : "6", "b" : "22", "guard" : 6000 }, + { "a" : "6", "b" : "26", "guard" : 6000 }, + { "a" : "7", "b" : "21", "guard" : 6000 }, + { "a" : "7", "b" : "25", "guard" : 6000 }, + { "a" : "8", "b" : "22", "guard" : 6000 }, + { "a" : "8", "b" : "26", "guard" : 6000 }, + { "a" : "11", "b" : "19", "guard" : 9000 }, + { "a" : "11", "b" : "20", "guard" : 9000 }, + { "a" : "11", "b" : "22", "guard" : 9000 }, + { "a" : "12", "b" : "19", "guard" : 9000 }, + { "a" : "12", "b" : "21", "guard" : 9000 }, + { "a" : "12", "b" : "22", "guard" : 9000 }, + { "a" : "13", "b" : "25", "guard" : 9000 }, + { "a" : "14", "b" : "26", "guard" : 9000 }, + { "a" : "17", "b" : "19", "guard" : 12500 }, + { "a" : "18", "b" : "20", "guard" : 12500 }, + { "a" : "23", "b" : "21", "guard" : 12500 }, + { "a" : "24", "b" : "22", "guard" : 12500 }, + { "a" : "27", "b" : "41", "guard" : 6000 }, + { "a" : "27", "b" : "45", "guard" : 6000 }, + { "a" : "28", "b" : "42", "guard" : 6000 }, + { "a" : "28", "b" : "46", "guard" : 6000 }, + { "a" : "29", "b" : "41", "guard" : 6000 }, + { "a" : "29", "b" : "45", "guard" : 6000 }, + { "a" : "30", "b" : "42", "guard" : 6000 }, + { "a" : "30", "b" : "46", "guard" : 6000 }, + { "a" : "31", "b" : "47", "guard" : 6000 }, + { "a" : "31", "b" : "51", "guard" : 6000 }, + { "a" : "32", "b" : "48", "guard" : 6000 }, + { "a" : "32", "b" : "52", "guard" : 6000 }, + { "a" : "33", "b" : "47", "guard" : 6000 }, + { "a" : "33", "b" : "51", "guard" : 6000 }, + { "a" : "34", "b" : "48", "guard" : 6000 }, + { "a" : "34", "b" : "52", "guard" : 6000 }, + { "a" : "37", "b" : "45", "guard" : 9000 }, + { "a" : "37", "b" : "46", "guard" : 9000 }, + { "a" : "37", "b" : "48", "guard" : 9000 }, + { "a" : "38", "b" : "45", "guard" : 9000 }, + { "a" : "38", "b" : "47", "guard" : 9000 }, + { "a" : "38", "b" : "48", "guard" : 9000 }, + { "a" : "39", "b" : "51", "guard" : 9000 }, + { "a" : "40", "b" : "52", "guard" : 9000 }, + { "a" : "43", "b" : "45", "guard" : 12500 }, + { "a" : "44", "b" : "46", "guard" : 12500 }, + { "a" : "49", "b" : "47", "guard" : 12500 }, + { "a" : "50", "b" : "48", "guard" : 12500 }, + { "a" : "3", "b" : "29", "guard" : 12500 }, + { "a" : "4", "b" : "30", "guard" : 12500 }, + { "a" : "7", "b" : "33", "guard" : 12500 }, + { "a" : "8", "b" : "34", "guard" : 12500 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON index a0aa22d07..46a5df473 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/elka.JSON @@ -1,168 +1,168 @@ -{ - "Elka" : - { - "minSize" : "l+u", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 14 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "14" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "15" : - { - "type" : "treasure", - "size" : 20, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "4", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "11", "guard" : 3000 }, - { "a" : "2", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "15", "guard" : 3000 }, - { "a" : "6", "b" : "15", "guard" : 3000 }, - { "a" : "5", "b" : "13", "guard" : 3000 }, - { "a" : "3", "b" : "13", "guard" : 3000 }, - { "a" : "2", "b" : "14", "guard" : 3000 }, - { "a" : "6", "b" : "14", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "6", "b" : "10", "guard" : 3000 }, - { "a" : "4", "b" : "12", "guard" : 3000 }, - { "a" : "3", "b" : "12", "guard" : 3000 } - ] - } -} +{ + "Elka" : + { + "minSize" : "l+u", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 14 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "14" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "15" : + { + "type" : "treasure", + "size" : 20, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "4", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "11", "guard" : 3000 }, + { "a" : "2", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "15", "guard" : 3000 }, + { "a" : "6", "b" : "15", "guard" : 3000 }, + { "a" : "5", "b" : "13", "guard" : 3000 }, + { "a" : "3", "b" : "13", "guard" : 3000 }, + { "a" : "2", "b" : "14", "guard" : 3000 }, + { "a" : "6", "b" : "14", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "6", "b" : "10", "guard" : 3000 }, + { "a" : "4", "b" : "12", "guard" : 3000 }, + { "a" : "3", "b" : "12", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON index 42b00a3b5..f0409ac22 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/greatSands.JSON @@ -1,321 +1,321 @@ -{ - "GreatSands XL 2-8" : - { - "minSize" : "xl", "maxSize" : "h", - "players" : "2-8", - "zones" : - { - "1" : - { - "type" : "treasure", - "size" : 1600, - "monsters" : "strong", - "neutralTowns" : { "castles" : 1 }, - "allowedTowns" : [ "conflux" ], - "allowedMonsters" : [ "neutral", "conflux" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "sand" ], - "mines" : { "gold" : 2 }, - "treasure" : - [ - { "min" : 6000, "max" : 10000, "density" : 5 }, - { "min" : 4000, "max" : 6000, "density" : 5 }, - { "min" : 2000, "max" : 4000, "density" : 5 } - ] - }, - "11" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "mines" : { "mercury" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 5000, "max" : 7000, "density" : 3 }, - { "min" : 3000, "max" : 5000, "density" : 4 }, - { "min" : 1000, "max" : 3000, "density" : 5 } - ] - }, - "12" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "13" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "14" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "15" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "16" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "17" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "18" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 11, - "treasureLikeZone" : 11 - }, - "21" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "mines" : { "crystal" : 1, "gems" : 1 }, - "treasureLikeZone" : 11 - }, - "22" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "23" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "24" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "25" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "26" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "27" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "28" : - { - "type" : "treasure", - "size" : 200, - "monsters" : "strong", - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 21, - "treasureLikeZone" : 11 - }, - "31" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 2600, "max" : 4000, "density" : 2 }, - { "min" : 1600, "max" : 2500, "density" : 2 }, - { "min" : 100, "max" : 1500, "density" : 8 } - ] - }, - "32" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "33" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "34" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "35" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "36" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 6, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "37" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 7, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - }, - "38" : - { - "type" : "playerStart", - "size" : 600, - "owner" : 8, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], - "matchTerrainToTown" : true, - "minesLikeZone" : 31, - "treasureLikeZone" : 31 - } - }, - "connections" : - [ - { "a" : "11", "b" : "1", "guard" : 8000 }, - { "a" : "12", "b" : "1", "guard" : 8000 }, - { "a" : "13", "b" : "1", "guard" : 8000 }, - { "a" : "14", "b" : "1", "guard" : 8000 }, - { "a" : "15", "b" : "1", "guard" : 8000 }, - { "a" : "16", "b" : "1", "guard" : 8000 }, - { "a" : "17", "b" : "1", "guard" : 8000 }, - { "a" : "18", "b" : "1", "guard" : 8000 }, - { "a" : "21", "b" : "1", "guard" : 8000 }, - { "a" : "22", "b" : "1", "guard" : 8000 }, - { "a" : "23", "b" : "1", "guard" : 8000 }, - { "a" : "24", "b" : "1", "guard" : 8000 }, - { "a" : "25", "b" : "1", "guard" : 8000 }, - { "a" : "26", "b" : "1", "guard" : 8000 }, - { "a" : "27", "b" : "1", "guard" : 8000 }, - { "a" : "28", "b" : "1", "guard" : 8000 }, - { "a" : "31", "b" : "11", "guard" : 5000 }, - { "a" : "32", "b" : "12", "guard" : 5000 }, - { "a" : "33", "b" : "13", "guard" : 5000 }, - { "a" : "34", "b" : "14", "guard" : 5000 }, - { "a" : "35", "b" : "15", "guard" : 5000 }, - { "a" : "36", "b" : "16", "guard" : 5000 }, - { "a" : "37", "b" : "17", "guard" : 5000 }, - { "a" : "38", "b" : "18", "guard" : 5000 }, - { "a" : "31", "b" : "21", "guard" : 5000 }, - { "a" : "32", "b" : "22", "guard" : 5000 }, - { "a" : "33", "b" : "23", "guard" : 5000 }, - { "a" : "34", "b" : "24", "guard" : 5000 }, - { "a" : "35", "b" : "25", "guard" : 5000 }, - { "a" : "36", "b" : "26", "guard" : 5000 }, - { "a" : "37", "b" : "27", "guard" : 5000 }, - { "a" : "38", "b" : "28", "guard" : 5000 } - ] - } -} +{ + "GreatSands XL 2-8" : + { + "minSize" : "xl", "maxSize" : "h", + "players" : "2-8", + "zones" : + { + "1" : + { + "type" : "treasure", + "size" : 1600, + "monsters" : "strong", + "neutralTowns" : { "castles" : 1 }, + "allowedTowns" : [ "conflux" ], + "allowedMonsters" : [ "neutral", "conflux" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "sand" ], + "mines" : { "gold" : 2 }, + "treasure" : + [ + { "min" : 6000, "max" : 10000, "density" : 5 }, + { "min" : 4000, "max" : 6000, "density" : 5 }, + { "min" : 2000, "max" : 4000, "density" : 5 } + ] + }, + "11" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "mines" : { "mercury" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 5000, "max" : 7000, "density" : 3 }, + { "min" : 3000, "max" : 5000, "density" : 4 }, + { "min" : 1000, "max" : 3000, "density" : 5 } + ] + }, + "12" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "13" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "14" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "15" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "16" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "17" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "18" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 11, + "treasureLikeZone" : 11 + }, + "21" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "mines" : { "crystal" : 1, "gems" : 1 }, + "treasureLikeZone" : 11 + }, + "22" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "23" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "24" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "25" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "26" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "27" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "28" : + { + "type" : "treasure", + "size" : 200, + "monsters" : "strong", + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 21, + "treasureLikeZone" : 11 + }, + "31" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 2600, "max" : 4000, "density" : 2 }, + { "min" : 1600, "max" : 2500, "density" : 2 }, + { "min" : 100, "max" : 1500, "density" : 8 } + ] + }, + "32" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "33" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "34" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "35" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "36" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 6, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "37" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 7, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + }, + "38" : + { + "type" : "playerStart", + "size" : 600, + "owner" : 8, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "allowedMonsters" : [ "castle", "rampart", "tower", "inferno", "necropolis", "dungeon", "stronghold", "fortress" ], + "matchTerrainToTown" : true, + "minesLikeZone" : 31, + "treasureLikeZone" : 31 + } + }, + "connections" : + [ + { "a" : "11", "b" : "1", "guard" : 8000 }, + { "a" : "12", "b" : "1", "guard" : 8000 }, + { "a" : "13", "b" : "1", "guard" : 8000 }, + { "a" : "14", "b" : "1", "guard" : 8000 }, + { "a" : "15", "b" : "1", "guard" : 8000 }, + { "a" : "16", "b" : "1", "guard" : 8000 }, + { "a" : "17", "b" : "1", "guard" : 8000 }, + { "a" : "18", "b" : "1", "guard" : 8000 }, + { "a" : "21", "b" : "1", "guard" : 8000 }, + { "a" : "22", "b" : "1", "guard" : 8000 }, + { "a" : "23", "b" : "1", "guard" : 8000 }, + { "a" : "24", "b" : "1", "guard" : 8000 }, + { "a" : "25", "b" : "1", "guard" : 8000 }, + { "a" : "26", "b" : "1", "guard" : 8000 }, + { "a" : "27", "b" : "1", "guard" : 8000 }, + { "a" : "28", "b" : "1", "guard" : 8000 }, + { "a" : "31", "b" : "11", "guard" : 5000 }, + { "a" : "32", "b" : "12", "guard" : 5000 }, + { "a" : "33", "b" : "13", "guard" : 5000 }, + { "a" : "34", "b" : "14", "guard" : 5000 }, + { "a" : "35", "b" : "15", "guard" : 5000 }, + { "a" : "36", "b" : "16", "guard" : 5000 }, + { "a" : "37", "b" : "17", "guard" : 5000 }, + { "a" : "38", "b" : "18", "guard" : 5000 }, + { "a" : "31", "b" : "21", "guard" : 5000 }, + { "a" : "32", "b" : "22", "guard" : 5000 }, + { "a" : "33", "b" : "23", "guard" : 5000 }, + { "a" : "34", "b" : "24", "guard" : 5000 }, + { "a" : "35", "b" : "25", "guard" : 5000 }, + { "a" : "36", "b" : "26", "guard" : 5000 }, + { "a" : "37", "b" : "27", "guard" : 5000 }, + { "a" : "38", "b" : "28", "guard" : 5000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON index 9431b5355..131927e8a 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/kite.JSON @@ -1,141 +1,141 @@ -{ - "Kite" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 1000, "max" : 2100, "density" : 4 }, - { "min" : 3500, "max" : 4900, "density" : 7 }, - { "min" : 800, "max" : 800, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 8, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "neutralTowns" : { "towns" : 1 }, - "townsAreSameType" : true, - "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "matchTerrainToTown" : false, - "minesLikeZone" : 3, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 5 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "9" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - }, - "10" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 5 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 6000 }, - { "a" : "1", "b" : "10", "guard" : 6000 }, - { "a" : "2", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "9", "guard" : 6000 }, - { "a" : "9", "b" : "10", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 }, - { "a" : "5", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "6", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "9", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 12500 }, - { "a" : "4", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "10", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "4", "b" : "9", "guard" : 6000 } - ] - } -} +{ + "Kite" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 1000, "max" : 2100, "density" : 4 }, + { "min" : 3500, "max" : 4900, "density" : 7 }, + { "min" : 800, "max" : 800, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 8, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "neutralTowns" : { "towns" : 1 }, + "townsAreSameType" : true, + "allowedTowns" : [ "castle", "rampart", "tower", "inferno", "dungeon", "stronghold", "fortress" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "matchTerrainToTown" : false, + "minesLikeZone" : 3, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 5 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "9" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + }, + "10" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 5 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 6000 }, + { "a" : "1", "b" : "10", "guard" : 6000 }, + { "a" : "2", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "9", "guard" : 6000 }, + { "a" : "9", "b" : "10", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 }, + { "a" : "5", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "6", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "9", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 12500 }, + { "a" : "4", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "10", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "4", "b" : "9", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON b/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON index 609193652..acaf04086 100755 --- a/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON +++ b/Mods/vcmi/config/vcmi/rmg/unknownUnused/wheel.JSON @@ -1,154 +1,154 @@ -{ - "Wheel" : - { - "minSize" : "m", "maxSize" : "l+u", - "players" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 14, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 100, "max" : 500, "density" : 1 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 14, - "monsters" : "normal", - "neutralTowns" : { "castles" : 1 }, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 30, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 9 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 20000, "max" : 30000, "density" : 1 } - ] - }, - "10" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "13" : - { - "type" : "treasure", - "size" : 7, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "6", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "4", "guard" : 3000 }, - { "a" : "9", "b" : "13", "guard" : 10000 }, - { "a" : "9", "b" : "12", "guard" : 10000 }, - { "a" : "9", "b" : "11", "guard" : 10000 }, - { "a" : "9", "b" : "10", "guard" : 10000 }, - { "a" : "6", "b" : "13", "guard" : 6000 }, - { "a" : "7", "b" : "10", "guard" : 6000 }, - { "a" : "4", "b" : "11", "guard" : 6000 }, - { "a" : "5", "b" : "12", "guard" : 6000 } - ] - } -} +{ + "Wheel" : + { + "minSize" : "m", "maxSize" : "l+u", + "players" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 14, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 100, "max" : 500, "density" : 1 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 14, + "monsters" : "normal", + "neutralTowns" : { "castles" : 1 }, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 30, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 9 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 20000, "max" : 30000, "density" : 1 } + ] + }, + "10" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "13" : + { + "type" : "treasure", + "size" : 7, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "6", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "4", "guard" : 3000 }, + { "a" : "9", "b" : "13", "guard" : 10000 }, + { "a" : "9", "b" : "12", "guard" : 10000 }, + { "a" : "9", "b" : "11", "guard" : 10000 }, + { "a" : "9", "b" : "10", "guard" : 10000 }, + { "a" : "6", "b" : "13", "guard" : 6000 }, + { "a" : "7", "b" : "10", "guard" : 6000 }, + { "a" : "4", "b" : "11", "guard" : 6000 }, + { "a" : "5", "b" : "12", "guard" : 6000 } + ] + } +} From 7e9a3ea04218e830fe06a372778cc1dc95bac8fc Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 22 Oct 2023 17:44:51 +0000 Subject: [PATCH 0918/1248] CI step "Ensure LF line endings": Don't create subshell for grouped commands --- .github/workflows/github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index aa3604c5c..2ffefd71b 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -151,7 +151,7 @@ jobs: find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./android -prune \ -o -path ./osx -prune -o -path ./test/googletest -prune -o -type f \ -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' \ - -not -name '*.wav' -not -name '*.ico' -print0 | ( ! xargs -0 grep -l -z -P '\r\n' ) + -not -name '*.wav' -not -name '*.ico' -print0 | { ! xargs -0 grep -l -z -P '\r\n'; } - name: Validate JSON # the Python yaml module doesn't seem to work on mac-arm From 9eb9404f28e9b6809911fd91fdd5e4f636d2eef8 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 3 Oct 2023 08:16:12 +0300 Subject: [PATCH 0919/1248] BattleAI: divide only positive part of score onto turns count --- AI/BattleAI/BattleEvaluator.cpp | 10 +- AI/BattleAI/BattleExchangeVariant.cpp | 148 +++++++++++++++++++------- AI/BattleAI/BattleExchangeVariant.h | 75 +++++++++++-- AI/Nullkiller/Engine/Nullkiller.cpp | 9 +- 4 files changed, 187 insertions(+), 55 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index cae77e61f..10e11906a 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -190,6 +190,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) if(stack->waited()) { + logAi->debug( + "Moving %s towards hex %s[%d], score: %2f/%2f", + stack->getDescription(), + moveTarget.cachedAttack->attack.defender->getDescription(), + moveTarget.cachedAttack->attack.defender->getPosition().hex, + moveTarget.score, + moveTarget.scorePerTurn); + return goTowardsNearest(stack, moveTarget.positions); } else @@ -572,7 +580,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) } else { - ps.value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state); + ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, *targets, innerCache, state); } for(auto unit : allUnits) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 8ae233e41..547f02cd2 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -18,10 +18,8 @@ AttackerValue::AttackerValue() } MoveTarget::MoveTarget() - : positions(), cachedAttack() + : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) { - score = EvaluationResult::INEFFECTIVE_SCORE; - scorePerTurn = EvaluationResult::INEFFECTIVE_SCORE; turnsToRich = 1; } @@ -58,7 +56,7 @@ float BattleExchangeVariant::trackAttack( auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb); attackValue -= attackerDamageReduce; - dpsScore -= attackerDamageReduce * negativeEffectMultiplier; + dpsScore.ourDamageReduce += attackerDamageReduce; attackerValue[unitToUpdate->unitId()].isRetalitated = true; unitToUpdate->damage(retaliationDamage); @@ -80,7 +78,7 @@ float BattleExchangeVariant::trackAttack( auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb); attackValue -= collateralDamageReduce; - dpsScore -= collateralDamageReduce * negativeEffectMultiplier; + dpsScore.ourDamageReduce += collateralDamageReduce; unitToUpdate->damage(collateralDamage); @@ -101,7 +99,7 @@ float BattleExchangeVariant::trackAttack( float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb); attackValue += defenderDamageReduce; - dpsScore += defenderDamageReduce * positiveEffectMultiplier; + dpsScore.enemyDamageReduce += defenderDamageReduce; attackerValue[attacker->unitId()].value += defenderDamageReduce; unitToUpdate->damage(attackDamage); @@ -119,7 +117,7 @@ float BattleExchangeVariant::trackAttack( } attackValue += ap.shootersBlockedDmg; - dpsScore += ap.shootersBlockedDmg * positiveEffectMultiplier; + dpsScore.enemyDamageReduce += ap.shootersBlockedDmg; attacker->afterAttack(ap.attack.shooting, false); return attackValue; @@ -156,11 +154,11 @@ float BattleExchangeVariant::trackAttack( if(isOurAttack) { - dpsScore += defenderDamageReduce * positiveEffectMultiplier; + dpsScore.enemyDamageReduce += defenderDamageReduce; attackerValue[attacker->unitId()].value += defenderDamageReduce; } else - dpsScore -= defenderDamageReduce * negativeEffectMultiplier; + dpsScore.ourDamageReduce += defenderDamageReduce; defender->damage(attackDamage); attacker->afterAttack(shooting, false); @@ -182,12 +180,12 @@ float BattleExchangeVariant::trackAttack( if(isOurAttack) { - dpsScore -= attackerDamageReduce * negativeEffectMultiplier; + dpsScore.ourDamageReduce += attackerDamageReduce; attackerValue[attacker->unitId()].isRetalitated = true; } else { - dpsScore += attackerDamageReduce * positiveEffectMultiplier; + dpsScore.enemyDamageReduce += attackerDamageReduce; attackerValue[defender->unitId()].value += attackerDamageReduce; } @@ -200,13 +198,18 @@ float BattleExchangeVariant::trackAttack( #if BATTLE_TRACE_LEVEL>=1 if(!score) { - logAi->trace("Attack has zero score d:%2f a:%2f", defenderDamageReduce, attackerDamageReduce); + logAi->trace("Attack has zero score def:%2f att:%2f", defenderDamageReduce, attackerDamageReduce); } #endif return score; } +float BattleExchangeEvaluator::scoreValue(const BattleScore & score) const +{ + return score.enemyDamageReduce * getPositiveEffectMultiplier() - score.ourDamageReduce * getNegativeEffectMultiplier(); +} + EvaluationResult BattleExchangeEvaluator::findBestTarget( const battle::Unit * activeStack, PotentialTargets & targets, @@ -215,7 +218,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( { EvaluationResult result(targets.bestAction()); - if(!activeStack->waited()) + if(!activeStack->waited() && !activeStack->acquireState()->hadMorale) { #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Evaluating waited attack for %s", activeStack->getDescription()); @@ -230,7 +233,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( for(auto & ap : targets.possibleAttacks) { - float score = calculateExchange(ap, targets, damageCache, hbWaited); + float score = evaluateExchange(ap, targets, damageCache, hbWaited); if(score > result.score) { @@ -247,9 +250,15 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( updateReachabilityMap(hb); + if(result.bestAttack.attack.shooting && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) + { + if(!canBeHitThisTurn(result.bestAttack)) + return result; // lets wait + } + for(auto & ap : targets.possibleAttacks) { - float score = calculateExchange(ap, targets, damageCache, hb); + float score = evaluateExchange(ap, targets, damageCache, hb); if(score >= result.score) { @@ -269,7 +278,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( std::shared_ptr hb) { MoveTarget result; - BattleExchangeVariant ev(getPositiveEffectMultiplier(), getNegativeEffectMultiplier()); + BattleExchangeVariant ev; if(targets.unreachableEnemies.empty()) return result; @@ -301,6 +310,9 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( auto turnsToRich = (distance - 1) / speed + 1; auto hexes = closestStack->getSurroundingHexes(); + auto enemySpeed = closestStack->speed(); + auto speedRatio = speed / static_cast(enemySpeed); + auto penalty = speedRatio > 1 ? 1 : speedRatio; for(auto hex : hexes) { @@ -310,13 +322,13 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure - auto score = calculateExchange(attack, targets, damageCache, hb); - auto scorePerTurn = score / turnsToRich; + auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); + auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(penalty / turnsToRich), score.ourDamageReduce); - if(result.scorePerTurn < scorePerTurn) + if(result.scorePerTurn < scoreValue(scorePerTurn)) { - result.scorePerTurn = scorePerTurn; - result.score = score; + result.scorePerTurn = scoreValue(scorePerTurn); + result.score = scoreValue(score); result.positions = closestStack->getAttackableHexes(activeStack); result.cachedAttack = attack; result.turnsToRich = turnsToRich; @@ -357,16 +369,17 @@ std::vector BattleExchangeEvaluator::getAdjacentUnits(cons return checkedStacks; } -std::vector BattleExchangeEvaluator::getExchangeUnits( +ReachabilityData BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, PotentialTargets & targets, std::shared_ptr hb) { + ReachabilityData result; + auto hexes = ap.attack.defender->getHexes(); if(!ap.attack.shooting) hexes.push_back(ap.from); - std::vector exchangeUnits; std::vector allReachableUnits; for(auto hex : hexes) @@ -404,7 +417,28 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( logAi->trace("Reachability map contains only %d stacks", allReachableUnits.size()); #endif - return exchangeUnits; + return result; + } + + for(auto unit : allReachableUnits) + { + auto accessible = !unit->canShoot(); + + if(!accessible) + { + for(auto hex : unit->getSurroundingHexes()) + { + if(ap.attack.defender->coversPos(hex)) + { + accessible = true; + } + } + } + + if(accessible) + result.melleeAccessible.push_back(unit); + else + result.shooters.push_back(unit); } for(int turn = 0; turn < turnOrder.size(); turn++) @@ -412,19 +446,30 @@ std::vector BattleExchangeEvaluator::getExchangeUnits( for(auto unit : turnOrder[turn]) { if(vstd::contains(allReachableUnits, unit)) - exchangeUnits.push_back(unit); + result.units.push_back(unit); } } - vstd::erase_if(exchangeUnits, [&](const battle::Unit * u) -> bool + vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool { return !hb->battleGetUnitByID(u->unitId())->alive(); }); - return exchangeUnits; + return result; } -float BattleExchangeEvaluator::calculateExchange( +float BattleExchangeEvaluator::evaluateExchange( + const AttackPossibility & ap, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb) +{ + BattleScore score = calculateExchange(ap, targets, damageCache, hb); + + return scoreValue(score); +} + +BattleScore BattleExchangeEvaluator::calculateExchange( const AttackPossibility & ap, PotentialTargets & targets, DamageCache & damageCache, @@ -438,7 +483,7 @@ float BattleExchangeEvaluator::calculateExchange( && cb->battleGetGateState() == EGateState::BLOCKED && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) { - return EvaluationResult::INEFFECTIVE_SCORE; + return BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); } std::vector ourStacks; @@ -447,17 +492,19 @@ float BattleExchangeEvaluator::calculateExchange( if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) enemyStacks.push_back(ap.attack.defender); - std::vector exchangeUnits = getExchangeUnits(ap, targets, hb); + vstd::amin(turn, reachabilityMapByTurns.size() - 1); - if(exchangeUnits.empty()) + ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); + + if(exchangeUnits.units.empty()) { - return 0; + return BattleScore(); } auto exchangeBattle = std::make_shared(env.get(), hb); - BattleExchangeVariant v(getPositiveEffectMultiplier(), getNegativeEffectMultiplier()); + BattleExchangeVariant v; - for(auto unit : exchangeUnits) + for(auto unit : exchangeUnits.units) { if(unit->isTurret()) continue; @@ -481,7 +528,7 @@ float BattleExchangeEvaluator::calculateExchange( bool canUseAp = true; - for(auto activeUnit : exchangeUnits) + for(auto activeUnit : exchangeUnits.units) { bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true); battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; @@ -521,9 +568,16 @@ float BattleExchangeEvaluator::calculateExchange( return score; }; - if(!oppositeQueue.empty()) + auto unitsInOppositeQueueExceptInaccessible = oppositeQueue; + + vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool + { + return vstd::contains(exchangeUnits.shooters, u); + }); + + if(!unitsInOppositeQueueExceptInaccessible.empty()) { - targetUnit = *vstd::maxElementByFun(oppositeQueue, estimateAttack); + targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack); } else { @@ -594,7 +648,7 @@ float BattleExchangeEvaluator::calculateExchange( v.adjustPositions(melleeAttackers, ap, reachabilityMap); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Exchange score: %2f", v.getScore()); + logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); #endif return v.getScore(); @@ -662,10 +716,26 @@ void BattleExchangeVariant::adjustPositions( if(notRealizedDamage > ap.attackValue() && notRealizedDamage > attackerValue[ap.attack.attacker->unitId()].value) { - dpsScore = EvaluationResult::INEFFECTIVE_SCORE; + dpsScore = BattleScore(EvaluationResult::INEFFECTIVE_SCORE, 0); } } +bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) +{ + for(auto pos : ap.attack.attacker->getSurroundingHexes()) + { + for(auto u : reachabilityMap[pos]) + { + if(u->unitSide() != ap.attack.attacker->unitSide()) + { + return true; + } + } + } + + return false; +} + void BattleExchangeEvaluator::updateReachabilityMap( std::shared_ptr hb) { const int TURN_DEPTH = 2; diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 36b8b2592..a7365591a 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -14,6 +14,34 @@ #include "PotentialTargets.h" #include "StackWithBonuses.h" +struct BattleScore +{ + float ourDamageReduce; + float enemyDamageReduce; + + BattleScore(float enemyDamageReduce, float ourDamageReduce) + :enemyDamageReduce(enemyDamageReduce), ourDamageReduce(ourDamageReduce) + { + } + + BattleScore() : BattleScore(0, 0) {} + + float value() + { + return enemyDamageReduce - ourDamageReduce; + } + + BattleScore operator+(BattleScore & other) + { + BattleScore result = *this; + + result.ourDamageReduce += other.ourDamageReduce; + result.enemyDamageReduce += other.enemyDamageReduce; + + return result; + } +}; + struct AttackerValue { float value; @@ -59,8 +87,8 @@ struct EvaluationResult class BattleExchangeVariant { public: - BattleExchangeVariant(float positiveEffectMultiplier, float negativeEffectMultiplier) - : dpsScore(0), positiveEffectMultiplier(positiveEffectMultiplier), negativeEffectMultiplier(negativeEffectMultiplier) {} + BattleExchangeVariant() + : dpsScore() {} float trackAttack( const AttackPossibility & ap, @@ -76,7 +104,7 @@ public: std::shared_ptr hb, bool evaluateOnly = false); - float getScore() const { return dpsScore; } + const BattleScore & getScore() const { return dpsScore; } void adjustPositions( std::vector attackers, @@ -84,12 +112,21 @@ public: std::map & reachabilityMap); private: - float positiveEffectMultiplier; - float negativeEffectMultiplier; - float dpsScore; + BattleScore dpsScore; std::map attackerValue; }; +struct ReachabilityData +{ + std::vector units; + + // shooters which are within mellee attack and mellee units + std::vector melleeAccessible; + + // far shooters + std::vector shooters; +}; + class BattleExchangeEvaluator { private: @@ -99,12 +136,22 @@ private: std::vector turnOrder; float negativeEffectMultiplier; + float scoreValue(const BattleScore & score) const; + + BattleScore calculateExchange( + const AttackPossibility & ap, + PotentialTargets & targets, + DamageCache & damageCache, + std::shared_ptr hb); + + bool canBeHitThisTurn(const AttackPossibility & ap); + public: BattleExchangeEvaluator( std::shared_ptr cb, std::shared_ptr env, float strengthRatio): cb(cb), env(env) { - negativeEffectMultiplier = strengthRatio; + negativeEffectMultiplier = std::sqrt(strengthRatio); } EvaluationResult findBestTarget( @@ -113,14 +160,20 @@ public: DamageCache & damageCache, std::shared_ptr hb); - float calculateExchange( + float evaluateExchange( const AttackPossibility & ap, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb); void updateReachabilityMap(std::shared_ptr hb); - std::vector getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, std::shared_ptr hb); + + ReachabilityData getExchangeUnits( + const AttackPossibility & ap, + uint8_t turn, + PotentialTargets & targets, + std::shared_ptr hb); + bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position); MoveTarget findMoveTowardsUnreachable( @@ -131,6 +184,6 @@ public: std::vector getAdjacentUnits(const battle::Unit * unit); - float getPositiveEffectMultiplier() { return 1; } - float getNegativeEffectMultiplier() { return negativeEffectMultiplier; } + float getPositiveEffectMultiplier() const { return 1; } + float getNegativeEffectMultiplier() const { return negativeEffectMultiplier; } }; diff --git a/AI/Nullkiller/Engine/Nullkiller.cpp b/AI/Nullkiller/Engine/Nullkiller.cpp index 3d9a78c33..79c21d1ab 100644 --- a/AI/Nullkiller/Engine/Nullkiller.cpp +++ b/AI/Nullkiller/Engine/Nullkiller.cpp @@ -274,6 +274,7 @@ void Nullkiller::makeTurn() bestTask = choseBestTask(bestTasks); + std::string taskDescription = bestTask->toString(); HeroPtr hero = bestTask->getHero(); HeroRole heroRole = HeroRole::MAIN; @@ -292,7 +293,7 @@ void Nullkiller::makeTurn() logAi->trace( "Goal %s has low priority %f so decreasing scan depth to gain performance.", - bestTask->toString(), + taskDescription, bestTask->priority); } @@ -308,7 +309,7 @@ void Nullkiller::makeTurn() { logAi->trace( "Goal %s has too low priority %f so increasing scan depth to full.", - bestTask->toString(), + taskDescription, bestTask->priority); scanDepth = ScanDepth::ALL_FULL; @@ -316,7 +317,7 @@ void Nullkiller::makeTurn() continue; } - logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString()); + logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", taskDescription); return; } @@ -325,7 +326,7 @@ void Nullkiller::makeTurn() if(i == MAXPASS) { - logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", bestTask->toString()); + logAi->error("Goal %s exceeded maxpass. Terminating AI turn.", taskDescription); } } } From 870fbd50e3eeae378395c7f6f8525a83d08c1a00 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Fri, 13 Oct 2023 22:04:26 +0300 Subject: [PATCH 0920/1248] BattleAI: bigger reachability map --- AI/BattleAI/AttackPossibility.cpp | 8 +- AI/BattleAI/BattleEvaluator.cpp | 4 +- AI/BattleAI/BattleExchangeVariant.cpp | 147 +++++++++++++++++++------- AI/BattleAI/BattleExchangeVariant.h | 6 +- 4 files changed, 115 insertions(+), 50 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 9a34420ea..54a28005d 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -157,13 +157,9 @@ float AttackPossibility::calculateDamageReduce( auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); auto enemiesKilled = damageDealt / maxHealth + (damageDealt % maxHealth >= defender->getFirstHPleft() ? 1 : 0); auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); - - // lets use cached maxHealth here instead of getAvailableHealth - auto firstUnitHpLeft = (availableHealth - damageDealt) % maxHealth; - auto firstUnitHealthRatio = firstUnitHpLeft == 0 ? 1 : static_cast(firstUnitHpLeft) / maxHealth; - auto firstUnitKillValue = (1 - firstUnitHealthRatio) * (1 - firstUnitHealthRatio); + auto lastUnitKillValue = (damageDealt % maxHealth) / (double)maxHealth;; - return damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY); + return damagePerEnemy * (enemiesKilled + lastUnitKillValue * HEALTH_BOUNTY); } int64_t AttackPossibility::evaluateBlockedShootersDmg( diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index 10e11906a..7373f77ed 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -149,7 +149,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) bestAttack.attack.attacker->speed(0, true), bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, - bestAttack.attackValue() + score ); if (moveTarget.scorePerTurn <= score) @@ -580,7 +580,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack) } else { - ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, *targets, innerCache, state); + ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state); } for(auto unit : allUnits) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 547f02cd2..e7878cf7d 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -116,6 +116,10 @@ float BattleExchangeVariant::trackAttack( } } +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg); +#endif + attackValue += ap.shootersBlockedDmg; dpsScore.enemyDamageReduce += ap.shootersBlockedDmg; attacker->afterAttack(ap.attack.shooting, false); @@ -233,13 +237,17 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( for(auto & ap : targets.possibleAttacks) { - float score = evaluateExchange(ap, targets, damageCache, hbWaited); + float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited); if(score > result.score) { result.score = score; result.bestAttack = ap; result.wait = true; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score %2f", result.score); +#endif } } } @@ -258,13 +266,17 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( for(auto & ap : targets.possibleAttacks) { - float score = evaluateExchange(ap, targets, damageCache, hb); + float score = evaluateExchange(ap, 0, targets, damageCache, hb); - if(score >= result.score) + if(score > result.score || score == result.score && result.wait) { result.score = score; result.bestAttack = ap; result.wait = false; + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace("New high score %2f", result.score); +#endif } } @@ -312,7 +324,10 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( auto hexes = closestStack->getSurroundingHexes(); auto enemySpeed = closestStack->speed(); auto speedRatio = speed / static_cast(enemySpeed); - auto penalty = speedRatio > 1 ? 1 : speedRatio; + auto multiplier = speedRatio > 1 ? 1 : speedRatio; + + if(enemy->canShoot()) + multiplier *= 1.5f; for(auto hex : hexes) { @@ -323,7 +338,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); - auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(penalty / turnsToRich), score.ourDamageReduce); + auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce); if(result.scorePerTurn < scoreValue(scorePerTurn)) { @@ -371,12 +386,13 @@ std::vector BattleExchangeEvaluator::getAdjacentUnits(cons ReachabilityData BattleExchangeEvaluator::getExchangeUnits( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, std::shared_ptr hb) { ReachabilityData result; - auto hexes = ap.attack.defender->getHexes(); + auto hexes = ap.attack.defender->getSurroundingHexes(); if(!ap.attack.shooting) hexes.push_back(ap.from); @@ -384,7 +400,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( for(auto hex : hexes) { - vstd::concatenate(allReachableUnits, reachabilityMap[hex]); + vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap[hex] : getOneTurnReachableUnits(turn, hex)); } vstd::removeDuplicates(allReachableUnits); @@ -460,17 +476,33 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits( float BattleExchangeEvaluator::evaluateExchange( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb) { - BattleScore score = calculateExchange(ap, targets, damageCache, hb); + if(ap.from.hex == 127) + { + logAi->trace("x"); + } + + BattleScore score = calculateExchange(ap, turn, targets, damageCache, hb); + +#if BATTLE_TRACE_LEVEL >= 1 + logAi->trace( + "calculateExchange score +%2f -%2fx%2f = %2f", + score.enemyDamageReduce, + score.ourDamageReduce, + getNegativeEffectMultiplier(), + scoreValue(score)); +#endif return scoreValue(score); } BattleScore BattleExchangeEvaluator::calculateExchange( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb) @@ -492,8 +524,6 @@ BattleScore BattleExchangeEvaluator::calculateExchange( if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) enemyStacks.push_back(ap.attack.defender); - vstd::amin(turn, reachabilityMapByTurns.size() - 1); - ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb); if(exchangeUnits.units.empty()) @@ -511,10 +541,15 @@ BattleScore BattleExchangeEvaluator::calculateExchange( bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true); auto & attackerQueue = isOur ? ourStacks : enemyStacks; + auto u = exchangeBattle->getForUpdate(unit->unitId()); - if(exchangeBattle->getForUpdate(unit->unitId())->alive() && !vstd::contains(attackerQueue, unit)) + if(u->alive() && !vstd::contains(attackerQueue, unit)) { attackerQueue.push_back(unit); + +#if BATTLE_TRACE_LEVEL + logAi->trace("Exchanging: %s", u->getDescription()); +#endif } } @@ -562,7 +597,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange( true); #if BATTLE_TRACE_LEVEL>=1 - logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), u->getDescription(), score); + logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score); #endif return score; @@ -645,6 +680,13 @@ BattleScore BattleExchangeEvaluator::calculateExchange( // avoid blocking path for stronger stack by weaker stack // the method checks if all stacks can be placed around enemy + std::map reachabilityMap; + + auto hexes = ap.attack.defender->getSurroundingHexes(); + + for(auto hex : hexes) + reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); + v.adjustPositions(melleeAttackers, ap, reachabilityMap); #if BATTLE_TRACE_LEVEL>=1 @@ -736,18 +778,38 @@ bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) return false; } -void BattleExchangeEvaluator::updateReachabilityMap( std::shared_ptr hb) +void BattleExchangeEvaluator::updateReachabilityMap(std::shared_ptr hb) { const int TURN_DEPTH = 2; turnOrder.clear(); - - hb->battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); - reachabilityMap.clear(); - for(int turn = 0; turn < turnOrder.size(); turn++) + hb->battleGetTurnOrder(turnOrder, std::numeric_limits::max(), TURN_DEPTH); + + for(auto turn : turnOrder) { - auto & turnQueue = turnOrder[turn]; + for(auto u : turn) + { + if(!vstd::contains(reachabilityCache, u->unitId())) + { + reachabilityCache[u->unitId()] = hb->getReachability(u); + } + } + } + + for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) + { + reachabilityMap[hex] = getOneTurnReachableUnits(0, hex); + } +} + +std::vector BattleExchangeEvaluator::getOneTurnReachableUnits(uint8_t turn, BattleHex hex) +{ + std::vector result; + + for(int i = 0; i < turnOrder.size(); i++, turn++) + { + auto & turnQueue = turnOrder[i]; HypotheticBattle turnBattle(env.get(), cb); for(const battle::Unit * unit : turnQueue) @@ -755,46 +817,49 @@ void BattleExchangeEvaluator::updateReachabilityMap( std::shared_ptrisTurret()) continue; - auto unitSpeed = unit->speed(turn); - if(turnBattle.battleCanShoot(unit)) { - for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) - { - reachabilityMap[hex].push_back(unit); - } + result.push_back(unit); continue; } - auto unitReachability = turnBattle.getReachability(unit); + auto unitSpeed = unit->speed(turn); + auto radius = unitSpeed * (turn + 1); - for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) - { - bool reachable = unitReachability.distances[hex] <= unitSpeed; - - if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) + ReachabilityInfo unitReachability = vstd::getOrCompute( + reachabilityCache, + unit->unitId(), + [&](ReachabilityInfo & data) { - const battle::Unit * hexStack = cb->battleGetUnitByPos(hex); + data = turnBattle.getReachability(unit); + }); - if(hexStack && cb->battleMatchOwner(unit, hexStack, false)) + bool reachable = unitReachability.distances[hex] <= radius; + + if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) + { + const battle::Unit * hexStack = cb->battleGetUnitByPos(hex); + + if(hexStack && cb->battleMatchOwner(unit, hexStack, false)) + { + for(BattleHex neighbor : hex.neighbouringTiles()) { - for(BattleHex neighbor : hex.neighbouringTiles()) - { - reachable = unitReachability.distances[neighbor] <= unitSpeed; + reachable = unitReachability.distances[neighbor] <= radius; - if(reachable) break; - } + if(reachable) break; } } + } - if(reachable) - { - reachabilityMap[hex].push_back(unit); - } + if(reachable) + { + result.push_back(unit); } } } + + return result; } // avoid blocking path for stronger stack by weaker stack diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index a7365591a..0b8aed671 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -132,6 +132,7 @@ class BattleExchangeEvaluator private: std::shared_ptr cb; std::shared_ptr env; + std::map reachabilityCache; std::map> reachabilityMap; std::vector turnOrder; float negativeEffectMultiplier; @@ -140,6 +141,7 @@ private: BattleScore calculateExchange( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb); @@ -151,7 +153,7 @@ public: std::shared_ptr cb, std::shared_ptr env, float strengthRatio): cb(cb), env(env) { - negativeEffectMultiplier = std::sqrt(strengthRatio); + negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio; } EvaluationResult findBestTarget( @@ -162,10 +164,12 @@ public: float evaluateExchange( const AttackPossibility & ap, + uint8_t turn, PotentialTargets & targets, DamageCache & damageCache, std::shared_ptr hb); + std::vector getOneTurnReachableUnits(uint8_t turn, BattleHex hex); void updateReachabilityMap(std::shared_ptr hb); ReachabilityData getExchangeUnits( From f74daa2e1f778f4f9f7c54c7510abb7a929a1e33 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 22 Oct 2023 21:37:43 +0300 Subject: [PATCH 0921/1248] BattleAI: fix health bounty calculation --- AI/BattleAI/AttackPossibility.cpp | 33 +++++++++++++++++++++++++-- AI/BattleAI/BattleExchangeVariant.cpp | 16 ++++++------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/AI/BattleAI/AttackPossibility.cpp b/AI/BattleAI/AttackPossibility.cpp index 54a28005d..73dfd12af 100644 --- a/AI/BattleAI/AttackPossibility.cpp +++ b/AI/BattleAI/AttackPossibility.cpp @@ -114,6 +114,23 @@ float AttackPossibility::attackValue() const return damageDiff(); } +float hpFunction(uint64_t unitHealthStart, uint64_t unitHealthEnd, uint64_t maxHealth) +{ + float ratioStart = static_cast(unitHealthStart) / maxHealth; + float ratioEnd = static_cast(unitHealthEnd) / maxHealth; + float base = 0.666666f; + + // reduce from max to 0 must be 1. + // 10 hp from end costs bit more than 10 hp from start because our goal is to kill unit, not just hurt it + // ********** 2 * base - ratioStart ********* + // * * + // * height = ratioStart - ratioEnd * + // * * + // ******************** 2 * base - ratioEnd ****** + // S = (a + b) * h / 2 + return (base * (4 - ratioStart - ratioEnd)) * (ratioStart - ratioEnd) / 2 ; +} + /// /// How enemy damage will be reduced by this attack /// Half bounty for kill, half for making damage equal to enemy health @@ -127,6 +144,7 @@ float AttackPossibility::calculateDamageReduce( std::shared_ptr state) { const float HEALTH_BOUNTY = 0.5; + const float KILL_BOUNTY = 0.5; // FIXME: provide distance info for Jousting bonus auto attackerUnitForMeasurement = attacker; @@ -157,9 +175,20 @@ float AttackPossibility::calculateDamageReduce( auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state); auto enemiesKilled = damageDealt / maxHealth + (damageDealt % maxHealth >= defender->getFirstHPleft() ? 1 : 0); auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount(); - auto lastUnitKillValue = (damageDealt % maxHealth) / (double)maxHealth;; + auto exceedingDamage = (damageDealt % maxHealth); + float hpValue = (damageDealt / maxHealth); + + if(defender->getFirstHPleft() >= exceedingDamage) + { + hpValue += hpFunction(defender->getFirstHPleft(), defender->getFirstHPleft() - exceedingDamage, maxHealth); + } + else + { + hpValue += hpFunction(defender->getFirstHPleft(), 0, maxHealth); + hpValue += hpFunction(maxHealth, maxHealth + defender->getFirstHPleft() - exceedingDamage, maxHealth); + } - return damagePerEnemy * (enemiesKilled + lastUnitKillValue * HEALTH_BOUNTY); + return damagePerEnemy * (enemiesKilled * KILL_BOUNTY + hpValue * HEALTH_BOUNTY); } int64_t AttackPossibility::evaluateBlockedShootersDmg( diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index e7878cf7d..be1a04fae 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -268,7 +268,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget( { float score = evaluateExchange(ap, 0, targets, damageCache, hb); - if(score > result.score || score == result.score && result.wait) + if(score > result.score || (score == result.score && result.wait)) { result.score = score; result.bestAttack = ap; @@ -558,7 +558,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange( vstd::removeDuplicates(melleeAttackers); vstd::erase_if(melleeAttackers, [&](const battle::Unit * u) -> bool { - return !cb->battleCanShoot(u); + return cb->battleCanShoot(u); }); bool canUseAp = true; @@ -687,7 +687,10 @@ BattleScore BattleExchangeEvaluator::calculateExchange( for(auto hex : hexes) reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); - v.adjustPositions(melleeAttackers, ap, reachabilityMap); + if(!ap.attack.shooting) + { + v.adjustPositions(melleeAttackers, ap, reachabilityMap); + } #if BATTLE_TRACE_LEVEL>=1 logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce); @@ -714,11 +717,8 @@ void BattleExchangeVariant::adjustPositions( return attackerValue[u1->unitId()].value > attackerValue[u2->unitId()].value; }); - if(!ap.attack.shooting) - { - vstd::erase_if_present(hexes, ap.from); - vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); - } + vstd::erase_if_present(hexes, ap.from); + vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); float notRealizedDamage = 0; From d1d4db26e0badcb577f307a7cd967597c8f5a4af Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 22 Oct 2023 22:20:36 +0300 Subject: [PATCH 0922/1248] Moved existing files to new directory --- lib/{ => networkPacks}/NetPackVisitor.h | 0 lib/{ => networkPacks}/NetPacksBase.h | 0 lib/{ => networkPacks}/NetPacksLib.cpp | 0 lib/{ => networkPacks}/NetPacksLobby.h | 0 lib/{NetPacks.h => networkPacks/PacksForClient.h} | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename lib/{ => networkPacks}/NetPackVisitor.h (100%) rename lib/{ => networkPacks}/NetPacksBase.h (100%) rename lib/{ => networkPacks}/NetPacksLib.cpp (100%) rename lib/{ => networkPacks}/NetPacksLobby.h (100%) rename lib/{NetPacks.h => networkPacks/PacksForClient.h} (100%) diff --git a/lib/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h similarity index 100% rename from lib/NetPackVisitor.h rename to lib/networkPacks/NetPackVisitor.h diff --git a/lib/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h similarity index 100% rename from lib/NetPacksBase.h rename to lib/networkPacks/NetPacksBase.h diff --git a/lib/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp similarity index 100% rename from lib/NetPacksLib.cpp rename to lib/networkPacks/NetPacksLib.cpp diff --git a/lib/NetPacksLobby.h b/lib/networkPacks/NetPacksLobby.h similarity index 100% rename from lib/NetPacksLobby.h rename to lib/networkPacks/NetPacksLobby.h diff --git a/lib/NetPacks.h b/lib/networkPacks/PacksForClient.h similarity index 100% rename from lib/NetPacks.h rename to lib/networkPacks/PacksForClient.h From b88a8da4e89923450bff9f45fefe13d6e64c19d2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 13:59:15 +0300 Subject: [PATCH 0923/1248] Split off some netpack structures into separate files --- AI/BattleAI/StackWithBonuses.cpp | 2 +- AI/Nullkiller/AIGateway.cpp | 6 +- AI/VCAI/VCAI.cpp | 5 +- CCallback.cpp | 3 +- client/CPlayerInterface.cpp | 6 +- client/CServerHandler.cpp | 1 - client/ClientCommandManager.cpp | 2 +- client/ClientNetPackVisitors.h | 2 +- client/HeroMovementController.cpp | 2 +- client/LobbyClientNetPackVisitors.h | 2 +- client/NetPacksLobbyClient.cpp | 1 - client/adventureMap/CInfoBar.h | 1 + client/battle/BattleEffectsController.cpp | 2 +- client/battle/BattleInterface.cpp | 2 +- client/battle/BattleInterfaceClasses.cpp | 2 +- client/battle/BattleSiegeController.cpp | 2 +- client/lobby/CLobbyScreen.cpp | 2 +- client/lobby/CSelectionBase.cpp | 1 - client/lobby/OptionsTab.cpp | 2 +- client/lobby/SelectionTab.cpp | 1 - client/mainmenu/CMainMenu.cpp | 1 - client/widgets/CArtifactHolder.cpp | 1 + client/widgets/CArtifactsOfHeroAltar.cpp | 3 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 1 + client/widgets/CArtifactsOfHeroBase.cpp | 1 + client/widgets/CArtifactsOfHeroKingdom.cpp | 1 + client/widgets/CArtifactsOfHeroMain.cpp | 3 +- client/widgets/CComponent.cpp | 2 +- client/widgets/CGarrisonInt.cpp | 1 + client/widgets/CWindowWithArtifacts.cpp | 1 + client/windows/CCreatureWindow.cpp | 1 + client/windows/CHeroWindow.cpp | 2 +- client/windows/CTradeWindow.cpp | 1 + cmake_modules/VCMI_lib.cmake | 20 +- lib/CArtifactInstance.cpp | 2 +- lib/CCreatureSet.cpp | 1 - lib/CGameInfoCallback.cpp | 3 +- lib/CStack.cpp | 2 +- lib/IGameCallback.cpp | 2 +- lib/IGameEventsReceiver.h | 3 +- lib/battle/BattleInfo.cpp | 2 +- lib/battle/CBattleInfoCallback.cpp | 2 +- lib/battle/CBattleInfoEssentials.cpp | 3 +- lib/battle/CObstacleInstance.cpp | 1 - lib/battle/CObstacleInstance.h | 3 +- lib/battle/CUnitState.cpp | 2 +- lib/battle/Unit.cpp | 1 - lib/events/ApplyDamage.cpp | 3 +- lib/mapObjects/CBank.cpp | 6 +- lib/mapObjects/CGCreature.cpp | 5 +- lib/mapObjects/CGDwelling.cpp | 5 +- lib/mapObjects/CGHeroInstance.cpp | 3 +- lib/mapObjects/CGMarket.cpp | 2 +- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGObjectInstance.h | 3 + lib/mapObjects/CGPandoraBox.cpp | 4 +- lib/mapObjects/CGTownBuilding.cpp | 3 +- lib/mapObjects/CGTownInstance.cpp | 5 +- lib/mapObjects/CObjectHandler.cpp | 1 + lib/mapObjects/CQuest.cpp | 3 +- lib/mapObjects/CRewardableObject.cpp | 3 +- lib/mapObjects/IObjectInterface.cpp | 3 +- lib/mapObjects/IObjectInterface.h | 6 +- lib/mapObjects/MiscObjects.cpp | 5 +- lib/mapping/MapFormatH3M.cpp | 2 + lib/networkPacks/BattleChanges.h | 81 + lib/networkPacks/Component.h | 54 + lib/networkPacks/EInfoWindowMode.h | 22 + lib/networkPacks/EOpenWindowMode.h | 28 + lib/networkPacks/EntityChanges.h | 33 + lib/networkPacks/NetPackVisitor.h | 4 +- lib/networkPacks/NetPacksBase.h | 149 +- lib/networkPacks/NetPacksLib.cpp | 6 +- lib/networkPacks/NetPacksLobby.h | 45 +- lib/networkPacks/PacksForClient.h | 1431 ++--------------- lib/networkPacks/PacksForClientBattle.h | 575 +++++++ lib/networkPacks/PacksForServer.h | 667 ++++++++ lib/networkPacks/StackLocation.h | 40 + lib/registerTypes/RegisterTypes.h | 6 +- lib/registerTypes/TypesClientPacks1.cpp | 1 - lib/registerTypes/TypesClientPacks2.cpp | 1 - lib/registerTypes/TypesLobbyPacks.cpp | 1 - lib/registerTypes/TypesMapObjects1.cpp | 1 - lib/registerTypes/TypesMapObjects2.cpp | 1 - lib/registerTypes/TypesMapObjects3.cpp | 1 - lib/registerTypes/TypesServerPacks.cpp | 1 - lib/rewardable/Configuration.h | 2 +- lib/rewardable/Interface.cpp | 5 +- lib/rewardable/Limiter.cpp | 1 + lib/rewardable/Reward.h | 1 + lib/spells/AdventureSpellMechanics.cpp | 6 +- lib/spells/BattleSpellMechanics.cpp | 3 +- lib/spells/ISpellMechanics.cpp | 1 - lib/spells/ObstacleCasterProxy.h | 1 - lib/spells/effects/Catapult.cpp | 2 +- lib/spells/effects/Clone.cpp | 2 +- lib/spells/effects/Damage.cpp | 3 +- lib/spells/effects/DemonSummon.cpp | 2 +- lib/spells/effects/Dispel.cpp | 4 +- lib/spells/effects/Heal.cpp | 3 +- lib/spells/effects/Moat.cpp | 3 +- lib/spells/effects/Obstacle.cpp | 3 +- lib/spells/effects/RemoveObstacle.cpp | 2 +- lib/spells/effects/Sacrifice.cpp | 2 +- lib/spells/effects/Summon.cpp | 4 +- lib/spells/effects/Teleport.cpp | 4 +- lib/spells/effects/Timed.cpp | 4 +- lib/spells/effects/UnitEffect.cpp | 1 - scripting/lua/LuaScriptingContext.cpp | 1 - scripting/lua/api/netpacks/BattleLogMessage.h | 2 + scripting/lua/api/netpacks/EntitiesChanged.h | 2 + scripting/lua/api/netpacks/InfoWindow.h | 2 + scripting/lua/api/netpacks/PackForClient.h | 2 +- scripting/lua/api/netpacks/SetResources.cpp | 1 + server/CGameHandler.cpp | 1 + server/CVCMIServer.cpp | 1 - server/LobbyNetPackVisitors.h | 2 +- server/NetPacksLobbyServer.cpp | 1 - server/ServerNetPackVisitors.h | 2 +- server/ServerSpellCastEnvironment.cpp | 2 +- server/TurnTimerHandler.cpp | 3 +- server/battles/BattleActionProcessor.cpp | 3 +- server/battles/BattleFlowProcessor.cpp | 2 +- server/battles/BattleProcessor.cpp | 4 + server/battles/BattleResultProcessor.cpp | 2 + server/battles/BattleResultProcessor.h | 8 +- server/processors/HeroPoolProcessor.cpp | 3 +- server/processors/PlayerMessageProcessor.cpp | 3 +- server/processors/TurnOrderProcessor.cpp | 1 - server/queries/BattleQueries.cpp | 2 + server/queries/BattleQueries.h | 3 +- server/queries/CQuery.cpp | 2 +- server/queries/MapQueries.cpp | 16 + server/queries/MapQueries.h | 8 +- 134 files changed, 1844 insertions(+), 1619 deletions(-) create mode 100644 lib/networkPacks/BattleChanges.h create mode 100644 lib/networkPacks/Component.h create mode 100644 lib/networkPacks/EInfoWindowMode.h create mode 100644 lib/networkPacks/EOpenWindowMode.h create mode 100644 lib/networkPacks/EntityChanges.h create mode 100644 lib/networkPacks/PacksForClientBattle.h create mode 100644 lib/networkPacks/PacksForServer.h create mode 100644 lib/networkPacks/StackLocation.h diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index dd4ce6580..2de2fd498 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -12,9 +12,9 @@ #include -#include "../../lib/NetPacks.h" #include "../../lib/CStack.h" #include "../../lib/ScriptHandler.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #if SCRIPTING_ENABLED using scripting::Pool; diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 342592af9..55d8a59b7 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -12,14 +12,18 @@ #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacks.h" #include "../../lib/serializer/CTypeList.h" #include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinaryDeserializer.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/PacksForServer.h" +#include "../../lib/networkPacks/StackLocation.h" #include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleInfo.h" diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 8f79eaa43..b619cc4b1 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -21,12 +21,13 @@ #include "../../lib/CHeroHandler.h" #include "../../lib/GameSettings.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacksBase.h" -#include "../../lib/NetPacks.h" #include "../../lib/bonuses/CBonusSystemNode.h" #include "../../lib/bonuses/Limiters.h" #include "../../lib/bonuses/Updaters.h" #include "../../lib/bonuses/Propagators.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/serializer/CTypeList.h" #include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinaryDeserializer.h" diff --git a/CCallback.cpp b/CCallback.cpp index 0758967b9..753c443f0 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -15,15 +15,16 @@ #include "client/CPlayerInterface.h" #include "client/Client.h" #include "lib/mapping/CMap.h" +#include "lib/mapObjects/CGHeroInstance.h" #include "lib/CBuildingHandler.h" #include "lib/CGeneralTextHandler.h" #include "lib/CHeroHandler.h" -#include "lib/NetPacks.h" #include "lib/CArtHandler.h" #include "lib/GameConstants.h" #include "lib/CPlayerState.h" #include "lib/UnlockGuard.h" #include "lib/battle/BattleInfo.h" +#include "lib/networkPacks/PacksForServer.h" bool CCallback::teleportHero(const CGHeroInstance *who, const CGTownInstance *where) { diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 4be7b8c2a..760010a25 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -73,8 +73,6 @@ #include "../lib/CondSh.h" #include "../lib/GameConstants.h" #include "../lib/JsonNode.h" -#include "../lib/NetPacks.h" //todo: remove -#include "../lib/NetPacksBase.h" #include "../lib/RoadHandler.h" #include "../lib/StartInfo.h" #include "../lib/TerrainHandler.h" @@ -95,6 +93,10 @@ #include "../lib/mapping/CMapHeader.h" +#include "../lib/networkPacks/PacksForClient.h" +#include "../lib/networkPacks/PacksForClientBattle.h" +#include "../lib/networkPacks/PacksForServer.h" + #include "../lib/pathfinder/CGPathNode.h" #include "../lib/serializer/BinaryDeserializer.h" diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 59ee91e67..6e6018f0e 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -38,7 +38,6 @@ #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CThreadHelper.h" -#include "../lib/NetPackVisitor.h" #include "../lib/StartInfo.h" #include "../lib/TurnTimerInfo.h" #include "../lib/VCMIDirs.h" diff --git a/client/ClientCommandManager.cpp b/client/ClientCommandManager.cpp index 1c36d983c..412131142 100644 --- a/client/ClientCommandManager.cpp +++ b/client/ClientCommandManager.cpp @@ -18,7 +18,6 @@ #include "gui/CGuiHandler.h" #include "gui/WindowHandler.h" #include "render/IRenderHandler.h" -#include "../lib/NetPacks.h" #include "ClientNetPackVisitors.h" #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" @@ -28,6 +27,7 @@ #include "../lib/mapping/CMapService.h" #include "../lib/mapping/CMap.h" #include "windows/CCastleInterface.h" +#include "../lib/mapObjects/CGHeroInstance.h" #include "render/CAnimation.h" #include "../CCallback.h" #include "../lib/CGeneralTextHandler.h" diff --git a/client/ClientNetPackVisitors.h b/client/ClientNetPackVisitors.h index 46f33f998..08c0908e2 100644 --- a/client/ClientNetPackVisitors.h +++ b/client/ClientNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class CClient; diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 313d8b653..681b8ea4c 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -24,9 +24,9 @@ #include "../lib/pathfinder/CGPathNode.h" #include "../lib/mapObjects/CGHeroInstance.h" +#include "../lib/networkPacks/PacksForClient.h" #include "../lib/RoadHandler.h" #include "../lib/TerrainHandler.h" -#include "../lib/NetPacks.h" bool HeroMovementController::isHeroMovingThroughGarrison(const CGHeroInstance * hero, const CArmedInstance * garrison) const { diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 276250455..0c4af1aa0 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class CClient; VCMI_LIB_NAMESPACE_BEGIN diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index 7dd44cc88..dc3632757 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -27,7 +27,6 @@ #include "../lib/CConfigHandler.h" #include "../lib/CGeneralTextHandler.h" -#include "../lib/NetPacksLobby.h" #include "../lib/serializer/Connection.h" void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientConnected & pack) diff --git a/client/adventureMap/CInfoBar.h b/client/adventureMap/CInfoBar.h index 5800377b7..496016354 100644 --- a/client/adventureMap/CInfoBar.h +++ b/client/adventureMap/CInfoBar.h @@ -12,6 +12,7 @@ #include "../gui/CIntObject.h" #include "CConfigHandler.h" #include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/client/battle/BattleEffectsController.cpp b/client/battle/BattleEffectsController.cpp index 74b219f5f..f1db77c16 100644 --- a/client/battle/BattleEffectsController.cpp +++ b/client/battle/BattleEffectsController.cpp @@ -28,7 +28,7 @@ #include "../../CCallback.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/filesystem/ResourcePath.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/CStack.h" #include "../../lib/IGameEventsReceiver.h" #include "../../lib/CGeneralTextHandler.h" diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 9542c2245..8cdb5a7e7 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -40,7 +40,7 @@ #include "../../lib/CondSh.h" #include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/UnlockGuard.h" #include "../../lib/TerrainHandler.h" #include "../../lib/CThreadHelper.h" diff --git a/client/battle/BattleInterfaceClasses.cpp b/client/battle/BattleInterfaceClasses.cpp index 483a947b2..8a5ce5261 100644 --- a/client/battle/BattleInterfaceClasses.cpp +++ b/client/battle/BattleInterfaceClasses.cpp @@ -48,10 +48,10 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CTownHandler.h" #include "../../lib/CHeroHandler.h" -#include "../../lib/NetPacks.h" #include "../../lib/StartInfo.h" #include "../../lib/CondSh.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/TextOperations.h" void BattleConsole::showAll(Canvas & to) diff --git a/client/battle/BattleSiegeController.cpp b/client/battle/BattleSiegeController.cpp index c041f3743..660440f1e 100644 --- a/client/battle/BattleSiegeController.cpp +++ b/client/battle/BattleSiegeController.cpp @@ -26,9 +26,9 @@ #include "../render/IRenderHandler.h" #include "../../CCallback.h" -#include "../../lib/NetPacks.h" #include "../../lib/CStack.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" ImagePath BattleSiegeController::getWallPieceImageName(EWallVisual::EWallVisual what, EWallState state) const { diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index f2256f91f..d8c739370 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -25,7 +25,7 @@ #include "../../CCallback.h" #include "../CGameInfo.h" -#include "../../lib/NetPacksLobby.h" +#include "../../lib/networkPacks/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 0f8804c1b..4d7f8987e 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -41,7 +41,6 @@ #include "../render/IFont.h" #include "../render/IRenderHandler.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/CThreadHelper.h" diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 26c7ed634..de17abd5b 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -34,7 +34,7 @@ #include "../eventsSDL/InputHandler.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/NetPacksLobby.h" +#include "../../lib/networkPacks/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" #include "../../lib/CTownHandler.h" diff --git a/client/lobby/SelectionTab.cpp b/client/lobby/SelectionTab.cpp index 5e5e7fa68..8098715f2 100644 --- a/client/lobby/SelectionTab.cpp +++ b/client/lobby/SelectionTab.cpp @@ -34,7 +34,6 @@ #include "../../CCallback.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/GameSettings.h" diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 825389069..68e270866 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -51,7 +51,6 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/VCMIDirs.h" #include "../../lib/CStopWatch.h" -#include "../../lib/NetPacksLobby.h" #include "../../lib/CThreadHelper.h" #include "../../lib/CConfigHandler.h" #include "../../lib/GameConstants.h" diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 4b19e92c9..40f186332 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -26,6 +26,7 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" #include "../../lib/CConfigHandler.h" void CArtPlace::setInternals(const CArtifactInstance * artInst) diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 040da2eb8..672b4df60 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -16,6 +16,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) : visibleArtSet(ArtBearer::ArtBearer::HERO) @@ -109,4 +110,4 @@ void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst) getArtPlace(part.slot)->setArtifact(nullptr); } } -} \ No newline at end of file +} diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 0b38d5dfa..4d2e426f6 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -18,6 +18,7 @@ #include "../CPlayerInterface.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" #include "../../CCallback.h" diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 20f27e002..d66772eb7 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -22,6 +22,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" CArtifactsOfHeroBase::CArtifactsOfHeroBase() : backpackPos(0), diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index ff365e0ef..c2043bd94 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -15,6 +15,7 @@ #include "../CPlayerInterface.h" #include "../../CCallback.h" +#include "../../lib/networkPacks/NetPacksBase.h" CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector Backpack, std::shared_ptr leftScroll, std::shared_ptr rightScroll) diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index fd8e78b3a..2681f5a9c 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -13,6 +13,7 @@ #include "../CPlayerInterface.h" #include "../../CCallback.h" +#include "../../lib/networkPacks/NetPacksBase.h" CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { @@ -37,4 +38,4 @@ void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace) { LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); -} \ No newline at end of file +} diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index dd96e48ac..12844ceb3 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -31,11 +31,11 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CTownHandler.h" #include "../../lib/CHeroHandler.h" +#include "../../lib/networkPacks/Component.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/CCreatureHandler.h" #include "../../lib/CSkillHandler.h" #include "../../lib/CGeneralTextHandler.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/CArtHandler.h" #include "../../lib/CArtifactInstance.h" diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index e23f209e7..52ca984ec 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -30,6 +30,7 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" #include "../../lib/TextOperations.h" #include "../../lib/gameState/CGameState.h" diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 5c2c3964d..b3c2c2700 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -29,6 +29,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/NetPacksBase.h" #include "../../lib/CConfigHandler.h" void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index b43f23dd1..8c91103a5 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -33,6 +33,7 @@ #include "../../lib/GameSettings.h" #include "../../lib/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" +#include "../../lib/networkPacks/NetPacksBase.h" #include "../../lib/TextOperations.h" class CCreatureArtifactInstance; diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index ec98bc1d8..890504063 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -39,7 +39,7 @@ #include "../lib/CHeroHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/mapObjects/CGHeroInstance.h" -#include "../lib/NetPacksBase.h" +#include "../../lib/networkPacks/NetPacksBase.h" void CHeroSwitcher::clickPressed(const Point & cursorPosition) { diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index dfa72c231..c87db89b7 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -35,6 +35,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" +#include "../../lib/networkPacks/NetPacksBase.h" CTradeWindow::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos), diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 7846aa0f4..998bf074b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -123,6 +123,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/IdentifierStorage.cpp ${MAIN_LIB_DIR}/modding/ModUtility.cpp + ${MAIN_LIB_DIR}/networkPacks/NetPacksLib.cpp + ${MAIN_LIB_DIR}/pathfinder/CGPathNode.cpp ${MAIN_LIB_DIR}/pathfinder/CPathfinder.cpp ${MAIN_LIB_DIR}/pathfinder/NodeStorage.cpp @@ -255,7 +257,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/JsonRandom.cpp ${MAIN_LIB_DIR}/LoadProgress.cpp ${MAIN_LIB_DIR}/LogicalExpression.cpp - ${MAIN_LIB_DIR}/NetPacksLib.cpp ${MAIN_LIB_DIR}/MetaString.cpp ${MAIN_LIB_DIR}/ObstacleHandler.cpp ${MAIN_LIB_DIR}/StartInfo.cpp @@ -475,6 +476,19 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/ModScope.h ${MAIN_LIB_DIR}/modding/ModUtility.h + ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h + ${MAIN_LIB_DIR}/networkPacks/Component.h + ${MAIN_LIB_DIR}/networkPacks/EInfoWindowMode.h + ${MAIN_LIB_DIR}/networkPacks/EntityChanges.h + ${MAIN_LIB_DIR}/networkPacks/EOpenWindowMode.h + ${MAIN_LIB_DIR}/networkPacks/NetPacksBase.h + ${MAIN_LIB_DIR}/networkPacks/NetPacksLobby.h + ${MAIN_LIB_DIR}/networkPacks/NetPackVisitor.h + ${MAIN_LIB_DIR}/networkPacks/PacksForClient.h + ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h + ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h + ${MAIN_LIB_DIR}/networkPacks/StackLocation.h + ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h ${MAIN_LIB_DIR}/pathfinder/CPathfinder.h @@ -615,10 +629,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/LoadProgress.h ${MAIN_LIB_DIR}/LogicalExpression.h ${MAIN_LIB_DIR}/MetaString.h - ${MAIN_LIB_DIR}/NetPacksBase.h - ${MAIN_LIB_DIR}/NetPacks.h - ${MAIN_LIB_DIR}/NetPacksLobby.h - ${MAIN_LIB_DIR}/NetPackVisitor.h ${MAIN_LIB_DIR}/ObstacleHandler.h ${MAIN_LIB_DIR}/Point.h ${MAIN_LIB_DIR}/Rect.h diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 3b1f5305e..754831590 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -13,7 +13,7 @@ #include "ArtifactUtils.h" #include "CArtHandler.h" -#include "NetPacksBase.h" +#include "networkPacks/NetPacksBase.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 5659636e4..34849d337 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -23,7 +23,6 @@ #include "CHeroHandler.h" #include "IBonusTypeHandler.h" #include "serializer/JsonSerializeFormat.h" -#include "NetPacksBase.h" #include #include diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 674b20023..5ab149426 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -14,10 +14,11 @@ #include "gameState/InfoAboutArmy.h" #include "gameState/SThievesGuildInfo.h" #include "gameState/TavernHeroesPool.h" +#include "gameState/QuestInfo.h" +#include "mapObjects/CGHeroInstance.h" #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo -#include "NetPacks.h" // for InfoWindow #include "GameSettings.h" #include "TerrainHandler.h" #include "spells/CSpellHandler.h" diff --git a/lib/CStack.cpp b/lib/CStack.cpp index f76a82a75..128b1e2bb 100644 --- a/lib/CStack.cpp +++ b/lib/CStack.cpp @@ -18,7 +18,7 @@ #include "CGeneralTextHandler.h" #include "battle/BattleInfo.h" #include "spells/CSpellHandler.h" -#include "NetPacks.h" +#include "networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index fd576a211..a2b7f8d3d 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -13,7 +13,6 @@ #include "CHeroHandler.h" // for CHeroHandler #include "spells/CSpellHandler.h"// for CSpell #include "CSkillHandler.h"// for CSkill -#include "NetPacks.h" #include "CBonusTypeHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" @@ -36,6 +35,7 @@ #include "gameState/CGameState.h" #include "gameState/CGameStateCampaign.h" #include "gameState/TavernHeroesPool.h" +#include "gameState/QuestInfo.h" #include "mapping/CMap.h" #include "modding/CModHandler.h" #include "modding/CModInfo.h" diff --git a/lib/IGameEventsReceiver.h b/lib/IGameEventsReceiver.h index 959e28f8d..9ee40945a 100644 --- a/lib/IGameEventsReceiver.h +++ b/lib/IGameEventsReceiver.h @@ -9,8 +9,7 @@ */ #pragma once - -#include "NetPacksBase.h" +#include "networkPacks/EInfoWindowMode.h" #include "battle/BattleHex.h" #include "GameConstants.h" #include "int3.h" diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index 61b1c4225..fcccfafe2 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -9,11 +9,11 @@ */ #include "StdInc.h" #include "BattleInfo.h" +#include "CObstacleInstance.h" #include "bonuses/Limiters.h" #include "bonuses/Updaters.h" #include "../CStack.h" #include "../CHeroHandler.h" -#include "../NetPacks.h" #include "../filesystem/Filesystem.h" #include "../mapObjects/CGTownInstance.h" #include "../CGeneralTextHandler.h" diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 0d7823b56..28fbf09ee 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -17,12 +17,12 @@ #include "CObstacleInstance.h" #include "DamageCalculator.h" #include "PossiblePlayerBattleAction.h" -#include "../NetPacks.h" #include "../spells/ObstacleCasterProxy.h" #include "../spells/ISpellMechanics.h" #include "../spells/Problem.h" #include "../spells/CSpellHandler.h" #include "../mapObjects/CGTownInstance.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../BattleFieldHandler.h" #include "../Rect.h" diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index 5e896a9eb..c113dddef 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -11,9 +11,10 @@ #include "CBattleInfoEssentials.h" #include "../CStack.h" #include "BattleInfo.h" -#include "../NetPacks.h" +#include "CObstacleInstance.h" #include "../mapObjects/CGTownInstance.h" #include "../gameState/InfoAboutArmy.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/battle/CObstacleInstance.cpp b/lib/battle/CObstacleInstance.cpp index 426859fb3..ccf01f07c 100644 --- a/lib/battle/CObstacleInstance.cpp +++ b/lib/battle/CObstacleInstance.cpp @@ -13,7 +13,6 @@ #include "../CTownHandler.h" #include "../ObstacleHandler.h" #include "../VCMI_Lib.h" -#include "../NetPacksBase.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" diff --git a/lib/battle/CObstacleInstance.h b/lib/battle/CObstacleInstance.h index 99178c20c..cf8bc157f 100644 --- a/lib/battle/CObstacleInstance.h +++ b/lib/battle/CObstacleInstance.h @@ -9,8 +9,9 @@ */ #pragma once #include "BattleHex.h" -#include "NetPacksBase.h" #include "../filesystem/ResourcePath.h" +#include "../networkPacks/BattleChanges.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 099ae50be..ac8b30413 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -13,8 +13,8 @@ #include -#include "../NetPacks.h" #include "../CCreatureHandler.h" +#include "../MetaString.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 77905eafe..7e0c9be2d 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -14,7 +14,6 @@ #include "../VCMI_Lib.h" #include "../CGeneralTextHandler.h" #include "../MetaString.h" -#include "../NetPacksBase.h" #include "../serializer/JsonDeserializer.h" #include "../serializer/JsonSerializer.h" diff --git a/lib/events/ApplyDamage.cpp b/lib/events/ApplyDamage.cpp index 305149edf..2056f0c2d 100644 --- a/lib/events/ApplyDamage.cpp +++ b/lib/events/ApplyDamage.cpp @@ -12,8 +12,7 @@ #include #include "ApplyDamage.h" - -#include "../../lib/NetPacks.h" +#include "../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 8b26f3655..53f219939 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -14,13 +14,17 @@ #include #include -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../GameSettings.h" #include "../CPlayerState.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/Component.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../MetaString.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 62883711b..b0c9c8384 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -10,12 +10,15 @@ #include "StdInc.h" #include "CGCreature.h" +#include "CGHeroInstance.h" -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CConfigHandler.h" #include "../GameSettings.h" #include "../IGameCallback.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/StackLocation.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 550be13fe..65da8db66 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -13,11 +13,14 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/StackLocation.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../CTownHandler.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" #include "../CPlayerState.h" -#include "../NetPacks.h" #include "../GameSettings.h" #include "../CConfigHandler.h" diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index dc22f4ae5..049623801 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -14,7 +14,6 @@ #include #include -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../ArtifactUtils.h" #include "../CHeroHandler.h" @@ -35,6 +34,8 @@ #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../modding/ModScope.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../constants/StringConstants.h" #include "../battle/Unit.h" diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index 595c130ba..ef20b1024 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -11,7 +11,6 @@ #include "StdInc.h" #include "CGMarket.h" -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" #include "../CCreatureHandler.h" @@ -20,6 +19,7 @@ #include "../CSkillHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index e4cda9ab0..7d5cacbe2 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -17,12 +17,12 @@ #include "../gameState/CGameState.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" -#include "../NetPacks.h" #include "../constants/StringConstants.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../mapping/CMap.h" +#include "../networkPacks/PacksForClient.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 98f6dceba..1e57c2344 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -10,11 +10,14 @@ #pragma once #include "IObjectInterface.h" +#include "../constants/EntityIdentifiers.h" +#include "../filesystem/ResourcePath.h" #include "../int3.h" #include "../bonuses/BonusEnum.h" VCMI_LIB_NAMESPACE_BEGIN +struct Component; class JsonSerializeFormat; class ObjectTemplate; class CMap; diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 81035cbb1..007f0b929 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -14,13 +14,15 @@ #include #include -#include "../NetPacks.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" #include "../StartInfo.h" #include "../IGameCallback.h" #include "../constants/StringConstants.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../mapObjects/CGHeroInstance.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 1590985e3..4921b085a 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -12,9 +12,10 @@ #include "CGTownBuilding.h" #include "CGTownInstance.h" #include "../CGeneralTextHandler.h" -#include "../NetPacks.h" #include "../IGameCallback.h" #include "../gameState/CGameState.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9d8abbcd3..9e7f842b2 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -14,7 +14,6 @@ #include "../spells/CSpellHandler.h" #include "../bonuses/Bonus.h" #include "../battle/IBattleInfoCallback.h" -#include "../NetPacks.h" #include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" #include "../IGameCallback.h" @@ -24,7 +23,11 @@ #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" #include "../modding/ModScope.h" +#include "../networkPacks/StackLocation.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index 492974869..34162c4c3 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -13,6 +13,7 @@ #include "CGObjectInstance.h" #include "../filesystem/ResourcePath.h" +#include "../JsonNode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index d79549f04..5431a12a1 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -14,7 +14,6 @@ #include #include "../ArtifactUtils.h" -#include "../NetPacks.h" #include "../CSoundBase.h" #include "../CGeneralTextHandler.h" #include "../CHeroHandler.h" @@ -26,8 +25,10 @@ #include "../constants/StringConstants.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" +#include "../mapObjects/CGHeroInstance.h" #include "../modding/ModScope.h" #include "../modding/ModUtility.h" +#include "../networkPacks/PacksForClient.h" #include "../spells/CSpellHandler.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 341318dbf..4ec6783e0 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -14,9 +14,10 @@ #include "../CGeneralTextHandler.h" #include "../CPlayerState.h" #include "../IGameCallback.h" -#include "../NetPacks.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" #include "../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index 867c02456..268c99c4c 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -14,9 +14,10 @@ #include "CGTownInstance.h" #include "MiscObjects.h" -#include "../NetPacks.h" #include "../IGameCallback.h" #include "../TerrainHandler.h" +#include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index b0992a868..1ed826903 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -9,18 +9,22 @@ */ #pragma once -#include "../NetPacksBase.h" +#include "../networkPacks/EInfoWindowMode.h" VCMI_LIB_NAMESPACE_BEGIN struct BattleResult; struct UpgradeInfo; +class BoatId; class CGObjectInstance; class CRandomGenerator; +class CStackInstance; +class CGHeroInstance; class IGameCallback; class ResourceSet; class int3; class MetaString; +class PlayerColor; class DLL_LINKAGE IObjectInterface { diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c0c49975e..0bfb191a7 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -12,7 +12,6 @@ #include "MiscObjects.h" #include "../constants/StringConstants.h" -#include "../NetPacks.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" @@ -24,7 +23,11 @@ #include "../serializer/JsonSerializeFormat.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjects/CGHeroInstance.h" #include "../modding/ModScope.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/StackLocation.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 709e55fad..8152ed991 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -36,6 +36,8 @@ #include "../mapObjects/MapObjects.h" #include "../mapObjects/ObjectTemplate.h" #include "../modding/ModScope.h" +#include "../networkPacks/Component.h" +#include "../networkPacks/NetPacksBase.h" #include "../spells/CSpellHandler.h" #include diff --git a/lib/networkPacks/BattleChanges.h b/lib/networkPacks/BattleChanges.h new file mode 100644 index 000000000..510505e23 --- /dev/null +++ b/lib/networkPacks/BattleChanges.h @@ -0,0 +1,81 @@ +/* + * BattleChanges.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 "JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BattleChanges +{ +public: + enum class EOperation : si8 + { + ADD, + RESET_STATE, + UPDATE, + REMOVE, + }; + + JsonNode data; + EOperation operation = EOperation::RESET_STATE; + + BattleChanges() = default; + explicit BattleChanges(EOperation operation_) + : operation(operation_) + { + } +}; + +class UnitChanges : public BattleChanges +{ +public: + uint32_t id = 0; + int64_t healthDelta = 0; + + UnitChanges() = default; + UnitChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_) + , id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & healthDelta; + h & data; + h & operation; + } +}; + +class ObstacleChanges : public BattleChanges +{ +public: + uint32_t id = 0; + + ObstacleChanges() = default; + + ObstacleChanges(uint32_t id_, EOperation operation_) + : BattleChanges(operation_), + id(id_) + { + } + + template void serialize(Handler & h, const int version) + { + h & id; + h & data; + h & operation; + } +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/networkPacks/Component.h b/lib/networkPacks/Component.h new file mode 100644 index 000000000..fa764e085 --- /dev/null +++ b/lib/networkPacks/Component.h @@ -0,0 +1,54 @@ +/* + * Component.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 + +VCMI_LIB_NAMESPACE_BEGIN + +class CStackBasicDescriptor; + +struct Component +{ + enum class EComponentType : uint8_t + { + PRIM_SKILL, + SEC_SKILL, + RESOURCE, + CREATURE, + ARTIFACT, + EXPERIENCE, + SPELL, + MORALE, + LUCK, + BUILDING, + HERO_PORTRAIT, + FLAG, + INVALID //should be last + }; + EComponentType id = EComponentType::INVALID; + ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels + si32 val = 0; // + give; - take + si16 when = 0; // 0 - now; +x - within x days; -x - per x days + + template void serialize(Handler &h, const int version) + { + h & id; + h & subtype; + h & val; + h & when; + } + Component() = default; + DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); + Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) + :id(Type),subtype(Subtype),val(Val),when(When) + { + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/EInfoWindowMode.h b/lib/networkPacks/EInfoWindowMode.h new file mode 100644 index 000000000..3559ce9ae --- /dev/null +++ b/lib/networkPacks/EInfoWindowMode.h @@ -0,0 +1,22 @@ +/* + * EInfoWindowMode.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 + +VCMI_LIB_NAMESPACE_BEGIN + +enum class EInfoWindowMode : uint8_t +{ + AUTO, + MODAL, + INFO +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/networkPacks/EOpenWindowMode.h b/lib/networkPacks/EOpenWindowMode.h new file mode 100644 index 000000000..655137ac7 --- /dev/null +++ b/lib/networkPacks/EOpenWindowMode.h @@ -0,0 +1,28 @@ +/* + * EOpenWindowMode.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 + +VCMI_LIB_NAMESPACE_BEGIN + +enum class EOpenWindowMode : uint8_t +{ + EXCHANGE_WINDOW, + RECRUITMENT_FIRST, + RECRUITMENT_ALL, + SHIPYARD_WINDOW, + THIEVES_GUILD, + UNIVERSITY_WINDOW, + HILL_FORT_WINDOW, + MARKET_WINDOW, + PUZZLE_MAP, + TAVERN_WINDOW +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/EntityChanges.h b/lib/networkPacks/EntityChanges.h new file mode 100644 index 000000000..296148297 --- /dev/null +++ b/lib/networkPacks/EntityChanges.h @@ -0,0 +1,33 @@ +/* + * EInfoWindowMode.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 + +#include "../JsonNode.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class EntityChanges +{ +public: + Metatype metatype = Metatype::UNKNOWN; + int32_t entityIndex = 0; + JsonNode data; + template void serialize(Handler & h, const int version) + { + h & metatype; + h & entityIndex; + h & data; + } +}; + +VCMI_LIB_NAMESPACE_END + diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 40e81ab03..73b090b4a 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -9,7 +9,9 @@ */ #pragma once -#include "NetPacks.h" +#include "PacksForClient.h" +#include "PacksForClientBattle.h" +#include "PacksForServer.h" #include "NetPacksLobby.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/networkPacks/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h index e3d878706..295594c46 100644 --- a/lib/networkPacks/NetPacksBase.h +++ b/lib/networkPacks/NetPacksBase.h @@ -9,11 +9,8 @@ */ #pragma once -#include - -#include "ConstTransitivePtr.h" -#include "GameConstants.h" -#include "JsonNode.h" +#include "../ConstTransitivePtr.h" +#include "../GameConstants.h" class CClient; class CGameHandler; @@ -35,27 +32,6 @@ struct ArtSlotInfo; class ICPackVisitor; -enum class EInfoWindowMode : uint8_t -{ - AUTO, - MODAL, - INFO -}; - -enum class EOpenWindowMode : uint8_t -{ - EXCHANGE_WINDOW, - RECRUITMENT_FIRST, - RECRUITMENT_ALL, - SHIPYARD_WINDOW, - THIEVES_GUILD, - UNIVERSITY_WINDOW, - HILL_FORT_WINDOW, - MARKET_WINDOW, - PUZZLE_MAP, - TAVERN_WINDOW -}; - struct DLL_LINKAGE CPack { std::shared_ptr c; // Pointer to connection that pack received from @@ -92,6 +68,11 @@ protected: virtual void visitBasic(ICPackVisitor & cpackVisitor) override; }; +struct DLL_LINKAGE Query : public CPackForClient +{ + QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) +}; + struct DLL_LINKAGE CPackForServer : public CPack { mutable PlayerColor player = PlayerColor::NEUTRAL; @@ -115,44 +96,6 @@ protected: virtual void visitBasic(ICPackVisitor & cpackVisitor) override; }; -struct Component -{ - enum class EComponentType : uint8_t - { - PRIM_SKILL, - SEC_SKILL, - RESOURCE, - CREATURE, - ARTIFACT, - EXPERIENCE, - SPELL, - MORALE, - LUCK, - BUILDING, - HERO_PORTRAIT, - FLAG, - INVALID //should be last - }; - EComponentType id = EComponentType::INVALID; - ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels - si32 val = 0; // + give; - take - si16 when = 0; // 0 - now; +x - within x days; -x - per x days - - template void serialize(Handler &h, const int version) - { - h & id; - h & subtype; - h & val; - h & when; - } - Component() = default; - DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); - Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) - :id(Type),subtype(Subtype),val(Val),when(When) - { - } -}; - using TArtHolder = std::variant, ConstTransitivePtr>; struct ArtifactLocation @@ -205,82 +148,4 @@ struct ArtifactLocation } }; -class EntityChanges -{ -public: - Metatype metatype = Metatype::UNKNOWN; - int32_t entityIndex = 0; - JsonNode data; - template void serialize(Handler & h, const int version) - { - h & metatype; - h & entityIndex; - h & data; - } -}; - -class BattleChanges -{ -public: - enum class EOperation : si8 - { - ADD, - RESET_STATE, - UPDATE, - REMOVE, - }; - - JsonNode data; - EOperation operation = EOperation::RESET_STATE; - - BattleChanges() = default; - BattleChanges(EOperation operation_) - : operation(operation_) - { - } -}; - -class UnitChanges : public BattleChanges -{ -public: - uint32_t id = 0; - int64_t healthDelta = 0; - - UnitChanges() = default; - UnitChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_) - , id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & healthDelta; - h & data; - h & operation; - } -}; - -class ObstacleChanges : public BattleChanges -{ -public: - uint32_t id = 0; - - ObstacleChanges() = default; - - ObstacleChanges(uint32_t id_, EOperation operation_) - : BattleChanges(operation_), - id(id_) - { - } - - template void serialize(Handler & h, const int version) - { - h & id; - h & data; - h & operation; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 2c67efc48..508379a18 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -9,7 +9,11 @@ */ #include "StdInc.h" #include "ArtifactUtils.h" -#include "NetPacks.h" +#include "PacksForClient.h" +#include "PacksForClientBattle.h" +#include "PacksForServer.h" +#include "StackLocation.h" +#include "NetPacksLobby.h" #include "NetPackVisitor.h" #include "CGeneralTextHandler.h" #include "CArtHandler.h" diff --git a/lib/networkPacks/NetPacksLobby.h b/lib/networkPacks/NetPacksLobby.h index 2f772cb3b..4e819d98f 100644 --- a/lib/networkPacks/NetPacksLobby.h +++ b/lib/networkPacks/NetPacksLobby.h @@ -9,9 +9,8 @@ */ #pragma once -#include "NetPacksBase.h" - #include "StartInfo.h" +#include "NetPacksBase.h" class CServerHandler; class CVCMIServer; @@ -43,7 +42,7 @@ struct DLL_LINKAGE LobbyClientConnected : public CLobbyPackToPropagate int clientId = -1; int hostClientId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -62,7 +61,7 @@ struct DLL_LINKAGE LobbyClientDisconnected : public CLobbyPackToPropagate bool shutdownServer = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -75,7 +74,7 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate { std::string playerName, message; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -91,7 +90,7 @@ struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate } action = NONE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -103,7 +102,7 @@ struct DLL_LINKAGE LobbyLoadProgress : public CLobbyPackToPropagate { unsigned char progress; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -115,7 +114,7 @@ struct DLL_LINKAGE LobbyEndGame : public CLobbyPackToPropagate { bool closeConnection = false, restart = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -131,7 +130,7 @@ struct DLL_LINKAGE LobbyStartGame : public CLobbyPackToPropagate CGameState * initializedGameState = nullptr; int clientId = -1; //-1 means to all clients - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -148,7 +147,7 @@ struct DLL_LINKAGE LobbyChangeHost : public CLobbyPackToPropagate { int newHostConnectionId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -161,7 +160,7 @@ struct DLL_LINKAGE LobbyUpdateState : public CLobbyPackToPropagate LobbyState state; bool hostChanged = false; // Used on client-side only - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -176,7 +175,7 @@ struct DLL_LINKAGE LobbySetMap : public CLobbyPackToServer LobbySetMap() : mapInfo(nullptr), mapGenOpts(nullptr) {} - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -189,7 +188,7 @@ struct DLL_LINKAGE LobbySetCampaign : public CLobbyPackToServer { std::shared_ptr ourCampaign; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -201,7 +200,7 @@ struct DLL_LINKAGE LobbySetCampaignMap : public CLobbyPackToServer { CampaignScenarioID mapId = CampaignScenarioID::NONE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -213,7 +212,7 @@ struct DLL_LINKAGE LobbySetCampaignBonus : public CLobbyPackToServer { int bonusId = -1; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -228,7 +227,7 @@ struct DLL_LINKAGE LobbyChangePlayerOption : public CLobbyPackToServer int32_t value = 0; PlayerColor color = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -242,7 +241,7 @@ struct DLL_LINKAGE LobbySetPlayer : public CLobbyPackToServer { PlayerColor clickedColor = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -255,7 +254,7 @@ struct DLL_LINKAGE LobbySetPlayerName : public CLobbyPackToServer PlayerColor color = PlayerColor::CANNOT_DETERMINE; std::string name = ""; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -268,7 +267,7 @@ struct DLL_LINKAGE LobbySetSimturns : public CLobbyPackToServer { SimturnsInfo simturnsInfo; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -280,7 +279,7 @@ struct DLL_LINKAGE LobbySetTurnTime : public CLobbyPackToServer { TurnTimerInfo turnTimerInfo; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -292,7 +291,7 @@ struct DLL_LINKAGE LobbySetDifficulty : public CLobbyPackToServer { ui8 difficulty = 0; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler &h, const int version) { @@ -305,7 +304,7 @@ struct DLL_LINKAGE LobbyForceSetPlayer : public CLobbyPackToServer ui8 targetConnectedPlayer = -1; PlayerColor targetPlayerColor = PlayerColor::CANNOT_DETERMINE; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -318,7 +317,7 @@ struct DLL_LINKAGE LobbyShowMessage : public CLobbyPackToPropagate { std::string message; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 251fc89e9..b37551cd7 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -10,21 +10,21 @@ #pragma once #include "NetPacksBase.h" +#include "Component.h" +#include "EOpenWindowMode.h" +#include "EInfoWindowMode.h" +#include "EntityChanges.h" -#include "ConstTransitivePtr.h" -#include "MetaString.h" -#include "ResourceSet.h" -#include "TurnTimerInfo.h" -#include "int3.h" - -#include "battle/BattleAction.h" -#include "battle/CObstacleInstance.h" -#include "gameState/EVictoryLossCheckResult.h" -#include "gameState/TavernSlot.h" -#include "gameState/QuestInfo.h" -#include "mapObjects/CGHeroInstance.h" -#include "mapping/CMapDefines.h" -#include "spells/ViewSpellInt.h" +#include "../CCreatureSet.h" +#include "../MetaString.h" +#include "../ResourceSet.h" +#include "../TurnTimerInfo.h" +#include "../gameState/EVictoryLossCheckResult.h" +#include "../gameState/QuestInfo.h" +#include "../gameState/TavernSlot.h" +#include "../int3.h" +#include "../mapping/CMapDefines.h" +#include "../spells/ViewSpellInt.h" class CClient; class CGameHandler; @@ -45,40 +45,15 @@ class BattleInfo; // For now it's will be there till teleports code refactored and moved into own file using TTeleportExitsList = std::vector>; -struct DLL_LINKAGE Query : public CPackForClient -{ - QueryID queryID; // equals to -1 if it is not an actual query (and should not be answered) -}; - -struct StackLocation -{ - ConstTransitivePtr army; - SlotID slot; - - StackLocation() = default; - StackLocation(const CArmedInstance * Army, const SlotID & Slot) - : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - - DLL_LINKAGE const CStackInstance * getStack(); - template void serialize(Handler & h, const int version) - { - h & army; - h & slot; - } -}; - /***********************************************************************************************************/ struct DLL_LINKAGE PackageApplied : public CPackForClient { PackageApplied() = default; - PackageApplied(ui8 Result) + explicit PackageApplied(ui8 Result) : result(Result) { } - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ui8 result = 0; //0 - something went wrong, request hasn't been realized; 1 - OK ui32 packType = 0; //type id of applied package @@ -96,13 +71,13 @@ struct DLL_LINKAGE PackageApplied : public CPackForClient struct DLL_LINKAGE SystemMessage : public CPackForClient { - SystemMessage(std::string Text) + explicit SystemMessage(std::string Text) : text(std::move(Text)) { } SystemMessage() = default; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; std::string text; @@ -121,7 +96,7 @@ struct DLL_LINKAGE PlayerBlocked : public CPackForClient EMode startOrEnd = BLOCKADE_STARTED; PlayerColor player; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -139,7 +114,7 @@ struct DLL_LINKAGE PlayerCheated : public CPackForClient bool losingCheatCode = false; bool winningCheatCode = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -169,7 +144,7 @@ struct DLL_LINKAGE PlayerStartsTurn : public Query PlayerColor player; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -185,7 +160,7 @@ struct DLL_LINKAGE DaysWithoutTown : public CPackForClient PlayerColor player; std::optional daysWithoutCastle; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -200,7 +175,7 @@ struct DLL_LINKAGE EntitiesChanged : public CPackForClient void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -212,11 +187,11 @@ struct DLL_LINKAGE SetResources : public CPackForClient { void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; bool abs = true; //false - changes by value; 1 - sets to value PlayerColor player; - TResources res; //res[resid] => res amount + ResourceSet res; //res[resid] => res amount template void serialize(Handler & h, const int version) { @@ -230,7 +205,7 @@ struct DLL_LINKAGE SetPrimSkill : public CPackForClient { void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ui8 abs = 0; //0 - changes by value; 1 - sets to value ObjectInstanceID id; @@ -250,7 +225,7 @@ struct DLL_LINKAGE SetSecSkill : public CPackForClient { void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ui8 abs = 0; //0 - changes by value; 1 - sets to value ObjectInstanceID id; @@ -270,7 +245,7 @@ struct DLL_LINKAGE HeroVisitCastle : public CPackForClient { void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ui8 flags = 0; //1 - start ObjectInstanceID tid, hid; @@ -292,7 +267,7 @@ struct DLL_LINKAGE ChangeSpells : public CPackForClient { void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ui8 learn = 1; //1 - gives spell, 0 - takes ObjectInstanceID hid; @@ -310,7 +285,7 @@ struct DLL_LINKAGE SetMana : public CPackForClient { void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ObjectInstanceID hid; si32 val = 0; @@ -332,7 +307,7 @@ struct DLL_LINKAGE SetMovePoints : public CPackForClient si32 val = 0; bool absolute = true; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -351,7 +326,7 @@ struct DLL_LINKAGE FoWChange : public CPackForClient ETileVisibility mode; bool waitForDialogs = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -376,7 +351,7 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient HeroTypeID hid; //HeroTypeID::NONE if no hero CSimpleArmy army; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -392,7 +367,7 @@ struct DLL_LINKAGE GiveBonus : public CPackForClient { enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; - GiveBonus(ETarget Who = ETarget::HERO) + explicit GiveBonus(ETarget Who = ETarget::HERO) :who(Who) { } @@ -404,7 +379,7 @@ struct DLL_LINKAGE GiveBonus : public CPackForClient Bonus bonus; MetaString bdescr; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -427,7 +402,7 @@ struct DLL_LINKAGE ChangeObjPos : public CPackForClient /// Player that initiated this action, if any PlayerColor initiator; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -443,7 +418,7 @@ struct DLL_LINKAGE PlayerEndsTurn : public CPackForClient PlayerColor player; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -458,7 +433,7 @@ struct DLL_LINKAGE PlayerEndsGame : public CPackForClient PlayerColor player; EVictoryLossCheckResult victoryLossCheckResult; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -474,7 +449,7 @@ struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient std::vector players; ui8 playerConnectionId; //PLAYER_AI for AI player - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -485,7 +460,7 @@ struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient struct DLL_LINKAGE RemoveBonus : public CPackForClient { - RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) + explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) :who(Who) { } @@ -502,7 +477,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient //used locally: copy of removed bonus Bonus bonus; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -526,7 +501,7 @@ struct DLL_LINKAGE SetCommanderProperty : public CPackForClient si32 additionalInfo = 0; //for secondary skills choice Bonus accumulatedBonus; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -545,7 +520,7 @@ struct DLL_LINKAGE AddQuest : public CPackForClient PlayerColor player; QuestInfo quest; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -559,7 +534,7 @@ struct DLL_LINKAGE UpdateArtHandlerLists : public CPackForClient std::map allocatedArtifacts; void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -572,7 +547,7 @@ struct DLL_LINKAGE UpdateMapEvents : public CPackForClient std::list events; void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -586,7 +561,7 @@ struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient std::list events; void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -601,7 +576,7 @@ struct DLL_LINKAGE ChangeFormation : public CPackForClient ui8 formation = 0; void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -620,7 +595,7 @@ struct DLL_LINKAGE RemoveObject : public CPackForClient } void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; /// ID of removed object ObjectInstanceID objectID; @@ -656,7 +631,7 @@ struct DLL_LINKAGE TryMoveHero : public CPackForClient std::unordered_set fowRevealed; //revealed tiles std::optional attackedFrom; // Set when stepping into endangered tile. - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; bool stopMovement() const { @@ -683,7 +658,7 @@ struct DLL_LINKAGE NewStructures : public CPackForClient std::set bid; si16 builded = 0; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -701,7 +676,7 @@ struct DLL_LINKAGE RazeStructures : public CPackForClient std::set bid; si16 destroyed = 0; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -718,7 +693,7 @@ struct DLL_LINKAGE SetAvailableCreatures : public CPackForClient ObjectInstanceID tid; std::vector > > creatures; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -733,7 +708,7 @@ struct DLL_LINKAGE SetHeroesInTown : public CPackForClient ObjectInstanceID tid, visiting, garrison; //id of town, visiting hero, hero in garrison - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -753,7 +728,7 @@ struct DLL_LINKAGE HeroRecruited : public CPackForClient int3 tile; PlayerColor player; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -773,7 +748,7 @@ struct DLL_LINKAGE GiveHero : public CPackForClient ObjectInstanceID boatId; PlayerColor player; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -789,7 +764,7 @@ struct DLL_LINKAGE OpenWindow : public Query ObjectInstanceID object; ObjectInstanceID visitor; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -815,7 +790,7 @@ struct DLL_LINKAGE NewObject : public CPackForClient ObjectInstanceID createdObjectID; //used locally, filled during applyGs - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -833,7 +808,7 @@ struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient si32 id = 0; //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) std::vector arts; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -855,7 +830,7 @@ struct DLL_LINKAGE ChangeStackCount : CGarrisonOperationPack void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -874,7 +849,7 @@ struct DLL_LINKAGE SetStackType : CGarrisonOperationPack void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -890,7 +865,7 @@ struct DLL_LINKAGE EraseStack : CGarrisonOperationPack SlotID slot; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -907,7 +882,7 @@ struct DLL_LINKAGE SwapStacks : CGarrisonOperationPack SlotID dstSlot; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -926,7 +901,7 @@ struct DLL_LINKAGE InsertNewStack : CGarrisonOperationPack TQuantity count = 0; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -948,7 +923,7 @@ struct DLL_LINKAGE RebalanceStacks : CGarrisonOperationPack TQuantity count; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -965,7 +940,7 @@ struct DLL_LINKAGE BulkRebalanceStacks : CGarrisonOperationPack std::vector moves; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) @@ -980,7 +955,7 @@ struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack std::vector changes; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) @@ -990,20 +965,6 @@ struct DLL_LINKAGE BulkSmartRebalanceStacks : CGarrisonOperationPack } }; -struct GetEngagedHeroIds -{ - std::optional operator()(const ConstTransitivePtr & h) const - { - return h->id; - } - std::optional operator()(const ConstTransitivePtr & s) const - { - if(s->armyObj && s->armyObj->ID == Obj::HERO) - return s->armyObj->id; - return std::optional(); - } -}; - struct DLL_LINKAGE CArtifactOperationPack : CPackForClient { }; @@ -1011,7 +972,7 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { PutArtifact() = default; - PutArtifact(ArtifactLocation * dst, bool askAssemble = true) + explicit PutArtifact(ArtifactLocation * dst, bool askAssemble = true) : al(*dst), askAssemble(askAssemble) { } @@ -1021,7 +982,7 @@ struct DLL_LINKAGE PutArtifact : CArtifactOperationPack ConstTransitivePtr art; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1036,7 +997,7 @@ struct DLL_LINKAGE NewArtifact : public CArtifactOperationPack ConstTransitivePtr art; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1049,7 +1010,7 @@ struct DLL_LINKAGE EraseArtifact : CArtifactOperationPack ArtifactLocation al; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1068,7 +1029,7 @@ struct DLL_LINKAGE MoveArtifact : CArtifactOperationPack bool askAssemble = true; void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1106,8 +1067,8 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack { } BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) - : srcArtHolder(std::move(std::move(srcArtHolder))) - , dstArtHolder(std::move(std::move(dstArtHolder))) + : srcArtHolder(std::move(srcArtHolder)) + , dstArtHolder(std::move(dstArtHolder)) , swap(swap) { } @@ -1120,7 +1081,7 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack CArtifactSet * getSrcHolderArtSet(); CArtifactSet * getDstHolderArtSet(); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1139,7 +1100,7 @@ struct DLL_LINKAGE AssembledArtifact : CArtifactOperationPack void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1154,7 +1115,7 @@ struct DLL_LINKAGE DisassembledArtifact : CArtifactOperationPack void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1172,7 +1133,7 @@ struct DLL_LINKAGE HeroVisit : public CPackForClient void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1189,7 +1150,7 @@ struct DLL_LINKAGE NewTurn : public CPackForClient void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; struct Hero { @@ -1205,7 +1166,7 @@ struct DLL_LINKAGE NewTurn : public CPackForClient }; std::set heroes; //updates movement and mana points - std::map res; //player ID => resource value[res_id] + std::map res; //player ID => resource value[res_id] std::map cres;//creatures to be placed in towns ui32 day = 0; ui8 specialWeek = 0; //weekType @@ -1232,7 +1193,7 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i PlayerColor player; ui16 soundID = 0; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1278,7 +1239,7 @@ struct DLL_LINKAGE SetObjectProperty : public CPackForClient { } - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1303,7 +1264,7 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; ChangeObjectVisitors() = default; @@ -1331,7 +1292,7 @@ struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient void applyGs(CGameState * gs); - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1349,7 +1310,7 @@ struct DLL_LINKAGE HeroLevelUp : public Query void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1370,7 +1331,7 @@ struct DLL_LINKAGE CommanderLevelUp : public Query void applyGs(CGameState * gs) const; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1409,7 +1370,7 @@ struct DLL_LINKAGE BlockingDialog : public Query } BlockingDialog() = default; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1427,7 +1388,7 @@ struct DLL_LINKAGE GarrisonDialog : public Query ObjectInstanceID objid, hid; bool removableUnits = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1445,7 +1406,7 @@ struct DLL_LINKAGE ExchangeDialog : public Query ObjectInstanceID hero1; ObjectInstanceID hero2; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1470,7 +1431,7 @@ struct DLL_LINKAGE TeleportDialog : public Query TTeleportExitsList exits; bool impassable = false; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1490,7 +1451,7 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query MetaString description; std::vector objects; - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { @@ -1503,559 +1464,6 @@ struct DLL_LINKAGE MapObjectSelectDialog : public Query } }; -struct DLL_LINKAGE BattleStart : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - BattleInfo * info = nullptr; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & info; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleNextRound : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - ui32 stack = 0; - ui8 askPlayerInterface = true; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stack; - h & askPlayerInterface; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleCancelled: public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - - template void serialize(Handler & h, const int version) - { - h & battleID; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResultAccepted : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - struct HeroBattleResults - { - HeroBattleResults() - : hero(nullptr), army(nullptr), exp(0) {} - - CGHeroInstance * hero; - CArmedInstance * army; - TExpType exp; - - template void serialize(Handler & h, const int version) - { - h & hero; - h & army; - h & exp; - } - }; - - BattleID battleID = BattleID::NONE; - std::array heroResult; - ui8 winnerSide; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & heroResult; - h & winnerSide; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResult : public Query -{ - void applyFirstCl(CClient * cl); - - BattleID battleID = BattleID::NONE; - EBattleResult result = EBattleResult::NORMAL; - ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] - std::map casualties[2]; //first => casualties of attackers - map crid => number - TExpType exp[2] = {0, 0}; //exp for attacker and defender - std::set artifacts; //artifacts taken from loser to winner - currently unused - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & queryID; - h & result; - h & winner; - h & casualties[0]; - h & casualties[1]; - h & exp; - h & artifacts; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleLogMessage : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - std::vector lines; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & lines; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleStackMoved : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - ui32 stack = 0; - std::vector tilesToMove; - int distance = 0; - bool teleporting = false; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stack; - h & tilesToMove; - h & distance; - h & teleporting; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector changedStacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & changedStacks; - assert(battleID != BattleID::NONE); - } -}; - -struct BattleStackAttacked -{ - DLL_LINKAGE void applyGs(CGameState * gs); - DLL_LINKAGE void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - ui32 stackAttacked = 0, attackerID = 0; - ui32 killedAmount = 0; - int64_t damageAmount = 0; - UnitChanges newState; - enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; - ui32 flags = 0; //uses EFlags (above) - SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set - - bool killed() const//if target stack was killed - { - return flags & KILLED || flags & CLONE_KILLED; - } - bool cloneKilled() const - { - return flags & CLONE_KILLED; - } - bool isSecondary() const//if stack was not a primary target (receives no spell effects) - { - return flags & SECONDARY; - } - ///Attacked with spell (SPELL_LIKE_ATTACK) - bool isSpell() const - { - return flags & SPELL_EFFECT; - } - bool willRebirth() const//resurrection, e.g. Phoenix - { - return flags & REBIRTH; - } - bool fireShield() const - { - return flags & FIRE_SHIELD; - } - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackAttacked; - h & attackerID; - h & newState; - h & flags; - h & killedAmount; - h & damageAmount; - h & spellID; - assert(battleID != BattleID::NONE); - } - bool operator<(const BattleStackAttacked & b) const - { - return stackAttacked < b.stackAttacked; - } -}; - -struct DLL_LINKAGE BattleAttack : public CPackForClient -{ - void applyGs(CGameState * gs); - BattleUnitsChanged attackerChanges; - - BattleID battleID = BattleID::NONE; - std::vector bsa; - ui32 stackAttacking = 0; - ui32 flags = 0; //uses Eflags (below) - enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; - - BattleHex tile; - SpellID spellID = SpellID::NONE; //for SPELL_LIKE - - bool shot() const//distance attack - decrease number of shots - { - return flags & SHOT; - } - bool counter() const//is it counterattack? - { - return flags & COUNTER; - } - bool lucky() const - { - return flags & LUCKY; - } - bool unlucky() const - { - return flags & UNLUCKY; - } - bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg - { - return flags & BALLISTA_DOUBLE_DMG; - } - bool deathBlow() const - { - return flags & DEATH_BLOW; - } - bool spellLike() const - { - return flags & SPELL_LIKE; - } - bool lifeDrain() const - { - return flags & LIFE_DRAIN; - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & bsa; - h & stackAttacking; - h & flags; - h & tile; - h & spellID; - h & attackerChanges; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE StartAction : public CPackForClient -{ - StartAction() = default; - StartAction(BattleAction act) - : ba(std::move(act)) - { - } - void applyFirstCl(CClient * cl); - void applyGs(CGameState * gs); - - BattleID battleID = BattleID::NONE; - BattleAction ba; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & ba; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE EndAction : public CPackForClient -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - BattleID battleID = BattleID::NONE; - - template void serialize(Handler & h, const int version) - { - h & battleID; - } -}; - -struct DLL_LINKAGE BattleSpellCast : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - bool activeCast = true; - ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender - SpellID spellID; //id of spell - ui8 manaGained = 0; //mana channeling ability - BattleHex tile; //destination tile (may not be set in some global/mass spells - std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) - std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) - std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) - si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID - bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & side; - h & spellID; - h & manaGained; - h & tile; - h & affectedCres; - h & resistedCres; - h & reflectedCres; - h & casterStack; - h & castByHero; - h & activeCast; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE SetStackEffect : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector>> toAdd; - std::vector>> toUpdate; - std::vector>> toRemove; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & toAdd; - h & toUpdate; - h & toRemove; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE StacksInjured : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector stacks; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stacks; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleResultsApplied : public CPackForClient -{ - BattleID battleID = BattleID::NONE; - PlayerColor player1, player2; - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & player1; - h & player2; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector changes; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & changes; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE CatapultAttack : public CPackForClient -{ - struct AttackInfo - { - si16 destinationTile; - EWallPart attackedPart; - ui8 damageDealt; - - template void serialize(Handler & h, const int version) - { - h & destinationTile; - h & attackedPart; - h & damageDealt; - } - }; - - CatapultAttack(); - ~CatapultAttack() override; - - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector< AttackInfo > attackedParts; - int attacker = -1; //if -1, then a spell caused this - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & attackedParts; - h & attacker; - assert(battleID != BattleID::NONE); - } -}; - -struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient -{ - enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; - - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - int stackID = 0; - BattleStackProperty which = CASTS; - int val = 0; - int absolute = 0; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackID; - h & which; - h & val; - h & absolute; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -///activated at the beginning of turn -struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient -{ - void applyGs(CGameState * gs) const; //effect - - BattleID battleID = BattleID::NONE; - int stackID = 0; - int effect = 0; //use corresponding Bonus type - int val = 0; - int additionalInfo = 0; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & stackID; - h & effect; - h & val; - h & additionalInfo; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient -{ - void applyGs(CGameState * gs) const; - - BattleID battleID = BattleID::NONE; - EGateState state = EGateState::NONE; - template void serialize(Handler & h, const int version) - { - h & battleID; - h & state; - assert(battleID != BattleID::NONE); - } - -protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient { ObjectInstanceID casterID; @@ -2067,7 +1475,7 @@ struct DLL_LINKAGE AdvmapSpellCast : public CPackForClient } protected: - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; }; struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient @@ -2085,658 +1493,7 @@ struct DLL_LINKAGE ShowWorldViewEx : public CPackForClient } protected: - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE GamePause : public CPackForServer -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - } -}; - -struct DLL_LINKAGE EndTurn : public CPackForServer -{ - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - } -}; - -struct DLL_LINKAGE DismissHero : public CPackForServer -{ - DismissHero() = default; - DismissHero(const ObjectInstanceID & HID) - : hid(HID) - { - } - ObjectInstanceID hid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - } -}; - -struct DLL_LINKAGE MoveHero : public CPackForServer -{ - MoveHero() = default; - MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) - : dest(Dest) - , hid(HID) - , transit(Transit) - { - } - int3 dest; - ObjectInstanceID hid; - bool transit = false; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - h & transit; - } -}; - -struct DLL_LINKAGE CastleTeleportHero : public CPackForServer -{ - CastleTeleportHero() = default; - CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) - : dest(Dest) - , hid(HID) - , source(Source) - { - } - ObjectInstanceID dest; - ObjectInstanceID hid; - si8 source = 0; //who give teleporting, 1=castle gate - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & dest; - h & hid; - } -}; - -struct DLL_LINKAGE ArrangeStacks : public CPackForServer -{ - ArrangeStacks() = default; - ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) - : what(W) - , p1(P1) - , p2(P2) - , id1(ID1) - , id2(ID2) - , val(VAL) - { - } - - ui8 what = 0; //1 - swap; 2 - merge; 3 - split - SlotID p1, p2; //positions of first and second stack - ObjectInstanceID id1, id2; //ids of objects with garrison - si32 val = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & what; - h & p1; - h & p2; - h & id1; - h & id2; - h & val; - } -}; - -struct DLL_LINKAGE BulkMoveArmy : public CPackForServer -{ - SlotID srcSlot; - ObjectInstanceID srcArmy; - ObjectInstanceID destArmy; - - BulkMoveArmy() = default; - - BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) - : srcArmy(srcArmy) - , destArmy(destArmy) - , srcSlot(srcSlot) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcSlot; - h & srcArmy; - h & destArmy; - } -}; - -struct DLL_LINKAGE BulkSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - si32 amount = 0; - - BulkSplitStack() = default; - - BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) - : src(src) - , srcOwner(srcOwner) - , amount(howMany) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - h & amount; - } -}; - -struct DLL_LINKAGE BulkMergeStacks : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkMergeStacks() = default; - - BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer -{ - SlotID src; - ObjectInstanceID srcOwner; - - BulkSmartSplitStack() = default; - - BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) - : src(src) - , srcOwner(srcOwner) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template - void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & srcOwner; - } -}; - -struct DLL_LINKAGE DisbandCreature : public CPackForServer -{ - DisbandCreature() = default; - DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) - : pos(Pos) - , id(ID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - } -}; - -struct DLL_LINKAGE BuildStructure : public CPackForServer -{ - BuildStructure() = default; - BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) - : tid(TID) - , bid(BID) - { - } - ObjectInstanceID tid; //town id - BuildingID bid; //structure id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & bid; - } -}; - -struct DLL_LINKAGE RazeStructure : public BuildStructure -{ - virtual void visitTyped(ICPackVisitor & visitor) override; -}; - -struct DLL_LINKAGE RecruitCreatures : public CPackForServer -{ - RecruitCreatures() = default; - RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) - : tid(TID) - , dst(DST) - , crid(CRID) - , amount(Amount) - , level(Level) - { - } - ObjectInstanceID tid; //dwelling id, or town - ObjectInstanceID dst; //destination ID, e.g. hero - CreatureID crid; - ui32 amount = 0; //creature amount - si32 level = 0; //dwelling level to buy from, -1 if any - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - h & dst; - h & crid; - h & amount; - h & level; - } -}; - -struct DLL_LINKAGE UpgradeCreature : public CPackForServer -{ - UpgradeCreature() = default; - UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) - : pos(Pos) - , id(ID) - , cid(CRID) - { - } - SlotID pos; //stack pos - ObjectInstanceID id; //object id - CreatureID cid; //id of type to which we want make upgrade - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & pos; - h & id; - h & cid; - } -}; - -struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer -{ - GarrisonHeroSwap() = default; - GarrisonHeroSwap(const ObjectInstanceID & TID) - : tid(TID) - { - } - ObjectInstanceID tid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & tid; - } -}; - -struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer -{ - ArtifactLocation src, dst; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & src; - h & dst; - } -}; - -struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer -{ - ObjectInstanceID srcHero; - ObjectInstanceID dstHero; - bool swap = false; - bool equipped = true; - bool backpack = true; - - BulkExchangeArtifacts() = default; - BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) - : srcHero(srcHero) - , dstHero(dstHero) - , swap(swap) - , equipped(equipped) - , backpack(backpack) - { - } - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & srcHero; - h & dstHero; - h & swap; - h & equipped; - h & backpack; - } -}; - -struct DLL_LINKAGE AssembleArtifacts : public CPackForServer -{ - AssembleArtifacts() = default; - AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) - : heroID(_heroID) - , artifactSlot(_artifactSlot) - , assemble(_assemble) - , assembleTo(_assembleTo) - { - } - ObjectInstanceID heroID; - ArtifactPosition artifactSlot; - bool assemble = false; // True to assemble artifact, false to disassemble. - ArtifactID assembleTo; // Artifact to assemble into. - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & heroID; - h & artifactSlot; - h & assemble; - h & assembleTo; - } -}; - -struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer -{ - EraseArtifactByClient() = default; - EraseArtifactByClient(const ArtifactLocation & al) - : al(al) - { - } - ArtifactLocation al; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & al; - } -}; - -struct DLL_LINKAGE BuyArtifact : public CPackForServer -{ - BuyArtifact() = default; - BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) - : hid(HID) - , aid(AID) - { - } - ObjectInstanceID hid; - ArtifactID aid; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & aid; - } -}; - -struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer -{ - ObjectInstanceID marketId; - ObjectInstanceID heroId; - - EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; - std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] - std::vector val; //units of sold resource - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & marketId; - h & heroId; - h & mode; - h & r1; - h & r2; - h & val; - } -}; - -struct DLL_LINKAGE SetFormation : public CPackForServer -{ - SetFormation() = default; - ; - SetFormation(const ObjectInstanceID & HID, ui8 Formation) - : hid(HID) - , formation(Formation) - { - } - ObjectInstanceID hid; - ui8 formation = 0; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & formation; - } -}; - -struct DLL_LINKAGE HireHero : public CPackForServer -{ - HireHero() = default; - HireHero(HeroTypeID HID, const ObjectInstanceID & TID) - : hid(HID) - , tid(TID) - { - } - HeroTypeID hid; //available hero serial - ObjectInstanceID tid; //town (tavern) id - PlayerColor player; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & tid; - h & player; - } -}; - -struct DLL_LINKAGE BuildBoat : public CPackForServer -{ - ObjectInstanceID objid; //where player wants to buy a boat - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & objid; - } -}; - -struct DLL_LINKAGE QueryReply : public CPackForServer -{ - QueryReply() = default; - QueryReply(const QueryID & QID, std::optional Reply) - : qid(QID) - , reply(Reply) - { - } - QueryID qid; - PlayerColor player; - std::optional reply; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & qid; - h & player; - h & reply; - } -}; - -struct DLL_LINKAGE MakeAction : public CPackForServer -{ - MakeAction() = default; - MakeAction(BattleAction BA) - : ba(std::move(BA)) - { - } - BattleAction ba; - BattleID battleID; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & ba; - h & battleID; - } -}; - -struct DLL_LINKAGE DigWithHero : public CPackForServer -{ - ObjectInstanceID id; //digging hero id - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & id; - } -}; - -struct DLL_LINKAGE CastAdvSpell : public CPackForServer -{ - ObjectInstanceID hid; //hero id - SpellID sid; //spell id - int3 pos; //selected tile (not always used) - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & hid; - h & sid; - h & pos; - } -}; - -/***********************************************************************************************************/ - -struct DLL_LINKAGE SaveGame : public CPackForServer -{ - SaveGame() = default; - SaveGame(std::string Fname) - : fname(std::move(Fname)) - { - } - std::string fname; - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & fname; - } -}; - -struct DLL_LINKAGE PlayerMessage : public CPackForServer -{ - PlayerMessage() = default; - PlayerMessage(std::string Text, const ObjectInstanceID & obj) - : text(std::move(Text)) - , currObj(obj) - { - } - - void applyGs(CGameState * gs) {}; - - virtual void visitTyped(ICPackVisitor & visitor) override; - - std::string text; - ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) - - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & text; - h & currObj; - } + void visitTyped(ICPackVisitor & visitor) override; }; struct DLL_LINKAGE PlayerMessageClient : public CPackForClient @@ -2747,7 +1504,7 @@ struct DLL_LINKAGE PlayerMessageClient : public CPackForClient , text(std::move(Text)) { } - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; PlayerColor player; std::string text; @@ -2765,7 +1522,7 @@ struct DLL_LINKAGE CenterView : public CPackForClient int3 pos; ui32 focusTime = 0; //ms - virtual void visitTyped(ICPackVisitor & visitor) override; + void visitTyped(ICPackVisitor & visitor) override; template void serialize(Handler & h, const int version) { diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h new file mode 100644 index 000000000..4e87f8a39 --- /dev/null +++ b/lib/networkPacks/PacksForClientBattle.h @@ -0,0 +1,575 @@ +/* + * NetPacks.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 "NetPacksBase.h" +#include "BattleChanges.h" +#include "../MetaString.h" +#include "../battle/BattleAction.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IBattleState; +class BattleInfo; + +struct DLL_LINKAGE BattleStart : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + BattleInfo * info = nullptr; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & info; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleNextRound : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetActiveStack : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + ui8 askPlayerInterface = true; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & askPlayerInterface; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleCancelled: public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultAccepted : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + struct HeroBattleResults + { + HeroBattleResults() + : hero(nullptr), army(nullptr), exp(0) {} + + CGHeroInstance * hero; + CArmedInstance * army; + TExpType exp; + + template void serialize(Handler & h, const int version) + { + h & hero; + h & army; + h & exp; + } + }; + + BattleID battleID = BattleID::NONE; + std::array heroResult; + ui8 winnerSide; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & heroResult; + h & winnerSide; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResult : public Query +{ + void applyFirstCl(CClient * cl); + + BattleID battleID = BattleID::NONE; + EBattleResult result = EBattleResult::NORMAL; + ui8 winner = 2; //0 - attacker, 1 - defender, [2 - draw (should be possible?)] + std::map casualties[2]; //first => casualties of attackers - map crid => number + TExpType exp[2] = {0, 0}; //exp for attacker and defender + std::set artifacts; //artifacts taken from loser to winner - currently unused + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & queryID; + h & result; + h & winner; + h & casualties[0]; + h & casualties[1]; + h & exp; + h & artifacts; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleLogMessage : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + std::vector lines; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & lines; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleStackMoved : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + ui32 stack = 0; + std::vector tilesToMove; + int distance = 0; + bool teleporting = false; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stack; + h & tilesToMove; + h & distance; + h & teleporting; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleUnitsChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changedStacks; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changedStacks; + assert(battleID != BattleID::NONE); + } +}; + +struct BattleStackAttacked +{ + DLL_LINKAGE void applyGs(CGameState * gs); + DLL_LINKAGE void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + ui32 stackAttacked = 0, attackerID = 0; + ui32 killedAmount = 0; + int64_t damageAmount = 0; + UnitChanges newState; + enum EFlags { KILLED = 1, SECONDARY = 2, REBIRTH = 4, CLONE_KILLED = 8, SPELL_EFFECT = 16, FIRE_SHIELD = 32, }; + ui32 flags = 0; //uses EFlags (above) + SpellID spellID = SpellID::NONE; //only if flag SPELL_EFFECT is set + + bool killed() const//if target stack was killed + { + return flags & KILLED || flags & CLONE_KILLED; + } + bool cloneKilled() const + { + return flags & CLONE_KILLED; + } + bool isSecondary() const//if stack was not a primary target (receives no spell effects) + { + return flags & SECONDARY; + } + ///Attacked with spell (SPELL_LIKE_ATTACK) + bool isSpell() const + { + return flags & SPELL_EFFECT; + } + bool willRebirth() const//resurrection, e.g. Phoenix + { + return flags & REBIRTH; + } + bool fireShield() const + { + return flags & FIRE_SHIELD; + } + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackAttacked; + h & attackerID; + h & newState; + h & flags; + h & killedAmount; + h & damageAmount; + h & spellID; + assert(battleID != BattleID::NONE); + } + bool operator<(const BattleStackAttacked & b) const + { + return stackAttacked < b.stackAttacked; + } +}; + +struct DLL_LINKAGE BattleAttack : public CPackForClient +{ + void applyGs(CGameState * gs); + BattleUnitsChanged attackerChanges; + + BattleID battleID = BattleID::NONE; + std::vector bsa; + ui32 stackAttacking = 0; + ui32 flags = 0; //uses Eflags (below) + enum EFlags { SHOT = 1, COUNTER = 2, LUCKY = 4, UNLUCKY = 8, BALLISTA_DOUBLE_DMG = 16, DEATH_BLOW = 32, SPELL_LIKE = 64, LIFE_DRAIN = 128 }; + + BattleHex tile; + SpellID spellID = SpellID::NONE; //for SPELL_LIKE + + bool shot() const//distance attack - decrease number of shots + { + return flags & SHOT; + } + bool counter() const//is it counterattack? + { + return flags & COUNTER; + } + bool lucky() const + { + return flags & LUCKY; + } + bool unlucky() const + { + return flags & UNLUCKY; + } + bool ballistaDoubleDmg() const //if it's ballista attack and does double dmg + { + return flags & BALLISTA_DOUBLE_DMG; + } + bool deathBlow() const + { + return flags & DEATH_BLOW; + } + bool spellLike() const + { + return flags & SPELL_LIKE; + } + bool lifeDrain() const + { + return flags & LIFE_DRAIN; + } + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & bsa; + h & stackAttacking; + h & flags; + h & tile; + h & spellID; + h & attackerChanges; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StartAction : public CPackForClient +{ + StartAction() = default; + explicit StartAction(BattleAction act) + : ba(std::move(act)) + { + } + void applyFirstCl(CClient * cl); + void applyGs(CGameState * gs); + + BattleID battleID = BattleID::NONE; + BattleAction ba; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & ba; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE EndAction : public CPackForClient +{ + void visitTyped(ICPackVisitor & visitor) override; + + BattleID battleID = BattleID::NONE; + + template void serialize(Handler & h, const int version) + { + h & battleID; + } +}; + +struct DLL_LINKAGE BattleSpellCast : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + bool activeCast = true; + ui8 side = 0; //which hero did cast spell: 0 - attacker, 1 - defender + SpellID spellID; //id of spell + ui8 manaGained = 0; //mana channeling ability + BattleHex tile; //destination tile (may not be set in some global/mass spells + std::set affectedCres; //ids of creatures affected by this spell, generally used if spell does not set any effect (like dispel or cure) + std::set resistedCres; // creatures that resisted the spell (e.g. Dwarves) + std::set reflectedCres; // creatures that reflected the spell (e.g. Magic Mirror spell) + si32 casterStack = -1; // -1 if not cated by creature, >=0 caster stack ID + bool castByHero = true; //if true - spell has been cast by hero, otherwise by a creature + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & side; + h & spellID; + h & manaGained; + h & tile; + h & affectedCres; + h & resistedCres; + h & reflectedCres; + h & casterStack; + h & castByHero; + h & activeCast; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE SetStackEffect : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector>> toAdd; + std::vector>> toUpdate; + std::vector>> toRemove; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & toAdd; + h & toUpdate; + h & toRemove; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE StacksInjured : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector stacks; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stacks; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleResultsApplied : public CPackForClient +{ + BattleID battleID = BattleID::NONE; + PlayerColor player1, player2; + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & player1; + h & player2; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleObstaclesChanged : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector changes; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & changes; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE CatapultAttack : public CPackForClient +{ + struct AttackInfo + { + si16 destinationTile; + EWallPart attackedPart; + ui8 damageDealt; + + template void serialize(Handler & h, const int version) + { + h & destinationTile; + h & attackedPart; + h & damageDealt; + } + }; + + CatapultAttack(); + ~CatapultAttack() override; + + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector< AttackInfo > attackedParts; + int attacker = -1; //if -1, then a spell caused this + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & attackedParts; + h & attacker; + assert(battleID != BattleID::NONE); + } +}; + +struct DLL_LINKAGE BattleSetStackProperty : public CPackForClient +{ + enum BattleStackProperty { CASTS, ENCHANTER_COUNTER, UNBIND, CLONED, HAS_CLONE }; + + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + int stackID = 0; + BattleStackProperty which = CASTS; + int val = 0; + int absolute = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & which; + h & val; + h & absolute; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +///activated at the beginning of turn +struct DLL_LINKAGE BattleTriggerEffect : public CPackForClient +{ + void applyGs(CGameState * gs) const; //effect + + BattleID battleID = BattleID::NONE; + int stackID = 0; + int effect = 0; //use corresponding Bonus type + int val = 0; + int additionalInfo = 0; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & stackID; + h & effect; + h & val; + h & additionalInfo; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE BattleUpdateGateState : public CPackForClient +{ + void applyGs(CGameState * gs) const; + + BattleID battleID = BattleID::NONE; + EGateState state = EGateState::NONE; + template void serialize(Handler & h, const int version) + { + h & battleID; + h & state; + assert(battleID != BattleID::NONE); + } + +protected: + void visitTyped(ICPackVisitor & visitor) override; +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h new file mode 100644 index 000000000..d71681450 --- /dev/null +++ b/lib/networkPacks/PacksForServer.h @@ -0,0 +1,667 @@ +/* + * NetPacks.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 "NetPacksBase.h" +#include "../int3.h" +#include "../battle/BattleAction.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct DLL_LINKAGE GamePause : public CPackForServer +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE EndTurn : public CPackForServer +{ + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + } +}; + +struct DLL_LINKAGE DismissHero : public CPackForServer +{ + DismissHero() = default; + DismissHero(const ObjectInstanceID & HID) + : hid(HID) + { + } + ObjectInstanceID hid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + } +}; + +struct DLL_LINKAGE MoveHero : public CPackForServer +{ + MoveHero() = default; + MoveHero(const int3 & Dest, const ObjectInstanceID & HID, bool Transit) + : dest(Dest) + , hid(HID) + , transit(Transit) + { + } + int3 dest; + ObjectInstanceID hid; + bool transit = false; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + h & transit; + } +}; + +struct DLL_LINKAGE CastleTeleportHero : public CPackForServer +{ + CastleTeleportHero() = default; + CastleTeleportHero(const ObjectInstanceID & HID, const ObjectInstanceID & Dest, ui8 Source) + : dest(Dest) + , hid(HID) + , source(Source) + { + } + ObjectInstanceID dest; + ObjectInstanceID hid; + si8 source = 0; //who give teleporting, 1=castle gate + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & dest; + h & hid; + } +}; + +struct DLL_LINKAGE ArrangeStacks : public CPackForServer +{ + ArrangeStacks() = default; + ArrangeStacks(ui8 W, const SlotID & P1, const SlotID & P2, const ObjectInstanceID & ID1, const ObjectInstanceID & ID2, si32 VAL) + : what(W) + , p1(P1) + , p2(P2) + , id1(ID1) + , id2(ID2) + , val(VAL) + { + } + + ui8 what = 0; //1 - swap; 2 - merge; 3 - split + SlotID p1, p2; //positions of first and second stack + ObjectInstanceID id1, id2; //ids of objects with garrison + si32 val = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & what; + h & p1; + h & p2; + h & id1; + h & id2; + h & val; + } +}; + +struct DLL_LINKAGE BulkMoveArmy : public CPackForServer +{ + SlotID srcSlot; + ObjectInstanceID srcArmy; + ObjectInstanceID destArmy; + + BulkMoveArmy() = default; + + BulkMoveArmy(const ObjectInstanceID & srcArmy, const ObjectInstanceID & destArmy, const SlotID & srcSlot) + : srcArmy(srcArmy) + , destArmy(destArmy) + , srcSlot(srcSlot) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcSlot; + h & srcArmy; + h & destArmy; + } +}; + +struct DLL_LINKAGE BulkSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + si32 amount = 0; + + BulkSplitStack() = default; + + BulkSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src, si32 howMany) + : src(src) + , srcOwner(srcOwner) + , amount(howMany) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + h & amount; + } +}; + +struct DLL_LINKAGE BulkMergeStacks : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkMergeStacks() = default; + + BulkMergeStacks(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE BulkSmartSplitStack : public CPackForServer +{ + SlotID src; + ObjectInstanceID srcOwner; + + BulkSmartSplitStack() = default; + + BulkSmartSplitStack(const ObjectInstanceID & srcOwner, const SlotID & src) + : src(src) + , srcOwner(srcOwner) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template + void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & srcOwner; + } +}; + +struct DLL_LINKAGE DisbandCreature : public CPackForServer +{ + DisbandCreature() = default; + DisbandCreature(const SlotID & Pos, const ObjectInstanceID & ID) + : pos(Pos) + , id(ID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + } +}; + +struct DLL_LINKAGE BuildStructure : public CPackForServer +{ + BuildStructure() = default; + BuildStructure(const ObjectInstanceID & TID, const BuildingID & BID) + : tid(TID) + , bid(BID) + { + } + ObjectInstanceID tid; //town id + BuildingID bid; //structure id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & bid; + } +}; + +struct DLL_LINKAGE RazeStructure : public BuildStructure +{ + void visitTyped(ICPackVisitor & visitor) override; +}; + +struct DLL_LINKAGE RecruitCreatures : public CPackForServer +{ + RecruitCreatures() = default; + RecruitCreatures(const ObjectInstanceID & TID, const ObjectInstanceID & DST, const CreatureID & CRID, si32 Amount, si32 Level) + : tid(TID) + , dst(DST) + , crid(CRID) + , amount(Amount) + , level(Level) + { + } + ObjectInstanceID tid; //dwelling id, or town + ObjectInstanceID dst; //destination ID, e.g. hero + CreatureID crid; + ui32 amount = 0; //creature amount + si32 level = 0; //dwelling level to buy from, -1 if any + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + h & dst; + h & crid; + h & amount; + h & level; + } +}; + +struct DLL_LINKAGE UpgradeCreature : public CPackForServer +{ + UpgradeCreature() = default; + UpgradeCreature(const SlotID & Pos, const ObjectInstanceID & ID, const CreatureID & CRID) + : pos(Pos) + , id(ID) + , cid(CRID) + { + } + SlotID pos; //stack pos + ObjectInstanceID id; //object id + CreatureID cid; //id of type to which we want make upgrade + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & pos; + h & id; + h & cid; + } +}; + +struct DLL_LINKAGE GarrisonHeroSwap : public CPackForServer +{ + GarrisonHeroSwap() = default; + GarrisonHeroSwap(const ObjectInstanceID & TID) + : tid(TID) + { + } + ObjectInstanceID tid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & tid; + } +}; + +struct DLL_LINKAGE ExchangeArtifacts : public CPackForServer +{ + ArtifactLocation src, dst; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & src; + h & dst; + } +}; + +struct DLL_LINKAGE BulkExchangeArtifacts : public CPackForServer +{ + ObjectInstanceID srcHero; + ObjectInstanceID dstHero; + bool swap = false; + bool equipped = true; + bool backpack = true; + + BulkExchangeArtifacts() = default; + BulkExchangeArtifacts(const ObjectInstanceID & srcHero, const ObjectInstanceID & dstHero, bool swap, bool equipped, bool backpack) + : srcHero(srcHero) + , dstHero(dstHero) + , swap(swap) + , equipped(equipped) + , backpack(backpack) + { + } + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & srcHero; + h & dstHero; + h & swap; + h & equipped; + h & backpack; + } +}; + +struct DLL_LINKAGE AssembleArtifacts : public CPackForServer +{ + AssembleArtifacts() = default; + AssembleArtifacts(const ObjectInstanceID & _heroID, const ArtifactPosition & _artifactSlot, bool _assemble, const ArtifactID & _assembleTo) + : heroID(_heroID) + , artifactSlot(_artifactSlot) + , assemble(_assemble) + , assembleTo(_assembleTo) + { + } + ObjectInstanceID heroID; + ArtifactPosition artifactSlot; + bool assemble = false; // True to assemble artifact, false to disassemble. + ArtifactID assembleTo; // Artifact to assemble into. + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & heroID; + h & artifactSlot; + h & assemble; + h & assembleTo; + } +}; + +struct DLL_LINKAGE EraseArtifactByClient : public CPackForServer +{ + EraseArtifactByClient() = default; + EraseArtifactByClient(const ArtifactLocation & al) + : al(al) + { + } + ArtifactLocation al; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & al; + } +}; + +struct DLL_LINKAGE BuyArtifact : public CPackForServer +{ + BuyArtifact() = default; + BuyArtifact(const ObjectInstanceID & HID, const ArtifactID & AID) + : hid(HID) + , aid(AID) + { + } + ObjectInstanceID hid; + ArtifactID aid; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & aid; + } +}; + +struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer +{ + ObjectInstanceID marketId; + ObjectInstanceID heroId; + + EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; + std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] + std::vector val; //units of sold resource + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & marketId; + h & heroId; + h & mode; + h & r1; + h & r2; + h & val; + } +}; + +struct DLL_LINKAGE SetFormation : public CPackForServer +{ + SetFormation() = default; + ; + SetFormation(const ObjectInstanceID & HID, ui8 Formation) + : hid(HID) + , formation(Formation) + { + } + ObjectInstanceID hid; + ui8 formation = 0; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & formation; + } +}; + +struct DLL_LINKAGE HireHero : public CPackForServer +{ + HireHero() = default; + HireHero(HeroTypeID HID, const ObjectInstanceID & TID) + : hid(HID) + , tid(TID) + { + } + HeroTypeID hid; //available hero serial + ObjectInstanceID tid; //town (tavern) id + PlayerColor player; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & tid; + h & player; + } +}; + +struct DLL_LINKAGE BuildBoat : public CPackForServer +{ + ObjectInstanceID objid; //where player wants to buy a boat + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & objid; + } +}; + +struct DLL_LINKAGE QueryReply : public CPackForServer +{ + QueryReply() = default; + QueryReply(const QueryID & QID, std::optional Reply) + : qid(QID) + , reply(Reply) + { + } + QueryID qid; + PlayerColor player; + std::optional reply; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & qid; + h & player; + h & reply; + } +}; + +struct DLL_LINKAGE MakeAction : public CPackForServer +{ + MakeAction() = default; + MakeAction(BattleAction BA) + : ba(std::move(BA)) + { + } + BattleAction ba; + BattleID battleID; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & ba; + h & battleID; + } +}; + +struct DLL_LINKAGE DigWithHero : public CPackForServer +{ + ObjectInstanceID id; //digging hero id + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & id; + } +}; + +struct DLL_LINKAGE CastAdvSpell : public CPackForServer +{ + ObjectInstanceID hid; //hero id + SpellID sid; //spell id + int3 pos; //selected tile (not always used) + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & hid; + h & sid; + h & pos; + } +}; + +/***********************************************************************************************************/ + +struct DLL_LINKAGE SaveGame : public CPackForServer +{ + SaveGame() = default; + SaveGame(std::string Fname) + : fname(std::move(Fname)) + { + } + std::string fname; + + void applyGs(CGameState * gs) {}; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & fname; + } +}; + +struct DLL_LINKAGE PlayerMessage : public CPackForServer +{ + PlayerMessage() = default; + PlayerMessage(std::string Text, const ObjectInstanceID & obj) + : text(std::move(Text)) + , currObj(obj) + { + } + + void applyGs(CGameState * gs) {}; + + void visitTyped(ICPackVisitor & visitor) override; + + std::string text; + ObjectInstanceID currObj; // optional parameter that specifies current object. For cheats :) + + template void serialize(Handler & h, const int version) + { + h & static_cast(*this); + h & text; + h & currObj; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/StackLocation.h b/lib/networkPacks/StackLocation.h new file mode 100644 index 000000000..abb922d0f --- /dev/null +++ b/lib/networkPacks/StackLocation.h @@ -0,0 +1,40 @@ +/* + * StackLocation.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 "../ConstTransitivePtr.h" +#include "../GameConstants.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CArmedInstance; +class CStackInstance; + +struct StackLocation +{ + ConstTransitivePtr army; + SlotID slot; + + StackLocation() = default; + StackLocation(const CArmedInstance * Army, const SlotID & Slot) + : army(const_cast(Army)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! + , slot(Slot) + { + } + + DLL_LINKAGE const CStackInstance * getStack(); + template void serialize(Handler & h, const int version) + { + h & army; + h & slot; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index fd52db8f8..4f375148f 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -9,8 +9,10 @@ */ #pragma once -#include "../NetPacks.h" -#include "../NetPacksLobby.h" +#include "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/PacksForServer.h" +#include "../networkPacks/NetPacksLobby.h" #include "../VCMI_Lib.h" #include "../CArtHandler.h" #include "../CCreatureSet.h" diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp index a27008c89..c13b78c83 100644 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ b/lib/registerTypes/TypesClientPacks1.cpp @@ -18,7 +18,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp index cfa8edc7e..bf68ca257 100644 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ b/lib/registerTypes/TypesClientPacks2.cpp @@ -20,7 +20,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp index 56a26e4ff..fe3838d05 100644 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ b/lib/registerTypes/TypesLobbyPacks.cpp @@ -27,7 +27,6 @@ #include "../RiverHandler.h" #include "../TerrainHandler.h" #include "../campaign/CampaignState.h" -#include "../NetPacks.h" #include "../rmg/CMapGenOptions.h" #include "../serializer/BinaryDeserializer.h" diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp index 2e4df3a01..1e383a581 100644 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ b/lib/registerTypes/TypesMapObjects1.cpp @@ -18,7 +18,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp index 0bcb062d5..ab7d4f1a1 100644 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ b/lib/registerTypes/TypesMapObjects2.cpp @@ -20,7 +20,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/registerTypes/TypesMapObjects3.cpp b/lib/registerTypes/TypesMapObjects3.cpp index 79a471c4f..3833d0dbf 100644 --- a/lib/registerTypes/TypesMapObjects3.cpp +++ b/lib/registerTypes/TypesMapObjects3.cpp @@ -18,7 +18,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp index 7fa0991a7..3d37cbc92 100644 --- a/lib/registerTypes/TypesServerPacks.cpp +++ b/lib/registerTypes/TypesServerPacks.cpp @@ -18,7 +18,6 @@ #include "../CHeroHandler.h" #include "../spells/CSpellHandler.h" #include "../CTownHandler.h" -#include "../NetPacks.h" #include "../serializer/BinaryDeserializer.h" #include "../serializer/BinarySerializer.h" diff --git a/lib/rewardable/Configuration.h b/lib/rewardable/Configuration.h index c31290506..6ea12df6a 100644 --- a/lib/rewardable/Configuration.h +++ b/lib/rewardable/Configuration.h @@ -12,8 +12,8 @@ #include "Limiter.h" #include "MetaString.h" -#include "NetPacksBase.h" #include "Reward.h" +#include "../networkPacks/EInfoWindowMode.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index 18fd6cc9b..da7f86520 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -15,11 +15,14 @@ #include "../TerrainHandler.h" #include "../CPlayerState.h" #include "../CSoundBase.h" -#include "../NetPacks.h" #include "../gameState/CGameState.h" #include "../spells/CSpellHandler.h" #include "../spells/ISpellMechanics.h" +#include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/MiscObjects.h" +#include "../mapping/CMapDefines.h" +#include "../networkPacks/StackLocation.h" +#include "../networkPacks/PacksForClient.h" #include "../IGameCallback.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index d0db694fb..ae6602231 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -14,6 +14,7 @@ #include "../IGameCallback.h" #include "../CPlayerState.h" #include "../mapObjects/CGHeroInstance.h" +#include "../networkPacks/Component.h" #include "../serializer/JsonSerializeFormat.h" #include "../constants/StringConstants.h" #include "../CHeroHandler.h" diff --git a/lib/rewardable/Reward.h b/lib/rewardable/Reward.h index 2b0a5a829..5c4fd84a2 100644 --- a/lib/rewardable/Reward.h +++ b/lib/rewardable/Reward.h @@ -13,6 +13,7 @@ #include "../ResourceSet.h" #include "../bonuses/Bonus.h" #include "../CCreatureSet.h" +#include "../networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 9f21a598c..7e1e3c19e 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -14,12 +14,12 @@ #include "CSpellHandler.h" +#include "../CGameInfoCallback.h" +#include "../CPlayerState.h" #include "../CRandomGenerator.h" #include "../mapObjects/CGHeroInstance.h" -#include "../NetPacks.h" -#include "../CGameInfoCallback.h" #include "../mapping/CMap.h" -#include "../CPlayerState.h" +#include "../networkPacks/PacksForClient.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index e656e70e2..36ce8dd96 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -16,9 +16,8 @@ #include "../battle/IBattleState.h" #include "../battle/CBattleInfoCallback.h" - +#include "../networkPacks/PacksForClientBattle.h" #include "../CStack.h" -#include "../NetPacks.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 755d36f16..0a2be59cc 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -36,7 +36,6 @@ #include "effects/Timed.h" #include "CSpellHandler.h" -#include "../NetPacks.h" #include "../CHeroHandler.h"//todo: remove #include "../IGameCallback.h"//todo: remove diff --git a/lib/spells/ObstacleCasterProxy.h b/lib/spells/ObstacleCasterProxy.h index 761eea2b5..075d31cf4 100644 --- a/lib/spells/ObstacleCasterProxy.h +++ b/lib/spells/ObstacleCasterProxy.h @@ -9,7 +9,6 @@ */ #include "ProxyCaster.h" -#include "../lib/NetPacksBase.h" #include "../battle/BattleHex.h" #include "../battle/CObstacleInstance.h" diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 5212278b1..74d0cb332 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -14,11 +14,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../mapObjects/CGTownInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index 65bd7df44..d54d33caf 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -12,10 +12,10 @@ #include "Clone.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index 0d3b17b76..bb788193b 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -14,10 +14,11 @@ #include "../CSpellHandler.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../CStack.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../CGeneralTextHandler.h" #include "../../serializer/JsonSerializeFormat.h" diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index b2be990fa..09f8ec79e 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -12,10 +12,10 @@ #include "DemonSummon.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/BattleInfo.h" #include "../../battle/CUnitState.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 19d4fe470..c2765cc40 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -16,10 +16,12 @@ #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../bonuses/BonusList.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Heal.cpp b/lib/spells/effects/Heal.cpp index a4a2bc051..d3d84d8a9 100644 --- a/lib/spells/effects/Heal.cpp +++ b/lib/spells/effects/Heal.cpp @@ -13,11 +13,12 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 92b96dfab..8546f779b 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -14,12 +14,13 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../mapObjects/CGTownInstance.h" #include "../../bonuses/Limiters.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../networkPacks/PacksForClient.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Obstacle.cpp b/lib/spells/effects/Obstacle.cpp index 731bb736c..b9dbe3b0b 100644 --- a/lib/spells/effects/Obstacle.cpp +++ b/lib/spells/effects/Obstacle.cpp @@ -14,10 +14,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../CRandomGenerator.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/RemoveObstacle.cpp b/lib/spells/effects/RemoveObstacle.cpp index 10c5ca57f..f24b0e881 100644 --- a/lib/spells/effects/RemoveObstacle.cpp +++ b/lib/spells/effects/RemoveObstacle.cpp @@ -14,10 +14,10 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/CObstacleInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Sacrifice.cpp b/lib/spells/effects/Sacrifice.cpp index 537697c5e..978d0f4d4 100644 --- a/lib/spells/effects/Sacrifice.cpp +++ b/lib/spells/effects/Sacrifice.cpp @@ -13,11 +13,11 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index a1b623293..432f6ab7c 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -13,15 +13,15 @@ #include "Registry.h" #include "../ISpellMechanics.h" +#include "../../MetaString.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/BattleInfo.h" #include "../../battle/Unit.h" -#include "../../NetPacks.h" #include "../../serializer/JsonSerializeFormat.h" - #include "../../CCreatureHandler.h" #include "../../CHeroHandler.h" #include "../../mapObjects/CGHeroInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index c845793da..bffffa6fd 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -12,10 +12,10 @@ #include "Teleport.h" #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" #include "../../battle/CBattleInfoCallback.h" -#include "../../serializer/JsonSerializeFormat.h" #include "../../battle/Unit.h" +#include "../../networkPacks/PacksForClientBattle.h" +#include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 31e85de67..33d8e6cdf 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -13,10 +13,12 @@ #include "Registry.h" #include "../ISpellMechanics.h" -#include "../../NetPacks.h" +#include "../../MetaString.h" #include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" +#include "../../mapObjects/CGHeroInstance.h" +#include "../../networkPacks/PacksForClientBattle.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/UnitEffect.cpp b/lib/spells/effects/UnitEffect.cpp index c9dc5ff98..3d1be5842 100644 --- a/lib/spells/effects/UnitEffect.cpp +++ b/lib/spells/effects/UnitEffect.cpp @@ -14,7 +14,6 @@ #include "../ISpellMechanics.h" #include "../../bonuses/BonusSelector.h" -#include "../../NetPacksBase.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../serializer/JsonSerializeFormat.h" diff --git a/scripting/lua/LuaScriptingContext.cpp b/scripting/lua/LuaScriptingContext.cpp index 41676a2b6..2eda9ac47 100644 --- a/scripting/lua/LuaScriptingContext.cpp +++ b/scripting/lua/LuaScriptingContext.cpp @@ -20,7 +20,6 @@ #include "api/Registry.h" #include "../../lib/JsonNode.h" -#include "../../lib/NetPacks.h" #include "../../lib/filesystem/Filesystem.h" #include "../../lib/battle/IBattleInfoCallback.h" #include "../../lib/CGameInfoCallback.h" diff --git a/scripting/lua/api/netpacks/BattleLogMessage.h b/scripting/lua/api/netpacks/BattleLogMessage.h index ebe6f049e..62ed18bc7 100644 --- a/scripting/lua/api/netpacks/BattleLogMessage.h +++ b/scripting/lua/api/netpacks/BattleLogMessage.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/EntitiesChanged.h b/scripting/lua/api/netpacks/EntitiesChanged.h index 4b7e0f0b8..062edb979 100644 --- a/scripting/lua/api/netpacks/EntitiesChanged.h +++ b/scripting/lua/api/netpacks/EntitiesChanged.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/InfoWindow.h b/scripting/lua/api/netpacks/InfoWindow.h index f941761f8..d841a342c 100644 --- a/scripting/lua/api/netpacks/InfoWindow.h +++ b/scripting/lua/api/netpacks/InfoWindow.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/PackForClient.h b/scripting/lua/api/netpacks/PackForClient.h index c81732ce3..4ff676473 100644 --- a/scripting/lua/api/netpacks/PackForClient.h +++ b/scripting/lua/api/netpacks/PackForClient.h @@ -12,7 +12,7 @@ #include "../../LuaWrapper.h" -#include "../../../../lib/NetPacks.h" +#include "../../../../lib/networkPacks/NetPacksBase.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/scripting/lua/api/netpacks/SetResources.cpp b/scripting/lua/api/netpacks/SetResources.cpp index d71e2aa34..4338fa987 100644 --- a/scripting/lua/api/netpacks/SetResources.cpp +++ b/scripting/lua/api/netpacks/SetResources.cpp @@ -12,6 +12,7 @@ #include "SetResources.h" #include "../../LuaStack.h" +#include "../../../../lib/networkPacks/PacksForClient.h" #include "../Registry.h" diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 2fa72d5d1..7a0e39c98 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -48,6 +48,7 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" #include "../lib/modding/ModIncompatibility.h" +#include "../lib/networkPacks/StackLocation.h" #include "../lib/pathfinder/CPathfinder.h" #include "../lib/pathfinder/PathfinderOptions.h" #include "../lib/pathfinder/TurnInfo.h" diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 271ece218..45aa2dea1 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -26,7 +26,6 @@ #include "../lib/StartInfo.h" #include "../lib/mapping/CMapHeader.h" #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/NetPackVisitor.h" #include "LobbyNetPackVisitors.h" #ifdef VCMI_ANDROID #include diff --git a/server/LobbyNetPackVisitors.h b/server/LobbyNetPackVisitors.h index e55c7040f..5ee02504c 100644 --- a/server/LobbyNetPackVisitors.h +++ b/server/LobbyNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class ClientPermissionsCheckerNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) { diff --git a/server/NetPacksLobbyServer.cpp b/server/NetPacksLobbyServer.cpp index 69c66fcd8..a079309ef 100644 --- a/server/NetPacksLobbyServer.cpp +++ b/server/NetPacksLobbyServer.cpp @@ -13,7 +13,6 @@ #include "CVCMIServer.h" #include "CGameHandler.h" -#include "../lib/NetPacksLobby.h" #include "../lib/serializer/Connection.h" #include "../lib/StartInfo.h" diff --git a/server/ServerNetPackVisitors.h b/server/ServerNetPackVisitors.h index de1ec7ff6..487af47de 100644 --- a/server/ServerNetPackVisitors.h +++ b/server/ServerNetPackVisitors.h @@ -9,7 +9,7 @@ */ #pragma once -#include "../lib/NetPackVisitor.h" +#include "../lib/networkPacks/NetPackVisitor.h" class ApplyGhNetPackVisitor : public VCMI_LIB_WRAP_NAMESPACE(ICPackVisitor) { diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index 5f856cae0..ae57ebbff 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -15,7 +15,7 @@ #include "queries/CQuery.h" #include "../lib/gameState/CGameState.h" -#include "../lib/NetPacks.h" +#include "../lib/networkPacks/PacksForClientBattle.h" ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index 489ee2fa6..ebd51e826 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -15,10 +15,11 @@ #include "processors/TurnOrderProcessor.h" #include "../lib/battle/BattleInfo.h" #include "../lib/gameState/CGameState.h" +#include "../lib/networkPacks/PacksForClient.h" +#include "../lib/networkPacks/PacksForClientBattle.h" #include "../lib/CPlayerState.h" #include "../lib/CStack.h" #include "../lib/StartInfo.h" -#include "../lib/NetPacks.h" TurnTimerHandler::TurnTimerHandler(CGameHandler & gh): gameHandler(gh) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 53a70645a..293b119e3 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -18,10 +18,11 @@ #include "../../lib/CStack.h" #include "../../lib/GameSettings.h" #include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/IBattleState.h" #include "../../lib/battle/BattleAction.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/spells/AbilityCaster.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 501408660..3a28f2fd9 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -20,7 +20,7 @@ #include "../../lib/battle/IBattleState.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/spells/BonusCaster.h" #include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ObstacleCasterProxy.h" diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 9a443b0c9..e4436a83d 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -20,10 +20,14 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/battle/CBattleInfoCallback.h" +#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/BattleInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapping/CMap.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/modding/IdentifierStorage.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/CPlayerState.h" BattleProcessor::BattleProcessor(CGameHandler * gameHandler) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 1369cb8de..230d2cf1b 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -23,6 +23,8 @@ #include "../../lib/battle/SideInBattle.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/serializer/Cast.h" #include "../../lib/spells/CSpellHandler.h" diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index 88ac0e795..88d4bb11a 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -10,10 +10,14 @@ #pragma once #include "../../lib/GameConstants.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/StackLocation.h" +#include "../../lib/networkPacks/NetPacksBase.h" VCMI_LIB_NAMESPACE_BEGIN struct SideInBattle; +struct BattleResult; +class CBattleInfoCallback; +class CGHeroInstance; VCMI_LIB_NAMESPACE_END class CBattleQuery; @@ -24,7 +28,6 @@ struct CasualtiesAfterBattle { using TStackAndItsNewCount = std::pair; using TSummoned = std::map; - // enum {ERASE = -1}; const CArmedInstance * army; std::vector newStackCounts; std::vector removedWarMachines; @@ -37,7 +40,6 @@ struct CasualtiesAfterBattle struct FinishingBattleHelper { -// FinishingBattleHelper(); FinishingBattleHelper(const CBattleInfoCallback & battle, const BattleResult & result, int RemainingBattleQueriesCount); inline bool isDraw() const {return winnerSide == 2;} diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index 733d75806..f37f09f8a 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -16,9 +16,10 @@ #include "../../lib/CHeroHandler.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameSettings.h" -#include "../../lib/NetPacks.h" #include "../../lib/StartInfo.h" #include "../../lib/mapObjects/CGTownInstance.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/gameState/TavernHeroesPool.h" #include "../../lib/gameState/TavernSlot.h" diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index 74aeb36d7..c2d319380 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -19,12 +19,13 @@ #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/CPlayerState.h" #include "../../lib/GameConstants.h" -#include "../../lib/NetPacks.h" #include "../../lib/StartInfo.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/modding/IdentifierStorage.h" #include "../../lib/modding/ModScope.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/StackLocation.h" PlayerMessageProcessor::PlayerMessageProcessor() :gameHandler(nullptr) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index b1359fadd..242055dc0 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -16,7 +16,6 @@ #include "../CVCMIServer.h" #include "../../lib/CPlayerState.h" -#include "../../lib/NetPacks.h" #include "../../lib/pathfinder/CPathfinder.h" #include "../../lib/pathfinder/PathfinderOptions.h" diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index 6b18f90f8..a49e55be3 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -16,6 +16,8 @@ #include "../battles/BattleProcessor.h" #include "../../lib/battle/IBattleState.h" +#include "../../lib/mapObjects/CGObjectInstance.h" +#include "../../lib/networkPacks/PacksForServer.h" void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { diff --git a/server/queries/BattleQueries.h b/server/queries/BattleQueries.h index ca2077995..4d2fb10fe 100644 --- a/server/queries/BattleQueries.h +++ b/server/queries/BattleQueries.h @@ -10,8 +10,7 @@ #pragma once #include "CQuery.h" - -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN class IBattleInfo; diff --git a/server/queries/CQuery.cpp b/server/queries/CQuery.cpp index 406d0bee9..ad23dcc25 100644 --- a/server/queries/CQuery.cpp +++ b/server/queries/CQuery.cpp @@ -15,7 +15,7 @@ #include "../CGameHandler.h" #include "../../lib/serializer/Cast.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForServer.h" template std::string formatContainer(const Container & c, std::string delimeter = ", ", std::string opener = "(", std::string closer=")") diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index 55acbe223..e2561adcf 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -13,8 +13,24 @@ #include "QueriesProcessor.h" #include "../CGameHandler.h" #include "../../lib/mapObjects/MiscObjects.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/serializer/Cast.h" +struct GetEngagedHeroIds +{ + std::optional operator()(const ConstTransitivePtr & h) const + { + return h->id; + } + std::optional operator()(const ConstTransitivePtr & s) const + { + if(s->armyObj && s->armyObj->ID == Obj::HERO) + return s->armyObj->id; + return std::optional(); + } +}; + TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): CQuery(owner) { diff --git a/server/queries/MapQueries.h b/server/queries/MapQueries.h index 3db5889a3..5668dad02 100644 --- a/server/queries/MapQueries.h +++ b/server/queries/MapQueries.h @@ -10,8 +10,14 @@ #pragma once #include "CQuery.h" +#include "../../lib/networkPacks/PacksForClient.h" + +VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CGObjectInstance; +class int3; +VCMI_LIB_NAMESPACE_END -#include "../../lib/NetPacks.h" class TurnTimerHandler; From f63c2ac5db1d8c43565b9ba6eac356cc0e5c4f11 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 23 Oct 2023 14:55:09 +0200 Subject: [PATCH 0924/1248] CI step "Ensure LF line endings": No need to ignore Android files Co-authored-by: Ivan Savenko --- .github/workflows/github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 2ffefd71b..8f9567f9a 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -148,7 +148,7 @@ jobs: - name: Ensure LF line endings if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | - find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./android -prune \ + find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune \ -o -path ./osx -prune -o -path ./test/googletest -prune -o -type f \ -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' \ -not -name '*.wav' -not -name '*.ico' -print0 | { ! xargs -0 grep -l -z -P '\r\n'; } From ae92bdfb5179eaa564b6bfce0ddcf1ae59584626 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 16:05:38 +0300 Subject: [PATCH 0925/1248] Fix Lua and test building --- scripting/lua/api/ServerCb.cpp | 2 +- scripting/lua/api/netpacks/BattleStackMoved.h | 2 ++ scripting/lua/api/netpacks/BattleUnitsChanged.h | 2 ++ scripting/lua/api/netpacks/SetResources.h | 2 ++ test/battle/CBattleInfoCallbackTest.cpp | 2 -- test/battle/CHealthTest.cpp | 1 - test/events/ApplyDamageTest.cpp | 2 +- test/game/CGameStateTest.cpp | 3 ++- test/mock/BattleFake.h | 1 - test/netpacks/EntitiesChangedTest.cpp | 1 + test/netpacks/NetPackFixture.h | 1 - test/scripting/LuaSpellEffectAPITest.cpp | 2 +- test/scripting/ScriptFixture.h | 1 - test/spells/AbilityCasterTest.cpp | 1 - test/spells/TargetConditionTest.cpp | 1 - test/spells/effects/EffectFixture.cpp | 2 +- test/spells/effects/EffectFixture.h | 1 - test/spells/targetConditions/TargetConditionItemFixture.h | 1 - 18 files changed, 13 insertions(+), 15 deletions(-) diff --git a/scripting/lua/api/ServerCb.cpp b/scripting/lua/api/ServerCb.cpp index 1d0f9d501..aa257c6e9 100644 --- a/scripting/lua/api/ServerCb.cpp +++ b/scripting/lua/api/ServerCb.cpp @@ -14,7 +14,7 @@ #include "Registry.h" #include "../LuaStack.h" -#include "../../../lib/NetPacks.h" +#include "../../../lib/networkPacks/PacksForClientBattle.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/scripting/lua/api/netpacks/BattleStackMoved.h b/scripting/lua/api/netpacks/BattleStackMoved.h index 3926a737e..a0fc9a9e4 100644 --- a/scripting/lua/api/netpacks/BattleStackMoved.h +++ b/scripting/lua/api/netpacks/BattleStackMoved.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/BattleUnitsChanged.h b/scripting/lua/api/netpacks/BattleUnitsChanged.h index 2344076e3..83837f814 100644 --- a/scripting/lua/api/netpacks/BattleUnitsChanged.h +++ b/scripting/lua/api/netpacks/BattleUnitsChanged.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClientBattle.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/scripting/lua/api/netpacks/SetResources.h b/scripting/lua/api/netpacks/SetResources.h index d58908240..4b35ef6f6 100644 --- a/scripting/lua/api/netpacks/SetResources.h +++ b/scripting/lua/api/netpacks/SetResources.h @@ -12,6 +12,8 @@ #include "PackForClient.h" +#include "../../../../lib/networkPacks/PacksForClient.h" + VCMI_LIB_NAMESPACE_BEGIN namespace scripting diff --git a/test/battle/CBattleInfoCallbackTest.cpp b/test/battle/CBattleInfoCallbackTest.cpp index 391401b05..5cef1f907 100644 --- a/test/battle/CBattleInfoCallbackTest.cpp +++ b/test/battle/CBattleInfoCallbackTest.cpp @@ -14,8 +14,6 @@ #include -#include "../../lib/NetPacksBase.h" - #include "mock/mock_BonusBearer.h" #include "mock/mock_battle_IBattleState.h" #include "mock/mock_battle_Unit.h" diff --git a/test/battle/CHealthTest.cpp b/test/battle/CHealthTest.cpp index 6ea80a7d9..ac32cee5e 100644 --- a/test/battle/CHealthTest.cpp +++ b/test/battle/CHealthTest.cpp @@ -12,7 +12,6 @@ #include "mock/mock_battle_Unit.h" #include "mock/mock_BonusBearer.h" #include "../../lib/battle/CUnitState.h" -#include "../../lib/NetPacksBase.h" using namespace testing; using namespace battle; diff --git a/test/events/ApplyDamageTest.cpp b/test/events/ApplyDamageTest.cpp index 7df90298b..b396ccd44 100644 --- a/test/events/ApplyDamageTest.cpp +++ b/test/events/ApplyDamageTest.cpp @@ -12,7 +12,7 @@ #include #include "../../lib/events/ApplyDamage.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../mock/mock_Environment.h" #include "../mock/mock_battle_Unit.h" diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 0141bc2a7..42e595eec 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -16,7 +16,8 @@ #include "../../lib/VCMIDirs.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClient.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" diff --git a/test/mock/BattleFake.h b/test/mock/BattleFake.h index cb265e008..8ce30c38f 100644 --- a/test/mock/BattleFake.h +++ b/test/mock/BattleFake.h @@ -20,7 +20,6 @@ #endif #include "../../lib/JsonNode.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/battle/CBattleInfoCallback.h" namespace test diff --git a/test/netpacks/EntitiesChangedTest.cpp b/test/netpacks/EntitiesChangedTest.cpp index adb6b0647..e4423176a 100644 --- a/test/netpacks/EntitiesChangedTest.cpp +++ b/test/netpacks/EntitiesChangedTest.cpp @@ -11,6 +11,7 @@ #include "StdInc.h" #include "NetPackFixture.h" +#include "../../lib/networkPacks/PacksForClient.h" namespace test { diff --git a/test/netpacks/NetPackFixture.h b/test/netpacks/NetPackFixture.h index 2e995cba1..176d37e21 100644 --- a/test/netpacks/NetPackFixture.h +++ b/test/netpacks/NetPackFixture.h @@ -10,7 +10,6 @@ #pragma once -#include "../../lib/NetPacks.h" #include "../../lib/gameState/CGameState.h" namespace test diff --git a/test/scripting/LuaSpellEffectAPITest.cpp b/test/scripting/LuaSpellEffectAPITest.cpp index 0a477f41b..8040b99df 100644 --- a/test/scripting/LuaSpellEffectAPITest.cpp +++ b/test/scripting/LuaSpellEffectAPITest.cpp @@ -11,7 +11,7 @@ #include "ScriptFixture.h" -#include "../../lib/NetPacks.h" +#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../mock/mock_ServerCallback.h" diff --git a/test/scripting/ScriptFixture.h b/test/scripting/ScriptFixture.h index 4d7b780bc..ea9f0cbe3 100644 --- a/test/scripting/ScriptFixture.h +++ b/test/scripting/ScriptFixture.h @@ -16,7 +16,6 @@ #include "../../lib/JsonNode.h" #include "../../lib/ScriptHandler.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/battle/CBattleInfoCallback.h" #include "../../lib/bonuses/Bonus.h" diff --git a/test/spells/AbilityCasterTest.cpp b/test/spells/AbilityCasterTest.cpp index 1172c4ca3..02c3a9e44 100644 --- a/test/spells/AbilityCasterTest.cpp +++ b/test/spells/AbilityCasterTest.cpp @@ -13,7 +13,6 @@ #include "mock/mock_BonusBearer.h" #include "mock/mock_spells_Spell.h" -#include "../../lib/NetPacksBase.h" #include "../../lib/spells/AbilityCaster.h" namespace test diff --git a/test/spells/TargetConditionTest.cpp b/test/spells/TargetConditionTest.cpp index 88abf7fcd..2f6f604fb 100644 --- a/test/spells/TargetConditionTest.cpp +++ b/test/spells/TargetConditionTest.cpp @@ -11,7 +11,6 @@ #include -#include "../../lib/NetPacksBase.h" #include "../../lib/spells/TargetCondition.h" #include "../../lib/serializer/JsonDeserializer.h" diff --git a/test/spells/effects/EffectFixture.cpp b/test/spells/effects/EffectFixture.cpp index 0395700aa..2b122d85e 100644 --- a/test/spells/effects/EffectFixture.cpp +++ b/test/spells/effects/EffectFixture.cpp @@ -13,7 +13,7 @@ #include -#include "../../../lib/NetPacks.h" +#include "../../../lib/networkPacks/PacksForClientBattle.h" #include "../../../lib/serializer/JsonDeserializer.h" diff --git a/test/spells/effects/EffectFixture.h b/test/spells/effects/EffectFixture.h index bc07ae1fa..dde8e9a2d 100644 --- a/test/spells/effects/EffectFixture.h +++ b/test/spells/effects/EffectFixture.h @@ -34,7 +34,6 @@ #include "../../../lib/JsonNode.h" -#include "../../../lib/NetPacksBase.h" #include "../../../lib/battle/CBattleInfoCallback.h" namespace battle diff --git a/test/spells/targetConditions/TargetConditionItemFixture.h b/test/spells/targetConditions/TargetConditionItemFixture.h index 060731fb5..67df578a4 100644 --- a/test/spells/targetConditions/TargetConditionItemFixture.h +++ b/test/spells/targetConditions/TargetConditionItemFixture.h @@ -12,7 +12,6 @@ #include -#include "../../../lib/NetPacksBase.h" #include "../../../lib/spells/TargetCondition.h" From 941181b8b9ebe8b2ddc0e1152277d41cdd0d1b60 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 23 Oct 2023 13:27:42 +0000 Subject: [PATCH 0926/1248] Convert XML files in android/ from CRLF to LF, exclude .bat files from CI check --- .github/workflows/github.yml | 8 ++++---- .../res/drawable/compat_toolbar_shadow.xml | 6 +++--- .../drawable/recycler_divider_drawable.xml | 12 +++++------ .../vcmi-app/src/main/res/menu/menu_mods.xml | 16 +++++++-------- .../vcmi-app/src/main/res/values/colors.xml | 18 ++++++++--------- .../vcmi-app/src/main/res/values/dimen.xml | 20 +++++++++---------- 6 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 8f9567f9a..cadab3783 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -148,10 +148,10 @@ jobs: - name: Ensure LF line endings if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | - find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune \ - -o -path ./osx -prune -o -path ./test/googletest -prune -o -type f \ - -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' \ - -not -name '*.wav' -not -name '*.ico' -print0 | { ! xargs -0 grep -l -z -P '\r\n'; } + find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -prune -o -path ./test/googletest \ + -o -path ./osx -prune -o -type f \ + -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ + { ! xargs -0 grep -l -z -P '\r\n'; } - name: Validate JSON # the Python yaml module doesn't seem to work on mac-arm diff --git a/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml b/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml index 3ed263fd7..5f44696a5 100644 --- a/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml +++ b/android/vcmi-app/src/main/res/drawable/compat_toolbar_shadow.xml @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml b/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml index 579b3ebc5..2deccc9ef 100644 --- a/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml +++ b/android/vcmi-app/src/main/res/drawable/recycler_divider_drawable.xml @@ -1,7 +1,7 @@ - - - - + + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/menu/menu_mods.xml b/android/vcmi-app/src/main/res/menu/menu_mods.xml index aa713155b..b50094a7f 100644 --- a/android/vcmi-app/src/main/res/menu/menu_mods.xml +++ b/android/vcmi-app/src/main/res/menu/menu_mods.xml @@ -1,9 +1,9 @@ - - - + + + \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/values/colors.xml b/android/vcmi-app/src/main/res/values/colors.xml index 91c8286d4..764ef76ea 100644 --- a/android/vcmi-app/src/main/res/values/colors.xml +++ b/android/vcmi-app/src/main/res/values/colors.xml @@ -1,10 +1,10 @@ - - - #FFFFFF - #AAAAAA - #2C332C - #1F221F - #BBBB55 - @color/accent - #00000000 + + + #FFFFFF + #AAAAAA + #2C332C + #1F221F + #BBBB55 + @color/accent + #00000000 \ No newline at end of file diff --git a/android/vcmi-app/src/main/res/values/dimen.xml b/android/vcmi-app/src/main/res/values/dimen.xml index ca9e1b63d..cd9fe2b7c 100644 --- a/android/vcmi-app/src/main/res/values/dimen.xml +++ b/android/vcmi-app/src/main/res/values/dimen.xml @@ -1,11 +1,11 @@ - - - 16dp - - 80dp - 48dp - - 14sp - 18sp - 22sp + + + 16dp + + 80dp + 48dp + + 14sp + 18sp + 22sp \ No newline at end of file From 5523f05284633f8a542e99bf62e0ddca7bb4af10 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 16:38:05 +0300 Subject: [PATCH 0927/1248] Moved ArtifactLocation to a separate file --- client/LobbyClientNetPackVisitors.h | 2 + client/lobby/CLobbyScreen.cpp | 2 +- client/lobby/OptionsTab.cpp | 2 +- client/widgets/CArtifactHolder.cpp | 2 +- client/widgets/CArtifactsOfHeroAltar.cpp | 2 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 2 +- client/widgets/CArtifactsOfHeroBase.cpp | 2 +- client/widgets/CArtifactsOfHeroKingdom.cpp | 2 +- client/widgets/CArtifactsOfHeroMain.cpp | 2 +- client/widgets/CGarrisonInt.cpp | 2 +- client/widgets/CWindowWithArtifacts.cpp | 2 +- client/windows/CCreatureWindow.cpp | 2 +- client/windows/CHeroWindow.cpp | 2 +- client/windows/CTradeWindow.cpp | 2 +- cmake_modules/VCMI_lib.cmake | 3 +- lib/CArtifactInstance.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- lib/networkPacks/ArtifactLocation.h | 76 +++++++++++++++++++ lib/networkPacks/NetPackVisitor.h | 2 +- lib/networkPacks/NetPacksBase.h | 74 +----------------- lib/networkPacks/NetPacksLib.cpp | 2 +- lib/networkPacks/PacksForClient.h | 7 +- lib/networkPacks/PacksForClientBattle.h | 6 +- .../{NetPacksLobby.h => PacksForLobby.h} | 2 +- lib/networkPacks/PacksForServer.h | 4 +- lib/registerTypes/RegisterTypes.h | 2 +- server/battles/BattleResultProcessor.h | 2 +- 27 files changed, 116 insertions(+), 96 deletions(-) create mode 100644 lib/networkPacks/ArtifactLocation.h rename lib/networkPacks/{NetPacksLobby.h => PacksForLobby.h} (99%) diff --git a/client/LobbyClientNetPackVisitors.h b/client/LobbyClientNetPackVisitors.h index 0c4af1aa0..7e19e614b 100644 --- a/client/LobbyClientNetPackVisitors.h +++ b/client/LobbyClientNetPackVisitors.h @@ -10,8 +10,10 @@ #pragma once #include "../lib/networkPacks/NetPackVisitor.h" +#include "../lib/networkPacks/PacksForLobby.h" class CClient; +class CLobbyScreen; VCMI_LIB_NAMESPACE_BEGIN class CGameState; VCMI_LIB_NAMESPACE_END diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index d8c739370..fc0333f37 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -25,7 +25,7 @@ #include "../../CCallback.h" #include "../CGameInfo.h" -#include "../../lib/networkPacks/NetPacksLobby.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index de17abd5b..72e3817f3 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -34,7 +34,7 @@ #include "../eventsSDL/InputHandler.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/networkPacks/NetPacksLobby.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CArtHandler.h" #include "../../lib/CTownHandler.h" diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 40f186332..a3b72d320 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -26,7 +26,7 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/CConfigHandler.h" void CArtPlace::setInternals(const CArtifactInstance * artInst) diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 672b4df60..737b05f9c 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -16,7 +16,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) : visibleArtSet(ArtBearer::ArtBearer::HERO) diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index 4d2e426f6..f95d5cf65 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -18,7 +18,7 @@ #include "../CPlayerInterface.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../CCallback.h" diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index d66772eb7..769da2258 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -22,7 +22,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroBase::CArtifactsOfHeroBase() : backpackPos(0), diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index c2043bd94..67e702b5f 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -15,7 +15,7 @@ #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector Backpack, std::shared_ptr leftScroll, std::shared_ptr rightScroll) diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index 2681f5a9c..46b5329ca 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -13,7 +13,7 @@ #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position) { diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 52ca984ec..3942da8a0 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -30,7 +30,7 @@ #include "../../lib/CCreatureHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/TextOperations.h" #include "../../lib/gameState/CGameState.h" diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index b3c2c2700..8c6d2b7c9 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -29,7 +29,7 @@ #include "../../lib/ArtifactUtils.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/CConfigHandler.h" void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 8c91103a5..bece268cf 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -33,7 +33,7 @@ #include "../../lib/GameSettings.h" #include "../../lib/CHeroHandler.h" #include "../../lib/gameState/CGameState.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" #include "../../lib/TextOperations.h" class CCreatureArtifactInstance; diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 890504063..7d05fe38a 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -39,7 +39,7 @@ #include "../lib/CHeroHandler.h" #include "../lib/CSkillHandler.h" #include "../lib/mapObjects/CGHeroInstance.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" void CHeroSwitcher::clickPressed(const Point & cursorPosition) { diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index c87db89b7..8a1d3672a 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -35,7 +35,7 @@ #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" CTradeWindow::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos), diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 998bf074b..1d7ac1aa2 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -476,16 +476,17 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/ModScope.h ${MAIN_LIB_DIR}/modding/ModUtility.h + ${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h ${MAIN_LIB_DIR}/networkPacks/Component.h ${MAIN_LIB_DIR}/networkPacks/EInfoWindowMode.h ${MAIN_LIB_DIR}/networkPacks/EntityChanges.h ${MAIN_LIB_DIR}/networkPacks/EOpenWindowMode.h ${MAIN_LIB_DIR}/networkPacks/NetPacksBase.h - ${MAIN_LIB_DIR}/networkPacks/NetPacksLobby.h ${MAIN_LIB_DIR}/networkPacks/NetPackVisitor.h ${MAIN_LIB_DIR}/networkPacks/PacksForClient.h ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h + ${MAIN_LIB_DIR}/networkPacks/PacksForLobby.h ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h ${MAIN_LIB_DIR}/networkPacks/StackLocation.h diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index 754831590..d80373251 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -13,7 +13,7 @@ #include "ArtifactUtils.h" #include "CArtHandler.h" -#include "networkPacks/NetPacksBase.h" +#include "networkPacks/ArtifactLocation.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 8152ed991..800dc4d55 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -37,7 +37,7 @@ #include "../mapObjects/ObjectTemplate.h" #include "../modding/ModScope.h" #include "../networkPacks/Component.h" -#include "../networkPacks/NetPacksBase.h" +#include "../networkPacks/ArtifactLocation.h" #include "../spells/CSpellHandler.h" #include diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h new file mode 100644 index 000000000..777a1fdf5 --- /dev/null +++ b/lib/networkPacks/ArtifactLocation.h @@ -0,0 +1,76 @@ +/* + * ArtifactLocation.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 "../ConstTransitivePtr.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class CGHeroInstance; +class CStackInstance; +class CArmedInstance; +class CArtifactSet; +class CBonusSystemNode; +struct ArtSlotInfo; + +using TArtHolder = std::variant, ConstTransitivePtr>; + +struct ArtifactLocation +{ + TArtHolder artHolder;//TODO: identify holder by id + ArtifactPosition slot = ArtifactPosition::PRE_FIRST; + + ArtifactLocation() + : artHolder(ConstTransitivePtr()) + { + } + template + ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) + : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! + , slot(Slot) + { + } + ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) + : artHolder(std::move(std::move(ArtHolder))) + , slot(Slot) + { + } + + template + bool isHolder(const T *t) const + { + if(auto ptrToT = std::get>(artHolder)) + { + return ptrToT == t; + } + return false; + } + + DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) + + DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner + DLL_LINKAGE PlayerColor owningPlayer() const; + DLL_LINKAGE CArtifactSet *getHolderArtSet(); + DLL_LINKAGE CBonusSystemNode *getHolderNode(); + DLL_LINKAGE CArtifactSet *getHolderArtSet() const; + DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; + + DLL_LINKAGE const CArtifactInstance *getArt() const; + DLL_LINKAGE CArtifactInstance *getArt(); + DLL_LINKAGE const ArtSlotInfo *getSlot() const; + template void serialize(Handler &h, const int version) + { + h & artHolder; + h & slot; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 73b090b4a..08f7c13e0 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -12,7 +12,7 @@ #include "PacksForClient.h" #include "PacksForClientBattle.h" #include "PacksForServer.h" -#include "NetPacksLobby.h" +#include "PacksForLobby.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/networkPacks/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h index 295594c46..dc22af57e 100644 --- a/lib/networkPacks/NetPacksBase.h +++ b/lib/networkPacks/NetPacksBase.h @@ -9,26 +9,12 @@ */ #pragma once -#include "../ConstTransitivePtr.h" -#include "../GameConstants.h" - -class CClient; -class CGameHandler; -class CLobbyScreen; -class CServerHandler; -class CVCMIServer; +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN class CGameState; class CConnection; -class CStackBasicDescriptor; -class CGHeroInstance; -class CStackInstance; -class CArmedInstance; -class CArtifactSet; -class CBonusSystemNode; -struct ArtSlotInfo; class ICPackVisitor; @@ -65,7 +51,7 @@ protected: struct DLL_LINKAGE CPackForClient : public CPack { protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; + void visitBasic(ICPackVisitor & cpackVisitor) override; }; struct DLL_LINKAGE Query : public CPackForClient @@ -85,7 +71,7 @@ struct DLL_LINKAGE CPackForServer : public CPack } protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; + void visitBasic(ICPackVisitor & cpackVisitor) override; }; struct DLL_LINKAGE CPackForLobby : public CPack @@ -93,59 +79,7 @@ struct DLL_LINKAGE CPackForLobby : public CPack virtual bool isForServer() const; protected: - virtual void visitBasic(ICPackVisitor & cpackVisitor) override; -}; - -using TArtHolder = std::variant, ConstTransitivePtr>; - -struct ArtifactLocation -{ - TArtHolder artHolder;//TODO: identify holder by id - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; - - ArtifactLocation() - : artHolder(ConstTransitivePtr()) - { - } - template - ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) - : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) - : artHolder(std::move(std::move(ArtHolder))) - , slot(Slot) - { - } - - template - bool isHolder(const T *t) const - { - if(auto ptrToT = std::get>(artHolder)) - { - return ptrToT == t; - } - return false; - } - - DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) - - DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner - DLL_LINKAGE PlayerColor owningPlayer() const; - DLL_LINKAGE CArtifactSet *getHolderArtSet(); - DLL_LINKAGE CBonusSystemNode *getHolderNode(); - DLL_LINKAGE CArtifactSet *getHolderArtSet() const; - DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; - - DLL_LINKAGE const CArtifactInstance *getArt() const; - DLL_LINKAGE CArtifactInstance *getArt(); - DLL_LINKAGE const ArtSlotInfo *getSlot() const; - template void serialize(Handler &h, const int version) - { - h & artHolder; - h & slot; - } + void visitBasic(ICPackVisitor & cpackVisitor) override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 508379a18..251c39180 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -13,7 +13,7 @@ #include "PacksForClientBattle.h" #include "PacksForServer.h" #include "StackLocation.h" -#include "NetPacksLobby.h" +#include "PacksForLobby.h" #include "NetPackVisitor.h" #include "CGeneralTextHandler.h" #include "CArtHandler.h" diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index b37551cd7..31bc4752e 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1,5 +1,5 @@ /* - * NetPacks.h, part of VCMI engine + * PacksForClient.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,11 +9,12 @@ */ #pragma once -#include "NetPacksBase.h" +#include "ArtifactLocation.h" #include "Component.h" -#include "EOpenWindowMode.h" #include "EInfoWindowMode.h" +#include "EOpenWindowMode.h" #include "EntityChanges.h" +#include "NetPacksBase.h" #include "../CCreatureSet.h" #include "../MetaString.h" diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index 4e87f8a39..d52ea6483 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -1,5 +1,5 @@ /* - * NetPacks.h, part of VCMI engine + * PacksForClientBattle.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -14,8 +14,12 @@ #include "../MetaString.h" #include "../battle/BattleAction.h" +class CClient; + VCMI_LIB_NAMESPACE_BEGIN +class CGHeroInstance; +class CArmedInstance; class IBattleState; class BattleInfo; diff --git a/lib/networkPacks/NetPacksLobby.h b/lib/networkPacks/PacksForLobby.h similarity index 99% rename from lib/networkPacks/NetPacksLobby.h rename to lib/networkPacks/PacksForLobby.h index 4e819d98f..ef55fe054 100644 --- a/lib/networkPacks/NetPacksLobby.h +++ b/lib/networkPacks/PacksForLobby.h @@ -1,5 +1,5 @@ /* - * NetPacksLobby.h, part of VCMI engine + * PacksForLobby.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index d71681450..9911d1ba8 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -1,5 +1,5 @@ /* - * NetPacks.h, part of VCMI engine + * PacksForServer.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -9,7 +9,9 @@ */ #pragma once +#include "ArtifactLocation.h" #include "NetPacksBase.h" + #include "../int3.h" #include "../battle/BattleAction.h" diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 4f375148f..7f5c086dc 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -12,7 +12,7 @@ #include "../networkPacks/PacksForClient.h" #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/PacksForServer.h" -#include "../networkPacks/NetPacksLobby.h" +#include "../networkPacks/PacksForLobby.h" #include "../VCMI_Lib.h" #include "../CArtHandler.h" #include "../CCreatureSet.h" diff --git a/server/battles/BattleResultProcessor.h b/server/battles/BattleResultProcessor.h index 88d4bb11a..7616c1775 100644 --- a/server/battles/BattleResultProcessor.h +++ b/server/battles/BattleResultProcessor.h @@ -11,7 +11,7 @@ #include "../../lib/GameConstants.h" #include "../../lib/networkPacks/StackLocation.h" -#include "../../lib/networkPacks/NetPacksBase.h" +#include "../../lib/networkPacks/ArtifactLocation.h" VCMI_LIB_NAMESPACE_BEGIN struct SideInBattle; From 91e202e0e34d780dc5dd8356d1e5366bf7bd7d96 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 19:10:24 +0300 Subject: [PATCH 0928/1248] Ignore case when checking whether path is white-listed. Fixes data import with H3 data that has different case in directory names --- .../vcmi/vcmi/settings/CopyDataController.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java index 9acf5e0a2..9b39f6e78 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java @@ -127,9 +127,21 @@ public class CopyDataController extends LauncherSettingController for (DocumentFile child : sourceDir.listFiles()) { - if (allowed != null && !allowed.contains(child.getName())) + if (allowed != null) { - continue; + boolean fileAllowed = false; + + for (String str : allowed) + { + if (str.equalsIgnoreCase(child.getName())) + { + fileAllowed = true; + break; + } + } + + if (fileAllowed) + continue; } File exported = new File(targetDir, child.getName()); From 7dfdee2db80649440802566c53cc5d7a18a9bed5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 19:17:58 +0300 Subject: [PATCH 0929/1248] Check whether H3 data has been imported using most common spellings of data directory --- android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java index 9986f890e..b3fc1b9db 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/Storage.java @@ -22,7 +22,9 @@ public class Storage public static boolean testH3DataFolder(final File baseDir) { final File testH3Data = new File(baseDir, "Data"); - return testH3Data.exists(); + final File testH3data = new File(baseDir, "data"); + final File testH3DATA = new File(baseDir, "DATA"); + return testH3Data.exists() || testH3data.exists() || testH3DATA.exists(); } public static String getH3DataFolder(Context context){ From 2315ea84ec7fe7eb061af0c696f0ede61db861bb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 22:18:21 +0300 Subject: [PATCH 0930/1248] Invert condition --- .../src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java index 9b39f6e78..2d5396d88 100644 --- a/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java +++ b/android/vcmi-app/src/main/java/eu/vcmi/vcmi/settings/CopyDataController.java @@ -140,7 +140,7 @@ public class CopyDataController extends LauncherSettingController } } - if (fileAllowed) + if (!fileAllowed) continue; } From 77780e0de57ff2cc76fdbf3c04dfe2ee979c465a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 23 Oct 2023 22:28:36 +0300 Subject: [PATCH 0931/1248] Fix crash on accessing "any" spellschool via bonus system --- lib/constants/EntityIdentifiers.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index fb63f42ef..95d56a267 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -420,6 +420,9 @@ si32 SpellSchool::decode(const std::string & identifier) std::string SpellSchool::encode(const si32 index) { + if (index == ANY.getNum()) + return "any"; + return SpellConfig::SCHOOL[index].jsonName; } From ab7caa07772b2cea9a0342442617c4b258935a88 Mon Sep 17 00:00:00 2001 From: krs Date: Mon, 23 Oct 2023 22:17:57 +0300 Subject: [PATCH 0932/1248] Removed space after Torosar hero name --- config/heroes/tower.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/heroes/tower.json b/config/heroes/tower.json index 41380fc1c..0fecfc54b 100644 --- a/config/heroes/tower.json +++ b/config/heroes/tower.json @@ -67,7 +67,7 @@ } } }, - "torosar ": + "torosar": { "index": 36, "class" : "alchemist", From 5cbf5031ea35d4300acb5daa2f8572ace34ea2b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 24 Oct 2023 01:27:52 +0300 Subject: [PATCH 0933/1248] move SetStackEffect to a separate file --- AI/BattleAI/StackWithBonuses.cpp | 1 + cmake_modules/VCMI_lib.cmake | 1 + lib/networkPacks/NetPackVisitor.h | 1 + lib/networkPacks/NetPacksLib.cpp | 1 + lib/networkPacks/PacksForClientBattle.h | 22 ------------- lib/networkPacks/SetStackEffect.h | 42 ++++++++++++++++++++++++ lib/registerTypes/RegisterTypes.h | 1 + lib/spells/BattleSpellMechanics.cpp | 1 + lib/spells/effects/Clone.cpp | 1 + lib/spells/effects/Dispel.cpp | 1 + lib/spells/effects/Timed.cpp | 1 + server/ServerSpellCastEnvironment.cpp | 1 + server/battles/BattleActionProcessor.cpp | 1 + test/game/CGameStateTest.cpp | 1 + test/spells/effects/EffectFixture.cpp | 1 + 15 files changed, 55 insertions(+), 22 deletions(-) create mode 100644 lib/networkPacks/SetStackEffect.h diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 2de2fd498..7beabf514 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -15,6 +15,7 @@ #include "../../lib/CStack.h" #include "../../lib/ScriptHandler.h" #include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" #if SCRIPTING_ENABLED using scripting::Pool; diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 1d7ac1aa2..4654082d6 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -488,6 +488,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h ${MAIN_LIB_DIR}/networkPacks/PacksForLobby.h ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h + ${MAIN_LIB_DIR}/networkPacks/SetStackEffect.h ${MAIN_LIB_DIR}/networkPacks/StackLocation.h ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 08f7c13e0..72d8dce9d 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -13,6 +13,7 @@ #include "PacksForClientBattle.h" #include "PacksForServer.h" #include "PacksForLobby.h" +#include "SetStackEffect.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 251c39180..a15553cc5 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -14,6 +14,7 @@ #include "PacksForServer.h" #include "StackLocation.h" #include "PacksForLobby.h" +#include "SetStackEffect.h" #include "NetPackVisitor.h" #include "CGeneralTextHandler.h" #include "CArtHandler.h" diff --git a/lib/networkPacks/PacksForClientBattle.h b/lib/networkPacks/PacksForClientBattle.h index d52ea6483..ad0878fe9 100644 --- a/lib/networkPacks/PacksForClientBattle.h +++ b/lib/networkPacks/PacksForClientBattle.h @@ -398,28 +398,6 @@ struct DLL_LINKAGE BattleSpellCast : public CPackForClient } }; -struct DLL_LINKAGE SetStackEffect : public CPackForClient -{ - void applyGs(CGameState * gs); - void applyBattle(IBattleState * battleState); - - BattleID battleID = BattleID::NONE; - std::vector>> toAdd; - std::vector>> toUpdate; - std::vector>> toRemove; - - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & battleID; - h & toAdd; - h & toUpdate; - h & toRemove; - assert(battleID != BattleID::NONE); - } -}; - struct DLL_LINKAGE StacksInjured : public CPackForClient { void applyGs(CGameState * gs); diff --git a/lib/networkPacks/SetStackEffect.h b/lib/networkPacks/SetStackEffect.h new file mode 100644 index 000000000..f53b4cfe2 --- /dev/null +++ b/lib/networkPacks/SetStackEffect.h @@ -0,0 +1,42 @@ +/* + * SetStackEffect.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 "NetPacksBase.h" + +#include "../bonuses/Bonus.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IBattleState; + +struct DLL_LINKAGE SetStackEffect : public CPackForClient +{ + void applyGs(CGameState * gs); + void applyBattle(IBattleState * battleState); + + BattleID battleID = BattleID::NONE; + std::vector>> toAdd; + std::vector>> toUpdate; + std::vector>> toRemove; + + void visitTyped(ICPackVisitor & visitor) override; + + template void serialize(Handler & h, const int version) + { + h & battleID; + h & toAdd; + h & toUpdate; + h & toRemove; + assert(battleID != BattleID::NONE); + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 7f5c086dc..907361a40 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -13,6 +13,7 @@ #include "../networkPacks/PacksForClientBattle.h" #include "../networkPacks/PacksForServer.h" #include "../networkPacks/PacksForLobby.h" +#include "../networkPacks/SetStackEffect.h" #include "../VCMI_Lib.h" #include "../CArtHandler.h" #include "../CCreatureSet.h" diff --git a/lib/spells/BattleSpellMechanics.cpp b/lib/spells/BattleSpellMechanics.cpp index 36ce8dd96..fc333fc93 100644 --- a/lib/spells/BattleSpellMechanics.cpp +++ b/lib/spells/BattleSpellMechanics.cpp @@ -17,6 +17,7 @@ #include "../battle/IBattleState.h" #include "../battle/CBattleInfoCallback.h" #include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/SetStackEffect.h" #include "../CStack.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Clone.cpp b/lib/spells/effects/Clone.cpp index d54d33caf..491fe5023 100644 --- a/lib/spells/effects/Clone.cpp +++ b/lib/spells/effects/Clone.cpp @@ -16,6 +16,7 @@ #include "../../battle/IBattleState.h" #include "../../battle/CUnitState.h" #include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index c2765cc40..3fb418acd 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -22,6 +22,7 @@ #include "../../battle/Unit.h" #include "../../bonuses/BonusList.h" #include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/spells/effects/Timed.cpp b/lib/spells/effects/Timed.cpp index 33d8e6cdf..e006d37a4 100644 --- a/lib/spells/effects/Timed.cpp +++ b/lib/spells/effects/Timed.cpp @@ -19,6 +19,7 @@ #include "../../battle/Unit.h" #include "../../mapObjects/CGHeroInstance.h" #include "../../networkPacks/PacksForClientBattle.h" +#include "../../networkPacks/SetStackEffect.h" #include "../../serializer/JsonSerializeFormat.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/server/ServerSpellCastEnvironment.cpp b/server/ServerSpellCastEnvironment.cpp index ae57ebbff..b89b9ad8f 100644 --- a/server/ServerSpellCastEnvironment.cpp +++ b/server/ServerSpellCastEnvironment.cpp @@ -16,6 +16,7 @@ #include "../lib/gameState/CGameState.h" #include "../lib/networkPacks/PacksForClientBattle.h" +#include "../lib/networkPacks/SetStackEffect.h" ///ServerSpellCastEnvironment ServerSpellCastEnvironment::ServerSpellCastEnvironment(CGameHandler * gh) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 293b119e3..80242a49c 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -23,6 +23,7 @@ #include "../../lib/battle/BattleAction.h" #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" #include "../../lib/spells/AbilityCaster.h" #include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/ISpellMechanics.h" diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 42e595eec..02222e712 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -18,6 +18,7 @@ #include "../../lib/gameState/CGameState.h" #include "../../lib/networkPacks/PacksForClient.h" #include "../../lib/networkPacks/PacksForClientBattle.h" +#include "../../lib/networkPacks/SetStackEffect.h" #include "../../lib/StartInfo.h" #include "../../lib/TerrainHandler.h" diff --git a/test/spells/effects/EffectFixture.cpp b/test/spells/effects/EffectFixture.cpp index 2b122d85e..88d1ed2f4 100644 --- a/test/spells/effects/EffectFixture.cpp +++ b/test/spells/effects/EffectFixture.cpp @@ -14,6 +14,7 @@ #include #include "../../../lib/networkPacks/PacksForClientBattle.h" +#include "../../../lib/networkPacks/SetStackEffect.h" #include "../../../lib/serializer/JsonDeserializer.h" From c202f58a1519138dfff16ffa4344fe59b63c58b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 24 Oct 2023 13:59:06 +0300 Subject: [PATCH 0934/1248] Account for conflicts (and reverse-conflicts) when loading mods --- lib/modding/CModHandler.cpp | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 0216941b0..983e80130 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -86,17 +86,28 @@ std::vector CModHandler::validateAndSortDependencies(std::vector resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements // Mod is resolved if it has not dependencies or all its dependencies are already resolved - auto isResolved = [&](const CModInfo & mod) -> CModInfo::EValidationStatus + auto isResolved = [&](const CModInfo & mod) -> bool { if(mod.dependencies.size() > resolvedModIDs.size()) - return CModInfo::PENDING; + return false; for(const TModID & dependency : mod.dependencies) { if(!vstd::contains(resolvedModIDs, dependency)) - return CModInfo::PENDING; + return false; } - return CModInfo::PASSED; + + for(const TModID & conflict : mod.conflicts) + { + if(vstd::contains(resolvedModIDs, conflict)) + return false; + } + for(const TModID & reverseConflict : resolvedModIDs) + { + if (vstd::contains(allMods.at(reverseConflict).conflicts, mod.identifier)) + return false; + } + return true; }; while(true) @@ -104,7 +115,7 @@ std::vector CModHandler::validateAndSortDependencies(std::vector resolvedOnCurrentTreeLevel; for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree { - if(isResolved(allMods.at(*it)) == CModInfo::PASSED) + if(isResolved(allMods.at(*it))) { resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node childs will be resolved on the next iteration sortedValidMods.push_back(*it); @@ -131,6 +142,16 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' has been disabled: dependency '%s' is missing.", brokenMod.getVerificationInfo().name, dependency); } + for(const TModID & conflict : brokenMod.conflicts) + { + if(vstd::contains(resolvedModIDs, conflict)) + logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, conflict); + } + for(const TModID & reverseConflict : resolvedModIDs) + { + if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID)) + logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, reverseConflict); + } } return sortedValidMods; } From abb279f5b48e005ab5f2de644ba137c19fd90fa5 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:51:58 +0200 Subject: [PATCH 0935/1248] use config --- client/gui/InterfaceObjectConfigurable.cpp | 1 + client/lobby/RandomMapTab.cpp | 11 ++++------- client/lobby/RandomMapTab.h | 1 - 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index c9f9bcb36..935dc88da 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -540,6 +540,7 @@ std::shared_ptr InterfaceObjectConfigurable::buildTexture(const { auto result = std::make_shared(image, rect); result->playerColored(playerColor); + return result; } return std::make_shared(image, rect); } diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 8e67eb48e..7928320f9 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -403,16 +403,9 @@ TeamAlignments::TeamAlignments(RandomMapTab & randomMapTab) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; widget = std::make_shared(randomMapTab); - pos = widget->pos; - backgroundTexture = std::make_shared(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h)); - backgroundTexture->playerColored(PlayerColor(1)); - - std::swap(GH.createdObj.front()->children.end()[-1], GH.createdObj.front()->children.end()[-2]); // widget to top - updateShadow(); - center(); } @@ -432,6 +425,10 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): pos.w = variables["windowSize"]["x"].Integer() + totalPlayers * variables["cellMargin"]["x"].Integer(); pos.h = variables["windowSize"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); + variables["backgroundRect"]["x"].Integer() = 0; + variables["backgroundRect"]["y"].Integer() = 0; + variables["backgroundRect"]["w"].Integer() = pos.w; + variables["backgroundRect"]["h"].Integer() = pos.h; variables["okButtonPosition"]["x"].Integer() = variables["buttonsOffset"]["ok"]["x"].Integer(); variables["okButtonPosition"]["y"].Integer() = variables["buttonsOffset"]["ok"]["y"].Integer() + totalPlayers * variables["cellMargin"]["y"].Integer(); variables["cancelButtonPosition"]["x"].Integer() = variables["buttonsOffset"]["cancel"]["x"].Integer(); diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index 5c113b44c..a5d7b59ba 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -68,7 +68,6 @@ private: class TeamAlignments: public CWindowObject { std::shared_ptr widget; - std::shared_ptr backgroundTexture; public: TeamAlignments(RandomMapTab & randomMapTab); }; From e28d14d16d67708ca19ac9f1f0b9d7aad43797f3 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:56:30 +0200 Subject: [PATCH 0936/1248] not needed anymore --- client/lobby/RandomMapTab.h | 1 - 1 file changed, 1 deletion(-) diff --git a/client/lobby/RandomMapTab.h b/client/lobby/RandomMapTab.h index a5d7b59ba..37b72f4ac 100644 --- a/client/lobby/RandomMapTab.h +++ b/client/lobby/RandomMapTab.h @@ -27,7 +27,6 @@ class CLabel; class CLabelGroup; class CSlider; class CPicture; -class FilledTexturePlayerColored; class RandomMapTab : public InterfaceObjectConfigurable { From b6b75beb29025135f78180c14a6178631b2cb86a Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 24 Oct 2023 23:58:26 +0200 Subject: [PATCH 0937/1248] Fixes for map editor 1) fix owner serialization for hero placeholder 2) fix roads/rivers layout 3) fix lasso --- lib/gameState/CGameStateCampaign.cpp | 3 ++- lib/mapObjects/CGHeroInstance.cpp | 2 ++ mapeditor/inspector/inspector.cpp | 4 +++ mapeditor/mainwindow.ui | 12 +++++++-- mapeditor/mapview.cpp | 37 +++++++++++++++++++++++++--- 5 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 443aedf9f..462204cb7 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -336,7 +336,8 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vectorid = campaignHeroReplacement.heroPlaceholderId; - heroToPlace->tempOwner = heroPlaceholder->tempOwner; + if(heroPlaceholder->tempOwner.isValidPlayer()) + heroToPlace->tempOwner = heroPlaceholder->tempOwner; heroToPlace->pos = heroPlaceholder->pos; heroToPlace->type = VLC->heroh->objects[heroToPlace->subID]; heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front(); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index dc22f4ae5..5c8952beb 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -42,6 +42,8 @@ VCMI_LIB_NAMESPACE_BEGIN void CGHeroPlaceholder::serializeJsonOptions(JsonSerializeFormat & handler) { + serializeJsonOwner(handler); + bool isHeroType = heroType.has_value(); handler.serializeBool("placeholderType", isHeroType, false); diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 61a96bb83..b0b1e6261 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -123,6 +123,8 @@ void Initializer::initialize(CGHeroPlaceholder * o) { if(!o) return; + o->tempOwner = defaultPlayer; + if(!o->powerRank.has_value() && !o->heroType.has_value()) o->powerRank = 0; @@ -274,6 +276,8 @@ void Inspector::updateProperties(CGHeroPlaceholder * o) { if(!o) return; + addProperty("Owner", o->tempOwner, false); + bool type = false; if(o->heroType.has_value()) type = true; diff --git a/mapeditor/mainwindow.ui b/mapeditor/mainwindow.ui index 226dd3b39..bedb9c465 100644 --- a/mapeditor/mainwindow.ui +++ b/mapeditor/mainwindow.ui @@ -953,7 +953,11 @@ 0 - + + + 1 + + @@ -989,7 +993,11 @@ 0 - + + + 1 + + diff --git a/mapeditor/mapview.cpp b/mapeditor/mapview.cpp index 9171fd7e3..52cb9f373 100644 --- a/mapeditor/mapview.cpp +++ b/mapeditor/mapview.cpp @@ -89,8 +89,6 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) if(tile == tilePrev) //do not redraw return; - tilePrev = tile; - emit currentCoordinates(tile.x, tile.y); switch(selectionTool) @@ -208,7 +206,21 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) case MapView::SelectionTool::Lasso: if(mouseEvent->buttons() == Qt::LeftButton) { - sc->selectionTerrainView.select(tile); + for(auto i = tilePrev; i != tile;) + { + int length = std::numeric_limits::max(); + int3 dir; + for(auto & d : int3::getDirs()) + { + if(tile.dist2dSQ(i + d) < length) + { + dir = d; + length = tile.dist2dSQ(i + d); + } + } + i += dir; + sc->selectionTerrainView.select(i); + } sc->selectionTerrainView.draw(); } break; @@ -235,6 +247,8 @@ void MapView::mouseMoveEvent(QMouseEvent *mouseEvent) sc->selectionObjectsView.draw(); break; } + + tilePrev = tile; } void MapView::mousePressEvent(QMouseEvent *event) @@ -459,6 +473,23 @@ void MapView::mouseReleaseEvent(QMouseEvent *event) case MapView::SelectionTool::Lasso: { if(event->button() == Qt::RightButton) break; + + //connect with initial tile + for(auto i = tilePrev; i != tileStart;) + { + int length = std::numeric_limits::max(); + int3 dir; + for(auto & d : int3::getDirs()) + { + if(tileStart.dist2dSQ(i + d) < length) + { + dir = d; + length = tileStart.dist2dSQ(i + d); + } + } + i += dir; + sc->selectionTerrainView.select(i); + } //key: y position of tile //value.first: x position of left tile From 90f20b20d59a894021e219d0872faa53af3f36c1 Mon Sep 17 00:00:00 2001 From: krs Date: Wed, 25 Oct 2023 20:52:58 +0300 Subject: [PATCH 0938/1248] Added ai-description to docs --- docs/developers/ai-description.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 docs/developers/ai-description.md diff --git a/docs/developers/ai-description.md b/docs/developers/ai-description.md new file mode 100644 index 000000000..fcb62f000 --- /dev/null +++ b/docs/developers/ai-description.md @@ -0,0 +1,28 @@ +< [Documentation](../Readme.md) / AI Description - WIP + +... excrept from Slack chat with Danylchenko + +There are two types of AI: adventure and battle. + +**Adventure AIs** are responsible on moving heroes on map and developing towns +**Battle AIs** are responsible on actually fighting: moving stack on battlefield + +We have 3 battle AIs so far: BattleAI - strongest, StupidAI - for neutrals, should be simple so that expirienced players can abuse it. And Empty AI - should do nothing at all. If needed another battle AI can be introduced. + + +Each battle AI consist of a few classes, but the main class, kind of entry point usually has same name as package itself. In BattleAI it is BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by system when it is time to move your stack. The thing you use to interact with game and recieve gamestate is usually referencesi n code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf() can retrieve all units on battlefield matching some lambda condition. +sides in battle are represented by two CArmedInstance objects. CHeroInstance is subclass of CArmedInstance. Same can be said abot CGDwelling, CGMonster and so on. CArmedInstance contains a set of stacks. When battle starts these stacks are converted in battle stacks. Usually Battle AIs reference them using interface battle::Unit *. +Units have bonuses. Mostly everything about unit is configured in form of bonuses. Attack, defense, health, retalitation, shooter or not, initial count of shots and so on. +So when you call unit->getAttack() it summarize all these bonuses and return resulting value. + +Now important part is HypotheticBattle. It is used to evaluate some effect, action without changing real gamestate. First of all it is a wrapper around CPlayerSpecificCallback or anther HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate attack you can ask hypotheticbattle.getforupdate() and it wil lreturn you this CStackWithBonuses which you can safely change. + +Now about BattleAI. All possible attacks are measured using value I called damage reduce. It is how much damage enemy will loose after our attack. Also for negative effects we have our damage reduce. We get a difference and this value is used as attack score. + +AttackPossibility - one particular way to attack on battlefield. +PotentialTargets - a set of all AttackPossibility +BattleExchangeVariant - it is extension of AttackPossibility, a result of a set of units attacking each other fixed amount of turns according to turn order. Kind of oversimplified battle simulation. A set of units is restricted according to AttackPossibility which particular exchange extends. Exchanges can be waited (when stacks/units wait better time to attack) and non-waited (when stack acts right away). For non-waited exchanges the first attack score is taken from AttackPossibility (together with various effects like 2hex breath, shooters blocking and so on). All the other attacks are simplified, only respect retaliations. At the end we have a final score. + +BattleExchangeEvaluator - calculates all possible BattleExchangeVariant and select the best +BattleEvaluator - is top level logic layer also adds spellcasts and movement to unreachable targets +BattleAI itself handles all the rest and issue actual commands From d0cdd06383f31e805b765bb3dac5cd4d74c9c196 Mon Sep 17 00:00:00 2001 From: krs Date: Wed, 25 Oct 2023 22:17:21 +0300 Subject: [PATCH 0939/1248] Update docs/developers/ai-description.md Co-authored-by: Alexander Wilms --- docs/developers/ai-description.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/developers/ai-description.md b/docs/developers/ai-description.md index fcb62f000..61923be4c 100644 --- a/docs/developers/ai-description.md +++ b/docs/developers/ai-description.md @@ -1,21 +1,21 @@ < [Documentation](../Readme.md) / AI Description - WIP -... excrept from Slack chat with Danylchenko - There are two types of AI: adventure and battle. -**Adventure AIs** are responsible on moving heroes on map and developing towns -**Battle AIs** are responsible on actually fighting: moving stack on battlefield +**Adventure AIs** are responsible for moving heroes across the map and developing towns +**Battle AIs** are responsible for fighting, i.e. moving stacks on the battlefield -We have 3 battle AIs so far: BattleAI - strongest, StupidAI - for neutrals, should be simple so that expirienced players can abuse it. And Empty AI - should do nothing at all. If needed another battle AI can be introduced. +We have 3 battle AIs so far: +* BattleAI - strongest +* StupidAI - for neutrals, should be simple so that experienced players can abuse it +* Empty AI - should do nothing at all. If needed another battle AI can be introduced. +Each battle AI consist of a few classes, but the main class, kind of entry point usually has the same name as the package itself. In BattleAI it is the BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by the system when it's time to move your stack. The thing you use to interact with the game and receive the gamestate is usually referenced in the code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf(), which returns all units on the battlefield matching some lambda condition. +Each side in a battle is represented by an CArmedInstance object. CHeroInstance and CGDwelling, CGMonster and more are subclasses of CArmedInstance. CArmedInstance contains a set of stacks. When the battle starts, these stacks are converted to battle stacks. Usually Battle AIs reference them using the interface battle::Unit *. +Units have bonuses. Nearly everything aspect of a unit is configured in the form of bonuses. Attack, defense, health, retalitation, shooter or not, initial count of shots and so on. +When you call unit->getAttack() it summarizes all these bonuses and returns the resulting value. -Each battle AI consist of a few classes, but the main class, kind of entry point usually has same name as package itself. In BattleAI it is BattleAI class. It implements some battle specific interface, do not remember. Main method there is activeStack(battle::Unit* stack). It is invoked by system when it is time to move your stack. The thing you use to interact with game and recieve gamestate is usually referencesi n code as cb. CPlayerSpecificCallback it should be. It has a lot of methods and can do anything. For instance it has battleGetUnitsIf() can retrieve all units on battlefield matching some lambda condition. -sides in battle are represented by two CArmedInstance objects. CHeroInstance is subclass of CArmedInstance. Same can be said abot CGDwelling, CGMonster and so on. CArmedInstance contains a set of stacks. When battle starts these stacks are converted in battle stacks. Usually Battle AIs reference them using interface battle::Unit *. -Units have bonuses. Mostly everything about unit is configured in form of bonuses. Attack, defense, health, retalitation, shooter or not, initial count of shots and so on. -So when you call unit->getAttack() it summarize all these bonuses and return resulting value. - -Now important part is HypotheticBattle. It is used to evaluate some effect, action without changing real gamestate. First of all it is a wrapper around CPlayerSpecificCallback or anther HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate attack you can ask hypotheticbattle.getforupdate() and it wil lreturn you this CStackWithBonuses which you can safely change. +One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change. Now about BattleAI. All possible attacks are measured using value I called damage reduce. It is how much damage enemy will loose after our attack. Also for negative effects we have our damage reduce. We get a difference and this value is used as attack score. From 27c4a1fe15c44d72ec57efd705eac93ecb741ad8 Mon Sep 17 00:00:00 2001 From: krs Date: Wed, 25 Oct 2023 22:59:45 +0300 Subject: [PATCH 0940/1248] Update ai-description.md --- docs/developers/ai-description.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/docs/developers/ai-description.md b/docs/developers/ai-description.md index 61923be4c..72bc5d409 100644 --- a/docs/developers/ai-description.md +++ b/docs/developers/ai-description.md @@ -17,12 +17,18 @@ When you call unit->getAttack() it summarizes all these bonuses and returns the One important class is HypotheticBattle. It is used to evaluate the effects of an action without changing the actual gamestate. It is a wrapper around CPlayerSpecificCallback or another HypotheticBattle so it can provide you data, Internally it has a set of modified unit states and intercepts some calls to underlying callback and returns these internal states instead. These states in turn are wrappers around original units and contain modified bonuses (CStackWithBonuses). So if you need to emulate an attack you can call hypotheticbattle.getforupdate() and it will return the CStackWithBonuses which you can safely change. -Now about BattleAI. All possible attacks are measured using value I called damage reduce. It is how much damage enemy will loose after our attack. Also for negative effects we have our damage reduce. We get a difference and this value is used as attack score. +# BattleAI -AttackPossibility - one particular way to attack on battlefield. -PotentialTargets - a set of all AttackPossibility -BattleExchangeVariant - it is extension of AttackPossibility, a result of a set of units attacking each other fixed amount of turns according to turn order. Kind of oversimplified battle simulation. A set of units is restricted according to AttackPossibility which particular exchange extends. Exchanges can be waited (when stacks/units wait better time to attack) and non-waited (when stack acts right away). For non-waited exchanges the first attack score is taken from AttackPossibility (together with various effects like 2hex breath, shooters blocking and so on). All the other attacks are simplified, only respect retaliations. At the end we have a final score. +BattleAI's most important classes are the following: -BattleExchangeEvaluator - calculates all possible BattleExchangeVariant and select the best -BattleEvaluator - is top level logic layer also adds spellcasts and movement to unreachable targets -BattleAI itself handles all the rest and issue actual commands +- AttackPossibility - one particular way to attack on the battlefield. Each AttackPossibility instance has multiple ...DamageReduce attributes. These represent how much damage an enemy will lose after our attack. Effects can reduce this damage. We add them up and this value is used as attack score. + +- PotentialTargets - a set of all AttackPossibility instances + +- BattleExchangeVariant - it is an extension of AttackPossibility, a result of a set of units attacking each other for a fixed number of turns according to the turn order. It is kind of an oversimplified battle simulation. A set of units is restricted according to AttackPossibility which particular exchange extends. Exchanges can be waited (when stacks/units wait for a better time to attack) and non-waited (when stack acts right away). For non-waited exchanges the first attack score is taken from AttackPossibility (together with various effects like 2 hex breath, shooters blocking and so on). All the other attacks are simplified, only respect retaliations. At the end we have a final score. + +- BattleExchangeEvaluator - calculates all possible BattleExchangeVariants and selects the best + +- BattleEvaluator - is a top level logic layer which also adds spellcasts and movement to unreachable targets + +BattleAI itself handles all the rest and issues actual commands From 7d473317aceb6c4dd7044442066e4410a0e14bf4 Mon Sep 17 00:00:00 2001 From: krs Date: Thu, 26 Oct 2023 00:13:22 +0300 Subject: [PATCH 0941/1248] Renamed to AI.md --- docs/developers/{ai-description.md => AI.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/developers/{ai-description.md => AI.md} (100%) diff --git a/docs/developers/ai-description.md b/docs/developers/AI.md similarity index 100% rename from docs/developers/ai-description.md rename to docs/developers/AI.md From a817e481d0c533992d667e00f49dbaba51833421 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 26 Oct 2023 10:46:04 +0200 Subject: [PATCH 0942/1248] Merge fix --- client/lobby/RandomMapTab.cpp | 157 ---------------------------------- 1 file changed, 157 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 690c8a9da..443b92082 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -408,163 +408,6 @@ std::vector RandomMapTab::getPossibleMapSizes() return {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, CMapHeader::MAP_SIZE_HUGE, CMapHeader::MAP_SIZE_XHUGE, CMapHeader::MAP_SIZE_GIANT}; } -TemplatesDropBox::ListItem::ListItem(const JsonNode & config, TemplatesDropBox & _dropBox, Point position) - : InterfaceObjectConfigurable(LCLICK | HOVER, position), - dropBox(_dropBox) -{ - OBJ_CONSTRUCTION; - - build(config); - - if(auto w = widget("hoverImage")) - { - pos.w = w->pos.w; - pos.h = w->pos.h; - } - setRedrawParent(true); -} - -void TemplatesDropBox::ListItem::updateItem(int idx, const CRmgTemplate * _item) -{ - if(auto w = widget("labelName")) - { - item = _item; - if(item) - { - w->setText(item->getName()); - } - else - { - if(idx) - w->setText(""); - else - w->setText(readText(dropBox.variables["randomTemplate"])); - } - } -} - -void TemplatesDropBox::ListItem::hover(bool on) -{ - auto h = widget("hoverImage"); - auto w = widget("labelName"); - if(h && w) - { - if(w->getText().empty()) - h->visible = false; - else - h->visible = on; - } - redraw(); -} - -void TemplatesDropBox::ListItem::clickPressed(const Point & cursorPosition) -{ - if(isHovered()) - dropBox.setTemplate(item); -} - -void TemplatesDropBox::ListItem::clickReleased(const Point & cursorPosition) -{ - dropBox.clickPressed(cursorPosition); - dropBox.clickReleased(cursorPosition); -} - -TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size): - InterfaceObjectConfigurable(LCLICK | HOVER), - randomMapTab(randomMapTab) -{ - REGISTER_BUILDER("templateListItem", &TemplatesDropBox::buildListItem); - - curItems = VLC->tplh->getTemplates(); - - boost::range::sort(curItems, [](const CRmgTemplate * a, const CRmgTemplate * b){ - return a->getName() < b->getName(); - }); - - curItems.insert(curItems.begin(), nullptr); //default template - - const JsonNode config(ResourceID("config/widgets/randomMapTemplateWidget.json")); - - addCallback("sliderMove", std::bind(&TemplatesDropBox::sliderMove, this, std::placeholders::_1)); - - OBJ_CONSTRUCTION; - pos = randomMapTab.pos; - - build(config); - - if(auto w = widget("slider")) - { - w->setAmount(curItems.size()); - } - - //FIXME: this should be done by InterfaceObjectConfigurable, but might have side-effects - pos = children.front()->pos; - for (auto const & child : children) - pos = pos.include(child->pos); - - updateListItems(); -} - -std::shared_ptr TemplatesDropBox::buildListItem(const JsonNode & config) -{ - auto position = readPosition(config["position"]); - listItems.push_back(std::make_shared(config, *this, position)); - return listItems.back(); -} - -void TemplatesDropBox::sliderMove(int slidPos) -{ - auto w = widget("slider"); - if(!w) - return; // ignore spurious call when slider is being created - updateListItems(); - redraw(); -} - -bool TemplatesDropBox::receiveEvent(const Point & position, int eventType) const -{ - if (eventType == LCLICK) - return true; // we want drop box to close when clicking outside drop box borders - - return CIntObject::receiveEvent(position, eventType); -} - -void TemplatesDropBox::clickPressed(const Point & cursorPosition) -{ - if (!pos.isInside(cursorPosition)) - { - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); - } -} - -void TemplatesDropBox::updateListItems() -{ - if(auto w = widget("slider")) - { - int elemIdx = w->getValue(); - for(auto item : listItems) - { - if(elemIdx < curItems.size()) - { - item->updateItem(elemIdx, curItems[elemIdx]); - elemIdx++; - } - else - { - item->updateItem(elemIdx); - } - } - } -} - -void TemplatesDropBox::setTemplate(const CRmgTemplate * tmpl) -{ - randomMapTab.setTemplate(tmpl); - assert(GH.windows().isTopWindow(this)); - GH.windows().popWindows(1); -} - void TeamAlignmentsWidget::checkTeamCount() { //Do not allow to select one team only From 6337b0d3b91e2d295c36d4ee2e29ef9c38a16736 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 26 Oct 2023 15:32:13 +0300 Subject: [PATCH 0943/1248] Fix potential crash on loading mod with invalid handler name --- lib/mapObjectConstructors/CObjectClassesHandler.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 84ccbde4e..81f8ba01f 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -193,13 +193,16 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin assert(identifier.find(':') == std::string::npos); assert(!scope.empty()); + std::string handler = obj->handlerName; if(!handlerConstructors.count(obj->handlerName)) { - logGlobal->error("Handler with name %s was not found!", obj->handlerName); - return nullptr; + logMod->error("Handler with name %s was not found!", obj->handlerName); + // workaround for potential crash - if handler does not exists, continue with generic handler that is used for objects without any custom logc + handler = "generic"; + assert(handlerConstructors.count(handler) != 0); } - auto createdObject = handlerConstructors.at(obj->handlerName)(); + auto createdObject = handlerConstructors.at(handler)(); createdObject->modScope = scope; createdObject->typeName = obj->identifier;; From feae8b6ae42a57e7cbbff5b5e3c03288e7bc52d4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 26 Oct 2023 15:32:46 +0300 Subject: [PATCH 0944/1248] Do not allow loading new objects into another mod namespace --- lib/modding/ContentTypeHandler.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 457e6fdc5..977ad172b 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -104,7 +104,16 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) JsonNode & data = entry.second; if (data.meta != modName) - logMod->warn("Mod %s is attempting to inject object %s into mod %s! This may not be supported in future versions!", data.meta, name, modName); + { + // in this scenario, entire object record comes from another mod + // normally, this is used to "patch" object from another mod (which is legal) + // however in this case there is no object to patch. This might happen in such cases: + // - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases) + // - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data + // so emit warning and skip such case + logMod->warn("Mod %s attempts to edit object %s from mod %s but no such object exist!", data.meta, name, modName); + continue; + } if (vstd::contains(data.Struct(), "index") && !data["index"].isNull()) { From 6516ecbedd69dd5469d71221ed23780e083cb900 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 13:30:35 +0000 Subject: [PATCH 0945/1248] Format CI/linux-qt6/validate_json.py with 'black -l999 .' --- CI/linux-qt6/validate_json.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/CI/linux-qt6/validate_json.py b/CI/linux-qt6/validate_json.py index 82dab94ec..e48364120 100755 --- a/CI/linux-qt6/validate_json.py +++ b/CI/linux-qt6/validate_json.py @@ -12,19 +12,19 @@ import yaml # json: strict, but doesn't preserve line numbers necessarily, since it strips comments before parsing # json5: strict and preserves line numbers even for files with line comments # yaml: less strict, allows e.g. leading zeros -VALIDATION_TYPE = 'json5' +VALIDATION_TYPE = "json5" errors = [] -for path in sorted(Path('.').glob('**/*.json')): +for path in sorted(Path(".").glob("**/*.json")): # because path is an object and not a string path_str = str(path) try: - with open(path_str, 'r') as file: - if VALIDATION_TYPE == 'json': + with open(path_str, "r") as file: + if VALIDATION_TYPE == "json": jstyleson.load(file) - elif VALIDATION_TYPE == 'json5': + elif VALIDATION_TYPE == "json5": json5.load(file) - elif VALIDATION_TYPE == 'yaml': + elif VALIDATION_TYPE == "yaml": file = file.read().replace("\t", " ") file = file.replace("//", "#") yaml.safe_load(file) @@ -36,16 +36,16 @@ for path in sorted(Path('.').glob('**/*.json')): error_pos = path_str # create error position strings for each type of parser - if hasattr(exc, 'pos'): + if hasattr(exc, "pos"): # 'json' # https://stackoverflow.com/a/72850269/2278742 error_pos = f"{path_str}:{exc.lineno}:{exc.colno}" print(error_pos) - elif VALIDATION_TYPE == 'json5': + elif VALIDATION_TYPE == "json5": # 'json5' - pos = re.findall(r'\d+', str(exc)) + pos = re.findall(r"\d+", str(exc)) error_pos = f"{path_str}:{pos[0]}:{pos[-1]}" - elif hasattr(exc, 'problem_mark'): + elif hasattr(exc, "problem_mark"): # 'yaml' mark = exc.problem_mark error_pos = f"{path_str}:{mark.line+1}:{mark.column+1}" From 1d206253e03b91ecc27d4c083708946db55df10f Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 13:33:38 +0000 Subject: [PATCH 0946/1248] CI/linux-qt6/validate_json.py: Use sys.exit(1) instead of throwing Exception --- CI/linux-qt6/validate_json.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CI/linux-qt6/validate_json.py b/CI/linux-qt6/validate_json.py index e48364120..5fbe0ac76 100755 --- a/CI/linux-qt6/validate_json.py +++ b/CI/linux-qt6/validate_json.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import re +import sys from pathlib import Path from pprint import pprint @@ -54,6 +55,6 @@ for path in sorted(Path(".").glob("**/*.json")): errors.append({"error_pos": error_pos, "error_msg": exc}) if errors: - print("Summary of errors:") + print("The following JSON files are invalid:") pprint(errors) - raise Exception("Not all JSON files are valid") + sys.exit(1) From 04b3dca773cb5a5e5a1a430ba58b32c66931a64a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 13:34:26 +0000 Subject: [PATCH 0947/1248] github.yml: Remove duplicate -prune in line endings stage --- .github/workflows/github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index cadab3783..a357a5f7d 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -148,7 +148,7 @@ jobs: - name: Ensure LF line endings if: ${{ startsWith(matrix.preset, 'linux-clang-test') }} run: | - find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -prune -o -path ./test/googletest \ + find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \ -o -path ./osx -prune -o -type f \ -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ { ! xargs -0 grep -l -z -P '\r\n'; } From e4aaeef5da1fccc3ce1579bf141e76507f36e720 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 13:50:33 +0000 Subject: [PATCH 0948/1248] AUTHORS.h: Global variables should be const. Non-const global variables should not be used --- AUTHORS.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS.h b/AUTHORS.h index 483d475af..a9ff41e66 100644 --- a/AUTHORS.h +++ b/AUTHORS.h @@ -10,7 +10,7 @@ #pragma once //VCMI PROJECT CODE CONTRIBUTORS: -std::vector> contributors = { +const std::vector> contributors = { // Task Name Aka E-Mail { "Idea", "Michał Urbańczyk", "Tow", "impono@gmail.com" }, { "Idea", "Mateusz B.", "Tow dragon", "matcio1@gmail.com" }, From bcaa2e5966314949553a3908ae84b23f743d2161 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 14:11:55 +0000 Subject: [PATCH 0949/1248] AI/VCAI/VCAI.cpp: Remove "e" from "throw" statement to rethrow the original exception. The original exception object should be rethrown --- AI/VCAI/VCAI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 8f79eaa43..3c5dd3691 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1510,7 +1510,7 @@ void VCAI::wander(HeroPtr h) if(e.goal->goalType == Goals::EGoals::VISIT_TILE || e.goal->goalType == Goals::EGoals::VISIT_OBJ) continue; - throw e; + throw; } } else From 3616235bb5857a7d00ef3643e8a6b737630821db Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 13:44:29 +0000 Subject: [PATCH 0950/1248] FramerateManager.cpp: Merge this "if" statement with the enclosing one. Collapsible "if" statements should be merged --- client/gui/FramerateManager.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/client/gui/FramerateManager.cpp b/client/gui/FramerateManager.cpp index 26757f96f..cc6b7d64c 100644 --- a/client/gui/FramerateManager.cpp +++ b/client/gui/FramerateManager.cpp @@ -28,13 +28,10 @@ void FramerateManager::framerateDelay() { Duration timeSpentBusy = Clock::now() - lastTimePoint; - if(!vsyncEnabled) + if(!vsyncEnabled && timeSpentBusy < targetFrameTime) { // if FPS is higher than it should be, then wait some time - if(timeSpentBusy < targetFrameTime) - { - boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); - } + boost::this_thread::sleep_for(targetFrameTime - timeSpentBusy); } // compute actual timeElapsed taking into account actual sleep interval From 2b210017438924250c4f650d61b3f8f944236d08 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 14:35:03 +0000 Subject: [PATCH 0951/1248] lib/rmg/RoadPlacer.cpp: Remove "e" from this "throw" statement to rethrow the original exception. The original exception object should be rethrown --- lib/rmg/modificators/RoadPlacer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index 0441c4f05..aca1acfd7 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -201,7 +201,7 @@ void RoadPlacer::connectRoads() catch (const std::exception & e) { logGlobal->error("Unhandled exception while drawing road to node %s: %s", node.toString(), e.what()); - throw e; + throw; } } From 70acf987b4f81bb3928fbf46c4f81c64b45b80b6 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 14:47:09 +0000 Subject: [PATCH 0952/1248] lib/BasicTypes.cpp: Remove the unary minus operator or change the expression's underlying type. Unary minus should not be applied to an unsigned expression --- lib/BasicTypes.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index af58094f0..6dd55e4db 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -111,7 +111,7 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const bonusList = getBonusBearer()->getBonuses(moraleSelector, cachingStrMor); int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size(); - int32_t maxBadMorale = -VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); + int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size(); return std::clamp(bonusList->totalValue(), maxBadMorale, maxGoodMorale); } @@ -130,7 +130,7 @@ int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const bonusList = getBonusBearer()->getBonuses(luckSelector, cachingStrLuck); int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size(); - int32_t maxBadLuck = -VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); + int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size(); return std::clamp(bonusList->totalValue(), maxBadLuck, maxGoodLuck); } From c1707bcc71d6be13aaea9c2e69437e5c1ea57315 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 27 Oct 2023 18:52:25 +0200 Subject: [PATCH 0953/1248] fix datatype --- client/mainmenu/CHighScoreScreen.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index d71e9148d..c3762859f 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -252,7 +252,7 @@ int CHighScoreInputScreen::addEntry(std::string text) { auto sortFunctor = [](const JsonNode & left, const JsonNode & right) { if(left["points"].Integer() == right["points"].Integer()) - return left["posFlag"].Integer() > right["posFlag"].Integer(); + return left["posFlag"].Bool() > right["posFlag"].Bool(); return left["points"].Integer() > right["points"].Integer(); }; From f01ec55d21ac122ad522aa8e245cd0d2579aa231 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 15:10:54 +0000 Subject: [PATCH 0954/1248] Use the "nullptr" literal. "nullptr" should be used to denote the null pointer --- AI/VCAI/VCAI.h | 2 +- client/CMT.cpp | 2 +- client/CVideoHandler.cpp | 2 +- client/CVideoHandler.h | 2 +- client/mainmenu/CHighScoreScreen.cpp | 2 +- client/renderSDL/CursorSoftware.cpp | 2 +- client/renderSDL/SDL_Extensions.cpp | 6 +++--- launcher/aboutProject/aboutproject_moc.h | 2 +- launcher/firstLaunch/firstlaunch_moc.h | 2 +- launcher/lobby/lobby.h | 2 +- launcher/modManager/cmodlistmodel_moc.h | 4 ++-- launcher/modManager/cmodlistview_moc.h | 2 +- launcher/modManager/imageviewer_moc.h | 4 ++-- launcher/settingsView/csettingsview_moc.h | 2 +- lib/CConsoleHandler.cpp | 2 +- lib/StartInfo.h | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/serializer/CSerializer.h | 2 +- server/CVCMIServer.cpp | 6 +++--- 19 files changed, 25 insertions(+), 25 deletions(-) diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index bbf754210..8231003d9 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -260,7 +260,7 @@ public: //optimization - use one SM for every hero call const CGTownInstance * findTownWithTavern() const; - bool canRecruitAnyHero(const CGTownInstance * t = NULL) const; + bool canRecruitAnyHero(const CGTownInstance * t = nullptr) const; Goals::TSubgoal getGoal(HeroPtr h) const; bool canAct(HeroPtr h) const; diff --git a/client/CMT.cpp b/client/CMT.cpp index 2a1c40a8a..8f38897f7 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -86,7 +86,7 @@ static void prog_version() static void prog_help(const po::options_description &opts) { - auto time = std::time(0); + auto time = std::time(nullptr); printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); printf("This is free software; see the source for copying conditions. There is NO\n"); diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index fae06b68a..cd3cc8b6c 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -305,7 +305,7 @@ bool CVideoPlayer::nextFrame() sws_scale(sws, frame->data, frame->linesize, 0, codecContext->height, data, linesize); - SDL_UpdateYUVTexture(texture, NULL, data[0], linesize[0], + SDL_UpdateYUVTexture(texture, nullptr, data[0], linesize[0], data[1], linesize[1], data[2], linesize[2]); av_freep(&data[0]); diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index ca4d3b293..e39d9445c 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -31,7 +31,7 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: - virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = 0){} + virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr){} virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { return false; diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index d71e9148d..de76d62b5 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -264,7 +264,7 @@ int CHighScoreInputScreen::addEntry(std::string text) { newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName; newNode["days"].Integer() = calc.calculate().sumDays; newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total; - newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(0)); + newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(nullptr)); newNode["posFlag"].Bool() = true; baseNode.push_back(newNode); diff --git a/client/renderSDL/CursorSoftware.cpp b/client/renderSDL/CursorSoftware.cpp index 1437d12ce..78b9e1250 100644 --- a/client/renderSDL/CursorSoftware.cpp +++ b/client/renderSDL/CursorSoftware.cpp @@ -59,7 +59,7 @@ void CursorSoftware::updateTexture() CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY)); cursorImage->draw(cursorSurface); - SDL_UpdateTexture(cursorTexture, NULL, cursorSurface->pixels, cursorSurface->pitch); + SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch); needUpdate = false; } diff --git a/client/renderSDL/SDL_Extensions.cpp b/client/renderSDL/SDL_Extensions.cpp index 04254db36..300611d70 100644 --- a/client/renderSDL/SDL_Extensions.cpp +++ b/client/renderSDL/SDL_Extensions.cpp @@ -68,7 +68,7 @@ void CSDL_Ext::updateRect(SDL_Surface *surface, const Rect & rect ) logGlobal->error("%sSDL_UpdateTexture %s", __FUNCTION__, SDL_GetError()); SDL_RenderClear(mainRenderer); - if(0 != SDL_RenderCopy(mainRenderer, screenTexture, NULL, NULL)) + if(0 != SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr)) logGlobal->error("%sSDL_RenderCopy %s", __FUNCTION__, SDL_GetError()); SDL_RenderPresent(mainRenderer); @@ -813,8 +813,8 @@ void CSDL_Ext::fillRectBlended( SDL_Surface *dst, const Rect & dstrect, const SD uint32_t sdlColor = SDL_MapRGBA(dst->format, color.r, color.g, color.b, color.a); SDL_Surface * tmp = SDL_CreateRGBSurface(0, newRect.w, newRect.h, dst->format->BitsPerPixel, dst->format->Rmask, dst->format->Gmask, dst->format->Bmask, dst->format->Amask); - SDL_FillRect(tmp, NULL, sdlColor); - SDL_BlitSurface(tmp, NULL, dst, &newRect); + SDL_FillRect(tmp, nullptr, sdlColor); + SDL_BlitSurface(tmp, nullptr, dst, &newRect); SDL_FreeSurface(tmp); } diff --git a/launcher/aboutProject/aboutproject_moc.h b/launcher/aboutProject/aboutproject_moc.h index 386565a60..aaad48d18 100644 --- a/launcher/aboutProject/aboutproject_moc.h +++ b/launcher/aboutProject/aboutproject_moc.h @@ -23,7 +23,7 @@ class AboutProjectView : public QWidget void changeEvent(QEvent *event) override; public: - explicit AboutProjectView(QWidget * parent = 0); + explicit AboutProjectView(QWidget * parent = nullptr); public slots: diff --git a/launcher/firstLaunch/firstlaunch_moc.h b/launcher/firstLaunch/firstlaunch_moc.h index c45bd2b18..dd773c365 100644 --- a/launcher/firstLaunch/firstlaunch_moc.h +++ b/launcher/firstLaunch/firstlaunch_moc.h @@ -67,7 +67,7 @@ class FirstLaunchView : public QWidget void installMod(const QString & modID); public: - explicit FirstLaunchView(QWidget * parent = 0); + explicit FirstLaunchView(QWidget * parent = nullptr); public slots: diff --git a/launcher/lobby/lobby.h b/launcher/lobby/lobby.h index 4e77c802a..84fe67e22 100644 --- a/launcher/lobby/lobby.h +++ b/launcher/lobby/lobby.h @@ -194,7 +194,7 @@ class SocketLobby : public QObject { Q_OBJECT public: - explicit SocketLobby(QObject *parent = 0); + explicit SocketLobby(QObject *parent = nullptr); void connectServer(const QString & host, int port, const QString & username, int timeout); void disconnectServer(); void requestNewSession(const QString & session, int totalPlayers, const QString & pswd, const QMap & mods); diff --git a/launcher/modManager/cmodlistmodel_moc.h b/launcher/modManager/cmodlistmodel_moc.h index 65a905409..5125b61b8 100644 --- a/launcher/modManager/cmodlistmodel_moc.h +++ b/launcher/modManager/cmodlistmodel_moc.h @@ -56,7 +56,7 @@ class CModListModel : public QAbstractItemModel, public CModList QVariant getIcon(const CModEntry & mod, int field) const; public: - explicit CModListModel(QObject * parent = 0); + explicit CModListModel(QObject * parent = nullptr); /// CModListContainer overrides void resetRepositories() override; @@ -93,5 +93,5 @@ class CModFilterModel : public QSortFilterProxyModel public: void setTypeFilter(int filteredType, int filterMask); - CModFilterModel(CModListModel * model, QObject * parent = 0); + CModFilterModel(CModListModel * model, QObject * parent = nullptr); }; diff --git a/launcher/modManager/cmodlistview_moc.h b/launcher/modManager/cmodlistview_moc.h index a1538755d..cadfb0a51 100644 --- a/launcher/modManager/cmodlistview_moc.h +++ b/launcher/modManager/cmodlistview_moc.h @@ -64,7 +64,7 @@ signals: void modsChanged(); public: - explicit CModListView(QWidget * parent = 0); + explicit CModListView(QWidget * parent = nullptr); ~CModListView(); void loadScreenshots(); diff --git a/launcher/modManager/imageviewer_moc.h b/launcher/modManager/imageviewer_moc.h index 771a4e9a4..5c0c1e674 100644 --- a/launcher/modManager/imageviewer_moc.h +++ b/launcher/modManager/imageviewer_moc.h @@ -23,12 +23,12 @@ class ImageViewer : public QDialog void changeEvent(QEvent *event) override; public: - explicit ImageViewer(QWidget * parent = 0); + explicit ImageViewer(QWidget * parent = nullptr); ~ImageViewer(); void setPixmap(QPixmap & pixmap); - static void showPixmap(QPixmap & pixmap, QWidget * parent = 0); + static void showPixmap(QPixmap & pixmap, QWidget * parent = nullptr); protected: void mousePressEvent(QMouseEvent * event) override; diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 466901389..0a144f7f5 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -20,7 +20,7 @@ class CSettingsView : public QWidget Q_OBJECT public: - explicit CSettingsView(QWidget * parent = 0); + explicit CSettingsView(QWidget * parent = nullptr); ~CSettingsView(); void loadSettings(); diff --git a/lib/CConsoleHandler.cpp b/lib/CConsoleHandler.cpp index 00dca3da3..72de08baa 100644 --- a/lib/CConsoleHandler.cpp +++ b/lib/CConsoleHandler.cpp @@ -134,7 +134,7 @@ LONG WINAPI onUnhandledException(EXCEPTION_POINTERS* exception) HMODULE hModule = nullptr; GetModuleFileNameA(hModule, buffer, MAX_PATH); mname = strrchr(buffer, '\\'); - if (mname != 0) + if (mname != nullptr) mname++; else mname = buffer; diff --git a/lib/StartInfo.h b/lib/StartInfo.h index b4f563e7c..925df407c 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -140,7 +140,7 @@ struct DLL_LINKAGE StartInfo } StartInfo() : mode(INVALID), difficulty(1), seedToBeUsed(0), seedPostInit(0), - mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(0))), fileURI("") + mapfileChecksum(0), startTimeIso8601(vstd::getDateTimeISO8601Basic(std::time(nullptr))), fileURI("") { } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c349e851d..a73a2391a 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -573,7 +573,7 @@ void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRan const std::string templateName = options->getMapTemplate()->getName(); const ui32 seed = scenarioOps->seedToBeUsed; - const std::string dt = vstd::getDateTimeISO8601Basic(std::time(0)); + const std::string dt = vstd::getDateTimeISO8601Basic(std::time(nullptr)); const std::string fileName = boost::str(boost::format("%s_%s_%d.vmap") % dt % templateName % seed ); const auto fullPath = path / fileName; diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index fa3dedefc..df7278f00 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -54,7 +54,7 @@ struct VectorizedObjectInfo class DLL_LINKAGE CSerializer { template - static si32 idToNumber(const T &t, typename std::enable_if::value>::type * dummy = 0) + static si32 idToNumber(const T &t, typename std::enable_if::value>::type * dummy = nullptr) { return t; } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 271ece218..dccd090d0 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -317,7 +317,7 @@ bool CVCMIServer::prepareToStartGame() { case StartInfo::CAMPAIGN: logNetwork->info("Preparing to start new campaign"); - si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; si->campState->setCurrentMap(campaignMap); si->campState->setCurrentMapBonus(campaignBonus); @@ -326,7 +326,7 @@ bool CVCMIServer::prepareToStartGame() case StartInfo::NEW_GAME: logNetwork->info("Preparing to start new game"); - si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(0)); + si->startTimeIso8601 = vstd::getDateTimeISO8601Basic(std::time(nullptr)); si->fileURI = mi->fileURI; gh->init(si.get(), progressTracking); break; @@ -1125,7 +1125,7 @@ static void handleCommandOptions(int argc, const char * argv[], boost::program_o #ifndef SINGLE_PROCESS_APP if(options.count("help")) { - auto time = std::time(0); + auto time = std::time(nullptr); printf("%s - A Heroes of Might and Magic 3 clone\n", GameConstants::VCMI_VERSION.c_str()); printf("Copyright (C) 2007-%d VCMI dev team - see AUTHORS file\n", std::localtime(&time)->tm_year + 1900); printf("This is free software; see the source for copying conditions. There is NO\n"); From 10ad5b67890889aa4912b304c3c2efc16a993d61 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 17:16:27 +0000 Subject: [PATCH 0955/1248] client/windows/CCastleInterface.cpp: Identical sub-expressions on both sides of operator "&&". Identical expressions should not be used on both sides of a binary operator --- client/windows/CCastleInterface.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 84c1ccd05..a0bc94cc8 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -969,9 +969,9 @@ void CCastleBuildings::enterMagesGuild() { const StartInfo *si = LOCPLINT->cb->getStartInfo(); // it would be nice to find a way to move this hack to config/mapOverrides.json - if(si && si->campState && si->campState && // We're in campaign, + if(si && si->campState && // We're in campaign, (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", - (hero->subID == 45)) // and the hero is Yog (based on Solmyr) + (hero->subID == 45)) // and the hero is Yog (based on Solmyr) { // "Yog has given up magic in all its forms..." LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); From af330ff03821e08b67fd9f4660344320890632ad Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 17:23:54 +0000 Subject: [PATCH 0956/1248] AI/BattleAI/BattleEvaluator.cpp: Remove this redundant cast. Redundant casts should not be used --- AI/BattleAI/BattleEvaluator.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index cae77e61f..b52c04c25 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -58,7 +58,7 @@ std::vector BattleEvaluator::getBrokenWallMoatHexes() const if(state != EWallState::DESTROYED) continue; - auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex((EWallPart)wallPart); + auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart); auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT); result.push_back(moatHex); @@ -142,7 +142,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack) logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%2f -%2f = %2f", bestAttack.attackerState->unitType()->getJsonKey(), bestAttack.affectedUnits[0]->unitType()->getJsonKey(), - (int)bestAttack.affectedUnits[0]->getCount(), + bestAttack.affectedUnits[0]->getCount(), (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex, bestAttack.attack.chargeDistance, From b10b1a54446ef308cca4768aacaeca850349236f Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 17:29:08 +0000 Subject: [PATCH 0957/1248] AI/BattleAI/BattleExchangeVariant.cpp: This function should be declared "const". Member functions that don't mutate their objects should be declared "const" --- AI/BattleAI/BattleExchangeVariant.cpp | 2 +- AI/BattleAI/BattleExchangeVariant.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 8ae233e41..6a274152a 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -327,7 +327,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable( return result; } -std::vector BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) +std::vector BattleExchangeEvaluator::getAdjacentUnits(const battle::Unit * blockerUnit) const { std::queue queue; std::vector checkedStacks; diff --git a/AI/BattleAI/BattleExchangeVariant.h b/AI/BattleAI/BattleExchangeVariant.h index 36b8b2592..e71f2785d 100644 --- a/AI/BattleAI/BattleExchangeVariant.h +++ b/AI/BattleAI/BattleExchangeVariant.h @@ -129,7 +129,7 @@ public: DamageCache & damageCache, std::shared_ptr hb); - std::vector getAdjacentUnits(const battle::Unit * unit); + std::vector getAdjacentUnits(const battle::Unit * unit) const; float getPositiveEffectMultiplier() { return 1; } float getNegativeEffectMultiplier() { return negativeEffectMultiplier; } From ead1140b9b982ffa60214dc4ea35354913f6f320 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 17:47:43 +0000 Subject: [PATCH 0958/1248] client/icons/generate_icns.py: Replace print statement by built-in function. The "print" statement should not be used --- client/icons/generate_icns.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/icons/generate_icns.py b/client/icons/generate_icns.py index 25d351afb..4b1defbba 100755 --- a/client/icons/generate_icns.py +++ b/client/icons/generate_icns.py @@ -3,7 +3,7 @@ import os, sys, shutil img = Image.open(sys.argv[1]) if img.size != (1024,1024): - print "Input image must be 1024x1024. Provided image is %dx%d" % img.size + print("Input image must be 1024x1024. Provided image is %dx%d" % img.size) os.mkdir("vcmi.iconset") for i in [16, 32, 128, 256, 512]: From 457e73ed12381dd8968879692556019246e51bd8 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 18:41:27 +0000 Subject: [PATCH 0959/1248] AI/BattleAI/BattleExchangeVariant.cpp: Do not assign data members in a constructor. Initialize members in an initialization list. Member data should be initialized in-class or in a constructor initialization list --- AI/BattleAI/BattleExchangeVariant.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 6a274152a..7b7a73f63 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -12,9 +12,9 @@ #include "../../lib/CStack.h" AttackerValue::AttackerValue() + : value(0), + isRetalitated(false) { - value = 0; - isRetalitated = false; } MoveTarget::MoveTarget() From 0a6c82c63991e1f140529486405edebf0eab1e09 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 19:32:52 +0000 Subject: [PATCH 0960/1248] AI/Nullkiller/Behaviors/DefenceBehavior.{h,cpp}: treat -> threat --- .../Analyzers/DangerHitMapAnalyzer.cpp | 52 +++++++------- .../Analyzers/DangerHitMapAnalyzer.h | 10 +-- AI/Nullkiller/Behaviors/DefenceBehavior.cpp | 72 +++++++++---------- AI/Nullkiller/Behaviors/DefenceBehavior.h | 4 +- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 6 +- 5 files changed, 72 insertions(+), 72 deletions(-) diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index aa2f91e59..1ee4e57f5 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -15,7 +15,7 @@ namespace NKAI { -HitMapInfo HitMapInfo::NoTreat; +HitMapInfo HitMapInfo::NoThreat; double HitMapInfo::value() const { @@ -39,7 +39,7 @@ void DangerHitMapAnalyzer::updateHitMap() hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); enemyHeroAccessibleObjects.clear(); - townTreats.clear(); + townThreats.clear(); std::map> heroes; @@ -57,7 +57,7 @@ void DangerHitMapAnalyzer::updateHitMap() for(auto town : ourTowns) { - townTreats[town->id]; // insert empty list + townThreats[town->id]; // insert empty list } foreach_tile_pos([&](const int3 & pos){ @@ -91,21 +91,21 @@ void DangerHitMapAnalyzer::updateHitMap() auto & node = hitMap[pos.x][pos.y][pos.z]; - HitMapInfo newTreat; + HitMapInfo newThreat; - newTreat.hero = path.targetHero; - newTreat.turn = path.turn(); - newTreat.danger = path.getHeroStrength(); + newThreat.hero = path.targetHero; + newThreat.turn = path.turn(); + newThreat.danger = path.getHeroStrength(); - if(newTreat.value() > node.maximumDanger.value()) + if(newThreat.value() > node.maximumDanger.value()) { - node.maximumDanger = newTreat; + node.maximumDanger = newThreat; } - if(newTreat.turn < node.fastestDanger.turn - || (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger)) + if(newThreat.turn < node.fastestDanger.turn + || (newThreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newThreat.danger)) { - node.fastestDanger = newTreat; + node.fastestDanger = newThreat; } auto objects = cb->getVisitableObjs(pos, false); @@ -114,24 +114,24 @@ void DangerHitMapAnalyzer::updateHitMap() { if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID) { - auto & treats = townTreats[obj->id]; - auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool + auto & threats = townThreats[obj->id]; + auto threat = std::find_if(threats.begin(), threats.end(), [&](const HitMapInfo & i) -> bool { return i.hero.hid == path.targetHero->id; }); - if(treat == treats.end()) + if(threat == threats.end()) { - treats.emplace_back(); - treat = std::prev(treats.end(), 1); + threats.emplace_back(); + threat = std::prev(threats.end(), 1); } - if(newTreat.value() > treat->value()) + if(newThreat.value() > threat->value()) { - *treat = newTreat; + *threat = newThreat; } - if(newTreat.turn == 0) + if(newThreat.turn == 0) { if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) enemyHeroAccessibleObjects.emplace_back(path.targetHero, obj); @@ -240,13 +240,13 @@ void DangerHitMapAnalyzer::calculateTileOwners() }); } -const std::vector & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const +const std::vector & DangerHitMapAnalyzer::getTownThreats(const CGTownInstance * town) const { static const std::vector empty = {}; - auto result = townTreats.find(town->id); + auto result = townThreats.find(town->id); - return result == townTreats.end() ? empty : result->second; + return result == townThreats.end() ? empty : result->second; } PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const @@ -271,14 +271,14 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & || (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger)); } -const HitMapNode & DangerHitMapAnalyzer::getObjectTreat(const CGObjectInstance * obj) const +const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const { auto tile = obj->visitablePos(); - return getTileTreat(tile); + return getTileThreat(tile); } -const HitMapNode & DangerHitMapAnalyzer::getTileTreat(const int3 & tile) const +const HitMapNode & DangerHitMapAnalyzer::getTileThreat(const int3 & tile) const { const HitMapNode & info = hitMap[tile.x][tile.y][tile.z]; diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h index 614312649..45538c99b 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h @@ -18,7 +18,7 @@ struct AIPath; struct HitMapInfo { - static HitMapInfo NoTreat; + static HitMapInfo NoThreat; uint64_t danger; uint8_t turn; @@ -74,7 +74,7 @@ private: bool hitMapUpToDate = false; bool tileOwnersUpToDate = false; const Nullkiller * ai; - std::map> townTreats; + std::map> townThreats; public: DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {} @@ -82,14 +82,14 @@ public: void updateHitMap(); void calculateTileOwners(); uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const; - const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const; - const HitMapNode & getTileTreat(const int3 & tile) const; + const HitMapNode & getObjectThreat(const CGObjectInstance * obj) const; + const HitMapNode & getTileThreat(const int3 & tile) const; std::set getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const; void reset(); void resetTileOwners() { tileOwnersUpToDate = false; } PlayerColor getTileOwner(const int3 & tile) const; const CGTownInstance * getClosestTown(const int3 & tile) const; - const std::vector & getTownTreats(const CGTownInstance * town) const; + const std::vector & getTownThreats(const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp index 494682d3b..7d2ab874e 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.cpp +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.cpp @@ -25,7 +25,7 @@ namespace NKAI { -const float TREAT_IGNORE_RATIO = 2; +const float THREAT_IGNORE_RATIO = 2; using namespace Goals; @@ -46,20 +46,20 @@ Goals::TGoalVec DefenceBehavior::decompose() const return tasks; } -bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector & paths) +bool isThreatUnderControl(const CGTownInstance * town, const HitMapInfo & threat, const std::vector & paths) { int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK); for(const AIPath & path : paths) { - bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO; - bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7; + bool threatIsWeak = path.getHeroStrength() / (float)threat.danger > THREAT_IGNORE_RATIO; + bool needToSaveGrowth = threat.turn == 0 && dayOfWeek == 7; - if(treatIsWeak && !needToSaveGrowth) + if(threatIsWeak && !needToSaveGrowth) { - if((path.exchangeCount == 1 && path.turn() < treat.turn) - || path.turn() < treat.turn - 1 - || (path.turn() < treat.turn && treat.turn >= 2)) + if((path.exchangeCount == 1 && path.turn() < threat.turn) + || path.turn() < threat.turn - 1 + || (path.turn() < threat.turn && threat.turn >= 2)) { #if NKAI_TRACE_LEVEL >= 1 logAi->trace( @@ -79,16 +79,16 @@ bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, void handleCounterAttack( const CGTownInstance * town, - const HitMapInfo & treat, + const HitMapInfo & threat, const HitMapInfo & maximumDanger, Goals::TGoalVec & tasks) { - if(treat.hero.validAndSet() - && treat.turn <= 1 - && (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn)) + if(threat.hero.validAndSet() + && threat.turn <= 1 + && (threat.danger == maximumDanger.danger || threat.turn < maximumDanger.turn)) { - auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos()); - auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get()); + auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(threat.hero->visitablePos()); + auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, threat.hero.get()); for(int i = 0; i < heroCapturingPaths.size(); i++) { @@ -99,7 +99,7 @@ void handleCounterAttack( Composition composition; - composition.addNext(DefendTown(town, treat, path, true)).addNext(goal); + composition.addNext(DefendTown(town, threat, path, true)).addNext(goal); tasks.push_back(Goals::sptr(composition)); } @@ -152,19 +152,19 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { logAi->trace("Evaluating defence for %s", town->getNameTranslated()); - auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town); - std::vector treats = ai->nullkiller->dangerHitMap->getTownTreats(town); + auto threatNode = ai->nullkiller->dangerHitMap->getObjectThreat(town); + std::vector threats = ai->nullkiller->dangerHitMap->getTownThreats(town); - treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there + threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks)) { return; } - if(!treatNode.fastestDanger.hero) + if(!threatNode.fastestDanger.hero) { - logAi->trace("No treat found for town %s", town->getNameTranslated()); + logAi->trace("No threat found for town %s", town->getNameTranslated()); return; } @@ -179,23 +179,23 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos()); - for(auto & treat : treats) + for(auto & threat : threats) { logAi->trace( - "Town %s has treat %lld in %s turns, hero: %s", + "Town %s has threat %lld in %s turns, hero: %s", town->getNameTranslated(), - treat.danger, - std::to_string(treat.turn), - treat.hero ? treat.hero->getNameTranslated() : std::string("")); + threat.danger, + std::to_string(threat.turn), + threat.hero ? threat.hero->getNameTranslated() : std::string("")); - handleCounterAttack(town, treat, treatNode.maximumDanger, tasks); + handleCounterAttack(town, threat, threatNode.maximumDanger, tasks); - if(isTreatUnderControl(town, treat, paths)) + if(isThreatUnderControl(town, threat, paths)) { continue; } - evaluateRecruitingHero(tasks, treat, town); + evaluateRecruitingHero(tasks, threat, town); if(paths.empty()) { @@ -236,7 +236,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(path.turn() <= treat.turn - 2) + if(path.turn() <= threat.turn - 2) { #if NKAI_TRACE_LEVEL >= 1 logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun", @@ -264,7 +264,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta { tasks.push_back( Goals::sptr(Composition() - .addNext(DefendTown(town, treat, path.targetHero)) + .addNext(DefendTown(town, threat, path.targetHero)) .addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get(), HeroLockedReason::DEFENCE)))); } @@ -281,7 +281,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta tasks.push_back( Goals::sptr(Composition() - .addNext(DefendTown(town, treat, path)) + .addNext(DefendTown(town, threat, path)) .addNextSequence({ sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())), sptr(ExecuteHeroChain(path, town)), @@ -291,7 +291,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta continue; } - if(treat.turn == 0 || (path.turn() <= treat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= treat.danger)) + if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger)) { if(ai->nullkiller->arePathHeroesLocked(path)) { @@ -324,7 +324,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta } Composition composition; - composition.addNext(DefendTown(town, treat, path)); + composition.addNext(DefendTown(town, threat, path)); TGoalVec sequence; if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1) @@ -402,7 +402,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta logAi->debug("Found %d tasks", tasks.size()); } -void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const +void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const { if(town->hasBuilt(BuildingID::TAVERN) && cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) @@ -411,7 +411,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM for(auto hero : heroesInTavern) { - if(hero->getTotalStrength() < treat.danger) + if(hero->getTotalStrength() < threat.danger) continue; auto myHeroes = cb->getHeroesInfo(); @@ -463,7 +463,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM sequence.push_back(sptr(Goals::RecruitHero(town, hero))); - tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence))); + tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, threat, hero)).addNextSequence(sequence))); } } } diff --git a/AI/Nullkiller/Behaviors/DefenceBehavior.h b/AI/Nullkiller/Behaviors/DefenceBehavior.h index b9b7c486e..fab4745f5 100644 --- a/AI/Nullkiller/Behaviors/DefenceBehavior.h +++ b/AI/Nullkiller/Behaviors/DefenceBehavior.h @@ -1,5 +1,5 @@ /* -* BuyArmyBehavior.h, part of VCMI engine +* DefenceBehavior.h, part of VCMI engine * * Authors: listed in file AUTHORS in main folder * @@ -39,7 +39,7 @@ namespace Goals private: void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const; - void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const; + void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town) const; }; } diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 9b3ef27c0..98ce1a86f 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -593,15 +593,15 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const { - auto & treatNode = ai->dangerHitMap->getTileTreat(tile); + auto & treatNode = ai->dangerHitMap->getTileThreat(tile); if(treatNode.maximumDanger.danger == 0) - return HitMapInfo::NoTreat; + return HitMapInfo::NoThreat; if(treatNode.maximumDanger.turn <= turn) return treatNode.maximumDanger; - return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoTreat; + return treatNode.fastestDanger.turn <= turn ? treatNode.fastestDanger : HitMapInfo::NoThreat; } int32_t getArmyCost(const CArmedInstance * army) From 860f6150aae443f08d031b357d79c7e32128c82e Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 19:40:24 +0000 Subject: [PATCH 0961/1248] lib/mapObjects/IMarket.cpp: Forming reference to null pointer Null pointers should not be dereferenced --- lib/mapObjects/IMarket.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index 4bbccc872..f4dd54a12 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -157,7 +157,7 @@ std::vector IMarket::availableItemsIds(EMarketMode mode) const const IMarket * IMarket::castFrom(const CGObjectInstance *obj, bool verbose) { auto * imarket = dynamic_cast(obj); - if(verbose && !imarket) + if(verbose && !imarket && obj) logGlobal->error("Cannot cast to IMarket object type %s", obj->typeName); return imarket; } From cc8cc11da31a958a0ee0f561469a9a229d8fb369 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 20:00:52 +0000 Subject: [PATCH 0962/1248] server/battles/BattleFlowProcessor.cpp: Called C++ object pointer is null Null pointers should not be dereferenced --- server/battles/BattleFlowProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/battles/BattleFlowProcessor.cpp b/server/battles/BattleFlowProcessor.cpp index 501408660..6e9fd8573 100644 --- a/server/battles/BattleFlowProcessor.cpp +++ b/server/battles/BattleFlowProcessor.cpp @@ -284,7 +284,7 @@ const CStack * BattleFlowProcessor::getNextStack(const CBattleInfoCallback & bat gameHandler->sendAndApply(&bte); } - if(!next->willMove()) + if(!next || !next->willMove()) return nullptr; return stack; From 03835236fb3f122ea8e0c24b7f0a79b766fcfbf6 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 20:04:51 +0000 Subject: [PATCH 0963/1248] lib/mapObjects/CGPandoraBox.cpp: Identical sub-expressions on both sides of operator "||". Identical expressions should not be used on both sides of a binary operator --- lib/mapObjects/CGPandoraBox.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 81035cbb1..ddd6a9c5c 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -277,11 +277,10 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) || vinfo.reward.heroExperience || vinfo.reward.manaDiff || vinfo.reward.resources.nonZero() + || !vinfo.reward.artifacts.empty() || !vinfo.reward.bonuses.empty() - || !vinfo.reward.artifacts.empty() - || !vinfo.reward.secondary.empty() - || !vinfo.reward.artifacts.empty() - || !vinfo.reward.creatures.empty(); + || !vinfo.reward.creatures.empty() + || !vinfo.reward.secondary.empty(); if(hasSomething) configuration.info.push_back(vinfo); From d2c03773cda7a6656249026c253dc8eb4b61efe9 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 20:56:31 +0000 Subject: [PATCH 0964/1248] client/NetpacksClient.cpp: Called C++ object pointer is null Null pointers should not be dereferenced --- client/NetPacksClient.cpp | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 81cd835d3..7fc6b69bf 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -367,19 +367,27 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeOut(obj, pack.initiator); - - CGI->mh->waitForOngoingAnimations(); + if(CGI) + { + if(CGI->mh) + { + CGI->mh->onObjectFadeOut(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); + } + } } void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI->mh) - CGI->mh->onObjectFadeIn(obj, pack.initiator); - - CGI->mh->waitForOngoingAnimations(); + if(CGI) + { + if(CGI->mh) + { + CGI->mh->onObjectFadeIn(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); + } + } cl.invalidatePaths(); } From 92bab6dd085a5ae03a226303331bba6d66b19fbb Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 00:20:59 +0200 Subject: [PATCH 0965/1248] client/NetPacksClient.cpp: Combine null pointer checks Co-authored-by: Nordsoft91 --- client/NetPacksClient.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 7fc6b69bf..664ccd6a0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -367,13 +367,10 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI) + if(CGI && CGI->mh) { - if(CGI->mh) - { - CGI->mh->onObjectFadeOut(obj, pack.initiator); - CGI->mh->waitForOngoingAnimations(); - } + CGI->mh->onObjectFadeOut(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); } } From bb04ca46b5f46b4be7a3b3ce09dabb3cfcb454b1 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 00:21:16 +0200 Subject: [PATCH 0966/1248] client/NetPacksClient.cpp: Combine null pointer checks Co-authored-by: Nordsoft91 --- client/NetPacksClient.cpp | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 664ccd6a0..26183016b 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -377,13 +377,10 @@ void ApplyFirstClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) void ApplyClientNetPackVisitor::visitChangeObjPos(ChangeObjPos & pack) { CGObjectInstance *obj = gs.getObjInstance(pack.objid); - if(CGI) + if(CGI && CGI->mh) { - if(CGI->mh) - { - CGI->mh->onObjectFadeIn(obj, pack.initiator); - CGI->mh->waitForOngoingAnimations(); - } + CGI->mh->onObjectFadeIn(obj, pack.initiator); + CGI->mh->waitForOngoingAnimations(); } cl.invalidatePaths(); } From 703ab677ba0b3628d4ecf68e615ab4be29291e0a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 27 Oct 2023 22:47:56 +0000 Subject: [PATCH 0967/1248] lib/mapObjects/IMarket.cpp: Show error message about failed dynamic_cast() even if cast object is nullptr --- lib/mapObjects/IMarket.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index f4dd54a12..d37662028 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -157,8 +157,14 @@ std::vector IMarket::availableItemsIds(EMarketMode mode) const const IMarket * IMarket::castFrom(const CGObjectInstance *obj, bool verbose) { auto * imarket = dynamic_cast(obj); - if(verbose && !imarket && obj) - logGlobal->error("Cannot cast to IMarket object type %s", obj->typeName); + if(verbose && !imarket) + { + logGlobal->error("Cannot cast to IMarket"); + if(obj) + { + logGlobal->error("Object type %s", obj->typeName); + } + } return imarket; } From 3d8dd35d435aa0044ff7cff95e206b394ed79671 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 10:15:50 +0000 Subject: [PATCH 0968/1248] lib/NetPacksLib.cpp: Directly assign to the "std::optional", without dereferencing it. Assigning to an optional should directly target the optional --- lib/NetPacksLib.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/NetPacksLib.cpp b/lib/NetPacksLib.cpp index 2c67efc48..d77d6709f 100644 --- a/lib/NetPacksLib.cpp +++ b/lib/NetPacksLib.cpp @@ -2100,7 +2100,7 @@ void SetObjectProperty::applyGs(CGameState * gs) const state->towns -= t; if(state->towns.empty()) - *state->daysWithoutCastle = 0; + state->daysWithoutCastle = 0; } if(PlayerColor(val).isValidPlayer()) { From a6db82f6f197bbd513cce733683dc25eab2cf092 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 10:16:33 +0000 Subject: [PATCH 0969/1248] mapeditor/inspector/inspector.cpp: Directly assign to the "std::optional", without dereferencing it. Assigning to an optional should directly target the optiona --- mapeditor/inspector/inspector.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index b0b1e6261..fe81dd27e 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -685,7 +685,7 @@ void Inspector::setProperty(CGHeroPlaceholder * o, const QString & key, const QV if(key == "Hero type") { - o->heroType.value() = HeroTypeID(value.toInt()); + o->heroType = HeroTypeID(value.toInt()); } } From 14590069161d63557b380610116307200f4fa9ed Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 28 Oct 2023 17:54:39 +0300 Subject: [PATCH 0970/1248] Added separate bonus for native terrain check --- docs/modders/Bonus/Bonus_Types.md | 4 ++++ lib/BasicTypes.cpp | 7 +++---- lib/bonuses/BonusEnum.h | 1 + 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 8d5a4c2a8..5acc09854 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -62,6 +62,10 @@ Eliminates terrain penalty on certain terrain types for affected heroes (Nomads - subtype: type of terrain +### TERRAIN_NATIVE + +Affected units will view any terrain as native + ### PRIMARY_SKILL Changes selected primary skill for affected heroes and units diff --git a/lib/BasicTypes.cpp b/lib/BasicTypes.cpp index af58094f0..c1c7b723d 100644 --- a/lib/BasicTypes.cpp +++ b/lib/BasicTypes.cpp @@ -33,14 +33,13 @@ bool INativeTerrainProvider::isNativeTerrain(TerrainId terrain) const TerrainId AFactionMember::getNativeTerrain() const { - constexpr auto any = TerrainId(ETerrainId::ANY_TERRAIN); - const std::string cachingStringNoTerrainPenalty = "type_NO_TERRAIN_PENALTY_sANY"; - static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(any)); + const std::string cachingStringNoTerrainPenalty = "type_TERRAIN_NATIVE_NONE"; + static const auto selectorNoTerrainPenalty = Selector::typeSubtype(BonusType::TERRAIN_NATIVE, BonusSubtypeID()); //this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses //and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties. return getBonusBearer()->hasBonus(selectorNoTerrainPenalty, cachingStringNoTerrainPenalty) - ? any : VLC->factions()->getById(getFaction())->getNativeTerrain(); + ? TerrainId::ANY_TERRAIN : VLC->factions()->getById(getFaction())->getNativeTerrain(); } int32_t AFactionMember::magicResistance() const diff --git a/lib/bonuses/BonusEnum.h b/lib/bonuses/BonusEnum.h index 06cdc8f96..6902508a9 100644 --- a/lib/bonuses/BonusEnum.h +++ b/lib/bonuses/BonusEnum.h @@ -169,6 +169,7 @@ class JsonNode; BONUS_NAME(MAX_LEARNABLE_SPELL_LEVEL) /*This can work as wisdom before. val = max learnable spell level*/\ BONUS_NAME(SPELL_SCHOOL_IMMUNITY) /*This bonus will work as spell school immunity for all spells, subtype - spell school: 0 - air, 1 - fire, 2 - water, 3 - earth. Any is not handled for reducing overlap from LEVEL_SPELL_IMMUNITY*/\ BONUS_NAME(NEGATIVE_EFFECTS_IMMUNITY) /*This bonus will work as spell school immunity for negative effects from spells of school, subtype - spell school: -1 - any, 0 - air, 1 - fire, 2 - water, 3 - earth*/\ + BONUS_NAME(TERRAIN_NATIVE) /* end of list */ From 1e0ea5f1f09e7f753c0a755bc4005e3a0cb201fb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 28 Oct 2023 17:56:11 +0300 Subject: [PATCH 0971/1248] documented NEGATIVE_EFFECTS_IMMUNITY, fixed subtype loading --- docs/modders/Bonus/Bonus_Types.md | 8 +++++++- lib/JsonNode.cpp | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 5acc09854..27516eefe 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -795,7 +795,7 @@ Affected unit is permanently enchanted with a spell, that is cast again every tu Affected unit is immune to all spell with level below or equal to value of this bonus -- val: level to which this unit is immune to +- val: level up to which this unit is immune to TODO: additional info? @@ -897,6 +897,12 @@ Affected unit will never retaliate to an attack (Blind, Paralyze) # Others +### NEGATIVE_EFFECTS_IMMUNITY + +Affected unit is immune to all negative spells of specified spell school + +- subtype: affected spell school + ### BLOCK_MAGIC_ABOVE Blocks casting spells of the level above specified one in battles affected by this bonus diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 76ab8b9a6..353414417 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -446,6 +446,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso case BonusType::SPELLS_OF_SCHOOL: case BonusType::SPELL_DAMAGE_REDUCTION: case BonusType::SPELL_SCHOOL_IMMUNITY: + case BonusType::NEGATIVE_EFFECTS_IMMUNITY: { VLC->identifiers()->requestIdentifier( "spellSchool", node, [&subtype](int32_t identifier) { From 4ba80145739eb5f781ca0e4da5d1bc6474e2b98b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 28 Oct 2023 17:57:56 +0300 Subject: [PATCH 0972/1248] Added subtype for SPELL_DURATION --- docs/modders/Bonus/Bonus_Types.md | 1 + lib/JsonNode.cpp | 1 + lib/mapObjects/CGHeroInstance.cpp | 6 +++++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 27516eefe..0dd612588 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -256,6 +256,7 @@ Allows creature upgrade for affected armies Changes duration of timed spells casted by affected hero - val: additional duration, turns +- subtype: optional, identifier of affected spells, or all if not set ### SPELL diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 353414417..3f5170223 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -486,6 +486,7 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso break; } case BonusType::SPELL_IMMUNITY: + case BonusType::SPELL_DURATION: case BonusType::SPECIAL_ADD_VALUE_ENCHANT: case BonusType::SPECIAL_FIXED_VALUE_ENCHANT: case BonusType::SPECIAL_PECULIAR_ENCHANT: diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index dc22f4ae5..dd7ad037f 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -697,7 +697,11 @@ int32_t CGHeroInstance::getEffectPower(const spells::Spell * spell) const int32_t CGHeroInstance::getEnchantPower(const spells::Spell * spell) const { - return getPrimSkillLevel(PrimarySkill::SPELL_POWER) + valOfBonuses(BonusType::SPELL_DURATION); + int32_t spellpower = getPrimSkillLevel(PrimarySkill::SPELL_POWER); + int32_t durationCommon = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID()); + int32_t durationSpecific = valOfBonuses(BonusType::SPELL_DURATION, BonusSubtypeID(spell->getId())); + + return spellpower + durationCommon + durationSpecific; } int64_t CGHeroInstance::getEffectValue(const spells::Spell * spell) const From 36eacf99d5307ab42dc1896dd1337031e5c8f6d3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 28 Oct 2023 17:58:08 +0300 Subject: [PATCH 0973/1248] Cleanup --- docs/modders/Bonus/Bonus_Types.md | 2 ++ lib/pathfinder/TurnInfo.cpp | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 0dd612588..027bf2ac0 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -60,6 +60,8 @@ Allows flying movement for affected heroes Eliminates terrain penalty on certain terrain types for affected heroes (Nomads ability). +Note: to eliminate all terrain penalties see ROUGH_TERRAIN_DISCOUNT bonus + - subtype: type of terrain ### TERRAIN_NATIVE diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index f9cedc6b3..01ba846ed 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -22,8 +22,8 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) { for(const auto & terrain : VLC->terrainTypeHandler->objects) { - noTerrainPenalty.push_back(static_cast( - bl->getFirst(Selector::type()(BonusType::NO_TERRAIN_PENALTY).And(Selector::subtype()(BonusSubtypeID(terrain->getId())))))); + auto selector = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(terrain->getId())); + noTerrainPenalty.push_back(static_cast(bl->getFirst(selector))); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); From e95439e556a687dd2c89a443a30f34a6021129ab Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 17:13:13 +0200 Subject: [PATCH 0974/1248] Installation_Linux.md: Fix link to Building_Linux.md --- docs/players/Installation_Linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/players/Installation_Linux.md b/docs/players/Installation_Linux.md index e5418a768..009522776 100644 --- a/docs/players/Installation_Linux.md +++ b/docs/players/Installation_Linux.md @@ -63,7 +63,7 @@ If you are interested in providing builds for other distributions, please let us ## Compiling from source -Please check following developer guide: [How to build VCMI (Linux)]((../developers/Building_Linux.md)) +Please check following developer guide: [How to build VCMI (Linux)](../developers/Building_Linux.md) # Step 2: Installing Heroes III data files From f6b03201dbf7e84eb35c50fe6fc8d71ccf259e23 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 28 Oct 2023 18:34:11 +0300 Subject: [PATCH 0975/1248] BattleAction: handle obstacle for standing units --- server/battles/BattleActionProcessor.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 53a70645a..c81b2615e 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -852,6 +852,8 @@ int BattleActionProcessor::moveStack(const CBattleInfoCallback & battle, int sta || start == battle::Unit::occupiedHex(dest, curStack->doubleWide(), curStack->unitSide())) passed.clear(); //Just empty passed, obstacles will handled automatically } + if(dest == start) //If dest is equal to start, then we should handle obstacles for it anyway + passed.clear(); //Just empty passed, obstacles will handled automatically //handling obstacle on the final field (separate, because it affects both flying and walking stacks) battle.handleObstacleTriggersForUnit(*gameHandler->spellEnv, *curStack, passed); From df07b27129a6806fe4c5b2ff4ba618b45fd07e77 Mon Sep 17 00:00:00 2001 From: Konstantin Date: Sat, 28 Oct 2023 18:35:59 +0300 Subject: [PATCH 0976/1248] parseSelector: fix anyOf and noneOf --- lib/JsonNode.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 76ab8b9a6..4cdead98c 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -1156,7 +1156,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) { CSelector base = Selector::none; for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); + base = base.Or(parseSelector(andN)); ret = ret.And(base); } @@ -1166,7 +1166,7 @@ CSelector JsonUtils::parseSelector(const JsonNode & ability) { CSelector base = Selector::none; for(const auto & andN : value->Vector()) - base.Or(parseSelector(andN)); + base = base.Or(parseSelector(andN)); ret = ret.And(base.Not()); } From d686d40bb49f7733172d53d2c41d988bf84efe32 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 15:44:17 +0000 Subject: [PATCH 0977/1248] launcher/firstLaunch/firstlaunch_moc.cpp: "static" members should be accessed statically --- launcher/firstLaunch/firstlaunch_moc.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher/firstLaunch/firstlaunch_moc.cpp b/launcher/firstLaunch/firstlaunch_moc.cpp index cbcc2b7a3..7d09dd688 100644 --- a/launcher/firstLaunch/firstlaunch_moc.cpp +++ b/launcher/firstLaunch/firstlaunch_moc.cpp @@ -150,7 +150,7 @@ void FirstLaunchView::activateTabModPreset() void FirstLaunchView::exitSetup() { - if(auto * mainWindow = dynamic_cast(qApp->activeWindow())) + if(auto * mainWindow = dynamic_cast(QApplication::activeWindow())) mainWindow->exitSetup(); } @@ -160,7 +160,7 @@ void FirstLaunchView::languageSelected(const QString & selectedLanguage) Settings node = settings.write["general"]["language"]; node->String() = selectedLanguage.toStdString(); - if(auto * mainWindow = dynamic_cast(qApp->activeWindow())) + if(auto * mainWindow = dynamic_cast(QApplication::activeWindow())) mainWindow->updateTranslation(); } @@ -398,7 +398,7 @@ bool FirstLaunchView::checkCanInstallExtras() CModListView * FirstLaunchView::getModView() { - auto * mainWindow = dynamic_cast(qApp->activeWindow()); + auto * mainWindow = dynamic_cast(QApplication::activeWindow()); assert(mainWindow); if (!mainWindow) From 9e1629fb40ed0fd90dfe2e369665cd35d34c7dee Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 16:44:58 +0000 Subject: [PATCH 0978/1248] launcher/modManager/cmodlist.cpp: Member functions that don't mutate their objects should be declared "const" --- launcher/modManager/cmodlist.cpp | 2 +- launcher/modManager/cmodlist.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 7e887b5d6..da18609fb 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -181,7 +181,7 @@ QVariant CModEntry::getValueImpl(QString value, bool localized) const return QVariant(); } -QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) +QVariantMap CModList::copyField(QVariantMap data, QString from, QString to) const { QVariantMap renamed; diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index c82a0b88c..41b2f0fae 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -85,7 +85,7 @@ class CModList QVariantMap localModList; QVariantMap modSettings; - QVariantMap copyField(QVariantMap data, QString from, QString to); + QVariantMap copyField(QVariantMap data, QString from, QString to) const; public: virtual void resetRepositories(); From 07dac8b6d418ffa3d943797eb92c36a1bf1cb890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 28 Oct 2023 20:30:38 +0200 Subject: [PATCH 0979/1248] Works more or less --- client/lobby/RandomMapTab.cpp | 150 ++++++++++++------ lib/rmg/CMapGenOptions.cpp | 278 +++++++++++++++++++++++----------- lib/rmg/CMapGenOptions.h | 17 ++- lib/rmg/CMapGenerator.cpp | 160 +++++++++++-------- lib/rmg/CZonePlacer.cpp | 6 +- mapeditor/windownewmap.cpp | 8 +- test/map/CMapFormatTest.cpp | 2 +- 7 files changed, 414 insertions(+), 207 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 443b92082..8398ef767 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -65,7 +65,7 @@ RandomMapTab::RandomMapTab(): addCallback("setPlayersCount", [&](int btnId) { - mapGenOptions->setPlayerCount(btnId); + mapGenOptions->setHumanOrCpuPlayerCount(btnId); setMapGenOptions(mapGenOptions); updateMapInfoByHost(); }); @@ -184,12 +184,9 @@ void RandomMapTab::updateMapInfoByHost() // Generate player information int playersToGen = PlayerColor::PLAYER_LIMIT_I; - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) + if(mapGenOptions->getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) { - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) - playersToGen = mapGenOptions->getPlayerCount() + mapGenOptions->getCompOnlyPlayerCount(); - else - playersToGen = mapGenOptions->getPlayerCount(); + playersToGen = mapGenOptions->getHumanOrCpuPlayerCount(); } mapInfo->mapHeader->howManyTeams = playersToGen; @@ -198,33 +195,39 @@ void RandomMapTab::updateMapInfoByHost() //TODO: Where are human / CPU players toggled in player configuration? //TODO: Get human player count - std::set occupiedTeams; + //std::set occupiedTeams; for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { mapInfo->mapHeader->players[i].canComputerPlay = false; mapInfo->mapHeader->players[i].canHumanPlay = false; } - for(int i = 0; i < playersToGen; ++i) + std::vector availableColors; + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) { - PlayerInfo player; - player.isFactionRandom = true; - player.canComputerPlay = true; - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE && i >= mapGenOptions->getPlayerCount()) - { - player.canHumanPlay = false; - } - else - { - player.canHumanPlay = true; - } - auto team = mapGenOptions->getPlayersSettings().at(PlayerColor(i)).getTeam(); - player.team = team; - occupiedTeams.insert(team); - player.hasMainTown = true; - player.generateHeroAtMainTown = true; - mapInfo->mapHeader->players[i] = player; + availableColors.push_back(PlayerColor(color)); } + + //First restore known players + for (auto& player : mapGenOptions->getPlayersSettings()) + { + PlayerInfo playerInfo; + playerInfo.isFactionRandom = (player.second.getStartingTown() == CMapGenOptions::CPlayerSettings::RANDOM_TOWN); + playerInfo.canComputerPlay = (player.second.getPlayerType() != EPlayerType::HUMAN); + playerInfo.canHumanPlay = (player.second.getPlayerType() != EPlayerType::COMP_ONLY); + + auto team = player.second.getTeam(); + playerInfo.team = team; + //occupiedTeams.insert(team); + playerInfo.hasMainTown = true; + playerInfo.generateHeroAtMainTown = true; + mapInfo->mapHeader->players[player.first] = playerInfo; + vstd::erase(availableColors, player.first); + } + + /* + //Reset teams to default (?) + // TODO: Do not reset teams here, this is handled by CMapGenOptions for(auto & player : mapInfo->mapHeader->players) { for(int i = 0; player.team == TeamID::NO_TEAM; ++i) @@ -238,15 +241,17 @@ void RandomMapTab::updateMapInfoByHost() } } } + */ mapInfoChanged(mapInfo, mapGenOptions); } +// TODO: This method only sets GUI options, doesn't alter any actual configurations done void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { mapGenOptions = opts; - //prepare allowed options + //Prepare allowed options - add all, then erase the ones above the limit for(int i = 0; i <= PlayerColor::PLAYER_LIMIT_I; ++i) { playerCountAllowed.insert(i); @@ -260,39 +265,63 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) compTeamsAllowed.insert(i); } } + int minComps = 0; + auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) { + // TODO: Debug / print actual numbers + // Most templates just skip this setting + auto compNumbers = tmpl->getCpuPlayers().getNumbers(); + if (!compNumbers.empty()) + { + compCountAllowed = compNumbers; + minComps = *boost::min_element(compCountAllowed); + } + playerCountAllowed = tmpl->getPlayers().getNumbers(); - compCountAllowed = tmpl->getCpuPlayers().getNumbers(); + + auto minPlayerCount = *boost::min_element(playerCountAllowed); + auto maxCompCount = *boost::max_element(compCountAllowed); + for (int i = 1; i >= (minPlayerCount - maxCompCount) && i >= 1; i--) + { + //We can always add extra CPUs to meet the minimum total player count + playerCountAllowed.insert(i); + } } - if(mapGenOptions->getPlayerCount() != CMapGenOptions::RANDOM_SIZE) + + si8 playerLimit = opts->getPlayerLimit(); + si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); + si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); + + if(mapGenOptions->getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) { - vstd::erase_if(compCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getPlayerCount() < el; + vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) + { + return (playerLimit - humanOrCpuPlayerCount) < el; }); - vstd::erase_if(playerTeamsAllowed, - [opts](int el){ - return opts->getPlayerCount() <= el; + vstd::erase_if(playerTeamsAllowed, [humanOrCpuPlayerCount](int el) + { + return humanOrCpuPlayerCount <= el; }); if(!playerTeamsAllowed.count(opts->getTeamCount())) + { opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); + } } if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) { - vstd::erase_if(playerCountAllowed, - [opts](int el){ - return PlayerColor::PLAYER_LIMIT_I - opts->getCompOnlyPlayerCount() < el; - }); - vstd::erase_if(compTeamsAllowed, - [opts](int el){ - return opts->getCompOnlyPlayerCount() <= el; + // This setting doesn't impact total number of players + vstd::erase_if(compTeamsAllowed, [compOnlyPlayersCount](int el) + { + return compOnlyPlayersCount<= el; }); if(!compTeamsAllowed.count(opts->getCompOnlyTeamCount())) + { opts->setCompOnlyTeamCount(CMapGenOptions::RANDOM_SIZE); + } } if(auto w = widget("groupMapSize")) @@ -321,7 +350,9 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) } if(auto w = widget("groupMaxPlayers")) { - w->setSelected(opts->getPlayerCount()); + // FIXME: OH3 allows any setting here, even if currently selected template doesn't fit it + // TODO: Set max players to current template limit wherever template is explicitely selected + w->setSelected(opts->getHumanOrCpuPlayerCount()); deactivateButtonsFrom(*w, playerCountAllowed); } if(auto w = widget("groupMaxTeams")) @@ -433,10 +464,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; - int humanPlayers = randomMapTab.obtainMapGenOptions().getPlayerCount(); - int cpuPlayers = randomMapTab.obtainMapGenOptions().getCompOnlyPlayerCount(); - int totalPlayers = humanPlayers == CMapGenOptions::RANDOM_SIZE || cpuPlayers == CMapGenOptions::RANDOM_SIZE - ? PlayerColor::PLAYER_LIMIT_I : humanPlayers + cpuPlayers; + int totalPlayers = randomMapTab.obtainMapGenOptions().getTotalPlayersCount(); assert(totalPlayers <= PlayerColor::PLAYER_LIMIT_I); auto settings = randomMapTab.obtainMapGenOptions().getPlayersSettings(); variables["totalPlayers"].Integer() = totalPlayers; @@ -475,6 +503,29 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): OBJ_CONSTRUCTION; + + // Window should have X * X columns, where X is players + compOnly players. + // For random player count, X is 8 + + if (totalPlayers > settings.size()) + { + auto savedPlayers = randomMapTab.obtainMapGenOptions().getSavedPlayersMap(); + for (const auto & player : savedPlayers) + { + if (!vstd::contains(settings, player.first)) + { + settings[player.first] = player.second; + } + } + } + + std::vector settingsVec; + for (const auto & player : settings) + { + settingsVec.push_back(player.second); + } + + // FIXME: Flag is missing on windows show for(int plId = 0; plId < totalPlayers; ++plId) { players.push_back(std::make_shared([&, totalPlayers, plId](int sel) @@ -501,6 +552,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): })); OBJ_CONSTRUCTION_TARGETED(players.back().get()); + for(int teamId = 0; teamId < totalPlayers; ++teamId) { variables["point"]["x"].Integer() = variables["cellOffset"]["x"].Integer() + plId * variables["cellMargin"]["x"].Integer(); @@ -509,9 +561,13 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): players.back()->addToggle(teamId, std::dynamic_pointer_cast(button)); } - auto team = settings.at(PlayerColor(plId)).getTeam(); + // plId is not neccessarily player color, just an index + auto team = settingsVec.at(plId).getTeam(); if(team == TeamID::NO_TEAM) + { + logGlobal->warn("Player %d (id %d) has uninitialized team", settingsVec.at(plId).getColor(), plId); players.back()->setSelected(plId); + } else players.back()->setSelected(team.getNum()); } diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 492ffeed1..b5920ce15 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -22,8 +22,9 @@ VCMI_LIB_NAMESPACE_BEGIN CMapGenOptions::CMapGenOptions() : width(CMapHeader::MAP_SIZE_MIDDLE), height(CMapHeader::MAP_SIZE_MIDDLE), hasTwoLevels(true), - playerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), - waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr) + humanOrCpuPlayerCount(RANDOM_SIZE), teamCount(RANDOM_SIZE), compOnlyPlayerCount(RANDOM_SIZE), compOnlyTeamCount(RANDOM_SIZE), + waterContent(EWaterContent::RANDOM), monsterStrength(EMonsterStrength::RANDOM), mapTemplate(nullptr), + customizedPlayers(false) { initPlayersMap(); setRoadEnabled(RoadId(Road::DIRT_ROAD), true); @@ -63,23 +64,47 @@ void CMapGenOptions::setHasTwoLevels(bool value) hasTwoLevels = value; } -si8 CMapGenOptions::getPlayerCount() const +si8 CMapGenOptions::getHumanOrCpuPlayerCount() const { - return playerCount; + return humanOrCpuPlayerCount; } -void CMapGenOptions::setPlayerCount(si8 value) +void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) { - assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); - playerCount = value; + // Set total player count (human + AI)? - auto possibleCompPlayersCount = PlayerColor::PLAYER_LIMIT_I - value; + assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); + humanOrCpuPlayerCount = value; + + // Use template player limit, if any + auto playerLimit = getPlayerLimit(); + auto possibleCompPlayersCount = playerLimit - std::max(0, humanOrCpuPlayerCount); if (compOnlyPlayerCount > possibleCompPlayersCount) + { setCompOnlyPlayerCount(possibleCompPlayersCount); + } resetPlayersMap(); } +si8 CMapGenOptions::getTotalPlayersCount() const +{ + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); + if (humans == RANDOM_SIZE || cpus == RANDOM_SIZE) + { + totalPlayers = PlayerColor::PLAYER_LIMIT_I; + } + else + { + totalPlayers = humans + cpus; + } + assert (totalPlayers <= PlayerColor::PLAYER_LIMIT_I); + assert (totalPlayers >= 2); + return totalPlayers; +} + si8 CMapGenOptions::getTeamCount() const { return teamCount; @@ -87,7 +112,7 @@ si8 CMapGenOptions::getTeamCount() const void CMapGenOptions::setTeamCount(si8 value) { - assert(getPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getPlayerCount()) || value == RANDOM_SIZE); + assert(getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value < getHumanOrCpuPlayerCount()) || value == RANDOM_SIZE); teamCount = value; } @@ -96,9 +121,21 @@ si8 CMapGenOptions::getCompOnlyPlayerCount() const return compOnlyPlayerCount; } +si8 CMapGenOptions::getPlayerLimit() const +{ + si8 playerLimit = PlayerColor::PLAYER_LIMIT_I; + if (auto temp = getMapTemplate()) + { + playerLimit = *boost::max_element(temp->getPlayers().getNumbers()); + } + return playerLimit; +} + void CMapGenOptions::setCompOnlyPlayerCount(si8 value) { - assert(value == RANDOM_SIZE || (getPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= PlayerColor::PLAYER_LIMIT_I - getPlayerCount()))); + auto playerLimit = getPlayerLimit(); + + assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= playerLimit - getHumanOrCpuPlayerCount()))); compOnlyPlayerCount = value; resetPlayersMap(); @@ -137,75 +174,69 @@ void CMapGenOptions::setMonsterStrength(EMonsterStrength::EMonsterStrength value void CMapGenOptions::initPlayersMap() { - std::map rememberTownTypes; - std::map rememberTeam; - - for(const auto & p : players) - { - auto town = p.second.getStartingTown(); - if (town != CPlayerSettings::RANDOM_TOWN) - rememberTownTypes[p.first] = FactionID(town); - rememberTeam[p.first] = p.second.getTeam(); - } - - players.clear(); - int realPlayersCnt = playerCount; - int realCompOnlyPlayersCnt = (compOnlyPlayerCount == RANDOM_SIZE) ? (PlayerColor::PLAYER_LIMIT_I - realPlayersCnt) : compOnlyPlayerCount; - int totalPlayersLimit = realPlayersCnt + realCompOnlyPlayersCnt; - if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) - totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); + int realPlayersCnt = getHumanOrCpuPlayerCount(); - for(int color = 0; color < totalPlayersLimit; ++color) + // TODO: Initialize settings for all color even if not present? + for(int color = 0; color < getPlayerLimit(); ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); + + /* + if (vstd::contains(savedPlayerSettings, pc)) + { + player.setTeam(savedPlayerSettings[pc].getTeam()); + player.setStartingTown(savedPlayerSettings[pc].getStartingTown()); + //TODO: Restore starting hero and bonus? + } + // Assign new owner of this player + */ + auto playerType = EPlayerType::AI; - if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) + // Color doesn't have to be continuous. Player colors can later be changed manually + if (getHumanOrCpuPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) { playerType = EPlayerType::HUMAN; } - else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) - || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) + else if((getHumanOrCpuPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) + || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I - compOnlyPlayerCount))) { playerType = EPlayerType::COMP_ONLY; } player.setPlayerType(playerType); - player.setTeam(rememberTeam[pc]); - players[pc] = player; - if (vstd::contains(rememberTownTypes, pc)) - players[pc].setStartingTown(rememberTownTypes[pc]); + players[pc] = player; } + savePlayersMap(); } void CMapGenOptions::resetPlayersMap() { + // Called when number of player changes + // TODO: Should it? + //But do not update info about already made selections - std::map rememberTownTypes; - std::map rememberTeam; - for(const auto & p : players) - { - auto town = p.second.getStartingTown(); - if (town != CPlayerSettings::RANDOM_TOWN) - rememberTownTypes[p.first] = FactionID(town); - rememberTeam[p.first] = p.second.getTeam(); - } + savePlayersMap(); + /* //Remove players who have undefined properties vstd::erase_if(players, [](const std::pair & p) { return p.second.getPlayerType() != EPlayerType::AI && p.second.getStartingTown() == CPlayerSettings::RANDOM_TOWN; }); + */ - int realPlayersCnt = getPlayerCount(); + // FIXME: This should be total players count + int realPlayersCnt = getHumanOrCpuPlayerCount(); if (realPlayersCnt != RANDOM_SIZE) { //Trim the number of AI players, then CPU-only players, finally human players auto eraseLastPlayer = [this](EPlayerType playerType) -> bool { + //FIXME: Infinite loop for 0 players for (auto it = players.rbegin(); it != players.rend(); ++it) { if (it->second.getPlayerType() == playerType) @@ -214,33 +245,28 @@ void CMapGenOptions::resetPlayersMap() return true; } } - return false; + return false; //Can't earse any player of this type }; - while (players.size() < realPlayersCnt) + while (players.size() > realPlayersCnt) { - if (eraseLastPlayer(EPlayerType::AI)) - continue; - if (eraseLastPlayer(EPlayerType::COMP_ONLY)) - continue; - if (eraseLastPlayer(EPlayerType::HUMAN)) - continue; + while (eraseLastPlayer(EPlayerType::AI)); + while (eraseLastPlayer(EPlayerType::COMP_ONLY)); + while (eraseLastPlayer(EPlayerType::HUMAN)); } } + else + { + //If count is random, generate info for all the players + realPlayersCnt = PlayerColor::PLAYER_LIMIT_I; + } - int realCompOnlyPlayersCnt = (compOnlyPlayerCount == RANDOM_SIZE) ? (PlayerColor::PLAYER_LIMIT_I - realPlayersCnt) : compOnlyPlayerCount; - int totalPlayersLimit = realPlayersCnt + realCompOnlyPlayersCnt; - if (getPlayerCount() == RANDOM_SIZE || compOnlyPlayerCount == RANDOM_SIZE) - totalPlayersLimit = static_cast(PlayerColor::PLAYER_LIMIT_I); + int realCompOnlyPlayersCnt = getCompOnlyPlayerCount(); + //int totalPlayersLimit = getPlayerLimit(); //First colors from the list are assigned to human players, then to CPU players - //FIXME: Assign human players colors first - - //TODO: Where is player type is set in void CVCMIServer::updateAndPropagateLobbyState() - //in ApplyOnServerAfterAnnounceNetPackVisitor::visitForLobby - //CPackForLobby std::vector availableColors; - for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; ++color) + for (ui8 color = 0; color < PlayerColor::PLAYER_LIMIT_I; color++) { availableColors.push_back(PlayerColor(color)); } @@ -251,7 +277,7 @@ void CMapGenOptions::resetPlayersMap() { if (player.second.getPlayerType() == playerType) { - vstd::erase(availableColors, player.second.getColor()); + vstd::erase(availableColors, player.second.getColor()); //FIXME: Where is this color initialized at lobby launch? } } }; @@ -259,8 +285,59 @@ void CMapGenOptions::resetPlayersMap() removeUsedColors(EPlayerType::COMP_ONLY); //removeUsedColors(EPlayerType::AI); - //TODO: Assign the remaining colors to random players (AI players) + //Assign unused colors to remaining AI players + while (players.size() < realPlayersCnt && !availableColors.empty()) + { + auto color = availableColors.front(); + setPlayerTypeForStandardPlayer(color, EPlayerType::AI); + players[color].setColor(color); + availableColors.erase(availableColors.begin()); + if (vstd::contains(savedPlayerSettings, color)) + { + setPlayerTeam(color, savedPlayerSettings.at(color).getTeam()); + // TODO: setter + players[color].setStartingTown(savedPlayerSettings.at(color).getStartingTown()); + } + else + { + logGlobal->warn("Adding settings for player %s", color.encode(color)); + // Usually, all players should be initialized in initPlayersMap() + CPlayerSettings settings; + players[color] = settings; + } + } + // TODO: Assign players to teams at the beginning (if all players belong to the same team) + + std::set occupiedTeams; + for(auto & player : players) + { + auto team = player.second.getTeam(); + if (team != TeamID::NO_TEAM) + { + occupiedTeams.insert(team); + } + } + // TODO: Handle situation when we remove a player and remaining players belong to only one team + + for(auto & player : players) + { + if (player.second.getTeam() == TeamID::NO_TEAM) + { + //Find first unused team + for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) + { + TeamID team(i); + if(!occupiedTeams.count(team)) + { + player.second.setTeam(team); + occupiedTeams.insert(team); + break; + } + } + } + } + /* for(int color = 0; color < totalPlayersLimit; ++color) { CPlayerSettings player; @@ -284,6 +361,21 @@ void CMapGenOptions::resetPlayersMap() if (vstd::contains(rememberTownTypes, pc)) players[pc].setStartingTown(rememberTownTypes[pc]); } + */ +} + +void CMapGenOptions::savePlayersMap() +{ + //Only save already configured players + for (const auto& player : players) + { + savedPlayerSettings[player.first] = player.second; + } +} + +const std::map & CMapGenOptions::getSavedPlayersMap() const +{ + return savedPlayerSettings; } const std::map & CMapGenOptions::getPlayersSettings() const @@ -294,16 +386,18 @@ const std::map & CMapGenOptions::g void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, si32 town) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setStartingTown(town); } void CMapGenOptions::setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType) { + // FIXME: Why actually not set it to COMP_ONLY? Ie. when swapping human to another color? assert(playerType != EPlayerType::COMP_ONLY); auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); it->second.setPlayerType(playerType); + customizedPlayers = true; } const CRmgTemplate * CMapGenOptions::getMapTemplate() const @@ -325,8 +419,8 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) setHasTwoLevels(sizes.first.z - 1); } - if(!mapTemplate->getPlayers().isInRange(getPlayerCount())) - setPlayerCount(RANDOM_SIZE); + if(!mapTemplate->getPlayers().isInRange(getHumanOrCpuPlayerCount())) + setHumanOrCpuPlayerCount(RANDOM_SIZE); if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) setCompOnlyPlayerCount(RANDOM_SIZE); if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) @@ -365,15 +459,17 @@ bool CMapGenOptions::isRoadEnabled() const void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & team) { auto it = players.find(color); - if(it == players.end()) assert(0); + assert(it != players.end()); + // TODO: Make pivate friend method to avoid unintended changes? it->second.setTeam(team); + customizedPlayers = true; } void CMapGenOptions::finalize(CRandomGenerator & rand) { logGlobal->info("RMG map: %dx%d, %s underground", getWidth(), getHeight(), getHasTwoLevels() ? "WITH" : "NO"); logGlobal->info("RMG settings: players %d, teams %d, computer players %d, computer teams %d, water %d, monsters %d", - static_cast(getPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), + static_cast(getHumanOrCpuPlayerCount()), static_cast(getTeamCount()), static_cast(getCompOnlyPlayerCount()), static_cast(getCompOnlyTeamCount()), static_cast(getWaterContent()), static_cast(getMonsterStrength())); if(!mapTemplate) @@ -384,18 +480,18 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) logGlobal->info("RMG template name: %s", mapTemplate->getName()); - if (getPlayerCount() == RANDOM_SIZE) + if (getHumanOrCpuPlayerCount() == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); //ignore all non-randomized players, make sure these players will not be missing after roll possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers())); assert(!possiblePlayers.empty()); - setPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); + setHumanOrCpuPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); updatePlayers(); } if(teamCount == RANDOM_SIZE) { - teamCount = rand.nextInt(getPlayerCount() - 1); + teamCount = rand.nextInt(getHumanOrCpuPlayerCount() - 1); if (teamCount == 1) teamCount = 0; } @@ -432,11 +528,6 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) assert (vstd::iswithin(waterContent, EWaterContent::NONE, EWaterContent::ISLANDS)); assert (vstd::iswithin(monsterStrength, EMonsterStrength::GLOBAL_WEAK, EMonsterStrength::GLOBAL_STRONG)); - - //rectangular maps are the future of gaming - //setHeight(20); - //setWidth(50); - logGlobal->trace("Player config:"); int cpuOnlyPlayers = 0; for(const auto & player : players) @@ -457,21 +548,25 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) default: assert(false); } + // FIXME: Every player is player 0 with type of AI + // FIXME: player.first != player.second.getColor() + // TODO: Set player color everywhere players is set, or only once here logGlobal->trace("Player %d: %s", player.second.getColor(), playerType); } - setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automaticlaly (?) + // FXIME: Do not set this again after options were set + setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automatically (?) logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), static_cast(getCompOnlyPlayerCount())); } void CMapGenOptions::updatePlayers() { - // Remove AI players only from the end of the players map if necessary + // Remove non-human players only from the end of the players map if necessary for(auto itrev = players.end(); itrev != players.begin();) { auto it = itrev; --it; - if (players.size() == getPlayerCount()) break; - if(it->second.getPlayerType() == EPlayerType::AI) + if (players.size() == getHumanOrCpuPlayerCount()) break; + if(it->second.getPlayerType() != EPlayerType::HUMAN) { players.erase(it); } @@ -489,7 +584,7 @@ void CMapGenOptions::updateCompOnlyPlayers() { auto it = itrev; --it; - if (players.size() <= getPlayerCount()) break; + if (players.size() <= getHumanOrCpuPlayerCount()) break; if(it->second.getPlayerType() == EPlayerType::COMP_ONLY) { players.erase(it); @@ -501,11 +596,11 @@ void CMapGenOptions::updateCompOnlyPlayers() } // Add some comp only players if necessary - int compOnlyPlayersToAdd = static_cast(getPlayerCount() - players.size()); + int compOnlyPlayersToAdd = static_cast(getHumanOrCpuPlayerCount() - players.size()); if (compOnlyPlayersToAdd < 0) { - logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", playerCount, players.size()); + logGlobal->error("Incorrect number of players to add. Requested players %d, current players %d", humanOrCpuPlayerCount, players.size()); assert (compOnlyPlayersToAdd < 0); } for(int i = 0; i < compOnlyPlayersToAdd; ++i) @@ -561,6 +656,11 @@ bool CMapGenOptions::checkOptions() const } } +bool CMapGenOptions::arePlayersCustomized() const +{ + return customizedPlayers; +} + std::vector CMapGenOptions::getPossibleTemplates() const { int3 tplSize(width, height, (hasTwoLevels ? 2 : 1)); @@ -576,9 +676,9 @@ std::vector CMapGenOptions::getPossibleTemplates() const if(!tmpl->isWaterContentAllowed(getWaterContent())) return true; - if(getPlayerCount() != -1) + if(getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getPlayers().isInRange(getPlayerCount())) + if (!tmpl->getPlayers().isInRange(getHumanOrCpuPlayerCount())) return true; } else @@ -588,7 +688,7 @@ std::vector CMapGenOptions::getPossibleTemplates() const return true; } - if(compOnlyPlayerCount != -1) + if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { if (!tmpl->getCpuPlayers().isInRange(compOnlyPlayerCount)) return true; diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 65b089722..7ea58bf5d 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -88,8 +88,11 @@ public: /// The count of all (human or computer) players ranging from 1 to PlayerColor::PLAYER_LIMIT or RANDOM_SIZE for random. If you call /// this method, all player settings are reset to default settings. - si8 getPlayerCount() const; - void setPlayerCount(si8 value); + si8 getHumanOrCpuPlayerCount() const; + void setHumanOrCpuPlayerCount(si8 value); + + si8 getTotalPlayersCount() const; + si8 getPlayerLimit() const; /// The count of the teams ranging from 0 to or RANDOM_SIZE for random. si8 getTeamCount() const; @@ -117,6 +120,7 @@ public: /// The first player colors belong to standard players and the last player colors belong to comp only players. /// All standard players are by default of type EPlayerType::AI. const std::map & getPlayersSettings() const; + const std::map & getSavedPlayersMap() const; void setStartingTownForPlayer(const PlayerColor & color, si32 town); /// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The /// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN. @@ -139,12 +143,15 @@ public: /// Returns false if there is no template available which fits to the currently selected options. bool checkOptions() const; + /// Returns true if player colors or teams were set in game GUI + bool arePlayersCustomized() const; static const si8 RANDOM_SIZE = -1; private: void initPlayersMap(); void resetPlayersMap(); + void savePlayersMap(); int countHumanPlayers() const; int countCompOnlyPlayers() const; PlayerColor getNextPlayerColor() const; @@ -154,11 +161,13 @@ private: si32 width, height; bool hasTwoLevels; - si8 playerCount, teamCount, compOnlyPlayerCount, compOnlyTeamCount; + si8 humanOrCpuPlayerCount, teamCount, compOnlyPlayerCount, compOnlyTeamCount; EWaterContent::EWaterContent waterContent; EMonsterStrength::EMonsterStrength monsterStrength; std::map players; + std::map savedPlayerSettings; std::set enabledRoads; + bool customizedPlayers; const CRmgTemplate * mapTemplate; @@ -169,7 +178,7 @@ public: h & width; h & height; h & hasTwoLevels; - h & playerCount; + h & humanOrCpuPlayerCount; h & teamCount; h & compOnlyPlayerCount; h & compOnlyTeamCount; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 4a3e919d8..be317d717 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -104,7 +104,7 @@ void CMapGenerator::initPrisonsRemaining() if (isAllowed) allowedPrisons++; } - allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getPlayerCount()); //so at least 16 heroes will be available for every player + allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getHumanOrCpuPlayerCount()); //so at least 16 heroes will be available for every player } void CMapGenerator::initQuestArtsRemaining() @@ -162,7 +162,7 @@ std::string CMapGenerator::getMapDescription() const std::stringstream ss; ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") + ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() % - map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getPlayerCount()) % + map->width() % map->height() % static_cast(map->levels()) % static_cast(mapGenOptions.getHumanOrCpuPlayerCount()) % static_cast(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] % monsterStrengthStr[monsterStrengthIndex]); @@ -185,84 +185,120 @@ std::string CMapGenerator::getMapDescription() const void CMapGenerator::addPlayerInfo() { - // Calculate which team numbers exist + // Teams are already configured in CMapGenOptions. However, it's not the case when it comes to map editor - enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class - std::array, 2> teamNumbers; std::set teamsTotal; - int teamOffset = 0; - int playerCount = 0; - int teamCount = 0; - - for (int i = CPHUMAN; i < AFTER_LAST; ++i) + if (mapGenOptions.arePlayersCustomized()) { - if (i == CPHUMAN) - { - playerCount = mapGenOptions.getPlayerCount(); - teamCount = mapGenOptions.getTeamCount(); - } - else - { - playerCount = mapGenOptions.getCompOnlyPlayerCount(); - teamCount = mapGenOptions.getCompOnlyTeamCount(); - } + // Simply copy existing settings set in GUI - if(playerCount == 0) + for (const auto & player : mapGenOptions.getPlayersSettings()) { - continue; + PlayerInfo playerInfo; + playerInfo.team = player.second.getTeam(); + if (player.second.getPlayerType() == EPlayerType::COMP_ONLY) + { + playerInfo.canHumanPlay = false; + } + else + { + playerInfo.canHumanPlay = true; + } + map->getMap(this).players[player.first.getNum()] = playerInfo; + teamsTotal.insert(player.second.getTeam()); } - int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); - int teamCountNorm = teamCount; - if(teamCountNorm == 0) + } + else + { + // Assign standard teams (in map editor) + + // Calculate which team numbers exist + + enum ETeams {CPHUMAN = 0, CPUONLY = 1, AFTER_LAST = 2}; // Used as a kind of a local named array index, so left as enum, not enum class + std::array, 2> teamNumbers; + + int teamOffset = 0; + int playerCount = 0; + int teamCount = 0; + + // FIXME: Player can be any color, not just 0 + for (int i = CPHUMAN; i < AFTER_LAST; ++i) { - teamCountNorm = playerCount; - } - for(int j = 0; j < teamCountNorm; ++j) - { - for(int k = 0; k < playersPerTeam; ++k) + if (i == CPHUMAN) + { + playerCount = mapGenOptions.getHumanOrCpuPlayerCount(); + teamCount = mapGenOptions.getTeamCount(); + } + else + { + playerCount = mapGenOptions.getCompOnlyPlayerCount(); + teamCount = mapGenOptions.getCompOnlyTeamCount(); + } + + if(playerCount == 0) + { + continue; + } + int playersPerTeam = playerCount / (teamCount == 0 ? playerCount : teamCount); + int teamCountNorm = teamCount; + if(teamCountNorm == 0) + { + teamCountNorm = playerCount; + } + for(int j = 0; j < teamCountNorm; ++j) + { + for(int k = 0; k < playersPerTeam; ++k) + { + teamNumbers[i].push_back(j + teamOffset); + } + } + for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) { teamNumbers[i].push_back(j + teamOffset); } + teamOffset += teamCountNorm; } - for(int j = 0; j < playerCount - teamCountNorm * playersPerTeam; ++j) - { - teamNumbers[i].push_back(j + teamOffset); - } - teamOffset += teamCountNorm; - } + logGlobal->info("Current player settings size: %d", mapGenOptions.getPlayersSettings().size()); - // Team numbers are assigned randomly to every player - //TODO: allow customize teams in rmg template - for(const auto & pair : mapGenOptions.getPlayersSettings()) - { - const auto & pSettings = pair.second; - PlayerInfo player; - player.canComputerPlay = true; - int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; - if (j == CPHUMAN) + // Team numbers are assigned randomly to every player + //TODO: allow to customize teams in rmg template + for(const auto & pair : mapGenOptions.getPlayersSettings()) { - player.canHumanPlay = true; - } - - if(pSettings.getTeam() != TeamID::NO_TEAM) - { - player.team = pSettings.getTeam(); - } - else - { - if (teamNumbers[j].empty()) + const auto & pSettings = pair.second; + PlayerInfo player; + player.canComputerPlay = true; + int j = (pSettings.getPlayerType() == EPlayerType::COMP_ONLY) ? CPUONLY : CPHUMAN; + if (j == CPHUMAN) { - logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); - assert (teamNumbers[j].size()); + player.canHumanPlay = true; } - auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); - player.team = TeamID(*itTeam); - teamNumbers[j].erase(itTeam); + + if(pSettings.getTeam() != TeamID::NO_TEAM) + { + player.team = pSettings.getTeam(); + } + else + { + if (teamNumbers[j].empty()) + { + logGlobal->error("Not enough places in team for %s player", ((j == CPUONLY) ? "CPU" : "CPU or human")); + assert (teamNumbers[j].size()); + } + auto itTeam = RandomGeneratorUtil::nextItem(teamNumbers[j], rand); + player.team = TeamID(*itTeam); + teamNumbers[j].erase(itTeam); + } + teamsTotal.insert(player.team.getNum()); + map->getMap(this).players[pSettings.getColor().getNum()] = player; } - teamsTotal.insert(player.team.getNum()); - map->getMap(this).players[pSettings.getColor().getNum()] = player; + + logGlobal->info("Current team count: %d", teamsTotal.size()); + } + // FIXME: 0 + // Can't find info for player 0 (starting zone) + // Can't find info for player 1 (starting zone) map->getMap(this).howManyTeams = teamsTotal.size(); } diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 07c56688f..79cca6f5f 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -444,9 +444,13 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const auto playerSettings = map.getMapGenOptions().getPlayersSettings(); si32 faction = FactionID::RANDOM; if (vstd::contains(playerSettings, player)) + { faction = playerSettings[player].getStartingTown(); + } else - logGlobal->error("Can't find info for player %d (starting zone)", player.getNum()); + { + logGlobal->trace("Player %d (starting zone %d) does not participate in game", player.getNum(), zone.first); + } if (faction == FactionID::RANDOM) //TODO: check this after a town has already been randomized zonesToPlace.push_back(zone); diff --git a/mapeditor/windownewmap.cpp b/mapeditor/windownewmap.cpp index 68e3b70a7..bfe436e99 100644 --- a/mapeditor/windownewmap.cpp +++ b/mapeditor/windownewmap.cpp @@ -332,7 +332,7 @@ void WindowNewMap::on_humanCombo_activated(int index) ui->humanCombo->setCurrentIndex(humans); } - mapGenOptions.setPlayerCount(humans); + mapGenOptions.setHumanOrCpuPlayerCount(humans); int teams = mapGenOptions.getTeamCount(); if(teams > humans - 1) @@ -361,8 +361,10 @@ void WindowNewMap::on_humanCombo_activated(int index) void WindowNewMap::on_cpuCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int cpu = ui->cpuCombo->currentData().toInt(); + + // FIXME: Use mapGenOption method only to calculate actual number of players for current template if(cpu > PlayerColor::PLAYER_LIMIT_I - humans) { cpu = PlayerColor::PLAYER_LIMIT_I - humans; @@ -455,7 +457,7 @@ void WindowNewMap::on_checkSeed_toggled(bool checked) void WindowNewMap::on_humanTeamsCombo_activated(int index) { - int humans = mapGenOptions.getPlayerCount(); + int humans = mapGenOptions.getHumanOrCpuPlayerCount(); int teams = ui->humanTeamsCombo->currentData().toInt(); if(teams >= humans) { diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index 647a76907..f11f20543 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -55,7 +55,7 @@ TEST(MapFormat, Random) opt.setHeight(CMapHeader::MAP_SIZE_MIDDLE); opt.setWidth(CMapHeader::MAP_SIZE_MIDDLE); opt.setHasTwoLevels(true); - opt.setPlayerCount(4); + opt.setHumanOrCpuPlayerCount(4); opt.setPlayerTypeForStandardPlayer(PlayerColor(0), EPlayerType::HUMAN); opt.setPlayerTypeForStandardPlayer(PlayerColor(1), EPlayerType::AI); From 35e7fbb36665e10e066a886a204ca1d8561d6164 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Sat, 28 Oct 2023 22:22:30 +0200 Subject: [PATCH 0980/1248] Fix crash cause by duplicated delete --- lib/rmg/modificators/ObjectManager.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 9d34b3833..70b3a6a51 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -447,7 +447,6 @@ bool ObjectManager::createRequiredObjects() instance->object().getObjectName(), instance->getPosition(true).toString()); mapProxy->removeObject(&instance->object()); } - rmgNearObject.clear(); } } From e4db6f2af8bb199a2f1ec388552bb0fe25036276 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 17:09:07 +0000 Subject: [PATCH 0981/1248] CClient::removeGUI(): This function should be declared 'const' --- client/Client.cpp | 2 +- client/Client.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/Client.cpp b/client/Client.cpp index d01828b97..c6e99d405 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -698,7 +698,7 @@ void CClient::reinitScripting() #endif } -void CClient::removeGUI() +void CClient::removeGUI() const { // CClient::endGame GH.curInt = nullptr; diff --git a/client/Client.h b/client/Client.h index 53c785fda..2be88e55a 100644 --- a/client/Client.h +++ b/client/Client.h @@ -216,7 +216,7 @@ public: void showInfoDialog(InfoWindow * iw) override {}; void showInfoDialog(const std::string & msg, PlayerColor player) override {}; - void removeGUI(); + void removeGUI() const; #if SCRIPTING_ENABLED scripting::Pool * getGlobalContextPool() const override; From 248877245e46efa977bb820030a3592ba09bd3bc Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 22:13:36 +0000 Subject: [PATCH 0982/1248] Improve ccache hit rate for PRs --- .github/workflows/github.yml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index cadab3783..aa0632816 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -166,18 +166,36 @@ jobs: env: VCMI_BUILD_PLATFORM: x64 - - name: ccache + # ensure the ccache for each PR is separate so they don't interfere with each other + # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found + - name: Ccache for PRs uses: hendrikmuhs/ccache-action@v1.2 + if: ${{ github.event.number != '' }} with: - key: ${{ matrix.preset }} - # actual cache takes up less space, at most ~1 GB - max-size: "5G" - verbose: 2 + key: ${{ matrix.preset }}-PR-${{ github.event.number }} + restore-keys: | + ${{ matrix.preset }}-PR-${{ github.event.number }} + ${{ matrix.preset }}-no-PR + # actual cache takes up less space, at most ~1 GB + max-size: "5G" + verbose: 2 + + - name: Ccache for everything but PRs + uses: hendrikmuhs/ccache-action@v1.2 + if: ${{ github.event.number == '' }} + with: + key: ${{ matrix.preset }}-no-PR + restore-keys: | + ${{ matrix.preset }}-no-PR + # actual cache takes up less space, at most ~1 GB + max-size: "5G" + verbose: 2 - uses: actions/setup-python@v4 if: "${{ matrix.conan_profile != '' }}" with: python-version: '3.10' + - name: Conan setup if: "${{ matrix.conan_profile != '' }}" run: | From 3025785839a8a31ea573ec21a99a25933a3cf7e1 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 29 Oct 2023 09:29:57 +0200 Subject: [PATCH 0983/1248] BattleAI: fix freeze fighting in dwelling --- server/battles/BattleProcessor.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index 9a443b0c9..3c1ed522d 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -100,9 +100,6 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm assert(gameHandler->gameState()->getBattle(army1->getOwner()) == nullptr); assert(gameHandler->gameState()->getBattle(army2->getOwner()) == nullptr); - engageIntoBattle(army1->tempOwner); - engageIntoBattle(army2->tempOwner); - const CArmedInstance *armies[2]; armies[0] = army1; armies[1] = army2; From 4d6d64837f7f9c29fcbb871084ca878c8be36afc Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 29 Oct 2023 09:41:22 +0200 Subject: [PATCH 0984/1248] #2993 - destruct battle info first when destruction CGameState --- lib/gameState/CGameState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c349e851d..2a8ed2955 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -409,6 +409,7 @@ CGameState::CGameState() CGameState::~CGameState() { + currentBattles.clear(); map.dellNull(); } From b0830a837baabcdabd89ca5698d774567c05bb67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 29 Oct 2023 09:35:38 +0100 Subject: [PATCH 0985/1248] Fixed template with no starting towns for players 3 and 4. --- Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index b93afd0c0..bf6bc85e0 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -2,7 +2,8 @@ "2SM2i(2)" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-4", + "players" : "2", + "cpu": "1-2", "zones" : { "1" : @@ -32,7 +33,7 @@ }, "3" : { - "type" : "playerStart", + "type" : "treasure", "size" : 11, "monsters" : "normal", "minesLikeZone" : 1, @@ -54,7 +55,7 @@ }, "5" : { - "type" : "playerStart", + "type" : "treasure", "size" : 11, "monsters" : "normal", "neutralTowns" : { "towns" : 1 }, From b579ca8a3300512c6407526be39185e64448b2c2 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 16 Sep 2023 19:34:50 +0300 Subject: [PATCH 0986/1248] #1912 trap, exception on adding duplicating hero --- lib/mapObjects/CGHeroInstance.cpp | 22 ++++++++++++++++++++++ mapeditor/mainwindow.cpp | 4 ++++ 2 files changed, 26 insertions(+) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 5c8952beb..2a7d14149 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -37,6 +37,7 @@ #include "../modding/ModScope.h" #include "../constants/StringConstants.h" #include "../battle/Unit.h" +#include "CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -1504,8 +1505,29 @@ std::string CGHeroInstance::getHeroTypeName() const void CGHeroInstance::afterAddToMap(CMap * map) { + auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool + { + return (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; + }); + + if(existingHero != map->objects.end()) + { + if(settings["session"]["editor"].Bool()) + { + logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + } + else + { + logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + + throw std::runtime_error("Hero is already on the map"); + } + } + if(ID == Obj::HERO) + { map->heroesOnMap.emplace_back(this); + } } void CGHeroInstance::afterRemoveFromMap(CMap* map) { diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 680e4337a..aaf5b19f5 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -68,6 +68,10 @@ QPixmap pixmapFromJson(const QJsonValue &val) void init() { loadDLLClasses(); + + Settings config = settings.write["session"]["editor"]; + config->Bool() = true; + logGlobal->info("Initializing VCMI_Lib"); } From 368a18500897c26c856b015babfe61de0cf9cea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 29 Oct 2023 10:05:24 +0100 Subject: [PATCH 0987/1248] Cleaning up the code --- client/lobby/RandomMapTab.cpp | 41 ++++++++--------------------------- lib/rmg/CMapGenOptions.cpp | 25 ++++++++------------- 2 files changed, 18 insertions(+), 48 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 8398ef767..737880184 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -191,11 +191,8 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->mapHeader->howManyTeams = playersToGen; - //FIXME: Assign all human-controlled colors in first place - //TODO: Where are human / CPU players toggled in player configuration? - //TODO: Get human player count + //TODO: Assign all human-controlled colors in first place - //std::set occupiedTeams; for(int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i) { mapInfo->mapHeader->players[i].canComputerPlay = false; @@ -225,28 +222,10 @@ void RandomMapTab::updateMapInfoByHost() vstd::erase(availableColors, player.first); } - /* - //Reset teams to default (?) - // TODO: Do not reset teams here, this is handled by CMapGenOptions - for(auto & player : mapInfo->mapHeader->players) - { - for(int i = 0; player.team == TeamID::NO_TEAM; ++i) - { - TeamID team(i); - if(!occupiedTeams.count(team)) - { - player.team = team; - occupiedTeams.insert(team); - break; //First assigned team is ok - } - } - } - */ - mapInfoChanged(mapInfo, mapGenOptions); } -// TODO: This method only sets GUI options, doesn't alter any actual configurations done +// This method only sets GUI options, doesn't alter any actual configurations done void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { mapGenOptions = opts; @@ -265,13 +244,12 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) compTeamsAllowed.insert(i); } } - int minComps = 0; auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) { - // TODO: Debug / print actual numbers - // Most templates just skip this setting + playerCountAllowed = tmpl->getPlayers().getNumbers(); + compCountAllowed = tmpl->getCpuPlayers().getNumbers(); auto compNumbers = tmpl->getCpuPlayers().getNumbers(); if (!compNumbers.empty()) { @@ -292,9 +270,9 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) si8 playerLimit = opts->getPlayerLimit(); si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); - si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); + si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); - if(mapGenOptions->getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) { vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) { @@ -310,7 +288,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); } } - if(mapGenOptions->getCompOnlyPlayerCount() != CMapGenOptions::RANDOM_SIZE) + if(compOnlyPlayersCount != CMapGenOptions::RANDOM_SIZE) { // This setting doesn't impact total number of players vstd::erase_if(compTeamsAllowed, [compOnlyPlayersCount](int el) @@ -464,7 +442,8 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; - int totalPlayers = randomMapTab.obtainMapGenOptions().getTotalPlayersCount(); + int totalPlayers = randomMapTab.obtainMapGenOptions().getPlayerLimit(); + //randomMapTab.obtainMapGenOptions().getTotalPlayersCount(); assert(totalPlayers <= PlayerColor::PLAYER_LIMIT_I); auto settings = randomMapTab.obtainMapGenOptions().getPlayersSettings(); variables["totalPlayers"].Integer() = totalPlayers; @@ -503,7 +482,6 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): OBJ_CONSTRUCTION; - // Window should have X * X columns, where X is players + compOnly players. // For random player count, X is 8 @@ -525,7 +503,6 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): settingsVec.push_back(player.second); } - // FIXME: Flag is missing on windows show for(int plId = 0; plId < totalPlayers; ++plId) { players.push_back(std::make_shared([&, totalPlayers, plId](int sel) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index b5920ce15..a2a741e08 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -71,8 +71,6 @@ si8 CMapGenOptions::getHumanOrCpuPlayerCount() const void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) { - // Set total player count (human + AI)? - assert((value >= 1 && value <= PlayerColor::PLAYER_LIMIT_I) || value == RANDOM_SIZE); humanOrCpuPlayerCount = value; @@ -177,23 +175,13 @@ void CMapGenOptions::initPlayersMap() players.clear(); int realPlayersCnt = getHumanOrCpuPlayerCount(); - // TODO: Initialize settings for all color even if not present? + // Initialize settings for all color even if not present for(int color = 0; color < getPlayerLimit(); ++color) { CPlayerSettings player; auto pc = PlayerColor(color); player.setColor(pc); - /* - if (vstd::contains(savedPlayerSettings, pc)) - { - player.setTeam(savedPlayerSettings[pc].getTeam()); - player.setStartingTown(savedPlayerSettings[pc].getStartingTown()); - //TODO: Restore starting hero and bonus? - } - // Assign new owner of this player - */ - auto playerType = EPlayerType::AI; // Color doesn't have to be continuous. Player colors can later be changed manually if (getHumanOrCpuPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) @@ -215,8 +203,6 @@ void CMapGenOptions::initPlayersMap() void CMapGenOptions::resetPlayersMap() { // Called when number of player changes - // TODO: Should it? - //But do not update info about already made selections savePlayersMap(); @@ -307,7 +293,6 @@ void CMapGenOptions::resetPlayersMap() players[color] = settings; } } - // TODO: Assign players to teams at the beginning (if all players belong to the same team) std::set occupiedTeams; for(auto & player : players) @@ -407,6 +392,12 @@ const CRmgTemplate * CMapGenOptions::getMapTemplate() const void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) { + if (mapTemplate == value) + { + //Does not trigger during deserialization + return; + } + mapTemplate = value; //validate & adapt options according to template if(mapTemplate) @@ -419,6 +410,8 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) setHasTwoLevels(sizes.first.z - 1); } + // FIXME: GUI settings are not the same as template parameters + // TODO: Recalculate GUI ranges in separate method if(!mapTemplate->getPlayers().isInRange(getHumanOrCpuPlayerCount())) setHumanOrCpuPlayerCount(RANDOM_SIZE); if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) From be5505690d6019eaf425d55f79ce88a85f17f81c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 29 Oct 2023 13:47:56 +0200 Subject: [PATCH 0988/1248] Apply suggestions from code review Co-authored-by: Nordsoft91 --- lib/mapObjectConstructors/CObjectClassesHandler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 81f8ba01f..9d62b028a 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -194,9 +194,9 @@ TObjectTypeHandler CObjectClassesHandler::loadSubObjectFromJson(const std::strin assert(!scope.empty()); std::string handler = obj->handlerName; - if(!handlerConstructors.count(obj->handlerName)) + if(!handlerConstructors.count(handler)) { - logMod->error("Handler with name %s was not found!", obj->handlerName); + logMod->error("Handler with name %s was not found!", handler); // workaround for potential crash - if handler does not exists, continue with generic handler that is used for objects without any custom logc handler = "generic"; assert(handlerConstructors.count(handler) != 0); From f1bb6b999c384d3566498fd1cd9fbeb7a32ec9df Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 17:29:50 +0000 Subject: [PATCH 0989/1248] client/widgets/Buttons.{h,cpp}: Pass expensive to copy object "callback" by reference to const. --- client/widgets/Buttons.cpp | 2 +- client/widgets/Buttons.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index d84fc4b73..1e7badfa0 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -66,7 +66,7 @@ void CButton::setBorderColor(std::optional newBorderColor) borderColor = newBorderColor; } -void CButton::addCallback(std::function callback) +void CButton::addCallback(std::function const & callback) { this->callback += callback; } diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index 8944d2928..fda839940 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -67,7 +67,7 @@ public: void setBorderColor(std::optional borderColor); /// adds one more callback to on-click actions - void addCallback(std::function callback); + void addCallback(std::function const & callback); /// adds overlay on top of button image. Only one overlay can be active at once void addOverlay(std::shared_ptr newOverlay); From 466cdb9d2b36c6841fadad2be09ed4da39ef2608 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 17:34:18 +0000 Subject: [PATCH 0990/1248] client/widgetsImages.{h,cpp}: implicit conversion loses integer precision: 'int' to 'uint8_t' (aka 'unsigned char') --- client/widgets/Images.cpp | 2 +- client/widgets/Images.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index cac50a7eb..76ccd2838 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -96,7 +96,7 @@ void CPicture::showAll(Canvas & to) } } -void CPicture::setAlpha(int value) +void CPicture::setAlpha(uint8_t value) { bg->setAlpha(value); } diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 0e2fcf5a0..2a9f3ae10 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -56,7 +56,7 @@ public: /// set alpha value for whole surface. Note: may be messed up if surface is shared /// 0=transparent, 255=opaque - void setAlpha(int value); + void setAlpha(uint8_t value); void scaleTo(Point size); void colorize(PlayerColor player); From bfddc90ff46348a8ca8b6dc1994c97a250954e73 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 17:56:21 +0000 Subject: [PATCH 0991/1248] AI/BattleAI.cpp: Use "std::array" or "std::vector" instead of a C-style array. Variables of array type should not be declared cpp:M23_356 --- AI/BattleAI/BattleAI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 45e1d626e..6881060e9 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -197,7 +197,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st } else { - EWallPart wallParts[] = { + std::array wallParts { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER, From 0232ae5327542e4340d2948718fe7b9f60c3381b Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sat, 28 Oct 2023 18:01:37 +0000 Subject: [PATCH 0992/1248] AI/BattleAI/BattleEvaluator.cpp: Convert this integer literal to a bool literal. Integer literals should not be cast to bool --- AI/BattleAI/BattleEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleEvaluator.cpp b/AI/BattleAI/BattleEvaluator.cpp index b52c04c25..4e8be4fdf 100644 --- a/AI/BattleAI/BattleEvaluator.cpp +++ b/AI/BattleAI/BattleEvaluator.cpp @@ -307,7 +307,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector else { BattleHex currentDest = bestNeighbor; - while(1) + while(true) { if(!currentDest.isValid()) { From 5d7b83c10b8b293f160db11ae76d941e1d313573 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 29 Oct 2023 00:55:25 +0200 Subject: [PATCH 0993/1248] client/widgets/Buttons.cpp: Fix const reference Co-authored-by: Andrey Filipenkov --- client/widgets/Buttons.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/widgets/Buttons.cpp b/client/widgets/Buttons.cpp index 1e7badfa0..b4706fa3a 100644 --- a/client/widgets/Buttons.cpp +++ b/client/widgets/Buttons.cpp @@ -66,7 +66,7 @@ void CButton::setBorderColor(std::optional newBorderColor) borderColor = newBorderColor; } -void CButton::addCallback(std::function const & callback) +void CButton::addCallback(const std::function & callback) { this->callback += callback; } From e677164ed07f0ebb1e7a2440ddb998b75f91ca7d Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 29 Oct 2023 00:55:42 +0200 Subject: [PATCH 0994/1248] client/widgets/Buttons.h: Fix const reference Co-authored-by: Andrey Filipenkov --- client/widgets/Buttons.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/widgets/Buttons.h b/client/widgets/Buttons.h index fda839940..9eea639fd 100644 --- a/client/widgets/Buttons.h +++ b/client/widgets/Buttons.h @@ -67,7 +67,7 @@ public: void setBorderColor(std::optional borderColor); /// adds one more callback to on-click actions - void addCallback(std::function const & callback); + void addCallback(const std::function & callback); /// adds overlay on top of button image. Only one overlay can be active at once void addOverlay(std::shared_ptr newOverlay); From ed86a917ebc06e17d4a39ab4756862c9bda1647a Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Sun, 29 Oct 2023 14:25:02 +0100 Subject: [PATCH 0995/1248] AI/BattleAI/BattleAI.cpp: Use Class template argument deduction for wallParts array Co-authored-by: Ivan Savenko --- AI/BattleAI/BattleAI.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 6881060e9..3a428e122 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -197,7 +197,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st } else { - std::array wallParts { + std::array wallParts { EWallPart::KEEP, EWallPart::BOTTOM_TOWER, EWallPart::UPPER_TOWER, From e9c77f963dc89f5e12e33e5160b56886747ccd56 Mon Sep 17 00:00:00 2001 From: krs Date: Sun, 29 Oct 2023 15:57:26 +0200 Subject: [PATCH 0996/1248] Update AI.md --- docs/developers/AI.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers/AI.md b/docs/developers/AI.md index 72bc5d409..7f1c61b65 100644 --- a/docs/developers/AI.md +++ b/docs/developers/AI.md @@ -1,4 +1,4 @@ -< [Documentation](../Readme.md) / AI Description - WIP +< [Documentation](../Readme.md) / AI Description There are two types of AI: adventure and battle. From 73b89d4e84451cc268056db1511594499d61ff54 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 29 Oct 2023 16:04:48 +0200 Subject: [PATCH 0997/1248] #2977 - fix obstackle path deserialization --- client/battle/BattleObstacleController.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/client/battle/BattleObstacleController.cpp b/client/battle/BattleObstacleController.cpp index 0bb6e0bd2..0c6e7e809 100644 --- a/client/battle/BattleObstacleController.cpp +++ b/client/battle/BattleObstacleController.cpp @@ -27,6 +27,7 @@ #include "../../CCallback.h" #include "../../lib/battle/CObstacleInstance.h" #include "../../lib/ObstacleHandler.h" +#include "../../lib/serializer/JsonDeserializer.h" BattleObstacleController::BattleObstacleController(BattleInterface & owner): owner(owner), @@ -77,7 +78,14 @@ void BattleObstacleController::obstacleRemoved(const std::vectorpreload(); auto first = animation->getImage(0, 0); @@ -88,7 +96,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector if we know how to blit obstacle, let's blit the effect in the same place Point whereTo = getObstaclePosition(first, obstacle); //AFAIK, in H3 there is no sound of obstacle removal - owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true)); + owner.stacksController->addNewAnim(new EffectAnimation(owner, animationPath, whereTo, obstacle["position"].Integer(), 0, true)); obstacleAnimations.erase(oi.id); //so when multiple obstacles are removed, they show up one after another From 61fc236d79c0aa7ab7ca2db7d1a997cabf5e4708 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 24 Jul 2023 19:09:17 +0300 Subject: [PATCH 0998/1248] ArtifactLocation now use ID for artHolder identification part1 --- AI/Nullkiller/AIGateway.cpp | 3 ++- AI/VCAI/VCAI.cpp | 3 ++- client/windows/CHeroWindow.cpp | 2 +- lib/ArtifactUtils.cpp | 43 +++++++++++++++++++++++++++++++ lib/ArtifactUtils.h | 2 ++ lib/CArtHandler.cpp | 12 ++++----- lib/constants/EntityIdentifiers.h | 15 ++++++----- 7 files changed, 65 insertions(+), 15 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 55d8a59b7..343813525 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -9,6 +9,7 @@ */ #include "StdInc.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" @@ -1021,7 +1022,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto location : allArtifacts) { - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + if(location.relatedObj() == target && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index b619cc4b1..8e1ab3202 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -14,6 +14,7 @@ #include "BuildingManager.h" #include "Goals/Goals.h" +#include "../../lib/ArtifactUtils.h" #include "../../lib/UnlockGuard.h" #include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/ObjectTemplate.h" @@ -1195,7 +1196,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) continue; // don't attempt to move catapult and spellbook - if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST) + if(location.relatedObj() == target && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear auto s = location.getSlot(); diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 7d05fe38a..b3a3cad8b 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -334,7 +334,7 @@ void CHeroWindow::commanderWindow() if(pickedArtInst) { const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); - if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack! + if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack! { ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); ArtifactLocation dst(curHero->commander.get(), freeSlot); diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 92da87763..4e92ffeb2 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -74,6 +74,49 @@ DLL_LINKAGE const std::vector & ArtifactUtils::constituentWorn return positions; } +DLL_LINKAGE const std::vector & ArtifactUtils::allWornSlots() +{ + static const std::vector positions = + { + ArtifactPosition::HEAD, + ArtifactPosition::SHOULDERS, + ArtifactPosition::NECK, + ArtifactPosition::RIGHT_HAND, + ArtifactPosition::LEFT_HAND, + ArtifactPosition::TORSO, + ArtifactPosition::RIGHT_RING, + ArtifactPosition::LEFT_RING, + ArtifactPosition::FEET, + ArtifactPosition::MISC1, + ArtifactPosition::MISC2, + ArtifactPosition::MISC3, + ArtifactPosition::MISC4, + ArtifactPosition::MISC5, + ArtifactPosition::MACH1, + ArtifactPosition::MACH2, + ArtifactPosition::MACH3, + ArtifactPosition::MACH4, + ArtifactPosition::SPELLBOOK + }; + + return positions; +} + +DLL_LINKAGE const std::vector & ArtifactUtils::commanderSlots() +{ + static const std::vector positions = + { + ArtifactPosition::COMMANDER1, + ArtifactPosition::COMMANDER2, + ArtifactPosition::COMMANDER3, + ArtifactPosition::COMMANDER4, + ArtifactPosition::COMMANDER5, + ArtifactPosition::COMMANDER6 + }; + + return positions; +} + DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair & slot) { return slot.second.artifact diff --git a/lib/ArtifactUtils.h b/lib/ArtifactUtils.h index 4b30f946d..b4af3b401 100644 --- a/lib/ArtifactUtils.h +++ b/lib/ArtifactUtils.h @@ -31,6 +31,8 @@ namespace ArtifactUtils // TODO: Make this constexpr when the toolset is upgraded DLL_LINKAGE const std::vector & unmovableSlots(); DLL_LINKAGE const std::vector & constituentWornSlots(); + DLL_LINKAGE const std::vector & allWornSlots(); + DLL_LINKAGE const std::vector & commanderSlots(); DLL_LINKAGE bool isArtRemovable(const std::pair & slot); DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, const ArtifactID & artID, const ArtifactPosition & slot); DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot); diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 413a51bb2..e943b58c6 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -693,8 +693,8 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) a->possibleSlots[ArtBearer::HERO].clear(); a->possibleSlots[ArtBearer::CREATURE].clear(); } - for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i) - a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i)); + for(auto & slot : ArtifactUtils::commanderSlots()) + a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot)); } bool CArtHandler::legalArtifact(const ArtifactID & id) @@ -975,9 +975,9 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const } if(vstd::contains(artifactsWorn, pos)) return &artifactsWorn.at(pos); - if(pos >= ArtifactPosition::AFTER_LAST ) + if(ArtifactUtils::isSlotBackpack(pos)) { - int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START; + auto backpackPos = pos - ArtifactPosition::BACKPACK_START; if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size()) return nullptr; else @@ -1080,9 +1080,9 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) { - for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1)) + for(auto & slot : ArtifactUtils::allWornSlots()) { - serializeJsonSlot(handler, ap, map); + serializeJsonSlot(handler, slot, map); } std::vector backpackTemp; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index d074b0f8a..6a5e7b5d8 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -607,20 +607,23 @@ public: TRANSITION_POS = -3, FIRST_AVAILABLE = -2, PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack + + // Hero HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5 RIGHT_RING, LEFT_RING, FEET, //8 MISC1, MISC2, MISC3, MISC4, //12 MACH1, MACH2, MACH3, MACH4, //16 SPELLBOOK, MISC5, //18 - AFTER_LAST, - //cres + BACKPACK_START = 19, + + // Creatures CREATURE_SLOT = 0, - COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST, - - BACKPACK_START = 19 + + // Commander + COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6 }; - static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots"); + static_assert(MISC5 < BACKPACK_START, "incorrect number of artifact slots"); DLL_LINKAGE static si32 decode(const std::string & identifier); DLL_LINKAGE static std::string encode(const si32 index); From ab2f6abb877db93d5f4643ba1e23b71f8c4b70d4 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sat, 14 Oct 2023 22:00:39 +0300 Subject: [PATCH 0999/1248] ArtifactLocation now use ID for artHolder identification part2 --- AI/Nullkiller/AIGateway.cpp | 22 +-- AI/VCAI/VCAI.cpp | 22 +-- client/CPlayerInterface.cpp | 18 +- client/NetPacksClient.cpp | 20 +-- client/widgets/CArtifactHolder.cpp | 6 +- client/widgets/CArtifactsOfHeroAltar.cpp | 4 +- client/widgets/CArtifactsOfHeroBackpack.cpp | 4 +- client/widgets/CArtifactsOfHeroBase.cpp | 6 +- client/widgets/CArtifactsOfHeroKingdom.cpp | 5 +- client/widgets/CArtifactsOfHeroMain.cpp | 5 +- client/widgets/CGarrisonInt.cpp | 4 +- client/widgets/CWindowWithArtifacts.cpp | 10 +- client/windows/CCreatureWindow.cpp | 3 +- client/windows/CHeroWindow.cpp | 10 +- client/windows/CTradeWindow.cpp | 4 +- lib/CArtifactInstance.cpp | 18 +- lib/CArtifactInstance.h | 9 +- lib/CCreatureSet.cpp | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameStateCampaign.cpp | 8 +- lib/mapObjects/CGHeroInstance.cpp | 4 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapping/MapFormatH3M.cpp | 5 +- lib/networkPacks/NetPacksLib.cpp | 177 +++++++------------- server/CGameHandler.cpp | 73 ++++---- server/CGameHandler.h | 2 +- server/NetPacksServer.cpp | 4 +- server/battles/BattleResultProcessor.cpp | 26 +-- server/queries/MapQueries.cpp | 12 +- 29 files changed, 216 insertions(+), 273 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 343813525..9dde74d18 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -996,21 +996,21 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto p : h->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); } } for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); } for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero @@ -1022,13 +1022,13 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance for(auto location : allArtifacts) { - if(location.relatedObj() == target && ArtifactUtils::isSlotEquipment(location.slot)) + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult continue; - auto s = location.getSlot(); + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; @@ -1039,9 +1039,9 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance bool emptySlotFound = false; for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { + ArtifactLocation destLocation(target->id, slot); cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; @@ -1055,11 +1055,11 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { - ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 8e1ab3202..0f11c93fa 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1167,21 +1167,21 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot for(auto p : h->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(h, p.first)); + allArtifacts.push_back(ArtifactLocation(h->id, p.first)); } } for(auto slot : h->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact))); if(otherh) { for(auto p : otherh->artifactsWorn) { if(p.second.artifact) - allArtifacts.push_back(ArtifactLocation(otherh, p.first)); + allArtifacts.push_back(ArtifactLocation(otherh->id, p.first)); } for(auto slot : otherh->artifactsInBackpack) - allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact))); + allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact))); } //we give stuff to one hero or another, depending on giveStuffToFirstHero @@ -1196,10 +1196,10 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK) continue; // don't attempt to move catapult and spellbook - if(location.relatedObj() == target && ArtifactUtils::isSlotEquipment(location.slot)) + if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot)) continue; //don't reequip artifact we already wear - auto s = location.getSlot(); + auto s = cb->getHero(location.artHolder)->getSlot(location.slot); if(!s || s->locked) //we can't move locks continue; auto artifact = s->artifact; @@ -1210,9 +1210,9 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot bool emptySlotFound = false; for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType())) { - ArtifactLocation destLocation(target, slot); - if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { + ArtifactLocation destLocation(target->id, slot); cb->swapArtifacts(location, destLocation); //just put into empty slot emptySlotFound = true; changeMade = true; @@ -1226,11 +1226,11 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot auto otherSlot = target->getSlot(slot); if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one { - ArtifactLocation destLocation(target, slot); //if that artifact is better than what we have, pick it - if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move + if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move { - cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact))); + ArtifactLocation destLocation(target->id, slot); + cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact))); changeMade = true; break; } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 760010a25..491380f3b 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1753,8 +1753,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won) void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) { - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - if(hero) + if(auto hero = cb->getHero(al.artHolder)) { auto art = hero->getArt(al.slot); if(art == nullptr) @@ -1770,15 +1769,13 @@ void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al) void CPlayerInterface::artifactPut(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); } void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactRemoved(al); @@ -1789,8 +1786,7 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al) void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), dst.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(dst.artHolder)); bool redraw = true; // If a bulk transfer has arrived, then redrawing only the last art movement. @@ -1815,8 +1811,7 @@ void CPlayerInterface::bulkArtMovementStart(size_t numOfArts) void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactAssembled(al); @@ -1825,8 +1820,7 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al) void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al) { EVENT_HANDLER_CALLED_BY_CLIENT; - auto hero = std::visit(HeroObjectRetriever(), al.artHolder); - adventureInt->onHeroChanged(hero); + adventureInt->onHeroChanged(cb->getHero(al.artHolder)); for(auto artWin : GH.windows().findWindows()) artWin->artifactDisassembled(al); diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 81cd835d3..c0d60dea0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -267,14 +267,14 @@ void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalance void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al); if(pack.askAssemble) - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al); } void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al); } void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) @@ -286,16 +286,16 @@ void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst); }; - moveArtifact(pack.src.owningPlayer()); - if(pack.src.owningPlayer() != pack.dst.owningPlayer()) - moveArtifact(pack.dst.owningPlayer()); + moveArtifact(cl.getOwner(pack.src.artHolder)); + if(cl.getOwner(pack.src.artHolder) != cl.getOwner(pack.dst.artHolder)) + moveArtifact(cl.getOwner(pack.dst.artHolder)); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) { - auto applyMove = [this, &pack](std::vector & artsPack) -> void + /*auto applyMove = [this, &pack](std::vector & artsPack) -> void { for(auto & slotToMove : artsPack) { @@ -316,19 +316,19 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) applyMove(pack.artsPack0); if(pack.swap) - applyMove(pack.artsPack1); + applyMove(pack.artsPack1);*/ } void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack) { - callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al); + callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al); cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings } diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index a3b72d320..48136a6cc 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -121,10 +121,10 @@ void CCommanderArtPlace::returnArtToHeroCallback() } else { - ArtifactLocation src(commanderOwner->commander.get(), artifactPos); - ArtifactLocation dst(commanderOwner, freeSlot); + ArtifactLocation src(commanderOwner->id, artifactPos); + ArtifactLocation dst(commanderOwner->id, freeSlot); - if(ourArt->canBePutAt(dst, true)) + if(ourArt->canBePutAt(commanderOwner, freeSlot, true)) { LOCPLINT->cb->swapArtifacts(src, dst); setArtifact(nullptr); diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 737b05f9c..5c33ee05c 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -72,7 +72,7 @@ void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace) if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot)) pickedArtFromSlot = curHero->getSlotByInstance(art); assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, pickedArtFromSlot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, pickedArtFromSlot), ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } } @@ -89,7 +89,7 @@ void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot) if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS) { assert(curHero->getSlot(slot)->getArt()); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, slot), ArtifactLocation(curHero, pickedArtFromSlot)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, slot), ArtifactLocation(curHero->id, pickedArtFromSlot)); pickedArtFromSlot = ArtifactPosition::PRE_FIRST; } } diff --git a/client/widgets/CArtifactsOfHeroBackpack.cpp b/client/widgets/CArtifactsOfHeroBackpack.cpp index f95d5cf65..6a487799c 100644 --- a/client/widgets/CArtifactsOfHeroBackpack.cpp +++ b/client/widgets/CArtifactsOfHeroBackpack.cpp @@ -80,8 +80,8 @@ void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, co void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } void CArtifactsOfHeroBackpack::scrollBackpack(int offset) diff --git a/client/widgets/CArtifactsOfHeroBase.cpp b/client/widgets/CArtifactsOfHeroBase.cpp index 769da2258..319c40e2f 100644 --- a/client/widgets/CArtifactsOfHeroBase.cpp +++ b/client/widgets/CArtifactsOfHeroBase.cpp @@ -39,11 +39,11 @@ void CArtifactsOfHeroBase::putBackPickedArtifact() auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId()); if(slot == ArtifactPosition::PRE_FIRST) { - LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } else { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero, slot)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero->id, slot)); } } if(putBackPickedArtCallback) @@ -178,7 +178,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved) { for(auto artPlace : artWorn) - artPlace.second->selectSlot(art->artType->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); + artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved)); } void CArtifactsOfHeroBase::unmarkSlots() diff --git a/client/widgets/CArtifactsOfHeroKingdom.cpp b/client/widgets/CArtifactsOfHeroKingdom.cpp index 67e702b5f..14fd222f4 100644 --- a/client/widgets/CArtifactsOfHeroKingdom.cpp +++ b/client/widgets/CArtifactsOfHeroKingdom.cpp @@ -13,6 +13,7 @@ #include "Buttons.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" #include "../../lib/networkPacks/ArtifactLocation.h" @@ -56,7 +57,7 @@ void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, con void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } diff --git a/client/widgets/CArtifactsOfHeroMain.cpp b/client/widgets/CArtifactsOfHeroMain.cpp index 46b5329ca..b4ac981b0 100644 --- a/client/widgets/CArtifactsOfHeroMain.cpp +++ b/client/widgets/CArtifactsOfHeroMain.cpp @@ -11,6 +11,7 @@ #include "CArtifactsOfHeroMain.h" #include "../CPlayerInterface.h" +#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../CCallback.h" #include "../../lib/networkPacks/ArtifactLocation.h" @@ -36,6 +37,6 @@ void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot), - ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot), + ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS)); } diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 3942da8a0..72e52ca54 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -196,7 +196,7 @@ bool CGarrisonSlot::highlightOrDropArtifact() artSelected = true; if (myStack) // try dropping the artifact only if the slot isn't empty { - ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS); + /*ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS); ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT); if(pickedArtInst->canBePutAt(dst, true)) { //equip clicked stack @@ -208,7 +208,7 @@ bool CGarrisonSlot::highlightOrDropArtifact() ArtifactUtils::getArtBackpackPosition(srcHero, dst.getArt()->getTypeId()))); } LOCPLINT->cb->swapArtifacts(src, dst); - } + }*/ } } } diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 8c6d2b7c9..2b6d4d18d 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -122,8 +122,8 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst if(pickedArtInst) { - auto srcLoc = ArtifactLocation(heroPickedArt, ArtifactPosition::TRANSITION_POS); - auto dstLoc = ArtifactLocation(hero, artPlace.slot); + auto srcLoc = ArtifactLocation(heroPickedArt->id, ArtifactPosition::TRANSITION_POS); + auto dstLoc = ArtifactLocation(hero->id, artPlace.slot); if(ArtifactUtils::isSlotBackpack(artPlace.slot)) { @@ -141,7 +141,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst } } // Check if artifact transfer is possible - else if(pickedArtInst->canBePutAt(dstLoc, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) + else if(pickedArtInst->canBePutAt(hero, artPlace.slot, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID)) { isTransferAllowed = true; } @@ -270,7 +270,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const // we have a different artifact may look surprising... but it's valid. auto pickedArtInst = std::get(curState.value()); - assert(!pickedArtInst || destLoc.isHolder(std::get(curState.value()))); + assert(!pickedArtInst || destLoc.artHolder == std::get(curState.value())->id); auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void { @@ -316,7 +316,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const } // Make sure the status bar is updated so it does not display old text - if(destLoc.getHolderArtSet() == hero) + if(destLoc.artHolder == hero->id) { if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot)) artPlace->hover(true); diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index bece268cf..e1b881775 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -974,7 +974,7 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos) const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); if(slot != ArtifactPosition::PRE_FIRST) { - LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot)); + //LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner->id, slot)); stackArtifactButton.reset(); stackArtifactHelp.reset(); stackArtifactIcon.reset(); @@ -982,3 +982,4 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos) } } + diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index b3a3cad8b..35bcc508e 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -336,10 +336,10 @@ void CHeroWindow::commanderWindow() const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack! { - ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(curHero->commander.get(), freeSlot); - - if(pickedArtInst->canBePutAt(dst, true)) + ArtifactLocation src(hero->id, ArtifactPosition::TRANSITION_POS); + ArtifactLocation dst(curHero->id, freeSlot); + // TODO add ->commander.get() !!! + /*if(pickedArtInst->canBePutAt(dst, true)) { //equip clicked stack if(dst.getArt()) { @@ -347,7 +347,7 @@ void CHeroWindow::commanderWindow() ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); } LOCPLINT->cb->swapArtifacts(src, dst); - } + }*/ } } else diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 8a1d3672a..ef65e7912 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -191,8 +191,8 @@ void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition) const auto hero = aw->arts->getHero(); const auto slot = hero->getSlotByInstance(art); assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slot), - ArtifactLocation(hero, ArtifactPosition::TRANSITION_POS)); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), + ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); aw->arts->pickedArtFromSlot = slot; aw->arts->artifactsOnAltar.erase(art); setID(-1); diff --git a/lib/CArtifactInstance.cpp b/lib/CArtifactInstance.cpp index d80373251..c790ca379 100644 --- a/lib/CArtifactInstance.cpp +++ b/lib/CArtifactInstance.cpp @@ -155,9 +155,9 @@ void CArtifactInstance::setId(ArtifactInstanceID id) this->id = id; } -bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const +bool CArtifactInstance::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const { - return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved); + return artType->canBePutAt(artSet, slot, assumeDestRemoved); } bool CArtifactInstance::isCombined() const @@ -165,15 +165,15 @@ bool CArtifactInstance::isCombined() const return artType->isCombined(); } -void CArtifactInstance::putAt(const ArtifactLocation & al) +void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot) { - auto placementMap = al.getHolderArtSet()->putArtifact(al.slot, this); + auto placementMap = set.putArtifact(slot, this); addPlacementMap(placementMap); } -void CArtifactInstance::removeFrom(const ArtifactLocation & al) +void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot) { - al.getHolderArtSet()->removeArtifact(al.slot); + set.removeArtifact(slot); for(auto & part : partsInfo) { if(part.slot != ArtifactPosition::PRE_FIRST) @@ -181,10 +181,10 @@ void CArtifactInstance::removeFrom(const ArtifactLocation & al) } } -void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst) +void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot) { - removeFrom(src); - putAt(dst); + removeFrom(srcSet, srcSlot); + putAt(dstSet, dstSlot); } void CArtifactInstance::deserializationFix() diff --git a/lib/CArtifactInstance.h b/lib/CArtifactInstance.h index 2c87adf08..d72397afe 100644 --- a/lib/CArtifactInstance.h +++ b/lib/CArtifactInstance.h @@ -84,11 +84,12 @@ public: ArtifactInstanceID getId() const; void setId(ArtifactInstanceID id); - bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const; + bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE, + bool assumeDestRemoved = false) const; bool isCombined() const; - void putAt(const ArtifactLocation & al); - void removeFrom(const ArtifactLocation & al); - void move(const ArtifactLocation & src, const ArtifactLocation & dst); + void putAt(CArtifactSet & set, const ArtifactPosition slot); + void removeFrom(CArtifactSet & set, const ArtifactPosition slot); + void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot); void deserializationFix(); template void serialize(Handler & h, const int version) diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 34849d337..f02ecd4a2 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -870,7 +870,7 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { assert(!getArt(pos)); - assert(art->artType->canBePutAt(this, pos)); + assert(art->canBePutAt(this, pos)); attachTo(*art); return CArtifactSet::putArtifact(pos, art); diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index c349e851d..1b634d28e 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -2103,7 +2103,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) auto slot = ArtifactUtils::getArtAnyPosition(h, aid); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) { - ai->putAt(ArtifactLocation(h, slot)); + ai->putAt(*h, slot); return true; } else diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 462204cb7..c97c159a6 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -134,9 +134,9 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorartType->getId()); - ArtifactLocation al(hero, artifactPosition); - if(!takeable && !al.getSlot()->locked) //don't try removing locked artifacts -> it crashes #1719 - al.removeArtifact(); + ArtifactLocation al(hero->id, artifactPosition); + if(!takeable && !hero->getSlot(al.slot)->locked) //don't try removing locked artifacts -> it crashes #1719 + hero->getArt(al.slot)->removeFrom(*hero, al.slot); }; // process on copy - removal of artifact will invalidate container @@ -300,7 +300,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero) CArtifactInstance * scroll = ArtifactUtils::createScroll(SpellID(curBonus->info2)); const auto slot = ArtifactUtils::getArtAnyPosition(hero, scroll->getTypeId()); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) - scroll->putAt(ArtifactLocation(hero, slot)); + scroll->putAt(*hero, slot); else logGlobal->error("Cannot give starting scroll - no free slots!"); break; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8864891aa..fc5cbb7ad 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1087,7 +1087,7 @@ std::string CGHeroInstance::getBiographyTextID() const CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art) { - assert(art->artType->canBePutAt(this, pos)); + assert(art->canBePutAt(this, pos)); if(ArtifactUtils::isSlotEquipment(pos)) attachTo(*art); @@ -1130,7 +1130,7 @@ void CGHeroInstance::removeSpellbook() if(hasSpellbook()) { - ArtifactLocation(this, ArtifactPosition(ArtifactPosition::SPELLBOOK)).removeArtifact(); + getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK); } } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5431a12a1..a489b5171 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -144,7 +144,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const { if(h->hasArt(elem)) { - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(elem, false))); + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(elem, false))); } else { @@ -153,7 +153,7 @@ void CQuest::completeQuest(IGameCallback * cb, const CGHeroInstance *h) const auto parts = assembly->getPartsInfo(); // Remove the assembly - cb->removeArtifact(ArtifactLocation(h, h->getArtPos(assembly))); + cb->removeArtifact(ArtifactLocation(h->id, h->getArtPos(assembly))); // Disassemble this backpack artifact for(const auto & ci : parts) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 800dc4d55..06c3e951a 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -944,10 +944,9 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) // He has Shackles of War (normally - MISC slot artifact) in LEFT_HAND slot set in editor // Artifact seems to be missing in game, so skip artifacts that don't fit target slot auto * artifact = ArtifactUtils::createArtifact(map, artifactID); - auto dstLoc = ArtifactLocation(hero, ArtifactPosition(slot)); - if(artifact->canBePutAt(dstLoc)) + if(artifact->canBePutAt(hero, ArtifactPosition(slot))) { - artifact->putAt(dstLoc); + artifact->putAt(*hero, ArtifactPosition(slot)); } else { diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index a15553cc5..2a198ebf4 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1574,67 +1574,6 @@ struct GetBase } }; - -void ArtifactLocation::removeArtifact() -{ - CArtifactInstance *a = getArt(); - assert(a); - a->removeFrom(*this); -} - -const CArmedInstance * ArtifactLocation::relatedObj() const -{ - return std::visit(ObjectRetriever(), artHolder); -} - -PlayerColor ArtifactLocation::owningPlayer() const -{ - const auto * obj = relatedObj(); - return obj ? obj->tempOwner : PlayerColor::NEUTRAL; -} - -CArtifactSet *ArtifactLocation::getHolderArtSet() -{ - return std::visit(GetBase(), artHolder); -} - -CBonusSystemNode *ArtifactLocation::getHolderNode() -{ - return std::visit(GetBase(), artHolder); -} - -const CArtifactInstance *ArtifactLocation::getArt() const -{ - const auto * s = getSlot(); - if(s) - return s->getArt(); - else - return nullptr; -} - -CArtifactSet * ArtifactLocation::getHolderArtSet() const -{ - auto * t = const_cast(this); - return t->getHolderArtSet(); -} - -const CBonusSystemNode * ArtifactLocation::getHolderNode() const -{ - auto * t = const_cast(this); - return t->getHolderNode(); -} - -CArtifactInstance *ArtifactLocation::getArt() -{ - const ArtifactLocation *t = this; - return const_cast(t->getArt()); -} - -const ArtSlotInfo *ArtifactLocation::getSlot() const -{ - return getHolderArtSet()->getSlot(slot); -} - void ChangeStackCount::applyGs(CGameState * gs) { auto * srcObj = gs->getArmyInstance(army); @@ -1709,41 +1648,43 @@ void RebalanceStacks::applyGs(CGameState * gs) if(srcCount == count) //moving whole stack { - [[maybe_unused]] const CCreature *c = dst.army->getCreature(dst.slot); + const auto c = dst.army->getCreature(dst.slot); if(c) //stack at dest -> merge { assert(c == srcType); - auto alHere = ArtifactLocation (src.getStack(), ArtifactPosition::CREATURE_SLOT); - auto alDest = ArtifactLocation (dst.getStack(), ArtifactPosition::CREATURE_SLOT); - auto * artHere = alHere.getArt(); - auto * artDest = alDest.getArt(); - if (artHere) + + const auto srcHero = dynamic_cast(src.army.get()); + const auto dstHero = dynamic_cast(dst.army.get()); + auto srcLoc = ArtifactLocation(srcHero->id, ArtifactPosition::CREATURE_SLOT); + auto dstLoc = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); + // TODO set creature id !!! + /*if (auto artHere = srcLoc.getArt()) { - if (alDest.getArt()) + if (dstLoc.getArt()) { - auto * hero = dynamic_cast(src.army.get()); - auto dstSlot = ArtifactUtils::getArtBackpackPosition(hero, alDest.getArt()->getTypeId()); - if(hero && dstSlot != ArtifactPosition::PRE_FIRST) + + auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstLoc.getArt()->getTypeId()); + if(srcHero && dstSlot != ArtifactPosition::PRE_FIRST) { - artDest->move (alDest, ArtifactLocation (hero, dstSlot)); + dstLoc.getArt()->move (dstLoc, ArtifactLocation (srcHero, dstSlot)); } //else - artifact cna be lost :/ else { EraseArtifact ea; - ea.al = alDest; + ea.al = dstLoc; ea.applyGs(gs); logNetwork->warn("Cannot move artifact! No free slots"); } - artHere->move (alHere, alDest); + artHere->move (srcLoc, dstLoc); //TODO: choose from dialog } else //just move to the other slot before stack gets erased { - artHere->move (alHere, alDest); + artHere->move (srcLoc, dstLoc); } - } + }*/ if (stackExp) { ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); @@ -1811,53 +1752,57 @@ void BulkSmartRebalanceStacks::applyGs(CGameState * gs) void PutArtifact::applyGs(CGameState *gs) { - assert(art->canBePutAt(al)); // Ensure that artifact has been correctly added via NewArtifact pack assert(vstd::contains(gs->map->artInstances, art)); assert(!art->getParentNodes().empty()); - art->putAt(al); + auto hero = gs->getHero(al.artHolder); + assert(hero); + assert(art->canBePutAt(hero, al.slot)); + art->putAt(*hero, al.slot); } void EraseArtifact::applyGs(CGameState *gs) { - const auto * slot = al.getSlot(); + const auto hero = gs->getHero(al.artHolder); + assert(hero); + const auto slot = hero->getSlot(al.slot); if(slot->locked) { logGlobal->debug("Erasing locked artifact: %s", slot->artifact->artType->getNameTranslated()); DisassembledArtifact dis; dis.al.artHolder = al.artHolder; - auto * aset = al.getHolderArtSet(); - #ifndef NDEBUG - bool found = false; - #endif - for(auto& p : aset->artifactsWorn) + + for(auto & slotInfo : hero->artifactsWorn) { - auto art = p.second.artifact; + auto art = slotInfo.second.artifact; if(art->isCombined() && art->isPart(slot->artifact)) { - dis.al.slot = aset->getArtPos(art); - #ifndef NDEBUG - found = true; - #endif + dis.al.slot = hero->getArtPos(art); break; } } - assert(found && "Failed to determine the assembly this locked artifact belongs to"); - logGlobal->debug("Found the corresponding assembly: %s", dis.al.getSlot()->artifact->artType->getNameTranslated()); + assert((dis.al.slot != ArtifactPosition::PRE_FIRST) && "Failed to determine the assembly this locked artifact belongs to"); + logGlobal->debug("Found the corresponding assembly: %s", hero->getArt(dis.al.slot)->artType->getNameTranslated()); dis.applyGs(gs); } else { logGlobal->debug("Erasing artifact %s", slot->artifact->artType->getNameTranslated()); } - al.removeArtifact(); + auto art = hero->getArt(al.slot); + assert(art); + art->removeFrom(*hero, al.slot); } void MoveArtifact::applyGs(CGameState * gs) { - CArtifactInstance * art = src.getArt(); + auto srcHero = gs->getHero(src.artHolder); + auto dstHero = gs->getHero(dst.artHolder); + assert(srcHero); + assert(dstHero); + auto art = srcHero->getArt(src.slot); assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); - art->move(src, dst); + art->move(*srcHero, src.slot, *dstHero, dst.slot); } void BulkMoveArtifacts::applyGs(CGameState * gs) @@ -1887,7 +1832,7 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) assert(slotInfo); auto * art = const_cast(slotInfo->getArt()); assert(art); - switch(operation) + /*switch(operation) { case EBulkArtsOp::BULK_MOVE: const_cast(art)->move( @@ -1901,7 +1846,7 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) break; default: break; - } + }*/ if(srcPos >= ArtifactPosition::BACKPACK_START) { @@ -1932,15 +1877,16 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) void AssembledArtifact::applyGs(CGameState *gs) { - CArtifactSet * artSet = al.getHolderArtSet(); - const CArtifactInstance * transformedArt = al.getArt(); + auto hero = gs->getHero(al.artHolder); + assert(hero); + const auto transformedArt = hero->getArt(al.slot); assert(transformedArt); - assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(artSet, transformedArt->getTypeId()), [=](const CArtifact * art)->bool + assert(vstd::contains_if(ArtifactUtils::assemblyPossibilities(hero, transformedArt->getTypeId()), [=](const CArtifact * art)->bool { return art->getId() == builtArt->getId(); })); - const auto transformedArtSlot = artSet->getSlotByInstance(transformedArt); + const auto transformedArtSlot = hero->getSlotByInstance(transformedArt); auto * combinedArt = new CArtifactInstance(builtArt); gs->map->addNewArtifactInstance(combinedArt); @@ -1952,7 +1898,7 @@ void AssembledArtifact::applyGs(CGameState *gs) if(transformedArt->getTypeId() == constituent->getId()) slot = transformedArtSlot; else - slot = artSet->getArtPos(constituent->getId(), false, false); + slot = hero->getArtPos(constituent->getId(), false, false); assert(slot != ArtifactPosition::PRE_FIRST); slotsInvolved.emplace_back(slot); @@ -1972,8 +1918,8 @@ void AssembledArtifact::applyGs(CGameState *gs) break; } - if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), al.slot) - && vstd::contains(combinedArt->artType->getPossibleSlots().at(artSet->bearerType()), slot)) + if(!vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), al.slot) + && vstd::contains(combinedArt->artType->getPossibleSlots().at(hero->bearerType()), slot)) al.slot = slot; } else @@ -1986,8 +1932,8 @@ void AssembledArtifact::applyGs(CGameState *gs) // Delete parts from hero for(const auto slot : slotsInvolved) { - const auto constituentInstance = artSet->getArt(slot); - constituentInstance->removeFrom(ArtifactLocation(al.artHolder, slot)); + const auto constituentInstance = hero->getArt(slot); + constituentInstance->removeFrom(*hero, slot); if(ArtifactUtils::isSlotEquipment(al.slot) && slot != al.slot) combinedArt->addPart(constituentInstance, slot); @@ -1996,25 +1942,26 @@ void AssembledArtifact::applyGs(CGameState *gs) } // Put new combined artifacts - combinedArt->putAt(al); + combinedArt->putAt(*hero, al.slot); } void DisassembledArtifact::applyGs(CGameState *gs) { - auto * disassembled = al.getArt(); - assert(disassembled); + auto hero = gs->getHero(al.artHolder); + assert(hero); + auto disassembledArt = hero->getArt(al.slot); + assert(disassembledArt); - auto parts = disassembled->getPartsInfo(); - disassembled->removeFrom(al); + auto parts = disassembledArt->getPartsInfo(); + disassembledArt->removeFrom(*hero, al.slot); for(auto & part : parts) { - ArtifactLocation partLoc = al; // ArtifactPosition::PRE_FIRST is value of main part slot -> it'll replace combined artifact in its pos - partLoc.slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); - disassembled->detachFrom(*part.art); - part.art->putAt(partLoc); + auto slot = (ArtifactUtils::isSlotEquipment(part.slot) ? part.slot : al.slot); + disassembledArt->detachFrom(*part.art); + part.art->putAt(*hero, slot); } - gs->map->eraseArtifactInstance(disassembled); + gs->map->eraseArtifactInstance(disassembledArt); } void HeroVisit::applyGs(CGameState *gs) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 7a0e39c98..f6e524883 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2270,7 +2270,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, if(!t->visitingHero || !t->visitingHero->hasArt(ArtifactID::GRAIL)) COMPLAIN_RET("Cannot build this without grail!") else - removeArtifact(ArtifactLocation(t->visitingHero, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); + removeArtifact(ArtifactLocation(t->visitingHero->id, t->visitingHero->getArtPos(ArtifactID::GRAIL, false))); } break; } @@ -2664,74 +2664,71 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) // With the amount of changes done to the function, it's more like transferArtifacts. // Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. -bool CGameHandler::moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) +bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) { - ArtifactLocation src = al1, dst = al2; - const PlayerColor srcPlayer = src.owningPlayer(), dstPlayer = dst.owningPlayer(); - const CArmedInstance *srcObj = src.relatedObj(), *dstObj = dst.relatedObj(); + const auto srcHero = getHero(src.artHolder), dstHero = getHero(dst.artHolder); + ArtifactLocation srcLoc = src, dstLoc = dst; // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcObj->id, dstObj->id)) + if(!isAllowedExchange(srcLoc.artHolder, dstLoc.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); - const CArtifactInstance *srcArtifact = src.getArt(); - const CArtifactInstance *destArtifact = dst.getArt(); - const bool isDstSlotBackpack = ArtifactUtils::isSlotBackpack(dst.slot); + const auto srcArtifact = srcHero->getArt(srcLoc.slot); + const auto dstArtifact = dstHero->getArt(dstLoc.slot); + const bool isDstSlotBackpack = ArtifactUtils::isSlotBackpack(dstLoc.slot); if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); - if(destArtifact && srcPlayer != dstPlayer && !isDstSlotBackpack) + if(dstArtifact && srcHero->getOwner() != dstHero->getOwner() && !isDstSlotBackpack) COMPLAIN_RET("Can't touch artifact on hero of another player!"); // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) - && srcArtifact && !srcArtifact->canBePutAt(dst, true)) + if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstHero, dstLoc.slot, true)) COMPLAIN_RET("Cannot move artifact!"); - auto srcSlot = src.getSlot(); - auto dstSlot = dst.getSlot(); + auto srcSlotInfo = srcHero->getSlot(srcLoc.slot); + auto dstSlotInfo = dstHero->getSlot(dstLoc.slot); - if((srcSlot && srcSlot->locked) || (dstSlot && dstSlot->locked)) + if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); if(isDstSlotBackpack && srcArtifact->artType->isBig()) COMPLAIN_RET("Cannot put big artifacts in backpack!"); - if(src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) + if(srcLoc.slot == ArtifactPosition::MACH4 || dstLoc.slot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); if(isDstSlotBackpack) { - if(!ArtifactUtils::isBackpackFreeSlots(dst.getHolderArtSet())) + if(!ArtifactUtils::isBackpackFreeSlots(dstHero)) COMPLAIN_RET("Backpack is full!"); - vstd::amin(dst.slot, ArtifactPosition::BACKPACK_START + dst.getHolderArtSet()->artifactsInBackpack.size()); + vstd::amin(dstLoc.slot, ArtifactPosition::BACKPACK_START + dstHero->artifactsInBackpack.size()); } - if(!(src.slot == ArtifactPosition::TRANSITION_POS && dst.slot == ArtifactPosition::TRANSITION_POS)) + if(!(srcLoc.slot == ArtifactPosition::TRANSITION_POS && dstLoc.slot == ArtifactPosition::TRANSITION_POS)) { - if(src.slot == dst.slot && src.artHolder == dst.artHolder) + if(srcLoc.slot == dstLoc.slot && srcLoc.artHolder == dstLoc.artHolder) COMPLAIN_RET("Won't move artifact: Dest same as source!"); // Check if dst slot is occupied - if(!isDstSlotBackpack && destArtifact) + if(!isDstSlotBackpack && dstArtifact) { // Previous artifact must be removed first - moveArtifact(dst, ArtifactLocation(dst.artHolder, ArtifactPosition::TRANSITION_POS)); + moveArtifact(dstLoc, ArtifactLocation(dstLoc.artHolder, ArtifactPosition::TRANSITION_POS)); } try { - auto hero = std::get>(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dst.slot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + if(ArtifactUtils::checkSpellbookIsNeeded(dstHero, srcArtifact->artType->getId(), dstLoc.slot)) + giveHeroNewArtifact(dstHero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); } catch(const std::bad_variant_access &) { // object other than hero received an art - ignore } - MoveArtifact ma(&src, &dst); - if(src.artHolder == dst.artHolder) + MoveArtifact ma(&srcLoc, &dstLoc); + if(srcLoc.artHolder == dstLoc.artHolder) ma.askAssemble = false; sendAndApply(&ma); } @@ -2854,7 +2851,7 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a if(!destArtifact) COMPLAIN_RET("assembleArtifacts: there is no such artifact instance!"); - const auto dstLoc = ArtifactLocation(hero, artifactSlot); + const auto dstLoc = ArtifactLocation(hero->id, artifactSlot); if(assemble) { CArtifact * combinedArt = VLC->arth->objects[assembleTo]; @@ -2864,8 +2861,8 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a { COMPLAIN_RET("assembleArtifacts: It's impossible to assemble requested artifact!"); } - if(!destArtifact->canBePutAt(dstLoc) - && !destArtifact->canBePutAt(ArtifactLocation(hero, ArtifactPosition::BACKPACK_START))) + if(!destArtifact->canBePutAt(hero, artifactSlot) + && !destArtifact->canBePutAt(hero, ArtifactPosition::BACKPACK_START)) { COMPLAIN_RET("assembleArtifacts: It's impossible to give the artholder requested artifact!"); } @@ -2897,15 +2894,15 @@ bool CGameHandler::assembleArtifacts(ObjectInstanceID heroID, ArtifactPosition a bool CGameHandler::eraseArtifactByClient(const ArtifactLocation & al) { - const auto * hero = getHero(al.relatedObj()->id); + const auto * hero = getHero(al.artHolder); if(hero == nullptr) COMPLAIN_RET("eraseArtifactByClient: wrong hero's ID"); - const auto * art = al.getArt(); + const auto * art = hero->getArt(al.slot); if(art == nullptr) COMPLAIN_RET("Cannot remove artifact!"); - if(al.getArt()->artType->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) + if(art->canBePutAt(hero) || al.slot != ArtifactPosition::TRANSITION_POS) COMPLAIN_RET("Illegal artifact removal request"); removeArtifact(al); @@ -3012,7 +3009,7 @@ bool CGameHandler::sellArtifact(const IMarket *m, const CGHeroInstance *h, Artif int resVal = 0, dump = 1; m->getOffer(art->artType->getId(), rid, dump, resVal, EMarketMode::ARTIFACT_RESOURCE); - removeArtifact(ArtifactLocation(h, h->getArtPos(art))); + removeArtifact(ArtifactLocation(h->id, h->getArtPos(art))); giveResource(h->tempOwner, rid, resVal); return true; } @@ -3750,8 +3747,8 @@ bool CGameHandler::sacrificeArtifact(const IMarket * m, const CGHeroInstance * h for(int i = 0; i < slot.size(); ++i) { - ArtifactLocation al(hero, slot[i]); - const CArtifactInstance * a = al.getArt(); + ArtifactLocation al(hero->id, slot[i]); + const CArtifactInstance * a = hero->getArt(al.slot); if(!a) { @@ -3963,7 +3960,7 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) { assert(a->artType); - ArtifactLocation al(h, ArtifactPosition::PRE_FIRST); + ArtifactLocation al(h->id, ArtifactPosition::PRE_FIRST); if(pos == ArtifactPosition::FIRST_AVAILABLE) { @@ -3978,7 +3975,7 @@ bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactIns al.slot = pos; } - if(a->canBePutAt(al)) + if(a->canBePutAt(h, al.slot)) putArtifact(al, a); else return false; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index b6996aa85..a71d6a1dd 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -130,7 +130,7 @@ public: bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; void removeArtifact(const ArtifactLocation &al) override; - bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override; + bool moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) override; bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); bool eraseArtifactByClient(const ArtifactLocation & al); void synchronizeArtifactHandlerLists(); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index 540751603..c5e03acf5 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -134,7 +134,7 @@ void ApplyGhNetPackVisitor::visitGarrisonHeroSwap(GarrisonHeroSwap & pack) void ApplyGhNetPackVisitor::visitExchangeArtifacts(ExchangeArtifacts & pack) { - gh.throwIfWrongPlayer(&pack, pack.src.owningPlayer()); //second hero can be ally + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.src.artHolder)); //second hero can be ally result = gh.moveArtifact(pack.src, pack.dst); } @@ -154,7 +154,7 @@ void ApplyGhNetPackVisitor::visitAssembleArtifacts(AssembleArtifacts & pack) void ApplyGhNetPackVisitor::visitEraseArtifactByClient(EraseArtifactByClient & pack) { - gh.throwIfWrongPlayer(&pack, pack.al.owningPlayer()); + gh.throwIfWrongPlayer(&pack, gh.getOwner(pack.al.artHolder)); result = gh.eraseArtifactByClient(pack.al); } diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 230d2cf1b..0d54063f1 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -79,7 +79,7 @@ CasualtiesAfterBattle::CasualtiesAfterBattle(const CBattleInfoCallback & battle, logGlobal->debug("War machine has been destroyed"); auto hero = dynamic_ptr_cast (army); if (hero) - removedWarMachines.push_back (ArtifactLocation(hero, hero->getArtPos(warMachine, true))); + removedWarMachines.push_back (ArtifactLocation(hero->id, hero->getArtPos(warMachine, true))); else logGlobal->error("War machine in army without hero"); } @@ -339,7 +339,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) if(slot != ArtifactPosition::PRE_FIRST) { arts.push_back(art); - ma->dst = ArtifactLocation(finishingBattle->winnerHero, slot); + ma->dst = ArtifactLocation(finishingBattle->winnerHero->id, slot); if(ArtifactUtils::isSlotBackpack(slot)) ma->askAssemble = false; gameHandler->sendAndApply(ma); @@ -353,8 +353,8 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + const CArtifactInstance * art = finishingBattle->loserHero->getArt(artSlot.first); if (art && !art->artType->isBig() && art->artType->getId() != ArtifactID::SPELLBOOK) // don't move war machines or locked arts (spellbook) @@ -366,9 +366,9 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { //we assume that no big artifacts can be found MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero, + ma.src = ArtifactLocation(finishingBattle->loserHero->id, ArtifactPosition(ArtifactPosition::BACKPACK_START + slotNumber)); //backpack automatically shifts arts to beginning - const CArtifactInstance * art = ma.src.getArt(); + const CArtifactInstance * art = finishingBattle->loserHero->getArt(ArtifactPosition::BACKPACK_START + slotNumber); if (art->artType->getId() != ArtifactID::GRAIL) //grail may not be won { sendMoveArtifact(art, &ma); @@ -380,12 +380,13 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(finishingBattle->loserHero->commander.get(), artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + // TODO add ->commander.get() !!! + /*const CArtifactInstance* art = ma.src.getArt(); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); - } + }*/ } } } @@ -395,16 +396,17 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; - for (auto artSlot : artifactsWorn) + /*for (auto artSlot : artifactsWorn) { MoveArtifact ma; ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance * art = ma.src.getArt(); + const CArtifactInstance* art = ma.src.getArt(); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); } - } + // TODO add stack !!! + }*/ } } diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index e2561adcf..b851646e4 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -127,12 +127,12 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const if(auto arts = dynamic_ptr_cast(pack)) { - if(auto id1 = std::visit(GetEngagedHeroIds(), arts->src.artHolder)) - if(!vstd::contains(ourIds, *id1)) + if(auto id1 = arts->src.artHolder) + if(!vstd::contains(ourIds, id1)) return true; - if(auto id2 = std::visit(GetEngagedHeroIds(), arts->dst.artHolder)) - if(!vstd::contains(ourIds, *id2)) + if(auto id2 = arts->dst.artHolder) + if(!vstd::contains(ourIds, id2)) return true; return false; } @@ -144,8 +144,8 @@ bool CGarrisonDialogQuery::blocksPack(const CPack * pack) const if(auto art = dynamic_ptr_cast(pack)) { - if (auto id = std::visit(GetEngagedHeroIds(), art->al.artHolder)) - return !vstd::contains(ourIds, *id); + if(auto id = art->al.artHolder) + return !vstd::contains(ourIds, id); } if(auto dismiss = dynamic_ptr_cast(pack)) From 3c5527a222bb7355a2cd38c85258398dc888a17b Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:37:18 +0300 Subject: [PATCH 1000/1248] ArtifactLocation now use ID for artHolder identification part3 --- client/widgets/CArtifactHolder.cpp | 1 + client/widgets/CGarrisonInt.cpp | 16 ++++--- client/windows/CCreatureWindow.cpp | 6 +-- client/windows/CHeroWindow.cpp | 13 +----- lib/CCreatureSet.cpp | 2 +- lib/CCreatureSet.h | 2 +- lib/CGameInfoCallback.cpp | 16 +++++++ lib/CGameInfoCallback.h | 3 ++ lib/networkPacks/ArtifactLocation.h | 57 +++++------------------- lib/networkPacks/NetPacksLib.cpp | 31 +++++++------ server/CGameHandler.cpp | 28 +++++++----- server/battles/BattleResultProcessor.cpp | 16 +++---- 12 files changed, 87 insertions(+), 104 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 48136a6cc..bbe3e07be 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -122,6 +122,7 @@ void CCommanderArtPlace::returnArtToHeroCallback() else { ArtifactLocation src(commanderOwner->id, artifactPos); + src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; ArtifactLocation dst(commanderOwner->id, freeSlot); if(ourArt->canBePutAt(commanderOwner, freeSlot, true)) diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 72e52ca54..8c7a78697 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -196,19 +196,21 @@ bool CGarrisonSlot::highlightOrDropArtifact() artSelected = true; if (myStack) // try dropping the artifact only if the slot isn't empty { - /*ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS); - ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT); - if(pickedArtInst->canBePutAt(dst, true)) + ArtifactLocation src(srcHero->id, ArtifactPosition::TRANSITION_POS); + ArtifactLocation dst(getObj()->id, ArtifactPosition::CREATURE_SLOT); + dst.creature = getSlot(); + + if(pickedArtInst->canBePutAt(myStack, ArtifactPosition::CREATURE_SLOT, true)) { //equip clicked stack - if(dst.getArt()) + if(auto dstArt = myStack->getArt(ArtifactPosition::CREATURE_SLOT)) { //creature can wear only one active artifact //if we are placing a new one, the old one will be returned to the hero's backpack - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero, - ArtifactUtils::getArtBackpackPosition(srcHero, dst.getArt()->getTypeId()))); + LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero->id, + ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()))); } LOCPLINT->cb->swapArtifacts(src, dst); - }*/ + } } } } diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index e1b881775..0db3a97e1 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -974,12 +974,12 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos) const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId()); if(slot != ArtifactPosition::PRE_FIRST) { - //LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner->id, slot)); + auto artLoc = ArtifactLocation(info->owner->id, pos); + artLoc.creature = info->stackNode->armyObj->findStack(info->stackNode); + LOCPLINT->cb->swapArtifacts(artLoc, ArtifactLocation(info->owner->id, slot)); stackArtifactButton.reset(); stackArtifactHelp.reset(); stackArtifactIcon.reset(); redraw(); } } - - diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 35bcc508e..6acc90b27 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -336,18 +336,9 @@ void CHeroWindow::commanderWindow() const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId()); if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack! { - ArtifactLocation src(hero->id, ArtifactPosition::TRANSITION_POS); ArtifactLocation dst(curHero->id, freeSlot); - // TODO add ->commander.get() !!! - /*if(pickedArtInst->canBePutAt(dst, true)) - { //equip clicked stack - if(dst.getArt()) - { - LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero, - ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId()))); - } - LOCPLINT->cb->swapArtifacts(src, dst); - }*/ + dst.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER; + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS), dst); } } else diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index f02ecd4a2..b72f16521 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -458,7 +458,7 @@ const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const return *getStackPtr(slot); } -const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const +CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const { if(hasStackAtSlot(slot)) return stacks.find(slot)->second; diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 634e62ba4..752eb203f 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -253,7 +253,7 @@ public: void setToArmy(CSimpleArmy &src); //erases all our army and moves stacks from src to us; src MUST NOT be an armed object! WARNING: use it wisely. Or better do not use at all. const CStackInstance & getStack(const SlotID & slot) const; //stack must exist - const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr + CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr const CCreature * getCreature(const SlotID & slot) const; //workaround of map issue; int getStackCount(const SlotID & slot) const; TExpType getStackExperience(const SlotID & slot) const; diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 5ab149426..0326ab30a 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -966,6 +966,22 @@ const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid return gs->map->objects[oid.num]; } +CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const +{ + auto hero = const_cast(getHero(loc.artHolder)); + if(loc.creature.has_value()) + { + if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER) + return hero->commander; + else + return hero->getStackPtr(loc.creature.value()); + } + else + { + return hero; + } +} + std::vector CGameInfoCallback::getVisibleTeleportObjects(std::vector ids, PlayerColor player) const { vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index e1a19d5c3..794719187 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -40,6 +40,8 @@ class CGameState; class PathfinderConfig; struct TurnTimerInfo; +struct ArtifactLocation; +class CArtifactSet; class CArmedInstance; class CGObjectInstance; class CGHeroInstance; @@ -174,6 +176,7 @@ public: virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const; virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const; + virtual CArtifactSet * getArtSet(const ArtifactLocation & loc) const; //virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const; //objects diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h index 777a1fdf5..085d295d0 100644 --- a/lib/networkPacks/ArtifactLocation.h +++ b/lib/networkPacks/ArtifactLocation.h @@ -9,67 +9,34 @@ */ #pragma once -#include "../ConstTransitivePtr.h" #include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN -class CGHeroInstance; -class CStackInstance; -class CArmedInstance; -class CArtifactSet; -class CBonusSystemNode; -struct ArtSlotInfo; - -using TArtHolder = std::variant, ConstTransitivePtr>; - struct ArtifactLocation { - TArtHolder artHolder;//TODO: identify holder by id - ArtifactPosition slot = ArtifactPosition::PRE_FIRST; + ObjectInstanceID artHolder; + ArtifactPosition slot; + std::optional creature; ArtifactLocation() - : artHolder(ConstTransitivePtr()) + : artHolder(ObjectInstanceID::NONE) + , slot(ArtifactPosition::PRE_FIRST) + , creature(std::nullopt) { } - template - ArtifactLocation(const T * ArtHolder, ArtifactPosition Slot) - : artHolder(const_cast(ArtHolder)) //we are allowed here to const cast -> change will go through one of our packages... do not abuse! - , slot(Slot) - { - } - ArtifactLocation(TArtHolder ArtHolder, const ArtifactPosition & Slot) - : artHolder(std::move(std::move(ArtHolder))) - , slot(Slot) + ArtifactLocation(const ObjectInstanceID id, const ArtifactPosition & slot = ArtifactPosition::PRE_FIRST) + : artHolder(id) + , slot(slot) + , creature(std::nullopt) { } - template - bool isHolder(const T *t) const - { - if(auto ptrToT = std::get>(artHolder)) - { - return ptrToT == t; - } - return false; - } - - DLL_LINKAGE void removeArtifact(); // BE CAREFUL, this operation modifies holder (gs) - - DLL_LINKAGE const CArmedInstance *relatedObj() const; //hero or the stack owner - DLL_LINKAGE PlayerColor owningPlayer() const; - DLL_LINKAGE CArtifactSet *getHolderArtSet(); - DLL_LINKAGE CBonusSystemNode *getHolderNode(); - DLL_LINKAGE CArtifactSet *getHolderArtSet() const; - DLL_LINKAGE const CBonusSystemNode *getHolderNode() const; - - DLL_LINKAGE const CArtifactInstance *getArt() const; - DLL_LINKAGE CArtifactInstance *getArt(); - DLL_LINKAGE const ArtSlotInfo *getSlot() const; - template void serialize(Handler &h, const int version) + template void serialize(Handler & h, const int version) { h & artHolder; h & slot; + h & creature; } }; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 2a198ebf4..8769f1572 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1656,35 +1656,34 @@ void RebalanceStacks::applyGs(CGameState * gs) const auto srcHero = dynamic_cast(src.army.get()); const auto dstHero = dynamic_cast(dst.army.get()); - auto srcLoc = ArtifactLocation(srcHero->id, ArtifactPosition::CREATURE_SLOT); - auto dstLoc = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); - // TODO set creature id !!! - /*if (auto artHere = srcLoc.getArt()) + auto srcStack = const_cast(src.getStack()); + auto dstStack = const_cast(dst.getStack()); + if(auto srcArt = srcStack->getArt(ArtifactPosition::CREATURE_SLOT)) { - if (dstLoc.getArt()) + if(auto dstArt = dstStack->getArt(ArtifactPosition::CREATURE_SLOT)) { - - auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstLoc.getArt()->getTypeId()); + auto dstSlot = ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId()); if(srcHero && dstSlot != ArtifactPosition::PRE_FIRST) { - dstLoc.getArt()->move (dstLoc, ArtifactLocation (srcHero, dstSlot)); + dstArt->move(*dstStack, ArtifactPosition::CREATURE_SLOT, *srcHero, dstSlot); } //else - artifact cna be lost :/ else { EraseArtifact ea; - ea.al = dstLoc; + ea.al = ArtifactLocation(dstHero->id, ArtifactPosition::CREATURE_SLOT); + ea.al.creature = dst.slot; ea.applyGs(gs); logNetwork->warn("Cannot move artifact! No free slots"); } - artHere->move (srcLoc, dstLoc); + srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); //TODO: choose from dialog } else //just move to the other slot before stack gets erased { - artHere->move (srcLoc, dstLoc); + srcArt->move(*srcStack, ArtifactPosition::CREATURE_SLOT, *dstStack, ArtifactPosition::CREATURE_SLOT); } - }*/ + } if (stackExp) { ui64 totalExp = srcCount * src.army->getStackExperience(src.slot) + dst.army->getStackCount(dst.slot) * dst.army->getStackExperience(dst.slot); @@ -1757,7 +1756,7 @@ void PutArtifact::applyGs(CGameState *gs) assert(!art->getParentNodes().empty()); auto hero = gs->getHero(al.artHolder); assert(hero); - assert(art->canBePutAt(hero, al.slot)); + assert(art && art->canBePutAt(hero, al.slot)); art->putAt(*hero, al.slot); } @@ -1796,12 +1795,12 @@ void EraseArtifact::applyGs(CGameState *gs) void MoveArtifact::applyGs(CGameState * gs) { - auto srcHero = gs->getHero(src.artHolder); - auto dstHero = gs->getHero(dst.artHolder); + auto srcHero = gs->getArtSet(src); + auto dstHero = gs->getArtSet(dst); assert(srcHero); assert(dstHero); auto art = srcHero->getArt(src.slot); - assert(!ArtifactUtils::isSlotEquipment(dst.slot) || !dst.getArt()); + assert(art && art->canBePutAt(dstHero, dst.slot)); art->move(*srcHero, src.slot, *dstHero, dst.slot); } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index f6e524883..424ddf7bb 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2666,29 +2666,32 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) // Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) { - const auto srcHero = getHero(src.artHolder), dstHero = getHero(dst.artHolder); ArtifactLocation srcLoc = src, dstLoc = dst; + const auto srcArtSet = getArtSet(srcLoc); + const auto dstArtSet = getArtSet(dstLoc); + assert(srcArtSet); + assert(dstArtSet); // Make sure exchange is even possible between the two heroes. if(!isAllowedExchange(srcLoc.artHolder, dstLoc.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); - const auto srcArtifact = srcHero->getArt(srcLoc.slot); - const auto dstArtifact = dstHero->getArt(dstLoc.slot); - const bool isDstSlotBackpack = ArtifactUtils::isSlotBackpack(dstLoc.slot); + const auto srcArtifact = srcArtSet->getArt(srcLoc.slot); + const auto dstArtifact = dstArtSet->getArt(dstLoc.slot); + const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dstLoc.slot) : false; if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); - if(dstArtifact && srcHero->getOwner() != dstHero->getOwner() && !isDstSlotBackpack) + if(dstArtifact && getHero(src.artHolder)->getOwner() != getHero(dst.artHolder)->getOwner() && !isDstSlotBackpack) COMPLAIN_RET("Can't touch artifact on hero of another player!"); // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstHero, dstLoc.slot, true)) + if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstArtSet, dstLoc.slot, true)) COMPLAIN_RET("Cannot move artifact!"); - auto srcSlotInfo = srcHero->getSlot(srcLoc.slot); - auto dstSlotInfo = dstHero->getSlot(dstLoc.slot); + auto srcSlotInfo = srcArtSet->getSlot(srcLoc.slot); + auto dstSlotInfo = dstArtSet->getSlot(dstLoc.slot); if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); @@ -2700,9 +2703,9 @@ bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLoca if(isDstSlotBackpack) { - if(!ArtifactUtils::isBackpackFreeSlots(dstHero)) + if(!ArtifactUtils::isBackpackFreeSlots(dstArtSet)) COMPLAIN_RET("Backpack is full!"); - vstd::amin(dstLoc.slot, ArtifactPosition::BACKPACK_START + dstHero->artifactsInBackpack.size()); + vstd::amin(dstLoc.slot, ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size()); } if(!(srcLoc.slot == ArtifactPosition::TRANSITION_POS && dstLoc.slot == ArtifactPosition::TRANSITION_POS)) @@ -2719,8 +2722,9 @@ bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLoca try { - if(ArtifactUtils::checkSpellbookIsNeeded(dstHero, srcArtifact->artType->getId(), dstLoc.slot)) - giveHeroNewArtifact(dstHero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); + auto hero = getHero(dst.artHolder); + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstLoc.slot)) + giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); } catch(const std::bad_variant_access &) { diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 0d54063f1..6d8cb6a79 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -381,12 +381,12 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) { MoveArtifact ma; ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); - // TODO add ->commander.get() !!! - /*const CArtifactInstance* art = ma.src.getArt(); + ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); + const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); - }*/ + } } } } @@ -396,17 +396,17 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; - /*for (auto artSlot : artifactsWorn) + for(auto & artSlot : artifactsWorn) { MoveArtifact ma; - ma.src = ArtifactLocation(armySlot.second, artSlot.first); - const CArtifactInstance* art = ma.src.getArt(); + ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); + ma.src.creature = finishingBattle->loserHero->findStack(finishingBattle->loserHero->commander); + const auto art = finishingBattle->loserHero->commander->getArt(artSlot.first); if (art && !art->artType->isBig()) { sendMoveArtifact(art, &ma); } - // TODO add stack !!! - }*/ + } } } From c2f658d638b190aa00eeb1297dcfe318302fdd48 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 23 Oct 2023 19:39:12 +0300 Subject: [PATCH 1001/1248] Bulk move artifacts. id --- client/NetPacksClient.cpp | 8 +++--- lib/networkPacks/NetPacksLib.cpp | 41 +++++++++++-------------------- lib/networkPacks/PacksForClient.h | 12 ++++----- server/CGameHandler.cpp | 3 +-- 4 files changed, 25 insertions(+), 39 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index c0d60dea0..1eed81ff0 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -295,7 +295,7 @@ void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack) void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) { - /*auto applyMove = [this, &pack](std::vector & artsPack) -> void + auto applyMove = [this, &pack](std::vector & artsPack) -> void { for(auto & slotToMove : artsPack) { @@ -306,8 +306,8 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) } }; - auto srcOwner = std::get>(pack.srcArtHolder)->tempOwner; - auto dstOwner = std::get>(pack.dstArtHolder)->tempOwner; + auto srcOwner = cl.getOwner(pack.srcArtHolder); + auto dstOwner = cl.getOwner(pack.dstArtHolder); // Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization. callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size()); @@ -316,7 +316,7 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) applyMove(pack.artsPack0); if(pack.swap) - applyMove(pack.artsPack1);*/ + applyMove(pack.artsPack1); } void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack) diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 8769f1572..6356ab7fc 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1813,8 +1813,8 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) BULK_PUT }; - auto bulkArtsOperation = [this](std::vector & artsPack, - CArtifactSet * artSet, EBulkArtsOp operation) -> void + auto bulkArtsOperation = [this, gs](std::vector & artsPack, + CArtifactSet & artSet, EBulkArtsOp operation) -> void { int numBackpackArtifactsMoved = 0; for(auto & slot : artsPack) @@ -1827,25 +1827,22 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) { srcPos = ArtifactPosition(srcPos.num - numBackpackArtifactsMoved); } - const auto * slotInfo = artSet->getSlot(srcPos); - assert(slotInfo); - auto * art = const_cast(slotInfo->getArt()); + auto * art = artSet.getArt(srcPos); assert(art); - /*switch(operation) + switch(operation) { case EBulkArtsOp::BULK_MOVE: - const_cast(art)->move( - ArtifactLocation(srcArtHolder, srcPos), ArtifactLocation(dstArtHolder, slot.dstPos)); + art->move(artSet, srcPos, *gs->getHero(dstArtHolder), slot.dstPos); break; case EBulkArtsOp::BULK_REMOVE: - art->removeFrom(ArtifactLocation(dstArtHolder, srcPos)); + art->removeFrom(artSet, srcPos); break; case EBulkArtsOp::BULK_PUT: - art->putAt(ArtifactLocation(srcArtHolder, slot.dstPos)); + art->putAt(*gs->getHero(srcArtHolder), slot.dstPos); break; default: break; - }*/ + } if(srcPos >= ArtifactPosition::BACKPACK_START) { @@ -1854,23 +1851,23 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) } }; + auto * leftSet = gs->getArtSet(ArtifactLocation(srcArtHolder)); if(swap) { // Swap - auto * leftSet = getSrcHolderArtSet(); - auto * rightSet = getDstHolderArtSet(); + auto * rightSet = gs->getArtSet(ArtifactLocation(dstArtHolder)); CArtifactFittingSet artFittingSet(leftSet->bearerType()); artFittingSet.artifactsWorn = rightSet->artifactsWorn; artFittingSet.artifactsInBackpack = rightSet->artifactsInBackpack; - bulkArtsOperation(artsPack1, rightSet, EBulkArtsOp::BULK_REMOVE); - bulkArtsOperation(artsPack0, leftSet, EBulkArtsOp::BULK_MOVE); - bulkArtsOperation(artsPack1, &artFittingSet, EBulkArtsOp::BULK_PUT); + bulkArtsOperation(artsPack1, *rightSet, EBulkArtsOp::BULK_REMOVE); + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack1, artFittingSet, EBulkArtsOp::BULK_PUT); } else { - bulkArtsOperation(artsPack0, getSrcHolderArtSet(), EBulkArtsOp::BULK_MOVE); + bulkArtsOperation(artsPack0, *leftSet, EBulkArtsOp::BULK_MOVE); } } @@ -2549,14 +2546,4 @@ const CArtifactInstance * ArtSlotInfo::getArt() const return artifact; } -CArtifactSet * BulkMoveArtifacts::getSrcHolderArtSet() -{ - return std::visit(GetBase(), srcArtHolder); -} - -CArtifactSet * BulkMoveArtifacts::getDstHolderArtSet() -{ - return std::visit(GetBase(), dstArtHolder); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 31bc4752e..f768c297d 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1060,14 +1060,16 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack } }; - TArtHolder srcArtHolder; - TArtHolder dstArtHolder; + ObjectInstanceID srcArtHolder; + ObjectInstanceID dstArtHolder; BulkMoveArtifacts() - : swap(false) + : srcArtHolder(ObjectInstanceID::NONE) + , dstArtHolder(ObjectInstanceID::NONE) + , swap(false) { } - BulkMoveArtifacts(TArtHolder srcArtHolder, TArtHolder dstArtHolder, bool swap) + BulkMoveArtifacts(const ObjectInstanceID srcArtHolder, const ObjectInstanceID dstArtHolder, bool swap) : srcArtHolder(std::move(srcArtHolder)) , dstArtHolder(std::move(dstArtHolder)) , swap(swap) @@ -1079,8 +1081,6 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack std::vector artsPack0; std::vector artsPack1; bool swap; - CArtifactSet * getSrcHolderArtSet(); - CArtifactSet * getDstHolderArtSet(); void visitTyped(ICPackVisitor & visitor) override; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 424ddf7bb..8bda133e7 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2750,8 +2750,7 @@ bool CGameHandler::bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID if((!psrcHero) || (!pdstHero)) COMPLAIN_RET("bulkMoveArtifacts: wrong hero's ID"); - BulkMoveArtifacts ma(static_cast>(psrcHero), - static_cast>(pdstHero), swap); + BulkMoveArtifacts ma(srcHero, dstHero, swap); auto & slotsSrcDst = ma.artsPack0; auto & slotsDstSrc = ma.artsPack1; From 7e6ab5e87bb9738b4b784a0dd0871e75462d8aec Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Tue, 24 Oct 2023 17:32:09 +0300 Subject: [PATCH 1002/1248] fix test & fix build & suggested changes --- lib/CArtHandler.cpp | 4 ++-- lib/CGameInfoCallback.cpp | 1 + server/battles/BattleResultProcessor.cpp | 2 +- server/queries/MapQueries.cpp | 14 -------------- test/game/CGameStateTest.cpp | 4 ++-- 5 files changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index e943b58c6..28baf2412 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -693,7 +693,7 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) a->possibleSlots[ArtBearer::HERO].clear(); a->possibleSlots[ArtBearer::CREATURE].clear(); } - for(auto & slot : ArtifactUtils::commanderSlots()) + for(const auto & slot : ArtifactUtils::commanderSlots()) a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot)); } @@ -1080,7 +1080,7 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map) { - for(auto & slot : ArtifactUtils::allWornSlots()) + for(const auto & slot : ArtifactUtils::allWornSlots()) { serializeJsonSlot(handler, slot, map); } diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 0326ab30a..44de7bee3 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -16,6 +16,7 @@ #include "gameState/TavernHeroesPool.h" #include "gameState/QuestInfo.h" #include "mapObjects/CGHeroInstance.h" +#include "networkPacks/ArtifactLocation.h" #include "CGeneralTextHandler.h" #include "StartInfo.h" // for StartInfo #include "battle/BattleInfo.h" // for BattleInfo diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 6d8cb6a79..4b3f60491 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -396,7 +396,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto armySlot : battle.battleGetArmyObject(loser)->stacks) { auto artifactsWorn = armySlot.second->artifactsWorn; - for(auto & artSlot : artifactsWorn) + for(const auto & artSlot : artifactsWorn) { MoveArtifact ma; ma.src = ArtifactLocation(finishingBattle->loserHero->id, artSlot.first); diff --git a/server/queries/MapQueries.cpp b/server/queries/MapQueries.cpp index b851646e4..74319bc26 100644 --- a/server/queries/MapQueries.cpp +++ b/server/queries/MapQueries.cpp @@ -17,20 +17,6 @@ #include "../../lib/networkPacks/PacksForServer.h" #include "../../lib/serializer/Cast.h" -struct GetEngagedHeroIds -{ - std::optional operator()(const ConstTransitivePtr & h) const - { - return h->id; - } - std::optional operator()(const ConstTransitivePtr & s) const - { - if(s->armyObj && s->armyObj->ID == Obj::HERO) - return s->armyObj->id; - return std::optional(); - } -}; - TimerPauseQuery::TimerPauseQuery(CGameHandler * owner, PlayerColor player): CQuery(owner) { diff --git a/test/game/CGameStateTest.cpp b/test/game/CGameStateTest.cpp index 02222e712..1c9334d1f 100644 --- a/test/game/CGameStateTest.cpp +++ b/test/game/CGameStateTest.cpp @@ -240,7 +240,7 @@ TEST_F(CGameStateTest, issue2765) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(defender, ArtifactPosition::MACH1); + pack.al = ArtifactLocation(defender->id, ArtifactPosition::MACH1); pack.art = a; gameCallback->sendAndApply(&pack); } @@ -334,7 +334,7 @@ TEST_F(CGameStateTest, battleResurrection) gameCallback->sendAndApply(&na); PutArtifact pack; - pack.al = ArtifactLocation(attacker, ArtifactPosition::SPELLBOOK); + pack.al = ArtifactLocation(attacker->id, ArtifactPosition::SPELLBOOK); pack.art = a; gameCallback->sendAndApply(&pack); } From 4f2cde018cd66718d919176ae657f2c62a8fd366 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 29 Oct 2023 21:25:39 +0100 Subject: [PATCH 1003/1248] Fixed most of reported issues, removed unused code. --- client/lobby/RandomMapTab.cpp | 53 +++++------ lib/rmg/CMapGenOptions.cpp | 161 +++++++++++++++++++++++----------- lib/rmg/CMapGenOptions.h | 6 +- lib/rmg/CRmgTemplate.cpp | 16 +++- lib/rmg/CRmgTemplate.h | 7 +- test/map/CMapFormatTest.cpp | 2 +- 6 files changed, 152 insertions(+), 93 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 737880184..ed6972681 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -209,13 +209,12 @@ void RandomMapTab::updateMapInfoByHost() for (auto& player : mapGenOptions->getPlayersSettings()) { PlayerInfo playerInfo; - playerInfo.isFactionRandom = (player.second.getStartingTown() == CMapGenOptions::CPlayerSettings::RANDOM_TOWN); + playerInfo.isFactionRandom = (player.second.getStartingTown() == FactionID::RANDOM); playerInfo.canComputerPlay = (player.second.getPlayerType() != EPlayerType::HUMAN); playerInfo.canHumanPlay = (player.second.getPlayerType() != EPlayerType::COMP_ONLY); auto team = player.second.getTeam(); playerInfo.team = team; - //occupiedTeams.insert(team); playerInfo.hasMainTown = true; playerInfo.generateHeroAtMainTown = true; mapInfo->mapHeader->players[player.first] = playerInfo; @@ -244,31 +243,16 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) compTeamsAllowed.insert(i); } } + std::set humanCountAllowed; auto * tmpl = mapGenOptions->getMapTemplate(); if(tmpl) { playerCountAllowed = tmpl->getPlayers().getNumbers(); - compCountAllowed = tmpl->getCpuPlayers().getNumbers(); - auto compNumbers = tmpl->getCpuPlayers().getNumbers(); - if (!compNumbers.empty()) - { - compCountAllowed = compNumbers; - minComps = *boost::min_element(compCountAllowed); - } - - playerCountAllowed = tmpl->getPlayers().getNumbers(); - - auto minPlayerCount = *boost::min_element(playerCountAllowed); - auto maxCompCount = *boost::max_element(compCountAllowed); - for (int i = 1; i >= (minPlayerCount - maxCompCount) && i >= 1; i--) - { - //We can always add extra CPUs to meet the minimum total player count - playerCountAllowed.insert(i); - } + humanCountAllowed = tmpl->getHumanPlayers().getNumbers(); // Unused now? } - si8 playerLimit = opts->getPlayerLimit(); + si8 playerLimit = opts->getMaxPlayersCount(); si8 humanOrCpuPlayerCount = opts->getHumanOrCpuPlayerCount(); si8 compOnlyPlayersCount = opts->getCompOnlyPlayerCount(); @@ -282,12 +266,23 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { return humanOrCpuPlayerCount <= el; }); - - if(!playerTeamsAllowed.count(opts->getTeamCount())) - { - opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); - } } + else // Random + { + vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) + { + return (playerLimit - 1) < el; // Must leave at least 1 human player + }); + vstd::erase_if(playerTeamsAllowed, [playerLimit](int el) + { + return playerLimit <= el; + }); + } + if(!playerTeamsAllowed.count(opts->getTeamCount())) + { + opts->setTeamCount(CMapGenOptions::RANDOM_SIZE); + } + if(compOnlyPlayersCount != CMapGenOptions::RANDOM_SIZE) { // This setting doesn't impact total number of players @@ -328,8 +323,6 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) } if(auto w = widget("groupMaxPlayers")) { - // FIXME: OH3 allows any setting here, even if currently selected template doesn't fit it - // TODO: Set max players to current template limit wherever template is explicitely selected w->setSelected(opts->getHumanOrCpuPlayerCount()); deactivateButtonsFrom(*w, playerCountAllowed); } @@ -442,8 +435,8 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): const JsonNode config(JsonPath::builtin("config/widgets/randomMapTeamsWidget.json")); variables = config["variables"]; - int totalPlayers = randomMapTab.obtainMapGenOptions().getPlayerLimit(); - //randomMapTab.obtainMapGenOptions().getTotalPlayersCount(); + //int totalPlayers = randomMapTab.obtainMapGenOptions().getPlayerLimit(); + int totalPlayers = randomMapTab.obtainMapGenOptions().getMaxPlayersCount(); assert(totalPlayers <= PlayerColor::PLAYER_LIMIT_I); auto settings = randomMapTab.obtainMapGenOptions().getPlayersSettings(); variables["totalPlayers"].Integer() = totalPlayers; @@ -482,7 +475,7 @@ TeamAlignmentsWidget::TeamAlignmentsWidget(RandomMapTab & randomMapTab): OBJ_CONSTRUCTION; - // Window should have X * X columns, where X is players + compOnly players. + // Window should have X * X columns, where X is max players allowed for current settings // For random player count, X is 8 if (totalPlayers > settings.size()) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index a2a741e08..fcf87bb0d 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -85,11 +85,48 @@ void CMapGenOptions::setHumanOrCpuPlayerCount(si8 value) resetPlayersMap(); } -si8 CMapGenOptions::getTotalPlayersCount() const +si8 CMapGenOptions::getMinPlayersCount(bool withTemplateLimit) const { auto totalPlayers = 0; si8 humans = getHumanOrCpuPlayerCount(); si8 cpus = getCompOnlyPlayerCount(); + + if (humans == RANDOM_SIZE && cpus == RANDOM_SIZE) + { + totalPlayers = 2; + } + else if (humans == RANDOM_SIZE) + { + totalPlayers = cpus + 1; // Must add at least 1 player + } + else if (cpus == RANDOM_SIZE) + { + totalPlayers = humans; + } + else + { + totalPlayers = humans + cpus; + } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amax(totalPlayers, playersRange.minValue()); + } + + // Can't play without at least 2 players + vstd::amax(totalPlayers, 2); + return totalPlayers; +} + +si8 CMapGenOptions::getMaxPlayersCount(bool withTemplateLimit) const +{ + // Max number of players possible with current settings + auto totalPlayers = 0; + si8 humans = getHumanOrCpuPlayerCount(); + si8 cpus = getCompOnlyPlayerCount(); if (humans == RANDOM_SIZE || cpus == RANDOM_SIZE) { totalPlayers = PlayerColor::PLAYER_LIMIT_I; @@ -98,6 +135,15 @@ si8 CMapGenOptions::getTotalPlayersCount() const { totalPlayers = humans + cpus; } + + if (withTemplateLimit && mapTemplate) + { + auto playersRange = mapTemplate->getPlayers(); + + //New template can also impose higher limit than current settings + vstd::amin(totalPlayers, playersRange.maxValue()); + } + assert (totalPlayers <= PlayerColor::PLAYER_LIMIT_I); assert (totalPlayers >= 2); return totalPlayers; @@ -121,10 +167,11 @@ si8 CMapGenOptions::getCompOnlyPlayerCount() const si8 CMapGenOptions::getPlayerLimit() const { + //How many players could we set with current template, ignoring other settings si8 playerLimit = PlayerColor::PLAYER_LIMIT_I; if (auto temp = getMapTemplate()) { - playerLimit = *boost::max_element(temp->getPlayers().getNumbers()); + playerLimit = static_cast(temp->getPlayers().maxValue()); } return playerLimit; } @@ -207,48 +254,28 @@ void CMapGenOptions::resetPlayersMap() savePlayersMap(); - /* - //Remove players who have undefined properties - vstd::erase_if(players, [](const std::pair & p) - { - return p.second.getPlayerType() != EPlayerType::AI && p.second.getStartingTown() == CPlayerSettings::RANDOM_TOWN; - }); - */ + int realPlayersCnt = getMaxPlayersCount(); - // FIXME: This should be total players count - int realPlayersCnt = getHumanOrCpuPlayerCount(); - if (realPlayersCnt != RANDOM_SIZE) + //Trim the number of AI players, then CPU-only players, finally human players + auto eraseLastPlayer = [this](EPlayerType playerType) -> bool { - //Trim the number of AI players, then CPU-only players, finally human players - auto eraseLastPlayer = [this](EPlayerType playerType) -> bool + for (auto it = players.rbegin(); it != players.rend(); ++it) { - //FIXME: Infinite loop for 0 players - for (auto it = players.rbegin(); it != players.rend(); ++it) + if (it->second.getPlayerType() == playerType) { - if (it->second.getPlayerType() == playerType) - { - players.erase(it->first); - return true; - } + players.erase(it->first); + return true; } - return false; //Can't earse any player of this type - }; - - while (players.size() > realPlayersCnt) - { - while (eraseLastPlayer(EPlayerType::AI)); - while (eraseLastPlayer(EPlayerType::COMP_ONLY)); - while (eraseLastPlayer(EPlayerType::HUMAN)); } - } - else - { - //If count is random, generate info for all the players - realPlayersCnt = PlayerColor::PLAYER_LIMIT_I; - } + return false; //Can't earse any player of this type + }; - int realCompOnlyPlayersCnt = getCompOnlyPlayerCount(); - //int totalPlayersLimit = getPlayerLimit(); + while (players.size() > realPlayersCnt) + { + while (eraseLastPlayer(EPlayerType::AI)); + while (eraseLastPlayer(EPlayerType::COMP_ONLY)); + while (eraseLastPlayer(EPlayerType::HUMAN)); + } //First colors from the list are assigned to human players, then to CPU players std::vector availableColors; @@ -263,7 +290,7 @@ void CMapGenOptions::resetPlayersMap() { if (player.second.getPlayerType() == playerType) { - vstd::erase(availableColors, player.second.getColor()); //FIXME: Where is this color initialized at lobby launch? + vstd::erase(availableColors, player.second.getColor()); } } }; @@ -409,13 +436,17 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) setHeight(sizes.first.y); setHasTwoLevels(sizes.first.z - 1); } - - // FIXME: GUI settings are not the same as template parameters - // TODO: Recalculate GUI ranges in separate method - if(!mapTemplate->getPlayers().isInRange(getHumanOrCpuPlayerCount())) + + si8 maxPlayerCount = getMaxPlayersCount(false); + si8 minPlayerCount = getMinPlayersCount(false); + + // Neither setting can fit within the template range + if(!mapTemplate->getPlayers().isInRange(minPlayerCount) && + !mapTemplate->getPlayers().isInRange(maxPlayerCount)) + { setHumanOrCpuPlayerCount(RANDOM_SIZE); - if(!mapTemplate->getCpuPlayers().isInRange(getCompOnlyPlayerCount())) setCompOnlyPlayerCount(RANDOM_SIZE); + } if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) setWaterContent(EWaterContent::RANDOM); } @@ -473,11 +504,17 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) logGlobal->info("RMG template name: %s", mapTemplate->getName()); + auto maxPlayers = getMaxPlayersCount(); if (getHumanOrCpuPlayerCount() == RANDOM_SIZE) { auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); //ignore all non-randomized players, make sure these players will not be missing after roll possiblePlayers.erase(possiblePlayers.begin(), possiblePlayers.lower_bound(countHumanPlayers() + countCompOnlyPlayers())); + + vstd::erase_if(possiblePlayers, [maxPlayers](int i) + { + return i > maxPlayers; + }); assert(!possiblePlayers.empty()); setHumanOrCpuPlayerCount (*RandomGeneratorUtil::nextItem(possiblePlayers, rand)); updatePlayers(); @@ -490,7 +527,13 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) } if(compOnlyPlayerCount == RANDOM_SIZE) { - auto possiblePlayers = mapTemplate->getCpuPlayers().getNumbers(); + // Use remaining range + auto presentPlayers = getHumanOrCpuPlayerCount(); + auto possiblePlayers = mapTemplate->getPlayers().getNumbers(); + vstd::erase_if(possiblePlayers, [maxPlayers, presentPlayers](int i) + { + return i > (maxPlayers - presentPlayers); + }); compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); updateCompOnlyPlayers(); } @@ -541,13 +584,8 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) default: assert(false); } - // FIXME: Every player is player 0 with type of AI - // FIXME: player.first != player.second.getColor() - // TODO: Set player color everywhere players is set, or only once here logGlobal->trace("Player %d: %s", player.second.getColor(), playerType); } - // FXIME: Do not set this again after options were set - setCompOnlyPlayerCount(cpuOnlyPlayers); //human players are set automatically (?) logGlobal->info("Final player config: %d total, %d cpu-only", players.size(), static_cast(getCompOnlyPlayerCount())); } @@ -669,21 +707,38 @@ std::vector CMapGenOptions::getPossibleTemplates() const if(!tmpl->isWaterContentAllowed(getWaterContent())) return true; - if(getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) + auto humanOrCpuPlayerCount = getHumanOrCpuPlayerCount(); + auto compOnlyPlayerCount = getCompOnlyPlayerCount(); + // Check if total number of players fall inside given range + + if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE && compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getPlayers().isInRange(getHumanOrCpuPlayerCount())) + if (!tmpl->getPlayers().isInRange(humanOrCpuPlayerCount + compOnlyPlayerCount)) + return true; + + } + else if(humanOrCpuPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + // We can always add any number CPU players, but not subtract + if (!(humanOrCpuPlayerCount <= tmpl->getPlayers().maxValue())) + return true; + } + else if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) + { + //We must fit at least one more human player, but can add any number + if (!(compOnlyPlayerCount < tmpl->getPlayers().maxValue())) return true; } else { // Human players shouldn't be banned when playing with random player count - if(humanPlayers > *boost::min_element(tmpl->getPlayers().getNumbers())) + if(humanPlayers > tmpl->getPlayers().minValue()) return true; } if(compOnlyPlayerCount != CMapGenOptions::RANDOM_SIZE) { - if (!tmpl->getCpuPlayers().isInRange(compOnlyPlayerCount)) + if (!tmpl->getHumanPlayers().isInRange(compOnlyPlayerCount)) return true; } diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 7ea58bf5d..1fc4fd547 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -53,9 +53,6 @@ public: TeamID getTeam() const; void setTeam(const TeamID & value); - /// Constant for a random town selection. - static const si32 RANDOM_TOWN = -1; - private: PlayerColor color; si32 startingTown; @@ -91,7 +88,8 @@ public: si8 getHumanOrCpuPlayerCount() const; void setHumanOrCpuPlayerCount(si8 value); - si8 getTotalPlayersCount() const; + si8 getMinPlayersCount(bool withTemplateLimit = true) const; + si8 getMaxPlayersCount(bool withTemplateLimit = true) const; si8 getPlayerLimit() const; /// The count of the teams ranging from 0 to or RANDOM_SIZE for random. diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 3d96df035..98887914d 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -557,9 +557,9 @@ const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getPlayers() const return players; } -const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getCpuPlayers() const +const CRmgTemplate::CPlayerCountRange & CRmgTemplate::getHumanPlayers() const { - return cpuPlayers; + return humanPlayers; } const CRmgTemplate::Zones & CRmgTemplate::getZones() const @@ -675,13 +675,23 @@ void CRmgTemplate::CPlayerCountRange::fromString(const std::string & value) } } +int CRmgTemplate::CPlayerCountRange::maxValue() const +{ + return *boost::max_element(getNumbers()); +} + +int CRmgTemplate::CPlayerCountRange::minValue() const +{ + return *boost::min_element(getNumbers()); +} + void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("name", name); serializeSize(handler, minSize, "minSize"); serializeSize(handler, maxSize, "maxSize"); serializePlayers(handler, players, "players"); - serializePlayers(handler, cpuPlayers, "cpu"); + serializePlayers(handler, humanPlayers, "cpu"); // TODO: Rename this parameter { auto connectionsData = handler.enterArray("connections"); diff --git a/lib/rmg/CRmgTemplate.h b/lib/rmg/CRmgTemplate.h index 3f5a91197..3281dddb5 100644 --- a/lib/rmg/CRmgTemplate.h +++ b/lib/rmg/CRmgTemplate.h @@ -231,6 +231,9 @@ public: std::string toString() const; void fromString(const std::string & value); + int maxValue() const; + int minValue() const; + private: std::vector > range; }; @@ -247,7 +250,7 @@ public: const std::string & getName() const; const CPlayerCountRange & getPlayers() const; - const CPlayerCountRange & getCpuPlayers() const; + const CPlayerCountRange & getHumanPlayers() const; std::pair getMapSizes() const; const Zones & getZones() const; const std::vector & getConnectedZoneIds() const; @@ -261,7 +264,7 @@ private: std::string id; std::string name; int3 minSize, maxSize; - CPlayerCountRange players, cpuPlayers; + CPlayerCountRange players, humanPlayers; Zones zones; std::vector connectedZoneIds; std::set allowedWaterContent; diff --git a/test/map/CMapFormatTest.cpp b/test/map/CMapFormatTest.cpp index f11f20543..20078ea11 100644 --- a/test/map/CMapFormatTest.cpp +++ b/test/map/CMapFormatTest.cpp @@ -46,7 +46,7 @@ TEST(MapFormat, Random) CRmgTemplate tmpl; std::shared_ptr zoneOptions = std::make_shared(); - const_cast(tmpl.getCpuPlayers()).addRange(1, 4); + const_cast(tmpl.getHumanPlayers()).addRange(1, 4); const_cast(tmpl.getZones())[0] = zoneOptions; zoneOptions->setOwner(1); From 1028041eaeb454a1c1ebbd2ab05b5875037ff43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 07:48:17 +0100 Subject: [PATCH 1004/1248] Fixed messed up player count in 7sb0c non-standard template --- Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index 978ea9453..ed62a587d 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -2,7 +2,7 @@ "7SB0c" : { "minSize" : "s+u", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", + "players" : "2-7", "cpu" : "2-6", "zones" : { "1" : From f39edf94139010c44d5e6b398f1f3914387ff5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 07:48:51 +0100 Subject: [PATCH 1005/1248] Update ally and enemy flags when selecting new template --- client/lobby/RandomMapTab.cpp | 7 +------ lib/rmg/CMapGenOptions.cpp | 5 +++-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index ed6972681..651a74f14 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -183,11 +183,7 @@ void RandomMapTab::updateMapInfoByHost() mapInfo->mapHeader->twoLevel = mapGenOptions->getHasTwoLevels(); // Generate player information - int playersToGen = PlayerColor::PLAYER_LIMIT_I; - if(mapGenOptions->getHumanOrCpuPlayerCount() != CMapGenOptions::RANDOM_SIZE) - { - playersToGen = mapGenOptions->getHumanOrCpuPlayerCount(); - } + int playersToGen = mapGenOptions->getMaxPlayersCount(); mapInfo->mapHeader->howManyTeams = playersToGen; @@ -224,7 +220,6 @@ void RandomMapTab::updateMapInfoByHost() mapInfoChanged(mapInfo, mapGenOptions); } -// This method only sets GUI options, doesn't alter any actual configurations done void RandomMapTab::setMapGenOptions(std::shared_ptr opts) { mapGenOptions = opts; diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index fcf87bb0d..e4b518890 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -250,7 +250,7 @@ void CMapGenOptions::initPlayersMap() void CMapGenOptions::resetPlayersMap() { // Called when number of player changes - //But do not update info about already made selections + // But do not update info about already made selections savePlayersMap(); @@ -449,6 +449,8 @@ void CMapGenOptions::setMapTemplate(const CRmgTemplate * value) } if(!mapTemplate->getWaterContentAllowed().count(getWaterContent())) setWaterContent(EWaterContent::RANDOM); + + resetPlayersMap(); // Update teams and player count } } @@ -484,7 +486,6 @@ void CMapGenOptions::setPlayerTeam(const PlayerColor & color, const TeamID & tea { auto it = players.find(color); assert(it != players.end()); - // TODO: Make pivate friend method to avoid unintended changes? it->second.setTeam(team); customizedPlayers = true; } From ec0bf05163053df9b6fea7c012d8c1cd1729f3a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 17:21:03 +0100 Subject: [PATCH 1006/1248] Rename "cpu" parameter to correct "humans" --- Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json | 2 +- Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON | 2 +- Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON | 2 +- config/schemas/template.json | 2 +- lib/rmg/CRmgTemplate.cpp | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json index 539c90dbc..d62bdde01 100755 --- a/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json +++ b/Mods/vcmi/config/vcmi/rmg/hdmod/coldshadowsFantasy.json @@ -2,7 +2,7 @@ "Coldshadow's Fantasy": { "minSize" : "xl+u", "maxSize" : "g+u", - "players" : "4-8", "cpu" : "3-6", + "players" : "4-8", "humans" : "3-6", "zones": { "1": diff --git a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON index 33af217a1..015cf2168 100644 --- a/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON +++ b/Mods/vcmi/config/vcmi/rmg/hdmodUnused/midnightMix.JSON @@ -5117,7 +5117,7 @@ "Midnight Mix 34" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "6", "cpu" : "2", + "players" : "6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON index d88b64e4a..189124ae2 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/meetingInMuzgob.JSON @@ -2,7 +2,7 @@ "Meeting in Muzgob" : { "minSize" : "l", "maxSize" : "xl+u", - "players" : "2-6", "cpu" : "2", + "players" : "2-6", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON index 1c18e5574..ba1663150 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON @@ -2,7 +2,7 @@ "Newcomers" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-3", "cpu" : "1", + "players" : "2-3", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON index 2be3522d0..8b76bb100 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON @@ -2,7 +2,7 @@ "South of Hell" : { "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON index e3d640f71..d3f603b66 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/worldsAtWar.JSON @@ -2,7 +2,7 @@ "Worlds at War" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2", "cpu" : "3", + "players" : "2", "humans" : "3", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON index 399bc52ed..a71dc2ae4 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3unused/gauntlet.JSON @@ -2,7 +2,7 @@ "Gauntlet" : { "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "1", "cpu" : "5", + "players" : "1", "humans" : "5", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON index 5a8c3ca18..a8f6e81d4 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON @@ -2,7 +2,7 @@ "2SM0k" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "6", + "players" : "2", "humans" : "6", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index bf6bc85e0..3cc442b31 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -3,7 +3,7 @@ { "minSize" : "s", "maxSize" : "m+u", "players" : "2", - "cpu": "1-2", + "humans": "1-2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON index 61b8fd0aa..6b784d53f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON @@ -2,7 +2,7 @@ "3SB0b" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON index 47647e043..3be8223b8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON @@ -2,7 +2,7 @@ "3SB0c" : { "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2", "cpu" : "1", + "players" : "2", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON index 774a10cfa..ca3b27d8c 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON @@ -2,7 +2,7 @@ "5SB0a" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", + "players" : "2-3", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON index cae17f7f8..c92c16a7c 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON @@ -2,7 +2,7 @@ "5SB0b" : { "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "cpu" : "2", + "players" : "2-3", "humans" : "2", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON index eb3c34ba6..75428a8d8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON @@ -2,7 +2,7 @@ "7SB0b" : { "minSize" : "s", "maxSize" : "l", - "players" : "2-6", "cpu" : "1", + "players" : "2-6", "humans" : "1", "zones" : { "1" : diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index ed62a587d..aefb1e142 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -2,7 +2,7 @@ "7SB0c" : { "minSize" : "s+u", "maxSize" : "l", - "players" : "2-7", "cpu" : "2-6", + "players" : "2-7", "humans" : "2-6", "zones" : { "1" : diff --git a/config/schemas/template.json b/config/schemas/template.json index c91f3bd4d..67cbca49f 100644 --- a/config/schemas/template.json +++ b/config/schemas/template.json @@ -119,7 +119,7 @@ "description" : "Number of players that will be present on map (human or AI)", "type": "string" }, - "cpu" : { + "humans" : { "description" : "Optional, number of AI-only players", "type": "string" }, diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 98887914d..5a688da6a 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -691,7 +691,7 @@ void CRmgTemplate::serializeJson(JsonSerializeFormat & handler) serializeSize(handler, minSize, "minSize"); serializeSize(handler, maxSize, "maxSize"); serializePlayers(handler, players, "players"); - serializePlayers(handler, humanPlayers, "cpu"); // TODO: Rename this parameter + serializePlayers(handler, humanPlayers, "humans"); // TODO: Rename this parameter { auto connectionsData = handler.enterArray("connections"); From 5962e5c0e0fefa770ae909783372b0ac1ece9774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 19:47:59 +0100 Subject: [PATCH 1007/1248] Fix infinite loop --- lib/rmg/CMapGenOptions.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index e4b518890..0951932bd 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -535,7 +535,14 @@ void CMapGenOptions::finalize(CRandomGenerator & rand) { return i > (maxPlayers - presentPlayers); }); - compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + if (possiblePlayers.empty()) + { + compOnlyPlayerCount = 0; + } + else + { + compOnlyPlayerCount = *RandomGeneratorUtil::nextItem(possiblePlayers, rand); + } updateCompOnlyPlayers(); } if(compOnlyTeamCount == RANDOM_SIZE) From d3352cc6daea250f7a1da6863cf30d899c982410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 19:54:58 +0100 Subject: [PATCH 1008/1248] Fix line endings --- .../config/vcmi/rmg/symmetric/2sm2i(2).JSON | 152 +++++++++--------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON index 3cc442b31..18dc90ed9 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm2i(2).JSON @@ -1,76 +1,76 @@ -{ - "2SM2i(2)" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", - "humans": "1-2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "5", "b" : "4", "guard" : 6000 } - ] - } -} +{ + "2SM2i(2)" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", + "humans": "1-2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "5", "b" : "4", "guard" : 6000 } + ] + } +} From 3504cd4e6cd93520686eb5a152d43e959f19d7d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 19:58:58 +0100 Subject: [PATCH 1009/1248] More EOL conversions --- .../config/vcmi/rmg/heroes3/newcomers.JSON | 354 +++++++++--------- .../config/vcmi/rmg/heroes3/southOfHell.JSON | 330 ++++++++-------- .../vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON | 210 +++++------ .../vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON | 210 +++++------ .../vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON | 246 ++++++------ .../vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON | 242 ++++++------ .../vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON | 194 +++++----- .../vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON | 210 +++++------ .../vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON | 202 +++++----- 9 files changed, 1099 insertions(+), 1099 deletions(-) diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON index ba1663150..7593292e7 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/newcomers.JSON @@ -1,177 +1,177 @@ -{ - "Newcomers" : - { - "minSize" : "m+u", "maxSize" : "xl+u", - "players" : "2-3", "humans" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 500, "max" : 3000, "density" : 10 } - ] - }, - "2" : - { - "type" : "treasure", - "size" : 10, - "owner" : 1, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "treasure", - "size" : 10, - "owner" : 2, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 10, - "owner" : 3, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "9" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "10" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "11" : - { - "type" : "treasure", - "size" : 10, - "owner" : 4, - "monsters" : "weak", - "playerTowns" : { "towns" : 1 }, - "allowedTowns" : [ "conflux" ], - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "12" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 3000, "max" : 6000, "density" : 9 }, - { "min" : 6000, "max" : 15000, "density" : 6 }, - { "min" : 15000, "max" : 20000, "density" : 1 } - ] - }, - "13" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - }, - "14" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 12 - } - }, - "connections" : - [ - { "a" : "12", "b" : "1", "guard" : 6000 }, - { "a" : "12", "b" : "3", "guard" : 6000 }, - { "a" : "12", "b" : "7", "guard" : 6000 }, - { "a" : "12", "b" : "9", "guard" : 6000 }, - - { "a" : "13", "b" : "2", "guard" : 6000 }, - { "a" : "13", "b" : "5", "guard" : 6000 }, - { "a" : "13", "b" : "7", "guard" : 6000 }, - { "a" : "13", "b" : "8", "guard" : 6000 }, - { "a" : "13", "b" : "10", "guard" : 6000 }, - - { "a" : "14", "b" : "4", "guard" : 6000 }, - { "a" : "14", "b" : "6", "guard" : 6000 }, - { "a" : "14", "b" : "8", "guard" : 6000 }, - { "a" : "14", "b" : "11", "guard" : 6000 } - ] - } -} +{ + "Newcomers" : + { + "minSize" : "m+u", "maxSize" : "xl+u", + "players" : "2-3", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 500, "max" : 3000, "density" : 10 } + ] + }, + "2" : + { + "type" : "treasure", + "size" : 10, + "owner" : 1, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "treasure", + "size" : 10, + "owner" : 2, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 10, + "owner" : 3, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "9" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "10" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "11" : + { + "type" : "treasure", + "size" : 10, + "owner" : 4, + "monsters" : "weak", + "playerTowns" : { "towns" : 1 }, + "allowedTowns" : [ "conflux" ], + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "12" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 3000, "max" : 6000, "density" : 9 }, + { "min" : 6000, "max" : 15000, "density" : 6 }, + { "min" : 15000, "max" : 20000, "density" : 1 } + ] + }, + "13" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + }, + "14" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 12 + } + }, + "connections" : + [ + { "a" : "12", "b" : "1", "guard" : 6000 }, + { "a" : "12", "b" : "3", "guard" : 6000 }, + { "a" : "12", "b" : "7", "guard" : 6000 }, + { "a" : "12", "b" : "9", "guard" : 6000 }, + + { "a" : "13", "b" : "2", "guard" : 6000 }, + { "a" : "13", "b" : "5", "guard" : 6000 }, + { "a" : "13", "b" : "7", "guard" : 6000 }, + { "a" : "13", "b" : "8", "guard" : 6000 }, + { "a" : "13", "b" : "10", "guard" : 6000 }, + + { "a" : "14", "b" : "4", "guard" : 6000 }, + { "a" : "14", "b" : "6", "guard" : 6000 }, + { "a" : "14", "b" : "8", "guard" : 6000 }, + { "a" : "14", "b" : "11", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON index 8b76bb100..c93d0fb96 100644 --- a/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON +++ b/Mods/vcmi/config/vcmi/rmg/heroes3/southOfHell.JSON @@ -1,165 +1,165 @@ -{ - "South of Hell" : - { - "minSize" : "m+u", "maxSize" : "l+u", - "players" : "2", "humans" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "matchTerrainToTown" : false, - "terrainTypes" : [ "snow" ], - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 10 }, - { "min" : 3000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 30000, "density" : 2 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 20, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "towns" : 1, "castles" : 1 }, - "terrainTypeLikeZone" : 1, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 35, - "owner" : 3, - "monsters" : "strong", - "playerTowns" : { "towns" : 2, "castles" : 3 }, - "allowedTowns" : [ "inferno" ], - "matchTerrainToTown" : false, - "terrainTypes" : [ "lava" ], - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 12 }, - { "min" : 3000, "max" : 9000, "density" : 5 }, - { "min" : 9000, "max" : 30000, "density" : 4 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 40, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 1000, "max" : 4000, "density" : 12 }, - { "min" : 4000, "max" : 9000, "density" : 4 }, - { "min" : 9000, "max" : 40000, "density" : 1 } - ] - }, - "5" : - { - "type" : "treasure", - "size" : 10, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 4 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "6" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 6 }, - { "min" : 3000, "max" : 9000, "density" : 2 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - }, - "7" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 6 - }, - "8" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 2 }, - { "min" : 0, "max" : 0, "density" : 1 }, - { "min" : 0, "max" : 0, "density" : 1 } - ] - }, - "9" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "10" : - { - "type" : "junction", - "size" : 5, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 8 - }, - "11" : - { - "type" : "treasure", - "size" : 15, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "matchTerrainToTown" : false, - "treasure" : - [ - { "min" : 500, "max" : 3000, "density" : 8 }, - { "min" : 3000, "max" : 9000, "density" : 3 }, - { "min" : 9000, "max" : 30000, "density" : 1 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 0 }, - { "a" : "2", "b" : "4", "guard" : 0 }, - { "a" : "4", "b" : "5", "guard" : 0 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "3", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 }, - { "a" : "1", "b" : "8", "guard" : 8000 }, - { "a" : "2", "b" : "9", "guard" : 8000 }, - { "a" : "3", "b" : "10", "guard" : 10000 }, - { "a" : "8", "b" : "11", "guard" : 3000 }, - { "a" : "9", "b" : "11", "guard" : 3000 }, - { "a" : "10", "b" : "11", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "7", "guard" : 6000 } - ] - } -} +{ + "South of Hell" : + { + "minSize" : "m+u", "maxSize" : "l+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "matchTerrainToTown" : false, + "terrainTypes" : [ "snow" ], + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 10 }, + { "min" : 3000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 30000, "density" : 2 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 20, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "towns" : 1, "castles" : 1 }, + "terrainTypeLikeZone" : 1, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 35, + "owner" : 3, + "monsters" : "strong", + "playerTowns" : { "towns" : 2, "castles" : 3 }, + "allowedTowns" : [ "inferno" ], + "matchTerrainToTown" : false, + "terrainTypes" : [ "lava" ], + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2, "gold" : 2 }, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 12 }, + { "min" : 3000, "max" : 9000, "density" : 5 }, + { "min" : 9000, "max" : 30000, "density" : 4 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 40, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 1000, "max" : 4000, "density" : 12 }, + { "min" : 4000, "max" : 9000, "density" : 4 }, + { "min" : 9000, "max" : 40000, "density" : 1 } + ] + }, + "5" : + { + "type" : "treasure", + "size" : 10, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 4 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "6" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 6 }, + { "min" : 3000, "max" : 9000, "density" : 2 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + }, + "7" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 6 + }, + "8" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 2 }, + { "min" : 0, "max" : 0, "density" : 1 }, + { "min" : 0, "max" : 0, "density" : 1 } + ] + }, + "9" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "10" : + { + "type" : "junction", + "size" : 5, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 8 + }, + "11" : + { + "type" : "treasure", + "size" : 15, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "matchTerrainToTown" : false, + "treasure" : + [ + { "min" : 500, "max" : 3000, "density" : 8 }, + { "min" : 3000, "max" : 9000, "density" : 3 }, + { "min" : 9000, "max" : 30000, "density" : 1 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 0 }, + { "a" : "2", "b" : "4", "guard" : 0 }, + { "a" : "4", "b" : "5", "guard" : 0 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "3", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 }, + { "a" : "1", "b" : "8", "guard" : 8000 }, + { "a" : "2", "b" : "9", "guard" : 8000 }, + { "a" : "3", "b" : "10", "guard" : 10000 }, + { "a" : "8", "b" : "11", "guard" : 3000 }, + { "a" : "9", "b" : "11", "guard" : 3000 }, + { "a" : "10", "b" : "11", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "7", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON index a8f6e81d4..9874daab2 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/2sm0k.JSON @@ -1,105 +1,105 @@ -{ - "2SM0k" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "humans" : "6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 3 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "neutralTowns" : { "towns" : 1 }, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "3", "guard" : 6000 }, - { "a" : "1", "b" : "5", "type": "wide" }, - { "a" : "1", "b" : "7", "type": "wide" }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "6", "type": "wide" }, - { "a" : "2", "b" : "8", "type": "wide" }, - { "a" : "5", "b" : "6", "guard" : 6000 }, - { "a" : "7", "b" : "8", "guard" : 6000 } - ] - } -} +{ + "2SM0k" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "humans" : "6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1, "sulfur" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 3 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "neutralTowns" : { "towns" : 1 }, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "3", "guard" : 6000 }, + { "a" : "1", "b" : "5", "type": "wide" }, + { "a" : "1", "b" : "7", "type": "wide" }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "6", "type": "wide" }, + { "a" : "2", "b" : "8", "type": "wide" }, + { "a" : "5", "b" : "6", "guard" : 6000 }, + { "a" : "7", "b" : "8", "guard" : 6000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON index 6b784d53f..7411144d4 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0b.JSON @@ -1,105 +1,105 @@ -{ - "3SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2", "humans" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 3 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 3 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 3 - } - }, - "connections" : - [ - { "a" : "1", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "3", "b" : "6", "guard" : 6000 }, - { "a" : "3", "b" : "8", "guard" : 6000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 3000 }, - { "a" : "7", "b" : "8", "guard" : 3000 }, - { "a" : "5", "b" : "6", "guard" : 3000 } - ] - } -} +{ + "3SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "mines" : { "mercury" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 3 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 3 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 3 + } + }, + "connections" : + [ + { "a" : "1", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "3", "b" : "6", "guard" : 6000 }, + { "a" : "3", "b" : "8", "guard" : 6000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 3000 }, + { "a" : "7", "b" : "8", "guard" : 3000 }, + { "a" : "5", "b" : "6", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON index 3be8223b8..fea84faa8 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/3sb0c.JSON @@ -1,123 +1,123 @@ -{ - "3SB0c" : - { - "minSize" : "s+u", "maxSize" : "m+u", - "players" : "2", "humans" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "4" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "treasureLikeZone" : 3 - }, - "10" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "normal", - "matchTerrainToTown" : false, - "treasureLikeZone" : 1 - } - }, - "connections" : - [ - { "a" : "1", "b" : "8", "guard" : 3000 }, - { "a" : "1", "b" : "10", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "2", "b" : "10", "guard" : 3000 }, - { "a" : "3", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "6", "guard" : 6000 }, - { "a" : "4", "b" : "7", "guard" : 6000 }, - { "a" : "4", "b" : "10", "guard" : 6000 }, - { "a" : "5", "b" : "6", "guard" : 9000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "8", "b" : "6", "guard" : 3000 }, - { "a" : "8", "b" : "10", "guard" : 3000 }, - { "a" : "7", "b" : "9", "guard" : 3000 }, - { "a" : "9", "b" : "10", "guard" : 3000 } - ] - } -} +{ + "3SB0c" : + { + "minSize" : "s+u", "maxSize" : "m+u", + "players" : "2", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "4" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "treasureLikeZone" : 3 + }, + "10" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "normal", + "matchTerrainToTown" : false, + "treasureLikeZone" : 1 + } + }, + "connections" : + [ + { "a" : "1", "b" : "8", "guard" : 3000 }, + { "a" : "1", "b" : "10", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "2", "b" : "10", "guard" : 3000 }, + { "a" : "3", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "6", "guard" : 6000 }, + { "a" : "4", "b" : "7", "guard" : 6000 }, + { "a" : "4", "b" : "10", "guard" : 6000 }, + { "a" : "5", "b" : "6", "guard" : 9000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "8", "b" : "6", "guard" : 3000 }, + { "a" : "8", "b" : "10", "guard" : 3000 }, + { "a" : "7", "b" : "9", "guard" : 3000 }, + { "a" : "9", "b" : "10", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON index ca3b27d8c..da29fda5b 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0a.JSON @@ -1,121 +1,121 @@ -{ - "5SB0a" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "humans" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 4, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "mercury" : 1 }, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "sulfur" : 1 }, - "treasureLikeZone" : 4 - }, - "8" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "crystal" : 1 }, - "treasureLikeZone" : 4 - }, - "9" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "strong", - "matchTerrainToTown" : false, - "mines" : { "gems" : 1 }, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "6", "guard" : 6000 }, - { "a" : "1", "b" : "8", "guard" : 6000 }, - { "a" : "2", "b" : "6", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 3000 }, - { "a" : "2", "b" : "8", "guard" : 3000 }, - { "a" : "2", "b" : "9", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 6000 }, - { "a" : "3", "b" : "9", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 9000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "8", "guard" : 9000 }, - { "a" : "5", "b" : "9", "guard" : 9000 } - ] - } -} +{ + "5SB0a" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "humans" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 4, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "mercury" : 1 }, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "sulfur" : 1 }, + "treasureLikeZone" : 4 + }, + "8" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "crystal" : 1 }, + "treasureLikeZone" : 4 + }, + "9" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "strong", + "matchTerrainToTown" : false, + "mines" : { "gems" : 1 }, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "6", "guard" : 6000 }, + { "a" : "1", "b" : "8", "guard" : 6000 }, + { "a" : "2", "b" : "6", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 3000 }, + { "a" : "2", "b" : "8", "guard" : 3000 }, + { "a" : "2", "b" : "9", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 6000 }, + { "a" : "3", "b" : "9", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 9000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "8", "guard" : 9000 }, + { "a" : "5", "b" : "9", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON index c92c16a7c..6d7d63f5f 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/5sb0b.JSON @@ -1,97 +1,97 @@ -{ - "5SB0b" : - { - "minSize" : "s", "maxSize" : "m+u", - "players" : "2-3", "humans" : "2", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - }, - "5" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 4 - }, - "6" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - }, - "7" : - { - "type" : "treasure", - "size" : 11, - "monsters" : "weak", - "matchTerrainToTown" : false, - "treasureLikeZone" : 4 - } - }, - "connections" : - [ - { "a" : "1", "b" : "4", "guard" : 6000 }, - { "a" : "1", "b" : "5", "guard" : 6000 }, - { "a" : "2", "b" : "4", "guard" : 6000 }, - { "a" : "2", "b" : "5", "guard" : 6000 }, - { "a" : "3", "b" : "4", "guard" : 6000 }, - { "a" : "3", "b" : "5", "guard" : 6000 }, - { "a" : "4", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 3000 } - ] - } -} +{ + "5SB0b" : + { + "minSize" : "s", "maxSize" : "m+u", + "players" : "2-3", "humans" : "2", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + }, + "5" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 4 + }, + "6" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + }, + "7" : + { + "type" : "treasure", + "size" : 11, + "monsters" : "weak", + "matchTerrainToTown" : false, + "treasureLikeZone" : 4 + } + }, + "connections" : + [ + { "a" : "1", "b" : "4", "guard" : 6000 }, + { "a" : "1", "b" : "5", "guard" : 6000 }, + { "a" : "2", "b" : "4", "guard" : 6000 }, + { "a" : "2", "b" : "5", "guard" : 6000 }, + { "a" : "3", "b" : "4", "guard" : 6000 }, + { "a" : "3", "b" : "5", "guard" : 6000 }, + { "a" : "4", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 3000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON index 75428a8d8..10d05291e 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0b.JSON @@ -1,105 +1,105 @@ -{ - "7SB0b" : - { - "minSize" : "s", "maxSize" : "l", - "players" : "2-6", "humans" : "1", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "6", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "4", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 }, - { "a" : "4", "b" : "5", "guard" : 3000 }, - { "a" : "4", "b" : "7", "guard" : 9000 }, - { "a" : "5", "b" : "6", "guard" : 3000 }, - { "a" : "5", "b" : "7", "guard" : 9000 }, - { "a" : "6", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0b" : + { + "minSize" : "s", "maxSize" : "l", + "players" : "2-6", "humans" : "1", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "6", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "4", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 }, + { "a" : "4", "b" : "5", "guard" : 3000 }, + { "a" : "4", "b" : "7", "guard" : 9000 }, + { "a" : "5", "b" : "6", "guard" : 3000 }, + { "a" : "5", "b" : "7", "guard" : 9000 }, + { "a" : "6", "b" : "7", "guard" : 9000 } + ] + } +} diff --git a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON index aefb1e142..266d0ba57 100644 --- a/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON +++ b/Mods/vcmi/config/vcmi/rmg/symmetric/7sb0c.JSON @@ -1,101 +1,101 @@ -{ - "7SB0c" : - { - "minSize" : "s+u", "maxSize" : "l", - "players" : "2-7", "humans" : "2-6", - "zones" : - { - "1" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 1, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, - "treasure" : - [ - { "min" : 10000, "max" : 15000, "density" : 1 }, - { "min" : 3000, "max" : 6000, "density" : 6 }, - { "min" : 500, "max" : 3000, "density" : 9 } - ] - }, - "2" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 2, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, - "treasureLikeZone" : 1 - }, - "3" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 3, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "4" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 4, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "5" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 5, - "monsters" : "normal", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 1, - "treasureLikeZone" : 1 - }, - "6" : - { - "type" : "playerStart", - "size" : 11, - "owner" : 6, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "minesLikeZone" : 2, - "treasureLikeZone" : 1 - }, - "7" : - { - "type" : "cpuStart", - "size" : 11, - "owner" : 7, - "monsters" : "weak", - "playerTowns" : { "castles" : 1 }, - "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, - "treasure" : - [ - { "min" : 20000, "max" : 30000, "density" : 1 }, - { "min" : 15000, "max" : 20000, "density" : 6 }, - { "min" : 10000, "max" : 15000, "density" : 9 } - ] - } - }, - "connections" : - [ - { "a" : "1", "b" : "2", "guard" : 3000 }, - { "a" : "1", "b" : "4", "guard" : 3000 }, - { "a" : "1", "b" : "7", "guard" : 9000 }, - { "a" : "2", "b" : "3", "guard" : 3000 }, - { "a" : "2", "b" : "5", "guard" : 3000 }, - { "a" : "2", "b" : "7", "guard" : 9000 }, - { "a" : "3", "b" : "6", "guard" : 3000 }, - { "a" : "3", "b" : "7", "guard" : 9000 } - ] - } -} +{ + "7SB0c" : + { + "minSize" : "s+u", "maxSize" : "l", + "players" : "2-7", "humans" : "2-6", + "zones" : + { + "1" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 1, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "mercury" : 1, "ore" : 1, "sulfur" : 1 }, + "treasure" : + [ + { "min" : 10000, "max" : 15000, "density" : 1 }, + { "min" : 3000, "max" : 6000, "density" : 6 }, + { "min" : 500, "max" : 3000, "density" : 9 } + ] + }, + "2" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 2, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 1, "ore" : 1, "crystal" : 1, "gems" : 1, "gold" : 1 }, + "treasureLikeZone" : 1 + }, + "3" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 3, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "4" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 4, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "5" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 5, + "monsters" : "normal", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 1, + "treasureLikeZone" : 1 + }, + "6" : + { + "type" : "playerStart", + "size" : 11, + "owner" : 6, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "minesLikeZone" : 2, + "treasureLikeZone" : 1 + }, + "7" : + { + "type" : "cpuStart", + "size" : 11, + "owner" : 7, + "monsters" : "weak", + "playerTowns" : { "castles" : 1 }, + "mines" : { "wood" : 2, "mercury" : 2, "ore" : 2, "sulfur" : 2, "crystal" : 2, "gems" : 2 }, + "treasure" : + [ + { "min" : 20000, "max" : 30000, "density" : 1 }, + { "min" : 15000, "max" : 20000, "density" : 6 }, + { "min" : 10000, "max" : 15000, "density" : 9 } + ] + } + }, + "connections" : + [ + { "a" : "1", "b" : "2", "guard" : 3000 }, + { "a" : "1", "b" : "4", "guard" : 3000 }, + { "a" : "1", "b" : "7", "guard" : 9000 }, + { "a" : "2", "b" : "3", "guard" : 3000 }, + { "a" : "2", "b" : "5", "guard" : 3000 }, + { "a" : "2", "b" : "7", "guard" : 9000 }, + { "a" : "3", "b" : "6", "guard" : 3000 }, + { "a" : "3", "b" : "7", "guard" : 9000 } + ] + } +} From c753a1fdf7bc1e36ac33e5905c97a58c88d1f233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 20:59:13 +0100 Subject: [PATCH 1010/1248] Fix unused variable --- lib/rmg/RmgObject.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 9934602a5..00006584a 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -328,9 +328,12 @@ void rmg::Object::setGuardedIfMonster(const Instance& object) void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) { - if(!map.isOnMap(getPosition(true))) + // FIXME: Crash, but does not trigger under debugger. Race condition? + if (!map.isOnMap(getPosition(true))) + { throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); - + } + //If no specific template was defined for this object, select any matching if (!dObject.appearance) { From 7614a3f49b3e63bdc59eb2041bf0352a853847ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 21:03:12 +0100 Subject: [PATCH 1011/1248] Update docs. Old info is irelevant anyway. --- docs/modders/Random_Map_Template.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modders/Random_Map_Template.md b/docs/modders/Random_Map_Template.md index 5a1df570d..0e31d9da7 100644 --- a/docs/modders/Random_Map_Template.md +++ b/docs/modders/Random_Map_Template.md @@ -18,8 +18,8 @@ /// Number of players that will be present on map (human or AI) "players" : "2-4", - /// Optional, number of AI-only players - "cpu" : "2", + /// Since 1.4.0 - Optional, number of human-only players (as in original templates) + "humans" : "1-4", ///Optional parameter allowing to prohibit some water modes. All modes are allowed if parameter is not specified "allowedWaterContent" : ["none", "normal", "islands"] From 3b7b21bcc86db1c429b7de48406a679d8ff52f42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 21:07:14 +0100 Subject: [PATCH 1012/1248] Fix unused variable --- lib/rmg/CMapGenOptions.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 0951932bd..d75a52cca 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -178,9 +178,7 @@ si8 CMapGenOptions::getPlayerLimit() const void CMapGenOptions::setCompOnlyPlayerCount(si8 value) { - auto playerLimit = getPlayerLimit(); - - assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= playerLimit - getHumanOrCpuPlayerCount()))); + assert(value == RANDOM_SIZE || (getHumanOrCpuPlayerCount() == RANDOM_SIZE || (value >= 0 && value <= getPlayerLimit() - getHumanOrCpuPlayerCount()))); compOnlyPlayerCount = value; resetPlayersMap(); From 80198f21d88eb1b2c067671977bb59a4c69a895c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 21:07:50 +0100 Subject: [PATCH 1013/1248] Revert "Fix unused variable" This reverts commit c753a1fdf7bc1e36ac33e5905c97a58c88d1f233. --- lib/rmg/RmgObject.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 00006584a..9934602a5 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -328,12 +328,9 @@ void rmg::Object::setGuardedIfMonster(const Instance& object) void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) { - // FIXME: Crash, but does not trigger under debugger. Race condition? - if (!map.isOnMap(getPosition(true))) - { + if(!map.isOnMap(getPosition(true))) throw rmgException(boost::str(boost::format("Position of object %d at %s is outside the map") % dObject.id % getPosition(true).toString())); - } - + //If no specific template was defined for this object, select any matching if (!dObject.appearance) { From 6e7b68d0e644dd63b39dd0b1b59f889a576242c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Mon, 30 Oct 2023 21:33:49 +0100 Subject: [PATCH 1014/1248] Unused variable, unused code --- client/lobby/RandomMapTab.cpp | 2 +- lib/rmg/CMapGenOptions.cpp | 25 ------------------------- 2 files changed, 1 insertion(+), 26 deletions(-) diff --git a/client/lobby/RandomMapTab.cpp b/client/lobby/RandomMapTab.cpp index 954b98043..1deff1ece 100644 --- a/client/lobby/RandomMapTab.cpp +++ b/client/lobby/RandomMapTab.cpp @@ -264,7 +264,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr opts) } else // Random { - vstd::erase_if(compCountAllowed, [playerLimit, humanOrCpuPlayerCount](int el) + vstd::erase_if(compCountAllowed, [playerLimit](int el) { return (playerLimit - 1) < el; // Must leave at least 1 human player }); diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index d75a52cca..c963e5aad 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -347,31 +347,6 @@ void CMapGenOptions::resetPlayersMap() } } } - /* - for(int color = 0; color < totalPlayersLimit; ++color) - { - CPlayerSettings player; - auto pc = PlayerColor(color); - player.setColor(pc); - auto playerType = EPlayerType::AI; - if (getPlayerCount() != RANDOM_SIZE && color < realPlayersCnt) - { - playerType = EPlayerType::HUMAN; - } - else if((getPlayerCount() != RANDOM_SIZE && color >= realPlayersCnt) - || (compOnlyPlayerCount != RANDOM_SIZE && color >= (PlayerColor::PLAYER_LIMIT_I-compOnlyPlayerCount))) - { - //FIXME: Allow humans to choose any color, even from the end of the list - playerType = EPlayerType::COMP_ONLY; - } - player.setPlayerType(playerType); - player.setTeam(rememberTeam[pc]); - players[pc] = player; - - if (vstd::contains(rememberTownTypes, pc)) - players[pc].setStartingTown(rememberTownTypes[pc]); - } - */ } void CMapGenOptions::savePlayersMap() From 858ba22a27270d9cac5304aed1395bcf00e01513 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 31 Oct 2023 12:11:41 +0200 Subject: [PATCH 1015/1248] Ignore corrupted .def files --- client/render/CAnimation.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/render/CAnimation.cpp b/client/render/CAnimation.cpp index 8077c3438..662f41ccc 100644 --- a/client/render/CAnimation.cpp +++ b/client/render/CAnimation.cpp @@ -204,11 +204,19 @@ void CAnimation::printError(size_t frame, size_t group, std::string type) const CAnimation::CAnimation(const AnimationPath & Name): name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")), - preloaded(false), - defFile() + preloaded(false) { if(CResourceHandler::get()->existsResource(name)) - defFile = std::make_shared(name); + { + try + { + defFile = std::make_shared(name); + } + catch ( const std::runtime_error & e) + { + logAnim->error("Def file %s failed to load! Reason: %s", Name.getOriginalName(), e.what()); + } + } init(); From 5623d3fb117242f1bc0cb3ca50f75d65a59fed11 Mon Sep 17 00:00:00 2001 From: Andrey Filipenkov Date: Tue, 31 Oct 2023 17:58:33 +0300 Subject: [PATCH 1016/1248] describe how to put ipa on iOS device --- docs/players/Installation_iOS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/players/Installation_iOS.md b/docs/players/Installation_iOS.md index 46d88c99c..a57eef464 100644 --- a/docs/players/Installation_iOS.md +++ b/docs/players/Installation_iOS.md @@ -16,6 +16,10 @@ have the following options: - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool. - [Sign from any OS](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing). You'd still need to find a way to create signing assets (private key and provisioning profile) though. +To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop: + + /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa + ## Step 2: Installing Heroes III data files Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from data folder - it will save your time and space. The same applies to the Mp3 directory. From 29f023eaa51fb758a13d7cb70edd4307bffdce35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 31 Oct 2023 16:04:21 +0100 Subject: [PATCH 1017/1248] More tolerance for Subterranean Gates placement --- lib/rmg/modificators/ConnectionsPlacer.cpp | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/lib/rmg/modificators/ConnectionsPlacer.cpp b/lib/rmg/modificators/ConnectionsPlacer.cpp index 44b1e11d2..10ab3a184 100644 --- a/lib/rmg/modificators/ConnectionsPlacer.cpp +++ b/lib/rmg/modificators/ConnectionsPlacer.cpp @@ -334,16 +334,12 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c return -1.f; //This could fail is accessibleArea is below the map - rmg::Area toPlace(rmgGate1.getArea() + rmgGate1.getAccessibleArea()); - auto inTheMap = toPlace.getTilesVector(); - toPlace.clear(); - for (const int3& tile : inTheMap) + rmg::Area toPlace(rmgGate1.getArea()); + toPlace.unite(toPlace.getBorderOutside()); // Add a bit of extra space around + toPlace.erase_if([this](const int3 & tile) { - if (map.isOnMap(tile)) - { - toPlace.add(tile); - } - } + return !map.isOnMap(tile); + }); toPlace.translate(-zShift); path2 = managerOther.placeAndConnectObject(toPlace, rmgGate2, minDist, guarded2, true, ObjectManager::OptimizeType::NONE); From 25b64dd08b9d8a6e5064f3ea4f579f25b9e302dd Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Tue, 31 Oct 2023 21:43:06 +0200 Subject: [PATCH 1018/1248] Update lib/gameState/CGameState.cpp Co-authored-by: Ivan Savenko --- lib/gameState/CGameState.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 2a8ed2955..2b54a9063 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -409,6 +409,7 @@ CGameState::CGameState() CGameState::~CGameState() { + // explicitly delete all ongoing battles first - BattleInfo destructor requires valid CGameState currentBattles.clear(); map.dellNull(); } From 8a9f10ec3a97ac1b8822345c871c68ec57d78e46 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 31 Oct 2023 22:05:22 +0100 Subject: [PATCH 1019/1248] fix asset failures --- client/mainmenu/CHighScoreScreen.cpp | 3 +-- client/windows/CWindowObject.cpp | 6 ++++-- client/windows/CWindowObject.h | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/client/mainmenu/CHighScoreScreen.cpp b/client/mainmenu/CHighScoreScreen.cpp index c3762859f..eca87c31e 100644 --- a/client/mainmenu/CHighScoreScreen.cpp +++ b/client/mainmenu/CHighScoreScreen.cpp @@ -325,7 +325,6 @@ void CHighScoreInputScreen::deactivate() { CCS->videoh->close(); CCS->soundh->stopSound(videoSoundHandle); - CIntObject::deactivate(); } void CHighScoreInputScreen::clickPressed(const Point & cursorPosition) @@ -361,7 +360,7 @@ void CHighScoreInputScreen::keyPressed(EShortcut key) } CHighScoreInput::CHighScoreInput(std::string playerName, std::function readyCB) - : CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB) + : CWindowObject(NEEDS_ANIMATED_BACKGROUND, ImagePath::builtin("HIGHNAME")), ready(readyCB) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; diff --git a/client/windows/CWindowObject.cpp b/client/windows/CWindowObject.cpp index c21c1a219..231213219 100644 --- a/client/windows/CWindowObject.cpp +++ b/client/windows/CWindowObject.cpp @@ -39,7 +39,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point ce options(options_), background(createBg(imageName, options & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) + assert(parent == nullptr); //Safe to remove, but windows should not have parent defActions = 255-DISPOSE; @@ -60,7 +61,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName): options(options_), background(createBg(imageName, options_ & PLAYER_COLORED)) { - assert(parent == nullptr); //Safe to remove, but windows should not have parent + if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet) + assert(parent == nullptr); //Safe to remove, but windows should not have parent defActions = 255-DISPOSE; diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index dd49f107b..4746f52c8 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -38,7 +38,8 @@ public: PLAYER_COLORED=1, //background will be player-colored RCLICK_POPUP=2, // window will behave as right-click popup BORDERED=4, // window will have border if current resolution is bigger than size of window - SHADOW_DISABLED=8 //this window won't display any shadow + SHADOW_DISABLED=8, //this window won't display any shadow + NEEDS_ANIMATED_BACKGROUND=16 //there are videos in the background that have to be played }; /* From 03e116978128a0fc95933561999f657fed5d9bc5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 24 Oct 2023 17:11:25 +0300 Subject: [PATCH 1020/1248] Reduced number of accesses to CGObjectInstance::subID --- lib/campaign/CampaignState.cpp | 9 +++--- lib/constants/EntityIdentifiers.h | 30 ++++++++++++++++++-- lib/gameState/CGameState.cpp | 6 ++-- lib/gameState/CGameStateCampaign.cpp | 12 ++++---- lib/gameState/TavernHeroesPool.cpp | 6 ++-- lib/mapObjects/CBank.cpp | 2 +- lib/mapObjects/CGCreature.cpp | 29 ++++++++++++-------- lib/mapObjects/CGCreature.h | 1 + lib/mapObjects/CGDwelling.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 13 ++++++--- lib/mapObjects/CGHeroInstance.h | 1 + lib/mapObjects/CGMarket.cpp | 2 +- lib/mapObjects/CGObjectInstance.cpp | 11 ++++++-- lib/mapObjects/CGObjectInstance.h | 12 +++++--- lib/mapObjects/CGTownBuilding.cpp | 4 +-- lib/mapObjects/CGTownBuilding.h | 4 +-- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/CObjectHandler.cpp | 10 ------- lib/mapObjects/CObjectHandler.h | 11 -------- lib/mapObjects/CRewardableObject.cpp | 2 +- lib/mapObjects/IObjectInterface.h | 5 ++-- lib/mapObjects/MiscObjects.cpp | 41 ++++++++++++++++++---------- lib/mapObjects/MiscObjects.h | 3 ++ lib/mapping/CMap.cpp | 4 +-- lib/mapping/CMap.h | 2 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/rmg/RmgObject.cpp | 6 ++-- 27 files changed, 135 insertions(+), 97 deletions(-) diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index 082ddf7ee..eb6144f3e 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -274,17 +274,16 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe for (auto * hero : heroes) { - HeroTypeID heroType(hero->subID); JsonNode node = CampaignState::crossoverSerialize(hero); - if (reservedHeroes.count(heroType)) + if (reservedHeroes.count(hero->getHeroType())) { - logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->subID, hero->getNameTranslated()); - globalHeroPool[heroType] = node; + logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroType(), hero->getNameTranslated()); + globalHeroPool[hero->getHeroType()] = node; } else { - logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroType(), hero->getNameTranslated()); scenarioHeroPool[*currentMap].push_back(node); } } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 6a5e7b5d8..c9b10fd83 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -407,7 +407,7 @@ public: static std::string entityType(); }; -class ObjBase : public IdentifierBase +class MapObjectBaseID : public IdentifierBase { public: enum Type @@ -552,15 +552,38 @@ public: }; }; -class DLL_LINKAGE Obj : public IdentifierWithEnum +class DLL_LINKAGE MapObjectID : public IdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using IdentifierWithEnum::IdentifierWithEnum; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); }; +class MapObjectSubID : public Identifier +{ +public: + constexpr MapObjectSubID(const IdentifierBase & value): + Identifier(value.getNum()) + {} + constexpr MapObjectSubID(int32_t value): + Identifier(value) + {} + + MapObjectSubID & operator =(int32_t value) + { + this->num = value; + return *this; + } + + MapObjectSubID & operator =(const IdentifierBase & value) + { + this->num = value.getNum(); + return *this; + } +}; + class DLL_LINKAGE RoadId : public Identifier { public: @@ -966,6 +989,7 @@ public: // Deprecated // TODO: remove +using Obj = MapObjectID; using ETownType = FactionID; using EGameResID = GameResID; using River = RiverId; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 80a80be05..69a2fbed6 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -77,7 +77,7 @@ public: } }; -static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & pos, const PlayerColor & owner) +static CGObjectInstance * createObject(MapObjectID id, MapObjectSubID subid, const int3 & pos, const PlayerColor & owner) { CGObjectInstance * nobj; switch(id) @@ -810,7 +810,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } } - CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId.getNum(), townPos, playerColor); + CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId, townPos, playerColor); hero->pos += hero->getVisitableOffset(); map->getEditManager()->insertObject(hero); } @@ -880,8 +880,6 @@ void CGameState::initHeroes() CGBoat * boat = dynamic_cast(handler->create()); handler->configureObject(boat, gs->getRandomGenerator()); - boat->ID = Obj::BOAT; - boat->subID = hero->getBoatType().getNum(); boat->pos = hero->pos; boat->appearance = handler->getTemplates().front(); boat->id = ObjectInstanceID(static_cast(gs->map->objects.size())); diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index c97c159a6..597bdd7eb 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -213,7 +213,7 @@ void CGameStateCampaign::placeCampaignHeroes() std::set heroesToRemove = campaignState->getReservedHeroes(); for(auto & campaignHeroReplacement : campaignHeroReplacements) - heroesToRemove.insert(HeroTypeID(campaignHeroReplacement.hero->subID)); + heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType()); for(auto & heroID : heroesToRemove) { @@ -339,7 +339,7 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vectortempOwner.isValidPlayer()) heroToPlace->tempOwner = heroPlaceholder->tempOwner; heroToPlace->pos = heroPlaceholder->pos; - heroToPlace->type = VLC->heroh->objects[heroToPlace->subID]; + heroToPlace->type = VLC->heroh->objects[heroToPlace->getHeroType().getNum()]; heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front(); gameState->map->removeBlockVisTiles(heroPlaceholder, true); @@ -396,7 +396,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT CGHeroInstance * hero = CampaignState::crossoverDeserialize(node, gameState->map); - logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } @@ -422,7 +422,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT CGHeroInstance * hero = CampaignState::crossoverDeserialize(*nodeListIter, gameState->map); nodeListIter++; - logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->subID, hero->getNameTranslated()); + logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->getHeroType(), hero->getNameTranslated()); campaignHeroReplacements.emplace_back(hero, placeholder->id); } @@ -468,7 +468,7 @@ void CGameStateCampaign::initHeroes() { for (auto & heroe : heroes) { - if (heroe->subID == chosenBonus->info1) + if (heroe->getHeroType().getNum() == chosenBonus->info1) { giveCampaignBonusToHero(heroe); break; @@ -557,7 +557,7 @@ void CGameStateCampaign::initTowns() if(gameState->scenarioOps->campState->formatVCMI()) newBuilding = BuildingID(chosenBonus->info1); else - newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->subID, town->builtBuildings); + newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFaction(), town->builtBuildings); // Build granted building & all prerequisites - e.g. Mages Guild Lvl 3 should also give Mages Guild Lvl 1 & 2 while(true) diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 4dabd8ab3..00aec3a73 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -25,7 +25,7 @@ std::map TavernHeroesPool::unusedHeroesFromPool() c { std::map pool = heroesPool; for(const auto & slot : currentTavern) - pool.erase(HeroTypeID(slot.hero->subID)); + pool.erase(slot.hero->getHeroType()); return pool; } @@ -34,7 +34,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const { for (auto const & slot : currentTavern) { - if (HeroTypeID(slot.hero->subID) == hero) + if (slot.hero->getHeroType() == hero) return slot.role; } return TavernSlotRole::NONE; @@ -132,7 +132,7 @@ void TavernHeroesPool::onNewDay() void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero) { - heroesPool[HeroTypeID(hero->subID)] = hero; + heroesPool[hero->getHeroType()] = hero; } void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set mask) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 53f219939..849d171c6 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -46,7 +46,7 @@ void CBank::initObj(CRandomGenerator & rand) { daycounter = 0; resetDuration = 0; - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } bool CBank::isCoastVisitable() const diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index b0c9c8384..dff3ed95d 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -28,7 +28,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const if(stacks.empty()) { //should not happen... - logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), subID, id.getNum()); + logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), getCreature(), id.getNum()); return "INVALID_STACK"; } @@ -41,7 +41,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const else ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); + ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); hoverName = ms.toString(); return hoverName; } @@ -54,7 +54,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID); + ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); ms.appendRawString("\n\n"); @@ -132,7 +132,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature()); cb->showBlockingDialog(&ynd); break; } @@ -146,7 +146,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const std::string tmp = VLC->generaltexth->advobtxt[90]; boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0)))); boost::algorithm::replace_first(tmp, "%d", std::to_string(action)); - boost::algorithm::replace_first(tmp,"%s",VLC->creh->objects[subID]->getNamePluralTranslated()); + boost::algorithm::replace_first(tmp,"%s",VLC->creatures()->getById(getCreature())->getNamePluralTranslated()); ynd.text.appendRawString(tmp); cb->showBlockingDialog(&ynd); break; @@ -155,6 +155,11 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const } +CreatureID CGCreature::getCreature() const +{ + return CreatureID(getObjTypeIndex().getNum()); +} + void CGCreature::initObj(CRandomGenerator & rand) { blockVisit = true; @@ -177,16 +182,16 @@ void CGCreature::initObj(CRandomGenerator & rand) break; } - stacks[SlotID(0)]->setType(CreatureID(subID)); + stacks[SlotID(0)]->setType(getCreature()); TQuantity &amount = stacks[SlotID(0)]->count; - CCreature &c = *VLC->creh->objects[subID]; + const Creature * c = VLC->creatures()->getById(getCreature()); if(amount == 0) { - amount = rand.nextInt(c.ammMin, c.ammMax); + amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax()); if(amount == 0) //armies with 0 creatures are illegal { - logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName()); + logGlobal->warn("Stack cannot have 0 creatures. Check properties of %s", c->getJsonKey()); amount = 1; } } @@ -252,7 +257,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const powerFactor = -3; std::set myKindCres; //what creatures are the same kind as we - const CCreature * myCreature = VLC->creh->objects[subID]; + const CCreature * myCreature = VLC->creh->objects[getCreature().getNum()]; myKindCres.insert(myCreature->getId()); //we myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades @@ -290,7 +295,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const return JOIN_FOR_FREE; else if(diplomacy * 2 + sympathy + 1 >= character) - return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold + return VLC->creatures()->getById(getCreature())->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold } //we are still here - creatures have not joined hero, flee or fight @@ -404,7 +409,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID); + ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature()); cb->showBlockingDialog(&ynd); } diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 385b96cb5..438e89c92 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -44,6 +44,7 @@ public: void newTurn(CRandomGenerator & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + CreatureID getCreature() const; //stack formation depends on position, bool containsUpgradedStack() const; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 65da8db66..70fc4d983 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -95,7 +95,7 @@ void CGDwelling::initObj(CRandomGenerator & rand) case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); if (getOwner() != PlayerColor::NEUTRAL) cb->gameState()->players[getOwner()].dwellings.emplace_back(this); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 1d49f631f..c122af7ab 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -285,6 +285,11 @@ PlayerColor CGHeroInstance::getOwner() const return tempOwner; } +HeroTypeID CGHeroInstance::getHeroType() const +{ + return HeroTypeID(getObjTypeIndex().getNum()); +} + void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) { subID = SUBID.getNum(); @@ -305,7 +310,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { assert(validTypes(true)); if(!type) - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType().getNum()]; if (ID == Obj::HERO) appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); @@ -1050,7 +1055,7 @@ HeroTypeID CGHeroInstance::getPortraitSource() const if (customPortraitSource.isValid()) return customPortraitSource; else - return HeroTypeID(subID); + return getHeroType(); } int32_t CGHeroInstance::getIconIndex() const @@ -1502,7 +1507,7 @@ std::string CGHeroInstance::getHeroTypeName() const } else { - return VLC->heroh->objects[subID]->getJsonKey(); + return VLC->heroh->objects[getHeroType()]->getJsonKey(); } } return ""; @@ -1736,7 +1741,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!appearance) { // crossoverDeserialize - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType()]; appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); } diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 2035a488b..a742f2d7b 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -231,6 +231,7 @@ public: ////////////////////////////////////////////////////////////////////////// + HeroTypeID getHeroType() const; void setType(si32 ID, si32 subID) override; void initHero(CRandomGenerator & rand); diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index ef20b1024..d6a1e433d 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -25,7 +25,7 @@ VCMI_LIB_NAMESPACE_BEGIN void CGMarket::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } void CGMarket::onHeroVisit(const CGHeroInstance * h) const diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 7d5cacbe2..1f153a75f 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -40,12 +40,12 @@ CGObjectInstance::CGObjectInstance(): //must be instantiated in .cpp file for access to complete types of all member fields CGObjectInstance::~CGObjectInstance() = default; -int32_t CGObjectInstance::getObjGroupIndex() const +MapObjectID CGObjectInstance::getObjGroupIndex() const { - return ID.num; + return ID; } -int32_t CGObjectInstance::getObjTypeIndex() const +MapObjectSubID CGObjectInstance::getObjTypeIndex() const { return subID; } @@ -197,6 +197,11 @@ void CGObjectInstance::setProperty( ui8 what, ui32 val ) } } +TObjectTypeHandler CGObjectInstance::getObjectHandler() const +{ + return VLC->objtypeh->getHandlerFor(ID, subID); +} + void CGObjectInstance::setPropertyDer( ui8 what, ui32 val ) {} diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 1e57c2344..4d1fbc9fd 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -21,6 +21,8 @@ struct Component; class JsonSerializeFormat; class ObjectTemplate; class CMap; +class AObjectTypeHandler; +using TObjectTypeHandler = std::shared_ptr; class DLL_LINKAGE CGObjectInstance : public IObjectInterface { @@ -28,9 +30,9 @@ public: /// Position of bottom-right corner of object on map int3 pos; /// Type of object, e.g. town, hero, creature. - Obj ID; + MapObjectID ID; /// Subtype of object, depends on type - si32 subID; + MapObjectSubID subID; /// Current owner of an object (when below PLAYER_LIMIT) PlayerColor tempOwner; /// Index of object in map's list of objects @@ -45,8 +47,8 @@ public: CGObjectInstance(); //TODO: remove constructor ~CGObjectInstance() override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; /// "center" tile from which the sight distance is calculated int3 getSightCenter() const; @@ -94,6 +96,8 @@ public: std::optional getVisitSound() const; std::optional getRemovalSound() const; + TObjectTypeHandler getObjectHandler() const; + /** VIRTUAL METHODS **/ /// Returns true if player can pass through visitable tiles of this object diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 4921b085a..b449dc0e0 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -24,12 +24,12 @@ PlayerColor CGTownBuilding::getOwner() const return town->getOwner(); } -int32_t CGTownBuilding::getObjGroupIndex() const +MapObjectID CGTownBuilding::getObjGroupIndex() const { return -1; } -int32_t CGTownBuilding::getObjTypeIndex() const +MapObjectSubID CGTownBuilding::getObjTypeIndex() const { return 0; } diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h index 1fffb3710..77085f67f 100644 --- a/lib/mapObjects/CGTownBuilding.h +++ b/lib/mapObjects/CGTownBuilding.h @@ -45,8 +45,8 @@ public: } PlayerColor getOwner() const override; - int32_t getObjGroupIndex() const override; - int32_t getObjTypeIndex() const override; + MapObjectID getObjGroupIndex() const override; + MapObjectSubID getObjTypeIndex() const override; int3 visitablePos() const override; int3 getPosition() const override; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9e7f842b2..a4841a3cd 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -763,7 +763,7 @@ void CGTownInstance::updateAppearance() { auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); //FIXME: not the best way to do this - auto app = VLC->objtypeh->getHandlerFor(ID, subID)->getOverride(terrain, this); + auto app = getObjectHandler()->getOverride(terrain, this); if (app) appearance = app; } diff --git a/lib/mapObjects/CObjectHandler.cpp b/lib/mapObjects/CObjectHandler.cpp index 34162c4c3..bf9c2158d 100644 --- a/lib/mapObjects/CObjectHandler.cpp +++ b/lib/mapObjects/CObjectHandler.cpp @@ -28,14 +28,4 @@ CObjectHandler::CObjectHandler() logGlobal->trace("\t\tDone loading resource prices!"); } -CGObjectInstanceBySubIdFinder::CGObjectInstanceBySubIdFinder(CGObjectInstance * obj) : obj(obj) -{ - -} - -bool CGObjectInstanceBySubIdFinder::operator()(CGObjectInstance * obj) const -{ - return this->obj->subID == obj->subID; -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CObjectHandler.h b/lib/mapObjects/CObjectHandler.h index 128d579b4..9cba71ab1 100644 --- a/lib/mapObjects/CObjectHandler.h +++ b/lib/mapObjects/CObjectHandler.h @@ -16,17 +16,6 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; class int3; -/// function object which can be used to find an object with an specific sub ID -class CGObjectInstanceBySubIdFinder -{ -public: - CGObjectInstanceBySubIdFinder(CGObjectInstance * obj); - bool operator()(CGObjectInstance * obj) const; - -private: - CGObjectInstance * obj; -}; - class DLL_LINKAGE CObjectHandler { public: diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 4ec6783e0..1d441b9f3 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -373,7 +373,7 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const void CRewardableObject::initObj(CRandomGenerator & rand) { - VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand); + getObjectHandler()->configureObject(this, rand); } CRewardableObject::CRewardableObject() diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 1ed826903..6a6ca40b7 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -10,6 +10,7 @@ #pragma once #include "../networkPacks/EInfoWindowMode.h" +#include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -33,8 +34,8 @@ public: virtual ~IObjectInterface() = default; - virtual int32_t getObjGroupIndex() const = 0; - virtual int32_t getObjTypeIndex() const = 0; + virtual MapObjectID getObjGroupIndex() const = 0; + virtual MapObjectSubID getObjTypeIndex() const = 0; virtual PlayerColor getOwner() const = 0; virtual int3 visitablePos() const = 0; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 0bfb191a7..5e688e4d9 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -85,7 +85,7 @@ void CGMine::onHeroVisit( const CGHeroInstance * h ) const { BlockingDialog ynd(true,false); ynd.player = h->tempOwner; - ynd.text.appendLocalString(EMetaText::ADVOB_TXT, subID == 7 ? 84 : 187); + ynd.text.appendLocalString(EMetaText::ADVOB_TXT, isAbandoned() ? 84 : 187); cb->showBlockingDialog(&ynd); return; } @@ -119,19 +119,19 @@ void CGMine::initObj(CRandomGenerator & rand) } else { - producedResource = GameResID(subID); + producedResource = GameResID(getObjTypeIndex()); } producedQuantity = defaultResProduction(); } bool CGMine::isAbandoned() const { - return (subID >= 7); + return (getObjTypeIndex() >= 7); } std::string CGMine::getObjectName() const { - return VLC->generaltexth->translate("core.minename", subID); + return VLC->generaltexth->translate("core.minename", getObjTypeIndex()); } std::string CGMine::getHoverText(PlayerColor player) const @@ -206,7 +206,7 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) if(handler.saving) { JsonNode node(JsonNode::JsonType::DATA_VECTOR); - for(auto const & resID : abandonedMineResources) + for(const auto & resID : abandonedMineResources) { JsonNode one(JsonNode::JsonType::DATA_STRING); one.String() = GameConstants::RESOURCE_NAMES[resID]; @@ -237,9 +237,14 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) } } +GameResID CGResource::resourceID() const +{ + return getObjTypeIndex().getNum(); +} + std::string CGResource::getHoverText(PlayerColor player) const { - return VLC->generaltexth->restypes[subID]; + return VLC->generaltexth->restypes[resourceID()]; } void CGResource::initObj(CRandomGenerator & rand) @@ -248,7 +253,7 @@ void CGResource::initObj(CRandomGenerator & rand) if(amount == CGResource::RANDOM_AMOUNT) { - switch(static_cast(subID)) + switch(resourceID()) { case EGameResID::GOLD: amount = rand.nextInt(5, 10) * 100; @@ -285,7 +290,7 @@ void CGResource::onHeroVisit( const CGHeroInstance * h ) const void CGResource::collectRes(const PlayerColor & player) const { - cb->giveResource(player, static_cast(subID), amount); + cb->giveResource(player, resourceID(), amount); InfoWindow sii; sii.player = player; if(!message.empty()) @@ -297,9 +302,9 @@ void CGResource::collectRes(const PlayerColor & player) const { sii.type = EInfoWindowMode::INFO; sii.text.appendLocalString(EMetaText::ADVOB_TXT,113); - sii.text.replaceLocalString(EMetaText::RES_NAMES, subID); + sii.text.replaceLocalString(EMetaText::RES_NAMES, resourceID()); } - sii.components.emplace_back(Component::EComponentType::RESOURCE,subID,amount,0); + sii.components.emplace_back(Component::EComponentType::RESOURCE, resourceID(), amount, 0); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); cb->showInfoDialog(&sii); cb->removeObject(this, player); @@ -688,6 +693,14 @@ bool CGWhirlpool::isProtected(const CGHeroInstance * h) || (h->stacksCount() == 1 && h->Slots().begin()->second->count == 1); } +ArtifactID CGArtifact::getArtifact() const +{ + if(ID == Obj::SPELL_SCROLL) + return ArtifactID::SPELL_SCROLL; + else + return getObjTypeIndex().getNum(); +} + void CGArtifact::initObj(CRandomGenerator & rand) { blockVisit = true; @@ -700,7 +713,7 @@ void CGArtifact::initObj(CRandomGenerator & rand) storedArtifact = a; } if(!storedArtifact->artType) - storedArtifact->setType(VLC->arth->objects[subID]); + storedArtifact->setType(VLC->arth->objects[getArtifact()]); } if(ID == Obj::SPELL_SCROLL) subID = 1; @@ -713,7 +726,7 @@ void CGArtifact::initObj(CRandomGenerator & rand) std::string CGArtifact::getObjectName() const { - return VLC->artifacts()->getByIndex(subID)->getNameTranslated(); + return VLC->artifacts()->getByIndex(getArtifact())->getNameTranslated(); } void CGArtifact::onHeroVisit(const CGHeroInstance * h) const @@ -730,11 +743,11 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const { case Obj::ARTIFACT: { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, subID, 0, 0); + iw.components.emplace_back(Component::EComponentType::ARTIFACT, getArtifact(), 0, 0); if(!message.empty()) iw.text = message; else - iw.text.appendLocalString(EMetaText::ART_EVNTS, subID); + iw.text.appendLocalString(EMetaText::ART_EVNTS, getArtifact()); } break; case Obj::SPELL_SCROLL: diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 53021a4a9..c40256dc1 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -93,6 +93,8 @@ public: void afterAddToMap(CMap * map) override; BattleField getBattlefield() const override; + ArtifactID getArtifact() const; + template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -118,6 +120,7 @@ public: std::string getHoverText(PlayerColor player) const override; void collectRes(const PlayerColor & player) const; + GameResID resourceID() const; template void serialize(Handler &h, const int version) { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 806c953c1..46156dac6 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -250,10 +250,10 @@ void CMap::calculateGuardingGreaturePositions() } } -CGHeroInstance * CMap::getHero(int heroID) +CGHeroInstance * CMap::getHero(HeroTypeID heroID) { for(auto & elem : heroesOnMap) - if(elem->subID == heroID) + if(elem->getObjTypeIndex() == heroID.getNum()) return elem; return nullptr; } diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index de9d5e5a2..518b1fb6a 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -118,7 +118,7 @@ public: /// Gets object of specified type on requested position const CGObjectInstance * getObjectiveObjectFrom(const int3 & pos, Obj type); - CGHeroInstance * getHero(int heroId); + CGHeroInstance * getHero(HeroTypeID heroId); /// Sets the victory/loss condition objectives ?? void checkForObjectives(); diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 0208204a1..06abea28c 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1229,7 +1229,7 @@ void CMapLoaderJson::MapObjectLoader::configure() else if(art->ID == Obj::ARTIFACT) { //specific artifact - artID = ArtifactID(art->subID); + artID = art->getArtifact(); } art->storedArtifact = ArtifactUtils::createArtifact(owner->map, artID, spellID.getNum()); diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 9934602a5..a2f26c03d 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -113,7 +113,7 @@ void Object::Instance::setPositionRaw(const int3 & position) void Object::Instance::setAnyTemplate(CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(); + auto templates = dObject.getObjectHandler()->getTemplates(); if(templates.empty()) throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); @@ -124,7 +124,7 @@ void Object::Instance::setAnyTemplate(CRandomGenerator & rng) void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) { - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrain); + auto templates = dObject.getObjectHandler()->getTemplates(terrain); if (templates.empty()) { auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); @@ -335,7 +335,7 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) if (!dObject.appearance) { const auto * terrainType = map.getTile(getPosition(true)).terType; - auto templates = VLC->objtypeh->getHandlerFor(dObject.ID, dObject.subID)->getTemplates(terrainType->getId()); + auto templates = dObject.getObjectHandler()->getTemplates(terrainType->getId()); if (templates.empty()) { throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); From dcb8f4fc7b0798729cec3d5e81b954393250c6ab Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 25 Oct 2023 13:50:11 +0300 Subject: [PATCH 1021/1248] Moved object type randomization to object class --- lib/constants/EntityIdentifiers.h | 2 +- lib/gameState/CGameState.cpp | 245 +++------------------------- lib/gameState/CGameState.h | 2 - lib/mapObjects/CGCreature.cpp | 32 ++++ lib/mapObjects/CGCreature.h | 1 + lib/mapObjects/CGDwelling.cpp | 176 +++++++++++++------- lib/mapObjects/CGDwelling.h | 49 ++---- lib/mapObjects/CGHeroInstance.cpp | 11 ++ lib/mapObjects/CGHeroInstance.h | 1 + lib/mapObjects/CGObjectInstance.cpp | 5 + lib/mapObjects/CGObjectInstance.h | 1 + lib/mapObjects/CGTownInstance.cpp | 29 ++++ lib/mapObjects/CGTownInstance.h | 2 + lib/mapObjects/IObjectInterface.h | 1 + lib/mapObjects/MiscObjects.cpp | 36 ++++ lib/mapObjects/MiscObjects.h | 2 + lib/mapping/MapFormatH3M.cpp | 59 ++----- 17 files changed, 288 insertions(+), 366 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index c9b10fd83..1ff20c597 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -567,7 +567,7 @@ public: constexpr MapObjectSubID(const IdentifierBase & value): Identifier(value.getNum()) {} - constexpr MapObjectSubID(int32_t value): + constexpr MapObjectSubID(int32_t value = -1): Identifier(value) {} diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 69a2fbed6..bd88d0b03 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -153,223 +153,6 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return HeroTypeID::NONE; // no available heroes at all } -std::pair CGameState::pickObject (CGObjectInstance *obj) -{ - switch(obj->ID) - { - case Obj::RANDOM_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC)); - case Obj::RANDOM_TREASURE_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE)); - case Obj::RANDOM_MINOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MINOR)); - case Obj::RANDOM_MAJOR_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MAJOR)); - case Obj::RANDOM_RELIC_ART: - return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_RELIC)); - case Obj::RANDOM_HERO: - return std::make_pair(Obj::HERO, pickNextHeroType(obj->tempOwner)); - case Obj::RANDOM_MONSTER: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator())); - case Obj::RANDOM_MONSTER_L1: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 1)); - case Obj::RANDOM_MONSTER_L2: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 2)); - case Obj::RANDOM_MONSTER_L3: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 3)); - case Obj::RANDOM_MONSTER_L4: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 4)); - case Obj::RANDOM_RESOURCE: - return std::make_pair(Obj::RESOURCE,getRandomGenerator().nextInt(6)); //now it's OH3 style, use %8 for mithril - case Obj::RANDOM_TOWN: - { - PlayerColor align = (dynamic_cast(obj))->alignmentToPlayer; - si32 f; // can be negative (for random) - if(!align.isValidPlayer()) //same as owner / random - { - if(!obj->tempOwner.isValidPlayer()) - f = -1; //random - else - f = scenarioOps->getIthPlayersSettings(obj->tempOwner).castle; - } - else - { - f = scenarioOps->getIthPlayersSettings(align).castle; - } - if(f<0) - { - do - { - f = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - } - while ((*VLC->townh)[f]->town == nullptr); // find playable faction - } - return std::make_pair(Obj::TOWN,f); - } - case Obj::RANDOM_MONSTER_L5: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 5)); - case Obj::RANDOM_MONSTER_L6: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 6)); - case Obj::RANDOM_MONSTER_L7: - return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 7)); - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - { - auto * dwl = dynamic_cast(obj); - int faction; - - //if castle alignment available - if(auto * info = dynamic_cast(dwl->info)) - { - faction = getRandomGenerator().nextInt((int)VLC->townh->size() - 1); - if(info->asCastle && !info->instanceId.empty()) - { - auto iter = map->instanceNames.find(info->instanceId); - - if(iter == map->instanceNames.end()) - logGlobal->error("Map object not found: %s", info->instanceId); - else - { - auto elem = iter->second; - if(elem->ID==Obj::RANDOM_TOWN) - { - randomizeObject(elem.get()); //we have to randomize the castle first - faction = elem->subID; - } - else if(elem->ID==Obj::TOWN) - faction = elem->subID; - else - logGlobal->error("Map object must be town: %s", info->instanceId); - } - } - else if(info->asCastle) - { - - for(auto & elem : map->objects) - { - if(!elem) - continue; - - if(elem->ID==Obj::RANDOM_TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - randomizeObject(elem); //we have to randomize the castle first - faction = elem->subID; - break; - } - else if(elem->ID==Obj::TOWN - && dynamic_cast(elem.get())->identifier == info->identifier) - { - faction = elem->subID; - break; - } - } - } - else - { - std::set temp; - - for(int i = 0; i < info->allowedFactions.size(); i++) - if(info->allowedFactions[i]) - temp.insert(i); - - if(temp.empty()) - logGlobal->error("Random faction selection failed. Nothing is allowed. Fall back to random."); - else - faction = *RandomGeneratorUtil::nextItem(temp, getRandomGenerator()); - } - } - else // castle alignment fixed - faction = obj->subID; - - int level; - - //if level set to range - if(auto * info = dynamic_cast(dwl->info)) - { - level = getRandomGenerator().nextInt(info->minLevel, info->maxLevel) - 1; - } - else // fixed level - { - level = obj->subID; - } - - delete dwl->info; - dwl->info = nullptr; - - std::pair result(Obj::NO_OBJ, -1); - CreatureID cid; - if((*VLC->townh)[faction]->town) - cid = (*VLC->townh)[faction]->town->creatures[level][0]; - else - { - //neutral faction - std::vector possibleCreatures; - std::copy_if(VLC->creh->objects.begin(), VLC->creh->objects.end(), std::back_inserter(possibleCreatures), [faction](const CCreature * c) - { - return c->getFaction().getNum() == faction; - }); - assert(!possibleCreatures.empty()); - cid = (*RandomGeneratorUtil::nextItem(possibleCreatures, getRandomGenerator()))->getId(); - } - - //NOTE: this will pick last dwelling with this creature (Mantis #900) - //check for block map equality is better but more complex solution - auto testID = [&](const Obj & primaryID) -> void - { - auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); - for (si32 entry : dwellingIDs) - { - const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); - - if (handler->producesCreature(VLC->creh->objects[cid])) - result = std::make_pair(primaryID, entry); - } - }; - - testID(Obj::CREATURE_GENERATOR1); - if (result.first == Obj::NO_OBJ) - testID(Obj::CREATURE_GENERATOR4); - - if (result.first == Obj::NO_OBJ) - { - logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); - result = std::make_pair(Obj::CREATURE_GENERATOR1, *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), getRandomGenerator())); - } - - return result; - } - } - return std::make_pair(Obj::NO_OBJ,-1); -} - -void CGameState::randomizeObject(CGObjectInstance *cur) -{ - std::pair ran = pickObject(cur); - if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything - { - if(cur->ID==Obj::TOWN || cur->ID==Obj::MONSTER) - cur->setType(cur->ID, cur->subID); // update def, if necessary - } - else if(ran.first==Obj::HERO)//special code for hero - { - auto * h = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->heroesOnMap.emplace_back(h); - } - else if(ran.first==Obj::TOWN)//special code for town - { - auto * t = dynamic_cast(cur); - cur->setType(ran.first, ran.second); - map->towns.emplace_back(t); - } - else - { - cur->setType(ran.first, ran.second); - } -} - int CGameState::getDate(Date mode) const { int temp; @@ -766,9 +549,33 @@ void CGameState::randomizeMapObjects() logGlobal->debug("\tRandomizing objects"); for(CGObjectInstance *obj : map->objects) { - if(!obj) continue; + if(!obj) + continue; - randomizeObject(obj); + { + std::pair ran = pickObject(obj); + if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything + { + if(obj->ID==Obj::TOWN || obj->ID==Obj::MONSTER) + obj->setType(obj->ID, obj->subID); // update def, if necessary + } + else if(ran.first==Obj::HERO)//special code for hero + { + auto * h = dynamic_cast(obj); + obj->setType(ran.first, ran.second); + map->heroesOnMap.emplace_back(h); + } + else if(ran.first==Obj::TOWN)//special code for town + { + auto * t = dynamic_cast(obj); + obj->setType(ran.first, ran.second); + map->towns.emplace_back(t); + } + else + { + obj->setType(ran.first, ran.second); + } + } //handle Favouring Winds - mark tiles under it if(obj->ID == Obj::FAVORABLE_WINDS) diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 94db3c60e..a99076230 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -187,7 +187,6 @@ private: void initGrailPosition(); void initRandomFactionsForPlayers(); void randomizeMapObjects(); - void randomizeObject(CGObjectInstance *cur); void initPlayerStates(); void placeStartingHeroes(); void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos); @@ -214,7 +213,6 @@ private: CGHeroInstance * getUsedHero(const HeroTypeID & hid) const; bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; - std::pair pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index dff3ed95d..a4f359b3e 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -160,6 +160,38 @@ CreatureID CGCreature::getCreature() const return CreatureID(getObjTypeIndex().getNum()); } +void CGCreature::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID) + { + case MapObjectID::RANDOM_MONSTER: + subID = VLC->creh->pickRandomMonster(rand); + break; + case MapObjectID::RANDOM_MONSTER_L1: + subID = VLC->creh->pickRandomMonster(rand, 1); + break; + case MapObjectID::RANDOM_MONSTER_L2: + subID = VLC->creh->pickRandomMonster(rand, 2); + break; + case MapObjectID::RANDOM_MONSTER_L3: + subID = VLC->creh->pickRandomMonster(rand, 3); + break; + case MapObjectID::RANDOM_MONSTER_L4: + subID = VLC->creh->pickRandomMonster(rand, 4); + break; + case MapObjectID::RANDOM_MONSTER_L5: + subID = VLC->creh->pickRandomMonster(rand, 5); + break; + case MapObjectID::RANDOM_MONSTER_L6: + subID = VLC->creh->pickRandomMonster(rand, 6); + break; + case MapObjectID::RANDOM_MONSTER_L7: + subID = VLC->creh->pickRandomMonster(rand, 7); + break; + } + ID = MapObjectID::MONSTER; +} + void CGCreature::initObj(CRandomGenerator & rand) { blockVisit = true; diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 438e89c92..13ff02f30 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -41,6 +41,7 @@ public: std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void newTurn(CRandomGenerator & rand) const override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 70fc4d983..a911e3089 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -11,8 +11,10 @@ #include "StdInc.h" #include "CGDwelling.h" #include "../serializer/JsonSerializeFormat.h" +#include "../mapping/CMap.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" +#include "../mapObjectConstructors/DwellingInstanceConstructor.h" #include "../mapObjects/CGHeroInstance.h" #include "../networkPacks/StackLocation.h" #include "../networkPacks/PacksForClient.h" @@ -26,41 +28,10 @@ VCMI_LIB_NAMESPACE_BEGIN -CSpecObjInfo::CSpecObjInfo(): - owner(nullptr) -{ - -} - -void CCreGenAsCastleInfo::serializeJson(JsonSerializeFormat & handler) +void CGDwellingRandomizationInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeString("sameAsTown", instanceId); - - if(!handler.saving) - { - asCastle = !instanceId.empty(); - allowedFactions.clear(); - } - - if(!asCastle) - { - std::vector standard; - standard.resize(VLC->townh->size(), true); - - JsonSerializeFormat::LIC allowedLIC(standard, &FactionID::decode, &FactionID::encode); - allowedLIC.any = allowedFactions; - - handler.serializeLIC("allowedFactions", allowedLIC); - - if(!handler.saving) - { - allowedFactions = allowedLIC.any; - } - } -} - -void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) -{ + handler.serializeIdArray("allowedFactions", allowedFactions); handler.serializeInt("minLevel", minLevel, static_cast(1)); handler.serializeInt("maxLevel", maxLevel, static_cast(7)); @@ -72,20 +43,119 @@ void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler) } } -void CCreGenLeveledCastleInfo::serializeJson(JsonSerializeFormat & handler) +CGDwelling::CGDwelling() = default; +CGDwelling::~CGDwelling() = default; + +FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) { - CCreGenAsCastleInfo::serializeJson(handler); - CCreGenLeveledInfo::serializeJson(handler); + assert(randomizationInfo.has_value()); + if (!randomizationInfo) + return FactionID::CASTLE; + + CGTownInstance * linkedTown = nullptr; + + if (!randomizationInfo->instanceId.empty()) + { + auto iter = cb->gameState()->map->instanceNames.find(randomizationInfo->instanceId); + + if(iter == cb->gameState()->map->instanceNames.end()) + logGlobal->error("Map object not found: %s", randomizationInfo->instanceId); + linkedTown = dynamic_cast(iter->second.get()); + } + + if (randomizationInfo->identifier != 0) + { + for(auto & elem : cb->gameState()->map->objects) + { + auto town = dynamic_cast(elem.get()); + if(town && town->identifier == randomizationInfo->identifier) + { + linkedTown = town; + break; + } + } + } + + if (linkedTown) + { + if(linkedTown->ID==Obj::RANDOM_TOWN) + linkedTown->pickRandomObject(rand); //we have to randomize the castle first + + assert(linkedTown->ID == Obj::TOWN); + if(linkedTown->ID==Obj::TOWN) + return linkedTown->getFaction(); + } + + if(!randomizationInfo->allowedFactions.empty()) + return *RandomGeneratorUtil::nextItem(randomizationInfo->allowedFactions, rand); + + + std::vector potentialPicks; + + for (FactionID faction(0); faction < VLC->townh->size(); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); } -CGDwelling::CGDwelling() - : info(nullptr) +int CGDwelling::randomizeLevel(CRandomGenerator & rand) { + assert(randomizationInfo.has_value()); + + if (!randomizationInfo) + return rand.nextInt(1, 7) - 1; + + if(randomizationInfo->minLevel == randomizationInfo->maxLevel) + return randomizationInfo->minLevel - 1; + + return rand.nextInt(randomizationInfo->minLevel, randomizationInfo->maxLevel) - 1; } -CGDwelling::~CGDwelling() +void CGDwelling::pickRandomObject(CRandomGenerator & rand) { - vstd::clear_pointer(info); + if (ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL || ID == Obj::RANDOM_DWELLING_FACTION) + { + FactionID faction = randomizeFaction(rand); + int level = randomizeLevel(rand); + assert(faction != FactionID::NONE && faction != FactionID::NEUTRAL); + assert(level >= 1 && level <= 7); + randomizationInfo.reset(); + + CreatureID cid = (*VLC->townh)[faction]->town->creatures[level][0]; + + //NOTE: this will pick last dwelling with this creature (Mantis #900) + //check for block map equality is better but more complex solution + auto testID = [&](const Obj & primaryID) -> MapObjectSubID + { + auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); + for (si32 entry : dwellingIDs) + { + const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); + + if (handler->producesCreature(VLC->creh->objects[cid])) + return MapObjectSubID(entry); + } + return MapObjectSubID(); + }; + + ID = Obj::CREATURE_GENERATOR1; + subID = testID(Obj::CREATURE_GENERATOR1); + + if (subID == MapObjectSubID()) + { + ID = Obj::CREATURE_GENERATOR4; + subID = testID(Obj::CREATURE_GENERATOR4); + } + + if (subID == MapObjectSubID()) + { + logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level)); + ID = Obj::CREATURE_GENERATOR4; + subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand); + } + } } void CGDwelling::initObj(CRandomGenerator & rand) @@ -121,23 +191,6 @@ void CGDwelling::initObj(CRandomGenerator & rand) } } -void CGDwelling::initRandomObjectInfo() -{ - vstd::clear_pointer(info); - switch(ID) - { - case Obj::RANDOM_DWELLING: info = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: info = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: info = new CCreGenLeveledInfo(); - break; - } - - if(info) - info->owner = this; -} - void CGDwelling::setPropertyDer(ui8 what, ui32 val) { switch (what) @@ -425,9 +478,6 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) { - if(!handler.saving) - initRandomObjectInfo(); - switch (ID) { case Obj::WAR_MACHINE_FACTORY: @@ -437,8 +487,10 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) case Obj::RANDOM_DWELLING: case Obj::RANDOM_DWELLING_LVL: case Obj::RANDOM_DWELLING_FACTION: - info->serializeJson(handler); - //fall through + if (!handler.saving) + randomizationInfo = CGDwellingRandomizationInfo(); + randomizationInfo->serializeJson(handler); + [[fallthrough]]; default: serializeJsonOwner(handler); break; diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index f27cf44e2..bc63dcd85 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -16,62 +16,39 @@ VCMI_LIB_NAMESPACE_BEGIN class CGDwelling; -class DLL_LINKAGE CSpecObjInfo +class DLL_LINKAGE CGDwellingRandomizationInfo { public: - CSpecObjInfo(); - virtual ~CSpecObjInfo() = default; - - virtual void serializeJson(JsonSerializeFormat & handler) = 0; - - const CGDwelling * owner; -}; - -class DLL_LINKAGE CCreGenAsCastleInfo : public virtual CSpecObjInfo -{ -public: - bool asCastle = false; - ui32 identifier = 0;//h3m internal identifier - - std::vector allowedFactions; + std::set allowedFactions; std::string instanceId;//vcmi map instance identifier - void serializeJson(JsonSerializeFormat & handler) override; + int32_t identifier = 0;//h3m internal identifier + + uint8_t minLevel = 1; + uint8_t maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> + + void serializeJson(JsonSerializeFormat & handler); }; -class DLL_LINKAGE CCreGenLeveledInfo : public virtual CSpecObjInfo -{ -public: - ui8 minLevel = 1; - ui8 maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7> - - void serializeJson(JsonSerializeFormat & handler) override; -}; - -class DLL_LINKAGE CCreGenLeveledCastleInfo : public CCreGenAsCastleInfo, public CCreGenLeveledInfo -{ -public: - CCreGenLeveledCastleInfo() = default; - void serializeJson(JsonSerializeFormat & handler) override; -}; - - class DLL_LINKAGE CGDwelling : public CArmedInstance { public: typedef std::vector > > TCreaturesSet; - CSpecObjInfo * info; //random dwelling options; not serialized + std::optional randomizationInfo; //random dwelling options; not serialized TCreaturesSet creatures; //creatures[level] -> CGDwelling(); ~CGDwelling() override; - void initRandomObjectInfo(); protected: void serializeJsonOptions(JsonSerializeFormat & handler) override; private: + FactionID randomizeFaction(CRandomGenerator & rand); + int randomizeLevel(CRandomGenerator & rand); + + void pickRandomObject(CRandomGenerator & rand) override; void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index c122af7ab..673d91684 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -571,6 +571,17 @@ void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() wisdomCounter = 1; } +void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO); + + if (ID == Obj::RANDOM_HERO) + { + ID = Obj::HERO; + subID = cb->gameState()->pickNextHeroType(getOwner()); + } +} + void CGHeroInstance::initObj(CRandomGenerator & rand) { if(!type) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a742f2d7b..492495d42 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -295,6 +295,7 @@ public: void deserializationFix(); void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; std::string getObjectName() const override; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 1f153a75f..435e37c6c 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -166,6 +166,11 @@ void CGObjectInstance::setType(si32 newID, si32 newSubID) cb->gameState()->map->addBlockVisTiles(this); } +void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) +{ + // no-op +} + void CGObjectInstance::initObj(CRandomGenerator & rand) { switch(ID) diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 4d1fbc9fd..d56427e3b 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -128,6 +128,7 @@ public: /** OVERRIDES OF IObjectInterface **/ void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; /// method for synchronous update. Note: For new properties classes should override setPropertyDer instead void setProperty(ui8 what, ui32 val) final; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index a4841a3cd..5b66236d1 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -20,6 +20,7 @@ #include "../gameState/CGameState.h" #include "../mapping/CMap.h" #include "../CPlayerState.h" +#include "../StartInfo.h" #include "../TerrainHandler.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" @@ -459,6 +460,34 @@ void CGTownInstance::deleteTownBonus(BuildingID bid) delete freeIt; } +FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) +{ + if(getOwner().isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(getOwner()).castle; + + if(alignmentToPlayer.isValidPlayer()) + return cb->gameState()->scenarioOps->getIthPlayersSettings(alignmentToPlayer).castle; + + std::vector potentialPicks; + + for (FactionID faction(0); faction < VLC->townh->size(); ++faction) + if (VLC->factions()->getById(faction)->hasTown()) + potentialPicks.push_back(faction); + + assert(!potentialPicks.empty()); + return *RandomGeneratorUtil::nextItem(potentialPicks, rand); +} + +void CGTownInstance::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == MapObjectID::TOWN || ID == MapObjectID::RANDOM_TOWN); + if (ID == MapObjectID::RANDOM_TOWN) + { + ID = MapObjectID::TOWN; + subID = randomizeFaction(rand); + } +} + void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structures { blockVisit = true; diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 07da57644..18a6e8eb2 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -197,6 +197,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void onHeroLeave(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void battleFinished(const CGHeroInstance * hero, const BattleResult & result) const override; std::string getObjectName() const override; @@ -216,6 +217,7 @@ protected: void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; private: + FactionID randomizeFaction(CRandomGenerator & rand); void setOwner(const PlayerColor & owner) const; void onTownCaptured(const PlayerColor & winner) const; int getDwellingBonus(const std::vector& creatureIds, const std::vector >& dwellings) const; diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 6a6ca40b7..8dab1a59d 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -45,6 +45,7 @@ public: virtual void onHeroLeave(const CGHeroInstance * h) const; virtual void newTurn(CRandomGenerator & rand) const; virtual void initObj(CRandomGenerator & rand); //synchr + virtual void pickRandomObject(CRandomGenerator & rand); virtual void setProperty(ui8 what, ui32 val);//synchr //Called when queries created DURING HERO VISIT are resolved diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 5e688e4d9..fe6367aaf 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -247,6 +247,17 @@ std::string CGResource::getHoverText(PlayerColor player) const return VLC->generaltexth->restypes[resourceID()]; } +void CGResource::pickRandomObject(CRandomGenerator & rand) +{ + assert(ID == Obj::RESOURCE || ID == Obj::RANDOM_RESOURCE); + + if (ID == Obj::RANDOM_RESOURCE) + { + ID = Obj::RESOURCE; + subID = rand.nextInt(EGameResID::WOOD, EGameResID::GOLD); + } +} + void CGResource::initObj(CRandomGenerator & rand) { blockVisit = true; @@ -701,6 +712,31 @@ ArtifactID CGArtifact::getArtifact() const return getObjTypeIndex().getNum(); } +void CGArtifact::pickRandomObject(CRandomGenerator & rand) +{ + switch(ID) + { + case MapObjectID::RANDOM_ART: + subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); + break; + case MapObjectID::RANDOM_TREASURE_ART: + subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE); + break; + case MapObjectID::RANDOM_MINOR_ART: + subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR); + break; + case MapObjectID::RANDOM_MAJOR_ART: + subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR); + break; + case MapObjectID::RANDOM_RELIC_ART: + subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_RELIC); + break; + } + + if (ID != Obj::SPELL_SCROLL) + ID = MapObjectID::ARTIFACT; +} + void CGArtifact::initObj(CRandomGenerator & rand) { blockVisit = true; diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index c40256dc1..3d0e4264f 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -89,6 +89,7 @@ public: void pick( const CGHeroInstance * h ) const; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void afterAddToMap(CMap * map) override; BattleField getBattlefield() const override; @@ -115,6 +116,7 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; void initObj(CRandomGenerator & rand) override; + void pickRandomObject(CRandomGenerator & rand) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::string getHoverText(PlayerColor player) const override; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 06c3e951a..a94fbc262 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1322,60 +1322,27 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s { auto * object = new CGDwelling(); - CSpecObjInfo * spec = nullptr; - switch(objectTemplate->id) - { - case Obj::RANDOM_DWELLING: - spec = new CCreGenLeveledCastleInfo(); - break; - case Obj::RANDOM_DWELLING_LVL: - spec = new CCreGenAsCastleInfo(); - break; - case Obj::RANDOM_DWELLING_FACTION: - spec = new CCreGenLeveledInfo(); - break; - default: - throw std::runtime_error("Invalid random dwelling format"); - } - spec->owner = object; - setOwnerAndValidate(mapPosition, object, reader->readPlayer32()); - //216 and 217 - if(auto * castleSpec = dynamic_cast(spec)) + object->randomizationInfo = CGDwellingRandomizationInfo(); + + bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION; + bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL; + + if (hasFactionInfo) { - castleSpec->instanceId = ""; - castleSpec->identifier = reader->readUInt32(); - if(!castleSpec->identifier) - { - castleSpec->asCastle = false; - const int MASK_SIZE = 8; - ui8 mask[2]; - mask[0] = reader->readUInt8(); - mask[1] = reader->readUInt8(); + object->randomizationInfo->identifier = reader->readUInt32(); - castleSpec->allowedFactions.clear(); - castleSpec->allowedFactions.resize(VLC->townh->size(), false); - - for(int i = 0; i < MASK_SIZE; i++) - castleSpec->allowedFactions[i] = ((mask[0] & (1 << i)) > 0); - - for(int i = 0; i < (GameConstants::F_NUMBER - MASK_SIZE); i++) - castleSpec->allowedFactions[i + MASK_SIZE] = ((mask[1] & (1 << i)) > 0); - } - else - { - castleSpec->asCastle = true; - } + if(object->randomizationInfo->identifier != 0) + reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false); } - //216 and 218 - if(auto * lvlSpec = dynamic_cast(spec)) + if(hasLevelInfo) { - lvlSpec->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; - lvlSpec->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; + object->randomizationInfo->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; + object->randomizationInfo->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; } - object->info = spec; + return object; } From 7a096460091530330ed04387d22c9f4d5c086cf1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 25 Oct 2023 21:56:00 +0300 Subject: [PATCH 1022/1248] Cleaned up dwelling randomization --- lib/gameState/CGameState.cpp | 83 +++++++++-------------------- lib/gameState/CGameState.h | 3 +- lib/mapObjects/CGCreature.cpp | 1 - lib/mapObjects/CGDwelling.cpp | 8 +++ lib/mapObjects/CGHeroInstance.cpp | 18 +++---- lib/mapObjects/CGHeroInstance.h | 1 - lib/mapObjects/CGObjectInstance.h | 7 ++- lib/mapObjects/CGTownInstance.cpp | 15 +++--- lib/mapObjects/CGTownInstance.h | 1 - lib/mapObjects/IObjectInterface.cpp | 3 ++ lib/mapping/MapFormatH3M.cpp | 7 +++ mapeditor/inspector/inspector.cpp | 27 +++------- 12 files changed, 69 insertions(+), 105 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index bd88d0b03..b3aebe1b7 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -77,34 +77,6 @@ public: } }; -static CGObjectInstance * createObject(MapObjectID id, MapObjectSubID subid, const int3 & pos, const PlayerColor & owner) -{ - CGObjectInstance * nobj; - switch(id) - { - case Obj::HERO: - { - auto handler = VLC->objtypeh->getHandlerFor(id, VLC->heroh->objects[subid]->heroClass->getIndex()); - nobj = handler->create(handler->getTemplates().front()); - break; - } - case Obj::TOWN: - nobj = new CGTownInstance(); - break; - default: //rest of objects - nobj = new CGObjectInstance(); - break; - } - nobj->ID = id; - nobj->subID = subid; - nobj->pos = pos; - nobj->tempOwner = owner; - if (id != Obj::HERO) - nobj->appearance = VLC->objtypeh->getHandlerFor(id, subid)->getTemplates().front(); - - return nobj; -} - HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner) { const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner); @@ -547,44 +519,30 @@ void CGameState::initRandomFactionsForPlayers() void CGameState::randomizeMapObjects() { logGlobal->debug("\tRandomizing objects"); - for(CGObjectInstance *obj : map->objects) + for(CGObjectInstance *object : map->objects) { - if(!obj) + if(!object) continue; - { - std::pair ran = pickObject(obj); - if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything - { - if(obj->ID==Obj::TOWN || obj->ID==Obj::MONSTER) - obj->setType(obj->ID, obj->subID); // update def, if necessary - } - else if(ran.first==Obj::HERO)//special code for hero - { - auto * h = dynamic_cast(obj); - obj->setType(ran.first, ran.second); - map->heroesOnMap.emplace_back(h); - } - else if(ran.first==Obj::TOWN)//special code for town - { - auto * t = dynamic_cast(obj); - obj->setType(ran.first, ran.second); - map->towns.emplace_back(t); - } - else - { - obj->setType(ran.first, ran.second); - } - } + object->pickRandomObject(getRandomGenerator()); + + auto * hero = dynamic_cast(object); + auto * town = dynamic_cast(object); + + if (hero) + map->heroesOnMap.emplace_back(hero); + + if (town) + map->towns.emplace_back(town); //handle Favouring Winds - mark tiles under it - if(obj->ID == Obj::FAVORABLE_WINDS) + if(object->ID == Obj::FAVORABLE_WINDS) { - for (int i = 0; i < obj->getWidth() ; i++) + for (int i = 0; i < object->getWidth() ; i++) { - for (int j = 0; j < obj->getHeight() ; j++) + for (int j = 0; j < object->getHeight() ; j++) { - int3 pos = obj->pos - int3(i,j,0); + int3 pos = object->pos - int3(i,j,0); if(map->isInTheMap(pos)) map->getTile(pos).extTileFlags |= 128; } } @@ -617,7 +575,14 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } } - CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId, townPos, playerColor); + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, VLC->heroh->objects[heroTypeId]->heroClass->getIndex()); + CGObjectInstance * hero = handler->create(handler->getTemplates().front()); + + hero->ID = Obj::HERO; + hero->subID = VLC->heroh->objects[heroTypeId]->heroClass->getIndex(); + hero->tempOwner = playerColor; + + hero->pos = townPos; hero->pos += hero->getVisitableOffset(); map->getEditManager()->insertObject(hero); } diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index a99076230..7b613c05d 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -115,6 +115,8 @@ public: void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override; bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid); + /// picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly + HeroTypeID pickNextHeroType(const PlayerColor & owner); void apply(CPack *pack); BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand); @@ -214,7 +216,6 @@ private: bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons std::set getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const; HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly - HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const; // ---- data ----- diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index a4f359b3e..cab0a1523 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -152,7 +152,6 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const break; } } - } CreatureID CGCreature::getCreature() const diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index a911e3089..ff8e73886 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -48,6 +48,10 @@ CGDwelling::~CGDwelling() = default; FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) { + if (ID == Obj::RANDOM_DWELLING_FACTION) + return FactionID(subID); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL); assert(randomizationInfo.has_value()); if (!randomizationInfo) return FactionID::CASTLE; @@ -102,6 +106,10 @@ FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) int CGDwelling::randomizeLevel(CRandomGenerator & rand) { + if (ID == Obj::RANDOM_DWELLING_LVL) + return subID.getNum(); + + assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_FACTION); assert(randomizationInfo.has_value()); if (!randomizationInfo) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 673d91684..c625e7c95 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -296,16 +296,6 @@ void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) initHero(rand); } -void CGHeroInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::HERO); // just in case - type = VLC->heroh->objects[subID]; - - CGObjectInstance::setType(ID, type->heroClass->getIndex()); // to find object handler we must use heroClass->id - this->subID = subID; // after setType subID used to store unique hero identify id. Check issue 2277 for details - randomizeArmy(type->heroClass->faction); -} - void CGHeroInstance::initHero(CRandomGenerator & rand) { assert(validTypes(true)); @@ -580,6 +570,14 @@ void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) ID = Obj::HERO; subID = cb->gameState()->pickNextHeroType(getOwner()); } + type = VLC->heroh->objects[subID]; + + // to find object handler we must use heroClass->id + // after setType subID used to store unique hero identify id. Check issue 2277 for details + setType(ID, type->heroClass->getIndex()); + this->subID = subID; + + randomizeArmy(type->heroClass->faction); } void CGHeroInstance::initObj(CRandomGenerator & rand) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 492495d42..0c141d1e0 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -232,7 +232,6 @@ public: ////////////////////////////////////////////////////////////////////////// HeroTypeID getHeroType() const; - void setType(si32 ID, si32 subID) override; void initHero(CRandomGenerator & rand); void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index d56427e3b..d235694b6 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -106,10 +106,6 @@ public: virtual int getSightRadius() const; /// returns (x,y,0) offset to a visitable tile of object virtual int3 getVisitableOffset() const; - /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") - virtual void setType(si32 ID, si32 subID); - - /// returns text visible in status bar with specific hero/player active. /// Returns generic name of object, without any player-specific info virtual std::string getObjectName() const; @@ -160,6 +156,9 @@ protected: /// virtual method that allows synchronously update object state on server and all clients virtual void setPropertyDer(ui8 what, ui32 val); + /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") + void setType(si32 ID, si32 subID); + /// Gives dummy bonus from this object to hero. Can be used to track visited state void giveDummyBonus(const ObjectInstanceID & heroID, BonusDuration::Type duration = BonusDuration::ONE_DAY) const; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 5b66236d1..b16d548f7 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -486,6 +486,12 @@ void CGTownInstance::pickRandomObject(CRandomGenerator & rand) ID = MapObjectID::TOWN; subID = randomizeFaction(rand); } + + assert(ID == Obj::TOWN); // just in case + setType(ID, subID); + town = (*VLC->townh)[subID]->town; + randomizeArmy(subID); + updateAppearance(); } void CGTownInstance::initObj(CRandomGenerator & rand) ///initialize town structures @@ -779,15 +785,6 @@ std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const return IMarket::availableItemsIds(mode); } -void CGTownInstance::setType(si32 ID, si32 subID) -{ - assert(ID == Obj::TOWN); // just in case - CGObjectInstance::setType(ID, subID); - town = (*VLC->townh)[subID]->town; - randomizeArmy(subID); - updateAppearance(); -} - void CGTownInstance::updateAppearance() { auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 18a6e8eb2..2a197ec55 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -141,7 +141,6 @@ public: bool allowsTrade(EMarketMode mode) const override; std::vector availableItemsIds(EMarketMode mode) const override; - void setType(si32 ID, si32 subID) override; void updateAppearance(); ////////////////////////////////////////////////////////////////////////// diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index 268c99c4c..b0e0b11cd 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -46,6 +46,9 @@ void IObjectInterface::newTurn(CRandomGenerator & rand) const void IObjectInterface::initObj(CRandomGenerator & rand) {} +void IObjectInterface::pickRandomObject(CRandomGenerator & rand) +{} + void IObjectInterface::setProperty( ui8 what, ui32 val ) {} diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index a94fbc262..6bb12268b 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1336,12 +1336,19 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s if(object->randomizationInfo->identifier != 0) reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false); } + else + object->randomizationInfo->allowedFactions.insert(FactionID(objectTemplate->subid)); if(hasLevelInfo) { object->randomizationInfo->minLevel = std::max(reader->readUInt8(), static_cast(0)) + 1; object->randomizationInfo->maxLevel = std::min(reader->readUInt8(), static_cast(6)) + 1; } + else + { + object->randomizationInfo->minLevel = objectTemplate->subid; + object->randomizationInfo->maxLevel = objectTemplate->subid; + } return object; } diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index fe81dd27e..459a7daa4 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -87,14 +87,6 @@ void Initializer::initialize(CGDwelling * o) if(!o) return; o->tempOwner = defaultPlayer; - - switch(o->ID) - { - case Obj::RANDOM_DWELLING: - case Obj::RANDOM_DWELLING_LVL: - case Obj::RANDOM_DWELLING_FACTION: - o->initRandomObjectInfo(); - } } void Initializer::initialize(CGGarrison * o) @@ -243,11 +235,8 @@ void Inspector::updateProperties(CGDwelling * o) addProperty("Owner", o->tempOwner, false); - if(dynamic_cast(o->info)) - { - auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); - addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); - } + auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); + addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); } void Inspector::updateProperties(CGLighthouse * o) @@ -641,12 +630,12 @@ void Inspector::setProperty(CGDwelling * o, const QString & key, const QVariant if(key == "Same as town") { - if(auto * info = dynamic_cast(o->info)) - { - info->instanceId = ""; - if(CGTownInstance * town = data_cast(value.toLongLong())) - info->instanceId = town->instanceName; - } + if (!o->randomizationInfo.has_value()) + o->randomizationInfo = CGDwellingRandomizationInfo(); + + o->randomizationInfo->instanceId = ""; + if(CGTownInstance * town = data_cast(value.toLongLong())) + o->randomizationInfo->instanceId = town->instanceName; } } From cc30bdda044fd975ed40ddfd98f3920a56b01ecf Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 26 Oct 2023 14:38:21 +0300 Subject: [PATCH 1023/1248] Remove few more subID usages --- lib/gameState/CGameState.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index b3aebe1b7..bff5932a8 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -638,7 +638,7 @@ void CGameState::initHeroes() hero->initHero(getRandomGenerator()); getPlayerState(hero->getOwner())->heroes.push_back(hero); - map->allHeroes[hero->type->getIndex()] = hero; + map->allHeroes[hero->getHeroType()] = hero; } // generate boats for all heroes on water @@ -666,7 +666,10 @@ void CGameState::initHeroes() for(auto obj : map->objects) //prisons { if(obj && obj->ID == Obj::PRISON) - map->allHeroes[obj->subID] = dynamic_cast(obj.get()); + { + auto * hero = dynamic_cast(obj.get()); + map->allHeroes[hero->getHeroType()] = hero; + } } std::set heroesToCreate = getUnusedAllowedHeroes(); //ids of heroes to be created and put into the pool @@ -678,7 +681,7 @@ void CGameState::initHeroes() heroesPool->addHeroToPool(ph); heroesToCreate.erase(ph->type->getId()); - map->allHeroes[ph->subID] = ph; + map->allHeroes[ph->getHeroType()] = ph; } for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool @@ -789,6 +792,7 @@ void CGameState::initTowns() for (auto & elem : map->towns) { CGTownInstance * vti =(elem); + assert(vti->town); if(!vti->town) { vti->town = (*VLC->townh)[vti->subID]->town; From 461c481ef37186560cda75a1f9f40b053287d11b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 26 Oct 2023 16:50:29 +0300 Subject: [PATCH 1024/1248] Fix game startup --- lib/gameState/CGameState.cpp | 12 ++++++------ lib/mapObjects/CGCreature.cpp | 1 + lib/mapObjects/CGHeroInstance.cpp | 14 ++++++++------ lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGObjectInstance.h | 2 +- lib/mapObjects/MiscObjects.cpp | 3 +++ 6 files changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index bff5932a8..8f9ca1a0f 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -529,7 +529,7 @@ void CGameState::randomizeMapObjects() auto * hero = dynamic_cast(object); auto * town = dynamic_cast(object); - if (hero) + if (hero && hero->ID != Obj::PRISON) map->heroesOnMap.emplace_back(hero); if (town) @@ -579,7 +579,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy CGObjectInstance * hero = handler->create(handler->getTemplates().front()); hero->ID = Obj::HERO; - hero->subID = VLC->heroh->objects[heroTypeId]->heroClass->getIndex(); + hero->subID = heroTypeId; hero->tempOwner = playerColor; hero->pos = townPos; @@ -1826,10 +1826,10 @@ void CGameState::buildBonusSystemTree() buildGlobalTeamPlayerTree(); attachArmedObjects(); - for(CGTownInstance *t : map->towns) - { - t->deserializationFix(); - } +// for(CGTownInstance *t : map->towns) +// { +// t->deserializationFix(); +// } // CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact // are provided on initializing / deserializing diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index cab0a1523..2aa2563d2 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -189,6 +189,7 @@ void CGCreature::pickRandomObject(CRandomGenerator & rand) break; } ID = MapObjectID::MONSTER; + setType(ID, subID); } void CGCreature::initObj(CRandomGenerator & rand) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index c625e7c95..ad0c1f2f4 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -569,15 +569,17 @@ void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) { ID = Obj::HERO; subID = cb->gameState()->pickNextHeroType(getOwner()); + type = VLC->heroh->objects[subID]; + randomizeArmy(type->heroClass->faction); } - type = VLC->heroh->objects[subID]; - // to find object handler we must use heroClass->id - // after setType subID used to store unique hero identify id. Check issue 2277 for details - setType(ID, type->heroClass->getIndex()); + if (ID != Obj::PRISON) + { + // to find object handler we must use heroClass->id + // after setType subID used to store unique hero identify id. Check issue 2277 for details + setType(ID, type->heroClass->getIndex()); + } this->subID = subID; - - randomizeArmy(type->heroClass->faction); } void CGHeroInstance::initObj(CRandomGenerator & rand) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 435e37c6c..92187bd28 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -122,7 +122,7 @@ std::set CGObjectInstance::getBlockedOffsets() const return appearance->getBlockedOffsets(); } -void CGObjectInstance::setType(si32 newID, si32 newSubID) +void CGObjectInstance::setType(MapObjectID newID, MapObjectSubID newSubID) { auto position = visitablePos(); auto oldOffset = getVisitableOffset(); diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index d235694b6..01eeffd2d 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -157,7 +157,7 @@ protected: virtual void setPropertyDer(ui8 what, ui32 val); /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") - void setType(si32 ID, si32 subID); + void setType(MapObjectID ID, MapObjectSubID subID); /// Gives dummy bonus from this object to hero. Can be used to track visited state void giveDummyBonus(const ObjectInstanceID & heroID, BonusDuration::Type duration = BonusDuration::ONE_DAY) const; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index fe6367aaf..78a5079ee 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -255,6 +255,7 @@ void CGResource::pickRandomObject(CRandomGenerator & rand) { ID = Obj::RESOURCE; subID = rand.nextInt(EGameResID::WOOD, EGameResID::GOLD); + setType(ID, subID); } } @@ -735,6 +736,8 @@ void CGArtifact::pickRandomObject(CRandomGenerator & rand) if (ID != Obj::SPELL_SCROLL) ID = MapObjectID::ARTIFACT; + + setType(ID, subID); } void CGArtifact::initObj(CRandomGenerator & rand) From 5cdbf408c79f82f54312c324eaf8ab8d8606560e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 26 Oct 2023 19:11:18 +0300 Subject: [PATCH 1025/1248] Slightly simplified heroes initialization --- lib/gameState/CGameState.cpp | 46 +++++++++++++++---------------- lib/mapObjects/CGHeroInstance.cpp | 24 +++++++--------- 2 files changed, 32 insertions(+), 38 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 8f9ca1a0f..003f1989c 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -576,7 +576,8 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, VLC->heroh->objects[heroTypeId]->heroClass->getIndex()); - CGObjectInstance * hero = handler->create(handler->getTemplates().front()); + CGObjectInstance * obj = handler->create(handler->getTemplates().front()); + CGHeroInstance * hero = dynamic_cast(obj); hero->ID = Obj::HERO; hero->subID = heroTypeId; @@ -668,6 +669,7 @@ void CGameState::initHeroes() if(obj && obj->ID == Obj::PRISON) { auto * hero = dynamic_cast(obj.get()); + hero->initHero(getRandomGenerator()); map->allHeroes[hero->getHeroType()] = hero; } } @@ -675,7 +677,7 @@ void CGameState::initHeroes() std::set heroesToCreate = getUnusedAllowedHeroes(); //ids of heroes to be created and put into the pool for(auto ph : map->predefinedHeroes) { - if(!vstd::contains(heroesToCreate, HeroTypeID(ph->subID))) + if(!vstd::contains(heroesToCreate, ph->getHeroType())) continue; ph->initHero(getRandomGenerator()); heroesPool->addHeroToPool(ph); @@ -793,10 +795,7 @@ void CGameState::initTowns() { CGTownInstance * vti =(elem); assert(vti->town); - if(!vti->town) - { - vti->town = (*VLC->townh)[vti->subID]->town; - } + if(vti->getNameTranslated().empty()) { size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1); @@ -1906,12 +1905,15 @@ std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow if(hero->type) ret -= hero->type->getId(); else - ret -= HeroTypeID(hero->subID); + ret -= hero->getHeroType(); } for(auto obj : map->objects) //prisons - if(obj && obj->ID == Obj::PRISON) - ret -= HeroTypeID(obj->subID); + { + auto * hero = dynamic_cast(obj.get()); + if(hero && hero->ID == Obj::PRISON) + ret -= hero->getHeroType(); + } return ret; } @@ -1923,23 +1925,19 @@ bool CGameState::isUsedHero(const HeroTypeID & hid) const CGHeroInstance * CGameState::getUsedHero(const HeroTypeID & hid) const { - for(auto hero : map->heroesOnMap) //heroes instances initialization - { - if(hero->type && hero->type->getId() == hid) - { - return hero; - } - } - for(auto obj : map->objects) //prisons { - if(obj && obj->ID == Obj::PRISON ) - { - auto * hero = dynamic_cast(obj.get()); - assert(hero); - if ( hero->type && hero->type->getId() == hid ) - return hero; - } + if (!obj) + continue; + + if ( obj->ID !=Obj::PRISON && obj->ID != Obj::HERO) + continue; + + auto * hero = dynamic_cast(obj.get()); + assert(hero); + + if (hero->getHeroType() == hid) + return hero; } return nullptr; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ad0c1f2f4..ba916b3bb 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -572,28 +572,24 @@ void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) type = VLC->heroh->objects[subID]; randomizeArmy(type->heroClass->faction); } + else + type = VLC->heroh->objects[subID]; + auto oldSubID = subID; + + // to find object handler we must use heroClass->id + // after setType subID used to store unique hero identify id. Check issue 2277 for details if (ID != Obj::PRISON) - { - // to find object handler we must use heroClass->id - // after setType subID used to store unique hero identify id. Check issue 2277 for details setType(ID, type->heroClass->getIndex()); - } - this->subID = subID; + else + setType(ID, 0); + + this->subID = oldSubID; } void CGHeroInstance::initObj(CRandomGenerator & rand) { - if(!type) - initHero(rand); //TODO: set up everything for prison before specialties are configured - if (ID != Obj::PRISON) - { - auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId(); - auto customApp = VLC->objtypeh->getHandlerFor(ID, type->heroClass->getIndex())->getOverride(terrain, this); - if (customApp) - appearance = customApp; - } } void CGHeroInstance::recreateSecondarySkillsBonuses() From 8346d71c98bbc1d22db62f937726c029b7309fea Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 28 Oct 2023 12:27:10 +0300 Subject: [PATCH 1026/1248] Remove more subID access --- AI/Nullkiller/Analyzers/BuildAnalyzer.cpp | 2 +- AI/Nullkiller/Behaviors/StartupBehavior.cpp | 2 +- AI/Nullkiller/Engine/FuzzyHelper.cpp | 7 ++--- AI/Nullkiller/Engine/PriorityEvaluator.cpp | 31 ++++++++++++++------- AI/VCAI/AIUtility.h | 1 - AI/VCAI/FuzzyEngines.cpp | 2 +- AI/VCAI/FuzzyHelper.cpp | 11 ++------ AI/VCAI/Goals/CollectRes.cpp | 4 +-- AI/VCAI/Goals/GatherTroops.cpp | 2 +- AI/VCAI/Pathfinding/PathfindingManager.cpp | 2 +- AI/VCAI/ResourceManager.cpp | 14 +--------- AI/VCAI/VCAI.cpp | 4 +-- AI/VCAI/VCAI.h | 2 +- CCallback.cpp | 2 +- client/NetPacksClient.cpp | 2 +- client/mapView/MapRendererContext.cpp | 6 +--- client/windows/CCastleInterface.cpp | 6 ++-- client/windows/CTradeWindow.cpp | 4 +-- lib/gameState/CGameState.cpp | 2 +- lib/gameState/CGameStateCampaign.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 6 ++++ lib/mapObjects/CGHeroInstance.h | 1 + lib/mapObjects/MiscObjects.cpp | 10 ++++++- lib/mapObjects/MiscObjects.h | 1 + lib/mapping/MapFormatJson.cpp | 2 +- lib/networkPacks/NetPacksLib.cpp | 3 +- lib/pathfinder/CPathfinder.cpp | 17 ++++++----- lib/rmg/RmgObject.cpp | 6 ++-- lib/rmg/modificators/TreasurePlacer.cpp | 2 +- lib/spells/ViewSpellInt.cpp | 2 +- mapeditor/mapcontroller.cpp | 4 +-- server/CGameHandler.cpp | 6 ++-- server/processors/HeroPoolProcessor.cpp | 14 +++++----- 33 files changed, 92 insertions(+), 90 deletions(-) diff --git a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp index 386e71779..8a6edc8c7 100644 --- a/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/BuildAnalyzer.cpp @@ -318,7 +318,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const { for(auto tdi : developmentInfos) { - if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid)) + if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid)) return true; } diff --git a/AI/Nullkiller/Behaviors/StartupBehavior.cpp b/AI/Nullkiller/Behaviors/StartupBehavior.cpp index 84abb41fe..3a7f59f72 100644 --- a/AI/Nullkiller/Behaviors/StartupBehavior.cpp +++ b/AI/Nullkiller/Behaviors/StartupBehavior.cpp @@ -71,7 +71,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown) for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects()) { - if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD)) + if((obj->ID == Obj::RESOURCE && dynamic_cast(obj)->resourceID() == EGameResID::GOLD) || obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::CAMPFIRE || obj->ID == Obj::WATER_WHEEL) diff --git a/AI/Nullkiller/Engine/FuzzyHelper.cpp b/AI/Nullkiller/Engine/FuzzyHelper.cpp index e550c47ba..5252f89c3 100644 --- a/AI/Nullkiller/Engine/FuzzyHelper.cpp +++ b/AI/Nullkiller/Engine/FuzzyHelper.cpp @@ -24,7 +24,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); @@ -161,10 +161,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj) } case Obj::PYRAMID: { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; + return estimateBankDanger(dynamic_cast(obj)); } default: return 0; diff --git a/AI/Nullkiller/Engine/PriorityEvaluator.cpp b/AI/Nullkiller/Engine/PriorityEvaluator.cpp index 98ce1a86f..be3d87845 100644 --- a/AI/Nullkiller/Engine/PriorityEvaluator.cpp +++ b/AI/Nullkiller/Engine/PriorityEvaluator.cpp @@ -122,7 +122,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer { //Fixme: unused variable hero - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); auto resources = bankInfo->getPossibleResourcesReward(); TResources result = TResources(); @@ -139,7 +139,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero) { - auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance); + auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); auto creatures = bankInfo->getPossibleCreaturesReward(); uint64_t result = 0; @@ -467,14 +467,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons switch(target->ID) { case Obj::MINE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto mine = dynamic_cast(target); + return mine->producedResource == EGameResID::GOLD ? 0.5f - : 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID); + : 0.4f * getTotalResourceRequirementStrength(mine->producedResource) + 0.1f * getResourceRequirementStrength(mine->producedResource); + } case Obj::RESOURCE: - return target->subID == GameResID(EGameResID::GOLD) + { + auto resource = dynamic_cast(target); + return resource->resourceID() == EGameResID::GOLD ? 0 - : 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID); + : 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID()); + } case Obj::CREATURE_BANK: { @@ -626,12 +632,14 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG const int dailyIncomeMultiplier = 5; const float enemyArmyEliminationGoldRewardRatio = 0.2f; const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2; - auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power switch(target->ID) { case Obj::RESOURCE: - return isGold ? 600 : 100; + { + auto * res = dynamic_cast(target); + return res->resourceID() == GameResID::GOLD ? 600 : 100; + } case Obj::TREASURE_CHEST: return 1500; case Obj::WATER_WHEEL: @@ -640,7 +648,10 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG return dailyIncomeMultiplier * estimateTownIncome(ai->cb.get(), target, hero); case Obj::MINE: case Obj::ABANDONED_MINE: - return dailyIncomeMultiplier * (isGold ? 1000 : 75); + { + auto * mine = dynamic_cast(target); + return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); + } case Obj::MYSTICAL_GARDEN: case Obj::WINDMILL: return 100; @@ -1005,7 +1016,7 @@ public: uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const { - if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id)) + if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id)) return 0; auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); diff --git a/AI/VCAI/AIUtility.h b/AI/VCAI/AIUtility.h index 64cf46d28..2b0fe415e 100644 --- a/AI/VCAI/AIUtility.h +++ b/AI/VCAI/AIUtility.h @@ -26,7 +26,6 @@ using crint3 = const int3 &; using crstring = const std::string &; using dwellingContent = std::pair>; -const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; const int ACTUAL_RESOURCE_COUNT = 7; const int ALLOWED_ROAMING_HEROES = 8; diff --git a/AI/VCAI/FuzzyEngines.cpp b/AI/VCAI/FuzzyEngines.cpp index 91ba40139..f4702f3a7 100644 --- a/AI/VCAI/FuzzyEngines.cpp +++ b/AI/VCAI/FuzzyEngines.cpp @@ -406,7 +406,7 @@ float VisitObjEngine::evaluate(Goals::VisitObj & goal) else { MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0); - logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue)); + logGlobal->error("AI met object type it doesn't know - ID: %d, subID: %d - adding to database with value %d ", obj->ID, obj->subID, objValue); } setSharedFuzzyVariables(goal); diff --git a/AI/VCAI/FuzzyHelper.cpp b/AI/VCAI/FuzzyHelper.cpp index c125e2315..a47cbafa0 100644 --- a/AI/VCAI/FuzzyHelper.cpp +++ b/AI/VCAI/FuzzyHelper.cpp @@ -66,7 +66,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank) { //this one is not fuzzy anymore, just calculate weighted average - auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance); + auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance); CBankInfo * bankInfo = dynamic_cast(objectInfo.get()); @@ -324,15 +324,8 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai) case Obj::DRAGON_UTOPIA: case Obj::SHIPWRECK: //shipwreck case Obj::DERELICT_SHIP: //derelict ship - // case Obj::PYRAMID: - return estimateBankDanger(dynamic_cast(obj)); case Obj::PYRAMID: - { - if(obj->subID == 0) - return estimateBankDanger(dynamic_cast(obj)); - else - return 0; - } + return estimateBankDanger(dynamic_cast(obj)); default: return 0; } diff --git a/AI/VCAI/Goals/CollectRes.cpp b/AI/VCAI/Goals/CollectRes.cpp index 27e168fa6..d4513dbeb 100644 --- a/AI/VCAI/Goals/CollectRes.cpp +++ b/AI/VCAI/Goals/CollectRes.cpp @@ -43,10 +43,10 @@ TGoalVec CollectRes::getAllPossibleSubgoals() return resID == GameResID(EGameResID::GOLD); break; case Obj::RESOURCE: - return obj->subID == resID; + return dynamic_cast(obj)->resourceID() == GameResID(resID); break; case Obj::MINE: - return (obj->subID == resID && + return (dynamic_cast(obj)->producedResource == GameResID(resID) && (cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines break; case Obj::CAMPFIRE: diff --git a/AI/VCAI/Goals/GatherTroops.cpp b/AI/VCAI/Goals/GatherTroops.cpp index 4af9e7434..ed4e1a6c0 100644 --- a/AI/VCAI/Goals/GatherTroops.cpp +++ b/AI/VCAI/Goals/GatherTroops.cpp @@ -88,7 +88,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals() } auto creature = VLC->creatures()->getByIndex(objid); - if(t->subID == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O + if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O { auto tryFindCreature = [&]() -> std::optional> { diff --git a/AI/VCAI/Pathfinding/PathfindingManager.cpp b/AI/VCAI/Pathfinding/PathfindingManager.cpp index 3b56951ed..c983150bc 100644 --- a/AI/VCAI/Pathfinding/PathfindingManager.cpp +++ b/AI/VCAI/Pathfinding/PathfindingManager.cpp @@ -190,7 +190,7 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet if(isBlockedBorderGate(firstTileToGet)) { //FIXME: this way we'll not visit gate and activate quest :? - return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID)); + return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->getObjTypeIndex())); } auto topObj = cb->getTopObj(firstTileToGet); diff --git a/AI/VCAI/ResourceManager.cpp b/AI/VCAI/ResourceManager.cpp index 8a97e430f..b69c4e9bc 100644 --- a/AI/VCAI/ResourceManager.cpp +++ b/AI/VCAI/ResourceManager.cpp @@ -59,19 +59,7 @@ TResources ResourceManager::estimateIncome() const if (obj->ID == Obj::MINE) { auto mine = dynamic_cast(obj); - switch (mine->producedResource.toEnum()) - { - case EGameResID::WOOD: - case EGameResID::ORE: - ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION; - break; - case EGameResID::GOLD: - ret[EGameResID::GOLD] += GOLD_MINE_PRODUCTION; - break; - default: - ret[obj->subID] += RESOURCE_MINE_PRODUCTION; - break; - } + ret += mine->dailyIncome(); } } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index ca3e5055e..19313633b 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -1760,11 +1760,11 @@ void VCAI::addVisitableObj(const CGObjectInstance * obj) CGTeleport::addToChannel(knownTeleportChannels, teleportObj); } -const CGObjectInstance * VCAI::lookForArt(int aid) const +const CGObjectInstance * VCAI::lookForArt(ArtifactID aid) const { for(const CGObjectInstance * obj : ai->visitableObjs) { - if(obj->ID == Obj::ARTIFACT && obj->subID == aid) + if(obj->ID == Obj::ARTIFACT && dynamic_cast(obj)->getArtifact() == aid) return obj; } diff --git a/AI/VCAI/VCAI.h b/AI/VCAI/VCAI.h index 8231003d9..5f327e5c3 100644 --- a/AI/VCAI/VCAI.h +++ b/AI/VCAI/VCAI.h @@ -251,7 +251,7 @@ public: void retrieveVisitableObjs(); virtual std::vector getFlaggedObjects() const; - const CGObjectInstance * lookForArt(int aid) const; + const CGObjectInstance * lookForArt(ArtifactID aid) const; bool isAccessible(const int3 & pos) const; HeroPtr getHeroWithGrail() const; diff --git a/CCallback.cpp b/CCallback.cpp index 753c443f0..78c8fe884 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -265,7 +265,7 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn assert(townOrTavern); assert(hero); - HireHero pack(HeroTypeID(hero->subID), townOrTavern->id); + HireHero pack(hero->getHeroType(), townOrTavern->id); pack.player = *player; sendRequest(&pack); } diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 5755b34ab..0a7861a3f 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -604,7 +604,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack) void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack) { CGHeroInstance *h = gs.map->heroesOnMap.back(); - if(h->subID != pack.hid) + if(h->getHeroType() != pack.hid) { logNetwork->error("Something wrong with hero recruited!"); } diff --git a/client/mapView/MapRendererContext.cpp b/client/mapView/MapRendererContext.cpp index d3c896029..07b5575eb 100644 --- a/client/mapView/MapRendererContext.cpp +++ b/client/mapView/MapRendererContext.cpp @@ -427,11 +427,7 @@ size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates) if(!object->visitableAt(coordinates.x, coordinates.y)) continue; - ObjectPosInfo info; - info.pos = coordinates; - info.id = object->ID; - info.subId = object->subID; - info.owner = object->tempOwner; + ObjectPosInfo info(object); size_t iconIndex = selectOverlayImageForObject(info); diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index a0bc94cc8..8751dcf1e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -920,7 +920,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { - std::vector> comps(1, std::make_shared(CComponent::building,town->subID,building)); + std::vector> comps(1, std::make_shared(CComponent::building,town->getFaction(),building)); std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); std::string hasNotProduced; std::string hasProduced; @@ -969,9 +969,9 @@ void CCastleBuildings::enterMagesGuild() { const StartInfo *si = LOCPLINT->cb->getStartInfo(); // it would be nice to find a way to move this hack to config/mapOverrides.json - if(si && si->campState && // We're in campaign, + if(si && si->campState && // We're in campaign, (si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian", - (hero->subID == 45)) // and the hero is Yog (based on Solmyr) + (hero->getHeroType() == 45)) // and the hero is Yog (based on Solmyr) { // "Yog has given up magic in all its forms..." LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]); diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index ef65e7912..2d9da6c4c 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -667,10 +667,10 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta title = (*CGI->townh)[ETownType::STRONGHOLD]->town->buildings[BuildingID::FREELANCERS_GUILD]->getNameTranslated(); break; case EMarketMode::RESOURCE_ARTIFACT: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); break; case EMarketMode::ARTIFACT_RESOURCE: - title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); + title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated(); // create image that copies part of background containing slot MISC_1 into position of slot MISC_5 // this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 003f1989c..760a036df 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -580,7 +580,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy CGHeroInstance * hero = dynamic_cast(obj); hero->ID = Obj::HERO; - hero->subID = heroTypeId; + hero->setHeroType(heroTypeId); hero->tempOwner = playerColor; hero->pos = townPos; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 597bdd7eb..79ee3b3c1 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -256,7 +256,7 @@ void CGameStateCampaign::placeCampaignHeroes() assert(0); // should not happen } - hero->subID = heroTypeId; + hero->setHeroType(heroTypeId); gameState->map->getEditManager()->insertObject(hero); } } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ba916b3bb..1143b557b 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -290,6 +290,12 @@ HeroTypeID CGHeroInstance::getHeroType() const return HeroTypeID(getObjTypeIndex().getNum()); } +void CGHeroInstance::setHeroType(HeroTypeID heroType) +{ + assert(type == nullptr); + subID = heroType; +} + void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID) { subID = SUBID.getNum(); diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 0c141d1e0..a07ccff9c 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -232,6 +232,7 @@ public: ////////////////////////////////////////////////////////////////////////// HeroTypeID getHeroType() const; + void setHeroType(HeroTypeID type); void initHero(CRandomGenerator & rand); void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 78a5079ee..19193d39f 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -129,6 +129,14 @@ bool CGMine::isAbandoned() const return (getObjTypeIndex() >= 7); } +ResourceSet CGMine::dailyIncome() const +{ + ResourceSet result; + result[producedResource] += defaultResProduction(); + + return result; +} + std::string CGMine::getObjectName() const { return VLC->generaltexth->translate("core.minename", getObjTypeIndex()); @@ -466,7 +474,7 @@ TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, int Su if(!obj) continue; - const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); + const auto * teleportObj = dynamic_cast(cb->getObj(obj->id)); if(teleportObj && vstd::contains(IDs, teleportObj->ID) && teleportObj->subID == SubID) return teleportObj->channel; } diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 3d0e4264f..5afd6f603 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -142,6 +142,7 @@ public: std::set abandonedMineResources; bool isAbandoned() const; + ResourceSet dailyIncome() const; private: void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 06abea28c..c5ddcc26c 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -1263,7 +1263,7 @@ void CMapLoaderJson::readObjects() std::sort(map->heroesOnMap.begin(), map->heroesOnMap.end(), [](const ConstTransitivePtr & a, const ConstTransitivePtr & b) { - return a->subID < b->subID; + return a->getObjTypeIndex() < b->getObjTypeIndex(); }); } diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 3e7dde286..147e99f81 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1487,6 +1487,7 @@ void NewObject::applyGs(CGameState *gs) CGObjectInstance * o = handler->create(); handler->configureObject(o, gs->getRandomGenerator()); + assert(o->ID == this->ID); if (ID == Obj::MONSTER) //probably more options will be needed { @@ -1514,8 +1515,6 @@ void NewObject::applyGs(CGameState *gs) o->appearance = handler->getTemplates().front(); o->id = ObjectInstanceID(static_cast(gs->map->objects.size())); - o->ID = ID; - o->subID = subID; o->pos = targetPos + o->getVisitableOffset(); gs->map->objects.emplace_back(o); diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index ee237bfe5..3887cb774 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -260,15 +260,18 @@ std::vector CPathfinderHelper::getTeleportExits(const PathNodeInfo & sourc teleportationExits.push_back(exit); } } - else if(options.useCastleGate - && (source.nodeObject->ID == Obj::TOWN && source.nodeObject->subID == ETownType::INFERNO - && source.objectRelations != PlayerRelations::ENEMIES)) + else if(options.useCastleGate && source.nodeObject->ID == Obj::TOWN && source.objectRelations != PlayerRelations::ENEMIES) { - /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo - /// This may be handy if we allow to use teleportation to friendly towns - for(const auto & exit : getCastleGates(source)) + auto * town = dynamic_cast(source.nodeObject); + assert(town); + if (town && town->getFaction() == FactionID::INFERNO) { - teleportationExits.push_back(exit); + /// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo + /// This may be handy if we allow to use teleportation to friendly towns + for(const auto & exit : getCastleGates(source)) + { + teleportationExits.push_back(exit); + } } } diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index a2f26c03d..a54d54fa3 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -115,7 +115,7 @@ void Object::Instance::setAnyTemplate(CRandomGenerator & rng) { auto templates = dObject.getObjectHandler()->getTemplates(); if(templates.empty()) - throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.subID)); + throw rmgException(boost::str(boost::format("Did not find any graphics for object (%d,%d)") % dObject.ID % dObject.getObjTypeIndex())); dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); dAccessibleAreaCache.clear(); @@ -128,7 +128,7 @@ void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng) if (templates.empty()) { auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated(); - throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.subID % terrainName)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.getObjTypeIndex() % terrainName)); } dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng); @@ -338,7 +338,7 @@ void Object::Instance::finalize(RmgMap & map, CRandomGenerator & rng) auto templates = dObject.getObjectHandler()->getTemplates(terrainType->getId()); if (templates.empty()) { - throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.subID % getPosition(true).toString() % terrainType)); + throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s (terrain %d)") % dObject.ID % dObject.getObjTypeIndex() % getPosition(true).toString() % terrainType)); } else { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 20dc751b2..ed3f42bb9 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -111,7 +111,7 @@ void TreasurePlacer::addAllPossibleObjects() auto factory = VLC->objtypeh->getHandlerFor(Obj::PRISON, 0); auto* obj = dynamic_cast(factory->create()); - obj->subID = hid; //will be initialized later + obj->setHeroType(hid); //will be initialized later obj->exp = generator.getConfig().prisonExperience[i]; obj->setOwner(PlayerColor::NEUTRAL); generator.banHero(hid); diff --git a/lib/spells/ViewSpellInt.cpp b/lib/spells/ViewSpellInt.cpp index 2b8e36ec7..1507d761a 100644 --- a/lib/spells/ViewSpellInt.cpp +++ b/lib/spells/ViewSpellInt.cpp @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN ObjectPosInfo::ObjectPosInfo(const CGObjectInstance * obj): - pos(obj->visitablePos()), id(obj->ID), subId(obj->subID), owner(obj->tempOwner) + pos(obj->visitablePos()), id(obj->ID), subId(obj->getObjTypeIndex()), owner(obj->tempOwner) { } diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 4912541de..0ba761307 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -217,7 +217,7 @@ void MapController::repairMap(CMap * map) const art->storedArtifact = a; } else - map->allowedArtifact.at(art->subID) = true; + map->allowedArtifact.at(art->getArtifact()) = true; } } } @@ -623,7 +623,7 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) if(obj->ID == Obj::HERO) continue; //stub! - auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); + auto handler = obj->getObjectHandler(); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 8bda133e7..c702f35ee 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2315,7 +2315,7 @@ bool CGameHandler::buildStructure(ObjectInstanceID tid, BuildingID requestedID, auto isLibrary = isMageGuild ? false : t->town->buildings.at(buildingID)->subId == BuildingSubID::EBuildingSubID::LIBRARY; - if(isMageGuild || isLibrary || (t->subID == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) + if(isMageGuild || isLibrary || (t->getFaction() == ETownType::CONFLUX && buildingID == BuildingID::GRAIL)) { if(t->visitingHero) giveSpells(t,t->visitingHero); @@ -3284,7 +3284,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->hasBuilt(i)) { buildStructure(town->id, i, true); - iw.components.emplace_back(Component::EComponentType::BUILDING, town->subID, i, 0); + iw.components.emplace_back(Component::EComponentType::BUILDING, town->getFaction(), i, 0); } } @@ -3430,7 +3430,7 @@ void CGameHandler::objectVisited(const CGObjectInstance * obj, const CGHeroInsta { using events::ObjectVisitStarted; - logGlobal->debug("%s visits %s (%d:%d)", h->nodeName(), obj->getObjectName(), obj->ID, obj->subID); + logGlobal->debug("%s visits %s (%d)", h->nodeName(), obj->getObjectName(), obj->ID); if (getVisitingHero(obj) != nullptr) { diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index f37f09f8a..eeb2ed73d 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -50,8 +50,8 @@ TavernHeroSlot HeroPoolProcessor::selectSlotForRole(const PlayerColor & player, // try to find "better" slot to overwrite // we want to avoid overwriting retreated heroes when tavern still has slot with random hero // as well as avoid overwriting surrendered heroes if we can overwrite retreated hero - auto roleLeft = heroesPool->getSlotRole(HeroTypeID(heroes[0]->subID)); - auto roleRight = heroesPool->getSlotRole(HeroTypeID(heroes[1]->subID)); + auto roleLeft = heroesPool->getSlotRole(heroes[0]->getHeroType()); + auto roleRight = heroesPool->getSlotRole(heroes[1]->getHeroType()); if (roleLeft > roleRight) return TavernHeroSlot::RANDOM; @@ -73,7 +73,7 @@ void HeroPoolProcessor::onHeroSurrendered(const PlayerColor & color, const CGHer sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid.setNum(hero->subID); + sah.hid = hero->getHeroType(); gameHandler->sendAndApply(&sah); } @@ -84,7 +84,7 @@ void HeroPoolProcessor::onHeroEscaped(const PlayerColor & color, const CGHeroIns sah.slotID = selectSlotForRole(color, sah.roleID); sah.player = color; - sah.hid.setNum(hero->subID); + sah.hid = hero->getHeroType(); sah.army.clearSlots(); sah.army.setCreature(SlotID(0), hero->type->initialArmy.at(0).creature, 1); @@ -111,7 +111,7 @@ void HeroPoolProcessor::selectNewHeroForSlot(const PlayerColor & color, TavernHe if (newHero) { - sah.hid.setNum(newHero->subID); + sah.hid = newHero->getHeroType(); if (giveArmy) { @@ -193,7 +193,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy for(const auto & hero : recruitableHeroes) { - if(hero->subID == heroToRecruit) + if(hero->getHeroType() == heroToRecruit) recruitedHero = hero; } @@ -206,7 +206,7 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy HeroRecruited hr; hr.tid = mapObject->id; - hr.hid.setNum(recruitedHero->subID); + hr.hid = recruitedHero->getHeroType(); hr.player = player; hr.tile = recruitedHero->convertFromVisitablePos(targetPos ); if(gameHandler->getTile(targetPos)->isWater() && !recruitedHero->boat) From 71d6c4e953e6714f1df34d0b0cbcead28e42c935 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 1 Nov 2023 16:57:17 +0200 Subject: [PATCH 1027/1248] Correctly initialize current value of allied AI in Launcher --- launcher/settingsView/csettingsview_moc.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index 5ac6648c2..e0c6bb635 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -90,7 +90,9 @@ void CSettingsView::loadSettings() ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String())); ui->comboBoxNeutralAI->setCurrentText(QString::fromStdString(settings["server"]["neutralAI"].String())); ui->comboBoxEnemyAI->setCurrentText(QString::fromStdString(settings["server"]["enemyAI"].String())); + ui->comboBoxEnemyPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["playerAI"].String())); + ui->comboBoxAlliedPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["alliedAI"].String())); ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Integer()); From e57276b7c55c4dd2005ece722e2a0da920382088 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 1 Nov 2023 17:24:41 +0200 Subject: [PATCH 1028/1248] Correctly initialize battleID for teleport action --- lib/spells/effects/Teleport.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/spells/effects/Teleport.cpp b/lib/spells/effects/Teleport.cpp index bffffa6fd..4d29e9483 100644 --- a/lib/spells/effects/Teleport.cpp +++ b/lib/spells/effects/Teleport.cpp @@ -12,6 +12,7 @@ #include "Teleport.h" #include "Registry.h" #include "../ISpellMechanics.h" +#include "../../battle/IBattleState.h" #include "../../battle/CBattleInfoCallback.h" #include "../../battle/Unit.h" #include "../../networkPacks/PacksForClientBattle.h" @@ -76,6 +77,7 @@ void Teleport::apply(ServerCallback * server, const Mechanics * m, const EffectT const auto destination = target[1].hexValue; BattleStackMoved pack; + pack.battleID = m->battle()->getBattle()->getBattleID(); pack.distance = 0; pack.stack = targetUnit->unitId(); std::vector tiles; From b42f073f0c233da57fdc4fb8eef6b01880b1389c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 1 Nov 2023 16:10:33 +0200 Subject: [PATCH 1029/1248] Stabilization --- lib/gameState/CGameState.cpp | 17 ++++------------- lib/mapObjects/CGDwelling.cpp | 4 +++- lib/mapObjects/CGHeroInstance.cpp | 4 ++-- lib/mapObjects/CGTownInstance.cpp | 6 ++---- lib/mapping/MapFormatH3M.cpp | 6 +++--- mapeditor/inspector/inspector.cpp | 7 +++++-- 6 files changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 760a036df..4fd640557 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -526,15 +526,6 @@ void CGameState::randomizeMapObjects() object->pickRandomObject(getRandomGenerator()); - auto * hero = dynamic_cast(object); - auto * town = dynamic_cast(object); - - if (hero && hero->ID != Obj::PRISON) - map->heroesOnMap.emplace_back(hero); - - if (town) - map->towns.emplace_back(town); - //handle Favouring Winds - mark tiles under it if(object->ID == Obj::FAVORABLE_WINDS) { @@ -1825,10 +1816,10 @@ void CGameState::buildBonusSystemTree() buildGlobalTeamPlayerTree(); attachArmedObjects(); -// for(CGTownInstance *t : map->towns) -// { -// t->deserializationFix(); -// } + for(CGTownInstance *t : map->towns) + { + t->deserializationFix(); + } // CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact // are provided on initializing / deserializing diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index ff8e73886..b47429e1e 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -128,7 +128,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) FactionID faction = randomizeFaction(rand); int level = randomizeLevel(rand); assert(faction != FactionID::NONE && faction != FactionID::NEUTRAL); - assert(level >= 1 && level <= 7); + assert(level >= 0 && level <= 6); randomizationInfo.reset(); CreatureID cid = (*VLC->townh)[faction]->town->creatures[level][0]; @@ -163,6 +163,8 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) ID = Obj::CREATURE_GENERATOR4; subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand); } + + setType(ID, subID); } } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 1143b557b..870e10c8f 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1547,14 +1547,14 @@ void CGHeroInstance::afterAddToMap(CMap * map) } } - if(ID == Obj::HERO) + if(ID != Obj::PRISON) { map->heroesOnMap.emplace_back(this); } } void CGHeroInstance::afterRemoveFromMap(CMap* map) { - if (ID == Obj::HERO) + if (ID == Obj::PRISON) vstd::erase_if_present(map->heroesOnMap, this); } diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index b16d548f7..f0243e86a 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1100,14 +1100,12 @@ void CGTownInstance::onTownCaptured(const PlayerColor & winner) const void CGTownInstance::afterAddToMap(CMap * map) { - if(ID == Obj::TOWN) - map->towns.emplace_back(this); + map->towns.emplace_back(this); } void CGTownInstance::afterRemoveFromMap(CMap * map) { - if (ID == Obj::TOWN) - vstd::erase_if_present(map->towns, this); + vstd::erase_if_present(map->towns, this); } void CGTownInstance::reset() diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 6bb12268b..362205a89 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1326,14 +1326,14 @@ CGObjectInstance * CMapLoaderH3M::readDwellingRandom(const int3 & mapPosition, s object->randomizationInfo = CGDwellingRandomizationInfo(); - bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION; - bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL; + bool hasFactionInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_LVL; + bool hasLevelInfo = objectTemplate->id == Obj::RANDOM_DWELLING || objectTemplate->id == Obj::RANDOM_DWELLING_FACTION; if (hasFactionInfo) { object->randomizationInfo->identifier = reader->readUInt32(); - if(object->randomizationInfo->identifier != 0) + if(object->randomizationInfo->identifier == 0) reader->readBitmaskFactions(object->randomizationInfo->allowedFactions, false); } else diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 459a7daa4..01839d01c 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -235,8 +235,11 @@ void Inspector::updateProperties(CGDwelling * o) addProperty("Owner", o->tempOwner, false); - auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); - addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); + if (o->ID == Obj::RANDOM_DWELLING || o->ID == Obj::RANDOM_DWELLING_LVL) + { + auto * delegate = new PickObjectDelegate(controller, PickObjectDelegate::typedFilter); + addProperty("Same as town", PropertyEditorPlaceholder(), delegate, false); + } } void Inspector::updateProperties(CGLighthouse * o) From 10e50548e7e2b4363a830eec7593a1842048c68a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 31 Oct 2023 11:09:56 +0200 Subject: [PATCH 1030/1248] Converted Component class to use VariantIdentifier instead of int --- AI/Nullkiller/AIGateway.cpp | 2 +- client/CPlayerInterface.cpp | 8 +- client/adventureMap/CInfoBar.cpp | 30 ++- client/lobby/OptionsTab.cpp | 2 +- client/widgets/CArtifactHolder.cpp | 13 +- client/widgets/CComponent.cpp | 308 +++++++++++++---------- client/widgets/CComponent.h | 23 +- client/widgets/CWindowWithArtifacts.cpp | 2 +- client/widgets/MiscWidgets.cpp | 44 ++-- client/widgets/MiscWidgets.h | 10 +- client/windows/CCastleInterface.cpp | 35 +-- client/windows/CCreatureWindow.cpp | 10 +- client/windows/CHeroWindow.cpp | 19 +- client/windows/CKingdomInterface.cpp | 4 +- client/windows/CSpellWindow.cpp | 4 +- client/windows/GUIClasses.cpp | 17 +- client/windows/GUIClasses.h | 2 +- lib/CCreatureSet.cpp | 5 + lib/CCreatureSet.h | 1 + lib/constants/EntityIdentifiers.cpp | 15 ++ lib/constants/EntityIdentifiers.h | 4 +- lib/mapObjects/CBank.cpp | 24 +- lib/mapObjects/CGCreature.cpp | 8 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/CGTownBuilding.cpp | 10 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/MiscObjects.cpp | 10 +- lib/networkPacks/Component.h | 79 +++--- lib/networkPacks/NetPacksLib.cpp | 7 - lib/rewardable/Info.cpp | 2 +- lib/rewardable/Limiter.cpp | 26 +- lib/rewardable/Reward.cpp | 22 +- lib/spells/AdventureSpellMechanics.cpp | 3 +- server/CGameHandler.cpp | 31 ++- server/battles/BattleResultProcessor.cpp | 10 +- 36 files changed, 435 insertions(+), 363 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 9dde74d18..a4613db1b 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -664,7 +664,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vectorheroManager->getHeroRole(hero) != HeroRole::MAIN || nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE)) { diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 491380f3b..409b0f30e 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -293,7 +293,7 @@ void CPlayerInterface::yourTurn(QueryID queryID) std::string msg = CGI->generaltexth->allTexts[13]; boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); showInfoDialog(msg, cmp); } else @@ -326,7 +326,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID) auto playerColor = *cb->getPlayerID(); std::vector components; - components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0); + components.emplace_back(ComponentType::FLAG, playerColor); MetaString text; const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle; @@ -1228,7 +1228,7 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated()); // Picture of assembled artifact at bottom. - auto sc = std::make_shared(CComponent::artifact, assembledArtifact->getIndex(), 0); + auto sc = std::make_shared(ComponentType::ARTIFACT, assembledArtifact->getId()); scs.push_back(sc); } else @@ -1441,7 +1441,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start) std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked"); boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name); std::vector> cmp; - cmp.push_back(std::make_shared(CComponent::flag, playerID.getNum(), 0)); + cmp.push_back(std::make_shared(ComponentType::FLAG, playerID)); makingTurn = true; //workaround for stiff showInfoDialog implementation showInfoDialog(msg, cmp); makingTurn = false; diff --git a/client/adventureMap/CInfoBar.cpp b/client/adventureMap/CInfoBar.cpp index 360dd4946..c583f461f 100644 --- a/client/adventureMap/CInfoBar.cpp +++ b/client/adventureMap/CInfoBar.cpp @@ -376,47 +376,51 @@ void CInfoBar::pushComponents(const std::vector & components, std::st std::array, int>, 10> reward_map; for(const auto & c : components) { - switch(c.id) + switch(c.type) { - case Component::EComponentType::PRIM_SKILL: - case Component::EComponentType::EXPERIENCE: + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + case ComponentType::MANA: reward_map.at(0).first.push_back(c); reward_map.at(0).second = 8; //At most 8, cannot be more break; - case Component::EComponentType::SEC_SKILL: + case ComponentType::SEC_SKILL: reward_map.at(1).first.push_back(c); reward_map.at(1).second = 4; //At most 4 break; - case Component::EComponentType::SPELL: + case ComponentType::SPELL: reward_map.at(2).first.push_back(c); reward_map.at(2).second = 4; //At most 4 break; - case Component::EComponentType::ARTIFACT: + case ComponentType::ARTIFACT: + case ComponentType::SPELL_SCROLL: reward_map.at(3).first.push_back(c); reward_map.at(3).second = 4; //At most 4, too long names break; - case Component::EComponentType::CREATURE: + case ComponentType::CREATURE: reward_map.at(4).first.push_back(c); reward_map.at(4).second = 4; //At most 4, too long names break; - case Component::EComponentType::RESOURCE: + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: reward_map.at(5).first.push_back(c); reward_map.at(5).second = 7; //At most 7 break; - case Component::EComponentType::MORALE: - case Component::EComponentType::LUCK: + case ComponentType::MORALE: + case ComponentType::LUCK: reward_map.at(6).first.push_back(c); reward_map.at(6).second = 2; //At most 2 - 1 for morale + 1 for luck break; - case Component::EComponentType::BUILDING: + case ComponentType::BUILDING: reward_map.at(7).first.push_back(c); reward_map.at(7).second = 1; //At most 1 - only large icons available AFAIK break; - case Component::EComponentType::HERO_PORTRAIT: + case ComponentType::HERO_PORTRAIT: reward_map.at(8).first.push_back(c); reward_map.at(8).second = 1; //I do not think than we even can get more than 1 hero break; - case Component::EComponentType::FLAG: + case ComponentType::FLAG: reward_map.at(9).first.push_back(c); reward_map.at(9).second = 1; //I do not think than we even can get more than 1 player in notification break; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 72e3817f3..e62bf26c4 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -571,7 +571,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() for(auto & elem : town->creatures) { if(!elem.empty()) - components.push_back(std::make_shared(CComponent::creature, elem.front(), 0, CComponent::tiny)); + components.push_back(std::make_shared(ComponentType::CREATURE, elem.front(), 0, CComponent::tiny)); } boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140)); } diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index bbe3e07be..c4440aaef 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -49,7 +49,7 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) if(settings["general"]["enableUiEnhancements"].Bool()) { imageIndex = spellID.num; - if(baseType != CComponent::spell) + if(component.type != ComponentType::SPELL_SCROLL) { image->setScale(Point(pos.w, 34)); image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex); @@ -57,21 +57,20 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) } } // Add spell component info (used to provide a pic in r-click popup) - baseType = CComponent::spell; - type = spellID; + component.type = ComponentType::SPELL_SCROLL; + component.subType = spellID; } else { - if(settings["general"]["enableUiEnhancements"].Bool() && baseType != CComponent::artifact) + if(settings["general"]["enableUiEnhancements"].Bool() && component.type != ComponentType::ARTIFACT) { image->setScale(Point()); image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex); image->moveTo(Point(pos.x, pos.y)); } - baseType = CComponent::artifact; - type = artInst->getTypeId(); + component.type = ComponentType::ARTIFACT; + component.subType = artInst->getTypeId(); } - bonusValue = 0; image->enable(); text = artInst->getDescription(); } diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 12844ceb3..2a77b46da 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -39,41 +39,35 @@ #include "../../lib/CArtHandler.h" #include "../../lib/CArtifactInstance.h" -CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font): - perDay(false) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font) { init(Type, Subtype, Val, imageSize, font, ""); } -CComponent::CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize, EFonts font): - perDay(false) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::string Val, ESize imageSize, EFonts font) { - init(Type, Subtype, 0, imageSize, font, Val); + init(Type, Subtype, std::nullopt, imageSize, font, Val); } CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) - : perDay(false) { - if(c.id == Component::EComponentType::RESOURCE && c.when==-1) - perDay = true; - - init((Etype)c.id, c.subtype, c.val, imageSize, font); + init(c.type, c.subType, c.value, imageSize, font); } -void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt, std::string ValText) +void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, std::string ValText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); addUsedEvents(SHOW_POPUP); - compType = Type; - subtype = Subtype; - val = Val; - valText = ValText; + data.type = Type; + data.subType = Subtype; + data.value = Val; + + customSubtitle = ValText; size = imageSize; font = fnt; - assert(compType < typeInvalid); assert(size < sizeInvalid); setSurface(getFileName()[size], (int)getIndex()); @@ -94,7 +88,7 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts if (size < small) max = 30; - if(Type == Etype::resource && !valText.empty()) + if(Type == ComponentType::RESOURCE && !ValText.empty()) max = 80; std::vector textLines = CMessage::breakText(getSubtitle(), std::max(max, pos.w), font); @@ -115,151 +109,207 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts std::vector CComponent::getFileName() { - static const std::string primSkillsArr [] = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; - static const std::string secSkillsArr [] = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; - static const std::string resourceArr [] = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; - static const std::string creatureArr [] = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; - static const std::string artifactArr[] = {"Artifact", "Artifact", "Artifact", "Artifact"}; - static const std::string spellsArr [] = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; - static const std::string moraleArr [] = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; - static const std::string luckArr [] = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; - static const std::string heroArr [] = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; - static const std::string flagArr [] = {"CREST58", "CREST58", "CREST58", "CREST58"}; + static const std::array primSkillsArr = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; + static const std::array secSkillsArr = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; + static const std::array resourceArr = {"SMALRES", "RESOURCE", "RESOURCE", "RESOUR82"}; + static const std::array creatureArr = {"CPRSMALL", "CPRSMALL", "CPRSMALL", "TWCRPORT"}; + static const std::array artifactArr = {"Artifact", "Artifact", "Artifact", "Artifact"}; + static const std::array spellsArr = {"SpellInt", "SpellInt", "SpellInt", "SPELLSCR"}; + static const std::array moraleArr = {"IMRL22", "IMRL30", "IMRL42", "imrl82"}; + static const std::array luckArr = {"ILCK22", "ILCK30", "ILCK42", "ilck82"}; + static const std::array heroArr = {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"}; + static const std::array flagArr = {"CREST58", "CREST58", "CREST58", "CREST58"}; - auto gen = [](const std::string * arr) -> std::vector + auto gen = [](const std::array & arr) -> std::vector { return { AnimationPath::builtin(arr[0]), AnimationPath::builtin(arr[1]), AnimationPath::builtin(arr[2]), AnimationPath::builtin(arr[3]) }; }; - switch(compType) + switch(data.type) { - case primskill: return gen(primSkillsArr); - case secskill: return gen(secSkillsArr); - case resource: return gen(resourceArr); - case creature: return gen(creatureArr); - case artifact: return gen(artifactArr); - case experience: return gen(primSkillsArr); - case spell: return gen(spellsArr); - case morale: return gen(moraleArr); - case luck: return gen(luckArr); - case building: return std::vector(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons); - case hero: return gen(heroArr); - case flag: return gen(flagArr); + case ComponentType::PRIM_SKILL: + case ComponentType::EXPERIENCE: + case ComponentType::MANA: + case ComponentType::LEVEL: + return gen(primSkillsArr); + case ComponentType::SEC_SKILL: + return gen(secSkillsArr); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return gen(resourceArr); + case ComponentType::CREATURE: + return gen(creatureArr); + case ComponentType::ARTIFACT: + return gen(artifactArr); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return gen(spellsArr); + case ComponentType::MORALE: + return gen(moraleArr); + case ComponentType::LUCK: + return gen(luckArr); + case ComponentType::BUILDING: + return std::vector(4, (*CGI->townh)[data.subType.as().getFaction()]->town->clientInfo.buildingsIcons); + case ComponentType::HERO_PORTRAIT: + return gen(heroArr); + case ComponentType::FLAG: + return gen(flagArr); + default: + assert(0); + return {}; } - assert(0); - return {}; } size_t CComponent::getIndex() { - switch(compType) + switch(data.type) { - case primskill: return subtype; - case secskill: return subtype*3 + 3 + val - 1; - case resource: return subtype; - case creature: return CGI->creatures()->getByIndex(subtype)->getIconIndex(); - case artifact: return CGI->artifacts()->getByIndex(subtype)->getIconIndex(); - case experience: return 4; - case spell: return (size < large) ? subtype + 1 : subtype; - case morale: return val+3; - case luck: return val+3; - case building: return val; - case hero: return CGI->heroTypes()->getByIndex(subtype)->getIconIndex(); - case flag: return subtype; + case ComponentType::PRIM_SKILL: + return data.subType.getNum(); + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return 4; // for whatever reason, in H3 experience icon is located in primary skills icons + case ComponentType::MANA: + return 5; // for whatever reason, in H3 mana points icon is located in primary skills icons + case ComponentType::SEC_SKILL: + return data.subType.getNum() * 3 + 3 + data.value.value_or(0) - 1; + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return data.subType.getNum(); + case ComponentType::CREATURE: + return CGI->creatures()->getById(data.subType.as())->getIconIndex(); + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getIconIndex(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return (size < large) ? data.subType.getNum() + 1 : data.subType.getNum(); + case ComponentType::MORALE: + return data.value.value_or(0) + 3; + case ComponentType::LUCK: + return data.value.value_or(0) + 3; + case ComponentType::BUILDING: + return data.subType.as().getBuilding(); + case ComponentType::HERO_PORTRAIT: + return CGI->heroTypes()->getById(data.subType.as())->getIconIndex(); + case ComponentType::FLAG: + return data.subType.getNum(); + default: + assert(0); + return 0; } - assert(0); - return 0; } std::string CComponent::getDescription() { - switch(compType) + switch(data.type) { - case primskill: return (subtype < 4)? CGI->generaltexth->arraytxt[2+subtype] //Primary skill - : CGI->generaltexth->allTexts[149]; //mana - case secskill: return CGI->skillh->getByIndex(subtype)->getDescriptionTranslated(val); - case resource: return CGI->generaltexth->allTexts[242]; - case creature: return ""; - case artifact: - { - auto artID = ArtifactID(subtype); - auto description = VLC->arth->objects[artID]->getDescriptionTranslated(); - if(artID == ArtifactID::SPELL_SCROLL) + case ComponentType::PRIM_SKILL: + return CGI->generaltexth->arraytxt[2+data.subType.getNum()]; + case ComponentType::EXPERIENCE: + case ComponentType::LEVEL: + return CGI->generaltexth->allTexts[241]; + case ComponentType::MANA: + return CGI->generaltexth->allTexts[149]; + case ComponentType::SEC_SKILL: + return CGI->skillh->getByIndex(data.subType.getNum())->getDescriptionTranslated(data.value.value_or(0)); + case ComponentType::RESOURCE: + case ComponentType::RESOURCE_PER_DAY: + return CGI->generaltexth->allTexts[242]; + case ComponentType::CREATURE: + return ""; + case ComponentType::ARTIFACT: + return VLC->artifacts()->getById(data.subType.as())->getDescriptionTranslated(); + case ComponentType::SPELL_SCROLL: { - ArtifactUtils::insertScrrollSpellName(description, SpellID(val)); + auto description = VLC->arth->objects[ArtifactID::SPELL_SCROLL]->getDescriptionTranslated(); + ArtifactUtils::insertScrrollSpellName(description, data.subType.as()); + return description; } - return description; + case ComponentType::SPELL: + return VLC->spells()->getById(data.subType.as())->getDescriptionTranslated(data.value.value_or(0)); + case ComponentType::MORALE: + return CGI->generaltexth->heroscrn[ 4 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; + case ComponentType::LUCK: + return CGI->generaltexth->heroscrn[ 7 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)]; + case ComponentType::BUILDING: + { + auto index = data.subType.as(); + return (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]->getDescriptionTranslated(); + } + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return ""; + default: + assert(0); + return 0; } - case experience: return CGI->generaltexth->allTexts[241]; - case spell: return (*CGI->spellh)[subtype]->getDescriptionTranslated(val); - case morale: return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)]; - case luck: return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)]; - case building: return (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]->getDescriptionTranslated(); - case hero: return ""; - case flag: return ""; - } - assert(0); - return ""; } std::string CComponent::getSubtitle() { - if(!perDay) - return getSubtitleInternal(); + if (!customSubtitle.empty()) + return customSubtitle; - std::string ret = CGI->generaltexth->allTexts[3]; - boost::replace_first(ret, "%d", getSubtitleInternal()); - return ret; -} - -std::string CComponent::getSubtitleInternal() -{ - //FIXME: some of these are horrible (e.g creature) - switch(compType) + switch(data.type) { - case primskill: return boost::str(boost::format("%+d %s") % val % (subtype < 4 ? CGI->generaltexth->primarySkillNames[subtype] : CGI->generaltexth->allTexts[387])); - case secskill: return CGI->generaltexth->levels[val-1] + "\n" + CGI->skillh->getByIndex(subtype)->getNameTranslated(); - case resource: return valText.empty() ? std::to_string(val) : valText; - case creature: - { - auto creature = CGI->creh->getByIndex(subtype); - if ( val ) - return std::to_string(val) + " " + (val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); + case ComponentType::PRIM_SKILL: + if (data.value) + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->primarySkillNames[data.subType.getNum()]); else - return val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated(); - } - case artifact: return CGI->artifacts()->getByIndex(subtype)->getNameTranslated(); - case experience: + return CGI->generaltexth->primarySkillNames[data.subType.getNum()]; + case ComponentType::EXPERIENCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::LEVEL: { - if(subtype == 1) //+1 level - tree of knowledge - { - std::string level = CGI->generaltexth->allTexts[442]; - boost::replace_first(level, "1", std::to_string(val)); - return level; - } + std::string level = CGI->generaltexth->allTexts[442]; + boost::replace_first(level, "1", std::to_string(data.value.value_or(0))); + return level; + } + case ComponentType::MANA: + return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->allTexts[387]); + case ComponentType::SEC_SKILL: + return CGI->generaltexth->levels[data.value.value_or(0)-1] + "\n" + CGI->skillh->getById(data.subType.as())->getNameTranslated(); + case ComponentType::RESOURCE: + return std::to_string(data.value.value_or(0)); + case ComponentType::RESOURCE_PER_DAY: + return boost::str(boost::format(CGI->generaltexth->allTexts[387]) % data.value.value_or(0)); + case ComponentType::CREATURE: + { + auto creature = CGI->creh->getById(data.subType.as()); + if ( data.value.value_or(0) ) + return std::to_string(data.value.value_or(0)) + " " + (data.value.value_or(0) > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); else - { - return std::to_string(val); //amount of experience OR level required for seer hut; - } + return data.value.value_or(0) > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated(); } - case spell: return CGI->spells()->getByIndex(subtype)->getNameTranslated(); - case morale: return ""; - case luck: return ""; - case building: - { - auto building = (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]; - if(!building) + case ComponentType::ARTIFACT: + return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::SPELL_SCROLL: + case ComponentType::SPELL: + return CGI->spells()->getById(data.subType.as())->getNameTranslated(); + case ComponentType::MORALE: + return ""; + case ComponentType::LUCK: + return ""; + case ComponentType::BUILDING: { - logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[subtype]->town->faction->getNameTranslated(), val); - return (boost::format("Missing building #%d") % val).str(); + auto index = data.subType.as(); + auto building = (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]; + if(!building) + { + logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[index.getFaction()]->town->faction->getNameTranslated(), index.getBuilding().getNum()); + return (boost::format("Missing building #%d") % index.getBuilding().getNum()).str(); + } + return building->getNameTranslated(); } - return building->getNameTranslated(); - } - case hero: return ""; - case flag: return CGI->generaltexth->capColors[subtype]; + case ComponentType::HERO_PORTRAIT: + return ""; + case ComponentType::FLAG: + return CGI->generaltexth->capColors[data.subType.as().getNum()]; + default: + assert(0); + return ""; } - logGlobal->error("Invalid CComponent type: %d", (int)compType); - return ""; } void CComponent::setSurface(const AnimationPath & defName, int imgPos) @@ -299,7 +349,7 @@ CSelectableComponent::CSelectableComponent(const Component &c, std::function OnSelect): +CSelectableComponent::CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize, std::function OnSelect): CComponent(Type,Sub,Val, imageSize),onSelect(OnSelect) { setRedrawParent(true); diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 3f94137ff..813ca5667 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -12,6 +12,7 @@ #include "../gui/CIntObject.h" #include "../render/EFont.h" #include "../../lib/filesystem/ResourcePath.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -26,11 +27,6 @@ class CLabel; class CComponent : public virtual CIntObject { public: - enum Etype - { - primskill, secskill, resource, creature, artifact, experience, spell, morale, luck, building, hero, flag, typeInvalid - }; - //NOTE: not all types have exact these sizes or have less than 4 of them. In such cases closest one will be used enum ESize { @@ -47,26 +43,21 @@ private: size_t getIndex(); std::vector getFileName(); void setSurface(const AnimationPath & defName, int imgPos); - std::string getSubtitleInternal(); - void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText=""); + void init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText=""); public: std::shared_ptr image; - - Etype compType; //component type + Component data; + std::string customSubtitle; ESize size; //component size. EFonts font; //Font size of label - int subtype; //type-dependant subtype. See getSomething methods for details - int val; // value \ strength \ amount of component. See getSomething methods for details - std::string valText; // value instead of amount; currently only for resource - bool perDay; // add "per day" text to subtitle std::string getDescription(); std::string getSubtitle(); - CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL); - CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val = std::nullopt, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL); CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL); void showPopupWindow(const Point & cursorPosition) override; //call-in @@ -86,7 +77,7 @@ public: void clickPressed(const Point & cursorPosition) override; //call-in void clickDouble(const Point & cursorPosition) override; //call-in - CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); + CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize=large, std::function OnSelect = nullptr); CSelectableComponent(const Component & c, std::function OnSelect = nullptr); }; diff --git a/client/widgets/CWindowWithArtifacts.cpp b/client/widgets/CWindowWithArtifacts.cpp index 2b6d4d18d..bf5ca043e 100644 --- a/client/widgets/CWindowWithArtifacts.cpp +++ b/client/widgets/CWindowWithArtifacts.cpp @@ -95,7 +95,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT) { // The Catapult must be equipped - std::vector> catapult(1, std::make_shared(CComponent::artifact, 3, 0)); + std::vector> catapult(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT))); LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult); return false; } diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 4d5eff1d5..0e8d8c353 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -95,16 +95,16 @@ void LRClickableAreaWTextComp::clickPressed(const Point & cursorPosition) LOCPLINT->showInfoDialog(text, comp); } -LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, int BaseType) - : LRClickableAreaWText(Pos), baseType(BaseType), bonusValue(-1) +LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, ComponentType BaseType) + : LRClickableAreaWText(Pos) { - type = -1; + component.type = BaseType; } std::shared_ptr LRClickableAreaWTextComp::createComponent() const { - if(baseType >= 0) - return std::make_shared(CComponent::Etype(baseType), type, bonusValue); + if(component.type != ComponentType::NONE) + return std::make_shared(component); else return std::shared_ptr(); } @@ -164,17 +164,11 @@ void CHeroArea::hover(bool on) void LRClickableAreaOpenTown::clickPressed(const Point & cursorPosition) { if(town) - { LOCPLINT->openTownWindow(town); - if ( type == 2 ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::VILLAGE_HALL); - else if ( type == 3 && town->fortLevel() ) - LOCPLINT->castleInt->builds->buildingClicked(BuildingID::FORT); - } } LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town) - : LRClickableAreaWTextComp(Pos, -1), town(Town) + : LRClickableAreaWTextComp(Pos), town(Town) { } @@ -542,20 +536,21 @@ void MoraleLuckBox::set(const AFactionMember * node) { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - const int textId[] = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} + const std::array textId = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:} const int noneTxtId = 108; //Russian version uses same text for neutral morale\luck - const int neutralDescr[] = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. - const int componentType[] = {CComponent::luck, CComponent::morale}; - const int hoverTextBase[] = {7, 4}; + const std::array neutralDescr = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat. + const std::array componentType = {ComponentType::LUCK, ComponentType::MORALE}; + const std::array hoverTextBase = {7, 4}; TConstBonusListPtr modifierList = std::make_shared(); - bonusValue = 0; + + component.value = 0; if(node) - bonusValue = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); + component.value = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList); - int mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good + int mrlt = (component.value>0)-(component.value<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good hoverText = CGI->generaltexth->heroscrn[hoverTextBase[morale] - mrlt]; - baseType = componentType[morale]; + component.type = componentType[morale]; text = CGI->generaltexth->arraytxt[textId[morale]]; boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]); @@ -563,19 +558,19 @@ void MoraleLuckBox::set(const AFactionMember * node) || node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING))) { text += CGI->generaltexth->arraytxt[113]; //unaffected by morale - bonusValue = 0; + component.value = 0; } else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE)) { auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE)); text += "\n" + noMorale->Description(); - bonusValue = 0; + component.value = 0; } else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK)) { auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK)); text += "\n" + noLuck->Description(); - bonusValue = 0; + component.value = 0; } else { @@ -595,7 +590,7 @@ void MoraleLuckBox::set(const AFactionMember * node) else imageName = morale ? "IMRL42" : "ILCK42"; - image = std::make_shared(AnimationPath::builtin(imageName), bonusValue + 3); + image = std::make_shared(AnimationPath::builtin(imageName), *component.value + 3); image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon } @@ -603,7 +598,6 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small) : morale(Morale), small(Small) { - bonusValue = 0; pos = r + pos.topLeft(); defActions = 255-DISPOSE; } diff --git a/client/widgets/MiscWidgets.h b/client/widgets/MiscWidgets.h index 021193e9a..28e306614 100644 --- a/client/widgets/MiscWidgets.h +++ b/client/widgets/MiscWidgets.h @@ -10,6 +10,7 @@ #pragma once #include "../gui/CIntObject.h" +#include "../../lib/networkPacks/Component.h" VCMI_LIB_NAMESPACE_BEGIN @@ -202,13 +203,12 @@ private: class LRClickableAreaWTextComp: public LRClickableAreaWText { public: - int type; - int baseType; - int bonusValue; + Component component; + void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), int BaseType = -1); + LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), ComponentType baseType = ComponentType::NONE); std::shared_ptr createComponent() const; }; @@ -263,4 +263,4 @@ class SimpleLine : public CIntObject public: SimpleLine(Point pos1, Point pos2, ColorRGBA color); void showAll(Canvas & to) override; -}; \ No newline at end of file +}; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 8751dcf1e..f1ff6dfee 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -157,7 +157,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition) if (bid < BuildingID::DWELL_FIRST) { CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()), - std::make_shared(CComponent::building, bld->town->faction->getIndex(), bld->bid)); + std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(bld->town->faction->getId(), bld->bid))); } else { @@ -860,7 +860,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID) void CCastleBuildings::enterBuilding(BuildingID building) { - std::vector> comps(1, std::make_shared(CComponent::building, town->subID, building)); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps); } @@ -920,7 +920,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow() void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades) { - std::vector> comps(1, std::make_shared(CComponent::building,town->getFaction(),building)); + std::vector> comps(1, std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building))); std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated(); std::string hasNotProduced; std::string hasProduced; @@ -986,7 +986,7 @@ void CCastleBuildings::enterMagesGuild() CFunctionList onYes = [this](){ openMagesGuild(); }; CFunctionList onNo = onYes; onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); }; - std::vector> components(1, std::make_shared(CComponent::artifact,ArtifactID::SPELLBOOK,0)); + std::vector> components(1, std::make_shared(ComponentType::ARTIFACT, ArtifactID(ArtifactID::SPELLBOOK))); LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components); } @@ -1129,7 +1129,7 @@ void CCreaInfo::showPopupWindow(const Point & cursorPosition) if (showAvailable) GH.windows().createAndPushWindow(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level); else - CRClickPopup::createAndPush(genGrowthText(), std::make_shared(CComponent::creature, creature->getId())); + CRClickPopup::createAndPush(genGrowthText(), std::make_shared(ComponentType::CREATURE, creature->getId())); } bool CCreaInfo::getShowAvailable() @@ -1180,7 +1180,7 @@ void CTownInfo::showPopupWindow(const Point & cursorPosition) { if(building) { - auto c = std::make_shared(CComponent::building, building->town->faction->getIndex(), building->bid); + auto c = std::make_shared(ComponentType::BUILDING, BuildingTypeUniqueID(building->town->faction->getId(), building->bid)); CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c); } } @@ -1526,15 +1526,22 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin //Create components for all required resources std::vector> components; - for(int i = 0; iresources[i]) { - std::string text = std::to_string(building->resources[i]); - int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i]; - if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) - text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text; - components.push_back(std::make_shared(CComponent::resource, i, text, CComponent::small)); + MetaString message; + int resourceAmount = LOCPLINT->cb->getResourceAmount(i); + bool canAfford = resourceAmount >= building->resources[i]; + + if(!canAfford && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + message.appendRawString("{H3Red|%d}/%d"); + else + message.appendRawString("%d"); + + message.replaceNumber(resourceAmount); + message.replaceNumber(building->resources[i]); + components.push_back(std::make_shared(ComponentType::RESOURCE, i, message.toString(), CComponent::small)); } } @@ -1889,12 +1896,12 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell) void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition) { - LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); + LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); } void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition) { - CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(CComponent::spell, spell->id)); + CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared(ComponentType::SPELL, spell->id)); } void CMageGuildScreen::Scroll::hover(bool on) diff --git a/client/windows/CCreatureWindow.cpp b/client/windows/CCreatureWindow.cpp index 0db3a97e1..f6e073f2c 100644 --- a/client/windows/CCreatureWindow.cpp +++ b/client/windows/CCreatureWindow.cpp @@ -325,7 +325,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset) std::vector> resComps; for(TResources::nziterator i(totalCost); i.valid(); i++) { - resComps.push_back(std::make_shared(CComponent::resource, i->resType, (int)i->resVal)); + resComps.push_back(std::make_shared(ComponentType::RESOURCE, i->resType, i->resVal)); } if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost)) @@ -582,10 +582,10 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s const CCommanderInstance * commander = parent->info->commander; expRankIcon = std::make_shared(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y); - auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), CComponent::experience); + auto area = std::make_shared(Rect(pos.x, pos.y, 44, 44), ComponentType::EXPERIENCE); expArea = area; area->text = CGI->generaltexth->allTexts[2]; - area->bonusValue = commander->getExpRank(); + area->component.value = commander->getExpRank(); boost::replace_first(area->text, "%d", std::to_string(commander->getExpRank())); boost::replace_first(area->text, "%d", std::to_string(CGI->heroh->reqExp(commander->getExpRank() + 1))); boost::replace_first(area->text, "%d", std::to_string(commander->experience)); @@ -611,8 +611,8 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s if(art) { parent->stackArtifactIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y); - parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), CComponent::artifact); - parent->stackArtifactHelp->type = art->artType->getId(); + parent->stackArtifactHelp = std::make_shared(Rect(pos, Point(44, 44)), ComponentType::ARTIFACT); + parent->stackArtifactHelp->component.subType = art->artType->getId(); if(parent->info->owner) { diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 6acc90b27..e4e93337d 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -119,9 +119,9 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v) { - auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill); + auto area = std::make_shared(Rect(30 + 70 * v, 109, 42, 64), ComponentType::PRIM_SKILL); area->text = CGI->generaltexth->arraytxt[2+v]; - area->type = v; + area->component.subType = PrimarySkill(v); area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]); primSkillAreas.push_back(area); @@ -154,7 +154,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero) for(int i = 0; i < std::min(hero->secSkills.size(), 8u); ++i) { Rect r = Rect(i%2 == 0 ? 18 : 162, 276 + 48 * (i/2), 136, 42); - secSkillAreas.push_back(std::make_shared(r, CComponent::secskill)); + secSkillAreas.push_back(std::make_shared(r, ComponentType::SEC_SKILL)); secSkillImages.push_back(std::make_shared(secSkills, 0, 0, r.x, r.y)); int x = (i % 2) ? 212 : 68; @@ -232,20 +232,21 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) //primary skills support for(size_t g=0; gbonusValue = curHero->getPrimSkillLevel(static_cast(g)); - primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue)); + int value = curHero->getPrimSkillLevel(static_cast(g)); + primSkillAreas[g]->component.value = value; + primSkillValues[g]->setText(std::to_string(value)); } //secondary skills support for(size_t g=0; g< secSkillAreas.size(); ++g) { - int skill = curHero->secSkills[g].first; - int level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first)); + SecondarySkill skill = curHero->secSkills[g].first; + int level = curHero->getSecSkillLevel(skill); std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated(); std::string skillValue = CGI->generaltexth->levels[level-1]; - secSkillAreas[g]->type = skill; - secSkillAreas[g]->bonusValue = level; + secSkillAreas[g]->component.subType = skill; + secSkillAreas[g]->component.value = level; secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName); secSkillImages[g]->setFrame(skill*3 + level + 2); diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 85d157bbe..4be0b5569 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -256,7 +256,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr break; case HERO_PRIMARY_SKILL: text = CGI->generaltexth->arraytxt[2+getSubID()]; - comp = std::make_shared(CComponent::primskill, getSubID(), (int)getValue()); + comp = std::make_shared(ComponentType::PRIM_SKILL, PrimarySkill(getSubID()), getValue()); break; case HERO_MANA: text = CGI->generaltexth->allTexts[149]; @@ -271,7 +271,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr if(value) { text = CGI->skillh->getByIndex(subID)->getDescriptionTranslated((int)value); - comp = std::make_shared(CComponent::secskill, subID, (int)value); + comp = std::make_shared(ComponentType::SEC_SKILL, SecondarySkill(subID), (int)value); } break; } diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index bc43ea654..5fb2f1a8d 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -558,7 +558,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition) //battle spell on adv map or adventure map spell during combat => display infowindow, not cast if((combatSpell ^ inCombat) || inCastle) { - std::vector> hlp(1, std::make_shared(CComponent::spell, mySpell->id, 0)); + std::vector> hlp(1, std::make_shared(ComponentType::SPELL, mySpell->id)); LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp); } else if(combatSpell) @@ -614,7 +614,7 @@ void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition) boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg)); } - CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(CComponent::spell, mySpell->id)); + CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared(ComponentType::SPELL, mySpell->id)); } } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index dd06d2dab..34824e619 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -401,7 +401,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std std::vector> comps; for(auto & skill : skills) { - auto comp = std::make_shared(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); + auto comp = std::make_shared(ComponentType::SEC_SKILL, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium); comp->onChoose = std::bind(&CLevelWindow::close, this); comps.push_back(comp); } @@ -693,9 +693,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, else primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32)); primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g]; - primSkillAreas[g]->type = g; - primSkillAreas[g]->bonusValue = 0; - primSkillAreas[g]->baseType = 0; + primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g)); primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1]; boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]); } @@ -708,14 +706,11 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, //secondary skill's clickable areas for(int g=0; gsecSkills.size(); ++g) { - int skill = hero->secSkills[g].first, - level = hero->secSkills[g].second; // <1, 3> + SecondarySkill skill = hero->secSkills[g].first; + int level = hero->secSkills[g].second; // <1, 3> secSkillAreas[b].push_back(std::make_shared()); secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) ); - secSkillAreas[b][g]->baseType = 1; - - secSkillAreas[b][g]->type = skill; - secSkillAreas[b][g]->bonusValue = level; + secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level); secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level); secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21]; @@ -1082,7 +1077,7 @@ void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition) void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition) { - CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(CComponent::secskill, ID, 1)); + CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared(ComponentType::SEC_SKILL, ID, 1)); } void CUniversityWindow::CItem::hover(bool on) diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 5580c5a36..07b47b8eb 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -375,7 +375,7 @@ class CUniversityWindow : public CStatusbarWindow std::shared_ptr name; std::shared_ptr level; public: - int ID;//id of selected skill + SecondarySkill ID;//id of selected skill CUniversityWindow * parent; void showAll(Canvas & to) override; diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index b72f16521..1a75578cc 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -1017,6 +1017,11 @@ const Creature * CStackBasicDescriptor::getType() const return type; } +CreatureID CStackBasicDescriptor::getId() const +{ + return type->getId(); +} + TQuantity CStackBasicDescriptor::getCount() const { return count; diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 752eb203f..9b3cad26c 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -39,6 +39,7 @@ public: virtual ~CStackBasicDescriptor() = default; const Creature * getType() const; + CreatureID getId() const; TQuantity getCount() const; virtual void setType(const CCreature * c); diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 95d56a267..722033f51 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -458,6 +458,21 @@ std::string GameResID::entityType() return "resource"; } +const std::array & GameResID::ALL_RESOURCES() +{ + static const std::array allResources = { + GameResID(WOOD), + GameResID(MERCURY), + GameResID(ORE), + GameResID(SULFUR), + GameResID(CRYSTAL), + GameResID(GEMS), + GameResID(GOLD) + }; + + return allResources; +} + std::string SecondarySkill::entityType() { return "secondarySkill"; diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 1ff20c597..2a0aaf2ca 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -960,9 +960,11 @@ public: static si32 decode(const std::string & identifier); static std::string encode(const si32 index); static std::string entityType(); + + static const std::array & ALL_RESOURCES(); }; -class BuildingTypeUniqueID : public Identifier +class DLL_LINKAGE BuildingTypeUniqueID : public Identifier { public: BuildingTypeUniqueID(FactionID faction, BuildingID building ); diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 849d171c6..7a6634326 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -79,11 +79,7 @@ std::vector CBank::getPopupComponents(PlayerColor player) const for (auto const & guard : guardsAmounts) { - Component comp; - comp.id = Component::EComponentType::CREATURE; - comp.subtype = guard.first.getNum(); - comp.val = guard.second; - + Component comp(ComponentType::CREATURE, guard.first, guard.second); result.push_back(comp); } return result; @@ -236,7 +232,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const break; } cb->giveHeroBonus(&gbonus); - iw.components.emplace_back(Component::EComponentType::MORALE, 0, -1, 0); + iw.components.emplace_back(ComponentType::MORALE, -1); iw.soundID = soundBase::GRAVEYARD; break; } @@ -247,7 +243,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const gb.id = hero->id.getNum(); cb->giveHeroBonus(&gb); textID = 107; - iw.components.emplace_back(Component::EComponentType::LUCK, 0, -2, 0); + iw.components.emplace_back(ComponentType::LUCK, -2); break; } case Obj::CREATURE_BANK: @@ -267,21 +263,21 @@ void CBank::doVisit(const CGHeroInstance * hero) const //grant resources if (bc) { - for (int it = 0; it < bc->resources.size(); it++) + for (GameResID it : GameResID::ALL_RESOURCES()) { if (bc->resources[it] != 0) { - iw.components.emplace_back(Component::EComponentType::RESOURCE, it, bc->resources[it], 0); + iw.components.emplace_back(ComponentType::RESOURCE, it, bc->resources[it]); loot.appendRawString("%d %s"); - loot.replaceNumber(iw.components.back().val); - loot.replaceLocalString(EMetaText::RES_NAMES, iw.components.back().subtype); + loot.replaceNumber(bc->resources[it]); + loot.replaceLocalString(EMetaText::RES_NAMES, it); cb->giveResource(hero->getOwner(), static_cast(it), bc->resources[it]); } } //grant artifacts for (auto & elem : bc->artifacts) { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE); @@ -325,7 +321,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const if(hero->canLearnSpell(spell)) { spells.insert(spellId); - iw.components.emplace_back(Component::EComponentType::SPELL, spellId, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, spellId); } } else @@ -356,7 +352,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const for(const auto & elem : ourArmy.Slots()) { - iw.components.emplace_back(*elem.second); + iw.components.emplace_back(ComponentType::CREATURE, elem.second->getId(), elem.second->getCount()); loot.appendRawString("%s"); loot.replaceCreatureName(*elem.second); } diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 2aa2563d2..7fbaaabba 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -563,17 +563,17 @@ void CGCreature::giveReward(const CGHeroInstance * h) const if(!resources.empty()) { cb->giveResources(h->tempOwner, resources); - for(int i = 0; i < resources.size(); i++) + for(auto const & res : GameResID::ALL_RESOURCES()) { - if(resources[i] > 0) - iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0); + if(resources[res] > 0) + iw.components.emplace_back(ComponentType::RESOURCE, res, resources[res]); } } if(gainedArtifact != ArtifactID::NONE) { cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE); - iw.components.emplace_back(Component::EComponentType::ARTIFACT, gainedArtifact, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact); } if(!iw.components.empty()) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 870e10c8f..d9cd4b6c6 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -944,7 +944,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::pickup01 + rand.nextInt(6); iw.player = tempOwner; - iw.components.emplace_back(raisedStack); + iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count); if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural) { diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index b449dc0e0..1406786f9 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -207,31 +207,31 @@ void CTownBonus::onHeroVisit (const CGHeroInstance * h) const case BuildingSubID::KNOWLEDGE_VISITING_BONUS: //wall of knowledge what = PrimarySkill::KNOWLEDGE; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 3, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::KNOWLEDGE, 1); break; case BuildingSubID::SPELL_POWER_VISITING_BONUS: //order of fire what = PrimarySkill::SPELL_POWER; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 2, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::SPELL_POWER, 1); break; case BuildingSubID::ATTACK_VISITING_BONUS: //hall of Valhalla what = PrimarySkill::ATTACK; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 0, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::ATTACK, 1); break; case BuildingSubID::EXPERIENCE_VISITING_BONUS: //academy of battle scholars what = PrimarySkill::EXPERIENCE; val = static_cast(h->calculateXp(1000)); - iw.components.emplace_back(Component::EComponentType::EXPERIENCE, 0, val, 0); + iw.components.emplace_back(ComponentType::EXPERIENCE, val); break; case BuildingSubID::DEFENSE_VISITING_BONUS: //cage of warlords what = PrimarySkill::DEFENSE; val = 1; - iw.components.emplace_back(Component::EComponentType::PRIM_SKILL, 1, 1, 0); + iw.components.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill::DEFENSE, 1); break; case BuildingSubID::CUSTOM_VISITING_BONUS: diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index f0243e86a..0f41e30e8 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -325,7 +325,7 @@ void CGTownInstance::onHeroVisit(const CGHeroInstance * h) const InfoWindow iw; iw.player = h->tempOwner; iw.text.appendRawString(h->commander->getName()); - iw.components.emplace_back(*h->commander); + iw.components.emplace_back(ComponentType::CREATURE, h->commander->getId(), h->commander->getCount()); cb->showInfoDialog(&iw); } } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index a489b5171..640641685 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -204,13 +204,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) { - components.emplace_back(Component::EComponentType::HERO_PORTRAIT, heroPortrait, 0, 0); + components.emplace_back(ComponentType::HERO_PORTRAIT, heroPortrait); addKillTargetReplacements(text); } if(killTarget != ObjectInstanceID::NONE && stackToKill.type) { - components.emplace_back(stackToKill); + components.emplace_back(ComponentType::CREATURE, stackToKill.getId(), stackToKill.getCount()); addKillTargetReplacements(text); } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 19193d39f..39506c80a 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -169,7 +169,7 @@ void CGMine::flagMine(const PlayerColor & player) const iw.soundID = soundBase::FLAGMINE; iw.text.appendLocalString(EMetaText::MINE_EVNTS, producedResource); //not use subID, abandoned mines uses default mine texts iw.player = player; - iw.components.emplace_back(Component::EComponentType::RESOURCE, producedResource, producedQuantity, -1); + iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity); cb->showInfoDialog(&iw); } @@ -324,7 +324,7 @@ void CGResource::collectRes(const PlayerColor & player) const sii.text.appendLocalString(EMetaText::ADVOB_TXT,113); sii.text.replaceLocalString(EMetaText::RES_NAMES, resourceID()); } - sii.components.emplace_back(Component::EComponentType::RESOURCE, resourceID(), amount, 0); + sii.components.emplace_back(ComponentType::RESOURCE, resourceID(), amount); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); cb->showInfoDialog(&sii); cb->removeObject(this, player); @@ -666,7 +666,7 @@ void CGWhirlpool::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 168); - iw.components.emplace_back(CStackBasicDescriptor(h->getCreature(targetstack), -countToTake)); + iw.components.emplace_back(ComponentType::CREATURE, h->getCreature(targetstack)->getId(), -countToTake); cb->showInfoDialog(&iw); cb->changeStackCount(StackLocation(h, targetstack), -countToTake); } @@ -790,7 +790,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const { case Obj::ARTIFACT: { - iw.components.emplace_back(Component::EComponentType::ARTIFACT, getArtifact(), 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, getArtifact()); if(!message.empty()) iw.text = message; else @@ -800,7 +800,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const case Obj::SPELL_SCROLL: { int spellID = storedArtifact->getScrollSpellID(); - iw.components.emplace_back(Component::EComponentType::SPELL, spellID, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, spellID); if(!message.empty()) iw.text = message; else diff --git a/lib/networkPacks/Component.h b/lib/networkPacks/Component.h index fa764e085..43ec61277 100644 --- a/lib/networkPacks/Component.h +++ b/lib/networkPacks/Component.h @@ -9,44 +9,67 @@ */ #pragma once +#include "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + VCMI_LIB_NAMESPACE_BEGIN -class CStackBasicDescriptor; +enum class ComponentType : int8_t +{ + NONE = -1, + PRIM_SKILL, + SEC_SKILL, + RESOURCE, + RESOURCE_PER_DAY, + CREATURE, + ARTIFACT, + SPELL_SCROLL, + MANA, + EXPERIENCE, + LEVEL, + SPELL, + MORALE, + LUCK, + BUILDING, + HERO_PORTRAIT, + FLAG +}; + +using ComponentSubType = VariantIdentifier; struct Component { - enum class EComponentType : uint8_t - { - PRIM_SKILL, - SEC_SKILL, - RESOURCE, - CREATURE, - ARTIFACT, - EXPERIENCE, - SPELL, - MORALE, - LUCK, - BUILDING, - HERO_PORTRAIT, - FLAG, - INVALID //should be last - }; - EComponentType id = EComponentType::INVALID; - ui16 subtype = 0; //id==EXPPERIENCE subtype==0 means exp points and subtype==1 levels - si32 val = 0; // + give; - take - si16 when = 0; // 0 - now; +x - within x days; -x - per x days + ComponentType type = ComponentType::NONE; + ComponentSubType subType; + std::optional value; // + give; - take template void serialize(Handler &h, const int version) { - h & id; - h & subtype; - h & val; - h & when; + h & type; + h & subType; + h & value; } + Component() = default; - DLL_LINKAGE explicit Component(const CStackBasicDescriptor &stack); - Component(Component::EComponentType Type, ui16 Subtype, si32 Val, si16 When) - :id(Type),subtype(Subtype),val(Val),when(When) + + template, bool> = true> + Component(ComponentType type, Numeric value) + : type(type) + , value(value) + { + } + + template, bool> = true> + Component(ComponentType type, IdentifierType subType) + : type(type) + , subType(subType) + { + } + + Component(ComponentType type, ComponentSubType subType, int32_t value) + : type(type) + , subType(subType) + , value(value) { } }; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 147e99f81..da35e7298 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2522,13 +2522,6 @@ void TurnTimeUpdate::applyGs(CGameState *gs) const playerState.turnTimer = turnTimer; } -Component::Component(const CStackBasicDescriptor & stack) - : id(EComponentType::CREATURE) - , subtype(stack.type->getId()) - , val(stack.count) -{ -} - void EntitiesChanged::applyGs(CGameState * gs) { for(const auto & change : changes) diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index aadf30285..8cfb9e81e 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -201,7 +201,7 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); CreatureID dest(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.second.String()).value()); - reward.extraComponents.emplace_back(Component::EComponentType::CREATURE, dest.getNum(), 0, 0); + reward.extraComponents.emplace_back(ComponentType::CREATURE, dest.getNum()); reward.creaturesChange[from] = dest; } diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index ae6602231..6f78d4868 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -187,49 +187,45 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, const CGHeroInstance * h) const { if (heroExperience) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h->calculateXp(heroExperience)), 0); + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h->calculateXp(heroExperience))); if (heroLevel > 0) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + comps.emplace_back(ComponentType::EXPERIENCE, heroLevel); if (manaPoints || manaPercentage > 0) { int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, absoluteMana + manaPoints, 0); + comps.emplace_back(ComponentType::MANA, absoluteMana + manaPoints); } for (size_t i=0; i(i), primary[i], 0); + comps.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill(i), primary[i]); } for(const auto & entry : secondary) - comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + comps.emplace_back(ComponentType::SEC_SKILL, entry.first, entry.second); for(const auto & entry : artifacts) - comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); for(const auto & entry : players) - comps.emplace_back(Component::EComponentType::FLAG, entry, 0, 0); + comps.emplace_back(ComponentType::FLAG, entry); - //FIXME: portrait may not match hero, if custom portrait was set in map editor for(const auto & entry : heroes) - comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroTypes()->getById(entry)->getIconIndex(), 0, 0); + comps.emplace_back(ComponentType::HERO_PORTRAIT, entry); - for(const auto & entry : heroClasses) - comps.emplace_back(Component::EComponentType::HERO_PORTRAIT, VLC->heroClasses()->getById(entry)->getIconIndex(), 0, 0); - for (size_t i=0; i(i), resources[i], 0); + comps.emplace_back(ComponentType::RESOURCE, GameResID(i), resources[i]); } } diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index cb080adca..135206d90 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -74,42 +74,42 @@ void Rewardable::Reward::loadComponents(std::vector & comps, const CG for (auto & bonus : bonuses) { if (bonus.type == BonusType::MORALE) - comps.emplace_back(Component::EComponentType::MORALE, 0, bonus.val, 0); + comps.emplace_back(ComponentType::MORALE, bonus.val); if (bonus.type == BonusType::LUCK) - comps.emplace_back(Component::EComponentType::LUCK, 0, bonus.val, 0); + comps.emplace_back(ComponentType::LUCK, bonus.val); } if (heroExperience) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 0, static_cast(h ? h->calculateXp(heroExperience) : heroExperience), 0); + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); if (heroLevel) - comps.emplace_back(Component::EComponentType::EXPERIENCE, 1, heroLevel, 0); + comps.emplace_back(ComponentType::LEVEL, heroLevel); if (manaDiff || manaPercentage >= 0) - comps.emplace_back(Component::EComponentType::PRIM_SKILL, 5, h ? (calculateManaPoints(h) - h->mana) : manaDiff, 0); + comps.emplace_back(ComponentType::MANA, h ? (calculateManaPoints(h) - h->mana) : manaDiff); for (size_t i=0; i(i), primary[i], 0); + comps.emplace_back(ComponentType::PRIM_SKILL, PrimarySkill(i), primary[i]); } for(const auto & entry : secondary) - comps.emplace_back(Component::EComponentType::SEC_SKILL, entry.first, entry.second, 0); + comps.emplace_back(ComponentType::SEC_SKILL, entry.first, entry.second); for(const auto & entry : artifacts) - comps.emplace_back(Component::EComponentType::ARTIFACT, entry, 1, 0); + comps.emplace_back(ComponentType::ARTIFACT, entry); for(const auto & entry : spells) - comps.emplace_back(Component::EComponentType::SPELL, entry, 1, 0); + comps.emplace_back(ComponentType::SPELL, entry); for(const auto & entry : creatures) - comps.emplace_back(Component::EComponentType::CREATURE, entry.type->getId(), entry.count, 0); + comps.emplace_back(ComponentType::CREATURE, entry.type->getId(), entry.count); for (size_t i=0; i(i), resources[i], 0); + comps.emplace_back(ComponentType::RESOURCE, GameResID(i), resources[i]); } } diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 7e1e3c19e..2f8b056b3 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -528,8 +528,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons request.player = parameters.caster->getCasterOwner(); request.title.appendLocalString(EMetaText::JK_TXT, 40); request.description.appendLocalString(EMetaText::JK_TXT, 41); - request.icon.id = Component::EComponentType::SPELL; - request.icon.subtype = owner->id.toEnum(); + request.icon = Component(ComponentType::SPELL, owner->id); env->genericQuery(&request, request.player, queryCallback); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index c702f35ee..a8134dfe4 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1571,11 +1571,12 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t if (!cs1.spells.empty() || !cs2.spells.empty())//create a message { - int ScholarSkillLevel = std::max(h1->getSecSkillLevel(SecondarySkill::SCHOLAR), - h2->getSecSkillLevel(SecondarySkill::SCHOLAR)); + SecondarySkill scholarSkill = SecondarySkill::SCHOLAR; + + int scholarSkillLevel = std::max(h1->getSecSkillLevel(scholarSkill), h2->getSecSkillLevel(scholarSkill)); InfoWindow iw; iw.player = h1->tempOwner; - iw.components.emplace_back(Component::EComponentType::SEC_SKILL, 18, ScholarSkillLevel, 0); + iw.components.emplace_back(ComponentType::SEC_SKILL, scholarSkill, scholarSkillLevel); iw.text.appendLocalString(EMetaText::GENERAL_TXT, 139);//"%s, who has studied magic extensively, iw.text.replaceRawString(h1->getNameTranslated()); @@ -1586,7 +1587,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs2.spells.size()); for (auto it : cs2.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); + iw.components.emplace_back(ComponentType::SPELL, it); iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); switch (size--) { @@ -1614,7 +1615,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t int size = static_cast(cs1.spells.size()); for (auto it : cs1.spells) { - iw.components.emplace_back(Component::EComponentType::SPELL, it, 1, 0); + iw.components.emplace_back(ComponentType::SPELL, it); iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); switch (size--) { @@ -3216,10 +3217,10 @@ void CGameHandler::handleTimeEvents() iw.player = color; iw.text = ev.message; - for (int i=0; iresources[i] != n.res.at(player)[i]) //if resource had changed, we add it to the dialog - iw.components.emplace_back(Component::EComponentType::RESOURCE,i,n.res.at(player)[i]-was[i],0); - + iw.components.emplace_back(ComponentType::RESOURCE, i, n.res.at(player)[i] - was[i]); } for (auto & i : ev.buildings) @@ -3284,7 +3284,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->hasBuilt(i)) { buildStructure(town->id, i, true); - iw.components.emplace_back(Component::EComponentType::BUILDING, town->getFaction(), i, 0); + iw.components.emplace_back(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), i)); } } @@ -3300,8 +3300,7 @@ void CGameHandler::handleTownEvents(CGTownInstance * town, NewTurn &n) if (!town->creatures.at(i).second.empty() && ev.creatures.at(i) > 0)//there is dwelling { sac.creatures[i].first += ev.creatures.at(i); - iw.components.emplace_back(Component::EComponentType::CREATURE, - town->creatures.at(i).second.back(), ev.creatures.at(i), 0); + iw.components.emplace_back(ComponentType::CREATURE, town->creatures.at(i).second.back(), ev.creatures.at(i)); } } sendAndApply(&iw); //show dialog @@ -3641,7 +3640,7 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC out.player = player; out.text = victoryLossCheckResult.messageToSelf; out.text.replaceLocalString(EMetaText::COLOR, player.getNum()); - out.components.emplace_back(Component::EComponentType::FLAG, player.getNum(), 0, 0); + out.components.emplace_back(ComponentType::FLAG, player); } bool CGameHandler::dig(const CGHeroInstance *h) @@ -3669,7 +3668,7 @@ bool CGameHandler::dig(const CGHeroInstance *h) sendAndApply(&iw); iw.soundID = soundBase::invalid; - iw.components.emplace_back(Component::EComponentType::ARTIFACT, ArtifactID::GRAIL, 0, 0); + iw.components.emplace_back(ComponentType::ARTIFACT, ArtifactID(ArtifactID::GRAIL)); iw.text.clear(); iw.text.appendLocalString(EMetaText::ART_DESCR, ArtifactID::GRAIL); sendAndApply(&iw); @@ -4144,7 +4143,7 @@ const CGHeroInstance * CGameHandler::getVisitingHero(const CGObjectInstance *obj { assert(obj); - for (auto const & query : queries->allQueries()) + for(const auto & query : queries->allQueries()) { auto visit = std::dynamic_pointer_cast(query); if (visit && visit->visitedObject == obj) diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 4b3f60491..99e50d389 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -419,9 +419,11 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) for (auto art : arts) //TODO; separate function to display loot for various ojects? { - iw.components.emplace_back( - Component::EComponentType::ARTIFACT, art->artType->getId(), - art->artType->getId() == ArtifactID::SPELL_SCROLL? art->getScrollSpellID() : SpellID(0), 0); + if (art->artType->getId() == ArtifactID::SPELL_SCROLL) + iw.components.emplace_back(ComponentType::SPELL_SCROLL, art->getScrollSpellID()); + else + iw.components.emplace_back(ComponentType::ARTIFACT, art->artType->getId()); + if (iw.components.size() >= 14) { gameHandler->sendAndApply(&iw); @@ -463,7 +465,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); if (i == cs.spells.size() - 2) //we just added pre-last name iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " - iw.components.emplace_back(Component::EComponentType::SPELL, *it, 0, 0); + iw.components.emplace_back(ComponentType::SPELL, *it); } gameHandler->sendAndApply(&iw); gameHandler->sendAndApply(&cs); From f201e3019a29267ce7e6e9c667623a27ae101d75 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 1 Nov 2023 00:11:02 +0200 Subject: [PATCH 1031/1248] Fix crash on right-clicking rewardable objects with no hero selected --- lib/rewardable/Interface.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index da7f86520..b1f41e8ce 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -35,7 +35,7 @@ std::vector Rewardable::Interface::getAvailableRewards(const CGHeroInstanc { const Rewardable::VisitInfo & visit = configuration.info[i]; - if(event == visit.visitType && visit.limiter.heroAllowed(hero)) + if(event == visit.visitType && (!hero || visit.limiter.heroAllowed(hero))) { logGlobal->trace("Reward %d is allowed", i); ret.push_back(static_cast(i)); From 056ef00f740b2f1a3ce82f6726eab809026b4ea2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 13:18:40 +0200 Subject: [PATCH 1032/1248] Bugfixing --- client/widgets/CComponent.cpp | 24 ++++++++++++------------ client/widgets/CComponent.h | 12 ++++++------ client/windows/CCastleInterface.cpp | 4 +++- lib/mapObjects/MiscObjects.cpp | 6 +++--- lib/rewardable/Info.cpp | 2 +- 5 files changed, 25 insertions(+), 23 deletions(-) diff --git a/client/widgets/CComponent.cpp b/client/widgets/CComponent.cpp index 2a77b46da..b3f725746 100644 --- a/client/widgets/CComponent.cpp +++ b/client/widgets/CComponent.cpp @@ -44,17 +44,17 @@ CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::option init(Type, Subtype, Val, imageSize, font, ""); } -CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::string Val, ESize imageSize, EFonts font) +CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize, EFonts font) { init(Type, Subtype, std::nullopt, imageSize, font, Val); } CComponent::CComponent(const Component & c, ESize imageSize, EFonts font) { - init(c.type, c.subType, c.value, imageSize, font); + init(c.type, c.subType, c.value, imageSize, font, ""); } -void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, std::string ValText) +void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts fnt, const std::string & ValText) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -107,7 +107,7 @@ void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optiona } } -std::vector CComponent::getFileName() +std::vector CComponent::getFileName() const { static const std::array primSkillsArr = {"PSKIL32", "PSKIL32", "PSKIL42", "PSKILL"}; static const std::array secSkillsArr = {"SECSK32", "SECSK32", "SECSKILL", "SECSK82"}; @@ -160,7 +160,7 @@ std::vector CComponent::getFileName() } } -size_t CComponent::getIndex() +size_t CComponent::getIndex() const { switch(data.type) { @@ -199,7 +199,7 @@ size_t CComponent::getIndex() } } -std::string CComponent::getDescription() +std::string CComponent::getDescription() const { switch(data.type) { @@ -242,11 +242,11 @@ std::string CComponent::getDescription() return ""; default: assert(0); - return 0; + return ""; } } -std::string CComponent::getSubtitle() +std::string CComponent::getSubtitle() const { if (!customSubtitle.empty()) return customSubtitle; @@ -273,14 +273,14 @@ std::string CComponent::getSubtitle() case ComponentType::RESOURCE: return std::to_string(data.value.value_or(0)); case ComponentType::RESOURCE_PER_DAY: - return boost::str(boost::format(CGI->generaltexth->allTexts[387]) % data.value.value_or(0)); + return boost::str(boost::format(CGI->generaltexth->allTexts[3]) % data.value.value_or(0)); case ComponentType::CREATURE: { auto creature = CGI->creh->getById(data.subType.as()); - if ( data.value.value_or(0) ) - return std::to_string(data.value.value_or(0)) + " " + (data.value.value_or(0) > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); + if(data.value) + return std::to_string(*data.value) + " " + (*data.value > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated()); else - return data.value.value_or(0) > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated(); + return creature->getNamePluralTranslated(); } case ComponentType::ARTIFACT: return CGI->artifacts()->getById(data.subType.as())->getNameTranslated(); diff --git a/client/widgets/CComponent.h b/client/widgets/CComponent.h index 813ca5667..f41ea3264 100644 --- a/client/widgets/CComponent.h +++ b/client/widgets/CComponent.h @@ -40,11 +40,11 @@ public: private: std::vector> lines; - size_t getIndex(); - std::vector getFileName(); + size_t getIndex() const; + std::vector getFileName() const; void setSurface(const AnimationPath & defName, int imgPos); - void init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText=""); + void init(ComponentType Type, ComponentSubType Subtype, std::optional Val, ESize imageSize, EFonts font, const std::string & ValText); public: std::shared_ptr image; @@ -53,11 +53,11 @@ public: ESize size; //component size. EFonts font; //Font size of label - std::string getDescription(); - std::string getSubtitle(); + std::string getDescription() const; + std::string getSubtitle() const; CComponent(ComponentType Type, ComponentSubType Subtype, std::optional Val = std::nullopt, ESize imageSize=large, EFonts font = FONT_SMALL); - CComponent(ComponentType Type, ComponentSubType Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL); + CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize=large, EFonts font = FONT_SMALL); CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL); void showPopupWindow(const Point & cursorPosition) override; //call-in diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index f1ff6dfee..2562fbb12 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1535,11 +1535,13 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin bool canAfford = resourceAmount >= building->resources[i]; if(!canAfford && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool()) + { message.appendRawString("{H3Red|%d}/%d"); + message.replaceNumber(resourceAmount); + } else message.appendRawString("%d"); - message.replaceNumber(resourceAmount); message.replaceNumber(building->resources[i]); components.push_back(std::make_shared(ComponentType::RESOURCE, i, message.toString(), CComponent::small)); } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 39506c80a..41d2ad2bf 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -799,14 +799,14 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const break; case Obj::SPELL_SCROLL: { - int spellID = storedArtifact->getScrollSpellID(); - iw.components.emplace_back(ComponentType::SPELL, spellID); + SpellID spell = storedArtifact->getScrollSpellID(); + iw.components.emplace_back(ComponentType::SPELL, spell); if(!message.empty()) iw.text = message; else { iw.text.appendLocalString(EMetaText::ADVOB_TXT,135); - iw.text.replaceLocalString(EMetaText::SPELL_NAME, spellID); + iw.text.replaceLocalString(EMetaText::SPELL_NAME, spell.getNum()); } } break; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 8cfb9e81e..47600c915 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -201,7 +201,7 @@ void Rewardable::Info::configureReward(Rewardable::Configuration & object, CRand CreatureID from(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.first).value()); CreatureID dest(VLC->identifiers()->getIdentifier(node.second.meta, "creature", node.second.String()).value()); - reward.extraComponents.emplace_back(ComponentType::CREATURE, dest.getNum()); + reward.extraComponents.emplace_back(ComponentType::CREATURE, dest); reward.creaturesChange[from] = dest; } From 02b4947db91cf902872e4da223c78e7b8925d7ff Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 20:01:20 +0200 Subject: [PATCH 1033/1248] Fix build --- lib/constants/VariantIdentifier.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 6b0394f65..8ea6a6096 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN /// This class represents field that may contain value of multiple different identifer types template -class DLL_LINKAGE VariantIdentifier +class VariantIdentifier { std::variant value; public: From 3b27e07385bb98435887dc1ac85d537df51634ba Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Fri, 3 Nov 2023 13:50:16 +0200 Subject: [PATCH 1034/1248] #3003 - fix hypnotize --- client/battle/BattleActionsController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index 7e476340c..f3adbe037 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -905,7 +905,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS const spells::Caster * BattleActionsController::getCurrentSpellcaster() const { if (heroSpellToCast) - return owner.getActiveHero(); + return owner.currentHero(); else return owner.stacksController->getActiveStack(); } From 2b9c362d5bd437c62da1787a4a3cb94bb0d6c571 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 16:29:59 +0200 Subject: [PATCH 1035/1248] Explicitly convert identifier to underlying enumeration --- lib/CArtHandler.cpp | 2 +- lib/CTownHandler.cpp | 2 +- lib/battle/CBattleInfoCallback.cpp | 2 +- lib/battle/SideInBattle.cpp | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CBank.cpp | 8 ++++---- lib/mapObjects/CGCreature.cpp | 2 +- lib/mapObjects/CGDwelling.cpp | 4 ++-- lib/mapObjects/CGObjectInstance.cpp | 4 ++-- lib/mapObjects/MiscObjects.cpp | 10 +++++----- lib/mapping/MapFormatH3M.cpp | 2 +- lib/networkPacks/NetPacksLib.cpp | 2 +- lib/pathfinder/CPathfinder.cpp | 4 ++-- lib/pathfinder/PathfindingRules.cpp | 6 +++--- lib/pathfinder/TurnInfo.cpp | 2 +- lib/rmg/modificators/ObjectManager.cpp | 4 ++-- lib/spells/ISpellMechanics.cpp | 2 +- 17 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 28baf2412..fbf75317d 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -167,7 +167,7 @@ bool CArtifact::isBig() const bool CArtifact::isTradable() const { - switch(id) + switch(id.getNum()) { case ArtifactID::SPELLBOOK: case ArtifactID::GRAIL: diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index a33205fb2..fdbeb98f7 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -686,7 +686,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons //MODS COMPATIBILITY FOR 0.96 if(!ret->produce.nonZero()) { - switch (ret->bid) { + switch (ret->bid.getNum()) { break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 28fbf09ee..7da7ced1d 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1663,7 +1663,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c || !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject))) continue; - switch (spellID) + switch (spellID.toEnum()) { case SpellID::SHIELD: case SpellID::FIRE_SHIELD: // not if all enemy units are shooters diff --git a/lib/battle/SideInBattle.cpp b/lib/battle/SideInBattle.cpp index 25fe84127..bfdcafb95 100644 --- a/lib/battle/SideInBattle.cpp +++ b/lib/battle/SideInBattle.cpp @@ -18,7 +18,7 @@ void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army hero = Hero; armyObject = Army; - switch(armyObject->ID) + switch(armyObject->ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR2: diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 4fd640557..4707150f4 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -931,7 +931,7 @@ void CGameState::initMapObjects() if(!obj) continue; - switch (obj->ID) + switch (obj->ID.getNum()) { case Obj::QUEST_GUARD: case Obj::SEER_HUT: diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 7a6634326..dad8101ba 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -137,7 +137,7 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const cb->sendAndApply(&cov); int banktext = 0; - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: banktext = 41; @@ -180,7 +180,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const if (bc) { - switch (ID) + switch (ID.toEnum()) { case Obj::DERELICT_SHIP: textID = 43; @@ -203,7 +203,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const } else { - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: case Obj::DERELICT_SHIP: @@ -216,7 +216,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const gbonus.bonus.sid = BonusSourceID(ID); gbonus.bonus.type = BonusType::MORALE; gbonus.bonus.val = -1; - switch (ID) + switch (ID.toEnum()) { case Obj::SHIPWRECK: textID = 123; diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 7fbaaabba..b18d5dded 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -161,7 +161,7 @@ CreatureID CGCreature::getCreature() const void CGCreature::pickRandomObject(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case MapObjectID::RANDOM_MONSTER: subID = VLC->creh->pickRandomMonster(rand); diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index b47429e1e..2374939c5 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -170,7 +170,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) void CGDwelling::initObj(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case Obj::CREATURE_GENERATOR1: case Obj::CREATURE_GENERATOR4: @@ -488,7 +488,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler) { - switch (ID) + switch (ID.toEnum()) { case Obj::WAR_MACHINE_FACTORY: case Obj::REFUGEE_CAMP: diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 92187bd28..74546c07c 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -173,7 +173,7 @@ void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) void CGObjectInstance::initObj(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case Obj::TAVERN: blockVisit = true; @@ -302,7 +302,7 @@ std::vector CGObjectInstance::getPopupComponents(const CGHeroInstance void CGObjectInstance::onHeroVisit( const CGHeroInstance * h ) const { - switch(ID) + switch(ID.toEnum()) { case Obj::SANCTUARY: { diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 41d2ad2bf..4b9bc9604 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -273,7 +273,7 @@ void CGResource::initObj(CRandomGenerator & rand) if(amount == CGResource::RANDOM_AMOUNT) { - switch(resourceID()) + switch(resourceID().toEnum()) { case EGameResID::GOLD: amount = rand.nextInt(5, 10) * 100; @@ -532,7 +532,7 @@ void CGMonolith::initObj(CRandomGenerator & rand) { std::vector IDs; IDs.push_back(ID); - switch(ID) + switch(ID.toEnum()) { case Obj::MONOLITH_ONE_WAY_ENTRANCE: type = ENTRANCE; @@ -723,7 +723,7 @@ ArtifactID CGArtifact::getArtifact() const void CGArtifact::pickRandomObject(CRandomGenerator & rand) { - switch(ID) + switch(ID.toEnum()) { case MapObjectID::RANDOM_ART: subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); @@ -786,7 +786,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const if(storedArtifact->artType->canBePutAt(h)) { - switch (ID) + switch (ID.toEnum()) { case Obj::ARTIFACT: { @@ -821,7 +821,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const } else { - switch(ID) + switch(ID.toEnum()) { case Obj::ARTIFACT: { diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 362205a89..f8402b54a 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1488,7 +1488,7 @@ CGObjectInstance * CMapLoaderH3M::readBank(const int3 & mapPosition, std::shared CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr objectTemplate, const int3 & mapPosition, const ObjectInstanceID & objectInstanceID) { - switch(objectTemplate->id) + switch(objectTemplate->id.toEnum()) { case Obj::EVENT: return readEvent(mapPosition, objectInstanceID); diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index da35e7298..4dab06f35 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -944,7 +944,7 @@ void FoWChange::applyGs(CGameState *gs) const CGObjectInstance *o = elem; if (o) { - switch(o->ID) + switch(o->ID.toEnum()) { case Obj::HERO: case Obj::MINE: diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 3887cb774..c46f9650d 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -302,7 +302,7 @@ bool CPathfinder::isLayerTransitionPossible() const if(source.node->action == EPathNodeAction::BATTLE) return false; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case ELayer::LAND: if(destLayer == ELayer::AIR) @@ -505,7 +505,7 @@ void CPathfinderHelper::updateTurnInfo(const int Turn) bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.getNum()) { case EPathfindingLayer::AIR: if(!options.useFlying) diff --git a/lib/pathfinder/PathfindingRules.cpp b/lib/pathfinder/PathfindingRules.cpp index 48aaf7814..d041aff85 100644 --- a/lib/pathfinder/PathfindingRules.cpp +++ b/lib/pathfinder/PathfindingRules.cpp @@ -123,7 +123,7 @@ void DestinationActionRule::process( EPathNodeAction action = EPathNodeAction::NORMAL; const auto * hero = pathfinderHelper->hero; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(source.node->layer == EPathfindingLayer::SAIL) @@ -290,7 +290,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea if(destination.node->accessible == EPathAccessibility::BLOCKED) return BlockingReason::DESTINATION_BLOCKED; - switch(destination.node->layer) + switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) @@ -359,7 +359,7 @@ void LayerTransitionRule::process( if(source.node->layer == destination.node->layer) return; - switch(source.node->layer) + switch(source.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(destination.node->layer == EPathfindingLayer::SAIL) diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 01ba846ed..8b8763cdb 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -47,7 +47,7 @@ TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn): bool TurnInfo::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(hero && hero->boat && hero->boat->layer == EPathfindingLayer::AIR) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 9d34b3833..b0025d7d6 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -556,7 +556,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD } } - switch (instance->object().ID) + switch (instance->object().ID.getNum()) { case Obj::RANDOM_TREASURE_ART: case Obj::RANDOM_MINOR_ART: //In OH3 quest artifacts have higher value than normal arts @@ -586,7 +586,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD case Obj::MONOLITH_ONE_WAY_EXIT: */ - switch (object.instances().front()->object().ID) + switch (object.instances().front()->object().ID.getNum()) { case Obj::WATER_WHEEL: if (auto* m = zone.getModificator()) diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 0a2be59cc..71e6166a5 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -717,7 +717,7 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s) std::unique_ptr IAdventureSpellMechanics::createMechanics(const CSpell * s) { - switch (s->id) + switch (s->id.getNum()) { case SpellID::SUMMON_BOAT: return std::make_unique(s); From 8f25f1fd4b1da9e539a06cd57d0a939ec9c4b4e1 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 16:31:12 +0200 Subject: [PATCH 1036/1248] Serialize identifiers without implicit conversion to int --- lib/CCreatureHandler.cpp | 2 +- lib/CSkillHandler.cpp | 19 -------------- lib/CSkillHandler.h | 5 ---- lib/battle/Unit.cpp | 2 +- lib/mapObjects/CQuest.cpp | 8 +++--- lib/rewardable/Limiter.cpp | 2 +- lib/serializer/CSerializer.h | 8 +++--- lib/serializer/JsonSerializeFormat.h | 37 +++++++++++++++++++++------- 8 files changed, 39 insertions(+), 44 deletions(-) diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index eb03049ce..2b723aac5 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -388,7 +388,7 @@ void CCreature::serializeJson(JsonSerializeFormat & handler) if(handler.updating) { cost.serializeJson(handler, "cost"); - handler.serializeInt("faction", faction);//TODO: unify with deferred resolve + handler.serializeId("faction", faction); } handler.serializeInt("level", level); diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 82338746c..de080a1c1 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -262,23 +262,4 @@ std::vector CSkillHandler::getDefaultAllowed() const return allowedSkills; } -si32 CSkillHandler::decodeSkill(const std::string & identifier) -{ - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "skill", identifier); - if(rawId) - return rawId.value(); - else - return -1; -} - -std::string CSkillHandler::encodeSkill(const si32 index) -{ - return (*VLC->skillh)[SecondarySkill(index)]->identifier; -} - -std::string CSkillHandler::encodeSkillWithType(const si32 index) -{ - return ModUtility::makeFullIdentifier("", "skill", encodeSkill(index)); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index d8f84a3dd..83fe729bb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -111,11 +111,6 @@ public: std::vector getDefaultAllowed() const override; - ///json serialization helpers - static si32 decodeSkill(const std::string & identifier); - static std::string encodeSkill(const si32 index); - static std::string encodeSkillWithType(const si32 index); - template void serialize(Handler & h, const int version) { h & objects; diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 7e0c9be2d..1355ba089 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -218,7 +218,7 @@ int Unit::getRawSurrenderCost() const void UnitInfo::serializeJson(JsonSerializeFormat & handler) { handler.serializeInt("count", count); - handler.serializeId("type", type, CreatureID::NONE); + handler.serializeId("type", type, CreatureID(CreatureID::NONE)); handler.serializeInt("side", side); handler.serializeInt("position", position); handler.serializeBool("summoned", summoned); diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 640641685..c08ca9f4f 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -392,15 +392,15 @@ void CQuest::serializeJson(JsonSerializeFormat & handler, const std::string & fi if(missionType == "Hero") { - ui32 temp; - handler.serializeId("hero", temp, 0); + HeroTypeID temp; + handler.serializeId("hero", temp, HeroTypeID::NONE); mission.heroes.emplace_back(temp); } if(missionType == "Player") { - ui32 temp; - handler.serializeId("player", temp, PlayerColor::NEUTRAL); + PlayerColor temp; + handler.serializeId("player", temp, PlayerColor::NEUTRAL); mission.players.emplace_back(temp); } } diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 6f78d4868..2df9a6c24 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -249,7 +249,7 @@ void Rewardable::Limiter::serializeJson(JsonSerializeFormat & handler) std::vector> fieldValue(secondary.begin(), secondary.end()); a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("skill", e.first, SecondarySkill(SecondarySkill::NONE)); h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); }); a.syncSize(fieldValue); diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index df7278f00..706a0de9b 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -53,14 +53,14 @@ struct VectorizedObjectInfo /// Base class for serializers capable of reading or writing data class DLL_LINKAGE CSerializer { - template - static si32 idToNumber(const T &t, typename std::enable_if::value>::type * dummy = nullptr) + template, bool> = true> + static int32_t idToNumber(const Numeric &t) { return t; } - template - static NT idToNumber(const IdentifierBase &t) + template, bool> = true> + static int32_t idToNumber(const IdentifierType &t) { return t.getNum(); } diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index c5b274005..1d5efaeea 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -295,14 +295,16 @@ public: } ///si32-convertible identifier <-> Json string - template - void serializeId(const std::string & fieldName, T & value, const U & defaultValue) + template + void serializeId(const std::string & fieldName, IdentifierType & value, const IdentifierTypeBase & defaultValue = IdentifierType::NONE) { + static_assert(std::is_base_of_v, "This method can only serialize Identifier classes!"); + if (saving) { if (value != defaultValue) { - std::string fieldValue = E::encode(value); + std::string fieldValue = IdentifierType::encode(value.getNum()); serializeString(fieldName, fieldValue); } } @@ -313,13 +315,13 @@ public: if (!fieldValue.empty()) { - VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), E::entityType(), fieldValue, [&value](int32_t index){ - value = T(index); + VLC->identifiers()->requestIdentifier(ModScope::scopeGame(), IdentifierType::entityType(), fieldValue, [&value](int32_t index){ + value = IdentifierType(index); }); } else { - value = T(defaultValue); + value = IdentifierType(defaultValue); } } } @@ -333,7 +335,7 @@ public: std::vector fieldValue; for(const T & vitem : value) - fieldValue.push_back(E::encode(vitem)); + fieldValue.push_back(E::encode(vitem.getNum())); serializeInternal(fieldName, fieldValue); } @@ -362,7 +364,7 @@ public: std::vector fieldValue; for(const T & vitem : value) - fieldValue.push_back(U::encode(vitem)); + fieldValue.push_back(U::encode(vitem.getNum())); serializeInternal(fieldName, fieldValue); } @@ -387,7 +389,24 @@ public: const TDecoder decoder = std::bind(&IInstanceResolver::decode, instanceResolver, _1); const TEncoder encoder = std::bind(&IInstanceResolver::encode, instanceResolver, _1); - serializeId(fieldName, value, defaultValue, decoder, encoder); + if (saving) + { + if (value != defaultValue) + { + std::string fieldValue = encoder(value.getNum()); + serializeString(fieldName, fieldValue); + } + } + else + { + std::string fieldValue; + serializeString(fieldName, fieldValue); + + if (!fieldValue.empty()) + value = T(decoder(fieldValue)); + else + value = T(defaultValue); + } } ///any serializable object <-> Json struct From 885dce0c2701d66e54a36564f468d247d8b385e4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 16:56:02 +0200 Subject: [PATCH 1037/1248] Replace static_cast's of Identifiers with getNum call --- lib/CHeroHandler.cpp | 2 +- lib/JsonRandom.cpp | 6 +++--- lib/battle/CBattleInfoCallback.cpp | 2 +- lib/bonuses/Limiters.cpp | 4 ++-- lib/campaign/CampaignState.cpp | 10 +++++----- lib/constants/EntityIdentifiers.cpp | 2 +- lib/constants/IdentifierBase.h | 5 ----- lib/gameState/CGameStateCampaign.cpp | 6 +++--- lib/mapObjects/CArmedInstance.cpp | 2 +- lib/mapping/CMap.cpp | 14 +++----------- lib/networkPacks/NetPacksLib.cpp | 2 +- lib/pathfinder/CGPathNode.h | 2 +- lib/rewardable/Info.cpp | 2 +- 13 files changed, 23 insertions(+), 36 deletions(-) diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 8bb900e12..f284dbf16 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -209,7 +209,7 @@ CHeroClass::CHeroClass(): void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const { - const auto & skillName = NPrimarySkill::names[static_cast(pSkill)]; + const auto & skillName = NPrimarySkill::names[pSkill.getNum()]; auto currentPrimarySkillValue = static_cast(node["primarySkills"][skillName].Integer()); //minimal value is 0 for attack and defense and 1 for spell power and knowledge auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1; diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 2e78df61f..e0c4fcc33 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -113,7 +113,7 @@ namespace JsonRandom if (value.empty() || value[0] != '@') return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value)); else - return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast(PrimarySkill::NONE))); + return PrimarySkill(loadVariable("primarySkill", value, variables, PrimarySkill::NONE.getNum())); } /// Method that allows type-specific object filtering @@ -322,7 +322,7 @@ namespace JsonRandom for(const auto & pair : value.Struct()) { PrimarySkill id = decodeKey(pair.second.meta, pair.first, variables); - ret[static_cast(id)] += loadValue(pair.second, rng, variables); + ret[id.getNum()] += loadValue(pair.second, rng, variables); } } if(value.isVector()) @@ -333,7 +333,7 @@ namespace JsonRandom PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng); defaultSkills.erase(skillID); - ret[static_cast(skillID)] += loadValue(element, rng, variables); + ret[skillID.getNum()] += loadValue(element, rng, variables); } } return ret; diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 7da7ced1d..2e8412d0a 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -1155,7 +1155,7 @@ std::pair CBattleInfoCallback::getNearestStack( BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const { - bool twoHex = VLC->creh->objects[creID]->isDoubleWide(); + bool twoHex = VLC->creatures()->getById(creID)->isDoubleWide(); int pos; if (initialPos > -1) diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index a2461b1e7..93bf63eb9 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -317,7 +317,7 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context) std::string FactionLimiter::toString() const { boost::format fmt("FactionLimiter(faction=%s)"); - fmt % VLC->factions()->getByIndex(faction)->getJsonKey(); + fmt % VLC->factions()->getById(faction)->getJsonKey(); return fmt.str(); } @@ -326,7 +326,7 @@ JsonNode FactionLimiter::toJsonNode() const JsonNode root(JsonNode::JsonType::DATA_STRUCT); root["type"].String() = "FACTION_LIMITER"; - root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getByIndex(faction)->getJsonKey())); + root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getById(faction)->getJsonKey())); return root; } diff --git a/lib/campaign/CampaignState.cpp b/lib/campaign/CampaignState.cpp index eb6144f3e..d23ffd314 100644 --- a/lib/campaign/CampaignState.cpp +++ b/lib/campaign/CampaignState.cpp @@ -73,13 +73,13 @@ ImagePath CampaignRegions::getBackgroundName() const Point CampaignRegions::getPosition(CampaignScenarioID which) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; return Point(region.xpos, region.ypos); } ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const { - auto const & region = regions[static_cast(which)]; + auto const & region = regions[which.getNum()]; static const std::string colors[2][8] = { @@ -267,7 +267,7 @@ void CampaignState::setCurrentMapAsConquered(std::vector heroe return a->getHeroStrength() > b->getHeroStrength(); }); - logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast(*currentMap), getFilename(), getNameTranslated()); + logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated()); mapsConquered.push_back(*currentMap); auto reservedHeroes = getReservedHeroes(); @@ -320,7 +320,7 @@ std::unique_ptr CampaignState::getMap(CampaignScenarioID scenarioId) const CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; return mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); } @@ -333,7 +333,7 @@ std::unique_ptr CampaignState::getMapHeader(CampaignScenarioID scena CMapService mapService; std::string scenarioName = getFilename().substr(0, getFilename().find('.')); boost::to_lower(scenarioName); - scenarioName += ':' + std::to_string(static_cast(scenarioId)); + scenarioName += ':' + std::to_string(scenarioId.getNum()); const auto & mapContent = mapPieces.find(scenarioId)->second; return mapService.loadMapHeader(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding()); } diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 722033f51..1eb5d3c9d 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -369,7 +369,7 @@ si32 FactionID::decode(const std::string & identifier) if(rawId) return rawId.value(); else - return FactionID::DEFAULT; + return FactionID::DEFAULT.getNum(); } std::string FactionID::encode(const si32 index) diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index 5ad0a8dd3..b4a8686f9 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -52,11 +52,6 @@ public: num += change; } - constexpr operator int32_t () const - { - return num; - } - friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) { return os << dt.num; diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 79ee3b3c1..076403a31 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -90,7 +90,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vectorgetBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g]; + cgh->getBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g.getNum()]; } } } @@ -375,7 +375,7 @@ std::vector CGameStateCampaign::generateCampaignHeroesT auto * heroPlaceholder = dynamic_cast(obj.get()); // only 1 field must be set - assert(heroPlaceholder->powerRank != heroPlaceholder->heroType); + assert(heroPlaceholder->powerRank.has_value() != heroPlaceholder->heroType.has_value()); if(heroPlaceholder->powerRank) placeholdersByPower.push_back(heroPlaceholder); @@ -498,7 +498,7 @@ void CGameStateCampaign::initStartingResources() std::vector people = getHumanPlayerInfo(); //players we will give resource bonus for(const PlayerSettings *ps : people) { - std::vector res; //resources we will give + std::vector res; //resources we will give switch (chosenBonus->info1) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 39f7ed971..b1108fd40 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -92,7 +92,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(auto f : factions) { - if (VLC->factions()->getByIndex(f)->getAlignment() != EAlignment::EVIL) + if (VLC->factions()->getById(f)->getAlignment() != EAlignment::EVIL) mixableFactions++; } if (mixableFactions > 0) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 46156dac6..b0c49057c 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -71,16 +71,8 @@ void CMapEvent::serializeJson(JsonSerializeFormat & handler) void CCastleEvent::serializeJson(JsonSerializeFormat & handler) { CMapEvent::serializeJson(handler); - { - std::vector temp(buildings.begin(), buildings.end()); - auto a = handler.enterArray("buildings"); - a.syncSize(temp); - for(int i = 0; i < temp.size(); ++i) - { - a.serializeInt(i, temp[i]); - buildings.insert(temp[i]); - } - } + + handler.serializeIdArray("buildings", buildings); { auto a = handler.enterArray("creatures"); a.syncSize(creatures); @@ -393,7 +385,7 @@ const CGObjectInstance * CMap::getObjectiveObjectFrom(const int3 & pos, Obj type // There is weird bug because of which sometimes heroes will not be found properly despite having correct position // Try to workaround that and find closest object that we can use - logGlobal->error("Failed to find object of type %d at %s", static_cast(type), pos.toString()); + logGlobal->error("Failed to find object of type %d at %s", type.getNum(), pos.toString()); logGlobal->error("Will try to find closest matching object"); CGObjectInstance * bestMatch = nullptr; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 4dab06f35..c7041fd2e 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -2113,7 +2113,7 @@ void BattleStart::applyGs(CGameState * gs) const info->battleID = gs->nextBattleID; info->localInit(); - gs->nextBattleID = vstd::next(gs->nextBattleID, 1); + gs->nextBattleID = BattleID(gs->nextBattleID.getNum() + 1); } void BattleNextRound::applyGs(CGameState * gs) const diff --git a/lib/pathfinder/CGPathNode.h b/lib/pathfinder/CGPathNode.h index ad6e90383..15363f419 100644 --- a/lib/pathfinder/CGPathNode.h +++ b/lib/pathfinder/CGPathNode.h @@ -197,7 +197,7 @@ struct DLL_LINKAGE CPathsInfo STRONG_INLINE CGPathNode * getNode(const int3 & coord, const ELayer layer) { - return &nodes[layer][coord.z][coord.x][coord.y]; + return &nodes[layer.getNum()][coord.z][coord.x][coord.y]; } }; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index 47600c915..cb3a61ac4 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -234,7 +234,7 @@ void Rewardable::Info::configureVariables(Rewardable::Configuration & object, CR value = JsonRandom::loadSpell(input, rng, object.variables.values).getNum(); if (category.first == "primarySkill") - value = static_cast(JsonRandom::loadPrimary(input, rng, object.variables.values)); + value = JsonRandom::loadPrimary(input, rng, object.variables.values).getNum(); if (category.first == "secondarySkill") value = JsonRandom::loadSecondary(input, rng, object.variables.values).getNum(); From 3634fb2158368c52a698aca87279487a33a79bd5 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 17:12:58 +0200 Subject: [PATCH 1038/1248] Remove int <=> Identifier comparisons --- lib/mapObjects/CGDwelling.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 8 ++++---- lib/mapObjects/CGTownInstance.h | 2 +- lib/mapObjects/CQuest.cpp | 4 ++-- lib/mapObjects/CQuest.h | 2 +- lib/mapObjects/MiscObjects.cpp | 4 ++-- lib/mapObjects/MiscObjects.h | 2 +- lib/mapping/CMap.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 10 +++++----- lib/rmg/CMapGenOptions.cpp | 12 ++++++------ lib/rmg/CMapGenOptions.h | 6 +++--- lib/rmg/CZonePlacer.cpp | 2 +- 12 files changed, 28 insertions(+), 28 deletions(-) diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 2374939c5..b26f5c03d 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -96,7 +96,7 @@ FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) std::vector potentialPicks; - for (FactionID faction(0); faction < VLC->townh->size(); ++faction) + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) if (VLC->factions()->getById(faction)->hasTown()) potentialPicks.push_back(faction); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 0f41e30e8..a58d67f56 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -372,7 +372,7 @@ bool CGTownInstance::isBonusingBuildingAdded(BuildingID bid) const { auto present = std::find_if(bonusingBuildings.begin(), bonusingBuildings.end(), [&](CGTownBuilding* building) { - return building->getBuildingType().num == bid; + return building->getBuildingType() == bid; }); return present != bonusingBuildings.end(); @@ -470,7 +470,7 @@ FactionID CGTownInstance::randomizeFaction(CRandomGenerator & rand) std::vector potentialPicks; - for (FactionID faction(0); faction < VLC->townh->size(); ++faction) + for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction) if (VLC->factions()->getById(faction)->hasTown()) potentialPicks.push_back(faction); @@ -996,9 +996,9 @@ bool CGTownInstance::hasBuilt(const BuildingID & buildingID) const return vstd::contains(builtBuildings, buildingID); } -bool CGTownInstance::hasBuilt(const BuildingID & buildingID, int townID) const +bool CGTownInstance::hasBuilt(const BuildingID & buildingID, FactionID townID) const { - if (townID == town->faction->getIndex() || townID == ETownType::ANY) + if (townID == town->faction->getId() || townID == FactionID::ANY) return hasBuilt(buildingID); return false; } diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 2a197ec55..4a5e00511 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -160,7 +160,7 @@ public: bool hasBuilt(BuildingSubID::EBuildingSubID buildingID) const; //checks if building is constructed and town has same subID bool hasBuilt(const BuildingID & buildingID) const; - bool hasBuilt(const BuildingID & buildingID, int townID) const; + bool hasBuilt(const BuildingID & buildingID, FactionID townID) const; TResources getBuildingCost(const BuildingID & buildingID) const; TResources dailyIncome() const; //calculates daily income of this town diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index c08ca9f4f..408c0ca42 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -34,7 +34,7 @@ VCMI_LIB_NAMESPACE_BEGIN -std::map > CGKeys::playerKeyMap; +std::map > CGKeys::playerKeyMap; //TODO: Remove constructor CQuest::CQuest(): @@ -724,7 +724,7 @@ void CGQuestGuard::init(CRandomGenerator & rand) configuration.info.push_back({}); configuration.info.back().visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; - configuration.info.back().reward.removeObject = subID == 0 ? true : false; + configuration.info.back().reward.removeObject = subID.getNum() == 0 ? true : false; configuration.canRefuse = true; } diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3f76d3550..bb4980611 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -166,7 +166,7 @@ protected: class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards { public: - static std::map > playerKeyMap; //[players][keysowned] + static std::map > playerKeyMap; //[players][keysowned] //SubID 0 - lightblue, 1 - green, 2 - red, 3 - darkblue, 4 - brown, 5 - purple, 6 - white, 7 - black static void reset(); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 4b9bc9604..cec1db913 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -126,7 +126,7 @@ void CGMine::initObj(CRandomGenerator & rand) bool CGMine::isAbandoned() const { - return (getObjTypeIndex() >= 7); + return subID.getNum() >= 7; } ResourceSet CGMine::dailyIncome() const @@ -467,7 +467,7 @@ void CGTeleport::addToChannel(std::map & IDs, int SubID) const +TeleportChannelID CGMonolith::findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const { for(auto obj : cb->gameState()->map->objects) { diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 5afd6f603..f89876ffc 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -225,7 +225,7 @@ public: class DLL_LINKAGE CGMonolith : public CGTeleport { - TeleportChannelID findMeChannel(const std::vector & IDs, int SubID) const; + TeleportChannelID findMeChannel(const std::vector & IDs, MapObjectSubID SubID) const; protected: void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index b0c49057c..5ba1c3abc 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -245,7 +245,7 @@ void CMap::calculateGuardingGreaturePositions() CGHeroInstance * CMap::getHero(HeroTypeID heroID) { for(auto & elem : heroesOnMap) - if(elem->getObjTypeIndex() == heroID.getNum()) + if(elem->getHeroType() == heroID) return elem; return nullptr; } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index f8402b54a..03d6bda80 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1287,7 +1287,7 @@ CGObjectInstance * CMapLoaderH3M::readResource(const int3 & mapPosition, std::sh readMessageAndGuards(object->message, object, mapPosition); object->amount = reader->readUInt32(); - if(objectTemplate->subid == GameResID(EGameResID::GOLD)) + if(GameResID(objectTemplate->subid) == GameResID(EGameResID::GOLD)) { // Gold is multiplied by 100. object->amount *= 100; @@ -1740,7 +1740,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec for(auto & elem : map->disposedHeroes) { - if(elem.heroId.getNum() == object->subID) + if(elem.heroId == object->getHeroType()) { object->nameCustomTextId = elem.name; object->customPortraitSource = elem.portrait; @@ -1750,7 +1750,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec bool hasName = reader->readBool(); if(hasName) - object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->subID, "name")); + object->nameCustomTextId = readLocalizedString(TextIdentifier("heroes", object->getHeroType().getNum(), "name")); if(features.levelSOD) { @@ -1861,8 +1861,8 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec } } - if (object->subID != -1) - logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getByIndex(object->subID)->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); + if (object->subID != MapObjectSubID()) + logGlobal->debug("Map '%s': Hero on map: %s at %s, owned by %s", mapName, VLC->heroh->getById(object->getHeroType())->getJsonKey(), mapPosition.toString(), object->getOwner().toString()); else logGlobal->debug("Map '%s': Hero on map: (random) at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().toString()); diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 1a0265327..439dfd5e6 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -144,7 +144,7 @@ void CMapGenOptions::resetPlayersMap() for(const auto & p : players) { auto town = p.second.getStartingTown(); - if (town != RANDOM_SIZE) + if (town != FactionID::RANDOM) rememberTownTypes[p.first] = FactionID(town); rememberTeam[p.first] = p.second.getTeam(); } @@ -522,17 +522,17 @@ void CMapGenOptions::CPlayerSettings::setColor(const PlayerColor & value) color = value; } -si32 CMapGenOptions::CPlayerSettings::getStartingTown() const +FactionID CMapGenOptions::CPlayerSettings::getStartingTown() const { return startingTown; } -void CMapGenOptions::CPlayerSettings::setStartingTown(si32 value) +void CMapGenOptions::CPlayerSettings::setStartingTown(FactionID value) { - assert(value >= -1); - if(value >= 0) + assert(value >= FactionID::RANDOM); + if(value != FactionID::RANDOM) { - assert(value < static_cast(VLC->townh->size())); + assert(value < FactionID(VLC->townh->size())); assert((*VLC->townh)[value]->town != nullptr); } startingTown = value; diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index 2e25fd671..a4795620d 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -42,8 +42,8 @@ public: /// The starting town of the player ranging from 0 to town max count or RANDOM_TOWN. /// The default value is RANDOM_TOWN. - si32 getStartingTown() const; - void setStartingTown(si32 value); + FactionID getStartingTown() const; + void setStartingTown(FactionID value); /// The default value is EPlayerType::AI. EPlayerType getPlayerType() const; @@ -55,7 +55,7 @@ public: private: PlayerColor color; - si32 startingTown; + FactionID startingTown; EPlayerType playerType; TeamID team; diff --git a/lib/rmg/CZonePlacer.cpp b/lib/rmg/CZonePlacer.cpp index 07c56688f..09f8276e4 100644 --- a/lib/rmg/CZonePlacer.cpp +++ b/lib/rmg/CZonePlacer.cpp @@ -442,7 +442,7 @@ void CZonePlacer::prepareZones(TZoneMap &zones, TZoneVector &zonesVector, const { auto player = PlayerColor(*owner - 1); auto playerSettings = map.getMapGenOptions().getPlayersSettings(); - si32 faction = FactionID::RANDOM; + FactionID faction = FactionID::RANDOM; if (vstd::contains(playerSettings, player)) faction = playerSettings[player].getStartingTown(); else From 8d5fa41a191d38c4528038c4801b325c9ff36f25 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 17:48:48 +0200 Subject: [PATCH 1039/1248] Minor fixes --- client/lobby/CBonusSelection.cpp | 2 +- lib/CBuildingHandler.cpp | 12 ++++----- lib/CBuildingHandler.h | 2 +- lib/CCreatureHandler.cpp | 41 +++++++++++-------------------- lib/CHeroHandler.cpp | 2 +- lib/CSkillHandler.cpp | 4 +-- lib/constants/EntityIdentifiers.h | 4 --- lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CBank.cpp | 2 +- lib/mapObjects/CGDwelling.cpp | 2 +- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/CGPandoraBox.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 4 +-- lib/mapping/CMap.cpp | 13 +++++++++- lib/mapping/MapFormatH3M.cpp | 2 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/rewardable/Reward.cpp | 2 +- lib/rmg/CMapGenOptions.cpp | 2 +- lib/rmg/CMapGenOptions.h | 2 +- 20 files changed, 51 insertions(+), 55 deletions(-) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 2566bf075..dabc75028 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -161,7 +161,7 @@ void CBonusSelection::createBonusesIcons() break; case CampaignBonusType::BUILDING: { - int faction = -1; + FactionID faction; for(auto & elem : CSH->si->playerInfos) { if(elem.second.isControlledByHuman()) diff --git a/lib/CBuildingHandler.cpp b/lib/CBuildingHandler.cpp index 6db07ac15..be986ee8b 100644 --- a/lib/CBuildingHandler.cpp +++ b/lib/CBuildingHandler.cpp @@ -12,7 +12,7 @@ VCMI_LIB_NAMESPACE_BEGIN -BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set & builtBuildings) +BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std::set & builtBuildings) { static const std::vector campToERMU = { @@ -47,13 +47,13 @@ BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set 1) + if(hordeLvlsPerTType[townType.getNum()].size() > 1) { - BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]); + BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][1]); if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built return BuildingID::HORDE_2_UPGR; diff --git a/lib/CBuildingHandler.h b/lib/CBuildingHandler.h index a0aab5615..059b9f087 100644 --- a/lib/CBuildingHandler.h +++ b/lib/CBuildingHandler.h @@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE CBuildingHandler { public: - static BuildingID campToERMU(int camp, int townType, const std::set & builtBuildings); + static BuildingID campToERMU(int camp, FactionID townType, const std::set & builtBuildings); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 2b723aac5..54c3083da 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -263,7 +263,7 @@ bool CCreature::isDoubleWide() const */ bool CCreature::isGood () const { - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD; + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::GOOD; } /** @@ -272,7 +272,7 @@ bool CCreature::isGood () const */ bool CCreature::isEvil () const { - return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL; + return VLC->factions()->getById(faction)->getAlignment() == EAlignment::EVIL; } si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought @@ -1350,34 +1350,23 @@ CCreatureHandler::~CCreatureHandler() CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const { - int r = 0; - if(tier == -1) //pick any allowed creature + std::vector allowed; + for(const auto & creature : objects) { - do - { - r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId(); - } while (objects[r] && objects[r]->special); // find first "not special" creature + if(creature->special) + continue; + + if (creature->level == tier || tier == -1) + allowed.push_back(creature->getId()); } - else + + if(allowed.empty()) { - assert(vstd::iswithin(tier, 1, 7)); - std::vector allowed; - for(const auto & creature : objects) - { - if(!creature->special && creature->level == tier) - allowed.push_back(creature->getId()); - } - - if(allowed.empty()) - { - logGlobal->warn("Cannot pick a random creature of tier %d!", tier); - return CreatureID::NONE; - } - - return *RandomGeneratorUtil::nextItem(allowed, rand); + logGlobal->warn("Cannot pick a random creature of tier %d!", tier); + return CreatureID::NONE; } - assert (r >= 0); //should always be, but it crashed - return CreatureID(r); + + return *RandomGeneratorUtil::nextItem(allowed, rand); } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index f284dbf16..a555bec93 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -151,7 +151,7 @@ bool CHeroClass::isMagicHero() const EAlignment CHeroClass::getAlignment() const { - return VLC->factions()->getByIndex(faction)->getAlignment(); + return VLC->factions()->getById(faction)->getAlignment(); } int32_t CHeroClass::getIndex() const diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index de080a1c1..1d467b846 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -120,7 +120,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInf DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill) { - out << "Skill(" << (int)skill.id << "," << skill.identifier << "): ["; + out << "Skill(" << skill.id.getNum() << "," << skill.identifier << "): ["; for(int i=0; i < skill.levels.size(); i++) out << (i ? "," : "") << skill.levels[i]; return out << "]"; @@ -233,7 +233,7 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode & skillAtLevel.iconMedium = levelNode["images"]["medium"].String(); skillAtLevel.iconLarge = levelNode["images"]["large"].String(); } - logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id); + logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum()); return skill; } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 2a0aaf2ca..8cb2537bf 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -401,10 +401,6 @@ class BuildingID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; - - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); - static std::string entityType(); }; class MapObjectBaseID : public IdentifierBase diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 4707150f4..5355ee9da 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -226,7 +226,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog // Explicitly initialize static variables for(auto & elem : players) { - CGKeys::playerKeyMap[elem.first] = std::set(); + CGKeys::playerKeyMap[elem.first] = std::set(); } for(auto & elem : teams) { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index dad8101ba..962db0023 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -271,7 +271,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const loot.appendRawString("%d %s"); loot.replaceNumber(bc->resources[it]); loot.replaceLocalString(EMetaText::RES_NAMES, it); - cb->giveResource(hero->getOwner(), static_cast(it), bc->resources[it]); + cb->giveResource(hero->getOwner(), it, bc->resources[it]); } } //grant artifacts diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index b26f5c03d..58eb91f1d 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -344,7 +344,7 @@ void CGDwelling::updateGuards() const //default condition - creatures are of level 5 or higher for (auto creatureEntry : creatures) { - if (VLC->creatures()->getByIndex(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) + if (VLC->creatures()->getById(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP) { guarded = true; break; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index d9cd4b6c6..08f4c5be9 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1679,7 +1679,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) { auto addSkill = [this](const std::string & skillId, const std::string & levelId) { - const int rawId = CSkillHandler::decodeSkill(skillId); + const int rawId = SecondarySkill::decode(skillId); if(rawId < 0) { logGlobal->error("Invalid secondary skill %s", skillId); diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 22d5aa6e6..17e05e47f 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -257,7 +257,7 @@ void CGPandoraBox::serializeJsonOptions(JsonSerializeFormat & handler) const std::string skillName = p.first; const std::string levelId = p.second.String(); - const int rawId = CSkillHandler::decodeSkill(skillName); + const int rawId = SecondarySkill::decode(skillName); if(rawId < 0) { logGlobal->error("Invalid secondary skill %s", skillName); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index a58d67f56..c5cc44551 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -584,7 +584,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const TQuantity count = creatureGrowth(i); if (!count) // no dwelling - count = VLC->creatures()->getByIndex(c)->getGrowth(); + count = VLC->creatures()->getById(c)->getGrowth(); {//no lower tiers or above current month diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index cec1db913..5909b42bf 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -119,7 +119,7 @@ void CGMine::initObj(CRandomGenerator & rand) } else { - producedResource = GameResID(getObjTypeIndex()); + producedResource = GameResID(getObjTypeIndex().getNum()); } producedQuantity = defaultResProduction(); } @@ -773,7 +773,7 @@ void CGArtifact::initObj(CRandomGenerator & rand) std::string CGArtifact::getObjectName() const { - return VLC->artifacts()->getByIndex(getArtifact())->getNameTranslated(); + return VLC->artifacts()->getById(getArtifact())->getNameTranslated(); } void CGArtifact::onHeroVisit(const CGHeroInstance * h) const diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 5ba1c3abc..e116bf4e6 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -72,7 +72,18 @@ void CCastleEvent::serializeJson(JsonSerializeFormat & handler) { CMapEvent::serializeJson(handler); - handler.serializeIdArray("buildings", buildings); + // TODO: handler.serializeIdArray("buildings", buildings); + { + std::vector temp(buildings.begin(), buildings.end()); + auto a = handler.enterArray("buildings"); + a.syncSize(temp); + for(int i = 0; i < temp.size(); ++i) + { + a.serializeInt(i, temp[i]); + buildings.insert(temp[i]); + } + } + { auto a = handler.enterArray("creatures"); a.syncSize(creatures); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 03d6bda80..70822f1e8 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -883,7 +883,7 @@ void CMapLoaderH3M::readPredefinedHeroes() } map->predefinedHeroes.emplace_back(hero); - logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getByIndex(hero->subID)->getJsonKey()); + logGlobal->debug("Map '%s': Hero predefined in map: %s", mapName, VLC->heroh->getById(hero->getHeroType())->getJsonKey()); } } diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c5ddcc26c..f435dab2d 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -831,7 +831,7 @@ void CMapFormatJson::serializeOptions(JsonSerializeFormat & handler) serializePredefinedHeroes(handler); - handler.serializeLIC("allowedAbilities", &CSkillHandler::decodeSkill, &CSkillHandler::encodeSkill, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); + handler.serializeLIC("allowedAbilities", &SecondarySkill::decode, &SecondarySkill::encode, VLC->skillh->getDefaultAllowed(), map->allowedAbilities); handler.serializeLIC("allowedArtifacts", &ArtifactID::decode, &ArtifactID::encode, VLC->arth->getDefaultAllowed(), map->allowedArtifact); diff --git a/lib/rewardable/Reward.cpp b/lib/rewardable/Reward.cpp index 135206d90..c56837e57 100644 --- a/lib/rewardable/Reward.cpp +++ b/lib/rewardable/Reward.cpp @@ -133,7 +133,7 @@ void Rewardable::Reward::serializeJson(JsonSerializeFormat & handler) std::vector> fieldValue(secondary.begin(), secondary.end()); a.serializeStruct>(fieldValue, [](JsonSerializeFormat & h, std::pair & e) { - h.serializeId("skill", e.first, SecondarySkill{}, VLC->skillh->decodeSkill, VLC->skillh->encodeSkill); + h.serializeId("skill", e.first); h.serializeId("level", e.second, 0, [](const std::string & i){return vstd::find_pos(NSecondarySkill::levels, i);}, [](si32 i){return NSecondarySkill::levels.at(i);}); }); a.syncSize(fieldValue); diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 439dfd5e6..1ceb3331d 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -187,7 +187,7 @@ const std::map & CMapGenOptions::g return players; } -void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, si32 town) +void CMapGenOptions::setStartingTownForPlayer(const PlayerColor & color, FactionID town) { auto it = players.find(color); if(it == players.end()) assert(0); diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index a4795620d..278821fbb 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -114,7 +114,7 @@ public: /// The first player colors belong to standard players and the last player colors belong to comp only players. /// All standard players are by default of type EPlayerType::AI. const std::map & getPlayersSettings() const; - void setStartingTownForPlayer(const PlayerColor & color, si32 town); + void setStartingTownForPlayer(const PlayerColor & color, FactionID town); /// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The /// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN. void setPlayerTypeForStandardPlayer(const PlayerColor & color, EPlayerType playerType); From 184f5a72cc871e4f1eea1eb7a6d88f722199f5b6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 18:45:46 +0200 Subject: [PATCH 1040/1248] Use toEntity/toXXX methods in Identifier instead VLC objects access --- client/windows/CCastleInterface.cpp | 2 +- lib/ArtifactUtils.cpp | 2 +- lib/CHeroHandler.cpp | 16 ++++++------ lib/CHeroHandler.h | 8 +----- lib/IGameCallback.cpp | 6 ++--- lib/MetaString.cpp | 12 ++++----- lib/bonuses/Bonus.cpp | 6 ++--- lib/bonuses/Limiters.cpp | 2 +- lib/constants/EntityIdentifiers.cpp | 19 +++++++++++--- lib/constants/EntityIdentifiers.h | 12 ++++++--- lib/gameState/CGameState.cpp | 10 ++++---- .../CObjectClassesHandler.cpp | 25 ++++++++----------- .../CObjectClassesHandler.h | 18 ++++++------- lib/mapObjects/CArmedInstance.cpp | 2 +- lib/mapObjects/CBank.cpp | 4 +-- lib/mapObjects/CGCreature.cpp | 4 +-- lib/mapObjects/CGDwelling.cpp | 6 ++--- lib/mapObjects/CGHeroInstance.cpp | 5 +++- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/CQuest.cpp | 4 +-- lib/mapObjects/MiscObjects.cpp | 6 ++--- lib/mapping/MapFormatH3M.cpp | 2 +- lib/mapping/MapFormatJson.cpp | 4 +-- lib/rewardable/Limiter.cpp | 2 +- lib/spells/effects/DemonSummon.cpp | 4 +-- lib/spells/effects/Dispel.cpp | 2 +- lib/spells/effects/Summon.cpp | 2 +- mapeditor/validator.cpp | 2 +- server/battles/BattleResultProcessor.cpp | 2 +- 29 files changed, 101 insertions(+), 90 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 2562fbb12..a894a752d 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1944,7 +1944,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art cancelText.appendTextID("core.genrltxt.596"); cancelText.replaceTextID(creature->getNameSingularTextID()); - std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice()); + std::string costString = std::to_string(aid.toEntity(CGI->artifacts())->getPrice()); title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 4e92ffeb2..85a72bf66 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -257,7 +257,7 @@ DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description if(sid.getNum() >= 0) { if(nameStart != std::string::npos && nameEnd != std::string::npos) - description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toSpell(VLC->spells())->getNameTranslated()); + description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toEntity(VLC->spells())->getNameTranslated()); } } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index a555bec93..904880616 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -125,15 +125,17 @@ SecondarySkill CHeroClass::chooseSecSkill(const std::set & possi { int totalProb = 0; for(const auto & possible : possibles) - { - totalProb += secSkillProbability[possible]; - } + if (secSkillProbability.count(possible) != 0) + totalProb += secSkillProbability.at(possible); + if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) { auto ran = rand.nextInt(totalProb - 1); for(const auto & possible : possibles) { - ran -= secSkillProbability[possible]; + if (secSkillProbability.count(possible) != 0) + ran -= secSkillProbability.at(possible); + if(ran < 0) { return possible; @@ -271,8 +273,6 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js int probability = static_cast(skillPair.second.Integer()); VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID) { - if(heroClass->secSkillProbability.size() <= skillID) - heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later heroClass->secSkillProbability[skillID] = probability; }); } @@ -369,11 +369,11 @@ void CHeroClassHandler::afterLoadFinalization() auto chance = static_cast(heroClass->defaultTavernChance * faction->town->defaultTavernChance); heroClass->selectionProbability[faction->getId()] = static_cast(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it } + // set default probabilities for gaining secondary skills where not loaded previously - heroClass->secSkillProbability.resize(VLC->skillh->size(), -1); for(int skillID = 0; skillID < VLC->skillh->size(); skillID++) { - if(heroClass->secSkillProbability[skillID] < 0) + if(heroClass->secSkillProbability.count(skillID) == 0) { const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)]; logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey()); diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index fbd5c24b3..dacb15928 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -157,7 +157,7 @@ public: std::vector primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level std::vector primarySkillHighLevel;// same for high levels (> 10) - std::vector secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order + std::map secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order std::map selectionProbability; //probability of selection in towns @@ -201,12 +201,6 @@ public: h & imageBattleFemale; h & imageMapMale; h & imageMapFemale; - - if(!h.saving) - { - for(int & i : secSkillProbability) - vstd::amax(i, 0); - } } EAlignment getAlignment() const; }; diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index a2b7f8d3d..25f70c0e2 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -148,11 +148,11 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const { for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]); + out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]); + out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact()); - out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]); + out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact()); } void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 93665a0e9..b56705ffe 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -114,35 +114,35 @@ std::string MetaString::getLocalString(const std::pair & txt) c { case EMetaText::ART_NAMES: { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); if(art) return art->getNameTranslated(); return "#!#"; } case EMetaText::ART_DESCR: { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); if(art) return art->getDescriptionTranslated(); return "#!#"; } case EMetaText::ART_EVNTS: { - const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); if(art) return art->getEventTranslated(); return "#!#"; } case EMetaText::CRE_PL_NAMES: { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); + const auto * cre = CreatureID(ser).toEntity(VLC->creatures()); if(cre) return cre->getNamePluralTranslated(); return "#!#"; } case EMetaText::CRE_SING_NAMES: { - const auto * cre = CreatureID(ser).toCreature(VLC->creatures()); + const auto * cre = CreatureID(ser).toEntity(VLC->creatures()); if(cre) return cre->getNameSingularTranslated(); return "#!#"; @@ -157,7 +157,7 @@ std::string MetaString::getLocalString(const std::pair & txt) c } case EMetaText::SPELL_NAME: { - const auto * spell = SpellID(ser).toSpell(VLC->spells()); + const auto * spell = SpellID(ser).toEntity(VLC->spells()); if(spell) return spell->getNameTranslated(); return "#!#"; diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index fb45cbb46..6fe775536 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -100,13 +100,13 @@ std::string Bonus::Description(std::optional customValue) const switch(source) { case BonusSource::ARTIFACT: - str << sid.as().toArtifact(VLC->artifacts())->getNameTranslated(); + str << sid.as().toEntity(VLC->artifacts())->getNameTranslated(); break; case BonusSource::SPELL_EFFECT: - str << sid.as().toSpell(VLC->spells())->getNameTranslated(); + str << sid.as().toEntity(VLC->spells())->getNameTranslated(); break; case BonusSource::CREATURE_ABILITY: - str << sid.as().toCreature(VLC->creatures())->getNamePluralTranslated(); + str << sid.as().toEntity(VLC->creatures())->getNamePluralTranslated(); break; case BonusSource::SECONDARY_SKILL: str << VLC->skills()->getById(sid.as())->getNameTranslated(); diff --git a/lib/bonuses/Limiters.cpp b/lib/bonuses/Limiters.cpp index 93bf63eb9..b0e031368 100644 --- a/lib/bonuses/Limiters.cpp +++ b/lib/bonuses/Limiters.cpp @@ -114,7 +114,7 @@ CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool Inc void CCreatureTypeLimiter::setCreature(const CreatureID & id) { - creature = VLC->creh->objects[id]; + creature = id.toCreature(); } std::string CCreatureTypeLimiter::toString() const diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 1eb5d3c9d..6b343f2b0 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -28,6 +28,7 @@ #include "modding/IdentifierStorage.h" #include "modding/ModScope.h" #include "VCMI_Lib.h" +#include "CHeroHandler.h" #include "CArtHandler.h"//todo: remove #include "CCreatureHandler.h"//todo: remove #include "spells/CSpellHandler.h" //todo: remove @@ -191,10 +192,10 @@ std::string HeroTypeID::entityType() const CArtifact * ArtifactIDBase::toArtifact() const { - return dynamic_cast(toArtifact(VLC->artifacts())); + return dynamic_cast(toEntity(VLC->artifacts())); } -const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const +const Artifact * ArtifactIDBase::toEntity(const ArtifactService * service) const { return service->getByIndex(num); } @@ -237,7 +238,7 @@ const CCreature * CreatureIDBase::toCreature() const return VLC->creh->objects.at(num); } -const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const +const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) const { return creatures->getByIndex(num); } @@ -271,7 +272,17 @@ const CSpell * SpellIDBase::toSpell() const return VLC->spellh->objects[num]; } -const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const +const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) const +{ + return service->getByIndex(num); +} + +const CHero * HeroTypeID::toHeroType() const +{ + return dynamic_cast(toEntity(VLC->heroTypes())); +} + +const HeroType * HeroTypeID::toEntity(const HeroTypeService * service) const { return service->getByIndex(num); } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 8cb2537bf..ed6d46435 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -18,6 +18,9 @@ class Artifact; class ArtifactService; class Creature; class CreatureService; +class HeroType; +class CHero; +class HeroTypeService; namespace spells { @@ -186,6 +189,9 @@ public: DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); + const CHero * toHeroType() const; + const HeroType * toEntity(const HeroTypeService * service) const; + DLL_LINKAGE static const HeroTypeID NONE; DLL_LINKAGE static const HeroTypeID RANDOM; @@ -675,7 +681,7 @@ public: }; DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const; + DLL_LINKAGE const Artifact * toEntity(const ArtifactService * service) const; }; class ArtifactID : public IdentifierWithEnum @@ -715,7 +721,7 @@ public: }; DLL_LINKAGE const CCreature * toCreature() const; - DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const; + DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const; }; class DLL_LINKAGE CreatureID : public IdentifierWithEnum @@ -833,7 +839,7 @@ public: }; const CSpell * toSpell() const; //deprecated - const spells::Spell * toSpell(const spells::Service * service) const; + const spells::Spell * toEntity(const spells::Service * service) const; }; class DLL_LINKAGE SpellID : public IdentifierWithEnum diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 5355ee9da..2526e1feb 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -566,7 +566,7 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy } } - auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, VLC->heroh->objects[heroTypeId]->heroClass->getIndex()); + auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, heroTypeId.toHeroType()->heroClass->getIndex()); CGObjectInstance * obj = handler->create(handler->getTemplates().front()); CGHeroInstance * hero = dynamic_cast(obj); @@ -630,7 +630,7 @@ void CGameState::initHeroes() hero->initHero(getRandomGenerator()); getPlayerState(hero->getOwner())->heroes.push_back(hero); - map->allHeroes[hero->getHeroType()] = hero; + map->allHeroes[hero->getHeroType().getNum()] = hero; } // generate boats for all heroes on water @@ -661,7 +661,7 @@ void CGameState::initHeroes() { auto * hero = dynamic_cast(obj.get()); hero->initHero(getRandomGenerator()); - map->allHeroes[hero->getHeroType()] = hero; + map->allHeroes[hero->getHeroType().getNum()] = hero; } } @@ -674,7 +674,7 @@ void CGameState::initHeroes() heroesPool->addHeroToPool(ph); heroesToCreate.erase(ph->type->getId()); - map->allHeroes[ph->getHeroType()] = ph; + map->allHeroes[ph->getHeroType().getNum()] = ph; } for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool @@ -760,7 +760,7 @@ void CGameState::initStartingBonus() logGlobal->error("Cannot give starting artifact - no heroes!"); break; } - const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toArtifact(VLC->artifacts()); + const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC->artifacts()); CGHeroInstance *hero = elem.second.heroes[0]; if(!giveHeroArtifact(hero, toGive->getId())) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 9d62b028a..1ef76010c 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -285,10 +285,9 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons VLC->identifiersHandler->registerObject(scope, "object", name, object->id); } -void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID) +void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL - assert(ID < objects.size()); assert(objects[ID]); if ( subID >= objects[ID]->objects.size()) @@ -298,9 +297,8 @@ void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNo loadSubObject(config.meta, identifier, config, objects[ID], subID); } -void CObjectClassesHandler::removeSubObject(si32 ID, si32 subID) +void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(ID < objects.size()); assert(objects[ID]); assert(subID < objects[ID]->objects.size()); objects[ID]->objects[subID] = nullptr; @@ -311,7 +309,7 @@ std::vector CObjectClassesHandler::getDefaultAllowed() const return std::vector(); //TODO? } -TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const +TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try { @@ -352,9 +350,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID); } -std::set CObjectClassesHandler::knownObjects() const +std::set CObjectClassesHandler::knownObjects() const { - std::set ret; + std::set ret; for(auto * entry : objects) if (entry) @@ -363,9 +361,9 @@ std::set CObjectClassesHandler::knownObjects() const return ret; } -std::set CObjectClassesHandler::knownSubObjects(si32 primaryID) const +std::set CObjectClassesHandler::knownSubObjects(MapObjectID primaryID) const { - std::set ret; + std::set ret; if (!objects.at(primaryID)) { @@ -461,7 +459,7 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG() } } -std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const +std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubID subtype) const { const auto handler = getHandlerFor(type, subtype); if (handler && handler->hasNameTextID()) @@ -470,7 +468,7 @@ std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const return objects[type]->getNameTranslated(); } -SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) const +SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const { // TODO: these objects may have subID's that does not have associated handler: // Prison: uses hero type as subID @@ -479,19 +477,18 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) co if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - assert(type < objects.size()); assert(objects[type]); assert(subtype < objects[type]->objects.size()); return getHandlerFor(type, subtype)->getSounds(); } -std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const +std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const { return objects.at(type)->handlerName; } -std::string CObjectClassesHandler::getJsonKey(si32 type) const +std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const { return objects.at(type)->getJsonKey(); } diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index f75f9c434..000b50703 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -105,8 +105,8 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data) override; void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; - void loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID); - void removeSubObject(si32 ID, si32 subID); + void loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID); + void removeSubObject(MapObjectID ID, MapObjectSubID subID); void beforeValidate(JsonNode & object) override; void afterLoadFinalization() override; @@ -114,22 +114,22 @@ public: std::vector getDefaultAllowed() const override; /// Queries to detect loaded objects - std::set knownObjects() const; - std::set knownSubObjects(si32 primaryID) const; + std::set knownObjects() const; + std::set knownSubObjects(MapObjectID primaryID) const; /// returns handler for specified object (ID-based). ObjectHandler keeps ownership - TObjectTypeHandler getHandlerFor(si32 type, si32 subtype) const; + TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const; TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const; TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const; - std::string getObjectName(si32 type, si32 subtype) const; + std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const; - SObjectSounds getObjectSounds(si32 type, si32 subtype) const; + SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const; /// Returns handler string describing the handler (for use in client) - std::string getObjectHandlerName(si32 type) const; + std::string getObjectHandlerName(MapObjectID type) const; - std::string getJsonKey(si32 type) const; + std::string getJsonKey(MapObjectID type) const; template void serialize(Handler &h, const int version) { diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index b1108fd40..4f4ce64e7 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -73,7 +73,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(const auto & slot : Slots()) { const CStackInstance * inst = slot.second; - const CCreature * creature = VLC->creh->objects[inst->getCreatureID()]; + const auto * creature = inst->getCreatureID().toEntity(VLC->creatures()); factions.insert(creature->getFaction()); // Check for undead flag instead of faction (undead mummies are neutral) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 962db0023..590e4760b 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -280,7 +280,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); loot.replaceLocalString(EMetaText::ART_NAMES, elem); - cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } //display loot if (!iw.components.empty()) @@ -314,7 +314,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const } for(const SpellID & spellId : bc->spells) { - const auto * spell = spellId.toSpell(VLC->spells()); + const auto * spell = spellId.toEntity(VLC->spells()); iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId); if(spell->getLevel() <= hero->maxSpellLevel()) { diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index b18d5dded..04af6f8ad 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -427,7 +427,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const if(!upgrades.empty()) { auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault()); - cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]); + cb->changeStackType(StackLocation(this, slotID), it->toCreature()); } } } @@ -572,7 +572,7 @@ void CGCreature::giveReward(const CGHeroInstance * h) const if(gainedArtifact != ArtifactID::NONE) { - cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(h, gainedArtifact.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact); } diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 58eb91f1d..e379901d7 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -142,7 +142,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) { const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); - if (handler->producesCreature(VLC->creh->objects[cid])) + if (handler->producesCreature(cid.toCreature())) return MapObjectSubID(entry); } return MapObjectSubID(); @@ -319,7 +319,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const else creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL); - CCreature *cre = VLC->creh->objects[creatures[i].second[0]]; + const CCreature * cre =creatures[i].second[0].toCreature(); TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH); if (creaturesAccumulate && ID != Obj::REFUGEE_CAMP) //camp should not try to accumulate different kinds of creatures sac.creatures[i].first += amount; @@ -355,7 +355,7 @@ void CGDwelling::updateGuards() const { for (auto creatureEntry : creatures) { - const CCreature * crea = VLC->creh->objects[creatureEntry.second.at(0)]; + const CCreature * crea = creatureEntry.second.at(0).toCreature(); SlotID slot = getSlotFor(crea->getId()); if (hasStackAtSlot(slot)) //stack already exists, overwrite it diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 08f4c5be9..f0b7fd24b 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -218,7 +218,10 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const if (getSecSkillLevel(which) > 0) return false; - if (type->heroClass->secSkillProbability[which] == 0) + if (type->heroClass->secSkillProbability.count(which) == 0) + return false; + + if (type->heroClass->secSkillProbability.at(which) == 0) return false; return true; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index c5cc44551..4916af24a 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -917,7 +917,7 @@ const CTown * CGTownInstance::getTown() const { if(nullptr == town) { - return (*VLC->townh)[subID]->town; + return (*VLC->townh)[getFaction()]->town; } else return town; diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 408c0ca42..761be1090 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -775,7 +775,7 @@ std::string CGKeys::getHoverText(PlayerColor player) const std::string CGKeys::getObjectName() const { - return VLC->generaltexth->tentColors[subID] + " " + CGObjectInstance::getObjectName(); + return VLC->generaltexth->tentColors[subID.getNum()] + " " + CGObjectInstance::getObjectName(); } bool CGKeymasterTent::wasVisited (PlayerColor player) const @@ -810,7 +810,7 @@ void CGBorderGuard::getRolloverText(MetaString &text, bool onHover) const { if (!onHover) { - text.appendRawString(VLC->generaltexth->tentColors[subID]); + text.appendRawString(VLC->generaltexth->tentColors[subID.getNum()]); text.appendRawString(" "); text.appendRawString(VLC->objtypeh->getObjectName(Obj::KEYMASTER, subID)); } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 5909b42bf..ca5ac7b73 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -147,7 +147,7 @@ std::string CGMine::getHoverText(PlayerColor player) const std::string hoverName = CArmedInstance::getHoverText(player); if (tempOwner != PlayerColor::NEUTRAL) - hoverName += "\n(" + VLC->generaltexth->restypes[producedResource] + ")"; + hoverName += "\n(" + VLC->generaltexth->restypes[producedResource.getNum()] + ")"; if(stacksCount()) { @@ -252,7 +252,7 @@ GameResID CGResource::resourceID() const std::string CGResource::getHoverText(PlayerColor player) const { - return VLC->generaltexth->restypes[resourceID()]; + return VLC->generaltexth->restypes[resourceID().getNum()]; } void CGResource::pickRandomObject(CRandomGenerator & rand) @@ -760,7 +760,7 @@ void CGArtifact::initObj(CRandomGenerator & rand) storedArtifact = a; } if(!storedArtifact->artType) - storedArtifact->setType(VLC->arth->objects[getArtifact()]); + storedArtifact->setType(VLC->arth->objects[getArtifact().getNum()]); } if(ID == Obj::SPELL_SCROLL) subID = 1; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 70822f1e8..ae2e84c60 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -926,7 +926,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) if(artifactID == ArtifactID::NONE) return false; - const Artifact * art = artifactID.toArtifact(VLC->artifacts()); + const Artifact * art = artifactID.toEntity(VLC->artifacts()); if(!art) { diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index f435dab2d..0f5691da8 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -171,10 +171,10 @@ namespace TriggeredEventsDetail case EMetaclass::OBJECT: { //TODO - std::set subtypes = VLC->objtypeh->knownSubObjects(type); + auto subtypes = VLC->objtypeh->knownSubObjects(type); if(!subtypes.empty()) { - si32 subtype = *subtypes.begin(); + auto subtype = *subtypes.begin(); auto handler = VLC->objtypeh->getHandlerFor(type, subtype); identifier = handler->getTypeName(); } diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 2df9a6c24..103d4ad79 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -128,7 +128,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & spell : canLearnSpells) { - if (!hero->canLearnSpell(spell.toSpell(VLC->spells()), true)) + if (!hero->canLearnSpell(spell.toEntity(VLC->spells()), true)) return false; } diff --git a/lib/spells/effects/DemonSummon.cpp b/lib/spells/effects/DemonSummon.cpp index 09f8ec79e..8f5376702 100644 --- a/lib/spells/effects/DemonSummon.cpp +++ b/lib/spells/effects/DemonSummon.cpp @@ -49,7 +49,7 @@ void DemonSummon::apply(ServerCallback * server, const Mechanics * m, const Effe break; } - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); int32_t deadCount = targetStack->unitBaseAmount(); int32_t deadTotalHealth = targetStack->getTotalHealth(); @@ -111,7 +111,7 @@ bool DemonSummon::isValidTarget(const Mechanics * m, const battle::Unit * unit) if (unit->isGhost()) return false; - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); if (unit->getTotalHealth() < creatureType->getMaxHealth()) return false; diff --git a/lib/spells/effects/Dispel.cpp b/lib/spells/effects/Dispel.cpp index 3fb418acd..8ad649f9a 100644 --- a/lib/spells/effects/Dispel.cpp +++ b/lib/spells/effects/Dispel.cpp @@ -93,7 +93,7 @@ std::shared_ptr Dispel::getBonuses(const Mechanics * m, const b { if(bonus->source == BonusSource::SPELL_EFFECT) { - const Spell * sourceSpell = bonus->sid.as().toSpell(m->spells()); + const Spell * sourceSpell = bonus->sid.as().toEntity(m->spells()); if(!sourceSpell) return false;//error diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index 432f6ab7c..9f1eb6f41 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -107,7 +107,7 @@ void Summon::apply(ServerCallback * server, const Mechanics * m, const EffectTar if(summonByHealth) { - const auto *creatureType = creature.toCreature(m->creatures()); + const auto *creatureType = creature.toEntity(m->creatures()); auto creatureMaxHealth = creatureType->getMaxHealth(); amount = static_cast(valueWithBonus / creatureMaxHealth); } diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index 25783058d..daa69ce84 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -143,7 +143,7 @@ std::list Validator::validate(const CMap * map) if(ins->storedArtifact) { if(!map->allowedSpells[ins->storedArtifact->getScrollSpellID()]) - issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toSpell(VLC->spells())->getNameTranslated().c_str()), false); + issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false); } else issues.emplace_back(QString(tr("Spell scroll %1 doesn't have instance assigned and must be removed")).arg(ins->instanceName.c_str()), true); diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 99e50d389..472895572 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -323,7 +323,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) double eagleEyeChance = finishingBattle->winnerHero->valOfBonuses(BonusType::LEARN_BATTLE_SPELL_CHANCE); for(auto & spellId : battle.getBattle()->getUsedSpells(battle.otherSide(battleResult->winner))) { - auto spell = spellId.toSpell(VLC->spells()); + auto spell = spellId.toEntity(VLC->spells()); if(spell && spell->getLevel() <= eagleEyeLevel && !finishingBattle->winnerHero->spellbookContainsSpell(spell->getId()) && gameHandler->getRandomGenerator().nextInt(99) < eagleEyeChance) cs.spells.insert(spell->getId()); } From cac37df334f44ac903fafc1ac5bf16d5e8d8c050 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 19:57:00 +0200 Subject: [PATCH 1041/1248] Remove unused constructor --- lib/ResourceSet.cpp | 13 ------------- lib/ResourceSet.h | 5 ++--- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/lib/ResourceSet.cpp b/lib/ResourceSet.cpp index e9fa85243..ab786c7fd 100644 --- a/lib/ResourceSet.cpp +++ b/lib/ResourceSet.cpp @@ -25,19 +25,6 @@ ResourceSet::ResourceSet(const JsonNode & node) container[i] = static_cast(node[GameConstants::RESOURCE_NAMES[i]].Float()); } -ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal, - TResource gems, TResource gold, TResource mithril) -{ - container[GameResID(EGameResID::WOOD)] = wood; - container[GameResID(EGameResID::MERCURY)] = mercury; - container[GameResID(EGameResID::ORE)] = ore; - container[GameResID(EGameResID::SULFUR)] = sulfur; - container[GameResID(EGameResID::CRYSTAL)] = crystal; - container[GameResID(EGameResID::GEMS)] = gems; - container[GameResID(EGameResID::GOLD)] = gold; - container[GameResID(EGameResID::MITHRIL)] = mithril; -} - void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName) { if(handler.saving && !nonZero()) diff --git a/lib/ResourceSet.h b/lib/ResourceSet.h index 676ee3730..ad844b230 100644 --- a/lib/ResourceSet.h +++ b/lib/ResourceSet.h @@ -26,12 +26,11 @@ class ResourceSet; class ResourceSet { private: - std::array container; + std::array container = {}; public: // read resources set from json. Format example: { "gold": 500, "wood":5 } DLL_LINKAGE ResourceSet(const JsonNode & node); - DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0, - TResource gems = 0, TResource gold = 0, TResource mithril = 0); + DLL_LINKAGE ResourceSet() = default; #define scalarOperator(OPSIGN) \ From f1032063bc7c9b582ce7206fdb420efc5b3ddfe8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 19:58:30 +0200 Subject: [PATCH 1042/1248] Restore implicit conversion --- lib/constants/IdentifierBase.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index b4a8686f9..5ad0a8dd3 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -52,6 +52,11 @@ public: num += change; } + constexpr operator int32_t () const + { + return num; + } + friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) { return os << dt.num; From 04aeea9b6806a7a2080cdee41337afd1946df192 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 Nov 2023 19:20:25 +0200 Subject: [PATCH 1043/1248] use toEnum instead of getNum for switch'es --- lib/CArtHandler.cpp | 2 +- lib/CTownHandler.cpp | 2 +- lib/gameState/CGameState.cpp | 2 +- lib/pathfinder/CPathfinder.cpp | 2 +- lib/rmg/modificators/ObjectManager.cpp | 4 ++-- lib/spells/ISpellMechanics.cpp | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index fbf75317d..a912854b6 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -167,7 +167,7 @@ bool CArtifact::isBig() const bool CArtifact::isTradable() const { - switch(id.getNum()) + switch(id.toEnum()) { case ArtifactID::SPELLBOOK: case ArtifactID::GRAIL: diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index fdbeb98f7..d9541d545 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -686,7 +686,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons //MODS COMPATIBILITY FOR 0.96 if(!ret->produce.nonZero()) { - switch (ret->bid.getNum()) { + switch (ret->bid.toEnum()) { break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500; break; case BuildingID::TOWN_HALL : ret->produce[EGameResID::GOLD] = 1000; break; case BuildingID::CITY_HALL : ret->produce[EGameResID::GOLD] = 2000; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 2526e1feb..2287c2089 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -931,7 +931,7 @@ void CGameState::initMapObjects() if(!obj) continue; - switch (obj->ID.getNum()) + switch(obj->ID.toEnum()) { case Obj::QUEST_GUARD: case Obj::SEER_HUT: diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index c46f9650d..61cf7aa48 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -505,7 +505,7 @@ void CPathfinderHelper::updateTurnInfo(const int Turn) bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const { - switch(layer.getNum()) + switch(layer.toEnum()) { case EPathfindingLayer::AIR: if(!options.useFlying) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index b0025d7d6..3766c2df3 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -556,7 +556,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD } } - switch (instance->object().ID.getNum()) + switch (instance->object().ID.toEnum()) { case Obj::RANDOM_TREASURE_ART: case Obj::RANDOM_MINOR_ART: //In OH3 quest artifacts have higher value than normal arts @@ -586,7 +586,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD case Obj::MONOLITH_ONE_WAY_EXIT: */ - switch (object.instances().front()->object().ID.getNum()) + switch(object.instances().front()->object().ID.toEnum()) { case Obj::WATER_WHEEL: if (auto* m = zone.getModificator()) diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index 71e6166a5..cb7cc8832 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -717,7 +717,7 @@ IAdventureSpellMechanics::IAdventureSpellMechanics(const CSpell * s) std::unique_ptr IAdventureSpellMechanics::createMechanics(const CSpell * s) { - switch (s->id.getNum()) + switch(s->id.toEnum()) { case SpellID::SUMMON_BOAT: return std::make_unique(s); From d50ebd7d5879e9e21c0d3440e1740693ba36b18b Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 4 Nov 2023 14:34:07 +0200 Subject: [PATCH 1044/1248] #3142 - fi custom campaign selection screen --- client/lobby/CSelectionBase.cpp | 6 ++++-- lib/mapObjects/CGHeroInstance.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/client/lobby/CSelectionBase.cpp b/client/lobby/CSelectionBase.cpp index 4d7f8987e..51feeaff3 100644 --- a/client/lobby/CSelectionBase.cpp +++ b/client/lobby/CSelectionBase.cpp @@ -209,7 +209,6 @@ void InfoCard::changeSelection() return; labelSaveDate->setText(mapInfo->date); - labelMapSize->setText(std::to_string(mapInfo->mapHeader->width) + "x" + std::to_string(mapInfo->mapHeader->height)); mapName->setText(mapInfo->getNameTranslated()); mapDescription->setText(mapInfo->getDescriptionTranslated()); @@ -220,8 +219,11 @@ void InfoCard::changeSelection() if(SEL->screenType == ESelectionScreen::campaignList) return; - iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); const CMapHeader * header = mapInfo->mapHeader.get(); + + labelMapSize->setText(std::to_string(header->width) + "x" + std::to_string(header->height)); + iconsMapSizes->setFrame(mapInfo->getMapSizeIconId()); + iconsVictoryCondition->setFrame(header->victoryIconIndex); labelVictoryConditionText->setText(header->victoryMessage.toString()); iconsLossCondition->setFrame(header->defeatIconIndex); diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index d9cd4b6c6..d7254799a 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1530,7 +1530,7 @@ void CGHeroInstance::afterAddToMap(CMap * map) { auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool { - return (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; + return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; }); if(existingHero != map->objects.end()) From 91c04f471802d8689db62ec868ecbcf02c8fbe79 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 4 Nov 2023 15:43:08 +0200 Subject: [PATCH 1045/1248] #3006 - fix starting move from teleport --- client/HeroMovementController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 681b8ea4c..81a625d12 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -359,7 +359,7 @@ void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & p { stopMovementSound(); logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString()); - LOCPLINT->cb->moveHero(h, nextCoord, false); + LOCPLINT->cb->moveHero(h, h->pos, false); return; } else From 5487f07d3b1356916c4f5c78b8085a93b6b24206 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 17:04:53 +0200 Subject: [PATCH 1046/1248] added toEntity overload that accepts generic Services class --- client/windows/CCastleInterface.cpp | 2 +- lib/MetaString.cpp | 12 ++++++------ lib/bonuses/Bonus.cpp | 6 +++--- lib/constants/EntityIdentifiers.cpp | 23 +++++++++++++++++------ lib/constants/EntityIdentifiers.h | 7 +++++-- lib/gameState/CGameState.cpp | 2 +- lib/mapObjects/CArmedInstance.cpp | 2 +- lib/mapObjects/CBank.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- lib/rewardable/Limiter.cpp | 2 +- lib/spells/ISpellMechanics.h | 6 ++++++ 11 files changed, 43 insertions(+), 23 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index a894a752d..5bce62993 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1944,7 +1944,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art cancelText.appendTextID("core.genrltxt.596"); cancelText.replaceTextID(creature->getNameSingularTextID()); - std::string costString = std::to_string(aid.toEntity(CGI->artifacts())->getPrice()); + std::string costString = std::to_string(aid.toEntity(CGI)->getPrice()); title = std::make_shared(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString()); costText = std::make_shared(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]); diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index b56705ffe..bd02386f2 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -114,35 +114,35 @@ std::string MetaString::getLocalString(const std::pair & txt) c { case EMetaText::ART_NAMES: { - const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC); if(art) return art->getNameTranslated(); return "#!#"; } case EMetaText::ART_DESCR: { - const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC); if(art) return art->getDescriptionTranslated(); return "#!#"; } case EMetaText::ART_EVNTS: { - const auto * art = ArtifactID(ser).toEntity(VLC->artifacts()); + const auto * art = ArtifactID(ser).toEntity(VLC); if(art) return art->getEventTranslated(); return "#!#"; } case EMetaText::CRE_PL_NAMES: { - const auto * cre = CreatureID(ser).toEntity(VLC->creatures()); + const auto * cre = CreatureID(ser).toEntity(VLC); if(cre) return cre->getNamePluralTranslated(); return "#!#"; } case EMetaText::CRE_SING_NAMES: { - const auto * cre = CreatureID(ser).toEntity(VLC->creatures()); + const auto * cre = CreatureID(ser).toEntity(VLC); if(cre) return cre->getNameSingularTranslated(); return "#!#"; @@ -157,7 +157,7 @@ std::string MetaString::getLocalString(const std::pair & txt) c } case EMetaText::SPELL_NAME: { - const auto * spell = SpellID(ser).toEntity(VLC->spells()); + const auto * spell = SpellID(ser).toEntity(VLC); if(spell) return spell->getNameTranslated(); return "#!#"; diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 6fe775536..39c0c1c44 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -100,13 +100,13 @@ std::string Bonus::Description(std::optional customValue) const switch(source) { case BonusSource::ARTIFACT: - str << sid.as().toEntity(VLC->artifacts())->getNameTranslated(); + str << sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::SPELL_EFFECT: - str << sid.as().toEntity(VLC->spells())->getNameTranslated(); + str << sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::CREATURE_ABILITY: - str << sid.as().toEntity(VLC->creatures())->getNamePluralTranslated(); + str << sid.as().toEntity(VLC)->getNamePluralTranslated(); break; case BonusSource::SECONDARY_SKILL: str << VLC->skills()->getById(sid.as())->getNameTranslated(); diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 6b343f2b0..6ad6f37a1 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -192,12 +193,12 @@ std::string HeroTypeID::entityType() const CArtifact * ArtifactIDBase::toArtifact() const { - return dynamic_cast(toEntity(VLC->artifacts())); + return dynamic_cast(toEntity(VLC)); } -const Artifact * ArtifactIDBase::toEntity(const ArtifactService * service) const +const Artifact * ArtifactIDBase::toEntity(const Services * services) const { - return service->getByIndex(num); + return services->artifacts()->getByIndex(num); } si32 ArtifactID::decode(const std::string & identifier) @@ -238,6 +239,11 @@ const CCreature * CreatureIDBase::toCreature() const return VLC->creh->objects.at(num); } +const Creature * CreatureIDBase::toEntity(const Services * services) const +{ + return toEntity(services->creatures()); +} + const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) const { return creatures->getByIndex(num); @@ -272,6 +278,11 @@ const CSpell * SpellIDBase::toSpell() const return VLC->spellh->objects[num]; } +const spells::Spell * SpellIDBase::toEntity(const Services * services) const +{ + return toEntity(services->spells()); +} + const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) const { return service->getByIndex(num); @@ -279,12 +290,12 @@ const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) con const CHero * HeroTypeID::toHeroType() const { - return dynamic_cast(toEntity(VLC->heroTypes())); + return dynamic_cast(toEntity(VLC)); } -const HeroType * HeroTypeID::toEntity(const HeroTypeService * service) const +const HeroType * HeroTypeID::toEntity(const Services * services) const { - return service->getByIndex(num); + return services->heroTypes()->getByIndex(num); } si32 SpellID::decode(const std::string & identifier) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index ed6d46435..f7aad1cdd 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -14,6 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN +class Services; class Artifact; class ArtifactService; class Creature; @@ -190,7 +191,7 @@ public: static std::string entityType(); const CHero * toHeroType() const; - const HeroType * toEntity(const HeroTypeService * service) const; + const HeroType * toEntity(const Services * services) const; DLL_LINKAGE static const HeroTypeID NONE; DLL_LINKAGE static const HeroTypeID RANDOM; @@ -681,7 +682,7 @@ public: }; DLL_LINKAGE const CArtifact * toArtifact() const; - DLL_LINKAGE const Artifact * toEntity(const ArtifactService * service) const; + DLL_LINKAGE const Artifact * toEntity(const Services * service) const; }; class ArtifactID : public IdentifierWithEnum @@ -721,6 +722,7 @@ public: }; DLL_LINKAGE const CCreature * toCreature() const; + DLL_LINKAGE const Creature * toEntity(const Services * services) const; DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const; }; @@ -839,6 +841,7 @@ public: }; const CSpell * toSpell() const; //deprecated + const spells::Spell * toEntity(const Services * service) const; const spells::Spell * toEntity(const spells::Service * service) const; }; diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 2287c2089..dac105832 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -760,7 +760,7 @@ void CGameState::initStartingBonus() logGlobal->error("Cannot give starting artifact - no heroes!"); break; } - const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC->artifacts()); + const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); CGHeroInstance *hero = elem.second.heroes[0]; if(!giveHeroArtifact(hero, toGive->getId())) diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index 4f4ce64e7..baf878870 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -73,7 +73,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() for(const auto & slot : Slots()) { const CStackInstance * inst = slot.second; - const auto * creature = inst->getCreatureID().toEntity(VLC->creatures()); + const auto * creature = inst->getCreatureID().toEntity(VLC); factions.insert(creature->getFaction()); // Check for undead flag instead of faction (undead mummies are neutral) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 590e4760b..ae8fc22d0 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -314,7 +314,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const } for(const SpellID & spellId : bc->spells) { - const auto * spell = spellId.toEntity(VLC->spells()); + const auto * spell = spellId.toEntity(VLC); iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId); if(spell->getLevel() <= hero->maxSpellLevel()) { diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index ae2e84c60..5bd59ca3b 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -926,7 +926,7 @@ bool CMapLoaderH3M::loadArtifactToSlot(CGHeroInstance * hero, int slot) if(artifactID == ArtifactID::NONE) return false; - const Artifact * art = artifactID.toEntity(VLC->artifacts()); + const Artifact * art = artifactID.toEntity(VLC); if(!art) { diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 103d4ad79..05c77909a 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -128,7 +128,7 @@ bool Rewardable::Limiter::heroAllowed(const CGHeroInstance * hero) const for(const auto & spell : canLearnSpells) { - if (!hero->canLearnSpell(spell.toEntity(VLC->spells()), true)) + if (!hero->canLearnSpell(spell.toEntity(VLC), true)) return false; } diff --git a/lib/spells/ISpellMechanics.h b/lib/spells/ISpellMechanics.h index e3185acd9..2a0f8603d 100644 --- a/lib/spells/ISpellMechanics.h +++ b/lib/spells/ISpellMechanics.h @@ -23,6 +23,7 @@ VCMI_LIB_NAMESPACE_BEGIN struct Query; class IBattleState; class CRandomGenerator; +class CreatureService; class CMap; class CGameInfoCallback; class CBattleInfoCallback; @@ -32,6 +33,11 @@ class CStack; class CGObjectInstance; class CGHeroInstance; +namespace spells +{ +class Service; +} + namespace vstd { class RNG; From fbe3e0fe1234002b2dee859119605896a245b862 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sat, 4 Nov 2023 22:15:03 +0200 Subject: [PATCH 1047/1248] CArtPlace preparation --- client/widgets/CArtifactHolder.cpp | 102 +++++++--------------- client/widgets/CArtifactHolder.h | 39 +++------ client/widgets/CArtifactsOfHeroBase.h | 2 + client/widgets/CArtifactsOfHeroMarket.cpp | 2 +- 4 files changed, 49 insertions(+), 96 deletions(-) diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index c4440aaef..3a9bfc10f 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -75,12 +75,26 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst) text = artInst->getDescription(); } -CArtPlace::CArtPlace(Point position, const CArtifactInstance * Art) - : ourArt(Art) +CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) + : ourArt(art) + , locked(false) { - image = nullptr; pos += position; pos.w = pos.h = 44; + + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + imageIndex = 0; + if(locked) + imageIndex = ArtifactID::ART_LOCK; + else if(ourArt) + imageIndex = ourArt->artType->getIconIndex(); + + image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); + image->disable(); + + selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); + selection->visible = false; } const CArtifactInstance * CArtPlace::getArt() @@ -88,26 +102,12 @@ const CArtifactInstance * CArtPlace::getArt() return ourArt; } -CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art) - : CArtPlace(position, Art), +CCommanderArtPlace::CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art) + : CArtPlace(position, art), commanderOwner(commanderOwner), commanderSlotID(artSlot.num) { - createImage(); - setArtifact(Art); -} - -void CCommanderArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); - - imageIndex = 0; - if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - if(!ourArt) - image->disable(); + setArtifact(art); } void CCommanderArtPlace::returnArtToHeroCallback() @@ -145,20 +145,12 @@ void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition) CArtPlace::showPopupWindow(cursorPosition); } -void CCommanderArtPlace::setArtifact(const CArtifactInstance * art) +CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * art) + : CArtPlace(position, art) { - setInternals(art); } -CHeroArtPlace::CHeroArtPlace(Point position, const CArtifactInstance * Art) - : CArtPlace(position, Art), - locked(false), - marked(false) -{ - createImage(); -} - -void CHeroArtPlace::lockSlot(bool on) +void CArtPlace::lockSlot(bool on) { if(locked == on) return; @@ -173,26 +165,19 @@ void CHeroArtPlace::lockSlot(bool on) image->setFrame(0); } -bool CHeroArtPlace::isLocked() +bool CArtPlace::isLocked() const { return locked; } -void CHeroArtPlace::selectSlot(bool on) +void CArtPlace::selectSlot(bool on) { - if(marked == on) - return; - - marked = on; - if(on) - selection->enable(); - else - selection->disable(); + selection->visible = on; } -bool CHeroArtPlace::isMarked() const +bool CArtPlace::isSelected() const { - return marked; + return selection->visible; } void CHeroArtPlace::clickPressed(const Point & cursorPosition) @@ -207,18 +192,13 @@ void CHeroArtPlace::showPopupWindow(const Point & cursorPosition) showPopupCallback(*this); } -void CHeroArtPlace::showAll(Canvas & to) +void CArtPlace::showAll(Canvas & to) { - if(ourArt) - { - CIntObject::showAll(to); - } - - if(marked && isActive()) - to.drawBorder(pos, Colors::BRIGHT_YELLOW); + CIntObject::showAll(to); + selection->showAll(to); } -void CHeroArtPlace::setArtifact(const CArtifactInstance * art) +void CArtPlace::setArtifact(const CArtifactInstance * art) { setInternals(art); if(art) @@ -253,24 +233,6 @@ void CHeroArtPlace::addCombinedArtInfo(std::map & arts) } } -void CHeroArtPlace::createImage() -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - si32 imageIndex = 0; - - if(locked) - imageIndex = ArtifactID::ART_LOCK; - else if(ourArt) - imageIndex = ourArt->artType->getIconIndex(); - - image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); - image->disable(); - - selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); - selection->disable(); -} - bool ArtifactUtilsClient::askToAssemble(const CGHeroInstance * hero, const ArtifactPosition & slot) { assert(hero); diff --git a/client/widgets/CArtifactHolder.h b/client/widgets/CArtifactHolder.h index 5f8648a3f..43bc4e729 100644 --- a/client/widgets/CArtifactHolder.h +++ b/client/widgets/CArtifactHolder.h @@ -19,7 +19,6 @@ class CArtifactSet; VCMI_LIB_NAMESPACE_END class CAnimImage; -class CButton; class CArtifactHolder { @@ -32,19 +31,24 @@ public: class CArtPlace : public LRClickableAreaWTextComp { +public: + CArtPlace(Point position, const CArtifactInstance * art = nullptr); + const CArtifactInstance* getArt(); + void lockSlot(bool on); + bool isLocked() const; + void selectSlot(bool on); + bool isSelected() const; + void showAll(Canvas & to) override; + void setArtifact(const CArtifactInstance * art); + protected: std::shared_ptr image; const CArtifactInstance * ourArt; int imageIndex; + std::shared_ptr selection; + bool locked; void setInternals(const CArtifactInstance * artInst); - virtual void createImage()=0; - -public: - CArtPlace(Point position, const CArtifactInstance * Art = nullptr); - const CArtifactInstance * getArt(); - - virtual void setArtifact(const CArtifactInstance * art)=0; }; class CCommanderArtPlace : public CArtPlace @@ -53,14 +57,12 @@ protected: const CGHeroInstance * commanderOwner; ArtifactPosition commanderSlotID; - void createImage() override; void returnArtToHeroCallback(); public: - CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art = nullptr); + CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * art = nullptr); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - void setArtifact(const CArtifactInstance * art) override; }; class CHeroArtPlace: public CArtPlace @@ -72,23 +74,10 @@ public: ClickFunctor leftClickCallback; ClickFunctor showPopupCallback; - CHeroArtPlace(Point position, const CArtifactInstance * Art = nullptr); - void lockSlot(bool on); - bool isLocked(); - void selectSlot(bool on); - bool isMarked() const; + CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr); void clickPressed(const Point & cursorPosition) override; void showPopupWindow(const Point & cursorPosition) override; - void showAll(Canvas & to) override; - void setArtifact(const CArtifactInstance * art) override; void addCombinedArtInfo(std::map & arts); - -protected: - std::shared_ptr selection; - bool locked; - bool marked; - - void createImage() override; }; namespace ArtifactUtilsClient diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index aeacc10fa..2fe10de74 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -11,6 +11,8 @@ #include "CArtifactHolder.h" +class CButton; + class CArtifactsOfHeroBase : public CIntObject { protected: diff --git a/client/widgets/CArtifactsOfHeroMarket.cpp b/client/widgets/CArtifactsOfHeroMarket.cpp index 1f01775b3..94d04f512 100644 --- a/client/widgets/CArtifactsOfHeroMarket.cpp +++ b/client/widgets/CArtifactsOfHeroMarket.cpp @@ -30,7 +30,7 @@ void CArtifactsOfHeroMarket::scrollBackpack(int offset) { for(auto & artPlace : backpack) { - if(artPlace->isMarked()) + if(artPlace->isSelected()) { selectArtCallback(artPlace.get()); break; From 9f9317a8a0334dd6c24dde7b36f25303ddb9122b Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 5 Nov 2023 00:53:37 +0200 Subject: [PATCH 1048/1248] CTradeBase initial commit --- client/CMakeLists.txt | 2 + client/widgets/CTradeBase.cpp | 280 ++++++++++++++++++++++++++ client/widgets/CTradeBase.h | 88 +++++++++ client/windows/CTradeWindow.cpp | 335 ++------------------------------ client/windows/CTradeWindow.h | 66 +------ 5 files changed, 395 insertions(+), 376 deletions(-) create mode 100644 client/widgets/CTradeBase.cpp create mode 100644 client/widgets/CTradeBase.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 2912d02cf..caaedd72e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -99,6 +99,7 @@ set(client_SRCS widgets/CGarrisonInt.cpp widgets/CreatureCostBox.cpp widgets/ComboBox.cpp + widgets/CTradeBase.cpp widgets/Images.cpp widgets/MiscWidgets.cpp widgets/ObjectLists.cpp @@ -265,6 +266,7 @@ set(client_HEADERS widgets/CGarrisonInt.h widgets/CreatureCostBox.h widgets/ComboBox.h + widgets/CTradeBase.h widgets/Images.h widgets/MiscWidgets.h widgets/ObjectLists.h diff --git a/client/widgets/CTradeBase.cpp b/client/widgets/CTradeBase.cpp new file mode 100644 index 000000000..2d9ea755f --- /dev/null +++ b/client/widgets/CTradeBase.cpp @@ -0,0 +1,280 @@ +/* + * CTradeBase.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 "CTradeBase.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../widgets/TextControls.h" +#include "../windows/InfoWindows.h" + +#include "../CGameInfo.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +CTradeBase::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) + : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos) + , type(EType(-1)) // set to invalid, will be corrected in setType + , id(ID) + , serial(Serial) + , left(Left) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + downSelection = false; + hlp = nullptr; + setType(Type); +} + +void CTradeBase::CTradeableItem::setType(EType newType) +{ + if(type != newType) + { + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + type = newType; + + if(getIndex() < 0) + { + image = std::make_shared(getFilename(), 0); + image->disable(); + } + else + { + image = std::make_shared(getFilename(), getIndex()); + } + } +} + +void CTradeBase::CTradeableItem::setID(int newID) +{ + if(id != newID) + { + id = newID; + if(image) + { + int index = getIndex(); + if(index < 0) + image->disable(); + else + { + image->enable(); + image->setFrame(index); + } + } + } +} + +AnimationPath CTradeBase::CTradeableItem::getFilename() +{ + switch(type) + { + case RESOURCE: + return AnimationPath::builtin("RESOURCE"); + case PLAYER: + return AnimationPath::builtin("CREST58"); + case ARTIFACT_TYPE: + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + return AnimationPath::builtin("artifact"); + case CREATURE: + return AnimationPath::builtin("TWCRPORT"); + default: + return {}; + } +} + +int CTradeBase::CTradeableItem::getIndex() +{ + if(id < 0) + return -1; + + switch(type) + { + case RESOURCE: + case PLAYER: + return id; + case ARTIFACT_TYPE: + case ARTIFACT_INSTANCE: + case ARTIFACT_PLACEHOLDER: + return CGI->artifacts()->getByIndex(id)->getIconIndex(); + case CREATURE: + return CGI->creatures()->getByIndex(id)->getIconIndex(); + default: + return -1; + } +} + +void CTradeBase::CTradeableItem::showAll(Canvas & to) +{ + Point posToBitmap; + Point posToSubCenter; + + switch (type) + { + case RESOURCE: + posToBitmap = Point(19, 9); + posToSubCenter = Point(36, 59); + break; + case CREATURE_PLACEHOLDER: + case CREATURE: + posToSubCenter = Point(29, 76); + break; + case PLAYER: + posToSubCenter = Point(31, 76); + break; + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + posToSubCenter = Point(19, 55); + if (downSelection) + posToSubCenter.y += 8; + break; + case ARTIFACT_TYPE: + posToSubCenter = Point(19, 58); + break; + } + + if(image) + { + image->moveTo(pos.topLeft() + posToBitmap); + CIntObject::showAll(to); + } + + to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); +} + +void CTradeBase::CTradeableItem::clickPressed(const Point& cursorPosition) +{ + if(clickPressedCallback) + clickPressedCallback(shared_from_this()); +} + +void CTradeBase::CTradeableItem::showAllAt(const Point& dstPos, const std::string& customSub, Canvas& to) +{ + Rect oldPos = pos; + std::string oldSub = subtitle; + downSelection = true; + + moveTo(dstPos); + subtitle = customSub; + showAll(to); + + downSelection = false; + moveTo(oldPos.topLeft()); + subtitle = oldSub; +} + +void CTradeBase::CTradeableItem::hover(bool on) +{ + if(!on) + { + GH.statusbar()->clear(); + return; + } + + switch(type) + { + case CREATURE: + case CREATURE_PLACEHOLDER: + GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); + break; + case ARTIFACT_PLACEHOLDER: + if(id < 0) + GH.statusbar()->write(CGI->generaltexth->zelp[582].first); + else + GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); + break; + } +} + +void CTradeBase::CTradeableItem::showPopupWindow(const Point& cursorPosition) +{ + switch(type) + { + case CREATURE: + case CREATURE_PLACEHOLDER: + break; + case ARTIFACT_TYPE: + case ARTIFACT_PLACEHOLDER: + //TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription. + if (id >= 0) + CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); + break; + } +} + +std::string CTradeBase::CTradeableItem::getName(int number) const +{ + switch(type) + { + case PLAYER: + return CGI->generaltexth->capColors[id]; + case RESOURCE: + return CGI->generaltexth->restypes[id]; + case CREATURE: + if (number == 1) + return CGI->creh->objects[id]->getNameSingularTranslated(); + else + return CGI->creh->objects[id]->getNamePluralTranslated(); + case ARTIFACT_TYPE: + case ARTIFACT_INSTANCE: + return CGI->artifacts()->getByIndex(id)->getNameTranslated(); + } + logGlobal->error("Invalid trade item type: %d", (int)type); + return ""; +} + +const CArtifactInstance* CTradeBase::CTradeableItem::getArtInstance() const +{ + switch(type) + { + case ARTIFACT_PLACEHOLDER: + case ARTIFACT_INSTANCE: + return hlp; + default: + return nullptr; + } +} + +void CTradeBase::CTradeableItem::setArtInstance(const CArtifactInstance * art) +{ + assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE); + hlp = art; + if(art) + setID(art->artType->getId()); + else + setID(-1); +} + +CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero) + : market(market) + , hero(hero) +{ +} + +void CTradeBase::removeItems(const std::set> & toRemove) +{ + for(auto item : toRemove) + removeItem(item); +} + +void CTradeBase::removeItem(std::shared_ptr item) +{ + items[item->left] -= item; + + if(hRight == item) + hRight.reset(); +} + +void CTradeBase::getEmptySlots(std::set> & toRemove) +{ + for(auto item : items[1]) + if(!hero->getStackCount(SlotID(item->serial))) + toRemove.insert(item); +} diff --git a/client/widgets/CTradeBase.h b/client/widgets/CTradeBase.h new file mode 100644 index 000000000..75079fe0f --- /dev/null +++ b/client/widgets/CTradeBase.h @@ -0,0 +1,88 @@ +/* + * CTradeBase.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 "Images.h" + +#include "../../lib/FunctionList.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class IMarket; +class CGHeroInstance; + +VCMI_LIB_NAMESPACE_END + +class CButton; +class CTextBox; + +class CTradeBase +{ +public: + enum EType + { + RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE + }; + + class CTradeableItem : public CIntObject, public std::enable_shared_from_this + { + std::shared_ptr image; + AnimationPath getFilename(); + int getIndex(); + public: + const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact + EType type; + int id; + const int serial; + const bool left; + std::string subtitle; //empty if default + std::function altarSlot)> clickPressedCallback; + + void setType(EType newType); + void setID(int newID); + + const CArtifactInstance* getArtInstance() const; + void setArtInstance(const CArtifactInstance * art); + + CFunctionList callback; + bool downSelection; + + void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); + + void showPopupWindow(const Point & cursorPosition) override; + void hover(bool on) override; + void showAll(Canvas & to) override; + void clickPressed(const Point & cursorPosition) override; + std::string getName(int number = -1) const; + CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial); + }; + + const IMarket * market; + const CGHeroInstance * hero; + + //all indexes: 1 = left, 0 = right + std::array>, 2> items; + + //highlighted items (nullptr if no highlight) + std::shared_ptr hLeft; + std::shared_ptr hRight; + std::shared_ptr deal; + + CTradeBase(const IMarket * market, const CGHeroInstance * hero); + void removeItems(const std::set> & toRemove); + void removeItem(std::shared_ptr item); + void getEmptySlots(std::set> & toRemove); + virtual void makeDeal() = 0; + +protected: + std::vector> labels; + std::vector> buttons; + std::vector> texts; +}; diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 2d9da6c4c..f852824a0 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -12,317 +12,28 @@ #include "../gui/CGuiHandler.h" #include "../gui/CursorHandler.h" -#include "../widgets/Images.h" #include "../render/Canvas.h" -#include "../gui/TextAlignment.h" #include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/Buttons.h" #include "../widgets/Slider.h" #include "../widgets/TextControls.h" -#include "../windows/InfoWindows.h" #include "../CGameInfo.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" -#include "../../lib/VCMI_Lib.h" -#include "../../lib/CArtHandler.h" -#include "../../lib/CCreatureHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/CHeroHandler.h" #include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGMarket.h" -#include "../../lib/networkPacks/ArtifactLocation.h" - -CTradeWindow::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial) - : CIntObject(LCLICK | HOVER | SHOW_POPUP, pos), - type(EType(-1)),// set to invalid, will be corrected in setType - id(ID), - serial(Serial), - left(Left) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - downSelection = false; - hlp = nullptr; - setType(Type); -} - -void CTradeWindow::CTradeableItem::setType(EType newType) -{ - if(type != newType) - { - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - type = newType; - - if(getIndex() < 0) - { - image = std::make_shared(getFilename(), 0); - image->disable(); - } - else - { - image = std::make_shared(getFilename(), getIndex()); - } - } -} - -void CTradeWindow::CTradeableItem::setID(int newID) -{ - if (id != newID) - { - id = newID; - if (image) - { - int index = getIndex(); - if (index < 0) - image->disable(); - else - { - image->enable(); - image->setFrame(index); - } - } - } -} - -AnimationPath CTradeWindow::CTradeableItem::getFilename() -{ - switch(type) - { - case RESOURCE: - return AnimationPath::builtin("RESOURCE"); - case PLAYER: - return AnimationPath::builtin("CREST58"); - case ARTIFACT_TYPE: - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return AnimationPath::builtin("artifact"); - case CREATURE: - return AnimationPath::builtin("TWCRPORT"); - default: - return {}; - } -} - -int CTradeWindow::CTradeableItem::getIndex() -{ - if (id < 0) - return -1; - - switch(type) - { - case RESOURCE: - case PLAYER: - return id; - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - case ARTIFACT_PLACEHOLDER: - return CGI->artifacts()->getByIndex(id)->getIconIndex(); - case CREATURE: - return CGI->creatures()->getByIndex(id)->getIconIndex(); - default: - return -1; - } -} - -void CTradeWindow::CTradeableItem::showAll(Canvas & to) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - - Point posToBitmap; - Point posToSubCenter; - - switch(type) - { - case RESOURCE: - posToBitmap = Point(19,9); - posToSubCenter = Point(36, 59); - break; - case CREATURE_PLACEHOLDER: - case CREATURE: - posToSubCenter = Point(29, 76); - // Positing of unit count is different in Altar of Sacrifice and Freelancer's Guild - if(mw->mode == EMarketMode::CREATURE_EXP && downSelection) - posToSubCenter.y += 5; - break; - case PLAYER: - posToSubCenter = Point(31, 76); - break; - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - posToSubCenter = Point(19, 55); - if(downSelection) - posToSubCenter.y += 8; - break; - case ARTIFACT_TYPE: - posToSubCenter = Point(19, 58); - break; - } - - if (image) - { - image->moveTo(pos.topLeft() + posToBitmap); - CIntObject::showAll(to); - } - - to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle); -} - -void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition) -{ - CTradeWindow *mw = dynamic_cast(parent); - assert(mw); - if(type == ARTIFACT_PLACEHOLDER) - { - CAltarWindow *aw = static_cast(mw); - const auto pickedArtInst = aw->getPickedArtifact(); - - if(pickedArtInst) - { - aw->arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); - aw->moveArtToAltar(this->shared_from_this(), pickedArtInst); - } - else if(const CArtifactInstance *art = getArtInstance()) - { - const auto hero = aw->arts->getHero(); - const auto slot = hero->getSlotByInstance(art); - assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), - ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); - aw->arts->pickedArtFromSlot = slot; - aw->arts->artifactsOnAltar.erase(art); - setID(-1); - subtitle.clear(); - aw->deal->block(!aw->arts->artifactsOnAltar.size()); - } - - aw->calcTotalExp(); - return; - } - if(left) - { - if(mw->hLeft != this->shared_from_this()) - mw->hLeft = this->shared_from_this(); - else - return; - } - else - { - if(mw->hRight != this->shared_from_this()) - mw->hRight = this->shared_from_this(); - else - return; - } - mw->selectionChanged(left); -} - -void CTradeWindow::CTradeableItem::showAllAt(const Point &dstPos, const std::string &customSub, Canvas & to) -{ - Rect oldPos = pos; - std::string oldSub = subtitle; - downSelection = true; - - moveTo(dstPos); - subtitle = customSub; - showAll(to); - - downSelection = false; - moveTo(oldPos.topLeft()); - subtitle = oldSub; -} - -void CTradeWindow::CTradeableItem::hover(bool on) -{ - if(!on) - { - GH.statusbar()->clear(); - return; - } - - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated())); - break; - case ARTIFACT_PLACEHOLDER: - if(id < 0) - GH.statusbar()->write(CGI->generaltexth->zelp[582].first); - else - GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated()); - break; - } -} - -void CTradeWindow::CTradeableItem::showPopupWindow(const Point & cursorPosition) -{ - switch(type) - { - case CREATURE: - case CREATURE_PLACEHOLDER: - //GH.statusbar->print(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->namePl)); - break; - case ARTIFACT_TYPE: - case ARTIFACT_PLACEHOLDER: - //TODO: it's would be better for market to contain actual CArtifactInstance and not just ids of certain artifact type so we can use getEffectiveDescription. - if(id >= 0) - CRClickPopup::createAndPush(CGI->artifacts()->getByIndex(id)->getDescriptionTranslated()); - break; - } -} - -std::string CTradeWindow::CTradeableItem::getName(int number) const -{ - switch(type) - { - case PLAYER: - return CGI->generaltexth->capColors[id]; - case RESOURCE: - return CGI->generaltexth->restypes[id]; - case CREATURE: - if(number == 1) - return CGI->creh->objects[id]->getNameSingularTranslated(); - else - return CGI->creh->objects[id]->getNamePluralTranslated(); - case ARTIFACT_TYPE: - case ARTIFACT_INSTANCE: - return CGI->artifacts()->getByIndex(id)->getNameTranslated(); - } - logGlobal->error("Invalid trade item type: %d", (int)type); - return ""; -} - -const CArtifactInstance * CTradeWindow::CTradeableItem::getArtInstance() const -{ - switch(type) - { - case ARTIFACT_PLACEHOLDER: - case ARTIFACT_INSTANCE: - return hlp; - default: - return nullptr; - } -} - -void CTradeWindow::CTradeableItem::setArtInstance(const CArtifactInstance *art) -{ - assert(type == ARTIFACT_PLACEHOLDER || type == ARTIFACT_INSTANCE); - hlp = art; - if(art) - setID(art->artType->getId()); - else - setID(-1); -} CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, const std::function & onWindowClosed, EMarketMode Mode): + CTradeBase(Market, Hero), CWindowObject(PLAYER_COLORED, bgName), - market(Market), onWindowClosed(onWindowClosed), - hero(Hero), readyToTrade(false) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); @@ -403,6 +114,26 @@ void CTradeWindow::initItems(bool Left) auto item = std::make_shared(pos[j].topLeft(), itemsType[Left], id, Left, j); item->pos = pos[j] + this->pos.topLeft(); + if(mode != EMarketMode::ARTIFACT_EXP) + item->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void + { + if(altarSlot->left) + { + if(hLeft != altarSlot) + hLeft = altarSlot; + else + return; + } + else + { + if(hRight != altarSlot) + hRight = altarSlot; + else + return; + } + selectionChanged(altarSlot->left); + }; + items[Left].push_back(item); } vstd::clear_pointer(ids); @@ -571,30 +302,6 @@ void CTradeWindow::close() CWindowObject::close(); } -void CTradeWindow::removeItems(const std::set> & toRemove) -{ - for(auto item : toRemove) - removeItem(item); -} - -void CTradeWindow::removeItem(std::shared_ptr item) -{ - items[item->left] -= item; - - if(hRight == item) - { - hRight.reset(); - selectionChanged(false); - } -} - -void CTradeWindow::getEmptySlots(std::set> & toRemove) -{ - for(auto item : items[1]) - if(!hero->getStackCount(SlotID(item->serial))) - toRemove.insert(item); -} - void CTradeWindow::setMode(EMarketMode Mode) { const IMarket *m = market; diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 45a8cfee5..704d8aaef 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -9,76 +9,21 @@ */ #pragma once +#include "../widgets/CTradeBase.h" #include "../widgets/CWindowWithArtifacts.h" #include "CWindowObject.h" -#include "../../lib/FunctionList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class IMarket; - -VCMI_LIB_NAMESPACE_END class CSlider; -class CTextBox; -class CPicture; class CGStatusBar; -class CTradeWindow : public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice +class CTradeWindow : public CTradeBase, public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice { public: - enum EType - { - RESOURCE, PLAYER, ARTIFACT_TYPE, CREATURE, CREATURE_PLACEHOLDER, ARTIFACT_PLACEHOLDER, ARTIFACT_INSTANCE - }; - - class CTradeableItem : public CIntObject, public std::enable_shared_from_this - { - std::shared_ptr image; - AnimationPath getFilename(); - int getIndex(); - public: - const CArtifactInstance * hlp; //holds ptr to artifact instance id type artifact - EType type; - int id; - const int serial; - const bool left; - std::string subtitle; //empty if default - - void setType(EType newType); - void setID(int newID); - - const CArtifactInstance * getArtInstance() const; - void setArtInstance(const CArtifactInstance * art); - - CFunctionList callback; - bool downSelection; - - void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to); - - void showPopupWindow(const Point & cursorPosition) override; - void hover(bool on) override; - void showAll(Canvas & to) override; - void clickPressed(const Point & cursorPosition) override; - std::string getName(int number = -1) const; - CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial); - }; - - const IMarket * market; - const CGHeroInstance * hero; - - //all indexes: 1 = left, 0 = right - std::array>, 2> items; - - //highlighted items (nullptr if no highlight) - std::shared_ptr hLeft; - std::shared_ptr hRight; EType itemsType[2]; EMarketMode mode; std::shared_ptr ok; std::shared_ptr max; - std::shared_ptr deal; std::shared_ptr slider; //for choosing amount to be exchanged bool readyToTrade; @@ -93,9 +38,6 @@ public: void initItems(bool Left); std::vector *getItemsIds(bool Left); //nullptr if default void getPositionsFor(std::vector &poss, bool Left, EType type) const; - void removeItems(const std::set> & toRemove); - void removeItem(std::shared_ptr item); - void getEmptySlots(std::set> & toRemove); void setMode(EMarketMode Mode); //mode setter void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot @@ -130,7 +72,7 @@ public: void setMax(); void sliderMoved(int to); - void makeDeal(); + void makeDeal() override; void selectionChanged(bool side) override; //true == left CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); ~CMarketplaceWindow(); @@ -171,7 +113,7 @@ public: void putOnAltar(int backpackIndex); bool putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); - void makeDeal(); + void makeDeal() override; void showAll(Canvas & to) override; void blockTrade(); From 75ebd954af814e501f18a115c71d4c9b1effc97b Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Sun, 5 Nov 2023 02:01:23 +0200 Subject: [PATCH 1049/1248] Creatures altar & Artifacts altar --- client/CMakeLists.txt | 4 + client/CPlayerInterface.cpp | 9 +- client/widgets/CAltar.cpp | 457 +++++++++++++++++++++++++++ client/widgets/CAltar.h | 98 ++++++ client/widgets/CTradeBase.cpp | 9 +- client/windows/CAltarWindow.cpp | 136 ++++++++ client/windows/CAltarWindow.h | 38 +++ client/windows/CTradeWindow.cpp | 529 ++------------------------------ client/windows/CTradeWindow.h | 57 +--- client/windows/CWindowObject.h | 3 +- 10 files changed, 777 insertions(+), 563 deletions(-) create mode 100644 client/widgets/CAltar.cpp create mode 100644 client/widgets/CAltar.h create mode 100644 client/windows/CAltarWindow.cpp create mode 100644 client/windows/CAltarWindow.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index caaedd72e..6257722a2 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -93,6 +93,7 @@ set(client_SRCS renderSDL/SDL_Extensions.cpp widgets/Buttons.cpp + widgets/CAltar.cpp widgets/CArtifactHolder.cpp widgets/CComponent.cpp widgets/CExchangeController.cpp @@ -115,6 +116,7 @@ set(client_SRCS widgets/CWindowWithArtifacts.cpp widgets/RadialMenu.cpp + windows/CAltarWindow.cpp windows/CCastleInterface.cpp windows/CCreatureWindow.cpp windows/CHeroOverview.cpp @@ -260,6 +262,7 @@ set(client_HEADERS renderSDL/SDL_PixelAccess.h widgets/Buttons.h + widgets/CAltar.h widgets/CArtifactHolder.h widgets/CComponent.h widgets/CExchangeController.h @@ -282,6 +285,7 @@ set(client_HEADERS widgets/CWindowWithArtifacts.h widgets/RadialMenu.h + windows/CAltarWindow.h windows/CCastleInterface.h windows/CCreatureWindow.h windows/CHeroOverview.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 409b0f30e..2ee8b1caf 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -48,6 +48,7 @@ #include "widgets/CComponent.h" #include "widgets/CGarrisonInt.h" +#include "windows/CAltarWindow.h" #include "windows/CCastleInterface.h" #include "windows/CCreatureWindow.h" #include "windows/CHeroWindow.h" @@ -421,7 +422,7 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, Prim if (which == PrimarySkill::EXPERIENCE) { for (auto ctw : GH.windows().findWindows()) - ctw->setExpToLevel(); + ctw->updateExpToLevel(); } else adventureInt->onHeroChanged(hero); @@ -558,10 +559,10 @@ void CPlayerInterface::garrisonsChanged(std::vector ob for (auto cgh : GH.windows().findWindows()) cgh->updateGarrisons(); - for (auto cmw : GH.windows().findWindows()) + for (auto cmw : GH.windows().findWindows()) { - if (vstd::contains(objs, cmw->hero)) - cmw->garrisonChanged(); + if(vstd::contains(objs, cmw->getHero())) + cmw->updateGarrison(); } GH.windows().totalRedraw(); diff --git a/client/widgets/CAltar.cpp b/client/widgets/CAltar.cpp new file mode 100644 index 000000000..60f27c7a4 --- /dev/null +++ b/client/widgets/CAltar.cpp @@ -0,0 +1,457 @@ +/* + * CAltarWindow.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 "CAltar.h" + +#include "../widgets/CAltar.h" +#include "../gui/CGuiHandler.h" +#include "../gui/CursorHandler.h" +#include "../widgets/Buttons.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" + +#include "../CGameInfo.h" +#include "../CPlayerInterface.h" + +#include "../../CCallback.h" + +#include "../../lib/networkPacks/ArtifactLocation.h" +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" +#include "../../lib/mapObjects/CGMarket.h" + +CAltar::CAltar(const IMarket * market, const CGHeroInstance * hero) + : CTradeBase(market, hero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); + + // Experience needed to reach next level + texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + // Total experience on the Altar + texts.emplace_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltar::makeDeal, this)); + expToLevel = std::make_shared(75, 477, FONT_SMALL, ETextAlignment::CENTER); + expForHero = std::make_shared(75, 545, FONT_SMALL, ETextAlignment::CENTER); +} + +void CAltar::deselect() +{ + hLeft = hRight = nullptr; + deal->block(true); +} + +CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) + : CAltar(market, hero) +{ + OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE); + + labels.emplace_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); + labels.emplace_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); + selectedCost = std::make_shared(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + selectedArt = std::make_shared(Point(280, 442)); + + sacrificeAllButton = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), + CGI->generaltexth->zelp[571], std::bind(&CAltar::sacrificeAll, this)); + sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); + + sacrificeBackpackButton = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), + CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this)); + sacrificeBackpackButton->block(hero->artifactsInBackpack.empty()); + + arts = std::make_shared(Point(-365, -10)); + arts->setHero(hero); + + int slotNum = 0; + for(auto & altarSlotPos : posSlotsAltar) + { + auto altarSlot = std::make_shared(altarSlotPos, EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum++); + altarSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void + { + const auto pickedArtInst = arts->getPickedArtifact(); + if(pickedArtInst) + { + arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); + moveArtToAltar(altarSlot, pickedArtInst); + } + else if(const CArtifactInstance * art = altarSlot->getArtInstance()) + { + const auto hero = arts->getHero(); + const auto slot = hero->getSlotByInstance(art); + assert(slot != ArtifactPosition::PRE_FIRST); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), + ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); + arts->pickedArtFromSlot = slot; + arts->artifactsOnAltar.erase(art); + altarSlot->setID(-1); + altarSlot->subtitle.clear(); + deal->block(!arts->artifactsOnAltar.size()); + } + calcExpAltarForHero(); + }; + altarSlot->subtitle = ""; + items.front().emplace_back(altarSlot); + } + + calcExpAltarForHero(); + deselect(); +}; + +TExpType CAltarArtifacts::calcExpAltarForHero() +{ + auto artifactsOfHero = std::dynamic_pointer_cast(arts); + TExpType expOnAltar(0); + for(const auto art : artifactsOfHero->artifactsOnAltar) + { + int dmp, expOfArt; + market->getOffer(art->artType->getId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP); + expOnAltar += expOfArt; + } + auto resultExp = hero->calculateXp(expOnAltar); + expForHero->setText(std::to_string(resultExp)); + return resultExp; +} + +void CAltarArtifacts::makeDeal() +{ + std::vector positions; + for(const auto art : arts->artifactsOnAltar) + { + positions.push_back(hero->getSlotByInstance(art)); + } + std::sort(positions.begin(), positions.end(), std::greater<>()); + + LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, {}, {}, hero); + arts->artifactsOnAltar.clear(); + + for(auto item : items[0]) + { + item->setID(-1); + item->subtitle = ""; + } + deal->block(true); + calcExpAltarForHero(); +} + +void CAltarArtifacts::sacrificeAll() +{ + std::vector> artsForMove; + for(const auto & slotInfo : arts->getHero()->artifactsWorn) + { + if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) + artsForMove.push_back(slotInfo.second.artifact); + } + for(auto artInst : artsForMove) + moveArtToAltar(nullptr, artInst); + arts->updateWornSlots(); + sacrificeBackpack(); +} + +void CAltarArtifacts::sacrificeBackpack() +{ + while(!arts->visibleArtSet.artifactsInBackpack.empty()) + { + if(!putArtOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact)) + break; + }; + calcExpAltarForHero(); +} + +void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art) +{ + if(art) + { + selectedArt->setArtifact(art); + int dmp, exp; + market->getOffer(art->getTypeId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP); + selectedCost->setText(std::to_string(hero->calculateXp(exp))); + } + else + { + selectedArt->setArtifact(nullptr); + selectedCost->setText(""); + } +} + +void CAltarArtifacts::moveArtToAltar(std::shared_ptr altarSlot, const CArtifactInstance * art) +{ + if(putArtOnAltar(altarSlot, art)) + { + CCS->curh->dragAndDropCursor(nullptr); + arts->unmarkSlots(); + } +} + +std::shared_ptr CAltarArtifacts::getAOHset() const +{ + return arts; +} + +bool CAltarArtifacts::putArtOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art) +{ + if(!art->artType->isTradable()) + { + logGlobal->warn("Cannot put special artifact on altar!"); + return false; + } + + if(!altarSlot || altarSlot->id != -1) + { + int slotIndex = -1; + while(items[0][++slotIndex]->id >= 0 && slotIndex + 1 < items[0].size()); + slotIndex = items[0][slotIndex]->id == -1 ? slotIndex : -1; + if(slotIndex < 0) + { + logGlobal->warn("No free slots on altar!"); + return false; + } + altarSlot = items[0][slotIndex]; + } + + int dmp, exp; + market->getOffer(art->artType->getId(), 0, dmp, exp, EMarketMode::ARTIFACT_EXP); + exp = static_cast(hero->calculateXp(exp)); + + arts->artifactsOnAltar.insert(art); + altarSlot->setArtInstance(art); + altarSlot->subtitle = std::to_string(exp); + + deal->block(false); + return true; +}; + +CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero) + : CAltar(market, hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + labels.emplace_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, + boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); + labels.emplace_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); + texts.emplace_back(std::make_unique(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); + lSubtitle = std::make_shared(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + rSubtitle = std::make_shared(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); + + unitsSlider = std::make_shared(Point(231, 481), 137, [this](int newVal) -> void + { + if(hLeft) + unitsOnAltar[hLeft->serial] = newVal; + if(hRight) + updateAltarSlot(hRight); + deal->block(calcExpAltarForHero() == 0); + updateControls(); + updateSubtitlesForSelected(); + }, 0, 0, 0, Orientation::HORIZONTAL); + maxUnits = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, unitsSlider)); + + unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0); + expPerUnit.resize(GameConstants::ARMY_SIZE, 0); + sacrificeAllButton = std::make_shared( + Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltar::sacrificeAll, this)); + + auto clickPressed = [this](std::shared_ptr altarSlot, std::vector> & oppositeSlots, + std::shared_ptr & hCurSide, std::shared_ptr & hOppSide) -> void + { + std::shared_ptr oppositeSlot; + for(const auto & slot : oppositeSlots) + if(slot->serial == altarSlot->serial) + { + oppositeSlot = slot; + break; + } + + if(hCurSide != altarSlot && oppositeSlot) + { + hCurSide = altarSlot; + hOppSide = oppositeSlot; + updateControls(); + updateSubtitlesForSelected(); + redraw(); + } + }; + + for(int slotIdx = 0; slotIdx < GameConstants::ARMY_SIZE; slotIdx++) + { + CreatureID creatureId = CreatureID::NONE; + if(const auto & creature = hero->getCreature(SlotID(slotIdx))) + creatureId = creature->getId(); + else + continue; + + auto heroSlot = std::make_shared(posSlotsHero[slotIdx], EType::CREATURE, creatureId.num, true, slotIdx); + heroSlot->clickPressedCallback = [this, clickPressed](std::shared_ptr altarSlot) -> void + { + clickPressed(altarSlot, items[0], hLeft, hRight); + }; + heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(slotIdx))); + items[1].emplace_back(heroSlot); + } + assert(items[1].size() <= posSlotsAltar.size()); + for(const auto & heroSlot : items[1]) + { + auto altarSlot = std::make_shared(posSlotsAltar[heroSlot->serial], EType::CREATURE_PLACEHOLDER, heroSlot->id, false, heroSlot->serial); + altarSlot->pos.w = heroSlot->pos.w; altarSlot->pos.h = heroSlot->pos.h; + altarSlot->clickPressedCallback = [this, clickPressed](std::shared_ptr altarSlot) -> void + { + clickPressed(altarSlot, items[1], hRight, hLeft); + }; + items[0].emplace_back(altarSlot); + } + + readExpValues(); + calcExpAltarForHero(); + deselect(); +}; + +void CAltarCreatures::readExpValues() +{ + int dump; + for(auto heroSlot : items[1]) + { + if(heroSlot->id >= 0) + market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP); + } +} + +void CAltarCreatures::updateControls() +{ + int sliderAmount = 0; + if(hLeft) + { + std::optional lastSlot; + for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++) + { + if(hero->getStackCount(slot) > unitsOnAltar[slot.num]) + { + if(lastSlot.has_value()) + { + lastSlot = std::nullopt; + break; + } + else + { + lastSlot = slot; + } + } + } + sliderAmount = hero->getStackCount(SlotID(hLeft->serial)); + if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial)) + sliderAmount--; + } + unitsSlider->setAmount(sliderAmount); + unitsSlider->block(!unitsSlider->getAmount()); + if(hLeft) + unitsSlider->scrollTo(unitsOnAltar[hLeft->serial]); + maxUnits->block(unitsSlider->getAmount() == 0); +} + +void CAltarCreatures::updateSubtitlesForSelected() +{ + if(hLeft) + lSubtitle->setText(std::to_string(unitsSlider->getValue())); + else + lSubtitle->setText(""); + if(hRight) + rSubtitle->setText(hRight->subtitle); + else + rSubtitle->setText(""); +} + +void CAltarCreatures::updateGarrison() +{ + std::set> empty; + getEmptySlots(empty); + removeItems(empty); + readExpValues(); + for(auto & heroSlot : items[1]) + heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(heroSlot->serial))); +} + +void CAltarCreatures::deselect() +{ + CAltar::deselect(); + unitsSlider->block(true); + maxUnits->block(true); + updateSubtitlesForSelected(); +} + +TExpType CAltarCreatures::calcExpAltarForHero() +{ + TExpType expOnAltar(0); + auto oneUnitExp = expPerUnit.begin(); + for(const auto units : unitsOnAltar) + expOnAltar += *oneUnitExp++ * units; + auto resultExp = hero->calculateXp(expOnAltar); + expForHero->setText(std::to_string(resultExp)); + return resultExp; +} + +void CAltarCreatures::makeDeal() +{ + deselect(); + unitsSlider->scrollTo(0); + expForHero->setText(std::to_string(0)); + + std::vector ids; + std::vector toSacrifice; + + for(int i = 0; i < unitsOnAltar.size(); i++) + { + if(unitsOnAltar[i]) + { + ids.push_back(i); + toSacrifice.push_back(unitsOnAltar[i]); + } + } + + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_EXP, ids, {}, toSacrifice, hero); + + for(int & units : unitsOnAltar) + units = 0; + + for(auto heroSlot : items[0]) + { + heroSlot->setType(CREATURE_PLACEHOLDER); + heroSlot->subtitle = ""; + } +} + +void CAltarCreatures::sacrificeAll() +{ + std::optional lastSlot; + for(auto heroSlot : items[1]) + { + auto stackCount = hero->getStackCount(SlotID(heroSlot->serial)); + if(stackCount > unitsOnAltar[heroSlot->serial]) + { + if(!lastSlot.has_value()) + lastSlot = SlotID(heroSlot->serial); + unitsOnAltar[heroSlot->serial] = stackCount; + } + } + assert(lastSlot.has_value()); + unitsOnAltar[lastSlot.value().num]--; + + if(hRight) + unitsSlider->scrollTo(unitsOnAltar[hRight->serial]); + for(auto altarSlot : items[0]) + updateAltarSlot(altarSlot); + updateSubtitlesForSelected(); + + deal->block(calcExpAltarForHero() == 0); +} + +void CAltarCreatures::updateAltarSlot(std::shared_ptr slot) +{ + auto units = unitsOnAltar[slot->serial]; + slot->setType(units > 0 ? CREATURE : CREATURE_PLACEHOLDER); + slot->subtitle = units > 0 ? + boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : ""; +} \ No newline at end of file diff --git a/client/widgets/CAltar.h b/client/widgets/CAltar.h new file mode 100644 index 000000000..c6afe24b8 --- /dev/null +++ b/client/widgets/CAltar.h @@ -0,0 +1,98 @@ +/* + * CAltar.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 "../widgets/CArtifactsOfHeroAltar.h" +#include "../widgets/CTradeBase.h" + +class CSlider; + +class CAltar : public CTradeBase, public CIntObject +{ +public: + std::shared_ptr expToLevel; + std::shared_ptr expForHero; + std::shared_ptr sacrificeAllButton; + + CAltar(const IMarket * market, const CGHeroInstance * hero); + virtual ~CAltar() = default; + virtual void sacrificeAll() = 0; + virtual void deselect(); + virtual TExpType calcExpAltarForHero() = 0; +}; + +class CAltarArtifacts : public CAltar +{ +public: + CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void sacrificeBackpack(); + void setSelectedArtifact(const CArtifactInstance * art); + void moveArtToAltar(std::shared_ptr, const CArtifactInstance * art); + std::shared_ptr getAOHset() const; + +private: + std::shared_ptr selectedArt; + std::shared_ptr selectedCost; + std::shared_ptr sacrificeBackpackButton; + std::shared_ptr arts; + + const std::vector posSlotsAltar = + { + Point(317, 53), Point(371, 53), Point(425, 53), + Point(479, 53), Point(533, 53), Point(317, 123), + Point(371, 123), Point(425, 123), Point(479, 123), + Point(533, 123), Point(317, 193), Point(371, 193), + Point(425, 193), Point(479, 193), Point(533, 193), + Point(317, 263), Point(371, 263), Point(425, 263), + Point(479, 263), Point(533, 263), Point(398, 333), + Point(452, 333) + }; + + bool putArtOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); +}; + +class CAltarCreatures : public CAltar +{ +public: + CAltarCreatures(const IMarket * market, const CGHeroInstance * hero); + void updateGarrison(); + void deselect() override; + TExpType calcExpAltarForHero() override; + void makeDeal() override; + void sacrificeAll() override; + void updateAltarSlot(std::shared_ptr slot); + +private: + std::shared_ptr maxUnits; + std::shared_ptr unitsSlider; + std::vector unitsOnAltar; + std::vector expPerUnit; + std::shared_ptr lSubtitle, rSubtitle; + + const std::vector posSlotsAltar = + { + Point(334, 110), Point(417, 110), Point(500, 110), + Point(334, 208), Point(417, 208), Point(500, 208), + Point(417, 306) + }; + const std::vector posSlotsHero = + { + Point(45, 110), Point(128, 110), Point(211, 110), + Point(45, 208), Point(128, 208), Point(211, 208), + Point(128, 306) + }; + + void readExpValues(); + void updateControls(); + void updateSubtitlesForSelected(); +}; diff --git a/client/widgets/CTradeBase.cpp b/client/widgets/CTradeBase.cpp index 2d9ea755f..e1e7aacde 100644 --- a/client/widgets/CTradeBase.cpp +++ b/client/widgets/CTradeBase.cpp @@ -31,6 +31,11 @@ CTradeBase::CTradeableItem::CTradeableItem(Point pos, EType Type, int ID, bool L downSelection = false; hlp = nullptr; setType(Type); + if(image) + { + this->pos.w = image->pos.w; + this->pos.h = image->pos.h; + } } void CTradeBase::CTradeableItem::setType(EType newType) @@ -124,14 +129,14 @@ void CTradeBase::CTradeableItem::showAll(Canvas & to) break; case CREATURE_PLACEHOLDER: case CREATURE: - posToSubCenter = Point(29, 76); + posToSubCenter = Point(29, 77); break; case PLAYER: posToSubCenter = Point(31, 76); break; case ARTIFACT_PLACEHOLDER: case ARTIFACT_INSTANCE: - posToSubCenter = Point(19, 55); + posToSubCenter = Point(19, 54); if (downSelection) posToSubCenter.y += 8; break; diff --git a/client/windows/CAltarWindow.cpp b/client/windows/CAltarWindow.cpp new file mode 100644 index 000000000..faabbb5ca --- /dev/null +++ b/client/windows/CAltarWindow.cpp @@ -0,0 +1,136 @@ +/* + * CAltarWindow.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 "CAltarWindow.h" + +#include "../gui/CGuiHandler.h" +#include "../render/Canvas.h" +#include "../gui/Shortcut.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" + +#include "../CGameInfo.h" + +#include "../../lib/CGeneralTextHandler.h" +#include "../../lib/CHeroHandler.h" +#include "../../lib/mapObjects/CGHeroInstance.h" + +CAltarWindow::CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode) + : CWindowObject(PLAYER_COLORED, ImagePath::builtin(mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp")) + , hero(hero) + , windowClosedCallback(onWindowClosed) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + if(mode == EMarketMode::ARTIFACT_EXP) + createAltarArtifacts(market, hero); + else if (mode == EMarketMode::CREATURE_EXP) + createAltarCreatures(market, hero); + else + close(); + updateExpToLevel(); + statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); +} + +void CAltarWindow::updateExpToLevel() +{ + altar->expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(altar->hero->exp) + 1) - altar->hero->exp)); +} + +void CAltarWindow::updateGarrison() +{ + if(auto altarCreatures = std::static_pointer_cast(altar)) + altarCreatures->updateGarrison(); +} + +const CGHeroInstance * CAltarWindow::getHero() const +{ + return hero; +} + +void CAltarWindow::close() +{ + if(windowClosedCallback) + windowClosedCallback(); + + CWindowObject::close(); +} + +void CAltarWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED); + + auto altarArtifacts = std::make_shared(market, hero); + altar = altarArtifacts; + artSets.clear(); + addSetAndCallbacks(altarArtifacts->getAOHset()); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), + CGI->generaltexth->zelp[572], std::bind(&CAltarWindow::createAltarCreatures, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::GOOD) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero) +{ + OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + + background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED); + + altar = std::make_shared(market, hero); + + changeModeButton = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), + CGI->generaltexth->zelp[580], std::bind(&CAltarWindow::createAltarArtifacts, this, market, hero)); + if(altar->hero->getAlignment() == EAlignment::EVIL) + changeModeButton->block(true); + quitButton = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), + CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN); + altar->setRedrawParent(true); + redraw(); +} + +void CAltarWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) +{ + if(!getState().has_value()) + return; + + if(auto altarArtifacts = std::static_pointer_cast(altar)) + { + if(const auto pickedArt = getPickedArtifact()) + altarArtifacts->setSelectedArtifact(pickedArt); + else + altarArtifacts->setSelectedArtifact(nullptr); + } + CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw); +} + +void CAltarWindow::showAll(Canvas & to) +{ + // This func is temporary workaround for compliance with CTradeWindow + CWindowObject::showAll(to); + + if(altar->hRight) + { + to.drawBorder(Rect::createAround(altar->hRight->pos, 1), Colors::BRIGHT_YELLOW, 2); + altar->hRight->showAllAt(altar->pos.topLeft() + Point(396, 423), "", to); + } + if(altar->hLeft) + { + to.drawBorder(Rect::createAround(altar->hLeft->pos, 1), Colors::BRIGHT_YELLOW, 2); + altar->hLeft->showAllAt(altar->pos.topLeft() + Point(150, 423), "", to); + } +} diff --git a/client/windows/CAltarWindow.h b/client/windows/CAltarWindow.h new file mode 100644 index 000000000..9d1ffe2a5 --- /dev/null +++ b/client/windows/CAltarWindow.h @@ -0,0 +1,38 @@ +/* + * CAltarWindow.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 "../widgets/CAltar.h" +#include "../widgets/CWindowWithArtifacts.h" +#include "CWindowObject.h" + +class CAltarWindow : public CWindowObject, public CWindowWithArtifacts +{ +public: + CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode); + void updateExpToLevel(); + void updateGarrison(); + const CGHeroInstance * getHero() const; + void close() override; + + void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override; + void showAll(Canvas & to) override; + +private: + const CGHeroInstance * hero; + std::shared_ptr altar; + std::shared_ptr changeModeButton; + std::shared_ptr quitButton; + std::function windowClosedCallback; + std::shared_ptr statusBar; + + void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero); + void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero); +}; diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index f852824a0..04b1c59cf 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -66,14 +66,6 @@ void CTradeWindow::initTypes() itemsType[1] = ARTIFACT_INSTANCE; itemsType[0] = RESOURCE; break; - case EMarketMode::CREATURE_EXP: - itemsType[1] = CREATURE; - itemsType[0] = CREATURE_PLACEHOLDER; - break; - case EMarketMode::ARTIFACT_EXP: - itemsType[1] = ARTIFACT_TYPE; - itemsType[0] = ARTIFACT_PLACEHOLDER; - break; } } @@ -145,26 +137,7 @@ std::vector *CTradeWindow::getItemsIds(bool Left) { std::vector *ids = nullptr; - if(mode == EMarketMode::ARTIFACT_EXP) - return new std::vector(22, -1); - - if(Left) - { - switch(itemsType[1]) - { - case CREATURE: - ids = new std::vector; - for(int i = 0; i < 7; i++) - { - if(const CCreature *c = hero->getCreature(SlotID(i))) - ids->push_back(c->getId()); - else - ids->push_back(-1); - } - break; - } - } - else + if(!Left) { switch(itemsType[0]) { @@ -186,50 +159,31 @@ std::vector *CTradeWindow::getItemsIds(bool Left) void CTradeWindow::getPositionsFor(std::vector &poss, bool Left, EType type) const { - if(mode == EMarketMode::ARTIFACT_EXP && !Left) + //seven boxes: + // X X X + // X X X + // X + int h, w, x, y, dx, dy; + int leftToRightOffset; + getBaseForPositions(type, dx, dy, x, y, h, w, !Left, leftToRightOffset); + + const std::vector tmp = { - //22 boxes, 5 in row, last row: two boxes centered - int h, w, x, y, dx, dy; - h = w = 44; - x = 317; - y = 53; - dx = 54; - dy = 70; - for (int i = 0; i < 4 ; i++) - for (int j = 0; j < 5 ; j++) - poss.push_back(Rect(x + dx*j, y + dy*i, w, h)); + Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ), + Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ), + Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) ) + }; - poss.push_back(Rect((int)(x + dx * 1.5), (y + dy * 4), w, h)); - poss.push_back(Rect((int)(x + dx * 2.5), (y + dy * 4), w, h)); - } - else + vstd::concatenate(poss, tmp); + + if(!Left) { - //seven boxes: - // X X X - // X X X - // X - int h, w, x, y, dx, dy; - int leftToRightOffset; - getBaseForPositions(type, dx, dy, x, y, h, w, !Left, leftToRightOffset); - - const std::vector tmp = - { - Rect(Point(x + 0 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 0 * dx), Point(w, h) ), - Rect(Point(x + 0 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 2 * dx, y + 1 * dy), Point(w, h) ), - Rect(Point(x + 1 * dx, y + 2 * dy), Point(w, h) ) - }; - - vstd::concatenate(poss, tmp); - - if(!Left) - { - for(Rect &r : poss) - r.x += leftToRightOffset; - } + for(Rect &r : poss) + r.x += leftToRightOffset; } } @@ -288,9 +242,9 @@ void CTradeWindow::showAll(Canvas & to) if(readyToTrade) { if(hLeft) - hLeft->showAllAt(pos.topLeft() + selectionOffset(true), selectionSubtitle(true), to); + hLeft->showAllAt(pos.topLeft() + selectionOffset(true), updateSlotSubtitle(true), to); if(hRight) - hRight->showAllAt(pos.topLeft() + selectionOffset(false), selectionSubtitle(false), to); + hRight->showAllAt(pos.topLeft() + selectionOffset(false), updateSlotSubtitle(false), to); } } @@ -315,7 +269,6 @@ void CTradeWindow::setMode(EMarketMode Mode) { case EMarketMode::CREATURE_EXP: case EMarketMode::ARTIFACT_EXP: - GH.windows().createAndPushWindow(m, h, functor, Mode); break; default: GH.windows().createAndPushWindow(m, h, functor, Mode); @@ -612,7 +565,7 @@ bool CMarketplaceWindow::printButtonFor(EMarketMode M) const } } -void CMarketplaceWindow::garrisonChanged() +void CMarketplaceWindow::updateGarrison() { if(mode != EMarketMode::CREATURE_RESOURCE) return; @@ -642,7 +595,7 @@ void CMarketplaceWindow::artifactsChanged(bool Left) redraw(); } -std::string CMarketplaceWindow::selectionSubtitle(bool Left) const +std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const { if(Left) { @@ -811,431 +764,3 @@ void CMarketplaceWindow::updateTraderText() } traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]); } - -CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode) - : CTradeWindow(ImagePath::builtin(Mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"), Market, Hero, onWindowClosed, Mode) -{ - OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - - if(Mode == EMarketMode::CREATURE_EXP) - { - //%s's Creatures - labels.push_back(std::make_shared(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, - boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated()))); - - //Altar of Sacrifice - labels.push_back(std::make_shared(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479])); - - //To sacrifice creatures, move them from your army on to the Altar and click Sacrifice - new CTextBox(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW); - - slider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); - max = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider)); - - sacrificedUnits.resize(GameConstants::ARMY_SIZE, 0); - sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this)); - - initItems(true); - mimicCres(); - } - else - { - //Sacrifice artifacts for experience - labels.push_back(std::make_shared(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477])); - //%s's Creatures - labels.push_back(std::make_shared(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478])); - - sacrificeAll = std::make_shared(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"), CGI->generaltexth->zelp[571], std::bind(&CAltarWindow::SacrificeAll,this)); - sacrificeAll->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty()); - sacrificeBackpack = std::make_shared(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"), CGI->generaltexth->zelp[570], std::bind(&CAltarWindow::SacrificeBackpack,this)); - sacrificeBackpack->block(hero->artifactsInBackpack.empty()); - - arts = std::make_shared(Point(-365, -12)); - arts->setHero(hero); - addSetAndCallbacks(arts); - - initItems(true); - initItems(false); - artIcon = std::make_shared(AnimationPath::builtin("ARTIFACT"), 0, 0, 281, 442); - artIcon->disable(); - } - - //Experience needed to reach next level - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - //Total experience on the Altar - texts.push_back(std::make_shared(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW)); - - statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); - - ok = std::make_shared(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[568], [&](){ close();}, EShortcut::GLOBAL_RETURN); - - deal = std::make_shared(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"), CGI->generaltexth->zelp[585], std::bind(&CAltarWindow::makeDeal,this)); - - if(Mode == EMarketMode::CREATURE_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CTradeWindow::setMode,this, EMarketMode::ARTIFACT_EXP)); - if(Hero->getAlignment() == ::EAlignment::EVIL) - changeMode->block(true); - buttons.push_back(changeMode); - } - else if(Mode == EMarketMode::ARTIFACT_EXP) - { - auto changeMode = std::make_shared(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CTradeWindow::setMode,this, EMarketMode::CREATURE_EXP)); - if(Hero->getAlignment() == ::EAlignment::GOOD) - changeMode->block(true); - buttons.push_back(changeMode); - } - - expPerUnit.resize(GameConstants::ARMY_SIZE, 0); - getExpValues(); - - expToLevel = std::make_shared(73, 475, FONT_SMALL, ETextAlignment::CENTER); - expOnAltar = std::make_shared(73, 543, FONT_SMALL, ETextAlignment::CENTER); - - setExpToLevel(); - calcTotalExp(); - blockTrade(); -} - -CAltarWindow::~CAltarWindow() = default; - -void CAltarWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - leftToRightOffset = 289; - x = 45; - y = 110; - w = 58; - h = 64; - dx = 83; - dy = 98; -} - -void CAltarWindow::sliderMoved(int to) -{ - if(hLeft) - sacrificedUnits[hLeft->serial] = to; - if(hRight) - updateRight(hRight); - - deal->block(!to); - calcTotalExp(); - redraw(); -} - -void CAltarWindow::makeDeal() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - blockTrade(); - slider->scrollTo(0); - - std::vector ids; - std::vector toSacrifice; - - for(int i = 0; i < sacrificedUnits.size(); i++) - { - if(sacrificedUnits[i]) - { - ids.push_back(i); - toSacrifice.push_back(sacrificedUnits[i]); - } - } - - LOCPLINT->cb->trade(market, mode, ids, {}, toSacrifice, hero); - - for(int& val : sacrificedUnits) - val = 0; - - for(auto item : items[0]) - { - item->setType(CREATURE_PLACEHOLDER); - item->subtitle = ""; - } - } - else - { - std::vector positions; - for(const CArtifactInstance * art : arts->artifactsOnAltar) - { - positions.push_back(hero->getSlotByInstance(art)); - } - std::sort(positions.begin(), positions.end(), std::greater<>()); - - LOCPLINT->cb->trade(market, mode, positions, {}, {}, hero); - arts->artifactsOnAltar.clear(); - - for(auto item : items[0]) - { - item->setID(-1); - item->subtitle = ""; - } - - //arts->scrollBackpack(0); - deal->block(true); - } - - calcTotalExp(); -} - -void CAltarWindow::SacrificeAll() -{ - if(mode == EMarketMode::CREATURE_EXP) - { - bool movedAnything = false; - for(auto item : items[1]) - sacrificedUnits[item->serial] = hero->getStackCount(SlotID(item->serial)); - - sacrificedUnits[items[1].front()->serial]--; - - for(auto item : items[0]) - { - updateRight(item); - if(item->type == CREATURE) - movedAnything = true; - } - - deal->block(!movedAnything); - calcTotalExp(); - } - else - { - std::vector> artsForMove; - for(const auto& slotInfo : arts->visibleArtSet.artifactsWorn) - { - if(!slotInfo.second.locked && slotInfo.second.artifact->artType->isTradable()) - artsForMove.push_back(slotInfo.second.artifact); - } - for(auto artInst : artsForMove) - moveArtToAltar(nullptr, artInst); - arts->updateWornSlots(); - SacrificeBackpack(); - } - redraw(); -} - -void CAltarWindow::selectionChanged(bool side) -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - int stackCount = 0; - for (int i = 0; i < GameConstants::ARMY_SIZE; i++) - if(hero->getStackCount(SlotID(i)) > sacrificedUnits[i]) - stackCount++; - - slider->setAmount(hero->getStackCount(SlotID(hLeft->serial)) - (stackCount == 1)); - slider->block(!slider->getAmount()); - slider->scrollTo(sacrificedUnits[hLeft->serial]); - max->block(!slider->getAmount()); - selectOppositeItem(side); - readyToTrade = true; - redraw(); -} - -void CAltarWindow::selectOppositeItem(bool side) -{ - bool oppositeSide = !side; - int pos = vstd::find_pos(items[side], side ? hLeft : hRight); - int oppositePos = vstd::find_pos(items[oppositeSide], oppositeSide ? hLeft : hRight); - - if(pos >= 0 && pos != oppositePos) - { - if(oppositeSide) - hLeft = items[oppositeSide][pos]; - else - hRight = items[oppositeSide][pos]; - - selectionChanged(oppositeSide); - } -} - -void CAltarWindow::mimicCres() -{ - OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE); - std::vector positions; - getPositionsFor(positions, false, CREATURE); - - for(auto item : items[1]) - { - auto hlp = std::make_shared(positions[item->serial].topLeft(), CREATURE_PLACEHOLDER, item->id, false, item->serial); - hlp->pos = positions[item->serial] + this->pos.topLeft(); - items[0].push_back(hlp); - } -} - -Point CAltarWindow::selectionOffset(bool Left) const -{ - if(Left) - return Point(150, 421); - else - return Point(396, 421); -} - -std::string CAltarWindow::selectionSubtitle(bool Left) const -{ - if(Left && slider && hLeft) - return std::to_string(slider->getValue()); - else if(!Left && hRight) - return hRight->subtitle; - else - return ""; -} - -void CAltarWindow::artifactsChanged(bool left) -{ - -} - -void CAltarWindow::garrisonChanged() -{ - if(mode != EMarketMode::CREATURE_EXP) - return; - - std::set> empty; - getEmptySlots(empty); - - removeItems(empty); - - initSubs(true); - getExpValues(); -} - -void CAltarWindow::getExpValues() -{ - int dump; - for(auto item : items[1]) - { - if(item->id >= 0) - market->getOffer(item->id, 0, dump, expPerUnit[item->serial], EMarketMode::CREATURE_EXP); - } -} - -void CAltarWindow::calcTotalExp() -{ - int val = 0; - if(mode == EMarketMode::CREATURE_EXP) - { - for (int i = 0; i < sacrificedUnits.size(); i++) - { - val += expPerUnit[i] * sacrificedUnits[i]; - } - } - else - { - auto artifactsOfHero = std::dynamic_pointer_cast(arts); - for(const CArtifactInstance * art : artifactsOfHero->artifactsOnAltar) - { - int dmp, valOfArt; - market->getOffer(art->artType->getId(), 0, dmp, valOfArt, mode); - val += valOfArt; //WAS val += valOfArt * arts->artifactsOnAltar.count(*i); - } - } - val = static_cast(hero->calculateXp(val)); - expOnAltar->setText(std::to_string(val)); -} - -void CAltarWindow::setExpToLevel() -{ - expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(hero->exp)+1) - hero->exp)); -} - -void CAltarWindow::blockTrade() -{ - hLeft = hRight = nullptr; - readyToTrade = false; - if(slider) - { - slider->block(true); - max->block(true); - } - deal->block(true); -} - -void CAltarWindow::updateRight(std::shared_ptr toUpdate) -{ - int val = sacrificedUnits[toUpdate->serial]; - toUpdate->setType(val ? CREATURE : CREATURE_PLACEHOLDER); - toUpdate->subtitle = val ? boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(val * expPerUnit[toUpdate->serial]))) : ""; //%s exp -} - -int CAltarWindow::firstFreeSlot() -{ - int ret = -1; - while(items[0][++ret]->id >= 0 && ret + 1 < items[0].size()); - return items[0][ret]->id == -1 ? ret : -1; -} - -void CAltarWindow::SacrificeBackpack() -{ - while(!arts->visibleArtSet.artifactsInBackpack.empty()) - { - if(!putOnAltar(nullptr, arts->visibleArtSet.artifactsInBackpack[0].artifact)) - break; - }; - calcTotalExp(); -} - -void CAltarWindow::artifactPicked() -{ - redraw(); -} - -void CAltarWindow::showAll(Canvas & to) -{ - CTradeWindow::showAll(to); - if(mode == EMarketMode::ARTIFACT_EXP && arts) - { - if(auto pickedArt = arts->getPickedArtifact()) - { - artIcon->setFrame(pickedArt->artType->getIconIndex()); - artIcon->showAll(to); - - int dmp, val; - market->getOffer(pickedArt->getTypeId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - to.drawText(Point(304, 498), FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, std::to_string(val)); - } - } -} - -bool CAltarWindow::putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(!art->artType->isTradable()) //special art - { - logGlobal->warn("Cannot put special artifact on altar!"); - return false; - } - - if(!altarSlot || altarSlot->id != -1) - { - int slotIndex = firstFreeSlot(); - if(slotIndex < 0) - { - logGlobal->warn("No free slots on altar!"); - return false; - } - altarSlot = items[0][slotIndex]; - } - - int dmp, val; - market->getOffer(art->artType->getId(), 0, dmp, val, EMarketMode::ARTIFACT_EXP); - val = static_cast(hero->calculateXp(val)); - - arts->artifactsOnAltar.insert(art); - arts->deleteFromVisible(art); - altarSlot->setArtInstance(art); - altarSlot->subtitle = std::to_string(val); - - deal->block(false); - return true; -} - -void CAltarWindow::moveArtToAltar(std::shared_ptr altarSlot, const CArtifactInstance *art) -{ - if(putOnAltar(altarSlot, art)) - { - CCS->curh->dragAndDropCursor(nullptr); - arts->unmarkSlots(); - } -} diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 704d8aaef..9c0763d68 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -45,16 +45,13 @@ public: virtual void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const = 0; virtual void selectionChanged(bool side) = 0; //true == left virtual Point selectionOffset(bool Left) const = 0; - virtual std::string selectionSubtitle(bool Left) const = 0; - virtual void garrisonChanged() = 0; + virtual std::string updateSlotSubtitle(bool Left) const = 0; + virtual void updateGarrison() = 0; virtual void artifactsChanged(bool left) = 0; protected: std::function onWindowClosed; std::shared_ptr statusBar; - std::vector> labels; std::vector> images; - std::vector> buttons; - std::vector> texts; }; class CMarketplaceWindow : public CTradeWindow @@ -78,58 +75,12 @@ public: ~CMarketplaceWindow(); Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; + std::string updateSlotSubtitle(bool Left) const override; - void garrisonChanged() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) + void updateGarrison() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) void artifactsChanged(bool left) override; void resourceChanged(); void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; void updateTraderText(); }; - -class CAltarWindow : public CTradeWindow -{ - std::shared_ptr artIcon; -public: - std::vector sacrificedUnits; //[slot_nr] -> how many creatures from that slot will be sacrificed - std::vector expPerUnit; - - std::shared_ptr sacrificeAll; - std::shared_ptr sacrificeBackpack; - std::shared_ptr expToLevel; - std::shared_ptr expOnAltar; - std::shared_ptr arts; - - CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function & onWindowClosed, EMarketMode Mode); - ~CAltarWindow(); - - void getExpValues(); - - void selectionChanged(bool side) override; //true == left - void selectOppositeItem(bool side); - void SacrificeAll(); - void SacrificeBackpack(); - - void putOnAltar(int backpackIndex); - bool putOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); - void makeDeal() override; - void showAll(Canvas & to) override; - - void blockTrade(); - void sliderMoved(int to); - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; - void mimicCres(); - - Point selectionOffset(bool Left) const override; - std::string selectionSubtitle(bool Left) const override; - void garrisonChanged() override; - void artifactsChanged(bool left) override; - void calcTotalExp(); - void setExpToLevel(); - void updateRight(std::shared_ptr toUpdate); - - void artifactPicked(); - int firstFreeSlot(); - void moveArtToAltar(std::shared_ptr, const CArtifactInstance * art); -}; diff --git a/client/windows/CWindowObject.h b/client/windows/CWindowObject.h index 4746f52c8..52111aee6 100644 --- a/client/windows/CWindowObject.h +++ b/client/windows/CWindowObject.h @@ -16,8 +16,6 @@ class CGStatusBar; class CWindowObject : public WindowBase { - std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); - std::vector> shadowParts; void setShadow(bool on); @@ -32,6 +30,7 @@ protected: //To display border void updateShadow(); void setBackground(const ImagePath & filename); + std::shared_ptr createBg(const ImagePath & imageName, bool playerColored); public: enum EOptions { From 539b2e596de2be13ba2853d05c68f5eb2cdd9e76 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 6 Nov 2023 14:13:10 +0200 Subject: [PATCH 1050/1248] adjusting the placement of artifact slots --- client/widgets/CAltar.cpp | 2 +- client/widgets/CArtifactHolder.cpp | 2 +- client/widgets/CArtifactsOfHeroAltar.cpp | 7 +++++++ client/widgets/CArtifactsOfHeroBase.h | 12 ++++++------ client/windows/GUIClasses.cpp | 4 ++-- 5 files changed, 17 insertions(+), 10 deletions(-) diff --git a/client/widgets/CAltar.cpp b/client/widgets/CAltar.cpp index 60f27c7a4..338297456 100644 --- a/client/widgets/CAltar.cpp +++ b/client/widgets/CAltar.cpp @@ -66,7 +66,7 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this)); sacrificeBackpackButton->block(hero->artifactsInBackpack.empty()); - arts = std::make_shared(Point(-365, -10)); + arts = std::make_shared(Point(-365, -11)); arts->setHero(hero); int slotNum = 0; diff --git a/client/widgets/CArtifactHolder.cpp b/client/widgets/CArtifactHolder.cpp index 3a9bfc10f..334cd9f6e 100644 --- a/client/widgets/CArtifactHolder.cpp +++ b/client/widgets/CArtifactHolder.cpp @@ -93,7 +93,7 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * art) image = std::make_shared(AnimationPath::builtin("artifact"), imageIndex); image->disable(); - selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION); + selection = std::make_shared(AnimationPath::builtin("artifact"), ArtifactID::ART_SELECTION, 0, -1, -1); selection->visible = false; } diff --git a/client/widgets/CArtifactsOfHeroAltar.cpp b/client/widgets/CArtifactsOfHeroAltar.cpp index 5c33ee05c..a104974a2 100644 --- a/client/widgets/CArtifactsOfHeroAltar.cpp +++ b/client/widgets/CArtifactsOfHeroAltar.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "CArtifactsOfHeroAltar.h" +#include "Buttons.h" #include "../CPlayerInterface.h" #include "../../CCallback.h" @@ -27,6 +28,12 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position) position, std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1)); pickedArtFromSlot = ArtifactPosition::PRE_FIRST; + + // The backpack is in the altar window above and to the right + for(auto & slot : backpack) + slot->moveBy(Point(2, -1)); + leftBackpackRoll->moveBy(Point(2, -1)); + rightBackpackRoll->moveBy(Point(2, -1)); }; CArtifactsOfHeroAltar::~CArtifactsOfHeroAltar() diff --git a/client/widgets/CArtifactsOfHeroBase.h b/client/widgets/CArtifactsOfHeroBase.h index 2fe10de74..a85b8ebff 100644 --- a/client/widgets/CArtifactsOfHeroBase.h +++ b/client/widgets/CArtifactsOfHeroBase.h @@ -54,13 +54,13 @@ protected: const std::vector slotPos = { - Point(509,30), Point(567,240), Point(509,80), //0-2 - Point(383,68), Point(564,183), Point(509,130), //3-5 - Point(431,68), Point(610,183), Point(515,295), //6-8 - Point(383,143), Point(399,194), Point(415,245), //9-11 - Point(431,296), Point(564,30), Point(610,30), //12-14 + Point(509,30), Point(568,242), Point(509,80), //0-2 + Point(383,69), Point(562,184), Point(509,131), //3-5 + Point(431,69), Point(610,184), Point(515,295), //6-8 + Point(383,143), Point(399,193), Point(415,244), //9-11 + Point(431,295), Point(564,30), Point(610,30), //12-14 Point(610,76), Point(610,122), Point(610,310), //15-17 - Point(381,296) //18 + Point(381,295) //18 }; virtual void init(CHeroArtPlace::ClickFunctor lClickCallback, CHeroArtPlace::ClickFunctor showPopupCallback, diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 34824e619..0d0a1e710 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -677,9 +677,9 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, manaValues[leftRight] = std::make_shared(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); } - artifs[0] = std::make_shared(Point(-334, 150)); + artifs[0] = std::make_shared(Point(-334, 151)); artifs[0]->setHero(heroInst[0]); - artifs[1] = std::make_shared(Point(98, 150)); + artifs[1] = std::make_shared(Point(98, 151)); artifs[1]->setHero(heroInst[1]); addSetAndCallbacks(artifs[0]); From afc0d8665ac2a891a0c7c8d24efa394b95b21883 Mon Sep 17 00:00:00 2001 From: gamestales-com Date: Mon, 6 Nov 2023 13:29:36 +0100 Subject: [PATCH 1051/1248] #3151-windows-build-symlinks --- cmake_modules/VCMI_lib.cmake | 4 ++-- cmake_modules/create_link.cmake | 14 ++++++++++++++ launcher/CMakeLists.txt | 7 +++---- mapeditor/CMakeLists.txt | 4 ++-- 4 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 cmake_modules/create_link.cmake diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 4654082d6..85515c7c1 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -686,8 +686,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) add_custom_command(TARGET ${TARGET_NAME} POST_BUILD COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config - COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods ) endif() diff --git a/cmake_modules/create_link.cmake b/cmake_modules/create_link.cmake new file mode 100644 index 000000000..c5ea762ad --- /dev/null +++ b/cmake_modules/create_link.cmake @@ -0,0 +1,14 @@ + +#message(${CMAKE_ARGV0}) # cmake.exe +#message(${CMAKE_ARGV1}) # -P +#message(${CMAKE_ARGV2}) # thisfilename +#message(${CMAKE_ARGV3}) # existing +#message(${CMAKE_ARGV4}) # linkname +if (WIN32) + file(TO_NATIVE_PATH ${CMAKE_ARGV3} existing_native) + file(TO_NATIVE_PATH ${CMAKE_ARGV4} linkname_native) + execute_process(COMMAND cmd.exe /c RD /Q "${linkname_native}") + execute_process(COMMAND cmd.exe /c mklink /J "${linkname_native}" "${existing_native}") +else() + execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_ARGV3} ${CMAKE_ARGV4}) +endif() diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index f21ad852b..5ab52821f 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -174,12 +174,11 @@ if(APPLE_IOS) else() set(RESOURCES_DESTINATION ${DATA_DIR}/launcher) - # Copy to build directory for easier debugging + # Link to build directory for easier debugging add_custom_command(TARGET vcmilauncher POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation - + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation ) install(TARGETS vcmilauncher DESTINATION ${BIN_DIR}) diff --git a/mapeditor/CMakeLists.txt b/mapeditor/CMakeLists.txt index 14c409be9..3dd588fed 100644 --- a/mapeditor/CMakeLists.txt +++ b/mapeditor/CMakeLists.txt @@ -186,8 +186,8 @@ enable_pch(vcmieditor) # Copy to build directory for easier debugging add_custom_command(TARGET vcmieditor POST_BUILD COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/ - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons - COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/mapeditor/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/icons + COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/mapeditor/translation ) install(TARGETS vcmieditor DESTINATION ${BIN_DIR}) From f039b2065344fd1046277cb1c65615a0fb7e05e6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 15:49:21 +0200 Subject: [PATCH 1052/1248] Improvement for wandering monster tooltip/hover text: - show Visions information only on right-click (H3 logic) - show threat level only on right-click and only if UI tweaks are on --- client/widgets/MiscWidgets.cpp | 10 +++-- lib/mapObjects/CGCreature.cpp | 71 +++++++++++++++++++++++----------- lib/mapObjects/CGCreature.h | 3 ++ 3 files changed, 58 insertions(+), 26 deletions(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index 0e8d8c353..d2b879a01 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -515,14 +515,16 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature) { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - auto creatureData = (*CGI->creh)[creature->stacks.begin()->second->getCreatureID()].get(); - creatureImage = std::make_shared(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureData->getIconIndex()); + auto creatureID = creature->getCreature(); + int32_t creatureIconIndex = CGI->creatures()->getById(creatureID)->getIconIndex(); + + creatureImage = std::make_shared(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureIconIndex); creatureImage->center(Point(parent->pos.x + parent->pos.w / 2, parent->pos.y + creatureImage->pos.h / 2 + 11)); bool isHeroSelected = LOCPLINT->localState->getCurrentHero() != nullptr; std::string textContent = isHeroSelected - ? creature->getHoverText(LOCPLINT->localState->getCurrentHero()) - : creature->getHoverText(LOCPLINT->playerID); + ? creature->getPopupText(LOCPLINT->localState->getCurrentHero()) + : creature->getPopupText(LOCPLINT->playerID); //TODO: window is bigger than OH3 //TODO: vertical alignment does not match H3. Commented below example that matches H3 for creatures count but supports only 1 line: diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 04af6f8ad..cd5d90e82 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -32,7 +32,6 @@ std::string CGCreature::getHoverText(PlayerColor player) const return "INVALID_STACK"; } - std::string hoverName; MetaString ms; CCreature::CreatureQuantityId monsterQuantityId = stacks.begin()->second->getQuantityID(); int quantityTextIndex = 172 + 3 * (int)monsterQuantityId; @@ -42,20 +41,33 @@ std::string CGCreature::getHoverText(PlayerColor player) const ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); - hoverName = ms.toString(); - return hoverName; + + return ms.toString(); } std::string CGCreature::getHoverText(const CGHeroInstance * hero) const { - std::string hoverName; if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) { MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); + return ms.toString(); + } + else + { + return getHoverText(hero->tempOwner); + } +} +std::string CGCreature::getPopupText(const CGHeroInstance * hero) const +{ + std::string hoverName; + if(hero->hasVisions(this, BonusCustomSubtype::visionsMonsters)) + { + MetaString ms; + ms.appendRawString(getHoverText(hero)); ms.appendRawString("\n\n"); int decision = takenAction(hero, true); @@ -72,10 +84,10 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const ms.appendLocalString(EMetaText::GENERAL_TXT,243); break; default: //decision = cost in gold - ms.appendRawString(boost::str(boost::format(VLC->generaltexth->allTexts[244]) % decision)); + ms.appendLocalString(EMetaText::GENERAL_TXT,244); + ms.replaceNumber(decision); break; } - hoverName = ms.toString(); } else @@ -83,27 +95,42 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const hoverName = getHoverText(hero->tempOwner); } - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); + if (settings["general"]["enableUiEnhancements"].Bool()) + { + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.title"); - int choice; - double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); - if (ratio < 0.1) choice = 0; - else if (ratio < 0.25) choice = 1; - else if (ratio < 0.6) choice = 2; - else if (ratio < 0.9) choice = 3; - else if (ratio < 1.1) choice = 4; - else if (ratio < 1.3) choice = 5; - else if (ratio < 1.8) choice = 6; - else if (ratio < 2.5) choice = 7; - else if (ratio < 4) choice = 8; - else if (ratio < 8) choice = 9; - else if (ratio < 20) choice = 10; - else choice = 11; + int choice; + double ratio = (static_cast(getArmyStrength()) / hero->getTotalStrength()); + if (ratio < 0.1) choice = 0; + else if (ratio < 0.25) choice = 1; + else if (ratio < 0.6) choice = 2; + else if (ratio < 0.9) choice = 3; + else if (ratio < 1.1) choice = 4; + else if (ratio < 1.3) choice = 5; + else if (ratio < 1.8) choice = 6; + else if (ratio < 2.5) choice = 7; + else if (ratio < 4) choice = 8; + else if (ratio < 8) choice = 9; + else if (ratio < 20) choice = 10; + else choice = 11; - hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + hoverName += VLC->generaltexth->translate("vcmi.adventureMap.monsterThreat.levels." + std::to_string(choice)); + } return hoverName; } +std::string CGCreature::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} + +std::vector CGCreature::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::CREATURE, getCreature()) + }; +} + void CGCreature::onHeroVisit( const CGHeroInstance * h ) const { //show message diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index 13ff02f30..f2101d3b0 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -40,6 +40,9 @@ public: void onHeroVisit(const CGHeroInstance * h) const override; std::string getHoverText(PlayerColor player) const override; std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; void initObj(CRandomGenerator & rand) override; void pickRandomObject(CRandomGenerator & rand) override; void newTurn(CRandomGenerator & rand) const override; From 1ebb151b41fec4324d859138ba8e1102fc7e1652 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 15:49:45 +0200 Subject: [PATCH 1053/1248] Show available creatures in owned dwellings on right-click --- lib/mapObjects/CGDwelling.cpp | 24 ++++++++++++++++++++++++ lib/mapObjects/CGDwelling.h | 1 + 2 files changed, 25 insertions(+) diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index e379901d7..ea0138a67 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -335,6 +335,30 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const updateGuards(); } +std::vector CGDwelling::getPopupComponents(PlayerColor player) const +{ + if (getOwner() != player) + return {}; + + std::vector result; + + if (ID == Obj::CREATURE_GENERATOR1 && !creatures.empty()) + { + for (auto const & creature : creatures.front().second) + result.emplace_back(ComponentType::CREATURE, creature, creatures.front().first); + } + + if (ID == Obj::CREATURE_GENERATOR4) + { + for (auto const & creatureLevel : creatures) + { + if (!creatureLevel.second.empty()) + result.emplace_back(ComponentType::CREATURE, creatureLevel.second.back(), creatureLevel.first); + } + } + return result; +} + void CGDwelling::updateGuards() const { //TODO: store custom guard config and use it diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index bc63dcd85..7bed887db 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -55,6 +55,7 @@ private: void setPropertyDer(ui8 what, ui32 val) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; + std::vector getPopupComponents(PlayerColor player) const override; void updateGuards() const; void heroAcceptsCreatures(const CGHeroInstance *h) const; From c1c2119f3df7156fd7c1d18443876cfd08382c52 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 15:50:24 +0200 Subject: [PATCH 1054/1248] Show Seer Hut quest icon on right click. Fix broken tooltip. --- lib/mapObjects/CQuest.cpp | 29 +++++++++++++++++++++++++++++ lib/mapObjects/CQuest.h | 5 +++++ lib/rewardable/Limiter.cpp | 4 ++-- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 761be1090..ef35b6fbb 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -513,6 +513,35 @@ std::string CGSeerHut::getHoverText(PlayerColor player) const return hoverName; } +std::string CGSeerHut::getHoverText(const CGHeroInstance * hero) const +{ + return getHoverText(hero->getOwner()); +} + +std::string CGSeerHut::getPopupText(PlayerColor player) const +{ + return getHoverText(player); +} + +std::string CGSeerHut::getPopupText(const CGHeroInstance * hero) const +{ + return getHoverText(hero->getOwner()); +} + +std::vector CGSeerHut::getPopupComponents(PlayerColor player) const +{ + std::vector result; + quest->mission.loadComponents(result, nullptr); + return result; +} + +std::vector CGSeerHut::getPopupComponents(const CGHeroInstance * hero) const +{ + std::vector result; + quest->mission.loadComponents(result, hero); + return result; +} + void CGSeerHut::setPropertyDer(ui8 what, ui32 val) { switch(what) diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index bb4980611..6f9dbd017 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -119,6 +119,11 @@ public: void initObj(CRandomGenerator & rand) override; std::string getHoverText(PlayerColor player) const override; + std::string getHoverText(const CGHeroInstance * hero) const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; + std::vector getPopupComponents(const CGHeroInstance * hero) const override; void newTurn(CRandomGenerator & rand) const override; void onHeroVisit(const CGHeroInstance * h) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; diff --git a/lib/rewardable/Limiter.cpp b/lib/rewardable/Limiter.cpp index 05c77909a..e6fd3b361 100644 --- a/lib/rewardable/Limiter.cpp +++ b/lib/rewardable/Limiter.cpp @@ -187,14 +187,14 @@ void Rewardable::Limiter::loadComponents(std::vector & comps, const CGHeroInstance * h) const { if (heroExperience) - comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h->calculateXp(heroExperience))); + comps.emplace_back(ComponentType::EXPERIENCE, static_cast(h ? h->calculateXp(heroExperience) : heroExperience)); if (heroLevel > 0) comps.emplace_back(ComponentType::EXPERIENCE, heroLevel); if (manaPoints || manaPercentage > 0) { - int absoluteMana = h->manaLimit() ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; + int absoluteMana = (h && h->manaLimit()) ? (manaPercentage * h->mana / h->manaLimit() / 100) : 0; comps.emplace_back(ComponentType::MANA, absoluteMana + manaPoints); } From d7d81773907a88fb66b7b6ec7bb54454fc0f3dac Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 15:51:08 +0200 Subject: [PATCH 1055/1248] Show artifact description and icon on right click if UI tweaks are on --- lib/mapObjects/MiscObjects.cpp | 26 ++++++++++++++++++++++++++ lib/mapObjects/MiscObjects.h | 3 +++ 2 files changed, 29 insertions(+) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ca5ac7b73..40e12e03c 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -11,7 +11,9 @@ #include "StdInc.h" #include "MiscObjects.h" +#include "../ArtifactUtils.h" #include "../constants/StringConstants.h" +#include "../CConfigHandler.h" #include "../CGeneralTextHandler.h" #include "../CSoundBase.h" #include "../CSkillHandler.h" @@ -776,6 +778,30 @@ std::string CGArtifact::getObjectName() const return VLC->artifacts()->getById(getArtifact())->getNameTranslated(); } +std::string CGArtifact::getPopupText(PlayerColor player) const +{ + if (settings["general"]["enableUiEnhancements"].Bool()) + { + std::string description = VLC->artifacts()->getById(getArtifact())->getDescriptionTranslated(); + ArtifactUtils::insertScrrollSpellName(description, SpellID::NONE); // erase text placeholder + return description; + } + else + return getObjectName(); +} + +std::string CGArtifact::getPopupText(const CGHeroInstance * hero) const +{ + return getPopupText(hero->getOwner()); +} + +std::vector CGArtifact::getPopupComponents(PlayerColor player) const +{ + return { + Component(ComponentType::ARTIFACT, getArtifact()) + }; +} + void CGArtifact::onHeroVisit(const CGHeroInstance * h) const { if(!stacksCount()) diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index f89876ffc..6851888fa 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -86,6 +86,9 @@ public: void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::string getObjectName() const override; + std::string getPopupText(PlayerColor player) const override; + std::string getPopupText(const CGHeroInstance * hero) const override; + std::vector getPopupComponents(PlayerColor player) const override; void pick( const CGHeroInstance * h ) const; void initObj(CRandomGenerator & rand) override; From 2f3d14da5cde0a73eaccf70bbb46e8ca0ba98679 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 15:59:03 +0200 Subject: [PATCH 1056/1248] Show quest description only after visit --- lib/mapObjects/CQuest.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index ef35b6fbb..5739c71c2 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -531,14 +531,16 @@ std::string CGSeerHut::getPopupText(const CGHeroInstance * hero) const std::vector CGSeerHut::getPopupComponents(PlayerColor player) const { std::vector result; - quest->mission.loadComponents(result, nullptr); + if (quest->activeForPlayers.count(player)) + quest->mission.loadComponents(result, nullptr); return result; } std::vector CGSeerHut::getPopupComponents(const CGHeroInstance * hero) const { std::vector result; - quest->mission.loadComponents(result, hero); + if (quest->activeForPlayers.count(hero->getOwner())) + quest->mission.loadComponents(result, hero); return result; } From 414c25ea498ba087fd29c49b37bfe7a85cf69e54 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 6 Nov 2023 23:35:28 +0200 Subject: [PATCH 1057/1248] Use minstd_rand instead of mt19937 to avoid save game bloat --- lib/CRandomGenerator.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index bfd75f9a7..02974aaad 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -14,7 +14,7 @@ VCMI_LIB_NAMESPACE_BEGIN -using TGenerator = std::mt19937; +using TGenerator = std::minstd_rand; using TIntDist = std::uniform_int_distribution; using TInt64Dist = std::uniform_int_distribution; using TRealDist = std::uniform_real_distribution; From f10416be06bdc65a988291c4cbf5bf49db44cf34 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Mon, 18 Sep 2023 00:47:11 +0200 Subject: [PATCH 1058/1248] Do not show entries without name --- launcher/modManager/cmodlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index da18609fb..114506022 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -105,7 +105,7 @@ bool CModEntry::isVisible() const return false; } - return !localData.isEmpty() || !repository.isEmpty(); + return (!localData.isEmpty() || !repository.isEmpty()) && !getValue("name").isNull(); } bool CModEntry::isTranslation() const From e82df8c726ee86a1ce036cc56476d0e03a922ba7 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Tue, 19 Sep 2023 17:19:00 +0200 Subject: [PATCH 1059/1248] Better solution --- launcher/modManager/cmodlist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index 114506022..f31679cdd 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -105,7 +105,7 @@ bool CModEntry::isVisible() const return false; } - return (!localData.isEmpty() || !repository.isEmpty()) && !getValue("name").isNull(); + return !localData.isEmpty() || (!repository.isEmpty() && !repository.contains("mod")); } bool CModEntry::isTranslation() const From 66110d7bac9afffb92682b7b744fbf036f77475e Mon Sep 17 00:00:00 2001 From: Dydzio Date: Tue, 19 Sep 2023 01:40:44 +0200 Subject: [PATCH 1060/1248] Fix commanders levelup bonus to match WoG --- config/commanders.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/commanders.json b/config/commanders.json index 447a25570..045a478ad 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -3,9 +3,9 @@ //Commander receives these bonuses on level-up "bonusPerLevel": [ - ["CREATURE_DAMAGE", 1, "creatureDamageMin", 0 ], //+1 minimum damage + ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+1 minimum damage ["CREATURE_DAMAGE", 2, "creatureDamageMax", 0 ], //+2 maximum damage - ["STACK_HEALTH", 5, null, 0 ] //+5 hp + ["STACK_HEALTH", 20, null, 0 ] //+5 hp ], //Value of bonuses given by each skill level "skillLevels": From aa96a53f37866a1dd1bcfd5c49858c8456b2bb05 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Wed, 20 Sep 2023 15:25:31 +0200 Subject: [PATCH 1061/1248] Fix comment --- config/commanders.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/commanders.json b/config/commanders.json index 045a478ad..d1516cf15 100644 --- a/config/commanders.json +++ b/config/commanders.json @@ -3,8 +3,8 @@ //Commander receives these bonuses on level-up "bonusPerLevel": [ - ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+1 minimum damage - ["CREATURE_DAMAGE", 2, "creatureDamageMax", 0 ], //+2 maximum damage + ["CREATURE_DAMAGE", 2, "creatureDamageMin", 0 ], //+2 minimum damage + ["CREATURE_DAMAGE", 4, "creatureDamageMax", 0 ], //+4 maximum damage ["STACK_HEALTH", 20, null, 0 ] //+5 hp ], //Value of bonuses given by each skill level From 870aeddad5047afa31f7343b2be2156fe5cc0aeb Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 21 Sep 2023 16:44:13 +0200 Subject: [PATCH 1062/1248] Serialize owner for abandoned mine --- lib/mapObjects/MiscObjects.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ca5ac7b73..d92c35e17 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -208,7 +208,7 @@ void CGMine::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) con void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) { CArmedInstance::serializeJsonOptions(handler); - + serializeJsonOwner(handler); if(isAbandoned()) { if(handler.saving) @@ -239,10 +239,6 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) } } } - else - { - serializeJsonOwner(handler); - } } GameResID CGResource::resourceID() const From 01f70475ad9f6a57af8a18da10ac8113417fb85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Tue, 7 Nov 2023 20:58:22 +0100 Subject: [PATCH 1063/1248] Missing change --- lib/rmg/CMapGenOptions.h | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/rmg/CMapGenOptions.h b/lib/rmg/CMapGenOptions.h index f069ec9a7..8e25f2619 100644 --- a/lib/rmg/CMapGenOptions.h +++ b/lib/rmg/CMapGenOptions.h @@ -118,6 +118,7 @@ public: /// The first player colors belong to standard players and the last player colors belong to comp only players. /// All standard players are by default of type EPlayerType::AI. const std::map & getPlayersSettings() const; + const std::map & getSavedPlayersMap() const; void setStartingTownForPlayer(const PlayerColor & color, FactionID town); /// Sets a player type for a standard player. A standard player is the opposite of a computer only player. The /// values which can be chosen for the player type are EPlayerType::AI or EPlayerType::HUMAN. From 86a3806bec21afb9bb267b75c4788ceb674ce54d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 2 Nov 2023 22:01:49 +0200 Subject: [PATCH 1064/1248] MetaString refactor to eliminate integer usage for identifiers - entity names are now stored and serialized as text ID's - added helper methods for convenience to get entities names to metastring --- client/CPlayerInterface.cpp | 4 +- client/lobby/CBonusSelection.cpp | 10 +- lib/MetaString.cpp | 165 +++++++++++++---------- lib/MetaString.h | 37 ++--- lib/battle/CUnitState.cpp | 2 +- lib/battle/Unit.cpp | 8 +- lib/constants/EntityIdentifiers.h | 1 + lib/mapObjects/CBank.cpp | 8 +- lib/mapObjects/CGCreature.cpp | 8 +- lib/mapObjects/CGDwelling.cpp | 20 +-- lib/mapObjects/CGHeroInstance.cpp | 4 +- lib/mapObjects/CGPandoraBox.cpp | 2 +- lib/mapObjects/CQuest.cpp | 8 +- lib/mapObjects/MiscObjects.cpp | 6 +- lib/rewardable/Info.cpp | 12 +- lib/spells/BonusCaster.cpp | 2 +- lib/spells/ISpellMechanics.cpp | 2 +- lib/spells/effects/Damage.cpp | 2 +- lib/spells/effects/Summon.cpp | 2 +- server/CGameHandler.cpp | 36 ++--- server/battles/BattleActionProcessor.cpp | 2 +- server/battles/BattleResultProcessor.cpp | 2 +- 22 files changed, 185 insertions(+), 158 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 409b0f30e..fe353ec04 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -337,13 +337,13 @@ void CPlayerInterface::acceptTurn(QueryID queryID) if (daysWithoutCastle < 6) { text.appendLocalString(EMetaText::ARRAY_TXT,128); //%s, you only have %d days left to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + text.replaceName(playerColor); text.replaceNumber(7 - daysWithoutCastle); } else if (daysWithoutCastle == 6) { text.appendLocalString(EMetaText::ARRAY_TXT,129); //%s, this is your last day to capture a town or you will be banished from this land. - text.replaceLocalString(EMetaText::COLOR, playerColor.getNum()); + text.replaceName(playerColor); } showInfoDialogAndWait(components, text); diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index dabc75028..4605ba3ed 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -151,13 +151,13 @@ void CBonusSelection::createBonusesIcons() { case CampaignBonusType::SPELL: desc.appendLocalString(EMetaText::GENERAL_TXT, 715); - desc.replaceLocalString(EMetaText::SPELL_NAME, bonDescs[i].info2); + desc.replaceName(SpellID(bonDescs[i].info2)); break; case CampaignBonusType::MONSTER: picNumber = bonDescs[i].info2 + 2; desc.appendLocalString(EMetaText::GENERAL_TXT, 717); desc.replaceNumber(bonDescs[i].info3); - desc.replaceLocalString(EMetaText::CRE_PL_NAMES, bonDescs[i].info2); + desc.replaceNamePlural(bonDescs[i].info2); break; case CampaignBonusType::BUILDING: { @@ -187,11 +187,11 @@ void CBonusSelection::createBonusesIcons() } case CampaignBonusType::ARTIFACT: desc.appendLocalString(EMetaText::GENERAL_TXT, 715); - desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::SPELL_SCROLL: desc.appendLocalString(EMetaText::GENERAL_TXT, 716); - desc.replaceLocalString(EMetaText::ART_NAMES, bonDescs[i].info2); + desc.replaceName(ArtifactID(bonDescs[i].info2)); break; case CampaignBonusType::PRIMARY_SKILL: { @@ -229,7 +229,7 @@ void CBonusSelection::createBonusesIcons() case CampaignBonusType::SECONDARY_SKILL: desc.appendLocalString(EMetaText::GENERAL_TXT, 718); desc.replaceTextID(TextIdentifier("core", "genrltxt", "levels", bonDescs[i].info3 - 1).get()); - desc.replaceLocalString(EMetaText::SEC_SKILL_NAME, bonDescs[i].info2); + desc.replaceName(SecondarySkill(bonDescs[i].info2)); picNumber = bonDescs[i].info2 * 3 + bonDescs[i].info3 - 1; break; diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index bd02386f2..587a903f8 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -112,74 +112,29 @@ std::string MetaString::getLocalString(const std::pair & txt) c switch(type) { - case EMetaText::ART_NAMES: - { - const auto * art = ArtifactID(ser).toEntity(VLC); - if(art) - return art->getNameTranslated(); - return "#!#"; - } - case EMetaText::ART_DESCR: - { - const auto * art = ArtifactID(ser).toEntity(VLC); - if(art) - return art->getDescriptionTranslated(); - return "#!#"; - } - case EMetaText::ART_EVNTS: - { - const auto * art = ArtifactID(ser).toEntity(VLC); - if(art) - return art->getEventTranslated(); - return "#!#"; - } - case EMetaText::CRE_PL_NAMES: - { - const auto * cre = CreatureID(ser).toEntity(VLC); - if(cre) - return cre->getNamePluralTranslated(); - return "#!#"; - } - case EMetaText::CRE_SING_NAMES: - { - const auto * cre = CreatureID(ser).toEntity(VLC); - if(cre) - return cre->getNameSingularTranslated(); - return "#!#"; - } - case EMetaText::MINE_NAMES: - { - return VLC->generaltexth->translate("core.minename", ser); - } - case EMetaText::MINE_EVNTS: - { - return VLC->generaltexth->translate("core.mineevnt", ser); - } - case EMetaText::SPELL_NAME: - { - const auto * spell = SpellID(ser).toEntity(VLC); - if(spell) - return spell->getNameTranslated(); - return "#!#"; - } - case EMetaText::OBJ_NAMES: - return VLC->objtypeh->getObjectName(ser, 0); - case EMetaText::SEC_SKILL_NAME: - return VLC->skillh->getByIndex(ser)->getNameTranslated(); +// case EMetaText::ART_DESCR: +// { +// const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); +// if(art) +// return art->getDescriptionTranslated(); +// return "#!#"; +// } +// case EMetaText::MINE_NAMES: +// { +// return VLC->generaltexth->translate("core.minename", ser); +// } +// case EMetaText::MINE_EVNTS: +// { +// return VLC->generaltexth->translate(, ser); +// } case EMetaText::GENERAL_TXT: return VLC->generaltexth->translate("core.genrltxt", ser); case EMetaText::RES_NAMES: return VLC->generaltexth->translate("core.restypes", ser); case EMetaText::ARRAY_TXT: return VLC->generaltexth->translate("core.arraytxt", ser); - case EMetaText::CREGENS: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR1, ser); - case EMetaText::CREGENS4: - return VLC->objtypeh->getObjectName(Obj::CREATURE_GENERATOR4, ser); case EMetaText::ADVOB_TXT: return VLC->generaltexth->translate("core.advevent", ser); - case EMetaText::COLOR: - return VLC->generaltexth->translate("vcmi.capitalColors", ser); case EMetaText::JK_TXT: return VLC->generaltexth->translate("core.jktext", ser); default: @@ -287,20 +242,6 @@ DLL_LINKAGE std::string MetaString::buildList() const return lista; } -void MetaString::replaceCreatureName(const CreatureID & id, TQuantity count) //adds sing or plural name; -{ - if (count == 1) - replaceLocalString (EMetaText::CRE_SING_NAMES, id); - else - replaceLocalString (EMetaText::CRE_PL_NAMES, id); -} - -void MetaString::replaceCreatureName(const CStackBasicDescriptor & stack) -{ - assert(stack.type); //valid type - replaceCreatureName(stack.type->getId(), stack.count); -} - bool MetaString::operator == (const MetaString & other) const { return message == other.message && localStrings == other.localStrings && exactStrings == other.exactStrings && stringsTextID == other.stringsTextID && numbers == other.numbers; @@ -395,4 +336,80 @@ void MetaString::serializeJson(JsonSerializeFormat & handler) jsonDeserialize(handler.getCurrent()); } +void MetaString::appendName(const SpellID & id) +{ + appendTextID(id.toSpell(VLC->spells())->getNameTextID()); +} + +void MetaString::appendName(const PlayerColor & id) +{ + appendTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::appendName(const CreatureID & id, TQuantity count) +{ + if(count == 1) + appendNameSingular(id); + else + appendNamePlural(id); +} + +void MetaString::appendNameSingular(const CreatureID & id) +{ + appendTextID(id.toCreature(VLC->creatures())->getNameSingularTextID()); +} + +void MetaString::appendNamePlural(const CreatureID & id) +{ + appendTextID(id.toCreature(VLC->creatures())->getNamePluralTextID()); +} + +void MetaString::replaceName(const ArtifactID & id) +{ + replaceTextID(id.toArtifact(VLC->artifacts())->getNameTextID()); +} + +void MetaString::replaceName(const MapObjectID& id) +{ + replaceTextID(VLC->objtypeh->getObjectName(id, 0)); +} + +void MetaString::replaceName(const PlayerColor & id) +{ + replaceTextID(TextIdentifier("vcmi.capitalColors", id.getNum()).get()); +} + +void MetaString::replaceName(const SecondarySkill & id) +{ + replaceTextID(VLC->skillh->getById(id)->getNameTextID()); +} + +void MetaString::replaceName(const SpellID & id) +{ + replaceTextID(id.toSpell(VLC->spells())->getNameTextID()); +} + +void MetaString::replaceNameSingular(const CreatureID & id) +{ + replaceTextID(id.toCreature(VLC->creatures())->getNameSingularTextID()); +} + +void MetaString::replaceNamePlural(const CreatureID & id) +{ + replaceTextID(id.toCreature(VLC->creatures())->getNamePluralTextID()); +} + +void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing or plural name; +{ + if(count == 1) + replaceNameSingular(id); + else + replaceNamePlural(id); +} + +void MetaString::replaceName(const CStackBasicDescriptor & stack) +{ + replaceName(stack.type->getId(), stack.count); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/MetaString.h b/lib/MetaString.h index 2c6da6961..92d3e3450 100644 --- a/lib/MetaString.h +++ b/lib/MetaString.h @@ -12,31 +12,24 @@ VCMI_LIB_NAMESPACE_BEGIN class JsonNode; +class ArtifactID; class CreatureID; class CStackBasicDescriptor; class JsonSerializeFormat; +class MapObjectID; +class MapObjectSubID; +class PlayerColor; +class SecondarySkill; +class SpellID; using TQuantity = si32; /// Strings classes that can be used as replacement in MetaString enum class EMetaText : uint8_t { GENERAL_TXT = 1, - OBJ_NAMES, RES_NAMES, - ART_NAMES, ARRAY_TXT, - CRE_PL_NAMES, - CREGENS, - MINE_NAMES, - MINE_EVNTS, ADVOB_TXT, - ART_EVNTS, - SPELL_NAME, - SEC_SKILL_NAME, - CRE_SING_NAMES, - CREGENS4, - COLOR, - ART_DESCR, JK_TXT }; @@ -82,6 +75,12 @@ public: /// Appends specified number to resulting string void appendNumber(int64_t value); + void appendName(const SpellID& id); + void appendName(const PlayerColor& id); + void appendName(const CreatureID & id, TQuantity count); + void appendNameSingular(const CreatureID & id); + void appendNamePlural(const CreatureID & id); + /// Replaces first '%s' placeholder in string with specified local string void replaceLocalString(EMetaText type, ui32 serial); /// Replaces first '%s' placeholder in string with specified fixed, untranslated string @@ -93,10 +92,18 @@ public: /// Replaces first '%+d' placeholder in string with specified number using '+' sign as prefix void replacePositiveNumber(int64_t txt); + void replaceName(const ArtifactID & id); + void replaceName(const MapObjectID& id); + void replaceName(const PlayerColor& id); + void replaceName(const SecondarySkill& id); + void replaceName(const SpellID& id); + /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CreatureID & id, TQuantity count); + void replaceName(const CreatureID & id, TQuantity count); + void replaceNameSingular(const CreatureID & id); + void replaceNamePlural(const CreatureID & id); /// Replaces first '%s' placeholder with singular or plural name depending on creatures count - void replaceCreatureName(const CStackBasicDescriptor & stack); + void replaceName(const CStackBasicDescriptor & stack); /// erases any existing content in the string void clear(); diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index ac8b30413..72b8bf3c8 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -485,7 +485,7 @@ void CUnitState::getCastDescription(const spells::Spell * spell, const std::vect text.appendLocalString(EMetaText::GENERAL_TXT, 565);//The %s casts %s //todo: use text 566 for single creature getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); } int32_t CUnitState::manaLimit() const diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 1355ba089..316b131d5 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -187,11 +187,11 @@ void Unit::addText(MetaString & text, EMetaText type, int32_t serial, const boos void Unit::addNameReplacement(MetaString & text, const boost::logic::tribool & plural) const { if(boost::logic::indeterminate(plural)) - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); else if(plural) - text.replaceLocalString(EMetaText::CRE_PL_NAMES, creatureIndex()); + text.replaceName(creatureIndex(), 2); else - text.replaceLocalString(EMetaText::CRE_SING_NAMES, creatureIndex()); + text.replaceName(creatureIndex(), 1); } std::string Unit::formatGeneralMessage(const int32_t baseTextId) const @@ -200,7 +200,7 @@ std::string Unit::formatGeneralMessage(const int32_t baseTextId) const MetaString text; text.appendLocalString(EMetaText::GENERAL_TXT, textId); - text.replaceCreatureName(creatureId(), getCount()); + text.replaceName(creatureId(), getCount()); return text.toString(); } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index f7aad1cdd..edf4a36cb 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -704,6 +704,7 @@ public: NONE = -1, ARCHER = 2, // for debug / fallback IMP = 42, // for Deity of Fire + FAMILIAR = 43, // for Deity of Fire SKELETON = 56, // for Skeleton Transformer BONE_DRAGON = 68, // for Skeleton Transformer TROGLODYTES = 70, // for Abandoned Mine diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index ae8fc22d0..e62163e61 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -279,7 +279,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const { iw.components.emplace_back(ComponentType::ARTIFACT, elem); loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); + loot.replaceName(elem); cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); } //display loot @@ -293,7 +293,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const return a.type->getFightValue() < b.type->getFightValue(); })->type; - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, strongest->getId()); + iw.text.replaceNamePlural(strongest->getId()); iw.text.replaceRawString(loot.buildList()); } cb->showInfoDialog(&iw); @@ -315,7 +315,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const for(const SpellID & spellId : bc->spells) { const auto * spell = spellId.toEntity(VLC); - iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId); + iw.text.appendName(spellId); if(spell->getLevel() <= hero->maxSpellLevel()) { if(hero->canLearnSpell(spell)) @@ -354,7 +354,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const { iw.components.emplace_back(ComponentType::CREATURE, elem.second->getId(), elem.second->getCount()); loot.appendRawString("%s"); - loot.replaceCreatureName(*elem.second); + loot.replaceName(*elem.second); } if(ourArmy.stacksCount()) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index cd5d90e82..48349beb5 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -40,7 +40,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const else ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); + ms.appendNamePlural(getCreature()); return ms.toString(); } @@ -52,7 +52,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const MetaString ms; ms.appendNumber(stacks.begin()->second->count); ms.appendRawString(" "); - ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature()); + ms.appendName(getCreature(), stacks.begin()->second->count); return ms.toString(); } else @@ -159,7 +159,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature()); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); break; } @@ -468,7 +468,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const BlockingDialog ynd(true,false); ynd.player = h->tempOwner; ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91); - ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature()); + ynd.text.replaceName(getCreature(), getStackCount(SlotID(0))); cb->showBlockingDialog(&ynd); } diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index ea0138a67..262aab99d 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -234,7 +234,7 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::ADVOB_TXT, 44); //{%s} \n\n The camp is deserted. Perhaps you should try next week. - iw.text.replaceLocalString(EMetaText::OBJ_NAMES, ID); + iw.text.replaceName(ID); cb->sendAndApply(&iw); return; } @@ -249,12 +249,12 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const BlockingDialog bd(true,false); bd.player = h->tempOwner; bd.text.appendLocalString(EMetaText::GENERAL_TXT, 421); //Much to your dismay, the %s is guarded by %s %s. Do you wish to fight the guards? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); if(settings["gameTweaks"]["numericCreaturesQuantities"].Bool()) bd.text.replaceRawString(CCreature::getQuantityRangeStringForId(Slots().begin()->second->getQuantityID())); else bd.text.replaceLocalString(EMetaText::ARRAY_TXT, 173 + (int)Slots().begin()->second->getQuantityID()*3); - bd.text.replaceCreatureName(*Slots().begin()->second); + bd.text.replaceName(*Slots().begin()->second); cb->showBlockingDialog(&bd); return; } @@ -270,16 +270,16 @@ void CGDwelling::onHeroVisit( const CGHeroInstance * h ) const if(ID == Obj::CREATURE_GENERATOR1 || ID == Obj::CREATURE_GENERATOR4) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, ID == Obj::CREATURE_GENERATOR1 ? 35 : 36); //{%s} Would you like to recruit %s? / {%s} Would you like to recruit %s, %s, %s, or %s? - bd.text.replaceLocalString(ID == Obj::CREATURE_GENERATOR1 ? EMetaText::CREGENS : EMetaText::CREGENS4, subID); + bd.text.replaceTextID(getObjectHandler()->getNameTextID()); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::REFUGEE_CAMP) { bd.text.appendLocalString(EMetaText::ADVOB_TXT, 35); //{%s} Would you like to recruit %s? - bd.text.replaceLocalString(EMetaText::OBJ_NAMES, ID); + bd.text.replaceName(ID); for(const auto & elem : creatures) - bd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, elem.second[0]); + bd.text.replaceNamePlural(elem.second[0]); } else if(ID == Obj::WAR_MACHINE_FACTORY) bd.text.appendLocalString(EMetaText::ADVOB_TXT, 157); //{War Machine Factory} Would you like to purchase War Machines? @@ -436,7 +436,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.type = EInfoWindowMode::AUTO; iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 425);//The %s would join your hero, but there aren't enough provisions to support them. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); } else //give creatures @@ -452,7 +452,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const iw.player = h->tempOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 423); //%d %s join your army. iw.text.replaceNumber(count); - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); cb->showInfoDialog(&iw); cb->sendAndApply(&sac); @@ -464,7 +464,7 @@ void CGDwelling::heroAcceptsCreatures( const CGHeroInstance *h) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 422); //There are no %s here to recruit. - iw.text.replaceLocalString(EMetaText::CRE_PL_NAMES, crid); + iw.text.replaceNamePlural(crid); iw.player = h->tempOwner; cb->sendAndApply(&iw); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 8d385fc22..241493075 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -752,7 +752,7 @@ void CGHeroInstance::getCastDescription(const spells::Spell * spell, const std:: text.appendLocalString(EMetaText::GENERAL_TXT, textIndex); getCasterName(text); - text.replaceLocalString(EMetaText::SPELL_NAME, spell->getIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } @@ -958,7 +958,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta { iw.text.appendLocalString(EMetaText::GENERAL_TXT, 146); } - iw.text.replaceCreatureName(raisedStack); + iw.text.replaceName(raisedStack); cb->showInfoDialog(&iw); } diff --git a/lib/mapObjects/CGPandoraBox.cpp b/lib/mapObjects/CGPandoraBox.cpp index 17e05e47f..750ac50fe 100644 --- a/lib/mapObjects/CGPandoraBox.cpp +++ b/lib/mapObjects/CGPandoraBox.cpp @@ -135,7 +135,7 @@ void CGPandoraBox::grantRewardWithMessage(const CGHeroInstance * h, int index, b for(auto c : vi.reward.creatures) { loot.appendRawString("%s"); - loot.replaceCreatureName(c); + loot.replaceName(c); } if(vi.reward.creatures.size() == 1 && vi.reward.creatures[0].count == 1) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5739c71c2..5868869ca 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -223,7 +223,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com for(const auto & elem : mission.artifacts) { loot.appendRawString("%s"); - loot.replaceLocalString(EMetaText::ART_NAMES, elem); + loot.replaceName(elem); } text.replaceRawString(loot.buildList()); } @@ -234,7 +234,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com for(const auto & elem : mission.creatures) { loot.appendRawString("%s"); - loot.replaceCreatureName(elem); + loot.replaceName(elem); } text.replaceRawString(loot.buildList()); } @@ -258,7 +258,7 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com { MetaString loot; for(auto & p : mission.players) - loot.appendLocalString(EMetaText::COLOR, p); + loot.appendName(p); text.replaceRawString(loot.buildList()); } @@ -329,7 +329,7 @@ void CQuest::addKillTargetReplacements(MetaString &out) const out.replaceTextID(heroName); if(stackToKill.type) { - out.replaceCreatureName(stackToKill); + out.replaceName(stackToKill); out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); } } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 40e12e03c..f1710d08b 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -169,7 +169,7 @@ void CGMine::flagMine(const PlayerColor & player) const InfoWindow iw; iw.type = EInfoWindowMode::AUTO; iw.soundID = soundBase::FLAGMINE; - iw.text.appendLocalString(EMetaText::MINE_EVNTS, producedResource); //not use subID, abandoned mines uses default mine texts + iw.text.appendTextID(TextIdentifier("core.mineevnt", producedResource.getNum()).get()); //not use subID, abandoned mines uses default mine texts iw.player = player; iw.components.emplace_back(ComponentType::RESOURCE_PER_DAY, producedResource, producedQuantity); cb->showInfoDialog(&iw); @@ -820,7 +820,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const if(!message.empty()) iw.text = message; else - iw.text.appendLocalString(EMetaText::ART_EVNTS, getArtifact()); + iw.text.appendTextID(getArtifact().toArtifact()->getEventTextID()); } break; case Obj::SPELL_SCROLL: @@ -832,7 +832,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const else { iw.text.appendLocalString(EMetaText::ADVOB_TXT,135); - iw.text.replaceLocalString(EMetaText::SPELL_NAME, spell.getNum()); + iw.text.replaceName(spell); } } break; diff --git a/lib/rewardable/Info.cpp b/lib/rewardable/Info.cpp index cb3a61ac4..135f97c8c 100644 --- a/lib/rewardable/Info.cpp +++ b/lib/rewardable/Info.cpp @@ -249,23 +249,23 @@ void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variab for (const auto & variable : variables.values ) { if( boost::algorithm::starts_with(variable.first, "spell")) - target.replaceLocalString(EMetaText::SPELL_NAME, variable.second); + target.replaceName(SpellID(variable.second)); if( boost::algorithm::starts_with(variable.first, "secondarySkill")) - target.replaceLocalString(EMetaText::SEC_SKILL_NAME, variable.second); + target.replaceName(SecondarySkill(variable.second)); } } void Rewardable::Info::replaceTextPlaceholders(MetaString & target, const Variables & variables, const VisitInfo & info) const { for (const auto & artifact : info.reward.artifacts ) - target.replaceLocalString(EMetaText::ART_NAMES, artifact.getNum()); + target.replaceName(artifact); - for (const auto & artifact : info.reward.spells ) - target.replaceLocalString(EMetaText::SPELL_NAME, artifact.getNum()); + for (const auto & spell : info.reward.spells ) + target.replaceName(spell); for (const auto & secondary : info.reward.secondary ) - target.replaceLocalString(EMetaText::SEC_SKILL_NAME, secondary.first.getNum()); + target.replaceName(secondary.first); replaceTextPlaceholders(target, variables); } diff --git a/lib/spells/BonusCaster.cpp b/lib/spells/BonusCaster.cpp index 19ad943af..5c635c97a 100644 --- a/lib/spells/BonusCaster.cpp +++ b/lib/spells/BonusCaster.cpp @@ -45,7 +45,7 @@ void BonusCaster::getCastDescription(const Spell * spell, const std::vectorgetIndex()); + text.replaceName(spell->getId()); if(singleTarget) attacked.at(0)->addNameReplacement(text, true); } diff --git a/lib/spells/ISpellMechanics.cpp b/lib/spells/ISpellMechanics.cpp index cb7cc8832..f83773dd7 100644 --- a/lib/spells/ISpellMechanics.cpp +++ b/lib/spells/ISpellMechanics.cpp @@ -489,7 +489,7 @@ bool BaseMechanics::adaptProblem(ESpellCastProblem source, Problem & target) con { //The %s prevents %s from casting 3rd level or higher spells. text.appendLocalString(EMetaText::GENERAL_TXT, 536); - text.replaceLocalString(EMetaText::ART_NAMES, b->sid.as()); + text.replaceName(b->sid.as()); caster->getCasterName(text); target.add(std::move(text), spells::Problem::NORMAL); } diff --git a/lib/spells/effects/Damage.cpp b/lib/spells/effects/Damage.cpp index bb788193b..6a28a5349 100644 --- a/lib/spells/effects/Damage.cpp +++ b/lib/spells/effects/Damage.cpp @@ -176,7 +176,7 @@ void Damage::describeEffect(std::vector & log, const Mechanics * m, { MetaString line; line.appendLocalString(EMetaText::GENERAL_TXT, 376); // Spell %s does %d damage - line.replaceLocalString(EMetaText::SPELL_NAME, m->getSpellIndex()); + line.replaceName(m->getSpellId()); line.replaceNumber(static_cast(damage)); log.push_back(line); diff --git a/lib/spells/effects/Summon.cpp b/lib/spells/effects/Summon.cpp index 9f1eb6f41..ac3c0206b 100644 --- a/lib/spells/effects/Summon.cpp +++ b/lib/spells/effects/Summon.cpp @@ -66,7 +66,7 @@ bool Summon::applicable(Problem & problem, const Mechanics * m) const { text.replaceRawString(caster->getNameTranslated()); - text.replaceLocalString(EMetaText::CRE_PL_NAMES, elemental->creatureIndex()); + text.replaceNamePlural(elemental->creatureId()); if(caster->type->gender == EHeroGender::FEMALE) text.replaceLocalString(EMetaText::GENERAL_TXT, 540); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index a8134dfe4..0f2d4ca3c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -908,24 +908,24 @@ void CGameHandler::onNewTurn() { case NewTurn::DOUBLE_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 131); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + iw.text.replaceNameSingular(n.creatureid); + iw.text.replaceNameSingular(n.creatureid); break; case NewTurn::PLAGUE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 132); break; case NewTurn::BONUS_GROWTH: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 134); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, n.creatureid); + iw.text.replaceNameSingular(n.creatureid); + iw.text.replaceNameSingular(n.creatureid); break; case NewTurn::DEITYOFFIRE: iw.text.appendLocalString(EMetaText::ARRAY_TXT, 135); - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 42); //%s imp - iw.text.replacePositiveNumber(15); //%+d 15 - iw.text.replaceLocalString(EMetaText::CRE_SING_NAMES, 43); //%s familiar - iw.text.replacePositiveNumber(15); //%+d 15 + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replaceNameSingular(CreatureID::IMP); //%s imp + iw.text.replacePositiveNumber(15);//%+d 15 + iw.text.replaceNameSingular(CreatureID::FAMILIAR); //%s familiar + iw.text.replacePositiveNumber(15);//%+d 15 break; default: if (newMonth) @@ -1350,7 +1350,7 @@ void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owne InfoWindow iw; iw.player = oldOwner; iw.text.appendLocalString(EMetaText::GENERAL_TXT, 6); //%s, you have lost your last town. If you do not conquer another town in the next week, you will be eliminated. - iw.text.replaceLocalString(EMetaText::COLOR, oldOwner.getNum()); + iw.text.replaceName(oldOwner); sendAndApply(&iw); } } @@ -1588,7 +1588,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t for (auto it : cs2.spells) { iw.components.emplace_back(ComponentType::SPELL, it); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.text.appendName(it); switch (size--) { case 2: @@ -1616,7 +1616,7 @@ void CGameHandler::useScholarSkill(ObjectInstanceID fromHero, ObjectInstanceID t for (auto it : cs1.spells) { iw.components.emplace_back(ComponentType::SPELL, it); - iw.text.appendLocalString(EMetaText::SPELL_NAME, it.toEnum()); + iw.text.appendName(it); switch (size--) { case 2: @@ -3639,7 +3639,7 @@ void CGameHandler::getVictoryLossMessage(PlayerColor player, const EVictoryLossC { out.player = player; out.text = victoryLossCheckResult.messageToSelf; - out.text.replaceLocalString(EMetaText::COLOR, player.getNum()); + out.text.replaceName(player); out.components.emplace_back(ComponentType::FLAG, player); } @@ -3661,16 +3661,18 @@ bool CGameHandler::dig(const CGHeroInstance *h) iw.player = h->tempOwner; if (gs->map->grailPos == h->visitablePos()) { + ArtifactID grail = ArtifactID::GRAIL; + iw.text.appendLocalString(EMetaText::GENERAL_TXT, 58); //"Congratulations! After spending many hours digging here, your hero has uncovered the " - iw.text.appendLocalString(EMetaText::ART_NAMES, ArtifactID::GRAIL); + iw.text.replaceName(grail); iw.soundID = soundBase::ULTIMATEARTIFACT; - giveHeroNewArtifact(h, VLC->arth->objects[ArtifactID::GRAIL], ArtifactPosition::FIRST_AVAILABLE); //give grail + giveHeroNewArtifact(h, grail.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); //give grail sendAndApply(&iw); iw.soundID = soundBase::invalid; - iw.components.emplace_back(ComponentType::ARTIFACT, ArtifactID(ArtifactID::GRAIL)); + iw.components.emplace_back(ComponentType::ARTIFACT, grail); iw.text.clear(); - iw.text.appendLocalString(EMetaText::ART_DESCR, ArtifactID::GRAIL); + iw.text.appendTextID(grail.toArtifact()->getDescriptionTextID()); sendAndApply(&iw); } else diff --git a/server/battles/BattleActionProcessor.cpp b/server/battles/BattleActionProcessor.cpp index 6fcf831b0..56878c270 100644 --- a/server/battles/BattleActionProcessor.cpp +++ b/server/battles/BattleActionProcessor.cpp @@ -1067,7 +1067,7 @@ void BattleActionProcessor::makeAttack(const CBattleInfoCallback & battle, const { MetaString text; text.appendLocalString(EMetaText::GENERAL_TXT, 376); - text.replaceLocalString(EMetaText::SPELL_NAME, SpellID::FIRE_SHIELD); + text.replaceName(SpellID(SpellID::FIRE_SHIELD)); text.replaceNumber(totalDamage); blm.lines.push_back(std::move(text)); } diff --git a/server/battles/BattleResultProcessor.cpp b/server/battles/BattleResultProcessor.cpp index 472895572..52c4e7e6b 100644 --- a/server/battles/BattleResultProcessor.cpp +++ b/server/battles/BattleResultProcessor.cpp @@ -462,7 +462,7 @@ void BattleResultProcessor::endBattleConfirm(const CBattleInfoCallback & battle) auto it = cs.spells.begin(); for (int i = 0; i < cs.spells.size(); i++, it++) { - iw.text.replaceLocalString(EMetaText::SPELL_NAME, it->toEnum()); + iw.text.replaceName(*it); if (i == cs.spells.size() - 2) //we just added pre-last name iw.text.replaceLocalString(EMetaText::GENERAL_TXT, 141); // " and " iw.components.emplace_back(ComponentType::SPELL, *it); From 5cd340ad85177b3f55c0a2c0b8ed00da42c65b37 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 Nov 2023 16:22:03 +0200 Subject: [PATCH 1065/1248] Remove old code --- lib/MetaString.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 587a903f8..c8df35361 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -112,21 +112,6 @@ std::string MetaString::getLocalString(const std::pair & txt) c switch(type) { -// case EMetaText::ART_DESCR: -// { -// const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts()); -// if(art) -// return art->getDescriptionTranslated(); -// return "#!#"; -// } -// case EMetaText::MINE_NAMES: -// { -// return VLC->generaltexth->translate("core.minename", ser); -// } -// case EMetaText::MINE_EVNTS: -// { -// return VLC->generaltexth->translate(, ser); -// } case EMetaText::GENERAL_TXT: return VLC->generaltexth->translate("core.genrltxt", ser); case EMetaText::RES_NAMES: From 09e42cd9dfe97c88fa9bd2416002b94966c5a5b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 3 Nov 2023 16:27:19 +0200 Subject: [PATCH 1066/1248] Remove magic number --- lib/battle/Unit.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/battle/Unit.cpp b/lib/battle/Unit.cpp index 316b131d5..c08713f43 100644 --- a/lib/battle/Unit.cpp +++ b/lib/battle/Unit.cpp @@ -189,9 +189,9 @@ void Unit::addNameReplacement(MetaString & text, const boost::logic::tribool & p if(boost::logic::indeterminate(plural)) text.replaceName(creatureId(), getCount()); else if(plural) - text.replaceName(creatureIndex(), 2); + text.replaceNamePlural(creatureIndex()); else - text.replaceName(creatureIndex(), 1); + text.replaceNameSingular(creatureIndex()); } std::string Unit::formatGeneralMessage(const int32_t baseTextId) const From 748d70b1912db92b5f517a6c347b5b01b701fc3a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 6 Nov 2023 21:09:19 +0200 Subject: [PATCH 1067/1248] Fix build --- lib/MetaString.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index c8df35361..944c82190 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -323,7 +323,7 @@ void MetaString::serializeJson(JsonSerializeFormat & handler) void MetaString::appendName(const SpellID & id) { - appendTextID(id.toSpell(VLC->spells())->getNameTextID()); + appendTextID(id.toEntity(VLC)->getNameTextID()); } void MetaString::appendName(const PlayerColor & id) @@ -341,17 +341,17 @@ void MetaString::appendName(const CreatureID & id, TQuantity count) void MetaString::appendNameSingular(const CreatureID & id) { - appendTextID(id.toCreature(VLC->creatures())->getNameSingularTextID()); + appendTextID(id.toEntity(VLC)->getNameSingularTextID()); } void MetaString::appendNamePlural(const CreatureID & id) { - appendTextID(id.toCreature(VLC->creatures())->getNamePluralTextID()); + appendTextID(id.toEntity(VLC)->getNamePluralTextID()); } void MetaString::replaceName(const ArtifactID & id) { - replaceTextID(id.toArtifact(VLC->artifacts())->getNameTextID()); + replaceTextID(id.toEntity(VLC)->getNameTextID()); } void MetaString::replaceName(const MapObjectID& id) @@ -371,17 +371,17 @@ void MetaString::replaceName(const SecondarySkill & id) void MetaString::replaceName(const SpellID & id) { - replaceTextID(id.toSpell(VLC->spells())->getNameTextID()); + replaceTextID(id.toEntity(VLC)->getNameTextID()); } void MetaString::replaceNameSingular(const CreatureID & id) { - replaceTextID(id.toCreature(VLC->creatures())->getNameSingularTextID()); + replaceTextID(id.toEntity(VLC)->getNameSingularTextID()); } void MetaString::replaceNamePlural(const CreatureID & id) { - replaceTextID(id.toCreature(VLC->creatures())->getNamePluralTextID()); + replaceTextID(id.toEntity(VLC)->getNamePluralTextID()); } void MetaString::replaceName(const CreatureID & id, TQuantity count) //adds sing or plural name; From bdc83bb1b87e46b7b89e9ec6e9cf3a0e8756cba6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 23:48:12 +0200 Subject: [PATCH 1068/1248] Add description why minstd has been chosen --- lib/CRandomGenerator.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/CRandomGenerator.h b/lib/CRandomGenerator.h index 02974aaad..cb0360e60 100644 --- a/lib/CRandomGenerator.h +++ b/lib/CRandomGenerator.h @@ -14,6 +14,10 @@ VCMI_LIB_NAMESPACE_BEGIN +/// Generator to use for all randomization in game +/// minstd_rand is selected due to following reasons: +/// 1. Its randomization quality is below mt_19937 however this is unlikely to be noticeable in game +/// 2. It has very low state size, leading to low overhead in size of saved games (due to large number of random generator instances in game) using TGenerator = std::minstd_rand; using TIntDist = std::uniform_int_distribution; using TInt64Dist = std::uniform_int_distribution; From 41ee52d9d4340b28d548608710b7f1327a0b6a62 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Mon, 6 Nov 2023 15:21:40 +0200 Subject: [PATCH 1069/1248] fix build & suggested changed --- client/widgets/CAltar.cpp | 126 +++++++++++++++++--------------- client/widgets/CAltar.h | 5 ++ client/windows/CAltarWindow.cpp | 6 +- client/windows/CTradeWindow.cpp | 86 ++++++++++------------ client/windows/CTradeWindow.h | 4 - 5 files changed, 115 insertions(+), 112 deletions(-) diff --git a/client/widgets/CAltar.cpp b/client/widgets/CAltar.cpp index 338297456..ba76f47d3 100644 --- a/client/widgets/CAltar.cpp +++ b/client/widgets/CAltar.cpp @@ -73,29 +73,7 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * for(auto & altarSlotPos : posSlotsAltar) { auto altarSlot = std::make_shared(altarSlotPos, EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum++); - altarSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void - { - const auto pickedArtInst = arts->getPickedArtifact(); - if(pickedArtInst) - { - arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); - moveArtToAltar(altarSlot, pickedArtInst); - } - else if(const CArtifactInstance * art = altarSlot->getArtInstance()) - { - const auto hero = arts->getHero(); - const auto slot = hero->getSlotByInstance(art); - assert(slot != ArtifactPosition::PRE_FIRST); - LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), - ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); - arts->pickedArtFromSlot = slot; - arts->artifactsOnAltar.erase(art); - altarSlot->setID(-1); - altarSlot->subtitle.clear(); - deal->block(!arts->artifactsOnAltar.size()); - } - calcExpAltarForHero(); - }; + altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1); altarSlot->subtitle = ""; items.front().emplace_back(altarSlot); } @@ -227,6 +205,30 @@ bool CAltarArtifacts::putArtOnAltar(std::shared_ptr altarSlot, c return true; }; +void CAltarArtifacts::onSlotClickPressed(std::shared_ptr altarSlot) +{ + const auto pickedArtInst = arts->getPickedArtifact(); + if(pickedArtInst) + { + arts->pickedArtMoveToAltar(ArtifactPosition::TRANSITION_POS); + moveArtToAltar(altarSlot, pickedArtInst); + } + else if(const CArtifactInstance * art = altarSlot->getArtInstance()) + { + const auto hero = arts->getHero(); + const auto slot = hero->getSlotByInstance(art); + assert(slot != ArtifactPosition::PRE_FIRST); + LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot), + ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS)); + arts->pickedArtFromSlot = slot; + arts->artifactsOnAltar.erase(art); + altarSlot->setID(-1); + altarSlot->subtitle.clear(); + deal->block(!arts->artifactsOnAltar.size()); + } + calcExpAltarForHero(); +} + CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero) : CAltar(market, hero) { @@ -239,16 +241,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * lSubtitle = std::make_shared(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); rSubtitle = std::make_shared(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); - unitsSlider = std::make_shared(Point(231, 481), 137, [this](int newVal) -> void - { - if(hLeft) - unitsOnAltar[hLeft->serial] = newVal; - if(hRight) - updateAltarSlot(hRight); - deal->block(calcExpAltarForHero() == 0); - updateControls(); - updateSubtitlesForSelected(); - }, 0, 0, 0, Orientation::HORIZONTAL); + unitsSlider = std::make_shared(Point(231, 481), 137, std::bind(&CAltarCreatures::onUnitsSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL); maxUnits = std::make_shared(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, unitsSlider)); unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0); @@ -256,27 +249,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * sacrificeAllButton = std::make_shared( Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CAltar::sacrificeAll, this)); - auto clickPressed = [this](std::shared_ptr altarSlot, std::vector> & oppositeSlots, - std::shared_ptr & hCurSide, std::shared_ptr & hOppSide) -> void - { - std::shared_ptr oppositeSlot; - for(const auto & slot : oppositeSlots) - if(slot->serial == altarSlot->serial) - { - oppositeSlot = slot; - break; - } - - if(hCurSide != altarSlot && oppositeSlot) - { - hCurSide = altarSlot; - hOppSide = oppositeSlot; - updateControls(); - updateSubtitlesForSelected(); - redraw(); - } - }; - + // Creating slots for hero creatures for(int slotIdx = 0; slotIdx < GameConstants::ARMY_SIZE; slotIdx++) { CreatureID creatureId = CreatureID::NONE; @@ -286,21 +259,23 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * continue; auto heroSlot = std::make_shared(posSlotsHero[slotIdx], EType::CREATURE, creatureId.num, true, slotIdx); - heroSlot->clickPressedCallback = [this, clickPressed](std::shared_ptr altarSlot) -> void + heroSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void { - clickPressed(altarSlot, items[0], hLeft, hRight); + onSlotClickPressed(altarSlot, items[0], hLeft, hRight); }; heroSlot->subtitle = std::to_string(hero->getStackCount(SlotID(slotIdx))); items[1].emplace_back(heroSlot); } + + // Creating slots for creatures on altar assert(items[1].size() <= posSlotsAltar.size()); for(const auto & heroSlot : items[1]) { auto altarSlot = std::make_shared(posSlotsAltar[heroSlot->serial], EType::CREATURE_PLACEHOLDER, heroSlot->id, false, heroSlot->serial); altarSlot->pos.w = heroSlot->pos.w; altarSlot->pos.h = heroSlot->pos.h; - altarSlot->clickPressedCallback = [this, clickPressed](std::shared_ptr altarSlot) -> void + altarSlot->clickPressedCallback = [this](std::shared_ptr altarSlot) -> void { - clickPressed(altarSlot, items[1], hRight, hLeft); + onSlotClickPressed(altarSlot, items[1], hRight, hLeft); }; items[0].emplace_back(altarSlot); } @@ -454,4 +429,37 @@ void CAltarCreatures::updateAltarSlot(std::shared_ptr slot) slot->setType(units > 0 ? CREATURE : CREATURE_PLACEHOLDER); slot->subtitle = units > 0 ? boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : ""; -} \ No newline at end of file +} + +void CAltarCreatures::onUnitsSliderMoved(int newVal) +{ + if(hLeft) + unitsOnAltar[hLeft->serial] = newVal; + if(hRight) + updateAltarSlot(hRight); + deal->block(calcExpAltarForHero() == 0); + updateControls(); + updateSubtitlesForSelected(); +} + +void CAltarCreatures::onSlotClickPressed(std::shared_ptr altarSlot, + std::vector> & oppositeSlots, + std::shared_ptr & hCurSide, std::shared_ptr & hOppSide) +{ + std::shared_ptr oppositeSlot; + for(const auto & slot : oppositeSlots) + if(slot->serial == altarSlot->serial) + { + oppositeSlot = slot; + break; + } + + if(hCurSide != altarSlot && oppositeSlot) + { + hCurSide = altarSlot; + hOppSide = oppositeSlot; + updateControls(); + updateSubtitlesForSelected(); + redraw(); + } +} diff --git a/client/widgets/CAltar.h b/client/widgets/CAltar.h index c6afe24b8..685afa731 100644 --- a/client/widgets/CAltar.h +++ b/client/widgets/CAltar.h @@ -59,6 +59,7 @@ private: }; bool putArtOnAltar(std::shared_ptr altarSlot, const CArtifactInstance * art); + void onSlotClickPressed(std::shared_ptr altarSlot); }; class CAltarCreatures : public CAltar @@ -95,4 +96,8 @@ private: void readExpValues(); void updateControls(); void updateSubtitlesForSelected(); + void onUnitsSliderMoved(int newVal); + void onSlotClickPressed(std::shared_ptr altarSlot, + std::vector> & oppositeSlots, + std::shared_ptr & hCurSide, std::shared_ptr & hOppSide); }; diff --git a/client/windows/CAltarWindow.cpp b/client/windows/CAltarWindow.cpp index faabbb5ca..249abcde2 100644 --- a/client/windows/CAltarWindow.cpp +++ b/client/windows/CAltarWindow.cpp @@ -30,12 +30,12 @@ CAltarWindow::CAltarWindow(const IMarket * market, const CGHeroInstance * hero, { OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); + assert(mode == EMarketMode::ARTIFACT_EXP || mode == EMarketMode::CREATURE_EXP); if(mode == EMarketMode::ARTIFACT_EXP) createAltarArtifacts(market, hero); - else if (mode == EMarketMode::CREATURE_EXP) + else if(mode == EMarketMode::CREATURE_EXP) createAltarCreatures(market, hero); - else - close(); + updateExpToLevel(); statusBar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 04b1c59cf..5cb261793 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -163,9 +163,46 @@ void CTradeWindow::getPositionsFor(std::vector &poss, bool Left, EType typ // X X X // X X X // X - int h, w, x, y, dx, dy; - int leftToRightOffset; - getBaseForPositions(type, dx, dy, x, y, h, w, !Left, leftToRightOffset); + int h = 0, w = 0, x = 0, y = 0, dx = 0, dy = 0; + + switch(type) + { + case RESOURCE: + dx = 82; + dy = 79; + x = 39; + y = 180; + h = 68; + w = 70; + break; + case PLAYER: + dx = 83; + dy = 118; + h = 64; + w = 58; + x = 44; + y = 83; + assert(!Left); + break; + case CREATURE://45,123 + x = 45; + y = 123; + w = 58; + h = 64; + dx = 83; + dy = 98; + assert(Left); + break; + case ARTIFACT_TYPE://45,123 + x = 340 - 289; + y = 180; + w = 44; + h = 44; + dx = 83; + dy = 79; + break; + } + int leftToRightOffset = 289; const std::vector tmp = { @@ -672,49 +709,6 @@ void CMarketplaceWindow::resourceChanged() initSubs(true); } -void CMarketplaceWindow::getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const -{ - switch(type) - { - case RESOURCE: - dx = 82; - dy = 79; - x = 39; - y = 180; - h = 68; - w = 70; - break; - case PLAYER: - dx = 83; - dy = 118; - h = 64; - w = 58; - x = 44; - y = 83; - assert(Right); - break; - case CREATURE://45,123 - x = 45; - y = 123; - w = 58; - h = 64; - dx = 83; - dy = 98; - assert(!Right); - break; - case ARTIFACT_TYPE://45,123 - x = 340-289; - y = 180; - w = 44; - h = 44; - dx = 83; - dy = 79; - break; - } - - leftToRightOffset = 289; -} - void CMarketplaceWindow::updateTraderText() { if(readyToTrade) diff --git a/client/windows/CTradeWindow.h b/client/windows/CTradeWindow.h index 9c0763d68..04ea7529c 100644 --- a/client/windows/CTradeWindow.h +++ b/client/windows/CTradeWindow.h @@ -41,8 +41,6 @@ public: void setMode(EMarketMode Mode); //mode setter void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot - - virtual void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const = 0; virtual void selectionChanged(bool side) = 0; //true == left virtual Point selectionOffset(bool Left) const = 0; virtual std::string updateSlotSubtitle(bool Left) const = 0; @@ -80,7 +78,5 @@ public: void updateGarrison() override; //removes creatures with count 0 from the list (apparently whole stack has been sold) void artifactsChanged(bool left) override; void resourceChanged(); - - void getBaseForPositions(EType type, int &dx, int &dy, int &x, int &y, int &h, int &w, bool Right, int &leftToRightOffset) const override; void updateTraderText(); }; From 6b81012f313d9029ff864f81e0880f71c617b530 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 6 Nov 2023 18:27:16 +0200 Subject: [PATCH 1070/1248] Use variant identifier in netpacks where applicable --- AI/Nullkiller/AIGateway.cpp | 4 +- AI/VCAI/VCAI.cpp | 4 +- CCallback.cpp | 10 +-- CCallback.h | 13 ++-- client/Client.h | 5 +- client/NetPacksClient.cpp | 18 ++--- client/widgets/CAltar.cpp | 11 ++-- client/windows/CHeroWindow.cpp | 2 +- client/windows/CTradeWindow.cpp | 25 +++++-- client/windows/GUIClasses.cpp | 10 +-- client/windows/GUIClasses.h | 6 +- cmake_modules/VCMI_lib.cmake | 2 + lib/CCreatureSet.cpp | 7 +- lib/CCreatureSet.h | 8 +-- lib/IGameCallback.h | 6 +- lib/constants/Enumerations.h | 6 ++ lib/constants/VariantIdentifier.h | 2 +- lib/mapObjects/CBank.cpp | 14 ++-- lib/mapObjects/CBank.h | 2 +- lib/mapObjects/CGCreature.cpp | 31 ++++----- lib/mapObjects/CGCreature.h | 13 +--- lib/mapObjects/CGDwelling.cpp | 10 +-- lib/mapObjects/CGDwelling.h | 2 +- lib/mapObjects/CGHeroInstance.cpp | 10 +-- lib/mapObjects/CGHeroInstance.h | 2 +- lib/mapObjects/CGMarket.cpp | 2 +- lib/mapObjects/CGObjectInstance.cpp | 17 ++--- lib/mapObjects/CGObjectInstance.h | 4 +- lib/mapObjects/CGTownBuilding.cpp | 22 +++---- lib/mapObjects/CGTownBuilding.h | 8 +-- lib/mapObjects/CGTownInstance.cpp | 24 +++---- lib/mapObjects/CGTownInstance.h | 2 +- lib/mapObjects/CQuest.cpp | 29 ++++---- lib/mapObjects/CQuest.h | 7 +- lib/mapObjects/CRewardableObject.cpp | 14 ++-- lib/mapObjects/CRewardableObject.h | 2 +- lib/mapObjects/IObjectInterface.cpp | 2 +- lib/mapObjects/IObjectInterface.h | 3 +- lib/mapObjects/MiscObjects.cpp | 24 +++---- lib/mapObjects/MiscObjects.h | 7 +- lib/networkPacks/NetPacksLib.cpp | 44 +++++++------ lib/networkPacks/ObjProperty.h | 69 ++++++++++++++++++++ lib/networkPacks/PacksForClient.h | 59 ++++++----------- lib/networkPacks/PacksForServer.h | 8 ++- lib/networkPacks/TradeItem.h | 20 ++++++ lib/rewardable/Interface.cpp | 4 +- lib/spells/AdventureSpellMechanics.cpp | 4 +- lib/spells/effects/Moat.cpp | 2 +- mapeditor/inspector/armywidget.cpp | 2 +- server/CGameHandler.cpp | 47 +++++++------ server/CGameHandler.h | 9 +-- server/NetPacksServer.cpp | 25 ++++--- server/battles/BattleProcessor.cpp | 4 +- server/processors/PlayerMessageProcessor.cpp | 8 +-- 54 files changed, 391 insertions(+), 304 deletions(-) create mode 100644 lib/networkPacks/ObjProperty.h create mode 100644 lib/networkPacks/TradeItem.h diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index a4613db1b..501c61eeb 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -481,7 +481,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop) NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { - auto relations = myCb->getPlayerRelations(playerID, (PlayerColor)sop->val); + auto relations = myCb->getPlayerRelations(playerID, sop->identifier.as()); auto obj = myCb->getObj(sop->id, false); if(!nullkiller) // crash protection @@ -1419,7 +1419,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); accquiredResources = static_cast(toGet * (it->resVal / toGive)); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index 19313633b..c3068c989 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -564,7 +564,7 @@ void VCAI::objectPropertyChanged(const SetObjectProperty * sop) NET_EVENT_HANDLER; if(sop->what == ObjProperty::OWNER) { - if(myCb->getPlayerRelations(playerID, (PlayerColor)sop->val) == PlayerRelations::ENEMIES) + if(myCb->getPlayerRelations(playerID, sop->identifier.as()) == PlayerRelations::ENEMIES) { //we want to visit objects owned by oppponents auto obj = myCb->getObj(sop->id, false); @@ -2160,7 +2160,7 @@ void VCAI::tryRealize(Goals::Trade & g) //trade //TODO trade only as much as needed if (toGive) //don't try to sell 0 resources { - cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive); + cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive); accquiredResources = static_cast(toGet * (it->resVal / toGive)); logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName()); } diff --git a/CCallback.cpp b/CCallback.cpp index 78c8fe884..7b87bc913 100644 --- a/CCallback.cpp +++ b/CCallback.cpp @@ -237,12 +237,12 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid) sendRequest(&pack); } -void CCallback::trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero) { - trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); + trade(market, mode, std::vector(1, id1), std::vector(1, id2), std::vector(1, val1), hero); } -void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) +void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero) { TradeOnMarketplace pack; pack.marketId = dynamic_cast(market)->id; @@ -254,9 +254,9 @@ void CCallback::trade(const IMarket * market, EMarketMode mode, const std::vecto sendRequest(&pack); } -void CCallback::setFormation(const CGHeroInstance * hero, bool tight) +void CCallback::setFormation(const CGHeroInstance * hero, EArmyFormation mode) { - SetFormation pack(hero->id,tight); + SetFormation pack(hero->id, mode); sendRequest(&pack); } diff --git a/CCallback.h b/CCallback.h index d7af2e38c..068ac547b 100644 --- a/CCallback.h +++ b/CCallback.h @@ -12,6 +12,7 @@ #include "lib/CGameInfoCallback.h" #include "lib/battle/CPlayerBattleCallback.h" #include "lib/int3.h" // for int3 +#include "lib/networkPacks/TradeItem.h" VCMI_LIB_NAMESPACE_BEGIN @@ -78,8 +79,8 @@ public: virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made virtual void swapGarrisonHero(const CGTownInstance *town)=0; - virtual void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce - virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; + virtual void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce + virtual void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr)=0; virtual int selectionMade(int selection, QueryID queryID) =0; virtual int sendQueryReply(std::optional reply, QueryID queryID) =0; @@ -94,7 +95,7 @@ public: virtual bool dismissCreature(const CArmedInstance *obj, SlotID stackPos)=0; virtual void endTurn()=0; virtual void buyArtifact(const CGHeroInstance *hero, ArtifactID aid)=0; //used to buy artifacts in towns (including spell book in the guild and war machines in blacksmith) - virtual void setFormation(const CGHeroInstance * hero, bool tight)=0; + virtual void setFormation(const CGHeroInstance * hero, EArmyFormation mode)=0; virtual void save(const std::string &fname) = 0; virtual void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) = 0; @@ -181,9 +182,9 @@ public: void endTurn() override; void swapGarrisonHero(const CGTownInstance *town) override; void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override; - void trade(const IMarket * market, EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; - void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; - void setFormation(const CGHeroInstance * hero, bool tight) override; + void trade(const IMarket * market, EMarketMode mode, TradeItemSell id1, TradeItemBuy id2, ui32 val1, const CGHeroInstance * hero = nullptr) override; + void trade(const IMarket * market, EMarketMode mode, const std::vector & id1, const std::vector & id2, const std::vector & val1, const CGHeroInstance * hero = nullptr) override; + void setFormation(const CGHeroInstance * hero, EArmyFormation mode) override; void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override; void save(const std::string &fname) override; void sendMessage(const std::string &mess, const CGObjectInstance * currentObject = nullptr) override; diff --git a/client/Client.h b/client/Client.h index 2be88e55a..31516cb98 100644 --- a/client/Client.h +++ b/client/Client.h @@ -162,7 +162,7 @@ public: void changeSpells(const CGHeroInstance * hero, bool give, const std::set & spells) override {}; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;}; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override {}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override {}; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs = false) override {}; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs = false) override {}; @@ -212,7 +212,8 @@ public: void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, ETileVisibility mode) override {} void changeFogOfWar(std::unordered_set & tiles, PlayerColor player, ETileVisibility mode) override {} - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override {}; + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {}; void showInfoDialog(InfoWindow * iw) override {}; void showInfoDialog(const std::string & msg, PlayerColor player) override {}; diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 0a7861a3f..746c5ec2d 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -350,15 +350,16 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack) cl.invalidatePaths(); switch(pack.who) { - case GiveBonus::ETarget::HERO: + case GiveBonus::ETarget::OBJECT: { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.id)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); + const CGHeroInstance *h = gs.getHero(pack.id.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true); } break; case GiveBonus::ETarget::PLAYER: { - callInterfaceIfPresent(cl, PlayerColor(pack.id), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); + callInterfaceIfPresent(cl, pack.id.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, true); } break; } @@ -433,16 +434,17 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack) cl.invalidatePaths(); switch(pack.who) { - case GiveBonus::ETarget::HERO: + case GiveBonus::ETarget::OBJECT: { - const CGHeroInstance *h = gs.getHero(ObjectInstanceID(pack.whoID)); - callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); + const CGHeroInstance *h = gs.getHero(pack.whoID.as()); + if (h) + callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false); } break; case GiveBonus::ETarget::PLAYER: { //const PlayerState *p = gs.getPlayerState(pack.id); - callInterfaceIfPresent(cl, PlayerColor(pack.whoID), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); + callInterfaceIfPresent(cl, pack.whoID.as(), &IGameEventsReceiver::playerBonusChanged, pack.bonus, false); } break; } diff --git a/client/widgets/CAltar.cpp b/client/widgets/CAltar.cpp index ba76f47d3..a21766618 100644 --- a/client/widgets/CAltar.cpp +++ b/client/widgets/CAltar.cpp @@ -99,14 +99,15 @@ TExpType CAltarArtifacts::calcExpAltarForHero() void CAltarArtifacts::makeDeal() { - std::vector positions; + std::vector positions; for(const auto art : arts->artifactsOnAltar) { positions.push_back(hero->getSlotByInstance(art)); } - std::sort(positions.begin(), positions.end(), std::greater<>()); + std::sort(positions.begin(), positions.end()); + std::reverse(positions.begin(), positions.end()); - LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, {}, {}, hero); + LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector(), std::vector(), hero); arts->artifactsOnAltar.clear(); for(auto item : items[0]) @@ -374,14 +375,14 @@ void CAltarCreatures::makeDeal() unitsSlider->scrollTo(0); expForHero->setText(std::to_string(0)); - std::vector ids; + std::vector ids; std::vector toSacrifice; for(int i = 0; i < unitsOnAltar.size(); i++) { if(unitsOnAltar[i]) { - ids.push_back(i); + ids.push_back(SlotID(i)); toSacrifice.push_back(unitsOnAltar[i]); } } diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index e4e93337d..3ff4fc950 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -306,7 +306,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded) formations->resetCallback(); //setting formations formations->setSelected(curHero->formation == EArmyFormation::TIGHT ? 1 : 0); - formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, value);}); + formations->addCallback([=](int value){ LOCPLINT->cb->setFormation(curHero, static_cast(value));}); morale->set(curHero); luck->set(curHero); diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 5cb261793..ccea4ad11 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -503,14 +503,27 @@ void CMarketplaceWindow::makeDeal() if(allowDeal) { - if(slider) + switch(mode) { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, slider->getValue() * r1, hero); + case EMarketMode::RESOURCE_RESOURCE: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); slider->scrollTo(0); - } - else - { - LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, r2, hero); + break; + case EMarketMode::CREATURE_RESOURCE: + LOCPLINT->cb->trade(market, mode, SlotID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + case EMarketMode::RESOURCE_PLAYER: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), PlayerColor(hRight->id), slider->getValue() * r1, hero); + slider->scrollTo(0); + break; + + case EMarketMode::RESOURCE_ARTIFACT: + LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), ArtifactID(hRight->id), r2, hero); + break; + case EMarketMode::ARTIFACT_RESOURCE: + LOCPLINT->cb->trade(market, mode, ArtifactInstanceID(leftIdToSend), GameResID(hRight->id), r2, hero); + break; } } diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 0d0a1e710..9e5fa50d8 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -988,7 +988,7 @@ void CTransformerWindow::makeDeal() for(auto & elem : items) { if(!elem->left) - LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, elem->id, 0, 0, hero); + LOCPLINT->cb->trade(market, EMarketMode::CREATURE_UNDEAD, SlotID(elem->id), {}, {}, hero); } } @@ -1163,13 +1163,13 @@ void CUniversityWindow::close() CStatusbarWindow::close(); } -void CUniversityWindow::makeDeal(int skill) +void CUniversityWindow::makeDeal(SecondarySkill skill) { - LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, 6, skill, 1, hero); + LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_SKILL, GameResID(GameResID::GOLD), skill, 1, hero); } -CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bool available) +CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, SecondarySkill SKILL, bool available) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("UNIVERS2.PCX")), owner(owner_) { @@ -1204,7 +1204,7 @@ CUnivConfirmWindow::CUnivConfirmWindow(CUniversityWindow * owner_, int SKILL, bo statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); } -void CUnivConfirmWindow::makeDeal(int skill) +void CUnivConfirmWindow::makeDeal(SecondarySkill skill) { owner->makeDeal(skill); close(); diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 07b47b8eb..98413fa4a 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -403,7 +403,7 @@ class CUniversityWindow : public CStatusbarWindow public: CUniversityWindow(const CGHeroInstance * _hero, const IMarket * _market, const std::function & onWindowClosed); - void makeDeal(int skill); + void makeDeal(SecondarySkill skill); void close(); }; @@ -422,10 +422,10 @@ class CUnivConfirmWindow : public CStatusbarWindow std::shared_ptr costIcon; std::shared_ptr cost; - void makeDeal(int skill); + void makeDeal(SecondarySkill skill); public: - CUnivConfirmWindow(CUniversityWindow * PARENT, int SKILL, bool available); + CUnivConfirmWindow(CUniversityWindow * PARENT, SecondarySkill SKILL, bool available); }; /// Garrison window where you can take creatures out of the hero to place it on the garrison diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 85515c7c1..be60f369b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -484,12 +484,14 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/networkPacks/EOpenWindowMode.h ${MAIN_LIB_DIR}/networkPacks/NetPacksBase.h ${MAIN_LIB_DIR}/networkPacks/NetPackVisitor.h + ${MAIN_LIB_DIR}/networkPacks/ObjProperty.h ${MAIN_LIB_DIR}/networkPacks/PacksForClient.h ${MAIN_LIB_DIR}/networkPacks/PacksForClientBattle.h ${MAIN_LIB_DIR}/networkPacks/PacksForLobby.h ${MAIN_LIB_DIR}/networkPacks/PacksForServer.h ${MAIN_LIB_DIR}/networkPacks/SetStackEffect.h ${MAIN_LIB_DIR}/networkPacks/StackLocation.h + ${MAIN_LIB_DIR}/networkPacks/TradeItem.h ${MAIN_LIB_DIR}/pathfinder/INodeStorage.h ${MAIN_LIB_DIR}/pathfinder/CGPathNode.h diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 1a75578cc..8d79606ba 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -415,12 +415,9 @@ int CCreatureSet::stacksCount() const return static_cast(stacks.size()); } -void CCreatureSet::setFormation(bool tight) +void CCreatureSet::setFormation(EArmyFormation mode) { - if (tight) - formation = EArmyFormation::TIGHT; - else - formation = EArmyFormation::LOOSE; + formation = mode; } void CCreatureSet::setStackCount(const SlotID & slot, TQuantity count) diff --git a/lib/CCreatureSet.h b/lib/CCreatureSet.h index 9b3cad26c..305149b5b 100644 --- a/lib/CCreatureSet.h +++ b/lib/CCreatureSet.h @@ -203,12 +203,6 @@ public: } }; -enum class EArmyFormation : uint8_t -{ - LOOSE, - TIGHT -}; - namespace NArmyFormation { static const std::vector names{ "wide", "tight" }; @@ -235,7 +229,7 @@ public: void addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature void addToSlot(const SlotID & slot, CStackInstance * stack, bool allowMerging = true); //Adds stack to slot. Slot must be empty or with same type creature void clearSlots() override; - void setFormation(bool tight); + void setFormation(EArmyFormation tight); CArmedInstance *castToArmyObj(); //basic operations diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index dd80b9dde..ef9e95221 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -13,6 +13,7 @@ #include "CGameInfoCallback.h" // for CGameInfoCallback #include "CRandomGenerator.h" +#include "networkPacks/ObjProperty.h" VCMI_LIB_NAMESPACE_BEGIN @@ -73,14 +74,15 @@ public: class DLL_LINKAGE IGameEventCallback { public: - virtual void setObjProperty(ObjectInstanceID objid, int prop, si64 val) = 0; + virtual void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) = 0; + virtual void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) = 0; virtual void showInfoDialog(InfoWindow * iw) = 0; virtual void showInfoDialog(const std::string & msg, PlayerColor player) = 0; virtual void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells)=0; virtual bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) = 0; - virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) = 0; + virtual void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) = 0; virtual void setOwner(const CGObjectInstance * objid, PlayerColor owner)=0; virtual void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false)=0; virtual void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false)=0; diff --git a/lib/constants/Enumerations.h b/lib/constants/Enumerations.h index e358ed202..67ddc1b4a 100644 --- a/lib/constants/Enumerations.h +++ b/lib/constants/Enumerations.h @@ -246,4 +246,10 @@ enum class ETileVisibility : int8_t // Fog of war change REVEALED }; +enum class EArmyFormation : int8_t +{ + LOOSE, + TIGHT +}; + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 8ea6a6096..90269a3bb 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -13,7 +13,7 @@ VCMI_LIB_NAMESPACE_BEGIN -/// This class represents field that may contain value of multiple different identifer types +/// This class represents field that may contain value of multiple different identifier types template class VariantIdentifier { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index e62163e61..9f32ae1c2 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -94,12 +94,12 @@ void CBank::setConfig(const BankConfig & config) setCreature (SlotID(stacksCount()), stack.type->getId(), stack.count); } -void CBank::setPropertyDer (ui8 what, ui32 val) +void CBank::setPropertyDer (ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::BANK_DAYCOUNTER: //daycounter - daycounter+=val; + daycounter+= identifier.getNum(); break; case ObjProperty::BANK_RESET: // FIXME: Object reset must be done by separate netpack from server @@ -119,9 +119,9 @@ void CBank::newTurn(CRandomGenerator & rand) const if (resetDuration != 0) { if (daycounter >= resetDuration) - cb->setObjProperty (id, ObjProperty::BANK_RESET, 0); //daycounter 0 + cb->setObjPropertyValue(id, ObjProperty::BANK_RESET); //daycounter 0 else - cb->setObjProperty (id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ + cb->setObjPropertyValue(id, ObjProperty::BANK_DAYCOUNTER, 1); //daycounter++ } } } @@ -210,7 +210,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const case Obj::CRYPT: { GiveBonus gbonus; - gbonus.id = hero->id.getNum(); + gbonus.id = hero->id; gbonus.bonus.duration = BonusDuration::ONE_BATTLE; gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); @@ -240,7 +240,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const { GiveBonus gb; gb.bonus = Bonus(BonusDuration::ONE_BATTLE, BonusType::LUCK, BonusSource::OBJECT_INSTANCE, -2, BonusSourceID(id), VLC->generaltexth->arraytxt[70]); - gb.id = hero->id.getNum(); + gb.id = hero->id; cb->giveHeroBonus(&gb); textID = 107; iw.components.emplace_back(ComponentType::LUCK, -2); @@ -369,7 +369,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const cb->showInfoDialog(&iw); cb->giveCreatures(this, hero, ourArmy, false); } - cb->setObjProperty(id, ObjProperty::BANK_CLEAR, 0); //bc = nullptr + cb->setObjPropertyValue(id, ObjProperty::BANK_CLEAR); //bc = nullptr } } diff --git a/lib/mapObjects/CBank.h b/lib/mapObjects/CBank.h index 8f7105689..99bb4fdd7 100644 --- a/lib/mapObjects/CBank.h +++ b/lib/mapObjects/CBank.h @@ -23,7 +23,7 @@ class DLL_LINKAGE CBank : public CArmedInstance ui32 resetDuration; bool coastVisitable; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void doVisit(const CGHeroInstance * hero) const; public: diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index 48349beb5..f76915582 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -266,31 +266,28 @@ void CGCreature::newTurn(CRandomGenerator & rand) const if (stacks.begin()->second->count < VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP) && cb->getDate(Date::DAY_OF_WEEK) == 1 && cb->getDate(Date::DAY) > 1) { ui32 power = static_cast(temppower * (100 + VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_PERCENT)) / 100); - cb->setObjProperty(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, power); //increase temppower + cb->setObjPropertyValue(id, ObjProperty::MONSTER_COUNT, std::min(power / 1000, VLC->settings()->getInteger(EGameSettings::CREATURES_WEEKLY_GROWTH_CAP))); //set new amount + cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, power); //increase temppower } } if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) - cb->setObjProperty(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose + cb->setObjPropertyValue(id, ObjProperty::MONSTER_EXP, VLC->settings()->getInteger(EGameSettings::CREATURES_DAILY_STACK_EXPERIENCE)); //for testing purpose } -void CGCreature::setPropertyDer(ui8 what, ui32 val) +void CGCreature::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::MONSTER_COUNT: - stacks[SlotID(0)]->count = val; + stacks[SlotID(0)]->count = identifier.getNum(); break; case ObjProperty::MONSTER_POWER: - temppower = val; + temppower = identifier.getNum(); break; case ObjProperty::MONSTER_EXP: - giveStackExp(val); - break; - case ObjProperty::MONSTER_RESTORE_TYPE: - formation.basicType = val; + giveStackExp(identifier.getNum()); break; case ObjProperty::MONSTER_REFUSED_JOIN: - refusedJoining = val; + refusedJoining = identifier.getNum(); break; } } @@ -368,7 +365,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const void CGCreature::fleeDecision(const CGHeroInstance *h, ui32 pursue) const { if(refusedJoining) - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, false); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, false); if(pursue) { @@ -386,7 +383,7 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co { if(takenAction(h,false) == FLEE) { - cb->setObjProperty(id, ObjProperty::MONSTER_REFUSED_JOIN, true); + cb->setObjPropertyValue(id, ObjProperty::MONSTER_REFUSED_JOIN, true); flee(h); } else //they fight @@ -421,10 +418,6 @@ void CGCreature::joinDecision(const CGHeroInstance *h, int cost, ui32 accept) co void CGCreature::fight( const CGHeroInstance *h ) const { //split stacks - //TODO: multiple creature types in a stack? - int basicType = stacks.begin()->second->type->getId(); - cb->setObjProperty(id, ObjProperty::MONSTER_RESTORE_TYPE, basicType); //store info about creature stack - int stacksCount = getNumberOfStacks(h); //source: http://heroescommunity.com/viewthread.php3?TID=27539&PID=1266335#focus @@ -488,7 +481,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & { //merge stacks into one TSlots::const_iterator i; - CCreature * cre = VLC->creh->objects[formation.basicType]; + const CCreature * cre = getCreature().toCreature(); for(i = stacks.begin(); i != stacks.end(); i++) { if(cre->isMyUpgrade(i->second->type)) @@ -513,7 +506,7 @@ void CGCreature::battleFinished(const CGHeroInstance *hero, const BattleResult & cb->moveStack(StackLocation(this, i->first), StackLocation(this, slot), i->second->count); } - cb->setObjProperty(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties + cb->setObjPropertyValue(id, ObjProperty::MONSTER_POWER, stacks.begin()->second->count * 1000); //remember casualties } } diff --git a/lib/mapObjects/CGCreature.h b/lib/mapObjects/CGCreature.h index f2101d3b0..4d33ecdf3 100644 --- a/lib/mapObjects/CGCreature.h +++ b/lib/mapObjects/CGCreature.h @@ -54,17 +54,6 @@ public: bool containsUpgradedStack() const; int getNumberOfStacks(const CGHeroInstance *hero) const; - struct DLL_LINKAGE formationInfo // info about merging stacks after battle back into one - { - si32 basicType; - ui8 upgrade; //random seed used to determine number of stacks and is there's upgraded stack - template void serialize(Handler &h, const int version) - { - h & basicType; - h & upgrade; - } - } formation; - template void serialize(Handler &h, const int version) { h & static_cast(*this); @@ -80,7 +69,7 @@ public: h & formation; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; private: diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index 262aab99d..f405ed8d7 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -201,7 +201,7 @@ void CGDwelling::initObj(CRandomGenerator & rand) } } -void CGDwelling::setPropertyDer(ui8 what, ui32 val) +void CGDwelling::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -214,14 +214,14 @@ void CGDwelling::setPropertyDer(ui8 what, ui32 val) std::vector >* dwellings = &cb->gameState()->players[tempOwner].dwellings; dwellings->erase (std::find(dwellings->begin(), dwellings->end(), this)); } - if (PlayerColor(val) != PlayerColor::NEUTRAL) //can new owner be neutral? - cb->gameState()->players[PlayerColor(val)].dwellings.emplace_back(this); + if (identifier.as().isValidPlayer()) + cb->gameState()->players[identifier.as()].dwellings.emplace_back(this); } break; case ObjProperty::AVAILABLE_CREATURE: creatures.resize(1); creatures[0].second.resize(1); - creatures[0].second[0] = CreatureID(val); + creatures[0].second[0] = identifier.as(); break; } } @@ -300,7 +300,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const if(ID == Obj::REFUGEE_CAMP) //if it's a refugee camp, we need to pick an available creature { - cb->setObjProperty(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); + cb->setObjPropertyID(id, ObjProperty::AVAILABLE_CREATURE, VLC->creh->pickRandomMonster(rand)); } bool change = false; diff --git a/lib/mapObjects/CGDwelling.h b/lib/mapObjects/CGDwelling.h index 7bed887db..09fc96ca4 100644 --- a/lib/mapObjects/CGDwelling.h +++ b/lib/mapObjects/CGDwelling.h @@ -52,7 +52,7 @@ private: void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; std::vector getPopupComponents(PlayerColor player) const override; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 241493075..6fa16d8ce 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -348,7 +348,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if (gender == EHeroGender::DEFAULT) gender = type->gender; - setFormation(false); + setFormation(EArmyFormation::LOOSE); if (!stacksCount()) //standard army//initial army { initArmy(rand); @@ -501,7 +501,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const SetMovePoints smp; smp.hid = id; - cb->setManaPoints (id, manaLimit()); + cb->setManaPoints (id, manaLimit()); ObjectInstanceID boatId; const auto boatPos = visitablePos(); @@ -520,7 +520,7 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const smp.val = movementPointsLimit(true); } cb->giveHero(id, h->tempOwner, boatId); //recreates def and adds hero to player - cb->setObjProperty(id, ObjProperty::ID, Obj::HERO); //set ID to 34 AFTER hero gets correct flag color + cb->setObjPropertyID(id, ObjProperty::ID, Obj(Obj::HERO)); //set ID to 34 AFTER hero gets correct flag color cb->setMovePoints (&smp); h->showInfoDialog(102); @@ -620,10 +620,10 @@ void CGHeroInstance::updateSkillBonus(const SecondarySkill & which, int val) addNewBonus(std::make_shared(*b)); } -void CGHeroInstance::setPropertyDer( ui8 what, ui32 val ) +void CGHeroInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::PRIMARY_STACK_COUNT) - setStackCount(SlotID(0), val); + setStackCount(SlotID(0), identifier.getNum()); } double CGHeroInstance::getFightingStrength() const diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index a07ccff9c..2ddc83e2c 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -307,7 +307,7 @@ public: bool isCoastVisitable() const override; BattleField getBattlefield() const override; protected: - void setPropertyDer(ui8 what, ui32 val) override;//synchr + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;//synchr ///common part of hero instance and hero definition void serializeCommonOptions(JsonSerializeFormat & handler); diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index d6a1e433d..b429a84fe 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -91,7 +91,7 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const return; SetAvailableArtifacts saa; - saa.id = id.getNum(); + saa.id = id; cb->pickAllowedArtsSet(saa.arts, rand); cb->sendAndApply(&saa); } diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 74546c07c..3112c8e85 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -181,23 +181,20 @@ void CGObjectInstance::initObj(CRandomGenerator & rand) } } -void CGObjectInstance::setProperty( ui8 what, ui32 val ) +void CGObjectInstance::setProperty( ObjProperty what, ObjPropertyID identifier ) { - setPropertyDer(what, val); // call this before any actual changes (needed at least for dwellings) + setPropertyDer(what, identifier); // call this before any actual changes (needed at least for dwellings) switch(what) { case ObjProperty::OWNER: - tempOwner = PlayerColor(val); + tempOwner = identifier.as(); break; case ObjProperty::BLOCKVIS: - blockVisit = val; + blockVisit = identifier.getNum(); break; case ObjProperty::ID: - ID = Obj(val); - break; - case ObjProperty::SUBID: - subID = val; + ID = identifier.as(); break; } } @@ -207,7 +204,7 @@ TObjectTypeHandler CGObjectInstance::getObjectHandler() const return VLC->objtypeh->getHandlerFor(ID, subID); } -void CGObjectInstance::setPropertyDer( ui8 what, ui32 val ) +void CGObjectInstance::setPropertyDer( ObjProperty what, ObjPropertyID identifier ) {} int3 CGObjectInstance::getSightCenter() const @@ -229,7 +226,7 @@ void CGObjectInstance::giveDummyBonus(const ObjectInstanceID & heroID, BonusDura { GiveBonus gbonus; gbonus.bonus.type = BonusType::NONE; - gbonus.id = heroID.getNum(); + gbonus.id = heroID; gbonus.bonus.duration = duration; gbonus.bonus.source = BonusSource::OBJECT_TYPE; gbonus.bonus.sid = BonusSourceID(ID); diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 01eeffd2d..71cbabd41 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -127,7 +127,7 @@ public: void pickRandomObject(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; /// method for synchronous update. Note: For new properties classes should override setPropertyDer instead - void setProperty(ui8 what, ui32 val) final; + void setProperty(ObjProperty what, ObjPropertyID identifier) final; virtual void afterAddToMap(CMap * map); virtual void afterRemoveFromMap(CMap * map); @@ -154,7 +154,7 @@ public: protected: /// virtual method that allows synchronously update object state on server and all clients - virtual void setPropertyDer(ui8 what, ui32 val); + virtual void setPropertyDer(ObjProperty what, ObjPropertyID identifier); /// Called mostly during map randomization to turn random object into a regular one (e.g. "Random Monster" into "Pikeman") void setType(MapObjectID ID, MapObjectSubID subID); diff --git a/lib/mapObjects/CGTownBuilding.cpp b/lib/mapObjects/CGTownBuilding.cpp index 1406786f9..f00aa72f2 100644 --- a/lib/mapObjects/CGTownBuilding.cpp +++ b/lib/mapObjects/CGTownBuilding.cpp @@ -120,12 +120,12 @@ COPWBonus::COPWBonus(const BuildingID & bid, BuildingSubID::EBuildingSubID subId indexOnTV = static_cast(town->bonusingBuildings.size()); } -void COPWBonus::setProperty(ui8 what, ui32 val) +void COPWBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(val); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -148,7 +148,7 @@ void COPWBonus::onHeroVisit (const CGHeroInstance * h) const { GiveBonus gb; gb.bonus = Bonus(BonusDuration::ONE_WEEK, BonusType::MOVEMENT, BonusSource::OBJECT_TYPE, 600, BonusSourceID(Obj(Obj::STABLES)), BonusCustomSubtype::heroMovementLand, VLC->generaltexth->arraytxt[100]); - gb.id = heroID.getNum(); + gb.id = heroID; cb->giveHeroBonus(&gb); SetMovePoints mp; @@ -187,10 +187,10 @@ CTownBonus::CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID s indexOnTV = static_cast(town->bonusingBuildings.size()); } -void CTownBonus::setProperty (ui8 what, ui32 val) +void CTownBonus::setProperty(ObjProperty what, ObjPropertyID identifier) { if(what == ObjProperty::VISITORS) - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); } void CTownBonus::onHeroVisit (const CGHeroInstance * h) const @@ -272,7 +272,7 @@ void CTownBonus::applyBonuses(CGHeroInstance * h, const BonusList & bonuses) con bonus->duration = BonusDuration::ONE_DAY; } gb.bonus = * bonus; - gb.id = h->id.getNum(); + gb.id = h->id; cb->giveHeroBonus(&gb); if(bonus->duration == BonusDuration::PERMANENT) @@ -318,21 +318,21 @@ void CTownRewardableBuilding::newTurn(CRandomGenerator & rand) const { if(configuration.resetParameters.rewards) { - cb->setObjProperty(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::REWARD_RANDOMIZE, indexOnTV); } if(configuration.resetParameters.visitors) { - cb->setObjProperty(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); + cb->setObjPropertyValue(town->id, ObjProperty::STRUCTURE_CLEAR_VISITORS, indexOnTV); } } } -void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) +void CTownRewardableBuilding::setProperty(ObjProperty what, ObjPropertyID identifier) { switch (what) { case ObjProperty::VISITORS: - visitors.insert(ObjectInstanceID(val)); + visitors.insert(identifier.as()); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: visitors.clear(); @@ -341,7 +341,7 @@ void CTownRewardableBuilding::setProperty(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; } } diff --git a/lib/mapObjects/CGTownBuilding.h b/lib/mapObjects/CGTownBuilding.h index 77085f67f..47da63af5 100644 --- a/lib/mapObjects/CGTownBuilding.h +++ b/lib/mapObjects/CGTownBuilding.h @@ -69,8 +69,8 @@ protected: class DLL_LINKAGE COPWBonus : public CGTownBuilding {///used for OPW bonusing structures public: - std::set visitors; - void setProperty(ui8 what, ui32 val) override; + std::set visitors; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit (const CGHeroInstance * h) const override; COPWBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); @@ -89,7 +89,7 @@ class DLL_LINKAGE CTownBonus : public CGTownBuilding ///feel free to merge inheritance tree public: std::set visitors; - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit (const CGHeroInstance * h) const override; CTownBonus(const BuildingID & index, BuildingSubID::EBuildingSubID subId, CGTownInstance * TOWN); @@ -117,7 +117,7 @@ class DLL_LINKAGE CTownRewardableBuilding : public CGTownBuilding, public Reward void grantReward(ui32 rewardID, const CGHeroInstance * hero) const; public: - void setProperty(ui8 what, ui32 val) override; + void setProperty(ObjProperty what, ObjPropertyID identifier) override; void onHeroVisit(const CGHeroInstance * h) const override; void newTurn(CRandomGenerator & rand) const override; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 4916af24a..f2c331f50 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -53,28 +53,28 @@ int CGTownInstance::getSightRadius() const //returns sight distance return ret; } -void CGTownInstance::setPropertyDer(ui8 what, ui32 val) +void CGTownInstance::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { ///this is freakin' overcomplicated solution switch (what) { case ObjProperty::STRUCTURE_ADD_VISITING_HERO: - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, visitingHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, visitingHero->id); break; case ObjProperty::STRUCTURE_CLEAR_VISITORS: - bonusingBuildings[val]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::STRUCTURE_CLEAR_VISITORS, NumericID(0)); break; case ObjProperty::STRUCTURE_ADD_GARRISONED_HERO: //add garrisoned hero to visitors - bonusingBuildings[val]->setProperty(ObjProperty::VISITORS, garrisonHero->id.getNum()); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::VISITORS, garrisonHero->id); break; case ObjProperty::BONUS_VALUE_FIRST: - bonusValue.first = val; + bonusValue.first = identifier.getNum(); break; case ObjProperty::BONUS_VALUE_SECOND: - bonusValue.second = val; + bonusValue.second = identifier.getNum(); break; case ObjProperty::REWARD_RANDOMIZE: - bonusingBuildings[val]->setProperty(ObjProperty::REWARD_RANDOMIZE, 0); + bonusingBuildings[identifier.getNum()]->setProperty(ObjProperty::REWARD_RANDOMIZE, NumericID(0)); break; } } @@ -534,12 +534,12 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const resID = (resID==2)?1:resID; int resVal = rand.nextInt(1, 4);//with size 1..4 cb->giveResource(tempOwner, static_cast(resID), resVal); - cb->setObjProperty (id, ObjProperty::BONUS_VALUE_FIRST, resID); - cb->setObjProperty (id, ObjProperty::BONUS_VALUE_SECOND, resVal); + cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_FIRST, resID); + cb->setObjPropertyValue(id, ObjProperty::BONUS_VALUE_SECOND, resVal); } for(const auto * manaVortex : getBonusingBuildings(BuildingSubID::MANA_VORTEX)) - cb->setObjProperty(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_CLEAR_VISITORS, manaVortex->indexOnTV); //reset visitors for Mana Vortex //get Mana Vortex or Stables bonuses //same code is in the CGameHandler::buildStructure method @@ -1072,9 +1072,9 @@ CBuilding::TRequired CGTownInstance::genBuildingRequirements(const BuildingID & void CGTownInstance::addHeroToStructureVisitors(const CGHeroInstance *h, si64 structureInstanceID ) const { if(visitingHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_VISITING_HERO, structureInstanceID); //add to visitors else if(garrisonHero == h) - cb->setObjProperty(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero + cb->setObjPropertyValue(id, ObjProperty::STRUCTURE_ADD_GARRISONED_HERO, structureInstanceID); //then it must be garrisoned hero else { //should never ever happen diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 4a5e00511..37b6f7dfc 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -211,7 +211,7 @@ public: return defendingHero && garrisonHero && defendingHero != garrisonHero; } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override; diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 5868869ca..957c81757 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -544,18 +544,18 @@ std::vector CGSeerHut::getPopupComponents(const CGHeroInstance * hero return result; } -void CGSeerHut::setPropertyDer(ui8 what, ui32 val) +void CGSeerHut::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case CGSeerHut::SEERHUT_VISITED: + case ObjProperty::SEERHUT_VISITED: { - quest->activeForPlayers.emplace(val); + quest->activeForPlayers.emplace(identifier.as()); break; } - case CGSeerHut::SEERHUT_COMPLETE: + case ObjProperty::SEERHUT_COMPLETE: { - quest->isCompleted = val; + quest->isCompleted = identifier.getNum(); quest->activeForPlayers.clear(); break; } @@ -567,7 +567,7 @@ void CGSeerHut::newTurn(CRandomGenerator & rand) const CRewardableObject::newTurn(rand); if(quest->lastDay >= 0 && quest->lastDay <= cb->getDate() - 1) //time is up { - cb->setObjProperty (id, CGSeerHut::SEERHUT_COMPLETE, true); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, true); } } @@ -582,7 +582,7 @@ void CGSeerHut::onHeroVisit(const CGHeroInstance * h) const if(firstVisit) { - cb->setObjProperty(id, CGSeerHut::SEERHUT_VISITED, h->getOwner()); + cb->setObjPropertyID(id, ObjProperty::SEERHUT_VISITED, h->getOwner()); AddQuest aq; aq.quest = QuestInfo (quest, this, visitablePos()); @@ -665,7 +665,7 @@ void CGSeerHut::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) if(answer) { quest->completeQuest(cb, hero); - cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, !quest->repeatedQuest); //mission complete } } @@ -764,7 +764,7 @@ void CGQuestGuard::onHeroVisit(const CGHeroInstance * h) const if(!quest->isCompleted) CGSeerHut::onHeroVisit(h); else - cb->setObjProperty(id, CGSeerHut::SEERHUT_COMPLETE, false); + cb->setObjPropertyValue(id, ObjProperty::SEERHUT_COMPLETE, false); } bool CGQuestGuard::passableFor(PlayerColor color) const @@ -783,15 +783,14 @@ void CGKeys::reset() playerKeyMap.clear(); } -void CGKeys::setPropertyDer (ui8 what, ui32 val) //101-108 - enable key for player 1-8 +void CGKeys::setPropertyDer (ObjProperty what, ObjPropertyID identifier) { - if (what >= 101 && what <= (100 + PlayerColor::PLAYER_LIMIT_I)) + if (what == ObjProperty::KEYMASTER_VISITED) { - PlayerColor player(what-101); - playerKeyMap[player].insert(static_cast(val)); + playerKeyMap[identifier.as()].insert(subID); } else - logGlobal->error("Unexpected properties requested to set: what=%d, val=%d", static_cast(what), val); + logGlobal->error("Unexpected properties requested to set: what=%d, val=%d", static_cast(what), identifier.getNum()); } bool CGKeys::wasMyColorVisited(const PlayerColor & player) const @@ -819,7 +818,7 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const int txt_id; if (!wasMyColorVisited (h->getOwner()) ) { - cb->setObjProperty(id, h->tempOwner.getNum()+101, subID); + cb->setObjPropertyID(id, ObjProperty::KEYMASTER_VISITED, h->tempOwner); txt_id=19; } else diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 6f9dbd017..3dba2903d 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -144,10 +144,7 @@ public: h & seerName; } protected: - static constexpr int SEERHUT_VISITED = 10; - static constexpr int SEERHUT_COMPLETE = 11; - - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; void serializeJsonOptions(JsonSerializeFormat & handler) override; }; @@ -186,7 +183,7 @@ public: h & static_cast(*this); } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; class DLL_LINKAGE CGKeymasterTent : public CGKeys diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index 1d441b9f3..eaf9b797a 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -169,7 +169,7 @@ void CRewardableObject::blockingDialogAnswered(const CGHeroInstance *hero, ui32 void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, true); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, true); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_ADD, id, hero->id); cb->sendAndApply(&cov); @@ -177,7 +177,7 @@ void CRewardableObject::markAsVisited(const CGHeroInstance * hero) const void CRewardableObject::grantReward(ui32 rewardID, const CGHeroInstance * hero) const { - cb->setObjProperty(id, ObjProperty::REWARD_SELECT, rewardID); + cb->setObjPropertyValue(id, ObjProperty::REWARD_SELECT, rewardID); grantRewardBeforeLevelup(cb, configuration.info.at(rewardID), hero); // hero is not blocked by levelup dialog - grant remainer immediately @@ -338,7 +338,7 @@ std::vector CRewardableObject::getPopupComponents(const CGHeroInstanc return getPopupComponentsImpl(hero->getOwner(), hero); } -void CRewardableObject::setPropertyDer(ui8 what, ui32 val) +void CRewardableObject::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch (what) { @@ -346,10 +346,10 @@ void CRewardableObject::setPropertyDer(ui8 what, ui32 val) initObj(cb->gameState()->getRandomGenerator()); break; case ObjProperty::REWARD_SELECT: - selectedReward = val; + selectedReward = identifier.getNum(); break; case ObjProperty::REWARD_CLEARED: - onceVisitableObjectCleared = val; + onceVisitableObjectCleared = identifier.getNum(); break; } } @@ -360,11 +360,11 @@ void CRewardableObject::newTurn(CRandomGenerator & rand) const { if (configuration.resetParameters.rewards) { - cb->setObjProperty(id, ObjProperty::REWARD_RANDOMIZE, 0); + cb->setObjPropertyValue(id, ObjProperty::REWARD_RANDOMIZE, 0); } if (configuration.resetParameters.visitors) { - cb->setObjProperty(id, ObjProperty::REWARD_CLEARED, false); + cb->setObjPropertyValue(id, ObjProperty::REWARD_CLEARED, false); ChangeObjectVisitors cov(ChangeObjectVisitors::VISITOR_CLEAR, id); cb->sendAndApply(&cov); } diff --git a/lib/mapObjects/CRewardableObject.h b/lib/mapObjects/CRewardableObject.h index 3d9484dca..fa31f963d 100644 --- a/lib/mapObjects/CRewardableObject.h +++ b/lib/mapObjects/CRewardableObject.h @@ -65,7 +65,7 @@ public: void initObj(CRandomGenerator & rand) override; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; CRewardableObject(); diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index b0e0b11cd..caaa497b8 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -49,7 +49,7 @@ void IObjectInterface::initObj(CRandomGenerator & rand) void IObjectInterface::pickRandomObject(CRandomGenerator & rand) {} -void IObjectInterface::setProperty( ui8 what, ui32 val ) +void IObjectInterface::setProperty(ObjProperty what, ObjPropertyID identifier) {} bool IObjectInterface::wasVisited (PlayerColor player) const diff --git a/lib/mapObjects/IObjectInterface.h b/lib/mapObjects/IObjectInterface.h index 8dab1a59d..7cf9c8525 100644 --- a/lib/mapObjects/IObjectInterface.h +++ b/lib/mapObjects/IObjectInterface.h @@ -10,6 +10,7 @@ #pragma once #include "../networkPacks/EInfoWindowMode.h" +#include "../networkPacks/ObjProperty.h" #include "../constants/EntityIdentifiers.h" VCMI_LIB_NAMESPACE_BEGIN @@ -46,7 +47,7 @@ public: virtual void newTurn(CRandomGenerator & rand) const; virtual void initObj(CRandomGenerator & rand); //synchr virtual void pickRandomObject(CRandomGenerator & rand); - virtual void setProperty(ui8 what, ui32 val);//synchr + virtual void setProperty(ObjProperty what, ObjPropertyID identifier);//synchr //Called when queries created DURING HERO VISIT are resolved //First parameter is always hero that visited object and triggered the query diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ae7daee89..c82484c54 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -44,10 +44,10 @@ static std::string visitedTxt(const bool visited) return VLC->generaltexth->allTexts[id]; } -void CTeamVisited::setPropertyDer(ui8 what, ui32 val) +void CTeamVisited::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { - if(what == CTeamVisited::OBJPROP_VISITED) - players.insert(PlayerColor(val)); + if(what == ObjProperty::VISITED) + players.insert(identifier.as()); } bool CTeamVisited::wasVisited(PlayerColor player) const @@ -1195,13 +1195,13 @@ void CGObelisk::onHeroVisit( const CGHeroInstance * h ) const cb->sendAndApply(&iw); // increment general visited obelisks counter - cb->setObjProperty(id, CGObelisk::OBJPROP_INC, team.getNum()); + cb->setObjPropertyID(id, ObjProperty::OBELISK_VISITED, team); cb->showObjectWindow(this, EOpenWindowMode::PUZZLE_MAP, h, false); // mark that particular obelisk as visited for all players in the team for(const auto & color : ts->players) { - cb->setObjProperty(id, CGObelisk::OBJPROP_VISITED, color.getNum()); + cb->setObjPropertyID(id, ObjProperty::VISITED, color); } } else @@ -1228,14 +1228,14 @@ std::string CGObelisk::getHoverText(PlayerColor player) const return getObjectName() + " " + visitedTxt(wasVisited(player)); } -void CGObelisk::setPropertyDer( ui8 what, ui32 val ) +void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) { switch(what) { - case CGObelisk::OBJPROP_INC: + case ObjProperty::OBELISK_VISITED: { - auto progress = ++visited[TeamID(val)]; - logGlobal->debug("Player %d: obelisk progress %d / %d", val, static_cast(progress) , static_cast(obeliskCount)); + auto progress = ++visited[identifier.as()]; + logGlobal->debug("Player %d: obelisk progress %d / %d", identifier.getNum(), static_cast(progress) , static_cast(obeliskCount)); if(progress > obeliskCount) { @@ -1246,7 +1246,7 @@ void CGObelisk::setPropertyDer( ui8 what, ui32 val ) break; } default: - CTeamVisited::setPropertyDer(what, val); + CTeamVisited::setPropertyDer(what, identifier); break; } } @@ -1263,7 +1263,7 @@ void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const if(oldOwner.isValidPlayer()) //remove bonus from old owner { RemoveBonus rb(GiveBonus::ETarget::PLAYER); - rb.whoID = oldOwner.getNum(); + rb.whoID = oldOwner; rb.source = BonusSource::OBJECT_INSTANCE; rb.id = BonusSourceID(id); cb->sendAndApply(&rb); @@ -1285,7 +1285,7 @@ void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const GiveBonus gb(GiveBonus::ETarget::PLAYER); gb.bonus.type = BonusType::MOVEMENT; gb.bonus.val = 500; - gb.id = player.getNum(); + gb.id = player; gb.bonus.duration = BonusDuration::PERMANENT; gb.bonus.source = BonusSource::OBJECT_INSTANCE; gb.bonus.sid = BonusSourceID(id); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index 6851888fa..de8ae3862 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -29,15 +29,13 @@ public: bool wasVisited (const CGHeroInstance * h) const override; bool wasVisited(PlayerColor player) const override; bool wasVisited(const TeamID & team) const; - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; template void serialize(Handler &h, const int version) { h & static_cast(*this); h & players; } - - static constexpr int OBJPROP_VISITED = 10; }; class DLL_LINKAGE CGSignBottle : public CGObjectInstance //signs and ocean bottles @@ -361,7 +359,6 @@ class DLL_LINKAGE CGDenOfthieves : public CGObjectInstance class DLL_LINKAGE CGObelisk : public CTeamVisited { public: - static constexpr int OBJPROP_INC = 20; static ui8 obeliskCount; //how many obelisks are on map static std::map visited; //map: team_id => how many obelisks has been visited @@ -375,7 +372,7 @@ public: h & static_cast(*this); } protected: - void setPropertyDer(ui8 what, ui32 val) override; + void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; class DLL_LINKAGE CGLighthouse : public CGObjectInstance diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index c7041fd2e..41890cf47 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -971,18 +971,15 @@ void GiveBonus::applyGs(CGameState *gs) CBonusSystemNode *cbsn = nullptr; switch(who) { - case ETarget::HERO: - cbsn = gs->getHero(ObjectInstanceID(id)); + case ETarget::OBJECT: + cbsn = dynamic_cast(gs->getObjInstance(id.as())); break; case ETarget::PLAYER: - cbsn = gs->getPlayerState(PlayerColor(id)); - break; - case ETarget::TOWN: - cbsn = gs->getTown(ObjectInstanceID(id)); + cbsn = gs->getPlayerState(id.as()); break; case ETarget::BATTLE: assert(Bonus::OneBattle(&bonus)); - cbsn = dynamic_cast(gs->getBattle(BattleID(id))); + cbsn = dynamic_cast(gs->getBattle(id.as())); break; } @@ -1114,11 +1111,20 @@ void PlayerReinitInterface::applyGs(CGameState *gs) void RemoveBonus::applyGs(CGameState *gs) { - CBonusSystemNode * node = nullptr; - if (who == GiveBonus::ETarget::HERO) - node = gs->getHero(ObjectInstanceID(whoID)); - else - node = gs->getPlayerState(PlayerColor(whoID)); + CBonusSystemNode *node = nullptr; + switch(who) + { + case GiveBonus::ETarget::OBJECT: + node = dynamic_cast(gs->getObjInstance(whoID.as())); + break; + case GiveBonus::ETarget::PLAYER: + node = gs->getPlayerState(whoID.as()); + break; + case GiveBonus::ETarget::BATTLE: + assert(Bonus::OneBattle(&bonus)); + node = dynamic_cast(gs->getBattle(whoID.as())); + break; + } BonusList &bonuses = node->getExportedBonusList(); @@ -1483,7 +1489,7 @@ void NewObject::applyGs(CGameState *gs) const TerrainTile & t = gs->map->getTile(targetPos); terrainType = t.terType->getId(); - auto handler = VLC->objtypeh->getHandlerFor(ID, subID); + auto handler = VLC->objtypeh->getHandlerFor(ID, subID.getNum()); CGObjectInstance * o = handler->create(); handler->configureObject(o, gs->getRandomGenerator()); @@ -1499,13 +1505,13 @@ void NewObject::applyGs(CGameState *gs) cre->character = 2; cre->gainedArtifact = ArtifactID::NONE; cre->identifier = -1; - cre->addToSlot(SlotID(0), new CStackInstance(CreatureID(subID), -1)); //add placeholder stack + cre->addToSlot(SlotID(0), new CStackInstance(subID.as(), -1)); //add placeholder stack } assert(!handler->getTemplates(terrainType).empty()); if (handler->getTemplates().empty()) { - logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID); + logGlobal->error("Attempt to create object (%d %d) with no templates!", ID, subID.getNum()); return; } @@ -2049,9 +2055,9 @@ void SetObjectProperty::applyGs(CGameState * gs) const if(state->towns.empty()) state->daysWithoutCastle = 0; } - if(PlayerColor(val).isValidPlayer()) + if(identifier.as().isValidPlayer()) { - PlayerState * p = gs->getPlayerState(PlayerColor(val)); + PlayerState * p = gs->getPlayerState(identifier.as()); p->towns.emplace_back(t); //reset counter before NewTurn to avoid no town message if game loaded at turn when one already captured @@ -2062,12 +2068,12 @@ void SetObjectProperty::applyGs(CGameState * gs) const CBonusSystemNode & nodeToMove = cai->whatShouldBeAttached(); nodeToMove.detachFrom(cai->whereShouldBeAttached(gs)); - obj->setProperty(what,val); + obj->setProperty(what, identifier); nodeToMove.attachTo(cai->whereShouldBeAttached(gs)); } else //not an armed instance { - obj->setProperty(what,val); + obj->setProperty(what, identifier); } } diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h new file mode 100644 index 000000000..b375ef476 --- /dev/null +++ b/lib/networkPacks/ObjProperty.h @@ -0,0 +1,69 @@ +/* + * ObjProperty.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 "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +enum class ObjProperty : int8_t +{ + INVALID, + OWNER, + BLOCKVIS, + PRIMARY_STACK_COUNT, + VISITORS, + VISITED, + ID, + AVAILABLE_CREATURE, + MONSTER_COUNT, + MONSTER_POWER, + MONSTER_EXP, + MONSTER_RESTORE_TYPE, + MONSTER_REFUSED_JOIN, + + //town-specific + STRUCTURE_ADD_VISITING_HERO, + STRUCTURE_CLEAR_VISITORS, + STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state + BONUS_VALUE_FIRST, + BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) + + SEERHUT_VISITED, + SEERHUT_COMPLETE, + KEYMASTER_VISITED, + OBELISK_VISITED, + + //creature-bank specific + BANK_DAYCOUNTER, + BANK_RESET, + BANK_CLEAR, + + //object with reward + REWARD_RANDOMIZE, + REWARD_SELECT, + REWARD_CLEARED +}; + +class NumericID : public Identifier +{ +public: + using Identifier::Identifier; + + static si32 decode(const std::string & identifier) + { + return std::stoi(identifier); + } + static std::string encode(const si32 index) + { + return std::to_string(index); + } +}; + +using ObjPropertyID = VariantIdentifier; diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index f768c297d..7066f3315 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -15,6 +15,7 @@ #include "EOpenWindowMode.h" #include "EntityChanges.h" #include "NetPacksBase.h" +#include "ObjProperty.h" #include "../CCreatureSet.h" #include "../MetaString.h" @@ -366,17 +367,17 @@ struct DLL_LINKAGE SetAvailableHero : public CPackForClient struct DLL_LINKAGE GiveBonus : public CPackForClient { - enum class ETarget : ui8 { HERO, PLAYER, TOWN, BATTLE }; + enum class ETarget : int8_t { OBJECT, PLAYER, BATTLE }; - explicit GiveBonus(ETarget Who = ETarget::HERO) + explicit GiveBonus(ETarget Who = ETarget::OBJECT) :who(Who) { } void applyGs(CGameState * gs); - ETarget who = ETarget::HERO; //who receives bonus - si32 id = 0; //hero. town or player id - whoever receives it + ETarget who = ETarget::OBJECT; + VariantIdentifier id; Bonus bonus; MetaString bdescr; @@ -388,7 +389,7 @@ struct DLL_LINKAGE GiveBonus : public CPackForClient h & id; h & bdescr; h & who; - assert(id != -1); + assert(id.getNum() != -1); } }; @@ -461,7 +462,7 @@ struct DLL_LINKAGE PlayerReinitInterface : public CPackForClient struct DLL_LINKAGE RemoveBonus : public CPackForClient { - explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::HERO) + explicit RemoveBonus(GiveBonus::ETarget Who = GiveBonus::ETarget::OBJECT) :who(Who) { } @@ -469,7 +470,7 @@ struct DLL_LINKAGE RemoveBonus : public CPackForClient void applyGs(CGameState * gs); GiveBonus::ETarget who; //who receives bonus - ui32 whoID = 0; //hero, town or player id - whoever loses bonus + VariantIdentifier whoID; //vars to identify bonus: its source BonusSource source; @@ -574,7 +575,7 @@ struct DLL_LINKAGE UpdateCastleEvents : public CPackForClient struct DLL_LINKAGE ChangeFormation : public CPackForClient { ObjectInstanceID hid; - ui8 formation = 0; + EArmyFormation formation{}; void applyGs(CGameState * gs) const; void visitTyped(ICPackVisitor & visitor) override; @@ -781,9 +782,9 @@ struct DLL_LINKAGE NewObject : public CPackForClient void applyGs(CGameState * gs); /// Object ID to create - Obj ID; + MapObjectID ID; /// Object secondary ID to create - ui32 subID = 0; + VariantIdentifier subID; /// Position of visitable tile of created object int3 targetPos; /// Which player initiated creation of this object @@ -806,7 +807,8 @@ struct DLL_LINKAGE SetAvailableArtifacts : public CPackForClient { void applyGs(CGameState * gs) const; - si32 id = 0; //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) + //two variants: id < 0: set artifact pool for Artifact Merchants in towns; id >= 0: set pool for adv. map Black Market (id is the id of Black Market instance then) + ObjectInstanceID id; std::vector arts; void visitTyped(ICPackVisitor & visitor) override; @@ -1207,38 +1209,16 @@ struct DLL_LINKAGE InfoWindow : public CPackForClient //103 - displays simple i InfoWindow() = default; }; -namespace ObjProperty -{ - enum - { - OWNER = 1, BLOCKVIS = 2, PRIMARY_STACK_COUNT = 3, VISITORS = 4, VISITED = 5, ID = 6, AVAILABLE_CREATURE = 7, SUBID = 8, - MONSTER_COUNT = 10, MONSTER_POWER = 11, MONSTER_EXP = 12, MONSTER_RESTORE_TYPE = 13, MONSTER_REFUSED_JOIN, - - //town-specific - STRUCTURE_ADD_VISITING_HERO, STRUCTURE_CLEAR_VISITORS, STRUCTURE_ADD_GARRISONED_HERO, //changing buildings state - BONUS_VALUE_FIRST, BONUS_VALUE_SECOND, //used in Rampart for special building that generates resources (storing resource type and quantity) - - //creature-bank specific - BANK_DAYCOUNTER, BANK_RESET, BANK_CLEAR, - - //object with reward - REWARD_RANDOMIZE, REWARD_SELECT, REWARD_CLEARED - }; -} - struct DLL_LINKAGE SetObjectProperty : public CPackForClient { void applyGs(CGameState * gs) const; ObjectInstanceID id; - ui8 what = 0; // see ObjProperty enum - ui32 val = 0; + ObjProperty what{}; + + ObjPropertyID identifier; + int32_t value = 0; + SetObjectProperty() = default; - SetObjectProperty(const ObjectInstanceID & ID, ui8 What, ui32 Val) - : id(ID) - , what(What) - , val(Val) - { - } void visitTyped(ICPackVisitor & visitor) override; @@ -1246,7 +1226,8 @@ struct DLL_LINKAGE SetObjectProperty : public CPackForClient { h & id; h & what; - h & val; + h & identifier; + h & value; } }; diff --git a/lib/networkPacks/PacksForServer.h b/lib/networkPacks/PacksForServer.h index 9911d1ba8..de1d7d613 100644 --- a/lib/networkPacks/PacksForServer.h +++ b/lib/networkPacks/PacksForServer.h @@ -11,6 +11,7 @@ #include "ArtifactLocation.h" #include "NetPacksBase.h" +#include "TradeItem.h" #include "../int3.h" #include "../battle/BattleAction.h" @@ -472,7 +473,8 @@ struct DLL_LINKAGE TradeOnMarketplace : public CPackForServer ObjectInstanceID heroId; EMarketMode mode = EMarketMode::RESOURCE_RESOURCE; - std::vector r1, r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] + std::vector r1; + std::vector r2; //mode 0: r1 - sold resource, r2 - bought res (exception: when sacrificing art r1 is art id [todo: make r2 preferred slot?] std::vector val; //units of sold resource void visitTyped(ICPackVisitor & visitor) override; @@ -493,13 +495,13 @@ struct DLL_LINKAGE SetFormation : public CPackForServer { SetFormation() = default; ; - SetFormation(const ObjectInstanceID & HID, ui8 Formation) + SetFormation(const ObjectInstanceID & HID, EArmyFormation Formation) : hid(HID) , formation(Formation) { } ObjectInstanceID hid; - ui8 formation = 0; + EArmyFormation formation{}; void visitTyped(ICPackVisitor & visitor) override; diff --git a/lib/networkPacks/TradeItem.h b/lib/networkPacks/TradeItem.h new file mode 100644 index 000000000..43846d1d1 --- /dev/null +++ b/lib/networkPacks/TradeItem.h @@ -0,0 +1,20 @@ +/* + * TradeItem.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 "../constants/VariantIdentifier.h" +#include "../constants/EntityIdentifiers.h" + +VCMI_LIB_NAMESPACE_BEGIN + +using TradeItemSell = VariantIdentifier; +using TradeItemBuy = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index b1f41e8ce..b7cc60c25 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -150,9 +150,9 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re for(const Bonus & bonus : info.reward.bonuses) { GiveBonus gb; - gb.who = GiveBonus::ETarget::HERO; + gb.who = GiveBonus::ETarget::OBJECT; gb.bonus = bonus; - gb.id = hero->id.getNum(); + gb.id = hero->id; cb->giveHeroBonus(&gb); } diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index 2f8b056b3..b6b440d02 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -83,7 +83,7 @@ ESpellCastResult AdventureSpellMechanics::applyAdventureEffects(SpellCastEnviron for(const Bonus & b : bonuses) { GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = b; env->apply(&gb); } @@ -323,7 +323,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm } GiveBonus gb; - gb.id = parameters.caster->getCasterUnitId(); + gb.id = ObjectInstanceID(parameters.caster->getCasterUnitId()); gb.bonus = Bonus(BonusDuration::ONE_DAY, BonusType::NONE, BonusSource::SPELL_EFFECT, 0, BonusSourceID(owner->id)); env->apply(&gb); diff --git a/lib/spells/effects/Moat.cpp b/lib/spells/effects/Moat.cpp index 8546f779b..359f2460b 100644 --- a/lib/spells/effects/Moat.cpp +++ b/lib/spells/effects/Moat.cpp @@ -117,7 +117,7 @@ void Moat::apply(ServerCallback * server, const Mechanics * m, const EffectTarge for(auto & b : converted) { GiveBonus gb(GiveBonus::ETarget::BATTLE); - gb.id = m->battle()->getBattle()->getBattleID().getNum(); + gb.id = m->battle()->getBattle()->getBattleID(); gb.bonus = b; server->apply(&gb); } diff --git a/mapeditor/inspector/armywidget.cpp b/mapeditor/inspector/armywidget.cpp index 65740a54a..fcba00cf0 100644 --- a/mapeditor/inspector/armywidget.cpp +++ b/mapeditor/inspector/armywidget.cpp @@ -101,7 +101,7 @@ bool ArmyWidget::commitChanges() } } - army.setFormation(ui->formationTight->isChecked()); + army.setFormation(ui->formationTight->isChecked() ? EArmyFormation::TIGHT : EArmyFormation::LOOSE ); return isArmed; } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 0f2d4ca3c..32463f6c3 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -886,7 +886,7 @@ void CGameHandler::onNewTurn() if (newMonth) { SetAvailableArtifacts saa; - saa.id = -1; + saa.id = ObjectInstanceID::NONE; pickAllowedArtsSet(saa.arts, getRandomGenerator()); sendAndApply(&saa); } @@ -1328,8 +1328,8 @@ bool CGameHandler::teleportHero(ObjectInstanceID hid, ObjectInstanceID dstid, ui void CGameHandler::setOwner(const CGObjectInstance * obj, const PlayerColor owner) { PlayerColor oldOwner = getOwner(obj->id); - SetObjectProperty sop(obj->id, ObjProperty::OWNER, owner.getNum()); - sendAndApply(&sop); + + setObjPropertyID(obj->id, ObjProperty::OWNER, owner); std::set playerColors = {owner, oldOwner}; checkVictoryLossConditions(playerColors); @@ -2973,12 +2973,12 @@ bool CGameHandler::buyArtifact(const IMarket *m, const CGHeroInstance *h, GameRe SetAvailableArtifacts saa; if(dynamic_cast(m)) { - saa.id = -1; + saa.id = ObjectInstanceID::NONE; saa.arts = CGTownInstance::merchantArtifacts; } else if(const CGBlackMarket *bm = dynamic_cast(m)) //black market { - saa.id = bm->id.getNum(); + saa.id = bm->id; saa.arts = bm->artifacts; } else @@ -3044,23 +3044,23 @@ bool CGameHandler::buySecSkill(const IMarket *m, const CGHeroInstance *h, Second return true; } -bool CGameHandler::tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2) +bool CGameHandler::tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy) { - TResourceCap r1 = getPlayerState(player)->resources[id1]; + TResourceCap haveToSell = getPlayerState(player)->resources[toSell]; - vstd::amin(val, r1); //can't trade more resources than have + vstd::amin(amountToSell, haveToSell); //can't trade more resources than have int b1, b2; //base quantities for trade - market->getOffer(id1, id2, b1, b2, EMarketMode::RESOURCE_RESOURCE); - int units = val / b1; //how many base quantities we trade + market->getOffer(toSell, toBuy, b1, b2, EMarketMode::RESOURCE_RESOURCE); + int amountToBoy = amountToSell / b1; //how many base quantities we trade - if (val%b1) //all offered units of resource should be used, if not -> somewhere in calculations must be an error + if (amountToSell % b1 != 0) //all offered units of resource should be used, if not -> somewhere in calculations must be an error { COMPLAIN_RET("Invalid deal, not all offered units of resource were used."); } - giveResource(player, GameResID(id1), - b1 * units); - giveResource(player, GameResID(id2), b2 * units); + giveResource(player, toSell, -b1 * amountToBoy); + giveResource(player, toBuy, b2 * amountToBoy); return true; } @@ -3144,7 +3144,7 @@ bool CGameHandler::sendResources(ui32 val, PlayerColor player, GameResID r1, Pla return true; } -bool CGameHandler::setFormation(ObjectInstanceID hid, ui8 formation) +bool CGameHandler::setFormation(ObjectInstanceID hid, EArmyFormation formation) { const CGHeroInstance *h = getHero(hid); if (!h) @@ -4047,8 +4047,8 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) createObject(*tile, PlayerColor::NEUTRAL, Obj::MONSTER, creatureID); auto monsterId = getTopObj(*tile)->id; - setObjProperty(monsterId, ObjProperty::MONSTER_COUNT, count); - setObjProperty(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_COUNT, count); + setObjPropertyValue(monsterId, ObjProperty::MONSTER_POWER, (si64)1000*count); } tiles.erase(tile); //not use it again } @@ -4170,12 +4170,21 @@ bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, con return true; } -void CGameHandler::setObjProperty(ObjectInstanceID objid, int prop, si64 val) +void CGameHandler::setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) { SetObjectProperty sob; sob.id = objid; sob.what = prop; - sob.val = static_cast(val); + sob.identifier = NumericID(value); + sendAndApply(&sob); +} + +void CGameHandler::setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) +{ + SetObjectProperty sob; + sob.id = objid; + sob.what = prop; + sob.identifier = identifier; sendAndApply(&sob); } @@ -4209,7 +4218,7 @@ scripting::Pool * CGameHandler::getGlobalContextPool() const //} #endif -void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype) +void CGameHandler::createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) { NewObject no; no.ID = type; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index a71d6a1dd..e100bfcbd 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -101,7 +101,7 @@ public: //do sth void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override; bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override; - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype ) override; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override; void setOwner(const CGObjectInstance * obj, PlayerColor owner) override; void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override; void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override; @@ -156,7 +156,8 @@ public: /// Returns hero that is currently visiting this object, or nullptr if no visit is active const CGHeroInstance * getVisitingHero(const CGObjectInstance *obj); bool isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, const CGHeroInstance *hero) override; - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override; + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value) override; + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override; void showInfoDialog(InfoWindow * iw) override; void showInfoDialog(const std::string & msg, PlayerColor player) override; @@ -182,8 +183,8 @@ public: bool queryReply( QueryID qid, std::optional reply, PlayerColor player ); bool buildBoat( ObjectInstanceID objid, PlayerColor player ); - bool setFormation( ObjectInstanceID hid, ui8 formation ); - bool tradeResources(const IMarket *market, ui32 val, PlayerColor player, ui32 id1, ui32 id2); + bool setFormation( ObjectInstanceID hid, EArmyFormation formation ); + bool tradeResources(const IMarket *market, ui32 amountToSell, PlayerColor player, GameResID toSell, GameResID toBuy); bool sacrificeCreatures(const IMarket * market, const CGHeroInstance * hero, const std::vector & slot, const std::vector & count); bool sendResources(ui32 val, PlayerColor player, GameResID r1, PlayerColor r2); bool sellCreatures(ui32 count, const IMarket *market, const CGHeroInstance * hero, SlotID slot, GameResID resourceID); diff --git a/server/NetPacksServer.cpp b/server/NetPacksServer.cpp index c5e03acf5..a3d77db31 100644 --- a/server/NetPacksServer.cpp +++ b/server/NetPacksServer.cpp @@ -221,42 +221,49 @@ void ApplyGhNetPackVisitor::visitTradeOnMarketplace(TradeOnMarketplace & pack) { case EMarketMode::RESOURCE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i], pack.r2[i]); + result &= gh.tradeResources(market, pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::RESOURCE_PLAYER: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sendResources(pack.val[i], pack.player, GameResID(pack.r1[i]), PlayerColor(pack.r2[i])); + result &= gh.sendResources(pack.val[i], pack.player, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::CREATURE_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellCreatures(pack.val[i], market, hero, SlotID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellCreatures(pack.val[i], market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::RESOURCE_ARTIFACT: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.buyArtifact(market, hero, GameResID(pack.r1[i]), ArtifactID(pack.r2[i])); + result &= gh.buyArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::ARTIFACT_RESOURCE: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.sellArtifact(market, hero, ArtifactInstanceID(pack.r1[i]), GameResID(pack.r2[i])); + result &= gh.sellArtifact(market, hero, pack.r1[i].as(), pack.r2[i].as()); break; case EMarketMode::CREATURE_UNDEAD: for(int i = 0; i < pack.r1.size(); ++i) - result &= gh.transformInUndead(market, hero, SlotID(pack.r1[i])); + result &= gh.transformInUndead(market, hero, pack.r1[i].as()); break; case EMarketMode::RESOURCE_SKILL: for(int i = 0; i < pack.r2.size(); ++i) - result &= gh.buySecSkill(market, hero, SecondarySkill(pack.r2[i])); + result &= gh.buySecSkill(market, hero, pack.r2[i].as()); break; case EMarketMode::CREATURE_EXP: { - std::vector slotIDs(pack.r1.begin(), pack.r1.end()); + std::vector slotIDs; std::vector count(pack.val.begin(), pack.val.end()); + + for(auto const & slot : pack.r1) + slotIDs.push_back(slot.as()); + result = gh.sacrificeCreatures(market, hero, slotIDs, count); return; } case EMarketMode::ARTIFACT_EXP: { - std::vector positions(pack.r1.begin(), pack.r1.end()); + std::vector positions; + for(auto const & slot : pack.r1) + positions.push_back(slot.as()); + result = gh.sacrificeArtifact(market, hero, positions); return; } diff --git a/server/battles/BattleProcessor.cpp b/server/battles/BattleProcessor.cpp index e32811505..00a238dd1 100644 --- a/server/battles/BattleProcessor.cpp +++ b/server/battles/BattleProcessor.cpp @@ -122,8 +122,8 @@ void BattleProcessor::startBattlePrimary(const CArmedInstance *army1, const CArm { for(auto bonus : attackerInfo->battleBonuses) { - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero1->id.getNum(); + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero1->id; giveBonus.bonus = bonus; gameHandler->sendAndApply(&giveBonus); } diff --git a/server/processors/PlayerMessageProcessor.cpp b/server/processors/PlayerMessageProcessor.cpp index c2d319380..030169019 100644 --- a/server/processors/PlayerMessageProcessor.cpp +++ b/server/processors/PlayerMessageProcessor.cpp @@ -139,8 +139,8 @@ void PlayerMessageProcessor::cheatGiveSpells(PlayerColor player, const CGHeroIns gameHandler->giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); ///Give all spells with bonus (to allow banned spells) - GiveBonus giveBonus(GiveBonus::ETarget::HERO); - giveBonus.id = hero->id.getNum(); + GiveBonus giveBonus(GiveBonus::ETarget::OBJECT); + giveBonus.id = hero->id; giveBonus.bonus = Bonus(BonusDuration::PERMANENT, BonusType::SPELLS_OF_LEVEL, BonusSource::OTHER, 0, BonusSourceID()); //start with level 0 to skip abilities for (int level = 1; level <= GameConstants::SPELL_LEVELS; level++) @@ -300,11 +300,11 @@ void PlayerMessageProcessor::cheatMovement(PlayerColor player, const CGHeroInsta gameHandler->sendAndApply(&smp); - GiveBonus gb(GiveBonus::ETarget::HERO); + GiveBonus gb(GiveBonus::ETarget::OBJECT); gb.bonus.type = BonusType::FREE_SHIP_BOARDING; gb.bonus.duration = BonusDuration::ONE_DAY; gb.bonus.source = BonusSource::OTHER; - gb.id = hero->id.getNum(); + gb.id = hero->id; gameHandler->giveHeroBonus(&gb); } From 32a2e540d3fa4739a6e8217895809ae69583dd3c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 6 Nov 2023 21:20:44 +0200 Subject: [PATCH 1071/1248] Fix build --- lib/networkPacks/ObjProperty.h | 4 ++++ lib/networkPacks/PacksForClient.h | 2 -- test/mock/mock_IGameCallback.h | 5 +++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h index b375ef476..15b55afba 100644 --- a/lib/networkPacks/ObjProperty.h +++ b/lib/networkPacks/ObjProperty.h @@ -12,6 +12,8 @@ #include "../constants/VariantIdentifier.h" #include "../constants/EntityIdentifiers.h" +VCMI_LIB_NAMESPACE_BEGIN + enum class ObjProperty : int8_t { INVALID, @@ -67,3 +69,5 @@ public: }; using ObjPropertyID = VariantIdentifier; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 7066f3315..7de437f00 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1216,7 +1216,6 @@ struct DLL_LINKAGE SetObjectProperty : public CPackForClient ObjProperty what{}; ObjPropertyID identifier; - int32_t value = 0; SetObjectProperty() = default; @@ -1227,7 +1226,6 @@ struct DLL_LINKAGE SetObjectProperty : public CPackForClient h & id; h & what; h & identifier; - h & value; } }; diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index f92c1690c..a772469fe 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -35,13 +35,14 @@ public: //TODO: fail all stub calls - void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {} + void setObjPropertyValue(ObjectInstanceID objid, ObjProperty prop, int32_t value = 0) override {} + void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {} void showInfoDialog(InfoWindow * iw) override {} void showInfoDialog(const std::string & msg, PlayerColor player) override {} void changeSpells(const CGHeroInstance * hero, bool give, const std::set &spells) override {} bool removeObject(const CGObjectInstance * obj, const PlayerColor & initiator) override {return false;} - void createObject(const int3 & visitablePosition, const PlayerColor & initiator, Obj type, int32_t subtype = 0) override {}; + void createObject(const int3 & visitablePosition, const PlayerColor & initiator, MapObjectID type, MapObjectSubID subtype) override {}; void setOwner(const CGObjectInstance * objid, PlayerColor owner) override {} void changePrimSkill(const CGHeroInstance * hero, PrimarySkill which, si64 val, bool abs=false) override {} void changeSecSkill(const CGHeroInstance * hero, SecondarySkill which, int val, bool abs=false) override {} From ad3c870fb6672e8729b80de2c7b2e80200b9b3c3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 15:21:23 +0200 Subject: [PATCH 1072/1248] Remove serialization of VLC, remove unused code --- cmake_modules/VCMI_lib.cmake | 2 - lib/IGameCallback.cpp | 7 --- lib/VCMI_Lib.h | 45 -------------- lib/modding/IdentifierStorage.h | 12 ---- lib/registerTypes/RegisterTypes.h | 51 ---------------- lib/serializer/CLoadIntegrityValidator.cpp | 68 ---------------------- lib/serializer/CLoadIntegrityValidator.h | 33 ----------- 7 files changed, 218 deletions(-) delete mode 100644 lib/serializer/CLoadIntegrityValidator.cpp delete mode 100644 lib/serializer/CLoadIntegrityValidator.h diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index be60f369b..0b969d102 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -180,7 +180,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.cpp ${MAIN_LIB_DIR}/serializer/BinarySerializer.cpp - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.cpp ${MAIN_LIB_DIR}/serializer/CMemorySerializer.cpp ${MAIN_LIB_DIR}/serializer/Connection.cpp ${MAIN_LIB_DIR}/serializer/CSerializer.cpp @@ -546,7 +545,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.h ${MAIN_LIB_DIR}/serializer/BinarySerializer.h - ${MAIN_LIB_DIR}/serializer/CLoadIntegrityValidator.h ${MAIN_LIB_DIR}/serializer/CMemorySerializer.h ${MAIN_LIB_DIR}/serializer/Connection.h ${MAIN_LIB_DIR}/serializer/CSerializer.h diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 25f70c0e2..55dcecdeb 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -24,7 +24,6 @@ #include "serializer/CSerializer.h" // for SAVEGAME_MAGIC #include "serializer/BinaryDeserializer.h" #include "serializer/BinarySerializer.h" -#include "serializer/CLoadIntegrityValidator.h" #include "rmg/CMapGenOptions.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" @@ -191,9 +190,6 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) logGlobal->info("\tReading options"); in.serializer & si; - logGlobal->info("\tReading handlers"); - in.serializer & *VLC; - logGlobal->info("\tReading gamestate"); in.serializer & gs; } @@ -207,14 +203,11 @@ void CPrivilegedInfoCallback::saveCommonState(Saver & out) const out.serializer & static_cast(*gs->map); logGlobal->info("\tSaving options"); out.serializer & gs->scenarioOps; - logGlobal->info("\tSaving handlers"); - out.serializer & *VLC; logGlobal->info("\tSaving gamestate"); out.serializer & gs; } // hardly memory usage for `-gdwarf-4` flag -template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadIntegrityValidator &); template DLL_LINKAGE void CPrivilegedInfoCallback::loadCommonState(CLoadFile &); template DLL_LINKAGE void CPrivilegedInfoCallback::saveCommonState(CSaveFile &) const; diff --git a/lib/VCMI_Lib.h b/lib/VCMI_Lib.h index c6cd971f9..dc9912e3b 100644 --- a/lib/VCMI_Lib.h +++ b/lib/VCMI_Lib.h @@ -120,51 +120,6 @@ public: #if SCRIPTING_ENABLED void scriptsLoaded(); #endif - - template void serialize(Handler &h, const int version) - { - h & identifiersHandler; // must be first - identifiers registry is used for handlers loading -#if SCRIPTING_ENABLED - h & scriptHandler;//must be first (or second after identifiers), it can modify factories other handlers depends on - if(!h.saving) - { - scriptsLoaded(); - } -#endif - - h & settingsHandler; - h & heroh; - h & arth; - h & creh; - h & townh; - h & objh; - h & objtypeh; - h & spellh; - h & skillh; - h & battlefieldsHandler; - h & obstacleHandler; - h & roadTypeHandler; - h & riverTypeHandler; - h & terrainTypeHandler; - - if(!h.saving) - { - //modh will be changed and modh->content will be empty after deserialization - auto content = getContent(); - h & modh; - setContent(content); - } - else - h & modh; - - h & IS_AI_ENABLED; - h & bth; - - if(!h.saving) - { - callWhenDeserializing(); - } - } }; extern DLL_LINKAGE LibClasses * VLC; diff --git a/lib/modding/IdentifierStorage.h b/lib/modding/IdentifierStorage.h index 004d724c7..31857c447 100644 --- a/lib/modding/IdentifierStorage.h +++ b/lib/modding/IdentifierStorage.h @@ -52,12 +52,6 @@ class DLL_LINKAGE CIdentifierStorage { return id == other.id && scope == other.scope; } - - template void serialize(Handler &h, const int version) - { - h & id; - h & scope; - } }; std::multimap registeredObjects; @@ -102,12 +96,6 @@ public: /// called at the very end of loading to check for any missing ID's void finalize(); - - template void serialize(Handler &h, const int version) - { - h & registeredObjects; - h & state; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 907361a40..847c4b1eb 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -95,57 +95,6 @@ void registerTypesMapObjects1(Serializer &s) template void registerTypesMapObjectTypes(Serializer &s) { - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -#define REGISTER_GENERIC_HANDLER(TYPENAME) s.template registerType >() - - REGISTER_GENERIC_HANDLER(CGObjectInstance); - REGISTER_GENERIC_HANDLER(CGArtifact); - REGISTER_GENERIC_HANDLER(CGBlackMarket); - REGISTER_GENERIC_HANDLER(CGBoat); - REGISTER_GENERIC_HANDLER(CGBorderGate); - REGISTER_GENERIC_HANDLER(CGBorderGuard); - REGISTER_GENERIC_HANDLER(CGCreature); - REGISTER_GENERIC_HANDLER(CGDenOfthieves); - REGISTER_GENERIC_HANDLER(CGDwelling); - REGISTER_GENERIC_HANDLER(CGEvent); - REGISTER_GENERIC_HANDLER(CGGarrison); - REGISTER_GENERIC_HANDLER(CGHeroPlaceholder); - REGISTER_GENERIC_HANDLER(CGHeroInstance); - REGISTER_GENERIC_HANDLER(CGKeymasterTent); - REGISTER_GENERIC_HANDLER(CGLighthouse); - REGISTER_GENERIC_HANDLER(CGTerrainPatch); - REGISTER_GENERIC_HANDLER(CGMagi); - REGISTER_GENERIC_HANDLER(CGMarket); - REGISTER_GENERIC_HANDLER(CGMine); - REGISTER_GENERIC_HANDLER(CGObelisk); - REGISTER_GENERIC_HANDLER(CGPandoraBox); - REGISTER_GENERIC_HANDLER(CGQuestGuard); - REGISTER_GENERIC_HANDLER(CGResource); - REGISTER_GENERIC_HANDLER(CGSeerHut); - REGISTER_GENERIC_HANDLER(CGShipyard); - REGISTER_GENERIC_HANDLER(CGSignBottle); - REGISTER_GENERIC_HANDLER(CGSirens); - REGISTER_GENERIC_HANDLER(CGMonolith); - REGISTER_GENERIC_HANDLER(CGSubterraneanGate); - REGISTER_GENERIC_HANDLER(CGWhirlpool); - REGISTER_GENERIC_HANDLER(CGTownInstance); - REGISTER_GENERIC_HANDLER(CGUniversity); - REGISTER_GENERIC_HANDLER(HillFort); - -#undef REGISTER_GENERIC_HANDLER - s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/lib/serializer/CLoadIntegrityValidator.cpp b/lib/serializer/CLoadIntegrityValidator.cpp deleted file mode 100644 index 81b3be830..000000000 --- a/lib/serializer/CLoadIntegrityValidator.cpp +++ /dev/null @@ -1,68 +0,0 @@ -/* - * CLoadIntegrityValidator.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 "CLoadIntegrityValidator.h" - -#include "../registerTypes/RegisterTypes.h" - -VCMI_LIB_NAMESPACE_BEGIN - -CLoadIntegrityValidator::CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion) - : serializer(this), foundDesync(false) -{ - registerTypes(serializer); - primaryFile = std::make_unique(primaryFileName, minimalVersion); - controlFile = std::make_unique(controlFileName, minimalVersion); - - assert(primaryFile->serializer.fileVersion == controlFile->serializer.fileVersion); - serializer.fileVersion = primaryFile->serializer.fileVersion; -} - -int CLoadIntegrityValidator::read( void * data, unsigned size ) -{ - assert(primaryFile); - assert(controlFile); - - if(!size) - return size; - - std::vector controlData(size); - auto ret = primaryFile->read(data, size); - - if(!foundDesync) - { - controlFile->read(controlData.data(), size); - if(std::memcmp(data, controlData.data(), size) != 0) - { - logGlobal->error("Desync found! Position: %d", primaryFile->sfile->tellg()); - foundDesync = true; - //throw std::runtime_error("Savegame dsynchronized!"); - } - } - return ret; -} - -std::unique_ptr CLoadIntegrityValidator::decay() -{ - primaryFile->serializer.loadedPointers = this->serializer.loadedPointers; - primaryFile->serializer.loadedPointersTypes = this->serializer.loadedPointersTypes; - return std::move(primaryFile); -} - -void CLoadIntegrityValidator::checkMagicBytes(const std::string & text) const -{ - assert(primaryFile); - assert(controlFile); - - primaryFile->checkMagicBytes(text); - controlFile->checkMagicBytes(text); -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadIntegrityValidator.h b/lib/serializer/CLoadIntegrityValidator.h deleted file mode 100644 index 5adb4f3d3..000000000 --- a/lib/serializer/CLoadIntegrityValidator.h +++ /dev/null @@ -1,33 +0,0 @@ -/* - * CLoadIntegrityValidator.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 "BinaryDeserializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -/// Simple byte-to-byte saves comparator -class DLL_LINKAGE CLoadIntegrityValidator - : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - std::unique_ptr primaryFile, controlFile; - bool foundDesync; - - CLoadIntegrityValidator(const boost::filesystem::path &primaryFileName, const boost::filesystem::path &controlFileName, int minimalVersion = SERIALIZATION_VERSION); //throws! - - int read( void * data, unsigned size) override; //throws! - void checkMagicBytes(const std::string & text) const; - - std::unique_ptr decay(); //returns primary file. CLoadIntegrityValidator stops being usable anymore -}; - -VCMI_LIB_NAMESPACE_END From f8a7f6e5a753112583799f0ebbfa6b5d8d441543 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 16:21:54 +0200 Subject: [PATCH 1073/1248] Remove remaining boost::mpl usages --- lib/JsonNode.h | 4 +- lib/bonuses/BonusSelector.h | 2 +- lib/serializer/BinaryDeserializer.h | 56 ++++------------------ lib/serializer/CSerializer.h | 73 ++++++++++++++++------------- 4 files changed, 53 insertions(+), 82 deletions(-) diff --git a/lib/JsonNode.h b/lib/JsonNode.h index 91eddce76..47ae9a1a0 100644 --- a/lib/JsonNode.h +++ b/lib/JsonNode.h @@ -247,8 +247,8 @@ namespace JsonDetail static Type convert(const JsonNode & node) { ///this should be triggered only for numeric types and enums - static_assert(boost::mpl::or_, std::is_enum, boost::is_class >::value, "Unsupported type for JsonNode::convertTo()!"); - return JsonConvImpl, boost::is_class >::value >::convertImpl(node); + static_assert(std::is_arithmetic_v || std::is_enum_v || std::is_class_v, "Unsupported type for JsonNode::convertTo()!"); + return JsonConvImpl || std::is_class_v >::convertImpl(node); } }; diff --git a/lib/bonuses/BonusSelector.h b/lib/bonuses/BonusSelector.h index 672fcfdc2..4e2ebbc74 100644 --- a/lib/bonuses/BonusSelector.h +++ b/lib/bonuses/BonusSelector.h @@ -22,7 +22,7 @@ public: template CSelector(const T &t, //SFINAE trick -> include this c-tor in overload resolution only if parameter is class //(includes functors, lambdas) or function. Without that VC is going mad about ambiguities. - typename std::enable_if < boost::mpl::or_ < std::is_class, std::is_function> ::value>::type *dummy = nullptr) + typename std::enable_if_t < std::is_class_v || std::is_function_v > *dummy = nullptr) : TBase(t) {} diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 2ed7d553d..caa36ee7d 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -9,9 +9,6 @@ */ #pragma once -#include -#include - #include "CTypeList.h" #include "../mapObjects/CGHeroInstance.h" #include "../../Global.h" @@ -37,41 +34,6 @@ public: /// Effectively revesed version of BinarySerializer class DLL_LINKAGE BinaryDeserializer : public CLoaderBase { - template - struct VariantLoaderHelper - { - Source & source; - std::vector> funcs; - - template - struct mpl_types_impl; - - template - struct mpl_types_impl> { - using type = boost::mpl::vector; - }; - - template - using mpl_types = typename mpl_types_impl::type; - - VariantLoaderHelper(Source & source): - source(source) - { - boost::mpl::for_each>(std::ref(*this)); - } - - template - void operator()(Type) - { - funcs.push_back([&]() -> Variant - { - Type obj; - source.load(obj); - return Variant(obj); - }); - } - }; - template struct LoadIfStackInstance { @@ -510,17 +472,19 @@ public: this->read((void*)data.c_str(),length); } - template - void load(std::variant & data) + template + void load(std::variant & data) { - using TVariant = std::variant; - - VariantLoaderHelper loader(*this); - si32 which; load( which ); - assert(which < loader.funcs.size()); - data = loader.funcs.at(which)(); + assert(which < sizeof...(TN)); + + // Create array of variants that contains all default-constructed alternatives + const std::variant table[] = { TN{ }... }; + // use appropriate alternative for result + data = table[which]; + // perform actual load via std::visit dispatch + std::visit([this](auto& o) { load(o); }, data); } template diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 706a0de9b..525caf2bc 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -149,42 +149,49 @@ struct is_serializeable template //metafunction returning CGObjectInstance if T is its derivate or T elsewise struct VectorizedTypeFor { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>::type; + using type = std::conditional_t, CGObjectInstance, T>; }; -template + +template <> +struct VectorizedTypeFor +{ + using type = CGHeroInstance; +}; + +template struct VectorizedIDType { - using type = typename - //if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else if - boost::mpl::eval_if, - boost::mpl::identity, - //else - boost::mpl::identity - >>>>>>::type; + using type = std::conditional_t, ObjectInstanceID, int32_t>; +}; + +template <> +struct VectorizedIDType +{ + using type = ArtifactID; +}; + +template <> +struct VectorizedIDType +{ + using type = CreatureID; +}; + +template <> +struct VectorizedIDType +{ + using type = HeroTypeID; +}; + +template <> +struct VectorizedIDType +{ + using type = ArtifactInstanceID; +}; + +template <> +struct VectorizedIDType +{ + using type = HeroTypeID; }; /// Base class for deserializers From c7676bde5353f84b199feaa007442ec28a89e32e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 18:22:27 +0200 Subject: [PATCH 1074/1248] Serialize Enitity classes by their ID --- include/vcmi/Entity.h | 2 ++ lib/constants/EntityIdentifiers.cpp | 22 ++++++++++++++++++++++ lib/constants/EntityIdentifiers.h | 20 +++++++++++++++----- lib/serializer/BinaryDeserializer.h | 20 +++++++++++++++++++- lib/serializer/BinarySerializer.h | 13 +++++++++++++ lib/serializer/CSerializer.h | 18 ------------------ 6 files changed, 71 insertions(+), 24 deletions(-) diff --git a/include/vcmi/Entity.h b/include/vcmi/Entity.h index 34ccc2757..eded843b7 100644 --- a/include/vcmi/Entity.h +++ b/include/vcmi/Entity.h @@ -50,6 +50,8 @@ template class DLL_LINKAGE EntityT : public Entity { public: + using IdentifierType = IdType; + virtual IdType getId() const = 0; }; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 6ad6f37a1..2d908b6b7 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -37,6 +37,8 @@ #include "constants/StringConstants.h" #include "CGeneralTextHandler.h" #include "TerrainHandler.h" //TODO: remove +#include "RiverHandler.h" +#include "RoadHandler.h" #include "BattleFieldHandler.h" #include "ObstacleHandler.h" #include "CTownHandler.h" @@ -404,6 +406,11 @@ std::string FactionID::entityType() return "faction"; } +const Faction * FactionID::toEntity(const Services * service) const +{ + return service->factions()->getByIndex(num); +} + si32 TerrainId::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); @@ -423,6 +430,21 @@ std::string TerrainId::entityType() return "terrain"; } +const TerrainType * TerrainId::toEntity(const Services * service) const +{ + return VLC->terrainTypeHandler->getByIndex(num); +} + +const RoadType * RoadId::toEntity(const Services * service) const +{ + return VLC->roadTypeHandler->getByIndex(num); +} + +const RiverType * RiverId::toEntity(const Services * service) const +{ + return VLC->riverTypeHandler->getByIndex(num); +} + const BattleField BattleField::NONE; const BattleFieldInfo * BattleField::getInfo() const diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index edf4a36cb..9c842c1f4 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -22,6 +22,10 @@ class CreatureService; class HeroType; class CHero; class HeroTypeService; +class Faction; +class RoadType; +class RiverType; +class TerrainType; namespace spells { @@ -181,20 +185,20 @@ public: static std::string entityType(); }; -class HeroTypeID : public Identifier +class DLL_LINKAGE HeroTypeID : public Identifier { public: using Identifier::Identifier; ///json serialization helpers - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); const CHero * toHeroType() const; const HeroType * toEntity(const Services * services) const; - DLL_LINKAGE static const HeroTypeID NONE; - DLL_LINKAGE static const HeroTypeID RANDOM; + static const HeroTypeID NONE; + static const HeroTypeID RANDOM; bool isValid() const { @@ -349,6 +353,7 @@ public: static si32 decode(const std::string& identifier); static std::string encode(const si32 index); + const Faction * toEntity(const Services * service) const; static std::string entityType(); bool isValid() const @@ -596,6 +601,8 @@ public: static const RoadId DIRT_ROAD; static const RoadId GRAVEL_ROAD; static const RoadId COBBLESTONE_ROAD; + + const RoadType * toEntity(const Services * service) const; }; class DLL_LINKAGE RiverId : public Identifier @@ -608,6 +615,8 @@ public: static const RiverId ICY_RIVER; static const RiverId MUD_RIVER; static const RiverId LAVA_RIVER; + + const RiverType * toEntity(const Services * service) const; }; class DLL_LINKAGE EPathfindingLayerBase : public IdentifierBase @@ -912,6 +921,7 @@ public: DLL_LINKAGE static si32 decode(const std::string & identifier); DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); + const TerrainType * toEntity(const Services * service) const; }; class ObstacleInfo; diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index caa36ee7d..124f89370 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -258,6 +258,25 @@ public: return; } + loadPointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > + void loadPointerImpl(T &data) + { + using DataType = std::remove_pointer_t; + + typename DataType::IdentifierType index; + load(index); + + auto * constEntity = index.toEntity(VLC); + auto * constData = dynamic_cast(constEntity); + data = const_cast(constData); + } + + template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > + void loadPointerImpl(T &data) + { ui32 pid = 0xffffffff; //pointer id (or maybe rather pointee id) if(smartPointerSerialization) { @@ -273,7 +292,6 @@ public: return; } } - //get type id ui16 tid; load( tid ); diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 0afcab95e..05db4a4ca 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -203,6 +203,19 @@ public: return; } + savePointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { + auto index = data->getId(); + save(index); + } + + template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { if(smartPointerSerialization) { // We might have an object that has multiple inheritance and store it via the non-first base pointer. diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 525caf2bc..9adc58e72 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -164,24 +164,6 @@ struct VectorizedIDType using type = std::conditional_t, ObjectInstanceID, int32_t>; }; -template <> -struct VectorizedIDType -{ - using type = ArtifactID; -}; - -template <> -struct VectorizedIDType -{ - using type = CreatureID; -}; - -template <> -struct VectorizedIDType -{ - using type = HeroTypeID; -}; - template <> struct VectorizedIDType { From 54103813ddc56a95e57697761d2f8826e469fc0d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 18:54:15 +0200 Subject: [PATCH 1075/1248] Remove no longer used serialization methods --- lib/BattleFieldHandler.h | 18 ---- lib/CArtHandler.h | 38 -------- lib/CCreatureHandler.h | 77 --------------- lib/CHeroHandler.h | 61 ------------ lib/CSkillHandler.h | 24 ----- lib/CTownHandler.h | 96 ------------------- lib/ObstacleHandler.h | 22 ----- lib/RiverHandler.h | 21 ---- lib/RoadHandler.h | 14 --- lib/TerrainHandler.h | 29 ------ .../AObjectTypeHandler.h | 14 --- .../CBankInstanceConstructor.h | 9 -- .../CObjectClassesHandler.h | 15 --- .../CRewardableConstructor.h | 7 -- .../CommonConstructors.h | 34 ------- .../DwellingInstanceConstructor.h | 7 -- .../HillFortInstanceConstructor.h | 5 - lib/mapObjectConstructors/RandomMapInfo.h | 8 -- lib/mapObjectConstructors/SObjectSounds.h | 7 -- .../ShipyardInstanceConstructor.h | 5 - lib/mapObjects/CGTownInstance.h | 13 ++- lib/mapObjects/CObjectHandler.h | 5 - lib/spells/CSpellHandler.h | 42 -------- 23 files changed, 12 insertions(+), 559 deletions(-) diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index 9bacf79a2..d51aeac57 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -53,19 +53,6 @@ public: std::string getNameTranslated() const override; void registerIcons(const IconRegistar & cb) const override; BattleField getId() const override; - - template void serialize(Handler & h, const int version) - { - h & name; - h & identifier; - h & isSpecial; - h & graphics; - h & icon; - h & iconIndex; - h & battlefield; - h & impassableHexes; - - } }; class DLL_LINKAGE BattleFieldService : public EntityServiceT @@ -85,11 +72,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 898c5ec23..43b5eaa16 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -53,12 +53,6 @@ public: bool isCombined() const; const std::vector & getConstituents() const; const std::vector & getPartOf() const; - - template void serialize(Handler & h, const int version) - { - h & constituents; - h & partOf; - } }; class DLL_LINKAGE CScrollArtifact @@ -83,12 +77,6 @@ public: const std::vector > & getBonusesPerLevel() const; std::vector > & getThresholdBonuses(); const std::vector > & getThresholdBonuses() const; - - template void serialize(Handler & h, const int version) - { - h & bonusesPerLevel; - h & thresholdBonuses; - } }; // Container for artifacts. Not for instances. @@ -144,25 +132,6 @@ public: // Is used for testing purposes only void setImage(int32_t iconIndex, std::string image, std::string large); - template void serialize(Handler & h, const int version) - { - h & static_cast(*this); - h & static_cast(*this); - h & static_cast(*this); - h & image; - h & large; - h & advMapDef; - h & iconIndex; - h & price; - h & possibleSlots; - h & aClass; - h & id; - h & modScope; - h & identifier; - h & warMachine; - h & onlyOnWaterMap; - } - CArtifact(); ~CArtifact(); @@ -203,13 +172,6 @@ public: std::vector getDefaultAllowed() const override; - template void serialize(Handler &h, const int version) - { - h & objects; - h & allowedArtifacts; - h & allocatedArtifacts; - } - protected: const std::vector & getTypeNames() const override; CArtifact * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index ab8d57f2c..e7f6ccf6a 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -83,11 +83,6 @@ public: struct RayColor { ColorRGBA start; ColorRGBA end; - - template void serialize(Handler &h, const int version) - { - h & start & end; - } }; double timeBetweenFidgets, idleAnimationTime, @@ -100,25 +95,7 @@ public: AnimationPath projectileImageName; std::vector projectileRay; - //bool projectileSpin; //if true, appropriate projectile is spinning during flight - template void serialize(Handler &h, const int version) - { - h & timeBetweenFidgets; - h & idleAnimationTime; - h & walkAnimationTime; - h & attackAnimationTime; - h & upperRightMissleOffsetX; - h & rightMissleOffsetX; - h & lowerRightMissleOffsetX; - h & upperRightMissleOffsetY; - h & rightMissleOffsetY; - h & lowerRightMissleOffsetY; - h & missleFrameAngles; - h & attackClimaxFrame; - h & projectileImageName; - h & projectileRay; - } } animation; //sound info @@ -132,18 +109,6 @@ public: AudioPath wince; // attacked but did not die AudioPath startMoving; AudioPath endMoving; - - template void serialize(Handler &h, const int version) - { - h & attack; - h & defend; - h & killed; - h & move; - h & shoot; - h & wince; - h & startMoving; - h & endMoving; - } } sounds; ArtifactID warMachine; @@ -210,35 +175,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & cost; - h & upgrades; - h & fightValue; - h & AIValue; - h & growth; - h & hordeGrowth; - h & ammMin; - h & ammMax; - h & level; - h & animDefName; - h & iconIndex; - h & smallIconName; - h & largeIconName; - - h & idNumber; - h & faction; - h & sounds; - h & animation; - - h & doubleWide; - h & special; - h & identifier; - h & modScope; - h & warMachine; - } - CCreature(); private: @@ -303,19 +239,6 @@ public: std::vector loadLegacyData() override; std::vector getDefaultAllowed() const override; - - template void serialize(Handler &h, const int version) - { - //TODO: should be optimized, not all these informations needs to be serialized (same for ccreature) - h & doubledCreatures; - h & objects; - h & expRanks; - h & maxExpPerBattle; - h & expAfterUpgrade; - h & skillLevels; - h & skillRequirements; - h & commanderLevelPremy; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index dacb15928..83b2e878f 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -52,13 +52,6 @@ public: ui32 minAmount; ui32 maxAmount; CreatureID creature; - - template void serialize(Handler &h, const int version) - { - h & minAmount; - h & maxAmount; - h & creature; - } }; si32 imageIndex = 0; @@ -104,29 +97,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & ID; - h & imageIndex; - h & initialArmy; - h & heroClass; - h & secSkillsInit; - h & specialty; - h & spells; - h & haveSpellBook; - h & gender; - h & special; - h & onlyOnWaterMap; - h & onlyOnMapWithoutWater; - h & iconSpecSmall; - h & iconSpecLarge; - h & portraitSmall; - h & portraitLarge; - h & identifier; - h & modScope; - h & battleImage; - } }; class DLL_LINKAGE CHeroClass : public HeroClass @@ -183,25 +153,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) - { - h & modScope; - h & identifier; - h & faction; - h & id; - h & defaultTavernChance; - h & primarySkillInitial; - h & primarySkillLowLevel; - h & primarySkillHighLevel; - h & secSkillProbability; - h & selectionProbability; - h & affinity; - h & commander; - h & imageBattleMale; - h & imageBattleFemale; - h & imageMapMale; - h & imageMapFemale; - } EAlignment getAlignment() const; }; @@ -218,11 +169,6 @@ public: ~CHeroClassHandler(); - template void serialize(Handler &h, const int version) - { - h & objects; - } - protected: const std::vector & getTypeNames() const override; CHeroClass * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; @@ -262,13 +208,6 @@ public: std::vector getDefaultAllowed() const override; - template void serialize(Handler &h, const int version) - { - h & classes; - h & objects; - h & expPerLevel; - } - protected: const std::vector & getTypeNames() const override; CHero * loadFromJson(const std::string & scope, const JsonNode & node, const std::string & identifier, size_t index) override; diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index 83fe729bb..920689e9e 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -29,14 +29,6 @@ public: std::string iconMedium; std::string iconLarge; std::vector> effects; - - template void serialize(Handler & h, const int version) - { - h & iconSmall; - h & iconMedium; - h & iconLarge; - h & effects; - } }; private: @@ -82,17 +74,6 @@ public: bool onlyOnWaterMap; - template void serialize(Handler & h, const int version) - { - h & id; - h & identifier; - h & gainChance; - h & levels; - h & obligatoryMajor; - h & obligatoryMinor; - h & onlyOnWaterMap; - } - friend class CSkillHandler; friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill); friend DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info); @@ -111,11 +92,6 @@ public: std::vector getDefaultAllowed() const override; - template void serialize(Handler & h, const int version) - { - h & objects; - } - protected: const std::vector & getTypeNames() const override; CSkill * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 67c2a10c6..170cc2f1f 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -123,25 +123,6 @@ public: void addNewBonus(const std::shared_ptr & b, BonusList & bonusList) const; - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & town; - h & bid; - h & resources; - h & produce; - h & requirements; - h & upgrade; - h & mode; - h & subId; - h & height; - h & overrideBids; - h & buildingBonuses; - h & onVisitBonuses; - h & rewardableObjectInfo; - } - friend class CTownHandler; }; @@ -160,17 +141,6 @@ struct DLL_LINKAGE CStructure std::string identifier; bool hiddenUpgrade; // used only if "building" is upgrade, if true - structure on town screen will behave exactly like parent (mouse clicks, hover texts, etc) - template void serialize(Handler &h, const int version) - { - h & pos; - h & defName; - h & borderName; - h & areaName; - h & identifier; - h & building; - h & buildable; - h & hiddenUpgrade; - } }; struct DLL_LINKAGE SPuzzleInfo @@ -179,15 +149,6 @@ struct DLL_LINKAGE SPuzzleInfo si16 x, y; //position ui16 whenUncovered; //determines the sequnce of discovering (the lesser it is the sooner puzzle will be discovered) ImagePath filename; //file with graphic of this puzzle - - template void serialize(Handler &h, const int version) - { - h & number; - h & x; - h & y; - h & whenUncovered; - h & filename; - } }; class DLL_LINKAGE CFaction : public Faction @@ -238,20 +199,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - - template void serialize(Handler &h, const int version) - { - h & modScope; - h & identifier; - h & index; - h & nativeTerrain; - h & boatType; - h & alignment; - h & town; - h & creatureBg120; - h & creatureBg130; - h & puzzleMap; - } }; class DLL_LINKAGE CTown @@ -323,44 +270,7 @@ public: std::string towerIconSmall; std::string towerIconLarge; - template void serialize(Handler &h, const int version) - { - h & icons; - h & iconSmall; - h & iconLarge; - h & tavernVideo; - h & musicTheme; - h & townBackground; - h & guildBackground; - h & guildWindow; - h & buildingsIcons; - h & hallBackground; - h & hallSlots; - h & structures; - h & siegePrefix; - h & siegePositions; - h & siegeShooter; - h & towerIconSmall; - h & towerIconLarge; - } } clientInfo; - - template void serialize(Handler &h, const int version) - { - h & namesCount; - h & faction; - h & creatures; - h & dwellings; - h & dwellingNames; - h & buildings; - h & hordeLvl; - h & mageLevel; - h & primaryRes; - h & warMachine; - h & clientInfo; - h & moatAbility; - h & defaultTavernChance; - } private: ///generated bonusing buildings messages for all towns of this type. @@ -445,12 +355,6 @@ public: static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); - template void serialize(Handler &h, const int version) - { - h & objects; - h & randomTown; - } - protected: const std::vector & getTypeNames() const override; CFaction * loadFromJson(const std::string & scope, const JsonNode & data, const std::string & identifier, size_t index) override; diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 958e46e62..36268426a 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -54,23 +54,6 @@ public: std::vector getBlocked(BattleHex hex) const; //returns vector of hexes blocked by obstacle when it's placed on hex 'hex' bool isAppropriate(const TerrainId terrainType, const BattleField & specialBattlefield) const; - - template void serialize(Handler &h, const int version) - { - h & obstacle; - h & iconIndex; - h & identifier; - h & animation; - h & appearAnimation; - h & appearSound; - h & allowedTerrains; - h & allowedSpecialBfields; - h & isAbsoluteObstacle; - h & isForegroundObstacle; - h & width; - h & height; - h & blockedTiles; - } }; class DLL_LINKAGE ObstacleService : public EntityServiceT @@ -89,11 +72,6 @@ public: const std::vector & getTypeNames() const override; std::vector loadLegacyData() override; std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index dddd81d41..5c028ddbd 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -24,12 +24,6 @@ struct DLL_LINKAGE RiverPaletteAnimation int32_t start; /// total numbers of colors to cycle int32_t length; - - template void serialize(Handler& h, const int version) - { - h & start; - h & length; - } }; class DLL_LINKAGE RiverType : public EntityT @@ -57,16 +51,6 @@ public: std::vector paletteAnimation; RiverType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & deltaName; - h & id; - h & paletteAnimation; - } }; class DLL_LINKAGE RiverTypeService : public EntityServiceT @@ -88,11 +72,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index e9e1d9df6..dbb9476b5 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -41,15 +41,6 @@ public: ui8 movementCost; RoadType(); - - template void serialize(Handler& h, const int version) - { - h & tilesFilename; - h & identifier; - h & modScope; - h & id; - h & movementCost; - } }; class DLL_LINKAGE RoadTypeService : public EntityServiceT @@ -71,11 +62,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index b94e4427d..447e1efc3 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -91,30 +91,6 @@ public: bool isSurface() const; bool isUnderground() const; bool isTransitionRequired() const; - - template void serialize(Handler &h, const int version) - { - h & battleFields; - h & prohibitTransitions; - h & minimapBlocked; - h & minimapUnblocked; - h & modScope; - h & identifier; - h & musicFilename; - h & tilesFilename; - h & shortIdentifier; - h & terrainViewPatterns; - h & rockTerrain; - h & river; - h & paletteAnimation; - - h & id; - h & moveCost; - h & horseSound; - h & horseSoundPenalty; - h & passabilityType; - h & transitionRequired; - } }; class DLL_LINKAGE TerrainTypeService : public EntityServiceT @@ -134,11 +110,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; virtual std::vector getDefaultAllowed() const override; - - template void serialize(Handler & h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index ce35a9f82..2f5028e57 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -112,20 +112,6 @@ public: /// Returns object configuration, if available. Otherwise returns NULL virtual std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const; - - template void serialize(Handler &h, const int version) - { - h & type; - h & subtype; - h & templates; - h & rmgInfo; - h & modScope; - h & typeName; - h & subTypeName; - h & sounds; - h & aiValue; - h & battlefield; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CBankInstanceConstructor.h b/lib/mapObjectConstructors/CBankInstanceConstructor.h index f926a65f2..e683f01df 100644 --- a/lib/mapObjectConstructors/CBankInstanceConstructor.h +++ b/lib/mapObjectConstructors/CBankInstanceConstructor.h @@ -92,15 +92,6 @@ public: bool hasNameTextID() const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - h & levels; - h & bankResetDuration; - h & blockVisit; - h & coastVisitable; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 000b50703..ef42a32a1 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -62,16 +62,6 @@ public: std::string getJsonKey() const; std::string getNameTextID() const; std::string getNameTranslated() const; - - template void serialize(Handler &h, const int version) - { - h & id; - h & handlerName; - h & base; - h & objects; - h & identifier; - h & modScope; - } }; /// Main class responsible for creation of all adventure map objects @@ -130,11 +120,6 @@ public: std::string getObjectHandlerName(MapObjectID type) const; std::string getJsonKey(MapObjectID type) const; - - template void serialize(Handler &h, const int version) - { - h & objects; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CRewardableConstructor.h b/lib/mapObjectConstructors/CRewardableConstructor.h index 0461148fa..a92836f7f 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.h +++ b/lib/mapObjectConstructors/CRewardableConstructor.h @@ -30,13 +30,6 @@ public: void configureObject(CGObjectInstance * object, CRandomGenerator & rng) const override; std::unique_ptr getObjectInfo(std::shared_ptr tmpl) const override; - - template void serialize(Handler &h, const int version) - { - AObjectTypeHandler::serialize(h, version); - - h & objectInfo; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/CommonConstructors.h b/lib/mapObjectConstructors/CommonConstructors.h index 5c50ded30..76215b97d 100644 --- a/lib/mapObjectConstructors/CommonConstructors.h +++ b/lib/mapObjectConstructors/CommonConstructors.h @@ -66,14 +66,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & faction; - h & filters; - h & static_cast&>(*this); - } }; class CHeroInstanceConstructor : public CDefaultObjectTypeHandler @@ -93,14 +85,6 @@ public: bool hasNameTextID() const override; std::string getNameTextID() const override; - - template void serialize(Handler &h, const int version) - { - h & filtersJson; - h & heroClass; - h & filters; - h & static_cast&>(*this); - } }; class DLL_LINKAGE BoatInstanceConstructor : public CDefaultObjectTypeHandler @@ -122,18 +106,6 @@ public: /// Returns boat preview animation, for use in Shipyards AnimationPath getBoatAnimationName() const; - - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & layer; - h & onboardAssaultAllowed; - h & onboardVisitAllowed; - h & bonuses; - h & actualAnimation; - h & overlayAnimation; - h & flagAnimations; - } }; class MarketInstanceConstructor : public CDefaultObjectTypeHandler @@ -152,12 +124,6 @@ public: void initializeObject(CGMarket * object) const override; void randomizeObject(CGMarket * object, CRandomGenerator & rng) const override; - template void serialize(Handler &h, const int version) - { - h & static_cast&>(*this); - h & marketModes; - h & marketEfficiency; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/DwellingInstanceConstructor.h b/lib/mapObjectConstructors/DwellingInstanceConstructor.h index 4022be309..ee06219be 100644 --- a/lib/mapObjectConstructors/DwellingInstanceConstructor.h +++ b/lib/mapObjectConstructors/DwellingInstanceConstructor.h @@ -33,13 +33,6 @@ public: bool producesCreature(const CCreature * crea) const; std::vector getProducedCreatures() const; - - template void serialize(Handler &h, const int version) - { - h & availableCreatures; - h & guards; - h & static_cast&>(*this); - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/HillFortInstanceConstructor.h b/lib/mapObjectConstructors/HillFortInstanceConstructor.h index 526e66571..ace75dd1d 100644 --- a/lib/mapObjectConstructors/HillFortInstanceConstructor.h +++ b/lib/mapObjectConstructors/HillFortInstanceConstructor.h @@ -24,11 +24,6 @@ protected: void initializeObject(HillFort * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/RandomMapInfo.h b/lib/mapObjectConstructors/RandomMapInfo.h index 5bb98daf6..fe5dc2bd3 100644 --- a/lib/mapObjectConstructors/RandomMapInfo.h +++ b/lib/mapObjectConstructors/RandomMapInfo.h @@ -33,14 +33,6 @@ struct DLL_LINKAGE RandomMapInfo {} void setMapLimit(ui32 val) { mapLimit = val; } - - template void serialize(Handler &h, const int version) - { - h & value; - h & mapLimit; - h & zoneLimit; - h & rarity; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/SObjectSounds.h b/lib/mapObjectConstructors/SObjectSounds.h index 8d41817c0..5f94cd44a 100644 --- a/lib/mapObjectConstructors/SObjectSounds.h +++ b/lib/mapObjectConstructors/SObjectSounds.h @@ -18,13 +18,6 @@ struct SObjectSounds std::vector ambient; std::vector visit; std::vector removal; - - template void serialize(Handler &h, const int version) - { - h & ambient; - h & visit; - h & removal; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h index c0d47be4f..845bf77b0 100644 --- a/lib/mapObjectConstructors/ShipyardInstanceConstructor.h +++ b/lib/mapObjectConstructors/ShipyardInstanceConstructor.h @@ -24,11 +24,6 @@ protected: void initializeObject(CGShipyard * object) const override; public: - template void serialize(Handler &h, const int version) - { - h & static_cast(*this); - h & parameters; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 37b6f7dfc..258adab42 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -92,7 +92,18 @@ public: for(auto * bonusingBuilding : bonusingBuildings) bonusingBuilding->town = this; - h & town; + if (h.saving) + { + CFaction * faction = town ? town->faction : nullptr; + h & faction; + } + else + { + CFaction * faction; + h & faction; + town = faction ? faction->town : nullptr; + } + h & townAndVis; BONUS_TREE_DESERIALIZATION_FIX diff --git a/lib/mapObjects/CObjectHandler.h b/lib/mapObjects/CObjectHandler.h index 9cba71ab1..7413918b2 100644 --- a/lib/mapObjects/CObjectHandler.h +++ b/lib/mapObjects/CObjectHandler.h @@ -22,11 +22,6 @@ public: std::vector resVals; //default values of resources in gold CObjectHandler(); - - template void serialize(Handler &h, const int version) - { - h & resVals; - } }; VCMI_LIB_NAMESPACE_END diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index ccf575518..e9de9440a 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -267,39 +267,6 @@ public: void updateFrom(const JsonNode & data); void serializeJson(JsonSerializeFormat & handler); - template void serialize(Handler & h, const int version) - { - h & identifier; - if (version > 820) - h & modScope; - h & id; - h & level; - h & power; - h & probabilities; - h & attributes; - h & combat; - h & creatureAbility; - h & positiveness; - h & counteredSpells; - h & rising; - h & damage; - h & offensive; - h & targetType; - h & targetCondition; - h & iconImmune; - h & defaultProbability; - h & special; - h & castSound; - h & iconBook; - h & iconEffect; - h & iconScenarioBonus; - h & iconScroll; - h & levels; - h & school; - h & animationInfo; - h & nonMagical; - h & onlyOnWaterMap; - } friend class CSpellHandler; friend class Graphics; friend class test::CSpellTest; @@ -384,15 +351,6 @@ public: */ std::vector getDefaultAllowed() const override; - template void serialize(Handler & h, const int version) - { - h & objects; - if(!h.saving) - { - afterLoadFinalization(); - } - } - protected: const std::vector & getTypeNames() const override; CSpell * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & identifier, size_t index) override; From 2cc8b5baeb089b5942cea891903358ec959700e6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 19:25:50 +0200 Subject: [PATCH 1076/1248] Fix map startup --- lib/gameState/CGameStateCampaign.cpp | 2 +- lib/mapObjects/CGTownInstance.h | 2 +- lib/registerTypes/RegisterTypes.h | 13 +-------- lib/serializer/BinaryDeserializer.h | 40 ++++++++++++++-------------- lib/serializer/BinarySerializer.h | 26 +++++++++--------- 5 files changed, 36 insertions(+), 47 deletions(-) diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 076403a31..f67ed7311 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -16,7 +16,7 @@ #include "../campaign/CampaignState.h" #include "../mapping/CMapEditManager.h" #include "../mapObjects/CGHeroInstance.h" -#include "../registerTypes/RegisterTypes.h" +#include "../networkPacks/ArtifactLocation.h" #include "../mapObjectConstructors/AObjectTypeHandler.h" #include "../mapObjectConstructors/CObjectClassesHandler.h" #include "../StartInfo.h" diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 258adab42..2239e728a 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -99,7 +99,7 @@ public: } else { - CFaction * faction; + CFaction * faction = nullptr; h & faction; town = faction ? faction->town : nullptr; } diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 847c4b1eb..db6866b78 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -14,27 +14,16 @@ #include "../networkPacks/PacksForServer.h" #include "../networkPacks/PacksForLobby.h" #include "../networkPacks/SetStackEffect.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CCreatureSet.h" -#include "../CPlayerState.h" -#include "../CHeroHandler.h" -#include "../CTownHandler.h" -#include "../mapObjectConstructors/CRewardableConstructor.h" -#include "../mapObjectConstructors/CommonConstructors.h" #include "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjectConstructors/DwellingInstanceConstructor.h" -#include "../mapObjectConstructors/HillFortInstanceConstructor.h" -#include "../mapObjectConstructors/ShipyardInstanceConstructor.h" #include "../mapObjects/MapObjects.h" #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGTownBuilding.h" #include "../mapObjects/ObjectTemplate.h" #include "../battle/CObstacleInstance.h" -#include "../bonuses/CBonusSystemNode.h" #include "../bonuses/Limiters.h" #include "../bonuses/Updaters.h" #include "../bonuses/Propagators.h" +#include "../CPlayerState.h" #include "../CStack.h" VCMI_LIB_NAMESPACE_BEGIN diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 124f89370..cc3d06b08 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -234,6 +234,25 @@ public: return; } + loadPointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > + void loadPointerImpl(T &data) + { + using DataType = std::remove_pointer_t; + + typename DataType::IdentifierType index; + load(index); + + auto * constEntity = index.toEntity(VLC); + auto * constData = dynamic_cast(constEntity); + data = const_cast(constData); + } + + template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > + void loadPointerImpl(T &data) + { if(reader->smartVectorMembersSerialization) { typedef typename std::remove_const::type>::type TObjectType; //eg: const CGHeroInstance * => CGHeroInstance @@ -258,25 +277,6 @@ public: return; } - loadPointerImpl(data); - } - - template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > - void loadPointerImpl(T &data) - { - using DataType = std::remove_pointer_t; - - typename DataType::IdentifierType index; - load(index); - - auto * constEntity = index.toEntity(VLC); - auto * constData = dynamic_cast(constEntity); - data = const_cast(constData); - } - - template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > - void loadPointerImpl(T &data) - { ui32 pid = 0xffffffff; //pointer id (or maybe rather pointee id) if(smartPointerSerialization) { @@ -502,7 +502,7 @@ public: // use appropriate alternative for result data = table[which]; // perform actual load via std::visit dispatch - std::visit([this](auto& o) { load(o); }, data); + std::visit([&](auto& o) { load(o); }, data); } template diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 05db4a4ca..f9ecba9e0 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -181,6 +181,19 @@ public: if(!hlp) return; + savePointerImpl(data); + } + + template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { + auto index = data->getId(); + save(index); + } + + template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > + void savePointerImpl(const T &data) + { if(writer->smartVectorMembersSerialization) { typedef typename std::remove_const::type>::type TObjectType; @@ -203,19 +216,6 @@ public: return; } - savePointerImpl(data); - } - - template < typename T, typename std::enable_if < std::is_base_of_v>, int >::type = 0 > - void savePointerImpl(const T &data) - { - auto index = data->getId(); - save(index); - } - - template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > - void savePointerImpl(const T &data) - { if(smartPointerSerialization) { // We might have an object that has multiple inheritance and store it via the non-first base pointer. From 34c012d119e34677a74fa22ac72fec1b7b508793 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 4 Nov 2023 21:49:47 +0200 Subject: [PATCH 1077/1248] Serialization version bump --- lib/serializer/CSerializer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 9adc58e72..85673f215 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 828; -const ui32 MINIMAL_SERIALIZATION_VERSION = 828; +const ui32 SERIALIZATION_VERSION = 829; +const ui32 MINIMAL_SERIALIZATION_VERSION = 829; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From 0691dfef3b77fc30522514448b5324e8dad363c6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 14:27:25 +0200 Subject: [PATCH 1078/1248] Moved stateful artifact randomization logic to gamestate from handler --- lib/CArtHandler.cpp | 70 ------------------------------- lib/CArtHandler.h | 9 ---- lib/IGameCallback.cpp | 8 ++-- lib/IGameCallback.h | 2 +- lib/JsonRandom.cpp | 3 +- lib/gameState/CGameState.cpp | 71 +++++++++++++++++++++++++++++++- lib/gameState/CGameState.h | 10 +++++ lib/mapObjects/MiscObjects.cpp | 10 ++--- lib/networkPacks/NetPacksLib.cpp | 2 +- server/CGameHandler.cpp | 2 +- 10 files changed, 94 insertions(+), 93 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index a912854b6..7b31c813e 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -607,75 +607,6 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node) } } -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) -{ - std::set potentialPicks; - - // Select artifacts that satisfy provided criterias - for (auto const * artifact : allowedArtifacts) - { - assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized - - if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) - continue; - - if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) - continue; - - if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) - continue; - - if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) - continue; - - if (!accepts(artifact->id)) - continue; - - potentialPicks.insert(artifact->id); - } - - return pickRandomArtifact(rand, potentialPicks); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) -{ - // No allowed artifacts at all - give Grail - this can't be banned (hopefully) - // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior - if (potentialPicks.empty()) - { - logGlobal->warn("Failed to find artifact that matches requested parameters!"); - return ArtifactID::GRAIL; - } - - // Find how many times least used artifacts were picked by randomizer - int leastUsedTimes = std::numeric_limits::max(); - for (auto const & artifact : potentialPicks) - if (allocatedArtifacts[artifact] < leastUsedTimes) - leastUsedTimes = allocatedArtifacts[artifact]; - - // Pick all artifacts that were used least number of times - std::set preferredPicks; - for (auto const & artifact : potentialPicks) - if (allocatedArtifacts[artifact] == leastUsedTimes) - preferredPicks.insert(artifact); - - assert(!preferredPicks.empty()); - - ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); - allocatedArtifacts[artID] += 1; // record +1 more usage - return artID; -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) -{ - return pickRandomArtifact(rand, 0xff, std::move(accepts)); -} - -ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags) -{ - return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); -} - void CArtHandler::makeItCreatureArt(CArtifact * a, bool onlyCreature) { if (onlyCreature) @@ -723,7 +654,6 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) { allowedArtifacts.clear(); - allocatedArtifacts.clear(); for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) { diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index 43b5eaa16..d8417d558 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -141,9 +141,6 @@ public: class DLL_LINKAGE CArtHandler : public CHandlerBase { public: - /// Stores number of times each artifact was placed on map via randomization - std::map allocatedArtifacts; - /// List of artifacts allowed on the map std::vector allowedArtifacts; @@ -151,12 +148,6 @@ public: static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor - /// Gets a artifact ID randomly and removes the selected artifact from this handler. - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); - ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); - bool legalArtifact(const ArtifactID & id); void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 55dcecdeb..4f625228a 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -144,14 +144,14 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set & tiles, std: } } -void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const +void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) { for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact()); for (int j = 0; j < 3 ; j++) - out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact()); - out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact()); + out.push_back(gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact()); } void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std::optional level) diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index ef9e95221..98d520701 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -61,7 +61,7 @@ public: void getAllTiles(std::unordered_set &tiles, std::optional player, int level, std::function filter) const; //gives 3 treasures, 3 minors, 1 major -> used by Black Market and Artifact Merchant - void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand) const; + void pickAllowedArtsSet(std::vector & out, CRandomGenerator & rand); void getAllowedSpells(std::vector &out, std::optional level = std::nullopt); template diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index e0c4fcc33..f960f0137 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -24,6 +24,7 @@ #include "CSkillHandler.h" #include "CHeroHandler.h" #include "IGameCallback.h" +#include "gameState/CGameState.h" #include "mapObjects/IObjectInterface.h" #include "modding/IdentifierStorage.h" #include "modding/ModScope.h" @@ -388,7 +389,7 @@ namespace JsonRandom std::set potentialPicks = filterKeys(value, allowedArts, variables); - return VLC->arth->pickRandomArtifact(rng, potentialPicks); + return IObjectInterface::cb->gameState()->pickRandomArtifact(rng, potentialPicks); } std::vector loadArtifacts(const JsonNode & value, CRandomGenerator & rng, const Variables & variables) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index dac105832..a365f1295 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -760,7 +760,7 @@ void CGameState::initStartingBonus() logGlobal->error("Cannot give starting artifact - no heroes!"); break; } - const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); + const Artifact * toGive = pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC); CGHeroInstance *hero = elem.second.heroes[0]; if(!giveHeroArtifact(hero, toGive->getId())) @@ -1971,4 +1971,73 @@ CRandomGenerator & CGameState::getRandomGenerator() return rand; } +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts) +{ + std::set potentialPicks; + + // Select artifacts that satisfy provided criterias + for (auto const * artifact : VLC->arth->allowedArtifacts) + { + assert(artifact->aClass != CArtifact::ART_SPECIAL); // should be filtered out when allowedArtifacts is initialized + + if ((flags & CArtifact::ART_TREASURE) == 0 && artifact->aClass == CArtifact::ART_TREASURE) + continue; + + if ((flags & CArtifact::ART_MINOR) == 0 && artifact->aClass == CArtifact::ART_MINOR) + continue; + + if ((flags & CArtifact::ART_MAJOR) == 0 && artifact->aClass == CArtifact::ART_MAJOR) + continue; + + if ((flags & CArtifact::ART_RELIC) == 0 && artifact->aClass == CArtifact::ART_RELIC) + continue; + + if (!accepts(artifact->getId())) + continue; + + potentialPicks.insert(artifact->getId()); + } + + return pickRandomArtifact(rand, potentialPicks); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::set potentialPicks) +{ + // No allowed artifacts at all - give Grail - this can't be banned (hopefully) + // FIXME: investigate how such cases are handled by H3 - some heavily customized user-made maps likely rely on H3 behavior + if (potentialPicks.empty()) + { + logGlobal->warn("Failed to find artifact that matches requested parameters!"); + return ArtifactID::GRAIL; + } + + // Find how many times least used artifacts were picked by randomizer + int leastUsedTimes = std::numeric_limits::max(); + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] < leastUsedTimes) + leastUsedTimes = allocatedArtifacts[artifact]; + + // Pick all artifacts that were used least number of times + std::set preferredPicks; + for (auto const & artifact : potentialPicks) + if (allocatedArtifacts[artifact] == leastUsedTimes) + preferredPicks.insert(artifact); + + assert(!preferredPicks.empty()); + + ArtifactID artID = *RandomGeneratorUtil::nextItem(preferredPicks, rand); + allocatedArtifacts[artID] += 1; // record +1 more usage + return artID; +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, std::function accepts) +{ + return pickRandomArtifact(rand, 0xff, std::move(accepts)); +} + +ArtifactID CGameState::pickRandomArtifact(CRandomGenerator & rand, int flags) +{ + return pickRandomArtifact(rand, flags, [](const ArtifactID &) { return true; }); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/gameState/CGameState.h b/lib/gameState/CGameState.h index 7b613c05d..b2b03a56e 100644 --- a/lib/gameState/CGameState.h +++ b/lib/gameState/CGameState.h @@ -83,6 +83,9 @@ class DLL_LINKAGE CGameState : public CNonConstInfoCallback friend class CGameStateCampaign; public: + /// Stores number of times each artifact was placed on map via randomization + std::map allocatedArtifacts; + /// List of currently ongoing battles std::vector> currentBattles; /// ID that can be allocated to next battle @@ -130,6 +133,12 @@ public: std::vector guardingCreatures (int3 pos) const; void updateRumor(); + /// Gets a artifact ID randomly and removes the selected artifact from this handler. + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, int flags, std::function accepts); + ArtifactID pickRandomArtifact(CRandomGenerator & rand, std::set filtered); + /// Returns battle in which selected player is engaged, or nullptr if none. /// Can NOT be used with neutral player, use battle by ID instead const BattleInfo * getBattle(const PlayerColor & player) const; @@ -176,6 +185,7 @@ public: h & rand; h & rumor; h & campaign; + h & allocatedArtifacts; BONUS_TREE_DESERIALIZATION_FIX } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c82484c54..6583478aa 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -724,19 +724,19 @@ void CGArtifact::pickRandomObject(CRandomGenerator & rand) switch(ID.toEnum()) { case MapObjectID::RANDOM_ART: - subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC); break; case MapObjectID::RANDOM_TREASURE_ART: - subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE); + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_TREASURE); break; case MapObjectID::RANDOM_MINOR_ART: - subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR); + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MINOR); break; case MapObjectID::RANDOM_MAJOR_ART: - subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR); + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_MAJOR); break; case MapObjectID::RANDOM_RELIC_ART: - subID = VLC->arth->pickRandomArtifact(rand, CArtifact::ART_RELIC); + subID = cb->gameState()->pickRandomArtifact(rand, CArtifact::ART_RELIC); break; } diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 41890cf47..b8be2b40a 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -858,7 +858,7 @@ void AddQuest::applyGs(CGameState * gs) const void UpdateArtHandlerLists::applyGs(CGameState * gs) const { - VLC->arth->allocatedArtifacts = allocatedArtifacts; + gs->allocatedArtifacts = allocatedArtifacts; } void UpdateMapEvents::applyGs(CGameState * gs) const diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 32463f6c3..3c64115d8 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -4057,7 +4057,7 @@ void CGameHandler::spawnWanderingMonsters(CreatureID creatureID) void CGameHandler::synchronizeArtifactHandlerLists() { UpdateArtHandlerLists uahl; - uahl.allocatedArtifacts = VLC->arth->allocatedArtifacts; + uahl.allocatedArtifacts = gs->allocatedArtifacts; sendAndApply(&uahl); } From 4e654a7e6eef65426fd23bec8024b9380f9fb050 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 17:29:57 +0200 Subject: [PATCH 1079/1248] Better names --- lib/serializer/BinaryDeserializer.h | 6 +++--- lib/serializer/BinarySerializer.h | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index cc3d06b08..9ce3fbf98 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -226,9 +226,9 @@ public: template < typename T, typename std::enable_if < std::is_pointer::value, int >::type = 0 > void load(T &data) { - ui8 hlp; - load( hlp ); - if(!hlp) + bool isNull; + load( isNull ); + if(isNull) { data = nullptr; return; diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index f9ecba9e0..18a6d88a0 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -174,11 +174,11 @@ public: void save(const T &data) { //write if pointer is not nullptr - ui8 hlp = (data!=nullptr); - save(hlp); + bool isNull = (data == nullptr); + save(isNull); //if pointer is nullptr then we don't need anything more... - if(!hlp) + if(data == nullptr) return; savePointerImpl(data); From ed66fc2fb3d90815f2fc334d30f9db28af53eae6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 17:30:16 +0200 Subject: [PATCH 1080/1248] Minor optimization of map startup time --- lib/gameState/CGameState.cpp | 4 +--- lib/mapObjects/CGObjectInstance.cpp | 2 +- lib/mapObjects/CGObjectInstance.h | 2 +- lib/mapObjects/ObjectTemplate.h | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index a365f1295..ed23c5566 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -921,11 +921,9 @@ void CGameState::initMapObjects() for(CGObjectInstance *obj : map->objects) { if(obj) - { - logGlobal->trace("Calling Init for object %d, %s, %s", obj->id.getNum(), obj->typeName, obj->subTypeName); obj->initObj(getRandomGenerator()); - } } + logGlobal->debug("\tObject initialization done"); for(CGObjectInstance *obj : map->objects) { if(!obj) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 3112c8e85..167e24c75 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -117,7 +117,7 @@ std::set CGObjectInstance::getBlockedPos() const return ret; } -std::set CGObjectInstance::getBlockedOffsets() const +const std::set & CGObjectInstance::getBlockedOffsets() const { return appearance->getBlockedOffsets(); } diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index 71cbabd41..cea2a4dad 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -77,7 +77,7 @@ public: bool coveringAt (const int3 & pos) const; //returns true if object covers with picture location (x, y) (h3m pos) std::set getBlockedPos() const; //returns set of positions blocked by this object - std::set getBlockedOffsets() const; //returns set of relative positions blocked by this object + const std::set & getBlockedOffsets() const; //returns set of relative positions blocked by this object /// returns true if object is visitable bool isVisitable() const; diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 3f967d76d..0a43b06c7 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -80,7 +80,7 @@ public: bool isVisibleAt(si32 X, si32 Y) const; bool isBlockedAt(si32 X, si32 Y) const; - inline std::set getBlockedOffsets() const + inline const std::set & getBlockedOffsets() const { return blockedOffsets; }; From a61ceaf2a744a57cdb511d115dd2b460f47ec002 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 17:30:59 +0200 Subject: [PATCH 1081/1248] Serialization version bump --- lib/serializer/CSerializer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 85673f215..4bb783a9a 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 829; -const ui32 MINIMAL_SERIALIZATION_VERSION = 829; +const ui32 SERIALIZATION_VERSION = 830; +const ui32 MINIMAL_SERIALIZATION_VERSION = 830; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From eb167d94a6ec22931f0252e537daaced6345caeb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 7 Nov 2023 18:32:40 +0200 Subject: [PATCH 1082/1248] Mod compatibility check is now in a separate class and not part of ModHandler --- client/mapView/mapHandler.cpp | 4 + cmake_modules/VCMI_lib.cmake | 3 + lib/IGameCallback.cpp | 9 +++ lib/mapping/CMapHeader.h | 2 +- lib/mapping/CMapService.h | 2 +- lib/mapping/MapFormatJson.cpp | 2 +- lib/modding/ActiveModsInSaveList.cpp | 112 +++++++++++++++++++++++++++ lib/modding/ActiveModsInSaveList.h | 51 ++++++++++++ lib/modding/CModHandler.cpp | 82 -------------------- lib/modding/CModHandler.h | 37 ++------- lib/modding/CModInfo.cpp | 2 +- lib/modding/CModInfo.h | 36 +-------- lib/modding/CModVersion.h | 2 + lib/modding/ModVerificationInfo.h | 44 +++++++++++ mapeditor/mapcontroller.h | 2 +- 15 files changed, 238 insertions(+), 152 deletions(-) create mode 100644 lib/modding/ActiveModsInSaveList.cpp create mode 100644 lib/modding/ActiveModsInSaveList.h create mode 100644 lib/modding/ModVerificationInfo.h diff --git a/client/mapView/mapHandler.cpp b/client/mapView/mapHandler.cpp index fdd4fae86..aac3232e1 100644 --- a/client/mapView/mapHandler.cpp +++ b/client/mapView/mapHandler.cpp @@ -74,6 +74,10 @@ std::string CMapHandler::getTerrainDescr(const int3 & pos, bool rightClick) cons bool CMapHandler::compareObjectBlitOrder(const CGObjectInstance * a, const CGObjectInstance * b) { + //FIXME: Optimize + // this method is called A LOT on game start and some parts, e.g. for loops are too slow for that + + assert(a && b); if(!a) return true; if(!b) diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 0b969d102..3e7663c2b 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -116,6 +116,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.cpp ${MAIN_LIB_DIR}/mapping/ObstacleProxy.cpp + ${MAIN_LIB_DIR}/modding/ActiveModsInSaveList.cpp ${MAIN_LIB_DIR}/modding/CModHandler.cpp ${MAIN_LIB_DIR}/modding/CModInfo.cpp ${MAIN_LIB_DIR}/modding/CModVersion.cpp @@ -466,6 +467,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/mapping/MapFormatJson.h ${MAIN_LIB_DIR}/mapping/ObstacleProxy.h + ${MAIN_LIB_DIR}/modding/ActiveModsInSaveList.h ${MAIN_LIB_DIR}/modding/CModHandler.h ${MAIN_LIB_DIR}/modding/CModInfo.h ${MAIN_LIB_DIR}/modding/CModVersion.h @@ -474,6 +476,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/modding/ModIncompatibility.h ${MAIN_LIB_DIR}/modding/ModScope.h ${MAIN_LIB_DIR}/modding/ModUtility.h + ${MAIN_LIB_DIR}/modding/ModVerificationInfo.h ${MAIN_LIB_DIR}/networkPacks/ArtifactLocation.h ${MAIN_LIB_DIR}/networkPacks/BattleChanges.h diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 4f625228a..76ff89457 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -40,6 +40,7 @@ #include "modding/CModInfo.h" #include "modding/IdentifierStorage.h" #include "modding/CModVersion.h" +#include "modding/ActiveModsInSaveList.h" #include "CPlayerState.h" #include "GameSettings.h" #include "ScriptHandler.h" @@ -183,6 +184,7 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) CMapHeader dum; StartInfo * si = nullptr; + ActiveModsInSaveList activeMods; logGlobal->info("\tReading header"); in.serializer & dum; @@ -190,6 +192,9 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) logGlobal->info("\tReading options"); in.serializer & si; + logGlobal->info("\tReading mod list"); + in.serializer & activeMods; + logGlobal->info("\tReading gamestate"); in.serializer & gs; } @@ -197,12 +202,16 @@ void CPrivilegedInfoCallback::loadCommonState(Loader & in) template void CPrivilegedInfoCallback::saveCommonState(Saver & out) const { + ActiveModsInSaveList activeMods; + logGlobal->info("Saving lib part of game..."); out.putMagicBytes(SAVEGAME_MAGIC); logGlobal->info("\tSaving header"); out.serializer & static_cast(*gs->map); logGlobal->info("\tSaving options"); out.serializer & gs->scenarioOps; + logGlobal->info("\tSaving mod list"); + out.serializer & activeMods; logGlobal->info("\tSaving gamestate"); out.serializer & gs; } diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index a64a08dfe..b492237dc 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -20,7 +20,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; enum class EMapFormat : uint8_t; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /// The hero name struct consists of the hero id and the hero name. struct DLL_LINKAGE SHeroName diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index ed791197b..f2b89d423 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -23,7 +23,7 @@ class CInputStream; class IMapLoader; class IMapPatcher; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /** * The map service provides loading of VCMI/H3 map files. It can diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 0f5691da8..62ad052f4 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -966,7 +966,7 @@ void CMapLoaderJson::readHeader(const bool complete) { for(auto & mod : header["mods"].Vector()) { - CModInfo::VerificationInfo info; + ModVerificationInfo info; info.version = CModVersion::fromString(mod["version"].String()); info.checksum = mod["checksum"].Integer(); info.name = mod["name"].String(); diff --git a/lib/modding/ActiveModsInSaveList.cpp b/lib/modding/ActiveModsInSaveList.cpp new file mode 100644 index 000000000..264e122ae --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.cpp @@ -0,0 +1,112 @@ +/* + * ActiveModsInSaveList.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 "ActiveModsInSaveList.h" + +#include "../VCMI_Lib.h" +#include "CModInfo.h" +#include "CModHandler.h" +#include "ModIncompatibility.h" + +VCMI_LIB_NAMESPACE_BEGIN + +std::vector ActiveModsInSaveList::getActiveMods() +{ + return VLC->modh->getActiveMods(); +} + +const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod) +{ + return VLC->modh->getModInfo(mod).getVerificationInfo(); +} + +void ActiveModsInSaveList::verifyActiveMods(const std::vector> & modList) +{ + auto searchVerificationInfo = [&modList](const TModID & m) -> const ModVerificationInfo* + { + for(auto & i : modList) + if(i.first == m) + return &i.second; + return nullptr; + }; + + std::vector missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + + for(const auto & m : VLC->modh->getActiveMods()) + { + if(searchVerificationInfo(m)) + continue; + + //TODO: support actual disabling of these mods + if(VLC->modh->getModInfo(m).checkModGameplayAffecting()) + excessiveMods.push_back(m); + } + + for(const auto & infoPair : modList) + { + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + + if(!vstd::contains(VLC->modh->getAllMods(), remoteModId)) + { + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed + continue; + } + + auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo(); + modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(VLC->modh->getActiveMods(), remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + continue; + + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //incompatible mod impacts gameplay + } + + //filter mods + for(auto & m : missingMods) + { + if(auto * vInfo = searchVerificationInfo(m)) + { + assert(vInfo->parent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + for(auto & m : excessiveMods) + { + auto & vInfo = VLC->modh->getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } + + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); + + //TODO: support actual enabling of required mods +} + + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ActiveModsInSaveList.h b/lib/modding/ActiveModsInSaveList.h new file mode 100644 index 000000000..846b51af7 --- /dev/null +++ b/lib/modding/ActiveModsInSaveList.h @@ -0,0 +1,51 @@ +/* + * ActiveModsInSaveList.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 "ModVerificationInfo.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class ActiveModsInSaveList +{ + std::vector getActiveMods(); + const ModVerificationInfo & getVerificationInfo(TModID mod); + + /// Checks whether provided mod list is compatible with current VLC and throws on failure + void verifyActiveMods(const std::vector> & modList); +public: + template void serialize(Handler &h, const int version) + { + if(h.saving) + { + std::vector activeMods = getActiveMods(); + h & activeMods; + for(const auto & m : activeMods) + h & getVerificationInfo(m); + } + else + { + std::vector saveActiveMods; + h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } + + verifyActiveMods(saveModInfos); + } + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 983e80130..7c220914d 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -491,88 +491,6 @@ void CModHandler::afterLoad(bool onlyEssential) std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc); file << modSettings.toJson(); } - -} - -void CModHandler::trySetActiveMods(const std::vector> & modList) -{ - auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* - { - for(auto & i : modList) - if(i.first == m) - return &i.second; - return nullptr; - }; - - std::vector missingMods, excessiveMods; - ModIncompatibility::ModListWithVersion missingModsResult; - ModIncompatibility::ModList excessiveModsResult; - - for(const auto & m : activeMods) - { - if(searchVerificationInfo(m)) - continue; - - //TODO: support actual disabling of these mods - if(getModInfo(m).checkModGameplayAffecting()) - excessiveMods.push_back(m); - } - - for(const auto & infoPair : modList) - { - auto & remoteModId = infoPair.first; - auto & remoteModInfo = infoPair.second; - - bool modAffectsGameplay = remoteModInfo.impactsGameplay; - //parent mod affects gameplay if child affects too - for(const auto & subInfoPair : modList) - modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); - - if(!allMods.count(remoteModId)) - { - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //mod is not installed - continue; - } - - auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); - bool modVersionCompatible = localModInfo.version.isNull() - || remoteModInfo.version.isNull() - || localModInfo.version.compatible(remoteModInfo.version); - bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); - - if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) - continue; - - if(modAffectsGameplay) - missingMods.push_back(remoteModId); //incompatible mod impacts gameplay - } - - //filter mods - for(auto & m : missingMods) - { - if(auto * vInfo = searchVerificationInfo(m)) - { - assert(vInfo->parent != m); - if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) - continue; - missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); - } - } - for(auto & m : excessiveMods) - { - auto & vInfo = getModInfo(m).getVerificationInfo(); - assert(vInfo.parent != m); - if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) - continue; - excessiveModsResult.push_back(vInfo.name); - } - - if(!missingModsResult.empty() || !excessiveModsResult.empty()) - throw ModIncompatibility(missingModsResult, excessiveModsResult); - - //TODO: support actual enabling of required mods } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index af0959fe9..2842d90aa 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -9,21 +9,22 @@ */ #pragma once -#include "CModInfo.h" - VCMI_LIB_NAMESPACE_BEGIN class CModHandler; class CModIndentifier; +class CModInfo; +struct CModVersion; class JsonNode; class IHandlerBase; class CIdentifierStorage; class CContentHandler; +struct ModVerificationInfo; class ResourcePath; using TModID = std::string; -class DLL_LINKAGE CModHandler : boost::noncopyable +class DLL_LINKAGE CModHandler final : boost::noncopyable { std::map allMods; std::vector activeMods;//active mods, in order in which they were loaded @@ -50,9 +51,6 @@ class DLL_LINKAGE CModHandler : boost::noncopyable CModVersion getModVersion(TModID modName) const; - /// Attempt to set active mods according to provided list of mods from save, throws on failure - void trySetActiveMods(const std::vector> & modList); - public: std::shared_ptr content; //(!)Do not serialize FIXME: make private @@ -79,32 +77,7 @@ public: void afterLoad(bool onlyEssential); CModHandler(); - virtual ~CModHandler(); - - template void serialize(Handler &h, const int version) - { - if(h.saving) - { - h & activeMods; - for(const auto & m : activeMods) - h & getModInfo(m).getVerificationInfo(); - } - else - { - loadMods(); - std::vector saveActiveMods; - h & saveActiveMods; - - std::vector> saveModInfos(saveActiveMods.size()); - for(int i = 0; i < saveActiveMods.size(); ++i) - { - saveModInfos[i].first = saveActiveMods[i]; - h & saveModInfos[i].second; - } - - trySetActiveMods(saveModInfos); - } - } + ~CModHandler(); }; VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 0e534da45..7cb346789 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -177,7 +177,7 @@ bool CModInfo::checkModGameplayAffecting() const return *modGameplayAffecting; } -const CModInfo::VerificationInfo & CModInfo::getVerificationInfo() const +const ModVerificationInfo & CModInfo::getVerificationInfo() const { return verificationInfo; } diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index f9f227e2a..2c29e2159 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -10,12 +10,10 @@ #pragma once #include "../JsonNode.h" -#include "CModVersion.h" +#include "ModVerificationInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using TModID = std::string; - class DLL_LINKAGE CModInfo { /// cached result of checkModGameplayAffecting() call @@ -30,34 +28,6 @@ public: PASSED }; - struct VerificationInfo - { - /// human-readable mod name - std::string name; - - /// version of the mod - CModVersion version; - - /// CRC-32 checksum of the mod - ui32 checksum = 0; - - /// parent mod ID, empty if root-level mod - TModID parent; - - /// for serialization purposes - bool impactsGameplay = true; - - template - void serialize(Handler & h, const int v) - { - h & name; - h & version; - h & checksum; - h & parent; - h & impactsGameplay; - } - }; - /// identifier, identical to name of folder with mod std::string identifier; @@ -94,7 +64,7 @@ public: /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects bool checkModGameplayAffecting() const; - const VerificationInfo & getVerificationInfo() const; + const ModVerificationInfo & getVerificationInfo() const; private: /// true if mod is enabled by user, e.g. in Launcher UI @@ -103,7 +73,7 @@ private: /// true if mod can be loaded - compatible and has no missing deps bool implicitlyEnabled; - VerificationInfo verificationInfo; + ModVerificationInfo verificationInfo; void loadLocalData(const JsonNode & data); }; diff --git a/lib/modding/CModVersion.h b/lib/modding/CModVersion.h index 15221dab3..e0ae7c8be 100644 --- a/lib/modding/CModVersion.h +++ b/lib/modding/CModVersion.h @@ -18,6 +18,8 @@ VCMI_LIB_NAMESPACE_BEGIN +using TModID = std::string; + struct DLL_LINKAGE CModVersion { static const int Any = -1; diff --git a/lib/modding/ModVerificationInfo.h b/lib/modding/ModVerificationInfo.h new file mode 100644 index 000000000..e6652e40d --- /dev/null +++ b/lib/modding/ModVerificationInfo.h @@ -0,0 +1,44 @@ +/* + * ModVerificationInfo.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 "CModVersion.h" + +VCMI_LIB_NAMESPACE_BEGIN + +struct ModVerificationInfo +{ + /// human-readable mod name + std::string name; + + /// version of the mod + CModVersion version; + + /// CRC-32 checksum of the mod + ui32 checksum = 0; + + /// parent mod ID, empty if root-level mod + TModID parent; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h, const int v) + { + h & name; + h & version; + h & checksum; + h & parent; + h & impactsGameplay; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index 18d660a3a..7b8a246eb 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -16,7 +16,7 @@ #include "../lib/modding/CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END From ce3d407396cdfcefd4f85e11fc114261bee1646a Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:12:32 +0200 Subject: [PATCH 1083/1248] artifacts swap optimization --- client/NetPacksClient.cpp | 2 +- lib/networkPacks/ArtifactLocation.h | 6 +++ lib/networkPacks/NetPacksLib.cpp | 8 +-- lib/networkPacks/PacksForClient.h | 12 +++++ server/CGameHandler.cpp | 80 ++++++++++++++--------------- 5 files changed, 62 insertions(+), 46 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index 746c5ec2d..df4c66d1b 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -301,7 +301,7 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack) { auto srcLoc = ArtifactLocation(pack.srcArtHolder, slotToMove.srcPos); auto dstLoc = ArtifactLocation(pack.dstArtHolder, slotToMove.dstPos); - MoveArtifact ma(&srcLoc, &dstLoc, false); + MoveArtifact ma(&srcLoc, &dstLoc, pack.askAssemble); visitMoveArtifact(ma); } }; diff --git a/lib/networkPacks/ArtifactLocation.h b/lib/networkPacks/ArtifactLocation.h index 085d295d0..d3acb2cdd 100644 --- a/lib/networkPacks/ArtifactLocation.h +++ b/lib/networkPacks/ArtifactLocation.h @@ -31,6 +31,12 @@ struct ArtifactLocation , creature(std::nullopt) { } + ArtifactLocation(const ObjectInstanceID id, const std::optional creatureSlot) + : artHolder(id) + , slot(ArtifactPosition::PRE_FIRST) + , creature(creatureSlot) + { + } template void serialize(Handler & h, const int version) { diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 41890cf47..41887e1be 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1837,13 +1837,13 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) switch(operation) { case EBulkArtsOp::BULK_MOVE: - art->move(artSet, srcPos, *gs->getHero(dstArtHolder), slot.dstPos); + art->move(artSet, srcPos, *gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)), slot.dstPos); break; case EBulkArtsOp::BULK_REMOVE: art->removeFrom(artSet, srcPos); break; case EBulkArtsOp::BULK_PUT: - art->putAt(*gs->getHero(srcArtHolder), slot.dstPos); + art->putAt(*gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)), slot.dstPos); break; default: break; @@ -1856,11 +1856,11 @@ void BulkMoveArtifacts::applyGs(CGameState * gs) } }; - auto * leftSet = gs->getArtSet(ArtifactLocation(srcArtHolder)); + auto * leftSet = gs->getArtSet(ArtifactLocation(srcArtHolder, srcCreature)); if(swap) { // Swap - auto * rightSet = gs->getArtSet(ArtifactLocation(dstArtHolder)); + auto * rightSet = gs->getArtSet(ArtifactLocation(dstArtHolder, dstCreature)); CArtifactFittingSet artFittingSet(leftSet->bearerType()); artFittingSet.artifactsWorn = rightSet->artifactsWorn; diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 7de437f00..caf99c251 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1064,17 +1064,25 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack ObjectInstanceID srcArtHolder; ObjectInstanceID dstArtHolder; + std::optional srcCreature; + std::optional dstCreature; BulkMoveArtifacts() : srcArtHolder(ObjectInstanceID::NONE) , dstArtHolder(ObjectInstanceID::NONE) , swap(false) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) { } BulkMoveArtifacts(const ObjectInstanceID srcArtHolder, const ObjectInstanceID dstArtHolder, bool swap) : srcArtHolder(std::move(srcArtHolder)) , dstArtHolder(std::move(dstArtHolder)) , swap(swap) + , askAssemble(false) + , srcCreature(std::nullopt) + , dstCreature(std::nullopt) { } @@ -1083,6 +1091,7 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack std::vector artsPack0; std::vector artsPack1; bool swap; + bool askAssemble; void visitTyped(ICPackVisitor & visitor) override; @@ -1092,7 +1101,10 @@ struct DLL_LINKAGE BulkMoveArtifacts : CArtifactOperationPack h & artsPack1; h & srcArtHolder; h & dstArtHolder; + h & srcCreature; + h & dstCreature; h & swap; + h & askAssemble; } }; diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 32463f6c3..ccb9be167 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2667,19 +2667,18 @@ bool CGameHandler::garrisonSwap(ObjectInstanceID tid) // Function moves artifact from src to dst. If dst is not a backpack and is already occupied, old dst art goes to backpack and is replaced. bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) { - ArtifactLocation srcLoc = src, dstLoc = dst; - const auto srcArtSet = getArtSet(srcLoc); - const auto dstArtSet = getArtSet(dstLoc); + const auto srcArtSet = getArtSet(src); + const auto dstArtSet = getArtSet(dst); assert(srcArtSet); assert(dstArtSet); // Make sure exchange is even possible between the two heroes. - if(!isAllowedExchange(srcLoc.artHolder, dstLoc.artHolder)) + if(!isAllowedExchange(src.artHolder, dst.artHolder)) COMPLAIN_RET("That heroes cannot make any exchange!"); - const auto srcArtifact = srcArtSet->getArt(srcLoc.slot); - const auto dstArtifact = dstArtSet->getArt(dstLoc.slot); - const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dstLoc.slot) : false; + const auto srcArtifact = srcArtSet->getArt(src.slot); + const auto dstArtifact = dstArtSet->getArt(dst.slot); + const bool isDstSlotBackpack = dstArtSet->bearerType() == ArtBearer::HERO ? ArtifactUtils::isSlotBackpack(dst.slot) : false; if(srcArtifact == nullptr) COMPLAIN_RET("No artifact to move!"); @@ -2688,55 +2687,54 @@ bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLoca // Check if src/dest slots are appropriate for the artifacts exchanged. // Moving to the backpack is always allowed. - if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstArtSet, dstLoc.slot, true)) + if((!srcArtifact || !isDstSlotBackpack) && srcArtifact && !srcArtifact->canBePutAt(dstArtSet, dst.slot, true)) COMPLAIN_RET("Cannot move artifact!"); - auto srcSlotInfo = srcArtSet->getSlot(srcLoc.slot); - auto dstSlotInfo = dstArtSet->getSlot(dstLoc.slot); + auto srcSlotInfo = srcArtSet->getSlot(src.slot); + auto dstSlotInfo = dstArtSet->getSlot(dst.slot); if((srcSlotInfo && srcSlotInfo->locked) || (dstSlotInfo && dstSlotInfo->locked)) COMPLAIN_RET("Cannot move artifact locks."); if(isDstSlotBackpack && srcArtifact->artType->isBig()) COMPLAIN_RET("Cannot put big artifacts in backpack!"); - if(srcLoc.slot == ArtifactPosition::MACH4 || dstLoc.slot == ArtifactPosition::MACH4) + if(src.slot == ArtifactPosition::MACH4 || dst.slot == ArtifactPosition::MACH4) COMPLAIN_RET("Cannot move catapult!"); + if(isDstSlotBackpack && !ArtifactUtils::isBackpackFreeSlots(dstArtSet)) + COMPLAIN_RET("Backpack is full!"); - if(isDstSlotBackpack) + auto dstSlot = std::min(dst.slot, ArtifactPosition(ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size())); + + if(src.slot == dstSlot && src.artHolder == dst.artHolder) + COMPLAIN_RET("Won't move artifact: Dest same as source!"); + + BulkMoveArtifacts ma(src.artHolder, dst.artHolder, false); + ma.srcCreature = src.creature; + ma.dstCreature = dst.creature; + + // Check if dst slot is occupied + if(!isDstSlotBackpack && dstArtifact) { - if(!ArtifactUtils::isBackpackFreeSlots(dstArtSet)) - COMPLAIN_RET("Backpack is full!"); - vstd::amin(dstLoc.slot, ArtifactPosition::BACKPACK_START + dstArtSet->artifactsInBackpack.size()); + // Previous artifact must be removed + ma.artsPack1.push_back(BulkMoveArtifacts::LinkedSlots(dstSlot, src.slot)); + ma.swap = true; } - if(!(srcLoc.slot == ArtifactPosition::TRANSITION_POS && dstLoc.slot == ArtifactPosition::TRANSITION_POS)) + try { - if(srcLoc.slot == dstLoc.slot && srcLoc.artHolder == dstLoc.artHolder) - COMPLAIN_RET("Won't move artifact: Dest same as source!"); - - // Check if dst slot is occupied - if(!isDstSlotBackpack && dstArtifact) - { - // Previous artifact must be removed first - moveArtifact(dstLoc, ArtifactLocation(dstLoc.artHolder, ArtifactPosition::TRANSITION_POS)); - } - - try - { - auto hero = getHero(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstLoc.slot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - } - catch(const std::bad_variant_access &) - { - // object other than hero received an art - ignore - } - - MoveArtifact ma(&srcLoc, &dstLoc); - if(srcLoc.artHolder == dstLoc.artHolder) - ma.askAssemble = false; - sendAndApply(&ma); + auto hero = getHero(dst.artHolder); + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) + giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); } + catch(const std::bad_variant_access &) + { + // object other than hero received an art - ignore + } + + ma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(src.slot, dstSlot)); + if(src.artHolder != dst.artHolder) + ma.askAssemble = true; + sendAndApply(&ma); return true; } From 57e3abc54848fb4437244a2504e7232ddaf37f2e Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Wed, 8 Nov 2023 19:54:02 +0200 Subject: [PATCH 1084/1248] ask assemble regression fixed --- client/Client.h | 3 +- lib/ArtifactUtils.cpp | 9 +++--- lib/IGameCallback.h | 3 +- lib/mapObjects/MiscObjects.cpp | 2 +- lib/networkPacks/PacksForClient.h | 6 ++-- server/CGameHandler.cpp | 50 ++++++++++++++++++------------- server/CGameHandler.h | 3 +- 7 files changed, 41 insertions(+), 35 deletions(-) diff --git a/client/Client.h b/client/Client.h index 31516cb98..32cbe1db2 100644 --- a/client/Client.h +++ b/client/Client.h @@ -188,8 +188,7 @@ public: void removeAfterVisit(const CGObjectInstance * object) override {}; bool swapGarrisonOnSiege(ObjectInstanceID tid) override {return false;}; bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation & al, const CArtifactInstance * a) override {}; + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;}; void removeArtifact(const ArtifactLocation & al) override {}; bool moveArtifact(const ArtifactLocation & al1, const ArtifactLocation & al2) override {return false;}; diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 85a72bf66..c306e1ef4 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -33,10 +33,11 @@ DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtBackpackPosition(const CArtifactSet * target, const ArtifactID & aid) { const auto * art = aid.toArtifact(); - if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) - { - return ArtifactPosition::BACKPACK_START; - } + if(target->bearerType() == ArtBearer::HERO) + if(art->canBePutAt(target, ArtifactPosition::BACKPACK_START)) + { + return ArtifactPosition::BACKPACK_START; + } return ArtifactPosition::PRE_FIRST; } diff --git a/lib/IGameCallback.h b/lib/IGameCallback.h index ef9e95221..6658df008 100644 --- a/lib/IGameCallback.h +++ b/lib/IGameCallback.h @@ -107,8 +107,7 @@ public: virtual void removeAfterVisit(const CGObjectInstance *object) = 0; //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! virtual bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) = 0; - virtual bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) = 0; - virtual void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) = 0; + virtual bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble = std::nullopt) = 0; virtual void removeArtifact(const ArtifactLocation &al) = 0; virtual bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) = 0; diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c82484c54..700863c76 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -881,7 +881,7 @@ void CGArtifact::onHeroVisit(const CGHeroInstance * h) const void CGArtifact::pick(const CGHeroInstance * h) const { - if(cb->giveHeroArtifact(h, storedArtifact, ArtifactPosition::FIRST_AVAILABLE)) + if(cb->putArtifact(ArtifactLocation(h->id, ArtifactPosition::FIRST_AVAILABLE), storedArtifact)) cb->removeObject(this, h->getOwner()); } diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index caf99c251..44ef00595 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -975,13 +975,13 @@ struct DLL_LINKAGE CArtifactOperationPack : CPackForClient struct DLL_LINKAGE PutArtifact : CArtifactOperationPack { PutArtifact() = default; - explicit PutArtifact(ArtifactLocation * dst, bool askAssemble = true) - : al(*dst), askAssemble(askAssemble) + explicit PutArtifact(ArtifactLocation & dst, bool askAssemble = true) + : al(dst), askAssemble(askAssemble) { } ArtifactLocation al; - bool askAssemble = false; + bool askAssemble; ConstTransitivePtr art; void applyGs(CGameState * gs); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index ccb9be167..59c4ee89c 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -3959,38 +3959,46 @@ bool CGameHandler::swapStacks(const StackLocation & sl1, const StackLocation & s } } -bool CGameHandler::giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) +bool CGameHandler::putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) { - assert(a->artType); - ArtifactLocation al(h->id, ArtifactPosition::PRE_FIRST); + assert(art && art->artType); + ArtifactLocation dst(al.artHolder, ArtifactPosition::PRE_FIRST); + dst.creature = al.creature; + auto putTo = getArtSet(al); + assert(putTo); - if(pos == ArtifactPosition::FIRST_AVAILABLE) + if(al.slot == ArtifactPosition::FIRST_AVAILABLE) { - al.slot = ArtifactUtils::getArtAnyPosition(h, a->getTypeId()); + dst.slot = ArtifactUtils::getArtAnyPosition(putTo, art->getTypeId()); } - else if(ArtifactUtils::isSlotBackpack(pos)) + else if(ArtifactUtils::isSlotBackpack(al.slot) && !al.creature.has_value()) { - al.slot = ArtifactUtils::getArtBackpackPosition(h, a->getTypeId()); + dst.slot = ArtifactUtils::getArtBackpackPosition(putTo, art->getTypeId()); } else { - al.slot = pos; + dst.slot = al.slot; } - if(a->canBePutAt(h, al.slot)) - putArtifact(al, a); + if(!askAssemble.has_value()) + { + if(!dst.creature.has_value() && ArtifactUtils::isSlotEquipment(dst.slot)) + askAssemble = true; + else + askAssemble = false; + } + + if(art->canBePutAt(putTo, dst.slot)) + { + PutArtifact pa(dst, askAssemble.value()); + pa.art = art; + sendAndApply(&pa); + return true; + } else + { return false; - - return true; -} - -void CGameHandler::putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) -{ - PutArtifact pa; - pa.art = a; - pa.al = al; - sendAndApply(&pa); + } } bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) @@ -4019,7 +4027,7 @@ bool CGameHandler::giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact na.art = newArtInst; sendAndApply(&na); // -> updates newArtInst!!! - if(giveHeroArtifact(h, newArtInst, pos)) + if(putArtifact(ArtifactLocation(h->id, pos), newArtInst, false)) return true; else return false; diff --git a/server/CGameHandler.h b/server/CGameHandler.h index e100bfcbd..9d15d33a8 100644 --- a/server/CGameHandler.h +++ b/server/CGameHandler.h @@ -127,8 +127,7 @@ public: void removeAfterVisit(const CGObjectInstance *object) override; bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos = ArtifactPosition::FIRST_AVAILABLE) override; - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override; - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override; + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override; void removeArtifact(const ArtifactLocation &al) override; bool moveArtifact(const ArtifactLocation & src, const ArtifactLocation & dst) override; bool bulkMoveArtifacts(ObjectInstanceID srcHero, ObjectInstanceID dstHero, bool swap, bool equipped, bool backpack); From 4a7fa9bf8c26d2a5cb33443fb13cac922f38811e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 22:05:36 +0200 Subject: [PATCH 1085/1248] Simplified CTypeList class --- AI/Nullkiller/AIGateway.cpp | 4 +- AI/VCAI/VCAI.cpp | 4 +- client/CPlayerInterface.cpp | 4 +- client/CServerHandler.cpp | 8 +- client/Client.cpp | 10 +- lib/gameState/CGameState.cpp | 2 +- lib/registerTypes/RegisterTypes.cpp | 3 +- lib/registerTypes/RegisterTypes.h | 2 +- lib/serializer/BinaryDeserializer.h | 63 +++----- lib/serializer/BinarySerializer.h | 10 +- lib/serializer/CTypeList.cpp | 119 --------------- lib/serializer/CTypeList.h | 224 ++++++---------------------- lib/serializer/Cast.h | 40 ----- server/CGameHandler.cpp | 4 +- server/CVCMIServer.cpp | 4 +- 15 files changed, 96 insertions(+), 405 deletions(-) diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index 501c61eeb..c57a0fb34 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -421,14 +421,14 @@ void AIGateway::requestRealized(PackageApplied * pa) NET_EVENT_HANDLER; if(status.haveTurn()) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { if(pa->result) status.madeTurn(); } } - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } diff --git a/AI/VCAI/VCAI.cpp b/AI/VCAI/VCAI.cpp index c3068c989..c0b4efa3a 100644 --- a/AI/VCAI/VCAI.cpp +++ b/AI/VCAI/VCAI.cpp @@ -505,14 +505,14 @@ void VCAI::requestRealized(PackageApplied * pa) NET_EVENT_HANDLER; if(status.haveTurn()) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { if(pa->result) status.madeTurn(); } } - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) { status.receivedAnswerConfirmation(pa->requestID, pa->result); } diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index e604e070f..9a2ede905 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -1243,10 +1243,10 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con void CPlayerInterface::requestRealized( PackageApplied *pa ) { - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) movementController->onMoveHeroApplied(); - if(pa->packType == typeList.getTypeID()) + if(pa->packType == CTypeList::getInstance().getTypeID(nullptr)) movementController->onQueryReplyApplied(); } diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 6e6018f0e..d0930fe15 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -94,7 +94,7 @@ public: T * ptr = static_cast(pack); ApplyOnLobbyHandlerNetPackVisitor visitor(*handler); - logNetwork->trace("\tImmediately apply on lobby: %s", typeList.getTypeInfo(ptr)->name()); + logNetwork->trace("\tImmediately apply on lobby: %s", typeid(ptr).name()); ptr->visit(visitor); return visitor.getResult(); @@ -105,7 +105,7 @@ public: T * ptr = static_cast(pack); ApplyOnLobbyScreenNetPackVisitor visitor(*handler, lobby); - logNetwork->trace("\tApply on lobby from queue: %s", typeList.getTypeInfo(ptr)->name()); + logNetwork->trace("\tApply on lobby from queue: %s", typeid(ptr).name()); ptr->visit(visitor); } }; @@ -298,7 +298,7 @@ void CServerHandler::applyPacksOnLobbyScreen() boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); CPackForLobby * pack = packsForLobbyScreen.front(); packsForLobbyScreen.pop_front(); - CBaseForLobbyApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + CBaseForLobbyApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier apply->applyOnLobbyScreen(dynamic_cast(SEL), this, pack); GH.windows().totalRedraw(); delete pack; @@ -953,7 +953,7 @@ void CServerHandler::threadHandleConnection() void CServerHandler::visitForLobby(CPackForLobby & lobbyPack) { - if(applier->getApplier(typeList.getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) + if(applier->getApplier(CTypeList::getInstance().getTypeID(&lobbyPack))->applyOnLobbyHandler(this, &lobbyPack)) { if(!settings["session"]["headless"].Bool()) { diff --git a/client/Client.cpp b/client/Client.cpp index c6e99d405..280f4daf1 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -523,20 +523,20 @@ void CClient::installNewBattleInterface(std::shared_ptr ba void CClient::handlePack(CPack * pack) { - CBaseForCLApply * apply = applier->getApplier(typeList.getTypeID(pack)); //find the applier + CBaseForCLApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //find the applier if(apply) { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); apply->applyOnClBefore(this, pack); - logNetwork->trace("\tMade first apply on cl: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tMade first apply on cl: %s", typeid(pack).name()); gs->apply(pack); - logNetwork->trace("\tApplied on gs: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tApplied on gs: %s", typeid(pack).name()); apply->applyOnClAfter(this, pack); - logNetwork->trace("\tMade second apply on cl: %s", typeList.getTypeInfo(pack)->name()); + logNetwork->trace("\tMade second apply on cl: %s", typeid(pack).name()); } else { - logNetwork->error("Message %s cannot be applied, cannot find applier!", typeList.getTypeInfo(pack)->name()); + logNetwork->error("Message %s cannot be applied, cannot find applier!", typeid(pack).name()); } delete pack; } diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index ed23c5566..7f9fda6df 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1134,7 +1134,7 @@ PlayerRelations CGameState::getPlayerRelations( PlayerColor color1, PlayerColor void CGameState::apply(CPack *pack) { - ui16 typ = typeList.getTypeID(pack); + ui16 typ = CTypeList::getInstance().getTypeID(pack); applier->getApplier(typ)->applyOnGS(this, pack); } diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp index 9408fa7ff..cc96a27b5 100644 --- a/lib/registerTypes/RegisterTypes.cpp +++ b/lib/registerTypes/RegisterTypes.cpp @@ -34,8 +34,7 @@ VCMI_LIB_NAMESPACE_BEGIN #define DEFINE_EXTERNAL_METHOD(METHODNAME) \ extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ -extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); \ -extern template DLL_LINKAGE void METHODNAME(CTypeList & s); \ +extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); //DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index db6866b78..9736aa285 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -19,6 +19,7 @@ #include "../mapObjects/CGCreature.h" #include "../mapObjects/CGTownBuilding.h" #include "../mapObjects/ObjectTemplate.h" +#include "../battle/BattleInfo.h" #include "../battle/CObstacleInstance.h" #include "../bonuses/Limiters.h" #include "../bonuses/Updaters.h" @@ -351,7 +352,6 @@ void registerTypes(Serializer &s) extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); -extern template DLL_LINKAGE void registerTypes(CTypeList & s); #endif diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 9ce3fbf98..9b57ee480 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -9,6 +9,7 @@ */ #pragma once +#include "CSerializer.h" #include "CTypeList.h" #include "../mapObjects/CGHeroInstance.h" #include "../../Global.h" @@ -100,40 +101,39 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase return length; } - template class CPointerLoader; + template class CPointerLoader; - class CBasicPointerLoader + class IPointerLoader { public: - virtual const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER - virtual ~CBasicPointerLoader(){} + virtual void * loadPtr(CLoaderBase &ar, ui32 pid) const =0; //data is pointer to the ACTUAL POINTER + virtual ~IPointerLoader() = default; - template static CBasicPointerLoader *getApplier(const T * t=nullptr) + template static IPointerLoader *getApplier(const Type * t = nullptr) { - return new CPointerLoader(); + return new CPointerLoader(); } }; - template class CPointerLoader : public CBasicPointerLoader + template class CPointerLoader : public IPointerLoader { public: - const std::type_info * loadPtr(CLoaderBase &ar, void *data, ui32 pid) const override //data is pointer to the ACTUAL POINTER + void * loadPtr(CLoaderBase &ar, ui32 pid) const override //data is pointer to the ACTUAL POINTER { auto & s = static_cast(ar); - T *&ptr = *static_cast(data); //create new object under pointer - typedef typename std::remove_pointer::type npT; - ptr = ClassObjectCreator::invoke(); //does new npT or throws for abstract classes + Type * ptr = ClassObjectCreator::invoke(); //does new npT or throws for abstract classes s.ptrAllocated(ptr, pid); - //T is most derived known type, it's time to call actual serialize + assert(s.fileVersion != 0); ptr->serialize(s,s.fileVersion); - return &typeid(T); + + return static_cast(ptr); } }; - CApplier applier; + CApplier applier; int write(const void * data, unsigned size); @@ -143,7 +143,7 @@ public: std::map loadedPointers; std::map loadedPointersTypes; - std::map loadedSharedPointers; + std::map> loadedSharedPointers; bool smartPointerSerialization; bool saving; @@ -288,7 +288,7 @@ public: // We already got this pointer // Cast it in case we are loading it to a non-first base pointer assert(loadedPointersTypes.count(pid)); - data = reinterpret_cast(typeList.castRaw(i->second, loadedPointersTypes.at(pid), &typeid(typename std::remove_const::type>::type))); + data = static_cast(i->second); return; } } @@ -313,8 +313,7 @@ public: data = nullptr; return; } - auto typeInfo = app->loadPtr(*this,&data, pid); - data = reinterpret_cast(typeList.castRaw((void*)data, typeInfo, &typeid(typename std::remove_const::type>::type))); + data = static_cast(app->loadPtr(*this, pid)); } } @@ -340,7 +339,7 @@ public: NonConstT *internalPtr; load(internalPtr); - void *internalPtrDerived = typeList.castToMostDerived(internalPtr); + void * internalPtrDerived = static_cast(internalPtr); if(internalPtr) { @@ -349,35 +348,13 @@ public: { // This pointers is already loaded. The "data" needs to be pointed to it, // so their shared state is actually shared. - try - { - auto actualType = typeList.getTypeInfo(internalPtr); - auto typeWeNeedToReturn = typeList.getTypeInfo(); - if(*actualType == *typeWeNeedToReturn) - { - // No casting needed, just unpack already stored shared_ptr and return it - data = std::any_cast>(itr->second); - } - else - { - // We need to perform series of casts - auto ret = typeList.castShared(itr->second, actualType, typeWeNeedToReturn); - data = std::any_cast>(ret); - } - } - catch(std::exception &e) - { - logGlobal->error(e.what()); - logGlobal->error("Failed to cast stored shared ptr. Real type: %s. Needed type %s. FIXME FIXME FIXME", itr->second.type().name(), typeid(std::shared_ptr).name()); - //TODO scenario with inheritance -> we can have stored ptr to base and load ptr to derived (or vice versa) - throw; - } + data = std::static_pointer_cast(itr->second); } else { auto hlp = std::shared_ptr(internalPtr); data = hlp; - loadedSharedPointers[internalPtrDerived] = typeList.castSharedToMostDerived(hlp); + loadedSharedPointers[internalPtrDerived] = std::static_pointer_cast(hlp); } } else diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 18a6d88a0..f65ef07ec 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -9,6 +9,7 @@ */ #pragma once +#include "CSerializer.h" #include "CTypeList.h" #include "../mapObjects/CArmedInstance.h" @@ -194,9 +195,10 @@ public: template < typename T, typename std::enable_if < !std::is_base_of_v>, int >::type = 0 > void savePointerImpl(const T &data) { + typedef typename std::remove_const::type>::type TObjectType; + if(writer->smartVectorMembersSerialization) { - typedef typename std::remove_const::type>::type TObjectType; typedef typename VectorizedTypeFor::type VType; typedef typename VectorizedIDType::type IDType; @@ -220,7 +222,7 @@ public: { // We might have an object that has multiple inheritance and store it via the non-first base pointer. // Therefore, all pointers need to be normalized to the actual object address. - auto actualPointer = typeList.castToMostDerived(data); + const void * actualPointer = static_cast(data); auto i = savedPointers.find(actualPointer); if(i != savedPointers.end()) { @@ -236,13 +238,13 @@ public: } //write type identifier - ui16 tid = typeList.getTypeID(data); + uint16_t tid = CTypeList::getInstance().getTypeID(data); save(tid); if(!tid) save(*data); //if type is unregistered simply write all data in a standard way else - applier.getApplier(tid)->savePtr(*this, typeList.castToMostDerived(data)); //call serializer specific for our real type + applier.getApplier(tid)->savePtr(*this, static_cast(data)); //call serializer specific for our real type } template < typename T, typename std::enable_if < is_serializeable::value, int >::type = 0 > diff --git a/lib/serializer/CTypeList.cpp b/lib/serializer/CTypeList.cpp index 1c94dcf43..8c75517ca 100644 --- a/lib/serializer/CTypeList.cpp +++ b/lib/serializer/CTypeList.cpp @@ -14,128 +14,9 @@ VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(CTypeList & s); - -CTypeList typeList; - CTypeList::CTypeList() { registerTypes(*this); } -CTypeList::TypeInfoPtr CTypeList::registerType(const std::type_info *type) -{ - if(auto typeDescr = getTypeDescriptor(type, false)) - return typeDescr; //type found, return ptr to structure - - //type not found - add it to the list and return given ID - auto newType = std::make_shared(); - newType->typeID = static_cast(typeInfos.size() + 1); - newType->name = type->name(); - typeInfos[type] = newType; - - return newType; -} - -ui16 CTypeList::getTypeID(const std::type_info *type, bool throws) const -{ - auto descriptor = getTypeDescriptor(type, throws); - if (descriptor == nullptr) - { - return 0; - } - return descriptor->typeID; -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(ui16 typeID) const -{ - auto found = std::find_if(typeInfos.begin(), typeInfos.end(), [typeID](const std::pair & p) -> bool - { - return p.second->typeID == typeID; - }); - - if(found != typeInfos.end()) - { - return found->second; - } - - return TypeInfoPtr(); -} - -std::vector CTypeList::castSequence(TypeInfoPtr from, TypeInfoPtr to) const -{ - if(!strcmp(from->name, to->name)) - return std::vector(); - - // Perform a simple BFS in the class hierarchy. - - auto BFS = [&](bool upcast) - { - std::map previous; - std::queue q; - q.push(to); - while(!q.empty()) - { - auto typeNode = q.front(); - q.pop(); - for(auto & weakNode : (upcast ? typeNode->parents : typeNode->children) ) - { - auto nodeBase = weakNode.lock(); - if(!previous.count(nodeBase)) - { - previous[nodeBase] = typeNode; - q.push(nodeBase); - } - } - } - - std::vector ret; - - if(!previous.count(from)) - return ret; - - ret.push_back(from); - TypeInfoPtr ptr = from; - do - { - ptr = previous.at(ptr); - ret.push_back(ptr); - } while(ptr != to); - - return ret; - }; - - // Try looking both up and down. - auto ret = BFS(true); - if(ret.empty()) - ret = BFS(false); - - if(ret.empty()) - THROW_FORMAT("Cannot find relation between types %s and %s. Were they (and all classes between them) properly registered?", from->name % to->name); - - return ret; -} - -std::vector CTypeList::castSequence(const std::type_info *from, const std::type_info *to) const -{ - //This additional if is needed because getTypeDescriptor might fail if type is not registered - // (and if casting is not needed, then registereing should no be required) - if(!strcmp(from->name(), to->name())) - return std::vector(); - - return castSequence(getTypeDescriptor(from), getTypeDescriptor(to)); -} - -CTypeList::TypeInfoPtr CTypeList::getTypeDescriptor(const std::type_info *type, bool throws) const -{ - auto i = typeInfos.find(type); - if(i != typeInfos.end()) - return i->second; //type found, return ptr to structure - - if(!throws) - return nullptr; - - THROW_FORMAT("Cannot find type descriptor for type %s. Was it registered?", type->name()); -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CTypeList.h b/lib/serializer/CTypeList.h index fc5b7c0ac..df0eec7bc 100644 --- a/lib/serializer/CTypeList.h +++ b/lib/serializer/CTypeList.h @@ -9,199 +9,72 @@ */ #pragma once -#include "CSerializer.h" - VCMI_LIB_NAMESPACE_BEGIN -struct IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const = 0; // takes From*, returns To* - virtual std::any castSharedPtr(const std::any &ptr) const = 0; // takes std::shared_ptr, performs dynamic cast, returns std::shared_ptr - virtual std::any castWeakPtr(const std::any &ptr) const = 0; // takes std::weak_ptr, performs dynamic cast, returns std::weak_ptr. The object under poitner must live. - //virtual std::any castUniquePtr(const std::any &ptr) const = 0; // takes std::unique_ptr, performs dynamic cast, returns std::unique_ptr - virtual ~IPointerCaster() = default; -}; - -template -struct PointerCaster : IPointerCaster -{ - virtual std::any castRawPtr(const std::any &ptr) const override // takes void* pointing to From object, performs dynamic cast, returns void* pointing to To object - { - From * from = (From*)std::any_cast(ptr); - To * ret = static_cast(from); - return (void*)ret; - } - - // Helper function performing casts between smart pointers - template - std::any castSmartPtr(const std::any &ptr) const - { - try - { - auto from = std::any_cast(ptr); - auto ret = std::static_pointer_cast(from); - return ret; - } - catch(std::exception &e) - { - THROW_FORMAT("Failed cast %s -> %s. Given argument was %s. Error message: %s", typeid(From).name() % typeid(To).name() % ptr.type().name() % e.what()); - } - } - - virtual std::any castSharedPtr(const std::any &ptr) const override - { - return castSmartPtr>(ptr); - } - virtual std::any castWeakPtr(const std::any &ptr) const override - { - auto from = std::any_cast>(ptr); - return castSmartPtr>(from.lock()); - } -}; - /// Class that implements basic reflection-like mechanisms /// For every type registered via registerType() generates inheritance tree /// Rarely used directly - usually used as part of CApplier -class DLL_LINKAGE CTypeList: public boost::noncopyable +class CTypeList { -//public: - struct TypeDescriptor; - using TypeInfoPtr = std::shared_ptr; - using WeakTypeInfoPtr = std::weak_ptr; - struct TypeDescriptor - { - ui16 typeID; - const char *name; - std::vector children, parents; - }; - using TMutex = boost::shared_mutex; - using TUniqueLock = boost::unique_lock; - using TSharedLock = boost::shared_lock; + std::map typeInfos; -private: - mutable TMutex mx; - - std::map typeInfos; - std::map, std::unique_ptr> casters; //for each pair we provide a caster (each registered relations creates a single entry here) - - /// Returns sequence of types starting from "from" and ending on "to". Every next type is derived from the previous. - /// Throws if there is no link registered. - std::vector castSequence(TypeInfoPtr from, TypeInfoPtr to) const; - std::vector castSequence(const std::type_info *from, const std::type_info *to) const; - - template - std::any castHelper(std::any inputPtr, const std::type_info *fromArg, const std::type_info *toArg) const - { - TSharedLock lock(mx); - auto typesSequence = castSequence(fromArg, toArg); - - std::any ptr = inputPtr; - for(int i = 0; i < static_cast(typesSequence.size()) - 1; i++) - { - auto &from = typesSequence[i]; - auto &to = typesSequence[i + 1]; - auto castingPair = std::make_pair(from, to); - if(!casters.count(castingPair)) - THROW_FORMAT("Cannot find caster for conversion %s -> %s which is needed to cast %s -> %s", from->name % to->name % fromArg->name() % toArg->name()); - - const auto & caster = casters.at(castingPair); - ptr = (*caster.*CastingFunction)(ptr); //Why does unique_ptr not have operator->* ..? - } - - return ptr; - } - CTypeList & operator=(CTypeList &) = delete; - - TypeInfoPtr getTypeDescriptor(const std::type_info *type, bool throws = true) const; //if not throws, failure returns nullptr - TypeInfoPtr registerType(const std::type_info *type); - -public: - - CTypeList(); - - template - void registerType(const Base * b = nullptr, const Derived * d = nullptr) - { - TUniqueLock lock(mx); - static_assert(std::is_base_of::value, "First registerType template parameter needs to ba a base class of the second one."); - static_assert(std::has_virtual_destructor::value, "Base class needs to have a virtual destructor."); - static_assert(!std::is_same::value, "Parameters of registerTypes should be two different types."); - auto bt = getTypeInfo(b); - auto dt = getTypeInfo(d); //obtain std::type_info - auto bti = registerType(bt); - auto dti = registerType(dt); //obtain our TypeDescriptor - - // register the relation between classes - bti->children.push_back(dti); - dti->parents.push_back(bti); - casters[std::make_pair(bti, dti)] = std::make_unique>(); - casters[std::make_pair(dti, bti)] = std::make_unique>(); - } - - ui16 getTypeID(const std::type_info *type, bool throws = false) const; + DLL_LINKAGE CTypeList(); template - ui16 getTypeID(const T * t = nullptr, bool throws = false) const - { - return getTypeID(getTypeInfo(t), throws); - } - - TypeInfoPtr getTypeDescriptor(ui16 typeID) const; - - template - void * castToMostDerived(const TInput * inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr); - - if (strcmp(baseType.name(), derivedType->name()) == 0) - { - return const_cast(reinterpret_cast(inputPtr)); - } - - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>( - const_cast(reinterpret_cast(inputPtr)), &baseType, - derivedType)); - } - - template - std::any castSharedToMostDerived(const std::shared_ptr inputPtr) const - { - const auto & baseType = typeid(typename std::remove_cv::type); - auto derivedType = getTypeInfo(inputPtr.get()); - - if (!strcmp(baseType.name(), derivedType->name())) - return inputPtr; - - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, &baseType, derivedType); - } - - void * castRaw(void *inputPtr, const std::type_info *from, const std::type_info *to) const - { - return std::any_cast(castHelper<&IPointerCaster::castRawPtr>(inputPtr, from, to)); - } - std::any castShared(std::any inputPtr, const std::type_info *from, const std::type_info *to) const - { - return castHelper<&IPointerCaster::castSharedPtr>(inputPtr, from, to); - } - - template const std::type_info * getTypeInfo(const T * t = nullptr) const + const std::type_info & getTypeInfo(const T * t = nullptr) const { if(t) - return &typeid(*t); + return typeid(*t); else - return &typeid(T); + return typeid(T); + } + +public: + static CTypeList & getInstance() + { + static CTypeList registry; + return registry; + } + + template + void registerType() + { + registerType(); + registerType(); + } + + template + void registerType() + { + const std::type_info & typeInfo = typeid(T); + + if (typeInfos.count(typeInfo.name()) != 0) + return; + + typeInfos[typeInfo.name()] = typeInfos.size() + 1; + } + + template + uint16_t getTypeID(T * typePtr) + { + static_assert(!std::is_pointer_v, "CTypeList does not supports pointers!"); + static_assert(!std::is_reference_v, "CTypeList does not supports references!"); + + const std::type_info & typeInfo = getTypeInfo(typePtr); + + if (typeInfos.count(typeInfo.name()) == 0) + return 0; + + return typeInfos.at(typeInfo.name()); } }; -extern DLL_LINKAGE CTypeList typeList; - /// Wrapper over CTypeList. Allows execution of templated class T for any type /// that was resgistered for this applier template class CApplier : boost::noncopyable { - std::map> apps; + std::map> apps; template void addApplier(ui16 ID) @@ -224,9 +97,8 @@ public: template void registerType(const Base * b = nullptr, const Derived * d = nullptr) { - typeList.registerType(b, d); - addApplier(typeList.getTypeID(b)); - addApplier(typeList.getTypeID(d)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); + addApplier(CTypeList::getInstance().getTypeID(nullptr)); } }; diff --git a/lib/serializer/Cast.h b/lib/serializer/Cast.h index ebc7957af..7525f6d85 100644 --- a/lib/serializer/Cast.h +++ b/lib/serializer/Cast.h @@ -9,58 +9,18 @@ */ #pragma once -#include -#include -#include "CTypeList.h" - VCMI_LIB_NAMESPACE_BEGIN template inline const T * dynamic_ptr_cast(const F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } template inline T * dynamic_ptr_cast(F * ptr) { -#ifndef VCMI_APPLE return dynamic_cast(ptr); -#else - if(!strcmp(typeid(*ptr).name(), typeid(T).name())) - { - return static_cast(ptr); - } - try - { - auto * sourceTypeInfo = typeList.getTypeInfo(ptr); - auto * targetTypeInfo = &typeid(typename std::remove_const::type>::type); - typeList.castRaw((void *)ptr, sourceTypeInfo, targetTypeInfo); - } - catch(...) - { - return nullptr; - } - return static_cast(ptr); -#endif } VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 3c64115d8..5bf054899 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -463,12 +463,12 @@ void CGameHandler::handleReceivedPack(CPackForServer * pack) PackageApplied applied; applied.player = pack->player; applied.result = succesfullyApplied; - applied.packType = typeList.getTypeID(pack); + applied.packType = CTypeList::getInstance().getTypeID(pack); applied.requestID = pack->requestID; pack->c->sendPack(&applied); }; - CBaseForGHApply * apply = applier->getApplier(typeList.getTypeID(pack)); //and appropriate applier object + CBaseForGHApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack)); //and appropriate applier object if(isBlockedByQueries(pack, pack->player)) { sendPackageResponse(false); diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 49e303be1..0eba3ed90 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -485,7 +485,7 @@ void CVCMIServer::threadHandleClient(std::shared_ptr c) void CVCMIServer::handleReceivedPack(std::unique_ptr pack) { - CBaseForServerApply * apply = applier->getApplier(typeList.getTypeID(pack.get())); + CBaseForServerApply * apply = applier->getApplier(CTypeList::getInstance().getTypeID(pack.get())); if(apply->applyOnServerBefore(this, pack.get())) addToAnnounceQueue(std::move(pack)); } @@ -502,7 +502,7 @@ void CVCMIServer::announcePack(std::unique_ptr pack) c->sendPack(pack.get()); } - applier->getApplier(typeList.getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); + applier->getApplier(CTypeList::getInstance().getTypeID(pack.get()))->applyOnServerAfter(this, pack.get()); } void CVCMIServer::announceMessage(const std::string & txt) From 864462b84ab94f510774d001f3ba8b7eeaa02769 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 00:01:48 +0200 Subject: [PATCH 1086/1248] Reduce excessive updates of garrisons. Fixes selection reset in simturns --- client/CPlayerInterface.cpp | 32 +++++++++++---------------- client/CPlayerInterface.h | 2 +- client/gui/CIntObject.h | 13 +++++++++++ client/windows/CAltarWindow.cpp | 7 +++++- client/windows/CAltarWindow.h | 5 +++-- client/windows/CCastleInterface.cpp | 5 +++++ client/windows/CCastleInterface.h | 3 ++- client/windows/CHeroWindow.cpp | 5 +++++ client/windows/CHeroWindow.h | 1 + client/windows/CKingdomInterface.cpp | 33 ++++++++++++++++++++++++++++ client/windows/CKingdomInterface.h | 5 +++++ client/windows/GUIClasses.cpp | 20 +++++++++++++++++ client/windows/GUIClasses.h | 4 ++++ 13 files changed, 111 insertions(+), 24 deletions(-) diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index e604e070f..298ccd646 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -499,8 +499,9 @@ void CPlayerInterface::heroInGarrisonChange(const CGTownInstance *town) adventureInt->onHeroChanged(nullptr); adventureInt->onTownChanged(town); - for (auto gh : GH.windows().findWindows()) - gh->updateGarrisons(); + for (auto cgh : GH.windows().findWindows()) + if (cgh->holdsGarrison(town)) + cgh->updateGarrisons(); for (auto ki : GH.windows().findWindows()) ki->townChanged(town); @@ -521,49 +522,42 @@ void CPlayerInterface::heroVisitsTown(const CGHeroInstance* hero, const CGTownIn void CPlayerInterface::garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) { - std::vector instances; + std::vector instances; - if(auto obj = cb->getObj(id1)) + if(auto obj = dynamic_cast(cb->getObjInstance(id1))) instances.push_back(obj); if(id2 != ObjectInstanceID() && id2 != id1) { - if(auto obj = cb->getObj(id2)) + if(auto obj = dynamic_cast(cb->getObjInstance(id2))) instances.push_back(obj); } garrisonsChanged(instances); } -void CPlayerInterface::garrisonsChanged(std::vector objs) +void CPlayerInterface::garrisonsChanged(std::vector objs) { for (auto object : objs) { auto * hero = dynamic_cast(object); auto * town = dynamic_cast(object); + if (town) + adventureInt->onTownChanged(town); + if (hero) { adventureInt->onHeroChanged(hero); - - if(hero->inTownGarrison) - { + if(hero->inTownGarrison && hero->visitedTown != town) adventureInt->onTownChanged(hero->visitedTown); - } } - if (town) - adventureInt->onTownChanged(town); } for (auto cgh : GH.windows().findWindows()) - cgh->updateGarrisons(); - - for (auto cmw : GH.windows().findWindows()) - { - if(vstd::contains(objs, cmw->getHero())) - cmw->updateGarrison(); - } + if (cgh->holdsGarrisons(objs)) + cgh->updateGarrisons(); GH.windows().totalRedraw(); } diff --git a/client/CPlayerInterface.h b/client/CPlayerInterface.h index 41245ee5f..3d9c17cf2 100644 --- a/client/CPlayerInterface.h +++ b/client/CPlayerInterface.h @@ -223,7 +223,7 @@ private: }; void heroKilled(const CGHeroInstance* hero); - void garrisonsChanged(std::vector objs); + void garrisonsChanged(std::vector objs); void requestReturningToMainMenu(bool won); void acceptTurn(QueryID queryID); //used during hot seat after your turn message is close void initializeHeroTownList(); diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 150e55b8a..723cfcc4e 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -19,6 +19,10 @@ class CGuiHandler; class CPicture; class Canvas; +VCMI_LIB_NAMESPACE_BEGIN +class CArmedInstance; +VCMI_LIB_NAMESPACE_BEGIN + class IUpdateable { public: @@ -150,6 +154,15 @@ protected: class IGarrisonHolder { public: + bool holdsGarrisons(std::vector armies) + { + for (auto const * army : armies) + if (holdsGarrison(army)) + return true; + return false; + } + + virtual bool holdsGarrison(const CArmedInstance * army) = 0; virtual void updateGarrisons() = 0; }; diff --git a/client/windows/CAltarWindow.cpp b/client/windows/CAltarWindow.cpp index 249abcde2..a6a312b22 100644 --- a/client/windows/CAltarWindow.cpp +++ b/client/windows/CAltarWindow.cpp @@ -45,12 +45,17 @@ void CAltarWindow::updateExpToLevel() altar->expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(altar->hero->exp) + 1) - altar->hero->exp)); } -void CAltarWindow::updateGarrison() +void CAltarWindow::updateGarrisons() { if(auto altarCreatures = std::static_pointer_cast(altar)) altarCreatures->updateGarrison(); } +bool CAltarWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + const CGHeroInstance * CAltarWindow::getHero() const { return hero; diff --git a/client/windows/CAltarWindow.h b/client/windows/CAltarWindow.h index 9d1ffe2a5..83ddea490 100644 --- a/client/windows/CAltarWindow.h +++ b/client/windows/CAltarWindow.h @@ -13,12 +13,13 @@ #include "../widgets/CWindowWithArtifacts.h" #include "CWindowObject.h" -class CAltarWindow : public CWindowObject, public CWindowWithArtifacts +class CAltarWindow : public CWindowObject, public CWindowWithArtifacts, public IGarrisonHolder { public: CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function & onWindowClosed, EMarketMode mode); void updateExpToLevel(); - void updateGarrison(); + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; const CGHeroInstance * getHero() const; void close() override; diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 5bce62993..9757a8fb3 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1262,6 +1262,11 @@ void CCastleInterface::updateGarrisons() redraw(); } +bool CCastleInterface::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + void CCastleInterface::close() { if(town->tempOwner == LOCPLINT->playerID) //we may have opened window for an allied town diff --git a/client/windows/CCastleInterface.h b/client/windows/CCastleInterface.h index 2cc86700c..1b7884737 100644 --- a/client/windows/CCastleInterface.h +++ b/client/windows/CCastleInterface.h @@ -249,7 +249,8 @@ public: CCastleInterface(const CGTownInstance * Town, const CGTownInstance * from = nullptr); ~CCastleInterface(); - virtual void updateGarrisons() override; + void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void castleTeleport(int where); void townChange(); diff --git a/client/windows/CHeroWindow.cpp b/client/windows/CHeroWindow.cpp index 3ff4fc950..9d5d66636 100644 --- a/client/windows/CHeroWindow.cpp +++ b/client/windows/CHeroWindow.cpp @@ -353,3 +353,8 @@ void CHeroWindow::updateGarrisons() garr->recreateSlots(); morale->set(curHero); } + +bool CHeroWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == curHero; +} diff --git a/client/windows/CHeroWindow.h b/client/windows/CHeroWindow.h index 8f5bb2776..94e91ad33 100644 --- a/client/windows/CHeroWindow.h +++ b/client/windows/CHeroWindow.h @@ -107,6 +107,7 @@ public: void commanderWindow(); void switchHero(); //changes displayed hero void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void createBackpackWindow(); //friends diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 4be0b5569..9410dc5aa 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -662,6 +662,11 @@ void CKingdomInterface::updateGarrisons() garrison->updateGarrisons(); } +bool CKingdomInterface::holdsGarrison(const CArmedInstance * army) +{ + return army->getOwner() == LOCPLINT->playerID; +} + void CKingdomInterface::artifactAssembled(const ArtifactLocation& artLoc) { if(auto arts = std::dynamic_pointer_cast(tabArea->getItem())) @@ -709,6 +714,15 @@ void CKingdHeroList::updateGarrisons() } } +bool CKingdHeroList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : heroes->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdHeroList::createHeroItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -761,6 +775,15 @@ void CKingdTownList::updateGarrisons() } } +bool CKingdTownList::holdsGarrison(const CArmedInstance * army) +{ + for(std::shared_ptr object : towns->getItems()) + if(IGarrisonHolder * garrison = dynamic_cast(object.get())) + if (garrison->holdsGarrison(army)) + return true; + return false; +} + std::shared_ptr CKingdTownList::createTownItem(size_t index) { ui32 picCount = 4; // OVSLOT contains 4 images @@ -836,6 +859,11 @@ void CTownItem::updateGarrisons() garr->recreateSlots(); } +bool CTownItem::holdsGarrison(const CArmedInstance * army) +{ + return army == town || army == town->getUpperArmy() || army == town->visitingHero; +} + void CTownItem::update() { std::string incomeVal = std::to_string(town->dailyIncome()[EGameResID::GOLD]); @@ -998,6 +1026,11 @@ void CHeroItem::updateGarrisons() garr->recreateSlots(); } +bool CHeroItem::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + std::shared_ptr CHeroItem::onTabSelected(size_t index) { return artTabs.at(index); diff --git a/client/windows/CKingdomInterface.h b/client/windows/CKingdomInterface.h index 2b951d2dd..26dd395a0 100644 --- a/client/windows/CKingdomInterface.h +++ b/client/windows/CKingdomInterface.h @@ -253,6 +253,7 @@ public: void townChanged(const CGTownInstance *town); void heroRemoved(); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void artifactRemoved(const ArtifactLocation &artLoc) override; void artifactMoved(const ArtifactLocation &artLoc, const ArtifactLocation &destLoc, bool withRedraw) override; void artifactDisassembled(const ArtifactLocation &artLoc) override; @@ -290,6 +291,7 @@ public: CTownItem(const CGTownInstance * Town); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void update(); }; @@ -322,6 +324,7 @@ public: std::shared_ptr heroArts; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; CHeroItem(const CGHeroInstance * hero); }; @@ -340,6 +343,7 @@ public: CKingdHeroList(size_t maxSize); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; /// Tab with all town-specific data @@ -358,4 +362,5 @@ public: void townChanged(const CGTownInstance * town); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 9e5fa50d8..63f92507f 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -861,6 +861,11 @@ void CExchangeWindow::updateGarrisons() updateWidgets(); } +bool CExchangeWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + void CExchangeWindow::questlog(int whichHero) { CCS->curh->dragAndDropCursor(nullptr); @@ -1008,6 +1013,11 @@ void CTransformerWindow::updateGarrisons() item->update(); } +bool CTransformerWindow::holdsGarrison(const CArmedInstance * army) +{ + return army == hero; +} + CTransformerWindow::CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("SKTRNBK")), hero(_hero), @@ -1251,6 +1261,11 @@ void CGarrisonWindow::updateGarrisons() garr->recreateSlots(); } +bool CGarrisonWindow::holdsGarrison(const CArmedInstance * army) +{ + return garr->upperArmy() == army || garr->lowerArmy() == army; +} + CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object) : CStatusbarWindow(PLAYER_COLORED, ImagePath::builtin("APHLFTBK")), fort(object), @@ -1292,6 +1307,11 @@ CHillFortWindow::CHillFortWindow(const CGHeroInstance * visitor, const CGObjectI updateGarrisons(); } +bool CHillFortWindow::holdsGarrison(const CArmedInstance * army) +{ + return hero == army; +} + void CHillFortWindow::updateGarrisons() { std::array costs;// costs [slot ID] [resource ID] = resource count for upgrade diff --git a/client/windows/GUIClasses.h b/client/windows/GUIClasses.h index 98413fa4a..a58001e5c 100644 --- a/client/windows/GUIClasses.h +++ b/client/windows/GUIClasses.h @@ -290,6 +290,7 @@ public: std::array, 2> artifs; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right @@ -362,6 +363,7 @@ public: void addAll(); void close() override; void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; CTransformerWindow(const IMarket * _market, const CGHeroInstance * _hero, const std::function & onWindowClosed); }; @@ -443,6 +445,7 @@ public: CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits); void updateGarrisons() override; + bool holdsGarrison(const CArmedInstance * army) override; }; /// Hill fort is the building where you can upgrade units @@ -482,6 +485,7 @@ private: public: CHillFortWindow(const CGHeroInstance * visitor, const CGObjectInstance * object); void updateGarrisons() override;//update buttons after garrison changes + bool holdsGarrison(const CArmedInstance * army) override; }; class CThievesGuildWindow : public CStatusbarWindow From 18c0217679b2cf598e09ca822e297061bee69380 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 00:08:13 +0200 Subject: [PATCH 1087/1248] Relaxed ordering requirements - player can start turn even if players before him are waiting to act. E.g. green can start turn even if blue and red are in contact and blue is yet to start his turn --- server/processors/TurnOrderProcessor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 242055dc0..87a63cb92 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -163,7 +163,7 @@ bool TurnOrderProcessor::mustActBefore(PlayerColor left, PlayerColor right) cons if (!leftInfo->isHuman() && rightInfo->isHuman()) return false; - return left < right; + return false; } bool TurnOrderProcessor::canStartTurn(PlayerColor which) const From 359af84d5900736ddea888ee51e0f66d9add0fc2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 00:33:34 +0200 Subject: [PATCH 1088/1248] Fix crash on starting battle in MP --- client/Client.cpp | 3 +++ client/gui/CIntObject.h | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/Client.cpp b/client/Client.cpp index c6e99d405..6bf4de6cd 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -641,6 +641,9 @@ void CClient::battleFinished(const BattleID & battleID) void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor color) { + if (battleints.count(color) == 0) + return; // not our combat in MP + auto battleint = battleints.at(color); if (!battleint->human) diff --git a/client/gui/CIntObject.h b/client/gui/CIntObject.h index 723cfcc4e..7909e72e7 100644 --- a/client/gui/CIntObject.h +++ b/client/gui/CIntObject.h @@ -21,7 +21,7 @@ class Canvas; VCMI_LIB_NAMESPACE_BEGIN class CArmedInstance; -VCMI_LIB_NAMESPACE_BEGIN +VCMI_LIB_NAMESPACE_END class IUpdateable { From e1a9763ae40c58e8391c2e0ef02d58f8d89b1195 Mon Sep 17 00:00:00 2001 From: SoundSSGood <87084363+SoundSSGood@users.noreply.github.com> Date: Thu, 9 Nov 2023 12:38:50 +0200 Subject: [PATCH 1089/1248] fix test, throwing exception fix --- server/CGameHandler.cpp | 13 +++---------- test/mock/mock_IGameCallback.h | 3 +-- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 59c4ee89c..ac81ed403 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -2720,16 +2720,9 @@ bool CGameHandler::moveArtifact(const ArtifactLocation & src, const ArtifactLoca ma.swap = true; } - try - { - auto hero = getHero(dst.artHolder); - if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) - giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); - } - catch(const std::bad_variant_access &) - { - // object other than hero received an art - ignore - } + auto hero = getHero(dst.artHolder); + if(ArtifactUtils::checkSpellbookIsNeeded(hero, srcArtifact->artType->getId(), dstSlot)) + giveHeroNewArtifact(hero, VLC->arth->objects[ArtifactID::SPELLBOOK], ArtifactPosition::SPELLBOOK); ma.artsPack0.push_back(BulkMoveArtifacts::LinkedSlots(src.slot, dstSlot)); if(src.artHolder != dst.artHolder) diff --git a/test/mock/mock_IGameCallback.h b/test/mock/mock_IGameCallback.h index a772469fe..48a2fb639 100644 --- a/test/mock/mock_IGameCallback.h +++ b/test/mock/mock_IGameCallback.h @@ -67,8 +67,7 @@ public: void removeAfterVisit(const CGObjectInstance *object) override {} //object will be destroyed when interaction is over. Do not call when interaction is not ongoing! bool giveHeroNewArtifact(const CGHeroInstance * h, const CArtifact * artType, ArtifactPosition pos) override {return false;} - bool giveHeroArtifact(const CGHeroInstance * h, const CArtifactInstance * a, ArtifactPosition pos) override {return false;} - void putArtifact(const ArtifactLocation &al, const CArtifactInstance *a) override {} + bool putArtifact(const ArtifactLocation & al, const CArtifactInstance * art, std::optional askAssemble) override {return false;} void removeArtifact(const ArtifactLocation &al) override {} bool moveArtifact(const ArtifactLocation &al1, const ArtifactLocation &al2) override {return false;} From f12aac8c871fe1c32d57675cdf4cc3d3fe49d153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 9 Nov 2023 13:33:10 +0100 Subject: [PATCH 1090/1248] Removed old Manual.md. Moved content to Game_Mechanics.md. --- docs/players/Game_Mechanics.md | 105 +++++++++++++++++++-- docs/players/Manual.md | 168 --------------------------------- 2 files changed, 96 insertions(+), 177 deletions(-) delete mode 100644 docs/players/Manual.md diff --git a/docs/players/Game_Mechanics.md b/docs/players/Game_Mechanics.md index cb16df499..556caae7e 100644 --- a/docs/players/Game_Mechanics.md +++ b/docs/players/Game_Mechanics.md @@ -2,23 +2,58 @@ # List of features added in VCMI +## High resolutions + +VCMI supports resolutions higher than original game, which ran only in 800 x 600. It also allows a number of additional features: + +- High-resolution screens of any ascpect ratio are supported. +- In-game GUI can be freely scaled +- Adventure map can be freely zoomed + +Assets from Heroes of Might & Magic III HD - Remake released by Ubisoft in 2015 - are **not** supported. + +## Extended engine limits + Some of game features have already been extended in comparison to Shadow of Death: - Support for 32-bit graphics with alpha channel. Supported formats are .def, .bmp, .png and .tga - Support for maps of any size, including rectangular shapes - No limit of number of map objects, such as dwellings and stat boosters - Hero experience capacity currently at 2^64, which equals 199 levels with typical progression -- Heroes can have primary stats up to 2^16 -- Unlimited backpack -- Support for Stack Experience +- Heroes can have primary stats up to 2^16. +- Unlimited backpack (by default). This can be toggled off to restore original 64-slot backpack limit. The list of implemented cheat codes and console commands is [here](Cheat_codes.md). +# New mechanics (Optional) + +## Stack Experience module + +VCMI natively suppoorts stack experience feature known from WoG. Any creature - old or modded - can get stack experience bonuses. However, this feature needs to be enabled as a part of WoG VCMI submod. + +Stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: + +- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). +- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. +- [Stack Artifact](#stack-artifact-module). You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. + +## Commanders module + +VCMI offers native support for Commanders. Commanders are part of WoG mod for VCMI and require it to be enabled. However, once this is done, any new faction can use its own Commander, too. + +## Mithril module + +VCMI natively supports Mithril resource known from WoG. However, it is not currently used by any mod. + +## Stack Artifact module + +In original WoG, there is one available Stack Artifact - Warlord's Banner, which is related directly to stack experience. VCMI natively supports any number of Stack Artifacts regardless if of Stack Experience module is enabled or not. However, currently no mods make use of this feature and it hasn't been tested for many years. + # List of bugs fixed in VCMI These bugs were present in original Shadow of Death game, however the team decided to fix them to bring back desired behaviour: -# List of game mechanics changes +## List of game mechanics changes Some of H3 mechanics can't be straight considered as bug, but default VCMI behaviour is different: @@ -27,12 +62,48 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - Battles. Spells from artifacts like AOTD are autocasted on beginning of the battle, not beginning of turn. - Spells. Dimension Door spell doesn't allow to teleport to unexplored tiles. -# List of extended game functionality +# List of extended GUI features ## Adventure map + +### New Shortcuts + +- [LCtrl] + [R] - Quick restart of current scenario. +- [LCtrl] + Arrows - scrolls Adventure Map behind an open window. +- [LCtrl] pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). +- NumPad 5 - centers view on selected hero. +- NumPad Enter functions same as normal Enter in the game (it didn't in H3). - [LCtrl] + LClick – perform a normal click (same as no hero is selected). This make it possible to select other hero instead of changing path of current hero. -## Quick Army Management +## Pathfinder + +VCMI introduces improved pathfinder, which may find the way on adventure map using ships,Subterranean Gates and Monoliths. Simply click your destination anywhere on adventure map and it will find shortest path, if if target position is reachable. + +### Quest log + +VCMI itroduces custom Quest Log window. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. + +### Power rating + +When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to curently selected hero. This feature has been originally introduced in Heroes of Might and Magic V. + +### Minor GUI features + +Some windows and dialogs now display extra info and images to make game more accessible for new players. This can be turned off, if desired. + +## Battles + +### Stack Queue + +Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press [Q] during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. + +### Attack range + +In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. + +## Town Screen + +### Quick Army Management - [LShift] + LClick – splits a half units from the selected stack into an empty slot. - [LCtrl] + LClick – splits a single unit from the selected stack into an empty slot. @@ -40,8 +111,9 @@ Some of H3 mechanics can't be straight considered as bug, but default VCMI behav - [Alt] + LClick – merge all splitted single units into one stack - [Alt] + [LCtrl] + LClick - move all units of selected stack to the city's garrison or to the met hero - [Alt] + [LShift] + LClick - dismiss selected stack` +- Directly type numbers in the Split Stack window to split them in any way you wish -## Interface Shourtcuts +### Interface Shortcuts It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitment and Marketplace (click on gold) from various places: @@ -49,11 +121,26 @@ It's now possible to open Tavern (click on town icon), Townhall, Quick Recruitme - Kingdom overview for each town - Infobox (only if info box army management is enabled) -## Quick Recruitment +### Quick Recruitment Mouse click on castle icon in the town screen open quick recruitment window, where we can purhase in fast way units. -## Color support +## Pregame - Scenario / Saved Game list + +- Mouse wheel - scroll through the Scenario list. +- [Home] - move to the top of the list. +- [End] - move to the bottom of the list. +- NumPad keys can be used in the Save Game screen (they didn't work in H3). + +## Fullscreen + +- [F4] - Toggle fullscreen mode on/off. + +## FPS counter + +It's the new feature meant for testing game performance on various platforms. + +## Color support in game text Additional color are supported for text fields (e.g. map description). Uses HTML color syntax (e.g. #abcdef) / HTML predefined colors (e.g. green). diff --git a/docs/players/Manual.md b/docs/players/Manual.md deleted file mode 100644 index fb7c3d682..000000000 --- a/docs/players/Manual.md +++ /dev/null @@ -1,168 +0,0 @@ -< [Documentation](../Readme.md) / Manual - -# Introduction - -The purpose of VCMI project is to rewrite entire HoMM3: WoG engine from scratch, giving it new and extended possibilities. We are hoping to support mods and new towns already made by fans, but abandoned because of game code limitations. -VCMI is a fan-made open-source project in progress. We already allow support for maps of any sizes, higher resolutions and extended engine limits. However, although working, the game is not finished. There are still many features and functionalities to add, both old and brand new. - -# Installation - -VCMI requires Heroes of Might & Magic 3 complete installation and will not run properly without their files. - -## Windows - -To install VCMI, simply unzip downloaded archive to main HoMM3 directory. To launch it, click `VCMI_client` icon. Server mode is inactive yet. - -## Linux - -Visit [Installation on Linux](Installation_Linux.md) for Linux packages and installation guidelines. - -# New features - -A number of enchancements had been introduced thorough new versions of VCMI. In this section you can learn about all of them. - -## High resolutions - -VCMI supports resolutions higher than original 800x600. -Switching resolution may not only change visible area of map, but also alters some interface features such as [Stack Queue.](#stack-queue) -To change resolution or full screen mode use System Options menu when in game. Fullscreen mode can be toggled anytime using F4 hotkey. - -## Stack Experience - -In 0.85, new stack experience interface has been merged with regular creature window. Among old functionalities, it includes new useful info: - -- Click experience icon to see detailed info about creature rank and experience needed for next level. This window works only if stack experience module is enabled (true by default). -- Stack Artifact. As yet creature artifacts are not handled, so this place is unused. You can choose enabled artifact with arrow buttons. There is also additional button below to pass currently selected artifact back to hero. -- Abilities description contain information about actual values and types of bonuses received by creature - be it default ability, stack experience, artifact or other effect. These descriptions use custom text files which have not been translated. - -## Commanders - -VCMI offers native support for Commanders. By default, they resemble original WoG behaviour with basic "Commanders" script enabled. - -## Stack Queue - -Stack queue is a feature coming straight from HoMM5, which allows you to see order of stacks on the battlefield, sorted from left to right. To toggle in on/off, press 'Q' during the battle. There is smaller and bigger version of it, the second one is available only in higher resolutions. - -## Pathfinder - -VCMI introduces improved pathfinder, which may find the way on adventure map using ships and subterranean gates. Simply click your destination on another island or level and the proposed path will be displayed. - -## Quest log - -In 0.9 new quest log was introduced. It can display info about Seer Hut or Quest Guard mission, but also handle Borderguard and Border Gate missions. When you choose a quest from the list on the left, it's description is shown. Additionally, on inner minimap you can see small icons indicating locations of quest object. Clicking these objects immediately centers adventure map on desired location. - -## Attack range - -In combat, some creatures, such as Dragon or Cerberi, may attack enemies on multiple hexes. All such attacked stacks will be highlighted if the attack cursor is hovered over correct destination tile. Whenever battle stack is hovered, its movement range is highlighted in darker shade. This can help when you try to avoid attacks of melee units. - -## Power rating - -When hovering cursor over neutral stack on adventure map, you may notice additional info about relative threat this stack poses to selected hero. This feature has been introduced in Heroes of Might and Magic V and is planned to be extended to all kinds of armed objects. - -## FPS counter - -VCMI 0.85 introduces new feature for testing, the FPS counter. - -## Minor improvements - -## New controls - -VCMI introduces several minor improvements and new keybinds in user -interface. - -### Pregame - Scenario / Saved Game list - -- Mouse wheel - scroll through the Scenario list. -- Home - move to the top of the list. -- End - move to the bottom of the list. -- NumPad keys can be used in the Save Game screen (they didn't work in H3). - -### Adventure Map - -- CTRL + R - Quick restart of current scenario. -- CTRL + Arrows - scrolls Adventure Map behind an open window. -- CTRL pressed blocks Adventure Map scrolling (it allows us to leave the application window without losing current focus). -- NumPad 5 - centers view on selected hero. -- NumPad Enter functions same as normal Enter in the game (it didn't in H3). - -### Miscellaneous - -- Numbers for components in selection window - for example Treasure Chest, skill choice dialog and more yet to come. -- Type numbers in the Split Stack screen (for example 25 will split the stacks as such that there are 25 creatures in the second stack). -- 'Q' - Toggles the [Stack Queue](#stack-queue) display (so it can be enabled/disabled with single key press). -- During Tactics phase, click on any of your stack to instantly activate it. No need to scroll trough entire army anymore. - -## Cheat codes - -Following cheat codes have been implemented in VCMI. Type them in console: - -- `vcmiistari` - Gives all spells and 999 mana to currently selected hero -- `vcmiainur` - Gives 5 Archangels to every empty slot of currently selected hero -- `vcmiangband` - Gives 10 Black Knights into each slot -- `vcmiarmenelos` - Build all structures in currently selected town -- `vcminoldor` - All war machines -- `vcminahar` - 1000000 movement points -- `vcmiformenos` - Give resources (100 wood, ore and rare resources and 20000 gold) -- `vcmieagles` - Reveals fog of war -- `vcmiglorfindel` - Advances currently selected hero to the next level -- `vcmisilmaril` - Player wins -- `vcmimelkor` - Player loses -- `vcmiforgeofnoldorking` - Hero gets all artifacts except spell book, spell scrolls and war machines - -# Feedback - -Our project is open and its sources are available for everyone to browse and download. We do our best to inform community of Heroes fans with all the details and development progress. We also look forward to your comments, support and advice.\ -A good place to start is [VCMI Documentation](../Readme.md) which contains all necessary information for developers, testers and the people who would like to get familiar with our project. If you want to report a bug, use [Mantis Bugtracker](http://bugs.vcmi.eu/bug_report_advanced_page.php). -Make sure the issue is not already mentioned on [the list](http://bugs.vcmi.eu/view_all_bug_page.php) unless you can provide additional details for it. Please do not report as bugs features not yet implemented. For proposing new ideas and requests, visit [our board](http://forum.vcmi.eu/index.php). -VCMI comes with its own bug handlers: the console which prints game log `(server_log, VCMI_Client_log, VCMI_Server_log)` and memory dump file (`.dmp`) created on crash on Windows systems. These may be very helpful when the nature of bug is not obvious, please attach them if necessary. -To resolve an issue, we must be able to reproduce it on our computers. Please put down all circumstances in which a bug occurred and what did you do before, especially if it happens rarely or is not clearly visible. The better report, the better chance to track the bug quickly. - -# FAQ - -### What does "VCMI" stand for? - -VCMI is an acronym of the [Quenya](https://en.wikipedia.org/wiki/Quenya) phrase "Vinyar Callor Meletya Ingole", meaning "New Heroes of Might and Magic". ([Source](https://forum.vcmi.eu/t/what-vcmi-stands-for/297/4)) - -## How to turn off creature queue panel in battles? - -Hotkey to switch this panel is "Q" - -### Is it possible to add town X to vcmi? - -This depends on town authors or anyone else willing to port it to vcmi. Aim of VCMI is to provide *support* for such features. - -## When will the final version be released? - -When it is finished, which is another year at least. Exact date is impossible to predict. Development tempo depends mostly on free time of active programmers and community members, there is no exact shedule. You may expect new version every three months. Of course, joining the project will speed things up. - -## Are you going to add / change X? - -VCMI recreates basic H3:SoD engine and does not add new content or modify original mechanics by default. Only engine and interface improvements are likely to be supported now. If you want something specific to be done, please present detailed project on [our board](http://forum.vcmi.eu/index.php). Of course you are free to contribute with anything you can do. - -## Will it be possible to do Y? - -Removing engine restrictions and allowing flexible modding of game is the main aim of the project. As yet modification of game is not supported. - -## The game is not working, it crashes and I get strange console messages. - -Report your bug. Details are described [here](#Feedback). The sooner you tell the team about the problem, the sooner it will be resolved. Many problems come just from improper installation or system settings. - -## What is the current status of the project? - -Check [Documentation](../Readme.md), [release notes](http://forum.vcmi.eu/viewforum.php?f=1) or [changelog](https://github.com/vcmi/vcmi/blob/develop/ChangeLog). The best place to watch current changes as they are committed to the develop branch is the [Github commits page](https://github.com/vcmi/vcmi/commits/develop). The game is quite playable by now, although many important features are missing. - -## I have a great idea! - -Share it on [VCMI forum](http://forum.vcmi.eu/index.php) so all team members can see it and share their thoughts. Remember, brainstorming is good for your health. - -## Are you going to support Horn of The Abyss / Wog 3.59 / Grove Town etc.? - -Yes, of course. VCMI is designed as a base for any further mods and uses own executables, so the compatibility is not an issue. The team is not going to compete, but to cooperate with the community of creative modders. - -## Can I help VCMI Project in any way? - -If you are C++ programmer, graphican, tester or just have tons of ideas, do not hesistate - your help is needed. The game is huge and many different ares of activity are still waiting for someone like you. - -## I would like to join development team. - -You are always welcome. Contact the core team via [our board](http://forum.vcmi.eu/index.php). The usual way to join the team is to post your patch for review on our board. If the patch is positively rated by core team members, you will be given access to SVN repository. From 096a1d74f502c99c2ee0ce1d5169508404794d5c Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 10 Nov 2023 02:35:38 +0100 Subject: [PATCH 1091/1248] Docs: Fix CMake commands for enabling Ccache --- docs/developers/Building_Android.md | 2 +- docs/developers/Building_Linux.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/developers/Building_Android.md b/docs/developers/Building_Android.md index 6f04b71a5..ef0c2c909 100644 --- a/docs/developers/Building_Android.md +++ b/docs/developers/Building_Android.md @@ -62,7 +62,7 @@ Building for Android is a 2-step process. First, native C++ code is compiled to This is a traditional CMake project, you can build it from command line or some IDE. You're not required to pass any custom options (except Conan toolchain file), defaults are already good. If you wish to use your own CMake presets, inherit them from our `build-with-conan` preset. Example: ``` -cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_COMPILER_LAUNCHER=ccache --toolchain ... +cmake -S . -B ../build -G Ninja -D CMAKE_BUILD_TYPE=Debug -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_C_COMPILER_LAUNCHER=ccache --toolchain ... cmake --build ../build ``` diff --git a/docs/developers/Building_Linux.md b/docs/developers/Building_Linux.md index a1371532c..5bd2f871c 100644 --- a/docs/developers/Building_Linux.md +++ b/docs/developers/Building_Linux.md @@ -76,7 +76,7 @@ cmake ../vcmi **Notice**: The ../vcmi/ is not a typo, it will place makefile scripts into the build dir as the build dir is your working dir when calling CMake. ## To use ccache: -`cmake ../vcmi -D CMAKE_COMPILER_LAUNCHER=ccache` +`cmake ../vcmi -D CMAKE_CXX_COMPILER_LAUNCHER=ccache -D CMAKE_C_COMPILER_LAUNCHER=ccache` ## Trigger build From eee1a4d0614876d21bf2b33b98f13f8e7848ceb7 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Fri, 10 Nov 2023 03:37:38 +0100 Subject: [PATCH 1092/1248] CVideoHandler.cpp: Clear screen before rendering each frame of the intro This way, the background is black instead of showing glitchy artifacts when one resizes the window --- client/CVideoHandler.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index cd3cc8b6c..38cecc84b 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -631,6 +631,7 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey) SDL_Rect rect = CSDL_Ext::toSDL(pos); + SDL_RenderClear(mainRenderer); SDL_RenderCopy(mainRenderer, texture, nullptr, &rect); SDL_RenderPresent(mainRenderer); From a574f18e6909f94f4df31f6293cda7e4f6da4785 Mon Sep 17 00:00:00 2001 From: gamestales-com Date: Fri, 10 Nov 2023 15:11:35 +0100 Subject: [PATCH 1093/1248] #2903-morale-description-#1442 --- client/widgets/MiscWidgets.cpp | 9 +++++++-- lib/bonuses/Bonus.cpp | 28 ++++++++++++++++------------ lib/constants/EntityIdentifiers.h | 2 +- lib/mapObjects/CArmedInstance.cpp | 2 +- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/client/widgets/MiscWidgets.cpp b/client/widgets/MiscWidgets.cpp index d2b879a01..ff1bc50d8 100644 --- a/client/widgets/MiscWidgets.cpp +++ b/client/widgets/MiscWidgets.cpp @@ -579,8 +579,13 @@ void MoraleLuckBox::set(const AFactionMember * node) std::string addInfo = ""; for(auto & bonus : * modifierList) { - if(bonus->val) - addInfo += "\n" + bonus->Description(); + if(bonus->val) { + const std::string& description = bonus->Description(); + //arraytxt already contains \n + if (description.size() && description[0] != '\n') + addInfo += '\n'; + addInfo += description; + } } text = addInfo.empty() ? text + CGI->generaltexth->arraytxt[noneTxtId] diff --git a/lib/bonuses/Bonus.cpp b/lib/bonuses/Bonus.cpp index 39c0c1c44..bcf782b78 100644 --- a/lib/bonuses/Bonus.cpp +++ b/lib/bonuses/Bonus.cpp @@ -91,7 +91,7 @@ JsonNode CAddInfo::toJsonNode() const } std::string Bonus::Description(std::optional customValue) const { - std::ostringstream str; + std::string str; if(description.empty()) { @@ -100,38 +100,42 @@ std::string Bonus::Description(std::optional customValue) const switch(source) { case BonusSource::ARTIFACT: - str << sid.as().toEntity(VLC)->getNameTranslated(); + str = sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::SPELL_EFFECT: - str << sid.as().toEntity(VLC)->getNameTranslated(); + str = sid.as().toEntity(VLC)->getNameTranslated(); break; case BonusSource::CREATURE_ABILITY: - str << sid.as().toEntity(VLC)->getNamePluralTranslated(); + str = sid.as().toEntity(VLC)->getNamePluralTranslated(); break; case BonusSource::SECONDARY_SKILL: - str << VLC->skills()->getById(sid.as())->getNameTranslated(); + str = VLC->skills()->getById(sid.as())->getNameTranslated(); break; case BonusSource::HERO_SPECIAL: - str << VLC->heroTypes()->getById(sid.as())->getNameTranslated(); + str = VLC->heroTypes()->getById(sid.as())->getNameTranslated(); break; default: //todo: handle all possible sources - str << "Unknown"; + str = "Unknown"; break; } } else - str << stacking; + str = stacking; } else { - str << description; + str = description; } - if(auto value = customValue.value_or(val)) - str << " " << std::showpos << value; + if(auto value = customValue.value_or(val)) { + //arraytxt already contains +-value + std::string valueString = boost::str(boost::format(" %+d") % value); + if(!boost::algorithm::ends_with(str, valueString)) + str += valueString; + } - return str.str(); + return str; } static JsonNode additionalInfoToJson(BonusType type, CAddInfo addInfo) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index edf4a36cb..c6b6533a7 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -760,7 +760,7 @@ public: DIMENSION_DOOR = 8, TOWN_PORTAL = 9, - // Combar spells + // Combat spells QUICKSAND = 10, LAND_MINE = 11, FORCE_FIELD = 12, diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index baf878870..e6e74d09d 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -111,7 +111,7 @@ void CArmedInstance::updateMoraleBonusFromArmy() { b->val = 2 - static_cast(factionsInArmy); description = boost::str(boost::format(VLC->generaltexth->arraytxt[114]) % factionsInArmy % b->val); //Troops of %d alignments %d - description = b->description.substr(0, description.size()-2);//trim value + description = description.substr(0, description.size()-3);//trim value } boost::algorithm::trim(description); From 5c810df36f00755ec6c4985d1993c3cadc205bb2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 11 Nov 2023 00:39:08 +0200 Subject: [PATCH 1094/1248] Reorganized types registration code --- client/CServerHandler.cpp | 2 +- client/Client.cpp | 6 +- client/windows/CMapOverview.cpp | 2 +- cmake_modules/VCMI_lib.cmake | 17 +- lib/IGameCallback.cpp | 5 +- lib/gameState/CGameState.cpp | 5 +- lib/mapping/CMapInfo.cpp | 2 +- lib/registerTypes/RegisterTypes.cpp | 51 --- lib/registerTypes/RegisterTypes.h | 343 +------------------ lib/registerTypes/RegisterTypesClientPacks.h | 125 +++++++ lib/registerTypes/RegisterTypesLobbyPacks.h | 62 ++++ lib/registerTypes/RegisterTypesMapObjects.h | 137 ++++++++ lib/registerTypes/RegisterTypesServerPacks.h | 56 +++ lib/registerTypes/TypesClientPacks1.cpp | 33 -- lib/registerTypes/TypesClientPacks2.cpp | 37 -- lib/registerTypes/TypesLobbyPacks.cpp | 43 --- lib/registerTypes/TypesMapObjects1.cpp | 34 -- lib/registerTypes/TypesMapObjects2.cpp | 36 -- lib/registerTypes/TypesMapObjects3.cpp | 32 -- lib/registerTypes/TypesServerPacks.cpp | 32 -- lib/serializer/BinaryDeserializer.cpp | 91 +---- lib/serializer/BinaryDeserializer.h | 40 +-- lib/serializer/BinarySerializer.cpp | 62 +--- lib/serializer/BinarySerializer.h | 32 +- lib/serializer/CLoadFile.cpp | 100 ++++++ lib/serializer/CLoadFile.h | 42 +++ lib/serializer/CMemorySerializer.cpp | 4 - lib/serializer/CSaveFile.cpp | 72 ++++ lib/serializer/CSaveFile.h | 42 +++ lib/serializer/Connection.cpp | 5 +- server/CGameHandler.cpp | 10 +- server/CVCMIServer.cpp | 2 +- 32 files changed, 681 insertions(+), 881 deletions(-) delete mode 100644 lib/registerTypes/RegisterTypes.cpp create mode 100644 lib/registerTypes/RegisterTypesClientPacks.h create mode 100644 lib/registerTypes/RegisterTypesLobbyPacks.h create mode 100644 lib/registerTypes/RegisterTypesMapObjects.h create mode 100644 lib/registerTypes/RegisterTypesServerPacks.h delete mode 100644 lib/registerTypes/TypesClientPacks1.cpp delete mode 100644 lib/registerTypes/TypesClientPacks2.cpp delete mode 100644 lib/registerTypes/TypesLobbyPacks.cpp delete mode 100644 lib/registerTypes/TypesMapObjects1.cpp delete mode 100644 lib/registerTypes/TypesMapObjects2.cpp delete mode 100644 lib/registerTypes/TypesMapObjects3.cpp delete mode 100644 lib/registerTypes/TypesServerPacks.cpp create mode 100644 lib/serializer/CLoadFile.cpp create mode 100644 lib/serializer/CLoadFile.h create mode 100644 lib/serializer/CSaveFile.cpp create mode 100644 lib/serializer/CSaveFile.h diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index d0930fe15..361382a17 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -47,7 +47,7 @@ #include "../lib/modding/ModIncompatibility.h" #include "../lib/rmg/CMapGenOptions.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" diff --git a/client/Client.cpp b/client/Client.cpp index 280f4daf1..e9238b11e 100644 --- a/client/Client.cpp +++ b/client/Client.cpp @@ -24,6 +24,7 @@ #include "../CCallback.h" #include "../lib/CConfigHandler.h" #include "../lib/gameState/CGameState.h" +#include "../lib/CPlayerState.h" #include "../lib/CThreadHelper.h" #include "../lib/VCMIDirs.h" #include "../lib/UnlockGuard.h" @@ -32,7 +33,7 @@ #include "../lib/mapping/CMapService.h" #include "../lib/pathfinder/CGPathNode.h" #include "../lib/filesystem/Filesystem.h" -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesClientPacks.h" #include "../lib/serializer/Connection.h" #include @@ -137,8 +138,7 @@ CClient::CClient() { waitingRequest.clear(); applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); + registerTypesClientPacks(*applier); IObjectInterface::cb = this; gs = nullptr; } diff --git a/client/windows/CMapOverview.cpp b/client/windows/CMapOverview.cpp index 335839991..b78d07b84 100644 --- a/client/windows/CMapOverview.cpp +++ b/client/windows/CMapOverview.cpp @@ -39,7 +39,7 @@ #include "../../lib/TerrainHandler.h" #include "../../lib/filesystem/Filesystem.h" -#include "../../lib/serializer/BinaryDeserializer.h" +#include "../../lib/serializer/CLoadFile.h" #include "../../lib/StartInfo.h" #include "../../lib/rmg/CMapGenOptions.h" diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 3e7663c2b..93bf03483 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -133,15 +133,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/pathfinder/PathfindingRules.cpp ${MAIN_LIB_DIR}/pathfinder/TurnInfo.cpp - ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesClientPacks2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects1.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects2.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesMapObjects3.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesLobbyPacks.cpp - ${MAIN_LIB_DIR}/registerTypes/TypesServerPacks.cpp - ${MAIN_LIB_DIR}/rewardable/Configuration.cpp ${MAIN_LIB_DIR}/rewardable/Info.cpp ${MAIN_LIB_DIR}/rewardable/Interface.cpp @@ -181,8 +172,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.cpp ${MAIN_LIB_DIR}/serializer/BinarySerializer.cpp + ${MAIN_LIB_DIR}/serializer/CLoadFile.cpp ${MAIN_LIB_DIR}/serializer/CMemorySerializer.cpp ${MAIN_LIB_DIR}/serializer/Connection.cpp + ${MAIN_LIB_DIR}/serializer/CSaveFile.cpp ${MAIN_LIB_DIR}/serializer/CSerializer.cpp ${MAIN_LIB_DIR}/serializer/CTypeList.cpp ${MAIN_LIB_DIR}/serializer/JsonDeserializer.cpp @@ -505,6 +498,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/pathfinder/TurnInfo.h ${MAIN_LIB_DIR}/registerTypes/RegisterTypes.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesClientPacks.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesLobbyPacks.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesMapObjects.h + ${MAIN_LIB_DIR}/registerTypes/RegisterTypesServerPacks.h ${MAIN_LIB_DIR}/rewardable/Configuration.h ${MAIN_LIB_DIR}/rewardable/Info.h @@ -548,8 +545,10 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/BinaryDeserializer.h ${MAIN_LIB_DIR}/serializer/BinarySerializer.h + ${MAIN_LIB_DIR}/serializer/CLoadFile.h ${MAIN_LIB_DIR}/serializer/CMemorySerializer.h ${MAIN_LIB_DIR}/serializer/Connection.h + ${MAIN_LIB_DIR}/serializer/CSaveFile.h ${MAIN_LIB_DIR}/serializer/CSerializer.h ${MAIN_LIB_DIR}/serializer/CTypeList.h ${MAIN_LIB_DIR}/serializer/JsonDeserializer.h diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index 76ff89457..e14a5ac1c 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -21,9 +21,8 @@ #include "bonuses/Propagators.h" #include "bonuses/Updaters.h" -#include "serializer/CSerializer.h" // for SAVEGAME_MAGIC -#include "serializer/BinaryDeserializer.h" -#include "serializer/BinarySerializer.h" +#include "serializer/CLoadFile.h" +#include "serializer/CSaveFile.h" #include "rmg/CMapGenOptions.h" #include "mapObjectConstructors/AObjectTypeHandler.h" #include "mapObjectConstructors/CObjectClassesHandler.h" diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7f9fda6df..beb8eeda4 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -42,7 +42,7 @@ #include "../modding/IdentifierStorage.h" #include "../pathfinder/CPathfinder.h" #include "../pathfinder/PathfinderOptions.h" -#include "../registerTypes/RegisterTypes.h" +#include "../registerTypes/RegisterTypesClientPacks.h" #include "../rmg/CMapGenerator.h" #include "../serializer/CMemorySerializer.h" #include "../serializer/CTypeList.h" @@ -157,8 +157,7 @@ CGameState::CGameState() gs = this; heroesPool = std::make_unique(); applier = std::make_shared>(); - registerTypesClientPacks1(*applier); - registerTypesClientPacks2(*applier); + registerTypesClientPacks(*applier); globalEffects.setNodeType(CBonusSystemNode::GLOBAL_EFFECTS); } diff --git a/lib/mapping/CMapInfo.cpp b/lib/mapping/CMapInfo.cpp index ede7a16e6..f729cdbcf 100644 --- a/lib/mapping/CMapInfo.cpp +++ b/lib/mapping/CMapInfo.cpp @@ -21,7 +21,7 @@ #include "../campaign/CampaignHandler.h" #include "../filesystem/Filesystem.h" -#include "../serializer/CMemorySerializer.h" +#include "../serializer/CLoadFile.h" #include "../CGeneralTextHandler.h" #include "../rmg/CMapGenOptions.h" #include "../CCreatureHandler.h" diff --git a/lib/registerTypes/RegisterTypes.cpp b/lib/registerTypes/RegisterTypes.cpp deleted file mode 100644 index cc96a27b5..000000000 --- a/lib/registerTypes/RegisterTypes.cpp +++ /dev/null @@ -1,51 +0,0 @@ -/* - * RegisterTypes.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" -#define INSTANTIATE_REGISTER_TYPES_HERE -#include "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../spells/CSpellHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -// For reference: peak memory usage by gcc during compilation of register type templates -// registerTypesMapObjects: 1.9 Gb -// registerTypes2: 2.2 Gb -// registerTypesClientPacks1 1.6 Gb -// registerTypesClientPacks2 1.6 Gb -// registerTypesServerPacks: 1.3 Gb -// registerTypes4: 1.3 Gb - - -#define DEFINE_EXTERNAL_METHOD(METHODNAME) \ -extern template DLL_LINKAGE void METHODNAME(BinaryDeserializer & s); \ -extern template DLL_LINKAGE void METHODNAME(BinarySerializer & s); - -//DEFINE_EXTERNAL_METHOD(registerTypesMapObjects) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects1) -DEFINE_EXTERNAL_METHOD(registerTypesMapObjects2) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks1) -DEFINE_EXTERNAL_METHOD(registerTypesClientPacks2) -DEFINE_EXTERNAL_METHOD(registerTypesServerPacks) -DEFINE_EXTERNAL_METHOD(registerTypesLobbyPacks) - -template void registerTypes(BinaryDeserializer & s); -template void registerTypes(BinarySerializer & s); -template void registerTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypes.h b/lib/registerTypes/RegisterTypes.h index 9736aa285..2c2284452 100644 --- a/lib/registerTypes/RegisterTypes.h +++ b/lib/registerTypes/RegisterTypes.h @@ -9,351 +9,20 @@ */ #pragma once -#include "../networkPacks/PacksForClient.h" -#include "../networkPacks/PacksForClientBattle.h" -#include "../networkPacks/PacksForServer.h" -#include "../networkPacks/PacksForLobby.h" -#include "../networkPacks/SetStackEffect.h" -#include "../mapObjectConstructors/CBankInstanceConstructor.h" -#include "../mapObjects/MapObjects.h" -#include "../mapObjects/CGCreature.h" -#include "../mapObjects/CGTownBuilding.h" -#include "../mapObjects/ObjectTemplate.h" -#include "../battle/BattleInfo.h" -#include "../battle/CObstacleInstance.h" -#include "../bonuses/Limiters.h" -#include "../bonuses/Updaters.h" -#include "../bonuses/Propagators.h" -#include "../CPlayerState.h" -#include "../CStack.h" +#include "RegisterTypesClientPacks.h" +#include "RegisterTypesLobbyPacks.h" +#include "RegisterTypesMapObjects.h" +#include "RegisterTypesServerPacks.h" VCMI_LIB_NAMESPACE_BEGIN -class BinarySerializer; -class BinaryDeserializer; -class CTypeList; - -template -void registerTypesMapObjects1(Serializer &s) -{ - ////////////////////////////////////////////////////////////////////////// - // Adventure map objects - ////////////////////////////////////////////////////////////////////////// - s.template registerType(); - - // Non-armed objects - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); s.template registerType(); s.template registerType(); - - // Armed objects - s.template registerType(); s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); s.template registerType(); - s.template registerType(); -} - -template -void registerTypesMapObjectTypes(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - //new types (other than netpacks) must register here - //order of type registration is critical for loading old savegames -} - -template -void registerTypesMapObjects2(Serializer &s) -{ - //Other object-related - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - - s.template registerType(); - s.template registerType(); - - //s.template registerType(); - //s.template registerType(); - - //end of objects - - ////////////////////////////////////////////////////////////////////////// - // Bonus system - ////////////////////////////////////////////////////////////////////////// - //s.template registerType(); - s.template registerType(); - - // Limiters - //s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - -// s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); //TODO - //s.template registerType(); - s.template registerType(); - s.template registerType(); - //s.template registerType(); - s.template registerType(); - - //s.template registerType(); - s.template registerType(); -} -template -void registerTypesClientPacks1(Serializer &s) -{ - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesClientPacks2(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesServerPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - -template -void registerTypesLobbyPacks(Serializer &s) -{ - s.template registerType(); - s.template registerType(); - s.template registerType(); - - // Any client can sent - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only host client send - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - // Only server send - s.template registerType(); - s.template registerType(); - - // For client with permissions - s.template registerType(); - // Only for host client - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); - s.template registerType(); -} - template void registerTypes(Serializer &s) { - registerTypesMapObjects1(s); - registerTypesMapObjects2(s); - registerTypesMapObjectTypes(s); - registerTypesClientPacks1(s); - registerTypesClientPacks2(s); + registerTypesMapObjects(s); + registerTypesClientPacks(s); registerTypesServerPacks(s); registerTypesLobbyPacks(s); } -#ifndef INSTANTIATE_REGISTER_TYPES_HERE - -extern template DLL_LINKAGE void registerTypes(BinaryDeserializer & s); -extern template DLL_LINKAGE void registerTypes(BinarySerializer & s); - -#endif - - VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesClientPacks.h b/lib/registerTypes/RegisterTypesClientPacks.h new file mode 100644 index 000000000..7779c618a --- /dev/null +++ b/lib/registerTypes/RegisterTypesClientPacks.h @@ -0,0 +1,125 @@ +/* + * RegisterTypesClientPacks.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 "../networkPacks/PacksForClient.h" +#include "../networkPacks/PacksForClientBattle.h" +#include "../networkPacks/SetStackEffect.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesClientPacks(Serializer &s) +{ + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesLobbyPacks.h b/lib/registerTypes/RegisterTypesLobbyPacks.h new file mode 100644 index 000000000..6e20ee244 --- /dev/null +++ b/lib/registerTypes/RegisterTypesLobbyPacks.h @@ -0,0 +1,62 @@ +/* + * RegisterTypesLobbyPacks.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 "../networkPacks/PacksForLobby.h" +#include "../gameState/CGameState.h" +#include "../campaign/CampaignState.h" +#include "../mapping/CMapInfo.h" +#include "../rmg/CMapGenOptions.h" +#include "../gameState/TavernHeroesPool.h" +#include "../gameState/CGameStateCampaign.h" +#include "../mapping/CMap.h" +#include "../TerrainHandler.h" +#include "../RiverHandler.h" +#include "../RoadHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesLobbyPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + + // Any client can sent + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only host client send + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + // Only server send + s.template registerType(); + s.template registerType(); + + // For client with permissions + s.template registerType(); + // Only for host client + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesMapObjects.h b/lib/registerTypes/RegisterTypesMapObjects.h new file mode 100644 index 000000000..73f87e5e3 --- /dev/null +++ b/lib/registerTypes/RegisterTypesMapObjects.h @@ -0,0 +1,137 @@ +/* + * RegisterTypesMapObjects.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 "../mapObjectConstructors/CBankInstanceConstructor.h" +#include "../mapObjects/MapObjects.h" +#include "../mapObjects/CGCreature.h" +#include "../mapObjects/CGTownBuilding.h" +#include "../mapObjects/ObjectTemplate.h" +#include "../battle/BattleInfo.h" +#include "../battle/CObstacleInstance.h" +#include "../bonuses/Limiters.h" +#include "../bonuses/Updaters.h" +#include "../bonuses/Propagators.h" +#include "../CPlayerState.h" +#include "../CStack.h" +#include "../CHeroHandler.h" + +VCMI_LIB_NAMESPACE_BEGIN + +template +void registerTypesMapObjects(Serializer &s) +{ + ////////////////////////////////////////////////////////////////////////// + // Adventure map objects + ////////////////////////////////////////////////////////////////////////// + s.template registerType(); + + // Non-armed objects + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); s.template registerType(); s.template registerType(); + + // Armed objects + s.template registerType(); s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + s.template registerType(); + s.template registerType(); + //new types (other than netpacks) must register here + //order of type registration is critical for loading old savegames + + //Other object-related + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + + s.template registerType(); + + s.template registerType(); + s.template registerType(); + + //end of objects + + ////////////////////////////////////////////////////////////////////////// + // Bonus system + ////////////////////////////////////////////////////////////////////////// + //s.template registerType(); + s.template registerType(); + + // Limiters + //s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + +// s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); //TODO + //s.template registerType(); + s.template registerType(); + s.template registerType(); + //s.template registerType(); + s.template registerType(); + + //s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/RegisterTypesServerPacks.h b/lib/registerTypes/RegisterTypesServerPacks.h new file mode 100644 index 000000000..f5eed0156 --- /dev/null +++ b/lib/registerTypes/RegisterTypesServerPacks.h @@ -0,0 +1,56 @@ +/* + * RegisterTypesServerPacks.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 "../networkPacks/PacksForServer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class BinarySerializer; +class BinaryDeserializer; +class CTypeList; + +template +void registerTypesServerPacks(Serializer &s) +{ + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); + s.template registerType(); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks1.cpp b/lib/registerTypes/TypesClientPacks1.cpp deleted file mode 100644 index c13b78c83..000000000 --- a/lib/registerTypes/TypesClientPacks1.cpp +++ /dev/null @@ -1,33 +0,0 @@ -/* - * TypesClientPacks1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks1(BinaryDeserializer & s); -template void registerTypesClientPacks1(BinarySerializer & s); -template void registerTypesClientPacks1(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesClientPacks2.cpp b/lib/registerTypes/TypesClientPacks2.cpp deleted file mode 100644 index bf68ca257..000000000 --- a/lib/registerTypes/TypesClientPacks2.cpp +++ /dev/null @@ -1,37 +0,0 @@ -/* - * TypesClientPacks2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesClientPacks2(BinaryDeserializer & s); -template void registerTypesClientPacks2(BinarySerializer & s); -template void registerTypesClientPacks2(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesLobbyPacks.cpp b/lib/registerTypes/TypesLobbyPacks.cpp deleted file mode 100644 index fe3838d05..000000000 --- a/lib/registerTypes/TypesLobbyPacks.cpp +++ /dev/null @@ -1,43 +0,0 @@ -/* - * TypesLobbyPacks.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 "RegisterTypes.h" - -#include "../mapping/CMapInfo.h" -#include "../StartInfo.h" -#include "../gameState/CGameState.h" -#include "../gameState/CGameStateCampaign.h" -#include "../gameState/TavernHeroesPool.h" -#include "../mapping/CMap.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" -#include "../RoadHandler.h" -#include "../RiverHandler.h" -#include "../TerrainHandler.h" -#include "../campaign/CampaignState.h" -#include "../rmg/CMapGenOptions.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesLobbyPacks(BinaryDeserializer & s); -template void registerTypesLobbyPacks(BinarySerializer & s); -template void registerTypesLobbyPacks(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects1.cpp b/lib/registerTypes/TypesMapObjects1.cpp deleted file mode 100644 index 1e383a581..000000000 --- a/lib/registerTypes/TypesMapObjects1.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* - * TypesMapObjects1.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjects1(BinaryDeserializer & s); -template void registerTypesMapObjects1(BinarySerializer & s); -template void registerTypesMapObjects1(CTypeList & s); - - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects2.cpp b/lib/registerTypes/TypesMapObjects2.cpp deleted file mode 100644 index ab7d4f1a1..000000000 --- a/lib/registerTypes/TypesMapObjects2.cpp +++ /dev/null @@ -1,36 +0,0 @@ -/* - * TypesMapObjects2.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../CStack.h" -#include "../battle/BattleInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - - -template void registerTypesMapObjects2(BinaryDeserializer & s); -template void registerTypesMapObjects2(BinarySerializer & s); -template void registerTypesMapObjects2(CTypeList & s); - - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesMapObjects3.cpp b/lib/registerTypes/TypesMapObjects3.cpp deleted file mode 100644 index 3833d0dbf..000000000 --- a/lib/registerTypes/TypesMapObjects3.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * TypesMapObjects3.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesMapObjectTypes(BinaryDeserializer & s); -template void registerTypesMapObjectTypes(BinarySerializer & s); -template void registerTypesMapObjectTypes(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/registerTypes/TypesServerPacks.cpp b/lib/registerTypes/TypesServerPacks.cpp deleted file mode 100644 index 3d37cbc92..000000000 --- a/lib/registerTypes/TypesServerPacks.cpp +++ /dev/null @@ -1,32 +0,0 @@ -/* - * TypesServerPacks.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 "RegisterTypes.h" - -#include "../StartInfo.h" -#include "../mapObjects/CObjectHandler.h" -#include "../CCreatureHandler.h" -#include "../VCMI_Lib.h" -#include "../CArtHandler.h" -#include "../CHeroHandler.h" -#include "../spells/CSpellHandler.h" -#include "../CTownHandler.h" - -#include "../serializer/BinaryDeserializer.h" -#include "../serializer/BinarySerializer.h" -#include "../serializer/CTypeList.h" - -VCMI_LIB_NAMESPACE_BEGIN - -template void registerTypesServerPacks(BinaryDeserializer & s); -template void registerTypesServerPacks(BinarySerializer & s); -template void registerTypesServerPacks(CTypeList & s); - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 95400caf9..0cb95f14a 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -9,97 +9,18 @@ */ #include "StdInc.h" #include "BinaryDeserializer.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinaryDeserializer & s); - -CLoadFile::CLoadFile(const boost::filesystem::path & fname, int minimalVersion) - : serializer(this) +BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) { - registerTypes(serializer); - openNextFile(fname, minimalVersion); -} + saving = false; + fileVersion = 0; + smartPointerSerialization = true; + reverseEndianess = false; -//must be instantiated in .cpp file for access to complete types of all member fields -CLoadFile::~CLoadFile() = default; - -int CLoadFile::read(void * data, unsigned size) -{ - sfile->read(reinterpret_cast(data), size); - return size; -} - -void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalVersion) -{ - assert(!serializer.reverseEndianess); - assert(minimalVersion <= SERIALIZATION_VERSION); - - try - { - fName = fname.string(); - sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to read %s!", fName); - - //we can read - char buffer[4]; - sfile->read(buffer, 4); - if(std::memcmp(buffer, "VCMI", 4) != 0) - THROW_FORMAT("Error: not a VCMI file(%s)!", fName); - - serializer & serializer.fileVersion; - if(serializer.fileVersion < minimalVersion) - THROW_FORMAT("Error: too old file format (%s)!", fName); - - if(serializer.fileVersion > SERIALIZATION_VERSION) - { - logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", serializer.fileVersion, SERIALIZATION_VERSION , fName); - - auto * versionptr = reinterpret_cast(&serializer.fileVersion); - std::reverse(versionptr, versionptr + 4); - logGlobal->warn("Version number reversed is %x, checking...", serializer.fileVersion); - - if(serializer.fileVersion == SERIALIZATION_VERSION) - { - logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); - serializer.reverseEndianess = true; - } - else - THROW_FORMAT("Error: too new file format (%s)!", fName); - } - } - catch(...) - { - clear(); //if anything went wrong, we delete file and rethrow - throw; - } -} - -void CLoadFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CLoadFile"); - if(!!sfile && *sfile) - out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); -} - -void CLoadFile::clear() -{ - sfile = nullptr; - fName.clear(); - serializer.fileVersion = 0; -} - -void CLoadFile::checkMagicBytes(const std::string &text) -{ - std::string loaded = text; - read((void *)loaded.data(), static_cast(text.length())); - if(loaded != text) - throw std::runtime_error("Magic bytes doesn't match!"); + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 9b57ee480..1c4858469 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -12,12 +12,9 @@ #include "CSerializer.h" #include "CTypeList.h" #include "../mapObjects/CGHeroInstance.h" -#include "../../Global.h" VCMI_LIB_NAMESPACE_BEGIN -class CStackInstance; - class DLL_LINKAGE CLoaderBase { protected: @@ -115,7 +112,8 @@ class DLL_LINKAGE BinaryDeserializer : public CLoaderBase } }; - template class CPointerLoader : public IPointerLoader + template + class CPointerLoader : public IPointerLoader { public: void * loadPtr(CLoaderBase &ar, ui32 pid) const override //data is pointer to the ACTUAL POINTER @@ -147,13 +145,7 @@ public: bool smartPointerSerialization; bool saving; - BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) - { - saving = false; - fileVersion = 0; - smartPointerSerialization = true; - reverseEndianess = false; - } + BinaryDeserializer(IBinaryReader * r); template BinaryDeserializer & operator&(T & t) @@ -538,30 +530,4 @@ public: } }; -class DLL_LINKAGE CLoadFile : public IBinaryReader -{ -public: - BinaryDeserializer serializer; - - std::string fName; - std::unique_ptr sfile; - - CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! - virtual ~CLoadFile(); - int read(void * data, unsigned size) override; //throws! - - void openNextFile(const boost::filesystem::path & fname, int minimalVersion); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void checkMagicBytes(const std::string & text); - - template - CLoadFile & operator>>(T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.cpp b/lib/serializer/BinarySerializer.cpp index 46c99f6de..8ba41babd 100644 --- a/lib/serializer/BinarySerializer.cpp +++ b/lib/serializer/BinarySerializer.cpp @@ -9,69 +9,15 @@ */ #include "StdInc.h" #include "BinarySerializer.h" - #include "../registerTypes/RegisterTypes.h" VCMI_LIB_NAMESPACE_BEGIN -extern template void registerTypes(BinarySerializer & s); - -CSaveFile::CSaveFile(const boost::filesystem::path &fname) - : serializer(this) +BinarySerializer::BinarySerializer(IBinaryWriter * w): CSaverBase(w) { - registerTypes(serializer); - openNextFile(fname); -} - -//must be instantiated in .cpp file for access to complete types of all member fields -CSaveFile::~CSaveFile() = default; - -int CSaveFile::write(const void * data, unsigned size) -{ - sfile->write((char *)data,size); - return size; -} - -void CSaveFile::openNextFile(const boost::filesystem::path &fname) -{ - fName = fname; - try - { - sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); - sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway - - if(!(*sfile)) - THROW_FORMAT("Error: cannot open to write %s!", fname); - - sfile->write("VCMI",4); //write magic identifier - serializer & SERIALIZATION_VERSION; //write format version - } - catch(...) - { - logGlobal->error("Failed to save to %s", fname.string()); - clear(); - throw; - } -} - -void CSaveFile::reportState(vstd::CLoggerBase * out) -{ - out->debug("CSaveFile"); - if(sfile.get() && *sfile) - { - out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); - } -} - -void CSaveFile::clear() -{ - fName.clear(); - sfile = nullptr; -} - -void CSaveFile::putMagicBytes(const std::string &text) -{ - write(text.c_str(), static_cast(text.length())); + saving=true; + smartPointerSerialization = true; + registerTypes(*this); } VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index f65ef07ec..37a054d2d 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -115,11 +115,7 @@ public: bool smartPointerSerialization; bool saving; - BinarySerializer(IBinaryWriter * w): CSaverBase(w) - { - saving=true; - smartPointerSerialization = true; - } + BinarySerializer(IBinaryWriter * w); template void registerType(const Base * b = nullptr, const Derived * d = nullptr) @@ -404,30 +400,4 @@ public: } }; -class DLL_LINKAGE CSaveFile : public IBinaryWriter -{ -public: - BinarySerializer serializer; - - boost::filesystem::path fName; - std::unique_ptr sfile; - - CSaveFile(const boost::filesystem::path &fname); //throws! - ~CSaveFile(); - int write(const void * data, unsigned size) override; - - void openNextFile(const boost::filesystem::path &fname); //throws! - void clear(); - void reportState(vstd::CLoggerBase * out) override; - - void putMagicBytes(const std::string &text); - - template - CSaveFile & operator<<(const T &t) - { - serializer & t; - return * this; - } -}; - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.cpp b/lib/serializer/CLoadFile.cpp new file mode 100644 index 000000000..cf5ea34d7 --- /dev/null +++ b/lib/serializer/CLoadFile.cpp @@ -0,0 +1,100 @@ +/* + * CLoadFile.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 "CLoadFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CLoadFile::CLoadFile(const boost::filesystem::path & fname, int minimalVersion) + : serializer(this) +{ + openNextFile(fname, minimalVersion); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CLoadFile::~CLoadFile() = default; + +int CLoadFile::read(void * data, unsigned size) +{ + sfile->read(reinterpret_cast(data), size); + return size; +} + +void CLoadFile::openNextFile(const boost::filesystem::path & fname, int minimalVersion) +{ + assert(!serializer.reverseEndianess); + assert(minimalVersion <= SERIALIZATION_VERSION); + + try + { + fName = fname.string(); + sfile = std::make_unique(fname.c_str(), std::ios::in | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to read %s!", fName); + + //we can read + char buffer[4]; + sfile->read(buffer, 4); + if(std::memcmp(buffer, "VCMI", 4) != 0) + THROW_FORMAT("Error: not a VCMI file(%s)!", fName); + + serializer & serializer.fileVersion; + if(serializer.fileVersion < minimalVersion) + THROW_FORMAT("Error: too old file format (%s)!", fName); + + if(serializer.fileVersion > SERIALIZATION_VERSION) + { + logGlobal->warn("Warning format version mismatch: found %d when current is %d! (file %s)\n", serializer.fileVersion, SERIALIZATION_VERSION , fName); + + auto * versionptr = reinterpret_cast(&serializer.fileVersion); + std::reverse(versionptr, versionptr + 4); + logGlobal->warn("Version number reversed is %x, checking...", serializer.fileVersion); + + if(serializer.fileVersion == SERIALIZATION_VERSION) + { + logGlobal->warn("%s seems to have different endianness! Entering reversing mode.", fname.string()); + serializer.reverseEndianess = true; + } + else + THROW_FORMAT("Error: too new file format (%s)!", fName); + } + } + catch(...) + { + clear(); //if anything went wrong, we delete file and rethrow + throw; + } +} + +void CLoadFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CLoadFile"); + if(!!sfile && *sfile) + out->debug("\tOpened %s Position: %d", fName, sfile->tellg()); +} + +void CLoadFile::clear() +{ + sfile = nullptr; + fName.clear(); + serializer.fileVersion = 0; +} + +void CLoadFile::checkMagicBytes(const std::string &text) +{ + std::string loaded = text; + read((void *)loaded.data(), static_cast(text.length())); + if(loaded != text) + throw std::runtime_error("Magic bytes doesn't match!"); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CLoadFile.h b/lib/serializer/CLoadFile.h new file mode 100644 index 000000000..8f9079bb9 --- /dev/null +++ b/lib/serializer/CLoadFile.h @@ -0,0 +1,42 @@ +/* + * CLoadFile.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 "BinaryDeserializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CLoadFile : public IBinaryReader +{ +public: + BinaryDeserializer serializer; + + std::string fName; + std::unique_ptr sfile; + + CLoadFile(const boost::filesystem::path & fname, int minimalVersion = SERIALIZATION_VERSION); //throws! + virtual ~CLoadFile(); + int read(void * data, unsigned size) override; //throws! + + void openNextFile(const boost::filesystem::path & fname, int minimalVersion); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void checkMagicBytes(const std::string & text); + + template + CLoadFile & operator>>(T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CMemorySerializer.cpp b/lib/serializer/CMemorySerializer.cpp index f9d62c619..797c10a6d 100644 --- a/lib/serializer/CMemorySerializer.cpp +++ b/lib/serializer/CMemorySerializer.cpp @@ -10,8 +10,6 @@ #include "StdInc.h" #include "CMemorySerializer.h" -#include "../registerTypes/RegisterTypes.h" - VCMI_LIB_NAMESPACE_BEGIN int CMemorySerializer::read(void * data, unsigned size) @@ -34,8 +32,6 @@ int CMemorySerializer::write(const void * data, unsigned size) CMemorySerializer::CMemorySerializer(): iser(this), oser(this), readPos(0) { - registerTypes(iser); - registerTypes(oser); iser.fileVersion = SERIALIZATION_VERSION; } diff --git a/lib/serializer/CSaveFile.cpp b/lib/serializer/CSaveFile.cpp new file mode 100644 index 000000000..5f377aed6 --- /dev/null +++ b/lib/serializer/CSaveFile.cpp @@ -0,0 +1,72 @@ +/* + * CSaveFile.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 "CSaveFile.h" + +VCMI_LIB_NAMESPACE_BEGIN + +CSaveFile::CSaveFile(const boost::filesystem::path &fname) + : serializer(this) +{ + openNextFile(fname); +} + +//must be instantiated in .cpp file for access to complete types of all member fields +CSaveFile::~CSaveFile() = default; + +int CSaveFile::write(const void * data, unsigned size) +{ + sfile->write((char *)data,size); + return size; +} + +void CSaveFile::openNextFile(const boost::filesystem::path &fname) +{ + fName = fname; + try + { + sfile = std::make_unique(fname.c_str(), std::ios::out | std::ios::binary); + sfile->exceptions(std::ifstream::failbit | std::ifstream::badbit); //we throw a lot anyway + + if(!(*sfile)) + THROW_FORMAT("Error: cannot open to write %s!", fname); + + sfile->write("VCMI",4); //write magic identifier + serializer & SERIALIZATION_VERSION; //write format version + } + catch(...) + { + logGlobal->error("Failed to save to %s", fname.string()); + clear(); + throw; + } +} + +void CSaveFile::reportState(vstd::CLoggerBase * out) +{ + out->debug("CSaveFile"); + if(sfile.get() && *sfile) + { + out->debug("\tOpened %s \tPosition: %d", fName, sfile->tellp()); + } +} + +void CSaveFile::clear() +{ + fName.clear(); + sfile = nullptr; +} + +void CSaveFile::putMagicBytes(const std::string &text) +{ + write(text.c_str(), static_cast(text.length())); +} + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/CSaveFile.h b/lib/serializer/CSaveFile.h new file mode 100644 index 000000000..f1b823bf2 --- /dev/null +++ b/lib/serializer/CSaveFile.h @@ -0,0 +1,42 @@ +/* + * CSaveFile.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 "BinarySerializer.h" + +VCMI_LIB_NAMESPACE_BEGIN + +class DLL_LINKAGE CSaveFile : public IBinaryWriter +{ +public: + BinarySerializer serializer; + + boost::filesystem::path fName; + std::unique_ptr sfile; + + CSaveFile(const boost::filesystem::path &fname); //throws! + ~CSaveFile(); + int write(const void * data, unsigned size) override; + + void openNextFile(const boost::filesystem::path &fname); //throws! + void clear(); + void reportState(vstd::CLoggerBase * out) override; + + void putMagicBytes(const std::string &text); + + template + CSaveFile & operator<<(const T &t) + { + serializer & t; + return * this; + } +}; + +VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index 3029aaf91..a4ad78056 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -10,8 +10,7 @@ #include "StdInc.h" #include "Connection.h" -#include "../registerTypes/RegisterTypes.h" -#include "../mapping/CMapHeader.h" +#include "../networkPacks/NetPacksBase.h" #include @@ -45,8 +44,6 @@ void CConnection::init() enableSmartPointerSerialization(); disableStackSendingByID(); - registerTypes(iser); - registerTypes(oser); #ifndef VCMI_ENDIAN_BIG myEndianess = true; #else diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5bf054899..a1f3c25b0 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -27,6 +27,7 @@ #include "../lib/CCreatureSet.h" #include "../lib/CGeneralTextHandler.h" #include "../lib/CHeroHandler.h" +#include "../lib/CPlayerState.h" #include "../lib/CSoundBase.h" #include "../lib/CThreadHelper.h" #include "../lib/CTownHandler.h" @@ -47,20 +48,19 @@ #include "../lib/mapping/CMap.h" #include "../lib/mapping/CMapService.h" +#include "../lib/mapObjects/CGMarket.h" #include "../lib/modding/ModIncompatibility.h" #include "../lib/networkPacks/StackLocation.h" #include "../lib/pathfinder/CPathfinder.h" #include "../lib/pathfinder/PathfinderOptions.h" #include "../lib/pathfinder/TurnInfo.h" -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesServerPacks.h" #include "../lib/rmg/CMapGenOptions.h" -#include "../lib/serializer/CTypeList.h" -#include "../lib/serializer/Cast.h" -#include "../lib/serializer/Connection.h" -#include "../lib/serializer/JsonSerializer.h" +#include "../lib/serializer/CSaveFile.h" +#include "../lib/serializer/CLoadFile.h" #include "../lib/spells/CSpellHandler.h" diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index 0eba3ed90..e9651f91c 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -47,7 +47,7 @@ #include "../lib/UnlockGuard.h" // for applier -#include "../lib/registerTypes/RegisterTypes.h" +#include "../lib/registerTypes/RegisterTypesLobbyPacks.h" // UUID generation #include From 01fe06b61bc65b5a49f38968ddd0342631e30878 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sat, 11 Nov 2023 20:04:48 +0100 Subject: [PATCH 1095/1248] Updated and clarified some info about Bonus types. --- docs/modders/Bonus/Bonus_Types.md | 168 +++++++++++++++--------------- 1 file changed, 84 insertions(+), 84 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 027bf2ac0..6b812fa78 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -6,13 +6,13 @@ The bonuses were grouped according to their original purpose. The bonus system a ### NONE -Special bonus that gives no effect +Bonus placeholder that gives no effect ### MORALE Changes morale of affected units -- val: change in morale +- val: change in morale, eg. 1, -2 ### LUCK @@ -29,7 +29,7 @@ Changes mastery level of spells of affected heroes and units. Examples are magic ### DARKNESS -On each turn, hides area in fog of war around affected town for all players other than town owner +On each turn, hides area in fog of war around affected town for all players other than town owner. Currently does not work for any entities other than towns. - val: radius in tiles @@ -40,21 +40,21 @@ On each turn, hides area in fog of war around affected town for all players othe Increases amount of movement points available to affected hero on new turn - subtype: -- - heroMovementLand: only land movement will be affected -- - heroMovementSea: only sea movement will be affected + - heroMovementLand: only land movement will be affected + - heroMovementSea: only sea movement will be affected - val: number of movement points (100 points for a tile) ### WATER_WALKING Allows movement over water for affected heroes -- val: TODO +- val: Penalty to movement, in percent (Basic Water Walk - 40, Advanced Water Walk - 20) ### FLYING_MOVEMENT Allows flying movement for affected heroes -- val: TODO +- val: Penalty to movement, in percent ### NO_TERRAIN_PENALTY @@ -62,11 +62,11 @@ Eliminates terrain penalty on certain terrain types for affected heroes (Nomads Note: to eliminate all terrain penalties see ROUGH_TERRAIN_DISCOUNT bonus -- subtype: type of terrain +- subtype: type of terrain, eg `terrain.sand` ### TERRAIN_NATIVE -Affected units will view any terrain as native +Affected units will view any terrain as native. This means army containing these creature will have no movement penalty, and will be able to see Mines and Quick Sand in battle. ### PRIMARY_SKILL @@ -93,38 +93,38 @@ Restores entire mana pool for affected heroes on new turn ### NONEVIL_ALIGNMENT_MIX -Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale +Allows mixing of creaturs of neutral and good factions in affected armies without penalty to morale (Angelic Alliance effect) ### SURRENDER_DISCOUNT Changes surrender cost for affected heroes -- val: decrease in cost, in precentage +- val: decrease in cost, in percentage ### IMPROVED_NECROMANCY -TODO: Determine units which is raised by necromancy skill. +Allows to raise different creatures than Skeletons after battle. - subtype: creature raised - val: Necromancer power -- addInfo: limiter by Necromancer power -- Example (from Necromancy skill): +- addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) +- Example (from Cloak Of The Undead King): -` "power" : {` -` "type" : "IMPROVED_NECROMANCY",` -` "subtype" : "creature.skeleton",` -` "addInfo" : 0` -` }` + { + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 + } ### LEARN_BATTLE_SPELL_CHANCE -Determines chance for affected heroes to learn spell casted by enemy hero after battle +Determines chance for affected heroes to learn spell cast by enemy hero after battle - val: chance to learn spell, percentage ### LEARN_BATTLE_SPELL_LEVEL_LIMIT -Allows affected heroes to learn spell casted by enemy hero after battle +Allows affected heroes to learn spell cast by enemy hero after battle - val: maximal level of spell that can be learned @@ -207,7 +207,7 @@ Defines maximum level of spells than hero can learn from any source (Wisdom) ### SPECIAL_SPELL_LEV -Gives additional bonus to effect of specific spell based on level of creature it is casted on +Gives additional bonus to effect of specific spell based on level of creature it is cast on - subtype: identifier of affected spell - val: bonus to spell effect, percentage, divided by target creature level @@ -228,25 +228,39 @@ Gives additional bonus to effect of specific spell ### SPECIAL_PECULIAR_ENCHANT -TODO: blesses and curses with id = val dependent on unit's level +Gives creature under effect of this spell additional bonus, which is hardcoded and depends on the creature tier. -- subtype: affected spell identifier +- subtype: affected spell identifier, ie. `spell.haste` ### SPECIAL_ADD_VALUE_ENCHANT -TODO: specialty spell like Aenin has, increased effect of spell, additionalInfo = value to add +Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray decrease target's defence by additional 2 points: + + "disruptingRay" : { + "addInfo" : -2, + "subtype" : "spell.disruptingRay", + "type" : "SPECIAL_ADD_VALUE_ENCHANT" + } - subtype: affected spell identifier +- additionalInfo: value to add ### SPECIAL_FIXED_VALUE_ENCHANT -TODO: specialty spell like Melody has, constant spell effect (i.e. 3 luck), additionalInfo = value to fix. +Spell affecting creature has fixed effect, eg. hero Melody has constant spell effect of +3: + + "fortune" : { + "addInfo" : 3, + "subtype" : "spell.fortune", + "type" : "SPECIAL_FIXED_VALUE_ENCHANT" + } - subtype: affected spell identifier +- additionalInfo = fixed value ### SPECIAL_UPGRADE -Allows creature upgrade for affected armies +Allows creature being upgraded to another creature (Gelu, Dracon) - subtype: identifier of creature that can being upgraded - addInfo: identifier of creature to which perform an upgrade @@ -255,7 +269,7 @@ Allows creature upgrade for affected armies ### SPELL_DURATION -Changes duration of timed spells casted by affected hero +Changes duration of timed spells cast by affected hero - val: additional duration, turns - subtype: optional, identifier of affected spells, or all if not set @@ -308,8 +322,8 @@ Heroes affected by this bonus can not retreat or surrender in battle Negates all natural immunities for affected stacks. (Orb of Vulnerability) - subtype: -- - immunityBattleWide: Entire battle will be affected by bonus -- - immunityEnemyHero: Only enemy hero will be affected by bonus + - immunityBattleWide: Entire battle will be affected by bonus + - immunityEnemyHero: Only enemy hero will be affected by bonus ### OPENING_BATTLE_SPELL @@ -320,7 +334,7 @@ In battle, army affected by this bonus will cast spell at the very start of the ### FREE_SHIP_BOARDING -Heroes affected by this bonus will keep all their movement points when embarking or disembarking ship +Heroes affected by this bonus will not lose all movement points when embarking or disembarking ship. Movement points are converted depending on max land and water movement range. ### WHIRLPOOL_PROTECTION @@ -345,9 +359,9 @@ Increases movement speed of units in battle Increases base damage of creature in battle - subtype: -- - creatureDamageMin: increases only minimal damage -- - creatureDamageMax: increases only maximal damage -- - creatureDamageBoth: increases both minimal and maximal damage + - creatureDamageMin: increases only minimal damage + - creatureDamageMax: increases only maximal damage + - creatureDamageBoth: increases both minimal and maximal damage - val: additional damage points ### SHOTS @@ -405,8 +419,8 @@ Affected units can not receive good or bad morale Affected unit can fly on the battlefield - subtype: -- - movementFlying: creature will fly (slowly move across battlefield) -- - movementTeleporting: creature will instantly teleport to destination + - movementFlying: creature will fly (slowly move across battlefield) + - movementTeleporting: creature will instantly teleport to destination ### SHOOTER @@ -480,9 +494,9 @@ Affected units will receive reduced damage from attacks by other units - val: damage reduction, percentage - subtype: -- - damageTypeMelee: only melee damage will be reduced -- - damageTypeRanged: only ranged damage will be reduced -- - damageTypeAll: all damage will be reduced + - damageTypeMelee: only melee damage will be reduced + - damageTypeRanged: only ranged damage will be reduced + - damageTypeAll: all damage will be reduced ### PERCENTAGE_DAMAGE_BOOST @@ -490,8 +504,8 @@ Affected units will deal increased damage when attacking other units - val: damage increase, percentage - subtype: -- - damageTypeMelee: only melee damage will increased -- - damageTypeRanged: only ranged damage will increased + - damageTypeMelee: only melee damage will increased + - damageTypeRanged: only ranged damage will increased ### GENERAL_ATTACK_REDUCTION @@ -519,7 +533,7 @@ Affected unit will deal full damage when shooting over walls in sieges. Does not ### FREE_SHOOTING -Affected unit can use ranged attack even when blocked by enemy unit. (Sharpshooter's Bow) +Affected unit can use ranged attack even when blocked by enemy unit, like with Bow of the Sharpshooter relic ### BLOCKS_RETALIATION @@ -531,8 +545,8 @@ Affected unit will gain new creatures for each enemy killed by this unit - val: number of units gained per enemy killed - subtype: -- - soulStealPermanent: creature will stay after the battle -- - soulStealBattle: creature will be lost after the battle + - soulStealPermanent: creature will stay after the battle + - soulStealBattle: creature will be lost after the battle ### TRANSMUTATION @@ -540,8 +554,8 @@ Affected units have chance to transform attacked unit to other creature type - val: chance for ability to trigger, percentage - subtype: -- - transmutationPerHealth: transformed unit will have same HP pool as original stack, -- - transmutationPerUnit: transformed unit will have same number of units as original stack + - transmutationPerHealth: transformed unit will have same HP pool as original stack, + - transmutationPerUnit: transformed unit will have same number of units as original stack - addInfo: creature to transform to. If not set, creature will transform to same unit as attacker ### SUMMON_GUARDIANS @@ -577,8 +591,8 @@ Affected unit will kills additional units after attack - val: chance to trigger, percentage - subtype: -- - destructionKillPercentage: kill percentage of units, -- - destructionKillAmount: kill amount + - destructionKillPercentage: kill percentage of units, + - destructionKillAmount: kill amount - addInfo: amount or percentage to kill ### LIMITED_SHOOTING_RANGE @@ -691,11 +705,11 @@ Affected unit will deal additional damage after attack Affected unit will kill additional units after attack - subtype: -- - deathStareGorgon: random amount -- - deathStareCommander: fixed amount + - deathStareGorgon: random amount + - deathStareCommander: fixed amount - val: -- - for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula -- - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val + - for deathStareGorgon: chance to kill, counted separately for each unit in attacking stack, percentage. At most (stack size \* chance) units can be killed at once. TODO: recheck formula + - for deathStareCommander: number of creatures to kill, total amount of killed creatures is (attacker level / defender level) \* val ### SPECIAL_CRYSTAL_GENERATION @@ -737,53 +751,41 @@ Determines how many times per combat affected creature can cast its targeted spe ### SPELL_AFTER_ATTACK -TODO: - -- subtype - spell id -- value - chance % +- subtype - spell id, eg. spell.iceBolt +- value - chance (percent) - additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only + - X - spell level + - Y = 0 - all attacks, 1 - shot only, 2 - melee only ### SPELL_BEFORE_ATTACK -TODO: - - subtype - spell id - value - chance % - additional info - \[X, Y\] -- X - spell level -- Y = 0 - all attacks, 1 - shot only, 2 - melee only + - X - spell level + - Y = 0 - all attacks, 1 - shot only, 2 - melee only -### SPECIFIC_SPELL_POWER +### SPECIFIC_SPELL_POWER -TODO: - -- value used for Thunderbolt and Resurrection casted by units, also for Healing secondary skill (for core:spell.firstAid used by First Aid tent) +- value: Used for Thunderbolt and Resurrection cast by units (multiplied by stack size). Also used for Healing secondary skill (for core:spell.firstAid used by First Aid tent) - subtype - spell id ### CREATURE_SPELL_POWER -TODO: - -- value per unit, divided by 100 (so faerie Dragons have 500) +- value: Spell Power of offensive spell cast unit, divided by 100. ie. Faerie Dragons have value fo 500, which gives them 5 Spell Power for each unit in the stack. ### CREATURE_ENCHANT_POWER -TODO: - -total duration of spells casted by creature + - val: Total duration of spells cast by creature, in turns ### REBIRTH Affected stack will resurrect after death -TODO: recheck math - -- val - percent of total stack HP restored +- val - percent of total stack HP restored, not rounded. For instance, when 4 Phoenixes with Rebirth chance of 20% die, there is 80% chance than one Phoenix will rise. - subtype: -- - rebirthRegular: (Phoenix) -- - rebirthSpecial: at least one unit will always resurrect (sacred Phoenix) + - rebirthRegular: Phoenix, as described above. + - rebirthSpecial: At least one unit will always rise (Sacred Phoenix) ### ENCHANTED @@ -798,9 +800,7 @@ Affected unit is permanently enchanted with a spell, that is cast again every tu Affected unit is immune to all spell with level below or equal to value of this bonus -- val: level up to which this unit is immune to - -TODO: additional info? +- val: level of spell up to which this unit is immune to ### MAGIC_RESISTANCE @@ -846,7 +846,7 @@ Affected unit can be affected by all friendly spells even it would be normally i ### POISON -TODO: describe +Unit affected by poison will lose 10% of max health every combat turn, up to `val`. After that, effect ends. - val - max health penalty from poison possible @@ -934,9 +934,9 @@ Affected heroes will be under effect of Visions spell, revealing information of - val: multiplier to effect range. Information is revealed within (val \* hero spell power) range - subtype: -- - visionsMonsters: reveal information on monsters, -- - visionsHeroes: reveal information on heroes, -- - visionsTowns: reveal information on towns + - visionsMonsters: reveal information on monsters, + - visionsHeroes: reveal information on heroes, + - visionsTowns: reveal information on towns ### BLOCK_MAGIC_BELOW From f105a34be50981219762404c9f75c6c3c030a021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 12 Nov 2023 05:57:39 +0100 Subject: [PATCH 1096/1248] defence -> defense json syntax --- docs/modders/Bonus/Bonus_Types.md | 50 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 6b812fa78..709e2c62d 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -110,11 +110,13 @@ Allows to raise different creatures than Skeletons after battle. - addInfo: Level of Necromancy secondary skill (1 - Basic, 3 - Expert) - Example (from Cloak Of The Undead King): - { - "type" : "IMPROVED_NECROMANCY", - "subtype" : "creature.walkingDead", - "addInfo" : 1 - } +```jsonc +{ + "type" : "IMPROVED_NECROMANCY", + "subtype" : "creature.walkingDead", + "addInfo" : 1 +} +``` ### LEARN_BATTLE_SPELL_CHANCE @@ -234,13 +236,15 @@ Gives creature under effect of this spell additional bonus, which is hardcoded a ### SPECIAL_ADD_VALUE_ENCHANT -Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray decrease target's defence by additional 2 points: +Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray decrease target's defense by additional 2 points: - "disruptingRay" : { - "addInfo" : -2, - "subtype" : "spell.disruptingRay", - "type" : "SPECIAL_ADD_VALUE_ENCHANT" - } +```jsonc +"disruptingRay" : { + "addInfo" : -2, + "subtype" : "spell.disruptingRay", + "type" : "SPECIAL_ADD_VALUE_ENCHANT" +} +`````` - subtype: affected spell identifier - additionalInfo: value to add @@ -249,11 +253,13 @@ Increased effect of spell affecting creature, ie. Aenain makes Disrupting Ray de Spell affecting creature has fixed effect, eg. hero Melody has constant spell effect of +3: - "fortune" : { - "addInfo" : 3, - "subtype" : "spell.fortune", - "type" : "SPECIAL_FIXED_VALUE_ENCHANT" - } +```jsonc +"fortune" : { + "addInfo" : 3, + "subtype" : "spell.fortune", + "type" : "SPECIAL_FIXED_VALUE_ENCHANT" +} +``` - subtype: affected spell identifier - additionalInfo = fixed value @@ -484,9 +490,9 @@ Affected unit can return to his starting location after attack (Harpies) ### ENEMY_DEFENCE_REDUCTION -Affected unit will ignore specified percentage of attacked unit defence (Behemoth) +Affected unit will ignore specified percentage of attacked unit defense (Behemoth) -- val: amount of defence points to ignore, percentage +- val: amount of defense points to ignore, percentage ### GENERAL_DAMAGE_REDUCTION @@ -515,9 +521,9 @@ Affected units will deal reduced damage when attacking other units (Blind or Par ### DEFENSIVE_STANCE -Affected units will receive increased bonus to defence while defending +Affected units will receive increased bonus to defense while defending -- val: additional bonus to defence, in skill points +- val: additional bonus to defense, in skill points ### NO_DISTANCE_PENALTY @@ -886,9 +892,9 @@ Affected unit can not be controlled by player and instead it will attempt to mov ### IN_FRENZY -Affected unit's defence is reduced to 0 and is transferred to attack with specified multiplier +Affected unit's defense is reduced to 0 and is transferred to attack with specified multiplier -- val: multiplier factor with which defence is transferred to attack (percentage) +- val: multiplier factor with which defense is transferred to attack (percentage) ### HYPNOTIZED From 805c922c801850fd0dc56a3539610cb1d262ac97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Sun, 12 Nov 2023 09:07:52 +0100 Subject: [PATCH 1097/1248] More detailed info with examples --- docs/modders/Bonus/Bonus_Types.md | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/modders/Bonus/Bonus_Types.md b/docs/modders/Bonus/Bonus_Types.md index 709e2c62d..6836066cc 100644 --- a/docs/modders/Bonus/Bonus_Types.md +++ b/docs/modders/Bonus/Bonus_Types.md @@ -311,17 +311,17 @@ Affected heroes will add specified resources amounts to player treasure on new d Increases weekly growth of creatures in affected towns (Legion artifacts) - value: number of additional weekly creatures -- subtype: dwelling level, in form "creatureLevelX" where X is desired level (1-7) +- subtype: dwelling level, in form `creatureLevelX` where X is desired level (1-7) ### CREATURE_GROWTH_PERCENT -Increases weekly growth of creatures in affected towns +Increases weekly growth of creatures in affected towns (Statue of Legion) - val: additional growth, percentage ### BATTLE_NO_FLEEING -Heroes affected by this bonus can not retreat or surrender in battle +Heroes affected by this bonus can not retreat or surrender in battle (Shackles of War effect) ### NEGATE_ALL_NATURAL_IMMUNITIES @@ -390,11 +390,11 @@ Affected unit is considered to be a gargoyle and not affected by certain spells ### UNDEAD -Affected unit is considered to be undead +Affected unit is considered to be undead, which makes it immune to many effects, and also reduce morale of allied living units. ### SIEGE_WEAPON -Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. +Affected unit is considered to be a siege machine and can not be raised, healed, have morale or move. All War Machines should have this bonus. ### DRAGON_NATURE @@ -426,7 +426,7 @@ Affected unit can fly on the battlefield - subtype: - movementFlying: creature will fly (slowly move across battlefield) - - movementTeleporting: creature will instantly teleport to destination + - movementTeleporting: creature will instantly teleport to destination, skipping movement animation. ### SHOOTER @@ -434,21 +434,21 @@ Affected unit can shoot ### CHARGE_IMMUNITY -Affected unit is immune to JOUSTING ability +Affected unit is immune to JOUSTING ability of (ie. Champions). ### ADDITIONAL_ATTACK -Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if can +Affected unit can perform additional attacks. Attacked unit will retaliate after each attack if able. - val: number of additional attacks to perform ### UNLIMITED_RETALIATIONS -Affected unit will always retaliate if able +Affected unit will always retaliate if able (Royal Griffin) ### ADDITIONAL_RETALIATION -Affected unit can retaliate multiple times per turn +Affected unit can retaliate multiple times per turn (basic Griffin) - value: number of additional retaliations @@ -462,7 +462,7 @@ Affected unit will deal more damage based on movement distance (Champions) Affected unit will deal more damage when attacking specific creature -- subtype - identifier of hated creature +- subtype - identifier of hated creature, ie. "creature.genie" - val - additional damage, percentage ### SPELL_LIKE_ATTACK @@ -612,7 +612,7 @@ Affected unit can use ranged attacks only within specified range ### CATAPULT -Affected unit can attack walls during siege battles +Affected unit can attack walls during siege battles (Cyclops) - subtype: spell that describes attack parameters @@ -649,7 +649,7 @@ All units adjacent to affected unit will receive additional spell resistance bon ### HP_REGENERATION -Affected unit will regenerate portion of its health points on its turn +Affected unit will regenerate portion of its health points on its turn. - val: amount of health points gained per round @@ -667,19 +667,19 @@ Affected unit will give his hero specified portion of mana points spent by enemy ### LIFE_DRAIN -Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage +Affected unit will heal itself, resurrecting any dead units, by amount of dealt damage (Vampire Lord) - val: percentage of damage that will be converted into health points ### DOUBLE_DAMAGE_CHANCE -Affected unit has chance to deal double damage on attack +Affected unit has chance to deal double damage on attack (Death Knight) - val: chance to trigger, percentage ### FEAR -If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance. Blocked by FEARLESS bonus. +If enemy army has creatures affected by this bonus, they will skip their turn with 10% chance (Azure Dragon). Blocked by FEARLESS bonus. ### HEALER From 142d0083d519c40fdffcb4212e3cf018d16e4c2a Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 12 Nov 2023 16:14:06 +0200 Subject: [PATCH 1098/1248] #3173 - fix trap crash on random hero --- lib/gameState/CGameState.cpp | 3 +-- lib/mapObjects/CGHeroInstance.cpp | 31 +++++++++++++++++-------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index beb8eeda4..0a70046fd 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -121,8 +121,7 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner) return *notAllowedHeroesButStillBetterThanCrash.begin(); logGlobal->error("No free heroes at all!"); - assert(0); //current code can't handle this situation - return HeroTypeID::NONE; // no available heroes at all + throw std::runtime_error("Can not allocate hero. All heroes are already used."); } int CGameState::getDate(Date mode) const diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 6fa16d8ce..8e0eec91f 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1531,22 +1531,25 @@ std::string CGHeroInstance::getHeroTypeName() const void CGHeroInstance::afterAddToMap(CMap * map) { - auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool - { - return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; - }); - - if(existingHero != map->objects.end()) + if(ID != Obj::RANDOM_HERO) { - if(settings["session"]["editor"].Bool()) - { - logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); - } - else - { - logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool + { + return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this; + }); - throw std::runtime_error("Hero is already on the map"); + if(existingHero != map->objects.end()) + { + if(settings["session"]["editor"].Bool()) + { + logGlobal->warn("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + } + else + { + logGlobal->error("Hero is already on the map at %s", (*existingHero)->visitablePos().toString()); + + throw std::runtime_error("Hero is already on the map"); + } } } From 7b92e23f3f0cb8ebe9e4b466fbb61733a28888e3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 16:55:42 +0200 Subject: [PATCH 1099/1248] Fix crash on right-clicking some visited configurable objects --- lib/mapObjects/CRewardableObject.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CRewardableObject.cpp b/lib/mapObjects/CRewardableObject.cpp index eaf9b797a..c05fbd9aa 100644 --- a/lib/mapObjects/CRewardableObject.cpp +++ b/lib/mapObjects/CRewardableObject.cpp @@ -301,7 +301,7 @@ std::string CRewardableObject::getDescriptionMessage(PlayerColor player, const C return configuration.description.toString(); auto rewardIndices = getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT); - if (rewardIndices.empty() && !configuration.info[0].description.empty()) + if (rewardIndices.empty() || !configuration.info[0].description.empty()) return configuration.info[0].description.toString(); if (!configuration.info[rewardIndices.front()].description.empty()) From 0b7c61e53ffdcbf788bc06943892c42bdb35d054 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 16:58:02 +0200 Subject: [PATCH 1100/1248] Fix Tree of Knowledge config --- config/objects/rewardableOncePerHero.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/objects/rewardableOncePerHero.json b/config/objects/rewardableOncePerHero.json index 43065ed8d..779575307 100644 --- a/config/objects/rewardableOncePerHero.json +++ b/config/objects/rewardableOncePerHero.json @@ -249,7 +249,7 @@ "description" : "@core.arraytxt.202", "message" : 148, "appearChance" : { "max" : 34 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { "description" : "@core.arraytxt.203", @@ -257,7 +257,7 @@ "appearChance" : { "min" : 34, "max" : 67 }, "limiter" : { "resources" : { "gold" : 2000 } }, "resources" : { "gold" : -2000 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, { "description" : "@core.arraytxt.204", @@ -265,7 +265,7 @@ "appearChance" : { "min" : 67 }, "limiter" : { "resources" : { "gems" : 10 } }, "resources" : { "gems" : -10 }, - "gainedLevels" : 1 + "heroLevel" : 1 }, ] } From 59806bf1120a7c01f9bbab8c8a39623354216b7e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 17:17:07 +0200 Subject: [PATCH 1101/1248] Fix right-click tooltips for starting hero/towns --- client/lobby/OptionsTab.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index e62bf26c4..a2f5027b0 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -561,27 +561,35 @@ void OptionsTab::CPlayerOptionTooltipBox::genHeader() void OptionsTab::CPlayerOptionTooltipBox::genTownWindow() { + auto factionIndex = playerSettings.getCastleValidated(); + + if (playerSettings.castle == FactionID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 228, 290); genHeader(); labelAssociatedCreatures = std::make_shared(pos.w / 2 + 8, 122, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[79]); - auto factionIndex = playerSettings.getCastleValidated(); std::vector> components; const CTown * town = (*CGI->townh)[factionIndex]->town; for(auto & elem : town->creatures) { if(!elem.empty()) - components.push_back(std::make_shared(ComponentType::CREATURE, elem.front(), 0, CComponent::tiny)); + components.push_back(std::make_shared(ComponentType::CREATURE, elem.front(), std::nullopt, CComponent::tiny)); } boxAssociatedCreatures = std::make_shared(components, Rect(10, 140, pos.w - 20, 140)); } void OptionsTab::CPlayerOptionTooltipBox::genHeroWindow() { + auto heroIndex = playerSettings.getHeroValidated(); + + if (playerSettings.hero == HeroTypeID::RANDOM) + return genBonusWindow(); + pos = Rect(0, 0, 292, 226); genHeader(); labelHeroSpeciality = std::make_shared(pos.w / 2 + 4, 117, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[78]); - auto heroIndex = playerSettings.getHeroValidated(); imageSpeciality = std::make_shared(AnimationPath::builtin("UN44"), (*CGI->heroh)[heroIndex]->imageIndex, 0, pos.w / 2 - 22, 134); labelSpecialityName = std::make_shared(pos.w / 2, 188, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, (*CGI->heroh)[heroIndex]->getSpecialtyNameTranslated()); From 833051ccedb0ce5a5672b359c5b32c95dd018b2e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 17:35:43 +0200 Subject: [PATCH 1102/1248] Display exit confirmation dialog on Alt-F4 --- client/CMT.cpp | 9 +++++++-- client/eventsSDL/InputHandler.cpp | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 8f38897f7..f3741bbbf 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -24,6 +24,7 @@ #include "CServerHandler.h" #include "ClientCommandManager.h" #include "windows/CMessage.h" +#include "windows/InfoWindows.h" #include "render/IScreenHandler.h" #include "render/Graphics.h" @@ -502,10 +503,14 @@ static void quitApplication() void handleQuit(bool ask) { - if(CSH->client && LOCPLINT && ask) + if(ask) { CCS->curh->set(Cursor::Map::POINTER); - LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + + if (LOCPLINT) + LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr); + else + CInfoWindow::showYesNoDialog(CGI->generaltexth->allTexts[69], {}, quitApplication, {}, PlayerColor(1)); } else { diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 23aa2ea25..257ee70d0 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -116,7 +116,11 @@ void InputHandler::preprocessEvent(const SDL_Event & ev) if(ev.type == SDL_QUIT) { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); +#ifdef VCMI_ANDROID handleQuit(false); +#else + handleQuit(true); +#endif return; } else if(ev.type == SDL_KEYDOWN) From 05e861c86269432f541d6beae76949d9dead937a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 17:42:03 +0200 Subject: [PATCH 1103/1248] Added tooltip for revisit object button --- Mods/vcmi/config/vcmi/english.json | 3 +++ Mods/vcmi/config/vcmi/ukrainian.json | 3 +++ 2 files changed, 6 insertions(+) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 079e3de1a..1d0432297 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -154,6 +154,9 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Show heroes statistics windows}\n\nPermanently toggle on heroes statistics windows that show primary stats and spell points.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Skip Intro Music}\n\nAllow actions during the intro music that plays at the beginning of each battle.", + + "vcmi.adventureMap.revisitObject.hover" : "Revisit Object", + "vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Press any key to start battle immediately", "vcmi.battleWindow.damageEstimation.melee" : "Attack %CREATURE (%DAMAGE).", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 9773fc7af..81a7244db 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -130,6 +130,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.", + "vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт", + "vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Натисніть будь-яку клавішу, щоб розпочати бій", "vcmi.battleWindow.damageEstimation.melee" : "Атакувати %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Атакувати %CREATURE (%DAMAGE, %KILLS).", From 071fb97b339b5b09b1e6682fbba83c246e76f0eb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 17:50:52 +0200 Subject: [PATCH 1104/1248] Fix tooltip of visited banks after leaving some troops for banks with creatures as reward --- lib/mapObjects/CBank.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 9f32ae1c2..14ca4b93e 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -70,6 +70,9 @@ std::vector CBank::getPopupComponents(PlayerColor player) const if (!VLC->settings()->getBoolean(EGameSettings::BANKS_SHOW_GUARDS_COMPOSITION)) return {}; + if (bc == nullptr) + return {}; + std::map guardsAmounts; std::vector result; From 1d430d03280637737530d82df33af5629f353a28 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 19:18:00 +0200 Subject: [PATCH 1105/1248] Fix initialization order of Seer Huts and Creatures - Seer Hut might be initialized before creature that its quest is linked to --- lib/mapObjects/CQuest.cpp | 15 +++++++-------- lib/mapObjects/CQuest.h | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 957c81757..ed1871ef1 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -208,9 +208,9 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com addKillTargetReplacements(text); } - if(killTarget != ObjectInstanceID::NONE && stackToKill.type) + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) { - components.emplace_back(ComponentType::CREATURE, stackToKill.getId(), stackToKill.getCount()); + components.emplace_back(ComponentType::CREATURE, stackToKill); addKillTargetReplacements(text); } @@ -314,7 +314,7 @@ void CQuest::defineQuestName() if(!mission.spells.empty()) questName = CQuest::missionName(2); if(!mission.secondary.empty()) questName = CQuest::missionName(2); if(killTarget != ObjectInstanceID::NONE && !heroName.empty()) questName = CQuest::missionName(3); - if(killTarget != ObjectInstanceID::NONE && stackToKill.getType()) questName = CQuest::missionName(4); + if(killTarget != ObjectInstanceID::NONE && stackToKill != CreatureID::NONE) questName = CQuest::missionName(4); if(!mission.artifacts.empty()) questName = CQuest::missionName(5); if(!mission.creatures.empty()) questName = CQuest::missionName(6); if(mission.resources.nonZero()) questName = CQuest::missionName(7); @@ -327,9 +327,9 @@ void CQuest::addKillTargetReplacements(MetaString &out) const { if(!heroName.empty()) out.replaceTextID(heroName); - if(stackToKill.type) + if(stackToKill != CreatureID::NONE) { - out.replaceName(stackToKill); + out.replaceNamePlural(stackToKill); out.replaceRawString(VLC->generaltexth->arraytxt[147+stackDirection]); } } @@ -429,9 +429,8 @@ void CGSeerHut::setObjToKill() if(getCreatureToKill(true)) { - quest->stackToKill = getCreatureToKill(false)->getStack(SlotID(0)); //FIXME: stacks tend to disappear (desync?) on server :? - assert(quest->stackToKill.type); - quest->stackToKill.count = 0; //no count in info window + quest->stackToKill = getCreatureToKill(false)->getCreature(); + assert(quest->stackToKill != CreatureID::NONE); quest->stackDirection = checkDirection(); } else if(getHeroToKill(true)) diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 3dba2903d..bfeaecd97 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -40,7 +40,7 @@ public: // needed for messages / hover text ui8 textOption; ui8 completedOption; - CStackBasicDescriptor stackToKill; + CreatureID stackToKill; ui8 stackDirection; std::string heroName; //backup of hero name HeroTypeID heroPortrait; From bbd33127059029b35aad524f4d2782ce98c2966f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 19:18:17 +0200 Subject: [PATCH 1106/1248] Fix randomization of heroes in campaigns --- lib/gameState/CGameStateCampaign.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index f67ed7311..6ba1a44d6 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -217,6 +217,10 @@ void CGameStateCampaign::placeCampaignHeroes() for(auto & heroID : heroesToRemove) { + // Do not replace reserved heroes initially, e.g. in 1st campaign scenario in which they appear + if (campaignState->getHeroByType(heroID).isNull()) + continue; + auto * hero = gameState->getUsedHero(heroID); if(hero) { From 35ee8c1397e43a3562bbfb3b38619eac9f4d62e2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 19:30:33 +0200 Subject: [PATCH 1107/1248] Only apply normal mana regen for retreated heroes in taverns (h3 logic) --- lib/gameState/TavernHeroesPool.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gameState/TavernHeroesPool.cpp b/lib/gameState/TavernHeroesPool.cpp index 00aec3a73..40161ff2e 100644 --- a/lib/gameState/TavernHeroesPool.cpp +++ b/lib/gameState/TavernHeroesPool.cpp @@ -126,7 +126,7 @@ void TavernHeroesPool::onNewDay() continue; hero.second->setMovementPoints(hero.second->movementPointsLimit(true)); - hero.second->mana = hero.second->manaLimit(); + hero.second->mana = hero.second->getManaNewTurn(); } } From 9c9127be7d0a37f0116f6420b07dce6e717797a8 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 00:08:14 +0200 Subject: [PATCH 1108/1248] Fix tooltip for spell scrolls on adventure map --- lib/ArtifactUtils.cpp | 7 +++++-- lib/mapObjects/MiscObjects.cpp | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index 85a72bf66..2a5eeeb0a 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -254,10 +254,13 @@ DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description // However other language versions don't have name placeholder at all, so we have to be careful auto nameStart = description.find_first_of('['); auto nameEnd = description.find_first_of(']', nameStart); - if(sid.getNum() >= 0) + + if(nameStart != std::string::npos && nameEnd != std::string::npos) { - if(nameStart != std::string::npos && nameEnd != std::string::npos) + if(sid.getNum() >= 0) description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toEntity(VLC->spells())->getNameTranslated()); + else + description = description.erase(nameStart, nameEnd - nameStart + 2); // erase "[spell name] " - including space } } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 6583478aa..fda428a9a 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -779,7 +779,8 @@ std::string CGArtifact::getPopupText(PlayerColor player) const if (settings["general"]["enableUiEnhancements"].Bool()) { std::string description = VLC->artifacts()->getById(getArtifact())->getDescriptionTranslated(); - ArtifactUtils::insertScrrollSpellName(description, SpellID::NONE); // erase text placeholder + if (getArtifact() == ArtifactID::SPELL_SCROLL) + ArtifactUtils::insertScrrollSpellName(description, SpellID::NONE); // erase text placeholder return description; } else From c0c16fed75006c4bbd52205ad2b564dbc04fc383 Mon Sep 17 00:00:00 2001 From: Alexander Wilms Date: Mon, 13 Nov 2023 00:04:42 +0100 Subject: [PATCH 1109/1248] Move FPS counter to bottom left, set width of black background to suffice for 3 digit FPS Fixes #3171 --- client/gui/CGuiHandler.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index db3838b61..c8a103e58 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -183,11 +183,17 @@ Point CGuiHandler::screenDimensions() const void CGuiHandler::drawFPSCounter() { - static SDL_Rect overlay = { 0, 0, 24, 24}; + int x = 7; + int y = screen->h-20; + int width3digitFPSIncludingPadding = 48; + int heightFPSTextIncludingPadding = 11; + static SDL_Rect overlay = { x, y, width3digitFPSIncludingPadding, heightFPSTextIncludingPadding}; uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10); SDL_FillRect(screen, &overlay, black); - std::string fps = std::to_string(framerate().getFramerate()); - graphics->fonts[FONT_BIG]->renderTextLeft(screen, fps, Colors::YELLOW, Point(4, 2)); + + std::string fps = std::to_string(framerate().getFramerate())+" FPS"; + + graphics->fonts[FONT_SMALL]->renderTextLeft(screen, fps, Colors::WHITE, Point(8, screen->h-22)); } bool CGuiHandler::amIGuiThread() From 20ef3a69e74655a87737e1e285f6d7a83a9bfa7b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 12:09:55 +0200 Subject: [PATCH 1110/1248] Fix most of memleaks discovered by valgrind --- AI/Nullkiller/Pathfinding/AINodeStorage.cpp | 2 +- client/CMT.cpp | 8 +++++--- client/eventsSDL/InputHandler.cpp | 2 ++ lib/CGameInfoCallback.cpp | 10 +++++----- lib/CGameInfoCallback.h | 2 +- lib/CPlayerState.h | 2 +- lib/ObstacleHandler.cpp | 2 -- lib/VCMI_Lib.cpp | 10 ++++++++++ lib/gameState/CGameState.cpp | 4 ++-- lib/mapObjects/CGHeroInstance.cpp | 18 +++++++++++++++--- lib/mapObjects/CGObjectInstance.cpp | 6 +++++- lib/mapping/CMap.cpp | 3 +++ lib/networkPacks/NetPacksLib.cpp | 4 ++-- lib/pathfinder/NodeStorage.cpp | 2 +- lib/pathfinder/PathfinderUtil.h | 4 ++-- lib/serializer/BinaryDeserializer.h | 2 +- lib/spells/AdventureSpellMechanics.cpp | 2 +- server/CGameHandler.cpp | 2 +- 18 files changed, 58 insertions(+), 27 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index f40aa89dd..7e029fe64 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -90,7 +90,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta //TODO: fix this code duplication with NodeStorage::initialize, problem is to keep `resetTile` inline const PlayerColor fowPlayer = ai->playerID; - const auto fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(fowPlayer)->fogOfWarMap; const int3 sizes = gs->getMapSize(); //Each thread gets different x, but an array of y located next to each other in memory diff --git a/client/CMT.cpp b/client/CMT.cpp index f3741bbbf..45199effc 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -346,7 +346,6 @@ int main(int argc, char * argv[]) session["autoSkip"].Bool() = vm.count("autoSkip"); session["oneGoodAI"].Bool() = vm.count("oneGoodAI"); session["aiSolo"].Bool() = false; - std::shared_ptr mmenu; if(vm.count("testmap")) { @@ -362,7 +361,7 @@ int main(int argc, char * argv[]) } else { - mmenu = CMainMenu::create(); + auto mmenu = CMainMenu::create(); GH.curInt = mmenu.get(); } @@ -399,7 +398,7 @@ int main(int argc, char * argv[]) //start lobby immediately names.push_back(session["username"].String()); ESelectionScreen sscreen = session["gamemode"].Integer() == 0 ? ESelectionScreen::newGame : ESelectionScreen::loadGame; - mmenu->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); + CMM->openLobby(sscreen, session["host"].Bool(), &names, ELoadMode::MULTI); } // Restore remote session - start game immediately @@ -472,6 +471,9 @@ static void quitApplication() CCS->musich->release(); CCS->soundh->release(); + delete CCS->consoleh; + delete CCS->curh; + vstd::clear_pointer(CCS); } CMessage::dispose(); diff --git a/client/eventsSDL/InputHandler.cpp b/client/eventsSDL/InputHandler.cpp index 257ee70d0..2db1020a9 100644 --- a/client/eventsSDL/InputHandler.cpp +++ b/client/eventsSDL/InputHandler.cpp @@ -316,6 +316,8 @@ void InputHandler::handleUserEvent(const SDL_UserEvent & current) auto heapFunctor = static_cast*>(current.data1); (*heapFunctor)(); + + delete heapFunctor; } const Point & InputHandler::getCursorPosition() const diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 44de7bee3..e69dfa0ea 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -726,11 +726,11 @@ CGameInfoCallback::CGameInfoCallback(CGameState * GS): { } -std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const -{ - //boost::shared_lock lock(*gs->mx); - return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap; -} +//std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const +//{ +// //boost::shared_lock lock(*gs->mx); +// return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap; +//} int CPlayerSpecificInfoCallback::howManyTowns() const { diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 794719187..2ab121e08 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -247,7 +247,7 @@ public: virtual int getResourceAmount(GameResID type) const; virtual TResources getResourceAmount() const; - virtual std::shared_ptr> getVisibilityMap() const; //returns visibility map + //virtual std::shared_ptr> getVisibilityMap() const; //returns visibility map //virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; }; diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 66c70619a..7f31ff8ee 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -98,7 +98,7 @@ public: TeamID id; //position in gameState::teams std::set players; // members of this team //TODO: boost::array, bool if possible - std::shared_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden + std::unique_ptr> fogOfWarMap; //[z][x][y] true - visible, false - hidden TeamState(); TeamState(TeamState && other) noexcept; diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index fbfc7efe2..deb87f804 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -106,8 +106,6 @@ ObstacleInfo * ObstacleHandler::loadFromJson(const std::string & scope, const Js info->isAbsoluteObstacle = json["absolute"].Bool(); info->isForegroundObstacle = json["foreground"].Bool(); - objects.emplace_back(info); - return info; } diff --git a/lib/VCMI_Lib.cpp b/lib/VCMI_Lib.cpp index 3a4dea264..f297e5dff 100644 --- a/lib/VCMI_Lib.cpp +++ b/lib/VCMI_Lib.cpp @@ -259,6 +259,11 @@ void LibClasses::clear() delete battlefieldsHandler; delete generaltexth; delete identifiersHandler; + delete obstacleHandler; + delete terrainTypeHandler; + delete riverTypeHandler; + delete roadTypeHandler; + delete settingsHandler; makeNull(); } @@ -282,6 +287,11 @@ void LibClasses::makeNull() #endif battlefieldsHandler = nullptr; identifiersHandler = nullptr; + obstacleHandler = nullptr; + terrainTypeHandler = nullptr; + riverTypeHandler = nullptr; + roadTypeHandler = nullptr; + settingsHandler = nullptr; } LibClasses::LibClasses() diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index beb8eeda4..b2301be4f 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -700,7 +700,7 @@ void CGameState::initFogOfWar() int layers = map->levels(); for(auto & elem : teams) { - auto fow = elem.second.fogOfWarMap; + auto & fow = elem.second.fogOfWarMap; fow->resize(boost::extents[layers][map->width][map->height]); std::fill(fow->data(), fow->data() + fow->num_elements(), 0); @@ -1952,7 +1952,7 @@ bool RumorState::update(int id, int extra) TeamState::TeamState() { setNodeType(TEAM); - fogOfWarMap = std::make_shared>(); + fogOfWarMap = std::make_unique>(); } TeamState::TeamState(TeamState && other) noexcept: diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 6fa16d8ce..7f8818121 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -327,13 +327,21 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) { // hero starts with default spellbook presence status if(!getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) - putArtifact(ArtifactPosition::SPELLBOOK, ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK); + cb->gameState()->map->addNewArtifactInstance(artifact); + putArtifact(ArtifactPosition::SPELLBOOK, artifact); + } } else spells -= SpellID::SPELLBOOK_PRESET; if(!getArt(ArtifactPosition::MACH4)) - putArtifact(ArtifactPosition::MACH4, ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT)); //everyone has a catapult + { + auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT); + cb->gameState()->map->addNewArtifactInstance(artifact); + putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult + } if(!hasBonus(Selector::sourceType()(BonusSource::HERO_BASE_SKILL))) { @@ -448,7 +456,11 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) ArtifactPosition slot = art->getPossibleSlots().at(ArtBearer::HERO).front(); if(!getArt(slot)) - putArtifact(slot, ArtifactUtils::createNewArtifactInstance(aid)); + { + auto artifact = ArtifactUtils::createNewArtifactInstance(aid); + cb->gameState()->map->addNewArtifactInstance(artifact); + putArtifact(slot, artifact); + } else logGlobal->warn("Hero %s already has artifact at %d, omitting giving artifact %d", getNameTranslated(), slot.toEnum(), aid.toEnum()); } diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 167e24c75..de38be9d0 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -35,10 +35,14 @@ CGObjectInstance::CGObjectInstance(): tempOwner(PlayerColor::UNFLAGGABLE), blockVisit(false) { + logGlobal->debug("Created object at %d", ptrdiff_t(this)); } //must be instantiated in .cpp file for access to complete types of all member fields -CGObjectInstance::~CGObjectInstance() = default; +CGObjectInstance::~CGObjectInstance() +{ + logGlobal->debug("Deleted object %d at %d", id.getNum(), ptrdiff_t(this)); +} MapObjectID CGObjectInstance::getObjGroupIndex() const { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index e116bf4e6..72839c594 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -181,6 +181,9 @@ CMap::~CMap() for(auto quest : quests) quest.dellNull(); + for(auto artInstance : artInstances) + artInstance.dellNull(); + resetStaticData(); } diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index ac5488e35..9bfe3e96c 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -932,7 +932,7 @@ void SetMovePoints::applyGs(CGameState * gs) const void FoWChange::applyGs(CGameState *gs) { TeamState * team = gs->getPlayerTeam(player); - auto fogOfWarMap = team->fogOfWarMap; + auto & fogOfWarMap = team->fogOfWarMap; for(const int3 & t : tiles) (*fogOfWarMap)[t.z][t.x][t.y] = mode != ETileVisibility::HIDDEN; @@ -1327,7 +1327,7 @@ void TryMoveHero::applyGs(CGameState *gs) gs->map->addBlockVisTiles(h); } - auto fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; + auto & fogOfWarMap = gs->getPlayerTeam(h->getOwner())->fogOfWarMap; for(const int3 & t : fowRevealed) (*fogOfWarMap)[t.z][t.x][t.y] = 1; } diff --git a/lib/pathfinder/NodeStorage.cpp b/lib/pathfinder/NodeStorage.cpp index 38936dcf5..67c27fe68 100644 --- a/lib/pathfinder/NodeStorage.cpp +++ b/lib/pathfinder/NodeStorage.cpp @@ -27,7 +27,7 @@ void NodeStorage::initialize(const PathfinderOptions & options, const CGameState int3 pos; const PlayerColor player = out.hero->tempOwner; const int3 sizes = gs->getMapSize(); - const auto fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; + const auto & fow = static_cast(gs)->getPlayerTeam(player)->fogOfWarMap; //make 200% sure that these are loop invariants (also a bit shorter code), let compiler do the rest(loop unswitching) const bool useFlying = options.useFlying; diff --git a/lib/pathfinder/PathfinderUtil.h b/lib/pathfinder/PathfinderUtil.h index 49efd7833..749cc513e 100644 --- a/lib/pathfinder/PathfinderUtil.h +++ b/lib/pathfinder/PathfinderUtil.h @@ -19,11 +19,11 @@ VCMI_LIB_NAMESPACE_BEGIN namespace PathfinderUtil { - using FoW = std::shared_ptr>; + using FoW = std::unique_ptr>; using ELayer = EPathfindingLayer; template - EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, FoW fow, const PlayerColor player, const CGameState * gs) + EPathAccessibility evaluateAccessibility(const int3 & pos, const TerrainTile & tinfo, const FoW & fow, const PlayerColor player, const CGameState * gs) { if(!(*fow)[pos.z][pos.x][pos.y]) return EPathAccessibility::BLOCKED; diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 1c4858469..0f880baaf 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -292,7 +292,7 @@ public: { typedef typename std::remove_pointer::type npT; typedef typename std::remove_const::type ncpT; - data = ClassObjectCreator::invoke(); + data = ClassObjectCreator::invoke();// !!! ptrAllocated(data, pid); load(*data); } diff --git a/lib/spells/AdventureSpellMechanics.cpp b/lib/spells/AdventureSpellMechanics.cpp index b6b440d02..6e8b02233 100644 --- a/lib/spells/AdventureSpellMechanics.cpp +++ b/lib/spells/AdventureSpellMechanics.cpp @@ -600,7 +600,7 @@ ESpellCastResult ViewMechanics::applyAdventureEffects(SpellCastEnvironment * env const auto spellLevel = parameters.caster->getSpellSchoolLevel(owner); - const auto fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; + const auto & fowMap = env->getCb()->getPlayerTeam(parameters.caster->getCasterOwner())->fogOfWarMap; for(const CGObjectInstance * obj : env->getMap()->objects) { diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 0d69c0c16..b9011b4db 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -860,7 +860,7 @@ void CGameHandler::onNewTurn() fw.mode = ETileVisibility::REVEALED; fw.player = player; // find all hidden tiles - const auto fow = getPlayerTeam(player)->fogOfWarMap; + const auto & fow = getPlayerTeam(player)->fogOfWarMap; auto shape = fow->shape(); for(size_t z = 0; z < shape[0]; z++) From a6f37b7cd74eebe1b13afad36fc6f3198dfdf51c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 13:43:46 +0200 Subject: [PATCH 1111/1248] Fixed few more memory leaks in client --- client/CMT.cpp | 1 + lib/networkPacks/NetPacksBase.h | 4 +++- lib/serializer/BinaryDeserializer.cpp | 5 +++++ lib/serializer/BinaryDeserializer.h | 6 +----- lib/serializer/Connection.cpp | 6 ------ server/CVCMIServer.cpp | 1 + 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 45199effc..01fd32bb6 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -481,6 +481,7 @@ static void quitApplication() vstd::clear_pointer(graphics); } + vstd::clear_pointer(CSH); vstd::clear_pointer(VLC); vstd::clear_pointer(console);// should be removed after everything else since used by logging diff --git a/lib/networkPacks/NetPacksBase.h b/lib/networkPacks/NetPacksBase.h index dc22af57e..b77e92e94 100644 --- a/lib/networkPacks/NetPacksBase.h +++ b/lib/networkPacks/NetPacksBase.h @@ -20,7 +20,9 @@ class ICPackVisitor; struct DLL_LINKAGE CPack { - std::shared_ptr c; // Pointer to connection that pack received from + /// Pointer to connection that pack received from + /// Only set & used on server + std::shared_ptr c; CPack() = default; virtual ~CPack() = default; diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 0cb95f14a..6efa9ed18 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -23,4 +23,9 @@ BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) registerTypes(*this); } +BinaryDeserializer::~BinaryDeserializer() +{ + +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 0f880baaf..09cd89efe 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -140,12 +140,12 @@ public: si32 fileVersion; std::map loadedPointers; - std::map loadedPointersTypes; std::map> loadedSharedPointers; bool smartPointerSerialization; bool saving; BinaryDeserializer(IBinaryReader * r); + ~BinaryDeserializer(); template BinaryDeserializer & operator&(T & t) @@ -279,7 +279,6 @@ public: { // We already got this pointer // Cast it in case we are loading it to a non-first base pointer - assert(loadedPointersTypes.count(pid)); data = static_cast(i->second); return; } @@ -313,10 +312,7 @@ public: void ptrAllocated(const T *ptr, ui32 pid) { if(smartPointerSerialization && pid != 0xffffffff) - { - loadedPointersTypes[pid] = &typeid(T); loadedPointers[pid] = (void*)ptr; //add loaded pointer to our lookup map; cast is to avoid errors with const T* pt - } } template void registerType(const Base * b = nullptr, const Derived * d = nullptr) diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index a4ad78056..c712c383b 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -269,13 +269,7 @@ CPack * CConnection::retrievePack() iser & pack; logNetwork->trace("Received CPack of type %s", (pack ? typeid(*pack).name() : "nullptr")); if(pack == nullptr) - { logNetwork->error("Received a nullptr CPack! You should check whether client and server ABI matches."); - } - else - { - pack->c = this->shared_from_this(); - } enableBufferedRead = false; diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index e9651f91c..46b8be27f 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -445,6 +445,7 @@ void CVCMIServer::threadHandleClient(std::shared_ptr c) try { pack = c->retrievePack(); + pack->c = c; } catch(boost::system::system_error & e) { From af0afb251e0a9eff37d2a6809818cd17cd96468c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 15:24:14 +0200 Subject: [PATCH 1112/1248] Correctly shutdown client media handlers --- client/CMT.cpp | 3 +++ client/CMusicHandler.h | 7 ++++--- client/CVideoHandler.h | 5 +++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/client/CMT.cpp b/client/CMT.cpp index 01fd32bb6..b6ca35859 100644 --- a/client/CMT.cpp +++ b/client/CMT.cpp @@ -473,6 +473,9 @@ static void quitApplication() delete CCS->consoleh; delete CCS->curh; + delete CCS->videoh; + delete CCS->musich; + delete CCS->soundh; vstd::clear_pointer(CCS); } diff --git a/client/CMusicHandler.h b/client/CMusicHandler.h index 44f39673c..f0b653e7c 100644 --- a/client/CMusicHandler.h +++ b/client/CMusicHandler.h @@ -23,8 +23,9 @@ protected: bool initialized; int volume; // from 0 (mute) to 100 -public: CAudioBase(): initialized(false), volume(0) {}; + ~CAudioBase() = default; +public: virtual void init() = 0; virtual void release() = 0; @@ -32,7 +33,7 @@ public: ui32 getVolume() const { return volume; }; }; -class CSoundHandler: public CAudioBase +class CSoundHandler final : public CAudioBase { private: //update volume on configuration change @@ -124,7 +125,7 @@ public: bool stop(int fade_ms=0); }; -class CMusicHandler: public CAudioBase +class CMusicHandler final: public CAudioBase { private: //update volume on configuration change diff --git a/client/CVideoHandler.h b/client/CVideoHandler.h index e39d9445c..211b9db3f 100644 --- a/client/CVideoHandler.h +++ b/client/CVideoHandler.h @@ -31,6 +31,7 @@ public: class IMainVideoPlayer : public IVideoPlayer { public: + virtual ~IMainVideoPlayer() = default; virtual void update(int x, int y, SDL_Surface *dst, bool forceRedraw, bool update = true, std::function restart = nullptr){} virtual bool openAndPlayVideo(const VideoPath & name, int x, int y, bool stopOnKey = false, bool scale = false) { @@ -39,7 +40,7 @@ public: virtual std::pair, si64> getAudio(const VideoPath & videoToOpen) { return std::make_pair(nullptr, 0); }; }; -class CEmptyVideoPlayer : public IMainVideoPlayer +class CEmptyVideoPlayer final : public IMainVideoPlayer { public: int curFrame() const override {return -1;}; @@ -64,7 +65,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CInputStream; VCMI_LIB_NAMESPACE_END -class CVideoPlayer : public IMainVideoPlayer +class CVideoPlayer final : public IMainVideoPlayer { int stream; // stream index in video AVFormatContext *format; From d4496c81f92daa26e6f94630457b15891b1cdd36 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 16:02:22 +0200 Subject: [PATCH 1113/1248] Fix memory leaks in library --- lib/CTownHandler.cpp | 1 + lib/gameState/CGameState.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index d9541d545..a064bb0b1 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -318,6 +318,7 @@ CTownHandler::CTownHandler(): CTownHandler::~CTownHandler() { delete randomTown; + delete randomFaction; } JsonNode readBuilding(CLegacyConfigParser & parser) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index b2301be4f..23c9792b7 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -166,6 +166,8 @@ CGameState::~CGameState() // explicitly delete all ongoing battles first - BattleInfo destructor requires valid CGameState currentBattles.clear(); map.dellNull(); + scenarioOps.dellNull(); + initialOpts.dellNull(); } void CGameState::preInit(Services * services) From 301ac2457aa2f9cac05de8a6ee977e85c188af0a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 16:37:02 +0200 Subject: [PATCH 1114/1248] Cleanup --- client/CServerHandler.cpp | 3 ++- client/mapView/MapRenderer.cpp | 6 ++++++ client/mapView/MapRendererContextState.cpp | 2 ++ lib/CGameInfoCallback.cpp | 6 ------ lib/CGameInfoCallback.h | 2 -- lib/CTownHandler.cpp | 3 +-- lib/mapObjects/CGObjectInstance.cpp | 6 +----- lib/serializer/BinaryDeserializer.cpp | 5 ----- lib/serializer/BinaryDeserializer.h | 3 +-- 9 files changed, 13 insertions(+), 23 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 361382a17..4909ecf82 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -1021,7 +1021,8 @@ void CServerHandler::threadRunServer() void CServerHandler::onServerFinished() { threadRunLocalServer.reset(); - CSH->campaignServerRestartLock.setn(false); + if (CSH) + CSH->campaignServerRestartLock.setn(false); } void CServerHandler::sendLobbyPack(const CPackForLobby & pack) const diff --git a/client/mapView/MapRenderer.cpp b/client/mapView/MapRenderer.cpp index 6a3947ce9..ebe2dbf14 100644 --- a/client/mapView/MapRenderer.cpp +++ b/client/mapView/MapRenderer.cpp @@ -134,8 +134,10 @@ std::shared_ptr MapTileStorage::find(size_t fileIndex, size_t rotationIn MapRendererTerrain::MapRendererTerrain() : storage(VLC->terrainTypeHandler->objects.size()) { + logGlobal->debug("Loading map terrains"); for(const auto & terrain : VLC->terrainTypeHandler->objects) storage.load(terrain->getIndex(), terrain->tilesFilename, EImageBlitMode::OPAQUE); + logGlobal->debug("Done loading map terrains"); } void MapRendererTerrain::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -173,8 +175,10 @@ uint8_t MapRendererTerrain::checksum(IMapRendererContext & context, const int3 & MapRendererRiver::MapRendererRiver() : storage(VLC->riverTypeHandler->objects.size()) { + logGlobal->debug("Loading map rivers"); for(const auto & river : VLC->riverTypeHandler->objects) storage.load(river->getIndex(), river->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map rivers"); } void MapRendererRiver::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) @@ -208,8 +212,10 @@ uint8_t MapRendererRiver::checksum(IMapRendererContext & context, const int3 & c MapRendererRoad::MapRendererRoad() : storage(VLC->roadTypeHandler->objects.size()) { + logGlobal->debug("Loading map roads"); for(const auto & road : VLC->roadTypeHandler->objects) storage.load(road->getIndex(), road->tilesFilename, EImageBlitMode::COLORKEY); + logGlobal->debug("Done loading map roads"); } void MapRendererRoad::renderTile(IMapRendererContext & context, Canvas & target, const int3 & coordinates) diff --git a/client/mapView/MapRendererContextState.cpp b/client/mapView/MapRendererContextState.cpp index 196ff9671..aa1a6ab0a 100644 --- a/client/mapView/MapRendererContextState.cpp +++ b/client/mapView/MapRendererContextState.cpp @@ -34,8 +34,10 @@ MapRendererContextState::MapRendererContextState() objects.resize(boost::extents[mapSize.z][mapSize.x][mapSize.y]); + logGlobal->debug("Loading map objects"); for(const auto & obj : CGI->mh->getMap()->objects) addObject(obj); + logGlobal->debug("Done loading map objects"); } void MapRendererContextState::addObject(const CGObjectInstance * obj) diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index e69dfa0ea..aa810e90f 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -726,12 +726,6 @@ CGameInfoCallback::CGameInfoCallback(CGameState * GS): { } -//std::shared_ptr> CPlayerSpecificInfoCallback::getVisibilityMap() const -//{ -// //boost::shared_lock lock(*gs->mx); -// return gs->getPlayerTeam(*getPlayerID())->fogOfWarMap; -//} - int CPlayerSpecificInfoCallback::howManyTowns() const { //boost::shared_lock lock(*gs->mx); diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 2ab121e08..494ab54cb 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -247,8 +247,6 @@ public: virtual int getResourceAmount(GameResID type) const; virtual TResources getResourceAmount() const; - //virtual std::shared_ptr> getVisibilityMap() const; //returns visibility map - //virtual const PlayerSettings * getPlayerSettings(PlayerColor color) const; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index a064bb0b1..c739d3c7c 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -317,8 +317,7 @@ CTownHandler::CTownHandler(): CTownHandler::~CTownHandler() { - delete randomTown; - delete randomFaction; + delete randomFaction; // will also delete randomTown } JsonNode readBuilding(CLegacyConfigParser & parser) diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index de38be9d0..167e24c75 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -35,14 +35,10 @@ CGObjectInstance::CGObjectInstance(): tempOwner(PlayerColor::UNFLAGGABLE), blockVisit(false) { - logGlobal->debug("Created object at %d", ptrdiff_t(this)); } //must be instantiated in .cpp file for access to complete types of all member fields -CGObjectInstance::~CGObjectInstance() -{ - logGlobal->debug("Deleted object %d at %d", id.getNum(), ptrdiff_t(this)); -} +CGObjectInstance::~CGObjectInstance() = default; MapObjectID CGObjectInstance::getObjGroupIndex() const { diff --git a/lib/serializer/BinaryDeserializer.cpp b/lib/serializer/BinaryDeserializer.cpp index 6efa9ed18..0cb95f14a 100644 --- a/lib/serializer/BinaryDeserializer.cpp +++ b/lib/serializer/BinaryDeserializer.cpp @@ -23,9 +23,4 @@ BinaryDeserializer::BinaryDeserializer(IBinaryReader * r): CLoaderBase(r) registerTypes(*this); } -BinaryDeserializer::~BinaryDeserializer() -{ - -} - VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 09cd89efe..231f54c5c 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -145,7 +145,6 @@ public: bool saving; BinaryDeserializer(IBinaryReader * r); - ~BinaryDeserializer(); template BinaryDeserializer & operator&(T & t) @@ -291,7 +290,7 @@ public: { typedef typename std::remove_pointer::type npT; typedef typename std::remove_const::type ncpT; - data = ClassObjectCreator::invoke();// !!! + data = ClassObjectCreator::invoke(); ptrAllocated(data, pid); load(*data); } From 3268ebe27c7b463354261de76d347e7c47d39527 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 13:45:46 +0200 Subject: [PATCH 1115/1248] Moved some code from OptionTab to separate class, OptionTabBase --- client/CMakeLists.txt | 2 + client/lobby/OptionsTab.cpp | 219 +--------------------------- client/lobby/OptionsTab.h | 4 +- client/lobby/OptionsTabBase.cpp | 244 ++++++++++++++++++++++++++++++++ client/lobby/OptionsTabBase.h | 22 +++ 5 files changed, 274 insertions(+), 217 deletions(-) create mode 100644 client/lobby/OptionsTabBase.cpp create mode 100644 client/lobby/OptionsTabBase.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6257722a2..0edca41da 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -52,6 +52,7 @@ set(client_SRCS lobby/CScenarioInfoScreen.cpp lobby/CSelectionBase.cpp lobby/OptionsTab.cpp + lobby/OptionsTabBase.cpp lobby/RandomMapTab.cpp lobby/SelectionTab.cpp @@ -212,6 +213,7 @@ set(client_HEADERS lobby/CScenarioInfoScreen.h lobby/CSelectionBase.h lobby/OptionsTab.h + lobby/OptionsTabBase.h lobby/RandomMapTab.h lobby/SelectionTab.h diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index a2f5027b0..afa3dd12b 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -42,164 +42,10 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" -OptionsTab::OptionsTab() : humanPlayers(0) +OptionsTab::OptionsTab() + : OptionsTabBase(JsonPath::builtin("config/widgets/optionsTab.json")) + , humanPlayers(0) { - recActions = 0; - - addCallback("setTimerPreset", [&](int index){ - if(!variables["timerPresets"].isNull()) - { - auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); - TurnTimerInfo tinfo; - tinfo.baseTimer = tpreset.at(0).Integer() * 1000; - tinfo.turnTimer = tpreset.at(1).Integer() * 1000; - tinfo.battleTimer = tpreset.at(2).Integer() * 1000; - tinfo.creatureTimer = tpreset.at(3).Integer() * 1000; - CSH->setTurnTimerInfo(tinfo); - } - }); - - addCallback("setSimturnDuration", [&](int index){ - SimturnsInfo info; - info.optionalTurns = index; - CSH->setSimturnsInfo(info); - }); - - //helper function to parse string containing time to integer reflecting time in seconds - //assumed that input string can be modified by user, function shall support user's intention - // normal: 2:00, 12:30 - // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, - // adding symbol (>60 seconds): 12:095 -> 129:05 - // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 - auto parseTimerString = [](const std::string & str) -> int - { - auto sc = str.find(":"); - if(sc == std::string::npos) - return str.empty() ? 0 : std::stoi(str); - - auto l = str.substr(0, sc); - auto r = str.substr(sc + 1, std::string::npos); - if(r.length() == 3) //symbol added - { - l.push_back(r.front()); - r.erase(r.begin()); - } - else if(r.length() == 1) //symbol removed - { - r.insert(r.begin(), l.back()); - l.pop_back(); - } - else if(r.empty()) - r = "0"; - - int sec = std::stoi(r); - if(sec >= 60) - { - if(l.empty()) //9:00 -> 0:09 - return sec / 10; - - l.push_back(r.front()); //0:090 -> 9:00 - r.erase(r.begin()); - } - else if(l.empty()) - return sec; - - return std::stoi(l) * 60 + std::stoi(r); - }; - - addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.baseTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.turnTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.battleTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){ - int time = parseTimerString(str) * 1000; - if(time >= 0) - { - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.creatureTimer = time; - CSH->setTurnTimerInfo(tinfo); - } - }); - - const JsonNode config(JsonPath::builtin("config/widgets/optionsTab.json")); - build(config); - - //set timers combo box callbacks - if(auto w = widget("timerModeSwitch")) - { - w->onConstructItems = [&](std::vector & curItems){ - if(variables["timers"].isNull()) - return; - - for(auto & p : variables["timers"].Vector()) - { - curItems.push_back(&p); - } - }; - - w->onSetItem = [&](const void * item){ - if(item) - { - if(auto * tObj = reinterpret_cast(item)) - { - for(auto wname : (*tObj)["hideWidgets"].Vector()) - { - if(auto w = widget(wname.String())) - w->setEnabled(false); - } - for(auto wname : (*tObj)["showWidgets"].Vector()) - { - if(auto w = widget(wname.String())) - w->setEnabled(true); - } - if((*tObj)["default"].isVector()) - { - TurnTimerInfo tinfo; - tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; - tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; - tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; - tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; - CSH->setTurnTimerInfo(tinfo); - } - } - redraw(); - } - }; - - w->getItemText = [this](int idx, const void * item){ - if(item) - { - if(auto * tObj = reinterpret_cast(item)) - return readText((*tObj)["text"]); - } - return std::string(""); - }; - - w->setItem(0); - } } void OptionsTab::recreate() @@ -221,64 +67,7 @@ void OptionsTab::recreate() entries.insert(std::make_pair(pInfo.first, std::make_shared(pInfo.second, * this))); } - //Simultaneous turns - if(auto turnSlider = widget("labelSimturnsDurationValue")) - turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); - - if(auto w = widget("labelSimturnsDurationValue")) - { - MetaString message; - message.appendRawString("Simturns: up to %d days"); - message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); - w->setText(message.toString()); - } - - const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; - - //classic timer - if(auto turnSlider = widget("sliderTurnDuration")) - { - if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer) - { - for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) - { - auto & tpreset = variables["timerPresets"].Vector()[idx]; - if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) - { - turnSlider->scrollTo(idx); - if(auto w = widget("labelTurnDurationValue")) - w->setText(CGI->generaltexth->turnDurations[idx]); - } - } - } - } - - //chess timer - auto timeToString = [](int time) -> std::string - { - std::stringstream ss; - ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; - return ss.str(); - }; - - if(auto ww = widget("chessFieldBase")) - ww->setText(timeToString(turnTimerRemote.baseTimer), false); - if(auto ww = widget("chessFieldTurn")) - ww->setText(timeToString(turnTimerRemote.turnTimer), false); - if(auto ww = widget("chessFieldBattle")) - ww->setText(timeToString(turnTimerRemote.battleTimer), false); - if(auto ww = widget("chessFieldCreature")) - ww->setText(timeToString(turnTimerRemote.creatureTimer), false); - - if(auto w = widget("timerModeSwitch")) - { - if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer) - { - if(auto turnSlider = widget("sliderTurnDuration")) - if(turnSlider->isActive()) - w->setItem(1); - } - } + OptionsTabBase::recreate(); } size_t OptionsTab::CPlayerSettingsHelper::getImageIndex(bool big) diff --git a/client/lobby/OptionsTab.h b/client/lobby/OptionsTab.h index 23eff9c96..691a162bb 100644 --- a/client/lobby/OptionsTab.h +++ b/client/lobby/OptionsTab.h @@ -9,9 +9,9 @@ */ #pragma once +#include "OptionsTabBase.h" #include "../windows/CWindowObject.h" #include "../widgets/Scrollable.h" -#include "../gui/InterfaceObjectConfigurable.h" VCMI_LIB_NAMESPACE_BEGIN struct PlayerSettings; @@ -30,7 +30,7 @@ class CButton; class FilledTexturePlayerColored; /// The options tab which is shown at the map selection phase. -class OptionsTab : public InterfaceObjectConfigurable +class OptionsTab : public OptionsTabBase { struct PlayerOptionsEntry; diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp new file mode 100644 index 000000000..8c9fdd3d1 --- /dev/null +++ b/client/lobby/OptionsTabBase.cpp @@ -0,0 +1,244 @@ +/* + * OptionsTabBase.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 "OptionsTabBase.h" +#include "CSelectionBase.h" + +#include "../widgets/ComboBox.h" +#include "../widgets/Slider.h" +#include "../widgets/TextControls.h" +#include "../CServerHandler.h" +#include "../CGameInfo.h" + +#include "../../lib/StartInfo.h" +#include "../../lib/MetaString.h" +#include "../../lib/CGeneralTextHandler.h" + +OptionsTabBase::OptionsTabBase(const JsonPath & configPath) +{ + recActions = 0; + + addCallback("setTimerPreset", [&](int index){ + if(!variables["timerPresets"].isNull()) + { + auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); + TurnTimerInfo tinfo; + tinfo.baseTimer = tpreset.at(0).Integer() * 1000; + tinfo.turnTimer = tpreset.at(1).Integer() * 1000; + tinfo.battleTimer = tpreset.at(2).Integer() * 1000; + tinfo.creatureTimer = tpreset.at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } + }); + + addCallback("setSimturnDuration", [&](int index){ + SimturnsInfo info; + info.optionalTurns = index; + CSH->setSimturnsInfo(info); + }); + + //helper function to parse string containing time to integer reflecting time in seconds + //assumed that input string can be modified by user, function shall support user's intention + // normal: 2:00, 12:30 + // adding symbol: 2:005 -> 2:05, 2:305 -> 23:05, + // adding symbol (>60 seconds): 12:095 -> 129:05 + // removing symbol: 129:0 -> 12:09, 2:0 -> 0:20, 0:2 -> 0:02 + auto parseTimerString = [](const std::string & str) -> int + { + auto sc = str.find(":"); + if(sc == std::string::npos) + return str.empty() ? 0 : std::stoi(str); + + auto l = str.substr(0, sc); + auto r = str.substr(sc + 1, std::string::npos); + if(r.length() == 3) //symbol added + { + l.push_back(r.front()); + r.erase(r.begin()); + } + else if(r.length() == 1) //symbol removed + { + r.insert(r.begin(), l.back()); + l.pop_back(); + } + else if(r.empty()) + r = "0"; + + int sec = std::stoi(r); + if(sec >= 60) + { + if(l.empty()) //9:00 -> 0:09 + return sec / 10; + + l.push_back(r.front()); //0:090 -> 9:00 + r.erase(r.begin()); + } + else if(l.empty()) + return sec; + + return std::stoi(l) * 60 + std::stoi(r); + }; + + addCallback("parseAndSetTimer_base", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.baseTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_turn", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.turnTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_battle", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.battleTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){ + int time = parseTimerString(str) * 1000; + if(time >= 0) + { + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; + tinfo.creatureTimer = time; + CSH->setTurnTimerInfo(tinfo); + } + }); + + const JsonNode config(configPath); + build(config); + + //set timers combo box callbacks + if(auto w = widget("timerModeSwitch")) + { + w->onConstructItems = [&](std::vector & curItems){ + if(variables["timers"].isNull()) + return; + + for(auto & p : variables["timers"].Vector()) + { + curItems.push_back(&p); + } + }; + + w->onSetItem = [&](const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + { + for(auto wname : (*tObj)["hideWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(false); + } + for(auto wname : (*tObj)["showWidgets"].Vector()) + { + if(auto w = widget(wname.String())) + w->setEnabled(true); + } + if((*tObj)["default"].isVector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; + tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; + tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; + tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; + CSH->setTurnTimerInfo(tinfo); + } + } + redraw(); + } + }; + + w->getItemText = [this](int idx, const void * item){ + if(item) + { + if(auto * tObj = reinterpret_cast(item)) + return readText((*tObj)["text"]); + } + return std::string(""); + }; + + w->setItem(0); + } +} + +void OptionsTabBase::recreate() +{ + //Simultaneous turns + if(auto turnSlider = widget("labelSimturnsDurationValue")) + turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); + + if(auto w = widget("labelSimturnsDurationValue")) + { + MetaString message; + message.appendRawString("Simturns: up to %d days"); + message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); + w->setText(message.toString()); + } + + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; + + //classic timer + if(auto turnSlider = widget("sliderTurnDuration")) + { + if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer) + { + for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) + { + auto & tpreset = variables["timerPresets"].Vector()[idx]; + if(tpreset.Vector().at(1).Integer() == turnTimerRemote.turnTimer / 1000) + { + turnSlider->scrollTo(idx); + if(auto w = widget("labelTurnDurationValue")) + w->setText(CGI->generaltexth->turnDurations[idx]); + } + } + } + } + + //chess timer + auto timeToString = [](int time) -> std::string + { + std::stringstream ss; + ss << time / 1000 / 60 << ":" << std::setw(2) << std::setfill('0') << time / 1000 % 60; + return ss.str(); + }; + + if(auto ww = widget("chessFieldBase")) + ww->setText(timeToString(turnTimerRemote.baseTimer), false); + if(auto ww = widget("chessFieldTurn")) + ww->setText(timeToString(turnTimerRemote.turnTimer), false); + if(auto ww = widget("chessFieldBattle")) + ww->setText(timeToString(turnTimerRemote.battleTimer), false); + if(auto ww = widget("chessFieldCreature")) + ww->setText(timeToString(turnTimerRemote.creatureTimer), false); + + if(auto w = widget("timerModeSwitch")) + { + if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer) + { + if(auto turnSlider = widget("sliderTurnDuration")) + if(turnSlider->isActive()) + w->setItem(1); + } + } +} diff --git a/client/lobby/OptionsTabBase.h b/client/lobby/OptionsTabBase.h new file mode 100644 index 000000000..4ba7e82c8 --- /dev/null +++ b/client/lobby/OptionsTabBase.h @@ -0,0 +1,22 @@ +/* + * OptionsTabBase.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 "../gui/InterfaceObjectConfigurable.h" +#include "../../lib/filesystem/ResourcePath.h" + +/// The options tab which is shown at the map selection phase. +class OptionsTabBase : public InterfaceObjectConfigurable +{ +public: + OptionsTabBase(const JsonPath & configPath); + + void recreate(); +}; From 32633d5f521b9964e274dd56537048ebcd02235e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 16:41:00 +0200 Subject: [PATCH 1116/1248] Initial version of Turn Options tab screen --- Mods/vcmi/config/vcmi/english.json | 24 ++- client/CMakeLists.txt | 2 + client/lobby/CLobbyScreen.cpp | 30 ++- client/lobby/CSelectionBase.h | 3 + client/lobby/OptionsTab.cpp | 2 +- client/lobby/OptionsTabBase.cpp | 2 +- client/lobby/TurnOptionsTab.cpp | 18 ++ client/lobby/TurnOptionsTab.h | 18 ++ ...{optionsTab.json => playerOptionsTab.json} | 36 +--- config/widgets/turnOptionsTab.json | 186 ++++++++++++++++++ 10 files changed, 273 insertions(+), 48 deletions(-) create mode 100644 client/lobby/TurnOptionsTab.cpp create mode 100644 client/lobby/TurnOptionsTab.h rename config/widgets/{optionsTab.json => playerOptionsTab.json} (75%) create mode 100644 config/widgets/turnOptionsTab.json diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 1d0432297..5cd4da2a5 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -220,14 +220,24 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Road Types", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", - "vcmi.optionsTab.widgets.labelTimer" : "Timer", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Classic timer", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Chess timer", + "vcmi.optionsTab.turnOptions.hover" : "Turn Options", + "vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options", + "vcmi.optionsTab.chessFieldBase.hover" : "Base timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", + "vcmi.optionsTab.chessFieldCreature.hover" : "Unit timer", + "vcmi.optionsTab.chessFieldBase.help" : "Starts counting down when the {Turn Timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", + "vcmi.optionsTab.chessFieldTurn.help" : "Starts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra Timer} if it is in use.", + "vcmi.optionsTab.chessFieldBattle.help" : "Counts down during battles when the {Unit Timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", + "vcmi.optionsTab.chessFieldCreature.help" : "Starts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", + + "vcmi.optionsTab.simturnsMin.hover" : "Simultaneous turns (minimum)", + "vcmi.optionsTab.simturnsMax.hover" : "Simultaneous turns (maximum)", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", + "vcmi.optionsTab.simturnsMin.help" : "Players will act simultaneously for specified number of days. Contacts between players are blocked", + "vcmi.optionsTab.simturnsMax.help" : "Players will act simultaneously until they meet each other or if specified date has been reached", + "vcmi.optionsTab.simturnsAI.help" : "", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 0edca41da..2f11a87c9 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -51,6 +51,7 @@ set(client_SRCS lobby/CSavingScreen.cpp lobby/CScenarioInfoScreen.cpp lobby/CSelectionBase.cpp + lobby/TurnOptionsTab.cpp lobby/OptionsTab.cpp lobby/OptionsTabBase.cpp lobby/RandomMapTab.cpp @@ -212,6 +213,7 @@ set(client_HEADERS lobby/CSavingScreen.h lobby/CScenarioInfoScreen.h lobby/CSelectionBase.h + lobby/TurnOptionsTab.h lobby/OptionsTab.h lobby/OptionsTabBase.h lobby/RandomMapTab.h diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index fc0333f37..4537ce332 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -8,14 +8,15 @@ * */ #include "StdInc.h" - #include "CLobbyScreen.h" -#include "CBonusSelection.h" -#include "SelectionTab.h" -#include "RandomMapTab.h" -#include "OptionsTab.h" -#include "../CServerHandler.h" +#include "CBonusSelection.h" +#include "TurnOptionsTab.h" +#include "OptionsTab.h" +#include "RandomMapTab.h" +#include "SelectionTab.h" + +#include "../CServerHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/Shortcut.h" #include "../widgets/Buttons.h" @@ -50,6 +51,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) }); buttonOptions = std::make_shared(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); + buttonTurnOptions = std::make_shared(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE); }; buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); @@ -60,6 +62,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) case ESelectionScreen::newGame: { tabOpt = std::make_shared(); + tabTurnOptions = std::make_shared(); tabRand = std::make_shared(); tabRand->mapInfoChanged += std::bind(&IServerAPI::setMapInfo, CSH, _1, _2); buttonRMG = std::make_shared(Point(411, 105), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[47], 0, EShortcut::LOBBY_RANDOM_MAP); @@ -78,6 +81,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) case ESelectionScreen::loadGame: { tabOpt = std::make_shared(); + tabTurnOptions = std::make_shared(); buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; @@ -145,6 +149,7 @@ void CLobbyScreen::toggleMode(bool host) auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); + buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor); if(buttonRMG) { buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); @@ -152,9 +157,13 @@ void CLobbyScreen::toggleMode(bool host) } buttonSelect->block(!host); buttonOptions->block(!host); + buttonTurnOptions->block(!host); if(CSH->mi) + { tabOpt->recreate(); + tabTurnOptions->recreate(); + } } void CLobbyScreen::toggleChat() @@ -168,8 +177,13 @@ void CLobbyScreen::toggleChat() void CLobbyScreen::updateAfterStateChange() { - if(CSH->mi && tabOpt) - tabOpt->recreate(); + if(CSH->mi) + { + if (tabOpt) + tabOpt->recreate(); + if (tabTurnOptions) + tabTurnOptions->recreate(); + } buttonStart->block(CSH->mi == nullptr || CSH->isGuest()); diff --git a/client/lobby/CSelectionBase.h b/client/lobby/CSelectionBase.h index 92b0c2b4f..e614468cf 100644 --- a/client/lobby/CSelectionBase.h +++ b/client/lobby/CSelectionBase.h @@ -26,6 +26,7 @@ class CAnimImage; class CToggleGroup; class RandomMapTab; class OptionsTab; +class TurnOptionsTab; class SelectionTab; class InfoCard; class CChatBox; @@ -57,12 +58,14 @@ public: std::shared_ptr buttonSelect; std::shared_ptr buttonRMG; std::shared_ptr buttonOptions; + std::shared_ptr buttonTurnOptions; std::shared_ptr buttonStart; std::shared_ptr buttonBack; std::shared_ptr buttonSimturns; std::shared_ptr tabSel; std::shared_ptr tabOpt; + std::shared_ptr tabTurnOptions; std::shared_ptr tabRand; std::shared_ptr curTab; diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index afa3dd12b..0584a1999 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -43,7 +43,7 @@ #include "../../lib/mapping/CMapHeader.h" OptionsTab::OptionsTab() - : OptionsTabBase(JsonPath::builtin("config/widgets/optionsTab.json")) + : OptionsTabBase(JsonPath::builtin("config/widgets/playerOptionsTab.json")) , humanPlayers(0) { } diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 8c9fdd3d1..60996951b 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -190,7 +190,7 @@ void OptionsTabBase::recreate() if(auto w = widget("labelSimturnsDurationValue")) { MetaString message; - message.appendRawString("Simturns: up to %d days"); + message.appendRawString("%d days"); message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); w->setText(message.toString()); } diff --git a/client/lobby/TurnOptionsTab.cpp b/client/lobby/TurnOptionsTab.cpp new file mode 100644 index 000000000..6bf3523a8 --- /dev/null +++ b/client/lobby/TurnOptionsTab.cpp @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.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 "TurnOptionsTab.h" + +TurnOptionsTab::TurnOptionsTab() + : OptionsTabBase(JsonPath::builtin("config/widgets/turnOptionsTab.json")) +{ + +} diff --git a/client/lobby/TurnOptionsTab.h b/client/lobby/TurnOptionsTab.h new file mode 100644 index 000000000..ad9ad1450 --- /dev/null +++ b/client/lobby/TurnOptionsTab.h @@ -0,0 +1,18 @@ +/* + * TurnOptionsTab.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 "OptionsTabBase.h" + +class TurnOptionsTab : public OptionsTabBase +{ +public: + TurnOptionsTab(); +}; diff --git a/config/widgets/optionsTab.json b/config/widgets/playerOptionsTab.json similarity index 75% rename from config/widgets/optionsTab.json rename to config/widgets/playerOptionsTab.json index bd8c76126..ca3c936f2 100644 --- a/config/widgets/optionsTab.json +++ b/config/widgets/playerOptionsTab.json @@ -73,41 +73,16 @@ "adoptHeight": true }, + // timer { - "name": "simturnsDuration", - "type": "slider", - "orientation": "horizontal", - "position": {"x": 55, "y": 537}, - "size": 194, - "callback": "setSimturnDuration", - "itemsVisible": 1, - "itemsTotal": 28, - "selected": 0, - "style": "blue", - "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, - "panningStep": 20 - }, - - { - "name": "labelSimturnsDurationValue", "type": "label", "font": "small", "alignment": "center", - "color": "white", - "text": "", - "position": {"x": 319, "y": 545} + "color": "yellow", + "text": "core.genrltxt.521", + "position": {"x": 222, "y": 544} }, - // timer - //{ - // "type": "label", - // "font": "small", - // "alignment": "center", - // "color": "yellow", - // "text": "core.genrltxt.521", - // "position": {"x": 222, "y": 544} - //}, - { "name": "labelTurnDurationValue", "type": "label", @@ -129,8 +104,7 @@ "itemsTotal": 11, "selected": 11, "style": "blue", - "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, - //"scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, + "scrollBounds": {"x": -3, "y": -25, "w": 337, "h": 43}, "panningStep": 20 }, ], diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json new file mode 100644 index 000000000..b158c5b08 --- /dev/null +++ b/config/widgets/turnOptionsTab.json @@ -0,0 +1,186 @@ +{ + "customTypes" : { + "verticalLayout66" : { + "type" : "layout", + "vertical" : true, + "dynamic" : false, + "distance" : 66 + }, + "labelTitle" : { + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow" + }, + "labelDescription" : { + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "rect": {"x": 0, "y": 0, "w": 300, "h": 35}, + "adoptHeight": true + }, + "timeInput" : { + "type": "textInput", + "background": "timerField", + "alignment": "center", + "text": "00:00", + "rect": {"x": 0, "y": 0, "w": 84, "h": 25}, + "offset": {"x": 0, "y": 0} + } + }, + + "items": + [ + { + "name": "background", + "type": "picture", + "image": "RANMAPBK", + "position": {"x": 0, "y": 6} + }, + + { + "name": "labelTitle", + "type": "label", + "font": "big", + "alignment": "center", + "color": "yellow", + "text": "vcmi.optionsTab.turnOptions.hover", + "position": {"x": 222, "y": 36} + }, + + { + "name": "labelSubTitle", + "type": "multiLineLabel", + "font": "small", + "alignment": "center", + "color": "white", + "text": "vcmi.optionsTab.turnOptions.help", + "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, + "adoptHeight": true + }, + { + "type" : "verticalLayout66", + "customType" : "labelTitle", + "position": {"x": 70, "y": 134}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldTurn.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.hover" + }, + { + "text": "vcmi.optionsTab.chessFieldCreature.hover" + }, + { + "text": "vcmi.optionsTab.simturnsMin.hover" + }, + { + "text": "vcmi.optionsTab.simturnsMax.hover" + } + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "labelDescription", + "position": {"x": 70, "y": 153}, + "items": + [ + { + "text": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "text": "vcmi.optionsTab.chessFieldTurn.help" + }, + { + "text": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "text": "vcmi.optionsTab.chessFieldCreature.help" + }, + { + "text": "vcmi.optionsTab.simturnsMin.help" + }, + { + "text": "vcmi.optionsTab.simturnsMax.help" + } + ] + }, + + { + "type" : "verticalLayout66", + "customType" : "timeInput", + "position": {"x": 294, "y": 129}, + "items": + [ + { + "name": "chessFieldBase", + "callback": "parseAndSetTimer_base", + "help": "vcmi.optionsTab.chessFieldBase.help" + }, + { + "name": "chessFieldTurn", + "callback": "parseAndSetTimer_turn", + "help": "vcmi.optionsTab.chessFieldTurn.help" + }, + { + "name": "chessFieldBattle", + "callback": "parseAndSetTimer_battle", + "help": "vcmi.optionsTab.chessFieldBattle.help" + }, + { + "name": "chessFieldCreature", + "callback": "parseAndSetTimer_creature", + "help": "vcmi.optionsTab.chessFieldCreature.help" + }, + ] + }, + + // simturns + { + "name": "simturnsDuration", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 258, "y": 398}, + "size": 120, + "callback": "setSimturnDuration", + "itemsVisible": 1, + "itemsTotal": 28, + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + + { + "name": "labelSimturnsDurationValue", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 320, "y": 406} + }, + + { + "position": {"x": 70, "y": 535}, + "type": "toggleButton", + "image": "sysopchk.def" + }, + { + "name": "labelSimturnsAI", + "type": "label", + "font": "small", + "alignment": "left", + "color": "yellow", + "text": "vcmi.optionsTab.simturnsAI.hover", + "position": {"x": 110, "y": 540} + } + ] +} From 1f045ab128d89e0a4ac1fe12fbaa459a3da45b1b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 20:37:41 +0200 Subject: [PATCH 1117/1248] Layout improvements for turn options tab --- Mods/vcmi/config/vcmi/english.json | 17 ++--- config/widgets/turnOptionsTab.json | 107 ++++++++++++++++++++++++----- 2 files changed, 98 insertions(+), 26 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 5cd4da2a5..0f9e38383 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -227,16 +227,17 @@ "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", "vcmi.optionsTab.chessFieldCreature.hover" : "Unit timer", - "vcmi.optionsTab.chessFieldBase.help" : "Starts counting down when the {Turn Timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.chessFieldTurn.help" : "Starts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra Timer} if it is in use.", - "vcmi.optionsTab.chessFieldBattle.help" : "Counts down during battles when the {Unit Timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.chessFieldCreature.help" : "Starts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", + "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", + "vcmi.optionsTab.chessFieldTurn.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to Base Timer at turn's end.", + "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", + "vcmi.optionsTab.chessFieldCreature.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn.", - "vcmi.optionsTab.simturnsMin.hover" : "Simultaneous turns (minimum)", - "vcmi.optionsTab.simturnsMax.hover" : "Simultaneous turns (maximum)", + "vcmi.optionsTab.simturns" : "Simultaneous turns", + "vcmi.optionsTab.simturnsMin.hover" : "At least for", + "vcmi.optionsTab.simturnsMax.hover" : "At most for", "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", - "vcmi.optionsTab.simturnsMin.help" : "Players will act simultaneously for specified number of days. Contacts between players are blocked", - "vcmi.optionsTab.simturnsMax.help" : "Players will act simultaneously until they meet each other or if specified date has been reached", + "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously at least for specified number of days. Contacts between players during this period are blocked", + "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously at most for specified number of days or until they meet each other.", "vcmi.optionsTab.simturnsAI.help" : "", // Custom victory conditions for H3 campaigns and HotA maps diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index b158c5b08..17c7ad33b 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -14,7 +14,7 @@ }, "labelDescription" : { "type": "multiLineLabel", - "font": "small", + "font": "tiny", "alignment": "center", "color": "white", "rect": {"x": 0, "y": 0, "w": 300, "h": 35}, @@ -22,11 +22,16 @@ }, "timeInput" : { "type": "textInput", - "background": "timerField", "alignment": "center", "text": "00:00", - "rect": {"x": 0, "y": 0, "w": 84, "h": 25}, + "rect": {"x": 0, "y": 0, "w": 86, "h": 23}, "offset": {"x": 0, "y": 0} + }, + "timeInputBackground" : { + "type": "transparentFilledRectangle", + "rect": {"x": 0, "y": 0, "w": 86, "h": 23}, + "color": [0, 0, 0, 128], + "colorLine": [64, 80, 128, 128] } }, @@ -59,6 +64,24 @@ "rect": {"x": 60, "y": 48, "w": 320, "h": 0}, "adoptHeight": true }, + { + "type": "texture", + "image": "DiBoxBck", + "color" : "blue", + "rect": {"x" : 64, "y" : 416, "w": 316, "h": 102} + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 64, "y" : 416, "w": 316, "h": 102}, + "color": [0, 0, 0, 0], + "colorLine": [64, 80, 128, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 64, "y" : 480, "w": 316, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [32, 40, 128, 128] + }, { "type" : "verticalLayout66", "customType" : "labelTitle", @@ -78,10 +101,7 @@ "text": "vcmi.optionsTab.chessFieldCreature.hover" }, { - "text": "vcmi.optionsTab.simturnsMin.hover" - }, - { - "text": "vcmi.optionsTab.simturnsMax.hover" + "text": "vcmi.optionsTab.simturns" } ] }, @@ -89,7 +109,7 @@ { "type" : "verticalLayout66", "customType" : "labelDescription", - "position": {"x": 70, "y": 153}, + "position": {"x": 70, "y": 155}, "items": [ { @@ -105,14 +125,27 @@ "text": "vcmi.optionsTab.chessFieldCreature.help" }, { - "text": "vcmi.optionsTab.simturnsMin.help" + "text": "" }, { - "text": "vcmi.optionsTab.simturnsMax.help" + "text": "vcmi.optionsTab.simturnsMin.help" } ] }, + { + "type" : "verticalLayout66", + "customType" : "timeInputBackground", + "position": {"x": 294, "y": 129}, + "items": + [ + {}, + {}, + {}, + {} + ] + }, + { "type" : "verticalLayout66", "customType" : "timeInput", @@ -138,17 +171,33 @@ "name": "chessFieldCreature", "callback": "parseAndSetTimer_creature", "help": "vcmi.optionsTab.chessFieldCreature.help" - }, + } ] }, - // simturns { - "name": "simturnsDuration", + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMin.hover", + "position": {"x": 70, "y": 430} + }, + { + "type": "label", + "font": "small", + "alignment": "left", + "color": "white", + "text": "vcmi.optionsTab.simturnsMax.hover", + "position": {"x": 70, "y": 460} + }, + + { + "name": "simturnsDurationMin", "type": "slider", "orientation": "horizontal", - "position": {"x": 258, "y": 398}, - "size": 120, + "position": {"x": 178, "y": 430}, + "size": 200, "callback": "setSimturnDuration", "itemsVisible": 1, "itemsTotal": 28, @@ -157,15 +206,37 @@ "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, "panningStep": 20 }, - { - "name": "labelSimturnsDurationValue", + "name": "simturnsDurationMax", + "type": "slider", + "orientation": "horizontal", + "position": {"x": 178, "y": 460}, + "size": 200, + "callback": "setSimturnDuration", + "itemsVisible": 1, + "itemsTotal": 28, + "selected": 0, + "style": "blue", + "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, + "panningStep": 20 + }, + { + "name": "labelSimturnsDurationValueMin", "type": "label", "font": "small", "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 406} + "position": {"x": 320, "y": 438} + }, + { + "name": "labelSimturnsDurationValueMax", + "type": "label", + "font": "small", + "alignment": "center", + "color": "white", + "text": "", + "position": {"x": 320, "y": 468} }, { From e160b8557b2faf2e21a3fa72ab2afd84b32df09d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 9 Nov 2023 23:18:03 +0200 Subject: [PATCH 1118/1248] Implement min/max simturns callbacks --- Mods/vcmi/config/vcmi/english.json | 3 +-- client/lobby/OptionsTabBase.cpp | 25 +++++++++++++++--- config/widgets/turnOptionsTab.json | 42 ++++++++++++++++++------------ 3 files changed, 47 insertions(+), 23 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 0f9e38383..db41b2216 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -236,8 +236,7 @@ "vcmi.optionsTab.simturnsMin.hover" : "At least for", "vcmi.optionsTab.simturnsMax.hover" : "At most for", "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", - "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously at least for specified number of days. Contacts between players during this period are blocked", - "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously at most for specified number of days or until they meet each other.", + "vcmi.optionsTab.simturns.help" : "Play simultaneously at least for specified number of days, after which simultaneous turns will stay active until specified day or until players make contact. Contacts between players during this period are blocked", "vcmi.optionsTab.simturnsAI.help" : "", // Custom victory conditions for H3 campaigns and HotA maps diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 60996951b..1fc917809 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -38,8 +38,14 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) } }); - addCallback("setSimturnDuration", [&](int index){ - SimturnsInfo info; + addCallback("setSimturnDurationMin", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.requiredTurns = index; + CSH->setSimturnsInfo(info); + }); + + addCallback("setSimturnDurationMax", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; info.optionalTurns = index; CSH->setSimturnsInfo(info); }); @@ -184,10 +190,21 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) void OptionsTabBase::recreate() { //Simultaneous turns - if(auto turnSlider = widget("labelSimturnsDurationValue")) + if(auto turnSlider = widget("simturnsDurationMin")) + turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.requiredTurns); + + if(auto turnSlider = widget("simturnsDurationMax")) turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); - if(auto w = widget("labelSimturnsDurationValue")) + if(auto w = widget("labelSimturnsDurationValueMin")) + { + MetaString message; + message.appendRawString("%d days"); + message.replaceNumber(SEL->getStartInfo()->simturnsInfo.requiredTurns); + w->setText(message.toString()); + } + + if(auto w = widget("labelSimturnsDurationValueMax")) { MetaString message; message.appendRawString("%d days"); diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index 17c7ad33b..dda539f8e 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -78,7 +78,13 @@ }, { "type": "transparentFilledRectangle", - "rect": {"x" : 64, "y" : 480, "w": 316, "h": 1}, + "rect": {"x" : 64, "y" : 465, "w": 316, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [80, 96, 160, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 64, "y" : 466, "w": 316, "h": 1}, "color": [0, 0, 0, 0], "colorLine": [32, 40, 128, 128] }, @@ -123,16 +129,10 @@ }, { "text": "vcmi.optionsTab.chessFieldCreature.help" - }, - { - "text": "" - }, - { - "text": "vcmi.optionsTab.simturnsMin.help" } ] }, - + { "type" : "verticalLayout66", "customType" : "timeInputBackground", @@ -181,7 +181,7 @@ "alignment": "left", "color": "white", "text": "vcmi.optionsTab.simturnsMin.hover", - "position": {"x": 70, "y": 430} + "position": {"x": 70, "y": 420} }, { "type": "label", @@ -189,16 +189,16 @@ "alignment": "left", "color": "white", "text": "vcmi.optionsTab.simturnsMax.hover", - "position": {"x": 70, "y": 460} + "position": {"x": 70, "y": 445} }, { "name": "simturnsDurationMin", "type": "slider", "orientation": "horizontal", - "position": {"x": 178, "y": 430}, + "position": {"x": 178, "y": 420}, "size": 200, - "callback": "setSimturnDuration", + "callback": "setSimturnDurationMin", "itemsVisible": 1, "itemsTotal": 28, "selected": 0, @@ -210,9 +210,9 @@ "name": "simturnsDurationMax", "type": "slider", "orientation": "horizontal", - "position": {"x": 178, "y": 460}, + "position": {"x": 178, "y": 445}, "size": 200, - "callback": "setSimturnDuration", + "callback": "setSimturnDurationMax", "itemsVisible": 1, "itemsTotal": 28, "selected": 0, @@ -227,7 +227,7 @@ "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 438} + "position": {"x": 320, "y": 428} }, { "name": "labelSimturnsDurationValueMax", @@ -236,9 +236,17 @@ "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 468} + "position": {"x": 320, "y": 453} + }, + { + "type" : "label", + "text": "vcmi.optionsTab.simturns.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 470, "w": 300, "h": 40} }, - { "position": {"x": 70, "y": 535}, "type": "toggleButton", From 7391f2a6eee42d17ccf581e628be9f8c6a329d5a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 10 Nov 2023 16:09:41 +0200 Subject: [PATCH 1119/1248] Added custom-made blue version of checkbox --- Mods/vcmi/Sprites/lobby/checkbox.json | 8 ++++++++ Mods/vcmi/Sprites/lobby/checkboxBlueOff.png | Bin 0 -> 485 bytes Mods/vcmi/Sprites/lobby/checkboxBlueOn.png | Bin 0 -> 400 bytes Mods/vcmi/Sprites/lobby/checkboxOff.png | Bin 0 -> 522 bytes Mods/vcmi/Sprites/lobby/checkboxOn.png | Bin 0 -> 374 bytes config/widgets/turnOptionsTab.json | 2 +- 6 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Mods/vcmi/Sprites/lobby/checkbox.json create mode 100644 Mods/vcmi/Sprites/lobby/checkboxBlueOff.png create mode 100644 Mods/vcmi/Sprites/lobby/checkboxBlueOn.png create mode 100644 Mods/vcmi/Sprites/lobby/checkboxOff.png create mode 100644 Mods/vcmi/Sprites/lobby/checkboxOn.png diff --git a/Mods/vcmi/Sprites/lobby/checkbox.json b/Mods/vcmi/Sprites/lobby/checkbox.json new file mode 100644 index 000000000..bfb1e6587 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/checkbox.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "checkboxBlueOff.png"}, + { "frame" : 1, "file" : "checkboxBlueOn.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb75ae7cd9f1d18a552caafebbc8bb05efe1ba0 GIT binary patch literal 485 zcmVBLbuT~EpJ^b8i{oQKe#9mNJLXLe&iFiT8t5K$qL-W*S z!l_rvuu}~iHz+79lY&U{)MkNmOzh5L2NEbOFF})nN;Wn^0|*uX0R+OTTItMV``c=W zb~g6cXUDEn0002Pt6KZqY2CqGT23Jc5-HHNR$x;k1q~nu5GD>DJY<0fJ^%m!*GWV{ zRCobmAPgAs0zj+ufMqcZKmqi`@7uO*j)T9~h$VJ=9JED}2+FNb zaRg51p$23css41enqFPw=Jw8TS(yr~+&?^44S-bJgP9Ps!QuS$9P(g>^Yv!4o)0hj bk=22=CL&y7`qZ?bHsAZ}wG5!?*LFt&P3I8S;+MCh z+yX?zzC4o(H2vk7Bwr_KHhyKe>6BZ5h}f^!(`&K~m^me(rc-VKX2hy1i9iw$ITqw3 zdOSX!+)?SPq3eh@V(rPa;OU8EP&iym&rNg!nt~@{$+Td{!U%|Cx6Jd_*0li!&ZA=> zD_@>PiCFS2@N#vzeIO3viMCSbBWt1{_ATjmV&;@UGo8#x4)AmaCPbj2?=Pkx2R*lh z5!DL2r4xgI=0ec*J&2&E+yX?p{CGJHX!@_)nH3p&tl*%ha&cXl>hbb)Vqdj|nxX)o uj1^7l13p=6V2igP)zYH4K zYat66#cmkI9Ao}5vf!WajdEw=Hq2p$ERp?v#WlpbBl#VMwCMHftBX@;+m<0Tq)$tL zXM#_OSVxFSe788_#rY|NT`8dAC%x27dW|wxPOo5MX;}p zt7=Yq^m>gkpHO*zZgTg!;y+xQZc-!HK?I%JJLZ}z1RP`N&E;uy{@^&J@Xsm(@2Q`b z=GG@7{qwR5i1~D52Jx$NjVU@Db*5qcTkU^-e)7J;zpFJ%BKVucP}aBQ5OB;{0WyPT zBh>lHnMqAP-k7n~k_h%U5)F&r{Bxy%M(|k{QA>dAvJ#jDSQb&M1hPw-5OXfZ4rWmV zKkuy4g#TPJhFCI~bNDQY$oa9di0{P=?J$jSz6oJsy%LrT=Da=;rD1Dy!~uR+pkIuk z^Q#!^6@QE+Bj#LU8VMv`QhSY<^)h0jUoNEilRG2%Va7Fv-H!E&1eW-DZ??8{MmHpX zAXc$Utqs;InhPC{F#~vpPt+WoU^*~vvr(eC{xP!PpYY+E|5(V%0pDr}khVIXb>VxxhuoCk>40P+71mr@=Bg^cKJc`wLv5Z*M~<3CXN7`-fK1>!=O z6l8JoHL*7eJQ3KAZI0*GU>=`5KVrbWL>M zZ>0aO$u_6N*Pnr5>+5R0>pMe%tBN%9E}ql&=P?+=aO!KEc<#b{r{>du2cWs&FY6a zYDvEn$nuLcO?1cB1S-A1nDP-?y#B!w@9{KG@t>}wW`it+;p=cyp{0=Wcjb# zng1&?^gx!c1zS!p@cdMQF|efj*;j4xUtN*w3sA^_VE}+85T6HPS8#qG>R=QA01M+7 UUG8P(w*UYD07*qoM6N<$g5^Y|lK=n! literal 0 HcmV?d00001 diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index dda539f8e..e18e13b99 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -250,7 +250,7 @@ { "position": {"x": 70, "y": 535}, "type": "toggleButton", - "image": "sysopchk.def" + "image": "lobby/checkbox" }, { "name": "labelSimturnsAI", From 7d54f6a9c0ecb30e9c7ac06c56c5c1bfb4e0082f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 10 Nov 2023 16:10:01 +0200 Subject: [PATCH 1120/1248] Implemented basic support for plural forms in translations --- Mods/vcmi/config/vcmi/english.json | 12 ++++ client/lobby/OptionsTabBase.cpp | 41 ++++++++---- lib/Languages.h | 100 +++++++++++++++++++++++------ 3 files changed, 121 insertions(+), 32 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index db41b2216..fc102ff64 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -238,6 +238,18 @@ "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", "vcmi.optionsTab.simturns.help" : "Play simultaneously at least for specified number of days, after which simultaneous turns will stay active until specified day or until players make contact. Contacts between players during this period are blocked", "vcmi.optionsTab.simturnsAI.help" : "", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d days", + "vcmi.optionsTab.simturns.days.1" : " %d day", + "vcmi.optionsTab.simturns.days.2" : " %d days", + "vcmi.optionsTab.simturns.weeks.0" : " %d weeks", + "vcmi.optionsTab.simturns.weeks.1" : " %d week", + "vcmi.optionsTab.simturns.weeks.2" : " %d weeks", + "vcmi.optionsTab.simturns.months.0" : " %d months", + "vcmi.optionsTab.simturns.months.1" : " %d month", + "vcmi.optionsTab.simturns.months.2" : " %d months", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "The enemy has managed to survive till this day. Victory is theirs!", diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 1fc917809..39c9e52c7 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -18,6 +18,7 @@ #include "../CGameInfo.h" #include "../../lib/StartInfo.h" +#include "../../lib/Languages.h" #include "../../lib/MetaString.h" #include "../../lib/CGeneralTextHandler.h" @@ -189,6 +190,32 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) void OptionsTabBase::recreate() { + auto const & generateSimturnsDurationText = [](int days) -> std::string + { + bool canUseMonth = days % 28 == 0 && days >= 28*2; + bool canUseWeek = days % 7 == 0 && days >= 7*2; + + MetaString message; + int value = days; + std::string text = "vcmi.optionsTab.simturns.days"; + + if (canUseWeek) + { + value = days / 7; + text = "vcmi.optionsTab.simturns.weeks"; + } + + if (canUseMonth) + { + value = days / 28; + text = "vcmi.optionsTab.simturns.months"; + } + + message.appendTextID(Languages::getPluralFormTextID( CGI->generaltexth->getPreferredLanguage(), value, text)); + message.replaceNumber(value); + return message.toString(); + }; + //Simultaneous turns if(auto turnSlider = widget("simturnsDurationMin")) turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.requiredTurns); @@ -197,20 +224,10 @@ void OptionsTabBase::recreate() turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); if(auto w = widget("labelSimturnsDurationValueMin")) - { - MetaString message; - message.appendRawString("%d days"); - message.replaceNumber(SEL->getStartInfo()->simturnsInfo.requiredTurns); - w->setText(message.toString()); - } + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); if(auto w = widget("labelSimturnsDurationValueMax")) - { - MetaString message; - message.appendRawString("%d days"); - message.replaceNumber(SEL->getStartInfo()->simturnsInfo.optionalTurns); - w->setText(message.toString()); - } + w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.optionalTurns)); const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; diff --git a/lib/Languages.h b/lib/Languages.h index cfd47021d..2f7e41564 100644 --- a/lib/Languages.h +++ b/lib/Languages.h @@ -12,6 +12,17 @@ namespace Languages { +enum class EPluralForms +{ + NONE, + VI_1, // Single plural form, (Vietnamese) + EN_2, // Two forms, singular used for one only (English) + FR_2, // Two forms, singular used for zero and one (French) + UK_3, // Three forms, special cases for numbers ending in 1 and 2, 3, 4, except those ending in 1[1-4] (Ukrainian) + CZ_3, // Three forms, special cases for 1 and 2, 3, 4 (Czech) + PL_3, // Three forms, special case for one and some numbers ending in 2, 3, or 4 (Polish) +}; + enum class ELanguages { CZECH, @@ -57,6 +68,9 @@ struct Options /// primary IETF language tag std::string tagIETF; + /// Ruleset for plural forms in this language + EPluralForms pluralForms = EPluralForms::NONE; + /// VCMI supports translations into this language bool hasTranslation = false; }; @@ -65,27 +79,27 @@ inline const auto & getLanguageList() { static const std::array languages { { - { "czech", "Czech", "Čeština", "CP1250", "cs", true }, - { "chinese", "Chinese", "简体中文", "GBK", "zh", true }, // Note: actually Simplified Chinese - { "english", "English", "English", "CP1252", "en", true }, - { "finnish", "Finnish", "Suomi", "CP1252", "fi", true }, - { "french", "French", "Français", "CP1252", "fr", true }, - { "german", "German", "Deutsch", "CP1252", "de", true }, - { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", true }, - { "italian", "Italian", "Italiano", "CP1250", "it", true }, - { "korean", "Korean", "한국어", "CP949", "ko", true }, - { "polish", "Polish", "Polski", "CP1250", "pl", true }, - { "portuguese", "Portuguese", "Português", "CP1252", "pt", true }, // Note: actually Brazilian Portuguese - { "russian", "Russian", "Русский", "CP1251", "ru", true }, - { "spanish", "Spanish", "Español", "CP1252", "es", true }, - { "swedish", "Swedish", "Svenska", "CP1252", "sv", true }, - { "turkish", "Turkish", "Türkçe", "CP1254", "tr", true }, - { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", true }, - { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", true }, // Fan translation uses special encoding + { "czech", "Czech", "Čeština", "CP1250", "cs", EPluralForms::CZ_3, true }, + { "chinese", "Chinese", "简体中文", "GBK", "zh", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese + { "english", "English", "English", "CP1252", "en", EPluralForms::EN_2, true }, + { "finnish", "Finnish", "Suomi", "CP1252", "fi", EPluralForms::EN_2, true }, + { "french", "French", "Français", "CP1252", "fr", EPluralForms::FR_2, true }, + { "german", "German", "Deutsch", "CP1252", "de", EPluralForms::EN_2, true }, + { "hungarian", "Hungarian", "Magyar", "CP1250", "hu", EPluralForms::EN_2, true }, + { "italian", "Italian", "Italiano", "CP1250", "it", EPluralForms::EN_2, true }, + { "korean", "Korean", "한국어", "CP949", "ko", EPluralForms::VI_1, true }, + { "polish", "Polish", "Polski", "CP1250", "pl", EPluralForms::PL_3, true }, + { "portuguese", "Portuguese", "Português", "CP1252", "pt", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese + { "russian", "Russian", "Русский", "CP1251", "ru", EPluralForms::UK_3, true }, + { "spanish", "Spanish", "Español", "CP1252", "es", EPluralForms::EN_2, true }, + { "swedish", "Swedish", "Svenska", "CP1252", "sv", EPluralForms::EN_2, true }, + { "turkish", "Turkish", "Türkçe", "CP1254", "tr", EPluralForms::EN_2, true }, + { "ukrainian", "Ukrainian", "Українська", "CP1251", "uk", EPluralForms::UK_3, true }, + { "vietnamese", "Vietnamese", "Tiếng Việt", "UTF-8", "vi", EPluralForms::VI_1, true }, // Fan translation uses special encoding - { "other_cp1250", "Other (East European)", "", "CP1250", "", false }, - { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", false }, - { "other_cp1252", "Other (West European)", "", "CP1252", "", false } + { "other_cp1250", "Other (East European)", "", "CP1250", "", EPluralForms::NONE, false }, + { "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", EPluralForms::NONE, false }, + { "other_cp1252", "Other (West European)", "", "CP1252", "", EPluralForms::NONE, false } } }; static_assert(languages.size() == static_cast(ELanguages::COUNT), "Languages array is missing a value!"); @@ -109,4 +123,50 @@ inline const Options & getLanguageOptions(const std::string & language) return emptyValue; } +template +inline constexpr int getPluralFormIndex(EPluralForms form, Numeric value) +{ + // Based on https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html + switch(form) + { + case EPluralForms::NONE: + case EPluralForms::VI_1: + return 0; + case EPluralForms::EN_2: + if (value == 1) + return 1; + return 2; + case EPluralForms::FR_2: + if (value == 1 || value == 0) + return 1; + return 2; + case EPluralForms::UK_3: + if (value % 10 == 1 && value % 100 != 11) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + case EPluralForms::CZ_3: + if (value == 1) + return 1; + if (value>=2 && value<=4) + return 2; + return 0; + case EPluralForms::PL_3: + if (value == 1) + return 1; + if (value%10>=2 && value%10<=4 && (value%100<10 || value%100>=20)) + return 2; + return 0; + } + throw std::runtime_error("Invalid plural form enumeration received!"); +} + +template +inline std::string getPluralFormTextID(std::string languageName, Numeric value, std::string textID) +{ + int formIndex = getPluralFormIndex(getLanguageOptions(languageName).pluralForms, value); + return textID + '.' + std::to_string(formIndex); +} + } From c5eeaa6526bb3f24eab27b1080d512341c5da864 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 10 Nov 2023 17:18:49 +0200 Subject: [PATCH 1121/1248] Added non-linear slider for better simturn duration selection --- Mods/vcmi/config/vcmi/english.json | 3 +- client/gui/InterfaceObjectConfigurable.cpp | 21 ++++++++-- client/lobby/OptionsTabBase.cpp | 16 ++++++-- client/widgets/Slider.cpp | 37 +++++++++++++++++- client/widgets/Slider.h | 18 ++++++++- config/widgets/turnOptionsTab.json | 45 +++++++++++++++------- 6 files changed, 114 insertions(+), 26 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index fc102ff64..89ea0a1f8 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -236,7 +236,8 @@ "vcmi.optionsTab.simturnsMin.hover" : "At least for", "vcmi.optionsTab.simturnsMax.hover" : "At most for", "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", - "vcmi.optionsTab.simturns.help" : "Play simultaneously at least for specified number of days, after which simultaneous turns will stay active until specified day or until players make contact. Contacts between players during this period are blocked", + "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked", + "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player", "vcmi.optionsTab.simturnsAI.help" : "", // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 935dc88da..fecc485da 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -501,12 +501,25 @@ std::shared_ptr InterfaceObjectConfigurable::buildSlider(const JsonNode auto position = readPosition(config["position"]); int length = config["size"].Integer(); auto style = config["style"].String() == "brown" ? CSlider::BROWN : CSlider::BLUE; - auto itemsVisible = config["itemsVisible"].Integer(); - auto itemsTotal = config["itemsTotal"].Integer(); auto value = config["selected"].Integer(); bool horizontal = config["orientation"].String() == "horizontal"; - const auto & result = - std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL, style); + auto orientation = horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL; + + std::shared_ptr result; + + if (config["items"].isNull()) + { + auto itemsVisible = config["itemsVisible"].Integer(); + auto itemsTotal = config["itemsTotal"].Integer(); + + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), itemsVisible, itemsTotal, value, orientation, style); + } + else + { + auto items = config["items"].convertTo>(); + result = std::make_shared(position, length, callbacks_int.at(config["callback"].String()), items, value, orientation, style); + } + if(!config["scrollBounds"].isNull()) { diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 39c9e52c7..2aefb19f3 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -42,12 +42,14 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) addCallback("setSimturnDurationMin", [&](int index){ SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; info.requiredTurns = index; + info.optionalTurns = std::max(info.optionalTurns, index); CSH->setSimturnsInfo(info); }); addCallback("setSimturnDurationMax", [&](int index){ SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; info.optionalTurns = index; + info.requiredTurns = std::min(info.requiredTurns, index); CSH->setSimturnsInfo(info); }); @@ -192,14 +194,19 @@ void OptionsTabBase::recreate() { auto const & generateSimturnsDurationText = [](int days) -> std::string { + if (days == 0) + return CGI->generaltexth->translate("core.genrltxt.523"); + + if (days >= 1000000) // Not "unlimited" but close enough + return CGI->generaltexth->translate("core.turndur.10"); + bool canUseMonth = days % 28 == 0 && days >= 28*2; bool canUseWeek = days % 7 == 0 && days >= 7*2; - MetaString message; int value = days; std::string text = "vcmi.optionsTab.simturns.days"; - if (canUseWeek) + if (canUseWeek && !canUseMonth) { value = days / 7; text = "vcmi.optionsTab.simturns.weeks"; @@ -211,6 +218,7 @@ void OptionsTabBase::recreate() text = "vcmi.optionsTab.simturns.months"; } + MetaString message; message.appendTextID(Languages::getPluralFormTextID( CGI->generaltexth->getPreferredLanguage(), value, text)); message.replaceNumber(value); return message.toString(); @@ -218,10 +226,10 @@ void OptionsTabBase::recreate() //Simultaneous turns if(auto turnSlider = widget("simturnsDurationMin")) - turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.requiredTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.requiredTurns); if(auto turnSlider = widget("simturnsDurationMax")) - turnSlider->scrollTo(SEL->getStartInfo()->simturnsInfo.optionalTurns); + turnSlider->setValue(SEL->getStartInfo()->simturnsInfo.optionalTurns); if(auto w = widget("labelSimturnsDurationValueMin")) w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.requiredTurns)); diff --git a/client/widgets/Slider.cpp b/client/widgets/Slider.cpp index 955c01649..b8dfc98bf 100644 --- a/client/widgets/Slider.cpp +++ b/client/widgets/Slider.cpp @@ -69,6 +69,11 @@ int CSlider::getValue() const return value; } +void CSlider::setValue(int to) +{ + scrollTo(value); +} + int CSlider::getCapacity() const { return capacity; @@ -119,7 +124,7 @@ void CSlider::scrollTo(int to) updateSliderPos(); - moved(to); + moved(getValue()); } void CSlider::clickPressed(const Point & cursorPosition) @@ -164,7 +169,7 @@ bool CSlider::receiveEvent(const Point &position, int eventType) const return testTarget.isInside(position); } -CSlider::CSlider(Point position, int totalw, std::function Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) +CSlider::CSlider(Point position, int totalw, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style) : Scrollable(LCLICK | DRAG, position, orientation ), capacity(Capacity), amount(Amount), @@ -297,3 +302,31 @@ void CSlider::scrollToMax() { scrollTo(amount); } + +SliderNonlinear::SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style) + : CSlider(position, length, Moved, 1, values.size(), Value, orientation, style) + , scaledValues(values) +{ + +} + +int SliderNonlinear::getValue() const +{ + return scaledValues.at(CSlider::getValue()); +} + +void SliderNonlinear::setValue(int to) +{ + size_t nearest = 0; + + for(size_t i = 0; i < scaledValues.size(); ++i) + { + int nearestDistance = std::abs(to - scaledValues[nearest]); + int currentDistance = std::abs(to - scaledValues[i]); + + if(currentDistance < nearestDistance) + nearest = i; + } + + scrollTo(nearest); +} diff --git a/client/widgets/Slider.h b/client/widgets/Slider.h index f7a8cad10..b6e8be677 100644 --- a/client/widgets/Slider.h +++ b/client/widgets/Slider.h @@ -59,10 +59,11 @@ public: /// Amount modifier void setAmount(int to); + virtual void setValue(int to); /// Accessors int getAmount() const; - int getValue() const; + virtual int getValue() const; int getCapacity() const; void addCallback(std::function callback); @@ -80,7 +81,20 @@ public: /// @param Capacity maximal number of visible at once elements /// @param Amount total amount of elements, including not visible /// @param Value starting position - CSlider(Point position, int length, std::function Moved, int Capacity, int Amount, + CSlider(Point position, int length, const std::function & Moved, int Capacity, int Amount, int Value, Orientation orientation, EStyle style = BROWN); ~CSlider(); }; + +class SliderNonlinear : public CSlider +{ + /// If non-empty then slider has non-linear values, e.g. if slider is at position 5 out of 10 then actual "value" is not 5, but 5th value in this vector + std::vector scaledValues; + + using CSlider::setAmount; // make private +public: + void setValue(int to) override; + int getValue() const override; + + SliderNonlinear(Point position, int length, const std::function & Moved, const std::vector & values, int Value, Orientation orientation, EStyle style); +}; diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index e18e13b99..d39b7ed89 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -68,23 +68,35 @@ "type": "texture", "image": "DiBoxBck", "color" : "blue", - "rect": {"x" : 64, "y" : 416, "w": 316, "h": 102} + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124} }, { "type": "transparentFilledRectangle", - "rect": {"x" : 64, "y" : 416, "w": 316, "h": 102}, + "rect": {"x" : 64, "y" : 394, "w": 316, "h": 124}, "color": [0, 0, 0, 0], "colorLine": [64, 80, 128, 128] }, { "type": "transparentFilledRectangle", - "rect": {"x" : 64, "y" : 465, "w": 316, "h": 1}, + "rect": {"x" : 65, "y" : 416, "w": 314, "h": 1}, "color": [0, 0, 0, 0], "colorLine": [80, 96, 160, 128] }, { "type": "transparentFilledRectangle", - "rect": {"x" : 64, "y" : 466, "w": 316, "h": 1}, + "rect": {"x" : 65, "y" : 417, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [32, 40, 128, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 466, "w": 314, "h": 1}, + "color": [0, 0, 0, 0], + "colorLine": [80, 96, 160, 128] + }, + { + "type": "transparentFilledRectangle", + "rect": {"x" : 65, "y" : 467, "w": 314, "h": 1}, "color": [0, 0, 0, 0], "colorLine": [32, 40, 128, 128] }, @@ -189,7 +201,7 @@ "alignment": "left", "color": "white", "text": "vcmi.optionsTab.simturnsMax.hover", - "position": {"x": 70, "y": 445} + "position": {"x": 70, "y": 470} }, { @@ -199,8 +211,7 @@ "position": {"x": 178, "y": 420}, "size": 200, "callback": "setSimturnDurationMin", - "itemsVisible": 1, - "itemsTotal": 28, + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168 ], "selected": 0, "style": "blue", "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, @@ -210,11 +221,10 @@ "name": "simturnsDurationMax", "type": "slider", "orientation": "horizontal", - "position": {"x": 178, "y": 445}, + "position": {"x": 178, "y": 470}, "size": 200, "callback": "setSimturnDurationMax", - "itemsVisible": 1, - "itemsTotal": 28, + "items": [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 28, 35, 42, 49, 56, 84, 112, 140, 168, 1000000 ], "selected": 0, "style": "blue", "scrollBounds": {"x": 0, "y": 0, "w": 194, "h": 32}, @@ -236,16 +246,25 @@ "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 453} + "position": {"x": 320, "y": 478} }, { "type" : "label", - "text": "vcmi.optionsTab.simturns.help", + "text": "vcmi.optionsTab.simturnsMin.help", "type": "multiLineLabel", "font": "tiny", "alignment": "center", "color": "white", - "rect": {"x": 70, "y": 470, "w": 300, "h": 40} + "rect": {"x": 70, "y": 430, "w": 300, "h": 40} + }, + { + "type" : "label", + "text": "vcmi.optionsTab.simturnsMax.help", + "type": "multiLineLabel", + "font": "tiny", + "alignment": "center", + "color": "white", + "rect": {"x": 70, "y": 480, "w": 300, "h": 40} }, { "position": {"x": 70, "y": 535}, From d1ae5bbee5331ffa307bf9ebd2acd76308d83484 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 10 Nov 2023 21:21:10 +0200 Subject: [PATCH 1122/1248] Implement AI simturns checkbox --- client/lobby/OptionsTabBase.cpp | 9 +++++++++ config/widgets/turnOptionsTab.json | 1 + 2 files changed, 10 insertions(+) diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 2aefb19f3..18953eb97 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -53,6 +53,12 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) CSH->setSimturnsInfo(info); }); + addCallback("setSimturnAI", [&](int index){ + SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; + info.allowHumanWithAI = index; + CSH->setSimturnsInfo(info); + }); + //helper function to parse string containing time to integer reflecting time in seconds //assumed that input string can be modified by user, function shall support user's intention // normal: 2:00, 12:30 @@ -237,6 +243,9 @@ void OptionsTabBase::recreate() if(auto w = widget("labelSimturnsDurationValueMax")) w->setText(generateSimturnsDurationText(SEL->getStartInfo()->simturnsInfo.optionalTurns)); + if(auto buttonSimturnsAI = widget("buttonSimturnsAI")) + buttonSimturnsAI->setSelectedSilent(SEL->getStartInfo()->simturnsInfo.allowHumanWithAI); + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; //classic timer diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index d39b7ed89..feef3ae61 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -267,6 +267,7 @@ "rect": {"x": 70, "y": 480, "w": 300, "h": 40} }, { + "name": "buttonSimturnsAI", "position": {"x": 70, "y": 535}, "type": "toggleButton", "image": "lobby/checkbox" From eb20e29b2af0c26831b743abc3714c39f3c07cba Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 12 Nov 2023 00:11:49 +0200 Subject: [PATCH 1123/1248] Updated translations --- Mods/vcmi/config/vcmi/chinese.json | 16 ++++---- Mods/vcmi/config/vcmi/czech.json | 5 --- Mods/vcmi/config/vcmi/english.json | 4 +- Mods/vcmi/config/vcmi/russian.json | 15 +++---- Mods/vcmi/config/vcmi/ukrainian.json | 59 ++++++++++++++++++++++++++- Mods/vcmi/config/vcmi/vietnamese.json | 15 +++---- 6 files changed, 84 insertions(+), 30 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 70c7b3275..8f1b403eb 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -202,14 +202,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "同盟关系", "vcmi.randomMapTab.widgets.roadTypesLabel" : "道路类型", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{额外计时器}\n\n当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{转动计时器}\n\n当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{战斗计时器}\n\n战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{堆栈计时器}\n\n当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", - "vcmi.optionsTab.widgets.labelTimer" : "计时器", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "经典计时器", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "国际象棋计时器", - + "vcmi.optionsTab.chessFieldBase.hover" : "额外计时器", + "vcmi.optionsTab.chessFieldTurn.hover" : "转动计时器", + "vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器", + "vcmi.optionsTab.chessFieldCreature.hover" : "堆栈计时器", + "vcmi.optionsTab.chessFieldBase.help" : "当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", + "vcmi.optionsTab.chessFieldTurn.help" : "当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", + "vcmi.optionsTab.chessFieldBattle.help" : "战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", + "vcmi.optionsTab.chessFieldCreature.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!", diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index f1c561b97..fbe240eb8 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -192,15 +192,10 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team Alignments", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Druhy cest", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Extra timer}\n\nStarts counting down when the {turn timer} reaches zero. It is set only once at the beginning of the game. When this timer reaches zero, the player's turn ends.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Turn timer}\n\nStarts counting down when the player starts their turn on the adventure map. It is reset to its initial value at the start of each turn. Any unused turn time will be added to the {Extra timer} if it is in use.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Battle timer}\n\nCounts down during battles when the {stack timer} reaches zero. It is reset to its initial value at the start of each battle. If the timer reaches zero, the currently active stack will defend.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Stack timer}\n\nStarts counting down when the player is selecting an action for the current stack during battle. It resets to its initial value after the stack's action is completed.", "vcmi.optionsTab.widgets.labelTimer" : "Časovač", "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač", "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač", - // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulace! Zvládli jste přežít. Vítězství je vaše!", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 89ea0a1f8..4ede80f5c 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -228,7 +228,7 @@ "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", "vcmi.optionsTab.chessFieldCreature.hover" : "Unit timer", "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", - "vcmi.optionsTab.chessFieldTurn.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to Base Timer at turn's end.", + "vcmi.optionsTab.chessFieldTurn.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", "vcmi.optionsTab.chessFieldCreature.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn.", @@ -238,7 +238,7 @@ "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", "vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked", "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player", - "vcmi.optionsTab.simturnsAI.help" : "", + "vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.", // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language // Using this information, VCMI will automatically select correct plural form for every possible amount diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 24b7d7931..3374b9df4 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -201,13 +201,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Распределение команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Виды дорог", - "vcmi.optionsTab.widgets.chessFieldBase.help" : "{Время игрока}\n\nОбратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", - "vcmi.optionsTab.widgets.chessFieldTurn.help" : "{Время на ход}\n\nОбратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", - "vcmi.optionsTab.widgets.chessFieldBattle.help" : "{Время на битву}\n\nОбратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", - "vcmi.optionsTab.widgets.chessFieldCreature.help" : "{Время на отряд}\n\nОбратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", - "vcmi.optionsTab.widgets.labelTimer" : "Таймер", - "vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Классические часы", - "vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Шахматные часы", + "vcmi.optionsTab.chessFieldBase.hover" : "Время игрока", + "vcmi.optionsTab.chessFieldTurn.hover" : "Время на ход", + "vcmi.optionsTab.chessFieldBattle.hover" : "Время на битву", + "vcmi.optionsTab.chessFieldCreature.hover" : "Время на отряд", + "vcmi.optionsTab.chessFieldBase.help" : "Обратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", + "vcmi.optionsTab.chessFieldTurn.help" : "Обратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", + "vcmi.optionsTab.chessFieldBattle.help" : "Обратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", + "vcmi.optionsTab.chessFieldCreature.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", "mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов", "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 81a7244db..6e01eafb4 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -30,6 +30,11 @@ "vcmi.capitalColors.6" : "Сизий", "vcmi.capitalColors.7" : "Рожевий", + "vcmi.heroOverview.startingArmy" : "Початкові загони", + "vcmi.heroOverview.warMachine" : "Бойові машини", + "vcmi.heroOverview.secondarySkills" : "Навички", + "vcmi.heroOverview.spells" : "Закляття", + "vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот", "vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами", "vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту", @@ -37,8 +42,21 @@ "vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії", "vcmi.radialWheel.splitUnit" : "Розділити істоту в інший слот", + "vcmi.radialWheel.heroGetArmy" : "Отримати армію іншого героя", + "vcmi.radialWheel.heroSwapArmy" : "Обміняти армії героїв", + "vcmi.radialWheel.heroExchange" : "Відкрити вікно обміну", + "vcmi.radialWheel.heroGetArtifacts" : "Отримати артефакти іншого героя", + "vcmi.radialWheel.heroSwapArtifacts" : "Обміняти артефакти героїв", + "vcmi.radialWheel.heroDismiss" : "Звільнити цього героя", + + "vcmi.radialWheel.moveTop" : "Перемістити на початок", + "vcmi.radialWheel.moveUp" : "Перемістити вгору", + "vcmi.radialWheel.moveDown" : "Перемістити вниз", + "vcmi.radialWheel.moveBottom" : "Перемістити у кінець", + "vcmi.mainMenu.serverConnecting" : "Підключення...", "vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:", + "vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання", "vcmi.mainMenu.serverClosing" : "Завершення...", "vcmi.mainMenu.hostTCP" : "Створити TCP/IP гру", "vcmi.mainMenu.joinTCP" : "Приєднатися до TCP/IP гри", @@ -46,9 +64,14 @@ "vcmi.lobby.filepath" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", + "vcmi.lobby.scenarioName" : "Scenario name", + "vcmi.lobby.mapPreview" : "Огляд мапи", + "vcmi.lobby.noPreview" : "огляд недоступний", + "vcmi.lobby.noUnderground" : "немає підземелля", "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", + "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", @@ -84,6 +107,8 @@ "vcmi.systemOptions.framerateButton.help" : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Тактильний відгук", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Розширення інтерфейсу", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", @@ -167,7 +192,7 @@ "vcmi.townHall.greetingCustomBonus" : "%s дає вам +%d %s%s", "vcmi.townHall.greetingCustomUntil" : " до наступної битви.", "vcmi.townHall.greetingInTownMagicWell" : "%s повністю відновлює ваш запас очків магії.", - + "vcmi.logicalExpressions.anyOf" : "Будь-що з перерахованого:", "vcmi.logicalExpressions.allOf" : "Все з перерахованого:", "vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:", @@ -195,6 +220,38 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Розподіл команд", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Види доріг", + "vcmi.optionsTab.turnOptions.hover" : "Параметри ходів", + "vcmi.optionsTab.turnOptions.help" : "Виберіть опції таймера ходів та одночасних ходів", + + "vcmi.optionsTab.chessFieldBase.hover" : "Основний таймер", + "vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу", + "vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви", + "vcmi.optionsTab.chessFieldCreature.hover" : "Таймер загону", + "vcmi.optionsTab.chessFieldBase.help" : "Використовується коли {Таймер ходу} вичерпується. Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", + "vcmi.optionsTab.chessFieldTurn.help" : "Використовується під час ходу. Встановлюється кожен хід. Залишок додається до {основного таймеру} у кінці ходу", + "vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.", + "vcmi.optionsTab.chessFieldCreature.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку кожної дії.", + + "vcmi.optionsTab.simturns" : "Одночасні ходи", + "vcmi.optionsTab.simturnsMin.hover" : "Щонайменше", + "vcmi.optionsTab.simturnsMax.hover" : "Щонайбільше", + "vcmi.optionsTab.simturnsAI.hover" : "(Експериментально) Одночасні ходи ШІ", + "vcmi.optionsTab.simturnsMin.help" : "Грати одночасно обрану кількість днів. Контакти між гравцями у цей період заблоковані", + "vcmi.optionsTab.simturnsMax.help" : "Грати одночасно обрану кількість днів чи до першого контакту з іншим гравцем", + "vcmi.optionsTab.simturnsAI.help" : "{Одночасні ходи ШІ}\nЕкспериментальна опція. Дозволяє гравцям-ШІ діяти одночасно с гравцями-людьми якщо одночасні ходи увімкнені.", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d днів", + "vcmi.optionsTab.simturns.days.1" : " %d день", + "vcmi.optionsTab.simturns.days.2" : " %d дні", + "vcmi.optionsTab.simturns.weeks.0" : " %d тижнів", + "vcmi.optionsTab.simturns.weeks.1" : " %d тиждень", + "vcmi.optionsTab.simturns.weeks.2" : " %d тижні", + "vcmi.optionsTab.simturns.months.0" : " %d місяців", + "vcmi.optionsTab.simturns.months.1" : " %d місяць", + "vcmi.optionsTab.simturns.months.2" : " %d місяці", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Ворогу вдалося вижити до сьогоднішнього дня. Він переміг!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Вітаємо! Вам вдалося залишитися в живих. Перемога за вами!", diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json index 890e699d1..0750201cf 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -198,13 +198,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel": "Sắp đội", "vcmi.randomMapTab.widgets.roadTypesLabel": "Kiểu đường xá", - "vcmi.optionsTab.widgets.chessFieldBase.help": "{Thời gian thêm}\n\nBắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", - "vcmi.optionsTab.widgets.chessFieldTurn.help": "{Thời gian lượt}\n\nBắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", - "vcmi.optionsTab.widgets.chessFieldBattle.help": "{Thời gian trận đánh}\n\nĐếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", - "vcmi.optionsTab.widgets.chessFieldCreature.help": "{Thời gian lính}\n\nBắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", - "vcmi.optionsTab.widgets.labelTimer": "Đồng hồ", - "vcmi.optionsTab.widgets.timerModeSwitch.classic": "Đồng hồ cơ bản", - "vcmi.optionsTab.widgets.timerModeSwitch.chess": "Đồng hồ đánh cờ", + "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", + "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", + "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh", + "vcmi.optionsTab.chessFieldCreature.hover" : "Thời gian lính", + "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", + "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", + "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", + "vcmi.optionsTab.chessFieldCreature.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", From f65f4b128567cd0cc4015a9afaba3cea44094364 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 19:49:41 +0200 Subject: [PATCH 1124/1248] Disable new UI if enhancements are off --- Mods/vcmi/config/vcmi/ukrainian.json | 2 +- client/lobby/CLobbyScreen.cpp | 17 ++++++++++++----- config/widgets/turnOptionsTab.json | 4 ++-- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 6e01eafb4..3a50c17f9 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -227,7 +227,7 @@ "vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу", "vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви", "vcmi.optionsTab.chessFieldCreature.hover" : "Таймер загону", - "vcmi.optionsTab.chessFieldBase.help" : "Використовується коли {Таймер ходу} вичерпується. Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", + "vcmi.optionsTab.chessFieldBase.help" : "Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", "vcmi.optionsTab.chessFieldTurn.help" : "Використовується під час ходу. Встановлюється кожен хід. Залишок додається до {основного таймеру} у кінці ходу", "vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.", "vcmi.optionsTab.chessFieldCreature.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку кожної дії.", diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 4537ce332..135e0d078 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -25,12 +25,13 @@ #include "../../CCallback.h" -#include "../CGameInfo.h" -#include "../../lib/networkPacks/PacksForLobby.h" +#include "../../lib/CConfigHandler.h" #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/networkPacks/PacksForLobby.h" #include "../../lib/rmg/CMapGenOptions.h" +#include "../CGameInfo.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) : CSelectionBase(screenType), bonusSel(nullptr) @@ -51,7 +52,8 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) }); buttonOptions = std::make_shared(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS); - buttonTurnOptions = std::make_shared(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE); + if(settings["general"]["enableUiEnhancements"].Bool()) + buttonTurnOptions = std::make_shared(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE); }; buttonChat = std::make_shared(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT); @@ -149,7 +151,10 @@ void CLobbyScreen::toggleMode(bool host) auto buttonColor = host ? Colors::WHITE : Colors::ORANGE; buttonSelect->addTextOverlay(CGI->generaltexth->allTexts[500], FONT_SMALL, buttonColor); buttonOptions->addTextOverlay(CGI->generaltexth->allTexts[501], FONT_SMALL, buttonColor); - buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor); + + if (buttonTurnOptions) + buttonTurnOptions->addTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.turnOptions.hover"), FONT_SMALL, buttonColor); + if(buttonRMG) { buttonRMG->addTextOverlay(CGI->generaltexth->allTexts[740], FONT_SMALL, buttonColor); @@ -157,7 +162,9 @@ void CLobbyScreen::toggleMode(bool host) } buttonSelect->block(!host); buttonOptions->block(!host); - buttonTurnOptions->block(!host); + + if (buttonTurnOptions) + buttonTurnOptions->block(!host); if(CSH->mi) { diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index feef3ae61..b84aba8e8 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -237,7 +237,7 @@ "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 428} + "position": {"x": 278, "y": 428} }, { "name": "labelSimturnsDurationValueMax", @@ -246,7 +246,7 @@ "alignment": "center", "color": "white", "text": "", - "position": {"x": 320, "y": 478} + "position": {"x": 278, "y": 478} }, { "type" : "label", From 8392125b047be8083750d398db7db008de6295cd Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 13 Nov 2023 19:50:53 +0200 Subject: [PATCH 1125/1248] Disable experimental simturns AI option by default --- lib/StartInfo.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 925df407c..208ef2706 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -30,7 +30,7 @@ struct DLL_LINKAGE SimturnsInfo /// Maximum number of turns that might be played simultaneously unless contact is detected int optionalTurns = 0; /// If set to true, human and 1 AI can act at the same time - bool allowHumanWithAI = true; + bool allowHumanWithAI = false; template void serialize(Handler &h, const int version) From f7a31865f3a1bc39416b5a457e88801192338881 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 14 Nov 2023 21:52:30 +0100 Subject: [PATCH 1126/1248] basic touch introduction implementation --- client/CMakeLists.txt | 2 ++ client/CPlayerInterface.cpp | 3 ++ client/battle/BattleInterface.cpp | 3 ++ client/windows/CTutorialWindow.cpp | 56 ++++++++++++++++++++++++++++++ client/windows/CTutorialWindow.h | 33 ++++++++++++++++++ 5 files changed, 97 insertions(+) create mode 100644 client/windows/CTutorialWindow.cpp create mode 100644 client/windows/CTutorialWindow.h diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 6257722a2..e14411ea5 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -128,6 +128,7 @@ set(client_SRCS windows/CQuestLog.cpp windows/CSpellWindow.cpp windows/CTradeWindow.cpp + windows/CTutorialWindow.cpp windows/CWindowObject.cpp windows/CreaturePurchaseCard.cpp windows/GUIClasses.cpp @@ -297,6 +298,7 @@ set(client_HEADERS windows/CQuestLog.h windows/CSpellWindow.h windows/CTradeWindow.h + windows/CTutorialWindow.h windows/CWindowObject.h windows/CreaturePurchaseCard.h windows/GUIClasses.h diff --git a/client/CPlayerInterface.cpp b/client/CPlayerInterface.cpp index 4ced1dbc9..722048dd5 100644 --- a/client/CPlayerInterface.cpp +++ b/client/CPlayerInterface.cpp @@ -57,6 +57,7 @@ #include "windows/CQuestLog.h" #include "windows/CSpellWindow.h" #include "windows/CTradeWindow.h" +#include "windows/CTutorialWindow.h" #include "windows/GUIClasses.h" #include "windows/InfoWindows.h" @@ -275,6 +276,8 @@ void CPlayerInterface::gamePause(bool pause) void CPlayerInterface::yourTurn(QueryID queryID) { + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_ADVENTUREMAP); + EVENT_HANDLER_CALLED_BY_CLIENT; { LOCPLINT = this; diff --git a/client/battle/BattleInterface.cpp b/client/battle/BattleInterface.cpp index 8cdb5a7e7..ab7cffcb4 100644 --- a/client/battle/BattleInterface.cpp +++ b/client/battle/BattleInterface.cpp @@ -29,6 +29,7 @@ #include "../gui/CursorHandler.h" #include "../gui/CGuiHandler.h" #include "../gui/WindowHandler.h" +#include "../windows/CTutorialWindow.h" #include "../render/Canvas.h" #include "../adventureMap/AdventureMapInterface.h" @@ -148,6 +149,8 @@ void BattleInterface::openingEnd() tacticNextStack(nullptr); activateStack(); battleOpeningDelayActive = false; + + CTutorialWindow::openWindowFirstTime(TutorialMode::TOUCH_BATTLE); } BattleInterface::~BattleInterface() diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp new file mode 100644 index 000000000..c176e93ac --- /dev/null +++ b/client/windows/CTutorialWindow.cpp @@ -0,0 +1,56 @@ +/* + * CTutorialWindow.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 "CTutorialWindow.h" + +#include "../eventsSDL/InputHandler.h" +#include "../../lib/CConfigHandler.h" +#include "../../lib/CondSh.h" +#include "../CPlayerInterface.h" + +#include "../gui/CGuiHandler.h" +#include "../gui/WindowHandler.h" +#include "../widgets/Images.h" + +CTutorialWindow::CTutorialWindow(const TutorialMode & m) + : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m } +{ + OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; + + pos = Rect(pos.x, pos.y, 480, 320); + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + + updateShadow(); + + center(); + + addUsedEvents(LCLICK); +} + +void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) +{ + if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) + { + if(LOCPLINT) + LOCPLINT->showingDialog->set(true); + GH.windows().pushWindow(std::make_shared(m)); + + Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)]; + s->Bool() = true; + } +} + +void CTutorialWindow::clickPressed(const Point & cursorPosition) +{ + close(); + + if(LOCPLINT) + LOCPLINT->showingDialog->setn(false); +} \ No newline at end of file diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h new file mode 100644 index 000000000..9b4b8ddcc --- /dev/null +++ b/client/windows/CTutorialWindow.h @@ -0,0 +1,33 @@ +/* + * CTutorialWindow.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 "../windows/CWindowObject.h" + +class CFilledTexture; + +enum TutorialMode +{ + TOUCH_ADVENTUREMAP, + TOUCH_BATTLE +}; + +class CTutorialWindow : public CWindowObject +{ + TutorialMode mode; + std::shared_ptr background; + +public: + CTutorialWindow(const TutorialMode & m); + static void openWindowFirstTime(const TutorialMode & m); + + + void clickPressed(const Point & cursorPosition) override; +}; \ No newline at end of file From cb92778e00e0acd62d3367921069afd7e3fdfe93 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:39:11 +0100 Subject: [PATCH 1127/1248] add ui --- client/windows/CTutorialWindow.cpp | 29 +++++++++++++++++++++++++---- client/windows/CTutorialWindow.h | 19 +++++++++++++++---- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index c176e93ac..9055919ba 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -16,15 +16,18 @@ #include "../CPlayerInterface.h" #include "../gui/CGuiHandler.h" +#include "../gui/Shortcut.h" #include "../gui/WindowHandler.h" #include "../widgets/Images.h" +#include "../widgets/Buttons.h" +#include "../widgets/TextControls.h" CTutorialWindow::CTutorialWindow(const TutorialMode & m) : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m } { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(pos.x, pos.y, 480, 320); + pos = Rect(pos.x, pos.y, 540, 400); //video: 480x320 background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); updateShadow(); @@ -32,6 +35,14 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) center(); addUsedEvents(LCLICK); + + buttonOk = std::make_shared(Point(239, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 + buttonLeft = std::make_shared(Point(5, 177), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 + buttonRight = std::make_shared(Point(513, 177), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 + buttonRight->block(true); + + labelTitle = std::make_shared(270, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); + labelInformation = std::make_shared(Rect(5, 50, 530, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); } void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) @@ -47,10 +58,20 @@ void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) } } -void CTutorialWindow::clickPressed(const Point & cursorPosition) +void CTutorialWindow::close() { - close(); - if(LOCPLINT) LOCPLINT->showingDialog->setn(false); + + WindowBase::close(); +} + +void CTutorialWindow::next() +{ + +} + +void CTutorialWindow::previous() +{ + } \ No newline at end of file diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index 9b4b8ddcc..afcc07e09 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -12,6 +12,9 @@ #include "../windows/CWindowObject.h" class CFilledTexture; +class CButton; +class CLabel; +class CMultiLineLabel; enum TutorialMode { @@ -24,10 +27,18 @@ class CTutorialWindow : public CWindowObject TutorialMode mode; std::shared_ptr background; + std::shared_ptr buttonOk; + std::shared_ptr buttonLeft; + std::shared_ptr buttonRight; + + std::shared_ptr labelTitle; + std::shared_ptr labelInformation; + + void close(); + void next(); + void previous(); + public: CTutorialWindow(const TutorialMode & m); - static void openWindowFirstTime(const TutorialMode & m); - - - void clickPressed(const Point & cursorPosition) override; + static void openWindowFirstTime(const TutorialMode & m); }; \ No newline at end of file From 05efb12d8263547c70b3bc1f54c1f911f0f1c75b Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Tue, 14 Nov 2023 22:50:22 +0100 Subject: [PATCH 1128/1248] format fix --- client/windows/CTutorialWindow.cpp | 41 +++++++++++++++--------------- client/windows/CTutorialWindow.h | 26 +++++++++---------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 9055919ba..4869c8fe0 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -28,42 +28,43 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; pos = Rect(pos.x, pos.y, 540, 400); //video: 480x320 - background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); + background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); updateShadow(); center(); - addUsedEvents(LCLICK); + addUsedEvents(LCLICK); - buttonOk = std::make_shared(Point(239, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 - buttonLeft = std::make_shared(Point(5, 177), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 - buttonRight = std::make_shared(Point(513, 177), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 - buttonRight->block(true); + buttonOk = std::make_shared(Point(239, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 + buttonLeft = std::make_shared(Point(5, 177), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 + buttonRight = std::make_shared(Point(513, 177), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 - labelTitle = std::make_shared(270, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); - labelInformation = std::make_shared(Rect(5, 50, 530, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); + buttonLeft->block(true); + + labelTitle = std::make_shared(270, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); + labelInformation = std::make_shared(Rect(5, 50, 530, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); } void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) { - if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) - { - if(LOCPLINT) - LOCPLINT->showingDialog->set(true); - GH.windows().pushWindow(std::make_shared(m)); + if(GH.input().hasTouchInputDevice() && !persistentStorage["gui"]["tutorialCompleted" + std::to_string(m)].Bool()) + { + if(LOCPLINT) + LOCPLINT->showingDialog->set(true); + GH.windows().pushWindow(std::make_shared(m)); - Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)]; - s->Bool() = true; - } + Settings s = persistentStorage.write["gui"]["tutorialCompleted" + std::to_string(m)]; + s->Bool() = true; + } } void CTutorialWindow::close() { - if(LOCPLINT) + if(LOCPLINT) LOCPLINT->showingDialog->setn(false); - WindowBase::close(); + WindowBase::close(); } void CTutorialWindow::next() @@ -73,5 +74,5 @@ void CTutorialWindow::next() void CTutorialWindow::previous() { - -} \ No newline at end of file + +} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index afcc07e09..896351dfd 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -18,27 +18,27 @@ class CMultiLineLabel; enum TutorialMode { - TOUCH_ADVENTUREMAP, - TOUCH_BATTLE + TOUCH_ADVENTUREMAP, + TOUCH_BATTLE }; class CTutorialWindow : public CWindowObject { - TutorialMode mode; - std::shared_ptr background; + TutorialMode mode; + std::shared_ptr background; - std::shared_ptr buttonOk; - std::shared_ptr buttonLeft; - std::shared_ptr buttonRight; + std::shared_ptr buttonOk; + std::shared_ptr buttonLeft; + std::shared_ptr buttonRight; std::shared_ptr labelTitle; std::shared_ptr labelInformation; - void close(); - void next(); - void previous(); + void close(); + void next(); + void previous(); public: - CTutorialWindow(const TutorialMode & m); - static void openWindowFirstTime(const TutorialMode & m); -}; \ No newline at end of file + CTutorialWindow(const TutorialMode & m); + static void openWindowFirstTime(const TutorialMode & m); +}; From faea00a7e1472013006b16b6eaab4b3fe67cb72e Mon Sep 17 00:00:00 2001 From: gamestales-com Date: Mon, 13 Nov 2023 22:26:26 +0100 Subject: [PATCH 1129/1248] Use arraytxt for object bonus description --- config/objects/rewardableOnceVisitable.json | 10 +++--- lib/JsonNode.cpp | 4 +-- lib/networkPacks/NetPacksLib.cpp | 39 ++++++++++----------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 353b2d666..02b77567b 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -154,7 +154,7 @@ "onVisited" : [ { "message" : 163, - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ], "rewards" : [ @@ -162,25 +162,25 @@ "appearChance" : { "max" : 30 }, "message" : 162, "artifacts" : [ { "class" : "TREASURE" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 30, "max" : 80 }, "message" : 162, "artifacts" : [ { "class" : "MINOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 80, "max" : 95 }, "message" : 162, "artifacts" : [ { "class" : "MAJOR" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] }, { "appearChance" : { "min" : 95 }, "message" : 162, "artifacts" : [ { "class" : "RELIC" } ], - "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE" } ] + "bonuses" : [ { "type" : "MORALE", "val" : -3, "duration" : "ONE_BATTLE", "description" : 104 } ] } ] } diff --git a/lib/JsonNode.cpp b/lib/JsonNode.cpp index 869e632ce..d49f1e8b4 100644 --- a/lib/JsonNode.cpp +++ b/lib/JsonNode.cpp @@ -427,14 +427,14 @@ static void loadBonusSubtype(BonusSubtypeID & subtype, BonusType type, const Jso if (node.isNumber()) // Compatibility code for 1.3 or older { - logMod->warn("Bonus subtype must be string!"); + logMod->warn("Bonus subtype must be string! (%s)", node.meta); subtype = BonusCustomSubtype(node.Integer()); return; } if (!node.isString()) { - logMod->warn("Bonus subtype must be string!"); + logMod->warn("Bonus subtype must be string! (%s)", node.meta); subtype = BonusSubtypeID(); return; } diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index ac5488e35..8639b6e6a 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -993,29 +993,28 @@ void GiveBonus::applyGs(CGameState *gs) std::string &descr = b->description; - if(bdescr.empty() && (bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE)) - { - if (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE) - { - descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" - } - else if(bonus.source == BonusSource::TOWN_STRUCTURE) - { - descr = bonus.description; - return; - } - else - { - descr = bdescr.toString(); - } - } - else + if(!bdescr.empty()) { descr = bdescr.toString(); } - // Some of(?) versions of H3 use %s here instead of %d. Try to replace both of them - boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); - boost::replace_first(descr, "%s", std::to_string(std::abs(bonus.val))); + else if(!descr.empty()) + { + //use preseet description + } + else if((bonus.type == BonusType::LUCK || bonus.type == BonusType::MORALE) + && (bonus.source == BonusSource::OBJECT_TYPE || bonus.source == BonusSource::OBJECT_INSTANCE)) + { + //no description, use generic + //?could use allways when Type == BonusDuration::Type::ONE_BATTLE + descr = VLC->generaltexth->arraytxt[bonus.val > 0 ? 110 : 109]; //+/-%d Temporary until next battle" + } + else + { + logGlobal->debug("Empty bonus decription. Type=%d", (int) bonus.type); + } + // Some of(?) versions of H3 use " %s" here instead of %d. Try to replace both of them + boost::replace_first(descr, "%d", std::to_string(std::abs(bonus.val))); // " +/-%d Temporary until next battle + boost::replace_first(descr, " %s", boost::str(boost::format(" %+d") % bonus.val)); // " %s" in arraytxt.69, fountian of fortune } void ChangeObjPos::applyGs(CGameState *gs) From 0842f5afee3f7e47d7e448fab20adea7af7e7f4e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 5 Nov 2023 15:24:26 +0200 Subject: [PATCH 1130/1248] Removed remaining usages of std::vector --- client/lobby/OptionsTab.cpp | 5 +- lib/BattleFieldHandler.cpp | 5 -- lib/BattleFieldHandler.h | 1 - lib/CArtHandler.cpp | 19 +++--- lib/CArtHandler.h | 4 +- lib/CCreatureHandler.cpp | 12 ---- lib/CCreatureHandler.h | 1 - lib/CGameInfoCallback.cpp | 24 ++++---- lib/CGameInfoCallback.h | 8 ++- lib/CHeroHandler.cpp | 18 ++---- lib/CHeroHandler.h | 4 +- lib/CSkillHandler.cpp | 10 +++- lib/CSkillHandler.h | 2 +- lib/CTownHandler.cpp | 29 ++++----- lib/CTownHandler.h | 2 +- lib/IGameCallback.cpp | 2 +- lib/IHandlerBase.h | 7 --- lib/JsonRandom.cpp | 8 +-- lib/ObstacleHandler.cpp | 5 -- lib/ObstacleHandler.h | 1 - lib/RiverHandler.cpp | 5 -- lib/RiverHandler.h | 1 - lib/RoadHandler.cpp | 5 -- lib/RoadHandler.h | 1 - lib/ScriptHandler.cpp | 5 -- lib/ScriptHandler.h | 1 - lib/TerrainHandler.cpp | 5 -- lib/TerrainHandler.h | 1 - lib/constants/EntityIdentifiers.h | 4 ++ lib/gameState/CGameState.cpp | 5 +- .../CObjectClassesHandler.cpp | 5 -- .../CObjectClassesHandler.h | 2 - lib/mapObjects/CGHeroInstance.cpp | 6 +- lib/mapObjects/CGMarket.cpp | 14 ----- lib/mapObjects/CGMarket.h | 1 - lib/mapObjects/CGTownInstance.cpp | 3 +- lib/mapping/CMap.cpp | 59 ++++++------------- lib/mapping/CMap.h | 6 +- lib/mapping/CMapHeader.h | 4 +- lib/mapping/MapFormatH3M.cpp | 16 ++--- lib/mapping/MapFormatJson.cpp | 17 ++---- lib/mapping/MapReaderH3M.cpp | 37 +++--------- lib/mapping/MapReaderH3M.h | 13 ++-- lib/pathfinder/TurnInfo.cpp | 5 +- lib/pathfinder/TurnInfo.h | 2 +- lib/rmg/CMapGenerator.cpp | 21 +++---- lib/rmg/CRmgTemplate.cpp | 8 +-- lib/rmg/CRmgTemplateStorage.cpp | 6 -- lib/rmg/CRmgTemplateStorage.h | 1 - lib/rmg/RmgMap.cpp | 2 +- lib/serializer/BinaryDeserializer.h | 9 --- lib/serializer/BinarySerializer.h | 8 --- lib/spells/CSpellHandler.cpp | 10 ++-- lib/spells/CSpellHandler.h | 2 +- 54 files changed, 140 insertions(+), 317 deletions(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 0584a1999..f9a211c25 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -407,10 +407,7 @@ OptionsTab::SelectionWindow::SelectionWindow(PlayerColor _color, SelType _type) selectedHero = initialHero; selectedBonus = initialBonus; allowedFactions = SEL->getPlayerInfo(color).allowedFactions; - std::vector allowedHeroesFlag = SEL->getMapInfo()->mapHeader->allowedHeroes; - for(int i = 0; i < allowedHeroesFlag.size(); i++) - if(allowedHeroesFlag[i]) - allowedHeroes.insert(HeroTypeID(i)); + allowedHeroes = SEL->getMapInfo()->mapHeader->allowedHeroes; for(auto & player : SEL->getStartInfo()->playerInfos) { diff --git a/lib/BattleFieldHandler.cpp b/lib/BattleFieldHandler.cpp index b5bcedb90..9b64a5336 100644 --- a/lib/BattleFieldHandler.cpp +++ b/lib/BattleFieldHandler.cpp @@ -54,11 +54,6 @@ const std::vector & BattleFieldHandler::getTypeNames() const return types; } -std::vector BattleFieldHandler::getDefaultAllowed() const -{ - return std::vector(); -} - int32_t BattleFieldInfo::getIndex() const { return battlefield.getNum(); diff --git a/lib/BattleFieldHandler.h b/lib/BattleFieldHandler.h index d51aeac57..ec5535bc0 100644 --- a/lib/BattleFieldHandler.h +++ b/lib/BattleFieldHandler.h @@ -71,7 +71,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 7b31c813e..97351d3b6 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -651,24 +651,27 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) return false; } -void CArtHandler::initAllowedArtifactsList(const std::vector &allowed) +void CArtHandler::initAllowedArtifactsList(const std::set & allowed) { allowedArtifacts.clear(); - for (ArtifactID i=ArtifactID::SPELLBOOK; i < ArtifactID(static_cast(objects.size())); i.advance(1)) + for (ArtifactID i : allowed) { - if (allowed[i] && legalArtifact(ArtifactID(i))) + if (legalArtifact(ArtifactID(i))) allowedArtifacts.push_back(objects[i]); //keep im mind that artifact can be worn by more than one type of bearer } } -std::vector CArtHandler::getDefaultAllowed() const +std::set CArtHandler::getDefaultAllowed() const { - std::vector allowedArtifacts; - allowedArtifacts.resize(127, true); - allowedArtifacts.resize(141, false); - allowedArtifacts.resize(size(), true); + std::set allowedArtifacts; + + for (auto artifact : objects) + { + if (!artifact->isCombined()) + allowedArtifacts.insert(artifact->getId()); + } return allowedArtifacts; } diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index d8417d558..f9673a450 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -149,7 +149,7 @@ public: static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor bool legalArtifact(const ArtifactID & id); - void initAllowedArtifactsList(const std::vector &allowed); //allowed[art_id] -> 0 if not allowed, 1 if allowed + void initAllowedArtifactsList(const std::set & allowed); static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true); static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true); @@ -161,7 +161,7 @@ public: void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 54c3083da..630992b51 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -666,18 +666,6 @@ const std::vector & CCreatureHandler::getTypeNames() const return typeNames; } -std::vector CCreatureHandler::getDefaultAllowed() const -{ - std::vector ret; - - ret.reserve(objects.size()); - for(const CCreature * crea : objects) - { - ret.push_back(crea ? !crea->special : false); - } - return ret; -} - void CCreatureHandler::loadCrExpMod() { if (VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_EXPERIENCE)) //reading default stack experience values diff --git a/lib/CCreatureHandler.h b/lib/CCreatureHandler.h index e7f6ccf6a..6d821e0c7 100644 --- a/lib/CCreatureHandler.h +++ b/lib/CCreatureHandler.h @@ -238,7 +238,6 @@ public: std::vector loadLegacyData() override; - std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index 44de7bee3..a62a69c43 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -53,19 +53,19 @@ const PlayerSettings * CGameInfoCallback::getPlayerSettings(PlayerColor color) c return &gs->scenarioOps->getIthPlayersSettings(color); } -bool CGameInfoCallback::isAllowed(int32_t type, int32_t id) const +bool CGameInfoCallback::isAllowed(SpellID id) const { - switch(type) - { - case 0: - return gs->map->allowedSpells[id]; - case 1: - return gs->map->allowedArtifact[id]; - case 2: - return gs->map->allowedAbilities[id]; - default: - ERROR_RET_VAL_IF(1, "Wrong type!", false); - } + return gs->map->allowedSpells.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(ArtifactID id) const +{ + return gs->map->allowedArtifact.count(id) != 0; +} + +bool CGameInfoCallback::isAllowed(SecondarySkill id) const +{ + return gs->map->allowedAbilities.count(id) != 0; } std::optional CGameInfoCallback::getPlayerID() const diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index 794719187..aeec312d3 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -57,7 +57,9 @@ public: // //various virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month // const StartInfo * getStartInfo(bool beforeRandomization = false)const; - virtual bool isAllowed(int32_t type, int32_t id) const = 0; //type: 0 - spell; 1- artifact; 2 - secondary skill + virtual bool isAllowed(SpellID id) const = 0; + virtual bool isAllowed(ArtifactID id) const = 0; + virtual bool isAllowed(SecondarySkill id) const = 0; //player virtual std::optional getPlayerID() const = 0; @@ -143,7 +145,9 @@ public: //various int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const; - bool isAllowed(int32_t type, int32_t id) const override; //type: 0 - spell; 1- artifact; 2 - secondary skill + bool isAllowed(SpellID id) const override; + bool isAllowed(ArtifactID id) const override; + bool isAllowed(SecondarySkill id) const override; //player std::optional getPlayerID() const override; diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 904880616..f71549e62 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -393,11 +393,6 @@ void CHeroClassHandler::afterLoadFinalization() } } -std::vector CHeroClassHandler::getDefaultAllowed() const -{ - return std::vector(size(), true); -} - CHeroClassHandler::~CHeroClassHandler() = default; CHeroHandler::~CHeroHandler() = default; @@ -763,18 +758,15 @@ ui64 CHeroHandler::reqExp (ui32 level) const } } -std::vector CHeroHandler::getDefaultAllowed() const +std::set CHeroHandler::getDefaultAllowed() const { - // Look Data/HOTRAITS.txt for reference - std::vector allowedHeroes; - allowedHeroes.reserve(size()); + std::set result; for(const CHero * hero : objects) - { - allowedHeroes.push_back(hero && !hero->special); - } + if (hero && !hero->special) + result.insert(hero->getId()); - return allowedHeroes; + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CHeroHandler.h b/lib/CHeroHandler.h index 83b2e878f..3103594bc 100644 --- a/lib/CHeroHandler.h +++ b/lib/CHeroHandler.h @@ -165,8 +165,6 @@ public: void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - ~CHeroClassHandler(); protected: @@ -206,7 +204,7 @@ public: CHeroHandler(); ~CHeroHandler(); - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index 1d467b846..cea99072d 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -256,10 +256,14 @@ void CSkillHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSkillHandler::getDefaultAllowed() const +std::set CSkillHandler::getDefaultAllowed() const { - std::vector allowedSkills(objects.size(), true); - return allowedSkills; + std::set result; + + for (auto const & skill : objects) + result.insert(skill->getId()); + + return result; } VCMI_LIB_NAMESPACE_END diff --git a/lib/CSkillHandler.h b/lib/CSkillHandler.h index 920689e9e..fe699edcb 100644 --- a/lib/CSkillHandler.h +++ b/lib/CSkillHandler.h @@ -90,7 +90,7 @@ public: void afterLoadFinalization() override; void beforeValidate(JsonNode & object) override; - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index d9541d545..b20f8f872 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1259,31 +1259,28 @@ void CTownHandler::initializeWarMachines() warMachinesToLoad.clear(); } -std::vector CTownHandler::getDefaultAllowed() const +std::set CTownHandler::getDefaultAllowed() const { - std::vector allowedFactions; - allowedFactions.reserve(objects.size()); + std::set allowedFactions; + for(auto town : objects) - { - allowedFactions.push_back(town->town != nullptr); - } + if (town->town != nullptr) + allowedFactions.insert(town->getId()); + return allowedFactions; } std::set CTownHandler::getAllowedFactions(bool withTown) const { - std::set allowedFactions; - std::vector allowed; - if (withTown) - allowed = getDefaultAllowed(); - else - allowed.resize( objects.size(), true); + if (!withTown) + return getDefaultAllowed(); - for (size_t i=0; i(i)); + std::set result; + for(auto town : objects) + result.insert(town->getId()); + + return result; - return allowedFactions; } const std::vector & CTownHandler::getTypeNames() const diff --git a/lib/CTownHandler.h b/lib/CTownHandler.h index 170cc2f1f..5a8421b35 100644 --- a/lib/CTownHandler.h +++ b/lib/CTownHandler.h @@ -350,7 +350,7 @@ public: void loadCustom() override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; std::set getAllowedFactions(bool withTown = true) const; static void loadSpecialBuildingBonuses(const JsonNode & source, BonusList & bonusList, CBuilding * building); diff --git a/lib/IGameCallback.cpp b/lib/IGameCallback.cpp index e14a5ac1c..f40aebcd8 100644 --- a/lib/IGameCallback.cpp +++ b/lib/IGameCallback.cpp @@ -160,7 +160,7 @@ void CPrivilegedInfoCallback::getAllowedSpells(std::vector & out, std:: { const spells::Spell * spell = SpellID(i).toSpell(); - if (!isAllowed(0, spell->getIndex())) + if (!isAllowed(spell->getId())) continue; if (level.has_value() && spell->getLevel() != level) diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index f513f3593..615e23f54 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -44,13 +44,6 @@ public: /// allows handler to do post-loading step for validation or integration of loaded data virtual void afterLoadFinalization(){}; - /** - * Gets a list of objects that are allowed by default on maps - * - * @return a list of allowed objects, the index is the object id - */ - virtual std::vector getDefaultAllowed() const = 0; - virtual ~IHandlerBase(){} }; diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index f960f0137..e87f9d6d2 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -164,7 +164,7 @@ namespace JsonRandom if(!allowedClasses.empty() && !allowedClasses.count(art->aClass)) continue; - if(!IObjectInterface::cb->isAllowed(1, art->getIndex())) + if(!IObjectInterface::cb->isAllowed(art->getId())) continue; if(!allowedPositions.empty()) @@ -344,7 +344,7 @@ namespace JsonRandom { std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) - if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + if (IObjectInterface::cb->isAllowed(skill->getId())) defaultSkills.insert(skill->getId()); std::set potentialPicks = filterKeys(value, defaultSkills, variables); @@ -366,7 +366,7 @@ namespace JsonRandom { std::set defaultSkills; for(const auto & skill : VLC->skillh->objects) - if (IObjectInterface::cb->isAllowed(2, skill->getIndex())) + if (IObjectInterface::cb->isAllowed(skill->getId())) defaultSkills.insert(skill->getId()); for(const auto & element : value.Vector()) @@ -406,7 +406,7 @@ namespace JsonRandom { std::set defaultSpells; for(const auto & spell : VLC->spellh->objects) - if (IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if (IObjectInterface::cb->isAllowed(spell->getId())) defaultSpells.insert(spell->getId()); std::set potentialPicks = filterKeys(value, defaultSpells, variables); diff --git a/lib/ObstacleHandler.cpp b/lib/ObstacleHandler.cpp index fbfc7efe2..f9ef0f0b2 100644 --- a/lib/ObstacleHandler.cpp +++ b/lib/ObstacleHandler.cpp @@ -116,11 +116,6 @@ std::vector ObstacleHandler::loadLegacyData() return {}; } -std::vector ObstacleHandler::getDefaultAllowed() const -{ - return {}; -} - const std::vector & ObstacleHandler::getTypeNames() const { static const std::vector types = { "obstacle" }; diff --git a/lib/ObstacleHandler.h b/lib/ObstacleHandler.h index 36268426a..e47d1c0ea 100644 --- a/lib/ObstacleHandler.h +++ b/lib/ObstacleHandler.h @@ -71,7 +71,6 @@ public: const std::vector & getTypeNames() const override; std::vector loadLegacyData() override; - std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RiverHandler.cpp b/lib/RiverHandler.cpp index 3cc04eb37..5154505b4 100644 --- a/lib/RiverHandler.cpp +++ b/lib/RiverHandler.cpp @@ -68,11 +68,6 @@ std::vector RiverTypeHandler::loadLegacyData() return {}; } -std::vector RiverTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RiverType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RiverHandler.h b/lib/RiverHandler.h index 5c028ddbd..c070a53a8 100644 --- a/lib/RiverHandler.h +++ b/lib/RiverHandler.h @@ -71,7 +71,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/RoadHandler.cpp b/lib/RoadHandler.cpp index 458d1588d..85380d460 100644 --- a/lib/RoadHandler.cpp +++ b/lib/RoadHandler.cpp @@ -59,11 +59,6 @@ std::vector RoadTypeHandler::loadLegacyData() return {}; } -std::vector RoadTypeHandler::getDefaultAllowed() const -{ - return {}; -} - std::string RoadType::getJsonKey() const { return modScope + ":" + identifier; diff --git a/lib/RoadHandler.h b/lib/RoadHandler.h index dbb9476b5..f58d5b9bb 100644 --- a/lib/RoadHandler.h +++ b/lib/RoadHandler.h @@ -61,7 +61,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/ScriptHandler.cpp b/lib/ScriptHandler.cpp index d672c74dd..5fe39407c 100644 --- a/lib/ScriptHandler.cpp +++ b/lib/ScriptHandler.cpp @@ -217,11 +217,6 @@ const Script * ScriptHandler::resolveScript(const std::string & name) const } } -std::vector ScriptHandler::getDefaultAllowed() const -{ - return std::vector(); -} - std::vector ScriptHandler::loadLegacyData() { return std::vector(); diff --git a/lib/ScriptHandler.h b/lib/ScriptHandler.h index e2ff3a7e4..a0177fc65 100644 --- a/lib/ScriptHandler.h +++ b/lib/ScriptHandler.h @@ -97,7 +97,6 @@ public: const Script * resolveScript(const std::string & name) const; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; ScriptPtr loadFromJson(vstd::CLoggerBase * logger, const std::string & scope, const JsonNode & json, const std::string & identifier) const; diff --git a/lib/TerrainHandler.cpp b/lib/TerrainHandler.cpp index f7a3eb96f..805f41362 100644 --- a/lib/TerrainHandler.cpp +++ b/lib/TerrainHandler.cpp @@ -140,11 +140,6 @@ std::vector TerrainTypeHandler::loadLegacyData() return result; } -std::vector TerrainTypeHandler::getDefaultAllowed() const -{ - return {}; -} - bool TerrainType::isLand() const { return !isWater(); diff --git a/lib/TerrainHandler.h b/lib/TerrainHandler.h index 447e1efc3..68103abd1 100644 --- a/lib/TerrainHandler.h +++ b/lib/TerrainHandler.h @@ -109,7 +109,6 @@ public: virtual const std::vector & getTypeNames() const override; virtual std::vector loadLegacyData() override; - virtual std::vector getDefaultAllowed() const override; }; VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index a4dcc9d0e..a789b2e85 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -23,6 +23,7 @@ class HeroType; class CHero; class HeroTypeService; class Faction; +class Skill; class RoadType; class RiverType; class TerrainType; @@ -308,6 +309,9 @@ public: static std::string entityType(); static si32 decode(const std::string& identifier); static std::string encode(const si32 index); + + const CSkill * toSkill() const; + const Skill * toEntity(const Services * services) const; }; class DLL_LINKAGE PrimarySkill : public Identifier diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index beb8eeda4..d40f541b4 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1877,10 +1877,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) std::set CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllowed) const { - std::set ret; - for(int i = 0; i < map->allowedHeroes.size(); i++) - if(map->allowedHeroes[i] || alsoIncludeNotAllowed) - ret.insert(HeroTypeID(i)); + std::set ret = map->allowedHeroes; for(const auto & playerSettingPair : scenarioOps->playerInfos) //remove uninitialized yet heroes picked for start by other players { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 1ef76010c..712b8c910 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -304,11 +304,6 @@ void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID objects[ID]->objects[subID] = nullptr; } -std::vector CObjectClassesHandler::getDefaultAllowed() const -{ - return std::vector(); //TODO? -} - TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index ef42a32a1..835301916 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -101,8 +101,6 @@ public: void beforeValidate(JsonNode & object) override; void afterLoadFinalization() override; - std::vector getDefaultAllowed() const override; - /// Queries to detect loaded objects std::set knownObjects() const; std::set knownSubObjects(MapObjectID primaryID) const; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 6fa16d8ce..00b4e5dbc 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -212,7 +212,7 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const if ( !canLearnSkill()) return false; - if (!cb->isAllowed(2, which)) + if (!cb->isAllowed(which)) return false; if (getSecSkillLevel(which) > 0) @@ -777,7 +777,7 @@ void CGHeroInstance::spendMana(ServerCallback * server, const int spellCost) con bool CGHeroInstance::canCastThisSpell(const spells::Spell * spell) const { - const bool isAllowed = IObjectInterface::cb->isAllowed(0, spell->getIndex()); + const bool isAllowed = IObjectInterface::cb->isAllowed(spell->getId()); const bool inSpellBook = vstd::contains(spells, spell->getId()) && hasSpellbook(); const bool specificBonus = hasBonusOfType(BonusType::SPELL, BonusSubtypeID(spell->getId())); @@ -841,7 +841,7 @@ bool CGHeroInstance::canLearnSpell(const spells::Spell * spell, bool allowBanned return false;//creature abilities can not be learned } - if(!allowBanned && !IObjectInterface::cb->isAllowed(0, spell->getIndex())) + if(!allowBanned && !IObjectInterface::cb->isAllowed(spell->getId())) { logGlobal->warn("Hero %s try to learn banned spell %s", nodeName(), spell->getNameTranslated()); return false;//banned spells should not be learned diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index b429a84fe..f6ac45633 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -96,20 +96,6 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const cb->sendAndApply(&saa); } -void CGUniversity::initObj(CRandomGenerator & rand) -{ - CGMarket::initObj(rand); - - std::vector toChoose; - for(int i = 0; i < VLC->skillh->size(); ++i) - { - if(!vstd::contains(skills, i) && cb->isAllowed(2, i)) - { - toChoose.push_back(i); - } - } -} - std::vector CGUniversity::availableItemsIds(EMarketMode mode) const { switch (mode) diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index aff11ce56..386988dfc 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -67,7 +67,6 @@ public: std::vector skills; //available skills std::vector availableItemsIds(EMarketMode mode) const override; - void initObj(CRandomGenerator & rand) override;//set skills for trade void onHeroVisit(const CGHeroInstance * h) const override; //open window template void serialize(Handler &h, const int version) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index f2c331f50..49fa6800b 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1201,8 +1201,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) } { - std::vector standard = VLC->spellh->getDefaultAllowed(); - JsonSerializeFormat::LIC spellsLIC(standard, SpellID::decode, SpellID::encode); + JsonSerializeFormat::LIC spellsLIC(VLC->spellh->getDefaultAllowed(), SpellID::decode, SpellID::encode); if(handler.saving) { diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index e116bf4e6..98d629adb 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -621,67 +621,44 @@ void CMap::banWaterContent() void CMap::banWaterSpells() { - for (int j = 0; j < allowedSpells.size(); j++) + vstd::erase_if(allowedSpells, [&](SpellID spell) { - if (allowedSpells[j]) - { - auto* spell = dynamic_cast(VLC->spells()->getByIndex(j)); - if (spell->onlyOnWaterMap && !isWaterMap()) - { - allowedSpells[j] = false; - } - } - } + return spell.toSpell()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterArtifacts() { - for (int j = 0; j < allowedArtifact.size(); j++) + vstd::erase_if(allowedArtifact, [&](ArtifactID artifact) { - if (allowedArtifact[j]) - { - auto* art = dynamic_cast(VLC->artifacts()->getByIndex(j)); - if (art->onlyOnWaterMap && !isWaterMap()) - { - allowedArtifact[j] = false; - } - } - } + return artifact.toArtifact()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterSkills() { - for (int j = 0; j < allowedAbilities.size(); j++) + vstd::erase_if(allowedAbilities, [&](SecondarySkill skill) { - if (allowedAbilities[j]) - { - auto* skill = dynamic_cast(VLC->skills()->getByIndex(j)); - if (skill->onlyOnWaterMap && !isWaterMap()) - { - allowedAbilities[j] = false; - } - } - } + return skill.toSkill()->onlyOnWaterMap && !isWaterMap(); + }); } void CMap::banWaterHeroes() { - for (int j = 0; j < allowedHeroes.size(); j++) + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) { - if (allowedHeroes[j]) - { - auto* h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap()) || (h->onlyOnMapWithoutWater && isWaterMap())) - { - banHero(HeroTypeID(j)); - } - } - } + return hero.toHeroType()->onlyOnWaterMap && !isWaterMap(); + }); + + vstd::erase_if(allowedHeroes, [&](HeroTypeID hero) + { + return hero.toHeroType()->onlyOnMapWithoutWater && isWaterMap(); + }); } void CMap::banHero(const HeroTypeID & id) { - allowedHeroes.at(id) = false; + allowedHeroes.erase(id); } void CMap::initTerrain() diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index 518b1fb6a..d1fbbc14d 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -129,9 +129,9 @@ public: std::vector rumors; std::vector disposedHeroes; std::vector > predefinedHeroes; - std::vector allowedSpells; - std::vector allowedArtifact; - std::vector allowedAbilities; + std::set allowedSpells; + std::set allowedArtifact; + std::set allowedAbilities; std::list events; int3 grailPos; int grailRadius; diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index b492237dc..2a83e7d0e 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -238,8 +238,8 @@ public: std::vector players; /// The default size of the vector is PlayerColor::PLAYER_LIMIT. ui8 howManyTeams; - std::vector allowedHeroes; - std::vector reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign + std::set allowedHeroes; + std::set reservedCampaignHeroes; /// Heroes that have placeholders in this map and are reserverd for campaign bool areAnyPlayers; /// Unused. True if there are any playable players on the map. diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 5bd59ca3b..02624ccdf 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -692,7 +692,7 @@ void CMapLoaderH3M::readAllowedHeroes() for (uint32_t i = 0; i < placeholdersQty; ++i) { auto heroID = reader->readHero(); - mapHeader->reservedCampaignHeroes.push_back(heroID); + mapHeader->reservedCampaignHeroes.insert(heroID); } } } @@ -764,13 +764,13 @@ void CMapLoaderH3M::readAllowedArtifacts() { for(CArtifact * artifact : VLC->arth->objects) if(artifact->isCombined()) - map->allowedArtifact[artifact->getId()] = false; + map->allowedArtifact.erase(artifact->getId()); } if(!features.levelAB) { - map->allowedArtifact[ArtifactID::VIAL_OF_DRAGON_BLOOD] = false; - map->allowedArtifact[ArtifactID::ARMAGEDDONS_BLADE] = false; + map->allowedArtifact.erase(ArtifactID::VIAL_OF_DRAGON_BLOOD); + map->allowedArtifact.erase(ArtifactID::ARMAGEDDONS_BLADE); } // Messy, but needed @@ -780,7 +780,7 @@ void CMapLoaderH3M::readAllowedArtifacts() { if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) { - map->allowedArtifact[cond.objectType] = false; + map->allowedArtifact.erase(cond.objectType); } return cond; }; @@ -1160,7 +1160,7 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share auto defaultAllowed = VLC->skillh->getDefaultAllowed(); for(int skillID = features.skillsCount; skillID < defaultAllowed.size(); ++skillID) - if(defaultAllowed[skillID]) + if(defaultAllowed.count(skillID)) allowedAbilities.insert(SecondarySkill(skillID)); } @@ -2085,7 +2085,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) { auto artid = reader->readArtifact(); guard->quest->mission.artifacts.push_back(artid); - map->allowedArtifact[artid] = false; //these are unavailable for random generation + map->allowedArtifact.erase(artid); //these are unavailable for random generation } break; } @@ -2212,7 +2212,7 @@ CGObjectInstance * CMapLoaderH3M::readTown(const int3 & position, std::shared_pt //add all spells from mods for(int i = features.spellsCount; i < defaultAllowed.size(); ++i) - if(defaultAllowed[i]) + if(defaultAllowed.count(i)) object->possibleSpells.emplace_back(i); } diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 62ad052f4..fda1e4329 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -389,28 +389,19 @@ RoadType * CMapFormatJson::getRoadByCode(const std::string & code) void CMapFormatJson::serializeAllowedFactions(JsonSerializeFormat & handler, std::set & value) const { - //TODO: unify allowed factions with others - make them std::vector - - std::vector temp; - temp.resize(VLC->townh->size(), false); - auto standard = VLC->townh->getDefaultAllowed(); + std::set temp; if(handler.saving) { for(auto faction : VLC->townh->objects) if(faction->town && vstd::contains(value, faction->getId())) - temp[static_cast(faction->getIndex())] = true; + temp.insert(faction->getId()); } - handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, standard, temp); + handler.serializeLIC("allowedFactions", &FactionID::decode, &FactionID::encode, VLC->townh->getDefaultAllowed(), temp); if(!handler.saving) - { - value.clear(); - for (std::size_t i=0; i(i)); - } + value = temp; } void CMapFormatJson::serializeHeader(JsonSerializeFormat & handler) diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 772c251de..00334d443 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -266,12 +266,12 @@ void MapReaderH3M::readBitmaskHeroClassesSized(std::set & dest, boo readBitmask(dest, classesBytes, classesCount, invert); } -void MapReaderH3M::readBitmaskHeroes(std::vector & dest, bool invert) +void MapReaderH3M::readBitmaskHeroes(std::set & dest, bool invert) { readBitmask(dest, features.heroesBytes, features.heroesCount, invert); } -void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) +void MapReaderH3M::readBitmaskHeroesSized(std::set & dest, bool invert) { uint32_t heroesCount = readUInt32(); uint32_t heroesBytes = (heroesCount + 7) / 8; @@ -280,12 +280,12 @@ void MapReaderH3M::readBitmaskHeroesSized(std::vector & dest, bool invert) readBitmask(dest, heroesBytes, heroesCount, invert); } -void MapReaderH3M::readBitmaskArtifacts(std::vector &dest, bool invert) +void MapReaderH3M::readBitmaskArtifacts(std::set &dest, bool invert) { readBitmask(dest, features.artifactsBytes, features.artifactsCount, invert); } -void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool invert) +void MapReaderH3M::readBitmaskArtifactsSized(std::set &dest, bool invert) { uint32_t artifactsCount = reader->readUInt32(); uint32_t artifactsBytes = (artifactsCount + 7) / 8; @@ -294,28 +294,18 @@ void MapReaderH3M::readBitmaskArtifactsSized(std::vector &dest, bool inver readBitmask(dest, artifactsBytes, artifactsCount, invert); } -void MapReaderH3M::readBitmaskSpells(std::vector & dest, bool invert) -{ - readBitmask(dest, features.spellsBytes, features.spellsCount, invert); -} - void MapReaderH3M::readBitmaskSpells(std::set & dest, bool invert) { readBitmask(dest, features.spellsBytes, features.spellsCount, invert); } -void MapReaderH3M::readBitmaskSkills(std::vector & dest, bool invert) -{ - readBitmask(dest, features.skillsBytes, features.skillsCount, invert); -} - void MapReaderH3M::readBitmaskSkills(std::set & dest, bool invert) { readBitmask(dest, features.skillsBytes, features.skillsCount, invert); } template -void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, const int objectsToRead, bool invert) +void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) { for(int byte = 0; byte < bytesToRead; ++byte) { @@ -331,26 +321,13 @@ void MapReaderH3M::readBitmask(std::vector & dest, const int bytesToRead, Identifier h3mID(index); Identifier vcmiID = remapIdentifier(h3mID); - if (vcmiID.getNum() >= dest.size()) - dest.resize(vcmiID.getNum() + 1); - dest[vcmiID.getNum()] = result; + if (result) + dest.insert(vcmiID); } } } } -template -void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) -{ - std::vector bitmap; - bitmap.resize(objectsToRead, false); - readBitmask(bitmap, bytesToRead, objectsToRead, invert); - - for(int i = 0; i < bitmap.size(); i++) - if(bitmap[i]) - dest.insert(static_cast(i)); -} - int3 MapReaderH3M::readInt3() { int3 p; diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index 9c34aba3d..92da10ab0 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -50,13 +50,11 @@ public: void readBitmaskPlayers(std::set & dest, bool invert); void readBitmaskResources(std::set & dest, bool invert); void readBitmaskHeroClassesSized(std::set & dest, bool invert); - void readBitmaskHeroes(std::vector & dest, bool invert); - void readBitmaskHeroesSized(std::vector & dest, bool invert); - void readBitmaskArtifacts(std::vector & dest, bool invert); - void readBitmaskArtifactsSized(std::vector & dest, bool invert); - void readBitmaskSpells(std::vector & dest, bool invert); + void readBitmaskHeroes(std::set & dest, bool invert); + void readBitmaskHeroesSized(std::set & dest, bool invert); + void readBitmaskArtifacts(std::set & dest, bool invert); + void readBitmaskArtifactsSized(std::set & dest, bool invert); void readBitmaskSpells(std::set & dest, bool invert); - void readBitmaskSkills(std::vector & dest, bool invert); void readBitmaskSkills(std::set & dest, bool invert); int3 readInt3(); @@ -88,9 +86,6 @@ private: template void readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert); - template - void readBitmask(std::vector & dest, int bytesToRead, int objectsToRead, bool invert); - MapFormatFeaturesH3M features; MapIdentifiersH3M remapper; diff --git a/lib/pathfinder/TurnInfo.cpp b/lib/pathfinder/TurnInfo.cpp index 8b8763cdb..1f32139a6 100644 --- a/lib/pathfinder/TurnInfo.cpp +++ b/lib/pathfinder/TurnInfo.cpp @@ -23,7 +23,8 @@ TurnInfo::BonusCache::BonusCache(const TConstBonusListPtr & bl) for(const auto & terrain : VLC->terrainTypeHandler->objects) { auto selector = Selector::typeSubtype(BonusType::NO_TERRAIN_PENALTY, BonusSubtypeID(terrain->getId())); - noTerrainPenalty.push_back(static_cast(bl->getFirst(selector))); + if (bl->getFirst(selector)) + noTerrainPenalty.insert(terrain->getId()); } freeShipBoarding = static_cast(bl->getFirst(Selector::type()(BonusType::FREE_SHIP_BOARDING))); @@ -87,7 +88,7 @@ bool TurnInfo::hasBonusOfType(BonusType type, BonusSubtypeID subtype) const case BonusType::WATER_WALKING: return bonusCache->waterWalking; case BonusType::NO_TERRAIN_PENALTY: - return bonusCache->noTerrainPenalty[subtype.getNum()]; + return bonusCache->noTerrainPenalty.count(subtype.as()); } return static_cast( diff --git a/lib/pathfinder/TurnInfo.h b/lib/pathfinder/TurnInfo.h index 4390e13c6..6fff27e6f 100644 --- a/lib/pathfinder/TurnInfo.h +++ b/lib/pathfinder/TurnInfo.h @@ -21,7 +21,7 @@ struct DLL_LINKAGE TurnInfo /// This is certainly not the best design ever and certainly can be improved /// Unfortunately for pathfinder that do hundreds of thousands calls onus system add too big overhead struct BonusCache { - std::vector noTerrainPenalty; + std::set noTerrainPenalty; bool freeShipBoarding; bool flyingMovement; int flyingMovementVal; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index be317d717..3c66b3c39 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -493,19 +493,16 @@ const std::vector CMapGenerator::getAllPossibleHeroes() const auto isWaterMap = map->getMap(this).isWaterMap(); //Skip heroes that were banned, including the ones placed in prisons std::vector ret; - for (int j = 0; j < map->getMap(this).allowedHeroes.size(); j++) + for (HeroTypeID hero : map->getMap(this).allowedHeroes) { - if (map->getMap(this).allowedHeroes[j]) + auto * h = dynamic_cast(VLC->heroTypes()->getById(hero)); + if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) { - auto * h = dynamic_cast(VLC->heroTypes()->getByIndex(j)); - if ((h->onlyOnWaterMap && !isWaterMap) || (h->onlyOnMapWithoutWater && isWaterMap)) - { - continue; - } - else - { - ret.push_back(HeroTypeID(j)); - } + continue; + } + else + { + ret.push_back(hero); } } return ret; @@ -514,7 +511,7 @@ const std::vector CMapGenerator::getAllPossibleHeroes() const void CMapGenerator::banQuestArt(const ArtifactID & id) { //TODO: Protect with mutex - map->getMap(this).allowedArtifact[id] = false; + map->getMap(this).allowedArtifact.erase(id); } void CMapGenerator::banHero(const HeroTypeID & id) diff --git a/lib/rmg/CRmgTemplate.cpp b/lib/rmg/CRmgTemplate.cpp index 5a688da6a..384d186a0 100644 --- a/lib/rmg/CRmgTemplate.cpp +++ b/lib/rmg/CRmgTemplate.cpp @@ -188,13 +188,7 @@ std::set ZoneOptions::getDefaultTerrainTypes() const std::set ZoneOptions::getDefaultTownTypes() const { - std::set defaultTowns; - auto towns = VLC->townh->getDefaultAllowed(); - for(int i = 0; i < towns.size(); ++i) - { - if(towns[i]) defaultTowns.insert(FactionID(i)); - } - return defaultTowns; + return VLC->townh->getDefaultAllowed(); } const std::set ZoneOptions::getTownTypes() const diff --git a/lib/rmg/CRmgTemplateStorage.cpp b/lib/rmg/CRmgTemplateStorage.cpp index 738d65c1a..c340219fd 100644 --- a/lib/rmg/CRmgTemplateStorage.cpp +++ b/lib/rmg/CRmgTemplateStorage.cpp @@ -51,12 +51,6 @@ void CRmgTemplateStorage::loadObject(std::string scope, std::string name, const } } -std::vector CRmgTemplateStorage::getDefaultAllowed() const -{ - //all templates are allowed - return std::vector(); -} - std::vector CRmgTemplateStorage::loadLegacyData() { return std::vector(); diff --git a/lib/rmg/CRmgTemplateStorage.h b/lib/rmg/CRmgTemplateStorage.h index 6ce5c1411..4470c51d8 100644 --- a/lib/rmg/CRmgTemplateStorage.h +++ b/lib/rmg/CRmgTemplateStorage.h @@ -24,7 +24,6 @@ class DLL_LINKAGE CRmgTemplateStorage : public IHandlerBase public: CRmgTemplateStorage() = default; - std::vector getDefaultAllowed() const override; std::vector loadLegacyData() override; /// loads single object into game. Scope is namespace of this object, same as name of source mod diff --git a/lib/rmg/RmgMap.cpp b/lib/rmg/RmgMap.cpp index 85c0c8590..cdf8e0bb0 100644 --- a/lib/rmg/RmgMap.cpp +++ b/lib/rmg/RmgMap.cpp @@ -345,7 +345,7 @@ bool RmgMap::isAllowedSpell(const SpellID & sid) const assert(sid.getNum() >= 0); if (sid.getNum() < mapInstance->allowedSpells.size()) { - return mapInstance->allowedSpells[sid]; + return mapInstance->allowedSpells.count(sid); } else return false; diff --git a/lib/serializer/BinaryDeserializer.h b/lib/serializer/BinaryDeserializer.h index 1c4858469..4c6c9b39b 100644 --- a/lib/serializer/BinaryDeserializer.h +++ b/lib/serializer/BinaryDeserializer.h @@ -197,15 +197,6 @@ public: data = static_cast(read); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void load(T & data) - { - std::vector convData; - load(convData); - convData.resize(data.size()); - range::copy(convData, data.begin()); - } - template ::value, int >::type = 0> void load(std::vector &data) { diff --git a/lib/serializer/BinarySerializer.h b/lib/serializer/BinarySerializer.h index 37a054d2d..ef2ddf416 100644 --- a/lib/serializer/BinarySerializer.h +++ b/lib/serializer/BinarySerializer.h @@ -137,14 +137,6 @@ public: save(writ); } - template < typename T, typename std::enable_if < std::is_same >::value, int >::type = 0 > - void save(const T &data) - { - std::vector convData; - std::copy(data.begin(), data.end(), std::back_inserter(convData)); - save(convData); - } - template < class T, typename std::enable_if < std::is_fundamental::value && !std::is_same::value, int >::type = 0 > void save(const T &data) { diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index ff235b245..3af7be51e 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -986,15 +986,13 @@ void CSpellHandler::beforeValidate(JsonNode & object) inheritNode("expert"); } -std::vector CSpellHandler::getDefaultAllowed() const +std::set CSpellHandler::getDefaultAllowed() const { - std::vector allowedSpells; - allowedSpells.reserve(objects.size()); + std::set allowedSpells; for(const CSpell * s : objects) - { - allowedSpells.push_back( !(s->isSpecial() || s->isCreatureAbility())); - } + if (!s->isSpecial() && !s->isCreatureAbility()) + allowedSpells.insert(s->getId()); return allowedSpells; } diff --git a/lib/spells/CSpellHandler.h b/lib/spells/CSpellHandler.h index e9de9440a..efe807831 100644 --- a/lib/spells/CSpellHandler.h +++ b/lib/spells/CSpellHandler.h @@ -349,7 +349,7 @@ public: * Gets a list of default allowed spells. OH3 spells are all allowed by default. * */ - std::vector getDefaultAllowed() const override; + std::set getDefaultAllowed() const; protected: const std::vector & getTypeNames() const override; From 10e110320b0e3120dc0a672b1c9fa3df708a1155 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 5 Nov 2023 16:35:51 +0200 Subject: [PATCH 1131/1248] Remove std::vector from Json Serializer, simplify affected code --- cmake_modules/VCMI_lib.cmake | 2 - lib/constants/EntityIdentifiers.cpp | 10 ++++ lib/mapObjects/CGTownInstance.cpp | 31 +--------- lib/serializer/ILICReader.cpp | 49 ---------------- lib/serializer/ILICReader.h | 23 -------- lib/serializer/JsonDeserializer.cpp | 72 ++---------------------- lib/serializer/JsonDeserializer.h | 6 +- lib/serializer/JsonSerializeFormat.cpp | 23 ++++---- lib/serializer/JsonSerializeFormat.h | 39 +++++++------ lib/serializer/JsonSerializer.cpp | 11 +--- lib/serializer/JsonSerializer.h | 3 +- lib/serializer/JsonUpdater.cpp | 78 ++------------------------ lib/serializer/JsonUpdater.h | 6 +- mapeditor/inspector/inspector.cpp | 6 +- mapeditor/inspector/questwidget.cpp | 6 +- mapeditor/inspector/rewardswidget.cpp | 6 +- mapeditor/mapcontroller.cpp | 22 +------- mapeditor/mapsettings/mapsettings.cpp | 38 +++++++------ mapeditor/validator.cpp | 6 +- server/CVCMIServer.cpp | 2 +- 20 files changed, 95 insertions(+), 344 deletions(-) delete mode 100644 lib/serializer/ILICReader.cpp delete mode 100644 lib/serializer/ILICReader.h diff --git a/cmake_modules/VCMI_lib.cmake b/cmake_modules/VCMI_lib.cmake index 93bf03483..bcf8c1979 100644 --- a/cmake_modules/VCMI_lib.cmake +++ b/cmake_modules/VCMI_lib.cmake @@ -182,7 +182,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.cpp ${MAIN_LIB_DIR}/serializer/JsonSerializer.cpp ${MAIN_LIB_DIR}/serializer/JsonUpdater.cpp - ${MAIN_LIB_DIR}/serializer/ILICReader.cpp ${MAIN_LIB_DIR}/spells/AbilityCaster.cpp ${MAIN_LIB_DIR}/spells/AdventureSpellMechanics.cpp @@ -555,7 +554,6 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE) ${MAIN_LIB_DIR}/serializer/JsonSerializeFormat.h ${MAIN_LIB_DIR}/serializer/JsonSerializer.h ${MAIN_LIB_DIR}/serializer/JsonUpdater.h - ${MAIN_LIB_DIR}/serializer/ILICReader.h ${MAIN_LIB_DIR}/serializer/Cast.h ${MAIN_LIB_DIR}/spells/AbilityCaster.h diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 2d908b6b7..73efb5b9c 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -236,6 +236,16 @@ std::string SecondarySkill::encode(const si32 index) return VLC->skills()->getById(SecondarySkill(index))->getJsonKey(); } +const CSkill * SecondarySkill::toSkill() const +{ + return dynamic_cast(toEntity(VLC)); +} + +const Skill * SecondarySkill::toEntity(const Services * services) const +{ + return services->skills()->getByIndex(num); +} + const CCreature * CreatureIDBase::toCreature() const { return VLC->creh->objects.at(num); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 49fa6800b..6d3cacbd6 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1201,35 +1201,8 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) } { - JsonSerializeFormat::LIC spellsLIC(VLC->spellh->getDefaultAllowed(), SpellID::decode, SpellID::encode); - - if(handler.saving) - { - for(const SpellID & id : possibleSpells) - spellsLIC.any[id.num] = true; - - for(const SpellID & id : obligatorySpells) - spellsLIC.all[id.num] = true; - } - - handler.serializeLIC("spells", spellsLIC); - - if(!handler.saving) - { - possibleSpells.clear(); - for(si32 idx = 0; idx < spellsLIC.any.size(); idx++) - { - if(spellsLIC.any[idx]) - possibleSpells.emplace_back(idx); - } - - obligatorySpells.clear(); - for(si32 idx = 0; idx < spellsLIC.all.size(); idx++) - { - if(spellsLIC.all[idx]) - obligatorySpells.emplace_back(idx); - } - } + handler.serializeIdArray( "possibleSpells", possibleSpells); + handler.serializeIdArray( "obligatorySpells", obligatorySpells); } } diff --git a/lib/serializer/ILICReader.cpp b/lib/serializer/ILICReader.cpp deleted file mode 100644 index a8bcc3793..000000000 --- a/lib/serializer/ILICReader.cpp +++ /dev/null @@ -1,49 +0,0 @@ -/* - * JsonTreeSerializer.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 "ILICReader.h" - -#include "../JsonNode.h" - -#include - -VCMI_LIB_NAMESPACE_BEGIN - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - const std::string type = typeid(decltype(this)).name(); - - const si32 rawId = decoder(identifier); - if(rawId >= 0) - { - if(rawId < value.size()) - value[rawId] = val; - else - logGlobal->error("%s::serializeLIC: id out of bounds %d", type, rawId); - } - } -} - -void ILICReader::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const -{ - for(const auto & index : part.Vector()) - { - const std::string & identifier = index.String(); - - const si32 rawId = decoder(identifier); - if(rawId != -1) - value.insert(rawId); - } -} - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/ILICReader.h b/lib/serializer/ILICReader.h deleted file mode 100644 index 83d16b10f..000000000 --- a/lib/serializer/ILICReader.h +++ /dev/null @@ -1,23 +0,0 @@ -/* - * ILICReader.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 "JsonTreeSerializer.h" - -VCMI_LIB_NAMESPACE_BEGIN - -class DLL_LINKAGE ILICReader -{ -protected: - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, bool val, std::vector & value) const; - void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; -}; - -VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonDeserializer.cpp b/lib/serializer/JsonDeserializer.cpp index 0c933aee2..a2b840777 100644 --- a/lib/serializer/JsonDeserializer.cpp +++ b/lib/serializer/JsonDeserializer.cpp @@ -131,75 +131,11 @@ void JsonDeserializer::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonDeserializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonDeserializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonDeserializer::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonDeserializer.h b/lib/serializer/JsonDeserializer.h index 9a4f52c7f..9a3aab5ee 100644 --- a/lib/serializer/JsonDeserializer.h +++ b/lib/serializer/JsonDeserializer.h @@ -9,18 +9,16 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN -class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonDeserializer: public JsonTreeSerializer { public: JsonDeserializer(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/lib/serializer/JsonSerializeFormat.cpp b/lib/serializer/JsonSerializeFormat.cpp index aff5c8eb9..4da52c445 100644 --- a/lib/serializer/JsonSerializeFormat.cpp +++ b/lib/serializer/JsonSerializeFormat.cpp @@ -91,17 +91,6 @@ size_t JsonArraySerializer::size() const return thisNode->Vector().size(); } -//JsonSerializeFormat::LIC -JsonSerializeFormat::LIC::LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder): - standard(Standard), - decoder(std::move(Decoder)), - encoder(std::move(Encoder)) -{ - any.resize(standard.size(), false); - all.resize(standard.size(), false); - none.resize(standard.size(), false); -} - JsonSerializeFormat::LICSet::LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder): standard(Standard), decoder(std::move(Decoder)), @@ -140,4 +129,16 @@ void JsonSerializeFormat::serializeBool(const std::string & fieldName, bool & va serializeBool(fieldName, value, true, false, defaultValue); } +void JsonSerializeFormat::readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const +{ + for(const auto & index : part.Vector()) + { + const std::string & identifier = index.String(); + + const si32 rawId = decoder(identifier); + if(rawId != -1) + value.insert(rawId); + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index 1d5efaeea..93dc2caa9 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -137,18 +137,6 @@ public: ///may assume that object index is valid using TEncoder = std::function; - using TSerialize = std::function; - - struct LIC - { - LIC(const std::vector & Standard, TDecoder Decoder, TEncoder Encoder); - - const std::vector & standard; - const TDecoder decoder; - const TEncoder encoder; - std::vector all, any, none; - }; - struct LICSet { LICSet(const std::set & Standard, TDecoder Decoder, TEncoder Encoder); @@ -211,13 +199,28 @@ public: * @param value target value, must be resized properly * */ - virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) = 0; + virtual void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) = 0; - /** @brief Complete serialization of Logical identifier condition - */ - virtual void serializeLIC(const std::string & fieldName, LIC & value) = 0; + template + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) + { + std::set standardInt; + std::set valueInt; - /** @brief Complete serialization of Logical identifier condition. (Special version) + for (auto entry : standard) + standardInt.insert(entry.getNum()); + + for (auto entry : value) + valueInt.insert(entry.getNum()); + + serializeLIC(fieldName, decoder, encoder, standard, value); + + value.clear(); + for (auto entry : valueInt) + value.insert(T(entry)); + } + + /** @brief Complete serialization of Logical identifier condition. * Assumes that all values are allowed by default, and standard contains them */ virtual void serializeLIC(const std::string & fieldName, LICSet & value) = 0; @@ -454,6 +457,8 @@ protected: virtual void serializeInternal(std::string & value) = 0; virtual void serializeInternal(int64_t & value) = 0; + void readLICPart(const JsonNode & part, const JsonSerializeFormat::TDecoder & decoder, std::set & value) const; + private: const IInstanceResolver * instanceResolver; diff --git a/lib/serializer/JsonSerializer.cpp b/lib/serializer/JsonSerializer.cpp index 15a050d0e..aa1553e3a 100644 --- a/lib/serializer/JsonSerializer.cpp +++ b/lib/serializer/JsonSerializer.cpp @@ -95,7 +95,7 @@ void JsonSerializer::serializeInternal(int64_t & value) currentObject->Integer() = value; } -void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { assert(standard.size() == value.size()); if(standard == value) @@ -104,15 +104,6 @@ void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder writeLICPart(fieldName, "anyOf", encoder, value); } -void JsonSerializer::serializeLIC(const std::string & fieldName, LIC & value) -{ - if(value.any != value.standard) - writeLICPart(fieldName, "anyOf", value.encoder, value.any); - - writeLICPart(fieldName, "allOf", value.encoder, value.all); - writeLICPart(fieldName, "noneOf", value.encoder, value.none); -} - void JsonSerializer::serializeLIC(const std::string & fieldName, LICSet & value) { if(value.any != value.standard) diff --git a/lib/serializer/JsonSerializer.h b/lib/serializer/JsonSerializer.h index e4fe9d87e..89bbbf26d 100644 --- a/lib/serializer/JsonSerializer.h +++ b/lib/serializer/JsonSerializer.h @@ -18,8 +18,7 @@ class DLL_LINKAGE JsonSerializer : public JsonTreeSerializer public: JsonSerializer(const IInstanceResolver * instanceResolver_, JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/lib/serializer/JsonUpdater.cpp b/lib/serializer/JsonUpdater.cpp index f80581f19..6a29bc910 100644 --- a/lib/serializer/JsonUpdater.cpp +++ b/lib/serializer/JsonUpdater.cpp @@ -105,81 +105,11 @@ void JsonUpdater::serializeInternal(int64_t & value) value = currentObject->Integer(); } -void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) +void JsonUpdater::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty() && allOf.Vector().empty()) - { - //permissive mode - value = standard; - } - else - { - //restrictive mode - value.clear(); - value.resize(standard.size(), false); - - readLICPart(anyOf, decoder, true, value); - readLICPart(allOf, decoder, true, value); - } - - readLICPart(noneOf, decoder, false, value); -} - -void JsonUpdater::serializeLIC(const std::string & fieldName, LIC & value) -{ - const JsonNode & field = currentObject->operator[](fieldName); - - if(field.isNull()) - return; - - const JsonNode & anyOf = field["anyOf"]; - const JsonNode & allOf = field["allOf"]; - const JsonNode & noneOf = field["noneOf"]; - - if(anyOf.Vector().empty()) - { - //permissive mode - value.any = value.standard; - } - else - { - //restrictive mode - value.any.clear(); - value.any.resize(value.standard.size(), false); - - readLICPart(anyOf, value.decoder, true, value.any); - } - - readLICPart(allOf, value.decoder, true, value.all); - readLICPart(noneOf, value.decoder, true, value.none); - - //remove any banned from allowed and required - for(si32 idx = 0; idx < value.none.size(); idx++) - { - if(value.none[idx]) - { - value.all[idx] = false; - value.any[idx] = false; - } - } - - //add all required to allowed - for(si32 idx = 0; idx < value.all.size(); idx++) - { - if(value.all[idx]) - { - value.any[idx] = true; - } - } + LICSet lic(standard, decoder, encoder); + serializeLIC(fieldName, lic); + value = lic.any; } void JsonUpdater::serializeLIC(const std::string & fieldName, LICSet & value) diff --git a/lib/serializer/JsonUpdater.h b/lib/serializer/JsonUpdater.h index b09fd6044..e0cd5f508 100644 --- a/lib/serializer/JsonUpdater.h +++ b/lib/serializer/JsonUpdater.h @@ -9,20 +9,18 @@ */ #pragma once -#include "ILICReader.h" #include "JsonTreeSerializer.h" VCMI_LIB_NAMESPACE_BEGIN class CBonusSystemNode; -class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer, public ILICReader +class DLL_LINKAGE JsonUpdater: public JsonTreeSerializer { public: JsonUpdater(const IInstanceResolver * instanceResolver_, const JsonNode & root_); - void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::vector & standard, std::vector & value) override; - void serializeLIC(const std::string & fieldName, LIC & value) override; + void serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) override; void serializeLIC(const std::string & fieldName, LICSet & value) override; void serializeString(const std::string & fieldName, std::string & value) override; diff --git a/mapeditor/inspector/inspector.cpp b/mapeditor/inspector/inspector.cpp index 01839d01c..3587dd1b5 100644 --- a/mapeditor/inspector/inspector.cpp +++ b/mapeditor/inspector/inspector.cpp @@ -186,7 +186,7 @@ void Initializer::initialize(CGArtifact * o) std::vector out; for(auto spell : VLC->spellh->objects) //spellh size appears to be greater (?) { - if(VLC->spellh->getDefaultAllowed().at(spell->id)) + if(VLC->spellh->getDefaultAllowed().count(spell->id) != 0) { out.push_back(spell->id); } @@ -319,7 +319,7 @@ void Inspector::updateProperties(CGHeroInstance * o) auto * delegate = new InspectorDelegate; for(int i = 0; i < VLC->heroh->objects.size(); ++i) { - if(controller.map()->allowedHeroes.at(i)) + if(controller.map()->allowedHeroes.count(HeroTypeID(i)) != 0) { if(o->ID == Obj::PRISON || (o->type && VLC->heroh->objects[i]->heroClass->getIndex() == o->type->heroClass->getIndex())) { @@ -356,7 +356,7 @@ void Inspector::updateProperties(CGArtifact * o) auto * delegate = new InspectorDelegate; for(auto spell : VLC->spellh->objects) { - if(controller.map()->allowedSpells.at(spell->id)) + if(controller.map()->allowedSpells.count(spell->id) != 0) delegate->options.push_back({QObject::tr(spell->getNameTranslated().c_str()), QVariant::fromValue(int(spell->getId()))}); } addProperty("Spell", VLC->spellh->getById(spellId)->getNameTranslated(), delegate, false); diff --git a/mapeditor/inspector/questwidget.cpp b/mapeditor/inspector/questwidget.cpp index 97e22f586..982138741 100644 --- a/mapeditor/inspector/questwidget.cpp +++ b/mapeditor/inspector/questwidget.cpp @@ -54,7 +54,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!controller.map()->allowedArtifact[i]) + if(controller.map()->allowedArtifact.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lArtifacts->addItem(item); } @@ -66,7 +66,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!controller.map()->allowedSpells[i]) + if(controller.map()->allowedSpells.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); ui->lSpells->addItem(item); } @@ -82,7 +82,7 @@ QuestWidget::QuestWidget(MapController & _controller, CQuest & _sh, QWidget *par for(auto & s : NSecondarySkill::levels) widget->addItem(QString::fromStdString(s)); - if(!controller.map()->allowedAbilities[i]) + if(controller.map()->allowedAbilities.count(i) == 0) { item->setFlags(item->flags() & ~Qt::ItemIsEnabled); widget->setEnabled(false); diff --git a/mapeditor/inspector/rewardswidget.cpp b/mapeditor/inspector/rewardswidget.cpp index 6ec62d84b..373588a3c 100644 --- a/mapeditor/inspector/rewardswidget.cpp +++ b/mapeditor/inspector/rewardswidget.cpp @@ -73,7 +73,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!map.allowedArtifact[i]) + if(map.allowedArtifact.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); w->addItem(item); } @@ -88,7 +88,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : item->setData(Qt::UserRole, QVariant::fromValue(i)); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(Qt::Unchecked); - if(!map.allowedSpells[i]) + if(map.allowedSpells.count(i) == 0) item->setFlags(item->flags() & ~Qt::ItemIsEnabled); w->addItem(item); } @@ -115,7 +115,7 @@ RewardsWidget::RewardsWidget(CMap & m, CRewardableObject & p, QWidget *parent) : for(auto & s : NSecondarySkill::levels) widget->addItem(QString::fromStdString(s)); - if(!map.allowedAbilities[i]) + if(map.allowedAbilities.count(i) == 0) { item->setFlags(item->flags() & ~Qt::ItemIsEnabled); widget->setEnabled(false); diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index 0ba761307..b26bf03c1 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -94,24 +94,6 @@ void MapController::repairMap(CMap * map) const if(!map) return; - //there might be extra skills, arts and spells not imported from map - if(VLC->skillh->getDefaultAllowed().size() > map->allowedAbilities.size()) - { - map->allowedAbilities.resize(VLC->skillh->getDefaultAllowed().size()); - } - if(VLC->arth->getDefaultAllowed().size() > map->allowedArtifact.size()) - { - map->allowedArtifact.resize(VLC->arth->getDefaultAllowed().size()); - } - if(VLC->spellh->getDefaultAllowed().size() > map->allowedSpells.size()) - { - map->allowedSpells.resize(VLC->spellh->getDefaultAllowed().size()); - } - if(VLC->heroh->getDefaultAllowed().size() > map->allowedHeroes.size()) - { - map->allowedHeroes.resize(VLC->heroh->getDefaultAllowed().size()); - } - //make sure events/rumors has name to have proper identifiers int emptyNameId = 1; for(auto & e : map->events) @@ -149,7 +131,7 @@ void MapController::repairMap(CMap * map) const //fix hero instance if(auto * nih = dynamic_cast(obj.get())) { - map->allowedHeroes.at(nih->subID) = true; + map->allowedHeroes.insert(nih->getHeroType()); auto type = VLC->heroh->objects[nih->subID]; assert(type->heroClass); //TODO: find a way to get proper type name @@ -217,7 +199,7 @@ void MapController::repairMap(CMap * map) const art->storedArtifact = a; } else - map->allowedArtifact.at(art->getArtifact()) = true; + map->allowedArtifact.insert(art->getArtifact()); } } } diff --git a/mapeditor/mapsettings/mapsettings.cpp b/mapeditor/mapsettings/mapsettings.cpp index 5b410cb56..369e1007f 100644 --- a/mapeditor/mapsettings/mapsettings.cpp +++ b/mapeditor/mapsettings/mapsettings.cpp @@ -30,36 +30,36 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) : show(); - for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i) + for(auto objectPtr : VLC->skillh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedAbilities[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedAbilities.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listAbilities->addItem(item); } - for(int i = 0; i < controller.map()->allowedSpells.size(); ++i) + for(auto objectPtr : VLC->spellh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->spellh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedSpells[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedSpells.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listSpells->addItem(item); } - for(int i = 0; i < controller.map()->allowedArtifact.size(); ++i) + for(auto objectPtr : VLC->arth->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->arth->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedArtifact[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedArtifact.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listArts->addItem(item); } - for(int i = 0; i < controller.map()->allowedHeroes.size(); ++i) + for(auto objectPtr : VLC->heroh->objects) { - auto * item = new QListWidgetItem(QString::fromStdString(VLC->heroh->objects[i]->getNameTranslated())); - item->setData(Qt::UserRole, QVariant::fromValue(i)); + auto * item = new QListWidgetItem(QString::fromStdString(objectPtr->getNameTranslated())); + item->setData(Qt::UserRole, QVariant::fromValue(objectPtr->getIndex())); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(controller.map()->allowedHeroes[i] ? Qt::Checked : Qt::Unchecked); + item->setCheckState(controller.map()->allowedHeroes.count(objectPtr->getId()) ? Qt::Checked : Qt::Unchecked); ui->listHeroes->addItem(item); } @@ -78,12 +78,14 @@ MapSettings::~MapSettings() void MapSettings::on_pushButton_clicked() { - auto updateMapArray = [](const QListWidget * widget, std::vector & arr) + auto updateMapArray = [](const QListWidget * widget, auto & arr) { + arr.clear(); for(int i = 0; i < arr.size(); ++i) { auto * item = widget->item(i); - arr[i] = item->checkState() == Qt::Checked; + if (item->checkState() == Qt::Checked) + arr.emplace(i); } }; diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index daa69ce84..1d7b8455d 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -125,7 +125,7 @@ std::list Validator::validate(const CMap * map) } if(ins->type) { - if(!map->allowedHeroes[ins->type->getId().getNum()]) + if(map->allowedHeroes.count(ins->getHeroType()) == 0) issues.emplace_back(QString(tr("Hero %1 is prohibited by map settings")).arg(ins->type->getNameTranslated().c_str()), false); if(!allHeroesOnMap.insert(ins->type).second) @@ -142,7 +142,7 @@ std::list Validator::validate(const CMap * map) { if(ins->storedArtifact) { - if(!map->allowedSpells[ins->storedArtifact->getScrollSpellID()]) + if(map->allowedSpells.count(ins->storedArtifact->getScrollSpellID()) == 0) issues.emplace_back(QString(tr("Spell scroll %1 is prohibited by map settings")).arg(ins->storedArtifact->getScrollSpellID().toEntity(VLC->spells())->getNameTranslated().c_str()), false); } else @@ -150,7 +150,7 @@ std::list Validator::validate(const CMap * map) } else { - if(ins->ID == Obj::ARTIFACT && !map->allowedArtifact[ins->subID]) + if(ins->ID == Obj::ARTIFACT && map->allowedArtifact.count(ins->getArtifact()) == 0) { issues.emplace_back(QString(tr("Artifact %1 is prohibited by map settings")).arg(ins->getObjectName().c_str()), false); } diff --git a/server/CVCMIServer.cpp b/server/CVCMIServer.cpp index e9651f91c..0285f7067 100644 --- a/server/CVCMIServer.cpp +++ b/server/CVCMIServer.cpp @@ -1058,7 +1058,7 @@ bool CVCMIServer::canUseThisHero(PlayerColor player, HeroTypeID ID) return VLC->heroh->size() > ID && si->playerInfos[player].castle == VLC->heroh->objects[ID]->heroClass->faction && !vstd::contains(getUsedHeroes(), ID) - && mi->mapHeader->allowedHeroes[ID]; + && mi->mapHeader->allowedHeroes.count(ID); } std::vector CVCMIServer::getUsedHeroes() From abad4b01ce9be0ed59fb046af1f15ca186b34033 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 5 Nov 2023 18:58:07 +0200 Subject: [PATCH 1132/1248] Remove explicit convesion to int in operators --- include/vcmi/spells/Caster.h | 3 +- lib/ArtifactUtils.cpp | 2 +- lib/CArtHandler.cpp | 15 ++++----- lib/CArtHandler.h | 2 +- lib/CCreatureHandler.cpp | 8 ++--- lib/CCreatureSet.cpp | 14 ++++---- lib/CHeroHandler.cpp | 4 +-- lib/JsonRandom.cpp | 8 ++--- lib/battle/CUnitState.cpp | 2 +- lib/battle/CUnitState.h | 2 +- lib/constants/EntityIdentifiers.cpp | 6 ++-- lib/constants/EntityIdentifiers.h | 7 ++++ lib/gameState/CGameState.cpp | 3 +- .../CObjectClassesHandler.cpp | 32 +++++++++---------- lib/mapObjects/CArmedInstance.cpp | 2 +- lib/mapObjects/CArmedInstance.h | 2 +- lib/mapObjects/CGHeroInstance.cpp | 26 +++++++-------- lib/mapObjects/CGHeroInstance.h | 2 +- lib/mapObjects/CGTownInstance.cpp | 10 +++--- lib/mapObjects/MiscObjects.cpp | 4 +-- lib/mapObjects/MiscObjects.h | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- lib/mapping/MapFormatJson.cpp | 9 ++---- lib/rewardable/Interface.cpp | 2 +- lib/rmg/CMapGenerator.cpp | 7 +--- lib/rmg/modificators/ObjectManager.cpp | 6 ++-- lib/spells/AbilityCaster.cpp | 2 +- lib/spells/AbilityCaster.h | 2 +- lib/spells/CSpellHandler.cpp | 2 +- lib/spells/ExternalCaster.cpp | 2 +- lib/spells/ExternalCaster.h | 4 ++- lib/spells/ObstacleCasterProxy.cpp | 2 +- lib/spells/ObstacleCasterProxy.h | 2 +- lib/spells/ProxyCaster.cpp | 2 +- lib/spells/ProxyCaster.h | 2 +- 35 files changed, 97 insertions(+), 105 deletions(-) diff --git a/include/vcmi/spells/Caster.h b/include/vcmi/spells/Caster.h index 0d779a2a4..4158c0c53 100644 --- a/include/vcmi/spells/Caster.h +++ b/include/vcmi/spells/Caster.h @@ -17,6 +17,7 @@ class MetaString; class ServerCallback; class CGHeroInstance; class Spell; +class SpellSchool; namespace battle { @@ -38,7 +39,7 @@ public: /// returns level on which given spell would be cast by this(0 - none, 1 - basic etc); /// caster may not know this spell at all /// optionally returns number of selected school by arg - 0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic - virtual int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const = 0; + virtual int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const = 0; ///default spell school level for effect calculation virtual int32_t getEffectLevel(const Spell * spell) const = 0; diff --git a/lib/ArtifactUtils.cpp b/lib/ArtifactUtils.cpp index d5b9154ed..bfe46894c 100644 --- a/lib/ArtifactUtils.cpp +++ b/lib/ArtifactUtils.cpp @@ -216,7 +216,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifa DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const ArtifactID & aid) { - return ArtifactUtils::createNewArtifactInstance(VLC->arth->objects[aid]); + return ArtifactUtils::createNewArtifactInstance((*VLC->arth)[aid]); } DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const ArtifactID & aid, SpellID spellID) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 97351d3b6..0854429b8 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -371,7 +371,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode objects.emplace_back(object); - registerObject(scope, "artifact", name, object->id); + registerObject(scope, "artifact", name, object->id.getNum()); } void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -383,7 +383,7 @@ void CArtHandler::loadObject(std::string scope, std::string name, const JsonNode assert(objects[index] == nullptr); // ensure that this id was not loaded before objects[index] = object; - registerObject(scope, "artifact", name, object->id); + registerObject(scope, "artifact", name, object->id.getNum()); } const std::vector & CArtHandler::getTypeNames() const @@ -630,7 +630,7 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander) bool CArtHandler::legalArtifact(const ArtifactID & id) { - auto art = objects[id]; + auto art = id.toArtifact(); //assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components if(art->isCombined()) @@ -639,13 +639,13 @@ bool CArtHandler::legalArtifact(const ArtifactID & id) if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC) return false; // invalid class - if(!art->possibleSlots[ArtBearer::HERO].empty()) + if(art->possibleSlots.count(ArtBearer::HERO) && !art->possibleSlots.at(ArtBearer::HERO).empty()) return true; - if(!art->possibleSlots[ArtBearer::CREATURE].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) + if(art->possibleSlots.count(ArtBearer::CREATURE) && !art->possibleSlots.at(ArtBearer::CREATURE).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_STACK_ARTIFACT)) return true; - if(!art->possibleSlots[ArtBearer::COMMANDER].empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) + if(art->possibleSlots.count(ArtBearer::COMMANDER) && !art->possibleSlots.at(ArtBearer::COMMANDER).empty() && VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS)) return true; return false; @@ -658,7 +658,7 @@ void CArtHandler::initAllowedArtifactsList(const std::set & allowed) for (ArtifactID i : allowed) { if (legalArtifact(ArtifactID(i))) - allowedArtifacts.push_back(objects[i]); + allowedArtifacts.push_back(i.toArtifact()); //keep im mind that artifact can be worn by more than one type of bearer } } @@ -682,7 +682,6 @@ void CArtHandler::afterLoadFinalization() { for(auto &bonus : art->getExportedBonusList()) { - assert(art == objects[art->id]); assert(bonus->source == BonusSource::ARTIFACT); bonus->sid = BonusSourceID(art->id); } diff --git a/lib/CArtHandler.h b/lib/CArtHandler.h index f9673a450..957d2b1c5 100644 --- a/lib/CArtHandler.h +++ b/lib/CArtHandler.h @@ -142,7 +142,7 @@ class DLL_LINKAGE CArtHandler : public CHandlerBase allowedArtifacts; + std::vector allowedArtifacts; void addBonuses(CArtifact *art, const JsonNode &bonusList); diff --git a/lib/CCreatureHandler.cpp b/lib/CCreatureHandler.cpp index 630992b51..3b72d3719 100644 --- a/lib/CCreatureHandler.cpp +++ b/lib/CCreatureHandler.cpp @@ -324,7 +324,7 @@ bool CCreature::isMyUpgrade(const CCreature *anotherCre) const bool CCreature::valid() const { - return this == VLC->creh->objects[idNumber]; + return this == (*VLC->creh)[idNumber]; } std::string CCreature::nodeName() const @@ -778,15 +778,13 @@ void CCreatureHandler::loadCrExpBon(CBonusSystemNode & globalEffects) } do //parse everything that's left { - CreatureID sid = static_cast(parser.readNumber()); //id = this particular creature ID + CreatureID sid = parser.readNumber(); //id = this particular creature ID b.sid = BonusSourceID(sid); bl.clear(); loadStackExp(b, bl, parser); for(const auto & b : bl) - { - objects[sid]->addNewBonus(b); //add directly to CCreature Node - } + (*this)[sid]->addNewBonus(b); //add directly to CCreature Node } while (parser.endLine()); diff --git a/lib/CCreatureSet.cpp b/lib/CCreatureSet.cpp index 8d79606ba..913ec72f6 100644 --- a/lib/CCreatureSet.cpp +++ b/lib/CCreatureSet.cpp @@ -79,7 +79,7 @@ bool CCreatureSet::setCreature(SlotID slot, CreatureID type, TQuantity quantity) SlotID CCreatureSet::getSlotFor(const CreatureID & creature, ui32 slotsAmount) const /*returns -1 if no slot available */ { - return getSlotFor(VLC->creh->objects[creature], slotsAmount); + return getSlotFor(creature.toCreature(), slotsAmount); } SlotID CCreatureSet::getSlotFor(const CCreature *c, ui32 slotsAmount) const @@ -304,7 +304,7 @@ void CCreatureSet::sweep() void CCreatureSet::addToSlot(const SlotID & slot, const CreatureID & cre, TQuantity count, bool allowMerging) { - const CCreature *c = VLC->creh->objects[cre]; + const CCreature *c = cre.toCreature(); if(!hasStackAtSlot(slot)) { @@ -749,10 +749,10 @@ void CStackInstance::giveStackExp(TExpType exp) void CStackInstance::setType(const CreatureID & creID) { - if(creID.getNum() >= 0 && creID.getNum() < VLC->creh->objects.size()) - setType(VLC->creh->objects[creID]); + if (creID == CreatureID::NONE) + setType(nullptr);//FIXME: unused branch? else - setType((const CCreature*)nullptr); + setType(creID.toCreature()); } void CStackInstance::setType(const CCreature *c) @@ -809,7 +809,7 @@ bool CStackInstance::valid(bool allowUnrandomized) const { if(!randomStack) { - return (type && type == VLC->creh->objects[type->getId()]); + return (type && type == type->getId().toEntity(VLC)); } else return allowUnrandomized; @@ -999,7 +999,7 @@ bool CCommanderInstance::gainsLevel() const CStackBasicDescriptor::CStackBasicDescriptor() = default; CStackBasicDescriptor::CStackBasicDescriptor(const CreatureID & id, TQuantity Count): - type(VLC->creh->objects[id]), + type(id.toCreature()), count(Count) { } diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index f71549e62..5de91ebe8 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -518,7 +518,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba for (auto const & upgradeSourceID : oldTargets) { - const CCreature * upgradeSource = VLC->creh->objects[upgradeSourceID]; + const CCreature * upgradeSource = upgradeSourceID.toCreature(); targets.insert(upgradeSource->upgrades.begin(), upgradeSource->upgrades.end()); } @@ -528,7 +528,7 @@ static std::vector> createCreatureSpecialty(CreatureID ba for(CreatureID cid : targets) { - const CCreature &specCreature = *VLC->creh->objects[cid]; + auto const & specCreature = *cid.toCreature(); int stepSize = specCreature.getLevel() ? specCreature.getLevel() : 5; { diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index e87f9d6d2..69d0636f4 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -156,7 +156,7 @@ namespace JsonRandom for (auto const & artID : valuesSet) { - CArtifact * art = VLC->arth->objects[artID]; + const CArtifact * art = artID.toArtifact(); if(!vstd::iswithin(art->getPrice(), minValue, maxValue)) continue; @@ -482,13 +482,13 @@ namespace JsonRandom else logMod->warn("Failed to select suitable random creature!"); - stack.type = VLC->creh->objects[pickedCreature]; + stack.type = pickedCreature.toCreature(); stack.count = loadValue(value, rng, variables); if (!value["upgradeChance"].isNull() && !stack.type->upgrades.empty()) { if (int(value["upgradeChance"].Float()) > rng.nextInt(99)) // select random upgrade { - stack.type = VLC->creh->objects[*RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)]; + stack.type = RandomGeneratorUtil::nextItem(stack.type->upgrades, rng)->toCreature(); } } return stack; @@ -523,7 +523,7 @@ namespace JsonRandom if (node["upgradeChance"].Float() > 0) { for(const auto & creaID : crea->upgrades) - info.allowedCreatures.push_back(VLC->creh->objects[creaID]); + info.allowedCreatures.push_back(creaID.toCreature()); } ret.push_back(info); } diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 72b8bf3c8..9763dfa97 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -428,7 +428,7 @@ const CGHeroInstance * CUnitState::getHeroCaster() const return nullptr; } -int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CUnitState::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { int32_t skill = valOfBonuses(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spell->getId()))); vstd::abetween(skill, 0, 3); diff --git a/lib/battle/CUnitState.h b/lib/battle/CUnitState.h index e4597b4ad..9e0fb41c9 100644 --- a/lib/battle/CUnitState.h +++ b/lib/battle/CUnitState.h @@ -182,7 +182,7 @@ public: int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const spells::Spell * spell) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const Unit * affectedStack) const override; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 73efb5b9c..43694c809 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -160,12 +160,12 @@ std::string CampaignScenarioID::encode(const si32 index) return std::to_string(index); } -std::string Obj::encode(int32_t index) +std::string MapObjectID::encode(int32_t index) { - return VLC->objtypeh->getObjectHandlerName(index); + return VLC->objtypeh->getObjectHandlerName(MapObjectID(index)); } -si32 Obj::decode(const std::string & identifier) +si32 MapObjectID::decode(const std::string & identifier) { auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); if(rawId) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index a789b2e85..a21c3dbeb 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -672,6 +672,12 @@ class ArtifactPosition : public IdentifierWithEnum::IdentifierWithEnum; + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } }; class ArtifactIDBase : public IdentifierBase @@ -728,6 +734,7 @@ public: FIRE_ELEMENTAL = 114, // for tests PSYCHIC_ELEMENTAL = 120, // for hardcoded ability MAGIC_ELEMENTAL = 121, // for hardcoded ability + AZURE_DRAGON = 132, CATAPULT = 145, BALLISTA = 146, FIRST_AID_TENT = 147, diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index d40f541b4..3a1e040f1 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1860,8 +1860,7 @@ void CGameState::attachArmedObjects() bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid) { - CArtifact * const artifact = VLC->arth->objects[aid]; //pointer to constant object - CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(artifact); + CArtifactInstance * ai = ArtifactUtils::createNewArtifactInstance(aid); map->addNewArtifactInstance(ai); auto slot = ArtifactUtils::getArtAnyPosition(h, aid); if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot)) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 712b8c910..89230a45b 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -288,27 +288,26 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID) { config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL - assert(objects[ID]); + assert(objects[ID.getNum()]); - if ( subID >= objects[ID]->objects.size()) - objects[ID]->objects.resize(subID+1); + if ( subID.getNum() >= objects[ID.getNum()]->objects.size()) + objects[ID.getNum()]->objects.resize(subID.getNum()+1); - JsonUtils::inherit(config, objects.at(ID)->base); - loadSubObject(config.meta, identifier, config, objects[ID], subID); + JsonUtils::inherit(config, objects.at(ID.getNum())->base); + loadSubObject(config.meta, identifier, config, objects[ID.getNum()], subID.getNum()); } void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID) { - assert(objects[ID]); - assert(subID < objects[ID]->objects.size()); - objects[ID]->objects[subID] = nullptr; + assert(objects[ID.getNum()]); + objects[ID.getNum()]->objects[subID.getNum()] = nullptr; } TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const { try { - auto result = objects.at(type)->objects.at(subtype); + auto result = objects.at(type.getNum())->objects.at(subtype.getNum()); if (result != nullptr) return result; @@ -318,7 +317,7 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObj // Leave catch block silently } - std::string errorString = "Failed to find object of type " + std::to_string(type) + "::" + std::to_string(subtype); + std::string errorString = "Failed to find object of type " + std::to_string(type.getNum()) + "::" + std::to_string(subtype.getNum()); logGlobal->error(errorString); throw std::runtime_error(errorString); } @@ -360,13 +359,13 @@ std::set CObjectClassesHandler::knownSubObjects(MapObjectID prim { std::set ret; - if (!objects.at(primaryID)) + if (!objects.at(primaryID.getNum())) { logGlobal->error("Failed to find object %d", primaryID); return ret; } - for(const auto & entry : objects.at(primaryID)->objects) + for(const auto & entry : objects.at(primaryID.getNum())->objects) if (entry) ret.insert(entry->subtype); @@ -460,7 +459,7 @@ std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubI if (handler && handler->hasNameTextID()) return handler->getNameTranslated(); else - return objects[type]->getNameTranslated(); + return objects[type.getNum()]->getNameTranslated(); } SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const @@ -472,20 +471,19 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObject if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL) subtype = 0; - assert(objects[type]); - assert(subtype < objects[type]->objects.size()); + assert(objects[type.getNum()]); return getHandlerFor(type, subtype)->getSounds(); } std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const { - return objects.at(type)->handlerName; + return objects.at(type.getNum())->handlerName; } std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const { - return objects.at(type)->getJsonKey(); + return objects.at(type.getNum())->getJsonKey(); } VCMI_LIB_NAMESPACE_END diff --git a/lib/mapObjects/CArmedInstance.cpp b/lib/mapObjects/CArmedInstance.cpp index e6e74d09d..6c588df7b 100644 --- a/lib/mapObjects/CArmedInstance.cpp +++ b/lib/mapObjects/CArmedInstance.cpp @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN -void CArmedInstance::randomizeArmy(int type) +void CArmedInstance::randomizeArmy(FactionID type) { for (auto & elem : stacks) { diff --git a/lib/mapObjects/CArmedInstance.h b/lib/mapObjects/CArmedInstance.h index 378dcf96c..95cb59d43 100644 --- a/lib/mapObjects/CArmedInstance.h +++ b/lib/mapObjects/CArmedInstance.h @@ -29,7 +29,7 @@ private: public: BattleInfo *battle; //set to the current battle, if engaged - void randomizeArmy(int type); + void randomizeArmy(FactionID type); virtual void updateMoraleBonusFromArmy(); void armyChanged() override; diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 00b4e5dbc..80ae5b9f5 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -578,11 +578,11 @@ void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) { ID = Obj::HERO; subID = cb->gameState()->pickNextHeroType(getOwner()); - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType().getNum()]; randomizeArmy(type->heroClass->faction); } else - type = VLC->heroh->objects[subID]; + type = VLC->heroh->objects[getHeroType().getNum()]; auto oldSubID = subID; @@ -657,7 +657,7 @@ int32_t CGHeroInstance::getCasterUnitId() const return id.getNum(); } -int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool) const +int32_t CGHeroInstance::getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool) const { int32_t skill = -1; //skill level @@ -909,7 +909,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b // raise upgraded creature (at 2/3 rate) if no space available otherwise if(getSlotFor(creatureTypeRaised) == SlotID()) { - for(const CreatureID & upgraded : VLC->creh->objects[creatureTypeRaised]->upgrades) + for(const CreatureID & upgraded : creatureTypeRaised.toCreature()->upgrades) { if(getSlotFor(upgraded) != SlotID()) { @@ -920,7 +920,7 @@ CStackBasicDescriptor CGHeroInstance::calculateNecromancy (const BattleResult &b } } // calculate number of creatures raised - low level units contribute at 50% rate - const double raisedUnitHealth = VLC->creh->objects[creatureTypeRaised]->getMaxHealth(); + const double raisedUnitHealth = creatureTypeRaised.toCreature()->getMaxHealth(); double raisedUnits = 0; for(const auto & casualty : casualties) { @@ -1523,7 +1523,7 @@ std::string CGHeroInstance::getHeroTypeName() const } else { - return VLC->heroh->objects[getHeroType()]->getJsonKey(); + return getHeroType().toEntity(VLC)->getJsonKey(); } } return ""; @@ -1657,15 +1657,11 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler) for(size_t skillIndex = 0; skillIndex < secondarySkills.size(); ++skillIndex) { JsonArraySerializer inner = secondarySkills.enterArray(skillIndex); - const si32 rawId = secSkills.at(skillIndex).first; + SecondarySkill skillId = secSkills.at(skillIndex).first; - if(rawId < 0 || rawId >= VLC->skillh->size()) - logGlobal->error("Invalid secondary skill %d", rawId); - - auto value = (*VLC->skillh)[SecondarySkill(rawId)]->getJsonKey(); - handler.serializeString("skill", value); - value = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); - handler.serializeString("level", value); + handler.serializeId("skill", skillId); + std::string skillLevel = NSecondarySkill::levels.at(secSkills.at(skillIndex).second); + handler.serializeString("level", skillLevel); } } } @@ -1757,7 +1753,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(!appearance) { // crossoverDeserialize - type = VLC->heroh->objects[getHeroType()]; + type = VLC->heroh->objects[getHeroType().getNum()]; appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front(); } diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 2ddc83e2c..1349b4687 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -274,7 +274,7 @@ public: ///spells::Caster int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const spells::Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const spells::Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int64_t getSpellBonus(const spells::Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const spells::Spell * spell, int64_t base) const override; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 6d3cacbd6..84bda3112 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -136,7 +136,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const if (creatures[level].second.empty()) return ret; //no dwelling - const CCreature *creature = VLC->creh->objects[creatures[level].second.back()]; + const Creature *creature = creatures[level].second.back().toEntity(VLC); const int base = creature->getGrowth(); int castleBonus = 0; @@ -489,8 +489,8 @@ void CGTownInstance::pickRandomObject(CRandomGenerator & rand) assert(ID == Obj::TOWN); // just in case setType(ID, subID); - town = (*VLC->townh)[subID]->town; - randomizeArmy(subID); + town = (*VLC->townh)[getFaction()]->town; + randomizeArmy(getFaction()); updateAppearance(); } @@ -571,7 +571,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const } else //upgrade { - cb->changeStackType(sl, VLC->creh->objects[*c->upgrades.begin()]); + cb->changeStackType(sl, c->upgrades.begin()->toCreature()); } } if ((stacksCount() < GameConstants::ARMY_SIZE && rand.nextInt(99) < 25) || Slots().empty()) //add new stack @@ -592,7 +592,7 @@ void CGTownInstance::newTurn(CRandomGenerator & rand) const { StackLocation sl(this, n); if (slotEmpty(n)) - cb->insertNewStack(sl, VLC->creh->objects[c], count); + cb->insertNewStack(sl, c.toCreature(), count); else //add to existing cb->changeStackCount(sl, count); } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 82c5cfe72..00968d887 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -33,7 +33,7 @@ VCMI_LIB_NAMESPACE_BEGIN -std::map > CGMagi::eyelist; +std::map > CGMagi::eyelist; ui8 CGObelisk::obeliskCount = 0; //how many obelisks are on map std::map CGObelisk::visited; //map: team_id => how many obelisks has been visited @@ -219,7 +219,7 @@ void CGMine::serializeJsonOptions(JsonSerializeFormat & handler) for(const auto & resID : abandonedMineResources) { JsonNode one(JsonNode::JsonType::DATA_STRING); - one.String() = GameConstants::RESOURCE_NAMES[resID]; + one.String() = GameConstants::RESOURCE_NAMES[resID.getNum()]; node.Vector().push_back(one); } handler.serializeRaw("possibleResources", node, std::nullopt); diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index de8ae3862..d761b2a89 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -338,7 +338,7 @@ protected: class DLL_LINKAGE CGMagi : public CGObjectInstance { public: - static std::map > eyelist; //[subID][id], supports multiple sets as in H5 + static std::map > eyelist; //[subID][id], supports multiple sets as in H5 static void reset(); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 02624ccdf..8003f257e 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -2095,7 +2095,7 @@ int CMapLoaderH3M::readQuest(IQuestObject * guard, const int3 & position) guard->quest->mission.creatures.resize(typeNumber); for(int hh = 0; hh < typeNumber; ++hh) { - guard->quest->mission.creatures[hh].type = VLC->creh->objects[reader->readCreature()]; + guard->quest->mission.creatures[hh].type = reader->readCreature().toCreature(); guard->quest->mission.creatures[hh].count = reader->readUInt16(); } break; diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index fda1e4329..90b747419 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -528,13 +528,10 @@ void CMapFormatJson::serializePlayerInfo(JsonSerializeFormat & handler) { std::string temp; if(hero->type) - { temp = hero->type->getJsonKey(); - } else - { - temp = VLC->heroh->objects[hero->subID]->getJsonKey(); - } + temp = hero->getHeroType().toEntity(VLC)->getJsonKey(); + handler.serializeString("type", temp); } } @@ -753,7 +750,7 @@ void CMapFormatJson::writeDisposedHeroes(JsonSerializeFormat & handler) for(DisposedHero & hero : map->disposedHeroes) { - std::string type = HeroTypeID::encode(hero.heroId); + std::string type = HeroTypeID::encode(hero.heroId.getNum()); auto definition = definitions->enterStruct(type); diff --git a/lib/rewardable/Interface.cpp b/lib/rewardable/Interface.cpp index b7cc60c25..534641f37 100644 --- a/lib/rewardable/Interface.cpp +++ b/lib/rewardable/Interface.cpp @@ -157,7 +157,7 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re } for(const ArtifactID & art : info.reward.artifacts) - cb->giveHeroNewArtifact(hero, VLC->arth->objects[art],ArtifactPosition::FIRST_AVAILABLE); + cb->giveHeroNewArtifact(hero, art.toArtifact(), ArtifactPosition::FIRST_AVAILABLE); if(!info.reward.spells.empty()) { diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 3c66b3c39..78c03890e 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -98,12 +98,7 @@ const CMapGenOptions& CMapGenerator::getMapGenOptions() const void CMapGenerator::initPrisonsRemaining() { - allowedPrisons = 0; - for (auto isAllowed : map->getMap(this).allowedHeroes) - { - if (isAllowed) - allowedPrisons++; - } + allowedPrisons = map->getMap(this).allowedHeroes.size(); allowedPrisons = std::max (0, allowedPrisons - 16 * mapGenOptions.getHumanOrCpuPlayerCount()); //so at least 16 heroes will be available for every player } diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 970a6f4bf..683f9f6e8 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -638,14 +638,14 @@ CGCreature * ObjectManager::chooseGuard(si32 strength, bool zoneGuard) if(!possibleCreatures.empty()) { creId = *RandomGeneratorUtil::nextItem(possibleCreatures, zone.getRand()); - amount = strength / VLC->creh->objects[creId]->getAIValue(); + amount = strength / creId.toEntity(VLC)->getAIValue(); if (amount >= 4) amount = static_cast(amount * zone.getRand().nextDouble(0.75, 1.25)); } else //just pick any available creature { - creId = CreatureID(132); //Azure Dragon - amount = strength / VLC->creh->objects[creId]->getAIValue(); + creId = CreatureID::AZURE_DRAGON; //Azure Dragon + amount = strength / creId.toEntity(VLC)->getAIValue(); } auto guardFactory = VLC->objtypeh->getHandlerFor(Obj::MONSTER, creId); diff --git a/lib/spells/AbilityCaster.cpp b/lib/spells/AbilityCaster.cpp index 01b1c105a..f6365684d 100644 --- a/lib/spells/AbilityCaster.cpp +++ b/lib/spells/AbilityCaster.cpp @@ -28,7 +28,7 @@ AbilityCaster::AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpe AbilityCaster::~AbilityCaster() = default; -int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool) const +int32_t AbilityCaster::getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool) const { auto skill = baseSpellLevel; const auto * unit = dynamic_cast(actualCaster); diff --git a/lib/spells/AbilityCaster.h b/lib/spells/AbilityCaster.h index bbfd723d7..35685c409 100644 --- a/lib/spells/AbilityCaster.h +++ b/lib/spells/AbilityCaster.h @@ -23,7 +23,7 @@ public: AbilityCaster(const battle::Unit * actualCaster_, int32_t baseSpellLevel_); virtual ~AbilityCaster(); - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; void getCastDescription(const Spell * spell, const std::vector & attacked, MetaString & text) const override; void spendMana(ServerCallback * server, const int32_t spellCost) const override; diff --git a/lib/spells/CSpellHandler.cpp b/lib/spells/CSpellHandler.cpp index 3af7be51e..1acb58c6b 100644 --- a/lib/spells/CSpellHandler.cpp +++ b/lib/spells/CSpellHandler.cpp @@ -154,7 +154,7 @@ void CSpell::forEachSchool(const std::functiongetSpellSchoolLevel(spell, outSelectedSchool); diff --git a/lib/spells/ProxyCaster.h b/lib/spells/ProxyCaster.h index 85fcf240c..67557dc02 100644 --- a/lib/spells/ProxyCaster.h +++ b/lib/spells/ProxyCaster.h @@ -24,7 +24,7 @@ public: virtual ~ProxyCaster(); int32_t getCasterUnitId() const override; - int32_t getSpellSchoolLevel(const Spell * spell, int32_t * outSelectedSchool = nullptr) const override; + int32_t getSpellSchoolLevel(const Spell * spell, SpellSchool * outSelectedSchool = nullptr) const override; int32_t getEffectLevel(const Spell * spell) const override; int64_t getSpellBonus(const Spell * spell, int64_t base, const battle::Unit * affectedStack) const override; int64_t getSpecificSpellBonus(const Spell * spell, int64_t base) const override; From 34338f4eaa254fcc3e6fea55977ca724f24bb1ac Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 5 Nov 2023 19:13:18 +0200 Subject: [PATCH 1133/1248] Remove few more implicit conversions --- lib/CSkillHandler.cpp | 2 +- lib/CTownHandler.cpp | 30 +++++++++---------- lib/constants/EntityIdentifiers.h | 12 ++++++++ lib/constants/IdentifierBase.h | 8 ++--- .../CObjectClassesHandler.cpp | 2 +- .../CObjectClassesHandler.h | 2 +- lib/mapObjects/CGDwelling.cpp | 4 +-- lib/mapObjects/ObjectTemplate.h | 4 +-- lib/mapping/MapFormatH3M.cpp | 2 +- lib/rmg/Zone.cpp | 4 +-- lib/rmg/Zone.h | 4 +-- lib/rmg/modificators/TownPlacer.cpp | 2 +- lib/rmg/modificators/TreasurePlacer.cpp | 6 ++-- lib/rmg/modificators/TreasurePlacer.h | 2 +- 14 files changed, 48 insertions(+), 36 deletions(-) diff --git a/lib/CSkillHandler.cpp b/lib/CSkillHandler.cpp index cea99072d..85452226c 100644 --- a/lib/CSkillHandler.cpp +++ b/lib/CSkillHandler.cpp @@ -77,7 +77,7 @@ void CSkill::registerIcons(const IconRegistar & cb) const { for(int level = 1; level <= 3; level++) { - int frame = 2 + level + 3 * id; + int frame = 2 + level + 3 * id.getNum(); const LevelInfo & skillAtLevel = at(level); cb(frame, 0, "SECSK32", skillAtLevel.iconSmall); cb(frame, 0, "SECSKILL", skillAtLevel.iconMedium); diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index b20f8f872..d1aaf4410 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -144,12 +144,12 @@ CFaction::~CFaction() int32_t CFaction::getIndex() const { - return index; + return index.getNum(); } int32_t CFaction::getIconIndex() const { - return index; //??? + return index.getNum(); //??? } std::string CFaction::getJsonKey() const @@ -172,8 +172,8 @@ void CFaction::registerIcons(const IconRegistar & cb) const cb(info.icons[1][0] + 2, 0, "ITPA", info.iconSmall[1][0]); cb(info.icons[1][1] + 2, 0, "ITPA", info.iconSmall[1][1]); - cb(index, 1, "CPRSMALL", info.towerIconSmall); - cb(index, 1, "TWCRPORT", info.towerIconLarge); + cb(index.getNum(), 1, "CPRSMALL", info.towerIconSmall); + cb(index.getNum(), 1, "TWCRPORT", info.towerIconLarge); } } @@ -734,7 +734,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons ret->town->buildings[ret->bid] = ret; - registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid); + registerObject(source.meta, ret->town->getBuildingScope(), ret->identifier, ret->bid.getNum()); } void CTownHandler::loadBuildings(CTown * town, const JsonNode & source) @@ -1115,10 +1115,10 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod if (object->town) { auto & info = object->town->clientInfo; - info.icons[0][0] = 8 + object->index * 4 + 0; - info.icons[0][1] = 8 + object->index * 4 + 1; - info.icons[1][0] = 8 + object->index * 4 + 2; - info.icons[1][1] = 8 + object->index * 4 + 3; + info.icons[0][0] = 8 + object->index.getNum() * 4 + 0; + info.icons[0][1] = 8 + object->index.getNum() * 4 + 1; + info.icons[1][0] = 8 + object->index.getNum() * 4 + 2; + info.icons[1][1] = 8 + object->index.getNum() * 4 + 3; VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { @@ -1142,7 +1142,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod }); } - registerObject(scope, "faction", name, object->index); + registerObject(scope, "faction", name, object->index.getNum()); } void CTownHandler::loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) @@ -1158,10 +1158,10 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod if (object->town) { auto & info = object->town->clientInfo; - info.icons[0][0] = (GameConstants::F_NUMBER + object->index) * 2 + 0; - info.icons[0][1] = (GameConstants::F_NUMBER + object->index) * 2 + 1; - info.icons[1][0] = object->index * 2 + 0; - info.icons[1][1] = object->index * 2 + 1; + info.icons[0][0] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 0; + info.icons[0][1] = (GameConstants::F_NUMBER + object->index.getNum()) * 2 + 1; + info.icons[1][0] = object->index.getNum() * 2 + 0; + info.icons[1][1] = object->index.getNum() * 2 + 1; VLC->identifiers()->requestIdentifier(scope, "object", "town", [=](si32 index) { @@ -1173,7 +1173,7 @@ void CTownHandler::loadObject(std::string scope, std::string name, const JsonNod }); } - registerObject(scope, "faction", name, object->index); + registerObject(scope, "faction", name, object->index.getNum()); } void CTownHandler::loadRandomFaction() diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index a21c3dbeb..bbba7b9b6 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -571,6 +571,12 @@ public: static std::string encode(int32_t index); static si32 decode(const std::string & identifier); + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } }; class MapObjectSubID : public Identifier @@ -594,6 +600,12 @@ public: this->num = value.getNum(); return *this; } + + // TODO: Remove + constexpr operator int32_t () const + { + return num; + } }; class DLL_LINKAGE RoadId : public Identifier diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index 5ad0a8dd3..b8f06d391 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -52,10 +52,10 @@ public: num += change; } - constexpr operator int32_t () const - { - return num; - } +// constexpr operator int32_t () const +// { +// return num; +// } friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) { diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index 89230a45b..ad23e9fcb 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -117,7 +117,7 @@ std::vector CObjectClassesHandler::loadLegacyData() tmpl->readTxt(parser); parser.endLine(); - std::pair key(tmpl->id.num, tmpl->subid); + std::pair key(tmpl->id, tmpl->subid); legacyTemplates.insert(std::make_pair(key, std::shared_ptr(tmpl))); } diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 835301916..8e8b2ae28 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -74,7 +74,7 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase std::map > handlerConstructors; /// container with H3 templates, used only during loading, no need to serialize it - using TTemplatesContainer = std::multimap, std::shared_ptr>; + using TTemplatesContainer = std::multimap, std::shared_ptr>; TTemplatesContainer legacyTemplates; TObjectTypeHandler loadSubObjectFromJson(const std::string & scope, const std::string & identifier, const JsonNode & entry, ObjectClass * obj, size_t index); diff --git a/lib/mapObjects/CGDwelling.cpp b/lib/mapObjects/CGDwelling.cpp index f405ed8d7..4eeaad6e5 100644 --- a/lib/mapObjects/CGDwelling.cpp +++ b/lib/mapObjects/CGDwelling.cpp @@ -49,7 +49,7 @@ CGDwelling::~CGDwelling() = default; FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand) { if (ID == Obj::RANDOM_DWELLING_FACTION) - return FactionID(subID); + return FactionID(subID.getNum()); assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL); assert(randomizationInfo.has_value()); @@ -138,7 +138,7 @@ void CGDwelling::pickRandomObject(CRandomGenerator & rand) auto testID = [&](const Obj & primaryID) -> MapObjectSubID { auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID); - for (si32 entry : dwellingIDs) + for (MapObjectSubID entry : dwellingIDs) { const auto * handler = dynamic_cast(VLC->objtypeh->getHandlerFor(primaryID, entry).get()); diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 0a43b06c7..332227570 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -43,8 +43,8 @@ class DLL_LINKAGE ObjectTemplate public: /// H3 ID/subID of this object - Obj id; - si32 subid; + MapObjectID id; + MapObjectSubID subid; /// print priority, objects with higher priority will be print first, below everything else si32 printPriority; /// animation file that should be used to display object diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 8003f257e..204d9c925 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -707,7 +707,7 @@ void CMapLoaderH3M::readDisposedHeroes() for(int g = 0; g < disp; ++g) { map->disposedHeroes[g].heroId = reader->readHero(); - map->disposedHeroes[g].portrait.setNum(reader->readHeroPortrait()); + map->disposedHeroes[g].portrait = reader->readHeroPortrait(); map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); } diff --git a/lib/rmg/Zone.cpp b/lib/rmg/Zone.cpp index 8da771c6d..5f6e12d0c 100644 --- a/lib/rmg/Zone.cpp +++ b/lib/rmg/Zone.cpp @@ -127,10 +127,10 @@ rmg::Area & Zone::freePaths() FactionID Zone::getTownType() const { - return FactionID(townType); + return townType; } -void Zone::setTownType(si32 town) +void Zone::setTownType(FactionID town) { townType = town; } diff --git a/lib/rmg/Zone.h b/lib/rmg/Zone.h index 906605832..b6aa3b3ce 100644 --- a/lib/rmg/Zone.h +++ b/lib/rmg/Zone.h @@ -59,7 +59,7 @@ public: void fractalize(); FactionID getTownType() const; - void setTownType(si32 town); + void setTownType(FactionID town); TerrainId getTerrainType() const; void setTerrainType(TerrainId terrain); @@ -108,7 +108,7 @@ protected: std::vector possibleQuestArtifactPos; //template info - si32 townType; + FactionID townType; TerrainId terrainType; }; diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index 76bf3bbec..f4a153cad 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -179,7 +179,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player { for(int i = 0; i < count; i++) { - si32 subType = zone.getTownType(); + FactionID subType = zone.getTownType(); if(totalTowns>0) { diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index ed3f42bb9..4d482dfa1 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -155,8 +155,8 @@ void TreasurePlacer::addAllPossibleObjects() if(dwellingType == Obj::CREATURE_GENERATOR1) { //don't spawn original "neutral" dwellings that got replaced by Conflux dwellings in AB - static int elementalConfluxROE[] = {7, 13, 16, 47}; - for(int & i : elementalConfluxROE) + static MapObjectSubID elementalConfluxROE[] = {7, 13, 16, 47}; + for(auto const & i : elementalConfluxROE) vstd::erase_if_present(subObjects, i); } @@ -918,7 +918,7 @@ char TreasurePlacer::dump(const int3 & t) return Modificator::dump(t); } -void ObjectInfo::setTemplates(si32 type, si32 subtype, TerrainId terrainType) +void ObjectInfo::setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrainType) { auto templHandler = VLC->objtypeh->getHandlerFor(type, subtype); if(!templHandler) diff --git a/lib/rmg/modificators/TreasurePlacer.h b/lib/rmg/modificators/TreasurePlacer.h index ef4881e25..88d10f5a7 100644 --- a/lib/rmg/modificators/TreasurePlacer.h +++ b/lib/rmg/modificators/TreasurePlacer.h @@ -29,7 +29,7 @@ struct ObjectInfo //ui32 maxPerMap; //unused std::function generateObject; - void setTemplates(si32 type, si32 subtype, TerrainId terrain); + void setTemplates(MapObjectID type, MapObjectSubID subtype, TerrainId terrain); }; class TreasurePlacer: public Modificator From 13763cad8e9262390082d45aa50f2bfdd244e111 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 16:13:08 +0200 Subject: [PATCH 1134/1248] Remove few more implicit conversions --- client/lobby/CBonusSelection.cpp | 46 ++++++++----------- lib/CGameInfoCallback.cpp | 8 ++-- lib/CGameInfoCallback.h | 4 +- lib/MetaString.cpp | 7 ++- lib/MetaString.h | 3 +- lib/gameState/CGameStateCampaign.cpp | 4 +- lib/gameState/SThievesGuildInfo.h | 2 +- lib/mapObjects/CBank.cpp | 2 +- lib/mapObjects/CQuest.cpp | 4 +- lib/mapObjects/MiscObjects.cpp | 2 +- lib/modding/IdentifierStorage.cpp | 4 +- lib/networkPacks/NetPacksLib.cpp | 4 +- lib/rmg/CMapGenOptions.cpp | 2 +- lib/rmg/CMapGenerator.cpp | 4 +- lib/rmg/modificators/TownPlacer.cpp | 2 +- lib/rmg/modificators/TownPlacer.h | 2 +- .../ElementalConditionTest.cpp | 4 +- 17 files changed, 50 insertions(+), 54 deletions(-) diff --git a/client/lobby/CBonusSelection.cpp b/client/lobby/CBonusSelection.cpp index 4605ba3ed..ed572d72d 100644 --- a/client/lobby/CBonusSelection.cpp +++ b/client/lobby/CBonusSelection.cpp @@ -235,38 +235,30 @@ void CBonusSelection::createBonusesIcons() break; case CampaignBonusType::RESOURCE: { - int serialResID = 0; + desc.appendLocalString(EMetaText::GENERAL_TXT, 717); + switch(bonDescs[i].info1) { - case 0: - case 1: - case 2: - case 3: - case 4: - case 5: - case 6: - serialResID = bonDescs[i].info1; - break; - case 0xFD: //wood + ore - serialResID = 7; - break; - case 0xFE: //rare resources - serialResID = 8; - break; + case 0xFD: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 721); + picNumber = 7; + break; + } + case 0xFE: //wood + ore + { + desc.replaceLocalString(EMetaText::GENERAL_TXT, 722); + picNumber = 8; + break; + } + default: + { + desc.replaceName(GameResID(bonDescs[i].info1)); + picNumber = bonDescs[i].info1; + } } - picNumber = serialResID; - desc.appendLocalString(EMetaText::GENERAL_TXT, 717); desc.replaceNumber(bonDescs[i].info2); - - if(serialResID <= 6) - { - desc.replaceLocalString(EMetaText::RES_NAMES, serialResID); - } - else - { - desc.replaceLocalString(EMetaText::GENERAL_TXT, 714 + serialResID); - } break; } case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO: diff --git a/lib/CGameInfoCallback.cpp b/lib/CGameInfoCallback.cpp index a62a69c43..5d054efb6 100644 --- a/lib/CGameInfoCallback.cpp +++ b/lib/CGameInfoCallback.cpp @@ -122,17 +122,17 @@ TurnTimerInfo CGameInfoCallback::getPlayerTurnTime(PlayerColor color) const return TurnTimerInfo{}; } -const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(int identifier) const +const CGObjectInstance * CGameInfoCallback::getObjByQuestIdentifier(ObjectInstanceID identifier) const { if(gs->map->questIdentifierToId.empty()) { //assume that it is VCMI map and quest identifier equals instance identifier - return getObj(ObjectInstanceID(identifier), true); + return getObj(identifier, true); } else { - ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier), "There is no object with such quest identifier!", nullptr); - return getObj(gs->map->questIdentifierToId[identifier]); + ERROR_RET_VAL_IF(!vstd::contains(gs->map->questIdentifierToId, identifier.getNum()), "There is no object with such quest identifier!", nullptr); + return getObj(gs->map->questIdentifierToId[identifier.getNum()]); } } diff --git a/lib/CGameInfoCallback.h b/lib/CGameInfoCallback.h index aeec312d3..65cc686ab 100644 --- a/lib/CGameInfoCallback.h +++ b/lib/CGameInfoCallback.h @@ -93,7 +93,7 @@ public: // std::vector getFlaggableObjects(int3 pos) const; // const CGObjectInstance * getTopObj (int3 pos) const; // PlayerColor getOwner(ObjectInstanceID heroID) const; -// const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) +// const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed) //map // int3 guardingCreaturePosition (int3 pos) const; @@ -190,7 +190,7 @@ public: virtual std::vector getFlaggableObjects(int3 pos) const; virtual const CGObjectInstance * getTopObj (int3 pos) const; virtual PlayerColor getOwner(ObjectInstanceID heroID) const; - virtual const CGObjectInstance *getObjByQuestIdentifier(int identifier) const; //nullptr if object has been removed (eg. killed) + virtual const CGObjectInstance *getObjByQuestIdentifier(ObjectInstanceID identifier) const; //nullptr if object has been removed (eg. killed) //map virtual int3 guardingCreaturePosition (int3 pos) const; diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 944c82190..3da43f519 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -114,8 +114,6 @@ std::string MetaString::getLocalString(const std::pair & txt) c { case EMetaText::GENERAL_TXT: return VLC->generaltexth->translate("core.genrltxt", ser); - case EMetaText::RES_NAMES: - return VLC->generaltexth->translate("core.restypes", ser); case EMetaText::ARRAY_TXT: return VLC->generaltexth->translate("core.arraytxt", ser); case EMetaText::ADVOB_TXT: @@ -374,6 +372,11 @@ void MetaString::replaceName(const SpellID & id) replaceTextID(id.toEntity(VLC)->getNameTextID()); } +void MetaString::replaceName(const GameResID& id) +{ + replaceTextID(TextIdentifier("core.restypes", id.getNum()).get()); +} + void MetaString::replaceNameSingular(const CreatureID & id) { replaceTextID(id.toEntity(VLC)->getNameSingularTextID()); diff --git a/lib/MetaString.h b/lib/MetaString.h index 92d3e3450..ff2ffca19 100644 --- a/lib/MetaString.h +++ b/lib/MetaString.h @@ -21,13 +21,13 @@ class MapObjectSubID; class PlayerColor; class SecondarySkill; class SpellID; +class GameResID; using TQuantity = si32; /// Strings classes that can be used as replacement in MetaString enum class EMetaText : uint8_t { GENERAL_TXT = 1, - RES_NAMES, ARRAY_TXT, ADVOB_TXT, JK_TXT @@ -97,6 +97,7 @@ public: void replaceName(const PlayerColor& id); void replaceName(const SecondarySkill& id); void replaceName(const SpellID& id); + void replaceName(const GameResID& id); /// Replaces first '%s' placeholder with singular or plural name depending on creatures count void replaceName(const CreatureID & id, TQuantity count); diff --git a/lib/gameState/CGameStateCampaign.cpp b/lib/gameState/CGameStateCampaign.cpp index 6ba1a44d6..b78828b57 100644 --- a/lib/gameState/CGameStateCampaign.cpp +++ b/lib/gameState/CGameStateCampaign.cpp @@ -189,8 +189,8 @@ void CGameStateCampaign::placeCampaignHeroes() auto it = gameState->scenarioOps->playerInfos.find(playerColor); if(it != gameState->scenarioOps->playerInfos.end()) { - auto heroTypeId = campaignBonus->info2; - if(heroTypeId == 0xffff) // random bonus hero + HeroTypeID heroTypeId = HeroTypeID(campaignBonus->info2); + if(heroTypeId.getNum() == 0xffff) // random bonus hero { heroTypeId = gameState->pickUnusedHeroTypeRandomly(playerColor); } diff --git a/lib/gameState/SThievesGuildInfo.h b/lib/gameState/SThievesGuildInfo.h index b712d543e..45ed3bdef 100644 --- a/lib/gameState/SThievesGuildInfo.h +++ b/lib/gameState/SThievesGuildInfo.h @@ -23,7 +23,7 @@ struct DLL_LINKAGE SThievesGuildInfo std::map colorToBestHero; //maps player's color to his best heros' std::map personality; // color to personality // ai tactic - std::map bestCreature; // color to ID // id or -1 if not known + std::map bestCreature; // color to ID // id or -1 if not known // template void serialize(Handler &h, const int version) // { diff --git a/lib/mapObjects/CBank.cpp b/lib/mapObjects/CBank.cpp index 14ca4b93e..46661af86 100644 --- a/lib/mapObjects/CBank.cpp +++ b/lib/mapObjects/CBank.cpp @@ -273,7 +273,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const iw.components.emplace_back(ComponentType::RESOURCE, it, bc->resources[it]); loot.appendRawString("%d %s"); loot.replaceNumber(bc->resources[it]); - loot.replaceLocalString(EMetaText::RES_NAMES, it); + loot.replaceName(it); cb->giveResource(hero->getOwner(), it, bc->resources[it]); } } diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index ed1871ef1..f2f9c23b0 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -242,13 +242,13 @@ void CQuest::addTextReplacements(MetaString & text, std::vector & com if(mission.resources.nonZero()) { MetaString loot; - for(int i = 0; i < 7; ++i) + for(auto i : GameResID::ALL_RESOURCES()) { if(mission.resources[i]) { loot.appendRawString("%d %s"); loot.replaceNumber(mission.resources[i]); - loot.replaceLocalString(EMetaText::RES_NAMES, i); + loot.replaceName(i); } } text.replaceRawString(loot.buildList()); diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index 00968d887..ca35c3fdd 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -320,7 +320,7 @@ void CGResource::collectRes(const PlayerColor & player) const { sii.type = EInfoWindowMode::INFO; sii.text.appendLocalString(EMetaText::ADVOB_TXT,113); - sii.text.replaceLocalString(EMetaText::RES_NAMES, resourceID()); + sii.text.replaceName(resourceID()); } sii.components.emplace_back(ComponentType::RESOURCE, resourceID(), amount); sii.soundID = soundBase::pickup01 + CRandomGenerator::getDefault().nextInt(6); diff --git a/lib/modding/IdentifierStorage.cpp b/lib/modding/IdentifierStorage.cpp index cce9700c5..2a360db36 100644 --- a/lib/modding/IdentifierStorage.cpp +++ b/lib/modding/IdentifierStorage.cpp @@ -26,9 +26,9 @@ CIdentifierStorage::CIdentifierStorage() { //TODO: moddable spell schools for (auto i = 0; i < GameConstants::DEFAULT_SCHOOLS; ++i) - registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id); + registerObject(ModScope::scopeBuiltin(), "spellSchool", SpellConfig::SCHOOL[i].jsonName, SpellConfig::SCHOOL[i].id.getNum()); - registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool(SpellSchool::ANY)); + registerObject(ModScope::scopeBuiltin(), "spellSchool", "any", SpellSchool::ANY.getNum()); for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; ++i) registerObject(ModScope::scopeBuiltin(), "resource", GameConstants::RESOURCE_NAMES[i], i); diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index ac5488e35..2fd3b9cbb 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1971,9 +1971,9 @@ void HeroVisit::applyGs(CGameState *gs) void SetAvailableArtifacts::applyGs(CGameState * gs) const { - if(id >= 0) + if(id != ObjectInstanceID::NONE) { - if(auto * bm = dynamic_cast(gs->map->objects[id].get())) + if(auto * bm = dynamic_cast(gs->getObjInstance(id))) { bm->artifacts = arts; } diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index d1bae95eb..035c3bc9b 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -325,7 +325,7 @@ void CMapGenOptions::resetPlayersMap() } else { - logGlobal->warn("Adding settings for player %s", color.encode(color)); + logGlobal->warn("Adding settings for player %s", color); // Usually, all players should be initialized in initPlayersMap() CPlayerSettings settings; players[color] = settings; diff --git a/lib/rmg/CMapGenerator.cpp b/lib/rmg/CMapGenerator.cpp index 78c03890e..188ebb5df 100644 --- a/lib/rmg/CMapGenerator.cpp +++ b/lib/rmg/CMapGenerator.cpp @@ -182,7 +182,7 @@ void CMapGenerator::addPlayerInfo() { // Teams are already configured in CMapGenOptions. However, it's not the case when it comes to map editor - std::set teamsTotal; + std::set teamsTotal; if (mapGenOptions.arePlayersCustomized()) { @@ -284,7 +284,7 @@ void CMapGenerator::addPlayerInfo() player.team = TeamID(*itTeam); teamNumbers[j].erase(itTeam); } - teamsTotal.insert(player.team.getNum()); + teamsTotal.insert(player.team); map->getMap(this).players[pSettings.getColor().getNum()] = player; } diff --git a/lib/rmg/modificators/TownPlacer.cpp b/lib/rmg/modificators/TownPlacer.cpp index f4a153cad..baa6b2d6e 100644 --- a/lib/rmg/modificators/TownPlacer.cpp +++ b/lib/rmg/modificators/TownPlacer.cpp @@ -223,7 +223,7 @@ void TownPlacer::addNewTowns(int count, bool hasFort, const PlayerColor & player } } -si32 TownPlacer::getRandomTownType(bool matchUndergroundType) +FactionID TownPlacer::getRandomTownType(bool matchUndergroundType) { auto townTypesAllowed = (!zone.getTownTypes().empty() ? zone.getTownTypes() : zone.getDefaultTownTypes()); if(matchUndergroundType) diff --git a/lib/rmg/modificators/TownPlacer.h b/lib/rmg/modificators/TownPlacer.h index a144da17d..75028093b 100644 --- a/lib/rmg/modificators/TownPlacer.h +++ b/lib/rmg/modificators/TownPlacer.h @@ -28,7 +28,7 @@ public: protected: void cleanupBoundaries(const rmg::Object & rmgObject); void addNewTowns(int count, bool hasFort, const PlayerColor & player, ObjectManager & manager); - si32 getRandomTownType(bool matchUndergroundType = false); + FactionID getRandomTownType(bool matchUndergroundType = false); void placeTowns(ObjectManager & manager); bool placeMines(ObjectManager & manager); int3 placeMainTown(ObjectManager & manager, CGTownInstance & town); diff --git a/test/spells/targetConditions/ElementalConditionTest.cpp b/test/spells/targetConditions/ElementalConditionTest.cpp index 61df6eb5a..b0085dee7 100644 --- a/test/spells/targetConditions/ElementalConditionTest.cpp +++ b/test/spells/targetConditions/ElementalConditionTest.cpp @@ -30,8 +30,8 @@ public: EXPECT_CALL(spellMock, forEachSchool(NotNull())).Times(AtLeast(1)).WillRepeatedly([](const spells::Spell::SchoolCallback & cb) { bool stop = false; - cb(SpellSchool(SpellSchool::AIR), stop); - cb(SpellSchool(SpellSchool::FIRE), stop); + cb(SpellSchool::AIR, stop); + cb(SpellSchool::FIRE, stop); }); EXPECT_CALL(mechanicsMock, isPositiveSpell()).WillRepeatedly(Return(isPositive)); From 96c81be68ea106c88185b0d1a4f3fc19fea41343 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 17:35:17 +0200 Subject: [PATCH 1135/1248] Win/loss conditions now use VariantIdentifier. Removed non-implemented options --- AI/VCAI/Goals/Win.cpp | 5 - lib/constants/EntityIdentifiers.h | 15 ++ lib/constants/VariantIdentifier.h | 5 +- lib/gameState/CGameState.cpp | 42 ++---- lib/mapObjects/CGHeroInstance.cpp | 2 +- lib/mapObjects/CGTownInstance.cpp | 4 +- lib/mapping/CMap.cpp | 20 +-- lib/mapping/CMapHeader.cpp | 7 +- lib/mapping/CMapHeader.h | 18 +-- lib/mapping/MapFormatH3M.cpp | 34 ++--- lib/mapping/MapFormatJson.cpp | 159 ++++----------------- lib/mapping/MapReaderH3M.cpp | 18 +++ lib/mapping/MapReaderH3M.h | 2 + lib/networkPacks/NetPacksLib.cpp | 4 +- mapeditor/mapsettings/abstractsettings.cpp | 2 - 15 files changed, 108 insertions(+), 229 deletions(-) diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index 2ad9abdf8..86e6ddc8d 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -173,11 +173,6 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::CONST_VALUE: break; - case EventCondition::HAVE_0: - case EventCondition::HAVE_BUILDING_0: - case EventCondition::DESTROY_0: - //TODO: support new condition format - return sptr(Conquer()); default: assert(0); } diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index bbba7b9b6..637eb9e98 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -417,6 +417,21 @@ class BuildingID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; + + static BuildingID TOWN_HALL_LEVEL(uint level) + { + assert(level < 4); + return BuildingID(Type::TOWN_HALL + level); + } + static BuildingID FORT_LEVEL(uint level) + { + assert(level < 3); + return BuildingID(Type::TOWN_HALL + level); + } + + static std::string encode(int32_t index); + static si32 decode(const std::string & identifier); + }; class MapObjectBaseID : public IdentifierBase diff --git a/lib/constants/VariantIdentifier.h b/lib/constants/VariantIdentifier.h index 90269a3bb..175411f67 100644 --- a/lib/constants/VariantIdentifier.h +++ b/lib/constants/VariantIdentifier.h @@ -17,9 +17,10 @@ VCMI_LIB_NAMESPACE_BEGIN template class VariantIdentifier { - std::variant value; -public: + using Type = std::variant; + Type value; +public: VariantIdentifier() {} diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 3a1e040f1..82c79c06e 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1380,7 +1380,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio case EventCondition::HAVE_ARTIFACT: //check if any hero has winning artifact { for(const auto & elem : p->heroes) - if(elem->hasArt(ArtifactID(condition.objectType))) + if(elem->hasArt(condition.objectType.as())) return true; return false; } @@ -1396,7 +1396,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio && (ai = dynamic_cast(object.get()))) //contains army { for(const auto & elem : ai->Slots()) //iterate through army - if(elem.second->type->getIndex() == condition.objectType) //it's searched creature + if(elem.second->getId() == condition.objectType.as()) //it's searched creature total += elem.second->count; } } @@ -1404,20 +1404,20 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::HAVE_RESOURCES: { - return p->resources[condition.objectType] >= condition.value; + return p->resources[condition.objectType.as()] >= condition.value; } case EventCondition::HAVE_BUILDING: { if (condition.object) // specific town { const auto * t = dynamic_cast(condition.object); - return (t->tempOwner == player && t->hasBuilt(BuildingID(condition.objectType))); + return (t->tempOwner == player && t->hasBuilt(condition.objectType.as())); } else // any town { for (const CGTownInstance * t : p->towns) { - if (t->hasBuilt(BuildingID(condition.objectType))) + if (t->hasBuilt(condition.objectType.as())) return true; } return false; @@ -1436,7 +1436,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { for(const auto & elem : map->objects) // mode B - destroy all objects of this type { - if(elem && elem->ID.getNum() == condition.objectType) + if(elem && elem->ID == condition.objectType.as()) return false; } return true; @@ -1457,7 +1457,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio for(const auto & elem : map->objects) // mode B - flag all objects of this type { //check not flagged objs - if ( elem && elem->ID.getNum() == condition.objectType && team.count(elem->tempOwner) == 0 ) + if ( elem && elem->ID == condition.objectType.as() && team.count(elem->tempOwner) == 0 ) return false; } return true; @@ -1466,8 +1466,8 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio case EventCondition::TRANSPORT: { const auto * t = dynamic_cast(condition.object); - return (t->visitingHero && t->visitingHero->hasArt(ArtifactID(condition.objectType))) || - (t->garrisonHero && t->garrisonHero->hasArt(ArtifactID(condition.objectType))); + return (t->visitingHero && t->visitingHero->hasArt(condition.objectType.as())) || + (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType.as())); } case EventCondition::DAYS_PASSED: { @@ -1488,24 +1488,6 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio { return condition.value; // just convert to bool } - case EventCondition::HAVE_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::HAVE_BUILDING_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } - case EventCondition::DESTROY_0: - { - logGlobal->debug("Not implemented event condition type: %d", (int)condition.condition); - //TODO: support new condition format - return false; - } default: logGlobal->error("Invalid event condition type: %d", (int)condition.condition); return false; @@ -1789,13 +1771,13 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) { if(playerInactive(player.second.color)) //do nothing for neutral player continue; - int bestCre = -1; //best creature's ID + CreatureID bestCre; //best creature's ID for(const auto & elem : player.second.heroes) { for(const auto & it : elem->Slots()) { - int toCmp = it.second->type->getId(); //ID of creature we should compare with the best one - if(bestCre == -1 || VLC->creh->objects[bestCre]->getAIValue() < VLC->creh->objects[toCmp]->getAIValue()) + CreatureID toCmp = it.second->type->getId(); //ID of creature we should compare with the best one + if(bestCre == -1 || bestCre.toEntity(VLC)->getAIValue() < toCmp.toEntity(VLC)->getAIValue()) { bestCre = toCmp; } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 80ae5b9f5..febfd8231 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1778,7 +1778,7 @@ bool CGHeroInstance::isMissionCritical() const auto const & testFunctor = [&](const EventCondition & condition) { - if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object) + if ((condition.condition == EventCondition::CONTROL) && condition.object) { const auto * hero = dynamic_cast(condition.object); return (hero != this); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 84bda3112..891ad3d74 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1149,7 +1149,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) for(const BuildingID & id : forbiddenBuildings) { - buildingsLIC.none.insert(id); + buildingsLIC.none.insert(id.getNum()); customBuildings = true; } @@ -1166,7 +1166,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) if(id == BuildingID::FORT) hasFort = true; - buildingsLIC.all.insert(id); + buildingsLIC.all.insert(id.getNum()); customBuildings = true; } diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 98d629adb..3f0e285d0 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -79,7 +79,7 @@ void CCastleEvent::serializeJson(JsonSerializeFormat & handler) a.syncSize(temp); for(int i = 0; i < temp.size(); ++i) { - a.serializeInt(i, temp[i]); + a.serializeInt(i, temp[i].getNum()); buildings.insert(temp[i]); } } @@ -429,16 +429,16 @@ void CMap::checkForObjectives() switch (cond.condition) { case EventCondition::HAVE_ARTIFACT: - event.onFulfill.replaceTextID(VLC->artifacts()->getByIndex(cond.objectType)->getNameTextID()); + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameTextID()); break; case EventCondition::HAVE_CREATURES: - event.onFulfill.replaceTextID(VLC->creatures()->getByIndex(cond.objectType)->getNameSingularTextID()); + event.onFulfill.replaceTextID(cond.objectType.as().toEntity(VLC)->getNameSingularTextID()); event.onFulfill.replaceNumber(cond.value); break; case EventCondition::HAVE_RESOURCES: - event.onFulfill.replaceLocalString(EMetaText::RES_NAMES, cond.objectType); + event.onFulfill.replaceName(cond.objectType.as()); event.onFulfill.replaceNumber(cond.value); break; @@ -449,7 +449,7 @@ void CMap::checkForObjectives() case EventCondition::CONTROL: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.object = getObjectiveObjectFrom(cond.position, cond.objectType.as()); if (cond.object) { @@ -464,7 +464,7 @@ void CMap::checkForObjectives() case EventCondition::DESTROY: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, static_cast(cond.objectType)); + cond.object = getObjectiveObjectFrom(cond.position, cond.objectType.as()); if (cond.object) { @@ -480,14 +480,6 @@ void CMap::checkForObjectives() //break; case EventCondition::IS_HUMAN: //break; case EventCondition::DAYS_WITHOUT_TOWN: //break; case EventCondition::STANDARD_WIN: - - //TODO: support new condition format - case EventCondition::HAVE_0: - break; - case EventCondition::DESTROY_0: - break; - case EventCondition::HAVE_BUILDING_0: - break; } return cond; }; diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index 2ca1ee8e0..bdc3a808e 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -69,21 +69,16 @@ bool PlayerInfo::hasCustomMainHero() const EventCondition::EventCondition(EWinLoseType condition): object(nullptr), - metaType(EMetaclass::INVALID), value(-1), - objectType(-1), - objectSubtype(-1), position(-1, -1, -1), condition(condition) { } -EventCondition::EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position): +EventCondition::EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position): object(nullptr), - metaType(EMetaclass::INVALID), value(value), objectType(objectType), - objectSubtype(-1), position(position), condition(condition) {} diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 2a83e7d0e..e5b34892c 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,6 +10,7 @@ #pragma once +#include "../constants/VariantIdentifier.h" #include "../modding/CModInfo.h" #include "../LogicalExpression.h" #include "../int3.h" @@ -99,7 +100,6 @@ struct DLL_LINKAGE PlayerInfo struct DLL_LINKAGE EventCondition { enum EWinLoseType { - //internal use, deprecated HAVE_ARTIFACT, // type - required artifact HAVE_CREATURES, // type - creatures to collect, value - amount to collect HAVE_RESOURCES, // type - resource ID, value - amount to collect @@ -108,27 +108,21 @@ struct DLL_LINKAGE EventCondition DESTROY, // position - position of object, optional, type - type of object TRANSPORT, // position - where artifact should be transported, type - type of artifact - //map format version pre 1.0 DAYS_PASSED, // value - number of days from start of the game IS_HUMAN, // value - 0 = player is AI, 1 = player is human DAYS_WITHOUT_TOWN, // value - how long player can live without town, 0=instakill STANDARD_WIN, // normal defeat all enemies condition CONST_VALUE, // condition that always evaluates to "value" (0 = false, 1 = true) - - //map format version 1.0+ - HAVE_0, - HAVE_BUILDING_0, - DESTROY_0 }; + using TargetTypeID = VariantIdentifier; + EventCondition(EWinLoseType condition = STANDARD_WIN); - EventCondition(EWinLoseType condition, si32 value, si32 objectType, const int3 & position = int3(-1, -1, -1)); + EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position = int3(-1, -1, -1)); const CGObjectInstance * object; // object that was at specified position or with instance name on start - EMetaclass metaType; si32 value; - si32 objectType; - si32 objectSubtype; + TargetTypeID objectType; std::string objectInstanceName; int3 position; EWinLoseType condition; @@ -141,9 +135,7 @@ struct DLL_LINKAGE EventCondition h & objectType; h & position; h & condition; - h & objectSubtype; h & objectInstanceName; - h & metaType; } }; diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 204d9c925..7d7c5676a 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -268,7 +268,7 @@ void CMapLoaderH3M::readPlayerInfo() { SHeroName vv; vv.heroId = reader->readHero(); - vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId)); + vv.heroName = readLocalizedString(TextIdentifier("header", "heroNames", vv.heroId.getNum())); playerInfo.heroesNames.push_back(vv); } @@ -381,7 +381,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::GATHERRESOURCE: { EventCondition cond(EventCondition::HAVE_RESOURCES); - cond.objectType = reader->readUInt8(); + cond.objectType = reader->readGameResID(); cond.value = reader->readInt32(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); @@ -397,9 +397,9 @@ void CMapLoaderH3M::readVictoryLossConditions() EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); - cond.objectType = BuildingID::TOWN_HALL + reader->readUInt8(); + cond.objectType = BuildingID::TOWN_HALL_LEVEL(reader->readUInt8()); oper.expressions.emplace_back(cond); - cond.objectType = BuildingID::FORT + reader->readUInt8(); + cond.objectType = BuildingID::FORT_LEVEL(reader->readUInt8()); oper.expressions.emplace_back(cond); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.283"); @@ -414,7 +414,7 @@ void CMapLoaderH3M::readVictoryLossConditions() assert(allowNormalVictory == true); // not selectable in editor assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::HAVE_BUILDING); - cond.objectType = BuildingID::GRAIL; + cond.objectType = BuildingID(BuildingID::GRAIL); cond.position = reader->readInt3(); if(cond.position.z > 2) cond.position = int3(-1, -1, -1); @@ -433,7 +433,7 @@ void CMapLoaderH3M::readVictoryLossConditions() allowNormalVictory = true; // H3 behavior assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::HERO; + cond.objectType = MapObjectID(MapObjectID::HERO); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); @@ -446,7 +446,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::CAPTURECITY: { EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = MapObjectID(MapObjectID::TOWN); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); @@ -460,7 +460,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(appliesToAI == true); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; + cond.objectType = MapObjectID(MapObjectID::MONSTER); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); @@ -473,8 +473,8 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::TAKEDWELLINGS: { EventExpression::OperatorAll oper; - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR1)); - oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj::CREATURE_GENERATOR4)); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR1))); + oper.expressions.emplace_back(EventCondition(EventCondition::CONTROL, 0, Obj(Obj::CREATURE_GENERATOR4))); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.289"); specialVictory.onFulfill.appendTextID("core.genrltxt.288"); @@ -486,7 +486,7 @@ void CMapLoaderH3M::readVictoryLossConditions() case EVictoryConditionType::TAKEMINES: { EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::MINE; + cond.objectType = MapObjectID(MapObjectID::MINE); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.291"); specialVictory.onFulfill.appendTextID("core.genrltxt.290"); @@ -499,7 +499,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(allowNormalVictory == true); // not selectable in editor EventCondition cond(EventCondition::TRANSPORT); - cond.objectType = reader->readUInt8(); + cond.objectType = reader->readArtifact8(); cond.position = reader->readInt3(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.293"); @@ -513,7 +513,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { assert(appliesToAI == false); // not selectable in editor EventCondition cond(EventCondition::DESTROY); - cond.objectType = Obj::MONSTER; + cond.objectType = MapObjectID(MapObjectID::MONSTER); specialVictory.effect.toOtherMessage.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toOthers"); specialVictory.onFulfill.appendTextID("vcmi.map.victoryCondition.eliminateMonsters.toSelf"); @@ -602,7 +602,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); cond.position = reader->readInt3(); noneOf.expressions.emplace_back(cond); @@ -616,7 +616,7 @@ void CMapLoaderH3M::readVictoryLossConditions() { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); cond.position = reader->readInt3(); noneOf.expressions.emplace_back(cond); @@ -708,7 +708,7 @@ void CMapLoaderH3M::readDisposedHeroes() { map->disposedHeroes[g].heroId = reader->readHero(); map->disposedHeroes[g].portrait = reader->readHeroPortrait(); - map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId)); + map->disposedHeroes[g].name = readLocalizedString(TextIdentifier("header", "heroes", map->disposedHeroes[g].heroId.getNum())); reader->readBitmaskPlayers(map->disposedHeroes[g].players, false); } } @@ -780,7 +780,7 @@ void CMapLoaderH3M::readAllowedArtifacts() { if(cond.condition == EventCondition::HAVE_ARTIFACT || cond.condition == EventCondition::TRANSPORT) { - map->allowedArtifact.erase(cond.objectType); + map->allowedArtifact.erase(cond.objectType.as()); } return cond; }; diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 90b747419..ba30eeb21 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -139,63 +139,6 @@ namespace TriggeredEventsDetail static const std::array typeNames = { "victory", "defeat" }; - static EMetaclass decodeMetaclass(const std::string & source) - { - if(source.empty()) - return EMetaclass::INVALID; - auto rawId = vstd::find_pos(NMetaclass::names, source); - - if(rawId >= 0) - return static_cast(rawId); - else - return EMetaclass::INVALID; - } - - static std::string encodeIdentifier(EMetaclass metaType, si32 type) - { - std::string metaclassName = NMetaclass::names[static_cast(metaType)]; - std::string identifier; - - switch(metaType) - { - case EMetaclass::ARTIFACT: - { - identifier = ArtifactID::encode(type); - } - break; - case EMetaclass::CREATURE: - { - identifier = CreatureID::encode(type); - } - break; - case EMetaclass::OBJECT: - { - //TODO - auto subtypes = VLC->objtypeh->knownSubObjects(type); - if(!subtypes.empty()) - { - auto subtype = *subtypes.begin(); - auto handler = VLC->objtypeh->getHandlerFor(type, subtype); - identifier = handler->getTypeName(); - } - } - break; - case EMetaclass::RESOURCE: - { - identifier = GameConstants::RESOURCE_NAMES[type]; - } - break; - default: - { - logGlobal->error("Unsupported metaclass %s for event condition", metaclassName); - return ""; - } - break; - } - - return ModUtility::makeFullIdentifier("", metaclassName, identifier); - } - static EventCondition JsonToCondition(const JsonNode & node) { EventCondition event; @@ -210,54 +153,28 @@ namespace TriggeredEventsDetail { const JsonNode & data = node.Vector()[1]; + event.objectInstanceName = data["object"].String(); + event.value = data["value"].Integer(); + switch (event.condition) { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes - - std::string fullIdentifier = data["type"].String(); - std::string metaTypeName; - std::string scope; - std::string identifier; - ModUtility::parseIdentifier(fullIdentifier, scope, metaTypeName, identifier); - - event.metaType = decodeMetaclass(metaTypeName); - - auto type = VLC->identifiers()->getIdentifier(ModScope::scopeBuiltin(), fullIdentifier, false); - - if(type) - event.objectType = type.value(); - event.objectInstanceName = data["object"].String(); - if(data["value"].isNumber()) - event.value = static_cast(data["value"].Integer()); - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if (data["type"].getType() == JsonNode::JsonType::DATA_STRING) - { - auto identifier = VLC->identifiers()->getIdentifier(data["type"]); - if(identifier) - event.objectType = identifier.value(); - else - throw std::runtime_error("Identifier resolution failed in event condition"); - } - - if (data["type"].isNumber()) - event.objectType = static_cast(data["type"].Float()); - - if (!data["value"].isNull()) - event.value = static_cast(data["value"].Float()); - } - break; + case EventCondition::HAVE_ARTIFACT: + case EventCondition::TRANSPORT: + event.objectType = ArtifactID(ArtifactID::decode(data["type"].String())); + break; + case EventCondition::HAVE_CREATURES: + event.objectType = CreatureID(CreatureID::decode(data["type"].String())); + break; + case EventCondition::HAVE_RESOURCES: + event.objectType = GameResID(GameResID::decode(data["type"].String())); + break; + case EventCondition::HAVE_BUILDING: + event.objectType = BuildingID(BuildingID::decode(data["type"].String())); + break; + case EventCondition::CONTROL: + case EventCondition::DESTROY: + event.objectType = MapObjectID(MapObjectID::decode(data["type"].String())); + break; } if (!data["position"].isNull()) @@ -283,39 +200,11 @@ namespace TriggeredEventsDetail JsonNode data; - switch (event.condition) - { - case EventCondition::HAVE_0: - case EventCondition::DESTROY_0: - { - //todo: support subtypes + if(!event.objectInstanceName.empty()) + data["object"].String() = event.objectInstanceName; - if(event.metaType != EMetaclass::INVALID) - data["type"].String() = encodeIdentifier(event.metaType, event.objectType); - - if(event.value > 0) - data["value"].Integer() = event.value; - - if(!event.objectInstanceName.empty()) - data["object"].String() = event.objectInstanceName; - } - break; - case EventCondition::HAVE_BUILDING_0: - { - //todo: support of new condition format HAVE_BUILDING_0 - } - break; - default: - { - //old format - if(event.objectType != -1) - data["type"].Integer() = event.objectType; - - if(event.value != -1) - data["value"].Integer() = event.value; - } - break; - } + data["type"].String() = event.objectType.toString(); + data["value"].Integer() = event.value; if(event.position != int3(-1, -1, -1)) { diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 00334d443..e0ee0a83c 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -81,6 +81,17 @@ ArtifactID MapReaderH3M::readArtifact() return ArtifactID::NONE; } +ArtifactID MapReaderH3M::readArtifact8() +{ + ArtifactID result(reader->readInt8()); + + if (result.getNum() < features.artifactsCount) + return remapIdentifier(result); + + logGlobal->warn("Map contains invalid artifact %d. Will be removed!", result.getNum()); + return ArtifactID::NONE; +} + ArtifactID MapReaderH3M::readArtifact32() { ArtifactID result(reader->readInt32()); @@ -197,6 +208,13 @@ SpellID MapReaderH3M::readSpell32() return result; } +GameResID MapReaderH3M::readGameResID() +{ + GameResID result(readInt8()); + assert(result.getNum() < features.resourcesCount); + return result; +} + PlayerColor MapReaderH3M::readPlayer() { uint8_t value = readUInt8(); diff --git a/lib/mapping/MapReaderH3M.h b/lib/mapping/MapReaderH3M.h index 92da10ab0..e2b5e01b8 100644 --- a/lib/mapping/MapReaderH3M.h +++ b/lib/mapping/MapReaderH3M.h @@ -32,6 +32,7 @@ public: void setIdentifierRemapper(const MapIdentifiersH3M & remapper); ArtifactID readArtifact(); + ArtifactID readArtifact8(); ArtifactID readArtifact32(); CreatureID readCreature(); HeroTypeID readHero(); @@ -42,6 +43,7 @@ public: SecondarySkill readSkill(); SpellID readSpell(); SpellID readSpell32(); + GameResID readGameResID(); PlayerColor readPlayer(); PlayerColor readPlayer32(); diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 2fd3b9cbb..90ae0064d 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1218,12 +1218,12 @@ void RemoveObject::applyGs(CGameState *gs) { if (cond.object == obj) { - if (cond.condition == EventCondition::DESTROY || cond.condition == EventCondition::DESTROY_0) + if (cond.condition == EventCondition::DESTROY) { cond.condition = EventCondition::CONST_VALUE; cond.value = 1; // destroyed object, from now on always fulfilled } - else if (cond.condition == EventCondition::CONTROL || cond.condition == EventCondition::HAVE_0) + else if (cond.condition == EventCondition::CONTROL) { cond.condition = EventCondition::CONST_VALUE; cond.value = 0; // destroyed object, from now on can not be fulfilled diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index c15c52cd9..6b4d251a1 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -129,9 +129,7 @@ JsonNode AbstractSettings::conditionToJson(const EventCondition & event) result["condition"].Integer() = event.condition; result["value"].Integer() = event.value; result["objectType"].Integer() = event.objectType; - result["objectSubytype"].Integer() = event.objectSubtype; result["objectInstanceName"].String() = event.objectInstanceName; - result["metaType"].Integer() = (ui8)event.metaType; { auto & position = result["position"].Vector(); position.resize(3); From 6cb1f6ff111349592f8a551f4ffd4cb998214e05 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 17:49:08 +0200 Subject: [PATCH 1136/1248] Remove all remaining implicit conversion in lib --- client/windows/CTradeWindow.cpp | 2 +- client/windows/GUIClasses.cpp | 2 +- lib/gameState/CGameState.cpp | 6 ++++-- lib/mapObjectConstructors/CommonConstructors.cpp | 2 +- lib/mapObjects/CGMarket.cpp | 16 ++++++++-------- lib/mapObjects/CGMarket.h | 8 ++++---- lib/mapObjects/CGTownInstance.cpp | 8 ++++---- lib/mapObjects/CGTownInstance.h | 4 ++-- lib/mapObjects/IMarket.cpp | 8 ++++---- lib/mapObjects/IMarket.h | 5 +++-- lib/mapping/CMap.cpp | 5 +++-- 11 files changed, 35 insertions(+), 31 deletions(-) diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index ccea4ad11..dce610f92 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -632,7 +632,7 @@ void CMarketplaceWindow::artifactsChanged(bool Left) if(mode != EMarketMode::RESOURCE_ARTIFACT) return; - std::vector available = market->availableItemsIds(mode); + std::vector available = market->availableItemsIds(mode); std::set> toRemove; for(auto item : items[0]) if(!vstd::contains(available, item->id)) diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 63f92507f..ff78b7336 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1156,7 +1156,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket clerkSpeech = std::make_shared(speechStr, Rect(24, 129, 413, 70), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE); title = std::make_shared(231, 26, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, titleStr); - std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); + std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); for(int i=0; i(this, goods[i], 54+i*104, 234)); diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 82c79c06e..ca3ccb7f5 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -778,8 +778,10 @@ void CGameState::initTowns() campaign->initTowns(); CGTownInstance::universitySkills.clear(); - for ( int i=0; i<4; i++) - CGTownInstance::universitySkills.push_back(14+i);//skills for university + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::FIRE_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::AIR_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::WATER_MAGIC)); + CGTownInstance::universitySkills.push_back(SecondarySkill(SecondarySkill::EARTH_MAGIC)); for (auto & elem : map->towns) { diff --git a/lib/mapObjectConstructors/CommonConstructors.cpp b/lib/mapObjectConstructors/CommonConstructors.cpp index 800e930af..13f286cbf 100644 --- a/lib/mapObjectConstructors/CommonConstructors.cpp +++ b/lib/mapObjectConstructors/CommonConstructors.cpp @@ -261,7 +261,7 @@ void MarketInstanceConstructor::randomizeObject(CGMarket * object, CRandomGenera if(auto * university = dynamic_cast(object)) { for(auto skill : JsonRandom::loadSecondaries(predefinedOffer, rng, emptyVariables)) - university->skills.push_back(skill.first.getNum()); + university->skills.push_back(skill.first); } } diff --git a/lib/mapObjects/CGMarket.cpp b/lib/mapObjects/CGMarket.cpp index f6ac45633..951dd41e9 100644 --- a/lib/mapObjects/CGMarket.cpp +++ b/lib/mapObjects/CGMarket.cpp @@ -48,18 +48,18 @@ int CGMarket::availableUnits(EMarketMode mode, int marketItemSerial) const return -1; } -std::vector CGMarket::availableItemsIds(EMarketMode mode) const +std::vector CGMarket::availableItemsIds(EMarketMode mode) const { if(allowsTrade(mode)) return IMarket::availableItemsIds(mode); - return std::vector(); + return std::vector(); } CGMarket::CGMarket() { } -std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const +std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const { switch(mode) { @@ -67,16 +67,16 @@ std::vector CGBlackMarket::availableItemsIds(EMarketMode mode) const return IMarket::availableItemsIds(mode); case EMarketMode::RESOURCE_ARTIFACT: { - std::vector ret; + std::vector ret; for(const CArtifact *a : artifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } default: - return std::vector(); + return std::vector(); } } @@ -96,7 +96,7 @@ void CGBlackMarket::newTurn(CRandomGenerator & rand) const cb->sendAndApply(&saa); } -std::vector CGUniversity::availableItemsIds(EMarketMode mode) const +std::vector CGUniversity::availableItemsIds(EMarketMode mode) const { switch (mode) { @@ -104,7 +104,7 @@ std::vector CGUniversity::availableItemsIds(EMarketMode mode) const return skills; default: - return std::vector(); + return std::vector(); } } diff --git a/lib/mapObjects/CGMarket.h b/lib/mapObjects/CGMarket.h index 386988dfc..abeefd284 100644 --- a/lib/mapObjects/CGMarket.h +++ b/lib/mapObjects/CGMarket.h @@ -34,7 +34,7 @@ public: int getMarketEfficiency() const override; bool allowsTrade(EMarketMode mode) const override; int availableUnits(EMarketMode mode, int marketItemSerial) const override; //-1 if unlimited - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; template void serialize(Handler &h, const int version) { @@ -52,7 +52,7 @@ public: std::vector artifacts; //available artifacts void newTurn(CRandomGenerator & rand) const override; //reset artifacts for black market every month - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; template void serialize(Handler &h, const int version) { @@ -64,9 +64,9 @@ public: class DLL_LINKAGE CGUniversity : public CGMarket { public: - std::vector skills; //available skills + std::vector skills; //available skills - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; void onHeroVisit(const CGHeroInstance * h) const override; //open window template void serialize(Handler &h, const int version) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 891ad3d74..ac887cbc9 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -34,7 +34,7 @@ VCMI_LIB_NAMESPACE_BEGIN std::vector CGTownInstance::merchantArtifacts; -std::vector CGTownInstance::universitySkills; +std::vector CGTownInstance::universitySkills; int CGTownInstance::getSightRadius() const //returns sight distance @@ -765,16 +765,16 @@ bool CGTownInstance::allowsTrade(EMarketMode mode) const } } -std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const +std::vector CGTownInstance::availableItemsIds(EMarketMode mode) const { if(mode == EMarketMode::RESOURCE_ARTIFACT) { - std::vector ret; + std::vector ret; for(const CArtifact *a : merchantArtifacts) if(a) ret.push_back(a->getId()); else - ret.push_back(-1); + ret.push_back(ArtifactID{}); return ret; } else if ( mode == EMarketMode::RESOURCE_SKILL ) diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index 2239e728a..ceca1e89c 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -68,7 +68,7 @@ public: ////////////////////////////////////////////////////////////////////////// static std::vector merchantArtifacts; //vector of artifacts available at Artifact merchant, NULLs possible (for making empty space when artifact is bought) - static std::vector universitySkills;//skills for university of magic + static std::vector universitySkills;//skills for university of magic template void serialize(Handler &h, const int version) { @@ -150,7 +150,7 @@ public: const IObjectInterface * getObject() const override; int getMarketEfficiency() const override; //=market count bool allowsTrade(EMarketMode mode) const override; - std::vector availableItemsIds(EMarketMode mode) const override; + std::vector availableItemsIds(EMarketMode mode) const override; void updateAppearance(); diff --git a/lib/mapObjects/IMarket.cpp b/lib/mapObjects/IMarket.cpp index d37662028..907d90378 100644 --- a/lib/mapObjects/IMarket.cpp +++ b/lib/mapObjects/IMarket.cpp @@ -140,16 +140,16 @@ int IMarket::availableUnits(EMarketMode mode, int marketItemSerial) const } } -std::vector IMarket::availableItemsIds(EMarketMode mode) const +std::vector IMarket::availableItemsIds(EMarketMode mode) const { - std::vector ret; + std::vector ret; switch(mode) { case EMarketMode::RESOURCE_RESOURCE: case EMarketMode::ARTIFACT_RESOURCE: case EMarketMode::CREATURE_RESOURCE: - for (int i = 0; i < 7; i++) - ret.push_back(i); + for (auto res : GameResID::ALL_RESOURCES()) + ret.push_back(res); } return ret; } diff --git a/lib/mapObjects/IMarket.h b/lib/mapObjects/IMarket.h index f279ffe6d..cc20300ab 100644 --- a/lib/mapObjects/IMarket.h +++ b/lib/mapObjects/IMarket.h @@ -9,7 +9,8 @@ */ #pragma once -#include "../GameConstants.h" +#include "../networkPacks/TradeItem.h" +#include "../constants/Enumerations.h" VCMI_LIB_NAMESPACE_BEGIN @@ -24,7 +25,7 @@ public: virtual int getMarketEfficiency() const = 0; virtual bool allowsTrade(EMarketMode mode) const; virtual int availableUnits(EMarketMode mode, int marketItemSerial) const; //-1 if unlimited - virtual std::vector availableItemsIds(EMarketMode mode) const; + virtual std::vector availableItemsIds(EMarketMode mode) const; bool getOffer(int id1, int id2, int &val1, int &val2, EMarketMode mode) const; //val1 - how many units of id1 player has to give to receive val2 units std::vector availableModes() const; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 3f0e285d0..ec17344ef 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -79,8 +79,9 @@ void CCastleEvent::serializeJson(JsonSerializeFormat & handler) a.syncSize(temp); for(int i = 0; i < temp.size(); ++i) { - a.serializeInt(i, temp[i].getNum()); - buildings.insert(temp[i]); + int buildingID = temp[i].getNum(); + a.serializeInt(i, buildingID); + buildings.insert(buildingID); } } From 52050d0ef15961ec8c48ad605ef7689828ee5209 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 18:50:37 +0200 Subject: [PATCH 1137/1248] Fix build --- AI/VCAI/Goals/Win.cpp | 8 ++++---- client/windows/CSpellWindow.cpp | 4 ++-- client/windows/CTradeWindow.cpp | 6 ++++-- client/windows/GUIClasses.cpp | 4 ++-- lib/constants/EntityIdentifiers.cpp | 10 ++++++++++ lib/constants/EntityIdentifiers.h | 7 +++---- lib/constants/IdentifierBase.h | 8 ++++---- lib/gameState/CGameState.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 2 +- mapeditor/mapsettings/abstractsettings.cpp | 2 +- mapeditor/mapsettings/loseconditions.cpp | 4 ++-- mapeditor/mapsettings/victoryconditions.cpp | 16 ++++++++-------- scripting/lua/api/GameCb.cpp | 1 - test/mock/mock_IGameInfoCallback.h | 5 ++++- test/mock/mock_battle_Unit.h | 2 +- 15 files changed, 47 insertions(+), 34 deletions(-) diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index 86e6ddc8d..f54ed6dc7 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -51,7 +51,7 @@ TSubgoal Win::whatToDoToAchieve() switch(goal.condition) { case EventCondition::HAVE_ARTIFACT: - return sptr(GetArtOfType(goal.objectType)); + return sptr(GetArtOfType(goal.objectType.as())); case EventCondition::DESTROY: { if(goal.object) @@ -78,7 +78,7 @@ TSubgoal Win::whatToDoToAchieve() // goal.object = optional, town in which building should be built // Represents "Improve town" condition from H3 (but unlike H3 it consists from 2 separate conditions) - if(goal.objectType == BuildingID::GRAIL) + if(goal.objectType.as() == BuildingID::GRAIL) { if(auto h = ai->getHeroWithGrail()) { @@ -149,9 +149,9 @@ TSubgoal Win::whatToDoToAchieve() case EventCondition::HAVE_RESOURCES: //TODO mines? piles? marketplace? //save? - return sptr(CollectRes(static_cast(goal.objectType), goal.value)); + return sptr(CollectRes(goal.objectType.as(), goal.value)); case EventCondition::HAVE_CREATURES: - return sptr(GatherTroops(goal.objectType, goal.value)); + return sptr(GatherTroops(goal.objectType.as(), goal.value)); case EventCondition::TRANSPORT: { //TODO. merge with bring Grail to town? So AI will first dig grail, then transport it using this goal and builds it diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 5fb2f1a8d..a7f18b776 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -639,7 +639,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) mySpell = spell; if(mySpell) { - int32_t whichSchool = 0; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, + SpellSchool whichSchool; //0 - air magic, 1 - fire magic, 2 - water magic, 3 - earth magic, schoolLevel = owner->myHero->getSpellSchoolLevel(mySpell, &whichSchool); auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero); @@ -648,7 +648,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool : owner->selectedTab], schoolLevel); + schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool.getNum() : owner->selectedTab], schoolLevel); } ColorRGBA firstLineColor, secondLineColor; diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index dce610f92..4b0bf9ccd 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -149,7 +149,9 @@ std::vector *CTradeWindow::getItemsIds(bool Left) break; case ARTIFACT_TYPE: - ids = new std::vector(market->availableItemsIds(mode)); + ids = new std::vector; + for (auto const & item : market->availableItemsIds(mode)) + ids->push_back(item.getNum()); break; } } @@ -635,7 +637,7 @@ void CMarketplaceWindow::artifactsChanged(bool Left) std::vector available = market->availableItemsIds(mode); std::set> toRemove; for(auto item : items[0]) - if(!vstd::contains(available, item->id)) + if(!vstd::contains(available, ArtifactID(item->id))) toRemove.insert(item); removeItems(toRemove); diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index ff78b7336..498f4a6d4 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -1159,7 +1159,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, const IMarket std::vector goods = market->availableItemsIds(EMarketMode::RESOURCE_SKILL); for(int i=0; i(this, goods[i], 54+i*104, 234)); + items.push_back(std::make_shared(this, goods[i].as(), 54+i*104, 234)); cancel = std::make_shared(Point(200, 313), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[632], [&](){ close(); }, EShortcut::GLOBAL_ACCEPT); statusbar = CGStatusBar::create(std::make_shared(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26)); @@ -1579,7 +1579,7 @@ CThievesGuildWindow::CThievesGuildWindow(const CGObjectInstance * _owner): counter = 0; for(auto & it : tgi.bestCreature) { - if(it.second >= 0) + if(it.second != CreatureID::NONE) bestCreatures.push_back(std::make_shared(AnimationPath::builtin("TWCRPORT"), it.second+2, 0, 255 + 66 * counter, 479)); counter++; } diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 43694c809..32bff064c 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -532,4 +532,14 @@ std::string SecondarySkill::entityType() return "secondarySkill"; } +std::string BuildingID::encode(int32_t index) +{ + return std::to_string(index); +} + +si32 BuildingID::decode(const std::string & identifier) +{ + return std::stoi(identifier); +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 637eb9e98..e69bfff0d 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -413,15 +413,15 @@ public: } }; -class BuildingID : public IdentifierWithEnum +class DLL_LINKAGE BuildingID : public IdentifierWithEnum { public: using IdentifierWithEnum::IdentifierWithEnum; - static BuildingID TOWN_HALL_LEVEL(uint level) + static BuildingID HALL_LEVEL(uint level) { assert(level < 4); - return BuildingID(Type::TOWN_HALL + level); + return BuildingID(Type::VILLAGE_HALL + level); } static BuildingID FORT_LEVEL(uint level) { @@ -431,7 +431,6 @@ public: static std::string encode(int32_t index); static si32 decode(const std::string & identifier); - }; class MapObjectBaseID : public IdentifierBase diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index b8f06d391..5ad0a8dd3 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -52,10 +52,10 @@ public: num += change; } -// constexpr operator int32_t () const -// { -// return num; -// } + constexpr operator int32_t () const + { + return num; + } friend std::ostream& operator<<(std::ostream& os, const IdentifierBase& dt) { diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index ca3ccb7f5..104f3ac66 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1779,7 +1779,7 @@ void CGameState::obtainPlayersStats(SThievesGuildInfo & tgi, int level) for(const auto & it : elem->Slots()) { CreatureID toCmp = it.second->type->getId(); //ID of creature we should compare with the best one - if(bestCre == -1 || bestCre.toEntity(VLC)->getAIValue() < toCmp.toEntity(VLC)->getAIValue()) + if(bestCre == CreatureID::NONE || bestCre.toEntity(VLC)->getAIValue() < toCmp.toEntity(VLC)->getAIValue()) { bestCre = toCmp; } diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 7d7c5676a..0edefb82c 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -397,7 +397,7 @@ void CMapLoaderH3M::readVictoryLossConditions() EventExpression::OperatorAll oper; EventCondition cond(EventCondition::HAVE_BUILDING); cond.position = reader->readInt3(); - cond.objectType = BuildingID::TOWN_HALL_LEVEL(reader->readUInt8()); + cond.objectType = BuildingID::HALL_LEVEL(reader->readUInt8() + 1); oper.expressions.emplace_back(cond); cond.objectType = BuildingID::FORT_LEVEL(reader->readUInt8()); oper.expressions.emplace_back(cond); diff --git a/mapeditor/mapsettings/abstractsettings.cpp b/mapeditor/mapsettings/abstractsettings.cpp index 6b4d251a1..3ea3dcbe6 100644 --- a/mapeditor/mapsettings/abstractsettings.cpp +++ b/mapeditor/mapsettings/abstractsettings.cpp @@ -128,7 +128,7 @@ JsonNode AbstractSettings::conditionToJson(const EventCondition & event) JsonNode result; result["condition"].Integer() = event.condition; result["value"].Integer() = event.value; - result["objectType"].Integer() = event.objectType; + result["objectType"].String() = event.objectType.toString(); result["objectInstanceName"].String() = event.objectInstanceName; { auto & position = result["position"].Vector(); diff --git a/mapeditor/mapsettings/loseconditions.cpp b/mapeditor/mapsettings/loseconditions.cpp index e19962734..dc679b685 100644 --- a/mapeditor/mapsettings/loseconditions.cpp +++ b/mapeditor/mapsettings/loseconditions.cpp @@ -156,7 +156,7 @@ void LoseConditions::update() case 0: { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; @@ -171,7 +171,7 @@ void LoseConditions::update() case 1: { EventExpression::OperatorNone noneOf; EventCondition cond(EventCondition::CONTROL); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); assert(loseTypeWidget); int townIdx = loseTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; diff --git a/mapeditor/mapsettings/victoryconditions.cpp b/mapeditor/mapsettings/victoryconditions.cpp index f1db91643..5b4efe9e6 100644 --- a/mapeditor/mapsettings/victoryconditions.cpp +++ b/mapeditor/mapsettings/victoryconditions.cpp @@ -219,7 +219,7 @@ void VictoryConditions::update() case 0: { EventCondition cond(EventCondition::HAVE_ARTIFACT); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.281"); specialVictory.onFulfill.appendTextID("core.genrltxt.280"); specialVictory.trigger = EventExpression(cond); @@ -229,7 +229,7 @@ void VictoryConditions::update() case 1: { EventCondition cond(EventCondition::HAVE_CREATURES); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = CreatureID(victoryTypeWidget->currentData().toInt()); cond.value = victoryValueWidget->text().toInt(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.277"); specialVictory.onFulfill.appendTextID("core.genrltxt.276"); @@ -240,7 +240,7 @@ void VictoryConditions::update() case 2: { EventCondition cond(EventCondition::HAVE_RESOURCES); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = GameResID(victoryTypeWidget->currentData().toInt()); cond.value = victoryValueWidget->text().toInt(); specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.279"); specialVictory.onFulfill.appendTextID("core.genrltxt.278"); @@ -251,7 +251,7 @@ void VictoryConditions::update() case 3: { EventCondition cond(EventCondition::HAVE_BUILDING); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = BuildingID(victoryTypeWidget->currentData().toInt()); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) cond.position = controller->map()->objects[townIdx]->pos; @@ -264,7 +264,7 @@ void VictoryConditions::update() case 4: { EventCondition cond(EventCondition::CONTROL); assert(victoryTypeWidget); - cond.objectType = Obj::TOWN; + cond.objectType = Obj(Obj::TOWN); int townIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[townIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.250"); @@ -276,7 +276,7 @@ void VictoryConditions::update() case 5: { EventCondition cond(EventCondition::DESTROY); assert(victoryTypeWidget); - cond.objectType = Obj::HERO; + cond.objectType = Obj(Obj::HERO); int heroIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[heroIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.253"); @@ -288,7 +288,7 @@ void VictoryConditions::update() case 6: { EventCondition cond(EventCondition::TRANSPORT); assert(victoryTypeWidget); - cond.objectType = victoryTypeWidget->currentData().toInt(); + cond.objectType = ArtifactID(victoryTypeWidget->currentData().toInt()); int townIdx = victorySelectWidget->currentData().toInt(); if(townIdx > -1) cond.position = controller->map()->objects[townIdx]->pos; @@ -301,7 +301,7 @@ void VictoryConditions::update() case 7: { EventCondition cond(EventCondition::DESTROY); assert(victoryTypeWidget); - cond.objectType = Obj::MONSTER; + cond.objectType = Obj(Obj::MONSTER); int monsterIdx = victoryTypeWidget->currentData().toInt(); cond.position = controller->map()->objects[monsterIdx]->pos; specialVictory.effect.toOtherMessage.appendTextID("core.genrltxt.287"); diff --git a/scripting/lua/api/GameCb.cpp b/scripting/lua/api/GameCb.cpp index 7227f403b..0aa3cd0e6 100644 --- a/scripting/lua/api/GameCb.cpp +++ b/scripting/lua/api/GameCb.cpp @@ -29,7 +29,6 @@ VCMI_REGISTER_CORE_SCRIPT_API(GameCbProxy, "Game"); const std::vector GameCbProxy::REGISTER_CUSTOM = { {"getDate", LuaMethodWrapper::invoke, false}, - {"isAllowed", LuaMethodWrapper::invoke, false}, {"getPlayer", LuaMethodWrapper::invoke, false}, {"getHero", LuaMethodWrapper::invoke, false}, diff --git a/test/mock/mock_IGameInfoCallback.h b/test/mock/mock_IGameInfoCallback.h index 879f0c374..94939fc24 100644 --- a/test/mock/mock_IGameInfoCallback.h +++ b/test/mock/mock_IGameInfoCallback.h @@ -17,7 +17,10 @@ class IGameInfoCallbackMock : public IGameInfoCallback public: //various MOCK_CONST_METHOD1(getDate, int(Date)); - MOCK_CONST_METHOD2(isAllowed, bool(int32_t, int32_t)); + + MOCK_CONST_METHOD1(isAllowed, bool(SpellID)); + MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID)); + MOCK_CONST_METHOD1(isAllowed, bool(SecondarySkill)); //player MOCK_CONST_METHOD1(getPlayer, const Player *(PlayerColor)); diff --git a/test/mock/mock_battle_Unit.h b/test/mock/mock_battle_Unit.h index 5b32c7e48..c9ea0625a 100644 --- a/test/mock/mock_battle_Unit.h +++ b/test/mock/mock_battle_Unit.h @@ -19,7 +19,7 @@ public: MOCK_CONST_METHOD0(getTreeVersion, int64_t()); MOCK_CONST_METHOD0(getCasterUnitId, int32_t()); - MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, int32_t *)); + MOCK_CONST_METHOD2(getSpellSchoolLevel, int32_t(const spells::Spell *, SpellSchool *)); MOCK_CONST_METHOD1(getEffectLevel, int32_t(const spells::Spell *)); MOCK_CONST_METHOD3(getSpellBonus, int64_t(const spells::Spell *, int64_t, const battle::Unit *)); MOCK_CONST_METHOD2(getSpecificSpellBonus, int64_t(const spells::Spell *, int64_t)); From 59b2cbe4d21e0dd8ae376850e754a60b21696de2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 8 Nov 2023 19:49:42 +0200 Subject: [PATCH 1138/1248] Fix regressions --- lib/constants/EntityIdentifiers.h | 4 ++-- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/serializer/JsonSerializeFormat.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index e69bfff0d..63b3f4373 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -418,12 +418,12 @@ class DLL_LINKAGE BuildingID : public IdentifierWithEnum::IdentifierWithEnum; - static BuildingID HALL_LEVEL(uint level) + static BuildingID HALL_LEVEL(unsigned int level) { assert(level < 4); return BuildingID(Type::VILLAGE_HALL + level); } - static BuildingID FORT_LEVEL(uint level) + static BuildingID FORT_LEVEL(unsigned int level) { assert(level < 3); return BuildingID(Type::TOWN_HALL + level); diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index ac887cbc9..f7b5081f5 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1208,7 +1208,7 @@ void CGTownInstance::serializeJsonOptions(JsonSerializeFormat & handler) FactionID CGTownInstance::getFaction() const { - return town->faction->getId(); + return FactionID(subID.getNum()); } TerrainId CGTownInstance::getNativeTerrain() const diff --git a/lib/serializer/JsonSerializeFormat.h b/lib/serializer/JsonSerializeFormat.h index 93dc2caa9..cb4a6c248 100644 --- a/lib/serializer/JsonSerializeFormat.h +++ b/lib/serializer/JsonSerializeFormat.h @@ -213,7 +213,7 @@ public: for (auto entry : value) valueInt.insert(entry.getNum()); - serializeLIC(fieldName, decoder, encoder, standard, value); + serializeLIC(fieldName, decoder, encoder, standardInt, valueInt); value.clear(); for (auto entry : valueInt) From d1a4e84255c8b1a8f60769f7078a2ffa7eedc121 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 15 Nov 2023 17:57:40 +0200 Subject: [PATCH 1139/1248] Show message about mods that failed to load on opening main menu --- Mods/vcmi/config/vcmi/english.json | 2 ++ Mods/vcmi/config/vcmi/ukrainian.json | 2 ++ client/mainmenu/CMainMenu.cpp | 12 ++++++++++ lib/modding/CModHandler.cpp | 36 +++++++++++++++++++++++----- lib/modding/CModHandler.h | 11 ++++++--- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 4ede80f5c..e6cb860bb 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -73,6 +73,8 @@ "vcmi.server.errors.modsToEnable" : "{Following mods are required}", "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", + "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 3a50c17f9..e7be77050 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -73,6 +73,8 @@ "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", "vcmi.server.errors.modsToDisable" : "{Модифікації що мають бути вимкнені}", "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", + "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", + "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 68e270866..4f579fb41 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -49,6 +49,7 @@ #include "../../lib/filesystem/Filesystem.h" #include "../../lib/filesystem/CCompressedStream.h" #include "../../lib/mapping/CMapInfo.h" +#include "../../lib/modding/CModHandler.h" #include "../../lib/VCMIDirs.h" #include "../../lib/CStopWatch.h" #include "../../lib/CThreadHelper.h" @@ -339,6 +340,17 @@ void CMainMenu::update() menu->switchToTab(menu->getActiveTab()); } + static bool warnedAboutModDependencies = false; + + if (!warnedAboutModDependencies) + { + warnedAboutModDependencies = true; + auto errorMessages = CGI->modh->getModLoadErrors(); + + if (!errorMessages.empty()) + CInfoWindow::showInfoDialog(errorMessages, std::vector>(), PlayerColor(1)); + } + // Handles mouse and key input GH.handleEvents(); GH.windows().simpleRedraw(); diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 7c220914d..88e3f1851 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -21,6 +21,7 @@ #include "../CStopWatch.h" #include "../GameSettings.h" #include "../Languages.h" +#include "../MetaString.h" #include "../ScriptHandler.h" #include "../constants/StringConstants.h" #include "../filesystem/Filesystem.h" @@ -133,6 +134,24 @@ std::vector CModHandler::validateAndSortDependencies(std::vector (); + + auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID) + { + modLoadErrors->appendTextID(textID); + + if (allMods.count(brokenModID)) + modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name); + else + modLoadErrors->replaceRawString(brokenModID); + + if (allMods.count(missingModID)) + modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name); + else + modLoadErrors->replaceRawString(missingModID); + + }; + // Left mods have unresolved dependencies, output all to log. for(const auto & brokenModID : modsToResolve) { @@ -140,17 +159,17 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' has been disabled: dependency '%s' is missing.", brokenMod.getVerificationInfo().name, dependency); + addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency); } for(const TModID & conflict : brokenMod.conflicts) { if(vstd::contains(resolvedModIDs, conflict)) - logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, conflict); + addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict); } for(const TModID & reverseConflict : resolvedModIDs) { if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID)) - logMod->error("Mod '%s' has been disabled: conflicts with enabled mod '%s'.", brokenMod.getVerificationInfo().name, reverseConflict); + addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict); } } return sortedValidMods; @@ -235,7 +254,7 @@ void CModHandler::loadMods(bool onlyEssential) coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); } -std::vector CModHandler::getAllMods() +std::vector CModHandler::getAllMods() const { std::vector modlist; modlist.reserve(allMods.size()); @@ -244,11 +263,16 @@ std::vector CModHandler::getAllMods() return modlist; } -std::vector CModHandler::getActiveMods() +std::vector CModHandler::getActiveMods() const { return activeMods; } +std::string CModHandler::getModLoadErrors() const +{ + return modLoadErrors->toString(); +} + const CModInfo & CModHandler::getModInfo(const TModID & modId) const { return allMods.at(modId); @@ -320,7 +344,7 @@ void CModHandler::loadModFilesystems() } } -TModID CModHandler::findResourceOrigin(const ResourcePath & name) +TModID CModHandler::findResourceOrigin(const ResourcePath & name) const { for(const auto & modID : boost::adaptors::reverse(activeMods)) { diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 2842d90aa..4028ce6c2 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -21,6 +21,7 @@ class CIdentifierStorage; class CContentHandler; struct ModVerificationInfo; class ResourcePath; +class MetaString; using TModID = std::string; @@ -29,6 +30,7 @@ class DLL_LINKAGE CModHandler final : boost::noncopyable std::map allMods; std::vector activeMods;//active mods, in order in which they were loaded std::unique_ptr coreMod; + mutable std::unique_ptr modLoadErrors; bool hasCircularDependency(const TModID & mod, std::set currentList = std::set()) const; @@ -60,15 +62,18 @@ public: void loadModFilesystems(); /// returns ID of mod that provides selected file resource - TModID findResourceOrigin(const ResourcePath & name); + TModID findResourceOrigin(const ResourcePath & name) const; std::string getModLanguage(const TModID & modId) const; std::set getModDependencies(const TModID & modId, bool & isModFound) const; /// returns list of all (active) mods - std::vector getAllMods(); - std::vector getActiveMods(); + std::vector getAllMods() const; + std::vector getActiveMods() const; + + /// Returns human-readable string that describes erros encounter during mod loading, such as missing dependencies + std::string getModLoadErrors() const; const CModInfo & getModInfo(const TModID & modId) const; From 2cfbcd067b4602835f3af2e448ec605bdf37c263 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 16 Nov 2023 02:58:43 +0100 Subject: [PATCH 1140/1248] videos --- Mods/vcmi/Video/tutorial/AbortSpell.webm | Bin 0 -> 86022 bytes Mods/vcmi/Video/tutorial/BattleDirection.webm | Bin 0 -> 299243 bytes .../Video/tutorial/BattleDirectionAbort.webm | Bin 0 -> 163353 bytes Mods/vcmi/Video/tutorial/MapPanning.webm | Bin 0 -> 122433 bytes Mods/vcmi/Video/tutorial/MapZooming.webm | Bin 0 -> 115248 bytes Mods/vcmi/Video/tutorial/RightClick.webm | Bin 0 -> 67800 bytes client/windows/CTutorialWindow.cpp | 29 ++++++++++++++++++ client/windows/CTutorialWindow.h | 6 ++++ lib/filesystem/ResourcePath.cpp | 1 + lib/filesystem/ResourcePath.h | 2 +- 10 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 Mods/vcmi/Video/tutorial/AbortSpell.webm create mode 100644 Mods/vcmi/Video/tutorial/BattleDirection.webm create mode 100644 Mods/vcmi/Video/tutorial/BattleDirectionAbort.webm create mode 100644 Mods/vcmi/Video/tutorial/MapPanning.webm create mode 100644 Mods/vcmi/Video/tutorial/MapZooming.webm create mode 100644 Mods/vcmi/Video/tutorial/RightClick.webm diff --git a/Mods/vcmi/Video/tutorial/AbortSpell.webm b/Mods/vcmi/Video/tutorial/AbortSpell.webm new file mode 100644 index 0000000000000000000000000000000000000000..6aa7e8b9bec7d624dc7144c21cff2c73d1de3867 GIT binary patch literal 86022 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
  3. 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 YLbGmVfXxrD@A=7uza$an_cMwA2OOx(F#rGn literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Video/tutorial/BattleDirection.webm b/Mods/vcmi/Video/tutorial/BattleDirection.webm new file mode 100644 index 0000000000000000000000000000000000000000..67b3407faa65a76968a608fc9f33b1deb84dc933 GIT binary patch literal 299243 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

    m?hq@}E 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�(AAAG0->(3u6>rOqpj6uAH4*9PZI;8ooSMU0j5t^Bgmu-&vk=k3@gHsT-8 z+D=oAnd&hj1`8x^Qcd2e<-;e-cO)i#psFZ$>V9~tHs$hOzBC`QJO*jbUwh-sR5e6f zs34XN=0c|k(-YG|Lazo*G#r`6tdZ>sF)Dd*MdlDB-B!_u21k7Tj9ju?ac_RBu^CF$^Pw z&}z$4)>q?zbo6ZFlN|x6CLJl>!QSICDNog2LwZ3oAOUJurkWOxOq(PuA;xn#$DtIU zI^v5AOkxayZHa3Q?e8^ZsO^aPEr(j3)yR%qyD-E5QL!3q%rR4AE&cH0)+sNa7hFGJ z+>REkzr1VTRQoGgD{mC=z8XD#^6cOP7FD5cRH-JpeK54OU`_rLu>2|f-a>T?U}(5f zh!P|%F~H#~R0}(WtZ-?O0N1%#B2^8wWq9cmH zyC+KFYq0?1N{w}ix#YW#iC{vy?RtOxhSaFwOB8!y*yf(zG#5zG3+WPxK~8&-rje-P z3;D#<9;F2N5#BxcEwKL3FqirHZONaH?3z2t^9CIpHqC01lI^*zfBsBddn)V^Kk4|W zu+Vi|-mEZKJ8{b9xo`DuN21LnjH}&EW#465RYsZU2q>G+Fhdy0ecRvp4+B<^@u_#+1`Gkw_1Sf%! z22f&?f#kq*SlzK_mrE5I5`7_T?euV22=7Ul#e?67fsD!PX(`e2= zKctOSm@x0cKy}tQ(pR5D+#4b%bcH1`A+e5utUeq;PrVP*qz~Pjgz9W(knvagX>)t4 zk+Fal78*a=V%(-lBvq(zXr)%tM5HJbuZOl|fs=TPE#iy9*|-Q3gf>7ErM++mTfh(= zWueVzg@$VKH9mtYCE>Ada45IXW*Nu{c)J6Np=GS6y1~ls0q3+>=Bh6quiU<7^u09; zUmCt&nQ(Z*b31C3**9w~(l}TW*J+o!*xx9zLf1IvFTOz^4l%q4V<@6h)JIB?)aFS^ zVm9@F8104dT(5B`&rq3goDUnv3b@bz(lf$lYrRYQh1^@ZM^@Z97VG#R8mBK{x*wRy z@js|dzunENZS$vVXS<*2fF&K9c=j?sx?gG(-=w;R1%lP`)ZhV#@>-X|@;7dmg``zJjw=;T3?ju;FEc2h^oMs2OpK_%-r+bL zQWNv2g_$5eu^9XBZVkAE{A?ue87YTmvIKV^k7O~YN^=NyeXO@U8sy$ zTZ`Eu>hEQQTnw09fseu9DMPmU63apU0tKb_+o?WI%5S?fjn5gDY-gXOEMoT2ib<5+ zSNvh#42S#9+lt>E=6`$pLu}Kdm;|p~ED$0cf^dv3jvR(D z`${2^pv@1{6Z09oT6mWLAtLFdfEoz|wNFAZVq>IK#3=Q^fY=JSa4EqtW})#oJR+2W zzCf({C&BJJFOe(6AyB}3<+DTpm?T9|Kf1+N!2Geu;W=63@NyZvCOIS&I@6OmeN!t5kY_ zvwzjwdaX^L8@yh|Sdr^~B&niE;{=FehbY7OLfo2Lh$Jc`&Y0lkX_L zApayX0TyneGhcyK)t8M>8@5%NDX7O_BP#e%@yqHrJ9~32fq?Sw<>)q3 zbY8EJ=VHg9^dMF7fMe@W^CZKN-=C6$H_^{?W=^yBmn~S%T@gcr1w#ACBHXA zNby{jbEwLK&xnzJxYoc^Z05Gf9mRNF5~bYhV+zlD={iRjT3~Gakc9)5V3SSIsM$pc zB^H;D$DaGLcKn6tdnZ%#)@<~ZxmnXSy0t&gq#-ce(EByz7jw;?@yiycZ)x&bQ$3{T z%#^bELoL=d&4jl{NUz4EZ|g6PpB*HaTkl79Wr?LyNH{1m!D+*x0YXwh(Y(PTnhk`q z!J!9K3_`mH=1mHSdW`zZNpS!*K`vs3BAbB=35=U9saIkJ0fHUyZHoZKKCRUa?FTgA zM)evCCbmg6S3~B?$)C}W7me~>n*#WBtRu64g)^02u^ zxyo(&F|J40&Gv;h$*(?E+;JMzHe=A94~ItmUUID5sOFB{>L(S`_xmQu7#k*5FM4hm z_4cy-vT5Jyzj`Yc7Cw3VpyKPsNO{pGY}MO>tIN}fyiX%-|vFiwUJ*1BDt8Mwk)Htom$A!|&V z823-OjeK74rgI6%M?h9YTaF0Q0372u`f)5HQ5lW#Cad(-KYDooMDh~kL1A1$-nkUk z{@R|Zk-4moCv#uzZlz&Myrw$vB}xPqM;sK10|}{@!cBK9`!mmQl_nn9)3qylhj(k|41N*M&WWg9t6;s(W%(Ku}ehWy5&cr&H zyFv7waG|AYkore_0TaR2yG=43B|ufY`!T!)R@&j<%j{zT17YdN8%LJAUXS~{d(ery z)!`2AzA~j~>(Z#BZW#!Rq{g>Dp2lXu#z~=jMz1kjL3?%I+VQQ$21s#+OC7fYK|a9u zg|t2A)D+e=!Cs!@n<;COU20yHOnF1s+)$cQi>YO1v-=&wiMs(9?d^6v4_?s;*ua$D zFdHygXPlJuJ?Iq+IoRH}6N=#BdLnS8U9GxEEBA*6%i-jrA(5*+7FtrP2sP`(_&xi& zm^R;h4M(LPkL~GSXfl8Cz{ZddcM6A5moHtj7{+zGZM_G{h9AM(9_3FMBS;_1ogBFh z&X=axx6R7deUPK}W5vDN4`;T#@k?Ah)SMna^-JQUi$(`acq$_|$!~PHF?@U$(q^gI zcSf=)uP%mbe8_9x`*L0d=ZVb6t`?qeL>eSL2~-yqUccjr0%IzeN^A2RzK|~p#^WMP z915p7`p3uuYaR|GjByLs^Vh(vzuQ1#Ti{W`DGTk=5vGQ{8<jxRX(D>YT4|K$o6bl z-nFEUvA*twwf#fV)KunW(`w}o;x>ujOf}9Q&%=t9nFtMPoZxw6=B3SrKa4-8#~{Tu zY6vz*{QybpY#jffrf}W~mKI*K6BRJ|b6=^yWckJ8EG+v?(s8``q!TK8p0bX0aooh| z1%KW&FRJNAl-vWOCgecL8WH>bo_&={0W_=CFeLV>bB8-b6NZW?4elr5;_KUiG-knp zfZ>gTQ0hp{sCa7FvJscCFy&?)B$M4VG)@-z4;r|?;qX+~p?STNiFPO%Oq6W3?Yfar zFQvgMilD|o8N953WG*F7)vsYeDzDXnm1{c9d_)c1vg*j8IlyXyKVOd`ds8O7vfQxr z#&+0*hbpnkT%29-$ZEAFm3EKJ;QcZlMaByCDuPQRN+=c0+iV@3 zyD1nprKDrm)p)6_k5uH>fc`$T=}jtNYm)%k~ym=`Nh!&tCBVA4x$U1 zS|;+hJb#tD%;zt>ev*>$)ZL2lyE=yGIe^Z_>0|Bn=KAgU!Og2AHMa(2VyFv{{n?>(>j?_WA3BgH(h(oG6PT-1S1|X@pH1+Q|o}SCM_EB5!^A?L%q%<&JM36{gREGH6MDA!rpm2s{iJ zR$MA(;X=G4%UzniK}w}J3bsIp`&XsIYM>G-LuEvrEGd1(lOo~;6Nx)j2qSEXPBS4B z-CwBJp##B1m89%H;tE{?ThX26U{qP`U#MY_Uue;)HNA*y6_=O~w2^MbX9{-Ouw6Nxvi z+{JhB?K~&OMF^%a5EtQ+@ZTe-2nvMfKujnr@+I0xxmH!ub__~UkrIt$NfAOc^lZ9f_~aQuiZ2qSH<8kIS`qp{3GtdzWL_n*|AhGAoBC(r+vXAHQDr)1Slc zMi)Qd%vE@N^(gb5ZM4eQ>5Z1%-6!KYo$UwK1so&3C=0EUr|8l|$jH-GS7nJg|6OGW zm0p#OJO?X=;DKy?I8yX|nfLT0PIBQOG!Y$XZtgsHzKBa~_p8sLmJ{3kiq_bfUK}aN zSmIF3d!8z?;zn{Jf8be4ajVN|@i)H}<=iPMZE{UB8a(eOo?E z(RrP|Q19uCo&6`|}=}Td)!m(@W%-sBQ{|^s#qD4l4Re;scl#-iwqF#9ra) zP;#jO!0S_&0$A`F$TYDoeXbY7iB+U{>=prk5$g0UL(G*C>UeW zN*f%>RSCST5TbcycJm77UlTXHxU~4bJbUwxd!;|#=EZNnqv_w3a0&%6&c7Z){oeX@ zZ|uajv5(0T9LtLdEE;5_HT+NS8y4*MwsZHCy#sytdS2kCttST7q1-p6#Lbj`Ie*5uJz$(xm)__{D*_1*5GDUhr2>Q|9aJU|KM8dgd@?E zF;$}@xu&0W)|AJ`Z@%epyV*@f;WB&v$HBAv$j)6|{0zE`~d~6}>?{$}C z2c&Q1hV%aoVC0HY^d2dzSAGRH8!>xNw#ELJ3O5iQRe?JM%eUJlf zNIQ9D%EX&?G-pHj~D-KMWO%s9X`r)lHjVK0;CbG`kAqTGoh>y5c3F zNBfU zH8S7Wb@|$)6}M^A5k&zDd~k!(^0J`z`wirDV?pEY=S|Q)FnlS?R60)Wt4gIkrpP@Z zs<@KeEie!#qxP|oGFbyOn<@Ly)vf#3QZ6^>{RN5xStufPbGtymsLvd+s>UyG^2dvM zXtHb1t?H-48)qB7o{Ja=7XApf;8=m=xCZ@<&5&}-kXUqjS^uzRT-NSqf|XK~Y~7@_ z4kvfSYQcva`h$8k(e|o%qPgSyX3~FJ>=}o_5FKuUntY7~xkiDjARQ$py7_psh5E6m z4(}}0IfXTC-m!E}+A~JG8s$N%U+R^I>7$z8x_Mt*cqrrzoz901)~vc@=*uBKpDrqF&Q*aQmIuS{Iz)da=3TtGV~ZY_%rR|Q5oW)R!v5*M6-BgM&`)=UB?!9@^I_hCeI-=$DStqWigJjUvg zL_mVfJJvpWHli*OF~lMDZTqM*>EE;Yw`wPUPfajjs*F|UcDlknNuZp>; zn!3d9lrnO(?&ae_$tDzeF4`PwE2<-0Izi{aQdUdbVI@oRRQLI|<(3n6-Prl?x8Ea; z#*n+_Ms=5V%zH|mM6VroQ!mHMVeX)%x_;w*o}E=t)x5U8#;aR_=uKOTd^$V$mdB9( zU4$Rs2gVF!A$M*a2%u<40R|iti6dlQcwdT5q8(Y_^hQazQ@8?Lm+)<*pwpKv8h{aV z=~Lmu0*le2!S=STUT(T}0n&3B@U0|#Boj;v7*Qc$*#yFK8Oxz|61#p0)27uTsX-g< z^RkS%5-msBIQ2s9Ks)c?fjONq&#GsQKDK&+=26Aw$pwa=iuN#?`?F(gb$+QeyuGsH z&qGVi59=!jeTb@@{&+#Qe}p_bc|aQv-R%p>1!dm@ZuXy6f{4jHycQ=yDd>>GMW_xm zBH$5GmxB$`Ef#SJ535%S2?gMMVW-&O`5%VYqYg^6;Sv@Vh#ESQ`BzT?-ONR14HR4B zovr~=C&-L)!F58V7($fCYUR-6P#v+7M(|prrAC^%?z1cJ3VPbK52OUf@NZ1`FurTr zE*bSA%@J&xrcU6mo!f~*1RVqTNr31A)#f)z7zCyOr55kSlIlQ+r~IKtdH=muL2{a9 zRTVs_sNw*hn!1gRfo+CiKWJ$hC7Lkf%7I3S0&5t-*7gFP3!6jJAts6$YVG9A+py_H z`jLAlcqLqmIiXU2_v@Y>g`s$GrSe;vRni58fcskq^3Is;t?2xvI4H}5_O*zn#rM8? z@yUDDxvPHfqKsSmV6I}jyw|~(Nqc9{8>2UYD{vBtmPr6(AcD{f>;()iqPhl~6(E_o zUVI>wS$#Pq9sd^>iVDrI7==8+=z=y~O2f|09hZ=FElApFVArg0BwG)J2)^uOA@s}_ z?ZeTxtg*SPO~83QLkOXzq>O;s4tu#ydh@_}W$!sIEoCE>w6jpH;M~vxcqD4x*%e`s z)^t96$4tN{jy#t_4sEDpE?(Pn*r~ihFJ#ww@Sm)mn7&Z?V(@^IW?RPWNLtplHDz+} z$Y+lxR0Vb64s%`Pt>l_hjr~n>o9&lEY5Q{NR$=ItcmC!{OW%*SDF6zA@Qi%_G@U~!Ipa-Y*Wf64Lp$A+r6)yM~)u8rQI2z&l$`S5I z0G7kKJU@Vs5VYDr8W0^{IFO(`N1M_zL>;@sEeB5(-|b@6?%Me0k8Nb|TZ@>1Y0@1qFkMkUxuz zr}&^cA>!`Xn8*Ovd8J~W5cw}d{9kPx8^pmf{|X^*)P-gyA9+NxNgA^gvWbA_qQIefBOA#G zjk%|+Ry74PX`XslWAL{*s|LoZwY+Xm^_zd1XSg)hrm|m(PPR;xajg54<8!xZQ_iJ7 z-}2a^TwA78o2(3Agvkeg2p9LmiCYLE6dX-!5%Hhl@fe|~8bTr=$lQRcJ&7+Ake4?F zNngO4#2hMLjEWHkVVA>M0-6GmB|wEm2I2$HWgv$ZM0^-QRvi{{!Y7@E-TatFJ#sE= zd-H{?1V#FTk^i7i(az=MqXS}zi7dva?ZCyA$Hm8=Wcuw zwzF`zc9SjC3xwb}e$^B2KOgJXx^$Mj1BhoU!V`5BCsa2(&`A_9Z& zHL9{;l{L6U!pbKS5urHQ1Wfu{tqn{Zrs|kTrlAM!&Jpm5QSBVhRo@KwqAE-9#i{tu z2I4vdDa!;)0-+dqF4H-*#VD5emb@O3O09!m4KJk|tE4c0=Pe8#dL`B0+-}IBSQWLk zoP#TF&)Q|zQ1{8=-l;D$4~%v4OPto+p0JVYZWU_3WuwP$=AWKDrt|JkUlsAK(`i7- z%a?MW>4nGE4m*)`EA_(rmntC;37Fv%-yE8;-T>`JUl_R$8jV%+?fWaz-NK7vZmmoN z4N?1^g;v}1nO8&2P&gfjnj{2ai6E&l;8QXjPVz-EVYrG@=BGyeCm(DF?AvAnTva9^ z3Wrwz0YvjG7q$wpBsc)dk4oW~gpibrK?+bugAYVGg+(0~K8lSVD5fkC@Zm^&?}nC0 z>}eo;9|4(P($f3W&5t=(P~Lha?%tE>xhrF36$f82)XO@OpSWquiyNFjXKW{)>l*$G zKXP~NH;-RO7Ek?n`~#bLi4vg|=-iF`$a+a6AXaP$3p6R_l@K#&C)+S*2~I|V_{JNf z>1qQ>5C^@PX2a9$aTp2ycp=9GoP{6`@l*sreHJc8ltu`N80_4l#(i0O^xMjKktGs~ zkdxY9{az5pOB9gM0w~%Y0B_;gyfPMY2>3qKd8a@H`!#ptND|d#OPvI$MrAP?!J;ix zL*bLn?BSv5eAp_INhtALXK-j6i5+R_#Ht;%kd+qwa=@-jd&U;evCO&@7RC8OuRq5Q zcYQSKBm3Z^Pa|u06sMqJ?8O)MDPA!5GW0*cdAL!n@zD#h;opzJ>y%3C*Bwhc?fZeyzod9+5mY+Wk|{O8Js8cQ)q#nye%dgMM3g(lIMHS3?IKJVRv9S_k)4G` zjE4-Oh>JITv}Fnm1_i@LG5J8F$~yHDV#C?@0OcU_7du}&9GS?ikf(7jPV zx>P8ZNr`4bwH_2a{6#EyU>~~vdkRsZCs&rz%jls&>!$_b+LNqT*v3e- zv{|HC#p*aQv9>m(Yc z2Wc(3IUSbJ?fZHbqS&4l6hzm9x1NVzpMAZFP-Pj7a}6)s@(b$@n-MEw~xZH+;ILiOX1ZHHHfzTIEaeE-z7v)|Ti+^}xx zW?+~su^4i^a#v78@o}SG=iNfsqi484>#w8XnZ>CBrmEM5 zeOxmA`zXzAqOge(HJKtlC5(^DcJ`5zWW4+Coql(f#PwIK!%%a_#e^8Kdn^kt_wQzQ z)e2ZNUzzc)yjB>D1rtuxZc#)y&I?|zX1&7cmCj2q+eO+h?`pNC287)ee?gUKTvl*s zrNqnT0_R2>4SJqjN;kT{_dZW{cIzwVniqqAFI{i3Rc^^A*_(S-Rm_+cJ2Q6CrrVY! zQI{G|WoW0JDSf*5@``f3qTN>l6<1%ZSXuaCueIMO6XkFF4T5jp9y;glJ@)yvDTh}g z$OBtJk9>{M278J%cvx$UX&Y|&% zw+u;t3DZCot`j%DdY7p(+Bx>z;CTZdY-}s3n|x{BEFa^;m0_CPsT*|_n&+!gyBO+Oak_^{|-yBF+am)!@ECqBNDeAD0ERPKzAyQvj zeF~x?1d5pe=@2nsRJR%)wDXPIJ(wA&5Aj6Qr;NR&q<8>JAOG*(lCGjmKRy)3X33?5 z*lD9;*+M_TH3Tdw6G4Rj1Bhgo-;ToshBv}axQeulH?s`#Lyy2l%Pl$aK@NKQ&KHWoUf!m7i&{ z{VToWw_@e)b#uC6N)Dc){eHVFZ@KcsBoP6O#-#)b8S|~BBonHo2q~28D@oBJ^qdI!mrxP%TxYY%zesxd zn#84Na{o<>;2mm08sonFE$xT%doy`1o*Y$5I`sxaN-TFukh;k@=1+sTycc!Ufp_fvL5V80rqP~yGU0m#G1O{ zDhaAASS<&I`PnL5A{@Wq810%axV?`f`OvG3kP?BC+*s(1!?MzpJ(4rTnDQhuxU3!0 zVJn<&lyJxUn<3dJGbU@^v)eP^)tV$e*=mZ%*kZ?rBc+pR6MV&IAl^7|Kfjh5_4nhIl$$+SNP}jsj zTFd&b51HO#mH+|sLyYMm0~jKc>K18DyceuAW{%sJCV?8P?>>iq0uF{086J=BCuBqG z4!x0fsc~*Q@M&T|+91{d1bPgF7uYve)a9(ca|Osy7WC;Et%z=9I^3}yF}O5e+3adm zGXk0_YVA$Kq!uyth|8LD8Z4e=7HHrwJ2d&tU*pN`y&Uz_GZkJLUtuOXddG5&F9 z#{Q6%%clBs4LV-vSGxI}-J($MK7QM^?$l~l?Dw@fDMi=5a%UlwAw8nr+sY9G5Q|V$xymdlRUwaT z2PEtaz6h5c#1f)Zwy@~56AT6}WJ?0@GclTkl)@;XQUrmT;7m9g5Gd70flq`|K=LJY zlmwN=h?m7=)CB9t1RoF|W?rp5Y-Y`QI`~bpK%R2Ag#apk&OIq6O? z$Y9_MQds#W81OS@p*XWNE*f(#>2gu^|1tIza8W$(|Fd^o&__uqZ~_ua3!-8y2#8pO zfqfz(Vqt?Q?qMMcf(hm+*oBRKDk?S@$VXAn#zsWNg1E|zlqr3yl?WxqF!Exhj&>111s3Uyv-CZ9O&^L;YS+L}c6M5s1a-)8k^*MM z;NS!|kL&~~;uJ#q*pmtzE-nbkkC3QD1*f*HPyuP$#Fn3Slmf~RI4tJ);*SaQ&G@(m z3T;y>Xcaf4!+-(W0lz$6ZC?9jjO|U{$4%CA#{b~{>iqnXNnQef&DD^w`)~U8X#Lu5 z%0TH9_=4dfzrW|p-&SmVKgle1aqQ7&!oIiJ{pP~8G<2`)-%+FpharhMsM3LOO$zA* zKf50mY04aSX5VYm>HKlR*=YM_E94U-frf{VO5}QEr~^sQ%bG8GP>2lohrH}bF&ZLU z;X--VLIE_kE!81&k(%VQp0Y{bSwfgX8-=G+6e{}&5R5>C{wz=_Hh@@dGN7*%REJA})3kv{Xg zzufCT=w0HTOHaHzytF!8I^{%Nl1)cR#%?e6k|Nf9Jf3i-M)sH)IH z`?;4u$pqR=pdR`0I6p_0YUO%#mLY?B5(NfQ(7qTdejO`uM$D3SVNl7$+slw?eou}F ztGKK1CMTSJoa#2!3deF=r?3IgwAhluIP1SBV%ILq7mmwATi%Y@BC-yXX7grz9-k_< zqdM{oigKILzD?a9uq>e(?k-|Y?dRRUDy}_>5zI1)FZe!)IA!jZ*~1EyTA^BVsH7o9 zvN*gJ+g5_E&Kf%P)Mt2r{tfym$eLmwDr&09V$|3iL#p_HaqHScQRqgMvTa_6AHOPM zyX@&N)XHU(M!O)(WAe81(@$~e*e8F_9*Jj1z4nn?kG&nHv%tp$IpTHUFZ{0{sAw6;BwoQiE#w(%9>$QxgI zXy_GZ0`*U)i@Ci}l_$Kl%QO^Nk62^Yh0bKhoB;BNUZ1 z*H#oN+wn|R0uoYi{hrGDKdY^i0WwUD39!a49t)um^SibTrT(HY@=bRFuZ(5qq z)nC|qug8(LhCi&v5144U)=(EBbNQhfh;Rw0CO+=m%BeLlgaBXLMYMr~TyioK0i$-Y zn6jMGdagllxjV4*DTe3%e%WzF?skrh5f$dr$x_ty>NCCvr9fy~tN1YdND~cAF8Doy z6qZwxAV3CfAEl6K8-}-t?cprkBS#5iR;wHh7jnV25>+mtRQW;SUKTW05X9yuPwrm* z*H$V=u?4M=mS=oVy=5XbP{mqoqf~;Dq zM8@_;8)P&$h>=yM_Be@3P;soQ?K{f0AYv$`!WfQNo^r;ZAb0+gh8^g^98@;gc#}A103M#mmkdii1lEr~+HPmiHNvT%U zaVuuSt6agpQVPp_PN(cBRGn%qg+(Vu0f`RukdM~|8<8`GxKtw)rUo3voH3}(KpsDq z*{xRRy76v~y52S{DWuFQpPLn(rCIaof1Sw2i4`-QkvJ!u&R*KOcE)MXG4a zlY6`;`|?l5*A%Wzgdi=aw+<)A2pWe@6M3o<4N2%?anmK%;b!~fd5cyWh<=2fJFack zcUD^04nB_uJruPtr(E5$hC+-3F|}g|g^1Hq_|{!^CI;2WBu-7xWQp-t_EVY=mJDV- zRtIgO(h;$JO)m5Rfm2)TWS45i!_Wos3&K_I_1h79WS=gQTK!h1KGuq7Ib#>FK%zP6 zNpz5^yr?+N`v8ej!XI^mxz(~rZ^}mrLL&+Hi~DC_tEnu3+UrM`{wvmrv(2m3+tU1i z0bbSSn>lCLke+P9QS)9CBW-#cntnPcj>~3xoAB}30SIn2KdTA@ZEW(S${opo798?8C zkZ;G53JI}A#G8wzrMv*jXt<68Rjb-l#?I{$SqP2Rv&jd-k%9B4=?OP>E4@J_^fWOz z9*tcP^^-Chl1HK7@g$5}FtLTS1zlb!cPjeroN~G-RDyh#kM)xx{jZVGMBm+ROyS(a zW>$2GT{ah>K@7(Ak`bx$3UdND4b5Bb>vpbLIMRwLQs@Y)Z8^Bpt>GFg^8*%$;U*)P z=VZ<*S%y0Pt#f#_vf292QiESBUbRhVIojpUFS7;nEy__Fi;V>%0-?4mAY&Fm$>4e~()MUrO%jnlsorr&P9*FX$NfzHdJhHS>Np9 z(F4O=cG}$9HmF{vmvC%r(E!`qPddSSAks%FpQ+!j_4t0lv#rG;hC97m*6d#v2U$s- zIyggJ^indYnQFR8ZQfDcC9;}S8zteLp9y2KfDQUm7*|5|540Bk;bcC{A>wk5rz7Rm z#c*v!?`p*RpzD9M(vC_^j{X!|=1Lh}zOZ#c`bThtKfCgpwiRA9Y&?Z(xS)LX_#!E4 z-Kqql&Qzs0lEC4w-w=WWRk2Js=)x2wL2OdxO2Op^JrHwtp$H3kBh!3xi!?O%Nn%;} z)^`(~^4Gqsw|HRtg3~^}+ma98e;jI)SewYM)3Q2Yd*RNn#XH#1L(bjRo;8PZb?x(? zzh!9R-|E$MHImIQdlhvj&y-$W?ba@leWFKP*yfqCc+Sw7_XavR7k{tF5gvMYuH)~X zZQM_tmU^7%JN$5mw9tqg@vE;^U9Y{#v$s7LnSN;R#qVY7g~mMsum4`Sx%aum``4W8 zyC&PnYEH%1|H*9Kv+Luf=d%)=xi*;-ZiQYiJg+A@1^d1UF7%fUv4Z8W z_H8LFhs?)ftzeRUZJ^i)irsq$wi-o@ye3!%VZwq`-P z1ltXWEfE?}8(SUpgeF5s>mZ)0_^dRZDf;HRcKC+z6tcEYfFSicJxDkWzT8LF*?dZk zP0dUdO=k7W8h4mVSi(+y0pF8CTne`%^_3$fB(j_W*ScM)BHmbKP7VnPltOve@h&n|XiJKT={Y@COWyU=7K8kdb!w`e<(&E*P4tuWcS5(TSf*M(1lj?R_` z$S1R{1o|MvmQ7R;rRD(FNv^Xpl?bk6tBYg5*4rHh&Kur z+9Za4%=s&!TRJTPGpZ7hjDid0Q;i7Yj9y#jL3C9^9>qghR4i0Xlz}rn0P+K7i#aEl zFKpvYiSY4)0%zf=Q>XZWRGveNcV~Bv=e#v~+o$S6?VNGTJ?&Gn2*Jve3X;nh$D zNFWyF^uHpn9m(`YiR~#j6B+mH!xIvW4OttC@wOXG>WybLea#P4FUZPnrvzy`P|#5H zhUNlB^Gy+#YzoEQSi4jApFn0G)(jj}7lA={MX_t2d-~zf)7=3pN;?BCwumrtkYE@n zW4M&I8yZdR;bo$Kub6WU<={zqyp~}Z>Xcc5zhp4lN2h1%?sc87w*I8w=G;e*;O?dW z4K`mmkL7Bdfqs@QE z`VhZngnPK+vyCC}fmr#Dx`ts>$dEoPA8$$_P9ota%HaPVK)8NWBi!TpC+TxRg(poh zK}`>rOySsrop0H+#TZtqDGM4<lgNi-$lqzHpR}|8>HHD#h@5*pwwfW(q@r>5exaGA2EmQP$ zy!jX~hv|7WZ(n{vJzDc-@1^C=oMj}mU+i2DoMwpd#ilA*u%skyus?~fMguieH2R`c!+t&tRH(mTvyCKFN2P%g1S2`j)~v@ ztps8O_QonuWDsT~LGANhi(y4C98i6whN&%Jd8@Sudb^e2ZDTg=Uhn7lCVic*m(gcjJD>2{xsp2* zJ(GX_t=Y*xlzM4)?k3K&9QSuszkiM_z1Dm5uS-Yv^nYS+BOs8BeX=sk`*~Zpx{Foj z1+5o{?bmnlUw-&Z^{1EX+?QsLIXLU_+-8@?4Zh;^rIpK&=Eti2zf4MBC^cU=ZN>h~ z!mMi_gGYCX8?}q`NY}9y`$q7rn|Z;1B+Z($1V2p+&jdgIxla)CK>YF4kFRYU3=V!6 z{lI#V<}?XXLBO|@L1urUuFz=@vAl^wNVo)s(!(VYz}h*pr%z|Q$ekrEL`%d> zH;7e&LP)s2;yLx_?QjV`vgQcR!9&etX5k#FjH!7SJ}xcB=}KpieL)t|bx?_&fwQa? za{-f5J&nY)mMuiCLVN`ZjDk^6XLJ-D5U2Ac05_)IbVJk#$D7^MNb7Y-S_~CDND7z< zi3^pIn8VA+js(O9{`oI5@m7w@)Wkr14i~0~!djV9I-U?q*a<|61qI`LI_rX@Bb<5A zK!_YOjziziaj6vYE+)9j4y`9)-F8nPB9^N^<+d(H`&5)*RG5dGh1JW*!|1 zgoyPjCwskx&YV|K&weGG{+vJl%IdKTMM9)w$hSEv-qCK}d)CIdTUJx{_oRrKw<#E; zR_jH{*p*s|!?ccBXg-yzxioURzjwB5%G4@OLCwyIk^CIavxMaA0LVxPf1R zO!s%Q5U}cCMEIUPdhys@l|`t<34@a42Nw+~$gOM(cvRXqG6ha4I^#^&(RKU>}ZoV{zZ^V6U16ubcj5b~H|0?Jx#}@1|O(I1|lU4%`#-Ha?^?(8N}P=PfoA+9s7uiF%Jrzbb3Ej zsRRX8M!DQI8`7nPMuh}r;6Eu!8fsP0u08Ci9*)t`>(8I8|Q_eMz6aFC$( zO3dQfhVd}9A#ztK_|Rg^8Y6@)nOqf!IcP}O54jUEC?r_C?~F$xK(SE8wg&rABxf`1 zx#{&~_Dnd&yngi))WzI?-%v?b!HtbI{et@3|MEH+5 ze$W)%&AjqFQL6XirG4{-XmZHPr3;WF=}0mqEt)xeEc@s_3bHzx>OZ-o2tC3ahA|xDJegoxfphf9#9UO ziX6uknoeEU;n0o(vr)P>*Iz98RDSAxrfrWIQ8n4q^5gy+m$&JYBxG1lQvI9KiZ+gy zuD4F?>+oRu=4DG{Yf|pS3T~8Yb-P}g(xV`x&~)~+LpJ60Rm-GMn7hNIF}&76`RYat zn}pplJMJof8FWqIPVw8mJxOr7p#Tm!hkB|!!EJ!Bo34*gZX^wrtE}3QOqWA3&n3qp zMZJi-=$`d;awf9dAVo&e6ja~}!fl-q2`i)u0NeQi=3?$heBT=148AuEpPOkTDl|Pu z7Oeg>>Ucko`|;M5V*ZkNpNZ`4JC4NIj_8#uav$MMz2V+=37dBHznAT9_H13RfAf<= zq<>-BB<+lkqjnChub+P8P5BI5gZ6@-r2abKp{K1Pc2ML`yvXOX`JxIWDuK5rVJuP! z*HhO%iJf6uBEf3rTa37v>-#0atkQc?Cfv-^p29lZj{?JxGz@~@Y0HmSZLstWvdosS z9t7nSiL~CcuY|KR?1wC*y)-cBj->*_*N1SYFclO&?_p8o%#oVryzBnqbf5A*?_2n` zT8RuketVw5?G$i9=6ZN*Ub|m**>$OTBU4Www{WtXlzG$RX6uwzQEf%y%geoIQBdP} zsGmI^Mk!uJ+lo`<8XzsS(`k$;5w=vxYB0@q^on*ALdOHgih^vG&BtT)Bx@<_tO5f& zKfqMXO~dE*75gdNZb~e@AD&uXsXLjYA2Fba>XY1afA6HTF|F2A>iBgWlGe@9a#^)+ z!tg<5!d)cWWAw!T*w>a755Z~PLw&Ujs#?UAjy==bw?~EA8&MVroW?C- z0=&`uk{+e*2M1l*VLQWHV0S=MO}m96g&i4oPeiw&s?!2LG~w4+LPRqAR7(`p^u+wR~utY?njfFrCid9!_g*vNb|DsXXpv-eR4k z-)A=r>*1&EY&^wp#iO1dseTX6bg4NU7dWux7U}0;8~q{s>nxt0az=W%BcE5&4H38>OAbr!W*CiI!hKA2^LgCBs zE>pOIgTlF+@VRZ(ApjPW$x1R0Wq;hwhQ<%K0GOoVjBPRx z+k+UPfyr4b1rLc0Pz{lIH#LY-A@La_0>q|=kwHu%vEb~$K64NW1J%U}2Y9LJXJ7r3 z&CVdAes7s|B~s?`LQVy5 z79i$dAhc|WTvs_}KP+g~@6UMu=!e~8lrDjzw>QRu+L36cM8nNn6~)S6H=Sid8CEj=}Ia>^4a z{h6xal{-kr0c#3HZxXr6TtTp9F9j{HkS)}fm>kHMWwGJ*cvxF&wzj>!fEs2mUU1{_ z5cW-1!drxgg4?MU>Clgm%AW*Lqex>w_!6YsQVi7s{5K@lcL_j=Go@@2?K~P+Fx2CV z#HGQ?EBH)l=nu`28j@BAMa)WVBP{?tu~rZzSL6qX#oWWl2>PX_^o9yg_x8;nzV6>< zVA#yQb=TH;Pucc%!frv*q778rTw*h6LMX*0`j+R7qRfo!REzNS5 zkxG9?+oqveA%&;;yW5Xx^(wz5iCU};38YkB5ZV{YNXkkOqTPR0iMkUdpHF@fb1yOr z0rW{9iDN4Z!SF{XAD-oInq`~T&$0QLao?}+&S~v1SU)DfuWjO>@Ixunt4uCPZ!fJW zIWaEq+4ox!ix+M2|NY}&NtbbDLmw=OHpvz}8M|6&R6DH4iVZ7Y5ad%JCblrg z1-5git%(_g)wc>>qSm;FA`B}iKfprFEyq^^DDDb$ZQtJFwq3CMq#ZQU(OXYPZ8u@>|%o1=Ji6W6N9 zRd7?cz88C<(V-)9K$$+OaI2oeaI9!GgpfeF>IaFrKnht;my)X{teQQu2F{ z1=0E>F0;vuOK5xh$YBP%*XWyV$-4ZfMJOxw?&k3|Sywzyn*5%$AR%)^9$yNX za2UcLbl}CRKVQKc2x-niO2`zD+$KIcoq>Yuj3P;+pi-4mvuovH590w$#uRP+%g|`Q zVzpJyIN9vZa<5D_;zI6hSfv>+^f%k&$#}XemLg-R>H$cf=5eavo+(4Dlu@4Oh2XX` z!TX63)=n_=D9l2xTo#6{c$%kNdx|ilBts=9X9zOV{ciz( z=z{GM^IJdu#6!u1q?(}l0@!9lY9h63O4)as&HG^#zJ-}G&Q|JeDD5r2(Dtgx=aCE! zN@vLwM{Er@>FiZ#A@8At90}q^RMNHr&sfmh9-7CFdVIuP18xFLg^KFDbXr4Vp3MoGY&acof zf>h&uh9EVd_J+%<8$eiuaKhXfrf8D3G!4#@QcZxCItx*I@`;(ICS+ka0GvV`Zw-ij z+ou6Si>w!=dP>}9;X;^I+Cd-cf;qySbeCSKg)A9bs@3mPv{&tf8V}@h4t?q%$zg-XPa^J ze-HiAZeZK_zc=V{YG1GONcg^yB%ul(L2nP%tRk8Sk`2`a2v8~ag_EKEgWVLY1jt?B z4=dp5jgNLY>a~zUE0HtUvYc3r2(xsE-W|{zzo)&$r?*0DA9bh*23>gG)Zn=?8i|*u zb4WD%yORmD-?n`yxblnrs>N9KDX4$_l>C78Vx9}Wv|X_q7(R^GYRG7>yG1o0^rvs& z8O)9}jC(Y$&yqt+etr6T{$tAFCv&==U1#9L_P2CvtL0E=nvh+(T;SI;Wy$)7dzSt- z#y1X5qbuunicmMSHS0Lg;)zN4sfa@dlS^UgUt~ur4@5wL2B9(Go_mlQtPgx035E|5 z`xVU3?o$P(P$6oNix7*aWY}bg z-jD<=9sdl(vU<*vYt=27R76}TGd}kKg%Hld$`WkgdzIxBuV8o!IzbZON`!ktAK;?- zrP7~L=olMH8DF9aZ&B;W@bTfq`y5!Eb4k=iM0Q2PFC4UXP3mqD}#1s z|8CYUI4_&kpe)e>-g^y@O;j|Pf zZ((bZTR-MUg%TFQra2WR36BZN$AUaArFZ>ITr^t^w_svd#-TpgDu~uy|%DW@i8HL1o&VqwI zMmwlb#LskHK72A8N?j7NW1(Y0cB<>&Rel*p)^qC1;f*LVyuj)<<TRLIFELIucBg z(LhrM$H8XB`h=_(F=1T|GXKumwUz=`Tqz=a9c56+C-Z9KP>;=vmVL`a=TEV17jfSV zg{$2bQ;4HWAtmwB_?R~mf$1kB0(qHr(Si5!>*r8>cQk@I^Vpzw0 zzaYc#&WS~{<1SNudWzo|j(TJiQrWq7WtTCVUnTq3E~Fq#E(a!u`L&EXootBu*cTzK zv0S5^FL9>G*mWzsoi~)ECla~NUa$ABfX^JtD}N(^Vc!RPJXitQU|_0~(3a95PEI@A zvjN7LR+fbH#>-gzG15f%HmA}tFh-$!D5QmxstMwpkf~6@I8Py*g*8BEh-oqxGWO^d zCwEpxeeF1;`_HM7dRr%YRQprfn|EL0qqeJ-$5$it7Cr?DWEebO$G7)4lriwq@lJ`H zj*+sFb+BF0z{oZcHr@jf**dGS4Y!#|m1T)7!~osZ5ReEdYf&1d4Lpd%R|F}^0k{YX zS_KhwoEW*HvegKF`zcIQN~NPY2Ik-9B*o z1-Fu3K5)d#v~SIm)@w%iti3hJ;Z{ZIMEEEuu6>vc!&_!x7ZnG;8pl}^6k{MkEe2`} zlA6#>9#p7o$sK-3l`f!ZV^#R+kaB>KK&9G4sl|*|DK^bWq~nSiP2#RCs#~QADWv>L zF)tHEa21l-O15%<^sztB_|EE<1dCJ$88&Y zwsLvf@VJ*ZEBfAB6)HP*>C_lQJg8bk@6%~&{>Dfs-y3nVQ5fO@!8t|QH1wjBWe21G zAiCe8rsD{3*NGFP2GJ~izv_E6xXC@|#5Y-b1S$8cv6bF50`d%DW`%ZCYfdB(uObZv zc@vM2+>C3}%u+(KWZKRW{=WHXiVPn@A)-nKC9*xNC_ztDNwy=DtOk=$UJ>*5;V+Uf z*8y2bT*py@weW%Kt>L=8vsxhkDYyE#ZdjYM;6lvZ{cH61C3e1I;S?Njm>kl6tIy?2 zqh|gnKj?J3`PQDlR-D!fl0nkDsSTy*chMP8^S+nzMJp!`LQOt{Ia;Sdhm-TDO+_q zgsL}UuIW5Z$TVFkC0s(AQ;o*}@~*hj9o>8>B^3iJs7;Wrun7>RbuGp7hGRn;oxU>`~vZ zmiqohSzgiG-a~vsMFib&Iq72?7f3b2k<^$PI06Sh6IM`--C=}HcBD{ZJO4)NeE4(- zPlnh26`7>X!}cxxrdN8NF4-4!ZBEv^X{XyR_a7VRbfe>(PoVUK$ zwXA(7U0rJ4m2~heHkrj5LT8I^|5{Mj$;JU`NXGR=A`5XNpgQk1UM z9IF?6Im}GMF?AjdS*p2j%;L!oHL{0htO;sdvBJJYJfjvb=6yym9^sl^r^}&-c67_i zhuOS2VPDqYUU#ActNy8d^197u?p~bxGW*@sF9UqulIe9n)(l?02O2LA#dIdrW5Ncn zC1M)@N>K}f>~oK|vG^GZmM8nhP%A#@mBT;m*6Z!pUP?aHjkkz*jI3E0CvDHn6R$c`zAuQGhqvzn*@FU{Qs-`LVN9ieF1TBf2bi5 z#t|;g5{%+03gtpBLdpwPKm}DFL;2(lF`tFM$1LBfS>#qGF|$1SjpOpEgWXxqqvEb@ zzVth26yvZCCXq7_|3KIFbMCBczQQ;N+D2Q&ymP2^$rfrvQEn9r<)g=4pk!8jy3lp08 zeXF>B(qhHk(93NCjlHrg#n+CAe?A*v7#Cc0rnvIt?1E2zYY%n%@wPs|n=6LPJ^TE*)6j`UTf(rh8hQ`wikn z8q$&>{(W;Y9Og(}xA`vBbCSXsJ!lXqo-F3>z%Dq=AZgd7D+|3IBod)T`&Ai^&O(&{ z-0f;cWZTNX_k0bVh7gjTE|;Szbp?B<-b$F4rP#lzHBM8b*%OL8KPsX) zI|sTGd4AEfYcPCC_(?H)nBNuBjp^RYl{-M!kl5iWgz1K=WSYbEkx9TV%yT8!M0S?& z$`qPaGol<{ID3~OwrLvVuZ^IX03}g`rjcGk5OV7`3?YelsYAa-tZ|fmpeJB0KAjnY zb?4e-N{Px6;LZr)cAfXY7~dU;H8hRifV3#Sj1d@9SuuNMtHVm3odhiIO?47$p~zAt z4h(C&y2S8>_zHqX#ClQ@j#f7pLJ7YIKX_@YrNq9s0ogw%#e08T;P@6|xINOnLL4)? z|MT|5o)^B0{vslCUM!`b*0MNuQQ4i0gdd`aoc8cs)}t!c<;$d%HaC+3u{G2~EKe&v zA@G=H!DvV~V|pSZX_^+wA855&2n%MUrdlN(l(SyGc`7%Hs%X@Zlppld!sBiot&UYt zY4J|xNRhMb6D&t9qiAQeK}SE$jiXq8DqO*-cFrmguyH5_YO;zgou%Uc$>|K%P$^Sa z0|mig18J1LnK-(}(@{t~6ftUod0rmR1G8N*B5&RFkfZaEWM=^0L{{ zlExa|#$Iv|Ix)y760+(+1b04eLeR5eL-~{wJ61#?YImcwgT6EY#f05Y;ZVbt&p<*E zYfyFCEQx0tL#v-KPeIB(5!0TBLVjO#4K8DwO2JsyS@ECYERcdri)hYP z3z2pS)GJ2eAg?78-@#d=eXztZ`v88DRtdUQx!9kC(a{dUvp^Ut!~%MK2iDOwA>tux z=Hx8{shfo9P+BybjZeU-5l%o&Vg-9@0*u7m9^6>NHlP}UP5|g<%NoLrOt>=bkzA`4 z3QREt9ma!VK73r5t?0x=dk8G}6f%xnbGmX&pRm&L|4|*M!igwI9r_RZ&%Yq%`=HMp z@KX$ZOp9pO{!%=m*YOTPHxvBG-}~I|+YOj_^jk`=Y1Xr+g@%s%bgRKbNU@k7 zguIkbNpv4srFVwn&*)#%qrCgMvQfIWqUk?fjn?s^E?>P?e5(M@ZCEG+0paKjf)x&s zoT?N|L;GI}yw8W#x~H(4*ZX_O zp)*47S;NAIZXXQ=&XPo48=)_8Hn5|)-8O}1uyvxtvj)8uKyVb4e@e`cLq^KfiFSMA z%ulJWU3+h@U)QNai`T^+J??&Okk#bQlS55&uDFt#IV2!K%%8z*XvbN^($no!V$176 zVRtEE+Ua?|CS9IyU2$dDo=cenbPm~XIh@vecVEi( zm{-*L)S@YgknY0}S_pHqMT!tMuyI^T8Wk(tiPnbyCdn}L?lZKT>Y=Wzh5E7ROpXM0 z)&#|?D}e8$DdY6O1kG?$)Br9;goNp#E3ZwlXFTFCs;4zO!&Qb{Zr{YavDHT**cFe$ zrY7Y-6!RD1=>TPM953NddNTLZ%O%kl2S3u|a%94oHUrLAlIM^?O>RrpqUviTTHw&n@H6#99`OKoluX(C z@rQL3B60CFQmVCv%mTGK|O0WMgJ}P;IN(jt;XVe@YI)Q)Aaw0@twQC@TU7cS#iMN zPQ%C43@xjBVlEBRzAlBH-3x?b2=v2KkAl@)_~*s8r7Md%&g z^los!ntlqD!?E7T{1obN)b0s{^fC#>J{dRNdn0HE0tw3$jXQn8z+KAvmW|B%ILw31 z_fozPvGKSDaSCma`+L!K=rFd~9FJ=I^{JeJShq8_EixA&gSRE{f>KTg;`=(mx^*Sa z%i+njk7LK9v*qZ|E(@bIy0wd?_%T`{R=l;{5dVEkpd=8h0U3Q59yx`=N_Ye}o@P*I zGr?G^&avml%pb@u?jT+}nNQ>F!S{v0Y?9#EbGBT7Lgsyo+Fk)qfPbQ5Pk-7K7+rNI z6kr};og&QeOSP`qbIdoc*Ip`N56UUYJ{Zovmbtg}d-2W=<8=FZwT-ASAg|3`v}@|3 zKH8pIk11Q7cwx{4@8u#J%JQ!S)e^^+rmeVlWq#c%n>jP$q=+XJ;^)F@`Sf3x%i6B? z5(EiYwj>S74b~o*=;WmdkY21wuDXKe*@Lcy+fJ~ro2x6+)U}I&1v9B8MoYZVCO`~9 zS=95wX`cE<4@L*W^+(BH$6l*5?|E!BYXOC{`pF=Xv>oBwOr^@nhIz*a^fiM7IIvdV z_ooDz1nR-S#8HHwDW~C51*c}SkKBkx$a*h8AL3#ES2~H6Rd5J6=TbJy^?T5@!Sm+d z8T_Zs&5G%rGqTx}#&z7;_xG6^bLVFQaLAi?evV}v(sAZZ^u``}l3B?7+lM#ELY`T9 zkI@|Kky@aNprHLK#g1sg0~U2+$r6=fjDeEXVDb-(`45m0UI1k}%I+nw09ya(t5#*z zo|Hx1C;#fV^ZN4lS4B)q3??Sry?&?By`EPG?YZ=pEw{+c;qsIA;WLq7cQe^+U6ZX# zqO8v)x9a%IAnh*IP=Kn@H%4R#-?R6;Ke~aLMw8-{c?o)WUa0cvR(q620EGc=G z;E~OohazaM;UN}GFu;T=R-=QBs+6h*On$&{v9=z{!BhKmhtI<8WNu4I!R`vN>E+$+AE+yi~mIdL_{kQ#0J2k*N@#d`Z!@CM0o~FXiWBS2tX`^>F4cLi)x#C zn~_A*87+OO)-7xTmP3TBp^pAJI0L9t{yVX@JDRLknodZeG+lZ@PMla1r03pkY~+76 zJ%5gI%z73#Zd?1$zfW)_OmTF>j}egS4^!|z6-)y=pZq0;?|MZjJM^XQ7P*#wGs$7a z)&>!`R$UqP^4SO@@6~QAoU7bp>I1fy3^e|BZPSZ>Q$|gLJ~t$;yV?`sBQ9TLYBEc0 z_KSfPQE8JSfdw1*XUKYKxPpa>P^99U;+V{#)X1Y87rQ1&eRf*u02e)|{YZ%Gz9>ie z>xfugd)wU>S+>V^T7*gYPKK!+M>)4FvT515#ezu_V&X@-6^@-s!jBJy zOsae)GM1`%B`1I}x@wXO&xt`Z?nMDOL+OTLaQ>kCkB-6y8a43R(|XJ=>t3;EH!@{h$!%UkQL6HLBg^K3TI!MA%5=lFG87rmFlPFHzH>5w zA}#4#-n!A#&H=6Hl>!E%^L7$F)p?-jELTSOiYZE`Cb* zfG=Y*bK0e4aU#~_>!t2fUN{|*cn8HJzm0+DTR;wpV!8*9t_rKlwdm=i*s*H84!qec z-Y>61+dieNoK5lj)s|b>Q>I}=o!GPN6*DT&D0PLLC(1NHqM!m!I!V+pL+cWuh>jrOV4aJh60Nm#8IfBxOczSMOHb1Br8?Cl&2 zXQ@#Sw1a}8!#%6b97AAch8(hr5VD{gOT3*Rkiiq7_#Va{tJ+LyRh?!jEk=_#O zVl54nC6bH09Q`3G4cEMGPL0@JG`D$Cr+VzffCc$aPP%!Wg~Yy5^RgH0Gk9yvG4~`$ z0j-LVFfP{5oqbKjms)F6jI1)dVZ=JH4kJ8!EGUV+QIA60$+&1fpfGB$#XJ?_-I$qm zG{5*^9ZyTpLANA=Lc^ul5lye0VPaCq@XAfn2IKZg0J^n1WO4@ymGX2V&b3a8v~lTY#KAF!aUGod(vCk z@4M8D2hYxS^gzDxL7B5^-JibPFks9m<|WCwVO2k8hixNXazzjmn?As!h&oR&5!a30 z|Fmcd($;59c>qX^>mZa&VI46>iY>&lwu4dpJ(~aD24AAk)1yv0%oz%ZvHx9M;x~DS zELZ_PhbKr+XPqupcr)4UL@9VRJO8LydpU|wnM0$(MvFbl_wuw3>nMNZ@Mme$M9frd^QWu8M{;;xq~=e_E{ljaht^ zr@R>jHx%-#ubmZzxOTZ0bbOK>>9vW!!TjW_^5H(Gilv$v=dDI#SJeH^;9Rkn%t@Z9S$m@%$5F9dnILIy@R@(q^M> zMfE%UV{cZ=LpKb{CUxCdU=p??zF}(46YDsm2t1Hra-%z22D!0(q{F$ew!JQeZH@BW z{On`rL4&vV_nOH&+j9SUc`k>{cvmwVYSIWf?rwW3=g+u(-S%Kw=J}xl>zOy+@b2!Lf1ri!z&iWGI}N(tV(qD2TDkG!r{Kr6 zpSONGw#w~f(V*U${?8ZnI8DCG<5W+b?A=vhyM5QkZpY3Q@!kB|h9){3%I8o1@8008 zyr*YI%`|>awe*9eqPpzV+f>ckSMq`orDv!T7#AYunC=9Ac3C($|y7IBY) zHUI<2YArNy#9m0$pR`PG8KSxDT5A~d%1%IPH<{o|8omh*_BTO#GIfyk7&18h*;pF2 zV39CiEd*ZC!nAzZPJrwRLSqViMn$Yi>i#3h(u$(j-jLp8aLiuT%NH!|58Dlw%b?ZR z;Hfp3g0~-#}s_rQA)uoZRI#k6`Rbo3M{CQa9Nca$p=8Hju1DB z74F5k8zeZjF{>n+cIiFhoVNsXtCmQ-eA4q4a3h2KYjCn!3}`e2bEc*dnGx{+8l0N2pFctEm-i2&M56LOv zr9(H_>9FBYLTmWB0f5-aw+ny`oY69pkB-^|bqlJkT$@(x_|i&w6PawJKg;yykOc z%lC7BGtz^#MOuDUuJi>R!=Y+y@-1=@V!J6KPB(>OOlwnm<6wv6lZ<=SCTfs}Qe5#5 zRwSdrCQrhwODVbr7^~lc+vG+x`$OXxqaEFC3XOe`rIhCST*Kr;(piO-LKolqn67l7#~bTNW?rYlB8{7 zvkxPq7zUsvD>+76h7xs2!S_)x|FUN(nksb%>dhJ+uh68=RNQhYxmwO>SgGeJ4Ve*m zPwZ#)!z>SXWxNUztFEXCXDSFMUw&Y?SZ677znm$~e$rtDnRhoO4c@jkYYwYfn+Q+N z*tKGlmHSNkerdjB_p8ad=Zb7!)i3F>cgKE*S_!g#lGi*xl1omUSNDI6y$M`Q-}^Xz z?o2i9i_)fL(xONzNsBagNd+u}3mHQoZec+EA$7<=Y`X_nFpF<9RX56n2m)Dds z0dJNJzQu~3ivuRz^WzMEc1wt)BGUa0Y%F%)viLm)N+}L1jD+hiEv^?1mwB8IZV(q< zh?oo>#xcH*8cQ&>;%Gk=mfYbHXDof!W+>YG>|XIfaGJ?uyx)NfI-ZA&5}B286t{$g zOvVHfJtj)t?UX`J!bq&CVF6KU_+hUmXq#W*4q$C1h2*R`V! zlSx2Z-MBoVWWoBjG$Ag`%~*gnm*P@qNF zVv9D7BcFj{6k*%Q50phS{idpy?G)>@inzA9iS+(>+-S=#-z@ zaq2an_1;T2K0Gt^gw>_>u5~W?rFY-3I(_|g?B}b77hAI@OdRZEt$s}FNq+74wVQtq zwN~#OiuY>9PtvUsdXz6Hg2)4xC^(2J2L2rlR>=0S z=D=ocJNiIaBp$H35rdw|dTrmL5L>^{)NxqSnsbg{*PgVxx#ZCVrAHfASe%yHr9NLf zob99%_V`Z%t>fs&Q|Sh>nvts#wku_6tn{7b!<+W}%1-^Y?6*^ob6!3A_P{cBzQfty zi(bn0$+vKfby&ad@kZnORvKpy6q?=7+4OLiHOzYxzuA$i zC#XJago&h4Y==by(qMNBVt8D{B^QF@JV)Hs7!Ex6cu&GCRlvulie6{Nvj&=#K`ow2 znInIoaAj$#m)rqtD<(0BCKVMH73fMaVS$F>&BuDBeNuZdM5KzhpjE`@#+NnF)b-&1 zRJXGD3Ft!@8I*fI2`Fws<)#K4e}PX7JTKCih0x2I{v>Ky&a^bsZI*tFf29pmj;n83 z2#tyC-O_5f`sK@wGRP$whj}0@A@K(^A;XGxRGM2 zO6oj>zLi4}LG&;9?_CtI<@U26Qk7_-tCD!>#Ef*tLArck(yolZ zb9As_4wLC6Yk*hNw4vKl7wJjVDk@k)>T{#`{HD;z=hJ_Dy)jNzr~^5*+8|>>3!``J zv`nRw%Xz>Y&gJiDa)ktfnv}3r1t0tl-v{r)R#n^b56j5TP1*Bv=XvKpS1uWcpZw&Y zlJx%a4D~-7HM~siwvI3K(T{yBHR!10k6H0%TfPjtKGuJxxrbH;b0{wTR4T{|uE2-! zLPU@g#W}q=5+s4YSDGNf!URJ@&}jf){g?D_hS3^6PlhWYix*I9YvNol1h)Us~9&g=<6du`OO&ca8@rVe8Og)*cWWssRvu-c4yZ(ZQENSqi;26$iR@{OXz#YmL=7F*NAR)zSok|;=5_# zlyfsmKl{E`a*K;@F#6R{w&Kn8BxBQ`2}YfCW%q*$Bi+~=_U!!e!#jP%2ePpHL(;y+ zRge8*0uL|0*)%cp*@=LdlBl1b);wDE{QH?gXXRVD-&8w~L=9=%!~OE|uyfuy9qmo! z^U1hzy2EZY-sGOR9=vJo{%i~Hdt*UfY;vf|nn4lI<}9@j{PwAE z?qemt*4;>5J*@%VB#;yhabdW|!*?0gvgIzV|GId$c5IQV`l{tdQAj>*3>Ak%uVH@s z_4UImHbNF{6HOnS|AtJCOMPS;%c0;=Zbh+);DLDSKu0sR?eZNg8y$On8I0Xq`LIWl z-VGiLTSwy8jeKx_E=RMS&yyf*X{SyQ5LQRjVE+LM#d!VA0Kqk)DV+s4uy=OC?S)%tTvAgk4BWsMqJNz4ePJu{^}_4Yzto(rJ0P)Yt=F|$2xRAhnpNLes|KklwkdLhD42DOs1GaoTt%~&p;A~_=$Z?Tg zGSpLupKeYJtQ}__K}hq+m0H&Pboic3a*Odev?mj{fDmGZh!Vfrg(NaG6Qc4-@D8yT zxv+percaBNv~B-`FXZ8D#)!yOH)`#WwE^Pfc{o&w3y&I zp3XovU8C)4j>Co@hiR{yBbjEuhXh5XNmbHj!OyMD-{h&XZXA7zp0?%jEt%{3hppok$ghu7`Dh=Qy3T81#)rWt zx339mqps{n+Ic6e&rS{JgF(&)f~xjp(QDxuW{fb zkkWbJCJUQ9h=ovAQKOg}2pZe$w<6tP8)G-bx9_H{KUaI+91(F!F)yH<;GUpVn=@_p zknGR5Nl>ML=J;dSA0ABPC6LTwAv7`llp;703!o1vR#YP)$ai`z(w*N*mXRcRNGJEl(YM3=I^QzO0#_tWthgDwCowbk$-wsN7mFR`NR&YWu z-wGM_WQ_lH_hJ8e-VN_(vTuILf{Nx#w3}fvZ!79M7(X2|cmKC^QM~7S=vsEPa1DaH z;rh)Z2ekXOK?NY~E&MA(O28Z6LXgGf{7v#VPT)V9+Z67Oq(5BT{`AEW6i~Oif<(bg zct4kMKAw|+Ms(bg-|vw!XA2FEQyG6=c%GhIiU+tK3!zNNM@ltA3ZNgdj~wGS`X!(; zBxWGg1`c|2Z5*GFSz3{>J0L`D)r`^T46KQaQz%CYKK@~CS5;Yx-cseD#gzWBzEF`3 z>&M90ONv*h%V5$T(xaxpkK=>X5GIm7Dm~0(cc0UWjp4fGB;im_nm0GK@~2+;{Eaf; zWYB#Ns)D!7g{Y38Ko&UEhSFlMx(Jrds z_%DNO<&_`Wj1s+aQq@+$d}a05*J2nsFBb<2O2iW6e`P}VT1>$BT{p;DjHXb^<@_@B z@@KBlBrIaVYu99tUN&nhM?sAr8Oy_kjviF1tvF-J{-D;O%o!zfzxV&*(BxY=xm0cP zRg2kHkE;*Ij6E~j{8yfnLgtm-&r}fn2utW5aK?W-KRuUn=|KUrgNZ5!>JGdnsR%5Y z#lmT}n*&Fozu1x=vZB@NFFrQCb7PF|uEd7?qKHLbGUV3<7IG8HeI|KcpYH1InBsb* zA-Z5lVSCkjgRUV|tGnd7C?^)fSkORP zvv?vFqBrRE5MYtI1k}&fg$56KqEIve7o2`Zy3!q1&mp1%fTe?oKI6BLl+b`An0yRK zk>R04q9mJjo1M4_sH>a9r^qgYABqam9@BthKCP?e@BI zwcw8%M%sJ2oj62tt)U^I>L!RzM#KUhm7dbbA9gLzz6PQ>42%S*ykcfS_n*y;9 z&3bmzZMp5TzjR*rS$>w@d}~RXl+B+AlNK}F6ZwDC;XIB)F#p9IsxBrE9#k&7{QDp0 z9L36G-^r)O`#KX>s65*q;rjl^i$N6~KK)p4@+Hr}%HSx3@uQ=uQzTniJE&gygLab- zJg%PEHseg7@xVJj=J)$>W3_B;-$?(&hwtVMCbg(ictLC+^)vpJbo`46VT5e(;dpSk zT?u zj{#q!WAkoLq8|@=Z4`_{nq^Q?G*XtiGYi5_kIv&EqAkf9(H61vSF`0cS!a_O=p+h8PVt^6>SZpznnZPG;85ny5VT%c-I;`ekjqEy(!Ui1GBCT;5o5sno z^Ayzu54bG8*oPLleb{iPA@>)c*SUM&#=p3^ZQnXlq6gt~@Ihh;INQsdqp%mXE5Z&? z-WqHa!_58s(pmpOYti-FW1d$na%UGg>!}J9uj#ex(yA}JcMA?!n!!E#lSLT3X@={W+O ze`a`65d&3q2n4dYh{onie~fk|?ebxk!+`Awy$|6uj|m8&^<+c?iL^Do1S4}rBvY_K z5x9sR(||M_F!mP=es_7}D{!c;^TvH4!GUTA4cLUdPYKzxU%-=yR;Q05(TH0r%nh73jtu8K414mc}7EKbCL zZf*qL%Ku-O#rj*54g*HYCi!1j&dX3>7Vtjz(el;fT6tJ!nJ>KdTacz6(=b+tb*Jk z5u8)HtpGei?HoH8YT3r0k|y)dasEa5*bhnrzO~)qtk7pTM5PR7E2EnCr1ivTQefjzTU!YyhSp!ez zD(6oVMsL6TJ#F!|f$S3oUvI|9*6h6$TyVVJ{E&Is)vHQZlke{xKY0YWSO^)xW}e3u zz_}MJ=fPuf6&PV-*^f7a+h)xmc`z9-A&F(yu-dR)6&#U3C_J|7c!hz2V@=3#B;1L@ z*Oo_36V$2S;lNvLGm>UaJIZ*^6DZ)lGA4W6braX|kA=J-@HQjuoXsa_KuKZ#lB~s~4+z?rKZ|X4YkT*6gT4@jG z4x)orw%ZzxLLst&`GY@2w+!6M*8W)i^WIX=bJHx&f88)SXSjA* zO`NT(cjs98Wk%Ygjro0sd|2+aH~)%L-#Vk>{oH#moEqN+kX|RQooVZ%RJSoQ;o8_o zQ#x~Wym`?}}k>XFRcXwjiy zt_Z5D6%Sh5=U`zQ2r}HISS5Ps0`w#4Ld1x2mRV<@zCqKOAtO49kI=%1df2pp0=nvK z5*(h+jC43*m4-LDhX;HexUBkX#;*%&*A!>FkWQrS)>p|&NpVPOB)^Sy`(qzXodvqt zZ#i(MZPVi-S?Lv0pJ2Dq*wnC0?)}mY^|m{G?FYYDt~ol_IplCR3p^w(>hJ$~zX+$t zAQ)5-!yv^`c!boH;Q0>zuCWf=%Z_U`O3x?PRsH=M-*I-w)b-VVIJ|FiATsAI?RL*7 zpIowt9iyV8`z=B*_KeQ_*k^|?9)6MWbIR5iXY7V#yz%#998bOe`i05DVV$yLBRCgW z4{bHoZ+)aa`#8kq!QL;{W2t3+XN0;F`X2IJcdi&g%S&DkS-(^2JmE3w-BR1 zL^sfgqBKwmDHaXFfI%RF9ZcxzuhIN2S_8z9$8bdgjQmW7#AP=$$b7YYK&ifS3`>MS z&)7l=@t$0{boo)Xh?JYNpM(`jQU+2CYFsLVubltBnGu+uLa!#AKB}=~)S%3AeXj&K z{f7ir$C!m^JNe2kf@69zx+4jDMX$Te1~)4jX+SR!1BQO#j1ywC1gg~ad_$v%Gx%6Y z2{rn&(# zl?l|#aIa51yIg*|s-Nz%)xTqg99-=+gNIp!vsWIRX|B*L0?c`ZxlbqNHbK5OE=r>XTd~6!&H?N7K-$0tM?{^ z4rfyIV7qA~@~%)HBa(`cY7vH!Y_|lC!VlDrzqn3Sy0+<8l6j_iN%P7a`j_1cPK|Y# zXcN^nyKW1Se(>$5fv;6hnyx)`_8Hn5cDbPG#a&Nb;enMK32e0?W% zEW3Zgzq}>wQP(}U?vJNZ)JGQo0tX#$;soiN#l&N!5*HfAdAa@Ppx=5kgD*LfU%Xe!wPa0My*T9Cxk;sS>)HZN(asIY z9Mc|dwZ~8Oy%Rm`la37VGkTWsTV=`j& zNf8zk=|*ETYMpt6zh2=;uKfK5>=sTzz<6T} zjj4j)p(oP6;H$2qa9{=>>#36+z?z_Z|s* zmGA(;fJ5rQP6|LK!&JuOR5sjIZbw;+xpT2lizJNxVD)&U|aRmkZqMwg|nY@ z^{ccZN=C#hSVgz|m%2G9s59ya^Xn1!WXs{7e(MkEtau*fGd_fNH739QT=0mb6Z8d} zs_ycZQeS}wgD3!VQiY{6pNT14_+>l@!-HHf;21Em@nI8G<|-0eor=STNTF>!XbdTV z6tZfJnv*yc0JJm)zWwLpA2djW=B&0!4unw;XB>VM^Lv^AQ(`eyn1pzBlov~3VoB|8 z5WHu@s9QinL=jUYGTe{Rt6(`;&}TI$J`P|J0az0NQJ)g>p_IgC6B5in9`pQ}D>|rN zD2CjIi3z9&3o~0#1_aHyVGgM=OFIR(D<6m~mH>lHD#A`gNc^sBH)W2ZC4axSZg7IM zt1ZsUYM;8c{FT>^dsdVFoPYamX-A6N0rT|e9ojL&7I^;BFg(?|xG8mYuvCkEQPuW8 z&Ze6_`)l0^%=qRc8$Mr07|$TO91NE>1Xf1`*L#gLz91kL|AfEb5cwAi7*yAw1TrG| zEEa!{X^Q>-N^ncGiIiwJL@3NmCpn5Ps6ZaZgDmz6y$3azo6Z<&r7G*cKlXyn(;Yr- z0f)o3o&RMQ7I?Ifshs0Zi}6GMC$W+mt7k(e;Bv*`T3>h zKNEw;(q2AKWtcECEHRrwghfE%w( z-Nd{O`~MV-(#?d&lqUf%;qa(_Aw3zMhkqp1nW9Hqd`(e0z(IscBL8Rf2YyWfZm{tY zLCEN57?d0rS9wwJBp9zqnFrBGcy<*5HdcI~vxJz$C=@Y!5U_+DK*6E?n9yyZD$H0t zQkhjfxNsn3k6O!73_wAeu&N55rc=n@JIydF>}0pu_t!+R<&8gmBU(1IEsK(tuX}TqHT*)+JT8O4iwU^x z1VG%g@I8JLFB`8ge^IkY_AVF@#_!+Ju$uJ4+^f~DRo!*7Y0SOz2B!JXQ+Et=*wNxq zb$adTRdLf>wtX^6om0Pc{ky*9Qb8sQL%-7&oRmS?1Dz*!ij?t8K@_iuwgKa;KnY2wSr|bUlgEA`^;51v3Vf<<{j%_ z0oPJnH%!a)CiYz+w!i+qVD3y($1+5{rLS#pc2Wgty>}&Pc7kjR9&+cW?z%|M#4_-$ zuh%sB`3@iNJ>A}9O}B;7^O{goc8ZPS(`G*s`5UMp8%tTJfje@){HG!M^TqCw@6oCw z9&N<>s&mEjcV2tGbNC)odkOh-$CI<4UUNP#cwPYQLX7MOsSu<-vJkNZ_DI=iteiqO zcGz}hyPnfD-zW%_VPrpdUl?C>{nwvjomN{-*=sT4rt{o=&XY$yx<2p4 zil=3EHM!f4E;$}MF=5R0qY-(k#uF>Tz!$M29N@EQI*J!>PWsTKVUdiS9FKK=F{$%s}?Q)OIUoDw@%ZT(iMem;-KO!_icN43B7#OR6B?kj1WRGD&F^=0eh z+K9BWSyE2tp5<(szruI0QB7z{yzPca7dh6~(TGUUUx$>FX>6MOJ9nv7H=+}EvygsL zp0p<{^j|wLBDYmx8Bf2dxF*P?6Mghw=CKHxI{OqyaR!2q6bc{f44I7Iit5+*t<&(d zBAIlysWF#;ilP2+y=1#x<|r=0-%n;xt4#1Q_IKK*jiY?$I?^g4^(*JKpIvSKecqBw zPDIeiIdUJXYfL>lXCAyid#3E+>Vt{gucv=ct1)=@*udu5MdiHtw`%)F=cUv3ypExt z^L>{#Npo&$sH;gqiU&5!@LXI1dZtcb_sFnEEuRk-*P`!u(ugH-2%oSeaV~xIKA#jz z)tyM7=q$?RoCO)4nWncv(iv4&j{_R|Lm7b9d(HH=#EumI4io{{pqCj za6hN;-2p}QORBkq^33Y{XAgA0{4&|Nlf{@bz-HFr^9IARpD)NiyfUECUCy$7Yr_J& z&$o1Fcs*+r^(Xm$fUr#X4RI zlheX2%O>Yg7p{|P-rQLPt;=EmHXGcBRAS1W`u<@HICRZ;h9$wp!O$Mmd z`t^Bb_hrBnHq;q(c~AZ&2`|*yaL2e}JkfqDVmyrU zBS3mFF)1@bz?5nETPi~WZ?}NY;8Jz?w-&`wQe*uOiGWQ#t3%>#Db&w)AJ0+DLfiI4L`KKd;aBZBwR^`&V0Un`g^B)^;P^>|3(F>H3wa(-7=BRRtjeSAKqaOk< zRawk8g6dK69)Y9=jjfRUJ7;3rW>;Y5djbZ3LrQgV2ZJr{$F4~{x8YR9W0?} z^p(tHA&zA4^^-L&6UvCL@<5Dt=MUjs;F9Jk$wxp~h{64x%kO%|2m#GMqOFo*5r zD4s?(N-&;V_~Uu0l7&qbr@Q6(IwfUaF^`?BC69 znsEMFS#VDJ=!gXitdnPczH@8a*>2`p{u|-7@;_l@EQ6 zhitoeqSRm{-k2Vo0si=gn)%<*L3M?{+sz^&(k|U+p2-1JEryb^x9axt{@mAP=e4yyPIccofAVM6 zRy^5xsoVTAvV>jlpgsg8Qeg=Yu}o}C3%OHyLhOI#;s*&%+Otdt2f5fkCjWhGb#FF4 zT)fLl%K!C+7CA*O{uZu;B5tSNknJADQM`v@G=$F#Qp}n?nw%UB)Ls}r?61B?%BcIq z^mT3VoA#}*sPoc2a`9BmvdST&5^r?ZUD~jQ=cnG#eqm=&dxN^Y)P}L8s-ZtzvZsWq zW=(JmnAtCBx0O|lf@ktfw-@o6YgeU82QNI*bjMvwYxg;4#krgAUDz<$Z0+|HdivXT z&D+)X`e6oP$5&Lf28YX?x>D-%#$$DPT%SL(o0goJ(CMN3euR|?bc~tQgGSM>!0R0} zLrnoq?cm9`o7CGcA~imbBF$LXWcvfom;}kdX!wXHm}jLnN(zZazF@UF9@gxFl4s-F z0Sq!u=t%j5(|n?LN@Q8&_O5PNAW(u<9^G3P)3j6DQlUW;4X{oQrgAQ4Kg(w~ZzG5yDco^ub>{)Dw&_SBW_VRtl-}}PrPkI>zoPVIB zvnzl9cIj0gTBub*&@^HhvG4&W-F=N%#<`lNU<*nLn8?_>8V~DCj$$*mwSr;oZ7S7b zk{?xXHO}l?h@OEP%X9JT3N;(;33~rMTalact6!^s3U|==wNI`$j95OsV$lctQQjcWPhZ?RBQSkm)q%rH-4c%x6vsS6Jp*l5|}Dr|EU)Ic6`KQ`)Sk*CV~4 zNJutHxPA_8`!^U|V)kZQ0*CrJk^+&R#b@KQ!7`R4v=u~=C}6z4h+9Gm;_alFjUuKP zvc3nWS@+-)7{rKiNn@5W4_bE)k*Q~K>)Q9b_6|K<#GKB&KRmDU%ev?-R*Vbo&cOqgGoSz1 zp4bw4;P;1xTf%BTH@(Q3wSw)t>_V7Qxe~|H^H(7!ZPRDVraMuZC))p0*Nnw+wIS4G zNR>3pJY29Osw*fP6AW`c_(!S&T9nD~APR#EW--B?*sepOe}WCO=l(*%hv#d*hrS{F z`~q-F1Pyc_OR2CLSdG)-QO+G^i2z3KUm!9ucq-!q(dg+S#6>yG3V7wS&tw=~8S#%q ze3b2ZjiV%u)C?eC16Kc}p1MxeC{2*peka}S^d7xj=yv6Zm4&Ku$!_un!@=o5po@aVXr+>rjt>0GWdVEc! zUq9)t*X&J;9J8%T>Xp|_*RH(dxi0_x(AOHB^SEj5@5#$;$*J-4bfbM+kkz7=mzrzCqCemt08-`8I) z0F5A!D}fq?Rbe~9Dg7WRqy)L+p^1*X954Jjo#+3l# zt7Iz*p434)F%fcxQQCsiX)SP9=J8UM(4hNd zPJd+P5v5qYE#*cFyotSZ7+#{8C^b3Yh&v}v(*8t)^8e}#IPK6Q=_ZCUeGqp2> zwDfgwy7n#$L~^&b+($I?$~`==FSc0R@2Jh^Q354tL=qhkJL5nIuv!&zW|bUe@Prqp4!cn*;M+l&R(%-r?O-W;9C~EKqy$ z%srp_z=-L7z+l0;XE1IP;?Tpaj=6z%xQaR!eJx~d=RU0T*0^-!E^ln{?IA4)t?66` z?veQq*qrq^$9#<$9I;NV3gWLux;tvr)zb}41eYn+ZoPPSYY3cEJs;*zp4inf8YiM< zcrrDS!lU3lsAk*9vuStFc zPpznpR2h%T4;E{#*mvscN%^J;rRmbqog@KuNB!Z;WV?;yD48KO{CYlKh11ctI?`fv z_C(YB?L_>QgTHjUr~Gg-h`;n@kLtz57an_+wwkpc_v+ZdPYX>nztkdf>+FV=JSTUNCoT4Ew{r{gPWzdc~%4 z^)7?i-IZ699S0u^zkH$Q>Jzgar_9T4PuV=_?aUnIhYm%v)1BnU+Uq?zd2P;~Q4y^F zA{q`Bb3Z9rzBSmpFz|BXivx!f#(rcin-vu2s+(EFRa)hIY3Y|;(Nk9KTd3VKdIobU zYdIBAMj0wm2Z1dX!0b?fk!n6BFn>S@Zq#O1#vx@XSl>CRNAmr>mm6S`BT^x>=4K#1 zC}biCmH`sMgpGNGAhlSCL4Lu9C8Q+QN`NV-b#S1OGn20*{Yn)k$+0A#5=)6S!Nz}c zu|fp?2{qfmYKnmi6Nn($<9=+JJXEKDffSPoP~2($hH!EcxkP5noU);5~3+7uuB2IRlhddA~xF@#UsgMu(KkQ6v(uALZxiCQjXF*q$XPKVh^6!V)4hYd&#dj+Qje@ox#gf zmZ*>eJ%1Lc3_kw6eu}NZ+Ux4cKR23IbE~VIZ_Q=R3C=HDIJRF{ z<$@};nO{S*baxpZ@v42kJn!wo?I|N?pi3X!E&B#XY-h9|4vpK;KkUz{n-^oNV-Nz9 zHb$(dxCs@9k+3Nci+aEyexEO<253cuBn_0#rgwkwFr#;HuB4LkCAK7kq8Q$hpT*85 zKBk`WE3K8hJQaw1D z9!rW*y-{rRQE?A9+kG)dX%mXUyN^-fzIlUml5MUk|MFh7W~HBbi{rEgjj?IR*m27~ zoxGPEzUfxet>o!pRqRjo_daj?mi98~qi;!P@r-+^12^ThY8C&ySg>pK5t9>X-!7)z z40rag_*FOeJomYZoaW)!O2&k3m*&+CJaND<>e?#n4EM3+*9ZMres=lLeo+q%f`#50)WaC7&C(uu8=97t6b3- zQi(Fq(s~&#J(G)+D@>40ldU&AmC|9QU`;QQS}TDm4Tye5^2UAQb#ur(;~p3P)ORm5GluqH@9$_73m3(zkD7~^jbF9TxO3@k-$w1Qy}rp zcD3;^Ay+(KFY0bJ8pK=k*Q7wGMCFXb(}#}D^&wGYX#}-IRw9%mI8L*V-z&JO#+nil zj`*cmDXoi9po^bwsTOYp~aEKtm5}w|ak0(0iOpq{|sM zSF!|3N7Jgm)FWaoi(XCF$b^o?1p(d)Mmt4On_4wh`cLd(h__jDi^=PCXM6@^z1o<& zRrAY{WfijzNgt1>yYc>hP|MwDxm^y`dp-wUUSqc<>f+G-C!hO-ou9oUV*TZH3k?nK zT6i9H3|xMCaDh{&S$0C~;;W+~WXG3P1U~9>9zU-4q$b}(q@Zm7%J76LwkBuIlT;Gc z)Cyr=6N#}YACV6gJBbwW1js3=ntx8h|JO*T4(&0D6`Uvfy?EGbF*1DWJ=_VuB&bLUuaC10ESPO84h4zs@3iw6fhEbCiywAkljOaf7@g zEOw}=`5E`S_H)jo)wVZdLho09G@Q#`Vn22L^^?`bC+-e?N?+nlq|9!v3XtyqZR$wh zgLy;dGB*4u`t@d*U3#WoiRF?fts8cqpD@bfnIbO*?E6CMCPT`-=fX(dPK@Y5sqxoC z0@k799f(wqdrL5p3{)Sy=_?76H+4NS;(y#Y@-=3$__?OH0O6Cu6h_nk6U1K+kR18? zOzL3Vj5KUPOes<_`E-tl;>T?>8_9#!inlN_v1t&10Vx>9LIybY_-Bq`i5N=xUPOY3 z5<{{*S8l|SoDA7s@ZJ+u2_+@7l62fg&;qf}$Blm{f2g&2-3*yFV2P(xI$ zZDexC$uG8>ey{B4G+dSPd|vUSlSY0PMC{s!M?Z~tX&lY|6m_$uyZz4n#VgR5qs?*h z1zYA{ICj}8aslo8^s1LO=B?e+=0`=vsGc#~y+yh7WOnL;n&R1;$J-uEobj|ABT>Jt>bI2{h6!B0utuDo{`9LSPc zo4?k^;m_BkFB?fTFV5%Txg)5MI^GTzK;bl-IniLS=VS`3CFWtMgny^W^&}#~-F-s1 zm?ea8*@q~93vP8-QVNGe%nhZOR5Est^q6(SRt5^*oD zoW9FdA8(n&MChIOo3s=e8y2F6*Bf0$QNF&5f^6sUkm}D^M2klE-h|emYz-;hm(Y)L zGHQ6lraHsD=HYX%{_vTy;;2`%`ux6oK7GjRt4g9}3;(=(N$Q^8m&1Yr@a>T$GhW@p z0A=c=2+Cy4Whn(YB9FFFx{>)ZwcP!E!>6>Iy!0xnoQVC9r9*(r!0&9y!!gXB?=CbaVS?zYC#E31fJ<9) zU%+(a?<7^lGN@PfkQnR**0X$G*|DjkB@_KwAH^BZNO4Mp7?=50ufDRvPek`Ss<$>g=R`u(@Hk&YeYI0FCWS>rRqqL ziMv_$P)9C3{R}<-cYP@>&h5s)wo=$jDULV+PlH6DH*V(ku5A%4v|w~?zL0h+Ln+y< z_JLA2=$V_&%E0>>deg4UqhUPJMG&!S+I@51#WA$-P{m3OI0b+#{^_n8q8r<+4@XXp zD@V#nL}BW^KYc7%NSP&PSzN_~XJkLpSjg{+PFTn|czLLMd=3o1-Z0EygE}c>3T>~* zzM5ycuyhVU*Vfo1)FCP=w!}Eb>NfY+v+>_e%xurImCnB}YjnK1n7fqjXX=L-Zc*?p z_4sP3%dbK@rAk9NG0s&>YMXhxV_P%7v_)K3i}9Ednd@Q^62m?`zUlLwspG#!K6p7w z{ou%@$~QJfab&L@e8#?*)~qPFq?dG(*-QMkl%ibF9=`}gTpD>ZKjl3b2HIC|u{V1z zag?g?2Y7@FtAlyk)NUig##~5j8u*#{Zcb6^&DPwL`Od`=IwS$Viu%J>$#zrVC_Tg5 zhVmVD?P@F6E*;0B`m3=OqsP3O>Yd{IEOq4N_4nkD{+6m_=2UI|dDXwKeoFPA(Jv~( zHmqphlGphAHrg}or0&a|i`%TJbv`Ys{0z8>q+XP9td78pWjw0wC>cNLAjJrb%JgOc zk2)3$#R1-O5xi>C!&SgN<8Khm)$xBzgd7+`0?2lo%Ta1Y1^D^9BCLw)Ad`CzdWi+J z^P%?d475HU8b4pnIsZ%Ofcp(`6Wv4PO5%E_?6!{sYMDnsH~{4bE$i07Xg zcn?XjRRr-fNO~ERBtkSK#1%!ptday*j3Br@M7G;>j?y2bCJ!zHwN9ozs4f);Fzvhh zpwe!c{*Q;&LACJE_F;}~zK<3=@kFFidekfFpCOMb-kD#_Q0=~Sy8qmG{}XcTt8(c> zJ64t4Tu6PJ* zw)+&0vMMSN-H4|Q@|u>`ocp`7)~!=lVU5*+hmmK!msIU}{#9Yy!S1J1rth_IoBmWM z?(!*J>jW!xo4RRZmuFU;-K259a^m;B7rN|*+;TjqtfJ4@Ra8WtGTbsiecFi^+97Vi zC~&&@SldfOzYOu%_t?$r!6w45CAm=Redy^$I_LIiuB^Ft_veG}pFXwUN!J>oAwB+S zz$|Mf^!Q!W26j4{M`}tgR}*&NXt<}sPcX=apD6gK75;4kpe@v7^@c4m?#zINtueL3B>( zTVkgWqR{RPQb_dVid?|uEPEIj3rTC-BFPc=%;@eBl0t}N>H*QXqqFIzFH%9ExOd%- zhk6i6li%o)C%UQTAEDP5a(IOS)^+~VWedFfk$@2EqhSc7@U?^~elVcQMKYXN6&|T1 zr}}fE@XF36o_*T4&AMDOp*Fbd8>5y~m^Au2_{X!I@M#*!*?oa;pO0(04rWU_93>-f@Xx6e6 z3!fN^!Dmedq|48lc=#+i8dmO5@5M*$Fi~MO0%gaa#uTAN;-NB)3F|+FBrqg^|MDfp zA{LWEk+g#lgWMRZ5ULC*WJ~}VC+~J>41OuJyNw1Gf7PM0&EfKVn0DB&UVY=DBGYQkv;Yv zSYvJ)Z04P-QoHHl%Ydiq1-_MyWh&LaL*DPYw_;aJ+@p5AWOq|WURTT5M#4k&Np!>= zwsOa`QCC-&zC2;S{>}OBzTxRFN`6eXhBcF=!dD@w+1P*-UKZz2q9_3kgG&tT`FO2{ z__FMEwwOlmKv|hVB+{D<3Qn}%0}q&zJZRBs)O&>^r&1Q)Ho?qNx{qC9Y`wOM(P~_$J0tpYzr)oIkh(T-qwR3lu7| z)uBhx;4LX21RVgIa3Oe+uyW27QoVD7ow!xSftHMD6|&+SF&uXpNl>(wqy>}#w682z zQiQGJKS3i9TrBwS1(yBcfKcdOy+qmWwH)PfsJ*W+cDUa3^NCvRb3+e*e={8EKiFiE z;usyI(mrzc+L}9$UG|be4`xrc8)|WP!mfFip$22-U(}PHzG>B&B<7;NoJS#BHjJLo zV1NI|IpXGx?m4qZG^HdbygK&l_7A1Rg9l#vm)$%sy}?=cTUOZDWmA3~$Xzefetdj# z;)}G3QO9b2WXD|E9JxHg=WG@_^?a6E`_G>&#U;;v5AXZl^hP(iq)}nG`31SW@mWDn zcUDFxA+(3u&vTj2kGhNp4pO>p6$OW3=a;tbvG3TjIe|#_NIJ(BZe7z6YCYOz$VT+} z*>m{SqTYtonXnHpNs-j(Asl&PYfIzGqbU(iE(+EiF)r`38{t^Oc_#3xOcoQf1hBwb zh~a#Iw}^t}N|z7n!Zd2Q%A647?B zfq_By0nCqzfLQLhG(VAI;e-}$3p*tl17?S^P=G|!QmZSQMXC~rE_P1n`IhM}geg+8 zG$$dZ?Xo~4aZJ@?e zpDU0vG`A-ohNlF+p9&|D$e{XqDunP+H*x}nY&ScO@@!Ot=Lu>Xffmyn_1gPY7+VG& zjx^}=b!OP+XBVB${kgnU*RY~dKS__V@!pV)OEz;hELhtjs}%?zIy7K)zMBgh%)ZE2 z6uw|<%CRt?4NKndBC1xa$=TtLPQY|Q{3`@!zUVjn>t1C>9EfF7xIIdU*lyP-Mha0| zo)G&-E-m&oCM?eShYn_~`X78KyBA3u1gVp_Mz&`hM>!U$^%K0iem-TZ*V``-s~r>X zygk?Hm;U)JW5ig8)qz*+TI|yyKy8((Y!O}k;1i_Ml+R~4I>I)6#E&|<9`)3 zj46y&97}qixN&&&l;`6gr5p?O^tc-K{>eM_4C{z3gT}7Ry&E6lzFyzb=&`rag>P4IOS~rE9C9rQw0* z5J-68QlWTN-!!MShpVG5 z^odYs-Q($fUFG*1t++ap6~1udgQ_!=l2sP{H2cPb^&*vkWy5YA{OFC=zX~$?-e=dv zc;}}Za-M!qJiTN`H5z@QX#>}G*9N6rw(6l9YE!~cyUXjT9u9CfVZ9zL>yHZou14>P z1YEPkvMoF)=1*Ir7D`8$tOEr9_XhA9=&fN6U|=3ra$%dmi4C_8_T?GYa~WqF>78EY zDgmh-2`Pf38qIFsvOw14m{%<-f&F2JaTtI8Y@TeO##x7np)IAB3z5E}vfD0_M^|Ux zEv}kj#IjRAvn-n7u(u%XRxH{3;(S_KRe~?{Crk>ivaoSRYdN&?`lENVP?f4WQv;?!*f;MVFnmxmG08Z~lO>WF>w_LoTeZ z+%qSsXn{^=;iqy$-{@xs#slG9gB2Y70?kMrwEyLT`CN+DXoPB$?|IrzyoG5A*>*Idk(zJj+vFknj7{(&qQ0zb;M@D7cTTMwL~Apr zjwEVd_sde{1D5pcP7*Rn z*eW8wWExD8f*sBdAK0ItL!u=lN+({dBhx@ZdZz;^)FXavPmdQ-Qb`SHqgv*K;Fs#- zX0LbEEf~^#m_D)1q+&YbW6=RE*LTS=)8R;UmO9HOXU~!wffaYt*9U$4b8BGW;O~8` zxJduu^^jJ-lO~a!+w-TU*D;|v)W!R=!CAZSu}Xm$gZoohwpWkEoWS26s0$% zju`cI)ca!#tuAmTPxy1E_2*5)kFEQihQ23n+Kzwg>1S__6n1*UE%}x}f{W-LHhoVz zguvb%ed8ExqPk`~bP$buP;2ve;897v9)9c{3#x?=;IGPzChcK1PdmJHIE>^_r-7Z%`;Oj8L;Rj6DaU@7uz=j8k;v}hlLHFo&XJD2C zF5H_&IED|iSX9gfMTEygZgKVOMW~xs#Ht~P-n9d zP*ps@{y)OL1CXlsef&N5UasxhBXr3sTeMKe3`vwq8>x^=ltklPQHVkdt(#G3X=|vP zB$Y~vc3n+v6`G&=Kj+*@eSiPQ^}f$K?|I*I-ub-aNIE@24MB2*1gQYeiZl&3F_Fq~ zTyQv+B$JYQeQxkbj{;@cwHrwU`d;)V>pU*SggOjnBW!l8OV2?L@4srlI%N_QILn~_ z?ga^WoOPH$hp}1Un^p;ojbx|SoB&>l`)Q8KCgdOsmx)>q8;2&5s!#aDcze<`(kc+*?v?eOex+%~xV7asP_ z`B2l|E#>jl0w!)2>)0^%+ovn(K zCP_g?>7k2-Gg(yUy$4r{=!XjlW~mgHLYp+s{|c2hU4mIppnZwxt=og{Pw4Q(423YH z3>g%FTw?tf^vnVDj%Di%Ae+bu^)Q||vzNG^h3TRMM$63ZgO`jX;X z#o?Q^2rbLImfD_~q0F-bHZ$Vd7~||ZZ)9F8>dN09?73#A{6VEvx28F?oipmr`}6hA z!FbR8ZCfqli51W;^|bMKX67OWzVYSNe8{fQ-oQjvNmIFc?LO$o4xI1R`;bf|HH1|P z2>UCE6vK!R@(0AptAg9JkGc3F6D zeowmRW^FpIio%0fg+KyKjGr;9SVNKv}rdxnX)Q2snrIV#6du<)!yq?c=c ze29(O-eSh;>+>%2zk2Lb;XJwX(8aU~$4{NNnX|gl*;w1R|5{h4ZDD`H)Tf|%d1#IJy?}SPDz9AN&x;Oa-I|=(^ZTC+IzMo^+NT{v9A$^} zlthBUtn&x~$4wzHe&Mw!#4Orc%3zK&LCk4;FAK69)MsQ133UkXYC(fQJsGk>^{*Fu z8qktMxXUvlfDrKQc>+I5;E`@yi@k)9!dj$o(!eGFK;%lt3u*}wkMtQ^h8)AC8Gqpp zY$0nW?=P7&Lw03hEX;j;^!%VGTibvkLH9OK?X+^-aE#Yse?OO{y?RNRztMos z!_6P~73<&ob;mAQU+XjN%kZ2^>)jDUYl0YkKFW5k`}0k=F{$sM%e+;z{?*|NJiecm zFU?eZlC-+3ap8Xf>DG>S9|T`LxAJ}6jQ#%Vc!IC#wVf-^{bx1kiN$*JA93DkJ1fw^ zGvCeEd=B_A`oB3{CqnLQUBG)|=fqo*dNfqQf%h}FfV5YjL689KkXIlN`9-eAk?xyi z8GKsSsk2vhjl}z&^0>1IMa+1)cB+Ff7gn^x(Y$aKqI*H-qKZm+xIN|l<&bt0c%(+F z!w~(4Pzuj2d^LPmHHHTaY>wob5L|*wVN0cO3F<*WJCF{M7~TH_(mYIvB>H*;wE_Gh z3`^h)m9dv};U@Z^rw+MYG?n|d@;9lkF9##5Fzo12(ZxOe-Q>s%sty5HjZ3a>ROX{4 zLvYIw0grM?!;V2&ahQl@2Hy~Q7gj!R($|4r?T^zIQEe$2_6~ZN+eAA5v_N7<$$jA- zTP?*}$f$7`Nmz1WU5_XtB%^a1AJ&f(ZaTJ&UOVWAVNas2J&gA}2xTi5S^MX{u(IdB zd7w8P2S*I@R$>%;5GKaSu0(&1%3ZXOJW5A!W~2G_Se?!f@n!w}$LXv%bG7M!LD$>h zpY;vR^IuZwy>Wo&c%Ne}tACQEArBk4=RKW_SXtkU7CO*Y;5 z57us?a7RJL%ez2_JZZz0nlgnq1MdX>`@RW+m%x>Zh!9mB@}lPiCf42w=Vgt{*$D4Y z>hTEF={V_k3JB`k-ZV|=N?Rl#eSwu+nj?*q+wV!>;7@qSNU=>tz5}S9VU@UV=fKvz zLlUdF?Wl2Gx?%a7?T?$zvsrZ@=m;-XUq|(fbJE7z37s#8TstyOYrc76_OX6ZADl;) zrk%O@SVmpPs;uw3_UE^)2JXE*>1tc^(?}xh1k*E?m<2sa=tPmRpm*!dk9?1lxt8fv zr(Pb0Hf=k++MMcwn>#j_XQMqmmO?E67e|ji94wOg&K89@ zY?9{QfILi2sYwBOT$&Kb{+IHRP#8c=5V3^56EvaAJIFD!7@E1dVPc{OB7RU~aUtJW zv=Ia=)J3h&i@(alO#}e_uiPRL0Y0oxk~MQgu>X1Tn%Wi*HHnBmnTO~d-;nGk2~U8L zMOpyNV#m(Wmb4etdBhvB1`+@L848=iMyjJ!<3!vZzDXG5vdsdP^i-EitVB%}8lE2e zjRd0A)6F5`y^4j@WDk=o)w3;ef5A~zklG>QYreos=M%SX&kSCE{o^{UtKZg>^-sSZ z=*l^~Ac*NPEqa+(+|P;H+vk)AZO=}e`bNpWz(IlcBT%DH>MCW8>0?#O-QII=#)YmQ ze8}+54~zGT1Gd)R+H>3}UgqA3tCmAgx!P_2GkcBIlN*V*y~q>914J{G;&dr(&gBXS zIi8qFA;Je?9hxD{r$xe_6IeeUgwb(mWuf|)77Ot{;(X9cD7&Y9v$@n2GkGF~3v4+w zH1~#{x#M&Zi~LH}zd#n)iJntB;C=MLk^gdkvC@sW=VDw6`Jw1%4MP-wKLi~rrL$~; zq9o4XJ;N**q={X6>zqUk=gf$&aI>^!2fqzNL-Mu`KYakT+VL8VX>^`hiM?3A#65|l zs!txR$JFc~S6bQk{~ct^h&OS3wBeKN%_V=9>@!JgTjJz$;cvmy5vdQEqQR!Q*Q(0RMzl3{mE{h?kOPMP6D4i1YTtc3aTFz@d-xg=hRHZeHq+Zl zAQbBN3tq!$)2eV_-@P&#OjZ|;D!`8)cjhW1%az<0B@!?fqRbS`PW zq0@vH_VPNr#&O<@yVYh)c*d?fN(oexYlM#}R`WhF*<-8oeWtaJ@H&~`d9Y@`bqF#X zTz?*$1#eFb*h~W6>IGktap_rzY!K!$i1zW~U}7PE!Wyj{#dD0o%vUZ|GXCwPEmw_8 zFBbH9RlUZZ@||GloQI2pB5RRUG1U`^3+YKCR)09-o2ClA@~8Q#xZ(g7Poyx=v}VFL z$ale4A4102zZq@V!?2WR-3+k|u(P%XHRuzLHZPifPd@M3>U-A?dl#+w+}CT=WD9%7 z592cqD;aewncF=x^h8av+MFxi-ptjCJnWh`a|Z*g||W~ zn{+y6&f zXPEKP#|ycq)Y&7}!pDM%-{x>f+D79nNCX_j-3gf&l^o)zx}rl;M+M{LQNbEv#S?30 zY%m=(Yun(twg`iFMg7Pqm$rPHDpRWbM7hXRx=(dXI6%%*LknSqNAJk#f+`o zkKHIruq9030ONs_DRL0p5=tYc2^E%1Hp2G7q_xpmoZKP_5Aisd=g7Xx{{+`6Su&ks zkR@PkEniFu5w*oqBS0a~2a4Lip%eOv!>ysHwpq>U1>C3ggPQNX^(Qykg)Ls7^y_ z-VqGBz0!Z=NE?3Q>Xf`Gj3=4|eh>Gb2C~OQ4;`2j z;r3z(Nb)N%)Fem`sgdvV9<-1c%>5TviYFaCdYC|BuyB`i0{A!S^oFjkC-I7smkkHj@8 zm&AHOKU|stBd-4sNIxD>W%eV)7pQQB=t8J#$sQ#I?jl6uvwTssf`mjmJm}`p3Wf#| zuyU#ZE6gHPB+?R(uN>7CC|G3A)R(gv@g@Gnkbwp39t^(`Zo2YH`o@#pr#x9j&8N1O zcj_jsj{ANvNr5}2DZ=T^q|y98<37(k6gKegsONk3{ds@;{W+gwiJe=XD2&-UT4kn8 z#rOWz6U;YU>M(RJDu32bt-ycGS`W+MF>aI6U+*{adC6EWm(RDVPnX+M&~RkqeXh)n zc*k+odH?-9?2z^0zsG9|J;%I#HZS&%Qq*j;JXl9|;j3Wxg7O+z2Zd0OC=op@bpj?P z?8eD7NR1@Y$(4~p^PrpPV#P4}YQjo*z=2c`8!8Ma)MTAD%AvFGY$=J?WE$QR0=Z;L45qjGAm$F9 zkWYF#({PSCQPg;`#(`V365E-z$fs=(7%*{VN_pp<(#QuTtC=Jc@2kKD=FJ@XCY z>=CnrCHv;Y=h;=tLv_X3LpdHyEf7R2yiA&82NqP&vK-a*WCblJFo;&OcTQ3%I@C4b z#o4l3MPpQcAMG<}u}kXx?H8AyAF#6_=t@+q`*hl;&e_HCF8j5HXz$VZytMCZg`tO; zx;GZtqqWCcWcQ{`U1*njG5*1Vh~uq<4K+sxF7&%ZX|UvqFp)3LiXw)-2>F-qrsm7- z&1Vw5IZ}^EN@T~AA|z-Dev6~ZMW1Aa!)M)U>+4lo%9CTin?`gU2>zwuP4hRiJGnn0 z_sF`vD~yJny8fAcTJg}(!H0h)M|VDC6VUO6h6q}4JqWZ&60itU6ux2+E%EIL3H~?6 zxcxT|{cGC`rAAeWdOXo3CGI&K)hco`ny~%#wb8LOTW}6`POde4IVaoi$Lyb19&D*y zb~d{xruCviV#71DC$`@v$4x&KIChVwS^xI4S8DaPKbHHM**#K@e3uE7xi1HYJHRUv zzfsW*1%7c)Qi>9v>@4BY?_7ByMYT(CX)Fuo9zvxV0-Wrl{7>lF?mz?8MA;u?A5LXM z*J_YC>Yuv%yRaAw0>J{nr{(kxkq$dE5ZT4imM-2heVcV{u7K{;++G(j?F9@YQ}87= z59kl!*Ep)@(I**rAI~AP=vLdx^0o9%uN+WusBXum_6cP_8Ov7PZDl;I$Y1zj%j1aI zeO|q>ezu+mS<8;fU1Y%2y!=0M4LNH49{_78|C0PY!O%kmrf5WETmGCmUPBm}kO>+)>_uSx&p9IBom0V=WqkI5qZ9qQ4Qh9scY4r_{532zp_Z zRps`TKjdmj9l1Ugeo!+_{`Xy^Z;3$(>ka?zQILnsN`vfQt%j(33$-)_!rE7S5j_m5 z5NNo1!fd&oG%aBl-jmaV@`KxA4n0_q6T^BjfP{%4vj5x*N_+-$RG*?l+VF%!HN$8p zX1!cvvf^yOs^dFgwog>2d-9R)n(8fK#kB`d@0r2&TNE8WUM(fmyrB zM-DOdPE9|%@3aHP?Q&AqJw5Pt%$3>ioO5c@TP?cYd*AQZZ+T2o)A0Xx?cCHHePM0C zYcEhTZQS}6f8r)XWFAe6eAm|jd^sXSvP#+YVsJaSBZqogH3(HTff1EGib#y^DyBh zl);g%8Gs9D+8M)d7e=MML}LiGj+M5uGD8b}c+5|~5N$+*Kx1ahJ6_w(+6^ndckJXR zoIE;^d-H>gE*?Jp$c68Y#)iYRpxAzjeHw)rt)`gyvFD4Q?0WhppaWs!tMF}Ue zY}ES99ib>|nI=JyDwv$?wWO3abG9K74UT;m-!oCTkdI z_C3@X<bROuKZ9; zB|@{K@WOc=VU3H=u1A>6*3zt>#sZbm+9yo#_) za{}p-^_@!<6sHf@(J$Bl$;Ng$d84S6V9JSc!qxnBMsSvX9&iR}E?Zu@oC;PLP1-dm zZEh!@)J`c_Tr_kOt;R%wNB4q|kQ@|O5==Pb7^u(OrJFCzYSx8OG)usSkk>)t+vD_k zA_ja&7MI|F89YF=?XfaLeWsMl4mB{Pg{@%IHY8+>UZA4IDodNN3g{95=w zVg4G<1R9G#Ps?6IrcDoN0{f1}W1*_rGzZ!-qU729RN9w8-a3P*tonf#jzxfg3r?L0-CCn$?YU&Bi&yeg+pdyC3tcd?Ni78D6H*NQ_>Creh> z;QoRu+wsw%uM-jNOqK(|#8QyLA$U#F@UQAzT8^-Kcp*wNE)xPS9LCxF8@hVSh6Wv+ zuQq>UXs2D1(UBmVvioE0EY7f6aqUn>19lw|kUR7;`*L~45t;*2C&;*A^?CdGg;i^h zmKHkB;+}o+$7JoxEMh`D3fO%evD9OS(FRmuH)nSdDDyeO;e~6(ldBN~U^w3u5H^ti zG7tQ{J`RfsVqqnrg~;Uegt*3?8m%{XeZc0b3@1Z_g(NhCN3*QQa^Eapb-b!g{oDJ3 zhcep|V+LMw2@Ak)SM=W!W_q*o{u%nA&C_W0hXYS82^*dBLb&P^(=}F#YE4n#>|Ijs zR!!_bcxG_%ASfRi=DR(3nm5+Wf3B4zb3KkZANm>|1{~Ed=##2w;a!rAeV^5l``=cd zcYWYs7j)$A`xAEu7d?o&D67*)0{BJKmB)HeD1ex@AabM2HOd-DXiF~W$P`JT;5r( zy*R! zP%P&F4$t85^_!(@A~a)S9ooA}EgQ%BTdbOvT`=j{`1#ATcqbM;H5sevzVUFhv7JuMp^05i zw_Nq&$+uz)y*tsZ!9Od7|Bp44^bmngc)H|$39q6j_qd!QsK+c|!(dX9P70kx38>v| z0+s*OZVIHm!^eg;aRriIE-gdMFo$|d4D=%ek}-##0=7?G%#=c82?i z>4w#q9C{zv@;18tjZyCY+`wB_m6K&gD&^l_hy9-NmE}D5+~CuXuir`ErNU+FXyePQ zn`14uy?pgfQ~&#wO&Y$Ib1md(vlKMg z$bk(if_DP>kZF@Tg^gqmq7URmp9eu*E_T39CxKW(f>RiMWMl13zB5J5rUhHV;fK zmSe{*Y4ZCWw`G9emceGBbM3HwZpp(U#s)hs9(CfX={Fv=R9|rJCR#KTLjKQm;tnk} z=&bBDZ~E!W6|9XPjm}OaK=?PbU38qWLdYJLe`M$swo2iJO>#>vtjiU0vF^GV=4CZw ztvgWi(U@t0voIcdH*{XyXwyZy213?m_kKo+32->R$g*n#hGM3_wvnHiJvzo$0P}=w;G|tnpO>5zTmzDehuw zwJmx|M#Zp^Gp3Xybgyj#AvD@R$c!bFrd+R@w8VWahh>KH;FMX-PHK2)kzlGbnEKMy z&TYOWhe9@fCSJA51!P#3ZCpP(`EXe34F8u=h4VMqd^6cU(oJTgV$-%`IR|LD!Qh$>(Rb0lo6d+-V<{e`Wlvc{aoAh;W~^A7np55byuP6`Twaf&WWI zjSD$v`7?qZh5&j5MizvB0BIg{Gv&j=lwG()Oi0kbFtWulA;Kjp3VyKI2$*o|8YE93 z#iyUxBn5z(kghBgoWCI)#BgXerkRZQ5W--k$AJi>u*99gVGR-t*58rFhbHUUEZt_y zBA3b0hh2uab`N`dFMdb+P_2mtmVVdd&Od$p@LtnN`<;QO52uDbQvPrIfvyF4WJM13 zu?`s^33SK{J>cK*6sip7QixbbRLA!SL5i?#!lkHxpgAN@+_m_Jf`G;33Va5N@qT(#Ph zu2@HN9*az zVvIUn=1rSbbZmM;WJ^KV+H;v6KP!Cpt?M3L-wtdVwCR(;<9FaN#TA-goP>}MWqra3)_zEOOu`-H@OHlI`vj^O#^ zG_jUil?S9^TNoQ%-}~?Acb_|kK1tKp6gh8e+>M+2l{PV&;a)nf`<{8Qmm>Pxc@_!> zk1x6xhm;*XoYIa7qXGFqNsOW>X+px5B}BL~f`0tpV4fPejonme`ZN;Sh^fcn`xrqR zO8oL;t|Om<4F_a%L&GZ!y)LiE5t<8U&p2_l%i&fX$bf)#qK1jDaHRX+WEzp;oKwqO>}=3r(7nj0!AB>NcrzvCmaZ;}yD*Dn@E znRd_7)3UGv#!%EW3TasP zg0<`~*u)dFsJuP$F4PbeB#&G#BLKz#yc&_Q5`tuvp+bzut}#A`_90ADBqYZR+c~U> zNR9k5nHEj z{P5iAhtu6>?>TvcKjO=n)h&5Z(^4jm`wxIYg5Z#n_`l zAD@^br^%0^OqvI#;G3y4HS$GKCu?YDTto?cnhYCOr(ehzz?SBB3W%PQ4RR%x-7Hd| zA3^AQs^!Z_uE5-_^jk_TnSM8wBex-%C5sI*s>%`TD5yaP$ui*BC^1KXkYAl3ceFXh zyBPEm!PzqSUo4WL$d|CGI7UfmzklU9y_=Ets&!zmt1;YQNfuj>)Bq4XVGK@WmV z8b<{J4@nuRJ;bK5DN^V_W}!irg+z!zmYE~SbwY-{&{xbwYk49iX`dQ-NHcoikOJ`; zPS`~lm?K%ZU)I6#Mi8;g#IZih`C@%WyvK!cQ}+8lMOx`HzHmVHjXs+O)5naCJ~=a*nfLasj?%4m@!hURG{-uJhyEEX zzhll+i`#o<`en_!<7#Me$fYaNXy(d$FN~9?&pvy*RWq-Oac@SBN2$KVyzAlwmR6$W{P@NKrtv{?8cT{ zwLRL_TOAhZwkZs{n9+`Fr}pt$f2rx?l4JH)w@0e$^B^b^)E7NSN zYIP!@C!HF*r%b9{t6<7U*zW1@K0kiM;yA?h0dvHGLLG!TzO9m6O1F$@Mzp7c zMp&7@5BkyR$hGfb(17Od{+vW3`55||kN1#vqh8?D12dac+5>rlAw>>#3#z0U0he@3@aG&>61n|FPK!bI0J#>ExU3mZo;Ym*eWq8)5_^Iu1Nkg1 zNX!I8(GN$o8nC-S+Iv>tIy`AT3NP^-!eM2Sb0&h8OkXZ8F~hs^RucE9(zOF=C&y+f zk9jy}qx-i49j93*?=D6%D;7VRO&{p;X>i~*_Sys8b2=YJ74_@ezcDJ#c2$M(zC!h* zX3c|)I>%kezGr1%JbKL0gFe+RFV>}8E70s8&A+0wc+RnRik02jIqUO`e{F9xQRg0T zF&^Lzmf(Fc%LzBkw8I+(bf4XTDx8r+ii~AjLg%yX;#*#&~XZ` zJZ*~~mZZj|^l4gULFMD`a1U}Ztqr%d&R^&}W%1o4bDKU|dRVOpr*ufC&ofl(j?Z+c zulsqjdCnul0UdKL1^LSn$Z_Y0N2fmC{?NVxYcSxn2rp1|BA$jKl}$IDTQ^T71Sb`d zg_Z<}ESXz-<#tjM9y`01ETlpxBSe^#&)j`{QHpcfLe0|CG84Ql(cP}`>1L~StLPk1 zBI}Zc-tDigmlhU2nLq5eNoHNY`H40(@NR`enout3s39g7co8@_Vwkt>O(%yy0}Jv_kq@yK$hJK&%a-umaq1(e@V!+D z4-6W+F(rhy%ra9*H*j*%?OKmZOOe6p_8_Dl*Mo;jy!LZgMdV5-!NKD(Xx(*|>snc_ zxhBqS12eu)`1N~|(fTjh%1ibHrzc;Jj=AsISzo+++0{4mW;*pfFT&G*LUkU>>WEZcU`{^d8N*yN-dO}?i^8{IBm193VZtKb-!kZvkX#n zAK_vCN)2cxhTug*(}QGEjD5;Yts-5~x&()Pu$4n&`}(XQJV6rmL+l>zv5Pyuu`F>? z$jop@uu5v&HYz+8;8PU*W@PrYqj~Y|O4VG1PDde`&GC~iCt-i|Q#BWIOan6m=cjKr zqOA-!Q5+xIadOHfjLweDo(zi!j$=;r>UZ=Te0Cg4*!$l|4cj}8{N|OH6gMfieF^jL z(^98E`)x2uUS{9~hh1^W=AHbSnDGB%dxrWSe)e%Wu+|>=h{2W=3A&O z>Z5V;`%(F`zh(F7!DP~Ve(*yBBzMzERHQKawA%XTEJSzJnlE!&?PlRTr(HqNTxGYH zBl`A^%-0aDKWs=d77oi~`go?vsPEEc0_E`g5KDDfHuV!3pLm4OY61wXaO;W4bNNnb$ff&V&g5 zbm)Ue6QzVuQ<35MlQzsD+L^Vq1l0d9?U4Mqt=G+0+NlrkGHGhDD@u(*j(g!`htQn3 zr8^(3VLwoNy_yqz_m-_D^BR}_dhTNR+6lMXh$U518qDT=lHbS;m_t9!qEA0d*R(n9 zF+%CsRs7hDZyB42J_|gR3kVday_39kSn9nbSv_9c<19>@Z5$k0H=hv3NaD@a+}Cfk z(;xP|xWV>fF4=~UInKCkHz}>ftikNCSO6!~v=DG$$ZMVyONr(eNKXGrDc(IIgBc* zpkEVf$=8!lly67a*1U#^ZmCJTZ?C{PrgwiR^;;BhYcXOY=3!mL_#wlngJ7|6n%%>6 zzsMChbq_Mj=@HPMH1`LW{KQ~yfKMQ@D#=eE3RcAGh6+A`C~tV}QDUX1lB55H?$58U zg@xpt#4!k-`jKIj)N)v-(M=h+jz@NcLamR6$}RZVHet)cbvs97uH2tBUE$;9^S9;Q zbxg8utRAg&K=rZDr87OlILcu)A~o{ff-vl4u>`-T!mhlkR%!WW`|q$J@3m zzc3uvyFC9hVPa`+|iK*e|RQ~JCg?=q**ROi~`Q74UaQl1q{NP==(K*(^>G2a=y?qk4 zRX$34IQ4tEvaEw`LFKu@>MLf_X9FHO$$xwmLUn*01YGuJzEBHyon#L-mTH*lI& z7BZtMYU#c>`GIU{pjo(#wlvvpR!~{bKmj_1`ti9=TK8{;vG~xEQ;JH}p+`(_@ewU; zxEC;hW`C`Y(B~4sp?PvrSvbZ9l{nAIPuG)(z()T2& z$a_20z;DP%ZbOwgVa2sLvNRy98G2E&_~f#eo_hUACI z4y;Yc<{6%>mJLN$n3?yO7xFYUR}hTx+nrP35v&PFCj^}ya*cz=dDv|2DPg}2%}vuElK6()~n8gJkKRKCrSti_wEe_g+D%l$oO-Y+++dPYm8 zr7Dr*XLR0Y!W2`!AUp=#KxtSgfw1#*$F>#vu)#XWPrt9u8v3wUOa*kRpwg=Naz=dj z9takhQ{O;+2YH>3JyMA>$i&?B&vGp;tIl z#MJQAHL@s$t;nGrFrmu7r_eXfF^3RHEiC0H!gSN2F}n#y|8}~?OB^c7qsE0QFvBmr z9!&F1JUGLvc!K{Y0xO=wc@MAJ&=K~_k(m-6lgerfuIiEd0rIY+43+a&9ey=Mmlj!M zlXjC&uW~4?nRcS#4|G&6jvfM^sC|g6JlwMnKzJlDnqb{Xn)&N`)ADh8xw}%sQzL^k zk(w#~Qvp*o-+)t@QJ@O0%CRr={yN;7l)}pq1I9&4Nf5#0UGD8dCY2JS@n>S-XujWX zmUQkVK!Ck2o4A8SARX{u;!fwV-jheq3x}1}7B#+_>+ik(dD!Jvbh63eM}-esY`3f* z`XhT=#rDM&*GKB$F;h0sSG^j4A|SAV`Ro0m1v$LCO^Z|Cn7sIUy?QDBb>^`+*+-_r zfuuKeCkBI{e4&Gdk;rW!_5bmxOb0qT?0+bpAT*A7@iu?&>fe?f_-C5QDJ z-P93O>|mdo9NXdkW9Yiu?`Ev+*17e5@3flet*axRIe2ZD*12O)!O?XN8iOZYvc+a~ z?WuWlXYuJJ^?qL!oBLePxV((peQ%)dsbxb|C(u8p2accdsnzj(!43PIA&txSRA1Xy z{Y3hOy9%NShKY$Kv8N2@v`8Gacc=9L(cwLGbxRfp|SfG6q()181fizm;UdvJID>%fC zoP5;_vmVOopD^jV{hG_)UoFg;u;1Y4t_|<9TCW`*X1Zdd^+CqlRGZI9qg>ehN4K1{ zRX-d4Xb3R6YGtkxpJ-}pn;0S<@3w$aQ zyCK>*iljH+6R^eZ0$hv=krMn4N6j8Nkjvf4?TotBHap9;bmpE~y54Tipo9$uOcPDz z3Pqp#dGhzCdvbl8t^`)L_(%9GdiZ3R&9_xwYw~eOIfl?wVSuVQ=GXfsy=d9L-jTq{ zBg~DI_PqRR?c{_!&c?JpN{%Y?cT9zeN;c@`FuI|AD*1fJQ=JEgq=G# zJ-0gdCNM5(vR~um!r0OzQ$|p3fj?aj`kJBEqfuepHc%Y}ogJvWk!-FgK+-4Qx`7io zSCA}^b_%EQ07vu~@K`vBlAy?^yq=C~(uFO{M~uV~+5!KGIC<836g?85nT@kul2h3I zk3J!dp|SfIO?36QvH_t`PkdoFqfp;~Q+R&8JtF`**jNHIBZ5j_y8$A3nSdsd#8BT^ z;8Y0<5bfPRO_iXbkMvCnc!2Z-`eS=Ut4**LJSG&{d7xEKTaO&OI1rSk+@c^DZu4PQ zFwL?9bx=G(W>ho~bS{;Hnum0{sKlSoUXZB~_Q^FUiSXEUqkz|UR@V@uO2k8JeOr-* zWCK&n4>s!9&cCTHZ@`i0cB{ zQs7m=zZwo%@gG2sApXG>vt(}wQb979Jg2!l)M>t+-8uX7X?VoHxMDr%uzIiOs0E`u znCjLH(+o4eYWQ=t_8#TrjPA@M43oy$84YdbYg z9`KrC)$O|RvEif4hxxu8<*c+T0i1h6hva+Bl*u-SN2d1Y~tgO99R+gNzR&45y_N zYzS3LGstr!$X9?W`x6(92sB1ExRt^;@2w%lg~VeTD$^Wee!D5H0fQGWMl(Yv+~Om7%BF~TM&C6^BBFz*M~ zTd@7zInV;gG^yaK_E;#Vg(n9wlm=+}dxKMR5m(3~s}K>8iZz6Yf@-2%f=gp$E~Nl7 zq@e+!-PX5fyvg5ve*>Zx)?sR$m!_Whg?@!xVXex=7_6$;Jo&gkx+qc(8{W5KVEwvu z9tuw5Aq5^S`h?=pm9LaE-TGuHHkhh2KIh%FUAn|#qRyRh3iGxgMO$Q0I%^&w>};q7 zKrO}K!L7%dtT;{}?N=Md?Iw_nHby%m^BjE$qar16sEubr8msGT%Z@{w<2)9Z>4US! z5;G^1Tnml-s;IV{dEk%1Sr4dYg&=!*;AjG&ifML@e!1v0Z~Hw@`TD{=YSZM^p$uj7 zU{5vK{9@Cg#^PO$f^KxE=u#;k>wHJQWrZ(Vs{i$AAjQX2Z~wlF?qOl7^7*>wEZOzj z&x3W^j%O?k`BjvRJ8oX&IEK#CD z#u~z?IzK$4Zfa)4dBeR+%cq+es~z&!y741;beTf@S9W&AclY*lGPTZ_lI?Tk3MY3E zT4~jF8BQ`47Bhbn+dc;RR$Mu{@2c+Z&9^qrw=biO@?1JuIf7pnV~r!$;qo9^OLZDw z$9;V^HuhvfV3WDg&d0Mta)=P`+KYajnAs`~*Ufh0%H?)OhAcHowS#=45N0~^rXw!( z2pCVr#P^*r>C(@W^9PwMM_nA=#&iilitt1RD|*Uc}MOE#Yx zs=i|A<8cK`4dSqyGpk_nfDZb=vikn3f3fb~;Q`ArTb0Gq^^1*%`yH=ertVsf^&^Sc?^OIJi#jZI#9Tx-FqlAAV8vR8PZoQW$7;N2a&6dCKh=bYe;3n{;dK2={Bs|f z{O2C`oi5DGYN1sJ&0X7sVS5Q^(^gQs@CB(*<3T0(={td}O*a!TH_3*vvp@nlgpE|( zrc^9n<&X=R;Z6b@4aJ;1=hS6KH{eEq3X698whOQcP@J-D(~z zuqP0f0b#G>!3VrW{ciP}gr?W$%^aTdLYo159iKmr;>vGq=ztjq@=~=1gu#`JqX9hG zywn&=I;+qKK?|;fBl6(3zd;sGG$rV`^zCJNiXGF@w6ROat_qP2_i$#xSC64{G2pLea0D|1tjV(HboE%?oz;>R~shR{qkDWCv5Y?zR-Z_Z#^Bzvon#FD7Z~#(j*lS+X<}*rO9%Km zp!Po?!bBvH?4+em^b+p!QigIMtn&qmbs~L<-{a^OeJ~#p5}7ncFG!Up!9=xxdZS!V zmVkmpNBzYwaUaQ1TO}CDb&|tzU7O_@##}$|vcTy>I|_&qT^AxWMqCZuJvQ+1)7xfq zmK9k%Klar~Yf5^*ov&_lT91d9dm#jG)LSZOD1uTzPnIHjqwpyH1$IPFZU&WS#Ju@p zOn}hKdop04Olud{Bt2y$rAGYCg9rYPS1QZ+TXQh?Zysc5kAobwjc6hH?Lu<#k6wpO zbjL805X-X{20pIH9??{b`^odeHTEq#kSV*oypQdYVLRxDvi;ba-`RU6`;6G|W<<`7lvgdAns+(BD3+fy_wl*zem}i`3|pGJ zR=e;%ZEVM|`Day!z9{lgx4F2iXaavZ+JnfSIFFexh8SD?ft55_y|Oet$#x5_Gqy>T zB5jrkLv&n>#&*NO*%*(cdt@QYnUe~J_2?-(L)~J=N4i4_-9H=^Uk`{mE~ss!cs<_z zDG->|Zxo38LQO+fatOu2=rbSb_smfOC1&y47)YUZ3{yahU3ya#inbXHi$;2P$_(Hx z-bs=)&|&QNH_%=g^?Ud?TgZ`h1tgBiMf79?dyy8LIq!QHh*1PmHsgx80$=ab7if|? zdNB7j7v5qaMS>7}sr?{fmbe>o)JjpF*!tLHaRce@wHZG0-(?5GYh{~Vc~f3qQbNH{Mc{@sVg_J>f?fe!~WoEH$Y z#xuvTs}EmX8>UN{AE*3twptPg^zXtXR*>U~&?s2P4BrcF7tn&-A(BC>j|1MbQWXdn zkOC1{5F)InFQea+7EmSbn>cF6P#$?`2vs-wS|i+S_Ozl1{j-FIat3xLIcvz|)tRI5 zox|VErK!`){T6OY4^Ckv1<$PLm$B1aBDZu*>y%>m=-@9kl zTSG|8b1CHP{}7AVSkGKfu`vVb2ByS)B1f$rEhLYqp$=2Cx4q6YesHYWzW?#pH}skV zqi+X4)=8JOi+K0hs$}%)MdM_Xy>2hQ{iV99yb4|bdOM(MTK{3?|bMx`kpm+ z0$1g6f<|{m)bPGn8{as^RLxx)FnpKA#{n}Fv=5o(^S_ztEVT;LD8d%)c6z>H@pkQv zeGl3+c&XHl({0+bh+M@pSz-ew<8g_Ap@Sqsn2iJ%V{|^eyE9@^h^XI#4LiI50;yCu z^>-C8u?#_P6XQT4Rp<4-grrr3jR!327lXF+1GdY>#R3Y!xNv)(EFsOMjxjYz#-(|Z zRtCw|_YlC?fu!lB_F{JPWJII0#sK|Ikt^l`(n&~DLLBobbIV2YCZz3<0y5`NWgc6E zK|KRtz&oXfQy2_H*I|$7M6AinmU~0yqL%Jq?r+ICd|Cn_R3Llf1st_kg5EfFXtJ+m zVw{<3&Bz(;>P9n8<{CZSIG?w&=57A(eVcD*9Gto<`qrT5JCAoe#n7hIo&T~kG9~fH zueY<$O>z4+?)yx9_D6UA8Q)89jtma_YBO}-^)<5%^@>Jb#F6RVVP(Iqg=1>i#< zO`eDaezF87YhyVcML#Ae@83?}GF&2{s(7rp5;$aT!WM}{CHQ@g+Bft`GS_1Dsz9@@ zY(W1y-m#%iU+pfhz2dc#9=P>w3-?Qi%59;MjgGogQm+itk;~9y4;-$ zj~;419Z|TDFS-$GfCpv6hpIy2AE`g!;J;NzY7;u3pkP#+-g*`IVFEXYcv(6{?zG<=KU;*-*H20&Hkj#L*Ck|I}&?$ zKRP!=ucBtVQ=zZ^$I7-7WFw-FdW=Hq0y7UQG%I^=@#zS9s zEZ*~F*afSjj|MJOG>p}~v%1n|&5mN2>BXT_c)gj> zX|6xCW&Xwa$M&5FNnW^p>h&ihjruc3Th^^Dw__W%c5w#>yz5xmcYWd4z8KH)Li0k- z$sysFPd^!dS$>kHtBi)I`{jnkA*?cfRy7w4i=udurHTA5eaAOd-AK;7AeG(EbD8pUCO)lK$xQOPGf1b) z;B=^4-`l(LZ*ElV%Kw(IHD0O2E6#84o0o%APb5~H+N<7OU9cCmzGQXmRSAEq+J$_W zeV6Q0yd`-Ff?q{ zmyLwz^??SyCop|`(jp2&W?Z`O1MUD@CV??HjXZ|&(%~&x;!lBI`SU=%=#S^hN{?f{ zMR#*M5F08-sK(8Ui3bE4wk&oc!t5*ga6UM{^5UySqqJ(u4lZrb-wIWAFTf<7^j;Ea za>?x~lB^>g=8b*}?_AvxhiD@MQbwDP3s|XOj>cLiXQ_XO2aizq_Ep$ynNx|XL_g>- zO^sZJNRhng?3A9u{$!s&c%lxyFo;fX|h`XbxPt zZVj|X!|T%(wJS((;OX#aG9Dx(*Gu(>WWJvuV3GYS;7g2%q!udK%Td=upJT zHD+sQyZ5MTNDCM0M8S_zKF3iXfELQYR|C#K6Qvt!qfN*y`TFD7oc&1$My`qvnb&_# z!z=8_kovncI`5`dtmlKUo&k7-a@2>BllJ0I>z%pKRyb?G-2px~Jb!zS+?d^_XpNd1 zi@Jxc>iTWC;mLLaDJNLXjmYH5G+Q5@jh3sf3D>RN{Z1v7|ol@9*_L z<=%77JC1;Q`6f#|HYbHc#J`3*4_=j?Fo+gl&nt8 z+_;j%GVE>d(zed@;3Ezz?mYLQPZ~U0jW<0145~cXISP*A}y(rd)N8FbVc#_GCf}p#`G83}`|6fT#CF z9LN&a#dOz^F@XiJf&^Qc1@s(>Jv;|*hm&nc>;zbPUX$@piDJ3|cPo=ESC#d7N{R6r z3uwIna%8y6HGh4wSe?O+21d%+-mr6apX9k4K-I`^^6YE){^?H5vR}h*WfA6Ch)RQ!jP=GWPpQg!tW?Jf^bCh`_ z6=oQ-3Ne=}X({buBRxECG6iqhtlj-sv|c1-_p*rAjlbDMstE(R`%^mG$NI8A zJU+MN+aR)eVz1Xo-g}SfPLh7<2lL~C;tHb&yQ`#F5{Fi?D%fqfYGbC#a-s#=Rj|RG zFD=jtTLJLT=Tm}jVe$C6jQK0f@WvLX=vgUzIE6g_z#nAZD4SzRV93JX0kfWAh5EnY zuZ2f5Vu&McRvxpA`5!_DYO*{{HW7qx1jGLTKlu;-e_-5qF?EZZ%XtaFTf!i~g0b+) zT+;(o0XMLQ2$f0))Dz-18V4AP1M8o^9Zb49kQ*t~KD5`&dZ}2z@~ec^8zrAdhxo1^ z>{n4O#nXVe^SCDCI2rE!K_`zzg&#Qr;D0-Vm_agI~`;E4cO|J zzkr1XKtALI0hYgVes)fU9fe{1X7I{^5QzK*5#OP`Y^q4SG#;{RWR zYRpa5*h6!ZAaL*l>u;=S1Hg#Pd>fYm(_1*?f>nf#Q~`ew%r>i~;Y2~LjT|knsM|En z)1kg%Df<;0;qZv$bX_iT8F+edBL8rF(yE6FkWfwdc=rVT3j<)4CP7*c7dsbTQ0zZnnREYo&BdnhAe8O>-RZh|biCLVhS@!BGm_eiUv- z9-;~y0&B26I1O0)SFNdQAgnokf^A}8al(e6EOBXdfmD0^8yAiSE0*({P+BYsD=aLU ztFZSXPxFIdufh+*X8@~^|G-RE%`#gjeKCu49&*VsRDncn$I)zSAEbUWh^u@mrr2@&MZO`9bNBWA zE-$e@DEvTq?O40Af(+YaGxuY^F{q!!pcOs$9$pKGv5m};b3U>_L(cIq|33y5b1SF< z=dfK%b5@7O&;VP+^!TZ4GqtL!%6ls>%2}^H-ErR=#ys;Mdr@vFRp1IZvEcTN?IUa` zkQ9FVd(#!4eVNK(Wfy0~;X}+PY!Ipd9q{u4ypW9$g%FnX`FPl~orXu>7<|wcySh@K z!+9AlZH4BmiCqRx8q1GevbpOUUEoub9jHHj<<&)HcFnO5d4!D(Z*v(z%$p?)wMT>9{FP$Dg-k@9euyT_Pc|hWzY%#D$b8)4C6p!gM=L{-UuBQ-EgM zil&?wcMAZSQ#$=?HZ)7Lk4DMSGQpRp$JbTNd?Mz;Lrnh=|6&QVj67Iw(`o3x!L#%V zV*j5ITqT1$zF+U!D;fa0-!hVj;>xE8-!z7VOHf9-s;M0^A7tj!21v!;DUF zQ97CZjvET(KBHIy{{k^|j$}1tqIf7j2h5ulXNa*m9?Zc@fA9sE0&M{?kFu=mKrw=o?b#Z68Q)R6C;6$UA{KjqKQQ+9f zL%dx4F2aQUb`EN=Ez3{-x;+B8W8g3WeyrAl_z)w~!l)4^xRB`IE-7*QnRn$Y zMxc0U^DR_PH>MDj7+Q3$10Zi~qhh0nUxF>%mm661iNMU4_>>x{&qFA_rH#U!ZXWq%Mw*=UI{4P0hzY56gFkMg-fG}RS0XEsL2}3&E%|o#sHStubJ;OHXK8Y!V1`SjV~(pRTL$T5=-QRVh?O0h)YDX#a{k{4LF z=JWO*KZ8dtKXgDNgao5&!7(5On}<}|96ZZo*{sJ!T&KLq|pM~Q_PG6!pv99 zIcD)Si^9<$yiZPnN|PWl(bnS}8E`dX)|;^RGWv`dPPa5{{OkIC>Cy*i>SV(DEs_*0 z0GG2|IYSaN^Ds(6%`I8=-s3f}(f$`4=>KwN>058m|084p8>^fU1exvvhocM@L#G9d zIaCQn?Ei~D7e4EAGX7!AlDJ$kbOsIF7sN!K>AlKa@sz8Gu$AxfOF+m27cl`uOq2%g zw1MS06Re0GSkfTBmNUfBpLI367H9V||1dI#R>(w&5&eRpS!rVHJ`}FmT2#lm`S>#yIBU(Yj&L&-9LD;;K``@w!*JZ>Ii?n`6imkg;CYj}{wd02 zDKmc_;T7v|f~2r{rEa?NWI8j*|4)+vwkkv`3|4 z%mo}$f8GMyz`VSLBokkT9sKl;-PJadnuZc(#}q?HTVz#+3dg&cVS8U%j-S&bE9IyqzL955dtH+ zYIOu4Z|R((Jzg%uH?f%YaB%a0FA^!crG?(rhUV?abi^0v5Ab`K2k>EVKCyfrY8GzUBK#X?k*rY5m1?oI#%k$bK zZ$3)`UW9jjlw}N_VfgcXm29)!9-_0a=hfvg#|i6o6f6ZZu!OE#0M!FLKDFQM{#n0I zemgbFOp>j06qlcIa4TTUiNQCASM06^oS8}h*?K*p-8`^p>T2bJNf@~QXb{>Myy%}t zFpQO6WYF<&0WX)_O%?b8f?4P6)oQgvbfG(TEc@SwrH`zzk8sh5l@BzXbds@uH{xkd zYZ!`@6N>w_zQJ%)Q)(lK{%!GD7+3@BKbEs_>=Y_el!?tda>ekIkaS2h1DO4nIwkUK zIQJbN@}HvzX4pvm^la#iho=K>5{7P_p=aHMSs$8z3t+hmoLjSGGIGnQf;cd`z^vv( z*-*g$ApM)l`fcoAnl9{?OpIGbf=QqGk3E%ZRz?*R1pESX!+!KF-h4*)!^h*JKXR`% z;xUxkD1E)}Y|5+8It0gMPp?<)D>eGkkkTEzDbqWi^NB@dbE7H!>Qv-6uN|w{txg|& z^*%b#y}waHYYtwg~q&%O!1s(gM|B=_no{h}9 z>`=1Nkg>tcb2`8E(|sQwyQ0Nk0tqhQEWA;CHWO#R7u!rd58PwkjR2^9$7zjHawKDUW*2J&gGT8Tu?KUCcyCv!s+Kx+obpOU!!+ zZ65uDNI@nt=nYGb#GXq5Gv7+^Ab`>*XOj}2oDOQFNYU7M)TCb?i1q2Z2kN^b1AUXB z&4oGx!FBQ-JkH02-aJywft(X}WS%s=mN>V@$|9OVZJ3Qhh5{8tIV7_7%yRyCZ%-Wo zpdm90g+?n0V`sp$z-9)at$~Lik`*=M9}rBAm%37Th49BfK8kQxTxhgW;TGQ^ge;Ah zfdi-{d9!d7@Bzs$C@Fd2CN^GJxc>uk$?vIx3g84gEVEdTR;*g-D4)>ovl-29mj=In zE0=TMWuTGWFW7it#hpt9edmn#X6|%~e5`^7G0Zq#9K6%gq2KIzIJwVMVPt~Pu{wKY z*q55*7InDn6Q=s#g(dUx`_5zkBG^v?iw%G!#p2(ve=0Ey{_C_-D>yof*>AJJN&|p| z&QVy?phy+e!3KYZILV#cbJZdO#^IkG~e z=J@a=yot0#UU{kJ^H3pniIuBAzPyo&lJ*Mc7=^y%I75Av_i0P=*!Zbpi_dYN+v;Su zHWjhxzMlFXkUuqg!=a{PI&yveoqg)PHU2B^g>2t0{LZ3y+4YgF5sXv-uKaz6*WYsm zhzB1%+!TN~fP;z&pfeu@kqSD%4Rr>thJ5$5Y-eK2Dxis3V0fL1|q z2S_OVfXEyhnv{T;la@Y5|F^&LqLwyoE@%axsDk!R3*_xRmBSX%dlT_|s$Ir96YTr_ zUR4ke)){EAzZcsX1*J9UbYil-qxYS#osYvmSd@M|W?%Sp62;z=iL}vS#IS1c+xxCS z!M>Hg=36M(pc`ChgZ=)qV6-dMnMDC^dYq;45FyRrc(UYX`V5M>;bKoN768w-rMP?D zP(%hU^&g?1s6k?((n$bHwGPoO3P;XNT>M(6Tf%PdmR$|kBM&P2!u`iE2jEG+m)_73 zvBUClo(e$jmzS*otZYW9nyK?$=D;JArPplz^Zmc%^fqNfEkhUS`*se;bf z+Z^&PJ6^6p)PAEX8~W3rmehg$05(k91zUc?5hOzg9bg|U zIR66f>>LNlgGq%t{ci{)&(cUK6OlnNRS_;m2Dng+F|Q$5D^V?Z&%aj)=JPU`53OUm zPzMg6|5TF!7{X3t2G1>-mP=!f_|Ed6hfN3k2jr6PPz8NJ+5CDR+L9F=d}*6}gtnhe zS>-txUAVVHW_OEWk?OJ2r3pG;t$i}`zLuyyd~+j-T7DIOPx+aDvE54p`13(46E+Ng zfMw(l4z)gcA@vuIEdSpym^=Ruu=+bp*1T#DY14!EN0j+;-Q%fHBeWa^S%@kbfz~@K zTXb}6bX06A-pI3)IQfhFE+N#(*5*9E`M~d$L4iolBbP4!q|v?l$>nZBb@jP73>O`% zbK8enX^g`&FWbh7d|B_laFSkfO5S^@{ghMfmi0mJE`)}PwCJtzdqcWi=Ilo-9V9jP^Y*jRFYTjYp(r{>M^Rm0k6 zw-N8;4{v{zWMa?e!t2o=^=az5|LVsAyw?b7sdqN3^cVM&B4lhXi_p6PbzA6(-b6|2 zy^V?1&n!256x67ro@$G#{o$6fw(I-t?GFZ4HYFaX*AB^Hw8yESrhJ957q9hZdac+ z%-Ph^eSafswAUcxqrKKsOrdH@G-Lll3 z5`|ac96E)Qcvp(14w~gpy>fj`EkOX`~;K_Imsy z{8eM%RP+tYTE~y}H#ydCyR;!gYfrlI)-Njp1#=2K*Hn$HI>5K}pyKAiJxi<^S8nKm z&TE@>%Z=>vtkX5gSG`6Aehh?uerdg&=STaXN=w^dA>mg_M82_-+Tr-n9f6-&GW)lE z-nc288X*j1=Ie1>XrNq)~@lK~g+Z;^{9g7594ni^*yF7Lm``-rSkzmhb%;_V{0 z5Rofqxh~)-8(ep*+EU)8@w#dUqk@A%W%o>T8y_B6!T;%+hE6!^>6-^GMywe$NODTr z_*lL2{x`Mwp4H`r54w=3pGVEjv^vtwrmDUecMvjnpV>Sj`#dt+#nL5joTyOCt&-&a zQ0Ueg<61GriAk4pi_g@SeHQ*Dc>VfP9uZg08{$q08> z=B@$bQl;e?EI+f)_#R`cHE8~&wcnR^SU9rjg~yAAQOl24uSM-&s~u+EdsFRd$d?$s z{F`xWb#MM4BHX%SS#%fP<2lr~SC(dxIwHKdltdXL?8Ve?*}hxPu_Z}l8Eli2zV2l8 z_P#}NV{(=xAs^MqG##Mjw5bJDjbA+!zq($1C^u2_>N{$Y#K@{f#;=XXLw7%6tkGRk ze4F5N{>y#yUX!ek`DXsrk@aY(tDtJbMm1*AaS`)Y+`e%^%=ZX--9x}LmqOeKrT4gpyoJH^wQ{v_eZw1 ziNs0n(ii1vT{sHsqtWu;YqYuO&hW_C^L8;~mM0u(xse)mF@0(5^w8#Pj-rz`vg7LJ z;AgCjkBewxf50VDYP+w6-F7nxv1U4qJZru z5v`syj{mfW%cf?{)FZYEk)}+8#s4ZX^#Pmu19O2*8ad< zcB5>)Pai*cN0P73mp5^`+#ayB?a=4ggXraBf#}IeWx39e?WE61$6|R<@S+Fc=;QUL zLAcI+#k*gZWYFJ@r({f;7C<*~NQr z)XKE**zETXX=A_Alz$svPpV6R^h&gI;t3ZpR50+(5HI-dCg1xNtrFI;RE zY>NTQCJ8u>1308RkX+GTGN<2mx-_J#;h$R#1t1Jlh%w-w3~C+xE~YL7mL)b4lR}G=eeE&e4Jy3_vPe>%}IH zN_wEnH3%`P z`iWsoKsZm+*(eYdp#vh$17_?5bJezF7{C?GE}{lyeGhC1!4lHGpwo{}^`;^w4*_w( ztrY&oQDm{-HF+wc^qZaJO0yL_b&EVuyVS$aHB)|Y7X`J~4Q2zvof4|m)*C~~&nm)) z+?|%}eRSHnlKp9>4Tnx;=wSz1KfhdEQOL6Dm%qJRn{rjIe~X(uwf(TY|9~f%wZ+iv z%vpBFOy3U26ie0B^>V=QTCFtTwXFU~*(3FIO;ll7PL;YE&4fi~S+RZ1MxTKfyszj%1L&#OfA7uSci*##7jHBt+gHN^;Zeom6%xL!raDVWxn2#G&cOb6OGk;hy& zJRZ?G0Lsp4ByM4GsovtJ`>){Y)8~DKytOvh*o4%KM1NZ}_CxN1UWP`wkYtH@ceoQz z)$_$rGwxQcf5;|Jb7Z@o_U5Fz%zhc+J%nSSw0JekQ9r?D_2aL5WKEWO<|^5~6eC zJwNySj$5hkcZI=E@O>&F5L{pu@8G9Q`P!)Je|nexZnq)ZRUEnuxl4!(!Qx~PQ22KI z0TC|78elVi_~RI7oqm`w*XMJrZ}-x`)}!UwHp^CBUyezQ<~n`XmUAJ3s1U!*xnLl|j-kj!NiPN~9|+)C4v?9tX~;F% z54*FXL84?RPzscUswoGtDSbesbKZGHz8weAcN0Kf%n>4RdVvPlk-`=P;726L(5DKyYDVu;dFH_e{EEMxan5sgAIb|#+y6T6teh(cc7GLcgl;q zCwPB62&qV2f23-^`TAJ;&G!==wZqq5SGqgxud_FVhzaCzCa^OX+cuVNYj8=3G6 z$DoH$fYJsS@oqC1FVKeC4{pgn#1x(8N7UD=OTNyS8zF6tj~52k+QJQHegfh$@a@==odpr)4iMao}YZJ z+<2q=epmTN13B%ziJt_79=X`)O~`8QUuG^$0XUvLM-`+m(Rma3eA&B{txta@+xmXW zdfQqOr6Wg%aft%4;rn5y?nVm{V!kQm$k!Ffd3e8ot(Nd!`2l^vKUm7?#I6I+Yo~fC zJ?MZB;FhC={sT-aXznLNg7f(b6kLKNKN+yBDx0w|0Boz| zw?HN!T!TWmgCyVtxF`XT;Yfj*{g!%8MW}51O}+2=XZI~3opT2Swmt<(9H0>Ijsk;Z z`hGJgs`>Ts2okeCS7HDX0MQE?p}a3UxWW0uD-=lZI{|kVJ^RaLN8QO_MN9x7`j|0< zN&%>e_8py4EvbML*-aEg>6d>g%eagNdqd*WK{pOm(2V?j8 zcTgi<2$R`s^{#GM|Kbavn=9AS{3R!Ajn>ey#Dg~ho44VijMx`ec3TIC?~~1yH*Z@c zv8%!-Pt)!6c3f-HVV+fM%(iSSd40l4p-=08KAcY0*D04y*<=b#w99)pZ8%QG>r?~q zA>{W4(?e%P1$84%)@WSqe&##szBGw?;m0+}*zVt6%f36wUp{sX52q+RI8FSoF5mgS z_hy_#xw+NLQSMgBqKjDd!xI-DDcximJ7xS{3pdJbOej2kN<{n@BG%OZO>>K9>_(&L z;0!|1 zl#ZSh{WN~Y<2S*#%!L>QPB!U5t z^AFHukzMlT0cPTj0-gO{~4*aJ>&NCm*t9L{@etGmHC-RLB!N*$pFf$CPUX***0 zp5Li5V>t;=1NnVL-w1Hm6d8*LeighZJun@5EFC!1!382S@R-8%jmhvJ0~*i^0_*57 zEo*fimO4pHBxwO01rSkK3xx7$fIuVSvMZ8fdt+Ky3M7DopCuAS@kqEK%IAGyGZ7X{Vd;s+e+D?&@~-wyI^i}!2MZCt!f=w=)}MxLhg~b~l7TbAQ!{iGrzhex5_bR7^i&>e zfm;afM9E+8 zTyZTYDZKBOlKnes8~BSW10s?9Vl5x@o!qFQ!=3a`&TCtL9raFcwtr8|9q~-rA)(sd zJ=rN*IQnfsMrN~Ek1!w3dldl5Rm=K{8sG4qFBf~Uif#kCt}sq3>;6)=A2bXVP3Jl* z7s{InT#v15(P`|gIrXH?)Ijf|)d)Nu8zHGqY*vZ4xa+Cov`K`kJ|iuaW#5Kd#Ircg z4D+s|Z*4mBhrY;;545uy9?LBh_oOer{cW8d8GeKyU^x=B-AjDbxy1KzO>t~^h$qS! z1Op_-2Pk7ep38zn7)?RXZLkL4!?(7|jj(8Gl&wF=a-zMUNErp7ARdJa+P68etGGTC z83RPm3k77@YCUM@Sl&ZMR*NKa!H#!oj?xF0?5yYt_QDIh4pra}3ZA|QaDHiv{7*F& zYkl2Q+TH40Y5DFG&$rTDFX_)O*`;`vs4X9U;Fu)zQPHwX_LHZHpT6iD@PUKI#!611`*45_7c}`1K(#fBnzt^Ahzpy%}xpz9y>9M?S*vjV`;(b@z zE-d<8zcTSK)hgw+Jv#w$qaiV8id@o%8{0CU#`g@ST`N1ccb%qIPr*apZ{}_BKCAm< z5JPzjzog`W1Xol!?}YXth= zJP>n@JSS4fz0q7t*mr77?(C6bVU3{kHx!R#AoP6*oY zuGieg@hqP2`R_pX)&5O#wvV^I{c`G!v(mjR0grWP-^ImwdpJG zec}6w;lPz#jaSlxdyYw6pdAl5#QE8^^$70OO)oF|kr=C-(L<{ksHdjqv*ZL6&_9`f zy=A%awUybXhSbHTRk!+BBX}alI|ANpa$m|Vkd#hF+B{!RiL{Jr$*xSiwI!ae;8+3< z4~{mSQ4oiPjRH9DX3~XeT;)f`c2{TV*P_5_Yt2K`Eobj}?N4$3j@=*J ze8Q$*$7cm65&{*I*4F0UrbyPnx!sUQCMMLP?9Pw(nnwi%wyu?DJb-jtW6 zw`x+-E|rOI66)xtaUbVc4(zc}f&Nl9Sn4t0^|fj*t9V5Q9(V@^)E^Q+&^|t>&-t@* zTHOmwfJlXE;#_6|1?sa>-ZbGauup^+d&dFWJ8cDSVbX1mLLWg^hb#Jw`;U_mnKh9p z5E3JAb$bWza9_4zt4!?CaUp_O2@1e9c*h?dz7e+<;Oj@z5fz9-Kv)N$Yc0$q05G!I z%GJ1%#H3_Z(+OJ4qt)O*7S9QE1mkX_A&DC#;OvqeO+gt&yxUsX{MEtfYj`p=`9d;| zoCDzU+%_WRWU!b5cu-seVkQ1Mp8?p6y=5{`&@Brm#RwyJNJl!@Q9}7f93L}2m>7*2 z1o<|>`pnNB5?W}wYb#KIjp0>yz64;%fLStmxCMqN)?l(PnjOk+{Cr+|+~CkRLFETO8x{o!t;Q>{n%~<$ zD%5VY{6+r<%lNxqR@bz0_Nf}y&>S|`sPHz}?_<6FWSu`u-VVwc^A7n)NB8XwqUb?@ z99s3henNo+*9C}Mo<&(Rf5N7kWI_dmhwo8tE%rC?5lJ_c*8_#$fx!UK3BMlj>8Bj=Q7n#o7we+4McKWhyzn6lHS7VcBpK>PrVmO4J0R zZ9q)MbDUclB?NFNMNVYm<+u!|HSdXwB-m7aXxi9)H3J>PM_dS%y<0-LO%)n{XNw0%D07&vIMd`U%^U7&lI#TLA|)Ty(aF0?&S zEm3)Vm&MI3;`trX-G%GMTtD#sICACSiQEEJdvyk5m!p(g=h}CNA`S={oK}xi%3t@w zNBu23d*Y^pTtM)eB7^i|WS88pm+SiUgaS9_g(@dHx3l(&UOJT3?Q$!FhFCar8C>NP zeegy=VZCgt;n;151BY^>2Y$AH*~)wP$JopDlj}7{!Gq0ICkS3`@?(vJ6E$v zjmcSDVvQ!=y(Ljv3evYcHq0$sl+kc#+T!Dx?2#SXANcljcU?1Izk6Mr!jycZFwo{%O#!H9V|Qg>VQv9*HZnTm+KQ6 z^~Y>}v?SfG1BUPPzIn*(z5Jm~J?Z9Cv!KZvkAGG_APX&SaDHNj<9YB~YO_a|tgwv!F@6gRvmN%eSc1i7k9hrGd9`flZ#UyF|lI#hd0L#@rj5k0g+{ zYO)XZbxoV@dX!qg`8f5sI8uB0(eO`I?Y+G|j_!*bzqg(+9(Cn^AJMm|`OWFbBEY49 zppknIPZzey#~i9`PCcfa>{sojeUfG!2gaw41w?#b{Ctfv!|-OO`IaEQ=e80|rb3+4j6#%m0fFV8RN^OVQ=o!`8VZiT^E{>_ zb<)d|TUeUO?D87JD+HD9oQ3U#0Uf-t(c_f-#0S=h-lq4^I~oq_0i>#u3rvc zDJ#qpaT77wS2A|$nXT(Yv!_kdnKI%i)OILT=|z6ak$ZgR1h9k*)e~Q`hvA8>cSu~g zBp5gc#22~{z50UIJjVfqNWhW|WZ84r?#}X9!z8W6aT}#b6PR_6fs)>@cJ)^gk=sr3 z>jkXRPbJT=S)fNS>7#ra?Lucb^Day4$Z~x^5JR?F_I}D2NZ%{Ok}Y8r#E42$%UC2! z23iRnvZkDi)K~(t4o~mFFkB8wP=!*UBKCrVaaP{1z44IT#aJ@XrNHodI&@}1v10a%=NP%pz7cH9n2CC?Xb4}o`nq02@}zVeZqI-i%Yt>K{U%L1mbpF$#Y=pn;U_pW#B653)qb5JB{y zGY^6k4Ar1Aplgbu_(zxvkmCs`5c_SIiW|UY;IMT++*DvbvrT#e`*1B4)`6u*S09tQ zSl&9|70MR&TIAZ!H(Ph*e2w4iExK=0Z%CPX=M}GC{`Vz`0g0^YtRMQf)IGa+uIz2p z_`AdW{YL}0Ny*ac-mY3az4P|7?l?z_r+2){uP4J)z+VvBKYsuY;`xA#YZY@G3Cav+ zGH=#^Whi86Vh#qf)?}s+9d+y{X9B*9jQJWU=3>D6d&b9Hm_3_~0V^4{*i0tEV0ui4 zTk8+bRE!vzYr3Bb)`1h;v+Lq1&lSWBxqEsI_SoCz(3HK$UruG_iP*mIIRC`rTzcI0 zl$I|?gqJR^I~8rUR#(KXc!hS7P@xz+$TX$jE#2=*r}b5Hk*=Jok5$c=yK#%phr2!$ zH1;XAa!%e^*gaon$h_paz7BuZBb0}fiOZp6M zH0v)|VKpZNjuQ9)H)WoThnCYaXm&H8F7$<5dHQ?;4ao5JTvQANxb#af1c0&|KyWjs z857SnT}=hvGi8y1mBlcX*8@g3bbl8=*(;_f;Xde!_{IE?a8n*ylTVeaxHZh%Jh@3M zeAC7pmag%sidR=CQIie@iT{0o?;B9)6JuI^8^_jh7EB_gyIB z_XS2NVE4Rqu5~OG>;YvefJ>-C$Wvjb_geYu@9Wy>-7hpZ+1p-HZ@#+U;<2QBl*jnX zH6JDWw0_Y%B)11Vsy@%qD94?%7rnBJx9k2s(~BKfwjJSmvQ~sL{M`Gt%h8eR4kiKO zKP9xh5*VNMnJs=C3FO=>C$$4&g|FYWT`79lP}*>`+w-uJ-_>N%E%tYAniY8_EE^aHP`==lL;Gq=({xEgaR(|j$#=h09ug17T}lxuYNpE^VsDDX z_vZ5SHRD7w)8(`!Z)o0~GG1=|qqs;&&5yd`FmUnK`?P1fnPP!f1p+Q0;1n{Gzshf? z9|hrdv}R8yKUk4`PuG9%&gBJw&jtrXjOmz21RoF$c^@<*4cN)E$lu=mzsXq&*~5NM zG@PH0mEoI2|yj3H)YE~cl81R zk3*UI5C#T~G4OWzQS@-(&o%8);d0K-!7_Pf=k(VQz}5*aVe1PM=EsCMQ|jNWJm9x4 zF6OHCHkA1CFbF*pAJX`Hmv*rd;;E%1eK=Zf(Z;oY70)gJ+)_}L{-3e_UnRc>lSYIB z%qFeh+4W7qQXd1`x+jV)UdAW9o@Aco7fK5}G&1^e!<~zLqIXstc=zGt1u_0r>h&58 z--|thIr$t~ipvEyAp0NmtWE*dx`qX6M^4vBoL?eDI>5bqef%{gEP_n-)H+`bflw?` zM+#Q4=iBXNdc60W#8B}J8#AF`(?o!#_x}J4`u`1Za{j-I1S~!bpy>bYr|g8rt|9yH zj_%zJXOyeQR<#?z?WT7CC6)X7(;^MK3)!yWj6N=tMH3#bb$xeBr;bbrO179EmDkGN z_%bqnQh*DG=CuS==q+Y*4xzXRep=M;881E2og}RBdz;v00SQA9f!{aJt=(G@Wj{$` z_0f6IW2fkfiXO#X4$j;|$3Em3%w`PmqBB0zmA)eVc-mXKwvn_-|BYX}YUOqdK?63+ z840tuySZ)!?H<-F^crkU2N6SLagn33WZ0*}V9zgC!By+|+@d5Uo2fTYxZmr`V{M1c z>^VQA(*=}KrVLPfEdEl2yZlE1P~uJoQoF&%$rdbRR$CzmU~?;-5ru9p`7;$nW4nj2 z4m{jF)S?t5rm}wI_J#ZF-QOIHVk8(#v4$bMA8yE|<>shQJ?afRUETgCF+2nX|+|L5bGMdnl^4u{!Zf5VLN!R!>w1;C4E z{~Klt(_A1l^RRpV76N%80bucGW#$6>Vhec3eHwO4us9q^5Cf8YEkQy1PxR<;UGvLv zzH=@0ynVFV?X-g>;+LlRC$_S?DF-aQs8agKD_!v6qNNXHGvuo)g>t;O=?Sq-CGS#V zAFC9Vymt7yXx+iqtJ+TDf<_On^;9M7?vU}9TXS+{ea&%uJngnYTJ!)CF~)<{J9bBLmB_PYNYeyRbf1BR9M?b6 z_5#jX-tCkl=7%LWKYmv1v;)7mPcL%6T-L-pw@=kgZAOfnx(&Vw0ijI-H)o3MCirrk+~$M zD0B$bfKP%fZCARdHL5{r+=;y`vuZL5S9i(sr(Rc{oW!Y%Qo(hvbx%=HC@eRgK_koS z9{FnHzb-PAEL#>FTWqvROla<9&>mh5_hon#!{OjL9=x=L1o1zXJ-#6`mqT_fU7=R% z)B_K&C1#aEX9tGe}unSpD($=c~)arTC5dW4(4Bs@<6LOjdrbkT+wXEflhr9m!IquERO z?#bP+i-H6c3Ax;Y46*^Cn*%$!5}RWb*l-F`$P(8xH0Il2Mju0GavN~}_)C%VkueDe z6x`?bD8Q*;1O(^v8`tOp@+SZvmb@;=R|=kioFECB%Z#ye@{9%RqsNFqtfsX0!zw_!p6)i2FBn>#@zn~ zu_YX3)-d6O`CyJA&z&|~rfERgj_kb?VQP7=~L|)yY=%l=)yzTML|nY@xlJK{ggIsby6<%miHg_(ySoI=F7x|{!su5F4 z7ln6!udk8()ZtZ?n{)BBOKES=$(Fs{J0k90K22Kc=s*evKQ0TnY7 z<~{`%G#l_AUfloiFtrIk^k2v&*HFPV0GIO2oMR|PQw<>&n`mL5`Z{G86~`SjhP@idl!a6xtJkp$@w)t!8>Rw}@sAB!cd zaW<|E=q&P=H&d@>d;h%grC6ciffBpmAGZUpJ70cN3wU`r>_Pt{ z-M*SEDIJ~a#~yDAO4D-9EQJPRGk1J6&>S$JUb>{hQ;Syq>Hti6{!A!5=E0xxWmsy^4YZ z4(h}ky+&>ZpIrem?gS3X(21O1iZ8=Y9$xb2yKyR>FPdbnSnn3|fg`Y1an70#R9LMp z!O@8z7l2=;HM4V0*J-ZZpM4%51{72OybGQHlS2QiWJuhL6hA?AFI?=Y^;nMtc0wI^ zA`C3?B(u?oBa%(Li`d&l-RCZXty5H#TUKMz(m769@L5hQ5Ip?&BWD?^b>1-N#<0DMbC63Oqrz(~`7y1*hFMG1 z3S2<`SGGIH<-Cbg8CEY)bLCd$!jGAj53W9XCKM98*0QDY;KbEyztoEh_Vj%FEl3$7 zd>axw{JyWjRYZN#XvxG{>xQqQ(=4atjo4+kELp_!QI;05yjI;}0Vq>LOg{9|m>{IrxCxLe9#( zSxIa_Rljsez23o}G0+6Ub30Z)ZLYxCS z9Fzr2h!U{?2plA)2S;-05Mj}prEqlEi^lLGZpJLDl-b4-`GERr$}9`>U`6;Buvh)b zvs|+ZDzt~53IiUd;V40J)eTx_osgTiX3)fHn`c#r(TjuL#E#=eW$D#tvv?!A_1u=M zzQN6}llFU^NyaXJT=KY@ZK!}rz%!eM9@WjEpGFGK_=_Y)?cbO$cC|;v%bFn?RC=5z z?ss?Ey*-*5@7Q^_J-88&mq^>=TJz;Quk5YWd4*ygYxyoZYT}+e5~*|ix@wdX@b+Dn zbZ%Xy$m>CgZA+AqzLci7PR2_;){2lXovdb^NYnC!D~RCW=FG>jatU2bj+I)rO$X6W zMiM4#zj{6=8?;y0GdMy&a2gH8(5DsnIbk6yfbzl=z>`AL5|kGgJsm-c+FLSo~2oCeP)cAKnz>#sf_<=418<1%%jR z_+Ky}S0VpkMS^JF_$RTot+I9gIxLMZ!5tw)Lj|1?ojlm!y;!yKc!AP}-0?H& zfW6U6U zER;nA#XMvO=FUQ{wILOe1i>P3&LLH3Uu7o8RLiyAG2vv2R%Jcp8QD<|0hOL$VnueEPWNC>YHSs$sQ_Qs~u+db-Iy}Il|DBdd z1OdOAuWwp_ z0^L%WK_3A?@0mFYkN$zO|6f~Q0vFT%1%B?GsitMLyvnp`T9qYo! znlGP#`-j-CeH;pO<=3f|V3tQYOR*quuTS1I;HQT#6xse!0>vcJw{ zurvo!)o@Pp3N{?m5uoc21{J=2u(6TP|HVyQcQ7hrV;^W1dre%MbcGc2g=Zi+&6b&Z=30;n9whxEGqdM$$+8na@~ zr86GD8Natz zWbk-+(rzCy)XMaUM?oBZ6cg|72D2x~@dTYD*pn=xK_3u-T@e-)0X%-7Ffr>&2;ol< zk0B%KAQq!`BfRD(rRzq*U|sl^7}r^PC5xslVT8{J#(0}MfSM4dWFQ(tQGzvVmGkwC zCNe8IT^*jd6$U0#wqUonjP@NgsWPkc3r0){LDS_MS^rwu1taC=1~QvfbFxTBu4^m6 z6p_3jC;Sl1YFJ_b<*C5o=6eaF#-jaHHySLPMy19Mi;)-0`0t@2Wf+m0&j;^xXJ!u$ z$$+^h$4CU`feAXUc2Qppebd6+OYY2jBjv*@3`Y+G<7MaJgZtOydre%yR*5-0&g2Z{ zfRlzqZpxsw&F_d%>*a={p{!P<&uA7Q%Pms>goZb3A_b$Bwb%^|TZKHHeL>Wp<*dLt zKLkdINRUHF1ey>6d7n%Lhxb&{LWO{^KZBOy1wbkDbEJp}36NF_)OztgZJn>&!))_L z^l(Joj`UMc_vLujl2q-(KW-A4CRCV+2B(S`VLaQzf`tglCg{s&(9}p2@mN-0sFrz{ z>x15>JUX*i&R?PrQS=m3#!#;t-d(M>>@vW$;1{F3>xwmmvyECvx7BUb!sA(E7j~)R zV42zvdFQ6wA3ACpT7VWFxpjd4_ODU-Qi6_{N+uM|$&)iZ*^f;z09AtZ5*$7r{eEL| zxl@Vh&WN|2L(B|Wi*cf%2`R^XAtFOtMOg}+!PbK%g7%kvS1t;BqwlaOeU2%{z9lg_HWCCc4hKXM zYuwjRDxpThQ3Je-3@^Q6M^ZcKnE0lFYub)n^m3nQm74#oxp2ZK&l1zLonDC-KF

    N{;OUu&n`zIg15Mqv%dZeh4XEqs%b8kFSb3`bH@XOW!X>R&utYIqSw*O7;zxoXeX z)%hsu72&@RyftL}#rVPDM~>SU*Rcv4L@}r?o4GZBnq7~`QIUD#A5V?Tvkli?M#pG0 z4Lk+gbvE#Z-{Q;ZwNLuXetligbc$-=QNw3q0i-b441`b;aWW42Z7l~2fbpz7hOK~6 z!mERhc0oqviXetADjc=Ovb5q)q0(+l?mqXFal^}hx-t9p0`YuB%fJd8BGi#-{agqm zhObp^zcX8sS6V~-3|gZ}O1%THZ51$u5-Nl&8>l{F0M=oPBiBZRb)OGv;%y$ZHdMEL zd(>8U9o;`|hTItUu3)_M59=8sO(VNs3Vss`GZIebZ5BrP7Y#qUZtjhJ?XNqJuD@GF zG+!NT92kkA&VuVCP8_a_5E+gsBDA8T;oc)80$E8?z@8jxv652sPY__aTZT2w24w|4 zsS;{BbSjrO%+EP%&%0rKYmDB%k%OOQ{@M81My-`SdqbCnqx6g}{FH|jP*D)l?}o!G*DCzZs| zy#EM#v6Q8-7x$fPPs3WE^GuKVE>RH!CjVEW;x#xbqQK`*3AG3!^g%~4ayBV)XsYp> z+bza&$f0!x_btu8 zRbGC0ZsC`}U4!<=eGFN2RW#IiS@hIrySl3MPyqz6^~idei$J@Jux2ClBcAwSoz!PQZKyH)tR6yF=dmi6V?kTZY+Ev6_0#Yz zby+1xV>#gZvzi>F<7H&>;_&#RwGReGAL5RdVN!HsS%@8^19rvEuyx{8?(+H$^fH*C zY|AxujWT;e31u?|R%i2&+&^?+3+}XSI!u~m^NCGQu2u-Vt0I0JlV{9l?M?j zXJABvCGBN|gjx%Wb$Z%$fbH!rmwSKIr$6$D`cl>^tTrxAYCUnL@b>^Mp}_l*)?$<2 zCy$9L_TM0#_4MZ5uyL`kXI)x2_N3M&?!H^I^DQ-GtqTu6{d6fet0+lpXO?iIsm77n z5jC|(p5F784^X__&GCvzma`xS+k;Gzh92a~nk#xT#ekRQ_K3}X$dlnplox~Ji5^*x z$<4z3b{w?n0o=r-Zqth=a(de{d|warQMnNf7=sk`Mx}&=vMM>h@vw@*i3aSM#L+q6 z9A@pH66$4V#dlHHgGB-clH?-HxmF2o+0Zuv163XGzhOel=)yxQ+cIhlqlPww&8V_1 zyX^X9Gd)q9Futk3!v)8foWef)mtTFmS+wY<>VdxIX`xnY76-@JJZ01!2LGcK?{5Tu zd_KDRyz!>!`@51gGTylqAL?5i@A6mJ_Ra1Vfs3b4TEC=om~=&@U)$VwCqjh2yPeNw zXw`?$zL;UgjGYJqLnDUsHS@rby6u2FgjfNn#xvTpz_Jf zwZOCUU-NVi4QPaAF9t7KPh0Z(__X=OWnL3|VuXC{+$Z6S25I*_%~Wi5AGnRBIi!(3Dkh+_y5Sh$S>tRgRc1M}+W$RAH!4F)j(0vArcYtjEYP-xRE_cDsiw?_t zat^4i`f04>Yj@pdGsjrt952^KYD5E4eRN{%a_`mNstd)LTcMqTkL zHN8J=>8XkNw_oDtA8~S?dazS0ws&>%3TKB`#3u;!=y**07w(bGjL_M^vD)(E>@#u zwb$uft!m6fV;i)x3g1YatccTW!Wy%NhX*jUQyO4m9Cg*+VK|)0DTl0%23wYIcs;7{ zAgCp{3Q?0`QPo|ghyFNN1x>q#3g?F*1C%c!y~)Cyi(Ub!=k83c)arLiQnOm?Ss2Xl zP=9x6<~cD(AlGUcqr0BPk;{xP z$N@daN%f^9knM;}&`vSBcCn3KWi*ZjvOn-MJSMec7yf~wVZ#Z0C|YPHguau6c!t6m zhxpF4)dLJQGvFc17asx0xK1gDKfwJ_O1!1}0<{2%1vLwq{ezimCyk=kexjKs=C?iu zGeBuqTbGgVZmI{tkQ;2Y{W8Im`qX^f3vNA277O(oQ>hlp5SuluG!9EoYD>-lzVGi3}QDy_OhNWzS!N}F;W(f7X8@KC! zp#m8&bk%j{q8HvN5jgT$+`{ftL+`Vf;n&WCu3`u7$+A>%5u&}@{oZ7o9i`#J=X&jX)I@RC$3n9V%EhvjFWl*6`0r zd`F4^_eWz*A`4uaxEV{NTZCb7OMyFm6{8kzEq%J$(U-r%+-isGZ1R%b)PhV zfkVaK>-7(UYMTaL?=S(Q88rPftBx!W=$QL{}s5=V0g>dA^>Pl ze826H;G*nMeZ`O*x4Rcj7n*f2lMa`}hP?M{2>YAeaO;;UJe}OYzvmOP*?ja6|L1om zPbfXtXnQMbPE7r|J->S#Zb?upILa&iqv~+3x1RTqoP6(7BKpbk=$CW%HkUnm5Z-K` z{vj*yzP0#x+<}s9wq7++2c`wYPh4^oi|o_9E}ti6^(m{4*hLTeDd$QsJDyCuDlZZX zIGez5er$e7(>EorQ*?ME#g+9VcSqmzdE{DpEJ(m^3>xcu;%SN9k8G~a`P-OD`Nys! zj@*iGt1vJ)?8!O7A`facqc)wqA=DkVP&Rv~sCt{M;y9yOdHY>AE(Okz(asm{dcE?$ zaz{@FNx%z6O?=r1un0nrW+m{h?S5#MgT?RFVeQ$G%Ulm08yPTW&r<8pdb`esv;cm^ z5WEM-Y_Nb4q7pBqyp76D7-MN+lJIiS#~d}cMXnZj#C2768dd0H4LEq!2sdN{(k2BC zjPkttz9}ooEl<@4%3g?V;X zT4GVh`nw-5ZmNoTBDmPR-ym}k&QNgd-b zuRXQB&sR|hkU@hBst>H<@ep~|?v}ws1FYuU5`2kVp2bTY0Gd7?YZ$cbrz5u)JNGz+ zz9xiLXXweK9ex)J{QgQ>9yZ2<#~1a;qU=vH;3ppKu+&wil<{@sU~KGLpfhKZ!}NBY zg;tFabm1+du+(8GgG(JscLZ-2e(7PTqM&scX?d^%-6I0d(ZhzDIKBd^-$9)372|$D zSe@5nOT{pp%Ty-zYvg61g46a(WhzoPWXz(Pg?Zm9GEPWWr>BZkV&I-@gEwYU(5W9< z9DZEcLZRS`g!&gY_I%_{klBBF#7&FwMH?#8EX;mAy=g8YjKTEQ_r7pKo?fHhWX^Xq#+uT8s}MRMZ7kwA>%6SrrL3>52Rj z%?2S1ZtW47{@O#Kd7fe$4|0~-t4At?6gC)smU6bTmn-Zo%U<=B(DT^X?gJ|MUg2Pk zzOQ>Z%1s6bnSw9CM>T?o@1L`nJYaBRzu1aOZwFYE*1UapQ~1?GQTl)V;WnXZ&c&gZ z#=rj|vD}y+Rs8ab`>Z8@i>Y|KD-zlshW;-q$16qV zQ!A3gKJmjc9?U!TOe^8@qptMG$7OBhf~A2s>^V2phffWw=P)O3IX=+6wkzCeTc| z->>TT(EkK=M?#P5oe7R7!YwqC7L~W({b%%q(vpzs!rSj&a&~nVFDlYnn3?WB*M0BB zk70+_L=UyiEctlPZuFrB-myMhRl_#1A%2_yJG&CxXNAs27^F+ z7O(y*5fG5ZfAanRkpH0SpMc(Q0wjI5;GzvpVA zO3IS*aw1^?jOOEcqD2Y9^0EqoDw0AnqW>MEED{!O>11c~-+@`8MM?iW=>H{Cl9v>r z|3Be@Of2T(nW9BuqH@CWBBDwnVgHjDC#MzT{wU-BJ8$#xg8$b$`5&#u|7c}l`46px ze=Ln$?cM*&CtO5T=^vo~U`%3UVE<5{o4CT052b*-)DfXE7+H?AHVlF4~8$l zd%(F@C;%Nm@pJp`_XZ%$o8|9$4)s0z1OkWwG|%?|T|Wf<@~?g$`W=2i`ppE-ef&TE ze*S>BXJEi(IYtX$^hdxW?44m2Q2TTJ4q!eA|Ay0B)ZIJG;+$2K)>BoqkgQn$M9R zX#n6yzt2ytubcndXTZ+}@c6mwO$?ln1kO;i;~IUL&#wTw#m!b(ew01GZfNIb{Z8H0 z?>WwaUu57PQ^MW8|FggGqD`%WJSThK?zsgZ_?#p<-;|KuNS3`Ik9TC3<_^?NPQRhP zsnLnQAMSp)CDCc6>k>O$at1jLmN2QKm_|X7*fcvHbceV*I0m_k99i{njLms{Cv)KX z`>;T2f^Qw7sp60~!)+y#Pd4SwJcdxyMJ&;D8kiIXx3fc*S1T-?Rl#b|a(gaTe7Dh_ zal|YCNOicN+1)jW;{u`+h2CyO=ioUcG3YYncbzK0q{fcYuvfF_kn+{GI)>V|#+x0@ z$`oajjCJ2>`*t*r`}+3Hg<1rIBb3i55_L-O`}KKgPW{T;tDK`a;*TydII<#$at2l< zQFmv9#02qiJ$wYQ9hB(fn_<}s&pHvLp6!#BTmpm@Ag!xa6`JKBTq@jn1_>wqa3!Yx zYEbzz=Zia)$s>F5v9J9@9$0n%RT~CYx!N6gzCZq9ay4?`t4kHGw=Jt;>tkYX^RRDa zxmrcYhK9R=FJx+SxLXnIDT!ERMI?h#QfV-XpQIBX5hZ_4T^HzwR0sKT@y^%P8qgOr zb_!1K33SJN-C0Rp%{>2i2m0FRFSg4}mz;Ww4STDBk$Y%K-Om6+h;;yw+$G5`!egP< z6N^2D)JsiF3c+A|V-!Qe-EI4Rnuhs1q{XeLH1=3GiKrI4q#VD7U!=MBsPB46mjNtg zYV|w8k4BexP2s`ujLUbC1b&rZ#1KH-xn59#b6IU-x>}Us8^&)-aB?+&U?=BPq&P+! zi5&3i6N~k~axlQOyE?ncVlV2V9VXY|-i7JeT6*OGwn0ovgH=9%ip_~M7pdJ}&fWG zJY^*FOB}de`QvF3bLFOsKR?QSDkH|i$0YkhqK;58s~wZw&z|cSb&>soLTA_pkoi$7 z4zQ{i`Cn;(pSgNh_dJn;jZjeRRD9amJaW8O6IZNe-t-TrwaTrE0Zjg)s$I$i=G!fU z{oymmZ&q2-!i|BUj9uoqcEvxOQjWd3k=-x(Xm?d$>BDP9FiNU=&EPxtSzQ>Os36Bb zuW}}t!0FX>dC>88S+$=y|=iR$?98 z6TVa{KCReADhLKX%E~0t3bW}2BLXR}zu6-P4!|Wh zOdOZxok|sk%D20ZtbWsa=32o;ZP}82VW@>pKom+b{Iv}$o1J&olkxUWl_utx8143i zUS{wn(krGB+(A#@5qEaRfx|R&j`uo?*QH6Uu_E{#`T<~0u0-J_tAHzOd}{p=6msM% zU(K|n5y(r^=iAhI#{aOKD;O_>&*2LmONzYKn&v}ro@{pWBGqeW4IJljVYoJ`yyHfw zF|MHj4=CZz?U>U}`;}ucL+-Dz+~0MwmDiEn1@j4z^42Kp&|52gBPa@#-=K_SfY6mMUB8 z-Dsn7U3r_fK7L;*(n$Y{&^J}S+tPlxFh@Pj?!^VyJb}L3JyOT+H~;s#l2Tz2U+$02 zDOVye4R+8wJyyvqK400eZ0NCR{SGzJ0r>uRQ8)S=oP`r3di>_=Tk~8-~FC; zc`z7%()@@3(aX;&_5SEz$|qDzb6}%2M>0Pm zjT;khoFtlg*zn9Qu`N_eK07AOKjC?R%0u}c-b#L>Y$MQlyc!x+tH2p)+IZj@b~IgMrR)O#iskT zjvz`k)qh9z1!1u$=5f!s#V&GDFwj;zuUNnTlBv56;nzQ!Wo@?r={3tZ^{!gd86U?# z6n?VU%s=Y8QgLfW^r9oNrmK#z@7rpvCsA4`DQ)VCwXUMR$-xyQ%`G}oME;jjKnHp5M6h=$;3|D9mtjlsFVPx zoDk^e(>tMukQe-v2DV`H0NGKOs64+ox`aeeHG!GMPO4oy*hnIq0S*soK%Td6Oi%2> z>$f>6qag>+1T+w>fxxVV70-r=U}Q{SP}$=x_rR6_lH9MHUe`@u;DIM1-W z5d4-`e_^iph@DSe5#9XR61_U;<*|W_7^e)}v9VnLbR+}~D@eMWRxkls!iV@!M0MFz zw?V(@@$gtAa6jwjz&JL|No-7W<;NE4w#9j|;gE)QH?2eS2=uIiG*~Q_M)|Bi^9O7C z;sGpa_cn4Ei(aMk%WuLs?9PW*$R-I)ejAavggpgt1!BO(Rd(B6p1q6O;4?#rAkvB! z!GWmSr4tU3>va@?hg@So;VyS5u%wA~Qd*>(Ax>MxJfTb1+Qp(3$_AkPWoJWJJGA(n zE&!YZu6YkU+xWmuu6OPfOr zW-|ku@^oK8s?JpVN4Soh&js%16JPz-DxJsw{EK_fAQKb=;(O=M?b&kdr_j~<7j3n{ zbSyjtLlLA*R>Ol~Tj(;44EWsTirH<8pJvEyU?xuR{9X8bI>oI2s2=R+_V00^p?h7f z;Z{Y=fxEDwh=x%fp!}9=*gidvRc-N}ZQ8&m;UU{bw{0}LxdzzORcxtjHV3@g9*!3v z5%T3t940E7&!^=eVjQy!(+FT@6&zy|c$23e^*?UFP?yv?BF4(-ZJ^@Nm?@H(8T>iM zSSN+SmIJB*v??&b1Rkn>t2COJUw@AyQ`DrinEnbNf_f5#{HwqBEZ$k%U7UQfrx_{{GLVMzjhJWDTlwK0+^P$j^19j8J=`09a&DZ3T^ z=CBd*eeLv}$2sv5RXl<{O1r~wrETft@r4XE_MvyYRjpIj4u2H24eV{Lao0f-{=%3D z&DOj46&pUyfP(y+rhw^>O$O5aYL0ZhnL4TnlYg$AFv`@tx1(LxT>jxUjE&Ig8vSgl zvo3beci;=Pe)|fNe;|-lnYviK5l>XC?Ifr3iw6uXm%FTN{f+ty660xmn9&S?=vxgZ z?MA*VEE0ADwzg4o*jjv@Y6ctAfCS9UVb~Ux+4$z-S89uq4n=SIA1h6@A#-Z?x}<@& z41Go}w{l1_g$l4JEBkP(0i-zcDNWI@*0dMo}Kd z1Iu3I7K3DEI??c$LSFG9vDT?yR`UY^hjeIo87w06xo*%k?uU0A27r)Bg&YD!`V$AH z)ju=zM-WpH`1xE!=($)YLyy}nD2_bY$UGdU+$xH zT#{y~J;MADenQAc&C?vPM=sSQj>Cyn_xo@UUh^~^7M2ngR=n8hw!qBYg43t=8J?m7Qm5A01YJ6TV8G zsr{U4#bU&k^Ob4l>{wCl=eL>DQTA0;;%bhV`#hRpYYLtYq7B-VSbKKAeHa5 z^m+S1k)mc`BWjV&qp84dN^4fcGB$iIEY_+R1s$@7Gzo&@7fhcf)2`J#tVnZCJX>To ze38Mt-_w6Q?bL4a>>ge8$^ht{7@4{BUXM*@Mfwq~@k%rq;J&CNy;e`&H(9da1_q@A-liiWE1O!HOwKA(G|7T5Aa|?kQZpRe`k;t` z)f@`Ts_oPStt!uwaD%RbfX)S2m9t2)v*zx`aK_!OvH8(9V$GaiE7`+~x0MH#3ddw3 z;<1O78T71?obne6CCYbje!_8<-^B>qR58|)IAO@~@Z#S{kY@>*fgRBLeAaQfjmVui zOie=8GpV!m5|Uu5)DTQjNd?n^_!yV?*5|f$oFXogs41e3*1b7Grlz6R_L-D7-Abg) zcx2QVg^FmmGn`UtPa@>bK;Y4Ea(7i|f$GK(g0pD&sKs^Wi-fsOLWIt0Rr;ODw~So) zlnjeR(LxrXJF-qJ#((P712a5YRC9-P4mb`IBg&FYFUr7cgbOUfppcsv3`~)_l?0Td zE|mmyi*2*Adg4yHhi`CS7>y&C^>wZ$tLkxm1@v}fU5iFtwZ&h)Mc~CDoFlXD?5fprC(-z7Y~>~H#+-V zx;t!Y#$!x1El@U-BG)STTOTE*Tdvg^`gLRzdHa~$cObl&wV#jp;#*^$-1}IrftNmo zD<(yQqXii}3pf2r;N6b{<)b)MenrdWxtXfq_iE`Y8X6yu%*RR6m;#(qTaki;U^R6$ zqoXq``d@aQfg|dH@BTQ7v|o&v4d__fjHrg~t>JJPtyQ9=rPz>8WVP4mBCl?f)HD^3&vbiE+2XQ2a7Er^pqC<6VTk1G>xrB?D2=N3r1S&q zyUdV!pA!z!Kx~VWpDmmGTajavN%C(ggzp$?@4pO?I*bnRi51b#?+AetWLrXWg9tp!G+nLu(ese^I=6A(PX@z%Px?mMY4DM+log}5h)9vAb{c6pgcE%9mZ;Dx zmWf7^*J1O#4AE5TUp(U-p_N^+S zQ`@ET^2BH_xM9+$BYoyI`zIJbLpGv31uNZsQqwnm#Z-AA?3>Oh|0+K#n~1@S8&tE3 zjscGaJ;7@QqjdJsWG350AXdH*w{$Y0X&9a0xdyqV9Y8RlblD6lCVCn>Y1XD!&-`0A z{Pl4|noLV^y@R0sA*@hdeJBp~R1b>`t^iCw7*DjcqZigyPfz!@P1Svi@>QjMgnRB^ zpaHtzDL&>iEb8djlEqp)f~YE*6mM+MlTi8G2rMVZ(ZS=B`MRK(zKSoCP{P4rPLYf< zinsrWw@^?lkPy?(2wZhI-Tq4QdSXb-Y?5{38-cTN<|=R>fe~ya)>x?n{Vx1 zBU~uDVkcu^6zPAfNoagz6v|JDWbnY&uqHk5?TE$$j1gOSf(E6l20aVuAeLpbPW6kL znN1!@6h7Ev!N&4l>iI&VFin{kF!5s+lcU~Agq2tMlnWWQfFsSNY>FNK76*usvk~cb z!cG?O8?RYn3{z>YBLohAyMmpfQo6Z}1yWLhU_pan!hQgjidg9K6$;)61g=baQax~9 zh&w7e(d_9h`H~RJS@})x1Q!0yQ9;w{ks@NLVh_b+Z8qUJA78*DY5e|oyjNh}wD(U|=u51~G9fGZ zh!zZlc6p?@I9|jm?q5p`7shS9oHMU0x~X)nk)el72fR0FZEQ~3#q8UBW!n1N*Xvgc zI&ZYn9PA7~!wl>8-iffY;1gx&Fj)@+(LNH`n0!QXh0zCj6Kc9Q0!vBM+xy#=l3YJXD!n>oQ4RB<&Gc<7Lf#J+!-%39%Wj;s3<=uycLYQ+Qd^0xMZo<$nKX$ps#GO27ept@rJ zwbuqyRk$%V6f;l+ z{63%~Quc&v&b9m7OwvXu zw}rK%y5jE3<0fB*U<}!B8l@NMsWyVklV9){U`CSK%Wp+Q-*;*gBkrtB1rld<|_(vGNM4oL5?f ze^*J>zLqO~7|ISnjLO(<-*Se!*#fnEMc7W9%#P2|-W(kWo$PG3&SKZO)x@Ns;1CstR1>1$L z7T|ZbgMOMbU<+FlAtkzi^V}DocDSOLiFG1G)x+#BC4Yk!8h$JE=sqC*(Z5{SIBsy} z&S=Q{s2x#cYUaRSnOZt23ce6#d+G^XICVvM@38`A;ky~qLc0K#N$P~h&N<^RAqGjeEVTa`jGyz#Y-#uYbOL)>wsnfC*8L_vYk;6U+2D^rh~}yYe%g6gD&7i92&nsA zF&vEJ4PV^Ro`>@0mhgk?&V$BBBfkwzatPJ<9nAxV5ghq&w(4$TEFU(?>Md=7v=NmK zB{0T2=8MX?OopR}1pRjOUTJgYDGFyLUwGgj;C|AilcU54d9FzH%bMpEWRG%+`ZX$A z8)tz_MfY}i+JudPHa0J`K^4y}dzr}f{yIAwIlVgG)F?{^!#*4&xEB%^{bMKqtN(BR z?xu$Oi)z1hsyReYYz6dZk8K&~536{rmi{zbob%9!s1xttWl+cf6539k-3eHek;_Z+ zK)%`HzF3cq84lis7PHfE|{(o1A9hM}ESNXVm)C zmjdjZme?hdwI}`IUyrk=4i|f-hl+BxGIhUz?3x)NRahhZS@0$F``h+|VAH#FlV`cM zORpl8l}?Pn_?f6`SCgRCa&2cJWcP~Lhq&GOyv#w8PrUXgDgC^$y|%HTDLv`VChaya zgEN?9N=HzzoUHzAywBCC)tnazY;O``vS9b$x9g@)>I__PO(C5H$8Sx5lJXLVySF+4 zIOKAR)_9{AXW31n`(l2i9cAjq8FuFM3Ad0B&J6`=I|zYl4sLv2Ae{roYkfa#h%sy* zqvTYTzVY{mdtH{K8pTQWY{-k3vpfwDQtCC4)Zi4sPL)D@WG0p|qrQ8;--fcXiP3@%gGv1U-lhQ*@f)c81;)rfwn& z&`TqIQU`3^gWJo#R6YO{#H^{#K``qwqA}5+b24gpjFUw-T#PD*2}LTm$wV6^w!)AzQ>*bRRW>-R97jwT^z_ZZ6lLhdk{b~f>yRMFC8$g z3rr=@M!#)dP$e|?_ye2%8M1+Mw>XCGbND_RS)3u*mc*G2wsK}ghGlROhv_tl)rF|} zB8E#hQ*iR{{uW8tlVEFbC3Om#-hekf{A-r7MU0akFQm#ih(p5I0^p;-lfO&|&4t~e z%@4$x&Me96m>thpSh4~mSl(Kz>Gn^3hv{)m;`d-ORQu7cO*fnny`f}DfBnyw?-NH& zZY|?EWdG!At*>!ova>k_%0viD8vX)h4@_ik@z&jcNf3f6X4%S)fUinwre*Ql_SiF> zhizq@7z5@1i(&jor*p6>A3u#w7`$Iwa`@8VAut84IU*F=*NM)zI z8?1aChzNCszf=y>jx=YIOZ9X3uA(9`m#>bC7>=AOAUEoK?AdCz**ncas6p zO5*HBo5^8R9i^7D35wkHs|IqL`Iv=bG3Y6L);0<+DW|#uozqpDvBWGt#>7;AYkheO zgj6Cgar+qD6mn1A51uSp?up%0ZMMNTs|kg5`9_{(e3%^5+6aUd7;zFf?TLxE z4yeif0`m+M(cRz*yxBrT0~k*|C_!md`^VM0ZFXgQ^~BG{PR=Fpo5eq&FT23;Sa%}^ z7siM+RlJFBnX5MEbG@@$W?9YP)9h<$JO6TqSdxVK7s{evr)Z`ZdbrztR;))2M^lS- z&d?a25{iCMr#&Jpkb6m4!3W6?cY*hVxRy&Z-GU;wEue>~diU49;ZnvLqn~m_q zyqum&)?0pnhtJA2bz`n*Yg(d~zYHWL{&Ch~Q2lnULJd{j;tR2LUI0(}#B0K>q}`!* zDxWLya;AS#svop{QAY}dF`tyD6f(uu)ByH5-5I)uoSY4&iKo29Ev+ITFjC zWMA@baJgy@;{&JCYzHNSTV{+TeP*e-)MxdFv2?YH)eo;{tE!JJA~YzTejFrUhRUJ( zC+`n7Zi6~A{>xV4Af|dh!o!4Aq-`uz_?=eZ0#RnuwB$LkOAW0B?`^kV6u;w_m(uFJ zO1&2$cf7J0pvQrC`S(ibsJO>P%0;nYUkskXJAVjtm z`XV9_?lt+(rw}PO;`dk@Ix;htZfD%7E|yvlAxS(F#HiWNf2(bOo92e~)um-u3qc>C9CSf@Q8ldFF#SQQ)4)LJ3=QbT zrx^pE;0H%t)YiM*p_GZHgR-E0vFvLdIGxm|SqtXtl*6jVjKPJ{lk zjSM^udY84F^2-ueK60(-mKYXyOPsu7?a*`a9uo{>hu2J2l zGcLXeV*dnc4e{!jn&QA)1o`@E<0P5s&Nx;B?^&aRoi8~h)+^RQc}Tv_2fVgXU-cvT zOa1~MahOvGA}tIqY~N*wT4o?wa@hDB`t;NVGvpxSg^ZwVC;#U2fa3^U8 z^0oCki26>@91$5U94;hZv|cVKx-kinGdv~VY!PZ*Uc*2MChpZMK9f%e!#4BNzJ&?C z2R#Eq7W9ePc7kNDRmA}`Z|S4WegflqtZwMGTt-1^AcKmM@rTg74P;rB3wsqL+He~> zCBMD_)zR+cfGXK${rL!p zrvpoRIget%8#6Cqw>oX@6uN(#Z-uu|2EqRiJ>h9mAci$7{IzGUE47#Ss+@22F1Hk( z5jY#dP1PnKOgCM-0yz3GU}(gutkHzCFJfXE=8n;wGO@Z`Xof<;j|G+iQ4a6XE#P=$eh5vj)=Re}$!kE3 zX;k6v_;srGOXw9*5;emPdsaZxJoU@Zh2nH6==qsVw{vgov1vEI;^3M5bS=`{ruWRH zzlUUy6&7(8!muZDZd)_^_4Hg=|0b5{5x#a`@#EZan>0ZVZTzf+2f3f7gG*)C?hNr& zmikKY)%TU52{wUfOWgd~@M|%0qyEgwah9l_iF}A6Cuq^Z)?1vsO@q66mGH6cKn6+I zWZQVoW0JPhgNy=vDB-3achSw7*a4Yq4MZgVjp81^@8e=%q8}^s17Yu;pkNp?R>_<8 zH8~`4Q9e{*h&%o+ULT9-y7}(g-04QUk_Uol%(=r|8lA)s_hr-OFPOasTbg2WLV&h* z?c2dG1?%O;qyhlle6DAX0yO+=muWd1oFe!wMWf-?oi*4CWcna5xfsY+H4c34Ei%*@ zG(H)L2?E5p063kjLMywiM&Pt#?m0O4(a31SDmluY}tO>y&19x z8K{e{mlS3*&qfpGlay zq%5a&|8MnsLK+B+gK$z{mdq65-wJynfw^Li6mq1bJzR~myH9$ch;seOI**^L<7*7@ z!P`7oYOx$tSGt4prz+eltj(v)6FL?1bm?#o9Ew}P4&^1q)khIz_*(rsGadX%!cJP! z!eo5An-F`qc&yF{I5=HIXM$KTIMztwFBYr&A&nOQ_H$|wBrV%GrIKi#j(%W-4SbJt z-~6G9*jtSu;YCcEk+t(Rg3*Y(XPFgbWx?EUMa^+H*5E&=4jSs|tHQc#ej1L>OkHxB zDbTE-Yhb_8+(v{pHjm5df7BjfokzZQmF8o?9`i+B05r8!nGn27b^%v)P~5pfR9M)) zMYujVv_jh^6$dKs3wh0_9z7&!iAbGz=^WQDxM`1g1NM&UNkLH1o=7Nr#-}jRzfJz z19AkE201yrp-Q08~_3Fxs zQ^U;@9{zZ$r9bc5Y-F?+jzqGz&WyIe#!B%*MWK(fW>GLMlg54vpi?EDwE!+S4tucd zz>C*mhCiCEaRc7kkQ~$ZtwA%PMG0AbJ$CgPmp;jYe<zIfU3%M9~VYAdI_+aZ8IY z7$5U+i-dpgL^NQ>J&XzNP=!R_v@Vz-wsmH^hH6dXGm6Jr<=;iOUi!`#Fr%%al)jKT zl2Q11#*wf8X*r`+Q0Zy^0!it+Xu{6)j=TZZe5$43sDg&b|J&ko8wvDcg$gG zDA0oQ25stwV6G*5l!#`J+u)1eA>iwKa{EPO`++)L!{y?Vullq@0`N2o3fjxzS?a0b z{r(J|oX3|8Vo=KYR%en;G#GWNtlA%_g$=ux+ZdPy*x574s|I>-YgDM2YwRi6Gn9FQ zYw}zIv+n`!(k0gYo_pwekk!_c$gl@67S!|d#&!y^TK^$I&^i1ha2}A8o|+2vEZ}hy znyd96`CaM$V$B5~xh5d<^wD?z?drKK*5FQ}BM0%qf%3r?K*KaLCNt%#?Hia{{3V$a z;ihtah^bKGdjm3{XMt}S%;gi72bQ_+ik>Us|DfDNCRPRs$1NVmJ{t(PDuR{yUW!n>(vgNOuQeAa@4=RyOwW+O5O%Dt7{9*-$au z(hBn}Xc}rFCE!@usm#;>+&>)pk7=Gq0}q0g822o#JTg|}d~)p5OKoy@`18W#0;Af085ucO^va1m-fvIY_%>alWahmv7@M7F>&t+f7%fahRcmV1- z(BdxD%a9yvYgdip0NUXrg{~vh?Ii4Ajd~ZD8}xDX#uD2{D_(>)cXC$a<=Tz6wYVn6 z0FAJ|`+GRb(3Y=`h70;$;T(tyu_rIv^sqXAJk;bU67jN3t$G8Sq%VC;A1eZoiSq@S zq~X8y567<~0+sh`em_J;KGI632bRVg1;;F;QXWi-db!UE-r8D5s5NQk8c$&KEChr? zss~GP*z%MP2wZs~XZ8^d3y#S14`psg)6ZpPPzSiI4vK^@!?W{A>4&lf91eq})mk9{ z_c4Vcs_5j4pnSdek?pQ0B~w?Zy6v6v@@hNu8;jqO{E@NHmr~v@IYQ$C6KkN@#1#8U z?l#poN%GfAc5*pk-63BEORJ0T1}G7v1?Ki!8q;{bm6j;wS^G{kW*IDW|71TyeVnFg zjOn;=@j`7><2}mUUpm(UxW=+cuh0Br-HIX^4LyX;I@PsD#KVVhXxSdjyXmSN1;;6| z;vsgsls^P{?i@92m&1r<5!K(D-(QkudLlIru$kVdP5WaKf@J>Q zZ}sP;#(p5Rr;O+0=PQZtS@WJxnkppFoqw0l7y5)h7K=f$ zl$H%ngL*;zSQaCVC`6{tQ^hOw6Y(yQ)tJV9w(e>Pn*i&9} ztjCa7mA*mbrPEu%&bdf52%Y-->2(}J#h;Ib)uPn{hT9|`H=#GJqlG)^j#h^>#Ly? z=0IW=F*Ia*duZVwr`TF&&>Wi)toBxyiz(biq))+4+}|`~+_re~WzP}IRyIv0B$0@+ zHSD65y%))DcL>GBgdQ-R`LXIEFFPRq;%1n$N?Z_ew*mD*N|KE(d~x6mo|4dJj1rT! z)4XTo&I*dTB!WTsjnx$D0E*TG1^&D9_!{86gD@XqUMjy;n&S|^wF4@>EUXm^j2K%2 zd=dSILQd5Q&HKQ4qZ!y!#0+($14UexMgG*E(9b5!!>{nbymG?cw*j+1#DgUpKW?<= z{0oZ8jtop&)NoW)I>OAJs5KZ{LM@O0^c3Zc$9C?UU~==Oc;6j1M~;fnu%(+DO6bVz zcPDx-Ug#Dih9EC`-^6@$)RFNWz^KTe@9KvGixgFw&k(szk~g7E!Q3ZdW8kZ3clfsYvDat;Xs&XwC+1%9>iTuGUE#RE@R06wBuE^O#&Ra#|Wx{ton{RLSb8ii{+A zNY7E#qoV9Iw9xgLY}KTq1gPhTsmV^`47sv1lgM?UuvPwp108N1pAXXUvatqCv>Mz) zC1k`2=J{jC?TJA0_a;qOfW8Slzp%fD*<5z#ToXLRP?R_f@8j>2B2(5gN6iz1 z#!6_ZHP+6rr*fm9G>(YhQ?TFzw%5>5Jkex*;wyHg1U%-wF*vc9lZ{e{vtnm*Cx*|Z zUK{kxS5PUF!8oKrhC$Hq=4m;>7A5SCjr5cjo131l9fN#vl4@+t8KAcCSXJh?fYpw- zxu)+*q*eT~5s31|I!W~y_FkVuLHfz4?5eFX+OTBXU@jGNLTqar{2_OpwfX#6W{Uk+ zoAighX8KeFoxL)M2!>i&-!!(sB&p7yIw!4u7yaG7v1f4Cvc|yRUxym55eErOtH8rf zPi-3onNkVEBNw6?b5C*5=Gf|-RZG<0$4na)= zfsP4+!_Ew$HTngRFii*&U<^s>@Uxm0tvtg4aRxVw0lin!)$%qK)-Ca=A%g8SZUkE3 zp`yRGPGPnn>0|!HwtAXGIG390Li#g^nDwQTk+W(0ue|2FklNI6J%SHGVq|ghgCfPt ziLHIEC!^Q!uI5YwxgcT!FvhYqusHVJ`aiwJn$u#Hcl{F!LQDj!@jUt~k|EH6N|<-> z3Z%t#;6o^8@ZCRA3rm;Ms-9NqxLa(3^L=fjm8bV2iQ&ixK(>L7Y(5 zt^CD^ux2(t=cDEg{3@_2y@S~sPufrvSAZ>1NVyXMsMT<(TO-}-BXqIb!=l5 zwLqSHE)Yjltlw;`;khTZqg*WYFRP%0LmW~*VA|#Mm$xw>vV_rC!5&#{LnYPU%@tQTsGzh|n>(H@rX45M_8|@}ADE)^kT)3^Kfo@zUX?~__+7;Sm zMv_noDvI?0PA zjBh(7_aO8bC9$7yb{8~!f~Wp=!}6OR_B#$7 z06(^t$6&d}5IqMDr${{hluIK(h3mOMl6-!h`t05OEOA`+0H+Q()NCg89zXOfrz(5~ zr_{+bDQ(^2$3hd=Lve8v`(DYv7qmlE6vtToPD&%{+SetCV)eG4nhF<-4N)U^8;8V^ z2iS@#wO4g=`6QcN)D{jFsys7N47N`TOQzjdZAefE&S3GK131n{q2 zTm-v9k{Csj;?H5Ly~MgU_(a_c!yGS$tkQKM;to!}*s3OcC>-_Lv3T*1bqgS@xA;Mk z%KquGkrJJBg-VSd`o6q3#^f06^4)n!s^br{ugz>i5;wgPMgxRO3dlF%%*J-#$GL$K z*>E+ScZs?|J64gRKwRGka3t<4mINlk2y{}^B2a1su{O`SQ6=bq0dITSiAID0+fLwA zn-j(HNFa}1zUL`@dT52Rx^*F_GE(9go5ITCA5mWWh^6mxOHiLoyS`mABgXZFmT2t5{tY!o2iBo@R}#OqgHLbRDc z)*okv6K|EWsfj*mwXTG!^@2r~6h05#W-F{e-`4@Uw)^5@j?~JAoZ5QXK2CzIF%u#A znxz$~OcDzH4@?lnoDk>f57~8WM&+j0)evJfO#3o|nB_?BNiI3ASS%iAq z|CZge%6+DOKe9{)xn+g{2es^!w*QO(j*$XC;vhmAug5w`pLclC)=qICBTUnm*!PaO zb`u$Em9xpo_B0J8wYi<3R;m~4OuMZc^1gN6$QQP%wy&*Y{XyOh*ZPz60O`=WcTP7^ z#T^p1hi>KCr50xhH1}vxUKAKVL1g2E|2gC12CpqzOcVAgy;R;`)r_d${7yhjs;!c4 zL!xJ{2eJUEYN~JVs(YX<`CI$3t8Q07>KkUbd?-Y)Hwg!&alW`k=jS50a#k-a?+>qN zbM-CLHgZBRMz{5~p@#nS9@0SeGB$`s#!CsfM0vyGq8<62X?gXHq$udSnakrO*&iE? zE#9XWCs8p=NZyY73Guc3yGd)>PcEHaN0{Im!{OHWf-pIw$H4(Pn{S-Yd01rmd>r6` zZ$+Lou`hP)5^Fbwy~)S@p(Ne3#59v)q{n!a2Fb6Fdh#$~O>+$Bii^FCWGG-bbRoXj z{rDjni}U@AT zu-wZZuzWVH+wkVx+DhpH-+g~j!()L5t8TXYG6d70LEZIyS8=YFLYL5x>~5l~mp2J_ z#R*+K-LYU4!VHJvv2D;ZBZ`swdxbxn^bPg%DgGWjt<2LDDTzOY&^-x}R4WjS3lVDd zBu$QMndiU0$7_X6K^K3ei?A&fs+x87%ZRTb(nFV3k|hQR@J+8hBcjcx55)`pSiH7_ z6#u(jcvOST2|(0*$YQWls0DGWnD`!-3lJ%e2^l|8bl}qE;o_!jxb<8frZ%;&k`@?3 z_71+wLUP;XwZ5x2V<1yI;%yUHB}dF@YHcAwmH>8KO8OIu*IZv@wnzNc8xK<|8N8m3 z?+=vmMOZt%y>P^y#7SAqFdhB1=jG%LsGJke3<%Tnt-%oxHSP5a4`SavS;NGhz4jjK zhP4AWJ;PyCA2$gsX@pV($qntvKX*K!VfEVl+#?Jtp? zH>}zmcqVXUO9?M9lF~lLxvjQ4HCqe9D4R;)PKe8AA~d~dL@{f<1@;^Ac;Kn-Jk~s^ z6*p1I%>8-I*v_+;V9|P&OOS#NhPd}z`R|5Azgn(IMAmfk#mx_}2jy{G^JP=z=0L6* zK;8Q%l|??D6C+0s_(tWVhVy0SK7xH{Pep^4q#X?(NtuUqJfl%!QLlZ*+klb_F;(DK zxxm_T{%mW^b{JOmmg~t_#mTQ4pvvd_{G)Y&6)gdFVDf6U^ycHmO>~c2>rq7stY67w z1lR1RSh0+cnYeIf{Gr-?laZ1{lUEq_3*#CPj24>`Qtfh%gjK7*P#pIAQ#ny42j>hM zIcNrHoiM(AqMeVPLMXqNX>th0_`{2Z-%!Bl!JHBVDfzCm=6_#2sZPr_OdCv`;O@^% zh$mDxA0}FPc;K?|97{p(bZ8EzW41fD%su_wbL$Udxv=%_5Jh*q#ch|jxaKXWzxWv5 z*2-$&LiyRtu6rTl=&eNCw|#jj&QhLK=2fZ-^XKC=%{90FacHH%azC_D1JQpCvVU>X zoS5I|cor;xF(-DS(?)+_K@lXLQkmv9M?H3SHKjLqNF%f;3q7Y{9p?SB0$F z>Q^~}_H@&Z5mwmLf)$AFK2(nG$fdJ4X=tqc77ABtS&*;q9Gg#IBh00L&-zBJ!W$wq z7kqSY0}U=!EJ)YpHwMA}3sZ$)DM-eY(5?}s4-!s8 zR_yoV2X@I&%`E|Zlh%`!+ zi;j4QM{DIe-|vMa8=Y2$b>N&DV;!Ko{{y@0G7CHXN=1;wvmbcwL32o)nXpF~;h240nY=qj9!& z#_9Kb!9I6%6O9VJIeTw7dEAd8LVSY1q~F$eKhhOS!s||{V$hd=Ork-zxQnAM6w~bo z558kw8T8*on1O~-=19E?eby_yo^fQdl#B{I_thmV4$grIVc=`xu3$-UVDgiK_|n-J zhs(?cNjd3*{7lDO2)bBvRA#Et9F+S-eq1eET%d(yI$98wE@VX5NE8aczhYNh#^dIF zlkJfQk`6LZ1R+X?Dh|kvV)|fm&(wxf6fyacmLB)C2d$2T>bQP@p=H2P zO9C(`Jd%(nBt$~vPY!0F912u5+$4#DwqS!GQiHYksELq#6tH&57rwM;94gLK0%Y4Y zEGgr-m_J|QtRV?wZ?Q2k#`X4k41nG9;)MFH?Dc#!b7HI{hIAeu=R99R&>PQwj-*_C zpU+b8MGbCYDBQ^r8Jg52^COO9ub?q=4Ra)d8bU1=aQd#9xB{Dr!TKl7A{f|&X3`2R zU{rmhkz=HgXDR#W6fX}kLSjI*P5V}Pz$(2l4MW7T3y`sGeUuN!>LV$rb6&q*`8x}9 zfC?p}x=q7CsBmjpT&IoDwKZvet+q89y$xi+fdCG(j0a@)#a6l|xS7O!33yaGBVs;D<#g#=&GS2LO0Js!(grXCO2p+T06#J(F)^KnyL>}fH$wx_#vE%GdhD7aUZ5(WpvQ=&v zS<~#0War|Q6oAg5WPwKqQx4qLlf|Yh2(N3bOn!j5<8$4822N*NTYy&Y)LD7i9lnzh z^x0rd;+k48@qxH?OIkwyag}Ry;!cKny9)15vS7^f*x=-N-{f)z0(Egaiae}9V$5BD zkUytUdJnCq(GeyLRJW;9U-r~#bqWz98kty-67xZ&J?R!d=%#)Yt38o-s;^~^He+>o zT73bDwxV?U5t_Swp#JghG!VsZJJ~PuX0-bx4`Tm&yvoVS?ktiEyH6`S!T_GUzIUoD z8S7#pvx1L+1?6=D;a1=$2-$?u3TU)n1%f~o4bM{`NX*1*Nh98EwcO}cH+rGC z^qUjLDUJe(AaTPl*<^BgU|Ppc^*1_09GHGVxr&iVEH%>xD89zlqZBf>5gBvS)E+H^ zN(awHn|~njdwl~%m$cB1Kzfa8MbfEBlGagE?oYJ6nK#ud9hyp>>RFxapNG^UDdO}4 zm$W#n_?MpTIMWZhuaheCwo6uQ8~|KLOaW!L8^zhGQ1tpZj3IIwi6QThnf91Y0MaO} zENB~5VLlWsC&p)*ph*WJ6p>!5f^vfeBRY&TnJ1~07V~)!c|NIq%<;M~42a@u#MMl2 zU|_rB%|iY%HQLf7Rvh_LVBsc$NpV(DXt9=ke}FL!ZDPY~q=Pth_3+n|*mQx0Y=&_? zqUDlZ%57=S?S9;yVJ85krzIY0*JJWq=a;5Cjv!ON`v-6f;~L$pua_m}E*b^D`Ib

    ?#g+vVY}P4B-)i@LX!T=k7fvT5F`YNAXS+F)KIFL?F_rb4%x8fv zAJ4;V{nO#8;vQR?NFv#*OE1ceC9r}&#z=PN*&DS8Y$AOi3X36RYDp5Pg-Z4wa1&&t zgbeE{sb4fJI&7+t2UbX@+|xBvs0p`_XWi$C`gBexOTq&=c~9NVwP14D{AK*iBdC@z z{R=E?v#Y%@;{}m9+JwJjWOaP-sZaGa)B=}>N(QrfQ*Hhln8I38J8(?egmm6QkL2oa z-Gf#4t7Ht;FUgR zlZ!7FEv{9C+{LzJ?=Cy0x>WA6?&uGBf<#fK*y{P>cRSpS;n-%z30JpqgUmFvJAhd1 z@-)BXtgd`&ZK$YsOCo54OTzTxbCIhW`6z2`9~DH~TVW^Z_p~--dgCOFS^Wh>i_1Mc z9ND6d_-Jjq1y?Q6@f#uVJK%ZH5Ce~Gy<=8~Eaz>ZZbUsLe8g%1*rCj5{0~+9gMPZ9 zFmw1Qp2vg98OGq>W2#mDq51(TQKTIto*~cl&u!NX)}(8MXheo-UZS-yB$oM!NfM*~ zhX;A{f6XzRwf450fiLT%f&QI7JD}5LDfGC=k^hibn!;>IK8^S`XiF&0o|1pMQC;CB zW5&6*rea!;vaD!UJ8Z~zFO|>|hA?v?1s z(WCQxh?jqJdJkYJN+_wDtk^eYQVPb1#Ti;^9C5ostzsx?aW~GdT=Qa-ojT(_2x`c6 zHUII!4WMyPL^vvBtMOy;X5Bpctt@s7&LV-DXE3h9+03~03V}1dC;37beEZyRR^rew zfSi8-_TAy8q~R;DCb0#o=*#DKj+E#K^WS>`QP4qm{j^-IZpn~?sEee9o*kez&{Z%v zR47dw&!$xCfV}u_w*pprObs*QO9d-gwUOeZM{{q{m#c!MGh=iHMn_uwKc6vG>UZbg z`Q!n5=2Bw~;#Kfdr9}&;=ASRg;V0oTE|C`K zO~;}vd3fierk2925R<4eT@Tz$0VE=42m=D5Wo7tV|Nm5@@_H0><}C0|)nFOAoc02* zyryVndGiB>=RND}8M(&?C~VQ;VFt|y|s2x@tIqR~56<{7yR_u3{>5Q5HfmbSV$5jpI$~G6fs*9)>O~iqfEhr>deXAB} z#rKh_o9pyQH6@iPc_6>5n_Mb0HCI*VO4<`nXHr#YG8U%}to$FsM2o>ih?5)_dmg+5 zx&T3UAOb(BLX|x{1=ul1k1k)UkxAgL3qw#u@~ytR%_2^Z7v|#NA8ENcqUX!9)^Md! zu5<<|%aIQVx`n&p;z5;5OK=>11e<)LjU6Kl0?>?@LzEjiXxD!GF^?ywWMZK(@rb-pj)wzl*8Z!pIbB2TF>ak3XOea}vQ{@PqtB>*VrbJ33#hg*5 zlwd`O^9T<>0+jTb25dR^ql3^pYCrEDX{jfTVnI?m=8p_VmsIAM%IhH1@e zu&$^~?4i$n{-JUAP7|Yi*KY(-wkjl5Lhs7$C=jk2xs>mHMg|Dsf7FJ+SYUudggRck zQa=KfC~~rWlp5&nEgI8M_7%$A;Ok}B(oWK+uj?C{7)P-?LTnxo#W1Nx)D6OVgL^_{ z7tjS>##LLQ8;W4mU_;YM*vaIL9OA2L;MtA`5|;y- z%f~Q7$w&C7@gC3Yp&Ys`UYTO*5A`Ev`(?t>L_UfJeKvDBH>=<|0q5XyAOD`xxha_x zBhz;WhttZ&pYZ?8NUB6bmatbEw0`QokK$+Yp>oJGPea?Y&>#(E8RSc~8pZS*%b;zl z;)7mOq-ZHYnTas22h!DVorUNn3-e*{=$d|`e|nXZiNJ0Ls2 zOC(6Iw&(p@ml|f$+(w60!Fk@dNQ>?Xf#%DBDpb8mB6}6v(_D4~64p<9pZTcYo4&3l z*SZ5r;ITSF>PU8RXFX|po?5UF(dXa~k3?N4m>`UKy#9;Nw!X{`5$Q!)B9j)){oH-G zxLMt;@RBQFYn8-bEF*m7h^1DACdWtk8sYK0Mu9hUUqcq2ixiZ&j(4G}uYYlUPoSjZ z4J7Hdk4G*T+|dk>p2R>~k6Yw`;$LryPQMi_egaFP2%!Q-^SvAMkQZAl%?o#2OyDQ) z1KnZ3B!-lUVCOkmmFF&vdLq=QtCPE^)}5(CsDY#&a0ev|Wf42oo5xaijHMr^Af3Be zF`Yn__D=_T7qBXpeoa2aM=x%*oNdRxV|dF+XbRz7E4;A`q~<%+qEO2D03mC9JyM~s*&D~O^_@!6p$ z5W%m?#u=f&>whcqP-ee$jPnCRSXu<72-t4mkFppz>P2K?FP*Hqp}lfQStYs+&-4Q1 zkl_)9y`GRIQ#OG=t&czA&W zkIt7*q69eC7QLBK@Lz}JuqTewL1|u|S}hx&ETg!MZ6nlV?%C7b+h;#9NRT1#ZOBlEpfHt@)h17TioTdz-@gua%9q=zQ)tLx{F|HcvvkV&X ztBhKZMg^BVzQ;3k1g34RD*ja(Ix311aRSA1k!t<(%(#ZQYL zvF#J0cv!F(aO3L5JgFVEr!&DwFRXJQ0*N$`jSp1x#i`Vr@ZSMpBm$MdMb5C(<9Gqoz1AI-LaVDmxvXkk4Zt)jW_|EDWCmS@OXFqFC=upuBM=N$|0K}Lx zCxZamz!2nUyg7z>+_1ltYt8XSWj4KqJW8Q~?VLen(c7}G(8z7obmo}WysnhVz}S9E zZ9zf*ai*VDkm21pdPcE=c^@i>($kBA11OzV(I#6y6SKpVIKs;OG;%ae!-K#?2D0W1 zn@&c9(+?cz5LyH-)Rml~85tnl(9+hT8zI(K(1MTs{hn_}>_PQN1b$KVSw(6ilO>Q} zpSV@dnpy0kB=dj~iwN7x8Wmwyt>cMx9zqejYS&Go;=Lt_v%D}`6 zUjbIoky?j$shiQanE(7bH*=(e7HWWa&Xi3x*Ck;Y;O)*hitv}7?2=Fw@XC+PvMX#$ zGIa&aaY|-JjPd!Ga(9UGI6Z%@I4)z%c95RJsJ*8=kg9)h`tUB5_jp2~Q57e0Zg%p4 z`If}NQOUdp_H^|~V4wD`onfE6&Qf%~7W4H`uw0KG1f&JJVE%WbPM3yp@G3Z=cSocB zS8Ou$sZE)RRrZbyq$2GkKJ(u=--E(QA6o8JyrWZFpc>jV8b3>fX+g83hjw56S(FB|aIZca_d)lOA1g zP`VZ##U_gpu2XQ5E(KpoDdss>QSEu zYOiGjB&Py>4HJT(04!3L^Gw)a1{eIZgOMiS#@1x8G2jikhE3+guK;WsN&Nmkaou)@ z=nysC+R`CdZ{)Yr zM3?PAfNs$6QC<|R)Z^YRD_ZY2gk-4Ducz8y!8)Oyt1o7?ysFi6G==IJT7EwZWEDzf;xyl=2w``icfnr5Ns98R*y^ zod~Z!uy*0I9R5u@kcKcxg+)Z&ZLY%B$9-3J?_9lxPZPc!zQa}GJ8zD$vNAj!`QFV= zTQUg+{Kb0IeTAst&WGp2*7CHOqe?R#BDxF(Ma$3 zK@K1;ZrbZCBAVjN1K3;;Sg9 z{+Dn#W#+Sw+8N5}?wh`ZwiNGpcV+?k|K5;(31Tu`XY#6g$Ky2~WdnGv1;g&(=uers z^%`HGc%!_}eE!*w?tL_s!#JM$Q_j1q2`O%|3Zpk!KVwyIqA<<@^0|P>szWx38y}gBubz-KG*h$7w6CG;Um>? zoUZN_Yfk=L`Jbr=bmWdmJr6_B^!X>Nrl3OeBF%;mW^QIIFW30Qr^o$rh~o1p4%*2m z`+D)?9T9R(90B(!gL9N{$BCl)8^6z=4KOw~%SSAgC4pGy;nxV;=xq6!QIg|KH(>ER ztrS%X(Q*X+!m`2se2A#!f!G;6!cWA@3iwIP+S_<)o~M+iRIoa>NK3(9P>MhJj|P57i0 zTGrz~!T=Nzm>n~vADe*jH}%&ZA5-;|gJvs3cPWmyYqkNh5t*3fA2_=kuKS)ZLZ}}I zViH+7b>25n4D9}bK1VXpw~)E{*u|*A-V}6a$xu+}#gM_9vz>BI6;isLQtE!VCYS`O zGs=2~;Qpeu6pPaAqiNCw>UMfQJG+3cV>g*_W790i<&=+<>r!#2GiUnuZooKiePZmS znKoz@eosyg6g&+qWT}4_LGO!Ntf@9jlZrtIrJF^9d8$c>8q0`8k~MB1ShiFA^UaLO zHXo5LKPPy#e^S7=#F;#U(iC^ngcHO4pvRR@=aGhZP#RE9b|Qu>W-Rts;kbO74o zDu}ld^%(;|yaDAa(%C)$>AKHcp_D$$gL5f_V8_S@wmfZ#f)w-{yzeB;3J83h1nq^L zg484)fh~{p?jl0yzP_a7#aE=Am%{yhsANtLs{{K7ML<0S2gCaKg24>IFv;e#gCpsR*^wV#?e#R0ZT^hAhYcdjkQk-r=JCS6Esnol z=l!sW7IYk3#Km0Th+Id8-*f3`f{&8{MD?_;B!{Z_>?XNbg9Qn zo>9un+#^VET=RR=)myO+JIDBj6lf*EUz{+i#dAknzw18a^?5$!SzN1|;Q~**cU`PB z(k~S9WV^u+ZPpdoR#f+5aOguRT#R}h^di?+bQ)4nkV%$P89hNNyyut~7vNy$9cB7x zmG2o^JY!Adl0g6Cly-1wzC5ZS*vd8{y1H0iXLnWA#68J52Y-#u)z**6hL*SHRivV6 zUr0dpEZJ!*qoAY^i>3Z$pF-kIQACq*Z7^NaRQ%a{->5Ii)>Hh{N?HCmcHGtr$xH~XG#@3#ED9f`Z}?L zfsEEcI+;$Dc&11q_k2T`J3&7-r`7zO~nT^s9$hG)K%B z<7W2c^~_mk^td50_+f;G&t!vkEeNlDjLL^1eH-K4Uie3Ik`;u_vjV~4NTVT=sZty% zxFkMI=}})Ws|CHE??L>~yLP+Y>iQ zk{H9d>E2&&Sk9$)#CC)Wa(zY0>vx6E?Cvg)7n`G^8U{uy=0w^-{C5=MJolacuOS;Y zjk3C2gBaKh3++cx%MjNdYCOZ>k~vxxB8aR%L?C^2@<^$86F`@wzXXG6W+k%(fzYvE zDzTEDuPmev%m4tQ$s`L5D`}%aIL2g^(3Q;4fagL|z-8g@t+X%ZIop#P{|ABi9!?Ta zgG?#6fs=yP{!u~VJcJHkrXxb3sH3;j25c9`zF*)0 zZ|;kcS`XY7#BJc}14;>!`;NZ@=lQ{5Z$ zp`!J`xwHD*xb*Tz`fP0GC;?!mazZU@UWgh zus6TG0+=L?N!~;*BzdzKpu{EQ&62<78XsaLN^)?@ao(Y+P?%=mkcs(e13&5CZo51v zRRu-cJK6L&Nh?Z7Orp~kB8}tZ8XUFIOl{Te6DJ)(bWUSBuipCEuKBPPQg~Ip(q>xx zKtE)n{9CD8k9BQzkz{1YZ#MPDGX42ru=cv)!u^Y`_wA_hf90qnG}o_x=~Z_b%IO|6 zfazu?bx&SHheyfkn@<`QXQ?_K&YcDlH2IL=pEe({Ks{3G=ZP#KE+*9N-9QcC{W_jS z8tzzenq6gP(qcNhBLf?|yYC|wrTL_8h)J-R>*$CirRjou*3QyCI2C(Y2R^w6?u!IC z?oppA+;hIh0t<&F3EiRj5sj7Xs`lykZ8}+B7AW%{WJ}(k$vj@60=pCa>h$~8sz-IW zYZ!y6C%C8tA)~)W%xMIK$&BZ?F=i;7duU7SdS%te@^(S`RQ~uqB3V2H8T{i|op$xD zk@(WdGokM)8!RxO>>SoxyXQ2>HmV<V#7OLI1b)>9{!Xs|uqtC2>Ni??1~R(W=H=*wMs&44!1z zdsz8A)$jtAJT+x4L*kPlmtVRkCxUUi)6gm09$`o7GN~Oan4xOYu*fC44dR9X#at>y zyU%zPmL{!+{@N+3?w&~BIET`Z_@S?RggsM}Q^-5f=j{nOnx&nOj#DN4f|uV$7vJ4N z)t{uWp#Uw{ml+UCk1i%Sa<5&DQNHbl4uh6kPNE(A3R+VnWHc&K3fNyCVgrm3<;$eF zg%k7($*`?_ve$vB>lVg{^V%c>0(jq)ejDhy5C1*3=7B#bzF6KWH zL%b4tY5ffLw;Livx)Vh(lG4iEVO}(uP~&wY59T2`Piyyvs}-7m;oMzlUppDlh1M86 z5%)%q8&xH`UKT5v1Rcx1m2kNZWv;c;FjbcCYZ41znttamIe3K12iCS38_L14m-L7g zKCQr>qWIFAKDrJphN*v$e8XMBDS!xb*k}LHXvE_#9?$w|ek!+|0NzpGRcAQ8Gyi#_ zER+ZaaQHSX3E_!rR|*uTZ-M2fdo0a+>3^jxmJ?6$XAkMtF01u|KH49AE&7h>amQT* z$A%chpe$(ARiT>AxU`0!h0nIxN$D7y?9KX|PD2jXzwKATxQIo%w6x=1Rl*%h-Me1j zQnnA}>_=EPpVCV|{y;K=yKlFmggBAy0xG_g?Y@+PMb8c}WN6aJ$=6*p2)V8s?zOTX zLPc=i0FAk1(+32WZZ@B3XI0;Gx{4M#(4;vg)Pnjz1CQE7hlz>9rm)!chOY10f>vUj zNK3uXs{sguIw0F-h7-}st&s6HyLY?4Q58?;itmxZ;ZtXp1lO8!%Hdb$IMQBRIZLjN zk6VPeVE`Rnl|vorJC;>8+7j!J)>4(_CJq4L&nf5X*&#>YOdK0i;CPK(VnX*3bIzyA zX_&Jr2#bgCrT>TYOUqELEGwzpBqhd@)%)TWKiD%Ak%zw@a~!`MO9_Me=6ru0XJzfc zO~z4^YyC&i9JuNKYC!u^#hv1|3^fRrs*6JG)=z2#S+~dKJJKgvtlI&e9tdZ6PUZf6 z%`-?`vCIhBndEYobwA&^8qWV?Int|SmiRoZSXKvd{?WsxI-5PT>4JX&y>TI*{tOSi zMoO>qlnIVf+IA(;o6tgh5AJ^|TfxY+=GNNgQdfGEjKC}Fl~@IgYEM>8O@>p8haKxD z{zEg64Qlhmww?~Ls5-7y-p30VLdjQvyMRPrHkw~Wfs3Z>)os-J4{?=zp#+&jxdwOT zx7Tgpz!ptQqc=w-O2M%Ws2SNVJpkZtK0qG^fJu+naLM22#@ztf)(+zB4=50$=c?0q z-!JqoZ$^~17TYSKH06c7xIo0m1meSN+9o3geF#(R#s)sacl=xt9yx-xy!Tm~kVuhl z@Hc-_4ViiARr9v-xmap5dg|LwS+j`L`tEQPk(ICOJXUb@yx*?|HkTq9X~}(M?U5CbO8VWE=esw{|Q_O?&cM$A`H>8&K`u*Uu7o? z6{hw=LOoSoo_#O-<4>wziLWP2mC-@=r`>(p5^0F+BhPHY2?qEV@0T z_4s2W{8K$V*eVf6&zNoUZ^syyM=U|Sdl+J}QZ&d!(2!(^rIjWtG!eKF3oCuHuN1@D z3d|$QxG;dG1?>o%{PKyJ2ljAe;|W$Vshg+Z01<=9K09{#j zYunHv7FflUG9VH;!EtD{MT`RXBrUBRx`Q6E+dm*@pJ|Nw!5*!@xXmco-zhakAjRb& zK@BU8qtter&4e5$UbF7~Ts9T8WlruKTG4Gb>__nmno*po^qIHbIgt!92Nd-wZ=@0Fo+kG2)hA&^}E2HoRDuU zM8{zf_z@^g9JiJZV7{pV%JH?MyLSqp=Y3=83aPHO27rT;${fGU;vw>nGGAJtn&N>U zwV@xbG2C3!K;7S+;G(PuKz*&mgSg`I1z&OXri`7V9Z5%7vsQwjZ6ewd+=7(omQ~e8 zjTSf5iJ)Kac*ukp@3Q3%hqzs%vWpQJ2l`G|;*=4u69j(d!}}~F;86tc3V8s24(a+Z z;3C2w_1I`a!5Ehn*9>9K%+EfeIeg=^)SQ3emv_L!2~pCf9nJwl4+@Lzq;M=?^`LfI zsrx9I3uqs|wQA&bULdSmc_PghNB42_@fsaCPAIcp62({WuZHLP%IJbcF^Mn>=3%h5;Nug~6dXO$35dfJ z--d~ij;w|W<~|_N&xpK&PkeAY&GdBo3Lh5voqoW~ags|~^6kXL9S)f=II_Bvi!_^* zKV-SIk!VCG!eswZ3t@0lq)qZe96_RkqTFVtNQ|;kkZx}3;2|mQ@OzXR!iz|!VM=WL zF};bBZdz-bpj;O*8NMY#Cav10*uif^$80yF*Zv_5H|{3#yxY1#6BcM(LVROGU45Fgl~;Q4 zYuf{4&9+a#fA7=^rkBDPXznPcP3VE0Sy-J>RQ|z7wbUC>ckcT~UsSAvuua1GX*4 z;{EnLnpr=drU4ZRNtHI}Dp@56ppCB>9vhO`!&AVCAZJjHbT3fXWL(H4 zEKN=N|CbmqYb{i+CdzFpW-Ts0DxUbP1nUgYc#;z*3n~SK7b>bL!=7xBKZ8*2M{+NR zLutMvPSav$<#fF(UsR|I`#eu-69%Jx=2I3J<3s*b93ko73&u3;Q;Q}9W`^2HZ2l+* zYr3wa3l#t(8aUJAt(h{9r~AOIS-Efa3&VJ_&}FBcr~m)~=98h<{MxM9YGJj@tkV;~ z?gShIHnSF7Ezd6%l8m*`@=;3ff4%Sm%T(luC#IyWzO*E8*+|rR$Cw+wi|A?HIE$nQ z6Q+NKb=7qMUW+OnDY9vPgvrotNKbCJ7bEa^{iY zu)ExS=LUp4$JJA&7yb|7V0e;}1>N<7ga&wuoFUsZLH;lWqyK_BDxR^zi%V$+f34J+Z9;XJx`47Lq#tR-=Z_O442 z^<#`rcX{Ni{xQ5N2Rg;@@Q)~E-MnnIa>|lnY5GAC$09Q? zHgO|T3|XQBw2Ua+CzQ{e$U=a-=|rGB=n4rg;2lA&+InMpinZX5SZ}d= z`m7kkh1Kbz3Ji-*(F}s=yCrWpB@3($?KE}F7^V*01zMLc?$iE}^~BXo`iV70j9(YA zllUeg#U?(_g-0TYvNB<~8ucZp6eRPqiaojX|9XWiJJlxb-axH6Mp0>&o#>lBL1192@zQOyUl!!7Elqz572t4VQ?G&pccfByX{4$H%JSM3o|) zzcBaqV9w2uyMC`SY+_}t9nu`H*kJ0r{K{Q)Jd~yz%3??lfZH5EdSTcjiV3>|V0iL$ zt;`8HYOCJ1ays$Y(uL7<=6;-}godbq+!JPpPLp~qV3S8$bghy~InCzv3*VP>FdKmU zuut_y5Ezef3KJwHBL6){i=kXcEYD`$Gh%A3B>_7WEB^F+eIj)(sN z=P%W)JxA#w+w0shP_*I$-Gt7eTirCiBK-KX*y=*e?& zuUU%nPft4CXGMhVTt{281CKIn3H@n0J@}|PdOkc;&7Wp97NWP$y+vQrj4yT<;o_*x zk?Lo;X&w6l*4CAiDhe?s2|$?$fR~Bi1|?kfhY_quF8~%cgZUUi3lmlTkIpBfs^P}H z1ITT>)r}8i^VV0fX7Ozb5dP63RMJI>jRA&F3+r~)YCV1@q1BL%4(%0F%<%OxN^Nu{ zoKmF)186idWbhj#zHjH&0Z42JSJeVg?SB?JXKSWaLzfng#A054sif0K-@yO=uvWQt zX6K12cLYp@(GaRnKC&$23s1rh(yi$?dejiP${ zKxTj~!sSAx0;c6B!Nhs%o@@uQN2OQ1H#r+5x~^*${HKG!ZPBuKI{9mi6`q@`S%?s$ zQesfv|93H9a*tp5*bxyJB%{8wkR^7pm-r|Ij)j{A;}F3S&;cgk`GyDSI;D9|ji~iq&H6D}ronkd?u4LbYSvrj#A;jX`&J z$7lxvkO0)>k#Z!4iUezeRpG&+Jr4w?^V(lo6JGP)Rq|(3Rz%L{Nlx4{02-`RI8Qec zdk6c&z3^H@4N@O%_aH%NOR~?!6w$0N(%uQdPL(GY0wQ(E=W3Lm1_{wD?HyzTd4Nd@Pf{bMp5o*~`sC?*!X)=+sIc^z`kHT;y(fgQ)y3 z`)bOBjeQR)4q@P;jb;r)CW7*Cojac6eFNu zRol9{{>zx$eoJyZVRD`#OhM=;QI-2@!gjt4;u&pAo7bfc`irmqGzhCtDx zbsf{Xi7;{edNGT z-rhvE(SqR$31Gfts(i>vT`exqywh0isLksWfG5pE+|YogpBu%t7FxFB=yPmh!eRg* zkib6LXmb(ut?IPjkYO8d0w@1`&aKfA1K72b8ILI23w31q?aApi6{sa<|8D!hqYm+V z5_?z*0qYy$7)VRNsxAq5vrEarON}xCjsb8gOrhflE)z&&mZ1DwpJ3+HyNSLxElSX$ zP@OOKZREpdX+sckjE_7hEr8PdPH%X;0{t5VgX;3TICXa{NCo>V7pq!?M9z|PY>OMP zHJ7m%*>lGL5_xnJ-0$x|QRTJ{;mXi~S>>Z{fCl+e&^BBZ_b>_##a@z);cH%2n{DcA z1Aconnd<$@MCGOk?o1XIObtpi=fpQ0up8V4)S_W_vQEU-Y5uEm{54clzCA}kijmx> zU0iCY<8Cs|(F_d~RrrzjTuxQsS)(c9yau!R5d_(9!d_VQM6oy17u{4iQBX8tI$+eA-?l zJr5@A?glOYcqLpOs0ds^yMpC)i9uf2i>NQ@640z#ai7gb8=40#A%G|yF`Rz>R@Eu4 zmE;J-$-ltj8UKu2jd^eJTi@O;zaPQy@Z=Pa|Hl!I#p8=-q?dHg8TfwiqPk$Mzm|KO zo#HDxYHrv^sa(&zpS-*5qCcUxm?g;USXRJ=^+n&?{me`3#-kBS>bIy_+U^5D@-;c; zYnqZW!Nh#jB_j>sy)nZ4`@%4s=}x*F@uA25j0-5K?mZHPO=&AK0(|Lo*c2I6vb#>r zmHz&NQR6N9H_okdmX*s}`yuz-UgaB`;9r5^AAS$=H1*Xd7SpfUBEFcig+5VL4j}Z54O`El6ECm+=mp4D>(%00#4P@20$Us_&uw-!w7? z;8m?CMY{V4mEFmfLOk8hO2Y2O`DrE%3;|-&2t2W+w>h4!@1hRK zXbL0}+6f{Go##dAplYbf7k2)2=Qk_NUAqEAtvdhfd1m!facBF+ef$ETzW_$cP{tOz)FY^GZ_ee7GyL2aJSfz*>*!G(BdL|?va~pPEdM*rO zJXg(!H&+~HJAh$uf`fRK`E*T!flG27DtoyG0vaO^P-k4e!^J+q1t}->jZ7FgpaqI8 zhmKjJuD&G7N}%l?gBPC4DT|GSw4yC9#{}iSB^xerD$gBP=CyDdWtn2u2QzX3Sum2{ z=6IVh6<^vL7J?6+v!;UIb;ge@;e@@R=!nIThACdvGOg+O%$F~3Y=hsrBBw%x?Z@M@|Zj+FF79!$X1HL7d- zA*d>W3rL8-TVuUzQFKw|2q3U72C4 z8v>cd3o`DKQ&=^sOCZ4oMz~^z-i(;^F>vb(|M`Jl zWDAvvqgOQF^%!5btX-{Ae^u+T@s&qMkpq{^9{ELP-J1h81NEa%)o}vcj<#>B>#TVo z*SGWf^bP0cb&{wFyd~?s0yP}G44(UC^)puL@(y%%^+trMme(fS8p-h4(_d>=?^J$V zz9D_g_jUtP`h+-YdB|tCS8lRq){9?FI_BQ+uo10NjvO@MM0~om*s-eUc;>9DHQa|+ zw%<#&8Pu3JYxeYBt2ws8%B+id<0Ity8YLLr1q-%XhwXqzzOM5JM}4+0+8Y+U_ak|Z z!@8sj=P9SIIM#7%_I^gvOiE>ErJZ`1A-DWMg8U90&q0$7AJR52Su|$T_IHTwrUeqw zlb#k9U-3P&htvM${C?$qWAZE=cK+BuHr(swVdhMhO{iv1vF#5tW2w=*b&BT}$p(@q zU*A5fmQBrwD;-xCb=f!W{Dp${l=i9t&f(a3CwY;_e#|vleCK}d2D4bzHJ8_o2;S^* zv0>oZO4YvDBdYoNOO{waec9twJ}G-Lb3skO$yf22D{UI;j=|6Rzf?SREFegQ-fYVI ziQc?xI{n0fCf$T7;SWpXavr;NCJiK?4Y?oc^0W*|+xqKL$|RlY)F+0qM@$ZdF0~78 zYIwZ8z_}u1pZ3RT6#-qH4#%#^?r61gh?#a%DbqOG{lo5Mxh3waIy5ayxy_ zW{Y)qw`o20&T~}WZ1>BwGo|@BJLpN}=yNyw%$KVBe?G4Bq3vT7W9c&UD-t7b^tR9@ zygUE-cGtA0yn%DEMbUCIu4F2~PipL*A8BHDZCqI<1~|uAKmj};0XdaHpbpf%&H8{L ziW$VcI$(Y?n0QX=-U6iSdCxiL-vVbMnO#w9J z1|om6^BpQEFniVQv)lYgi!D%cIpN)Qd)&OsOC5S>0P2D(hvxx6sT~^t5+nhr(0}_u z6Gs3&w{P+pHv$D1*QWF38GBxYuO}h+QbEqu9;WAHH)na zvpdrgoxJ_}!d;o3$*ZP+TxE4f(I8M~`_gi${U@!Pl8kN)a1(!Ly?!uX0)6iFYm0MK zLoRbPq1YIHsiZThr%^tNhsL^$Awq=d7RY_7TvRloX!pxIX-bGOVld&Fj)YE|Q3oJN zVxnXaAsGU442%^Tj*MGz{dZ2yCzl>VuD&RPgc2B_scG>Meu&6`%_|=p?W|vmiL(Kx zlLZ!na)z+6z4cahynV)ck==A~$%Ro~%$(}J(xtvjZj+MUYS9(fIVH`>-=E@>Gy3*5 z^x)LR`K4p~-#V^8*(hgSRxy4+Fi42z2f8S~AVc~tY?7d(ISnkZ4D6-@x^f3G%=G9T ztvTL-b9BbDwm+NM$T?~*X}Xl#vp1emrzOdJJeGOR*gCoM>%t3BA66_mcO?vYlx_~Q z=l|jPa~xV&U;|K)hNCe~Is7I1OfKNkA*(gs)NYN{XPKdyRa&Oph4s(3fXM|3pXGLV zC2jD|eNm`-Z^vpoWyw=O=rKl zS)E?&@7NCZ0dO0DIDVx>p&}ciWQ}skaUvFAVnEqnlM8?fg$Hj(ug>v#1`reg$!%K% zi4^<=h%gGe$HvF8nLn5qg+eCM3BV7zQH4p1QVhH5KT5j(wKsUG1vM0#{-~dMNb`}VUNJdig1jc|ZCkC{8 zUgg;s0FaAl^aurHe<4Q1R#1on03W3k12v4`(b@cn+*)V}^a~6r$GMsW2W&I*RZ28^ zcyY?vJSKcllK7__Z~>*boO722w&I46s$5CTmT_+CjLoP|!gM8G>AlllubD4s;^!+0rBMOOL9;(W7=j@!?sYDACMayxBi0>?Z@DEd1l!T~UDq@-JTQ z@FaYS^MwVD0t-5{KF^36YB^)8eq|T8*t#z2J35WMWYfKId3#yQ^ux;T+*Dn)W`U*4 zo49zc+Hb{+i>Tcjk6-{+c*q#wnWp}$9Gpi+0f@yB4w}aT=Ru?dkW%sIH-!=Nmxj)W zw#&uG&jhU=zc#VSZe-=e_pfS;C&}Ji*fMvF$6Q@m&jk$0*pc=V4@=UgjF&uIxgh$S znOld)^@1;>O6)6DgzbTs06~dMM1KXr;v9;)yf_JQ+#J_{`8257dZIxz_An-%i+$MX zKIwGF=7}G5KfEwdQyg`TXC>?Fn<=va(EV&6p&WJ=U5x3IJ828w4dHUfA%m1(f43w#=&r09swdQX*fziBSYfei!j4i-WeY zzeCbUEqXjSHen3wTefDjD+GOE$U4S_??;1{?Gv9TplRfS8NQ|An0z07vhA8 zQLqQ7kVJI-1xyV9pal~Fzjt01i5UMALQJen=s(09G@XS=;bTlf#hjRJnm^5h_JwR8 zz5bDYNBX@;zveLG0}ryN&6s&kVqcYUMb?Qj!}gjJbjQhzf(Tm)W2WLsyLuE>1OCA= z{^KNE8VN|Hcnix*`D>I2C1SBYiW3|(n}ulco7bJtM=PBhHTl++aFY=mT;vX!R$V>i z)x7wHlxtd<-puJ+f|J*32d$o2jE>K(TJdz7?ph^hx$AlwUu2g}^oApcuhj5X0~y9# zyeJ^B@;D+w!N_hNAsilXrdO~KQ&27sD9RbiAy<#6$!}n}d@7vQ34aYF|H;X1zok#C zrW*y%&3>|^j+KCT|3%kZWPBC722t8c}CO>!3OR=4HTFU+DVzUmVZ9UuC+aoTVgunmO z&2*;)w{MpIK+K7QLLH5Qh1jsz`?HiaxULO6^8&L@b zWdCb>v!Wm((~^dpi3^!9xcFH98sxa!0fgM9^qdLBaD_fcUj`$x0}I7T0%*P%K$3U> zNC%d3DgkoD67iL!UgpAwYmtD^H!{nC)a2}3Ok*XWW_)z;cLP2RAxddUhc$JLGUPmF zwx{D3agVbOKsuf#L3POwUi3f(7&hg$wvp+80^nMK~S=omvr%GB)=6DG3W=jh6X)36`nLSXdmn=_$N&7#9%*I3I1V|1iZ3E~IHuq6nP zK>?Tso7hM|^P{#s{y?#CIqa;H7yw?0o49_6E=pW083yzfenD(QHYrN!+_L3 zHvTr;+`$w`2$WFazW`*mK7?lX1n$_NFV-1dRgC{4L#;c6adpF0CUUOVZj4rUjzk61?1V4ley5vw~YX}J-N(6rhsyM z$zDb*N8=dkDq3w#ThW~Jxu-@-UMrc{rx=X={xl~b@l;0h*38qfs}LLes&w*-?kzlH z07em31ZIVUnkSEl{|pfJ5C^uuu=^l71lHIP4l()^$6+*1>?r$-g}Xz@T+mB{2HF^} zK#E2JbLz*58&3y!C`FYltww&eN;ck~{5zgW8<(i9*<`!!#Ffk~6Q_?SNeAbx9|xc} zD71n^=4O8t!DozY3&s8r(U^ZQ09e2lV{z;$EMy73yCcpe5S83>aT5;|F5mU0A2U+^ zP2ThpJ&@~9o@MPl@umE?C-%!frYG&e@&mfx*c%98=gJp{@eU^}C zs)ncT`;F3GZkNlWoy)0@LRI{KfDx{%yc*@cj(EiF%J$= zyC(Q|*?6=+kaAL&dt6H6`YImwI$bN}rx_hoQ{%R`Mi#k|%H2ANcI#omgPytE*yo*l zboZ4t`c8I!X59Z{XCjCOU_Jow;E)djw-{eb0Wt7p+>BwX0XJj#4+kzCHljZ+5uDvw0)z(T ze*(-3pU}99AXrS;0O+v%27W%!>FxWG$`ljxQB`Hp9zaZHC8(+xON36*;}SR(!8It~p2P4@&?XqQHh=O(wAjof z@o=@;SjZ!MQpu_`6RV@vCGHlEw${o|1$z!PJ^g%Y{`>`OIeG299`e4ls%Hnhl9?SJ zx_O)Cu2(gG_qF?DRe9>$HO7aF82Th1-@Lo80SqN5K5T-bT+*Qr(y&w(lzMI{RMDigbyG<>*4n-kOiZ-uzrqg7V-)_(1M#a zb7g;{X}Y67c)A%IW-6&|Kj0S<6JXJEIY!eXr*)}sVN}tV4_jwH@ml%u8@HmQcDr6g zPU^YQqY^(nv#d+{)tv*}-dw;KtKWmu{Y7D`L%yiaXtHG=GIcb*j!w22Nbg}7Qe$RT z%)BfS6RhEXX+(MKPUZiS&je^>%%3`!ab4z{@tyj~3#5GH6Qs|73HfRg@jc;wpI@)+_QTkOsgBgK20D}ntOotzvA{`2t3c@A1 zLu~g}Qqs2(5;u52--f}NgB$^Uu#b+FV8RT5^mX&P@dTxHvu$XKlFjS*N!GLbuSe8X zs@w$BAQ^`UYPdoK{F&kDO23sbAk`P4i2_z9d-&o-Kn5&kue5aTEZf>p3uF3^F2+CAcR|GWR)^0;9DZxO6xmr$Z4sq8$?0`m9f>L*;7M8*`naR*?wiXMi20A^|6^VoSz z6LrrMD=R0kec3$Cql`j7Jgrl-pfDulfra z{Fs^bB7$Bpz$eafVSCT%+svoP{-GO+C;GghBp-N3A^ z=7rbWrt@`AD|eJJibd9e3RMpLZ~N?c3NNERW{{Uq_!dJ7Z&a z$m#Q!SK75z*nqkGf0@Ugy}phi#9{|mVm=HFF(GGo3={U@9m^ih=AgS-Br13y31n1G z;@Ws?OQ}B$);VM!zEIIjqg!*{3ZGl?r?U>dWv#}n%g$~4GGNpL&b9;s~LW z3h2cbsT+h&n(GJLuqhvUtCtTXP<3vn~LL%RjYq{Yy2S4qDUsXJ2pdRIAR=NAS4P?JYT{ zIBSkZt($r;a{s-L1AQ)cH!fwyhLWVA1}Z}ukR32smdN!ImP=kH#Bk>ffz}>h83m39 zA>yE17RefvOTd;Q4(!>Q>s%@l4MtG+f6CB*dpriEOYe5b_RM;V)&}1TFGuxf#k-iQ z@3zeeTRZyNn*R4zQ!{=lj$Gip=&p%w5yawh{zeU2!(qTd*(}lwU?FnzBQ!V2yR+QfRjBrEUMgyedjsC2tY}+Q zAD-n)fJ&}XWh*Ia(D{H$K`Ihfj)iPRhH$7rusq5o|K6U}KWl9fV zu!a8^dsa=bY*pBZe*&~~Vt_P2f>JExaO@>mByaFQPT2RAnrUVJ7{g$ulI`BIkXn|- z?O3;~3+RSB-6VdNyh!JyNbKj_S01(Td7;;xdiJsQHyrMx{+3;vt;kzB&1dz&mlAy+Co~^jg=pomXZm6a z7~I&y13PGcL?AXo{v5DyMRF4zOTcx3%YSI_15@<$zmQ=5%!HzwH?e;t;Cy0{LO`!* z8hC42vQGzTQresx&hCBU9wC>ZS)6myM8Qld?Av|{XSG*Zhy(+1GPoZm;Zj%OCcSf} z$yc>IUhJKp;cv_~=a=zk0UT;~*#9UEvN-5j7AYF+7LAV-%@38Q15=xCVsFlDG#__K zdo#PATAR)N-I#H1?zkfLY~LEmsR1bqb-24yYyhewSe1hZT#UgD7y}I!I-k`F- z9`$gUU)EDI&9JM#ny)-n`w;}s8IUFAwhUV}?e`P15Xi zooll9T}Wlp9vLqa%gmiGx+X37^7*9H=FiMMu0As>#@c^;{A$$}{}(5EM>`bQ+Wt54 z{BD5u79l$?nV(;A-X!lAh%kb`i+SsW%hBx<>d6_!k0u9@Qg;{ox5p2bsd*z5nIx}o zqj!D^DBoso^|pQTy;##u=+nJmf=uFvdql%;fSBvCujb z6{7^GlRv^Hu^cMAL#pQgN{zzw&fc|e*$ahC=*V0hG~t%LI%8y|wOf`@&Z3&n~ zH)m?h4S@Ong2RD&9o{Xe9%*hn2#{s~BFO1nfjU4M6=0g-Z%uKhdI1Q8C!~lA0L@Jp zk1mkn8ou%{hYqYiY#A=WMQ8(;Mm7LB9zlWNJ39%)670jXPYs@4p^1KC^1=GZNbd0F4{r z4fF#0VmQcEfmfZ+V&{6n`4l3{5*A_haqm-AamwEK(&MGfv?iid#Lurz(*gz zf=|jlgE5g3f$JYxHNE=$Gg#ek>im9uKIsKwdX5>SuNY7#S^(j)&c3)I(ocv;f36gw z{Fv~2p=ElAuRl9}`DL%Sh^0?k!tPCKEN!$+w{WL2iJ!IublU@(GUs(miMH z^KLo9J)anmnPLQpnT8P_gpr7e)HPwS5*-ACba|}5NHQEDiwTfT<=3m2av!&in=~=# z`}gaTY~1ev%mwaJC|puf3SDD>^4neSzOL5yvG&J6l%vm8)i2#2;C-{oLtHs|m{tJr zr>vgCK}jl^3K>?GY=S+Z8b)#`{~d=1zR5>m8WE89`GW?9yaA}SIiOA%!*6<#m_Rr@ z7HL0tKoc&;-liSuhEsl-2JtR9_q@uwe*1ctOPo~5(X94u$BN~iZK{t_ex4IASFxJ) zeO8|Y1`U!L%pPXNo6#?(u2>-)^frrhg6~xk$GdFJ1C>h`-GJ1U zlBuM3dtO)6fB3IHN3$`oq-H|1OsKZ(?(bRN&sq<5-cZheaWWxs^Can}3oCx>Ua-R7 zE7_~iGYJx*g(*8BC&xN26z~e1RhU8vxNhF-7f}gX^b!y z;#)fW9GFxMB4vR*{~lq4=Gh3}Nu@S0J_z>kQ6)SwQN_OWpI_{J_?L5gvr(+%A(Vr5vPf;97e9XG;X9H_Ez^i~8$F8b z&e0#D*ob%XhCiI5-Mp;063*tSw+>A#IH@2V6PTuV-(v7wQLp|d?&NLBTJV>ngWqqT z%(mw4V5EkEp5F7`rezyx&AnHYWK*ZiUY4m2ObC}1HXDF9jRepRtYnhea94kgF6%C2 z{uS#&ryMtPf}Jj0Pnhs*^^ar--sJVnh2BX2^(JTBsXT%$hVf9l>cq$?T`43u;bL3K62z>(mzg~ z&Ol82j&C(9@{YxdvQUHTwMNoy|1GZ;w17b zjP{0%{T2PlAh>wL&pENS4$f-En{sxrGo^B;MvNV-8ehio%RggNO}+B^afyaTs{QuV zNWa}Z;T?5(wKnybF~I8}8sD_PeMdqW`~;T51vVIXjS96B#bprF#o(iV05sZ@#93T% zLWpzF>nySy=*4$`#6Fp=<;z>Q@p9v9%9xs40r9lrl8T|anreF{*A&0%gFk^#=YY{n8P~3KUlwiX#9WVk>?No zLT=4Pg}UEu{s| zUw&{@(my~OJax<5k?iBd-v8O;r^?1PUm40kFL%pgib8^pZ#^&Z)Wp2W$0gR!IMqJd zZuQD!M|@MOMU1z9DKVtxF#jK}RHAMb3ivh`O88(R9n=5f)uW5`=@MMMdyu;z0$OoC zaL_^)*#eXcUYEkd70udUpF5h!y(4+o95mJG{#%ETBNbF5$4SM|U9Hg+H|G-LbEi3-O4(Lc)P zS^npW_nBa66O=D*M}Ij7R0{PP7IPfSS!8?gfQer^Q}!qC$_Y<;ckNE>_1Sx*<)gBA z6FIZSzK!%AQ8U@_{DLEkS2i8Xt-W-v@x9CS={B3jY`8xD?2r7Y!|14P)lDX^TVDEF zD0p>!tfpsQI+1Hu({ldvPW__khkOm*w`|+GY|Pa?TQ6vyZQil$^e6eF51!|-)-$S( z&$vkYGBVKYJ_QYatre&?si55>u64TOKGo!gfEOLl4;%j)pVdi!yTbH%hNo5KW4Otf z9g;u&rMlB8t**4%+Wxq*#V3lU)h&*7i|a|RTQUEN(t`gwnY59%wkpD3p;gI-Bo+ z!Lwd`PT*?>!h|tnfLSVopD$IQKn7n-1pWn(fLcmGHU14`HqNiX@NgKU3{k|93M}TV z+YnEP*>08KLec7j2OsN#4Lu<8awn+;6#K=%7a6W_zj$mAfIOIZ^V{s%P^Sro&X zWAG4`OVX`@KJZJ7PZA*v1UG>8LNpHpuuq}oAv-B%!m;xU^lhGy3Fh2!0fRAmyHJ4W zqqMsDutu<42z<*lt}sOvUN_7t#Rw)0cEE)l5>!jB2X{cksXt-nk}tVHxgU2jCPB|i0DYvnJP^6tR+4sozKarkvqSLtOPrvRkYfSc zKm-&Q5lSF^pbv54;wTM$#Kz-4fP)@qkr&{{P=Y&|DY`D|4er4WS8pbN%09JeT)~4I zX4yIPyn)ubTOX5N&ABjTeS9T+3$)GUB`;{i+m_-%kH^vO+Lj&jciWsl5Qf>o#w6TT zkPt>}csUJ3k;DWT`$y@2lf{3tL_G>mu!>mZAW$wk!HUp%)FZ96y5i~Tif8o8&99H_ zZ0ovz`s-6l{Kcx!J7X2MWSM0ir5@RK&VfnW-LR*v@5<`9TRU76f4g6slrS==OWT!e ziRGTb4DFN8c*?_V1#En%y$}eUg#;1H`iqmuI1Loqq*b3y^VP7KmMhvQNWe;vrS`er& z+rNC=+nc|OQo35{+<}BI=Xs{KkE<`-9aMIN?h@SH5K}@>HhI~Qks1EpUXTEe!WbOY zy3Z4E-l}cNPho0b>v%-QIil$eHc!I@Nroa0TEQZ38FqTu#a>nrkUed-cEJySsj2sU z4G>1~t!I%B0|n7Of~|?Se$cLxLb|!%p-*v|)@7Wu=(cA+*5$uF`E>W* zd(n=O56dTh*@BvE-}NO5l|M3@`QLa|1G(Gh6rxwyzW>a))!~QDs)?5$+-c(f3G0DX zg2PEPq9$@MXbd%jm?CYUX~9>!KY$ogK>Lv#j}QpA+%UHx_1`*jf|V zVI>IwEV7OFaqU3;7 zG=z&`_%(3yZ7^SCl)Sa97?0z=U+Pm{5;l9L`c{kkf%5x0R;4qm65iyvPky`U?rtnO zG)0ZB|Mi%)tF1fpUAY0zbeX5-#S2bZz7JEE1ZG89BA>mLT*{1zK_;jN!WJu)Kw%Fd zugh>0VnV<#rjVcDuYqJ#s`>l1IW{lrTMza(P}-iodvj~ah063DIhW1T^ZhIC#(O$m z28LONu2xCjGe2HY(3>;%!(>$c2jho7xc5Bt*4G~evmAXYeyRCBKNbUfb&Oz)SQ*Oz z%FGK{=E7o!VnMsgGKW|YLpt1`nglOqT4fDfw|RsdHZp|xx8E@sQM_?N1ouRk7(z^i zj$zjI6dHHkZ)U|&z0R{|tKwWe^t@N6Tdiy=Noyk|*q`gqayst4zAbT+`bheV>!snN z4?V)JZ`n{HJw9hk^j>9*M@Zf+rJHQJTSR;I56F`L_ zw2(0ZQW%LgD+EImF4(a9{s4H=Bszr>ia5^FEb=#?pbZ$x&y)iVmtCXJ4VPNwqVZT+ zL+XITJNc>`<91Z9j#_7>^zw<8d+6itPS3~kbLHQ9$JWUgdR`1@>8&d(KQv!{{HM5^ z`9BsU#Wic*U0S)h{$1{%k7WBY-VNa8J0Z8DI@?~dJ*9COttuqG`$pl+)ms+cc_z8D z&uGGk;L}^z|EJ5m$nw6!IwEarHR+ZE1Ov% z(fNh@=B@+52#7&|m2h6-4S?YOf=nlluDCvh39lz`m73`%K~t{s4=&DyU|`nBM`&ja zBKQ~)F5v>OR~341fZ73bVF2*WN-YW_WO#rqc@`Ro$vlF;s!j0D&9KTRl}Rt0q_i)6 z?c2lYe%kvmNnj8m?Pe|Wo9)m*-DFZFsnTWxEweJ5_`1L7Uw7~<>oDSBfM(&jz)tU@ zkLn?hCoejj>h8Klaui0{8%o1^F`)0v2IMGm2DBQNCU45U40i$M00fPv0ap`w9SR#Y zWjz8FjB;}3dUbG)T}-O<=jVJ=(hX2^*Kh%SK`yxFZ<-y{&ld~}BmKdgUM|cWr9Q+6 zNxShhOjQDoGZAz=6QM#RcEF#HYV0k82(lF6LjPT!;E~?*Mf`9YmxyOG`Q%WDkN^MV zQ(-4j=g+4>J|Y1*e*oW7Dx$PmM^)OUF3-CyQ+0>+?jz6}f0{@v_h|Yil>z+qJ2>f& zKMOzVivv=jUt~Re+7oLwY63)_&l83UOaZ1`X620nvqdlKAmacU1YW~)>^f*vCHPOk z#>0V(5)oY8jYtR<%0dswZsCCr=&XPFZc2!Ro*;6EK@PQH<2=L zrvX-B3qyfMoWooUEWAEyHW!%i;$5g3Y{KM|W@pNvr1jiW-$+!|FQr(|AoH66o&{;) zyQwf@WZ(oCPlQB3Ghq)Ae9Ug#7U3>(k_j9Jeask``E?9Rb3CqHgl;X#lnX!U!5ebD zIL}!W37|k19nzatsxvnAj5Jvo_1{rdJA>(&_jxO}z8zcTMaw$d>A7q2#Ept_^H&dI zC(YJ9ZJx-(eK=@g#f>ba*sw$a;Kzg%P253{RB<631kD59L2IkRe3mY^|Dk6gV_9Di z9)^%!>D^>4x{xx7VsJZHhbHNJZ%o_^Rau(;`Bm{8x=GX0^-iTaGgzm5Bl7KIt0QhN zvwMV1BF;LY0xGDmw7=s%$QtHJ;tUa7Jcz*wV-G(H!xiyhyTM^QAVxz}6DJ#n^YEP) z#RPx5Af*z4SB7g7B4R=MBBIX&9%b{7xM=e=cVWkts)4F*QFbeC$5oEec_{N1!; z<;^h*dqZfQ?C!iR*Uz_g!1*S9317K z$5@m(_}5QJst}(^6zJYy?6a-VeEgn^b9EHLUnc~RI#RT9Sahdj43qHL7gq?qKz^!RiM&;x8MJrCsdR0oBj9{2Yt(;ECvd)aOQRL z2hXa3a=8<_%Wlr|c1A{KuGJrrm+|ynflJnserLtI`>!`lHF{)viX^rBRNM&6RG#Cr z?)Ib|u^lrMkK2zqhe=p@lFm6fmt>9~+*NGh*pg!vQDk6u_n`7Zvhmu>@b`9~z4cQm zZ1*+-urhGE^4pI5V34m)j8I46cpwJ6kO^b5eYNl*NDZIw!en0fn{1d9sfFKvnkbZT zezGX5L8RD*Wyip}6&$Tf|Lvm|kMdLUICFk`dgGyk3$#kK1D8I}s(Sy#XTL}Ev+vX5 z{VGPv{?KJoCS0UVJ6(L-LW<}5b9C6=+_L@i2Cs(CnJ+jtA5ta^gQmC#e!gf3IA|Y> zl7L@Ilu4 zWfd)(R^MwS&(-{CZ*6I@&1c(7Ozt|keI@Ds<69v~7jjPhatz*wHzaIPv7rcCuE?K^ zid4W`k_{UL6L9`Dk^~}P!UbR{{QnbVFd>H+LqdXxVS^Je1wKt9RwGDq&{P&>7kD80 zB$2oF_}rw|BYa18TF`y!sfp}q=03#zZ_1emS1l$EB5Y!mns`W zTw1tpwAb&)Z1cU$Fj|#cVnia3T~&m^i3^(>&WeBA+<(>${yrIta)`hC7MzLr)~wsG zeZ2Y6Z;xd^+Ov&w$TzR^vcow*(x17EYohj)^YkgR$x||Jr#>#YHe1N z>>de!qWxa*iO4(tlYi?drP;3z1h&I=-@?r)!p7SK!-HWUKaAoNe=99N%({vm;W$lT zQObaY3fwylDEoIB?%w@-;62JyIWneO@n`b{Q%BRhI>sc8rS-}=cXKzbw>Q!=IWoS2 z)cxSf0DCs<*=*I=zS?rLTZKqo>cY%2JdWL zet!|te3;lkRmb?KlRp4ao^h^_Lg4F#m%A##=wHE*qLmki5lL{%0HkLTxZgY?0=g+~ z1cLly@RAym(&s`cB00A4bL510FO<4xCzr%R2jv-O7aieezz;-xt`X|OYNCvo9cX&ui`9<9`}hY}o+=Uh01ZMFifS9cA*KhzHf zBxthW-hO~I5|44fyUENS?yq|Q)nmg549L&eKrbUiu!ju+B}I=5$SdE`ij{$~MJp_u zv~?K|mlq2XLAR1x4akquFad)fk!V9CW6NXKZ7iWuc>W}mQ14_EinTGp=F+=VEihpU zB3>p&nY5OA8?aEe7z?7r?pB4zXObOy7RWaO(hK$vjpxOP{n#mXaBk%(UPH_Q^3*le zz$PpO&Q@e`M!pR1N^-;js)WR~tKUairuf5d55_aWuQtIkKpHE@!%1jP)t#OB7!e{V zLV~~S;MYCiBmr=#S4fb?r0$M|b3>U4H!Sd92>55aDOW+cjBu9`VU(gS?WoUie6llj z^vAZGj46-uWX+ks-j#p5;ce@!sHQ3PT!9VD2@_{fd zZh&5Vmn+~j!Z;Lz1_#zy2L6kH2ASf(K|iu65Agd~aw@M0XH9DgGhC_YS=?sVbzdxk z8ml{>yIf)!?rD-?IY%BlZtg%jaZ&zcqh?=EMR!tqTkM;TBKF(Uq6p!J-p)1;n6~i#mC<3r9(fl7Cn_g(>G?c6#8FXJpI))~_n0&IJ1!NX8h+v` z@o{gcHEw@}y@g-|UyEeQm;Vvs^)iM$_7ER_Y>!Pb#yKPmat*k+@6f`cyac=P>koq3 z=3_DQa;6-bovGaFbF*>hy;*N=6jzo?kHbF5)a*Q)cW#GA(}dG;a=F!Od}b=cPP{_wvg6{RMldAU5&c#yPNVlL@112#f#U z04ePu#BebG4-owS=M&q}FTg??-df=jJ*hp`ZF^dYtljuoTgQ#t-*)8dyTz$hb+2ut z>VCLL+?-ebeX`UO-k_(|w)bkuD^y#L$$dB*ZnRR4=@K3j$gODcG~V|`oY#^4Cpb+fsG(s?%6cA68l7}}s@x^jBlBCA zGct6}N?z00b*Y9Hxcp6HdqYG<(YMym36J8bAy0IErpCV0Ei2JIuCwcE12g6={a{MX z#$t>=t6<0nyv?T*C_ca70#D$0fP1QL0*Zq^VNsRwW|vW^clUA$QVWSnXw7iIW zg?kK)OeK@d(1xJXlSo0PhmDc`ZzHq7e(K00i5VJKXAT5tn$2>_dUc`acD4f-Rs+@t zhX0-%|K@V+wOP~&z(P@IZY#CUlKFW=B$!q8^M&Ohw-m z==*$pZOd?+zkbo%M~UfURZ^suyqR;$V9B$S<-3tAhdB~Q^XeLWwDup4W-0GD{`FUZ z*{nFcCa`lxiy9by8di%bZjNvXK5%{wB^iz(g1JHUcSF9}ipd(pa3LC_%)eX--3a_0 z3LiM=4HnfIzqls26q=%~f+a`2bY7#a_rmggOt|GS?!8HGUb~r|-o5zduJJyPj~ur8 zd9p0WUbdwuXP&&*>s)Hdo4fsFg~*+5ccwQTYp7t%$zL%pOa4Z(_wxsc`{|e|=RZ~B zbu|$T#4th8!y+pIH8ec=jOi>25BUbGLoVeKoWFXRR0;VUw3kKo0v7n9%EPDBuwGuB zbIRnq=ZaIVGtA;$C23u2zDdtdTwu;cBKMSONlUk?_GG&!&zw_ZZ{gJEv1awbkLs@w z>&VpmZ{CInbuEk20-|y6S?3r9Y!(3DKDb?iQ4>df6BA0E8F!EWA*%ntEla5{we;Jj5@hA8P5&b z7p|_zxSe#nJ^2l3y=3zFpksTijUMk<*lCX1TSf2=+x0`UJT5j8uip2J3Z&_zOEHX zP2ZPSwl!8AnsZ>urzxc|g&&ZlIa_G!d&$?rw_*nAjz0?v{h3Ll#`>gWtH>Sj_nc#7 zgqod17a%|u0g6IfJ0+b*SKv~*rIO(M?R3c+NHS?UVK}zhRaV$ zH|NLF8T=TI5s?OmT4-Cllu$sDx51bgNgj-&4I)KyJbp_3Q$`q|b#mc86IaOv7rVSTR#yr6BVL*)A)9~f9T+>rGDoDUe z_L;g_ZfQg`8es9fVQC5C$p;3kiM&vN3%h>{Zr3uvAhD5 z;ZCWCX67kt7_-dRb)I#|YIC@sM{c1~J^aT=u+=1L1X)Ddqp(&lp=)#62eQn(2|j|6 z_f-g!2vQD3%CJ32|G~#Pao&>G3c>el!*X?KcZ2e z==K*|y=0QI>@Wr3j*L=$&?f&lby#BBp^+|tw#a`N6YkH&=_Oo(8amVRc`rR(gI?b28 zGNOIk(k;^y(7F*SYj3M0ersvIV;28S;ExCm5>aY!AB@N;1dgvt`7CNG{uvy>>z)X$ zi55~9qkrvlES`4g+{w6OL6fe!#g|rGGb?;PA#&CRpYzZ2_HA1C$j_i{foz*|*SlxC zHcFPGjvnNz376evr%UzMu3=$tEgcQ(MHDhL?*(PzQ7~+p9)gD7b4A0eO@L_f3K{rk zf~k4HLLRP~J;bW19ab*IZh9D_tEc@j)rUGWIXP&q%ihO!S{A*UYBDYHPs02A(|Q-= zg}(W8;AnZD)8e1KTUPCRH|a}lvf>2AF}9{brD3#1B?Sp3No{clYPGv$l4nq-ygN$? zHMzCp!2Tx^yK2o#ujh|cx0(B)YLSy+cHWD5emsqAd?ThTs1x3V;y_~J->(BXy1RH} zJQl&Q{ugjz-QnJiNVl9POeyDp;b|qfLWVG2BSc_dRj{bXfr1R&Fx22}5urU&fu7@c zVi)}Ky}XTBN6hofho zN4xA;%J9h650EsDp8Jst2c4;K-0=$+EOOGsHC>U8K!GR1iO!Tmk%@9VK;Ef|froJJ zi==IOAYfr4E%R#B@<1_E${9{y1L1U&v9P;V)mLp(!Fpn1_PY(Vo$lY6ma3 zYx|mP)IWJD%HW_bwurv5f*kU`mS};pPQcle6?(U zT_eXu3@?fI_;#g)RZnNff8-@4}F zrL(o#BBd@yrC*)0pw9G^!}63l%5=xFdWG~wyZqeiPO_VfPG`5>`o%lUYU>VI;J4aj zYk^8tq}lxvZKaQUm7}Uozx%Z5?eYIi^CiD0jSMh{Zo61Jkx{%%0g!~qKV!KFKzs6V zmWr`jZW+9`%27AAQqv{kl1Z-$>5Byv{cj{rB=U9Q2-y7z~}I5$(IzX2+;UJl12S zgMK}#0T)lacS-Gf^Sx;H^hiu5FJ4V|c2U$gUcU0(u(@3i==Xz| zOA+9NfKjm51c2y5L<;wD0QH;Q0xmoe_a-$UAK{w^PMU5okR$udr=E#`V&sk2jv_qW zFrX05Dv4`7j4L8^_lL|=UiFaTD&oMeB(om`1!uFSR10{2t$M`zF!TV#7`$O5L8!XH z$oCa|3qfk3z!Un3?$~UM{~?AoK*q-OqEx<26ms`i(uy9VHy|-ic6ECG9YyAdbIW@% z3uk_QvenS-P_~1Wvw;lVUGI7TNeX~$lrI)|CODxV;16D+IEDjFeijB0ZQ++VH?83k zY&<{=@KEzO8_eWFN_Q3ji7=La5sULrjfKN?dS+}CP7v2IaDNLrHn{Oba2p6usl~uH zPK@{qMLPHkirkr2hd9_rk_vkM(R=r&LGl{74QL9s}wr1F7ok!RmM-(&%Ce%im0z>_nHFB$GnkiR~p{34K=L zHhX>Xx?-3ha>gn6?Fs|%!#j=8u^PaifuO06{o!%)`S4cvy-@Pw4OPvG?`tloudr7; z^Ip9ZOO+Yym_D-x$UdTfo!ltvy>V=8K zNE#*`rGyQm3BvFX)*LSUn$j?j?`2?g9W@o3P+`8&u`G?;F$*E5qdp{_LRN!XgS7gn zy9Tr{N~D`t;yZxsftuo1Q!bjLWG;N|1p(1jvxm`$T)4x4GsH-TKh}twhqzn}jFXD6 zArkq2ggpm96TjEF7E0M=4;f{sAj(EjK@MfP5S*xpRZ&qKI6+)jM8yFjB10V2I>3pz za972Js5nsEb>jpT5f$Zoxob;7e*f=LlJ{P6cNs4)FS#VAPvazv6DWGEor{28g(QWS zn9v{m6mqQbSyNmi9^Dx^mYZ_>o=gZjm2vWvr%%R+9;x@MTo#OM+(7039saC+l+=;b z>^d6d5VMW`jJeTo$pVYf(cZWRH*D5D($B;S9D@dg{R?8UT*z@j;hOMl3%V<^lPEXV*VtZvi;)D z%O>tgvgk%e$NklF^i#>`Tt7tpM7EAeo@!uS4s?dy>2$T)6`%VzQOr0sMk#Yw;eiRV!?-9%ZA4R6b_J0jQ0;FWe%+|1y#b~%q1e*Qe$r}pb1kGXj^ zCwr)6mxy|O{n%$-h|p+K7fxM%`N zF1aqk5td=nU6odCcMQ(9`fBE$ z@Y%!r=i0_#f5RO%UxyQW&E4jkzj5u$-d=845Z%G`)ryhPi>v2;aSC6eQ)V#c4poq= zY2W!o{ocfBA;-oqeUq0PB4SDpTm?W{l>pE7bC%%MjIz~n5p9a4b8oJFZ2!KIJ!ZqH z=|`8WbE^BG7jo;IXAl|ZGGgLY%ZKXc1{D5$uyl{9W#^u$Lk5sNGmr0eD`8}q4i4SD0BF5B(Vx%zvF9Uo08nr67-w|%kQ*9%()tk}MB zW&G}OhTXmf?xXIHI?VsDZt&AZo7txKib~kevNpYl@he?uI8PA0`rPhg`<$*P@Su{x zDuy2eY!X>fa)LEej{WQ7y!|u803POJXthAZfu2In5#(kFD~HMq1!x9;daumo>)*Bi zz1A!&{7A39UIX)%F3!m}88pP>r`<)nlS4LI%obLLyT@?n&CZUqoYvoR{fzTkcl-W$ ze%`}q@1k+`22ZZ_biB|Z_VK709=)!oEZD~hJ8@zAHfQ{qQ*beuo!%)SC9EOL zI48H)&rZ_!*Mo^OJGcWiJ+5~BtaDxCr;gd3cF_yYXS>EH4>){!%sGkiXcXk@Q|L{g z5CYlA+dwyqfl&D%01W8z!4JgSd*w^&0ZWW?2e$>d_0hmRB2vTl1(?JvE+Rg`X-&t~fvnHDl~T$~2G>^Nfq1DFK`%U(lt4#Hv1?pJe%dV0ugLap z85jiHuUMIgz_?0>D+#!48h8ybKN5lII$!{BLSb0X0GlVr9LY{G#40(p7cx?ZR3(Vm zG8cHP(ZPKy>FnVM=zr0>>*n{1;)l%Ia5;4e^0iKV0>xlZ zh{_b#$PspDIVSUoanD+Cfb2#t`-iD#TL}~L5)`nZkUi#C>TiT19hOlz8vE0D_hh~K zO;->r_&{7FM|<=It1lgvk?g5N>R0z5B9RqtTzRx(-O%m#v+W#(|fB7D$uoK(_LpkoBou(>-Vm6qtHk3!Z}Hu6k63 zWm7f@Pkp}?5-@P`&3uKYF<%nk&hN~*+3~rgsJwJ<}^)>YvN6c#^Uvc z2e!PRiA!UwDJ>X4s*p6VJ9$@&(*017@CW&A$SAzO(alN)f2)J&i`pL&)Cq|m6qvwh zAZzV>RX%+M?HJt$FA1Xcl z5}^-aAcc2fiwXvDy^wPefAfY5bqJYS6>V#8ApIknhwQ+E|z*MNQqW6+ZPoqJV!OxJsV@n4#dFu)rjxXfg!yUbvA-z01 z#V$+4Bk|>(qDPyjUpQYCf4X>P|0l7xyImZbD!p2z8X2~gTQD=p=7FSh-xZxZZ$Gxo zBTJILzd`TJ(>61|4Z6m8l~okH?g-z3PL5-%Jp~s8@n2{)g^FlKlmbQK*Fw%)W;Z51 zF&g3UVR&6|>{z=s`(0xLMz>9Qwf$zE$>L9GluqIrrMW4Df3B88HH+Gd`lGS8EFqRb^;H6!JO%wDvHc?0=tM+hQZ zd&(myd^{tm#>F z4YB}df?$UWEhy0oV+?ehA(f;5_$vOuf~G(N{(==8EA~Z0o>q}XdQP@*Ux&?_uE@h= z>yeyV=TpnrT{btA{BT=#Xl-J*pIxNqDtTA*M7?d7xOpzKNs&xKvj#2yaBS(?wc~2X zQ8)jLh(O!bXJ_uXTClMe7c&T>tYL7W1-Am&D54X@Hiho4zLgj>tJ!!9p$fRFC5Xv) zLN!Nh*vYob6kAl+h}+J(eY|GZ_)|GHxp~4bZ-bL>#eCiH^Ygiw7g9H;D}jSF)T5Ua zck|wIId)cLb)HYy*}c{84ED@h996$D`YDTAWuxYLD0tV}nBS(o)|^~?(Vz%t2{Ojy z3idaE9jXkqV7$Tk)DM&-<$pIX99?#UbBY_$(-E9P1>J-Yt1&JncL>$G;rUz*qew=~ zVz_(!4qon?*)gDZoW`34JIVrO!{zdv;bQv-8WF zpLXb#|NYDR9DZT+lRcC8A4}KXTc3NN|K6FO56Kz`(?;lL!%7#0+$a(Q);op&!_DB)7WnI*ZMS>v)s9gU3>ImGUW`>e>o_&irr+TQuA$ra zYMwvR)^&RQ_=3EfE*tOt2=sA(I!i~hVRdGHNLAUd5nkKVq$fkR2X}w*+;i@XaS{@$ z1X{H$x#jS1tEJG1O5~}a?P(aa)5mBkm`aEnh46e040;wTH6$hi-bkZ15+8b|Wt54F zy4V7Rt#a;v|9rHFusF4&?~6|-{@M&X@oCQZFW;UWDETw%#-*1o_9^$&=gemHji_3E zCV1B{&DlL0SSe-)a|7R~8YS$ui*lLm-#N=dJgCRjJyW`Ghyj6u3>d=`WvWKeER<4- z*2oBK9)__zc0rC>0#RixZQ{kTDIi*FEI1A}7L-O&;(!}MwJCTEp#eL$1!$Udw$$!c z%5jUpGvB^UF*mpE_vvlmroCMQ|18OUy!pw*x`A#QZ9RTn@DKkSVBcZm`^KzI#Np`W z6+e5pWKYoS-D7QZ#_QoT_K!+gZm{;Pxz8Kb%Vs~c{O6Z8JRG;NTU3b6tZ7r0o%O%w z^H@K3R_Ec54WGT^O@1Nzu6u0Kn!A}cxyiv(LZ9r~b0!*1xS3O3{IDi6@Wq=WSqTt{Bp}dDoCz7=b;VsXF+oiG0FFL3bBqN{D4bV{g`x?V-MapFZwX-K+ffPy@gR&% zBB#wmIuzn)LN5Cc$6M8jWNadr430-o_kxI#8DiBw=} z%yp?+%V!4UBaA~MxaM8JdJF_m>Y+pHUct)Xubl zD+VnaNVcV%H_)*dMe>r?R9fb8?XCUCfVm-yx!%(Q8PDkODLf?ccH$pj}{3^n)$UluhjJPK&7OB9LfO; zAxlJ4Q0?fBoG)ur;u@h^D*is5Y(Nmv(X)m-VByc41LTjpMz;4|G4b zs{62Y{{43PtA9`5flk@0Mc=?i2|@piNcnvgzPhmjZH9=+eL}U(_-kNlhEm$fc9>eu zi_YksH+%g6p9^C@&5_10X^@WAFrMezRlBa8urdDk!Uc2vrnJ3_j8Yti7E^kh$|F zmkL2tJa2(Wfi@vx@}yAhEVkxlAKp!ABQzUXH}zOe?vuSwrx|pAF{{1DuLp)7r{&ro ztD8D%@yQ3jjioPEPwU-w?%=*7r=)n5E;%%{*Zk{7=_|>q&VKuf`#a?ps9-5^)ceEtUzQ4p~im^Y)8H;=<`ng%g>K&V!Z_h`z#D3a2)dnIqErYd#h zmWWfv=m?UT9Pu69n>)A5$83%RTt+77pFdYf4NWP7|ATNId< zQ+hSkYT&H?FJ~t}V!P4BDNliOx*Vf@g+1HKyokzPN*1?e`Z1d>@8@_#BKu}=imhHH z&wb&5g1Tb2%J|6>V*ECSc7`Lj!bfNq1_i%u?^UdR@9|*P!MP{$=gx8|Je`tL$??&8 zGfhLM(rQmlawq3A%SVm*^E=70?Zf$&JtJl97q?}y zv3rsV;(s1K_%rRdYI**SUeX#H*Hpvu1*^AgC!rKUav^CeFr zR#}iuxD1nXm^IAu;6ypn>eM_FNW4=-!UuXPKpaR4xpwFvoQh(&(J)rxs=6cd9(hy)8VP_oThh5yBytVi?k?t%ZPBef=Qpl<;kkW@yS0{EZjIwywB#?w}hx@maCX z@yfJ(e*#UUzqwFDE%3*QqAj9fFq>Tt?b92nZ}9Ww9L$1%CL51Qbc`>(e5XpTRcTLHL-!Y z;{f@g9wr!J?Z>(}#2VJeo1K=2W9Ul2LBvG}B;uujUQknCLP!rT*oal?37mvPh(Qo<#7)3I;U4GQR$Gsf?rK5t?twjC0?Nub7m} zxxREIUNqqoHs41}+2sTY9sv6ypHCwS$3%g_(k8sL$ezvvuZgX|EYUE&f&j#I6-|0cm6;6w z^ZEF_A#!%49Kj1b{u?wFD8fMZiEH63B{@SyBFAW7TYx>4FrAN{PiV$fU}#6Gz-VQI z1O{8E_xdE4!9M#a#O6a~bwm@(3Hx*N5PLRyKqBDmb_AKip!txCSm6?k62vae5(;A& z;FV1i%#dK5*sGRA^B_Mer5m-6P)k5HeWY0m2Y>mC>xpiuf(m_1TdJLEOnE1Ne|li# zSP~aHAv^;1eVP#Q+0;(l_3FGrMp@6E~-jYC%df8a_Laq!op8A4Bj&=m#02@#VA zh1_`b1s6B{nx@$KQkao7=`?qO?S(6cM|I0H>bj!z+QZ#f8%p=qcha~5dGMjjybalO zmsT(~i3n>^vj7gi8Oab#-+@qTVXc-GHlkC6+~rs@HH{Lmh{QR!DbzaI`0M>0uOl-F zl^-Y8-WoPCSmI|kDaP~U>5SJ=EmW@XS3 zpu?sFDN^P!I3s9)i^)tOHwT5|^O%{nKNe5&Hy*z_Epbb^%luC}K94%{z4qX&)YOA#pFvN_c;Dwpr(-kKfKst zyd*Q7Po74nd}dg689gXx^T*dgYGqM>Y>qmm9sAI=`p|Qq*EMFFI6n#nz6Y-+zfjGZ zBTZ>H#?fhcCLcE#dhVEgv6V!$o&vEbjJgEyBkI8+GOxQY&Lc_TcTD; zgm+n=dNnP3*@gw48=?;ss;Jzr+O%X{*bGx#&`q_^Jg8?pr4xqHei9H?3S<8bTBJNo zDWmkVYle@tq6mtCVo2uq>9veI36qXh+bk=ncKOw@bfWbf3)^mC-b2njp1is)Z^lIR8yfB} z$+=&(`?zm(Id1c!i=*cZ&az(St`;MsCteX5zR8eNISk^@II=Yj<*X-yYwA z2d_rq_oFFcxZs)dMi3vk9R7W(DG)kSL$Zm)mu9L zB1~{7>>}R8d7#u>q+tGdQ;JRZ-rFsJ1Hk5~$;87Azen+-Xs)u;dE^A-)`sD@aUrt1 zq)P>mtY0eewU7M~r~Dqg;MW$hVq))6IGVk5ZLSI;lBYov%P0nhjyGn*uBEIK=(!&t zmIAOBcwkcSfk4F909^_rXs9*g14>k4#-jlw{h9;g+Sq9hy1^7Mh&1H>CcZ8L(6b@^ zwaWXwtG6<;_uHlwr04EljAJk`o%XEW-D`&tZ2xIXHGFV0uh_f?ZjcM zt4}G}EP>Kc5~tR?gRTg!eMv$rB~afQ>2c7BVmMmc0stXK^6=uMCWxYF+(lq4VaD08nq6b@BChxzdCc}X#dCp>8S)9Tnn3iuJYl1-bAKj6=k$@`p65>xK{##a_&z9tQx94_^bCAM2V|FdzVW7vweeJsq| zYi+5Szv0%coZn+-f9sM1`j7e1H@3->_d&= zH|O4x{$1#wT9I(^lKY42(b~yIuftLU+X)YzFb+KxH2%!a56{#~zFo38=5a9hQfS3? zC)NwxzjaVr!HCa4RXk3P$TWnURQO&=VLl3iMhrmwngJndMk*03se*v-imGcX^&=5Z zXyuMWxD4owtgCB*mc#@sBb0@5A0m| zKB3`(ONOgqk2Gt&@z$IEuKdH&nfC6-&+k=+h2d(OcKN!BE`;pvrn|y5ByeWl7nRqW zKDK!<>9ASxokNpyw=X_jL^{eXU3oibdN^Rr1QkyN->anX!YB+KB4|(9QBfx3xJ=*3 zNyZ1!{TIXyLUq_NulPGz|5p< zCO^f>*f+&S!~E#pIr;pe-W+S!lS4c1i`qYXT~950lau3A{hnXD#7WDVjVc~+SVJRF z{{dUA3X_tpS6{gCHf!qyRi6uAg?HUQ8W(xouT&Kn(o=pW%u{JGYpM9zBpvuvmN4uY zxeUkNe@Nw>!Cs^YheJ!Q6{-(FYw_2$?$Xwy#lo6YgRr(ka*|To{z%R+JUDZ*apGD7 z!GmOvqQY%wpI+E9^XH~@-zIgxyA?$U4P5nxmy-^GyY^06IPTzgftOSe4le~2<+y@* zN=p}?zN0R{AAZm=qQ{$Y796`mOO4&TZ|ejQd&fQg-hWSz;>Qj5M-LTj8uG^Zn6D&y z-QA0ob9Yal^`-++J&F2RHg@*$jz%M6mrgIJjjSCaFa?FlJhxRD!6}fH24m>~-FieZ zDHW>Ez+1p&9}6#T^QkhWa!=Q_UC(`ryt~EES||HhaS#4$n_tD_x<{OUU@PkKEUvnL zb|)4=9yguv{lePi&wtxrKR4}l5FZi+KM<+k*wH9*ev#xp8l^_Uvj%98}@H?EYc9Q(DuU2P`>f)+OYXR>6T0 z;}7~@we9((>kf_RLld>yax({c@p1zXn>$jG>OTmkqd{$a}cCWLYo zfbA2&^Tl!iaDAc# zeW1Ufm!h&=Ss|D|!(9RgK*kkshW%SZ|0dR89#I&?vLoplB^CqOuoQDK;QB0qf8sPg znahNZjZ90EViGJ)NZ7OD?xF5;cwiGF)2Q+M9TeP|!pmPvmKP&-+c<i{u{`lK#u0IYgQ%Vy_i>BM(#Ape2(s`9iX~d?ZK$&T8_o zLJl)LQUf5ewt6|Slk7>MuBKqOW^i#ybHgBj%SV1BmSrW-hyY^XkFVfb=_~O8wO&;r zY?yeWlSnIO8zl=`8$x4ov51|WtqzPf9*@fbC2y`k^y3F(pNie@klQL04H=t6;lLn^TULfq&8wck4gauI8aiA{U; zL)f&J?N}=AWdHqwLEEXZ0aIol33jV`8vK)3epEdvEB)TrH3r3w+uJ*Bj2YkddrIAv zp9}hY>#~_Y_1fSeU*~9lj`^FMeQA9e?)E#-(?(5CL7Xe_^#l>anBYp!^0blAq!-0- zp-x3KMpVfe;KQeLN<^V*loCsUDj9zY9w5P(5RLqw_g=~?x$ihRqx?^yVcSz9d zO&w<+`csrQfW;cv?`dJ=&{@TO;xC{PhUV9z?~W)o?bL=}e`}1R@KLz-+>$HN7bYYP zwlo;G_H^!JcDi6$%)GO9`=)+Bj1562x(4ZynmqMNDJd6$02a|2IZVc8b%_@!#h}p@ zEg4F*uM!SZ>^`T{ygKgRr%a?_x(q|4*y|-WO+1RY~ zXMbI^3Nq@0Zw1=MMjtNdJ!6#ry;XGwO0SNQ7<+WNG_ZE~!U1;;?(JVA*gR@TV=vox z%NKd@X5OjoJQk-I+9V`h32?E~U@!Aqc%BU1RJ3=2F_~g!9-HEd{ksU&>(C4>DDByh zY%^qeC`_^?eP2fYyMpyoCwN?{n*8bYzHYB?4YatUb;N2~{QBFGL4P!gVpiKm`?w9g z^Q+I9HG$??j&-4~m+m|}6yNh!%G0Y`Zx{XTamA#%^VtVf;Oc`>9iml;1Sx7y?w8`cZVJ#R)_9OqAxXT=Ip--}h~tF1P>5 z5k`fQ)0cLvnHLnUaW;HXr`y*GE+nl&=5u(f*yBHr%-fn=s^*ZIxUuf(0nyhxU)3e^ zk41CC`mJe8JlRWS^!7rhy`nEpInljs$H@5=%P%cwIpYohgw_x~&13veL1@Zs9i;$G zYbAm~Igh#j@Q_bcIkMmpZ-Xd0%<3*`9M8n6lrX-Mdn8DSU{FHQk`zH+mvtXV6_f&r zRbWj>mb3|1U}Ar3p@s=_u3|lIYvmT^>1!6pT#vWRe>6D1dMy9YaF6hu!Pka=%SfGb z@!He#D{6-O1W_?hhL72}ykzjHOV;Nft~)L@6PnpfO=#qW^xN8)cd%E7X=z7Fbt9wB z-Ms&yPHoVuZZ*3*?MX$uUKVsgq`V1s6DX3=3KA0r4HbhH2^}fc7pgo=>Q5CR>ot<5 ziB-(R&`$!6Aqh?kVB54{G$+dhcrPr=8*@R#8IU2xxMQdjYB=B$lARk>?3q!Mcf&09 zn&Ib%HLo`B2|WEaDaUrnGELJtM7*!xg}DaLo*fZR?X>CfqYOP~3;wF)iJXagHNrir zdz>n_nhu!lN8LVO`I-Bdm49qTWY|!o58yPmjVk z2B}m6SmKOSNgV;f<|V-1jRt7+I~7ZsAs$FFi)yK<=$()^BBN zr94fmuQwYr_w)Bf;hnrg4pmJL_INpG!H(T==_2m-zZ+ejpPyH`*_E@b)M@S)&G4vU z2Y;QLaKZL|$pZYg(nRsDuNtSdkt3NFxpc#oLBJmD2y6$!4U_o7i#oIGt6P!T5K4bP(f|<&ysc<)oS#kS* zy96J!|G@X#exdOy{LbU97oGp&!@1U33)gjz@94k zBeB3^BN^Zc#e0ae<~)UTrLR6_wzr3|@o*LsDquVn6}vMTw(lv1Qg*qui+1`0w1C1tV*G7l!hYDAS*w!5T|d*--L@^sT+ek>F-+1RT_HE{k*UUt zSs}}q02rt8*n{-Lkjn3BSdPYCySwqB!@#`5!?)>qvF}`q`@Hc+!oG=z7ugb7)77@I z*L9tn-P6hA^W|9mV|^Bw|9*}ZefgDZSl8zKc%tf#@Ak~uB)+eD?PUt}{UZ^Cg*kLF z&z&J&UNqyO4T+riNPU-qjfMQ#O}>E?Qf^J{`_0tT`Dy^xW5IhweTIRgqpJ36Rew387lL} zkBfp^s}zIC$~d%;sH6k6H#dD2WymVT`E%8KjKfz$o)yqm!k|RQ$OynRr@;RaiaZ?8 zu>HO*E?T#%y?Y#cMQ{yka1YN}Ng~(^5-34>@Qm}Y9E*>L8YXUY*R;MlO;f%4@az|F zFbl3}Gj?XbM)x6wk9OI=Thvk2y?e!mKAi;8v633g)G1S>pZrh0?=e^Rj09Od;k+*y z|29E(GtN&+wLngwp%ndgkrSXufy{t;7{LSTKHQZ!_>X|jVM@;h>V@AIaA<+D7E@a?!O%S{+^H)x2{w*yS6v*YF63z2f;m zqaFgU>F$5?m7hxE#P8aFcq@tCQaN+?(}cM1i|GkjH>dj`*K77g?t24S#ru?Jlp zmQFtOdnPB)=Z{_w&g3$;gGXI9_VXR%>1(yY-{h`iT(R*&4{v0ct z+1k;gT{ROf93MR*ef7>rb<<(adybx89ng1v`;>(Dk2ZF%35O$)Wz!Q-76ggMRkXfs zN*Gdk9HvA;tT&CV=yh2XE2>#sOn~la_L8~wDEUtuV03*;wPdRvZOMVBZzV-CE|JXp zH=OA+@kB!+N#S6vp{A|GT9Pau@hJku#8WeNitRV83x~$V3AzJ?FWN zhwUbYEIn+I6x&rR^#UKp=3w~_P&^U845`IJ*bKC_>rweunLNru3^f!{S{tGMht%S- z(7wZ5DXSPUSufOBjo_IfvbVD7yP#4bJ-IROsk}eZvjX9$#KKm{d#OSgdb_d{}BAJ5XHgQ<0%p`C-hRcsu9PRHqLa{c&y}Ptc|m05jhy3}Hnom~uodU5>Tx z2T-l@ZAk_DpJBGb_+?RwNXSU4z~z%0czRM}6W)a-TdpVS@X6g{adNG(z59?i^V^Pj zd~r?3?4J7L`p;WcNZhJkmbIZ&H$$tl%9?t?Hs`pXk!uQ{Js8;Tarx1k?|t_ik3EG| z7UUgV8sG~$O0B1P-pduK0R(FeLk;Y0>LylGT$+5FO$F^mSU!__)q*(GMTQ#8Lk3;?c ztUKo7du7kMdA0|Rbs?_pG;r>#9@`=APfcD~LCK!iUnSbHK5iT$1}!>z0fo}yDlb9E zA*4i3wIXyK1WZanwejT`_T?&4ekl{iOvNh4!?-S~+c%D+xd#6Vi!~k4ii7onr?9Cu zAPRg>3r2`gCwS_#IY7gMfs;38{u)uXvw0v;aMo=rfZGr#b1=;1lYIENSB*2wBEtoE zAX)7AdaR)zXZYUK1ozIW?=PRapSf00|2lih)fjhw!r*@Vk5 z?a}|R*jN5x0Ygky2{oSKclvVG&R?dkGx}ymhcE9(9iO&l^To$5!evoQ$38fcKmKIb zx1mKtD=$rWpStY$wLWI$huiJeURtAde3?wWk%|KMF~a0^F10 zD*LP8Amhp=955rY8HtICnr19cp$?|cAw53Bm$j^7*c(V!F2hy0;vk+-qXw-tfRTHS zp@1E5doQc<&YJWmu_=RtUw-*nZM18{IeqbF(qZxIa*fe$^{9vTH7dQJYs#~_A@g7A z|7xE&bdtICi(kJYc$4NX37%7tJ^RDB`(^cd*=u7T9*@p1^3=N1<)wvrIM6W4XoR8u zlyOefWCEoTD1bl>Cd(TK*F0ESPsC3T?r?=$Mj;Iz*FtqfakKqlcik2c2u|UG2ch_r_5r6iFQQr`Gd%l&IBt&EMdKEx`PaZ7Ktr3(~y=SV}1)zU|S__C+{YL zyNeI{1eJb=E<9scdkKVq=xE(00cim-U{~4Lf`U;2<;CTuz@{fO;dF_!1jcFcnq7*s z5Igr4x)V+8yxHI|xF{b5=CZ1?eDgV5uLY1WYkvP2g@`X&rI2~cf;WY*9oQ=$9$^Zj z2LNIU1#i^T6A-#l0}48RC75PBbVwg1<}dsST=lj+fp0CraS_{;_85m)SFWHy2ll08 zGm=&C2!y^W!thRU27U$R%!HdEc+Xh-^9q`Mmz}vbzJC}w?n`~cf?A~NDvO6bZ73KF zrHt3oRVAF8vAkpQ!)TpObL9hr=5Gp_nHbL@whpKSGHwjX?-qySt9C@}2xQm^O-w+K z5pM^|qHO#`T;8i_)K6?xL?$&rllH5y&eE@yJ=B(zAqbDjtG-oX$?7c+m50r~I;$VI zF?bfmFpN)6CIK_LHY2)c&pc#pBurrfKyCrZT)X%1K))xTjQ3 z=~vq6DN;b{KM9MfePjxnz2iq92D; zQsEtbr2Ix;cN4>x79sWs2sV*oHoFkJDtOuh$_+LXvAEA2^d%oxc61>Rj;6B2s9>a5WqtKh|mPwxDvx8 zn34k#jn4c&cu^9n7;@7gqqIff+g$F+#|tc&4V9-M8*=%}{wzi8{wD>i;(v#Z`!En9DugO0h0}D z&k!mG1W6J13J)DWCjbWWQ{{{BK_b39iYi0wO_viP#ipMjjN36QnUBy&TM8MMs=z7X zB;`xL%0ps_P?JDwRl($Uz+HMo$2e2&^k?61u*ZCvWijz*Ucb#IHJAKZd+&Vc+1-6^ zh|Z}VPOb!$82!Uf6_ZD}({Df3Q%~7JmYRNue2X7<22x`%BZE%u;pU?7r$$v9oi#`$pXt zKO5paHN9@8=e@laSzSXPrDb=`4b)gNH`w^lwtG)*XB#x$T-@dSsdsJXOP5z@RO$Vm z^|(`cQY=^J?4#g;i9g?N`o(YKJ>W4M0SZHBNIHoh@A(0ADXPF|3z)RqeCzHOJi`_= zUM2J|sO>WITSh6rhNj~@Y?nt z{&``^J6~HooE%$oV&sy|i^r#E-`A@d9JksuZfL4O*0YV;R%h&sFZYWSoH}%`=O#w!~~7cN}d z6oNmGNmI?HV{jo&hZel6J3dd=K}!AnsIH88c&TX0d+GP9pG@tJMY7EOXEEg^dlX zHx3z5Nlje4piRHN@}7$fXD3?%UL+M3}9glT@<#OKSCfk3CEJRizi)5-{ojTnu9W z{X)(0=$4keRVTDBw|Q3n^ZE#nL3=B5;~LKzxO>jsk?eDGRl%Uf+p~U;j8X4dee%)( z=OHitoPIDhU6-osr?cApq5l>0HK!bH8>j$2pLR=s z&^Mb&s?-2_It&XUv@`ZJzAmY#bKqe~#9A4?7(>z?vXc3zm7y%|4V%ET38gWOoE8j* zO!V6#Q3^1CI{qJk55;@QhX}2S0L&~kPlD`5Sb0DPB3D-|n0BXtB&rmnlvb2394pW| z$OMAE1;h~hPZDa*K{K%O;6q+f;g z@X&$njQU-AmOE+dw=O$J`Kcc*PyEnp&)Wv0G}UYI>-_q+3q)jM(jC+1!MUs#y&SvP z?~!!cU%Bh`$@sDAT}L+Lw>2Hghp{E73lN#SR?-NzhdD_x9bt=LC>~jhZ?*hDueVg` zqhJ?iSc|*J6fDE_CH7*(y8INlSn(3NahL%RO46R((H6)sELGgkK;VkO05#Gwxw?ED z&;rEdPod^Av{qBz_KF?Qrj%U<{;#J$UUJtk^J>X!w$8ShJtDTFn}7UzeTj6Llw^GR zb+}ti$dM%@gE+ipae?l(4~z$1di3?zmz?}V9_cqQY54mNs)!INkt?;AZyJTI8YacQ+$j-_#_GK%N-8YWazf4ip2f_ zLd|SOG0<{`>|SoVaN5^?H^BQb#rq|~u3o1_dLtX}?)-G>W~X`ISGznvdb+yW{_Q^E zc#nM<8V5EH@$Is#xNqV7Nk7U`cZhrJ{ZRZ(8n@_W=?5~S!`HhexA%WNYyRft<+H;_ zZWlnMgGMmJ>BFnLIgd%$@0Rl43fMio0l|r%h`kzeB5zm@fVT>|!O*#IdCkM{>{S!! zmCJBW&!kcHuI6Dfvzg|?JEqCht&RwZnl8Xi8~`p;T}J_;i=CDr4jCxa+=bSfAT`4T z!zt3Emk(@k_5Ja&;+fMo&9~OsPfkpUFS?Wv^|@w5IPc++Sn4BsU)p)Yngz?!@7%aw znl7C6`q~dmk3^3}kxTk;1B~kasM;^RSQMsW8uBpv$#b3Hs-Ay!S5NPGC|);nprxkX zrS@NoJN&Xc_Eq9;I#h-Kvrkl1*U(!*^Sj?M?bze`4-*&rdp%66$4qn&N0nPN&V;$1 z<7bVD@4q^$x?r16P;6T4^T*fz9GmUFWTsvHL>-;!qejR5ejDM$Lj76j5SCH&6w15~ zEcI)@Xy zjgi3Xg85HWs}LGKxSkK&sk}z^4?^fk00c}QMWIAEmmThD4^B&9yYE_^WNU2JD}@+r z6-uV=@L_ER3$_)Qa)GiRY^oH{n-7G!5(A^`nc(cA{1GzHi9p!`b1s$VgQVUS4&iU0 z)3U4z;(4R7R>nJQc<7|9+Evl=_U%!$Fw=!vV`r&_#uXamwmmnu_Zn4G?P5e^f~i`j z`}TBs?*USn5YUFl<5x!79i^ajKuw!de(*?;WfKP&RS(sGg9u+e!O1`oZ%M>s8h%)+2PhZz?FQ5V$tF%RMb(C^dq<5V1Fq3?g?&*{98rfSuK| zKbeSkv~Dv13J##h%L@Q}@nCh%bGVTl8f15p)3ncel)0vtB?)KntYPJYcc7kK&WV8_ zt^j2%AEx25>?XZW7&unUjiG8a^<5#m7_cX8_%tF}f+@5DN&?2}77z!dniwa6SKO>D zV70@10Aj~R2BF>C=DI5vc*9`3cSfc3=#DpOj!VK=>FLEs-_~w<)Tx{Q%{#ueV>2%m zs#zB&5e?j)@=J-q%DRcwGwwabGQ7SBbt;s{={y zhk(rk4g)q4?6Y`PVIWkG5*Q|)7lIBh4EZB-k0U8vA+t0PRT=?+XqE`oN$PHDNL!?8 zTqO%jT>C;IL$TL^Y9dtDm-tLMieBrUzVdQ#sY&?P^zSFCp8nV@ycm!ws`^QdTInRDI|nv0 zs5roCCk$}dkLn%ZBJ(MoDe&l(A*!X2V4A`v2ms!!s|vY=xsbSk6!x59Dhpr^j>wHV zKSUKOaEq~-+n&aSD$eh%*N}hJzAnjO;Zg~dKtoPSIeQewx`*+_0D5#Sa&D&xkosKr zfciEhFj@PKP~f!yaxyTu{1#k2To1;zNVgA#SoBA#pl@myNy(!r1jcEkM9x$AE{i%? z&spwTMgfPF5C17Rh!7+qjkIi}if^resO==+YSA`OtAG$9aA@Evq*QyOJK>j~9KE_a zqUul9ME`9wdy){v7nw*9n=eF2sKt?t1iP@EiHN*JoiEfhy z_^D1WnZ`dX#bF6%-l){4^4AMeG5rQt)`OG*h{^Xt7@Wc`3fV`RV2wtbUnL%=pZ|Ps zq-yLptW(Lk7X{8MS6`m+s`u3Hc5bHHQR0-m>sdwfR@+%lmQ-tJ4jDXH>^Z^n-s}qV z+~al+@q{^uJ9Me@pg=c+ECYp0QV!UL#$e8P*i{pO$;l>R!CNy(7a^iI)r{pO@G+Mg)#}nTY=9Cnvl!>MoEB+sicSy&L4HG;H2?^}<-rC`(+p~1 zwM+9-`MqfnEvwC<;fjR3Jzzd>g^g@@xj87=#FM{K<&)%BWN^mB4gC$aZj6}UPObNw zF1Hrhz8%?Lxar8EL38=1Uk$p(+u?R*Vy~Lvn(D9Co=>dVK017l=YE$bciuTc70|n4 zx-riiPC@EH(f$JXq2_yhUOF>SH7&MX z@A1-8uXam*q!j--tYNYImaZoKHO$^PA_&F>!kGTMcd=U9PGnep&Bvj; z&w$2qXt_`Z&2k2Zqs7po909dTs^> zQvb$)aoY?v-i&1}Lavu^TX4`n46vl)T1Em|?9Ud$QAc0!c9>|oVc2Pg8IJe+9v(h) zbYA!RVOfch>HF6??F{mGvHry8n-f31JYi~@k?B#(FNtu`DUXwA6Gx|7ZeKZjY2M;b zhr1M$lUx^E5AN>i{pMoFUgNE+U1qGlHPB#1-fV&KyL{&_6tov;EZ{1GtfZ7#D(fZ6 zB*yXbxh5!YLYuHd!-|>+C5%0Exe^7P7=^U$NCivWGf}8zfo2%NUM32e5qe__I_zo+gO zYSrtgHcr{z5^f)Wpzofhm*Jd%(b7Ac_XAxYLC+)MpCtEaRg;*qQ1Cq`uSfT z=4}rTJN)ZdVKP1(Q}Gf9%bYs&_v&<}x>PUIQ{=8SB+>8OUmbq)5bp?U>5KNikqQ=+ z>1u9gDHytZM{3*4$-JLhU-$K)8Wd-0hsZ9imEU3*g^)eGfJOSip+kv$*OQ+4-aCwr z)u4qf;Jd^{blRjt4HA#CQ2=07l6eJznb!#>+>0= zvfE1gaiKzRogyfODU>z}eIUgbXewj`mrhHFK{56+9^@jXMX*9QC!)mZZ;s$lU4Dx= zG|47){UN|26dVRXeZfT-R}(RuHlL+BPgEiiSmd&Wz2IvJObTA5;J)dWP^%N3{Gx>q zj_0#{MV=DZb{@K+=+2sx1<|*=99{cC%WV6WdcSG=?4Pc4pQJlX@-eVr`>i3fJx!iR zZwq^N-8x{S)rbu1yANLN2Hxj3H>+6#0y0hPog>Azgg z6?GK~2dZGFIl$Hb8|3+gJY5%R`7--7p+Vt19V#r2J94VM>CN*;oDxQKKCO~i(KqJx z)tWbvM(N#>?gt&LoK!yV_K?np9DHB**`|H!z0alb?oWo;?ayi7+3v~Lii%DfPF0=_ z+xF*7TSuq#PxvJQq{o3tPDx(fvHt zmwrOSPLA(BphscyeQV7LbJh&(5nb!oIBDAe!MD)4#%r{mS;L93*erx!Gq5FPBo^qA{i)r8%CvJRF9eu$i3N{&q(0{oix)E00qKp^e~P5_b8FxBy=+rQ zcv`f7I7<+0^w4>y;q4KV<~>~A7~SK;mDtJc^4|OIK6vEk@T;2!tamJ2@7XtReeF#C zHsg>kMqYlAb|)TW5GKjP4jde1Vg2fo)!l}RsvaH6O@9rsPRo4T?r*A<7O&sP;}fT6 zZw=aMTU$Q%{+qKi*4G(5es`n4=7uGwZ_K{yko_p21^X~S$)*su(b~u&FvN@e6d1Q2r zE9-otgsTHynRFo$;wBCiz||>L-@QiQ*e1dUhlQ*TfVBoz9j1**SQU*GU=)BmsM|}K zMtvrQ*n1&jZG~tY;##%qbG{fh4Ed2TD!l?Yd8 z1>+IMaCxthf#n&sxcg@9WX9PpBE-faTAXYMr@)p219}k#%Lf^Es;3!NMqT?^R11BO zxvkc|?N!&bxVk-4E_HVe)bTuZs@-!3@qkVf5?uQYeARBM)p`oC?xax)pTJVKk^!<7 zo&sb1#8GPjb{^pTIqV*tXAnQUa@uhO%rmSkR4zp4ourKdWZJMJ|NZi!v^H&yB4TL+ z3YmCJX1spL$`Na{;)gZtYwPxfLK<778<9os5(b4hZ5j}_P`atdMCOeHmrfS#^8AUm zdPRDLtVn1E0h9`(w-F8!5e_*hN{F#P;6yhe^!J*Z6>Pl9iqHYuYn@?fXQ@QNMXYos zhPa<>gP6C>16@dD3uTMY01L96woB>V2|Dd|?7g4$x0T zyi;fmSCfw&A|E!10!c*ldIOtT33$p{n1(Eh-Jf`!uLw zL}**GOBFG^EO)4Q(PsG)(Cr(J;9d^OR#HjBdeFW@&`z?y38B;}%*O8`Q1?>`9zDI( zNXSdYBG7Xo4Co{ngP7x7Yz;)u%|3|uomvi!V%DN^WHu|FnKs-3JF?q(QUi{;#vk`G zh58~WK)g-wGtqXh!V3!$5Jtd)oDklvjvwl#^GW?Qa0ch(!;gqWsA+*N6|_@`e7{$w zozXbZJENjHlKgtKlBdsWaKFSy71s}Z8~JOgf0RIso3+p=-aeu9qLA@pDH3smR-kiYRWc32$YTzOK zoT5g;6LrL^Ya1(D@qj32;g~|)CA6h5*d$&NMNH5z3{oKsSV;NYH;$hB3Z`7q(j8Fo zCZ|WQCH0)1))~3qqu*?Zjs2av$!wDLxU(yM9hw?6&@neWt0e9dQ61q@)$q3b)sDli zd0)mYn!54Fvqchy;o?!fFLhjO;~81A$;EL@#_Q3&|8s4#Ean;)55R5dM9SkpK(^!f z{0;4>KY2#7KRLMn`HM$)7&I5UTIMINA0>piTRuptJA9+uZ?lZaLTC2 zw~iAqns0hFraeExtBc+~cX4*xi>a)B>>K8`nTM$xJ{F@QKPO+)**{AwEAL05&U8=S zcxrc-%f&{%-6}>;4BQ(w=(wAAuws4BP#Lr$-{qS)1WN9|ZMPP-bqlJ(NEOzCNd3aa zI@?hW-dbZ0={c7O)XZ2i4cy^AHcJ0#gGc$iggO`sWh5Ko^<7~ zUm9e1Y;4Cqdx}j?-Wi(Y(C5T=-VL(-;etuE8+aw;9jg(?Iom^Z#W@#3n%R;|!IQk_ z9qku;!AG-0?PjXL?}O9(HkAIw=kv?u)xfCL1gj-%U5Fbrk}ZnMXpyO5rY*QUVL)>v z&}m#0wn*d{u>tJQt-0_;3M?K&?kGX(*GsH(5r4F(Y2=NS2wAT7{Ib*TL8G>Ak4DYU z=N}l@SKYd!Gef%YTH7xdvjtP<>$(o>899DRfqCJLjy)C>r!QG(ZI}IP_HnPqsHJ^^ zhjyNGWK^+VN7Lh`=Pv#j@ZsB`p4>poz~0;9Ba^xhG@3lqX4P|{T8p5Fh+3Q=IdIxM zX(VLA_7Z<(L}{BIUZ~OyEc}9?#l`a$^iK@?s2H87MBtk#{3!u~QwN4R@h1*WA(}Ku z{dB}Sx6w3BI5P`ZL*u$Geaej+4lg}N4t8|!Fl_nQh5j?s zdRGiA%6XGNy5z>h-NP0)XEf;V(z!p%^}TETCr6E!Ww^<-qrWmxi{Bq{z)_I4iZUzc z4<|~JEl^@|I~K1x6OtAtWGQg88d^9l*dN(~1#~fhJx++J)*ux=7VA8hpJP9Ou0RfM z$%e&OH|q5wKRtf2&G`M&`tTG#rz`E6Dd+vH4(D{^&)5uj?{%z4Nu|M_S&8{?e(OYC zYs@tM;D)ont0fmem0Km(O|1`s1gL|0v8wUq@wO1Ev6QH_68RtY?HG^VF>)E5!Y}xR zk!s;eR(g8f{JV8Gr}g$6>VGV%&GefMdx}r>8EWbFq^az9?5;E27R*_5<#X|f#(_OO z>aB*^*%&?`sj&;GeSs5hlxO-yS(W&X^;}LL>>D|6uFjrgeL_kG%>wU+bU zZ+1D)S~zQcuim#BLK=$q>ZC;MfwALZ?W)uH&Tp8ZQGKh}pV_@U{cG2BJ{;HZ-Qp9Y z&G4VK`7087L{)0_%m3VC<>vDlrabo(W7gl{y?7NioV$4N-MrszbiQ~s?6|kNvsO>h zO9I^?(0l@sVIQG?f(2uXOJBp`O%f3um4(yGZXnHturm2DHEZh&ZqN*vVtzi?_i02l zi44xn%kim1Xid7tc5-zccC7^Ff^HQHfUwxiv(9=iM2za65&FIOo?wL`+GDc1CD;umG{MG~SldenAuD_WBS)Ga7nn4j3b1oRw)d$6uV8 zhd+iov0&Zst&c;qh>8<*RF#2)y?Gj`i!7FH;KTWcS&b0RZ;kDBSe_}vrW~Nv`xn^H z1NY}b%uQ1B;5}@DNa$nR^?1T^nB0tpO5H1nWHfN`78ryg-uHBXwCvnJukrozZm~#j zi$n5~FJX7UsjPI^;K8xO!XyP5SS(wmv-rFwPau9sO2 z{(PSOt4#{;o*{VH&g&3`l6FT%eF}iZ`Y(M+==v<*=ZxNnqBY7tLRJ9y?>Vw!f8;G_ z&>roEXtdc6dVQFJg7@Y=f57<@MEu-+zoOdY|^R8%M~nV_dC}8Iro5 zeMz1WJhHpwIpzuk#)Rc{;l%1?aCUHy%X^vHzV{s9ZK4H<5Ns#etqf6V6De=zK9^vM zSfo!PpKiA4h}CbJ4lNhwSZFZ~_m#q&h6rOzP$A1JYH)rxW9Hz2Lv9pw8;}fD!ntZ% zl>45Y9e1sIAAhWP^s2px(=Vd%;279YVtY>8$%cYO2LaNqLkwYw-+@_ANW|Rg8In1Y zM8uG5EIJsNPWZP?BSy@|p`)T}o{BzIuE@4%-MfJRW{I;*i=d>DQ^ zNH+iKh9wPhInvlZ4WtSJ=?ml-BUj0h+iO&$=A1U-#YpRA&7$!ICkKu=Qn-n+HL3AX z=XlPs{TpvT@Cp_hRpe^c<}E9o@RvH^Og^#_{!?BcyjpWZ2tQ zZ-4*r7sA9dAqAJ*9LyvC+qiUiYWbJrn~%*k`*7%ST8-Vd9BK97&Cgby@0m*JfnQG^ zgP{($i8^8OG}y-eJppOU5NiZAPz=jn0A`;qX%|w6OM(I`^9$G=9#^irs{L_ISuu8` z0)JO;=>DYMp_eXw89cK}#m5_{udn^sw;cIvKA0wK;bV-j+?5HeWv#g_6w@u}z|sX< z@6A6o^inl(pMp{MycLtLbW*xtY#Y(TVJ445*_Y(BO#M_C2Pg!jnYnUtCN`eW~&$7*^0Q5gn1gvJ4x0UFQK30WUF}q zwcbcNUMM6HIx7KVr~siYiE;;n%OUU;x_MCTQ$PV3hXytxJ(t6cLJe!3-`EKr^)UPZ zx#As~!QO=)SDW~E10Js<*2i`~_4-Ek>Dr|ZY41e5-nZU?A9^086q+EKAcBXa!ZfjN zTYQCqVoaFmxYtDW{-IK-NDK&jA7H>D zxaU4u+kd#n3f>|2!jb3t-?w4Mxt%&k>^mrdu;u^QFR5^`Shu@8IkZQKjzjnA9QlEG zmyRyv6z7KRy>er4m-C+UzUO5=y5DI)<#w)HY2kh0_lg%IPkaB21)pFfgVSNHjGDUr zFD9w(K#5EJSYq8EyyZiAA&|quY{N^2;ej=F?`v3PO&0ZM@|h8x{hc3tkMOv@vAvk- z&p+u)1(cZ+aZztqexB2D==v`mIu^y8nil=F4>i<=UD>eMdsC(;g zAF?ZZC*qy&V5qg%xcX$lf~yH#Mvd&}(|7IDqgy*X_X}TsffcrPa(ia{!I4GBw=H~R zUF3fBn5K-pwUi7~O+X+_&Z8=59dVW5Na!dKBHEJEc<2Q16n-}FWBgH^5Nox`9d=2B z58b)R(**kQH>eH;!6JL=4+q~vRgr7=R89JE0!?72=f6dLDgl0Ao#VX^ZR0w63HsJiKGfuAu7LN&I9?LtrE~PdP1V0@%)z~f$J6PR1SnHtTn=YC;|pypyU$O zXM{?ej;wZhFH*rMS1aolH`o;!#q>>0Bea-8msRN*D~;e`oUJ9V-eCu(FMvqS<#j_c-rLY-XHJ?tQdy z|3sgYla~!Hdv(U=)QyYRmNAXC>c+ePE}I=|lc9qGXuVDZ?lbJ}4M ztiBp5mbD-z%$97&@v4MnE_$I${Zoy9s8OIGbP=+*uI`lY3pZVvb)%dGDdK6Bb6jR8AsetxRW zgNNn+ICfIMMPl7ty!TV?he?n_W2w#5`HRb0NfnNUDa}i5JX3?axt9(+FmZJ8FCuEh zbW|{OmF`HVR|h68OU-+_+@`-l&X3++U6#}fYENrh4HSgKRa2n+Eot{VA!s3@6fMhk zH$0YY4OrW>u3aHpa}iqlCj%E0;>>ptfq?Ojma=B(k7vZaTh(u-oK!YTVFH*EIIC(9 zEtD7g2oX)F8CZQ;TiZL+tWD(*1pl*|m_{|aKXil)oVXKHjLRg=L{1qEazx!^CO!{U z(aO}?ULfI4E?~>29;Os_A?Y$3-$;11d2;E^n^!LX}-)|?4yD)Gl^KAXMx*ruqXBUpGe<)(SyF=E8A%6t+LyCZYrX8IXUEoaW< zA$Jkyn~?26*x14T7hV<8v#!*%A0R;8EUg(`;h0sl7qs?ychCD8QH=gjByGN=h2KXy z)dyCQa3E7QLKEmUiz=Fn7%akzW*a6EgHa?Skt1L~TLkV4jv+0q zV61eQq7(|V3TEsrb(rn4X@Iv~`nfBbshyJFWe7HJX--c~%GZx#46t_ZW@I3Q&zd4r zODjk%W<6qMYLdz=|8wXtS{ba$bfYC6h#1J7I}fJY9YE*<>sa#po4MIcI;WE;)idi2 zNQgJIeXT$bD9siQGXODmKnEqn?Cb42cSwYc9kH3 zB?kAvrDYytRl8#kMMU@2#Z>}eR3suIiyg0BPxq=s3<*3rI0@zUWIdT2QJ4%Fay{sY z*lS?jW~aG=8I%fH&7**OSIaaGm~i|qsgT9X=1~|WJcZa&fFgZ*7tX!b%(ybVuwu5s zyvzQR?2wN?U$=a4pMZp;#uc1x8N$(XZadU=YKJt^D-geP`aG*G7IQSW9I}J&7&KxL zJAh(r%bmiQIc;?C>i{%P^9=UiJaF^kUIX3`@XC16iG_5Cj2~cy-xNZXfggtVuLS%= z#DJn_G{%B{_a>(hS`bZ0Ao6nh2`+lH$wS0XUp*3V*kO{AW^we_JNu+9Zr-T=!Ko{* zpHul#=?<&!q*0{itN`o6tjuqt?o6K&KSh>0SNFq!*U!C0(0;Wa6AE4l#nAPnD27#p zGxo8{tT>3Y$kEVg`;`+l+PUJtaedCx(+e!@B|-+%zUIJcq5pE>Zoym$@Q6)9FfpjK zg9U!eA&XLCU=y7S%hgC|L6F8_3(PV5NwyOFO-!PjT{&Od#c)f^xiou+KvI9F0C)o~ z>o88^WT8(`vxx;O?n+>lbEgpI=k_8AB5r{MAeN*prJ(~&kqX>N$jn+?9IZjafW5Nm zVH$d8xt@Y-*WR8oqYq-5mUHJkLlqIoNuVpy)Yl$1%&=DiR122NX2(Jh{@FMjPOovXgXOGvGkN( z^{xRQ9W8-(VFxmDvAr6ZK`ua??$6epH-uJqK{1KHB^h-xXV>d6Zap8_TE)oF-n3?@ zzj|*6T+K!=o{Mb}Gv%cay!aEO$HltW(KOgVU}Q+ox8Z10?m{7V<=rb3vmqpO<;V94 zgF1(JPKu}(wjOkS5bHid`FH>*f3VAeQtqH{=k`q=@0onp>FPyM?T(5$5hb4vAF4AR zUtnpmZ-a%Y#qKG_MtZU*Q82d#-@sh{`4G$INK{_sN;LiEYSn+9h z$&ZYf9wFOL#r!;7duV}9pnK^yt8JSrw&76Z|JYw?y;%1*(r<~g!!-eS_Iug8!`t6( z`BdB|B*caN;mB`<6G|F z=4^bcL!e>JQ;+E{pH@(J9xo2d9nO}HqnsPlug3A-IhV%NAiF+aGxhf0_pTY?7~wYc z&DA3#77M!#eJX?C+tD93KtD#WOyGNt2;6L|L#l(&en+G_M}=!IhOPwi}HO;(v2C^&2^OZT6jrgqizhN1JL_I@{5D@;_At z0>#H#7zq9fUCtP*;%f|0p=e4A3K)@0s%WX7kyy_KUmq>kG85#G>)f8ZBXh^21vPKC z-5rxj8HRe9HF4OS}fM{Z&i0L*U@$7z_u~Fdq00R`KBx?hnrb%8gBE&Chh#xrR$E}`t*H} z8QHo9!a9)NVDtf3K|ZuMCdg6Ov;A2Ar9ASrhi>kUH;?XK+*Jto2AZyj3p8=L+aN?tT#?QKOAI@?fJzWJk~@$)Z4wW?)UaM(z|K>IAXwWL2Stp zPPpOrq&=n%{szyEe4#Q9hIXOV%)7;NLRE}hiID{Jy%a(X5i+GPt{9Um*a~YH9UlaW z7QoA+4~ij1i&{IuR0#PePffx86v^pg<<6-u{68GP2>736Chg-c)=NSz25>iboZ$pk zY1cD3!5iaJM6ca_z3!FhKdF)y{I-v})@NSy;E4FqK8H^%*yz@{&12@=5#xFr9^TOF zqvpsvjlV|)4w`cBX!ph6yVk!QY*TbYqh3!mfB&LAeT92&#J_zV9p#d`XZFsV@Ly7q zzWu(O{Ij>br|PY}X5VXY{{{Qzd_8R2X0iXR3AssYpT0o9wX$A5ac7H)36w=3M^I`Q zEj$*&n(KbGMES}yQNb}L0kNi_a(kx4bT|T)w~isp0j6E_c2Hsy9c9LVGYl4i#@H%n zKcC&N-Q6U65*fPnCou~pP^jOSGYbGCUak?smukCY(0a3>tlh5*k(I!r97F)0#=&Np z6qBut#UxET$SoQNgNd(&R)XeYz?Q?$Nup$-e(|IVmm`6H&|i3>B;+ZGqgwOvo>Wa| zG(g{9yjVnl953;Vb1$57oC zCSx4tdxO9byr`#%C}ivn8ZH74xG)tO-YZgqtfGLDLGnGftu{zAJ7oPsC?8`@FN1oL6gfpUZmP$ubBLIhB~?zKp@_IsU=_yeXe zMOcu6&Ilo^2S+eqHgOd(1oN!2VXXFyf#ZfjLc)9TA2keC1xeFj#5W?IfnF@!oD!j9 z2ZFu8NQSjps&yZBQ`NKX50#>;ui8$Yzb_)gcFoFRpF1T8W7XwInYvwX1^Vhsz^>W%$2=LI*iQg#T@ln%=faa8gX zqQfIuLKswfF&jYdgf%7LeTOt~i-AL%p9i>$ow_h0a_r7*=*T=n^RSQ)2J1$!0Ihl2 zn2l1IUsR%$=QAkmj1dn8^VHHrOele|Do}uQdzHm;(i4#{qkqzaH55Te+ik;mdl+wJR4eeqGM&pt8I);Ou!nf4+pc-&rp8brRjBw7nr7rZVsiVAooxCKm)orVWIL^cw2C3>n-trzP<< zqrn~rUbCVp#1Ubp223o_C3C2g#zIVEQ1s80sLFyw7TNtNlDMtkQ?Pn4dUeRom|wL- zhW?vCBI{$-u{E&b(oDkcYq)C^bP4GMu0qTR{2m)gPl@#wqY6Bk!GjCqD8()8vn{GO zPSzOim$~+>rjf&eo@X{O3m$Mo35XW`k7FSHDAxN=KA%xMd&7>t!5xkOF`kfo#U|m` zl#&O9VM})8ar30lm-oN#KFl+2=8*qf@6Vrr{b@^&QxVe&`}P;a$=Jz|dJUu!pK zx?eG4Vbb#@3sM_9H};LKnr2&K%A#Q434>*5fm*$dQU%Nu5qT;CkU8pv3XkY3!8nWt zTHqgJA7qB#y8H=JVU1YtxO@mLPnP4fvmvs@aYxIZ)b||utj)QLteTPW?}lDJFfj1^ zjd=a9=_}ond+0s5-Q^fDd9C5O39=j8XRpG^fPc`sY|)=V0R6A%AY7UM02`ovdEz>v z0bw0cz(1b05GsZLu8;3r5{eVb=`REHUZF7jReU0@^Cui0mKy19Jns6$e#G=e`HNQ#_&Mr3vDUrU z;YS0GUOP7X{e=4?oqu_63w(ag_*B2EGqj3?gC}2}Y5g;TyT)z8@+S$04$aJ}Ugg-f z?DEeaF$v=mYdXI4(JRoIx|)1N~Aoxus}u&c7WOVM~2v6Ci_)i zl3=+CIO7E>ycNO}VIQ?gU`mD0#CqT5AGpTSDYgit3>}ZXEpj~eVP1L7+WWh`*P*JC zjgms&rLS&SjqkMFBYXE=-IypNdzasmsOTw^FFoilowT{5ws3B`j_7XV+fPf7mF`%l zr|lL}cEg5ymEvk=OIN;stIl6T{(l~2`0r;`$zqC9psl9?*DPN-syw1n>Nil#)54!x zYtft9O^gLT`}BrvUg>n{;iny$Yg~?%8XdT^zj%$WpzVaSk8Ngu(uwRlF8k-Ka|hPD zdS|TAzHiDlvN&rh=&YGz-`t*crT>v^-F3p!7@>J+We?YV0~RsQlelAYkZ+s7McJrO zad}L@#99wsvk1Dtq6WZ_PBu0HV&oQ>%92(wFhZxoO^8udUKPRJrb%WI}dJ! zqg$u*N*O)u#w|6xCRZ+8Fm^*M=P6yN*)V%|m8nGp2hSs6Ssh*zi{$R4emP>Enf!oV zPtY2ZpiX7OiZfU0B#*mb{d8%&9ouD@(QCRb)!4eD{7FCWo6B5H9RtUA)H``LxGA&J zrP~)D+fSzEAJdXt`?u-pZtgP1QZIT7>sv(903P?}#v<`!n}jzbxRRX_mL+|3D{q+1 zY9t|#pjyC0=MF^WpAZMhn5rZt5v+=XNdjxl09)xwbWbrSQs zpfC%V=FWp#6P9PZh~0U0OYPa<=*byJA3JGTIM|5#`Jb-2_S~gQ&zC!XRc`GZy<2MD z)5OYgqtAcZoz}UyO0O+DOLeg=Z~LU#a@o-%vsPvee`L6f8>RK^RYG6mRRaC{Y_7Cs*%CP^Q9HITeNXDEf+1nw*20DDQT=*^O^r_3&B7ZXWT}WKiwiJo^RvM&2hJM{XS~E>=xrR`k2SP>svb1mOfi$FpPD?eZc0(p;l2wOFz#EvP-;1 zAQ^$;2vokrQ=TK0J4AepO@Ijs%!NtFtlB-*MdW0k10=FDepwFHQ#jjQ6pwSfyaKM#sT*Lfmx{6KVi8l&*JU zg)o{O^J}To4F*q{YV&_e42Dn>s(n3=!3Z!o*6*7ouA$WKhg@w1nYEZ9{rMqh9q*u*FFP z$G7#PB@n{m3@-E5UCX!?55;GL(4<*4B1W=(?VF8QZgxHF`?aVZ*XM}fa7M;92$40O zgav^-kZA}(k%)twaTKWBaU4qwdRW5%mTT19@m-o&lQeX)mOK)O)3|SDd=SW{unk=v zZ%)%Ny*fj%wcc)V(TI`sGYGk8G-_AEJ%B)7-r?S{Z!CLV_X0@1rD3tR#O3Aa(2AQy zZ#J>k%s+U~7D~prC?%-Zd-=2Q@&mCi<2p8?3UkhepoqA+;tXVqCotw9f(_0dLS4_<`oGEV|Le zA2Q~kdr0@kZO9t0;tO|-kIvd2-NDZV)>VQGIvCBccrm_AWcMKWExg_4y$|eV?w=j1 zg&%GOni9qosFzd2CxC*=6e4yhGqVx;4;nfwDrbr?=HKMU)l>j6579{ijnpP~x5P^!}aB{WP;|~w~ z&V1~)+HPlxuNO2%13a1??4LL*-EcC-elOz7U_mB-a4|%^C1kk^i};Thly0#NGd4~W ze@UaywE12Fpo!>ZDGj;07x&(@*zd=sy5zIkmoM~;bLX!(5N02cZbZU89>oULREAm5 z5OCg22u2HN8hgUxMvBX1#7K=XfW@MWY%Fv@I#pn?-Sa@<=L?~Mt9Sq_e#Lc&9a^;f zG+Yu}ECV?W?M9QNeB0~E!uPx1HaC2`i%z>(gmhkA!w;BTSRK~k zHy+ixK$^iT1;l}!q9m}5%S#P>3}EZbt4dH%?Yx+;BsgaOTI08jVF&A7_RwI^K;)@L zd2$9eAp@8a+^_?{u4djq`L1R<+EEUZlVMl$vNtC-Fr<&(`gDq$6;OR(u64Igw-)qX zRMA!1G-&q=vvCuGJw*^#`9JNC-w-ix6#AeAvmN{$w2869CyAAtpI$Ff=ex!_&w{86 z#ph29TKIfPj0MNos?*#CL+{heLLyGD8U4}D(ye==@6%21?qtoH>WsJt&J=9pPsFJY}x z14g|CsYdE8nzxoxqg&7oPxiJD^0cunTR2ibp_rF|Pxe;KBJ0}6&&^|NS-y>3__ZL| zYy8BVYNG_=pjqU3*K*_gR+oc6UCq=%hp#nO)CYvkeOu-i9{t$5uFrLy$!O}yl9^}I zGTE*{x;JO6rxL%8x?DQtWS-1xz@Rl_7Mv4?Zs;k9fZKfN`4yBQNyM>Ocw}2^<=NubPdg73; z1wS4AaLWpyOCP^drXS3uJ^7yo$8%oZ7PoHDBw>*H%@^|);!g|}JDP(m=dWQJ`HnwT zI>p)VyuMC&QG8t7=z_>*)-E?{WMsfnjhu|}h4qzpX2pG%9v=Gga=<=aw`2Ab&FY8? zSHd%V9*tpUMV~*{p;Lvn)utmctPZ=-Xv?L(TZ2#-oY|w;?2eTb+8J-CYvrLBMY_(o zI)8>?8C@rtBB0>AA3NImx54<}U#>23goXMRpo3svbdf<5(9pt`V+P4`TZ00dDFOrO z%t;a;mLo<1x}P9+$+6ZZBljd`D5twBp}s(csv}i;LQFDb*u*d^aNBu&P`3)Ce$isy zO7sD5JjQK|iLu>-uqM6H-GZ%G1$8_#I&{_i_0ONh*i_W^`L<(4-MR4H0fVQmjVR7K z<$1t7GO}B+<8Y6~_}Q=W9HHDn3et*tg?Ai3D+WoxtrOvbQmnTyv>`&JvsQLrP;dco zYFlyX`K(4U(ioUb(%%_ckWLMVv$Kd#RBR6Ts?uHaR!_BS^D?^?0~5I$M1S%;Fr(^E zF6S&qqTu1hTAl=N=TO{ebi$!bXrw!Q=GH5B_l4|@zU!JZI`yKbPtv2)OUBskZf84R zbCu1rrbV^aAC^q_UG;ut@WssNoKg2j1j+=r#`PgVXC0s=Qif;-2yV=L6pRQ;fxv5c zp#tqCLI^h(Bl8pt+QK^24kWD?WBa~d&*?+I!iPP5%qaB-;@~(!*F*(JDtsg6<*T1b zYBduX23mM0e{?vLpOIG6nR?!=b*E&XnMPwpq5h=sgJQB*r|&hnS1)%m95^cW$FJqL zlO!L>OLeS<{E1TwlJ-vhFsO@`#-jSe-E|IcN@sR4SUkF2Ls#X28jsGC>T#iQvy<~J zBq}aL1S6?>q)!1SSH)E!HY)5N2+ZV>8NK$Q5;m$ZK%~O2VqOtmJX3t-bi&sDe(3@y zar>ET#RI1Y>n|v~JS_BAkEPyD`#iD(8>a`pZ*y=ed*`M87v2sx^Bcb8#+9L${Y&j; zo*kBTc*11g%Nb>ESwFk;CYpBGd;j(Hi-Hl>0z>8bed+<@iBKz$w_LzduI{vwn#*vD zhFP>;wvy3=?oweZW&{O|Q>)fwABoVnW#JnHvWH1ttk3LwNTLq^gr2eq3^bF?2c2fVh?|gm@Iz`+O+_eiTsZP|;QMpAg}+z>rs(V6)a4)GcD(6g z(qz5$_1u+f^sU}aS=DA`QpcXFUmHD5F8H)3Zb9|(J5bEi@dF}1FDrC^jzK_DzZ{ug*a!@${2*DH;!PZ+$N$=G5N5|lyod3zM>S(?!aM*zf%sD8!log z4a7)Nfk7p|DCT`aIe3#1t?bJC+Pe)KZg_?_Xn$12iEDMshE0jAo~XCFUB_u>gQO#@ z_OIX7FFm&+xeL7(O}ER;1K{UOxOjKWCO{N5iC)uypl}5SRZ;-ci27SEv7V+x7oNtd zB2hSGkWk})!lO8GtpQi{(<+u!cuCA>p?v&R3!Mr{m#Syyc6RLCpSyPbX#M2e{5#}W z6z@7je9T;LU-OafiBspa%i=qob$YaRXTF5?@? zUzaIb9wBVHY(mDCRfUW}hx=X4*%7tYJ67NG+pn^jM&-+hUDW%%yC2QMag;YduvbCh zS8Ge^w?)ji!FQ)ChCCt%lG}OKsdx9RCr^3Pd@;|IOit~cHE(|Ak41|^GrA63z008T z@yAiUcWx+UT&l|t+@yQ&clWxXdo9y?{d7(zGKV$V)>U1$e4lFhbnnce3wdNH-1VV0 z)-i+du|AH0AJULPx7!HSaS%}FDd5lmgCS^9C>n2NFhy!4EH$WrDN4b(#uXI@5LS?a zVqIJ>QWpSj#>xj%HW^;rZI9wk%x~;yLfQ#vNA?h1q$8y9e;8GN@aRrRYeQ>Qgz=m% zDR;>F!+k5VePG6VrUfHZCO5@LQqw%x(g= zw6qe}_&fi7d?ZK=$CtdKUjM7f0WBsnWXMcA5!%S(5ET{BQzE+AB=!@n`n2vMg(Cvr z+wv!11;;}#6`T~P&_9p>vW3BDLDexf_4)6yCY^~%&WtSvN~izKekQDdsoI@%gd0>9 zc&II3^dVJjB{GmwVlB4R?}?b-SFI9J4zeMAiGiW}eO3qt-r2UW;j_*8f{`=h%Wm~L zYh}IjQ^@w!BaUnvC%cfjZ_gsrvWF*SFGakt597@4j}yL++0b7YliGJ_^{IiKQVWeO zgS19Tpl2ONZ%Cj6)ysvJGXRRMR!VFv=ov8yV8OvXVE>mJU zbYTOY1mn@q^jL$u(p;-Q(HAlA25{FgfbP*^aA>I%o;hDLJleQR$vi&YDa8?M#h> zpHP6>_(;i;+m?zt!IjZ8&LF5UaEe%-Ts~cHDa~*SV=DFVB{8W60gIF}o=lBRqq|--xL~xa?t-H72dSA5+Q&;LF#u@%%7vK8jeF^9gmMX z|9keb118c5mqT6z&N$F7-=J{_HK=L<&vVazFUYM2JaRPHJ&D?8ze!uW6DQ}|?;Lcs zsqFM>&2$sJpo2rTwI*juXwo8t{!agmtLMedp)ZQ_O%*IouF7K-^>uH>k{Utf7S zw$)5S?z^;4vzWgTxfmfP>@63WwOJB#{8Tct^hWNojU7`3^U@QBIi(wKH?X4yJLMFum(2MzfA+&SCwzn3b$b>f8+z9@`kHK(X^P>#0>=k= z(O<(I_UwO!z7}{2c1C|~KU9`n@iuPl-A<`d!MR)P29cKu^npMz1WLV+tIAXOy-({H zi_^St4T&wMz|fl{((^RlE*o>eUj}3FS0a5V$mtxi4+}pUv}9ru8HTQuAS7slCxh(@)8XZm#>p-3$KclVPScb@B#IqNA~ar$M2=@RpQaN8 zq84CczoCK1YS7{`-ulI&^T^t?xWZ5zQc~iBWkfdNg_sv9gsz4>I8mn&1zS!E#yq5a zU8_j5kJmzkbs9acd|L+`5ja`+204{De`>zyf zn3Pl@&d`_7Oh{D93hgeccu9Jb$dz+;{MA0YKNQ@(@OJ7uY1wI`v~MF{>;-+IQE=i$fbE@Lp@huGwG4Z+9EjBnKWmn*`y}pLSwf z9o)E7K8pMzgjlb$lyIi5=^lU*V`$PIe zmnTA+LyVTx!Ki3yB~S@!xd|2-)P zG3^@ZK@0Qy{2|?Q!;2|sP^y1cG<>>ycWSXDEG}S$Sr5;KKK&~^HQd+hY`;4FUFO1J zrn~DqQuAZu3*)YuRNG-s9r6V`xiV(IxEtBFka%J`_=DQ^!zSCO9SIgF76m2A2SWiK z8~T4lT74wkhzBAK7sRZKl(66NUqVLIBaID5?mAx{_p?F97ozr{mfF=J@MJ1HYAEYK z1(Jx_&SG{b)TeA?g?Bs|Mm{5>F}6C#d&UKR-W`AXdV@mvyxEPy9=DnBaaDE1w2afX zRc*t(yS)M?2CZ3gyRAq5nw)v{uM3_9l8&W1erv98u9g*5#w5md{5biwdC4aa=A$W` zCwj)jans@63pU(rn&!D_v8&BlDgtt`vTU@)a*b^b&H;h762u_EPmP5QoL=|Gp8QG} zoXp-V1P(D-e~bsouKNZL7%zp?VIIp5zVrLyND>`@>Nwad=UfKns{t$8PFGJ1rTqt3 zYF9wq&+8<^{;cy|Ddd%hI>&1Xvhm275qefgJJX8Ssn!g3k#F|xKhnf$da=egs(-DT zQ-Zr|puSa+<1Wj!>8Y8}9-v+4AG#;PUX7IXK)~ihSUt}f+^#ERIk89tRGZn5sWEf) zi^CEJ9ZmE(kyh!O-6|u>)%W$a1rAQ}Mi2O)!3&Tnc0oW+w z^80h?DG}rO<1UG2Z(!Ou3Jyes_Sx3(-H&hH`+JQ)uY13DAcbC#pxAt(aj)VId_U(&%~+f0@P?03^Qa|d{m@6T^WnYz+J>iVABqC24Me9r ziCzoI);XMt45tv?aaBW`nxiy)et8X3o*yi2SNgS)} z{rw#D9!BUtnR0DGVbSu{)2eO!e-3jOz@z~di0mMCaqweG!SIEjYQBUgatz2*rgFT+ z#nL~ax&izU{K1_9i;$mN^B@ub0I6`hnEw#vX2N$ zEiS>)ZezZ%vkuia>rzX<&45EVD>_H{J|>#X4hP&$=+#TqmQDwxhF-w=D;(To{sI3? z3y$_5?tql~X^Q!8@wRepBYMYhonsqMlK~qXUhSVktd?jtZr@wF@ZaMA=3`l&>-t{8S7H6%J?H{QW<1&ZqNAYhUhj8}UFbJCIM)^6dxlh^v~7vEuKk` zc?f@d$?W9dABS~sIqu&wr_JqDTl>wcB0f$bE4-{aZpf!D-B@+%XKq!%i-ZOH(rv=D z9`5bhab1*_!O!c11Ll1g6#usB&IqzILPFIpW(Z6FraVBE2`w}9BZvs(t5|9XC-pvw zN%{2l*Z$bRVMkJrhN>onEl%~mw%{$8O*X~}1KMg~115kUtGNu!LLnGR_(xv z=Pfp@Fu3FT)*2Y1)-+I3zincDo_uPFtA9i5!gJz~+v)Z|we>U%Qg>@aY1KcE}`l~~Mv#+xpfvdkYP!vvCE z!~RDK426Hi0yCH*FaQ+Mb^=r@57tSY)vLIC^%CBbz!e#6;dcr@pn`lA21rZl_mS#+ z1#SsmE5L)ciPd~118n?sijpQ;G*rpbkRdjuB%lFXtH9|cD5Zj6o{4~#C{OBQtmQOd zMlsE8ZL~@MAfY*g>O1fS3TJBL}F5**Mnl@1bA z>MrH?ZPwsB&vT87v>%~y`t7m@6Q|GFx%TXZ)!(}~%@J)(!DSMv+DDHw1tN>}{a-*& z;rHK$fCp2$jzYH(flH zj#ajCxe8TBaFYhBOZasyo01FQpbC9w`9T$QpAhBPmgLU<_LF<&*Y#oJhkUHL@zs+3 zGq!~2cx}`ayUokZ&M%O(?j!wd#rl58MRf>)v16dIRfeI5Z<`Pss}uGbQ``1B?{)Ia z$9X|bDWBuH;X#)k?+9x9!NM!XLF>#Sx68Nq&=ZcKi^N4HkJMjg>pW!a00-`r`~?T} ztY`xX#=p))`bRz}auGkE1T6s+9sJbbAIc7w=j^DrSV^k60<`TPw3^fm>883XSB`)$ z?Ncn)4?`7gU^<^@80#cy|HI|{<~qmHNpo|a?74on!|(i!KifAyn9Hl(DG*veDr)c0 zqiU_jg*`z(+L~TSIc}CSbWmTt0kiJ-zdF4#a+FQT(*lR5N1J_b98Ty`62-b>S2y@t zt;uhXM8EzP`j@N4O>NFb>Ge!LX-YgbDKH#6N&Nk5!>?ZpZY_2EIVYgmvFzJRH=_*l z>Y_wWnEB8?<_~YZ|4&czY6L1I&@%${f>NfzOW@30dQxjzqGIgfmA-s5g?GJdx!|Cg zH^Lb5ZZEaf4Tf(|J6=FUAJSTbe7&)=PF>(k!@s=!FO^eYs?v2x%3;e&X6-)5hcbA0K+^ z+yiL}kjt8&HI?w1>E@_{a-_FxX9_f(L^g0nmkLG?`H!`kQ^*^(V^Gx66Hozlu1jEO zFbG`C+uDHV`Z7iX5jgPB3K+?{YHUa-BN6}Wio;%H8V{AMFbeO_^YbTBAc?FKK4>>& zts&(fgRHD^fS3y+_+a*t^pL?ZeEc#exXEV9$9h=%o*(ov4;f$jzP!rH%yO~-nwrng zckkc0=I#4=Hr9we;&D_XVsz{p(N%Bx#Y9pzJM>pU`v#MqXM%CqnE(@Q~3D#ITcX}Yg#wD>Q0*nBbaZuDZU~a z+nd+cKn023Q#Dg-N#-I+>Q4eChcE7ay5}oUO$_$i?W%4$BRxde2d&z&^xX0xOSh4l z4e>Wdve|iCMYJ2FHQPL-@Rv}#cLUJV&6ok&Wj=;sNOy9#FQRV8iTVm4xJjoC_X zHpYV9>7Bk@`oX&S{i-ast3GRT2h3E5`aUvbYaCeKVGD`gZE=Ijl)g3Vbg1D^owUBf z6cjC_9h=Rkvw-;7R!;Ym<(w?<++=w(aDCHf#}8jDYVTE=4AA~Ay;*u~rN?NyPP6hx zUq`({mW+A!YgB4%nmEOEXTt|EsqyjL*Z$AiTfBDZE1*4A7`^5_ibNxBElqgm7QX-U zslDnl7Jsh=&Qf&v_ib~{qODYMubCi`(B#9KlyGi9Pw}{m` zLI}C9MaW&kH-vR7QSK1_&%Ezm#qa-oc4waFnR#dCoq1;Fne(+1CrQ1_Gr%Y7p%<7F z+r+J$j`tf|ki9w_7Td<2~a@n9LH6E`wRV7!v!4K^r63LfD_14Gme% zG7@z%-I}nHLfrbRalJbm7nNG_z!$6SeJ`)>?i%s?z*EX9xR$HG{*sJ%VpWD#RpY`l zUEtY(NhEZiIlA>m4p}ERQAk%48n}URH9rK1<>`8@Z;%gVmm#iq5+bNxlD#6sXw-qN z7)3^Si1DD@dE>G~@80>PGmnzcb1*-M9aQVdBE#u;A2>7WwD5GPZLoFOF*Me0gmuxY z{D#XnQ;#?N_vD`aRJT9-soVNJ<80PTPR`%4;!DV%W2NuLpL*;$T>p;c&P|Ul6&&4> z@9*5DuWZU`d-n-bI!@47xCafRH$K1%C?KoNW5^H%gaXpEb_L|%|AKEvNI?rX!p^O& zG7Qj~SUthRXZZ7)=sj9Q)yY25KyvF?j>x#x*aVon81!GEN@&=xDCieRV8 zgP}UN*-0a96++s~6I<9m6k@rqkA{;BnAAS^yDx^3ZG_cP*95b z2O9;0u_X^hhO3zic9}?BFXq7gYR{HiF52#I!(Hkyrq%gf=emE?9T=R#A9wj+oaxim zX+eo)_o=ZBOl%P0WM!DiLL~|aT!d!09eu#N0hz$vN5f@Kzli~E4I}&1AMCp)KWKEx zxvj>_d*++0_p&(n^>d;&cZj77f;6RHM(s>FuI#trkD;(+9Mcjqq}-H{i$sQp(Lk)t z($OWlKIZ90O+T;iI=cJf+>h~3T8)43Y;IOvZPkSf`>ZD$hTjRhe`$Hql($1GY;4b% zP3bU+f|!|XgfQ_)h%IZjadJ_!2>(Htcrr}<1v0}+%=q6E4@%A54wcznvXSzL^KU zjZ1zpFTUMNHwS2NLg+2XbUYgSo2LO+80f`d>NZT_U?5On|F&7{_2mv&UZ4l8?9Q=0 zzQW&pxmBUH-T`}_hASWQXJO+nNSryfPV;TPOwMX&ccSDsuqYCTw3Uqx>IY4&5;TMiT1e3% zg%9!cFaVqeXnYyaoWv=VqY6vsEM?w+O#Fo5-Q=uYhjClh44|N0NEL@AWAM>5kNwz9 z;l#~RU~0BRCNg}D43!lIW?Nal#*gBj!LNE5yNgCln|A{<4GmXoIb5|Wx?55#O3Sa1 zy4`-9{>)sO#|;)U)dftoimDbq?m-enhK=}3e9ULEEj-_rnv_^nKAy_owwB-TS?q<@ z{eL+Wo$Yydu)UpAf3u#SAEbS}cpxY&T1ISMTEU$(pw?{fi)Tq_eKohqKR~b8OmOh+ z{kYUIZ@2!~eCpt)J`Q*KHg@y2*7F~ln(a1~-mV47`A0qNbQQ++r2TZrdI2`z7h?OHv?j@ql29BkLWA~Trs^{P4x_6ELIV6 zOz&@&&tIN&!Y4{$rGQd6n28mb%GRS8ToSTMWTb@#!cCm`dh8XgWhVRPPn$ezm`$bk zy}PqAXHGjFWB%pihzm76+cm`7PDwv-Zl#9wfNW^s+>e%T9M_00|G5-&yzR+#lTS~} z8fdNWDSTOKhIUjGb>1=U>DnD*mL`2p+k82EBF}!q{coKR9` zl>QEWo3LxjI`*_TpJtS;YHx}*9V~p;_R!BaO%`jCR&1LCAME(#=aO_T$BQa?nk>;4 zz3rXRA=ZXxb~wM^uCHIF1)j5=CfL9VS>NvMoZt^{^P>vemx;SRv{<#B46IprQfkXW zEEd{@#TrA;p+%ATd;0GW&WoDgwxf&+-q!l|^%$hlbm%nkBfOd&J#Uf*WxAXFTIbuX zM!}AD)mO5J@b6}>ApKOo-fFvy3I2i{BU9) z)`YZa{C?@M#}l<_Kpbei>9&I!I#bXm`?00qZXjD!jP#499ujEQ zQ|M}xCqE0gav=H$Wi-m_&+2=*0;4x5IN2r0TYQIvPN^IW8h8TQVkTt!4JDn$G>r{B z-K+p&Z0NY`kwBf*X-I9#U3YKn{wMd&WrggDExg<-sarnms|%%0sk5tL=nM{dLC8`nxTm)ijkH(Evl>=@b*5jFPm3^d?CNr4_r zO^z^bz2J79m@psYEyeBvJ~ojd{DW92B(jhq%wffcG`!+^qTr$dt;Xoy`8jLBsbTDK znW-ZJx?dUeeC!KkaMI|-KH?=iv;L8**qTBqnb{k%KhM_Ye2esbUYBb9CA3S_s)-hd z^*gZ+N-EyzES3znCymFIY%oNVa~|#+A#R{}8{ZHdqI2>0!tTgA@8;7B6Z-}f_k<~N z7Dxhdbp@q%sT?E;^)BuYwP)EWYyi{p>6)HC#EcguH5tzc54dS0(jMMC zPTG;)wRGo?a?_R93;6vt3Ua0QreyK9zI(8*esqUxgWqo4Ye#Zkl)r^5O48VI^k%3D#u-8PceVs2xJ?#h=cUD!u|~+Ul%_(`B>JeoX>#O|Q`_2ZW@zCh>75CROrm(tqsE?`t zh?MFhva1lHPjSB6y6Q(PCiUio(ltl^IEbDQY4#z3e8s^d2nCE*AhhjAg?y-bZ7Q@j zxrXT=DK=?fr;dDh9VUTV%QH{Gha3JV`TB}QIyKwwF6AKwg^*9B*$8Prbg2Z^-Wa}t zh^|0}ti|SSC~kp(LxJ=QLc+H}{rnR*d&p(cCqC$jUhCD_j7FKyFSZB_W=PjhOhW9qQ3cg-uB8E@ zvzq(mRlAc2rOp<`-C>Ib`sq+!J@Ax=4Lm&Iv3~{uo!%YlL$?^6_2!@v4K*o69K9M$pMj(1v-G|9hiZj z2Z`q$TJ<%$aZ}ju#P)?_li#*S1Uyau@Pm?TA|n^|(XdNoDzER!QHaR(br;lYyS_j=7Eu4L`GfF2fP`8PQWp@Ax_4n zmF`}TR;-KFUGePGc)JmwJ)|FUgO+CYmk8TlDoadwBkr)XoAAlR63TMbaM!ti4j!`m zJj?v;kBo^$c#!Bz*D<;aR=uba)4VWLswCt{6bIgJQb-zat}f@{3$eDrFo8|m5`l5M zc`dxIH6Rtw`cWF+z;)yr1q0iJ_X|bu3lzIh-3{Z}3YLU?C^G8HT#?7Tx65{^@sSA= ztq*VczQjYzZd>1tM|)Z%)m4bv9A0?bzWu2wsc8D)NaOb=;V0_Z0lZN}p~U4KzGWKn z6JGjU%cvA|vq7G&K*3hG|5sMgey}3&*L%pH#`1|5W92~Hks}haN@O&eS-z5&>QGN8 zRo4kuy)Uj>5m~FvG8q%-r#bcd4ZA)Wwm)XA{Nw(6_rA2lzN52$L{~mfIDj{DIMMIc z1}ei}0R1@&tNj{DND(RQ*eC9=j>FBYkSbf~8|a4){QWdL`>)_=1`dcTrBT%@DZGxv z-4X`L6OqvrM*3j_ANIkx?BMbGJ$iEQ%u7^|bnA}EmQUQ?0SUpw9J}AMagOh7Tyvh+ zk$uR=dfQ0DvoFHr^$5zW85~nl5`SZn(Ol-vbQ}ZRKi)mWijbko6b@f_wN>OPSe!iB1$i5t4SY+>a2Rsilm!zIYZ))L`KU| zg%*rgg*fBOUHes;m$8T+4!K*u&nKvUBK znw_(Rw#cf5J0`WZC6WJNLo!0tS`XTXdexJVVv*4nl#6EtAOnzjcALu)`9&9gx@O(W zJzqX)XU~ga+NJER){Y&#qfA~zrx(s!;WMLq-r(y$pT%yS?$Oorl2aH7q6a^CVK7KK zj(-{K9M5*1h*RF&Es!R02Mf!@BsN=sB=CmFXg`|9N1Cu2E96XmZp-<;O7wl+xZIyH ztEE+$jjLTuN6vik^Ik~Tpg!3{$2-%-7g#? zzh-)jDfy6WW|X}rC+C%19#zfBtRCUWJ7Mov zX}#h|`vES;Z5M?6E*B7J5P_~Kl4vr@at{Tnu=;>q;f7xQ!YJ|nnqTb(R7hE`z7@21 zV;L{lk9QE*Ghy^Lw`-SeyWEGzk>JlTW)vTBT*c5}yc$En{!`mB#I2I;T?2TVT;YL6 zBmm4G!PNG28JIu-l#J4&kWN)Bpc{JOYsEMzQp|^Ks1$9f3?gkCX!6s;p_wzJ;2z_( zS_cu!u^nPl)^L1kV8Wa}+Jo~Snhl@LwV&~-*TERFwcCyuX_uOa+FiAS^F9lWt%DC% zYvg_^w1toz`a=*UXD@WRi->Wr6X7mH-@@py5y;^3&I~$AjU4U-{azW-6cGh6wuE>3 zb;zCMe#`GIY{Y|%Sa!xge8Jmvw{I9uSp}?}BSZFghqc-Zi8u1}v`v;lrN;7RO2V?H zp=r?$WVPWGVQkNhr=!vuwWe25XT+`;_;|cSp{|qGYHRm*lPi1^HQ%>$oHnYRj{AfC zYnp1cABoScuswa;tO3R|VTiSeqMueXG^CEjDpEHzVh>Oel!^soiPauLIqkz9@x)xsHBHyxsH<6EkDvK8DXE0qaWjb0F^fh2AZ_Vxd*j z&Z;Yj2n|LbxNyl4Mpq)%M^NmB%Fdd>s4i*gyQ7y((~gz~V7FMia03cY8nGP6mAUXO zs;xM4K4T~DWEnI^B=l=SXeHp!0B4wUr4X4^bvQIuigc{33lMUT>6yol9siVkgS{A( zfu+;m_Uv^K*5$B;_3o_)B>n7!$!$KDKdo26g+L=!>w8*JWcT2jGc>o~{r{XdqB?DFSPHd!9GD0|-CBd_HRE3~Z`BtJw(6^wevJm~!N4JgSe zW$()`%T8>K`c)cJW;I23$o=}vIrH|-S~P8F`|>oqjcpt^Tw4%-f17<@@~79*wNj7_ zrm_Ba@APjjTsCF&l(}q*iQ8>}kT+oNPf~-^^vY$^fUw_LBinTjy!&?eoc|7wFfxC> zGiine$M?TGpQR8P^P}GZ_ctHaULxCNq?EtF%j-g5vRCMbpzn{V(m|aj3QwJsbom)q zXOT8cT7K?a+@|E>o4H$-8}N*}-PhmLExXv5uKp12015}&0+KP!4d;fdDi&X1@H0-xZW4P9g4cB?bb1>#sErUoEQA&7N^7(lY3Idnc=*eS5DNuzes| zJ&@~?^?XZ_-^yR>CmuhaQ5JN$Q^s7QCwrfM#b=7)XNu6{#(zWzx+Ul(%u-GtXtfl8 z`$X*j@`hU^!#hV?>@HG<{OgTe(Y=`z?PkGPj6Z0uX{0ShN2FjXH6wN9P2mVO&OWUx z^x&7xQ>bpyY90WUkmaIQt(fl-(SmWzc>`&@$#J=3y=R}{J+c;Uaxm~XB%)%b>PL|Q zmm+hQYD(|@4DOKOT;sI&)%R9chDU$aljyhIsQ>m6b{X7iT_Md zD<5WGD=l;(E{6QJHp>heUM4>rxA^sfOQWxknp-*M`@@Q)cdOI;d>v4Grz|VIqLWy~`(?Ah)8X>_$|aMQIzBsb2%nMeK`W#9dZm_t%h$e~sUqMDgNu;1 z)D7GO{|$lE-@F!_HaVEV+rl0E)J@^Up#bZ|%%>=LO*Ha%i4iXpKESpk)d*Z)tZ>CQ zP;ymb83=rJXtq2jcTf``Nxi65Z+t~R*BGvkfU%dGuO~EYjYB?{HQnMkj4fH0IH#*) zkLO*CHnmUxEj{%ut5xN(uEvzliz#~)Gn#J+oeVj z^vUf*_E0D=x^zX0)!$f-_e^AvRbWx&Fm{#G=*TQ&Nz!h$h#9>x9+f7O^NQ9ba3O_O zE8UtQgBa?c@2R$()|_hgl#;MhVklV~a4d`*5)>w>%0Y=qf^uS+ndza!7tY& zID1j;cf}se5*qZ4oKpHUZvIS{=lwi>FOAO$km_yjxZ~KyH63bQk_dF2KwN@OEBl_J zuJKfjy@^GW5ki9WLeSNfZ^R(1;*2dNmM^ zM)lgcR|co{(s1~vYmW;7A6B!%n92d$lkfm}!IiWDCOUZf7 z&S3L}pcqFqYMG%i+4-d0hssiGD1g~*l%)W1FB6L?R9mNi*L#x~bnNR~!2`d}Z9;K8 z0Lh+G+$=dqP>9nifo8E~X%@Uyt2)kvgjAj`TG4mssF)~Ha;&>)y4hP%^~~5M?+Q5P zPsdnl6%zNJM@@CC*{<8U`5yjN~-4hlscitM;1Mho`Z2+1-Ns*PnWw8er52Y@WCGb6K@w(9kA(!ivMCvctUqi{gX#Ut3xF-#v^O#|A;Nl{n_%e?D9a?wo<-M+jf@Xhu>6^Yun2}jPf52)H^&q~2Mg(rq?|obs{kEn zF%MMKcZTbtN*pM*^M%0YM+Aw;w`1Lk;kzbG-J0+s$$IKFVeaA~yVy5i&rr?j)XMFL z!;7UC+TLn1emi{9#HaS|Lz35QOWby~wTvpg6;dE&V#TIbsvIZIQ8Wg3!KhUjh9~zT zxBfMM&T$*9@}wa~+^a2GUr>wGK`$!%i0+l-uC9xJ9irB^epHw~;zY&)zV4&Ur&InAw_m9FP*nIT$T-qt&l1^iSc2l zPLS<6@YG>H;NE|RT>ywDQrd2AXqwAFYkc)gidvJ#wohRnejrbX9u%5_H}>v2FoQ;P znrQN_8zQ#6d%LWmLu$a?5wlQ6#vcuVaI+VMIF}P)j2)MoKxw{CZ?yrwK@W@r8p?3pn#6Ek<9lSj}Zw$|Nz#F}@*%_r;WzLc0<4*lMJZyp*9x)2`^x)e!sohRV+pPui2W5KV}oW183 z=}qM|WjuTqTM=WCHRVA2UK^fX&$2CDeC`dL43GzfQzfYp!R)nKQrz9mc`4t2-9+5Q zH~ZypdCKv+eu8BXqP5(8>C!!hkAm9}bH2zxiG?2o%9k0)v;&fw3~H~F*^ZBAKJUNSI0 zy-#^vXtl|WxIaN1hn?`3X%t0E87TmBN+N`7PN@g}&yzgOJzz?c55_;`1=o=nb-{G| z6~bghl8c7L;K?7Zy}n#*QlD1hURSo5TV1s{&HUzTadX)wYelg61@8l86!>yCy$18! z{Ora*_9@#uTU~xQ*x$ChXHa7AlU2T=5t7ak=3k4`?pVe9%f5@@ss)Rk`ton8!aK@G zkF*ln9VZ=TpYAetb5pHZS2!e63QT2#{>1>uC5%mJXi;?fG_xP$_HQ#Aaj=L1NCG`Y zXd#{oXLR7nvhBnCHx2pWX59Z;%|-(aBLUm|K<9e9?D*>^uNI8Cvu|#CgX_e+g#BG| zhc4G&edkRti&!_e14Uzxej9da`iU-+BecInx|o_pX!U-%_}9l0uVt;*BzCwn@m#8f zp#Qq1y>4{%jy*MTpX=*RhQl_G`~H5#^MvGfTN5t3RCQS^v?7oJfr^-v!MEinxyq}G zaiM~zLE#GWKY*~{NWqEaa<$71dvhuBSR;-^od)%XdUOIRlpO(?M$FBGz;tdLD zsNW0VWXD6_C6d9DaY8VfgVwBKR+$v**k;9qy||ep@v_#u6tq2;yswshDQMgv77S6X zt6^V%Tfi=~ISLejUQ^c6` zlB$>nr_U`_Q0!ek-iVk7EN9FRTE?VGkBAHs>|llVauz?!FL zAksDH9RVq)RD8&PXY_i#$G+de^$Blg|JThn|4M`0xpp*^95UmX_D{*b`2ZejwOj7O zihL5mLdwksV`HN;*d#k-Vx+-{D!apLPlshp>&xdY6$NakSlJX}#T6qv2n#C3#tB~Q zPgZp_y5daA8)6GFY%J;LfUMrg14k&ufkBPHO8FsYbJ;|#6yx$wfD-Khr>hy!B|PfY zv@;$>oKGf89`H9itVp+XaXM{YTzuVN^)b@(cL-%f;wR<6&2f9n`Ib*vue>r&M$Goa zu4reUzPK0#sk-8K)sj?7x&#PT~Z+K@M8Hk@&D zT?4XJ`8jUYg~+_=qaLqC9d#fU4NfCUZof!qt|cSkK_uBHLTk}LHuN=(^!$cLHr|Y3 z%T7N(OPb)~z!g%>+f)DUBD4by)RsSBfn0W!wfXw)(bBfoW>eO@e=5~X@xGGs?rq;; zvp(;AtULIWiSvlzku_i2grq%9@ABemnf1?EvwxnG&K-MMeD#%h($=WYmc=m~w`;x> zB&Zjj+){$Q6<>*?Krw9S-LSMo#c6?Q3CR{5s5g57=F9Gau=024f(mX7 zVuvHJ!v=sMG9*sLFu0=QQ}%$EEw5H`A971_K?J9;KVaSEGc8YUM+Cg7^|Hf{woiTc z(O_nFvBj0;ua?er4S!p2kaF(0?8>^1L)@+jS_l~-LN)k;3wbDST=!AtA9%Vs`8i43 zE~eysA~tFo$~t}&?# zmNri;hvbk3WHTPt|E(z%Oa78}q>2R^W;iUQ`KwwO>^4E&1K=(e*d6b<;AmW3 z$oRn6O;Z+cJxwJ$wKsb!sTy6Ue{Ah#ulTw1M^}B-sL<#!r^IVU{qMcp9@0rL@ds03 zFdfC80g--g@xG<rFmz<}_!m@iZ|po91ya~%y?za!y~P+bkFX;fv0-^0 zLWYv&U@nG0#EJk%T&!9`%pi)jqQPP{qI}3nu2_SEZo>+z8CIlP!rvl5Xd**9#LLw( zlWJ?hheBqzU-+oOlAN!4l0y>RDOI}!2f&%=;boxk5>*~SM?asA*r5KPILyWJDW(*E zb|e0ZIB3o>BBC3lAJ^pyv z>t!*qcEzbye+I3d=-Q6p_STDir@LhP@z{X#qAH`(sGTM``vT3E<&0?WxOKS6%k5Lz zHmpaJ2xLm2BM=({U!)4`Z(BL%1fmtYu8e|A+to{(LKZ$T;OahAijI7=#MS0+P?ojIiIr;y0W`~O za3uV2mNuJN#NzLLsZ0tgn+)Qy-?QEm^x4im3K5T#ooVF)yd>E-w$G6lUKIc1*hsha zT#uXc#Zxsh$SE4_yMA7NBRxuY@D&+qKi<1t+-mUdeSE@5(87a*SRiB|?RpESZX>PS zucK2A3(?zl31U2{s}4E=MSIj~!0{OXFiTbJW|O}vHiKy)v>%eqe@Vf{hXn#e%<&pm z1s!LgG?Bvyz2b>McjTB!a>dK#rM47ghwgCw-&?QDZME!_ zgY>~e3t;(xjz@f*Yhf;iGxj~6Z}D zm{9T1lHOv9m$Q%;&NvYr+hxvVP^GEeUx}&^Z7JXaOlz2&&OxYSEEH(~Gr3>R;Tz|8 zL1MJGc}(g@9$e{i+jpw@ygjY=i{}$nys?Wm8*Fb%ABKdYj52D{L$n50{W3HW%M;NBHv9!q7HX5{6=sQ2{iFmr2C>F{Bj;xYVm)EuZJ< z4|&1BvceRYYtQ^<5Q=mMuFI^CI|IGN8xr<_7Vqu1E8mF?#oYj{DD*4D0hgU$D9aZH zkcrUD>=zOJ&@`-VGhK<|9<&=Nevb#brl%)g=R;Lte)I!mv51JmyIQ%%4OpKNL+8U^ z<>eJN>cO+bNVAo^YcGQeldgSR*)(a?@9GU9+!c?{`g;c(r?kz z=1$g+y9QblX)BhE{#`uZU_wdbhcg>9ZuIV3MtlhVrm=-PdiAnp=klh+UQ3x0_j0#g zwi9##V__T9zdK?M2}AXpcHi?T`qg-h1vtwcOA5OQ6joe#zk8 zVOppSGUQL-4)$N5OE%-Oo5iU=`%$rN2};F77f!nPBV8q9o#-xxm8 zl07)f*g1uG96I&S%{^&Gc_V9PdX2ZewK&%6%)Y##t=pUGtyn#?+tb3F7+PO&R>e(Y zMpD{3)`AWBcwWIG+KA;S$eRjS<){r!O#u^YA{SI{7~^1B16agZv?_#7<-Hke%CA2R)1Njrri>G z-Gk}})2eDgtD!CXXEo_k^94e3QbgQF)09Gz;~qr0A9*;nA$9QO+#S1SUY#9qN-*^* z=WS4rnX8UYI2YR?E3b2xADmmrX|0ogW-BC+R@AxutakeNPCkJaS0Jd%M)3 zRGomK`uHp{#|K9GTrvBnlvQL%L0-~cR)cACi==VdSV^0GX){o-W3NYoFnUxgpHJoB z%R=Zu4)&o>d!r2}p#pax0b`Y8RkK*ZW4S3X#vaq0{KqjVP9H3CFoE%xC^?M_QGZ%r zOjDd<8} zYUF@4LD6}tHF3=&MO};Dfv_1+@CpJf^O`jz_CcSQnwKd>y8Ob`p$2bf9dj5HDKJCH@>ydwK3^IEB^!z>{?81NKZnIe@OoPwHm!e(=Y|rME zKCepiU+u7Q=IPR}ZnjmUiD6@0PP>%-tkk*o;LNXzq8~-nfm=HQZanvo8or`a8?AbA zzoCa5-Yt0^{-8(Y?DHmm5st23qO!Eh--ja!fo9O_C-Q_6pPHqZCLGIFP%w)wjYGT@ z6$*a?rMBV121GOsfH9t&`C4wPdKCqOwr5{{PC+=qcS)36jCA(wyZiCJhsKeKeCAL z_u~!9@v1Ij`MFIw_4V84;<5cq`@~86oQ-kjgv~NOQ{>+;-_&84TcmU{J2K9IKzcB2fd_e~tCA8wFive(w;W z6X0gG#h8U*-_%pxptuBYSXZTeUG7`Y^}PMtcwE6|ebLzYgB_|V+LMN!UHBd$LCr>} zt*CZHAI}~d?K@1nb~Au`>bP~yh&t~f(7WRuCWzSiWaHH))b^2-eh{v>2i|fZKopoM zTViraAfG2dy0w>PY`Rb$bhGPLWA7t5{GT3hJA4oqKW2_d!vQLu)FhPA)HJ8JthjS9Le&LX7HP%eAC z3F+_Z7W?uI5LgptKsUP4X7wum;@*pPedNX_MYD1(GR+?b9A0zIY~D(})CF7mQsEPu z^yZZ$rxvae^}G}R#A|G;s@xf^(NYKq!`@Ylt!|!g1WLa}ucGi^_N9zQ*@bX1qYw9< z*UqesYhLuenzTJ#wD$Mga9Vt z*Lt@224BxRDvHYE;-LY~zx7?1}5%}{z zXs`lv2}e?iFj9BU`e+~0hC-5J5sQOz@gSWUr^pBCj+{Nz zt&T(9j@5s+m4X2Y^TTf-AvcLw@VzDV-l?TpGu6cw^HrQMYa3tcdr4kFNV~oLXJ9P$#*vB57==vAcI&Ksn7I)zYYgN03-)s!bS*Jn|i!2rp#dJp;x3|O!4zufw!x?Zyq-+xzX=teey7BUYlj_ zZ%r#d7t^B1{M(9H!Dt{}9HECT$lWc`DmpMrGIjNztJ3WPi+;1)W*>?+%Nlpz?x&5O zwqL`YllIBIPf1>y?R;GmJk6;KKl+%+v>-}=JP>T5!6#o~t(}1fV6pc!an~}W6$zgM zp%8+mhh17ORV>6aj+*BgyAOB(_d;y?QGOKK*h%=!x4#fD=$&&gyBWAawEqKq_4!hj z6HBPVm6!>sM4Kbb@Pw=As3dS$9%l9-pX51S?qdAL{Ol81uUARaXZ?)$uZ_zE&kFgsfifS-dfu=jtZxR+_dEhVLE<12*Tpz zNYUAZd9xrl_{fIx5Mm004P#-GxJs#rf4)qMsz3--{sAMDEgKNEFjaC8c=Mp?g7yys z%wn-(o6Ta4ip96pxq80tpJ{ZoqP@$hos&M@(`bLJVBit6v>^W{x8w~1Gz)Y9lEM_Bz8}ljlFbS6fz{*LuO z(@Y7uPQ+TQHipc09~H|#dh*Hl->dl@&lZhoWmWCcFXZT}HCrZYj$E@o{_;vc@kZ}o ztOne&L5agp3DCs;FJSqHV5dc>R8{DI`;`|L?I7`Ii&%Mhky9a`u6@hkj5c~D>-qjc z@FxrQ@NWK_=dHHyEE#y(YoN`?F9Yj0)_rKSh&Hv9Bn*}r-CdkNIlIlRKYhAC_(ht* zy&9myFi{c>L$;{p7G|EqhH_JGAVsGZ^JpN7C^;4yTO`FONSXe&8|wvatFdh1PFoIK z83?zO)&K`7cwp0YU5&=5RTn50o5m4vIVf8Ow7wV&1<2QlBmo8@)*gH&jRgZ1A(}uI zT^DJ;==oxe!2Ia;2`5L~_e(T2o6HV;?Ec^JIa1TUI-R?wtS?;Un;*2Wi^YS?#3GYU z&a-Um#wR?!l05f!O1qb*ri03f@SU6^*IAQoyrha4)otC}fM1lH5d}7#K6MYE3Pnwe*?0DW&wzA7{&M%?=`RY}MZJ;pZn zY~kQ;SzYoo_?&T+K(wKMj|BdA7wSB$VV&RZ!9$+<$FEjyx*l|Z)HPqZFxy6|_Ux=g z1)(?c2ZeFsIEs;}%~#OwoUpM=NWegtQCt{KZ2JZ<{s&BuYO)RzfvapHl0L#XVuZK{ zWZjW*KI0tI6WS}EAz&WYf%GAP0b3_or|{*jijLCb7~a=2J~G#_ciA*I?MB?DFdEDv z<@3!ENrQ-W9e;jB5rj*9BCp@$`qd}qe@=b*?rhq4?{|A=6GZIHb{BVV?{=wIL{;F& z%H6MC4H$Fw4FnAl!0+FK&Z6c~$tm3EK5EwJwud|H@kV6r#H_oc!tP7Q{ApkN(D^Ab zZR5k%H`6T6Tx;3rg6{Sn@Aj0ZS?uyYoWD9SG%WDj=kmutt`9r5ASa~K{2{OZyBl+M z&8a@H{rc#>qwNiKG>=ZT+iAXg)w*gYI$v(PWLe2- z%a0HFV?ukz#-wJ6tTv3_@}+X4WZQzw#%m3WI=t1kwBX#}#!Q(F<9}Z`{tD3~Gf`nB zME)MTD2Amn36&5jneD3OD0_{PhY{$i*}J_NmLEmite-OIWOj-~QES3ly9*GJP;l{w zV8l4_+HfxtftwPDb_$ffh(kdr>}&<1w0n5JkSPhG1fp+NO=|y`&0T`bJ<1VENS{>ucFl8XcX7eN>Rv~6$fEHC9+5o{T zR_7>i9a4cQ?QWD3RYzbeCjH8wJVDP8LOVe{VhftpBl=qq-hQAT8`sB_`6ph$y4b6kXAYBhTxUL7 zP&F^tb3$LO&j%0K`o0R?kg7d7XRm>Vv3PP*DsL9M%ynTVG*Vq5H*kp1%xPeG(taUV zaRDbbouZ_~o~PL@3@oQu#e}UPNJAMm zY?VVS5jbJM^}|DoF`PmpT52U{L~MUF5U*v@*%0CG;lwmZx^ZJXi#phz-SN)2(dYet zFLt-yW7DDD@s_YfK5|6tL3kZX9>5#7cCqG-84d*N?xm#R-h&;Z*G%Q^8_?v{^|XEB z%AQrHo*9QPGKl!{q49HNX^5kE2AWdL^>~BpG02_^LNfp(V8ImLMID%7gMz~@qp=o~ z_JI`5ZMJ@Mj%NLDBN@5MRX4eUZY0E0j>-r0#7eSB$pi#vrV1p1LJ>O}ebA%lQ-vC= z^So7I3wU{lTU+|Z4t!#F=|s+MjqN^xPA)FKo`X(Lc@c0)GTW(>@s3gFA5=tcGu-n| zyv&q4V<0hdN}IK#+V%>pEp9b&)PFUT7Zn7}a;m&F@z;fjU;lN!OvZQM?CF}&;fT-Y zZfRN4%5oC|bthoEDq@eKaB?*I<@6O|1h06?L8nS%C-G20HM2GQmx5dc+`oa$pkwVD3DoX|cPudYgYVnpF7%x-X5W66jCxZeLqvpV& z)M)N5Ub-4N@6|F2)+|8LTUZ&iLWZ2ZKvnu=XSY?1h(+AG&9hb}#;xePs$%c9zLUHk zT|BVutU*-Lu&yWWKXDc5JGHBGiRqpV!F|h!7j5%KtF{*u!l-H)WWlQH>Z&^w@eX~M z{Vg8p@WIovgTqrH?S4bl;#Ws?Nd?8>eafBJHdDa!i){;a!~TMxOj?Nn=) zec<>&a)XoeZ0Ec+7xdV7ABXiV>m`!ji>NAmJSbK7EWd=pL2_1OQw7l~RDmrKDlj#h zeSWB3Jqc3|+d3(@xY5;YRjO5LH6bA=Z`R8Kw?|I4R-3^S20bV_goYlBH|cg$KgTz6 zX6`V**cZDFiZL6+=H_n)PozmgrgfMk#+(@;th*A8C)wVs{Db_7szV2_7yws83WgnA zin3(A6lrsgU!Z`NL&2R8q~-ZdNRvGuJDV0-`Y&Jwkw6npg+h7}mR|j2mNg%EO}V9`1v~SmAYu*_fSb?(1Uy9 z?@ZFA=qR99%HQ3{o4PPG>EzMJc|QBE3)~!F58g*MN8f z1Fi2_1+mYYP7L+>&?z?b{)G86$F*)NM4B@PK0%sBPL5I2toC;{Thbw6ObBbzZ-h2o z;2(-Vu@{28yLHNrF{B2UMUGCetVw++uAl1_;9jG#SU3G!u&1LdtNgy6gR}OahaDF2&OK;#aM8oO z0biY`74H0W-(}(oXUhp;%M2%PvbfP@`exB=O8m$UfL!tXD91Aw_$b0VcHYS{>%8`&ZzB6!SSzpmpS#4=68B z3T6w?7Sjw?L@z_Cg%cv|6)epdm77~avl6BqB$XmId?rAxS6m{44~4Tvhg$FNIzGBF z_)repbED|cj?sHBd)PUS5=bMl5?})52G1{21+I=sNKjg~GjphP2_AD}?(fSByf!mo z@EpBUX$wPLR=dx95tAT0{meJaI&b<0$=gEW)U-J7YU8cT`H=6%bQJ%H!J1T$OOCsW z|G`wSu=KV7eKAZ=Q%WZ8a}5P57X1cCn7bmFH-K}LCPsS;wQ9J*9e2I!efGNM;BaY* zvG3=oyAeJYw||mmw5vaS_hS9b%)STZ(>(AJElz;p_~sr{#;S%~mZ=d6^qc}w+RSwg zN)mA>6%PHhpNf5l-Ji?IDIFR#9^LgDB;mFlbnMH* z_4S1ZtbY|g|20++Qxd5=F?Lzkk~d>rt+$6cd}}x7rs1wu(b+S5+u0%Qur7YP>}I46 zGt}FAWpZqP5?1RbGu`Jr8J&lMi++v8s#F1H*o?xbJGmzV%7yPSJ6qxAB2-{XTX~&F zG-5X(&E<@PTphucvE$lFO3!kHSQIuzidY#TLd=k>Qtxjp9ES!|7pz%Ln@GeVv6B)A zHznF)LWb*&^0kcATw9`81C@jKkd*Vq+AoNyUucFNd?Q)mM86|Wb#qyUvg!+7JfvV~ zI?d@vf!z}1PqEV|KhsXBb^daNSgHi3LnOf?MV zCD?D6O<+tr?H)+-Qkq!WWW%Z=>cQa^rYAy=T$*)pN6}(eH{IK_Pew<{W8V%HkyA1I>-Xv*}@wv3p7ab2o4LHKB z(Ot0U+Jge#y3JyjWpC$o+e>UX6nkg!ij#{9J8tO_Gly##dw7Ufx^|zDAq$UrIA1u? z|Ii8HyW5}2(i4`N4T!(d^J;q6+)b^bpN{Z;G^C@~xZ2D9RpDC@=NQX+%zOSzgxJqi@YUT|rlhrckGf@2K*kY~L9kJaOfGtl;D z@k?-E=67C}Vf%lAeory&riD#h=|;ywa)@KE@5NUk_M!qrVh2M3I|p=iJJfG?(v(IG zTwNWd*daBD0@KcJs^*HAetr_;=-KX(yF~$hWt#Vm2JRIYZ*(u*Hvq+YevnleA(j(< zL=JWz&WRu&Rp20oJP*ix0UXQrP;H%V2emFhXq(SAg&)>ij13YRSlk`;b!cPD!6D*< z>&^@Br&Gw6;}vT>ZU1e(Yx(=@xVo(!O*i7}7c{nzXq0=*Gzu1#(_weciw3QfpULcF zcTU<}lSjh7zu=80;B|6ZZc!wj!c_y@vLMb*$f|u(Sj2s+OCr<-^N7cfPuFanpoK_% zeao}0k9X*~zFqWDoTn2g=WkMe;PHLfn;dMRxK{?`zYhbw}>s z7IdnAR%$%DDEU2K=V_0*bKDM?3V$V3oHRK?#lNg!jXQ41gVHsFbEYs$&rH(Y^<=o)c}fy5x7}NNu*<&e zac%71r8Jy~Z$GT=7^Uo&8maps8S&u=@lDUR=Y^s8irPScqEK33Ism*g1+ zF=LRByG5{|J`ne9bXa2d9(;cN^qE-}lMkg;zOFj|>~!F~->ZU;4%S)dbneuwnZj*W zU7~|S96!c8(J6=Yk7mMBHz{>Ofy=u#J5s^;%LXSI7czhW)r4G63Ul!H%{g1}C6MyO zX1n}|WS8?_fc1dZ|Tapa3p4Uw~^2M zVnvl_&xEUr8Rzd`@XsFjpFZV}@+j4HACmVX*w&!R&j`1VSP6maTb8>gn?K!>u;EJQ z=R2NvxBi^@I(FZM4PCoWxRms9XJO{ia#gYvNs=qOdIAMQ-6pnSO5#6I#E~-B!r=mh z9qs+g+fMZRui#VUN6re1sJ9MYYeb8#UkkJ%{@a~$6$Jfrf_v2A}rW8CJ)_>ogjn^Da%_arhjPiYP_dF-9b@g;jn?Iam z9kdgkwJEt>JZwnUuF3jcroJqEwSH{exB+DzE1yMnaSwks=V9C9+t4t3?K%2}vLOU= zBhUkg*c0-cSEZf;2Im$twlPQP)~im5Lc7;Xjlk%Ya2Nv!F&1z&h^1b92!(jE@H-RP zC%~-AkTOFE0f}^^tPo%^pdoJ;6tv2=p;WjU#-Wc%sxe?{Uo~1$@ohdUa2~~)gA$*O z#L@w35p>wS;Q9N&x*CG!-bV{(5P>O6F8Ca9xV~HrsrUp< z;@@Di4-G{Eig8ekKULmXgS>?*ipIkEU}-c9ObaAM1wJHXD(5;S=lllSv|oP#SWc97 z0Z-0e(whS8RzyyZSgGB(|7+|?;A(olpZnI*_S);K<+V{Nm3@hBB+0(CDDfnFC3{5f zOO|Y9%bs|BlP?jGWP7PdL}knV5c0KUm-T<<-usr~`}_P)y_s{)>~m*1b7t;b#21}r z9hgc|5j0}9T}cb-lo)ICrVHV+a59miZ!W?m87=3m!*9qL#Pbj_xg-<$r9QM)Bnbkd zsL@w9|0EEFI3!|0<5fC_BUq($`h!jMq?BB-5>eYjX;i&^1`VZjHHDd&N7U2KYQxkd z+Azhc#}o=!L^9@Y+WZ$e2?U^reH-A#FX%P;a?D11F4ofae;J%OPYT@hCPGSDAUOU8 zhIW47^vP4fx&*!$76uGQ4Uburiij2?p7j@-V8;NF_BMw(Y4Cy9G;q|*l5EICdUF8X z3pqYGu^xob_MSk>_DOk<&|8jj0u1%2g57(QipR{GQ|aua?cXNc?vMj__MOSXg0VxxWuJRr4R&TFA&^Zp zGmqn?;g8J!|I7&+XnZcSfyN3={&0*6zYMtZAG?$ii%a*yx;uP3xViXQRdTz8hoM(a z%bS)BcE|Z9%&Q0VwCCxzleF_GX)Jzi{&8sElKj=ibFzGX9Mfv)8G77r*UJmxYqkd^ zCD;ym?l-z^<&B%7zJo&U~cP%(1`yiDO+GaN+4%Z9iIUti#D-{$+ryU zO*la4bO)pD=_=E)9ha8OyV`wLa-{D-N^9aJe*@wE;HJLQRb&*ULX`-t-aIPgFk2)_ zlq_s#?e&jS(_s`LS5nXtxmD0RNu)(7d9ey^l1dzGp%@da8XT}Lm_!CpeZRwen#CiPE=QCI zLeXlJ#=w(Ee3-8O8erU@nz?I`O4%hI`;n20r2LM|xw<;M;EQv+LgII`YDGtv22N<~ zahMv!HGcazy+B-ZeV##|9wGZb?la!Mytf%@O-tp_S(<>6T!Z8c&>-k3Sc7I!neR*~ z-=8@fjB{Wa;67)FQ?1C+*m1Sx@&(tQRP9*&wXxOFqxo;vR@_)GlZkBR>3P5G7BlZ> z$(SBn`*u$zr_zV!+;D^gDK=WQB@i<>J@L!o766K&X%o(O(r^}=>pkIs1Z0_8#pp=8 zJCeY)Cy~?hMNAV1lKs{mJ1lX;Fw5&IFkc@VH1ME(vluiz)KUT}le8Kf5eJt%u4m(sVF^ef}l>>vQ=3(}*0$XQX37$(qKD?AgYKG`6*4&(8T(~B! zEtnzZFUBuD4PR!1(Qoc?#VmzdLWY$fdm-gdWOUJV=L>aTGlbd4>UemA<6@+rlKmri z`A;p^gdbe^LI2t~Mv9+|Nu68+3k_d6|fcMX+wcr&dLj#sHf?UV|H?ucY4~j zo_G;bu9!KefwTQwnig7JDW*LbOF&CR(x|5KJEvI-WIc1Q6WhmMV{>&IGkNgyRc*^P zdo;XWVvd+s#Rk^2)WZO9t?cWHYXxxL<`2+i)1D0uNAaiPt)=rykJ+lkGp9Vt1YaxM z2Xy9_4FdX1lGQK`n>Od$yIZ0Wv&9FedUS zIhy#IH!y5Fg(Zqs1$@FtjbYHDeq!k`h*0B;ip}7W_BSd3y1RFI=(G74TP8` zz|-H?zX~!o^o?D$N zZH@F|w0d>UHp+TaAUc~?vqpN;oVd$fb}rGld;gws#20-WR1@YG<*CW3ig^2~r@><< zo~zubbMxc0gz1mBTi%CJRUQl9GI}~funif^=+F4)>-zNiP!8pCNa~_!30dlV@ji(J zaY*N+D#F)d8I21mY!QJG8{yXMo;efjX2+1i$@F)nk>h0daxv1yN0Or`_)gfYiGa~S z;F}+Ko{OIooR-6QIkR{YWEfJYKfah0<#F2I9x@oq9KCA3BXp^t^jpTyN|faK+Ir`+ zqivSW^yS73nZV^7jxgaBf+*z+Ly6X~$(hn8-D`B(zxG0gZSYRPgu&LIpl4f5J62g3 zsIW(uOsd*e#%J~&Ivms%Pq7Le$`p^o8*&p#QMRN(xLMm!%xSevrBGv zze2%0%>3hw%ic=)|1hgyCajxS0pDe52>ub6DEG^Fp789-^zCLB+}mwy7`A2WvdRg< zc~$$D=C^V-&Dt{*uYw_}|5^qAd2Nh93Cu5IPRO%K?w2!{9q#n#VvueWDT)zg&hc=y zm1Wly-x3%|GSl_K`__eoRE@riGY^w9m@1G0#wKL#qi3vV3p ze>7<&>qE7sJ&;eyUH9BQafZ{wnja~xvo^Jwo7`J(&6xq`cW3S9KJPYQD%&U_ zc7zJ(-0}^f2&!eXMxx5K6!?;ni=_g6X0M7aO`O|@THS-s{*7us!Em2(SIc2FRVno; zUhU5eZE0g^ZYy3exw=hYWm&;~1O0Lr&4s0ogzE^&h0#f~2ChsjRSCr`<39>6tCb3D znBx>|mEMvtSDp5Wy6aNxyYAK|-JDrrubdxE2)7vcooY;bk({@0$33<$Hm_0jq8-Y& zPb%z*emx{Qvwu>Ard#c;3G-#@SWql`xp%4nYp;@Kv zR)o?RHUz5n$~3iS6fnmTiN+*+@GTB-S+97h0Pa?3f%x<`!$e6=C!(cWw0ql%{w|%2 zHZ9Z(Y8^Jud$9iQ!)q*$EN^j`==x! zBV~qB2u*wi4FYinl#*b9M0Tr`wv|SKRn6;d6yHq_>;FS$Zp&UEUSQDL-9hVG4;=9~ ztAwHb3d*do?HNUa$zN0W9)ul0=7)b;ZYACx9O*-z68fy1eCV7%`j?ofyLj-|kW>1( z(Wxk@dMEbT1c-8yr3G~w4plorHY+##VqOLN;-0JnPF65vH!^^7aBopc@3|y{;Uy*K zJK{i9v#K-g>wCt^I{i3aen))OIet?f93y{~fQa<+#YV~N$zDaPJweD-f_HQ|x3gu3 z=-rlLB};A7%T_&B1=zr2gOcq&(nL;Ke)Qu=hdBiw$?peZQE|}7&W$b(U*izqi*_~zW9B`*-m$k z_UJ#5vOTye(I+X<%m4k&aa(?xki%)E5ecbhQV1z#V+^`j9&Ks7lod%cut6B5z~mE_ zX_f;MfuS?>5H)b_zE%@TEb>uFS-WMH*_nBLau#1HwDC0Gk=n|!cEO>!Cf{}iW(q~v zqbaHM{s$XRa9U#8;aXb4KVQClB?4o z@Ek)S0?W!y{DnixEmR)dDF&7KI!XmujC@4bH)=BnE;zq^U+?S6qQC8`7hedUo2VyV zcSpT+FYh3GD&O3J=z0Cy zf@!I)Std!=v}{tb_-vVmGYCMbqDc%0d=80$*HYl7C{$D__f;ae4A5wv)C1wL*nG3{5(7gde<~v;~C}UolqpKvq z?eb=)*d7hXZXo{;rZ+B>k%C6YahLi=Jj~V7DZ8&_wB+{G0~W6Q?CtyPcn7+9Z|f1f zFk3v87O2hO_9e4Yh&2Tf>}byJL;6X8tLm^c4mae;ArmA`Fa}Y;`^C>`wO4oi$?2v8BJ_TQ#Z0+bANTpfLauw1)~WeVeI4E=ZQJYf zA+Ar|b8nKQZ#Hqmu`4sjc0z3hUxlFf!+Jk+W^$owu!1)q@@R^|>diFEjmE{`0%XOCs!nIYM^*K|UvFLVlj z*Spp*`PwS7F4CsbI;l3jS7>W-qBoV)y;a}al6B`AKb5`PR=IIQoG7v6{^(1)C$HRf zJUe2B{GXe=ig+#A{=od7E8gujSX|b*;!wXe!{%PFShRG~*#T5i$?E!cR9qg_@kW_Z?65+ef0sbD@?@y zn~LT+4bM8@AK~im-B2t>W`kGzR&6NbT@K9@^cjWbP*Hm*ywNeb9A^VSI}cVwB#pw5 z7FIxGi~!OSgo?$>Stq4sJKdH+dwzk@c*iR4R0^fUmsBAl%9ec6x7sDb0$yu-l+!3l z{3OlW6Xi^F#lzH~4uRiMTwB~1V%x0`u=%0o{B||lmBvm z(Kn*gY2!)pUL_~JV{Sj}{e0ZThnpi0oPHm5V)4=kF&m=}*xuP8v+rP8G@t^1Qb|00&8qLn#ovh`@mMH40h)Oc5Od@=lG=7S#K#d~X!BB%jy` zdcVWAS-cF{Wp@L*Xx893E}XMfxb9aBda7i zR7md5JY?QafcQ~d&-IJXgTvfRG~$lnUSgWz%uN?i%f3+`NXrE?2fpr91sB-8XgAYe zI0fFqAR=}RCgItKS#RiiciNaG@!-q8 zm1lmCmP!@oGA*df-EE#)LgN1g+NuWp#T44F5_BXi2A^ycT4=1?LS4_+!fEFR!wc-K z;&pBrE=t#4-+s#Itqoxz_sCae^BqeB?R}0O({h?>)vMVG^sQ9r!f1l&@`SoALRjGz z^zlGJ-`ydz!=g4!nfh(jyIr%#e=GIhW>+VDJtZM^P=WrL8wNFaH1EFwbX4N*H z%G4Cp;Tfs08}pe+I?Y12ZaF5U6$z2aO#B5od(XX~e3DvQiL&*%8Bj+H7WQXhy>jtWncy$KRK?Sd!`? z6$Ua_70@Dsr(0LjuKWEjll4>I<8;O&;jde$MR-Ao0c-GGMyJ4 zUFVTHh&L>6IqgY0btgG(X&Ssc6hjKcYBnyUAIu|0P=Wv@ zI|Yvl00rfqQ6&6b3eYI;sU5?&TmRHm9Mj^DVL_;tu$dc z^Wh`5(N(}l8{a&w3j#R`tpa#VVi=Bx-Iy)^KEc4FC8C)3x*L2l7-n2S2XAa zl%N*D8S!7qr)|hB$9x6{(E}*!Lij-(h2trBTu}{5-?)R=kIFW3NCWoj?MgPbjr#f# z>zpYi%kg-v5@l^eil`);@qlEPCgUbZ=qv;0!8?fE_V~VaZ@|hZrGV8Opyx0XzRCgj zXJvnSa12@44?l2-B?mep8Y&sLkK`}YRB>Q-TI z-`RMaE3$duH7(oo!;Nb}ajPf$6(@@KUN$Iivw9~rrOu)KsL~ksN%0vdiauk=))-cO z{26qTEMR#qS30V)fDeRVz-olwfvr#t2dwDDpdR@%DIhryVT7hZo5U6oJ>WD=stn#2 zl%*n+$R9hl1qIzt0`n%ESrF?^0ZIc3gkm)%^PMIYW*|7f!`5GbyGxGJ!U|m^qYsrLxQ*Hw=0>L z;IiI7rNRSftsY28u)wo(6l%37?2s_}t9aTCj~njg9;b^;O1|`bmcTJs;On4o)GbD_ zRXV;Tc=YVzB~lT(^NCZEPl%FlJxscr(%)uerzKB6)O3nOuXp8b%&tv*Hono4ST)6< z;#G0EedLPIT4sv`eUhiaY85?zi$a^{)Zwm@qdS*WqRnic1U6;NuH=I|p?y`DZ$Cp3 znW<&xJI1mb+S4}%TC+t5cqW3oPVdFlS>P+dd=s)P%E6=Z@FEE0abqe4IO6D1m6?KJ zq9$*+V+RaG*ARaSIgNxl!(mV%A_m`1ZHVW%lG)Oe;CjG%MeBC6QUBH>d*2h5cUje^ zz>&H`g^$1J6?t}h{I;@ZE$ZLS3X8xGlr#PAX9@B;-IxhP3V$37bZu7254X1RYny6t zvFu;tYQ(F>ZD{}WGvFiSjdq9v@%Qf)+I52tV==r=^3_|U?K7#Pf;a}g4nx*nCvTwu zMMgF`*g_ngw`Nyh8A!wnPL|q#ip!ojfBUz4aeV3UFGk*HZrDUF{P9_TysnS4b0V1r zuNnW!qk}ay!es$7;COKOkV2L(^n4Wu+KV+PHpL~)xqJHZkEir2`|k>4olP5h`)^Dg z0`qy`0G)1@I>vTm%3qivtzSgPwyYQ4N#C;tGC4~zmSGdB8KE1`584O3ep}&u?THnx zOR>96+N@a8RDn2ek&6PZ8JC0Xn1&tDXD3S_phCO0DV@c&&;Ou(Zf1JtqM(H-39~k& zZGU9GWx7>)Ke%0buH*DI!~C{BA`13>=11|OJYmFUdpZvPU$7MXH3vOP%^4|pG3DJV zQDa2apes?w<#o#>@Wk$iSG;8Wj26TCDSB(V-Z!AViq^z|Yiu0|2zjVEaNL@6;NoaG zGK`gjelaK$>9(9+zX|jd=^26m%QNSX*vfw*&H_%ro|lpdV%QDqh465d` zBd$eg+b{U~M8l#T)eh#`qouQlIbL!wa0Xe3`NuiM5?~cRoxzOK7NKsHc)wdwcjDY9 zVg+$&?vVYa|HNk484tQM=UmpU{Q0l1=qCh|Oa!Jlh74@{1~Ok?sZfEh;8jW;?mz48 z{vLM7aHdpW=lFX5rj66vESa{thBGj`n5fv+Xwse%=kNc(Nq3m{x_5i?hCyQkjvdJn z<(1CBM|V2Xa{x|7TjXNbJT5JRz8&=zI8qmAb z>)E)i0+>;>^=AV|WB%I{A3uWi-JMbg>nx^m)BU!u@2k1LW1K)GZ;{{c%~*C#DtwE- zrm1{3>?iIkf4w+n9%=rfe$b@tUvAtvu+>x?Z;e)U% PK1+$WNgN-zsqp^*Z|C)} literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Video/tutorial/MapPanning.webm b/Mods/vcmi/Video/tutorial/MapPanning.webm new file mode 100644 index 0000000000000000000000000000000000000000..90ad7da27b5c988ebfa0970809ef4fa1b61cb54e GIT binary patch literal 122433 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⪼bMN>rj@LCkYC&~k*jexk$**4WUGQa;VrypbB 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 literal 0 HcmV?d00001 diff --git a/Mods/vcmi/Video/tutorial/MapZooming.webm b/Mods/vcmi/Video/tutorial/MapZooming.webm new file mode 100644 index 0000000000000000000000000000000000000000..8f3a0e3d369ea0ce83c8abd0b10d34d780fef489 GIT binary patch literal 115248 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.x + 200, pos.y + 200, to.getInternalSurface(), true, false, + [&]() + { + CCS->videoh->close(); + CCS->videoh->open(VideoPath::builtin(video)); + }); + redraw(); + + CIntObject::show(to); +} + +void CTutorialWindow::activate() +{ + video = "tutorial/BattleDirection"; + + CCS->videoh->open(VideoPath::builtin(video)); + CIntObject::activate(); +} + +void CTutorialWindow::deactivate() +{ + CCS->videoh->close(); +} \ No newline at end of file diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index 896351dfd..4b6e165ec 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -34,6 +34,8 @@ class CTutorialWindow : public CWindowObject std::shared_ptr labelTitle; std::shared_ptr labelInformation; + std::string video; + void close(); void next(); void previous(); @@ -41,4 +43,8 @@ class CTutorialWindow : public CWindowObject public: CTutorialWindow(const TutorialMode & m); static void openWindowFirstTime(const TutorialMode & m); + + void show(Canvas & to) override; + void activate() override; + void deactivate() override; }; diff --git a/lib/filesystem/ResourcePath.cpp b/lib/filesystem/ResourcePath.cpp index cba1fbb43..5d982c2d1 100644 --- a/lib/filesystem/ResourcePath.cpp +++ b/lib/filesystem/ResourcePath.cpp @@ -119,6 +119,7 @@ EResType EResTypeHelper::getTypeFromExtension(std::string extension) {".MJPG", EResType::VIDEO}, {".MPG", EResType::VIDEO}, {".AVI", EResType::VIDEO}, + {".WEBM", EResType::VIDEO}, {".ZIP", EResType::ARCHIVE_ZIP}, {".LOD", EResType::ARCHIVE_LOD}, {".PAC", EResType::ARCHIVE_LOD}, diff --git a/lib/filesystem/ResourcePath.h b/lib/filesystem/ResourcePath.h index baad69bbe..2a4086141 100644 --- a/lib/filesystem/ResourcePath.h +++ b/lib/filesystem/ResourcePath.h @@ -28,7 +28,7 @@ class JsonSerializeFormat; * Font: .fnt * Image: .bmp, .jpg, .pcx, .png, .tga * Sound: .wav .82m - * Video: .smk, .bik .mjpg .mpg + * Video: .smk, .bik .mjpg .mpg .webm * Music: .mp3, .ogg * Archive: .lod, .snd, .vid .pac .zip * Palette: .pal From 1bf05d43a1da5bd31d864f5328128871ee5d8f2a Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:39:50 +0100 Subject: [PATCH 1141/1248] fix video implementation error; implement paging; video loading --- Mods/vcmi/mod.json | 4 +++ client/CVideoHandler.cpp | 10 +++++-- client/windows/CTutorialWindow.cpp | 44 ++++++++++++++++++++---------- client/windows/CTutorialWindow.h | 4 +++ 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/Mods/vcmi/mod.json b/Mods/vcmi/mod.json index e1eeccc8a..cedf5bf5b 100644 --- a/Mods/vcmi/mod.json +++ b/Mods/vcmi/mod.json @@ -214,6 +214,10 @@ "SOUNDS/": [ {"type" : "dir", "path" : "/Sounds"} + ], + "VIDEO/": + [ + {"type" : "dir", "path" : "/Video"} ] } } diff --git a/client/CVideoHandler.cpp b/client/CVideoHandler.cpp index 38cecc84b..afdd19cc7 100644 --- a/client/CVideoHandler.cpp +++ b/client/CVideoHandler.cpp @@ -41,8 +41,11 @@ extern "C" { static int lodRead(void* opaque, uint8_t* buf, int size) { auto video = reinterpret_cast(opaque); + int bytes = static_cast(video->data->read(buf, size)); + if(bytes == 0) + return AVERROR_EOF; - return static_cast(video->data->read(buf, size)); + return bytes; } static si64 lodSeek(void * opaque, si64 pos, int whence) @@ -59,8 +62,11 @@ static si64 lodSeek(void * opaque, si64 pos, int whence) static int lodReadAudio(void* opaque, uint8_t* buf, int size) { auto video = reinterpret_cast(opaque); + int bytes = static_cast(video->dataAudio->read(buf, size)); + if(bytes == 0) + return AVERROR_EOF; - return static_cast(video->dataAudio->read(buf, size)); + return bytes; } static si64 lodSeekAudio(void * opaque, si64 pos, int whence) diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 53f4ad9ff..48afe2e4a 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -26,11 +26,11 @@ #include "../render/Canvas.h" CTutorialWindow::CTutorialWindow(const TutorialMode & m) - : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m } + : CWindowObject(BORDERED, ImagePath::builtin("DIBOXBCK")), mode { m }, page { 0 } { OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE; - pos = Rect(pos.x, pos.y, 540, 400); //video: 480x320 + pos = Rect(pos.x, pos.y, 380, 400); //video: 320x240 background = std::make_shared(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h)); updateShadow(); @@ -39,14 +39,25 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) addUsedEvents(LCLICK); - buttonOk = std::make_shared(Point(239, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 - buttonLeft = std::make_shared(Point(5, 177), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 - buttonRight = std::make_shared(Point(513, 177), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 + if(mode == TutorialMode::TOUCH_ADVENTUREMAP) videos = { "RightClick", "MapPanning", "MapZooming" }; + else if(mode == TutorialMode::TOUCH_BATTLE) videos = { "BattleDirection", "BattleDirectionAbort", "AbortSpell" }; - buttonLeft->block(true); + labelTitle = std::make_shared(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); + buttonOk = std::make_shared(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 + buttonLeft = std::make_shared(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 + buttonRight = std::make_shared(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 - labelTitle = std::make_shared(270, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); - labelInformation = std::make_shared(Rect(5, 50, 530, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); + setContent(); +} + +void CTutorialWindow::setContent() +{ + video = "tutorial/" + videos[page]; + + buttonLeft->block(page<1); + buttonRight->block(page>videos.size() - 2); + + labelInformation = std::make_shared(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); } void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) @@ -72,31 +83,34 @@ void CTutorialWindow::close() void CTutorialWindow::next() { - + page++; + setContent(); + deactivate(); + activate(); } void CTutorialWindow::previous() { - + page--; + setContent(); + deactivate(); + activate(); } void CTutorialWindow::show(Canvas & to) { - CCS->videoh->update(pos.x + 200, pos.y + 200, to.getInternalSurface(), true, false, + CCS->videoh->update(pos.x + 30, pos.y + 120, to.getInternalSurface(), true, false, [&]() { CCS->videoh->close(); CCS->videoh->open(VideoPath::builtin(video)); }); - redraw(); CIntObject::show(to); } void CTutorialWindow::activate() { - video = "tutorial/BattleDirection"; - CCS->videoh->open(VideoPath::builtin(video)); CIntObject::activate(); } @@ -104,4 +118,4 @@ void CTutorialWindow::activate() void CTutorialWindow::deactivate() { CCS->videoh->close(); -} \ No newline at end of file +} diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index 4b6e165ec..17c73bc50 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -35,10 +35,14 @@ class CTutorialWindow : public CWindowObject std::shared_ptr labelInformation; std::string video; + std::vector videos; + + int page; void close(); void next(); void previous(); + void setContent(); public: CTutorialWindow(const TutorialMode & m); From 514811dc89894c63862a7a42e22299bc5a201663 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 16 Nov 2023 22:42:20 +0100 Subject: [PATCH 1142/1248] line ending --- .github/workflows/github.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 955cdbe55..2e719223b 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -150,7 +150,7 @@ jobs: run: | find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \ -o -path ./osx -prune -o -type f \ - -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ + -not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \ { ! xargs -0 grep -l -z -P '\r\n'; } - name: Validate JSON From a6dd5c28873b22d1ac93c86313e9f0cd798d5883 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Thu, 16 Nov 2023 23:03:41 +0100 Subject: [PATCH 1143/1248] translation --- Mods/vcmi/config/vcmi/english.json | 8 ++++++++ client/windows/CTutorialWindow.cpp | 12 +++++++----- client/windows/CTutorialWindow.h | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 1d0432297..17e1f8063 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -172,6 +172,14 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Apply battle result", + "vcmi.tutorialWindow.title" : "Touchscreen Introduction", + "vcmi.tutorialWindow.decription.RightClick" : "Touch and hold the element on which you want to right-click. Touch the free area to close.", + "vcmi.tutorialWindow.decription.MapPanning" : "Panning with one finger to move the map.", + "vcmi.tutorialWindow.decription.MapZooming" : "Pinching with two fingers to zoom the map. Standard zoom has a snap-in effect.", + "vcmi.tutorialWindow.decription.BattleDirection" : "To attack from a defined side, swipe in the corresponding direction from which the attack is to be made.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "The attack direction gesture can be canceled if the finger is far enough away.", + "vcmi.tutorialWindow.decription.AbortSpell" : "A spell can be canceled by touching and holding.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Show Available Creatures", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Show Available Creatures}\n\nShow the number of creatures available to purchase instead of their growth in town summary (bottom-left corner of town screen).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Show Weekly Growth of Creatures", diff --git a/client/windows/CTutorialWindow.cpp b/client/windows/CTutorialWindow.cpp index 48afe2e4a..b4925b2a3 100644 --- a/client/windows/CTutorialWindow.cpp +++ b/client/windows/CTutorialWindow.cpp @@ -13,6 +13,7 @@ #include "../eventsSDL/InputHandler.h" #include "../../lib/CConfigHandler.h" #include "../../lib/CondSh.h" +#include "../../lib/CGeneralTextHandler.h" #include "../CPlayerInterface.h" #include "../CGameInfo.h" #include "../CVideoHandler.h" @@ -42,8 +43,9 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m) if(mode == TutorialMode::TOUCH_ADVENTUREMAP) videos = { "RightClick", "MapPanning", "MapZooming" }; else if(mode == TutorialMode::TOUCH_BATTLE) videos = { "BattleDirection", "BattleDirectionAbort", "AbortSpell" }; - labelTitle = std::make_shared(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, "Touchscreen Intro"); - buttonOk = std::make_shared(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::close, this), EShortcut::GLOBAL_ACCEPT); //62x28 + labelTitle = std::make_shared(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title")); + labelInformation = std::make_shared(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, ""); + buttonOk = std::make_shared(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_ACCEPT); //62x28 buttonLeft = std::make_shared(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46 buttonRight = std::make_shared(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46 @@ -57,7 +59,7 @@ void CTutorialWindow::setContent() buttonLeft->block(page<1); buttonRight->block(page>videos.size() - 2); - labelInformation = std::make_shared(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."); + labelInformation->setText(CGI->generaltexth->translate("vcmi.tutorialWindow.decription." + videos[page])); } void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) @@ -73,12 +75,12 @@ void CTutorialWindow::openWindowFirstTime(const TutorialMode & m) } } -void CTutorialWindow::close() +void CTutorialWindow::exit() { if(LOCPLINT) LOCPLINT->showingDialog->setn(false); - WindowBase::close(); + close(); } void CTutorialWindow::next() diff --git a/client/windows/CTutorialWindow.h b/client/windows/CTutorialWindow.h index 17c73bc50..d002f9aed 100644 --- a/client/windows/CTutorialWindow.h +++ b/client/windows/CTutorialWindow.h @@ -39,7 +39,7 @@ class CTutorialWindow : public CWindowObject int page; - void close(); + void exit(); void next(); void previous(); void setContent(); From 843e97349ae638d9364afcb4da0749823c3e315f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 00:48:55 +0200 Subject: [PATCH 1144/1248] Identifiers that can be added by mods are now serialized as string --- lib/bonuses/BonusCustomTypes.h | 8 +- lib/constants/EntityIdentifiers.cpp | 149 ++++++++++++----- lib/constants/EntityIdentifiers.h | 247 +++++++++------------------- lib/constants/IdentifierBase.h | 182 +++++++++++++++++++- lib/networkPacks/ObjProperty.h | 4 +- 5 files changed, 371 insertions(+), 219 deletions(-) diff --git a/lib/bonuses/BonusCustomTypes.h b/lib/bonuses/BonusCustomTypes.h index 2a2f3df48..a6a2c96e2 100644 --- a/lib/bonuses/BonusCustomTypes.h +++ b/lib/bonuses/BonusCustomTypes.h @@ -13,10 +13,10 @@ VCMI_LIB_NAMESPACE_BEGIN -class DLL_LINKAGE BonusCustomSource : public Identifier +class DLL_LINKAGE BonusCustomSource : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); @@ -24,10 +24,10 @@ public: static const BonusCustomSource undeadMoraleDebuff; // -2 }; -class DLL_LINKAGE BonusCustomSubtype : public Identifier +class DLL_LINKAGE BonusCustomSubtype : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 32bff064c..121c94827 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -123,15 +123,16 @@ namespace GameConstants si32 HeroClassID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); + return rawId.value(); } std::string HeroClassID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->heroClasses()->getByIndex(index)->getJsonKey(); } @@ -162,29 +163,50 @@ std::string CampaignScenarioID::encode(const si32 index) std::string MapObjectID::encode(int32_t index) { + if (index == -1) + return ""; return VLC->objtypeh->getObjectHandlerName(MapObjectID(index)); } si32 MapObjectID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); + return rawId.value(); +} + +std::string BoatId::encode(int32_t index) +{ + if (index == -1) + return ""; + return VLC->objtypeh->getObjectHandlerName(MapObjectID(index)); +} + +si32 BoatId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); + return rawId.value(); } si32 HeroTypeID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + if (identifier == "random") + return -2; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); + return rawId.value(); } std::string HeroTypeID::encode(const si32 index) { + if (index == -1) + return ""; + if (index == -2) + return "random"; return VLC->heroTypes()->getByIndex(index)->getJsonKey(); } @@ -205,15 +227,16 @@ const Artifact * ArtifactIDBase::toEntity(const Services * services) const si32 ArtifactID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); + return rawId.value(); } std::string ArtifactID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->artifacts()->getByIndex(index)->getJsonKey(); } @@ -224,15 +247,16 @@ std::string ArtifactID::entityType() si32 SecondarySkill::decode(const std::string& identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); + return rawId.value(); } std::string SecondarySkill::encode(const si32 index) { + if (index == -1) + return ""; return VLC->skills()->getById(SecondarySkill(index))->getJsonKey(); } @@ -263,15 +287,16 @@ const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) con si32 CreatureID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); + return rawId.value(); } std::string CreatureID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->creatures()->getById(CreatureID(index))->getJsonKey(); } @@ -312,11 +337,10 @@ const HeroType * HeroTypeID::toEntity(const Services * services) const si32 SpellID::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); + return rawId.value(); } std::string SpellID::encode(const si32 index) @@ -326,11 +350,10 @@ std::string SpellID::encode(const si32 index) si32 BattleField::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); + return rawId.value(); } std::string BattleField::encode(const si32 index) @@ -399,15 +422,17 @@ std::string PrimarySkill::entityType() si32 FactionID::decode(const std::string & identifier) { + if (identifier.empty()) + return -1; + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else - return FactionID::DEFAULT.getNum(); + return rawId.value(); } std::string FactionID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->factions()->getByIndex(index)->getJsonKey(); } @@ -423,15 +448,17 @@ const Faction * FactionID::toEntity(const Services * service) const si32 TerrainId::decode(const std::string & identifier) { - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - if(rawId) - return rawId.value(); - else + if (identifier.empty()) return static_cast(TerrainId::NONE); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return rawId.value(); } std::string TerrainId::encode(const si32 index) { + if (index == TerrainId::NONE) + return ""; return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); } @@ -440,6 +467,48 @@ std::string TerrainId::entityType() return "terrain"; } +si32 RoadId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RoadId::NO_ROAD.getNum(); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return rawId.value(); +} + +std::string RoadId::encode(const si32 index) +{ + if (index == RoadId::NO_ROAD.getNum()) + return ""; + return VLC->roadTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RoadId::entityType() +{ + return "road"; +} + +si32 RiverId::decode(const std::string & identifier) +{ + if (identifier.empty()) + return RiverId::NO_RIVER.getNum(); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return rawId.value(); +} + +std::string RiverId::encode(const si32 index) +{ + if (index == RiverId::NO_RIVER.getNum()) + return ""; + return VLC->riverTypeHandler->getByIndex(index)->getJsonKey(); +} + +std::string RiverId::entityType() +{ + return "road"; +} + const TerrainType * TerrainId::toEntity(const Services * service) const { return VLC->terrainTypeHandler->getByIndex(num); diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index 63b3f4373..f9c7466c8 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -43,153 +43,50 @@ class CSkill; class CGameInfoCallback; class CNonConstInfoCallback; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons -template -class Identifier : public IdentifierBase +class ArtifactInstanceID : public StaticIdentifier { - using BaseClass = IdentifierBase; public: - constexpr Identifier() - {} - - explicit constexpr Identifier(int32_t value): - IdentifierBase(value) - {} - - constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass & operator--() - { - --BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator--(int) - { - FinalClass ret(num); - --BaseClass::num; - return ret; - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(num); - ++BaseClass::num; - return ret; - } + using StaticIdentifier::StaticIdentifier; }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -template -class IdentifierWithEnum : public BaseClass -{ - using EnumType = typename BaseClass::Type; - - static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); -public: - constexpr EnumType toEnum() const - { - return static_cast(BaseClass::num); - } - - constexpr IdentifierWithEnum(const EnumType & enumValue) - { - BaseClass::num = static_cast(enumValue); - } - - constexpr IdentifierWithEnum(int32_t _num = -1) - { - BaseClass::num = _num; - } - - constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } - constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } - constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } - constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } - constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } - constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } - - constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } - constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } - constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } - constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } - constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } - constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } - - constexpr FinalClass & operator++() - { - ++BaseClass::num; - return static_cast(*this); - } - - constexpr FinalClass operator++(int) - { - FinalClass ret(BaseClass::num); - ++BaseClass::num; - return ret; - } -}; - -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -class ArtifactInstanceID : public Identifier +class QueryID : public StaticIdentifier { public: - using Identifier::Identifier; -}; - -class QueryID : public Identifier -{ -public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const QueryID NONE; DLL_LINKAGE static const QueryID CLIENT; }; -class BattleID : public Identifier +class BattleID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const BattleID NONE; }; -class DLL_LINKAGE ObjectInstanceID : public Identifier +class DLL_LINKAGE ObjectInstanceID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const ObjectInstanceID NONE; static si32 decode(const std::string & identifier); static std::string encode(const si32 index); }; -class HeroClassID : public Identifier +class HeroClassID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; ///json serialization helpers DLL_LINKAGE static si32 decode(const std::string & identifier); DLL_LINKAGE static std::string encode(const si32 index); static std::string entityType(); }; -class DLL_LINKAGE HeroTypeID : public Identifier +class DLL_LINKAGE HeroTypeID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; ///json serialization helpers static si32 decode(const std::string & identifier); static std::string encode(const si32 index); @@ -207,10 +104,10 @@ public: } }; -class SlotID : public Identifier +class SlotID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const SlotID COMMANDER_SLOT_PLACEHOLDER; DLL_LINKAGE static const SlotID SUMMONED_SLOT_PLACEHOLDER; /// +class DLL_LINKAGE PlayerColor : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; enum EPlayerColor { @@ -249,18 +146,18 @@ public: static std::string entityType(); }; -class TeamID : public Identifier +class TeamID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; DLL_LINKAGE static const TeamID NO_TEAM; }; -class TeleportChannelID : public Identifier +class TeleportChannelID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; }; class SecondarySkillBase : public IdentifierBase @@ -302,10 +199,10 @@ public: static_assert(GameConstants::SKILL_QUANTITY == SKILL_SIZE, "Incorrect number of skills"); }; -class SecondarySkill : public IdentifierWithEnum +class DLL_LINKAGE SecondarySkill : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; static std::string entityType(); static si32 decode(const std::string& identifier); static std::string encode(const si32 index); @@ -314,10 +211,10 @@ public: const Skill * toEntity(const Services * services) const; }; -class DLL_LINKAGE PrimarySkill : public Identifier +class DLL_LINKAGE PrimarySkill : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const PrimarySkill NONE; static const PrimarySkill ATTACK; @@ -335,10 +232,10 @@ public: static std::string entityType(); }; -class DLL_LINKAGE FactionID : public Identifier +class DLL_LINKAGE FactionID : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; static const FactionID NONE; static const FactionID DEFAULT; @@ -413,10 +310,10 @@ public: } }; -class DLL_LINKAGE BuildingID : public IdentifierWithEnum +class DLL_LINKAGE BuildingID : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; static BuildingID HALL_LEVEL(unsigned int level) { @@ -578,10 +475,10 @@ public: }; }; -class DLL_LINKAGE MapObjectID : public IdentifierWithEnum +class DLL_LINKAGE MapObjectID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; static std::string encode(int32_t index); static si32 decode(const std::string & identifier); @@ -593,14 +490,16 @@ public: } }; -class MapObjectSubID : public Identifier +class MapObjectSubID : public StaticIdentifier { public: + MapObjectID primaryIdentifier; + constexpr MapObjectSubID(const IdentifierBase & value): - Identifier(value.getNum()) + StaticIdentifier(value.getNum()) {} constexpr MapObjectSubID(int32_t value = -1): - Identifier(value) + StaticIdentifier(value) {} MapObjectSubID & operator =(int32_t value) @@ -615,6 +514,9 @@ public: return *this; } + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + // TODO: Remove constexpr operator int32_t () const { @@ -622,10 +524,13 @@ public: } }; -class DLL_LINKAGE RoadId : public Identifier +class DLL_LINKAGE RoadId : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); static const RoadId NO_ROAD; static const RoadId DIRT_ROAD; @@ -635,10 +540,13 @@ public: const RoadType * toEntity(const Services * service) const; }; -class DLL_LINKAGE RiverId : public Identifier +class DLL_LINKAGE RiverId : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); + static std::string entityType(); static const RiverId NO_RIVER; static const RiverId WATER_RIVER; @@ -658,10 +566,10 @@ public: }; }; -class EPathfindingLayer : public IdentifierWithEnum +class EPathfindingLayer : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; }; class ArtifactPositionBase : public IdentifierBase @@ -694,10 +602,10 @@ public: DLL_LINKAGE static std::string encode(const si32 index); }; -class ArtifactPosition : public IdentifierWithEnum +class ArtifactPosition : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; // TODO: Remove constexpr operator int32_t () const @@ -730,10 +638,10 @@ public: DLL_LINKAGE const Artifact * toEntity(const Services * service) const; }; -class ArtifactID : public IdentifierWithEnum +class ArtifactID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers DLL_LINKAGE static si32 decode(const std::string & identifier); @@ -773,10 +681,10 @@ public: DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const; }; -class DLL_LINKAGE CreatureID : public IdentifierWithEnum +class DLL_LINKAGE CreatureID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers static si32 decode(const std::string & identifier); @@ -892,10 +800,10 @@ public: const spells::Spell * toEntity(const spells::Service * service) const; }; -class DLL_LINKAGE SpellID : public IdentifierWithEnum +class DLL_LINKAGE SpellID : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; ///json serialization helpers static si32 decode(const std::string & identifier); @@ -904,10 +812,10 @@ public: }; class BattleFieldInfo; -class DLL_LINKAGE BattleField : public Identifier +class DLL_LINKAGE BattleField : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; static const BattleField NONE; const BattleFieldInfo * getInfo() const; @@ -916,10 +824,13 @@ public: static std::string encode(const si32 index); }; -class DLL_LINKAGE BoatId : public Identifier +class DLL_LINKAGE BoatId : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; + + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static const BoatId NONE; static const BoatId NECROPOLIS; @@ -950,29 +861,29 @@ public: }; }; -class TerrainId : public IdentifierWithEnum +class DLL_LINKAGE TerrainId : public EntityIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using EntityIdentifierWithEnum::EntityIdentifierWithEnum; - DLL_LINKAGE static si32 decode(const std::string & identifier); - DLL_LINKAGE static std::string encode(const si32 index); + static si32 decode(const std::string & identifier); + static std::string encode(const si32 index); static std::string entityType(); const TerrainType * toEntity(const Services * service) const; }; class ObstacleInfo; -class Obstacle : public Identifier +class Obstacle : public EntityIdentifier { public: - using Identifier::Identifier; + using EntityIdentifier::EntityIdentifier; DLL_LINKAGE const ObstacleInfo * getInfo() const; }; -class DLL_LINKAGE SpellSchool : public Identifier +class DLL_LINKAGE SpellSchool : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static const SpellSchool ANY; static const SpellSchool AIR; @@ -1005,10 +916,10 @@ public: }; }; -class DLL_LINKAGE GameResID : public IdentifierWithEnum +class DLL_LINKAGE GameResID : public StaticIdentifierWithEnum { public: - using IdentifierWithEnum::IdentifierWithEnum; + using StaticIdentifierWithEnum::StaticIdentifierWithEnum; static si32 decode(const std::string & identifier); static std::string encode(const si32 index); @@ -1017,7 +928,7 @@ public: static const std::array & ALL_RESOURCES(); }; -class DLL_LINKAGE BuildingTypeUniqueID : public Identifier +class DLL_LINKAGE BuildingTypeUniqueID : public StaticIdentifier { public: BuildingTypeUniqueID(FactionID faction, BuildingID building ); @@ -1028,13 +939,13 @@ public: BuildingID getBuilding() const; FactionID getFaction() const; - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; }; -class DLL_LINKAGE CampaignScenarioID : public Identifier +class DLL_LINKAGE CampaignScenarioID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static si32 decode(const std::string & identifier); static std::string encode(int32_t index); diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index 5ad0a8dd3..c72903ca0 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -42,11 +42,6 @@ public: } }; - template void serialize(Handler &h, const int version) - { - h & num; - } - constexpr void advance(int change) { num += change; @@ -62,3 +57,180 @@ public: return os << dt.num; } }; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +// Note: use template to force different type, blocking any Identifier <=> Identifier comparisons +template +class Identifier : public IdentifierBase +{ + using BaseClass = IdentifierBase; +public: + constexpr Identifier() + {} + + explicit constexpr Identifier(int32_t value): + IdentifierBase(value) + {} + + constexpr bool operator == (const Identifier & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const Identifier & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const Identifier & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const Identifier & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const Identifier & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const Identifier & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass & operator--() + { + --BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator--(int) + { + FinalClass ret(num); + --BaseClass::num; + return ret; + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class IdentifierWithEnum : public BaseClass +{ + using EnumType = typename BaseClass::Type; + + static_assert(std::is_same_v, int32_t>, "Entity Identifier must use int32_t"); +public: + constexpr EnumType toEnum() const + { + return static_cast(BaseClass::num); + } + + constexpr IdentifierWithEnum(const EnumType & enumValue) + { + BaseClass::num = static_cast(enumValue); + } + + constexpr IdentifierWithEnum(int32_t _num = -1) + { + BaseClass::num = _num; + } + + constexpr bool operator == (const EnumType & b) const { return BaseClass::num == static_cast(b); } + constexpr bool operator <= (const EnumType & b) const { return BaseClass::num <= static_cast(b); } + constexpr bool operator >= (const EnumType & b) const { return BaseClass::num >= static_cast(b); } + constexpr bool operator != (const EnumType & b) const { return BaseClass::num != static_cast(b); } + constexpr bool operator < (const EnumType & b) const { return BaseClass::num < static_cast(b); } + constexpr bool operator > (const EnumType & b) const { return BaseClass::num > static_cast(b); } + + constexpr bool operator == (const IdentifierWithEnum & b) const { return BaseClass::num == b.num; } + constexpr bool operator <= (const IdentifierWithEnum & b) const { return BaseClass::num <= b.num; } + constexpr bool operator >= (const IdentifierWithEnum & b) const { return BaseClass::num >= b.num; } + constexpr bool operator != (const IdentifierWithEnum & b) const { return BaseClass::num != b.num; } + constexpr bool operator < (const IdentifierWithEnum & b) const { return BaseClass::num < b.num; } + constexpr bool operator > (const IdentifierWithEnum & b) const { return BaseClass::num > b.num; } + + constexpr FinalClass & operator++() + { + ++BaseClass::num; + return static_cast(*this); + } + + constexpr FinalClass operator++(int) + { + FinalClass ret(BaseClass::num); + ++BaseClass::num; + return ret; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +template +class EntityIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class EntityIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + std::string value; + + if (h.saving) + value = FinalClass::encode(finalClass->num); + + h & value; + + if (!h.saving) + finalClass->num = FinalClass::decode(value); + } +}; + +template +class StaticIdentifier : public Identifier +{ +public: + using Identifier::Identifier; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +template +class StaticIdentifierWithEnum : public IdentifierWithEnum +{ +public: + using IdentifierWithEnum::IdentifierWithEnum; + + template + void serialize(Handler &h, const int version) + { + auto * finalClass = static_cast(this); + h & finalClass->num; + } +}; + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h index 15b55afba..9d40028c4 100644 --- a/lib/networkPacks/ObjProperty.h +++ b/lib/networkPacks/ObjProperty.h @@ -53,10 +53,10 @@ enum class ObjProperty : int8_t REWARD_CLEARED }; -class NumericID : public Identifier +class NumericID : public StaticIdentifier { public: - using Identifier::Identifier; + using StaticIdentifier::StaticIdentifier; static si32 decode(const std::string & identifier) { From f9e6d1467f9aa6dd7998b6654be8fb9ad6e8088c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 15:57:39 +0200 Subject: [PATCH 1145/1248] Do not select random towns as starting faction for player --- lib/CTownHandler.cpp | 2 +- lib/StartInfo.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/CTownHandler.cpp b/lib/CTownHandler.cpp index 106ac688f..153125538 100644 --- a/lib/CTownHandler.cpp +++ b/lib/CTownHandler.cpp @@ -1272,7 +1272,7 @@ std::set CTownHandler::getDefaultAllowed() const std::set CTownHandler::getAllowedFactions(bool withTown) const { - if (!withTown) + if (withTown) return getDefaultAllowed(); std::set result; diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index b9e9cfb4b..cf1a70112 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -32,7 +32,7 @@ FactionID PlayerSettings::getCastleValidated() const { if (!castle.isValid()) return FactionID(0); - if (castle.getNum() < VLC->townh->size()) + if (castle.getNum() < VLC->townh->size() && VLC->townh->objects[castle.getNum()]->town != nullptr) return castle; return FactionID(0); From f53a53051b387f97b1506e7ebbf37db3fdf66077 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 16:19:07 +0200 Subject: [PATCH 1146/1248] Fixed map startup --- lib/constants/EntityIdentifiers.cpp | 17 ++++++++++++----- lib/mapping/MapIdentifiersH3M.cpp | 3 +++ lib/serializer/CSerializer.h | 4 ++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 121c94827..acc344022 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -34,6 +34,7 @@ #include "CCreatureHandler.h"//todo: remove #include "spells/CSpellHandler.h" //todo: remove #include "CSkillHandler.h"//todo: remove +#include "mapObjectConstructors/AObjectTypeHandler.h" #include "constants/StringConstants.h" #include "CGeneralTextHandler.h" #include "TerrainHandler.h" //TODO: remove @@ -165,14 +166,14 @@ std::string MapObjectID::encode(int32_t index) { if (index == -1) return ""; - return VLC->objtypeh->getObjectHandlerName(MapObjectID(index)); + return VLC->objtypeh->getJsonKey(MapObjectID(index)); } si32 MapObjectID::decode(const std::string & identifier) { if (identifier.empty()) return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "object", identifier); return rawId.value(); } @@ -180,14 +181,14 @@ std::string BoatId::encode(int32_t index) { if (index == -1) return ""; - return VLC->objtypeh->getObjectHandlerName(MapObjectID(index)); + return VLC->objtypeh->getHandlerFor(MapObjectID::BOAT, index)->getJsonKey(); } si32 BoatId::decode(const std::string & identifier) { if (identifier.empty()) return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "objects", identifier); + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "core:boat", identifier); return rawId.value(); } @@ -345,6 +346,8 @@ si32 SpellID::decode(const std::string & identifier) std::string SpellID::encode(const si32 index) { + if (index == -1) + return ""; return VLC->spells()->getByIndex(index)->getJsonKey(); } @@ -450,6 +453,8 @@ si32 TerrainId::decode(const std::string & identifier) { if (identifier.empty()) return static_cast(TerrainId::NONE); + if (identifier == "native") + return TerrainId::NATIVE_TERRAIN; auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); return rawId.value(); @@ -459,6 +464,8 @@ std::string TerrainId::encode(const si32 index) { if (index == TerrainId::NONE) return ""; + if (index == TerrainId::NATIVE_TERRAIN) + return "native"; return VLC->terrainTypeHandler->getByIndex(index)->getJsonKey(); } @@ -506,7 +513,7 @@ std::string RiverId::encode(const si32 index) std::string RiverId::entityType() { - return "road"; + return "river"; } const TerrainType * TerrainId::toEntity(const Services * service) const diff --git a/lib/mapping/MapIdentifiersH3M.cpp b/lib/mapping/MapIdentifiersH3M.cpp index afa50c338..0f7207e0c 100644 --- a/lib/mapping/MapIdentifiersH3M.cpp +++ b/lib/mapping/MapIdentifiersH3M.cpp @@ -37,6 +37,9 @@ void MapIdentifiersH3M::loadMapping(std::map & resul void MapIdentifiersH3M::loadMapping(const JsonNode & mapping) { + if (!mapping["supported"].Bool()) + throw std::runtime_error("Unsupported map format!"); + for (auto entryFaction : mapping["buildings"].Struct()) { FactionID factionID (*VLC->identifiers()->getIdentifier(entryFaction.second.meta, "faction", entryFaction.first)); diff --git a/lib/serializer/CSerializer.h b/lib/serializer/CSerializer.h index 4bb783a9a..76a372690 100644 --- a/lib/serializer/CSerializer.h +++ b/lib/serializer/CSerializer.h @@ -14,8 +14,8 @@ VCMI_LIB_NAMESPACE_BEGIN -const ui32 SERIALIZATION_VERSION = 830; -const ui32 MINIMAL_SERIALIZATION_VERSION = 830; +const ui32 SERIALIZATION_VERSION = 831; +const ui32 MINIMAL_SERIALIZATION_VERSION = 831; const std::string SAVEGAME_MAGIC = "VCMISVG"; class CHero; From 9f906ff1d2316c719e892c69517fbd3d755eac71 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 16:39:15 +0200 Subject: [PATCH 1147/1248] Remove pointer to CGObjectInstance from map header --- AI/VCAI/Goals/Win.cpp | 14 +++++++------- lib/gameState/CGameState.cpp | 16 ++++++++-------- lib/mapObjects/CGHeroInstance.cpp | 7 ++----- lib/mapping/CMap.cpp | 18 +++++++++--------- lib/mapping/CMapHeader.cpp | 2 -- lib/mapping/CMapHeader.h | 4 ++-- lib/networkPacks/NetPacksLib.cpp | 2 +- 7 files changed, 29 insertions(+), 34 deletions(-) diff --git a/AI/VCAI/Goals/Win.cpp b/AI/VCAI/Goals/Win.cpp index f54ed6dc7..d2ab613e3 100644 --- a/AI/VCAI/Goals/Win.cpp +++ b/AI/VCAI/Goals/Win.cpp @@ -54,15 +54,15 @@ TSubgoal Win::whatToDoToAchieve() return sptr(GetArtOfType(goal.objectType.as())); case EventCondition::DESTROY: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto obj = cb->getObj(goal.object->id); + auto obj = cb->getObj(goal.objectID); if(obj) if(obj->getOwner() == ai->playerID) //we can't capture our own object return sptr(Conquer()); - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { @@ -124,13 +124,13 @@ TSubgoal Win::whatToDoToAchieve() } case EventCondition::CONTROL: { - if(goal.object) + if(goal.objectID != ObjectInstanceID::NONE) { - auto objRelations = cb->getPlayerRelations(ai->playerID, goal.object->tempOwner); + auto obj = cb->getObj(goal.objectID); - if(objRelations == PlayerRelations::ENEMIES) + if(obj && cb->getPlayerRelations(ai->playerID, obj->tempOwner) == PlayerRelations::ENEMIES) { - return sptr(VisitObj(goal.object->id.getNum())); + return sptr(VisitObj(goal.objectID.getNum())); } else { diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 2da8b4603..75158bdab 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -1411,9 +1411,9 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::HAVE_BUILDING: { - if (condition.object) // specific town + if (condition.objectID != ObjectInstanceID::NONE) // specific town { - const auto * t = dynamic_cast(condition.object); + const auto * t = getTown(condition.objectID); return (t->tempOwner == player && t->hasBuilt(condition.objectType.as())); } else // any town @@ -1428,12 +1428,12 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::DESTROY: { - if (condition.object) // mode A - destroy specific object of this type + if (condition.objectID != ObjectInstanceID::NONE) // mode A - destroy specific object of this type { - if(const auto * hero = dynamic_cast(condition.object)) + if(const auto * hero = getHero(condition.objectID)) return boost::range::find(gs->map->heroesOnMap, hero) == gs->map->heroesOnMap.end(); else - return getObj(condition.object->id) == nullptr; + return getObj(condition.objectID) == nullptr; } else { @@ -1451,9 +1451,9 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio // NOTE: cgameinfocallback specified explicitly in order to get const version const auto & team = CGameInfoCallback::getPlayerTeam(player)->players; - if (condition.object) // mode A - flag one specific object, like town + if (condition.objectID != ObjectInstanceID::NONE) // mode A - flag one specific object, like town { - return team.count(condition.object->tempOwner) != 0; + return team.count(getObjInstance(condition.objectID)->tempOwner) != 0; } else { @@ -1468,7 +1468,7 @@ bool CGameState::checkForVictory(const PlayerColor & player, const EventConditio } case EventCondition::TRANSPORT: { - const auto * t = dynamic_cast(condition.object); + const auto * t = getTown(condition.objectID); return (t->visitingHero && t->visitingHero->hasArt(condition.objectType.as())) || (t->garrisonHero && t->garrisonHero->hasArt(condition.objectType.as())); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index ad7771840..0ea8bb52d 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1793,11 +1793,8 @@ bool CGHeroInstance::isMissionCritical() const auto const & testFunctor = [&](const EventCondition & condition) { - if ((condition.condition == EventCondition::CONTROL) && condition.object) - { - const auto * hero = dynamic_cast(condition.object); - return (hero != this); - } + if ((condition.condition == EventCondition::CONTROL) && condition.objectID != ObjectInstanceID::NONE) + return (id != condition.objectID); if(condition.condition == EventCondition::IS_HUMAN) return true; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 005ebb5b2..f7f19c658 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -448,19 +448,19 @@ void CMap::checkForObjectives() case EventCondition::HAVE_BUILDING: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; break; case EventCondition::CONTROL: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, cond.objectType.as()); + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; - if (cond.object) + if (cond.objectID != ObjectInstanceID::NONE) { - const auto * town = dynamic_cast(cond.object); + const auto * town = dynamic_cast(objects[cond.objectID].get()); if (town) event.onFulfill.replaceRawString(town->getNameTranslated()); - const auto * hero = dynamic_cast(cond.object); + const auto * hero = dynamic_cast(objects[cond.objectID].get()); if (hero) event.onFulfill.replaceRawString(hero->getNameTranslated()); } @@ -468,17 +468,17 @@ void CMap::checkForObjectives() case EventCondition::DESTROY: if (isInTheMap(cond.position)) - cond.object = getObjectiveObjectFrom(cond.position, cond.objectType.as()); + cond.objectID = getObjectiveObjectFrom(cond.position, cond.objectType.as())->id; - if (cond.object) + if (cond.objectID != ObjectInstanceID::NONE) { - const auto * hero = dynamic_cast(cond.object); + const auto * hero = dynamic_cast(objects[cond.objectID].get()); if (hero) event.onFulfill.replaceRawString(hero->getNameTranslated()); } break; case EventCondition::TRANSPORT: - cond.object = getObjectiveObjectFrom(cond.position, Obj::TOWN); + cond.objectID = getObjectiveObjectFrom(cond.position, Obj::TOWN)->id; break; //break; case EventCondition::DAYS_PASSED: //break; case EventCondition::IS_HUMAN: diff --git a/lib/mapping/CMapHeader.cpp b/lib/mapping/CMapHeader.cpp index bdc3a808e..cacfc4c24 100644 --- a/lib/mapping/CMapHeader.cpp +++ b/lib/mapping/CMapHeader.cpp @@ -68,7 +68,6 @@ bool PlayerInfo::hasCustomMainHero() const } EventCondition::EventCondition(EWinLoseType condition): - object(nullptr), value(-1), position(-1, -1, -1), condition(condition) @@ -76,7 +75,6 @@ EventCondition::EventCondition(EWinLoseType condition): } EventCondition::EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position): - object(nullptr), value(value), objectType(objectType), position(position), diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index e5b34892c..4ba9d9a0a 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -120,7 +120,7 @@ struct DLL_LINKAGE EventCondition EventCondition(EWinLoseType condition = STANDARD_WIN); EventCondition(EWinLoseType condition, si32 value, TargetTypeID objectType, const int3 & position = int3(-1, -1, -1)); - const CGObjectInstance * object; // object that was at specified position or with instance name on start + ObjectInstanceID objectID; // object that was at specified position or with instance name on start si32 value; TargetTypeID objectType; std::string objectInstanceName; @@ -130,7 +130,7 @@ struct DLL_LINKAGE EventCondition template void serialize(Handler & h, const int version) { - h & object; + h & objectID; h & value; h & objectType; h & position; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index e50ce9412..1dd78cabc 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1215,7 +1215,7 @@ void RemoveObject::applyGs(CGameState *gs) { auto patcher = [&](EventCondition cond) -> EventExpression::Variant { - if (cond.object == obj) + if (cond.objectID == obj->id) { if (cond.condition == EventCondition::DESTROY) { From 70b6906e6d7e3b55ceec3a12ee6f3016e3a9e80d Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Fri, 17 Nov 2023 18:23:11 +0100 Subject: [PATCH 1148/1248] adjust messages, add radial wheel --- Mods/vcmi/Video/tutorial/RadialWheel.webm | Bin 0 -> 232438 bytes Mods/vcmi/config/vcmi/english.json | 11 ++++++----- client/windows/CTutorialWindow.cpp | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 Mods/vcmi/Video/tutorial/RadialWheel.webm diff --git a/Mods/vcmi/Video/tutorial/RadialWheel.webm b/Mods/vcmi/Video/tutorial/RadialWheel.webm new file mode 100644 index 0000000000000000000000000000000000000000..f91e87f39ddd1c3ca77137056fbb01b3a99ff187 GIT binary patch literal 232438 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

    yy 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%0DOuw)aBZ(m;+XJtBwdfn{^ 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⩔=}fMx$K=MFz~X5q@`dEQ#5`+A$4sOz9S&M9 zK#flT1$_nF2RLx<)d+aaVFkEJ7bn2Inej<%+6~4{vq^-yOXTLev8!Jf#=uJJ78F{ul%M zcIS66tPuStD**8qB%ngefW$*0kk}c@&*2tihgZ|kr9exWq_QG;^83$n!d{GKW+{VoP< zl9Idz-4C4Vl-hM3!jN+ol>@iMv(?|UXkI%y$;tx`JGUfwWRAKK@7R)E>kP0FBn+Rv z0XsA8b<}LO?TRZaa@-R)c`Tep-y(QT0x8U|CN79h8n2;aSOseO@Q1r{!qQ)DJuqxa z^ptK9&gm8M`bdFu@gJi{F4Db&$nmBNx=h8EPAhmP^0+94)grPy+p-&1xzlcx*m{FxD(WlGL z+1l`CeeVNl^S6jG1Zl;Xha&)vJ^0;7r?m`dfBBE^Gp^6_8o)2Y`W1yMg0Zx6008F5 zg3{A#LU7n+TQV2w5vh=@Kr2J&PZLi}`D7#r$PW_bwj@Fg#;4{*y!8-2)wdp8w2_Jv zaL_cs=S8Mf)I)$)nFCSbs5JrrT4ndX_#gyF>{(uGBch)(Cl=tQ1=!ddAHuOStV>UC z?|<+q%6~Al)~tn5K8%Y8763llG-^_~Y+73d_UthRMM40iaSaOY+D8}`C0F`kwq0I2 z$`%Rn4IzQU6h2-6JZm0Lx_TpUo9{hNzq$Y7UoWY01JP>tCJv3+!>ZwsO`ru}QeoHe z$7~X4h~^;t9|(X;w0gz+6Gh>j`4td{L+FV{AauOYGt15hYHC-!VE`oT3ed7aY@aUv zMLlSw8PXHM*2VM=-*V_qqe(l}47-`qm^&FB8E|>RpWI8+j~QgBL}K$lj5n9kjkO;l zB=D$2E{ISjNf>luUV**{OyT4l(JM-V0C4@@{?vhM?%=LfMGm9o!|Ht@!S9MjX1!F0cv2{2=XfW#~{b{~wlAdu*_vA5&$3w9aPM0W$9;MZCPb&_?3y>>zsYzjeb9}Kcr7OY z2d)%FK}Ut204Lg&6jUM*+qRt~gw6h@pGa7;IlM66{HFA@XH8V@?QF%ssz%qLl}@h~ zvvbTJ$pKCjw?DpX5xvcK1bTUmWQ5n8%g@nZ6V9Pf4dCL2sv*-Tv*|Ywz*IZ{@zyRT zADX8|^cc+|0ANY!qZYYMTM=ka$*2eb$xtSqT@&vDzjdcV@NqEop$KTu&KYBY`;YY& zAYc=Kw1}qhd~b~}IG(Ue2}Ukprj0K3+I5a0u%VU|LUGCtAB`*dOW4L!kM;wTeX-F_9 zR*=@G@3|aJ3hn?zr^Ol4GK&dVZmSIXhjsh#Pj)8if~<1m;)edqkMb^-i! z(P8!9PkvCXzG&&wPVYS=$#nz#TM*<6afHBwdgS%~ir}Nof~Qb-#qdT7;XaRL;0PqG zJvoUI187Hqib&)=X70;KU*SRZL{d5gSZmol!YYUVZON~9fkmaEPUYiHHW7;_f2mw` zb0V{~5Jd7&&4vq}RPD7-i&c#8uCD?JWx9qo?nwxUk!)ZT?O;ss?ex&*|OaTDzts))Xhox;JNFG%+z2L1WMCnDJEbH;)!#`3K3 zhD=b{4t$F^UX9Htpo;TivSdu-V`_j>d@WZRmB*)AWJV%&8b(+0bVoun^@?jea-IMQ zymhpXz^E<@s{r_vaDef`5vrcdMc~%DOOP&q=2JAvTz9Lx_G}?H&4cCcJkaPSZ@ebI zK9;=x*gdk5s>m2PJvkz~lDH<(EnxLsaqVcNnGFdSEx40lwPM*|`q}+O8VH~Er9XvU zrSGlWQJlvuv;6s~%W}Y+wVHHWB+^Xj~r&q4KIXtWj`w}>8VOUS)Xx2`Qrg*RULMo6-qois)x!9y&u$1&=v-T_F zw^7vfI~n<^yddTCUmP4HkX1EThl!~HmmvFhT$_>tvzk36zq@z;+KS}^z7OZ6s9x|G zCImoJLWozs6fVZZu4J^RZvFy1&iCmdu>k`bW7ANyR2EFUYDz(O#xO3fzUk`Na7790 zk!)Gdl1F@i^8U5aK8U@Nma3g>IgONpq!G`~soeV$q$=<8ZwmX8i!+^#{kM#?SG6n^ z=2CKn&MJjs96Nt)&vKDbr7MUlUTzE9v^u4`%cjTb_$CwB;vyPz=7u$KWAeCOW`WNy;g}+`Y&OfC|2xXxoC|PO|lOYB2@BthGAR7;0 z?i&qbrVDTX1`q>@f*n{(X)j=vre$47l2^UC>`PDI9}BX>XCtUBGCc^@lSY-G?r(5B zL40;A=~F8=A6jwWX@X_5MH#*dyHcxbIGIwMu!)Z z515SP=t{M5?2Bs5+viL@4@;|UPe2YMc{$HX z1&mU-3#9i5b1c6bbmh&z*E}z&^i;FftN3U$$}`3A&PpOvTguuRD~9X)!#>{c)89xF z2gy7f^DEqjnp9NQGxbG)5odxN9Ccbyp$dLsz%EbLy#z+~zGFV-Uj;0@C zAy-wqT9_C_J;AC#ZrE&CP`3WCGa79V>lSs)UJ-uZvs0xYBVAF_!R2(7YRcMp{g?jt z1+aj`Tj+jOwAQLyV!KZHodN-u1la8TC9@?+R5&(O_%D1E^f=!nvKP%O-i@5#;yspg zM2gw_sO=IWK?+u@S#0^+35S2Hdzv11A4xekMke!2S=;Q_v~XQcA;zsq`?W8#bV$Ax zj^3puVD*^W&UD0?)@H0`=@}6?!V|3>t%OAC(v*~}<23oq&XKBneynrR^J1t(qNb70 z;}QkTmE%0hzqvMAU+yX6r8z}o52J^DyZ&A~4^w=AJ~L~f=`Fl5V6aZ40d)|fL0wU0 zIO=cza1m>zFf640MnT>ppHU}!(KlPM6R>t;VQ9Vkc%_{Vl}fJMlTxLg+(*9%j`7hJ#3j%e|ZOavOsAX^6T|&JC)@E68ieLRvHf*$+QSip}3ZcD`|f1(PS6)`(R9)fh065{j-6))<&Z#QL%}X@N7i$Y}8PUoN$a6-+yvQ zbtByld78u)aA)u&>=>irIUIa`Ev)=$IgAm&oj|gDlKP)}C?Ys@cLdjsn z3WTud@10@ik`fC$;NmIB$|jm_-@55XdMFJrtm+$ISM@WreflYh2-)y{jM3@mxfef> z9s?|~7I#ODR3djIMhT9N`ZQau7pu0)_FKOcTwg+OS{Iy{WbOc(AQ9d4$cv}n$=Cax zqCdrd^A&xDJ}M?S(Giz5IsLY;KXe~AZ0jP0bl zPTm@6j!-qYSBfmo<9kUgp5HHhsBHChlVvIRz4g$X0Bm4E<>@6L5H2)8t-AOl>e-TW zJO~AZU}Pjn#gqzOB8r2PD~EA{@vEwNGyjT(($fiPQagZQjPSoNtS!$ zX)cxmXgcn-U^EY3Rq!Q%tH(ErFHuA_tjh?pHvas2Z1M3*^n60d6BHZ}fg;DTMuQ^C zk?X{ojfS4D&5pL7e2HopuCJy?}C4H)a;gSu*M(AJxd_8zXYDD6HyCJp7#_85k(<{zB*M-i}axWN`(w zt+E>y<6Hc_*4U>3Q&@ywxSUgPc@23k-s2sU>5Bmgaed_YbXM-7kF)$v8dypisv1n* zF!558<~Hm{&=wp{*ynqWhJaeX4C3BSxIA0zVVFkN9!mR zEZ45g+Zrbmw^Gk|e@%HtgkaY)I-VYYqY?+)zu`WNdE^=i zJ&mv&aGm*Ho%HTipGh4N}3HLkJ6bchR1u|&(VJEl5acvdGb3WvfpHeysT#RF2Hxx z1^-kE5SI#MOzT^TikcmaV5eVnc=+92MN1%3YTC$LAwn(p8efIxI#VdQiq6!Rq{YU7-^iT+(PPrFdX|2`<3V%X^Q$>|_ z0Y|pg&2(2KgMs2BiNObCTKj~P{`cn;U`c;CH`I&+DK58j8DLe!0f)ZT*$!_BEV15I zxSBm+Lq5`<`z=k4Oz@@A*!4#;I`30Dex4kDzjj7CTR-m3o1V6_FVN2V;f%Pg>d`qv z*ynJUUs;VA>KR~f1{W?iuu5}bSQgh>er2=Wa-o#no>Z-pa&UI7cm--*v>~RM;obWK zVr`W|N{e8DGF)7>IpLo;(Ib(i%Bg86fEoZfVD={9tu)M-;&NpWTJw(|^}peH3LFFm zNRS|NjO<}fq?oSm_8aZ9f0{fhaO`X#6iGKad!C!`mBbSki2y(d0NWD~L4uDcUjxR0 zEshvQ8YmlJ0l*yqKAZ5O(=oMwpOZX>%>SQ2Zh&O1Y6O4{BuOKt5`xF$$5D(VTR)fn z_c=y%3HR&FJQ4tBV>~WE(4a@hc`qJNPW<>}aQ1&7#DSPto+6_SbnC^d<8YG&ZzG$b z%UvJydsjiJ_xxGfG_TmWZJIqZ$3q1D7B?equr^t}Cj@i_HUm}Evw53#HT-i#zUODG ztb^h`Vy4@QRCVnmlUT030?pfb0Fb}fnj~l;O#yD0*tyQ=c?mcsctU%LG^P*ZK1Gay zkfAzFjYt4P!2y*%*j4{3r2-Bb@Mv4O9;o_K)&cGpc`fs9TEPAJvIPP13ho0U3k=Hs zES{*2CTZo@0FI$G>|F5G@~0xr^wdt=cGDpS7c(vm+Kio~SoqBW9c0iTr5KXiZI zSKk+J_n*h>wx!80WtR=rHR|Q;Ses`v>f5nvT>CpG-dHKs=p;FN?H0r+VqoH74kKKr zlRNZ~HY%;g;Vii%UsO7oGV%QTMA__(Hq*1E)C+fbB$H-O9Ic?*mx~?6=FeL;4)q%D z8@+#x5s_Y4x3qX|3W1DF0-q)dlybi-Jb3~&tJhIVHQ5{;nss(~_es3bCWFb@qDC0X1M%4 zwc=55qw>T-q-B==3c6S;M${PWR zmJu^6xdfYd(HcL5c=z)k*o!IjSkjfvlG@iT%uhdy!c2!tlLY<*8sG68Fp1;T*2uy6 zU;t3C@(Kmi50Os17cKt_-sUgpTSUk0x=p5aV2+1a;h4|OD}SOY9;Zav0i;Lbe&Co= zHMTRtVb=IF8ITdUR#(RDm+^|u=OaUYsksMzj(1Kxavpt3u8lgLG5st=6^|C0vm0a* z*FkAc^j8sC{;86};wUIw#PW&SKWr{q2jlJc{c%Qr$&@m+_9%y-{ z^}4VPfP_Cl5%c`K{aF-a0zlq+N9|o7iHN%uZvHkRYx@>%Y_NFQd%y2T%!6{Bl%mNr zfOX!08>$dl*Xwos)sl*J~k06A;%27lLz zPx@miiRbV#778$76Lw46i;Zm)>h* z2GTn5-~?sR$W}n(-dM(IDe;k$pcM7UmdX1kUVy?NK=aYD3!d6APLBY^H8}>%z5LZG z#QJF^%viWZ<&T-0F@lMpLwK~lis*-z@_;);&(I6f);=txS6~kt`}$((V@1a4v){gE zPWcWx1>WS=yag2%QR68jPcjP&=Tf!VRuu%Vy`Ik{W?>`C_ULzblXngRS{YLA4NpnW z83?4*UrqQ%Cvy>=J#JiZe;?$MnvIRl>pfAnX(%kXv)3bE8Kze6O5yl>bXq|-;H*lz z&&_4}1ObeKraKGXEW+ExWB{h5PmjTyr?z0Og6{dB&`Num$0#h)CqKNuW*Z+&)5*i8v$a; z0CY34F%A*Qc4fig{LNXc_58grY)@WfR+roYQCRA@2tbGcOBKm@VT0jGeZR=6hT|*( z%GkGuzD|G?SRsPr-wLNGD6huoPCvQmJ<^1h_<>+}g`sSRYKIM3+iqS@Yf)yyQ%-uM zr360?6m0;?3wBuP7zMZuG?C30t(jmH7m8!h9Tp9MF*KaQ@0cC##a2mQaM0Cz1#L;9 zK38VlM#^hv(uU{4v1zRbb;-+B<6<@f!X{jJ(r?=>h35$3F$P{p(k}mS1WRj{Exz`B zeWPu2U8mEzB>6(7a;ik0oJO?D@#Bf!yVE|!`;qOQqzx>m_6j8vHG2yl(hGhaJ_D~t z#!mOEnTzgxOHNl^*h86SW!vY;qToa{3?vaXE}hXT#U+Y<&FgUaB>sDG$M(pG(}?wr znTgr%dwy>S8$#?sbC2(WQLb6WL5huyop&)xXf_YCtiIPu&YQIzs%`4NhRhy{D^w|; z%44V=tWG#qkiH2ob4#%|ej=k?cWPTiiHf$~oBtRSllQAj{N=ICT&*$TYn;zV zYCbv*(`Lgb0ex77YldHEOBS}@_T}o7Lsx>T=I^#Gu*=i}GL#{KfkF2M7T!J&%XQ~|KjYGF& zv&=acR>|LMqZ2}~@|W^0Rp6!*VV>miK!7JY<)p(5UaPh&6OqlL`rH?{hqq8g47!}g zz_zp}@`b$J4Ak*K;E|{QmSm?RHJMuJ_o(g8yKe*z7Q>AAKk8|1_(bF8Bt+>QXOeJi z^5!ywf2Qvz6u7-Rqfn-@M|P_#zcC;)okvcn=%52=dl8)9iU8`S5F^2AIO}<*m|Ag^ zU0|_=n_5WgzMi&jC8sye#UFKop*|&3gmp$1X`M3loaa5Wlotp$fQ(UOJ3XJi-qx?_ zq5rJ4&9f{=?dM58AXbmuj+i9f&t*)m)MZcVUfCN z&dK+clrgIrK{=P81M@jfSZhwE*??ig>YyyColeqlyjV-(^~;8W3r?<6_jEIuNRiow z6Cb1tTP&d5-h{+Q@dMf1(J2dGcv&xE6^F7h=2`pI$-F+(+?iuWvX?q5bg46FU)(k= zNPcKnL7Wu^$lMVo^bR$3h5!~P6>D?os9yoS)fK0Di3JN&YV6oUs23NqH8AKie0@*i zB}A5X-f(is+b)wVLJ#E*iE3n1h;4(bLnhWi&Yc1?qXr!hdWhdAD3HUrym0-Dc7R{d zmhR6bF5UK5S{>iqb=w=J(0N9f1nH`BMCgZfDF)NZ?qdq!n(Y%kvZqujEXPJUb2mLM zo~Z;A6?w1JYBaw9jE96KV>cg8Oqv^K0}`oR9~xH#sy6)wX&F_NE$XP>m)IKX#nrpqP;qpWQ-5HShME2~ zkH~RqzMK=!&~bkgwAO`wumiiCGBM8K-tE%`lRD{B8MzPnV&dbDJ!3)v#)9|YtcbbT z6S4qLf;CfJ5g~gEnMtqG_jDfNCaH=IKV(cv*7(qjf;u=-xFq5a9bV#R=5vP2dxa!a zcKVkKhTIVcD`~wMFiJPrPQo2vbV~V+^HX1l&?$W}Vt)CTY^KTYM=Wry7G<3~A`;rc zM{*P(zI4hn?!S0{2$9u+ZUd~HB_%D)A11@<2lV{eQE%nq?{o3oLA@Dp{3o$i$v})-KMGgx%&w+ zq7|0XK4Hk<~j>jIEiVmcJiVJpr}K-6zEO_woE=3HNrP&hkh|R5B9Ba9CplV_j!mE zn@o$>h~~0r@!?i>I)3q)hL?MJ`oS|$v=mr`PUq~+y`FyP?}uS)GrS}Gz*k<$a)0jF zO3ZPUN2U;w(8Z#{DTG|kD2R8TfBJEFIg-yuU&WQ9(@Z!^KrW;ZBtXUGUK?-c=r^(! z=?i;Zd5`PIP*T=W($hgxU6n376)dtqz1MXDwb{*A!O{8 z-d8+h`B9;&YbfDddO}^+b;K%R$t}6dSYF!#Jxik9Gy%AzKgem9&7a+*;6m-I5X+764g{Gj998O_L zevfx|J&p)9KjU4u3Y)kQ{=iNmJBgs)`$KvYSS zj4UccQ81*cG=1=l*BXk*tONwwUGs)53eFsELi%TGX+EQ6c!xr)X;n$n5FzPjSE$)K zlND71-+mqw+|djeMli9mJ%aDaXgOs5p$Hn&aq#(G>EJ7}3-s8pRjINtn>T+{~1u>0HV z6|EWnYoU#fnfju%s*v---EK{#u;)C(?>Xsm07vZ(#rR z#oevWLFg%uR<}N0fXy2g#1j}^`=kDH9Oe*W?B31ZWrQBC9&a)!fG_T%0_k5|x;8Q^ z_4bKwT*7lXG47Eto+az<%cSr2Vp_rpA*qD{pw*mLXMWqH!u^{=&y8$OXh>i3Xg&K(Xp-f_>_keURR9VWs4uU$Kh> zh|<3VbX=^jlIZLqh1BeqLr5L{?6B$Z95dmfXKS;mWoK(2*Ip*5|I3je|Km@`ojkaA z8!S>c1iuv6xfi(m`SgZ1MfA<~I8g+!ne`8hjw34k{C4c1t-Oc|zvZ#9uI7XCn?9z0cq~a}qr$q%l8$SsVEp!nI<5hGvcRNA? z9GfRpK+wG}UVM~>U92M5^L}l8hVkl-p@Y`uq-pAE6+aAq!vVslb#gN24uYdUE^Dmx z4=V3ZiiLRN$32jcYHw8N!{_~>R51TCNd8eC0I;74;?1N}+NXmIv4;%d4}ah&v1nL)QJ^c}t zq~oR-)is?iy>VRV<-QOPsdcCzQyG8_2S_)(+Qy64Go6`JVt=4ZsZX7YJ&0BH$D}4= zY}!3iZTC65(3tE<^7u!gIu&QNH9eX?uVZd&PpWdii2kNd9Q_1P zU|@l>k`x30BJKhU z#`OG?6x-MDEhVWc1*DRt&q^^sp!$^e#>Z1k4D0xY4=mn=+{n<|UF00}ulf`2HN>Wl zz9aW0YzzM;8Fc&z$-GSpfa&9$1p%&T)`iHskOGxwQXgo4mRx$f?Fr!aJ$_ZCAp20| z%VPjs1VAz?i{pNkLv{~UlP>c|A;Mbos7kDlmM9!gOsV%(Pk%Z3wN_Z`9bz;ll^8?Q-O;e-c3?7XHc~C?ZO@z`$V)mxc-2d?*#5Acu+%`B@DnTb!4D6Qtvu z?W(GR_x|d@zyJxX&(Z`x`L2;wJF$q=8 ze~Q01Pud#ONfL)-g62k>SbpYQObbo$QX1}Gb4Uga=WeQdd9C&3bwcH~6gVVtmzF4u zBI9Lag}Q(inPWSaPl2f{yI=K?W@Ihw;{8AuwzvQiQvnFLHKRnbDiBNFMR1Fgt5_Z= zW&TykmlFp>4r-bcNS0>Cw?0b|$EILv*-D~~DecT1r8c-xT2qmCOdqR>-~4@R(JT{= z!>qE&hElgn2EZ5c-^L`WXz|}!9_fNkFeO-nWk+ryj7S{wp{|v5!GMgMV=Xr<9|;9- z(sojOIRNAhxO0{XeNN1LCN%yWpfD6McqZDz^Rmuf&1k)!>_f^+Je4;4dq$PhHf7xW zInVmL3!4~!4*Ndj(6lZc=Bot?iO>3e8qe^A2%gX7CdVnh_ymdP?D+doi_rES%klow z4Aom)i2G(i;$~m=y+nl0JCQX23kmRW9BI>Y(=J_#d0(*DHXGFBYWBU%QRy_{=SMSH zKVvZqiOP?_f#qIG7=4=($@@H>AA$|#}uRQYazS&2M+R|1Cn)O(}KwN z2Q9N#{2Zdgi-2l}>8J7xiqR&JRY{`T`-a_z16ZF*on(qkB}?O!5JYx;l@jedtN`#~ zA7p=_KBQ*S*!b~1yw#^GT;#!Fiu2i*ID1Yi9<3L4%<5mxXNmO-wTcKq|tD@g6kL z?c&?@(WQoIC``?wPjjGe+INIsfdv`z!@Oe8XVyA6*g~Q!!y@_0KDYw-3yxa~rz|i$ zv57Pqa|yi>L4hHnKCZFwJ4*bFyhp%M*w^RLo!S;=_I~ZPuS3HUw2N8_s>}ePr^E|@ zZ`Q3e^&n1!%)`c8Il5h-p^4N*byfkOG@b zI8HLsv3hpLBEnIdAUYzpD4#dNJAkqzG~6X2Inxvut(#D zc4kC-c%rXp{ve^H75L7()%Fh%K#!`!0aL;GjbJWkyl2ex(Nx&5P4Ou85Xqw2hJP%juyucYn&ynNkoAl*AFn4fdW6{;JXH#2Z*f$9yzxM z;cy1f8}}|#rvCmWB6t&D_)1)2mlT{v^w|-!&SYZg@VauK(wyji`2cq1*R=1o9_9@S zhA%YSDP-uW?MJn%h-`E{_?$kyf zwvE~RC_sGHVr)12-0zJqmt}#F+_81K4vkJNLUBn_^Wlx~wu7#}<7dsu{&t z8O4nHgneH#Vp`wp9}+erpTyD-$UmJESB=PR7!0h*8nf`SkxXekV>6W3x}tZNKq3=L z=ct9*n*kl40;$kj#)fe%o^JDPREh>o(r_D85>iU$ONPkML<8mA<`jxb zqFfq;q>^OHMHCqdMI@I(NJ7Yz-@3iu&*$y)JfH9L{k?ww{7$dV*=O&4&t7}&wbovH z?X~yTx^&`A*ak7k-zVDMVjgez?y zFWyQF{T>mm)0pXa`olW`B`)DoMA&Q9t1EZv=1mEF`Fna@f{E&)i(UDbmUwh5(tOx# z6(U*j!mx7X8mpqMO?FxD!&XhW43fFWG}!M`4{5F!&|`P`_08LqbzY96$Wu>ydBVh) zB5_r%B$yLzzSZzX`7dT-%ypKxo!{b}dtaplWCjGAmv~f|_jNM@SPDAbs~8*3lJNI+ zcH>?2Gj#Xo!bFgh1F9k$v{r>ql{eAk51*+9z z`AbAIl2fbKA5ye^eNpes>XDf3U4rQk!oydE)`~L~);juq600ne53o(Ic`JS@+}P4< z%D68*mnAd zWFDy2PJe!DeZH(A?7B5Kp^f7~(EwPTWSV^WAwW&i+%s822m>r)qxL{0?~fyIs@)U- z)EmKCQF)-~Wug8h)jlzHne`A*thjTgUD70#JY?}F5FX9~$4-mGu1z~YCN5hL*38!y zn@*9(sYo?_Hc>=Uni5r6K}hEYkg6-*LS%b2#Q#eSm`%eKCX}b zp(o)p(Rft&;gi4zkszRCsmsC*0JL5_$~{iGiyNscu`!uo509x^AR;-UGK6Q<4PUb3 z1JzWM2M%=Z_J#fq<^(#)Okn|=FU*dMg~o%dlo?4(Dqnl|Y8ub9x>YySO@c#9wR`WS zlWo#R3nt8vsG-ayswk=iq9xXA^ zn8ZyrJayQ#bfOarfZKAnLeP<1RY_nZ0MKj;KQ#p2$st$@q=NKkw3E^y*%%*qWW#bnZlJ0(XrT4TKA6g&TIzihAzX3|;186qc`43VQ zfKEbA)64C}cC0`)jSaL?hjcOHX%!pKU?UJw-UFg%LD}2~XinxhQNF+^49Q?`m8(VL zH2hA9Qu^Fk_hGJwz&;(EszHX$CKN&R0W9Lfb=<~iZ>ffPN;chIK4N7{qlR%SSgQfO@37cS= z?_!~k8w)Y{kuc_M8v$CmDMM%AzTW0<6L}jKaRoA>_*09>0zxh2kRnhU1^Oh8KtzFv zRLR2Azd4wy8$~Skpd-Mi2|5Mem7_(=nfS!bTMl#4qd7+cXhNxyh_)M z%&smwJ6`(oqLmIu#Wz%M`k1L@xjgUJGCi|u#Y|q(4TjdfqlZ3r?z5@odun(`&MunK z5)d{sT||4{nYjfIcFle)_M^Y&?!-y44_LtfNEU7Y&AUVR=x_gIu~-c9pIsoEhz8=O z%q1QUBewwb;qL4f3rki<(HAucyr+_M_4df74bP>rL2nLqp*BgET!?r}2GtUl>?=Na zrB9M<#-GxI_8m1O5n+1z97Jgu8vxiLj1kbXPsjF9X+&oSPWsYuuFDrIe@QYRValah z<#MiTqQGPZL!J9`b3W6$HcRbg_j|n?>;j7iLt+v4PA_Q=McWApl25~hpMIGIhWM^Z zf`Ct7#|C3u%%U-$%1y zwo4xZR-0rBqQRiHO>81=H*+3VLQ-SHLuld4Y$1}`*ox;da}h3Wr^HI)I`h$O*EmO zu9mreyZC91EOU*_xzASSv}}A?f9u ziOy-N(_`COH22h}HycGY)=LC~B7hy1jfC-QEpj9OV!Sv-?Sgw1)|dq+%i}hS`hMsO zdwR$srL>hjZ9|pUSHA0~&GWV?rQ!CKbuxR?xd#)yvx)N_Lu}?15`1A951~b%%DAu% z!WqE$3#c{(%Ib%)+g#&IF;aWaT(7cp@-?Pj&p6E~5MA-Ds<7<))NGw~HP+sCw>j!# z8y3+}j)DYexRDgIExIrK)QEdq>VvFrJkl#Cu>{6F$7M~wh6DE0OnG6qi4j?%YI$KC z_*NSVaoT6w_yMW z^6y_UNFY$2%pWgxYMU1TCjE28IPGwmVh|)>{}f`(+?>yxH_ZZxVR~BDHeWfGPHb|I zgth8Mj{_&o_M3;BljU*O53<4(C^T&kLFcj!e62L@!on~H@~%wz*`w^gKl?Yl3Pd)s zkRqg$ z%L#0mxk$k=?#PPzg`5<#Gtz$-gAYQr2`ahY3uLtNvPppPHQvL5!JYjh3 za@WUg!2*aZ0Zku7{p+xoqLCCg0o_p5dvEJEESdxJXY(nX3nPge!jNiQe7Mj6vuR^2 zr*YY8X(jQsm?_4`Z43I$e|4Pt*w-Vu;?1=U^r+nB05M~XY%*5T|8o1+)Y`xDjQ>~Y zzXxRgi4Yb16aEh&5fUTm?7vbJIDb9=ywe{3@kZhO@$w&n75~KP{uBK7kRJK>`~L;9 zv0TDRm$(dY;SR`Qw~fL=v#k7Ucum{f_ILIdD-~X+E!+EPk-L~`@XJq26g!x^vIZ(0 z_l!=A{rTc^qO>OUcwpy?6&WY}DAq4V-@fPxlie~ZU;N`rV{a0(*Zfn=(Rn_HB=l_tMd$7VZVKnVAbHme|%gloKERnJjG8`awtn;On{=h8~ z4j_o4?}ht-$r}*+hSmf9wK6VO+#)dt**3=ZQp#){HXEdal%)A!&w{sCVe;}Ax18lm zo)_INUgE{3Ws(xrv?0{vK$OoG`6}yQBg zqX3Sy>EhSa1@jtpqe-8x5+lkCK*rBBgEW-w6Ut_FKG&CjfrKO4 zThzpQ!0Oov=?aPYSi3~Z1L6y43OU4IG>gn5Q(V_9YXY$rr!dfz_4dH{UE^^j=9N9j ziTHxLX1jzr3gi}IcLS1|-4H-zBS>{a`?NkxwG;Nh*VsP#Zlb^%cTm{e$GGqkEab3n zZ~!gCqdd!|6;&LeqW<_0}& z|8-1^HX1yeyDZGT^H#~U%i6L_Y<}{^yAe;JS@!0WapU|#^#{S*J*hfRRp${9VMf^e%q_bs3b5C3g6A(usgbh+G z5Eo#-$&)Am+Cze18D6GQ;sV(yyMSthKP3cTnApQ`TuB*W6Y9HIx*vNlnoVr$((*~+o8Z|B~AqqX#1gYn%X4;K{Ii>1Cx$($3E zKI^;R^^D|Wn@95wFpm{w#NC92_A&nLwVy^m>XHEiH6*r05-Q>dW+7R7{H2;N<8ctw zqzSb(p%yl4X_Me53h$3-`brlD>2yU<+xO1F@V`xEtMwKc~-GU7!pk zh=ftD97P;pe+)7)IMgIV+-!i-NrDpx2@GK#6iFyYs)!;?epR+XI5dwqPr!IEbK zJ0L0fcb4tGx7kMqPjXmg!_O^O)V~8qt)mB&GD>$H;|Ix_*5NQJh%ep( zL@taIP-;=f)svOU#`Xj$9+DKb5JE{%Wg`qN3T3Fw0lO$0s60FXoiTfx53aHnZxI{9 z=ETc_sK>_AXJ~*xd?Bm=;!zDrSVN|m9b^P3r{kbXR;(Xpu!Y>TjEh4uJpGG~+{6T} zAcI-2yIu}X)gCUj>}cJTQ|r3Tn#0*$X%ZN5XYH(*5fh5@44GJ;PCu)}qbJ(cs|v$@ z6%WQJO;pvN@Jnu=ZT$s9H~%<1m1C9X(amY1^{wIc;EBhapa4O*h)+r!P#Mx~6M4-i zf~A#mx?wyUaY(enmRmmoyGo)2K1+nnK1>u)#1S`Alx{Gwekg1Ni8d+4btD$Gew+6F_9crn-V0@)6%Te#3z5P>7g(`C z!B!Z1(z5PAB2a)ArpY6 zufd-FK=+*@MLzo{8RUrBhxwh5UW`kD@~6aJ38^ZN*;zf1 z>Skj*YnS=FO?x79dau@B8Q8OpEZP-aAoJ~Kq;R~4eS;PPnL}BL@3Io5~|n@MSj}eSW%^Ga%_X?0nOC$ z>!d5|yw)w;I{jWyxP_YMU;=nX6dgcvA5qUJ=?FuU{TDDvf6K?V$RSdcOH6K6DA+01LL(ouhy;+{hyx_Tl3oY zHLPctIG3?Lv8v|_+!{HQMWx8eI)~+0M0l+)b+$RkFXW>Ls*>RYZrM5XXn7Mhz(;QY zYxWc#b>ZbI>IawtDjj?0;4U0%Ey}5yvfm z8N-dFMkK%qj^-waB4MVCK7YHw?kq%a0kO)d2)+ADK2ZoFo*-q!2Z`2x1&0X&Sq?Wh zbT@^}A_;|3L{I9}Y5++Z=w5oY>?iWcT3GwIVqARUAe-UHrdUUipyq?SRckFR6ZH%o zi3nL($BF?-IywQoe2c@S8(bm?a&S>D@_|N^Z_wn6HiDVv=5J$^B_8cn_bHvOw(->j zJ+Wbfk%?7Dm{G5?E`AM}S>FS+-nNnmW3vF+%@deA$N8Q-v}Z$aB3taH-@BlT><{*{ zFMJN=^#a@*fMGKv?XfHM@8^GdFoLXZkpG?_-hoo$f4K+^ zp9hMA|8Xrf|0_z|_s=9n{$Fo&t3P8=YRUim`&)2`hx>Nou61BWPu77{S1Ue6I|NSEVt!9`?>F1B-q5oC&|M3;b zWsjx**NtKHU!lLB6WGFl#Mm7J;~oiL|GtD>&40U;$bXBNefA0a-y)DnP!9fc{g2@m zwRkL%&ik)CC9bVpVgp#{3CI4Z+1z#it-Pqn@vh3&H%l&mn0ocmI;sBmuPM_ft))2= zpNQML_U?boeO~Crh<<-e#5SUA=DvIWS!v&G)n?O^sh{P@AzIr3D{JM^dplOTD$S58 zwmnW4J8^OQ_Rk~3mlY>=`i2%ASAXDgr*@v`SMHW2S2MYGdz_DuKU0+}iQVi+c zG3l0(nXJZRX6GGO52_c9Bp!GvD%IYjyfRx%SgC`&hw z*)LFK3McYOpvWTjZUXEbLn4y|U0!mw1{)w(ibX?8K)DYJ9Hb6mSJDIqBuF5?(0KTU zy^>Rx#9zW$gd|WDVP`WWyb~ibc+0+xa;YWf1Bv-(TXB+OFhz<*U6xfo2Zo45J`(&r zq8(Eyp18BKt`teBz;K6WfEA;R!7zqxoErcgYNaq$+{nzB7My?~#%dreX^Yty zs=`-K61&^|Tp|_PRsW)9P-h$87W>{3c%?biBWPt~YO{84?17AT8wyR|*Ht^&>up?n zZ`KjI?B$t86H8>6-Jf>_J>Ak}7#bSW^mv?Q^SJXtFB(Dvhys=MzJ_t~zJ7V1r=PU7 z)7^8~r+!`A`N{dSpG`fG{mJn2^)pH<_f|HwoL&(;3I?N(zO1|c<&C1Q%?V1W+r4Wm zm+?H`NfoKbbVinKb=x%AKkGn9<`(-mPvs@)?vr+=-sjG=yYIuudVK#JKeGYG4L-`g zY_5~pe6ralL*M1Gr?@#70R#yxfw)N#UXl+;A(b4K6~2HPCK6LIKut#@89v1uP*GG()mHfYM4*!Zq0#1&grz1F^rvPy`rb9AV7Is0tFtQ5eub zWt|)=RYaB&g9(HYVG2YXhCl`jptKP-!Z1iFk|F|pVvI6d_)Nxsi59VHF&cu7q!1?5 zhURe5R^6`hck`%KkDvIgf9VCs>wB*n9FOqa#8CNmLr*R_Y0|r4cs^tC*~wqH<*Su?W*uBxaPPqd z19|SP6IP0s44a&LJeWCh_7|9b_n(R?HXL7CeEdqfSD=JV@FkZLf6}qMC!7$KtE1a{ zMj7wDbz;jJ{u5sEQ4@nN$@6AE)pUQor}f4|&3C_7*VycIJ6q~zmb0^r)Ei^qaab#) zz#w2yAccc3gtsLhtobkwTf}v3Hbw|AR-VKgy-g?*Bv=>(5gw+T1~E32Qc;S4_#t3& z&~|}AFj&C@qQ*`blw6dGcP$R8N__fH5EI2W!YgnRAE=oA7uH1tFqemDmEjU~P=m`z zg|>u1L7Dr8N2eLz&V_D#9Cj>ohIHVqlhsCY1v6?c71y0>Zobl;cKJzp-;n+%kG=af zcM+NAk`^p`e)j-naKXu|b<5kkkNCd)$m@$?shi(5kPnMn5TW;Gb8EG_bi9^h%L80A z(5Vq+_q_M$==OJ+1qRgVil-%;Mq4x_obT%>w?ulVp0BC;=JeS0^vAUwA{lqMuNYfX zy8S<&^F4c zaWsZQ6{>KkBH5eqaL3SOmxD*54)r?`{Dw`9r`GK2Pv)lFWGJZ7CdNsm2&tn|q!}&=N1q|FLO8qR*0+XRQn? zUDEJ$`Lt70J4@oGEU4JH_5Q#a6N#G?lbknp9799qIhM;B`P82ksVg2y2j)%-yAtf#puSNekM;O%m28Eyh+hRtQKUm-q%XVq;qFUw~A2 zrn;=Tshl)q;+ZqFr&(5mdCcaN>fq5yTRT0M%osUb@O`71@_xf5^(8)y^9nnD9r-$S z&FvJOT7S#)^Be1ZmCM(Y2nJ&9FEFym5%J~15=)y%>F0Y1d942*fTUzW$yY9le+x03 zY#c*|xe?vmd0|U|$M}w~7I|-_H>{nTld5uRSLoN>BFwl6v7EleHll&7--eQFpI$q+ zOvclSUH9aRq48d~nC4Pyw%QF~{DFo`~68WhBY&s*q-iEUo_!CX1S?;PT{dZhlg@IRi>%ew8>vzS#yIUgrYsPvGSJE;+7v{Tg5whwy;`0m&Uj^v z?@|~AiJe@vaZHN3tW*nn__~d-=iFJqqhBKuY_r^qOGzS-EpgJ+hfieDcaxTnQP6aS zlk#gW!-~$yApU~{;~T&{yLyO4>_WBx#!Ohv21a3nzUj0&5^h8kJOue)!p2VGQk)%uUnrw4|aE$N&_^9Y%z%WPp?4j{9^F9%!jGsk$qlq4AG~RM%X@Ob#wRlsvdL;vy zTRj7A+TIZZ13TwFJ*lbw*6obliHo;|?JS8Z2J-K`3gV4KI(W~cK?D22EGWR~8A8f{ZtrDre&0PGL`jJ#>P;8v*r|Fw+xLqHYR#AU0ZQANPGGbNU z&Gls!T4Lw@FY^?~kdB0iJ3jKwLqKXW3qTJ43B(s(Q6y0NF;U-&jc>|qp+;>-V)9xH zz@%L=ys-&U>^3oyA&bfty1aroONhn~Tr_Itpc!Gz45daI#D=GDD=Q7E+P&O!xrUo~ z^O>SJzgLU4KN;O5H^b0i!}cUeXKFa7c3Eq*>VY9F8sLD`JjDJ9DZU=$6gY=#3){fL}LScznyRtR@a z0HR4`XsilAAVP#y)?8=@lx#33iK|$rJ6t?4uw7WEF}YJ=Xuouaq)NAUdm5#M_O$Qa zuL}LRes6y(*DSkdwtM<+qy!yHTv)Nfbm{)Rx#v4RQp9QV-`3pPIIs2TsW-(EO@=3& zoX^k)s*ZT>@HjivAQF9jV*-D=^Y#P7e9xBhbEA?-AO5 zr<{0sZeq`{PWA9EY301#TO`aDEcTqpigDRC*FOG&bK6YXG-v5J-!m0g>P#EYyYn7R zT@;a(w_JTkr~Vvy*DZ^eINDxX^0>rclVjZePraV6F67VmpouC*S0_C<)hwp4I(mPS zEJCdGS3jDDPCbM%Ni9H_DF!b%peewRB+HgWc4Y&hgbYhyC8;RKP+EcvCday3<6P1h z4+?vHN{i6jO;|xK8(E3K*;YIE*gf30^!X+CbGEEdr5TH8INYh&vI zxg8G5nS7)3EnVLzW}fqAer4E;{NH&lD=R9XnK!;x?Qv(!GRe`$b!+Rx?;jJX&dY4=Z4Z5MaPzF% zzVxZ*OG^_x?ek{t+cRPDQTyEL44>3xt`lbq)c_

    ;)-rITLwQq3AR5A+*CtieVI6 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, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title")); From c872f8418f0fb05cf8059b72d637dc2992a6dea4 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 21:18:34 +0200 Subject: [PATCH 1149/1248] Implemented serialization of MapObjectSubID, refactoring of related code --- lib/CPlayerState.h | 22 ++++++++++++++++++++++ lib/constants/EntityIdentifiers.cpp | 29 +++++++++++++++++++++++++++++ lib/constants/EntityIdentifiers.h | 26 +++++++++++++++++++------- lib/gameState/CGameState.cpp | 5 ----- lib/mapObjects/CGObjectInstance.h | 2 +- lib/mapObjects/CQuest.cpp | 26 +++++++------------------- lib/mapObjects/CQuest.h | 7 ------- lib/mapObjects/MiscObjects.cpp | 24 +++++++++++------------- lib/mapObjects/MiscObjects.h | 4 ---- lib/mapObjects/ObjectTemplate.h | 2 +- lib/mapping/CMap.cpp | 2 -- lib/mapping/CMap.h | 2 -- lib/networkPacks/NetPacksLib.cpp | 10 ++++++++-- lib/networkPacks/ObjProperty.h | 1 - lib/networkPacks/PacksForClient.h | 9 +++++---- 15 files changed, 103 insertions(+), 68 deletions(-) diff --git a/lib/CPlayerState.h b/lib/CPlayerState.h index 7f31ff8ee..175d6ff21 100644 --- a/lib/CPlayerState.h +++ b/lib/CPlayerState.h @@ -27,12 +27,33 @@ struct QuestInfo; struct DLL_LINKAGE PlayerState : public CBonusSystemNode, public Player { + struct VisitedObjectGlobal + { + MapObjectID id; + MapObjectSubID subID; + + bool operator < (const VisitedObjectGlobal & other) const + { + if (id != other.id) + return id < other.id; + else + return subID < other.subID; + } + + template void serialize(Handler &h, const int version) + { + h & id; + subID.serializeIdentifier(h, id, version); + } + }; + public: PlayerColor color; bool human; //true if human controlled player, false for AI TeamID team; TResources resources; std::set visitedObjects; // as a std::set, since most accesses here will be from visited status checks + std::set visitedObjectsGlobal; std::vector > heroes; std::vector > towns; std::vector > dwellings; //used for town growth @@ -82,6 +103,7 @@ public: h & dwellings; h & quests; h & visitedObjects; + h & visitedObjectsGlobal; h & status; h & daysWithoutCastle; h & cheated; diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index acc344022..224bd986a 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -177,6 +177,35 @@ si32 MapObjectID::decode(const std::string & identifier) return rawId.value(); } +std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) +{ + if (index == -1) + return ""; + + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::encode(index); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::encode(index); + + return VLC->objtypeh->getHandlerFor(primaryID, index)->getJsonKey(); +} + +si32 MapObjectSubID::decode(MapObjectID primaryID, const std::string & identifier) +{ + if (identifier.empty()) + return -1; + + if(primaryID == Obj::PRISON || primaryID == Obj::HERO) + return HeroTypeID::decode(identifier); + + if (primaryID == Obj::SPELL_SCROLL) + return SpellID::decode(identifier); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), VLC->objtypeh->getJsonKey(primaryID), identifier); + return rawId.value(); +} + std::string BoatId::encode(int32_t index) { if (index == -1) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index f9c7466c8..b678e7fdc 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -490,16 +490,14 @@ public: } }; -class MapObjectSubID : public StaticIdentifier +class DLL_LINKAGE MapObjectSubID : public Identifier { public: - MapObjectID primaryIdentifier; - constexpr MapObjectSubID(const IdentifierBase & value): - StaticIdentifier(value.getNum()) + Identifier(value.getNum()) {} constexpr MapObjectSubID(int32_t value = -1): - StaticIdentifier(value) + Identifier(value) {} MapObjectSubID & operator =(int32_t value) @@ -514,14 +512,28 @@ public: return *this; } - static si32 decode(const std::string & identifier); - static std::string encode(const si32 index); + static si32 decode(MapObjectID primaryID, const std::string & identifier); + static std::string encode(MapObjectID primaryID, si32 index); // TODO: Remove constexpr operator int32_t () const { return num; } + + template + void serializeIdentifier(Handler &h, const MapObjectID & primaryID, const int version) + { + std::string secondaryStringID; + + if (h.saving) + secondaryStringID = encode(primaryID, num); + + h & secondaryStringID; + + if (!h.saving) + num = decode(primaryID, secondaryStringID); + } }; class DLL_LINKAGE RoadId : public EntityIdentifier diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 75158bdab..7c251a286 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -223,11 +223,6 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog initVisitingAndGarrisonedHeroes(); initFogOfWar(); - // Explicitly initialize static variables - for(auto & elem : players) - { - CGKeys::playerKeyMap[elem.first] = std::set(); - } for(auto & elem : teams) { CGObelisk::visited[elem.first] = 0; diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index cea2a4dad..f85a9a550 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -140,7 +140,7 @@ public: h & subTypeName; h & pos; h & ID; - h & subID; + subID.serializeIdentifier(h, ID, version); h & id; h & tempOwner; h & blockVisit; diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index f2f9c23b0..944efa994 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -23,6 +23,7 @@ #include "../serializer/JsonSerializeFormat.h" #include "../GameConstants.h" #include "../constants/StringConstants.h" +#include "../CPlayerState.h" #include "../CSkillHandler.h" #include "../mapping/CMap.h" #include "../mapObjects/CGHeroInstance.h" @@ -34,8 +35,6 @@ VCMI_LIB_NAMESPACE_BEGIN -std::map > CGKeys::playerKeyMap; - //TODO: Remove constructor CQuest::CQuest(): qid(-1), @@ -777,24 +776,9 @@ void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) quest->serializeJson(handler, "quest"); } -void CGKeys::reset() -{ - playerKeyMap.clear(); -} - -void CGKeys::setPropertyDer (ObjProperty what, ObjPropertyID identifier) -{ - if (what == ObjProperty::KEYMASTER_VISITED) - { - playerKeyMap[identifier.as()].insert(subID); - } - else - logGlobal->error("Unexpected properties requested to set: what=%d, val=%d", static_cast(what), identifier.getNum()); -} - bool CGKeys::wasMyColorVisited(const PlayerColor & player) const { - return playerKeyMap.count(player) && vstd::contains(playerKeyMap[player], subID); + return cb->getPlayerState(player)->visitedObjectsGlobal.count({ID, subID}) != 0; } std::string CGKeys::getHoverText(PlayerColor player) const @@ -817,7 +801,11 @@ void CGKeymasterTent::onHeroVisit( const CGHeroInstance * h ) const int txt_id; if (!wasMyColorVisited (h->getOwner()) ) { - cb->setObjPropertyID(id, ObjProperty::KEYMASTER_VISITED, h->tempOwner); + ChangeObjectVisitors cow; + cow.mode = ChangeObjectVisitors::VISITOR_GLOBAL; + cow.hero = h->id; + cow.object = id; + cb->sendAndApply(&cow); txt_id=19; } else diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index bfeaecd97..644059aa6 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -168,11 +168,6 @@ protected: class DLL_LINKAGE CGKeys : public CGObjectInstance //Base class for Keymaster and guards { public: - static std::map > playerKeyMap; //[players][keysowned] - //SubID 0 - lightblue, 1 - green, 2 - red, 3 - darkblue, 4 - brown, 5 - purple, 6 - white, 7 - black - - static void reset(); - bool wasMyColorVisited(const PlayerColor & player) const; std::string getObjectName() const override; //depending on color @@ -182,8 +177,6 @@ public: { h & static_cast(*this); } -protected: - void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override; }; class DLL_LINKAGE CGKeymasterTent : public CGKeys diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index ca35c3fdd..c3369810b 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -33,7 +33,6 @@ VCMI_LIB_NAMESPACE_BEGIN -std::map > CGMagi::eyelist; ui8 CGObelisk::obeliskCount = 0; //how many obelisks are on map std::map CGObelisk::visited; //map: team_id => how many obelisks has been visited @@ -1003,26 +1002,27 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler) CArmedInstance::serializeJsonOptions(handler); } -void CGMagi::reset() -{ - eyelist.clear(); -} - void CGMagi::initObj(CRandomGenerator & rand) { if (ID == Obj::EYE_OF_MAGI) - { blockVisit = true; - eyelist[subID].push_back(id); - } } + void CGMagi::onHeroVisit(const CGHeroInstance * h) const { if (ID == Obj::HUT_OF_MAGI) { h->showInfoDialog(61); - if (!eyelist[subID].empty()) + std::vector eyes; + + for (auto object : cb->gameState()->map->objects) + { + if (object && object->ID == Obj::EYE_OF_MAGI && object->subID == this->subID) + eyes.push_back(object); + } + + if (!eyes.empty()) { CenterView cv; cv.player = h->tempOwner; @@ -1033,10 +1033,8 @@ void CGMagi::onHeroVisit(const CGHeroInstance * h) const fw.mode = ETileVisibility::REVEALED; fw.waitForDialogs = true; - for(const auto & it : eyelist[subID]) + for(const auto & eye : eyes) { - const CGObjectInstance *eye = cb->getObj(it); - cb->getTilesInRange (fw.tiles, eye->pos, 10, ETileVisibility::HIDDEN, h->tempOwner); cb->sendAndApply(&fw); cv.pos = eye->pos; diff --git a/lib/mapObjects/MiscObjects.h b/lib/mapObjects/MiscObjects.h index d761b2a89..b3fbe421b 100644 --- a/lib/mapObjects/MiscObjects.h +++ b/lib/mapObjects/MiscObjects.h @@ -338,10 +338,6 @@ protected: class DLL_LINKAGE CGMagi : public CGObjectInstance { public: - static std::map > eyelist; //[subID][id], supports multiple sets as in H5 - - static void reset(); - void initObj(CRandomGenerator & rand) override; void onHeroVisit(const CGHeroInstance * h) const override; diff --git a/lib/mapObjects/ObjectTemplate.h b/lib/mapObjects/ObjectTemplate.h index 332227570..258e9aea0 100644 --- a/lib/mapObjects/ObjectTemplate.h +++ b/lib/mapObjects/ObjectTemplate.h @@ -164,7 +164,7 @@ public: h & animationFile; h & stringID; h & id; - h & subid; + subid.serializeIdentifier(h, id, version); h & printPriority; h & visitDir; h & editorAnimationFile; diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index f7f19c658..d161eb773 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -671,8 +671,6 @@ CMapEditManager * CMap::getEditManager() void CMap::resetStaticData() { - CGKeys::reset(); - CGMagi::reset(); CGObelisk::reset(); CGTownInstance::reset(); } diff --git a/lib/mapping/CMap.h b/lib/mapping/CMap.h index d1fbbc14d..7922df144 100644 --- a/lib/mapping/CMap.h +++ b/lib/mapping/CMap.h @@ -191,8 +191,6 @@ public: h & artInstances; // static members - h & CGKeys::playerKeyMap; - h & CGMagi::eyelist; h & CGObelisk::obeliskCount; h & CGObelisk::visited; h & CGTownInstance::merchantArtifacts; diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 1dd78cabc..8faa7044a 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -1061,6 +1061,12 @@ void ChangeObjectVisitors::applyGs(CGameState * gs) const } break; + case VISITOR_GLOBAL: + { + CGObjectInstance * objectPtr = gs->getObjInstance(object); + gs->getPlayerState(gs->getHero(hero)->tempOwner)->visitedObjectsGlobal.insert({objectPtr->ID, objectPtr->subID}); + break; + } case VISITOR_REMOVE: gs->getHero(hero)->visitedObjects.erase(object); break; @@ -1488,7 +1494,7 @@ void NewObject::applyGs(CGameState *gs) const TerrainTile & t = gs->map->getTile(targetPos); terrainType = t.terType->getId(); - auto handler = VLC->objtypeh->getHandlerFor(ID, subID.getNum()); + auto handler = VLC->objtypeh->getHandlerFor(ID, subID); CGObjectInstance * o = handler->create(); handler->configureObject(o, gs->getRandomGenerator()); @@ -1504,7 +1510,7 @@ void NewObject::applyGs(CGameState *gs) cre->character = 2; cre->gainedArtifact = ArtifactID::NONE; cre->identifier = -1; - cre->addToSlot(SlotID(0), new CStackInstance(subID.as(), -1)); //add placeholder stack + cre->addToSlot(SlotID(0), new CStackInstance(subID.getNum(), -1)); //add placeholder stack } assert(!handler->getTemplates(terrainType).empty()); diff --git a/lib/networkPacks/ObjProperty.h b/lib/networkPacks/ObjProperty.h index 9d40028c4..67029d375 100644 --- a/lib/networkPacks/ObjProperty.h +++ b/lib/networkPacks/ObjProperty.h @@ -39,7 +39,6 @@ enum class ObjProperty : int8_t SEERHUT_VISITED, SEERHUT_COMPLETE, - KEYMASTER_VISITED, OBELISK_VISITED, //creature-bank specific diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index 44ef00595..ef7376ad5 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -784,7 +784,7 @@ struct DLL_LINKAGE NewObject : public CPackForClient /// Object ID to create MapObjectID ID; /// Object secondary ID to create - VariantIdentifier subID; + MapObjectSubID subID; /// Position of visitable tile of created object int3 targetPos; /// Which player initiated creation of this object @@ -797,7 +797,7 @@ struct DLL_LINKAGE NewObject : public CPackForClient template void serialize(Handler & h, const int version) { h & ID; - h & subID; + subID.serializeIdentifier(h, ID, version); h & targetPos; h & initiator; } @@ -1247,10 +1247,11 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient { VISITOR_ADD, // mark hero as one that have visited this object VISITOR_ADD_TEAM, // mark team as one that have visited this object + VISITOR_GLOBAL, // mark player as one that have visited object of this type VISITOR_REMOVE, // unmark visitor, reversed to ADD VISITOR_CLEAR // clear all visitors from this object (object reset) }; - ui32 mode = VISITOR_CLEAR; // uses VisitMode enum + VisitMode mode = VISITOR_CLEAR; // uses VisitMode enum ObjectInstanceID object; ObjectInstanceID hero; // note: hero owner will be also marked as "visited" this object @@ -1260,7 +1261,7 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient ChangeObjectVisitors() = default; - ChangeObjectVisitors(ui32 mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) + ChangeObjectVisitors(VisitMode mode, const ObjectInstanceID & object, const ObjectInstanceID & heroID = ObjectInstanceID(-1)) : mode(mode) , object(object) , hero(heroID) From 54480c62094d2f7e477bbfc229ae191663ff4bec Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 17 Nov 2023 22:12:11 +0200 Subject: [PATCH 1150/1248] Fixed Building ID serialization to use string --- lib/constants/EntityIdentifiers.h | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/constants/EntityIdentifiers.h b/lib/constants/EntityIdentifiers.h index b678e7fdc..98d5ddf3c 100644 --- a/lib/constants/EntityIdentifiers.h +++ b/lib/constants/EntityIdentifiers.h @@ -940,7 +940,7 @@ public: static const std::array & ALL_RESOURCES(); }; -class DLL_LINKAGE BuildingTypeUniqueID : public StaticIdentifier +class DLL_LINKAGE BuildingTypeUniqueID : public Identifier { public: BuildingTypeUniqueID(FactionID faction, BuildingID building ); @@ -951,7 +951,20 @@ public: BuildingID getBuilding() const; FactionID getFaction() const; - using StaticIdentifier::StaticIdentifier; + using Identifier::Identifier; + + template + void serialize(Handler & h, const int version) + { + FactionID faction = getFaction(); + BuildingID building = getBuilding(); + + h & faction; + h & building; + + if (!h.saving) + *this = BuildingTypeUniqueID(faction, building); + } }; class DLL_LINKAGE CampaignScenarioID : public StaticIdentifier From a3f9450d8350b407cbbb444d61b9153bc19f2508 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 19:41:00 +0200 Subject: [PATCH 1151/1248] Relax UI locking to reduce delays in AI actions --- client/gui/CGuiHandler.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/gui/CGuiHandler.cpp b/client/gui/CGuiHandler.cpp index c8a103e58..72bc72c37 100644 --- a/client/gui/CGuiHandler.cpp +++ b/client/gui/CGuiHandler.cpp @@ -114,16 +114,20 @@ void CGuiHandler::renderFrame() { boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); - if(nullptr != curInt) + if (nullptr != curInt) curInt->update(); - if(settings["video"]["showfps"].Bool()) + if (settings["video"]["showfps"].Bool()) drawFPSCounter(); + } - SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); + SDL_UpdateTexture(screenTexture, nullptr, screen->pixels, screen->pitch); - SDL_RenderClear(mainRenderer); - SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + SDL_RenderClear(mainRenderer); + SDL_RenderCopy(mainRenderer, screenTexture, nullptr, nullptr); + + { + boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex); CCS->curh->render(); From 9b8145f522d3359c7a034e8761d1e219047f623d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 19:41:29 +0200 Subject: [PATCH 1152/1248] Compute creature speed only once during evaluation --- AI/BattleAI/BattleExchangeVariant.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleExchangeVariant.cpp b/AI/BattleAI/BattleExchangeVariant.cpp index 9d1f4b584..dc37412da 100644 --- a/AI/BattleAI/BattleExchangeVariant.cpp +++ b/AI/BattleAI/BattleExchangeVariant.cpp @@ -890,11 +890,12 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb auto ratio = blockedUnitDamage / (blockedUnitDamage + activeUnitDamage); auto unitReachability = turnBattle.getReachability(unit); + auto unitSpeed = unit->speed(turn); // Cached value, to avoid performance hit for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1) { bool enemyUnit = false; - bool reachable = unitReachability.distances[hex] <= unit->speed(turn); + bool reachable = unitReachability.distances[hex] <= unitSpeed; if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK) { @@ -906,7 +907,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb for(BattleHex neighbor : hex.neighbouringTiles()) { - reachable = unitReachability.distances[neighbor] <= unit->speed(turn); + reachable = unitReachability.distances[neighbor] <= unitSpeed; if(reachable) break; } From 3ac80e1f09da600f5a3527b62261d21e374b9e46 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 19:42:02 +0200 Subject: [PATCH 1153/1248] Avoid costly std vector construction --- lib/battle/CBattleInfoCallback.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/battle/CBattleInfoCallback.cpp b/lib/battle/CBattleInfoCallback.cpp index 2e8412d0a..523be0346 100644 --- a/lib/battle/CBattleInfoCallback.cpp +++ b/lib/battle/CBattleInfoCallback.cpp @@ -357,7 +357,7 @@ const battle::Unit * CBattleInfoCallback::battleGetUnitByPos(BattleHex pos, bool auto ret = battleGetUnitsIf([=](const battle::Unit * unit) { return !unit->isGhost() - && vstd::contains(battle::Unit::getHexes(unit->getPosition(), unit->doubleWide(), unit->unitSide()), pos) + && unit->coversPos(pos) && (!onlyAlive || unit->alive()); }); From f5a64a0ac5e6189f7c4bbcdb5a680ab5e0b31849 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 19:42:45 +0200 Subject: [PATCH 1154/1248] Call cheap to compute conditions before costly conditions --- lib/battle/CUnitState.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/battle/CUnitState.cpp b/lib/battle/CUnitState.cpp index 9763dfa97..eb026acc4 100644 --- a/lib/battle/CUnitState.cpp +++ b/lib/battle/CUnitState.cpp @@ -46,7 +46,7 @@ int32_t CAmmo::available() const bool CAmmo::canUse(int32_t amount) const { - return !isLimited() || (available() - amount >= 0); + return (available() - amount >= 0) || !isLimited(); } bool CAmmo::isLimited() const @@ -99,7 +99,7 @@ CShots & CShots::operator=(const CShots & other) bool CShots::isLimited() const { - return !env->unitHasAmmoCart(owner) || !shooter.getHasBonus(); + return !shooter.getHasBonus() || !env->unitHasAmmoCart(owner); } void CShots::setEnv(const IUnitEnvironment * env_) From b58301849b69ce2d6c9c5a12d335f09b6116f175 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 19:43:11 +0200 Subject: [PATCH 1155/1248] Pass functor via reference to avoid costly copies --- AI/BattleAI/StackWithBonuses.cpp | 2 +- AI/BattleAI/StackWithBonuses.h | 2 +- lib/battle/BattleInfo.cpp | 4 ++-- lib/battle/BattleInfo.h | 4 ++-- lib/battle/BattleProxy.cpp | 4 ++-- lib/battle/BattleProxy.h | 4 ++-- lib/battle/CBattleInfoEssentials.cpp | 4 ++-- lib/battle/CBattleInfoEssentials.h | 4 ++-- lib/battle/IBattleInfoCallback.h | 2 +- lib/battle/IBattleState.h | 4 ++-- 10 files changed, 17 insertions(+), 17 deletions(-) diff --git a/AI/BattleAI/StackWithBonuses.cpp b/AI/BattleAI/StackWithBonuses.cpp index 7beabf514..21246b515 100644 --- a/AI/BattleAI/StackWithBonuses.cpp +++ b/AI/BattleAI/StackWithBonuses.cpp @@ -298,7 +298,7 @@ std::shared_ptr HypotheticBattle::getForUpdate(uint32_t id) } } -battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const +battle::Units HypotheticBattle::getUnitsIf(const battle::UnitFilter & predicate) const { battle::Units proxyed = BattleProxy::getUnitsIf(predicate); diff --git a/AI/BattleAI/StackWithBonuses.h b/AI/BattleAI/StackWithBonuses.h index a69bbb887..6a50f6f84 100644 --- a/AI/BattleAI/StackWithBonuses.h +++ b/AI/BattleAI/StackWithBonuses.h @@ -114,7 +114,7 @@ public: int32_t getActiveStackID() const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; void nextRound() override; void nextTurn(uint32_t unitId) override; diff --git a/lib/battle/BattleInfo.cpp b/lib/battle/BattleInfo.cpp index fcccfafe2..6db67eab8 100644 --- a/lib/battle/BattleInfo.cpp +++ b/lib/battle/BattleInfo.cpp @@ -562,14 +562,14 @@ int32_t BattleInfo::getActiveStackID() const return activeStack; } -TStacks BattleInfo::getStacksIf(TStackFilter predicate) const +TStacks BattleInfo::getStacksIf(const TStackFilter & predicate) const { TStacks ret; vstd::copy_if(stacks, std::back_inserter(ret), predicate); return ret; } -battle::Units BattleInfo::getUnitsIf(battle::UnitFilter predicate) const +battle::Units BattleInfo::getUnitsIf(const battle::UnitFilter & predicate) const { battle::Units ret; vstd::copy_if(stacks, std::back_inserter(ret), predicate); diff --git a/lib/battle/BattleInfo.h b/lib/battle/BattleInfo.h index 030f56051..3b892c680 100644 --- a/lib/battle/BattleInfo.h +++ b/lib/battle/BattleInfo.h @@ -85,9 +85,9 @@ public: int32_t getActiveStackID() const override; - TStacks getStacksIf(TStackFilter predicate) const override; + TStacks getStacksIf(const TStackFilter & predicate) const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; BattleField getBattlefieldType() const override; TerrainId getTerrainType() const override; diff --git a/lib/battle/BattleProxy.cpp b/lib/battle/BattleProxy.cpp index 9ec4339d4..6bc34a77e 100644 --- a/lib/battle/BattleProxy.cpp +++ b/lib/battle/BattleProxy.cpp @@ -40,12 +40,12 @@ int32_t BattleProxy::getActiveStackID() const return -1; } -TStacks BattleProxy::getStacksIf(TStackFilter predicate) const +TStacks BattleProxy::getStacksIf(const TStackFilter & predicate) const { return subject->battleGetStacksIf(predicate); } -battle::Units BattleProxy::getUnitsIf(battle::UnitFilter predicate) const +battle::Units BattleProxy::getUnitsIf(const battle::UnitFilter & predicate) const { return subject->battleGetUnitsIf(predicate); } diff --git a/lib/battle/BattleProxy.h b/lib/battle/BattleProxy.h index d52d4231e..c43ede14d 100644 --- a/lib/battle/BattleProxy.h +++ b/lib/battle/BattleProxy.h @@ -29,9 +29,9 @@ public: int32_t getActiveStackID() const override; - TStacks getStacksIf(TStackFilter predicate) const override; + TStacks getStacksIf(const TStackFilter & predicate) const override; - battle::Units getUnitsIf(battle::UnitFilter predicate) const override; + battle::Units getUnitsIf(const battle::UnitFilter & predicate) const override; BattleField getBattlefieldType() const override; TerrainId getTerrainType() const override; diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index c113dddef..2b69caa29 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -109,13 +109,13 @@ TStacks CBattleInfoEssentials::battleGetAllStacks(bool includeTurrets) const }); } -TStacks CBattleInfoEssentials::battleGetStacksIf(TStackFilter predicate) const +TStacks CBattleInfoEssentials::battleGetStacksIf(const TStackFilter & predicate) const { RETURN_IF_NOT_BATTLE(TStacks()); return getBattle()->getStacksIf(std::move(predicate)); } -battle::Units CBattleInfoEssentials::battleGetUnitsIf(battle::UnitFilter predicate) const +battle::Units CBattleInfoEssentials::battleGetUnitsIf(const battle::UnitFilter & predicate) const { RETURN_IF_NOT_BATTLE(battle::Units()); return getBattle()->getUnitsIf(predicate); diff --git a/lib/battle/CBattleInfoEssentials.h b/lib/battle/CBattleInfoEssentials.h index 0dc3cab1b..f19563a5d 100644 --- a/lib/battle/CBattleInfoEssentials.h +++ b/lib/battle/CBattleInfoEssentials.h @@ -62,8 +62,8 @@ public: * @return filtered stacks * */ - TStacks battleGetStacksIf(TStackFilter predicate) const; //deprecated - battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const override; + TStacks battleGetStacksIf(const TStackFilter & predicate) const; //deprecated + battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const override; const battle::Unit * battleGetUnitByID(uint32_t ID) const override; const battle::Unit * battleActiveUnit() const override; diff --git a/lib/battle/IBattleInfoCallback.h b/lib/battle/IBattleInfoCallback.h index 5996d5bda..6ee05b2f6 100644 --- a/lib/battle/IBattleInfoCallback.h +++ b/lib/battle/IBattleInfoCallback.h @@ -72,7 +72,7 @@ public: virtual uint32_t battleNextUnitId() const = 0; - virtual battle::Units battleGetUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units battleGetUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual const battle::Unit * battleGetUnitByID(uint32_t ID) const = 0; virtual const battle::Unit * battleGetUnitByPos(BattleHex pos, bool onlyAlive = true) const = 0; diff --git a/lib/battle/IBattleState.h b/lib/battle/IBattleState.h index 8af3ad429..fd643c56c 100644 --- a/lib/battle/IBattleState.h +++ b/lib/battle/IBattleState.h @@ -42,9 +42,9 @@ public: virtual int32_t getActiveStackID() const = 0; - virtual TStacks getStacksIf(TStackFilter predicate) const = 0; + virtual TStacks getStacksIf(const TStackFilter & predicate) const = 0; - virtual battle::Units getUnitsIf(battle::UnitFilter predicate) const = 0; + virtual battle::Units getUnitsIf(const battle::UnitFilter & predicate) const = 0; virtual BattleField getBattlefieldType() const = 0; virtual TerrainId getTerrainType() const = 0; From 118dafb71ba174393504f450e8f1700047f3ff0a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 18 Nov 2023 21:17:40 +0200 Subject: [PATCH 1156/1248] Fix tests --- test/mock/mock_IBattleInfoCallback.h | 2 +- test/mock/mock_battle_IBattleState.h | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/test/mock/mock_IBattleInfoCallback.h b/test/mock/mock_IBattleInfoCallback.h index 2bc00a19b..f28dd37f6 100644 --- a/test/mock/mock_IBattleInfoCallback.h +++ b/test/mock/mock_IBattleInfoCallback.h @@ -29,7 +29,7 @@ public: MOCK_CONST_METHOD0(battleNextUnitId, uint32_t()); - MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(battleGetUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD1(battleGetUnitByID, const battle::Unit *(uint32_t)); MOCK_CONST_METHOD2(battleGetUnitByPos, const battle::Unit *(BattleHex, bool)); diff --git a/test/mock/mock_battle_IBattleState.h b/test/mock/mock_battle_IBattleState.h index 483c13b23..6ef0998d8 100644 --- a/test/mock/mock_battle_IBattleState.h +++ b/test/mock/mock_battle_IBattleState.h @@ -17,8 +17,8 @@ class BattleStateMock : public IBattleState { public: MOCK_CONST_METHOD0(getActiveStackID, int32_t()); - MOCK_CONST_METHOD1(getStacksIf, TStacks(TStackFilter)); - MOCK_CONST_METHOD1(getUnitsIf, battle::Units(battle::UnitFilter)); + MOCK_CONST_METHOD1(getStacksIf, TStacks(const TStackFilter&)); + MOCK_CONST_METHOD1(getUnitsIf, battle::Units(const battle::UnitFilter &)); MOCK_CONST_METHOD0(getBattlefieldType, BattleField()); MOCK_CONST_METHOD0(getTerrainType, TerrainId()); MOCK_CONST_METHOD0(getAllObstacles, IBattleInfo::ObstacleCList()); From cc71651ee48ef3bc132cb9d271eeb01b4f9928b2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 19 Nov 2023 19:30:55 +0200 Subject: [PATCH 1157/1248] Show proper error message if player attempts to load save with missing identifiers instead of silent crash to main menu --- Mods/vcmi/config/vcmi/english.json | 1 + Mods/vcmi/config/vcmi/ukrainian.json | 1 + lib/constants/EntityIdentifiers.cpp | 84 ++++++++++------------------ lib/constants/IdentifierBase.h | 20 ++++++- server/CGameHandler.cpp | 11 ++++ 5 files changed, 60 insertions(+), 57 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index e6cb860bb..fdf481902 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -75,6 +75,7 @@ "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", "vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n", "vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index e7be77050..8fc46d9a9 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -75,6 +75,7 @@ "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n", "vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index 224bd986a..d119c0f76 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -122,12 +122,21 @@ namespace GameConstants #endif } -si32 HeroClassID::decode(const std::string & identifier) +int32_t IdentifierBase::resolveIdentifier(const std::string & entityType, const std::string identifier) { if (identifier.empty()) return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "heroClass", identifier); - return rawId.value(); + + auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType, identifier); + + if (rawId) + return rawId.value(); + throw IdentifierResolutionException(identifier); +} + +si32 HeroClassID::decode(const std::string & identifier) +{ + return resolveIdentifier("heroClass", identifier); } std::string HeroClassID::encode(const si32 index) @@ -171,10 +180,7 @@ std::string MapObjectID::encode(int32_t index) si32 MapObjectID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "object", identifier); - return rawId.value(); + return resolveIdentifier("object", identifier); } std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) @@ -193,17 +199,13 @@ std::string MapObjectSubID::encode(MapObjectID primaryID, int32_t index) si32 MapObjectSubID::decode(MapObjectID primaryID, const std::string & identifier) { - if (identifier.empty()) - return -1; - if(primaryID == Obj::PRISON || primaryID == Obj::HERO) return HeroTypeID::decode(identifier); if (primaryID == Obj::SPELL_SCROLL) return SpellID::decode(identifier); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), VLC->objtypeh->getJsonKey(primaryID), identifier); - return rawId.value(); + return resolveIdentifier(VLC->objtypeh->getJsonKey(primaryID), identifier); } std::string BoatId::encode(int32_t index) @@ -215,20 +217,14 @@ std::string BoatId::encode(int32_t index) si32 BoatId::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "core:boat", identifier); - return rawId.value(); + return resolveIdentifier("core:boat", identifier); } si32 HeroTypeID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; if (identifier == "random") return -2; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "hero", identifier); - return rawId.value(); + return resolveIdentifier("hero", identifier); } std::string HeroTypeID::encode(const si32 index) @@ -257,10 +253,7 @@ const Artifact * ArtifactIDBase::toEntity(const Services * services) const si32 ArtifactID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "artifact", identifier); - return rawId.value(); + return resolveIdentifier("artifact", identifier); } std::string ArtifactID::encode(const si32 index) @@ -277,10 +270,7 @@ std::string ArtifactID::entityType() si32 SecondarySkill::decode(const std::string& identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "secondarySkill", identifier); - return rawId.value(); + return resolveIdentifier("secondarySkill", identifier); } std::string SecondarySkill::encode(const si32 index) @@ -317,10 +307,7 @@ const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) con si32 CreatureID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "creature", identifier); - return rawId.value(); + return resolveIdentifier("creature", identifier); } std::string CreatureID::encode(const si32 index) @@ -367,10 +354,7 @@ const HeroType * HeroTypeID::toEntity(const Services * services) const si32 SpellID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - return rawId.value(); + return resolveIdentifier("spell", identifier); } std::string SpellID::encode(const si32 index) @@ -382,10 +366,7 @@ std::string SpellID::encode(const si32 index) si32 BattleField::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier); - return rawId.value(); + return resolveIdentifier("battlefield", identifier); } std::string BattleField::encode(const si32 index) @@ -439,7 +420,7 @@ std::string PlayerColor::entityType() si32 PrimarySkill::decode(const std::string& identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string PrimarySkill::encode(const si32 index) @@ -454,11 +435,7 @@ std::string PrimarySkill::entityType() si32 FactionID::decode(const std::string & identifier) { - if (identifier.empty()) - return -1; - - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string FactionID::encode(const si32 index) @@ -480,13 +457,10 @@ const Faction * FactionID::toEntity(const Services * service) const si32 TerrainId::decode(const std::string & identifier) { - if (identifier.empty()) - return static_cast(TerrainId::NONE); if (identifier == "native") return TerrainId::NATIVE_TERRAIN; - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string TerrainId::encode(const si32 index) @@ -508,8 +482,7 @@ si32 RoadId::decode(const std::string & identifier) if (identifier.empty()) return RoadId::NO_ROAD.getNum(); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string RoadId::encode(const si32 index) @@ -529,8 +502,7 @@ si32 RiverId::decode(const std::string & identifier) if (identifier.empty()) return RiverId::NO_RIVER.getNum(); - auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); - return rawId.value(); + return resolveIdentifier(entityType(), identifier); } std::string RiverId::encode(const si32 index) @@ -574,7 +546,7 @@ const ObstacleInfo * Obstacle::getInfo() const si32 SpellSchool::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string SpellSchool::encode(const si32 index) @@ -592,7 +564,7 @@ std::string SpellSchool::entityType() si32 GameResID::decode(const std::string & identifier) { - return *VLC->identifiers()->getIdentifier(ModScope::scopeGame(), entityType(), identifier); + return resolveIdentifier(entityType(), identifier); } std::string GameResID::encode(const si32 index) diff --git a/lib/constants/IdentifierBase.h b/lib/constants/IdentifierBase.h index c72903ca0..62c2e671b 100644 --- a/lib/constants/IdentifierBase.h +++ b/lib/constants/IdentifierBase.h @@ -9,6 +9,19 @@ */ #pragma once +VCMI_LIB_NAMESPACE_BEGIN + +class IdentifierResolutionException : public std::runtime_error +{ +public: + const std::string identifierName; + + IdentifierResolutionException(const std::string & identifierName ) + : std::runtime_error("Failed to resolve identifier " + identifierName) + , identifierName(identifierName) + {} +}; + class IdentifierBase { protected: @@ -21,6 +34,11 @@ protected: {} ~IdentifierBase() = default; + + /// Attempts to resolve identifier using provided entity type + /// Returns resolved numeric identifier + /// Throws IdentifierResolutionException on failure + static int32_t resolveIdentifier(const std::string & entityType, const std::string identifier); public: int32_t num; @@ -233,4 +251,4 @@ public: } }; -/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +VCMI_LIB_NAMESPACE_END diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index b9011b4db..5bdba02b1 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1802,9 +1802,20 @@ bool CGameHandler::load(const std::string & filename) lobby->announceMessage(errorMsg); return false; } + catch(const IdentifierResolutionException & e) + { + logGlobal->error("Failed to load game: %s", e.what()); + MetaString errorMsg; + errorMsg.appendTextID("vcmi.server.errors.unknownEntity"); + errorMsg.replaceRawString(e.identifierName); + lobby->announceMessage(errorMsg.toString());//FIXME: should be localized on client side + return false; + } + catch(const std::exception & e) { logGlobal->error("Failed to load game: %s", e.what()); + lobby->announceMessage(std::string("Failed to load game: ") + e.what()); return false; } gs->preInit(VLC); From c717bb55040a20d5404fbe96db9fc7a29e958123 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 19 Nov 2023 20:44:28 +0200 Subject: [PATCH 1158/1248] Always convert mod ID to lower case before using it --- launcher/modManager/cmodlist.cpp | 20 ++++++++++++++++++-- launcher/modManager/cmodlist.h | 3 +++ launcher/modManager/cmodlistview_moc.cpp | 12 ++++++------ launcher/modManager/cmodmanager.cpp | 8 ++++---- lib/modding/CModInfo.cpp | 14 ++++++++++++-- lib/modding/CModInfo.h | 1 + 6 files changed, 44 insertions(+), 14 deletions(-) diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp index f31679cdd..b36d4b198 100644 --- a/launcher/modManager/cmodlist.cpp +++ b/launcher/modManager/cmodlist.cpp @@ -141,6 +141,22 @@ QVariant CModEntry::getValue(QString value) const return getValueImpl(value, true); } +QStringList CModEntry::getDependencies() const +{ + QStringList result; + for (auto const & entry : getValue("depends").toStringList()) + result.push_back(entry.toLower()); + return result; +} + +QStringList CModEntry::getConflicts() const +{ + QStringList result; + for (auto const & entry : getValue("conflicts").toStringList()) + result.push_back(entry.toLower()); + return result; +} + QVariant CModEntry::getBaseValue(QString value) const { return getValueImpl(value, false); @@ -341,8 +357,8 @@ QStringList CModList::getRequirements(QString modname) { auto mod = getMod(modname); - for(auto entry : mod.getValue("depends").toStringList()) - ret += getRequirements(entry); + for(auto entry : mod.getDependencies()) + ret += getRequirements(entry.toLower()); } ret += modname; diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h index 41b2f0fae..05aaa4f6b 100644 --- a/launcher/modManager/cmodlist.h +++ b/launcher/modManager/cmodlist.h @@ -76,6 +76,9 @@ public: QVariant getValue(QString value) const; QVariant getBaseValue(QString value) const; + QStringList getDependencies() const; + QStringList getConflicts() const; + static QString sizeToString(double size); }; diff --git a/launcher/modManager/cmodlistview_moc.cpp b/launcher/modManager/cmodlistview_moc.cpp index 0e713bfda..23789707d 100644 --- a/launcher/modManager/cmodlistview_moc.cpp +++ b/launcher/modManager/cmodlistview_moc.cpp @@ -218,12 +218,12 @@ QStringList CModListView::getModNames(QStringList input) for(const auto & modID : input) { - auto mod = modModel->getMod(modID); + auto mod = modModel->getMod(modID.toLower()); QString modName = mod.getValue("name").toString(); if (modName.isEmpty()) - result += modID; + result += modID.toLower(); else result += modName; } @@ -311,8 +311,8 @@ QString CModListView::genModInfoText(CModEntry & mod) if(needToShowSupportedLanguages) result += replaceIfNotEmpty(supportedLanguages, lineTemplate.arg(tr("Languages"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("depends").toStringList()), lineTemplate.arg(tr("Required mods"))); - result += replaceIfNotEmpty(getModNames(mod.getValue("conflicts").toStringList()), lineTemplate.arg(tr("Conflicting mods"))); + result += replaceIfNotEmpty(getModNames(mod.getDependencies()), lineTemplate.arg(tr("Required mods"))); + result += replaceIfNotEmpty(getModNames(mod.getConflicts()), lineTemplate.arg(tr("Conflicting mods"))); result += replaceIfNotEmpty(getModNames(mod.getValue("description").toStringList()), textTemplate.arg(tr("Description"))); result += "

    "; // to get some empty space @@ -464,7 +464,7 @@ QStringList CModListView::findBlockingMods(QString modUnderTest) if(mod.isEnabled()) { // one of enabled mods have requirement (or this mod) marked as conflict - for(auto conflict : mod.getValue("conflicts").toStringList()) + for(auto conflict : mod.getConflicts()) { if(required.contains(conflict)) ret.push_back(name); @@ -485,7 +485,7 @@ QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled) if(!current.isInstalled()) continue; - if(current.getValue("depends").toStringList().contains(mod)) + if(current.getDependencies().contains(mod, Qt::CaseInsensitive)) { if(!(current.isDisabled() && excludeDisabled)) ret += modName; diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp index 8e9ed4ee3..9559630ff 100644 --- a/launcher/modManager/cmodmanager.cpp +++ b/launcher/modManager/cmodmanager.cpp @@ -192,7 +192,7 @@ bool CModManager::canEnableMod(QString modname) if(!mod.isCompatible()) return addError(modname, "Mod is not compatible, please update VCMI and checkout latest mod revisions"); - for(auto modEntry : mod.getValue("depends").toStringList()) + for(auto modEntry : mod.getDependencies()) { if(!modList->hasMod(modEntry)) // required mod is not available return addError(modname, QString("Required mod %1 is missing").arg(modEntry)); @@ -205,11 +205,11 @@ bool CModManager::canEnableMod(QString modname) auto mod = modList->getMod(modEntry); // "reverse conflict" - enabled mod has this one as conflict - if(mod.isEnabled() && mod.getValue("conflicts").toStringList().contains(modname)) + if(mod.isEnabled() && mod.getConflicts().contains(modname)) return addError(modname, QString("This mod conflicts with %1").arg(modEntry)); } - for(auto modEntry : mod.getValue("conflicts").toStringList()) + for(auto modEntry : mod.getConflicts()) { // check if conflicting mod installed and enabled if(modList->hasMod(modEntry) && modList->getMod(modEntry).isEnabled()) @@ -232,7 +232,7 @@ bool CModManager::canDisableMod(QString modname) { auto current = modList->getMod(modEntry); - if(current.getValue("depends").toStringList().contains(modname) && current.isEnabled()) + if(current.getDependencies().contains(modname) && current.isEnabled()) return addError(modname, QString("This mod is needed to run %1").arg(modEntry)); } return true; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 7cb346789..ca7d0a97d 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -22,6 +22,16 @@ static JsonNode addMeta(JsonNode config, const std::string & meta) return config; } +std::set CModInfo::readModList(const JsonNode & input) +{ + std::set result; + + for (auto const & string : input.convertTo>()) + result.insert(boost::to_lower_copy(string)); + + return result; +} + CModInfo::CModInfo(): explicitlyEnabled(false), implicitlyEnabled(true), @@ -32,8 +42,8 @@ CModInfo::CModInfo(): CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): identifier(identifier), - dependencies(config["depends"].convertTo>()), - conflicts(config["conflicts"].convertTo>()), + dependencies(readModList(config["depends"])), + conflicts(readModList(config["conflicts"])), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING), diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 2c29e2159..b89fdae02 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -20,6 +20,7 @@ class DLL_LINKAGE CModInfo /// Do not serialize - depends on local mod version, not server/save mod version mutable std::optional modGameplayAffecting; + static std::set readModList(const JsonNode & input); public: enum EValidationStatus { From f7ef4742010af537e675a2fc3ec975f6892ad7df Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 19 Nov 2023 23:59:49 +0200 Subject: [PATCH 1159/1248] Fix format of custom win/loss conditions for h3 campaigns --- config/mapOverrides.json | 186 +++++++++++++++++----------------- lib/mapping/MapFormatJson.cpp | 8 +- 2 files changed, 96 insertions(+), 98 deletions(-) diff --git a/config/mapOverrides.json b/config/mapOverrides.json index 9a520fe7c..c3a2ddf63 100644 --- a/config/mapOverrides.json +++ b/config/mapOverrides.json @@ -7,7 +7,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -35,7 +35,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 9, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -47,7 +47,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 13, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -95,7 +95,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 16, 23, 0 ], "type" : 84 } ] + [ "transport", { "position" : [ 16, 23, 0 ], "type" : "artifact.spiritOfOppression" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -135,7 +135,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 2 } ] + [ "haveArtifact", { "type" : "artifact.grail" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -163,7 +163,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 63, 65, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -205,7 +205,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 102, 18, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -217,7 +217,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 128 } ] + [ "haveArtifact", { "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -246,7 +246,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 66, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 7, 66, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -260,7 +260,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 18, 68, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 18, 68, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -274,7 +274,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 58, 12, 0 ], "type" : 34 } ] //Gelu + [ "control", { "position" : [ 58, 12, 0 ], "type" : "hero" } ] //Gelu ] ], "effect" : { @@ -287,7 +287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 33, 37, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 33, 37, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -316,7 +316,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 3, 0 ], "type" : 34 } ] // Gelu + [ "control", { "position" : [ 4, 3, 0 ], "type" : "hero" } ] // Gelu ] ], "effect" : { @@ -330,7 +330,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 12, 63, 0 ], "type" : 34 } ] // Catherine + [ "control", { "position" : [ 12, 63, 0 ], "type" : "hero" } ] // Catherine ] ], "effect" : { @@ -344,7 +344,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 63, 51, 0 ], "type" : 34 } ] // Roland + [ "control", { "position" : [ 63, 51, 0 ], "type" : "hero" } ] // Roland ] ], "effect" : { @@ -369,7 +369,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 35, 36, 0 ], "type" : 128 } ] + [ "transport", { "position" : [ 35, 36, 0 ], "type" : "artifact.armageddonsBlade" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -397,7 +397,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 99, 101, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -421,7 +421,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 53 } ] + [ "control", { "type" : "mine" } ] ], "effect" : { "messageToSend" : "core.genrltxt.291", @@ -449,7 +449,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 104, 61, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -470,7 +470,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 6, 49, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -497,7 +497,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 88, 82, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -518,7 +518,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 107, 3, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -545,7 +545,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 23, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -566,7 +566,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 57, 61, 0 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -594,7 +594,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -608,7 +608,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 4, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 4, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -622,7 +622,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 15, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 15, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -636,7 +636,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 10, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 10, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -686,7 +686,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 94, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 94, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -700,7 +700,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 93, 51, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 93, 51, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -714,7 +714,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 85, 64, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 85, 64, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -728,7 +728,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [100, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [100, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -766,7 +766,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 38, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 38, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -780,7 +780,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 32, 7, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 32, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -794,7 +794,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 8, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 8, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -808,7 +808,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 63, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 63, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -838,7 +838,7 @@ "message" : "core.genrltxt.7" }, "specialVictory" : { - "condition" : [ "destroy", { "type" : 54} ], + "condition" : [ "destroy", { "type" : "monster"} ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", "type" : "victory" @@ -857,7 +857,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 1, 3, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -878,7 +878,7 @@ "message" : "core.genrltxt.254" }, "specialVictory" : { - "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : 54 } ], + "condition" : [ "destroy", { "position" : [ 62, 5, 1 ], "type" : "monster" } ], "effect" : { "messageToSend" : "core.genrltxt.287", "type" : "victory" @@ -905,7 +905,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -933,7 +933,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "type" : 54} ] + [ "destroy", { "type" : "monster"} ] ], "effect" : { "messageToSend" : "vcmi.map.victoryCondition.eliminateMonsters.toOthers", @@ -954,7 +954,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "type" : 17 } ] + [ "control", { "type" : "creatureGeneratorCommon" } ] ], "effect" : { "messageToSend" : "core.genrltxt.289", @@ -994,7 +994,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 60, 45, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1044,7 +1044,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 19, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 19, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1058,7 +1058,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 17, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 17, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1068,7 +1068,7 @@ "message" : "core.genrltxt.253" }, "specialVictory" : { - "condition" : [ "haveArtifact", { "type" : 54 } ], + "condition" : [ "haveArtifact", { "type" : "artifact.amuletOfTheUndertaker" } ], "effect" : { "messageToSend" : "core.genrltxt.281", "type" : "victory" @@ -1096,7 +1096,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1110,7 +1110,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 30, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 30, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1123,7 +1123,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 9, 11, 0 ], "type" : 55 } ] + [ "transport", { "position" : [ 9, 11, 0 ], "type" : "artifact.vampiresCowl" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1152,7 +1152,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 55, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 55, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1166,7 +1166,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 53, 17, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 53, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1179,7 +1179,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "transport", { "position" : [ 53, 16, 0 ], "type" : 56 } ] + [ "transport", { "position" : [ 53, 16, 0 ], "type" : "artifact.deadMansBoots" } ] ], "effect" : { "messageToSend" : "core.genrltxt.293", @@ -1207,7 +1207,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 58, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1219,7 +1219,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 20 } ] + [ "haveArtifact", { "type" : "artifact.skullHelmet" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1247,7 +1247,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 69, 2, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1259,7 +1259,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 8 } ] + [ "haveArtifact", { "type" : "artifact.blackshardOfTheDeadKnight" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1287,7 +1287,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 2, 1, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1299,7 +1299,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 26 } ] + [ "haveArtifact", { "type" : "artifact.ribCage" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1327,7 +1327,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 10, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1339,7 +1339,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveArtifact", { "type" : 14 } ] + [ "haveArtifact", { "type" : "artifact.shieldOfTheYawningDead" } ] ], "effect" : { "messageToSend" : "core.genrltxt.281", @@ -1368,7 +1368,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 56, 54, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 56, 54, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1382,7 +1382,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1420,7 +1420,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 65, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 65, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1434,7 +1434,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 67, 25, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 67, 25, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1471,7 +1471,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 71, 14, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -1495,7 +1495,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 68, 4, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 68, 4, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1524,7 +1524,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 57, 12, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 57, 12, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1538,7 +1538,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 25, 11, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 25, 11, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1551,7 +1551,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 8, 29, 1 ], "type" : 34 } ] + [ "destroy", { "position" : [ 8, 29, 1 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -1580,7 +1580,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 54, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 54, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1594,7 +1594,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 13, 6, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 13, 6, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1632,7 +1632,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 34, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 34, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1646,7 +1646,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 36, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 36, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1659,7 +1659,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 36, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 36, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -1688,7 +1688,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 10, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 10, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1702,7 +1702,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 44, 9, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 44, 9, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1746,7 +1746,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 70, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 70, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1760,7 +1760,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 70, 66, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 70, 66, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1804,7 +1804,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 7, 13, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 7, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1818,7 +1818,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 15, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 15, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1832,7 +1832,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 6, 103, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 6, 103, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1846,7 +1846,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 9, 105, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 9, 105, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1889,7 +1889,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 14, 53, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 14, 53, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1903,7 +1903,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 21, 69, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 21, 69, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1917,7 +1917,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 38, 59, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 38, 59, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1931,7 +1931,7 @@ "allOf", [ "isHuman", { "value" : 1 } ], [ "noneOf", - [ "control", { "position" : [ 66, 60, 0 ], "type" : 34 } ] + [ "control", { "position" : [ 66, 60, 0 ], "type" : "hero" } ] ] ], "effect" : { @@ -1980,7 +1980,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 68, 13, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2004,7 +2004,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "control", { "position" : [ 4, 67, 0 ], "type" : 98 } ] + [ "control", { "position" : [ 4, 67, 0 ], "type" : "town" } ] ], "effect" : { "messageToSend" : "core.genrltxt.250", @@ -2032,7 +2032,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 66, 17, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2044,7 +2044,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "haveResources", { "type" : 6, "value" : 100000 } ] + [ "haveResources", { "type" : "gold", "value" : 100000 } ] ], "effect" : { "messageToSend" : "core.genrltxt.279", @@ -2072,7 +2072,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 6, 8, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", @@ -2096,7 +2096,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "destroy", { "position" : [ 31, 26, 0 ], "type" : 34 } ] + [ "destroy", { "position" : [ 31, 26, 0 ], "type" : "hero" } ] ], "effect" : { "messageToSend" : "core.genrltxt.253", @@ -2124,7 +2124,7 @@ "condition" : [ "allOf", [ "isHuman", { "value" : 1 } ], - [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : 34 } ] ] + [ "noneOf", [ "control", { "position" : [ 24, 7, 0 ], "type" : "hero" } ] ] ], "effect" : { "messageToSend" : "core.genrltxt.5", diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index ba30eeb21..c8f3a476a 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -128,16 +128,14 @@ namespace HeaderDetail namespace TriggeredEventsDetail { - static const std::array conditionNames = + static const std::array conditionNames = { "haveArtifact", "haveCreatures", "haveResources", "haveBuilding", "control", "destroy", "transport", "daysPassed", - "isHuman", "daysWithoutTown", "standardWin", "constValue", - - "have_0", "haveBuilding_0", "destroy_0" + "isHuman", "daysWithoutTown", "standardWin", "constValue" }; - static const std::array typeNames = { "victory", "defeat" }; + static const std::array typeNames = { "victory", "defeat" }; static EventCondition JsonToCondition(const JsonNode & node) { From e5e01ab35d440761b28f6dcbb0b7a29ce244558f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 14:38:57 +0200 Subject: [PATCH 1160/1248] Disable spectator mode in single player to avoid confusing players --- client/lobby/CLobbyScreen.cpp | 4 ++-- client/lobby/OptionsTab.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index 135e0d078..95623b02f 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -76,7 +76,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) card->iconDifficulty->addCallback(std::bind(&IServerAPI::setDifficulty, CSH, _1)); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_BEGIN_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRBEG.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_BEGIN_GAME); initLobby(); break; } @@ -84,7 +84,7 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) { tabOpt = std::make_shared(); tabTurnOptions = std::make_shared(); - buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, true), EShortcut::LOBBY_LOAD_GAME); + buttonStart = std::make_shared(Point(411, 535), AnimationPath::builtin("SCNRLOD.DEF"), CGI->generaltexth->zelp[103], std::bind(&CLobbyScreen::startScenario, this, false), EShortcut::LOBBY_LOAD_GAME); initLobby(); break; } diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index f9a211c25..65d5cead3 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -959,7 +959,7 @@ void OptionsTab::PlayerOptionsEntry::updateName() { void OptionsTab::onSetPlayerClicked(const PlayerSettings & ps) const { - if(ps.isControlledByAI() || humanPlayers > 0) + if(ps.isControlledByAI() || humanPlayers > 1) CSH->setPlayer(ps.color); } From 845a259619cb63147adfc63f672f7b071e7ba15f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 14:39:19 +0200 Subject: [PATCH 1161/1248] Disable quick combat by default to avoid confusing players --- config/schemas/settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/schemas/settings.json b/config/schemas/settings.json index d0dedb7fe..5e7e5956d 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -255,7 +255,7 @@ }, "quickCombat" : { "type" : "boolean", - "default" : true + "default" : false }, "objectAnimation" : { "type" : "boolean", From e9ac8c67c1fc88363e5749a95cc8019eabee8e12 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 18:44:27 +0200 Subject: [PATCH 1162/1248] Reworked and fixed selection of secondary skills: - Fixed off-by-one error when checking for obligatory skills - If both wisdom and magic school must be offered in the same slot, magic school will be correctly offered on next levelup - Obligatory skill can now be proposed for upgrade - Obligatory skills are now offered using hero class weight instead of simple random - If hero has multiple skills not available to his class game will select random skill instead of first one - Moved storage of random seed to server instead of mutable member --- lib/CHeroHandler.cpp | 30 ++-- lib/mapObjects/CGHeroInstance.cpp | 153 ++++++++----------- lib/mapObjects/CGHeroInstance.h | 6 +- lib/networkPacks/NetPackVisitor.h | 1 - lib/networkPacks/NetPacksLib.cpp | 22 --- lib/networkPacks/PacksForClient.h | 17 --- lib/registerTypes/RegisterTypesClientPacks.h | 1 - server/CGameHandler.cpp | 13 +- server/processors/HeroPoolProcessor.cpp | 11 ++ server/processors/HeroPoolProcessor.h | 6 + 10 files changed, 106 insertions(+), 154 deletions(-) diff --git a/lib/CHeroHandler.cpp b/lib/CHeroHandler.cpp index 5de91ebe8..3ae6931a1 100644 --- a/lib/CHeroHandler.cpp +++ b/lib/CHeroHandler.cpp @@ -123,26 +123,30 @@ void CHero::serializeJson(JsonSerializeFormat & handler) SecondarySkill CHeroClass::chooseSecSkill(const std::set & possibles, CRandomGenerator & rand) const //picks secondary skill out from given possibilities { + assert(!possibles.empty()); + + if (possibles.size() == 1) + return *possibles.begin(); + int totalProb = 0; for(const auto & possible : possibles) if (secSkillProbability.count(possible) != 0) totalProb += secSkillProbability.at(possible); - if (totalProb != 0) // may trigger if set contains only banned skills (0 probability) - { - auto ran = rand.nextInt(totalProb - 1); - for(const auto & possible : possibles) - { - if (secSkillProbability.count(possible) != 0) - ran -= secSkillProbability.at(possible); + if (totalProb == 0) // may trigger if set contains only banned skills (0 probability) + return *RandomGeneratorUtil::nextItem(possibles, rand); - if(ran < 0) - { - return possible; - } - } + auto ran = rand.nextInt(totalProb - 1); + for(const auto & possible : possibles) + { + if (secSkillProbability.count(possible) != 0) + ran -= secSkillProbability.at(possible); + + if(ran < 0) + return possible; } - // FIXME: select randomly? How H3 handles such rare situation? + + assert(0); // should not be possible return *possibles.begin(); } diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 0ea8bb52d..798c2f97c 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -397,9 +397,7 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) commander->giveStackExp (exp); //after our exp is set } - skillsInfo.rand.setSeed(rand.nextInt()); - skillsInfo.resetMagicSchoolCounter(); - skillsInfo.resetWisdomCounter(); + skillsInfo = SecondarySkillsInfo(); //copy active (probably growing) bonuses from hero prototype to hero object for(const std::shared_ptr & b : type->specialty) @@ -569,17 +567,15 @@ ui8 CGHeroInstance::maxlevelsToWisdom() const CGHeroInstance::SecondarySkillsInfo::SecondarySkillsInfo(): magicSchoolCounter(1), wisdomCounter(1) -{ - rand.setSeed(0); -} +{} void CGHeroInstance::SecondarySkillsInfo::resetMagicSchoolCounter() { - magicSchoolCounter = 1; + magicSchoolCounter = 0; } void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter() { - wisdomCounter = 1; + wisdomCounter = 0; } void CGHeroInstance::pickRandomObject(CRandomGenerator & rand) @@ -1268,49 +1264,31 @@ ArtBearer::ArtBearer CGHeroInstance::bearerType() const return ArtBearer::HERO; } -std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() const +std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const { - std::vector obligatorySkills; //hero is offered magic school or wisdom if possible - auto getObligatorySkills = [](CSkill::Obligatory obl){ - std::vector obligatory = {}; + std::set obligatory; for(auto i = 0; i < VLC->skillh->size(); i++) if((*VLC->skillh)[SecondarySkill(i)]->obligatory(obl)) - obligatory.emplace_back(i); //Always return all obligatory skills + obligatory.insert(i); //Always return all obligatory skills return obligatory; }; - auto selectObligatorySkill = [&](std::vector& ss) -> void + auto intersect = [](const std::set & left, const std::set & right) { - std::shuffle(ss.begin(), ss.end(), skillsInfo.rand.getStdGenerator()); - - for(const auto & skill : ss) - { - if (canLearnSkill(skill)) //only skills hero doesn't know yet - { - obligatorySkills.push_back(skill); - break; //only one - } - } + std::set intersect; + std::set_intersection(left.begin(), left.end(), right.begin(), right.end(), + std::inserter(intersect, intersect.begin())); + return intersect; }; - if (!skillsInfo.wisdomCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MAJOR); - selectObligatorySkill(obligatory); - } - if (!skillsInfo.magicSchoolCounter) - { - auto obligatory = getObligatorySkills(CSkill::Obligatory::MINOR); - selectObligatorySkill(obligatory); - } + std::set wisdomList = getObligatorySkills(CSkill::Obligatory::MAJOR); + std::set schoolList = getObligatorySkills(CSkill::Obligatory::MINOR); - std::vector skills; - //picking sec. skills for choice std::set basicAndAdv; - std::set expert; std::set none; + for(int i = 0; i < VLC->skillh->size(); i++) if (canLearnSkill(SecondarySkill(i))) none.insert(SecondarySkill(i)); @@ -1319,58 +1297,56 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills() { if(elem.second < MasteryLevel::EXPERT) basicAndAdv.insert(elem.first); - else - expert.insert(elem.first); none.erase(elem.first); } - for(const auto & s : obligatorySkills) //don't duplicate them - { - none.erase (s); - basicAndAdv.erase (s); - expert.erase (s); - } - //first offered skill: - // 1) give obligatory skill - // 2) give any other new skill - // 3) upgrade existing - if(canLearnSkill() && !obligatorySkills.empty()) - { - skills.push_back (obligatorySkills[0]); - } - else if(!none.empty() && canLearnSkill()) //hero have free skill slot - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //new skill - none.erase(skills.back()); - } - else if(!basicAndAdv.empty()) - { - skills.push_back(type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand)); //upgrade existing - basicAndAdv.erase(skills.back()); - } + bool wantsWisdom = skillsInfo.wisdomCounter + 1 >= maxlevelsToWisdom(); + bool wantsSchool = skillsInfo.magicSchoolCounter + 1 >= maxlevelsToMagicSchool(); - //second offered skill: - //1) upgrade existing - //2) give obligatory skill - //3) give any other new skill - if(!basicAndAdv.empty()) - { - SecondarySkill s = type->heroClass->chooseSecSkill(basicAndAdv, skillsInfo.rand);//upgrade existing - skills.push_back(s); - basicAndAdv.erase(s); - } - else if (canLearnSkill() && obligatorySkills.size() > 1) - { - skills.push_back (obligatorySkills[1]); - } - else if(!none.empty() && canLearnSkill()) - { - skills.push_back(type->heroClass->chooseSecSkill(none, skillsInfo.rand)); //give new skill - none.erase(skills.back()); - } + std::vector skills; + + auto chooseSkill = [&](std::set & options) + { + bool selectWisdom = wantsWisdom && !intersect(options, wisdomList).empty(); + bool selectSchool = wantsSchool && !intersect(options, schoolList).empty(); + SecondarySkill selection; + + if (selectWisdom) + selection = type->heroClass->chooseSecSkill(intersect(options, wisdomList), rand); + else if (selectSchool) + selection = type->heroClass->chooseSecSkill(intersect(options, schoolList), rand); + else + selection = type->heroClass->chooseSecSkill(options, rand); + + skills.push_back(selection); + options.erase(selection); + + if (wisdomList.count(selection)) + wisdomList.clear(); + + if (schoolList.count(selection)) + schoolList.clear(); + }; + + if (!basicAndAdv.empty()) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty()) + chooseSkill(none); + + if (!basicAndAdv.empty() && skills.size() < 2) + chooseSkill(basicAndAdv); + + if (canLearnSkill() && !none.empty() && skills.size() < 2) + chooseSkill(none); + + if (skills.empty()) + logGlobal->info("Selecting secondary skills: Nothing to select!"); + if (skills.size() == 1) + logGlobal->info("Selecting secondary skills: %s (wisdom: %d, schools: %d)!", skills[0], skillsInfo.wisdomCounter, skillsInfo.magicSchoolCounter); + if (skills.size() == 2) + logGlobal->info("Selecting secondary skills: %s or %s (wisdom: %d, schools: %d)!", skills[0], skills[1], int(skillsInfo.wisdomCounter), int(skillsInfo.magicSchoolCounter)); - if (skills.size() == 2) // Fix for #1868 to avoid changing logic (possibly causing bugs in process) - std::swap(skills[0], skills[1]); return skills; } @@ -1406,7 +1382,7 @@ std::optional CGHeroInstance::nextSecondarySkill(CRandomGenerato assert(gainsLevel()); std::optional chosenSecondarySkill; - const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + const auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); if(!proposedSecondarySkills.empty()) { std::vector learnedSecondarySkills; @@ -1474,8 +1450,9 @@ void CGHeroInstance::levelUp(const std::vector & skills) ++level; //deterministic secondary skills - skillsInfo.magicSchoolCounter = (skillsInfo.magicSchoolCounter + 1) % maxlevelsToMagicSchool(); - skillsInfo.wisdomCounter = (skillsInfo.wisdomCounter + 1) % maxlevelsToWisdom(); + ++skillsInfo.magicSchoolCounter; + ++skillsInfo.wisdomCounter; + for(const auto & skill : skills) { if((*VLC->skillh)[skill]->obligatory(CSkill::Obligatory::MAJOR)) @@ -1495,7 +1472,7 @@ void CGHeroInstance::levelUpAutomatically(CRandomGenerator & rand) const auto primarySkill = nextPrimarySkill(rand); setPrimarySkill(primarySkill, 1, false); - auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(); + auto proposedSecondarySkills = getLevelUpProposedSecondarySkills(rand); const auto secondarySkill = nextSecondarySkill(rand); if(secondarySkill) diff --git a/lib/mapObjects/CGHeroInstance.h b/lib/mapObjects/CGHeroInstance.h index 1349b4687..208ea27be 100644 --- a/lib/mapObjects/CGHeroInstance.h +++ b/lib/mapObjects/CGHeroInstance.h @@ -111,9 +111,6 @@ public: struct DLL_LINKAGE SecondarySkillsInfo { - //skills are determined, initialized at map start - //FIXME remove mutable - mutable CRandomGenerator rand; ui8 magicSchoolCounter; ui8 wisdomCounter; @@ -126,7 +123,6 @@ public: { h & magicSchoolCounter; h & wisdomCounter; - h & rand; } } skillsInfo; @@ -194,7 +190,7 @@ public: std::optional nextSecondarySkill(CRandomGenerator & rand) const; /// Gets 0, 1 or 2 secondary skills which are proposed on hero level up. - std::vector getLevelUpProposedSecondarySkills() const; + std::vector getLevelUpProposedSecondarySkills(CRandomGenerator & rand) const; ui8 getSecSkillLevel(const SecondarySkill & skill) const; //0 - no skill diff --git a/lib/networkPacks/NetPackVisitor.h b/lib/networkPacks/NetPackVisitor.h index 72d8dce9d..05df2897a 100644 --- a/lib/networkPacks/NetPackVisitor.h +++ b/lib/networkPacks/NetPackVisitor.h @@ -87,7 +87,6 @@ public: virtual void visitInfoWindow(InfoWindow & pack) {} virtual void visitSetObjectProperty(SetObjectProperty & pack) {} virtual void visitChangeObjectVisitors(ChangeObjectVisitors & pack) {} - virtual void visitPrepareHeroLevelUp(PrepareHeroLevelUp & pack) {} virtual void visitHeroLevelUp(HeroLevelUp & pack) {} virtual void visitCommanderLevelUp(CommanderLevelUp & pack) {} virtual void visitBlockingDialog(BlockingDialog & pack) {} diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 8faa7044a..c1afbbdd0 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -380,11 +380,6 @@ void ChangeObjectVisitors::visitTyped(ICPackVisitor & visitor) visitor.visitChangeObjectVisitors(*this); } -void PrepareHeroLevelUp::visitTyped(ICPackVisitor & visitor) -{ - visitor.visitPrepareHeroLevelUp(*this); -} - void HeroLevelUp::visitTyped(ICPackVisitor & visitor) { visitor.visitHeroLevelUp(*this); @@ -2082,23 +2077,6 @@ void SetObjectProperty::applyGs(CGameState * gs) const } } -void PrepareHeroLevelUp::applyGs(CGameState * gs) -{ - auto * hero = gs->getHero(heroId); - assert(hero); - - auto proposedSkills = hero->getLevelUpProposedSecondarySkills(); - - if(skills.size() == 1 || hero->tempOwner == PlayerColor::NEUTRAL) //choose skill automatically - { - skills.push_back(*RandomGeneratorUtil::nextItem(proposedSkills, hero->skillsInfo.rand)); - } - else - { - skills = proposedSkills; - } -} - void HeroLevelUp::applyGs(CGameState * gs) const { auto * hero = gs->getHero(heroId); diff --git a/lib/networkPacks/PacksForClient.h b/lib/networkPacks/PacksForClient.h index ef7376ad5..9462179f6 100644 --- a/lib/networkPacks/PacksForClient.h +++ b/lib/networkPacks/PacksForClient.h @@ -1276,23 +1276,6 @@ struct DLL_LINKAGE ChangeObjectVisitors : public CPackForClient } }; -struct DLL_LINKAGE PrepareHeroLevelUp : public CPackForClient -{ - ObjectInstanceID heroId; - - /// Do not serialize, used by server only - std::vector skills; - - void applyGs(CGameState * gs); - - void visitTyped(ICPackVisitor & visitor) override; - - template void serialize(Handler & h, const int version) - { - h & heroId; - } -}; - struct DLL_LINKAGE HeroLevelUp : public Query { PlayerColor player; diff --git a/lib/registerTypes/RegisterTypesClientPacks.h b/lib/registerTypes/RegisterTypesClientPacks.h index 7779c618a..5ba7c3165 100644 --- a/lib/registerTypes/RegisterTypesClientPacks.h +++ b/lib/registerTypes/RegisterTypesClientPacks.h @@ -68,7 +68,6 @@ void registerTypesClientPacks(Serializer &s) s.template registerType(); s.template registerType(); s.template registerType(); - s.template registerType(); s.template registerType(); s.template registerType(); s.template registerType(); diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 5bdba02b1..65593b21a 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -187,25 +187,21 @@ void CGameHandler::levelUpHero(const CGHeroInstance * hero) sps.val = 1; sendAndApply(&sps); - PrepareHeroLevelUp pre; - pre.heroId = hero->id; - sendAndApply(&pre); - HeroLevelUp hlu; hlu.player = hero->tempOwner; hlu.heroId = hero->id; hlu.primskill = primarySkill; - hlu.skills = pre.skills; + hlu.skills = hero->getLevelUpProposedSecondarySkills(heroPool->getHeroSkillsRandomGenerator(hero->getHeroType())); if (hlu.skills.size() == 0) { sendAndApply(&hlu); levelUpHero(hero); } - else if (hlu.skills.size() == 1) + else if (hlu.skills.size() == 1 || !hero->getOwner().isValidPlayer()) { sendAndApply(&hlu); - levelUpHero(hero, pre.skills.front()); + levelUpHero(hero, hlu.skills.front()); } else if (hlu.skills.size() > 1) { @@ -551,6 +547,9 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack for (auto & elem : gs->players) turnOrder->addPlayer(elem.first); + for (auto & elem : gs->map->allHeroes) + heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + reinitScripting(); } diff --git a/server/processors/HeroPoolProcessor.cpp b/server/processors/HeroPoolProcessor.cpp index eeb2ed73d..39569a470 100644 --- a/server/processors/HeroPoolProcessor.cpp +++ b/server/processors/HeroPoolProcessor.cpp @@ -351,6 +351,17 @@ CGHeroInstance * HeroPoolProcessor::pickHeroFor(bool isNative, const PlayerColor return *RandomGeneratorUtil::nextItem(possibleHeroes, getRandomGenerator(player)); } +CRandomGenerator & HeroPoolProcessor::getHeroSkillsRandomGenerator(const HeroTypeID & hero) +{ + if (heroSeed.count(hero) == 0) + { + int seed = gameHandler->getRandomGenerator().nextInt(); + heroSeed.emplace(hero, std::make_unique(seed)); + } + + return *heroSeed.at(hero); +} + CRandomGenerator & HeroPoolProcessor::getRandomGenerator(const PlayerColor & player) { if (playerSeed.count(player) == 0) diff --git a/server/processors/HeroPoolProcessor.h b/server/processors/HeroPoolProcessor.h index 3cf48ca5f..eed300a79 100644 --- a/server/processors/HeroPoolProcessor.h +++ b/server/processors/HeroPoolProcessor.h @@ -29,6 +29,9 @@ class HeroPoolProcessor : boost::noncopyable /// per-player random generators std::map> playerSeed; + /// per-hero random generators used to randomize skills + std::map> heroSeed; + void clearHeroFromSlot(const PlayerColor & color, TavernHeroSlot slot); void selectNewHeroForSlot(const PlayerColor & color, TavernHeroSlot slot, bool needNativeHero, bool giveStartingArmy); @@ -54,6 +57,8 @@ public: void onNewWeek(const PlayerColor & color); + CRandomGenerator & getHeroSkillsRandomGenerator(const HeroTypeID & hero); + /// Incoming net pack handling bool hireHero(const ObjectInstanceID & objectID, const HeroTypeID & hid, const PlayerColor & player); @@ -61,5 +66,6 @@ public: { // h & gameHandler; // FIXME: make this work instead of using deserializationFix in gameHandler h & playerSeed; + h & heroSeed; } }; From 71825fcc4eeed0f616661ec4f309df48af78b440 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 19:56:55 +0200 Subject: [PATCH 1163/1248] Fix crash on artifact movement --- lib/mapObjects/CGHeroInstance.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 0ea8bb52d..11698ea0b 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -329,7 +329,6 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if(!getArt(ArtifactPosition::SPELLBOOK) && type->haveSpellBook) { auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::SPELLBOOK); - cb->gameState()->map->addNewArtifactInstance(artifact); putArtifact(ArtifactPosition::SPELLBOOK, artifact); } } @@ -339,7 +338,6 @@ void CGHeroInstance::initHero(CRandomGenerator & rand) if(!getArt(ArtifactPosition::MACH4)) { auto artifact = ArtifactUtils::createNewArtifactInstance(ArtifactID::CATAPULT); - cb->gameState()->map->addNewArtifactInstance(artifact); putArtifact(ArtifactPosition::MACH4, artifact); //everyone has a catapult } @@ -458,7 +456,6 @@ void CGHeroInstance::initArmy(CRandomGenerator & rand, IArmyDescriptor * dst) if(!getArt(slot)) { auto artifact = ArtifactUtils::createNewArtifactInstance(aid); - cb->gameState()->map->addNewArtifactInstance(artifact); putArtifact(slot, artifact); } else From 842d66afbcbcfb9b041fed4453a9418d80a17a47 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 19:57:07 +0200 Subject: [PATCH 1164/1248] Fix crash on battlefield deserialization --- lib/constants/EntityIdentifiers.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/constants/EntityIdentifiers.cpp b/lib/constants/EntityIdentifiers.cpp index d119c0f76..60cf811c2 100644 --- a/lib/constants/EntityIdentifiers.cpp +++ b/lib/constants/EntityIdentifiers.cpp @@ -371,7 +371,7 @@ si32 BattleField::decode(const std::string & identifier) std::string BattleField::encode(const si32 index) { - return VLC->spells()->getByIndex(index)->getJsonKey(); + return VLC->battlefields()->getByIndex(index)->getJsonKey(); } std::string SpellID::entityType() From a4d5c4917b61c78a5459585906cc3f0efd75e3a3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 20 Nov 2023 20:50:37 +0200 Subject: [PATCH 1165/1248] Limit resources to 1000 000 000 to prevent overflow --- lib/constants/NumericConstants.h | 1 + lib/networkPacks/NetPacksLib.cpp | 2 ++ 2 files changed, 3 insertions(+) diff --git a/lib/constants/NumericConstants.h b/lib/constants/NumericConstants.h index 9d61935ff..fcc3da876 100644 --- a/lib/constants/NumericConstants.h +++ b/lib/constants/NumericConstants.h @@ -50,6 +50,7 @@ namespace GameConstants constexpr int CREATURES_COUNT = 197; constexpr ui32 BASE_MOVEMENT_COST = 100; //default cost for non-diagonal movement + constexpr int64_t PLAYER_RESOURCES_CAP = 1000 * 1000 * 1000; } VCMI_LIB_NAMESPACE_END diff --git a/lib/networkPacks/NetPacksLib.cpp b/lib/networkPacks/NetPacksLib.cpp index 8faa7044a..20ebdd086 100644 --- a/lib/networkPacks/NetPacksLib.cpp +++ b/lib/networkPacks/NetPacksLib.cpp @@ -797,6 +797,7 @@ void SetResources::applyGs(CGameState * gs) const gs->getPlayerState(player)->resources = res; else gs->getPlayerState(player)->resources += res; + gs->getPlayerState(player)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); //just ensure that player resources are not negative //server is responsible to check if player can afford deal @@ -2022,6 +2023,7 @@ void NewTurn::applyGs(CGameState *gs) { assert(re.first.isValidPlayer()); gs->getPlayerState(re.first)->resources = re.second; + gs->getPlayerState(re.first)->resources.amin(GameConstants::PLAYER_RESOURCES_CAP); } for(const auto & creatureSet : cres) //set available creatures in towns From d58ea6f28d4018652afc221fce130b8964ef4ad2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 16:38:09 +0200 Subject: [PATCH 1166/1248] Fix keymaster visitation status tracking --- lib/mapObjects/CQuest.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mapObjects/CQuest.cpp b/lib/mapObjects/CQuest.cpp index 944efa994..3f4275a3a 100644 --- a/lib/mapObjects/CQuest.cpp +++ b/lib/mapObjects/CQuest.cpp @@ -778,7 +778,7 @@ void CGQuestGuard::serializeJsonOptions(JsonSerializeFormat & handler) bool CGKeys::wasMyColorVisited(const PlayerColor & player) const { - return cb->getPlayerState(player)->visitedObjectsGlobal.count({ID, subID}) != 0; + return cb->getPlayerState(player)->visitedObjectsGlobal.count({Obj::KEYMASTER, subID}) != 0; } std::string CGKeys::getHoverText(PlayerColor player) const From 83ca09f48379a5eaad3b293ead40c8134402a283 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 17:23:46 +0200 Subject: [PATCH 1167/1248] Fix loading of allowed heroes from h3m maps --- lib/mapping/MapFormatH3M.cpp | 7 +------ lib/mapping/MapReaderH3M.cpp | 2 ++ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 0edefb82c..8387885b0 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -678,8 +678,6 @@ void CMapLoaderH3M::readTeamInfo() void CMapLoaderH3M::readAllowedHeroes() { - mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); - if(features.levelHOTA0) reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); else @@ -749,8 +747,6 @@ void CMapLoaderH3M::readMapOptions() void CMapLoaderH3M::readAllowedArtifacts() { - map->allowedArtifact = VLC->arth->getDefaultAllowed(); - if(features.levelAB) { if(features.levelHOTA0) @@ -1827,9 +1823,8 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec logGlobal->debug("Hero %s subID=%d has spells set twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTextID(), object->subID); } - object->spells.insert(SpellID::PRESET); //placeholder "preset spells" - reader->readBitmaskSpells(object->spells, false); + object->spells.insert(SpellID::PRESET); //placeholder "preset spells" } } else if(features.levelAB) diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index e0ee0a83c..5626b53d5 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -325,6 +325,8 @@ void MapReaderH3M::readBitmaskSkills(std::set & dest, bool inver template void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) { + dest.clear(); + for(int byte = 0; byte < bytesToRead; ++byte) { const ui8 mask = reader->readUInt8(); From d0e320723c62c2bb1c5415c7fbbc0eb3f790810f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 18:31:43 +0200 Subject: [PATCH 1168/1248] Remove no longer relevant assertion --- lib/serializer/JsonSerializer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/serializer/JsonSerializer.cpp b/lib/serializer/JsonSerializer.cpp index aa1553e3a..430a6809a 100644 --- a/lib/serializer/JsonSerializer.cpp +++ b/lib/serializer/JsonSerializer.cpp @@ -97,7 +97,6 @@ void JsonSerializer::serializeInternal(int64_t & value) void JsonSerializer::serializeLIC(const std::string & fieldName, const TDecoder & decoder, const TEncoder & encoder, const std::set & standard, std::set & value) { - assert(standard.size() == value.size()); if(standard == value) return; From e96cde291a961e632ffab09c40fbc551c6749c58 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 18:32:07 +0200 Subject: [PATCH 1169/1248] Fix generation of additional monolith types for RMG --- .../CObjectClassesHandler.cpp | 14 ++++++++++---- lib/mapObjectConstructors/CObjectClassesHandler.h | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.cpp b/lib/mapObjectConstructors/CObjectClassesHandler.cpp index ad23e9fcb..928a72a29 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.cpp +++ b/lib/mapObjectConstructors/CObjectClassesHandler.cpp @@ -36,6 +36,7 @@ #include "../mapObjects/CGTownInstance.h" #include "../modding/IdentifierStorage.h" #include "../modding/CModHandler.h" +#include "../modding/ModScope.h" VCMI_LIB_NAMESPACE_BEGIN @@ -267,6 +268,10 @@ ObjectClass * CObjectClassesHandler::loadFromJson(const std::string & scope, con else loadSubObject(subData.second.meta, subData.first, subData.second, obj); } + + if (obj->id == MapObjectID::MONOLITH_TWO_WAY) + generateExtraMonolithsForRMG(obj); + return obj; } @@ -413,14 +418,12 @@ void CObjectClassesHandler::afterLoadFinalization() logGlobal->warn("No templates found for %s:%s", entry->getJsonKey(), obj->getJsonKey()); } } - - generateExtraMonolithsForRMG(); } -void CObjectClassesHandler::generateExtraMonolithsForRMG() +void CObjectClassesHandler::generateExtraMonolithsForRMG(ObjectClass * container) { //duplicate existing two-way portals to make reserve for RMG - auto& portalVec = objects[Obj::MONOLITH_TWO_WAY]->objects; + auto& portalVec = container->objects; //FIXME: Monoliths in this vector can be already not useful for every terrain const size_t portalCount = portalVec.size(); @@ -449,7 +452,10 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG() newPortal->type = portal->getIndex(); newPortal->subtype = portalVec.size(); //indexes must be unique, they are returned as a set + portalVec.push_back(newPortal); + + registerObject(ModScope::scopeGame(), container->getJsonKey(), newPortal->subTypeName, newPortal->subtype); } } diff --git a/lib/mapObjectConstructors/CObjectClassesHandler.h b/lib/mapObjectConstructors/CObjectClassesHandler.h index 8e8b2ae28..173fe8748 100644 --- a/lib/mapObjectConstructors/CObjectClassesHandler.h +++ b/lib/mapObjectConstructors/CObjectClassesHandler.h @@ -84,7 +84,7 @@ class DLL_LINKAGE CObjectClassesHandler : public IHandlerBase ObjectClass * loadFromJson(const std::string & scope, const JsonNode & json, const std::string & name, size_t index); - void generateExtraMonolithsForRMG(); + void generateExtraMonolithsForRMG(ObjectClass * container); public: CObjectClassesHandler(); From 07e7d6cc14ca5192791a7fc4b6fb0cbcb3c59e3f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 20:32:07 +0200 Subject: [PATCH 1170/1248] Do not allow fleeing from cities without fort --- lib/battle/CBattleInfoEssentials.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/battle/CBattleInfoEssentials.cpp b/lib/battle/CBattleInfoEssentials.cpp index 2b69caa29..b7761a6f8 100644 --- a/lib/battle/CBattleInfoEssentials.cpp +++ b/lib/battle/CBattleInfoEssentials.cpp @@ -282,7 +282,7 @@ bool CBattleInfoEssentials::battleCanFlee(const PlayerColor & player) const return false; //we are besieged defender - if(side == BattleSide::DEFENDER && battleGetSiegeLevel()) + if(side == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr) { const auto * town = battleGetDefendedTown(); if(!town->hasBuilt(BuildingSubID::ESCAPE_TUNNEL)) @@ -357,7 +357,7 @@ bool CBattleInfoEssentials::battleCanSurrender(const PlayerColor & player) const const auto side = playerToSide(player); if(!side) return false; - bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && battleGetSiegeLevel()); + bool iAmSiegeDefender = (side.value() == BattleSide::DEFENDER && getBattle()->getDefendedTown() != nullptr); //conditions like for fleeing (except escape tunnel presence) + enemy must have a hero return battleCanFlee(player) && !iAmSiegeDefender && battleHasHero(otherSide(side.value())); } From 9d298f42c3d57652f4a236741f090f2bbd396bc3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 21:22:17 +0200 Subject: [PATCH 1171/1248] Do not allow special spells such as Titan Bolt as a reward --- lib/JsonRandom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/JsonRandom.cpp b/lib/JsonRandom.cpp index 69d0636f4..f99a678fb 100644 --- a/lib/JsonRandom.cpp +++ b/lib/JsonRandom.cpp @@ -406,7 +406,7 @@ namespace JsonRandom { std::set defaultSpells; for(const auto & spell : VLC->spellh->objects) - if (IObjectInterface::cb->isAllowed(spell->getId())) + if (IObjectInterface::cb->isAllowed(spell->getId()) && !spell->isSpecial()) defaultSpells.insert(spell->getId()); std::set potentialPicks = filterKeys(value, defaultSpells, variables); From 26d22d0d9d714ffeff383acf744199e794a3093d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 21 Nov 2023 21:24:06 +0200 Subject: [PATCH 1172/1248] Fix guards composition of Pyramids --- config/objects/creatureBanks.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/config/objects/creatureBanks.json b/config/objects/creatureBanks.json index 2e117356c..cfcaf2a7a 100644 --- a/config/objects/creatureBanks.json +++ b/config/objects/creatureBanks.json @@ -1055,9 +1055,8 @@ { "chance": 100, "guards": [ - { "amount": 20, "type": "goldGolem" }, + { "amount": 40, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" }, - { "amount": 20, "type": "goldGolem" }, { "amount": 10, "type": "diamondGolem" } ], "combat_value": 786, From e979fb7056d65dde867f23fee7ec97652b646f17 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 22 Nov 2023 17:09:42 +0200 Subject: [PATCH 1173/1248] Better fix for allowed heroes in h3m - allow all heroes from mods --- lib/mapping/CMap.cpp | 2 +- lib/mapping/MapFormatH3M.cpp | 6 ++++-- lib/mapping/MapReaderH3M.cpp | 4 ++-- server/CGameHandler.cpp | 5 ++++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index d161eb773..92893cdbf 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -166,7 +166,7 @@ CMap::CMap() , grailRadius(0) , uidCounter(0) { - allHeroes.resize(allowedHeroes.size()); + allHeroes.resize(VLC->heroh->objects.size()); allowedAbilities = VLC->skillh->getDefaultAllowed(); allowedArtifact = VLC->arth->getDefaultAllowed(); allowedSpells = VLC->spellh->getDefaultAllowed(); diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 8387885b0..d99ffb630 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -108,8 +108,6 @@ void CMapLoaderH3M::init() inputStream->seek(0); readHeader(); - map->allHeroes.resize(map->allowedHeroes.size()); - readDisposedHeroes(); readMapOptions(); readAllowedArtifacts(); @@ -678,6 +676,8 @@ void CMapLoaderH3M::readTeamInfo() void CMapLoaderH3M::readAllowedHeroes() { + mapHeader->allowedHeroes = VLC->heroh->getDefaultAllowed(); + if(features.levelHOTA0) reader->readBitmaskHeroesSized(mapHeader->allowedHeroes, false); else @@ -747,6 +747,8 @@ void CMapLoaderH3M::readMapOptions() void CMapLoaderH3M::readAllowedArtifacts() { + map->allowedArtifact = VLC->arth->getDefaultAllowed(); + if(features.levelAB) { if(features.levelHOTA0) diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 5626b53d5..4fb3917d2 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -325,8 +325,6 @@ void MapReaderH3M::readBitmaskSkills(std::set & dest, bool inver template void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int objectsToRead, bool invert) { - dest.clear(); - for(int byte = 0; byte < bytesToRead; ++byte) { const ui8 mask = reader->readUInt8(); @@ -343,6 +341,8 @@ void MapReaderH3M::readBitmask(std::set & dest, int bytesToRead, int if (result) dest.insert(vcmiID); + else + dest.erase(vcmiID); } } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 65593b21a..4d989108f 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -548,7 +548,10 @@ void CGameHandler::init(StartInfo *si, Load::ProgressAccumulator & progressTrack turnOrder->addPlayer(elem.first); for (auto & elem : gs->map->allHeroes) - heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + { + if(elem) + heroPool->getHeroSkillsRandomGenerator(elem->getHeroType()); // init RMG seed + } reinitScripting(); } From 364977ef4081a273f1a4832cd571864cf0922131 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 22 Nov 2023 19:11:40 +0200 Subject: [PATCH 1174/1248] More meaningful exception messages --- lib/IHandlerBase.h | 2 +- lib/mapObjects/CGTownInstance.cpp | 2 +- lib/mapObjects/MiscObjects.cpp | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/IHandlerBase.h b/lib/IHandlerBase.h index 615e23f54..8bf602887 100644 --- a/lib/IHandlerBase.h +++ b/lib/IHandlerBase.h @@ -116,7 +116,7 @@ public: if(index < 0 || index >= objects.size()) { logMod->error("%s id %d is invalid", getTypeNames()[0], index); - throw std::runtime_error("internal error"); + throw std::runtime_error("Attempt to access invalid index " + std::to_string(index) + " of type " + getTypeNames().front()); } return objects[index]; diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index f7b5081f5..9813c4a58 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -1079,7 +1079,7 @@ void CGTownInstance::addHeroToStructureVisitors(const CGHeroInstance *h, si64 st { //should never ever happen logGlobal->error("Cannot add hero %s to visitors of structure # %d", h->getNameTranslated(), structureInstanceID); - throw std::runtime_error("internal error"); + throw std::runtime_error("unexpected hero in CGTownInstance::addHeroToStructureVisitors"); } } diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index c3369810b..fa89b76d7 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -1239,7 +1239,7 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier) if(progress > obeliskCount) { logGlobal->error("Visited %d of %d", static_cast(progress), obeliskCount); - throw std::runtime_error("internal error"); + throw std::runtime_error("Player visited more obelisks than present on map!"); } break; From 1fc9cc9e1b0dc2974f8c64292a65b00380662134 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 24 Nov 2023 22:11:13 +0200 Subject: [PATCH 1175/1248] Initial version of 1.4.0 changelog --- ChangeLog.md | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index 82f214f5e..8cea085d3 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,182 @@ +# 1.3.2 -> 1.4.0 + +### General +* Implemented High Score screen +* Implemented tracking of completed campaigns +* "Secret" Heroes 3 campaigns now require completion of prerequisite campaigns first +* Completing a campaign will now return player to campaign selection window instead of main menu +* Game will now play video on winning or losing a game +* Game will now correctly check for mod compatibility when loading saved games +* Game client will no longer load conflicting mods if player have both of them enabled +* If some mods fail to load due to missing dependencies or conflicts, game client will display message on opening main menu +* Game will no longer crash on loading save with different mod versions and will show error message instead +* Saved games are now 2-3 time smaller than before +* Added Vietnamese translation +* Failure to connect to a MP game will now show proper error message +* Added VSync support +* Implemented tutorial +* Slightly reduced server connection time +* Implemented support for playback of audio from video files +* Windows Installer will now automatically add required firewall rules +* Game audio will now be disabled if game window is not focused +* Fixed formtting of date and time of a savegame on Android +* Added list of vcmi authors to Credits screen +* Quick combat is now disabled by default +* Spectator mode in single player is now disabled + +### Multiplayer +* Implemented simultaneous turns +* Implemented turn timers, including chess timers version +* Game will now hide entire adventure map on hotseat turn transfer +* Added option to pause game timer while on system options window +* Implemented localization support for maps * game will now use texts from local player instead of host +* Multiple fixes to validation of player requests by server + +### Android +* Heroes 3 data import now accepts files in any case +* Fixed detection of Heroes 3 data presence when 'data' directory uses lower case + +### Touchscreen +* Added tutorial video clips that explain supported touch gestures +* Double tap will now be correctly interpreted as double click, e.g. to start scenario via double-click +* Implemented snapping to 100% scale for adventure map zooming +* Implemented smooth scrolling for adventure map +* Implemented radial wheel to reorder list of owned towns and heroes +* Implemented radial wheel for hero exchange in towns + +### Launcher +* When mod is being downloaded, launcher will now correctly show progress as well as total size of a mod +* Double-clicking mod name will now perform expected action, e.g. install/update/enable or disable +* Launcher will now show mod extraction progress instead of freezing +* "Friendly AI" option will now correctly display current type of friendly AI +* Player can now correctly switch to global chat after disconnect +* "Resolve mods conflicts" button now attempts to fix all mods if nothing is selected +* Implemented support for mention in game lobby +* Implemented support for global and room channels in game lobby +* Added option to reconnect to game lobby + +### Editor +* It is now possible to configure rewards for Seer Hut, Pandora Boxes and Events +* It is now possible to configure quest (limiter) in Seer Hut and Quest Guards +* It is now possible to configure events and rumors in map editor +* Improved army configuration interface +* Added option to customize hero skills +* It is now possible to select object on map for win/loss conditions or for main town +* Random dwellings can now be linked to a random town +* Added map editor zoom +* Added objects lock functionality +* It is now possible to configure hero placeholders in map editor +* Fixed duplicate artifact image on mouse drag +* Lasso tool will no longer skip tiles +* Fixed layout of roads and rivers + +### Stability +* Fix possible crash on generating random map +* Fixed multiple memory leaks in game client +* Fixed crash on casting Hypnotize multiple times +* Fixed crash on attempting to move all artifacts from hero that has no artifacts +* Fixed crash on attempting to load corrupted .def file +* Fixed crash on clicking on empty Altar of Sacrifice slots + +### AI +* BattleAI should now see strong stacks even if blocked by weak stacks. +* BattleAI will now prefers targets slower than own stack even if they are not reachable this turn. +* Improved BattleAI performance when selecting spell to cast +* Improved BattleAI performance when selection unit action +* Improved BattleAI spell selection logic +* Nullkiller AI can now use Fly and Water Walk spells + +### Campaigns +* Implemented voiceover audio support for Heroes 3 campaigns +* Fixes victory condition on 1st scenario of "Long Live the King" campaign +* Fixed loading of defeat/victory icon and message for some campaign scenarios + +### Interface +* Implemented adventure map dimming on opening windows +* Clicking town hall icon on town screen will now open town hall +* Clicking buildings in town hall will now show which resources are missing (if any) +* Fixed incorrect positioning of total experience text on Altart of Sacrifice +* Game will now show correct video file on battle end +* Game will now correctly loop battle end animation video +* Implemented larger version of spellbooks that displays up to 24 spells at once +* Spell scrolls in hero inventory now show icon of contained spell +* Fixed incorrect hero morale tooltip after visiting adventure map objects +* Fixed incorrect information for skills in hero exchange window + +### Main Menu +* Implemented window for quick selection of starting hero, town and bonus +* Implemented map preview in scenario selection and game load screen accessible via right click on map +* Added support for folders in scenario selection and save/load screens +* Added support for "Show Random Maps" button in random map setup screen +* Added starting hero preview screen +* Added option to change name of player while in map setup screen +* Implemeted loading screen with progress bar +* Game will now stay on loading screen while random map generation is in process +* Team Alignments popup in scenario options will no longer show empty teams +* Fixed missing borders on team alignments configuration window in random maps +* Fixed map difficulty icon on save/load screen +* Main menu animation will no longer appear on top of new game / load game text + +### Adventure Map Interface +* Picking up an artifact on adventure map will now show artifact assembly dialog if such option exists +* Minimap will now preserve correct aspect ratio on rectangular maps +* Fixed slot highlighting when an artifact is being assembled +* Ctrl-click on hero will now select him instead of changing path of active hero +* In selection windows (level up window, treasure chest pickup, etc) it is now possible to select reward via double-click +* Attacking wandering monsters with preconfigured message will now correctly show the message +* Revisit object button will now be blocked if there is no object to revisit +* Fixed missing tooltip for "revisit object" button +* Fixed calculation of fow reveal range for all objects +* Attempt to close game will now ask for confirmation +* Right-clicking previously visited Seer Huts or Quest Guards will show icon with preview of quest goal +* Right-clicking owned dwellings will show amount of creatures available to for recruitment +* Right-clicking previously visited creature banks will show exact guards composition with their portraits +* Right-clicking artifacts on map will show artifact description +* Right-clicking objects that give bonus to hero will show object description + +### Mechanics +* Heroes in tavern will correctly lose effects from spells or visited objects on new day +* Fixed multiple bugs in offering of Wisdom and Spell Schools on levelup. Mechanic should now work identically to Heroes 3 +* Retreated heroes will no longer restore their entire mana pool on new day +* Fixed Grail in Crypt on some custom maps +* Added support for repeateable quests in Seer Huts +* Using "Sacrifice All" on Altar will now correctly place all creatures but one on altar +* Fixed probabilities of luck and morale +* Blinded stack no longer can get morale +* Creature that attacks while standing in moat will now correctly receive moat damage +* Player resources are now limited to 1 000 000 000 to prevent overflow +* It is no longer possible to escape from town without fort + +### Random Maps Generator +* Increased tolerance for placement of Subterranean Gates +* Game will now select random object template out of available options instead of picking first one +* It is no longer possible to create map with a single team +* Fixed interface no displaying correct random map settings in some cases +* Fixed game failing to generate random map if number of AI players is set to non-zero +* Fixed misleading error "no info for player X found" +* Fixed bug leading to AI players defeated on day one. + +### Modding +* All bonuses now require string as a subtype. See documentation for exact list of possible strings for each bonus. +* Changes to existing objects parameters in mods will now be applied to ongoing saves +* Fixed handling of engine version compatibility check +* Added support for giving arbitrary bonuses to AI players +* Most of mods of type "Translation" are now hidden in Launcher +* Added new mod type: "Compatibility". Mods of this type are hidden in Launcher and are always active if they are compatible. +* Added new mod type: "Maps" +* Added new TERRAIN_NATIVE bonus that makes any terrain native to affected units +* SPELL_DURATION now allows subtypes. If set to spell, bonus will only affect specified spell +* Both game client and launcher will now correctly handle dependencies that are not in lower case +* Implemented support for refusable Witch Hut and Scholar +* Added "variables" to configurable objects that are shared between all rewards +* Added visit mode "limiter" for configurable objects. Hero will be considered as "visited this object" if he fulfills provided condition +* Added option to customize text displayed for visited objects, e.g. show "Already learned" instead of "Visited" +* Added option to define custom description of configurable object, accessible via right-click +* Added option to show object content icons on right-click +* Object lnow allows checking whether hero can learn spell +* Object limiter now allows checking whether hero can learn skill +* Object reward may now reveal terrain around visiting hero (e.g. Redwood Observatory) + # 1.3.1 -> 1.3.2 ### GENERAL From 1954447a9e0760bae092988d7fd89528d390fa67 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 25 Nov 2023 02:58:06 +0100 Subject: [PATCH 1176/1248] fix image order --- client/windows/CCastleInterface.cpp | 5 +++-- client/windows/CKingdomInterface.cpp | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 9757a8fb3..872c1084e 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1329,11 +1329,12 @@ void CCastleInterface::recreateIcons() fort = std::make_shared(122, 413, town, false); fastTownHall = std::make_shared(Point(80, 413), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&](){ builds->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setImageOrder(town->hallLevel(), town->hallLevel(), town->hallLevel(), town->hallLevel()); fastTownHall->setAnimateLonelyFrame(true); + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; fastArmyPurchase = std::make_shared(Point(122, 413), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&](){ builds->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); + fastArmyPurchase->setImageOrder(imageIndex, imageIndex, imageIndex, imageIndex); fastArmyPurchase->setAnimateLonelyFrame(true); fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() diff --git a/client/windows/CKingdomInterface.cpp b/client/windows/CKingdomInterface.cpp index 9410dc5aa..8c661de75 100644 --- a/client/windows/CKingdomInterface.cpp +++ b/client/windows/CKingdomInterface.cpp @@ -822,10 +822,11 @@ CTownItem::CTownItem(const CGTownInstance * Town) } fastTownHall = std::make_shared(Point(69, 31), AnimationPath::builtin("ITMTL.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterTownHall(); }); - fastTownHall->setImageOrder(town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1, town->hallLevel() - 1); + fastTownHall->setImageOrder(town->hallLevel(), town->hallLevel(), town->hallLevel(), town->hallLevel()); fastTownHall->setAnimateLonelyFrame(true); + int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1; fastArmyPurchase = std::make_shared(Point(111, 31), AnimationPath::builtin("itmcl.def"), CButton::tooltip(), [&]() { std::make_shared(town)->enterToTheQuickRecruitmentWindow(); }); - fastArmyPurchase->setImageOrder(town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1, town->fortLevel() - 1); + fastArmyPurchase->setImageOrder(imageIndex, imageIndex, imageIndex, imageIndex); fastArmyPurchase->setAnimateLonelyFrame(true); fastTavern = std::make_shared(Rect(5, 6, 58, 64), [&]() { From 2eada0efca784655818353c590d0172722c37560 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 25 Nov 2023 03:14:31 +0100 Subject: [PATCH 1177/1248] allow shortcout if any town has a marketplace --- client/windows/CCastleInterface.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 872c1084e..106de45be 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -1339,11 +1339,16 @@ void CCastleInterface::recreateIcons() fastMarket = std::make_shared(Rect(163, 410, 64, 42), [&]() { - if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + std::vector towns = LOCPLINT->cb->getTownsInfo(true); + for(auto & town : towns) { - if (town->getOwner() == LOCPLINT->playerID) - GH.windows().createAndPushWindow(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE); + if(town->builtBuildings.count(BuildingID::MARKETPLACE)) + { + GH.windows().createAndPushWindow(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE); + return; + } } + LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket")); }); fastTavern = std::make_shared(Rect(15, 387, 58, 64), [&]() From 856ce6c6a28da12a1a6cdccf191796505e35206f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sat, 25 Nov 2023 18:41:49 +0200 Subject: [PATCH 1178/1248] Apply suggestions from code review Co-authored-by: Alexander Wilms Co-authored-by: Laserlicht <13953785+Laserlicht@users.noreply.github.com> --- ChangeLog.md | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 8cea085d3..a4bc35f70 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,17 +10,16 @@ * Game client will no longer load conflicting mods if player have both of them enabled * If some mods fail to load due to missing dependencies or conflicts, game client will display message on opening main menu * Game will no longer crash on loading save with different mod versions and will show error message instead -* Saved games are now 2-3 time smaller than before +* Saved games are now 2-3 times smaller than before * Added Vietnamese translation * Failure to connect to a MP game will now show proper error message * Added VSync support * Implemented tutorial -* Slightly reduced server connection time * Implemented support for playback of audio from video files * Windows Installer will now automatically add required firewall rules * Game audio will now be disabled if game window is not focused -* Fixed formtting of date and time of a savegame on Android -* Added list of vcmi authors to Credits screen +* Fixed formatting of date and time of a savegame on Android +* Added list of VCMI authors to credits screen * Quick combat is now disabled by default * Spectator mode in single player is now disabled @@ -29,7 +28,8 @@ * Implemented turn timers, including chess timers version * Game will now hide entire adventure map on hotseat turn transfer * Added option to pause game timer while on system options window -* Implemented localization support for maps * game will now use texts from local player instead of host +* Implemented localization support for maps +* Game will now use texts from local player instead of host * Multiple fixes to validation of player requests by server ### Android @@ -45,7 +45,7 @@ * Implemented radial wheel for hero exchange in towns ### Launcher -* When mod is being downloaded, launcher will now correctly show progress as well as total size of a mod +* When a mod is being downloaded, the launcher will now correctly show progress as well as its total size * Double-clicking mod name will now perform expected action, e.g. install/update/enable or disable * Launcher will now show mod extraction progress instead of freezing * "Friendly AI" option will now correctly display current type of friendly AI @@ -87,7 +87,7 @@ * Nullkiller AI can now use Fly and Water Walk spells ### Campaigns -* Implemented voiceover audio support for Heroes 3 campaigns +* Implemented voice-over audio support for Heroes 3 campaigns * Fixes victory condition on 1st scenario of "Long Live the King" campaign * Fixed loading of defeat/victory icon and message for some campaign scenarios @@ -95,7 +95,7 @@ * Implemented adventure map dimming on opening windows * Clicking town hall icon on town screen will now open town hall * Clicking buildings in town hall will now show which resources are missing (if any) -* Fixed incorrect positioning of total experience text on Altart of Sacrifice +* Fixed incorrect positioning of total experience text on Altar of Sacrifice * Game will now show correct video file on battle end * Game will now correctly loop battle end animation video * Implemented larger version of spellbooks that displays up to 24 spells at once @@ -106,11 +106,12 @@ ### Main Menu * Implemented window for quick selection of starting hero, town and bonus * Implemented map preview in scenario selection and game load screen accessible via right click on map +* Show exact map size in map selection * Added support for folders in scenario selection and save/load screens * Added support for "Show Random Maps" button in random map setup screen * Added starting hero preview screen * Added option to change name of player while in map setup screen -* Implemeted loading screen with progress bar +* Implemented loading screen with progress bar * Game will now stay on loading screen while random map generation is in process * Team Alignments popup in scenario options will no longer show empty teams * Fixed missing borders on team alignments configuration window in random maps @@ -139,7 +140,7 @@ * Fixed multiple bugs in offering of Wisdom and Spell Schools on levelup. Mechanic should now work identically to Heroes 3 * Retreated heroes will no longer restore their entire mana pool on new day * Fixed Grail in Crypt on some custom maps -* Added support for repeateable quests in Seer Huts +* Added support for repeatable quests in Seer Huts * Using "Sacrifice All" on Altar will now correctly place all creatures but one on altar * Fixed probabilities of luck and morale * Blinded stack no longer can get morale @@ -161,7 +162,7 @@ * Changes to existing objects parameters in mods will now be applied to ongoing saves * Fixed handling of engine version compatibility check * Added support for giving arbitrary bonuses to AI players -* Most of mods of type "Translation" are now hidden in Launcher +* Most mods of type "Translation" are now hidden in Launcher * Added new mod type: "Compatibility". Mods of this type are hidden in Launcher and are always active if they are compatible. * Added new mod type: "Maps" * Added new TERRAIN_NATIVE bonus that makes any terrain native to affected units @@ -173,7 +174,7 @@ * Added option to customize text displayed for visited objects, e.g. show "Already learned" instead of "Visited" * Added option to define custom description of configurable object, accessible via right-click * Added option to show object content icons on right-click -* Object lnow allows checking whether hero can learn spell +* Object now allows checking whether hero can learn spell * Object limiter now allows checking whether hero can learn skill * Object reward may now reveal terrain around visiting hero (e.g. Redwood Observatory) From 3f60c1b0b3a7aa2cc56da65aaf357b7cd19c9f63 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 00:41:15 +0200 Subject: [PATCH 1179/1248] Remove excessive logging --- lib/mapObjects/CGHeroInstance.cpp | 7 ------- 1 file changed, 7 deletions(-) diff --git a/lib/mapObjects/CGHeroInstance.cpp b/lib/mapObjects/CGHeroInstance.cpp index 3031e1651..4037cec39 100644 --- a/lib/mapObjects/CGHeroInstance.cpp +++ b/lib/mapObjects/CGHeroInstance.cpp @@ -1337,13 +1337,6 @@ std::vector CGHeroInstance::getLevelUpProposedSecondarySkills(CR if (canLearnSkill() && !none.empty() && skills.size() < 2) chooseSkill(none); - if (skills.empty()) - logGlobal->info("Selecting secondary skills: Nothing to select!"); - if (skills.size() == 1) - logGlobal->info("Selecting secondary skills: %s (wisdom: %d, schools: %d)!", skills[0], skillsInfo.wisdomCounter, skillsInfo.magicSchoolCounter); - if (skills.size() == 2) - logGlobal->info("Selecting secondary skills: %s or %s (wisdom: %d, schools: %d)!", skills[0], skills[1], int(skillsInfo.wisdomCounter), int(skillsInfo.magicSchoolCounter)); - return skills; } From 6730ec64d5186f11ed2eb38f41783ddef81e9a6d Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 00:41:49 +0200 Subject: [PATCH 1180/1248] Fix loading of preset artifacts and creatures on some custom maps --- lib/mapObjects/CGCreature.cpp | 8 ++++++-- lib/mapObjects/MiscObjects.cpp | 9 ++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index f76915582..ca661dc8e 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -215,8 +215,12 @@ void CGCreature::pickRandomObject(CRandomGenerator & rand) subID = VLC->creh->pickRandomMonster(rand, 7); break; } - ID = MapObjectID::MONSTER; - setType(ID, subID); + + if (ID != MapObjectID::MONSTER) + { + ID = MapObjectID::MONSTER; + setType(ID, subID); + } } void CGCreature::initObj(CRandomGenerator & rand) diff --git a/lib/mapObjects/MiscObjects.cpp b/lib/mapObjects/MiscObjects.cpp index fa89b76d7..11c532f13 100644 --- a/lib/mapObjects/MiscObjects.cpp +++ b/lib/mapObjects/MiscObjects.cpp @@ -739,10 +739,13 @@ void CGArtifact::pickRandomObject(CRandomGenerator & rand) break; } - if (ID != Obj::SPELL_SCROLL) + if (ID != MapObjectID::SPELL_SCROLL && ID != MapObjectID::ARTIFACT) + { + ID = MapObjectID::ARTIFACT; + setType(ID, subID); + } + else if (ID != MapObjectID::SPELL_SCROLL) ID = MapObjectID::ARTIFACT; - - setType(ID, subID); } void CGArtifact::initObj(CRandomGenerator & rand) From 4b57b9f9a9a1e3420e5064f422ea773ecaad350b Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 26 Nov 2023 18:39:51 +0100 Subject: [PATCH 1181/1248] Display disabled connect button on server autojoin --- client/mainmenu/CMainMenu.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/mainmenu/CMainMenu.cpp b/client/mainmenu/CMainMenu.cpp index 4f579fb41..6e9cfdada 100644 --- a/client/mainmenu/CMainMenu.cpp +++ b/client/mainmenu/CMainMenu.cpp @@ -528,9 +528,11 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) textTitle = std::make_shared("", Rect(20, 20, 205, 50), 0, FONT_BIG, ETextAlignment::CENTER, Colors::WHITE); inputAddress = std::make_shared(Rect(25, 68, 175, 16), background->getSurface()); inputPort = std::make_shared(Rect(25, 115, 175, 16), background->getSurface()); + buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); if(host && !settings["session"]["donotstartserver"].Bool()) { textTitle->setText(CGI->generaltexth->translate("vcmi.mainMenu.serverConnecting")); + buttonOk->block(true); startConnectThread(); } else @@ -539,8 +541,6 @@ CSimpleJoinScreen::CSimpleJoinScreen(bool host) inputAddress->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->cb += std::bind(&CSimpleJoinScreen::onChange, this, _1); inputPort->filters += std::bind(&CTextInput::numberFilter, _1, _2, 0, 65535); - buttonOk = std::make_shared(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CSimpleJoinScreen::connectToServer, this), EShortcut::GLOBAL_ACCEPT); - inputAddress->giveFocus(); } inputAddress->setText(host ? CServerHandler::localhostAddress : CSH->getHostAddress(), true); From a4d76b25296b09e488afbf073856803d7bf4fb86 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sun, 26 Nov 2023 19:16:53 +0100 Subject: [PATCH 1182/1248] Split large spellbook from UI enhancements setting --- Mods/vcmi/config/vcmi/english.json | 4 +++- client/windows/CSpellWindow.cpp | 4 ++-- client/windows/settings/GeneralOptionsTab.cpp | 9 +++++++++ config/schemas/settings.json | 7 ++++++- config/widgets/settings/generalOptionsTab.json | 8 ++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index d5063016a..d50199528 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -111,7 +111,9 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptic feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Enhancements", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a larger spell book, backpack, etc. Disable to have a more classic experience.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Enhancements}\n\nToggle various quality of life interface improvements. Such as a backpack button etc. Disable to have a more classic experience.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Large Spell Book", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Large Spell Book}\n\nEnables larger spell book that fits more spells per page. Spell book page change animation does not work with this setting enabled.", "vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel", "vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.", diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index a7f18b776..cffa7144f 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -97,13 +97,13 @@ public: } spellsorter; CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED | (settings["general"]["enableUiEnhancements"].Bool() ? BORDERED : 0)), + CWindowObject(PLAYER_COLORED | (settings["general"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(settings["general"]["enableUiEnhancements"].Bool()), + isBigSpellbook(settings["general"]["enableLargeSpellbook"].Bool()), spellsPerPage(24), offL(-11), offR(195), diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 49e490249..76c5e4817 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -162,6 +162,11 @@ GeneralOptionsTab::GeneralOptionsTab() setBoolSetting("general", "enableUiEnhancements", value); }); + addCallback("enableLargeSpellbookChanged", [](bool value) + { + setBoolSetting("general", "enableLargeSpellbook", value); + }); + //moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content addCallback("availableCreaturesAsDwellingChanged", [=](int value) { @@ -206,6 +211,10 @@ GeneralOptionsTab::GeneralOptionsTab() if (enableUiEnhancementsCheckbox) enableUiEnhancementsCheckbox->setSelected(settings["general"]["enableUiEnhancements"].Bool()); + std::shared_ptr enableLargeSpellbookCheckbox = widget("enableLargeSpellbookCheckbox"); + if (enableLargeSpellbookCheckbox) + enableLargeSpellbookCheckbox->setSelected(settings["general"]["enableLargeSpellbook"].Bool()); + std::shared_ptr musicSlider = widget("musicSlider"); musicSlider->scrollTo(CCS->musich->getVolume()); diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 5e7e5956d..034a84a4e 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -39,7 +39,8 @@ "useSavePrefix", "savePrefix", "startTurnAutosave", - "enableUiEnhancements" + "enableUiEnhancements", + "enableLargeSpellbook" ], "properties" : { "playerName" : { @@ -131,6 +132,10 @@ "enableUiEnhancements" : { "type": "boolean", "default": true + }, + "enableLargeSpellbook" : { + "type": "boolean", + "default": false } } }, diff --git a/config/widgets/settings/generalOptionsTab.json b/config/widgets/settings/generalOptionsTab.json index e34131c6a..41d3b57a8 100644 --- a/config/widgets/settings/generalOptionsTab.json +++ b/config/widgets/settings/generalOptionsTab.json @@ -50,6 +50,9 @@ { "text": "vcmi.systemOptions.framerateButton.hover" }, + { + "text": "vcmi.systemOptions.enableLargeSpellbookButton.hover" + }, { "text": "core.genrltxt.577" }, @@ -102,6 +105,11 @@ "help": "vcmi.systemOptions.framerateButton", "callback": "framerateChanged" }, + { + "name": "enableLargeSpellbookCheckbox", + "help": "vcmi.systemOptions.enableLargeSpellbookButton", + "callback": "enableLargeSpellbookChanged" + }, { "name": "spellbookAnimationCheckbox", "help": "core.help.364", From 2c8d0338baf00e1c2d281d93ee36cd00c9045f9b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:53:34 +0200 Subject: [PATCH 1183/1248] Fix error messages in log on map start --- lib/MetaString.cpp | 7 +++++-- lib/mapObjects/CGTownInstance.cpp | 5 +++++ lib/mapObjects/CGTownInstance.h | 1 + lib/mapObjects/CQuest.h | 4 +++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/lib/MetaString.cpp b/lib/MetaString.cpp index 3da43f519..8fdb6d3a0 100644 --- a/lib/MetaString.cpp +++ b/lib/MetaString.cpp @@ -51,8 +51,11 @@ void MetaString::appendRawString(const std::string & value) void MetaString::appendTextID(const std::string & value) { - message.push_back(EMessage::APPEND_TEXTID_STRING); - stringsTextID.push_back(value); + if (!value.empty()) + { + message.push_back(EMessage::APPEND_TEXTID_STRING); + stringsTextID.push_back(value); + } } void MetaString::appendNumber(int64_t value) diff --git a/lib/mapObjects/CGTownInstance.cpp b/lib/mapObjects/CGTownInstance.cpp index 9813c4a58..ab3fe7cd7 100644 --- a/lib/mapObjects/CGTownInstance.cpp +++ b/lib/mapObjects/CGTownInstance.cpp @@ -947,6 +947,11 @@ std::string CGTownInstance::getNameTranslated() const return VLC->generaltexth->translate(nameTextId); } +std::string CGTownInstance::getNameTextID() const +{ + return nameTextId; +} + void CGTownInstance::setNameTextId( const std::string & newName ) { nameTextId = newName; diff --git a/lib/mapObjects/CGTownInstance.h b/lib/mapObjects/CGTownInstance.h index ceca1e89c..0bc2976ba 100644 --- a/lib/mapObjects/CGTownInstance.h +++ b/lib/mapObjects/CGTownInstance.h @@ -137,6 +137,7 @@ public: const CArmedInstance *getUpperArmy() const; //garrisoned hero if present or the town itself std::string getNameTranslated() const; + std::string getNameTextID() const; void setNameTextId(const std::string & newName); ////////////////////////////////////////////////////////////////////////// diff --git a/lib/mapObjects/CQuest.h b/lib/mapObjects/CQuest.h index 644059aa6..cb3046a22 100644 --- a/lib/mapObjects/CQuest.h +++ b/lib/mapObjects/CQuest.h @@ -45,7 +45,9 @@ public: std::string heroName; //backup of hero name HeroTypeID heroPortrait; - MetaString firstVisitText, nextVisitText, completedText; + MetaString firstVisitText; + MetaString nextVisitText; + MetaString completedText; bool isCustomFirst; bool isCustomNext; bool isCustomComplete; From 60ca6f968c842af87e09594346b030cbb9423a2e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:53:46 +0200 Subject: [PATCH 1184/1248] Fix hero limit check in AI --- AI/Nullkiller/Analyzers/HeroManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/AI/Nullkiller/Analyzers/HeroManager.cpp b/AI/Nullkiller/Analyzers/HeroManager.cpp index bd43d4df3..4131182cb 100644 --- a/AI/Nullkiller/Analyzers/HeroManager.cpp +++ b/AI/Nullkiller/Analyzers/HeroManager.cpp @@ -187,7 +187,8 @@ bool HeroManager::heroCapReached() const int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned); return heroCount >= ALLOWED_ROAMING_HEROES - || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP) + || heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP); } float HeroManager::getMagicStrength(const CGHeroInstance * hero) const From 560a1231a6fd673d634a9c965434311baf4b74cb Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:54:45 +0200 Subject: [PATCH 1185/1248] Fix loading of maps with Grail or Diplomat Suit placed on map --- lib/CArtHandler.cpp | 3 --- lib/mapping/MapFeaturesH3M.cpp | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/CArtHandler.cpp b/lib/CArtHandler.cpp index 0854429b8..64bbe63d7 100644 --- a/lib/CArtHandler.cpp +++ b/lib/CArtHandler.cpp @@ -474,9 +474,6 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode // Necessary for objects added via mods that don't have any templates in H3 VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->addTemplate(templ); } - // object does not have any templates - this is not usable object (e.g. pseudo-art like lock) - if(VLC->objtypeh->getHandlerFor(Obj::ARTIFACT, art->getIndex())->getTemplates().empty()) - VLC->objtypeh->removeSubObject(Obj::ARTIFACT, art->getIndex()); }); return art; diff --git a/lib/mapping/MapFeaturesH3M.cpp b/lib/mapping/MapFeaturesH3M.cpp index 4152e16cd..99bda2b09 100644 --- a/lib/mapping/MapFeaturesH3M.cpp +++ b/lib/mapping/MapFeaturesH3M.cpp @@ -97,7 +97,7 @@ MapFormatFeaturesH3M MapFormatFeaturesH3M::getFeaturesSOD() MapFormatFeaturesH3M result = getFeaturesAB(); result.levelSOD = true; - result.artifactsCount = 141; // + Combined artifacts + result.artifactsCount = 144; // + Combined artifacts + 3 unfinished artifacts (required for some maps) result.artifactsBytes = 18; result.heroesPortraitsCount = 163; // +Finneas +young Gem +young Sandro +young Yog From 81a48f2d80908c439afcadb239f024acb90cc083 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:55:50 +0200 Subject: [PATCH 1186/1248] Do not attempt to resize text box to zero-width --- client/widgets/TextControls.cpp | 2 ++ client/windows/CMessage.cpp | 4 ++++ client/windows/InfoWindows.cpp | 4 +++- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/client/widgets/TextControls.cpp b/client/widgets/TextControls.cpp index 7785fca2a..0bc99bab7 100644 --- a/client/widgets/TextControls.cpp +++ b/client/widgets/TextControls.cpp @@ -376,6 +376,7 @@ void CTextBox::setText(const std::string & text) { // decrease width again if slider still used label->pos.w = pos.w - 32; + assert(label->pos.w > 0); label->setText(text); slider->setAmount(label->textSize.y); } @@ -383,6 +384,7 @@ void CTextBox::setText(const std::string & text) { // create slider and update widget label->pos.w = pos.w - 32; + assert(label->pos.w > 0); label->setText(text); OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE); diff --git a/client/windows/CMessage.cpp b/client/windows/CMessage.cpp index 0684b4b14..5484a9425 100644 --- a/client/windows/CMessage.cpp +++ b/client/windows/CMessage.cpp @@ -120,6 +120,10 @@ SDL_Surface * CMessage::drawDialogBox(int w, int h, PlayerColor playerColor) std::vector CMessage::breakText( std::string text, size_t maxLineWidth, EFonts font ) { + assert(maxLineWidth != 0); + if (maxLineWidth == 0) + return { text }; + std::vector ret; boost::algorithm::trim_right_if(text,boost::algorithm::is_any_of(std::string(" "))); diff --git a/client/windows/InfoWindows.cpp b/client/windows/InfoWindows.cpp index 320aabef8..bc55dbb72 100644 --- a/client/windows/InfoWindows.cpp +++ b/client/windows/InfoWindows.cpp @@ -150,7 +150,9 @@ CInfoWindow::CInfoWindow(std::string Text, PlayerColor player, const TCompsInfo text = std::make_shared(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE); if(!text->slider) { - text->resize(text->label->textSize); + int finalWidth = std::min(250, text->label->textSize.x + 32); + int finalHeight = text->label->textSize.y; + text->resize(Point(finalWidth, finalHeight)); } if(buttons.size() == 1) From 68de34e5089cc37182c8444edd82679b1244f892 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:56:26 +0200 Subject: [PATCH 1187/1248] Workaround for starting map with invalid witch huts --- lib/mapping/MapFormatH3M.cpp | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index d99ffb630..846cf6126 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -1162,18 +1162,24 @@ CGObjectInstance * CMapLoaderH3M::readWitchHut(const int3 & position, std::share allowedAbilities.insert(SecondarySkill(skillID)); } - JsonVector anyOfList; - - for (auto const & skill : allowedAbilities) - { - JsonNode entry; - entry.String() = VLC->skills()->getById(skill)->getJsonKey(); - anyOfList.push_back(entry); - } JsonNode variable; - variable["anyOf"].Vector() = anyOfList; - variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods + if (allowedAbilities.size() == 1) + { + variable.String() = VLC->skills()->getById(*allowedAbilities.begin())->getJsonKey(); + } + else + { + JsonVector anyOfList; + for (auto const & skill : allowedAbilities) + { + JsonNode entry; + entry.String() = VLC->skills()->getById(skill)->getJsonKey(); + anyOfList.push_back(entry); + } + variable["anyOf"].Vector() = anyOfList; + } + variable.setMeta(ModScope::scopeGame()); // list may include skills from all mods rewardable->configuration.presetVariable("secondarySkill", "gainedSkill", variable); } return object; From e00d871082bef6cb8febd44409f6e35bcc701b9a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 18:56:48 +0200 Subject: [PATCH 1188/1248] Fix few more warnings on game start --- lib/gameState/CGameState.cpp | 8 +------- lib/mapping/MapFormatH3M.cpp | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/lib/gameState/CGameState.cpp b/lib/gameState/CGameState.cpp index 7c251a286..519b23e8f 100644 --- a/lib/gameState/CGameState.cpp +++ b/lib/gameState/CGameState.cpp @@ -784,7 +784,7 @@ void CGameState::initTowns() CGTownInstance * vti =(elem); assert(vti->town); - if(vti->getNameTranslated().empty()) + if(vti->getNameTextID().empty()) { size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1); vti->setNameTextId(vti->getTown()->getRandomNameTextID(nameID)); @@ -1797,12 +1797,6 @@ void CGameState::buildBonusSystemTree() { t->deserializationFix(); } - // CStackInstance <-> CCreature, CStackInstance <-> CArmedInstance, CArtifactInstance <-> CArtifact - // are provided on initializing / deserializing - - // NOTE: calling deserializationFix() might be more correct option, but might lead to side effects - for (auto hero : map->heroesOnMap) - hero->boatDeserializationFix(); } void CGameState::deserializationFix() diff --git a/lib/mapping/MapFormatH3M.cpp b/lib/mapping/MapFormatH3M.cpp index 846cf6126..2d99c745f 100644 --- a/lib/mapping/MapFormatH3M.cpp +++ b/lib/mapping/MapFormatH3M.cpp @@ -171,7 +171,7 @@ void CMapLoaderH3M::readHeader() identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_IN_THE_WAKE_OF_GODS)); if (features.levelHOTA0) identifierMapper.loadMapping(VLC->settings()->getValue(EGameSettings::MAP_FORMAT_HORN_OF_THE_ABYSS)); - + reader->setIdentifierRemapper(identifierMapper); // include basic mod @@ -898,7 +898,7 @@ void CMapLoaderH3M::loadArtifactsOfHero(CGHeroInstance * hero) if(!hero->artifactsWorn.empty() || !hero->artifactsInBackpack.empty()) { - logGlobal->debug("Hero %s at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getNameTranslated(), hero->pos.toString()); + logGlobal->debug("Hero %d at %s has set artifacts twice (in map properties and on adventure map instance). Using the latter set...", hero->getHeroType().getNum(), hero->pos.toString()); hero->artifactsInBackpack.clear(); while(!hero->artifactsWorn.empty()) @@ -1035,7 +1035,7 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi readMessageAndGuards(object->message, object, mapPosition); Rewardable::VisitInfo vinfo; auto & reward = vinfo.reward; - + reward.heroExperience = reader->readUInt32(); reward.manaDiff = reader->readInt32(); if(auto val = reader->readUInt8()) @@ -1052,7 +1052,7 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi { auto rId = reader->readSkill(); auto rVal = reader->readUInt8(); - + reward.secondary[rId] = rVal; } int gart = reader->readUInt8(); //number of gained artifacts @@ -1068,13 +1068,13 @@ void CMapLoaderH3M::readBoxContent(CGPandoraBox * object, const int3 & mapPositi { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); - + reward.creatures.emplace_back(rId, rVal); } - + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; object->configuration.info.push_back(vinfo); - + reader->skipZero(8); } @@ -1852,7 +1852,7 @@ CGObjectInstance * CMapLoaderH3M::readHero(const int3 & mapPosition, const Objec auto ps = object->getAllBonuses(Selector::type()(BonusType::PRIMARY_SKILL).And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL)), nullptr); if(ps->size()) { - logGlobal->debug("Hero %s subID=%d has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getNameTranslated(), object->subID ); + logGlobal->debug("Hero %s has set primary skills twice (in map properties and on adventure map instance). Using the latter set...", object->getHeroType().getNum() ); for(const auto & b : *ps) object->removeBonus(b); } @@ -2008,7 +2008,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { auto rId = reader->readUInt8(); auto rVal = reader->readUInt8(); - + reward.primary.at(rId) = rVal; break; } @@ -2016,7 +2016,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { auto rId = reader->readSkill(); auto rVal = reader->readUInt8(); - + reward.secondary[rId] = rVal; break; } @@ -2034,7 +2034,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con { auto rId = reader->readCreature(); auto rVal = reader->readUInt16(); - + reward.creatures.emplace_back(rId, rVal); break; } @@ -2043,7 +2043,7 @@ void CMapLoaderH3M::readSeerHutQuest(CGSeerHut * hut, const int3 & position, con assert(0); } } - + vinfo.visitType = Rewardable::EEventType::EVENT_FIRST_VISIT; hut->configuration.info.push_back(vinfo); } From 0cf490e2c3b2046c64a24be2326c6f2d8b503a9e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 19:20:05 +0200 Subject: [PATCH 1189/1248] Remove custom move constructor. Fixes serialization of visitation status --- lib/CPlayerState.cpp | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/lib/CPlayerState.cpp b/lib/CPlayerState.cpp index b59de08fa..9cb13671a 100644 --- a/lib/CPlayerState.cpp +++ b/lib/CPlayerState.cpp @@ -23,25 +23,7 @@ PlayerState::PlayerState() setNodeType(PLAYER); } -PlayerState::PlayerState(PlayerState && other) noexcept: - CBonusSystemNode(std::move(other)), - color(other.color), - human(other.human), - team(other.team), - resources(other.resources), - cheated(other.cheated), - enteredWinningCheatCode(other.enteredWinningCheatCode), - enteredLosingCheatCode(other.enteredLosingCheatCode), - status(other.status), - daysWithoutCastle(other.daysWithoutCastle) -{ - std::swap(visitedObjects, other.visitedObjects); - std::swap(heroes, other.heroes); - std::swap(towns, other.towns); - std::swap(dwellings, other.dwellings); - std::swap(quests, other.quests); - std::swap(battleBonuses, other.battleBonuses); -} +PlayerState::PlayerState(PlayerState && other) noexcept = default; PlayerState::~PlayerState() = default; From 344d8891e46b9d81a7c3c1a299025688aea5607a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 19:33:46 +0200 Subject: [PATCH 1190/1248] Fix display of creatures to sell in Freelancer Guild --- client/windows/CTradeWindow.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/client/windows/CTradeWindow.cpp b/client/windows/CTradeWindow.cpp index 4b0bf9ccd..d5a728dcb 100644 --- a/client/windows/CTradeWindow.cpp +++ b/client/windows/CTradeWindow.cpp @@ -137,7 +137,23 @@ std::vector *CTradeWindow::getItemsIds(bool Left) { std::vector *ids = nullptr; - if(!Left) + if(Left) + { + switch(itemsType[1]) + { + case CREATURE: + ids = new std::vector; + for(int i = 0; i < 7; i++) + { + if(const CCreature *c = hero->getCreature(SlotID(i))) + ids->push_back(c->getId()); + else + ids->push_back(-1); + } + break; + } + } + else { switch(itemsType[0]) { From d06ae15c6e53b6e8ff3631a99015b4611142290c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 26 Nov 2023 19:38:37 +0200 Subject: [PATCH 1191/1248] Fix teleporting animation --- client/battle/BattleStacksController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleStacksController.cpp b/client/battle/BattleStacksController.cpp index 1609745eb..85454fb83 100644 --- a/client/battle/BattleStacksController.cpp +++ b/client/battle/BattleStacksController.cpp @@ -534,7 +534,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vectorhasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementFlying))) + if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, BonusCustomSubtype::movementTeleporting))) { owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]() { From f2e7791326602b6225e9c514c76fccddbd8234e9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 13:25:49 +0200 Subject: [PATCH 1192/1248] Fix crash on Earthquake cast --- lib/spells/effects/Catapult.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/spells/effects/Catapult.cpp b/lib/spells/effects/Catapult.cpp index 74d0cb332..7b189d52b 100644 --- a/lib/spells/effects/Catapult.cpp +++ b/lib/spells/effects/Catapult.cpp @@ -72,6 +72,7 @@ void Catapult::applyMassive(ServerCallback * server, const Mechanics * m) const return; CatapultAttack ca; + ca.battleID = m->battle()->getBattle()->getBattleID(); ca.attacker = m->caster->getHeroCaster() ? -1 : m->caster->getCasterUnitId(); for(int i = 0; i < targetsToAttack; i++) From e21e3f83868de02a0b21c32378ca192b0cdbe622 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 13:26:16 +0200 Subject: [PATCH 1193/1248] Do not display "Spell Cancelled" message on right-click when no stack is active --- client/battle/BattleActionsController.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/battle/BattleActionsController.cpp b/client/battle/BattleActionsController.cpp index f3adbe037..25fd62093 100644 --- a/client/battle/BattleActionsController.cpp +++ b/client/battle/BattleActionsController.cpp @@ -999,7 +999,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex) return action.spellcast(); }; - bool isCurrentStackInSpellcastMode = std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); + bool isCurrentStackInSpellcastMode = !possibleActions.empty() && std::all_of(possibleActions.begin(), possibleActions.end(), spellcastActionPredicate); if (spellcastingModeActive() || isCurrentStackInSpellcastMode) { From ee46fc806a131872332609b2b067082b893c91f9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 13:47:10 +0200 Subject: [PATCH 1194/1248] Add workaround to loading old map format --- lib/mapping/MapFormatJson.cpp | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index c8f3a476a..7f6059400 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -158,20 +158,35 @@ namespace TriggeredEventsDetail { case EventCondition::HAVE_ARTIFACT: case EventCondition::TRANSPORT: - event.objectType = ArtifactID(ArtifactID::decode(data["type"].String())); + if (data["type"].isNumber()) // compatibility + event.objectType = ArtifactID(data["type"].Integer()); + else + event.objectType = ArtifactID(ArtifactID::decode(data["type"].String())); break; case EventCondition::HAVE_CREATURES: - event.objectType = CreatureID(CreatureID::decode(data["type"].String())); + if (data["type"].isNumber()) // compatibility + event.objectType = CreatureID(data["type"].Integer()); + else + event.objectType = CreatureID(CreatureID::decode(data["type"].String())); break; case EventCondition::HAVE_RESOURCES: - event.objectType = GameResID(GameResID::decode(data["type"].String())); + if (data["type"].isNumber()) // compatibility + event.objectType = GameResID(data["type"].Integer()); + else + event.objectType = GameResID(GameResID::decode(data["type"].String())); break; case EventCondition::HAVE_BUILDING: - event.objectType = BuildingID(BuildingID::decode(data["type"].String())); + if (data["type"].isNumber()) // compatibility + event.objectType = BuildingID(data["type"].Integer()); + else + event.objectType = BuildingID(BuildingID::decode(data["type"].String())); break; case EventCondition::CONTROL: case EventCondition::DESTROY: - event.objectType = MapObjectID(MapObjectID::decode(data["type"].String())); + if (data["type"].isNumber()) // compatibility + event.objectType = MapObjectID(data["type"].Integer()); + else + event.objectType = MapObjectID(MapObjectID::decode(data["type"].String())); break; } From 5d16f035d7216c94348a5c8aae6d0d07c67b70f9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 13:49:45 +0200 Subject: [PATCH 1195/1248] Workaround for crash on winning the game --- lib/serializer/Connection.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index c712c383b..c4c98f4f6 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -222,7 +222,13 @@ int CConnection::read(void * data, unsigned size) CConnection::~CConnection() { if(handler) - handler->join(); + { + // ugly workaround to avoid self-join if last strong reference to shared_ptr that owns this class has been released in this very thread, e.g. on netpack processing + if (boost::this_thread::get_id() != handler->get_id()) + handler->join(); + else + handler->detach(); + } close(); } From d5f8db2222286ae68ab02be90d6d442a1dde4b46 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 14:04:42 +0200 Subject: [PATCH 1196/1248] Fixed undefined behavior on accessing array with -1 index --- client/windows/CSpellWindow.cpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index a7f18b776..b0d6748dd 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -648,7 +648,14 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell) { OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE); - schoolBorder = std::make_shared(owner->schoolBorders[owner->selectedTab >= 4 ? whichSchool.getNum() : owner->selectedTab], schoolLevel); + schoolBorder.reset(); + if (owner->selectedTab >= 4) + { + if (whichSchool.getNum() != SpellSchool()) + schoolBorder = std::make_shared(owner->schoolBorders.at(whichSchool.getNum()), schoolLevel); + } + else + schoolBorder = std::make_shared(owner->schoolBorders.at(owner->selectedTab), schoolLevel); } ColorRGBA firstLineColor, secondLineColor; From b8a40913561cf8d415c34af7d9312c96b2300e1b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 19:57:48 +0200 Subject: [PATCH 1197/1248] Fix regressions from timers - client may be destroyed while receiving timer update or pack reply --- client/CServerHandler.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 4909ecf82..2def4c5fa 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -50,6 +50,7 @@ #include "../lib/registerTypes/RegisterTypesLobbyPacks.h" #include "../lib/serializer/Connection.h" #include "../lib/serializer/CMemorySerializer.h" +#include "../lib/UnlockGuard.h" #include #include @@ -417,8 +418,12 @@ void CServerHandler::sendClientDisconnecting() } sendLobbyPack(lcd); - c->close(); - c.reset(); + { + // Network thread might be applying network pack at this moment + auto unlockInterface = vstd::makeUnlockGuard(GH.interfaceMutex); + c->close(); + c.reset(); + } } void CServerHandler::setCampaignState(std::shared_ptr newCampaign) @@ -665,9 +670,6 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta void CServerHandler::endGameplay(bool closeConnection, bool restart) { - client->endGame(); - vstd::clear_pointer(client); - if(closeConnection) { // Game is ending @@ -675,6 +677,10 @@ void CServerHandler::endGameplay(bool closeConnection, bool restart) CSH->sendClientDisconnecting(); logNetwork->info("Closed connection."); } + + client->endGame(); + vstd::clear_pointer(client); + if(!restart) { if(CMM) From 5df98f3eca5124cea3461b40747ff8cca1dcd5fe Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 20:06:02 +0200 Subject: [PATCH 1198/1248] Allow timer pause pack on opening settings menu in battle --- server/queries/BattleQueries.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/queries/BattleQueries.cpp b/server/queries/BattleQueries.cpp index a49e55be3..a377e6c1b 100644 --- a/server/queries/BattleQueries.cpp +++ b/server/queries/BattleQueries.cpp @@ -18,6 +18,7 @@ #include "../../lib/battle/IBattleState.h" #include "../../lib/mapObjects/CGObjectInstance.h" #include "../../lib/networkPacks/PacksForServer.h" +#include "../../lib/serializer/Cast.h" void CBattleQuery::notifyObjectAboutRemoval(const CObjectVisitQuery & objectVisit) const { @@ -46,8 +47,13 @@ CBattleQuery::CBattleQuery(CGameHandler * owner): bool CBattleQuery::blocksPack(const CPack * pack) const { - const char * name = typeid(*pack).name(); - return strcmp(name, typeid(MakeAction).name()) != 0; + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + if(dynamic_ptr_cast(pack) != nullptr) + return false; + + return true; } void CBattleQuery::onRemoval(PlayerColor color) From 02f719326097779ca6e423a7dc8e771cf80a3f58 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 20:21:18 +0200 Subject: [PATCH 1199/1248] Fix regression from map translation - properly translate custom main hero name --- client/lobby/OptionsTab.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index 65d5cead3..a7f762965 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -190,7 +190,7 @@ std::string OptionsTab::CPlayerSettingsHelper::getName() return CGI->generaltexth->allTexts[522]; if(!playerSettings.heroNameTextId.empty()) - return playerSettings.heroNameTextId; + return CGI->generaltexth->translate(playerSettings.heroNameTextId); auto index = playerSettings.getHeroValidated(); return (*CGI->heroh)[index]->getNameTranslated(); } From 5df53f6ea27a653caf00f973fb860ede295041e3 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 23:20:24 +0200 Subject: [PATCH 1200/1248] Hota compatibility fix - allow stack selection border to have different size than creature icon --- client/widgets/CGarrisonInt.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/client/widgets/CGarrisonInt.cpp b/client/widgets/CGarrisonInt.cpp index 8c7a78697..bf444ede9 100644 --- a/client/widgets/CGarrisonInt.cpp +++ b/client/widgets/CGarrisonInt.cpp @@ -432,6 +432,7 @@ CGarrisonSlot::CGarrisonSlot(CGarrisonInt * Owner, int x, int y, SlotID IID, EGa selectionImage = std::make_shared(graphics->getAnimation(imgName), 1); selectionImage->disable(); + selectionImage->center(creatureImage->pos.center()); if(Owner->smallIcons) { From a7c45d8ec8f1d6c042e96b9981fe7ab8855b1cad Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 23:17:25 +0200 Subject: [PATCH 1201/1248] Remove assert - confirmed to be legal scenario --- client/HeroMovementController.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/HeroMovementController.cpp b/client/HeroMovementController.cpp index 81a625d12..6b1152f60 100644 --- a/client/HeroMovementController.cpp +++ b/client/HeroMovementController.cpp @@ -100,7 +100,8 @@ void HeroMovementController::showTeleportDialog(const CGHeroInstance * hero, Tel } } - assert(0); // exit not found? How? + // may happen when hero has path but does not moves alongside it + // for example, while standing on teleporter set path that does not leads throught teleporter and press space LOCPLINT->cb->selectionMade(-1, askID); return; } From faead7739a485e642a72097bf64314d74772cc3f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 23:18:48 +0200 Subject: [PATCH 1202/1248] Do not allow U-turns while flying. Works fine, but poor representation in UI --- lib/pathfinder/CPathfinder.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 61cf7aa48..5430548f1 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -136,6 +136,9 @@ void CPathfinder::calculatePaths() if(neighbour->locked) continue; + if (source.node->theNodeBefore && source.node->theNodeBefore->coord == neighbour->coord ) + continue; // block U-turns + if(!hlp->isLayerAvailable(neighbour->layer)) continue; From 8cbc2c01ad3031e63f76a85b7dcbdcc302ab5fcc Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Mon, 27 Nov 2023 23:19:19 +0200 Subject: [PATCH 1203/1248] Do not allow moving through most of visitable objects --- lib/pathfinder/CPathfinder.cpp | 38 ++++++++++++++++++++++++++++++++-- lib/pathfinder/CPathfinder.h | 1 + 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index 5430548f1..a713b5bdc 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -24,11 +24,45 @@ VCMI_LIB_NAMESPACE_BEGIN +bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const +{ + // we can always make the first step, even when standing on object + if(source.node->theNodeBefore == nullptr) + return true; + + if (!source.nodeObject) + return true; + + if (!source.isNodeObjectVisitable()) + return true; + + // we can always move from visitable object if hero has teleported here (e.g. went through monolith) + if (source.node->isTeleportAction()) + return true; + + // we can go through garrisons + if (source.nodeObject->ID == MapObjectID::GARRISON || source.nodeObject->ID == MapObjectID::GARRISON2) + return true; + + // or through border gate (if we stand on it then we already have the key) + if (source.nodeObject->ID == MapObjectID::BORDER_GATE) + return true; + + // or "through" boat, but only if this is embarking + if (source.nodeObject->ID == MapObjectID::BOAT && source.node->action == EPathNodeAction::EMBARK) + return true; + + return false; +} + std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const { std::vector neighbourTiles; - neighbourTiles.reserve(8); + if (!canMoveFromNode(source)) + return neighbourTiles; + + neighbourTiles.reserve(8); getNeighbours( *source.tile, source.node->coord, @@ -38,7 +72,7 @@ std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & sour if(source.isNodeObjectVisitable()) { - vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool + vstd::erase_if(neighbourTiles, [&](const int3 & tile) -> bool { return !canMoveBetween(tile, source.nodeObject->visitablePos()); }); diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index af12ca56a..4203c8ba1 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -79,6 +79,7 @@ public: virtual ~CPathfinderHelper(); void initializePatrol(); bool isHeroPatrolLocked() const; + bool canMoveFromNode(const PathNodeInfo & source) const; bool isPatrolMovementAllowed(const int3 & dst) const; void updateTurnInfo(const int turn = 0); bool isLayerAvailable(const EPathfindingLayer & layer) const; From 6cfa21466cc8ee7bfc5d1fb43d874fcc9c83a8ec Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 15:47:55 +0200 Subject: [PATCH 1204/1248] Fixed Gundula specialty: offence specialist, not sorcery --- config/heroes/stronghold.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/config/heroes/stronghold.json b/config/heroes/stronghold.json index e940ef6a8..13238de3d 100644 --- a/config/heroes/stronghold.json +++ b/config/heroes/stronghold.json @@ -237,13 +237,13 @@ ], "specialty" : { "bonuses" : { - "sorcery" : { - "targetSourceType" : "SECONDARY_SKILL", - "type" : "SPELL_DAMAGE", - "subtype" : "spellSchool.any", + "offence" : { + "subtype" : "damageTypeMelee", + "type" : "PERCENTAGE_DAMAGE_BOOST", "updater" : "TIMES_HERO_LEVEL", "val" : 5, - "valueType" : "PERCENT_TO_TARGET_TYPE" + "valueType" : "PERCENT_TO_TARGET_TYPE", + "targetSourceType" : "SECONDARY_SKILL" } } } From d326c53b9a6592e14117cf98f7cf5665a544f2f2 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 15:48:35 +0200 Subject: [PATCH 1205/1248] AI will now respect current value of "Use spells in autocombat" setting --- AI/BattleAI/BattleAI.cpp | 2 +- config/widgets/settings/battleOptionsTab.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/AI/BattleAI/BattleAI.cpp b/AI/BattleAI/BattleAI.cpp index 3a428e122..54870c763 100644 --- a/AI/BattleAI/BattleAI.cpp +++ b/AI/BattleAI/BattleAI.cpp @@ -145,7 +145,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack ) result = evaluator.selectStackAction(stack); - if(!skipCastUntilNextBattle && evaluator.canCastSpell()) + if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) { auto spelCasted = evaluator.attemptCastingSpell(stack); diff --git a/config/widgets/settings/battleOptionsTab.json b/config/widgets/settings/battleOptionsTab.json index 8f47a3fd5..3b5df7a76 100644 --- a/config/widgets/settings/battleOptionsTab.json +++ b/config/widgets/settings/battleOptionsTab.json @@ -69,7 +69,6 @@ [ { "name": "enableAutocombatSpellsCheckbox", - "help": "vcmi.battleOptions.enableAutocombatSpells", "callback": "enableAutocombatSpellsChanged" } ] From 9277761da55385abef8201430fa78b2911b16f78 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 15:49:03 +0200 Subject: [PATCH 1206/1248] Do not allow building boat if there is already boat with hero in it --- lib/mapObjects/IObjectInterface.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/lib/mapObjects/IObjectInterface.cpp b/lib/mapObjects/IObjectInterface.cpp index caaa497b8..eebc08478 100644 --- a/lib/mapObjects/IObjectInterface.cpp +++ b/lib/mapObjects/IObjectInterface.cpp @@ -89,11 +89,23 @@ int3 IBoatGenerator::bestLocation() const int3 targetTile = getObject()->visitablePos() + offset; const TerrainTile *tile = getObject()->cb->getTile(targetTile, false); - if(tile) //tile is in the map + if(!tile) + continue; // tile not visible / outside the map + + if(!tile->terType->isWater()) + continue; + + if (tile->blocked) { - if(tile->terType->isWater() && (!tile->blocked || tile->blockingObjects.front()->ID == Obj::BOAT)) //and is water and is not blocked or is blocked by boat - return targetTile; + bool hasBoat = false; + for (auto const * object : tile->blockingObjects) + if (object->ID == Obj::BOAT || object->ID == Obj::HERO) + hasBoat = true; + + if (!hasBoat) + continue; // tile is blocked, but not by boat -> check next potential position } + return targetTile; } return int3 (-1,-1,-1); } @@ -112,7 +124,7 @@ IBoatGenerator::EGeneratorState IBoatGenerator::shipyardStatus() const if(t->blockingObjects.empty()) return GOOD; //OK - if(t->blockingObjects.front()->ID == Obj::BOAT) + if(t->blockingObjects.front()->ID == Obj::BOAT || t->blockingObjects.front()->ID == Obj::HERO) return BOAT_ALREADY_BUILT; //blocked with boat return TILE_BLOCKED; //blocked From ea8995e901ac1ca7c29007ccaa16297f0b41b1ea Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 16:25:11 +0200 Subject: [PATCH 1207/1248] Fixed check for free slots when recruiting in towns --- client/windows/CCastleInterface.cpp | 4 ++-- client/windows/GUIClasses.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/windows/CCastleInterface.cpp b/client/windows/CCastleInterface.cpp index 106de45be..d501464c2 100644 --- a/client/windows/CCastleInterface.cpp +++ b/client/windows/CCastleInterface.cpp @@ -901,7 +901,7 @@ void CCastleBuildings::enterDwelling(int level) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, -87); + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, -87); } void CCastleBuildings::enterToTheQuickRecruitmentWindow() @@ -1110,7 +1110,7 @@ void CCreaInfo::clickPressed(const Point & cursorPosition) { LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level); }; - GH.windows().createAndPushWindow(town, level, town, recruitCb, nullptr, offset); + GH.windows().createAndPushWindow(town, level, town->getUpperArmy(), recruitCb, nullptr, offset); } std::string CCreaInfo::genGrowthText() diff --git a/client/windows/GUIClasses.cpp b/client/windows/GUIClasses.cpp index 498f4a6d4..d808480d9 100644 --- a/client/windows/GUIClasses.cpp +++ b/client/windows/GUIClasses.cpp @@ -159,7 +159,7 @@ void CRecruitmentWindow::buy() else { std::string txt; - if(dst->ID == Obj::HERO) + if(dwelling->ID != Obj::TOWN) { txt = CGI->generaltexth->allTexts[425]; //The %s would join your hero, but there aren't enough provisions to support them. boost::algorithm::replace_first(txt, "%s", slider->getValue() > 1 ? CGI->creh->objects[crid]->getNamePluralTranslated() : CGI->creh->objects[crid]->getNameSingularTranslated()); From 0d6bd0c60ae6d7d4d6ebc8fa6f143347ef140b05 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 16:37:21 +0200 Subject: [PATCH 1208/1248] Fix assertion failure in RMG --- lib/rmg/CMapGenOptions.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rmg/CMapGenOptions.cpp b/lib/rmg/CMapGenOptions.cpp index 035c3bc9b..72d9e786c 100644 --- a/lib/rmg/CMapGenOptions.cpp +++ b/lib/rmg/CMapGenOptions.cpp @@ -313,8 +313,8 @@ void CMapGenOptions::resetPlayersMap() while (players.size() < realPlayersCnt && !availableColors.empty()) { auto color = availableColors.front(); - setPlayerTypeForStandardPlayer(color, EPlayerType::AI); players[color].setColor(color); + setPlayerTypeForStandardPlayer(color, EPlayerType::AI); availableColors.erase(availableColors.begin()); if (vstd::contains(savedPlayerSettings, color)) From 37d81e916e6715ac83a223f2e7741c65b227bf11 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 19:31:38 +0200 Subject: [PATCH 1209/1248] Fix creature appearance on hota maps --- lib/mapObjects/CGCreature.cpp | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/mapObjects/CGCreature.cpp b/lib/mapObjects/CGCreature.cpp index ca661dc8e..f76915582 100644 --- a/lib/mapObjects/CGCreature.cpp +++ b/lib/mapObjects/CGCreature.cpp @@ -215,12 +215,8 @@ void CGCreature::pickRandomObject(CRandomGenerator & rand) subID = VLC->creh->pickRandomMonster(rand, 7); break; } - - if (ID != MapObjectID::MONSTER) - { - ID = MapObjectID::MONSTER; - setType(ID, subID); - } + ID = MapObjectID::MONSTER; + setType(ID, subID); } void CGCreature::initObj(CRandomGenerator & rand) From a7d6068bf6ec7214b3e1d10533b1b68933058e2b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Tue, 28 Nov 2023 19:32:03 +0200 Subject: [PATCH 1210/1248] Do not ignore block-visit objects when computing guardian locations --- lib/mapping/CMap.cpp | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/mapping/CMap.cpp b/lib/mapping/CMap.cpp index 92893cdbf..c825899e2 100644 --- a/lib/mapping/CMap.cpp +++ b/lib/mapping/CMap.cpp @@ -348,13 +348,8 @@ int3 CMap::guardingCreaturePosition (int3 pos) const { for (CGObjectInstance* obj : posTile.visitableObjects) { - if(obj->isBlockedVisitable()) - { - if (obj->ID == Obj::MONSTER) // Monster - return pos; - else - return int3(-1, -1, -1); //blockvis objects are not guarded by neighbouring creatures - } + if (obj->ID == Obj::MONSTER) + return pos; } } From fc035b1b553ed5514558e865962490688cba993c Mon Sep 17 00:00:00 2001 From: Dydzio Date: Tue, 28 Nov 2023 22:30:50 +0100 Subject: [PATCH 1211/1248] Change large spellbook config section to gameTweaks --- client/windows/CSpellWindow.cpp | 4 ++-- client/windows/settings/GeneralOptionsTab.cpp | 4 ++-- config/schemas/settings.json | 14 +++++++------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index cffa7144f..209932507 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -97,13 +97,13 @@ public: } spellsorter; CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells): - CWindowObject(PLAYER_COLORED | (settings["general"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), + CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)), battleSpellsOnly(openOnBattleSpells), selectedTab(4), currentPage(0), myHero(_myHero), myInt(_myInt), - isBigSpellbook(settings["general"]["enableLargeSpellbook"].Bool()), + isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()), spellsPerPage(24), offL(-11), offR(195), diff --git a/client/windows/settings/GeneralOptionsTab.cpp b/client/windows/settings/GeneralOptionsTab.cpp index 76c5e4817..c4a6ec84b 100644 --- a/client/windows/settings/GeneralOptionsTab.cpp +++ b/client/windows/settings/GeneralOptionsTab.cpp @@ -164,7 +164,7 @@ GeneralOptionsTab::GeneralOptionsTab() addCallback("enableLargeSpellbookChanged", [](bool value) { - setBoolSetting("general", "enableLargeSpellbook", value); + setBoolSetting("gameTweaks", "enableLargeSpellbook", value); }); //moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content @@ -213,7 +213,7 @@ GeneralOptionsTab::GeneralOptionsTab() std::shared_ptr enableLargeSpellbookCheckbox = widget("enableLargeSpellbookCheckbox"); if (enableLargeSpellbookCheckbox) - enableLargeSpellbookCheckbox->setSelected(settings["general"]["enableLargeSpellbook"].Bool()); + enableLargeSpellbookCheckbox->setSelected(settings["gameTweaks"]["enableLargeSpellbook"].Bool()); std::shared_ptr musicSlider = widget("musicSlider"); musicSlider->scrollTo(CCS->musich->getVolume()); diff --git a/config/schemas/settings.json b/config/schemas/settings.json index 034a84a4e..c65cf374d 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -39,8 +39,7 @@ "useSavePrefix", "savePrefix", "startTurnAutosave", - "enableUiEnhancements", - "enableLargeSpellbook" + "enableUiEnhancements" ], "properties" : { "playerName" : { @@ -132,10 +131,6 @@ "enableUiEnhancements" : { "type": "boolean", "default": true - }, - "enableLargeSpellbook" : { - "type": "boolean", - "default": false } } }, @@ -581,7 +576,8 @@ "compactTownCreatureInfo", "infoBarPick", "skipBattleIntroMusic", - "infoBarCreatureManagement" + "infoBarCreatureManagement", + "enableLargeSpellbook" ], "properties" : { "showGrid" : { @@ -615,6 +611,10 @@ "infoBarCreatureManagement": { "type" : "boolean", "default" : false + }, + "enableLargeSpellbook" : { + "type": "boolean", + "default": false } } } From 26b6d1cf745d58b3656f9afa5430822d8153e9d6 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sat, 2 Dec 2023 14:03:20 +0200 Subject: [PATCH 1212/1248] NKAI: fix capturing shipyards --- .../Pathfinding/Rules/AILayerTransitionRule.cpp | 9 ++------- client/adventureMap/AdventureMapInterface.cpp | 7 ++++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp index 241a1d9b6..a70826252 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp @@ -91,12 +91,12 @@ namespace AIPathfinding for(const CGHeroInstance * hero : nodeStorage->getAllHeroes()) { - if(hero->canCastThisSpell(waterWalk.toSpell())) + if(hero->canCastThisSpell(waterWalk.toSpell()) && hero->mana >= hero->getSpellCost(waterWalk.toSpell())) { waterWalkingActions[hero] = std::make_shared(hero); } - if(hero->canCastThisSpell(airWalk.toSpell())) + if(hero->canCastThisSpell(airWalk.toSpell()) && hero->mana >= hero->getSpellCost(airWalk.toSpell())) { airWalkingActions[hero] = std::make_shared(hero); } @@ -179,11 +179,6 @@ namespace AIPathfinding { bool result = false; - if(!specialAction->canAct(nodeStorage->getAINode(source.node))) - { - return false; - } - nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { auto castNodeOptional = nodeStorage->getOrCreateNode( diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index 7d879a0ea..c9b9e28da 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -441,7 +441,12 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) if(auto iw = GH.windows().topWindow()) iw->close(); - hotkeyEndingTurn(); + boost::thread newThread([this]() + { + hotkeyEndingTurn(); + }); + + newThread.detach(); } } From 0c270401b02d25d58f3ec72f30db0eebd6d4192f Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Dec 2023 14:10:38 +0100 Subject: [PATCH 1213/1248] update german in client --- Mods/vcmi/config/vcmi/german.json | 67 ++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 4c1cbdb79..d263656e4 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -36,10 +36,23 @@ "vcmi.heroOverview.spells" : "Zaubersprüche", "vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen", + "vcmi.radialWheel.fillSingleUnit" : "Füllen mit einzelnen Kreaturen", "vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen", "vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen", "vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee", "vcmi.radialWheel.splitUnit" : "Aufsplitten der Kreatur in anderen Slot", + + "vcmi.radialWheel.heroGetArmy" : "Armee von anderem Helden erhalten", + "vcmi.radialWheel.heroSwapArmy" : "Tausche Armee mit anderem Helden", + "vcmi.radialWheel.heroExchange" : "Öffne Tausch-Menü", + "vcmi.radialWheel.heroGetArtifacts" : "Artefakte von anderen Helden erhalten", + "vcmi.radialWheel.heroSwapArtifacts" : "Tausche Artefakte mit anderen Helden", + "vcmi.radialWheel.heroDismiss" : "Held entlassen", + + "vcmi.radialWheel.moveTop" : "Ganz nach oben bewegen", + "vcmi.radialWheel.moveUp" : "Nach oben bewegen", + "vcmi.radialWheel.moveDown" : "Nach unten bewegen", + "vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen", "vcmi.mainMenu.serverConnecting" : "Verbinde...", "vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:", @@ -58,7 +71,11 @@ "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", + "vcmi.server.errors.modsToDisable" : "{Folgende Mods müssen deaktiviert werden}", "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + "vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n", + "vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!", "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", @@ -94,7 +111,9 @@ "vcmi.systemOptions.hapticFeedbackButton.hover" : "Haptisches Feedback", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Interface Verbesserungen", - "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein größeres Zauberbuch, Rucksack, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Interface Verbesserungen}\n\nSchaltet verschiedene Interface Verbesserungen um. Wie z.B. ein Rucksack-Button, etc. Deaktivieren, um ein klassischeres Erlebnis zu haben.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Großes Zauberbuch", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Großes Zauberbuch}\n\nErmöglicht ein größeres Zauberbuch, in das mehr Zaubersprüche pro Seite passen. Die Animation des Seitenwechsels im Zauberbuch funktioniert nicht, wenn diese Einstellung aktiviert ist.", "vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen", "vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen", @@ -140,6 +159,9 @@ "vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Statistikfenster für Helden anzeigen}\n\nDauerhaftes Einschalten des Statistikfenster für Helden, das die primären Werte und Zauberpunkte anzeigt.", "vcmi.battleOptions.skipBattleIntroMusic.hover": "Intro-Musik überspringen", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Intro-Musik überspringen}\n\n Überspringe die kurze Musik, die zu Beginn eines jeden Kampfes gespielt wird, bevor die Action beginnt. Kann auch durch Drücken der ESC-Taste übersprungen werden.", + + "vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen", + "vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.", "vcmi.battleWindow.pressKeyToSkipIntro" : "Beliebige Taste drücken, um das Kampf-Intro zu überspringen", "vcmi.battleWindow.damageEstimation.melee" : "Angriff auf %CREATURE (%DAMAGE).", @@ -155,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Kampfergebnis übernehmen", + "vcmi.tutorialWindow.title" : "Touchscreen Einführung", + "vcmi.tutorialWindow.decription.RightClick" : "Berührt und haltet das Element, auf das mit der rechten Maustaste geklickt werden soll. Berührt den freien Bereich, um zu schließen.", + "vcmi.tutorialWindow.decription.MapPanning" : "Berührt und zieht mit einem Finger, um die Karte zu verschieben.", + "vcmi.tutorialWindow.decription.MapZooming" : "Berührt mit zwei Fingern, um den Kartenzoom zu ändern.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Durch Streichen öffnet sich das Radialrad für verschiedene Aktionen, wie z.B. Kreaturen-/Heldenverwaltung und Stadtreihenfolge.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Um aus einer bestimmten Richtung anzugreifen, wischt in die Richtung, aus der der Angriff erfolgen soll.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Die Geste für die Angriffsrichtung kann abgebrochen werden, wenn der Finger weit genug entfernt ist.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Berühren und halten, um einen Zauber abzubrechen.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Verfügbare Kreaturen anzeigen", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Verfügbare Kreaturen anzeigen}\n\n Zeigt in der Stadtübersicht (linke untere Ecke) die zum Kauf verfügbaren Kreaturen anstelle ihres Wachstums an.", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Wöchentl. Wachstum der Kreaturen anz.", @@ -182,6 +213,8 @@ "vcmi.heroWindow.openCommander.hover" : "Öffne Kommandanten-Fenster", "vcmi.heroWindow.openCommander.help" : "Zeige Informationen über Kommandanten dieses Helden", + "vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen", + "vcmi.heroWindow.openBackpack.help" : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert", "vcmi.commanderWindow.artifactMessage" : "Möchtet Ihr diesen Artefakt dem Helden zurückgeben?", @@ -201,6 +234,38 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", + "vcmi.optionsTab.turnOptions.hover" : "Zug-Optionen", + "vcmi.optionsTab.turnOptions.help" : "Ändere Optionen zu Zug-Timer und simultanen Zügen", + + "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Zug-Timer", + "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", + "vcmi.optionsTab.chessFieldCreature.hover" : "Einheiten-Timer", + "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Zug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Zug beendet. Jeder laufende Kampf endet mit einem Verlust.", + "vcmi.optionsTab.chessFieldTurn.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", + "vcmi.optionsTab.chessFieldCreature.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn der Runde jeder Einheit zurückgesetzt.", + + "vcmi.optionsTab.simturns" : "Simultane Züge", + "vcmi.optionsTab.simturnsMin.hover" : "Zumindest für", + "vcmi.optionsTab.simturnsMax.hover" : "Höchstens für", + "vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge", + "vcmi.optionsTab.simturnsMin.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen. Die Kontakte zwischen den Spielern sind während dieser Zeit blockiert", + "vcmi.optionsTab.simturnsMax.help" : "Spielt gleichzeitig für eine bestimmte Anzahl von Tagen oder bis zum Kontakt mit einem anderen Spieler", + "vcmi.optionsTab.simturnsAI.help" : "{Simultane KI Züge}\nExperimentelle Option. Ermöglicht es den KI-Spielern, gleichzeitig mit dem menschlichen Spieler zu agieren, wenn simultane Spielzüge aktiviert sind.", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : "%d Tage", + "vcmi.optionsTab.simturns.days.1" : "%d Tag", + "vcmi.optionsTab.simturns.days.2" : "%d Tage", + "vcmi.optionsTab.simturns.weeks.0" : "%d Wochen", + "vcmi.optionsTab.simturns.weeks.1" : "%d Woche", + "vcmi.optionsTab.simturns.weeks.2" : "%d Wochen", + "vcmi.optionsTab.simturns.months.0" : "%d Monate", + "vcmi.optionsTab.simturns.months.1" : "%d Monat", + "vcmi.optionsTab.simturns.months.2" : "%d Monate", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Der Feind hat es geschafft, bis zum heutigen Tag zu überleben. Der Sieg gehört ihm!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Herzlichen Glückwunsch! Ihr habt es geschafft, zu überleben. Der Sieg ist euer!", From 3540a8a915b6d9f4af32d61d3409ba2e22138879 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sat, 2 Dec 2023 18:36:07 +0100 Subject: [PATCH 1214/1248] Update german.json --- Mods/vcmi/config/vcmi/german.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index d263656e4..7f3db42ec 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -234,14 +234,14 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Team-Zuordnungen", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", - "vcmi.optionsTab.turnOptions.hover" : "Zug-Optionen", - "vcmi.optionsTab.turnOptions.help" : "Ändere Optionen zu Zug-Timer und simultanen Zügen", + "vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen", + "vcmi.optionsTab.turnOptions.help" : "Ändere Optionen zu Spielzug-Timer und simultanen Zügen", "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", - "vcmi.optionsTab.chessFieldTurn.hover" : "Zug-Timer", + "vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer", "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", "vcmi.optionsTab.chessFieldCreature.hover" : "Einheiten-Timer", - "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Zug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Zug beendet. Jeder laufende Kampf endet mit einem Verlust.", + "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Spielzug beendet. Jeder laufende Kampf endet mit einem Verlust.", "vcmi.optionsTab.chessFieldTurn.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", "vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", "vcmi.optionsTab.chessFieldCreature.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn der Runde jeder Einheit zurückgesetzt.", From 3b3c4860b20a3e9c7c645c13be68b9c8cb1a2417 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 3 Dec 2023 15:58:03 +0200 Subject: [PATCH 1215/1248] Avoid creation of new threads without mutex protection --- client/adventureMap/AdventureMapInterface.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/client/adventureMap/AdventureMapInterface.cpp b/client/adventureMap/AdventureMapInterface.cpp index c9b9e28da..8aa0de501 100644 --- a/client/adventureMap/AdventureMapInterface.cpp +++ b/client/adventureMap/AdventureMapInterface.cpp @@ -441,12 +441,10 @@ void AdventureMapInterface::onPlayerTurnStarted(PlayerColor playerID) if(auto iw = GH.windows().topWindow()) iw->close(); - boost::thread newThread([this]() - { - hotkeyEndingTurn(); - }); - - newThread.detach(); + GH.dispatchMainThread([this]() + { + hotkeyEndingTurn(); + }); } } From 80b82cc026649bccf502791f3a511dc07305bd7e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 3 Dec 2023 15:58:23 +0200 Subject: [PATCH 1216/1248] Remove invalid assert - may trigger on human player defeat --- server/processors/TurnOrderProcessor.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/server/processors/TurnOrderProcessor.cpp b/server/processors/TurnOrderProcessor.cpp index 87a63cb92..db3969124 100644 --- a/server/processors/TurnOrderProcessor.cpp +++ b/server/processors/TurnOrderProcessor.cpp @@ -263,8 +263,6 @@ void TurnOrderProcessor::onPlayerEndsGame(PlayerColor which) if (actingPlayers.empty()) doStartNewDay(); - - assert(!actingPlayers.empty()); } bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which) From 773071fee37eb52935fa44d0a37aae6302cd119b Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 3 Dec 2023 15:58:44 +0200 Subject: [PATCH 1217/1248] Fix AI movement through wandering monsters --- lib/pathfinder/CPathfinder.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index a713b5bdc..46d4f564b 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -40,19 +40,11 @@ bool CPathfinderHelper::canMoveFromNode(const PathNodeInfo & source) const if (source.node->isTeleportAction()) return true; - // we can go through garrisons - if (source.nodeObject->ID == MapObjectID::GARRISON || source.nodeObject->ID == MapObjectID::GARRISON2) - return true; + // we can not go through teleporters since moving onto a teleport will teleport hero and may invalidate path (e.g. one-way teleport or enemy hero on other side) + if (dynamic_cast(source.nodeObject) != nullptr) + return false; - // or through border gate (if we stand on it then we already have the key) - if (source.nodeObject->ID == MapObjectID::BORDER_GATE) - return true; - - // or "through" boat, but only if this is embarking - if (source.nodeObject->ID == MapObjectID::BOAT && source.node->action == EPathNodeAction::EMBARK) - return true; - - return false; + return true; } std::vector CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const From ea7278f79c1a7261b6e885e09fe5e1d799f6eed9 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 3 Dec 2023 18:36:48 +0200 Subject: [PATCH 1218/1248] TBB: use 1.7 vcpkg prebuild libs for msvc --- CI/msvc/before_install.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/msvc/before_install.sh b/CI/msvc/before_install.sh index 851cbd4a9..5388e84f8 100644 --- a/CI/msvc/before_install.sh +++ b/CI/msvc/before_install.sh @@ -1,5 +1,5 @@ curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \ - "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.6/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" + "https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.7/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" 7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" #rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug From a1f4748bbc6816c41ee6ed0dcc93509814436789 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 3 Dec 2023 18:56:23 +0200 Subject: [PATCH 1219/1248] Fix socket shutdown --- lib/serializer/Connection.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/serializer/Connection.cpp b/lib/serializer/Connection.cpp index c4c98f4f6..de5f3b85d 100644 --- a/lib/serializer/Connection.cpp +++ b/lib/serializer/Connection.cpp @@ -221,6 +221,8 @@ int CConnection::read(void * data, unsigned size) CConnection::~CConnection() { + close(); + if(handler) { // ugly workaround to avoid self-join if last strong reference to shared_ptr that owns this class has been released in this very thread, e.g. on netpack processing @@ -229,8 +231,6 @@ CConnection::~CConnection() else handler->detach(); } - - close(); } template @@ -246,6 +246,15 @@ void CConnection::close() { if(socket) { + try + { + socket->shutdown(boost::asio::ip::tcp::socket::shutdown_receive); + } + catch (const boost::system::system_error & e) + { + logNetwork->error("error closing socket: %s", e.what()); + } + socket->close(); socket.reset(); } From cd5d6a8f774d98dd1e8dbe14ab654c47f09e1315 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 3 Dec 2023 19:45:18 +0200 Subject: [PATCH 1220/1248] Fix loading of artifact ID for "transport item" victory condition --- lib/mapping/MapReaderH3M.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/mapping/MapReaderH3M.cpp b/lib/mapping/MapReaderH3M.cpp index 4fb3917d2..4a1c75e49 100644 --- a/lib/mapping/MapReaderH3M.cpp +++ b/lib/mapping/MapReaderH3M.cpp @@ -83,7 +83,10 @@ ArtifactID MapReaderH3M::readArtifact() ArtifactID MapReaderH3M::readArtifact8() { - ArtifactID result(reader->readInt8()); + ArtifactID result(reader->readUInt8()); + + if(result.getNum() == 0xff) + return ArtifactID::NONE; if (result.getNum() < features.artifactsCount) return remapIdentifier(result); From b238796feebafa57716a1c7ff502f0ec0fb1c7f8 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 3 Dec 2023 20:20:59 +0200 Subject: [PATCH 1221/1248] MSVC: attempt to get build with pdb --- .github/workflows/github.yml | 8 ++++++++ CMakeLists.txt | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 2e719223b..07dd4171b 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -279,6 +279,14 @@ jobs: name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} path: | ${{ env.ANDROID_APK_PATH }} + + - name: Symbols + if: ${{ matrix.platform == 'msvc' }} + uses: actions/upload-artifact@v3 + with: + name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols + path: | + ${{github.workspace}}/**/*.pdb - name: Android JNI ${{matrix.platform}} if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a56e91fd..b502b3fbf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -715,6 +715,11 @@ endif(WIN32) # Packaging section # ####################################### +if(MSVC) + SET(CMAKE_INSTALL_SYSTEM_RUNTIME_DESTINATION ${BIN_DIR}) + Include(InstallRequiredSystemLibraries) +endif() + set(CPACK_PACKAGE_VERSION_MAJOR ${VCMI_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MINOR ${VCMI_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_PATCH ${VCMI_VERSION_PATCH}) From 4318e94bd032f7b7143b3b26ea6ef251236edf70 Mon Sep 17 00:00:00 2001 From: Laserlicht <13953785+Laserlicht@users.noreply.github.com> Date: Sun, 3 Dec 2023 20:18:22 +0100 Subject: [PATCH 1222/1248] regenerate translations; german update --- launcher/translation/chinese.ts | 10 +- launcher/translation/english.ts | 10 +- launcher/translation/french.ts | 10 +- launcher/translation/german.ts | 41 +- launcher/translation/polish.ts | 10 +- launcher/translation/russian.ts | 10 +- launcher/translation/spanish.ts | 10 +- launcher/translation/ukrainian.ts | 10 +- launcher/translation/vietnamese.ts | 10 +- mapeditor/translation/english.ts | 1399 ++++++++++++++++++++------ mapeditor/translation/french.ts | 1415 ++++++++++++++++++++------ mapeditor/translation/german.ts | 1425 +++++++++++++++++++++------ mapeditor/translation/polish.ts | 1425 +++++++++++++++++++++------ mapeditor/translation/russian.ts | 1415 ++++++++++++++++++++------ mapeditor/translation/spanish.ts | 1415 ++++++++++++++++++++------ mapeditor/translation/ukrainian.ts | 1423 ++++++++++++++++++++------ mapeditor/translation/vietnamese.ts | 1425 +++++++++++++++++++++------ 17 files changed, 8962 insertions(+), 2501 deletions(-) diff --git a/launcher/translation/chinese.ts b/launcher/translation/chinese.ts index 518ce01ed..3b65d01a2 100644 --- a/launcher/translation/chinese.ts +++ b/launcher/translation/chinese.ts @@ -669,27 +669,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use 显示开场动画 - + Active 激活 - + Disabled 禁用 - + Enable 启用 - + Not Installed 未安装 - + Install 安装 diff --git a/launcher/translation/english.ts b/launcher/translation/english.ts index 94a2f2716..691c1b400 100644 --- a/launcher/translation/english.ts +++ b/launcher/translation/english.ts @@ -667,27 +667,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + Active - + Disabled - + Enable - + Not Installed - + Install diff --git a/launcher/translation/french.ts b/launcher/translation/french.ts index 3d02b580c..1f1949470 100644 --- a/launcher/translation/french.ts +++ b/launcher/translation/french.ts @@ -678,27 +678,27 @@ Mode exclusif plein écran - le jeu couvrira l"intégralité de votre écra Montrer l'intro - + Active Actif - + Disabled Désactivé - + Enable Activé - + Not Installed Pas Installé - + Install Installer diff --git a/launcher/translation/german.ts b/launcher/translation/german.ts index e08a8a7ad..b9f875973 100644 --- a/launcher/translation/german.ts +++ b/launcher/translation/german.ts @@ -121,7 +121,7 @@ Maps - + Karten @@ -180,7 +180,7 @@ Compatibility - Kompatibilität + Kompatibilität @@ -319,7 +319,7 @@ Size - + Größe @@ -410,12 +410,12 @@ Downloading %s%. %p% (%v MB out of %m MB) finished - + Herunterladen von %s%. %p% (%v MB von %m MB) beendet Download failed - + Download fehlgeschlagen @@ -424,30 +424,37 @@ Encountered errors: - + Es konnten nicht alle Dateien heruntergeladen werden. + +Es sind Fehler aufgetreten: + + Install successfully downloaded? - + + +Installation erfolgreich heruntergeladen? Installing mod %1 - + Installation von Mod %1 Operation failed - + Operation fehlgeschlagen Encountered errors: - + Aufgetretene Fehler: + @@ -598,7 +605,7 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend Reserved screen area - + Reservierter Bildschirmbereich @@ -649,7 +656,7 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend VSync - + VSync @@ -703,27 +710,27 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend Form - + Formular Users in lobby - + Benutzer in der Lobby Global chat - + Globaler Chat type you message - + Nachricht eingeben send - + senden diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 4116e62f7..68250a26e 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -673,27 +673,27 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane Pokaż intro - + Active Aktywny - + Disabled Wyłączone - + Enable Włącz - + Not Installed Nie zainstalowano - + Install Zainstaluj diff --git a/launcher/translation/russian.ts b/launcher/translation/russian.ts index 718618d7d..c421df14b 100644 --- a/launcher/translation/russian.ts +++ b/launcher/translation/russian.ts @@ -667,27 +667,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Вступление - + Active Активен - + Disabled Отключен - + Enable Включить - + Not Installed Не установлен - + Install Установить diff --git a/launcher/translation/spanish.ts b/launcher/translation/spanish.ts index b0a953ab7..2dba9fdbe 100644 --- a/launcher/translation/spanish.ts +++ b/launcher/translation/spanish.ts @@ -667,27 +667,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Idioma de los datos de Heroes III. - + Active Activado - + Disabled Desactivado - + Enable Activar - + Not Installed No Instalado - + Install Instalar diff --git a/launcher/translation/ukrainian.ts b/launcher/translation/ukrainian.ts index 37c76d07c..971474a49 100644 --- a/launcher/translation/ukrainian.ts +++ b/launcher/translation/ukrainian.ts @@ -680,27 +680,27 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use Вступні відео - + Active Активні - + Disabled Деактивований - + Enable Активувати - + Not Installed Не встановлено - + Install Встановити diff --git a/launcher/translation/vietnamese.ts b/launcher/translation/vietnamese.ts index f92e9e7fe..a5a1c99c1 100644 --- a/launcher/translation/vietnamese.ts +++ b/launcher/translation/vietnamese.ts @@ -673,27 +673,27 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ Hiện thị giới thiệu - + Active Bật - + Disabled Tắt - + Enable Bật - + Not Installed Chưa cài đặt - + Install Cài đặt diff --git a/mapeditor/translation/english.ts b/mapeditor/translation/english.ts index 3e5d669d1..052ea7d2f 100644 --- a/mapeditor/translation/english.ts +++ b/mapeditor/translation/english.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings - + Wide formation - + Tight formation + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + + + + + Map description + + + + + Limit maximum heroes level + + + + + Difficulty + + + GeneratorProgress @@ -27,6 +83,95 @@ + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + + + + + 7 days without town + + + + + Parameters + + + + + No special loss + + + + + Lose castle + + + + + Lose hero + + + + + Time expired + + + + + Days without town + + + MainWindow @@ -40,546 +185,499 @@ - + Map - + Edit - + View - + Player - + Toolbar - + Minimap - + Map Objects View - + Browser - + Inspector - + Property - + Value - - Terrains View + + Tools - - Brush + + Painting - + Terrains - + Roads - + Rivers - + + Preview + + + + Open - + Save - + New - + Save as... - + Ctrl+Shift+S - + U/G - - + + View underground - + Pass - + Cut - + Copy - + Paste - + Fill - + Fills the selection with obstacles - + Grid - + General - + Map title and description - + Players settings - - + + Undo - + Redo - + Erase - + Neutral - + Validate - - - - + + + + Update appearance - + Recreate obstacles - + Player 1 - + Player 2 - + Player 3 - + Player 4 - + Player 5 - + Player 6 - + Player 7 - + Player 8 - + Export as... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) - - + Save map - - + VCMI maps (*.vmap) - + Type - + View surface - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings - + General - - Map name - - - - - Map description - - - - - Limit maximum heroes level - - - - - Difficulty - - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events - + Victory - - Victory message - - - - - Only for human players - - - - - Allow standard victory - - - - - - Parameters - - - - + Loss - - 7 days without town + + Timed - - Defeat message + + Rumors - + Abilities - + Spells - + Artifacts - + Heroes - + Ok - - - No special victory - - - - - Capture artifact - - - - - Hire creatures - - - - - Accumulate resources - - - - - Construct building - - - - - Capture town - - - - - Defeat hero - - - - - Transport artifact - - - - - No special loss - - - - - Lose castle - - - - - Lose hero - - - - - Time expired - - - - - Days without town - - MapView - + Can't place object @@ -587,55 +685,108 @@ MessageWidget - + Message + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + PlayerParams - + Human/CPU - + CPU only - + Team - + Main town - + Color - + + ... + + + + Random faction - + Generate hero at main - + (default) - + Player ID: %1 @@ -663,45 +814,634 @@ + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + + + + + Spells + + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + RewardsWidget - + Rewards - - Remove selected + + + + + Add - Delete all + + + + Remove - - Add or change + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + + + + + + Spells + + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + + + + + + Value + + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + + + + + Hero classes + + + + + Players + + + + + None + + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok TownBulidingsWidget - + Buildings + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + + + + + Only for human players + + + + + Allow standard victory + + + + + Parameters + + + + + No special victory + + + + + Capture artifact + + + + + Hire creatures + + + + + Accumulate resources + + + + + Construct building + + + + + Capture town + + + + + Defeat hero + + + + + Transport artifact + + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -990,27 +1803,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/french.ts b/mapeditor/translation/french.ts index 91cd9b88a..0d1218d96 100644 --- a/mapeditor/translation/french.ts +++ b/mapeditor/translation/french.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Paramètres de l'armée - + Wide formation Formation large - + Tight formation Formation serrée + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nom de la carte + + + + Map description + Description de la carte + + + + Limit maximum heroes level + + + + + Difficulty + Difficulté + + GeneratorProgress @@ -27,6 +83,95 @@ Générer une carte + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Message de défaite + + + + 7 days without town + 7 jours sans ville + + + + Parameters + Paramètres + + + + No special loss + Aucune perte spéciale + + + + Lose castle + Perdre un château + + + + Lose hero + Perdre un héros + + + + Time expired + Délai expiré + + + + Days without town + Jours sans ville + + MainWindow @@ -40,546 +185,499 @@ Fichier - + Map Carte - + Edit Édition - + View Affichage - + Player Joueur - + Toolbar Barre d'outils - + Minimap Mini-carte - + Map Objects View Vue des objets cartographiques - + Browser Navigateur - + Inspector Inspecteur - + Property Propriété - + Value Valeur - - Terrains View - Vue des terrains + + Tools + - - Brush - Pinceau + + Painting + - + Terrains Terrains - + Roads Routes - + Rivers Rivières - + + Preview + + + + Open Ouvrir - + Save Enregistrer - + New Nouveau - + Save as... Enregistrer sous... - + Ctrl+Shift+S Ctrl+Maj+S - + U/G Sous-sol/Surface - - + + View underground Voir le sous-sol - + Pass Passage - + Cut Couper - + Copy Copier - + Paste Coller - + Fill Remplir - + Fills the selection with obstacles Remplir la sélection d'obstacles - + Grid Grille - + General Général - + Map title and description Titre et description de la carte - + Players settings Paramètres des joueurs - - + + Undo Annuler - + Redo Rétablir - + Erase Effacer - + Neutral Neutre - + Validate Valider - - - - + + + + Update appearance Mettre à jour l'apparence - + Recreate obstacles Recréer des obstacles - + Player 1 Joueur 1 - + Player 2 Joueur 2 - + Player 3 Joueur 3 - + Player 4 Joueur 4 - + Player 5 Joueur 5 - + Player 6 Joueur 6 - + Player 7 Joueur 7 - + Player 8 Joueur 8 - + Export as... Exporter sous... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Confirmation - + Unsaved changes will be lost, are you sure? Des modifications non sauvegardées vont être perdues. Êtes-vous sûr ? - - - - Failed to open map - - - Cannot open map from this folder - - - - + Open map Ouvrir la carte - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Toutes les cartes prises en charge (*.vmap *.h3m);;Cartes VCMI (*.vmap);;Cartes HoMM3 (*.h3m) - - + Save map Enregistrer la carte - - + VCMI maps (*.vmap) Cartes VCMI (*.vmap) - + Type Type - + View surface Afficher la surface - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Paramètres de la carte - + General Général - - Map name - Nom de la carte - - - - Map description - Description de la carte - - - - Limit maximum heroes level - - - - - Difficulty - Difficulté - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Événements - + Victory Victoire - - Victory message - Message de victoire - - - - Only for human players - Uniquement pour les joueurs humains - - - - Allow standard victory - Autoriser la victoire standard - - - - - Parameters - Paramètres - - - + Loss Perte - - 7 days without town - 7 jours sans ville + + Timed + - - Defeat message - Message de défaite + + Rumors + - + Abilities Capacités - + Spells Sorts - + Artifacts Artefacts - + Heroes Héros - + Ok OK - - - No special victory - Pas de victoire spéciale - - - - Capture artifact - Récupérer l'artefact - - - - Hire creatures - Engagez des créatures - - - - Accumulate resources - Accumuler des ressources - - - - Construct building - Construire un bâtiment - - - - Capture town - Conquérir une ville - - - - Defeat hero - Battre un héros - - - - Transport artifact - Transporter un artefact - - - - No special loss - Aucune perte spéciale - - - - Lose castle - Perdre un château - - - - Lose hero - Perdre un héros - - - - Time expired - Délai expiré - - - - Days without town - Jours sans ville - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Message - PlayerParams + ModSettings - No team - Aucune équipe + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Human/Ordinateur - + CPU only Ordinateur uniquement - + Team Équipe - + Main town Ville principale - + Color - + + ... + + + + Random faction Faction aléatoire - + Generate hero at main Générer un héros dans le principal - + (default) (par défaut) - + Player ID: %1 Identifiant du joueur : %1 @@ -667,45 +814,634 @@ OK + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objectif de la mission + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefacts + + + + Spells + Sorts + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + RewardsWidget - + Rewards Récompenses - - Remove selected - Supprimer ce qui est sélectionné + + + + + Add + - Delete all - Tout supprimer + + + + Remove + - - Add or change - Ajouter ou modifier + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefacts + + + + + Spells + Sorts + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Type + + + + + Value + Valeur + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héros + + + + Hero classes + + + + + Players + Joueurs + + + + None + Aucune + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + OK TownBulidingsWidget - + Buildings Bâtiments + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Résultats de la validation de la carte - + Map is not loaded Aucune carte n'est chargée - + No factions allowed for player %1 - + No players allowed to play this map Aucun joueur autorisé à jouer sur cette carte - + Map is allowed for one player and cannot be started La carte est autorisée pour un joueur et ne peut pas être démarrée - + No human players allowed to play this map Aucun joueur humain n'est autorisé à jouer sur cette carte - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner L'instance blindée %1 est IMMARQUABLE mais doit avoir un propriétaire NEUTRE ou joueur - + Object %1 is assigned to non-playable player %2 L'objet %1 est attribué au joueur non jouable %2 - + Town %1 has undefined owner %2 La ville %1 a le propriétaire indéfini %2 - + Prison %1 must be a NEUTRAL La prison %1 doit être NEUTRE - + Hero %1 must have an owner Le héros %1 doit avoir un propriétaire - + Hero %1 is prohibited by map settings Le héros %1 est interdit par les paramètres de la carte - + Hero %1 has duplicate on map Le héros %1 a un doublon sur la carte - + Hero %1 has an empty type and must be removed Le héros %1 a un type vide et doit être supprimé - + Spell scroll %1 is prohibited by map settings Le défilement des sorts %1 est interdit par les paramètres de la carte - + Spell scroll %1 doesn't have instance assigned and must be removed Le parchemin de sort %1 n'a pas d'instance assignée et doit être supprimé - + Artifact %1 is prohibited by map settings L'artefact %1 est interdit par les paramètres de la carte - + Player %1 doesn't have any starting town Le joueur %1 n'a pas de ville de départ - + Map name is not specified Le nom de la carte n'est pas spécifié - + Map description is not specified La description de la carte n'est pas spécifiée - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 Une exception se produit lors de la validation : %1 - + Unknown exception occurs during validation Une exception inconnue se produit lors de la validation + + VictoryConditions + + + Form + + + + + Victory message + Message de victoire + + + + Only for human players + Uniquement pour les joueurs humains + + + + Allow standard victory + Autoriser la victoire standard + + + + Parameters + Paramètres + + + + No special victory + Pas de victoire spéciale + + + + Capture artifact + Récupérer l'artefact + + + + Hire creatures + Engagez des créatures + + + + Accumulate resources + Accumuler des ressources + + + + Construct building + Construire un bâtiment + + + + Capture town + Conquérir une ville + + + + Defeat hero + Battre un héros + + + + Transport artifact + Transporter un artefact + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Annuler - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Chemin du fichier de la carte à ouvrir. - + Extract original H3 archives into a separate folder. Extraire les archives H3 d'origine dans un dossier séparé. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. À partir d'une archive extraite, il divise TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 et Un44 en fichiers PNG individuels. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. À partir d'une archive extraite, convertit des images uniques (trouvées dans le dossier Images) de .pcx en png. - + Delete original files, for the ones split / converted. Supprimer les fichiers d'origine, pour ceux fractionnés/convertis. diff --git a/mapeditor/translation/german.ts b/mapeditor/translation/german.ts index 07b7bac38..5c87e7634 100644 --- a/mapeditor/translation/german.ts +++ b/mapeditor/translation/german.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Einstellungen der Armee - + Wide formation Breite Formation - + Tight formation Enge Formation + + EventSettings + + + Form + Formular + + + + Timed events + Zeitlich begrenzte Ereignisse + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New event + Neues Ereignis + + + + GeneralSettings + + + Form + Formular + + + + Map name + Kartenname + + + + Map description + Kartenbeschreibung + + + + Limit maximum heroes level + Maximales Level des Helden begrenzen + + + + Difficulty + Schwierigkeit + + GeneratorProgress @@ -27,6 +83,95 @@ Karte generieren + + HeroSkillsWidget + + + Hero skills + Helden-Fertigkeiten + + + + + + + TextLabel + TextLabel + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Skill + Fertigkeiten + + + + Level + Level + + + + Customize skills + Fertigkeiten anpassen + + + + LoseConditions + + + Form + Formular + + + + Defeat message + Niederlage-Nachricht + + + + 7 days without town + 7 Tage ohne Stadt + + + + Parameters + Parameter + + + + No special loss + Keine besondere Niederlage + + + + Lose castle + Schloss verlieren + + + + Lose hero + Held verlieren + + + + Time expired + Zeit abgelaufen + + + + Days without town + Tage ohne Stadt + + MainWindow @@ -40,556 +185,499 @@ Datei - + Map Karte - + Edit Bearbeiten - + View Ansicht - + Player Spieler - + Toolbar Werkzeugleiste - + Minimap Minikarte - + Map Objects View Kartenobjekte-Ansicht - + Browser Browser - + Inspector Inspektor - + Property Eigenschaft - + Value Wert - - Terrains View - Terrain-Ansicht + + Tools + Werkzeuge - - Brush - Pinsel + + Painting + Malen - + Terrains Terrains - + Roads Straßen - + Rivers Flüsse - + + Preview + Vorschau + + + Open Öffnen - + Save Speichern - + New Neu - + Save as... Speichern unter... - + Ctrl+Shift+S Strg+Shift+S - + U/G U/G - - + + View underground Ansicht Untergrund - + Pass Passierbar - + Cut Ausschneiden - + Copy Kopieren - + Paste Einfügen - + Fill Füllen - + Fills the selection with obstacles Füllt die Auswahl mit Hindernissen - + Grid Raster - + General Allgemein - + Map title and description Titel und Beschreibung der Karte - + Players settings Spieler-Einstellungen - - + + Undo Rückgängig - + Redo Wiederholen - + Erase Löschen - + Neutral Neutral - + Validate Validieren - - - - + + + + Update appearance Aussehen aktualisieren - + Recreate obstacles Hindernisse neu erschaffen - + Player 1 Spieler 1 - + Player 2 Spieler 2 - + Player 3 Spieler 3 - + Player 4 Spieler 4 - + Player 5 Spieler 5 - + Player 6 Spieler 6 - + Player 7 Spieler 7 - + Player 8 Spieler 8 - + Export as... Exportieren als... - + + Translations + Übersetzungen + + + + Ctrl+T + Strg+T + + + + + h3m converter + h3m-Konverter + + + + Lock + Sperren + + + + Lock objects on map to avoid unnecessary changes + Objekte auf der Karte sperren, um unnötige Änderungen zu vermeiden + + + + Ctrl+L + Strg+L + + + + Unlock + Entsperren + + + + Unlock all objects on the map + Entsperre alle Objekte auf der Karte + + + + Ctrl+Shift+L + Strg+Umschalt+L + + + + Zoom in + Heranzoomen + + + + Ctrl+= + Strg+= + + + + Zoom out + Herauszoomen + + + + Ctrl+- + Strg+- + + + + Zoom reset + Zoom zurücksetzen + + + + Ctrl+Shift+= + Strg+Umschalt+= + + + Confirmation Bestätigung - + Unsaved changes will be lost, are you sure? Ungespeicherte Änderungen gehen verloren, sind sie sicher? - - Failed to open map - Öffnen der Karte fehlgeschlagen - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - - Cannot open map from this folder - Kann keine Karte aus diesem Ordner öffnen - - - + Open map Karte öffnen - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Alle unterstützten Karten (*.vmap *.h3m);;VCMI-Karten (*.vmap);;HoMM3-Karten (*.h3m) - - + Save map Karte speichern - - + VCMI maps (*.vmap) VCMI-Karten (*.vmap) - + Type Typ - + View surface Oberfläche anzeigen - + No objects selected Keine Objekte selektiert - + This operation is irreversible. Do you want to continue? Diese Operation ist unumkehrbar. Möchten sie fortsetzen? - + Errors occurred. %1 objects were not updated Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden - + Save to image Als Bild speichern + + + Select maps to convert + Zu konvertierende Karten auswählen + + + + HoMM3 maps(*.h3m) + HoMM3-Karten (*.h3m) + + + + Choose directory to save converted maps + Verzeichnis zum Speichern der konvertierten Karten wählen + + + + Operation completed + Vorgang abgeschlossen + + + + Successfully converted %1 maps + Erfolgreiche Konvertierung von %1 Karten + + + + Failed to convert the map. Abort operation + Die Karte konnte nicht konvertiert werden. Vorgang abgebrochen + MapSettings - + Map settings Karteneinstellungen - + General Allgemein - - Map name - Kartenname - - - - Map description - Kartenbeschreibung - - - - Limit maximum heroes level - Maximales Level des Helden begrenzen - - - - Difficulty - Schwierigkeit - - - + Mods Mods - - Mandatory mods for playing this map - Notwendige Mods zum Spielen dieser Karte - - - - Mod name - Mod Name - - - - Version - Version - - - - Automatic assignment - Automatische Zuweisung - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Sie Belohnungen, Garnisonen usw. von Mods angepasst haben - - - - Map objects mods - Mods für Kartenobjekte - - - - Set all mods having a game content as mandatory - Alle Mods, die einen Spielinhalt haben, als notwendig festlegen - - - - Full content mods - Vollwertige Mods - - - + Events Ereignisse - + Victory Sieg - - Victory message - Sieg-Nachricht - - - - Only for human players - Nur für menschliche Spieler - - - - Allow standard victory - Standardsieg zulassen - - - - - Parameters - Parameter - - - + Loss Niederlage - - 7 days without town - 7 Tage ohne Stadt + + Timed + Zeitgesteuert - - Defeat message - Niederlage-Nachricht + + Rumors + Gerüchte - + Abilities Fähigkeiten - + Spells Zaubersprüche - + Artifacts Artefakte - + Heroes Helden - + Ok Ok - - - No special victory - Kein besonderer Sieg - - - - Capture artifact - Artefakt sammeln - - - - Hire creatures - Kreaturen anheuern - - - - Accumulate resources - Ressourcen ansammeln - - - - Construct building - Gebäude errichten - - - - Capture town - Stadt einnehmen - - - - Defeat hero - Held besiegen - - - - Transport artifact - Artefakt transportieren - - - - No special loss - Keine besondere Niederlage - - - - Lose castle - Schloss verlieren - - - - Lose hero - Held verlieren - - - - Time expired - Zeit abgelaufen - - - - Days without town - Tage ohne Stadt - MapView - + Can't place object Objekt kann nicht platziert werden @@ -597,59 +685,108 @@ MessageWidget - + Message Nachricht - PlayerParams + ModSettings - No team - Kein Team + + Form + Formular - + + Mandatory mods to play this map + Benötigte Mods zum Spielen dieser Karte + + + + Mod name + Mod Name + + + + Version + Version + + + + Automatic assignment + Automatische Zuweisung + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Belohnungen, Garnisonen usw. von Mods angepasst wurden + + + + Map objects mods + Mods für Kartenobjekte + + + + Set all mods having a game content as mandatory + Alle Mods, die einen Spielinhalt haben, als notwendig festlegen + + + + Full content mods + Vollwertige Mods + + + + PlayerParams + + Human/CPU Mensch/CPU - + CPU only Nur CPU - + Team Team - + Main town Hauptstadt - + Color Farbe - + + ... + ... + + + Random faction Zufällige Fraktion - + Generate hero at main Held am Hauptplatz generieren - + (default) (Standard) - + Player ID: %1 Spieler-ID: %1 @@ -677,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + Porträt + + + + + ... + ... + + + + Default + Standard + + + + QObject + + + Beginner + Anfänger + + + + Advanced + Fortgeschrittene + + + + Expert + Experte + + + + Compliant + Konform + + + + Friendly + Freundlich + + + + Aggressive + Aggressiv + + + + Hostile + Feindlich + + + + Savage + Wild + + + + + neutral + neutral + + + + UNFLAGGABLE + UNFLAGGBAR + + QuestWidget - + Mission goal Missionsziel + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Hero level + Heldenstufe + + + + Hero experience + Heldenerfahrung + + + + Spell points + Zauberpunkte + + + + % + % + + + + Kill hero/monster + Held/Monster töten + + + + ... + ... + + + + Primary skills + Primäre Fähigkeiten + + + + Attack + Angriff + + + + Defence + Verteidigung + + + + Spell power + Zauberkraft + + + + Knowledge + Wissen + + + + Resources + Ressourcen + + + + Artifacts + Artefakte + + + + Spells + Zaubersprüche + + + + Skills + Fertigkeiten + + + + Creatures + Kreaturen + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + RewardsWidget - + Rewards Belohnungen - - Remove selected - Ausgewähltes entfernen + + + + + Add + Hinzufügen - Delete all - Alle löschen + + + + Remove + Entfernen - - Add or change - Hinzufügen oder ändern + + Visit mode + Besuchsmodus + + + + Select mode + Modus auswählen + + + + On select text + + + + + Can refuse + Kann ablehnen + + + + Reset parameters + Parameter zurücksetzen + + + + Period + Zeitraum + + + + days + Tage + + + + Reset visitors + Besucher zurücksetzen + + + + Reset rewards + Belohnungen zurücksetzen + + + + Window type + Fenstertyp + + + + Event info + Ereignis-Infos + + + + Message to be displayed on granting of this reward + Nachricht, die bei der Gewährung dieser Belohnung angezeigt wird + + + + Reward + Belohnung + + + + + Hero level + Heldenstufe + + + + + Hero experience + Heldenerfahrung + + + + + Spell points + Zauberpunkte + + + + + + + % + % + + + + Overflow + Überlauf + + + + Movement + Bewegung + + + + Remove object + Objekt entfernen + + + + + Primary skills + Primäre Fähigkeiten + + + + + Attack + Angriff + + + + + Defence + Verteidigung + + + + + Spell power + Zauberkraft + + + + + Knowledge + Wissen + + + + + Resources + Ressourcen + + + + + Artifacts + Artefakte + + + + + Spells + Zaubersprüche + + + + + Skills + Fertigkeiten + + + + + Creatures + Kreaturen + + + + Bonuses + Boni + + + + + Duration + Dauer + + + + + Type + Typ + + + + + Value + Wert + + + + Cast + Wirken + + + + Cast an adventure map spell + Einen Abenteuerkarten-Zauber wirken + + + + Spell + Zauberspruch + + + + Magic school level + Stufe der Zauberschule + + + + Limiter + Begrenzer + + + + Day of week + Tag der Woche + + + + Days passed + Verstrichene Tage + + + + Heroes + Helden + + + + Hero classes + Heldenklassen + + + + Players + Spieler + + + + None + Keine + + + + Day %1 + Tag %1 + + + + + Reward %1 + Belohnung %1 + + + + RumorSettings + + + Form + Formular + + + + Tavern rumors + Tavernen-Gerüchte + + + + Add + Hinzufügen + + + + Remove + Entfernen + + + + New rumor + Neues Gerücht + + + + TimedEvent + + + Timed event + Zeitgesteuertes Ereignis + + + + Event name + Name des Ereignisses + + + + Type event message text + Ereignistext eingeben + + + + affects human + beeinflusst Menschen + + + + affects AI + beeinflusst KI + + + + Day of first occurance + Tag des ersten Auftretens + + + + Repeat after (0 = no repeat) + Wiederholung nach (0 = keine Wiederholung) + + + + Affected players + Betroffene Spieler + + + + Resources + Ressourcen + + + + type + Typ + + + + qty + anz. + + + + Ok + Ok TownBulidingsWidget - + Buildings Gebäude + + Translations + + + Map translations + Übersetzungen der Karte + + + + Language + Sprache + + + + Suppported + Unterstützt + + + + String ID + String-ID + + + + Text + Text + + + + + Remove translation + Übersetzung entfernen + + + + Default language cannot be removed + Standardsprache kann nicht entfernt werden + + + + All existing text records for this language will be removed. Continue? + Alle vorhandenen Textsätze für diese Sprache werden entfernt. Weiter? + + Validator @@ -724,116 +1450,189 @@ Ergebnisse der Kartenvalidierung - + Map is not loaded Karte ist nicht geladen - + No factions allowed for player %1 Keine Fraktionen für Spieler %1 erlaubt - + No players allowed to play this map Keine Spieler dürfen diese Karte spielen - + Map is allowed for one player and cannot be started Karte ist für einen Spieler erlaubt und kann nicht gestartet werden - + No human players allowed to play this map Keine menschlichen Spieler dürfen diese Karte spielen - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Gepanzerte Instanz %1 ist UNFLAGGABLE, muss aber NEUTRAL oder Spielerbesitzer haben - + Object %1 is assigned to non-playable player %2 Objekt %1 ist dem nicht spielbaren Spieler %2 zugewiesen - + Town %1 has undefined owner %2 Stadt %1 hat undefinierten Besitzer %2 - + Prison %1 must be a NEUTRAL Gefängnis %1 muss NEUTRAL sein - + Hero %1 must have an owner Held %1 muss einen Besitzer haben - + Hero %1 is prohibited by map settings Held %1 ist durch Karteneinstellungen verboten - + Hero %1 has duplicate on map Held %1 hat Duplikat auf Karte - + Hero %1 has an empty type and must be removed Held %1 hat einen leeren Typ und muss entfernt werden - + Spell scroll %1 is prohibited by map settings Zauberschriftrolle %1 ist durch Karteneinstellungen verboten - + Spell scroll %1 doesn't have instance assigned and must be removed Zauberschriftrolle %1 hat keine Instanz zugewiesen und muss entfernt werden - + Artifact %1 is prohibited by map settings Artefakt %1 ist durch Karteneinstellungen verboten - + Player %1 doesn't have any starting town Spieler %1 hat keine Startstadt - + Map name is not specified Kartenname ist nicht angegeben - + Map description is not specified Kartenbeschreibung ist nicht angegeben - + Map contains object from mod "%1", but doesn't require it Karte enthält Objekt aus Mod "%1", benötigt es aber nicht - + Exception occurs during validation: %1 Bei der Validierung ist eine Ausnahme aufgetreten: %1 - + Unknown exception occurs during validation Unbekannte Ausnahme trat während der Validierung auf + + VictoryConditions + + + Form + Formular + + + + Victory message + Siegesnachricht + + + + Only for human players + Nur für menschliche Spieler + + + + Allow standard victory + Standardsieg zulassen + + + + Parameters + Parameter + + + + No special victory + Kein besonderer Sieg + + + + Capture artifact + Artefakt sammeln + + + + Hire creatures + Kreaturen anheuern + + + + Accumulate resources + Ressourcen ansammeln + + + + Construct building + Gebäude errichten + + + + Capture town + Stadt einnehmen + + + + Defeat hero + Held besiegen + + + + Transport artifact + Artefakt transportieren + + + + Kill monster + Monster töten + + WindowNewMap @@ -986,17 +1785,17 @@ Abbrechen - + No template Kein Template - + No template for parameters scecified. Random map cannot be generated. Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden. - + RMG failure RMG-Fehler @@ -1004,27 +1803,27 @@ main - + Filepath of the map to open. Dateipfad der zu öffnenden Karte. - + Extract original H3 archives into a separate folder. Extrahieren Sie die Original-H3-Archive in einen separaten Ordner. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Aus einem extrahierten Archiv zerlegt es TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 und Un44 in einzelne PNGs. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Aus einem extrahierten Archiv werden einzelne Bilder (aus dem Ordner "Images") von .pcx in png konvertiert. - + Delete original files, for the ones split / converted. Löschen Sie die Originaldateien für die gesplitteten/konvertierten Dateien. diff --git a/mapeditor/translation/polish.ts b/mapeditor/translation/polish.ts index df426e4e4..cb68b9c49 100644 --- a/mapeditor/translation/polish.ts +++ b/mapeditor/translation/polish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Ustawienia armii - + Wide formation Luźna formacja - + Tight formation Zwarta formacja + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nazwa mapy + + + + Map description + Opis mapy + + + + Limit maximum heroes level + Ogranicz maksymalny poziom bohaterów + + + + Difficulty + Poziom trudności + + GeneratorProgress @@ -27,6 +83,95 @@ Trwa generowanie mapy + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Komunikat o porażce + + + + 7 days without town + 7 dni bez miasta + + + + Parameters + Parametry + + + + No special loss + Bez specjalnych warunków porażki + + + + Lose castle + Utrata miasta + + + + Lose hero + Utrata bohatera + + + + Time expired + Upłynięcie czasu + + + + Days without town + Dni bez miasta + + MainWindow @@ -40,556 +185,499 @@ Plik - + Map Mapa - + Edit Edycja - + View Widok - + Player Gracz - + Toolbar Przybornik - + Minimap Minimapa - + Map Objects View Widok obiektów - + Browser Przeglądarka - + Inspector Inspektor - + Property Właściwość - + Value Wartość - - Terrains View - Widok terenów + + Tools + - - Brush - Pędzel + + Painting + - + Terrains Tereny - + Roads Drogi - + Rivers Rzeki - + + Preview + + + + Open Otwórz - + Save Zapisz - + New Nowy - + Save as... Zapisz jako... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Podziemia - - + + View underground Pokaż podziemia - + Pass Przejścia - + Cut Wytnij - + Copy Kopiuj - + Paste Wklej - + Fill Wypełnij - + Fills the selection with obstacles Wypełnia zaznaczony obszar przeszkodami - + Grid Siatka - + General Ogólne - + Map title and description Nazwa i opis mapy - + Players settings Ustawienia graczy - - + + Undo Cofnij - + Redo Przywróć - + Erase Wymaż - + Neutral Neutralny - + Validate Sprawdź - - - - + + + + Update appearance Aktualizuj wygląd - + Recreate obstacles Powtórnie stwórz przeszkody - + Player 1 Gracz 1 - + Player 2 Gracz 2 - + Player 3 Gracz 3 - + Player 4 Gracz 4 - + Player 5 Gracz 5 - + Player 6 Gracz 6 - + Player 7 Gracz 7 - + Player 8 Gracz 8 - + Export as... Eksportuj jako... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Potwierdzenie - + Unsaved changes will be lost, are you sure? Niezapisane zmiany zostaną utracone, jesteś pewny? - - Failed to open map - Nie udało się otworzyć mapy - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - - Cannot open map from this folder - Nie można otworzyć mapy z tego folderu - - - + Open map Otwórz mapę - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Wszystkie wspierane mapy (*.vmap *.h3m);;Mapy VCMI(*.vmap);;Mapy HoMM3(*.h3m) - - + Save map Zapisz mapę - - + VCMI maps (*.vmap) Mapy VCMI (*.vmap) - + Type Typ - + View surface Pokaż powierzchnię - + No objects selected Brak wybranych obiektów - + This operation is irreversible. Do you want to continue? Ta operacja jest nieodwracalna. Czy chcesz kontynuować? - + Errors occurred. %1 objects were not updated Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych - + Save to image Zapisz jako obraz + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Ustawienia mapy - + General Ogólne - - Map name - Nazwa mapy - - - - Map description - Opis mapy - - - - Limit maximum heroes level - Ogranicz maksymalny poziom bohaterów - - - - Difficulty - Poziom trudności - - - + Mods Modyfikacje - - Mandatory mods for playing this map - Obowiązkowe modyfikacje do uruchomienia tej mapy - - - - Mod name - Nazwa modyfikacji - - - - Version - Wersja - - - - Automatic assignment - Automatyczne przypisanie - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji - - - - Map objects mods - Mody od nowych obiektów mapy - - - - Set all mods having a game content as mandatory - Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe - - - - Full content mods - Mody od złożonej zawartości - - - + Events Zdarzenia - + Victory Zwycięstwo - - Victory message - Komunikat zwycięstwa - - - - Only for human players - Dotyczy tylko graczy ludzkich - - - - Allow standard victory - Także standardowy warunek zwycięstwa - - - - - Parameters - Parametry - - - + Loss Porażka - - 7 days without town - 7 dni bez miasta + + Timed + - - Defeat message - Komunikat o porażce + + Rumors + - + Abilities Umiejętności - + Spells Zaklęcia - + Artifacts Artefakty - + Heroes Bohaterowie - + Ok Ok - - - No special victory - Bez specjalnych warunków zwycięstwa - - - - Capture artifact - Zdobądź artefakt - - - - Hire creatures - Zdobądź stworzenia - - - - Accumulate resources - Zbierz zasoby - - - - Construct building - Zbuduj budynek - - - - Capture town - Zdobądź miasto - - - - Defeat hero - Pokonaj bohatera - - - - Transport artifact - Przenieś artefakt - - - - No special loss - Bez specjalnych warunków porażki - - - - Lose castle - Utrata miasta - - - - Lose hero - Utrata bohatera - - - - Time expired - Upłynięcie czasu - - - - Days without town - Dni bez miasta - MapView - + Can't place object Nie można umieścić obiektu @@ -597,59 +685,108 @@ MessageWidget - + Message Wiadomość - PlayerParams + ModSettings - No team - Brak drużyny + + Form + - + + Mandatory mods to play this map + + + + + Mod name + Nazwa modyfikacji + + + + Version + Wersja + + + + Automatic assignment + Automatyczne przypisanie + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji + + + + Map objects mods + Mody od nowych obiektów mapy + + + + Set all mods having a game content as mandatory + Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe + + + + Full content mods + Mody od złożonej zawartości + + + + PlayerParams + + Human/CPU Człowiek/Komputer - + CPU only Tylko komputer - + Team Drużyna - + Main town Główne miasto - + Color Kolor - + + ... + + + + Random faction Losowe miasto - + Generate hero at main Generuj bohatera w głównym - + (default) (domyślny) - + Player ID: %1 ID gracza: %1 @@ -677,45 +814,634 @@ Ok + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Cel misji + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefakty + + + + Spells + Zaklęcia + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + RewardsWidget - + Rewards Nagrody - - Remove selected - Usuń wybrane + + + + + Add + - Delete all - Usuń wszystkie + + + + Remove + - - Add or change - Dodaj lub zmień + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefakty + + + + + Spells + Zaklęcia + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Typ + + + + + Value + Wartość + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Bohaterowie + + + + Hero classes + + + + + Players + Gracze + + + + None + Brak + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Ok TownBulidingsWidget - + Buildings Budynki + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -724,116 +1450,189 @@ Wynik sprawdzenia mapy - + Map is not loaded Mapa nie została wczytana - + No factions allowed for player %1 Brak dozwolonych frakcji dla gracza %1 - + No players allowed to play this map Żaden gracz nie jest dozwolony do rozegrania tej mapy - + Map is allowed for one player and cannot be started Mapa jest dozwolona dla jednego gracza i nie może być rozpoczęta - + No human players allowed to play this map Żaden gracz ludzki nie został dozwolony by rozegrać tą mapę - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć właściciela neutralnego lub gracza - + Object %1 is assigned to non-playable player %2 Obiekt %1 został przypisany do niegrywalnego gracza %2 - + Town %1 has undefined owner %2 Miasto %1 ma niezdefiniowanego właściciela %2 - + Prison %1 must be a NEUTRAL Więzienie %1 musi być neutralne - + Hero %1 must have an owner Bohater %1 musi mieć właściciela - + Hero %1 is prohibited by map settings Bohater %1 jest zabroniony przez ustawienia mapy - + Hero %1 has duplicate on map Bohater %1 posiada duplikat na mapie - + Hero %1 has an empty type and must be removed Bohater %1 jest pustego typu i musi zostać usunięty - + Spell scroll %1 is prohibited by map settings Zwój z zaklęciem %1 jest zabroniony przez ustawienia mapy - + Spell scroll %1 doesn't have instance assigned and must be removed Zwój z zaklęciem %1 nie ma przypisanej instancji i musi zostać usunięty - + Artifact %1 is prohibited by map settings Artefakt %1 jest zabroniony przez ustawienia mapy - + Player %1 doesn't have any starting town Gracz %1 nie ma żadnego startowego miasta - + Map name is not specified Nazwa mapy nie została ustawiona - + Map description is not specified Opis mapy nie został ustawiony - + Map contains object from mod "%1", but doesn't require it Mapa zawiera obiekt z modyfikacji %1 ale nie wymaga tej modyfikacji - + Exception occurs during validation: %1 Wystąpił wyjątek podczas walidacji: %1 - + Unknown exception occurs during validation Wystąpił nieznane wyjątek podczas walidacji + + VictoryConditions + + + Form + + + + + Victory message + Komunikat zwycięstwa + + + + Only for human players + Dotyczy tylko graczy ludzkich + + + + Allow standard victory + Także standardowy warunek zwycięstwa + + + + Parameters + Parametry + + + + No special victory + Bez specjalnych warunków zwycięstwa + + + + Capture artifact + Zdobądź artefakt + + + + Hire creatures + Zdobądź stworzenia + + + + Accumulate resources + Zbierz zasoby + + + + Construct building + Zbuduj budynek + + + + Capture town + Zdobądź miasto + + + + Defeat hero + Pokonaj bohatera + + + + Transport artifact + Przenieś artefakt + + + + Kill monster + + + WindowNewMap @@ -986,17 +1785,17 @@ Anuluj - + No template Brak szablonu - + No template for parameters scecified. Random map cannot be generated. Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana. - + RMG failure Niepowodzenie generatora map losowych @@ -1004,27 +1803,27 @@ main - + Filepath of the map to open. Lokalizacja pliku mapy do otworzenia. - + Extract original H3 archives into a separate folder. Wyodrębnij oryginalne archiwa H3 do osobnego folderu. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Z wyodrębnionego archiwum, rozdzielenie TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 i Un44 do poszczególnych plików PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Z wyodrębnionego archiwum, konwersja pojedynczych obrazków (znalezionych w folderze Images) z .pcx do .png. - + Delete original files, for the ones split / converted. Usuń oryginalne pliki, dla już rozdzielonych / skonwertowanych. diff --git a/mapeditor/translation/russian.ts b/mapeditor/translation/russian.ts index 25e4ae21b..4244e36fa 100644 --- a/mapeditor/translation/russian.ts +++ b/mapeditor/translation/russian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Настройки армии - + Wide formation Расширенная формация - + Tight formation Суженная формация + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Название карты + + + + Map description + Описание карты + + + + Limit maximum heroes level + + + + + Difficulty + Сложность + + GeneratorProgress @@ -27,6 +83,95 @@ Создание карты + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Сообщение о поражении + + + + 7 days without town + 7 дней без городов + + + + Parameters + Параметры + + + + No special loss + Нет специального поражения + + + + Lose castle + Потерять город + + + + Lose hero + Потерять героя + + + + Time expired + Не успеть ко времени + + + + Days without town + Провести без городов + + MainWindow @@ -40,546 +185,499 @@ Файл - + Map Карта - + Edit Правка - + View Вид - + Player Игрок - + Toolbar Панель инструментов - + Minimap Мини-карта - + Map Objects View Объекты карты - + Browser Навигатор - + Inspector Инспектор - + Property Свойство - + Value Значение - - Terrains View - Кисти земель + + Tools + - - Brush - Кисть + + Painting + - + Terrains Земли - + Roads Дороги - + Rivers Реки - + + Preview + + + + Open Открыть - + Save Сохранить - + New Создать - + Save as... Сохранить как - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/Н - - + + View underground Вид на подземелье - + Pass Проходимость - + Cut Вырезать - + Copy Копировать - + Paste Вставить - + Fill Заливка - + Fills the selection with obstacles Заливает выбранное препятствиями - + Grid Сетка - + General Общее - + Map title and description Название и описание карты - + Players settings Настройки игроков - - + + Undo Отменить - + Redo Повторить - + Erase Удалить - + Neutral Нейтральный - + Validate Проверить - - - - + + + + Update appearance Обновить вид - + Recreate obstacles Обновить препятствия - + Player 1 Игрок 1 - + Player 2 Игрок 2 - + Player 3 Игрок 3 - + Player 4 Игрок 4 - + Player 5 Игрок 5 - + Player 6 Игрок 6 - + Player 7 Игрок 7 - + Player 8 Игрок 8 - + Export as... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - + Open map Открыть карту - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Все поддерживаемые карты (*.vmap *.h3m);;Карты VCMI (*.vmap);;Карты Героев III (*.h3m) - - + Save map Сохранить карту - - + VCMI maps (*.vmap) Карты VCMI (*.vmap) - + Type Тип - + View surface Вид на поверхность - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Настройки карты - + General Общее - - Map name - Название карты - - - - Map description - Описание карты - - - - Limit maximum heroes level - - - - - Difficulty - Сложность - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events События - + Victory Победа - - Victory message - Сообщение о победе - - - - Only for human players - Только для игроков-людей - - - - Allow standard victory - Разрешить стандартную победу - - - - - Parameters - Параметры - - - + Loss Поражение - - 7 days without town - 7 дней без городов + + Timed + - - Defeat message - Сообщение о поражении + + Rumors + - + Abilities Способности - + Spells Заклинания - + Artifacts Артефакты - + Heroes Герои - + Ok ОК - - - No special victory - Нет специальной победы - - - - Capture artifact - Взять артефакт - - - - Hire creatures - Нанять существ - - - - Accumulate resources - Собрать ресурсы - - - - Construct building - Построить - - - - Capture town - Захватить город - - - - Defeat hero - Победить героя - - - - Transport artifact - Переместить артефакт - - - - No special loss - Нет специального поражения - - - - Lose castle - Потерять город - - - - Lose hero - Потерять героя - - - - Time expired - Не успеть ко времени - - - - Days without town - Провести без городов - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Сообщение - PlayerParams + ModSettings - No team - Без команды + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Человек/ИИ - + CPU only Только ИИ - + Team Команда - + Main town Главный город - + Color - + + ... + + + + Random faction Случайная фракция - + Generate hero at main Создать героя - + (default) (по умолчанию) - + Player ID: %1 Игрок: %1 @@ -667,45 +814,634 @@ ОК + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Цель миссии + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакты + + + + Spells + Заклинания + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + RewardsWidget - + Rewards Награды - - Remove selected - Удалить выбранное + + + + + Add + - Delete all - Удалить все + + + + Remove + - - Add or change - Добавить/Изменить + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакты + + + + + Spells + Заклинания + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значение + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герои + + + + Hero classes + + + + + Players + + + + + None + Нет + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + ОК TownBulidingsWidget - + Buildings Постройки + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Результаты проверки карты - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 У города %1 неопределенный владелец %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + Сообщение о победе + + + + Only for human players + Только для игроков-людей + + + + Allow standard victory + Разрешить стандартную победу + + + + Parameters + Параметры + + + + No special victory + Нет специальной победы + + + + Capture artifact + Взять артефакт + + + + Hire creatures + Нанять существ + + + + Accumulate resources + Собрать ресурсы + + + + Construct building + Построить + + + + Capture town + Захватить город + + + + Defeat hero + Победить героя + + + + Transport artifact + Переместить артефакт + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Отмена - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Путь к файлу карты для открытия. - + Extract original H3 archives into a separate folder. Распаковать архивы оригинальных Героев III в отдельную папку. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Разделение в распакованном архиве TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 и Un44 на отдельные PNG. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Преобразование в расспакованном архиве изображений .pcx в .png. - + Delete original files, for the ones split / converted. Удалить оригиналы для преобразованных файлов. diff --git a/mapeditor/translation/spanish.ts b/mapeditor/translation/spanish.ts index 8018b8000..8a33ffbc1 100644 --- a/mapeditor/translation/spanish.ts +++ b/mapeditor/translation/spanish.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Configuracion del Ejército - + Wide formation Formación amplia - + Tight formation Formación ajustada + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Nombre del mapa + + + + Map description + Descripción del mapa + + + + Limit maximum heroes level + + + + + Difficulty + Dificultad + + GeneratorProgress @@ -27,6 +83,95 @@ Generando mapa + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Mensaje de derrota + + + + 7 days without town + 7 días sin ciudad + + + + Parameters + Parámetros + + + + No special loss + Sin pérdida especial + + + + Lose castle + Perder castillo + + + + Lose hero + Perder héroe + + + + Time expired + Expiró el tiempo + + + + Days without town + Días sin ciudad + + MainWindow @@ -40,546 +185,499 @@ Archivo - + Map Mapa - + Edit Editar - + View Ver - + Player Jugador - + Toolbar Barra de herramientas - + Minimap Miniatura del mapa - + Map Objects View Vista de Objetos del Mapa - + Browser Navegador - + Inspector Inspector - + Property Propiedad - + Value Valor - - Terrains View - Vista de Terrenos + + Tools + - - Brush - Pincel + + Painting + - + Terrains Terrenos - + Roads Caminos - + Rivers Ríos - + + Preview + + + + Open Abrir - + Save Guardar - + New Nuevo - + Save as... Guardar como... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G Subterráneo/Superficie - - + + View underground Ver subterráneo - + Pass Pasar - + Cut Cortar - + Copy Copiar - + Paste Pegar - + Fill Rellenar - + Fills the selection with obstacles Rellena la selección con obstáculos - + Grid Rejilla - + General General - + Map title and description Título y descripción del mapa - + Players settings Configuración de jugadores - - + + Undo Deshacer - + Redo Rehacer - + Erase Borrar - + Neutral Neutral - + Validate Validar - - - - + + + + Update appearance Actualizar apariencia - + Recreate obstacles Recrear obstáculos - + Player 1 Jugador 1 - + Player 2 Jugador 2 - + Player 3 Jugador 3 - + Player 4 Jugador 4 - + Player 5 Jugador 5 - + Player 6 Jugador 6 - + Player 7 Jugador 7 - + Player 8 Jugador 8 - + Export as... Exportar como... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Confirmación - + Unsaved changes will be lost, are you sure? Los cambios no guardados se perderán. Está usted seguro ? - - Failed to open map - No se pudo abrir el mapa - - - - Cannot open map from this folder - No se puede abrir el mapa de esta carpeta - - - + Open map Abrir mapa - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Todos los mapas soportados (*.vmap *.h3m);;Mapas VCMI (*.vmap);;Mapas HoMM3 (*.h3m) - - + Save map Guardar mapa - - + VCMI maps (*.vmap) Mapas VCMI (*.vmap) - + Type Tipo - + View surface Ver superficie - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Configuración del mapa - + General General - - Map name - Nombre del mapa - - - - Map description - Descripción del mapa - - - - Limit maximum heroes level - - - - - Difficulty - Dificultad - - - + Mods - - Mandatory mods for playing this map - - - - - Mod name - - - - - Version - - - - - Automatic assignment - - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - - - - - Map objects mods - - - - - Set all mods having a game content as mandatory - - - - - Full content mods - - - - + Events Eventos - + Victory Victoria - - Victory message - Mensaje de victoria - - - - Only for human players - Solo para jugadores humanos - - - - Allow standard victory - Permitir victoria estándar - - - - - Parameters - Parámetros - - - + Loss Derrota - - 7 days without town - 7 días sin ciudad + + Timed + - - Defeat message - Mensaje de derrota + + Rumors + - + Abilities Habilidades - + Spells Hechizos - + Artifacts Artefactos - + Heroes Héroes - + Ok Aceptar - - - No special victory - Sin victoria especial - - - - Capture artifact - Capturar artefacto - - - - Hire creatures - Contratar criaturas - - - - Accumulate resources - Acumular recursos - - - - Construct building - Construir edificio - - - - Capture town - Capturar ciudad - - - - Defeat hero - Vencer héroe - - - - Transport artifact - Transportar artefacto - - - - No special loss - Sin pérdida especial - - - - Lose castle - Perder castillo - - - - Lose hero - Perder héroe - - - - Time expired - Expiró el tiempo - - - - Days without town - Días sin ciudad - MapView - + Can't place object @@ -587,59 +685,108 @@ MessageWidget - + Message Mensaje - PlayerParams + ModSettings - No team - Sin equipo + + Form + - + + Mandatory mods to play this map + + + + + Mod name + + + + + Version + + + + + Automatic assignment + + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + + + + + Map objects mods + + + + + Set all mods having a game content as mandatory + + + + + Full content mods + + + + + PlayerParams + + Human/CPU Humano/CPU - + CPU only Sólo CPU - + Team Equipo - + Main town Ciudad principal - + Color - + + ... + + + + Random faction Facción aleatoria - + Generate hero at main Generar héroe en la ciudad principal - + (default) (predeterminado) - + Player ID: %1 ID de jugador: %1 @@ -667,45 +814,634 @@ Aceptar + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Objetivo de la misión + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Artefactos + + + + Spells + Hechizos + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + RewardsWidget - + Rewards Recompensas - - Remove selected - Eliminar seleccionado + + + + + Add + - Delete all - Borrar todo + + + + Remove + - - Add or change - Añadir o modificar + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Artefactos + + + + + Spells + Hechizos + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Tipo + + + + + Value + Valor + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Héroes + + + + Hero classes + + + + + Players + Jugadores + + + + None + Ninguno + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Aceptar TownBulidingsWidget - + Buildings Edificios + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -714,116 +1450,189 @@ Resultados de la validación del mapa - + Map is not loaded No se ha cargado ningún mapa - + No factions allowed for player %1 - + No players allowed to play this map No hay jugadores autorizados a jugar en este mapa - + Map is allowed for one player and cannot be started El mapa está autorizado para un jugador y no se puede iniciar - + No human players allowed to play this map Ningún jugador humano puede jugar en este mapa - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner La instancia protegida %1 NOSEPUEDEMARCAR, pero debe tener un propietario NEUTRAL o jugador - + Object %1 is assigned to non-playable player %2 El artículo %1 está asignado al jugador no jugable %2 - + Town %1 has undefined owner %2 La ciudad %1 no tiene un propietario definido %2 - + Prison %1 must be a NEUTRAL %1 prisión debe ser NEUTRA - + Hero %1 must have an owner El héroe %1 debe tener un propietario - + Hero %1 is prohibited by map settings El héroe %1 está prohibido por la configuración del mapa - + Hero %1 has duplicate on map El héroe %1 tiene un duplicado en el mapa - + Hero %1 has an empty type and must be removed El héroe %1 tiene un tipo vacío y debe eliminarse - + Spell scroll %1 is prohibited by map settings %1 desplazamiento de hechizos está prohibido por la configuración del mapa - + Spell scroll %1 doesn't have instance assigned and must be removed Pergamino ortográfico %1 no tiene una instancia asignada y debe eliminarse - + Artifact %1 is prohibited by map settings El artefacto %1 está prohibido por la configuración del mapa - + Player %1 doesn't have any starting town El jugador %1 no tiene ciudad inicial - + Map name is not specified No se especifica el nombre del mapa - + Map description is not specified No se especifica la descripción del mapa - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 Se produce una excepción durante la validación: %1 - + Unknown exception occurs during validation Se produce una excepción desconocida durante la validación + + VictoryConditions + + + Form + + + + + Victory message + Mensaje de victoria + + + + Only for human players + Solo para jugadores humanos + + + + Allow standard victory + Permitir victoria estándar + + + + Parameters + Parámetros + + + + No special victory + Sin victoria especial + + + + Capture artifact + Capturar artefacto + + + + Hire creatures + Contratar criaturas + + + + Accumulate resources + Acumular recursos + + + + Construct building + Construir edificio + + + + Capture town + Capturar ciudad + + + + Defeat hero + Vencer héroe + + + + Transport artifact + Transportar artefacto + + + + Kill monster + + + WindowNewMap @@ -976,17 +1785,17 @@ Cancelar - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -994,27 +1803,27 @@ main - + Filepath of the map to open. Ruta del archivo del mapa a abrir. - + Extract original H3 archives into a separate folder. Extraer archivos originales de H3 en una carpeta separada. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Desde un archivo extraído, separa TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 y Un44 en imágenes PNG individuales. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Desde un archivo extraído, convierte imágenes individuales (encontradas en la carpeta Imágenes) de .pcx a png. - + Delete original files, for the ones split / converted. Eliminar archivos originales, por los que se han separado / convertido. diff --git a/mapeditor/translation/ukrainian.ts b/mapeditor/translation/ukrainian.ts index 2233f658e..0a62b7157 100644 --- a/mapeditor/translation/ukrainian.ts +++ b/mapeditor/translation/ukrainian.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Налаштування армії - + Wide formation Широка формація - + Tight formation Щільна формація + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Назва мапи + + + + Map description + Опис мапи + + + + Limit maximum heroes level + Обмежити максимальний рівень героїв + + + + Difficulty + Складність + + GeneratorProgress @@ -27,6 +83,95 @@ Побудова мапи + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Повідомлення про програш + + + + 7 days without town + 7 днів без міста + + + + Parameters + Параметри + + + + No special loss + Немає особливої поразки + + + + Lose castle + Втратити місто + + + + Lose hero + Втратити героя + + + + Time expired + Закінчився час + + + + Days without town + Дні без міста + + MainWindow @@ -40,556 +185,499 @@ Файл - + Map Мапа - + Edit Редагування - + View Вигляд - + Player Гравець - + Toolbar Панель інструментів - + Minimap Мінімапа - + Map Objects View Перегляд об'єктів мапи - + Browser Навігатор - + Inspector Інспектор - + Property Властивість - + Value Значення - - Terrains View - Перегляд поверхні + + Tools + - - Brush - Кисть + + Painting + - + Terrains Землі - + Roads Шляхи - + Rivers Річки - + + Preview + + + + Open Відкрити - + Save Зберегти - + New Створити - + Save as... Зберегти як... - + Ctrl+Shift+S Ctrl+Shift+S - + U/G П/З - - + + View underground Дивитись підземелля - + Pass Прохідність - + Cut Вирізати - + Copy Скопіювати - + Paste Вставити - + Fill Заповнити - + Fills the selection with obstacles Заповнити перешкодами - + Grid Сітка - + General Загальний - + Map title and description Назва та опис мапи - + Players settings Налаштування гравців - - + + Undo Відмінити - + Redo Повторити - + Erase Стерти - + Neutral Нейтральний - + Validate Перевірити - - - - + + + + Update appearance Оновити вигляд - + Recreate obstacles Оновити перешкоди - + Player 1 Гравець 1 - + Player 2 Гравець 2 - + Player 3 Гравець 3 - + Player 4 Гравець 4 - + Player 5 Гравець 5 - + Player 6 Гравець 6 - + Player 7 Гравець 7 - + Player 8 Гравець 8 - + Export as... Експортувати як... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation - + Unsaved changes will be lost, are you sure? - - Failed to open map - - - - - Cannot open map from this folder - - - - - Confirmation - - - - - Unsaved changes will be lost, are you sure? - - - - + Open map Відкрити мапу - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Всі підтримувані мапи (*.vmap *.h3m);;Мапи VCMI (*.vmap);;Мапи HoMM3 (*.h3m) - - + Save map Зберегти мапу - - + VCMI maps (*.vmap) Мапи VCMI - + Type Тип - + View surface Дивитись поверхню - + No objects selected - + This operation is irreversible. Do you want to continue? - + Errors occurred. %1 objects were not updated - + Save to image + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Налаштування мапи - + General Загальний - - Map name - Назва мапи - - - - Map description - Опис мапи - - - - Limit maximum heroes level - Обмежити максимальний рівень героїв - - - - Difficulty - Складність - - - + Mods Модифікації - - Mandatory mods for playing this map - Модифікації необхідні для гри на мапи - - - - Mod name - Назва модифікації - - - - Version - Версія - - - - Automatic assignment - Автоматичне визначення - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів - - - - Map objects mods - Моди з об'єктами мапи - - - - Set all mods having a game content as mandatory - Встановити усі моди з ігровим контентом як обов'язкові - - - - Full content mods - Усі модифікації - - - + Events Події - + Victory Перемога - - Victory message - Повідомлення про перемогу - - - - Only for human players - Тільки для гравців-людей - - - - Allow standard victory - Дозволити типову перемогу - - - - - Parameters - Параметри - - - + Loss Програш - - 7 days without town - 7 днів без міста + + Timed + - - Defeat message - Повідомлення про програш + + Rumors + - + Abilities Уміння - + Spells Закляття - + Artifacts Артефакти - + Heroes Герої - + Ok Підтвердити - - - No special victory - Немає особливої перемоги - - - - Capture artifact - Отримати артефакт - - - - Hire creatures - Найняти істот - - - - Accumulate resources - Накопичити ресурси - - - - Construct building - Побудувати будівлю - - - - Capture town - Захопити місто - - - - Defeat hero - Перемогти героя - - - - Transport artifact - Доставити артефакт - - - - No special loss - Немає особливої поразки - - - - Lose castle - Втратити місто - - - - Lose hero - Втратити героя - - - - Time expired - Закінчився час - - - - Days without town - Дні без міста - MapView - + Can't place object @@ -597,55 +685,108 @@ MessageWidget - + Message Повідомлення + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Назва модифікації + + + + Version + Версія + + + + Automatic assignment + Автоматичне визначення + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Встановити необхідні модифікації на основі об'єктів, розміщених на мапі. Цей метод може викликати проблеми, якщо у вас є налаштовані нагороди, гарнізони тощо з модів + + + + Map objects mods + Моди з об'єктами мапи + + + + Set all mods having a game content as mandatory + Встановити усі моди з ігровим контентом як обов'язкові + + + + Full content mods + Усі модифікації + + PlayerParams - + Human/CPU Людина/Комп'ютер - + CPU only Тільки комп'ютер - + Team Команда - + Main town Головне місто - + Color Колір - + + ... + + + + Random faction Випадкова фракція - + Generate hero at main Згенерувати героя - + (default) (за замовчуванням) - + Player ID: %1 Гравець %1 @@ -673,45 +814,634 @@ Підтвердити + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Мета місії + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Артефакти + + + + Spells + Закляття + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + RewardsWidget - + Rewards Винагороди - - Remove selected - Видалити вибране + + + + + Add + - Delete all - Видалити усі + + + + Remove + - - Add or change - Додати або змінити + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Артефакти + + + + + Spells + Закляття + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Тип + + + + + Value + Значення + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Герої + + + + Hero classes + + + + + Players + + + + + None + Відсутня + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Підтвердити TownBulidingsWidget - + Buildings Будівлі + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -720,116 +1450,189 @@ Результати валідації карти - + Map is not loaded - + No factions allowed for player %1 - + No players allowed to play this map - + Map is allowed for one player and cannot be started - + No human players allowed to play this map - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner - + Object %1 is assigned to non-playable player %2 - + Town %1 has undefined owner %2 Місто %1 має невизначеного володаря %2 - + Prison %1 must be a NEUTRAL - + Hero %1 must have an owner - + Hero %1 is prohibited by map settings - + Hero %1 has duplicate on map - + Hero %1 has an empty type and must be removed - + Spell scroll %1 is prohibited by map settings - + Spell scroll %1 doesn't have instance assigned and must be removed - + Artifact %1 is prohibited by map settings - + Player %1 doesn't have any starting town - + Map name is not specified - + Map description is not specified - + Map contains object from mod "%1", but doesn't require it - + Exception occurs during validation: %1 - + Unknown exception occurs during validation + + VictoryConditions + + + Form + + + + + Victory message + Повідомлення про перемогу + + + + Only for human players + Тільки для гравців-людей + + + + Allow standard victory + Дозволити типову перемогу + + + + Parameters + Параметри + + + + No special victory + Немає особливої перемоги + + + + Capture artifact + Отримати артефакт + + + + Hire creatures + Найняти істот + + + + Accumulate resources + Накопичити ресурси + + + + Construct building + Побудувати будівлю + + + + Capture town + Захопити місто + + + + Defeat hero + Перемогти героя + + + + Transport artifact + Доставити артефакт + + + + Kill monster + + + WindowNewMap @@ -982,17 +1785,17 @@ Скасувати - + No template - + No template for parameters scecified. Random map cannot be generated. - + RMG failure @@ -1000,27 +1803,27 @@ main - + Filepath of the map to open. - + Extract original H3 archives into a separate folder. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. - + Delete original files, for the ones split / converted. diff --git a/mapeditor/translation/vietnamese.ts b/mapeditor/translation/vietnamese.ts index 0d37f5242..4794f75fd 100644 --- a/mapeditor/translation/vietnamese.ts +++ b/mapeditor/translation/vietnamese.ts @@ -4,21 +4,77 @@ ArmyWidget - + Army settings Cài đặt quân - + Wide formation Đội hình rộng - + Tight formation Đội hình kín + + EventSettings + + + Form + + + + + Timed events + + + + + Add + + + + + Remove + + + + + New event + + + + + GeneralSettings + + + Form + + + + + Map name + Tên bản đồ + + + + Map description + Mô tả bản đồ + + + + Limit maximum heroes level + Giới hạn cấp tướng tối đa + + + + Difficulty + Độ khó + + GeneratorProgress @@ -27,6 +83,95 @@ Tạo bản đồ + + HeroSkillsWidget + + + Hero skills + + + + + + + + TextLabel + + + + + Add + + + + + Remove + + + + + Skill + + + + + Level + + + + + Customize skills + + + + + LoseConditions + + + Form + + + + + Defeat message + Thông báo thất bại + + + + 7 days without town + 7 ngày không có thành + + + + Parameters + Tham số + + + + No special loss + Không có thất bại đặc biệt + + + + Lose castle + Mất thành + + + + Lose hero + Mất tướng + + + + Time expired + Hết thời gian + + + + Days without town + Số ngày không có thành + + MainWindow @@ -40,546 +185,499 @@ Tập tin - + Map Bản đồ - + Edit Hiệu chỉnh - + View Xem - + Player Người chơi - + Toolbar Thanh công cụ - + Minimap Bản đồ nhỏ - + Map Objects View Xem đối tượng bản đồ - + Browser Duyệt - + Inspector Giám định - + Property Đặc tính - + Value Giá trị - - Terrains View - Xem địa hình - - - - Brush - Quét - - - + Terrains Địa hình - + Roads Đường - + Rivers Sông - + Open Mở - + Save Lưu - + New Tạo mới - - Save as - Lưu vào + + Tools + - + + Painting + + + + + Preview + + + + + Save as... + + + + Ctrl+Shift+S Ctrl+Shift+S - + U/G U/G - - + + View underground Xem hang ngầm - + Pass Đi qua - + Cut Cắt - + Copy Sao chép - + Paste Dán - + Fill Làm đầy - + Fills the selection with obstacles Làm đầy vùng chọn với vật cản - + Grid Đường kẻ - + General Chung - + Map title and description Tên bản đồ và mô tả - + Players settings Cài đặt người chơi - - + + Undo Hoàn tác - + Redo Làm lại - + Erase Xóa - + Neutral Trung lập - + Validate Hiệu lực - - - - + + + + Update appearance Cập nhật hiện thị - + Recreate obstacles Tạo lại vật cản - + Player 1 Người chơi 1 - + Player 2 Người chơi 2 - + Player 3 Người chơi 3 - + Player 4 Người chơi 4 - + Player 5 Người chơi 5 - + Player 6 Người chơi 6 - + Player 7 Người chơi 7 - + Player 8 Người chơi 8 - + Export as... Xuất thành... - + + Translations + + + + + Ctrl+T + + + + + + h3m converter + + + + + Lock + + + + + Lock objects on map to avoid unnecessary changes + + + + + Ctrl+L + + + + + Unlock + + + + + Unlock all objects on the map + + + + + Ctrl+Shift+L + + + + + Zoom in + + + + + Ctrl+= + + + + + Zoom out + + + + + Ctrl+- + + + + + Zoom reset + + + + + Ctrl+Shift+= + + + + Confirmation Xác nhận - + Unsaved changes will be lost, are you sure? Thay đổi chưa lưu sẽ bị mất, bạn có chắc chắn? - - Failed to open map - Không thể mở bản đồ - - - - Cannot open map from this folder - Không thể mở bản đồ từ thư mục này - - - + Open map Mở bản đồ - + All supported maps (*.vmap *.h3m);;VCMI maps(*.vmap);;HoMM3 maps(*.h3m) Tất cả bản đồ hỗ trợ (*.vmap *.h3m);;Bản đồ VCMI (*.vmap);;Bản đồ HoMM3 (*.h3m) - - + Save map Lưu bản đồ - - + VCMI maps (*.vmap) Bản đồ VCMI (*.vmap) - + Type Loại - + View surface Xem bề mặt - + No objects selected Không mục tiêu được chọn - + This operation is irreversible. Do you want to continue? Thao tác này không thể đảo ngược. Bạn muốn tiếp tục? - + Errors occurred. %1 objects were not updated Xảy ra lỗi. %1 mục tiêu không được cập nhật - + Save to image Lưu thành ảnh + + + Select maps to convert + + + + + HoMM3 maps(*.h3m) + + + + + Choose directory to save converted maps + + + + + Operation completed + + + + + Successfully converted %1 maps + + + + + Failed to convert the map. Abort operation + + MapSettings - + Map settings Cài đặt bản đồ - + General Chung - - Map name - Tên bản đồ - - - - Map description - Mô tả bản đồ - - - - Limit maximum heroes level - Giới hạn cấp tướng tối đa - - - - Difficulty - Độ khó - - - + Mods Bản sửa đổi - - Mandatory mods for playing this map - Bản sửa đổi cần để chơi bản đồ này - - - - Mod name - Tên bản sửa đổi - - - - Version - Phiên bản - - - - Automatic assignment - Gán tự động - - - - Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods - Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi - - - - Map objects mods - Bản sửa đổi vật thể trên bản đồ - - - - Set all mods having a game content as mandatory - Tập bản sửa đổi cần cho nội dung trò chơi - - - - Full content mods - Bản sửa đổi nội dung đầy đủ - - - + Events Sự kiện - + Victory Chiến thắng - - Victory message - Thông báo chiến thắng - - - - Only for human players - Chỉ cho người - - - - Allow standard victory - Cho phép chiến thắng thông thường - - - - - Parameters - Tham số - - - + Loss Thất bại - - 7 days without town - 7 ngày không có thành + + Timed + - - Defeat message - Thông báo thất bại + + Rumors + - + Abilities Năng lực - + Spells Phép - + Artifacts Vật phẩm - + Heroes Tướng - + Ok Đồng ý - - - No special victory - Không có chiến thắng đặc biệt - - - - Capture artifact - Đoạt vật phẩm - - - - Hire creatures - Thuê quái - - - - Accumulate resources - Cộng dồn tài nguyên - - - - Construct building - Xây công trình - - - - Capture town - Đoạt thành - - - - Defeat hero - Đánh bại tướng - - - - Transport artifact - Vận chuyển vật phẩm - - - - No special loss - Không có thất bại đặc biệt - - - - Lose castle - Mất thành - - - - Lose hero - Mất tướng - - - - Time expired - Hết thời gian - - - - Days without town - Số ngày không có thành - MapView - + Can't place object Không thể đặt vật thể @@ -587,55 +685,108 @@ MessageWidget - + Message Thông báo + + ModSettings + + + Form + + + + + Mandatory mods to play this map + + + + + Mod name + Tên bản sửa đổi + + + + Version + Phiên bản + + + + Automatic assignment + Gán tự động + + + + Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods + Tập bản sửa đổi dựa vào vật thể đặt trên bản đồ. Phương pháp này có thể có vấn đề nếu bạn tùy chỉnh phần thưởng, lính đồn trú... từ các bản sửa đổi + + + + Map objects mods + Bản sửa đổi vật thể trên bản đồ + + + + Set all mods having a game content as mandatory + Tập bản sửa đổi cần cho nội dung trò chơi + + + + Full content mods + Bản sửa đổi nội dung đầy đủ + + PlayerParams - + Human/CPU Người/Máy - + CPU only Chỉ máy - + Team Phe - + Main town Thành chính - + Color Màu - + + ... + + + + Random faction Thành ngẫu nhiên - + Generate hero at main Tạo tướng ban đầu - + (default) (mặc định) - + Player ID: %1 ID người chơi: %1 @@ -663,45 +814,634 @@ Đồng ý + + PortraitWidget + + + Portrait + + + + + + ... + + + + + Default + + + + + QObject + + + Beginner + + + + + Advanced + + + + + Expert + + + + + Compliant + + + + + Friendly + + + + + Aggressive + + + + + Hostile + + + + + Savage + + + + + + neutral + + + + + UNFLAGGABLE + + + QuestWidget - + Mission goal Mục tiêu nhiệm vụ + + + Day of week + + + + + Days passed + + + + + Hero level + + + + + Hero experience + + + + + Spell points + + + + + % + + + + + Kill hero/monster + + + + + ... + + + + + Primary skills + + + + + Attack + + + + + Defence + + + + + Spell power + + + + + Knowledge + + + + + Resources + + + + + Artifacts + Vật phẩm + + + + Spells + Phép + + + + Skills + + + + + Creatures + + + + + Add + + + + + Remove + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + RewardsWidget - + Rewards Phần thưởng - - Remove selected - Bỏ chọn + + + + + Add + - Delete all - Xóa tất cả + + + + Remove + - - Add or change - Thêm hoặc sửa + + Visit mode + + + + + Select mode + + + + + On select text + + + + + Can refuse + + + + + Reset parameters + + + + + Period + + + + + days + + + + + Reset visitors + + + + + Reset rewards + + + + + Window type + + + + + Event info + + + + + Message to be displayed on granting of this reward + + + + + Reward + + + + + + Hero level + + + + + + Hero experience + + + + + + Spell points + + + + + + + + % + + + + + Overflow + + + + + Movement + + + + + Remove object + + + + + + Primary skills + + + + + + Attack + + + + + + Defence + + + + + + Spell power + + + + + + Knowledge + + + + + + Resources + + + + + + Artifacts + Vật phẩm + + + + + Spells + Phép + + + + + Skills + + + + + + Creatures + + + + + Bonuses + + + + + + Duration + + + + + + Type + Loại + + + + + Value + Giá trị + + + + Cast + + + + + Cast an adventure map spell + + + + + Spell + + + + + Magic school level + + + + + Limiter + + + + + Day of week + + + + + Days passed + + + + + Heroes + Tướng + + + + Hero classes + + + + + Players + Người chơi + + + + None + Không + + + + Day %1 + + + + + + Reward %1 + + + + + RumorSettings + + + Form + + + + + Tavern rumors + + + + + Add + + + + + Remove + + + + + New rumor + + + + + TimedEvent + + + Timed event + + + + + Event name + + + + + Type event message text + + + + + affects human + + + + + affects AI + + + + + Day of first occurance + + + + + Repeat after (0 = no repeat) + + + + + Affected players + + + + + Resources + + + + + type + + + + + qty + + + + + Ok + Đồng ý TownBulidingsWidget - + Buildings Công trình + + Translations + + + Map translations + + + + + Language + + + + + Suppported + + + + + String ID + + + + + Text + + + + + + Remove translation + + + + + Default language cannot be removed + + + + + All existing text records for this language will be removed. Continue? + + + Validator @@ -710,116 +1450,189 @@ Kết quả kiểm định bản đồ - + Map is not loaded Bản đồ không thể tải - + No factions allowed for player %1 Không có tộc được phép cho người chơi %1 - + No players allowed to play this map Không có người chơi được phép chơi bản đồ này - + Map is allowed for one player and cannot be started Bản đồ cho phép 1 người chơi nhưng không thể bắt đầu - + No human players allowed to play this map Không có người nào được phép chơi bản đồ này - + Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner Thực thể %1 không gắn cờ nhưng phải có quái trung lập hoặc người chơi sở hữu - + Object %1 is assigned to non-playable player %2 Vật thể %1 được gán cho người không thể chơi %2 - + Town %1 has undefined owner %2 Thành %1 có chủ nhân không xác định %2 - + Prison %1 must be a NEUTRAL Nhà giam %1 phải trung lập - + Hero %1 must have an owner Tướng %1 phải có chủ - + Hero %1 is prohibited by map settings Tướng %1 bị cấm bởi bản đồ - + Hero %1 has duplicate on map Tướng %1 bị trùng trên bản đồ - + Hero %1 has an empty type and must be removed Tướng %1 có kiểu rỗng và phải được xóa - + Spell scroll %1 is prohibited by map settings Cuộn phép %1 bị cấm bởi bản đồ - + Spell scroll %1 doesn't have instance assigned and must be removed Cuộn phép %1 không có đối tượng được gán và phải được xóa - + Artifact %1 is prohibited by map settings Vật phẩm %1 bị cấm bởi bản đồ - + Player %1 doesn't have any starting town Người chơi %1 không có thành khởi đầu nào - + Map name is not specified Tên bản đồ không có - + Map description is not specified Mô tả bản đồ không có - + Map contains object from mod "%1", but doesn't require it Bản đồ chứa đối tượng từ bản mở rộng "%1", nhưng bản mở rộng đó không được yêu cầu - + Exception occurs during validation: %1 Ngoại lệ xuất hiện trong quá trình phê chuẩn: %1 - + Unknown exception occurs during validation Ngoại lệ chưa biết xuất hiện trong quá trình phê chuẩn: %1 + + VictoryConditions + + + Form + + + + + Victory message + Thông báo chiến thắng + + + + Only for human players + Chỉ cho người + + + + Allow standard victory + Cho phép chiến thắng thông thường + + + + Parameters + Tham số + + + + No special victory + Không có chiến thắng đặc biệt + + + + Capture artifact + Đoạt vật phẩm + + + + Hire creatures + Thuê quái + + + + Accumulate resources + Cộng dồn tài nguyên + + + + Construct building + Xây công trình + + + + Capture town + Đoạt thành + + + + Defeat hero + Đánh bại tướng + + + + Transport artifact + Vận chuyển vật phẩm + + + + Kill monster + + + WindowNewMap @@ -972,17 +1785,17 @@ Hủy - + No template Không dùng mẫu - + No template for parameters scecified. Random map cannot be generated. Không có mẫu cho tham số chỉ định. Bản đồ ngẫu nhiên không thể tạo - + RMG failure Tạo bản đồ ngẫu nhiên thất bại @@ -990,27 +1803,27 @@ main - + Filepath of the map to open. Đường dẫn bản đồ - + Extract original H3 archives into a separate folder. Giải nén dữ liệu H3 gốc vào 1 thư mục riêng. - + From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG's. Từ dữ liệu giải nén, chia TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 và Un44 thành những hình PNG riêng lẻ. - + From an extracted archive, Converts single Images (found in Images folder) from .pcx to png. Từ dữ liệu giải nén, chuyển đổi các hình đơn (được tìm thấy trong thư mục Images) từ .pcx sang .png. - + Delete original files, for the ones split / converted. Xóa các tập tin gốc đã được phân chia / chuyển đổi. From 8f6d36b0154ab0a0191c3be31ae907fd1217f137 Mon Sep 17 00:00:00 2001 From: Andrii Danylchenko Date: Sun, 3 Dec 2023 21:03:09 +0200 Subject: [PATCH 1223/1248] MSVC: switch to RelWithDebInfo --- .github/workflows/github.yml | 7 ++++++- CMakePresets.json | 3 +-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/github.yml b/.github/workflows/github.yml index 07dd4171b..a0f6b69a9 100644 --- a/.github/workflows/github.yml +++ b/.github/workflows/github.yml @@ -84,6 +84,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-conan-ninja-release conan_profile: macos-intel @@ -93,6 +94,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: dmg preset: macos-arm-conan-ninja-release conan_profile: macos-arm @@ -102,6 +104,7 @@ jobs: os: macos-12 test: 0 pack: 1 + pack_type: Release extension: ipa preset: ios-release-conan-ccache conan_profile: ios-arm64 @@ -110,12 +113,14 @@ jobs: os: windows-latest test: 0 pack: 1 + pack_type: RelWithDebInfo extension: exe preset: windows-msvc-release-ccache - platform: mingw-ubuntu os: ubuntu-22.04 test: 0 pack: 1 + pack_type: Release extension: exe cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis` cmake_args: -G Ninja @@ -246,7 +251,7 @@ jobs: run: | cd '${{github.workspace}}/out/build/${{matrix.preset}}' CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey` - "$CPACK_PATH" -C ${{env.BUILD_TYPE}} ${{ matrix.cpack_args }} + "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \ && '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)" rm -rf _CPack_Packages diff --git a/CMakePresets.json b/CMakePresets.json index f43ac51ff..d96a905b0 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -327,8 +327,7 @@ { "name": "windows-msvc-release", "configurePreset": "windows-msvc-release", - "inherits": "default-release", - "configuration": "Release" + "inherits": "default-release" }, { "name": "windows-msvc-release-ccache", From 03fa75c51ebb5995c1ea47252daeab6f79ae9830 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 6 Dec 2023 09:49:41 +0100 Subject: [PATCH 1224/1248] Some refactoring, commiting current state after tests --- lib/rmg/RmgObject.cpp | 61 ++++++++++++++++--------- lib/rmg/RmgObject.h | 4 ++ lib/rmg/modificators/TreasurePlacer.cpp | 6 ++- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index a54d54fa3..50b88899d 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -40,7 +40,9 @@ const Area & Object::Instance::getBlockedArea() const { dBlockedAreaCache.assign(dObject.getBlockedPos()); if(dObject.isVisitable() || dBlockedAreaCache.empty()) - dBlockedAreaCache.add(dObject.visitablePos()); + if (!dObject.isBlockedVisitable()) + // Do no assume blocked tile is accessible + dBlockedAreaCache.add(dObject.visitablePos()); } return dBlockedAreaCache; } @@ -85,9 +87,7 @@ void Object::Instance::setPosition(const int3 & position) dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } void Object::Instance::setPositionRaw(const int3 & position) @@ -97,9 +97,7 @@ void Object::Instance::setPositionRaw(const int3 & position) dObject.pos = dPosition + dParent.getPosition(); dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } auto shift = position + dParent.getPosition() - dObject.pos; @@ -141,9 +139,7 @@ void Object::Instance::clear() delete &dObject; dBlockedAreaCache.clear(); dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaCache.clear(); - dParent.dAccessibleAreaFullCache.clear(); - dParent.dFullAreaCache.clear(); + dParent.clearCachedArea(); } bool Object::Instance::isVisitableFrom(const int3 & position) const @@ -152,6 +148,11 @@ bool Object::Instance::isVisitableFrom(const int3 & position) const return dObject.appearance->isVisitableFrom(relPosition.x, relPosition.y); } +bool Object::Instance::isBlockedVisitable() const +{ + return dObject.isBlockedVisitable(); +} + CGObjectInstance & Object::Instance::object() { return dObject; @@ -205,9 +206,7 @@ void Object::addInstance(Instance & object) setGuardedIfMonster(object); dInstances.push_back(object); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); } Object::Instance & Object::addInstance(CGObjectInstance & object) @@ -215,9 +214,7 @@ Object::Instance & Object::addInstance(CGObjectInstance & object) dInstances.emplace_back(*this, object); setGuardedIfMonster(dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); return dInstances.back(); } @@ -226,9 +223,7 @@ Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & p dInstances.emplace_back(*this, object, position); setGuardedIfMonster(dInstances.back()); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + clearCachedArea(); return dInstances.back(); } @@ -270,10 +265,25 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const return dAccessibleAreaFullCache; } +const rmg::Area & Object::getBlockVisitableArea() const +{ + if(dInstances.empty()) + return dBlockVisitableCache; + for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i) + { + // FIXME: Account for blockvis objects with multiple visitable tiles + if (i->isBlockedVisitable()) + dAccessibleAreaCache.add(i->getVisitablePosition()); + } + + return dBlockVisitableCache; +} + void Object::setPosition(const int3 & position) { dAccessibleAreaCache.translate(position - dPosition); dAccessibleAreaFullCache.translate(position - dPosition); + dBlockVisitableCache.translate(position - dPosition); dFullAreaCache.translate(position - dPosition); dPosition = position; @@ -374,14 +384,21 @@ void Object::finalize(RmgMap & map, CRandomGenerator & rng) } } +void Object::clearCachedArea() const +{ + dFullAreaCache.clear(); + dAccessibleAreaCache.clear(); + dAccessibleAreaFullCache.clear(); + dBlockVisitableCache.clear(); +} + void Object::clear() { for(auto & instance : dInstances) instance.clear(); dInstances.clear(); - dFullAreaCache.clear(); - dAccessibleAreaCache.clear(); - dAccessibleAreaFullCache.clear(); + + clearCachedArea(); } diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index 2ba78a29a..5176f1d7f 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -35,6 +35,7 @@ public: int3 getVisitablePosition() const; bool isVisitableFrom(const int3 & tile) const; + bool isBlockedVisitable() const; const Area & getAccessibleArea() const; void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation void setAnyTemplate(CRandomGenerator &); //cache invalidation @@ -71,6 +72,7 @@ public: int3 getVisitablePosition() const; const Area & getAccessibleArea(bool exceptLast = false) const; + const Area & getBlockVisitableArea() const; const int3 & getPosition() const; void setPosition(const int3 & position); @@ -83,12 +85,14 @@ public: void setGuardedIfMonster(const Instance & object); void finalize(RmgMap & map, CRandomGenerator &); + void clearCachedArea() const; void clear(); private: std::list dInstances; mutable Area dFullAreaCache; mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache; + mutable Area dBlockVisitableCache; int3 dPosition; ui32 dStrength; bool guarded; diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 4d482dfa1..7027b4f69 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -676,7 +676,8 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector auto instanceAccessibleArea = instance.getAccessibleArea(); if(instance.getBlockedArea().getTilesVector().size() == 1) { - if(instance.object().appearance->isVisitableFromTop() && instance.object().ID != Obj::CORPSE) + //TOOD: Move hardcoded option to template + if(instance.object().appearance->isVisitableFromTop() && !instance.object().isBlockedVisitable()) instanceAccessibleArea.add(instance.getVisitablePosition()); } @@ -684,6 +685,9 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector if(rmgObject.instances().size() == 1) break; + // TODO: Do not place blockvis objects so that they block access to existing accessible area + // Either object must be removable, or both must be accessible independently + //condition for good position if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea)) break; From 6cd19b81dd33b255a4b9bf29d9dd2c432e53b8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 6 Dec 2023 20:49:28 +0100 Subject: [PATCH 1225/1248] Working fir for Corpse issue: - Do not place guard next to blockVis object, if possible - Do not place two blockVis objects next to each other --- config/objects/generic.json | 22 +++++++++- config/objects/moddables.json | 7 +++ config/objects/rewardablePickable.json | 5 +++ .../AObjectTypeHandler.cpp | 14 ++++++ .../AObjectTypeHandler.h | 3 ++ lib/mapObjects/CGObjectInstance.cpp | 18 +++++--- lib/mapObjects/CGObjectInstance.h | 5 +++ lib/rmg/RmgObject.cpp | 43 +++++++++++++++++-- lib/rmg/RmgObject.h | 4 ++ lib/rmg/modificators/ObjectManager.cpp | 14 +++++- lib/rmg/modificators/TreasurePlacer.cpp | 42 +++++++++++------- 11 files changed, 149 insertions(+), 28 deletions(-) diff --git a/config/objects/generic.json b/config/objects/generic.json index 396ddc374..b3f2e967a 100644 --- a/config/objects/generic.json +++ b/config/objects/generic.json @@ -10,7 +10,11 @@ } }, "types" : { - "prison" : { "index" : 0, "aiValue" : 5000 } + "prison" : { + "index" : 0, + "aiValue" : 5000, + "removable": true + } } }, @@ -136,6 +140,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "templates" : { "normal" : { "animation" : "ava0128.def", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } }, @@ -150,6 +155,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { } } @@ -313,6 +319,7 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } @@ -444,6 +451,7 @@ "object" : { "index" : 0, "aiValue" : 10000, + "removable": true, "rmg" : { } } @@ -495,6 +503,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 2000, "rarity" : 150 @@ -511,6 +520,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 5000, "rarity" : 150 @@ -527,6 +537,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 10000, "rarity" : 150 @@ -543,6 +554,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "rmg" : { "value" : 20000, "rarity" : 150 @@ -572,6 +584,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon1", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -584,6 +597,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon2", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -596,6 +610,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon3", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -608,6 +623,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon4", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -632,6 +648,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon6", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -644,6 +661,7 @@ "types" : { "object" : { "index" : 0, + "removable": true, "templates" : { "normal" : { "animation" : "AVWmon7", "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] } } @@ -687,6 +705,7 @@ "object" : { "index" : 0, "aiValue" : 0, + "removable": true, "rmg" : { } } @@ -723,6 +742,7 @@ "index" :95, "handler": "generic", "base" : { + "blockVisit": true, "sounds" : { "ambient" : ["LOOPTAV"], "visit" : ["STORE"] diff --git a/config/objects/moddables.json b/config/objects/moddables.json index 2a2b31371..b97606eb9 100644 --- a/config/objects/moddables.json +++ b/config/objects/moddables.json @@ -8,6 +8,7 @@ "index" :5, "handler": "artifact", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -25,6 +26,7 @@ "handler": "hero", "base" : { "aiValue" : 7500, + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VVV", "VAV"] @@ -40,6 +42,7 @@ "index" :54, "handler": "monster", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VV", "VA"] @@ -55,6 +58,7 @@ "index" :76, "handler": "randomResource", "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -85,6 +89,7 @@ "handler": "resource", "lastReservedIndex" : 6, "base" : { + "removable": true, "base" : { "visitableFrom" : [ "+++", "+-+", "+++" ], "mask" : [ "VA" ] @@ -138,6 +143,7 @@ "lastReservedIndex" : 2, "base" : { "aiValue" : 0, + "removable": true, "layer" : "sail", "onboardAssaultAllowed" : true, "onboardVisitAllowed" : true, @@ -175,6 +181,7 @@ "lastReservedIndex" : 7, "base" : { "aiValue" : 0, + "removable": true, "sounds" : { "visit" : ["CAVEHEAD"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index 6b01cf3d6..5f6f327de 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -19,6 +19,7 @@ "value" : 2000, "rarity" : 500 }, + "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, @@ -61,6 +62,7 @@ "value" : 2000, "rarity" : 100 }, + "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, @@ -119,6 +121,7 @@ "value" : 1500, "rarity" : 500 }, + "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, @@ -170,6 +173,7 @@ "value" : 1500, "rarity" : 50 }, + "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, @@ -221,6 +225,7 @@ "value" : 1500, "rarity" : 1000 }, + "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 9766f4123..99af1e610 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -96,6 +96,18 @@ void AObjectTypeHandler::init(const JsonNode & input) else aiValue = static_cast>(input["aiValue"].Integer()); + // TODO: Define properties, move them to actual object instance + if(input["blockVisit"].isNull()) + blockVisit = false; + else + blockVisit = input["blockVisit"].Bool(); + + if(input["removable"].isNull()) + removable = false; + else + removable = input["removable"].Bool(); + + battlefield = BattleField::NONE; if(!input["battleground"].isNull()) @@ -120,6 +132,8 @@ void AObjectTypeHandler::preInitObject(CGObjectInstance * obj) const obj->subID = subtype; obj->typeName = typeName; obj->subTypeName = subTypeName; + obj->blockVisit = blockVisit; + obj->removable = removable; } void AObjectTypeHandler::initTypeData(const JsonNode & input) diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.h b/lib/mapObjectConstructors/AObjectTypeHandler.h index 2f5028e57..04e7c2ca5 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.h +++ b/lib/mapObjectConstructors/AObjectTypeHandler.h @@ -43,6 +43,9 @@ class DLL_LINKAGE AObjectTypeHandler : public boost::noncopyable si32 type; si32 subtype; + bool blockVisit; + bool removable; + protected: void preInitObject(CGObjectInstance * obj) const; virtual bool objectFilter(const CGObjectInstance * obj, std::shared_ptr tmpl) const; diff --git a/lib/mapObjects/CGObjectInstance.cpp b/lib/mapObjects/CGObjectInstance.cpp index 167e24c75..b2f7138dd 100644 --- a/lib/mapObjects/CGObjectInstance.cpp +++ b/lib/mapObjects/CGObjectInstance.cpp @@ -33,7 +33,8 @@ CGObjectInstance::CGObjectInstance(): ID(Obj::NO_OBJ), subID(-1), tempOwner(PlayerColor::UNFLAGGABLE), - blockVisit(false) + blockVisit(false), + removable(false) { } @@ -173,12 +174,7 @@ void CGObjectInstance::pickRandomObject(CRandomGenerator & rand) void CGObjectInstance::initObj(CRandomGenerator & rand) { - switch(ID.toEnum()) - { - case Obj::TAVERN: - blockVisit = true; - break; - } + // no-op } void CGObjectInstance::setProperty( ObjProperty what, ObjPropertyID identifier ) @@ -191,6 +187,7 @@ void CGObjectInstance::setProperty( ObjProperty what, ObjPropertyID identifier ) tempOwner = identifier.as(); break; case ObjProperty::BLOCKVIS: + // Never actually used in code, but possible in ERM blockVisit = identifier.getNum(); break; case ObjProperty::ID: @@ -327,9 +324,16 @@ bool CGObjectInstance::isVisitable() const bool CGObjectInstance::isBlockedVisitable() const { + // TODO: Read from json return blockVisit; } +bool CGObjectInstance::isRemovable() const +{ + // TODO: Read from json + return removable; +} + bool CGObjectInstance::isCoastVisitable() const { return false; diff --git a/lib/mapObjects/CGObjectInstance.h b/lib/mapObjects/CGObjectInstance.h index f85a9a550..7292435ba 100644 --- a/lib/mapObjects/CGObjectInstance.h +++ b/lib/mapObjects/CGObjectInstance.h @@ -54,6 +54,7 @@ public: int3 getSightCenter() const; /// If true hero can visit this object only from neighbouring tiles and can't stand on this object bool blockVisit; + bool removable; PlayerColor getOwner() const override { @@ -85,6 +86,9 @@ public: /// If true hero can visit this object only from neighbouring tiles and can't stand on this object virtual bool isBlockedVisitable() const; + // If true, can be possibly removed from the map + virtual bool isRemovable() const; + /// If true this object can be visited by hero standing on the coast virtual bool isCoastVisitable() const; @@ -144,6 +148,7 @@ public: h & id; h & tempOwner; h & blockVisit; + h & removable; h & appearance; //definfo is handled by map serializer } diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index 50b88899d..f826dff51 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -153,6 +153,11 @@ bool Object::Instance::isBlockedVisitable() const return dObject.isBlockedVisitable(); } +bool Object::Instance::isRemovable() const +{ + return dObject.isRemovable(); +} + CGObjectInstance & Object::Instance::object() { return dObject; @@ -269,21 +274,52 @@ const rmg::Area & Object::getBlockVisitableArea() const { if(dInstances.empty()) return dBlockVisitableCache; - for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i) + + for(const auto i : dInstances) { // FIXME: Account for blockvis objects with multiple visitable tiles - if (i->isBlockedVisitable()) - dAccessibleAreaCache.add(i->getVisitablePosition()); + if (i.isBlockedVisitable()) + dBlockVisitableCache.add(i.getVisitablePosition()); } return dBlockVisitableCache; } +const rmg::Area & Object::getRemovableArea() const +{ + if(dInstances.empty()) + return dRemovableAreaCache; + + for(const auto i : dInstances) + { + if (i.isRemovable()) + dRemovableAreaCache.unite(i.getBlockedArea()); + } + + return dRemovableAreaCache; +} + +const rmg::Area Object::getEntrableArea() const +{ + // Calculate Area that hero can freely pass + + // Do not use blockVisitTiles, unless they belong to removable objects (resources etc.) + // area = accessibleArea - (blockVisitableArea - removableArea) + + rmg::Area entrableArea = getAccessibleArea(); + rmg::Area blockVisitableArea = getBlockVisitableArea(); + blockVisitableArea.subtract(getRemovableArea()); + entrableArea.subtract(blockVisitableArea); + + return entrableArea; +} + void Object::setPosition(const int3 & position) { dAccessibleAreaCache.translate(position - dPosition); dAccessibleAreaFullCache.translate(position - dPosition); dBlockVisitableCache.translate(position - dPosition); + dRemovableAreaCache.translate(position - dPosition); dFullAreaCache.translate(position - dPosition); dPosition = position; @@ -390,6 +426,7 @@ void Object::clearCachedArea() const dAccessibleAreaCache.clear(); dAccessibleAreaFullCache.clear(); dBlockVisitableCache.clear(); + dRemovableAreaCache.clear(); } void Object::clear() diff --git a/lib/rmg/RmgObject.h b/lib/rmg/RmgObject.h index 5176f1d7f..f5c523dcc 100644 --- a/lib/rmg/RmgObject.h +++ b/lib/rmg/RmgObject.h @@ -36,6 +36,7 @@ public: int3 getVisitablePosition() const; bool isVisitableFrom(const int3 & tile) const; bool isBlockedVisitable() const; + bool isRemovable() const; const Area & getAccessibleArea() const; void setTemplate(TerrainId terrain, CRandomGenerator &); //cache invalidation void setAnyTemplate(CRandomGenerator &); //cache invalidation @@ -73,6 +74,8 @@ public: int3 getVisitablePosition() const; const Area & getAccessibleArea(bool exceptLast = false) const; const Area & getBlockVisitableArea() const; + const Area & getRemovableArea() const; + const Area getEntrableArea() const; const int3 & getPosition() const; void setPosition(const int3 & position); @@ -93,6 +96,7 @@ private: mutable Area dFullAreaCache; mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache; mutable Area dBlockVisitableCache; + mutable Area dRemovableAreaCache; int3 dPosition; ui32 dStrength; bool guarded; diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 683f9f6e8..93ae31612 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -664,7 +664,19 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard if(!guard) return false; - rmg::Area visitablePos({object.getVisitablePosition()}); + // Prefer non-blocking tiles, if any + auto entrableTiles = object.getEntrableArea().getTiles(); + int3 entrableTile; + if (entrableTiles.empty()) + { + entrableTile = object.getVisitablePosition(); + } + else + { + *RandomGeneratorUtil::nextItem(entrableTiles, zone.getRand()); + } + + rmg::Area visitablePos({entrableTile}); visitablePos.unite(visitablePos.getBorderOutside()); auto accessibleArea = object.getAccessibleArea(); diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 7027b4f69..84afa71e6 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -625,20 +625,31 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector for(const auto & oi : treasureInfos) { auto blockedArea = rmgObject.getArea(); - auto accessibleArea = rmgObject.getAccessibleArea(); + auto entrableArea = rmgObject.getEntrableArea(); + if(rmgObject.instances().empty()) - accessibleArea.add(int3()); + entrableArea.add(int3()); auto * object = oi->generateObject(); if(oi->templates.empty()) continue; object->appearance = *RandomGeneratorUtil::nextItem(oi->templates, zone.getRand()); + + auto blockingIssue = object->isBlockedVisitable() && !object->isRemovable(); + if (blockingIssue) + { + // Do not place next to another such object (Corpse issue) + // Calculate this before instance is added to rmgObject + auto blockVisitProximity = rmgObject.getBlockVisitableArea().getBorderOutside(); + entrableArea.subtract(blockVisitProximity); + } + auto & instance = rmgObject.addInstance(*object); do { - if(accessibleArea.empty()) + if(entrableArea.empty()) { //fail - fallback rmgObject.clear(); @@ -649,12 +660,14 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector if(densePlacement) { int bestPositionsWeight = std::numeric_limits::max(); - for(const auto & t : accessibleArea.getTilesVector()) + for(const auto & t : entrableArea.getTilesVector()) { instance.setPosition(t); - int w = rmgObject.getAccessibleArea().getTilesVector().size(); - if(w < bestPositionsWeight) + int w = rmgObject.getEntrableArea().getTilesVector().size(); + + if(w && w < bestPositionsWeight) { + // Minimum 1 position must be entrable bestPositions.clear(); bestPositions.push_back(t); bestPositionsWeight = w; @@ -664,10 +677,12 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector bestPositions.push_back(t); } } + } - else + + if (bestPositions.empty()) { - bestPositions = accessibleArea.getTilesVector(); + bestPositions = entrableArea.getTilesVector(); } int3 nextPos = *RandomGeneratorUtil::nextItem(bestPositions, zone.getRand()); @@ -676,7 +691,6 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector auto instanceAccessibleArea = instance.getAccessibleArea(); if(instance.getBlockedArea().getTilesVector().size() == 1) { - //TOOD: Move hardcoded option to template if(instance.object().appearance->isVisitableFromTop() && !instance.object().isBlockedVisitable()) instanceAccessibleArea.add(instance.getVisitablePosition()); } @@ -684,16 +698,12 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector //first object is good if(rmgObject.instances().size() == 1) break; - - // TODO: Do not place blockvis objects so that they block access to existing accessible area - // Either object must be removable, or both must be accessible independently - //condition for good position - if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea)) + if(!blockedArea.overlap(instance.getBlockedArea()) && entrableArea.overlap(instanceAccessibleArea)) break; - + //fail - new position - accessibleArea.erase(nextPos); + entrableArea.erase(nextPos); } while(true); } return rmgObject; From 69ff1734b0ce0dba4d1340783e38313290752478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 6 Dec 2023 21:05:27 +0100 Subject: [PATCH 1226/1248] Use reference --- lib/rmg/RmgObject.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rmg/RmgObject.cpp b/lib/rmg/RmgObject.cpp index f826dff51..19426e241 100644 --- a/lib/rmg/RmgObject.cpp +++ b/lib/rmg/RmgObject.cpp @@ -275,7 +275,7 @@ const rmg::Area & Object::getBlockVisitableArea() const if(dInstances.empty()) return dBlockVisitableCache; - for(const auto i : dInstances) + for(const auto & i : dInstances) { // FIXME: Account for blockvis objects with multiple visitable tiles if (i.isBlockedVisitable()) @@ -290,7 +290,7 @@ const rmg::Area & Object::getRemovableArea() const if(dInstances.empty()) return dRemovableAreaCache; - for(const auto i : dInstances) + for(const auto & i : dInstances) { if (i.isRemovable()) dRemovableAreaCache.unite(i.getBlockedArea()); From 5ad682048fb3d719567e8343837f69a46e4fa88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Wed, 6 Dec 2023 21:47:20 +0100 Subject: [PATCH 1227/1248] Fix --- lib/rmg/modificators/ObjectManager.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 93ae31612..1288f34f4 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -666,14 +666,14 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard // Prefer non-blocking tiles, if any auto entrableTiles = object.getEntrableArea().getTiles(); - int3 entrableTile; + int3 entrableTile(-1, -1, -1); if (entrableTiles.empty()) { entrableTile = object.getVisitablePosition(); } else { - *RandomGeneratorUtil::nextItem(entrableTiles, zone.getRand()); + entrableTile = *RandomGeneratorUtil::nextItem(entrableTiles, zone.getRand()); } rmg::Area visitablePos({entrableTile}); From 3b832126a9b4d330c408b072008b76d3d4ffdd35 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 7 Dec 2023 00:47:34 +0100 Subject: [PATCH 1228/1248] Translate game client strings for VCMI 1.4 to polish --- Mods/vcmi/config/vcmi/polish.json | 75 +++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index 1526e83e3..c18f9239c 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -30,14 +30,33 @@ "vcmi.capitalColors.6" : "Jasnoniebieski", "vcmi.capitalColors.7" : "Różowy", + "vcmi.heroOverview.startingArmy" : "Jednostki startowe", + "vcmi.heroOverview.warMachine" : "Machiny wojenne", + "vcmi.heroOverview.secondarySkills" : "Umiejętności drugorzędne", + "vcmi.heroOverview.spells" : "Zaklęcia", + "vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia", + "vcmi.radialWheel.fillSingleUnit" : "Wypełnij pojedynczymi stworzeniami", "vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie", "vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo", "vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii", "vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca", + "vcmi.radialWheel.heroGetArmy" : "Weź armię z innego bohatera", + "vcmi.radialWheel.heroSwapArmy" : "Zamień armię z innym bohaterem", + "vcmi.radialWheel.heroExchange" : "Rozpocznij wymianę między bohaterami", + "vcmi.radialWheel.heroGetArtifacts" : "Weź artefakty z innego bohatera", + "vcmi.radialWheel.heroSwapArtifacts" : "Zamień artefakty z innym bohaterem", + "vcmi.radialWheel.heroDismiss" : "Dymisja bohatera", + + "vcmi.radialWheel.moveTop" : "Przenieś na początek", + "vcmi.radialWheel.moveUp" : "Przenieś w górę", + "vcmi.radialWheel.moveDown" : "Przenieś w dół", + "vcmi.radialWheel.moveBottom" : "Przenieś na spód", + "vcmi.mainMenu.serverConnecting" : "Łączenie...", "vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:", + "vcmi.mainMenu.serverConnectionFailed" : "Połączenie nie powiodło się", "vcmi.mainMenu.serverClosing" : "Zamykanie...", "vcmi.mainMenu.hostTCP" : "Hostuj grę TCP/IP", "vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP", @@ -45,10 +64,18 @@ "vcmi.lobby.filepath" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", + "vcmi.lobby.scenarioName" : "Nazwa scenariusza", + "vcmi.lobby.mapPreview" : "Podgląd mapy", + "vcmi.lobby.noPreview" : "brak podglądu", + "vcmi.lobby.noUnderground" : "brak podziemi", "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", + "vcmi.server.errors.modsToDisable" : "{Następujące mody muszą zostać wyłączone}", "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", + "vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n", + "vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n", + "vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!", "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", @@ -83,6 +110,10 @@ "vcmi.systemOptions.framerateButton.help" : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.", "vcmi.systemOptions.hapticFeedbackButton.hover" : "Wibracje urządzenia", "vcmi.systemOptions.hapticFeedbackButton.help" : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym", + "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Ulepszenia interfejsu", + "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Duża księga zaklęć", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.", "vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym", "vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.", @@ -129,6 +160,9 @@ "vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe", "vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.", + "vcmi.adventureMap.revisitObject.hover" : "Odwiedź obiekt ponownie", + "vcmi.adventureMap.revisitObject.help" : "{Odwiedź obiekt ponownie}\n\nJeżeli bohater aktualnie stoi na polu odwiedzającym obiekt za pomocą tego przycisku może go odwiedzić ponownie.", + "vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo", "vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).", "vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).", @@ -143,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Zatwierdź wynik bitwy", + "vcmi.tutorialWindow.title" : "Samouczek ekranu dotykowego", + "vcmi.tutorialWindow.decription.RightClick" : "Dotknij i przytrzymaj palec na elemencie na którym chcesz wykonać akcję prawego przycisku myszy. Dotknij wolny obszar by zamknąć.", + "vcmi.tutorialWindow.decription.MapPanning" : "Dotknij i przeciągnij jednym palcem by przesunąć mapę.", + "vcmi.tutorialWindow.decription.MapZooming" : "Za pomocą gestu szczypania / rozwierania dwóch palców możesz zmieniać powiększenie mapy.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Przeciąganie palcem otwiera kołowe menu dla różnych akcji takich jak zarządzanie stworzeniami/bohaterami i sortowanie miast.", + "vcmi.tutorialWindow.decription.BattleDirection" : "By zaatakować z określonego kierunku przeciągnij palcem w stronę kierunku z którego chcesz zaatakować.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Atak z wyborem kierunku może zostać anulowany jeśli palec znajdzie się wystarczająco daleko.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Naciśnij i przytrzymaj by anulować rzucenie zaklęcia.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Pokaż dostępne stworzenia", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Pokaż dostępne stworzenia}\n\n Pokazuje dostępne stworzenia zamiast tygodniowego przyrostu w podsumowaniu miasta (lewy dolny róg).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Pokaż tygodniowy przyrost stworzeń", @@ -191,6 +234,38 @@ "vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Sojusze", "vcmi.randomMapTab.widgets.roadTypesLabel" : "Typy dróg", + "vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur", + "vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu (timery) oraz tury równoczesne", + + "vcmi.optionsTab.chessFieldBase.hover" : "Timer startowy", + "vcmi.optionsTab.chessFieldTurn.hover" : "Timer tury", + "vcmi.optionsTab.chessFieldBattle.hover" : "Timer bitwy", + "vcmi.optionsTab.chessFieldCreature.hover" : "Timer jednostki", + "vcmi.optionsTab.chessFieldBase.help" : "Używany gdy {Timer tury} osiągnie 0. Ustawiany raz przy starcie gry. Gdy osiągnie 0 tura się kończy, a trwająca bitwa zostanie przegrana.", + "vcmi.optionsTab.chessFieldTurn.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Nadwyżka czasu dodaje się do {Timera startowego} pod koniec tury.", + "vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Timer jednostki} się wyczerpie w bitwie pomiędzy graczami. Odnawia się przy starcie każdej bitwy.", + "vcmi.optionsTab.chessFieldCreature.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy graczami. Resetuje się przy rozpoczęciu akcji jednostką.", + + "vcmi.optionsTab.simturns" : "Tury równoczesne / symultaniczne", + "vcmi.optionsTab.simturnsMin.hover" : "Co najmniej przez", + "vcmi.optionsTab.simturnsMax.hover" : "Maks. przez", + "vcmi.optionsTab.simturnsAI.hover" : "(Eksperymentalne) Równoczesne tury graczy AI", + "vcmi.optionsTab.simturnsMin.help" : "Graj równocześnie przez określoną liczbę dni. Kontakt pomiędzy graczami do tego czasu jest zablokowany.", + "vcmi.optionsTab.simturnsMax.help" : "Graj równocześnie przez określoną liczbę dni lub do momentu napotkania innego gracza.", + "vcmi.optionsTab.simturnsAI.help" : "{Równoczesne tury graczy AI}\nOpcja eksperymentalna. Pozwala graczom sterowanym przez komputer wykonywać akcje w tym samym czasie co gracz ludzki gdy jednoczesne tury są włączone.", + + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language + // Using this information, VCMI will automatically select correct plural form for every possible amount + "vcmi.optionsTab.simturns.days.0" : " %d dni", + "vcmi.optionsTab.simturns.days.1" : " %d dzień", + "vcmi.optionsTab.simturns.days.2" : " %d dni", + "vcmi.optionsTab.simturns.weeks.0" : " %d tygodni", + "vcmi.optionsTab.simturns.weeks.1" : " %d tydzień", + "vcmi.optionsTab.simturns.weeks.2" : " %d tygodnie", + "vcmi.optionsTab.simturns.months.0" : " %d miesięcy", + "vcmi.optionsTab.simturns.months.1" : " %d miesiąc", + "vcmi.optionsTab.simturns.months.2" : " %d miesiące", + // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "Wróg dał radę przetrwać do dzisiejszego dnia. Zwycięstwo należy do niego!", "vcmi.map.victoryCondition.daysPassed.toSelf" : "Gratulacje! Dałeś radę przetrwać. Zwycięstwo jest twoje!", From 0fd966818f509eddf2a8242c3b2d8e7c1378028e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 23 Nov 2023 17:19:09 +0200 Subject: [PATCH 1229/1248] Remove 'visible' property from images, replaced with enable/disabled call --- client/gui/InterfaceObjectConfigurable.cpp | 2 -- client/widgets/ComboBox.cpp | 11 ++++++----- client/widgets/Images.cpp | 6 ++---- client/widgets/Images.h | 4 ---- client/windows/CSpellWindow.cpp | 12 ++++++++++-- 5 files changed, 18 insertions(+), 17 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index fecc485da..9d172d2a3 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -310,8 +310,6 @@ std::shared_ptr InterfaceObjectConfigurable::buildPicture(const JsonNo auto image = ImagePath::fromJson(config["image"]); auto position = readPosition(config["position"]); auto pic = std::make_shared(image, position.x, position.y); - if(!config["visible"].isNull()) - pic->visible = config["visible"].Bool(); if ( config["playerColored"].Bool() && LOCPLINT) pic->colorize(LOCPLINT->playerID); diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 7b6ee4920..eca9fd458 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -22,10 +22,11 @@ ComboBox::DropDown::Item::Item(const JsonNode & config, ComboBox::DropDown & _dr { build(config); - if(auto w = widget("hoverImage")) + if(auto w = widget("hoverImage")) { pos.w = w->pos.w; pos.h = w->pos.h; + w->disable(); } setRedrawParent(true); } @@ -42,14 +43,14 @@ void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) void ComboBox::DropDown::Item::hover(bool on) { - auto h = widget("hoverImage"); + auto h = widget("hoverImage"); auto w = widget("labelName"); if(h && w) { - if(w->getText().empty()) - h->visible = false; + if(w->getText().empty() || on == false) + h->disable(); else - h->visible = on; + h->enable(); } redraw(); } diff --git a/client/widgets/Images.cpp b/client/widgets/Images.cpp index 76ccd2838..8ae5795fb 100644 --- a/client/widgets/Images.cpp +++ b/client/widgets/Images.cpp @@ -35,7 +35,6 @@ CPicture::CPicture(std::shared_ptr image, const Point & position) : bg(image) - , visible(true) , needRefresh(false) { pos += position; @@ -53,7 +52,6 @@ CPicture::CPicture( const ImagePath & bmpname ) CPicture::CPicture( const ImagePath & bmpname, const Point & position ) : bg(GH.renderHandler().loadImage(bmpname)) - , visible(true) , needRefresh(false) { pos.x += position.x; @@ -81,13 +79,13 @@ CPicture::CPicture(std::shared_ptr image, const Rect &SrcRect, int x, in void CPicture::show(Canvas & to) { - if (visible && needRefresh) + if (needRefresh) showAll(to); } void CPicture::showAll(Canvas & to) { - if(bg && visible) + if(bg) { if (srcRect.has_value()) to.draw(bg, pos.topLeft(), *srcRect); diff --git a/client/widgets/Images.h b/client/widgets/Images.h index 2a9f3ae10..94fef0c1c 100644 --- a/client/widgets/Images.h +++ b/client/widgets/Images.h @@ -34,10 +34,6 @@ public: /// If set to true, iamge will be redrawn on each frame bool needRefresh; - /// If set to false, image will not be rendered - /// Deprecated, use CIntObject::disable()/enable() instead - bool visible; - std::shared_ptr getSurface() { return bg; diff --git a/client/windows/CSpellWindow.cpp b/client/windows/CSpellWindow.cpp index 43ad953c4..bbd104fd4 100644 --- a/client/windows/CSpellWindow.cpp +++ b/client/windows/CSpellWindow.cpp @@ -449,8 +449,16 @@ void CSpellWindow::setCurrentPage(int value) schoolPicture->visible = selectedTab!=4 && currentPage == 0; if(selectedTab != 4) schoolPicture->setFrame(selectedTab, 0); - leftCorner->visible = currentPage != 0; - rightCorner->visible = (currentPage+1) < pagesWithinCurrentTab(); + + if (currentPage != 0) + leftCorner->enable(); + else + leftCorner->disable(); + + if (currentPage + 1 < pagesWithinCurrentTab()) + rightCorner->enable(); + else + rightCorner->disable(); mana->setText(std::to_string(myHero->mana));//just in case, it will be possible to cast spell without closing book } From 1f0bcbc1940ccea0ac8331cca4c7907aecad7a95 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 23 Nov 2023 17:19:49 +0200 Subject: [PATCH 1230/1248] Added option to configure dropdown position --- client/gui/InterfaceObjectConfigurable.cpp | 4 +++- client/widgets/ComboBox.cpp | 10 +++++----- client/widgets/ComboBox.h | 4 ++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/client/gui/InterfaceObjectConfigurable.cpp b/client/gui/InterfaceObjectConfigurable.cpp index 9d172d2a3..589b45fe4 100644 --- a/client/gui/InterfaceObjectConfigurable.cpp +++ b/client/gui/InterfaceObjectConfigurable.cpp @@ -560,9 +560,11 @@ std::shared_ptr InterfaceObjectConfigurable::buildComboBox(const JsonN { logGlobal->debug("Building widget ComboBox"); auto position = readPosition(config["position"]); + auto dropDownPosition = readPosition(config["dropDownPosition"]); auto image = AnimationPath::fromJson(config["image"]); auto help = readHintText(config["help"]); - auto result = std::make_shared(position, image, help, config["dropDown"]); + auto result = std::make_shared(position, image, help, config["dropDown"], dropDownPosition); + if(!config["items"].isNull()) { for(const auto & item : config["items"].Vector()) diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index eca9fd458..9d98db32e 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -67,7 +67,7 @@ void ComboBox::DropDown::Item::clickReleased(const Point & cursorPosition) dropDown.clickReleased(cursorPosition); } -ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): +ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox, Point dropDownPosition): InterfaceObjectConfigurable(LCLICK | HOVER), comboBox(_comboBox) { @@ -78,7 +78,7 @@ ComboBox::DropDown::DropDown(const JsonNode & config, ComboBox & _comboBox): addCallback("sliderMove", std::bind(&ComboBox::DropDown::sliderMove, this, std::placeholders::_1)); - pos = comboBox.pos; + pos = comboBox.pos + dropDownPosition; build(config); @@ -156,12 +156,12 @@ void ComboBox::DropDown::setItem(const void * item) GH.windows().popWindows(1); } -ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key, bool playerColoredButton): +ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key, bool playerColoredButton): CButton(position, defName, help, 0, key, playerColoredButton) { - addCallback([&, dropDownDescriptor]() + addCallback([this, dropDownDescriptor, dropDownPosition]() { - GH.windows().createAndPushWindow(dropDownDescriptor, *this); + GH.windows().createAndPushWindow(dropDownDescriptor, *this, dropDownPosition); }); } diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index dafd496bf..561ad96f9 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -32,7 +32,7 @@ class ComboBox : public CButton friend struct Item; public: - DropDown(const JsonNode &, ComboBox &); + DropDown(const JsonNode &, ComboBox &, Point dropDownPosition); bool receiveEvent(const Point & position, int eventType) const override; void clickPressed(const Point & cursorPosition) override; @@ -54,7 +54,7 @@ class ComboBox : public CButton void setItem(const void *); public: - ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, EShortcut key = {}, bool playerColoredButton = false); + ComboBox(Point position, const AnimationPath & defName, const std::pair & help, const JsonNode & dropDownDescriptor, Point dropDownPosition, EShortcut key = {}, bool playerColoredButton = false); //define this callback to fill input vector with data for the combo box std::function &)> onConstructItems; From cb8e9e633a9c668ba54938f8c72f9840739f3c6e Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 23 Nov 2023 17:20:32 +0200 Subject: [PATCH 1231/1248] Implemented layout for simturn/timer presets --- Mods/vcmi/Sprites/lobby/checkboxBlueOff.png | Bin 485 -> 3122 bytes Mods/vcmi/Sprites/lobby/checkboxBlueOn.png | Bin 400 -> 2685 bytes Mods/vcmi/Sprites/lobby/dropdown.json | 8 + Mods/vcmi/Sprites/lobby/dropdownNormal.png | Bin 0 -> 1708 bytes Mods/vcmi/Sprites/lobby/dropdownPressed.png | Bin 0 -> 1542 bytes config/widgets/advancedOptionsTab.json | 129 +++++ config/widgets/playerOptionsTab.json | 43 +- .../widgets/turnOptionsDropdownLibrary.json | 520 ++++++++++++++++++ config/widgets/turnOptionsTab.json | 50 +- 9 files changed, 717 insertions(+), 33 deletions(-) create mode 100644 Mods/vcmi/Sprites/lobby/dropdown.json create mode 100644 Mods/vcmi/Sprites/lobby/dropdownNormal.png create mode 100644 Mods/vcmi/Sprites/lobby/dropdownPressed.png create mode 100644 config/widgets/advancedOptionsTab.json create mode 100644 config/widgets/turnOptionsDropdownLibrary.json diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png index cfb75ae7cd9f1d18a552caafebbc8bb05efe1ba0..b46086b14af74977926c66172d986167717e31a8 100644 GIT binary patch delta 2916 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 O0000aiui? delta 259 zcmV+e0sQ{5807w3PK_HMUES8p7L97j+NNn*2 zZ8nA}io_OE%rw9dFA!U7F<9%{OqGZwc6%JOMUn{0txs_TPUoQpWE!ddbhVmZUE}8V z&Tv_o3as2eJXQ^WRNI4@5VOJI{PZ01V21PcX0x6TFZ$%Z%`1YZ7S_1?erx~$002ov JPDHLkV1i5Ub7%kn diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOn.png index 1f414653f339daeffab0193b176d18883444fed2..ed017084a2b9b1b01ebb8dd83dd00268c279763c 100644 GIT binary patch literal 2685 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 delta 386 zcmV-|0e$}c6p#au7k?NC0ssI26ftOH00043Nklk=22=CL&y7`qZ?bHsAZ}wG5!?*LFt&P3I8S;+MCh+=a(BzinPp4?ICtD)33r0lt43`%t#LKbOt6wprP+CrXUABw}cVZ3cIBfgMj8j(Dglt zpr_mdM7sQVISpv~uiKdw8G5YXpr>+iU6|_e@^oTfwS}6Z0H2H%P3i+aS!-a9<&iQ2 grq_{KWzrM@03S66y`HaFyZ`_I07*qoM6N<$f(Q1n_W%F@ diff --git a/Mods/vcmi/Sprites/lobby/dropdown.json b/Mods/vcmi/Sprites/lobby/dropdown.json new file mode 100644 index 000000000..3a6d41203 --- /dev/null +++ b/Mods/vcmi/Sprites/lobby/dropdown.json @@ -0,0 +1,8 @@ +{ + "basepath" : "lobby/", + "images" : + [ + { "frame" : 0, "file" : "dropdownNormal.png"}, + { "frame" : 1, "file" : "dropdownPressed.png"} + ] +} diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Sprites/lobby/dropdownNormal.png new file mode 100644 index 0000000000000000000000000000000000000000..270e3cef0db9d24c8315b3330898d2435200222b GIT binary patch literal 1708 zcmV;d22=ToP) zaB^>EX>4U6ba`-PAZ2)IW&i+q+O1YwlH)cE{O2ih1Ogxk@;H2Mvw78d<`%{ zg1ekQ^Sb6Y#P#-or7PYa

    BTrQw%-~&*0=ACR_BrhQ0Gfvm~TD)58@FCmG?mCsDFKOzkg!j*;h#N)e4JtFC=e&JKP7Xp6Je%3FJYlht9 zIDtOO%1=67anjfN9V*>3BEF{ZZQyVyAHLjED zps}6XSImMT#;=u6?e#ph!iD{xQ`nF|VRv7ZoK>9)4y@L8e;Oil4zcx3YefJDfx zAcNJNC}t9+OwpK)2s#${)4&{yoI4yK4ayTNIf-PLQ+VgbGe*Z;RzAaMFEBzSi@_Rf z6quEgri}eou!LGr(WI(bO^a4-S#rvnbGE$uR5P(;YT3++Rcl2`Dq3=}Qc5kYauu{O z4K-J*rPkUSlD5=<)iA#y(78)bU3>1uie_ zmeNW~S6;Tts;fEHnu{k_&u(74di!+ka`n-*XXk$H8eOjOVbq>yH`fq0`+%U`PIP+) z$2bujx9tEHG;hyhp_F(#w>^uMC5R}4rEYU3ZO7nXTEyz~>D|q_x4s3a`@6pJE9b(t z?*DKuZ0qiv`|8^Z*JeA~tsDmJT?kFlAqCsddGvkM)JLtFt>XP;d$$fNE4S!9&uKp3 z8_YAbD+cfqZ{~R3M%Xt&-FR@u|7c^-|Qd}Gb*Mfr|i&X~~XI&j!1wrrw#MQ+~(M3x9 zUs`ApYciDr?=7ztEr4R#IH2If59L5Jv(c zWK>Z?85Y8{YNVJ*(tgy%KkWDu`t+kKS2Ov#dE#CkKhrn2Yve#YS-P76Izh_$g{QxNea`16-90C9U z00v@9M??VL0Nw!JDf*qR00009a7bBm001r{001r{0eGc9b^rhX2XskIMF-~#785%d z^oi1s0000>Nklg?mb#i zN0$Iof+;a2m=aThDKRCO5=@CH!IYR1Oo=H0KKB52`~`%pF^cB^0000 zaB^>EX>4U6ba`-PAZ2)IW&i+q+O3vplJq7FhW~Sl905rP!Esota)TT{FPNU5_RN=A zDyG`l0<=8>w;TWbJ>6fpD8%G~T8cSFk1OPmx!@uB>$-aKVc+-F+Zm%*xg$OU43VIh z>otxuzd)|f1s0CHzRH~)3!ILkGw@I-%knbTkCR7{@Fl17?(*@d#CPGG-)x+hNb5Cp ze-DOabcH@)$3sZ`zVl^C)=#8BLb$W=nz)};@|M`g{JFQ(eUiY3+xPtWc4o*k9hag< zSn-ypBPable>+R>Jt96+_&KoiCjQ~m$NbHn<{VdaB}9jG#S_#c#QlVcNNJG0ETS2H zif7v`qa|N_L--w`R_7~TGBGkfDNv_Elg92FHMkgfu%Tye$oP^aG0xlog(R#-lg)uc zmjn_G233t}nlx*P5<}D&qs4Vj6%z}l7R@YKwvr@;q$ws#Ddki$XTdS1F2`&+<(x}_ z(Gm)D70fRXRIXA()f%hSRC6s2<VWQW|0Ch{Hx2 zd6bD-n=-@H8K=!O^DJ-F4%Js`cXIztjSe;5pw>KlQiIs+p@PXS{g41ac1Hmweb{U*!oJt#g9iDHK>!TCtsJ^Y5xl!sBs0|}W^2HcxiWrGY~ zmXxvBsaBiaYO^&|d@5 z$mM1DnB7KSY~Dk4_gH@$s=LSf+fdy-*58KezQX$5P~BHpzZ+oh*ooc9bd>!9r;( zwCZ4T=@&F-f5dhp%^Gp5=b-&(W{sOa^#F z;yI=p7V!r0%%-Jt-X{*SqNEU?6OS8oLE=ZQ%T0dcTyR+6nL#6+oF@(vi-iuBJD3#> zm3W#sq^KI@3z;S>oVPfuYJ-YNQ> zt^fc432;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Ri3lbLE5hdL90UXN%lnm$nMF0Q*07*qoM6N<$f|PdA Date: Thu, 23 Nov 2023 17:59:18 +0200 Subject: [PATCH 1232/1248] Implemented accumulating timers, rename creature timer to unit timer --- client/NetPacksClient.cpp | 2 +- client/adventureMap/TurnTimerWidget.cpp | 4 +- client/lobby/OptionsTab.cpp | 10 ++- client/lobby/OptionsTabBase.cpp | 12 +-- lib/TurnTimerInfo.cpp | 2 +- lib/TurnTimerInfo.h | 9 ++- server/TurnTimerHandler.cpp | 103 +++++++++--------------- 7 files changed, 65 insertions(+), 77 deletions(-) diff --git a/client/NetPacksClient.cpp b/client/NetPacksClient.cpp index df4c66d1b..a6d0bff9a 100644 --- a/client/NetPacksClient.cpp +++ b/client/NetPacksClient.cpp @@ -901,7 +901,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsTurn(PlayerEndsTurn & pack) void ApplyClientNetPackVisitor::visitTurnTimeUpdate(TurnTimeUpdate & pack) { - logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.creatureTimer, pack.player.toString()); + logNetwork->debug("Server sets turn timer {turn: %d, base: %d, battle: %d, creature: %d} for %s", pack.turnTimer.turnTimer, pack.turnTimer.baseTimer, pack.turnTimer.battleTimer, pack.turnTimer.unitTimer, pack.player.toString()); } void ApplyClientNetPackVisitor::visitPlayerMessageClient(PlayerMessageClient & pack) diff --git a/client/adventureMap/TurnTimerWidget.cpp b/client/adventureMap/TurnTimerWidget.cpp index 0d3aaa571..41b77255a 100644 --- a/client/adventureMap/TurnTimerWidget.cpp +++ b/client/adventureMap/TurnTimerWidget.cpp @@ -128,9 +128,9 @@ void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed) if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman())) { if(time.isBattle) - timeCheckAndUpdate(time.creatureTimer); + timeCheckAndUpdate(time.baseTimer + time.turnTimer + time.battleTimer + time.unitTimer); else - timeCheckAndUpdate(time.turnTimer); + timeCheckAndUpdate(time.baseTimer + time.turnTimer); } else timeCheckAndUpdate(0); diff --git a/client/lobby/OptionsTab.cpp b/client/lobby/OptionsTab.cpp index a7f762965..e7698ba48 100644 --- a/client/lobby/OptionsTab.cpp +++ b/client/lobby/OptionsTab.cpp @@ -42,8 +42,16 @@ #include "../../lib/mapping/CMapInfo.h" #include "../../lib/mapping/CMapHeader.h" +static JsonPath optionsTabConfigLocation() +{ + if(settings["general"]["enableUiEnhancements"].Bool()) + return JsonPath::builtin("config/widgets/playerOptionsTab.json"); + else + return JsonPath::builtin("config/widgets/advancedOptionsTab.json"); +} + OptionsTab::OptionsTab() - : OptionsTabBase(JsonPath::builtin("config/widgets/playerOptionsTab.json")) + : OptionsTabBase(optionsTabConfigLocation()) , humanPlayers(0) { } diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 18953eb97..185454ce2 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -34,7 +34,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) tinfo.baseTimer = tpreset.at(0).Integer() * 1000; tinfo.turnTimer = tpreset.at(1).Integer() * 1000; tinfo.battleTimer = tpreset.at(2).Integer() * 1000; - tinfo.creatureTimer = tpreset.at(3).Integer() * 1000; + tinfo.unitTimer = tpreset.at(3).Integer() * 1000; CSH->setTurnTimerInfo(tinfo); } }); @@ -133,7 +133,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) if(time >= 0) { TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.creatureTimer = time; + tinfo.unitTimer = time; CSH->setTurnTimerInfo(tinfo); } }); @@ -175,7 +175,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) tinfo.baseTimer = (*tObj)["default"].Vector().at(0).Integer() * 1000; tinfo.turnTimer = (*tObj)["default"].Vector().at(1).Integer() * 1000; tinfo.battleTimer = (*tObj)["default"].Vector().at(2).Integer() * 1000; - tinfo.creatureTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; + tinfo.unitTimer = (*tObj)["default"].Vector().at(3).Integer() * 1000; CSH->setTurnTimerInfo(tinfo); } } @@ -251,7 +251,7 @@ void OptionsTabBase::recreate() //classic timer if(auto turnSlider = widget("sliderTurnDuration")) { - if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.creatureTimer && !turnTimerRemote.baseTimer) + if(!variables["timerPresets"].isNull() && !turnTimerRemote.battleTimer && !turnTimerRemote.unitTimer && !turnTimerRemote.baseTimer) { for(int idx = 0; idx < variables["timerPresets"].Vector().size(); ++idx) { @@ -281,11 +281,11 @@ void OptionsTabBase::recreate() if(auto ww = widget("chessFieldBattle")) ww->setText(timeToString(turnTimerRemote.battleTimer), false); if(auto ww = widget("chessFieldCreature")) - ww->setText(timeToString(turnTimerRemote.creatureTimer), false); + ww->setText(timeToString(turnTimerRemote.unitTimer), false); if(auto w = widget("timerModeSwitch")) { - if(turnTimerRemote.battleTimer || turnTimerRemote.creatureTimer || turnTimerRemote.baseTimer) + if(turnTimerRemote.battleTimer || turnTimerRemote.unitTimer || turnTimerRemote.baseTimer) { if(auto turnSlider = widget("sliderTurnDuration")) if(turnSlider->isActive()) diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index 0594fad65..c7fbcd936 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -19,7 +19,7 @@ bool TurnTimerInfo::isEnabled() const bool TurnTimerInfo::isBattleEnabled() const { - return creatureTimer > 0 || battleTimer > 0; + return unitTimer > 0 || battleTimer > 0; } VCMI_LIB_NAMESPACE_END diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index c708345b4..93d951538 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -17,7 +17,10 @@ struct DLL_LINKAGE TurnTimerInfo int turnTimer = 0; //in ms, counts down when player is making his turn on adventure map int baseTimer = 0; //in ms, counts down only when turn timer runs out int battleTimer = 0; //in ms, counts down during battles when creature timer runs out - int creatureTimer = 0; //in ms, counts down when player is choosing action in battle + int unitTimer = 0; //in ms, counts down when player is choosing action in battle + + bool accumulatingTurnTimer = false; + bool accumulatingUnitTimer = false; bool isActive = false; //is being counting down bool isBattle = false; //indicator for current timer mode @@ -31,7 +34,9 @@ struct DLL_LINKAGE TurnTimerInfo h & turnTimer; h & baseTimer; h & battleTimer; - h & creatureTimer; + h & unitTimer; + h & accumulatingTurnTimer; + h & accumulatingUnitTimer; h & isActive; h & isBattle; } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index ebd51e826..fcf80c4f8 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -74,7 +74,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { endTurnAllowed[player] = true; auto & timer = timers[player]; - if(si->turnTimerInfo.baseTimer > 0) + if(si->turnTimerInfo.accumulatingTurnTimer > 0) timer.baseTimer += timer.turnTimer; timer.turnTimer = si->turnTimerInfo.turnTimer; @@ -127,17 +127,14 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime) const auto * state = gameHandler.getPlayerState(player); if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME) { - if(!timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) - { - if(timer.baseTimer > 0) - { - timer.turnTimer = timer.baseTimer; - timer.baseTimer = 0; - onPlayerMakingTurn(player, 0); - } - else if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries - gameHandler.turnOrder->onPlayerEndsTurn(state->color); - } + if(timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if(timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + + if(endTurnAllowed[state->color] && !gameHandler.queries->topQuery(state->color)) //wait for replies to avoid pending queries + gameHandler.turnOrder->onPlayerEndsTurn(state->color); } } @@ -176,8 +173,8 @@ void TurnTimerHandler::onBattleStart(const BattleID & battleID) auto & timer = timers[i]; timer.isBattle = true; timer.isActive = si->turnTimerInfo.isBattleEnabled(); - timer.battleTimer = (pvpBattle ? si->turnTimerInfo.battleTimer : 0); - timer.creatureTimer = (pvpBattle ? si->turnTimerInfo.creatureTimer : si->turnTimerInfo.battleTimer); + timer.battleTimer = si->turnTimerInfo.battleTimer; + timer.unitTimer = (pvpBattle ? si->turnTimerInfo.unitTimer : 0); sendTimerUpdate(i); } @@ -201,8 +198,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) auto attacker = gs->getBattle(battleID)->getSidePlayer(BattleSide::ATTACKER); auto defender = gs->getBattle(battleID)->getSidePlayer(BattleSide::DEFENDER); - bool pvpBattle = isPvpBattle(battleID); - for(auto i : {attacker, defender}) { if(i.isValidPlayer()) @@ -210,14 +205,6 @@ void TurnTimerHandler::onBattleEnd(const BattleID & battleID) auto & timer = timers[i]; timer.isBattle = false; timer.isActive = true; - if(!pvpBattle) - { - if(si->turnTimerInfo.baseTimer && timer.baseTimer == 0) - timer.baseTimer = timer.creatureTimer; - else if(si->turnTimerInfo.turnTimer && timer.turnTimer == 0) - timer.turnTimer = timer.creatureTimer; - } - sendTimerUpdate(i); } } @@ -242,9 +229,9 @@ void TurnTimerHandler::onBattleNextStack(const BattleID & battleID, const CStack auto player = stack.getOwner(); auto & timer = timers[player]; - if(timer.battleTimer == 0) - timer.battleTimer = timer.creatureTimer; - timer.creatureTimer = si->turnTimerInfo.creatureTimer; + if(timer.accumulatingUnitTimer) + timer.battleTimer += timer.unitTimer; + timer.unitTimer = si->turnTimerInfo.unitTimer; sendTimerUpdate(player); } @@ -283,56 +270,44 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime) return; const auto * state = gameHandler.getPlayerState(player); - assert(state && state->status != EPlayerStatus::INGAME); + assert(state && state->status == EPlayerStatus::INGAME); if(!state || state->status != EPlayerStatus::INGAME || !state->human) return; auto & timer = timers[player]; - if(timer.isActive && timer.isBattle && !timerCountDown(timer.creatureTimer, si->turnTimerInfo.creatureTimer, player, waitTime)) + if(timer.isActive && timer.isBattle) { + if (timerCountDown(timer.unitTimer, si->turnTimerInfo.unitTimer, player, waitTime)) + return; + + if (timerCountDown(timer.battleTimer, si->turnTimerInfo.battleTimer, player, waitTime)) + return; + + if (timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime)) + return; + + if (timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime)) + return; + if(isPvpBattle(battleID)) { - if(timer.battleTimer > 0) - { - timer.creatureTimer = timer.battleTimer; - timerCountDown(timer.creatureTimer, timer.battleTimer, player, 0); - timer.battleTimer = 0; - } + BattleAction doNothing; + doNothing.side = side; + if(isTactisPhase) + doNothing.actionType = EActionType::END_TACTIC_PHASE; else { - BattleAction doNothing; - doNothing.side = side; - if(isTactisPhase) - doNothing.actionType = EActionType::END_TACTIC_PHASE; - else - { - doNothing.actionType = EActionType::DEFEND; - doNothing.stackNumber = stack->unitId(); - } - gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); + doNothing.actionType = EActionType::DEFEND; + doNothing.stackNumber = stack->unitId(); } + gameHandler.battles->makePlayerBattleAction(battleID, player, doNothing); } else { - if(timer.turnTimer > 0) - { - timer.creatureTimer = timer.turnTimer; - timerCountDown(timer.creatureTimer, timer.turnTimer, player, 0); - timer.turnTimer = 0; - } - else if(timer.baseTimer > 0) - { - timer.creatureTimer = timer.baseTimer; - timerCountDown(timer.creatureTimer, timer.baseTimer, player, 0); - timer.baseTimer = 0; - } - else - { - BattleAction retreat; - retreat.side = side; - retreat.actionType = EActionType::RETREAT; //harsh punishment - gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); - } + BattleAction retreat; + retreat.side = side; + retreat.actionType = EActionType::RETREAT; //harsh punishment + gameHandler.battles->makePlayerBattleAction(battleID, player, retreat); } } } From 861c53059eba345735c6a64763f8abc6df96e48f Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 23 Nov 2023 19:48:17 +0200 Subject: [PATCH 1233/1248] Added callbacks for preset dropdowns --- client/lobby/OptionsTabBase.cpp | 69 ++++++++++++++++++++++++-- client/widgets/ComboBox.cpp | 29 ++++++----- client/widgets/ComboBox.h | 5 +- config/widgets/advancedOptionsTab.json | 22 ++++---- config/widgets/playerOptionsTab.json | 35 +++++++++---- config/widgets/turnOptionsTab.json | 44 +++++++++++++--- server/TurnTimerHandler.cpp | 2 +- 7 files changed, 160 insertions(+), 46 deletions(-) diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 185454ce2..2fc0ad34f 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -26,18 +26,35 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) { recActions = 0; - addCallback("setTimerPreset", [&](int index){ + auto setTimerPresetCallback = [this](int index){ if(!variables["timerPresets"].isNull()) { auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); - TurnTimerInfo tinfo; + TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; tinfo.baseTimer = tpreset.at(0).Integer() * 1000; tinfo.turnTimer = tpreset.at(1).Integer() * 1000; tinfo.battleTimer = tpreset.at(2).Integer() * 1000; tinfo.unitTimer = tpreset.at(3).Integer() * 1000; + tinfo.accumulatingTurnTimer = tpreset.at(4).Bool(); + tinfo.accumulatingUnitTimer = tpreset.at(5).Bool(); CSH->setTurnTimerInfo(tinfo); } - }); + }; + + auto setSimturnsPresetCallback = [this](int index){ + if(!variables["simturnsPresets"].isNull()) + { + auto tpreset = variables["simturnsPresets"].Vector().at(index).Vector(); + SimturnsInfo tinfo = SEL->getStartInfo()->simturnsInfo; + tinfo.optionalTurns = tpreset.at(0).Integer(); + tinfo.requiredTurns = tpreset.at(1).Integer(); + tinfo.allowHumanWithAI = tpreset.at(2).Bool(); + CSH->setSimturnsInfo(tinfo); + } + }; + + addCallback("setTimerPreset", setTimerPresetCallback); + addCallback("setSimturnPreset", setSimturnsPresetCallback); addCallback("setSimturnDurationMin", [&](int index){ SimturnsInfo info = SEL->getStartInfo()->simturnsInfo; @@ -59,6 +76,18 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) CSH->setSimturnsInfo(info); }); + addCallback("setTurnTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingTurnTimer = index; + CSH->setTurnTimerInfo(info); + }); + + addCallback("setUnitTimerAccumulate", [&](int index){ + TurnTimerInfo info = SEL->getStartInfo()->turnTimerInfo; + info.accumulatingUnitTimer = index; + CSH->setTurnTimerInfo(info); + }); + //helper function to parse string containing time to integer reflecting time in seconds //assumed that input string can be modified by user, function shall support user's intention // normal: 2:00, 12:30 @@ -194,6 +223,34 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) w->setItem(0); } + + if(auto w = widget("simturnsPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["simturnsPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setSimturnsPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setSimturnsPresetCallback(itemIndex); + }; + } + + if(auto w = widget("timerPresetSelector")) + { + w->onConstructItems = [this](std::vector & curItems) + { + for (size_t i = 0; i < variables["timerPresets"].Vector().size(); ++i) + curItems.push_back((void*)i); + }; + + w->onSetItem = [setTimerPresetCallback](const void * item){ + size_t itemIndex = (size_t)item; + setTimerPresetCallback(itemIndex); + }; + } } void OptionsTabBase::recreate() @@ -246,6 +303,12 @@ void OptionsTabBase::recreate() if(auto buttonSimturnsAI = widget("buttonSimturnsAI")) buttonSimturnsAI->setSelectedSilent(SEL->getStartInfo()->simturnsInfo.allowHumanWithAI); + if(auto buttonTurnTimerAccumulate = widget("buttonTurnTimerAccumulate")) + buttonTurnTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer); + + if(auto buttonUnitTimerAccumulate = widget("buttonUnitTimerAccumulate")) + buttonUnitTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer); + const auto & turnTimerRemote = SEL->getStartInfo()->turnTimerInfo; //classic timer diff --git a/client/widgets/ComboBox.cpp b/client/widgets/ComboBox.cpp index 9d98db32e..f0150b6d1 100644 --- a/client/widgets/ComboBox.cpp +++ b/client/widgets/ComboBox.cpp @@ -33,9 +33,9 @@ ComboBox::DropDown::Item::Item(const JsonNode & config, ComboBox::DropDown & _dr void ComboBox::DropDown::Item::updateItem(int idx, const void * _item) { + item = _item; if(auto w = widget("labelName")) { - item = _item; if(dropDown.comboBox.getItemText) w->setText(dropDown.comboBox.getItemText(idx, item)); } @@ -130,20 +130,21 @@ void ComboBox::DropDown::clickPressed(const Point & cursorPosition) void ComboBox::DropDown::updateListItems() { + int elemIdx = 0; + if(auto w = widget("slider")) + elemIdx = w->getValue(); + + for(auto item : items) { - int elemIdx = w->getValue(); - for(auto item : items) + if(elemIdx < curItems.size()) { - if(elemIdx < curItems.size()) - { - item->updateItem(elemIdx, curItems[elemIdx]); - elemIdx++; - } - else - { - item->updateItem(elemIdx); - } + item->updateItem(elemIdx, curItems[elemIdx]); + elemIdx++; + } + else + { + item->updateItem(elemIdx); } } } @@ -167,7 +168,9 @@ ComboBox::ComboBox(Point position, const AnimationPath & defName, const std::pai void ComboBox::setItem(const void * item) { - if(auto w = std::dynamic_pointer_cast(overlay); getItemText) + auto w = std::dynamic_pointer_cast(overlay); + + if( w && getItemText) addTextOverlay(getItemText(0, item), w->font, w->color); if(onSetItem) diff --git a/client/widgets/ComboBox.h b/client/widgets/ComboBox.h index 561ad96f9..cd3c1e883 100644 --- a/client/widgets/ComboBox.h +++ b/client/widgets/ComboBox.h @@ -37,12 +37,13 @@ class ComboBox : public CButton bool receiveEvent(const Point & position, int eventType) const override; void clickPressed(const Point & cursorPosition) override; void setItem(const void *); + + void updateListItems(); private: std::shared_ptr buildItem(const JsonNode & config); void sliderMove(int slidPos); - void updateListItems(); ComboBox & comboBox; std::vector> items; @@ -66,4 +67,6 @@ public: std::function getItemText; void setItem(int id); + + void updateListItems(); }; diff --git a/config/widgets/advancedOptionsTab.json b/config/widgets/advancedOptionsTab.json index ca3c936f2..1175ce8b1 100644 --- a/config/widgets/advancedOptionsTab.json +++ b/config/widgets/advancedOptionsTab.json @@ -113,17 +113,17 @@ { "timerPresets" : [ - [0, 60, 0, 0], - [0, 120, 0, 0], - [0, 240, 0, 0], - [0, 360, 0, 0], - [0, 480, 0, 0], - [0, 600, 0, 0], - [0, 900, 0, 0], - [0, 1200, 0, 0], - [0, 1500, 0, 0], - [0, 1800, 0, 0], - [0, 0, 0, 0], + [0, 60, 0, 0, false, false], + [0, 120, 0, 0, false, false], + [0, 240, 0, 0, false, false], + [0, 360, 0, 0, false, false], + [0, 480, 0, 0, false, false], + [0, 600, 0, 0, false, false], + [0, 900, 0, 0, false, false], + [0, 1200, 0, 0, false, false], + [0, 1500, 0, 0, false, false], + [0, 1800, 0, 0, false, false], + [0, 0, 0, 0, false, false], ] } } diff --git a/config/widgets/playerOptionsTab.json b/config/widgets/playerOptionsTab.json index a6d8ce76b..65a36caa6 100644 --- a/config/widgets/playerOptionsTab.json +++ b/config/widgets/playerOptionsTab.json @@ -92,17 +92,30 @@ { "timerPresets" : [ - [0, 60, 0, 0], - [0, 120, 0, 0], - [0, 240, 0, 0], - [0, 360, 0, 0], - [0, 480, 0, 0], - [0, 600, 0, 0], - [0, 900, 0, 0], - [0, 1200, 0, 0], - [0, 1500, 0, 0], - [0, 1800, 0, 0], - [0, 0, 0, 0], + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 960, 480, 120, 0, true, false], + [ 960, 480, 75, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 120, 90, 60, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] ] } } diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index c5ec5f6c7..df85f2811 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -173,13 +173,15 @@ "name": "buttonTurnTimerAccumulate", "position": {"x": 160, "y": 195}, "type": "toggleButton", - "image": "lobby/checkbox" + "image": "lobby/checkbox", + "callback" : "setSimturnAI" }, { "name": "buttonUnitTimerAccumulate", "position": {"x": 160, "y": 327}, "type": "toggleButton", - "image": "lobby/checkbox" + "image": "lobby/checkbox", + "callback" : "setSimturnAI" }, { @@ -297,7 +299,6 @@ "position": {"x": 278, "y": 478} }, { - "type" : "label", "text": "vcmi.optionsTab.simturnsMin.help", "type": "multiLineLabel", "font": "tiny", @@ -306,7 +307,6 @@ "rect": {"x": 70, "y": 430, "w": 300, "h": 40} }, { - "type" : "label", "text": "vcmi.optionsTab.simturnsMax.help", "type": "multiLineLabel", "font": "tiny", @@ -318,7 +318,8 @@ "name": "buttonSimturnsAI", "position": {"x": 70, "y": 535}, "type": "toggleButton", - "image": "lobby/checkbox" + "image": "lobby/checkbox", + "callback" : "setSimturnAI" }, { "name": "labelSimturnsAI", @@ -329,5 +330,36 @@ "text": "vcmi.optionsTab.simturnsAI.hover", "position": {"x": 110, "y": 540} } - ] + ], + + "variables": + { + "timerPresets" : + [ + [ 0, 0, 0, 0, false, false], + [ 0, 60, 0, 0, false, false], + [ 0, 120, 0, 0, false, false], + [ 0, 300, 0, 0, false, false], + [ 0, 600, 0, 0, false, false], + [ 0, 1200, 0, 0, false, false], + [ 0, 1800, 0, 0, false, false], + [ 960, 480, 120, 0, true, false], + [ 960, 480, 75, 0, true, false], + [ 480, 240, 60, 0, true, false], + [ 120, 90, 60, 0, true, false], + [ 120, 60, 15, 0, true, false], + [ 60, 60, 0, 0, true, false] + ], + "simturnsPresets" : + [ + [ 0, 0, false], + [ 999, 0, false], + [ 7, 0, false], + [ 14, 0, false], + [ 28, 0, false], + [ 7, 7, false], + [ 14, 14, false], + [ 28, 28, false] + ] + } } diff --git a/server/TurnTimerHandler.cpp b/server/TurnTimerHandler.cpp index fcf80c4f8..5d3365ffe 100644 --- a/server/TurnTimerHandler.cpp +++ b/server/TurnTimerHandler.cpp @@ -74,7 +74,7 @@ void TurnTimerHandler::onPlayerGetTurn(PlayerColor player) { endTurnAllowed[player] = true; auto & timer = timers[player]; - if(si->turnTimerInfo.accumulatingTurnTimer > 0) + if(si->turnTimerInfo.accumulatingTurnTimer) timer.baseTimer += timer.turnTimer; timer.turnTimer = si->turnTimerInfo.turnTimer; From bac46e3992aac5f481c2713a5524674f1ee400d7 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 23 Nov 2023 22:24:29 +0200 Subject: [PATCH 1234/1248] Added arrow icon to dropdown images --- Mods/vcmi/Sprites/lobby/checkboxBlueOff.png | Bin 3122 -> 487 bytes Mods/vcmi/Sprites/lobby/checkboxBlueOn.png | Bin 2685 -> 401 bytes Mods/vcmi/Sprites/lobby/dropdownNormal.png | Bin 1708 -> 134 bytes Mods/vcmi/Sprites/lobby/dropdownPressed.png | Bin 1542 -> 132 bytes 4 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png b/Mods/vcmi/Sprites/lobby/checkboxBlueOff.png index b46086b14af74977926c66172d986167717e31a8..8e611ac95590eb88fd56fcc9d75cd5219d767a74 100644 GIT binary patch delta 250 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 diff --git a/Mods/vcmi/Sprites/lobby/dropdownNormal.png b/Mods/vcmi/Sprites/lobby/dropdownNormal.png index 270e3cef0db9d24c8315b3330898d2435200222b..71dc20b3dc87847d762b642ad433724774608948 100644 GIT binary patch delta 117 zcmZ3(+r~ISB9Dogfq}vITEsOV#S`EY;_4940Ava%IQ+k#y&$WE2got?ba4!+U`$rv zH!#XrtHi!WhU?e0|Nq^m?YsKF{$l9;RsYw&Se^f0>({Gj_w?G;Z4>qdd}3f=ofXC@ T@^q;H&;SNcS3j3^P67=H)?0002L+9whK00dEbR9JLUVRs;Ka&Km7Y-J#Hd2nSQWq4_3 z004N}tyWu-<2DTZ=P7ao0w4(TIDDZhH^}kRlr-bV*gLbENM*?esYaulgdEmCe|z`~ z2W2ZEsg+!FjyO_Hl^ed4cpm4SVmhwJF~>DVkMTf!4KPB2yMLTN^Sb6Y#P#-or7PYa z

    BTrQw%-~&*0=ACR_BrhQ0Gfvm~TD)58@FCmG? zmCsDFKOzkg!j*;h#N)e4JtFC=e&JKP7Xp6Je%3FJYlht9IDtOO%1=67anjfN9V*>3 zBEF{ZZQyVyAAi2wj(h!l7WZ{l*HR9UD_=!@lz4o@L`a(Ccr4?zxM2(K`>Z%iY;mCc ziBfxr9X~2D6n9EAsgctCSGi~GY=@fV?~TB512xd_DRkb;KP&xCkC7og!z*T z9eL}{hhznrCt)TNz-YU9e}?D4pPmmu?t7UNZ81?hoqq=NUSFWGo!eK;f+6IWrh4d0 z;k?D?V;d`QP!E|43T&~T5MAlEw&d_xq6SP&@mzRh^8$cG$gLoQ)txA25~WPhn2iWJ z7WmV^9E+Sg93TzK6D&E2WSCQU=f*Qe$6QuE!)PxsLM4mA8f+Apm6E26{Z_DqT2RrX zs##5oR)1|-a>|->w!HgPGqGf9+02SnYeh;bT5_>cN-eE&6|^x8HCL;p*4i49w$y;t zFux(txl2!7d+ydtue}W{pOJ=+JZzLvN1Hrlq6LJRr_D0!Y>O6_(n?ELUbf1rt2x%1 ziziplZeF~4`*iJc_0hFw=YH)PU9RzA)ShQI*MAT;`+%U`PIP+)$2bujx9tEHG;hyh zp_F(#w>^uMC5R}4rEYU3ZO7nXTEyz~>D|q_x4s3a`@6pJE9b(t?*DKuZ0qiv`|8^Z z*JeA~tsDmJT?kFlAqCsddGvkM)JLtFt>XP;d$$fNE4S!9&uKp38_YAbD+cfqZ{~R3 zMt|5hLEU|F9!uH2J|b&&QwKBJ&B_yi8&@5%`Za`}eU9S&hw<_i1j;sEF}dg2QC0O) z!NjriZ1%jmLjySMD6OwD6D1{!Kj%|=A@Mt?qqGf0SIQV~Nfn(W-B)k}dm_Kz{SW-{ zD*FQWRKuGB!ijfZ!}jr}+sismIJ(y@ynn9Y5$q=>gm)GEt_k5i``ztY6! z!{5W2ML3nQ&_PCCQrlRq_XhkHM)Ic0*k!Fdb!m*=hdr)yZLsO_2t8 zQ0)!?ue6h5zm(9X)#`Z9j3VAs&c0T&l|T+OUp>;KR`i=_p39g%XYx@TVnc@6M1Rap z#ra4I2}~q*jxV6T<^%c)Ia5wwp&ru#0QrDF#&|EX@6{G004NLeUUv#!$2IxUsI)`6^9lPamY}eEQpGB zlqwd%LTM|s>R@u|7c^-|Qd}Gb*Mfr|i&X~~XI&j!1wrrw#MQ+~(M3x9Us`Apm3W#sq^KI@3z;S>oVPfuYy`kk%-000SaNLh0L04^f{04^f|c%?sf00007bV*G`2j>eG6FV66iPDb% z002u#L_t(&-tE!>4FDhvz_3FQh6E&C5)zPrBy2pPm9VVMl>ud(og*UdJz7vlmjF|O yDKRCO5>tXHF(sH1Od*LW!IYR1Oo=H0KKB52`~`%pF^cB^0000pE#exG;tB8xaRt&20Sy8Q4*&0G^9u030`iSLT^vIy7?Tym zJK83(N-PPR$lBJl@9O{mlhg12um5s&`+xD;uWRdF<7-!kUkty$YT;sj250V&t0J9F Rn}FIGJYD@<);T3K0RWY&Dw+TQ delta 1536 zcmV+b2LJhl0fr2a7=H)?0002L+9whK00X9aR9JLUVRs;Ka&Km7Y-J#Hd2nSQWq4_3 z004N}t(Iw$^d=03|8t5Q0Z9nKaagNzgB(9En4X^Y%$HdzrrOv7v^@g18~^-0-CwvU z#N>iniaAD)E98*5;34_zx_a_q-}lwq8KYOZBR&HRk)W6BHGhsXzd)|f1s0CHzRH~) z3!ILkGw@I-%knbTkCR7{@Fl17?(*@d#CPGG-)x+hNb5Cpe-DOabcH@)$3sZ`zVl^C z)=#8BLb$W=nz)};@|M`g{JFQ(eUiY3+xPtWc4o*k9hag+R>Jt96+ z_&KoiCjQ~m$AA3Ip5`1^bR|TGbj1_YBgFlLiAZUXy)2>`eu`(?Eu$r0d_(vhp;qTB zUNSKoZpiqOB{9z20EHy1Mw88fLze^+4deu3e)EO4-Ms9< zSc2tFn8^fXv}nF>?k@Okdr>s!Ds!ThOvFwXU|#hFH-8qnb;m3qpm}ntJHOEtdgN*`MA!vx$ZwVz-oQeijjcS@SYl#v= z)EJ}1b$?D36APvm%`92Ak|c$sDJDxPr2aUc-4MF0iOiXS{g41ac1Hmweb{U*!oJt#g9iDHK>!TCtsJ^Y5xl!sBs0|}W^2HcxiWrGY~mXxvBsaBiaYO^&| ze19rw)Q*p?rV(TBElX=@jSX9#&w-_LCZ$U&9NBu^JZg~}PL+MF945|`H6>52I%l0I zp+UiNUT4TGy{yp=ZL9Y%or>d@cs$I$7{y`zFnbT?qt)y#qo~&xv&iLT_?X>BUu@n( zb@y0*8>+j<`rA<5J=Wib>b}DI-B8_ESbx78s{0D-cSChwVf}8X?xz*}r%>IuD|qe9 z_Z#!tme8E=Pf)tU=EZY~?8|=gzwF(Q7lJ^?UsCrkojl{~7?RSo0004nX+uL$Nkc;* zaB^>EX>4Tx0C=2zkv&MmKpe$iQ>CI6hZYfW$WWauh>CWUDi*;)X)CnqU~=gfG=FJG zQd}Gb*Mfr|i&X~~XI&j!1wrrw#MQ+~(M3x9Us`Apm3W#sq^KI@ z3z;S>oVPfueG6FVDifP*yv002-)L_t(&-tE#c5x_7I z13|@OV1ZyrhGqy4DMdJVzSwQlYc7Fio+R(uj-ZeEPu1}SO$-+xN)RQY1W_VN5GA4n mQGzHDC5RGHf+!Is-1Pw*%L9}Q=lw+h0000 Date: Thu, 7 Dec 2023 12:57:39 +0100 Subject: [PATCH 1235/1248] Do not route road through the Corpse --- lib/rmg/modificators/ObjectManager.cpp | 8 ++++++-- lib/rmg/modificators/RoadPlacer.cpp | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/rmg/modificators/ObjectManager.cpp b/lib/rmg/modificators/ObjectManager.cpp index 1288f34f4..f05ab1bc9 100644 --- a/lib/rmg/modificators/ObjectManager.cpp +++ b/lib/rmg/modificators/ObjectManager.cpp @@ -546,8 +546,12 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD objects.push_back(&instance->object()); if(auto * m = zone.getModificator()) { - //FIXME: Objects that can be removed, can be trespassed. Does not include Corpse - if(instance->object().appearance->isVisitableFromTop()) + if (instance->object().blockVisit && !instance->object().removable) + { + //Cannot be trespassed (Corpse) + continue; + } + else if(instance->object().appearance->isVisitableFromTop()) m->areaForRoads().add(instance->getVisitablePosition()); else { diff --git a/lib/rmg/modificators/RoadPlacer.cpp b/lib/rmg/modificators/RoadPlacer.cpp index aca1acfd7..b081f9485 100644 --- a/lib/rmg/modificators/RoadPlacer.cpp +++ b/lib/rmg/modificators/RoadPlacer.cpp @@ -85,7 +85,7 @@ bool RoadPlacer::createRoad(const int3 & dst) { if(areaIsolated().contains(dst) || areaIsolated().contains(src)) { - return 1e30; + return 1e12; } } else From 960697873a601348bece960ba98588a6f29b62fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 7 Dec 2023 14:54:05 +0100 Subject: [PATCH 1236/1248] Fix object configs --- config/objects/rewardableOnceVisitable.json | 3 +-- config/objects/rewardablePickable.json | 24 +++++++++------------ 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/config/objects/rewardableOnceVisitable.json b/config/objects/rewardableOnceVisitable.json index 02b77567b..5999d2cb5 100644 --- a/config/objects/rewardableOnceVisitable.json +++ b/config/objects/rewardableOnceVisitable.json @@ -41,6 +41,7 @@ "index" : 22, "handler": "configurable", "base" : { + "blockedVisitable" : true, "sounds" : { "visit" : ["MYSTERY"] } @@ -54,9 +55,7 @@ "rarity" : 100 }, "compatibilityIdentifiers" : [ "object" ], - "onVisitedMessage" : 38, - "blockedVisitable" : true, "visitMode" : "once", "selectMode" : "selectFirst", "rewards" : [ diff --git a/config/objects/rewardablePickable.json b/config/objects/rewardablePickable.json index 5f6f327de..764065a30 100644 --- a/config/objects/rewardablePickable.json +++ b/config/objects/rewardablePickable.json @@ -5,6 +5,8 @@ "index" : 12, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "ambient" : ["LOOPCAMP"], "visit" : ["EXPERNCE"], @@ -19,10 +21,7 @@ "value" : 2000, "rarity" : 500 }, - "removable": true, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -49,6 +48,8 @@ "index" : 29, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["GENIE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -62,17 +63,14 @@ "value" : 2000, "rarity" : 100 }, - "removable": true, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ { "message" : 51, "appearChance" : { "max" : 25 }, - "removeObject" : true, + "removeObject" : true }, { "message" : 52, @@ -108,6 +106,8 @@ "index" : 82, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -121,10 +121,7 @@ "value" : 1500, "rarity" : 500 }, - "removable": true, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -160,6 +157,8 @@ "index" : 86, "handler": "configurable", "base" : { + "blockedVisitable" : true, + "removable": true, "sounds" : { "visit" : ["TREASURE"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -173,10 +172,7 @@ "value" : 1500, "rarity" : 50 }, - "removable": true, "compatibilityIdentifiers" : [ "object" ], - - "blockedVisitable" : true, "visitMode" : "unlimited", "selectMode" : "selectFirst", "rewards" : [ @@ -212,6 +208,7 @@ "index" : 101, "handler": "configurable", "base" : { + "removable": true, "sounds" : { "visit" : ["CHEST"], "removal" : [ "PICKUP01", "PICKUP02", "PICKUP03", "PICKUP04", "PICKUP05", "PICKUP06", "PICKUP07" ] @@ -225,7 +222,6 @@ "value" : 1500, "rarity" : 1000 }, - "removable": true, "compatibilityIdentifiers" : [ "object" ], "blockedVisitable" : true, From edbe25b33a9aa5b019221fef74e2444defefce31 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 16:36:44 +0200 Subject: [PATCH 1237/1248] Updated layout and localizations --- Mods/vcmi/config/vcmi/chinese.json | 4 +- Mods/vcmi/config/vcmi/english.json | 36 ++++++++- Mods/vcmi/config/vcmi/german.json | 6 +- Mods/vcmi/config/vcmi/russian.json | 4 +- Mods/vcmi/config/vcmi/ukrainian.json | 37 ++++++++- Mods/vcmi/config/vcmi/vietnamese.json | 4 +- client/lobby/OptionsTabBase.cpp | 76 +++++++++++++------ client/lobby/OptionsTabBase.h | 10 +++ .../widgets/turnOptionsDropdownLibrary.json | 46 +++++------ config/widgets/turnOptionsTab.json | 44 +++++------ lib/StartInfo.h | 7 ++ lib/TurnTimerInfo.cpp | 2 +- lib/TurnTimerInfo.h | 10 +++ 13 files changed, 200 insertions(+), 86 deletions(-) diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 8f1b403eb..0c5010903 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -205,11 +205,11 @@ "vcmi.optionsTab.chessFieldBase.hover" : "额外计时器", "vcmi.optionsTab.chessFieldTurn.hover" : "转动计时器", "vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器", - "vcmi.optionsTab.chessFieldCreature.hover" : "堆栈计时器", + "vcmi.optionsTab.chessFieldUnit.hover" : "堆栈计时器", "vcmi.optionsTab.chessFieldBase.help" : "当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。", "vcmi.optionsTab.chessFieldTurn.help" : "当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。", "vcmi.optionsTab.chessFieldBattle.help" : "战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。", - "vcmi.optionsTab.chessFieldCreature.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", + "vcmi.optionsTab.chessFieldUnit.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。", // Custom victory conditions for H3 campaigns and HotA maps "vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index d50199528..655fd7320 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -236,17 +236,20 @@ "vcmi.optionsTab.turnOptions.hover" : "Turn Options", "vcmi.optionsTab.turnOptions.help" : "Select turn timer and simultaneous turns options", + "vcmi.optionsTab.selectPreset" : "Preset", "vcmi.optionsTab.chessFieldBase.hover" : "Base timer", "vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer", "vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer", - "vcmi.optionsTab.chessFieldCreature.hover" : "Unit timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Unit timer", "vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.", - "vcmi.optionsTab.chessFieldTurn.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Any unspent time is lost", "vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.", - "vcmi.optionsTab.chessFieldCreature.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost", - "vcmi.optionsTab.simturns" : "Simultaneous turns", + "vcmi.optionsTab.simturnsTitle" : "Simultaneous turns", "vcmi.optionsTab.simturnsMin.hover" : "At least for", "vcmi.optionsTab.simturnsMax.hover" : "At most for", "vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns", @@ -254,6 +257,31 @@ "vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player", "vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.", + "vcmi.optionsTab.turnTime.select" : "Select turn timer preset", + "vcmi.optionsTab.turnTime.unlimited" : "Unlimited turn time", + "vcmi.optionsTab.turnTime.classic.1" : "Classic timer: 1 minute", + "vcmi.optionsTab.turnTime.classic.2" : "Classic timer: 2 minutes", + "vcmi.optionsTab.turnTime.classic.5" : "Classic timer: 5 minutes", + "vcmi.optionsTab.turnTime.classic.10" : "Classic timer: 10 minutes", + "vcmi.optionsTab.turnTime.classic.20" : "Classic timer: 20 minutes", + "vcmi.optionsTab.turnTime.classic.30" : "Classic timer: 30 minutes", + "vcmi.optionsTab.turnTime.chess.20" : "Chess: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Chess: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Chess: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Chess: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Chess: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Chess: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Select simultaneous turns preset", + "vcmi.optionsTab.simturns.none" : "No simultaneous turns", + "vcmi.optionsTab.simturns.tillContactMax" : "Simturns: Until contact", + "vcmi.optionsTab.simturns.tillContact1" : "Simturns: 1 week, break on contact", + "vcmi.optionsTab.simturns.tillContact2" : "Simturns: 2 weeks, break on contact", + "vcmi.optionsTab.simturns.tillContact4" : "Simturns: 1 month, break on contact", + "vcmi.optionsTab.simturns.blocked1" : "Simturns: 1 week, contacts blocked", + "vcmi.optionsTab.simturns.blocked2" : "Simturns: 2 weeks, contacts blocked", + "vcmi.optionsTab.simturns.blocked4" : "Simturns: 1 month, contacts blocked", + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language // Using this information, VCMI will automatically select correct plural form for every possible amount "vcmi.optionsTab.simturns.days.0" : " %d days", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 7f3db42ec..d22c45af5 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -240,13 +240,13 @@ "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", "vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer", "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", - "vcmi.optionsTab.chessFieldCreature.hover" : "Einheiten-Timer", + "vcmi.optionsTab.chessFieldUnit.hover" : "Einheiten-Timer", "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Spielzug beendet. Jeder laufende Kampf endet mit einem Verlust.", "vcmi.optionsTab.chessFieldTurn.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", "vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", - "vcmi.optionsTab.chessFieldCreature.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn der Runde jeder Einheit zurückgesetzt.", + "vcmi.optionsTab.chessFieldUnit.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn der Runde jeder Einheit zurückgesetzt.", - "vcmi.optionsTab.simturns" : "Simultane Züge", + "vcmi.optionsTab.simturnsTitle" : "Simultane Züge", "vcmi.optionsTab.simturnsMin.hover" : "Zumindest für", "vcmi.optionsTab.simturnsMax.hover" : "Höchstens für", "vcmi.optionsTab.simturnsAI.hover" : "(Experimentell) Simultane KI Züge", diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 3374b9df4..636d36d7c 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -204,11 +204,11 @@ "vcmi.optionsTab.chessFieldBase.hover" : "Время игрока", "vcmi.optionsTab.chessFieldTurn.hover" : "Время на ход", "vcmi.optionsTab.chessFieldBattle.hover" : "Время на битву", - "vcmi.optionsTab.chessFieldCreature.hover" : "Время на отряд", + "vcmi.optionsTab.chessFieldUnit.hover" : "Время на отряд", "vcmi.optionsTab.chessFieldBase.help" : "Обратный отсчет начинается когда {время на ход} истекает. Устанавливается один раз в начале игры. По истечении времени игрок завершает ход.", "vcmi.optionsTab.chessFieldTurn.help" : "Обратный отсчет начивается когда игрок начинает свой ход. В начале каждого хода устанавливается в иходное значение. Все неиспользованное время добавляется ко {времени игрока}, если оно используется.", "vcmi.optionsTab.chessFieldBattle.help" : "Обратный отсчет начинается когда {время на отряд истекает}. В начале каждой битвы устанавливается в исходное значение. По истечении времени текущий отряд получает приказ защищаться.", - "vcmi.optionsTab.chessFieldCreature.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", + "vcmi.optionsTab.chessFieldUnit.help" : "Обратный отсчет начинается когда игрок получает получает контроль над отрядом во время битвы. Устанавливается в исходное значение всякий раз, когда отряд получает возможность действовать.", "mapObject.core.creatureBank.cyclopsStockpile.name" : "Хранилище циклопов", "mapObject.core.creatureBank.dragonFlyHive.name" : "Улей летучих змиев", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 8fc46d9a9..f2add0ec2 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -229,13 +229,17 @@ "vcmi.optionsTab.chessFieldBase.hover" : "Основний таймер", "vcmi.optionsTab.chessFieldTurn.hover" : "Таймер ходу", "vcmi.optionsTab.chessFieldBattle.hover" : "Таймер битви", - "vcmi.optionsTab.chessFieldCreature.hover" : "Таймер загону", + "vcmi.optionsTab.chessFieldUnit.hover" : "Таймер загону", "vcmi.optionsTab.chessFieldBase.help" : "Встановлюється один раз на початку гри. Коли вичерпується, поточний хід буде перервано, поточна битва буде програна.", - "vcmi.optionsTab.chessFieldTurn.help" : "Використовується під час ходу. Встановлюється кожен хід. Залишок додається до {основного таймеру} у кінці ходу", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок додається до {основного таймеру}", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Використовується під час ходу. Встановлюється на початку ходу. Залишок часу буде втрачено", "vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.", - "vcmi.optionsTab.chessFieldCreature.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку кожної дії.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок додається до {таймеру битви}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок часу буде втрачено.", + + "vcmi.optionsTab.accumulate" : "Накопичувати", - "vcmi.optionsTab.simturns" : "Одночасні ходи", + "vcmi.optionsTab.simturnsTitle" : "Одночасні ходи", "vcmi.optionsTab.simturnsMin.hover" : "Щонайменше", "vcmi.optionsTab.simturnsMax.hover" : "Щонайбільше", "vcmi.optionsTab.simturnsAI.hover" : "(Експериментально) Одночасні ходи ШІ", @@ -243,6 +247,31 @@ "vcmi.optionsTab.simturnsMax.help" : "Грати одночасно обрану кількість днів чи до першого контакту з іншим гравцем", "vcmi.optionsTab.simturnsAI.help" : "{Одночасні ходи ШІ}\nЕкспериментальна опція. Дозволяє гравцям-ШІ діяти одночасно с гравцями-людьми якщо одночасні ходи увімкнені.", + "vcmi.optionsTab.turnTime.select" : "Типові налаштування таймерів", + "vcmi.optionsTab.turnTime.unlimited" : "Необмежений час ходу", + "vcmi.optionsTab.turnTime.classic.1" : "Класичний таймер: 1 хвилина", + "vcmi.optionsTab.turnTime.classic.2" : "Класичний таймер: 2 хвилини", + "vcmi.optionsTab.turnTime.classic.5" : "Класичний таймер: 5 хвилин", + "vcmi.optionsTab.turnTime.classic.10" : "Класичний таймер: 10 хвилин", + "vcmi.optionsTab.turnTime.classic.20" : "Класичний таймер: 20 хвилин", + "vcmi.optionsTab.turnTime.classic.30" : "Класичний таймер: 30 хвилин", + "vcmi.optionsTab.turnTime.chess.20" : "Шахи: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Шахи: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Шахи: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Шахи: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Шахи: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Шахи: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Типові налаштування одночасних ходів", + "vcmi.optionsTab.simturns.none" : "Без одночасних ходів", + "vcmi.optionsTab.simturns.tillContactMax" : "Одночасно: До контакту", + "vcmi.optionsTab.simturns.tillContact1" : "Одночасно: 1 тиждень, до контакту", + "vcmi.optionsTab.simturns.tillContact2" : "Одночасно: 2 тижні, до контакту", + "vcmi.optionsTab.simturns.tillContact4" : "Одночасно: 1 місяць, до контакту", + "vcmi.optionsTab.simturns.blocked1" : "Одночасно: 1 тиждень, без контактів", + "vcmi.optionsTab.simturns.blocked2" : "Одночасно: 2 тижні, без контактів", + "vcmi.optionsTab.simturns.blocked4" : "Одночасно: 1 місяць, без контактів", + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language // Using this information, VCMI will automatically select correct plural form for every possible amount "vcmi.optionsTab.simturns.days.0" : " %d днів", diff --git a/Mods/vcmi/config/vcmi/vietnamese.json b/Mods/vcmi/config/vcmi/vietnamese.json index 0750201cf..5939c1db0 100644 --- a/Mods/vcmi/config/vcmi/vietnamese.json +++ b/Mods/vcmi/config/vcmi/vietnamese.json @@ -201,11 +201,11 @@ "vcmi.optionsTab.chessFieldBase.hover" : "Thời gian thêm", "vcmi.optionsTab.chessFieldTurn.hover" : "Thời gian lượt", "vcmi.optionsTab.chessFieldBattle.hover" : "Thời gian trận đánh", - "vcmi.optionsTab.chessFieldCreature.hover" : "Thời gian lính", + "vcmi.optionsTab.chessFieldUnit.hover" : "Thời gian lính", "vcmi.optionsTab.chessFieldBase.help": "Bắt đầu đếm ngược khi {Thời gian lượt} giảm đến 0. Được đặt 1 lần khi bắt đầu trò chơi. Khi thời gian này giảm đến 0, lượt của người chơi kết thúc.", "vcmi.optionsTab.chessFieldTurn.help": "Bắt đầu đếm ngược khi đến lượt người chơi trên bản đồ phiêu lưu. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi lượt. Thời gian lượt chưa sử dụng sẽ được thêm vào {Thời gian thêm} nếu có.", "vcmi.optionsTab.chessFieldBattle.help": "Đếm ngược trong suốt trận đánh khi {Thời gian lính} giảm đến 0. Nó được đặt lại giá trị ban đầu khi bắt đầu mỗi trận đánh. Nếu thời gian giảm đến 0, đội lính hiện tại sẽ phòng thủ.", - "vcmi.optionsTab.chessFieldCreature.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", + "vcmi.optionsTab.chessFieldUnit.help": "Bắt đầu đếm ngược khi người chơi đang chọn hành động cho đội linh hiện tại trong suốt trận đánh. Nó được đặt lại giá trị ban đầu sau khi hành động của đội lính hoàn tất.", "vcmi.map.victoryCondition.daysPassed.toOthers": "Đối thủ đã xoay xở để sinh tồn đến ngày này. Họ giành chiến thắng!", "vcmi.map.victoryCondition.daysPassed.toSelf": "Chúc mừng! Bạn đã vượt khó để sinh tồn. Chiến thắng thuộc về bạn!", diff --git a/client/lobby/OptionsTabBase.cpp b/client/lobby/OptionsTabBase.cpp index 2fc0ad34f..3dc78819b 100644 --- a/client/lobby/OptionsTabBase.cpp +++ b/client/lobby/OptionsTabBase.cpp @@ -22,35 +22,49 @@ #include "../../lib/MetaString.h" #include "../../lib/CGeneralTextHandler.h" +std::vector OptionsTabBase::getTimerPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["timerPresets"].Vector()) + { + TurnTimerInfo tinfo; + tinfo.baseTimer = tpreset[0].Integer() * 1000; + tinfo.turnTimer = tpreset[1].Integer() * 1000; + tinfo.battleTimer = tpreset[2].Integer() * 1000; + tinfo.unitTimer = tpreset[3].Integer() * 1000; + tinfo.accumulatingTurnTimer = tpreset[4].Bool(); + tinfo.accumulatingUnitTimer = tpreset[5].Bool(); + result.push_back(tinfo); + } + return result; +} + +std::vector OptionsTabBase::getSimturnsPresets() const +{ + std::vector result; + + for (auto const & tpreset : variables["simturnsPresets"].Vector()) + { + SimturnsInfo tinfo; + tinfo.optionalTurns = tpreset[0].Integer(); + tinfo.requiredTurns = tpreset[1].Integer(); + tinfo.allowHumanWithAI = tpreset[2].Bool(); + result.push_back(tinfo); + } + return result; +} + OptionsTabBase::OptionsTabBase(const JsonPath & configPath) { recActions = 0; auto setTimerPresetCallback = [this](int index){ - if(!variables["timerPresets"].isNull()) - { - auto tpreset = variables["timerPresets"].Vector().at(index).Vector(); - TurnTimerInfo tinfo = SEL->getStartInfo()->turnTimerInfo; - tinfo.baseTimer = tpreset.at(0).Integer() * 1000; - tinfo.turnTimer = tpreset.at(1).Integer() * 1000; - tinfo.battleTimer = tpreset.at(2).Integer() * 1000; - tinfo.unitTimer = tpreset.at(3).Integer() * 1000; - tinfo.accumulatingTurnTimer = tpreset.at(4).Bool(); - tinfo.accumulatingUnitTimer = tpreset.at(5).Bool(); - CSH->setTurnTimerInfo(tinfo); - } + CSH->setTurnTimerInfo(getTimerPresets().at(index)); }; auto setSimturnsPresetCallback = [this](int index){ - if(!variables["simturnsPresets"].isNull()) - { - auto tpreset = variables["simturnsPresets"].Vector().at(index).Vector(); - SimturnsInfo tinfo = SEL->getStartInfo()->simturnsInfo; - tinfo.optionalTurns = tpreset.at(0).Integer(); - tinfo.requiredTurns = tpreset.at(1).Integer(); - tinfo.allowHumanWithAI = tpreset.at(2).Bool(); - CSH->setSimturnsInfo(tinfo); - } + CSH->setSimturnsInfo(getSimturnsPresets().at(index)); }; addCallback("setTimerPreset", setTimerPresetCallback); @@ -157,7 +171,7 @@ OptionsTabBase::OptionsTabBase(const JsonPath & configPath) CSH->setTurnTimerInfo(tinfo); } }); - addCallback("parseAndSetTimer_creature", [parseTimerString](const std::string & str){ + addCallback("parseAndSetTimer_unit", [parseTimerString](const std::string & str){ int time = parseTimerString(str) * 1000; if(time >= 0) { @@ -306,6 +320,22 @@ void OptionsTabBase::recreate() if(auto buttonTurnTimerAccumulate = widget("buttonTurnTimerAccumulate")) buttonTurnTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer); + if(auto chessFieldTurnLabel = widget("chessFieldTurnLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingTurnTimer) + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnAccumulate.help")); + else + chessFieldTurnLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldTurnDiscard.help")); + } + + if(auto chessFieldUnitLabel = widget("chessFieldUnitLabel")) + { + if (SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer) + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitAccumulate.help")); + else + chessFieldUnitLabel->setText(CGI->generaltexth->translate("vcmi.optionsTab.chessFieldUnitDiscard.help")); + } + if(auto buttonUnitTimerAccumulate = widget("buttonUnitTimerAccumulate")) buttonUnitTimerAccumulate->setSelectedSilent(SEL->getStartInfo()->turnTimerInfo.accumulatingUnitTimer); @@ -343,7 +373,7 @@ void OptionsTabBase::recreate() ww->setText(timeToString(turnTimerRemote.turnTimer), false); if(auto ww = widget("chessFieldBattle")) ww->setText(timeToString(turnTimerRemote.battleTimer), false); - if(auto ww = widget("chessFieldCreature")) + if(auto ww = widget("chessFieldUnit")) ww->setText(timeToString(turnTimerRemote.unitTimer), false); if(auto w = widget("timerModeSwitch")) diff --git a/client/lobby/OptionsTabBase.h b/client/lobby/OptionsTabBase.h index 4ba7e82c8..dc7bf63f2 100644 --- a/client/lobby/OptionsTabBase.h +++ b/client/lobby/OptionsTabBase.h @@ -12,9 +12,19 @@ #include "../gui/InterfaceObjectConfigurable.h" #include "../../lib/filesystem/ResourcePath.h" +VCMI_LIB_NAMESPACE_BEGIN + +struct TurnTimerInfo; +struct SimturnsInfo; + +VCMI_LIB_NAMESPACE_END + /// The options tab which is shown at the map selection phase. class OptionsTabBase : public InterfaceObjectConfigurable { + std::vector getTimerPresets() const; + std::vector getSimturnsPresets() const; + public: OptionsTabBase(const JsonPath & configPath); diff --git a/config/widgets/turnOptionsDropdownLibrary.json b/config/widgets/turnOptionsDropdownLibrary.json index 8e4049b1a..162fbd775 100644 --- a/config/widgets/turnOptionsDropdownLibrary.json +++ b/config/widgets/turnOptionsDropdownLibrary.json @@ -37,7 +37,7 @@ "font": "small", "alignment": "left", "color": "white", - "text": "Custom turn time" + "text": "vcmi.optionsTab.turnTime.select" } ], "dropDown": @@ -69,7 +69,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Unlimited turn time" + "text": "vcmi.optionsTab.turnTime.unlimited" }, { "type": "dropDownHover", @@ -89,7 +89,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 1 minute" + "text": "vcmi.optionsTab.turnTime.classic.1" }, { "type": "dropDownHover", @@ -109,7 +109,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 2 minutes" + "text": "vcmi.optionsTab.turnTime.classic.2" }, { "type": "dropDownHover", @@ -129,7 +129,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 5 minutes" + "text": "vcmi.optionsTab.turnTime.classic.5" }, { "type": "dropDownHover", @@ -149,7 +149,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 10 minutes" + "text": "vcmi.optionsTab.turnTime.classic.10" }, { "type": "dropDownHover", @@ -169,7 +169,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 20 minutes" + "text": "vcmi.optionsTab.turnTime.classic.20" }, { "type": "dropDownHover", @@ -189,7 +189,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Classic timer: 30 minutes" + "text": "vcmi.optionsTab.turnTime.classic.30" }, { "type": "dropDownHover", @@ -209,7 +209,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 16:00 + 08:00 + 02:00 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.20" }, { "type": "dropDownHover", @@ -229,7 +229,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 16:00 + 08:00 + 01:15 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.16" }, { "type": "dropDownHover", @@ -249,7 +249,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 08:00 + 04:00 + 01:00 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.8" }, { "type": "dropDownHover", @@ -269,7 +269,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 02:00 + 01:30 + 01:00 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.4" }, { "type": "dropDownHover", @@ -289,7 +289,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 02:00 + 01:00 + 00:15 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.2" }, { "type": "dropDownHover", @@ -309,7 +309,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Chess: 01:00 + 01:00 + 00:00 + 00:00" + "text": "vcmi.optionsTab.turnTime.chess.1" }, { "type": "dropDownHover", @@ -334,7 +334,7 @@ "font": "small", "alignment": "left", "color": "white", - "text": "Custom simultaneous turns" + "text": "vcmi.optionsTab.simturns.select" } ], "dropDown": @@ -366,7 +366,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "No simultaneous turns" + "text": "vcmi.optionsTab.simturns.none" }, { "type": "dropDownHover", @@ -386,7 +386,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: Until contact" + "text": "vcmi.optionsTab.simturns.tillContactMax" }, { "type": "dropDownHover", @@ -406,7 +406,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 1 week, break on contact" + "text": "vcmi.optionsTab.simturns.tillContact1" }, { "type": "dropDownHover", @@ -426,7 +426,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 2 weeks, break on contact" + "text": "vcmi.optionsTab.simturns.tillContact2" }, { "type": "dropDownHover", @@ -446,7 +446,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 1 month, break on contact" + "text": "vcmi.optionsTab.simturns.tillContact4" }, { "type": "dropDownHover", @@ -466,7 +466,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 1 week, contacts blocked" + "text": "vcmi.optionsTab.simturns.blocked1" }, { "type": "dropDownHover", @@ -486,7 +486,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 2 weeks, contacts blocked" + "text": "vcmi.optionsTab.simturns.blocked2" }, { "type": "dropDownHover", @@ -506,7 +506,7 @@ { "type": "dropDownLabel", "name": "labelName", - "text": "Simturns: 1 month, contacts blocked" + "text": "vcmi.optionsTab.simturns.blocked4" }, { "type": "dropDownHover", diff --git a/config/widgets/turnOptionsTab.json b/config/widgets/turnOptionsTab.json index df85f2811..79cdd1737 100644 --- a/config/widgets/turnOptionsTab.json +++ b/config/widgets/turnOptionsTab.json @@ -67,14 +67,14 @@ "adoptHeight": true }, - { - "type": "label", - "font": "medium", - "alignment": "center", - "color": "yellow", - "text": "Select preset", - "position": {"x": 105, "y": 100} - }, +// { +// "type": "label", +// "font": "medium", +// "alignment": "center", +// "color": "yellow", +// "text": "vcmi.optionsTab.selectPreset", +// "position": {"x": 105, "y": 100} +// }, { "type" : "dropDownTimers", "name": "timerPresetSelector", @@ -140,10 +140,10 @@ "text": "vcmi.optionsTab.chessFieldBattle.hover" }, { - "text": "vcmi.optionsTab.chessFieldCreature.hover" + "text": "vcmi.optionsTab.chessFieldUnit.hover" }, { - "text": "vcmi.optionsTab.simturns" + "text": "vcmi.optionsTab.simturnsTitle" } ] }, @@ -158,13 +158,13 @@ "text": "vcmi.optionsTab.chessFieldBase.help" }, { - "text": "vcmi.optionsTab.chessFieldTurn.help" + "name": "chessFieldTurnLabel" }, { "text": "vcmi.optionsTab.chessFieldBattle.help" }, { - "text": "vcmi.optionsTab.chessFieldCreature.help" + "name": "chessFieldUnitLabel" } ] }, @@ -174,25 +174,25 @@ "position": {"x": 160, "y": 195}, "type": "toggleButton", "image": "lobby/checkbox", - "callback" : "setSimturnAI" + "callback" : "setTurnTimerAccumulate" }, { "name": "buttonUnitTimerAccumulate", "position": {"x": 160, "y": 327}, "type": "toggleButton", "image": "lobby/checkbox", - "callback" : "setSimturnAI" + "callback" : "setUnitTimerAccumulate" }, { "type" : "labelTitle", "position": {"x": 195, "y": 199}, - "text" : "Accumulate" + "text" : "vcmi.optionsTab.accumulate" }, { "type" : "labelTitle", "position": {"x": 195, "y": 331}, - "text" : "Accumulate" + "text" : "vcmi.optionsTab.accumulate" }, { @@ -230,9 +230,9 @@ "help": "vcmi.optionsTab.chessFieldBattle.help" }, { - "name": "chessFieldCreature", - "callback": "parseAndSetTimer_creature", - "help": "vcmi.optionsTab.chessFieldCreature.help" + "name": "chessFieldUnit", + "callback": "parseAndSetTimer_unit", + "help": "vcmi.optionsTab.chessFieldUnit.help" } ] }, @@ -343,10 +343,10 @@ [ 0, 600, 0, 0, false, false], [ 0, 1200, 0, 0, false, false], [ 0, 1800, 0, 0, false, false], - [ 960, 480, 120, 0, true, false], - [ 960, 480, 75, 0, true, false], + [ 1200, 600, 120, 0, true, false], + [ 960, 480, 90, 0, true, false], [ 480, 240, 60, 0, true, false], - [ 120, 90, 60, 0, true, false], + [ 240, 120, 30, 0, true, false], [ 120, 60, 15, 0, true, false], [ 60, 60, 0, 0, true, false] ], diff --git a/lib/StartInfo.h b/lib/StartInfo.h index 208ef2706..86b5d6815 100644 --- a/lib/StartInfo.h +++ b/lib/StartInfo.h @@ -32,6 +32,13 @@ struct DLL_LINKAGE SimturnsInfo /// If set to true, human and 1 AI can act at the same time bool allowHumanWithAI = false; + bool operator == (const SimturnsInfo & other) const + { + return requiredTurns == other.requiredTurns && + optionalTurns == other.optionalTurns && + allowHumanWithAI == other.allowHumanWithAI; + } + template void serialize(Handler &h, const int version) { diff --git a/lib/TurnTimerInfo.cpp b/lib/TurnTimerInfo.cpp index c7fbcd936..1b0785ecd 100644 --- a/lib/TurnTimerInfo.cpp +++ b/lib/TurnTimerInfo.cpp @@ -19,7 +19,7 @@ bool TurnTimerInfo::isEnabled() const bool TurnTimerInfo::isBattleEnabled() const { - return unitTimer > 0 || battleTimer > 0; + return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0; } VCMI_LIB_NAMESPACE_END diff --git a/lib/TurnTimerInfo.h b/lib/TurnTimerInfo.h index 93d951538..0f98df4b8 100644 --- a/lib/TurnTimerInfo.h +++ b/lib/TurnTimerInfo.h @@ -27,6 +27,16 @@ struct DLL_LINKAGE TurnTimerInfo bool isEnabled() const; bool isBattleEnabled() const; + + bool operator == (const TurnTimerInfo & other) const + { + return turnTimer == other.turnTimer && + baseTimer == other.baseTimer && + battleTimer == other.battleTimer && + unitTimer == other.unitTimer && + accumulatingTurnTimer == other.accumulatingTurnTimer && + accumulatingUnitTimer == other.accumulatingUnitTimer; + } template void serialize(Handler &h, const int version) From 7fa01a34971d82510a201c6148522caa55720aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 7 Dec 2023 17:41:09 +0100 Subject: [PATCH 1238/1248] Fix for randomly banning or exceeding limits of zone objects --- lib/rmg/modificators/TreasurePlacer.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/rmg/modificators/TreasurePlacer.cpp b/lib/rmg/modificators/TreasurePlacer.cpp index 84afa71e6..755bda88a 100644 --- a/lib/rmg/modificators/TreasurePlacer.cpp +++ b/lib/rmg/modificators/TreasurePlacer.cpp @@ -836,13 +836,18 @@ void TreasurePlacer::createTreasures(ObjectManager& manager) int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; }); - for (ui32 attempt = 0; attempt <= 2; attempt++) + const ui32 maxPileGenerationAttemps = 2; + for (ui32 attempt = 0; attempt <= maxPileGenerationAttemps; attempt++) { auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts); - if (rmgObject.instances().empty()) //handle incorrect placement + if (rmgObject.instances().empty()) { - restoreZoneLimits(treasurePileInfos); + // Restore once if all attemps failed + if (attempt == (maxPileGenerationAttemps - 1)) + { + restoreZoneLimits(treasurePileInfos); + } continue; } From c316087950e71d288ec49005da15c2b897baa1c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Zieli=C5=84ski?= Date: Thu, 7 Dec 2023 17:41:41 +0100 Subject: [PATCH 1239/1248] Distribute objects among zones in random order --- lib/rmg/modificators/ObjectDistributor.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rmg/modificators/ObjectDistributor.cpp b/lib/rmg/modificators/ObjectDistributor.cpp index 7ed97708e..5e9eacca8 100644 --- a/lib/rmg/modificators/ObjectDistributor.cpp +++ b/lib/rmg/modificators/ObjectDistributor.cpp @@ -42,8 +42,6 @@ void ObjectDistributor::init() void ObjectDistributor::distributeLimitedObjects() { - //FIXME: Must be called after TerrainPainter::process() - ObjectInfo oi; auto zones = map.getZones(); @@ -77,6 +75,8 @@ void ObjectDistributor::distributeLimitedObjects() auto rmgInfo = handler->getRMGInfo(); + // FIXME: Random order of distribution + RandomGeneratorUtil::randomShuffle(matchingZones, zone.getRand()); for (auto& zone : matchingZones) { oi.generateObject = [primaryID, secondaryID]() -> CGObjectInstance * From c71b89ac02f02477d346a160f59cbb4573c2b9a4 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 7 Dec 2023 17:49:32 +0100 Subject: [PATCH 1240/1248] Update launcher translations --- launcher/translation/polish.ts | 77 ++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/launcher/translation/polish.ts b/launcher/translation/polish.ts index 4116e62f7..85db315f1 100644 --- a/launcher/translation/polish.ts +++ b/launcher/translation/polish.ts @@ -121,7 +121,7 @@ Maps - + Mapy @@ -180,7 +180,7 @@ Compatibility - Kompatybilność + Kompatybilność @@ -319,7 +319,7 @@ Size - + Rozmiar @@ -410,12 +410,12 @@ Downloading %s%. %p% (%v MB out of %m MB) finished - + Pobieranie %s%. %p% (%v MB z %m MB) ukończono Download failed - + Pobieranie nieudane @@ -424,30 +424,37 @@ Encountered errors: - + Nie udało się pobrać wszystkich plików. + +Napotkane błędy: + + Install successfully downloaded? - + + +Zainstalować pomyślnie pobrane? Installing mod %1 - + Instalowanie modyfikacji %1 Operation failed - + Operacja nieudana Encountered errors: - + Napotkane błędy: + @@ -598,7 +605,7 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane Reserved screen area - + Zarezerwowany obszar ekranu @@ -649,7 +656,7 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane VSync - + Synchronizacja pionowa (VSync) @@ -703,27 +710,27 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane Form - + Okno Users in lobby - + Gracze w lobby Global chat - + Globalny czat type you message - + napisz wiadomość send - + wyślij @@ -926,87 +933,87 @@ Heroes III: HD Edition nie jest obecnie wspierane! Czech - + Czeski Chinese - + Chiński English - + Angielski Finnish - + Fiński French - + Francuski German - + Niemiecki Hungarian - + Węgierski Italian - + Włoski Korean - + Koreański Polish - + Polski Portuguese - + Portugalski Russian - + Rosyjski Spanish - + Hiszpański Swedish - + Szwedzki Turkish - + Turecki Ukrainian - + Ukraiński Vietnamese - + Wietnamski @@ -1026,7 +1033,7 @@ Heroes III: HD Edition nie jest obecnie wspierane! Auto (%1) - + From 68e43b0622571f2b3f404fc2d1a6b1193a9fedb6 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Thu, 7 Dec 2023 19:37:33 +0100 Subject: [PATCH 1241/1248] Update turn settings translations --- Mods/vcmi/config/vcmi/polish.json | 36 +++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index c18f9239c..d659a0e58 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -236,17 +236,20 @@ "vcmi.optionsTab.turnOptions.hover" : "Ustawienia tur", "vcmi.optionsTab.turnOptions.help" : "Ustaw limity czasu (timery) oraz tury równoczesne", + "vcmi.optionsTab.selectPreset" : "Szablonowe ustawienie", "vcmi.optionsTab.chessFieldBase.hover" : "Timer startowy", "vcmi.optionsTab.chessFieldTurn.hover" : "Timer tury", "vcmi.optionsTab.chessFieldBattle.hover" : "Timer bitwy", - "vcmi.optionsTab.chessFieldCreature.hover" : "Timer jednostki", + "vcmi.optionsTab.chessFieldUnit.hover" : "Timer jednostki", "vcmi.optionsTab.chessFieldBase.help" : "Używany gdy {Timer tury} osiągnie 0. Ustawiany raz przy starcie gry. Gdy osiągnie 0 tura się kończy, a trwająca bitwa zostanie przegrana.", - "vcmi.optionsTab.chessFieldTurn.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Nadwyżka czasu dodaje się do {Timera startowego} pod koniec tury.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Nadwyżka czasu dodaje się do {Timera startowego} pod koniec tury.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Używany poza bitwą lub gdy {Timer bitwy} się wyczerpie. Odnawia się co turę. Niewykorzystany czas zostaje utracony.", "vcmi.optionsTab.chessFieldBattle.help" : "Używany w bitwach z graczem AI lub gdy {Timer jednostki} się wyczerpie w bitwie pomiędzy graczami. Odnawia się przy starcie każdej bitwy.", - "vcmi.optionsTab.chessFieldCreature.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy graczami. Resetuje się przy rozpoczęciu akcji jednostką.", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Nadwyżka czasu dodaje się do {Timera bitwy}", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Resetuje się przy rozpoczęciu akcji jednostką.", - "vcmi.optionsTab.simturns" : "Tury równoczesne / symultaniczne", + "vcmi.optionsTab.simturnsTitle" : "Tury równoczesne / symultaniczne", "vcmi.optionsTab.simturnsMin.hover" : "Co najmniej przez", "vcmi.optionsTab.simturnsMax.hover" : "Maks. przez", "vcmi.optionsTab.simturnsAI.hover" : "(Eksperymentalne) Równoczesne tury graczy AI", @@ -254,6 +257,31 @@ "vcmi.optionsTab.simturnsMax.help" : "Graj równocześnie przez określoną liczbę dni lub do momentu napotkania innego gracza.", "vcmi.optionsTab.simturnsAI.help" : "{Równoczesne tury graczy AI}\nOpcja eksperymentalna. Pozwala graczom sterowanym przez komputer wykonywać akcje w tym samym czasie co gracz ludzki gdy jednoczesne tury są włączone.", + "vcmi.optionsTab.turnTime.select" : "Predefiniowane schematy zegarów", + "vcmi.optionsTab.turnTime.unlimited" : "Nieograniczony czas tury", + "vcmi.optionsTab.turnTime.classic.1" : "Klasyczny: 1 minuta", + "vcmi.optionsTab.turnTime.classic.2" : "Klasyczny: 2 minuty", + "vcmi.optionsTab.turnTime.classic.5" : "Klasyczny: 5 minut", + "vcmi.optionsTab.turnTime.classic.10" : "Klasyczny: 10 minut", + "vcmi.optionsTab.turnTime.classic.20" : "Klasyczny: 20 minut", + "vcmi.optionsTab.turnTime.classic.30" : "Klasyczny: 30 minut", + "vcmi.optionsTab.turnTime.chess.20" : "Szach: 20:00 + 10:00 + 02:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Szach: 16:00 + 08:00 + 01:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Szach: 08:00 + 04:00 + 01:00 + 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Szach: 04:00 + 02:00 + 00:30 + 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Szach: 02:00 + 01:00 + 00:15 + 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Szach: 01:00 + 01:00 + 00:00 + 00:00", + + "vcmi.optionsTab.simturns.select" : "Predefiniowane schematy tur sym.", + "vcmi.optionsTab.simturns.none" : "Brak tur symultanicznych / równocz.", + "vcmi.optionsTab.simturns.tillContactMax" : "Tury sym.: Do kontaktu", + "vcmi.optionsTab.simturns.tillContact1" : "Tury sym.: 1 tydz., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.tillContact2" : "Tury sym.: 2 tyg., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.tillContact4" : "Tury sym.: 1 mies., przerw. przy kontakcie", + "vcmi.optionsTab.simturns.blocked1" : "Tury sym.: 1 tydz., kontakt zablokowany", + "vcmi.optionsTab.simturns.blocked2" : "Tury sym.: 2 tyg., kontakt zablokowany", + "vcmi.optionsTab.simturns.blocked4" : "Tury sym.: 1 mies., kontakt zablokowany", + // Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language // Using this information, VCMI will automatically select correct plural form for every possible amount "vcmi.optionsTab.simturns.days.0" : " %d dni", From 500f94522213687848f632379768cd4fad0c8e21 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 22:48:51 +0200 Subject: [PATCH 1242/1248] Update lib/mapObjectConstructors/AObjectTypeHandler.cpp --- lib/mapObjectConstructors/AObjectTypeHandler.cpp | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/lib/mapObjectConstructors/AObjectTypeHandler.cpp b/lib/mapObjectConstructors/AObjectTypeHandler.cpp index 99af1e610..1d915d5f8 100644 --- a/lib/mapObjectConstructors/AObjectTypeHandler.cpp +++ b/lib/mapObjectConstructors/AObjectTypeHandler.cpp @@ -97,16 +97,8 @@ void AObjectTypeHandler::init(const JsonNode & input) aiValue = static_cast>(input["aiValue"].Integer()); // TODO: Define properties, move them to actual object instance - if(input["blockVisit"].isNull()) - blockVisit = false; - else - blockVisit = input["blockVisit"].Bool(); - - if(input["removable"].isNull()) - removable = false; - else - removable = input["removable"].Bool(); - + blockVisit = input["blockVisit"].Bool(); + removable = input["removable"].Bool(); battlefield = BattleField::NONE; From b4b81289d8505ce5a918682f8d39035a74e9aff6 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 16:48:19 +0200 Subject: [PATCH 1243/1248] Update Ukrainian localization --- Mods/vcmi/config/vcmi/ukrainian.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index f2add0ec2..6c57e0d19 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -112,6 +112,8 @@ "vcmi.systemOptions.hapticFeedbackButton.help" : "{Тактильний відгук}\n\nВикористовувати вібрацію при використанні сенсорного екрану", "vcmi.systemOptions.enableUiEnhancementsButton.hover" : "Розширення інтерфейсу", "vcmi.systemOptions.enableUiEnhancementsButton.help" : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.", + "vcmi.systemOptions.enableLargeSpellbookButton.hover" : "Велика книга заклять", + "vcmi.systemOptions.enableLargeSpellbookButton.help" : "{Велика книга заклять}\n\nВмикає більшу книгу заклять, яка вміщує більше заклять на сторінці. Якщо цей параметр увімкнено, анімація зміни сторінок книги заклять не буде відображатися.", "vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна", "vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу", @@ -175,6 +177,15 @@ "vcmi.battleResultsWindow.applyResultsLabel" : "Прийняти результат бою", + "vcmi.tutorialWindow.title" : "Використання Сенсорного Екрану", + "vcmi.tutorialWindow.decription.RightClick" : "Торкніться і утримуйте елемент, на якому ви хочете натиснути правою кнопкою миші. Торкніться вільної області, щоб закрити.", + "vcmi.tutorialWindow.decription.MapPanning" : "Торкніться і перетягніть одним пальцем, щоб перемістити мапу.", + "vcmi.tutorialWindow.decription.MapZooming" : "Торкніться двома пальцями, щоб змінити масштаб мапи.", + "vcmi.tutorialWindow.decription.RadialWheel" : "Проводячи пальцем, ви відкриваєте радіальне колесо для різних дій, таких як управління істотами/героями та порядком міст/героїв.", + "vcmi.tutorialWindow.decription.BattleDirection" : "Для того, щоб атакувати з певного напрямку, проведіть пальцем у напрямку, звідки буде здійснено атаку.", + "vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Атаку можна скасувати, відвівши палець достатньо далеко.", + "vcmi.tutorialWindow.decription.AbortSpell" : "Щоб скасувати заклинання, торкніться і утримуйте палець.", + "vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Показувати доступних істот", "vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Показувати доступних істот}\n\n Показує істот, яких можна придбати, замість їхнього приросту у зведенні по місту (нижній лівий кут).", "vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Показувати приріст істот", From 2a1ee113aad1611939e1261cf51195b18625f1ec Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 17:01:52 +0200 Subject: [PATCH 1244/1248] Updated changelog --- ChangeLog.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index a4bc35f70..0843d2596 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -102,6 +102,8 @@ * Spell scrolls in hero inventory now show icon of contained spell * Fixed incorrect hero morale tooltip after visiting adventure map objects * Fixed incorrect information for skills in hero exchange window +* Confirmation button will now be disabled on automatic server connect dialog +* Attempting to recruit creature in town with no free slots in garrisons will now correctly show error message ### Main Menu * Implemented window for quick selection of starting hero, town and bonus @@ -147,6 +149,12 @@ * Creature that attacks while standing in moat will now correctly receive moat damage * Player resources are now limited to 1 000 000 000 to prevent overflow * It is no longer possible to escape from town without fort +* Pathfinder will no longer make U-turns when moving onto visitable objects while flying +* Pathfinder will no longer make paths that go over teleporters without actually using them +* Game will now correctly update guard status of tiles that are guarded by multiple wandering monsters +* Moving onto Garrisons and Border Guards entrance tiles that are guarded by wandering monsters will now correctly trigger battle +* It is no longer possible to build second boat in shipyard when shipyard should be blocked by boat with hero +* Gundula is now Offense specialist and not Sorcery, as in H3 ### Random Maps Generator * Increased tolerance for placement of Subterranean Gates From 07dd958f3c96fae19bb9e2533d20647a2fbe0aed Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 17:02:07 +0200 Subject: [PATCH 1245/1248] Updated 1.4.0 release date --- debian/changelog | 2 +- launcher/eu.vcmi.VCMI.metainfo.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/debian/changelog b/debian/changelog index 7c5b0b41f..0c7935b15 100644 --- a/debian/changelog +++ b/debian/changelog @@ -2,7 +2,7 @@ vcmi (1.4.0) jammy; urgency=medium * New upstream release - -- Ivan Savenko Fri, 22 Dec 2023 16:00:00 +0200 + -- Ivan Savenko Fri, 8 Dec 2023 16:00:00 +0200 vcmi (1.3.2) jammy; urgency=medium diff --git a/launcher/eu.vcmi.VCMI.metainfo.xml b/launcher/eu.vcmi.VCMI.metainfo.xml index 1ef4baf13..26ef16ef5 100644 --- a/launcher/eu.vcmi.VCMI.metainfo.xml +++ b/launcher/eu.vcmi.VCMI.metainfo.xml @@ -68,7 +68,7 @@ StrategyGame - + From e30de6c4914a622a528fb6c8ae1337182998658a Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Thu, 7 Dec 2023 17:02:33 +0200 Subject: [PATCH 1246/1248] Replaced 1.3 download counters with 1.4 counters --- docs/Readme.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/Readme.md b/docs/Readme.md index 3c630514b..6902f1bf6 100644 --- a/docs/Readme.md +++ b/docs/Readme.md @@ -1,7 +1,5 @@ [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.1) -[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.2) +[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.0) [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases) # VCMI Project From ddf741423c44925f1b9b4d40d84a2ccd90d3c89c Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 8 Dec 2023 12:02:12 +0200 Subject: [PATCH 1247/1248] Updated translations --- Mods/vcmi/config/vcmi/english.json | 2 ++ Mods/vcmi/config/vcmi/german.json | 37 ++++++++++++++++++++++++++---- Mods/vcmi/config/vcmi/polish.json | 2 ++ 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index 655fd7320..242099ee7 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -249,6 +249,8 @@ "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.", "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost", + "vcmi.optionsTab.accumulate" : "Accumulate", + "vcmi.optionsTab.simturnsTitle" : "Simultaneous turns", "vcmi.optionsTab.simturnsMin.hover" : "At least for", "vcmi.optionsTab.simturnsMax.hover" : "At most for", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index d22c45af5..07d13f06d 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -235,16 +235,43 @@ "vcmi.randomMapTab.widgets.roadTypesLabel" : "Straßentypen", "vcmi.optionsTab.turnOptions.hover" : "Spielzug-Optionen", - "vcmi.optionsTab.turnOptions.help" : "Ändere Optionen zu Spielzug-Timer und simultanen Zügen", + "vcmi.optionsTab.turnOptions.help" : "Optionen zu Spielzug-Timer und simultanen Zügen", "vcmi.optionsTab.chessFieldBase.hover" : "Basis-Timer", "vcmi.optionsTab.chessFieldTurn.hover" : "Spielzug-Timer", "vcmi.optionsTab.chessFieldBattle.hover" : "Kampf-Timer", "vcmi.optionsTab.chessFieldUnit.hover" : "Einheiten-Timer", - "vcmi.optionsTab.chessFieldBase.help" : "Wird verwendet, wenn {Spielzug-Timer} 0 erreicht. Wird einmal zu Beginn des Spiels gesetzt. Bei Erreichen von Null wird der aktuelle Spielzug beendet. Jeder laufende Kampf endet mit einem Verlust.", - "vcmi.optionsTab.chessFieldTurn.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", - "vcmi.optionsTab.chessFieldBattle.help" : "Wird in Kämpfen mit der KI oder im PvP-Kampf verwendet, wenn {Einheiten-Timer} abläuft. Wird zu Beginn eines jeden Kampfes zurückgesetzt.", - "vcmi.optionsTab.chessFieldUnit.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn der Runde jeder Einheit zurückgesetzt.", + "vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Reste werden am Ende der Runde zum {Basis-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldTurnDiscard.help" : "Wird außerhalb des Kampfes verwendet oder wenn der {Kampf-Timer} abgelaufen ist. Wird jede Runde zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Der Rest wird am Ende des Zuges der Einheit zum {Kampf-Timer} hinzugefügt.", + "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Wird bei der Auswahl der Einheitenaktion im PvP-Kampf verwendet. Wird zu Beginn des Zuges jeder Einheit zurückgesetzt. Jede nicht verbrauchte Zeit ist verloren", + + "vcmi.optionsTab.accumulate" : "Akkumulieren", + + "vcmi.optionsTab.turnTime.select" : "Spielzug-Timer-Voreinst. wählen", + "vcmi.optionsTab.turnTime.unlimited" : "Unbegrenzter Spielzug-Timer", + "vcmi.optionsTab.turnTime.classic.1" : "Klassischer Timer: 1 Minute", + "vcmi.optionsTab.turnTime.classic.2" : "Klassischer Timer: 2 Minuten", + "vcmi.optionsTab.turnTime.classic.5" : "Klassischer Timer: 5 Minuten", + "vcmi.optionsTab.turnTime.classic.10" : "Klassischer Timer: 10 Minuten", + "vcmi.optionsTab.turnTime.classic.20" : "Klassischer Timer: 20 Minuten", + "vcmi.optionsTab.turnTime.classic.30" : "Klassischer Timer: 30 Minuten", + "vcmi.optionsTab.turnTime.chess.20" : "Schach: 20:00 10:00 02:00 00:00", + "vcmi.optionsTab.turnTime.chess.16" : "Schach: 16:00 08:00 01:30 00:00", + "vcmi.optionsTab.turnTime.chess.8" : "Schach: 08:00 04:00 01:00 00:00", + "vcmi.optionsTab.turnTime.chess.4" : "Schach: 04:00 02:00 00:30 00:00", + "vcmi.optionsTab.turnTime.chess.2" : "Schach: 02:00 01:00 00:15 00:00", + "vcmi.optionsTab.turnTime.chess.1" : "Schach: 01:00 01:00 00:00 00:00", + + "vcmi.optionsTab.simturns.select" : "Voreinst. für simultane Züge wählen", + "vcmi.optionsTab.simturns.none" : "Keine simultanen Züge", + "vcmi.optionsTab.simturns.tillContactMax" : "Simzüge: Bis zum Kontakt", + "vcmi.optionsTab.simturns.tillContact1" : "Simzüge: 1 Woche, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact2" : "Simzüge: 2 Wochen, Stop bei Kontakt", + "vcmi.optionsTab.simturns.tillContact4" : "Simzüge: 1 Monat, Stop bei Kontakt", + "vcmi.optionsTab.simturns.blocked1" : "Simzüge: 1 Woche, Kontakte block.", + "vcmi.optionsTab.simturns.blocked2" : "Simzüge: 2 Wochen, Kontakte block.", + "vcmi.optionsTab.simturns.blocked4" : "Simzüge: 1 Monat, Kontakte block.", "vcmi.optionsTab.simturnsTitle" : "Simultane Züge", "vcmi.optionsTab.simturnsMin.hover" : "Zumindest für", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index d659a0e58..1028d8349 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -249,6 +249,8 @@ "vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Nadwyżka czasu dodaje się do {Timera bitwy}", "vcmi.optionsTab.chessFieldUnitDiscard.help" : "Używany podczas oczekiwania na podjęcie akcji jednostką w bitwie pomiędzy ludźmi. Resetuje się przy rozpoczęciu akcji jednostką.", + "vcmi.optionsTab.accumulate" : "Akumuluj", + "vcmi.optionsTab.simturnsTitle" : "Tury równoczesne / symultaniczne", "vcmi.optionsTab.simturnsMin.hover" : "Co najmniej przez", "vcmi.optionsTab.simturnsMax.hover" : "Maks. przez", From 5922445cbfbec00c47ef2232dee9f40bf8aa2489 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Fri, 8 Dec 2023 12:05:03 +0200 Subject: [PATCH 1248/1248] Update changelog --- ChangeLog.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 0843d2596..a9f6efe83 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -160,8 +160,9 @@ * Increased tolerance for placement of Subterranean Gates * Game will now select random object template out of available options instead of picking first one * It is no longer possible to create map with a single team +* Game will no longer route roads through non-removable treasure objects, such as Corpse +* Fixed placement of treasure piles with non-removable objects, such as Corpse * Fixed interface no displaying correct random map settings in some cases -* Fixed game failing to generate random map if number of AI players is set to non-zero * Fixed misleading error "no info for player X found" * Fixed bug leading to AI players defeated on day one.

9O!ZKFbY{fE|vC6g*Z!q8RA4a?X87;Dc?5#+Dm+|9lT*<@rvzEvS8`Z~?U2}kR zVkY4%^N{>ly*A;{1nGes0ttKt7C{;jhE7l14+-+DS4El^VAPjFTMM#j_XqmXsK8M2Nnb&Ryxp z#(2g=Um1L^h7d|LB7|b+`@ZBa-aVD9o*rwkFJibwA^}2D66MyczAS9qTulHa9oD&JW9sS)1v$nmYxT zJmw)wX}_hg!;S%Wa`!12uA_Y@8120kMLt4lHTzuHU9|Pc(!x3kK)-!b{PFanuZJKB zjdFX4CRVHi>LeZq^-rC8ToOQwU5POHbGLi;?wDJ^<=f(PCYHqT;vAG9$MCQD8BZP7 z5H#&U`Y(iwc$}{J=qpq+Pn>%Z-9e_ie@pYori@v(yFYzjinhvyOs>Zde1mjncYEVp zY&y$W@3XMiJtP#CF8v+@{pVq?WHNLx_Lh78{sB8I^I0?N+u$ovwZ#4gCMmw^`=s7fh`XDu zaurUrKwM(6VHjQ2S7CF?YO@SB^M`p!DoP9WhFfWC2CZUj2R!C!YdYl>|6t9tl&%R{ zFmY&kAt5mQ?99V|a$ z>G3J$M1~6?;Et-(fQ<*M$ATIB208F>%Wr=rq!A^Qe7{%%721S$KyQ-r&mf1wB{8!d)o8sC*KNXG^jex84{?@j(Ht*xX zyLkfUKS8aNRHL$eOit6aYui7@wo7^uXQ_+@|FmV$?0r9ayy>-VN6X{RJ7CV)r4wF% zz%yx}luP*FR^1fI5+z&2eSe$$yx&cewV$6Xx%4x#r-~Td{5+qIyfyqy$FRSa0I0c~ z`8?2{jHap!xk+wG7C#v2Ybx6=fry_BmQYB}Ph8Y)z%|8HydpNmJw1o}V1{jbx9=oP zo+gyl_jPit8uWKcO*>*cadR&&vF|&cf=Phg6!0iieU_bW`I}VqT{`Q~Huh52w7Hmb zab+LG2qSy2F7*qNv?)N;EadWBsu_Vw zDp-d65XohCinoWY52x_fPNFO{ph%V*ZGo0*5_2&x+4dLC*pbYX`tjBYkR0b6wZ_Cb zNT^RLR_cwGU~Cn$Mr?Ig=EVlxKTyI3=fun1swEH>-kMU@{A0E-TZVp2g00mcw4obW zX>2w)w)vF{HYhrmV$`9&&=b2m;XH&rcY8?F5FKKOU z2kvaDW!MOlaq&<1s&W7RLk9aJb>#qqi4CqBB~N}nJvrz5jn(p)pc~7J;&Nx$R)yi( zMEz#EFSwM}i8(S5N>_&fo@3snYfSIHZ25^g7f+rR78V1>X~m4V8kph)d3NcZ@Uc(+ zAmob;aL17eizDvH{t$CeF_BrpUaHNEr-Bfzs($ijzrh3%!y2m`xFE-lyrnnzqzC_Bh<>1_v_(}*`pjIM@?X9{W(A64KS#UDdvaeSsK47)v z1ywDlfoqY>rl?|^K!EpH4E1aPI(@}s$&i%tQU)J3Nc)n{IY@m2Y#l>(Ut`Z1WY7wo zO`_3=7rfp^th#n^J*Su$_}!Um;wkN2m-~NfKon1aur0kv~i_| z1|okosC7h%2!4f;rXxz@rst?g@uHW4Vi3+Z-sn_X4>>TTu#`6nu5)r zA>T=o%glU#KH=9nSy$O9-rD9rYd8*IXuB7b?NnpZA?TbpcKR3QqD7w~_?9pnO`ePW28 zk&Xx%v&=9P4C^qhEre@+OP-Zx8xgQQKiYMewT6(UM zb#_jSS(tlI7x*AfZn05e%4I9l7WE!!ufVUxR4IIv!vrTnN|yev(ByO@%9krV+nPpX zrzW}oGYJ3WQ~DKuvKa(Y%?&=zhyvr5*}FXMc55&p`tf-TLdwY&i9I#yQ3gGWLOXz( zqsN&&CVvW`Lh?aUjqm=4yU>!FK~bDc1~isd$f2$xuX|iZf_7?#$zbVX(5fEoFcaL} zmuN7R2%dGe&rT!^?}l96BNsZ5vpvcaYss#tiM-T&msiQ66>JMR5$=&lKTxx;2j@-~=qy_+QS~o{#+^<%*wRBRR2F+-~;2 zD$EB1m8O1IJ(zwxJfRR_hFDmb|NKy==KuWe8OI2ZP0WfV9|hxT1*mV8M9K;^Pyn2- zv~mq18^*0aSZ8{VLD290Y^NozVFHb}Fu~D1bR{eU4w64sD)&Xx4I1x#Nn(p#?;b(K zu$bkeK02*#uNER9wq3{cFPYB%lxF3X>L?sNZnY|iKn4*?jMQvHj3a)Z@pDCga)gj7 zjzB0_e~DfTJRHF?t`R0MO6|lHj-;X?2^J}zED%$j^;*~-=Cykl%NZo6Y}jH*U=G#P zL9^2~4V58ddex(>SUbLHn-`3j*40H^p6P)+AYWlWitg+R+dBoM%;PvaA*^4JYTF*l z`|n4=gR4f`#hf$&M4RriMcDe|T<+P#5#<0Aa3k&{y-Y6JtJ~~jM83ugm#Tp;Dm+A~ zAY1=;_vP0*<{xZezDQ^1<&k#ypoz_9PkfGo=y>fn$8*=Y&1)9hFx<*k$gPB#)j~oy7f+L%k zYb*zrMvDqz;uM~Jtx;Ota#}u1(g(7C2xV4(ke$tM+EKXaO|7lg>pu@4b}aDfYn3aM zAQB& z${Ah%CzMLNl;|Wsk~(ZWPcc_TyB68_Kyrj0`W@jsArn^DGXu#N6QC1BA%699_jQZ+ z1?#@kDnct)t~Uf;35E0X`qRxJuO9bq2~l{qtHXv=W34b$%N;@yHtrln+H^1-zZb#B z{N3;H22W?m*RlI?A-Jf$lcFxYGCH?g`pkT?CpvnjwC2i%E^*D8FHZPMgqRVZ&Rw zl(e=!vFf_V@+m66WbVf4vP^FDR#yzPamP(Gip70boOh)EZe1(^JnGjheOJOv`t)1} zHbpOpWge4~XtpUBvK_carNR*{K_I(77lC#p(r65P=D9f#o&_cwHgysTJeDDqr}Wvw zA3itRCayod4W0lF$7SzKa1nG`gK@?eI&m>aycJj*UqKMa##)hC4&K>0Nn&zzGoTC z(9Ot|$$-66#tYmKJANH4fm4n#_tByXK&%t($;{9_y0PG{oi2QcGBk%XHkPhbQMpY(te~tGfES3CLwua+-Gl z00F2fKGOd$qv1<~#}h|ieQTP#(Jm#`JapdWeSq~E79!zzK%@ku^58@&uz790fOMW$ zz(IkiYBFF)%fSEW6ZtSX+(32DWhSap)~XLc`!&`Z0a9fkYNK8s zO~OQ&mYKHCZ&TE&OH{ZdM(ax6@?DpghtN>38Y~u0C0hh|o#T)G9ijDLtUE08OmQmi z<{sipTP!rFHi!#QHO#!5mH6=L6R`>)W+rtsN9x^EeQ{3*H&p+`&|t)o!^`O@AXlC-VjoGU`c#)I z%w35WeMT^FENXO40Id5A-(E~6-Uff;JnxtcWy94(s0Ol8zG(H<%619_z0nYK_eq*Y zgcm`s{B>unWM&e_WcDa$Y^0ruu@EOP*YFIVq^p_lIoYEg*w9?TRmwm_Si8ya3JJrwWf=YFH^5avwjY=-qC=*sTz!?+e>x5A| zf-_=u_tE>iqoebOU*yh*>4ayb!ud({Y_3m*H*qul!Q3@U}ML z=l`1~^Lu`}hEO<@B-$wO%@TVg?*0g64x~E|&ov-`?b~0W+-^_%e19Y1U&?n?B=X(K zV;~>vb_zh}fG%r_LI>Y9^zm$ILG3?leRgcL^qXwk$i0Ogq4u1~^o^_(xv^p(=aNh8 z=I5TmC8Tt2(&gq_vQz)-c4aB2_nUP^Ws+$VwJU~`p}fDGPyT!An??1T+w)gbXnsYW#{d zQwM`}tA7wsS#UATT-H0$;MFt+@TG1j&p6S4%XQ$gBlDjKp0JbccfQ=l6PM5y5PflU z^7EM}FGXt8^Fr?)mQayF>eb4%C;O^5@_V#yx#7!SJQF>z9eQT(=9e(*Is&~_KSGkM zWq5m05?doFktUHqQgBL*)lsUYzONpdTA8mmSf1i#y9S2vj6s`@vQ~fFL`-{VGH9Qu zkG8%VK#HgjML2h)g6>HZ|GM^2_1L;eJ17VY{};at5i^mv!)R>42iq8!xcv57r+xmI z8(2R7Bl!BG1&>s&JjDh!oTQxkuVVTQBN7_Qmz8R5hdnSWeizc4R(3k;q<$CYNAut` zWuKG-Bl=_jkC$x6;=pr;I)SqJcs2~B`*@to2WlS*oCV;hHdATk!NJ>A zsA*vX+=YKddb&2Vs|q4;5qyYd;skut1g{Nzwv6x&dS#WX>krb2*Q`M$?K+xW9N7G42ttW5Wg_|NuR*D5zX)BOo<|00uOn#EFp zHdE#T24BY3l5W4UK;6K!2ROb58*@YXwC$wVawBoPTyG7u0u|uypSz!hkLp)309-%> zlv6JwOZq$w%-%paPviXvK8<$(5}<0eD5GY`{X%!i}*hp#0#O-Gek?x%I_gv~;|r4Iz>OV^yeZ}jv%y$6I8 z0vaRW;8kxpLihq6l@NDW6_GKxnIz^AT)j=eaB?M5Jv>cKPC2#G#v!Hh)i>S&Xw#ls z$f$rTv8f?_8c0e_4Ye^r>d+dYTYy*tNSxZLf7Uf%xeTPm0wO`rX108+fh^ zl$J-8<6dnEj=7WhR-ef_14?>2&W`7IK6AL*d&eF?1l-PzR)5q6 zsfGc*-#^@?Uq~4}^{iI$Q!?W6Un4g_uwKd&`A12qZ!UJz`L6xdp9(D!Aoj0U?BJa=nzK^@8Yr5o zxz=WqdCV>2L-CypinH@ETN=|yjtZ3xFDeUS*V29Aq?E0A9YeJ~dBhqq0JK&2)Z33) zn+(~ruB36;+)ZZuUBS`KHtzuW5S1~s3xEEmb0+|ul($f8*}b&*o9tr^G#jfh>?ng0MdxLayI#Wnx<27KuPQ0acH>1|IhYEEU zJl5H_%XJspHG-xC7FpY#t2|)}ii?m!%K~sSX3f*UtdY#8VzhejIAA))m-K5UD;V;RkVT{XDHo}TM3>Wpl z44-=R%k#wsH1EnU^;~Lhk>_=K{Y}wvDDxj;T)IDxbHlwiIinF_P>w0ehW?W3ycit~ zqhNcRvbqZ##y4thkFER?iF$fjoY2CCFM*FDN{}W=DeQTvufHGdsYMe2-$Gl!;fppI ze<2Hl(BZdrqI(^oL6(6lOM|WAt{i~;Z2Y}rNWyJ}4m{4$|Ih#M zUrVL^fFIj6*lGSTLm*MZXqLE9uUebA*Y`B2ViojH@|q8gQ}9C{T2j{81F5Gk$^e0O zVTTN6>e^%*t~-gt!c&7cP`Ws2x>mQD3{tv72l)uobh&k_O-52doqEqrG)+}Vp8A3@ z5;^qLSI7BGkRYub5#%!PnIH{)A$$#7|4Zj9MAtK`{q(pq$8-nzo$K7m zLe-(yZjw3-{$irRLJ1hWU`i}nt*^uPcg6Ss9O-tiv&Yse^t3`R?+6f{b15lU^|IlY z3RgnN-AoYb+x2-@zax8z8twjIRhFAYXZS>?Lt^%&G&CT!FIpWCarnz?1{Oe+2vkCDR z6p>Huf4WeAU<_=5_0EW_BGahpOQ`97DlRRpk%pJ`oi0HL15>TS9_T0)ix@P*p$&?C zNL5>k?0b%X4a30TUSY`fkD1b1c9nv`d!n>9ge1$%|5|J>?CywcFU~$13ukDTehK>@7A^rI-&x;-cSsQa- zI4@0Uc@mf4Riq4a9MBJ^`g>0WagBTgNobWdHbweWh_Z$$tUPicnrj>rVyD2+7vS=q zmXJ_>&MQx?+)35uaTOKnIj41l`-F<_WA{9BKInoeRHCfcYs~DNti8-qMR^wJv@uv{ zJaU@G>M<$mPg*J|rvJHzy%7m_Ju)q0X?B34%jFf}9YrmLqzTdUoVX$2M zN{N_p^?G{NUc|I?*ycWwLfW}jaV?RzcBS5gRY&YjHeRR{CqJq1-J>E#xuLq)r(wNA zL%N#i50&}|kCxWf%e|~c%yG`mze|{17H50~Ro;Bj#EqE!I*|ptf5v96NQ5hVLOVs= z?0lC6!4Kqx{wovTeEj+M_wU~~WS?LYvhWuI4QL4MSi@um0o2(n!{6Oq>)zLNTFY(s zv^!1Nx0SO>K*zUWxNNY>B~jDf70RT4j8A{fAkh(V)6Rc!RTQLbe~`a=mi_vk^FV1t`#w6e1^NXOnFLOF?Vp zyi~XfCH}}6=%L7Uxo{%FL^};YhZNEECd22TW$?N%f>hMh>64(Dl2Q^x)pX?STp9!8+6hiud5kx zLW@)v9(|d!XR!T;$O1t+HViL71>j{eRec z%dn{0wr_vU07Hj#g9u2MBA}Eg($YgpH`1aYHGp*I(1>)mGz{IMNLes62ugR%yST3V ze%|LFe9?`~FT$*I9mg;B6S!2zXL^QZzLZIV*#Mu>nS72Imonj`)>-p+uU6$f>JxMp|#nzn=W**~I#ng14IR^s%LD?ha~~n?HLQ+(lVa(nlLVT%QIJ zD2EVw<3(TQ_VFgrZ{9#7vZRhATd#XPZf955s!{9pV3(7|4%X8buWb#)_S9pS0<6H{8ffZb z-Eq>~&x#wPV_krfoxZ3C>>3c^Gry{H{|HzADweVA@=sORG;C8}7;1Y6PPR;_MT-tH z0&kt|SyQG#!RpT2oNzni?p2D^X1R#YqsPXH3Z+#fXjivbs6xXRFcwNvB9Q}{Ky+Q8 zy2Q5U$@XYk)zzNsWoTrHPSrPpOwkm_hlmF-uZ}iot0P5|Uxv*nRY>oEd)1^Z8IirM>5Tq{igY^bP zE@p}5{8WZD9TOa4fbPVew_o!AeWC#BfU-E_udX#5O71VGCl-|-o`gK#IX^$o_@Ny+ zMwMH1di&+Ot~bmh4UW<3A?^IqKo%}9B0hEN^9#xB+mp{?*ee@Ny9UO57C=3-Xh%Nj zw6uNB(~Y^XON&{1_U4^c_CWCU3;8umd%cltQ4LFx;r;j+iRX+;{VBefsHc%a%%Rpi zk6cfLMQv|^jhca+Tq>@$IKCDFZI8tN9F)pMs`?i>$tlSZE4GaTyo#MM+`MUZLAR4G zQw}p+lmTQ#?vmd z3?#tz>hSk5tNrt1{=*FPC{l2djMD%3ZQcdmM;zZ{gI55peE9hlrUY%dVv^K9dU{V7 zJ*=S(@a0Rybf<+_6VDQ0jS!n0G~AH|@3%snl9j@pd<%Y`-S4;cwhWR<2Q!S2WtqKL z;OyS`Ewg+-`^u|xkK;|jU)3@+pQ~YO#h^-}2OK1MQ~p;TdGZ(B?*@9IXhA4-5=(&6 zJiA%aSxSY>#N>`Wp4Cu-j{Yd=J!~P z*Y*j|?u_;%rr3qZReh_x4Gw`6)+wVqV8DaG&nW`*0R^f06Q_H5Z$3oK7?KiWELU%j zOas1l--Kff>Gkf&v))fFx88RJC84T3E(yC74Cw1H@|hVHGdv0LcO(NV|l5@56b)R7BM`F+l+leG2{4J9_s`Z~C0C?I0s zAO1-zR6`P;foX*mV_6p-@l+lX0&mMto_R|sK3gzMrH!r;8Pfu#5kpdaS6WnJunKI+ z{hbCxe=gWUx(Xmn0itede~On+?pzika6(n(e$-ltkO(wh>QvGp3jb+t!>#rRMQ-^X zv)Xs^edPG3pjF6YRK!SV_dkM({cvBJAoo8jZsW!G#bie%|8Es)l!fe|#0EO-97+1| z|DT`&KAEfMvN}MUdI=E+M`#h=Adh|yKO)%x&^HZKCk+4cyAtvj458gTZI`}Jp{{>0 zUX4k6xU~G8WJLk98cyxw8V8UXnOn-olB=Lo*Navc^}0q%KrfGvwj9a0!|p2vuXXyI zKbd2`#^yXQ^G!(YrkUWn?(df$aES#NDwW;_X*Ry``73=Tb+R=Iommxq)8$X4wklj} z8h~%TD#Z~NRY9a;C6I;f-xs?+(g~T8w)tpkVDp%)6dRTJ$MU%(om#tU^6-;~IrS%b z7Z1T+*HgH8#fbF>>lCXG7ls?!sTJ@y?Gc6kjQi3!`52g0Tu~3;)r__Aa&x#Apz(Kuj&>6Mj#wwz3cRpnS+86DA{`}eA*42e^ zEt$R|2ag{<7uDUnU$rvM$T62oY-%5R2lEVo`wxL}!@Pwz@?L9YyFXLTeor6#yI3fD z>SnGh{t`6F1Fbr_zfsoGcLe7-yKlN6xJJZDzN7T4yLkj;BNJ!rXFSemThlWAZNB9+ zkOB~`8(^9iE$A53uWPV6x^Z2fJi1~w18{WSrnmtCSb@Fy!lliB^b!kA_jJBSSiFev zDc3L$63np-_x;uW`cIIUclGWmPC9MOoLx?-dd%?ul1s)qInTb?tr*}K*#Ac_k-GM4 zo?rr`eU2H|ZEZvA1izeq1WsIkK&RB5#Dr{(Q0ut?pT;pyj{NexECaZH=9AIz5dEf$ zvYHTL7<&MYF+BD*ceq6RN}t(^y7NkH?WknW-^|8>zKpE8qtustF9rS`Wa`_;ZIyki z8)a;N0J7L`uCX^h{pR>yqRGKDJ?6tgCIMYhVV!KRg8X(o(3ncr+2YL%%DR9{0{DRsOj5K zj9=ao*z_Jb#EK6X{i@&`t*(8uOe{Q#OAJDx4{D-Z$$@+uoX;Ea_i5(-5VP4Ll=c6A zm64(wWkhYw@;}N*8%ZSv3=~M@@)6clt`Wj0i{Mdmvs+ED=KGMSR88MFP!hpP z(1Js2#sH)Q!Mdc3S0R7cv~ zuw+UQ+*=}wAQFRL2J$!pm3XJ4ELVjefem3klf-lF6w|)PT<=87Zc;lmgPe{VedK_ihxcW8Bg@g8es0~l`oJuhy6S_^&D9zDT(-Tv zkPgL;!q5nsejqvw6!{$R^9Qs*6gBq|Kxevi`C*R0khw<48GSFcP1$AeUZ$yV_h64# z=8X)_#FqTlRU*fEDnrj#nqM4>UDI_BMfLzGb2pB_AO1~x2Sqa>)M*X*?6=R@ zCf3O2JY^>l5up(BwF%Am`AkXCl}V=+uFd^VhfVU42^jnwKTk^kK*np#S_+;-+0VVUNY2hwR^GKGnwT#g(zwHn97H9$C3oOFBQ%V`GWa!k}_u z8;fJ$7ah9}t7RP8v*G0Zay!2wcF~KX{n)@XI5p*UJNGHBYkRuMvmq-mPVxI#>Xhpb z(Ej#}%@;8@%Hb|B-67iACUo~fSJ!D10sol)rrT#9!N&Sid$ALK?=>#k2v{JD0NJIT7%-F` z76pEB$9L*Ht_fXYEst-c#E!mc+M-9@~Z%b`X9G8GU zP|&s2MOTy#rI({hbymWYpzrlkK-q)c3}S--KPa-mHRU5*B>pb)e{_#YXu{ALIrlwR z11x=lf9ku#4mmvo2ZPTE!0QgM1hRQ@hy_NAAFhZDQ$5gvlN-|?giE07l+Eu6lpBnQ z{wfwRIBNOp!%(O_5n`IU26(-#PuY}FbLUT6=k>>cHx_d`R&#LDy{xzTtgIn=0;A%yg1jjPI{#XHF9c6~D@cR4kK0$AgM z@gZEM6*slHPM@YwASnt#3m7*3l87Tl8-GLnm%1nLFLhzugcWjqxjF`o-j9g9X@Epd zfDMoU5q`ETPo~x?3j3D36Lq9s`Om_N3_W&4$J)zPmx}i-W+i;q!M`~kWDGIftF9kA zPv8tK|MOKa8KV7P-NQ#TVx(xzefxM>sQ=Nx|HvMo=n=e~#Hw;*0%Ko2NUW}VUTE!R zaLwcVTE$>op3d6EAUQ&Kp|GN%HbZAYkKHH;&3mhZkXmfHP%{bfjf;fvdN|Xu;DCVN}j6_n^b1* zBZB^Jpn(_POC5NNTpAHl7_&bJzN>*|ELnkPBB#`9dn$`DpDBTh$*ckSJ108LvoUF$ zH>`yhZxs;kLXDhG5pnqb)DMtgbVZi{-e6{*hPj76)D$;+{MIAWGh@%UnrlS*) za?~M2*UacT@*4J3Vu5uC^@w&AfP4r#ygc>7uJ})G+)Cb;4HbOpEBpWYCOjzXnV!M! zLg7D;Qr*Y@Iua&3-tlvwS~VQ$kb^&)(7|asH?&y&r#3RifPozZ;QN_&5Rd@GbK9@B zpAL%iqlhfw+%0SX>@#x5h8z0#P22UL%=s{JwL7qr5G!bU9)|BvJU#G(5fsnByvi>} ztN^|&g7%C7Oo(!(fS@3*4Dj8G1y~Ob3sL$p@DWF{r5SZ`vV+s*vrp7^;({D$(ai6# zn<}xZN-Y zF)@TozK81g_zucVZQP@Nt#EA=S46b>m^eN&1DCb6tJFKj1>od$a&XQ_o)8v2kp_}4 z*-ZP#t@}S>pT5npb*NGu@w=MM%uq?6>xB45-B%kY1+P}!{Q+lq0xID_>3*E$BR4NK zrcp=5o#|2J_7m3e<)m*iHLGB%>FmhG<*&5whAwAUuPcEp@j&;}Im(opX@wiHAB=rm z=}Z1f6Rj?y*zFP&epMo#djSsJ=7cC?kP1L8e)Z$WqFD%d8oBm2O+GyF2gDbo{78)b z>Sce%&{ry0f+3P=pzVv_IhFFe7Fi$g2SVqwb~o_&C-@!jS;Bk~!8_pVEfyy-Yn!Ua z>L(QW6~U1_0Yj44u%@suMaBuKZs1fbNK@9|DqCijd*LG}iRwFtg|HVzflgVJLs z*Kdpy!`Ju1%i!k0-kp$tQiI$tKVaVrA*M7v=H@P^U(NVZ%w~GOBnV6ZSarHP`3+_U zLs>s-%nF)z9$E@duYa_1 z8*qIl>RxfL5yLXj5kI2H$W(N8^B9G(#^m24dPMG`e9O$f=5#>{2Fm@Ux8^&1-rRhh ztPCgJ?WG8V>^O3fkoXzOisK_>mUA9}YaKIg3f#jH@{0?V`JM+MHA^qMDrMvbEIUeB z;|O_J`%{<5fSZXUkNZpmOatxCY_@l4`6!3tEdkvdG@&S1H-nT_Kzy@j5FlH@S`}^` z62o0i5Hh~oN{y)j>n)ruJ)zuplT~9ah&AGMX-`hzRS{?Lb`42&JUp{yl@Rdp3W-Tx zNOO1U6a`cYOKKAK8B(TSf^lf)+2yM+pTK}z8K2ntdJp(Rm0Kt?4_aqxeXMs6bU@E* zRe&2`&U7R=oee~;!LKPpLW}^F3wfDqt6jCyuuNEWvAby8xtfcyB?C}FGbo@oq~Js##C6V&pBfZ zy%gs|F!Yt=U9v@v>x;h8My`v%L3@2gSXYnAd*)m@jsKrKwJq^f+S9rQYs**wp<_IJ z(xq5{w`?BnHzpyr9A;5N`fi1nnK$Rt+(A9G|1yE2sn`Y3op}8EeAMZ}(d>_p7F>%n zF?X82n)U2KD3}}w0!`n5>k*~rmUSW`HrykF$~S4R$^Q{bf(#8bzlq73RBCaSvAs0d z{62BlG(=i-ezu_E)m|Mm-H-of$7y82<)Gm25;h3gn;=Ub?rPOEQexWut=)-P7&av_ z&~K+m$a(r|ls^&E)r z5jCuGTTr~|gR+a*O23{0qlXZ|Ka|+Lx6mLCahz81rj|U>v$hY`juXT=Xkf#(yYinoc<>o&g#-u?Z*S65;>G_W{dK zab0!Ifna$7yAxmk2FZe>ueG*DV@qDcbk9KcNk@3|Z zCk9zab4&nxDPBCSv>x`5kdnYGyVYV+F%D(Vj3hrAyx_1Gq<1-XiUkRYV|)gmiX0(z zW!kg?WuAc%1MmQI_04eb(!MNxz8|6-EKpqqJ76Gm<(0O-W(E-eGK36^dd0fkxCoCf zilBoR^-#M*#mzb1!z3_Z@y%NYdUnG|Py@$tnzp}AIPa?qC2fW->al<<#$Q2lLg&r) z2TClziWAMO;2>kKu%^ZTLV)o4aJsxlt`iLWBCZ@95v5(7BlHx+cp{R$v9!~Ga?aMZM5Qk->#I!$K3^>pwf1+`6s_5%Ki_h3sBpYBkoi;88X6Msm>f{5F#dEIrvu z6ig3(RwObAsZ~C1w0t=Klrt$X;Ow{~ZTd}BZxoH7pFMY{BhQ=Gh%RGh>K(az%m<`q z?}%5`+-o|EK<^M;pL?~;?=%6LR4t_e=yz3~H|>HPWomfZ#) zcI&sJ@peh&f_DD?m2U59^T8YrClzxe-QqiSZ4or<-bpUhK+N;NGT8zMr){ueNwqnD zc-Ep)z=IM~v{FFCl9_V4`~9$}XY+9efy)df5Pj$Z6#2tVQV+&QV9bMJtDh$|{v;FX zjN{sZGw5MSzyJ=z=zT=&dg?!tHGIyiv$V!Qv!#gY!3QuhFK;mJ*4_egq<;O9`nz+1 zeZ;i8wCXX_WvnW?bRoX0EH68(WY+HSTEFB+0ecOiGr;!Xk!Koc6CUQXI1&PKbvjiz z*BTQ>z9s8IsIIDV9%Hu5h*Ofbm;Ulnn)6gAvq|@H zVFOWTA+V9tkOa_Y!~R&Wbxi&A0qG(C(W^^joQotJbEV5~kZ!XhW-ec^N7~I^*p3)C zd}&{$r%;iRwxt;9Q(X3r%sfqAA`i%rCpTt77JgVd=P~m=He0c!s|VSMMWx=%nR}kX z@@h49`sL7ud;vHm8> zBBFRXc=o*SZTk->8C6HHK;VYp^}b**j(bTT6QVl4vB<$!n}CMR@&3(FgdjSI*9y)j z$eD(m?nq>_UE2Sl2dV$naIBr%L^ZkGd$Ss3I$R_|Jz*Z=vp;}Og6L0F)tdPVs!_+A zVgE)TCOu1+P;Wmzn9vw#L|y8G_I{3;$rRY!_`jk^1sE5>2BRVW#6{#@<=p#UEj(~! zhEk7<0XM1$sS3OzoA+!wP(_7P)nA$Op>svjS*da4zqZJrkQ2&jsHy2;j$ zwrI@n)$+wK{KM?N6CS>6@(BSQY!vKb`8l+9w*kln+n#@$y4pZS(0r+)sf7kqqo3uO zS2`DgBu`)cQh?6XM?jM3RsmHJ)u(_h*rRDp5kkbMn9HO@)>G>wKeK}$Pyqn}aL2_p zsJc6WxFC(f`3@dC!|LZG_J3da3|v`!^*6hgzETDUYlYHwkCV;nX48OA(pRq8ZKeSQ zlY<9owUznoU-KeJgjb(VVkO-&yQ8P7@I=}8mTJ_tI)h-e=W99Wz6 zO|=jBN`QE~zR{=YWA{W!*SfU)&rV*9jJTPU37FoM?I>*te$^4L#qG4ESdAR*J0=#g zamX2cs;EKmctqJ{A^gVY*9AU!c7lEF>5t!sDuxI3E|=g8+!T%%p`K7wxn6a_Vwcb# z+dsHgFSlgYJ_6Ek+dLm9!rcpuC?1A*c^cubX6feOFSdVEq0+{mLN2L!0wo~tv>M_I z?6R*M{K*v4le*J-jP7~Yck*Qyr!iR2W1hqX(;v8cpAVb zT|xu~8`w~Vz-?I%$-%R)1Pm(hL08G9DKV6!D_tJH3)|ceF;LJUw>Ck9u@fL2>!XC)BbjXP;R)J3X8oV4W_<7%yb7Rh8^7&=z(Av5vZvMXIquSF{ z)uH6!hKg4|bxe%>XO3eujE1c0K2I4Ri2RE=db~G`9=+6V2&!n%0qZ6Sx~X?ktWu_n zx?PN%oEH`r{b#Xcb(lMRkI1XOZXpCpSs7VH#YlQ4`|zT=`szh))8$wv_QVLC)`&LY zTz(_P+=TqsvVr%Q0@pec%@t9R5(A(@;obg-3@KrZwqWbjeRtkiD(Z*dOxMjplU8p6 zq{~YCOtYkQfEjj67}A+ATmd4|=?MOeoDk1aW*cqhecn#SeG4RZ<@K2q1EB>pZzpq8 zK-bj<#Kjt<=W@hx^CIxK5dC#)v;6v4c>d>g6*k1^GbljztP)a}G8*;~wtFPw1okL& z!2x900c6p9Ne;I~pB<=;D^x;~E9UY|;9$u}TK+&M?BjIk=!(9PQ`P(kNDxC6S4$rq zrP{;eod{XaUJ;3crQfKx+drUH0@Z&(ir+%JWgs@17{M@xd#=$g%&7B3O$lKGsY$a* zi%{)1`aaYcBpVixxBSAy#N@2!&MbBu7(+0=rccG8D!<_H=jLB${PbxeeE^C6lneZ# zqZj=Sjj0^lX_PZnZgVYkePMy4kuIk80;vxH?N+vl?7aU;fpfi4sUy4%57SX!fcK;W{}dw=UrhY)`y<^j?6fE17rAfKBw zsQ4?+5Tf(>t>on`9 zE8p%0Aq}v&q%u^F0USp}Vxz3Z%HmmM05{R+8K_VX_!`O961du(2(ZfrG%O*&vV_jA zCLY6bz!dnQh*fBXxO-+YM954V3|w~tyOA7y$;fK~=fV@Jc5P+*a7j zt!Ds3cx@qVUB;WT-tk|!hYozcXZuuIcjF@y8j5x(w0rFuaD`}=)xkIh*zCVnkU7T} zE9L-pfc63ZG#HLao@~qjcn)qqwh}O!l5zfNM9h@p8+Gz1`+EAw^AO9dZ1VK?^FZ|r z@N~cqc+Q-LPU@BsqXfLZKZn{iIF%XZnYOY?T2RS0&T@eNefXM!-Uj%YiPpy*dy#JH z6w=8yA>p3O5%w>pajOynjfb#cu4n~MDVOW?!5~J5y8(Czf`CpO(|+szuXJ(xSnQv9 z>q}~2WWIYP0%pnC*CM1JL>(5YIrWAe1RW7|puX_TAfaDy=;`ja;D7kVGXR)(Q@iWf zNJpjuo+HWNn?-pBFlglYuhCv03Ru@h`GHD}tU%X|*WMy3`xySd8}i=W53^aWw+#;! zb|&DhI2{Zhi0tZ!m$4>BTdCSUOI~~pxeLNAR|Nw+uRsgIq&6UG@tp|$S#WD=kH|3H zzvEdlIDib^&Om>S7Ak*|eC=Pnw6wo3V!z5M3sMBh|FuKQ+e=eTHOO9l%G8<*L<{!}1K}Oky{C?U^eFJPH+)!{} zGLQjlvbW=+l8<>zjhpn6fxMVHnJ>Z&BUugo8w`YCh}#@j>A+TO#RLT#iSA-ybaRV_1@LlUk^6$qn9ojcXpbwXmE{vq3vBS zl(Im{%A`!>fLZt?$LY1qZ2N+EtgB1?g;cDZvBmf5CNajIMGM_u+ncU#RTc7JpRR$H zGdb@%b6H|4dvCEAB{YLuT3`bP`)m&`#T_;qMpUT{3Vd*@3pRZ!3k9V9kz zaxah3?$^F2-Q8p*y0Eu))YkU0!slqS_(P|*m$#AZ{dt*gCrDn*Ey9Z^8BCN4Re}&m zOuA#v@t!nTjLKw%V*GT&`Q3N-^+mgi?wLM@uP@a+lXp)bC^5;HH#_scesu%{*p|%= zz^|eVm#E<2sq*i$L($FxR6cg`2k8BtHh}RMug?8eBo(S}#COc{cPn@;!B~p7->)AH z0kKqwPJvn|*Yp>nmYq9}U!EPLI{GGE>gSB-QQYwp)cV<1Oo=@G%9n`#-ONWtHLKUp z<_I{3)L9X;T2QQSSwv=p*DakD+dcckz&nB3tAJEYuPE3P%PRY;!Z~h^n43+QWUk!ysS6J#V z!sm8geV-K*)2n~nLCU))jO>zi6H39I-uFw}#X_I``dS3o9qxV6^4OV{y;3x(sp1Vh z`y%;le}1&Vlv`7I77Wrxtq5t)*Jf|?Ze>W$Q5moOZvaHN4mMH+GJ3x`Iqn-m{xJoP zKA&s)9&dNyT44#yI15X_-WaaZT^P&2k*1CptyhHw-n>xCWwpB=sFD!L^B&D0B5ZBV z^Ky>1+-maU_l?k0-j~kv{<|mmMC%tAVzTkx*j9c-Eon19y1crx(+@1%bXWvLm_JVZ zxAKj@Nqb2?J#fLbq3H~9wVdZ*zT46x)%AO!&?owmc^PMtY4Z(mP15#851cN$Fu!*E zUBcC@wf)U7P7n@%yv#WPp}TP{|15Lv{$QN(FhMJ=K~Us?qyhEwQCb;%UGqao&f&>} zKkNUnl=llJ4stw#dQ6J0QrxS5849N=q>-nsjY4(_{nHkUogfh|>B$%0YsQdIx(q|#;qpaOp^VdhdAI6fitTLZ(=3(vgWx$Wp%K0bayT44g4 z2UoYH7mU4gshrG@Vo|)#go=@3h4Vtmob8qR=HAVDvLfVXo9Y3E53j33?;qFxJz{aW zt)nOyOb7FOH4=!c2V}9%nJO2;O4g;}&HW5tv)s<*3e{Y|GKrPLGOOPMuck@Eq1;MV zT201{hEJ^|*&Z7$A1{XM%Z9!4pBZt~zSV0}$5<{@cdaFadz@_Tc(YqPFpxq(ZTaP6 zUEW)xM4$qB-i-(xjms7U&fKK%tKgUH6HsIo7cZDbH#xT({0ug1-x4w5BF!P!w1c1V zUj#xaQ?}IZW87msQ~LLXxgwD=ODQ zJqqv|`;i=e^Dq1tF!5F>8iByvrowk#$#JYzrsrc)|C$(k>$HO&9z5z@N^HiW)>B(OTyX%%`^?PDt@VkVjx^%m zeshyfkUq{nhgT*fzjf!o-MjMx`e|Zf_jP))lZyj@w{!7lRGE&m-YrI z#lO>=ffQ=D)g}w(=BbfDJOOaD_N}@#GzK>A{>E1#E)GzIxB}f%rif8Qa_<{@BIDK- zy;duS`FpD*80JC$JnJAsM*wquhagk`f-}6Ypj1L$7D;~JnkofosSpVvPSSm;Cm*6# z7Et=6juN75PLZ$4Nul-Qp+&ZaQVVst;Se~q0vzQ}oEzObvjudkezh+TDv+92n{ zHQH{@Grl&BvjRVj z^vWSd%E8?HG;#NexR(cA#DV7a*4Fdjod66jH`Nvv(HNX;yS7!PNSt=mQtPo$9XOdu z?m+(rQUcoE4{#n!Wv|#>4%r_vh*?hf#-zUyW?*k82fl3kofjq;tlz4(PkCvvM_z$ zKJw=1%l;W4x*Aa78|E^*RihC&N-*g0$r_y~j#iIj1D+3_vZ6F6P;r8*;iWCy<={V7 zg;NnQyk|Dn(iqznIm0bq8dETdDAmZdtm$CrLI2P!ULpW;S^SO+w)UQ6k| z39&sa9xYv0@wUr|lsUJT)Rjk-5g?S&J2v~pJu~55BU?0oizxvP4j|f<)#az9VS_8i zD3u!Weil5`6A~8ZeqU!fL0Va+f=(=Rslk*FH4|i@&cqj5^f%ysfy!4(D3+7}w^Qy6 z$d6X^?YF1b8PH9l^P7StPW z`2X-JnkpcP zCsW%jtbn4Mm?m!Vxf(p2HYc0tHNZk3z&kBY-`Ba1!{?%TW{t%fH2IT)-1#qGy)QV# zi}NH)?D9Uj-Fuqre{Nr988ow0xIiTOh;>|w_aly-ylh1P*2Vz^gy2+a=ExAdv*OfN zrd_LrED=lg+R$>PE}A{plFq+>$mMKOB8R8He#H24J3E8T4CLG5oWAkd9XRz^W{mtf z)PZw2m<)XK%871ppceqzaa@U6OSs(p{QTP5x)!p*uRk5XcY5DOneuGNjY5l>s4o3J z;((Wnp|6<4HXjlzAUvK;Ren*5u3{*fJaHoUEI~l=v4_Af#7e~sDU&@JI`ri4#NdX% zpP?(x{{2Uld}iL->N#qzFOXfh?&S!cjJAFY&;@E0`RFvo7%?=s<7GVqT{Ox|;qA~u zhd%)K0R$8Y686`VnA2xL`8e`!3RWo>!B19%3OLF?hy1L{tP{NevMkis=sx9x&TEelHLd4lxPIHT4~b9X?1|C)Hd4zv zxKGUQ-T$P#no(IZoa{KIaK_P@oT5Vf=(y`XrQ%?~d2gr}|n3z&Y>>(xer=0SA0lOMw6-P5 zdGBw3;RT-h?bgY>GLJpXiDAwuI^H5pxb(DQ|N8z7fsT#I#qzYeUCdZ2;%xCTzRul~ z&*cqo@BAULXJZLFGlTv%&M3H74w{H=@A;{1UX1^;=>85f1Ef| zlea#DrwM^NIQm}3_*Mfz)XUdBpG~k*$(t)6e77kA{ivF;%`!7+Dj?+T7OW~I;^Y&B z8LoiwPBC4@pJPOm)#91&y-~=N!Iz6%AtM<914sGpe1TQU8H^Ry_kyR3)+I}e zgv$}o>R&_LLm?AI5t00WjNTmfDnf%|yb*>Lr2E~MZ{^cz$2(hK@odCOwI!6b96uyR zqblaG(T=+>#gVmi=C)TMJkeC+`>&sVD1q4NC&@@b)4XNYTx8VT$!FmZjb@f_)bZmH z25GD8LL=W2%)jS46i~w9*c#U=SEuEsSJUIae!XBMk9WmQr%Qo|2XN@2Hzz8JVpk!m z>mhQITPAStJWlm)dNV;B6!vK@^jZ|ra@@(eW2sS^MgQ|)fdi%f?B^%NfbAFFy%$ha z*GO-bvvUO{C#P+8~IdNO{8vsYN7l)LNI_)?8&8o z7_>{HcgcRp&*Q2m##rEG+*22c^YuukqV zXM&7Jxf+uDS60a#=TM53WE1OnPd?`IuMCzLOtC+vYuVatu~yh{xwMuap-;Of;n^IT zrg^(RnE=n=mcG>stAhr>VzopIyQU`h^04^wv`e;2m(<~BepG+pBVlGD6VjF?mq3pf zg*s>>cGf`Uria;sFNtgPAwm?FZ6RGJy4YheO@h;0K+m+=yEof$gR7DyhvEt6O!wV{ z4|H@f_J7V$Tq07(e5)ng=V3kF90g zx7oTrp&(LP+E@Y%p zRFxnPclU3kvUo$WbAV+!QdWIP9Dg}?>uDtqsyYQrPOV_6$nHa+e0uY4Vr1?F4ZoD( z?RMrFtourPDXkK!0?QqZgV_M{wtESf*^!QQl z-V4&I2Pk2MuOpawq!%DG(m_(JK1GqER5*sWAw&D)g^6q^EusPh*Qwvy}vf5vzk<$L9I`l9UJnxtjOba*>Cy1mR#Dk?QUFHbv4W_dfn?#;my6MW)c~ z*2DC>rc4vZJ12fW6-I1Di+ar7iFi+ewhWi6I2qT-clCB3 zCk(ykNBrTnfrP;#alhE@%z61v2wMVy9+VHOX({?Z-w_3>Xz$TpB-`7Ohx*PAe^GjA zVMX*Js%LNTL=N5apI6<2q13r6OBgjg1wRs;uKNT@iR~R#SU1Kkw;awJvzc(d^oiId zW?i6WUn;uvfo*F=<-NO78X--qftl@-2It1JVqJoPoTuZr1vbPrN9~QrtSq=^%{IP( zxuwKu_rc8P)~Ubmr(?oiGbTF3*pc?A1ZSSGtXu@{g5FTeYD5qppCjUa zfsNw*Xifil?zR5BZi8Ec9@7oJ{l(ZoJ?%z+BQ#(pW!mii8O z4Zi>WkPGs*@@x_gyj|^LP(h36H(=aA<6(xik)oPz_E_$BGs z+biUfL5Y5$IJb&kCzTSmr2N<|zG`n#xg5bwdS1$Rz0oQjAz&KZ`#3wSU7a8imgmL%y<%t1l#a=98;@VO81A#mZP!gELBs3{6nM!8*@hiPiF~s_hPDmP? zEX03FsjL!;74<6Ec!C}=tla+Uu>{JL@1s1<>62XLKV00ic;UR4yzOt6yZQ%Y`J_w)7tH`XHWGoI_jWwkTDaW=Y!JEyTG64X z0222TMvNMd-@&*phxi@;6mqn%A!G~=P{J|mK^Vgvk|(P!^&7miFVvVV34rl+Giii> zjn6MlKHk96lS?RyrN*bVv!&(w>aPDGo1D@uGEQSRp_K@`BtJ{7iC*O&IB)NC!WrYi z4x;Ky8ZK@;(1w0&0wLqwU!t?K#*uZ}c|(EErwL1@WQ_}<#=?x?vP=(M+|n?<)h!k>h8r|PhZ!yuLlijUoLuZ7?Y(+LL{(YOW9ulf*oq2Wpw2NE(;VrX{nw^*oTdFF;@JF57R#LDx*le}Fm zlLrKh`rwU_mXK7&A$>5RPqka#97orRU7B5k2P$LQ!a|*ln_(Z~-@3pA`sdwgmfaVC zzL{SS3piOtT_Ff!UjnD}OJzIY(v1Xt0IpRlDkP%vve`YH!|FUX1*-4dQMeI?HX4sl zVHz_4;((w^gZ)7gzHj3G@^b(CKJyvC*xzqz$(2eqdXi=wQ(tsPw*93C$u~iqQwrW( zDaveLL*Mdh;l)~NLT!on_dQ}yC~s4I3xE~o#kHxpWpDXi1)sg%^t*T+tebE!0zl{I zf88bfJ2<45+Zn|3S_}G4~A-P*- zw9Ph|{ZGGt6h*1aT%;dg?+EELd0h7F5##RLDm9B+ZjzA?&FY8o;`SK1sz5r`WYzf< zSIyO)D1U|JKh7X{m!tt5(*nk@F`|h1^B3imX(L^y^p1K+Z`j=kJv5bFO2X6`C3TX3UbI*U-mdt$Jf?A1S>x$n!!#bKVe) zca$SOFb&NA3019Yf$%v3c%7&518j;R zY7f7R2RDezdcO|=_AMylHWTNfbNFzzkOxuJ#{Py8y~$YVY$(l!L=ED|rZO(a9OnuO zoYTrXSh)Vov)_5h2w&7}>u|Hje)atoz%CPAtO-{(|9tk4*vNQEoVOAZ-kLA_8^@C}FnpJ(=P1QTO$8u{=nT0VPAIA zmT>CbaS>JSvJt0=uVM68YgKJ}GoRK?20poVOmxq?U61rPA<7eob6ZkI*G(Icp}b)P|;%$JF=Mt+DS0K_m(b?yhJd_Pj~i< zP;R=10@*W4VVvBj5BHxh$+~s*=c=^MDg^z`z2wW3Yx~D6C6kMZ3&JLeU8zpUPsUDoMb$=cntAut+c_1i1 zN&yfxGGmmk0Hij@Sf}pkG;o`@t%eZ~=lwhNr5S=EAw?#oo|DsOsiHt|QBXq?YKY=n z4T}Kjk6C(DvaW?)2r-|5y(4bn(=ESw`*Px_9ma?5Zczljm@RP3tvX4hFZv<&hGp`U znGXObT9^3q8W3eMkrh7xW6j1s{?eslcd&Q`MHI7rW+B8%CTIPKU#kDvJj;Oza=kl& zS<1k97xzt%EAvm-g87kYup8~>(YBbno(uyJuzB!*XgUkGsJicOp9#9VLqbYI0cmDH zxe=u|HbN1eAeb#-C zthy5vHK6CE?}RF#vojMKj_T3j&-OY8=w+1=9%F zVxJt);BnLg?>8;k$)-;R1fI2ooQjpwkzVWawm(5pP3k=1{r(!B2LI1Hg*~K>sT#zJ z7A#lGcblaKEIegr2%$2AZ*NC^Cprq`IYQj6L1y`mnGPHC+6${Z^ZLLxP1HCVI3-e_mmBAw!HiSY@<<*s(uJjg4OfEbv@9ol*ms7JUh3Od%oorD;7jCIi&; z8OC4Eof|B!!-T&CHge1bhR-;ub{>1eqAwkUjIcW%Zy`O8=4MPsT^ zocP1Q-y)rf(ifqX2)Fnz|0(8ES9C7G2S=*_AK?!2wFdZ<6qG3?M9Ek zWp*YORC0@Q@UL*+HR2#KXf3ICYh=)de*QoTfy$j+@kO3WNQRF>#FX7uGc~xplIw;k zy6Y=3FKxN&*r&Jn=vKkxl1u@*tW>G;D$?Nd{k?rc@YZEkjN`+3HGUHDpUvq;B|hE` zy2R#waD$(A2MX8O3do~!NX1N>2&WvAY%sFtq>C6^dq}d{5}>(^W0ffZ%BzJ?i#to7 zUrVCRPaGypfKPLT5NAk8MqNYN!`sz5^ zYoHmJTtOU;j*R;75a_%T`QV_PD-imWcL>}1&*#%hZZd*g5Po>cZ}z)fp5$Wkp`zct zm;O>K#7PS9N9#*T$MS*FcE_2Ji{$XubN4R?ISF0EYTuxzY{*pB+!C%Vz(-cQW>%=- z)Tu}0zT}xxe4Z!tAR^ZmA~32wVmm8T0OT^El5+8P;z4Z#KP03vES+F*yB{QYvm+E4 zL@goVYXs3;*0t2PnZ&nc zdRnpwbudIze@bn=EjaK<#p+XC8q>TT3DD*g0edJ%+pPqnT>{ki!=X4%FuzHXS1pK=Bws zb(9D8?=##hZPGR!TUbuU$Jfibo?t{(~aQS58)4X#1%R3?hz$P>AyqxapcS6+m0zBg)c`|VLePMCh`;B>Dd!@Sr;BwSfLDeE~vGEta1^ zjtxIj$Go!px{Z-ZC)^!no($}2_VP0KT)Pd39N&)RIbzNpy(r~FUDV|Td@^|muo{8o zGr$M}v<&Nd_W7qz2HBceHbA565ra2%ftMU1$KU9l%L0x!^QPa^#SWjx=fi#u=0i7m zYcpoxm)kI@?}@5lnXO>jwT+Jg$3~S8oS4<&7)xCx)kPv8-syBI_PHn37zx0sN;NEm zs@gH|0H7}9AA1tcX~uj09;uWh>mHj9^B{tA;z5*Zn(jhkAR=;EHz(|qU?&fFimM06 z*Slg{eZd-`sp&8Ja)pBjlsWiBsl+=8(=%La;bqn}?AT$+yV6_ymaPny1OlNm z-D6^+CPw-d*|s@AkC1*Y(hj|qK$dQ87g7Dm`5)13(+TmnzG2GHA6 zxo^6(4b>_P{>6TJD@4zaS>2zQ_53l(CvB*oJ4~%EMe4H}qwTbgZ`|kkbT`G37e<7k zz2{lOSH%1;pZneZny5R?XFfd>{^1=r^hMoi_{{aS)WcAwXg}b)(?YpKj*s>28O{RiL{gA<*3FVV`K-oIHWg-RhM`Lt7mN7TFH2$ZYD-}m zl1zZpU5U7ESgklE*heV3u};Djr}>3krQWp8S+KVya#@0xSv75`OVKG9$uD+v28S^-{S&>Dk0>J!S5UN_<~jel9MVhQp_$LX^Q# zR2)G^1k&Fj;00}NX_K5lXv?w737m<4w8zY#PxT17OTz@;E7J@jTp1#Y?Bu=HY9?!m zoY4?kO->bP%$=6!A;i5>y@?40CX_&|Sc?)YXHDwJlSXL5p!n%4C8-x3o7wbW zIt+yvMe#NR1|Yg8KYI>H*buQU=Q6>eGiLqIS99(;>h1~gBvng~@E7|S=k-K~2g5@0x#DG$<;XOuK?{|GA36E@sgEouq}vQQyp*wK#Xh|0-tf@SXB%Vy$!_lBBF ztM9 z1++N(XE0UBZAYFXUv5^akk)R~9E+c4y&pnm2w(|Yq?`?img7zWPm^Qk^JhIr#ra-M zI*t@i%*2$C6pn*pwqIB6di(BMTzOMz4`}+it4jD?YE?!<5BfVFOQmpiIT2L?+@t(e z;ivuss_S&#{PP7B>QW5io)q5C%$j|9BR@JL^)*kei5oVpVAPv+vBzR3FXe=Z^L{dc zdS3y(%m`}44p*r5=6CMpg#h`O9j@{nlKz-uTU#iv?-rE^LVw`+sJ4NaG`!4T;(>GK zk|FV$bpTCeRWBkV3xFgU-=5!X57MiwFxAblP@)fBmHZJoaPP8Hx{fI+-S%bG?RO9y zp>y;fxoL{ne4dy_B?wPR<8^2)cdYMlyedXGZRVxiiy)u> z>H@Z|_8;7bgQ zIme8eWI-;@GV|Vcd9EMx=u$Ag2TfY~VDtvavj;ZeGA@VGhMHqu;h7cQ4rb7dH@Mjd zi{J7uN!W@+4vhH^2Ul{Hza&j=x(RR!6G=PYw)=1Yw?`5;@L4}X7?{a8lj6@5A}Ll` zdr(VS`892$dh!`IgU0sP0Gd}t_+GmiU@rp;Ae{zSvxu_tpwtPtER?w=H-C?8xV#y( zLK!_%c-#vVXI#yCW1<-=&u>GfbN@F>Tdoj z{ovY%wU1f1w`^I?4iv?xOg{~{xr>s>pf{2!KYaxiAL;CV^3CtF7nr1v?rnNKSAP&b zl-^zf2V2I*|CERkWMQ)A)*#Y^qqhD-pMO41&gvA3%LJo4YJ)l;-xg!PI&Qt)wGLLH z*^9mBt?-i_Q0p$P1`%6p{?d6tdl=z8^xbtG@8l3*qek#ylll8R&LOF<1{){<2a_sb zxV}U1H*r+|y{tK54?2F&v?YR>PO68;v#Wn_CmW)<2k5JcHe?m@?+lJy9|wKnDI$rt zn)&9W9C(0OvVBazi*zdqF8;UImlGE0lSVKX86KP57fo#5nUzsdikV7>`EVrJ9Fnwq z0ZXk{s#R+=)r7!1k|r5}WJTtWnPZq9!i|3^0;~g0D|-fy6$Brv{M;PNGaP6e2m$Q2uEna02?|R|*)C!XE#v;jC-xA>HjLocrCsBBpD;n9w(xx1q&uq)Ue;r;+EE7zh^j=c%+){aUg}JA&(N z(EEH>W=Udwrp>9(x8`*zUPZw_QtW-vOA}Igl8wQJp!2xvP8Mn@ht6PyA?g=9HV>7d z6~(I(%Q5tKgv)q|RcfTb%5EeHDmaY?wgU8y1vzF&p9aDj5;0f+~>Q! zd<1JH9{WyLVG7%l@$ACHN8YGDjcDPrG7u|Q3LOJ@h|`xajIg!dyUdbuB-rq{2KMuu zmpIdLJ0gU1%}W6zGOXl{mh>9R{N9md+?5LsYqHp;Rxp+ z#BvM(1WsFH4A^m+6uExFJ~jB&R3Qy#Uj&txG3zn5BM%5I{E!8UcKo}H}{)-(I0BTg~V=|Txe(f69 zZ{kEXMsVz-d1P1s3b~7QGv!tTam>b-YPXMWFu_f*O!^s4{leszGWOJ^RFrZ zo|`5tD+nBn4%4S=Y0QPf9cf&j4OV;#epRoPu^(H=yW6dUF7TSqFi%?e{M3Dze`lK0 z)$o^UA!$xzE28#o#WDQH7sD5aeieZU=X09>;1A3DADPo62eO{&1;r+w%myy6kmFJD zv;Fg7f`}#3jOG~)-hJD*6h|kX@`aIP7y8q{yA;CI9;&M%i%`?CI2*@y36W(L^1*YQ z_+!4VrTk9RMDgI%)16A^a=ifAdGc7x`f97+1HaL26>Gm*YYy5?zWy;GJ5b6FFL)W; zmgc$Cot{)-PtE)=R=x5V60nhX?s2zKY`g7qL=e{y-OK&`28B%mCUpE<=At}hPj2(e zcNv5JAFOxlODtG$t9(q?*AesEWOQTaDxWby6{&ln89}_lj8Gjt;Hl@I^m!*+z^skA zE0yexr6F3VAR_SRq7>pnPG@$a;s7XxsQ$er0_wpap=eY%`EI(xbpt9Yae*F=kIuI2 zj1=uE>EC#&maSbxX|>9TI8b+vaz^5+4s0tEXoVENB=Cms9P<$-l1fIC2@h^m-SqhSKsm~?sC)w@5LiPB|kEDe$Ou)SK(xJhX)7S0P zE0y8HW6~%_$3!QL%gm+9%x)6A@|+$E%}K4*6iQ;S01>He@*Mux(X9>@QHhVmPgrqF zjzB)kU&e1%{!qMALC4u3;VgO!YLYWNYN7jJ75>%`p;)|s{0Gk{aT)KOpHNDti2%Jx zX@1OFCH?Ces)TZrX6j>d>O=l+E8cy~xfmAY7~u3T3(bWM8FXK?bf_anoP*HPr+SFG z`hKy&Jc!c)WK2nY&|s0|GSJ=H2!tzVF(I@b(=Ni|iFLN(iMrDDNY4bf5(8NpG_8Cz z6^Szha<*5Wdjj};xKs+@B;aU2P8SJbpQ!)9!&gqT#V@0~W9C`!GRhuQZgXlNS(>pPf&A@g&1v3Cw zyoEF-Ct4ga%eTw5&1a04iM*OD@Fjw~z43Ci1>$Ocb=-Ie!hy{NFg!N6v&Rb*ZrLhq28IW>D0oCV9K7 z)4L@APhG;jm3N%UsO8FH6{`Um(#AEmXjZAN(;cM}vh5 zCq3+lnEA$K)($D={<|X4i*C7qbC+rZwbi|AR`}Ni^}Fp3JeNO)UxzhnP5nB!x#_FT z$|D+nB($#cTZkD&*Pa_yu1o(~$6&9QiPPYWlQijkY8d^Lq+l*JvYD1T`~9Obwn1wr z=B~%i9$pj~ogjLtkxObwOps4Dqt7+)t@{@HIGR%G1%wx3ZAa1^Gal4NXJdrb3j7xE6VI$I&1<4`O)msPcU-}j+;yBbna5wPB*+53K3WVnx( zZ3R@g^o`i9KsBe{qP+xMn_po7%_t2!;&aEhZY)om69zx_jN}-XmXMA24)@29x=t+< zmfp1y<=#R6YnIrz%s_}x&dO$2c^oG$rb3k`h}h#Tukl^5mWw$iIvmq|opyEW7v>m4 zz^G`L{g%a4*}7SlGKWN|CE09c=~=1&n%NUCBvXnEX>zZ{Uvyn?Xs;eNVL07G55QU ze2tfUc25YcO5^dZ$Qdg1=Uzvuu~X`{mvvbAV|3S!(#cCJ?f~ReO)n#?nSZ*-KK{3>B_s%{d3*&?R4=9YM#I<)acD^IGhby>&2qj&yFK^Tm@&{CHm5dveH6 zAEaanw)rrc(B9)k7KeT1FwP2!_eD-!+|fjEg!MdsUz6~!;67M!|7WbBpFOJ=-F)Zq zaCG-h6o#oXvI|C)j2NL)JhBD5v3|r<3|?P<@;c`tr5=TaBp)A3!-RHe&(>*}iy!li zc)7LxZ{864KVCpikLU{q`16_i zAAT-zPE-WtBQ`G#PlLRYCAM>SqOJM%M-0!N;)jYH*#~h)!-?-mep^+bq_p*O1+Lf z5x)}~nx8N7!ue6+Y^N{@p&HGz=q&riJplAxUa3lIl)*_*3eO6U6d40=o1?o3H)}ro zkzko3-y&1;O1yiC6lk9h7D&1tM`@-ea1r`APf90Gd4uO8v^n+(^1vt-1W`Id3LGjb5z9ox5X&5*v+S z2SqJWUV^kjjcSyU>{Xpqx@@$aL^@cM^E7azSA~SY^k6X@Av}VvFIWyhD}Pb3dgfA& zi(lRq1~W7;l(AVnXS?`%@$0S)rL!u)+0OKnwgO}Co3!5U3s7r73a+?6$@%%PHUqog zxm`Psji7B#Oda}R_*I{1Y9w`)^SC1s`rkmdrD;PSsMd^HpmU8#wfrd+qWB>@`>glr zaChDMUCu|#E0^1J*mHM5SrCp}<(3UN?gl^3ju4?DrcL;DN2w`-y019JJZFII%bzUT z@BM6=$E&vBykrj**oT%R8w=LH4133)k(~IwVmvuerK04&nb?6Jsl^6yg%XIHBiH%f ztsOk@z(k1{ETQMscY|Leb>e{^sf}x1Y7j|XgvYPE9?)!>9m$csW5uzRN!Vs_oY?al z>bA~w6f6d3Ur=S5BMVoSg{QRm5;Vphe0*5!2H~b)v8zNmh$kX~0~4GLrvpt;tnQ&~ zPGT|*K}e_kDNF}ln@x{Bj&yJ<4F6J52Fb&hmt}ig0IU!kXYg}04<_m$mIu2-@UBHs zBKZrYqxP`bu$|e6)}FyXF43Ya;S0{I-1e5J*84-&%OeVWd>eVIht*olnE!nw+aqtT zT!})$yYqJdG-0J~Uw7fJVM#o?9fh@`axoFlV%W)DqHo+vv*K=9M1RGW8ej*5{WwSJ zE~p9Z4=xxXdEdHGP~%Gvu)Gae)mjnsyubQi2hr6-3l(?CLv9U-3GbI>4tPF*#*%sC z;y_vLxGIsi=C4!=x7}nCWk@M@MJ8V~QwdWCv4#wQw!$yR9y8LeRrX5!j$C7#O3EB{ z^o@wVR=b5*>lszp5XzB=`DfDOq%|qz(xbt&m9E>wgm~<3x%Em=*CDdz&Fjp+-WZ8l5jAsi!-DGXM+V4|2xknXw+4S(eP361wLyW4O_tUYX+{*?K$FIM9 zP$w&&0Kjgr3S?%-E8AU2K=biHnlXOgPH_tXEC^QkuDopLb6C~TRgsv=!PV7oSppCj zu?azVvTU-A?GRl!C)e~me-X-Z8|J~RFemez2uDTYBC#w`bX-;Tv_azQHl-U-IA2*N1Ts6+dKg(2jOD~*oxDW;H*H~Y=?*IO1i zZ-FpQduQ)+%J&4=_V!Va&1i#gQ!QU)1gBg zLw0tk;dp`p55GnbNnGnU98nL`q^TH#u@E96#Lq5AwhRl&Ul@IK0VKo;M2JyV;ukM3 zFQZOBi#dDv6H<)$h?}d^D8Tjs6MGkV|Z00XhC>a>SuC#C=7)UC{lqU}&pr zLE-htx#b=ABCX@0=LLS7|E}d)Q8#tpXG7YTe^!$@(Pj6=n?8=+Kx)44A^TG zlA|WfI#A#8_@FoCPwnMxFd;c@Y+MH8Xw+ z1at?*FU_ixGEe$czJ|a%U{A(EXlSMcwbLTw3*=j6{%eN{I$7nXB??{JPNZ=|u zT^YEOVP$G4hF6KaICfaAoUx=4f2$=nR#p(1+;tp;qaLH8ws%mCQf>4soh2Iq?8h9h zLXI3!{aIfkK6oTN!qt)U{5rnQk3P*%y~vO18CAuz;kIBmt~3@xOE>#LQ3r%sXUhis zS~q#)J-TuH1>q$xxMu-eBTgP64<9uz4rCI2-3sknkRi*ME|C=n8ktb67c;iN+{*m2 z&PN={h_M6j3)L(0V`i}#Xj@c^(WaOsf7DeB)E|U-rH#~DpEyQmACgk$6 zv@HT?bOC;yJX%{4Sz-hMvT(fU#41= zbn7cDS7Fo^89#(#QoF-!f)3IqV)%fF1!*d%N9I$}-&-}j<~cH+UKR)K7XGPJp8{Pp{>~2$}f{nzCL2u-k9WW`~wJ{q)5J=ctXQYV<>7` zZ)J?Whzysrg}*yIQ$BuI_(LdhqJfI7J#^yo*YaX|g9=gl)!KRLHvgkAclF^jUQ2t> zVf(L-A(_uyb{<6@MqtS;W_$IpZR2iA8amvV5RPY_g|5EFxKYw4y0`Y%{!maI@?8GM z|MhZPjBZ0JIrMI=)8T`zLy!+mQ~{hr6U}4cW_!KJM^s=b`M8r&?8pfl%PqZJICFz$ zxGj<}A)}m9MDQVyMD)7TN95^~cp%W|+VSO~>mo6QLZ03>=5MdANj}Rq199)!Ab9g> zAieP3eVqm6_+#~msFMAyZNh=j2xv3hH49UB1m!zht&jtHTki)I1 zISZ9#qnrMBu)cu*^PJk;)aYKz9Sh6ckobU4)Gcv%W|G;t7-q0>WB1bNWbjh}tq@U; z1wd4ZcyU@_^_Y+=5W9InPpN<-K@Uco?+ncsq7SGeq=Eu_Ef6E0%cS#6lO_lGq0!xX zz{qEeu;On{3++T|wNARhM^W2}{3Is?7PkjL-b_U6sRAenbs}}pl)KCwLGT$9E3(55vw$TbhdY9iPw5*SDWUJxjjv|dT<90~kNi;!UU z6~by+I*V8evY&Ya8`jMuN7v?B3x8bb4?CNY(?oUa7?1S68%|u1V%AdTHlWv@Osu@dqp#a;Tc{yDD+ce|&cw@9k zw~J!1;fev0;50+uPElh%JIgiU62cn<9RhVV61!3DqSmC)lSuO!av(BmIfn^V1i?kG z?{@_8ca`D3zv(IkkD=Q00Pj}P47aJlWA z0g8;INU8=O9$uvX5^c6@{_@&!f3%t6fN&sD;I66m^m=vGIVfaYa4){{t^{O^qHDnp zwtJj@TWrBHj%M{ntr#;nfri$3R;tJw=HhMVE4w4pm;Y(2nUf>l-Y@cpH4!17k{!5- zK8-DIaRk10Huy=@dU@40JfuwJzuRd6KF}>aeNn{pD!1PQ`KxeKdpP>%Ytxd03aSE| zLoe|!@$^k@MZnpQwPId(cm{AH({;g$C z6;YX~FI{IoW9v%ug}h4yU5=EX(6JBG%E1RTnKR1_=#x2Kf`EZ~i{Ce0)buGvEIc2{(^I=LF4v3k!DHXZzY4 zM!kmZ|io0wc9^2q#(%s%r0jdcP*)$X9lbl}Zd-Z(sk&|F0J0p=)K)q8>c_PlB+| ze-5JG2#^HUWPmT%IO%qprSqP0U;3A5c@{Izh>cIZP3s*Ze9v%RsXpJiC<(b5>6<&3 zTOl&6=ush;xw<#WA}pajHC)mmlwX;v#k&FpceV*qso@PZH$x%BvW*#ZlRPDAj}a03 zw&_2H-76-6Jm2@0mc7gXxskz9gkLdz><8gzS=0s*n^&{9L!637)5!TapXTHqld(hg z*Y5)ZV%CsGW5*(Mp{`Bo3$XVH=Jf&%sJL?Y=10ZI9|Y15jW~*|E>n#%>;6+7P9(7DhO`?UA1@KA-pKCAOF=?LFTrjim1$M2Zn=}e%8xa6AfK2e@L^1r~&lu6X}g+ znDec~N{QZ_M?i*!-sIi*U*Sv{Tc=sIxcj!eYnjlnt6+!te7a%BUS&3r^wNigm*kOiPY8AHQ3kZjaPKx)W8$5 zl`{RL%Hy(<6TTP0iv?!Ov+g#3FF3+IWGE01GMB(6VJ8Lgv6rW&X5trt4DE!;c09K^ zsq(g-qSKdgJx~GzjYOZ^QYAAuyl6^a2xl@wU{Pyj)It&KT2A9ABi)~K^e17)Kufb4e_malbm1@)3 zN@d}M!f)X+xyF@S-84W3LdJ{6hyAnmwVP*Qa8|2Xhyj0i$WKtzg74V*VTXFiHB#C6 z`j6R?K6#wh{O1kaMc2`NG2&s}5yyLiaAgP0v|7lwDI4dH+v5yt?ttg6vP?E})q4b9 zgz|>Bv6Ke*&0!91G$-JgO2pZhKVC2il={xJw9Krt`~ptB|fjrhjKQ`HEhLcSVnv0S4AU79Q z_sGihUgXoCT{evVWY?9{D}!#9Wl{TC;OD_lio{JGBEg%>E4O={;)nV@NMH%{pM&5m zMz_uwBZzoC%_!MUR39gCYk3_Wo|cxaeWIXkH%5$Hvn4&N?hXPbp2|EGL|6m5-<}{t z^p+(0BXFHWCo+wXe1UIMZm~U^l++q3c}fr|gy&3&{bbKf97V#!FB(+27r-RftoKf@ zn;kAjA7>_UL_05czP~6J*Q{{0_P60zjIOP$&jC*wg};Od=&EyceKJCdd~Dd9$H=FO z>uBZjzM{OL!VP4g8*lZUMgsNC2=HhIr##Y2-RRR2dlOXSwuhhb-@)f!2e)=~M6}KC z?T?O(jI6?)MZi6AZ;nyKz4vvv@>b4!m)guY)gKwpdo*=r0I3Vw)q&BLHaA{VZWJ0C z5EOAxcxf$J(uoXE?4-%HsjRx)6XAcYXaJ9OR@>kVy$FNd-MeAZ*oM%V^`YbKFU@w8 z>qhg2<05x?BOtt-qUt&u6f*P=o+YFu8DSc0n(4%G@GWk+r$=TZ2oU?1Q!bkh#@v#pAZ5d z9yqwq#59Y+VmTM+0x3|Ft~(5^TYqH{ypaaCh-&{l#rsIlXmRlI zooxP%yqr}mlqC(lKWBcWV_FbJRO|(usTI}TulH`ffA~E; z=AAHLC)l+{fjIuafLb+iH_dVAQa2-~{lEB064UGf;8L*IS9`)S=Eh~yV6jmuXrY^; z@Sg1KG(t6s0o#1FwCRj&q_NrCkqZYGcd3RH@B8t9bm=mB)Vay3hL|cThmKW7K(m>{ zmGA$pRw8={;Q7bICLWCh#~RhJSoHDbLUi?ahOqkw7uxWnNn`io0&eLD2yI4_;+ao< z7Gx_z{rkZ5;Ou(tgudsS7yffo0iRg)tz$>lJDQH4SUv;;-RokUr=l(Fh@d2`#F7$p z7qbvd`1gNcsq=pWSn%djDz(0kOTl4l_SXAdZooUj&+n8Z66u4rV&J9LDt*>H7QIU@M(#_>h&nxT1Xc?-HqhINqh__p2z}x( z;f+c3+n9*NsqQh*hT&raDxwTVwO?TgE{@)HP_Qvn!oYYk2%ZxX#^A+lwBB^OX!w=~ z*)QtO+I|xmQCA)7{vSiHB!2Q+AH@&qvhur;n_{w^ATX?1`F%J*+Mkv3^EHg|Q;4I` z*##*n;g|apq)qEh&adqS-=>LAdU^8a514MWSeHzK)fIn(kA!82vHGlml#{C82?;4C zVA(|vd$$XC$Al6+sl|&Y;`d;48`<0>H%?m&Dl|=WN^<>q74ws1Uz6mAs{UIu1s~$A4T2gms zksIYuW(vV=tbfaj#G9k>XI-i{^+n@8n+;2}I1)VWWp?9`+*KW{OK5d_RgtDYmJfkG zU+ulb%e%Sus$-(fH^SNNg3z;ZnPUN4`PZbZdB7R6PP>hN{CxCG_K@Nw~Yfn zY6BB=p^g7Nd2?G?=EfL*eoo*A&PQobi5;=;Y8*$sbztM`;^K+^4VMX_*&;t+KwXd> z2rU635>mn>2|0p7(NzbBRGimm0#u(KLUm@Dn^(MD5gubp_%<$_9R6my`TbbedDMT) z_|t-2uT;8bK<6hD)o@As#+@&~kWAeKJE}~+1YvdZ5`{lr>ar57PApvtH@v{$!YY4P z4rTx&knAJ^r+V4A;@{y<_VK(ZLOd6ZtO9z<6fw^}~zbA!U7P((b-EHf?`9CG?ro{_XTn=3-PfGfnUZMpxvtd!d+Q@D2S&Ht!}`r66qWC)JJ zO@i@K!PDFL5B}4jCHx6MCO0S0(P<+o-Wi&6N&%(?wK=qKn!sXUr>zH=H-ygX35f^b zyA|n8ygBLX6t5e2v*X)`7j_L+Z?hTzo1;n;rzo`g(sP+!$b0AMEx- z@l)z&MNm8nM~*n~b#{6gGjpex#yE7UPK&OmjtWh(%iYq_Vo899#hKKQ3L7|@9~8a7 ze?&@!Sbu@$7+fPz1%t~@(_WwU=M29zjcGMCKDRv-D^7drpw$RwWpz63I$p<330~i% z4Hb~WdHr>Jxf9%75u7Y5WyZcADq8(tkWvkrXqLo~?DsOqqdQbiv}sN?NDR-_|JL6# zd458wvyE5V{5%sB)BhT4bs0EYb~bE@oAl>zAx3~Mq&*M^KqZ8=u&cWwtFsE*CNNJZD=yQgr3^8F6a(9zB`=j zK<44D7RRlhEsxeLm6g6nOotE5*8JF*6|l9{n^aIi4A1^ z<7C(B+kY1{$*v**c*&md_`w}~ugX|G-=4!|NG^;MCNHG*qhaZlW%`;_Od^Y*EKqgz z{Vyc}^IZLZ{GOnN=N_8I%}06`6b8UG_UG7v$kVPJm7#NLAgoq+bGsTu%6(C(?0#Ut z)WVt(L?vHbafVA|`h#r$gRU6AtuqI@y^og17!VPm_6qBD^#nbQqrun)0#=!z8NTZQ zSPZM4fCH}Ru9FTk!A4;Lg{I+I?xd-@sEhdWgkW&l)s zk|3u5`dk+~!-8%I+~lQHo-=PQKjQLBPgLl`ry@S!E@mtSf>kK6efTW=ZR$Jn%W5;B zYD3vC#j^9sXRO@6k*i;#6n_{`)=|Miwt_lHPIOR<3W=)RY^+t#@DD)wYWyMm$KA{* z%4__$KkzzJUjHpplqw;q_WH00=+FLmnm3hIG0ynI=Ti#i6}~nIDFMQSOanUMOhLo! zybYPo2#0p)kAepr#(Cj2nfsvuOPePZpKgK!@lsNG-jv zcR$~BOAp(FSIm8$i-TJQ1=)N)*3=R%KA{Gd*GH?W5l!km(JElnIWtJv@Td*U36qfe z#5$H(1O=Q3-xkWLK?W!>0+aIxZrt|*@^rCx9|$0a%e;Slo+Fd*Riq7{vu5w2!$M~j z&<=mt0Yv5orG+&_=O3a`K>$3@?ATG@Bv@x0P}sCr;!pTO{+)P~m-XZgVUlcoe9O^d zcd*!*JAAM1jh>Pkd#?d|SVTq70>ROneO&}g&g|@7^_Tn&OXTTMe*U{bk0X01B-J}Y z#oPt;7!^r_rkO><=Fg3HBGI<`o)7gC>s8-W95G$I+_eU{Y`+D;IV*F6V8wr;FpRVv zLCQIIDErPph07V6b$RT+FU&i)>R>F<c}g6e-y-?g==tt3BzMsuN#$J>c%H~O4h1NSdp(zWG`5Z-cMHzX{A1Q>u-K*(Q9v|s0}RmoWg8R-%kUC6f}m` z;%IUk2D^oHRs|EHIPLH6JKdnI5VoLT1P>?OQCLV{fwbkbZ__TWFKD&;!~H10cQ10QMjLnH`&NGGfgc3KWx%+4m@|zy3t$Y34KBUP&#n2jf%OJrOahbu~z+RDqh! zDTejsc7T9tfR98WRst#1%*PY0s^sq2Y8(inPRIt(qoL=_V00P#P+C-7q|({4yNsd7 zA1;o(++iVG6FELX+qcg>5TE}em>Uq-AvoV0**esnmWIvQPS=RPG#(T#(Y-E3l&CYh% z)4qqV3D)9lEqQ zWEEMP&Q5(M1{i^Pw;{1Eh#~gyPAngqqFi$I58-{Hi9CQHIoef=Fyld7W9Plx{&KsT zJO+l}dh>e2DUGd1Jl@$GB?vfN6PN#ZuNB;Y!W)zzEbdPULyf*s$vp3KmtU03x$143 z_FMefMWV~?avUG=jgQ9&;z;Nc@cbx(h&?$)ijGLKG|Z_`0=RHRDhHxnmqN9Rrh^as~v_kL^KF`>S& z-aoU=yy&c6tg)ScIaNMB+^9eVFG|sKgkb}TF=E9bf`r&O;rZmZWzN=~5%^!-)l$HN zH6*P`m@0<~rJ*%MBT+AdoyT+04HOuJo639Nr}g08Z{=ew+l}pe8qq2x2Uk)LzbY*n z&HY_YR~F+Z4*$e%fa8PFGzk}}nk`ZwYV=|tK>Ybl5KPhSKkG`|z;K)GBs=fXP8H~@4ey)G?w^;5Rh@)W zzG-EP;@mJ}gc!!>&RibYk%0>Bjd2u5zNOpXMzsK)JUKyxW^ormJ$8MvHR;+dBYOdC zcHBdShynig{;L0G-9eR|Lu5Zg{(M!socyNFSdx`%Zozo6P0QZ)WHVAyIK}VfcciU5U$2=jO9~$3BQIz_Iaa( zrx9pO_>JGl)$^%H-A=d}KnwiuLU;OOvTRf_cnl*B9t-e}{0FNd;2eP<`dK$mU*f@% zAC$F$WuL?OCfD-q_1AA~3&lGL!*OF4b9$TeDyAn&@CcqkaxgtoXdQx}H6&8E@AHJL ztyshpeB-q;9h#2SEbaw4gBrfoQ5S}$My3jDp@{F@;Y%$g%TvkRn{C~E0|n84i?p?S z51FT^Y@bbphReOA;@zbohB4jhnw1EvsoOT^pyI}j4+e6zchk&sW&5Imy}G77t;C`| z&Y6xkGxF@7`XIQNv8YjA(vVF))x4H|!T%%btpl3;zp&wZqZ_0f326yInoUtsN|clm zk?xY*6r@u@KtNhRQW|McQRzlNO1d_Bz}TMK@9%w{cYp9F+o#UC&UMbY&ZjQCI1(?G z(ShCG-oBveIBSPR_;`kL58_`I(RvP1iQd;1UNU|n{Lv`==+}K_BIxj#%*5Sc0Tq>$;BFewfLz4G}m=OWKzKTM8ZV?gbgpKK9F2lZI7iO1yv&DL>>~J zt)3;qC4eHDxO(EHIdftwcW@XF2y|0C%m^eqFdaWDp}!lEAUeGJh_Jr#Pbo-z=DP_4 zY-ab8GX}pjtswFPt2?hwYcI!Px(;xH@FN&yUTvJ!N5$h%iFRX;pO3I~4}GEQUw4ZK zXhcJt3!cixj-e_Im0Vg3#odaFBkKBFZCbt|m04=0jmHW7gW#rnG-~l3yp`kYsu1I# zB*t;)dg;%<)!lI@IC+Lkm-Q(X&)?cXDC-i<-l+HctSr1>2N9;%F#Wd@7n)*h_Vg}j zdGA}4eXMNj`|Jp&ss^hbRB8Bp+T=va0bw+_Bh2a5CC$GV@UL9LYyLdfdaI;n7A2TC zq(mJ54iN49Q8u=1AF%!1tUIir`1!SOd#uj@L<`FpxO6I7y1=>}n@QO2Imw$A(|Ym| zi~QiPUax0T0Ix}I9Bps2Kv#InZKP{@>5^J5@oy3e)!28Ay|EyOa7FWH4;XUfm)N>! z_334Mu8stc>g+IL4;<>7BLutQvrHlO`VARz8q&5n`jFOL@QjNS1D12K` zDKi5uCA#R)sQ@_=Y8C?H+Fh~E$BWH!NHeKG_KNaXnb>=9GERC|Yg zw8g^)xZ!d?e;Y-HbTCvvf;vAv6j_*hiLkJDdZc6FEb1l8wX6xJiB^pIAhf$nq7YRj zm>0JZ0)!1C(yP;6aR^ugJ=(~F>5!zTsvqhxD_rNklFw%JJv@|;r*7bG<380B18r}+ z$iZAYrNM_g$+3_@=5NMKJ~AP4Z*ROb_K3E??{mE|Z=W1B-gf%GCW(T|=0tp@zYY)W zLcAvdr!83_P-h5yFf|Z3vUu_{SWo$GYz{HNlbNPEq^KxTX?99DV`)BVWBZ#-Tk57a zD4k9`K5uZ!XHDh`8f~YCheU6HK!C$mp03aPE*DBGOaiWm^DtmSnlYu!^3sMvDiTv? z^XfBh$wXy-a=VbRR>!GCL-G(&Vt#=M0AiDviL%1(@?tFW2Vp($yE-4~>P7FL5 zzM?qp5D%hY%zZwlUsEYUd8N@$v|YFukA8S={F0Hgyx~eb*r2>gJwY3E99=gqF2A7Q z^tKAB4~}0y54l_)Jjo66!3||4UYon*HqKQ#S+@Q|(#5peM*R6)Z~j^L>E)$tJ0%({ zRN&%2ChqpK@xTz%t|!r4Es*5>&sx2kC6`)rBiX24(2hPQB-O(Q?X&Zsqas#rBZ27X zEW8Hzs>3GFUTnrH+nU$yOE2v}Dq?JZ=CZx40SilSE>L$NT34FH&?~sN$9tljtTR!A z;v6%VW%`TZgRkV5Xq-i~57K1e^1p$F+5DRGWy7B(hJ3mfUgj+cP4Zlz;jKydV?q*2 zLJ?zheV4dP9Laor7c@+JSMC9bDZh;#C{H#Or;n{F@XDub(s`r4xXt}{j%fkC9=^78 zR8}t7T!f?tY{4Np4tYMl9-S5XS-pHba#ouL5zS^@=C`#VaY#T4?-!*MqEd5&;xF#cjTBrN?l{`VFEHbWK(0P=z$hW@MNr7CZEdpoB58J|oc3y!aH1CH0g>LwZ<9z^ zRV=zRF;h|1caJl<5$m2XRZP{8*1V_y4G1K(QRbz!jn{azubvC2CO9%Kxi$4}Bi2cf zWpr$A@ja)o<)iD!^}I*>ciPnu{>03GG$QsKZ+?GmB9b4F*X`Q1(GAXP(}w@P^$#$+ zQCD4#l-$<*nUgDt-yp_^a*I6~W7618NhS5&K8;CWlf5u?^Z|zl#6(9A*Kk~pE9)v( zof5#Stvfq#S%@PKUQ-}~@tFhGZ^(^HPL(OP+?xl2;Y`Y_gsgOEFr^dVFr{j$0<%nR z2RUq=r^D4>A$5yq zC=O%?iJ!wJrzg|dh=xfI0TtcyMNKo{=>jB&?~IX$9OBdUh)CmTZkm6_2z4hRJ<7kP zF}rrc?x)fmhiZG&aA*!6d-*-JynHuR&-}2wNn}=A)@@BC@qX4rz?CS2!}X;+ey}sW z3O&z=R047daZT0Miw_r39G=9_7f@|mAN(=Q;t=DDShxE_fWO5t!t@;~)e#?lYH-S`TYsdhylQ@9D&vQT)bXMe$)VtMb2A0cVK^$rK()wD zT{;(copHRuX$OrlH8!I+Pu|h3b(Cz>`2CLY!e7mLVIsI%h=9Mn-J>f%G$O1K0+(@H$N3r(I!PixKg}YEI_k^EG z4C3wPyX^|aa&4(3zbWg~N%3M$5y$SjtjRSrjV6#jks_sR+)nb6BgR4ul+6tJXY|!XU3Efmg z+41K2SHC_)cK)R%P*cEmwk1HFBsz^m@oFputO(^(nown7I5&~Jkn9)}Bs6|W@N<%Q$Mst3m{?3TUMaV>k&xxRQGh?Ykr}c|H2I4 zoQMI)@png`1Oi%aM_CX=m$iXXbRScdJNDSS*cA`IwWFTc1K?>DnS)%YPdYZdEr9QL7*Vd-Ed#` zn$`fzLoa$NDE4#1fldhrfb?+E5<%B=pq9?i6llHCSeP)BSRda`g5}se1Lv$^Adb=P zqr(61`2Kk+wl3R+>mgTA%!V=*?&OyB9k=Ss)03U7&I3Wjn;YRv<#$|lfJ=*pFg-A4 zOII<1@;e~4>i8O7UZaDnY(KI) zq4SY%CLS;Z9yo*)9uNMV0@`xa`@Y%+lN$j=lVKkTH{Z74K{cCVSwQQ4kKNnhKK06l zZAa0Q`n3ON4D%#aD5jSz`@jOE%qU5BSv1>EZva4_$Sux~EM1$v4+DOYFO5j}f2w!? zof(J7Crx(8Tb|RQZ>80hkl=M^Q`)-ieqWPgnD~fhsBmM>W`L*8%_lH9N|@giw=#XXDuUO zTBG-;?Thu#ze)@X)f13ZA^;ni_cm(aDCQuDF&)2e3g))3JP{|M`XHTv`T#a1ioL&5 zGlO^*cd1Q(U5{bK_MKB({M1i(eT|rxgF~@YJaW$Uvf83)w)6hokIiQs1}gweXqWLA z7Iq_xBQbK3zt(b^q)W?#e2yta?h;jM7mSNacou#)kNbU!&nD-HpXC$FnG=;K(^Ta& zwrib&$D2!d?!&xP;yg;6)~9YYI~@xPO`+;YmT+EP#X~joZ>THo)Ur+k+8Q^i<+D$I zv*rPX1Y^eQ{uY5rcBAVIfQ2I%^N-O`pHO)E-BsV3RBFakh@}*toq@V7E$CX7FVhg2G0UsXLYt@xi zym{4tg5X;b$DUN{cnl%{vphVltqa9Gv=$7{#1a#ZBbfC`NsWPCGKg>jY1Nc5MHB9F z>E69j61YB+BKWK$h0{FmX)fpCg*@wXPg)=R{^^3?q z-^{74n66H!-P=80Bi=>cq;Y}u0_R6ed!NC>;*@3UY3-bXcelH6qYc*7`c79 z-QxMPmN`Y^v>TOcXA?SOzL((v1X5*TISH`By6ZDk4`0IH-L_pklh@Rhd#~e_+qQFO z;@jl{6&xX{z%jeeO6M=%o5`+64o?puHXPQcV{0sxXAUv+-HdJ!Ba!!Y4mQutCVcpi zI(e~09^Jl2YN+f9zku*Gj&UOqFnMZ9?U&$P=2B|l%;OB6-+bgK8pr+L(SKwBN;;%N zfR(P?@KAV|QGGQ~Nk|$75Prm8WO=x^o6o07(~M6vTz$`}&89K-J(34jnz=&~zQ#Nb zqjQ>PoCyy2%5Z7M}cVs zoCK12IiD>ouz{yXqqZ#Oi>i@uo72|2sPB>7ZBKkYrUwZIfxkV${tq`5c47|qC(bf& ziIK|k1p8OJuP(XOp<6>oe@4{5cz(;{_N3m#N&($$c@!38V58l2&ZxwPxDUeY%yd5f zJK#>iLtlslqDYLOgp%B$fMhZZyFYrS8T;zPIk9QAw#A&4X5}oKtjIX{Kr4G47ywwn zCi8yFU3e~(wu0odDE(o<82XyyhGBzI}HBmM7MMh)dcN9bODp{_!ayhbkGUdlcb zSf;Ina@3jfO_#}Oa#v4G)lvzMB8a{j5jSsjc8c5wq_~0hlU8m9f}MvhbNyv|WVr>E zLK%&RTzv)qsEjuwUvmKe*x^B(g7qbINgL^5%0bc#7CZfIso+sG>V zDO8*L%i;PD(pm{I-(slp4SKYBQ=t_0K6LwZvDJ;u-I}Za{>~vP(46{gmFMLCjx8q zxv;1aKTgM{@Eu~8SKVwtUJjy)LI*n^D!xjM99hq(TdAjdZBavr-p}CcI%ZBJi>HQM zeyGIKbOwA)h0ut{t0#y!iK=Eq*t-;J{UbN`j!d}M1(1yFQgT-`I809(4<~^$1<+0e z09B9>!LRMDtveoI_s8Yx*0|tTMhI5y$p-xD{SB|3+`8Zg1%7KTH_{j&u7U`2xUKC} zBe2Jww@O(9I^3*cVEY#AdUOd<#^vSTQRF}>QUU)~kg&pZKB}r(eZh+hN$sg(%e)jy zlE;0U2uUo8Ol&*vJj3aSq9%2ytB#BT9E%on)gScQ7Iq70{WJ0r4** zP%MnnHgukRL{I~Ht$kVh3`dhe9_~jn zJ8rxwg1)QYaES~gx)MGzf2BYI1SCx z0EA)$@7@W`BoCAH#qzHt2nWa2O9kk$Ai+HiBra^v%{zgOGrDoC?Z1~dC=4`?0LCFv zd4Q+p63i6v)Y09i>8b@bCF|2}6}bV1hG-B3{5k8yU!l-xh^tGw>CsWAtJ+JSH&M9* zuf9r0vw2hN3z|zXEC)$NC9(G-4!h2n;OdOx?=j)6(#(&6$I5=FE)Cqk0lQe9i{kBS~ z;`Gvvu=n@&ONqp5HaxDA-9_i`eVgz~r%os*SI!!p{WhvT^6)0+uyA z2YUF@BRBqr{F@lc%}C*RA156ELDa{g_fZ9>IcM}W7G^?u?4<-CHwPr$h`^=_D296> zeQW`r$8VZoK4+!pR`-<3t=RAXMYvR&yE;QH`@6fl!pH@xsCD8JM8v^%>^lBlsv7}M z!5LuHBT5De0BnItJZ^Bj_{~>7*^cTCx-4f!Ad$U|f!#WqZlb(MAHe5_(ld;NjRWyx z?j%?xd|c#B?ps%dh^9WoRcB2K@frMU`=x{0T{J$H0p3Gq8 zu9f(H0ln4K>m=l}=$sHj<>NRR^928WO1bX6 z?~dz>Ih$lFwG+JXY} zGfpnfMuFfNw!x9YGrm)&uuorCFX}qTdW@3uxOP9+AX6W%b77}zNb2G8oy`lJKHKzP z&_HhfnRBH7=i*M7Mc8C#ssFvU($PNk^><;RGCZ_t1ViDN)gm1clIR2Ep%$dva2$|P zyy2GLuCj9IvIwkVKO+uy;mnl;Y_jIz{-H_KnyN0@E<;D`|9-UZ+yBxBARPrx$)i@Z z|L6B3(V!6Bgmbhll}#pIDW3zre6I0CkVX?kwBqATXuMPMAz88l!4p`MI=A3SD_1+N z!_D!a3C#D96jwU_x_!v@AKZBFhUvRgvd`$SS3qKfpx%YJ{6#cA?mC#n8oB^1Sdb== z6){pKzI{QgsS1g{YSnvAbJdu@6&u96d$E9q)TF3?PMLN*mUj{zboq}@Ple`H?uID) z{_?h}+x^WO)n5XiDnq_|3x6mobX}qSvCYYayjN&qSh{*ruAZfyph?I&Jr96ygf)$t zPn@avF9Yz#W5o7Vn_*$$i1EeG|BvDU{wMn+h^2a<8yPLw_yJcV~IqXt;1vT&M8}^*vdCIuAyZP)4tZhZ zvAA^c9WZ`o6BqD%*d?N+daRTqnG^KqL?tjzPC9kEM8D)_Zd8G>hhIJ8YYez{)Bl)! zwl1cHpTgvX>ZtnNji-1a<>Q> zvWBo%LW*Cn{kK*pioLyw{!VGPJ4Ffa=ucEGj+bkPzN$DmgZ$fdC(*tCop?5MDb*yB zeRA|CiWPS6ccnle5r%IV4|c~Ak>5wIu>+=l_VyWVxwY`MR+*sPds$8%3CglmT^hO( zw(P(Yc{}Nlwl7gPXM-eWN=Ne@|MzeI!x@H!N$)szA0swa+BDJOz5Km2@cL^uQ>i2; z1T|?dgGEuF{hYmLIb@56`gByN*m#&#P2Kl=Zg3@*3X8FGW|;3>$p5pEjil&<5#j#8 z!CIyMucKrWT)Y>2Z+HTCqmFZm5-DXWAY3jv%KV`70F5F>4~z}iWzFA~qVBlYSo+{B zCNDG#)zj+JhH61t=bD=~q$VcQvQW|4J8?C;ze(|e9`axjz=^^32y8G;2bG18Eb!c& zkL5R4n_kP##sxhUoJJ$Xn*n*Rl0F)({}`gl|1m^55Tu(7QoOAWoNBzf$=xYHpq)UiXK~KcNDYg+{SlanBS?~rP@pvS=*(b<`_&Dt;@=?Y*&2$)r$y@ z!vE?FoL5F%WKrXnVHSN_+=f*@xKg8cvP+LWa4r#u9hAyI$OcVV^}lp}$Ge^xs$n-Z&kVCsgIuz&H*s<`rvuFk7mrK&ft#=$e4tc{usX}3aUrx+@-B~KhB z5=bv^vt+CN}KO>&h2?hpaj3!h1h8>FvkMj~o2>sVjMu;)NO}WlwYm z%Zv-v-{2BDL$NsYp~5l@>#$QKD?}BQ@|5M)RA??@jfM=rwCHH34!+z*wJak*#`*a3 zC~(cq0h#H}qll@ifKaXhJW1D5KR~AoD+el#!;*y29uT~17Lr{*I1T!==N6%E(Vf3o z|Lw1~P87r{x@A7j^&i@zf9!munIwNdRx=vuB&rMEXfOUCAtIox)6d9VCimThMfr}s zZ(fkk(Cw3eq3*4i6zdZS(A;xnxiLn6TZBG+vHA##51K?I%7OznmL$TL`N>6IzkPBb z_%XxTmtS%6m-}Mjc!BKN{ZtF`mXAeXQAuRuSbfvKSkq1fpeDd1JYX9viNq&q>`Ymx z{v@MLK7RJYm)J4$LE;6msrB|CnN8*cH0rPt0l zmDr!Q84UV!H(2T9uv&KxjXUxh{e1s#UvI>ghiS^*RmRdd#IuNT-Nt6w>P03 ztRv7Rf)6s!#MW(ZBU}KD0^{laE!zIC*^f0%AFZ@|e4e%qBc`uJCjZreZ!#vXpF&_VI~E z8M#=4?tk`LWien%j-5-MEgj`zNCe`SJ{a}^h*%UB_hX)|%lrK!uq@!qe*RDMhNf)j z_GA2*6jGKas0@N!dv#ZcXy!v$e|N3{^wH|{re^)iN=Sk#6EAJs;`=Fg5ZrvM46*+2 zI?6HtcD;KHXlqNd5wz!^!+4Y%TUS?!5uK~H^Y1J6+tvw~AH6mQ=a z-Kf=`)&9Hi!-XU6^f53JUvmvDxG^vCv^8`CErIY4nM8eKMOR;ek{ux?#R6bs6>)-f z=Jd3>q1MNE)D@ z4Y`#gCWv$ytM1%KJLKm2D|LKP!3DTKWcA4~x+|yOdG&p{T<&P?JZez@yNqfeCz+5N zSmv3V(8~!sYUrI#@Bg0Ac_kO5-2P%y&e2zBR@?gbuV9Y2cOpPVAQw$xPy0!*Y-T7< zDGEP{U;EiDQhsj{PrZykh9SV;K3$YF30>mGcMhY6aR>(*%XpO^WDJ|~7*GJ?83h*X zW^<kpIzrKVOafrbl;-V37g|YDP2i}K7_>w?Et}x$CsaG<+6oVup#LM+qXr}hYX-R zf#ybnmqaU1sY=M7b@}i4{5H3fjzJv_H$qMOpr0HdmzlEtC9H;TzAuDT5&l?l{06r$ zR3+a!H5%bobyr*qy%!f3y7GS9PyBd%W*OsH_dHO)xoY~wnNbLyUhMGmR%(>qkv-ev z@oPlzgxrr~+W478t?{x>crU(0*ByDuO3v^LvPPNwirxQ1qR$8W0~)>qT}oira*9{3 zN3OfWPa3d`hkOa`{$RlNnLA&Y3{7LP%)m%M^zBh0f#J8$DR{&wuHxq#4jmdo*ru${ zT*xAya!5>c#k>eJb?eVe_Y|h<_L{MejD(Kvp3=V#$#NQEFFv0@*oJjQ4^XtW&OA;t zb9tL)HX!z}+4!?)xyUW)Tc_Xt@Xhz0g$Bsg#RVD=d3t*0k+++m3Y;-d2C79E$cgd{ z9*1yKQ(qwS+tg0AGh5Fls{5<+N{Gcg!Zs3|4q-(ezUu1gF^y2T$oa;c?o+tB`geYV z*ud8&bWvX9=1%&gHQLy3HOd^Qh$yLKEGAK5=+!&@JmX< zx1AjR>1XFTy9uJb)2-d(h4>MSr7r(NdUxh=FW>9gT9{muwV}GEh8bfXQMC(L?qBRvmLP8Vc*;9V3g|98DiQ`hmjQOmFNR0Yo~~*Ep%Gc!dRnTo^k-4XwDa7X zLwY^3d}r!5q{if%A)H-iD z2&>u;u;_P$cQgcFD}y5CRp@OI)d_rEcgO$&p*uHyB$gQfkBLr2!V$WZhlF{FzbiLv6!wea%OGre)n7V9V;GGJ5fhM&9Y=+{-GT{R-KaR*4DOvPX97Z_e6Sml2K`Yf8RsUj z*ldXOq4lGK2Kc@}hsQhOT?RF$2mJ<#(OaGOa3!3k>B_jHzq~&kj<8u3KrN)pRkA3D5Gsb?K5C<-EdCJrFi|0&LJ$d8 zIZsN#Ng*u(zT)RK&-<07A`}MPj$jeeG=TPKAYKFwS)vOY(QB80X4uUp2TRP$Q)f>S zB9~=~&C>Z;auNo{-wpkR)R~gOjm$D4!40|bBOUT+bv3^}$v(mnr_UKnesD|r9r=5= zZSN-{O_TBQ&sT*rg6Vak@YpR_15oNi_~RQO0zJOyNPE5GM|Yzh*bK-NlsNakBm6D8 z-HVbGv1a$ZtL>rgO$2!VD215-DHPssS?Z9R9&*?!z-I>) zlJ2oal0im_Un4^wT0Ys1NQfn#We-h@w05w=!5A#_`@JU=I&|m=9iM!;MN@ktVLeV2 zxK;^7j;X$*D`Vo*N$O}!5!S5>jvG3v>M!${)wSHI_`UB(+&0}0^Q2t6R7~|Apl~Yz z9^Ad4IMva>F%B@SekeSn@_2ljcIQoEnG>5cf6xO>?fQy%G~Y2b~53Bay>%y%Du+xyAA_Dh(O9$CxQOR4)Y@2_a1+aCXK!f zv`)pTBxH~6#h<>nF0^7o|~PIX;@&rUUCiY!ltC{K?v`f9C223$G=xj|aQ^ALdL z2>+&^Uc_GUbgGvP+GEWX`=ISZiVmtnscywSlZ>}qqUgO#AAyC&!HO(ndK_RS|2w4u zD0N`@Lqa@jRQf*D$uH7|!G?8z(265}KF&UoC*ur8(eyC1ddEpz_reMnFXWAP@hH#{ zM8@tJt#}YK*AjKTDR9G`0Kb$eAH4<05$>?Omr$Ya``u~+w2l!|0Qs+5nG+Q|C^J%k z;KkF+s4h01wd};d--dJs4RaXbu4b7|hv_MiUqKz2($2dRHAkW3YP&QhGged9Bb9HKC47qXgb4vbt`GELbX`-P=izY5HLJ1Va2 zgXoN_(o9-NWGNq;_?|;l`l)c$f;#UJ#rNaxckd`4aV9N^HS?y|bNI`WaOqV*-q|7> zQwVQYkn_Yq#~vZ$e_D@%}9 zy@s;;!2B|QVjOfag1%BjaEi_+gn4tR7}QfC0@5o9(A3e~Ne6sY0bi|b%@>k;i?f%` zwndXPo`pF0Mq}D$b)ap9*sXAxbk#{Vcmw&qNHFd*aTM|mBb*8HoCdKa1ACBtH2hbb zDDs%e$zs2wz)rMQd(ilgV1jLZ^)4X~A7Aes2Eeyrrpa3^E+qMT#LGRMA)pUULPL4b z)Es(wig&QLU&EI)SV0UND>ZhfDXMYm<~z_XO~vm$#-A|0o$~&>GuDfD*m2JGGE_6%l?Ou~=OFg&7j0)W(xm+*{AfhsW}s%C@iivCxw~ z?S~$s|FAQwET&@9jbHla+-|7beDA7O;gW-yj9(HZvQd7H%DhOy+03fzepok;3JrVx zz!o|(C#`SfE>-!dwJeRXv!O+uMC9>erTE92h{ZW`b@9W>KUte&(7YjTqzq@*3Ycw{ zgUsL8n$N<;MqU{{zNv%N$~d=#NGf`-{-~=GsYOZjX&o+}Ne5U)PEI~fI%dAV9NkU_ z{ozGr6CBKLpX$wTQn|18`D)_Y3!jcxM+>Oe<<6^}yzBQ_i?=aCrOr=_*oB_oMShI1 zrcgNp;|7YI6)qcj?!5i;PUWkDHvLz$k^~e9je&YOObtcfSq z5|U>6vX3x=U+HyZuZDk^8q1S3zE_^>mApClBSRtbFavaGK0WpQz@g+9@fZLein!~| z@&g|m8Cq9ib7w4tIIid_K0d~f;k5=XE*8zKeP?tOzwKcc%9+FsYg`8MVyPI0xgYTb zFg8@H6TN%{v57#p+aE5p z9Cf$PpiiG>+SL?wUW1gf*xZLG3c!U+M@c~`zuuvtMD{g}mrv`-H#s%X4SZNOck-7G zzg*7ZVucJhJioHq7*>+nkad0(R65(aN0_0ZfzUeN9!u>tBY@LB@1aO%x)H~^{$c96 zO(sI_lVB`3cLcru(GaRsa;hXDLk?&snrNW261UzTKMgd~45eASu_4_QsejeaRNgS} zFW3QJ+zFb+p0#6Hy(m8tG;UHJQ;drdm@+5fr_2D)S5`IFddEgYNU0DAq~}Db-flmz z-z5vLEIbeny$1oioQ(hmmHwTGf zYqFaD23H_dL0j{=CB{L*DphMUwgxqa3_350wVw38(|(n11f_&XYd8v8yZkljQXn{{ z`q3be@a2#hZkB>#2S}Npp%RHG%X}t76*5G6X}lJJyEo%IW9+NeN=bp}a-beu55*~A znOQ>O7=4KBxd(OVMth_`2 z&VpX^XVx|U?FVNTN!Y^k!j{sc?c{r3pRAL?MbvcGRX8o5R`WJB*xG4*A!_09`7Xl* zv~d%E{RU`HbPvT{jnW}Rpd)Tu zKU;xdcU9FlV~Te)P@#>LdD)ci|AolD(n3lXR%#l4@d zi(lpVaF1AoXvvA5lS543uPgel!YqJizSR1F%{soYK&T0FW)TBeSXj*b`O~l^1OzP- zPYj7lzcF!dHbgjVn}X?-LN52!&i6MB9{pnfOpNAMw&pMZzVqKqxZ^$Jwnnw4m3?2~ zhhx>-@DRE@_hLW~lQ1GfqO44c^KV~UeM~@XW$O4Pinb+WFvJ|GL7h9ss zS--$Sw|?cgHl-bI&NibjKU{KmW>ame(>kNftVrK|llI*97P~TWZjr3~r*~$k$3PI)WB{6n$zorxOI>8968ww9I!?9sQ_hvTL1-}KSs!~1wxVaVtJ#Jx8>vx zFb8%|JE%$<#Fx$sJtsP!;-C9k(-{}N0D%hD<+-}$jKcD#JZ}G;)8Qc7GkW?Ef@cFX zTwWBcW?=8mLk z6*Qvi?{5h4*)@XOX?dFI!m5*~MT9i%b+Mz$)11F6pGyHu&>&aEXg!&LN2bYyNIIlf zx*l-r^fu+srbHlN3vnMy!KP@jS9#QUsEUZE@e=L zSd~v5HU$jeC%>=l!U$#QyuUaOwo7(SWS& z+O=(~=?3B6uMXQC?>dtL^?JieXsjTnJG_oG;?- z!B)XL5cQ^Dgbi*~jt3onEwRlCkXQNm&HxP{N49@_gEC(#vvz>tgAs8O;rNb)lLk1f zfDv<-#}XwUV*x*ww@2}`He>GyfgrbVIt6a@PgP3=qy(!x zXC&O_#!tjz|GZReL09x8ga`avO>A%uMLwLF?c}cZ_n&`=)V@>cVcT{z@8`^5kV^mM z&;5kgNZD--VHTi92Y7!}E=ijUaW_Xw;gYiO;Zc6Xf3_S)ct=BpqbG^LG16Y&H#6~X zEuL#!nGHGT5Dw-2&7OVAPlrsEckZ!P%I&n>M_7-lPZ+rMJZ`;g5Zl}d&G|*rRdB15 zKTxh(Yy(vkZ1(9t91u0~jgxJEq5X2Z>m@->k?jBVKIcH*Tp2Gs=RT02WyqT{pyRl4q#JN0OzXEb!B1k0=)!zWX}jx=1(;t1p_i^ z0Mlz8+#TDnTjE`}cSWi)nere{&w2*%kYP6XOLB-9$N9oALDh#)CHMPwU^TPX-CgDbkf=QJe+ii12of^Tl$?;$aZckmcR@V_3W=RyuSzyYc0= z{fx^$uut!aT4+xP8?R^fv;md}DpVjuyITtw*b$BJLs>>b_tlV>5Txe=jg^C~oJb72 z4iJM$8a)xML{WKEivXPO?$C|FR-VGwNRavjl7&}bznZC;S$DH{Q+X`Zbg`#dNIO0B zeNBtDjKND;;4TbUs7Jopbh>7&eozTf-c}e-!Hcl7#ht26JDGinctgs{y6FKnonf6I z>ybf&AKia;QRk@C3pbSwYrfUtx^iIAyyYV1u!9lbxR5A}mM#j;GGAynUT3NB92!YW zuWz@S4iyJ!i|kiA=@PI#>Gph68|t&fVd6?L;07%O`FzS|W&ANjh~YAUll|@DM_9tm z5+DyxGx7}ExoE%7Ms$RhFoO^#0&xX0CWho$?CdX2cUf%|Z#hSd&ffSgk!xI7`)g=f zA}Jj&93&{lEyk_n#s$08&8@mD5okBShKvT_Jk-wy*hD(+eW*e%!_U1ou zJ`3Qhp&BM8{5#M5+))Yn44TM{-~sRTNnHwB=l)*!dt-5PSOPsQf*!%TV^mme^{ang z-`_m9e;y_n8rtuXj2G@-I6I^vs%~sLS?f>FV@F(8#@TthoKjU^4IK^`K{^sTKq*q7VzK$L6G|H07>1kBtkR z<(xX?btf;*#3bSmyE5#X;5bRob^M1vBu7+B3S>of>d(RX)n%kW5gBCEd44A&8RPH_ z{D^T`ha7wx&&kPYiwOKak)a7Vnli%QFXl?CAT5Z2#0j74z-q`*zc>&d4JYets!U}g z-vOs}W+>oeX>qRpca|t5unsSEc4an-}$2s>_ssu<RLEFG$a|%yKz5iwUdTrOsUAN z<<@$5`;+O=&<-&AQ=A9oe%HW)u%=eM03qsj(8UGXI&%YGp7M%UKpZp3^bhE4hS=Z! zl{&Q6z&P*%$L-6yZ}1)IEjlwT*hn-1E(B3W$U{d2Utu{|TIvpO_B#5gYb5&m=?<;u zSu7R7Cm$L16BCgd;w3$3X!!vs(I9}eA}i0RvjOQ`ZO(VSdY!Z=oa;grWaXD+PuBb?2xofGvM&t>saRWE4+AxH5&Gc$5 zP5*X!B$K}Do(sUUtM4g=j5=ucTZP%j0;buCB6)SGOyU0{5zL4i+j0dizLW7tUCJF@ z4mR?>1OCOZ7Z-|F9U)FyxL?0iT~QV!R_&x!WzDROCsbX8YzGs z_|V=N#}y;d$SR?M8CxWXrdEb>l>DDD0>-zgpKeU{Z~6&udHdjqL|+oR0Bp~{C)m3n z?c3?qpcbLskuBms+2CdisxB_T3%BY@SvYfmrAEN1P{zapGZf~Rc^QN%;mq63U(!ENCAAOWWOu5-c=gxNmsV z#FkVgb4OnsD3!EH%t`|Q8$9l+IRM2a^Jz_@{=4H2tv}bosf4D+%W@5I9xIXOR6K(@ zH;D*T2eZi`bzKJUQf2J7xLhk1>7kfZO2lDP^FvVAfu<5N3)Yd8l#MhakDR3XogkSQ zIV34W>ZtX$mUvxsjY)7XNOLY8E;M2NEl=B@;Ox7;>7;*f_3#*8y}gQ?UikWe5C~_w zi&PP~rmDMGm>?v1->g;7t0RN|U%Z?y2@Z>nKzubOT+#!!xRDPf0Z()ngyB9!g=@}m zDlLKFg_HV)%ZJD=Ie1zNIA&!4Yfbj*jy<-$q{L*Hz2F@7g2g73fHY#!E3h_Px=L{G zYXteHg(C~rXg|Z)SFj-Ne3>{MX-v^w{po&Ew>^|iF73B+h4w~m80E^=o zf$MeR7h0s9@n&s3qni*g76sPnnHxh-$iIr!h> z5-n!|QRCV9+oCo?QKAeqgA2f}7ef5inEXe^^&e0KodYuLi3UbCj@_J~ix1l{1@Kq3 zzHz1EQ6vMq=|A4Uza@QVG#_KHUQY9=rJbG>qT@j{GS04iP&X3LU&gi8e&(03b+|Zv zcqTRMtstX{L91;8HFG8h@NFl_wG8@o5IH-3*S^6(sMxP(5_62ZKQ4_A7xIacOuSOA z#vx_TNiwhHmC^thJ%FlQ=>_PwVZp?fW?<-Bcop^evIm3I`tkPRB)oEX?0pEqi`QeW z6W|3*{;L2B>$uXiF5V;1tBsTUxxF zYG;+v3UuB1NTwOpYQw5y!dWJyeBY?=nX&I%c2TmIHL{eM2t}48 zYnCEQ3WbnmMo45Ugsde?vP*U|A^Vza*|YEaGK`t`@qK^q=j~5_^!d#5Joj~<>zs3) zODY_Z_6t3(4ciRH{1^o9@o300|2UQ9dixuaRhi{SJJr;@FuRXh$sP$?TN?#$6z5u> zSoc-rA5<0Ah#p%>l0I*3weTY;Nbkk>Jbv%W%3_3=0C$Gqv5q3SfNfD@)GX^lKpLLp zIGaK`L$29`=Z9dp_5){U+HVpP!z1IK9Wa;mQ0@XyqcYI?O`9mLRR9x@4S{NvWpv+c1a15Zz*eVh1J*6 zosNEiflc?3^Y#~s_g>>J=D5;uaLOF0H~MbWIgo+lzn8+#KTvh#rucorz3YW$H+=!i zxUzVR_@jJ`xD#A+F^I3L=m-8WplO0*e#x|a3pNl9If1Iz&y!;zGea6}=lmyUo4RLw zn^@4V2Z3l&TTT?E9$e0zM(DNUi{5ncjF8HQ-a+!H_|FjXIe%*qjNQsG z^sCDsgNMgY}9zpd`qLP^63>7^#13 zOdFW4ZcrDiArkmE?(f$T@O_Wrih*oe~K77x!G-|N1025xBScQH~37h`qaH>=aX z-FI?DIRRU7xR@DzmX`g!SG?gjOG{f(_QW90_!PlX(8~;w7XQ0=H<|tW7{NKh9~vAD zJOfdk1>8d3Ah>bML_zOBouB1_9|pl2R$`?*px!7QLVoGi7@8}dC)N4~_f;walr%|X z5Io64{UPu>7E@4BqhsY!#fF%7iosstF2jrsA}I>JCZfjDE+vBR0=OkZm?X3QdA;iYp1&bf0-|Qr*E>YK>~d)b8h<4}Gq7Fag0#TBd)lng@5->cfyHw#M)= zH~@o#LnT!ufgDmg4n^9|x&AAh1y3Iy%#}&gW()#U`+~k$p!dCc7+Yc_j?M(|HiTBM zJkR~;=H!GbGe=mls(%x>8LcJ;#el>&CA> z_#oCiQxdQd#8`^5DeAss;Yn5}Y z5K@P}2|Q8w>HwF3ccm1=@TQS7Fsg^W`mDkllFO74QrsFq^I^|_aQ2UWIRAG#VG6f% z0>8L3)|0F~#_8MMI2OhEkg}VLtcI;T6yEnORIh(t^{pa266(ZKNLtntpk$<+noVmg zBY2Y8VIdord`clxn}J2Ry0;3$*OpoV)8A5;V68di(h{JDTEONKCBs?e;8CyjxSh@N z*Tyv8D5L)U8h0-DNowL2Sia!)L7jB3T|G(fw~tI~>(k2L>DyW-ZPej3u&?tZ1=6?~ z!cp_^+A~yWJ?tv0XO^^myms>Q(h!uP4eyR)66?E_)-h6b&QQONQOY6Z(EX2X#M1&o z+mLcB#$QXSmm%d8Esic8mR*jR7;^jH6JP!H`bw~@*F6eyZ+p_8-Eh{^1JNR5lQk*oZ&*0i z&vm}RcK@hVwI8KPxHl7I2I#=-kL>b&;(TB_=^sZlQ+NjE27S%e)~E6D7Nm4>P(jo;?7g*ycQK{JeW-hmO7`J!qLD zo*YXRS=Q{oe>5$GX-Q3ZD!^C#3AzYd;V7*|-?C)8;Cu*q@Y)BbE0K1{U@rUm#t76M zZ$8&D|HIyMR|5#c<^bT912fy;K-ZOJ5{R3MVdf=m?FH1kyN1xBB+k`I6QV2{#VBZ)tb*T_WmFWrT6s8Wil02tdX zSEWm^`77cT7khn+JGt)Kg#VE3xwE17cFgJFUl#OT5rJdN%xz*1E0)|Q;(3agpso0S z2R;)0}@ zonCw|p%8endt++Lr(TNTupXA@R&?uoIR!@Ruthk<3=vVDBT)Gn2`$#Tkg++o~#bCB)2wPSNqlvRD&R@FrzH+@nqo)$>NZl*REW zy+KGtIR&^``DDH*A|5mNBPnj=ZWZI)hzB-M*Y{Qucm8^tYJ+Q>#}jS{Pj3VmNRqUq30jwuo&`4q?^@kf^UJiu8 zF)oVr`8N|oi#|4i2Es}T;`uikNwy5(rK-<8Qtxi^E-ItCd5;Lro( zq=C^_J=tyB-&fF|WdPL|Y`oaP_`QBLnNvXxs)khluHLL>;G%Lrt#}1h*rv&}Mpqnw zHq%p`zJyUCRM;U!{s(=(_Jhe!w%#ue%uy5O4t%kP6pMNPdMCRz@-JGdQGb&%ZE?A9 z|2c(YgU}ATlK!5|bG<_mNbumLQ7}a$mbF9-*Q|ccbJzIx;S$%)5K2jg7~kLRQaF}Z z^Y?CXnGl7guvHslyU54%s9|Dh-{-$q}0z%~k2BBU)h!&YQ*{ zl#yTa4jbpnW2Y+xB>_{SapzU>OO9*7_#tBN#+Y>Qhs$NpA=ydnz6VR|`X{Z!@?uV~ z!zGQF1RtIlPk)43uuEi97Kp^ldBy)aO<;B#T9#^9?^j!E-~NrE!wxCy&wS{ju_<~W z^+Cy2c2)`Wt8c?VKOX}*?C9HXE!|yx^ih8z0)<0sc-_q>H=4UB{r_HpZ^`u9FWI4= zrhx5<9yW>Hz@Q8*Ok?_+e+yAuUHa;jq1npR!Wi}rD1EaC-CS-%aFj?mW1_kUa@B$1 zOaVQV0Of~pYtYv`feAC*cBIGT>EEEjF7TjO`!Hb$b0?eCmzV!=qjb(|YF6~xwPW5F zATWwU){-lj`uZZ_j)SgaLBhX(%WBUF^GkfZ!@N?nfBjpscqF_DPl&Sfsalz0-Rs}r zy<0{a>j;cFLK+FoSDs^DpD0X28wY#V$7>G4Esta{Ic`T@6^z#Wu4^$JG9nOUkboe= zoD#TueH&(DLYDCb+sug3eH*#|agM~muk@-#Kh2aD6tF2U@HOtoQWkQ)vaPt)ZurPf z^*qY@n7yRbgZw(3EVKsIbNHn2pLz5V;9By0<{3ZRGzg}2e~A!YXaHO?yUj1yBjvkU z;6Xq2srW?mhmhX0w?K!kx{2+$Yn=@F9El@2$D3vP2bV7Nkb1DkoRf+NkB@@)1&D_^ zJl>?8Ut<-w`W6)x7F7ex-}OsF$hOFMV70#1WF-ku@qLgSv5=<1@4B^ zmkhsv*L@%D@6puX;9(P5lk8NaGWxC=CJ%JVkI*kC!ACPhFYDJHU>l8!fVba4yBFXH zlabYS9*jF{|GG)2hLvoxKhT8Q%?C$sROfxEQqwd(MelvssL(&W<(Vi1%D=Da6*hT~ z5Az)FU=QDhZdyZ~41FxiF=Wo*{2#<0vo2vK;%2ZVaGR}f!B=BlEJL2IO-2v+9~YOE z*Ey?kxGYg$aa#|So)=vt_j<<65^#PN>b990tXFuz z1Vvn)=7DoQ?DDyTKslT?l|jUmZ@p;OA9y^^?lR6DBSaUA?9SP||7;BIX>e#yw@x=!x*rst-gM=SEnBPm36eau_xbP?85c@d zuBwmRo#2RQFOwhD4u7yh>7=oeuCU@27%k`eb#3J(DsWtF|v;z zN$wrwe9Z%Y&paAaQU1&CR;mwauf4yOIx^VpG?me_M-wWGD?0=C(w4@EkNItcNq>&9 zVXlAN=4o+4D>*ysQfNG?bS{jd+c$ed!zZ zar=6&H%++I+)ZPe-ive}BkC>I^)wfB6?vwk%@6={Hn!;v9=n6cDCmK;Y2##Wm%c(l z8kj$vA<|ru(ub`^Dr?b*56eK@RXlFb_)Qaoib?? z`sn>KN;PJen3JT72*pHLqJ~3;!+$X87;*U)@z`o!^3LC`9@u`=KM!dxWPiS~(Z~KZKBbne5PT;? zc9XmaKD@|h%=yoO=T6#J8#yS`km&m3opcgGrs{%00KMz`4))8mZwnJ50t9KHuH6z7 zHP1>55(hwp6!^J|%}@)}jWR{VIOngAh9W2#KXky1jX`D~%|4&vtAyzFHnVR5o|kQ2 z{gbS&Ke&)5w2e!F9jPz$rd>vL(o39oxw>h=oS~c>;a)F3&Nd0wX`VS?vXj5CVM>Iz z_)1JP$$I1Q$g$3llBs&>Oj|bz%qFy}QT9{C`G@>F*97_$GP%@QmU7_o8=*XPl;D{Y zs+v)xIfx1_+)c^vx9R5K5IVH23seSvaGhCBAu!GjGC)pgHRvl+o8+0WN(Isq{GfSk zk|h9j;l$`NRPRO30xHe@=(DO3BGLT54|qcLZ;*rJYgSf2<9uZnb9|=nXEXpICi#2h zkJUO06j37m$!=?%TOB)h!ok27*RG2hT zm#zKOI_Hy&?dXv{iQq0-9x4b~({ZKAA(f99Tl?YST3sINSXuW;vbTX=6Fv*DI-_!b zdg3<|+Go{bV@#))-f0&`16_LbpE~(Ie6q3 zD9$1l)`@Iz4o$U(-C&xn!0erL)`5%ad*42-0Ig)~IZY`>=<04of1MP0Guh#lS z#rbljvrD6$G3RvclP#6T485C4wo5qd`M-8BFOB;>|6& zXzGCZ*mZEF9`=O$y&Q^FQW5bbXg800nxC~FuCg=96tFCpQT>OrxEmC_$*@$FwMP(x zTjr0ij0bAeiybXzjm_4%F1q_mK~pW|^%EQ2?z}RS2d(vy+>*zXIBS+-nxkJmXkv_= z8TUzg0Rl%H|N9`pRu3vgnwlmyI|pBvu%cuJKQqpseof5RWqDLy?uxpMSKo_V6nD%j z_T0T81cvDzC_7_zFK7RZ>AY2QDU@%%h^6RNUiselnM0R9=`nln-|%n*e&q5Itsu*w zE`u@}5N$UI20Xv5UdZg{kUQ3(FlF{7+;C>f6j#`UnVS57fbF^fJ1p|~lx!!*qO;(V zZT$|wO{iIKc!m1xYo6c(BBcc)iOwt><}*^nZ7Kfq`H#eb!L8bzn!XkbmD&G~!DIj9 zy1B#604Ln_66_5>G&`Sh`^GK;^e*+$Kp9LLGTaZYo=wo{a*T{@Z21%pH;=Y`Qm;z7 z`e7>Zk}a$k8b(pUVrR{@1@7B@J*Tq0VN9MuJm|1%>%CU96{&+LediNfefbjZBPrVQ z_7JlyM{k;0keRH(&Q-gM5|FPKKHD^i&orW`z!3#1{%XbRoJ#4~Ce*h&*Q0y%D zEd`G6m_cTNpFxyci$8<5Ge5IwK0d<}8a2HXG^+cg{s$fEHZn1uDnnhaD(#{1F9QRi zPbL}a#5wr_E+D>xX6gsHNRo;(Mpkz_o&EN^5p9^TNLoB*iU&7Y@C7FK@kvA8iWqss zr)ekS3&+McId2}DzARq1zgr*`$rQuYPI$NCyJ1s=wD!1>9z0^Ldc-LETR-8XjNMI- z%KKp4Ya^Mh?F{yYl0;M#pRe9ZP&z$ehr1Rc4U{_sWB1}mVZ1-hN>QD?7(cO*!f(Sj`bA5rNP zlJzay&2N*SEYs)_zH6L@3!jglD+&gn3z2VHUdUa;aITx2$8fC-K?~nK$FyG0-XdO` zLHnlSxG|;-x$XRcD;Jdbfvs=AnTIOs)!fq}%z1aaTCwgb@^OPDDH-Qu1Jy`3Fk^c2 zk@qKKA>=wZROC@%yd5_?>i=LJ8ELCY`i9!@Kb~!iU1LNNHf1_i6)>Z2@#~j(al~pd z3#haB9}YhyBl)fs2a#IPJBmug!K2E9M-;tFS9G~Y5;}W(c8C`w>Xjt^{Y z-C29mkrnLVSYhAR)0TF$C+)R}&z$9XXH5P6rH7A}q{TFd6=Z32Lyxa`xbP8SSv|n$ z1FX4%49Id%*X|{xvgqxN8Ttp9`rMOBVG3|A*Y`)5KEtHsjDP(bMX32oZOdMy<9FP} z;k~oFze|0IiL^k;Q@JOAt{0=&y0%RHdbZMYV)0S=-fn=`$43voq|?Ce3m8B%LhJ@+ zuv!uk3bgj}@?l)$##px66H{YJ5p{3Jf?5IK9pjxL?ME*3i+m3~X9jKtr-#Gs%~5w8 zUWzBq|JdyvMvH4x9V?j^W92$Q?9mr#~WQ|89=Zk^oDRWX9L~xElx+`JdF3E^zfL z&<|1|7Fll`)T}p39M4Mv$I20rKFGtv#f7+m+P_s<2}kV}%tetLIaVEz$0LXUt~fZ3 zbA1J=3B^?M9@zJK-uj~Q{P5tGzgwF(_l;yn*)H2>qN?Ek*t^S zD`kdD0yEPRJ#)0T0`*X^Vda-B2Soww{J9)TXcdvb)Q2}24O)Il_bnOJ z@MJi^IM5qplVwr_Kb81LXYic@^JvGMN-#mHLVk(NOdWoYJZTC3$LIIzvIkRR!nK}& zp~W4EFW>w&o?sj5q>#~Pr8FfJ1NOV3WXC6utQ4!OC#9^XMuJpcbg>hkS&dXgLG`w} z?K$)aF4ptSC#VqQ&~}ix8~6*u)j2Xg!bMt5^*X!Vf2W(2a~ytRPm77rgMMj5gd~m+ z--mplj5>FlcGHg%mwsH7NRGj`@+v+5a-YG?YbbzEAv*0wN$IPtZoQL3VsJma6p6!M zPp%^D4|48YXA+?wAOsY&_fg2|;>;0oB0&t3qsOfxXmzHv#rJ{pBq$5;U)3ako&df$ z^SItjsOwFtLJZH?zh0yk@a3LY5xCBw@$I|p2Oe{0o2x&HH0z%fpWTpDejD`4ahb7l zgO`;ow++kJc}^9m;&=PCpc69rAfbwUN~p>%hP)F+)|x(47Y1>1B;2 zy`m-?gFy2aY&fKOuWfrb2Tnmy-|b-C^7w&RI6PTPZ>nu=MbgF6OvHm_c*L@m%It7f)tqvCgx{^ypoFmoG@F%$7G5Xx)F} zS(|0)C#&_hk_eU>V@>E66&VThdMMOGv4&`}GH1{jYA5WhuKAH3n*+AyO}b^+jZV@X-77cu z>8Dx-E zWmU=BER-qyFbO(PFNeY@i_RLu=UTDPQ}fbd`|C3~j!@RtcP!06jgU+e*AIz%@yrYi z-Q4+vt44RB5aw+A!0JaN6w#5l4O;3Eu=SRbic+|U>cPaEOlFOAoW*}HMV$>8^1s*P z&mP;9{rqKu2`x<3>$brQ84+(Ir1{yUNh)=M<6)YPpLe(boUQ1#ZMxL&CT>a%|nO__?)mH0oG~N<2VE);;wlG)v zfTnlGO&ddj{QCCg8rpZoYrRG*9e*naZ+xjSr`6qh#kX&GqE1tI}{eBGkR-|)exp85vB5wHYM^_B<*%;Ei`6^WX6Vf9G2Z6HE3(b}cJ2Nh3 z4DY4#v&yyB)Hk`~mymf?yLvadO|IONVY#Tk6Sr~2a&)*yWVC_^PuW2Y&3rXxlKvNL z)@|S{RYGV;lY1#Mmfjak$exghxlPwh(94JV%t#Oz4|bS= z6AkJNJCB%&_NiiNYGKjXVN!(I;_T&Kp=7k3oM*SX=G)%Gq=OYzM<;pjvyPjCMVmj? zKP$*YwGP^heri*RLV@T)0|HJ;=YS-0!mBHSlTsFtB{VdSFU^fpy-GGo!qJOV6%RbY z@S~l@a%?5nr-Kyh1=r%h$HNfjR=h<<>Mx7V`bj}*B;x^+C-CxjE(%tI?%lPUl%GYBIxa0!Q?-0JWVN*#x#)y~? zFtSMwDWi`kzB@ezg%qb~z)?`rF%ltS)i^Gts>?Uwn#KZVMI54v?RP!CBE{iM-)`8V ziaJ}BFSDH+ZN7=pF?k?(uzg9`o!g4gZl7>ExJOVQ9;gHn~^} zD`a1vf7t|~jbHvC)Q~e$vz{E-M2}92Uvq&0oXE-Qk+xXMRUeii*D;jLS&RO%AQ3Am za*U|YM#B}{@3Ge*nf>n`A7k|o%ky$ewjx?YUa>%YM_c{cH6wA%_6Oz*I*4$tKI-cO ztNke6%~Uew&qZTzurHqJCfYA}oCNS``*w)-IY@l=nXBnnnjQ(aSF-s)d~`gZmy@tG zbFx18yz>CYL*J%Bh*j{2~wkPuqDF`GWgb zyZ8&QAJanOEQ$|aUe7NcX*8(4$oJqWiFvU=eJ<+Uw$(ymvwl<4v=cc~|1HJxr+c0G zX1jHDd)G)1G)w)NE|$HduNj)53M$Wly=_+tn@UEE*QD=8pn}eX;(d)OE4eFgGAlf@ z>%M%t9DmyJEM@|x75rf2=Fn69NRb{|0vhf1Wc|$60jY-aa4>|!9qW>CScg=4Bek(= z;h~^>{d&sr*T+~<8k87Y=DYD|U0RUB9|-0J8@f7Lx8mc^@T$CIt8ktC!Jqb7)NSg} z>1yS(kgQPS^SF!F`Ev-R2otZIEKwR!wzraoIga0Wy41LN)qGcci4KAH1y${pY@Kg@ zT&wN#sq@JsGFW0&4t7tn=T9P5ldC_~gNLdBB9~HqseIZWv1q1(!6cH`+Ux^PPRIru zTu%)kv*VA+3-DbIby1DU9eo?k<4AH$2^HtNC0JS&x)YvJ$_~_4TpY@EuXY&oCzTi~ z2K*y)nXIz4Y>tX=_{r*^2Ez!eLLR|Wrt!Q?u-TxP&<^rX9@52r)*ThGzkP~zw-@uc@>aGba|sY7dQ^MPYy|2^bb(EEX) zBY3j=lS?A&<%GG1%cc(eUgS1UgT5|lZ4r`xQ2!GBH9nAid5Fp8-&dvuaYQn`)T?2T zXRcS_c@`T(x^j?Iz{n3CP7av8thh>y{nfbVbllfcdp6KkYoC z>ZzdTXZ=x-HF-z>-VXxmkq1Rt6J)cJ&L)A??_fuVkL=@DD77ADlL}{PL65ES3v|O; zZ1anXW^g})ymuGx3`iN6npw0APTP9FBAxwL?yO*yop~Gv^|7PhU?^cc{lf2Nho5Kb| zz*he=<2_M-uikc@zRKh8mjn41~X-AIP5h+VqPC-p%zmljEYLX z>hs%-(P)rNORo-ArsiiJ$ZnYlJ+9M#p1pQDZIrUrucYY(gy|f&kC$@NRsFX9kz1Fp zD7Om%p#YW{v63~h4*3jhgx&4{9Ft7knM`Downx{wNcvoGt}=+wgbFWbL4KtV@jjD4 z@Ps1eE>#`Z31#g7xGDjd%Fxd{Z}Ta7FES}8C~Ojb(TWzx401bDGC<`9R_nc8=`?Ua z(b>Q)fQ8J`VPg}o-_!@t9w%;$9VEi}29gzoGxEL=wIQK#hLAIQap9Qn2uM#4$nb=Q z!NDf%=CtJ%M>9-*x5r<=s>~-J_|Ch#YD5V9z$IyDjuYJ#XQgaXBsOD{y*uD3p z@Zo6AX%oBP$uf9G6u4y$@@-MJ+QRPw+LQ$18ctEjT>BZvvcEX*h(?h~T%-s;?y9P| z=Leg$?-&N@jP74hdA6f_xNJ1JgJj(;H=K{%h!UF~+fHQ;T|i_Q!SgmeApwS#Z0ERY z0qx_2Iyuz~RMi-slh=ISexyS?Niz zTh@<$XYE41`72%mg2Vx}jfSh%%8%?@uuyiI^ffTREm;lw%=#dB(fRWG-Lkk)jS< zh6K^N>qKe@7Az@3HtW@Nk?93JqI{!B#cV*v3RpW^oD4cIiQIw1`l^Vl3?M(n}#xPD>> z6X5_fH^FVbu<4~-4TGRyCTBGCtyYV=E&f@lJo)ADkmyd578J}ZYUOw5FSD!>8anum zvRHR@4c{a09T$4(gRN;V(>kg{nA>esFDFN~*b*8pky;i=E%QY806E(o5|Q?wxY8FS^^O_j!mW)mNzL$#3<;P)4#u7aSP&UeVq`|g z_IgMg)y;K5wnG@oNeF_L+_Ybtd_$|_q_t(9 z#P#Ua-wjf?c6H+488^{Xls3)I{YEkxkRRmh8Ch95Nee7LzejZC10Nre9C@?cPX3mQ zRBVj-{Ce*VFyZjA^FDOw5QRIoizK%B2n|~^a=_5Wl|h{|nzG9g(?*ic26h+e;|xtL z&3?8o=am70^O!C2sY|0;b;P^O7?;>8A}E0i#BRsk$qHkHiH5}H=smd14Xdf)G*N4q zjxDmiZ2qfoqR{Ci3pPELeR5Fl{;tn@c3(ks7k}n=i{C+2Jtes;%7*XHox7Mq$$p>k z6NKlX3HhNOtV8ATA1h2Yn6&`rBoX}hzc3-Me=uTVC$_9+xB*5KIK zd{Qdn2f%y0S|uv5>JFe(5UZ_p4hH_FNIt`6t#6&5K9{9;t=RAf6Ugs)RCxt_0h>M`(cB92bFcYQd0Rme6s7o6N z(#rdIhc{jV6#&3=iaD0NYk~D2x%-z^(2xnk_X-l`dFay^&I^K zG(@Q0%tRsOn+D}ML@*fip%WnsMx=R}-K5fmZT;KCwHKbNjnS%~cL-fwm1Hf~px4~$ z8Zp$AO=oBKU6Y>-AeOqCb?uf@z;+*6H0$Uo4CO3j`w6mgKYEnKyCed^<-S}t$_R3R z`Am~(Pip1>w|ZY5Qrd*o9n6bJzzt}D=pz#{)5&2RByep_1vrHni-!0g50;RI#jaMn ztBX=S{%f>ubUN_G581l*WhW6#eH5^#NAT=)X`Yh$qR&n_K80K-5CPRXDHVY4prmR$ zS;!S3UT*QMwf9+Z3OogqA*$4X<-!7I%BIA2g~hM*JsKr<3BMv*X#qBkz%vhtkkTez z%3lV+lo<>}?fSA}hquvxlrmDKjk|U#=s}mPKC9|&(|<=J(6%T&mK+2;Xv*}U zpr7Ra)Y5A{Q-ogI`ds$zPEc&9^&a+r0G=;{n^gLuWXwh(ll6us$?n}G&5j7n>Ql*-GgP{hHAUkDYDh3A!iK&Ax z$ue>Sw9HpNAMt(iV|p9>H1_qly;H>Lfsx?J(MvtzXSbYGZ-n->xQQZh%PTRmF>&%ow( z9$Z_HGtbQZKvJJW;GU!_`*ZUL?A~$m&tPjdpJ{boaKk5{t@MMrp>JN1hMdr~arM53 zf~@R&o7B)fO9ScDO{>pn{Ex}Bh+jV=+9mptxkt1wu#Gxh{0#E({zeiB(VfCi)QPZw^@_p#%rVc1 z4m`e(gxnUVuJaCK`SsJTid~|PYd9K&Jvn>zl?dbdpvfxl?!uF{_wPy+D<9q5xtsW| zkHeJ4yb2ifX4P1&um~V~EPwKxgzE1jBtX_Fj*^Fi$N(G?)qC^b-*V@kTg4Qca2$P8 z7<&9B_Ob@1CArY?ZQ*D{NQOQfn6zBT>cqTve@uPv? z=MPuLZT9;E&U^MGP}_$tDBq#PUET4uC%JvWD)$9oR)aF)gb3kHVtE5V(z@EHS&Ali zi&M*5B~)Sqi};|;rv1siUoB`XN=}wN+R6g4E8StaV4>k!kaJ&{9Fn^M+M|*fU6U~oOASGOkvkiLvv%TldPHGg*6>yrKqsC8$5iSz;%o|bM zs%8EM2SO)faGTD5Eo1*%W-(H@UXhJ-8+e9HUQ&s&?=_`LE zO@!v0?K$QECmP_jucd{zwI{vabWfbaua2ghQ&B-)avEF=@fGA@=HwucI;-;9|I}yu zPlL-uj92kFFU0hCZ|#aP!@va{I0R7rLrY2Tq)YQ1`D2hFI~s)>b10|XjTG?Mfi$^9 zzr1p?e&3mym-v%l+%iF$LOdqA>eWTW9QMyNKTq08s#1il&eZWeKP{zHK<3Xifs{&} zg>A~n_g>nET98u0tmMGfhFCLBTI;?Dj*bk|m^+~;lR53y_QdSnELoVdgG;SQPu68>8fMlT>N$5~EDXdxl&u zT{W@?N#f1D2)-LCzLj9Di|?tXPYt1(Ts(R_57<2LYf}B7z`rNO)V-I`d7?F|yvzM!w(76RfRBe*!}?=k z#Af_}14skIH#`}kQ&8-_*B;M-H7uY&Eo}3g!yLihP{Xvv@{K;jkoG^asz@09qctiX znd6G{n6Pj125f0zW&%_=Gni=$dPasukizKmt2{D{RQ+Lto{E9?^l;M7;r7hI{d!b# zoE;*@B&)5JlZ_3}oLKK;a;fHK&W_5h=G(WcJPJ3$piimH9_;7T#O<6g_yLS5xe=Bh zz1?>5SVPX-%tRphsD%(Bf6_hcbQb>lPB{LP6dZ)i(qhhLVljeqW84{fVVQ8u1&U3| zM02oNn}qEq$2_QwQ|W**{W6TfzE$ncPC63be5Jmmq_mV}4`D#1LpT!xz0g|xNpl&J zAD=UwLU4B_QG`b=n*?LW_EwscSMtXwreE5ec6X6a@o@%7=(0JUs2;taqT0!5u@(o#BcsS$n1NSY4oYu8p< z*4HF1<$+(h#e#%-2NGQS%!DX9wFTWqg9$2v+h{I$$|qQxbhP)*m?yI1E`h7G2v995 zZEp6n4eA-7mQge=irTBZxn!tk=C1=km5-qO$|VHmuwQXHvcvjEM8q%Dx!rXxcM8XU zlF9O5DdB)2)xo0ef#qYk&~%bnyxo^q<3>ZwYlri@yZDb`&u%&(%|HS?b4gyRiixbf@(=~sf|IfgZIqtvbOS7}Mn~=I zxRda;XhE{o^XsuT|I8PCgEquT1F{$+Gk>ZnC5$KQLVLVKk;`QL-uCKJXhFYqbT|^ws1v(vp!?tjGJVKoRlVia4uJob4lRG7^7=-MM4x z5D=1dY|VURLxV9?pu-?1k!pu;KmYl_X0|RR4Ckb@cz*`N?jn1Jymk)uH650f@y%Zz zGA6M^wG3BOhsTQOpWfsOuG(i5kN%q7$L`8tsd7iKNX6k)YFv-%XtaLe)6SYY@8SDA zGaJNm71}O!P4wpG5={W1ER(i|xE6X`trL-W)XykL`q&J;8t6Fvgp`TRcjoPGzv;bhkl+34v-Z-`Itob^)?kM`YFkz4RFY^7o4t7 zJH?fdP3vJ@X*Dr=F2PpS^fAxfkHjHZWY2Fj3BFJ!NEQwI;udN)xD$ZMm9O*>t7-CP%&v=kJKC{-~8)bWCp|WBA@N+ ziU7{io|{-`64VQOVfaMkTP4ZPllf`$L(-88d%x`at;br^?Q45Wtme}B+=VG6NO~yn zP5?3mkfhqho@SHw7LQW>kEg{kzv|268mjWk)Y8+7Bv){PL_5-UmJVa{j7(9sR9`nC zJ=S-fiAk+?ycrGXE61cGW4r&a&rY)e5%5L#9Pf)(?Yj{U!4(uQtQ=M5^WW#s%p*G69kp+O2{phDrQ;22EXj|?mso);n4+qT0XBU`OZJGLc_bU?^@5J6}?7^ zMm`XiyQg&mJe;B3I}(z|))g`9jh-*k_pqb`67grQ`Go#GD?%3y$Ykc)y8<~|MbH{| z;(|wR(~7={N8MRQHWMJ%?J_hk%YsWhX4S#Gdc!pK&gS|>mM5x-V4Baio;rfcfQ@_~ z@_-(OvFc#}#KS7ST+TO1O1q*XZdLh^p5ko)-L^G}NP*C}1iOh*xz8MME+xcg+P(Sn zXk+R3%)q*ja_18l{H=E7O4)iixK478q3oxzDnI--tMR=WP-r|VLyLLV^{m+1D&@NW zD_Qm+D^u|2HhC|V*80+mc6Buqa$wg1(ipDUcjt5v2HufeCU2zNvt<;#hE=4 z1&30kIqX%R?cJqr$jM=>8IQ*b?rTgJwhbHDJQ@F9>oCj22ym--!#JB06JvqScJ8RM2 z8)gouq08X&F`4)s07Zvzvwpi7aD8E?G3h7!y&rHSXd+BDi*%E66Q}U-seUL@y*js( zc;^had1f#7%vNOz+~C(w$KW9=G#_MBwbCA_sR5XoqEyJXd40ptQHN;BS~mKme$zm@ zmgP|=jrwB=;_ol(RVF2suQ#C`rY?El#;50Jp}CihTM78?3rJh`;T=oehs2G6(7{tz z$60~=G@}g?>YCQO2HS<(ck1!?9yM=gT!Etyid&R+>ns*RID+ACr0~M_+%aSv;SP*S zBHDFQ_+^pr_|oIVVmOvh*In%sCr@h72f4ELd*UgR*28@P4ugz9Lu2uipDStb<%j4a zxdukHL#Il2;mA~ohmJiIU5Zl4{jdVy|k0D zIec>|bl}?aA^A-hn1cGWQ4=iTJyfxANu(BUut3E&R)X*BB8C3W|voR6;_H&R0V1Lwc&;RTk*nj3TdAb65gILD2VOwfa0xE41VqXwjGs$;0es; zA6E<7qa5xYDd$=n2+ak1x9HbDgH;sX-%aCH$Lrj+PUoeg_Bn%zu%r80U}P z;Z_o4=stIUn~vLyrPcUG+aj;H%tc%8)`KcvgIIJm9Ou9xrONa<0ES*ir!J6w?S2rP zTRFDYRM~BZ<8W`lr2-gmz2CpCBhE9KZBO)ULI=#E*{LM~-vUf)XP&uY!7Ml{O~maz4*9HIrn-tdGGv9-3MQ(3_{N2-%8&6>jb-T z!2q>%7=4@8ApQ*M4%?$KC|1DTnHKU(|AJ^%1EYof!u7_!{-T-v1Z!_bYQ6ypEt=zr zZM9kEs0bsvtOCUz(!kl0&5}VtoI*1V1)K2CJ=f`XPOQ*yr0*57SX#NZq&zA9BRy$) zib?L-8n=HrSiIcxh2{I8_Ync#UG6-T_M3CFizK9n(j_MQ!EV-m3iUc?Yu2Yi3&Nq? z0mH-_-8oz%MS>@0xgW1B`$?1OloAeGkO7~I?I5lx^N4h85*Qh5b5raMv!ihZV;rTr6p!WT2eu}QyK+n1_7l( zKtj5^I|k;S@80_xew{gcpS{<+*0bp2N*seo=wpx1WbYmZkK)qhPW7_{tost2JC42s z7vNeOptO>zynS8&-Iu%=TW1T!mBQt3F#e}p^FQO`S0s4Al7d}X=Waw{mWHS;*6Nk-3}a;o*anXA$|L53FtY=J$tZ< z$SOb(m2ens?yw^Dk8n>F|$y7za;4#FzI7>#?GgPTlJl z*&D};9mJ2b^RJ-5GB<=I97JXXycr^O!0oS*#WsVOZ43$ zqkRl<<0nf#-?WGCGr40pY)E#74m-J@eCLIN)4H}ldmO-%7CAM2;m@b4!}ogQ2jy+; zsJ+w6&sI+45dvyQm6mP)b>+4p+S{dn*rGtkl<@q5#R0Fz>bS?**m2~~@)IhG>WaXD zz~3T4T~3g~F?J6|F~;6Su|NuYaRo2rd3a;AZTGhiR-NL|K|L@7eoF|}Ypgd{8#86+ z9JSLhWTFB`UKH=3bq@V=q@>$dMEV5q9n{D^oN)yjCss<>`y^1_$8z8Pk)3{?SnD|j%dllRCDf;bfziJ%*FRU-dr^2LL4U4Z?(M1 zdO(cWT7w$^V9~OAFU!9%ig|553c2}c<^Scsg%&=xEo1HASE^CUsRaqQaAcQmJA6%% zAfzi$|D!9F+ThQIBjdIQnJ0;H%91e}bKEVSAvBu-W+A-1*9OD>E6RlWPjfa&yKS}JABTiQ4>`D}t) z3V-Qh4A0jI>b8$&z6D{W+h>Kt-+v%bQ29V&@oH?ew=0+D;MR9E=lMd1&z;CK6c*eF zaO*m9KP_Jy`T8@Y!GplK?Vt6SsMC(y^uF-J#n-4VkBlHdmqt43R;X#z#S69TdyQRiI(>p; zE%6&JTCKQB{du~@2_o!9l_SVQ4+hD*@o!4I;8U|+kNgkk#oO)_W6v_BWQR+HO1%*P7_s|(?8ID1-Vc?N$fk`B{!{0N|w3%{;)dROWsqkyJUNM2Tu zIc*&BNlP8^AkKJ3gBI=%PEQCK8EuT8LVTA zZ#5xTez2oO6jBB?y465^^2%>6ym-(uWrZVb$weiMj=L@X_{`(gdan2>XJI%`&l!en z7f7Uw47@rF@U()R8L2_?OxMl+jY)?y{i^u(so}@%hX~#+qcm|ZgEVp+6|r)U;WcMA zsx+_O9!P?^ynNL`ucIx>oZ3L?H}~=CnDq8RDv)*4zCLD=l<(Wl&<;HVXON!E!J3cWPcV@JJ4=X>$nWCeGN)@j~SY_iEZPXkkn zlyqE<-_xP0KV6+2iz_{>|Cyq;)~JN+U$94u>S2`M9uFZGwErTlYz@!ZKZKAdQ~)8s ztNG|^9PBTa-MvF>pGao3pCgH3ZWo_(tKIS&eos~yu^gd{R<2|+hfu`-B0Yk%>@s1& zn{T_w_j{!*YWdiNSvhCE7*yHThh*lJ^z_gb?0?F8?c79C{wk9cm7b_z(3&ZL zA-pX8Aq#0{BrVkBMhB`zXGr9~??xaUu0{xza3PZ!1c0>}dP%qbT!LSSOn@|@ya#=sm-{B* zK)DweLye>DVbjQS?UZd&F$!B}+(UA_?CZ>O9P;zKm&LNyU;)lS-}MyK6rx!Cip*ff z7%ja0`Ms~!(z|zjC3Zvj7C*@M`aTkcY(?k3TN2gtKym;pjx`1DiEnYV?^^PXuoah{ z=B&xs<_wFTf|jlc6zIsui4o_$(gV zKgqkwXH9za6wU?^-0u9^q6@kHIbn6bDcZPS`h_SYz*Lr2w03E#ywprS}xP!#O7_tfS4Dd|O zFTK4#BC@~2>@Ye@ps^KQlsZ&?vw_3^sVFV3TvN!UD8RcO)MyvVs@?GcK#i-_{a5&# zg4*D@+9stZe?-Jn32pJMyYmO9;fzy2iDl?lC~sSk>ris}YyG^!hqmEAYj9-q_M^ZR z!_nHqrlDbFta{g5{MP;h~N8bOIZ1MlIsg-8xPy_+nxTCv4uwXXs#UDiNimu>&lL`$bpJPib2$H1i=z}POX7;61G#VO5RjLBQ)4(n-I+ND z&b!LWCypwCCU^MT*0R-PhHz<1;$T&DS-L0xBn)J3J`;CA)(i! zBIH<=#336q@=iik@C@tM>1Q_u4c z)fdu`?@#qnMo1m*V#6;W-0ZIZC=n#<`tP?4DqH2&#GkgceisV4-X7zkKthXeU<10a zIvIj@c(%~4Z-x_p1X_R2W8|kjkB-Ilawq%T=KAHJhD%?M%ceim^)PM2>#YXbAH+rb z(ZZOmMX(D#!v$AXxC8?{!kL;tWU3Itz!k;&TY*C7*Gco7`xPwAG9v~n; zJ-rd(MvMKf@KX%Qmx5a^<=*>z%Yjlyz{e1( zQ_wde0cE+KC&{aIMFjccj$h(6HI;SYooOW5mb2#;cbOOJhy?39brtDt^x+bGfj32C zhoAboG9b_Ty*SCK(kVx9e^wFqA*S7k7V%P06t`?3W;u3D>@roxVcaWKK)}+~T~7#h zc8MZ+E>PozoIWRCxwGv-h%B7ND#FxLk6Y&prrv#Yf$&Su3WpxjXw+m{Z8^R>(cOnO z;V%pH3Bx~9^0~pT$CMKgH*xMy_#GTK;{K&_@SM~G&vC64Kg5VR3X*rmF*Y4`(SU-C z-{Q^V-?rAm)F~4HFA~?#aL@_JA?^GYYOeImnw2)yiS#?H_?64$M@|P@v+uu~>Cy#t zZt$p#iu1n!8-lh5;u3G9Ze9+kC{DZnBK<3?2(V$!d~v%1qfe^n$@GcQ;$g=VN%DzX zqo!!^h;CED=tQw;Ju!P+jEiG9q=VbnXuE;6^;3ow%S4^I;;H(9Yk}&|-6%E3P-fjst+vh2w$$@ir;(d8G z19pKX;ov4i;7YB$gt8e81gEdARJmy&8C8X&|PrKV0`z2(ozeJ9fZ`Ysl!3A%kgcd)vuIGq#2$ z8DyrWu_}~Va1%fU#P-(7W!rBVB>dBO`hme)q_oq2ze>CaA|=109P|0s^R?tJXy>_l zvA~nBe3=4tMQ@uH7c#j4jd)cW`5J1K(L_*@vn;_KA%q9)>>dRl-T6K2$lRB14QH7L zVuHsGAyb|_@GN#r4PuZUzRE63q6;$xZrp5*WOpCTH6FYHK$&ab2oYV8+d=rO+Whk0 zhz#A1-d{yoyB8~Et=~5Orue?xwAG*_Z|vQ%2`!O&F0eBMrI$&69KG#bl>hDEB3i}_ zAMT<)1x%Kt3Mob$9e~-v?o&L+KqW_f*(WX}3;=GOt%@7$hE(H$otTaL&@SFR8@4>wBaL>E6E%U+$|?27f)(*=-^ z{Hqtfb^oFHq*m+`IRHzxxt~7V`wDCHeYz2K()be_0=_1XTs6Jy&xrO`q?%~q2;ujC za6924&PRYh`f?r)0(5g!5Q1imlg5sVDm0MM`{#ob1iMSR%j#lAvlpC)iTNEq3Ay~9 z3o-@fj;BWual!xfu&gTfGyE>PdTEHu@uAsY(Wu_{h9#hJ5R2q{$lUjrZu+09ahJbH z?!POr4M_u!>=tI83K}?tsLAuJ^5H)Y;^K|nO%`B zX5A-k!2ujsq4()St|D_)LLOZqapBX^KpUajU8!1Agw2-PP1^e(0_fhB@g;Zs(?i}Md$3-a^k(a> z#UAmYk~^3tlal$2tGX@2b)vUq0_ad*yC0*wA6sw_-ZR76S4ok*Hq*o_YrtGydAI8X zu}hTMtr5QONF0%1K}}}ef*oWlAV9JyM&Q;)mJ7v{%QD$m-Iv0um%G{P7P4fW1G9ii35 zA#6l>U%TioVR*s2?Ss#Dxr%ZOQg5}fWnMj;2EjxPI^&MPsuEk`^@z;YiXJ3Oto%33 zT0{~D)De3y4S#-Ge7>~e2--moXQSY^=%@(V*us-F!B2ol$%?mV-(`E>IFX97;vT=? zh!M#8;Ok_KhriXx_gB{K@X^3n&r-(IxZ>)6;FCgw$(-5N1^3T8yqOPdiwLezU+ADd zdE@Zc?yc=5R#PfZ-}5-Dye!OPS|ZQ;z$H{wD|o=W*WOB@Hu*6>Lik^GLqph7*r>HB zz=ka8wG5z`o{g<1J(Cl}Uk0<=Ibt4{&P!cebZu*`RQyA^9l=}>CNj`;pYha)mU^P% zrEI=>ux!d^zpd!d_7|QBI5AZJU~Pt=)pmI>Ff+R-MIcBjWa^~!GtUlN#sld$_f>JI zLyof`ODDktX?~>*{?9U*x`WaW7xwQGbg5@)LYS5AgUSR!68#C9XC94IaNX?O)>Z$< z$&v9@MiOZvqTrv5aCdcx2~iqB5ZMx@+|#iwa43ueFlG-Bxc`mA+Vh`(C7Df`)4pF1 zb1J`4|HdPZRWt#Wjh_Z-N*lC7!MY&?jIXlE%4?3a;0g3Tcb7js? z>LMFS+q`yE#C@}0fRi;uRC9kyGz_a#9*8XG_t_cWs#y#^YG+%zxoqTefV=5h)|6lv zp9Oy6`{u!)OCcMKlO^tj2|6)n{{5?4Ut8bXs;vtk8qJl)%&Q{Gi~0ox8j;pjM9Wpg zI?(|8S;^qWZ#9$l0ZO1HBmgYHv2sf|u0fqPjEoQVsf1@9X)6MhZWw|TTAJzbaW(pF zt%BsiynAxUfWt!C4HsqZ$Q7Ft*rkBcny7psCJ9bYM1f~0QA0^zL%GO>f>C$Z)ibm1 z?H*qor*j#lhh}S{Fgx$imfOqc2iU)$&v&(-Hu4V>oa8P_HgCT%!=yS5!FTJ)HOu;~ zge>OgI3WH=_=A>|kd-f4>F}=a&oh{;Ki%H-f^H1(>s)o;sYgFgV$zL+rVEhOtS!1Q z{wf26%xj?KIqlFr948#Uku)uCFSKhS=w6A-|5t&foa6Td8j+Zjql4@W`yEz z3pIH@dJCanG1#(M>In9~znE@kff~GAj;jeF^{v$92n=Z^5NF_UQ59Mv?5rZJmO5SI z3AESD8T1*(^XfQAYpDm-$w!jAgra)C!&A{0r3l?#8|Cwlet27g)Uh*{Kh7e5&lwJo z(w(QD3$1;jWq!;o&S~vH=1=VMKsGFm?={NP_U-i$*|Uo$i7#iLzZvO=eMSXb@J#Xq zVyT=I2S2oJIykfX1Isw8_o|0}YPe{3DU}$VmwF0ev2HJwuWzK~hC}qOyFl}s@OBvg z&d+MS1i=<8m@4#C{qw_P@+*>=_PgOFpTZsI% zra4XZ{YWSd796zfiVM1zUyzEAyEd5ltGt zD-Xd4K)pr%n^l@a#7D>%0X>KJc0s6gxpd4KFC(G~sG4ed69!c@J!6g^u68PMh~J;3 zmn7=_+hq0lMX?WP4?Iax2nA~)RxFl_x_2xI=`LS?Ds%U{%HnV_>z5m(h=vey{Zp5gb?Q=o|uYDx7;=f-w)X zJ{v=hGxjNyvC9vU`%%_};1=KkDL&ZeM;SY@8 zmdKv_fj6V4nxblgXk$3A#|!d|*XF?+d``O!2B802Wnx?gSt!#ohy%NGg7{1UXJGbE z5@TKs9s?+0^RIR~3U{T8E|&2YS7Cb`RxEw{M=%_c{R=LdsEUnA8?9@}*83R8Yn70b zQJ-xqUQ21|mC};>S@3knj__l(q62K$3;FUL(U^W4gS7A19)xE3`ZEyrnKFdx2hgfP z@f|a7x^9r@md)fPf1zYlSo^CY<8Ro@V>Zkpwt^mb@EOn6s@?A5``GklFxnWUEmZs* z!o1o?t2gZ##<~|Ol_nNgbljw>2rrhU0A}S|#x(K65$u8zO2rXi)6L&>=P@zLpJ7?x zNi7gWoAkEOt~l7K!GyB>lWF7hksT# zB_4!jMAk>@s&9s{8FNXNlY-aIhZ2dV4GNfOl#KiMYGPl&ajf&Y(%-)RXVQ^&y>4<^ ziDj;k5Tj@Hl=m(FLZ|smCWUMjr0p!&vvV4f30lpacBs}3r;-Lsl7fYMNVNxG!}2Kn zNw&#Wl}n~8F58l(h)?UHHyLlfFDX6ho9a1J+z;_#fO7w89}M31b!aQ31c#%fi#aEGvwH~xrR6TjX=Khz z$i&{hzVzB+D?G|8yvN{-O!YPXlIUoAD#vX_7CRx6t8aD&qr}alz9?lCO>*dO$CGmg z&Hy!#4PTdQgTgZzNF#5e>Qwr4vke2QaT9s(Y1 zt7}~k2Ngf%V8X#|8_VxNkuePBIRSm$#b<`2V^dRL2u;pQRVE+lIKfP>vATR6E7WQ1 z02&?axE1@{iiDF7Gtg$f`M-@xzO&yWFh7<*FNh21lvd6WTkg`NTF#lY z4yz;T`TnLD-eos9d-8gqXL}N}gqunj`o>4>j?ToSF>SGc2W zVZ$HtM_KWUZ8ER^oD@q~Fk){vzjgYV{WJzmBzMReuw@^>8{8%ddQI0AvapR%d7!i? zdmrj(|4M9~KcvRy3-Ea11;+(Rh}HNxj~@}+{Z}~vU!Ga&dQb~+z!Rwwv*QGoy|8L+ z)j>jW*s>_uvpdZ^4jji8fD{SL0P7v2B zbTH~@u_h$TvD_0)3YN1WaQSRqXr*G;YTA>8k+Rw7<4G_B_5>gx=;wDReIe?#f&f$D zRaqFfhsHC*o%6<0eDmuW$Jg zU~|imte(MxQtk0BjiY)uXk}tcKOdR->8Q2KwORGwWsPHZyjF_u>*%3l=7JBg*yDvD zitj-55%2@yZ3AvQ8TrmXZZzHX4-$C*dyMKKwJ!C2hRqY1igD%%AjDtUtjnFi07Uju zfZX0bxz3xjEt5~w4q%s15Vp<_bo$<%pP@s0SW>6*TUxpk4!dMLOogMhJ(dQ8NP~O> z+2-CM)K?Q2gDDz20N#s@ki@X` zpL2q|PmyJ=c46WFR}YNdb@1~nBb&XJRdeoB+->Rw%SR%ocWUU?&S)bzI!6LoUvj@c zaZJj_Obirz+zoG>&lMbnG7LPOW_^<`Zbi%hmr~4e1Wu!ruwmeko732z^7S->9lCkI z>Drw5HMU3Q7%WHUO;8Hz)5E7DD?NXOKPQI)6z(mU2QqKZV6%v1CDhOO_sHK$DKy#m z_PhdSi5!5y2!DSvyck4P?!FWy&l_2nvA@k7_O*|k`7pcw)j-$h$N+X3V%2`TQGq}X zB7(@+kSWZ6#_Hh`p7c)%WP)(HAk!;;&CkK=5du1ib%Wd8TTHFoz*c+VgaAs1FVM>- zaUFfy6Wg$f;C^bYK;~&9WkcX`wj8x}j7hc{D+)eJ2v|w+_u2-9 zdf~pa{OzK%CC~kX8en{7-J1$E0gjX2c6)cxiSlhR3)zZ9#XYF=Ili$}Jj%pD)vHZB z??d$>kg1IW@pTH1W9JvGo*jQ65A_kOnVK1rH>`=E~zXzN?t z34cxU7#G?+{dvsq*p7#s1^Jg#>1GP}Eeb!%$QD~-BFuoA1TZZhgQo?C6~9$_T3_xp zOfGDAq9Xa1XN6`8mWV24ln_OvQzh_?Ur29X>A?m~gTw?KPe|o)?Wf!CkA>vyUcK~# zWrhJCiW!hk{(JSZ#fV5DO*V}c$paL?_xDA=GY(qB^*`ZEQ56Hr(({O$Ohmxd#-NdH zJ@bFxwHi?TD&_Go1qcxyE6|vNV~r@Q)?@O@>Z(B6V7e1td={LI7$|Rx$>6*Y%cYny zD=`=}w_r?=Xs>^5Ii}T)s}l4w{Om;thS&tux82(rxaPu6%QamBEDI(-fme6og;*_O zAvA63du@K&Q9oA+X5w?4N?_L1^L3GYnhod0GnP--J72knYWQXXolg02ba8-4G#$XDRjVoq z0xA;7Y{WEYZ@&0oO?qoQ%VLTW-)~49qjiLhoeMeTT4o&5m_38zqQNzoZ7Yn%p6}@< z_T_`F*`jOwito^}OoCb+5c3~jk~yV>T}$X{cwmoK@FxS2_VtT*LJ9f%Q&6YqKVxyje0wCOE)`Fz{A%{*yINgoKn85ED=RrKgO&j|4st?(q8ju^Pwd zPAPE~jSQ`H5w?5mK9+Jae;zp4hWVj|ML8>W0FZYl0*6F1wpIjvf# zn?+x}e`9K*k#=uEQSy>EvY@fCkq&Jp0?B{clINpxL>^p=2WUgg(T*cjm%;DW8Vp7E zu-1;ab4K_UXbk`H!f>28nG#Wzzg%0H9n7eHqx6q6loXPDsf_k4FQvH!kBdvoNTAq5 zF2^UmnsUxwwY$}Zg`l!B_V_kk=o)!8TH%3T4F8Tet++nsd-(2mHc0l&1#faP&Y{lQ z3veuU$@o()HEl@sb+oSQVT@Pgck3V1ImuVtoZbzu8l%fzC{hhuPI@oNBp++J$Br-f zF>tUf9Cn&_m`b!v@J3ISDrx;Ol`04m`ZVl&&jeYyK`aSN{UkPeRm|`-Du;~ zM#u#Z=5WLmk>-)j%Ci7IQGH~ZzYTMTiJ%w}n?=*V(urh{6>|o*6UlTyN%pPZox<6E0TwKVon1`a`RWdNt za`IKuZW;^}E5Fan$~PGi49iq| zfge=4>sam}st;G(UBB%|=(?rrm9S?zV5kSXWFISlDhUyaJe7G^9+#$&p|l>F&-dCuR(`-k`_f(e9O}O2eip}nsa5dCzDxUi z4-PCPg}Li!6{;DQ8{Wu_+^|C)<57;RosG(rmafiEcZ4TAoN5zw49!V$5HWZ@cPTE)v=UEW(N*Lm zQC^(4C2=vJK0)NK$fwffs?_Gt&xYJ|5;-E}Gv`qM-EL-&gP^k?X;iGC>KLcCRnyXc z51lQ}2evIf@x%$Vr&9ko+*eNNNpVl|%~EX{{&Zcm;h?u}sS9VT{98{$QOW4J*PlSg zCbBz6w&yl-@H%rXySlo*@+mHY7BAc9d{l8R zAtWZoM#lQ7@9HI(glfm9Vk!^d87Yu~O^}=IMm3i0VM_@}08D$;=#bnQ9Vyw7m|T`NJnY1oCq|uWJEE3#eE0_H_lUQO zYN~!!9{0xT-F^TB&dJAl_$6&o0OAWFk}3C&*u3 zX<7R+L1fBW9T9<|HT^s!Sm~2m8%IN7#l8l*#zegc*S}xVmg`2^Bsy={G~jx1<+NdFUXR zJ<~f2D!oMM%4OI$qPGqZj>Pj)MJjQ1hcOb_E70nzdQ_z(oJr16ec zn;pFiAIeU=WV#P{tiuNf2PcS|HJH~G9r6PLHmjpPH^$WC@n-^>+yx>8j>nYg-p61% zATAGNf#vMXx0gpvZ6^D9(@6q)5RH3wPH37+_3Gf5VrmqFK^xP$OlMI}a)tA(#0Dl$ zJ2lmtcRmsiMAjIIkTnlv*S z*`bwB#2y(C2ZPKhFaj6>ve6Qbxfszqi;fxwRX%dasn-8+os0%PV`Igh(|j9U(>X)Y zh`KLMn0m?ik#~BDtPIjO z3ZF#%Hk%We^}yBYvc2kd>3t$Tb^rUYf*G`P2U}AAcY2gKJvw`Ag1+t?vnKHZwPnI+gN(*kyG8D@$gu7Xbn;*qm?)d<`2b3=Uq<5AkZ zIEYr{TP?gaa2Q19afl2E0tffJ=1y7nyEUt+uv2&bMb}fGorLAbaylf;1AANrFE4De zu$OmdeLIF75x(pB8e?O~-T0Z81jU+<-to1~DID^-nnWk)$_pPTs$GjH6gExkej*}@ zE1Ho1;8KdP)L%>L@Ay1LP4A;-VL)mAoY+F+)@8gs*yrWNeEpS@Vast}OZn=TLT!VO z!kZ|M=MY?sw+T-^dcC9k%2cf5I2&K@0gL9eCghs_A#Jnr5ly5mV}K}j9j(9}_*@-~ zi5G6OUqr$M?Oe|lqCDlzJCJL&e9NDW{g=A)vn2Aivg`gV+IW?1Z=krH42}5LUyDCZ z$HN3F+3!-le0q0tH)q|1JY^+v{tBr4;Rg$aTc+P;W7GdiQh-mQDWjF*s~*Na~4i0>q0l8|R$kwR{KNLmjIFaX+rss89&cmm2R=IBui+O#+F z%o+n{y8q3I9Wh}E;~uxpKfc|W^4q;iA4*8aNTk`+?mX> zb=jbayBs_b*+l$P)g-Eobe25n!U00(F{4Vv=0x({k``i>Z!QZiDBF`^I@-xv(vX^V zm&}&3k2}N-zQ;-**iGD}R2eoSBh8|z-v1PNVP<#O+}K;sbV-hCe9qGsllp`9%NFVQ zu5@#YIR{w#dfb?qAAPMj;=c z5KHGmz(zaF#iSv?E3rgwkwc5Dm`5(K|6kLg?v@_US>92<>N!{+~s?k$I z!{kg4-ez?Z-6X{q`+D(q+)uOC=lEZs>p0V}Pl-4>fcw35ux+7~+mgekCK`I-!_ebA zzu2t`Fg96ao1jTDqWtiF{PulM02nJh11;>&BegND!00N=&#e+v*0p*?&{(}qu>SK5 zck5M-AAAN38`M6qEM*l|6iIGbtDRLKF7~)axQaw0*8d(9^{)_US;DiDf z9o{mAFCP&*!LQC-l=Nt#Ti`j)qo3uc4uNOo%&b(IN~;{4`|4_YAF+k#uM+GmdDW!@ z+$1WUFMH~}7k>6&Aq=q{8KCt%E*2R-i`niXz_y0^>-o8MWrh595v_=lDh#|~zgMLL ze^NCjM`Cs#{6~L$+(x3+a`F>k;K-a;L4rIxqi~WR&8Qrb3CB53+;Zvo9yWjOEWkaR z2g(E~2QpBtExLvtI8_j^8B_k64nXQr9epd=2akQL7PKe)7=*e?S#=?p66@eiNPMELQV#)-qD`xQFes>Uo4lS z5jsw~X)zC-y~Xrk#+o7t+A@VLnQpUv3=e{-rE`6)iF@&tJ39*JJ90XCIAzG^EZZ;-W|% z;Y}I093daVCVLfz>su~s-1xx$tR_uv%nxo`y!c%W8)h^GSdK`&?-WNnKTP9Pe=f4Z zTA`ruiZ5+^hpHLIS{-2ZXHwp=Y4ltFh4Df-*hOOaD+Suq!nBf8yPAFQk^Sd{Y7T(0O+j5h3`tLXB}}ON6@$xEHg{tVwHt)T)7TPy%%)w7G91NoWQv*IV4Rnh&F^N+* zTF)f|UZdG{GIy?f z_Mn~9?|AN(Geu!mV-#}OGL%^B9?I8u{5;^kgI21Q(-W5|>Z(XlNS+mNN7ICSkbHsB zOGZ8IV08FXXSNFI@J%HajC+a za^o+*y{+i8pK{tu#`RQeJAFosJ1UDAfPUV@+F+e$ZOZc!4@@pCEoFj{PDr&U9=K4~ zgpj!(!t>Q#KX>KFB+hWE*FG^gHLOHW0fyN0>p5Ee`CF1Q)DKd3q1arrc65KK^V$?s z3Kb4Wm*DsO;aVWhKalUVrLYDU%MHJCccj93n3~)9x?U?is>>VqV<;{!v%%V*ze0nT znUFB+6Jzi`G)17GgcMkQH13k{gWyG+xser`v4zF2j^Mj;_`s`b2Bd-6_oq85sWV=> z9AfutCr&w%G#A?VqI$HB{uhB)BiX&9InseRh?*;)Q?mz*-H=0y0#>uc#`&XVLtW*? z?5)3Gu20GbguM>dxNE1s6Ceg0{e8F@GSGpuxpPss8=AY%r{se*jkpW{bspIJ2REwt zYhbhY`D&=-DfpOzZQ@v8LcW)=Gqw{o6Upu+%aK5INE0}##s^&w759c*E5p36 zQ1{vo{j4lVJ7u18H2%L%kgbv2GjsqDyK8V?JH=ol)_3ys%)3RzI8$o>$*)}vgAcf41=A(bS#<~=y?o;X7BSTj zo$9VnD$SDku;}s?BvNHeW~bU)WZz!kgm-pz{foZFA0>MHeZJs)|7*lO*=cT_aZy`# z(KHp?L)oPf0WFq60~!3@%x)uz>8hH(EXN9+1=-YVP&m6cRkr;S2xjRXPlvPJPiV(=sU>*coF~^C=_2cW<>V4eT+ySQ zs1{kTG1=$72Bu}RJa_sK-kQ^}!EJtrUY~772#-ZNx7I&p`?h{^@kT8mWs@|7@5+bZ zK-ifBtnLnoXp_{HcG&#apj})3dOTy%oYJlJcK9Q8);+b)~ zo{8Q`CHhQj={SE^%lDNBw4>>-m(-Q`gjwi}jva==IEGd_QhIj&D<9cIGLx_RU7dRh zBp*Fqhynne%KdIfTZDz~H1~(bhUT550I&F9!n$UDEn8>R-S>vj7u-Q4@99|$V?hT& z^cC#(>akqcQa!i%Zjh(musQnj9a+7VhIrV&X%B_mGi)b*;w}+HcwD0wP9ABOEfiR& zzN2A_DE@>hR-gdJJ8|C75O>mxv{D{sl(fr7xELOg&q->YI8& z9GwF2J&?#GW<7p$+)Zc|bUvo6-SKcybHvwN`t~bnm(02o1oUe8uMN>O1>Fb^i;V;y znE9hdA3ktJR>M@p{D^5YIKrlYJUK8E+5*pP$sV(%R)_3?T_M(pgL>*ED zZi5Jyh(!L%fUtuRQ^8k>Ro4A=0dJqJdm>MI`qIVzrz@qRDh5SnPNUkeW?i64>R>=? zI7Xcz&y+e*Dod$8A8e;a&{XPubbqd}rNrF;$S*RfaEtjf{=XqmYN4$-2IitZr!zr5VXe2$l$^_Kq`J2<3 zU)v$s9`q%OPoC-uEZ+`iUV) zyo%(3-X(C7`ky}EPX#H~&1&(>4X_*CbHy*mg)}OHI58qeK zDal=TmH%MKnzArE9UWBPn}^@p|9PDYhCGhFSo|o9)sRNAmVnH4H{JZh?KGkT(dMBj z3T7jlJGo0R#5@7$is=1}i8fx7$BG=--C0D#jsYhOInB-UF_?U>{PrD|d2iN7fLA%i zP*0rq$HgXR53zthiui}!u8~n8deL(mkP$WeBwYSPctW)_$dIYqz3ox{6DLNvzc_hZ zPM&m8Pqhp_+Cz7PvYdWrBu&B=P4ssuucnxT&$wkA0H{ZZ6coRO><;(H^fK zAXH+@32MY{1MjsC0^CB|`H*vo^!2T0gN1b50-SVBJ zB`Pk}X@w^562I_QSqm?1Hd*U)%_(2j_j|IUr7M=5@mVcOoa^0RA9c*=vL_SpsV!!KllhJDZH%I`o)44u<)Wx54<}{pR{;Q%FTT!LWwbu{m_zc==>7; zS7;f@^M}TVm{KHmK}`isYeWeJ{Yi-ZPK#=(u>sXX3fsUD7srVdm zhyygnNf1GX)wOXi)|~nUwe}q1R&Y)c*kis2GQap>mm`u14;6@8Bfw+Z|q|F}FH?W$Hw@|l-O@)A(JJ(w2i zEdN!ZH_g}Azw1ZJxBE9PZmu`k z#&2jl5vjz2)sza0r5zEvV6){etwjd2?%M}(xWh3Ia2*7*#gWAbZ7E*e4Fb|+K8qhR zXZ?tu+1(p&NyRGle@CQY zIbO%d$1Q4KWisMJkH~$7y2Nj`o&(I`SEK+ed%t4o?tTezVT9U6Z4hM?U80nLbazQNB13oAAVVnKNOyO43ntRnu{>tQod0dX`)NT@Gw?s`){X$M~nd4&?+5 z;S0X0g4I+3MOk!5u#%P6zQ*45+Ir@`B{)hzn+FuUp1ud{1ETN30s@v(LWA} zSYG$~KH47cs6 zh~oO%#@C2uJp8|^xY2(UCBzc?MWa60fIfct4dW39Drg>eWF}bg|uaRmj?aQp`herAge#zzMW3LMK zW*!tFIJYRtHEv>Nz|=(mLL`J$*7scN&1g`g|GT%@;$;dYR(JO|{IZ0segGNYowFX&9@Z4aOsw;T zeX510BgdKW+cV!9x{Ju1EW&CDLeY3VLCc zy^Kgo4m>zGctI$=_OR-EdCec=#s?9IsV40^tkrFCI-H$@v|K)*KFeAPJXw2l|G4xT zq=PGX|Jwg8Clb$>2Sr0MS~#^g)G2y^%KYJ1+v*F?@QdEN9tMrGwuldu>Nb8AV_E~MZE71kvg0`UBmKl9SEiS4% z%9quYhu<|r+wtsmurxeZIEX61cA0fN6}-@9OsP!nct4f`)p{{{LtmD>Z=^lEF=gy4 z#M58A?+$Cz(D1xX-k)QZIu1DO518Zzg7l&lwH_l(jQmCCVYV50hXzwJ0RM4jTJd9Eu z1e%W)6ajJ+@L=bPBZ2y^T$T};3n$Ne2941q5sBY<=~fui(+tLXwW;jC*mi-QOIP9w zqVZ6HZ#TB7f?~iKv_mHktB;SB-4D&I;2xKWvc+cEBIFG3O)Txere}-Q#=lPbhg8?s zY6-mDu9`e8B)>+udF?Hqw-1O*p2$$Kp%j44M5?h&sAD%#&U_9u6J1D<=0pduW4!9e zCNgA{wfA}*v@swoNWT}L`U4lim(@x`=x;q1yyD|eD}dhbGvZioCatpCjr;L)bT35| z{65ETEQW@PD%|MS3|l`RcoMXgv*~~FgVvI<&TR}P3=+3;`bR#R6z@>%HzS|nY-=JL ze)2nng`4)a7eqONK|I=?oi~XfE-kBT7fj)t*~Q zF|>`8p#v7OEKUY0UV;@9-5cNXYf!xz><3~Yf8Lz+eM;q~j2TMdH0cVC9RP$sW^Q8S zvk~ULxOXs5sT1?#4^SW%Q`=D)lxL6iKY-A`IGcS@+ zz0uWN0jGk^`|nEy(hrTJwh2@EI$RA|Lw4y@wh9K{MWY-=$S>POuX~?SEwu+8CmllL zv}<*WkRw0iJ(+`5!Mk0T_5)(5(v?Bk=BV;mZ4A-(+B<<+Z(iOJsNg@|Gk5K|VQ5*@ zNEoGE$XbLwtcCKB*4f}N7HJU3H^x-iAWqfiEEI#U55K0tT+hxHM0>Fel8jFjCnomE3bWZT5^G0Ow=5O zJvV+W>q2+WtK=88&A)!Haktxd^#h8Jv7zE6^0ePv)QKSBQF_*k=ujtLC04S({5f7N zPCt%-@6ZqkFbEm8`B-Wr^>5K3Frbxuh+J3)X-^pL3f=?#PH@B8WtE%b$LWalU9Qjm9!}Hop>k zb@zGuFEW(tcjwB=3MKr2>fyJ;mm14qW7pUL@S6;^BeeBhXM2GN+i?F0`e3JKmQRSG zCuc3f_kAIl!l+)GIst^z+!9##7L+xpaaere!0tmgBi{n#ptHWuF1!Uz^e@r4#CBff zYggco?^>g;OQ-X|gypxHNze3Cq78`~t%*1eXp@UliW_+H?0;H)jABm2ScFJlBl1;5 ziej$RjPPKP>d;m`68y|+=!;>C*WP~U!CX^ie6uxb!PT=dB+pij@$M6%_@tKDRcG7g z#h(yGSoY6Pv=_%=@77r(ZToxs2rrDpckYNAmVF4xXG>EK*BF2KnBQVWntYC2?`iMO zCyZ>uK`ro|7Jw^KAFnKVi&K|iqWT+?^`-Be5A~_L9+rVtvb8a?jXS&u%;2YV*K`jZ z*|M_mgWHOX7yD^1KfRX-f41(&(wP|Gp5Ap?cN*RV$QD*p-jB7uH0EFaIfz-gmRw~C z(6TBX;(~B&gvJ(8oM+8B zCq#2E*h$#B(|X4j8%i7^N(?Zg&QLT(e{L{p=U`V`SCHoJstm#XSeGjM!e?+t0wu*O z`R|07>h=j5QN0(xY=`}}YN#@F!DTn58o%;WH07R2kG{>$VJ`D0y?0YY_=e_sUwvyr zsP>)eDomNK15{k+@KaPqzjvVpqhGIe_XBp%2GO}hL7h_OsRDZ?XMvR(KB@#JW&&rD zA$hA#7oB|2K*WZaf!Wvb$akO)CpaIH`))|NZcE?=Ep5dF-c+QSSg+?JDhSx3XKSmg zr^nJgo7jPP**ZlZz?A>E^8#~Dd0kVVDD^;tEogT(-~swrES2YA(ih$?%Pg%T7Hr7R z-SHPa@034?RZ1$smnvqCfiX?@gHgsP;3MulsM}8SPeAbuG;LdTIKKJ=IdB zkb(4!saTDeX6q%Rg6Xd&Dc;;``RBq9g1k+8+m#{b7Q4V`%kVT@C-6F~8C{a`OFb4z zKnameL^$A_*>wi9&-|nO>G*pQ2b?Y~>aH%tXsLBcC|l;^<#$9xhY?1%un@k{Q(=}7 zWH6qhXXL?K(x){{K~ZEzDf>`sa9U zF3c&dpwsU6Qwk*o4Y?m*%3cR22vZ)tR?8l~EtrYeikqd5x0S`ygvU0_>LH@#1(~~R z^$;5hyHsz@A&|ldTl;>smldfLwqXYJwRm8F3DoJGdy;7l5V4594hEDoVvE}G^xeu3 zGDg-6^|>3hyphQPvzzOS#hN5DVXsY(i?m18z%OYw&K|r)Yoh%Ihv{_5c?N8s{xeJS z#HhRn*u6ETdH45MViNfTIsl%yClTV^e?a;|H0u_mlrTn1wN&I6V^BZ`ELyXFL`YU@ z*?-&@!KeCr4*v>&JZSb9vf#iAP+UiL7CS|pn6m~0jjq?>xvg;q4WD*NX*hkRcy3$^pAE7lsk~*(jy&(yYX(*H z?bs=H{n?O-HFEUGW{J2H#c6~hno3GM?3OYrN}A$6`M-Gf#)9y$mlaKu# zbACz9e0=v;kvGcZ%P}FrVdGAX(D_D9STGP*ea|Q%;UEL|Hi=dRm$(A4rUbJWEa^St ziqboH%a}-`OijCfPXj$aK$?7!-tl{_+KQ-w7Ri(k(*54?U*0X+Dr$q;nKQvt=O=gF zaMWgjS|%(#oGXb&C#E}I4j|*xq<>){jW}R!Se^W>KT2v%KFMd08Pn{5;$8U__DlJy zi8ctYj;4HVnrnr|n8L7|#n+PaHqBbGq!*+@smZ~&(}^$m->o_gysUg1GP0O!Bm2;V zV#*>jY^^xO`qN0w``t2noTZs`g_TZmM^><4(*9WW$)^hN$F|KrjM8E;r@vLt@w8!r zjUN?d9A5$`Pc7uiY_-{uN?TiJghHZ<*E9sgg^1&=iBm4^SG}3+I^DwS^)rL`?^``#u<)+^YDUXzT@clGG2+fpi3Jo_=Aj z&i8={$+`ARZ3n8f0zduHu2H`lZ}JBoG#_<&{q-Yv*#xSvlQ?wWH(JPDjYGOk$F$HLQ`r#daiz?7?;vEhC~t7;_+EeKu>d1 z(|4oc_rh;F%=CM-Tp!Z!QRr_8Vv#;_RU5U}ch=XBl{C)UA0%qLxaqH~ z-gu_nT#|;)hkroNMa)xQ&5x$Y-)$rWV_b6uKK8ae17ba=cSvI> z-!9y!!f#ZM9xA&p0!6DpV(mD|G$RUkZ{Ju5a)q%mS8vH6sa52c=H@COIP@P9vY8+C zfh@bfn71H2N3UN9!S)Pc?dLsw+&FAr>0`?F+gMJWTe)Xlx#NQ4R2z5Y5TodekYb!G zn(iy9{`FQ8lZx>b1Wu~;+yoE;su!6L7cl@bixTI63uLg3X98|8-&IbY#hE1 zuP8@$SU92}wM&84yIPEG!dw{aOR>`m*gyZ|880W)%{v*epAE!2xd8K;5N+Z|3&kwa ziJ=;WA9sH@eb#pRQUM!CWSijVa&n<`Z5NeIV?)DhPiw8`^SUNn#|+NNIx8N>!6sxl zJ{2^=ER_RJ>DuhVF81lk8rOd|z8aw2xenfEb6xR&FylrIqPA8)sS%ti`^5!2Cyi^B z8f+LQ0+IO3{E^nb+L(!R1A2Ku-fN41`+v-Ktv{>>fx+o+iynLT^vW+YmK2Og5hwY&x4}Rf3;C z5jAEHXF?R^LX=sR;AEn&xtrT4H&90hhtGX&@I6neD_sFnpoR*x^O5#l(9eY{VR~Jj z@12qHI|!RkHiP{XE*HJvsyCRQ$79ib2Q_FgK_rS%$*fk(5h>j{2gWPPTPC-repst! zSOI`sBhnIh{KzJXDDufuD>DMI$FK;wrY!wy1GIU?hph!~x9id9M#X)bOzihkld^h9 z=Rv6pgHtN(j7-l5QQ46?Gp;|t3BlPu@>QiH=mu)@xhmdb&P2XZRZ^)B|7^fjLwUa3 zZ85DT%^_Jqf8@oxDwUq_iS@@<>bRrh`}8a zXQ$)JX6_HNwF1~^8{3l)k={THOX4r}eWdo=d6{)XxAj0a+b|c%PrSMj$XtD#j0tpW z_U&^#1mmNLE)g=&P=7f|a3UH+7v)Ht?fJq#W}59RqqW{sOCaF4;dX!2NW#HejAX^k zQe&C6Y=GD>#~82cBbkPd)Wt18Glw22tcRqwhpMHiZ$Cm~Bu`ptk3NKMz{5Nf-=IJ% z#%@SD$w($*ZMo$}J4N~#QNCcNJ)Rxs#~5jj^fKN|K^0KG+Sdx?-iGxlNIAv9lG=dh zJX%My2}PImIchZWmA$_|1zW!Q=I=+Xf)#RGG*yN2%^8G-SwZthIQahN&OIMqItKD4 zN@GVbHy4)~Tmqa*kzJBLxe5w)?8XVWxQodZ2qHUkx1+1KD;#76-Y9Q(3`1gDYMr=v zB5loG*}z;GyHbEu>fI$mC{Q z%g1hrEr^ox`AqdLy;;bdwSg_P0+IcBe(=!8no*l`svfQUyVoQaR&z3vUSvT%u+>ig zyWY*A7?ZjJ%3L>4s$k1B&jJ@=W%N%bf=A)oY;G<`4h4Af8Qs|{l8^4HWkR(_G;03xV_H3W^S z7HkRR**j@QL)rKau&fDVdxjIFK}i8m*Lj4dkR@ui3APF2AT8=cV*IlqJuSz^Vv-09 zg1~=NfIFP@$RUj#MgycdLV=hXn)0~Xu*-MNw;xkNwE(n{TXf{*6){8n>cg`}cQhZ% z>??07Lgd-GSw_a=oFO?cvh|!jQi7=4dfT~>z{{K6W*B<_62J88rUKWB@f2FL0hHvl z*lz)YPXQK@U3ozIM`&@XO`$$QB-w@?;&o&(JecZ!jjto}$Hx-`Uno-FGtaNklkpGw zJ-L!$PQZHL+rlA;J90J9Hem-3R>~-D^Yr65`o6aXC8fzoYLE7A*;EiB=o>a=jW)A= zw#g1nl^VIsq*(ZCKpm$E;!v8m*nuB%u;arBDLK*E#F%D5*6u0##kn=+`wneaHZlg0 zI4zTbXL>6O3#ShKhO!UoRlTS0(r01oi;Igw*@2HYMlQqWqaqRy%z+A2)t~N%P?D(! zfSqLmK%xsb{m)Mb?p$_81Vneb16o@#Ui#gS2i*a-9qqPrNv9%>kteQfL9bu~*nQse zOn}5SfIM*n9OG&SZidH^?dqk223-_s8?RV-!_$SlB;}m1cwO*L%-;X?$HaK;4P;WT zbV?ygwpaR#DA5E>wy6=$kX>va-|Z@f%8m_&Db^^%*UmW}QbLoYIZa2&P8Z0f{xg8E z>t39;9}QDN3nvkm4A3wL z<%1(FTy8!kR_VI_CU8*r=@uGOpPE;Y-N=sCHo>~12z%TU&qnPiy8t-3ZF2wBL?j2c z+Kdt&5*(tKCk~9wJ*!S9q9zCcENUDTNT&U(K;AQ3Ih%1V4uxolY6;nL%($EsziH$mQLF+~;QF|BnZ;gfLJUhn|W5uZ& zt#?Ng4j!{0c0*j$K0$k2ZLbzB&tBCW3|u_w3e7#EUmh1Bkw-Z=lwn2g6}-S`2{&n! ze$3Ih+t0wUX#|mWw>`!;cJXo)57{0AqC`}Et$spu7Ds?_XHX5-w|BNP)sJYO`60Q>vB$prL1X#P@H@NO$qRW@Ot8U&UyA_s>tNoS|Pef zK|q6-yVO`f7_fB0ZTDgl1v6#grUB1vHJN$ZOLn36pd;Xn4@43y2syc_|X4O#|cMrygkvdT+@ zs1)UxOs9o`sGtVm^vq{L5DV;=&%!+LXvv}tWn~(IFUh4&(!@dxWY2!S&Q1O>@U ziEAZ9fTEF#in(V_J+^$z@Q_@-_lLOL2^#f>dz`%Bi-er9)sp4@3GrG6ya6XNZVvwe z(@I7ecl(%6Cr=IRv!VRjyD2`e&3h&KrGv{wA2e#NssCyH3F^_j~sQ5Q>PVZPBz7YtIN_W%s!|$W2$A<`U zP&4~Wk;^sXn*w%O0Yd>?0mPrA0(Vi4bRiJlv#1J7I z-I+RpRrY-DK$cEX@Io_AW7A$3cz$UfBD*qL3p*J@{zxK#*x_-wkO_{v-HR%1c)S z8Z;qR*wnMG6)J=upW2`>wjXn(P)yEGLKWlYvv%3-j~90t!yQzR3sqkl+G! zIEfJAJ<$J?(X&!K#6(>V?ZAt11Ha*meP**HkS?c;Bl9%97kS)kEo2aI)?&E8#6*Py5o9e*k8Hn|3lME#!v=HcE#m}O7 zVq0jMuDoozd*}d$nlfrV?^8WwG^DLe4dMk+P2z;xAzEK(M8;{uWohpl3mQYvecNY< zH~JJh{)IbrGt-1aoF&bq|FUiovVTT2ZkbPno#^`h2$vwF`(4=&4q91S!1%I@C396w zd!am6q-UoP5h5CJy7jAau{pa2vIr8Pxl7=D&|yLwKogBgh^tKmUSOHy2~*A_a~kT- z9L+?dyhWtXL3lvc6T%+wI{BTAuDMus zonlw~DbD~mz2LBa4sNSK!4+(_*USh2>C%!_4u0Gu){*}YNE~R?Tb3`LP{$Kv2pz;J z+7^o}TqwljNFGN@AJ=;o=Ao^#Mo1SoyT+~6_f%M1(iHT!J!O8tIy-sAh|9UW(v*}l z8UK5a??6xq&T~(p0hS;zzw`Jt>gfusCco3x{=nBpg?Vg9oocS%vzr|_CJPUWzwegv zs|vKyrMSsP>h*paLRbH%`ox<15DM{?GKeJ*_F#iNttlOr+b;H1AJ0{fByHhzs7SC? z2W0^c>KYWZz6Y!yuKGQb`La%1CuUsm{G@k)M?0OflqkP&)X(`99yKlk(9ar0M8JV9}V?4J8AJ359Ou9tVBllKtbQUDfj6=@GkG2jR zxC`F?@SRvOGGM(Ndg5M2|x6#zz^^d`q6xU*u1*YI@s#y zr{iI`<0v)BjKChnE7qe_^gXG^Y@wJ2Y0j#e=1g8tQUaN$Txf5mWa?Q-(Qno_9uG-c zcRU01_qnS(u0&fC(SP-;e<&VIsFbuf&4QxbS!XCpcLxOt(YjaZ7tq+)msGhqD^5-Iz)dN{dvNWP2}{jGuqB&+_~^U{WuTk>)Xyv2-Z{T z;$PML3PbOSPknCK)$wxs)puhc@R`dNu&B^G zL?uCqIdLB$Eppf10<|n?Xn}tEJ!Fmq3*N}wFI-Jb`s*aD z3~boeDnL5OaE}zN7@eYO$=}Pccj-JfbqdcIBS5SVVw;wW$&w11Pe3llt*|KfWe|G1 z*W?18{il<=f~2_VPja>bE}2sLoowGWfT%coFEw}faxOG}Y;+=`3jF?}Jcx_=GBOhb zCfjEnnrc(H%Fw=6nUJyaFH$inyFF24ulGxNfPfXu9G!T*nzW!)z>+9=#A58T)c^G?g_D`6Z@DkJg zg*F1sI@9y6kR(v`Uk`1BeDxq8tqygg=f=^GKcHAYmVU;~31A5E8>1THL&=N$NhO^8 zFG*iz{}+@SbkklXH=02yuo8bl@p+baDorTNboqekJe#ZAub!r|Hzr~$WCUj7QJcBEg~Mg4T2ON6@RdC)k;8?P_e(b>e8|DFKfhaM_(Z5v8smnhoGJpBk*MPkKQ zgk$Yjy%n(cy5(;;@-f2oi@NHfrhH&=f(h+|{`^>hsD<@Y+nk#xU#YC_Zn68*J1z-` z+v}OWUk!MebKL){`)JC97eFxfnjJ3ypPvzF{`syD#~Sc`vkLjrAC+R7^a^G!4%5Vj z50*@#9e&dG3RU=~U{N-W4Z9AXLk+W79Xl7WVo60j| zb+f^qkkgmHVWh3>$a*_P8f~apFoT&Q(@^XV(Xqv*X2r@A!0n)FslhLues6}{A;)dT zpz$B5Cq+Iy3xW!I37f_}d1|6$rsI5Ef7lg;a(!!nj+g*$YUZh8kTJE$ZXht6z_U=T z+5dzV@}7S7N?yJ-==?=sD}U7O*-5H&k7Z&qi0|QQH4uIy3oQ1Y_d{c_g+d*l!umqj zP1o_)pKT`72vTz52T-cmVdxs-PMX@f-R z`+p$6;i`l2ZzGGf2Dtc-%+~gn*wiorXR)$r0I!EN6KZ>To#3eVsY)(?TJI3FfhBda zB9$P_3u5)aI~?pCW)p6MR54$~4BY}dfSQMC&aS{44>fJ#X@Y%R@5GhpK|)M)|2|%t z?ZUH}by+pel&iSbmo;^P2-${g&Dcjf|Mq*i?*-eEuG zoXV)vcV*#|xLQP_SAcOday9CvQa9WXdn8cwuW}P~Z{2uT{sDl8mM_E= z0JDeUxUWH4nz01x`SRq}{3^_pr8<1YR{G_wyu^<8&l%!KB zkBaUfT_z;xS49cRhjCq$Cyrt`Hc^*jU{c~<{?VKb5dxw*z3%=CB>mid9Z9j^V&fe_ zEW!kLnC2#lyrnuVfJ=}qpac0wA)cOTQF!xz z1A;{^5Lr%AB5~?sVMFF-RsCPXjbi%k`YU6W?HtzFjsvgs5!rY5G0oh89V4JdXOJ$7 z?Cq#kEi;B)MWwRL3hFD4wHqJ_y&4E?hcdKAIDIWl?9%_t3!1Oy2}hzamKoFlW;#hE zWfOeZzIcAEK++Il=LUB4A+bS$sKq;eFg68lfN5zla`-GL!2rGEX2*aTV_OfM-1mVU zC_Ep=Z5R;TXpmr%7Kfawl0`&dfIFuCz16Igzy3Z6pXKi>u(*wmeIqgO0QvlZO*1-R z6>v1l(FrwJScSnvRrIawu_vfom0UguDfPG!`2j-{WNKedCSx^~~JTOIeq}gv|H%l_bnp0Z~uuQTD z)6>H^hhFLn@!G9Nu;IFzshV4a>RrLU<2h@|Ww4?tHZGoFXlRoE##CTB8QB@tg=1Uj zuZ$hiPtyp4`Ufay@U};t<$Y!4U{OC4*=QR4&eL<4W5P-r9vMkLk^|VdU0tN)5FxX=Eaq8CaZDZbs0ObH}QP-fq zg!%UM{;C@`KHx8Kta1hd)9+NXMf~M85l+y02pc>fQ}$8C*hxnuWn_+@5LzcR^_79u zDe}6m5>hPrkAWZKB5T(7T<`<9kAs}^#MNFAY&g#j~!i>Q4@>Dxd zV*}Y{8tb3Np4Z=kn*Qb6s9$=BHGpteLNWP)m#`iM!dMx^`VwrNM8!S{Gs@Mp1$F!2 zA5fXo^bl4 z`)0vp5U-%XO&;5d{_qM%Hec)kNQ&!eYr+<7a>3c z^H;F4v^^)w=szudo)_UM2GLhAm$0^{aL4g1=q?8I{WvYX5MJ;Io`~;YLPM%O`4V*t z{5zf{8tD8m7v9yQghVRs%Fj~57#pSQud8(c z7}x)|6BRQ|pULz9P82-z+=yYQsyqIP+l7gqFZzvDBsXuae-xmOg3t{IU|VBTN;2Fd zm4RrAOA8u5Yzewcwa=11)jwh7&=d%ebgDtc-HXhgF;h=p!82>Un)AE{4xv7Aw0H2} z-*=2|#5SpU-NRL7*h(V+Z%120FYWTc{ks5)J`K8dFq3&9gTu{HH5(h18SDy|f&_ za9+F@qok&rIYzhRZXhinSHOSyt2OoBYr13QIllC1LpoaA-+#S;4Z+(6^ldl3#D^Jh z-nalC>lLbs2*AW9N}Ue3Vkdb*Z7}1CR9nE=&hjpRN%O1_fc{3Y!Szvx z{~h%Q_vZy^pOTcc--%nFvQ?zfsf;ke~ykqsp8)9ahSp% zIWh#@r!aLe1n%4e<;A`*&yYupQ{zRc=i@C9aHw~sW>mjpQTjhHnZbxSdSr)@fr8M4 z*RQ5+v8~$FD(+a>0_lFO)E<~o2jno^L+%>=&GtH8U$>eqv<=U`^hm4X2BEJFT zC2{7oce}Xa()NqIJN231oJd{^wb?bf$LK@6qWxB@`70Io^M-Hg9O->M+30 zFZ^cIMMN*pOJPWB3zf2`t1LLp+5Z1K-k%#{!I!bd#O~H*{}$Se-ZU6wZ6Ese9ZR3~ zS@v0l`)4?y&4Iet-w|KBdQ@6x`Z2Yp8n$=;m#HyHa^t zt^z1QQamzIG7!}UsT*^bt`F0YT>F9~ts{fxY7U|$!_C8I&B?(w2kCePpQhrT@fbJR zzrpopFS5nj!n2-xSuZI>%1Q7`8{qfd{-;uNXNp}@_iSEZEex2p(@gMj#afOz8tPj_ zLxrHh?<2UbrvgDI=+NENl^ZJBJ9FMCL&oK~$Vdhmfatv>)%elApSYGNdXy+rhw-u; z>@@^myMnK+EVp}_Ojd-a4Zc#v1h|D4&8#GSSOU&#hy1G;4&MMqr8&!+FEW|Y^XA1T z$mf6Itn4!G=r2)RiR^;eR*VaWaY+=5&AIS`s3cXoJIFnB*k$561fE!L=jp0~w8w z%Og2_=Yg%2+H~ky$(2KQ$kGE8^Zpxg|8KHho!@0yPXC*9KP}o;Vi@L=)^@TP+EH&N zMaFJuvpbzu!&M3>b^yD}i6+~j^2DKtrOy!WgqkCDdNZS~EG@YSacek6^TNiZs8jnJ zpg<(f65yv^p%Etq20{2II~L-3s1v442gw8a9{4lLvI6b zcHsxgMhJDdJ+8|`Pgilh0XhkR0jYW^?es^j)q(tn(fqLm+O%S{z3NeWN{k5M`OyRx ze2OR@HrTjsrv^ppu64TE1Rs)$;@AMrUTqZ5J>1Suq7;LS&eGo2T@ zB8X6VUv>TcNtme`bNIPRE_s!IS9`+O7xczsZ>o@t{`03t$~0v-uoBol6JGcS$PdMT zYTYBkqy8xVU5Gv(0>I!ZL*fX+!a?BuUh()pQ-OAj2sAlBJ?&7q*wkila<*6pJ?T)- zrPrlE*iYD`35QC)eR`wDYIMD*0iVVCt#f)sKFOA!4Io+RBcd?@3XQToi*vmZDKAr) zM9-EUFJAnj=6~__|DO<7q`wkv$i@CqOvSrJ&B3JD_QfO)Wq&B1eE@?8U1BG0^`hVu z=}P!mB^V}H_!WCUx|1!%4n1-#!Wy7oAS}#}1riB;x5!!so~Y7qNDg0$StJ>u)4$vh4o%W5@SL z*edtFfN`e=1r(F`o}s3zDSFN%a8yxNeEnXhJ7M9u0?$8(ogyKo82%<(ADE*DHc{(+ zBFss{xdco-ZG&QRrV%_ST^*TH*KfVeS*HYTZK0ui#Q5teY{JW=VinVC|JcP;d~_5p z?DicfXF)z^xG(ifJ-#0ov0_wIR>sM3$alV(#a$^B!v#EbFpn0%pz{>}x4eK|obnb^^MAhuOv6 zD4_?yui$8(zf3_K_SP1-oD2o(WbcFp^kiQHd_AC&MkH!a%}Of*p6aX8bxs+;&h?2_ z43SRNLG`~e_5W$;b2%M{G$LcClMCLJwC9{YU(iDgzxcDCIOnPca9GaHVrDlLz=2hn(+D!XsCn3`isl8d@gbu-K)M!tnwL9&vk8c$R@+#whp&uxXd=0~i+#+~%MU zRm-4M#uV`R!yHX6vSsAoQ1@|e^-*SDw>$b_KoEY_P!WpkuzCS@R-Bwv6pE?~v$>4vJ z(AsNIAS_y?+Q4jayq+7I0RDWQI0sN3vQwHPD!XtA4Qc-)o>}Sqr$1*l4(a$P?c9SI zGehz0c$ieIx+;kX#C|w>aj{Jb_=g~`+eAy9oQ{F4tQY`Yt5|@%LhE9FZrwaFuJ7?; zr>veHlTIM*;MFVgG~)$(v_QMDK=wyIBr7Oz3%fo0n?WbC4_DtCpb~^$2cyj&PcT<+ zt93&Gy-%&C^tQY}q!=IJe%V7pY&7JZM#at#SsXb0m+!GaYuuUsdCs(w>IRYuy;W*x ze@60B2S&M_<+r-LdIq*P@=evp?p~*jmA={j4}O8i3*+kL$_nKTQ&rm9`tt4i^64q* zh_A!?ztFsi?Ekjx73h{v#;k=j;oQ}4QXf<%0cOrY6(}Cl*;bx~fEwE@7+I}9=mJT6 z9@aNm>}P^4@4VUrW)`&|VnU&FK4Y!3#mx!mY~T5|8#AvqzAq+9UUxRbLTmbpTtf(} z1x&du-^yAF-btX%k7jl?godIFZ2rCdeW7zGS$7Pu2erZQU&C+cQw$HvuGoIvB}d+4 za(#K6VZgOPqzB>dlaiW&Xr*va3lJD476D7;0_$DrgU@6F@}rMFX8ay5;eE@&4N z*@(`OTs66|^D9}NB3{znDs5TOhvfTmS{7=xim_uQ&1X@(V6tfm5T=l4v7xh=%ynl$ z5>0WCT)7GP1Z`#3+rMAIl&C!FjqR-)5_ zGj!2oPa|9%fr58G^DCDbs5wJAnvo-;$Oe@ER{FCL6jMu(r23r3*9(vZznK&C_pR5@C7W)6M}#&Mbpi}s2k6kJjg_+4@KI7pbK)#07nH`k4Y__ z^Yr^4-|^pVpqyM>BF|^BA;Btx6Dk~>&+IJT$StfW9b4!4T>tBckwhb$_=69a%$+Rs z!+&Du&{tNS5yWeQfszA0#CW^V(L~S&Zoxp*AwPNWHw_^aPsa!$9D86$_wUt2tycE7 zN0nc!-9xG8y2g#5TWsi)ZAnj0bHT(H@7n1{c-|+@mp=L`82&~&b0x6pOzLV`fdM64 z3hZ=>4r>FWgOEO6i|4Qs1B(Lvae_&(P6|_u)0W`_kC@N}uv&r{*s1vLE4W!Pr?HT* z)8Aj|%;3dBUA-5YA*DL914z*kQ&BJG#@j1q3EoFmU^a}MqZhWJmZ{{bF7Jq%?D6)N zSfhdpNhjs=ReZ1W3v39}QLA>mc66+jls@tXG`8WRypG?@T%pFwM{%DATVZxn7njrXrM|w8 zY`#CmpxWjNJ{QL4j28S(_lZr2|9ZaG!aN5*l8N8ac z?W*7Jt?MsM{Zi0GQbPkT=x8$|=%$tCDRt7tIuaQoF~^HN2<|)h2r(@i1JQhKpi@@8ZuVE}WBPkTgw?!#?wet?ZFK#=%BL+KU_EO=CDII;H@hpwHC1 zrEfmq9^oH<;Eb?r-|&yGViW0bZSa8#lZ;JEiWR%8a@CukuiHK%++VcnH^M;>5uvE} zLJ*`R6(o_?{a1mEXwBMGbe*ecw+jUYW#Z?OVZ{(iJa*J?lWsjf!dAngv?UM?q%_6y zMP{f8+Dk{AtIK!CC?}m;WV5;R^HVGZtz=Z_g5=uJq-4X~^rD+Epvo_VYK0j|@jNqG53pZE<34Q2_(2mYp&dWO=3rAji( z@K}LHeM>Iu0LtilWMX0>ro7%mw2H`tv;TTVi;<&Nm1hPPx$ADOIIsiH*O(F-f*kNn zAtMeKkRKgm71R=-=0(l;N_VAwj;(Sc8?ji$Cwu1%i_eG3M7Pc%zcXiokT%v)j@8L{8DS?ZJJ5WRD z4LnEEh;ud1TCkwI^KByV8?I<#auDanGt*xo$Q2Z%tdUuN3X0PWCSSK0`&%L#7I7|t z#7yyexR6r$!+d>#O89(gDJq8g3vap`>i6aSlw?PuqYqATV!?t6BAuC`A{s<4`t6XzcW6c@AG?JPk)s9r+enU&UL-FbB@u@ z)ZJ9KSe?S1^JjNn`_+p8kVCISu6MSwq=fCLzr`*4AcKW~QE%D~PmF+?a+5NYaLt?~ zCG_c^*W6tm_ZzK3I|MBvGL}FoE&DnQU5r&eG%#v^j*ZmNQo1Offw8@r{XlLt8n!&L z{f#ONUWKVANu}*nm}hp@EjyBm_mJEEN^n8J@{p&bM(+ zyOtI@h^7xVfwDu^*F{X??#7`gn_rPi&E1YcJ8dp!3s~T*QyQOe5X-=N7;8mqhH+kw z9l}ZERwzS`Wj)Iu^RFhErSA#9M^OJRnd=QB@th22+2>r`y&|-_!_Cy z()eq)GFKk&LtkeGtZ{pz;`Z^WWlnb%J+?pPCtIsTc9)fX?mE6S7st;c>2FZ*oxLpS zyY|C?B(#7d*oYG!8$ftdRRARu;1m{ld3jsc8wJzsEiyRg5!^NR zhQn9(D?%E&7ZNAA_1TEv=huB5C8t76I#o_Y4`r~b0M`%uu6762c#9?D(}ii7Uc#H9*O@?fL2LLNDSQ(?R+|IVSyow#)FCw7 zwGd!k<#}_4X?@iHay!Cy`Ey-1iclj?9Wl)T~FMmHs>&BF&Csti%ELqOl zde+vABi=B%GIT8TMOTD?T~_~{I_$y!ht(VRcV*vE4Lh3|e4EDFJ!j)+hj=M&SWruY zgJw07cWip*76+;&&*^OF3aZp>Zy%1^d0|{l2kFMz*Lk|wkVX7+neE5t1Ig8^W$|s4 zN_QsGpQX(Z^k1D|;Ye>R+9}NhUS}?02({TRY82V_yT({jBLA|!yf2NT;9=m$-9U+1WYT3LGH;XK2eC2KV84kAQs)~RpmRccnUzih;iCr7cRoF7 z>ZVas0fhMB*ma@_c|Sb7p$hHS8%-P;Zk$(z@l=ET(~Q*G?i3-e*L}wRiFL?HohV5g z^wjN5{>J+Zt}Mh@OuGFH|50+9vlP=!S@&Z9GcwwhmR-v*)sYP}>>V9u@>wiGYJT{+ zzJG~t=$OG}K6n0zl7IerrF!ojjqbX^JlwG=fpr*ST0pSJ_w*kHw*23N-kUGXRya=$ z=~9g0P<0<{AM-QtRwp74(JF#R26}N zY5}FzO_P^&U=#v?ktQqXds)(R51>qASQnBhS`A!%ZzIS$3CUuxR3%6!lXUP~pBC74 z7PLf?`LUiom7rW*)zQ=Q>+tOlSIujE6GPi>+_Tgq@{tBC-&wyzoGCI*Hk$xoLt&-u z$=FT4oYK2GdS$itG5>k{(m8Q&>|--JgjR;MF225gg{gdh)0X8BE>@D+L=j%EsF}LN z|9==;zh?s!b;{XMehYaTEdG&XeGjqF1n2xa&qI6Dg}1O`JcI~9lKlQEP>v5+=8Q?+1J*$ z-z#(uniPQRyEVEW#c+>wl`mnLu)y=auiXA+N=6{;5Z0RI(uek#JTLS6K zW$P)Ph?1h;^9OTx({2mhTz%!?SK>7SRbFE+dESAenDJJO!;6b3$sEj}siir>Uticx zwgvf<70>*K}G$Rv~SvQpIUnXb0(AoA5f#M6|2Mhvq%;@zTq zNTZ|hUZkmmS&W8J&!ojr>dG=?Sg~GRQu3sBYG^FY{jMd#r>BKf{B6iziA#@Ws|+Z~=erop-xx8>h-PyXx# z8M3vgMx^_P>(jyimw}prx8{>xuJ4zMZ*MBBD%T38wBZ9sUHbnnzA=|8A;9qq>`Xq+ z??iJ7X0jXirz~`7O0^}1g`HYFj)5+GJ~SQ~DpPy4b~i8J$;-a6=Ycg!t1nERcWoW` z&>e)&@nC&^INv0J^oBrZYST@i%u*Nke(#^u1ju?i9MOgpm z#X|HKTbeXsd^E3@Z-t)}V7Dq0X^bwAH_=_477Vu1z(^xGBTEAXLFe*6BG4$aVRy3{ zVdXqqeM8m@g-!`Th|yaZw&3R(DbcPvM@zd2n)}MXL^&zqv@N{pEPbWQ^AEUxuglPI zw-}=OM)+@<{e)WlR?P(E(TzoPapC%-*bqcjqF0WCm@u^H!ATKaFHBWBra zAg%LfidjB?9A0Dgd9$Egq6;EDwgr|Ai#qH0jeQM0S>v^@Y(BXQbwuyrcsfPg{ujW< z3|jcp0X`zh`@^nEdx5=Nq`lm}OQ-B7)H(|wO}{6|hUxzPisH#5e}!vIXpvU2Rck?d zy#c2`Sd=krZgvs3G&0mlNGRBr(Ne_d61t;F~TV3oU(YQ1JPVXAM?uvWtJ>6?|{l5~} z^6TAeOtj;ov?A5DC7;)tp=kwDnn=iQKqRvPGW$|IpOM8eH2F#+JQZ);|0!m%da?1l z$S3L6mOxHB1Q5XZn~)dtFNbA2EJm5j^}xHUDLY&Cp-@n^Sg2YB0`uVBA-+iHJO;6w zR|ubD55pAP9f3x325F>ng39%s3Q>TmBuPEm#BAQ&x*yJJ%nyoFsX>f6gH$O)XC=&AAW92N*w}k3Lt*dQ5=*DxYxIe*3_Vh)~`5QhPT@8Wbi?B zAqJ7Yx)-6sa8~k>FWoB0O@{_uSa@G|{OGTN)k*q?V)2i*UHYqSyg&Vom?&1f-DSw)C^-Aqlj{tXUsq@yx^z@mHv4Ayg)zufb{OmdY z%G^6+QAFdIWdTG6(~$00FpWVdca20eBGazURj(#kigWWM&dUM{7njZg3pe!fFioLgjs`*B1(({_23xj9?;+gHEu{lt~?w@q_Z=avcQA;uzJlu&FceeWdd%rN>e(w zyQciVdpYvPVh!EgQLctk1(?|PO;<7cvC92s#}AU+ZOkucJ*mueaw+<_^Y)_mR~^xB z%m!w|ozh3g;`i{IOJBZU%8`^&mbxZyw!k|~b^qu60+lj;!6>vJm(I)F(bERRuN!jn zd<{i#4%er+cr`J(|c#VNEBVIn{iwc+Tn@&$meKJ;nAe4k$if|F^sH<<3<< zBUDdQdx5D}avGD1k-%Sv2jfP~)cZxuXC&ppk}awea_+mASRz-udr!z2tmJGUfZHf1 zdA{iXz{8|J#fUFCod27*u{|NIJVHD`y617w3g-*ieU}!4h<~q~wnOkpTUWX6Hiq34 z7pGZh8AA9l^#=lwusD?T@1Wik8#obc;2v1l4PgJ$g;Z8b`^2D@CV|K5rmRKep$O)PKguKNu2d*|KJz+}DuXxU`$sy>ch* z=k>NBPd^oK6}t1asF7FZ8AkKtY=OJ129u0SHa40=y5P_Dy4X_oB=4B*7eQ+UA~Zt0 z-)m{O{*grO{wAa$z$}wFl@DqlTkhHZpstx+odfOX-4e1Ip#d;r_R~A^@f&LL#DyIu z@pGb>2=!v<3RuBEqh(%5<)hN8=;TY0C=}E5oBhVEMV-EPrFpn}l?W{?qV5$;hDHr) zXU@Cng|0~lFno>VE?3(V;D0E?pK%n2gfF=Ce8O}TbajVVfr;IUI@9~mdvLU|S1-}xn>{%5%xI;q3G5}RcX8#)j zC~F_ktQk@re0~09amhPyEIie!$AoxJrOYWPl zXsuv(Eg}62Q;DPy$LmK@Z2_<57;OWat|~&Iz*rf=!4M1fxFV@n-&TEeS|v?uu|A?3 z)zIRPyU&oyOHjGFH5M5Sx#}QB_+mX75VB*SC9m#sB&gBb2JemS?|r^L5dx618cms1 z$8jLppuAu#D=TZhe4*0cY65zbPynS#;Ln)D8f+p2MPcq3e`zBVOO=axS6VA;CK$n! zPA30TIrl20^f|oH#ROPYh(~0%rv7byQtsip zq%&n3(J~Fu+c6mbonYIES}i+76!<0n>61|dO{nC+-*tgxETx%?h-C9a9%Kb&61rrp z=6W^^68fH~Cc9&T99^r7+^pQp`Mr{4AP8-)>Ip(z%vfqo3s9>jJ|UO5t*d|UkKhxi zL?}6Fxg-wzfvVA|jczbTDnbe61gOe;y2urEL+ErRV~J!rfkb5SiP4W)vfRA$u7*HK zspB-VRqv$^1t~vg>7BC)w?o9o$8#i~6+&bMgNOqxXHT|tz5)K{ zB3)XcrXx*|@3O1o9x=EwQ+(Ie0l%!?__?*9^7|DigVuvm#3^KiZ=N~$8vX9~o>@S< zqc|0F9}-~yiOGjUm#*zpAUL&P_xA;4*eduz1pIdJfLfO)(QiCvPkq%Tvydm)$nwtQK2*QfYoS(`0g>UZpW+isnN zGT9i?@$@m$d7ptU2aZ&EjY>kc-+CqSQE#sdg4H&6PG0%0k%PG+Nj>YxF!c4j?hGUT zf&Mcu357ggMM$w&eHfB;Lxl=-M5`^jS0DiT_E3cE$xs@Jrwv!YYjE}0D0M*Gdm`uw zU<0MBuuY8-tpreF$|P>aTDU8Q<4u z4k^TvErOqh%q7Pa`o0lUgb!*)4%3nA6EZBpxG$%I02kYtlA-Kd?x<-Y6Xz<#cBoAk zjyh;#F$_8RL5i@Wk*FJr=&nd{dRJ=vdKui^&KC1Ya&*o`Yt-N{UO>Zb{LuE|2_r-c zY_x6hH-DDMR%;T8ssBbNM}Y6q{Z%d3Y4x-BFn}D8);dy88={xG*Rxa>c&Fk6(j*|m zN`4#`Un76e$;S5C(;&d1s|Ax{~EjoZxz(Z++tz zFLObo9b!txN=Z>oP3`BaNewKNhI%pF3T6|}tG6QXH*q|MUgr_)3XayV)ZhEj%m6`c zGTay8LYwk#i;gk+%8d!oV+>J;BEw04q!<-5WFgUWxd6=5}c%B8eY3$B_mUj|ZZjDAz&XybN5z^@EcW z7To;&{IPn6_TPg;9^e??3^LKug|S#bjTIPa)vd)XUdHxZ2S!z5Key0SnP9g?4KQe0 z8R$5XJyd`>wmfeO-xV2mZCM6V0tYBM53MOKR^84dO=~MdnXK4|QAGNFEU=EvtI0>L zjv^u6!KzQ|k1K{pAP2Pyvq?sbCiTA{UsMs^b?6yqblv01&C(xnlvHoLA`EWU9D(RY zZQ^%uVTL1?aQopg}B7WNze*d!CIL zwe(py#qs|}3k$upGsALQ=vwV#`7|x*=j=;SHrt&OE5BwQCT=hRXIeM7QyID!f&6eC z45_92I6bk{lPMlsdgUbXsF}Q9eQUfx4d}6iI-`?xZ$qDN^n>J-A2Y zXkA}>O(1yG(pWZQKDb|nKH-et7QpTg#irX+-H0Cx!(L`D`|oPWBL4juAelE9A!l~-G_P@B|4=EYW`G*QWuE~i5jpjFvi+j zig`&49X_H6|Bw+6C6KneBwiCJu_dz=2&?NMRfv1%_$1s=6@k2<-iUWlm}Z3c?1iFz z13vFMS{I#$CRTP;4kThJZ0L7Dl$iEGW_lWqZVwM{Z!|aaZH!r@g+5z~5`c<=5e*ho zLdP&@BoDp7zH5JCNS{N??nAk%8x?tt+@1R-p2*D;WvXVh=4*$z@*4JdSp%ZM& z0$n+2VNSWfIv00VOzq~QZ@JTaf2$#Z?gs>7EHn5VHyQf!-=xQU4NnuuaIbBhZpyUEnr?Mlh}NbIYoq^ldD2Zr{*`K0>L;7hhY)ch9NEzG@{k0{vo78Ah=g?Zdn| zV0(lKK8yBYqm=juUAy*kd0k_Pax)J_H#*%={b}w?6EhExJ>1+tS|}usC>LD1|4c%t+$hQEd(yRENHRfwduLy9CS$SwT`mznnCpeMaJ3Kx?*7NC-t zCI3g*QYs^L`?s7cN=l)mvF+1t^}YZDkK*>V10LU)3sM`A+=&U?M$s`UI%7MA%e*Vf zGCDV_r1cjDc%OI;hUp+0i;(nIDo<6j*U1e2W^pIK)zs}dlOrSL_Q+_b_~Jx&~=xa znV~%QeXvgGikUG#U9iZ%=6Ia%M4y@{^Ugq*Z`RuEIjo*XLa*A-)3}ZnquiVTXb8z=beM0ZW=I5g{ z)gPBCq1lsCwtrV<{(SiGLDXmLrZkd{IAp+*?7P`mcP-LyH?o5Rv!C9?p0Jb&x9ec= z`%rRhwi-38ro(gtoJCMo_4&|EVd=Q7?fkF;(pITZ>$kVyEhZ4xZHS7H2|fD5R*gW9!|QLh$q;b2D-n1j;S{oDNn%$TxV*mdia*Pb z#K(a`Kk`EgU~m#b+A~Y@B=1DXm9J7WqL}{x!9AF?+J+VrOuZ}GvTJ0XM+YDhM?I|| zVK!3sljXlYtOMfxzw3SbtY3>8Jx}9^4@j4?LmDD#U~E+$l!$TMNjXr0{L=_%rf~iXdPpi9x#|o*b5f!-;l?0jFLeHD7n;-Sx-f z2r2i@kwx+OB9=cqUP+`P7ES(O5=3uOrp9VkrmUsJ1JC0;jYJgrU4AIZB|qGGgB!ii9%AJba_IU*2>T76p=0A^R}+5w@S*&{H{F7NeXNDR(&Y(1bDtmJhi=Br>7dB z)zPUp3~fH~^G0EGMZYO?68dU@eQ$Aq4DMQoNdg_-DA^g&JE$d0{pmDWPn12!qZWlS zZoSImH?D%yGvZ2$rJ-OHrG>#by8wRkkwYoyM`QQbuU~Uaj?j`(OHwO-J0W$;OMcsG zrC>VC&(DgpT*|e)cxvNZ6O>vAsd@$MM%g?|s#C3<7TY?(#ZC0wPdd1N zNf0vp=`t!mF4fO||>A?}1$9_G$w&T#~FQ@X7j2T%*~ zgo0!}0T$Qa==3E+eY;=A8Ev~n0USRSl&>+RDCL3bKWeQj7d;HYj~+eJd)=jbVeu;_ zow#jh1@#DV%POb`B6rv09b*Z1m0GAlM|hwl7mo8ViRil5UatUBRwJi;4~#mc=v0n$ zW7^lRXD2o4mq*Sh4#*fgIG<0ikiuf|QOKnyT^tN60???t9}5Ic|NgD*c>4A0)aw@J z#XI{S8Cc4Yq2F~EKa<~s19w2!1S$bm1ftWRDd~DC8S$i}lmcbj$j#zg_^RH1wze@&1?;wK2ge#;5bm5=SP}Vb$xNQy!uz~ zkfHQZX$KFLaXO)}<>w{}Ns`78)HFlmX$Y^(0L~o2Gn6?E%vZ9PIlp}Ag3p!omtSO| zFBQOu)(+1+zgb!Z0xW;!04R(;ym8LVnhc56>X0Fs#7h;3e$O-l#EmbGz4=?3ef_Rw ziM-;=jEn~D4OBIBwh8HDg1M+^PWc=!J}??YRQ$DcecFI}tpKR7kQyx&I1!Dh}s@H6HW6UD$^Uc>x!@~%?4yS8$`dK@Kbdd}fgOwdZP*8As z=Jy}#(i1~@vbF^ijIm};qYwWB(-Xr8q(ba>-=sw`uuS%FAWZ8rzfXoHYbrwz%-1?k zGuqaHPw)!RLj|8xyD=ZG-f(eQXA5-ZfwK+hy(81}sjKYq;~q00X2|nAXIngEm>MM3 z8)VYCxVt8pnJEPbC`N$(^g38U9{7g!dWJ+%`{(RF3*;vT*SVwR(F1K=#$5 zS87qiM76MhS|2Lq>(3Ysx6x(_ycdeA_Z?F+6^>urk_pZf=n3(C~M-H7m8f$i0;z zMichVJhP~=&Z*Sey=6qa0fQcN&~IAxjpqTM%(TE{6lo=yX-U9xajJgl!?8LPF+u{? zs_(~ zHP0AZauHC`^dnkiSolR=C;>jeO>_tKpLmna8I0q$!>!%ZBZt!9o+>#FoNb&dxNlNK zD8~OxZRnCrcmez;GV=V!*d<`Gp(?TGyN?~5+X?@uY&C-hU6cO3AGDPfO~3K{8}{G# zWE_Q$S@!23i^k4CdmGsDwe9ErYIQu?c7jT~meEOg7^7I2VYyhzotp?3R=RX943#mdUrvO9zXhd#g zg8CtmeeLZ<*#Eq8EQ9T<_65w9b4$Nkv{pkNOy?v+yWCu^sEs=YWXN0aJIGyK`i#SG zKhfm_K#5b~4$3E{)ju%MEnROTpne?>q=O!GiDx5n|1184u(meC=J7wml`KDzvH0B! z6%df3OhXWcY06%J?@Wy}Ht@o3XpoaApL!v>lHo@Kv2P+LaBYL}?1*f%X(iO}!nT;YN_`5lfo-Sgf00s{D>RjK+zDi@Vtv%uK zqjPCk@tOVvco@ojkXTHBB*5r;llRX9g{Ga1Bcvw@fdsc21BW6c{`_VVi43m=Hd}}6 zT0r~+!uQ_6%ZC=gTmrTv17#A6sn5d_A7~=M6L)rYHa5RCiC?Ps2dS>AVve^uP#iBL z_N{h71y!4pN;0pbL!mcPo}8}nG12Tnj1;%^I;Wws#g=Mlf&~(n(Px5_CGd|>BpXgT zitc>$NJ1g%5%`E1N2`gyiw`Nne~yLSL7DqmKKX*9R&xNR3IF=d2rmd8;X*RsvFxEk zXz&R6TDPUkT}`(Tcu%`})Ql zTkW@@eN;3&XPEYk<0O=D+nl5-)HL}mMeR~+8PMnm9mAu^Mscydm=P#}vElpRx!Nkk z{*;SAdr@7}{28PCfjtp^mOh|N@3%mj0ajhiyFo+nO8OxPSW;Br`A-iEI^>vhATjgF}~m@*}R> z-5BD^Gcfm4Bn6i9d)-*julVp6JZ_|=8c@2{9(Ll0oKNNFVx6pb$Pn2fpaOAsEvS#h zXB_UtQ7j_9;J~AXJ#|_#r-3wnRdv;a35(uVn}!C&IVM(S0=MgI_uFIH>tl?v3{=cl z5bV!D$c@X~%UK^Q7o<<9z`gm(tt<_SC;bc0NrWk@?bJ^X#NC-_8KX26e{zGt+F>aQ zV}aM)#fm#D{iLBU>Lu&B4$?8N*8ME15X}4Z8HOR_ryWsE#@#au_n996DWdKEdGPG- z41O^dn1(Um(I&%GGNkdN&l6R#c8OaNmK@Q4g}3=`d1%*Hc%-K%%>KwP(ROhA*gO&h z%#171jiC%mnUExUsQ?(;N`bT8Xz=8|i^3oxogtx3#B0&*uYV=j%HsfxAk=zTD*fe4!GM4O*HW}C>h_)a)>ScI;!Le{HGyJ< zS#rFk)gNMclnfh}$RmM19X1U81?V`Q04L(xaJGC^h}5^sK3r~0`*NNTpbsycGjE^B z@fp6hVJ3b<~T*;-G<_q2cmbYhB!0v48S1nTA0YTx{XsFBF?p$wD` zgmGR0eAH4wggfeFCj=25&2X>e39?!FMfz4c0iEu7Pc0o7x8Q7xYSRdm{TIR~hFfq% zeN3QNKXsUM`9wK3=Sr zrZAxev;1LN7Ch5V7e>+Qdn+;YW7EfrIZy!d;km}XS?JJj@2P1XfX}a@$#u=ZLegxtW;3uNmJN631p{(x83C4~w zkZ{HW6muq~8Asu$=2e6|M$?+guj+ZRaz%H|WVzfTpSpcpGI7bk8Uo)%!7B|tbI4%& zL?cq?8bd|?*u+ei`0?bg>@|xArXYdPT}qIhHo`n26zQ6<4}A|wAE8e}4D;)y+I2Yb zkr3a+E76~?`hU};@x%Esz5YrJo+`qSa8+t(51Y;7Bm$VW%sV#KTb4c zqss)+I1Gp86Oh$2huf%dUJdY6;th63X;_Gb6tlfNYG7KsQzI0h2uv(e1TXz-?Lmz& zy^6*YO@8@G!#hp^<)eW^{oBKLd!s1`>cL%vAlSx7=|)gE7|sepATZJsa^tqM?jKKQ zbM6AaR`NFk;6iu{gZ{vQ{mqDfrw|+suIDob|A;%5@#_D`m`4#mSbqaf$3=#_zw|_i zKNcz?7#->u896Gpv-k8JpkF-VGA?UCEYDaBwWZ3>?eyVL9b9Ol-f_AGgmK^NZ>*{x zE>9iIdYSp&3L(cvVO&y$Z?3yNSN3xFae946Nzs6Ghz0(_%FG*vf_#hWL9c7?DRxe; z_2#;`vcfma4lssTPdpvFs$N!v->ZpuqLOZNT$ZYBCPR-vc3X!Gg9tz~4xR?`e$f#{ zQs%)&_NUzjHKxq{s;YsE`|DNxWh;nLI=CPNm8Kr>Z=$o4J%61hs-wN+rL1WFW;7*Y zngfH_qn*gmRCd`2Ea~-Y8wC=T9Tp$(bE#ZiT8NZ{@62(jZZ@EzShs5 zPcK}Vsnk+{Gk$O3qdwuJYa4O+V0^*$F!XUH<}hzB4u0p33gto8ypOxj;M0DwD1Z;N zY_5Zfr0XNlb^#PeAoi5A9Fx3ws39TD^O}gVib|H-pI~czP*?>e5lz;`)e+}~3@zpxUgHg^hHbIm~S{mGDDQwc3N z{N?|O#Qe@ch}=-i`VswL7-}%Gm}Kne!_5x=M;qQXDX5v@j&)kpeCMkh7a>wk+uaQu zBl^@-_#EXSp+iv53=e|NY&(ur0%Y;?hYz}C>oj4Qhs}wq!38?qIRI0&_~(XL*(XuGp}}dv@SP5p=3YT%S!Q>`zTEM zU5VJRG^OCEL(Y0Y9xB&;3+xe<;9+PsB>$o!IJsQ5-?Y-F;v8Wpfy3AN@kfjoq)7RFg;=e%1PAr<7^C#C!sHb+_G{- zY&aawHJY=4t$dt!xahUdDm{l}@$NV1YUu?xuhS#0;Au&i41*_D24Gs-AzC|dFp&&} z?Hu?}@^Q2@LiB>JoU;Xp3Gq%Z_Lc3(ye@zvKYyl+p}Ia}N2Pg=;gpWww-`v*%-$EpcPyO=jY!N z(VlCpcDmm6>$4Y=2@qf^19TPfPIl`64u8%ZZXmxO#HITRoFc=+L13ZPyk(e)zS3S2 z1hy)bm-Bo!)hBPo2us1gf3dQ2QyvF0{OEH>VE#H2kNa&a$*j@bpr~|u6PDh4>wMEk zMxE-uFi_qtYFa^CasEp@YwKX!w$*4oEpoY1t?F}K@q%?iBvQdqSyN_!Rqi6(#VdTC zc-I`Z;dCv*5K!;`eINL4=DB}jy~ttcO7qoyi)MRPh8T#~I;oal`qkPf0H;(^QI z&TwR$5ko`+Ys2Ynp<>{B;rMn_Onf75jN_GsE zeQUSbsvw}+$8&J}^uS`B7HRS@4^jLdo#S@P(ucG@GtO)_%E$y4bHvwSHK&kG-1<<u`FwPNGz+zCdE zPfHK@UDE)hd?!raHR31&W>3ga#+z3b;r2hPfXHwhP+}^9xuX=dCO*<%%>UUl>eD|L z3vR|s`D?LG1D-zmJn#`GVha`4Y(z4D+E3kw1pDAv4ycID?%0qj;%Hz)IO)w*dSf-6NFp7i6kl{xgdCgR3`1vo zvfP2Asl`|g`kt#I{9@_-2vlf~dgkA^kwde#w!U%m=JzxfBM_I9^ReY)Wa{Qujdeq$ zD*3uQk;&10TQq@pl4}PZ$OELmM{MPYudo!0J}*Q>i-sh+MLjXL81kE~naQ?{zWzHW ze16Kv_~3rd$ZFsu_*;nWSF){-w+AwdmF0(<=jBX%E^&wg62L zHzS~g`DGo1M2or+FkP+AbAsjHU*j7;^{yWp z++tKiA>IA+QI&{8c8EbQJ@-;S*KdtTfInLSb@cXE0~?WFv7Au=*_uzB+u0A=a8x6s z+-aR_r;VaXQ{^bQ=l#Wl$@+(#X{R{GTAlcMgQqa*_7 zq0+zFArww?cDO=sbP=l*`29boj)5LT#Vq)rE!TW2Z1_S*PwiI^#I$NN08FCm>S;l{VXZOh|E4XHQAF=U zK>bLcgJk(7DK`(rnUF;Ha~|Q2Kvw>8wk&Tby6S2cdVlhwXTIPcYb!9smD~*fT&O@W zx8?jBhT=jQk`(TZpM7+0KUlIIcn*C~B95jXad;~X25{Q3)4A_^MFwJ}@ibo8TPhAvb>*>Ndsl;W5aa^0rS!sk};Er85@B7#UvaJ>KrxAF9^bu9Ysd6J7QB_Y9q zIhQZ=H(7x&ePHx~X;q~E!J{X(Iw+u!F%Ov1{pdJug3*{}s6WTp@kV>V zuoT22Y4TrY7~xNV**a<64y4xKTOBLv=<5sjG>H%Ic%R*LTu7h;Y-s<*j^W_8RSu(W zkB@IRXorhYgAVv$Dq}UwPHS@3(x^Gwg@!r+weIO@*L5(ZgT31=U+#k&LaWyQG-73} z12?{(&VFEDPkGZi4Mh;8Ac3NG4u-qFx6a$zD=89f|J)k_p>^KDG9#b3u-ZQGylTI- z{s*~UDIlG`4)O&5)d4T4kfDXNOivW=@Yu=60@_=HxgTaw%M%rag>0yu9jitcKK$SglTkG^XnG&Ke){!8DM3yLEUo&r!OM`Qjn=WplhFb z33>~@tR1*1@J%Ck6k6TX+W{bye^r`cBM|(3XQWdl;(jkn4ETnnm~ps+X?m{v==AWZ z9YSj6(l3KXe<4+n z5YusPih+z^#db#stI-U|%>Wdl0&V|7-e-^{Ls!fx6q1H=wQ}t*C};Ez1N-MED_0Xg z(BZ1F5D?-cPyb?E`ker_H1}})7q)`ES9y;K#ErvIa4pMM*{B-0$D^l~Y6|@ju zJd2R#KuL%EL~eZjrg)&;p;%#z?jm63=yONuk*FJVBLbO&qOc+FIE_X7s4lqeKXd_S zUh13Jd#)ji0Z1IS7mNUQ%4kB~J!3Zl?b6jfN|*mRGE51dZ9Ak0y;}GF^v{`2Tjqk~ z;wZo-tw8;7vxc78A%L2iQ~>1`1QSD0?tfo9IykT#IO)#6J5?vKX7f6pB!(tk6U_7c zuFMvI%s}rcd5}I82|GM9bg5r6hr(F|(|o6Ygtsq!{!e$#YC_JlMDBItIr67*#`g1fuANLGO%4_!6NSVs1n>RzPXKlgZaM*ICxLfOdKw zUHgS1q zJOc0`hao>Lvl)=IppAcK^voNWKiC}cp<{G3`l;llFaDbhZhyRh;2VkqS1r5!Yx(}t z1l(kF;9gbV*!|0#vVvIBy4t7pHth+}A{gn!wiL0J!Mn!cAO$mAQd0tFaer+;aChlp z_{LOB(S^OeIl%U5D`Eg==#ay?imiSDk(zI($?_pN^s*ZY0_{OX447ik`^`tm@C^Wc z?nrJtpB6%D9IPSO{#s&be*Cu46X4W?d@o0#R5q4VKC$1R5R9BvX89Q_2AA=|8Q`;gJJ40^gwLiME!aX zZxM>OW~&-}zu zi|ugT7s@AK{EOYz8gguZUhT8J@yBD+!Bb=Iz^MW@nlDQ*P^B-^q4q&#I@0SF)BlZ( z-G@}?p~PMLM_;AAe!68=XE>iHBo5WXiX^HV58wxr6!#KhV4-SI~`SS z$kNch2Rn!Kd6qkuo#Mjx%|5{iKp@~Ew4UJRrYjm?OVdSW%W{{aX^wRtJq=Wb~r^}B{n8iI)E_(2{CpwNG@Dw!FD{?a{b%=*5l>Nms88%TGn<|CTtuT zp;3P{UDPF?q!C)WJJMt#Tp#$T<(YWmLSX!6!Oj+RS^nP#HBqSBz~krpz~o2NiROSd!h97 z-+F-HNqzFiZym!u;bm!NK-5=)dUGs7Je7~WV2PPR%HpscZV2=~tnI$q3G+EXf*mB^6Ir9|vUj zX$S)lky?qONJ+tq5DdZwhs%At#N=P`rwTwMDeh0yGD%yDJdIYw2&8`5xSb4LDg<_U zU3Szc&eqidz@L*{x4Zzrp8zv?NgzIG%SSk}?p$?kZxH01n`?eT7JoyzMz0Zh54b;? z#0V_Rg+3$=e=jVgr;@IGQOsK2utY9Nd>!A`U1Tk4W0c*>qVvQrdz5hlmOhUnz47v6aZ@$Z7{dfLsJNMt1nLc%z#?O1_ zw7Vt{=fn-c{=4TFBd%-@ueCPf()+`vRlj<3fNdB{i5!4v(Qf*9_$e&tWEOsQ**UIOc4`P0Qf8$2Q?O85)7>Qv`cRu?ruPK1dvc)LMnfDt6ak!3Z%;XGb4T8Hs27xxQU#eP9Ub^* zDJh%WJ`J8eA45?573g;$k3ek*;Q$Ar6fdc#c=^$E7oqH0n%uGfRR+IBfTJaej!<9F z3`s-F{>%%Soget%zq_n*O?SLqWDBrUMi%G^1!25f0Owkk5!sBCUPJvP)i?@ut+q1p z3MA@<;>99TORvu|VjQ#S!EE0bzDFzmvs35FRYmxe;Fmk^0)P&efk0U<2cZP-VbilJ z`LwmQ-LZFpcS)3nB^r^!cM#@ zxuWl)`zIchdU{^_HS6^?ODdQEWv@yMLu?pCDNq6qp>C`sB#hx;aEzJGLA1{$5gc|Y zE!Tcat^yd#7plpynPt9CBj2iqk>^j45_2A%KiDuR4Mh`e<&@IDh}tUqq<1rbILi?p ziiwd%!1&*lucC>HM4pb zX!&3J7Plw&CCu|At_nb9adbye){B0!74;fJx^Y{XGc%6m>UHl+c7!S8tkB$!iZ^>>GXdPamSS*NmrC5M~MIAX^2I+}od6^3;?FJ~E!P48` z2M6CdZz+E1!(b!a_iMHu8nbn!R904og&**p0GW6wMp(Qz<>MB>6*6-*#7raN6TZDd zzaL=*Ir&=ikYW!b5+T**-p!}^fzIP!XO4T2i)2`_7!_HfJ{6KMGN(j44*~7#bpLfB)*ffB84JV%%LRE!lX}Fx+$ngx+LT+g>UJIv8`!BI8sX$IM8Z z+Q?0sXNyLXi=ZZHPq)*@();zl;Qx=V?~bSXkN^Fga~$i~gv=uuWfs}vB+?K{McGA^ zy|;4|G76PKgr&`i;OQX9IY5}WRozzbI^1An}$$oZ-FT@>O za0ATcFz|`$%y5VSLk-&B3+ITg`*5fj}v6~U& zwk0){%R8H3VQhX2^5svFJx_{>j&Szs8{8)ZMSaPD;^m#uce8d|NnRUR&l$MyZB7UE z&6heo0i9VThwFb6VfyI+u|!_g5cDS;$-8j9j`_E80p~A4saQKfNE#(7D(dKZ?@D~A z8y|P3f{G1u1ac5d-q&BKMHCa+QfSJh)gw^Kjvt6l&0 z7WzK8m+vxAvyZY?xju7hI$Aom7A5HOo89OqZ!OplYILGV9;!AYzX?> zK3R1r04y)SfioRojmk_Y-i+ijV(mq_R}aYL`wCGqWHkhN)^J~+(qTNNPSPc8;P>m4 zJzoZS55ic12HbCC-@!U~_C2aR-Oq#$!*-d_=d}oE9+UHcrZ1LcoQThUO@YQs{biRt zegEk&KP2d2fQ;jR5X14VI5UkfAJZx(kB6O84DM%+NBl}vLW0`iXohP?8pnY9u7rF` zy?5(?_)vcgTO$l?SKsIkS}t_^sB=G}HbDW@I`>{am~Az1U*w$0#zCwQNllKOvq=@E zu~)wHVj%8Lxm|`RBhHj1N5fGSs+vp_WjM|_0}McH$coC!6Y}!7@naJu3pXm+FD*)i z@YK8jDj&_V4FD#-`%W(f90M39)O&u4eBsyO(%kRv{})eeazYu0WLhlanC(8~es<4K z05UIiIdl8=Tkf$QUl@%C17HE|%y8i1=@fK3`-Drg>lFt(EZQNaLDN(`bG|VL;yyG^K+X<>c8M?FJWm z6oEOxNJJS|#nSM-c!}0=nO}FSpI!KmARnI&z;e%)U|mDmC<2ZM6oDXS!vV_p;zG4H zY0P6g<8K?Jf>>!Hm)PCtJZ#gxW<)tG&#CiM%;j4=IQ2{7^fLyiWRDOdn19$;vO-4n zq@z-*hfnY{L%|0GZkkro+6))7u-1(9f_kCxXcb@Ak?U2)J2B5@#n+kL4h=)iJMS!RF&Mz~23cFczof4TfHyG%F03i@tH3FX^&jRcsrXeo` zpq3mAU;ix2T&oEuezAbw3kVnwAc0LIjRbT`?}M=GZ%=Gz{E0`DF|CD>4YJ7z6PGCm z1wlP1p1LZcP^x%PLTW#`uT2bUE7%B`0!Xfmtn^eQLf!)kXGh*s0Rpim`j;;|tX3fI z>n?o{>~23>A5Q|}n72r9%02sc-U0rP*5P`HD-Lv;2K=WzK6K3i4aASGd0O7VD*nal zFEJC^M!pXpKKwI0EGtAWcZ&yejE?SVIZY8Q9alIKbR(ggg&B{nYJ&UN`<{?%m`2Yh zGn2BM_8;24LQ-1)Yb?~(AG!Scc;M*E3g~Jzq$bA`4>NOL_PTZJ)`9-kkTOiAUK=YZ zMDfaCUZ-6*g8&h0C}c5t)h@%-a0F(9$dg8?mrQ6Og2KKGOVBk?(?*@E{RG4oS=?|kWL(u z(9*+BbM-CxOE~T@v8waKYz)|3^=YDG+)5 z#K|hcOxg;40O0hW^F)`|JrLw%fBnoU6<0S{l(H-NXN|O3>!Q_|QnjG))@}%}w!0 z0?A(0Bn0k}8*~yBWr8|rVDOojC%C(F%pbPq7%tlvx_IXfg-_+UnjAF016pB6hYQNM z*FeShS%TW-7J(0fHh~dU^o#=LV?;1_l;^+fVcj>&V#$s-)_JBRsgA4kIzR%4oHzV;E&tYo_QX zU!OGw7Z8|fkC2y=;#DVfU>X|~?KYo!$ke(3P`rV8*bB6XFw=5)Us4QQ@4g$ICj>l4 z(vW)TGUoX6Rm-1U=9?luA?EHqK{Nnp8kXm?-L?M*v!}gaDQ0mfGt#6tZPo zDKFKeK;4go|+ zh}5%Z6FY`3n_*;^KV@ZT`4C6HUEI0jqn#m)vl_%Rd#%cel1AT|{MV%<0yt^+_Dh10 z_-p6}bg`+AQ`0AOD@a=e6OX_4;92s`nB3VXuAYpR+FUrT zA(Uk3XL^l-2+N%@!-+E5y?AlbFJvvXaSSV@O*hKbS%MYZ!+(5~{S3;{pr_)(*J!C7 zF9xX8O+;`Zwh7hV0<`kYOYia$Rw?AD#o(QG6AED@DR(FMGUSCBfj=j(>}iROKYB- zGgSGfb5L50^m&#D^ka@dvq=I|UIKvZ0``fG+CrK^2P6$pyO0xS4@}F6-u2KQ8=TFH z8`O5;0ksO8aMV(9nM$?1)3O>4{h|L>@fuo~o;C>a-|U-~)!ZhEI&8Dx_*hZDeISAX zSJVw|-^Iab-IF)G=>S9#3649O+i1dXVorvc6pGgvgdhN2#YF?%=UX`Nl@s=3tt&%> zE3SW)VY}V9i?;Ju%^)!U7cG>wLVe`nidy2FCk_(xP0UT(7PEPFxe_pKK2*^Sl` z2i7z~e@r<-sSrHXu+OK9;lVY3m)HwGj3Dd>u&EB*dc71K{bL7g$)UR~&VqB)gSj^& zSy4^a=m<&Uqe~i>ar2P7+<;PGU{GG*#__V7$UK+q86gaSSEg2<@)IUT!G-T>AQFKC zsL+2mx&TJu+$kkn;JKa=o%nH;aWAKeBidhM-P`{%6N?#`gd;CvX5md)!m5GmB++&By-Bi=-9u;C5^Sfr0vsHKX>J0c9t8r*di=ghYpF>w+n(OcoRN8&L| zQ}+u!4H#>n5CFpRihTI!F^2_;kWVNKokT&h07F>oGnHri*6**k=F08xL#ZG%tvCW)m%t>M4TST!PZfMXxGx=Psa(VG_?L`3?Y>t{Zf_yo zuS=I|!QTQO?hgCd+)@wSXba0h*=XZHPV3Ls);l%38|v|gnhFyZrn{5u0d=jK;Q-FL z>qw-T^isL=b?LZoE_(@%juRH+0Du2h2fr{zOLVsKrI?M_w(BDdSJd1olM1m*B)6h} z5HimVIi$8{8brHaD2=MCtD6z#{rz%Dwg5PSnOP*!qz@113S3qTGswZDnLlrn)?^k) zVb(f1Th=|Rxi^(X4wNJw1MnD4X)Whi-+ROIj^KVYMqjhQ$Z`=xj}GpC4KjQjm!2%k zK_0X`%E^7lw{B{xsY4U1_1GoshshEP)Cy9&aMepGoL2ZR2 z0aY#i444`hH~x*E{oso05*?5)vNsL$R)28dL(h*$XC9}eQ)EIVr@XQx z&GpoLPu*O;3z-n(ayjkZVJ*kyj_m)JCgUFOQ@Z2Jw$gz3eTsd8&gA34g`8J~YzC{f z2TBS{2YvVc+>*oRAZ!AFp1zT>Nsg6?$);=W+Rx|3Pptk#r~8;6^)m*_Are1m0@(Om zft)1!B#`L9t))OSf9&g^yelql7`EWwH?R4EoNO;L)Eo6BcPhLJ13|&!=XFnCtzeAS zJ<{pze^SRS(OT3N1~luX@zx;Clwo*!RT`(y?%JlF|Fv@TFg`7X@ z4jRl%Xkcg9{(uag_|w>`y)$9r07N8Jp6`7rMc=(yZx$jZJ#Y$GYohNT7_nFQ-I>+!d87J7JJcw1OW@53EvLo0>RlAhho$}tpcHu_=U~a*HC_6 z(}$q@<6&7$iD~+iV7LvL0;c5u?8C~7{WZeASdm%YncqZB^yMEc_ZOppI`7FpbJR?uYUbyy#u`S#OaYULV~Bo(Uz&9G*u)&#ll&Se_;M=tAr} zel@~0c{4d!Z*T8SAXl`jRb|>D;C3mN3EZz^JlDB!xoLOJFga<_l8{lk-SLQ5>VD`u z<+9n#_2VAd{WUz%b{GC`%KIVCMLU5>@WJizMT)%_{sXd51_7vlT<-(T_En&=^8W@G zyp|_|I=)QXyHe2ex=>DeMi>YGHl(8Q2v{2S^h=b%7i$Z%wS=FQ#Q{w}BRyW7{3$D4 zZEgEt9}pGup{-hw_H0k_!CHFX2jQ|fhWkF0*K&`B@>+bLGdws+qpU4!F8z;E^m%g9 zJdj4i(sapywqJ1I?&__v?4x!gEEAA%Ec^otq+UcpnP$|Xw2jR}Qh6SB^{EbsACclr z&ed;h+Md__WQL-2!6?hSXaGF#T!4fH+`!X39MHPj^m_s6cBG$Wbg{YLbd1VGcfs$D zf!J4-zIV6$F9NX%04_II=?-fEoh5o=6n8PjV6$YvO`DPIwS~r=NM@~a;#-yzo%^p^ z`$EE7XzHX0uIE|8Wbk~j5JLX%a8E-lPg0FXkL6o1$QE;OgDeiBE$qw})7v&(nAchk zYg3!g&B}c_A*8O3uvs+H(X#=nMpqWmzG|Td-UQFWt@cAJAT4n z(`ZnmC`jZ(AB19Q@!#ei{9BAgSQ-MPO^sH}=Kv+!i%ow(cL93vG%ahV2TVTPTGcWv1`R|WinX22d38?wS&kG)`24M2 z!1?LJ3mwlzpg8~UXzzdHzr*i&ffT7h2=w+nRXanD&i6Li$E9+sI9|Pa<=HN*XND=K zrJGu;aB#c-coSU0RfxNp+4&^;?Sd#R7Ku%fTUTeZ<_Xz(y8A|{#82ohwm6mn*?G3|R zIV(=9D+Z6Ux+8?iX?#oc^t^NL-dL(%$h`kbktfYawSb~DL3IH%jS}aNGWJGUQIZbQ zs*0T=e-Cv0ttcxe^P8JyV(obDcbso|CtnV`2j;eOkJ+zV41gS;aL9EsEdDcxEAFdl zwLo2E&MMO`=y`xD*JH@1=TQ4q#L(SJ=m?;0%V~}u;wAzg_TO!g_5p^8KYGyCYst7T z=kN=v&Z{J)#r^3T6Y5deryhkfj#{`&mXPzykHB~7^-+}G;;id+s4bMzWf{f2(g^6g zwE31EMXz+7a+e2~;R3UvEHN;80sxpCXMv(~Z-?IZ!IAL6_mDrE5;^UGpPj z0caJ`13dWY2{DJHis&j7npxSn*_y-%o2yP0G)LTFz2p&X^4d2l?b6o1X3R?BLGC=f zQ^QZau^FiCBA3`pZsWvO;0RdUaA?3W;WFfX#sCQ-r$uTK&5E@ck)tU~N-{OWeHCLo z%E_=WT+SJ72hTYZ#+>O;r&Q#)yQJTZ4m%TXJR{C6^lyFM_&Q+@&9VUz-k?@ z59}2%zL%*Ub><@vuioxYDh=9lWy-O7^(E{uCngA8#Swu_v?F4|E}|Vltlh%GIR0P{ zFZ>`Q4<9vXQ}zJ?;(Ps$*jk<4^@-A=3K6Jiq}o|fYO2y<0A{brY^sN*?H_8RM~6n# z(zTV#5t+U8kt`6loX7&1pWY030|ie4AK=W)4B^GERMceJVz#cpmlCUJ-7{~m(0oKH zV_Z2mMjZn68*hAZ_+RFh8ZZlihzaGYjS$DX%j?evi|%qNx%4NJ6-v0RKHvGL&?G!ef)v$Sg{}COKlF4=tMQKfF zFa~o0_JcbWhESJ3RAt4ogu_|m*Z3tGzZ+V~3Pi;&1nR=#Fl?Q$(S@4yTKF#AE(o^i zzb`qOc<~)y-o5!lpp9)FZU=!Q@9HVh3*Szx_*SJ2L1A_{p#0F~;wHFd+34H5UTLCh zGdp3iAL;kzi{JCydIAC3T~w z*7MsLNv!bXvm2}=eab+Jpbn&}M_zLO?l$XThZ%gQNnoP)pMUMFB!vbXdZxcxa2D+O9RfVF|NA}n@pUw2lUdBz;j?5&8uTWP^1n{ z?o&Fo!~H`E=XnGiW}h!Dgf{T!-K(2=b6b+^_YSD3{-?!x1aj3PE(m7MaNUaG_S1Tsy6ZbI9yj6AmXm*y*0#pZ@AKa+sog z{HRHJ95#WE&e)3TgEwZn&rU<{5%4AK+^5)hjosd3zzsKgjLuZmiw-iPu+vSAj6kUe z25jQnwpxiMkdJ&CO9->h5zcm0sLkfCo}lbaFI-K1J!ZLeF8y#*^bJm&6J_f z&CG0C(uB&-e`$LICtKvJoUCm%$qopaU%&i!TC6ErzYB64p>6@W{OHf$U|*BNPy}~( z!j(v5*&5!bP4l)~6|vTSJ0oT5%mI_42>CM`=Vi7FQb>N*wh9jXNi^hfY(0#aYFIS6||BH{O2@Q>g%(-R8NcUz@#e5(!iG82hPP`;uVDs58; z?ih@D0?&jff7&?-CY(BYw6m`e1_w}D#`&$85-0NTJdDDm7B(v4A3gh{_ zR+|M$uZxSjp0|pUTCq0mv9hpNphdli?i|HM+QM$iLLdQ#ycSRP_Yb^t=SP>n5?~5g z>w@dC>4cmZ^24Ko28VIgP2l#-q0CSiw$q;uDpw8kppH*4IfQ@W*DXr$CTZfwN{e&6 z$@yA%n#rUg=S(7k0>ne6TtLp07h#tn!8noN>RjLYYQVN(JV(6s`RvV71{ckP)8DSv z+*+hiXO4%iqG109Tuv>&58Dv!-FjKN>n7b&OBrFGTUxq<WQ~U6f1}NSL zdP30QW~i676^BzE@tl7Rf5rVY1RY3+J551n5-BbGsK5&~>C6y4bs$uYJaVIHu><~F zp-S+V;82=5(EGg+Osb{kjw~b}pQa@aF@O+^nH$BlJ)~N7_ShY3WNy{!g>0+pmm;KW zHnb=y`(mb|Elm6Y-OEU%BpqufJ<2ROqm48)vOgr^pAiPhGmb*raix;_`wx$>8mQ8* z($nY&Qa5U$5qoghomWY1PeVIkFg-b4;Lg9eTD87_otupXAz_8*Uo2E6NeOY=%&S4i zg}L)QCoeb;R5W<4!1#fI$8F6OKx5l}jODO!{j(tXMR$@`()phhq%ZZ%zq{~!JvBR; zQ4Jh6s>A6+mHDAVYW`$6p=+O2{FVOpzP;D1-XC;K9@Oy-@xGb_29K-Z$l$3Dsd3;> zYb9S3ZC?1`XvyWryKN{K&aTgEN97;yn(+XoT{F8wKLdBq8n~}`IbM73{u^~P+K7$! zfgc9Lj|*9oDHW`CA0ypQr3eBCTTX*D0+YISBgdwBkl}WC<5s|l2y*`(=y^CcHdcE= zwee7FgM6Nk9G|%d(L&zWITETJ|$v9`h7wcLKN%e%F3*#mN5 z5ADSE;cx}Rq1SnB@a1=rR+Bqn-$jV+IFg)6i^HwcxU>f4qyn$6BNpid1I0 zy;tH=xDO`uYWE54x7gze4W%k6fX4)_c@f6&Yo76LYzuRbPS$qey>+mY|5sIN>vvrHv&nJTubr z`EYJ?zxrTl@-q`kGI4ElcdzA>F3f=km@OiEavKR4e9N%}DUNuF-*^`}zLz(TmYzQa zLPu`E9;*T2hrf~>)gBI7?E6Dq5&x3>$DZ+Lel6th!Ec0|Q&gPdus9kDvQEoxEf3#P zJF_rkzcXvoz90Ndf+cVE4Ul$!L!7dNzj%ESyf=bO>2TlzWig`9P5OrEu6USqZPnID zB593Y`{-%n zf|9+9FXHCm^1K4Sk4j@y^)hYE2*7difjnt_F!}CFKw6tgQ$&8s`axwzO2H?{C*#|a z_%s0Jz)57Gw6b{y?3f=8*3yLRwbdK#avU; zb~*w>t?#-@5jL;WQWu=mApbWQU0YbHl8cVD8> zNp4_(KH~@^fG_i#f&MlEs^QOk{}$DQUkq^^B^jEsvW&lU%K70kIk#hqqV5>|!hjXA zp&3z8u-=*w^@@rL3&Su0Ai>Y>2M=|L?+9efLjg@e zow9`jTsc+@K5Y{qt4<6DkYbc~zBcZ#x~g=(y~-m4<&@MW45L{M|bQqQV$Rd=ksff}2l8d^ipmJ^qQ} z6z;FwbKf@~j*ax@%du9nL4;I?lpXBO+cezb1Gn5K5t=4??bKEglA#*nh8gY!3TJd$|4x@WZYH5)gqHTAK8)S(fYj)m!Mq z1MJ)uemYj@b06|9_!&TS|FhSp+}iXRo>yc5!=S)0Pdl?<{&;?P)#@lepJw(D2x=IK zpU=e$t(XmVtg+%QZpY3lX}=Qr_wcr;|hYABPA%&_oe1FY&O@8 zz&Ox6MQc`mk2xoxLk{Q_hzA}eVb>6dvZdX5Q2R7zWf}64#VJ~Muv5pT-Ch<3@*g}1 zds}6E77&;Z?~te54(@P;-a0sftNWcguC!2uO?da<7$T3k1@;6;$U~{ayzyyM-n>Jz zr^6vj9s=C65ME}TND;AhY>_7Q7s{KZ>{;FmI_SfaXY&C_AikKxSpfS9-##4ECdocF{eEB+HLi99m-~sP<@Y(txI<0p*MS`pU`tqB z1fCi+2nYkQUK>NcdpvO&n(C*L)bZmj?<}|^^(Y}0gKu!nFyjzg*a@jf0LH&@n!K?5 z)drDoCa^z{gE6UmmW3+jC!C}IJ#d==D82Hax|?NAU@PbU2=w&3|K;VkCoW~HK5lJW zOF`sEtjx?~b51#DrC(qtZ_{I8Ok)l$wU!DGi9z!=Uy8(L7et7G2m9j~5kWl>iY>YS z0iCA>WWCqlBObb4Cv4>|ShFTjDNInj|9D%@t2RZ$MDy2ivuUk0sz%9;`W0$Kf!RpV z7New@Ir|8xF<8+(Ygayc`MSCSKn65ZN~tmePn#&I7Z)_;(YY2f5y*NQ+r|=?NA#mr z35|bB1C(!8?*4Ssy=XXETnAjTnb(YEXcG7{%l@(7m*O_mH*al8jnKCgn2E=2%y*B? zeaX3xj9uK?q7MvKCahAEaX=k(Fw96N$KtnRZxgj!Kz$ENe!tAGe%ItHOir25@+~HsIBZl>a>SFtC<+3W>}}_}ly43A z^3tsUsxCwnG!rl><-e;qce3)USmol8M4}{gu1G6AeEufg3QK4J{93 zC$N{a0`utx5%jRPmPn9LUs5c*MNXPvmkqVROWXz`zg_#1Qffq*C4%;qyjdR8ymkRG z&g&=;=mXT&-!|e)m0xT~@?%Ah+%$oU%-6sbcMoLZv%#4Lp&wTfIR_Dbwp~Q-?Q*VjIu$QLj{&HnQmR%K15NEoiU|iE z7JHJFUXp)1YU|`XPnZQnl&_L=^dAk&xTLvRf6x(3huMFKq~Splj!Cefc!h>>KJ(q> zbCN<>r#2|?tnas&hWQp|8)+Ian%uQTzn0t@%Zh0v~MHIu+QRd;^&ij<9 z#(RUcK$`DFOKq2__2b8N{LBp6y;m8==q#`d8upsA7r+&yPpK(?B9VlG1*70`PCBr= znQ)WL1Ce+SE|ahQ6H7Gr*GXvWnr-e!blY7i4kTCy3jMJMA0l=Cf}YYC>lWmW_`0*f;&{z5@52pCY7Twcg_{hJhf^WCHm?a*M)JF2fO~?hJcmoN+AjA@ zlXR}wb7Rkv)*iYQOV~@g_`H?Xo*;Pj z(V20hf}d(a}lC5`rJ~V zI_f-G2xo{j8e`L}T8bM@Z{ssoZ<1kn1m+O8F(VzWH0>wMSej=LMv$8JgNSLMkyv|( z%(f`!FkJxp-jlGNB^v}q8GR4*5_3(LM0kHP<3I?;gc5rh>`@(D#c2X)N-ti?4^W8o zLu3#6A~&m-1oc)~xkB~7y14j*y_wHN$vNIutlAw8%N_{HI=F@_PK^~cS_T#LW$!1;9b81x`LVb8)7*>OE<`;j{rS_LFRNU*X9kv= zmjD#BO^R4Uxk`P?SU`kQ@;u&iBS()(a3W&@zUhU1tb^YixS8*^Yy}pf8K^=lDuK$2 zXU#`!#z+t1nbACO#p5T`pnue|9Xdbg7!>cKF6VeUgHSO&RV7@mg8FVgh#8RX?1eye zuCzJ`bgte!ODG7&8UzIf5}Qq{I37Q~>#vHA!OQto#P$~2N6f-Ka8Edb!!u-1e!m22 zm%l#v+CKG;hdjmIt_qNugMtX}ti?Wd^5N5?83(f&XR)i#GAuk_JzpyqCWsNEYnqqb z?(I`yJn$yj?(2XL{66UX z0~cfI=o#~#Gr(S6uGTv~6fR}8!3`|*WmjVHL?5S~(HN;5U_RWk^Ln(-#XV{WSSTWC zt<7k8pmh1i&Q8FKB0SA#(5U%=9*{uJW7)!>r*6J|AvwBma7EF30%9Rt@^v7LVfVyg zu7oWeAE7j3oW5|#!r&QJ?UO&epgiA>W^nVF7*0WwFHlPmu^a}c*vLSlxrIlA(E7$^ zMH)P!PRE?gwp+S-0@iBFz{{=uAkV+>i5-WQS-Js+G3uqtq^*eR)V_|V$Q z9VJ(F{R+5hNpC=hXFw4Ld3;AiU8OYVd8(7OF>uV!7@(n&0c$fZjV%sY=HwS65bC)) zXIf%(_#^oEF?qJfzSBawN1(^dHbn}P04)Oi!!g1P1ZbR(-+^+z2+>osG6eQkgo$lM z`@?%IgJeovhpwFSBG=pyAvCgUj^h^l41~A|OK)PpHyN&NU`EK&+V~`Dq`A?VJ>T@o zp)mxyW6QlI?A?i_Z%5yVJpPKJbe=>p!ob7!CxpYG=gdg=hp?GQIM-%zK*xs?@}>l~ zOo2PLGVN_>B^EHJyJLtAVX!0NL>!Nm2Be<4G1=$K?GRUmn>A@AGON;r-qHgNk2_Ry zT#WD|(fb4Rwt{Q%Q?|aBeu*=xj&oYcGBXx^N#0F|AAC^exz+3-DV?WaNPFk|cg38_ zB{iV#k=KcBIM!B3j%{x_%uVRKedr-_Flf2jfzqmK(cy_CCYLg(`@hTd6=LAoMvXF7M$%3SMx!d_3+W^pc`H_N39n|(Z&V?!8}Igxe&w=NT; z*b?=*M%hRSZwNbU(Z2uQK2zHS3dMKckZ_2vB0UYL|v8Gm?{&&2ULk83>|1R}lHZQ>>r^&H#cYjzBc? z<+_1@mwRzak_NP}yr&hoFzXz*9*}j8%#X1(=r`Xwun%a1tJE${qpU0bg7tsBMYmtc zsO_uT!jSXYZA(S~y0(QaE-sdNj%XDX7ju_*JVX|4Fx%SfJwK=TK_pX&HZK#LI!m{j z#3>kYzUV4aajt0lFvSu0bAN$k5#W_7SR^)3J6d1(=+N7OsJ}ZM$;^5bd{V%g*F8&JG*HtjsVc zhU5fRfsY*f#S81U7$3U+mTq-oWPjnNj_YrHK-1TQ&mK_It`RU4{4-H~$}A9@3kJD^y25;Hp?aK=X`PkJjnlL|a!Lt6G>2ZQU9Y`V1b3enkX8-r`OB*rD61yvMIY3`NHv8$hzj*q!^eD;2BP+W7vVlW0H#T>;K+B`;l`NV>`i7|E2<_RaFxAt+ zDUEHNI_?Z94|#X|PzS_jn$(YgRt=_6u63FHljFHkw0SlSF!H5uZ2;a3hf@y$jShnj zY|s{Z&j`LznrsA7+%$w6*E=w>Iu+(uv*t2i-MuwvI~R%E{z@i!?)Xl~wyaXtH8?LE zWf4%JA7~A1fAv+?@X@Wg?TeUha;U(}-Rp-;X!}0{F-3os?%$i?PW?W|`b}=c2Z$aY zP$0-&R8_)Myg#IFeL!2}w>4=Z-Dc`#!U+Jln? zz%W4WFa8oyuxwreBkauzBSPXsU^n=- z4i2OEQK+`pA6OvRx-)4XCU0^JUf2SijgZi5dEXEP=fTC z0t&8`Iy{xi9melzn_;E&L$YC}ghbcD&{k19J;@iOwB1 zJ{&xhjD405na$h;(Wvm#4AkOo{ITE$%!~(WV<1$(cA+W-yhZQwHPfZV-Cq6BpfC0f zG}WYXA4?Ezkd$F9JMv-B{)Sa`B=S+ZO4yl)EYoM;RRaj_JFV%pDWYwnmB|@;=pufo zeQB1m+bzuiNW=dToiKYdXMca;Wc}FNORDso2{?%ra~HgQe9EtgX_9O3Wfhv_Iib+Y zX1I?mJx@I6a*8fuI+&r+pb_#xigL}$V>yD{ac$O7D~dhl(Y8@HXqAoGspUiG_5Ih7 zvkFd<3!}(_Gp8w|C209-%r7-GPSEu{UzH0aO^qM$LB#KWD?sm?=e*91j0z0hTFeBEXcVShiK_yZG{j_FgWw zN-D*mVeqKcg&F&R8qYBSs6qrjZgt?O8+Jtv8k`gt7blK~A)4--JQ!$(zu@Nf*qrf7 zR`rVnc^O|LhJD|H`wpyJCOG{8Vc$OLIraTtD-V}gR_U7-eI?X{1pTSG6(Lb=_EZ9% z9EEt}o$knHEWq}DtsZtcu-pDP#7`zBgkK~60cmn!n}`Pk6rFHR$zgK@}Ou*<6pKgfHnGW?o6Ku zndKmN7bFPtk0R}oxXT~lkUA+Kec}y}UTMvoEQeL>KFr&hGxfi{fB3aaN0YcSKyUK> ze_nf+7C!Ra>O>MI1^nVQGd3yvZ3n+tZG3iEJKA^YdpxX+?%)_%x87Q5Tw4mBw~2iM zK~2%x6b(~2fr^#sM&b}~wUOf@gri&-apWS|{ z8u;6P@z#v$X(lCU*FdIK@E?0rcb*c(<8Z*W^DDP17Fh!_u2+TvyU zcT^i5cQGmJu-av5Wxo}T4{Th1O&HGO=oKh(bJ}6R9b4;pf`Ypu+RG;}KXpl3O>KCwk zG95dnXYsP8-VXtpz07Fn`-4bU4XTg^w0oe-TLc;eUo8eSc z!0oEnQrqF zf>$942|>ay>~CP#e`xnsfEQ!f7VCIJ5CY9tiK*=?Sx14LM9Og)DFfQRLHf`zW=^4Cg)s z<@&i#HG+gc&3yD4ssI_Ioj4613<~FmaP(RJ^0oNXMpDLZ;n1Q~KR=K){d15g9iwe1 zI~|Jv;gdp^s>ZKl4u4lu&ZlXHvZWv$r1Os4^Gm=0yTS4~G~Db(#8Q`2e&|*g*c;XI zK7ZgQ=447O5H#k6?(dS^8a+o|HrzjTX4`W1Nd--_{`S!Oq!%w1N~`x5|E&UxA#Kn$ zcZ>)5z0i96_yGfrP6HMAmSkw=Wh8Jy(%Tf_JkkI%0~^qD)TE(q@SmFoud;?Z=xYd- z{uG1?s;;O)>&;!}W`xd^LjPF;?B}nxl_3b=O47210*wvN8WVm=gCydmOD}TyGrers zfEhKqdsEgFhQ)4uuMi|9ynCv;J9>^u}9gjOTqSXK2X+;P$PB z4hHc11JC4R_gK|tGGew1k3a;81L{35M(tu>+eMwDVPk-4z;F_@GltBPoUj}k_fv%< zy>c~w7azFoM)c&aJliJ){&PI$Gc8_B=?QWZ5)eM+?r$(k&l>*RZj@IG)B!BE<;~iL z=*t!Y+HuZz{+NKk=P(wlveTz3Vf__>`M%RypckL!kQPQFZ7AV?z^n~W#UPQ#ZNHG9 zQ{qV$)-M{z$!X$LM$gMVP(fc%c4r7C?*YeYcWLz^a+_i21hkd8?m@QACf}S8{)ZuX z0EBO0##a(WxCm#OZM7sbTYp05C%z+iX%vJA!sIbw=K+nA?PK#{C%vJ>vt06ikS;s- zs35=0cpj7^iS~v3ftVdM{k8^aFDE$Erll&&q5uTt;W4hV;QYb(jVoBHXDAN*w^+nL zSvs%|Qy*o|s%VSF-7|K}3WW8&kTI86R59DG{saoZPy%tU9SlpCE5P0{@K@k01AdPX zFQ};&G_=v99k4W5t_LcD9;G6Hs@tl@za}@guY7nVBby?cU!Xo!B9}cqa+Cqe=FL=) z%mf|Al0WqItIQAbd>eUSsWq{18ZU^MPh!;5+YPd1S{3-wv&y_B^h2Y^|y=ZU@mcnURH|-|K157d8MYHo*J+}Z@twCx@s+EIRM=0yM<#;G4GC`JP;-eL3tlo%*vMb_NEUYxDLXov39p zH`?>YOVdSL*oDi4+KGu{0$QHDg|3HFddd}|cBkM!s12B-#CshM2i z1?X{OXscqW6ITJpWUfF~K7U(+w1~r7ak{gR<(BU4W0Fl*3%khq)+{X*iE+i@=;Q`7JE@s3!>8(ady5d4WWJ`g zHw1Nn3{Mu8TOA+R6;CNUafEWYb{gwo^uVdI6rs7AKrFN;FOD2+)JtgZ=4IDEu7h|GHRxB5EueHmG1L-l%*|T{4bGor$8G&d;WIXc5~3kDn!LEa_>D2v{i*L# zTZ{9P&$d0I;lcL-;F2v#ND$GdR|(`o?O27n!M0BHCs+D&^EAf)T`gB=>1R$Dc(*$Q zJ^tk~W2t^0EYVCjcglaY7jFP~L>n8(yu|r5m|NAe9)(*c3SFoF_%IKw3-Qa+CK2nV zt@kQ=G%JgW>YuI_LN$yw+<_;^6&~a_>DZV(FiT%!pmv*m#;-rYhFX^LA%jMQI|{l4 zg)(3&gRqpwp9i}up-xA;YrftT8mcMZ$1{OsL$3DdmrMWp0yL!t;t%rkPk>d-@K_BI zLNzlkVyC4{Q7dM)<8M>hcZT0*K^n%R`4|w|eS^C{i0*f(ElMrO6v%e3#Z_m8fn43N zFqkvn;nP5=@R~(f`g9&}&d|5FpjjqNCB92c<>JR8_rZp66!9rB0J287^W9n%7GJV< z7}cil4gfWmM_~pDYAy{W*%?P?^>h@)cuS&#{2ds zSgY`7*Bzm#c*=HiZezy0l!5t@%y7$8^(+Lf_%k_0cH7iNZa8kPZ}YTxkni2)49!%O z4fFix8j?H#UPW75o5RR%NvY;OrL{g%byNd2stpskAMk(3dJ}l4zW;yx&KL}mu~)XS zMF=e@+f0^-3MEU{vbBjwb~CgfiLz!VrPW$w9b1+NQFcbzv#&FjnfaaJ{rP;}-~azU zlH%Su_nh-Oul@CWwc>d0(8?}So8vXm%fNwEOGl3gHa3ez^Hf_#9hokC0sEuK#;lL5 zDuI01%V~lBq`N`n`-IQG9$P*vxv3eR=VFqvUI$O9sXw2p89Cbc)M0b^cp}{0_0k*M z^&mceV1m8s8nsCBu4iD67Qk9qj57g5)u{s(_vb&?U7MSIP4RyKMS6Fj;v@-9;?~l4B=5OhA#l&s z{svZMqh~({n#pZ}a2EN6vsxLbZ9W55W`revTSE78#bCnyS+->ZgLM-aGye8T&c2?* z=GL0H-|&X>Pk#S=+tpN=-co;Gkk46ubh+=0wW`L`uciC1iOPs(fC?ArYQYJ!yQG(% z_UvV5?4o=|(8%D-o-4PmU$3DvAm_OQ63YW)#oWZMZ+E%x;FE7l?ZaT8jK@3VJh<53rAdZVO8_G|;(_VMOj1-0i@x zt)+&OM>LeeTU?B;pI{h6RNWhdLf9F~V4!a8)7qFmPOn|2{%V_%Jja6Tqdyu~G6{Jc zpk!Lx+&rN;U(#S@=zVVL4K^5vpJ)g$)GkmXsk4(94C%E><@Cw64BX^1fmJuASNoUj zbT)@(AUotppmx4(YevF7U=QzMXjrKa|ieNqQ@$L;#G4y;8F`3?Be+-SigeSM>*(T-2?BH9~n!a&nU5 zi4$QpHK($>rtOc}j>&!m;0y+noIpyyVj;-doFP|E1J<1yI4yrTOh3i<}*| zX-D(%zG-OqSZ~p*xr#KN1RAAD*p{S(j0l9m+jtXSy!cu_hG=M%0*&w#q$gG7d$LlS zC+hJLE=Rn7%S|X4)itujgBD|%Z})#pt-NMg$|Y;I7yT=7y*Qa<7DBpV8TQp-YGJ0a zqXP{ro@NF95!=a+hFyo`@Yg*rm1zksW?Ox^JKfoN({`(0Dji4N*iUvP6JhSCfMo?x zR<(bLV0wpEv38ZIG9Sz*YhEqTwwJni`z&bY?YtA$dM9*Wq`HxpGlhU&Q1Q?+T{Fl}IJ39L!P0QZ)stSg*->gb>$^@xIdsayfzuv(lxXzdh{q-L@ z$@i;J?(p_1{;R`B8?aI4NZ0|OilkazF2s$S2d(w_SJ6#Us|69nqIAdr)d`MoyNfG} z$BEH3DX|?*%9O52p@1fZLfv(SKAwW%ezWJz;2H;p3q`&0^Zb57bd)7~yDx|2(5fbQ zag_VAJp%y0OjHkKb!j^*S`>Sw*mEwu@_ep`GXCp}>#gVP>bF-J!MEx^gluk}j)7+x`fC%?prwZiezku!VnoZ&~3QirN=N@}sa?I& za`I6>2iqI7+64obAR@F#r2Wm?OR2+b*PYm|Tnvq#q1c5`GjIpRsTT+7!6M%sZ=cLl zuJi0otdQ7pp4u_Ua9j62yo$XVDpHuaPuQ5LGq))%QSf%3Wh;9tP4BhwLK+0evgr}L z&j7Hit;M(^~p|F-f>@J$ST)i4ZzHQC7*eZg<^U6=cA+fdtjoX^=M|2XFU ztBsR#hc~{$Fj?LVpbT9?pcBAwi2fQ3X_zoOe!L{Jpj47rB;wHD9B2?vQ?K+xae-!o zxwEI;j5z%K{i6+(Vo&v6&BcN|D&T~=ywYb78qE6~Asah)qjaRxK;dZUxL|~QyR4oic-nd=Q zy3rjv{l42VC*QC>NIB54(aj0J+sJ@CCT%Z>6~TMM^~*>@KYzw6_%*1kEt7hdsCEL6PF+8}p$w>F8!h_uqdQtxw=M`Q>e^OIIeZxVYM#bfe@3 z?u&D1%8rh{Q?hP4%P%Dt=@Fl=nWKNvliTc`7l1Ank~P1Vc?5&B|IB7}{`WFqlsg8& z`{c#)t|(y*)s=VVgo#5yZ;k-&h!KfZSK1XON(>7GSppyXE)ZU@u@YS&+K6X?ZZEfU z!|$d-g{NR8a39_xj`xH-1&SS&%w3+;Ih~zIX69rBscccRGw~Nu0pf)U+wAjHk$sa2BUX$|(8*_KY3$ym^9{hCz)d`{LZqcV^Y z)RT2Y-&l;3ozwVWb>4_)WQQ+8)SFZ1F#hwiwKsuUD#an9sm@H`rP)I5+8QH7u8K;k z`kw{c_;aL2ZV1|8BTTzYQO2~!2jZ(3!^~#T{faH~{#(U|=R~%e+}r^EbHN~Nxw+~~ z0>L^9pKjg!Z9GrZ+cLf+1;FEt^YQb!Pj^aMSKMVuP_SzlZ#f3wM|72wIk^Jj1Ma-7 z&_dd?q%Sl!t1^Dk!ZWrMcKl2^?df^!veo5+t6y0`{4=Zct>w({9&Zk?EHQyG{x-3E z!;<#;WQv`=Y~-Uw*#{8qyLi=t^>uopH>qN4$cRXaNk36=^fsJ|#D^Mhrr~~M;P4hm zcU((+;I4hiwWDRc(++Q7fGWm_hv7}Nu)8TxVK9svvSqe^c~b<0=ib|`eH-?s{ZUg{ zW~tOdA`h{S_4j?A-E4m%Jslr$fu{r5(o(GM$e@S1uwLbfi3!DHY7sBM{8CfD4%Vtq zOimubs6;*d%Gj%%xM?jr95KbcXHb?ePq2bF=fat6&>gck@w9`3IH+&_(b*XST25ql zU9WezL%>#xlGlz8KfHKC@HhYGN&ucFzMi$piV8ZVYCj;dMQ*escP7lAV8Co>f*{G_ zQ?kgfySP(K6b7;wRNiW(;M@^$G8ikpgJ+1m6ngY2^N8r!2pgz~fs~?DaZ#6Q@`rU z$sZPtq1}rIki&RJM^}NqS9|O&oBA!Qi#a1EnP`S%%rpkPkJwXFv2aHj04)`6D)wya z{Hu=M-YF@vbz$C@(9%g>(4)W~>?Suit}-8we9blXI>A`bUPg(Vy%of+*4=39O+@Mt zF|x*}-U$AMtyICei7l#~sBG+ciP(7Iv-Ez{B=Mt(+h9`R4o`POxyZ7Uv6HfZsEdOl_H zkW`tOWasL3 zR|XF7s@z>(bs1pq;r(AoL0_5)`pB@lfV0p&XS_G0UrKsm@ahk4yO~JTJn^OMuy-^p z{DQs-x@RKSsR!q*bEYIEQ5uB5_V(>6vyz;Qy$;8D7%R#_A&zfZCd&9~?ZWwT(y)Jf z#R6pkcO<4ce~ARF$-N_cLe{n;n6@vjZ8Gm)H7cHE*fGMacZL699yl}-=sI?IB@9z8 z{3`=rb{>BPafi4zwZS>`}MM#8V2RVCA0dzNnJS31NV! zjkp7sK(zQjOBSkD(HId4`s3|ge7q$4Y&fXvR`liJL!(#A=;AyexH_HRn8&{TYl)nK zxq5bf7Ra=SjvwvtVr7HucWN#2Xbd$Yo<3x_5Nzlk#oB+mxZGKp#Ds7f&KsE?JlW2RF9F`jbw0 zE&Og%P^k_~vqs^gH0s&F!(=JYMp2k%V&Jwv>}aiw!E38_jj09PKqm z4LGI|77+Fu{pcb(zu}?_2~1Sd@WLBXe}KjLZto5^N;(B`V%I;4LyJwg-XpNVE`};t zHIp;0JI+6EYBSP~7I!$_vo=o50M-=5qjScNCnFzXWdnb?0#i%aj2_|AVJJA^zOg*s zS{^hFKw`&!=;i#5D#snD4qW64jDgY4y3&pUTg!^}*^e4dSK$qI7vf@c7!+@BqXVF| z)nQmY#K<|$CT<3uOggV%%19p~&e*QJtMaax`&f4+?1F9`{H`u6@2_zus6H{lOP(1= zF`j_KRE%`~0OJMzNA@s)^ci78P%ET$gHg47C3`Ox=2<<}?nyXfQC(ZBwcZm%QAwlJ za0Iyv2QA}J)!sEyR54C$_m+Yf_4ps4?pY}Bki}0y137pY0!l*xhw|A}=sOg|iB&5n zcvn$AYUc&6MQZy!koh2OJPf=o`~M`g*tyLY%0<<}rR6g2X5k5`j^iE?6IuAYp*Rpk zXfLiT`Jj->JCs=1`ixm@dw%uRE6J@*D+(7gB zOZfysajw&l$8EkplBaq2jQjKP;3Uay$Te2Z6VjYX`57rColYWr_5;NfDcZP^ZP|G7 z!1+3(`Wy)4TL5!%(WsBzv-^>}qCZC^fXwG}LTaI09ZWwXPLve`6m zqvroNTpBPpvW}tIQ%Qrh1AHi3o<+N4bQYwNXA8)-rC?GEsD^h1lr<^Sv^!I@?NQ8A zFrosCtcquoj+`TiiBq}hv;DQ3o%~%4*3p>w)LcMyU3%jC#JD!E#-#3{M6BAhJag__$7mG20UMInUA!-Qj z@P3dE(#_a1oZ52>g#6{8?Jf+p>j#0lnqyRLRPO=>a93Y{P(+D9KrxB9RG(cxnbK}l z_IqtL#O?mjRS729z zGt{%Nz5ZV|bzzoiPiA{=T2vuJ3jzJi&BRtlz2f% z6| zJ>(oi>VN3%7rlZSq4ixnWC==fdxo+(-+5E^6G=)7FbLBAE5TbHdb1t%A z%H_qVs$G;HVL;)&(zokH;zue1A!ezwtt%40U8IE-oVX1Q&q4~!`7wPtp9|l=U*g6s zE~U~VPj|zgYi4qBOKO;4&&Pro)FLAsmlie)I}m@G zXM|XCYDoT8RgTh^X2}D`@G;a;5u*2mdC**sEr2r}`%1)#R7WUAka}MHRhYu#j!%gI z+wbO%?e`ZB(}oSU92CRPw?bJ*U@2=4W|E~qj9^p4~9B1`MO0pVTU|-e3gO+2qax@oP2%91JZ6e3Wp+t7F)ZaB1M#yB4 zaA<>>3`)G{0;(4XD1%+mE}(Nl7{4MEheSD_#q;1*#8kBLdI-=<(ruGMq)a`HpNKbg zC8Z^CzPhFZdsG`MDOauVAn{q?kcU{I0v}_&gsmNqecm~g2|iix3M|Cw&Pls+*)B#c zEIuFd-mK%mc+6`o_!Cz=Jw5lH^p2r1!_=85Fj{THcJf)0)Yc<=`up^N_W|hR02|-q_V-!0=v?PxCSlqLGOD5IdW}>R?9_bi)|CHUuPdO>6F?%0f7Ag`HdrXU$v-y z8Kv$=9a;!n@PseZ6!Cf)DkFCBMY-ZoVl)nGguw3%)C~suwmXoTj=FS)zoi5mA;b-u zI^_fOr>1a-p;hxE095-NG+QUqpLY9}zE3kMO!|@)2n9<6v0QJ9?+{=Kqs{e62I+EUYjke3~@9naCNwIxA9l%n8y_<%muANu~fFLXo z%a{(n1UBVFA_NWfcLSA9djKfRF`rMzW;^P~^Os7a=T9C^m9BpN&La#i{5dB(3-Wgb z6?KE^;3&vNv@CUxPuEz(0S3oZDXEgipwQadDpzAvOJX1&`lbe6!W36FmR^!(n9Y9! z*xcg(yo$kbFzpw-z>A&E3(wjI&I>Flnj2Qu=U_p&?K-1}*{*S&zN>0^f|HfaoyeW* zwBV!w-U79a&%(MFDzqHqXLNKV2Fi?cGD^TtHR}ZTdza^Lq;(^)g|JL|v`Mq(RUdA?P3N=qK2N)4Rr(RjjWuR%O4!XJiL_1lUxG6ieWrbfxr$ zV!j+6N<3efUEqEc@R=Lw(wA&3poET@cs5x&t!tFpI9@;12=iH-+*u@Xu5C zmS|`80EGQI5z17nTi~L$*fLE5&zwPAX@5?8qqikRck{<(zH*V3xZQUOLZDB5mWm%jkWqrV?1KMD&80f~Xelr1(YC15STJtyFb(8^*h45{D z%Oq*J5e(q4%~3(K4$Mbi>`|Bx!n87Nzh_!juv*81GG*+WOFJtY&nR{P(048$XSK#b z$Q&^>Gjk5NEiN($u+)DTpI{Nq6wBhtCVf(_h4a8+B`JDyENIrLm_Yf=gmGONRz*^v z<-8Y3L#jpgmj->YCtu+xZEB5}jZoZOeJnK{T0E8*gByBF4FN}49^UkivHMeIccx5E ze6Xw}F~v{Fj-kq({0a|V_?Qt zc4y3MfQC0c0R0Q-rI{BvGb$@9e|u|g4h;DDmn9pHLnG_^1h`%;hrw@T@l*l}^a?Cx z=B=E0X)x@51iUrS?d)yS5Dd1liYvzE}toO9i>q)G@mgMDv2h1KV>^~|8zQi zjsKIt;uIRD1N9vRS*bC&IG{Je?lv-Bg9H9pIeP>^*h*li&p2GIDsN;c5m8@bfO*wJ zsZmHEc*nMM0*0}OM1TWZJ5*OQzqWZ`0p7$(G{SA-1h+xY$Rq?Vo01Om5p=G{AUA-s zjsalNA>cJT=^C|#_q8CXUJR0DTBO@rW(dVpQS@Fk`$!~YR@C^Tw>ObI1&*uJP`}cg z$5Dxv_qVgMLH3o-p~u5r(t;4cb=aOW^)tcFLLHtAP0Km}9pOPoHNI-)d0flKmIRP} zQAnF>r_O#=z-`F7j7atEhcNI(!PO?hlU1N7U0mj` z@RRx-_`kz0q6MNVHLJMe+>vw^1{BfwQ zUzUVb{KT)8YJ9LRESLc+it=#_9^cQGQPs0S`Z!-11oE^Was-ma+cl}=(XK`A1xAR- zr1)5@)b87)8u7|iYTa|(z5T$=QQ_rwuu=|KH>z~-bodp^m-S-!*^Ba=XxPYU$f`OF zf{oH4e*JD28Xfxsu=nrn|31u^$-O_<|GRnj$rw#kCq_5ORS zOQ4howka5e`CWQ5-A!F{iaNjzb1Y)WpJC}CwLr5_W8~s;!cfOnTS=ww3BCwRt*mTW z)-TNupByi}9058;^DyrFW5q{zwM&0%)`88hIkpo$+c#PYdJY~i!Iv(8B_ZdXo7RP~ z8EI~eZP?Mi6a9Vz4`2kCu^}`MS7f9FQ$y;vW}f4)t{Xe{EL@ZhN{y&3)C*ezeK+G! zu(K{Kz3d}QWL@ZHPn(bsVL6y|`;27FowUlb{PR~}^IHv{XDbhbGEPs8I8e=*abQp$ zL#A?^r?oB6sD_}ZfA+4wvPU+}3X(^Y^=YS?2~QztM<+%c+TkXMSRI5SC){E=PZp7% zZ6_sX&*mWtyW>pkZff{lOeBA{>+2tkJwGAv%I5+=PkSYu@tfz(y{vi#U>auR;{l&8 zt2G0nS`T?>qjFwTVaS8;)6-7VW77QcoP3&A#oG{?BRidtk207exGcqcAE)9}&MkdNGosF&depQCm z$4wLfK3w2ISsU5ex|?Je%X(YprOZ}+G_|Pi?7Xl}19?JfcV}En&V{&@V;&4%qp>M2 zEae?q=Gc{r0(6Om2w}S<0e^#lY&b#1@hZ&c7_j})%~XdH$s=yt{S&``vkN_p30IXv z-!Q>yqot$%8-M^R7znT6hzEx1W={${qX6uzJ`MH2>CQh=)Tt=DFAG&$ZFO#Wph?4l zYNP0xxv2i-c5%uMgUNibqmrQZ{Y*rnOrbD_DeQT~lzo(_aFo?TAgR3D?(@Q9Sd7Ju z+qQt7=&C>d=tD*w)nljE!r+ZhMUpob^)S@I-*urbzoiB($HHhA6-CC3a(e7^sV%A_ zB0K4Sa=X(+3jTbC%wT1^23tHmCJ3u?@cAB@H|xKWJe&{eW-ESv?gvfBxEWjhj9zjw zR+L;gBn0vBp#bN86X(z2f8OFEk91W2TCamq%GOjPhMHxcO=BFBqlI$bip2^2*~yVX z$v`_B7|g&9gWiDN>b6nZa)_zNutQKDT=CC4!eU>Mzj@CVaV7WMTT>Zf*M&oN=*3Ch zLv)4akPb|=Mn!DvXbm=WmMu3YFU1Cm1FRTGLU5 z_lmJju~@OO8TJuqp|r`7IY7}z?V=?IMw=p6G3*o&gIIFu(Pf};((UXAM|b=$*yxM~ z9q0urLzW3787p~&;Qa|^s;TFKD>!d(4YR}wyjgvyQ~!Vvg-wBJGzw(0CB5%q) zx`Ghh`aw#I@1qxi=syQ>XsdVP5)V;L$I|t->Fy|mR``y>i@t`=MxwbG&gwuV zU{T-?UdZG#oaD@i@RkErt-sdN>^8riV`pc-b4-iZ3@CsG0G)6QxKBrhf9|-Rh$LzS z9+(00ZMfpdyr8-F4mjZOIP{og2Q6wt%&69RyT?8_pl$DPv4*nRMLZ{)BHhgzGjVPlywE|`U8=v_R8BYA2(xmhP4>p zCBr?&4ek(0)v)UR-o5+kSx|bn?W^CP-1>ast?4#p!MEqpQ2D*}ZW$Axv3T=6WPG!5 z<&16I14cpB0G~|`8ktZ*FWj+Q9Hm5|RlXC|Tm7g-&&Tt3W5L13Ogl^r`RL72PAsUh zQ?ePk416Zh6GDwGcJ~6?t%k{uemlzWuB6-=0!2vSZb1B~T>mM{Z_DEz<7sId3m7$e zWFlNY0A*5*b2U(4eZX71?W=q%V8)7+vi+X0vHA>DlL}9_!F}M>_ZJ(tX1Hh<>bG{B zNe_LIKP&UV0~qQskS>)J2ZzzHloh3 zg>s1SSyDkV>|VSlQl-i6kQ{B7ZST6aQCb-~V)zwTG58_^GTVdvL(er;g9agV%}J&~ z^5X;&tyaLcL_Y3py`gUGSQeBn-Q05Nx&{7VAL&^WE5I`aZWAP8P@OZJV@ETfyAb%F zt76%j~kC<(yUVfet;CvT22zIZ`bai*(fD4t-NF!v6z{S!&G^h-0xoIE%=WNOB_)S3`%r`Xr zOM=%!{2OPMXm-9{Y9e31M)+a^@|Jx6RY-wY_Y5U9* zT0VyI(utzG7T)VYBGO_&@kR?RXzI@^Lqn7mmB2P47t**fvv*x;X3aZ~;l{Fp*!Ipg zh>Go!KIbWI1hlP%eX9$_K^Y!!Q0bU5ei*W4!Rg}}s}J;@7+kL+ZFjz2U+0Y+moRA2 z1hF5WgZIYj?kqoWWy5HnS%A8_a(w>xv|(2Sjz*x7YTN*}PghA}&+~L}7YpOA()tMGpLW72;9@2kVD6`k6E(gO!WNmQ z*ZA32L6fb2hTP4>M(aG`ih$Zm=Jk#r1ZGL%7nq|c5VAd@D*EnvwPL@?vdYEeLmZhP1U1611r`i%L7n;sdS>xEPe|tg4Oow^4S= zX!91VM&ZpFuKgB#|LbS$`Rz`%;i{mX`P9mI?XVk1d-73fU6%CdVTj-*+O$sjd z;x^;{X}j2o!&rqPps!}I@2ESO55kNQ?h00q$1}9>(`R92qYSt7gRqaY64>zR@_LG7*6*BcsDTY`gi%3&I-mU-FSdc`(&3IWA}mL zwiGuZ7*-N7r3yQnv-fV5IeL=G;;?G*@3ooBxFsPsf%%z*n}<@+^w68CBPjnX5q>2N z{OKUyHzwzNJ2f4*odXhfenhoRc=}dSU7&4bH6o;u46M-?Lv}8)xFL&wG_6y+N6-n2 z5X1Xxe4sZoG&rJ#v<@zs`bs}Jid1sWe|CBircZ5NMBR#8N zs{-NsjBcIgsb}G>(IEGD#Khk~hBd#Z0iLgjNvAXnc}G!J2-^cwAi~7-$AZ@QY-Y_) z=Qz= zOt-Xe-@-rIR_$E?V*g&%+;_J@w4j6{;r2mRMMQcn9MR%8F2=#8c#jpd?xlMRuwU}= z&0RSYR059L19s(i)a&IlPrnn-Wq=kH$dN-n%w)EkYf(w$E`7QYWvsocb$olm6%Mr( ze&a;xAj7T(fS4BWF#18_S)+y^#M4vpH;$4Qu+h?e{J1jEzJD*)szT&(kC_AYcJaWC zZEa%j?0nF3Qf1*l<#_kN`L^%h!)K|=_&O*EJ|ed2M}GNaMZv3r&{oE=%Zm@}5&|#& zH4-8}gbWJ=bs%6xo^53A5K^#L!ka@=C?UI>H*YQ<26Y#iAk}u&YMv=OA2O#K=l_@i zz&b{mVD;DIDz^*OJx!`F9>vM`-Rh<;1(MP?CvlX7mpSKCE%AwDI~|Z2aZ2$|yJ3ka z9~{)q<&Z=arD!}0!2O5h-4#s6qCh6P=X^b}ajJm{DT|PRS;Oqzg8ELjC2skcbL=@@ zNpW?vExA&+d0sxVJuf*N4yqMvbYKN^*Q4}A+&2KPp8FSc5fWov6oT3@wCxQVDV@5p zxYCZn?YFI+Ev6`lsWcHsn=EN{tIIRJ)Vj-57(KHl*f?f)4`jlCUes1{7NvLx4tz%n zhtrHyc;Wp}#hz-v<|9{0xv8mc0d+Io_q_9N$yd^&(6M{*|CFPYu7MgF;Oq`!#!fe~ zc6dT3S{k1UCsU)^hXYoFL87yDkSym5P1uMgfLX0*B*v0k{I>eMh6@+~V#%D)LM5QQT*-WXCTM8JuIRPH*T)S34t@d;fDf%@nEjAkwfWN`c9r(p`mwQD zbHe)C54T}2(%Z8K#l^ddAYs_z@!DrWE~uJ}dcx0mTq@mL6w3upWAiC`Xa#L53P-{K zNZiVIZdk7u=SD&)BouQ}iXSD&YFoDpjifjB9^9{QYf!1D^o; z0Rx)V1YN;^CfJ%WLqK(^G^L|tW1R#cm-e(|T7Rnd)1bARcInoaFaLRVV~lVNN`oyb zdWb<$BQtycCV2glI`E4q(MGIa<~Yc149YdHe?99bDhUk5!d-dx=E4IW*x*c|8}CMs z7tknS+@e@>)D-r1f3;WPs1Z1`Gr5&zTXEp!>|ONy`?aGzBdusZjuBhD_M9qyzup=@?)=py^`|zRAS*_dS9NYsU<5Sev@wmRDZbQju z6f)^Yg3rrdH?)i(z`B3t3)15@U-6PTqgAX;C*ImVPfjogQz{Q zFHG2|KkE6uT}_Y8E4Cj6pA)aMDXC2l}0_8O*krNA9O`?t3#V3 zy=q0kJ2im%kpK0{Lq~)k{Nx|Z<0-u*mB=LywI;jf*JN>em;xEMlh6a=Qejwxd)`dI z3@cpqJ|d|MuCsB}8!K21nmVwVvcgj5=Gc4eyw9}*oU)T0ot@8*dO$idbm;)x_}hEw zxd#B1Q5u*qd+~4IzAXjqWJkl@f>v^yizeF+FDq`mtD zKl;Q*jt~8SzVX=I38hCeds#ZX7-DOAs|CbISfQJjCS$>AFrOdj@~T_w$-Au4K}>QR zyZZ70A0sI8GkL)1qxO47Ne<-CWLSVs>=oqBnib*}l_XU~yYJtwcyR+3RGXqJ-#|M8 z97QGrTZVn3d14fqmj6nP%;jA^c?0Eyf~73na7Q3`8RZ~K6ANqx(fIddduC?M#Le8Y zLYIX!o65+@$TKmg;HAFtq0WzWVUZp`RH3^q^lo3n_kMBlxdT|Whv^vd;#<1r9xmQU zrLE6@I`njAPu*hO!Fn+SL%-|nwWX|AGPW1u-kDN;o7gQ%i6nhl3)C;diO??vd-Che6wSrD<`zkZGCT2S`_iwYEia`Waaaeg^AFh z=LEMdx6KHI)8JbBdm+KGtK3jO6AXgxzIiUpa_xyt9X?SOZf(XuegK;;z4%=7+1*PQ zaBW+CBA}_-Y9JK^-iZP0GesbQGHvQi`fOGd)O-VP zWuU2mUI3t z18H?F7~InM_U!EJ=I|-2E`s>X0-DlF0B9C_7O7Ni1XP9h6{?zS*{4QWlC0omFvA>GrwNaE$NR!hkm?%>z$I(NER2=FH9%e7y6=DKccr1)^B#@*UkN*Ofb>W zuIHK;xR_GD`Qlzo{vK=Q4lKDnxQ}pW)Nii;mn^q~gTwt?S?EtF+-A$7dcMo0-Q2Cc zRc@QIC1Oh~_4oE&xW6q?^4jkCMC@@2N#bL3Q0Q<8*dcO@#ovXlMLMnWivwDf|8?*^ ztkZ#r%6Jk7t7_be4LDtW>5RDq`(y=SaBj@eb+L{^bKlnD{hOl;Mg%qGlS(H86q|)v zCAK)tARRWw$GMPuYR{^jCuzK8Lxr?J%p<0i7Ln^PLDrWoEc&krdiE+eusxrby}mga zM@%e#0Lx^@HKi}DT;J4Q*mRkj{FNl-euM=Qx=*KB3xQmwYeOZBkghpX;sM;n!#4+{ zrbG7D;-5Vs-55$wN)fD}F6oV$=UNNKWT(}ObAtE zXJukNv5V_-o9kd<%!Ji_J_V3+$_-0god3$o-kdg?GjbCctvKy{rn!@9C!rBjdc^zs zaG*dSQH|p3WqG4`Wkgk({$iZq#dZ#pH4E4v!9p4I%= z2s!N=80fII5z+UVrr}$-J@R8TE`4Fh%6D<(_vLq`cQ-o9x|ztYo*OL@6sF?_lUF(> zhI9HjfErgcNc`j~^v}|Piff^8uyjvhjVSEn@&g$KYwQdGX2`KQ7o|DT-`FUJxlh^N zl9hy#InNUcrIVh(4^Qd@ugI@pyH680-`devH2J9Q?76*0=g;qD-ZXEiYt=cMh~9bQNmms~jZ2-YnCG89)UlUL z$E<5l^lFx3#x8CIk;ZygrRhhcXA!~zc+i&o~Gu#KIZuG9%x=3kZ&d+qONhz&;8b|*?MyCbj^>8;tU$# zx|9p`_7C>IRp{xNFmAvjr+qOJR;QH!Ze}D2}rIs6JD}O(J z7LpYCLo#h%rcU0dy1(GZKm6*qf?p`cY&2czUSCpM!N+b;*?bzB@&Sy?y*2jJ0kp>6 zK)DPr!aK$4l9~mj?b#1KN8gRz31;&jV5uv8_A1T0EgAGeTT53}>c^<6g0G|fn4?EK z!Pg+>sCp-Hpl6_KpsVZH@#8;pgx}`ml(_gzIXZj|xp(iLH?hiwXcqOUys{EwOYH5n zw6PhuY-2NY*%Ex&*!F$MlXL5tX-Ni`+DTfdb+YlrB84+n1_!7v21!)@=gN+^KiMRC z5FZV@SmLgYD)ebX2_NTDc2>Lw3E2eGSN!dBbB63j(J01G^Np+wSC6mDpCe2c?RgaY zVb6(jl9zLyvs!wwLvh($V#H90S5G;`OOl;tDEXcG;W-NZ{>r`UAhgUx4vLaVqKdUp zR)X}7)4^(p|d1mI@?I$(@uEdGTZ_b$2+k}>voR*V`4I)r}6#ZRGLqxG=8mk5PhT+B|u;u0E&rfzBVjAGDTNjP9{*~J#@9cAq7@J`Qwq!rtCzy03{)0EeZQgI(p`z{sw z*-L!JUFaMc?qMO)n1%1f4ZgKYST#EnLntJDem53FHjf+pO*Eh+Hz?=rf6B>3&P6R= zUv~8zyK{EGL&C@FDWB8L)zrh;w4tdRfN1c6!lL+}X{WHZmHLqJFCjVuyVd`CGX3W% zX~leVwP#7k5|{i3O_p=k?pZZNI^%4bLl@87_I_oP?iwY8AP<)+I9THJj#Ih5V<16h z`X_431j(V_JKyxP5tzJ3^YEp-Ua{|=paf}&uqY}e7f%ohJ}Xf_ZWV~IBU#`9K~-~< z#5TQd64=*tQXoYz!KUw6zGt|qOm-UHDEauseWUVK>7dCy zSE_&&F%Gi)96e4ds2Z~F zI7pOAG;TYx|3>cQ&s8;#@viNX?yDb>$m^?tlLOSqX|Mi<*B@t2+$5UOHw6s|8iF}N z?~(6eg@2t8Fff`iI?r}>zN%%$Bhaqc({VG9%mgbgZ>@R{9k3!o8D7NCH3}YslS<8* z@t?hfCMoh~*Xp=S$qaZ0I!hV(@cljz<}OZnSIpgUd<$vGx6)V1^2Tf8!iNru7`hb9;3NL+BM{ffwv(^EBV!C+fl;F!9`A>a zH)NgylK24pLF=837xD{V$E|xCGzs^uk5eH$6MZpJ?I^4VflY`D;rJ^c=|;NRYGw;L zLV3Gtfq3`nxwTI=jSfeInXlb)V1yr;aE=`JX*D|*d2VuerZ#dy1d2Wl#+S}M7lL0s z(LKpje_(4{ZeS+%mGgn*8_M$EuUUeFaQL+T$Y#-5r7mU1S*sS^Q4Xj{3p^E;mE8!x zeACyvBtr9GM_D8%-vdGKzWy8XlLg3u)Ehl&Bf4<>hs)Ql*}oeXI#wBE_1%lOX0f6p zKMO%jbb5Lt!wSDl(iY}$A8N=g74!@gd0nuup>$~35}Gb>L!;Cr7EewTe*Sf9vWc`k zk+sAOeL;h9W5q6@qP^rMLYb4E^1Ajre9+0SG$XEXM|=$TVuu&r&=uiDz8D41opneE z>~Z+cT^h{*`RqLTH!K9-9Y}O8vnei4YVpla?Lle6uzrMAMf8MnHecC--v?hYes>5D z(cjM?b{|;N$3b{|v%}ezQNO16OGj}$ve0FcLx4p_ONtn++ssp_JWXV05YSi}bTb1< zex})k1JULRn_sZ4ddRDS^gB3uR-{XnUS-O{mK$`a0v)4uq2Sn<}YN14sBZzu%hWg%#Y;Yz&OzrQfdw?&sXOUw03D`RP~k zEWYB7rRO;?+XPQu?IvQr0@t2crJnUN>-Ciz@{N0qCgALlF`P~;R&UVArgx2d7_L5b zZM3#6pD92Un882IZz)d}ggopBv>LiMyXYw23PF-$U@o8H@Mx?|l??j*L|1g}LOq~95nDL+=v@euaMFqRYw|V* z%8;dhpz7&|@8JP|3$*TMDfN)7;*ScmH@*9s#J!wk^K_ghE?)*gdQZT89nWO;GJplK zJu|Z9X+0b>BWRw>zxN1L-FAP=TEy}7!8vB=?s57u=&|jAXJ3uzgphhOXvx#2M^m zxV7^5_)-Bi#g11L->$3kY<%sK+(6DW7)XOYP>(E%Nq;G=AnHf&R#jA?HbjgF+viP; z`t&>D8u*-4I^%HvV2WZQw;rCk9i*lSM;*mXcu=(X)sIsk{B11w{fT1MN3aXqShVBq zXSj=xqS+z}lYEkbAm{=T{MI;D5&JIpj$48=@zRS-?6dSc-$4;!SE{Kcw~7`n##`|3H&HO7zfvPFAT21(0Et*9 zteI4Otei8k-KRtAJH7=$f@tt7bO@JrFh%6f&xIo)oeHYg$S|l$5BwaNwuJLvmd-&BR8@f=LuYi)>*Q+LhlCrS*JEcrzzTlEenwyC0zhX0|3pGa zCh)B=r%MqWTZrCX+^2t*ay|c+iuC!~!>!o^rl z?s+bnyjKlz`M9K>Vu(kRs4Oy65;wwsQYYNBS5z;N_1VF??79521KIXrBaY8mk&rGI zLg4#+lI^eeDg?Fm7v5)aP01=o&huC1mS$r%Xu3n+JlAb+rUtgPsQY@*dWOq?E|5UU z=>PBf`uaHl(Sq(AiZhVpr)d^G`oDLet1sQ(2R{sl{0+$4kg;Gu;GNe{RRw*C{(Iq% zCrVPTAnzg-^UyE|k4XHgV3s~&c;LS`(Ep*xym?>v8p^&K3&`L z4>!OJV^tYKP&J$8)K!Svx>cOMpc1^k(p)abIM%V=YHy{K#NA3T? z`zhyzH-n&b08xFFaRiKwFU`c;wd&qx%P4x%cn{t<@cFSoc%BqaMv1N$S%@5SKT0Ix?LY<*UV551SBC1*)gfI&$XCGF$UGNnlekl0z_TLXXM%UwU%sbx@t^m&Oe=n>FzSk~sw2dos ztHX2pWvH@ZIv7yGXs_LB=F@~mes^2D%|nA0%h$A7C%?PN?Hup_K710MiP>;?-TlaN zniLfz+`%xx23APo1?&gLachMd!v6Kc`SOcjlI4T}mHhA0Je1#I#6vLBoLG9iy^tkt ziYZz6VzM>*gN6Y}T9Ab9SSXaAe@tRP*F8w$R6R4?u5pP|s^R411|vpRe9pzo8IFl;NT4&G|6HDDf*%HD_3d}U z$WGS0LNYKu+XHzpvp{Hb)`Q#S_0@Y|evc(a9Ezb!_m{r@Yvk%yC=)|+_-+6{b?XA> zpNj@E(IOCK^B5>i!piIT*}H3;P7A>e+m-c==6J;Ddl{s499dC+N<2Nv_AeU2J0AMd zVPqOmK=f{{plOsgw!b8vP%y!4I6|wV?M#3se}pfho`t%L3@DhUSY*pp@r(XIw~vzD zPE>{cXA0nZ7}EplU{u29lN#Ls+}KBsUh*r&BE0&E%{ev@=Efd}V@&J$C=MIs#lteb zxGSNLK{<{=<8sfnfRq1XOe2V$Pw9uMvj`&A?$d6{YmupY@E{gQpOw3_KH2EI=3PNz zwsN87*SGf+t?;?O$({dN6#FOpQJdk4fjEOBTJ@}_Yy-D5x#7#!wphV^l;wxOV~&=F ziKt&aSL$|EJ{?q`WqDT3WaWRBEgdb&$eXHIS|bLg^OTxvJC3O$E^iEK;|{NIO2??Y|IoODhL zTstIpC}nd?(K$U3IS?JrkQ?+jW6Ik|81S?_iG$mdlAAe6`$oir{$Bja|Ft~d@3(*cRv9w9+n88Z9u)MU2^j9~7nV3WoptxC}@O z=g}zF|AxBn7!$wmb-YE(Rs+{^;6aA^4&9%yAGkC@Ku_l-#Hfv2quM>mX*^ zn@~jO9M~n!QjVeC(S~%R+gp0Z%Y5cqP7{3&fbHFPjJXy!s~U3?^z z@$)ZcZH&)6{ezzsfp*7>3j>YHuRq$n*{A6yoT;-~F{{OwvbeoDY6ujBq=kaQg)I=p~(_SNHX>rm7U0A zHyCSUAG_J-y+_aQ`M>Y=&efIbx|+GaXU=`j=X}oR-1q+_NkRS)PG;<7xk7|x8lrYD zzkbb9yV0BUNt|6e)Kgb?5w-(8R0e{2b=|Uowb=;5ryViznr}(2EshB%O4uYTNq29u ze!FOW#w|(wo8+ng79G4hjMWH})W`PU!>{EGXCM%-CaF3m)kamhRl49-20y^b1)sGT z#h&|)Z8V5F@2`w6Z1l^-nk9>X|B#O%uv}`*7L%@72%v`ld($L!xAWr)kAG2h46SM~ zC=HoeuQ`{oTw%DSFWLP?aO?wndw=N_S0fPM^h~~Ia9pB(t;;Z&DN4aMKAuu9&S&(x z^hqm92Sx2swb6AejsKRY^WS@K+Y!Ri-``{B5H)S7d9At|E(hN{`|CUQh<%UWSSP8! z>B+k`Z7|aUS&JQ5#r^Yvqbnv*x{AlcO)k@74>KrLoWI02MFh6oR(PTUAmzU=x$jXC zETjknU0=9(Z0Q5%QX{`Scs@e73@22KafYqK2T)E(g?&4y|DVwf<53$b0My3 zlUGw2@9i5*S(a5p+0oH?(N(!P>K@gxta~k=6+*WDUtqewMHZVDlUnl2XC}GlBt7jZ zYft#b$G{i*w%)oTX-X50J6WAG)|uR&^W+^&n+Jw$#-%yTgR4|o7Ab-*<0gK)N<$g1 zoOy&pnV6XwFq<>;_MUZQ2i1q|)$AI3rnOi1$p0P*()+d}lJ`sowXfe>u>-s7owTdF zhjyXZqwpFm&QasLDwd4vPGza5LvZ{FYMS zEDmS~mVRycF5VNJQPjQq--kVQt~(2|>52wf+$wvd?(W%^Cs~Z7rdDlrfa# z;-<&y>7$8JT@DMDzIxqBVw4S?aE@8A%o`{XE&(ndo|=;v87oW{@pZ0MYFhm9cK<<- z|9xJI2>8H{JC|iwl6$jHpo#f#%MtipElU4w?;sj|-e^s0kf_oQM;>q|XPVjO1It-$ z)k73WK8H?Z-@hNcE~?*f{Qmg zi1sW@{Maq47#K%j@$>(_Nom#J#={Da8(``ekAKs^|7t5V3A{jzT}m3oI%ym1?jhVD ziE;}!b1#KMPfhssQ2F5Y(-Wa`V9V`a<;77Y`2XkItK_dpIW0X4U@x@w3iP{!b zmQvz6HujP*9EUJg6XO^%7K->=SMYxcMo<9BdF{0|gtKt3==4X#X8Rs(aVat_WsQ6n zoYg0T^Q16OYuopzS)_(X59~$bTc!D~wuZ@Z>i4YrBnyi8k>1Dn#gyN1bUf}r*r9)z zFJ!^e_r{}3t7)@l-i|>*3}%eD&b_#P5suvAyZyOBu$X2G+r#z~5FUwapL?6+ye`pUZp5|N|Jv9|&nz+KEeekK0?@~$86*jJbYZ7Y&7{KEvNilouzx> zK+l$NzNwm~L%gS64_OrXUuypQwaX)%2LXv8*B^;!@R=SfBAz~#hO(u&e66(fB^)u~ z2fJBkS3dejU<|9RVrb2ww8!LPU5-K~ zvG>m&_(|=^|DaF&q(c|-s~6~l>j96gD8kI*Tf{b2GNXl#c`%FbmB%q)7v7IV>St)HEV{r>;~MG z^kC1|=4E-Y-Yu10US1n%j~*H$sl!_cHFGie--Q_O4`U)kls}>$z|->nNqW`9M3R5s3cOqkb6NeLITCjyd+R zqMQDu@Qg4CefhOX)lRtjvO|-z+?JYi`)?~nTFqP6=dADgQPbMlT{qOVfz<;KWQ2K# zW0&p={<|p!1o*iJavVc1%?bJL7i8D&X4v|tgI3FHF*2~s!7-Whu76(sD-1Zqb$-fJ+zX`VJ4z;bLL~1)T zzW}dMt~R(TGS>{UlFUL}YQ0i&G02ATYB2oArStyqY4chAHvZ>!&cOwHWd2Ae zsv9_bM5hl#*37q0+HwL*T?=biy78b0DSbu%ymLf67yfc1S4IMW2uy=cNiR{E*{EYEHC+&xs~6!Riy}blhoJ+cFI~wVDw?m zA)%{vJfm2hSR`L31&9T!NWluq)w(p?Q5W4@)WdV7_ZhI(U6Yq)A-MY%;~RJ=5!mG4 zOhN;g?vUHtdc_CHcQy+zZ!F{L(4*1(0p3UkXRSsl)2}@7lR%cXQF*wZ>b1CHR(@H|tvqEF#+WDC1LrbJPRlh(4!J4EK)sPME?> zmiMY}Y5-eW9-oiK<&R?T={%#s`iCZL1Ln(Tj`V9Ps^>`Rq_z2M0}&sJ%jU$&aFXRW z(L`2kjQKyCZyBDQX+paktzk$I(c9Nm6}IIeR`Ktx-9z2=p*~@! z(_FG{f4c02${#lR6aJs`T_Y!T7Dx(UxP`wR$0@yfla$0-i!GmvcPg#$^6yuGtq#pl zT!!N#hjD}VcCkYX6cb)oPB)d3K(^5;!SwT?18o2d*%EeomFTlb?(yJ`cnTIG>W=db zXwTz4P_RdA)Bfe2>HWWF!3u|8K*L{ndCX2$?_fGn zMd~sDRmmV;rhvT4L+l!08}NV?!0g2?tkFqc}8DyxBDLdvI8Cu?qW|vq-o2R|4+w#q#pBqT3=C4Wn3Q)Fg zc5AJ{!qTx(A;Fbyn}&I-5;?{vjSq(De5T-C?68GeUu^dQyG-O^tLG;Yy;7g4-%3?|^ z+Po3f1D4Y)4v&L|T{^i7r?Q4|SJWWNm45KyX4BaRez_YWjlm7gS{7`W4J*K0VBs?5 zkZ7Zx`mq zI^AYp#%Kw9n~Sj)cYYQWb+r{ow)_SRQhA68^K=I72Q)hT}Km zB@Q`+V0tp4c)wF+F)7$%04T8K@ZU3Ljza|GnuOctqy%KJ#0x0m<({ij9d@ke8N8I;z_g}$;(auWM`bT>k|UR^Yjeh~9zvE2HYtX4mR|}E8;rLd z5|S>iffBW0z`FXRa#|^kz687=mX>oMz^UI}aN!A$pF1KivYcVnIg=7lA+r#=ernak z7DOJ}?j`RThs|hl(sNLd<9feiKgb1NBEd;MMFq!tx?W%X$ERtkc`X4NHlVE+Qr|$s zhxY2(xiNfd6??kOr80=ygrKuEqegFHw~Dn&8X7O+D&}4Y3?B-tzMky2^K_5RI`t3_ z{tO>|`%H{oM9K-{qzPbQJ-;$r{{$r%UMA?Se+r{lgBQvjSV8rIqP(RaeE7EC;mE=v zUh`^UdkxFs$kM5{iaFI7TzWNi-&R>{DdU$jpxH%*m4V18v$F1}GLg zK0C0Uy8m6jU21NA%tP-okP1<~+c@#A1SGca-Zs1{-h0l$3q4Ef=V7>+Q5WDPXL{;t zlrwo1H3|k`Tx|@Fu}H}qe0qHd`xSU~lCbo+!r$%Ooz@-*0Zu7FgdVu6C=b<$a6wr0 zXUf#FEAciavM2JyHxL@(#GJ}ssnFGG{ba)P~b0w0<#@O+w>=z#)th`3IRFIG$p z+GZHi&Lz}ftzJiq+&Wa53@A%6feXxX@77Zz4(_c)+@}^zya|+c7OZ;`5O_V_Fi?`K zu6cXwzpOnXYzxrv4PJ_Zt02g#`gP4SdlX?B7{JtRxaA2O_oQk+cgZO#2v@Ns+G{uA4UAmv6#-W8M^?3gSzIsZHH!|{;G zf};T@+oYSl* z^!1ZD;@6HT|Be%Q9#EB+*V5MGuNv-Yon3S)kMhUrmv7CuNX6SzJL>*md5~fY+Ww%k z3$d|&Nix6bTV=rm@QEzOyT9U~t+f&_%_Z4BB6lOc3L`OS$A%@{ChOe{b1wryhB8U9 z_m1=JCV&4d*A(r)UyFWw6J$}Y*nQASct}WW7R?tVhb~zzz-${#!ZiG~c z#x?Z;gmiid@~c0yzd|WsR{+{4^h4}xJ&~KF(9|hHq>qPG&wr>K-JjI^zmUGZ>yTlK zZj4ptyz}>v>`YX{D#_jyw~7zj?So1_DY9O~k1_k}#&f|FEK=GxNrs61wbN${5d{Ll z=ViegAlziDkIQYL9dFA}7Uoekq}~laOz41m+n7Y2(&<>O>rU8r{LpVH!FBcePn?HY zcb!y8FUm`)hb|s}MDA1wnL8fA`ltDvY*J=#k2k6E;|2=e@Y%IO z-hx;xkHZOKsSkWRjpx&`(1z`k(K_9(26s48ZE#I56+Cyn{{ zw2FK_`{eQ_eZd|=2wDpF3+xeo%)wFY+i)mHehclhZ31&`aYTv)V#lk0Ndg&{-+?LV z#hWPtnJr_&j*y3;XxY!lrBt1@Pup1XYlU@BV)5OA%V83 zD(z%>nMXVDuC8*0hn{%%5l@Lh`}O$s_>6<5u^>l{5(|(Jtk9n#t3%xvoz+ribm$w= zdBJBUV@R`Hszqz(3qJgfmlC(-7ZcGkr4baxFgD1KyH{P|d7Mps4_&jjpCEZx$ zc`8|3w8{V}eB=4qk|x^v33kc&Fw>M2I_$6iL}hS}L{xh7_Dg6{>P@xa0zBih|KQr( zSVwm~At2i_h4=}Ssl_5XzXd^mAH0A4?U1P= z&hw!6lTui#=H^E@&*U!+1s|8E#Pxqs`Wff8ECA@gw5qs5qR}K83_3%>^r{x+tZe_g zM-9ZlioxBzuf?VxHW;C#vaxqd(Iy!o!IH?8h%L?AlH{QfiB>?Ya3; z6>aR2PBkC&c4Npmr8mwmp7EFvAL%5`+QN~x-nARIT3CYarGG8%6TabM{rg9E)}U)aRTu&O z2OK>e+g0Uw+U`V0i+%Rr?T;G%+SZ^;aJNBFE{m@j)Pz?1_`pnuDZRadOXZB0NQ}5W zDji-g-^PBfkLXb@76|S{Vx@40_eD*T8(2ApYw^<0a-)SEnC#Q&;`oU^_}ZC$a+@lp z)7?+*$@Gw2^7As|-qJl7zhmtzNxonJxNp2EY`NifE2jAnpS16`lMJ$)ko4h2>^FlQ zFKW<8BuYM=DwBTdck%C8(yN)zi_5;73abKf?MD=^CWj6Ey2^fQLevGUpeuaF+uN2T z!$x;K1r?p|Vx+g!cZIJ7;nCDf|APJ?bRB>w8`yQb_YW;l(j}d;mi+3=p%)i=d?yqn z<*Paiva;ZCkTFwd-n~071ODZ4@nho2SNTA@`8(#i(_i5li{paRd0s-S=2V>$)u0& zr%kK>5_ODcG-r7ihg>PmltWhg!P$}GzAFfdY3lO(OmS8b>F6WW#d-VLbyWrti4+c>^hB

d0uJM(G8@|EEK%&_NBV%*XG2<%;Lz$H?X-Pm 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 Tg)aTC_nxYvrb4xxRrvn_=u?>A diff --git a/client/ios/vcmi_logo.png b/client/ios/vcmi_logo.png index 73d4324809fe875d1face35d262eddff38a5401f..12a7e7f037d17479547eb58d641cfdb714ef34a3 100644 GIT binary patch literal 32594 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 literal 23326 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 diff --git a/client/vcmi.ico b/client/vcmi.ico index 2c27331dd75d3c0738c216bec81c12bf5d49956f..0c431b80834046e105793a1999046b8dcc0e4fad 100644 GIT binary patch literal 410222 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˻zK%)_;?@BFIcX>$AB-Gd8?1^HXa9YVK$2#64W=qzDwx*rZO zj?A`=)3D|NT!3#-s4dEDf{o@M zWHrZe+%Rwfd=P}$Md{p<`1b<;CHaGFh8Z%<4~^|p6RD8vnk6@)xuh!?^E z1s)Iw7!SnnXCM57ZF1Jw^xxFFaI6i1FMv-#4gebf+kpET{~!HEO)$^F4Rx8%79TsE zR{$T_45PpY#hC>$55&h{AN&s#{&&?3{vq>Dazhrq-3{O8J#Lh2{n}O1`D@R1W7nVS z8aJNjnl_y08nxd54~PTCt`Y|);%*LQ?(;C%#QhEbd#Z~-+1p|%JvNb?6Ai|?5nKlVR10C_#uf+NpQ?GHIY zw&eZLZSVwfE@}bD4Zsa!#+3JhQ*Y>dl?%OzFZdosIKZT2c1bqREdG4#d;Cv5zztUX z5Bv`m{)g@JZZ|?PzZ&d)kN@*r^QH^jxXl;3=8Yc~4~P%eo#PtToaq|G0b^GxUZ|L0 zy5e%#Xcx^eO?T3PE)YkYL9KHE)Pcdnbf{sOJ_ zp&9s_E_Ji^z0%Fu?;1DvfRDL(2i)W?xGb!{W9xsg1^|5k_(ysAKb}oMJ;5=u1?WBS zJ;@0Nzys;`;bR2mBM>7n$>yBfOjFArSbY&Tfz=e0aeW5)*!Ldz9;B{q zg82H`-v;`>kFJ@yKRZj9`#}tY+1t7CzgwRBoxA0^-#U1pgbQQ~+?0vg1Mon6z2k*l z^A9Xe;ByD!0{ntl%pQo}&p!GaeAClQogcP6y*=czSQ`qP0KuPS9UyD~^c+3|_$NnD zTU(C*yxv5!39MJyY=RIc2s2L8>!Y@Juxk8vopW>n|3iiU;qn1SEkD)A026nyZMLRD!I)qJot8Z@m2p(SpN$Su(iKC zi~svN<7jqSs4>iD8G1e_&NhY<((l9i5G{A$*F{sj03L{I3Su5$-*~`psWr^)J-|PJ z2Z~%!h#TS`M0{Xz!}$7IJ0@kbO>M4es51;b2w~VgMVS0xWC2z!GGNQ&%7_<7`gNPKk>ugxmzWl5C^~o z#sei>fPWw^fCu91#r`LM00$TkSUo}Dfzo<{G=IRc^d0-YA7Xy!eC&JT0N4PmB?lic z+lK%1m>L(>kRvvLerwX29yK-P_%HMzWPJ$58AkcDOUx#SKOg(xf43P2xgm3v%U8Y6 z(8cd4=>L(zKXJc$>F9c`$yYDiA8FihzPAUOH);zvzyY!on%AG>TJ*XWy&h>+>|k6b z?d#8X9UCrk6E|%Rhj<~3re%F-VqD;#+5h0bu8!ZUJpPyVCHg)D&jsc4 z%(gVYq3E^`LXz^g}Livkt$=%|7xHFLRH&%!Ie~|uO@thKi6WGim&jXon;ByGF3DWm>^|)Xc zC|!r4ipHMp66dq8_piobs+Jcw|mYnvfvg;%E&wK%D zfK*%ZLt&>|`1k9jYF}xp4*1Kno`-dNX*nRJN-*b;W_L%$r_rG6|#~**(Aw2iUBae9DwGTe{pnK?{hy0d( zj)6S%(1Y#~f2>D!tncYKkGm(Ic*6bQsVCjjPe0{;_}nw@`5!*(UVPzs_v06zcQ5_; zNA4#-dC9%;w42=MMJKpX^N)19&pFHupK*w4j@5V2u||Cd<=+dlP1D|^U=w6(pJe?8 zVLrj5W#5POBp2cZU95+eexF@ECPuJ->>r2+Vw=D?fH(m*L43T{_KuufVm#Y4sx1;1 zEIs4XCGY20;Gfwd_?5!{%L4`sz<=&C{}2sAN&*l zqwkj(AhSKl?V0|kua%r1*aQEp2TE*!+&{TNasYYwFZ3y^?)W2F?XmSG84uXjW|y!{ zua9$pe{5>v4~GBme)qct?1TF!gjJIm`(PftgK^&To$q|def!(rcHjEew|uUHEyDkd zPvDD3AB6`V^T+(b4}KsHc-lSx{PXUG7hZ5Lz4VfomtTI_9sS;GT}Ar=uA+6bt7zHi zdNpr!y&KlJUSpQHn#S_J!@?ZHY`ud#E{OXM((l9a2Z0B|dIxX;bp_rx$ks}>I6?aU zt{#W{f#L;OybybU*kKwo#77`b5FZcy@BM?Uo*=F(Nb8HR36`DtnUeQ&ENXwiKQ-ml zmI{yb&UcA_t24AYW`!AMQQdJ_Z?gP@`1;x33jBk8?ES5p@Bir^|IvNw#Xoq7dBEER z!oRl(;DIbo5MOUw`}h*yppZkzVg<$r<`=My-_JhwKQVsddBpVT?~QE$n`?Q8){+N% z___E4^!?Ez#F}Aw_@@WKW|v?KSPimqfY}6buQKxr((42N3;~4#I zI5o_JS%m+CeJ~BSA9&yaci(;Yc@De#?z_FuPK<-kgs>04@fz$4@}Tene8G9(1kM4+ zzy-*2&pqdkc-K{~qHSN{e}nM9-t}r)FZ{3W6#sd1&QJ1Oko6&|7tz){fCtj=BlQpb z+#=-=%^vVvkl6%z`2*Hq9bMPtC2u`eiyK-^0oz~`& zte@*7-q-E<{-1g2PYw?F^ox>QE|7m9zX00+dmz5vcIkhM6Ic$RzyTIBgbO5AUlhNe zed7Q0lQa9rW`8p03l6}C#SXy!2lrq8@|V4UN9qBY13~R@9{#QF&}xtAN230?Ft0$I z5ICUNo0!+12>$6eBJLR5|LAh`y4nBc1Au+@`9H4#_xIj=ufOgued$ZyhQNOzPKOY$ zH{mfh2JsDE3+M5<;T$-_xWUfNc@BO1rLI?-bpN<@uA+IZ@W0OW9@{zoPxf=p!))VR zE-?Q<{fEpaNPn+5e?Xkj#LqL;GKvh^gLj_$y)co z_#mw3v&$T#?d=YaTnTie>B+(C#LTAZLzQy}b{ zkC5gMI2PEY?~gUVjRVXMV2x?8PR<|fWBa2k$rGUW=@FtoI1m3}RzWha01n9Pf`WfQ zZ&Eb3B>sHt1KzpimRo#ZAGJN7{p@EwFYrBJC!GBLOI%gQ{=)tmSJAXevSy3;xAhMEx+k{g zf!02;wUBJ>6N?k1--r1IHm}I$l!QEDY3=}9Abo#L{Ww=KxY<<GOqp0>utP zUvhDsR5-zSz~&Uh?+5>JonguaaqfUMP*#2D3uWgg?uY(owg9!I-PQj#$IN&jt}`t5 zBVrpA?E+$k@%6L675INe^Zh^flRvr7|Mbs}?Pp*5V}T32O<;CG!~^m5wkbz~Z4i8f z-}~G_94m-z0^@`D{p{msVFU18V{g&x!@6>FwYDtzeP)1w6Z9ZB;=ZR&fZAW||IYNk z??XsxkF5v6Yy#_3j%|YU`V9Z{)slZi4`Y*KkDIPX51akZK74@PkG@8S6W@fdh*dJ{ zjCsezE%K)vlsph2QobyGr)|KDqwC z%{mY3qJ?#kf=y6d2Px))^!rHj2c8cKF~jop)6@6Yj0OKKUf55cxB8+(vzh|qgLp>4 zL9z#|rXYTQOKV$MoB$5!tfpYid0#9$KY4%fPaLo(_%B;Wy`=Xbe?26)AijR~w*vn| zW$zQC+^YHhUl8`c@baJC=YJx}cpz{=unWv4FyA1)-d1rXeu2deL%cBK0p$=sk<}K( zJP^OXJNw{+{697^v43n~*8Kzv;064{AL+3QeTmc_TOUH!qs$DW(3|)(?-Ljg;3K5R z!TzU~2K-}#pmVXm!8n*Voo^h#|4sMf)1&vnetO((^?1(Qd2Z+NivKl8_t!7icBSyiA9@nCZaV87Wa4L*sJ4K9MDq`<_aHugi)?ovD~Mx;asD8S8CvdO&3Ruc zyFP5m82{2wdkI@z`v1jQ=b^rEnd* z#dAOj56CX~OlB9r0rB;M|I(OYumuV+L)iw{1F=mIzrQ>C=wEVy*u55iBVT|n#J!9Ov%SpTE@;R$p|z5V2`JKC(ObkLwDGToC4(`rd<1cp$!h_O}B6 zaMM=L_y3Qd|J8j-0uOvq_(!k@unPhYWWIs-5B^m0eGw}$yPzaipqyeHGxU6rlRrp5 zd){O8FZd@$hW;l8K)eu}82i67{9EmD+>>biNX7-$lf;Zun{%381Ad6*|FJ>v=ZVc* z>>qt^Jir!SAl{Fk0B7OLbFJxdx7FimGv>Iyiu+Ya=Lh}2%Jr@{{a=p%Q#9k4ImhYR zCuS3P{~%f~IlayhGt65rIh$wNMgAb*zg_rm&&7WskEk31IYi4N#ysG;01im&i{wj& z`~iJPIkQd03vnKC{RN%lUpVX${^>hdb@A7;`l8|*C^p~NuZyO6hGrA6B}R~bzgvO- zS2W)r{F{799DtZj02jakaDl}M;_JoU!auNBfyE6AHi3A+`v!#=L23RVK2~S@;Gg&# zu`m38V&}{TG5j+-C{O>Vb4%cZ*d{OzV2giXJ<92IfPZ?As6pTwh(qAlTV4(xK;I+S z1;qS#9-g94iTou#OnTgH^?35M*{-s2Wx&4I|H8iffX?iHuph5|0w0vv1o8*s7(sfS z@p{P?C$O4=U=M^jhQ6*qIfV569{)o+WCM)P#lPhb;DNve#kCQcUF3Pd)T0 zORg9Er?~^4M^wDf?14{3^+nVeS{|`9R*?A!>Oo4s5AcWnr~VI2VTXfx;$y_ZI>Ue3 zhhX+V%mcwTD4l(to*x^7nk0IVr~xEDXK_ICdiURdzZWpiW8#xsC-w@upZoYsSj=A3DKR3<3WwWzXBXh_>E=)e{u$ zf?yLA?E=LLqJG4<2dR_`A|A;6gH0EguP=`OPyZi1z}WxdfNt6U)EC(tGq}L7eIR=v z?nlH&C~!fxE?W9~ZYBOZo$vp(-~6}x+Hd~h-~rDC86S8q5FgwnK0u5Mj050<*e>w? zfp{RkUht1y@Hxc}OJat~DSEp=bw%Dk5C^~m#s!6#VPqFroFG0v`{;jkD0m|NMo$27 zZ}dMgaApMM>Ho5xB%5cZ9_2!xayH-8d<1NP^!(%^=trV20S+K9hhFD;u;>Y6c##8^(Ko4tQX1VmYkLKCNsw@JK7a=@3H|~7 z2Vt(M@j&|hYz6*pzF+Qq|9gJ>KkjbfA1UF1%qGA#fC~Z-&4!}KZqrd z2aF5w5AY3aTi^lN1=fEMAD?}6C;5M{Mm(L~An=d=-)HZAJpSS7^nEuJ>WnwkM>qq3e$k&5)#Pa&^1hq?GpIjvzk{-9vpR68) z^0m~99N>GCnO}rmARb7My3|6 zi~v3`9w^!br85nE{vhK4%OAi4`(5_6vg-r$?b{8k(wF?%59f^r`r@Ilcgz&8kzUa!RseI7C50&D{B z8)W%|kVg!)h9OQ+vVgBkA?lG|2uRHpvV7>GOqtMAaT!4^pTt(u@Mx z1)d8&VReSZ^^)O&yxwHu1&7HNz(*+L4&Vai5B9&jJpT^OZq!?_1JQW zv421wF$n&_pS&GFyuj=MI3PVPSYqB6J~zHL>wwYw1OBNS>pm0`Wm{PQj1;yrS4QU|vaj4e0+~!hi2^s{LxI?oRHV zIRS=$)&*h)c%J^hIh$+dIiQmnr{#Uh<^4(N^=zxhRU2)-9FTPuw|By0ljBSii| zUVn0j^nb60rNaLP*K6D+?W_Jbv;S)w%kh8e*TR};taXrbL0BU-;{w?Q?_urqJA;1! z4|pFT$shFe`>1JXbG=5)@WOsi-yiCWigkve|3Gs~)PrO^V6#oJ2fSUNd;$CBCj^_| zL3hwq<@l!`mHVlu__vw@KfCD4(1V0M;BA7e@4#jn8W*rlKmV(Rf8zn;0?!BH0dEsT z_JH{a>2>w=amXdAt{|!@NaF;?1^5ZUKgh>Fad+#xqxKK`ADeqi_^0O3*BrwG5WL}A}-0^1gRgNzTDX_%hR@ZX2tUvWV1hPAGb;sAZd z?B!~6`hRT=BzU061<#5LiZ($B5BRmyOV>$_ZG!ZAx_VsG*zS6boa1_roU8i8a(?RS z{nmfrc|iU^@DZ{;B#Rpw7sxiqw+SBfTyV(M_m_R2tZzykdQb6hGYaDRB5xCj3;gUN zZLK#s=7IF{{xk4zTu|a0$Tq+yF#jOEuAV;r55)_pD=OiELLM=$GYoM8HkXA|5uLLI~V_~b>L^6-yPOR z^lP7JA1;6cifbl6C!ZkL0$Yj|q}S8cV>qd@X0hv2vnaPdxvTfvddVl0tdCea%PiOg z!5%2F3#ci;CeRjp;LvN!^)L9RuMGTC3((#8e^+w~V!Oclkl=yz^ZqmOZ#+<96J&lu zdR^*=`kV5iFG!x3{4f*tU*Ng)^>?b4z;cOYdBn^n@U@0n%rL|WO6vVL9~5VtS`N|X zoR-!Xg*6bYCpoS+h6iFUDD)!HlbEfWYI921rr$IAzoLF`*Q-%IKaJ~z|Fy1CF@VnS zUt9+%;Q=^6V(TRr{exf|crIX#RN{ri4zsvndOeLz;}qAYwqJd84edds=SK_oHH{P8 zu*Mm#vTm8{UAM#yZJh4^Kbq$o5WQaS9bMld(JUU#3P_LtM$IZndyv9B!)&eO+sr0V z|3MZn^fAN8Ca{`=pHAG)8IE%>JwsJr+t%r(`llDv5(^dQBvi>wDB{e1r_{2Lbp zo8W)SzIW9_yr?>(_ekC>IUy7F-@|k1=P0Q!it36=xFGTkEKZOf2RonM8}Ltk4Rb#& z|A+n0x}bUb|299n#O9Zj_aRtMQs9ErCJ4C$n^|DB1O*--ZpgYwnWVLbmP^d5H}<^; zsy7Z?P+UK`(37Yhq>|p`Y)*m23DWO}_+LfcV&(ppxZdgk?4uk&mGIx0{cklzwlzLr z?GsxA#nw(Yo4|8{uy5<9vsSWSFIgOrUQexJ`h7-Bca^g5`;4BiJj6m>%Ra*V3Ri_) zGIouwk8513x&?Zcmbwc0Dit*gU9VB|T*Zjl$_-3YeNcXW(AO7<1K9c+L)9DS+XQ($ zP>d5`7aVc@x5};`{8Mw*Q~X1t`k<}Zg{(;#8>G$`q#=r4EdR@Z!^TPT`lEWnj zX2SkSJeR)SY7Ao@z#gzXVu1&|j}ZC~;DGcv;GcRMa(~p`A9&z_zUPnl|7PKzwL|FB zOyAelW3vfLc)jKI%*8qj^MqG?%2ZX`^Jbt7_g?*SXpC)~q7t6;QL_YZv9S zRERSwcAu;Kz;tg%q{okIi;M%X39R1O_arKJXmJ9zA!Zot0ka9f|B*L*yX^YGKQ<@$ zX9lqDt!~x-9jx^a(%u8Oz~&U>*#yK2($Dwriho_#bHX~`#YV}hOxQn?=hDv+Y6_@5 zR^4${ZwwdQmDL=V)f>YDNqrH=H2h=xqyL!`LeK9)@xp%l?d$mfpDjIqSC6SbwwgmY zpo9nH7u@_zh#wYW1>yp~9txZgt(j_^5ORpJ36xJXJ}9j@wzbrY^~N@%Anrrz>bleC zCr+KMdY^rQ{uh=i6$kM4f89Rv|I6cRzP2!`HD*mT>WXZ={rPisZvg|a@=ND*|MMHfJ_e5_CVK1|LZ+Yv&+O;72+=6!&GM+ zFh}{p^8A3+8Tzg2jq_@b%`S+!01ilJmK=Rk`TFL}{G(q}I?dz1yY|2DNmN}ys51;} zCo`wO+XJ@#K{nISY=Z3!|IZ5R=zZ2EUYrU02MYhsruQXvMx`|c#skzFQ(si9HH_l~ z>2bh6>;8a$d;suI&ma2#K!D?9Bhq(J-~n-h`3A9F5PSpiKo%=VuLsVl8naj&w9-{6A5qo1pQ~;^$W@O& z#8r(y)K#`0>Z;lgbCuff(|WM$)vDPA;(-d~E-R>EqK{Ft3wn*5C;YdUeIIZ6d~r^J zae&zbd0fB@vp8M=2bdgt^LNXR5B`}g3I3_u>aPB$o*=F>EaidX+#+ui(0@R0viKnV z{{D^i|1-k+X35@?Ju_i{v#$A>^m7zzj_E&$dk?ZcB-I&N??KE3>2bh6y+6eM4gd7~ z(ED@r(MS84VZ>|G_jUEy>J3BPaTX`YW);MCL5LX^*FXq)L}nTmXDr`Gs9 z%{q^Hpo9xb`j5gIsiignK0+~fkX{e?$M>j`KCfz#t>1Q_urJI{IKovv+|5^5Z+e|sbzx4l6y2rbm|KV#2vKqsf3yM5o{y~uo;DGe|`!~k_ z)A}AZNES+FXTtslUGvlF=dfHNTWXEtx?|suXnjbs0}A~I@(t4C82*{3K@M>L{rC4h zf6UZ5`skzl++etXm@V@_O|15o-!rfN7W~Izy@~Ox0^@-=k7zjrY763;g4iYqd{CTw z?&F4?#tg%{saI#cNa^Rt2jw$SPmEuR-NR=`M-c~|vq*El{MzdkkUbz%&QOZa|%2MgqcNQUP(5`EUP&# z_z3t1;(#I-ggAl43f^#Q`Fuj|BYMhoPy72uvE^>zKdvqEJ<2f`sNT@}4vY`14=Mfr z{>|~fPI#X$nVAXu>+We}>fz=dPZBe23Kz6}bJP(8(Wq2U{J`DfN+@uBw{8RV4 zk1%ljamV>}1L)6Ty>QkKpg(}MftevptuQl%^XvmZvmokIHhX}%=ky`K19@CcyZ-xD)J$%WEq2p2MgVE>cOlkF_K znfagN*2&)9?5c?Ybb^1YBZzAY%-?^nTT(V}$=8Rg;d@V@Zd2KpcQAP$fH{ikN`bFX$y7ptql2Rvy3beTiBBL6}wY zgzrHpu?IA_$ZP_*0J|Va+@JioJK?r+{G&IBG1CKt-srCWx84J`hJULwEX*;>e1t=? zwNejOTp<1a{*Cd^y2NWG^CYY%im<!YFfnLk3WA-%=c2f>WNr=EJs9rCtIH2Yt6 zbBpT#WIT|5 zui(E*a|o&xhp3isUp@XXS1p}iExcEH5eN9~alsbw9B@>K3A7yK^8(}xDrH0D#V-r< zO2h#LE{JB9zzH_bu+WbvKEN+1azWsMH{M>pz7e@U=1?>LyC?YHQk|jkL2-r|{Ra}o z3)AoK72rR&|JQKX(`_f$)noi~USCt7t@R+qHOJPUY+R6@7yNVIGjof3pL{<(KTDS^ z)w;oN_Ui_t>%j39Wxfz|#-4rl zS-%F*5%0W8^Z#WxivxN~-}cj5K-J;`kN>9f_}@iZ3yGRSYK!8!BKFNDp!WbCAWl%^ zg3yDQ#tPEwt!GgH8F5NHU5D7fJ%>l^=arQ7CM#xG;DXO)YahriIO$X6>zknek3IHSf8I{; zzdTRh`8Cv4Z*08>o)3IaVyHD_c2Q{`Qs_ZSKVOFb<Z360!CD;Y&dGqj(f3|Gt zGWU+RhxLM(&jZ%MJhcMM3Ls`k-!OInJ%s6TLr)TOOTz3Dc)rMzgOxSDi!x9?g##>J^qh(m6Iiz{)Y>yw9cid6v`2qi@obnFu@1JwdIbKK7C&ZipdIgyiNZ$an!_cAW z^;x~4^(F=$$YvHq{YdmB6>Nfx3$O)Z@%{l^kW*6tAAtW*XPB*pLhO+GB1sx2NUszA zW9AvY53>fJdFC09{a^m_m+m*e`HegF1J?`x2m1B@dTAZ7zU>Dp{5t&b5GYa4X;{djPc9Cp@!tA1GMnTag2tGpId;;u$ zdQ6#Zli{D5mr)Y%kIq|~cfV6tWVJF=H4Y^lxzrosQN zk}c`~S2X`KJ%4A9bzM*DIef*9mJA2W!|V zL--f=drALSw(cMBubF_@;{WXH_V&N#5mXb4Am>lcZ?f<&+rJ9kA4&D3H+Z|i+yBCU zwekQ};|}n#!%Eo@|BCAe|HP7+0YL6Wy}*a-9v>-TjSVol_p5ooXE-CTK1l+$S>I%se)@PM}VJS-jW_#K^G$$eH!@y#Fq*=kvp3b zH@jXP%AYIlS0VgY&;zVGntv63YMUm?eqZVOD)-+{xxea3N4Y*zWaA6>DgNOC?11VC z$0|pt7y%rhJYe4@;Em1IePD7uhxCbv2_mO{qRz2ChX69HP6dhSIqrG_m$ZHivMH(6XR!nPG*GQ zBV!|j6Xx>cFJn7^p;zmBd9{zr=jU^?rU2`J|MaInb-(`guic;i^e2!1<34y}z<+xY z|I`7p1`t?$wXZKder>bxulgfufBLHKr>}hfK2wej_@7d$|G|H?_W^|e3CHO5@)g<+ zk?pXb?+>bNDxd3^o^M+}CRRi&nchG8e^vi?i0<)Wk{U^y1e`s-tW~*xxY%`ziP@G3gY#Ci63yB>o@UO*H3wX ze(kCiQlCJz`UYy7%jY`0+P??!R_0AB@9*dTs<-4|-Q&dIumQO5S!eIjSL-@BHs_Fl zX|TPEq_3opq*omKXJ3E#2gE1Fsb=ve@yA1wM-@kWO!9rn6PY}zZFj_8&wC%^SR7Nw zzFFsh|Hw&Tmmuu7@my!;Rqp-o_^^DhSv?sv}cfBXk;%J5GPP&R<- z|9n4+v_^pM(^eL&6C`)adU)y)%}e> z*j20UuP=2!8UCxMoDeu*vbaD}oe6mW>Vd%j4)Ou|5;s&Xb9l3QfSM+U&(LVsAFg|h z&Np`4RM*lz*R@ZW?`F+j;D2As^$16P4PV`tel4$f%X9WT>->`IC*Ft7=l;THq^ArH zQ0>@(y2r7>he){RTXesF9GvQyk4v7AJSllfc=wfv=mwo*|^?$?w zmkS&3Rc#OW=Y7QfsSm*Rf8m7}e4Gq@iQm96=p7(8z%iNKPfroo#d-L>*_!Z}=g7Ge zQ#9c{?DKzwIsd$_E3xC+xi~)8i(t3dKF@Ppu8(Wwn&B@#1Ds~IKfeE8{_+?1```cG z9e=9){|Sdk|0@Ql+`o?lcn;X0p4*M;y;Uwi`Sd=G>Iqg)4$@n_0<0NS$^XES-NhNN<2{Bmj%7IG9AFx3 z7i@p+=js0)^nZK=UJqwrH*kI9CB!Q@rseCVX@5&%uj4(umt$}&jtTeS2Mv~RK7@V# z$8)*o)&2b=^fuVRmObpS!+iY?c&DC~+%_0Mz!9$lqxkZ?4}DIq8=u$uf5HEXWy`(& z4}Xy315?=g_z5q)^pbxMTX2K!eE8vqy$?+vA+rR)7RM(~KwUAyV-t>r-5m?(=P~a^ zc#XY2Z)?Y8%Xv)jW8f+i{CoEKKdpGxWdstJcH=YOQzf!x0IIymn$O*(&S(EB_#!3OX- zfr1TijOT!U>J94Gez4m?HNpM#8T|F>bKqO-AYSON+`{M?Z*eo1UF!DP|5kU#1)ufn zuffmQmf)IMzS75jJs-A+^sKiFu-`4l&+DiWr>2zqkKdAGU<1gW*ra@E{&}9)!4=>Ujv{UVCgG7WlDbT= z*S940I^M&3IR?k#m>ipP>?Gm4K-k~nd3A3;M=Xwen_0cg@1%#9cs+S7uuhE5*7gJk zeE(pLm>2k^&V^Vv|D!e={I6cQN;brM3hVN~9X$X1^Ur%*;b%YlnfI094(tIi1P_2G z^eO%{oIqY77XF9Wx}L-nV&T0!=K0Q!5g(6zVv@FHA7QOP?D|xAj`MRK>U+6vV*dF3 za3DHA#{U~m`?#x+uJt)UVXs1Zv!YFQfNF0mTa|xnQBGYLs?`6f$LS;STX99D;s#ZE zkH;*y)&G3Io@|M}=wEvG$R|oF{qZ=aL^$t*Xx;HjFX32~vO}ur^P3dv{rgI z?kk1=%BiLHzi|LtvjKu15ICT3#}N)5=&L+JKlKP!>vM5kJF2F5ka(qb?t9&yYj1Qb zkGRWSq*=b)D{!p&n*YDOI}f((D(}1BE%k8w_9!8SKx}~w1`i;ip*7PI0*pWcOGsi6 zLZBI`)q~WM5E#0Rkuea<*iNW8#zSn!*s(DN4@se_NR_0LO5vpJ#Fd!DDcW)6pTw2d z^~SuCj9|adXPxuAciq$H-j=SwM$)-mtM@*89@h7n*0a{8zuEVgkFM{zXYXTwtmoUf z5_G?|7Cz~CERZXzj$@1uuFfx4-^0#IeD&+H{c@Z+m-Du`Urmo-f9(HTZ##AZ9>ojD z4n7NJpEWKg)b#O`Y;Rx3a4g4kZ0GPCoG;I0#q$=!{}q4Q<>c<`t8@MJ*Ec(t-orXw zie0*y-edFP0|RVcb)LU#n`^GQruk@>zVVXM|3B1n{^k9$#s5v&`SFqI%D?!FzZgu( z0&>7Q@3Z8}}9O-+=kk zZ?Ea?EAl^*1-A}!3wFRb9w;VQKEcN7Gdx$Z0cE~?isMh;JG|(E`-Tgz`px08TmIGX z4}bYf?HQ4$Vm#QU)A{vm0o*62hrKVhk9}ju;auE6UMc?BU1Cv|SDfbZs_T4j?e9n9 z?D6o`Z-cevICHKA_G^AQ{*S43t}k)`pO}oz5?vjAjpOA?zpZ)4=$ZEnoX7cc&ODc? z=dC*ZWej?z*Is*V%dHTPqxbPl*5ciJvkg5-|Ix8nu&=uQ$Rm$5Ibr-ia)kf?L)HGT z&ySb^yU+d@$BNuIyqNkeZiXYWAVo}pd?6bqi{;j5+kV;~C;Q`cTA{mBTpL-aM2=v5 zvRH2)$97)VAKlLm!oTNXe)oN2{^bDD|B(TuFV~ihd`uf_qjGSIpLM6)I2==qu6t$W z+gDDGygc!_4W|~brzgd#2kf323m^kFRcX1xB}2pxM(qM*!Og>_aoKX(?Za_9?;Kuq;m3y8 zy#05FD|UZr`1xP`Pt7i3-{}`EcE^Ti5nDbjhVH z_gBAI{bTTNe;m{MV&v=wQ*r=5I7+ci=E(+ytWo;kwy~|^7@5!1`pA|{$%no~c0{H` zW<=&p`%U&ZhGROHdvHIA{dp!2Km71;>qmaI{Qt857x>o}ST;b(fwiT(*XmbO?dtm6 zA6u*+TimW2Gb$%%WAUb}7oWpr+3MuL935WUlLyMi`aM^+{KnGh8^_zNukF^IdQ(@% z?ANg{udLfS;-3wGdvO4^z=8~L?yk(8y1T^$$&ihe8?f<|vI#0jIAv4KZ#iwxaNL=9 z4JTdn_lDQLEFr}yLQhkxwbCj2KhjejinaRKHA!-N4pJTWt=X5W0e(a&IeeG+* z?LYc!OZjj1KmJP&yyDuD0c`%t!7G;e*z4Kk)fV5EjeZxG8|mq$tI2_459?Lt}bm+gQ2S$7XFe)W$Jueti44_EE^qv4l7dwBTz*T3FuD|S4#>2$nfU)yuo zXZPcu-_OSPeMk1b+!A9e@V~?Y@n3ep4~Dxbb^nO^K9&!wUEZ^_tj^E%JO!p}i-|Ae z7vtLU@h=7-&L`G|c{UrJk9$6F{9pF6`cC(&>tf^K|IIZ9=%IrjYHf$?fBdr}dj9RJ z^fDmqk_pzChH*@X>&OPbds*UGIiBU&KF2Vflp&LHp_d2YJj*hT?69w6I?p8k>ig~` z{Kx+v^UwY--G40Ik9~T!vA@V&_-own+%s&b!e%E6=FZsL3cFw=2dcfU$qJ0q;bciM z-}HUWlL?LYG5cfw*Y8}E1K3wa?E$%tl0Di6$94dFBr;&G`Uq@nb_y9VYA@V+`o7}- zp5eQS|BLXy_dgE5@|*voa2T4;#E!>5rjN#dumAB+|MSh{UXTkt|4#wlmh=t&+rHk4 z$vvNKi(mHt?%liFSRKaf&>q0oCl{jkwcV2e;%MwRKCoCf_7yS%|8f8>t{j37y#LrJz4CH}38tcVO)E&rI0&Nq+!9LqU!PyBzj z|2Mwzjp4T5{*Uf&_P%91Q%WAxbW@e~yX^lRrT_8&nzxtzfBPQ|zy7&DgMW*cXdj~6@h{F7 z{olv^68j7P^0A`-#b1152LBaH{DnH~w-r}UmFp>6>iO(j?BZX)N`0%||Ni&4b{h7& zA_k~UUi@F1U)sFsa&`T&#~y1%dx7zO@qcOA|M$M{?w0$@{@4C*-|$aYVn4<2upOT9 zZ+$qoOi_8bp8H<+AB}(dzs3KX|KIC>x}WV|>{s8KV=HzR z8(=}_)46>4(&ZaV*U#_V-^$$1y-gl$s44!H`JHzx%73#f!UF#zAAtVv`R}iZ9Pk_e?EU$Q0pNbiD|a_pu=&*6haKhrf7gY7Z+P|F ze{Xo}?mrrS?Q>rlzES_!Q~X2x^Xc)QxPR>b#Qx(0u=8z8ZixT0JB=|feptC`Uwo?Y z_U(3^`1d~8sdV#!0|$n?Ys?Nl09`KrFV{^jkTEr6w17a(X0n71^<8Y02 z&t=l~&-zS{rvK}GH~W8)e>y*9#6R7C%$WaC-u3+Y(0p?2%?ATfHzY9MxT=2Hv8Lry>hr_RY_D_e$zW!LV zndpaU{L}xD0euWWoS&`0Z_tNSJB~5t@LzGj8UL%ae<~N_`RZ`^zvrHNnhZF2@L

CgqcDGr}6E4_L*LWf~pGH9n6PBYOWYDg0EZ8 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�}K1x5nRj>^xX) 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%0D8?oZA&u#ze2mG5rAl5rScw@svjyaa?OV9s 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�y&LcK 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 UJ9}T(No@4mK2O88A>=Xb1P~W|y`WOIU;2&WC z83}knbBP9n7jl<7hA+Vj<;5?^5zoy{@Iy8?H3PRN&NgnI7OvKSr>Cccos+{$OA8lk z31?T^q;(~B06+kDRd4EeeO|3MEV|V0!CmRvJtKeeTiPAJZiA0?3l@~T8(3E=FdSm7 zyw!Kz(J08Vnf59IYoKzoc#@?_8@l_9Lq_QrVA1iKmm>yZ{8jGG*I%MCMZVQlxekR2 z?(=rEw%SXfpPw1fvC*-8_gwd%zP;A*>rhW!-Ky|61HL2de)oaD%m05EIO`;sKuBvh zmAZ`w8~jtm?25e$a6al%jVk)oo4@Jxg{;@WJwT)by{|#o+Y%WfNY5*Bn(!r8X zGxl?B^1osJwlND3YoqjHlYRY4 z?%n_A-h*6}%*PIyT%S#2T8IJWFiweC-3VZQ`T6=S&;Op!Kurv}TJ+8aDE`LrS$%Q} z3Q%HRMUD=tIb?9+XlByUH2)2U=+CbLVtKBQF2>nPwAcj$5YRntj99J6?7;y{V( zi~F8a|9iUGC}`wax*Z8n7Jz+`N+?``umHtxs90*3J=7>@J}7jjS7}NPn~?o?cbZSa z-+CnNF~}-xfxYwJXl&jY8vF*VCDP2A!&pYadf1I3xs;jxOjCT=g!6yjdN}NTT!WjH zFYuzvK%+)BImW*PU?~Bskfts~UYeN>vXRkSb@{1zW7Gc)UQb4y#TCcwCxp^$qOW^7 zJM%}>>qr$mF+gQuc(~A%vsr2WWOqUSZyPc3e^bQCy!WwQDRmiFZtR9=)T!>Qyf)`G zBfT+H3t$A91x`!+xQw|bNFwO#Uv4n}7g(1x7t)kvTn~|&m&-TNQxqx1^A_o~Ht;JX z*PXd*f=qOGS74-la>l>-Cd;0tiOL%R=KjvY|Gil!GB6c<&cp?sDL$p^uwwrCve_J$ z@Lu-)5fcCjk*7e%*Oj}E`_I2pQ{Cy4V=^83Gw5Oes+O|v|6cTFSP-RuGOCQs)tLW{ z?#eR;!;l7X4qb^WMj6UAl)zWyj9{GFA3?^D!YNm^NYg2 zSvJNy#``7JCT{54Go}ZxSem|~Qv~O&SF)NQhRUdN!Lt`EKn!xL)zW9HUA69VwsC5{ za5FqbQSstsoK*ksWjtvq>5qU&x?_>bPWZ7*M;FCFKwZvc_v5$sz?)2;IkN!Bn5`VF z_*7Ano}vDnY7Y}G%R3Lw*i%tal05O?e{13)Fm@AsnzC=6LE=09&$4J?TB_KBMvQQ@ ztkG0JB_$wLeEJvvy1H8tjwo&4b{lK;OICWl#pqp*97GLxPD}WZ{yRDi(teQ|Xn&FH zko|b03r~B&#JU4lmC2w`TzmF*i?b=?IlOIzwfA8()+}Rt$Hnj_f|aje48B zhT8L(b^0%Jp<)KJMEMOpEHARWy^A8ZOZoB+6X)ib7~ylhB%;Hf9uN#X;Rkjx{av^x z(;oq{m;>VEy_|+FBxdb0%utSL;;&5f+JMKj6XUCZo5}?i|ktarD*TDhxpM2 z1BS2nR>%UFj1Ae1S>`UyM(o9z0Kj`EH<^^g) z=(6uvps3 zkOr~tgw5`EgHu-Cc2aEHsmx>#>5dGc(9&`-@V;O1zS%^8d^q}1O73J88@zGUd?q?@ zDGr^yXlx20ir>rkHk9W|65Fq=8p2PRG(hD4Nk6N`cQ9q-6}5(lWa=-`bGA(9hAEp1 zOakyZi@HC-5^+N3cHS~jh#`-#;4H;1YQqs|M4mZTsAkfF!R?fr|7R~iLA3Ei`8 zzjk-U#n7>DyUazbWL`P_Zsq0o ziF#&_Qm71nV(k@BH^jw_JNkwdw3w@lmZ1QdxS_uPD37>qNWFcEqe-T+9ZEC!t<)gh zp&=2>P@VV#33=bD`#^3~hUg6H?iBlvw<8*pDFHDgnEYoVb@`5l<5qUMi(vs`-9K9O z(i#N{S}gu=B~TPO*xILh${#B>ILIphv#eHxF^#0c4ZrkDuFbiJ*wejI{y6CU>)SI+ zc1H95;TnoQC{UVN*rPvXZEb1ysNNcN`_T{Og=7j~R+!Ka1w?}IHd4sSAEUWMTy*u{ zG9_%Up-NMvB}KEFS4KaME||RaCSLFB>b(HCz*{$e+`!xjbTk4?47=p6ZdjqX6i zL&1nIs;uH~d&{8Ok)6N6Bka=zJ-SoDCSW8$C&9q?m47SWzR<>f071GXNddEBIx&P2 zmDXfdh*MwH&}YIMh6E%aF!oZ2yhAf9^F{%cs3%ies4~YmTlsmWYEfBj@I6DNZwLOk z(YQ_A@W&&P`6%6w;So5K`)Agl!@pIE6Lx6G!UnD3d!GHrM~1}cghl{iH}ZJ!0gc+q zwHg5Fu_eiHY7lVOJkP;Wlnl|S$&jHijnf&ky{un(9n4ak!BG3P_Nt#rqc$Tpbk^3l zdYz6hfMMlLP`EE>*{Kpjy7z9#MR~7#kuQp z44`!7IqGsy2`FYmQ$fb1=pZm>Z__4=X!y!dXg)-aH@|lBWty;V-vD)#>6i9~5iHMo@ z=>`oa^M*(0kJ3-C(-l1{6aEst1!ghodze!EC9*-?n8CyZay0P$xvl(2jr~I)k5W(H z@v`O<;d?6U5~P5R6MfdSiq;RyrR%2FJf+9squ+?)0L&P-=tI&fsLVmtV3_MWub*-Jxsqv~WglXo)DMFM!-ho%GmF+vFlf_1Z* z;IW$KfH`I{biizmRaNSH+Q?jKU#U`(rtY8XYrJ;73R3&SASidV{rN7eLhZF$doBD>MG%#~iOobUNnevm#r>|wBnW((S`tg}LQC;OUp#=-EiSbWB z?r?t%g{u8sTF^{P#mTA1BPzJaKWk{qVJ)dSnLQ%*7>t<0ieNfkbVY zt`v&y>OuiK2qX@HSv3et#G;VuUIJPM?t?mr_ayqUeLEZC+^ZIjZ;{2n!+ zV!I1L927jwB@f_$0OQB+G-asoH!N=Weo}^qpT9#$(l2wD3iQvCVak1Ctn+}CmvNfF z(Ur?ogRU@4Q$Jcpa*dBZeDiv*Bm==P2x6yFDegY4Pn~Eb_3gp1I?FF|;4`%GO>x;J z0;*``tNV_ln65lmidNF7#Y&b;){&tc7YA2uK5lrdLHNXg?CEBs#dwJzU&>FuQ7Z=? z?+9OA-1Q_~mZR00wOzhf0p(&rld|J_nbgRyvJi6~s6X-n8ll|fWWcdUM#snA!Ff@Q$ zi?z_~aHKxzH9(F8!w-0|5F~E@Q4Pt9I8h^kF|(09B&Yg1XRT%VIn*V^{DwF!F5!3g z%zRtrO)G|ho0&@Vg1AMeR6TWeH-+q1%)>H2Vk!CLW&@>}*7?Lr3pyG1Yj8QEg+YyN z+m7LWU_D&yKdggcB@(4kz9z5u6HUl|5R`#ch{Wk#U%*@C83k~*>8T}F#Oqb;1+kah zi%j_&XIE;XT=qbDz;-C(xRf7I9eV#QRdl&+egIqXy7oF0>Bj%~yH`o-VrGDd_P#E9 zRc5t81~SPDV_9@Mbhv_wAAeKUOp2isfCLK5Ndeb-Q!Z)Sj$iMRNTh?`Vc4q-hVVVG z2GQy6e^p&}T(H|Uq6T}bnV3nB$n*Z!6?3+>pYzK4cgM0vhT(_lr+krqk9-V+@PBRC zg7A6|(`( z8k7Li4(HJ_;x%btqUUO#lo6E3_*`g#$+OL>?!T$1+eWxDq#%I=a3KK};4oPF5|K>E zc_|UJ#@ky=Yr8bdUCwd*Z|e^k%73e;?YP87sh$OvhPTw?>gN~@~Z#|=eI?;4!M zy|GTO@ia_Vopq$&Rg}n~JF1L?C_?zyy<-WrvLK)vGD$1| zfjns{Vqi2^!#Wy)70&UpJt(_|Z@tbwkEB1h?m86#{HOpG^1wwQ4eqc}K8I_qOSklY z9RX4y<{`*k#UOLmnB2yp_O?|knOx52+ajN%GVi9)ILEaN?)-|I%YGr_Wuo}R*BB>J zdf$Zc3s?ZX(}kPj>y}IISQM3MrA(W)j{F*8+I38^0}#$l&U~j@3As-bkQgVRQO}eZ z@Mty@*stR4VL%R-{Ne}F`t+@KiU~WHfm1l396ZU(e|gLn!Mt}>0%e7{2OzX4fA!s+ zr~h$iL=0R{C9IG@+(J(~fA||Vt9jP~o8*8WWst?0W4)?-+bBuOg7?YENyYfuntd&< zpd7d6T035NajELhxt`Q@FX!poHRCRu@0^7^Ts+arC#z*E+A3$e$IHw19XzI)0MJT} zAS3yVq-U)rsciz{fk4Oc>*AeHrtT-ye7@Wb&K$=|x{`_UxT~9&E(e1#gD(yzSu>U| zY+ui*$Du~I&bpnS6ji#zxSw0;9p=ks`vgS%ZxKFhiJ`y1HAIOvk%;UO>aV$p$@g}@ z6B2Np$%_&yezTG^#{E)iuAb<`&;+ZgF~}l{pMmT>d^!rz10zmZ!2|f6?Ngsf9F|^k836Q4zQHSopZiT#ECpv(xbm zLF;^fdU<(&ch&E3zYCX?STr&A6hC&Kw;r#RS=q|0p7Q$ z42#8=BepA~KHi@%GOb@$@ju?3Z&M*`buF0r^`7@e2mDQakUgu0Y!*Pn+B#<^UOHK}&!yvlN>!@MF8*O(Icn?t#er#33fzfkJTDyt7UJ=;aryjf?i=2HVa`x;i zjC?kFTlRgUxyv{3*rD&9#X}S0BX)_MbM#`Yva)Bmxc)LilB{!4!(s2tPEm?-?WZoA zbW*hA#5Lg1Hl|L4YqeeCRohjaZcA_f7>G z&4Lg$p~O4@PzV02$D-&X$&G-6TKxFYAJ2xein6kPG91mYtKo~Bn^Scyqm(g$yW)VygZqYGPXpSeX9SOTaCScU zl#+PX9>C2SE!3}HWM-!1y}f&CMwsk=TUb~)(r^QJo%Bag#=xd1u*sn6fA4qS$>B^i?);D0S>6G9 z$roQcwB}}S;R`X2*iMKVahwNXi#GL!?;!g0aI1^O%ih z)8)|ByCq>YxZzMcHU-=W<9X$)`mj!RGK2Sxi7(XXj_hO%)l^z@zP2kF-vr|K*f+KJ zJP{vSv60rUC{AeK+@3-}n1w^TI4#qp3*jETRlJEy{xc}{KaW@|!k&a4=oY7p<Sl^GN-!t^$I1-#T{b|z+O}(GDt?(yX->&foyCIHaMyJ2}fQ%^)J1hI_emmQ2s^4#F3a~|cR*lg1eW&#phAl$$R^QTeF`9H z;II=^nQa4S*kxogSS9cpgX$Of`12A~>t^vcoFB)+Au##`iRLZ(DwLZWm*%g6kM zSfS4D=A2FM5$o~R`Tb52McRBL|GW9EJiOfALpb##LF+|FOU*;usYt;?RN=zM!LBm# z{ftahiS~RBjHJ{hy-w?*$hl!jZ>r#zk0cU1C(e9HFA(yt{4m~OZEdZbd#rXSM|Rmk zbl(~bM0CJ8E#c=5{-|EXYv!b3G0q=L3!ur|0E9Rzf)Zv90g@A;Al2ux0`Z2MiU=51 zGkI2B-hEo4eQuqua_mnAU@4joShMI#vldt-eTWiI^adXzbEsuoqS z66SBRl`4%)YaV%;>#E>4`0sLBoc%caKhW_XT-%m#VA`);2bIgUq?7K)dMVRpKP4o4 zvBDP7b4#Q7LBm6I+afYGd}cq@g#dGrMQabfFL+|3MU7`>8jF@F^_?o;`q7q9+EH5W zF$W&b)rx%&Ph*^MQo&=p63HV|;yPbXwMw zCy+Q2!PJ)YAYg?Zcvv4E4)W@A_tfJ=$Rr(b>I?r(oDQYI9}QN?5O%Oa(mGwq3PnaUH)hcRVUm8y zjhKk!i2C1p35jiwhg#q3mn3HH7<7LEOVz6j$?10C&Slatzl`sCz66aQRia291wu;;1xqpgk8^$je^(~xrs;PmA~4s!GDXUIoV zK+N^;R{b|$Qv%<3Tl&#gwZdP-kOHbN3H<>e<___CZShoK%?NX5%7@wkJF-}lf`N{E z;7V3+@kn;@^;Pseo9~)D+F1xLYB*k=*HAfrbRH?$uo(J;4CqtKc+f!Gq)FP1U=??H z@Rfy)^^PX)SEpr8XXe~o{2xnDlXU%OSV)&t)t{wzxD`h$o%dLaXy-n?QiIWtL{}M z<&&~=JM?=74b)rM=PxAhZ z@im^#*!n8$&=?+P2kwwmhz!`Xak{p}6{T>UADqvCuxE8O$`sBTexWdK-)W#pV<&q^ z4Gn`rFkjm*fq=@4_%n0`i1FZOB!(1c6AXW%FFhrI=}}a6avX1V6n}Ewi$CPRUyi?b z_Y#v@fL;ljMV;tt;OX_L$c1jw?i3ZC)6ITu=Lt|y+>W~*TlDGepg&Lz@%f4@*CD6+ z=9*DF27tX2yr5q4em6i26C=FYhU_akqPfSMRjScFqd2_t+S+=#hexGRK^DWYT5GERaf|+W zr}J5cB=1s^I^S6|WHR*@VzC_oaKK~yc^j=nKvj(FyMTpX7B3D}6wmRyOXyd3^?^y1 z*OO{|FFr@gJpcghy{W3=K8E>xQe}XUlp?iU_wY9Z4*O?e>+nsuls_d_DxD}07 z@;cg{doGg{%OZ_=TDKI@1_7QT;Z)Tj0#Yv2(@CABP>4u#{o+lG8Y#&(8LYYyWAcxB z{_T!5MTe9o)aKs8hm?u$qR7xszNCUqn>0L)e_GdZn+5=bMh3mk3qP23CVHp65<5p! zmLf7AdnyM~ht7kIafch#=py&xD%>azhgw^8tQuX@qrN<3LA)7#-i_WK**KfRdQp~3 zrQVdHnB@9J1Y~UxHC|+Xi8U1qaOdVd06Ay#m5@L?GJ~C? z0%Vp?SQhxdUVx7e80NiUq&jj*njD{LoZnUcyztLJ35^r`Og-^;@W;eprP@T|W#%&t zt6tFE<1qG6V>vPaSA44OZeG|gv0Ew*aP!_EGaF0e*&zgc&lq0kB}sprp14`E@=?xp zk+8q7XX)&mT~S{@FuLgU*Cf%iUMfk;7JbfhT7`ByTptIXe<82}1lhD#=+XC>a!Dfd zW}XvBDOeZO|3Ll6L9bHQpjUFO3|$HUqQ@E5)Ib135Qp*8{i5^ZBJ8;X{+uw>axBCo zkV4RWRTmMJ#9ZulUoh^d)^p=^kr|e|!3*RNr$;+eGi}kN-Ki?wSDzu{YXcE*cob_G z6t?R5xJfEmKV|)~TF(j$qO<1iHBg?D9JQyk-B+y?Oq44lSZP>ksA{+ZOG10y@9Z0K zqJx^X;y|TKajvA=@TL=V-Msn{jitm`5nGYN&KRnZ=pX*&t5VkQid_kv*4;SVh{G7D zPe23&DV38f*qiamIH_K*!1Sf$Ozmy2v-!0n{5k3Q9xW!d>|Evc<}IWQ0OJ4f5|H%- z0^B8fcfl$F4H}SbPn+#!qQ4#%AvgQ5=s2+|+?w8b9`@iMpk%;Ff1CcKl87i>iS*iU zn``H%r+%mNT}e%y`b8v?APOo4Y+G&4C7RK+&Xg=ElIVOVhusX+B%6?i6O*SuB8n}D z!e9y)W$6fleE1)O0+L1LG`1I25_IltgNG_EiUNvxwghiYxbmyUd3+5;0x8{$WCk}z zP#{TgP%P*uikp;e=kLEn_TyAgpk>$hhfnxr%9y?TPct8Ht#hr)UTE3Tk!9)q0}o@x zTE&930@Jd`Iqw^rAV9!M)%k(;`7UWHH<9=sEXbHhDga>rF>*WyxL6Hu>WF}Nx43xK zQLP(BEaR(g0S1j}6acnylu(P^ci50ME{NDT2?T@^xM8*`MkEy$j!LwCw~cG&-e^^; z(h6A41OX!tB@*k$$H$M?*PYJdw3SC9wr$Hkp6_qEfNn$5RAizgsibKgs}g$O@$j%c zj+dU*yg&CT7qH*V(@D)sPE)M1uNKmKHSbW;6^V#E)MnU@qyQMjS4_-QZ)_$)Ta6mb^IRJ%EmJw}cN3snEy|jdZ zY@si|!>xKlflWAIdJFMa^Y`Pg2dZbl{ZFwTcIcu<-xGuer2z!go3C97*do_rAaHMH zkSd2DuCgcAS5)YpMB+~-d^?~iB_)FUSCWR`vz(MRVuEkH@3aQ3htL8I(5wkqUkGpt zodtDsYZ1PVjl14g)@N&bZgNTAjM}upNBV+u2rH?~Qis>ew^s|(0%=Tyb$(7Z&gvkm z*+wSd4H+&S2HQ4Rb_MsE9m|H7Mn8e-)(y2Y?dEvjQEN5Pq!2Z`)%J{T8vlCpf?J#k zLd`tE4TOLJr8j0-o89(#_B@#!F=VG&WFh_n`^IdiO|APNEj=2!kF) zB~=W5Mny(1TlTqrmwx6VOla^Qv-b}w9!29OBPEGoJ$I%<4G0B zCW*Mo00HewWDi-lXp2oDfXTNov`nhsvLyXEHEwNoysnY*)- z9bOFX4Koue)f(Ow6h4<7d3~xl2Js1N=y>S=2&d!y-V$GqUqQ+2dsHUX7nnZTBuJ2k zJ}s#qa6q3j@Sv0fwg)Z=G#C+KK%#LiE%ms}?hyj0K0Zx>Sp99>-2|Zn zgxIN-p`y@>Qd94B`6LI^>nqmVuML$6h~SlkrrdmSt0(kp_P@ zlG>mF1JbeCcoIxEZ|_TmegI?GeJuMa);%S0ArjEP3dC$5sVrr^b04{@OHQJSbuP6& zot7j_-vD<4ZqO~cUD{kLHgMawwj346`Bm928pxKa*pJFOdMQSBxiIwOD++1;0ISF@*1_v zOr%SA#MPcsdRb@_a^KvbT={1iC@LE7fju@C5#jfGhj-ZZ;fN|QEix?Va~?T>0Nec{ zG@u;oa`y9kSfIzPE!clqU}$CtQB80Hcp!+r*XiXtHguJmSOdMQKpG@orGtA@7F!;t zNr85)hqf!((E@cgWrl>os)h=d8z3!L{%(0ys(9B9vL$H4Pt2)D*=19F$5}DK1RG#78LI#SROlRGAe5Z)-V8CB z@YbCc2typVAQZn_DW7m-7Cm_ur_MLqg;E`L^9JZSxWqpXd-g*)1;R{K54YNQoQpZ- zT_?wTdl%PmRT-c}rD-_b?j#Hh47`zw57#g;-Cu|B1U;9K51M2TN~BjmdSaFI+)iY% z3`XiEA6Hk`r|`Mmhuhok=^&!*Zi9H^R?8`kB2_vw> zv^Q{5lI#iE64oy_)%KXSL}Xro2B<#G{sDV!tzy)8azQJBVCvv$n_V%2d;T}ieR}P9 zcP)5u%MH(@1mN4ssY7_?7!Js}I1|F1{mxc!=fg%*s=n&q09*N%*5POO}E%A^I4lcszgQ8|Q$Xq#K+5CBs4> zz{f(Az9&;}IkUs0=m?QCfSBefj`1w(^II+E6WyS8o)9Hs?3+%$^|INI5k6eFx*z`R zso976B~b(t)oEq=T`s=VRsOs~jC+ztt%^oJG^J3-2lAj~*d$g(QH&u8fLnbZx8*VxoVQvQ&kE~Sq)JGlv{5c>l@c21x zkhvL8DJfJbeJqpAS2y1nKr#*Ugg-YCB&Z#M#!+4M(K#p$S6ZR+p`L(wr5fD{c$;9V zIakBO3MhbSx4Me0tNR=S36gDEgp5Rzm$=7LQw|C)Q$6zlni8qfCy_jdb|kX!y1caK zHvz6k*n9vJUaU_IA1>CN((%$wQmX9YFm&3@#o17k4Z}~tuAjidXK@+;qT6i(bUU+P zO)SsTvv*C%U^hOlBd?03in9nD4R*h*8ZYGJT$T0k@Z`8yC?GZ!;MV{8)OWRlA}+(I zAOZ61GVN@T*l*I*fsHMvI*heqaLJ3FXn`teDp<80m=K{05WlmT)Zx<^^ieQBUoEUj z7}Ex?zIGBIYd$#Qhv>_XiwpuZX=%m=z~1$JD`@GTQ41{Uj_E8E0PuwUD%twS?JZ6| zk4vIQo{y4z+@6G(d^!EmAX)4l2#~Vn4%pviMpQR6qtp$u@VlOg@XPR?iyS6A1jkcb1xO?Vn@Qy65VPaT7ZtOrHz;FM#&ZP&GS?U@T6 zs!v=ad!B#GtaX7GthMmEzJswDlMyI>JfQ~gKr=#Y-@ z?T-4OmMADNMLBy`HB{A(508!(I`7G>fR)$M@?}P4@6z89?fE0C4qJ*47lZmXQfOTd z9}VoGCv{Lfj2tLN0{z{)E;PVcIS?A(7|P?g8Q988|R zL=5sWh-`i1f&V#Z82h=py862M`1tzxDJd(?-^>i zBTy5!x=G~EJW#(Il0F}P=M$W!@mGu?6 zxQXQfSy@^8q$G~LsHmu8n=0cCP~fz4`rRAtM;?2E{7^_44nHW2d62+jGfg{lBd{l1 z^X-wrqR?y8!_;V&Tbm_xPgDR|@kWb}!+4w%k$AKc+4awNb~5d5nr~m3au!?N@u8bk zAO)npo_+c8z-l!7Z2&HRt*Pl|r)1c0_jUtt9Ck(KV(~(zyi=+`IfNe_%|)NBm}#KW zFOwuDCeDCid9h0z+~427)!W;92%5~d+x4l6-5C^1`9JSZHw$aM8hRZ;wjr)tt;bBwO|`s&mwBy{!4Muh14^yH_ZTry==|6Po~l3C&njkL4AWUyK7!+T>EK$ zNH5yP8Sog$bT6RF3g8Uu-19fsBxtTY#Ich=Loy)EM%BFl4W)X~kOM?=upI!TG(iUb z4TF8ix|C(`mMPyC02DtO80~Ak?_wb)^~qoR^XEb285G(3Q`u%Ei&!q7|JP*_K92nx zFx!QNnTrS{4HaooU27(SY@1sYm$W+0t}~BP)T(}kjlQ;H0Re#3NXHgo3qm4 zO7TZ~tl7Ra6|Zg}5$hXVoYyO5d@G_RY*66tM@jG6Um2O8?*|;?QAX{e-oWSgq&@?- z?w%jGcHg;tp$$S}SUeh8>A2z<2h{oDF(@P88arZsDQFrAX;#x>jv^uRy2zE1=T$}( zAbDe>3AMrsfhTM>k7@eD4blMCqFKD(OFhaRwf72=6D?y zeE-#CV7j=8_p8a9jv~w_ekB*#Qxdb!4FxtOB_)NxvQ>3ay5hiaff=*7xcK~ZeQ|Lg zlnCJRyQ)5+k;YHZLSWcuABf78CRTN59q_|t+glaWh!M*u0`wDxt0Mf?C%KWU((-(O~ zT-OEq8}suvYb~@YjO^5*ElWS==Dd0~y8m={x3OF#TUptQ-I10Iu>Rw!oSnlZIRGn$ z^P2tMT8ip}%Tl&PGG431?HScSpluhDWJ;}fIknO*aoYEy;p2`y76}9!vv?m$N9bPRO}{*mA<}i% z2~4e~CRS2##|Mm0ZO{)oelAbyy5Y9nFRRLb(qkZj@f3hT(49t0nNkMrd5zTM@bBPO zk-9+GS*^&vbQb)gaIU-o^U_WxDdzwu{Lsbw#T9vMHLKavr*E{iAGCt2Fb>pITT4qz zhai2f3T{Hmtbu10>?NoB)mc?>u^(>18LM9*5O87=r&zNe8cXDlQ^ce&a~~xQvL|L-S7mVl$Q>CF=!}A;To4pK2Mu&L}M^_){TSYo<4@(R*RDCyG+mp#NHa4yR zg~@2EdetayIN!Zy%|5%oz+ExfzIHrUCP{004z0{LOD=K6!egoJlYGfndEi|lDb%l$ zn{CJvI#9pmtQR4{i3AUmgISYl(k0jlpjrhRL070JtiVrVFZf5 z(D$b+rhj`FSrWQFZ_{P_$}iy&@PXQ*77kJXsY>q#$;Kv&aXMm(b$qQ*MH1MCGUXyy zUtRs-DHxq@Fl~BI$kP*9xp{b^@t{9tF~RMid79*;AB;QgGGnf3};%=WU=Y z54E6br=;)SPxO>RwyC!jhV^_8m%O(bxoz!jeMi;%I=&tnE?30h9c#i>Vws{R%EJ>^ znLT1KQmB^~FO#0VhXYGknf?mO>F=j5J!{JhW89*L!}zAYNTSlm#+DP9=^-s8Oh6=M zsNO<5gy9&b!9urvpH@)fHN1B<_TdNS#wZdXOy`Q8sKg!Q&NLE}#5_5P$r!g+e-e0S zA)R~ns)~7nm9o_7J@aI+r9Vc^UZ?8k_Q)N2M@L7UlI-(Yb-QP&r32*LKR&ubip2Nb zgGhhNR4Vf?f>fNQsg@jMNB@{pp!$qdzCzZ&k&lD3G$AVHVoxI>r71t7t~F;c_|7%B3^ z85b4HbjOi~ez37N31=O+6g8sbRa!?H*h510LmCNx9--=adA8F+`C}oHZ<+s`@5ugx zq>te@CtMkY7l*YlF@(LsD5tC!TzaUX%Bm`RkpB1CSBR|Xm6ev17=my$xEy7ZIq*Dz zBg!YU-)?C5s^77SP4u(Cg;%#Hw^##N$ov~??wjUMEVuSrbzY@R~N+kz{g8ycfFS=P(l8-7XNgEfE1 zsUlbT1%nWAoo<5cy2rjvaEEu(iMvqB~G});o%H^Y7MlPhT;P^GPTWV#D6bmF}py#y+%NdBk5S z3K3r|dCRGnD)x~4*06|nwpgrq{02x?N=hD$3=e;EbW<33P-U{@rVtNy7ur=B$Lp2p zqC7_joU)&wD)?gYxjtWBM{y-@d!=fVr_O$&P?G#xH|?5`swxXOxA6cQ zGq`9p%BEacPr;5@FazA=IPZs^HPxxG{(gPa#lpkeU79OOeFul@JzZY*QQ)LmnAws; zee-+t3M=LGX(zzF_*abqEevc3eq3NA?k=kkZ(pI8JG7kXRQ1G8=F^-{*&`>JAhWyy z5n1-Ad-bN0Ln{@oW#A>L%eCHA6x@CWFOowJdsn?Xs^i+^RH|CZl^&GP*R{GhN0@zX z)KsTjq^Nsu2QeM?k$}f=N=oME3SVFdBA}U=yx9Cv-<@3fE2)gCvWcPsPNg7B$B-@$ z7!iAQzcr;|z4`U1Gz~t@(?|?RefYdc?_ewvUuWGw1RA4!`&V%3;r9VHnfI6X)5qTfl-MmmP!XDG_mgA&-P*sj>fyISOWLyJ<)@f>KWir` zDQU?{DJ8mX#v6;imV5%7-9#^U`!CWSmmN8-fK(na_w2}#Ol?7THIQ~jqV-Loc#{&3q<+U>*VNZRv1{HR^iIliS(Y+`wMYCkG_3dL zuI_sZA771J#|uXr7QzPQ0fYjbh)`I?UD3#wpYG-8Y(thk4M zmW}-7iBi8t7ySinkz7;>tY&qn8f993tx7NQvLy%cSSaya#>BoD01zXbrlnG=P=b zD7oO#)g&{K_>O_h><)3tV=GHtng44G5* zQ`P{l{h^}p%?oU_(&oyi!XlfmT)Q3y&U_Tgq7~la%IeHJuE2Ug2lRf;XmZNhwTWUx~|Q64D&{ z^ut&0AfgVcg=Z;K!V@j$a$Z~L(?M=vz!5RF-FS1i->c1vDwI*jS5+3J_ zp6!``rbzy2W$J08?>h{tk-QSNDRxM%d+k$(_ZaQFmlz#hdm^J77VQ?9OGb}4eAAid z=7c`*2(%tvR(~sEO7=o7rbA7Jp60P1+1CShI45%~4}fYvg1Bx5%D4WmgeGh=l<&Qv z^%e^~y!)A6Pj{9yM2K>or8*Viu3GJ-wd$A?K>v_l<%J7?xtG~tFYBaG1vNN{f1w}? zZKUFklPDvxp5_7qnRcwq9&dJNQ$GAKv8IFJ%xH3wbSN zKpby0?c9}EBj?s1NQ2dP`(5&VTQ(EX^V;n~tNFR!H<*>(P~0M#2=)6xOZN78E|_Ug zi5XM#rJTCib5GiDWfS+%xOip}yHN8T21T=nC%xH6jF$tXmviGIWYf!rF^xu%B;bD> zK=jA%UdqA|@T?@7YOl?a|4PH-erT9XF3sMMx7k0W6>N$P?uVv(bP{t9zMvg?_=P9o z{fun`GymqMqOTsv*lK?pPkvE`r6D#WRKzj4XuZHT7JAvsTf8QL3t7Yu)#%o2PP|`m z9g(b@kb4$w#VtR^T@CniME+98+cy1^Bd3mAb=kM~o%tR!-1CLko&t?DS&r&xsSh0! zkTeEX;qqbq@~F`)v3+WFrs|@Ll0@q0FOD33kqsVy;&zReW%lY^tPT}3?X9Lxf2-L4 z^exdtI*jeMt&PAZzi?=MBh4D)-8roHo4CHMQEg}v3^ zC(JLF1A_iuk+Au`)A0S6GH*Fnlrit>M$*DZh5yIfdq>0dM{lENhS4T^@7+W~bkR$Q z5;c+_h@R+O5M_)KC3+-5)QBK@ixO>gB8VV*8ND05bC2)uecyZUKljhOmW737&78A8 zdq4Zx&wdV(L`KpHAyTxJC+dQ2uHEyjG4z-+n}um$I6w|g=QwKeztRA9al_eY$hN&OPX`FEtpy`R76BVE! zar3}4|B?hqw}TAoz03hDQqK;|hQtfJ*VP~ojgNeCVyV>1J5Y^a>(=rm%W%aGl6~4> zQNN^v^{@VXKq<3L_`*;xb_n`6%ui{G%Nao!n3o3|V-kZ>$Ya2yL!%PYez?rQi>1ts z!(dE?bK1k><#bMtFSfPdSv#~-jku^i%7Uxk#2=)L{X_Uq)P1P4NVY&^vKY6&AI1JY z0OH-v4%QhmyN9Vuk$ZLI00M#FWM!)yqa7#eK)&PW&fb};0vdYBoGgc4fKC;Tmi*w^ zkbSNdR1+QR*F8^7?YX%HZ$O!WKX@;YsL8-2jQJ5^Us}H}tbP+0WK4rk@AE5oHJh&| zCuV~EyEiRJ{eT5pu(+=`2hQ;qV+M{OO$ii*c* z$NuuzZG-!{WlVfZa~90U+ypY&iNB`rLMjMlau!Uj!6URKPV3{P67z@O2o|(bIr5tg zJJrke_3f(ZgMCfr?``i~-GnwSg?nr_MCE(VegLy=b+tWUwlS1XOUrnt0w~8z;`FXb zXj`N?j)2UvbJdI7M=X)_zyZXZvjC*1b_x9D--BC#cOVe8&?63c&hB@6-Typ^Aofa3 z_*Ug{9}yJ~+mV**{{g;R+w)bxLabkH_1+NlKzjR490OsWJB^Qc9?S3TCJ7jx+d&?_NwGGem^FtZDNJGcnbw&!@l3Ic$z>q?+$Nb}PB(|3_FYqI zaHv{a^w+sjSMzNicIW$%++sxL!YRK?!O55$ZlGtd-=kmfYePmYtmmr6a;&F z@xBRMv^kFN(HU2zf3E(oV4QTAvM4kAWspc<0gTc;&A&1&ke6W|LIbV}CGSkHc=#lX zKIH3*s_y@vQjtRI7irj(t?(mb{#5Fn1gcD(MXxS-Wm0Qo&_f_`aXs#Yu!H`TFe`10 z^u4Tz^8o$dz9sMXSY9N@98{j{oJ?&;jEt3rJZ~E@f8J4H#-l!Jw21%ot)$_b6s|dw zhTM5j6iJZEw@VrR16}Xkg~Q`O0^E=IPFh7{!e~*Z2SG)nwHf>@pH)+in^n-SBqb?* zLBk9sLDr*UYl!^Z_6^k7seE99ZtN!rfZFaT6feYj_sa&&e34$UXZ8-`)K4kl|55y? ziw)I|y z7Bsi(tUA4sC#RygA1O5NbDzRfR)}8b)e7y3Ro0=&0aPJjOlK&V<4cxN#-r!@Zb?vp zuZ%Lm>w&)_1Mr4?uH*DHg1Y1{V&-{A(<*N@cX{SgPgCqxMTZ>k+4whbU0w+DoYvc*dkxqy31HQC7DZ2BF818jJlDQbox^}ZB`dFhmmMty z%{U6UtTcWdXDD2tzYRd)M~9Pn%oI1_pngOig?tSEP-w|Gl3Mi*q>RTGAWh2+NF3vf zKXw1rC>;G22Z$zYAvM-kP5_YHNj*=6lFd4-$%cPCDyMM+t8&9~K;FPg%&1IS)aNv* zOxjhiC$DeC5uOPCIbn@F&hUIxVzzNIa3urS9+Eui46=CwM%^_hwB!U=c^A3{Y zT+vePe$uRZ>jHBA?bZ=Y4%dPV2n6E6f>5e{ftoFhJAt&2Yw<7law*Ou#x8@gDmfn1 zv%9+TY$edRm7=pqOfMI+KHSD;g_EZj-^WFZ= zL<$GI;4SaEk|>=Rh0^T4gdG0>K$8bq9}G9&POS!vWi;-a^hyfu3{+}cvp5=NYSi_@ zY;Ng~+kn%LAy1%T6q%&%POVAp1I8lS%t%@G=|wvtR;ah5Bd1yO zy#>2u8SKIcm{&S!NDoq*lb<|0#`7U+#cr}icN72 z=|L~|uW3xqAeAP^j^^~} z4XlRQF?)YUSDvH@Xom?gAG3Yu#nK%7Kvl*1?c&~`9fvRc*w!=JSHlF;&iZp-Mh5nN zpjx9D*e_&kf@rbHnS55mYOQY{hDvH)@|w!nh1&=@W99BtLYetnf|I!fGV?xefyR(O&7s`#4*JS&No^%MeR*0Idp z|0VK|NjMd0HK_XnKQQKUYL84HZe#P$Mtx>2lw@6piifqy69x&_ShMz$K zG{^paABx+2zO3Y*$b}3x5Nr@tMv|Qp{=z+^JyV=lQj0MZsd7443P0D4r#gvGDzl}2 ziv&LG?<3jvC`%NT?=2nNYGx2f^dv>;-vS}kZ(do{oOR?!d%RATv4Ef+nc(*oZ96a5?k~cWXPCAes_WpsaEa$x5026 zwVS`ZcxJ4h9F(;9W6lDJV+euilpDTbG@3Jx&{sc)+1s!H{q>E9yrK?C%ltD14eedZ ztJbXkPi%%CGSQUj$fj5h65s=F#z`1?gnY}VEUKrP`;-N*8dwDlqEV{$bk&ml`4JzO zUdR|0D=Z?YY!(qH_u*$*^9VHK{}eY{7?hE4MVHCM#59g4aZSb~rYh|hHjv)t>q4{Z zZW###pf<r>Hz zmR`3x^rW`qRlBCGqR=ntUfx-+g&XGP75;Twky^2hs_>kO^olb47+bIEq2^JZp8tC4 z32sJ&2(6DYIl4OHE>u4CgY*{!Ua|-p+xot4VW~WgQLL4&Xg8m{WANbx_ol^&=09Ee ztUJHU#e?4+*ohucUfAZ~Q8RyvtE}`Gq6I2Q1IA?@BR|s_el|3$^9D|ucKp^23_Z;& zO%pis%unGw1M;%6%qqc#XC#0nOZ_h}ZmQ{IP$u%6bR!+ZpDGk52%46q0YS!2@tpyJ z|DqIv`PR1rBMV%HE1dm6wTI3{))RDzcqiA=2`ti+0QzeV5IHx^v0pTo4v@W?K))>c zoz#3z48B}1dEsh7Ki`E21 zbNw2doY0x0B$ytoA(#IpC*}FO>P;aH>T_Q3%U4IP>TQRc?h>bRz#o>@R|?B{Jr$DU z6cqjs#BmE!+x{<$2xKxPcpSO6tI+Vcq+h*U--`_%%~7wsd~eb$Fi}ZcEf zUGA0A*fRf-kntmBV}e_{;-OpMZdL+6@{ZxL2N+>^p9bA@YRe)75n4BBx{R3Ue-fWt z2QJG@d5r+}uS$&CWvDb?RYb3xJ(r;nvlzz#<-E^0nUCF96<7HPL(RFzGbz|!GN12X zqc;8dfqMHlUk1Sb3U3#e_&68#1f5OI)M5c5mX#>r4M8GdS1|4orNf|6Xi(!W1*hPoW)!6H;+ z)LD>Y zfs|V;>73Bloq(n^ST(h?0kKO7ay3;gXGjjq$E+0pli=7K--kcFw|ojhql5Sbrmfz} zImFXGA9?cSCNgFXUBZS=DOzD68K>R3#8b(|Ow7_~_n3~+W{g^;| zGj*&WB0$uKt?WhBnCd_X99KeuiVkEN{2a+0kEw*HUr)jKhK3$WZ@;DRVPN)-Tb5v% zjie4j%zwBDofHWud8JO}qg4ptkFfd}O)F3e{SgOby8ZU=4A zsK~%cfN`Pncedl{Ig~3gXfHDTdzV579wD`i9A*mE(T`YmXBsl8CJW1()Y|XJq$%qc zNQ^d$Uq5Vz~q$02qlzqdwfbaw6)>p_v9{yBQCg#s^We`IFPlhfUUKE>Hj-| zquP6V=p`>^NdpsJ;biH&k1Mj$Emv>TV>0K)0h%(;c+qNw-*)BR<8_F3Lb%MJ5pKR- zpZp0?)J4MOrRIzwKh(=#bi#2TR)2?@4tEoenJ13q>;qLXwWDMPjBR=NAcT0pBv6tGLmll{vOc(Izv|^$dv9gx zDxm(kcZUn(_P?{^5qA4T`G`>Ah&ISXrxHgogFtU);7tVf5gu@hjJcg}oyddWH}5^e z`k_jZGjJo7;4z?WK0$TPVNBw!wyB{CS&aw)t&^CIFP>VvJp&KwMLY-AeXSN@VyK)P z;yGnCqdfP`a|$wlE`<0k<{1G3ugGgDj z-X)cM)k}5Wg#HHuK{(O7R)U7?fMxifbLS~H)xtZpJ{vOpS+~Ec1n1;tN(8Bn3WcDJ z*X$se-M^VM3bO(}@@f*FS3vHlP>>&JPQqTKoI@N`3Bswg0Hj1ga34-wIyLNxILR zFnxO?pgd+I0s6b#crg$1@;}m{y?{WbYJmhCj4AvL`=$lQw*K=#X0v1S+rmKjn7EA4 zBo+gkAbbefiTWUJMk);%4X6j0jPN6N`yo^h@*pSwD;G7CO=o9ihsfczVHVGR%3D+j zu>EnADq+~>wAyWOP_5=N%FIXi8KBO?TU8~z93K!8SL za4`s7JRNuTZUD@GHJ-#B*X6iV>9r}D=plfSjgUTKVs$4=C6nnJl&PeBr)s~-O&EK~ zwu$=69sV)b;i1!Jq*17Naz5wiTy5AeW0R!#n#mAIJZ7l=-+cIQAqzEih5NgdAEIDv z`gebY+L0TCj_IKOHzj`1?bB-HCWl+}4Xu;!KJL_h)q6Lez~b@` zv<^nMNpIUmmEn;$bTLr+`*ywfCz&?M zOrnkhWd%^3Imbvb(EMj7=m5!nlvs9^pUKt5C6<4CkCG>48DUGU%;&MY`+thtcjSAP zr&O@SWX`rzLMz@mfX!HUP?4|;F^mAd#vVvk#u5f5m=2F5yls=h100!LAJbR(?}eHP zy>TU1&K&NLvn*tiQE+?8h zs&+xqg7&`J3RCSiuimW*##p~X_aMaIr{GOX_b__(e6@D}Bl$tyn(RKa_fO(AX79A9 zlczz5IU0F;e&lILf9B_(e3gyYyeC9vgmNZ*&uuyU~lQ@Bvo5_-aR0?J`7iyA(L>2?K!S^Z@k{fYe!KsKH& zJ~KZ{I~Nu%DM3a3F!fk#^X)v?(EG^oMmXOyAZS5!g+6p8?UE!20esBC@Xg@40P zrKEh;WbPtD&LcWyS^$K&U&IWaWlyJRlpgnT9X8XTW(8{f2w&g^R_9yZx@-sGdC6D& zxh5KO+VGv&ue`uw5TzD`4I`rQ@6)vQVLRU?ZVAW(9|z1P7G|o2By6A;dp50O$-W68 zuj;8K^1>h{>W!&?EF`b(?^U}(kQMW<^wCS|4>*m6i z#Y*=<@`+@Qf1t`FaWg~EZL@qTo>On`%`HJoZwn;edjA<|dHM3aa(!I^)%DBw%TNJ^ z=UgNNFRzRMZvpiiNUXYzI8%I>brD*h1Q1-b!+Sf8sN8=;29GFnD}$r1tdzx$3JM~E@yB_P*N%@xP>z}8!}KuJINAT`LvCoH@a#6tx$oXnN|nE%0|?1? zW;y>=SP*dkWcL0QvG4?D8l)NnvU~rHl?$%mv_6<;=C`r8O)1EX)<>bWZ4ainfPKnL zevUTNlId%t;tl8_xaTUBI z(53?k9%%SL5-fipSFh|SnQFf96iiYKjo7#!gH_LvL0Q>OASB1FUUj2PgCEqEi_<%5 znX%ojcpXRp*2`NuTOFpiX;ai{BaRJY7wqo-O!hb1?AT?QyJ*_` zBFr?z6ua=IDOf0uH72X)J!%U?K7g!{9g^b!2dEgdL$|i%8a~A++E*lf`{^o1#Ffn3my9z?K-xx%Y)!RFEZj`K!hF1 zwr;c^n+=~ARYE3q%L|Z@lw}N4*K1Cve8UwEgIU|r2$$#@=8*$GvFx?)vblR7YCJbu z7Toj$=Q2@InEoTL)Bt=1+zMgEc)_Z{G#a)FEw2w_G_Q*Aii1+il)h(ez?WbbC|r_+%AgTBTEG0mNKw)f9#WbZ|~lr?1ve&oNXy5Z}{ zPa+4HtX>_1SQdjAz6_rEzbR8sS~q`xDeDUVIaL6t)LynIb8}7d4F&~dWOl-dTV++h z@g_UN&2bglK_&GHE$yx!V2Q~W8V)SZgDIa^$nvAgw1^4VoZ6vKsp`#LHDZ>WvFxtC z8%>Cz1qfcKA66OHEM(lnmVNoDehDd)yAPr?5P~TmyQlT?UKp zT>mSk3Z?i_$P}&@kvTm+JAbs`YS``RLEt1L2jr-al0FG2v1|YOiT7VdeP-wX;fUVU z>+f@E;XW-$ZEYQVV2E448E|j(pb$L8&4JHRk zlG(N` z>92bo)b)BwNuTY?a+iIzR-|u90+qj@O)unc81<3Nr-7B~<*=HG9tOr@07?M#a5GRI zLr_idjQ=xv_C1yJ?K4)fivDD6oBf5vMDoM{;+CWdAQ)$!(@tUUISWp;Z0bE+eJKFN z_VpxRgI%|D{b_i5>N|6sXV36K+_a0Bsl7S0!TW zQj*Y982lv5^1UTw>?u3gLC5TYgIA)6r$IJ0MyRp{l84kj>&(YF1+^zpMy^TVToH3G z;8P_ZR+kkTf8O)%Mst#xg#PMJu(q#unRD`0g+g9Yz+B<^^1RY{5ny7L;&?0-!w~s- zGZ%&{At|}0UoC3fY_GKY8x}~4UV14* zgpT{v#5(n%hueX(e9195CI)oK8dbzI-W+Z|#De@3^5;_;D;qv*vej?LyXL&|ipB#c@i&x6JFmsFhe!pViaBJ^Iia3v zXw+zX|EG13Y}ic{YZ=__K^ew~eqM#4IJUYh`K9{2K@$T7R|;9n9`{9x-mYw=oIc4m zKU!MaHa~p)RE6#2fC?Q{QCjz>%E&wMOFeS>8NvMFd|v^g~8A9;m4blnbxRRCbmHlF#L;jQCf*>i?{HW|@{gS-Q6n z04h{(rk`zEaC3X=8pis}E+!9!?Cl?{&yox6b%RrBP#!=5Qg7-pr)RNb7s4ldIwEtb z@i8Yx+rZNZ?I;)0Voq?0WW@_n^k?ec0{s(x<`nBp!t}o5esre?wP{c9Q0I@_(Jg-Z z<>?*rXCQa>F1k(W01j*t0GVY8#g$9urv>=#Yxw@@IN%t`;9b9XHf7|up)|xh;f_NI zd3n~HcLDlYykmE?;1Tz=9cktGQDeCTndH&yvZ>k@5_D*D(O`@@(1sy%c5wmUd@wC@ zcz!Wjf@Xi1n_>`D_Ln;~nua^);iNm$ivN6J>n#PERMai-$=I~J^`DCbTQlAqldz{Z z<-qlY^?iQS%Ny+i>6nU;P1Js35mew6Opa>nx%%90KMnQ+DSJQ$*5&j?Dve;~x!@|P zh2UzNxnO9jBCE4YmnVI@5N0Bdsq}W991Bvot7^0`6+9l)!`oTdRsjIs$!?j|`h&>nz^JX=ZZ;gsoMnrZJpeLBu zX8&m`_dtZ(w*n?YKYvhgWW_(wQ;d$Kj0#gT;+PVYCDN^db&Yc^>z`mRtv z6y{t*%*e>7TljQs`e6oNtr;K4^?zyki3{af*xlURjE+1*!*I<2Y$U7TO=&Q`HFhL? z{>^d;2f&Na4t@7iT{@-6s_c{&5DZyt!lb9+?mrHlzbpKdkRZY_eXPqVfc0K!DQ0mI z_nv@2OXfggYD4f?oMg6%yziM#QQo-93aCtP8y}|^)5{unfMfZ@E|PM(rfEk73vJ)47+)Gzy-2cGFzjwY|6*tGur0aA@4llaLg2<&+!gq|-g@W;DYW zV{4r`ekV}Jc+&x~Diomi6NAT8DxBj+9`)&b61C?tirD5B{U03>ZHaa#xBmjjK^sK@UVtONj-iX9u;sVKyl7u85KiCcPm_wi3D?UB0)3L_lk1DLLqBt>p|W$ zN!Vxd*WG?bC^!`4JZbIP{0)4KP??_ZzQ3pcVGXus1qAJx{6R_o+{|Lp@Qr4QA*ef@ zSnwI)C|j`Eh=R@YI52n-c!bY; z%BR20R%`_*gsRc@?5S;i@X8JdZL5~X=K!As_DoF_Fp7BM<(B0EA??goOyg;4E6jxU<*3 zBT+0|wya!R>v#lkKAO)hoBao#-mjN_2yfH7ZHN2L_5HT>%*&K;lP{9$!Nx6%a>j_} z4XU5X$$Y?a)xfX`n_eUtQm#7b&B;hV5>sT#jx)2&wTVVsn=6)&W6@?UZd0(w&^&4W zEPI`I=DL79tE0u?h1k4k&ICg-Ggg!>xAdQbrj3YZ-e`+mhIZbVqd1q@<5QJ?1pE@i zUVG~u34Sb=!GJR?j$ck3pe0Jc?O09rR6E67L7)=#)bX6xdmkzFDxDhR5$p_ z-r`ZsN2spG#r;WT%4UhKHsuve?-NYz!ww}01nszrwqh{src5mJ*bX=o3lrF9p>w_6 zjzp@`l)XL%jD-jI&{;I|E2w>x2$YfCncJ#?amD>(Yj(ls9UX3Qa7M&A8l(PM+tnp{ zfm8B<_GoMNmzASAcN`F;=Ef)?y!{U7(8l-IN7KDAr;hB~16AsuJGJ!Oy9$$3Pp&3w zPR1wipNt>>!(>LQ+SAt%R-QuTnOPU|7M@&q`oAO1lv~If8VY;4uRjOU;A!cZ3*e^U zt}1v<*MF)Ae^+}q{w;kI3<)e|A@6R^TS>g*!RV|nquibpcL_H_az^=xQF`sM_io%hpuT7=exm0 zRIV;QtAMjmrj=0U!iO|o-4QX8sXC}YY^N2kv?8HD(~OFL5y1a>dMYk9S0?0>vve6cxsA47cZ^mf-a!t82L0NX2n_7GiC zQo4K2I=fIK-_!*z;2$RoES`GkBdt)GP5v!QOWepq(ktAB-LY>~KPX4x!(J7$G;D44 zpxp-zfaAx*c-Uk6`7zcc-j1P|Hzz96Tc&;wmSl z2YSs$n|m{k{{#(I3}2W@xZQpFnzV73-FE?pQHSI)X_w$L=Cpu`_(^@DU^Rf7+t8VbGvwt?_ zpVb^U+vD20VLv?M+&E+aosAp&GA+c@=Y6GO3I&w@tS3{?s?>Qwvq~HqkcSlVe&=|z z0mn4*f#BD`^M5QKE`U}{3*>50{zBy9Yv75EVe#lm-wqPpd{Gz}!#dMwz@xu8`sM0y z_JSXI+7(FZV%p5ZyEO^b6G0axUvA`XS@%BUBxTpMsa!hkc4{GX#x{A%MUOc%E#D04L)3p&QbYE^8J^t# z>Lj)Kl(A@ZWA0CQOSH#Dsr)5sd{^~6{xCLBv0G~2sSt2?!yzp)@VeW2_%ObF{cw;` zOIIg$Y7x!8jv=VReW6eE6v!PNJt}JvFne&X(UcTagh@zASyJM(VFbIuC@v#C$I`Vq zIl%=#;Kt(q{+uM?qaraFe0lGaAN)JTN?hIwbHj!EjuUf(FCGLvnsiYovg>&yP53=UoQm&R=f9J}Nn3C(^PZy)E3EXZumvOSIYvyi@`_}65E_Y1>7zJYeufQ?b(4nu>`p*()Pdt=L zC3qSpp%sbvL639zpRYG>(FAX-Q3al-&YJ%)DpyQr&UbV@#CRtMydkX|53OOdKb_Er zP)Y1*y&k2zugQ>nNc8HAIvGifSF?TiIF6+CQIM~wqO6qK0F|pS_g$D8z5j%7V<&iu zn~;|buc77=bfG<{wl*au7G}&UmaNg#rJ3KH^$0`$cG?Bb7B`S8;lr28&)gtAO%rcM zTy__Kht~x@xAsU{Nbc`sybI#NPS7ic;xg+WQd4TSa9&foyN8XBPjq|hb{$_H2AR5}qrHLn zx$S4Nx4+Ze5qNHPxpFeiifos^T)DolJs^Pl@U8cbTox2N?LF(|h(I!}u>I2}s)2nN zd$0E4!w$PM{A4Xp!2MJ{{u(#yV(IT{qp0XN*)i->1n?7gCuDG=gviA~#-@xC0MoRu z6PW8(RKvrqQtSr3#~c5|`FulbRl{^A#vQ|&PbZr@CZw-3XwddvG5fPuAE#$$pPKQL zG#aVa)o$3Mg4JpHXNn92M`;ap1eC^gM^}ke@>P%cf_(i^Gpl5;1Id1o>l688@ZW_2 z=H(`qZeC%Pm6chk1qFiiF|IzIq2L3d$}H#NHE(UU$!)f^-8y%IkWRPwNlZeI!qxRg zu;T4Q@JV%!f{m!}J8%0*Ol|Q^kbN!t8pnB0{Ialzj$vzuRQsnUMC?&~aJG699j=P# zFr0=rheTjba6dmXTi8myPxqJmLfc%Ph43ev1gaeqXU((D+l_24g!rG}H(yv?cwhGg z##}FDUS^&Y2JWYFU3Pi1>W+%dGzEOEPQ0u}?O{*%bw;7WYNSADZEi9RiweezDq|8? zp^TULZ_J6Y16Lj1(zP56+a;PA6&1BMh{4qMjUQu#q|fVTXvk)xOLr${FZ-KL@#kjG zaL}6v2$PGeQVa2oB9Vzj^YB_HYK2~|2jry1X(>{}^MAy}8H-k)IMQtIiz0uZQmIyB zc)Y%$Pe4q(h4V7%BlM$?VCUQfF+%}c$)$r#%2*Fjkq&|ppwU!kzwqeJ_!t@3DQ?v9nbc=VRb!EYN0_uWQtF^~bV;^3jgAEGMBuve znkw*OKNFLAwSRqg?Y{ig2@9Czk~OE!760X^YiQ$l35gAb?QJQK)j!g~$v=InTFiem zzug)h$9hQv-09}6`((!^p>;o4zYUNrHEAN{kf|X=nMCM+OHrx+=XxqlX*Y5@K7Po0 zIZs;}RP|Nn%n9-$Er7q7iN6jXm}v}*Tk~YFy}aBX8j!f|GYi{g;6K+o+F1EDOG8@< zwM_=>mIlehdnLsSVgC$lzGM{q4EGg$ASS2aB1mFPaHqy7%laECjSf3nxkd$kbXg07 zn{f+nhTL-l3}ZSs0XpSkd;WX0*ac2t4aVtOa{Y9Rl~^y{ib;gw;uLyXeGp6Wzy+aw zby*FR6jz~(x2%ov2k6Y@GOeUiEydHvxi2c|Uk#=BDYj}%i@%EaYrn-+!tlL-L4X9| zbGgnv)8HF9x_!JnqH%C0%k#s=@gff*>umXg?h!NA*pS#9uJ;sPLw(g4xFYap=Xh8C zvh+Io@OzBV$@EQayxfpieKy*+#pU2|F?NgNMb*B9+RM<6YAy=yrTPIUNGBlQbnWwy z9e@K#+dH|eALvpxc#591?3}>?^nNlxQ2iC>#S<&rwjZ8>SmpS(K%)456|d8ai4ZV` zVWd^k&v_O08fD~d@W)!FMWoMz_h&BxEiOL!iDV8xwBYM~b`XYSWO*JWA|vdioH5Vp z_$&JWBbRAiZO@}DEJ@4^<_6`Y^7IKG;Fz{JX0$r00K z1=2cP&4aeyvzyc5ARx(|(++CZ!jFr2_J-lo1I)_J{A`a@7UTXG;1I2W73tqS77u(k z?jN@1=!}@M@yJe0PBH4Tb7Oq);hjD+anA+4#7>B?_R z4FmYhvXXlZq0i97!IhDQ>8B$AnM9Xm^>H)99P9PjK>4^MR{q-M8o$kEUlQqm%o2E= z6v$-!RajxFryBT2@_9nNl`B`i%n4#v+SCv3W-J}^EP>iL%|MEflT^H+-JRU?9d4O$ zLVz{<$Er$_(TGpF1KnPeNKh*1AH%DV#w|o%1_DRh)yY6B>_WXaR=e5TbhOeT?Rwe_ z`ihc;|1(qZlB1u}><1ej{STwRIcdEy32@?uG9&XwQ%jEk`gQ7OF9)dTVjmMDR*4a4 z+u)z_){{`+gA+U1{7Ga`Vj^YO@p-@v=BwSp^?IH*ZR$Epx(!~~JbJIWxziwd4UrF_ zIo_UFO;_StIBjPp7}nXfeK^O_w5walZQ_~48Ja&o?c1}z4=mAn2&qe}6ERH^u2H4- zbv8A6H-GoQ+U(Qo_fk*bxFD7aRW=U?G3-rm-bCClHN~VRsru~qX*3{rTN?-C2BuF4 z#Mvn+T`BEilGtCLjDSv8o66|wLI?*xLgcA!>Jgxib5S3->)?NJK?!=>Upj;ZO3Jl( z&o*^f3oG89<&@HVpLc&eYC3#jVO{{}W^>ze;k^2E?C?ge8*9x2NFj-I?}Um6@RN=c zRBhzWQg7}hCJbDkoFp^rvLm2IH83iPa|Wk*897%Qv#+v}a-IHiabc5mbaZJ~(`J^H zrsYfPG37-yt%k%PoHLr~rrov0O#dWOz;08BZ#xDN*%-c!44Od3x=NH&;)VznURbtby;s#X5f3Y=ISRV)#RXCGgBsZR) zds|+#eKB1UlJ{t2yv537+szoo{+i=$#KV5#sdOr00rms0`NhEQNvKJ{$07y`@B7u1 z2+!UE@=77Eo-5C=@u{7Gn*J;Ix~Hsveuj)vg<(<_+<)DD<7jQyX2=+FF|avFhvWj{^+?_4A^s_|NNK5 z(?^&Rcy#+v#J%e2n$xIO3_)41c0X|OdsZf0+o1B)Ux+0pxl6Bs9}Ry|p{MXiT$@Go z&#Uy2_dOx#+kd^}Z~zo|)OLsR)K+etErrio>>f5$ip;iG4l$nHlX3szNv+VMtWGGc z)ejo;%84JLTRLtrUVAkc%o*Hz3f`Lnd1W!W$uWM~-^F;h2|MY|BeU125SG%4fr983 z>)@Ui#R!CRlw$MshrVgBK$a-=Rs-_gI-S|fOfr-u??sc|DorpoSc@{@m4oB~O0JBS z;Vyk`NK$H#;Zo`A&9+KB8`If~?7G9jmgVGD~;P$FRw}2Q(PT*oZnY z+q?uK2YV`p^y~CsP0;-c^c5N!^qwVN`n~W1Y_AV#f+tOQUESR+ZMuW7b2PXv1GC)PKvD1bS_2@>Zih8AeLCJDSMly7N1t)DIbcZnH7T#RR4-6`kR z+FC4kgkujIH?RxfLr!RcDjnc~Nw1+slHGe#c)h?PNvlU1&<$H_>lS4=Z+`hTUfy>g zGOM`1f1tGae7oLYuQ<@0jR)%NV5|^Z-9A2Q>oULTP<23ju-ikJI0>AbT^$>i8Vn}a zET%^pE3Mh1-j5J-Z>}~n>%%;S@*ln)swGlzLEMS!^>BB76%e?g4VdR_ zg^~CDhp+gtn*9pT=y7eSfQN6vThI|I#$%$u1 z*df#ty#j=`ma$t*e}1D@6?0s4Tg~qtXsY?LQS3pq65maR_Pw&2X}(sgF=QNb8hg^( z9?wd}mt;y&<(KMGo9DtKmqZ9yzLuKJyDT2GjR`Y7-_gO7?ClG^6=J~O-5iAN`}mz!(bF!0Y#2G2%(-egJdl#xNhkNw{G zM9>P52+_ZbTzyy9_bJh1enDXfC3O)tOz`>yXQqmQZB8i3^W#nRmFdTIr-XcO+i^&W zz&VUpsAE9G=VhNLzwS|j223{fuAHa3m|{i)&+c9vx?IKjkmKbc@=nGFnpAC13hXqb z9;*FV8w_*z3$i6G6y>j?{Z~Z^SNluJBXmhp4NJ>N(>5sl_*+dH&^`awx?45%Bkca$ zR9a7bOr0ihg8uzX>F47sykCsBJ{&wrL4tma#DuSSpcy08sM``>Wo2a_()s-Kt(Ch1 zZ#89FXZHOAqJg4YPG8zmPwS^j&DOhX_H5^-Yl+zI_xyc}4-^@U6&au}q}Ins{m+!L zI&uu?50rR*)v)Ul>C6uAr%GH7lHfPN+ai#PGagS$1nv@Uq zw*ymkjyt^G!exO(lTag)#Ly$3cm2P$3}T853bCEYazpbvlb<2S%gI_chV?Zk6Cd;) zRqYE{vTMGo!=6&}d}TNNS#u&lLh1_E_*w(JC=Zz2W*-;C z2krVKjR0!QS55{vKIaOsidrwVEY|zvw^#yw3n5p#rPoY>m#MSCUeSq(PcANOW~C+f ztGPbx(!0P<{Qdoxg!59E5*-*A6$`%&nhOW6v>w6gLa;eEIEWB1|MWdsJiND)YZne^ z07~930wm>{oQxE^Gwwm3G^6spCf{$Gl#(IU;U)QLr<+Jn2(8P9>WoUCLlb@cM<7FvC2UelRU` zs)^*WH|aA9pk6t@ z|JgyJ$(2j|_VuW6|2KN_q+Hn(?67L)6wmU|=c|9)iNqbZ?Z^UMfHa(wejCq#fJPHO znqJJGlC3*$Hgtxz9gP)eZOnQH3?2kkvbro+MJ)+46)ZGMHfQJv)ys^;xTjJ+FT0ba zVSemoNd4z!uE=*k{(CQo<5M(m&nFZX{(T0dsDI#KDgO8rk}7tm5~A-hT;g;>hjh=n zkIuc1a=VMKIfCQW;0^%d8)7A;C9VP7q~Csgncb-zfiCRQeI2}g0s2xRVJ&hpSnLNd z+bb5FMMK9b6M76gL^Dh!ToD~u3ZrgY(%;EO!mgxiztbbuh&5jCxPu(}k5pwfqE>26a0#_qu z73Cw~qZHi)Q&5knoU|-WtV)}^bmCQ>BzU3Yn--iPJM#VqcLCchN`Nh}m-U0E^>&Dw z1ALmu9Ut>u2UvuPyr7RhW2qqTKDQ154m!P{VEc}~aUZ*>zYx@>-dDd_nz@A+rBqnz zr`V%w(4Ah$j5Ze`|FXB?);Sm-O zt%mU>&D5D$mV0VnImULl$9DKft~tIte8g0ty7$CM#tuIWT9r;EgYws)JMfc8i*}Vr znR69xZLy9vz{mGFwWJ`O;WWu_vrS`aed6qSuW~{DwHScw94wbIK!Ot>hFaJeS)Igd zrpK$h>3DnI5|8kWdWxayg(6f1d8#dvsRtdB=3eOIu>A|kSsQuDhsD1{(V*T&N;mv%Dl>vL?1OtyH zYoEn=C!fXjM;MC`H;z-qcDxb$SbMQ>R=SR$7sOAFr*ReKix>ovhiLvra&Vf7K^08V z<65Lbo=e`;1P;SeZ)r-p^D6YlR7-at4hj8T<`A#aqGF%vbtU;h#g1m&$UN?fp1ue)jK@mvy>5_D?3r?nL7SFlly5 z(2@RMbiH*{lx^ELI&=#%bc1w*bT`t?(2bNdD5!KOAkrb-&CuOlBHai>cS=a_i~D|_ z_j~uZ*ItV?YYl7ogSpNlesLV9D<{R88BcdtuWBVlqvTIU~-8ad#;@-Kme+Aa(2 zfaAimNtXKsXkDG-I(#1#F{?)Xj_E;8?j63+cP#=6;v=sJlc4a76B;&?G&Zc%IDeJ-xQef{}#845u_L~C0qEn+deyq6?Y9Fv<8 zHo*eTD|9Clzg*({8NR#3+!dxOfPs*xqzgQYn4d*L3zzo(k{wZ*K2HRcVh?!9&a)0b zZQQ>xSTf`t6t=Usi9D8U%KS4fnNf*w_>%+R_fi}M0 z&dlZL=T}jz2Tq2p_3JTl{52#JltwzPtoyb{{}Q@$y+G-C0tUx17^{z63vCX-b~v%;#{`@5^O6-*(-* zV9H9M$DTM(rGbq7gNIr+B|{gj!*FTC2ySBnHEyD_y@?W=ix^m@ip52N1%z00jVW^m ze%qxV85; zeZ1d!4^K5u613+L9+nFFdPReZGlCFYkHhl4V0E_M|4O0HMQ6{>pFeXVbU1p6;B@Z~ zp&uxVn%^D;^!{c~suo**Y4Su=q)8(UApddwhs;#Gd3Y%hKB2-$da(t?#9lDyzD@s_ zs&_Ffo&;H{_fJnJbirUj0@mEJ2C+eiXIIlQtt>iGJ`uZgI?M0ITzv`o_U<&prLU#7|bl{B!#( zpYXuEJT6?-)?Dp9KnPb^d}wBaO%;y05$8z?!GX4ftjE766^PHDZqkF|JAZ0P8yuf3 z`xpep0=?Q@4`a8aKcI*oDuETtVE=2SQp}rPo0^j^1 zRv%jf3rQ#*>-`%IsEDz?NFEwHVQ_Nll_%=4>++cZf5U#H`I(RdqEwbh(eNuYv1BcL6`~XAK6PX`PU+tSIwUp})BS#s%UeSce(+Ku%i&iFR<8#Ogy3v32p5 zDQk2aCYmmeBDu}Idf$2b+@SfcqKp-TXDQ|~bv(8V4wZk{;IrA6p$4{05>_CCzGW(d zg%mt>ZgOOnU_Wru$0z?TotMI~mQO3(*n?2hEV-6aE0Db&Mn@1+M|a*G#$C)D=H;|} zTwj7;X?t=x`pUS*1t>eXuZPtp70PJw4A)vP$IRiglwg)#ZPd2m(?ftI?B#$3N>bBc8KfKIz^^_|t1g$cNDfkyER)=4KlY}nT zj&At=p$I$KAqamz36&vx*@6Wcnn@(cWdx0UdMxZ%=5-J|C!y5vN4GA`M7Hppiaqfpevk&X!f66?R)A`xyHx zaDv@^tZcd7KR7z?Pk?YoxgE?&YrmvBYqiIAe4w|jit_3%`5Q73_8k${9T7HiQAx@I zG<;j_kkbF5%YC}ZqxP1lVvCG4aR>n3DH%HWw!Fw#BsXPlp80!-RL$XJZH#ViRNE;HCe(5X= zi2RiHI)H^p+bIGr9jBjJN{=BCg}lZv1oCqIWWT_xO#N+2$b$}3IcPG%xB=^%n& z=5GR|jSfK;f-ZAI@vT2b{Czv3G&=f$17+sSeOF3uo5=x^=k?dkV<1h$xB;hg@iKtA0N6^y9?8nLykDBv~; zD0E$@3jHESc4*R_Kdx!kv$Rv_W z^_pf)27TOuO^_Oh9AqL5J|zO zv!zQqjlFGGZ&FcZQn5iA`#rm)#Hw0C6&K)ar`srQ+bBM^QS3Lru+!05xM-F$UOhn^ z-av1tVe2Fcx?B|6O|A1>o=a76S&v)*j2eb1GbAi?zGdK!-j8inj`lV6-yiL*u{-0s zX#K1v6RKarJ6WcS4lamUpB@<##F>}0bsfy$hdziQ2T_DRP{=)q-99V|T|AiR6^I|s zbsg`0SH_ol5qN~15i#BWut823E?zJLlBj~#14V*`{%cZZoQ_VsOa;vo-SPUHR1kh@su18-&6o8+<^zRod1o!b|7Cd7gi|#b)1_wG zZY2FCe0%Fj#g8=?=9pi1CC7D0 zyxiST%VwrzBQ9^asfV#RW8Zhq+izBv=Daf>7Jcdzb~9=%@}XHxyr<>B_rXz0RShU6 z4QnwG4auE*KZB<64;Y52hQ)fE_F#0~hfXV@#M62RZ?W!fjDmBON(Vc)b!UdGx(Z4H zO8Su#3^ml@ux+a)FlGx_t74U)4VVvIAW?o|KoQos{ztS19cDmSC z#C-8vA61N8Vh}7b$Z~pkEiK%35Qci-Qqgnyw#Iz!;8IA$k}nl&HuZ1L+ctwPB1=!$ zGHo&7u!bR?yQ&K8brB zzl0*23D}W`LGNqkBL*&QWHspD!8{Z8gYDEO6l86SRh>TZc3O$3R?gUyXOREoM*Y+^ z8Z;H@W%qWfRIjtQ_V;Q%D3N{AHpYkY>3)yl1_>0j%>l|8wIc#rhZ|c#zKXK=*I?auk|@k!FJbX;1;m_Ira z4%obI$y|CBi6xi{D_>>YfV2i9)qAe6OeJske;PyjkI?`(qTpvwjni+EAA9;oXN z(?da6zBAH>w(dpFuQr>0g!XzwmmpbxWda)VpA7E02;bK^itp~?=gol1uCUPov`ET*d4k_<00kb zDKMvST>63l_dy;FCC=3%QTG5}ZiewnKjye~-Jjl)f<=b5Fut84#O@R361>>wTaN_A zFP5%B8oo^)`XoU-PqC+FU%1rV;3d!wW5%Ns?+hpJ8kJt1cVHdvfL3oYflERNBevXl zi7;>V3n|7ae*wW{?KK!dQo&~9B@wzO5Gwe$AQmtMftKa=g7xF z=Y)x*YBls6QqXq8)p6&ZSA;uWwF2kqUQ}8TV+ObSwSalQE9ai(b}R&Y%ioWjNW))j zf>n^R&`VJc^Zb`m%#02vr4MbdvRVJ?U}JL?St?}aU}`QH@7Mbm+$(U5z1BU(#xV8{ zH22Fgohv#(e`uDffR@*+lydDZ=#T_8ON|*{f8-4nJ=kUFi*;<7RDa85aXrTF@OF0B zoRwGYFmqt!$TZiT=`fSfS$L3pLDC3Ic0a|$Bo>; z3sqi5e)VKVej0dC@rahI5j|Tl-?%3o!@E|hytOS4Ycj_rd*CW8DXZG*^5sN52a6oK zoONX4`{c}Xq%_tE3Hqzkmw*;{W-I_2k2YN*1}*ZUzMvcZDnPjSj7yRLKMjmebpZ1M zId?0;LFT+29E}D7H3EC)+r9s6%g*{Elyup0)2f*pc80Q&U)GlPV2Xb}ozKqX>E70}=fP;d93^8Xk}h{-NRm9j=D{-d^I&3WfWLEZUuN zUV-;h4K=HzTM&sX7$ipDUo_rS_kGyT9-Wfz1z?9<9u`yf?NpVloM{nsik#tr@DV@O07ZhTS78jiQwk zcBc#(R{OCJ>9a!;-`^3kj!Rei!FB8aAj3vyT7k|sK-dT?YuA2h2BZPepo$RxIRo;a z^fO9Umg_}bY}hN+SvJ5Q)y1c^y!!r4i>Rri#DYX^?x?gv+%%K+wHF$j72bx=rzQBr zk*(W5JDHosT{|Fx@3D`zS;=xqpFR_vH6(tpX9Hp@ysS?B@DDXW; zB(1?5AM>LsY`6?;xQ7vI(9ZlVtUK60KzO6GoafY9I!yw^{O--WlN#$~HO6HTT1(j< z1`D&=2t^LUZ=j_@_rKknq5J4<=tUX}wZq1cm8O$y=sOCE8ZxUV>Y~o>jvUh;q3v&% z(ss2#89iO@T-V=#{Bmrxe^v3h_@<}Q50sKVa@ge#^P1ArmVeBc_IytwA6rMh19g{&}} z&)MJw;cv=s4yrRME1&XE7w*J3{8bS2z}?b#DWyXG75y+MR!1k~prraczvU5`n>PN( z7iC5iI>t-$JNo(zSYb=@@z;9)UNvFz94nFi+H`j z_Z)BQqms9ptqamNSrw5UPzeM&}Z!VmI1+{)*`C{4O~-}O}bFLoLM?7YL_`y`%_wF&lT ze3R>4jr&##Z8QjV%oX1MN1~L_0%I0T1xxO6k^?R+!-QY(gxLxF2aG6KDAv={J^J_A zK0wrCiv_A#=tW8H-~jW7^6hW|RwZhEML`VjM4@x$D5JCu4bCw2pRXVgq&Sp@D}`Pt zSRe!}*w9V_lGuaUKk=1~hsK7ms)cFYNnD znD8DP2kGf^&%wPX4hv;z(ncR(&ers_pniB+Ed1f@5$RIs#ESeLRvf@tP<6fOq~y0O;_U(glV0~@JfV3h&GKaL#*A$q5*lJo0V~0 zXw!J)5q+x-_xIuk{16Mkc5v^q^2n+(Gm`@HSiX45sTBk0o41E4d?PxdigPuh)7p2{ zwlNldNp((F7swL)&$rUwO7nSBBQrh}gBM`RPGIJ&<*;`yC5GOc>zXEPt)$*Or^nSK zu=cBi)vbUCJ`o@tol3KF!OokO`<5!%OW|U>W_(T}Juf>Pz92<651&L{3KgR|!t9GO z$lx3lX>0sX=FQhm`idjGaG|5NLO<=Y1o;ld7}DT+nr8nb0==wjebDl(OL)L&+utsQqsr8w zyNbH->|b|kSOi)-W@5~jbb(ltkHE z>e}|~)lhl%UuTRSo5E+fuEnp^ygzKm0HWo=ta#YGIeg(}r>tY2GUip+P5eqIuP`f4 zqci_4EIexUzO(PzFG;;BWxfL|g3hR z{RI@z8Bj4NDho*o&u9a=kb)WJ=@;?VKfwH>AUnvYu_M)2s+)F@Cvp~VQnXRMAx>l@ zi>W4W!q&sz*R&c7c7A@I0_P&9@5+$z;xW5OYO876V|Xkp?(cS-e<5$HFVL-+G|yBhsJqUZt6Q?Pi7{n z8jr02qEoVUl^==R$mu56)>0lUuCLs-%61wV5uGWT%nfImkri`5rB|O_s`G0qBL5mC zbztZHgSQA=K03}t(iDCi|BM1-Te$nTeVJlTYG+l5=3%faY>X4E@oRCnaau=?LDNst zM3#ZR6x>#UhA(l&H0z7uDT|6~A8(PYx2gOT6YG^us~pJKB$hCT&k-L(t1Pi{>DIj7 z<$}=VqQanmH67JQn8~KE{9tnYw zS87uV-Bl3}N~h2(wJY99-`CCdzPkChf9{bCnc2GZS!lgcjl5C|8ynnm!Vg}ZaR;;uVYs-gY!>Y5+m8=2iXNg15}ax5K5qZkr;L9*6I4{ z6L#cACGam z-+H|>*PEq!k=SMwCJW)&^^~xfg|cgh@=Ho4JS9o4Uo4_3k4TTaCTt#G(f3j{ z1idGBLm%a3OnIC253WC4B(-1Ko#Ow^e0$M2cEr;%dClr{rTV``BBZh?9Vo6RH!PW8tQS*04Zudh84&y z5}}~(iSJ2cH)YR@4&6gkclvd~tI2@I9x>oa$F&kaDVBerxTggtFE!#ebj3tZ)nq)8 zQ27ZDqAm-Ft_s=3MnLAM^^Crwga#1J;_%R4sGrFg-k8hsY1w zBCADFz<;&&<%21C0JV4%yhMS+9F+ift@^HecKq>5cU%TYT`EpWxoefAEnub6illcH z@Fs$&f4;4m@ene$#hxiZDGI-lFRzkKesM?ryYP~G2V^xu_h+rLE~Cu7Tvl6@(VR0lt;|hFRjbSb2eg?fpJyNY%=eq#H?oMS5@HT;#N=v zxKCixSpX;3JRY!vYh3KeDkLLnXr>2W#aY?p3DjBzCnh9EyFL)h0>lJ;goDukE42C& zle8sEI@B2ypN96r2{~rMXrdSAa-g&r*^SO7#S2c-QM3{eBZj=?TgwV^ZFsYTl3^^WpfQt$> zdCX=6*hdt?IK!%5)H@RMJau&AB@qpXeCo6y= zF{(#=xam?0GgQIZJcXyY5ZXCp_)Y{U2`8^nYac@0L2lemN3fearHxd9nDe=Amf{gd z07U-UV;T6AYD(jbQBmK#8m6@>d1Cyhz_ah)c)H|&6{(Dv0~cQVl3(4)iOk3~LeA}F zt*TXE94P|(&s^&2@TKW4{IxOtuSS9Xa?!4jG0F|*Owc9OQWg~kxB355fOANs(`kjc zat8BMagC|h0~7aYCv-O1U9u%};$&#E!Nnb@`9f%om~vA((_wikr8A6;1z?G$9|z2` z_{Kvt?#2q=_~|AJtD7}%wL9|#q#z4oeTcLT43RulpD@)lo}-D3P*(GnbFNMk~_ z!HGh84_*G^t;&x|Ajj;%u=q2A@8niD=w6Z$fSc=K5on^?V_usM)=bBolzkzKO8na| z#IZ(w_hpR5v5`}bgl6MuM;b-S7Llzf(9N9;S|kt*GVu*3{f=P}MH(Pm1?~sxlCq4T zL+*x!3%(r^%d5XAliCdZdx)`%M0qZ%keoM<6;j48;R8wXiRcM_K$LOR6xTgtIni?R zgkJXimVvaYOF^V0LRo|~Jgt+*G3Pw0k2X{CR1{w&1C7uHbN*A_=_r#YUtNIc2QBbQQ0J$|rkJ!7WpxyJh8_D~03_2cc=EDwnmzrn?s_K_^SR+)&T4*XM6o^FJ z+3MI2U)FG`FSqH-nvK)Y>jV~+6L}~&NzAp(HH*GYY(7E#7zRL|Q^(o&}f{RJy zRmv8RIA}iCrI|^%qmckOlUScfemZGlf!2fKZ8lSMpzURW#vPk_7ReA!A7+kO83F}G z=hq{z?~LR=AlptJQWhS)d-6v%PM8;`y*+sQlqFsoeniQ1p?fv9rgmBiToeM*p(TwP z$cv^gAf;roJKfVM3c!)N-MBaC1ZHivn(UDG5JB+{F`r@q!9vN;V@*&E^!KsfQR8*` zefE|!8%K&S&6KLDDztA(6g{Tv1D|BH`-+c=aEd^d2(FEhkT>YI&BV>QZ42FBCK39b z+$)zU%uxE5_s_ni(=1?!AYVjS;2iBA5(YL%_-AlthD`;Ax0h=Mj{+hgPy7ihnX*Y9 zOtyX^rWY^DdLDrryMFBf(4<+*HoL8=SDD$C`~z1kw^3{;Gb}?P_zQJ`d1_6h>5Ng2 zuRV6fn(B{ic6p2)d7IwPU2(u#8vG(RhI5AMTI&4#rxI%YN}vD@g?^N2-Rge#!R1x_n|uFxMU4q64etnw0w_il8k!-P%hNiz8z_aNuQ4*e9LzZRKSx7r2T_P zE;++tTQkb|l-HYBH3rG_MoLTY&YRhYVKzx0TVUAm7qx9`L^^Fj+Xpvz{gL9PFJ9)d z0q{9Hqq$eIALwXZNu!LGL*!G%6uplPc2t00P)E(n97*W$EGHM*j;(v6qszRqhIb4P zvNmIUDV=VF#y*P3hS6sMc)2V{1N4`R8na#yjR>OJZk8L(O3Y3W6i}7hYYI?9I;mZt zae53QQ~ov{-L5^Zi4hJYSV9_zv30B-bo!`RZgeh2yR@i!SuCpzaa6ZN1D@%$WU^1$ zwMHgw0J1{&jGYM6G>`OAnF`~|d-%GPavMMEhikOYoD1%AY2-`V(?WB?$hoX5gWAS$ zKQI)sD3J0b?>QVbuRj$2ejI9_op~fy>_*%)uF!@k+lD#_DV)vw)wcK3Dcq$%!9~w` zNtVIx&$o}m`-&E5znrx^=`^5-M0uv5B4#rYVx&wV)spBv{3Nqis5+1uxzK(U?TuF6a-}9WLN6UQ{le(x=kI zEax@Y5=e2$oW(y8Vr1`iS1l1op_-Q0$^H=J2olr}Pa6Oz42D=S+J-RUN0Kc^n9ly7 z^Ttp9^TFQiQIAdF-D`@bzXd|hYikBYjW4hW``_fs>}#6c!q742kRj%cu_EW6?{MzT zw>0GhIDabRD8P@%Pz_G@T4^wZDkzTUn{`O_sE;_tgif+(@ma7UxvsH$w zu0qyWfgt7K)jg7C038oS0*P=h29Te;Ge& zAAGtsa(4N#Em5V!@unm6l{;Hd-pr#@Gm6|1(KTVl#&e4l9+N!ll0u^O9s7ZNSd*Co0$5OB^LWYWcu^hlfzWris&eBNy+uM8G*Y}#1? zK}3BLERS$vLDAIA3`1gD3Hi0;#dGSEN`>nA!-E?9XFZbG;wK`yOprAd8dkn_V>Tk9 zryHFBqUwlLRdR)fZ7!qK8*n275KqZe=J4Dis8eY`F z*n!N;+H5ri<0{=Z=P;p|+X$}`uu=*TO%$FqU!`>%0Eo$y%OVSu*y*svIjghA=cA#y zSm(%%fE|F8T6)IMb*045^^YF|D7@t5JH5`)6;LsqKsOf#U=dni{_rKQgKzUmUX6g?NmTIsbmGm2`$z8jugwXS^#6^V)=B=HL|3C9a>z_54JMp{-O# z3}nTkG(wvlh7EInYtmZWoJ>Y^uI=F_Qi0^D(KVLM7M0==%IbK;PU#fI)ppCoG(lJWB5+te9_X5Iy0Bg;dD2|&WVgEi_%Be2ot#VY!2btf132Z+<14|B8o2b5 z@kW{ZIRNzcGJPGG821eQQ&ioq)Z&S<$2D|6RPh3)+X+1B z^lh)AP6=znXiFXtYTI?40QUQUc5VKm%;J3CXJ7Y-vdBxyFSM4@)le&wTS5e|ll+O} zx`Fq9^XdDu#CMN`6OOh{P;3yR&y<~f0~93hgim)P+T$l>04t#<2%NJ zH55ibq7oKu!uewBMt?yOkAOi zc`trF!OqA9%=*JM(}&m@V0geNvy||l7cd~v?BV;~ zpY`Ae6E!X{fb0g>=*&|;9n*pnHO$BF?*&RqnZ?8bsZ@tw2% z8r^!imle{l(?OMjcRjh~P$zSE2EaAbhxp^zNZ0q>rlemDrI1{7X9E|+LRoqcL!Z#^5TZt?*OO}j=$isg$TatX*5JP46>lQO1m(ey zl;7EV708FzO^F8%zmGBx9cT&jpLm3lCLg(~+^^Jj_uCofgqFlbmMRw0GN#Z#r1TqM zBg=@c%5N?MN!6sPOD+1_cd66!==OkH@cwFk)i%{wDL7BFB;^XzMjhsFl?g3~fhV;* z?V)^xK|n;;hVYG4b3CJrjhIcf1)q4Dfkv^905rAThvs$~swu^B;@DDvWsl}RYL%Xr zKFZhsjtIe$DjgN!`jn+t-vk}X9<2F~oZZK7u&uJ6 z2b?va^$Gcq_yzaF_Jl6KAjPxX-_&fsk>4vk;f)qW@DJg@M)YShQcy%GGHa{xE}|N( z7RO7`LBo_jOc-oVL*NGCq{*;4JeA4n%ryUps(A@t(Aa8xDj(D~<&m@hFU8v$=w+vE ztl(;F%p{}`L3TNv#}dLd6az4M=;kr%|C@ncZ+?7dI^{uQXkHTcH3;DK&^%*#*xJ;# zfHL`2peggQ__D4DK$^DEo%(sMrAWVVl5D;GZhwG2S|)q>6Ya zz;5fUh3hjMeHFKyxX-a#D8nAv919fj={mu-ihP$taXWegQ!s#i4dS3VFg#vGuJbEw z%$*1M0|yNt)r9{Yx}lrjL_0(Wk**_#CM%lTiM8c`=rHzZBCXvim*DYqABW*=b^xW| zV9#Ll9?AbYwTM2_jm`B3eb7t4kvjuvdGQO??wVwPB`T)Lo7`K?pR2C26%a-pC$ele zF;OMncAmy88F0rQks?!RR67U<>_$9heDq|9yMHRaywZNuo)$(0c5{6SB1sZDHx#}U zq+n|mJ{x>6@S+9;*NJZ7kyl{nKT4a98&x4IExSR$mh`o5BD;%aA7+Zjb)v2w^@&R1 zY^*?0UZ7G2&W0(w{91m)+I8dT=CT!HUKA!ULWj!e`b#|4`Y?SO-Hx9LdceH)1{G{P zkyFylT>=G2GohFuOHGC_D2}V7l4lNT%05#m%PhJAg%P0k0OI5NT^{DGJJY@l&d4#Y ztswN8a_enfMF^k()X^I&{EU##hvnG*!#%`|R~yBP^|^Yt7%LqV27?@5SnU0&~#I@A;A$Q(vL6XdVuS)v)`$m2D%thh&{Fbxfhrq1@ ziGaRSZ0F+-fV;D>#~nnEo}NH4zLFf@-51mf~dEpvW%{c4)T9W?AGY%-_kM^fVoyhFmgrzw&QwVZ-NnU!0hc z`eOq8Dmz&VAHdbM2t<|i7o@jnbi^1n*Xhrkhr^aDRk z{(wYv$)+FJP)(jLfL`S%Bdq-oBQ6Oa`U6oGPkmDgk^kGbxuUsr#=HcUi7;njw337H z-QcNknlp4<0m4yrn$Tf5=R94{ZWp)Q7y&}VDKtAKnwB)E)s%t(Tke)sv#J}w0%^|V zxhNdSFxJKgLm=fR&%ldZ0!roU(new)H3VD%1y2M-Lob=k;b1qC0#2# zTlnbjyS_XiO6boQxDJLUD7^-@R(-o#UtbqmwC!{GC~~dN3gowxXcaF)3YDM|sg*S? zf5eVRHMqp!av)A#MJ$osl&8PBao7mat1$dC`0zjGy}_FDW9Pi6?&di!=UjI{7Y!|i zL7hqL%4l#Rwelhx1KeJpzliAlPnft_b|QhLt?(Pr=6XrRzo&ey`7ce!YKf8{FU$W$ z_Qjc2x(pe-nkT+u#XfUiSt}lcXY4NeOTt=hAY5oZ1UNKXlt)j{CzU{x$H|hqYymr7 zOs!oV3DTSgJR`r&ElDw0)=#B5mdIH#N6p$Z|DxFcRkn4L)g2x~!f7SHf_26ZX3|jP|>s9VW>K4;m%%kMX@rr-L&Z zOk<%!s|MJD6d@l^N>FqmA+5Hlh~-cJWTN|TPaU6Kkir@GZBQWQM8k-u=URXUH2!fJ zl{Gw_^Tj>!Tbf}#VVMCsea+NrP7Odl?%$Kisv4#Hv3qMNd+uk~m=nh+ARDQP36A&) z*nSxVKmMa37pqF$DFEP%OyA-S9C601QI0sq4_4@&%!$@fvjx!<{v2h`Yas0z6xO^hscMEoR4P6HUoXHu!Cm2ActhSdC|>%TML;lZpM5M_ z7qV;cL$)C#92z-6TY5764cPfDR|!t$c?735?v+OhAvjo%@n^Iw zw%~IrzVGXQ6V)cKt4NwN@;Z9Jmo2{NTxmtXhoP#9YDXM@1G{H$ceAdcnNoSTW|^4r z4Aw0%1COtj5g7`T+zI-Vl410iwlw;^0zWJqbna}^7DKRj>XBo15wD+!g+k+IV|M>b zu37VEW-EDku4ZPtUv?afjl%(isG8)A&JcurQVicy_BP&n8Ok#0XV8JydFm)Gz2nwe zS|LOWoY+#pj#JEgx6`)-_)C$lux-b6!X6@O)5-v;*H4#szN7F;Cw1!2h&rw~W+gEA zTVz2&U-BL80n|e(k<^o9-)s%NC6wk zdH87}#O5`e**C6)ZT+`{eY|_T~Q%26JCIf%c3^mlx0 zz=&92lt?gQ2=FSx4Q%@9@)vsmiF^Y(T$1zw*tGUu5^0_w&|! z>s9sY(~yfJsFF&^uWSw0D2y7l0vm4G+XZRKiVHwqnv>T2hTZ z)&9${HSj%?ZE6!U^;aDG%;+9&zZyW=_%577k?t&B^5W!++NV-F^lnLZAeHM7?yRJV ziNdX{g}(-bb`qaWB);7-2Y*y^yTadF#7)?rDx5_CNj@fuGhXMciA5IBavDrs9?g)4 z?6e}8IapiV$b|H<4kcv5J zvgmy-0xf?w1gSCDnOS`_ctdE%D*Ay>g3kuU zrC7}O7O05`uB3Gf=P0Ob{AR|JlpbEcz@Vne$Zk7?#j6iA=bblweY^;GuNHzoyx;dR zL0{Qz1^Pj&T@0@pM^Jy(F}C{kR5{!=d9%Pp`-(I`s8x3rYo`ywuK-PCzKFGt2LP;= z0ABw4WyP!pHOJuJf@WQ-H(tF2%uwl`5U$C=J|SNCkrz-2*Ibw!xlxEYBe+w*n~8!0 z^EHzU?yJAAHE{a_I#`8b5;x7GH)#<00~1$Q%=oQ2Q=o5z3I9)(8_+~g-}Fcz_;H|U z(aPi+psWi~enl(%st+wJ)R>-E(#0DfW{m+E-XnJXKSC5hNNRqRh1gm0D#o}!Q)>UZ zRdz+6hi)g(tD7fpwKa(#iQ0ahY)c&kK(lXJ6eq)gyW?Ze00c{5-E>wCSTWUTCI3PC z5O~0`iL|NtIbdUm_uZGgK|mSFRoUCWB^jAX5p4T>*I%nH@t5;KpCsBJ&K*3 zowDpdUrdExgztQFr2bCd8t@@0EJ%*xm^wNQ!3=XBr4~yfG+poyI|p$tTYDH-fuhxT zoyrn$Pq_Y9mPhvr>~ER$21nI5hM+iRZcgTMajYa$_bX?Ch_`!Rss#Qo(i!3Zhjgyy z^lq^ud1r<`SwbHIc0(eA-w_J)?0$^}97QsQCdTIVKA?x^!POs;g2aZ5Q$gSdyj7@B z8C~KCLKn<3%t%~}8PMb>{wFq^wkuH;3_NmB7v~FWALC$}5aM*k(4M*`j0#1Vdvv;k@TqX7Ir&085iGxw$wL^83m8!TjJt zHTL1j1sI8=haayrpo+Gd@@jtU7Z*mywrbS}~ z2T&=2<7Cr`=-GyX621X_pV8_2Ps~9R9)cW6TsuLgBZcQL|K*mEdTLo=4`VoKTP*o=K9r}6rq&0D*o3QucgvMc~caiFDO zZlpiizU^B6w0TGwH!>2f&0n!21Hti{tR*?j40mXbU8jRD_F>ystR6q4T;s5f8j1W9ux%Uw42DkiD6M2Qa2vok@kYWQUA7sUMDCl1ak@8-mhnW0cUch1P_>X185peV*toc}`N32O-{7FMX*1UL{MF9U`BM*I-%3t}f z(K8CjYLdn9h!4e$j3qg7>L2lUh-LrE9cFzH;+2J0EXUWD@pHYE4*_N7b~^;`95lB9%-mYgm#fZVcW7MenH%}KEO z9!?^ZJE(R*GIuFrjVY&PS|=wH=!sn|$^iQ3SrcOmVz2>$cEy3G!gDxWH6r9CNWsp> z4W#oQYw4$qFyo)E7;NCw#sMA;sda)5$RGdsIP1r1DRC)UxpM7X@Bu&E5EZOj%09^Q zFEzwMWr%8ChIJEa_W5uCi922#tc@9j)%Q%41p7ay0(4>zR_PV<7+@$%)0UaAg35%X z>n8yI|Ft|JaF~>^)&GbAU<~`A}3OgYM8Qf>_Lw(T?p1lX5((fPQZBJbAf4n2j zes@XfMoRx*w@AS)Mw8XW0MLvUm5}tTMJpKUxFw+XieA^UN!%B!;zoyobb^gaTCN8u zdl3Dxk#8UUZiC#|{)`9ODfnGH3mX3qWp5o7_1eb$4lOB&ASnpSNQWRHAxenA03rowJrKVJ-dV=9(+M-_Ld5 zyuu>aczGv5PNABfJ-BOeanQ}Dq(dxbw+HFRXYjsJn!9njMjgxrDiRjw_jr@#IdFrvU7yK)+>Hk{+n&b1nAQgT)P>bB0n+Wx4x{*ipJeTNz0UR0&}^(p$WC`* z9rP6SSy$#F=S?}8C}l#V>^G&{H~2-=2`EJ1os&_Ss1bY^%WgIWDUczll|qz&vdCd= zEHWz(n^bCvpya4A+Rv64W~Tj925GW42bWX<4|nA&n}m`sBl91qX_LRJV7*N&>DtY% zLlJ!d76JI#QJOofSXA)*Wd@)xTW(q_02tIC0=i57T34+>9-t5W8xs-Pcy8JD{Qov% zD~cUTGyMvV2P3o3w`_r_^_j~JhikAcYkjO7O;~ouG zF8|#~`?%B#aqU?t>eX}n4d6yB&y&{cz>5t}l(4>fB*f_6U-KvaW3r$31txV&o?2Ob zh~6fZ^UTQs-T`^MN_cykb@*SYX0v4Q0~PuI2ww&7yXF&pKrlnr988Ra5<%H1*o%Jmw~pktqr$bZ$Ko z(-ObjWyj9z_w89OF<<+bb4uRX%EWgr!$f*hyErTyBOhn72 z2xVT~{hoEMoGWk0JsR^mYZ{<%enOBAdK!0!;dB58NdnQxxasq=arD3$gE0|uFl-u) zL(c;@{2hkEyDPma$3gd~aSD#y50=7rmT|nGAS9K@@G;(vqI;9-$l4*PuRwG2>}leY z(15FX2FR$%(kL>VAPhhpibFopsbE<(-JH4k3>pca_X(_B3l=ROzmj?~g4#VYI=_>9 zeTGmZ*UN_1N*pF7ojtsrP(}UEuL3LgD{{;w09UPM_%zgR;(Y$WFhAjn?f2KKFJ4Ms zwhMZ1Q3SV=%VnET+T5_>@?=G|FZPAD-9XH9z^Lu+i!+dO5B{SV+hhZZG5`N4#+#ep zx|#*^0I4dVDm4rEY^WYZkN;ag+50F&oR3V0+O(Oy9(EI~ouSs`Y*rCPkvku*9|-dwVBjM&KO1cA-oE$HEQ;jG^L4w%*M!q_Pe_?7 z&-+~vy+13?J}Bh%OZ?HmJnyIots*hOEQi8vdjjB1PALcvQd}@8_MLdisuP?0j=gD; z%hF}17iSqSc=$4>>c!pEgxk=E+1aZy?x?fkRxM#njG!Q*@Lhdf!^5r9VBjURmD`W2 zhA?fw3*h$bO&=Lv|Fp(|Umjn~UYzVi{D?TgP~`+J&7GJTO;WL_xjTx29#Sq23X~KU zjym$S#Rtp{pGVQkZ=au^_g`II)oc7h^i}yQf*%(T;3KEMHhrICZTbj`KPx-C=Vid4 zhhi=bXptVdguNp#KrEraqMztU0icQrh99Si)4M$tDldB822sp1MwCWF3(dwTZ@+CM